diff --git a/.github/actions/build_pyamplicol/action.yml b/.github/actions/build_pyamplicol/action.yml new file mode 100644 index 0000000000..9024253c78 --- /dev/null +++ b/.github/actions/build_pyamplicol/action.yml @@ -0,0 +1,38 @@ +name: Build pyamplicol +description: > + Build the pyamplicol install prefix once per commit with pyamplicol/install.py + and cache it (content-addressed on the pyamplicol package sources). Test jobs + restore the result with the install_pyamplicol action (they must `needs:` the + job that runs this action). Assumes the repository is already checked out. + +runs: + using: composite + steps: + # Must match the key computed by the install_pyamplicol action exactly. + - name: pyamplicol cache key + run: echo "PAC_PY=$(python3 -c 'import sys;print(f"{sys.version_info.major}{sys.version_info.minor}")')" >> $GITHUB_ENV + shell: bash + + - name: Cache pyamplicol install + id: cache-pyamplicol + uses: actions/cache@v5 + with: + path: pyamplicol/install + key: pyamplicol-${{ runner.os }}-py${{ env.PAC_PY }}-${{ hashFiles('pyamplicol/src/**', 'pyamplicol/rust/**', 'pyamplicol/build_backend/**', 'pyamplicol/Cargo.*', 'pyamplicol/pyproject.toml', 'pyamplicol/rust-toolchain.toml', 'pyamplicol/dependencies/*.toml') }} + + # ~1.5 GB: one entry per dependency set, pyamplicol's crates rebuild on top. + - name: Cache Cargo build tree + if: steps.cache-pyamplicol.outputs.cache-hit != 'true' + uses: actions/cache@v5 + with: + path: pyamplicol/build/cache + key: pyamplicol-cargo-${{ runner.os }}-${{ hashFiles('pyamplicol/Cargo.lock', 'pyamplicol/rust-toolchain.toml') }} + + - name: Prepared models + if: steps.cache-pyamplicol.outputs.cache-hit != 'true' + uses: ./.github/actions/pyamplicol_prepared_models + + - name: Build and install pyamplicol (only on cache miss) + if: steps.cache-pyamplicol.outputs.cache-hit != 'true' + run: python3 pyamplicol/install.py --source --yes + shell: bash diff --git a/.github/actions/install_pyamplicol/action.yml b/.github/actions/install_pyamplicol/action.yml new file mode 100644 index 0000000000..feb0a5e0a0 --- /dev/null +++ b/.github/actions/install_pyamplicol/action.yml @@ -0,0 +1,26 @@ +name: Install pyamplicol +description: > + Make the pyamplicol install (built once per commit by a build_pyamplicol job) + available to the tests: restores the cached pyamplicol/install directory and + puts it on PYTHONPATH. It does NOT build; the build_pyamplicol job must run + first (use `needs:`). + +runs: + using: composite + steps: + # Must match the key computed by the build_pyamplicol action exactly. + - name: pyamplicol cache key + run: echo "PAC_PY=$(python3 -c 'import sys;print(f"{sys.version_info.major}{sys.version_info.minor}")')" >> $GITHUB_ENV + shell: bash + + - name: Restore pyamplicol install + uses: actions/cache/restore@v5 + with: + path: pyamplicol/install + key: pyamplicol-${{ runner.os }}-py${{ env.PAC_PY }}-${{ hashFiles('pyamplicol/src/**', 'pyamplicol/rust/**', 'pyamplicol/build_backend/**', 'pyamplicol/Cargo.*', 'pyamplicol/pyproject.toml', 'pyamplicol/rust-toolchain.toml', 'pyamplicol/dependencies/*.toml') }} + fail-on-cache-miss: true + + # Prepended: in the MadGraph root, pyamplicol/ is the source directory. + - name: Put pyamplicol on PYTHONPATH + run: echo "PYTHONPATH=$GITHUB_WORKSPACE/pyamplicol/install:$PYTHONPATH" >> "$GITHUB_ENV" + shell: bash diff --git a/.github/actions/pyamplicol_prepared_models/action.yml b/.github/actions/pyamplicol_prepared_models/action.yml new file mode 100644 index 0000000000..0075f42f4a --- /dev/null +++ b/.github/actions/pyamplicol_prepared_models/action.yml @@ -0,0 +1,39 @@ +name: pyAmpliCol prepared models +description: > + Put the prepared-model packs into the pyamplicol source tree. They are not + tracked in Git: the archive of the matching (else latest) MadGraph release is + downloaded and validated against this source tree; when it is stale, the packs + are regenerated on this runner (bootstrap wheel + produce, about ten minutes). + Either way the archive ends up at pyamplicol/.artifacts/prepared-models/ for + jobs that publish or reuse it. Needs Python 3.11+ and, for a regeneration, + the pinned Rust toolchain and maturin. + +outputs: + regenerated: + description: "'true' when the released packs were stale and rebuilt here." + value: ${{ steps.packs.outputs.regenerated }} + +runs: + using: composite + steps: + - name: Fetch or regenerate the prepared models + id: packs + shell: bash + working-directory: pyamplicol + run: | + set -euo pipefail + archive_dir=.artifacts/prepared-models + tool=tools/release/prepared_models_archive.py + check='import sys; from pathlib import Path; sys.path.insert(0, "build_backend"); from prepared_models import stage_packaged_prepared_models as s; s(Path("."), "release")' + if python "$tool" fetch --version "$(sed -n 's/^version *= *//p' ../VERSION)" \ + --output "$archive_dir" && python -I -c "$check"; then + echo "regenerated=false" >> "$GITHUB_OUTPUT" + else + echo "::notice::The released prepared models are stale for this source tree; regenerating them." + rustup toolchain install "$(sed -n 's/^channel *= *"\(.*\)"/\1/p' rust-toolchain.toml)" --profile minimal + python -m pip install "maturin==1.15.0" "packaging==26.3" + PYAMPLICOL_BUILD_MODE=release python "$tool" regenerate --work .artifacts/prepared-models-work + rm -rf "$archive_dir" + python "$tool" pack --output "$archive_dir" + echo "regenerated=true" >> "$GITHUB_OUTPUT" + fi diff --git a/.github/scripts/import_pyamplicol.sh b/.github/scripts/import_pyamplicol.sh new file mode 100755 index 0000000000..d46e49d42f --- /dev/null +++ b/.github/scripts/import_pyamplicol.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +# Import (or re-sync) the history of github.com/MadGraphTeam/pyamplicol into +# the pyamplicol/ subdirectory. The filter is deterministic, so re-running it +# on a newer upstream main reproduces the SHAs of commits imported earlier and +# a merge only brings in the new ones. +# +# Usage: .github/scripts/import_pyamplicol.sh [upstream-ref] +# Leaves the filtered history in the local branch pyamplicol-import; merge it +# with `git merge pyamplicol-import` (first import: --allow-unrelated-histories). +# Requires git-filter-repo (set FILTER_REPO to override the command). +set -euo pipefail + +UPSTREAM_URL=${UPSTREAM_URL:-https://github.com/MadGraphTeam/pyamplicol.git} +REF=${1:-main} +FILTER_REPO=${FILTER_REPO:-git-filter-repo} +MG_ROOT=$(git rev-parse --show-toplevel) + +WORK=$(mktemp -d) +trap 'rm -rf "$WORK"' EXIT + +git clone --quiet --no-local --single-branch --branch "$REF" "$UPSTREAM_URL" "$WORK/pyamplicol" +cd "$WORK/pyamplicol" +UPSTREAM_SHA=$(git rev-parse HEAD) + +# Prepared-model packs are rebuilt in CI and shipped as one release archive; +# reports and papers are linked from the archived upstream repository. +$FILTER_REPO --force \ + --path-glob '*.pyamplicol-model' --invert-paths \ + --path-glob '*.pdf' --invert-paths \ + --to-subdirectory-filter pyamplicol + +git -C "$MG_ROOT" fetch --quiet --no-tags "$WORK/pyamplicol" "+HEAD:refs/heads/pyamplicol-import" +echo "Imported $UPSTREAM_URL $REF at $UPSTREAM_SHA into branch pyamplicol-import" diff --git a/.github/workflows/pyamplicol-eager-portability.yml b/.github/workflows/pyamplicol-eager-portability.yml new file mode 100644 index 0000000000..a6dc57f6d6 --- /dev/null +++ b/.github/workflows/pyamplicol-eager-portability.yml @@ -0,0 +1,218 @@ +name: pyAmpliCol eager JIT pack architecture transfer + +# Ported from pyamplicol: packs produced on one architecture must work on the +# others, which the installer relies on when it regenerates them locally. + +on: + pull_request: + paths: + - .github/workflows/pyamplicol-eager-portability.yml + - pyamplicol/Cargo.lock + - pyamplicol/Cargo.toml + - pyamplicol/build_backend/prepared_models.py + - pyamplicol/dependencies/release-lock.toml + - pyamplicol/pyproject.toml + - pyamplicol/rust/crates/rusticol-core/src/eager_runtime/** + - pyamplicol/rust/crates/rusticol-core/src/eager_runtime.rs + - pyamplicol/rust/crates/rusticol-core/src/eager_tables.rs + - pyamplicol/rust/crates/rusticol-core/src/engine/** + - pyamplicol/rust/crates/rusticol-core/src/engine.rs + - pyamplicol/rust/crates/rusticol-core/src/evaluator/symjit.rs + - pyamplicol/rust/crates/rusticol-core/src/evaluator/symjit_plane.rs + - pyamplicol/rust/crates/rusticol-core/src/evaluator/symjit_direct.rs + - pyamplicol/rust/crates/rusticol-core/src/evaluator/symjit_eager_direct.rs + - pyamplicol/rust/crates/rusticol-core/src/evaluator/symjit_compiled_direct.rs + - pyamplicol/rust/crates/rusticol-core/src/lib.rs + - pyamplicol/rust/crates/rusticol-python/src/lib.rs + - pyamplicol/src/pyamplicol/_internal/physics/symbols.py + - pyamplicol/src/pyamplicol/_internal/versions.py + - pyamplicol/src/pyamplicol/config/** + - pyamplicol/src/pyamplicol/evaluators/symbolica*.py + - pyamplicol/src/pyamplicol/generation/** + - pyamplicol/src/pyamplicol/models/prepared*.py + - pyamplicol/src/pyamplicol/runtime/eager_exact/** + - pyamplicol/tools/ci/eager_portability*.py + push: + branches: [main] + paths: + - .github/workflows/pyamplicol-eager-portability.yml + - pyamplicol/Cargo.lock + - pyamplicol/Cargo.toml + - pyamplicol/build_backend/prepared_models.py + - pyamplicol/dependencies/release-lock.toml + - pyamplicol/pyproject.toml + - pyamplicol/rust/crates/rusticol-core/src/eager_runtime/** + - pyamplicol/rust/crates/rusticol-core/src/eager_runtime.rs + - pyamplicol/rust/crates/rusticol-core/src/eager_tables.rs + - pyamplicol/rust/crates/rusticol-core/src/engine/** + - pyamplicol/rust/crates/rusticol-core/src/engine.rs + - pyamplicol/rust/crates/rusticol-core/src/evaluator/symjit.rs + - pyamplicol/rust/crates/rusticol-core/src/evaluator/symjit_plane.rs + - pyamplicol/rust/crates/rusticol-core/src/evaluator/symjit_direct.rs + - pyamplicol/rust/crates/rusticol-core/src/evaluator/symjit_eager_direct.rs + - pyamplicol/rust/crates/rusticol-core/src/evaluator/symjit_compiled_direct.rs + - pyamplicol/rust/crates/rusticol-core/src/lib.rs + - pyamplicol/rust/crates/rusticol-python/src/lib.rs + - pyamplicol/src/pyamplicol/_internal/physics/symbols.py + - pyamplicol/src/pyamplicol/_internal/versions.py + - pyamplicol/src/pyamplicol/config/** + - pyamplicol/src/pyamplicol/evaluators/symbolica*.py + - pyamplicol/src/pyamplicol/generation/** + - pyamplicol/src/pyamplicol/models/prepared*.py + - pyamplicol/src/pyamplicol/runtime/eager_exact/** + - pyamplicol/tools/ci/eager_portability*.py + workflow_dispatch: + +permissions: + contents: read + +defaults: + run: + working-directory: pyamplicol + +concurrency: + group: pyamplicol-eager-portability-${{ github.ref }} + cancel-in-progress: true + +jobs: + produce-architecture-packs: + name: Produce prepared JIT pack (${{ matrix.target }}) + runs-on: ${{ matrix.runner }} + concurrency: + group: "pyamplicol-symbolica-${{ startsWith(matrix.runner, 'macos-') && matrix.runner || format('{0}-{1}', matrix.runner, github.run_id) }}" + cancel-in-progress: false + queue: max + timeout-minutes: 90 + strategy: + fail-fast: false + matrix: + include: + - runner: ubuntu-24.04 + target: linux-x86-64 + system: Linux + machine: x86_64 + pack_architecture: x86_64 + - runner: macos-15 + target: macos-arm64 + system: Darwin + machine: arm64 + pack_architecture: aarch64 + env: + PYAMPLICOL_BUILD_MODE: release + PYTHONDONTWRITEBYTECODE: "1" + SYMBOLICA_HIDE_BANNER: "1" + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + with: + persist-credentials: false + - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 + with: + python-version: "3.11" + cache: pip + cache-dependency-path: | + pyamplicol/pyproject.toml + pyamplicol/dependencies/release-lock.toml + pyamplicol/dependencies/python-runtime-lock.toml + - uses: dtolnay/rust-toolchain@46511b1c83438f0dd37c02d843619ece5a4abb5b # 1.97.1 + - name: Install pinned build tools + run: python -m pip install "maturin==1.15.0" "packaging==26.3" + - name: Build the non-publishable release bootstrap wheel + run: >- + python tools/ci/memory_watchdog.py --limit-gib 30 -- + python tools/release/prepare_release_prepared_models.py + bootstrap-wheel .artifacts/release-prepared-bootstrap + - name: Install the isolated published-dependency runtime + run: | + python -m venv .venv + .venv/bin/python -m pip install .artifacts/release-prepared-bootstrap/pyamplicol-*.whl + - name: Produce host-labelled portable built-in JIT O2 packs + run: >- + .venv/bin/python tools/ci/memory_watchdog.py --limit-gib 30 -- + .venv/bin/python tools/ci/eager_portability.py produce + .artifacts/eager-portability-transfer/${{ matrix.pack_architecture }} + --python .venv/bin/python + --asset-mode release + --expected-system ${{ matrix.system }} + --expected-machine ${{ matrix.machine }} + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: pyamplicol-eager-jit-transfer-${{ matrix.pack_architecture }} + path: pyamplicol/.artifacts/eager-portability-transfer/${{ matrix.pack_architecture }}/ + if-no-files-found: error + retention-days: 14 + + consume-matching-architecture-pack: + name: Consume portable pack (${{ matrix.target }}) + needs: produce-architecture-packs + runs-on: ${{ matrix.runner }} + concurrency: + group: "pyamplicol-symbolica-${{ startsWith(matrix.runner, 'macos-') && matrix.runner || format('{0}-{1}', matrix.runner, github.run_id) }}" + cancel-in-progress: false + queue: max + timeout-minutes: 90 + strategy: + fail-fast: false + matrix: + include: + - runner: ubuntu-24.04 + target: linux-x86-64 + system: Linux + machine: x86_64 + pack_architecture: x86_64 + - runner: macos-15-intel + target: macos-x86-64 + system: Darwin + machine: x86_64 + pack_architecture: x86_64 + - runner: macos-15 + target: macos-arm64 + system: Darwin + machine: arm64 + pack_architecture: aarch64 + env: + PYAMPLICOL_BUILD_MODE: release + PYTHONDONTWRITEBYTECODE: "1" + SYMBOLICA_HIDE_BANNER: "1" + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + with: + persist-credentials: false + - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 + with: + python-version: "3.11" + cache: pip + cache-dependency-path: | + pyamplicol/pyproject.toml + pyamplicol/dependencies/release-lock.toml + pyamplicol/dependencies/python-runtime-lock.toml + - uses: dtolnay/rust-toolchain@46511b1c83438f0dd37c02d843619ece5a4abb5b # 1.97.1 + - uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 + with: + name: pyamplicol-eager-jit-transfer-${{ matrix.pack_architecture }} + path: pyamplicol/.artifacts/eager-portability-transfer + - name: Install pinned build tools + run: python -m pip install "maturin==1.15.0" "packaging==26.3" + - name: Build the non-publishable release bootstrap wheel + run: >- + python tools/ci/memory_watchdog.py --limit-gib 30 -- + python tools/release/prepare_release_prepared_models.py + bootstrap-wheel .artifacts/release-prepared-bootstrap + - name: Install the isolated published-dependency runtime + run: | + python -m venv .venv + .venv/bin/python -m pip install .artifacts/release-prepared-bootstrap/pyamplicol-*.whl + - name: Generate and evaluate from the portable pack + run: >- + .venv/bin/python tools/ci/memory_watchdog.py --limit-gib 30 -- + .venv/bin/python tools/ci/eager_portability.py consume + .artifacts/eager-portability-transfer + --python .venv/bin/python + --expected-system ${{ matrix.system }} + --expected-machine ${{ matrix.machine }} + --report .artifacts/eager-portability-results/${{ matrix.target }}.json + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: pyamplicol-eager-jit-result-${{ matrix.target }} + path: pyamplicol/.artifacts/eager-portability-results/${{ matrix.target }}.json + if-no-files-found: error + retention-days: 14 diff --git a/.github/workflows/pyamplicol.yml b/.github/workflows/pyamplicol.yml new file mode 100644 index 0000000000..b847a4b4e9 --- /dev/null +++ b/.github/workflows/pyamplicol.yml @@ -0,0 +1,623 @@ +name: pyAmpliCol + +# Ported from the standalone pyamplicol repository (tests.yml and +# release-artifacts.yml). Every job runs inside pyamplicol/. +on: + push: + # All branches, no tags (see madspace.yml): release.yml gates on the run + # for the commit on main and then calls this workflow to build the wheels. + branches: + - '**' + pull_request: + workflow_dispatch: + inputs: + full_matrix: + description: 'Also build and test the release sdist and wheels' + type: boolean + default: false + workflow_call: + inputs: + full_matrix: + description: 'Also build and test the release sdist and wheels' + type: boolean + default: false + +permissions: + contents: read + +concurrency: + group: pyamplicol-${{ github.ref }}-${{ github.event_name }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +defaults: + run: + working-directory: pyamplicol + +env: + PYTHONDONTWRITEBYTECODE: "1" + SYMBOLICA_HIDE_BANNER: "1" + +jobs: + # Skips the heavy jobs unless pyamplicol changed; a job, not `paths:`, so every + # commit still gets a (green) run for release.yml's CI gate. + changes: + runs-on: ubuntu-24.04 + outputs: + pyamplicol: ${{ steps.diff.outputs.pyamplicol }} + defaults: + run: + working-directory: . + steps: + - uses: actions/checkout@v5 + with: + fetch-depth: 0 + persist-credentials: false + - id: diff + env: + EVENT: ${{ github.event_name }} + PUSH_BEFORE: ${{ github.event.before }} + PR_BASE: ${{ github.event.pull_request.base.sha }} + # Release builds only build artifacts; main's run already tested them. + FULL_MATRIX: ${{ inputs.full_matrix && 'true' || 'false' }} + run: | + set -euo pipefail + if [[ "$FULL_MATRIX" == true ]]; then + echo "pyamplicol=false" | tee -a "$GITHUB_OUTPUT" + exit 0 + fi + case "$EVENT" in + push) base="$PUSH_BEFORE" ;; + pull_request) base="$PR_BASE" ;; + *) base="" ;; + esac + if [[ -z "$base" || "$base" =~ ^0+$ ]] || ! git cat-file -e "$base^{commit}" 2>/dev/null; then + changed=true + elif git diff --name-only "$base" HEAD \ + | grep -qE '^(pyamplicol/|\.github/workflows/pyamplicol|\.github/actions/(pyamplicol|build_pyamplicol|install_pyamplicol)|VERSION$)'; then + changed=true + else + changed=false + fi + echo "pyamplicol=$changed" | tee -a "$GITHUB_OUTPUT" + + python-compatibility: + name: Python ${{ matrix.python-version }} lightweight API + needs: changes + if: needs.changes.outputs.pyamplicol == 'true' + runs-on: ubuntu-24.04 + strategy: + fail-fast: false + matrix: + python-version: ["3.11", "3.12", "3.13", "3.14"] + steps: + - uses: actions/checkout@v5 + with: + persist-credentials: false + - uses: actions/setup-python@v6 + with: + python-version: ${{ matrix.python-version }} + cache: pip + cache-dependency-path: pyamplicol/pyproject.toml + - name: Install lightweight test dependency + run: python -m pip install "pytest>=8.3,<9" + - name: Check lightweight typed configuration API + env: + PYTHONPATH: ${{ github.workspace }}/pyamplicol/src + run: >- + python -m pytest + tests/unit/test_config_models.py + tests/unit/test_config_resolution.py + -q + - name: Check the CI memory watchdog + if: matrix.python-version == '3.11' + run: python -m pytest tests/unit/test_ci_memory_watchdog.py -q + + source-contracts: + name: Python 3.11 source and release contracts + needs: changes + if: needs.changes.outputs.pyamplicol == 'true' + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v5 + with: + persist-credentials: false + - uses: actions/setup-python@v6 + with: + python-version: "3.11" + cache: pip + cache-dependency-path: pyamplicol/pyproject.toml + - name: Install source-contract test dependency + run: python -m pip install "pytest>=8.3,<9" + - name: Prepared models + uses: ./.github/actions/pyamplicol_prepared_models + - name: Check the version follows the MadGraph VERSION file + run: python tools/release/sync_version.py --check + - name: Check public source, repository, and release contracts + env: + PYTHONPATH: ${{ github.workspace }}/pyamplicol/src + run: >- + python -m pytest + tests/unit/test_api_requests.py + tests/unit/test_cli_parser.py + tests/unit/test_deployment_harness.py + tests/unit/test_public_import_laziness.py + tests/unit/test_repository_policy.py + tests/unit/test_sdk_config.py + tests/unit/test_otf_cache.py + tests/release + --deselect + tests/unit/test_api_requests.py::test_public_compiled_model_uses_the_canonical_schema + -q + + # Builds through the MadGraph installer and checks that Symbolica accepts the + # package library key; the install is cached for install_pyamplicol. + installer: + name: MadGraph installer and Symbolica library key + needs: changes + if: needs.changes.outputs.pyamplicol == 'true' + runs-on: ubuntu-24.04 + defaults: + run: + working-directory: . + steps: + - uses: actions/checkout@v5 + with: + persist-credentials: false + - uses: actions/setup-python@v6 + with: + python-version: "3.11" + - name: Build pyamplicol with install.py (checks the library key) + uses: ./.github/actions/build_pyamplicol + - name: Library key from the MadGraph root, as the [amplicol] worker sees it + run: | + python - <<'EOF' + import sys + sys.path.insert(0, ".") + from madgraph.iolibs.template_files.mg7 import bootstrap + bootstrap.ensure_pyamplicol(interactive=False) + bootstrap.check_pyamplicol_license() + assert bootstrap.check_pyamplicol_source_hash() + print("pyamplicol library key accepted; source hash matches") + EOF + - name: Warn before the library key expires + run: | + expiry=$(grep -o 'SU-[0-9]*-pyamplicol-[0-9.]*' pyamplicol/src/pyamplicol/__init__.py \ + | sed 's/.*-//' | tr . -) + days=$(( ($(date -d "$expiry" +%s) - $(date +%s)) / 86400 )) + echo "pyamplicol's Symbolica library key expires on $expiry ($days days)." + if (( days < 90 )); then + echo "::warning::pyamplicol's Symbolica library key expires on $expiry; request a renewed key." + fi + - name: Symbolica is licensed outside the MadGraph root + env: + PYTHONPATH: ${{ github.workspace }}/pyamplicol/install + run: | + cd "$RUNNER_TEMP" + env -u SYMBOLICA_LICENSE python - <<'EOF' + import pyamplicol + from pyamplicol.licensing import detect_symbolica_license + state = detect_symbolica_license(suggest=False) + print(state) + assert state.licensed, "Symbolica runs in restricted mode" + EOF + + release-runtime: + name: Published-dependency generation, runtime, and native SDK + needs: [changes, python-compatibility, source-contracts] + if: needs.changes.outputs.pyamplicol == 'true' && github.event_name != 'push' + runs-on: ubuntu-24.04 + env: + CARGO_INCREMENTAL: "0" + PYAMPLICOL_BUILD_MODE: release + PYAMPLICOL_REQUIRE_NATIVE_TESTS: "1" + steps: + - uses: actions/checkout@v5 + with: + persist-credentials: false + - uses: actions/setup-python@v6 + with: + python-version: "3.11" + cache: pip + cache-dependency-path: | + pyamplicol/pyproject.toml + pyamplicol/dependencies/release-lock.toml + pyamplicol/dependencies/contributor-lock.toml + pyamplicol/dependencies/python-runtime-lock.toml + - uses: dtolnay/rust-toolchain@1.97.1 + - name: Cache Cargo downloads + uses: actions/cache@v5 + with: + path: | + ~/.cargo/registry/index + ~/.cargo/registry/cache + ~/.cargo/git/db + key: ${{ runner.os }}-cargo-1.97.1-${{ hashFiles('pyamplicol/Cargo.lock', 'pyamplicol/dependencies/contributor-lock.toml') }} + restore-keys: | + ${{ runner.os }}-cargo-1.97.1- + - name: Install C++17 and Fortran 2008 toolchains + run: | + sudo apt-get update + sudo apt-get install --no-install-recommends -y g++ gfortran make + command -v c++ + command -v gfortran + command -v make + - name: Prepared models + uses: ./.github/actions/pyamplicol_prepared_models + - name: Build and stage the published-dependency runtime (30 GiB RSS limit) + run: >- + python tools/ci/memory_watchdog.py --limit-gib 30 -- + python dependencies/install_dependencies.py + - name: Run lightweight dependency, typing, and CI-tool checks + run: | + .venv/bin/python tools/release/check_dependencies.py + PYTHON="$PWD/.venv/bin/python" just typing + .venv/bin/python -m ruff check tools/ci tests/unit/test_ci_memory_watchdog.py + - name: Build and exercise native APIs and UMAMI shared providers (30 GiB RSS limit) + env: + RUST_BACKTRACE: "1" + run: >- + .venv/bin/python tools/ci/memory_watchdog.py --limit-gib 30 -- + .venv/bin/python -m pytest + tests/integration/test_multilanguage_api.py + tests/integration/test_correlated_native_sdk.py + tests/integration/test_otf_cache_native_sdk.py + tests/integration/test_umami_api.py + -q -s + - name: Run focused Python unit checks (30 GiB RSS limit) + run: | + .venv/bin/python tools/ci/memory_watchdog.py --limit-gib 30 -- \ + .venv/bin/python -m pytest \ + tests/unit/test_api_requests.py \ + tests/unit/test_api_bundle.py \ + tests/unit/test_umami_metadata.py \ + tests/unit/test_umami_semantics.py \ + tests/unit/test_benchmarking.py \ + tests/unit/test_adjoint_color_contraction.py \ + tests/unit/test_adjoint_generation_scope.py \ + tests/unit/test_adjoint_singlet_color_certificate.py \ + tests/unit/test_fft_basis_strategy.py \ + tests/unit/test_compiler_kernel_scale.py \ + tests/unit/test_compiler_auxiliary_components.py \ + tests/unit/test_heft_contact_trees.py \ + tests/unit/test_sm_heft_model.py \ + tests/unit/test_recurrence_numerical_current_warmup.py \ + tests/unit/test_recurrence_exact_contracted_color.py \ + tests/unit/test_recurrence_direct_intrinsics.py \ + tests/unit/test_config_models.py \ + tests/unit/test_config_resolution.py \ + tests/unit/test_dag_model_independence.py \ + tests/unit/test_exact_parameter_precision.py \ + tests/unit/test_exact_color_weight.py \ + tests/unit/test_exact_symbolic_propagators.py \ + tests/unit/test_stage_exact_weights.py \ + tests/unit/test_color_connection_catalogue.py \ + tests/unit/test_color_connections.py \ + tests/unit/test_color_correlator_matrices.py \ + tests/unit/test_correlat*.py \ + tests/unit/test_nlo_limit_study.py \ + tests/unit/test_exact_vertex_normalization.py \ + tests/unit/test_external_model_independence.py \ + tests/unit/test_generation_config_provenance.py \ + tests/unit/test_generation_execution_schema.py \ + tests/unit/test_generation_stage_contract.py \ + tests/unit/test_model_loading.py \ + tests/unit/test_sdk_config.py \ + tests/unit/test_selftest_fixture.py \ + tests/unit/test_symbolica_exact.py \ + -q + - name: Run eager prepared-model and runtime unit checks (30 GiB RSS limit) + run: >- + .venv/bin/python tools/ci/memory_watchdog.py --limit-gib 30 -- + .venv/bin/python -m pytest + tests/unit/test_eager_*.py + tests/unit/test_model_source_prepared_compile.py + tests/unit/test_packaged_prepared_model*.py + tests/unit/test_prepared_*.py + -q + - name: Run focused generation and runtime integration (30 GiB RSS limit) + # The restricted child must run first with an uninitialized parent. + run: | + .venv/bin/python tools/ci/memory_watchdog.py --limit-gib 30 -- \ + .venv/bin/python -m pytest tests/integration/test_spenso_restricted_mode.py -q + .venv/bin/python tools/ci/memory_watchdog.py --limit-gib 30 -- \ + .venv/bin/python -m pytest \ + tests/integration/test_schema_v3_generation_runtime.py \ + tests/integration/test_exact_precision_normalization.py \ + tests/integration/test_sm_heft_backends.py \ + tests/integration/test_heft_correlated_precision.py \ + tests/integration/test_compiled_exact_color.py \ + tests/integration/test_correlators.py \ + tests/integration/test_correlator_colour_accuracy.py \ + tests/integration/test_otf_cache.py \ + tests/integration/test_adjoint_fft_runtime.py \ + tests/integration/test_generalized_adjoint_fft_runtime.py \ + tests/integration/test_recurrence_generation_runtime.py::test_ufo_sm_recurrence_refreshes_real_and_complex_derived_couplings \ + -q + - name: Check Rust workspace (30 GiB RSS limit) + run: >- + .venv/bin/python tools/ci/memory_watchdog.py --limit-gib 30 -- + env PYTHON=$PWD/.venv/bin/python just rust-check + - name: Test Rust workspace (30 GiB RSS limit) + run: >- + .venv/bin/python tools/ci/memory_watchdog.py --limit-gib 30 -- + env PYTHON=$PWD/.venv/bin/python just rust-test + - name: Exercise installed release runtime and checkout-independent examples + run: | + .venv/bin/python -m pyamplicol self-test --json + .venv/bin/python -m pytest tests/integration/test_examples.py -q + + # ---- Release artifacts (release.yml, or a manual run with full_matrix) ---- + + prepared-models: + name: Release prepared-model archive + if: inputs.full_matrix + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v5 + with: + persist-credentials: false + - uses: actions/setup-python@v6 + with: + python-version: "3.11" + - name: Prepared models + uses: ./.github/actions/pyamplicol_prepared_models + - uses: actions/upload-artifact@v4 + with: + name: pyamplicol-prepared-models + path: pyamplicol/.artifacts/prepared-models/pyamplicol-prepared-models.tar.gz + if-no-files-found: error + + release-source-preflight: + name: Focused release source preflight + needs: prepared-models + runs-on: ubuntu-24.04 + env: + PYAMPLICOL_BUILD_MODE: release + steps: + - uses: actions/checkout@v5 + - uses: actions/setup-python@v6 + with: + python-version: "3.11" + cache: pip + - uses: actions/download-artifact@v5 + with: + name: pyamplicol-prepared-models + path: pyamplicol/.artifacts/prepared-models + - name: Unpack the prepared models + run: python tools/release/prepared_models_archive.py unpack .artifacts/prepared-models/pyamplicol-prepared-models.tar.gz + - name: Install focused source-test tools + run: python -m pip install "pytest>=8.3,<9" "ruff>=0.9,<1" + - name: Verify exact published dependency inputs + run: python tools/release/check_dependencies.py + - name: Run focused clean-checkout release tests + run: >- + python -m pytest + tests/release + tests/unit/test_dependency_gate.py + tests/unit/test_release_*.py + -q + - name: Lint release and build tooling + run: >- + ruff check + build_backend + tools/release + tests/release + tests/unit/test_dependency_gate.py + tests/unit/test_release_*.py + + independent-physics-oracle: + name: Independent Fortran physics oracle + if: inputs.full_matrix + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v5 + - uses: actions/setup-python@v6 + with: + python-version: "3.11" + - name: Install the independent oracle toolchain + run: | + sudo apt-get update + sudo apt-get install --no-install-recommends -y gfortran make + python -m pip install "jsonschema>=4.22,<5" + - name: Run independent Fortran physics comparison + run: | + set -euo pipefail + python tools/ci/memory_watchdog.py --limit-gib 30 -- \ + python tools/developer/legacy_amplicol.py \ + --prepare-checkout \ + --fixture tests/fixtures/reference/physics-v2.json \ + --jobs 2 >/dev/null + + retained-sdist: + name: Build retained source distribution + needs: [release-source-preflight, independent-physics-oracle] + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v5 + - uses: actions/setup-python@v6 + with: + python-version: "3.11" + cache: pip + - uses: dtolnay/rust-toolchain@1.97.1 + - uses: actions/download-artifact@v5 + with: + name: pyamplicol-prepared-models + path: pyamplicol/.artifacts/prepared-models + - name: Unpack the prepared models + run: python tools/release/prepared_models_archive.py unpack .artifacts/prepared-models/pyamplicol-prepared-models.tar.gz + - name: Install build tools + run: python -m pip install "build>=1.2,<2" "maturin==1.15.0" + - name: Build the one retained sdist + run: >- + python tools/ci/memory_watchdog.py --limit-gib 30 -- + python tools/release/build_release_artifacts.py + --sdist-only + --output-dir .artifacts/release-source + - uses: actions/upload-artifact@v4 + with: + name: pyamplicol-retained-sdist + path: pyamplicol/.artifacts/release-source/ + if-no-files-found: error + + macos-wheels: + name: macOS release wheel and native deployment (${{ matrix.target }}) + needs: retained-sdist + strategy: + fail-fast: false + matrix: + include: + - runner: macos-15 + target: macos-arm64 + - runner: macos-15-intel + target: macos-x86_64 + runs-on: ${{ matrix.runner }} + env: + MACOSX_DEPLOYMENT_TARGET: "11.0" + PYAMPLICOL_BUILD_MODE: release + RUSTUP_TOOLCHAIN: "1.97.1" + steps: + - uses: actions/checkout@v5 + - uses: actions/download-artifact@v5 + with: + name: pyamplicol-retained-sdist + path: pyamplicol/.artifacts/retained + - uses: actions/setup-python@v6 + with: + python-version: "3.11" + cache: pip + - uses: dtolnay/rust-toolchain@1.97.1 + - name: Install build and deployment tools + run: python -m pip install "build>=1.2,<2" "maturin==1.15.0" "twine>=6,<7" + - name: Build and audit from retained sdist + run: >- + python tools/ci/memory_watchdog.py --limit-gib 30 -- + python tools/release/build_release_artifacts.py + --retained-sdist .artifacts/retained/pyamplicol-*.tar.gz + --output-dir .artifacts/platform + - name: Require C++17 and Fortran 2008 SDK toolchains + shell: bash + run: | + set -euo pipefail + command -v make + cxx="$(command -v c++)" + fc="$(command -v gfortran || true)" + if [[ -z "$fc" ]]; then + brew install gcc + prefix="$(brew --prefix gcc)" + for candidate in "$prefix/bin/gfortran" "$prefix"/bin/gfortran-*; do + if [[ -x "$candidate" ]]; then + fc="$candidate" + break + fi + done + fi + [[ -n "$fc" && -x "$fc" ]] + printf 'CXX=%s\n' "$cxx" >> "$GITHUB_ENV" + printf 'FC=%s\n' "$fc" >> "$GITHUB_ENV" + - name: Test installed wheel on CPython 3.11 + run: >- + python tools/ci/memory_watchdog.py --limit-gib 30 -- + python tools/release/test_deployment.py + --python "$(command -v python)" + --wheel .artifacts/platform/pyamplicol-*.whl + --no-build + - uses: actions/setup-python@v6 + with: + python-version: "3.14" + - name: Test installed abi3 wheel on CPython 3.14 + run: >- + python tools/ci/memory_watchdog.py --limit-gib 30 -- + python tools/release/test_deployment.py + --python "$(command -v python)" + --wheel .artifacts/platform/pyamplicol-*.whl + --abi3-smoke-only + --no-build + - uses: actions/upload-artifact@v4 + with: + name: pyamplicol-release-${{ matrix.target }} + path: pyamplicol/.artifacts/platform/ + if-no-files-found: error + + manylinux-wheel: + name: manylinux release wheel and native deployment + needs: retained-sdist + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v5 + - uses: actions/download-artifact@v5 + with: + name: pyamplicol-retained-sdist + path: pyamplicol/.artifacts/retained + - name: Build and deploy-test in manylinux_2_28 x86_64 + run: | + docker run --rm \ + -v "$PWD:/io" \ + -w /io \ + -e PYAMPLICOL_BUILD_MODE=release \ + quay.io/pypa/manylinux_2_28_x86_64@sha256:b04887b645dde99b9e955aeae3ff4da414992d0bd88259f046295b56361c5614 \ + /bin/bash -euxo pipefail -c ' + git config --global --add safe.directory "*" + yum install -y gcc-c++ gcc-gfortran make + curl --proto "=https" --tlsv1.2 -sSf \ + -o /tmp/rustup-init \ + https://static.rust-lang.org/rustup/archive/1.28.2/x86_64-unknown-linux-gnu/rustup-init + echo "20a06e644b0d9bd2fbdbfd52d42540bdde820ea7df86e92e533c073da0cdd43c /tmp/rustup-init" \ + | sha256sum --check --strict + chmod +x /tmp/rustup-init + /tmp/rustup-init -y --profile minimal --default-toolchain 1.97.1 + source "$HOME/.cargo/env" + export RUSTUP_TOOLCHAIN=1.97.1 + PY311=/opt/python/cp311-cp311/bin/python + PY314=/opt/python/cp314-cp314/bin/python + "$PY311" -m pip install "build>=1.2,<2" "maturin==1.15.0" "twine>=6,<7" + "$PY311" tools/ci/memory_watchdog.py --limit-gib 30 -- \ + "$PY311" tools/release/build_release_artifacts.py \ + --retained-sdist .artifacts/retained/pyamplicol-*.tar.gz \ + --output-dir .artifacts/platform + "$PY311" tools/ci/memory_watchdog.py --limit-gib 30 -- \ + "$PY311" tools/release/test_deployment.py \ + --python "$PY311" \ + --wheel .artifacts/platform/pyamplicol-*.whl \ + --no-build + "$PY311" tools/ci/memory_watchdog.py --limit-gib 30 -- \ + "$PY311" tools/release/test_deployment.py \ + --python "$PY314" \ + --wheel .artifacts/platform/pyamplicol-*.whl \ + --abi3-smoke-only \ + --no-build + ' + - uses: actions/upload-artifact@v4 + with: + name: pyamplicol-release-manylinux-x86_64 + path: pyamplicol/.artifacts/platform/ + if-no-files-found: error + + validated-artifacts: + name: Collect validated release artifacts + needs: [retained-sdist, macos-wheels, manylinux-wheel] + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v5 + - uses: actions/download-artifact@v5 + with: + pattern: pyamplicol-re* + path: pyamplicol/.artifacts/incoming + - uses: actions/setup-python@v6 + with: + python-version: "3.11" + cache: pip + - name: Install artifact validation tools + run: python -m pip install "twine>=6,<7" + - name: Audit and collect package files + run: >- + python tools/release/publish_dry_run.py + --artifact-dir .artifacts/incoming + --output-dir .artifacts/validated + --no-build + --require-all-targets + --skip-clean-install + - uses: actions/upload-artifact@v4 + with: + name: pyamplicol-validated-release + path: pyamplicol/.artifacts/validated/ + if-no-files-found: error diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 52ed6efdec..25160316e2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -2,8 +2,9 @@ name: Release # Fires once a GitHub Release is published (draft -> Publish release), never on # a tag push by itself. Checks that main's CI was green for the tagged commit, -# publishes madspace to PyPI with the same version as the MadGraph release, and -# attaches the MadGraph tarball to the release. If anything fails, the release +# publishes madspace and pyamplicol to PyPI with the same version as the +# MadGraph release, and attaches the MadGraph tarball and the pyamplicol +# prepared-model archive to the release. If anything fails, the release # is reverted to draft so a half-published version never stays public. # # The test suites are deliberately NOT re-run here. Every commit on main is @@ -89,6 +90,7 @@ jobs: .github/workflows/aloha.yml .github/workflows/check_xsec_processes_mg7.yml .github/workflows/madspace.yml + .github/workflows/pyamplicol.yml # Builds the wheels for every supported Python/platform. This one suite does # have to run at release time: the CI build on main only builds the @@ -132,10 +134,45 @@ jobs: - name: Publish to PyPI uses: pypa/gh-action-pypi-publish@release/v1 + # Builds and tests the pyamplicol sdist and wheels (and the prepared-model + # archive they contain), validated by pyamplicol's own release tooling. + pyamplicol: + needs: require_green_ci + uses: ./.github/workflows/pyamplicol.yml + with: + full_matrix: true + + # Defined here for the same reason as publish_pypi; pyamplicol's PyPI trusted + # publisher names this workflow and the same "pypi" environment. + publish_pypi_pyamplicol: + name: Publish pyamplicol to PyPI + needs: [pyamplicol, publish_pypi] + runs-on: ubuntu-latest + environment: + name: pypi + url: https://pypi.org/p/pyamplicol + permissions: + id-token: write # IMPORTANT: mandatory for trusted publishing + steps: + - name: Download the validated dists + uses: actions/download-artifact@v5 + with: + name: pyamplicol-validated-release + path: validated + - name: Keep only the package files + run: | + mkdir dist + find validated -name 'pyamplicol-*.whl' -exec cp {} dist/ \; + find validated -name 'pyamplicol-*.tar.gz' -exec cp {} dist/ \; + ls -l dist + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + tarball: needs: - check_version - publish_pypi + - publish_pypi_pyamplicol runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v5 @@ -152,6 +189,21 @@ jobs: path: wheels merge-multiple: true + - name: Download the pyamplicol wheels built for this release + uses: actions/download-artifact@v5 + with: + name: pyamplicol-validated-release + path: pyamplicol-validated + + - name: Download the pyamplicol prepared-model archive + uses: actions/download-artifact@v5 + with: + name: pyamplicol-prepared-models + path: dist + + - name: Collect the pyamplicol wheel names + run: find pyamplicol-validated -name 'pyamplicol-*.whl' -exec cp {} wheels/ \; + - name: Build the MadGraph tarball run: > python3 bin/create_release.py --version "${{ needs.check_version.outputs.version }}" @@ -175,6 +227,14 @@ jobs: done test -d madspace/install/madspace + # Also checks that Symbolica accepts pyamplicol's library key. + for attempt in $(seq 10); do + python3 pyamplicol/install.py --bin -y && break + echo "attempt $attempt: pyamplicol==${{ needs.check_version.outputs.version }} not installable yet, retrying..." + sleep 15 + done + test -d pyamplicol/install/pyamplicol + - name: Sign the tarball with Sigstore uses: sigstore/gh-action-sigstore-python@v3.0.0 with: @@ -191,6 +251,8 @@ jobs: - require_green_ci - madspace - publish_pypi + - pyamplicol + - publish_pypi_pyamplicol - tarball # 'skipped' has to count as a failure here: a job whose own guard is false, # or one whose dependency was skipped, is neither failure() nor cancelled(), diff --git a/.github/workflows/unittest.yml b/.github/workflows/unittest.yml index 96923b1fb9..473e91a117 100644 --- a/.github/workflows/unittest.yml +++ b/.github/workflows/unittest.yml @@ -202,6 +202,15 @@ jobs: cd $GITHUB_WORKSPACE ./tests/test_manager.py test_madspace_install -t0 + # ---- unittest_pyamplicol_install ---- + # tests/unit_tests/various/test_pyamplicol_install.py: install.py (build + # stubbed), the version sync with VERSION and the source hash + - name: "unittest_pyamplicol_install: run the pyamplicol installer unit tests" + if: ${{ !cancelled() }} + run: | + cd $GITHUB_WORKSPACE + ./tests/test_manager.py test_pyamplicol_install -t0 + # ---- unittest_hwu ---- # tests/unit_tests/various/test_mg7_hwu_output.py: the MADatLO.HwU an mg7 # run writes, read back and overlaid on an aMC@NLO MADatNLO.HwU by diff --git a/.gitignore b/.gitignore index 28254be189..91e028ef0c 100644 --- a/.gitignore +++ b/.gitignore @@ -84,6 +84,7 @@ docs/build/ # per-class API pages written by docs/generate_api_pages.py docs/source/madspace/cpp/ docs/source/madspace/python/ +docs/source/pyamplicol/ tests/input_files/IOTestsComparison_BackUp/ tests/input_files/IOTestsComparison/**/*.BackUp diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 530bd0877a..0c5413c5ce 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -46,3 +46,14 @@ repos: - id: clang-format types_or: [c++, c, cuda] files: ^madspace/ + # --- pyamplicol: the same lint scope as its CI (pyamplicol.yml) ------------- + # Run from pyamplicol/, so its own packages are first-party for isort. + - repo: local + hooks: + - id: pyamplicol-ruff + name: ruff (pyamplicol) + language: python + additional_dependencies: ["ruff==0.17.1"] + entry: bash -c 'cd pyamplicol && ruff check "${@#pyamplicol/}"' -- + types: [python] + files: ^pyamplicol/(build_backend|tools/release|tools/ci|tests/release)/ diff --git a/README.md b/README.md index d790154879..f26a6db42d 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,10 @@ Python 3.12 or higher and a C++ compiler are required. The `madspace` library is installed automatically the first time you launch a run. To build it yourself instead, run `install madspace` from the MadGraph7 prompt. +The `pyamplicol` package, the matrix-element generator of the `[amplicol]` backend, is +installed in the same way with `install pyamplicol`. It lives in `pyamplicol/` and keeps +its own license (0BSD, see `pyamplicol/LICENSE` and `pyamplicol/THIRD_PARTY_NOTICES.md`). + ## First steps Type `tutorial` at the MadGraph7 prompt for a guided walkthrough of the commands. diff --git a/bin/create_release.py b/bin/create_release.py index b926fdda8b..6ee832c706 100755 --- a/bin/create_release.py +++ b/bin/create_release.py @@ -23,8 +23,8 @@ python bin/create_release.py --version 0.2.0 --output dist/ It performs the following actions: - 1. Check that VERSION agrees with --version, and that madspace still derives - its version from it. (UpdateNotes.txt still tracks the legacy MG5_aMC@NLO + 1. Check that VERSION agrees with --version, that madspace still derives + its version from it, and that pyamplicol's versions are synced to it. (UpdateNotes.txt still tracks the legacy MG5_aMC@NLO 3.7.x series and is not part of this check.) 2. git-archive the current HEAD into a clean MG7_v directory (preserves the madgraph/VERSION symlink; ignores untracked/gitignored @@ -128,8 +128,9 @@ def parse_info_file(filepath): def check_versions(version): - """Fail loudly if VERSION disagrees with the version being released, or if - madspace/pyproject.toml has stopped deriving its version from VERSION.""" + """Fail loudly if VERSION disagrees with the version being released, if + madspace/pyproject.toml has stopped deriving its version from VERSION, or + if pyamplicol's literal (Cargo) versions are out of sync with VERSION.""" errors = [] mg_version = parse_info_file(pjoin(ROOT, 'VERSION')).get('version') @@ -143,6 +144,13 @@ def check_versions(version): "madspace/pyproject.toml pins its own version instead of reading it " "from VERSION; madspace must be released in lockstep with MadGraph") + sync = subprocess.run( + [sys.executable, pjoin(ROOT, 'pyamplicol', 'tools', 'release', 'sync_version.py'), + '--check', '--version', version], + capture_output=True, text=True) + if sync.returncode: + errors.append(sync.stderr.strip() + "; run pyamplicol/tools/release/sync_version.py") + if errors: for e in errors: print(f"ERROR: {e}", file=sys.stderr) diff --git a/docs/requirements.txt b/docs/requirements.txt index c4c8deb079..aff1220c44 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -2,3 +2,4 @@ sphinx==9.0.4 sphinx-autodoc-typehints==3.6.0 breathe==4.36.0 furo==2025.12.19 +myst-parser==5.0.0 diff --git a/docs/source/conf.py b/docs/source/conf.py index 541cbea840..164a6dbc0b 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -19,6 +19,30 @@ # For the full list of built-in configuration values, see the documentation: # https://www.sphinx-doc.org/en/master/usage/configuration.html +# pyamplicol/docs is copied (not symlinked: MyST resolves links from the real +# path) into the gitignored source/pyamplicol. +def _copy_pyamplicol_docs(): + import shutil + + here = os.path.dirname(os.path.abspath(__file__)) + source = os.path.join(here, "..", "..", "pyamplicol", "docs") + target = os.path.join(here, "pyamplicol") + shutil.rmtree(target, ignore_errors=True) + jekyll_only = shutil.ignore_patterns( + "_config.yml", "_includes", "css", "results", "archive", "arxiv", ".*" + ) + + def ignore(directory, names): + # Only the report index is tracked; campaigns are local output. + if os.path.basename(directory) == "performance_reports": + return {name for name in names if name != "README.md"} + return jekyll_only(directory, names) + + shutil.copytree(source, target, ignore=ignore) + + +_copy_pyamplicol_docs() + # -- Project information ----------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information @@ -60,8 +84,16 @@ def _read_version() -> str: # "sphinx.ext.linkcode", "sphinx_autodoc_typehints", "breathe", + "myst_parser", ] +# The pyAmpliCol pages are Markdown. +source_suffix = {".rst": "restructuredtext", ".md": "markdown"} +myst_enable_extensions = ["dollarmath", "colon_fence"] +myst_heading_anchors = 3 +# pyAmpliCol pages put an HTML banner above their first (H2) heading. +suppress_warnings = ["myst.header"] + # The docstrings generated from the Doxygen comments use Google-style "Args:" # blocks (see madspace/generate_docstrings.py); napoleon turns them into proper # parameter field lists. Keep the numpy parser off to avoid ambiguity. diff --git a/docs/source/index.rst b/docs/source/index.rst index 3c597d70cb..f761b2752e 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -40,3 +40,17 @@ If you need a stable release, use the madspace/umami-api madspace/python-api madspace/cpp-api + +.. toctree:: + :maxdepth: 1 + :caption: pyAmpliCol + :hidden: + + pyamplicol/README + pyamplicol/user/installation + pyamplicol/user/gentle-walkthrough + pyamplicol/user/configuration + pyamplicol/user/python-api + pyamplicol/user/profiling-and-benchmarking + pyamplicol/user/architecture-overview + pyamplicol/development/README diff --git a/docs/source/installation.rst b/docs/source/installation.rst index 462acbdf6a..2e9f7c0c77 100644 --- a/docs/source/installation.rst +++ b/docs/source/installation.rst @@ -13,3 +13,7 @@ Python 3.12 or higher and a C++ compiler are required. The ``madspace`` library installed automatically the first time you launch a run. To build it yourself instead, run ``install madspace`` from the MadGraph7 prompt, or see the :doc:`MadSpace installation instructions `. + +The ``pyamplicol`` package, used by the ``[amplicol]`` matrix-element backend, is +installed in the same way with ``install pyamplicol``; see the +:doc:`pyAmpliCol installation instructions `. diff --git a/madgraph/interface/madgraph_interface.py b/madgraph/interface/madgraph_interface.py index 836112fc28..4e5f7547f9 100755 --- a/madgraph/interface/madgraph_interface.py +++ b/madgraph/interface/madgraph_interface.py @@ -606,6 +606,9 @@ def help_install(self): logger.info(" --source Build from source (non-interactive).") logger.info(" -j N/--jobs=N Parallel compilation jobs (source build;") logger.info(" defaults to the 'nb_core' option).") + logger.info(" --system Install into the active Python environment instead of madspace/install.") + logger.info(" --clean Delete the build and install directories first (full rebuild).") + logger.info(" --docs Generate API docstrings and .pyi stubs (source build, needs doxygen).") logger.info(" --cuda Enable CUDA GPU backend (source build).") logger.info(" --hip Enable HIP/ROCm GPU backend (source build).") logger.info(" --openblas Build OpenBLAS from source (default on Linux/Windows, source build).") @@ -616,6 +619,20 @@ def help_install(self): logger.info(" --no-debug Optimized build, no debug symbols, Release (source build, default).") logger.info(" --cuda-arch=ARCHS Semicolon-separated CUDA compute capabilities, e.g. 75;80;86.") logger.info(" --hip-arch=ARCHS Semicolon-separated HIP architectures, e.g. gfx900;gfx906.") + logger.info(" ") + logger.info(" \"install pyamplicol [options]\"",'$MG:BOLD') + logger.info(" Install pyAmpliCol, the matrix-element generator of the [amplicol] backend.") + logger.info(" Without options an interactive installer is launched. Available options:") + logger.info(" -y/--yes Re-install non-interactively using saved settings.") + logger.info(" --bin Install pre-compiled package from PyPI (non-interactive;") + logger.info(" only guaranteed to match this checkout in a release tarball).") + logger.info(" --source Build from source (non-interactive; needs a Rust toolchain,") + logger.info(" which is installed locally if rustup is not available).") + logger.info(" -j N/--jobs=N Parallel compilation jobs (source build;") + logger.info(" defaults to the 'nb_core' option).") + logger.info(" --system Install into the active Python environment instead of pyamplicol/install.") + logger.info(" --clean Delete the build and install directories first (full rebuild).") + logger.info(" --build-models Regenerate the prepared models instead of downloading them.") def help_display(self): logger.info("syntax: display " + "|".join(self._display_opts),'$MG:color:BLUE') @@ -3665,7 +3682,8 @@ def complete_install(self, text, line, begidx, endidx): elif len(args) and args[0] == 'update': return self.list_completion(text, ['-f','--timeout=']) elif len(args) >= 2 and args[1] == 'madspace': - options = ['-y', '--yes', '--bin', '--source', + options = ['-y', '--yes', '--bin', '--source', '-j', '--jobs=', + '--system', '--clean', '--docs', '--no-docs', '--cuda', '--no-cuda', '--hip', '--no-hip', '--openblas', '--no-openblas', '--simd', '--no-simd', @@ -3675,6 +3693,13 @@ def complete_install(self, text, line, begidx, endidx): if any(a.startswith(opt) for a in args[2:]): options.remove(opt) return self.list_completion(text, options) + elif len(args) >= 2 and args[1] == 'pyamplicol': + options = ['-y', '--yes', '--bin', '--source', '-j', '--jobs=', + '--system', '--clean', '--build-models'] + for opt in options[:]: + if any(a.startswith(opt) for a in args[2:]): + options.remove(opt) + return self.list_completion(text, options) elif len(args)>=2 and args[1] in self._advanced_install_opts: options = ['--keep_source','--logging='] if args[1]=='pythia8': @@ -3729,7 +3754,8 @@ def _tutorial_opts(self): _import_formats = ['model_v4', 'model', 'proc_v4', 'command', 'banner'] _install_opts = ['Delphes', 'ExRootAnalysis', 'update', 'Golem95', 'QCDLoop', 'maddm', 'maddump', - 'looptools', 'MadSTR', 'RunningCoupling', 'madspace'] + 'looptools', 'MadSTR', 'RunningCoupling', 'madspace', + 'pyamplicol'] # The targets below are installed using the HEPToolsInstaller.py script _advanced_install_opts = ['pythia8','zlib','boost','lhapdf6','lhapdf5','collier', @@ -8393,8 +8419,8 @@ def do_install(self, line, paths=None, additional_options=[]): elif args[0] == 'looptools': self.install_reduction_library(force=True) return - elif args[0] == 'madspace': - install_script = pjoin(MG5DIR, 'madspace', 'install.py') + elif args[0] in ('madspace', 'pyamplicol'): + install_script = pjoin(MG5DIR, args[0], 'install.py') install_args = args[1:] # The source build compiles in parallel only if it is told how many # jobs it may use (see set_build_parallelism in install.py), so hand diff --git a/madgraph/iolibs/template_files/mg7/bootstrap.py b/madgraph/iolibs/template_files/mg7/bootstrap.py index 90fab5bcd4..416415d5bf 100644 --- a/madgraph/iolibs/template_files/mg7/bootstrap.py +++ b/madgraph/iolibs/template_files/mg7/bootstrap.py @@ -1,4 +1,4 @@ -"""One-off madspace bootstrap for the mg7 output. +"""One-off madspace (and pyamplicol) bootstrap for the mg7 output. The mg7 launcher needs the compiled ``madspace`` package. A git checkout ships only the sources, so the first run has to build and install it. That bootstrap @@ -19,6 +19,8 @@ safe to do lazily. """ +import importlib +import json import os import subprocess import sys @@ -32,6 +34,8 @@ MG_ROOT = Path(_mg_pkg.__file__).parents[1] MADSPACE_DIR = MG_ROOT / "madspace" INSTALL_DIR = MADSPACE_DIR / "install" +PYAMPLICOL_DIR = MG_ROOT / "pyamplicol" +PYAMPLICOL_INSTALL_DIR = PYAMPLICOL_DIR / "install" def madspace_is_installed() -> bool: @@ -39,6 +43,43 @@ def madspace_is_installed() -> bool: return (INSTALL_DIR / "madspace").is_dir() +def pyamplicol_is_installed() -> bool: + """True when a compiled pyamplicol is already available to import.""" + return (PYAMPLICOL_INSTALL_DIR / "pyamplicol").is_dir() + + +def _run_installer(name, source_dir, interactive, jobs) -> None: + """Run ``/install.py`` as the first-use bootstrap does.""" + if interactive is None: + interactive = sys.stdin.isatty() + print() + print(f"You don't have {name} installed for this madgraph instance") + print(f"Running the {name} installation script") + print() + + install_cmd = [sys.executable, str(source_dir / "install.py")] + # When the run is non-interactive (scripted / piped), install + # non-interactively with a source build and default options + # (--source --yes), and keep the installer away from our stdin (which + # may carry the run's scripted card-editing commands); when + # interactive, let it share the terminal so the user can answer. + install_stdin = None if interactive else subprocess.DEVNULL + if not interactive: + install_cmd += ["--source", "--yes"] + if jobs: + install_cmd += ["-j", str(jobs)] + # Expose madgraph on PYTHONPATH so the installer subprocess can import + # cmd.ask for its prompts. + install_env = os.environ.copy() + install_env["PYTHONPATH"] = os.pathsep.join( + [str(MG_ROOT)] + + ([install_env["PYTHONPATH"]] if install_env.get("PYTHONPATH") else []) + ) + result = subprocess.run(install_cmd, env=install_env, stdin=install_stdin) + if result.returncode != 0: + raise RuntimeError(f"{name} installation failed — see output above") + + def ensure_madspace(interactive=None, jobs=None) -> None: """Install madspace if needed, then put it on ``sys.path``. @@ -51,39 +92,96 @@ def ensure_madspace(interactive=None, jobs=None) -> None: (MG5's ``nb_core``); ``None`` lets the installer use every core. """ if not madspace_is_installed(): - if interactive is None: - interactive = sys.stdin.isatty() - print() - print("You don't have madspace installed for this madgraph instance") - print("Running the madspace installation script") - print() - - install_cmd = [sys.executable, str(MADSPACE_DIR / "install.py")] - # When the run is non-interactive (scripted / piped), install - # non-interactively with a source build and default options - # (--source --yes), and keep the installer away from our stdin (which - # may carry the run's scripted card-editing commands); when - # interactive, let it share the terminal so the user can answer. - install_stdin = None if interactive else subprocess.DEVNULL - if not interactive: - install_cmd += ["--source", "--yes"] - if jobs: - install_cmd += ["-j", str(jobs)] - # Expose madgraph on PYTHONPATH so the installer subprocess can import - # cmd.ask for its prompts. - install_env = os.environ.copy() - install_env["PYTHONPATH"] = os.pathsep.join( - [str(MG_ROOT)] - + ([install_env["PYTHONPATH"]] if install_env.get("PYTHONPATH") else []) - ) - result = subprocess.run(install_cmd, env=install_env, stdin=install_stdin) - if result.returncode != 0: - raise RuntimeError("madspace installation failed — see output above") + _run_installer("madspace", MADSPACE_DIR, interactive, jobs) if str(INSTALL_DIR) not in sys.path: sys.path.insert(0, str(INSTALL_DIR)) +def ensure_pyamplicol(interactive=None, jobs=None) -> None: + """Install pyamplicol if needed, then put it first on ``sys.path``. + + Same arguments as :func:`ensure_madspace`. The install directory must come + before the MadGraph root: there ``pyamplicol/`` is the source directory, + which would otherwise be imported as an empty namespace package. + """ + if not pyamplicol_is_installed(): + _run_installer("pyamplicol", PYAMPLICOL_DIR, interactive, jobs) + + while str(PYAMPLICOL_INSTALL_DIR) in sys.path: + sys.path.remove(str(PYAMPLICOL_INSTALL_DIR)) + sys.path.insert(0, str(PYAMPLICOL_INSTALL_DIR)) + + +def check_pyamplicol_source_hash() -> bool: + """Warn if the installed pyamplicol was built from other sources. + + Returns whether the hashes match. Must run after :func:`ensure_pyamplicol`. + """ + try: + installed = importlib.import_module("pyamplicol._source_hash").SOURCE_HASH + except ImportError: + installed = None + source = subprocess.run( + [sys.executable, str(PYAMPLICOL_DIR / "source_hash.py")], + capture_output=True, text=True, check=True, + ).stdout.strip() + if installed == source: + return True + print() + print( + "\033[1m\033[31mWARNING\033[39m: pyamplicol source and installed package " + "are not compatible (source hash mismatch) — consider recompiling " + "pyamplicol (e.g. `install pyamplicol -y`)\033[0m" + ) + print() + return False + + +def check_pyamplicol_license(pythonpath=None) -> None: + """Fail unless Symbolica accepts pyamplicol's package-scoped library key. + + The key is registered on ``import pyamplicol`` and only licenses calls made + from the ``pyamplicol`` package, so the check runs in a fresh process with + the same ``PYTHONPATH`` as the generation worker and without a personal + ``SYMBOLICA_LICENSE``. It also checks that the import resolves to the + installed package, not to the source directory. + """ + env = os.environ.copy() + env.pop("SYMBOLICA_LICENSE", None) + env["PYTHONPATH"] = os.pathsep.join(pythonpath if pythonpath is not None else sys.path) + code = ( + "import json, pyamplicol\n" + "from pyamplicol.licensing import detect_symbolica_license\n" + "print(json.dumps({'file': pyamplicol.__file__,\n" + " 'licensed': detect_symbolica_license(suggest=False).licensed}))\n" + ) + result = subprocess.run( + [sys.executable, "-c", code], cwd=MG_ROOT, env=env, + capture_output=True, text=True, + ) + try: + info = json.loads(result.stdout.strip().splitlines()[-1]) + except (IndexError, ValueError): + # E.g. Symbolica refuses to start while this process already holds + # its per-user lock (symbolica imported here before the check). + raise RuntimeError( + "pyamplicol cannot be imported:\n" + result.stdout + result.stderr + ) from None + location = Path(info["file"]).resolve() + if (location.is_relative_to(PYAMPLICOL_DIR.resolve()) + and not location.is_relative_to(PYAMPLICOL_INSTALL_DIR.resolve())): + raise RuntimeError( + f"pyamplicol resolves to its source directory ({PYAMPLICOL_DIR}), " + "not to an installed package" + ) + if not info["licensed"]: + raise RuntimeError( + "Symbolica does not accept pyamplicol's library license key; " + "generation would run in Symbolica's restricted mode" + ) + + def drop_install_path() -> None: """Take the madspace install directory back off ``sys.path``. diff --git a/madspace/install.py b/madspace/install.py index f21688e2b5..887104556a 100644 --- a/madspace/install.py +++ b/madspace/install.py @@ -577,7 +577,9 @@ def _release_wheel_available() -> bool: python_tag = f"cp{sys.version_info.major}{sys.version_info.minor}" return any( - python_tag in name and all(tag in name for tag in platform_tags) + name.startswith(f"{PACKAGE_NAME}-") + and python_tag in name + and all(tag in name for tag in platform_tags) for name in wheels.split(",") ) diff --git a/pyamplicol/.gitattributes b/pyamplicol/.gitattributes new file mode 100644 index 0000000000..871c3b6b41 --- /dev/null +++ b/pyamplicol/.gitattributes @@ -0,0 +1,14 @@ +* text=auto +*.json text eol=lf +*.md text eol=lf +*.py text eol=lf +*.rs text eol=lf +*.toml text eol=lf +*.f90 text eol=lf +*.h text eol=lf +*.hpp text eol=lf +*.pdf binary +*.a binary +*.bin binary +dependencies/patches/** whitespace=-trailing-space,-space-before-tab +licenses/rust/** -text whitespace=-trailing-space diff --git a/pyamplicol/.gitignore b/pyamplicol/.gitignore new file mode 100644 index 0000000000..ef7a14f629 --- /dev/null +++ b/pyamplicol/.gitignore @@ -0,0 +1,121 @@ +# SPDX-License-Identifier: 0BSD + +# Python +__pycache__/ +*.py[cod] +*.egg-info/ +.pytest_cache/ +.mypy_cache/ +.ruff_cache/ +.coverage +htmlcov/ + +# Build products +build/ +dist/ +target/ +wheelhouse/ +/FFT_FEATURE_RESOURCES/ +*.whl +*.mod +src/pyamplicol/_rusticol*.so +src/pyamplicol/_rusticol*.dylib +src/pyamplicol/_rusticol*.pyd +.artifacts/ +# Temporary upstream dependency fixes stay local to this development branch. +/TMP_FIXED_SYMJIT/ +/TMP_FIXED_SPENSO/ +/FUTURE_ufo_model_loader/ +/SPENSO_LATEST_SYMBOLICA_COMPATIBILITY_FIXES/ +/SYMJIT_FOLLOW_UP_FIXES/ +/DEPENDENCY_FIXES/ +/IMPLEMENTATION_DOCS/REFERENCES/ +/IMPLEMENTATION_DOCS/RESULTS/fft-scaling-study/raw/ +/IMPLEMENTATION_DOCS/RESULTS/fft-profiling/runs/ +/IMPLEMENTATION_DOCS/RESULTS/summary_plots_final.pdf +/IMPLEMENTATION_DOCS/RESULTS/summary_plots_final_helicity_sum.pdf +.cargo/config.toml + +# Retain local performance campaigns and generated reports outside Git. The +# repository publishes only the report index and four selected PDF snapshots. +/docs/performance_reports/* +!/docs/performance_reports/README.md +!/docs/performance_reports/macbook_M3_pyAmpliCol.pdf +!/docs/performance_reports/EPYC_pyAmpliCol.pdf +!/docs/performance_reports/summary_plots_final.pdf +!/docs/performance_reports/summary_plots_final_helicity_sum.pdf +/docs/arxiv/ + +# Managed environments and deployment sandboxes +.venv/ +venv/ +dependencies/.venv/ +dependencies/checkouts/ +dependencies/candidate-Cargo.lock +dependencies/candidate-cargo-config.toml +dependencies/install-state.json +dependencies/wheelhouse/ +PYPI_DEPLOYMENT_TEST/ + +# Generated SDK staging +src/pyamplicol/_sdk/lib/ +src/pyamplicol/_sdk/include/ +src/pyamplicol/_sdk/fortran/ +src/pyamplicol/_sdk/link.json +src/pyamplicol/_sdk/metadata.json +src/pyamplicol/_sdk/sboms/ +src/pyamplicol/assets/selftest/*/ +!src/pyamplicol/assets/selftest/portable-64le/ +!src/pyamplicol/assets/selftest/portable-64le/** + +# Generated process and benchmark output +outputs/ +.sass-cache/ +docs/_site/ +docs/.jekyll-cache/ +docs/.sass-cache/ +docs/.bundle/ +docs/.result_outputs/ +docs/**/results/*.lock +docs/**/results/.coordination/ +docs/performance_reports/.*.init-*/ +docs/*.aux +docs/*.bbl +docs/*.bcf +docs/*.blg +docs/*.fdb_latexmk +docs/*.fls +docs/*.log +docs/*.out +docs/*.run.xml +docs/*.synctex.gz +docs/*.toc +docs/**/*.aux +docs/**/*.bbl +docs/**/*.bcf +docs/**/*.blg +docs/**/*.fdb_latexmk +docs/**/*.fls +docs/**/*.log +docs/**/*.out +docs/**/*.run.xml +docs/**/*.synctex.gz +docs/**/*.toc + +# Local tooling state +/tmp/ +.trash/ +.agent-work/ +.DS_Store + +# Prepared-model packs and metadata come from the release archive +# (tools/release/prepared_models_archive.py), not from Git. +src/pyamplicol/assets/prepared_models/*.metadata.json +src/pyamplicol/assets/prepared_models/*.pyamplicol-model +release_assets/prepared_models/*.metadata.json +release_assets/prepared_models/*.pyamplicol-model + +# MadGraph7 installer +/install/ +/install_settings.json +src/pyamplicol/_source_hash.py diff --git a/pyamplicol/AGENTS.md b/pyamplicol/AGENTS.md new file mode 100644 index 0000000000..d487ef9522 --- /dev/null +++ b/pyamplicol/AGENTS.md @@ -0,0 +1,14 @@ +# Agent policy: keep development and release checks lean + +Release and dependency validation must be deliberately minimal. Complexity, cache invalidation, and wall time are costs, not signs of rigor. + +- Start with the smallest check that prevents a demonstrated failure. Do not add attestations, secondary manifests, duplicated hashes, pristine-tree proofs, repeated dependency scans, or extra provenance layers merely because they are possible. +- Trust standard package-manager guarantees where they already cover the invariant: pinned Git revisions, lockfiles, archive checksums, and Cargo/pip resolution must not be reimplemented by repository-specific ceremony. +- Keep identities narrowly scoped to inputs that can change the artifact they identify. Python, documentation, tests, reports, and staging metadata must not invalidate a native build; validation settings and provenance-only files must not change runtime artifact identity. +- Check each invariant once, at the cheapest authoritative boundary, and reuse that result. Do not repeat fresh installs, builds, source scans, artifact audits, or platform matrices unless the changed code can affect them. +- Prefer focused local tests plus one authoritative CI run. Do not run duplicate full suites or performance campaigns during ordinary finalization unless the user explicitly asks or a relevant performance-sensitive path changed. +- Backward-compatibility machinery is opt-in, not automatic. This repository does not preserve old internal artifact formats unless the user explicitly requests it. +- Before adding a release/authentication check, state the concrete failure it prevents, why an existing check is insufficient, and its expected runtime/cache cost. If that case cannot be made, do not add it. +- When existing ceremony is encountered, simplify or remove it when safe. Never broaden validation as an incidental part of an unrelated change. + +Correctness, dependency integrity, and publishability remain required; the rule is to establish each with the least duplicated work and the smallest maintainable mechanism. diff --git a/pyamplicol/Cargo.lock b/pyamplicol/Cargo.lock new file mode 100644 index 0000000000..462274d773 --- /dev/null +++ b/pyamplicol/Cargo.lock @@ -0,0 +1,1531 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom 0.3.4", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e76a019e91224d279006ff972f1e984179a6e9feb050adba6ce8274aef23195" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "append-only-vec" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2114736faba96bcd79595c700d03183f61357b9fbce14852515e59f3bee4ed4a" + +[[package]] +name = "approx" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" +dependencies = [ + "num-traits", +] + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "az" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be5eb007b7cacc6c660343e96f650fedf4b5a77512399eb952ca6642cf8d13f7" + +[[package]] +name = "bincode" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36eaf5d7b090263e8150820482d5d93cd964a81e4019913c972f4edcc6edb740" +dependencies = [ + "bincode_derive", + "serde", + "unty", +] + +[[package]] +name = "bincode-trait-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1f425dc6064b77d777fdecf6d1b374ae29e0be056b5ba8308a798d381e82632" +dependencies = [ + "bincode", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "bincode_derive" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf95709a440f45e986983918d0e8a1f30a9b1df04918fc828670606804ac3c09" +dependencies = [ + "virtue", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "brotli" +version = "8.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc91aac060a7a1e25823bdccbfb6af1875b88f17c6daac97894eed8207166b3" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "5.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a32acac15fe1967bc3986b2a6347dffc965602354ea6f450ad07e8bfd253583" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bytemuck" +version = "1.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "colored" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622f3fc73690be383c7214310406f28a90e6edeadc3cea882f9d71e495b9711a" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc74980687109a3b14c72fd458107bf0baa1da1a1a805e178d15501ba9b86d9d" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "data-encoding" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06" + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "directories" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16f5094c54661b38d03bd7e50df373292118db60b585c08a411c6d840017fe7d" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.61.2", +] + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "ed25519-compact" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05391a505666bdf2b5d2626f41b7f0f49052b1e33cceac960eaa818008141da" + +[[package]] +name = "either" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "flate2" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb" +dependencies = [ + "crc32fast", + "miniz_oxide", + "zlib-rs", +] + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "gmp-mpfr-sys" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7db155b537cb791b133341f99f68371d86ee7fa4c79aacfbc376d72d23c70531" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "graphica" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afa7211a7cfdba21893f634868e19459e5d1882e5957f98d7c084875a6e70ab6" +dependencies = [ + "ahash", + "dyn-clone", + "numerica", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hugepage-rs" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da5c9051569a6ff2f501fb927a7d994af9232e4bff255352811d7b87afc50603" +dependencies = [ + "libc", +] + +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", +] + +[[package]] +name = "inventory" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4f0c30c76f2f4ccee3fe55a2435f691ca00c0e4bd87abe4f4a851b1d4dac39b" +dependencies = [ + "rustversion", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "libloading" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "754ca22de805bb5744484a5b151a9e1a8e837d5dc232c2d7d8c2e3492edc8b60" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libredox" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6480ccc157a1389bb2e4891b24751b0f798ba640d22386f23143fbcc89da195a" +dependencies = [ + "libc", +] + +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "mach2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44" +dependencies = [ + "libc", +] + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "matrixmultiply" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f607c237553f086e7043417a51df26b2eb899d3caff94e6a67592ff992fedc7" +dependencies = [ + "autocfg", + "rawpointer", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "miniz_oxide" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "ndarray" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520080814a7a6b4a6e9070823bb24b4531daac8c4627e08ba5de8c5ef2f2752d" +dependencies = [ + "matrixmultiply", + "num-complex", + "num-integer", + "num-traits", + "portable-atomic", + "portable-atomic-util", + "rawpointer", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "numerica" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd28aae6e022a7b5846a8ae40b2d41104f8d8cba38adbb02c0f7933f3ed7bb02" +dependencies = [ + "ahash", + "bincode", + "bincode-trait-derive", + "colored", + "dyn-clone", + "gmp-mpfr-sys", + "itertools", + "num-traits", + "rand", + "rand_core", + "rayon", + "rug", + "simba", + "smallvec", + "wide 1.7.1", + "xprec", +] + +[[package]] +name = "numpy" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a5b15d63a5ff39e378daed0e1340d3a5964703ea9712eb09a0dc66fade996f4" +dependencies = [ + "libc", + "ndarray", + "num-complex", + "num-integer", + "num-traits", + "pyo3", + "pyo3-build-config", + "rustc-hash", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "portable-atomic" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" + +[[package]] +name = "portable-atomic-util" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10ab3eb7f3becc3a1cbc4f2c6f20267996cfc1a6467a873763411b136a122715" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "pyo3" +version = "0.29.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4688ddedf473e32662b9b067670129a8afb8c18e351482c70d62ba4a88171e8b" +dependencies = [ + "libc", + "once_cell", + "portable-atomic", + "pyo3-build-config", + "pyo3-ffi", + "pyo3-macros", +] + +[[package]] +name = "pyo3-build-config" +version = "0.29.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f41027e41b4bd03f6e60f9f417fe24a6341a6bb744edd62b6f709f2a52ea30e9" +dependencies = [ + "target-lexicon", +] + +[[package]] +name = "pyo3-ffi" +version = "0.29.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e591a95526fead067432c3b3a33fc74770b87b1e04e73671090d9c2055a2b327" +dependencies = [ + "libc", + "pyo3-build-config", +] + +[[package]] +name = "pyo3-macros" +version = "0.29.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73225868fc1cd84eef2c3c230ddb91273bf1de46aeb8a4248da76d32a0924a1c" +dependencies = [ + "proc-macro2", + "pyo3-macros-backend", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "pyo3-macros-backend" +version = "0.29.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "571575aa3749fa6216757dd47d2a3e7ef360f329a40f0666a9fbd14889024952" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rawpointer" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" + +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "region" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6b6ebd13bc009aef9cd476c1310d49ac354d36e240cf1bd753290f3dc7199a7" +dependencies = [ + "bitflags", + "libc", + "mach2", + "windows-sys 0.52.0", +] + +[[package]] +name = "rug" +version = "1.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07a8857882aec59d27254b02481c709327c13de6fad1da60bfc4f9783eaaa61e" +dependencies = [ + "az", + "gmp-mpfr-sys", + "libc", + "libm", +] + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rusticol-capi" +version = "0.2.1" +dependencies = [ + "rusticol-core", + "serde_json", +] + +[[package]] +name = "rusticol-core" +version = "0.2.1" +dependencies = [ + "bincode", + "libc", + "libloading", + "num-complex", + "rayon", + "serde", + "serde_json", + "sha2", + "symbolica", + "symjit", + "thiserror", + "wide 1.7.1", +] + +[[package]] +name = "rusticol-python" +version = "0.2.1" +dependencies = [ + "flate2", + "num-bigint", + "num-rational", + "num-traits", + "numpy", + "pyo3", + "pyo3-build-config", + "rusticol-core", + "serde_json", + "sha2", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "safe_arch" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b02de82ddbe1b636e6170c21be622223aea188ef2e139be0a5b219ec215323" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "safe_arch" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42c6efa15875e6ecb39ca61fb0b0c1a40b84fac5a5ffe71eef7d1000c8eb3f5f" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "self_cell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab42ca02749e120097e328d91d415325bdf43b1c72c4c8badf37375fe40a813" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "simba" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c99284beb21666094ba2b75bbceda012e610f5479dfcc2d6e2426f53197ffd95" +dependencies = [ + "approx", + "num-complex", + "num-traits", + "paste", + "wide 0.7.33", +] + +[[package]] +name = "simd-adler32" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" + +[[package]] +name = "smallvec" +version = "1.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba467056f1b547ed52077911161fc86985becbc60e8e1857c8a144dab0def891" + +[[package]] +name = "smartstring" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fb72c633efbaa2dd666986505016c32c3044395ceaf881518399d2f4127ee29" +dependencies = [ + "autocfg", + "static_assertions", + "version_check", +] + +[[package]] +name = "spec_math" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02ef091d4e8b981dbaa4cc17789d00b53d5d2c001ae689f14e6093f30c529036" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "symbolica" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c060dc0a9613c88aadee7095109bf350e549067c9ecf7ea217064cdf3d439721" +dependencies = [ + "ahash", + "append-only-vec", + "bincode", + "bincode-trait-derive", + "brotli", + "byteorder", + "bytes", + "data-encoding", + "directories", + "dyn-clone", + "ed25519-compact", + "graphica", + "inventory", + "libloading", + "numerica", + "once_cell", + "rand", + "rayon", + "rug", + "self_cell", + "smallvec", + "smartstring", + "symjit", + "tinyjson", + "tracing", + "tracing-subscriber", + "wide 1.7.1", +] + +[[package]] +name = "symjit" +version = "2.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37344a51408baa239bbaa771e40cd908686bffed4f4491ec6bc608e8d248c931" +dependencies = [ + "ahash", + "anyhow", + "hashbrown 0.16.1", + "hugepage-rs", + "lazy_static", + "libc", + "num-complex", + "num-rational", + "num-traits", + "rand", + "rayon", + "region", + "serde", + "serde_json", + "spec_math", + "toml", + "wasmtime-jit-icache-coherence", + "wide 1.7.1", + "windows-sys 0.61.2", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12df2e0110f65b775f769bb17ef989067a1d931b2eb822bd4346631eeada89f9" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "target-lexicon" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" + +[[package]] +name = "thiserror" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "thread_local" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tinyjson" +version = "2.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ab95735ea2c8fd51154d01e39cf13912a78071c2d89abc49a7ef102a7dd725a" + +[[package]] +name = "toml" +version = "1.1.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "920602543f0911ab71da12c50d59701da54c196d1a2bf5cb4b75667f137a406a" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_parser" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" +dependencies = [ + "winnow", +] + +[[package]] +name = "toml_writer" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unty" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "virtue" +version = "0.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "051eb1abcf10076295e815102942cc58f9d5e3b4560e46e53c21e8ff6f3af7b1" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasmtime-jit-icache-coherence" +version = "34.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea6b740d1a35f2cebfe88e013ac8a4a84ff8dabc3a392df920abf554e871cf2" +dependencies = [ + "anyhow", + "cfg-if", + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "wide" +version = "0.7.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce5da8ecb62bcd8ec8b7ea19f69a51275e91299be594ea5cc6ef7819e16cd03" +dependencies = [ + "bytemuck", + "safe_arch 0.7.4", +] + +[[package]] +name = "wide" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d920ac99c3c8edce110cb8d07dbb324d6d026011dce85b1e9355b70f0adacc4f" +dependencies = [ + "bytemuck", + "safe_arch 1.2.0", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "xprec" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39a30f7277acb4ef10faeec631c7fbb0ff92355bea37e1b01a63f3803c78e319" +dependencies = [ + "approx", + "libm", + "num-traits", + "simba", +] + +[[package]] +name = "zerocopy" +version = "0.8.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d35102a9f36d089ccae9e4c6802bc118be4487b80aaffc0ab4e0cf5ce92d2873" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c01f5ab44258da43cf276c74a2763db2ff3969c9c652c3f2de07041d0b2bc" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zlib-rs" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34b31d188d9d685a4f9c7b46d6e36631b07058d2cfe190267adce54dc230bf12" + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/pyamplicol/Cargo.toml b/pyamplicol/Cargo.toml new file mode 100644 index 0000000000..dfda5eb881 --- /dev/null +++ b/pyamplicol/Cargo.toml @@ -0,0 +1,49 @@ +# SPDX-License-Identifier: 0BSD + +[workspace] +members = [ + "rust/crates/rusticol-core", + "rust/crates/rusticol-python", + "rust/crates/rusticol-capi", +] +exclude = [ + "TMP_FIXED_SYMJIT", + "TMP_FIXED_SPENSO", +] +resolver = "2" + +[workspace.package] +version = "0.2.1" +edition = "2024" +rust-version = "1.89.0" +license = "0BSD" +authors = [ + "Rikkert Frederix ", + "Valentin Hirschi ", + "Timea Vitos ", +] +repository = "https://github.com/mg5amcnlo/pyamplicol" + +[workspace.dependencies] +bincode = { version = "2.0", features = ["serde"] } +flate2 = "1.1" +libc = "0.2" +numpy = "0.29" +num-bigint = "0.4" +num-rational = "0.4" +num-traits = "0.2" +pyo3 = { version = "0.29", features = ["abi3-py311"] } +rayon = "1.10" +serde = { version = "1.0", features = ["derive"] } +serde_json = { version = "1.0", features = ["float_roundtrip", "raw_value"] } +sha2 = "0.10" +symbolica = { version = "=3.0.0", default-features = true, features = ["bincode"] } +thiserror = "2.0" +wide = "1.7" + +[profile.release] +codegen-units = 1 +lto = "thin" +opt-level = 3 +panic = "unwind" +strip = "debuginfo" diff --git a/pyamplicol/LICENSE b/pyamplicol/LICENSE new file mode 100644 index 0000000000..2e88cb37b9 --- /dev/null +++ b/pyamplicol/LICENSE @@ -0,0 +1,14 @@ +BSD Zero Clause License + +Copyright (c) 2026 Rikkert Frederix, Valentin Hirschi, and Timea Vitos + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. diff --git a/pyamplicol/README.md b/pyamplicol/README.md new file mode 100644 index 0000000000..fe501ea5ea --- /dev/null +++ b/pyamplicol/README.md @@ -0,0 +1,389 @@ + + +

+ pyAmpliCol +

+ +

+ PyPI + Python versions + pyAmpliCol documentation + Tests + License: 0BSD +

+ +

Fast color-ordered scattering amplitudes from Python and native APIs.

+ +pyAmpliCol generates and evaluates color-ordered scattering amplitudes from +built-in, JSON, or UFO models. It provides a typed Python API and CLI, fast +Rust-backed execution, runtime helicity and color-flow selection, and generated +Python, C11, C++17, Fortran 2008, and Rust 2021 interfaces. + +Explore the complete [pyAmpliCol documentation](https://madgraph.org/pyamplicol/) +for guided workflows, API examples, technical reference, and release support. + +Opt-in [tree-level spin and colour correlations](docs/correlators.md) prepare +named colour operators at generation and accept spin-contraction vectors at +runtime. This opt-in path supports LC, NLC and full colour at generic +connection order. `CorrelatorConfig.all_color(through_order=k)` prepares the +complete, nonminimal tree-soft catalogue through any positive order `k`; +`Runtime.available_color_correlations()` lists the generated operators. +Catalogues grow rapidly with order. Multiple requests share amplitudes and +accept batches of phase-space points. The `correlators` development branch +also exposes these operations in the C, C++, Fortran and Rust SDKs with +binary64 arithmetic; Python supports double-double and arbitrary precision. + +## Installation + +Install the release from PyPI: + +```console +python -m venv .venv +. .venv/bin/activate +python -m pip install pyamplicol +``` + +The binary wheels include the Rust runtime and native SDK; wheel users do not +need a Rust compiler. pyAmpliCol has no LHAPDF dependency. + +Build the tagged 1.0.1 source snapshot with: + +```console +git clone --branch v1.0.1 --depth 1 https://github.com/MadGraphTeam/pyamplicol.git +cd pyamplicol +python -m pip install . +``` + +A source build requires Python 3.11 or newer, Rust 1.89 or newer, and a C/C++ +toolchain. A Fortran compiler is required only for Fortran consumers. + +Contributor setup defaults to published Python dependencies from +`pyproject.toml`, including Symbolica 3.0.0 and ufo-model-loader 1.0.0, and the +release-mode native build with published SymJIT 2.26.4. +Building a new release wheel requires a clean Git checkpoint and complete +release assets; the dependency mode does not relax publication guards. + +```console +nix develop # optional on Nix/NixOS +just dev-install +PYTHON=.venv/bin/python just dev-test +``` + +The `just dev-install` native build can take several minutes. Use +`--wheel-directory PATH` to avoid rebuilding an existing compatible wheel. +For a dirty development checkout, use `--wheel-directory PATH` to reuse an +already-built compatible release wheel, or `--dependencies-only` to install +dependencies without building or staging the project; the latter leaves the +existing native runtime untouched. An optional `--loader-wheel PATH` explicitly +selects a local wheel for dependency development; normal setup uses PyPI. +Editable installs are not used. `just dev-test` also includes a fresh release +build and requires a clean checkpoint; dirty edits can use focused tests against +the staged native runtime. +Historical dependency-development machinery remains behind `--candidate`, +but its pinned APIs are incompatible with the current tensor implementation. +It needs updated, coherent upstream pins before use; use the published lane +for current-source development and tests. + +Full installation details are in the +[documentation](https://madgraph.org/pyamplicol/). + +## Quick start + +Copy the installed examples into an editable workspace: + +```console +pyamplicol examples copy ./pyamplicol-examples --force +cd pyamplicol-examples +``` + +Keep the Python environment containing pyAmpliCol activated while using the +top-level CLI and Python examples, or invoke its executables by explicit path. +For a copy below a source checkout prepared by `just dev-install`, generated +artifact Python and native API drivers also find the nearest checkout `.venv` +automatically; explicit SDK overrides and an active environment take +precedence. + +The primary example generates a multiprocess `p p > Z j j` artifact from the +packaged serialized Standard Model, then evaluates and profiles one concrete +subprocess. It expands to eight candidates, one per side-permutation class; +it stores the seven tree-level representatives and reports the omitted +loop-induced `g g > Z g g` class. The card inherits the portable JIT +O2 default, so its process artifact can be moved between supported 64-bit +little-endian macOS arm64, macOS x86_64, and Linux x86_64 hosts: + +```console +pyamplicol generate_pp_zjj_from_ufo_sm.toml +pyamplicol evaluate_total.toml +pyamplicol evaluate_resolved.toml +pyamplicol benchmark.toml +``` + +For direct CLI use: + +```console +pyamplicol generate "d d~ > z g" ./artifacts/builtin_ddbar_to_zg \ + --model built-in-sm + +pyamplicol inspect ./artifacts/builtin_ddbar_to_zg +``` + +Process generation can also be steered directly from Python: + +```python +from pyamplicol import GenerationConfig, Generator + +generator = Generator(GenerationConfig(workers=4)) +plan = generator.plan("d d~ > z g") # Resolve without writing an artifact. +result = generator.generate( + "d d~ > z g", + "artifacts/builtin_ddbar_to_zg", + mode="replace", +) +print(result.output) +``` + +The same runtime is available from Python: + +```python +import json +from pathlib import Path + +from pyamplicol import Runtime + +momenta = json.loads(Path("data/pp_zjj_momenta.json").read_text()) +runtime = Runtime.load("artifacts/pp_zjj", process="d d~ > g z g") +total = runtime.evaluate(momenta) +resolved = runtime.evaluate_resolved(momenta) +assert resolved.total() == total +``` + +Concrete process expressions may reorder particles within the incoming side or +within the outgoing side. Rusticol maps momenta, helicities, color flows, and +resolved metadata to that requested order; particles never cross the `>` +boundary. Stable process IDs remain available when more than one generated +representative could match an expression. + +See the +[examples guide](https://github.com/MadGraphTeam/pyamplicol/blob/main/examples/README.md) +for complete cards, parameter updates, selector examples, and generated API +drivers. + +## Models and execution + +pyAmpliCol supports: + +- the packaged built-in Standard Model; +- packaged serialized JSON and trusted UFO examples; +- user-supplied JSON or trusted UFO model paths; +- leading-color, contracted next-to-leading-color, and contracted full-color + calculations; +- recurrence, compiled-DAG, eager, and on-the-fly execution modes; +- JIT, C++, and assembly evaluator backends where supported; +- native binary64 execution without Symbolica computations, plus precision-controlled + Python evaluation when exact expressions are retained. On-the-fly execution + currently supports native binary64 only. + +Reusable artifacts preserve complete public helicity and color physics unless +the request explicitly fixes selectors at generation time. Runtime calls can +then select one flow or helicity globally or per phase-space point without +regenerating the artifact. On-the-fly artifacts always keep selection at +runtime and carry the complete contract in a compact query-local seed rather +than materializing the full axes; `inspect` reports their physical census +without constructing it. +Recurrence, eager, and on-the-fly execution reuse the same prepared model +kernel bundle. + +Contracted NLC/full-colour recurrence and on-the-fly execution automatically +try exact `symmetric-group-fft` colour contraction with adaptive adjoint-basis +selection, falling back to direct contraction when the FFT plan is unsupported. +Select `--color-accuracy full` (or `nlc`) to use this default policy. +Certified pure-gluon Yang–Mills trees and single-insertion HEFT use two-anchor +DDM tensors: for `n` external gluons, `(n-2)!` ordered amplitudes replace the +trace basis's `(n-1)!`. Quark processes retain fundamental chains or their +products; uncertified adjoint processes retain trace tensors. The saved +`fft_basis_selection` records the actual representation and its reason, so a +fallback is not presented as a DDM reduction. Configuration defaults are +`contraction = "auto"` and `fft_basis = "adjoint"`; LC, compiled/eager execution, +and correlators retain direct contraction and the trace basis. Use +`--color-contraction direct` to opt out, or `--fft trace` / `--fft adjoint` to +force FFT with that basis and reject unsupported plans. In the benchmarked +pure-gluon family the adjoint basis was faster than trace for six or more +external gluons, by 3.9x per sample with 4x faster generation at ten gluons. See the +[FFT configuration guide](docs/user/configuration.md#color-accuracy-and-lc-layout). +FFT transforms certified permutation-orbit blocks and retains unsupported +terms as exact direct residuals. + +Recurrence artifacts persist one helicity-parametric physical-colour +schedule, its helicity-support masks, and precomputed per-helicity row groups; +loading binds those groups once, so warmed evaluation does not rescan the +masks. On-the-fly execution instead constructs and caches the requested family +on first use, which is why that warm-up belongs to its plotted setup time. +Completed OTF work can be retained between programs with `runtime.save(path)` +and restored with `runtime.load_cache(path)` after loading the original process +output. Python and every native SDK expose these operations; see the +[small save/restore example](docs/user/runtime-and-selectors.md#saving-an-otf-warm-cache) +and [native equivalents](docs/user/native-apis.md#saving-and-restoring-an-otf-cache). + +The public C ABI is version 1. Every generated artifact can include standalone +Python, C11, C++17, Fortran 2008, and dependency-free Rust 2021 drivers backed +by the wheel-owned static Rusticol SDK. + +## Profiling campaigns + +An installed wheel can populate a self-contained campaign workspace: + +```console +pyamplicol profiling-campaign copy ./pyamplicol-profiling-campaign --force +cd ./pyamplicol-profiling-campaign +./steer_performance_campaign.py run \ + --workers 1 --table matrix --process-id 1 --multiplicity 1 \ + --color-approximation lc --generation-mode non-union-flow \ + --generation-engine recurrence --model built_in +``` + +That deliberately small real campaign measures only the final-state- +multiplicity-one `d d~ > Z` recurrence cell. Broader campaign selections are +intended for dedicated profiling hosts. The +[documentation](https://madgraph.org/pyamplicol/) covers selection, +continuation, optional original-AmpliCol comparisons, artifact retention, and +PDF generation. + +The source checkout also contains a thin orchestrator for the dedicated +FullColor FFT comparison. It delegates generation and timing to the existing +pyAmpliCol profiling commands and schedules independent measurement children: + +```console +just dev-install --with-legacy-amplicol --with-reference-fft +.venv/bin/python tools/fft_profiling/fft_profiling.py \ + --multiplicities 2 3 4 5 \ + --lines reference-fft amplicol pyamplicol-recurrence pyamplicol-otf madgraph \ + --cores 8 --candidate-cores 1 \ + --memory-limit-gib 30 --time-limit-seconds 3600 \ + --amplicol-root /path/to/AmpliCol \ + --reference-fft-root /path/to/AllGluonsMultipletFFT \ + --madgraph-root /path/to/MG5_aMC \ + --build-amplicol +``` + +`--multiplicities` adds the selected values to the persistent fill history and +defaults to `2 ... 9`. `--lines` similarly adds any combination of +`reference-fft`, `amplicol`, `pyamplicol-recurrence`, `pyamplicol-otf`, and +`madgraph`; the two pyAmpliCol groups each schedule their direct and FFT +companion curves together so they reuse the same generation lane. Dependencies +are selected automatically. +Repeating a command against the same output unions both selections, resumes +unfinished cells, and skips completed cells; +`--resume` is an explicit alias for that default. `--cores` is the total +scheduler budget, while `--candidate-cores` is one candidate child's core +claim and evaluator setting. The memory and time limits are strict per-child +cutoffs. `--retry` reruns only failed/skipped cells in the active selection. +`--overwrite` reruns every selected cell and replaces each old result only when +that cell's worker is about to launch; queued or blocked cells retain their old +results. Use `--output PATH` for an independent run directory. `--refresh` +removes only that exact recognized output directory and restarts it, so a +custom output also scopes the refresh; a path that does not exist simply starts +cleanly. Without `--output`, fixed and summed +workloads use separate `IMPLEMENTATION_DOCS/RESULTS/fft-profiling/runs/` +directories named `cluster-fullcolor-n2-n9` and +`cluster-fullcolor-helicity-sum-n2-n9`. Refresh also shares the persistent +MadGraph cache-writer lock and refuses to delete a run while a standalone +MadGraph profiler is using that cache. + +Add `--compare-helicity-sums` for the independent complete physical-helicity- +sum workload. The fixed-helicity MadGraph lane selects the shared helicity +through the generated `MATRIX(P,NHEL,IC)` entry point. The summed lane instead +calls the generated `SMATRIX(P,ANS)` with `USERHEL=-1`; MadGraph applies its +native IDEN normalization and may reuse its warmed `GOODHEL` pruning. Fixed- +helicity and summed overlays carry distinct workload identities and cannot be +mixed. `just dev-install` omits the developer-only AmpliCol and Reference FFT +repositories unless they are requested with `--with-legacy-amplicol` and +`--with-reference-fft`; either opt-in also installs the `fft-profiling` Python +extra into `.venv`. Their profiler roots default to +`dependencies/checkouts/legacy-amplicol` and +`dependencies/checkouts/reference-fft`; `--build-amplicol` may build the +AmpliCol probe once. Both paths can be overridden explicitly with +`--amplicol-root` and `--reference-fft-root`. The MadGraph root defaults to +`PYAMPLICOL_MADGRAPH_ROOT` or a recognized developer checkout and may be set +explicitly with `--madgraph-root`. + +The published fixed-helicity MadGraph series currently has measured points +through `n=5` for pure gluons and `n=6` for +`d d~ > d d~ + gluons`. Pure-gluon `n=6` retains its measured resource cutoff; +`n=7..9` are explicit protocol-scope not-applicable cells for both families. +The independent helicity-sum MadGraph series has measured points for both +families at `n=2..5`. Every admitted point passed the same-workload numerical +gate before entering the PDFs. + +The rolling plot frontiers are per process and implementation, rather than a +claim that every curve reaches the same `n`. Both fixed-helicity and helicity- +sum OTF curves are requested only through final-state `n=6`; beyond that the +publication protocol retains recurrence, AmpliCol, and Reference FFT where +applicable. Within that frontier, cutoffs are annotated rather than hidden or +interpolated. Pure-gluon OTF FFT reached the 3,600 s first-use runtime cap at +`n=6` and retains its measured `n=5` point. The helicity-sum comparison extends +the `d d~ > d d~ + gluons` curves through `n=6` using an authenticated isolated +30 GiB extension. At that point every requested curve measured successfully. +For OTF, direct and FFT setup took 1,725.8 s and 1,607.9 s, while warmed runtime +was 5.321 and 3.759 ms/point respectively (a 1.416x FFT speedup). The measured +peak RSS values were 1.17 and 1.22 GiB. + +The plotted *setup time* is deliberately method-specific. pyAmpliCol includes +artifact generation, a fresh load, the first requested evaluation, and OTF +family warm-up where applicable. Reference FFT includes its build, +initialization, and first pass. AmpliCol includes process/color-object +generation for the fixed workload, or process/raw-library generation and build +plus the immutable snapshot for the summed workload. Warmed runtime is measured +separately after those setup boundaries. Resource limits apply individually to +each child. The retained isolated high-frontier extensions use a 30 GiB +process-tree guard. + +After the first snapshot is published, rendering never waits for workers and +uses the latest available data. Concurrent renders and refresh publication are +serialized so an older render cannot replace a newer one: + +```console +python tools/fft_profiling/fft_profiling.py --render +python tools/fft_profiling/fft_profiling.py --render --compare-helicity-sums +python tools/fft_profiling/fft_profiling.py --render --output /path/to/run +``` + +For a custom output, the saved manifest determines whether the workload is +fixed-helicity or helicity-summed, so a render does not need to repeat +`--compare-helicity-sums`. Default outputs still use that flag to select the +helicity-sum workspace. Default outputs also refresh the corresponding +canonical PDF; custom outputs keep their PDF inside the selected run directory. +During a scan, the progress display reports the active cell, total live RSS, +and occupied core slots. + +An older local MadGraph overlay that predates the node-fingerprint field may +appear only in a nonterminal anytime render, only when its system, machine, and +Python version match the current workstation. Such plots carry an explicit +provenance note; the strict terminal publication merger still requires the +complete host identity produced by a fresh profiler run. + +The public performance index retains four selected rendered snapshots. Raw +JSON, generated tables, attempts, and campaign workspaces stay untracked: + +- [MacBook M3 report](https://github.com/MadGraphTeam/pyamplicol/blob/main/docs/performance_reports/macbook_M3_pyAmpliCol.pdf) +- [AMD EPYC report](https://github.com/MadGraphTeam/pyamplicol/blob/main/docs/performance_reports/EPYC_pyAmpliCol.pdf) +- [FullColor FFT selected-helicity snapshot](https://github.com/MadGraphTeam/pyamplicol/blob/main/docs/performance_reports/summary_plots_final.pdf) +- [FullColor FFT helicity-sum snapshot](https://github.com/MadGraphTeam/pyamplicol/blob/main/docs/performance_reports/summary_plots_final_helicity_sum.pdf) + +These are manual measurement snapshots rather than release-CI results; raw +campaign data remains local. The general host report format is reproducible +from an installed package, while the FullColor FFT snapshots use the source- +checkout orchestrator above. + +## Documentation + +Read the complete [pyAmpliCol documentation](https://madgraph.org/pyamplicol/). + +## Dependencies and license + +Release builds use pinned published dependencies plus SymJIT 2.25.0 from an +immutable revision of the official +[symjit-crate repository](https://github.com/siravan/symjit-crate). + +pyAmpliCol is distributed under the 0BSD license. Third-party components and +model assets retain their own terms; see +[THIRD_PARTY_NOTICES.md](https://github.com/MadGraphTeam/pyamplicol/blob/main/THIRD_PARTY_NOTICES.md). diff --git a/pyamplicol/THIRD_PARTY_NOTICES.md b/pyamplicol/THIRD_PARTY_NOTICES.md new file mode 100644 index 0000000000..5957f6a10f --- /dev/null +++ b/pyamplicol/THIRD_PARTY_NOTICES.md @@ -0,0 +1,90 @@ +# Third-Party Notices + +pyAmpliCol and Rusticol source written for this project are licensed under the +BSD Zero Clause License. That license does not replace the terms attached to +third-party dependencies or bundled model data. + +## Symbolica + +pyAmpliCol uses Symbolica, including its idenso and spenso functionality. +Symbolica is proprietary software. Its upstream terms state that copying or +distribution of any part of the Symbolica code requires express prior +permission: +. + +The pyAmpliCol project has express authorization from the Symbolica licensor +to redistribute the Symbolica components required by pyAmpliCol's binary +runtime. This project-specific permission does not authorize separate +redistribution of Symbolica. Users remain responsible for obtaining the +appropriate Symbolica use license. Attribution, upstream terms, and the scope +of pyAmpliCol's redistribution permission are recorded in +`licenses/Symbolica.txt`. + +Release artifacts must record the exact Symbolica package, Rust crate, +serialization ABI, and source revision used to build them. + +## Developer-Only Legacy AmpliCol Oracle + +Contributors may opt into a pinned checkout of the original Fortran AmpliCol +repository for independent numerical and performance validation. That checkout +is not included in pyAmpliCol wheels or source distributions. The contributor +metadata records that the pinned upstream revision contains no `LICENSE` or +`COPYING` file. This observation is not a grant or a claim about the upstream +source's terms. The developer setup uses the dedicated upstream branch +`amplicol_with_patches`; no legacy-AmpliCol patch payload is +shipped in pyAmpliCol release artifacts. + +## SymJIT + +SymJIT is a separate Rust dependency used by Symbolica's JIT functionality. +The official `siravan/symjit-crate` 2.25.0 source pinned at +`f1c193d301897149de6609f706297b0c97a4f018` is distributed under the MIT +License, not the Symbolica proprietary license. Its copyright notice and +complete MIT License are reproduced in `licenses/SymJIT.txt`. +The generic unsafe raw P-kernel plane-descriptor callable was merged upstream +in `siravan/symjit-crate#1`; it does not alter generated kernels. + +## Native Runtime Feature Boundary + +`Cargo.lock` records optional Symbolica, Rug/GMP, and Malachite dependencies +used by developer-only arbitrary-precision Rust checks. The distributed +`pyamplicol._rusticol` extension and `librusticol_capi.a` select the f64-only +Rusticol closure and link SymJIT directly; they do not link Symbolica, Rug/GMP, +or Malachite. Higher-precision Python evaluation loads the separately installed +Symbolica Python package lazily and remains subject to Symbolica's terms. + +The native build and installed-wheel tests verify that the f64 C API archive +does not depend on Python or the arbitrary-precision Symbolica runtime. + +## UFO Model Loader + +The `ufo-model-loader` dependency is maintained at + and retains its own license. +Release metadata pins the exact compatible published version. + +The loader is distributed under the MIT License. Its complete license is +shipped with the bundled dependency provenance. + +## GammaLoop Model Assets + +The distribution includes Standard Model, scalar contact, and scalar-gravity +UFO examples together with serialized JSON forms. Their source provenance, +authors, generators, and applicable licenses are recorded in +`src/pyamplicol/assets/models/PROVENANCE.toml`. + +Publishing is blocked if that provenance inventory is absent, incomplete, or +does not match the packaged asset checksums. + +The model sources were distributed with GammaLoop. GammaLoop's upstream +license says that there are no usage restrictions for GammaLoop; because that +statement has no standard SPDX identifier, these assets use the local +identifier `LicenseRef-GammaLoop-No-Restrictions`. The upstream text is +reproduced in `licenses/GammaLoop.txt`, and its asset-specific scope is +recorded in `licenses/GammaLoop-model-assets.txt`. The SM UFO identifies N. +Christensen and C. Duhr as authors. The scalar and scalar-gravity models +identify Valentin Hirschi as author and were generated with FeynRules 1.7.69. + +The SM UFO's optional `build_restrict.py` originates from +MadGraph5_aMC@NLO. Its adapted University of Illinois/NCSA license is +reproduced in `licenses/MadGraph5_aMCatNLO.txt`. Generated JSON forms retain +the provenance and terms of their corresponding UFO source. diff --git a/pyamplicol/build_backend/_pyamplicol_build.py b/pyamplicol/build_backend/_pyamplicol_build.py new file mode 100644 index 0000000000..7d0970e3b3 --- /dev/null +++ b/pyamplicol/build_backend/_pyamplicol_build.py @@ -0,0 +1,2060 @@ +# SPDX-License-Identifier: 0BSD +"""Read-only-source PEP 517 backend for pyAmpliCol.""" + +from __future__ import annotations + +import hashlib +import importlib.metadata +import importlib.util +import json +import os +import re +import runpy +import shutil +import stat +import subprocess +import sys +import tomllib +from collections.abc import Callable, Iterator, Mapping +from contextlib import contextmanager +from pathlib import Path, PurePosixPath +from tempfile import TemporaryDirectory +from typing import Any, TypeVar + +import maturin # type: ignore[import-untyped] +from native_build_identity import load_native_build_contract +from native_build_identity import ( + native_build_inputs_digest as _native_build_inputs_digest, +) +from package_version import ( + canonical_package_version, + check_contributor_lock_consistency, +) +from prepared_models import ( + discard_release_packaged_prepared_model_store, + project_release_packaged_prepared_model_store, + stage_packaged_prepared_models, +) +from sdk import build_sdk + +ROOT = Path(__file__).resolve().parents[1] +_CONTRIBUTOR_LOCK = Path("dependencies/contributor-lock.toml") +_RUNTIME_ARTIFACT_ID_PAYLOAD_ROLES = frozenset( + { + "compiled-model", + "evaluator-manifest", + "evaluator-state", + "model-parameters", + "runtime-physics", + } +) +_ARTIFACT_IDENTITY_CONTRACT = { + "kind": "pyamplicol-runtime-payload-identity", + "schema_version": 1, +} + + +def _require_artifact_identity_contract(manifest: Mapping[str, object]) -> None: + extensions = manifest.get("extensions") + if ( + not isinstance(extensions, dict) + or extensions.get("artifact_identity") != _ARTIFACT_IDENTITY_CONTRACT + ): + raise RuntimeError( + "process artifact lacks the current identity contract; regenerate it" + ) + + +def _runtime_artifact_id(manifest: Mapping[str, object]) -> str: + payloads = manifest.get("payloads") + if not isinstance(payloads, list): + raise RuntimeError("process artifact payload inventory is invalid") + records = [ + dict(payload) + for payload in payloads + if isinstance(payload, dict) + and payload.get("role") in _RUNTIME_ARTIFACT_ID_PAYLOAD_ROLES + ] + records.sort(key=lambda payload: str(payload.get("path", ""))) + canonical = ( + json.dumps( + { + "kind": "pyamplicol-runtime-payload-identity", + "schema_version": 1, + "payloads": records, + }, + sort_keys=True, + separators=(",", ":"), + allow_nan=False, + ) + + "\n" + ).encode("utf-8") + return hashlib.sha256(canonical).hexdigest() + + +ALLOWLIST = ( + ".gitattributes", + "Cargo.lock", + "Cargo.toml", + "LICENSE", + "README.md", + "THIRD_PARTY_NOTICES.md", + "build_backend", + "config", + "dependencies", + "docs", + "examples", + "justfile", + "licenses", + "pyproject.toml", + "release_assets", + "rust", + "rust-toolchain.toml", + "schemas", + "source_hash.py", + "src", + "tests", + "tools/ci/__init__.py", + "tools/ci/memory_watchdog.py", + "tools/developer", + "tools/fft_profiling", + "tools/performance_report", + "tools/release", + "tools/typing", +) +_PROFILING_CAMPAIGN_SOURCE = Path("src/pyamplicol/_profiling_campaign") +_PROFILING_CAMPAIGN_REQUIRED_FILES = ( + Path("README.md"), + Path("TABLE_FILLING.md"), + Path("build_pdf.py"), + Path("pyAmpliCol.tex"), + Path("report-workspace.json"), + Path("report_environment.json"), + Path("report_environment.tex"), + Path("result_tables.py"), + Path("result_validation_summary.tex"), + Path("steer_performance_campaign.py"), +) +_PROFILING_CAMPAIGN_GLOBS = ( + "result_*_table.tex", + "section_*.tex", + "results/*.json", +) +_PROFILING_CAMPAIGN_FILE_COUNT = 69 +_RELEASE_BUILD_INFO_FIELDS = { + "publishable", + "schema_version", + "selftest_fixture_bootstrap", + "source_revision", + "version", +} +_PERFORMANCE_REPORT_REQUIRED_MODULES = { + "__init__.py", + "cli.py", + "manual_campaign.py", + "scheduler.py", +} +_LEGACY_ORACLE_REQUIRED_MODULES = { + "__init__.py", + "checkout.py", + "model.py", + "probe.py", +} +IGNORED_NAMES = { + "__pycache__", + ".agent-work", + ".artifacts", + ".coverage", + ".DS_Store", + ".git", + ".mypy_cache", + ".pytest_cache", + ".ruff_cache", + ".trash", + ".venv", + "PYPI_DEPLOYMENT_TEST", + "build", + "checkouts", + "dist", + "htmlcov", + "wheelhouse", + "target", + "venv", +} +_EXCLUDED_TREES = ( + Path("dependencies/patches"), + Path("docs/.result_outputs"), + Path("docs/archive"), + Path("outputs"), + _PROFILING_CAMPAIGN_SOURCE / "campaign_artifacts", + Path("src/pyamplicol/_sdk/fortran"), + Path("src/pyamplicol/_sdk/include"), + Path("src/pyamplicol/_sdk/lib"), +) +_EXCLUDED_PATHS = { + Path(".cargo/config.toml"), + Path("build_backend/python_lock.py"), + Path("dependencies/candidate-Cargo.lock"), + Path("dependencies/candidate-cargo-config.toml"), + Path("dependencies/contributor-lock.toml"), + Path("dependencies/install_dependencies.py"), + Path("dependencies/install-state.json"), + Path("dependencies/python-runtime-lock.toml"), + Path("dependencies/symbolica_patches.tar.gz"), + Path("src/pyamplicol/_sdk/link.json"), + Path("src/pyamplicol/_sdk/metadata.json"), +} +_IGNORED_SUFFIXES = (".mod", ".pyc", ".pyd", ".pyo", ".whl") +_NATIVE_EXTENSION_SUFFIXES = (".dylib", ".pyd", ".so") +_TEX_BUILD_SUFFIXES = ( + ".aux", + ".bbl", + ".bcf", + ".blg", + ".fdb_latexmk", + ".fls", + ".log", + ".out", + ".run.xml", + ".synctex.gz", + ".toc", +) +# SymJIT Application v3 stores the program and MIR, then recompiles it in +# Application::load. Its wire format uses native usize fields, so this template +# is shared only by the release's 64-bit little-endian targets. +_PORTABLE_SELFTEST_TEMPLATE = "portable-64le" +_PORTABLE_SELFTEST_TARGETS = frozenset( + { + "aarch64-apple-darwin", + "x86_64-apple-darwin", + "x86_64-unknown-linux-gnu", + } +) +_SELFTEST_FIXTURE_BOOTSTRAP_CONTEXT = "build-current-candidate-site-v1" +_RELEASE_PREPARED_MODEL_BOOTSTRAP_CONTEXT = "release-prepared-model-producer-v1" +_INJECTION_ENVIRONMENT_NAMES = { + "AR", + "CARGO", + "C_INCLUDE_PATH", + "CC", + "CFLAGS", + "CPATH", + "CPLUS_INCLUDE_PATH", + "CXX", + "CXXFLAGS", + "DYLD_FALLBACK_LIBRARY_PATH", + "DYLD_INSERT_LIBRARIES", + "DYLD_LIBRARY_PATH", + "LD_LIBRARY_PATH", + "LDFLAGS", + "LIBRARY_PATH", + "MACOSX_DEPLOYMENT_TARGET", + "OBJC_INCLUDE_PATH", + "PKG_CONFIG_PATH", + "PYAMPLICOL_BUILD_CACHE_ROOT", + "PYAMPLICOL_BUILD_JOBS", + "PYAMPLICOL_BUILD_OVERLAY", + "PYAMPLICOL_CANDIDATE_CACHE_ROOT", + "PYAMPLICOL_NATIVE_BUILD_INPUTS_SHA256", + "PYAMPLICOL_PREPARED_MODEL_BOOTSTRAP", + "PYAMPLICOL_SELFTEST_FIXTURE_BOOTSTRAP", + "PYAMPLICOL_SELFTEST_FIXTURE_BOOTSTRAP_CONTEXT", + "PYAMPLICOL_SDK_STAGING", + "PYTHONHOME", + "PYTHONPATH", + "RANLIB", + "SDKROOT", +} +_INJECTION_ENVIRONMENT_PREFIXES = ( + "CARGO_", + "GIT_", + "MATURIN_", + "PYO3_", + "RUST", +) +_Result = TypeVar("_Result") +_delegation_depth = 0 + + +def _prepared_model_bootstrap(mode: str) -> bool: + """Return whether this non-publishable build only bootstraps pack creation.""" + + value = os.environ.get("PYAMPLICOL_PREPARED_MODEL_BOOTSTRAP", "0") + if value not in {"0", "1"}: + raise RuntimeError( + "PYAMPLICOL_PREPARED_MODEL_BOOTSTRAP must be either '0' or '1'" + ) + if value == "1" and mode != "candidate": + raise RuntimeError( + "prepared-model bootstrap is restricted to non-publishable candidate builds" + ) + return value == "1" + + +def _release_prepared_model_bootstrap( + mode: str, + context: str | None, +) -> bool: + """Authorize only the dedicated release prepared-model producer.""" + + if context is None: + return False + if mode != "release": + raise RuntimeError( + "release prepared-model bootstrap requires release dependency mode" + ) + if context != _RELEASE_PREPARED_MODEL_BOOTSTRAP_CONTEXT: + raise RuntimeError( + "release prepared-model bootstrap requires the explicit producer context" + ) + return True + + +def _selftest_fixture_bootstrap( + mode: str, + *, + explicit_context: bool = False, +) -> bool: + """Authorize the one-time candidate used to regenerate the self-test fixture.""" + + value = os.environ.get("PYAMPLICOL_SELFTEST_FIXTURE_BOOTSTRAP", "0") + context = os.environ.get("PYAMPLICOL_SELFTEST_FIXTURE_BOOTSTRAP_CONTEXT") + if value not in {"0", "1"}: + raise RuntimeError( + "PYAMPLICOL_SELFTEST_FIXTURE_BOOTSTRAP must be either '0' or '1'" + ) + if value == "0": + if context is not None: + raise RuntimeError( + "self-test fixture bootstrap context requires the bootstrap flag" + ) + return False + if mode != "candidate": + raise RuntimeError( + "self-test fixture bootstrap is restricted to non-publishable " + "candidate builds" + ) + if not explicit_context or context != _SELFTEST_FIXTURE_BOOTSTRAP_CONTEXT: + raise RuntimeError( + "self-test fixture bootstrap is restricted to the explicit " + "build-current-candidate-site context" + ) + return True + + +def _strip_prepared_model_payloads(overlay: Path) -> None: + """Remove stale bundles from a bootstrap wheel used only to create replacements.""" + + root = overlay / "src" / "pyamplicol" / "assets" / "prepared_models" + if not root.is_dir() or root.is_symlink(): + raise RuntimeError("prepared-model bootstrap input has no safe asset directory") + for path in root.iterdir(): + if path.name == "__init__.py": + continue + if path.is_symlink() or not path.is_file(): + raise RuntimeError( + f"prepared-model bootstrap found an unsafe asset entry: {path.name}" + ) + if path.suffix not in {".json", ".pyamplicol-model"}: + raise RuntimeError( + f"prepared-model bootstrap found an unexpected asset: {path.name}" + ) + path.unlink() + + +def _mark_selftest_fixture_bootstrap( + overlay: Path, + *, + prepared_model_recovery: bool = False, +) -> None: + """Mark a regeneration-only build and remove its stale self-test fixture. + + The standalone self-test recovery remains restricted to an exact clean + candidate revision. An explicit prepared-model recovery is necessarily + coupled to self-test regeneration, however, and may operate on a dirty + candidate checkout because its wheel is non-publishable and omits both + generated asset families. + """ + + path = overlay / "src" / "pyamplicol" / "_build_info.json" + try: + build_info = json.loads(path.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError) as error: + raise RuntimeError( + "self-test fixture bootstrap has no candidate build marker" + ) from error + if ( + not isinstance(build_info, dict) + or build_info.get("schema_version") != 1 + or build_info.get("publishable") is not False + or build_info.get("selftest_fixture_bootstrap") is not False + ): + raise RuntimeError( + "self-test fixture bootstrap requires an ordinary non-publishable " + "candidate marker" + ) + source_revision = build_info.get("source_revision") + if prepared_model_recovery: + release_recovery = ( + build_info.get("release_prepared_model_bootstrap") is True + and build_info.get("candidate_fingerprint") is None + and "source_revision" not in build_info + ) + candidate_fingerprint = build_info.get("candidate_fingerprint") + candidate_recovery = ( + build_info.get("release_prepared_model_bootstrap") is None + and isinstance(candidate_fingerprint, str) + and re.fullmatch(r"[0-9a-f]{12}", candidate_fingerprint) is not None + and "source_revision" in build_info + and ( + source_revision is None + or ( + isinstance(source_revision, str) + and re.fullmatch(r"[0-9a-f]{40}", source_revision) is not None + ) + ) + ) + if not (candidate_recovery or release_recovery): + raise RuntimeError( + "prepared-model recovery requires a candidate or release " + "prepared-model bootstrap marker" + ) + elif ( + not isinstance(source_revision, str) + or re.fullmatch(r"[0-9a-f]{40}", source_revision) is None + ): + raise RuntimeError( + "self-test fixture bootstrap requires an exact clean source revision" + ) + build_info["selftest_fixture_bootstrap"] = True + path.write_text( + json.dumps(build_info, indent=2, sort_keys=True, allow_nan=False) + "\n", + encoding="utf-8", + ) + + fixture_root = overlay / "src" / "pyamplicol" / "assets" / "selftest" + if not fixture_root.is_dir() or fixture_root.is_symlink(): + raise RuntimeError( + "self-test fixture bootstrap input has no safe fixture directory" + ) + entries = tuple(fixture_root.iterdir()) + template = fixture_root / _PORTABLE_SELFTEST_TEMPLATE + if entries != (template,) or not template.is_dir() or template.is_symlink(): + raise RuntimeError( + "self-test fixture bootstrap input has an unexpected fixture inventory" + ) + shutil.rmtree(template) + + +def _build_mode() -> str: + value = os.environ.get("PYAMPLICOL_BUILD_MODE", "release") + if value not in {"candidate", "release"}: + raise RuntimeError("PYAMPLICOL_BUILD_MODE must be 'candidate' or 'release'") + return value + + +def _consume_host_wheel_setting( + config_settings: Mapping[str, Any] | None, +) -> tuple[Mapping[str, Any] | None, bool]: + """Remove the backend-only, explicitly opt-in host wheel setting.""" + + if config_settings is None or "pyamplicol.host-wheel" not in config_settings: + return config_settings, False + settings = dict(config_settings) + value = settings.pop("pyamplicol.host-wheel") + if value not in ("true", "false"): + raise RuntimeError("pyamplicol.host-wheel must be 'true' or 'false'") + return settings, value == "true" + + +def _release_wheel_config_settings( + config_settings: Mapping[str, Any] | None, +) -> Mapping[str, Any] | None: + """Use release manylinux tags unless a host-only wheel was requested.""" + + if config_settings is not None and ( + "maturin.build-args" in config_settings or "build-args" in config_settings + ): + raise RuntimeError( + "authenticated wheel builds do not accept caller-supplied Maturin " + "build arguments" + ) + config_settings, host_wheel = _consume_host_wheel_setting(config_settings) + if sys.platform != "linux" or (_build_mode() != "release" and not host_wheel): + return config_settings + settings = dict(config_settings or {}) + settings["maturin.build-args"] = [ + "--compatibility", + "linux" if host_wheel else "manylinux_2_28", + ] + return settings + + +def _check_dependencies(mode: str) -> None: + command = [ + sys.executable, + "-I", + str(ROOT / "tools" / "release" / "check_dependencies.py"), + ] + if mode == "candidate": + command.append("--candidate") + subprocess.run( + command, + cwd=ROOT, + env=_clean_environment(), + check=True, + ) + + +def _is_excluded(relative: Path) -> bool: + if any( + part in IGNORED_NAMES or part.endswith(".egg-info") for part in relative.parts + ): + return True + if relative in _EXCLUDED_PATHS or any( + relative.is_relative_to(tree) for tree in _EXCLUDED_TREES + ): + return True + if relative.name.endswith(_IGNORED_SUFFIXES): + return True + if ( + relative.parent == Path("src/pyamplicol") + and relative.name.startswith("_rusticol") + and relative.name.endswith(_NATIVE_EXTENSION_SUFFIXES) + ): + return True + report_source = relative.is_relative_to(Path("docs")) or relative.is_relative_to( + _PROFILING_CAMPAIGN_SOURCE + ) + return report_source and relative.name.endswith(_TEX_BUILD_SUFFIXES) + + +def _reject_symlinks(path: Path, relative: Path = Path()) -> None: + if relative.parts and _is_excluded(relative): + return + if path.is_symlink(): + raise RuntimeError(f"build inputs may not be symlinks: {path}") + if path.is_dir(): + for child in sorted(path.iterdir(), key=lambda item: item.name): + child_relative = relative / child.name + if _is_excluded(child_relative): + continue + _reject_symlinks(child, child_relative) + + +def _copy_ignore(root: Path) -> Callable[[str, list[str]], set[str]]: + def ignore(directory: str, names: list[str]) -> set[str]: + relative = Path(directory).relative_to(root) + return {name for name in names if _is_excluded(relative / name)} + + return ignore + + +def _is_allowlisted(relative: Path) -> bool: + return any( + relative == Path(name) or Path(name) in relative.parents for name in ALLOWLIST + ) + + +# Inside MadGraph7 these come from the prepared-model release archive (see +# tools/release/prepared_models_archive.py) and are not tracked by Git. +_UNTRACKED_PREPARED_MODEL_DIRECTORIES = ( + Path("src/pyamplicol/assets/prepared_models"), + Path("release_assets/prepared_models"), +) +_UNTRACKED_PREPARED_MODEL_SUFFIXES = (".metadata.json", ".pyamplicol-model") +# Symlinks to files shared with the rest of MadGraph7; copied by content. +_SHARED_SYMLINKS = frozenset( + {Path("src/pyamplicol/assets/api_templates/umami/umami.h")} +) + + +def _in_git_checkout() -> bool: + """Whether ROOT is a Git checkout, or a tracked subdirectory of one.""" + + if os.path.lexists(ROOT / ".git"): + return True + completed = subprocess.run( + ["git", "-C", str(ROOT), "ls-files", "--error-unmatch", "pyproject.toml"], + env=_clean_environment(), + check=False, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + return completed.returncode == 0 + + +def _local_build() -> bool: + """Opt-in MadGraph7 installer build from a dirty tree or release tarball.""" + + return os.environ.get("PYAMPLICOL_LOCAL_BUILD", "0") == "1" + + +def _untracked_prepared_models() -> list[Path]: + files: list[Path] = [] + for directory in _UNTRACKED_PREPARED_MODEL_DIRECTORIES: + if not (ROOT / directory).is_dir(): + continue + for path in sorted((ROOT / directory).iterdir()): + if path.name.endswith(_UNTRACKED_PREPARED_MODEL_SUFFIXES): + files.append(directory / path.name) + return files + + +def _git_inventory(*, build_inputs_only: bool = True) -> list[Path] | None: + if not _in_git_checkout(): + return None + completed = subprocess.run( + ["git", "-C", str(ROOT), "ls-files", "--cached", "-z", "--"], + env=_clean_environment(), + check=True, + capture_output=True, + ) + files = [Path(os.fsdecode(item)) for item in completed.stdout.split(b"\0") if item] + if not files: + history = subprocess.run( + ["git", "-C", str(ROOT), "rev-parse", "--verify", "HEAD"], + env=_clean_environment(), + check=False, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + if history.returncode != 0: + # An unpacked source archive and the initial no-history bootstrap have + # no tracked-file inventory. Their allowlisted archive bytes are the + # complete source contract. + return None + inventory: list[Path] = [] + for relative in [*files, *_untracked_prepared_models()]: + if relative.is_absolute() or ".." in relative.parts: + raise RuntimeError(f"Git reported an unsafe build input: {relative}") + if not build_inputs_only or ( + _is_allowlisted(relative) and not _is_excluded(relative) + ): + inventory.append(relative) + return sorted(set(inventory), key=lambda path: path.as_posix()) + + +def _archive_inventory() -> list[Path]: + inventory: list[Path] = [] + + def visit(directory: Path, relative: Path) -> None: + for child in sorted(directory.iterdir(), key=lambda item: item.name): + child_relative = relative / child.name + if _is_excluded(child_relative): + continue + if child.is_symlink() and child_relative not in _SHARED_SYMLINKS: + raise RuntimeError(f"build inputs may not be symlinks: {child}") + if child.is_dir(): + visit(child, child_relative) + elif child.is_file(): + inventory.append(child_relative) + else: + raise RuntimeError(f"build inputs must be regular files: {child}") + + for name in ALLOWLIST: + relative = Path(name) + source = ROOT / relative + if _is_excluded(relative) or not os.path.lexists(source): + continue + if source.is_symlink(): + raise RuntimeError(f"build inputs may not be symlinks: {source}") + if source.is_dir(): + visit(source, relative) + elif source.is_file(): + inventory.append(relative) + else: + raise RuntimeError(f"build inputs must be regular files: {source}") + return inventory + + +def _reject_symlink_ancestors(path: Path) -> None: + relative = path.relative_to(ROOT) + current = ROOT + for part in relative.parts: + current /= part + try: + mode = current.lstat().st_mode + except FileNotFoundError: + return + if stat.S_ISLNK(mode) and current.relative_to(ROOT) not in _SHARED_SYMLINKS: + raise RuntimeError(f"build inputs may not be symlinks: {current}") + + +def _copy_allowlisted_source(destination: Path) -> None: + destination.mkdir(parents=True) + inventory = _git_inventory() + if inventory is None: + inventory = _archive_inventory() + for relative in inventory: + source = ROOT / relative + _reject_symlink_ancestors(source) + shared = relative in _SHARED_SYMLINKS + try: + mode = (source.stat() if shared else source.lstat()).st_mode + except FileNotFoundError: + # A tracked deletion remains deleted in the overlay. + continue + if not stat.S_ISREG(mode): + raise RuntimeError(f"build inputs must be regular files: {source}") + target = destination / relative + target.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(source, target, follow_symlinks=shared) + + +def _stage_source_hash(overlay: Path) -> None: + """Record the source hash that MadGraph7 compares with its source tree.""" + + compute = runpy.run_path(str(ROOT / "source_hash.py"))["compute_source_hash"] + target = overlay / "src" / "pyamplicol" / "_source_hash.py" + target.parent.mkdir(parents=True, exist_ok=True) + target.write_text(f'SOURCE_HASH = "{compute(ROOT)}"\n', encoding="utf-8") + + +def _stage_packaged_examples(overlay: Path) -> None: + source = overlay / "examples" + target = overlay / "src" / "pyamplicol" / "_examples" + if not source.is_dir(): + raise RuntimeError("wheel build input has no examples directory") + if target.exists(): + raise RuntimeError("wheel overlay already contains packaged examples") + shutil.copytree(source, target, ignore=_copy_ignore(overlay)) + + +def _profiling_campaign_inventory(source: Path) -> tuple[Path, ...]: + selected = set(_PROFILING_CAMPAIGN_REQUIRED_FILES) + for pattern in _PROFILING_CAMPAIGN_GLOBS: + selected.update(path.relative_to(source) for path in source.glob(pattern)) + missing = tuple( + relative + for relative in _PROFILING_CAMPAIGN_REQUIRED_FILES + if not (source / relative).is_file() + ) + if missing or len(selected) != _PROFILING_CAMPAIGN_FILE_COUNT: + detail = ", ".join(path.as_posix() for path in missing) + raise RuntimeError( + "wheel profiling campaign has an invalid inventory" + + (f": missing {detail}" if detail else "") + ) + for relative in selected: + path = source / relative + if path.is_symlink() or not path.is_file(): + raise RuntimeError( + f"wheel profiling campaign has an unsafe member: {relative}" + ) + return tuple(sorted(selected, key=lambda path: path.as_posix())) + + +def _stage_packaged_profiling_campaign(overlay: Path) -> None: + source = overlay / _PROFILING_CAMPAIGN_SOURCE + if not source.is_dir(): + raise RuntimeError("wheel build input has no profiling campaign directory") + _profiling_campaign_inventory(source) + + performance_source = overlay / "tools" / "performance_report" + watchdog_source = overlay / "tools" / "ci" / "memory_watchdog.py" + legacy_source = overlay / "tools" / "developer" / "legacy_amplicol.py" + oracle_source = overlay / "tools" / "developer" / "legacy_oracle" + runtime_target = overlay / "src" / "pyamplicol" / "_performance_report" + performance_modules = tuple(sorted(performance_source.glob("*.py"))) + oracle_modules = tuple(sorted(oracle_source.glob("*.py"))) + if ( + not _PERFORMANCE_REPORT_REQUIRED_MODULES.issubset( + {path.name for path in performance_modules} + ) + or not _LEGACY_ORACLE_REQUIRED_MODULES.issubset( + {path.name for path in oracle_modules} + ) + or not watchdog_source.is_file() + or not legacy_source.is_file() + ): + raise RuntimeError("wheel build input has incomplete profiling runtime sources") + for path in (*performance_modules, watchdog_source, legacy_source, *oracle_modules): + if path.is_symlink() or not path.is_file(): + raise RuntimeError(f"wheel profiling runtime has an unsafe member: {path}") + if runtime_target.exists(): + raise RuntimeError("wheel overlay already contains a profiling runtime") + runtime_target.mkdir(parents=True) + for path in performance_modules: + shutil.copy2(path, runtime_target / path.name) + shutil.copy2(watchdog_source, runtime_target / "_memory_watchdog.py") + developer_target = runtime_target / "_developer" + developer_target.mkdir() + (developer_target / "__init__.py").write_text( + "# SPDX-License-Identifier: 0BSD\n", + encoding="utf-8", + ) + shutil.copy2(legacy_source, developer_target / legacy_source.name) + oracle_target = developer_target / "legacy_oracle" + oracle_target.mkdir() + for path in oracle_modules: + shutil.copy2(path, oracle_target / path.name) + + +def _stage_python_stub(overlay: Path) -> None: + source = ( + overlay + / "rust" + / "crates" + / "rusticol-python" + / "stubs" + / "pyamplicol" + / "_rusticol.pyi" + ) + target = overlay / "src" / "pyamplicol" / "_rusticol.pyi" + if not source.is_file(): + raise RuntimeError("wheel build input has no maintained Rusticol stub") + if target.exists(): + raise RuntimeError("wheel overlay already contains the Rusticol stub") + shutil.copy2(source, target) + + +def _stage_runtime_resources(overlay: Path) -> None: + """Place wheel-owned schemas below the pyamplicol package namespace.""" + + package_assets = overlay / "src" / "pyamplicol" / "assets" + sources = { + overlay / "schemas" / "README.md": package_assets / "schemas" / "README.md", + overlay / "schemas" / "artifact-manifest-v3.schema.json": ( + package_assets / "schemas" / "artifact-manifest-v3.schema.json" + ), + overlay / "schemas" / "runtime-physics-v1.schema.json": ( + package_assets / "schemas" / "runtime-physics-v1.schema.json" + ), + } + for source, target in sources.items(): + if not source.is_file(): + raise RuntimeError( + f"wheel build input is missing runtime resource: {source}" + ) + if target.exists(): + raise RuntimeError( + f"wheel overlay already contains runtime resource: {target}" + ) + target.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(source, target) + + +def _portable_selftest_validators(source_root: Path) -> tuple[Any, Any]: + """Load the dependency-free release contract from the source being built.""" + + source = source_root / "tools" / "release" / "prepare_selftest_fixture.py" + spec = importlib.util.spec_from_file_location( + "_pyamplicol_portable_selftest_contract", + source, + ) + if spec is None or spec.loader is None: + raise RuntimeError("could not load the portable self-test contract") + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + artifact_validator = getattr( + module, + "validate_portable_artifact_capabilities", + None, + ) + execution_validator = getattr( + module, + "validate_portable_execution_manifest", + None, + ) + if not callable(artifact_validator) or not callable(execution_validator): + raise RuntimeError("portable self-test contract has incomplete validators") + return artifact_validator, execution_validator + + +def _validate_portable_selftest_executions( + fixture: Path, + manifest: Mapping[str, object], + *, + source_root: Path, +) -> None: + """Fail the build before staging a legacy or partial compiled fixture.""" + + validate_artifact, validate_execution = _portable_selftest_validators(source_root) + validate_artifact( + manifest, + context="portable self-test artifact manifest", + ) + + payloads = manifest.get("payloads") + if not isinstance(payloads, list): + raise RuntimeError("portable self-test payload inventory is invalid") + execution_payloads = [ + payload + for payload in payloads + if isinstance(payload, dict) + and payload.get("role") == "evaluator-manifest" + and str(payload.get("path", "")).endswith("/execution.json") + ] + if not execution_payloads: + raise RuntimeError("portable self-test has no compiled execution manifest") + + for payload in execution_payloads: + relative = payload.get("path") + if not isinstance(relative, str): + raise RuntimeError("portable self-test execution manifest has no path") + path = Path(relative) + if path.is_absolute() or any(part in {"", ".", ".."} for part in path.parts): + raise RuntimeError("portable self-test execution-manifest path is unsafe") + execution_path = fixture / "artifact" / path + try: + execution = json.loads(execution_path.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError) as error: + raise RuntimeError( + f"portable self-test execution manifest is invalid: {relative}" + ) from error + if not isinstance(execution, dict): + raise RuntimeError( + f"portable self-test execution manifest is invalid: {relative}" + ) + validate_execution( + execution, + context=f"portable self-test execution manifest {relative}", + ) + + +def _stage_selftest_fixture(overlay: Path, target: str) -> None: + """Materialize the portable MIR fixture for the wheel's Rust target.""" + + if target not in _PORTABLE_SELFTEST_TARGETS: + raise RuntimeError(f"unsupported self-test target: {target}") + + with (overlay / "Cargo.toml").open("rb") as stream: + cargo = tomllib.load(stream) + try: + cargo_version = cargo["workspace"]["package"]["version"] + except (KeyError, TypeError) as error: + raise RuntimeError("build overlay has no workspace package version") from error + if not isinstance(cargo_version, str) or not cargo_version: + raise RuntimeError("build overlay workspace package version is invalid") + package_version = cargo_version.replace("-dev.", ".dev") + fixture_root = overlay / "src" / "pyamplicol" / "assets" / "selftest" + if not fixture_root.is_dir(): + raise RuntimeError("wheel build input has no portable self-test fixture") + template = fixture_root / _PORTABLE_SELFTEST_TEMPLATE + if not template.is_dir() or template.is_symlink(): + raise RuntimeError("wheel build input has no portable 64-bit self-test fixture") + for candidate in fixture_root.iterdir(): + if candidate == template: + continue + raise RuntimeError(f"unexpected source self-test fixture: {candidate}") + selected = fixture_root / target + shutil.copytree(template, selected, symlinks=False) + shutil.rmtree(template) + expected_path = selected / "expected.json" + try: + expected = json.loads(expected_path.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError) as error: + raise RuntimeError(f"invalid self-test expectation: {expected_path}") from error + if ( + not isinstance(expected, dict) + or expected.get("target") != _PORTABLE_SELFTEST_TEMPLATE + or expected.get("compatible_targets") != sorted(_PORTABLE_SELFTEST_TARGETS) + or expected.get("serialization") + != { + "endianness": "little", + "kind": "symjit-application-mir-v3", + "load_behavior": "recompile-mir-for-loading-host", + "source_optimization_level": 2, + "word_size_bits": 64, + } + ): + raise RuntimeError("portable self-test expectation is invalid") + expected["target"] = target + expected_path.write_text( + json.dumps(expected, indent=2, sort_keys=True, allow_nan=False) + "\n", + encoding="utf-8", + ) + path = selected / "artifact" / "artifact.json" + try: + manifest = json.loads(path.read_text(encoding="utf-8")) + _require_artifact_identity_contract(manifest) + manifest["producer"]["version"] = package_version + manifest["runtime"]["engine_version"] = package_version + producer_target = manifest["producer"]["target"] + if ( + not isinstance(producer_target, dict) + or producer_target.get("triple") != _PORTABLE_SELFTEST_TEMPLATE + ): + raise RuntimeError("portable self-test producer target is invalid") + producer_target["triple"] = target + producer_target["cpu_features"] = [] + payloads = manifest["payloads"] + if not isinstance(payloads, list): + raise RuntimeError("portable self-test payload inventory is invalid") + _validate_portable_selftest_executions( + selected, + manifest, + source_root=overlay, + ) + evaluator_targets = 0 + for payload in payloads: + if not isinstance(payload, dict): + raise RuntimeError("portable self-test payload entry is invalid") + payload_target = payload.get("target") + if payload_target is None: + continue + if ( + not isinstance(payload_target, dict) + or payload_target.get("triple") != _PORTABLE_SELFTEST_TEMPLATE + ): + raise RuntimeError("portable self-test payload target is invalid") + payload_target["triple"] = target + payload_target["cpu_features"] = [] + evaluator_targets += 1 + if evaluator_targets == 0: + raise RuntimeError( + "portable self-test has no target-tagged evaluator state" + ) + compiled_payloads = [ + payload + for payload in payloads + if isinstance(payload, dict) and payload.get("role") == "compiled-model" + ] + if len(compiled_payloads) != 1: + raise RuntimeError("portable self-test must contain one compiled model") + compiled_payload = compiled_payloads[0] + compiled_relative = compiled_payload.get("path") + if not isinstance(compiled_relative, str): + raise RuntimeError("portable self-test compiled model has no path") + compiled_path = Path(compiled_relative) + if compiled_path.is_absolute() or any( + part in {"", ".", ".."} for part in compiled_path.parts + ): + raise RuntimeError("portable self-test compiled-model path is unsafe") + compiled_path = selected / "artifact" / compiled_path + compiled_model = json.loads(compiled_path.read_text(encoding="utf-8")) + if not isinstance(compiled_model, dict): + raise RuntimeError("portable self-test compiled model is invalid") + compiled_producer = compiled_model.get("producer") + if not isinstance(compiled_producer, dict): + raise RuntimeError("portable self-test compiled model is invalid") + compiled_producer["pyamplicol"] = package_version + compiled_data = ( + json.dumps( + compiled_model, + sort_keys=True, + separators=(",", ":"), + allow_nan=False, + ) + + "\n" + ).encode("utf-8") + compiled_path.write_bytes(compiled_data) + compiled_payload["sha256"] = hashlib.sha256(compiled_data).hexdigest() + compiled_payload["size_bytes"] = len(compiled_data) + except (KeyError, OSError, TypeError, json.JSONDecodeError) as error: + raise RuntimeError(f"invalid self-test artifact manifest: {path}") from error + manifest["artifact_id"] = _runtime_artifact_id(manifest) + path.write_text( + json.dumps( + manifest, + sort_keys=True, + separators=(",", ":"), + allow_nan=False, + ) + + "\n", + encoding="utf-8", + ) + + +def _mark_candidate( + overlay: Path, + base_version: str, + *, + native_build_inputs_sha256: str, +) -> None: + if not (ROOT / _CONTRIBUTOR_LOCK).is_file(): + raise RuntimeError("candidate build has no contributor dependency contract") + check_contributor_lock_consistency(ROOT) + candidate_lock, candidate_config, _installer_state = _candidate_inputs() + if re.fullmatch(r"[0-9a-f]{64}", native_build_inputs_sha256) is None: + raise RuntimeError("candidate build has an invalid native source identity") + candidate_fingerprint = native_build_inputs_sha256[:12] + cargo_version = f"{base_version}-dev.0+candidate.{candidate_fingerprint}" + python_version = cargo_version.replace("-dev.", ".dev") + shutil.copy2(candidate_lock, overlay / "Cargo.lock") + overlay_config = overlay / ".cargo" / "config.toml" + overlay_config.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(candidate_config, overlay_config) + _rewrite_candidate_dependency_requirements(overlay) + cargo = overlay / "Cargo.toml" + text = cargo.read_text(encoding="utf-8") + updated, count = re.subn( + rf'(?m)^version = "{re.escape(base_version)}"$', + f'version = "{cargo_version}"', + text, + count=1, + ) + if count != 1: + raise RuntimeError("could not derive candidate version from Cargo.toml") + cargo.write_text(updated, encoding="utf-8") + + lock = overlay / "Cargo.lock" + lock_text = lock.read_text(encoding="utf-8") + for package_name in ( + "rusticol-capi", + "rusticol-core", + "rusticol-python", + ): + pattern = ( + rf'(?m)(\[\[package\]\]\nname = "{package_name}"\n)' + rf'version = "{re.escape(base_version)}"' + ) + lock_text, count = re.subn( + pattern, + rf'\g<1>version = "{cargo_version}"', + lock_text, + count=1, + ) + if count != 1: + raise RuntimeError( + f"could not derive candidate lock entry for {package_name}" + ) + lock.write_text(lock_text, encoding="utf-8") + + package = overlay / "src" / "pyamplicol" + package.mkdir(parents=True, exist_ok=True) + source_revision = _clean_source_revision(allow_generated_report_outputs=True) + (package / "_build_info.json").write_text( + json.dumps( + { + "schema_version": 1, + "publishable": False, + "candidate_fingerprint": candidate_fingerprint, + "native_build_inputs_sha256": native_build_inputs_sha256, + "selftest_fixture_bootstrap": False, + "source_checkout": str(ROOT.resolve()), + "source_revision": source_revision, + "version": python_version, + }, + indent=2, + sort_keys=True, + ) + + "\n", + encoding="utf-8", + ) + + +def _mark_release_prepared_model_bootstrap( + overlay: Path, + base_version: str, + *, + native_build_inputs_sha256: str, +) -> None: + """Mark a release-version wheel as regeneration-only and non-publishable.""" + + package = overlay / "src" / "pyamplicol" + package.mkdir(parents=True, exist_ok=True) + (package / "_build_info.json").write_text( + json.dumps( + { + "schema_version": 1, + "publishable": False, + "candidate_fingerprint": None, + "native_build_inputs_sha256": native_build_inputs_sha256, + "release_prepared_model_bootstrap": True, + "selftest_fixture_bootstrap": False, + "source_checkout": str(ROOT.resolve()), + "version": base_version, + }, + indent=2, + sort_keys=True, + allow_nan=False, + ) + + "\n", + encoding="utf-8", + ) + + +def _stage_release_build_info(overlay: Path, version: str) -> None: + """Create or retain the minimal provenance marker for a published release.""" + + path = overlay / "src" / "pyamplicol" / "_build_info.json" + if _local_build() and not path.exists(): + revision = _clean_source_revision(allow_dirty=True) or "0" * 40 + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text( + json.dumps( + { + "schema_version": 1, + "publishable": True, + "selftest_fixture_bootstrap": False, + "source_revision": revision, + "version": version, + }, + indent=2, + sort_keys=True, + allow_nan=False, + ) + + "\n", + encoding="utf-8", + ) + return + if _in_git_checkout(): + revision = _clean_source_revision() + if revision is None: + raise RuntimeError( + "publishable release artifacts require a clean Git source revision" + ) + payload: dict[str, object] = { + "schema_version": 1, + "publishable": True, + "selftest_fixture_bootstrap": False, + "source_revision": revision, + "version": version, + } + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text( + json.dumps(payload, indent=2, sort_keys=True, allow_nan=False) + "\n", + encoding="utf-8", + ) + return + + try: + payload = json.loads(path.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError) as error: + raise RuntimeError( + "release source archive has no retained build provenance" + ) from error + if ( + not isinstance(payload, dict) + or set(payload) != _RELEASE_BUILD_INFO_FIELDS + or payload.get("schema_version") != 1 + or payload.get("publishable") is not True + or payload.get("selftest_fixture_bootstrap") is not False + or payload.get("version") != version + or not isinstance(payload.get("source_revision"), str) + or re.fullmatch(r"[0-9a-f]{40}", payload["source_revision"]) is None + ): + raise RuntimeError("release source archive has invalid build provenance") + + +def _generated_report_status_only(status: bytes) -> bool: + """Recognize only report outputs already excluded by campaign identity.""" + + records = status.split(b"\0") + if ( + not records + or records[-1] + or not all(len(record) >= 4 and record[2:3] == b" " for record in records[:-1]) + ): + return False + entries = tuple( + (record[:2], os.fsdecode(record[3:])) for record in records[:-1] + ) + paths = tuple(path for _state, path in entries) + classifier_path = Path("tools/performance_report/source_identity.py") + if classifier_path.as_posix() in paths: + return False + try: + namespace = runpy.run_path(str(ROOT / classifier_path)) + classifier = namespace.get("_generated_report_path") + except (OSError, RuntimeError, TypeError, ValueError): + return False + if not callable(classifier): + return False + try: + tracked = _git_inventory(build_inputs_only=False) + if tracked is None: + return False + copied_campaign_roots = _untracked_profiling_campaign_roots( + entries, + tracked=tracked, + ) + return all( + classifier(path) is True + or ( + state == b"??" + and any( + len(PurePosixPath(path).parts) > len(root) + and PurePosixPath(path).parts[: len(root)] == root + for root in copied_campaign_roots + ) + ) + for state, path in entries + ) + except (OSError, RuntimeError, TypeError, ValueError): + return False + + +def _untracked_profiling_campaign_roots( + entries: tuple[tuple[bytes, str], ...], + *, + tracked: list[Path], +) -> set[tuple[str, ...]]: + """Recognize roots of user-owned untracked copied report campaigns. + + ``pyamplicol profiling-campaign copy`` is intentionally useful inside a + contributor checkout. Its files are trusted steering inputs and outputs, + but the candidate overlay is built exclusively from Git-tracked inventory, + so they cannot alter its Python or native runtime. This exception is + applied only to ``??`` status records; edits to tracked report sources + remain source dirt. + """ + + markers: dict[tuple[str, ...], set[str]] = {} + for state, value in entries: + path = PurePosixPath(value) + parts = path.parts + if ( + state != b"??" + or path.is_absolute() + or len(parts) < 2 + or any(part in {"", ".", ".."} for part in parts) + or parts[-1] + not in {"report-workspace.json", "steer_performance_campaign.py"} + ): + continue + markers.setdefault(parts[:-1], set()).add(parts[-1]) + required = {"report-workspace.json", "steer_performance_campaign.py"} + tracked_parts = tuple( + PurePosixPath(path.as_posix()).parts for path in tracked + ) + return { + root + for root, names in markers.items() + if required <= names + and not any( + len(parts) >= len(root) and parts[: len(root)] == root + for parts in tracked_parts + ) + } + + +def _clean_source_revision( + *, + allow_generated_report_outputs: bool = False, + allow_dirty: bool = False, +) -> str | None: + """Return the exact source revision only for a clean Git checkout. + + Candidate wheels remain useful for ordinary dirty-tree development, where + this field is ``null``. Strict reference capture rejects those wheels and + therefore cannot accidentally certify uncommitted or ambient source. + """ + + if not _in_git_checkout(): + return None + environment = _clean_environment() + try: + prefix = subprocess.run( + ["git", "rev-parse", "--show-prefix"], + cwd=ROOT, + env=environment, + check=True, + capture_output=True, + ).stdout.strip() + # Only this project's subtree counts; paths are made ROOT-relative. + status = subprocess.run( + [ + "git", + "status", + "--porcelain=v1", + "-z", + "--untracked-files=all", + "--no-renames", + "--", + ".", + ], + cwd=ROOT, + env=environment, + check=True, + capture_output=True, + ).stdout + status = b"\0".join( + record[:3] + record[3 + len(prefix) :] + if record[3:].startswith(prefix) + else record + for record in status.split(b"\0") + ) + if status and not allow_dirty and ( + not allow_generated_report_outputs + or not _generated_report_status_only(status) + ): + return None + revision = subprocess.run( + ["git", "rev-parse", "HEAD"], + cwd=ROOT, + env=environment, + check=True, + capture_output=True, + text=True, + ).stdout.strip() + except (OSError, subprocess.CalledProcessError): + return None + return revision if re.fullmatch(r"[a-f0-9]{40}", revision) else None + + +def _locked_package_version(lock_path: Path, package_name: str) -> str: + try: + with lock_path.open("rb") as stream: + payload = tomllib.load(stream) + except (OSError, tomllib.TOMLDecodeError) as error: + raise RuntimeError(f"invalid candidate Cargo lock: {error}") from error + packages = payload.get("package") if isinstance(payload, dict) else None + matches = ( + [package for package in packages if package.get("name") == package_name] + if isinstance(packages, list) + and all(isinstance(package, dict) for package in packages) + else [] + ) + if ( + len(matches) != 1 + or not isinstance(matches[0].get("version"), str) + or not matches[0]["version"] + ): + raise RuntimeError( + f"candidate Cargo lock must contain exactly one {package_name} package" + ) + return str(matches[0]["version"]) + + +def _rewrite_candidate_dependency_requirements(overlay: Path) -> None: + """Use managed candidate native dependencies only in the build overlay.""" + + candidate_symbolica = _locked_package_version( + overlay / "Cargo.lock", + "symbolica", + ) + manifest = overlay / "rust" / "crates" / "rusticol-core" / "Cargo.toml" + text = manifest.read_text(encoding="utf-8") + pattern = r'(?m)^(symbolica\s*=\s*\{\s*version\s*=\s*)"=[^"]+"' + text, count = re.subn( + pattern, + rf'\g<1>"={candidate_symbolica}"', + text, + count=1, + ) + if count != 1: + raise RuntimeError( + "could not project rusticol-core Symbolica requirement from the " + "candidate Cargo lock" + ) + manifest.write_text(text, encoding="utf-8") + + with (overlay / "dependencies" / "release-lock.toml").open("rb") as stream: + release = tomllib.load(stream) + with (ROOT / _CONTRIBUTOR_LOCK).open("rb") as stream: + contributor = tomllib.load(stream) + python_manifest = overlay / "pyproject.toml" + python_text = python_manifest.read_text(encoding="utf-8") + published_python = str(release["symbolica"]["python_version"]) + candidate_python = str(contributor["symbolica"]["candidate_version"]) + python_text, count = re.subn( + rf'(?m)^(\s*"symbolica==){re.escape(published_python)}(",\s*)$', + rf"\g<1>{candidate_python}\g<2>", + python_text, + count=1, + ) + if count != 1: + raise RuntimeError( + "could not project Python Symbolica requirement " + f"from {published_python} to candidate {candidate_python}" + ) + python_manifest.write_text(python_text, encoding="utf-8") + + +def _candidate_inputs() -> tuple[Path, Path, Path]: + lock = ROOT / "dependencies" / "candidate-Cargo.lock" + config = ROOT / "dependencies" / "candidate-cargo-config.toml" + state = ROOT / "dependencies" / "install-state.json" + missing = [path for path in (lock, config, state) if not path.is_file()] + if missing: + raise RuntimeError( + "candidate build inputs are missing; run 'just dev-install': " + + ", ".join(str(path) for path in missing) + ) + return lock, config, state + + +def _stage_cargo_inputs( + overlay: Path, + mode: str, + *, + native_build_inputs_sha256: str | None, +) -> None: + """Apply managed path overrides only to contributor builds.""" + + if mode not in {"candidate", "release"}: + raise RuntimeError(f"unsupported Cargo input mode: {mode}") + lock = overlay / "Cargo.lock" + if not lock.is_file(): + raise RuntimeError("build overlay has no canonical Cargo.lock") + config = overlay / ".cargo" / "config.toml" + if mode == "release": + if config.exists(): + raise RuntimeError( + "release build overlay contains an ambient Cargo patch configuration" + ) + return + if native_build_inputs_sha256 is None: + raise RuntimeError("candidate build has no native source identity") + _mark_candidate( + overlay, + canonical_package_version(overlay), + native_build_inputs_sha256=native_build_inputs_sha256, + ) + if not config.is_file(): + raise RuntimeError("candidate build overlay has no Cargo patch configuration") + + +def _candidate_cache_root(mode: str) -> Path | None: + """Return the one opt-in, workspace-local contributor build cache.""" + + if mode != "candidate": + return None + raw = os.environ.get("PYAMPLICOL_CANDIDATE_CACHE_ROOT") + if raw is None: + return None + candidate = Path(raw).expanduser() + expected = ROOT / ".artifacts" / "dev-install" + if not candidate.is_absolute() or candidate.resolve(strict=False) != expected: + raise RuntimeError( + "PYAMPLICOL_CANDIDATE_CACHE_ROOT must be the workspace-local " + f"developer cache: {expected}" + ) + candidate.mkdir(parents=True, exist_ok=True) + if candidate.is_symlink() or candidate.resolve(strict=True) != expected: + raise RuntimeError("candidate build cache may not use symbolic links") + return candidate + + +def _local_cache_root() -> Path | None: + """Return the persistent build directory of a MadGraph7 installer build. + + A fixed overlay and Cargo target directory keep rebuilds incremental. + """ + + if not _local_build(): + return None + raw = os.environ.get("PYAMPLICOL_BUILD_CACHE_ROOT") + if raw is None: + return None + root = Path(raw).expanduser() + if not root.is_absolute(): + raise RuntimeError("PYAMPLICOL_BUILD_CACHE_ROOT must be an absolute path") + root.mkdir(parents=True, exist_ok=True) + return root.resolve() + + +@contextmanager +def _candidate_cache_lock(cache_root: Path) -> Iterator[None]: + """Serialize refresh and use of the fixed contributor build overlay.""" + + try: + import fcntl + except ImportError as error: # pragma: no cover - unsupported build host + raise RuntimeError("candidate build caching requires a POSIX host") from error + lock_path = cache_root / "project-build.lock" + if lock_path.is_symlink(): + raise RuntimeError("candidate build cache lock may not be a symbolic link") + with lock_path.open("a+b") as stream: + fcntl.flock(stream.fileno(), fcntl.LOCK_EX) + try: + yield + finally: + fcntl.flock(stream.fileno(), fcntl.LOCK_UN) + + +def _populate_overlay( + source: Path, + mode: str, + *, + native_build_inputs_sha256: str | None, + release_prepared_model_bootstrap: bool, + project_release_prepared_models: bool, +) -> None: + _copy_allowlisted_source(source) + if mode == "release" and project_release_prepared_models: + project_release_packaged_prepared_model_store( + source, + require_store=_in_git_checkout() or _local_build(), + ) + else: + discard_release_packaged_prepared_model_store(source) + _stage_cargo_inputs( + source, + mode, + native_build_inputs_sha256=native_build_inputs_sha256, + ) + if release_prepared_model_bootstrap: + if native_build_inputs_sha256 is None: + raise RuntimeError( + "release prepared-model bootstrap has no native source identity" + ) + _mark_release_prepared_model_bootstrap( + source, + canonical_package_version(source), + native_build_inputs_sha256=native_build_inputs_sha256, + ) + + +@contextmanager +def _overlay( + mode: str, + *, + release_prepared_model_bootstrap: bool = False, + project_release_prepared_models: bool = False, + temporary_directory: Path | None = None, + cargo_target_directory: Path | None = None, +) -> Iterator[tuple[Path, Path]]: + native_build_inputs_sha256 = ( + _native_build_inputs_digest( + ROOT, + normalize_release_cargo_lock=release_prepared_model_bootstrap, + ) + if mode == "candidate" or release_prepared_model_bootstrap + else None + ) + cache_root = ( + _candidate_cache_root(mode) or _local_cache_root() + if temporary_directory is None and cargo_target_directory is None + else None + ) + if cache_root is not None: + with _candidate_cache_lock(cache_root): + build_root = cache_root / "project-build" + source = build_root / "source" + target = cache_root / "cargo-target" + cargo_home = cache_root / "cargo-home" + for path in (build_root, source, target, cargo_home): + if path.is_symlink(): + raise RuntimeError( + f"candidate build cache entry may not be a symlink: {path}" + ) + if source.exists(): + if not source.is_dir(): + raise RuntimeError( + "candidate build overlay path is not a directory" + ) + shutil.rmtree(source) + build_root.mkdir(parents=True, exist_ok=True) + _populate_overlay( + source, + mode, + native_build_inputs_sha256=native_build_inputs_sha256, + release_prepared_model_bootstrap=release_prepared_model_bootstrap, + project_release_prepared_models=project_release_prepared_models, + ) + yield source, target + return + + with TemporaryDirectory( + prefix="pyamplicol-build-", + dir=temporary_directory, + ) as temporary: + root = Path(temporary) + source = root / "source" + _populate_overlay( + source, + mode, + native_build_inputs_sha256=native_build_inputs_sha256, + release_prepared_model_bootstrap=release_prepared_model_bootstrap, + project_release_prepared_models=project_release_prepared_models, + ) + yield ( + source, + ( + root / "cargo-target" + if cargo_target_directory is None + else cargo_target_directory + ), + ) + + +@contextmanager +def _working_directory(path: Path) -> Iterator[None]: + previous = Path.cwd() + os.chdir(path) + try: + yield + finally: + os.chdir(previous) + + +def _clean_environment( + updates: Mapping[str, str] | None = None, +) -> dict[str, str]: + environment = { + name: value + for name, value in os.environ.items() + if name not in _INJECTION_ENVIRONMENT_NAMES + and not name.startswith(_INJECTION_ENVIRONMENT_PREFIXES) + } + # A project-local rustup (MadGraph7 installer) only selects where the + # pinned RUSTUP_TOOLCHAIN lives. + if "RUSTUP_HOME" in os.environ: + environment["RUSTUP_HOME"] = os.environ["RUSTUP_HOME"] + environment["PATH"] = _build_tool_path(os.environ.get("PATH", "")) + if updates: + environment.update(updates) + return environment + + +def _macos_native_build_updates() -> dict[str, str]: + """Select Apple's system compilers for authenticated macOS builds. + + An absolute ``CC`` also gives native build caches such as + ``gmp-mpfr-sys`` an unambiguous compiler identity. Without it, that + crate keys its cache by the unresolved name ``gcc`` and can reuse output + produced earlier by a package-manager GNU compiler despite the clean + build PATH. + """ + + if sys.platform != "darwin": + return {} + macos = load_native_build_contract(ROOT)["macos"] + return { + "CC": str(macos["cc"]), + "CXX": str(macos["cxx"]), + "MACOSX_DEPLOYMENT_TARGET": str(macos["deployment-target"]), + } + + +def _build_tool_path(inherited: str) -> str: + """Return a minimal build PATH that cannot select Homebrew/MacPorts tools.""" + + interpreter = Path(sys.executable) + # Keep the isolated build environment so Maturin's console script remains + # available, but never expose the base interpreter's package-manager bin. + # A venv Python may resolve into /opt/local or /opt/homebrew, where unrelated + # compiler wrappers would otherwise leak non-relocatable RPATHs into wheels. + directories: list[Path] = [interpreter.parent] + maturin_script = _maturin_console_script() + inherited_directories = { + Path(os.path.abspath(directory)) + for directory in inherited.split(os.pathsep) + if directory + } + if maturin_script is not None and maturin_script.parent in inherited_directories: + directories.append(maturin_script.parent) + for executable in ("cargo", "rustc"): + located = shutil.which(executable, path=inherited) + if located is None: + raise RuntimeError(f"required build tool is unavailable: {executable}") + path = Path(located) + directories.append(path.parent) + + system_directories = ( + [Path(path) for path in ("/usr/bin", "/bin", "/usr/sbin", "/sbin")] + if os.name == "posix" + else [] + ) + directories.extend(system_directories) + tool_search_path = os.pathsep.join( + [*(str(path) for path in system_directories), inherited] + ) + for executable in ("git", "cc", "clang", "ar", "ranlib", "nm"): + located = shutil.which(executable, path=tool_search_path) + if located is None: + continue + path = Path(located) + directories.append(path.parent) + if os.name != "posix": # Keep unsupported Windows source hooks usable. + directories.extend(Path(path) for path in inherited.split(os.pathsep) if path) + + unique: list[str] = [] + seen: set[str] = set() + for directory in directories: + rendered = str(directory) + if rendered in seen or not directory.is_dir(): + continue + seen.add(rendered) + unique.append(rendered) + return os.pathsep.join(unique) + + +def _maturin_console_script() -> Path | None: + """Locate the console script belonging to the imported Maturin package.""" + + package = importlib.metadata.distribution("maturin") + for relative in package.files or (): + if relative.name not in {"maturin", "maturin.exe"}: + continue + candidate = Path(os.path.abspath(package.locate_file(relative))) + if candidate.is_file(): + return candidate + # `pip install --target DIR` puts the script in DIR/bin, which its RECORD + # does not describe. + candidate = Path(maturin.__file__).resolve().parents[1] / "bin" / "maturin" + return candidate if candidate.is_file() else None + + +def _pinned_rustup_toolchain() -> str: + """Return the repository-owned Rustup toolchain selection.""" + + path = ROOT / "rust-toolchain.toml" + try: + toolchain = tomllib.loads(path.read_text(encoding="utf-8"))["toolchain"] + channel = toolchain["channel"] + except (KeyError, OSError, TypeError, tomllib.TOMLDecodeError) as error: + raise RuntimeError( + f"invalid repository Rust toolchain contract: {path}" + ) from error + if not isinstance(channel, str) or not channel.strip(): + raise RuntimeError(f"invalid repository Rust toolchain channel: {path}") + return channel + + +def _rust_remap_flags(overlay: Path, target_dir: Path) -> str: + completed = subprocess.run( + ["rustc", "--print", "sysroot"], + check=True, + capture_output=True, + text=True, + env=_clean_environment({"RUSTUP_TOOLCHAIN": _pinned_rustup_toolchain()}), + ) + sysroot = Path(completed.stdout.strip()).resolve() + remapping = load_native_build_contract(ROOT)["rust-path-remapping"] + mappings = { + (ROOT / "dependencies" / "checkouts").resolve(): ( + str(remapping["candidate-checkouts"]) + ), + ROOT.resolve(): str(remapping["checkout"]), + overlay.resolve(): str(remapping["source"]), + target_dir.parent.resolve(): str(remapping["build"]), + sysroot: str(remapping["sysroot"]), + } + flags = [ + f"--remap-path-prefix={source}={destination}" + for source, destination in sorted( + mappings.items(), key=lambda item: len(str(item[0])), reverse=True + ) + ] + return "\x1f".join(flags) + + +@contextmanager +def _environment(updates: Mapping[str, str]) -> Iterator[None]: + previous = dict(os.environ) + isolated = _clean_environment(updates) + os.environ.clear() + os.environ.update(isolated) + try: + yield + finally: + os.environ.clear() + os.environ.update(previous) + + +@contextmanager +def _delegating() -> Iterator[None]: + global _delegation_depth + if _delegation_depth or "PYAMPLICOL_BUILD_OVERLAY" in os.environ: + raise RuntimeError("recursive PEP 517 backend delegation is not permitted") + _delegation_depth += 1 + try: + yield + finally: + _delegation_depth -= 1 + + +def _from_overlay( + operation: Callable[..., _Result], + *args: Any, + with_sdk: bool, + validate_prepared_models: bool = False, + retain_release_build_info: bool = False, + release_prepared_model_bootstrap_context: str | None = None, + **kwargs: Any, +) -> _Result: + with _delegating(): + mode = _build_mode() + prepared_model_bootstrap = _prepared_model_bootstrap(mode) + release_prepared_model_bootstrap = _release_prepared_model_bootstrap( + mode, + release_prepared_model_bootstrap_context, + ) + # A raw environment escape is never sufficient for a PEP 517 build. + # Only the dedicated local regeneration helper supplies the explicit + # context accepted by this gate. + _selftest_fixture_bootstrap(mode) + _check_dependencies(mode) + if release_prepared_model_bootstrap: + overlay_context = _overlay( + mode, + release_prepared_model_bootstrap=True, + ) + elif mode == "release": + overlay_context = _overlay( + mode, + project_release_prepared_models=True, + ) + else: + overlay_context = _overlay(mode) + with overlay_context as (overlay, target_dir): + if retain_release_build_info and mode == "release": + _stage_release_build_info( + overlay, + canonical_package_version(overlay), + ) + prepared_model_recovery = ( + prepared_model_bootstrap or release_prepared_model_bootstrap + ) + environment = { + "CARGO_HOME": str(target_dir.parent / "cargo-home"), + "CARGO_ENCODED_RUSTFLAGS": _rust_remap_flags(overlay, target_dir), + "CARGO_TARGET_DIR": str(target_dir), + "PYAMPLICOL_BUILD_OVERLAY": str(overlay), + "RUSTUP_TOOLCHAIN": _pinned_rustup_toolchain(), + } + jobs = os.environ.get("PYAMPLICOL_BUILD_JOBS") + if jobs: + environment["CARGO_BUILD_JOBS"] = jobs + build_info_path = overlay / "src" / "pyamplicol" / "_build_info.json" + if build_info_path.is_file(): + try: + build_info = json.loads(build_info_path.read_text(encoding="utf-8")) + native_digest = build_info.get("native_build_inputs_sha256") + except (OSError, TypeError, ValueError) as error: + raise RuntimeError( + "build provenance could not be exported to Rust" + ) from error + if native_digest is not None: + if not isinstance(native_digest, str) or not native_digest: + raise RuntimeError( + "build provenance contains an invalid native identity" + ) + environment["PYAMPLICOL_NATIVE_BUILD_INPUTS_SHA256"] = native_digest + else: + environment["PYAMPLICOL_NATIVE_BUILD_INPUTS_SHA256"] = ( + _native_build_inputs_digest( + ROOT, + normalize_release_cargo_lock=mode == "release", + ) + ) + else: + environment["PYAMPLICOL_NATIVE_BUILD_INPUTS_SHA256"] = ( + _native_build_inputs_digest( + ROOT, + normalize_release_cargo_lock=mode == "release", + ) + ) + environment.update(_macos_native_build_updates()) + with _environment(environment), _working_directory(overlay): + if validate_prepared_models and not with_sdk: + stage_packaged_prepared_models(overlay, mode) + if with_sdk: + _stage_source_hash(overlay) + _stage_packaged_examples(overlay) + _stage_packaged_profiling_campaign(overlay) + _stage_python_stub(overlay) + _stage_runtime_resources(overlay) + if prepared_model_recovery: + _strip_prepared_model_payloads(overlay) + _mark_selftest_fixture_bootstrap( + overlay, + prepared_model_recovery=True, + ) + else: + stage_packaged_prepared_models(overlay, mode) + print( + "* Building native C SDK (Cargo; cold builds can take " + "several minutes)...", + flush=True, + ) + sdk = build_sdk(overlay, target_dir) + print("* Native C SDK built.", flush=True) + sdk_metadata = json.loads( + (sdk / "metadata.json").read_text(encoding="utf-8") + ) + if not prepared_model_recovery: + _stage_selftest_fixture( + overlay, + str(sdk_metadata["target"]), + ) + os.environ["PYAMPLICOL_SDK_STAGING"] = str(sdk) + return operation(*args, **kwargs) + + +def build_wheel( + wheel_directory: str, + config_settings: Mapping[str, Any] | None = None, + metadata_directory: str | None = None, +) -> str: + config_settings = _release_wheel_config_settings(config_settings) + filename = _from_overlay( + maturin.build_wheel, + wheel_directory, + config_settings, + metadata_directory, + with_sdk=True, + retain_release_build_info=True, + ) + return filename + + +def build_release_prepared_model_bootstrap_wheel( + wheel_directory: str, + *, + bootstrap_context: str, + config_settings: Mapping[str, Any] | None = None, +) -> str: + """Build a release-version wheel usable only to regenerate prepared packs.""" + + config_settings = _release_wheel_config_settings(config_settings) + if sys.platform == "linux": + # This non-publishable wheel runs only on its producer, which need not + # be a manylinux image. The generated prepared packs remain portable. + config_settings = dict(config_settings or {}) + config_settings["maturin.build-args"] = ["--compatibility", "linux"] + return _from_overlay( + maturin.build_wheel, + wheel_directory, + config_settings, + None, + with_sdk=True, + release_prepared_model_bootstrap_context=bootstrap_context, + ) + + +def build_sdist( + sdist_directory: str, + config_settings: Mapping[str, Any] | None = None, +) -> str: + if _build_mode() == "candidate": + raise RuntimeError( + "candidate builds are wheel-only and cannot produce source distributions" + ) + return _from_overlay( + maturin.build_sdist, + sdist_directory, + config_settings, + with_sdk=False, + validate_prepared_models=True, + retain_release_build_info=True, + ) + + +def get_requires_for_build_wheel( + config_settings: Mapping[str, Any] | None = None, +) -> list[str]: + config_settings, _ = _consume_host_wheel_setting(config_settings) + return _from_overlay( + maturin.get_requires_for_build_wheel, + config_settings, + with_sdk=False, + ) + + +def get_requires_for_build_sdist( + config_settings: Mapping[str, Any] | None = None, +) -> list[str]: + return _from_overlay( + maturin.get_requires_for_build_sdist, + config_settings, + with_sdk=False, + ) + + +def prepare_metadata_for_build_wheel( + metadata_directory: str, + config_settings: Mapping[str, Any] | None = None, +) -> str: + config_settings, _ = _consume_host_wheel_setting(config_settings) + return _from_overlay( + maturin.prepare_metadata_for_build_wheel, + metadata_directory, + config_settings, + with_sdk=False, + ) diff --git a/pyamplicol/build_backend/native_build_identity.py b/pyamplicol/build_backend/native_build_identity.py new file mode 100644 index 0000000000..8ce4ea47ba --- /dev/null +++ b/pyamplicol/build_backend/native_build_identity.py @@ -0,0 +1,620 @@ +# SPDX-License-Identifier: 0BSD +"""Canonical source identity for the native pyAmpliCol build.""" + +from __future__ import annotations + +import hashlib +import json +import re +import tomllib +from collections.abc import Mapping +from pathlib import Path +from typing import Any + +_NATIVE_BUILD_IDENTITY_DOMAIN = b"pyamplicol-native-build-inputs-v2\0" +_NATIVE_BUILD_INPUT_FILES = ( + Path("Cargo.lock"), + Path("Cargo.toml"), + Path("pyproject.toml"), + Path("rust-toolchain.toml"), + Path("dependencies/candidate-Cargo.lock"), + Path("dependencies/candidate-cargo-config.toml"), + Path("dependencies/install-state.json"), +) +_NATIVE_BUILD_INPUT_TREES = (Path("rust"),) +# Release sdists omit candidate-only resolution state. +_RELEASE_OMITTED_NATIVE_BUILD_INPUTS = frozenset( + { + Path("dependencies/candidate-Cargo.lock"), + Path("dependencies/candidate-cargo-config.toml"), + Path("dependencies/install-state.json"), + } +) +_NATIVE_MATURIN_CONFIG_KEYS = frozenset( + { + "all-features", + "bindings", + "config", + "features", + "include-debuginfo", + "manifest-path", + "module-name", + "no-default-features", + "profile", + "rustc-args", + "strip", + "target", + "targets", + "unstable-flags", + "zig", + } +) +_NON_OUTPUT_MATURIN_CONFIG_KEYS = frozenset( + { + "editable-profile", + "frozen", + "locked", + "pgo-command", + "target-dir", + "use-base-python", + } +) +_PACKAGING_MATURIN_CONFIG_KEYS = frozenset( + { + "auditwheel", + "compatibility", + "data", + "exclude", + "generate-ci", + "generate-stubs", + "include", + "include-import-lib", + "manylinux", + "python-packages", + "python-source", + "sbom", + "sdist-generator", + "skip-auditwheel", + "compression-enable-large-file-support", + "compression-level", + "compression-method", + "sbom-include", + } +) +_NATIVE_BUILD_IGNORED_TREE_PARTS = frozenset({".artifacts", "__pycache__", "target"}) +# This explicit inventory contains only separately compiled test modules, +# integration-test inputs, and Python/stub packaging files. Additions default +# to native inputs. Mixed production files with inline #[cfg(test)] modules are +# deliberately hashed at file granularity; stripping Rust syntax here would be +# a second, incomplete compiler frontend. +_NON_NATIVE_RUST_PATHS = frozenset( + { + Path("rust/crates/rusticol-capi/tests/eager_artifact.rs"), + Path("rust/crates/rusticol-capi/tests/runtime_selectors.rs"), + Path("rust/crates/rusticol-core/src/artifact_tests.rs"), + Path("rust/crates/rusticol-core/src/eager_layout_tests.rs"), + Path("rust/crates/rusticol-core/src/eager_lowering_v3_tests.rs"), + Path("rust/crates/rusticol-core/src/eager_plan_v3_pacbin_tests.rs"), + Path("rust/crates/rusticol-core/src/eager_runtime/plan_v3_tests.rs"), + Path("rust/crates/rusticol-core/src/engine/contraction_metadata_tests.rs"), + Path("rust/crates/rusticol-core/src/engine/eager_integration_tests.rs"), + Path("rust/crates/rusticol-core/src/engine/eager_v3_manifest_tests.rs"), + Path("rust/crates/rusticol-core/src/engine/quantum_number_flow_tests.rs"), + Path("rust/crates/rusticol-core/src/engine/recurrence_integration_tests.rs"), + Path("rust/crates/rusticol-core/src/engine/source_metadata_tests.rs"), + Path("rust/crates/rusticol-core/src/engine_tests.rs"), + Path("rust/crates/rusticol-core/src/metadata_tests.rs"), + Path("rust/crates/rusticol-core/src/pacbin_tests.rs"), + Path("rust/crates/rusticol-core/src/recurrence/direct_backend_tests.rs"), + Path("rust/crates/rusticol-core/src/recurrence/direct_codec_tests.rs"), + Path("rust/crates/rusticol-core/src/recurrence/direct_lowering_tests.rs"), + Path("rust/crates/rusticol-core/src/recurrence/direct_pacbin_tests.rs"), + Path("rust/crates/rusticol-core/src/recurrence/direct_plan_tests.rs"), + Path("rust/crates/rusticol-core/src/recurrence/direct_runtime_tests.rs"), + Path("rust/crates/rusticol-core/src/recurrence/tests.rs"), + Path("rust/crates/rusticol-core/tests/direct_arena_workspace_allocations.rs"), + Path("rust/crates/rusticol-core/tests/eager_runtime.rs"), + Path( + "rust/crates/rusticol-core/tests/fixtures/on_the_fly_query_parity_v1.json" + ), + Path( + "rust/crates/rusticol-core/tests/fixtures/recurrence_execution_hzz_full_v2.json" + ), + Path( + "rust/crates/rusticol-core/tests/fixtures/recurrence_execution_hzz_lc.json" + ), + Path( + "rust/crates/rusticol-core/tests/fixtures/recurrence_execution_hzz_nlc.json" + ), + Path("rust/crates/rusticol-core/tests/generated_artifact_odd_tails.rs"), + Path("rust/crates/rusticol-core/tests/recurrence_direct_arena_allocations.rs"), + Path("rust/crates/rusticol-python/stubs/pyamplicol/__init__.pyi"), + Path("rust/crates/rusticol-python/stubs/pyamplicol/_rusticol.pyi"), + Path("rust/crates/rusticol-python/tests/pyrightconfig.json"), + Path("rust/crates/rusticol-python/tests/stub_contract.rs"), + Path("rust/crates/rusticol-python/tests/typing_consumer.py"), + } +) +_NATIVE_BUILD_CONTRACT_KEYS = frozenset( + {"macos", "rust-path-remapping", "schema-version", "sdk"} +) +_RUST_PATH_REMAP_KEYS = frozenset( + {"build", "candidate-checkouts", "checkout", "source", "sysroot"} +) +_MACOS_NATIVE_BUILD_KEYS = frozenset({"cc", "cxx", "deployment-target"}) +_SDK_NATIVE_BUILD_KEYS = frozenset({"package", "profile", "rustc-codegen-arguments"}) +_CANDIDATE_CARGO_CONFIG = Path("dependencies/candidate-cargo-config.toml") +_CANDIDATE_INSTALL_STATE = Path("dependencies/install-state.json") +_CANDIDATE_PATCH_TARGETS = { + "graphica": "dependencies/checkouts/symbolica/lib/graphica", + "numerica": "dependencies/checkouts/symbolica/lib/numerica", + "symbolica": "dependencies/checkouts/symbolica", + "symjit": "dependencies/checkouts/symjit", +} +_NATIVE_CANDIDATE_SOURCE_NAMES = frozenset({"symbolica", "symjit"}) + + +def _canonical_json_bytes(payload: Any) -> bytes: + try: + return ( + json.dumps( + payload, + sort_keys=True, + separators=(",", ":"), + allow_nan=False, + ) + + "\n" + ).encode("utf-8") + except (TypeError, ValueError) as error: + raise RuntimeError( + f"candidate native build identity is not canonical JSON: {error}" + ) from error + + +def _native_build_contract(payload: Mapping[str, Any]) -> dict[str, Any]: + """Validate the one declarative contract consumed by native build code.""" + + tool = payload.get("tool") + pyamplicol = tool.get("pyamplicol") if isinstance(tool, Mapping) else None + contract = ( + pyamplicol.get("native-build") if isinstance(pyamplicol, Mapping) else None + ) + if not isinstance(contract, Mapping) or set(contract) != set( + _NATIVE_BUILD_CONTRACT_KEYS + ): + raise RuntimeError( + "native pyproject contract must contain exactly schema-version, " + "rust-path-remapping, macos, and sdk" + ) + schema_version = contract.get("schema-version") + if ( + not isinstance(schema_version, int) + or isinstance(schema_version, bool) + or schema_version != 1 + ): + raise RuntimeError("native pyproject contract must use schema-version 1") + + remapping = contract.get("rust-path-remapping") + if not isinstance(remapping, Mapping) or set(remapping) != set( + _RUST_PATH_REMAP_KEYS + ): + raise RuntimeError("native Rust path-remapping contract is incomplete") + canonical_remapping: dict[str, str] = {} + for key in sorted(_RUST_PATH_REMAP_KEYS): + value = remapping[key] + if not isinstance(value, str) or not value.startswith("/"): + raise RuntimeError( + f"native Rust path-remapping destination {key} must be absolute" + ) + canonical_remapping[key] = value + if len(set(canonical_remapping.values())) != len(canonical_remapping): + raise RuntimeError("native Rust path-remapping destinations must be unique") + + macos = contract.get("macos") + if not isinstance(macos, Mapping) or set(macos) != set(_MACOS_NATIVE_BUILD_KEYS): + raise RuntimeError("native macOS build contract is incomplete") + canonical_macos: dict[str, str] = {} + for key in sorted(_MACOS_NATIVE_BUILD_KEYS): + value = macos[key] + if not isinstance(value, str) or not value: + raise RuntimeError(f"native macOS build setting {key} is invalid") + canonical_macos[key] = value + if not canonical_macos["cc"].startswith("/") or not canonical_macos[ + "cxx" + ].startswith("/"): + raise RuntimeError("native macOS compilers must be absolute paths") + + sdk = contract.get("sdk") + if not isinstance(sdk, Mapping) or set(sdk) != set(_SDK_NATIVE_BUILD_KEYS): + raise RuntimeError("native SDK build contract is incomplete") + package = sdk.get("package") + profile = sdk.get("profile") + codegen = sdk.get("rustc-codegen-arguments") + if not isinstance(package, str) or not package: + raise RuntimeError("native SDK package must be a non-empty string") + if not isinstance(profile, str) or not profile: + raise RuntimeError("native SDK profile must be a non-empty string") + if ( + not isinstance(codegen, list) + or not codegen + or any(not isinstance(value, str) or not value for value in codegen) + ): + raise RuntimeError("native SDK rustc codegen arguments must be strings") + return { + "macos": canonical_macos, + "rust-path-remapping": canonical_remapping, + "schema-version": 1, + "sdk": { + "package": package, + "profile": profile, + "rustc-codegen-arguments": list(codegen), + }, + } + + +def canonical_native_pyproject_bytes(data: bytes) -> bytes: + """Project only semantic inputs to the authenticated native build.""" + + try: + payload = tomllib.loads(data.decode("utf-8")) + except (UnicodeDecodeError, tomllib.TOMLDecodeError) as error: + raise RuntimeError( + f"invalid native pyproject configuration: {error}" + ) from error + build_system = payload.get("build-system") + requirements = ( + build_system.get("requires") if isinstance(build_system, Mapping) else None + ) + if not isinstance(requirements, list) or any( + not isinstance(requirement, str) for requirement in requirements + ): + raise RuntimeError("native pyproject build-system requires must be a list") + maturin_pins: list[str] = [] + for requirement in requirements: + match = re.fullmatch( + r"\s*maturin\s*==\s*([0-9][A-Za-z0-9._+-]*)\s*", + requirement, + flags=re.IGNORECASE, + ) + if match is not None: + maturin_pins.append(f"maturin=={match.group(1)}") + elif re.match(r"\s*maturin(?:\W|$)", requirement, flags=re.IGNORECASE): + raise RuntimeError( + "native Maturin build requirement must be exactly pinned" + ) + if len(maturin_pins) != 1: + raise RuntimeError( + "native pyproject must contain exactly one pinned Maturin requirement" + ) + + tool = payload.get("tool") + maturin = tool.get("maturin") if isinstance(tool, Mapping) else None + if not isinstance(maturin, Mapping): + raise RuntimeError("native pyproject Maturin configuration must be a table") + classified = ( + _NATIVE_MATURIN_CONFIG_KEYS + | _NON_OUTPUT_MATURIN_CONFIG_KEYS + | _PACKAGING_MATURIN_CONFIG_KEYS + ) + unknown = set(maturin) - classified + if unknown: + rendered = ", ".join(sorted(str(key) for key in unknown)) + raise RuntimeError(f"unclassified Maturin configuration: {rendered}") + return _canonical_json_bytes( + { + "build-system": {"requires": maturin_pins}, + "tool": { + "maturin": { + key: maturin[key] + for key in sorted(_NATIVE_MATURIN_CONFIG_KEYS & set(maturin)) + }, + "pyamplicol": {"native-build": _native_build_contract(payload)}, + }, + } + ) + + +def load_native_build_contract(root: Path) -> dict[str, Any]: + """Load the canonical backend knobs from the hashed pyproject input.""" + + path = root / "pyproject.toml" + try: + payload = tomllib.loads(path.read_text(encoding="utf-8")) + except (OSError, tomllib.TOMLDecodeError) as error: + raise RuntimeError(f"invalid native build contract: {path}: {error}") from error + return _native_build_contract(payload) + + +def _canonical_checkout_path(raw: str) -> str: + normalized = raw.replace("\\", "/").rstrip("/") + if normalized in {"TMP_FIXED_SYMJIT", "./TMP_FIXED_SYMJIT"} or normalized.endswith( + "/TMP_FIXED_SYMJIT" + ): + return "TMP_FIXED_SYMJIT" + marker = "/dependencies/checkouts/" + if marker in normalized: + suffix = normalized.rsplit(marker, maxsplit=1)[1] + if suffix: + return f"dependencies/checkouts/{suffix}" + prefix = "dependencies/checkouts/" + if normalized.startswith(prefix) and len(normalized) > len(prefix): + return normalized + raise RuntimeError( + "candidate Cargo patch paths must resolve below dependencies/checkouts" + ) + + +def canonical_candidate_cargo_config_bytes(data: bytes) -> bytes: + """Return the exact path-independent candidate Cargo patch contract.""" + + try: + payload = tomllib.loads(data.decode("utf-8")) + patch = payload["patch"] + crates_io = patch["crates-io"] + except ( + KeyError, + TypeError, + UnicodeDecodeError, + tomllib.TOMLDecodeError, + ) as error: + raise RuntimeError(f"invalid candidate Cargo config: {error}") from error + if ( + not isinstance(payload, dict) + or set(payload) != {"patch"} + or not isinstance(patch, dict) + or set(patch) != {"crates-io"} + or not isinstance(crates_io, dict) + or not set(_CANDIDATE_PATCH_TARGETS).issubset(crates_io) + ): + raise RuntimeError( + "candidate Cargo config must contain the locked native crates.io " + "patch table" + ) + canonical: dict[str, Any] = {"patch": {"crates-io": {}}} + canonical_patches = canonical["patch"]["crates-io"] + for name, expected in _CANDIDATE_PATCH_TARGETS.items(): + entry = crates_io[name] + if ( + not isinstance(entry, Mapping) + or set(entry) != {"path"} + or not isinstance(entry["path"], str) + ): + raise RuntimeError( + f"candidate Cargo patch {name} must contain exactly one path" + ) + observed = _canonical_checkout_path(entry["path"]) + if observed != expected and not ( + name == "symjit" and observed == "TMP_FIXED_SYMJIT" + ): + raise RuntimeError( + f"candidate Cargo patch {name} must resolve to {expected}" + ) + canonical_patches[name] = {"path": observed} + return _canonical_json_bytes(canonical) + + +def _required_string( + payload: Mapping[str, Any], + key: str, + *, + description: str, +) -> str: + value = payload.get(key) + if not isinstance(value, str) or not value: + raise RuntimeError(f"{description} {key} must be a non-empty string") + return value + + +def _required_hex( + payload: Mapping[str, Any], + key: str, + *, + length: int, + description: str, +) -> str: + value = _required_string(payload, key, description=description) + if len(value) != length or any( + character not in "0123456789abcdef" for character in value + ): + raise RuntimeError( + f"{description} {key} must be a lowercase {length}-digit hex value" + ) + return value + + +def _canonical_candidate_sources(raw_sources: Any) -> dict[str, dict[str, str]]: + if not isinstance(raw_sources, Mapping): + raise RuntimeError("candidate installer state sources must be an object") + source_names = set(raw_sources) + if any(not isinstance(name, str) or not name for name in source_names): + raise RuntimeError("candidate installer source names must be non-empty strings") + if not _NATIVE_CANDIDATE_SOURCE_NAMES.issubset(source_names): + raise RuntimeError( + "candidate installer state has an incomplete native source map" + ) + sources: dict[str, dict[str, str]] = {} + for name in sorted(_NATIVE_CANDIDATE_SOURCE_NAMES): + raw_source = raw_sources[name] + description = f"candidate installer source {name}" + if not isinstance(raw_source, Mapping): + raise RuntimeError(f"{description} must be an object") + allowed_fields = {"url", "revision", "branch"} + required_fields = {"url", "revision"} + if not required_fields.issubset(raw_source) or not set(raw_source).issubset( + allowed_fields + ): + raise RuntimeError( + f"{description} must contain url/revision and optional branch only" + ) + source = { + "url": _required_string(raw_source, "url", description=description), + "revision": _required_hex( + raw_source, + "revision", + length=40, + description=description, + ), + } + if "branch" in raw_source: + source["branch"] = _required_string( + raw_source, + "branch", + description=description, + ) + sources[name] = source + return sources + + +def canonical_candidate_install_state_bytes( + root: Path, + data: bytes, + *, + cargo_config_data: bytes, +) -> bytes: + """Validate and canonicalize the minimal immutable-source install state.""" + + del root, cargo_config_data + + try: + payload = json.loads(data) + except (UnicodeDecodeError, json.JSONDecodeError) as error: + raise RuntimeError(f"invalid candidate installer state: {error}") from error + schema_version = ( + payload.get("schema_version") if isinstance(payload, dict) else None + ) + if not isinstance(payload, dict) or set(payload) != { + "publishable", + "schema_version", + "sources", + }: + raise RuntimeError( + "candidate installer state must contain only schema_version, " + "publishable, and sources" + ) + if ( + not isinstance(schema_version, int) + or isinstance(schema_version, bool) + or schema_version != 1 + or payload["publishable"] is not False + ): + raise RuntimeError( + "candidate installer state must be a non-publishable schema-1 object" + ) + sources = _canonical_candidate_sources(payload["sources"]) + return _canonical_json_bytes( + { + "publishable": False, + "schema_version": 1, + "sources": sources, + } + ) + + +def canonical_release_cargo_lock_bytes(root: Path, data: bytes) -> bytes: + """Return the ordinary immutable-Git Cargo lock without projection.""" + + del root + return data + + +def _is_native_build_tree_input(root: Path, tree: Path, path: Path) -> bool: + if not path.is_file(): + return False + tree_relative = path.relative_to(tree) + root_relative = path.relative_to(root) + return ( + not _NATIVE_BUILD_IGNORED_TREE_PARTS.intersection(tree_relative.parts) + and root_relative not in _NON_NATIVE_RUST_PATHS + ) + + +def native_build_inputs_digest( + root: Path, + *, + normalize_release_cargo_lock: bool = False, +) -> str: + """Hash every checked-out input that can change the native runtime build.""" + + candidate_config = root / _CANDIDATE_CARGO_CONFIG + candidate_state = root / _CANDIDATE_INSTALL_STATE + canonical_candidate_inputs: dict[Path, bytes] = {} + if not normalize_release_cargo_lock: + if candidate_config.is_file() != candidate_state.is_file(): + raise RuntimeError( + "candidate native build identity requires both " + "candidate-cargo-config.toml and install-state.json" + ) + if candidate_config.is_file(): + cargo_config_data = candidate_config.read_bytes() + canonical_candidate_inputs[candidate_config] = ( + canonical_candidate_cargo_config_bytes(cargo_config_data) + ) + canonical_candidate_inputs[candidate_state] = ( + canonical_candidate_install_state_bytes( + root, + candidate_state.read_bytes(), + cargo_config_data=cargo_config_data, + ) + ) + + paths = [ + root / relative + for relative in _NATIVE_BUILD_INPUT_FILES + if not ( + normalize_release_cargo_lock + and relative in _RELEASE_OMITTED_NATIVE_BUILD_INPUTS + ) + and not (canonical_candidate_inputs and relative == Path("Cargo.lock")) + ] + # Local upstream fixes have no new Git revision. Include their compiler + # inputs in the existing cache key, but not their tests or documentation. + manifest = root / "Cargo.toml" + if manifest.is_file(): + cargo = tomllib.loads(manifest.read_text(encoding="utf-8")) + local_symjit = cargo.get("patch", {}).get("crates-io", {}).get("symjit", {}) + local_path = local_symjit.get("path") + if isinstance(local_path, str): + local_root = (root / local_path).resolve() + if local_root == (root / "TMP_FIXED_SYMJIT").resolve(): + paths.extend(local_root / name for name in ("Cargo.toml", "build.rs")) + paths.extend( + path for path in (local_root / "src").rglob("*") if path.is_file() + ) + for relative in _NATIVE_BUILD_INPUT_TREES: + tree = root / relative + if not tree.is_dir(): + continue + paths.extend( + path + for path in tree.rglob("*") + if _is_native_build_tree_input(root, tree, path) + and not ( + normalize_release_cargo_lock + and path.relative_to(root) in _RELEASE_OMITTED_NATIVE_BUILD_INPUTS + ) + ) + digest = hashlib.sha256(_NATIVE_BUILD_IDENTITY_DOMAIN) + for path in sorted(set(paths)): + if not path.is_file(): + continue + relative = path.relative_to(root).as_posix().encode("utf-8") + digest.update(len(relative).to_bytes(8, "little")) + digest.update(relative) + data = canonical_candidate_inputs.get(path, path.read_bytes()) + if path == root / "pyproject.toml": + data = canonical_native_pyproject_bytes(data) + if normalize_release_cargo_lock and path == root / "Cargo.lock": + data = canonical_release_cargo_lock_bytes(root, data) + digest.update(len(data).to_bytes(8, "little")) + digest.update(data) + return digest.hexdigest() + + +__all__ = [ + "canonical_candidate_cargo_config_bytes", + "canonical_candidate_install_state_bytes", + "canonical_native_pyproject_bytes", + "canonical_release_cargo_lock_bytes", + "load_native_build_contract", + "native_build_inputs_digest", +] diff --git a/pyamplicol/build_backend/package_version.py b/pyamplicol/build_backend/package_version.py new file mode 100644 index 0000000000..10d5e59373 --- /dev/null +++ b/pyamplicol/build_backend/package_version.py @@ -0,0 +1,123 @@ +# SPDX-License-Identifier: 0BSD +"""Package-version checks sourced from the Cargo workspace manifest.""" + +from __future__ import annotations + +import tomllib +from pathlib import Path +from typing import Any + + +def _load_toml(path: Path) -> dict[str, Any]: + try: + with path.open("rb") as stream: + payload = tomllib.load(stream) + except (OSError, tomllib.TOMLDecodeError) as error: + raise RuntimeError( + f"cannot read package version contract {path}: {error}" + ) from error + if not isinstance(payload, dict): + raise RuntimeError(f"package version contract {path} is not a TOML table") + return payload + + +def _required_string(value: object, context: str) -> str: + if not isinstance(value, str) or not value: + raise RuntimeError(f"{context} must be a nonempty string") + return value + + +def canonical_package_version(root: Path) -> str: + """Return Cargo's package version after checking release-facing metadata.""" + + cargo = _load_toml(root / "Cargo.toml") + pyproject = _load_toml(root / "pyproject.toml") + release = _load_toml(root / "dependencies" / "release-lock.toml") + try: + cargo_version = _required_string( + cargo["workspace"]["package"]["version"], + "Cargo.toml [workspace.package].version", + ) + project = pyproject["project"] + locked_version = _required_string( + release["project"]["version"], + "dependencies/release-lock.toml [project].version", + ) + except (KeyError, TypeError) as error: + raise RuntimeError("package version contract is incomplete") from error + + if not isinstance(project, dict): + raise RuntimeError("pyproject.toml [project] must be a table") + dynamic = project.get("dynamic") + if ( + "version" in project + or not isinstance(dynamic, list) + or "version" not in dynamic + ): + raise RuntimeError( + "pyproject.toml must declare project.version as dynamic; " + "Cargo.toml [workspace.package].version is canonical" + ) + if locked_version != cargo_version: + raise RuntimeError( + "package version mismatch: Cargo.toml declares " + f"{cargo_version!r}, but dependencies/release-lock.toml declares " + f"{locked_version!r}" + ) + return cargo_version + + +def check_contributor_lock_consistency(root: Path) -> None: + """Check dependency identities shared by release and contributor locks.""" + + release = _load_toml(root / "dependencies" / "release-lock.toml") + contributor = _load_toml(root / "dependencies" / "contributor-lock.toml") + try: + values = { + "release [abis]": _required_string( + release["abis"]["symbolica_serialization"], + "release-lock [abis].symbolica_serialization", + ), + "release [symbolica]": _required_string( + release["symbolica"]["serialization_abi"], + "release-lock [symbolica].serialization_abi", + ), + "contributor [abis]": _required_string( + contributor["abis"]["symbolica_serialization"], + "contributor-lock [abis].symbolica_serialization", + ), + } + except (KeyError, TypeError) as error: + raise RuntimeError( + "dependency lock compatibility contract is incomplete" + ) from error + if len(set(values.values())) != 1: + rendered = ", ".join(f"{name}={value!r}" for name, value in values.items()) + raise RuntimeError( + "release and contributor dependency locks disagree on the Symbolica " + f"serialization ABI: {rendered}" + ) + + try: + plane_values = { + "release [abis]": _required_string( + release["abis"]["symjit_plane_application"], + "release-lock [abis].symjit_plane_application", + ), + "contributor [abis]": _required_string( + contributor["abis"]["symjit_plane_application"], + "contributor-lock [abis].symjit_plane_application", + ), + } + except (KeyError, TypeError) as error: + raise RuntimeError( + "dependency lock compatibility contract is incomplete" + ) from error + if len(set(plane_values.values())) != 1: + rendered = ", ".join( + f"{name}={value!r}" for name, value in plane_values.items() + ) + raise RuntimeError( + "release and contributor dependency locks disagree on the SymJIT " + f"plane-application ABI: {rendered}" + ) diff --git a/pyamplicol/build_backend/prepared_models.py b/pyamplicol/build_backend/prepared_models.py new file mode 100644 index 0000000000..a3b2e207f0 --- /dev/null +++ b/pyamplicol/build_backend/prepared_models.py @@ -0,0 +1,891 @@ +# SPDX-License-Identifier: 0BSD +"""Validate source-owned prepared models before wheel staging.""" + +from __future__ import annotations + +import ast +import hashlib +import importlib.util +import json +import os +import shutil +import sys +import tomllib +from collections.abc import Mapping, Sequence +from pathlib import Path +from types import ModuleType +from typing import Any, cast + +_ASSET_DIRECTORY = Path("src/pyamplicol/assets/prepared_models") +_RELEASE_STORE_CONTAINER = Path("release_assets") +_RELEASE_STORE_DIRECTORY = _RELEASE_STORE_CONTAINER / "prepared_models" +_EXPECTED_ARCHITECTURES = ("aarch64", "x86_64") +_METADATA_KEYS = frozenset( + { + "backend", + "bundle", + "bundle_sha256", + "bundle_size", + "build_contract", + "dependencies", + "eager_kernel_abi", + "id", + "jit_optimization_level", + "kernel_count", + "model", + "prepared_model_bundle_schema", + "producer", + "schema_version", + "target", + } +) +_PRODUCER_KEYS = frozenset( + { + "compiled_model_schema", + "model_compiler_sha256", + "model_compiler_version", + "model_source_digest", + "package_version", + } +) +_DEFAULT_ID = "built-in-sm-jit-o2" +_MODEL_SOURCE_BY_ID = { + _DEFAULT_ID: "built-in-sm", + "built-in-sm-heft-jit-o2": "built-in-sm-heft", +} + + +def _asset_names(identifier: str, architecture: str) -> tuple[str, str]: + stem = f"{identifier}-{architecture}" + return f"{stem}.metadata.json", f"{stem}.pyamplicol-model" + + +_EXPECTED_FILES = frozenset( + ( + "__init__.py", + *( + name + for identifier in _MODEL_SOURCE_BY_ID + for architecture in _EXPECTED_ARCHITECTURES + for name in _asset_names(identifier, architecture) + ), + ) +) +_PROJECTED_RELEASE_STORE_FILES = _EXPECTED_FILES - {"__init__.py"} +_EXPECTED_RELEASE_STORE_FILES = _PROJECTED_RELEASE_STORE_FILES | {"README.md"} + + +def project_release_packaged_prepared_model_store( + overlay: Path, + *, + require_store: bool, +) -> bool: + """Project source-owned release packs over candidate package assets.""" + + container = overlay / _RELEASE_STORE_CONTAINER + store = overlay / _RELEASE_STORE_DIRECTORY + if not os.path.lexists(container): + if require_store: + raise RuntimeError( + "release prepared-model source store is missing; generate and " + "commit both architecture pairs before building release artifacts" + ) + return False + if container.is_symlink() or not container.is_dir(): + raise RuntimeError("release prepared-model store container is unsafe") + container_entries = {path.name for path in container.iterdir()} + if container_entries != {"prepared_models"}: + raise RuntimeError( + "release prepared-model store container inventory is invalid" + ) + if not store.is_dir() or store.is_symlink(): + raise RuntimeError("release prepared-model source store is missing or unsafe") + store_entries = tuple(store.iterdir()) + actual_files = {path.name for path in store_entries} + if actual_files != _EXPECTED_RELEASE_STORE_FILES: + missing = sorted(_EXPECTED_RELEASE_STORE_FILES - actual_files) + unexpected = sorted(actual_files - _EXPECTED_RELEASE_STORE_FILES) + details: list[str] = [] + if missing: + details.append("missing: " + ", ".join(missing)) + if unexpected: + details.append("unexpected: " + ", ".join(unexpected)) + raise RuntimeError( + "release prepared-model source store inventory is invalid (" + + "; ".join(details) + + ")" + ) + if any(path.is_symlink() or not path.is_file() for path in store_entries): + raise RuntimeError( + "release prepared-model source store entries must be regular files" + ) + readme = store / "README.md" + if not readme.is_file() or readme.is_symlink(): + raise RuntimeError("release prepared-model store README is unsafe") + + package_assets = overlay / _ASSET_DIRECTORY + if not package_assets.is_dir() or package_assets.is_symlink(): + raise RuntimeError("candidate prepared-model package assets are missing") + package_entries = tuple(package_assets.iterdir()) + if {path.name for path in package_entries} != _EXPECTED_FILES: + raise RuntimeError( + "candidate prepared-model package asset inventory is invalid" + ) + if any(path.is_symlink() or not path.is_file() for path in package_entries): + raise RuntimeError( + "candidate prepared-model package assets must be regular files" + ) + for name in sorted(_PROJECTED_RELEASE_STORE_FILES): + shutil.copy2(store / name, package_assets / name) + shutil.rmtree(container) + return True + + +def discard_release_packaged_prepared_model_store(overlay: Path) -> None: + """Remove the auxiliary store from a candidate or bootstrap overlay.""" + + container = overlay / _RELEASE_STORE_CONTAINER + if not os.path.lexists(container): + return + if container.is_symlink() or not container.is_dir(): + raise RuntimeError("release prepared-model store container is unsafe") + shutil.rmtree(container) + + +def stage_packaged_prepared_models( + overlay: Path, + mode: str, + *, + source_root: Path | None = None, +) -> None: + """Fail closed unless the wheel-owned prepared model matches the overlay.""" + + if mode not in {"candidate", "release"}: + raise RuntimeError(f"unsupported prepared-model build mode: {mode}") + package_root = overlay / "src" / "pyamplicol" + asset_root = overlay / _ASSET_DIRECTORY + if not asset_root.is_dir() or asset_root.is_symlink(): + raise RuntimeError("wheel build input has no packaged prepared-model assets") + actual_files = {path.name for path in asset_root.iterdir()} + unsafe = [path for path in asset_root.iterdir() if path.is_symlink()] + if unsafe: + raise RuntimeError( + "packaged prepared-model assets may not be symlinks: " + + ", ".join(str(path) for path in sorted(unsafe)) + ) + if actual_files != _EXPECTED_FILES: + missing = sorted(_EXPECTED_FILES - actual_files) + unexpected = sorted(actual_files - _EXPECTED_FILES) + details: list[str] = [] + if missing: + details.append("missing: " + ", ".join(missing)) + if unexpected: + details.append("unexpected: " + ", ".join(unexpected)) + raise RuntimeError( + "packaged prepared-model asset set is invalid (" + "; ".join(details) + ")" + ) + + contract = _load_prepared_contract(package_root / "models" / "prepared.py") + for identifier, model_source in _MODEL_SOURCE_BY_ID.items(): + for architecture in _EXPECTED_ARCHITECTURES: + metadata_name, expected_bundle_name = _asset_names(identifier, architecture) + metadata = _load_json( + asset_root / metadata_name, + f"{identifier} {architecture} prepared-model metadata", + ) + _require_exact_keys(metadata, _METADATA_KEYS, "prepared-model metadata") + if metadata.get("schema_version") != 1: + raise RuntimeError( + "unsupported packaged prepared-model metadata schema" + ) + if ( + metadata.get("id") != identifier + or metadata.get("model") != model_source + ): + raise RuntimeError("packaged prepared-model identity is invalid") + bundle_name = _required_string(metadata.get("bundle"), "metadata.bundle") + if bundle_name != expected_bundle_name: + raise RuntimeError("packaged prepared-model bundle name is invalid") + bundle_path = asset_root / bundle_name + if not bundle_path.is_file() or bundle_path.is_symlink(): + raise RuntimeError( + "packaged prepared-model bundle must be a regular file" + ) + bundle_bytes = bundle_path.read_bytes() + if metadata.get("bundle_size") != len(bundle_bytes): + raise RuntimeError( + "packaged prepared-model bundle size does not match metadata" + ) + if ( + metadata.get("bundle_sha256") + != hashlib.sha256(bundle_bytes).hexdigest() + ): + raise RuntimeError( + "packaged prepared-model bundle SHA-256 does not match metadata" + ) + try: + bundle = contract.load_prepared_model_bundle(bundle_path) + except Exception as error: + raise RuntimeError( + f"packaged prepared-model bundle is invalid: {error}" + ) from error + _validate_bundle( + bundle, + metadata=metadata, + identifier=identifier, + model_source=model_source, + expected_target={ + "portable": True, + "word_bits": 64, + "endianness": "little", + "target_triple": "symjit-storage-v3-portable", + "cpu_features": [], + }, + package_root=package_root, + overlay=overlay, + mode=mode, + ) + + +def write_candidate_packaged_prepared_model_asset( + overlay: Path, + bundle_path: Path, + output_directory: Path, + *, + architecture: str, + identifier: str = _DEFAULT_ID, +) -> tuple[Path, Path]: + """Write one candidate source-ready architecture pack.""" + + return _write_packaged_prepared_model_asset( + overlay, + bundle_path, + output_directory, + architecture=architecture, + identifier=identifier, + mode="candidate", + ) + + +def write_release_packaged_prepared_model_asset( + overlay: Path, + bundle_path: Path, + output_directory: Path, + *, + architecture: str, + identifier: str = _DEFAULT_ID, +) -> tuple[Path, Path]: + """Write one release source-ready architecture pack.""" + + return _write_packaged_prepared_model_asset( + overlay, + bundle_path, + output_directory, + architecture=architecture, + identifier=identifier, + mode="release", + ) + + +def _write_packaged_prepared_model_asset( + overlay: Path, + bundle_path: Path, + output_directory: Path, + *, + architecture: str, + identifier: str, + mode: str, +) -> tuple[Path, Path]: + """Write one source-ready architecture pack and its derived metadata.""" + + if mode not in {"candidate", "release"}: + raise RuntimeError(f"unsupported prepared-model build mode: {mode}") + if architecture not in _EXPECTED_ARCHITECTURES: + raise RuntimeError(f"unsupported prepared-model architecture: {architecture}") + try: + model_source = _MODEL_SOURCE_BY_ID[identifier] + except KeyError as error: + choices = ", ".join(_MODEL_SOURCE_BY_ID) + raise RuntimeError( + f"unsupported packaged prepared-model identifier {identifier!r}; " + f"available: {choices}" + ) from error + package_root = overlay / "src" / "pyamplicol" + release_path = overlay / "dependencies" / "release-lock.toml" + with release_path.open("rb") as stream: + release = tomllib.load(stream) + contributor: Mapping[str, Any] | None = None + if mode == "candidate": + contributor_path = overlay / "dependencies" / "contributor-lock.toml" + with contributor_path.open("rb") as stream: + contributor = tomllib.load(stream) + + contract = _load_prepared_contract(package_root / "models" / "prepared.py") + source_bundle = bundle_path.resolve(strict=True) + bundle_bytes = source_bundle.read_bytes() + try: + bundle = contract.load_prepared_model_bundle(source_bundle) + except Exception as error: + raise RuntimeError(f"prepared-model bundle is invalid: {error}") from error + pack = bundle.kernel_pack + expected_target = { + "portable": True, + "word_bits": 64, + "endianness": "little", + "target_triple": "symjit-storage-v3-portable", + "cpu_features": [], + } + if _plain_json(pack.target) != expected_target: + raise RuntimeError("prepared-model bundle target does not match architecture") + if bundle.backend != "jit" or pack.backend != "jit": + raise RuntimeError("packaged built-in prepared model must use JIT") + optimization = dict(pack.optimization_settings) + if optimization.get("jit_optimization_level") != 2: + raise RuntimeError("packaged built-in prepared model must use portable JIT O2") + + compiled = dict(bundle.compiled_model) + compiled_producer = _mapping(compiled.get("producer"), "compiled_model.producer") + compiled_source = _mapping(compiled.get("source"), "compiled_model.source") + package_version = _required_string( + compiled_producer.get("pyamplicol"), "compiled_model producer version" + ) + candidate_fingerprint: str | None = None + if mode == "candidate": + marker = "+candidate." + if marker not in package_version: + raise RuntimeError( + "source-ready prepared assets require a candidate bundle" + ) + candidate_fingerprint = package_version.rsplit(marker, maxsplit=1)[1] + if len(candidate_fingerprint) != 12 or any( + character not in "0123456789abcdef" for character in candidate_fingerprint + ): + raise RuntimeError("prepared bundle has an invalid candidate fingerprint") + else: + release_version = _release_package_version(overlay, release) + if package_version != release_version: + raise RuntimeError( + "release prepared assets require the canonical package version " + f"{release_version!r}" + ) + if pack.producer.get("version") != package_version: + raise RuntimeError("prepared kernel-pack package version is inconsistent") + + compiled_schema = _literal_assignment( + package_root / "_internal" / "versions.py", + "COMPILED_MODEL_SCHEMA_VERSION", + ) + model_compiler_version = _literal_assignment( + package_root / "models" / "loading.py", "MODEL_COMPILER_VERSION" + ) + model_compiler_digest = _required_sha256( + compiled_producer.get("model_compiler_sha256"), + "compiled_model producer model_compiler_sha256", + ) + source_digest = _built_in_source_digest(package_root, model_source) + expected_compiled = { + "compiled_model_schema_version": compiled_schema, + "model_compiler_version": model_compiler_version, + } + for key, expected in expected_compiled.items(): + if compiled_producer.get(key) != expected: + raise RuntimeError(f"prepared compiled-model producer {key} is stale") + if compiled_source.get("kind") != model_source: + raise RuntimeError(f"prepared bundle does not contain the {model_source} model") + if compiled_source.get("digest") != source_digest: + raise RuntimeError("prepared bundle built-in model source digest is stale") + if pack.provenance.get("compiled_model_digest") != source_digest: + raise RuntimeError("prepared kernel-pack model source digest is stale") + pack_model_source = _mapping( + pack.provenance.get("model_source"), + "kernel_pack.provenance.model_source", + ) + if ( + pack_model_source.get("kind") != model_source + or pack_model_source.get("digest") != source_digest + ): + raise RuntimeError("prepared kernel-pack model source identity is stale") + + if mode == "candidate": + assert contributor is not None + symbolica_version = str(contributor["symbolica"]["candidate_version"]) + symjit_version = str(contributor["symjit"]["candidate_version"]) + else: + symbolica_version = _cargo_package_version(overlay, "symbolica") + symjit_version = _symjit_version(overlay) + dependencies = { + "symbolica_serialization_abi": _literal_assignment( + package_root / "_internal" / "versions.py", + "SYMBOLICA_SERIALIZATION_ABI", + ), + "symbolica_version": symbolica_version, + "symjit_application_abi": _literal_assignment( + package_root / "_internal" / "versions.py", + "SYMJIT_APPLICATION_ABI", + ), + "symjit_plane_application_abi": _literal_assignment( + package_root / "_internal" / "versions.py", + "SYMJIT_PLANE_APPLICATION_ABI", + ), + "symjit_version": symjit_version, + "ufo_model_loader_version": release["ufo_model_loader"]["required_version"], + } + expected_pack_dependencies = { + "symbolica_serialization": dependencies["symbolica_serialization_abi"], + "symbolica_version": dependencies["symbolica_version"], + "symjit_application": dependencies["symjit_application_abi"], + "symjit_plane_application": dependencies["symjit_plane_application_abi"], + } + for key, expected in expected_pack_dependencies.items(): + if pack.dependency_abis.get(key) != expected: + raise RuntimeError(f"prepared kernel-pack dependency {key} is stale") + + metadata_name, bundle_name = _asset_names(identifier, architecture) + metadata: dict[str, object] = { + "backend": "jit", + "build_contract": {"mode": mode}, + "bundle": bundle_name, + "bundle_sha256": hashlib.sha256(bundle_bytes).hexdigest(), + "bundle_size": len(bundle_bytes), + "dependencies": dependencies, + "eager_kernel_abi": _literal_assignment( + package_root / "models" / "prepared.py", "EAGER_KERNEL_ABI" + ), + "id": identifier, + "jit_optimization_level": 2, + "kernel_count": len(pack.kernels), + "model": model_source, + "prepared_model_bundle_schema": _literal_assignment( + package_root / "models" / "prepared.py", + "PREPARED_MODEL_BUNDLE_SCHEMA_VERSION", + ), + "producer": { + "compiled_model_schema": compiled_schema, + "model_compiler_sha256": model_compiler_digest, + "model_compiler_version": model_compiler_version, + "model_source_digest": source_digest, + "package_version": package_version, + }, + "schema_version": 1, + "target": expected_target, + } + + destination = output_directory.resolve(strict=False) + destination.mkdir(parents=True, exist_ok=True) + output_bundle = destination / bundle_name + output_metadata = destination / metadata_name + for path in (output_bundle, output_metadata): + if path.exists(): + raise RuntimeError(f"source-ready prepared asset already exists: {path}") + temporary_bundle = output_bundle.with_name(f".{output_bundle.name}.tmp") + temporary_metadata = output_metadata.with_name(f".{output_metadata.name}.tmp") + temporary_bundle.write_bytes(bundle_bytes) + temporary_metadata.write_text( + json.dumps(metadata, indent=2, sort_keys=True) + "\n", + encoding="utf-8", + ) + temporary_bundle.replace(output_bundle) + temporary_metadata.replace(output_metadata) + return output_metadata, output_bundle + + +def _validate_bundle( + bundle: Any, + *, + metadata: Mapping[str, object], + identifier: str, + model_source: str, + expected_target: Mapping[str, object], + package_root: Path, + overlay: Path, + mode: str, +) -> None: + pack = bundle.kernel_pack + if bundle.backend != "jit" or metadata.get("backend") != "jit": + raise RuntimeError("packaged built-in prepared model must use the JIT backend") + if metadata.get("jit_optimization_level") != 2: + raise RuntimeError("packaged built-in prepared model must use portable JIT O2") + optimization = dict(pack.optimization_settings) + if ( + optimization.get("backend") != "jit" + or optimization.get("jit_optimization_level") != 2 + ): + raise RuntimeError( + "prepared kernel pack does not record portable JIT O2 settings" + ) + target = _plain_json(pack.target) + if target != expected_target or metadata.get("target") != expected_target: + raise RuntimeError( + "packaged prepared model target does not match its architecture asset" + ) + kernel_count = len(pack.kernels) + if metadata.get("kernel_count") != kernel_count or kernel_count == 0: + raise RuntimeError("packaged prepared-model kernel count is invalid") + + eager_abi = _literal_assignment( + package_root / "models" / "prepared.py", "EAGER_KERNEL_ABI" + ) + bundle_schema = _literal_assignment( + package_root / "models" / "prepared.py", + "PREPARED_MODEL_BUNDLE_SCHEMA_VERSION", + ) + if metadata.get("eager_kernel_abi") != eager_abi: + raise RuntimeError("packaged prepared-model eager kernel ABI is stale") + if metadata.get("prepared_model_bundle_schema") != bundle_schema: + raise RuntimeError("packaged prepared-model container schema is stale") + if bundle.manifest.get("eager_kernel_abi") != eager_abi: + raise RuntimeError("prepared bundle declares an incompatible eager kernel ABI") + if bundle.manifest.get("schema_version") != bundle_schema: + raise RuntimeError("prepared bundle declares an incompatible container schema") + + build_contract = _mapping(metadata.get("build_contract"), "metadata.build_contract") + _require_exact_keys( + build_contract, + frozenset({"mode"}), + "metadata.build_contract", + ) + if build_contract.get("mode") != mode: + raise RuntimeError( + "packaged prepared model was built for " + f"{build_contract.get('mode')!r}, not the active {mode!r} dependency mode; " + "regenerate it with the active exact dependency lock" + ) + compiled = dict(bundle.compiled_model) + producer = _mapping(compiled.get("producer"), "compiled_model.producer") + source = _mapping(compiled.get("source"), "compiled_model.source") + model = _mapping(compiled.get("model"), "compiled_model.model") + if model.get("name") != model_source or source.get("kind") != model_source: + raise RuntimeError( + f"packaged prepared model does not contain the {model_source} model" + ) + if metadata.get("id") != identifier or metadata.get("model") != model_source: + raise RuntimeError("packaged prepared-model identity is invalid") + + compiled_schema = _literal_assignment( + package_root / "_internal" / "versions.py", + "COMPILED_MODEL_SCHEMA_VERSION", + ) + compiler_version = _literal_assignment( + package_root / "models" / "loading.py", "MODEL_COMPILER_VERSION" + ) + packaged_producer = _mapping(metadata.get("producer"), "metadata.producer") + _require_exact_keys( + packaged_producer, + _PRODUCER_KEYS, + "metadata.producer", + ) + model_source_digest = _built_in_source_digest(package_root, model_source) + # The producer source digest remains useful provenance, but it is not a + # compatibility boundary. Explicit schema and compiler versions carry + # that contract without forcing pack regeneration after harmless edits. + expected_producer = { + "compiled_model_schema": compiled_schema, + "model_compiler_version": compiler_version, + "model_source_digest": model_source_digest, + } + for key, expected in expected_producer.items(): + if packaged_producer.get(key) != expected: + raise RuntimeError( + f"packaged prepared-model producer {key} is stale: " + f"expected {expected!r}, got {packaged_producer.get(key)!r}" + ) + packaged_package_version = _required_string( + packaged_producer.get("package_version"), + "packaged prepared-model producer package_version", + ) + if producer.get("compiled_model_schema_version") != compiled_schema: + raise RuntimeError("prepared compiled-model schema is stale") + if producer.get("model_compiler_version") != compiler_version: + raise RuntimeError("prepared model compiler version is stale") + if producer.get("pyamplicol") != packaged_package_version: + raise RuntimeError( + "prepared compiled-model package version disagrees with metadata" + ) + if source.get("digest") != model_source_digest: + raise RuntimeError("prepared compiled-model source digest is stale") + if pack.producer.get("version") != packaged_package_version: + raise RuntimeError( + "prepared kernel-pack package version disagrees with metadata" + ) + if pack.producer.get("compiled_model_schema") != compiled_schema: + raise RuntimeError("prepared kernel-pack compiled-model schema is stale") + if pack.producer.get("model_compiler_version") != compiler_version: + raise RuntimeError("prepared kernel-pack model compiler version is stale") + if pack.provenance.get("model_name") != model_source: + raise RuntimeError("prepared kernel-pack model provenance is invalid") + if pack.provenance.get("compiled_model_digest") != model_source_digest: + raise RuntimeError("prepared kernel-pack compiled-model digest is stale") + pack_model_source = _mapping( + pack.provenance.get("model_source"), + "kernel_pack.provenance.model_source", + ) + if ( + pack_model_source.get("kind") != model_source + or pack_model_source.get("digest") != model_source_digest + ): + raise RuntimeError("prepared kernel-pack model source identity is stale") + + dependencies = _mapping(metadata.get("dependencies"), "metadata.dependencies") + _validate_dependency_contract( + dependencies, + pack_dependency_abis=pack.dependency_abis, + package_root=package_root, + overlay=overlay, + mode=mode, + ) + for kernel in pack.kernels: + manifest = kernel.f64_evaluator_manifest + expected = { + "kind": "symjit-application-evaluator", + "backend": "jit", + "runtime_capability": "symjit.application.complex-f64.v1", + "application_abi": dependencies["symjit_application_abi"], + "compiler_type": "native", + "translation_mode": "indirect", + "optimization_level": 2, + "word_bits": 64, + "endianness": "little", + "required_defuns": (), + } + for key, expected_value in expected.items(): + actual = manifest.get(key) + if key == "required_defuns" and isinstance(actual, Sequence): + actual = tuple(actual) + if actual != expected_value: + raise RuntimeError( + f"prepared kernel {kernel.kernel_id} has incompatible {key}: " + f"expected {expected_value!r}, got {actual!r}" + ) + + +def _validate_dependency_contract( + dependencies: Mapping[str, object], + *, + pack_dependency_abis: Mapping[str, object], + package_root: Path, + overlay: Path, + mode: str, +) -> None: + with (overlay / "dependencies" / "release-lock.toml").open("rb") as stream: + release = tomllib.load(stream) + symbolica_version = release["symbolica"]["python_version"] + if mode == "candidate": + symbolica_version = _cargo_package_version(overlay, "symbolica") + expected = { + "symbolica_version": symbolica_version, + "ufo_model_loader_version": release["ufo_model_loader"]["required_version"], + "symjit_version": _symjit_version(overlay), + "symbolica_serialization_abi": _literal_assignment( + package_root / "_internal" / "versions.py", + "SYMBOLICA_SERIALIZATION_ABI", + ), + "symjit_application_abi": _literal_assignment( + package_root / "_internal" / "versions.py", + "SYMJIT_APPLICATION_ABI", + ), + "symjit_plane_application_abi": _literal_assignment( + package_root / "_internal" / "versions.py", + "SYMJIT_PLANE_APPLICATION_ABI", + ), + } + for key, value in expected.items(): + if dependencies.get(key) != value: + raise RuntimeError( + f"packaged prepared-model dependency {key} is stale: " + f"expected {value!r}, got {dependencies.get(key)!r}" + ) + pack_expected = { + "symbolica_version": expected["symbolica_version"], + "symbolica_serialization": expected["symbolica_serialization_abi"], + "symjit_application": expected["symjit_application_abi"], + "symjit_plane_application": expected["symjit_plane_application_abi"], + } + for key, value in pack_expected.items(): + if pack_dependency_abis.get(key) != value: + raise RuntimeError(f"prepared kernel-pack dependency ABI {key} is stale") + + +def _symjit_version(overlay: Path) -> str: + return _cargo_package_version(overlay, "symjit") + + +def _release_package_version( + overlay: Path, release: Mapping[str, Any] | None = None +) -> str: + if release is None: + with (overlay / "dependencies" / "release-lock.toml").open("rb") as stream: + release = tomllib.load(stream) + with (overlay / "Cargo.toml").open("rb") as stream: + cargo = tomllib.load(stream) + cargo_version = _required_string( + cargo["workspace"]["package"]["version"], "Cargo package version" + ) + locked_version = _required_string( + release["project"]["version"], "release-lock project version" + ) + if cargo_version != locked_version: + raise RuntimeError( + "release package version differs between Cargo.toml and release-lock.toml" + ) + return cargo_version + + +def _cargo_package_version(overlay: Path, package_name: str) -> str: + with (overlay / "Cargo.lock").open("rb") as stream: + cargo_lock = tomllib.load(stream) + packages = cargo_lock.get("package") + if not isinstance(packages, list): + raise RuntimeError("Cargo.lock has no package array") + matches = [ + package.get("version") + for package in packages + if isinstance(package, dict) and package.get("name") == package_name + ] + if len(matches) != 1: + raise RuntimeError( + f"Cargo.lock must contain exactly one {package_name} package" + ) + return _required_string(matches[0], f"Cargo.lock {package_name} version") + + +def _built_in_source_digest( + package_root: Path, + model_source: str = "built-in-sm", +) -> str: + if model_source == "built-in-sm-heft": + return _sm_heft_source_digest(package_root) + if model_source != "built-in-sm": + raise RuntimeError(f"unsupported built-in model source {model_source!r}") + implementation = hashlib.sha256() + for path in sorted((package_root / "models" / "builtin").glob("*.py")): + implementation.update(path.name.encode("utf-8") + b"\0") + implementation.update(path.read_bytes()) + implementation.update(b"\0") + digest = hashlib.sha256() + digest.update(b"built-in-sm\0") + digest.update(implementation.hexdigest().encode("ascii")) + return digest.hexdigest() + + +def _sm_heft_source_digest(package_root: Path) -> str: + path = package_root / "_internal" / "sm_heft.py" + module_name = "_pyamplicol_build_sm_heft_source" + spec = importlib.util.spec_from_file_location(module_name, path) + if spec is None or spec.loader is None: + raise RuntimeError(f"cannot load built-in scalar HEFT source from {path}") + module = importlib.util.module_from_spec(spec) + sys.modules[module_name] = module + try: + spec.loader.exec_module(module) + digest = module.source_digest() + finally: + sys.modules.pop(module_name, None) + return _required_sha256(digest, "built-in scalar HEFT source digest") + + +def _literal_assignment(path: Path, name: str) -> str | int: + tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path)) + for node in tree.body: + if not isinstance(node, (ast.Assign, ast.AnnAssign)): + continue + targets = node.targets if isinstance(node, ast.Assign) else (node.target,) + if not any( + isinstance(target, ast.Name) and target.id == name for target in targets + ): + continue + value = ast.literal_eval(node.value) + if isinstance(value, bool) or not isinstance(value, (str, int)): + break + return value + raise RuntimeError(f"could not read literal {name} from {path}") + + +def _load_prepared_contract(path: Path) -> ModuleType: + digest = hashlib.sha256(str(path).encode("utf-8")).hexdigest()[:16] + package_name = f"_pyamplicol_build_prepared_contract_{digest}" + models_package_name = f"{package_name}.models" + module_name = f"{models_package_name}.prepared" + package = ModuleType(package_name) + package.__package__ = package_name + package.__path__ = [str(path.parent.parent)] # type: ignore[attr-defined] + models_package = ModuleType(models_package_name) + models_package.__package__ = models_package_name + models_package.__path__ = [str(path.parent)] # type: ignore[attr-defined] + sys.modules[package_name] = package + sys.modules[models_package_name] = models_package + spec = importlib.util.spec_from_file_location(module_name, path) + if spec is None or spec.loader is None: + sys.modules.pop(package_name, None) + raise RuntimeError(f"cannot load prepared-model contract from {path}") + module = importlib.util.module_from_spec(spec) + sys.modules[module_name] = module + try: + spec.loader.exec_module(module) + except Exception: + for name in tuple(sys.modules): + if name == package_name or name.startswith(f"{package_name}."): + sys.modules.pop(name, None) + raise + return module + + +def _load_json(path: Path, context: str) -> dict[str, object]: + try: + value = json.loads(path.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError) as error: + raise RuntimeError(f"cannot read {context} {path}: {error}") from error + if not isinstance(value, dict) or not all(isinstance(key, str) for key in value): + raise RuntimeError(f"{context} must be a JSON object") + return cast(dict[str, object], value) + + +def _mapping(value: object, context: str) -> Mapping[str, object]: + if not isinstance(value, Mapping) or not all(isinstance(key, str) for key in value): + raise RuntimeError(f"{context} must be an object") + return cast(Mapping[str, object], value) + + +def _required_string(value: object, context: str) -> str: + if not isinstance(value, str) or not value: + raise RuntimeError(f"{context} must be a nonempty string") + return value + + +def _required_sha256(value: object, context: str) -> str: + digest = _required_string(value, context) + if len(digest) != 64 or any( + character not in "0123456789abcdef" for character in digest + ): + raise RuntimeError(f"{context} must be a SHA-256 digest") + return digest + + +def _require_exact_keys( + value: Mapping[str, object], expected: frozenset[str], context: str +) -> None: + missing = expected - value.keys() + unknown = value.keys() - expected + if missing or unknown: + details: list[str] = [] + if missing: + details.append("missing: " + ", ".join(sorted(missing))) + if unknown: + details.append("unknown: " + ", ".join(sorted(unknown))) + raise RuntimeError(f"{context} fields are invalid ({'; '.join(details)})") + + +def _plain_json(value: object) -> object: + if isinstance(value, Mapping): + return {str(key): _plain_json(item) for key, item in value.items()} + if isinstance(value, Sequence) and not isinstance(value, (str, bytes, bytearray)): + return [_plain_json(item) for item in value] + return value + + +__all__ = [ + "discard_release_packaged_prepared_model_store", + "project_release_packaged_prepared_model_store", + "stage_packaged_prepared_models", + "write_candidate_packaged_prepared_model_asset", + "write_release_packaged_prepared_model_asset", +] diff --git a/pyamplicol/build_backend/python_lock.py b/pyamplicol/build_backend/python_lock.py new file mode 100644 index 0000000000..7d4de5d020 --- /dev/null +++ b/pyamplicol/build_backend/python_lock.py @@ -0,0 +1,194 @@ +# SPDX-License-Identifier: 0BSD +"""Typed parser for the immutable Python runtime artifact lock.""" + +from __future__ import annotations + +import re +import tomllib +from dataclasses import dataclass +from pathlib import Path + +_SHA256 = re.compile(r"[0-9a-f]{64}") +_CANONICAL_NAME = re.compile(r"[-_.]+") + + +def canonicalize_name(value: str) -> str: + """Return the normalized distribution name used by package indexes.""" + + return _CANONICAL_NAME.sub("-", value).lower() + + +@dataclass(frozen=True, slots=True) +class PythonArtifact: + """One published wheel admitted by the runtime dependency contract.""" + + filename: str + sha256: str + + +@dataclass(frozen=True, slots=True) +class PythonPackage: + """One exact package in the resolved runtime dependency graph.""" + + distribution: str + version: str + license: str + direct: bool + dependencies: tuple[str, ...] + artifacts: tuple[PythonArtifact, ...] + + @property + def name(self) -> str: + return canonicalize_name(self.distribution) + + +@dataclass(frozen=True, slots=True) +class PythonRuntimeLock: + """Complete Python runtime graph and its admitted binary artifacts.""" + + requires_python: str + supported_python: tuple[str, ...] + packages: tuple[PythonPackage, ...] + + @property + def by_name(self) -> dict[str, PythonPackage]: + return {package.name: package for package in self.packages} + + @property + def direct_packages(self) -> tuple[PythonPackage, ...]: + return tuple(package for package in self.packages if package.direct) + + +def _required_string(table: dict[str, object], key: str, context: str) -> str: + value = table.get(key) + if not isinstance(value, str) or not value: + raise ValueError(f"{context} requires a nonempty {key!r} string") + return value + + +def _string_list(value: object, context: str) -> tuple[str, ...]: + if not isinstance(value, list) or not all( + isinstance(item, str) and item for item in value + ): + raise ValueError(f"{context} must be a list of nonempty strings") + result = tuple(value) + if len(result) != len(set(result)): + raise ValueError(f"{context} contains duplicate entries") + return result + + +def _parse_artifacts(raw: object, context: str) -> tuple[PythonArtifact, ...]: + if raw is None: + return () + if not isinstance(raw, list): + raise ValueError(f"{context}.artifacts must be an array of tables") + artifacts: list[PythonArtifact] = [] + for index, item in enumerate(raw): + item_context = f"{context}.artifacts[{index}]" + if not isinstance(item, dict): + raise ValueError(f"{item_context} must be a table") + filename = _required_string(item, "filename", item_context) + digest = _required_string(item, "sha256", item_context) + if filename != Path(filename).name or not filename.endswith(".whl"): + raise ValueError(f"{item_context}.filename must be a plain wheel filename") + if _SHA256.fullmatch(digest) is None: + raise ValueError(f"{item_context}.sha256 must be a lowercase SHA-256") + artifacts.append(PythonArtifact(filename=filename, sha256=digest)) + filenames = [artifact.filename for artifact in artifacts] + if filenames != sorted(filenames) or len(filenames) != len(set(filenames)): + raise ValueError(f"{context}.artifacts must be unique and filename-sorted") + return tuple(artifacts) + + +def load_python_runtime_lock(path: Path) -> PythonRuntimeLock: + """Load and structurally validate a schema-v1 Python runtime lock.""" + + with path.open("rb") as stream: + payload = tomllib.load(stream) + if payload.get("schema_version") != 1: + raise ValueError(f"{path} must use schema_version = 1") + requires_python = _required_string(payload, "requires_python", str(path)) + supported_python = _string_list( + payload.get("supported_python"), f"{path}.supported_python" + ) + if supported_python != tuple(sorted(supported_python)): + raise ValueError(f"{path}.supported_python must be sorted") + raw_packages = payload.get("packages") + if not isinstance(raw_packages, list) or not raw_packages: + raise ValueError(f"{path}.packages must be a nonempty array of tables") + + packages: list[PythonPackage] = [] + for index, item in enumerate(raw_packages): + context = f"{path}.packages[{index}]" + if not isinstance(item, dict): + raise ValueError(f"{context} must be a table") + distribution = _required_string(item, "distribution", context) + version = _required_string(item, "version", context) + license_expression = _required_string(item, "license", context) + direct = item.get("direct") + if not isinstance(direct, bool): + raise ValueError(f"{context}.direct must be a boolean") + dependencies = tuple( + canonicalize_name(value) + for value in _string_list( + item.get("dependencies"), f"{context}.dependencies" + ) + ) + if dependencies != tuple(sorted(dependencies)): + raise ValueError(f"{context}.dependencies must be canonical and sorted") + package = PythonPackage( + distribution=distribution, + version=version, + license=license_expression, + direct=direct, + dependencies=dependencies, + artifacts=_parse_artifacts(item.get("artifacts"), context), + ) + packages.append(package) + + names = [package.name for package in packages] + if names != sorted(names) or len(names) != len(set(names)): + raise ValueError(f"{path}.packages must be unique and canonical-name-sorted") + by_name = {package.name: package for package in packages} + for package in packages: + missing = set(package.dependencies) - set(by_name) + if missing: + raise ValueError( + f"{path} package {package.name} references missing dependencies: " + f"{sorted(missing)}" + ) + if package.name in package.dependencies: + raise ValueError(f"{path} package {package.name} depends on itself") + + reachable: set[str] = set() + pending = [package.name for package in packages if package.direct] + while pending: + name = pending.pop() + if name in reachable: + continue + reachable.add(name) + pending.extend(by_name[name].dependencies) + if reachable != set(by_name): + raise ValueError( + f"{path} contains unreachable packages: {sorted(set(by_name) - reachable)}" + ) + + filenames = [ + artifact.filename for package in packages for artifact in package.artifacts + ] + if len(filenames) != len(set(filenames)): + raise ValueError(f"{path} repeats a wheel filename across packages") + return PythonRuntimeLock( + requires_python=requires_python, + supported_python=supported_python, + packages=tuple(packages), + ) + + +__all__ = [ + "PythonArtifact", + "PythonPackage", + "PythonRuntimeLock", + "canonicalize_name", + "load_python_runtime_lock", +] diff --git a/pyamplicol/build_backend/sdk.py b/pyamplicol/build_backend/sdk.py new file mode 100644 index 0000000000..7182ed5fbf --- /dev/null +++ b/pyamplicol/build_backend/sdk.py @@ -0,0 +1,569 @@ +# SPDX-License-Identifier: 0BSD +"""Build, validate, and stage Rusticol's native SDK.""" + +from __future__ import annotations + +import json +import os +import re +import shlex +import shutil +import subprocess +import tempfile +import tomllib +from collections.abc import Mapping +from pathlib import Path +from typing import Any + +from native_build_identity import load_native_build_contract + +SDK_SOURCES = ( + ("rust/crates/rusticol-capi/include/rusticol.h", "include/rusticol.h"), + ("rust/crates/rusticol-capi/include/rusticol.hpp", "include/rusticol.hpp"), + ("rust/crates/rusticol-capi/fortran/rusticol.f90", "fortran/rusticol.f90"), +) +RUST_SDK_SOURCE = "src/pyamplicol/_sdk/rust/rusticol.rs" +RUST_SDK_WORKSPACE_MEMBER = "rust/crates/rusticol-capi" +NATIVE_MARKER = "native-static-libs:" +FORBIDDEN_UNDEFINED_SYMBOL_PREFIXES = ( + "pyobject", + "pyexc_", + "pygilstate", + "pyerr_", + "pylong_", + "pyunicode_", + "pymem_", + "pytype_", + "pytuple_", + "pylist_", + "pydict_", + "pymodule_", + "pyimport_", + "pybytes_", + "pycapsule_", + "pyfloat_", + "pybool_", + "pythread_", + "py_", + "python", + "libpython", + "pyo3", + "numpy", +) +FORBIDDEN_BINARY_MARKERS = ( + b"/opt/homebrew", + b"/opt/local", + b"/site-packages/", + b"/.venv/", + b"/venv/", + b"PREPARE_STANDALONE_PYAMPLICOL", + b"/pyAmpliCol/pyAmpliCol", +) +COMMON_LIBRARIES = { + "c", + "dl", + "m", + "pthread", + "resolv", + "rt", + "util", +} +MACOS_LIBRARIES = COMMON_LIBRARIES | {"System", "gcc_s", "iconv"} +LINUX_LIBRARIES = COMMON_LIBRARIES | {"gcc_s"} +MACOS_FRAMEWORKS = { + "CoreFoundation", + "IOKit", + "Security", + "SystemConfiguration", +} + + +def _host_target(root: Path) -> str: + completed = subprocess.run( + ["rustc", "-vV"], + cwd=root, + check=True, + capture_output=True, + text=True, + ) + for line in completed.stdout.splitlines(): + if line.startswith("host: "): + return line.removeprefix("host: ").strip() + raise RuntimeError("rustc -vV did not report a host target") + + +def _requested_target(host: str) -> str: + requested = ( + os.environ.get("CARGO_BUILD_TARGET") + or os.environ.get("MATURIN_BUILD_TARGET") + or host + ) + if requested != host: + raise RuntimeError( + "the native SDK bootstrap does not permit cross-target builds: " + f"host={host}, requested={requested}" + ) + return requested + + +def _cargo_messages(stdout: str) -> list[dict[str, Any]]: + messages: list[dict[str, Any]] = [] + for line in stdout.splitlines(): + if not line.strip(): + continue + try: + payload = json.loads(line) + except json.JSONDecodeError as error: + raise RuntimeError( + f"cargo emitted non-JSON output before SDK validation: {line}" + ) from error + if not isinstance(payload, dict): + raise RuntimeError("cargo JSON messages must be objects") + messages.append(payload) + return messages + + +def _cargo_fetch(root: Path, target_dir: Path, target: str) -> None: + completed = subprocess.run( + ["cargo", "fetch", "--locked", "--target", target], + cwd=root, + env=dict(os.environ, CARGO_TARGET_DIR=str(target_dir)), + check=False, + capture_output=True, + text=True, + ) + if completed.returncode != 0: + diagnostics = completed.stderr.strip() or completed.stdout.strip() + detail = f"\n{diagnostics}" if diagnostics else "" + raise RuntimeError( + f"cargo fetch failed with exit code {completed.returncode}{detail}" + ) + + +def _subprocess_excerpt(*streams: str, lines: int = 120) -> str: + output = "\n".join(stream.strip() for stream in streams if stream.strip()) + if not output: + return "" + return "\n".join(output.splitlines()[-lines:]) + + +def _static_library(messages: list[dict[str, Any]]) -> Path: + candidates: list[Path] = [] + for message in messages: + if message.get("reason") != "compiler-artifact": + continue + target = message.get("target") + if not isinstance(target, dict) or target.get("name") != "rusticol_capi": + continue + for filename in message.get("filenames", []): + path = Path(str(filename)) + if path.suffix in {".a", ".lib"}: + candidates.append(path) + unique = sorted(set(candidates)) + if len(unique) != 1: + raise RuntimeError( + "expected one rusticol-capi static archive, found " + + ", ".join(map(str, unique)) + ) + return unique[0] + + +def _native_tokens(stderr: str) -> list[str]: + matches = [ + line.split(NATIVE_MARKER, 1)[1].strip() + for line in stderr.splitlines() + if NATIVE_MARKER in line + ] + if len(matches) != 1: + raise RuntimeError( + f"expected one rustc native-static-libs diagnostic, found {len(matches)}" + ) + return shlex.split(matches[0]) + + +def _typed_link_arguments(tokens: list[str], target: str) -> dict[str, Any]: + libraries = MACOS_LIBRARIES if "apple-darwin" in target else LINUX_LIBRARIES + result: dict[str, Any] = { + "schema_version": 1, + "target": target, + "system_libraries": [], + "frameworks": [], + } + index = 0 + while index < len(tokens): + token = tokens[index] + if token == "-framework": + if "apple-darwin" not in target or index + 1 >= len(tokens): + raise RuntimeError(f"invalid native framework token: {token}") + framework = tokens[index + 1] + if framework not in MACOS_FRAMEWORKS: + raise RuntimeError(f"native framework is not allowlisted: {framework}") + if framework not in result["frameworks"]: + result["frameworks"].append(framework) + index += 2 + continue + if token.startswith("-l") and len(token) > 2: + library = token[2:] + if library not in libraries: + raise RuntimeError( + f"native system library is not allowlisted: {library}" + ) + # rustc may repeat native libraries contributed by more than one + # crate. Dynamic system libraries do not need repeated linker + # entries, so retain their first occurrence and its ordering. + if library not in result["system_libraries"]: + result["system_libraries"].append(library) + index += 1 + continue + if ( + token.startswith(("-L", "-Wl,")) + or "rpath" in token.lower() + or Path(token).is_absolute() + ): + raise RuntimeError(f"non-relocatable native link token: {token}") + raise RuntimeError(f"unsupported native link token: {token}") + return result + + +def _scan_archive(path: Path) -> None: + """Reject embedded host paths that would make the SDK non-relocatable. + + Python linkage is checked authoritatively by the undefined-symbol scan and + the standalone C link probe. Searching arbitrary compressed archive bytes + for short text such as ``pyo3`` produces ordinary random false positives. + """ + + archive_bytes = path.read_bytes() + lowered_archive = archive_bytes.lower() + for binary_marker in FORBIDDEN_BINARY_MARKERS: + if binary_marker.lower() in lowered_archive: + raise RuntimeError( + "Rusticol static archive embeds a non-relocatable path marker: " + f"{binary_marker.decode(errors='replace')}" + ) + + +def _scan_archive_symbols(path: Path) -> bool: + """Inspect undefined symbols when a compatible object reader is available. + + Rust tracks upstream LLVM more closely than Apple's command-line tools. An + older ``nm`` can therefore reject a valid Rust archive before the platform + linker does. Returning ``False`` in that case delegates the authoritative + check to :func:`_validate_archive_linkage`; a successfully parsed table is + still checked for forbidden Python dependencies here. + """ + + candidates: list[str] = [] + for candidate in ( + os.environ.get("LLVM_NM"), + shutil.which("llvm-nm"), + shutil.which("nm"), + ): + if candidate and candidate not in candidates: + candidates.append(candidate) + + for nm in candidates: + completed = subprocess.run( + [nm, "-u", str(path)], + check=False, + capture_output=True, + text=True, + ) + forbidden: list[str] = [] + for line in completed.stdout.splitlines(): + fields = line.split() + if len(fields) < 2 or fields[-2] not in {"U", "u", "W", "w", "V", "v"}: + continue + symbol = fields[-1] + normalized = symbol.removeprefix("_").lower() + if normalized.startswith(FORBIDDEN_UNDEFINED_SYMBOL_PREFIXES): + forbidden.append(symbol) + if forbidden: + raise RuntimeError( + "Rusticol static archive has undefined Python/PyO3/NumPy symbols: " + + ", ".join(sorted(set(forbidden))) + ) + if completed.returncode == 0: + return True + return False + + +def _native_c_compiler(target: str) -> list[str]: + configured = os.environ.get("CC") + if configured: + command = shlex.split(configured) + if command: + return command + names = ("clang", "cc") if "apple-darwin" in target else ("cc", "clang") + for name in names: + compiler = shutil.which(name) + if compiler: + return [compiler] + raise RuntimeError("a C compiler is required to validate the Rusticol SDK") + + +def _c_api_exports(header: Path) -> tuple[str, ...]: + exports = tuple( + dict.fromkeys( + re.findall( + r"\b(rusticol_[a-z0-9_]+)\s*\(", + header.read_text(encoding="utf-8"), + ) + ) + ) + if not exports or "rusticol_abi_version" not in exports: + raise RuntimeError( + f"Rusticol C header declares no complete public ABI: {header}" + ) + return exports + + +def _native_link_flags(link: Mapping[str, Any], target: str) -> list[str]: + if link.get("target") != target: + raise RuntimeError("Rusticol native link metadata targets the wrong platform") + flags: list[str] = [] + libraries = link.get("system_libraries") + frameworks = link.get("frameworks") + if not isinstance(libraries, list) or not all( + isinstance(item, str) for item in libraries + ): + raise RuntimeError("Rusticol native system-library metadata is invalid") + if not isinstance(frameworks, list) or not all( + isinstance(item, str) for item in frameworks + ): + raise RuntimeError("Rusticol native framework metadata is invalid") + flags.extend(f"-l{library}" for library in libraries) + for framework in frameworks: + flags.extend(("-framework", framework)) + return flags + + +def _validate_archive_linkage( + archive: Path, + *, + header: Path, + link: Mapping[str, Any], + target: str, +) -> None: + """Link and execute a probe that references every declared C ABI export.""" + + exports = _c_api_exports(header) + entries = ",\n".join(f" (rusticol_probe_fn){name}" for name in exports) + source = f"""\ +#include +#include \"rusticol.h\" + +typedef void (*rusticol_probe_fn)(void); +static rusticol_probe_fn volatile rusticol_api[] = {{ +{entries} +}}; + +int main(void) {{ + uint32_t (*abi_version)(void) = (uint32_t (*)(void))rusticol_api[0]; + return abi_version() == RUSTICOL_ABI_VERSION ? 0 : 1; +}} +""" + with tempfile.TemporaryDirectory(prefix="pyamplicol-sdk-link-") as temporary: + directory = Path(temporary) + probe_source = directory / "probe.c" + probe_binary = directory / "probe" + probe_source.write_text(source, encoding="utf-8") + command = [ + *_native_c_compiler(target), + "-std=c11", + "-O0", + "-I", + str(header.parent), + str(probe_source), + str(archive), + *_native_link_flags(link, target), + "-o", + str(probe_binary), + ] + completed = subprocess.run( + command, + check=False, + capture_output=True, + text=True, + ) + if completed.returncode != 0: + detail = (completed.stderr or completed.stdout).strip() + suffix = f": {detail}" if detail else "" + raise RuntimeError( + "Rusticol static archive failed the complete C ABI link probe" + suffix + ) + completed = subprocess.run( + [str(probe_binary)], + check=False, + capture_output=True, + text=True, + ) + if completed.returncode != 0: + detail = (completed.stderr or completed.stdout).strip() + suffix = f": {detail}" if detail else "" + raise RuntimeError( + "Rusticol static archive failed the C ABI execution probe" + suffix + ) + + +def _package_version(root: Path) -> str: + candidate = root / "src" / "pyamplicol" / "_build_info.json" + if candidate.is_file(): + payload = json.loads(candidate.read_text(encoding="utf-8")) + version = payload.get("version") + if isinstance(version, str) and version: + return version + raise RuntimeError("candidate build information has no package version") + with (root / "Cargo.toml").open("rb") as stream: + payload = tomllib.load(stream) + version = payload.get("workspace", {}).get("package", {}).get("version") + if not isinstance(version, str) or not version: + raise RuntimeError("Cargo workspace has no package version") + return version + + +def _ensure_sdk_workspace_member(root: Path) -> None: + """Restore the SDK crate that Maturin prunes from unpacked sdists.""" + + path = root / "Cargo.toml" + text = path.read_text(encoding="utf-8") + try: + members = tomllib.loads(text)["workspace"]["members"] + except (KeyError, TypeError, tomllib.TOMLDecodeError) as error: + raise RuntimeError("Cargo workspace member inventory is invalid") from error + if not isinstance(members, list) or not all( + isinstance(member, str) for member in members + ): + raise RuntimeError("Cargo workspace member inventory is invalid") + if RUST_SDK_WORKSPACE_MEMBER in members: + return + expected = [ + "rust/crates/rusticol-core", + "rust/crates/rusticol-python", + ] + if ( + members != expected + or not (root / RUST_SDK_WORKSPACE_MEMBER / "Cargo.toml").is_file() + ): + raise RuntimeError("Cargo workspace does not contain the Rusticol C SDK") + + workspace = re.search( + r"(?ms)^\[workspace\]\s*\n(?P.*?)(?=^\[|\Z)", + text, + ) + if workspace is None: + raise RuntimeError("Cargo workspace member inventory is invalid") + body = workspace.group("body") + updated_body, count = re.subn( + r"(?ms)^(members\s*=\s*)\[.*?\]", + rf'\g<1>["{expected[0]}", "{expected[1]}", ' + rf'"{RUST_SDK_WORKSPACE_MEMBER}"]', + body, + count=1, + ) + if count != 1: + raise RuntimeError("Cargo workspace member inventory is invalid") + path.write_text( + text[: workspace.start("body")] + updated_body + text[workspace.end("body") :], + encoding="utf-8", + ) + + +def build_sdk(root: Path, target_dir: Path) -> Path: + _ensure_sdk_workspace_member(root) + host = _host_target(root) + target = _requested_target(host) + sdk_contract = load_native_build_contract(root)["sdk"] + profile = str(sdk_contract["profile"]) + profile_arguments = ( + ["--release"] if profile == "release" else ["--profile", profile] + ) + environment = dict(os.environ, CARGO_TARGET_DIR=str(target_dir)) + _cargo_fetch(root, target_dir, target) + completed = subprocess.run( + [ + "cargo", + "rustc", + "--locked", + "--offline", + *profile_arguments, + "--package", + str(sdk_contract["package"]), + "--target", + target, + "--message-format=json-render-diagnostics", + "--", + "--print", + "native-static-libs", + *[str(value) for value in sdk_contract["rustc-codegen-arguments"]], + ], + cwd=root, + env=environment, + check=False, + capture_output=True, + text=True, + ) + if completed.returncode != 0: + detail = _subprocess_excerpt(completed.stdout, completed.stderr) + suffix = f"\n{detail}" if detail else "" + raise RuntimeError( + "Rusticol C SDK cargo build failed with exit code " + f"{completed.returncode}{suffix}" + ) + messages = _cargo_messages(completed.stdout) + archive = _static_library(messages) + if not archive.is_file(): + raise RuntimeError(f"cargo reported a missing static archive: {archive}") + link = _typed_link_arguments(_native_tokens(completed.stderr), target) + _scan_archive(archive) + _scan_archive_symbols(archive) + _validate_archive_linkage( + archive, + header=root / "rust/crates/rusticol-capi/include/rusticol.h", + link=link, + target=target, + ) + + staging = target_dir.parent / "wheel-data" / "_sdk" + if staging.is_symlink() or (staging.exists() and not staging.is_dir()): + raise RuntimeError(f"Rusticol SDK staging path is unsafe: {staging}") + if staging.exists(): + shutil.rmtree(staging) + rust_source = root / RUST_SDK_SOURCE + if not rust_source.is_file(): + raise RuntimeError(f"missing Rusticol SDK source: {rust_source}") + for source_name, destination_name in SDK_SOURCES: + source = root / source_name + if not source.is_file(): + raise RuntimeError(f"missing Rusticol SDK source: {source}") + destination = staging / destination_name + destination.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(source, destination) + + archive_name = ( + "rusticol_capi.lib" if archive.suffix == ".lib" else "librusticol_capi.a" + ) + library = staging / "lib" / archive_name + library.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(archive, library) + (staging / "link.json").write_text( + json.dumps(link, indent=2, sort_keys=True) + "\n", + encoding="utf-8", + ) + (staging / "metadata.json").write_text( + json.dumps( + { + "schema_version": 1, + "abi_version": 1, + "version": _package_version(root), + "target": target, + "archive": f"lib/{archive_name}", + "rust_source": "rust/rusticol.rs", + }, + indent=2, + sort_keys=True, + ) + + "\n", + encoding="utf-8", + ) + return staging diff --git a/pyamplicol/dependencies/README.md b/pyamplicol/dependencies/README.md new file mode 100644 index 0000000000..43ac0d267f --- /dev/null +++ b/pyamplicol/dependencies/README.md @@ -0,0 +1,146 @@ +# Dependency Modes + +pyAmpliCol defaults to published runtime dependencies and release-mode native +builds. Historical pinned-source candidate machinery is retained, but its +upstream pins need a coherent update before use with the current tensor APIs. +Release mode uses Symbolica **3.0.0** and SymJIT **2.26.4**. Historical candidate +mode retains SymJIT **2.26.0**; version numbers alone do not establish Python API +compatibility. Candidate builds do not +relabel dependency versions or modify upstream source files. + +## Release Mode + +`just dev-install` uses pip to resolve the Python runtime dependencies declared +in the root `pyproject.toml`, then replaces published Symbolica and +ufo-model-loader without resolving dependencies again. This replaces older +source candidates reporting the same version. The upcoming release requires +ufo-model-loader 1.0.0, which is not yet published. Until publication, explicitly +supply the locally built loader wheel for contributor validation: + +```console +just dev-install --loader-wheel /path/to/ufo_model_loader-1.0.0-py3-none-any.whl +PYTHON=.venv/bin/python just dev-test +``` + +The installer never chooses a local loader wheel automatically; `--loader-wheel` +is an explicit dependency-development override. Native build and staging use +the existing release-mode workflow. Building a new release wheel requires a +clean Git checkpoint and complete release assets; published-dependency mode +does not relax publication guards. For dirty development checkouts, +`--wheel-directory PATH` reuses an already-built compatible release wheel, or +`--dependencies-only` skips the project build and staging while leaving the +existing native runtime untouched. `--no-build` also skips runtime dependency +installation. +Developer tools and explicitly requested optional references may still be +set up with `--no-build`. No editable installation is used. `just dev-test` +defaults to release mode and includes a fresh build, so it also requires a clean +checkpoint. Dirty edits can use focused tests against the staged native runtime. +The developer dashboard retains its pinned Ratatui 0.4.2 build and FFI checkout +in both lanes; it is not a core runtime dependency. + +Release-equivalent builds use exact published versions. Canonical Cargo +resolution uses the published Symbolica 3.0.0 and SymJIT 2.26.4 crates without +source patches. `release-lock.toml` records those versions, and `Cargo.lock` +records normal registry resolution and checksums. The +ordinary locked Cargo build is the dependency check; no local dependency +patches or release-only source projections are needed. + +Python Symbolica 3.0.0 is published on PyPI. The required ufo-model-loader 1.0.0 +entry has no published wheel recorded in `python-runtime-lock.toml` yet. After +the loader is uploaded, record its official PyPI wheel metadata before running +the final release gates. A local wheel is not a published runtime-lock artifact. +Successful candidate builds do not establish release availability or make +candidate artifacts publishable. + +Prepared models in `src/pyamplicol/assets/prepared_models` are the source-runtime +inputs. Release pairs use the release-locked dependencies and also +live in the source-only `release_assets/prepared_models` store and are regenerated +through the manual `release-prepared-models.yml` +workflow using `release-lock.toml` and canonical `Cargo.lock`. Its temporary +bootstrap wheel is non-publishable and omits prepared models and the portable +self-test fixture. The release overlay installs the release pairs at canonical +package paths and removes the auxiliary store, so the retained sdist contains +only release payloads. Prepared-pack compatibility uses model/compiler +identities, storage and plane ABIs, target, and payload hashes—not a separate +dependency checkout fingerprint. + +## Historical Candidate Mode (Opt-in) + +The retained `--candidate` dependency-development machinery selects managed +checkouts under `dependencies/checkouts`, pinned by `contributor-lock.toml`. +These historical pins are incompatible with the +current tensor implementation and must be updated together before use; this +is not a working current-source setup route. The retained pin table is: + +| Dependency | Upstream source | Revision | +| --- | --- | --- | +| Symbolica 3.0.0, including Numerica and Graphica | `symbolica-dev/symbolica`, `main` | `7b31114c7a77571aabacf96a311f27ef0f44d007` | +| SymJIT 2.26.0 | `siravan/symjit-crate` | `530304a07d1be6d5abc80291aa5e92cf28ac5546` | +| GammaLoop, including Spenso/Idenso/Vakint | `alphal00p/gammaloop`, `main` | `312cf6aaf4cd1414f6742ad87ce39922c497a0b6` | +| symbolica-integrate 2.0.1 | `symbolica-dev/symbolica-integrate`, `main` | `716354a07f2660b38390c95702aa617d1f9472c7` | +| ufo-model-loader 0.1.8 | `alphal00p/ufo_model_loader`, `main` | `70ddee6b416f8c8b340e0d087646d77095c5d24b` | + +The pinned Symbolica source declares 3.0.0, matching the loader's genuine +`symbolica>=3.0` requirement. The loader lives at +`dependencies/checkouts/ufo-model-loader`; no separate future-loader checkout +is needed. The symbolica-community revision is recorded in +`contributor-lock.toml`. + +The generated candidate Cargo overlay selects these upstream sources for both +pyAmpliCol and the community extension without changing canonical release +resolution. The installer configures the community build manifest with the +managed source paths and Symbolica's matching allocator; upstream Rust source +files remain unchanged. Rusticol enables Symbolica's +`native_code_generation` feature. SymJIT supplies the standard P-kernel +interface; Rusticol owns scheduling, factors, accumulation, fanout, and artifact +binding. Its plane adapter uses actual row indices and identity output, with +descriptor lifetime, alignment, aliasing, and mutability checked by the caller. + +Candidate mode is not for PyPI publication. Its retained test selector, +`PYAMPLICOL_BUILD_MODE=candidate`, does not make the historical pins compatible. +The retained build machinery uses the tracked prepared-model packs and +portable self-test fixture; an explicitly requested bootstrap omits both and +is non-publishable. Neither bootstrap nor `--update` repairs incompatible +upstream pins: `--update` only moves managed checkouts to the recorded revisions. +`--reset` archives managed state in the workspace-local `.trash` store before +recreating it. + +## Optional Profiling References + +These opt-ins are independent of the historical candidate machinery. + +The original Fortran AmpliCol checkout is optional, developer-only, and used +only as an independent validation and benchmarking reference. Enable it with +`just dev-install --with-legacy-amplicol`. The pinned +`amplicol_with_patches` branch removes the unnecessary LHAPDF +link from its direct color probe, exposes complete recursion-kind diagnostics, +and reports each LC contraction-row partition. This +resolves the physical color component only for a genuinely single-flow case; +multi-flow fixtures use the rows only to verify the complete per-helicity +aggregate. None modifies +amplitude physics. `just legacy-physics` builds that probe and checks the +tracked low-multiplicity LC/NLC/full fixture, including every physical +helicity and every independently resolvable color component, against the +pinned Fortran implementation. + +The pinned upstream revision contains no `LICENSE` or `COPYING` file. That fact +is recorded in contributor-only provenance, not the release dependency lock. +The checkout and its developer-only branch are never redistributed in a wheel +or sdist. + +The Reference FFT checkout is likewise developer-only and is used only by the +FFT profiling and hardware-sensitive acceptance tools. Enable it with +`just dev-install --with-reference-fft`, which clones the exact +public `AllGluonsMultipletFFT` revision recorded in `contributor-lock.toml` +into `dependencies/checkouts/reference-fft`. That repository keeps the +upstream contraction implementation and adds the calibrated +batch/helicity-sum benchmark interface plus a high-multiplicity roundoff guard. +It is never redistributed in a wheel or sdist. Ordinary `just dev-install` +runs omit both external references. Enable both for the complete FFT profiling +toolchain with +`just dev-install --with-legacy-amplicol --with-reference-fft`. +Enabling either external profiling reference also installs the project-owned +`fft-profiling` Python extra into `.venv`; no editable project reinstall is +required or supported. When updating a source tree that already has the former +Reference FFT checkout, add `--update`; the installer migrates its managed +`origin` to the public repository before fetching the pinned revision. diff --git a/pyamplicol/dependencies/contributor-lock.toml b/pyamplicol/dependencies/contributor-lock.toml new file mode 100644 index 0000000000..e5eb168114 --- /dev/null +++ b/pyamplicol/dependencies/contributor-lock.toml @@ -0,0 +1,69 @@ +# SPDX-License-Identifier: 0BSD +# Source-checkout-only dependency inputs for non-publishable contributor builds. +schema_version = 1 + +[abis] +symbolica_serialization = "symbolica-bincode2-v1" +symjit_application = "symjit-application-storage-v3" +symjit_plane_application = "pyamplicol-symjit-plane-application-v2" + +[symbolica] +source_url = "https://github.com/symbolica-dev/symbolica.git" +candidate_version = "3.0.0" +candidate_revision = "7b31114c7a77571aabacf96a311f27ef0f44d007" +community_url = "https://github.com/symbolica-dev/symbolica-community.git" +community_revision = "98740d198fb6667c63c0de7cffd4328b5dca57b0" +release_status = "merged-main-candidate" + +[gammaloop_candidate] +source_url = "https://github.com/alphal00p/gammaloop.git" +branch = "main" +revision = "312cf6aaf4cd1414f6742ad87ce39922c497a0b6" +release_status = "developer-build-input-only" + +[symjit] +candidate_version = "2.26.0" +repository = "https://github.com/siravan/symjit-crate.git" +revision = "530304a07d1be6d5abc80291aa5e92cf28ac5546" + +[symbolica_integrate] +source_url = "https://github.com/symbolica-dev/symbolica-integrate.git" +# Unmodified upstream main with Symbolica 3.0 support. +revision = "716354a07f2660b38390c95702aa617d1f9472c7" + +[ratatui] +distribution = "ratatui" +version = "0.4.2" +source_url = "https://files.pythonhosted.org/packages/83/ef/61a985816192cd3ee50ad5570a72f1fc6d22b86047771d5b72a05bbdc3eb/ratatui-0.4.2.tar.gz" +sdist_sha256 = "2778b066378f8e1629b4b5e1076f1957c8c439b45cdeaf51970d1949730bb0d5" +ffi_repository = "https://github.com/holo-q/ratatui-ffi.git" +ffi_revision = "7249c0bd1445c0c6ee76f3f24923eb35a1d931e0" +release_status = "developer-build-input-only" +license = "MIT" + +[ufo_model_loader] +source_url = "https://github.com/alphal00p/ufo_model_loader.git" +candidate_revision = "70ddee6b416f8c8b340e0d087646d77095c5d24b" +latest_verified_published_version = "0.1.8" +published_revision = "f5a9f8d2eaff76b99629fc3143952ca4f2425d55" +wheel_sha256 = "b253942f0a356eacd5fd68b104cacf11039c2391a9ee963107f674fad54f01ef" +release_status = "verified" +license = "MIT" + +[legacy_amplicol] +source_url = "git@github.com:rikkert-frederix/AmpliCol.git" +branch = "amplicol_with_patches" +revision = "dcb3efbcbe89c51969818cddf48113fba19ad763" +reference_fixture_revision = "f3fdca35631c9405d28337a79cdab84994d8e414" +release_status = "developer-only" +license = "NOASSERTION" +redistributed = false +license_evidence = "No LICENSE or COPYING file exists at the pinned branch revision" + +[reference_fft] +source_url = "https://github.com/rikkert-frederix/AllGluonsMultipletFFT.git" +branch = "main" +revision = "9c3cb4fb4658200884553bab796e85bd5e7fe7a9" +release_status = "developer-only" +license = "NOASSERTION" +redistributed = false diff --git a/pyamplicol/dependencies/install_dependencies.py b/pyamplicol/dependencies/install_dependencies.py new file mode 100644 index 0000000000..6b815e5e59 --- /dev/null +++ b/pyamplicol/dependencies/install_dependencies.py @@ -0,0 +1,1473 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: 0BSD +"""Create a contributor environment using published dependencies by default.""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import os +import re +import shlex +import shutil +import subprocess +import sys +import tempfile +import tomllib +import urllib.request +from collections.abc import Sequence +from dataclasses import dataclass +from datetime import UTC, datetime +from pathlib import Path +from typing import Any + +ROOT = Path(__file__).resolve().parents[1] +DEPENDENCIES = ROOT / "dependencies" +PYPROJECT = ROOT / "pyproject.toml" +RELEASE_LOCK = DEPENDENCIES / "release-lock.toml" +CONTRIBUTOR_LOCK = DEPENDENCIES / "contributor-lock.toml" +# Retained as the public constant used by older contributor-side callers. +LOCK = RELEASE_LOCK +PYTHON_LOCK = DEPENDENCIES / "python-runtime-lock.toml" +CHECKOUTS = DEPENDENCIES / "checkouts" +WHEELHOUSE = DEPENDENCIES / "wheelhouse" +VENV = ROOT / ".venv" +STATE = DEPENDENCIES / "install-state.json" +CANDIDATE_LOCK = DEPENDENCIES / "candidate-Cargo.lock" +CARGO_CONFIG = DEPENDENCIES / "candidate-cargo-config.toml" +ARTIFACTS = ROOT / ".artifacts" / "candidate" +RELEASE_ARTIFACTS = ROOT / ".artifacts" / "development-release" +TRASH = ROOT / ".trash" + +sys.path.insert(0, str(ROOT / "build_backend")) +from python_lock import load_python_runtime_lock # noqa: E402 + +_WORKSPACE_CRATES = frozenset( + { + "rusticol-capi", + "rusticol-core", + "rusticol-python", + } +) +_CANDIDATE_PATH_CRATES = frozenset( + { + "graphica", + "numerica", + "symbolica", + "symjit", + } +) +_CRATES_IO_SOURCE = "registry+https://github.com/rust-lang/crates.io-index" +_SHA256_PATTERN = re.compile(r"[0-9a-f]{64}") + + +class SetupError(RuntimeError): + """Contributor setup could not be completed safely.""" + + +@dataclass(frozen=True) +class Source: + key: str + url: str + revision: str + branch: str | None = None + + @property + def path(self) -> Path: + return CHECKOUTS / self.key + + +class Runner: + def __init__(self, *, dry_run: bool) -> None: + self.dry_run = dry_run + + def run( + self, + command: Sequence[str | os.PathLike[str]], + *, + cwd: Path | None = None, + env: dict[str, str] | None = None, + capture: bool = False, + check: bool = True, + ) -> subprocess.CompletedProcess[str]: + rendered = [str(item) for item in command] + suffix = f" # cwd={cwd}" if cwd else "" + print(f"$ {shlex.join(rendered)}{suffix}") + if self.dry_run: + return subprocess.CompletedProcess(rendered, 0, "", "") + completed = subprocess.run( + rendered, + cwd=cwd, + env=env, + text=True, + capture_output=capture, + ) + if check and completed.returncode != 0: + if capture: + print(completed.stdout, end="") + print(completed.stderr, end="", file=sys.stderr) + raise SetupError( + f"command exited with {completed.returncode}: {shlex.join(rendered)}" + ) + return completed + + +def _load_lock(path: Path, description: str) -> dict[str, Any]: + with path.open("rb") as stream: + payload = tomllib.load(stream) + if payload.get("schema_version") != 1: + raise SetupError(f"unsupported {description} schema") + return payload + + +def _release_lock() -> dict[str, Any]: + return _load_lock(RELEASE_LOCK, "dependency release-lock") + + +def _contributor_lock() -> dict[str, Any]: + return _load_lock(CONTRIBUTOR_LOCK, "dependency contributor-lock") + + +def _lock() -> dict[str, Any]: + """Return the contributor setup view without polluting release metadata.""" + + release = _release_lock() + contributor = _contributor_lock() + payload = dict(release) + for key, value in contributor.items(): + if isinstance(value, dict) and isinstance(payload.get(key), dict): + merged = dict(payload[key]) + merged.update(value) + payload[key] = merged + else: + payload[key] = value + return payload + + +def _sources( + payload: dict[str, Any], + *, + with_legacy: bool, + with_reference_fft: bool, +) -> tuple[Source, ...]: + symbolica = payload["symbolica"] + symjit = payload["symjit"] + gammaloop = payload["gammaloop_candidate"] + integrate = payload["symbolica_integrate"] + loader = payload["ufo_model_loader"] + overrides = _root_path_patches() + sources = [ + Source( + "symbolica", + str(symbolica["source_url"]), + str(symbolica["candidate_revision"]), + ), + Source( + "symbolica-community", + str(symbolica["community_url"]), + str(symbolica["community_revision"]), + ), + Source( + "ufo-model-loader", + str(loader["source_url"]), + str(loader["candidate_revision"]), + ), + ] + if "symjit" not in overrides: + sources.insert( + 0, Source("symjit", str(symjit["repository"]), str(symjit["revision"])) + ) + if "spenso" not in overrides: + sources.append( + Source( + "gammaloop", + str(gammaloop["source_url"]), + str(gammaloop["revision"]), + str(gammaloop["branch"]), + ) + ) + if "symbolica-integrate" not in overrides: + sources.append( + Source( + "symbolica-integrate", + str(integrate["source_url"]), + str(integrate["revision"]), + ) + ) + sources.extend( + _developer_sources( + payload, with_legacy=with_legacy, with_reference_fft=with_reference_fft + ) + ) + return tuple(sources) + + +def _developer_sources( + payload: dict[str, Any], *, with_legacy: bool, with_reference_fft: bool +) -> tuple[Source, ...]: + """Keep dashboard tooling independent of the selected runtime dependencies.""" + + ratatui = payload["ratatui"] + return ( + Source( + "ratatui-ffi", str(ratatui["ffi_repository"]), str(ratatui["ffi_revision"]) + ), + *_reference_sources( + payload, with_legacy=with_legacy, with_reference_fft=with_reference_fft + ), + ) + + +def _reference_sources( + payload: dict[str, Any], *, with_legacy: bool, with_reference_fft: bool +) -> tuple[Source, ...]: + sources: list[Source] = [] + if with_legacy: + legacy = payload["legacy_amplicol"] + sources.append( + Source( + "legacy-amplicol", + str(legacy["source_url"]), + str(legacy["revision"]), + str(legacy["branch"]), + ) + ) + if with_reference_fft: + reference = payload["reference_fft"] + sources.append( + Source( + "reference-fft", + str(reference["source_url"]), + str(reference["revision"]), + str(reference["branch"]), + ) + ) + return tuple(sources) + + +def _venv_python() -> Path: + return VENV / ("Scripts/python.exe" if os.name == "nt" else "bin/python") + + +def _venv_environment() -> dict[str, str]: + environment = dict(os.environ) + environment.pop("PYTHONPATH", None) + environment.pop("PYTHONHOME", None) + environment["VIRTUAL_ENV"] = str(VENV) + binary = VENV / ("Scripts" if os.name == "nt" else "bin") + environment["PATH"] = str(binary) + os.pathsep + environment.get("PATH", "") + return environment + + +def _require_tools() -> None: + missing = [name for name in ("cargo", "git", "rustc") if shutil.which(name) is None] + if missing: + raise SetupError("missing contributor tools: " + ", ".join(sorted(missing))) + + +def _managed_trash_destination(name: str, *, workspace_root: Path) -> Path: + """Return one new recoverable destination inside workspace-local trash.""" + + relative = Path(name) + if ( + relative.is_absolute() + or len(relative.parts) != 1 + or relative.name in {"", ".", ".."} + ): + raise SetupError(f"invalid managed dependency trash entry: {name!r}") + if workspace_root.is_symlink() or TRASH.is_symlink(): + raise SetupError( + "managed dependency workspace and trash must not be symbolic links: " + f"{workspace_root}, {TRASH}" + ) + resolved_workspace = workspace_root.resolve(strict=False) + resolved_trash = TRASH.resolve(strict=False) + try: + resolved_trash.relative_to(resolved_workspace) + except ValueError as error: + raise SetupError( + f"managed dependency trash resolves outside the workspace: {TRASH}" + ) from error + destination = TRASH / relative + try: + destination.resolve(strict=False).relative_to(resolved_trash) + except ValueError as error: + raise SetupError( + f"managed dependency trash entry escapes its root: {destination}" + ) from error + if destination.exists() or destination.is_symlink(): + raise SetupError( + f"managed dependency trash destination already exists: {destination}" + ) + return destination + + +def _archive_managed_state(runner: Runner) -> None: + managed = ( + VENV, + CHECKOUTS, + WHEELHOUSE, + STATE, + CANDIDATE_LOCK, + CARGO_CONFIG, + ARTIFACTS, + ) + present = [path for path in managed if path.exists() or path.is_symlink()] + if not present: + return + stamp = datetime.now(UTC).strftime("%Y%m%dT%H%M%S%fZ") + destination = _managed_trash_destination( + f"dependency-reset-{stamp}", + workspace_root=ROOT, + ) + for path in present: + relative = path.relative_to(ROOT) + target = destination / relative + print(f"$ mv {shlex.quote(str(path))} {shlex.quote(str(target))}") + if runner.dry_run: + continue + target.parent.mkdir(parents=True, exist_ok=True) + shutil.move(str(path), str(target)) + + +def _git_head(runner: Runner, path: Path) -> str: + completed = runner.run( + ["git", "rev-parse", "HEAD"], + cwd=path, + capture=True, + ) + return completed.stdout.strip() + + +def _managed_checkout(name: str) -> Path: + """Return one exact managed checkout child that cannot escape by symlink.""" + + relative = Path(name) + if ( + relative.is_absolute() + or len(relative.parts) != 1 + or relative.name in {"", ".", ".."} + ): + raise SetupError(f"invalid managed dependency checkout name: {name!r}") + destination = CHECKOUTS / relative + workspace_anchor = CHECKOUTS.parent.parent + try: + for path in ( + workspace_anchor, + CHECKOUTS.parent, + CHECKOUTS, + destination, + ): + if path.is_symlink(): + raise SetupError( + "managed dependency checkout path must not be a symbolic link: " + f"{path}" + ) + workspace_root = workspace_anchor.resolve(strict=False) + checkout_root = CHECKOUTS.resolve(strict=False) + resolved_destination = destination.resolve(strict=False) + except OSError as error: + raise SetupError( + f"could not validate managed checkout path {destination}: {error}" + ) from error + try: + checkout_root.relative_to(workspace_root) + resolved_relative = resolved_destination.relative_to(checkout_root) + except ValueError as error: + raise SetupError( + "managed dependency checkout resolves outside the workspace root: " + f"{destination}" + ) from error + if resolved_relative != relative: + raise SetupError( + "managed dependency path does not resolve to the expected checkout: " + f"{destination}" + ) + return destination + + +def _root_path_patches() -> dict[str, Path]: + """Read explicit contributor overrides from the ordinary Cargo manifest.""" + + with (ROOT / "Cargo.toml").open("rb") as stream: + patches = tomllib.load(stream).get("patch", {}).get("crates-io", {}) + return { + name: (ROOT / specification["path"]).resolve() + for name, specification in patches.items() + if isinstance(specification, dict) and "path" in specification + } + + +def _managed_symjit_checkout() -> Path: + """Use the explicit local SymJIT override, or the pinned managed checkout.""" + + return _root_path_patches().get("symjit") or _managed_checkout("symjit") + + +def _checkout(runner: Runner, source: Source, *, update: bool) -> None: + destination = _managed_checkout(source.key) + if destination.exists() and not (destination / ".git").exists(): + stamp = datetime.now(UTC).strftime("%Y%m%dT%H%M%S%fZ") + archived = _managed_trash_destination( + f"superseded-{source.key}-{stamp}", + workspace_root=ROOT, + ) + print(f"$ mv {shlex.quote(str(destination))} {shlex.quote(str(archived))}") + if not runner.dry_run: + archived.parent.mkdir(parents=True, exist_ok=True) + shutil.move(str(destination), str(archived)) + if not destination.exists(): + destination.parent.mkdir(parents=True, exist_ok=True) + clone_command = [ + "git", + "clone", + "--filter=blob:none", + ] + if source.branch is not None: + clone_command.extend(["--branch", source.branch, "--single-branch"]) + clone_command.extend(["--no-checkout", source.url, str(destination)]) + runner.run(clone_command) + runner.run( + ["git", "checkout", "--detach", source.revision], + cwd=destination, + ) + return + if runner.dry_run: + print(f"# verify {source.key} at {source.revision}") + return + head = _git_head(runner, destination) + if head == source.revision: + return + if not update: + raise SetupError( + f"{destination} is at {head}, expected {source.revision}; " + "rerun with --update or --reset" + ) + # The pinned source URL may move while retaining the same managed checkout + # name. Refresh origin before fetching so --update also migrates existing + # contributor checkouts away from an obsolete or private repository. + runner.run( + ["git", "remote", "set-url", "origin", source.url], + cwd=destination, + ) + fetch_ref = source.branch or source.revision + runner.run(["git", "fetch", "origin", fetch_ref], cwd=destination) + runner.run( + ["git", "checkout", "--detach", source.revision], + cwd=destination, + ) + + +def _replace_section(text: str, name: str, body: str) -> str: + pattern = re.compile(rf"(?ms)^\[{re.escape(name)}\]\n.*?(?=^\[[^\n]+\]\n|\Z)") + replacement = f"[{name}]\n{body.strip()}\n\n" + if not pattern.search(text): + return text.rstrip() + "\n\n" + replacement + return pattern.sub(replacement, text, count=1) + + +def _require_symjit_rlib_manifest( + path: Path, + *, + expected_version: str | None = None, +) -> None: + """Require the immutable upstream manifest to expose an rlib library.""" + + try: + with path.open("rb") as stream: + manifest = tomllib.load(stream) + except (OSError, tomllib.TOMLDecodeError) as error: + raise SetupError( + f"could not validate managed SymJIT manifest: {error}" + ) from error + package = manifest.get("package") + if not isinstance(package, dict) or package.get("name") != "symjit": + raise SetupError("managed SymJIT source has the wrong package identity") + if expected_version is not None and str(package.get("version")) != expected_version: + raise SetupError( + "managed SymJIT source has the wrong package version: " + f"expected {expected_version}, got {package.get('version')!r}" + ) + library = manifest.get("lib") + if not isinstance(library, dict) or library.get("crate-type") != ["rlib"]: + raise SetupError("managed SymJIT source must expose an rlib-only library") + + +def _configure_source_manifests(runner: Runner) -> None: + symjit = _managed_symjit_checkout() + symbolica = _managed_checkout("symbolica") + community = _managed_checkout("symbolica-community") + gammaloop = _managed_checkout("gammaloop") + overrides = _root_path_patches() + integrate = overrides.get("symbolica-integrate") or _managed_checkout( + "symbolica-integrate" + ) + if runner.dry_run: + print("# configure community Cargo paths; leave dependency sources unchanged") + return + _require_symjit_rlib_manifest( + symjit / "Cargo.toml", + expected_version=str(_contributor_lock()["symjit"]["candidate_version"]), + ) + + paths = { + "graphica": symbolica / "lib" / "graphica", + "numerica": symbolica / "lib" / "numerica", + "symbolica": symbolica, + "symjit": symjit, + "symbolica-integrate": integrate, + } + paths.update( + (name, gammaloop / "crates" / name) + for name in ( + "idenso", + "linnet", + "spenso", + "spenso-hep-lib", + "spenso-macros", + "spynso3", + "symbolica-utils", + "vakint", + ) + ) + paths.update(overrides) + quoted = {name: json.dumps(str(path.resolve())) for name, path in paths.items()} + dependencies = ( + 'example_extension = { path = "example_extension", default-features = false }\n' + f"idenso = {{ path = {quoted['idenso']}, default-features = false, " + 'features = ["bincode", "python"] }\n' + f"spynso3 = {{ path = {quoted['spynso3']}, default-features = false }}\n" + f"symbolica = {{ path = {quoted['symbolica']}, default-features = false, " + 'features = ["python_export"] }\n' + f"symbolica-integrate = {{ path = {quoted['symbolica-integrate']}, " + 'default-features = false, features = ["compressed-step-metadata"] }\n' + 'pyo3 = { version = "0.28" }\n' + 'pyo3-stub-gen = { version = "0.17", optional = true, ' + 'default-features = false, features = ["numpy"] }\n' + ) + native_dependencies = f""" +vakint = {{ path = {quoted["vakint"]}, features = [ + "symbolica_community_module", +] }} +# Follow Symbolica's allocator to avoid duplicate native mimalloc links. +mimalloc = {{ package = "rustfs-mimalloc", version = "0.5.4", features = [ + "local_dynamic_tls", +] }} +""" + patches = "\n".join( + f"{name} = {{ path = {path} }}" for name, path in quoted.items() + ) + community_cargo = community / "Cargo.toml" + text = community_cargo.read_text(encoding="utf-8") + if not re.search(r"(?m)^\[workspace\]\s*$", text): + # Managed checkouts live below pyAmpliCol's workspace directory but are + # independent build inputs. An explicit empty workspace prevents Cargo + # from adopting this package into the nearest ancestor workspace. + text = "[workspace]\n\n" + text + text = _replace_section(text, "dependencies", dependencies) + text = _replace_section( + text, + "target.'cfg(not(target_arch = \"wasm32\"))'.dependencies", + native_dependencies, + ) + text = _replace_section(text, "patch.crates-io", patches) + text = _replace_section( + text, + 'patch."https://github.com/symbolica-dev/symbolica"', + "\n".join( + f"{name} = {{ path = {quoted[name]} }}" + for name in ("symbolica", "numerica", "graphica") + ), + ) + community_cargo.write_text(text.rstrip() + "\n", encoding="utf-8") + + example = community / "example_extension" / "Cargo.toml" + text = example.read_text(encoding="utf-8") + text = re.sub( + r"(?m)^symbolica\s*=\s*\{[^\n]*\}\s*$", + f"symbolica = {{ path = {quoted['symbolica']}, default-features = false, " + 'features = ["python_export"] }', + text, + count=1, + ) + example.write_text(text.rstrip() + "\n", encoding="utf-8") + + +def _configure_sources(runner: Runner) -> None: + _configure_source_manifests(runner) + community = _managed_checkout("symbolica-community") + if runner.dry_run: + print("# restore the upstream symbolica-community Cargo.lock") + else: + upstream_lock = runner.run( + ["git", "show", "HEAD:Cargo.lock"], + cwd=community, + capture=True, + ).stdout + (community / "Cargo.lock").write_text(upstream_lock, encoding="utf-8") + # Resolve source substitutions, retaining compatible upstream lock entries. + runner.run( + ["cargo", "metadata", "--format-version", "1"], + cwd=community, + capture=True, + ) + runner.run( + ["cargo", "metadata", "--locked", "--format-version", "1"], + cwd=community, + capture=True, + ) + + +def _write_cargo_config(runner: Runner) -> None: + symjit = _managed_symjit_checkout() + symbolica = _managed_checkout("symbolica") + entries = { + "graphica": symbolica / "lib" / "graphica", + "numerica": symbolica / "lib" / "numerica", + "symbolica": symbolica, + "symjit": symjit, + } + entries.update(_root_path_patches()) + text = ["# Generated by dependencies/install_dependencies.py", "[patch.crates-io]"] + text.extend( + f"{name} = {{ path = {json.dumps(str(path.resolve()))} }}" + for name, path in entries.items() + ) + print(f"# write {CARGO_CONFIG}") + if runner.dry_run: + return + CARGO_CONFIG.parent.mkdir(parents=True, exist_ok=True) + CARGO_CONFIG.write_text("\n".join(text) + "\n", encoding="utf-8") + + +def _cargo_lock_packages(path: Path) -> list[dict[str, Any]]: + try: + with path.open("rb") as stream: + payload = tomllib.load(stream) + except (OSError, tomllib.TOMLDecodeError) as error: + raise SetupError(f"invalid Cargo lock file {path}: {error}") from error + packages = payload.get("package") + if payload.get("version") != 4 or not isinstance(packages, list): + raise SetupError(f"Cargo lock file {path} must use format version 4") + return packages + + +def _validate_release_cargo_lock(path: Path) -> None: + """Require ordinary registry resolution for every non-workspace crate.""" + invalid: list[str] = [] + for package in _cargo_lock_packages(path): + name = str(package.get("name", "")) + source = package.get("source") + checksum = package.get("checksum") + if name in _WORKSPACE_CRATES and source is None and checksum is None: + continue + if source != _CRATES_IO_SOURCE: + invalid.append(f"{name} has an unexpected source {source!r}") + continue + if not isinstance(checksum, str) or _SHA256_PATTERN.fullmatch(checksum) is None: + invalid.append(f"{name} has no valid registry checksum") + if invalid: + raise SetupError( + "canonical Cargo.lock is not release-resolved; regenerate it " + "without the candidate Cargo patch configuration:\n " + + "\n ".join(invalid) + ) + + +def _validate_candidate_cargo_lock(path: Path) -> None: + """Require only the managed patch crates to use local path resolution.""" + + packages = _cargo_lock_packages(path) + local_names = { + str(package.get("name")) + for package in packages + if package.get("source") is None and package.get("checksum") is None + } + expected_local = _WORKSPACE_CRATES | _CANDIDATE_PATH_CRATES + missing = sorted(expected_local - local_names) + unexpected = sorted(local_names - expected_local) + invalid_registry: list[str] = [] + for package in packages: + name = str(package.get("name", "")) + if name in expected_local: + continue + source = package.get("source") + checksum = package.get("checksum") + if ( + source != _CRATES_IO_SOURCE + or not isinstance(checksum, str) + or _SHA256_PATTERN.fullmatch(checksum) is None + ): + invalid_registry.append(name) + if missing or unexpected or invalid_registry: + details = [] + if missing: + details.append("missing local crates: " + ", ".join(missing)) + if unexpected: + details.append("unexpected local crates: " + ", ".join(unexpected)) + if invalid_registry: + details.append( + "invalid registry crates: " + ", ".join(sorted(invalid_registry)) + ) + raise SetupError("invalid candidate Cargo lock: " + "; ".join(details)) + + +def _write_candidate_lock(runner: Runner) -> None: + if runner.dry_run: + print(f"# resolve {CANDIDATE_LOCK} from canonical Cargo.lock") + return + release_lock = ROOT / "Cargo.lock" + release_lock_bytes = release_lock.read_bytes() + with tempfile.TemporaryDirectory(prefix="pyamplicol-candidate-lock-") as raw: + temporary = Path(raw) + shutil.copy2(ROOT / "Cargo.toml", temporary / "Cargo.toml") + shutil.copy2(release_lock, temporary / "Cargo.lock") + shutil.copytree(ROOT / "rust", temporary / "rust") + _rewrite_candidate_requirements(temporary) + config = temporary / ".cargo" / "config.toml" + config.parent.mkdir(parents=True) + shutil.copy2(CARGO_CONFIG, config) + # Resolving from the release lock preserves every unrelated registry + # version while replacing only the explicitly patched candidate crates. + runner.run( + ["cargo", "metadata", "--format-version", "1"], + cwd=temporary, + capture=True, + ) + _validate_candidate_cargo_lock(temporary / "Cargo.lock") + runner.run( + ["cargo", "metadata", "--locked", "--format-version", "1"], + cwd=temporary, + capture=True, + ) + shutil.copy2(temporary / "Cargo.lock", CANDIDATE_LOCK) + if release_lock.read_bytes() != release_lock_bytes: + raise SetupError("candidate lock generation modified canonical Cargo.lock") + + +def _rewrite_candidate_requirements(root: Path) -> None: + """Project published release pins onto the pinned candidate sources.""" + + lock = _lock() + manifest = root / "rust" / "crates" / "rusticol-core" / "Cargo.toml" + text = manifest.read_text(encoding="utf-8") + projections = ( + ( + "symbolica", + str(lock["symbolica"]["rust_version"]), + str(lock["symbolica"]["candidate_version"]), + ), + ( + "symjit", + str(lock["symjit"]["version"]), + str(lock["symjit"]["candidate_version"]), + ), + ) + for dependency, published, candidate in projections: + pattern = ( + rf"(?m)^({dependency}\s*=\s*\{{\s*version\s*=\s*)" + rf'"={re.escape(published)}"' + ) + text, count = re.subn(pattern, rf'\g<1>"={candidate}"', text, count=1) + if count != 1: + raise SetupError( + f"could not project rusticol-core {dependency} requirement " + f"from {published} to candidate {candidate}" + ) + manifest.write_text(text, encoding="utf-8") + + +def _runtime_requirements_text() -> str: + runtime_lock = load_python_runtime_lock(PYTHON_LOCK) + excluded = {"symbolica", "ufo-model-loader"} + lines: list[str] = [] + for package in runtime_lock.packages: + if package.name in excluded: + continue + if not package.artifacts: + raise SetupError( + f"locked runtime package {package.name} has no wheel artifacts" + ) + lines.append(f"{package.distribution}=={package.version} \\") + for index, artifact in enumerate(package.artifacts): + continuation = " \\" if index < len(package.artifacts) - 1 else "" + lines.append(f" --hash=sha256:{artifact.sha256}{continuation}") + return "\n".join(lines) + "\n" + + +def _contributor_python_requirements( + *, + with_fft_profiling: bool = False, +) -> tuple[str, ...]: + """Reuse the selected contributor requirements from ``pyproject.toml``.""" + + with PYPROJECT.open("rb") as stream: + project_file = tomllib.load(stream) + build_system = project_file.get("build-system") + project = project_file.get("project") + if not isinstance(build_system, dict) or not isinstance(project, dict): + raise SetupError("pyproject must define build-system and project tables") + optional = project.get("optional-dependencies") + if not isinstance(optional, dict): + raise SetupError("pyproject must define optional dependencies") + + requirements: list[str] = [] + selected = [ + ("build-system.requires", build_system.get("requires")), + ("project.optional-dependencies.test", optional.get("test")), + ("project.optional-dependencies.docs", optional.get("docs")), + ] + if with_fft_profiling: + selected.append( + ( + "project.optional-dependencies.fft-profiling", + optional.get("fft-profiling"), + ) + ) + for description, raw in selected: + if not isinstance(raw, list) or not all( + isinstance(item, str) and item for item in raw + ): + raise SetupError(f"pyproject {description} must be a string list") + requirements.extend(raw) + return tuple(dict.fromkeys(requirements)) + + +def _venv_bootstrap_python() -> Path: + """Return an interpreter that remains available if this venv is reset.""" + + base_executable = getattr(sys, "_base_executable", None) + if isinstance(base_executable, str) and base_executable: + return Path(base_executable) + return Path(sys.executable) + + +def _ensure_venv( + runner: Runner, *, with_fft_profiling: bool, install_runtime: bool = True +) -> None: + if not _venv_python().is_file(): + runner.run([_venv_bootstrap_python(), "-m", "venv", VENV]) + python = _venv_python() + if runner.dry_run: + print(f"# ensure pip is available in {VENV}") + else: + pip_probe = subprocess.run( + [python, "-m", "pip", "--version"], + env=_venv_environment(), + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + check=False, + ) + if pip_probe.returncode != 0: + runner.run( + [python, "-m", "ensurepip", "--upgrade"], + env=_venv_environment(), + ) + contributor_tools = [ + "pip", + *_contributor_python_requirements( + with_fft_profiling=with_fft_profiling, + ), + "setuptools>=68,<81", + ] + runner.run( + [python, "-m", "pip", "install", "--upgrade", *contributor_tools], + env=_venv_environment(), + ) + if not install_runtime: + return + requirements = _runtime_requirements_text() + if runner.dry_run: + print("# install the hash-locked non-candidate Python runtime closure") + else: + with tempfile.TemporaryDirectory( + prefix="pyamplicol-runtime-requirements-" + ) as raw_directory: + requirement_path = Path(raw_directory) / "requirements.txt" + requirement_path.write_text(requirements, encoding="utf-8") + runner.run( + [ + python, + "-m", + "pip", + "install", + "--upgrade", + "--require-hashes", + "--only-binary=:all:", + "--no-deps", + "--requirement", + requirement_path, + ], + env=_venv_environment(), + ) + + +def _ensure_just(runner: Runner) -> None: + if shutil.which("just") is None: + runner.run(["cargo", "install", "just", "--locked"]) + + +def _single_wheel(directory: Path, prefix: str) -> Path: + candidates = sorted(directory.glob(f"{prefix}*.whl")) + if len(candidates) != 1: + raise SetupError( + f"expected one {prefix} wheel in {directory}, found {len(candidates)}" + ) + return candidates[0] + + +def _archive_candidate_wheels(directory: Path, prefix: str) -> None: + candidates = sorted(directory.glob(f"{prefix}*.whl")) + if not candidates: + return + stamp = datetime.now(UTC).strftime("%Y%m%dT%H%M%S%fZ") + destination = _managed_trash_destination( + f"candidate-wheel-refresh-{stamp}", + workspace_root=ROOT, + ) / directory.relative_to(ROOT) + destination.mkdir(parents=True, exist_ok=True) + for wheel in candidates: + shutil.move(str(wheel), str(destination / wheel.name)) + + +def _verify_candidate_python_dependencies( + runner: Runner, + payload: dict[str, Any], +) -> None: + expected_version = str(payload["symbolica"]["candidate_version"]) + probe = "\n".join( + ( + "from importlib.metadata import version", + "import sys", + "import symbolica", + "from symbolica import Expression", + "try:", + " from symbolica.community.tensor import " + "TensorExpression, TensorNetwork", + " TensorExpression.dirac_gamma", + " TensorExpression.color_t", + " TensorExpression.color_f", + " TensorExpression.simplify_algebra", + "except (ImportError, AttributeError) as error:", + " raise SystemExit(", + ' "Historical candidate dependencies lack the current tensor API; "', + ' "update coherent upstream pins before using --candidate."', + " ) from error", + 'actual = version("symbolica")', + "if actual != sys.argv[1]:", + " raise SystemExit(", + ' "symbolica version mismatch: expected %s, got %s"', + " % (sys.argv[1], actual)", + " )", + ) + ) + environment = dict(_venv_environment(), SYMBOLICA_HIDE_BANNER="1") + runner.run( + [_venv_python(), "-I", "-c", probe, expected_version], + env=environment, + ) + + +def _materialize_ratatui_sdist( + runner: Runner, + payload: dict[str, Any], +) -> Path: + ratatui = payload["ratatui"] + version = str(ratatui["version"]) + source_url = str(ratatui["source_url"]) + expected_sha256 = str(ratatui["sdist_sha256"]) + if _SHA256_PATTERN.fullmatch(expected_sha256) is None: + raise SetupError("contributor lock has no valid Ratatui sdist SHA-256") + destination = WHEELHOUSE / "ratatui" / f"ratatui-{version}.tar.gz" + if runner.dry_run: + print( + f"# download and verify Ratatui {version} from {source_url} " + f"at sha256:{expected_sha256}" + ) + return destination + if destination.is_file(): + actual_sha256 = hashlib.sha256(destination.read_bytes()).hexdigest() + if actual_sha256 != expected_sha256: + raise SetupError( + "cached Ratatui sdist digest mismatch: " + f"expected {expected_sha256}, got {actual_sha256}; " + "rerun with --reset" + ) + return destination + if destination.exists(): + raise SetupError(f"invalid managed Ratatui sdist at {destination}") + + destination.parent.mkdir(parents=True, exist_ok=True) + with tempfile.NamedTemporaryFile( + prefix=f".ratatui-{version}-", + suffix=".tar.gz", + dir=destination.parent, + delete=False, + ) as temporary: + temporary_path = Path(temporary.name) + digest = hashlib.sha256() + try: + with urllib.request.urlopen(source_url) as response: + while block := response.read(1024 * 1024): + digest.update(block) + temporary.write(block) + except Exception: + temporary_path.unlink(missing_ok=True) + raise + actual_sha256 = digest.hexdigest() + if actual_sha256 != expected_sha256: + temporary_path.unlink(missing_ok=True) + raise SetupError( + "Ratatui sdist digest mismatch: " + f"expected {expected_sha256}, got {actual_sha256}" + ) + os.replace(temporary_path, destination) + return destination + + +def _build_ratatui_wheel( + runner: Runner, + payload: dict[str, Any], +) -> None: + """Build the pinned contributor-only Ratatui wheel from pinned FFI source.""" + + ratatui = payload["ratatui"] + version = str(ratatui["version"]) + ffi_revision = str(ratatui["ffi_revision"]) + if re.fullmatch(r"[0-9a-f]{40}", ffi_revision) is None: + raise SetupError("contributor lock has no valid Ratatui FFI revision") + sdist = _materialize_ratatui_sdist(runner, payload) + python = _venv_python() + wheel_directory = WHEELHOUSE / "ratatui" + ffi_source = CHECKOUTS / "ratatui-ffi" + if not runner.dry_run: + if not ffi_source.is_dir(): + raise SetupError(f"missing managed Ratatui FFI source at {ffi_source}") + _archive_candidate_wheels(wheel_directory, "ratatui") + with tempfile.TemporaryDirectory( + prefix="pyamplicol-ratatui-ffi-", + ) as temporary_name: + external_ffi_source = Path(temporary_name) / "ratatui-ffi" + if not runner.dry_run: + # A checkout nested below pyAmpliCol is discovered as part of its + # parent Cargo workspace. Build from an exact external copy so + # ratatui-ffi remains its own workspace without editing either + # pinned source tree. + shutil.copytree( + ffi_source, + external_ffi_source, + ignore=shutil.ignore_patterns(".git", "target"), + ) + # Contributor builds keep temporary outputs inside this checkout. + # Stop Cargo from treating the private copy as a pyAmpliCol member. + manifest = external_ffi_source / "Cargo.toml" + manifest_text = manifest.read_text(encoding="utf-8") + if "\n[workspace]" not in f"\n{manifest_text}": + manifest.write_text( + manifest_text.rstrip() + "\n\n[workspace]\n", + encoding="utf-8", + ) + environment = dict( + _venv_environment(), + RATATUI_FFI_SRC=str(external_ffi_source.resolve()), + CARGO_TARGET_DIR=str((external_ffi_source / "target").resolve()), + # Keep the exact revision visible to the upstream build even though + # RATATUI_FFI_SRC takes precedence and prevents its fallback clone. + RATATUI_FFI_TAG=ffi_revision, + ) + runner.run( + [ + python, + "-m", + "pip", + "wheel", + "--no-deps", + "--no-build-isolation", + "--wheel-dir", + wheel_directory, + sdist, + ], + env=environment, + ) + if runner.dry_run: + return + wheel = _single_wheel(wheel_directory, "ratatui") + runner.run( + [ + python, + "-m", + "pip", + "install", + "--force-reinstall", + "--no-deps", + wheel, + ], + env=_venv_environment(), + ) + probe = "\n".join( + ( + "from importlib.metadata import version", + "import sys", + "import ratatui", + "import ratatui_py", + 'actual = version("ratatui")', + "if actual != sys.argv[1]:", + " raise SystemExit(", + ' "ratatui version mismatch: expected %s, got %s"', + " % (sys.argv[1], actual)", + " )", + ) + ) + runner.run( + [python, "-I", "-c", probe, version], + env=_venv_environment(), + ) + + +def _build_candidate_dependency_wheels( + runner: Runner, + payload: dict[str, Any], +) -> None: + community = _managed_checkout("symbolica-community") + python = _venv_python() + environment = _venv_environment() + symbolica_wheels = WHEELHOUSE / "symbolica" + if not runner.dry_run: + symbolica_wheels.mkdir(parents=True, exist_ok=True) + _archive_candidate_wheels(symbolica_wheels, "symbolica") + + runner.run( + [ + python, + "-m", + "maturin", + "build", + "--release", + "--locked", + "--interpreter", + python, + "--out", + symbolica_wheels, + ], + cwd=community, + env=environment, + ) + if not runner.dry_run: + runner.run( + [ + python, + "-m", + "pip", + "install", + "--force-reinstall", + "--no-deps", + _single_wheel(symbolica_wheels, "symbolica"), + ], + env=environment, + ) + loader_wheels = WHEELHOUSE / "ufo-model-loader" + if not runner.dry_run: + loader_wheels.mkdir(parents=True, exist_ok=True) + _archive_candidate_wheels(loader_wheels, "ufo_model_loader") + runner.run( + [ + python, + "-m", + "pip", + "wheel", + "--no-deps", + "--wheel-dir", + loader_wheels, + _managed_checkout("ufo-model-loader"), + ], + env=environment, + ) + if not runner.dry_run: + runner.run( + [ + python, + "-m", + "pip", + "install", + "--force-reinstall", + "--no-deps", + _single_wheel(loader_wheels, "ufo_model_loader"), + ], + env=environment, + ) + _verify_candidate_python_dependencies(runner, payload) + _build_ratatui_wheel(runner, payload) + + +def _install_published_dependencies( + runner: Runner, *, loader_wheel: Path | None +) -> None: + with PYPROJECT.open("rb") as stream: + requirements = tomllib.load(stream)["project"]["dependencies"] + loader_arguments = ( + [str(loader_wheel.expanduser().resolve())] if loader_wheel else [] + ) + python = _venv_python() + environment = _venv_environment() + runner.run( + [python, "-m", "pip", "install", "--upgrade", *requirements, *loader_arguments], + env=environment, + ) + # Historical candidate builds report the same version as the released wheel. + # Replace both published packages (or the explicit loader override) after + # resolving dependencies once, including same-version source candidates. + symbolica = next(item for item in requirements if item.startswith("symbolica==")) + loader = next( + item for item in requirements if item.startswith("ufo-model-loader==") + ) + runner.run( + [ + python, + "-m", + "pip", + "install", + "--force-reinstall", + "--no-deps", + "--only-binary=:all:", + symbolica, + *(loader_arguments or [loader]), + ], + env=environment, + ) + + +def _install_project_wheel(runner: Runner, directory: Path) -> None: + if not runner.dry_run: + runner.run( + [ + _venv_python(), + "-m", + "pip", + "install", + "--force-reinstall", + "--no-deps", + _single_wheel(directory, "pyamplicol"), + ], + env=_venv_environment(), + ) + + +def _build_project_wheel(runner: Runner, *, mode: str) -> None: + python = _venv_python() + environment = _venv_environment() + project_wheels = ARTIFACTS if mode == "candidate" else RELEASE_ARTIFACTS + if not runner.dry_run: + project_wheels.mkdir(parents=True, exist_ok=True) + + build_environment = dict( + environment, + PYAMPLICOL_BUILD_MODE=mode, + ) + if not runner.dry_run: + _archive_candidate_wheels(project_wheels, "pyamplicol") + runner.run( + [ + python, + "-m", + "build", + "--wheel", + "--no-isolation", + "--skip-dependency-check", + "--outdir", + project_wheels, + *( + ["--config-setting", "pyamplicol.host-wheel=true"] + if sys.platform == "linux" + else [] + ), + ], + cwd=ROOT, + env=build_environment, + ) + _install_project_wheel(runner, project_wheels) + + +def _stage_project_runtime(runner: Runner, *, directory: Path, mode: str) -> None: + runner.run( + [ + _venv_python(), + ROOT / "tools/developer/prepare_source_runtime.py", + "--wheel-directory", + directory, + ], + cwd=ROOT, + env=dict(_venv_environment(), PYAMPLICOL_BUILD_MODE=mode), + ) + + +def _write_state( + runner: Runner, + sources: tuple[Source, ...], +) -> None: + source_checkouts = {source.key: _managed_checkout(source.key) for source in sources} + if runner.dry_run: + print(f"# write {STATE}") + return + selected_sources = list(sources) + overrides = _root_path_patches() + lock = _lock() + local_sources = ( + ( + "symjit", + "symjit", + str(lock["symjit"]["repository"]), + str(lock["symjit"]["revision"]), + None, + ), + ( + "spenso", + "gammaloop", + str(lock["gammaloop_candidate"]["source_url"]), + str(lock["gammaloop_candidate"]["revision"]), + str(lock["gammaloop_candidate"]["branch"]), + ), + ( + "symbolica-integrate", + "symbolica-integrate", + str(lock["symbolica_integrate"]["source_url"]), + str(lock["symbolica_integrate"]["revision"]), + None, + ), + ) + for crate, key, url, revision, branch in local_sources: + if crate in overrides: + selected_sources.append(Source(key, url, revision, branch)) + source_checkouts[key] = overrides[crate] + source_state: dict[str, dict[str, str]] = {} + for source in selected_sources: + checkout = source_checkouts[source.key] + head = _git_head(runner, checkout) + source_state[source.key] = { + "url": source.url, + "revision": head, + } + if source.branch is not None: + source_state[source.key]["branch"] = source.branch + state = { + "schema_version": 1, + "publishable": False, + "sources": source_state, + } + STATE.write_text( + json.dumps(state, indent=2, sort_keys=True) + "\n", + encoding="utf-8", + ) + + +def _parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--candidate", + action="store_true", + help="historical dependency development; retained pins need an upstream update", + ) + parser.add_argument( + "--loader-wheel", + type=Path, + help="explicit local ufo-model-loader wheel for dependency development", + ) + parser.add_argument("--reset", action="store_true") + parser.add_argument("--update", action="store_true") + parser.add_argument( + "--with-legacy-amplicol", + action="store_true", + help=( + "clone the optional pinned original-AmpliCol reference and install " + "the FFT profiling Python extra" + ), + ) + parser.add_argument( + "--with-reference-fft", + action="store_true", + help=( + "clone the optional pinned public FFT reference and install the FFT " + "profiling Python extra" + ), + ) + build_mode = parser.add_mutually_exclusive_group() + build_mode.add_argument("--no-build", action="store_true") + build_mode.add_argument( + "--dependencies-only", + action="store_true", + help="install Python dependencies without building or staging pyamplicol", + ) + build_mode.add_argument( + "--wheel-directory", + type=Path, + help="install and stage one existing pyamplicol wheel instead of rebuilding it", + ) + parser.add_argument("--dry-run", action="store_true") + return parser + + +def main(argv: Sequence[str] | None = None) -> int: + parser = _parser() + args = parser.parse_args(argv) + mode = ( + "candidate" + if args.candidate + else os.environ.get("PYAMPLICOL_BUILD_MODE", "release") + ) + if mode not in {"candidate", "release"}: + parser.error("PYAMPLICOL_BUILD_MODE must be 'candidate' or 'release'") + if mode == "candidate" and args.loader_wheel is not None: + parser.error("--loader-wheel belongs to the published-dependency lane") + payload = _lock() + runner = Runner(dry_run=args.dry_run) + if args.reset: + _archive_managed_state(runner) + if not args.dry_run and (mode == "candidate" or not args.no_build): + _require_tools() + sources = (_sources if mode == "candidate" else _developer_sources)( + payload, + with_legacy=args.with_legacy_amplicol, + with_reference_fft=args.with_reference_fft, + ) + _ensure_just(runner) + _ensure_venv( + runner, + with_fft_profiling=(args.with_legacy_amplicol or args.with_reference_fft), + install_runtime=mode == "candidate", + ) + for source in sources: + _checkout(runner, source, update=args.update) + if mode == "candidate": + print( + "Historical source-candidate lane: retained pins require a coherent " + "upstream update for the current tensor API." + ) + _configure_sources(runner) + _write_cargo_config(runner) + _write_candidate_lock(runner) + _write_state(runner, sources) + if not args.no_build: + if mode == "candidate": + _build_candidate_dependency_wheels(runner, payload) + else: + _install_published_dependencies(runner, loader_wheel=args.loader_wheel) + _build_ratatui_wheel(runner, payload) + if not args.dependencies_only: + directory = args.wheel_directory + if directory is None: + directory = ARTIFACTS if mode == "candidate" else RELEASE_ARTIFACTS + _build_project_wheel(runner, mode=mode) + else: + directory = directory.expanduser().resolve() + _install_project_wheel(runner, directory) + _stage_project_runtime(runner, directory=directory, mode=mode) + print(f"Contributor environment ready at {VENV}") + return 0 + + +if __name__ == "__main__": + try: + raise SystemExit(main()) + except SetupError as error: + print(f"error: {error}", file=sys.stderr) + raise SystemExit(2) from error diff --git a/pyamplicol/dependencies/python-runtime-lock.toml b/pyamplicol/dependencies/python-runtime-lock.toml new file mode 100644 index 0000000000..863b297161 --- /dev/null +++ b/pyamplicol/dependencies/python-runtime-lock.toml @@ -0,0 +1,233 @@ +# SPDX-License-Identifier: 0BSD +# Exact Python runtime graph and admitted wheels for supported release targets. +schema_version = 1 +requires_python = ">=3.11" +supported_python = ["3.11", "3.12", "3.13", "3.14"] + +[[packages]] +distribution = "colorama" +version = "0.4.6" +license = "BSD-3-Clause" +direct = true +dependencies = [] + + [[packages.artifacts]] + filename = "colorama-0.4.6-py2.py3-none-any.whl" + sha256 = "4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6" + +[[packages]] +distribution = "numpy" +version = "2.4.6" +license = "BSD-3-Clause AND 0BSD AND MIT AND Zlib AND CC0-1.0" +direct = true +dependencies = [] + + [[packages.artifacts]] + filename = "numpy-2.4.6-cp311-cp311-macosx_10_9_x86_64.whl" + sha256 = "0280e0356c0829a18d9de1cb7eee50ec22ca639878d7240307ca0943d73cd2c4" + + [[packages.artifacts]] + filename = "numpy-2.4.6-cp311-cp311-macosx_11_0_arm64.whl" + sha256 = "110f8b71aacb688ec69062bb7f6938a0f8acb01b7c1c4beb453c65b6d234584d" + + [[packages.artifacts]] + filename = "numpy-2.4.6-cp311-cp311-macosx_14_0_arm64.whl" + sha256 = "4cfe66903cc32a9921a6733d96b19bb6abf310397581bbad89c228f5abaf0ee8" + + [[packages.artifacts]] + filename = "numpy-2.4.6-cp311-cp311-macosx_14_0_x86_64.whl" + sha256 = "8155154c7c691289fe18f510b5d4657c68c67989f293f0535a91360392ff6538" + + [[packages.artifacts]] + filename = "numpy-2.4.6-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl" + sha256 = "89cd468399cfd2504718f0ba50e410dca55a170b61a02ad92bb18c8a65186e93" + + [[packages.artifacts]] + filename = "numpy-2.4.6-cp312-cp312-macosx_10_13_x86_64.whl" + sha256 = "001fbb8e08d942dd57599e781f2472269ee7f2755fae407b4f67b2f0b17da3f1" + + [[packages.artifacts]] + filename = "numpy-2.4.6-cp312-cp312-macosx_11_0_arm64.whl" + sha256 = "ebfb099f8dcf083deef3ac1ca4c1503f387cf76296fcb3816b66f5ecb5f54fdb" + + [[packages.artifacts]] + filename = "numpy-2.4.6-cp312-cp312-macosx_14_0_arm64.whl" + sha256 = "3213d622a0283a39a93d188f3cf72b26862df52fbb4ca3697f51705016523d41" + + [[packages.artifacts]] + filename = "numpy-2.4.6-cp312-cp312-macosx_14_0_x86_64.whl" + sha256 = "357cc07a6d7b0b182ff02249616a03742827ebb1277546b5c7cd7f7620a45698" + + [[packages.artifacts]] + filename = "numpy-2.4.6-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl" + sha256 = "90f9849678c75fe7afa2d348ac842c168b0a4d3d61919687216dfc547976d853" + + [[packages.artifacts]] + filename = "numpy-2.4.6-cp313-cp313-macosx_10_13_x86_64.whl" + sha256 = "511dbaf848decaaaf4b4ca48032619fb3138710c4bf7da7617765edad1ef96b0" + + [[packages.artifacts]] + filename = "numpy-2.4.6-cp313-cp313-macosx_11_0_arm64.whl" + sha256 = "bf162abab1c1a736333192707cef898e735a5ca00f38f27eeedf44b39d9e85eb" + + [[packages.artifacts]] + filename = "numpy-2.4.6-cp313-cp313-macosx_14_0_arm64.whl" + sha256 = "043191bfa8eab18c776647b62723ac9dddece59743b13f49b2016094129c2b3f" + + [[packages.artifacts]] + filename = "numpy-2.4.6-cp313-cp313-macosx_14_0_x86_64.whl" + sha256 = "6180d8b35af935aed8ece3a85e0a43f87393ae0ac87c8d2c8bd2c993f7270ef3" + + [[packages.artifacts]] + filename = "numpy-2.4.6-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl" + sha256 = "a7830bab239b79cda9c08c2da014761cafb48da6150e1da17ac06283f43b6089" + + [[packages.artifacts]] + filename = "numpy-2.4.6-cp314-cp314-macosx_10_15_x86_64.whl" + sha256 = "06ca2f61ec4385a07a6977c55ba998a4466c123642b4a32694d3128fce18c079" + + [[packages.artifacts]] + filename = "numpy-2.4.6-cp314-cp314-macosx_11_0_arm64.whl" + sha256 = "38efbc8de75c7a0fc1ac190162d892787f3f47b57cc291231aafee36b80982b7" + + [[packages.artifacts]] + filename = "numpy-2.4.6-cp314-cp314-macosx_14_0_arm64.whl" + sha256 = "d581b735e177fdcdce6fed8e7e8880a3fb6ee4e3653a3ac6af01c6f4c03effc5" + + [[packages.artifacts]] + filename = "numpy-2.4.6-cp314-cp314-macosx_14_0_x86_64.whl" + sha256 = "0a041d3d761dc3c35cc56ce0351506a02bcbc25f7b169f652435141a17db9096" + + [[packages.artifacts]] + filename = "numpy-2.4.6-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl" + sha256 = "a2c306dea656c12c68f51f4cea133cbe78ca7435eb28c735eac1d3ebe73be6e8" + +[[packages]] +distribution = "platformdirs" +version = "4.12.2" +license = "MIT" +direct = true +dependencies = [] + + [[packages.artifacts]] + filename = "platformdirs-4.12.2-py3-none-any.whl" + sha256 = "29dbf06d96c500bc6bdbce75fb0a14d63279c93b1842f97e72a135b33e856983" + +[[packages]] +distribution = "prettytable" +version = "3.18.0" +license = "BSD-3-Clause" +direct = true +dependencies = ["wcwidth"] + + [[packages.artifacts]] + filename = "prettytable-3.18.0-py3-none-any.whl" + sha256 = "b3346e0e6f79180833aebaac088ae926340586cf6d7d991b9eb125b65f72313a" + +[[packages]] +distribution = "progressbar2" +version = "4.6.0" +license = "BSD-3-Clause" +direct = true +dependencies = ["python-utils"] + + [[packages.artifacts]] + filename = "progressbar2-4.6.0-py3-none-any.whl" + sha256 = "2d78bdeb1debef92a39dd9042e0689542bd72d6e2f30b13432db371aa9d8ee4e" + +[[packages]] +distribution = "python-utils" +version = "4.1.0" +license = "BSD-3-Clause" +direct = false +dependencies = ["typing-extensions"] + + [[packages.artifacts]] + filename = "python_utils-4.1.0-py3-none-any.whl" + sha256 = "dd3c041dae1efc1323209a7598d3a4fe1a832d81be10a98846341af929a0119e" + +[[packages]] +distribution = "six" +version = "1.17.0" +license = "MIT" +direct = true +dependencies = [] + + [[packages.artifacts]] + filename = "six-1.17.0-py2.py3-none-any.whl" + sha256 = "4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274" + +[[packages]] +distribution = "symbolica" +version = "3.0.0" +license = "LicenseRef-Symbolica-Proprietary" +direct = true +dependencies = [] + + [[packages.artifacts]] + filename = "symbolica-3.0.0-cp39-abi3-macosx_10_12_x86_64.whl" + sha256 = "b2ea03b9be72a3ebc3fa950d7380b09223084cb5554aa0877c59e12d7749c2a7" + + [[packages.artifacts]] + filename = "symbolica-3.0.0-cp39-abi3-macosx_11_0_arm64.whl" + sha256 = "6b7414d8d06ac20c92460e0685c794d97afe976968b5fba5cc9562c471b2365d" + + [[packages.artifacts]] + filename = "symbolica-3.0.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" + sha256 = "a2977f78d8f40ce3206ba49850cf75f473785a32a9368e6115d059755a48d15e" + +[[packages]] +distribution = "tomli-w" +version = "1.2.0" +license = "MIT" +direct = true +dependencies = [] + + [[packages.artifacts]] + filename = "tomli_w-1.2.0-py3-none-any.whl" + sha256 = "188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90" + +[[packages]] +distribution = "typing-extensions" +version = "4.16.0" +license = "PSF-2.0" +direct = false +dependencies = [] + + [[packages.artifacts]] + filename = "typing_extensions-4.16.0-py3-none-any.whl" + sha256 = "481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8" + +[[packages]] +distribution = "ufo-model-loader" +version = "1.0.0" +license = "MIT" +direct = true +dependencies = ["symbolica"] + + [[packages.artifacts]] + filename = "ufo_model_loader-1.0.0-py3-none-any.whl" + sha256 = "4e2e807d6a0d16034a525f80f2a80f4fe17689f7955b27b1118b7595bd56a8eb" + +[[packages]] +distribution = "wcwidth" +version = "0.9.1" +license = "MIT" +direct = false +dependencies = [] + + [[packages.artifacts]] + filename = "wcwidth-0.9.1-cp310-abi3-macosx_10_9_x86_64.whl" + sha256 = "10b00ba23482e352f874d2e8135e7ace9da838646c7dd800566246bbd46125ff" + + [[packages.artifacts]] + filename = "wcwidth-0.9.1-cp310-abi3-macosx_11_0_arm64.whl" + sha256 = "40d936d72c9bdc10df43f93a8be502bc5024b487259139f66a328722c07f34a9" + + [[packages.artifacts]] + filename = "wcwidth-0.9.1-cp310-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl" + sha256 = "747fb724223f417a17541a95a17c1dac3a8ef9a0cf41684950f0eab191a35f65" + + [[packages.artifacts]] + filename = "wcwidth-0.9.1-py3-none-any.whl" + sha256 = "e0c3a1c45c5b9550c6919a4449e95f5b177f6e165786376981db8f1addae9b21" diff --git a/pyamplicol/dependencies/release-lock.toml b/pyamplicol/dependencies/release-lock.toml new file mode 100644 index 0000000000..cbbbc6739d --- /dev/null +++ b/pyamplicol/dependencies/release-lock.toml @@ -0,0 +1,97 @@ +# SPDX-License-Identifier: 0BSD +schema_version = 1 + +[project] +distribution = "pyamplicol" +version = "0.2.1" +python_requires = ">=3.11" +wheel_abi = "cp311-abi3" +license = "0BSD" + +[abis] +python_api = 1 +toml = 1 +compiled_model = 9 +process_artifact = 3 +runtime_physics = 1 +c_abi = 1 +symbolica_serialization = "symbolica-bincode2-v1" +symjit_application = "symjit-application-storage-v3" +symjit_plane_application = "pyamplicol-symjit-plane-application-v2" +prepared_model_bundle = 1 +prepared_kernel_variant = "pyamplicol-prepared-kernel-variant-v2" +eager_kernel = "pyamplicol-eager-kernel-v1" +eager_plan = "pyamplicol-eager-plan-v2" +eager_selector_domains = "pyamplicol-eager-selector-domains-v1" + +[toolchain] +just = "1.46.0" +maturin = "1.15.0" +packaging = "26.3" +rust_minimum = "1.89.0" +rust_toolchain = "1.97.1" +rust_toolchain_action_sha = "46511b1c83438f0dd37c02d843619ece5a4abb5b" +manylinux_image = "quay.io/pypa/manylinux_2_28_x86_64" +manylinux_image_digest = "sha256:b04887b645dde99b9e955aeae3ff4da414992d0bd88259f046295b56361c5614" + +[[targets]] +triple = "aarch64-apple-darwin" +platform_tag = "macosx_11_0_arm64" + +[[targets]] +triple = "x86_64-apple-darwin" +platform_tag = "macosx_11_0_x86_64" + +[[targets]] +triple = "x86_64-unknown-linux-gnu" +platform_tag = "manylinux_2_28_x86_64" + +[[python_dependencies]] +distribution = "colorama" +version = "0.4.6" + +[[python_dependencies]] +distribution = "numpy" +version = "2.4.6" + +[[python_dependencies]] +distribution = "platformdirs" +version = "4.12.2" + +[[python_dependencies]] +distribution = "prettytable" +version = "3.18.0" + +[[python_dependencies]] +distribution = "progressbar2" +version = "4.6.0" + +[[python_dependencies]] +distribution = "six" +version = "1.17.0" + +[[python_dependencies]] +distribution = "symbolica" +version = "3.0.0" + +[[python_dependencies]] +distribution = "tomli-w" +version = "1.2.0" + +[[python_dependencies]] +distribution = "ufo-model-loader" +version = "1.0.0" + +[symbolica] +python_distribution = "symbolica" +python_version = "3.0.0" +rust_crate = "symbolica" +rust_version = "3.0.0" +serialization_abi = "symbolica-bincode2-v1" + +[symjit] +version = "2.26.4" + +[ufo_model_loader] +python_distribution = "ufo-model-loader" +required_version = "1.0.0" diff --git a/pyamplicol/docs/README.md b/pyamplicol/docs/README.md new file mode 100644 index 0000000000..1eff0c1fec --- /dev/null +++ b/pyamplicol/docs/README.md @@ -0,0 +1,121 @@ +--- +title: "Home" +nav_order: 1 +description: "Generate and evaluate color-ordered scattering amplitudes from Python and native APIs." +--- + +

+ pyAmpliCol +

+ +

+ PyPI + Python versions + Tests + License: 0BSD +

+ +

Fast color-ordered scattering amplitudes from Python and native APIs.

+ +pyAmpliCol generates portable process artifacts from built-in, JSON, and UFO +models, then evaluates them through a fast Rust runtime. It supports a typed +Python API and CLI, runtime helicity and color-flow selection, and generated +Python, C11, C++17, Fortran 2008, and Rust 2021 interfaces. + +## Start here + +| I want to… | Read | +| --- | --- | +| follow a physicist-friendly, MadGraph-style walkthrough | [Get started: a gentle walkthrough](user/gentle-walkthrough.md) | +| install pyAmpliCol and verify it | [Installation](user/installation.md) | +| generate and evaluate my first process | [Quick Start](user/quick-start.md) | +| choose a model, process, color approximation, or evaluator | [Configuration](user/configuration.md) and [Models and Processes](user/models-and-processes.md), including the packaged scalar HEFT workflow | +| check whether my UFO model is supported | [UFO Model Coverage](user/ufo-model-coverage.md), the exact list of accepted, conditional and rejected model features | +| call pyAmpliCol from Python | [Python API](user/python-api.md) | +| prepare and evaluate tree-level spin and colour correlations | [Born Correlations](correlators.md) and [Correlation Conventions](correlator-conventions.md), LC/NLC/full colour and grouped Python evaluation | +| use C, C++, Fortran, Rust, or generated Python drivers | [Native APIs](user/native-apis.md) | +| build a UMAMI shared library or integrate with MadSpace | [UMAMI and MadSpace](user/umami-api.md) | +| benchmark, reproduce, or view performance reports | [Profiling and Benchmarking](user/profiling-and-benchmarking.md), [FullColor FFT Profiling](user/fullcolor-fft-profiling.md), and [published performance reports](performance_reports/README.md) | +| diagnose an error | [Troubleshooting](user/troubleshooting.md) | + +## Install and try it + +```console +python -m venv .venv +. .venv/bin/activate +python -m pip install pyamplicol +pyamplicol self-test +``` + +Generate a small built-in Standard Model process and inspect the artifact: + +```console +pyamplicol generate "d d~ > z" ./artifacts/ddbar_z \ + --model built-in-sm +pyamplicol inspect ./artifacts/ddbar_z +``` + +The generated process appears in the inspection table as: + +```text ++---------+-------------+------------------+-------+--------------+---------------+ +| default | stable ID | concrete process | color | helicities | color outputs | ++---------+-------------+------------------+-------+--------------+---------------+ +| * | d_dbar_to_z | d d~ > z | lc | 12 (6 eval.) | 1 | ++---------+-------------+------------------+-------+--------------+---------------+ +``` + +The CLI uses color automatically on an interactive terminal; the plain capture +above remains readable in logs and documentation. + +## One artifact, several interfaces + +```text +model + process request + │ + ▼ +generated process artifact + ├── Python Runtime + ├── C / C++ / Fortran / Rust APIs + ├── selectors and arbitrary precision + └── benchmarking and profiling +``` + +Ordinary Python evaluation uses the same runtime as the native interfaces: + +```python +import math + +from pyamplicol import Runtime + +runtime = Runtime.load("artifacts/ddbar_z", process="d d~ > z") +energy = 91.188 / 2.0 +momenta = [[ + [energy, 0.0, 0.0, energy], + [energy, 0.0, 0.0, -energy], + [2.0 * energy, 0.0, 0.0, 0.0], +]] +value = runtime.evaluate(momenta) +resolved = runtime.evaluate_resolved(momenta) +for optimized, explicit in zip(value, resolved.total(), strict=True): + assert math.isclose(optimized.real, explicit.real, rel_tol=1e-12, abs_tol=1e-15) + assert math.isclose(optimized.imag, explicit.imag, rel_tol=1e-12, abs_tol=1e-15) +``` + +Process expressions may reorder particles within the incoming side or within +the outgoing side. pyAmpliCol consistently permutes momenta, helicities, color +flows, and resolved metadata; particles never cross the `>` boundary. See +[Process Selection and Permutations](user/process-selection-and-permutations.md). + +## Explore the documentation + +- **Using pyAmpliCol:** [Examples Gallery](user/examples-gallery.md), [Command-Line Interface](user/command-line-interface.md), [Runtime and Selectors](user/runtime-and-selectors.md) +- **Generation:** [Models and Processes](user/models-and-processes.md), including scalar HEFT, [Generation Modes and Evaluators](user/generation-modes-and-evaluators.md), and [Process Grouping](user/process-grouping.md) of subprocesses with equal matrix elements +- **Guided workflows:** [LC workloads and execution modes](user/lc-workloads-and-execution-modes.md), [Profiling campaign: from measurements to the PDF](user/profiling-campaign-walkthrough.md), [FullColor FFT Profiling](user/fullcolor-fft-profiling.md) +- **Deployment:** [Artifacts and Portability](user/artifacts-and-portability.md), [Release and Platform Support](user/release-and-support.md) +- **Background:** [Architecture Overview](user/architecture-overview.md), [Symbolica and Licensing](user/symbolica-and-licensing.md) + +This site is the authoritative user documentation for the current `main` +branch. [Release and Support](user/release-and-support.md) records the published +platform and validation boundary, while public API stability is defined by the +[API contract](development/API_CONTRACT.md). diff --git a/pyamplicol/docs/_config.yml b/pyamplicol/docs/_config.yml new file mode 100644 index 0000000000..f40a6eea7c --- /dev/null +++ b/pyamplicol/docs/_config.yml @@ -0,0 +1,64 @@ +title: pyAmpliCol +description: Fast color-ordered scattering amplitudes from Python and native APIs. +url: https://madgraph.org +baseurl: /pyamplicol +repository: MadGraphTeam/pyamplicol +permalink: pretty + +remote_theme: just-the-docs/just-the-docs@2ed4c0af735e95af5c0dcf7a605b07d903005b4e +plugins: + - jekyll-remote-theme + - jekyll-include-cache + - jekyll-seo-tag + - jekyll-relative-links + +defaults: + - scope: + path: "" + type: pages + values: + layout: default + image: /assets/pyamplicol_social_preview.png + +logo: /assets/pyamplicol_logo.png +color_scheme: light +search_enabled: true +search: + heading_level: 3 + previews: 3 + rel_url: true + focus_shortcut_key: "k" +enable_copy_code_button: true +heading_anchors: true +nav_enabled: true +back_to_top: true +back_to_top_text: Back to top + +relative_links: + enabled: true + collections: true + +readme_index: + with_frontmatter: true + +aux_links: + Repository: + - https://github.com/MadGraphTeam/pyamplicol + PyPI: + - https://pypi.org/project/pyamplicol/ +aux_links_new_tab: true + +gh_edit_link: true +gh_edit_link_text: Edit this page on GitHub +gh_edit_repository: https://github.com/MadGraphTeam/pyamplicol +gh_edit_branch: main +gh_edit_source: docs +gh_edit_view_mode: edit + +callouts: + tip: + title: Tip + color: blue + important: + title: Important + color: purple diff --git a/pyamplicol/docs/_includes/head_custom.html b/pyamplicol/docs/_includes/head_custom.html new file mode 100644 index 0000000000..18fe68a3cb --- /dev/null +++ b/pyamplicol/docs/_includes/head_custom.html @@ -0,0 +1,3 @@ + + + diff --git a/pyamplicol/docs/_includes/nav_footer_custom.html b/pyamplicol/docs/_includes/nav_footer_custom.html new file mode 100644 index 0000000000..bcf2a9f221 --- /dev/null +++ b/pyamplicol/docs/_includes/nav_footer_custom.html @@ -0,0 +1,7 @@ +

+ Repository + · + PyPI + · + Issues +

diff --git a/pyamplicol/docs/assets/css/just-the-docs-dark.scss b/pyamplicol/docs/assets/css/just-the-docs-dark.scss new file mode 100644 index 0000000000..32337442bb --- /dev/null +++ b/pyamplicol/docs/assets/css/just-the-docs-dark.scss @@ -0,0 +1,9 @@ +--- +--- +{% include css/just-the-docs.scss.liquid color_scheme="dark" %} + +.site-logo, +.main-content img[alt="pyAmpliCol"] { + filter: brightness(2) saturate(1.1) + drop-shadow(0 0 1px rgba(255, 255, 255, 0.65)); +} diff --git a/pyamplicol/docs/assets/pyamplicol_logo.png b/pyamplicol/docs/assets/pyamplicol_logo.png new file mode 100644 index 0000000000..1db60f8a02 Binary files /dev/null and b/pyamplicol/docs/assets/pyamplicol_logo.png differ diff --git a/pyamplicol/docs/assets/pyamplicol_mark.png b/pyamplicol/docs/assets/pyamplicol_mark.png new file mode 100644 index 0000000000..76bf07c51f Binary files /dev/null and b/pyamplicol/docs/assets/pyamplicol_mark.png differ diff --git a/pyamplicol/docs/assets/pyamplicol_social_preview.png b/pyamplicol/docs/assets/pyamplicol_social_preview.png new file mode 100644 index 0000000000..dfa3a759fe Binary files /dev/null and b/pyamplicol/docs/assets/pyamplicol_social_preview.png differ diff --git a/pyamplicol/docs/correlator-conventions.md b/pyamplicol/docs/correlator-conventions.md new file mode 100644 index 0000000000..fb8d79d271 --- /dev/null +++ b/pyamplicol/docs/correlator-conventions.md @@ -0,0 +1,426 @@ +--- +title: "Correlation Conventions" +parent: "Python API" +nav_order: 5 +--- + +# Conventions for spin- and colour-correlated Born quantities + +This page connects the notation of the +[MadNkLO colour-correlator note](https://github.com/MadGraphTeam/pyamplicol/blob/80aec6f706c409b2f8f6c793dff8d8f04b118f7d/IMPLEMENTATION_DOCS/REFERENCES/ColorCorrelators_MadNkLO.pdf) +to pyAmpliCol's [correlated API](correlators.md). In particular, it makes the +bra/ket convention explicit: pyAmpliCol contracts the **adjoint of the bra +connection with the ket connection**, rather than adopting the note's +oriented, non-conjugated product unchanged. + +The implementation gives LC, NLC and full-colour tree-level quantities, +evaluated at physical `N_c=3`. The order of a colour connection (NLO, NNLO, +N3LO) is independent of this colour approximation. + +## Amplitudes, basis tensors, and correlated matrices + +| Symbol | Meaning | +| --- | --- | +| `N_c = 3` | Number of colours, not the size of the colour basis | +| `c, d = 1, ..., N_basis` | Labels of the retained colour-basis tensors | +| `h` | External helicity configuration, or spectator helicities after spin replacement | +| `C_c` | A colour tensor, including its external colour indices | +| `A[h,c]` | The coherent, complex partial amplitude in the generated convention | +| `S_B`, `S_K` | Ordered colour connections applied on the bra and ket side | +| `N` | The ordinary overall normalization, including any factored common coupling, averaging, and identical-particle factors | + +With momenta and model parameters left implicit, the definitions are: + +```text +|M_h> = sum_c A[h,c] |C_c> +K[B,K][c,d] = < S_B C_c | S_K C_d > +R[B,K] = N sum_(h,c,d) conj(A[h,c]) K[B,K][c,d] A[h,d] +``` + +The matrix includes the process's colour normalization factors. Its entries +are evaluated exactly as complex rational numbers at generation. At runtime, +`R[B,K]` is returned as a `CorrelatedValue` with decimal real and imaginary +parts. The ID `"born"` uses empty connections, giving the usual colour metric +``. + +Only spectator helicities are summed incoherently. Colour components are +combined **before squaring**, retaining their interference. Separate squared +flow/helicity components are therefore not sufficient to reconstruct a +general colour or spin correlation: their relative phases have been lost. + +For two different connections, neither an individual colour matrix nor its +answer is assumed real or symmetric. Interchanging the bra and ket gives +the Hermitian-conjugate matrix and the complex-conjugate answer: + +```text +K[K,B] = K[B,K]† +R[K,B] = conj(R[B,K]) +``` + +At full colour, identical connections give a squared norm. This distinction +also explains why an ordered connection should not be interpreted as a +positive squared matrix element in every case. + +## Colour accuracy + +The selected `color.accuracy` applies to the correlated contraction, while the +complete generated amplitude basis is retained. Empty connections use the +existing ordinary colour-accuracy convention. The inserted matrix is built +with symbolic powers of `N_c` before its selected coefficients are evaluated +at three colours. Amplitude coefficients and incoming averages remain at +physical `N_c=3`: this is the inherited colour-matrix approximation, not a +new strict expansion of every part of the matrix element. + +For a Born basis with `n_g` adjoints and `n_qbarq` fundamental pairs, and a +connection containing `r` operations of which `s` are gluon-to-quark-pair +splittings on either side, the common reference power is + +```text +P = n_g + n_qbarq + r - s. +``` + +LC keeps powers at least `P`. For purely adjoint final colour space, NLC +keeps powers at least `P-2`. With fundamental lines, NLC retains the exact +coefficient of an entry if its highest nonzero power is at least `P-2`, as +in the ordinary open-line approximation. Intervening odd powers are included. +Full colour retains all terms. The threshold is common to related connections, +not reset to each entry's leading nonzero term. Born-metric zeros are not used +to discard connected entries, and intermediate splitting terms are not pruned. + +Coherence and Casimir tests at approximate colour accuracy compare terms +through the retained order. Keeping exact admitted NLC coefficients can leave +residuals below that order; it does not imply exact finite-`N_c` cancellation +of those extra terms. Full-colour identities hold without that truncation. +LC/NLC matrices are approximations and are not generally positive operators. + +## Colour charges, indices, and crossing + +We use Hermitian physical generators with +`tr(t^a t^b) = T_R delta(a,b)`, `T_R = 1/2`, and +`[t^a,t^b] = i f^(abc) t^c`. In **output, input** index order the charges are: + +| All-outgoing representation | Charge matrix for emitted gluon colour `a` | +| --- | --- | +| Quark, `3` | `t^a[out,in]` | +| Antiquark, `3bar` | `-t^a[in,out]` | +| Gluon, `8` | `-i f^(a,out,in)` | + +These conventions give `C_F = (N_c^2-1)/(2 N_c) = 4/3` and `C_A = N_c = 3`. +Internally, some colour-ordered tensors use generators `tau = sqrt(2) t`; +the correlated matrices include that conversion. Do not supply an extra +factor of two or `sqrt(2)` at runtime. + +All colour charges use the all-outgoing convention: an incoming quark is an +outgoing antiquark for the colour algebra, and vice versa. Leg **labels do not +change**: they remain one-based in the generated public process order, +including colourless particles. The caller supplies normal physical incoming +momenta and must not add another colour-crossing sign. + +At NLO, + +```text +B_ij = N sum_h + = N sum_(h,a) +``` + +is requested as `ColorCorrelator.dipole("Tij", i, j)`. This is the quantity +multiplying soft eikonal factors in a factorized soft limit. pyAmpliCol does +not include the eikonal factor, its soft-limit sign, or the associated +`4 pi alpha_s` factor in `B_ij`. Diagonal entries are retained: their eikonal +coefficients vanish for massless legs but need not vanish for massive legs. + +## Ordered connections and auxiliary labels + +A connection is a chronological tuple of operations. If its entries are +`(E1, E2, ..., Em)`, its action on a ket is `S = Em ... E2 E1`: +the **first tuple entry acts first**. The bra is adjointed afterwards. +Operations on the same colour line generally do not commute. + +`EmitGluon(i, x)` keeps emitter `i` and adds an adjoint leg `x`. +`SplitGluon(x, q, qbar)` removes an adjoint parent `x` and replaces it with +a fundamental leg `q` and an antifundamental leg `qbar`, through +`t^a[q,qbar]`. New labels must be fresh, including with respect to retired +parents. Born labels are positive and one-based; we recommend reserving fresh +negative labels for auxiliary partons, although the operation classes do not +enforce that sign convention. Auxiliary partons have **no additional Born +phase-space momenta**. A later operation may act on an auxiliary leg +only after that leg has been introduced. + +The note's four connection classes have the following direct API counterparts: + +| Class in the note | Chronological pyAmpliCol operations | +| --- | --- | +| A: emissions from different Born lines | `(EmitGluon(i, -1), EmitGluon(j, -2))`, `i != j` | +| B: successive emissions from one Born line | `(EmitGluon(i, -1), EmitGluon(i, -2))` | +| C: an emitted gluon emits another gluon | `(EmitGluon(i, -1), EmitGluon(-1, -2))` | +| D: an emitted gluon splits into a quark pair | `(EmitGluon(i, -1), SplitGluon(-1, -2, -3))` | + +The algebra also permits splitting an active Born gluon. The note's +soft-connection class D instead starts with an already-emitted gluon which +can itself become soft; algebraic support for a Born-leg splitting does not +imply a soft singularity of that hard leg. + +Thus the note's emission triple `(i, x, i)` becomes `EmitGluon(i, x)`. +For a quark-pair splitting, the API explicitly names the two daughter +representations and uses fresh daughter labels; it does not reuse the +parent label as one daughter. When translating a whole pair of connections, +relabel their final partons consistently on both sides. + +Explicit declarations keep the ordered lists as supplied and use user-chosen +**string IDs for the requested pair**. The automatic catalogue described below +instead indexes canonical chronological histories. Neither form attaches +multiplicities to the result. In particular, do not apply the note's +descending-label sort to a chain whose next emitter has not yet been created. + +Both sides must have the same order, with no fixed upper bound, and +finish with identical labelled colour representations. A gluon-only final +colour space cannot be contracted with a quark-pair colour space. Invalid +pairs are rejected rather than returned as physical zeroes. One-, two-, and +three-step connections supply NLO, NNLO, and N3LO colour structures; this is +not a claim to supply the kinematic ingredients of an N3LO calculation. + +## Automatic generic catalogue + +`CorrelatorConfig.all_color(through_order=k)` registers every compatible +ordered pair through orders 1,...,k, together with the ordinary Born. This is +the generic construction of section 2 of the MadNkLO note, not a list of +dipole products. Its soft scope permits gluon emission from any active +coloured leg and quark-pair splitting of auxiliary gluons, but not splitting +hard Born gluons automatically. The latter operation remains available +explicitly. Colour roles are crossed to the all-outgoing convention by the +generator, separately for each process. + +Let `S_m` be the set of histories with m operations, starting with the identity +at m=0. From every history, append each allowed operation on every active leg. +Emitted gluons and the daughters of a quark-pair splitting are also active +emitters. Each operation increases the number of final auxiliary partons by +one. At order m, assign their labels in every possible way to `-1,...,-m`. +Retired intermediate parents receive separate dummy labels below `-m`. + +The spanning argument is constructive. A leading tree-level QCD soft current +is a sum of forests rooted on the hard coloured lines. Its quark-gluon and +three-gluon colour vertices are precisely the two supported operations; +four-gluon colour factors are sums of products of two structure constants +and can be resolved into successive binary operations. A topological traversal +of any such forest is therefore visited by the recursion, including multiple +quark pairs and every assignment of the labelled soft partons. It follows that + +```text +J_m^(0) = sum_alpha j_alpha S_alpha^(m) +N sum_h + = sum_(alpha,beta) conjugate(j_alpha) j_beta B_(alpha,beta) +``` + +where `B_(alpha,beta) = R[S_alpha,S_beta]` in the notation above, +with the unresolved-spin sums included when forming the physical squared +quantity. `N` is the common Born normalization defined above. +Chronological colour construction does **not** +assume strongly ordered soft energies: simultaneous-soft kinematics and +contact terms belong to the coefficients `j_alpha`. Flavour factors and +identical-fermion exchange signs also belong to the physical current. +This proves coverage of tree-soft colour tensors, not provision of loop +amplitudes or the kinematic ingredients of a complete fixed-order calculation. +For the factorization of the leading soft current see +[Catani and Cieri, section 2](https://doi.org/10.1140/epjc/s10052-022-10001-z); +the appearance of colour quadrupoles beyond dipoles at three soft gluons is +discussed by [Catani, Colferai and Torrini](https://arxiv.org/abs/1908.01616). + +Canonicalization removes only independent chronological interleavings and +dummy-name choices. It preserves the order on each emitter line and every +creation-before-use dependency. In contrast to a blind descending-label sort, +the stored representative is always executable. Colour-conservation, Jacobi +and sign-related redundancies are retained; a minimal basis is not required. +Finally, pair **all** canonical histories of the same order with identical +final labels and representations. Both directed orders and self-pairs remain. +There are no numerical multiplicity factors on these individual entries. + +Final-label assignments are essential even across different creation orders. +For example, these N3LO histories interfere in the same final space +`q(-1), qbar(-2), g(-3)`: + +```python +left = ( + EmitGluon(1, -4), SplitGluon(-4, -1, -2), EmitGluon(-1, -3), +) +right = ( + EmitGluon(1, -3), EmitGluon(1, -4), SplitGluon(-4, -1, -2), +) +``` + +Number the sorted histories independently at each order, starting from zero. +The pair of history indices `(i,j)` at order r has ID `N{r}LO/c{i}/c{j}`. +These are local indices for a particular process catalogue, not universal +operator numbers or hashes. The full typed chronological `bra` and `ket` +records are stored with the ID; retired-label conventions do not require any +additional momenta. The available metadata is returned by +`runtime.available_color_correlations()`, including `"born"` at order zero. +The caller can inspect, select and combine entries without parsing ID strings. + +No perturbative-order cap is imposed. Detailed physics tests cover NLO through +N3LO, with higher-order Casimir and two-pair checks. Enumeration tests compare +the NNLO classes A--D, preserve noncommuting histories, and verify N3LO +cross-order creation histories and N4LO two-pair relabelling. For n coloured +Born legs the NNLO catalogue has `(n*n + 3*n)**2 + 2*n*n` directed pairs: +108 for two legs and 816 for four. At N3LO four legs already give 90,720 pairs; +the full automatic option trades a larger output for not choosing a subset. + +## Translating the note's non-conjugated convention + +The note's sections 2.1-2.2 keep orientations in the connection labels and do +not conjugate the second connection. Our definition is instead +``. Although the NLO dipoles agree once index directions are +matched, beyond NLO one must translate the adjoint, the order of charges, +fundamental/antifundamental orientations, and final-parton labels together. +Copying two legacy connection IDs is not a defined conversion. + +The quark-pair example is particularly useful. Let + +```python +from pyamplicol import ColorCorrelator, EmitGluon, SplitGluon + +pair = (EmitGluon(1, -1), SplitGluon(-1, -2, -3)) +request = ColorCorrelator("pair-self", bra=pair, ket=pair) +``` + +The API's self-overlap is nonzero in general. At full colour, conjugation +pairs a fundamental index with its antifundamental partner and gives + +```text +sum_(q,qbar) conj(t^a[q,qbar]) t^b[q,qbar] = T_R delta(a,b) +R[pair,pair] = T_R B_11 +``` + +For a quark emitter this is `(1/2)(4/3) B = (2/3) B`; for a gluon emitter +it is `(1/2)(3) B = (3/2) B`, where `B` is the full-colour Born result. +For LC/NLC these identities apply through the retained colour order, rather +than as exact finite-`N_c` equalities between differently truncated matrices. +This is not the zero of the note's oriented `(D.1,D.1)` pairing: that pairing +does not represent the same adjointed contraction. With the generator +normalization above, the closed pair trace is `T_R delta(a,b)`, not +`C_F delta(a,b)`. + +`SplitGluon` represents **one labelled quark flavour**. Neither a flavour sum +`n_f`, additional powers of the strong coupling, unresolved phase space, +kinematic splitting functions, nor unresolved-particle symmetry factors are +included. These belong to the application using the correlated Born. + +## Products and anticommutators with a shared colour line + +The note's double-soft example contains an anticommutator +`{A,B} = AB + BA`, with `A = T_2 . T_3` and `B = T_1 . T_2`. +The shared leg 2 makes the order important. In the API's adjointed convention, +one valid construction is: + +```python +bra = (EmitGluon(3, -1), EmitGluon(1, -2)) +ab = ColorCorrelator( + "AB", bra=bra, + ket=(EmitGluon(2, -2), EmitGluon(2, -1)), +) +ba = ColorCorrelator( + "BA", bra=bra, + ket=(EmitGluon(2, -1), EmitGluon(2, -2)), +) +# Include ab and ba in CorrelatorConfig.color_correlations before generation. +``` + +For `AB`, the ket acts as `T_2^a T_2^b`; the bra adjoint supplies +`T_1^b T_3^a`. Charges on different Born legs commute, giving +`T_2^a T_3^a T_1^b T_2^b = AB`, with `a,b` summed. Swapping the +ket operations gives `BA`, without commuting the charges on leg 2. + +After generating and loading a runtime containing these IDs: + +```python +from pyamplicol import CorrelatedRequest + +values = runtime.evaluate_correlated_many(points, { + "AB": CorrelatedRequest(color_correlation="AB"), + "BA": CorrelatedRequest(color_correlation="BA"), +}) +anticommutator = tuple( + (x.real + y.real, x.imag + y.imag) + for x, y in zip(values["AB"], values["BA"], strict=True) +) +``` + +There is no factor of `1/2` in this definition of the anticommutator. +When combining high-precision `Decimal` results, use a decimal context with +at least the requested precision. More general connection products should +likewise be built at the tensor level, not by multiplying two Born overlap +matrices: a nonorthogonal colour basis has a nontrivial metric, and a Born +basis need not remain closed under an insertion. + +## Colour-coherence checks + +For a full-colour, colour-conserving Born amplitude, the standard checks are + +```text +sum_j B_ij = 0 (sum includes j = i) +B_ii = C_i B (C_i = C_F or C_A) +sum_(j != i) B_ij = -C_i B +``` + +The sum runs over all coloured Born legs, with incoming charges already +crossed. A nontrivial connected tensor also obeys colour conservation, but +now the sum includes **all its current coloured legs**, including emitted +gluons or daughter quarks, but excluding a parent removed by a splitting. +This gives iterative checks at NNLO and N3LO. +One should not expect an unweighted sum over every distinct higher-order +connection to vanish: those entries describe different emission histories, +and sometimes different final colour spaces. + +The note also groups histories in which only Born legs emit gluons, its +"abelian-like" subset. If commuting histories are collapsed to one canonical +representative, the missing ordering multiplicity must be restored. With +`m` emissions and `n_i` emissions on Born leg `i`, preserving the order on +each individual line, this multiplicity is `m! / product_i(n_i!)`. +At NNLO it is 2 for two distinct emitters and 1 for two emissions from the +same emitter; at N3LO the corresponding patterns give 6, 3, and 1. +These weights describe such a canonicalized sum; the API does not multiply +individual requested correlations by them. They apply only after identifying +genuinely equivalent labelled contractions: charges on a shared line must +not be commuted, and any relabelling of an auxiliary index must be made +consistently across the whole bra/ket contraction. + +For example, interchanging `EmitGluon(i, -1)` with `EmitGluon(j, -2)` +commutes when `i != j` and their labels are kept attached to the same emitters. +Changing the assignment to `EmitGluon(j, -1)` and `EmitGluon(i, -2)` is a +different labelled map. Preserve these assignments, or define a consistent +symmetrization on both sides, before using a weighted higher-order coherence +sum from the note. + +## Spin contractions and Ward identities + +External spin states and vector components are four-dimensional. The API +does not supply a full `d = 4 - 2 epsilon` spin tensor or the +epsilon-dependent polarization averages of conventional dimensional +regularization. + +For a declared external vector leg, the setter replaces its source by the +literal contravariant vector `(v0, vx, vy, vz)`, using metric `(+,-,-,-)`. +There is no normalization or transverse projection. In schematic notation, +if the amplitude is `v^mu M_mu`, the same vector on the ket and its complex +conjugate on the bra form the rank-one spin contraction. Distinct vectors on +several declared legs act jointly, not as a product of separate squared +matrix elements. + +No extra average over the supplied vectors is introduced. Original incoming +spin/colour averages are retained, and unreplaced helicities are summed. +The usual two-polarization completeness sum recovers the unpolarized +massless-vector result. Scaling a single vector by `z` scales the answer by +`abs(z)**2`. + +For a massless gauge boson, replacing its polarization by its momentum tests +the Ward identity with the other gauge legs in physical transverse states; +several momentum replacements are also allowed if their joint class was +declared. The vanishing is not guaranteed if another gauge leg is instead +given an arbitrary non-transverse source. This is a literal nonzero momentum +source, not its +projection onto physical helicities. A massive vector instead satisfies the +appropriate Goldstone relation; it is not subject to the same zero test. +The setter currently exposes rank-one contractions, not independent bra/ket +vectors or a general spin-density tensor. + +See [Born Correlations](correlators.md) for runnable generation/evaluation +examples, vector batching, reset behaviour, and the exact execution scope. diff --git a/pyamplicol/docs/correlators.md b/pyamplicol/docs/correlators.md new file mode 100644 index 0000000000..8fd42856ce --- /dev/null +++ b/pyamplicol/docs/correlators.md @@ -0,0 +1,481 @@ +--- +title: "Born Correlations" +parent: "Python API" +nav_order: 4 +--- + +# Tree-level spin and colour correlations + +For the mathematical definitions, charge normalization, emission ordering, +and translation of the original MadNkLO note, see +[Correlation Conventions](correlator-conventions.md). + +Correlations are an explicit generation-time option. Declare the colour +operators and allowed joint spin replacements once, then select an operator +by ID and supply numerical spin vectors at runtime. This provides +four-dimensional tree-level correlated Born quantities, with a generic +construction of ordered colour connections at any finite perturbative order. +It is not restricted to NLO or NNLO. This is **not** a complete subtraction implementation: no +loop amplitudes, kinematic splitting kernels, integrated counterterms, or +extra-dimensional spin components are supplied. + +The correlated path supports **LC, NLC and full colour** through Python and +the native C/C++/Fortran/Rust SDKs, with the approximation selected at generation. It retains the +complete colour basis and uses direct contraction. Ordinary uncorrelated +generation and evaluation are unchanged. + +## Generate and evaluate + +This small example uses the built-in Standard Model and a new output directory: + +```python +from pyamplicol import ( + ColorCorrelator, CorrelatorConfig, Generator, ModelSource, Runtime, +) +from pyamplicol.config import ColorConfig, RunConfig + +declarations = CorrelatorConfig( + color_correlations=(ColorCorrelator.dipole("T13", 1, 3),), + spin_correlations=((3,), (3, 4)), +) +config = RunConfig(action="generate", color=ColorConfig(accuracy="full")) +result = Generator(config).generate( + "g g > g g", + "artifacts/gg_correlated", + model=ModelSource.built_in_sm(), + correlators=declarations, +) +runtime = Runtime.load(result.output) +point = ( + (500, 0, 0, 500), + (500, 0, 0, -500), + (500, 300, 0, 400), + (500, -300, 0, -400), +) + +# "born" is always included: the ordinary full-colour overlap. +born = runtime.evaluate_correlated((point,), precision=40)[0] +colour = runtime.evaluate_correlated( + (point,), color_correlation="T13", precision=40, +)[0] + +# Replace leg 3's polarization by a literal transverse four-vector. +runtime.set_spin_correlation_vectors({3: (0, 0, 1, 0)}) +spin = runtime.evaluate_correlated((point,), precision=40)[0] +spin_colour = runtime.evaluate_correlated( + (point,), color_correlation="T13", precision=40, +)[0] +print(born.real, colour.real, spin.real, spin_colour.real, spin_colour.imag) +runtime.set_spin_correlation_vectors(None) +``` + +Thus `"born"` with no replacement gives the ordinary helicity-summed Born; +`"T13"` with no replacement gives a colour correlation; `"born"` with vectors +gives a spin correlation; and `"T13"` with vectors gives both together. +`helicities=None` sums all spectator helicities. A global `helicities=` selector +can instead restrict the physical helicity IDs, as for ordinary evaluation. + +The result is a tuple of `CorrelatedValue` objects, one per phase-space point. +Each has `real` and `imag` fields of type `Decimal`; `precision` specifies +decimal digits and defaults to 16. `complex(value)` is convenient but converts +to ordinary double precision. An ordered colour correlation can be complex: +do not discard its imaginary part before forming the intended physical +combination. All calls use the runtime's current model parameters and +normalization settings. + +## Several requests and several phase-space points + +Use `evaluate_correlated_many` to share amplitudes between requested operators, +instead of calling `evaluate_correlated` in a loop. Requests are named by the +caller; several names can select the same generated colour ID with different +spin vectors. For the generated example above: + +```python +from pyamplicol import CorrelatedRequest + +points = (point, point) # Replace with any batch of distinct physical points. +requests = { + "born": CorrelatedRequest(color_correlation="born", spin_vectors={}), + "dipole_13": CorrelatedRequest(color_correlation="T13", spin_vectors={}), + "spin_3": CorrelatedRequest( + color_correlation="born", spin_vectors={3: (0, 0, 1, 0)} + ), + "dipole_13_spin_3": CorrelatedRequest( + color_correlation="T13", spin_vectors={3: (0, 0, 1, 0)} + ), +} +values = runtime.evaluate_correlated_many(points, requests, precision=40) + +# Axis 1 is the request name; axis 2 is the original phase-space point index. +entry = values["dipole_13_spin_3"][1] # Second point, this specific contraction. +print(entry.real, entry.imag) # Decimal, preserving complex correlations. +all_points_for_dipole = values["dipole_13"] +all_requests_at_second_point = {name: series[1] for name, series in values.items()} +assert all(len(series) == len(points) for series in values.values()) +``` + +The returned dictionary preserves request order and each tuple preserves input +point order. No result axis is flattened or summed over the requested IDs. +Spectator helicities are still summed, or restricted by the batch-global +`helicities` selector; they are not a third returned axis. Per-point spin +vectors use the same vector-batch notation as the setter below. + +`spin_vectors=None` (the default) inherits a snapshot of the current setter +state. An explicit `{}` instead requests physical helicities, even when the +setter holds vectors. Request-local vectors do not change that state. All +requests are validated before evaluation; an unknown colour ID or invalid spin +class does not partially update the runtime. + +For each distinct spin assignment the amplitudes are calculated once and +shared by all its colour requests. Across assignments, existing evaluation +stages with exactly unchanged inputs can also be reused. This is stage-level +sharing, not construction of a full spin-density tensor. Its benefit depends +on which stages depend on the changed vectors. Numerical caches are local to +the call and streamed point by point; parameter or precision changes on later +calls cannot reuse an earlier numerical result. + +The standalone [local NLO limit example](https://github.com/MadGraphTeam/pyamplicol/blob/main/examples/NLO_limits/nlo_limit_stability_tests.py) +uses both axes: all required dipoles are requested together, with the whole +approach to an unresolved limit supplied as a phase-space batch. + +## Spin vectors and joint classes + +Leg labels are one-based, in the generated public process order, and components +are contravariant `(v0, vx, vy, vz)` with metric `(+,-,-,-)`. Values may be real +or complex. A four-vector broadcasts to every point; a sequence of four-vectors +provides one vector per point: + +```python +runtime.set_spin_correlation_vectors({3: ((0, 0, 1, 0), (0, 0, 2j, 0))}) +batch = runtime.evaluate_correlated((point, point), precision=40) + +runtime.set_spin_correlation_vectors({3: (0, 0, 1, 0), 4: (0, 0, 1, 0)}) +joint = runtime.evaluate_correlated((point,), precision=40)[0] +runtime.set_spin_correlation_vectors({}) # Reset; None is equivalent. +``` + +### Decimal inputs and double-double arithmetic + +Supply `Decimal` components directly when their digits matter. For a complex +component, use a `(real, imaginary)` pair of `Decimal` values; constructing a +Python `complex` first would round both parts to binary64. The setter and +`CorrelatedRequest.spin_vectors` both preserve these inputs until evaluation: + +```python +from decimal import Decimal +from pyamplicol import CorrelatedRequest + +zero = Decimal(0) +vector = (zero, zero, ( + Decimal("1.0000000000000000000000001"), + Decimal("0.0000000000000000000000002"), +), zero) +runtime.set_spin_correlation_vectors({3: vector}) +arb = runtime.evaluate_correlated((point,), precision=80) +dd = runtime.evaluate_correlated( + (point,), arithmetic="double-double", precision=31, +) +grouped = runtime.evaluate_correlated_many( + (point,), {"spin_dipole": CorrelatedRequest("T13", {3: vector})}, + arithmetic="double-double", precision=31, +) +print(grouped["spin_dipole"][0].real) # Decimal result, no float conversion. +runtime.set_spin_correlation_vectors(None) +``` + +The default `arithmetic="arbitrary"` uses arbitrary-precision arithmetic at +the requested decimal precision. `arithmetic="double-double"` explicitly +selects genuine Symbolica DoubleFloat arithmetic for sources, retained +evaluation stages, colour contraction and normalization; `precision` then +controls output rounding and must not exceed 31 digits. Inputs finer than +that are rounded when entering DoubleFloat, not when defining the vector. +Both modes return `Decimal` real/imaginary parts. Supply `Decimal` momenta as +well when kinematic input accuracy beyond binary64 is needed. Native SDK +correlated calls are binary64 only; the Python arithmetic selection does not +change their numerical type. + +The nonempty set of supplied legs must match a declared class **exactly**. +Here `{3}` and `{3, 4}` are allowed, but `{4}` is not. Every declared leg must +be a spin-1 source with four components. Batch lengths must match the number +of evaluated points. State belongs to each `Runtime` instance; setters copy +their inputs, and a failed setter leaves the previous vectors intact. + +Vectors replace the external source literally, including its temporal +component. They are not normalized, projected onto helicity states, or +conjugated on input; the existing source crossing phase is applied once. +Use physical incoming/outgoing momenta in their normal public ordering, with +no manual crossing of the supplied vectors. The same vector enters the +amplitude and is conjugated through the bra, giving a rank-one spin +contraction. Joint replacements contract the same amplitude with several +vectors; they are not products of separate one-leg results. Independent +bra/ket spin vectors and full spin-density tensors are not exposed. + +Each replaced leg counts once; spectator helicities are summed incoherently. +The original incoming-spin and colour averages and identical-particle factors +are retained, without an extra average over supplied vectors. Vector magnitude +is preserved: scaling one vector by `z` scales the correlation by `abs(z)**2`. +For a physical massless polarization choose `p.v = 0`; no such condition is +enforced by the setter. Setting, for example, `{3: point[2]}` supplies a literal +nonzero momentum vector for a Ward-identity check. Its vanishing is a +massless gauge-boson statement, not a generic test for massive vectors. + +## Automatically register every colour correlation through N^kLO + +Use `CorrelatorConfig.all_color(through_order=k)` when the required subset is +not known in advance. For example, this prepares every compatible pair of +one- and two-operation tree-soft colour connections, including the Born: + +```python +declarations = CorrelatorConfig.all_color( + through_order=2, # 1: NLO; 2: through NNLO; 3: through N3LO; any k >= 1. + spin_correlations=((3,),), +) +config = RunConfig(action="generate", color=ColorConfig(accuracy="full")) +result = Generator(config).generate( + "g g > g g", "artifacts/gg_all_correlations", + model=ModelSource.built_in_sm(), correlators=declarations, +) +runtime = Runtime.load(result.output) + +catalogue = runtime.available_color_correlations() +for entry in catalogue: + print(entry.id, entry.order, entry.bra, entry.ket) + +# Every NNLO component, at every point, in one grouped call. +requests = { + entry.id: CorrelatedRequest(entry.id, spin_vectors={}) + for entry in catalogue if entry.order == 2 +} +values = runtime.evaluate_correlated_many(points, requests, precision=40) +first_id = next(iter(requests)) +print(values[first_id][0].real, values[first_id][0].imag) +``` + +Here `points` is a batch of four-leg phase-space points as above. The catalogue +is specific to the selected process and includes `"born"` at order zero. +Listing it loads the stored declarations and matrices but does not initialize +amplitude evaluation. The ordinary `evaluate` path is unchanged. + +This is **not** a factory for products of dipoles. At each step every active +coloured leg can emit; an emitted gluon can also split into a quark pair, and +its daughters can radiate later. Every final unresolved-label assignment is +included. The factory pairs all histories of the same order with identical +labelled final representations, including both directed orders and self-pairs. +It follows the note's soft-current construction: hard Born-gluon splitting is +not included automatically, but remains available in explicit declarations. + +The recursion has no fixed order cap. Its complete, nonminimal catalogue grows +rapidly with order and multiplicity; it can be much larger than a chosen +subtraction basis. Chronology is the order of colour operations, **not** an +assumption of strongly ordered soft energies. The kinematic current, flavour +weights and identical-fermion exchange signs remain the caller's responsibility. +See the [completeness argument and encoding](correlator-conventions.md#automatic-generic-catalogue). + +Automatic IDs have the form `N{r}LO/c{i}/c{j}`, with zero-based connection +indices `i,j` in the sorted order-`r` catalogue. They are process-local labels, +not universal operator names: inspect `bra` and `ket` to identify the physics. +For manually declared operators, IDs remain arbitrary user-chosen strings. +Both forms can be combined with +`CorrelatorConfig(color_correlations=(...), all_color_through_order=k)`; +the `N{r}LO/c{i}/c{j}` ID namespace is reserved when automatic registration +is enabled, so conflicts are rejected before generation starts. + +The equivalent CLI declaration is simply: + +```json +{"all_color_through_order": 3, "spin_correlations": [[3]]} +``` + +Pass this file to the existing `generate ... --correlators correlators.json` +option. Expansion happens separately for each generated process, using its +actual coloured leg labels and crossed representations. + +## Ordered colour connections + +`ColorCorrelator.dipole("T13", 1, 3)` prepares +``. More general requests supply ordered `bra` and `ket` +operations, with the same number of steps on each side and no fixed order cap: + +```python +from pyamplicol import EmitGluon, SplitGluon + +cascade = ( + EmitGluon(1, -1), # Emitter, new gluon label. + SplitGluon(-1, -2, -3), # Parent gluon, new quark, new antiquark. + EmitGluon(-2, -4), # A later step may act on an emitted leg. +) +request = ColorCorrelator("cascade", bra=cascade, ket=cascade) +# Include request in color_correlations before generating the artifact. +``` + +For example, the following declares two- and three-step gluon connections with +different emitters on the bra and ket: + +```python +bra = (EmitGluon(1, -1), EmitGluon(2, -2), EmitGluon(-1, -3)) +ket = (EmitGluon(3, -1), EmitGluon(4, -2), EmitGluon(-2, -3)) +declarations = CorrelatorConfig(color_correlations=( + ColorCorrelator("double", bra=bra[:2], ket=ket[:2]), + ColorCorrelator("triple", bra=bra, ket=ket), + request, +)) +result = Generator().generate( + "g g > g g", "artifacts/gg_higher_correlations", + model=ModelSource.built_in_sm(), correlators=declarations, +) +runtime = Runtime.load(result.output) +double = runtime.evaluate_correlated((point,), color_correlation="double")[0] +triple = runtime.evaluate_correlated((point,), color_correlation="triple")[0] +``` + +These two- and three-step connections supply NNLO and N3LO **colour +structures**, respectively; the corresponding subtraction kinematics and +splitting functions remain the caller's responsibility. + +Positive labels identify Born legs; fresh negative labels identify auxiliary +particles. Both sides must have the same number of steps and finish with the +same labelled colour representations. These are colour operations only: +**do not add unresolved momenta** to the Born phase-space input. Prepare only +the IDs you need; IDs must be unique and `"born"` is reserved. + +The convention is explicitly **bra-adjoint times ket**. For connected basis +tensors, `K[c,d] = ` and the runtime computes +`normalization * sum_h,c,d conjugate(A[h,c]) * K[c,d] * A[h,d]`. +The full directed matrix is used, without assuming Hermiticity. Physical +SU(3) generators satisfy `tr(t^a t^b) = delta(a,b)/2`; colour roles use the +all-outgoing convention, with incoming fundamental representations crossed +by generation. No additional crossing sign should be supplied by the caller. +`SplitGluon` represents one labelled quark flavour with physical `T_R = 1/2`; +no flavour sum (`n_f`), extra `g_s` factors, unresolved phase-space factors, or +symmetry multiplicities are included automatically. +Build higher connections from ordered operations, not by multiplying Born +overlap matrices: those matrices are not action matrices in an orthonormal +closed basis. + +## JSON and command line + +The equivalent declaration file `correlators.json` is: + +```json +{ + "color_correlations": [ + { + "id": "T13", + "bra": [{"kind": "emit-gluon", "emitter": 1, "emitted": -1}], + "ket": [{"kind": "emit-gluon", "emitter": 3, "emitted": -1}] + } + ], + "spin_correlations": [[3], [3, 4]] +} +``` + +```console +pyamplicol generate 'g g > g g' artifacts/gg_correlated_cli \ + --model built-in-sm --color-accuracy full --correlators correlators.json +``` + +`CorrelatorConfig.to_json_dict()` and `from_json_dict()` use this same schema. +A splitting step uses `{"kind": "split-gluon", "parent": -1, +"quark": -2, "antiquark": -3}`. The CLI option prepares the artifact; +numerical vector setting and correlated evaluation use Python or the native +SDKs rather than the CLI `evaluate` command. + +## Native correlated evaluations + +The `correlators` development branch provides binary64 correlated evaluation +in C, C++, Fortran and standalone Rust. These entry points use RustiCol and do +not load Python or Symbolica. Python's separate executor supports +double-double and arbitrary precision. See [Native APIs](user/native-apis.md#correlated-born-evaluations) +for the language-specific owned request types, catalogue enumeration, spin +setter and result indexing. + +The complete native examples evaluate the Born overlap, a mixed +adjoint/fundamental N3LO interference and its spin-correlated counterpart +together over two points. Generate their input with: + +```python +from pyamplicol import ColorCorrelator, CorrelatorConfig, EmitGluon +from pyamplicol import Generator, ModelSource, ProcessSet +from pyamplicol.config import ColorConfig, RunConfig + +alpha = (EmitGluon(2, -3), EmitGluon(-3, -1), EmitGluon(-3, -2)) +beta = (EmitGluon(1, -1), EmitGluon(3, -2), EmitGluon(4, -3)) +declarations = CorrelatorConfig( + color_correlations=(ColorCorrelator("cascade-interference", alpha, beta),), + spin_correlations=((5,),), +) +config = RunConfig(action="generate", color=ColorConfig(accuracy="full")) +Generator(config).generate( + ProcessSet.from_expressions(("d d~ > d d~ g",), names=("dd_ddg",)), + "artifacts/native_correlated", model=ModelSource.built_in_sm(), + correlators=declarations, +) +``` + +The auxiliary gluon emitted from leg 2 radiates twice in `alpha`; `beta` +attaches three gluons to distinct quark or antiquark legs. Their final auxiliary colour +labels match, so their overlap is the interference, not the square of either +history. With the matching development SDK installed, run from the checkout: + +```console +make -C examples/native correlated +examples/native/correlated_cpp artifacts/native_correlated dd_ddg +examples/native/correlated_fortran artifacts/native_correlated dd_ddg +examples/native/correlated_rust artifacts/native_correlated dd_ddg +examples/native/correlated_c artifacts/native_correlated dd_ddg +``` + +Each reports `VALUE request_index point_index real imaginary`, with zero-based +indices and request order Born, colour interference, colour-plus-spin +interference. The programs also demonstrate reading catalogue IDs and full +histories, supplying default spin vectors, clearing them, and keeping ordinary +evaluation unchanged. C/C++/Rust grouped result access is request then point; +Fortran's natural array layout is `values(point,request)`. + +## Colour accuracy and execution scope + +| Requested quantity | Correlated support | +| --- | --- | +| Full colour | Exact SU(3) colour matrices, including interference | +| Leading colour (LC) | Leading powers of the inserted colour matrix | +| Next-to-leading colour (NLC) | Inherited first-subleading colour-matrix approximation | + +`color.accuracy` (CLI `--color-accuracy lc|nlc|full`) selects the correlated +answer at generation. The ordinary default is LC; specify `full` explicitly +for exact SU(3) contractions, as above. Passing `correlators=` still selects +direct contraction and generic compiled amplitudes and disables unsupported +reductions, recording those adjustments in the effective configuration. +There is no runtime colour-accuracy or single-flow selector for either +correlated method. Generate separate outputs to compare accuracies. + +The underlying amplitude output remains complete and full-colour. Ordinary +`Runtime.evaluate` on it therefore evaluates that underlying full-colour +process; use `evaluate_correlated(..., color_correlation="born")` for the +Born result at the requested correlated accuracy. See +[the colour-accuracy convention](correlator-conventions.md#colour-accuracy) +for the inherited LC/NLC prescription and its retained-order checks. + +Correlated generation selects complete full-colour, direct, generic compiled +amplitudes and disables helicity-specific zero/parity reductions and current +reuse that would invalidate arbitrary source vectors. Python correlated +evaluation uses a separate exact executor over retained Symbolica evaluator +states. Native SDK correlated evaluation uses RustiCol's binary64 coherent +amplitude executor. Both share amplitudes and unchanged stages between +requests and contract the generation-time colour matrices directly; neither +uses FFT or physical-helicity reductions for the correlated result. + +FFT, recurrence, on-the-fly execution, replay-based reductions, nonidentity +process aliases/permutations, partial helicity/colour generation, and append +mode are not supported by this correlation path. Existing artifacts without +the explicit declaration cannot be upgraded merely by setting vectors. +Ordinary generation and `Runtime.evaluate(...)` are unchanged; setting spin +vectors affects only `evaluate_correlated(...)` and inherited requests in +`evaluate_correlated_many(...)`. + +## References + +The [archived MadNkLO note](https://github.com/MadGraphTeam/pyamplicol/blob/80aec6f706c409b2f8f6c793dff8d8f04b118f7d/IMPLEMENTATION_DOCS/REFERENCES/ColorCorrelators_MadNkLO.pdf) +and [MadNkLO source at revision 646a3db](https://github.com/madnklo/madnklo/tree/646a3db9c8efd7b4cb00e9d89b9197cd5394c01b) +provide background. The note's oriented, non-conjugated connections are not +the runtime's literal bra-adjoint/ket convention: translate adjoints, +orientations, signs, and phases before comparing individual ordered entries. diff --git a/pyamplicol/docs/development/API_CONTRACT.md b/pyamplicol/docs/development/API_CONTRACT.md new file mode 100644 index 0000000000..65c6c92f1f --- /dev/null +++ b/pyamplicol/docs/development/API_CONTRACT.md @@ -0,0 +1,357 @@ +--- +title: "Public API Contract" +nav_order: 1 +parent: "Development Documentation" +--- + +# Public API Contract + +This document is normative for Python API version 1. Implementations may add +private helpers but may not widen the package-root export list without an API +review. + +## Package-Root Exports + +`pyamplicol` exports: + +- Model and process types: `ModelSource`, `CompiledModel`, `CompiledModelInfo`, + `CompiledModelSource`, `CompiledModelCapabilities`, + `ModelCompilationIssue`, `ModelCompilationPhase`, `ProcessRequest`, and + `ProcessSet`. +- Configuration: `RunConfig`, `GenerationConfig`, `EvaluationConfig`, and + `BenchmarkConfig`. +- Correlation declarations: `CorrelatorConfig`, `ColorCorrelator`, `EmitGluon`, + and `SplitGluon`. +- Services: `Generator`, `Runtime`, and `BenchmarkRunner`. +- Results: `GenerationPlan`, `GenerationResult`, `BenchmarkResult`, + `BenchmarkStatistics`, `BenchmarkComponentTiming`, `BenchmarkStageTiming`, + `BenchmarkTimingBreakdown`, `BenchmarkProfileCounters`, `ProcessPhysics`, `ExternalParticle`, + `HelicityConfiguration`, `ColorComponent`, `ColorFlow`, + `ContractedColorComponent`, `PhysicsReduction`, `ReductionGroup`, + `ModelParameter`, `ResolvedEvaluation`, and `CorrelatedValue`. +- Process metadata: `ProcessAlias`. +- Helpers: `generate`, `load`, and `benchmark`. +- Package metadata: `__version__`. +- Errors: `PyAmpliColError`, `ConfigurationError`, `ModelError`, + `GenerationError`, `ArtifactError`, `CompatibilityError`, + `EvaluationError`, and `DependencyError`. + +No DAG, evaluator, stage, color-sector, Symbolica, or legacy-reference class is +exported from the package root. + +## Model And Process Requests + +`ModelSource.built_in_sm()` identifies the built-in Standard Model. +`ModelSource.from_path(path, restriction=None, simplify=True)` accepts a UFO +directory, serialized JSON, or compiled-model file and records a resolved +source kind without importing Symbolica. + +`ModelSource.compile(*, cache_dir=None, use_cache=True, +require_supported=True, prepared_output=None, evaluator=None) -> CompiledModel` +is the explicit public model compilation operation. `prepared_output` may also +write a reusable prepared-model bundle using the supplied typed evaluator +configuration. `CompiledModel` is an opaque immutable handle accepted by +generation; compiler-owned expression and tensor IR remain private. +`CompiledModel.info` exposes stable typed source, capability, parameter, +diagnostic, and phase-timing records. Convenience properties mirror those +records, while `write(path)` serializes the complete private payload and +`write_parameter_card(path)` writes mutable external-parameter defaults. + +`ProcessRequest.parse(expression, *, name=None)` validates one process string. +`ProcessSet` contains a non-empty tuple of uniquely named requests and +explicit aliases. At runtime, stored aliases and uniquely inferred +side-preserving permutations use the same Rusticol mapping; incoming and +outgoing legs never cross the process boundary. Public physics, helicity, +color-flow, reduction, and momentum ordering follow the requested expression, +while internal representatives remain hidden. + +## Generation + +`Generator(config=None, progress=None)` accepts immutable typed configuration +or a `ConfigResolution`, plus an optional typed progress sink. Passing the +resolution preserves requested and effective TOML plus every clamp reason in +the generated artifact. + +`Generator.plan(processes, *, model=None) -> GenerationPlan` is strictly +non-writing: it creates no model-cache or output files, directories, locks, or +temporary trees. It expands concrete processes and validates structural color +coverage without compiling a model, process DAG, evaluator, or artifact. A +built-in model, `CompiledModel`, or compiled-model file can always be planned. +For a UFO/JSON `ModelSource`, planning may read a valid existing model-cache +entry but never populates one; on a cache miss callers must first invoke +`ModelSource.compile()` and pass the returned `CompiledModel`. + +Planning performs the same effective Symbolica license and resource resolution +as generation. Restricted mode clamps generation workers and Symbolica cores +to one. Licensed mode partitions the affinity-aware CPU budget after concrete +process expansion. `GenerationPlan.requested_settings` and +`GenerationPlan.effective_settings` retain both typed configurations, while +`GenerationPlan.adjustments` contains typed `ConfigClamp` values with the +requested value, effective value, path, and reason. `generate --dry-run` uses +this operation and has identical non-writing behavior. + +`Generator.generate(processes, output, *, model=None, mode="error", correlators=None) +-> GenerationResult` writes a transactional schema-v3 artifact. `mode` is +`error`, `append`, or `replace`. Both `model` arguments accept a `ModelSource`, +the canonical `CompiledModel`, or `None` for the configured/default source. + +`generate(processes, output, *, model=None, mode="error", config=None, +progress=None, correlators=None)` is a convenience wrapper with the same +generation semantics. + +### Correlation declarations + +`correlators=None` leaves ordinary generation unchanged. Passing +`CorrelatorConfig(color_correlations=(), spin_correlations=(), +all_color_through_order=None)` opts into tree-level correlated Born generation. +This declaration is separate from `RunConfig` and is not an argument to +`Generator.plan()`. + +`ColorCorrelator(id, bra=(), ket=())` names an ordered pair of colour +connections; `ColorCorrelator.dipole(id, left_leg, right_leg)` requests +``. Connection steps are `EmitGluon(emitter_label, +emitted_label)` or `SplitGluon(parent_label, quark_label, antiquark_label)`. +There is no fixed step-count cap; both sides have the same step count, and +their final labelled colour representations must agree. Born legs use public +one-based labels; emitted auxiliary labels must be fresh. Colour IDs must be +unique; `"born"` is reserved and always supplies the ordinary overlap. +`spin_correlations` declares the exact nonempty sets of four-component vector +legs that may be replaced simultaneously. `CorrelatorConfig.to_json_dict()` +and `from_json_dict()` implement the CLI declaration-file schema. + +`CorrelatorConfig.all_color(*, through_order, spin_correlations=())` accepts +a positive integer order `k` and resolves a complete, nonminimal tree-soft +catalogue at orders 1 through `k` separately for each process. Every active +coloured leg can emit; only auxiliary gluons split automatically. All compatible +directed bra/ket pairs are included, without numerical multiplicity or flavour +weights. Catalogue size grows rapidly. IDs of the form `N{k}LO/c{i}/c{j}` are +reserved when automatic generation is enabled; explicit requests may use other +IDs alongside `all_color_through_order`. + +The correlation path applies the requested LC/NLC/full colour accuracy to +the inserted matrices while generating complete full-colour amplitudes. +It records `direct` contraction, `compiled` execution, and disabled numerical +current reuse as configuration adjustments where needed. It preserves arbitrary vector-source dependence +without helicity-specific parity/zero reductions or replay. Partial +colour/helicity generation, nonidentity process permutations, and append mode +are unsupported. A previously generated ordinary artifact cannot acquire this +capability merely by setting spin vectors. See +[Born Correlations](../correlators.md) for conventions and examples. + +## Runtime + +`Runtime.load(artifact, *, process=None, model_parameters=None, +mute_warnings=False) -> Runtime` validates the schema and identity-contract +marker, confined references, target, and ABI before loading executable state. +Call `pyamplicol.artifacts.validate_payloads()` for an explicit full checksum +audit. + +`Runtime.physics` returns `ProcessPhysics` with stable particles, physical +helicity IDs, physical color-flow IDs, contraction metadata, coverage, and +selector capabilities. LC advertises `("helicity", "color_flow")`; NLC/full +advertise only `("helicity",)`. Their singleton contracted-color output axis is +metadata, not a selectable color flow. For an on-the-fly runtime, requesting +`physics` explicitly materializes the complete compact helicity and color-flow +axes and retains that compatibility view until the runtime is released; +`Runtime.clear()` does not discard it. + +`Runtime.artifact_id` returns the lowercase 64-hex manifest identity +authenticated by the native loader. `Runtime.execution_mode` returns exactly +`"compiled"`, `"eager"`, `"recurrence"`, or `"on-the-fly"` from authenticated +runtime metadata. Both fail with `EvaluationError` when an injected backend +cannot supply a valid identity. + +`Runtime.inspect() -> Mapping[str, object]` is the compact, observational +inspection path implemented by the built-in backend. It returns the +authenticated native metadata without opening `Runtime.physics`, together with +the live on-the-fly retained-state census when applicable. The OTF metadata +includes requested and host-effective query-construction thread counts, and its +inspection advertises `supported_precisions == (16,)`. Inspection is an optional +facade capability rather than a structural requirement of `RuntimeBackend`: +keeping it and the identity properties outside the protocol's original minimum +surface preserves compatibility with third-party backends. +The OTF state reports `family_cache_policy == "last-family-only"` and +`family_cache_limit == 1`; its semantic executor binding count is the exact +current-family binding cardinality rather than an all-seen cumulative count. + +`Runtime.evaluate(momenta, *, helicities=None, color_flows=None, +helicity_by_point=None, color_flow_by_point=None, precision=16)` returns one +fully summed value per point. Global and per-point selectors are mutually +exclusive within each selector dimension. Momenta have shape +`(point, particle, [E, px, py, pz])`. On-the-fly execution currently supports +only `precision=16` (native f64); every other precision fails before selector +resolution or dense metadata access. + +`Runtime.warm_up(momenta, *, precision=16, helicities=None, +color_flows=None, progress=None, n_cores=None) -> WarmUpResult` is available only for OTF +runtimes. `momenta` must contain exactly one binary64 phase-space point. The +operation transactionally constructs and retains the selected family, performs +its first evaluation, and reports elapsed time, total/new query counts, cache +reuse, and optional current/peak RSS. The optional `ProgressSink` observes +process preparation, query-family construction, family finalization, and first +evaluation. LC accepts helicity/flow selectors; contracted NLC/full accepts +helicity selectors only. +`n_cores` is a positive integer overriding the process's construction-worker +setting for this call; `None` retains that setting. Booleans, nonintegers, and +nonpositive counts are rejected. Query traces are constructed with bounded +parallelism, then merged serially. The override is not persisted and does not +change cache identity, family reuse, or later evaluation threading. + +`Runtime.save(path) -> None` and `Runtime.load_cache(path) -> None` are +optional facade capabilities implemented by the native OTF backend. They +write and restore the currently retained completed structural cache, including +its selection and reduction mappings. Restoration requires a compatible +loaded process, rebinds its prepared kernels, and preserves its current model +parameters. Invalid snapshots leave the existing cache unchanged. Snapshots +contain neither evaluated currents nor native addresses, and do not resume +an unfinished construction. Ordinary non-OTF evaluation is unchanged. +The Rust core and C/C++/Fortran/Rust SDKs expose the same operations. + +`Runtime.evaluate_resolved(momenta, *, helicities=None, color_flows=None, +precision=16) -> ResolvedEvaluation` returns LC values with shape `(point, +physical_helicity, physical_color_flow)` and NLC/full values with shape +`(point, physical_helicity, 1)`. `ResolvedEvaluation.total()` must reproduce +`evaluate()`. + +`Runtime.set_spin_correlation_vectors(vectors)` sets the numerical vectors for +`evaluate_correlated()` and inherited requests in `evaluate_correlated_many()`. +`vectors` maps public one-based leg labels to +literal real or complex contravariant `(v0, vx, vy, vz)` vectors, or one such +vector per evaluated point. Its nonempty key set must exactly match a declared +spin class. Inputs are copied; invalid updates leave the old state intact. +Vectors are neither normalized nor projected nor conjugated on input. `None` +or `{}` restores ordinary helicity sources. These settings never change +`evaluate()` or `evaluate_resolved()`. + +`Runtime.available_color_correlations() -> tuple[ColorCorrelator, ...]` lists +the selected process's resolved operators, including `"born"`. Each record +exposes `id`, `order`, `bra`, and `ket`. Listing reads and validates the stored +catalogue without initializing the amplitude executor; original leg ordering +and a correlation-enabled artifact are required. + +`Runtime.evaluate_correlated(momenta, *, color_correlation="born", +helicities=None, precision=16, arithmetic="arbitrary") -> tuple[CorrelatedValue, ...]` selects a declared +colour ID and returns one value per point using the current model parameters +and spin vectors. Global helicity IDs or `HelicityConfiguration` records may +be selected; LC-flow and per-point selectors are not arguments to this method. +`CorrelatedValue.real` and `.imag` are `Decimal` values at the requested decimal +precision; `complex(value)` converts to binary64. The full directed +bra-adjoint/ket contraction may be complex. Each replaced spin leg counts +once, spectator helicities are summed incoherently, and ordinary initial-state +averages and identical-particle factors remain in place. + +`Runtime.evaluate_correlated_many(momenta, requests, *, helicities=None, +precision=16, arithmetic="arbitrary") -> dict[str, tuple[CorrelatedValue, ...]]` accepts an ordered +mapping of user result labels to `CorrelatedRequest` records. Each record has +`color_correlation="born"` and `spin_vectors=None` defaults. `None` inherits a +snapshot of the setter; `{}` explicitly selects physical helicities. Request +vectors are copied and validated without changing setter state. Request labels +are distinct from generated colour IDs, allowing several spin projections of +one colour operator. The output preserves label order and input point order; +`values[label][point_index].real` and `.imag` select a single component. +Every series has `len(momenta)` entries. Spectator helicities are summed or +selected globally, not an additional output axis. + +Both methods accept `arithmetic="double-double"` for genuine DoubleFloat +arithmetic with at most 31 output digits (`precision=31`). Their default +`"arbitrary"` retains arbitrary precision. Spin-vector real components may be +`Decimal`; complex components may be `(Decimal(real), Decimal(imaginary))` +pairs. Setter and request validation preserve their digits until conversion +to the selected arithmetic, and results always use plain `Decimal` fields. + +Identical spin assignments share coherent amplitudes across colour IDs; +unchanged complete stage inputs can share stage outputs across assignments. +Numerical sharing is exact, signed-zero preserving, call-local and streamed +point by point. There is no persistent numerical cache or full spin-response +tensor. The underlying ordinary `evaluate()` remains full-colour; the +correlated `"born"` request uses the chosen correlated accuracy. + +Python correlated evaluation uses the Symbolica-backed exact executor even +at `precision=16`. The C/C++/Fortran/Rust SDKs provide a separate binary64 +RustiCol executor, catalogue enumeration, spin defaults and grouped requests. +Both paths require an explicitly correlated artifact and its original leg +ordering; both use the generation-time LC/NLC/full inserted colour matrices. +Native results are complex and request-major, with point-major Fortran array +access `values(point,request)`. Native APIs do not expose Python's arbitrary +precision or double-double arithmetic. Recurrence, eager, on-the-fly and FFT +correlated evaluation remain unsupported. See [Native APIs](../user/native-apis.md#correlated-born-evaluations) +for the typed SDK request and spin-vector conventions. + +`Runtime.set_model_parameters(mapping)` validates the complete update before +committing it. `set_model_parameter(name, value)` is a convenience wrapper. +`mute_warnings()` and `unmute_warnings()` modify only that runtime handle. +`Runtime.clear()` discards warmed OTF process/family/scratch state while +retaining the loaded artifact, current model parameters, and any already +materialized `physics` compatibility view. It is a no-op for recurrence, +compiled, and eager ordinary execution state. If a correlated evaluator has +been initialized, `clear()` additionally releases its loaded stage/amplitude +evaluators while retaining declarations, colour matrices, and current spin +vectors. The next correlated call reloads that state; use +`set_spin_correlation_vectors(None)` to reset the vectors themselves. + +`load(...)` is an alias for `Runtime.load(...)`. + +## Benchmarking + +`BenchmarkRunner(config=None, progress=None).run(target, *, points=None) +-> BenchmarkResult` accepts a runtime or artifact path. Results contain +requested/effective configuration, sample count, wall time per point, pure +evaluator time where available, uncertainty statistics, and environment +provenance. When native profiling is available, +`BenchmarkResult.timing_breakdown` contains typed top-level and internal timing +attribution, per-stage timings, and `BenchmarkProfileCounters`. Internal +leaf/backend/output-gather/remap attribution is non-additive: full-stage +evaluator envelopes own leaf gathering, while composed selected-chunk +input-pack envelopes own it. Movement and materialization counters are +normalized per profiled point; backend-call and explicit allocation counters +are normalized per runtime call. Compiled Direct-Arena engine/call counts and +its input/current-output/amplitude-output boundary-byte counters are likewise +normalized per runtime call. A valid fused Arena profile reports zero boundary +traffic and enough Arena calls to cover every evaluator backend call. + +For on-the-fly execution, benchmarking snapshots the authenticated compact +runtime-state census immediately before and after its separately timed first +requested-workload evaluation, before configured warm-up runs. The environment +records both immutable snapshots, whether the initial runtime was cold or +already retained, and evidence that the first evaluation left a complete +retained family. A missing, mismatched, partial, or malformed census fails +closed. Other execution modes do not require this private census capability. + +`benchmark(...)` is the convenience wrapper. + +## Stability Rules + +All public dataclasses are frozen. Public collections are tuples or immutable +mappings. Paths are accepted as `os.PathLike[str]` and returned as absolute +`Path` objects only when they describe user-created files. + +Machine-readable methods return typed objects or documented JSON, never +volatile dictionaries. JSON output writes only to stdout; diagnostics use +logging/stderr. + +## Typing Gate + +`just typing` is the normative public Python typing gate and is also a +dependency of `just check`. It runs strict mypy over an explicit source list: +the package root, `pyamplicol.api`, the public configuration models/registry +and `pyamplicol.config` facade, `pyamplicol.reporting`, and +`pyamplicol.runtime`. Imported implementation modules are followed for type +information with their diagnostics silenced; no public target uses +`ignore_errors`. + +A second mypy pass stages a temporary installed-style package, including +`py.typed` and the maintained `_rusticol.pyi`, then checks external consumers. +Those consumers assert exact Generator, Runtime, configuration, protocol, and +result types. Every name in the `__all__` lists of `pyamplicol` and its public +`api`, `config`, `reporting`, and `runtime` facades is also rejected if it +degrades to `Any`. The native consumer checks the maintained extension stub, +and the metadata check compares that stub with the Rust binding export list +without rebuilding Rust. + +This is intentionally not a project-wide strictness claim. Dynamic generation, +model compilation, evaluator, color, artifact, CLI, and other private +implementation modules are outside the source target. Bundled vendor UFO +assets under `src/pyamplicol/assets/models/ufo/` and generated standalone API +templates under `src/pyamplicol/assets/api_templates/` are explicitly +excluded. Their exclusion does not weaken checks of the documented public +signatures that expose their results. diff --git a/pyamplicol/docs/development/ARCHITECTURE_DECISIONS.md b/pyamplicol/docs/development/ARCHITECTURE_DECISIONS.md new file mode 100644 index 0000000000..288207fd1a --- /dev/null +++ b/pyamplicol/docs/development/ARCHITECTURE_DECISIONS.md @@ -0,0 +1,123 @@ +--- +title: "Architecture Decisions" +nav_order: 3 +parent: "Development Documentation" +--- + +# Architecture Decisions + +## Standalone Repository + +`pyamplicol` is an independent repository and distribution. Build, runtime, +tests, examples, and release tooling may use only files contained in the source +checkout, source distribution, installed wheel, or explicitly downloaded +dependencies. No installed path or generated artifact may depend on another +checkout. + +The Python distribution is named `pyamplicol`. Rusticol is the runtime and +native ABI name, not a second Python package. + +## Completion States + +**Candidate-complete** means source, API, physics, native SDK, wheel, source +distribution, and isolated-install tests pass against pinned contributor +inputs. Candidate package files are marked non-publishable. + +**Release-complete** additionally requires exact compatible published +dependencies and the full supported-platform deployment matrix. Release mode +never substitutes candidate state. + +## Model Boundary + +Serialized external JSON is the primary user path. Trusted UFO and JSON inputs +compile into one canonical model IR before process planning. Model-owned +symbols have deterministic namespaces; executable compiled expressions may not +retain raw process-global UFO symbols. + +The hand-coded built-in SM is a compatibility model contained under +`pyamplicol.models.builtin`. Its particle tables, aliases, synthetic fields, +and optimized kernels must not define generic external-model behavior. + +Generic physics decisions use explicit model metadata: canonical species and +anti-relations, signed-PDG orientation, spin/statistics, color representation, +mass/width policy, propagator, source/crossing records, exact quantum numbers, +and normalized color/Lorentz tensors. Absolute SM PDG values and particle names +are interoperability labels, not generic taxonomy. + +Unsupported model features fail in preflight with structured diagnostics. +Built-in optimizations are permitted only behind a proven applicability record +and a model-independent fallback. + +## Processes And Artifacts + +One process request may expand into many concrete processes. Stable process +names are artifact selectors and remain distinct from the output directory. +Generic `p`/`j` behavior is model/config data. Every model-derived catalog also +defines `all` from its propagating physical external states, excluding ghosts, +Goldstones, and auxiliary/non-propagating records. The complete legacy alias +table belongs only to the built-in compatibility model. + +Schema-v3 artifacts are transactionally written executable inputs. Their +manifest verifies payload paths, digests, producer/runtime ABI, target, and +required capabilities. These checks detect accidental mutation; trust in the +artifact producer remains an external user decision. + +The optimized total and resolved evaluation are two views of one runtime plan. +Resolved LC output retains physical color flows; NLC/full currently expose one +contracted SU(3) color component per helicity. + +## Runtime And APIs + +Rusticol core is Python-independent. Boundary crates expose one PyO3 extension +and C ABI v1. The wheel owns a header-only C++17 wrapper, Fortran 2008 module +source, static C ABI archive, and target link metadata. + +Generation emits one root API bundle with Python, dependency-free Rust 2021, +C++17, and Fortran 2008 drivers. All drivers support concrete-process +selection, JSON and direct model-parameter updates, resolved evaluation, and +explicit total comparison. Python supports precision-controlled evaluation; +native APIs expose f64 only. + +Runtime parameter batches are atomic. Mutable handles are not concurrently +reentrant; independent handles may run concurrently. + +## Symbolica Boundary + +Symbolica is imported lazily for model compilation, generation, and Python +precision paths that require it. Effective license state comes from +`symbolica.is_licensed()`. + +The default JIT artifact embeds a direct SymJIT f64 application. Rusticol loads +that capability without importing Symbolica or requiring a Symbolica license. +ASM/C++ artifacts and Python precision paths advertise their retained Symbolica +runtime requirement explicitly. + +## Build And Release + +The PEP 517 backend and pinned Maturin build the Python extension and static SDK +from one locked Rust workspace. `dependencies/release-lock.toml` describes exact +published release inputs; contributor state is source-checkout-only and cannot +enter release package files. + +Release validation uses standard package metadata, wheel `RECORD`, model and +artifact payload digests where the formats require them, `twine check`, platform +audits, clean installation, cross-language runtime tests, and PyPI Trusted +Publishing. Publication consumes already validated wheels and one source +distribution and performs no build. + +## Legacy Physics Parity Reference + +The legacy Fortran reference is developer-only. It is prepared as an isolated, +pinned checkout by developer tooling, is never imported by the installed +package, and is excluded from release package files. It provides a separately +implemented legacy parity authority and performance baseline. Because it shares +validation lineage and known defects with the current implementation, agreement +does not establish independent physics correctness. Future full-colour, +arbitrary-precision comparisons with MadGraph are required; agreement among +APIs that share Rusticol validates the ABI only. + +## Supported Platforms + +Binary releases target macOS arm64, macOS x86_64, and manylinux x86_64 with +`cp311-abi3`. Source installation may work elsewhere but is unsupported until +the complete native and physics matrix passes there. diff --git a/pyamplicol/docs/development/CONFIG_CONTRACT.md b/pyamplicol/docs/development/CONFIG_CONTRACT.md new file mode 100644 index 0000000000..c568afb0ee --- /dev/null +++ b/pyamplicol/docs/development/CONFIG_CONTRACT.md @@ -0,0 +1,302 @@ +--- +title: "Configuration Contract" +nav_order: 2 +parent: "Development Documentation" +--- + +# Configuration Contract + +TOML schema version 1 and the Python configuration dataclasses share one field +registry. Unknown fields are errors. Paths are resolved relative to the card. + +Precedence is defaults, card, dedicated CLI flags, repeatable `--set` +overrides in order, then recorded license/resource clamping. + +## Top Level + +- `schema_version: int = 1` +- `action: generate | evaluate | benchmark | inspect | model-inspect | + model-compile | model-processes | group-processes` + +## Model + +- `source: str = "built-in-sm"` +- `restriction: str | null` +- `simplify: bool = true` +- `cache: bool = true` +- `cache_dir: path | null` + +## Process + +- `entries: list[{ expression: str, name: str | null }]` +- `multiparticles: table[str, list[str]] = {}` +- `flavor_scheme: int = 5` +- `max_quark_lines: int | null` +- `coupling_order_policy: minimal | explicit = minimal` +- `max_coupling_orders: table[str, int] = {}` +- `max_color_sectors: int | null` +- `reference_color_order: list[int] = []` +- `selected_color_sector_ids: list[int] = []` +- `selected_source_helicities: table[int, int] = {}` +- `flavor_blind: bool = false` +- `consider_charge_conjugation: bool = true` +- `only_unit_multiplier: bool = false` +- `analyze_process_groups: none | numerical = none` + +Model defaults and this explicit table are merged by name. Every model supplies +`all` for its declaration-ordered valid physical external states; an explicit +`all` entry overrides it. Broad products such as `p p > all all` may expand +combinatorially. + +The explicit color-sector, reference-order, and source-helicity controls are +developer-facing generation constraints. Ordinary users should leave them at +their defaults and use the runtime selectors below. + +The four process-group fields decide what `process-groups.json` records: +`flavor_blind` generates one representative per flavour-relabelling class and +records the others as unverified members; `analyze_process_groups = "numerical"` +merges generated processes whose matrix elements agree numerically up to a +rational factor, restricted to factor 1 by `only_unit_multiplier`. +`consider_charge_conjugation` also admits charge-conjugate relations in both +cases. The `group-processes` action reads `evaluation.artifact` and these +fields; its CLI defaults `analyze_process_groups` to `numerical`. + +## Color + +- `accuracy: lc | nlc | full = lc` +- `contraction: auto | direct | symmetric-group-fft = auto` +- `fft_basis: trace | adjoint = adjoint` +- `lc_flow_layout: topology-replay | all-flow-union = topology-replay` + +LC generation always includes complete physical flow coverage. Runtime flow +selectors are configured under `evaluation` or `benchmark`; internal sector, +topology, replay, and reference-order IDs are not configurable. + +`symmetric-group-fft` is an exact contracted-colour reducer for NLC/full +recurrence and on-the-fly execution. It is rejected for LC, compiled, and eager +execution when explicitly requested. The default `auto` tries FFT in the +supported lanes, falling back to direct for unsupported process plans; LC, +compiled/eager, and correlated generation use direct contraction and trace +basis. Explicit `direct` opts out and ignores `fft_basis`. + +`fft_basis = "adjoint"` requests adaptive exact-basis selection with +`contraction = "auto"` or `"symmetric-group-fft"`. Certified Yang–Mills trees and +certified single-insertion scalar HEFT (`HIG = 1`) use two-anchor DDM; quark processes +retain fundamental chains or their products, and uncertified adjoint domains +retain trace tensors. Singlet domains have one trivial tensor. This does not +add a JO primitive decomposition or assume identities for arbitrary models. +Forced adjoint FFT remains unsupported for correlated generation. + +For `n` gluons, DDM retains `(n-2)!` ordered amplitudes instead of `(n-1)!`; +its two-adjoint endpoint case is the single delta tensor. Saved +`fft_basis_selection` contains `requested`, `actual_basis`, `name`, `reason`, +`permutation_degree`, and `tensor_count`. `actual_basis` selects the metric +implementation (`trace` or `adjoint`); `name` distinguishes DDM, fundamental +chains/products, singlets, and trace tensors. Counts exclude open-line +traversal aliases and do not assert linear independence at fixed `Nc`. + +The CLI `--fft {trace,adjoint}` sets both contraction and basis request without +changing accuracy. Explicit FFT requests retain unsupported-plan errors; +`--color-contraction symmetric-group-fft` uses adjoint by default. +`ColorFFTBasis` and `ColorConfig` are public Python exports. Examples +select the adaptive adjoint option explicitly without implying that every +process has a smaller basis or faster runtime. + +## Generation + +- `output: path | null` +- `mode: error | append | replace = error` +- `workers: auto | int = auto` +- `emit_api_bundle: bool = true` + +### Generation Validation + +- `enabled: bool = true` +- `samples: int = 2` +- `seed: int = 12345` +- `relative_tolerance: float = 1e-12` +- `absolute_tolerance: float = 1e-300` +- `post_build_validation: bool = false` + +### Relation Discovery + +- `mode: off | diagnostic | certified-reuse = off` +- `precision_digits: int >= 80 = 96` +- `probe_count: int >= 2 = 4` +- `verification_probe_count: int >= 2 = 4` +- `relative_tolerance: float >= 0 = 1e-70` +- `absolute_tolerance: float >= 0 = 1e-80` +- `seed: int >= 0 = 1348026701` + +Candidate and independent verification probes are bounded and deterministic. +Artifact writing always validates the schema, declared payloads, references, +and digests. Optional post-build validation additionally re-opens the completed +artifact and compares native binary64 optimized and resolved evaluation. It is +off by default because that second runtime pass does not alter the artifact and +can be disproportionately expensive for large resolved axes; enable it with +`--post-build-validation` when an immediate runtime smoke is wanted. Its +configured samples are independent of the 96-digit relation-discovery +certification probes below. +Exact binary64 term-vector or `ExactComplexRational` schedule proofs remain the +preferred promotion path. When no exact structural proof exists, +`certified-reuse` may apply equal, opposite, or zero-current reuse only after +the independent current-value probes pass both configured tolerances and the +complete certification input and mapping are persisted for replay. One warning +is emitted per generated artifact when such proof-less mappings are applied. +Malformed, non-finite, unstable, or stale evidence fails closed. + +The feature is off by default: its high-precision probes evaluate the complete +current schedule several times at generation time, which dominates the build at +high multiplicity. `mode = "certified-reuse"`—or the public +`--numerical-current-reuse` flag—enables it for LC, NLC, and full colour, for +compiled, eager, and recurrence generation using built-in or prepared +external/UFO models, to search for additional equal, opposite, or zero-current +reuse. The compact on-the-fly source projection does not run this configurable +relation-discovery pass. `mode = "off"`—or `--no-numerical-current-reuse`— +restates the default and never changes numerical results. Direct vertex-kernel +equivalence remains model-certificate-owned. + +## Evaluator + +- `backend: jit | asm | cpp = jit` +- `execution_mode: compiled | eager | recurrence | on-the-fly = recurrence` +- `batch_size: int = 128` +- `output_chunk_size: int | null = 512` +- Stage-local parameter layout is mandatory and is not a public toggle. + +### Eager Execution + +- `point_tile_size: int = 1024` +- `workspace_mib: int = 256` (soft batching target) + +Eager mode requires a prepared model bundle before DAG construction. A +`built-in-sm` source resolves automatically to the wheel-owned portable +`built-in-sm-jit-o2` pack; other models and built-in C++/ASM execution require +an explicit prepared path. The prepared pack is +authoritative for backend and code-shaping optimization settings. The runtime +may reduce `point_tile_size` to approach the workspace target, but never +increases it. The target does not reject a process whose minimum workspace +is larger: at least one point is always allowed, with sufficient allocation. + +`.pyAmplicol-model.json` IR is architecture-independent. SymJIT application +storage-v3 prepared packs use optimization level 2 and are portable across the +supported `x86_64` and `aarch64` targets. Loading rebuilds executable code for +the receiving CPU. Prepared-pack compilation forces O2 and records any +requested-to-effective adjustment. C++ and ASM prepared packs remain +target-native. + +### Recurrence Execution + +- `point_tile_size: int = 1024` +- `workspace_mib: int = 256` (soft batching target) + +Recurrence is the global default. The runtime may reduce `point_tile_size` to +approach the recurrence workspace target, but never increases it. Like eager +execution, it allocates at least the workspace needed for one point, even when +that exceeds the target. A strict total-memory limit belongs to an external +watchdog, not to the generated process. Recurrence JIT +kernels use the same portable prepared O2 contract as eager execution. A +missing prepared pack fails closed; configuration resolution never falls back +to compiled execution. Cards that require process-local compiled DAGs must set +`execution_mode = "compiled"` explicitly. + +### On-The-Fly Execution + +On-the-fly execution requires a prepared model bundle and native `f64` +precision. It stores a compact process seed and constructs recurrence schedules +for the requested selector instead of materializing a process layout. LC does +not materialize topology replay or all-flow union: static inspection exposes +the physical helicity/color-flow census and the runtime supplies both selector +families. NLC and full colour expose one contracted component, accept helicity +selectors, and reject LC-flow selectors. + +`evaluator.optimization.cores` resolves to a positive requested +query-construction thread count for this mode. It is not a numerical-runtime +thread guarantee. LC numerical execution sizes its warmed family to the +requested batch capacity. Contracted NLC/full execution applies the +authenticated recurrence point tile. There is no on-the-fly-specific tile or +workspace configuration section. + +### Evaluator Optimization + +- `horner_iterations: int = 10` +- `cpe_iterations: int | null = null` +- `cores: auto | int = auto` +- `max_horner_variables: int = 1000` +- `max_common_pair_cache_entries: int = 5000000` +- `max_common_pair_distance: int = 1000` +- `collect_factors: auto | bool = auto` + +For on-the-fly execution, only `cores` has a mode-specific runtime meaning; +the prepared kernel pack remains authoritative for code-shaping optimization. + +### JIT + +- `optimization_level: 0 | 1 | 2 | 3 = 2` +- `compress: AutoBool = "auto"` (compiled JIT O2 resolves to `true`; all other + modes/levels resolve to `false`; explicit booleans override the default) + +JIT artifacts embed direct SymJIT applications. The defaults above apply to +process-local compiled DAG evaluators. Prepared JIT kernel packs used by eager, +recurrence, and on-the-fly execution force optimization level 2 to preserve +their cross-architecture storage contract. + +Compression can reduce generated code size but add runtime overhead. Auto +selection remains in requested configuration; effective configuration records +the resolved boolean. Explicit settings remain authoritative for newly +compiled kernels; existing prepared packs retain their recorded setting. + +### C++ + +- `optimization: str = "O3"` +- `compiler: str | null` +- `native_arch: bool = false` +- `extra_flags: list[str] = []` + +Portable C++ generation is the default. Setting `native_arch = true` opts into +host-native code and records Rusticol's canonical, sorted runtime CPU-feature set +as an artifact requirement. Loaders reject that artifact before reading evaluator +state on a target without every declared feature. Additional flags are restricted +to the documented non-ISA allowlist; arbitrary `-march`, `-mcpu`, `-m*`, or target +flags are rejected because they could introduce unrecorded requirements. +Schema-v3 evaluator payload portability is currently defined for macOS arm64, +macOS x86_64, and glibc Linux x86_64. Other targets are rejected explicitly. + +## Evaluation + +- `artifact: path | null` +- `process: str | null` +- `precision: int = 16` +- `resolved: bool = false` +- `helicity_ids: list[str] = []` +- `color_flow_ids: list[str] = []` +- `model_parameters: path | null` +- `momenta: path | null` + +## Benchmark + +- `target_runtime: float = 10.0` +- `batch_size: int = 128` +- `precision: int = 16` +- `warmup_runs: int = 2` +- `minimum_samples: int = 5` +- `helicity_ids: list[str] = []` +- `color_flow_ids: list[str] = []` + +For LC profiling, empty benchmark selector lists resolve at runtime to the +generated layout's optimized workload: one physical flow and the helicity sum +for `topology-replay`, or all physical flows and one computed helicity for +`all-flow-union`. Explicit subsets and selected-axis lists are preserved; a +complete summed-axis list is normalized to equivalent omission. A valid +non-hot shape emits at most one pre-loop warning per loaded process. Evaluation +selector defaults remain the complete matrix element. + +## Output And Symbolica + +- `output.format: human | json = human` for typed API/config provenance. The + public CLI normalizes this to human tables unless `--json` is explicitly + present. +- `output.color: auto | always | never = auto` +- `output.progress: auto | tty | log | off = auto` +- `output.log_level: debug | info | warning | error = info` +- `symbolica.suggest_license: bool = true` diff --git a/pyamplicol/docs/development/MODEL_ASSET_PROVENANCE.md b/pyamplicol/docs/development/MODEL_ASSET_PROVENANCE.md new file mode 100644 index 0000000000..eeb46ca1dc --- /dev/null +++ b/pyamplicol/docs/development/MODEL_ASSET_PROVENANCE.md @@ -0,0 +1,39 @@ +--- +title: "Model Asset Provenance" +nav_order: 6 +parent: "Development Documentation" +--- + +# Model Asset Provenance + +Package-owned model assets live in `src/pyamplicol/assets/models`. Their exact +upstream revisions and file checksums are recorded next to the payloads in +`PROVENANCE.toml` and `MANIFEST.sha256`; no duplicate top-level asset tree is +used. + +| Asset | Origin | Intended notice | Port status | +|---|---|---|---| +| Built-in SM | Original pyAmpliCol model implementation | 0BSD | Eligible; code-backed implementation is ported separately | +| SM UFO 1.3 | FeynRules UFO by N. Christensen and C. Duhr, distributed through GammaLoop | Preserve authorship, UFO/FeynRules provenance, and all upstream notices | Extracted and manifest-covered | +| Scalar UFO | Model by Valentin Hirschi, generated with FeynRules 1.7.69 and distributed through GammaLoop | 0BSD for original model material; preserve generated UFO helper attribution | Extracted with deterministic static defaults | +| Scalar-gravity UFO | Model by Valentin Hirschi, generated with FeynRules 1.7.69 and distributed through GammaLoop | 0BSD for original model material; preserve generated UFO helper attribution | Extracted with deterministic static defaults | +| Serialized JSON | Deterministic `ufo-model-loader` serialization of the corresponding UFO | Same terms and provenance as source UFO; loader is MIT | Regenerated and manifest-covered | + +The SM UFO contains `build_restrict.py` from MadGraph5_aMC@NLO. Its license is +included as `licenses/MadGraph5_aMCatNLO.txt`; model, GammaLoop, and loader +provenance are recorded separately so generated JSON does not obscure the +terms of its source UFO. + +The completed extraction: + +1. Inventories 66 payload files plus `PROVENANCE.toml` from the pinned source. +2. Excludes caches, interpreter output, symlinks, and host paths. +3. Freezes scalar model environment switches to their historical defaults. +4. Regenerates JSON with `ufo-model-loader` 0.1.8 and records both source and + package hashes. +5. Records each deterministic transformation in + `model-assets/TRANSFORMATIONS.md` and verifies all package payloads through + `MANIFEST.sha256`. + +Numerical UFO/JSON/compiled-model parity remains an integration gate owned by +the model compiler rather than an asset-copying claim. diff --git a/pyamplicol/docs/development/ON_THE_FLY_MODE_ARCHITECTURE.md b/pyamplicol/docs/development/ON_THE_FLY_MODE_ARCHITECTURE.md new file mode 100644 index 0000000000..7a3d90e43b --- /dev/null +++ b/pyamplicol/docs/development/ON_THE_FLY_MODE_ARCHITECTURE.md @@ -0,0 +1,334 @@ +--- +title: "On-the-fly mode architecture" +nav_order: 8 +parent: "Development Documentation" +--- + + +# On-the-fly Mode Architecture and Implementation Record + +## Status and scope + +This document records the architecture selected from the on-the-fly research +prototypes based on source revision +`a08feed4aacf39c00dfedaeedd3a82a9666f1565` and the production implementation +that followed. The prototype commits are historical evidence, not code that +was merged wholesale. + +On-the-fly execution is a distinct artifact and native runtime lane. Python +produces a compact process source projection; Rust authenticates it against the +prepared template/executor catalogs, builds and encodes the binary +`OnTheFlyProcessSeedV1`, and decodes and validates that seed at load. The lane +never constructs a `DirectRecurrencePlan`. + +For LC, the native runtime accepts one arbitrary requested Cartesian family of +helicity ordinals times flow ordinals, subject to checked native `usize` sizes +and available resources. That family may range from one selector pair to the +complete all-helicity times all-flow product. Selected-flow/helicity-sum and +all-flow/single-helicity are the two campaign workloads; they are not +capability limits. NLC/full use an authenticated recurrence-v3 colour metric, +evaluate only canonical structural-owner amplitudes, and expose one public +`color:contracted` component rather than a selectable colour-flow axis. + +Cold preparation constructs transient selector-local traces and groups their +operations into one executable family. Only the last successfully evaluated +family is retained. The mode is integrated with generation, artifact loading +and inspection, selected/resolved/total evaluation, `Runtime.clear()`, +profiling, the CLI, explicit one-point warm-up progress, and the native SDKs. +It is native-f64-only. Contracted NLC/full correctness is covered through the +complete n<=4 acceptance catalog; their rapidly growing cold selector product +is not claimed to be practical at high multiplicity. + +Numerical comparisons now include independent full-colour MadGraph targets at +precision 200 and strict scale-only relative tolerance 1e-10. OTF full colour +is replayed at native f64 against those frozen targets; OTF NLC is replayed +against recurrence-p200 targets captured only after the full-colour lane was +MadGraph-gated. Legacy-only evidence is not treated as independent authority. + +## Prototype evidence and dispositions + +| Research commit | Evidence | Production disposition | +|---|---|---| +| `ce8037749c97ae04bae6cec42161991754ee2078` | Algorithmic helicity and LC-flow codecs addressed one selector without cardinality-sized tables. | Retain the algorithms and fixtures. Production ordinals and Cartesian products are bounded by native `usize`. | +| `4c9a1d6cedb082140c5b492171bd2fe7873b0f98` | A synthetic plain 8-byte VM was bitwise equal to its interpreter and used zero warm allocations; generic superinstructions were slower. | Historical only. Production executes grouped family rows and has no 8-byte VM. | +| `84f2246f0de8f5d9e598eb76f2ff8e85bd0dbd36` | Ordered prefixes carry distinct color-ordered amplitudes. LC contraction was a diagonal second moment in the checked domains; coherent summation was wrong. | Reject shared-amplitude DP. Retain exact LC selector construction and diagonal reduction. | +| `2312b1137c7e2f3d6bdf9064a902592c16bac58c` | Candidate A built a selected GenericDAG query without global color/projection/axis materialization. Retained n=5 cases agreed with a separate p32 recurrence path. | Port the selector-local construction to Rust. Treat p32 as historical regression evidence, not independent physics authority. | +| `caa500dd7fa67a7e496238e63c6b823aab4b8dd6` | A demand-first memo used synthetic kernels and the wrong ownership boundary. | Reject the evaluator; retain only useful reverse-index ideas. | +| `6b4f011d90d5262807d8316d73f28b106ea31fb9` | Candidate C explored a VM, byte-budgeted LRU, and ephemeral oversize plans. | Historical only. Production uses transactional `last-family-only` retention. | +| `dc171ac357c2817465f20baf3ff0efa91086b53a` | Query-sized structural construction reached n=10 under the 30 GiB watchdog without building the full selector product. | Retain as historical scaling evidence, not a numerical or completion gate. | +| `cf8de3820152be262bafc6f93e5af9a6f423b271` | Compact seeds retained crossed sources, pairing classes, coupling policy, source slots, and parity. | Adopt the seed concept; authenticate and build the production seed in Rust. | +| `e71161cb656ed5eeaa69d18221735b242c60554d` | A prepared pool served compact plans, but the prototype used incomplete traces and hard-coded executor IDs. | Adopt the plan-independent pool seam; use authenticated semantic lookup and a process/lane-owned source binding. | + +The production implementation resolves the prototype-era gaps in complete LC +family construction, semantic executor lookup, source-domain ownership, and +the binary seed/load boundary. The VM/LRU proposal and its byte-accounting +gates were not adopted. + +## Current production contract + +### Binary seed and execution manifest + +The Python projection contains process-owned source facts and policy inputs, +but no recurrence DAG, color plan, public-flow table, or direct plan. Rust +cross-checks it with the validated recurrence-template catalog, prepared +direct-executor catalog, and prepared-kernel-pack identity before producing the +PACBIN seed member. + +`OnTheFlyProcessSeedV1` stores exactly these classes of data: + +- process, compiled-model, recurrence-template-catalog, + prepared-kernel-pack, and direct-template-catalog digests; +- the normalization semantic digest and convention, with seed construction + requiring the raw-amplitude normalization factor to be exact one; +- source anchors: source/public slots, initial-state flag, LC color role, + statistics, optional pairing-contract digest, and all concrete source + states; +- per-state public/source helicities, source/current template identities and + semantic digests, momentum sign, exact crossing phase, spin/chirality, + flavour and quantum flow, color-seed proof, wavefunction family, + particle/antiparticle/self-conjugate orientation, and optional prepared mass + slot; +- the public-to-construction external permutation; +- coupling-order policy, positive hierarchy weights, and optional explicit + user hard caps; and +- species pairing classes with balanced fundamental/antifundamental endpoints, + plus the seed's semantic digest. + +The seed does not store enumerated public axes, selector census, reference +color word, reflection policy, executable rows, or the runtime normalization +factors. Those facts have different owners: + +- `execution.selector_policy` stores complete LC coverage, the optional + reference color word, whether trace reflections are folded, and the + authenticated physical helicity/flow census; +- `execution.runtime_metadata.normalization` stores LC color, averaging, + identical-particle, global-coupling, and coupling-policy runtime factors; + the runtime metadata also carries parameters, external legs, masses, and the + decoded seed identity; and +- the runtime container manifest owns the canonical PACBIN and seed-member + paths. + +The selector adapter derives axes from the decoded seed plus manifest policy. +The normal load/evaluate/profile path does not materialize dense public axis +records. Explicit `.physics` access may populate a separate metadata cache; +that does not change the seed or native load contract. + +There is no fallback to ordinary recurrence construction. In particular, OTF +must not call the global color-plan/projection/recurrence builders, +`build_replay_targets`, `finish_program`, or any `DirectRecurrencePlan` +constructor or loader. + +### Coupling policy and first cold warm-up + +The seed stores policy inputs, not always the effective runtime limits: + +- `Explicit` uses the seed's optional hard-cap vector directly and performs no + minimal-policy topology sweep. +- `Minimal` performs a process-global cold topology sweep. The sweep erases + public helicity labels while retaining source support, state/spin, flavour + and quantum ancestry, exact LC color, and fermion-pairing lineage. Explicit + hard caps constrain that sweep. The hierarchy-minimal viable total orders + define the effective per-order envelope used by every subsequent query until + the process preparation is cleared. + +Grammar preparation and minimal-policy resolution happen on the first genuinely +cold family preparation, not at artifact load or seed generation. Their result +is process-wide and reused by subsequent families. `Runtime.clear()` removes +the prepared grammar, effective policy, timing, families, and family semantic +bindings, so the next cold evaluation recomputes this warm-up. Generation and +warm-up therefore remain separately reportable timings. + +The cold sweep indexes parent currents by source-support size and visits only +size-complementary pairs. Final closure additionally requires a singleton +anchor. These filters preserve the original candidate order and exact colour, +flavour and fermion-pairing checks; they avoid scanning pairs that cannot +possibly form the requested current. + +### Selector family, transient traces, and retained rows + +The public selector adapter accepts arbitrary subsets of the helicity and flow +axes. It checks the requested Cartesian product in `usize`, collects the +selected indices/IDs and native-internal `DecodedLcQueryV1` requests during +cold preparation, and preserves public order. Requesting both complete axes is +supported by the contract, although it can be expensive. + +For each nonzero query, production constructs an +`OnTheFlyStructuralTraceV1`. This is the transient cold IR: it carries +query-local current/color identities, source/contribution/finalization/closure +operations, momentum forms, exact factors, pairing owners, workspace layout, +and structural proof. Prepared-executor parent order is authenticated and bound +before lowering. + +The family builder consumes those traces, unions repeated currents, groups +executor rows, and records amplitude destinations. Transient traces are then +dropped; retained row groups keep the semantic keys and member bindings needed +by the executor, while the retained-state census reports zero query-local +traces. Structural-zero requests retain their public placement but need no +executable destination. + +Family replacement is transactional. A candidate remains pending until its +first successful evaluation; a failed candidate is discarded without evicting +the last successful family. Successful promotion clears the old family and +retains exactly one selection and family. An executable family has its row +handle, destination map, and matching semantic bindings; a structural-zero +family has none of those executable members. `Runtime.clear()` leaves all +family-state counts at zero. + +Compactness means that the artifact/load boundary has no dense recurrence plan +and that retained executable state belongs only to the requested Cartesian +family. It does not promise memory independent of that family's size: an +all-helicity times all-flow request may itself be large. + +Warmed momentum preparation traverses contiguous phase-space-point planes. +Source bounds are checked once when the immutable family workspace is prepared; +each point retains the original momentum-term summation order. Only active +lanes are refreshed, with no warmed reallocation. + +### Completed-cache snapshots + +`NativeRuntime::save(path)` streams the completed retained OTF structure to a +snapshot; `load_cache(path)` restores it against a matching loaded process. +The Python facade and C/C++/Fortran/Rust SDKs expose the same operations and +format. This is independent of explicit warm-up: an ordinary successful +evaluation also leaves saveable state. +Small usage examples are maintained in +[Runtime and Selectors](../user/runtime-and-selectors.md#saving-an-otf-warm-cache) +and [Native APIs](../user/native-apis.md#saving-and-restoring-an-otf-cache). + +The snapshot retains process preparation, grouped current rows, exact factors, +momentum forms, semantic executor references, and the public-selection and +amplitude-reduction mappings. Restoring the resolved coupling policy avoids +repeating its process-global topology sweep. Prepared kernels are rebound +from the original process output, while numeric workspaces are created afresh +and the receiving runtime's parameters remain in effect. No machine addresses +or evaluated currents are serialized. Successful restoration replaces the +previous cache transactionally; rejection leaves it unchanged. + +Snapshot compatibility is tied to the existing process/seed/prepared-kernel +identities and a snapshot format version. A saved family follows the same +warmed execution path as one constructed in memory. The last-family retention +policy is unchanged: this is neither a history of evicted selections nor a +checkpoint of a partially constructed family. + +### Prepared executors, source binding, and checked resources + +The prepared executor pool is model/catalog-owned. At artifact load, source +domains are derived once from the process seed and template catalog and bound +to that pool. `OnTheFlySourceDomainBinding` is process/lane-owned and +query-independent; it survives family replacement and `Runtime.clear()`. +Family-specific semantic executor mappings are resolved and committed +transactionally beside the grouped rows. + +Ownership order ensures grouped rows and resolved handles disappear before the +process source binding, which in turn disappears before the model-level pool. +Missing, ambiguous, or digest-mismatched semantic mappings fail closed. + +Claims about checked resources are intentionally narrow. Selector counts, +Cartesian products, row/workspace offsets, and point/destination products use +checked size arithmetic. Specific large growth points use fallible reservation, +including seed/source projections, selector metadata caches, selected-family +and trace vectors, retained-family slots, and point/amplitude scratch buffers. +This is not a blanket claim that every Rust allocation is fallible. + +All expansion-prone development commands run under the 30 GiB process-tree +watchdog. + +### Public surface and limitations + +OTF participates in the normal Python generation/load/runtime APIs, compact +inspection, explicit one-point warm-up, selected/resolved/total evaluation, +lifecycle clearing, benchmark/profile reporting, CLI workflows, and native +Rust/C/C++/Fortran APIs. The profile path reports generation separately from +generation plus first warm-up, and it records the native retained-family +census. + +The runtime rejects non-f64 precision before expensive selector/physics work. +LC exposes flow selection. Contracted NLC/full reject flow selectors, apply the +authenticated colour factor exactly once, and reuse the same structural +query-family executor. External UFO-SM evidence includes the complete bounded +n<=4 acceptance catalog; this remains a bounded claim, not universal UFO +correctness. + +## Original staged plan and dispositions (historical) + +The original plan separated a structural interpreter, an optional 8-byte VM, +a bounded LRU, selector streaming, a compact artifact, and final public/UFO +integration into successive gates. Its useful dispositions are: + +- the compact seed, transient structural trace, semantic lookup, distinct + artifact/lane, and LC diagonal reduction were adopted; +- the 8-byte VM, general LRU, ephemeral oversize-plan policy, and exact cache + byte-accounting gates were not adopted; +- the production family API permits any requested helicity×flow Cartesian + product and may collect its requests during cold preparation, so strict + one-selector-at-a-time streaming is not a current gate; +- the old requirement for both compiled and recurrence authorities was + superseded by one designated legacy authority per cell; and +- broad UFO, p32, and exact n=10 gates were superseded by the bounded current + evidence and completion scope below. + +The deleted prototype-era file map and detailed stage gates described planned +filenames and sequencing, not the implemented contract. The source tree and +the sections above are authoritative. + +## Validation evidence and completion scope + +Focused tests cover seed/manifest validation, selector order and checked +products, source and semantic binding, transient trace construction, grouped +family rows, structural zeros, transactional failure/promotion, parameter +updates, `Runtime.clear()`, aliases/permutations, identical-fermion signs, +multi-line pairing, LC diagonal reduction, compact inspection, CLI/profile +workflows, and the generated C API. Built-in SM and the external JSON +scalar/contact lifecycle have both been exercised. + +The live fail-fast n<=4 process-table campaign completed from native candidate +`238a4ad`: all 66 OTF cells passed their matching recurrence authority, and all +66 legacy AmpliCol timing baselines were available. The campaign produced 132 +fresh successful OTF/recurrence results, reused 66 independently rehashed +AmpliCol results, and observed a maximum conditioned OTF/recurrence residual of +`3.93e-15`. This establishes legacy LC parity, not independent physics +correctness. + +The later contracted-colour acceptance suite covers the complete bounded +n<=4 catalog. OTF full colour is replayed against frozen independent MadGraph +targets produced at precision 200; OTF NLC is replayed against recurrence-p200 +targets admitted only after the matching full-colour lane passed its MadGraph +gate. This is the current bounded NLC/full correctness claim. + +Retained selected-flow/helicity-sum high-multiplicity comparisons have passed: + +- n=5: process IDs 7, 8, 11, 13, and 15 against legacy AmpliCol; +- n=6: process IDs 7, 8, 11, 13, and 15 against legacy AmpliCol, plus + four-quark-line process ID 14 against recurrence; and +- n=7: process IDs 7, 11, 13, and 15 against legacy AmpliCol, plus + four-quark-line process ID 14 against recurrence. + +The process-ID-8 pure-gluon n=7 probe reached its one-hour timeout without a +completed report. It is a bounded engineering probe, not a parity failure or +completion gate. + +After required parity, scaling exploration and performance investigation each +have a separate one-hour wall-clock cap. Scaling may attempt OTF-only n=8/n=9 +selected-flow/helicity-sum and bounded arbitrary-family feasibility under the +30 GiB watchdog; those probes require no numerical authority. Performance uses +batch size 128 and implements only obvious, generic, low-risk improvements +found within the hour. The former 2x AmpliCol aspiration remains context, not a +completion gate. + +## Original LC milestone definition of complete + +The LC OTF milestone is complete when: + +- the final live n<=4 campaign and designated bounded parity cells pass; +- the relevant source-tree and installed-package tests, API/CLI/profile/native + lifecycle checks, and generated C API test pass; +- the separate one-hour scaling and performance investigations conclude, + whether or not they find an improvement; and +- the intentional dirty worktree is preserved and the implementation is + committed and pushed. + +That original LC-only milestone did not require a p32 comparison, n=10 +reproduction, 2x runtime ratio, NLC/full-color OTF, or repair of shared legacy +physics bugs. The production scope has since added the bounded n<=4 contracted +NLC/full capability and independent full-colour MadGraph gate described above; +it still makes no high-multiplicity practicality claim for those families. diff --git a/pyamplicol/docs/development/PACKAGING_CONTRACT.md b/pyamplicol/docs/development/PACKAGING_CONTRACT.md new file mode 100644 index 0000000000..7896f2c1e2 --- /dev/null +++ b/pyamplicol/docs/development/PACKAGING_CONTRACT.md @@ -0,0 +1,218 @@ +--- +title: "Packaging Contract" +nav_order: 4 +parent: "Development Documentation" +--- + +# Packaging And Release Contract + +This document is normative for standalone pyAmpliCol package builds and +publication. + +## Canonical Build + +- Distribution: `pyamplicol==0.4.1`, Python 3.11+, license `0BSD`. +- Backend: the in-tree PEP 517 wrapper delegates to pinned Maturin. +- Python extension: `pyamplicol._rusticol`, built from `rusticol-python` with + `abi3-py311`. +- Native SDK: `rusticol-capi` links the Python-independent Rusticol core and + must contain no PyO3, NumPy, Python symbols, or Python linker dependency. +- Version source: root Cargo workspace metadata. Installed Python obtains the + version from distribution metadata. +- Rust resolution: the committed `Cargo.lock` is authoritative. + +Release builds use a temporary allowlisted source overlay and an external +`CARGO_TARGET_DIR`. Opted-in contributor builds use a persistent +workspace-local cache under `.artifacts/dev-install`; neither mode rewrites +the source inputs. + +## Native Build Identity + +The native identity hashes the Rust/Cargo source closure, the pinned Rust and +Maturin toolchains, effective native Maturin settings, and the declarative +`tool.pyamplicol.native-build` contract. Python orchestration, distribution +metadata, documentation, reports, staging paths, provenance-only locks, and an +explicit inventory of wholly test-only Rust files are excluded. Candidate +versions use the first twelve digits of this same digest; there is no parallel +candidate fingerprint. + +Rust files that mix production code with inline `#[cfg(test)]` modules remain +file-granular inputs. Avoiding that conservative invalidation requires moving +the tests into an explicitly audited test-only module; the identity code does +not attempt to parse or rewrite Rust source. + +## Dependency Modes + +Release mode reads `dependencies/release-lock.toml` and accepts exact published +package/crate versions, including Symbolica 3.0.0 and SymJIT 2.26.4. Canonical +`Cargo.lock` records standard crates.io resolution and checksums without source +patches. Git, path, editable, floating, and candidate dependencies are forbidden. + +Candidate mode is available only from a full source checkout. It reads the +repository-only contributor contract and produces explicitly non-publishable +artifacts. Contributor dependency setup and local candidate inputs are excluded +from release wheels and source distributions. + +Release and candidate Cargo resolution are physically separate. A release +command must fail closed when exact published inputs are unavailable; it must +never fall back to contributor state. + +## Release Prepared-Model Production + +Inside MadGraph7 neither the packs nor their metadata are tracked in Git. Each +MadGraph7 release attaches one archive, `pyamplicol-prepared-models.tar.gz`, +holding the metadata of every architecture pair and each distinct bundle once. +`tools/release/prepared_models_archive.py fetch` downloads the archive of the +matching release, else of the latest one, and writes the files into both +directories below; `unpack` does the same for a local archive, `pack` creates +one, and `regenerate` repeats the production described below on the current +host. The installer and CI validate fetched packs against the source tree and +regenerate them when they are stale. The release workflow attaches the archive +its wheels were built with. + +The canonical package directory +`src/pyamplicol/assets/prepared_models` remains candidate-owned in a source +checkout. Contributor overlays and wheels use those files byte-for-byte and +never consult release identity. + +Release-only source inputs live outside the Python package at +`release_assets/prepared_models`. A normal release build from a Git checkout +requires exactly the README and both architecture pairs in that directory. The +build overlay replaces the four canonical candidate payload files with the +release pairs, removes the auxiliary store, and then validates the projected +package assets against the active release lock. Missing, mixed, candidate, or +stale release packs are build errors; the PEP 517 interface has no release +bypass. The auxiliary store is deliberately outside the native-build identity, +so producing its packs cannot change the bootstrap runtime that produced them. + +When those source-owned packs must be regenerated, run +`tools/release/prepared_models_archive.py regenerate` (CI does this through the +`pyamplicol_prepared_models` action). Its dedicated backend entry +point creates a temporary release-version bootstrap wheel with both package +payloads and the auxiliary store removed. That wheel is explicitly marked +`publishable: false` and +`release_prepared_model_bootstrap: true`, requires an exact clean Git revision, +and is rejected by the ordinary release artifact audit. It exists only long +enough to run the architecture-local producer. + +The producer writes `mode: release`, an explicit null candidate fingerprint, +the producing package version, and a source identity derived only from +`dependencies/release-lock.toml` and canonical `Cargo.lock`. Producer version +is provenance: a pack may be reused by a later patch release when its schema, +compiler, model-source, dependency, and application ABI identities all match. +The producer writes each pair below `release_assets/prepared_models`; the packs +are portable, so the pair produced on one host is installed for both +architectures. The `pyamplicol-eager-portability.yml` workflow checks that +packs produced on one architecture work on the others. + +## Wheel Contents + +The wheel contains: + +- the typed Python package and one native extension; +- model assets with their provenance and content manifest; +- packaged examples, an empty profiling-campaign workspace, and + Python/C11/C++17/Fortran 2008/Rust 2021 API templates; +- direct-JIT f64 self-test data; +- license and third-party notice files; +- the target-specific Rusticol C ABI archive, C/C++ headers, Fortran module + source, SDK configuration, and relative link metadata. + +The installed SDK layout is: + +```text +pyamplicol/_sdk/ + include/rusticol.h + include/rusticol.hpp + fortran/rusticol.f90 + rust/rusticol.rs + lib/librusticol_capi.a + config.py + metadata.json + link.json +``` + +The backend captures one static archive from Cargo JSON messages, validates the +complete C ABI, rejects non-relocatable link inputs, scans for Python-family +symbols, and records only target-appropriate system libraries/frameworks. +`rusticol-config` exposes typed C/C++ flags, Fortran source, Rust linker flags, +and JSON metadata without a machine-specific build path. + +Wheel audits permit only the `pyamplicol/` package, one `.dist-info/` directory, +and an optional platform repair directory. Standard wheel `RECORD` protects +installed-file integrity. Model and process-artifact digests remain scoped to +the package features that verify those payloads against accidental mutation. + +## Source Distribution + +One retained source distribution is the source of all release wheels. It must +build with `python -m pip install .` using release-locked published +packages/crates and contain: + +- Python/Rust/build sources and lockfiles; +- schemas, tests, examples, user documentation, and release tooling; +- the README, licenses, and third-party notices. + +The canonical package paths in the retained sdist contain only the validated +release pairs. The auxiliary `release_assets` store and the candidate package +payloads are absent, so building a wheel from that sdist validates and reuses +the canonical release payload without a second projection. + +The sdist excludes dependency checkouts, contributor setup, local candidate +inputs, the auxiliary release source store, generated process outputs, build +products, caches, local environments, development histories, raw campaign +data, and rendered report PDFs. + +`just sdist`, `just wheel-from-sdist`, and the PEP 517 hooks exercise the same +backend. The sdist audit checks required members and forbidden source-checkout +inputs before a wheel is retained. + +## Validation Matrix + +Release targets are: + +- macOS 11 arm64, `cp311-abi3`; +- macOS 11 x86_64, `cp311-abi3`; +- manylinux 2.28 x86_64, `cp311-abi3`. + +Each wheel receives the full installed-package deployment on CPython 3.11, +without Rust in the consumer environment. That deployment includes: + +- `twine check` and platform/native-dependency audits; +- import, CLI, direct-JIT f64 self-test, and packaged-example checks; +- Python total/resolved runtime behavior with Symbolica imports blocked; +- C11, C++17, Fortran 2008, and Rust 2021 f64 driver compilation/execution; +- model-parameter card and direct UFO-parameter override behavior. + +CPython 3.14 receives a focused abi3 installation, import, metadata, and +direct-runtime smoke test; it does not duplicate generation or the +multilanguage SDK deployment. The independent Fortran physics oracle runs as a +source-level release check. Performance campaigns are intentionally separate +from release CI. + +Local entry points are: + +```console +just source-gate +just test-deployment +just release-artifacts +just publish-dry-run +``` + +`publish-dry-run` validates ordinary package files and prints the upload command +without uploading anything. + +## Publishing + +The release inventory is exactly one source distribution and one wheel per +supported target. Publication never rebuilds these files. + +The validated-artifact workflow is manually dispatched with read-only source +permissions. A separate manual publishing workflow downloads the successful +validated inventory, verifies the expected platforms and non-candidate version, +and publishes through a protected TestPyPI or PyPI environment using OIDC +Trusted Publishing. Only that final job receives `id-token: write`. + +Published releases reference one successful exact-source validated-artifact +workflow run. Uploading its retained files is a separate manual action and +never rebuilds them. diff --git a/pyamplicol/docs/development/PHYSICS_EXTRACTION_CONTRACT.md b/pyamplicol/docs/development/PHYSICS_EXTRACTION_CONTRACT.md new file mode 100644 index 0000000000..e74f7a4e20 --- /dev/null +++ b/pyamplicol/docs/development/PHYSICS_EXTRACTION_CONTRACT.md @@ -0,0 +1,175 @@ +--- +title: "Physics Extraction Contract" +nav_order: 5 +parent: "Development Documentation" +--- + +# Physics Extraction Contract + +This document records the first independent physics audit. It is normative +for the behavior-preserving extraction from source revision +`643bc6f99d7b2249af0a85204768df243e612411`. + +## Port Boundary + +Freeze numerical behavior before reorganizing: + +- color contraction and planning; +- process IR, enumeration, and all-outgoing crossing; +- current identity and DAG construction; +- lowering, phase-space generation, stage compilation, and Symbolica + evaluator construction; +- built-in model parameters and kernels; +- UFO model IR, tensor projection, contact decomposition, and propagators. + +Rewrite the CLI, artifact writer, loader boundary, logging, reporting, model +asset access, generated API bundle, and Python runtime facade around those +locked contracts. Validation-only Fortran adapters and reference kernels stay +under developer tooling and are excluded from distributions. + +The extraction initially preserves both Standard Model implementations. The +built-in path still uses its hard-coded production model, while UFO/JSON uses +the compiled external-model path. Converging those implementations is a +later, separately validated refactor. + +## Required Invariants + +- UFO and serialized JSON pass through the same `ufo-model-loader` model and + produce identical restricted, wrapped-index, canonical, and compiled model + representations. +- External filtering, particle/antiparticle crossing, external order, + helicity bases, Weyl projection, propagators, spin-2 polarizations, and + n-ary contact decomposition are preserved. +- A contact coupling is applied exactly once. +- `CurrentIndex` retains every physics identity field. Optimized and + unoptimized DAGs agree point by point. +- LC flow, reflection, helicity, averaging, coupling, and identical-particle + weights are each applied exactly once. +- NLC/full performs one coherent sparse color contraction, applies the + off-diagonal factor two exactly once, and never reapplies an LC color + factor. +- Inconsistent helicity weights in a contraction group are errors. They must + never be silently averaged. +- Unsupported colored representations are errors. They must never be treated + as singlets. +- Final-state permutation reuse is validated against fully materialized + direct artifacts. It is not described as generic crossing reuse. +- Resolved evaluation executes one DAG. LC exposes physical + `(point, helicity, flow)` values; NLC/full exposes one contracted color + component per helicity. Structural zeros remain exact and resolved members + sum to the compatibility total. +- A color-singlet LC axis may have an empty ordered color word. Every nonempty + LC word contains exactly once every external label whose model-derived color + representation is non-singlet. + +## Model Determinism + +The historical scalar UFO modules read `UFO_SCALARS_MODEL_*` and +`UFO_GRAVITY_MODEL_*` environment variables without including them in the +compiled-model cache key. Standalone model loading must start from a sanitized +environment. Any retained model option becomes a typed input included in +canonical content, provenance, and cache identity. The historical undefined +`N_POINT_INTERACTIONS` gravity branch must be removed or corrected before the +asset is shipped. + +## Reference Ladder + +The first frozen fixture-v2 bundle contains: + +- `d d~ > z`, `d d~ > z g`, and `d d~ > z g g` in LC/NLC/full; +- scalar contact `scalar_0 scalar_0 > scalar_0 scalar_0`; +- spin-2 `scalar_0 scalar_0 > graviton graviton`. + +The built-in-SM `d d~ > z` and `d d~ > z g` rows are also replayed through +the pinned Fortran AmpliCol color probe in LC, NLC, and full color. This +independent check covers the summed matrix element and every recorded nonzero +physical-helicity component. The probe is developer-only, links no LHAPDF, +and is never imported or packaged by the installed distribution. + +The next declarative capture revision first completes all low-multiplicity +classes before retaining any higher-multiplicity output. It adds `u d~ > w+`, +`d d~ > e- e+`, mixed- and same-flavour two-quark-line scattering, +`g g > g g`, and `g g > t t~` in LC/NLC/full. Built-in masses and exact +Fortran process-row, matching-row, and colour-order counts are independent +capture contracts. The massive top case includes a high-precision threshold +point. These cases become release evidence only after a clean candidate built +from the committed recipe regenerates all fixture and evidence documents. + +Before the model/generation milestone is accepted the ladder then expands to: + +- process-set `d d~ > z g` and `d d~ > z g g`; +- `d d~ > t t~` and `g g > t t~ g` at the next multiplicity; +- identical-vector/Higgs interference; +- scalar contacts with two, three, and five final scalars; +- scalar propagation with mass, width, and coupling variations; +- two and three final gravitons; +- reordered final states for permutation reuse. + +Use at least three distinct deterministic generic points and one quantified +unstable high-precision point for each substantive row. Each point records +the external masses used for its on-shell check, and capture validates both +four-momentum conservation and mass shells. Cross-language agreement checks +one Rust core; release physics additionally requires pinned Fortran, analytic +scalar, exact-color, and independent spin-2 oracles. + +Fixture schema v2 must retain decimal-string expectations per point rather +than round-tripping high-precision values through binary64. It records complete +helicity and color axes, every structural zero, topology and coverage, the +complete reduction-group partition, normalization inputs, model and dependency +provenance, and the independent oracle used for each point. Arithmetic +precision, serialization width, and independently certified digits are +distinct metadata; stored values are never claimed beyond the supporting +oracle's certified accuracy. Fast gates may select one generic point from the +complete fixture, but substantive tracked cases cannot weaken their declared +physical axes. Each case hash covers its model dependencies, exact points, +process, and the complete transitive source chain of any process alias. +Compiled-model identity hashes the canonical semantic payload and excludes only +conversion-duration and phase-timing diagnostics, which naturally differ +between otherwise identical color-accuracy builds. + +Completeness is not inferred from the emitted axes. Each process records the +external UFO spin code, color representation, mass, and runtime label in model +order. The strict reader reconstructs the physical helicity Cartesian product +and the exact set of labels required in each LC word. Every normalized oracle +record has a recomputable whole-record hash in addition to any raw-transcript +digest. Each reduction plan has a separate canonical hash and must partition +every nonzero physical helicity/color cell exactly once. The three fixture +documents become readable only after the bundle-v1 hash manifest is published +last. + +The pinned Fortran adapter follows these additional rules: + +- run probes in isolated temporary working directories so the dependency + checkout remains read-only; +- prefer an exact ordered external-PDG row, and persist the selected group, + integral, row PDGs, color order, source permutation, and match count; +- identify repeated-PDG legs with stable external IDs and compare every + physical helicity. A single-flow LC case may be resolved directly; for + multiple flows the row-wise quadratic-contraction partitions are diagnostic + only, so the independent Fortran evidence certifies the per-helicity + aggregate rather than mislabeling those rows as physical resolved cells; +- require every resolved or helicity-aggregate observation of a structural + zero to be exactly zero, independent of numerical tolerances; +- treat the probe's LC, NLC, and full outputs as cumulative contractions, not + additive components; +- compare fresh compiler output numerically with the tracked tolerances while + keeping process identity, axes, command, dependency provenance, structural + zeros, and capture metadata exact; +- do not require the legacy oracle for three-quark-line cases, which exceed + the pinned implementation's supported process class. + +The next independent built-in-SM ladder proceeds through `d d~ > z`, +`u d~ > w+`, `d d~ > e+ e-`, `d d~ > u u~`, `g g > g g`, +`g g > t t~`, and `d d~ > g g g`. The last case is the smallest planned +check with genuinely distinct LC, NLC, and full contractions. + +## Known Pre-Port Limitations + +- Existing schema-v2 artifacts have unconfined paths, no payload hashes, + ambiguous serialized-state fallbacks, nontransactional writes, and Python + pickle diagnostics. They are rejected, not migrated. +- Resolved topology replay is not proven in the reference runtime. Schema v3 + must not claim that capability until direct expanded-versus-replayed + resolved values pass. +- The old 180-case cache came from a dirty, older revision and contains + absolute paths. It is not a release fixture and will be regenerated. diff --git a/pyamplicol/docs/development/README.md b/pyamplicol/docs/development/README.md new file mode 100644 index 0000000000..59621a909b --- /dev/null +++ b/pyamplicol/docs/development/README.md @@ -0,0 +1,76 @@ +--- +title: "Development Documentation" +nav_order: 7 +has_children: true +--- + +# Development Documentation + +This directory contains the stable contracts and decisions that supplement the +source and tests: + +- [`API_CONTRACT.md`](API_CONTRACT.md) +- [`CONFIG_CONTRACT.md`](CONFIG_CONTRACT.md) +- [`PACKAGING_CONTRACT.md`](PACKAGING_CONTRACT.md) +- [`PHYSICS_EXTRACTION_CONTRACT.md`](PHYSICS_EXTRACTION_CONTRACT.md) +- [`ARCHITECTURE_DECISIONS.md`](ARCHITECTURE_DECISIONS.md) +- [`ON_THE_FLY_MODE_ARCHITECTURE.md`](ON_THE_FLY_MODE_ARCHITECTURE.md) +- [`MODEL_ASSET_PROVENANCE.md`](MODEL_ASSET_PROVENANCE.md) and + [`model-assets/`](model-assets/README.md) + +Historical feature plans, audit trails, milestone ledgers, and duplicated +machine-readable summaries are deliberately not maintained here. Current +behavior is defined by the implementation, tests, and contracts above. +The on-the-fly architecture page is retained because it records the current +production contract and validation boundary; its historical dispositions are +explicitly labelled as such. + +## Linux source-test wheels + +The contributor installer and `just source-runtime` build a host-compatible +Linux wheel for local tests, rather than claiming compatibility with an older +manylinux platform. They explicitly pass the backend setting +`--config-setting pyamplicol.host-wheel=true` to `python -m build --wheel`. +Published dependencies are unchanged. Normal release builds still target +`manylinux_2_28` and must run in the appropriate build environment; a host wheel +is not a substitute for the audited release artifact. + +## Focused SymJIT dependency tests + +The source checkout includes small opt-in numerical/compiler regressions in +`tests/integration/test_symjit_upstream_regressions.py`. They require Python +3.11+, pytest and Cargo, but no pyAmpliCol native build or Symbolica installation. +They are separate from full process-generation and performance tests. +The reproducer bundles in `SYMJIT_FOLLOW_UP_FIXES/` and `DEPENDENCY_FIXES/` +are local, untracked inputs; retain or supply those directories before running +this optional suite. They are not included in a fresh checkout. + +```sh +# Published SymJIT 2.25.4; outstanding regressions fail rather than being xfailed. +PYAMPLICOL_RUN_SYMJIT_REGRESSIONS=1 \ + python3 -m pytest tests/integration/test_symjit_upstream_regressions.py -q + +# Evaluate a local SymJIT source checkout instead, without modifying it. +PYAMPLICOL_RUN_SYMJIT_REGRESSIONS=1 \ +PYAMPLICOL_SYMJIT_SOURCE=/absolute/path/to/symjit \ + python3 -m pytest tests/integration/test_symjit_upstream_regressions.py -q +``` + +`just symjit-regressions` runs the same suite. The source-selection environment +variable is honoured by either command. ARM-specific tests are skipped on other +architectures. The root `SYMJIT_FOLLOW_UP_FIXES/README.md` maps the checks to the +reported defects and explains the relevant pyAmpliCol evaluation paths. + + +```{toctree} +:hidden: + +API_CONTRACT +CONFIG_CONTRACT +ARCHITECTURE_DECISIONS +PACKAGING_CONTRACT +PHYSICS_EXTRACTION_CONTRACT +MODEL_ASSET_PROVENANCE +model-assets/README +ON_THE_FLY_MODE_ARCHITECTURE +``` diff --git a/pyamplicol/docs/development/model-assets/README.md b/pyamplicol/docs/development/model-assets/README.md new file mode 100644 index 0000000000..201a8deaae --- /dev/null +++ b/pyamplicol/docs/development/model-assets/README.md @@ -0,0 +1,99 @@ +--- +title: "Packaged Model Assets" +nav_order: 7 +parent: "Development Documentation" +has_children: true +--- + +# Packaged Model Assets + +This directory records the release classification for the model resources under +`pyamplicol/assets/models`. The machine-readable authority is +`PROVENANCE.toml` in that package directory; `MANIFEST.sha256` is the single +content manifest. + +## Selection Boundary + +The external-model inventory starts from the 67 tracked files under +`pyAmpliCol/src/pyamplicol/assets/models` at AmpliCol revision +`643bc6f99d7b2249af0a85204768df243e612411`: one historical manifest and 66 +payload files. Only the 66 payload Git blobs were selected. The duplicate +`pyAmpliCol/assets/models` tree, ignored files, caches, bytecode, and source +working-tree contents were not used. + +The packaged release set contains complete UFO directories and loader JSON +examples for `sm`, `scalars`, and `scalar_gravity`. The historical built-in SM +is code-backed by `pyamplicol/models/builtin.py`; its exact baseline source and +companion symbol-table hashes are recorded as the `built-in-sm` model entry in +`PROVENANCE.toml`. No second serialization of that implementation was invented. + +## Deterministic Changes + +The scalar UFO uses the historical defaults of three scalar particles and +contact valences three through ten. Host environment variables can no longer +change those values. The scalar-gravity UFO similarly fixes three scalar +particles and removes its unused interaction-option branch, which referenced +an undefined variable when enabled. + +JSON models and restriction cards were regenerated with +`ufo-model-loader==0.1.8` at revision +`70ddee6b416f8c8b340e0d087646d77095c5d24b`. Full model serializations use +`restriction="full"`, simplification enabled, and `JSONLook.VERBOSE`. +`sm_wrapped_indices.json` additionally enables Lorentz-index wrapping. The +tracked `scalars_2p_3p.json` variant is reserialized from its own canonical +content and freezes `N_SCALARS=3` with contact valences two and three; it does +not retain a runtime environment option. + +The current JSONs also include the numerical parameter and coupling defaults +computed by the loader. Their symbolic expressions and model contents are +unchanged; restriction cards and the frozen scalar variant retain their +existing serialization when regeneration produces identical contents. + +Each JSON restriction card is a complete serialization of its matching UFO +`restrict_*.dat` card. Zero-valued parameters are therefore explicit. A +separately supplied sparse JSON card remains an override map: omitted entries +retain the unrestricted model defaults and are never inferred to be zero. + +Every changed source and result hash is listed in +[TRANSFORMATIONS.md](TRANSFORMATIONS.md) and in `PROVENANCE.toml`. + +## Redistribution Classification + +The applicable notices are the root `LICENSE`, +`licenses/GammaLoop-model-assets.txt`, and +`licenses/MadGraph5_aMCatNLO.txt`. The exact loader license is preserved at +`licenses/model-assets/ufo-model-loader-MIT.txt`. The MadGraph notice applies +specifically to `ufo/sm/build_restrict.py`. Loader-generated JSON retains the +terms of its source model; the loader itself is MIT licensed. + +All primary source identities point to exact AmpliCol baseline Git objects. +For additional traceability, each record compares that source blob with the +same path at GammaLoop revision +`db79edc84f6a1580decbcc4ede7ea0b1c79d9a08`. Twenty-eight of 66 baseline +payload blobs match that pinned revision exactly. The other 38 are not claimed +as exact GammaLoop-revision content; their exact provenance stops at the +AmpliCol baseline while GammaLoop remains the documented distribution origin. + +Under the repository's existing notices this inventory is release-eligible, +so no file is in `nonrelease/`. If a notice is withdrawn or found insufficient, +the affected complete model set must move to an explicitly excluded +`nonrelease/` directory and release tests must fail with that reason. Individual +files must not be silently dropped from a UFO model. + +## Integrity Contract + +`MANIFEST.sha256` lists every packaged file below the model root except the +manifest itself, including `PROVENANCE.toml`. Paths are sorted POSIX-relative +paths. Release tests require exact set equality, SHA-256 agreement, no symlinks, +no traversal or absolute-path references, complete UFO module sets, JSON +parseability, and `importlib.resources` access. + +Raw UFO modules execute Python and must only be loaded from trusted sources. +The serialized JSON forms are preferred for portable workflows. + + +```{toctree} +:hidden: + +TRANSFORMATIONS +``` diff --git a/pyamplicol/docs/development/model-assets/TRANSFORMATIONS.md b/pyamplicol/docs/development/model-assets/TRANSFORMATIONS.md new file mode 100644 index 0000000000..390b98bacd --- /dev/null +++ b/pyamplicol/docs/development/model-assets/TRANSFORMATIONS.md @@ -0,0 +1,63 @@ +--- +title: "Model Asset Transformations" +nav_order: 1 +parent: "Packaged Model Assets" +grand_parent: "Development Documentation" +--- + +# Model Asset Transformations + +The source hash is the SHA-256 of the selected blob at AmpliCol revision +`643bc6f99d7b2249af0a85204768df243e612411`. The package hash is the SHA-256 +after the named deterministic transformation. Files absent from this table are +copied byte-for-byte and have equal source and package hashes in +`PROVENANCE.toml`. + +| Package-relative path | Transformation | Source SHA-256 | Package SHA-256 | +|---|---|---|---| +| `json/scalar_gravity/restrict_default.json` | `loader-restriction-card-v1` | `78e536276cfa98a854ea9b418a6de9437da1e21ea258fcc57ba2b20a6a691900` | `78e536276cfa98a854ea9b418a6de9437da1e21ea258fcc57ba2b20a6a691900` | +| `json/scalar_gravity/scalar_gravity.json` | `loader-ufo-full-v1` | `e303fd1c39eb39e7deca5b36099202074a9048da3dbf2a73baf1f43a31648173` | `3bfc81548466ffc464fa72a3469a9c0c8930970bf048c64c43aee13c8e83bd72` | +| `json/scalars/restrict_default.json` | `loader-restriction-card-v1` | `31e30e8bdbdaf4753e361269fcd0294f7d7c82956c0afe66052dd527aa4b864a` | `ffe7759b8efcc86e6d12d3797c2e63a91c18c07de1bace6a51ee02144ecbf218` | +| `json/scalars/scalars.json` | `loader-ufo-full-v1` | `6085cb3b1307bfdf7f3bb709ac1df9354b0cef8b1a3f6316f673fa069c9d9c85` | `ca27b8cc228cd03db57556fc1c40298865a75269bf2744bee8c59c013eb28df7` | +| `json/scalars/scalars_2p_3p.json` | `loader-json-canonical-v1` | `ba4a928f4bf28975ca861c85106559a6d2747326572751eec3448bf67a5f763a` | `0a268a9fc959036e166367155ab2eeca5c52f76927a89245f8964b9b33b2a8b5` | +| `json/sm/restrict_c_mass.json` | `loader-restriction-card-v1` | `6b191f6d0c45a6e91ea867079b1095e37fc4882df1c1380e70312890959dc7a6` | `014e93a779812d9af1d3886143cebc8fd911e92f5f970aee4dfffe01429651f5` | +| `json/sm/restrict_ckm.json` | `loader-restriction-card-v1` | `57f3ffbd0a0784d433bb9b01211299f29dde1ddd0182ee0b1e4a87dc3f2b5c73` | `be8bdca54507167d8ae4b33d0b0b77808052cd841d6fabd26a3d99926d915b99` | +| `json/sm/restrict_default.json` | `loader-restriction-card-v1` | `9d357d17c254595dbf612b66d6b531c982eefb71432835360bbfd8b21dcf454b` | `8fdd48a3624620c1a15bab602f43a41519038cf69d8fef6aeaaaf13a3f1846c1` | +| `json/sm/restrict_lepton_masses.json` | `loader-restriction-card-v1` | `a22a9de56e536cd7c4e4bd4707f5f95d3255fb0dffd49d9567ec9a659576bb76` | `3d9fe3095907cef03f889bd95b9630a2f836d70244f1b450cfd5d2764913531b` | +| `json/sm/restrict_no_b_mass.json` | `loader-restriction-card-v1` | `2ca10cc1e02b34f226db983da9a787659257003e6d84001204389e8aef03dfa5` | `82d2fbc06f9d0b7daaed56673912d920ca459f435e9083deed2f4b716c2f4a30` | +| `json/sm/restrict_no_masses.json` | `loader-restriction-card-v1` | `9bc6bba7b940da83484e5f43346ba202333669aaca578dfa110ed3fbc17514b8` | `f925921df1b66f010cd6f8cd348b5f4728ba37678e0733d8618cef3f2b4e610e` | +| `json/sm/restrict_no_tau_mass.json` | `loader-restriction-card-v1` | `7560d6890d7cd26532f28865fb8f4092ffcbf49c54fa7a16e921e6b08c3f43e3` | `a26603f4f3572309dc7ce2f23295d5a8aec61d2f3c42a017341245e459648478` | +| `json/sm/restrict_no_widths.json` | `loader-restriction-card-v1` | `9002d18be203c34418a3966f7cedc5d20844c317c55906efca32f51a38f1f183` | `fdc03507f82a3d98571b97ba0ab34f712dc0eb02b101f8308b96c6ab8490c08c` | +| `json/sm/restrict_zeromass_ckm.json` | `loader-restriction-card-v1` | `2173adc123ef202816d56e20772747f8a513ce9294635e099fdbe4832329944d` | `b8b4376c82f9486266283a383a0347ec42498296180d92249e2e7a772e1a7e1a` | +| `json/sm/sm.json` | `sm-standard-ufo-spinor-order-v1` | `684f75c88c7faf6832e9e8122970ec5fe98b9cd30bc989b7f8e1818c327fe165` | `c1222e3b70b71804e5212e994a3b76e2f098f7fc3ba1693c9f9c65578d7e3e00` | +| `json/sm/sm_wrapped_indices.json` | `sm-standard-ufo-spinor-order-v1` | `b4221a0985ad5c6987c325a75012a3231995c604e44ad543d0b1ead22eb19404` | `e1ee0059f3fc534c462e98c358249acbce0a711171fcbfe41720a9a1f24b2933` | +| `ufo/sm/lorentz.py` | `sm-standard-ufo-spinor-order-v1` | `cd578c286b038dd3daae396ebd1515cf36d8089b63a1d20247d2b797054b162b` | `1f13e8b63e2ecf428e2875e14bc6901ce49952e1cf2b18ea9934c8d4264e3559` | +| `ufo/scalar_gravity/parameters.py` | `scalar-gravity-static-defaults-v1` | `68de6a8ab6ecb1f358a13954be99597bb30665d1e195b7395f8faf405bd8657e` | `01b0477e0b0e352f968a6d1bfd1e9724c505adc6dd5c1d39dd08e9da97cd9f00` | +| `ufo/scalars/parameters.py` | `scalar-static-defaults-v1` | `7620698c89ec9a87eb11177f682627e9a334222d42b378c18e111b866f7c3ad0` | `7b11a9b1d84126d31f58d5c4752ea0fe0921074bfcb338ecd6ef4d2ad7da89a7` | + +## Standard UFO Spinor Order + +The upstream SM Lorentz structures index each fermion pair in the reverse of +the standard UFO order, e.g. `FFV1 = Gamma(3,1,2)` instead of MadGraph's +`Gamma(3,2,1)`, and the importer compensated for it, which made standard UFO +models wrong for off-shell fermion lines. `sm-standard-ufo-spinor-order-v1` +swaps the two fermion legs in the spinor slots of `ufo/sm/lorentz.py` and of +the serialized JSON structures, so that they match the MadGraph SM; the +importer reads the standard order. + +## Serialized Model Shape + +The regenerated unrestricted JSON models have these stable counts: + +| File | Particles | Vertices | Couplings | Lorentz structures | +|---|---:|---:|---:|---:| +| `sm.json` | 43 | 153 | 108 | 22 | +| `sm_wrapped_indices.json` | 43 | 153 | 108 | 22 | +| `scalars.json` | 3 | 276 | 1 | 8 | +| `scalars_2p_3p.json` | 3 | 16 | 1 | 2 | +| `scalar_gravity.json` | 4 | 11 | 14 | 8 | + +The historical wrapped SM JSON contained default-restricted counts while its +serialized restriction field was unset. Regeneration makes it a complete, +unrestricted wrapped-index model, consistent with its package name and the +complete-model release requirement. diff --git a/pyamplicol/docs/performance_reports/README.md b/pyamplicol/docs/performance_reports/README.md new file mode 100644 index 0000000000..cb8b9c8453 --- /dev/null +++ b/pyamplicol/docs/performance_reports/README.md @@ -0,0 +1,93 @@ +--- +title: "Performance Reports" +nav_order: 2 +parent: "Profiling and Benchmarking" +--- + +# Performance Reports + +See [FullColor FFT Profiling](../user/fullcolor-fft-profiling.md) for the +source-checkout driver, cluster resource limits, incremental top-ups, +targeted overwrite, and render-at-any-time workflow that produces the FFT +snapshots below. + +The repository publishes four selected rendered snapshots from manual +measurement campaigns: + +- [MacBook M3 report](https://github.com/MadGraphTeam/pyamplicol/blob/main/docs/performance_reports/macbook_M3_pyAmpliCol.pdf) +- [AMD EPYC report](https://github.com/MadGraphTeam/pyamplicol/blob/main/docs/performance_reports/EPYC_pyAmpliCol.pdf) +- [FullColor FFT selected-helicity snapshot](https://github.com/MadGraphTeam/pyamplicol/blob/main/docs/performance_reports/summary_plots_final.pdf) +- [FullColor FFT helicity-sum snapshot](https://github.com/MadGraphTeam/pyamplicol/blob/main/docs/performance_reports/summary_plots_final_helicity_sum.pdf) + +The first two PDFs are general host reports. The FullColor FFT selected- +helicity snapshot times one shared known-nonzero helicity from helicity-general +artifacts; the helicity-sum snapshot times the complete physical helicity sum. +Both snapshots include genuine same-workload MadGraph standalone series. The +fixed series uses the generated helicity-general standalone and selects the +shared helicity through `MATRIX(P,NHEL,IC)`; it currently measures pure gluons +through `n=5` and `d d~ > d d~ + gluons` through `n=6`. The independent summed +series measures both families at `n=2..5` through MadGraph's generated +`SMATRIX(P,ANS)` entry point with `USERHEL=-1`, native IDEN normalization, and +warmed `GOODHEL` pruning. It does not reuse the fixed-helicity timings. +The FFT PDFs are rolling development snapshots, not claims that every +final-state multiplicity through `n=9` has completed. + +Their frontiers are curve-specific. Both fixed-helicity and helicity-sum OTF +curves are requested only through final-state `n=6`; beyond that the publication +protocol retains recurrence, AmpliCol, and Reference FFT where applicable. +Pure-gluon OTF FFT reached the 3,600 s first-use runtime cap at `n=6` and retains +its measured `n=5` point. The helicity-sum snapshot extends +`d d~ > d d~ + gluons` through `n=6`, where every requested curve measured. +OTF direct and FFT setup took 1,725.8 s and 1,607.9 s; their warmed runtimes were +5.321 and 3.759 ms/point (1.416x faster with FFT), at 1.17 and 1.22 GiB peak +RSS. These isolated measurements used a 10-hour time limit and 30 GiB +process-tree guard and retain their own per-cell provenance. + +The FFT plots use a deliberately method-specific *setup time*. pyAmpliCol +includes artifact generation, a fresh load, the first requested evaluation, +and OTF family warm-up where applicable. Reference FFT includes its build, +initialization, and first pass. AmpliCol includes process/color-object +generation for the fixed workload, or process/raw-library generation and build +plus the immutable snapshot for the summed workload. Warmed runtime remains a +separate measurement after those boundaries. + +These PDFs are measurement-host results, not release-CI results. Raw JSON, +generated TeX, build workspaces, attempts, logs, locks, and coordination state +remain untracked. The blank/reset JSON and TeX resources installed by +pyAmpliCol are campaign templates, not published measurements. + +Create or reset a self-contained campaign from any installed wheel with: + +```console +pyamplicol profiling-campaign copy ./pyamplicol-profiling-campaign --force +./pyamplicol-profiling-campaign/steer_performance_campaign.py run \ + --workers 1 --table matrix --process-id 1 --multiplicity 1 \ + --color-approximation lc --generation-mode non-union-flow \ + --generation-engine recurrence --model built_in \ + --no-dependencies-added --no-dashboard +./pyamplicol-profiling-campaign/steer_performance_campaign.py refresh-pdf +``` + +The run above is the supported quick installation check: it measures only the +final-state-multiplicity-one `d d~ > Z` recurrence cell. Broader selections +produce the same report format as the two general host PDFs. Re-running +`refresh-pdf` directly reproduces the PDF from the campaign's current results; +no repository report data is required. + +Runtime attempts, artifacts, locks, and coordination state live visibly in +`pyamplicol-profiling-campaign/campaign_artifacts/`. Moving or renaming the +whole campaign directory moves that state with it; campaigns with the same +basename in different parents remain independent. Historical repository-global +`.artifacts` state is intentionally ignored. + +`--force` replaces the managed template files and resets only +`campaign_artifacts/`, the managed PDF and summary IDs, measurement lineage, +and known LaTeX byproducts. It preserves unrelated destination files and the +recorded local-AmpliCol checkout. Stop an active campaign before resetting it; +an active controller makes the command fail without removing state. + +All pyAmpliCol backends use installed resources. Measuring the optional +original-AmpliCol reference additionally requires +`--original-amplicol PATH_TO_COMPLETE_CHECKOUT`; without that checkout only +that backend is reported unavailable. pyAmpliCol and the supported patched +comparison checkout have no LHAPDF dependency. diff --git a/pyamplicol/docs/user/architecture-overview.md b/pyamplicol/docs/user/architecture-overview.md new file mode 100644 index 0000000000..daf65ba6f7 --- /dev/null +++ b/pyamplicol/docs/user/architecture-overview.md @@ -0,0 +1,284 @@ +--- +title: "Architecture Overview" +nav_order: 6 +has_children: true +--- + + +# Architecture Overview + +pyAmpliCol turns a model plus one or more process requests into a standalone +schema-v3 executable artifact. Python owns configuration, model compilation, +process planning, and artifact generation. Rusticol owns artifact loading, +physics-aware process selection, optimized f64 execution, profiling, and the +native ABI. + +This page describes stable public boundaries rather than internal module names. + +```text +CLI or typed Python API + │ + ▼ +schema-v1 configuration → model IR and process expansion + │ + ▼ +recurrence / compiled / eager generation + │ + ▼ +schema-v3 process artifact → Rusticol runtime + ├── Python + ├── C11 + ├── C++17 + ├── Fortran 2008 + └── Rust 2021 +``` + +## User surfaces + +One typed configuration registry drives: + +- schema-v1 TOML cards; +- dedicated CLI flags and `--set` overrides; +- frozen Python configuration dataclasses. + +The public Python root exports model/process types, configuration, `Generator`, +`Runtime`, `BenchmarkRunner`, typed results, convenience helpers, and documented +errors. Generation DAGs, evaluator payloads, Symbolica objects, and artifact +writer internals remain private. + +See [Command-Line Interface](command-line-interface.md), [Configuration](configuration.md), +and [Python API](python-api.md). + +## Model boundary + +The primary portable user input is serialized JSON model IR. Trusted UFO and +JSON sources compile into the same canonical representation before process +planning. A compiled model records particle/anti-particle relations, spin and +statistics, color representation, mass and width policy, propagators, +interactions, exact quantum numbers, and normalized tensors. + +Two model products have different roles: + +- a portable compiled-model JSON contains model IR and exact expressions; +- a `.pyamplicol-model` bundle adds one prepared local-kernel backend for + recurrence or eager execution. + +When higher-point interactions are split into smaller kernels, model compilation +also reduces their internal auxiliary currents. It examines every consuming +kernel and both input orientations: unused components are removed, while +components with identical or opposite coefficient columns are replaced by the +corresponding signed sum of producer components. These are exact symbolic +relations valid for all model parameters, not numerical approximations. +Physical currents and externally prescribed bases are unchanged. The reduced +kernels are stored in the compiled/prepared model, so this work is not repeated +at the first process evaluation. + +Symbolic current weights, propagator phases and projector fractions are kept +exact during this construction. In particular, multiplying an exact coefficient +by a complex unit does not round it to binary64. This preserves cancellations +when the same process output is evaluated with double-double or arbitrary +precision. + +The hand-written built-in Standard Model is a compatibility model. Its aliases +and optimized kernels do not define generic UFO/JSON behavior. Unsupported +model features fail during preflight with structured diagnostics rather than +falling back to built-in assumptions. + +See [Models and Processes](models-and-processes.md). + +## Process planning and representative reuse + +A single multiparticle request may expand into many concrete processes. +Generation retains one evaluator representative per equivalent incoming and +outgoing permutation class when possible. Stable IDs select representatives; +readable process expressions select a requested public ordering. + +Rusticol resolves a unique side-preserving permutation and remaps momenta, +particles, helicities, LC flows, reductions, selectors, and resolved metadata +centrally. No leg crosses `>`, and ambiguous matches fail with their candidate +stable IDs. + +See [Process Selection and Permutations](process-selection-and-permutations.md). + +## Four generation lanes + +All lanes implement the same public process artifact and runtime result: + +- **recurrence** writes current-recursion schedules over prepared local + kernels; +- **compiled** writes process-local stage evaluators; +- **eager** writes compact DAG invocation tables over prepared local kernels; +- **on-the-fly** writes a compact process seed and builds the selected query + family on first use. + +Reusable contracted recurrence artifacts also carry one all-helicity +physical-colour plan and a compact per-helicity row-group dispatch. The +support metadata is used only during cold selector binding; warmed execution +runs the selected rows directly. On-the-fly does not carry that recurrence +companion: it builds and retains the requested family from its compact seed. + +The evaluator backend is a separate dimension: JIT, C++, or assembly. The +default direct JIT stores a SymJIT application. Prepared recurrence/eager/OTF +JIT kernels use the portable O2 storage contract. + +LC exposes physical color flows; NLC/full expose one contracted color output +per helicity. The optimized total and resolved tensor are two views of the same +runtime plan, and `ResolvedEvaluation.total()` must reproduce the optimized +total. + +See [Generation Modes and Evaluators](generation-modes-and-evaluators.md). + +## Schema-v3 artifact boundary + +A generated artifact is a transactional, standalone executable input. It +contains: + +- model and process metadata; +- physical helicity/color axes and reductions; +- execution-mode plans; +- evaluator or prepared-kernel payloads; +- configuration and generation records; +- optional generated API drivers; +- deterministic validation kinematics. + +The manifest confines payload paths and binds the runtime ABI, target, +capabilities, references, sizes, and required content digests. Normal runtime +loading checks the schema, identity-contract marker, path confinement, target, +and ABI at the cheapest authoritative boundary. An explicit full payload hash +audit is available when wanted; it is not repeated on every normal load. + +Artifacts are executable and must come from a trusted producer. Internal +consistency hashes do not establish publisher identity. + +Generated artifact formats intentionally have no automatic compatibility shim. +An old identity contract or runtime ABI fails with regeneration guidance. + +See [Artifacts and Portability](artifacts-and-portability.md). + +## Rusticol runtime boundary + +Rusticol core is Python-independent. It: + +- loads and validates the selected artifact/process; +- applies public process permutations; +- owns the optimized f64 execution engines; +- resolves global and per-point selectors; +- exposes resolved physical metadata; +- applies atomic model-parameter updates; +- measures runtime and native attribution; +- implements C ABI version 1. + +The PyO3 extension and C ABI are boundary layers over the same core. The wheel +also owns a header-only C++ wrapper, a Fortran module source, a dependency-free +safe Rust wrapper, the static C ABI archive, and target link metadata. + +One runtime handle has mutable parameter and warning state and is not +concurrently reentrant. Independent handles may run concurrently. + +## Native SDK and generated APIs + +Binary wheels contain a target-specific static SDK discovered with +`rusticol-config`. Generation can emit an `API/` bundle with Python, C11, +C++17, Fortran 2008, and Rust 2021 standalone checks. + +All five drivers support: + +- process selection by stable ID or side-preserving expression; +- bundled or user-supplied kinematics; +- UFO-style JSON parameter cards and direct overrides; +- resolved evaluation and explicit comparison with the optimized total. + +Python supports precision-controlled exact evaluation when retained expressions +permit it. Native APIs expose f64 only. + +Opt-in [Born Correlations](../correlators.md) use a separate Python exact +executor over retained Symbolica states, with full-colour direct compiled +amplitudes. This includes requests at precision 16; the native ABI and generated +standalone drivers remain ordinary total/resolved evaluation interfaces. + +See [Native APIs](native-apis.md). + +## Symbolica and SymJIT boundary + +Python package startup imports Symbolica and attempts to register the +pyAmpliCol library key. While valid, that package license covers model +compilation, generation, and Python higher-precision or correlated evaluation +without requiring a personal user key. Synchronous registration failure leaves +the personal-license and restricted-mode paths available. Generation queries +Symbolica's effective license state and records resource clamps. + +The default JIT artifact embeds a direct SymJIT application. Rusticol loads and +executes that f64 state without importing Symbolica or applying its generation- +time license/resource policy. Compatible precompiled C++/ASM payloads are also +Symbolica-independent at f64. Generating any of those payloads still uses +Symbolica. + +See [Symbolica and Licensing](symbolica-and-licensing.md). + +## Portability model + +Portability is authenticated from the complete executable content: + +- compiled all-JIT O1/O2 artifacts may use the `portable-64le` target and + rebuild native code on a supported receiving host; +- JIT O0/O3, C++, ASM, and explicitly native-architecture payloads remain + target-specific; +- prepared JIT O2 bundles are portable across supported x86-64 and AArch64 + release hosts; +- a single target-specific execution leaf makes the whole artifact + target-specific. + +Loaders reject incompatible target or CPU-feature requirements before running +the evaluator. + +## Build and release boundary + +One locked Rust workspace builds both the Python extension and static SDK. The +source distribution carries the release build backend and exact release +dependency contract; contributor dependency state is checkout-only and cannot +be substituted into a release build. + +Release publication consumes already validated wheels and one source +distribution. It does not rebuild during upload. The supported platform matrix +and publication boundary are recorded in +[Release and Support](release-and-support.md). + +See [Release and Support](release-and-support.md). + +## Independent physics reference + +The optional original-AmpliCol Fortran comparison is developer/campaign +infrastructure, not an installed runtime dependency. It provides an independent +numerical reference. Agreement among ordinary Python and native f64 APIs +validates a shared ABI and wrapper contract, but those operations all call +Rusticol and are therefore not independent physics implementations. + +## Design principles + +- Model-generic behavior comes from explicit model metadata, not hard-coded SM + particle numbers or names. +- Unsupported capabilities fail explicitly. +- Public inputs and outputs follow the user's selected particle ordering. +- Configuration, Python results, and public metadata are typed and immutable. +- Normal runtime checks each invariant once at its authoritative boundary. +- Performance instrumentation must not redefine the ordinary evaluation path. +- Artifacts are self-contained; installations do not depend on another source + checkout. + +## Deeper contracts + +The repository maintains concise normative contracts for contributors: + +- [Public API contract](../development/API_CONTRACT.md) +- [Configuration contract](../development/CONFIG_CONTRACT.md) +- [Architecture decisions](../development/ARCHITECTURE_DECISIONS.md) +- [Packaging contract](../development/PACKAGING_CONTRACT.md) +- [Physics extraction contract](../development/PHYSICS_EXTRACTION_CONTRACT.md) +- [On-the-fly mode architecture](../development/ON_THE_FLY_MODE_ARCHITECTURE.md) + + +```{toctree} +:hidden: + +symbolica-and-licensing +``` diff --git a/pyamplicol/docs/user/artifacts-and-portability.md b/pyamplicol/docs/user/artifacts-and-portability.md new file mode 100644 index 0000000000..bb060aba42 --- /dev/null +++ b/pyamplicol/docs/user/artifacts-and-portability.md @@ -0,0 +1,283 @@ +--- +title: "Artifacts and Portability" +nav_order: 3 +parent: "Python API" +--- + +# Artifacts and Portability + +pyAmpliCol separates model inputs, prepared model bundles, and generated +process artifacts. Understanding that boundary makes deployment predictable: +you can move the right object, know when native code is rebuilt, and avoid +mistaking an internal digest for publisher authentication. + +## Three related but different objects + +| Object | Typical suffix/path | Purpose | +| --- | --- | --- | +| Compiled model IR | `.pyAmplicol-model.json` | Architecture-independent normalized model, expressions, particles, parameters, and capabilities. | +| Prepared model bundle | `.pyamplicol-model` | Compiled model plus one prepared local-kernel backend for recurrence/eager/OTF generation. | +| Process artifact | directory containing `artifact.json` | Standalone selected processes, physics metadata, evaluators/schedules, model-parameter state, and optional API bundle. | + +A raw JSON or UFO model can feed process-local **compiled** generation. The +default **recurrence**, **eager**, and **on-the-fly** modes require a compatible +prepared model bundle; the installed `built-in-sm` and `built-in-sm-heft` +models are exceptions because wheels already ship their portable JIT O2 packs. + +See [Models and Processes](models-and-processes.md) and [Generation Modes and Evaluators](generation-modes-and-evaluators.md) for producing each +object. + +An optional **OTF cache file**, written by `runtime.save(path)`, is different +from all three: it retains completed current-construction work for the loaded +process. Keep the original process directory, load it normally, then call +`runtime.load_cache(path)`. The cache contains no evaluated currents or model +parameter values and cannot be loaded as a standalone process output. Its +compatibility follows the matching process output and cache format, rather +than providing a separate portability guarantee. See the +[small save/restore example](runtime-and-selectors.md#saving-an-otf-warm-cache). + +## Process-artifact layout + +A generated root resembles: + +```text +artifacts/pp_zjj/ + artifact.json + process-groups.json + evaluators.pacbin + model/ + processes/ + p_p_to_z_j_j_1/ + p_p_to_z_j_j_2/ + ... + API/ + python/ + c/ + cpp/ + fortran/ + rust/ +``` + +The exact payload set depends on execution mode, backend, color accuracy, +coverage, and whether the API bundle is enabled. Do not infer the contract from +filenames; `artifact.json` declares all runtime-bearing payloads and their +roles. + +Inspect without loading executable evaluator state: + +```console +pyamplicol inspect artifacts/pp_zjj +pyamplicol inspect artifacts/pp_zjj --json +``` + +The inventory reports process IDs and aliases, target, execution mode, +capabilities, selector coverage, physical and logical evaluator counts, +payload sizes, and dependency metadata. + +## Artifact identity + +`runtime.artifact_id` is the lowercase SHA-256 identity of runtime-bearing +payload records under the current schema-v3 identity contract. It deliberately +excludes provenance that cannot change runtime behavior, such as requested and +effective configuration records, validation momenta, timing, the +`process-groups.json` map of subprocesses with equal matrix elements, and +report-only metadata. + +```python +from pyamplicol import Runtime + +runtime = Runtime.load("artifacts/pp_zjj", process="d d~ > g z g") +print(runtime.artifact_id) +``` + +This content label answers “is this the same declared runtime payload?” It does +**not** answer “who published this artifact?” + +Artifacts must explicitly carry the current identity-contract marker and +runtime ABI. Old internal process-artifact formats are not automatically +migrated; regenerate them with the current pyAmpliCol version. + +## Fast loading versus an explicit checksum audit + +Normal loading validates the inexpensive authoritative boundary: + +- JSON/schema and identity-contract shape; +- confined relative paths and references; +- process/runtime capability coherence; +- target and CPU compatibility; +- native runtime ABI. + +It intentionally does not rehash every payload. Rehashing a multi-gigabyte +PACBIN file before every evaluation would duplicate work and can dominate +startup without changing the artifact. + +When a full on-disk corruption audit is specifically wanted, request it once: + +```python +from pyamplicol.artifacts import load_manifest, validate_payloads + +manifest = load_manifest("artifacts/pp_zjj") +validate_payloads(manifest) +``` + +That explicit audit recomputes the manifest identity, sizes and SHA-256 hashes +of declared payloads, executable bits, symlink/path constraints, and target +compatibility. + +Artifact writing always validates the structure and declared payload records +it creates. Optional post-build runtime validation is a different operation: it +reopens the completed artifact and compares optimized and resolved f64 +evaluation. It is off by default because this second runtime pass does not +modify the artifact and can become disproportionately expensive for large +resolved axes. Enable it only when an immediate runtime smoke is valuable: + +```console +pyamplicol generate --card run.toml --post-build-validation +``` + +The configured `generation.validation.samples` controls that optional reopened +runtime smoke. It is independent of high-precision numerical-current relation +certification performed during generation. + +## Portability matrix + +| Artifact/backend | Target metadata | Can move between supported x86-64 and arm64 hosts? | +| --- | --- | --- | +| Compiled all-JIT O1 or O2 process artifact | `portable-64le`, empty CPU-feature set | **Yes**, on supported 64-bit little-endian hosts. | +| Eager/recurrence process artifact using a prepared JIT O2 pack | `portable-64le`, empty CPU-feature set | **Yes**, on supported 64-bit little-endian hosts. | +| Prepared JIT O2 model bundle | portable SymJIT storage-v3 | **Yes**, between supported x86-64 and arm64 hosts. | +| Compiled JIT O0 or O3 process artifact | concrete target, empty CPU-feature set | No; regenerate on the destination. | +| C++ or ASM process artifact | concrete target; CPU features when native specialization is enabled | No; requires exact compatible target/features. | +| C++ or ASM prepared bundle | target-native | No. | +| Compiled model IR (`.json`) | architecture-independent | Yes; it contains no executable prepared backend. | + +For a portable compiled all-JIT artifact, Rusticol verifies that every +executable leaf and nested selector path is O1 or O2 SymJIT. Eager and +recurrence artifacts separately require a portable, exact-O2 prepared pack. +In every case, a target-specific capability prevents the portable marker. +Rusticol rebuilds executable code for the receiving CPU when the artifact is +loaded. + +`portable-64le` is an executable portability claim, not a claim that benchmark +results transfer between hosts. Profiling measurements and campaign currents +remain host/profile-specific. + +## Why some JIT artifacts are still target-specific + +JIT O0/O3 may specialize code and metadata for the producing host. C++ and ASM +artifacts contain target-native libraries. Their manifest records a concrete +triple and required CPU features, and loading fails before executable state is +opened if the destination is incompatible. + +For compiled mode, O1 and the default O2 are both portable choices. Prepared +eager/recurrence packs use the exact-O2 portability contract. To maximize a +specific machine's process-local performance with explicit O3, plan to +generate and deploy on a matching target. + +## PACBIN evaluator containers + +JIT evaluator state is stored as indexed logical members in a single +`evaluators.pacbin` container. Inspection distinguishes: + +- physical file count and on-disk size; +- indexed evaluator member count; +- logical unpacked evaluator size. + +It does not extract one filesystem object per evaluator. + +On Unix, runtime loading copies a PACBIN-backed artifact into anonymous memory, +removes write access from that mapping, and parses it there. Replacing, +truncating, or mutating the path afterward cannot change the running evaluator. +Plan for virtual memory and RAM or swap approximately equal to the loaded +PACBIN size for the handle's lifetime; no temporary filesystem space is +required. + +Target-native C++/ASM evaluator libraries use an analogous executable snapshot +root controlled by `PYAMPLICOL_NATIVE_SNAPSHOT_ROOT`. Its filesystem must +permit executable mappings. + +## Prepared models and standalone process artifacts + +A prepared bundle is a **generation input**, not a runtime dependency of the +resulting process artifact. Eager, recurrence, and OTF generation copy only the +needed kernels plus their compact tables, schedules, or process seed into the +process artifact. A machine that evaluates that artifact does not need the +original `.pyamplicol-model` bundle. + +For example: + +```console +pyamplicol model compile models/json/sm/sm.json \ + models/ufo-sm-jit-o2.pyamplicol-model \ + --backend jit --jit-optimization-level 2 + +pyamplicol generate 'd d~ > z g g g' artifacts/ddbar_z3g_eager \ + --model models/ufo-sm-jit-o2.pyamplicol-model \ + --execution-mode eager +``` + +The second artifact is self-contained for supported runtime APIs. + +## Transactional writing and replacement + +Generation writes artifacts transactionally. The output mode is explicit: + +| Mode | Behavior | +| --- | --- | +| `error` | Refuse an existing output (default). | +| `append` | Extend only under the supported compatible multiprocess contract. | +| `replace` | Build a replacement and atomically install it. | + +Use a separate output for materially different configuration during +experimentation. Avoid manually editing a sealed artifact: its declared size, +digest, and semantic links will no longer match. + +The one supported in-place update is `pyamplicol group-processes`, which +rewrites `process-groups.json` (payload role `process-groups`) and its record +in `artifact.json` under the same lock that serializes generation. The artifact +ID does not change. An append keeps the stored groups and adds the new +subprocesses. + +## Copying and archiving + +Copy the **entire** artifact directory. Preserve ordinary file contents and +executable bits; do not copy only `artifact.json` and selected payloads. + +```console +rsync -a artifacts/pp_zjj/ remote:/srv/processes/pp_zjj/ +``` + +After transfer, either load it normally (fast schema/target boundary) or run +one explicit `validate_payloads()` audit when the transport channel warrants +it. Do not add a full checksum pass to every runtime invocation. + +Process artifacts are trusted executable inputs. Direct SymJIT applications +are lowered to native code, and C++/ASM artifacts contain native libraries. +Generate them yourself or receive them through a channel you trust. + +## Campaign artifacts are a separate state layer + +A profiling campaign stores generated process artifacts, attempts, and current +pointers below its own visible `campaign_artifacts/` root. Move the entire +campaign directory to move that state. Do not transplant individual current +pointer files between campaigns. See [Profiling Campaigns](profiling-campaigns.md). + +## Common compatibility failures + +| Message or symptom | Meaning | Next step | +| --- | --- | --- | +| Target triple mismatch | Target-native artifact moved to a different architecture/OS. | Regenerate there or use a portable JIT artifact: compiled O1/O2, or eager/recurrence with a prepared O2 pack. | +| Required CPU feature unavailable | Artifact was specialized beyond the destination CPU. | Regenerate with compatible settings. | +| Old identity contract / runtime ABI | Artifact predates the current internal format. | Regenerate; there is no compatibility shim. | +| Payload size/digest mismatch during explicit audit | Files changed or transfer was incomplete. | Restore from a trusted source or regenerate. | +| PACBIN load consumes substantial memory | Container is intentionally snapshotted into anonymous read-only memory. | Size deployment memory for the artifact or generate a smaller/specialized artifact. | + +See [Troubleshooting](troubleshooting.md) for environment and API-driver failures. + +## Related pages + +- [Runtime and Selectors](runtime-and-selectors.md) — load, select, and evaluate a process. +- [Native APIs](native-apis.md) — consume the same artifact from five languages. +- [Models and Processes](models-and-processes.md) — compiled and prepared model inputs. +- [Release and Support](release-and-support.md) — validated wheel and source-distribution boundary. +- [Packaging contract](../development/PACKAGING_CONTRACT.md). diff --git a/pyamplicol/docs/user/command-line-interface.md b/pyamplicol/docs/user/command-line-interface.md new file mode 100644 index 0000000000..c536673d71 --- /dev/null +++ b/pyamplicol/docs/user/command-line-interface.md @@ -0,0 +1,539 @@ +--- +title: "Command-Line Interface" +nav_order: 5 +parent: "Configuration" +--- + + +# Command-Line Interface + +The `pyamplicol` command covers the complete user workflow: inspect or compile a +model, plan or generate process artifacts, inspect their physics axes, evaluate +phase-space points, and profile the optimized runtime. The same schema also +drives the typed [Python API](python-api.md). + +> [!TIP] +> Start with the four-card walkthrough in [Quick Start](quick-start.md). This page +> is a command map, not a requirement to configure every available switch. + +## Command map + +| Command | Purpose | Typical use | +| --- | --- | --- | +| `generate` | Plan or write a process artifact | `pyamplicol generate "d d~ > z g" artifacts/builtin_ddbar_to_zg --model built-in-sm` | +| `evaluate` | Evaluate totals or resolved components | `pyamplicol evaluate artifacts/builtin_ddbar_to_zg --momenta point.json` | +| `profile` | Calibrate and measure the optimized total path | `pyamplicol profile artifacts/builtin_ddbar_to_zg --target-runtime 1` | +| `benchmark` | Compatibility alias for `profile` | Existing cards use `action = "benchmark"` | +| `inspect` | Show artifact and process metadata without running it | `pyamplicol inspect artifacts/builtin_ddbar_to_zg` | +| `group-processes` | Find generated subprocesses with equal matrix elements | `pyamplicol group-processes artifacts/pp_jj` | +| `model inspect` | Summarize a built-in, JSON, UFO, or compiled model | `pyamplicol model inspect models/json/sm/sm.json` | +| `model compile` | Compile portable model IR or a prepared kernel bundle | See [Models and Processes](models-and-processes.md) | +| `model processes` | Expand and inspect a process request without generating it | `pyamplicol model processes "p p > all all" --model built-in-sm` | +| `config template` | Write the exhaustive schema-v1 TOML template | `pyamplicol config template run.toml` | +| `config resolve` | Show requested and effective settings | `pyamplicol config resolve run.toml` | +| `examples list` | List shipped card names, actions, and descriptions | `pyamplicol examples list` | +| `examples copy` | Create an editable, self-contained example workspace | `pyamplicol examples copy ./pyamplicol-examples --force` | +| `examples run` | Run one named packaged card in a private cache workspace | `pyamplicol examples run evaluate_total` | +| `profiling-campaign copy` | Create a self-contained performance campaign | See [Profiling Campaigns](profiling-campaigns.md) | +| `doctor` | Diagnose the installed runtime and SDK | `pyamplicol doctor` | +| `self-test` | Run the installed-package smoke test | `pyamplicol self-test` | + +License-request helpers are documented in +[Symbolica and Licensing](symbolica-and-licensing.md). + +Run `pyamplicol COMMAND --help` for the options accepted by a command. For the +model family, place `--help` after the second verb, for example +`pyamplicol model compile --help`. + +## Cards, direct options, and overrides + +There are three equivalent ways to supply configuration: + +1. a schema-v1 TOML card; +2. dedicated command-line options; +3. repeated `--set PATH=VALUE` overrides. + +Configuration precedence is: + +```text +defaults < TOML card < dedicated CLI options < repeated --set overrides +``` + +Any license or resource clamp is applied last and reported in the effective +configuration. Unknown fields are errors rather than silently ignored. + +Run a card directly: + +```console +pyamplicol generate_pp_zjj_from_ufo_sm.toml +``` + +Use an explicit action when the same card supplies shared settings: + +```console +pyamplicol generate --card qq_z6g_recurrence_jit_o2.toml +pyamplicol profile --card qq_z6g_recurrence_jit_o2.toml +``` + +Override one leaf without editing the card: + +```console +pyamplicol generate_pp_zjj_from_ufo_sm.toml \ + --set generation.workers=2 \ + --set generation.mode=replace +``` + +`--set` is deliberately repeatable and order-sensitive. For commonly used +fields, prefer the readable dedicated form—for example `--workers 2`, +`--execution-mode eager`, or `--color-accuracy nlc`. + +For the complete field reference, see [Configuration](configuration.md) or create +an annotated local template: + +```console +pyamplicol config template pyamplicol.toml +``` + +## Generate + +The shortest built-in-model command is: + +```console +pyamplicol generate "d d~ > z g" artifacts/builtin_ddbar_to_zg \ + --model built-in-sm +``` + +Useful generation options include: + +- `--process EXPRESSION` to add another request; +- `--name NAME` to name a request; +- `--multiparticle 'p=d,d~,g'` to define a multiparticle label; +- `--flavor-scheme N` and `--max-quark-lines N` to constrain expansion; +- `--flavor-blind` to generate one subprocess per flavour-relabelling class + (see [Group equivalent subprocesses](#group-equivalent-subprocesses)); +- `--analyze-process-groups {none,numerical}`, `--only-unit-multiplier`, and + `--[no-]consider-charge-conjugation` to search the generated subprocesses + for equal matrix elements; +- `--color-accuracy {lc,nlc,full}`; +- `--color-contraction {direct,symmetric-group-fft}`; +- `--fft {trace,adjoint}` as a shorthand selecting symmetric-group FFT and its basis; +- `--correlators FILE.json` to prepare named colour operators and allowed spin + replacements for the [Python and native correlated APIs](../correlators.md); +- `--lc-flow-layout {topology-replay,all-flow-union}`; +- `--execution-mode {recurrence,compiled,eager,on-the-fly}`; +- `--backend {jit,asm,cpp}`; +- `--workers auto|N`; +- `--mode {error,append,replace}` or the `--force` shortcut for replacement; +- `--no-emit-api-bundle` when standalone drivers are not wanted; +- `--numerical-current-reuse` to search for certified numerical current + relations at generation time (off by default because the high-precision + probes are costly); +- `--post-build-validation` for an optional immediate native total-versus- + resolved smoke after writing the artifact. + +Planning is non-writing: + +```console +pyamplicol generate "p p > Z j j" artifacts/unused \ + --model models/json/sm/sm.json \ + --multiparticle 'p=d,d~,g' --multiparticle 'j=d,d~,g' \ + --flavor-scheme 2 --max-quark-lines 2 \ + --execution-mode compiled --dry-run +``` + +Adaptive `--fft adjoint` is the recommended starting point for exact FFT colour +contraction. It selects certified DDM where valid and otherwise retains exact +fundamental-chain or trace tensors. Select contracted colour and +a supported execution lane (`recurrence` or `on-the-fly`) explicitly: + +```console +pyamplicol generate "g g > g g g" artifacts/ggg_adjoint_fft \ + --model built-in-sm --color-accuracy full \ + --fft adjoint --execution-mode recurrence +``` + +Use `--execution-mode on-the-fly` for the compact OTF counterpart. The same +option works for quark processes, using their existing fundamental chains: + +```console +pyamplicol generate "d d~ > z g g" artifacts/quark_zgg_fft \ + --model built-in-sm --color-accuracy full \ + --fft adjoint --execution-mode recurrence +``` + +For a certified `n`-gluon tree, DDM fixes two gluon anchors and retains +`(n-2)!` orderings instead of `(n-1)!`. Certified single-insertion HEFT also +uses DDM: see `examples/builtin_sm_heft.toml`, which explicitly sets `HIG = 1`. +Other interactions retain their exact trace representation; multi-quark-line +processes retain fundamental-chain products, not a JO primitive basis. +The saved `fft_basis_selection` states the requested and actual basis and the +selection reason. `--fft trace` forces the original representation. +Both requests preserve `full` or `nlc` accuracy; forced adjoint FFT rejects correlations. +In the benchmarked pure-gluon family the adjoint basis was faster than trace +for six or more external gluons (3.9x per sample and 4x faster generation at +ten gluons); at lower multiplicity the two are comparable, so compare warmed +evaluation and setup costs for other workloads. +`--color-contraction symmetric-group-fft` remains valid and defaults to adjoint. +Without an explicit contraction flag, NLC/full recurrence and on-the-fly +generation automatically try adaptive adjoint FFT and fall back to direct if +the FFT plan is unsupported. LC, compiled/eager, and correlated generation +remain direct/trace. Use `--color-contraction direct` to opt out. +Do not combine `--fft` and `--color-contraction`; use either spelling. Card +settings are overridden by dedicated flags, then by ordered `--set` options +as usual. + +For the dedicated direct/FFT/reference scaling scan, see +[FullColor FFT Profiling](fullcolor-fft-profiling.md). + +`--dry-run` performs the operation exposed as `Generator.plan()`. It creates no +artifact, output directory, or model-cache entry. A raw UFO or JSON source must +already have a reusable compiled-model cache entry, or be compiled explicitly, +because planning never compiles trusted model input as a side effect. + +See [Generation Modes and Evaluators](generation-modes-and-evaluators.md) before +combining execution modes and prepared-model backends. + +### Correlated Born quantities + +The [Born Correlations guide](../correlators.md#json-and-command-line) gives the +declaration format and complete runtime examples: + +```console +pyamplicol generate 'g g > g g' artifacts/gg_correlated \ + --model built-in-sm --color-accuracy full --correlators correlators.json +``` + +`--correlators` is a generation option, separate from the ordinary TOML colour +settings. `--color-accuracy lc|nlc|full` selects the correlated approximation +(the ordinary default is LC). Complete amplitudes are generated in all cases; +direct contraction and generic compiled execution are selected, with those +adjustments recorded in the effective configuration. Partial +helicity/colour generation, append, and `--dry-run` are not supported on this +path. Numerical spin-vector setting and correlated evaluation use +`Runtime.set_spin_correlation_vectors(...)` and +`Runtime.evaluate_correlated(...)` or `Runtime.evaluate_correlated_many(...)` +in Python, or the corresponding [native SDK calls](native-apis.md#correlated-born-evaluations), +not the CLI `evaluate` command. + +## Inspect and select a process + +List an artifact's stable process IDs and coverage: + +```console +pyamplicol inspect artifacts/pp_zjj +``` + +Focus on one public process ordering: + +```console +pyamplicol inspect artifacts/pp_zjj --process 'd d~ > g z g' +``` + +The selector may be a stable process ID, an explicit alias ID, an exact stored +expression, or a unique side-preserving permutation of one. See +[Process Selection and Permutations](process-selection-and-permutations.md) for +the ordering contract. + +Inspection does not execute evaluator state. Human output uses compact colored +tables; machine output is available with `--json`. + +The `Process groups` section shows the artifact's +[process-group map](process-grouping.md): an overview row, then one table of +groups, each representative in bold followed by its members. Colours mark +`generated` (green) and `routed` (cyan) members, `C` (magenta), factors other +than 1 and warnings (yellow), and the basis of each relation. For example, for +`p p > j j j j` generated with `--flavor-blind --analyze-process-groups +numerical` (abridged): + +```text +Process groups + ++--------+--------------+-----------+--------+------------+-------------+------------------+-----------+----------+ +| groups | subprocesses | generated | routed | conjugated | factor != 1 | bases | analyses | warnings | ++--------+--------------+-----------+--------+------------+-------------+------------------+-----------+----------+ +| 23 | 486 | 23 | 463 | 140 | 0 | flavor-blind 463 | numerical | 0 | ++--------+--------------+-----------+--------+------------+-------------+------------------+-----------+----------+ + ++--------------------+---------------------------------------------------------------------------------------------+ +| representative | members | ++--------------------+---------------------------------------------------------------------------------------------+ +| d d > d d d d~ | 4 routed u u > u u u u~ s s > s s s s~ c c > c c c c~ b b > b b b b~ | +| p_p_to_j_j_j_j_1 | 5 routed C d~ d~ > d d~ d~ d~ u~ u~ > u u~ u~ u~ s~ s~ > s s~ s~ s~ c~ c~ > c c~ c~ c~ | +| 10 subprocesses | b~ b~ > b b~ b~ b~ | ++--------------------+---------------------------------------------------------------------------------------------+ +... ++--------------------+---------------------------------------------------------------------------------------------+ + +routed: not generated, evaluated through its representative; C: charge-conjugated relation +compact view of 486 subprocesses; IDs and leg permutations: --json; one group row by row: --process REPRESENTATIVE +numerical analysis: 3 points, tolerance 1e-09 +``` + +`routed` members are not generated and are evaluated through their +representative; `C` marks a charge-conjugated relation. A map with at most 80 +subprocesses is listed one member per row, with its process ID, role, and +basis, plus a factor column when a factor differs from 1, a `C` column when a +relation is conjugated, a permutation column when a leg permutation is not the +identity, and a `note` column with `[n]` references to the warnings printed +below the table. Larger maps list each group's members as a grid of process +strings, one block per relation; subprocesses alone in their group are listed +as `ungrouped`. When every process is its own group, the section is the single +line `Process groups: none; each of the N processes is its own group`. With +`--process`, only the selected process's group is shown, so `--process +REPRESENTATIVE` lists a large group member by member. A routed member has no +runtime process of its own: without `--full-physics`, selecting it stops with +an error that names its representative. `--json` reports the +complete map under `process_groups`: `stored`, `analyses`, and per group the +`representative_id`, `representative`, and `members` (`id`, `process`, `pdgs`, +`initial_count`, `factor` as a fraction string, `permutation`, `conjugated`, +`basis`, `generated`, `warning`). + +`--process MEMBER --full-physics` on a grouped process adds a `Process Group` +table, and a `process_group` object to the JSON. For a member, it gives the +representative, factor, conjugation and the leg permutation from the +representative to the requested leg order; for a representative, the size of +its group, and that permutation when the requested leg order is not the stored +one: + +```text ++-----------------------------------------------------------------------------------------+ +| Process Group | ++----------------+------------------------------------------------------------------------+ +| field | value | ++----------------+------------------------------------------------------------------------+ +| role | routed member: not generated, evaluated through the representative | +| representative | p_p_to_j_j_j_j_52 d u~ > d d d~ u~ | +| factor | 1 | +| conjugated | yes (C) | +| permutation | p = [1 0 4 5 3 2]: representative leg r takes the momentum of leg p[r] | +| basis | flavor-blind | +| group | 40 subprocesses: 1 generated, 39 routed, 20 conjugated | ++----------------+------------------------------------------------------------------------+ +``` + +## Group equivalent subprocesses + +Every artifact records in `process-groups.json` which subprocesses share one +matrix element, up to a rational factor and a leg permutation. By default each +generated subprocess is its own group. + +`generate --flavor-blind` assumes that subprocesses related by a relabelling of +flavours with equal spin, colour, mass, and width (and, unless +`--no-consider-charge-conjugation`, by charge conjugation) are equal. Only one +representative per class is generated; the others are recorded as its members +and exported to UMAMI through the representative's runtime. The assumption is +not verified: a warning names the members whose relabelling is not a symmetry +of the model couplings, for example electroweak couplings of different quark +flavours. + +Generation logs the outcome of this pre-grouping before any representative is +generated: + +```text +INFO pyamplicol.generation: Flavor-blind grouping: 486 subprocesses -> 23 generated representatives; 463 routed members (140 by charge conjugation); 0 relabelling warnings +``` + +The summary table printed at the end has a `process grouping` row, coloured +like the `inspect` display, read from the written `process-groups.json` +(abridged): + +```text +| processes | 23 requests | +| process grouping | 486 subprocesses -> 23 groups: 23 generated, 463 routed | +| | 463 flavor-blind relations (140 charge-conjugated); 0 warnings | +| | numerical analysis: 3 points, tolerance 1e-09 | +``` + +Its first line counts the subprocesses, groups, and generated and routed +subprocesses; the second, the relations by basis, those that are +charge-conjugated or have a factor other than 1, and the relabelling warnings; +the last ones, each recorded numerical analysis. The `Generated Processes` +table gains a `group` column: the size of each representative's group, or +`member of REPRESENTATIVE` for a member generated in its own right, followed by +its factor when it is not 1 and `C` when the relation is conjugated, as in +`member of d_dbar_to_a_a (x16)`. When +every subprocess is its own group, the column is omitted, and so is the row +unless an analysis was recorded; the row then reads `no relation found; each of +the N processes is its own group`. + +The numerical analysis compares generated subprocesses with equal external +particle types at three random phase-space points and merges those whose +resolved components agree up to one common rational factor: + +```console +pyamplicol generate "p p > j j" artifacts/pp_jj --analyze-process-groups numerical +pyamplicol group-processes artifacts/pp_jj --only-unit-multiplier +``` + +`group-processes` runs the numerical analysis (its default) on an existing +artifact and rewrites `process-groups.json` in place; the artifact ID and the +UMAMI export are unchanged. `--only-unit-multiplier` accepts only relations +with factor 1. Subprocesses generated with a selected helicity or colour subset +are never grouped numerically. [Process Grouping](process-grouping.md) +describes the relations, the warnings and the file format in full. + +## Evaluate + +Evaluate a momenta file with optional parameter updates: + +```console +pyamplicol evaluate artifacts/pp_zjj \ + --process 'd d~ > g z g' \ + --model-parameters data/model_parameters.json \ + --momenta data/pp_zjj_momenta.json +``` + +Request all physical components rather than only the optimized sum: + +```console +pyamplicol evaluate artifacts/pp_zjj \ + --process 'd d~ > g z g' \ + --momenta data/pp_zjj_momenta.json \ + --resolved +``` + +Global selectors may be repeated: + +```console +pyamplicol evaluate artifacts/pp_zjj \ + --process p_p_to_z_j_j_3 \ + --helicity 'h:-1,+1,-1,+1,+1' \ + --color-flow 1 \ + --momenta data/pp_zjj_momenta.json +``` + +`--color-flow` accepts either a stable flow ID or the one-based ordinal shown +by `inspect`. Flow selection is LC-only; NLC and full-color artifacts expose a +contracted color output and accept helicity selection only. More examples are +in [Runtime and Selectors](runtime-and-selectors.md). + +## Profile + +The profiler measures the same optimized total path as `Runtime.evaluate()`: + +```console +pyamplicol profile artifacts/pp_zjj \ + --process 'd d~ > g z g' \ + --momenta data/pp_zjj_momenta.json \ + --target-runtime 1.0 \ + --batch-size 128 \ + --color-flow 1 \ + --precision 16 +``` + +It calibrates repetitions, keeps at least the requested independent sample +count, and reports uncertainty. `Ctrl-C` during sampling preserves completed +blocks and prints a result marked partial. See +[Profiling and Benchmarking](profiling-and-benchmarking.md) for metric meanings +and selector-aware profiling. If both profile selector axes are omitted, the +stored LC layout supplies a deterministic optimized selector; explicit +non-hot shapes are retained and produce at most one pre-loop warning per +loaded process. + +## Models + +Inspect a trusted UFO directory without generating a process: + +```console +pyamplicol model inspect models/ufo/sm +``` + +Compile portable model IR: + +```console +pyamplicol model compile models/ufo/sm models/sm.pyAmplicol-model.json +``` + +Prepare a reusable JIT-O2 kernel bundle for eager or recurrence execution: + +```console +pyamplicol model compile \ + models/json/sm/sm.json models/ufo-sm-jit-o2.pyamplicol-model \ + --backend jit --jit-optimization-level 2 +``` + +Enumerate a broad request without writing a process artifact: + +```console +pyamplicol model processes "p p > all all" \ + --model built-in-sm --flavor-scheme 1 --max-quark-lines 0 +``` + +The distinction between portable model IR and prepared kernel bundles is +explained in [Models and Processes](models-and-processes.md). + +## Examples and installed workspaces + +List the available names before running one: + +```console +pyamplicol examples list +pyamplicol examples run generate_pp_zjj_from_ufo_sm +``` + +The list uses a colored table on a terminal. Add `--json` for stable, +uncolored machine-readable output. + +`examples run` accepts the stem of a shipped `.toml` card. `all_options` is a +reference template, not a runnable example. Unknown names fail with the full +available-name list. + +For an editable and inspectable workspace, copying is usually clearer: + +```console +pyamplicol examples copy ./pyamplicol-examples --force +cd pyamplicol-examples +pyamplicol generate_pp_zjj_from_ufo_sm.toml +``` + +See [Examples Gallery](examples-gallery.md) for the complete card inventory and +recommended sequences. + +## Output, color, progress, and logging + +The common presentation options are: + +| Option | Values | Meaning | +| --- | --- | --- | +| `--json` | flag | Emit stable machine-readable stdout instead of the human-facing table | +| `--color` | `auto`, `always`, `never` | Detect a terminal, force ANSI color, or disable it | +| `--progress` | `auto`, `tty`, `log`, `off` | Select live bars, rate-limited log progress, or silence | +| `--log-level` | `debug`, `info`, `warning`, `error` | Control diagnostic verbosity | + +JSON mode keeps stdout machine-readable; progress and diagnostics use stderr. +This makes pipelines predictable: + +```console +pyamplicol inspect artifacts/pp_zjj --json > artifact.json +``` + +Use `--color always` only when the consumer understands ANSI escapes. The +default `auto` mode produces colored terminal tables and plain redirected +output. + +## Diagnostics + +Start with the two installed-package checks: + +```console +pyamplicol doctor +pyamplicol self-test +``` + +Then inspect the relevant artifact and effective configuration: + +```console +pyamplicol inspect artifacts/pp_zjj --json > inspect.json +pyamplicol config resolve run.toml --json > config.json +``` + +For common failures and the information to include in a bug report, see +[Troubleshooting](troubleshooting.md) and [Release and Support](release-and-support.md). + +## Next steps + +- [Configuration](configuration.md) +- [Models and Processes](models-and-processes.md) +- [Generation Modes and Evaluators](generation-modes-and-evaluators.md) +- [Runtime and Selectors](runtime-and-selectors.md) +- [Native APIs](native-apis.md) diff --git a/pyamplicol/docs/user/configuration.md b/pyamplicol/docs/user/configuration.md new file mode 100644 index 0000000000..786c117eef --- /dev/null +++ b/pyamplicol/docs/user/configuration.md @@ -0,0 +1,568 @@ +--- +title: "Configuration" +nav_order: 3 +has_children: true +--- + +# Configuration + +pyAmpliCol uses one typed configuration schema for TOML run cards, direct CLI +options, and Python configuration classes. Unknown fields are rejected rather +than ignored, with a nearest-field suggestion when a likely spelling exists. + +The complete field-by-field reference is the shipped +[`examples/all_options.toml`](https://github.com/MadGraphTeam/pyamplicol/blob/main/examples/all_options.toml). +This page focuses on practical configurations. + +## Minimal generation card + +```toml +schema_version = 1 +action = "generate" + +[model] +source = "built-in-sm" + +[process] +entries = [{ expression = "d d~ > Z g", name = "ddbar_Zg" }] + +[color] +accuracy = "lc" + +[generation] +output = "artifacts/ddbar_Zg" +``` + +Run it with: + +```console +pyamplicol run.toml +``` + +Equivalent direct command: + +```console +pyamplicol generate 'd d~ > Z g' artifacts/ddbar_Zg \ + --model built-in-sm --color-accuracy lc +``` + +## Configuration precedence + +Values are resolved in this order: + +1. schema defaults; +2. TOML card values; +3. dedicated command-line flags; +4. repeated `--set section.field=value` overrides, from left to right; +5. effective generation requirements and license/resource adjustments. + +For example: + +```console +pyamplicol generate_pp_zjj_from_ufo_sm.toml \ + --set generation.workers=2 \ + --set generation.mode=replace +``` + +Requested and effective configurations are retained separately in generated +artifact provenance. A restricted Symbolica environment may reduce effective +generation resources without rewriting the requested card. + +Inspect resolution without running the action: + +```console +pyamplicol config resolve generate_pp_zjj_from_ufo_sm.toml +pyamplicol config resolve generate_pp_zjj_from_ufo_sm.toml --json +``` + +Create a commented template: + +```console +pyamplicol config template run.toml +``` + +## Paths + +Paths in a TOML card are resolved relative to that card, including: + +- model sources; +- artifact outputs and inputs; +- model-parameter cards; +- momenta files; +- cache paths. + +This makes copied example workspaces movable. Direct CLI paths are resolved +from the current working directory. + +Generation output policy is explicit: + +| `generation.mode` | Behavior | +| --- | --- | +| `error` | Default; refuse an existing destination. | +| `replace` | Atomically replace the generated artifact. | +| `append` | Add to an existing compatible artifact. | + +Use `replace` deliberately while iterating: + +```console +pyamplicol generate --card run.toml --set generation.mode=replace +``` + +## External JSON or UFO model + +The primary example uses serialized JSON, which does not execute Python while +loading: + +```toml +[model] +source = "models/json/sm/sm.json" +restriction = "default" +simplify = true +cache = true +``` + +A trusted UFO directory uses the same field: + +```toml +[model] +source = "/absolute/path/to/MyUFO" +restriction = "default" +``` + +UFO modules are Python code and execute during loading. See +[Models and Processes](models-and-processes.md) before using an external model. + +## Processes and multiparticles + +One card may contain one inclusive multiparticle request or multiple named +requests: + +```toml +[process] +entries = [ + { expression = "p p > Z j j" }, + { expression = "u u~ > Z g", name = "uubar_Zg" }, +] +flavor_scheme = 2 +max_quark_lines = 2 + +[process.multiparticles] +p = ["d", "d~", "g"] +j = ["d", "d~", "g"] +``` + +Every model provides the generic `all` multiparticle for valid propagating +physical external particles. It excludes ghosts, Goldstones, +non-propagating records, and auxiliary states. An explicit `all` entry replaces +the default; other user labels are merged over model defaults. + +Broad products such as `p p > all all` can expand combinatorially. Prefer a +narrow user-defined label for production scans. + +`flavor_scheme`, `max_quark_lines`, and coupling-order settings constrain that +expansion. Coupling-order names come from the selected model: the generic +engine treats them as model-defined filtering and scheduling data and does not +assign arbitrary names fixed QCD or electroweak meanings. + +### Process groups + +Four `process` fields control which subprocesses are recorded as sharing one +matrix element in the artifact's `process-groups.json`: + +```toml +[process] +flavor_blind = false # generate one subprocess per flavour class +consider_charge_conjugation = true # also relate charge-conjugate subprocesses +only_unit_multiplier = false # accept numerical relations only with factor 1 +analyze_process_groups = "none" # or "numerical", run after generation +``` + +`flavor_blind` is an assumption, not a check: members whose flavour relabelling +is not a symmetry of the model couplings are reported with a warning. +`consider_charge_conjugation` matters only with `flavor_blind` or numerical +analysis. See +[Group equivalent subprocesses](command-line-interface.md#group-equivalent-subprocesses) +for the CLI flags and the `group-processes` command, and +[Process Grouping](process-grouping.md) for the full description. + +## Color accuracy and LC layout + +```toml +[color] +accuracy = "lc" # lc, nlc, or full +contraction = "auto" # auto, direct, or symmetric-group-fft +fft_basis = "adjoint" # trace or adjoint; used by FFT contraction +lc_flow_layout = "topology-replay" +``` + +| Accuracy | Resolved color output | +| --- | --- | +| `lc` | One entry per physical leading-color flow. | +| `nlc` | One contracted color entry per helicity. | +| `full` | One contracted color entry per helicity. | + +LC offers two complete-coverage layouts: + +| Layout | Best suited to | +| --- | --- | +| `topology-replay` | Default: one selected flow with a helicity sum. | +| `all-flow-union` | All flows with one selected helicity. | + +Both preserve all physical flows and helicities for runtime selection. +`all-flow-union` is LC-only and is incompatible with generation-fixed or +truncated color/helicity coverage. + +For contracted NLC/full output, `symmetric-group-fft` evaluates the same exact +colour interference as `direct` while Fourier-transforming certified +permutation orbits and retaining all other terms as direct residuals. It is +available for `recurrence` and `on-the-fly`; compiled/eager execution and LC +flows deliberately reject explicitly forced FFT. + +The default `contraction = "auto"` tries FFT for NLC/full recurrence and +on-the-fly execution, falling back to direct contraction when an FFT plan is +unsupported. LC, compiled/eager execution, and correlated generation use +direct contraction and the trace basis. The effective configuration can retain +`"auto"` until per-process selection. + +The default `fft_basis = "adjoint"` enables adaptive exact-basis selection. +The requested option and the mathematical representation are +distinct: generation chooses a certified Del Duca–Dixon–Maltoni (DDM) reduction +where available, and otherwise keeps the exact representation appropriate to +the process. + +| Process domain | Actual representation | +| --- | --- | +| Certified Yang–Mills trees | Two-anchor DDM half-ladders. | +| Certified single-insertion scalar HEFT, with gluons and one Higgs | DDM half-ladders; the two-gluon case is the single delta tensor. Use explicit `HIG = 1`. | +| One quark–antiquark line, with optional colour singlets | Fundamental chain, already fixing both quark endpoints. | +| Multiple quark lines | Exact products of fundamental chains and all endpoint routings; not a Melia/Johansson–Ochirov primitive decomposition. | +| Colour singlets only | The trivial singlet tensor. | +| Adjoint interactions without a DDM certificate | Original trace tensors, without assuming amplitude identities. | + +For `n` external gluons, a certified DDM reduction retains `(n-2)!` ordered +amplitudes instead of `(n-1)!`. Count only the gluons, not a colourless Higgs. +This removes redundant orderings without changing colour accuracy. A quark +process need not become smaller or faster: retaining its existing fundamental +chains is a deliberate exact choice, not an adjoint reduction. + +Generation records `fft_basis_selection`, with `requested`, `actual_basis`, +`name`, `reason`, `permutation_degree`, and `tensor_count`. For fundamental +chains, `actual_basis = "trace"` identifies the existing metric implementation, +while `name = "fundamental-chain"` or `"fundamental-chain-products"` identifies +the tensors. The tensor count excludes traversal aliases and is not a rank +claim at fixed `Nc`. Use these recorded fields when comparing artifacts. + +Explicit `fft_basis = "trace"` keeps the original FFT representation. +`contraction = "direct"` opts out of FFT and ignores `fft_basis`. Explicit +`contraction = "symmetric-group-fft"` forces FFT and retains errors for +unsupported plans, LC, and compiled/eager execution. Forced adjoint FFT cannot +be combined with correlated generation; correlations retain their separate +direct contraction path. + +This is an exact contraction algorithm, not an approximation. Its speedup is +process-dependent: a small certified symmetry subgroup or a residual-dominated +contraction can make the FFT and direct curves scale similarly. Keep the +direct result as the baseline when characterizing a new process family. +For the adjoint basis, single-helicity full-colour recurrence benchmarks of +`g g > (n-2) g` on one core showed it faster than trace for `n >= 6`: at +`n = 10` it evaluated 3.9x faster per sample (57.8 versus 227 ms), generated +4.2x faster and used 2.4x less memory during generation. Below `n = 6` the two +bases are comparable, so compare setup and warmed runtime for other workloads. See +[FullColor FFT Profiling](fullcolor-fft-profiling.md) for the resumable +comparison driver and published snapshots. + +```toml +[color] +accuracy = "full" +contraction = "symmetric-group-fft" +fft_basis = "adjoint" # for example, built-in-sm with g g > g g g + +[evaluator] +execution_mode = "recurrence" +``` + +This example forces FFT. Omit `contraction` and `fft_basis` to use automatic +adjoint selection with direct fallback. Use `execution_mode = "on-the-fly"` +for the compact OTF lane. The matching forced-FFT CLI shorthand is +`--fft adjoint --color-accuracy full`; `--fft trace` forces the original trace +path. Both require an explicit NLC/full accuracy setting (on the command line +or in the card). Use `--color-contraction direct` to opt out. + +## Execution mode and evaluator backend + +The accuracy, contraction, and execution choices on this page describe +ordinary Born evaluation. [Born Correlations](../correlators.md) are a separate +opt-in declaration: pass `CorrelatorConfig` as `correlators=` in Python or use +`generate --correlators declaration.json`. There is no `[correlators]` TOML +section. `color.accuracy` selects LC, NLC or full colour for the correlated +matrix contraction; all three retain complete underlying amplitudes. The +path records direct contraction, compiled execution, and numerical current +reuse off. Conflicting requested settings +are retained in provenance with those explicit adjustments. + +Correlated generation rejects partial colour/helicity coverage and append +mode. Numerical spin vectors are supplied later through the Python runtime, +not through the generation card or declaration file. + +```toml +[evaluator] +execution_mode = "compiled" # recurrence, compiled, eager, on-the-fly +backend = "jit" # jit, asm, cpp +batch_size = 128 + +[evaluator.jit] +optimization_level = 2 +compress = "auto" # true for compiled JIT O2; false for other modes/levels +``` + +### Execution modes + +| Mode | What is stored in the process artifact | +| --- | --- | +| `recurrence` | Compact current-recursion schedules over prepared local kernels. | +| `compiled` | Process-wide stage evaluators compiled during generation. | +| `eager` | Compact DAG invocation tables over prepared local kernels. | +| `on-the-fly` | A compact process seed; the selected LC query family, or contracted NLC/full family, is constructed on first use. | + +`recurrence` is the default. Eager, recurrence, and on-the-fly require a +compatible prepared `.pyamplicol-model` kernel pack; `built-in-sm` selects the +packaged JIT O2 pack automatically. Raw JSON/UFO model IR normally uses +`compiled` unless you first create a prepared bundle. + +Ordinary recurrence, compiled, and eager generation may fix +`process.selected_color_sector_ids`, `process.selected_source_helicities`, or +both at generation time when a deliberately specialized artifact is useful. +Omit them to retain reusable runtime selectors. On-the-fly keeps selection at +runtime instead: its last warmed family is retained until another selector is +requested. + +For reusable contracted recurrence artifacts, generation persists one shared +all-helicity physical-colour plan plus an exact per-helicity dispatch sidecar. +Cold selector binding copies only the dependency-closed active row groups; +warmed evaluation does not scan helicity-support masks. This companion belongs +to recurrence execution. On-the-fly instead constructs the requested family +from its compact process seed on first use and caches that family; compiled and +eager artifacts retain their own execution layouts. + +On-the-fly is native binary64 only. LC retains physical flow and helicity +selection without materializing either LC layout. NLC and full colour expose +one contracted color component, accept helicity selection, and reject LC-flow +selectors. Their cold family is correctness-oriented and may become +impractical at high multiplicity. For on-the-fly generation, +`evaluator.optimization.cores` requests query-construction workers for the cold +warm-up; it does not make steady-state numerical evaluation use that many +threads. See [Generation Modes and Evaluators](generation-modes-and-evaluators.md) +for the distinct lifecycle of each mode. + +### Backends + +| Backend | Notes | +| --- | --- | +| `jit` | Default direct SymJIT application. Compiled O1/O2 artifacts are portable on supported 64-bit little-endian hosts; prepared packs use exact O2. | +| `asm` | Target-native Symbolica assembly evaluator. | +| `cpp` | Target-native generated C++ evaluator. | + +Explicit JIT O0/O3 and ASM/C++ artifacts remain target-native. See +[Artifacts and Portability](artifacts-and-portability.md). + +## Generation validation + +```toml +[generation.validation] +enabled = true +samples = 2 +seed = 12345 +relative_tolerance = 1e-12 +absolute_tolerance = 1e-300 +post_build_validation = false +``` + +Generation validation checks symbolic/numerical construction before +publication. Structural artifact checks always run. The optional +`post_build_validation` reopens the finished artifact and evaluates it again; +it is disabled by default because its cost can dominate large process builds. +Enable it when you explicitly want that additional runtime smoke: + +```console +pyamplicol generate --card run.toml \ + --set generation.validation.post_build_validation=true +``` + +## Numerical current reuse + +Certified current-relation reuse is an opt-in optimization. It is off by +default because its high-precision probes are costly at generation time; +enable it to search for additional reuse. It prefers exact +structural proofs and may apply independently verified high-precision equal, +opposite, or zero relations. The artifact records the evidence, tolerances, +probe derivation, replay identity, and whether a structural proof was present. + +This configurable discovery pass applies to ordinary recurrence, compiled, +and eager generation. On-the-fly uses its compact source projection instead +and does not run relation discovery. Correlated generation always disables +this pass to preserve arbitrary external vector-source dependence. + +`diagnostic` records deterministic candidates and their exact replay without +changing the generated evaluator. `certified-reuse` may apply a relation only +after its independent verification pass succeeds; it emits one warning when +an applied relation has numerical evidence but no structural proof. Malformed, +non-finite, unstable, or stale evidence fails closed and is never reused. + +Discovery and application are reported separately. For contracted NLC/full-colour +recurrence, independently verified equal and zero-current relations can be +applied even when the same pass also finds opposite-current relations. The +opposite relations remain recorded but are not applied in that layout; their +currents retain their original computation. Existing helicity/flow-selector +restrictions still apply. No relation search is deferred to the first evaluation. + +Enable the search with: + +```console +pyamplicol generate --card run.toml --numerical-current-reuse +``` + +Or in TOML: + +```toml +[generation.relation_discovery] +mode = "certified-reuse" # off (default), diagnostic, certified-reuse +``` + +`--no-numerical-current-reuse` restates the default and overrides a card that +enables the search. + +The exhaustive probe, precision, seed, and tolerance fields are documented in +`all_options.toml`. + +## Evaluation card + +```toml +schema_version = 1 +action = "evaluate" + +[evaluation] +artifact = "artifacts/pp_zjj" +process = "d d~ > g z g" +precision = 16 +resolved = true +model_parameters = "data/model_parameters.json" +momenta = "data/pp_zjj_momenta.json" + +[output] +format = "human" # programmatic/config provenance default +color = "auto" # auto, always, never +progress = "off" # auto, tty, log, off +``` + +`helicity_ids` and `color_flow_ids` may be added under `[evaluation]`. Empty +lists mean complete retained coverage. + +The model-parameter card is a flat JSON object: + +```json +{ + "aS": 0.117, + "MZ": 91.1876, + "complex_parameter": [1.0, -0.25] +} +``` + +Values must be finite real numbers or `[real, imaginary]` pairs. Direct +parameter overrides are applied after the card and win atomically. + +## Profiling card + +Run cards retain `action = "benchmark"`; the preferred direct CLI spelling is +`pyamplicol profile`: + +```toml +action = "benchmark" + +[evaluation] +artifact = "artifacts/pp_zjj" +process = "d d~ > g z g" +momenta = "data/pp_zjj_momenta.json" + +[benchmark] +target_runtime = 1.0 +batch_size = 128 +precision = 16 +warmup_runs = 2 +minimum_samples = 5 +color_flow_ids = ["1"] +``` + +With both benchmark selector lists empty, LC profiling infers the stored +layout's deterministic hot workload. Explicit subsets and selected-axis lists +override that default; a complete summed-axis list is normalized to equivalent +omission. A valid shape outside the optimized layout emits at most one pre-loop +warning per loaded process. + +See [Runtime and Selectors](runtime-and-selectors.md) for evaluation and profiling +semantics. + +## Output and progress + +```toml +[output] +format = "human" +color = "auto" +progress = "auto" +log_level = "info" +``` + +- The `pyamplicol` CLI always produces aligned colored terminal tables unless + the invocation includes `--json`. The run-card `format` field remains part + of the typed configuration used by the Python API and recorded provenance; + it does not silently switch CLI stdout to JSON. +- `pyamplicol ... --json` reserves standard output for stable, uncolored, + machine-readable data. +- `auto` progress uses a TTY progress bar interactively and rate-limited log + messages otherwise. +- `Ctrl-C` during profiling retains completed timing blocks and marks the result + partial. + +## Resolve configuration from Python + +```python +from pathlib import Path + +from pyamplicol.config import resolve_config + +resolution = resolve_config( + { + "schema_version": 1, + "action": "generate", + "model": {"source": "models/json/sm/sm.json"}, + "process": {"entries": [{"expression": "d d~ > z g"}]}, + "generation": {"output": "artifacts/builtin_ddbar_to_zg"}, + }, + base_dir=Path("pyamplicol-examples"), + overrides=("generation.workers=2",), +) +print(resolution.effective.generation.output) +``` + +For a mapping, relative paths use `base_dir`; without it they use the current +working directory. Values supplied through `--set` or `overrides=` use TOML +syntax, so quote strings containing spaces. + +## Further reading + +- [Generation Modes and Evaluators](generation-modes-and-evaluators.md) +- [LC workloads and execution modes](lc-workloads-and-execution-modes.md) +- [Models and Processes](models-and-processes.md) +- [Runtime and Selectors](runtime-and-selectors.md) +- [Exhaustive current schema](https://github.com/MadGraphTeam/pyamplicol/blob/main/examples/all_options.toml) + + +```{toctree} +:hidden: + +models-and-processes +ufo-model-coverage +process-selection-and-permutations +process-grouping +generation-modes-and-evaluators +command-line-interface +``` diff --git a/pyamplicol/docs/user/examples-gallery.md b/pyamplicol/docs/user/examples-gallery.md new file mode 100644 index 0000000000..13b299f630 --- /dev/null +++ b/pyamplicol/docs/user/examples-gallery.md @@ -0,0 +1,340 @@ +--- +title: "Examples Gallery" +nav_order: 2 +parent: "Installation" +--- + + +# Examples Gallery + +The wheel ships runnable TOML cards, Python programs, model/data resources, and +native consumers. Copy them into an ordinary directory before experimenting: + +```console +pyamplicol examples copy ./pyamplicol-examples --force +cd pyamplicol-examples +``` + +Paths inside a card are resolved relative to that card, so the copied workspace +can be moved without referring back to the installation. + +> [!IMPORTANT] +> Keep the environment containing pyAmpliCol activated, or invoke its +> executables by explicit path. Generated native drivers discover the SDK +> through `rusticol-config` from that same environment. + +## See what is available + +```console +pyamplicol examples list +``` + +The command prints a colored table with each card stem, action, and one-line +description. Use `--json` when a script needs the same inventory as stable, +uncolored machine-readable output: + +```console +pyamplicol examples list --json +``` + +A card can also run in a versioned private cache workspace: + +```console +pyamplicol examples run evaluate_total +``` + +`examples run` accepts the stem shown by `examples list`; it is not an +arbitrary TOML path. Use the copied workspace when you want to edit cards, +inspect artifacts, or run several related steps. + +## The four-command tour + +The primary sequence generates a portable multiprocess `p p > Z j j` artifact +from the serialized UFO Standard Model, then evaluates and profiles one +subprocess: + +```console +pyamplicol generate_pp_zjj_from_ufo_sm.toml +pyamplicol evaluate_total.toml +pyamplicol evaluate_resolved.toml +pyamplicol benchmark.toml +``` + +What each card demonstrates: + +| Card | Demonstration | +| --- | --- | +| `generate_pp_zjj_from_ufo_sm.toml` | JSON model input, multiparticle expansion, compiled JIT O2, API bundle | +| `evaluate_total.toml` | Optimized total, model-parameter card, reordered process expression | +| `evaluate_resolved.toml` | Physical helicity/LC-flow components and explicit sum | +| `benchmark.toml` | One-second calibrated profile of the optimized total | + +The generation request finds eight candidates, one per side-permutation class, +stores seven tree-level representatives, and reports the loop-induced +`g g > Z g g` class as omitted. Evaluation selects the public +ordering `d d~ > g z g`; the stored representative is reused automatically. + +The three result cards use colored human tables by default. Add `--json` for +machine-readable stdout. + +## Run cards by topic + +| File | Purpose | Expected cost | +| --- | --- | --- | +| `generate_pp_zjj_from_ufo_sm.toml` | Primary portable multiprocess artifact | Short functional example | +| `evaluate_total.toml` | Fully summed primary subprocess | Fast after generation | +| `evaluate_resolved.toml` | Resolved helicity/color tensor | Fast after generation | +| `benchmark.toml` | Short primary runtime profile | About the configured one-second target plus setup | +| `builtin_sm_lc.toml` | Built-in SM recurrence JIT O2, LC | Small generation example | +| `builtin_sm_nlc.toml` | Built-in SM recurrence JIT O2, contracted NLC | Small generation example | +| `builtin_sm_full.toml` | Built-in SM compiled C++, contracted full color | Requires a C++ toolchain during generation | +| `builtin_sm_adjoint_fft.toml` | Pure-gluon full color in the adjoint DDM FFT basis | Recurrence; also supports an on-the-fly override | +| `builtin_sm_heft.toml` | Packaged scalar HEFT `g g > H g g`, one insertion, recurrence JIT O2 with adjoint DDM FFT | Small HEFT workflow; no external UFO required | +| `builtin_sm_eager.toml` | Built-in SM eager execution with wheel-owned prepared kernels | Small eager example | +| `builtin_sm_on_the_fly.toml` | Built-in SM compact OTF LC artifact | Small generation example; first selected family is built at runtime | +| `otf_pp_zjj.toml` | `p p > Z j j` OTF generation, one-point warm-up, and profiling | Short guided OTF workflow | +| `generate_pp_4j_flavor_blind.toml` | Flavour-blind `p p > j j j j` at `QED = 0`: 23 representatives for 486 subprocesses | About 30 seconds | +| `inspect_pp_4j.toml` | Artifact summary and process-group map | Fast after generation | +| `evaluate_pp_4j_member.toml` | Non-generated member `u~ g > u~ g g g` routed through its representative | Fast after generation | +| `group_pp_4j.toml` | Numerical `group-processes` analysis of the representatives | About a second after generation | +| `process_set_mixed_multiplicity.toml` | Named 2-to-2 and 2-to-3 processes in one artifact | Small multiprocess example | +| `external_ufo_sm.toml` | Trusted UFO-directory loading | Imports trusted model Python | +| `external_json_scalars.toml` | Repeated scalar particles and a contact model | Small external-JSON example | +| `external_json_scalar_gravity.toml` | Proven massless spin-2 path | Small external-JSON example | +| `qq_z6g_recurrence_jit_o2.toml` | `q q~ > Z + 6g`, recurrence prepared JIT O2 | Substantial | +| `qq_z6g_compiled_jit_o3.toml` | Same process, compiled process-local JIT O3 | Substantial and host-specific | +| `qq_z6g_eager_jit_o2.toml` | Same process, eager prepared JIT O2 | Substantial | +| `benchmark_z6g_single_flow_helicity_sum.toml` | Reusable topology-replay selector workload | Substantial | +| `benchmark_z6g_all_flows_single_helicity.toml` | Reusable all-flow-union selector workload | Substantial | +| `benchmark_z6g_generation_specialized_flow_helicity_sum.toml` | Generation-selected flow baseline | Substantial | +| `benchmark_z6g_generation_specialized_all_flows_single_helicity.toml` | Generation-selected helicity baseline | Substantial | +| `all_options.toml` | Exhaustive commented schema reference | Reference only; not runnable via `examples run` | + +Use the primary Z+jet sequence, not a six-gluon card, as an installation smoke. + +## Packaged scalar HEFT + +Generate a full-colour Higgs-plus-two-gluon artifact without an external UFO: + +```console +pyamplicol generate --card builtin_sm_heft.toml +pyamplicol inspect artifacts/builtin_sm_heft +``` + +The card selects the packaged `built-in-sm-heft` model and explicitly limits +the effective coupling order to `HIG = 1`. Recurrence uses the wheel-owned JIT +O2 prepared kernels. See +[Models and Processes](models-and-processes.md#built-in-scalar-heft-model) for +the compiled, eager, on-the-fly, Python API, and trusted-UFO variants. + +## Process grouping with `p p > j j j j` + +Four cards show [process grouping](process-grouping.md) on the 486 +five-flavour subprocesses of pure-QCD `p p > j j j j` in the built-in SM: + +```console +pyamplicol generate_pp_4j_flavor_blind.toml +pyamplicol inspect_pp_4j.toml +pyamplicol evaluate_pp_4j_member.toml +pyamplicol group_pp_4j.toml +``` + +| Step | Equivalent command | Result | +| --- | --- | --- | +| Generate | `pyamplicol generate "p p > j j j j" artifacts/pp_4j_flavor_blind --model built-in-sm --flavor-blind --max-coupling-order QED=0` | 23 representatives at LC; 463 members, 140 of them conjugated, in `process-groups.json`; about 30 seconds | +| Inspect | `pyamplicol inspect artifacts/pp_4j_flavor_blind` | Generated processes and the process-group map | +| Evaluate a member | `pyamplicol evaluate artifacts/pp_4j_flavor_blind --process "u~ g > u~ g g g" --momenta data/pp_4j_momenta.json` | Routed through `d g > d g g g` with relabelling and charge conjugation | +| Group numerically | `pyamplicol group-processes artifacts/pp_4j_flavor_blind` | No new relation: the 23 classes are distinct | + +`u~ g > u~ g g g` was never generated, yet its value agrees with a separately +generated copy to about `1e-14`. Any of the 486 subprocesses, in any leg order +within each side, can be selected in the same way, from the CLI, Python, or +the generated API drivers. Flavour-blind generation assumes equal matrix +elements within a class; see +[When to use it](process-grouping.md#when-to-use-it) before using it with +electroweak couplings. + +## Three materialized execution modes on one process + +The matched `Z + 6g` cards keep model, process, color accuracy, and LC layout +fixed while changing the execution mode: + +```console +pyamplicol generate --card qq_z6g_recurrence_jit_o2.toml +pyamplicol profile --card qq_z6g_recurrence_jit_o2.toml + +pyamplicol generate --card qq_z6g_compiled_jit_o3.toml +pyamplicol profile --card qq_z6g_compiled_jit_o3.toml + +pyamplicol generate --card qq_z6g_eager_jit_o2.toml +pyamplicol profile --card qq_z6g_eager_jit_o2.toml +``` + +Each writes a separate artifact. These are performance/acceptance workloads, +not quick examples; generation can take significant time and memory. + +See [Generation Modes and Evaluators](generation-modes-and-evaluators.md) before +interpreting their differences. + +## On-the-fly warm-up and profile + +The OTF `p p > Z j j` example keeps generation compact, explicitly warms one +LC flow summed over helicities, and then profiles the retained workload: + +```console +pyamplicol generate --card otf_pp_zjj.toml +python python/otf_pp_zjj_warm_up.py +pyamplicol profile --card otf_pp_zjj.toml +``` + +The Python warm-up takes exactly one double-precision phase-space point and +draws live progress while the selected family is constructed. Its colored +summary reports warm-up time, query counts, resident memory, and the matrix +element. The independent profiler then measures the configured 128-point +steady-state batch. See +[LC workloads and execution modes](lc-workloads-and-execution-modes.md#the-otf-warm-state-lifecycle) +for the cache lifecycle and native-language equivalents. + +Completed OTF preparation can also be retained between programs with +`runtime.save(path)` and `runtime.load_cache(path)`. The +[four-gluon save/restore example](runtime-and-selectors.md#saving-an-otf-warm-cache) +shows generation, a complete input point, and a cache-reuse check; the +[native examples](native-apis.md#saving-and-restoring-an-otf-cache) cover every SDK. + +## LC selector-layout examples + +The two reusable-selector cards retain complete helicity and physical-flow +coverage: + +```console +pyamplicol generate --card benchmark_z6g_single_flow_helicity_sum.toml +pyamplicol profile --card benchmark_z6g_single_flow_helicity_sum.toml + +pyamplicol generate --card benchmark_z6g_all_flows_single_helicity.toml +pyamplicol profile --card benchmark_z6g_all_flows_single_helicity.toml +``` + +The first uses default `topology-replay`, optimized for one selected flow and a +helicity sum. The second uses `all-flow-union`, optimized for all flows at one +selected helicity. Change selectors without regenerating: + +```console +pyamplicol profile \ + --card benchmark_z6g_single_flow_helicity_sum.toml \ + --color-flow 2 + +pyamplicol profile \ + --card benchmark_z6g_all_flows_single_helicity.toml \ + --helicity h:-1,+1,-1,+1,+1,-1,+1,-1,+1 +``` + +## Typed Python generation + +Plan first, then generate: + +```console +python python/typed_generation.py artifacts/pp_zjj_typed --plan-only +python python/typed_generation.py artifacts/pp_zjj_typed +``` + +The script compiles the copied serialized model, constructs explicit `p` and +`j` multiparticle definitions, and calls `Generator.plan()` or +`Generator.generate()`. The generated artifact includes the same standalone API +bundle as a CLI build. + +Evaluate through the typed runtime: + +```console +python python/runtime_evaluation.py \ + artifacts/pp_zjj data/pp_zjj_momenta.json \ + --process 'd d~ > g z g' \ + --parameters data/model_parameters.json \ + --set-parameter aS=0.1165 +``` + +The JSON result reports the resolved shape and flattened values, explicit +resolved sum, optimized total, and selected physics-axis IDs. + +Profile the same process: + +```console +python python/benchmark.py artifacts/pp_zjj \ + --process 'd d~ > g z g' \ + --momenta data/pp_zjj_momenta.json +``` + +`python/external_models.py` demonstrates typed JSON and trusted-UFO +`ModelSource` construction: + +```console +python python/external_models.py models/json/sm/sm.json models/ufo/sm +``` + +## Generated five-language API bundle + +After the primary generation, run the same public process ordering in every +generated driver: + +```console +python artifacts/pp_zjj/API/python/check_standalone.py \ + --process 'd d~ > g z g' --set-parameter aS 0.117 0 --json + +make -C artifacts/pp_zjj/API/c run \ + ARGS='--process "d d~ > g z g" --set-parameter aS 0.117 0 --json' + +make -C artifacts/pp_zjj/API/rust run \ + ARGS='--process "d d~ > g z g" --set-parameter aS 0.117 0 --precision 16 --json' + +make -C artifacts/pp_zjj/API/cpp run \ + ARGS='--process "d d~ > g z g" --set-parameter aS 0.117 0 --json' + +make -C artifacts/pp_zjj/API/fortran run \ + ARGS='--process "d d~ > g z g" --set-parameter aS 0.117 0 --json' +``` + +All drivers load one bundled point, evaluate every resolved component, sum +those components, and compare the sum to the optimized total. They use the +wheel-owned static Rusticol SDK discovered through `rusticol-config`. + +Every driver also accepts `--kinematics PATH` and `--model-parameters PATH`. +The full shape, precision, ordering, and precedence rules are documented in +[Native APIs](native-apis.md) and +[Process Selection and Permutations](process-selection-and-permutations.md). + +## Hand-written C++ and Fortran consumers + +The `native/` directory shows SDK use independent of generated driver sources: + +```console +make -C native +native/runtime_cpp artifacts/pp_zjj p_p_to_z_j_j_3 \ + data/model_parameters.json +native/runtime_fortran artifacts/pp_zjj p_p_to_z_j_j_3 \ + data/model_parameters.json +``` + +Both programs apply an `aS` override, evaluate one five-particle point, and +verify that resolved components reproduce the total. + +## Modify an example safely + +Prefer a new output directory when changing execution mode, backend, color +accuracy, or flow layout: + +```console +pyamplicol generate \ + --card benchmark_z6g_single_flow_helicity_sum.toml \ + --execution-mode eager \ + --set generation.output=artifacts/uubar_z6g_eager_experiment +``` + +Use `--set generation.mode=replace` only when you explicitly want to replace +an existing artifact transactionally. + +## See also + +- [Quick Start](quick-start.md) +- [Command-Line Interface](command-line-interface.md) +- [Python API](python-api.md) +- [Native APIs](native-apis.md) +- [Profiling and Benchmarking](profiling-and-benchmarking.md) diff --git a/pyamplicol/docs/user/fullcolor-fft-profiling.md b/pyamplicol/docs/user/fullcolor-fft-profiling.md new file mode 100644 index 0000000000..f1ddc34aae --- /dev/null +++ b/pyamplicol/docs/user/fullcolor-fft-profiling.md @@ -0,0 +1,277 @@ +--- +title: "FullColor FFT Profiling" +nav_order: 3 +parent: "Profiling and Benchmarking" +--- + + +# FullColor FFT Profiling + +The source checkout contains a thin, resumable orchestrator for comparing +exact full-colour matrix-element implementations. It delegates physics +generation, profiling, validation, plotting, and PDF assembly to the existing +pyAmpliCol developer tools; the driver owns only selection, scheduling, +per-child resource guards, and persistent progress. + +This workflow is distinct from selecting +`color.contraction = "symmetric-group-fft"` in an ordinary generated artifact: + +- **symmetric-group FFT contraction** is a pyAmpliCol runtime algorithm for + exact NLC/full colour in recurrence and on-the-fly execution; +- **Reference FFT** is the optional external `AllGluonsMultipletFFT` comparison + checkout; +- **the profiling orchestrator** measures those implementations alongside + direct pyAmpliCol, original AmpliCol, and MadGraph where applicable. + +The orchestrator is source-checkout-only. It is intended for a workstation or +cluster node with explicit CPU, memory, and wall-time budgets. + +For ordinary NLC/full recurrence or on-the-fly generation, the defaults +`contraction="auto"` and `fft_basis="adjoint"` try exact FFT, with direct +fallback for unsupported plans. Select `--color-accuracy full` to use this +policy. This adaptively selects certified two-anchor DDM for pure Yang–Mills +trees and single-insertion HEFT (`HIG = 1`), retains fundamental chains for +quarks, and uses exact trace tensors where DDM is unproven. Compare the saved +`fft_basis_selection` as well as the request: requesting adjoint does not imply +that every process used DDM or gained a smaller basis. LC, compiled/eager +execution, and correlators retain direct/trace. Use `--color-contraction direct` +to opt out, or `--fft adjoint` / `--fft trace` to force FFT and retain errors +for unsupported plans. Historical curves retain their saved basis settings +and must not be relabelled. In the benchmarked pure-gluon family the +adjoint basis was faster than trace for six or more external gluons; compare +setup and warmed runtime separately for other workloads. + +## Prepare a profiling checkout + +Enter the repository's Nix environment when available, then request only the +optional references needed by the scan: + +```console +nix develop +just dev-install --with-reference-fft --with-legacy-amplicol +``` + +`--with-reference-fft` clones the public +`rikkert-frederix/AllGluonsMultipletFFT` revision pinned by pyAmpliCol. +`--with-legacy-amplicol` clones the pinned original-AmpliCol comparison. +Both are omitted by a default `just dev-install`; either opt-in also installs +the PDF/plotting dependencies into `.venv`. If a managed Reference FFT checkout +predates the public repository, add `--update` once so its `origin` is migrated. + +MadGraph is not installed by `dev-install`. Pass a complete installation root +containing `bin/mg5_aMC` and `VERSION` with `--madgraph-root`. + +## Select process families and curves + +The scan has two independent process families: + +| Selector | Process family | +| --- | --- | +| `gg` | `g g > g g + gluons` | +| `ddbar` | `d d~ > d d~ + gluons` | + +`--lines` accepts grouped comparison series: + +| Selector | Curves requested | +| --- | --- | +| `reference-fft` | External pure-gluon Reference FFT | +| `amplicol` | Original AmpliCol | +| `pyamplicol-recurrence` | pyAmpliCol recurrence direct and FFT | +| `pyamplicol-otf` | pyAmpliCol on-the-fly direct and FFT | +| `madgraph` | Same-host MadGraph standalone | + +For a targeted top-up, use a concrete pyAmpliCol line: +`recurrence-direct`, `recurrence-fft`, `otf-direct`, or `otf-fft`. +Required authority/baseline shards are added automatically. For example, a +pyAmpliCol pure-gluon comparison may require Reference FFT, while a `ddbar` +comparison may require original AmpliCol. + +## Small Reference FFT example + +This fills only Reference FFT for the pure-gluon family at final-state +multiplicities 2, 3, and 4: + +```console +.venv/bin/python tools/fft_profiling/fft_profiling.py \ + --output /shared/path/fft-reference-gg \ + --families gg --lines reference-fft \ + --multiplicities 2 3 4 \ + --cores 10 \ + --memory-limit-gib 100 --time-limit-seconds 36000 \ + --reference-fft-root "$PWD/dependencies/checkouts/reference-fft" +``` + +Use `--dry-run` first to print the deterministic plan without creating the +output or starting workers. + +## Complete cluster scan + +The following command requests both process families, every grouped series, +ten scheduler cores, a 100 GiB per-child memory guard, and a 10-hour per-cell +time limit in one explicit output directory: + +```console +.venv/bin/python tools/fft_profiling/fft_profiling.py \ + --output /shared/path/pyamplicol-fft-profile \ + --families gg ddbar \ + --lines reference-fft amplicol pyamplicol-recurrence pyamplicol-otf madgraph \ + --multiplicities 2 3 4 5 6 7 8 9 \ + --cores 10 --candidate-cores 1 \ + --memory-limit-gib 100 --time-limit-seconds 36000 \ + --amplicol-root "$PWD/dependencies/checkouts/legacy-amplicol" \ + --reference-fft-root "$PWD/dependencies/checkouts/reference-fft" \ + --madgraph-root /shared/path/MG5_aMC \ + --build-amplicol +``` + +`--build-amplicol` permits the first fixed-helicity AmpliCol shard to build its +probe. It can remain on subsequent resumptions; an already valid probe is +reused. + +Resource options have deliberately narrow meanings: + +- `--cores` is the total scheduler slot budget across active children; +- `--candidate-cores` is one pyAmpliCol child's slot claim and exact + `evaluator.optimization.cores` setting; +- `--memory-limit-gib` and `--time-limit-seconds` are strict per-child/per-cell + limits, not aggregate campaign limits; +- the time limit covers generation/setup and runtime measurement; +- `--target-seconds` controls calibrated runtime sampling after setup and is + not the cell cutoff. + +The progress headline reports the active cell, aggregate live RSS, and occupied +core slots. A child crossing its own guard is terminated and retained as a +cutoff result; other cells continue. + +## Fixed helicity and complete helicity sums + +The default workload evaluates one shared known-nonzero helicity from artifacts +that retain general helicity coverage. Add `--compare-helicity-sums` for the +independent complete physical-helicity sum: + +```console +.venv/bin/python tools/fft_profiling/fft_profiling.py \ + --output /shared/path/pyamplicol-fft-profile-helicity-sum \ + --compare-helicity-sums \ + --families gg ddbar \ + --lines reference-fft amplicol pyamplicol-recurrence pyamplicol-otf madgraph \ + --multiplicities 2 3 4 5 6 \ + --cores 10 --candidate-cores 1 \ + --memory-limit-gib 100 --time-limit-seconds 36000 \ + --amplicol-root "$PWD/dependencies/checkouts/legacy-amplicol" \ + --reference-fft-root "$PWD/dependencies/checkouts/reference-fft" \ + --madgraph-root /shared/path/MG5_aMC +``` + +Fixed-helicity and helicity-sum measurements have different workload +identities and must use separate output directories. MadGraph's summed lane +uses its generated `SMATRIX(P,ANS)` entry point with `USERHEL=-1`; it does not +reuse fixed-helicity measurements. + +## Resume, retry, overwrite, or refresh + +The default behavior is resumable and additive. Repeating a command with the +same `--output`: + +- recovers unfinished prior requests; +- skips completed cells; +- adds newly selected families, lines, and multiplicities to the persistent + history; +- refreshes plots as new completed evidence becomes available. + +`--resume` is an explicit alias for that default. More forceful choices are +scoped to the current `--families`, `--lines`, and `--multiplicities`: + +| Option | Effect | +| --- | --- | +| `--retry` | Rerun selected failed or skipped cells. | +| `--overwrite` | Rerun every selected cell; keep its old result until that worker actually starts. | +| `--refresh` | Delete the exact recognized output workspace and restart it from scratch. | + +Use targeted overwrite to refine a measured subset without discarding the +rest of the campaign: + +```console +.venv/bin/python tools/fft_profiling/fft_profiling.py \ + --output /shared/path/pyamplicol-fft-profile \ + --families ddbar --lines otf-fft \ + --multiplicities 6 --overwrite \ + --cores 10 --memory-limit-gib 100 --time-limit-seconds 36000 +``` + +`--refresh` is intentionally conservative: it refuses an unrecognized path and +will not remove a workspace while a coordinated MadGraph writer is active. + +Inspect machine-readable progress without launching work: + +```console +.venv/bin/python tools/fft_profiling/fft_profiling.py \ + --status --output /shared/path/pyamplicol-fft-profile +``` + +## Render what is currently available + +The driver publishes immutable snapshots during a scan. Rendering freezes the +latest already-published snapshot immediately and never waits for active +workers: + +```console +.venv/bin/python tools/fft_profiling/fft_profiling.py \ + --render --output /shared/path/pyamplicol-fft-profile +``` + +For a custom output, the saved manifest determines whether the workload is +fixed-helicity or helicity-summed; repeating `--compare-helicity-sums` is not +required. The current PDF is written below: + +```text +RUN/render/current/summary_plots_final.pdf +RUN/render/current/summary_plots_final_helicity_sum.pdf +``` + +Only the filename matching the run's workload is produced. The same render +contains six PNGs: setup time, warmed runtime, and peak RSS for each process +family. + +Without a custom output, select the two canonical workspaces explicitly: + +```console +.venv/bin/python tools/fft_profiling/fft_profiling.py --render +.venv/bin/python tools/fft_profiling/fft_profiling.py \ + --render --compare-helicity-sums +``` + +Generation selectors (`--lines`) decide what work enters the campaign. The +render-only `--main-include-lines`, `--main-veto-lines`, +`--ratio-include-lines`, and `--ratio-veto-lines` options only control which +retained series appear in the main and ratio panels. + +## Interpret setup and warmed runtime + +The plotted *setup time* is method-specific: + +- pyAmpliCol includes artifact generation, fresh load, first requested + evaluation, and OTF family construction where applicable; +- Reference FFT includes its build, initialization, and first pass; +- AmpliCol includes its process/colour-object or raw-library preparation for + the selected workload. + +Warmed runtime is measured separately after those setup boundaries. The exact +symmetric-group FFT is not guaranteed to beat direct contraction for every +process: small certified permutation subgroups or large direct residuals can +produce similar scaling. + +Published rolling snapshots: + +- [selected-helicity FullColor FFT PDF](https://github.com/MadGraphTeam/pyamplicol/blob/main/docs/performance_reports/summary_plots_final.pdf) +- [complete-helicity-sum FullColor FFT PDF](https://github.com/MadGraphTeam/pyamplicol/blob/main/docs/performance_reports/summary_plots_final_helicity_sum.pdf) + +These are measurement-host results rather than release-CI benchmarks. Raw +campaign workspaces remain local. + +## See also + +- [Configuration](configuration.md#color-accuracy-and-lc-layout) — enable exact FFT contraction in a generated artifact. +- [Command-Line Interface](command-line-interface.md#generate) — direct generation example. +- [Performance Reports](../performance_reports/README.md) — published frontiers and measurement boundaries. +- [Installation](installation.md#contributor-installation) — optional profiling references. diff --git a/pyamplicol/docs/user/generation-modes-and-evaluators.md b/pyamplicol/docs/user/generation-modes-and-evaluators.md new file mode 100644 index 0000000000..6dbd2fb749 --- /dev/null +++ b/pyamplicol/docs/user/generation-modes-and-evaluators.md @@ -0,0 +1,449 @@ +--- +title: "Generation Modes and Evaluators" +nav_order: 4 +parent: "Configuration" +--- + + +# Generation Modes and Evaluators + +For ordinary generation, pyAmpliCol makes two independent choices when it +builds a process artifact: + +1. the **execution mode** describes how process-wide work is organized; +2. the **evaluator backend** describes how local symbolic expressions become + executable numerical kernels. + +Keeping these axes separate makes it possible to compare algorithms without +changing the model, process, color approximation, or public runtime API. + +## Choose an execution mode + +Set the mode in TOML: + +```toml +[evaluator] +execution_mode = "recurrence" +backend = "jit" +``` + +or on the command line: + +```console +pyamplicol generate "u u~ > Z g g" artifacts/uubar_zgg \ + --model built-in-sm \ + --execution-mode recurrence \ + --backend jit +``` + +Ordinary recurrence, compiled, and eager can deliberately specialize generation +to one or more flow IDs, helicity IDs, or both with +`process.selected_color_sector_ids` and +`process.selected_source_helicities`. This reduces the reusable coverage of +the resulting artifact and is useful when the same narrow workload will be +run repeatedly. Omit those fields when runtime selection is wanted. +On-the-fly does not use generation-time specialization: `warm_up(...)` builds +the runtime-selected family and retains only the most recently selected one. + +| Mode | What generation writes | Best starting point | Model requirement | +| --- | --- | --- | --- | +| `recurrence` | Compact current-recursion schedules over prepared local kernels | Default; scalable general-purpose generation and runtime | A compatible prepared model bundle | +| `compiled` | Process-local stage DAG evaluators | Self-contained compiled processes and direct backend experiments | Portable model IR is sufficient | +| `eager` | Compact process DAG invocation tables over prepared local kernels | Compare process DAG scheduling while reusing model kernels | A compatible prepared model bundle | +| `on-the-fly` | A compact process seed; the requested query family is built on first use | High-multiplicity LC single-flow/helicity-sum work with an explicit cold warm-up | A compatible prepared model bundle | + +### Recurrence + +Recurrence is the global default. Generation writes compact current schedules +and references only the prepared model kernels needed by the process. It is the +recommended first choice for the wheel-owned built-in Standard Model: + +```console +pyamplicol generate "u u~ > Z g g g" artifacts/uubar_z3g_recurrence \ + --model built-in-sm +``` + +The installed wheel supplies portable JIT-O2 prepared bundles for both +`built-in-sm` and `built-in-sm-heft`, so no explicit pack path is needed for +either model. A raw external JSON or UFO model does not have prepared kernels; +compile a `.pyamplicol-model` bundle first. + +Recurrence has two runtime tiling controls: + +```toml +[evaluator.recurrence] +point_tile_size = 1024 +workspace_mib = 256 +``` + +`workspace_mib` is a soft batching target, not a memory limit. The runtime may +reduce the effective point tile to approach this target, but always allocates +the workspace needed to evaluate at least one point, even when that exceeds +the target. It does not increase the requested tile. Use an external memory +watchdog when a strict limit on total process memory is required. + +For topology replay, generation groups current-building operations with similar +helicity coverage while keeping shared-input groups together. The row order is +stored in the process output; selecting one helicity does not rebuild or +optimise the currents. Momentum sums follow their stored term order across a +batch of points. These arrangements retain the full generated helicity +coverage and do not introduce an OTF-style first-call preparation step. +For a selected helicity, a current buffer is not cleared separately when its +first retained contribution already initializes it. Other selected schedules +keep the required clears. + +Recurrence outputs generated with `color.contraction = "symmetric-group-fft"` +also retain exact colour coefficients. Double-double and arbitrary-precision +evaluation use a direct contraction of those coefficients, streamed from the +compact FFT representation without constructing a dense matrix. This is an +exact-precision fallback, not a higher-precision FFT, and can be substantially +slower than the native binary64 path at high multiplicity. + +### Compiled + +Compiled mode lowers a process-wide DAG into evaluator stages while generating +the artifact. It can start from portable compiled model IR and is therefore the +primary shipped JSON-model example: + +```console +pyamplicol generate_pp_zjj_from_ufo_sm.toml +``` + +That card compiles seven tree-level representatives for `p p > Z j j` using +process-local JIT O2. The resulting all-JIT-O2 process artifact is portable +across the supported 64-bit little-endian release platforms. + +Select compiled mode explicitly when overriding another card: + +```console +pyamplicol generate --card qq_z6g_recurrence_jit_o2.toml \ + --execution-mode compiled \ + --set generation.output=artifacts/qq_z6g_compiled_override +``` + +Always use a different output when comparing modes; a process artifact is an +immutable executable input, not a directory into which unrelated plans should +be mixed. + +Generation also records which momentum sums the final compiled kernels use. +Evaluation fills only those sums, directly across the point batch, without +searching the expressions on the first call. The list covers every helicity +retained by that compiled output, not just the first runtime selection. +Source-wavefunction information is resolved once per source and point tile, +using the current model parameters, and reused across the points in that tile. +Changing masses or runtime source states therefore remains effective on the +next evaluation without regenerating the process output. + +### Correlated Born generation + +Declaring [Born Correlations](../correlators.md) is an explicit exception to +the independent mode/accuracy choices: the current path selects generic +`compiled` amplitudes, a complete full-colour basis, and `direct` contraction. +Requested LC/NLC/full accuracy selects the inserted colour matrices; requested +settings are preserved alongside the effective generation adjustments. +Recurrence, eager, OTF and FFT correlation executors are not implemented. + +Correlated generation retains complete source dependence without +physical-helicity zero/parity reductions, replay, or numerical current reuse. +Partial generation coverage and append mode are rejected. Correlated +evaluation uses either the Python Symbolica-backed exact executor (including +at `precision=16`) or the independent RustiCol binary64 executor exposed by +the C/C++/Fortran/Rust SDKs. The ordinary native evaluation path remains +available and unchanged. See the linked guide for declarations and runtime +spin vectors. + +### Eager + +Eager mode writes compact DAG invocation tables and applies prepared local +kernels directly at runtime: + +```console +pyamplicol generate "u u~ > Z g g g" artifacts/uubar_z3g_eager \ + --model built-in-sm \ + --execution-mode eager +``` + +Its workspace controls mirror recurrence: + +```toml +[evaluator.eager] +point_tile_size = 1024 +workspace_mib = 256 +``` + +An eager process artifact is standalone: it carries the referenced prepared +kernels and compact invocation data. The model bundle used during generation +is not needed to evaluate that artifact later. + +### On-the-fly + +On-the-fly (OTF) stores a compact process seed and the referenced prepared +kernels instead of materializing a reusable process schedule during +generation. The first explicit `warm_up(...)` or evaluation constructs the +requested query family; later evaluations on the same loaded handle reuse it. +Selecting a different family replaces the previous one. +After construction completes, `runtime.save(path)` and +`runtime.load_cache(path)` can carry that warm state between loaded runtimes +without reconstructing its currents; see +[Saving an OTF warm cache](runtime-and-selectors.md#saving-an-otf-warm-cache). + +This trade is designed around LC with one selected flow summed over all +helicities. The same artifact can also build the all-flow sum for one selected +helicity, so OTF does not use the materialized +`topology-replay`/`all-flow-union` layout setting. Contracted NLC and full +colour are available as low-multiplicity correctness capabilities, but their +cold family can grow too quickly for practical high-multiplicity work. + +OTF evaluation is native binary64 only (`precision=16`) and requires a +prepared model bundle; `built-in-sm` selects the wheel-owned JIT-O2 bundle. +Plan its cold stage explicitly with exactly one phase-space point: + +```python +result = runtime.warm_up( + (point,), + precision=16, + color_flows=(runtime.physics.color_flows[0],), + n_cores=4, + progress=progress, +) +``` + +`n_cores` is an optional positive integer limiting parallel query construction +for this call. Omit it to use `evaluator.optimization.cores` from generation. +Cache merging and finalization remain serial; the override changes neither +future evaluation threading nor cache identity. See the +[warm-up contract](runtime-and-selectors.md#explicit-otf-warm-up) for details. + +The optional progress observer reports process preparation, query-family +construction, family finalization, first evaluation, elapsed time, workers, +and resident memory where available. `runtime.clear()` returns a Python OTF +handle to cold state while retaining the loaded artifact and current model +parameters. Native callers close and reload the handle. See +[LC workloads and execution modes](lc-workloads-and-execution-modes.md#the-otf-warm-state-lifecycle) +for the cache lifecycle and all five API spellings. + +## Choose an evaluator backend + +| Backend | Output | Portability and runtime | +| --- | --- | --- | +| `jit` | Direct SymJIT application | Compiled O1/O2 state can be portable; prepared packs use exact O2; f64 execution is Symbolica-independent | +| `cpp` | Generated and compiled C++ evaluator library | Target-specific executable payload; f64 runtime is Symbolica-independent | +| `asm` | Symbolica assembly evaluator library | Target-specific executable payload; f64 runtime is Symbolica-independent | + +### JIT + +The public JIT defaults are optimization level 2 with mode-aware compression: + +```toml +[evaluator.jit] +optimization_level = 2 +compress = "auto" +``` + +Prepared JIT kernel bundles used by recurrence, eager, and on-the-fly always +use O2 because that is their cross-architecture storage contract. Process-local +compiled JIT O1 and O2 artifacts are marked `portable-64le` only when every +execution leaf is authenticated as compatible O1/O2 JIT state and no +target-specific capability is present. + +Explicit JIT O0 or O3 remains available for host-specific experiments: + +```console +pyamplicol generate --card qq_z6g_compiled_jit_o3.toml +``` + +Those artifacts are target-native. Copying one to another architecture is +expected to fail compatibility checks rather than silently relower it. + +With `compress = "auto"`, compiled JIT O2 uses compression; recurrence, eager, +on-the-fly, and other optimization levels leave it disabled. Explicit `true` +and `false` settings override this default. + +Compression shares repeated arithmetic sequences, trading extra calls for +smaller generated code. Either setting can be faster: larger evaluators can +benefit from the smaller instruction footprint, while smaller ones can spend +more time on those calls. Compare both settings with `profile` for an important +workload. Enable or disable compression explicitly with `--jit-compress` or +`--no-jit-compress`: + +```console +pyamplicol generate ... --jit-compress +``` + +### C++ + +C++ is a process-local compiled backend: + +```toml +[evaluator] +execution_mode = "compiled" +backend = "cpp" + +[evaluator.cpp] +optimization = "O3" +native_arch = false +``` + +Portable C++ code generation is the default. `native_arch = true` opts into +host-native instructions and records the required CPU features in the +artifact. Loaders reject an incompatible target before reading executable +state. Extra compiler flags use a restricted allowlist so an unrecorded ISA +requirement cannot be introduced accidentally. + +### Assembly + +The assembly backend is also target-native and uses the same public Rusticol +runtime surface. Like C++, it is primarily a compiled-mode backend. Generation +requires Symbolica; f64 evaluation of the completed compatible artifact does +not. + +## Prepared model bundles + +A JSON file such as `models/json/sm/sm.json` is portable model IR. It contains +model structure and expressions but no ready local-kernel backend. + +A file ending in `.pyamplicol-model` is a prepared bundle: model IR, exact +expressions, and one compiled local-kernel backend. Recurrence, eager, and +on-the-fly require this prepared boundary. + +Prepare an external JIT-O2 bundle: + +```console +pyamplicol model compile \ + models/json/sm/sm.json models/ufo-sm-jit-o2.pyamplicol-model \ + --backend jit \ + --jit-optimization-level 2 +``` + +Use it in recurrence or eager generation: + +```console +pyamplicol generate "d d~ > z g g g" artifacts/ddbar_z3g_ufo \ + --model models/ufo-sm-jit-o2.pyamplicol-model \ + --execution-mode recurrence +``` + +A missing or incompatible pack fails explicitly; configuration never falls +back from recurrence/eager/on-the-fly to compiled mode. + +## Color accuracy and LC flow layout + +For ordinary Born evaluation, execution mode is independent of color accuracy: + +| Accuracy | Runtime color axis | +| --- | --- | +| `lc` | One physical leading-color flow per resolved color entry | +| `nlc` | One contracted color entry per helicity | +| `full` | One contracted color entry per helicity | + +LC also has two complete-coverage layouts: + +| Layout | Optimized workload | +| --- | --- | +| `topology-replay` | Default: one runtime-selected flow, summed helicities | +| `all-flow-union` | All flows, one runtime-selected helicity | + +Choose union flow explicitly: + +```console +pyamplicol generate \ + --card benchmark_z6g_single_flow_helicity_sum.toml \ + --lc-flow-layout all-flow-union \ + --set generation.output=artifacts/uubar_z6g_all_flow_union +``` + +Both layouts retain all physical LC flows and helicities and support runtime +selectors. `all-flow-union` is LC-only and is incompatible with a +generation-specialized flow/helicity request or truncated color coverage. +OTF retains the same physical LC selector choices but constructs the requested +family at runtime and therefore ignores this materialized-layout setting. + +## Validation and current reuse + +Generation has three distinct checks; they should not be confused: + +1. **Artifact writing** always validates the schema, confined references, + declared payload sizes, and digests. +2. **Current relation discovery** uses deterministic high-precision probes and + independent verification to apply certified current reuse when structural + proof is unavailable, subject to the execution layout's + [application restrictions](configuration.md#numerical-current-reuse). +3. **Post-build validation** optionally reopens the completed artifact and + compares native f64 optimized and resolved evaluation. + +Post-build validation is off by default because it does not change the written +artifact and can be disproportionately expensive for large resolved axes. +Enable it when an immediate runtime smoke is useful: + +```console +pyamplicol generate ... --post-build-validation +``` + +Ordinary generation validation defaults to two deterministic samples. Current +relation discovery is off by default because its high-precision probes are +costly at generation time. Enable it to search for additional certified +equal, opposite, or zero-current reuse in the generated schedule: + +```console +pyamplicol generate ... --numerical-current-reuse +``` + +This changes optimization, not the expected physics result; the search may +find nothing for a given process, in which case the schedule is unchanged. +OTF uses its compact source projection and does not run the configurable +relation-discovery pass. Correlated generation also disables this pass, +regardless of the requested ordinary reuse setting. + +## Three matched examples + +The copied example workspace contains three cards for the same +`u u~ > Z + 6g` LC topology-replay workload: + +```console +pyamplicol generate --card qq_z6g_recurrence_jit_o2.toml +pyamplicol profile --card qq_z6g_recurrence_jit_o2.toml + +pyamplicol generate --card qq_z6g_compiled_jit_o3.toml +pyamplicol profile --card qq_z6g_compiled_jit_o3.toml + +pyamplicol generate --card qq_z6g_eager_jit_o2.toml +pyamplicol profile --card qq_z6g_eager_jit_o2.toml +``` + +These are intentionally explicit comparison cards: recurrence JIT O2, compiled +JIT O3, and eager JIT O2. Generation and profiling can be substantial for six +final-state gluons; use the primary Z+jet example for a quick functional +check. + +The packaged OTF walkthrough uses `p p > Z j j`, an explicit one-point +warm-up, and the same public profiler: + +```console +pyamplicol generate --card otf_pp_zjj.toml +python python/otf_pp_zjj_warm_up.py +pyamplicol profile --card otf_pp_zjj.toml +``` + +## Decision guide + +| Goal | Suggested choice | +| --- | --- | +| First built-in-SM artifact | Recurrence + JIT O2 (defaults) | +| Raw JSON/UFO model without a prepared pack | Compiled mode, or prepare a pack first | +| Spin-/colour-correlated Born quantities | Declare correlations; LC/NLC/full, complete direct compiled generation, grouped Python exact or native binary64 evaluation | +| Reuse prepared kernels with process tables | Eager + the pack's backend | +| Keep a high-multiplicity LC artifact compact and repeatedly run one selected flow | On-the-fly + an explicit one-point warm-up | +| Cross-architecture release-host movement | Compiled all-JIT O1/O2 artifact, or eager/recurrence with a prepared JIT O2 pack | +| One-flow/helicity-sum LC runtime | `topology-replay` | +| All-flow/single-helicity LC runtime | `all-flow-union` | +| Host-specific performance experiment | Explicit compiled JIT O3, C++, ASM, or C++ `native_arch` | +| Fast confidence after generation | Normal generation validation; enable post-build validation only when wanted | + +## See also + +- [Configuration](configuration.md) +- [Models and Processes](models-and-processes.md) +- [Artifacts and Portability](artifacts-and-portability.md) +- [Profiling and Benchmarking](profiling-and-benchmarking.md) +- [LC workloads and execution modes](lc-workloads-and-execution-modes.md) +- [Symbolica and Licensing](symbolica-and-licensing.md) diff --git a/pyamplicol/docs/user/gentle-walkthrough.md b/pyamplicol/docs/user/gentle-walkthrough.md new file mode 100644 index 0000000000..a37d422ded --- /dev/null +++ b/pyamplicol/docs/user/gentle-walkthrough.md @@ -0,0 +1,638 @@ +--- +title: "Get started: a gentle walkthrough" +nav_order: 2.5 +has_children: true +--- + + +# Get started: a gentle walkthrough + +This page is for physicists who are comfortable writing a MadGraph process such +as `p p > Z j j`, but who may not want to learn software-packaging vocabulary +before obtaining a matrix element. It follows one example from the model and +process request to numerical evaluation, timing, Python, and the native +languages used in event generators. + +pyAmpliCol generates and evaluates tree-level matrix elements. It does not +replace the PDF convolution, phase-space integration, event generation, +showering, or analysis parts of a Monte Carlo workflow. A useful first mental +picture is: + +| In a MadGraph-style workflow | In pyAmpliCol | +| --- | --- | +| Import or choose a model | Select `built-in-sm`, a serialized JSON model, a trusted UFO directory, or a prepared model | +| Define `p`, `j`, and other multiparticles | Fill `[process.multiparticles]` or pass `--multiparticle` | +| `generate p p > z j j` | Put `p p > Z j j` in a run card or pass it to `pyamplicol generate` | +| `output` | Generate a self-contained **process output directory** | +| Choose one subprocess | Select a readable expression or its stable process ID | +| Evaluate or time the matrix element | Use `evaluate`, `profile`, the Python API, or a native API | + +The process output directory is sometimes called an *artifact* in error +messages and in the more technical documentation. On this page, “process +output” means the same thing. + +## A five-minute `p p > Z j j` tour + +First activate the Python environment in which pyAmpliCol is installed. Copy +the packaged examples into a directory you can edit: + +```console +pyamplicol examples copy ./pyamplicol-examples +cd pyamplicol-examples +``` + +The copy includes cards, momenta, Python examples, native examples, and the +packaged model files. It is independent of the package installation, so it is +safe to edit. + +Generate a leading-colour recurrence output for a small two-flavour definition +of `p` and `j`: + +```console +pyamplicol generate "p p > Z j j" artifacts/pp_zjj_recurrence \ + --model built-in-sm \ + --multiparticle 'p=d,d~,g' \ + --multiparticle 'j=d,d~,g' \ + --flavor-scheme 2 \ + --max-quark-lines 2 \ + --color-accuracy lc \ + --execution-mode recurrence +``` + +Generation uses the protective `error` policy by default: if that output +directory already exists, the command stops instead of overwriting it. Choose a +new output name when repeating the tour, or use `--force` only when replacing +the complete existing output is intentional. + +The wheel contains prepared Standard Model building blocks for this recurrence +example. With these definitions, pyAmpliCol finds the ordered partonic channels, +identifies channels related by incoming or outgoing permutations, and stores +one reusable representative for each class. The loop-induced +`g g > Z g g` channel has no tree-level Standard Model amplitude and is +reported rather than generated. + +Here `p` and `j` are simply names for the particle lists supplied on the +command line. They expand the inclusive request into separate partonic matrix +elements; they do not apply PDFs or sum over incoming parton luminosities. +`--color-accuracy lc` keeps the leading-colour result resolved into its physical +flows. With no runtime selector, evaluation sums all those flows and all +helicities. + +Now ask what was produced: + +```console +pyamplicol inspect artifacts/pp_zjj_recurrence +``` + +The coloured tables list the model, runtime, concrete representatives, aliases, +helicity and colour coverage, and the recurrence execution mode. Pick the +representative with stable ID `p_p_to_z_j_j_3`, or use its readable requested +ordering `d d~ > g z g`: + +```console +pyamplicol inspect artifacts/pp_zjj_recurrence \ + --process 'd d~ > g z g' +``` + +The documentation uses *selector* for one of these explicit choices: a process +ID or expression, a helicity ID, or an LC flow ID. It is a choice, not a new +physics object. + +Evaluate the supplied phase-space point. Its external legs are in exactly the +order written in `--process`, and every four-vector is `[E, px, py, pz]`: + +```console +pyamplicol evaluate artifacts/pp_zjj_recurrence \ + --process 'd d~ > g z g' \ + --momenta data/pp_zjj_momenta.json \ + --precision 16 +``` + +This returns the helicity- and colour-summed result. To see the components and +their explicit sum, add `--resolved`: + +```console +pyamplicol evaluate artifacts/pp_zjj_recurrence \ + --process 'd d~ > g z g' \ + --momenta data/pp_zjj_momenta.json \ + --precision 16 \ + --resolved +``` + +Finally, time the optimized summed path: + +```console +pyamplicol profile artifacts/pp_zjj_recurrence \ + --process 'd d~ > g z g' \ + --momenta data/pp_zjj_momenta.json \ + --batch-size 128 \ + --target-runtime 1.0 \ + --precision 16 +``` + +The profile table records the selected process and selectors, batch size, +independent timing blocks, wall time per point, and the native phase breakdown +available for that execution mode. A 128-point profile batch is a throughput +measurement; it does not mean that an on-the-fly warm-up should use 128 points. + +At this stage you have exercised the usual loop: + +```text +choose model and process -> generate process output -> inspect -> evaluate -> profile +``` + +## The run card is a reusable set of choices + +Long commands are useful while exploring. A TOML run card is better when the +same physics choices must be recorded and repeated. The recurrence command +above corresponds to the following card, which you could save as +`my_pp_zjj.toml`: + +```toml +schema_version = 1 +action = "generate" + +[model] +source = "built-in-sm" + +[process] +entries = [{ expression = "p p > Z j j" }] +flavor_scheme = 2 +max_quark_lines = 2 + +[process.multiparticles] +p = ["d", "d~", "g"] +j = ["d", "d~", "g"] + +[color] +accuracy = "lc" + +[generation] +output = "artifacts/pp_zjj_recurrence_card" +emit_api_bundle = true + +[evaluator] +execution_mode = "recurrence" +``` + +Run a card directly: + +```console +pyamplicol my_pp_zjj.toml +``` + +or name the action explicitly: + +```console +pyamplicol generate --card my_pp_zjj.toml +``` + +These spellings use the same configuration. The first reads `action` from the +card; the second says that this invocation is a generation action. + +The packaged `generate_pp_zjj_from_ufo_sm.toml` card demonstrates a related +choice: it reads the serialized form of the UFO Standard Model and selects +`compiled` execution. Raw JSON or UFO model data contains the physics rules but +not the prepared recurrence building blocks, so that card deliberately builds +process-wide compiled evaluators. See [Models and Processes](models-and-processes.md) when +preparing recurrence, eager, or on-the-fly building blocks for another model. + +### Cards, named options, and `--set` + +Cards and command-line options use the same list of named settings (called the +configuration schema). Values are applied in this order: + +1. pyAmpliCol defaults; +2. values in the card; +3. named command-line options such as `--workers 4`; +4. repeated `--set dotted.path=value` options, from left to right; +5. any unavoidable license or resource adjustment. + +Later entries win. For example, this uses two workers and writes to a fresh +output without editing the packaged card: + +```console +pyamplicol generate --card generate_pp_zjj_from_ufo_sm.toml \ + --workers 4 \ + --set generation.workers=2 \ + --set generation.output=artifacts/pp_zjj_trial +``` + +The named option first requests four workers; the later `--set` changes that +request to two. `--set` values follow TOML syntax. Numbers and booleans are +written naturally; quote a string when it contains spaces. + +To see the complete interpretation without generating a process output, use: + +```console +pyamplicol config resolve generate_pp_zjj_from_ufo_sm.toml \ + --set generation.workers=2 +``` + +The `requested` section shows the card after defaults and overrides. The +`effective` section is the schema-valid configuration at this resolution step, +and `adjustments` records any clamps supplied to that resolver. Generation may +later apply constraints from a prepared model or the active Symbolica license; +those requested/effective differences and their reasons are retained in the +generated output and reported during the command. Use `config resolve` first +when a card or override does not appear to select the value you expected, then +read any adjustment reported by generation itself. + +Paths written in a card are interpreted relative to that card, not relative to +the shell from which it is run. That is why the packaged cards continue to find +their `models/`, `data/`, and `artifacts/` paths after the example directory is +moved. + +The exhaustive field reference is `all_options.toml` in the copied workspace. +The shorter explanation of colour, execution modes, and selectors is in +[Configuration](configuration.md). + +## What is inside a process output? + +Think of `artifacts/pp_zjj_recurrence/` as a generated, self-contained matrix- +element library for one model and a set of partonic processes. It contains: + +- the model identity, restriction, parameter catalogue, and configuration used + for generation; +- a stable list of concrete process representatives and their aliases; +- the helicity and colour information needed to interpret results; +- executable evaluator data for the selected execution mode; +- deterministic validation momenta; and +- when `emit_api_bundle = true`, small Python, C, C++, Fortran, and Rust + standalone drivers under `API/`. + +It is a directory rather than one shared library because the metadata and +physics axes are as important as the numerical code. You can move the whole +directory and load it from another working directory. Do not move individual +files out of it or edit them by hand. + +Process outputs are executable inputs, much like a compiled library. Only load +one that you generated yourself or obtained from a source you trust. Normal +loading validates its declared structure and compatibility. It is not a proof +of who published it; the trust rules and optional all-file hash check are +described in [Artifacts and Portability](artifacts-and-portability.md). + +### Representatives and aliases avoid duplicate work + +For the request `p p > Z j j`, several ordered channels differ only by a +permutation of incoming legs, outgoing legs, or both. pyAmpliCol stores one +representative evaluator where that equivalence is established. The other +orders appear as aliases. + +You may therefore select either a stable ID: + +```console +pyamplicol inspect artifacts/pp_zjj_recurrence \ + --process p_p_to_z_j_j_3 +``` + +or a readable ordering: + +```console +pyamplicol inspect artifacts/pp_zjj_recurrence \ + --process 'd d~ > g z g' +``` + +The runtime remaps momenta and result labels to the order you requested. An +incoming particle never crosses the `>` sign to become outgoing, or vice versa. +If a readable expression could refer to more than one representative, +pyAmpliCol stops and prints the possible stable IDs instead of guessing. + +Stable IDs are convenient in production scripts because they do not depend on +case or whitespace. Readable expressions are often clearer in a notebook. + +### What is shared between processes? + +A *current* here is an intermediate off-shell object built from a subset of the +external legs—the same physical idea that appears in Berends–Giele-style +recursion. Sharing is conservative: + +- Multiparticle aliases and side-preserving permutations use the same concrete + representative rather than duplicating its calculation. +- In eager, recurrence, and on-the-fly outputs, the model's prepared numerical + building blocks are stored once and referenced by the processes that need + them. These are the reusable rules for constructing currents from vertices. +- In recurrence mode, two process schedules are stored once only when an exact + process-to-process mapping proves that the complete current construction is + the same. A small per-process binding restores the external order, helicities, + and colour labels. +- The model provenance, common configuration, dependencies, and parameter + definitions belong to the output as a whole. + +What is not shared by assumption is just as important. Unrelated subprocesses +do not have their currents merged merely because some particle names coincide. +Each genuine process retains its own external-state metadata, selector +coverage, validation point, and process binding. Compiled mode has +process-specific stage evaluators; eager mode has process-specific invocation +tables; recurrence has a process binding to its recorded and checked schedule; +and on-the-fly mode has a compact seed for each process. + +This is why adding several processes to one output can save model and schedule +storage without changing the meaning of any individual matrix element. + +### Adding another process later + +The safest starting point is to put the complete process set in one +`process.entries` list and generate it together. pyAmpliCol can also append a +new process without exposing a half-written intermediate output when the +existing mode permits it and the model, target, pyAmpliCol build, and generation +settings are identical. + +The typed Python API makes the unchanging configuration explicit. This compact +example creates a compiled output, then appends a second process: + +```python +from pyamplicol import Generator +from pyamplicol.config import ( + Action, + EvaluatorConfig, + GenerationConfig, + RunConfig, +) + +config = RunConfig( + action=Action.GENERATE, + generation=GenerationConfig(emit_api_bundle=True), + evaluator=EvaluatorConfig(execution_mode="compiled"), +) +generator = Generator(config) +output = "artifacts/z_plus_jet" + +generator.generate("u u~ > Z g", output) +generator.generate("d d~ > Z g", output, mode="append") +``` + +Append rejects a duplicate stable ID or a change of model, target, prepared +model building blocks (called a kernel pack in technical messages), +configuration, or producer identity before modifying the existing output. +Current recurrence root schedules are immutable, so generate the full +recurrence process set together instead of appending to it. If settings must +change, write a new output or regenerate the complete set with explicit +`replace` mode. The default `error` mode protects an existing directory from +accidental replacement. + +Inspect the output after an append and keep the output directory together. The +root `API/` bundle is regenerated to include validation points for the full +process set. + +## Reading `inspect` as a physicist + +Start with the compact view: + +```console +pyamplicol inspect artifacts/pp_zjj_recurrence +``` + +It reads metadata and does not instantiate every helicity/colour evaluator. +The tables are intentionally more detailed than a MadGraph process list, but a +first-time user only needs a few fields. + +### The `Artifact` table + +| Field | What to check | +| --- | --- | +| `path` | The process output you meant to open. | +| `artifact type` | Usually a generated process set. “Artifact” means process output here. | +| `artifact ID` | A content label for the files that affect evaluation; useful when recording exactly what was benchmarked. | +| `producer` | The pyAmpliCol version that generated the output. | +| `target` | `portable-64le` can be loaded on supported 64-bit little-endian hosts; an explicit machine triple or CPU feature list indicates a target-specific output. | +| `model` | Model source and restriction. Check this before comparing physics results. | +| `default process` | The process selected when a multiprocess output is loaded without an explicit selector. Production code should normally select one explicitly. | +| `contents` | Number of stored representatives and aliases. | +| `runtime` / `capabilities` | The native engine and features required from the installed runtime. | +| `payloads` | Physical size and, for packed JIT data, the number and unpacked size of logical evaluators. | +| `integrity` | The top-level file inventory (the manifest) and declared structure were accepted. This is not publisher authentication. | + +### The `Processes` and `Aliases` tables + +| Field | Physical meaning | +| --- | --- | +| `stable ID` | Durable selector for CLI, Python, and native calls. | +| `concrete process` | External particles in the representative ordering. | +| `color` | `lc`, contracted `nlc`, or contracted `full`. | +| `helicities` | Physical configurations, with a smaller evaluated count in parentheses when certified reuse is active. | +| `color outputs` | Physical LC flows; NLC/full expose one contracted colour result per helicity. | +| `coverage (hel./color)` | `complete / complete` means selectors were not fixed during generation. A specialized output says which axis was restricted. | +| `aliases` | Number of requested orderings mapped to this representative. | + +The separate `Aliases` table names each mapped ordering and the representative +that evaluates it. This is the closest analogue to seeing the expanded +subprocess list while also learning which entries reuse one evaluator. + +### The `Execution` table + +The most useful lines are: + +- `mode / backend`: recurrence, compiled, eager, or on-the-fly and the prepared + numerical backend, where applicable; +- `LC flow layout`: whether a materialized LC output is arranged for one-flow + helicity sums (`topology-replay`) or all-flow single-helicity work + (`all-flow-union`); +- `runtime selectors`: whether helicity and colour flow remain selectable; +- `generation specialization` and `generation selection`: whether generation + fixed a particular flow, helicity assignment, or both; +- `native profile phases`: which timing pieces can appear in `profile`; and +- the requested/effective point tile and workspace, which matter for memory at + large batches. + +Recurrence rows and eager invocation counts are engineering descriptions of +the same current construction; they are mainly useful when comparing output +size or diagnosing performance. For an on-the-fly output, look instead for the +physical helicity/flow census, query-construction threads, and `f64 only` +(standard double precision). These describe what can be selected without +forcing the complete high-multiplicity family to be built during inspection. + +For a physicist-oriented comparison of both LC layouts, all four execution +modes, and the OTF warm-state lifecycle, continue with [LC workloads and +execution modes](lc-workloads-and-execution-modes.md). + +Use a process selector to shorten the table: + +```console +pyamplicol inspect artifacts/pp_zjj_recurrence \ + --process p_p_to_z_j_j_3 +``` + +The explicit full-physics view materializes complete helicity and colour +metadata and may be very large: + +```console +pyamplicol inspect artifacts/pp_zjj_recurrence \ + --process p_p_to_z_j_j_3 \ + --full-physics +``` + +Reserve it for a selected low-multiplicity process when the compact table is +not enough. + +## Choose the interface that fits your calculation + +All interfaces load the same process output and use the same stable process and +selector IDs. You do not generate a separate “Python version” and “Fortran +version.” + +| Interface | A natural use | +| --- | --- | +| CLI | Generate, inspect, make one-off evaluations, compare settings, and profile. | +| Typed Python API | Notebooks, scans, tests, parameter updates, and integration with Python phase-space code. | +| C11 API | A small set of stable C functions for an existing C or mixed-language program. | +| C++17 wrapper | Automatic lifetime management and `std::vector`-based evaluation. | +| Fortran 2008 module | Monte Carlo and phenomenology codes written in Fortran. | +| Rust 2021 wrapper | Ownership-checked access without an external Rust crate dependency. | + +### Typed Python + +The numerical part of the five-minute tour is only a few lines: + +```python +import json +from pathlib import Path + +from pyamplicol import Runtime + +points = json.loads(Path("data/pp_zjj_momenta.json").read_text()) +runtime = Runtime.load( + "artifacts/pp_zjj_recurrence", + process="d d~ > g z g", +) + +print(runtime.physics.process) +print(runtime.physics.helicity_ids[:2]) +print(runtime.physics.color_flow_ids[:2]) +print(runtime.evaluate(points, precision=16)) +``` + +`runtime.physics` describes the selected process. Pass IDs from that object to +`evaluate(..., helicities=..., color_flows=...)` to select components. LC +accepts both kinds of selector; NLC and full colour are already contracted and +therefore accept helicity selectors but not an LC flow selector. + +The packaged scripts show complete typed generation, parameter updates, +resolved evaluation, and benchmarking: + +```console +python python/typed_generation.py artifacts/pp_zjj_typed --plan-only +python python/runtime_evaluation.py \ + artifacts/pp_zjj_recurrence data/pp_zjj_momenta.json \ + --process 'd d~ > g z g' +``` + +See [Runtime and Selectors](runtime-and-selectors.md) for point-wise selectors, exact precision, model +parameters, resolved output shapes, and explicit on-the-fly `warm_up(...)`. + +### Generated Python, C, C++, Fortran, and Rust drivers + +Because the tour kept the default `emit_api_bundle = true`, the process output +contains standalone examples for every supported language. Run them from the +copied example workspace: + +```console +python artifacts/pp_zjj_recurrence/API/python/check_standalone.py \ + --process 'd d~ > g z g' --json + +make -C artifacts/pp_zjj_recurrence/API/c run \ + ARGS='--process "d d~ > g z g" --json' + +make -C artifacts/pp_zjj_recurrence/API/cpp run \ + ARGS='--process "d d~ > g z g" --json' + +make -C artifacts/pp_zjj_recurrence/API/fortran run \ + ARGS='--process "d d~ > g z g" --json' + +make -C artifacts/pp_zjj_recurrence/API/rust run \ + ARGS='--process "d d~ > g z g" --precision 16 --json' +``` + +With no `--kinematics` option, each driver uses the validation point stored in +the output and remaps it to the requested leg order. The drivers compare the +optimized total with the explicit sum of resolved components, so they are also +a useful first check of a compiler and linker setup. + +The installed `rusticol-config` command supplies the include paths, static +library, Fortran module source, and linker flags. Native calls use standard +double precision, also called binary64 (`precision = 16`). Python can also use +a retained higher-precision evaluator when that execution mode provides one; +on-the-fly execution is double precision in all languages. + +For embedding rather than running the example drivers, start with +[Native APIs](native-apis.md). It contains complete loading/evaluation examples, +memory layout, compiler commands, model-parameter updates, selector arrays, and +the cross-language on-the-fly warm-up callback. + +## Where on-the-fly mode fits + +Recurrence generated all reusable schedules before the first load. On-the-fly +(OTF) instead stores a compact process description and constructs a selected +family when it is first requested. Its practical focus is a leading-colour, +single-flow calculation summed over helicities, especially as multiplicity +grows. + +The packaged OTF walkthrough uses the same `p p > Z j j` process: + +```console +pyamplicol generate --card otf_pp_zjj.toml +python python/otf_pp_zjj_warm_up.py +pyamplicol profile --card otf_pp_zjj.toml +``` + +The Python step calls `warm_up(...)` with exactly one double-precision +phase-space point and one LC flow. Omitting the helicity selector requests the +helicity sum. Its progress display reports query construction and live memory; +the following table reports the selected flow, warm-up time, query counts, +memory, and matrix element. The independent `profile` command then measures +128-point throughput. + +You can keep completed preparation between programs: save it with +`runtime.save("process.otf-cache")`, then load the original process output in +the next program and call `runtime.load_cache("process.otf-cache")`. Continue +with the same helicity/flow selectors to reuse the saved work. This is explicit +API functionality, not an automatic cache shared by separate CLI commands. +See the [small Python example](runtime-and-selectors.md#saving-an-otf-warm-cache) +and [native SDK examples](native-apis.md#saving-and-restoring-an-otf-cache). + +OTF NLC and full-colour calculation are available for low multiplicity, but +their contracted family grows rapidly and is not intended here as a +high-multiplicity performance route. The detailed differences among recurrence, +compiled, eager, and OTF are summarized in +[Generation Modes and Evaluators](generation-modes-and-evaluators.md). + +## A practical checklist for a new calculation + +1. Copy the examples and make sure the five-minute tour works. +2. Choose a trusted model. Prefer serialized JSON for reproducibility; treat a + UFO directory as executable Python input. +3. Write the process and multiparticle definitions in a card. Start with a + narrow flavour scheme while testing. +4. Choose colour accuracy and an execution mode. Recurrence is the general + prepared-model default; compiled accepts a model that has not had its local + numerical building blocks prepared; eager stores compact invocation tables + around those prepared building blocks; OTF is aimed at selected + double-precision workloads. +5. Generate into a new output directory. Keep the default `error` policy until + replacement is intentional. +6. Run compact `inspect`. Check the model, process, colour accuracy, coverage, + execution mode, and target before evaluating. +7. Evaluate a known point and compare total with resolved output. +8. Profile the exact helicity/flow workload and batch size your application + will use. +9. Integrate the same output through Python or one native API. Select a process + explicitly in production code. +10. Record the card, process selector, model parameters, and process-output ID + with physics results. + +From here, continue with [Configuration](configuration.md) for all run-card +choices, [Models and Processes](models-and-processes.md) for UFO/JSON and multiprocess details, +[Runtime and Selectors](runtime-and-selectors.md) for evaluation, and [Native APIs](native-apis.md) for +language integration. To compare many validated process/mode combinations and +turn the measurements into the report, follow the +[profiling-campaign walkthrough](profiling-campaign-walkthrough.md). Every +maintained packaged card and script is indexed in the +[examples guide](https://github.com/MadGraphTeam/MadGraph7/blob/main/pyamplicol/examples/README.md). + + +```{toctree} +:hidden: + +lc-workloads-and-execution-modes +profiling-campaign-walkthrough +``` diff --git a/pyamplicol/docs/user/installation.md b/pyamplicol/docs/user/installation.md new file mode 100644 index 0000000000..b7949800dc --- /dev/null +++ b/pyamplicol/docs/user/installation.md @@ -0,0 +1,302 @@ +--- +title: "Installation" +nav_order: 2 +has_children: true +--- + +# Installation + +pyAmpliCol is distributed as a Python package with a Rust-backed runtime. For +most users, installation is a normal `pip` operation: the published wheel +already contains the Python extension and the Rusticol native SDK. + +> **Recommended path:** use Python 3.11 or newer in a fresh virtual +> environment and install from PyPI. + +## Installation for MadGraph7 + +pyAmpliCol is developed inside the +[MadGraph7 repository](https://github.com/MadGraphTeam/MadGraph7) (in +`pyamplicol/`) and released together with MadGraph7, with the same version +number. MadGraph7 uses it for processes generated with the `[amplicol]` +matrix-element backend. Its installer works like the madspace one: by default +it installs into `pyamplicol/install` inside the MadGraph7 directory and leaves +the active Python environment untouched. + +From the MadGraph7 command line: + +```console +install pyamplicol +``` + +or directly: + +```console +python pyamplicol/install.py # interactive +python pyamplicol/install.py --bin # wheel from PyPI (release tarballs) +python pyamplicol/install.py --source # build from source +``` + +| Option | Meaning | +| --- | --- | +| `-y`, `--yes` | No prompts; reuse the previous install mode. | +| `--bin` | Install the PyPI wheel of this MadGraph7 version. Offered by default in release tarballs on platforms with a wheel. | +| `--source` | Build from source. | +| `-j N` | Parallel compilation jobs (`install pyamplicol` passes the `nb_core` option). | +| `--system` | Install into the active Python environment instead of `pyamplicol/install`. | +| `--clean` | Delete `pyamplicol/build` and `pyamplicol/install` first. | +| `--build-models` | Regenerate the prepared models instead of downloading them. | + +A source build needs a C/C++ toolchain and the Rust version pinned in +`rust-toolchain.toml`. When `rustup` is not available, the installer installs +it into `pyamplicol/build/rust`, without touching `~/.rustup` or `~/.cargo`. +The Cargo build tree is kept in `pyamplicol/build`, so rebuilds after a source +change are incremental. + +The prepared SM and SM+HEFT models shipped in the package are not stored in +Git. The installer downloads them from the MadGraph7 release (the release of +this version, else the latest one) and checks that they match the source tree. +When they do not, for example after a change to the model compiler, it +regenerates them locally, which takes one extra build. The same can be done by +hand with `tools/release/prepared_models_archive.py fetch` or `regenerate`. + +After installing, the installer checks that Symbolica accepts pyAmpliCol's +library key (see [Symbolica licensing](#symbolica-licensing)). MadGraph7 also +warns when the installed package was built from different sources than the +ones in `pyamplicol/`; reinstall with `install pyamplicol -y` in that case. + +## Install from PyPI + +### macOS and Linux + +```console +python3 -m venv .venv +. .venv/bin/activate +python -m pip install --upgrade pip +python -m pip install pyamplicol +``` + +Verify the installation without generating a process: + +```console +python -c 'import pyamplicol; print(pyamplicol.__version__)' +pyamplicol doctor +pyamplicol examples list +``` + +`pyamplicol doctor` reports the package, Python, runtime, platform, and +Symbolica environment that pyAmpliCol can see. `examples list` confirms that +the packaged run cards and data files are available. + +## Supported binary-wheel platforms + +The current release provides `cp311-abi3` wheels for Python 3.11 and newer on: + +| Platform | Architecture | Minimum deployment target | +| --- | --- | --- | +| macOS | Apple silicon (`arm64`) | macOS 11 | +| macOS | Intel (`x86_64`) | macOS 11 | +| Linux | `x86_64` | manylinux 2.28 | + +Wheel users do **not** need a Rust compiler. pyAmpliCol does not depend on +LHAPDF. + +The generated C11, C++17, Fortran 2008, and Rust 2021 examples compile against +the wheel-owned Rusticol SDK. Install the compiler only for the language you +intend to use: + +- C/C++ compiler for C11 or C++17 consumers; +- Fortran compiler for Fortran 2008 consumers; +- Rust compiler for the standalone Rust driver. + +See [Native APIs](native-apis.md) for the generated driver workflow. + +## Create an editable examples workspace + +The examples are package resources. Copy them before editing or running them: + +```console +pyamplicol examples copy ./pyamplicol-examples --force +cd pyamplicol-examples +``` + +Paths inside the supplied TOML cards are relative to the card, so the copied +workspace can be moved. Keep the environment containing pyAmpliCol activated, +or invoke its executable by absolute path. + +Continue with [Quick Start](quick-start.md). + +## Install a tagged source release + +Use a tagged snapshot when your platform has no compatible wheel or when you +need a source build: + +```console +git clone --branch v0.2.1 --depth 1 \ + https://github.com/MadGraphTeam/MadGraph7.git +cd MadGraph7/pyamplicol +python tools/release/prepared_models_archive.py fetch +python -m pip install . +``` + +Inside MadGraph7, `python pyamplicol/install.py --source --system` does the +same, including the prepared models. + +A source build requires: + +- Python 3.11 or newer; +- Rust 1.89 or newer; +- a C/C++ toolchain; +- network access to the release-locked Python, Cargo, and SymJIT inputs. + +The repository-pinned CI toolchain may be newer than the minimum supported +Rust version. A Fortran compiler remains optional unless you build a Fortran +consumer. + +To build from the source distribution published on PyPI: + +```console +python -m pip download --no-binary pyamplicol pyamplicol +tar -xf pyamplicol-*.tar.gz +cd pyamplicol-*/ +python -m pip install . +``` + +## Contributor installation + +For development from a checkout, `just dev-install` creates a repository-managed +environment with published Python dependencies from `pyproject.toml`, including +Symbolica 3.0.0 and ufo-model-loader 1.0.0, and the release-mode native build with +published SymJIT 2.26.4. It does not use an editable installation. Building a new +release wheel requires a clean Git checkpoint and complete release assets; +published-dependency mode does not relax these publication guards. + +```console +git clone https://github.com/MadGraphTeam/MadGraph7.git +cd MadGraph7/pyamplicol +python tools/release/prepared_models_archive.py fetch +just dev-install +PYTHON=.venv/bin/python just dev-test +``` + +Normal setup uses published PyPI dependencies. For dependency development, +`--loader-wheel PATH` explicitly selects a local loader wheel; the installer +does not search for one automatically. + +The native build can take several minutes. For a dirty development checkout, add +`--wheel-directory PATH` to reuse an already-built compatible release wheel, +or use `--dependencies-only` to install dependencies without building or +staging pyAmpliCol, leaving the existing native runtime untouched. +`--no-build` skips both runtime dependency installation and the project +build/staging; developer tools and explicitly requested optional references +may still be set up. `just dev-test` includes a fresh release build and therefore +also requires a clean checkpoint; dirty edits can use focused Python tests +against the staged compatible native runtime. + +On Nix or NixOS: + +```console +nix develop +just dev-install +PYTHON=.venv/bin/python just dev-test +``` + +The Nix shell supplies Python, Rust, C/C++, Fortran, build libraries, and the +documentation/PDF tools. `just dev-install` prepares the published-dependency +environment in `.venv` and builds and stages the project in release mode. + +The original AmpliCol and Reference FFT repositories are optional profiling +inputs and are omitted by default. Request either or both explicitly: + +```console +just dev-install --with-legacy-amplicol --with-reference-fft +``` + +Either opt-in also installs the `fft-profiling` Python extra into `.venv`, so +the profiling driver and PDF renderer are ready without reinstalling the +project in editable mode. + +### Historical candidate mode (opt-in) + +The old pinned-source dependency machinery is retained behind `--candidate`, +but its pinned APIs are incompatible with the current tensor implementation, +even when the dependencies report the same version numbers. It needs updated, +coherent upstream pins before use and is not a working current-source setup +route. Its candidate wheels are non-publishable. Use the published dependency +lane above; ordinary `just dev-test` uses release mode. + +## Symbolica licensing + +Generation and Python arbitrary-precision execution use Symbolica. The native +binary64 runtime embedded in generated artifacts does not import Symbolica. +pyAmpliCol attempts to register its package key at startup, including in +spawned workers. A valid package key covers its Symbolica operations without a +personal license. If registration fails synchronously, the existing personal +license and restricted-mode paths remain available; restricted mode clamps +generation to one worker and one Symbolica core. + +For details and the built-in personal license-request helpers, see +[Symbolica and Licensing](symbolica-and-licensing.md). + +## Common installation issues + +### `No matching distribution found` + +Check your Python version and platform: + +```console +python -VV +python -c 'import platform; print(platform.system(), platform.machine())' +``` + +If no wheel matches, use the tagged source installation above. + +### `pyamplicol: command not found` + +Activate the environment where the package was installed: + +```console +. .venv/bin/activate +python -m pyamplicol --help +``` + +### Generated native driver cannot find `rusticol-config` + +The SDK discovery command belongs to the installed wheel. Activate that wheel's +environment before invoking `make`, or set `RUSTICOL_CONFIG` to its executable: + +```console +export RUSTICOL_CONFIG="$(python -c 'import sys; print(sys.prefix + "/bin/rusticol-config")')" +make -C artifacts/pp_zjj/API/c run +``` + +### A contributor import says the candidate wheel is stale + +The local candidate no longer matches the native build inputs. The historical +candidate pins are also incompatible with the current tensor implementation; +rebuilding them is not a supported recovery route. Switch the same checkout to +the default published-dependency environment: + +```console +just dev-install +``` + +Do not work around this by mixing an extension from a different checkout. + +## Next steps + +- [Quick Start](quick-start.md) — generate, inspect, evaluate, and profile a process. +- [Configuration](configuration.md) — run cards, overrides, color modes, and evaluators. +- [Models and Processes](models-and-processes.md) — built-in, JSON, UFO, and prepared models. +- [Troubleshooting](troubleshooting.md) — focused solutions for runtime and generation failures. + +The exact build and publication boundary is recorded in +[Release and Support](release-and-support.md). + + +```{toctree} +:hidden: + +quick-start +examples-gallery +``` diff --git a/pyamplicol/docs/user/lc-workloads-and-execution-modes.md b/pyamplicol/docs/user/lc-workloads-and-execution-modes.md new file mode 100644 index 0000000000..542204dd65 --- /dev/null +++ b/pyamplicol/docs/user/lc-workloads-and-execution-modes.md @@ -0,0 +1,668 @@ +--- +title: "LC workloads and execution modes" +nav_order: 1 +parent: "Get started: a gentle walkthrough" +--- + + +# LC workloads and execution modes + +This page helps with two choices that are easy to mix up: + +1. **Which part of the leading-colour result will be evaluated most often?** + This determines the LC flow layout for a materialized process output. +2. **When should pyAmpliCol construct the numerical representation?** This + determines the execution mode. + +Neither choice changes the requested physics. The LC flow layout reorganizes +the same leading-colour components, while the execution mode reorganizes when +the same current construction is prepared. Use [the gentle +walkthrough](gentle-walkthrough.md) first if “process output,” “flow,” or +“selector” is unfamiliar. + +Here are the few computing terms needed on this page: + +- A **process output** is the directory produced by `pyamplicol generate` for + one or more requested reactions. It plays roughly the role of a generated + standalone process directory in MadGraph: a later program loads it and + supplies phase-space points. +- A **current** is an intermediate off-shell object in the recursive amplitude + construction. Reusing a current avoids recomputing the same substructure in + many diagrams. +- A **materialized** output has already written the reusable current-building + plan to disk during generation. Recurrence, eager, and compiled outputs are + materialized in different forms. OTF instead writes a compact recipe and + constructs the requested plan after the output is loaded. +- The **runtime** is the loaded numerical calculator, and a **selector** is a + request for particular helicity or colour-flow components. “Cache” below + simply means state retained in the runtime's memory for reuse. An OTF cache + can also be saved to a file, but that file supplements the original process + output rather than replacing it. + +## Two useful slices through an LC result + +At one phase-space point, imagine the leading-colour result as a rectangular +table. Its rows are the physical helicity configurations \(h\), and its columns +are the physical LC flows \(f\). A runtime request may sum either axis: + +| Workload | Quantity requested repeatedly | Typical physics use | +| --- | --- | --- | +| One flow, helicity sum | Fix one \(f\), sum every retained \(h\) | A flow-resolved calculation or a colour-flow sampling/integration step | +| All flows, one helicity | Fix one \(h\), sum every retained \(f\) | A helicity-resolved calculation or repeated evaluations for sampled helicities | + +Asking for neither selector sums both axes. Asking for a set of IDs sums the +selected subset. The two layouts below are performance arrangements for these +requests; they do not add or remove LC terms when their coverage is complete. +They optimize different physical outer loops: colour-flow sampling repeatedly +needs a vertical slice of the table, whereas helicity sampling repeatedly needs +a horizontal slice. Neither layout is universally faster. + +### `topology-replay`: one flow, summed helicities + +`topology-replay` is the default. It retains complete physical flow and +helicity coverage, but arranges the generated construction so that the compact +topology can be replayed for the requested flow. It is therefore the natural +layout when each evaluation normally selects one flow and omits the helicity +selector, meaning “sum all helicities.” + +The profiling campaign calls this same workload `non-union-flow`: currents for +different physical flows are not first merged into one cross-flow plan. The +public process-output setting remains `topology-replay`. + +Use it when: + +- a downstream colour sampler asks for one LC flow at a time; +- the matrix element should remain reusable for another flow later; or +- the main observable is a flow-resolved contribution summed over spin. + +The explicit card setting is: + +```toml +[color] +accuracy = "lc" +lc_flow_layout = "topology-replay" +``` + +Because it is the default, omitting `lc_flow_layout` has the same effect. A +direct generation override is: + +```console +pyamplicol generate --card run.toml \ + --color-accuracy lc \ + --lc-flow-layout topology-replay +``` + +Once generated, select the first physical flow shown by `inspect` and sum all +helicities by omitting `--helicity`: + +```console +pyamplicol evaluate --card run.toml --color-flow 1 +pyamplicol profile --card run.toml --color-flow 1 +``` + +CLI flow numbers are **one-based**. A stable flow ID printed by `inspect`, such +as `flow:...`, may be used in place of `1`. + +### `all-flow-union`: all flows, one helicity + +`all-flow-union` retains the same complete axes, but builds one shared +cross-flow recurrence. It is the complementary layout for repeatedly selecting +one helicity and omitting the flow selector, meaning “sum all physical LC +flows.” + +The profiling campaign calls this workload `union-flow`: it measures the +cross-flow plan for one selected helicity. The public process-output setting is +`all-flow-union`. + +Use it when: + +- a helicity sampler asks for one physical helicity at a time; +- every LC flow is needed for each selected helicity; or +- the main observable is a helicity-resolved LC result with colour summed. + +Set it in the card: + +```toml +[color] +accuracy = "lc" +lc_flow_layout = "all-flow-union" +``` + +or override the card during generation: + +```console +pyamplicol generate --card run.toml \ + --color-accuracy lc \ + --lc-flow-layout all-flow-union +``` + +Then select one stable helicity ID from `inspect` and omit `--color-flow`: + +```console +pyamplicol evaluate --card run.toml \ + --helicity 'h:-1,+1,-1,+1,-1' +pyamplicol profile --card run.toml \ + --helicity 'h:-1,+1,-1,+1,-1' +``` + +The displayed helicity length depends on the number of external particles, so +copy an ID from the process output rather than copying the illustrative ID +above into an unrelated process. + +`all-flow-union` is LC-only. It is available for recurrence, eager, and +compiled outputs. OTF does not materialize either LC layout: the helicity and +flow arguments in the first runtime request determine which family of currents +is constructed in memory. + +### Pick the layout from the repeated workload + +For a complete materialized output, either layout can still evaluate all +flows, all helicities, or a supported selected subset. “Optimized for” does not +mean “only capable of.” It means that the current-building plan written during +generation is organized around one common loop direction. + +| If most calls request... | Start with... | +| --- | --- | +| One flow and every helicity | `topology-replay` | +| Every flow and one helicity | `all-flow-union` | +| The completely summed LC result | Benchmark both layouts for the real process and batch size | +| A mixture that changes during a run | Keep complete coverage, then profile the dominant request | + +The packaged cards provide a larger-multiplicity comparison without hiding +the settings: + +```console +pyamplicol examples copy ./pyamplicol-examples +cd pyamplicol-examples +pyamplicol generate --card benchmark_z6g_single_flow_helicity_sum.toml +pyamplicol profile --card benchmark_z6g_single_flow_helicity_sum.toml +pyamplicol generate --card benchmark_z6g_all_flows_single_helicity.toml +pyamplicol profile --card benchmark_z6g_all_flows_single_helicity.toml +``` + +These are performance examples, so generation can take appreciably longer +than the small `p p > Z j j` tour. + +## Runtime selection in Python and native programs + +For recurrence, eager, and compiled modes, the layout is fixed when the process +output is generated. Python and native programs load that output and select +physical IDs while evaluating points; they cannot rearrange its stored plan. +OTF is the exception: its first selected request constructs the corresponding +family after loading. + +### Typed Python + +The typed objects in `runtime.physics` avoid copying IDs by hand: + +```python +from pyamplicol import Runtime + +runtime = Runtime.load("artifacts/pp_zjj", process="d d~ > g z g") +flow = runtime.physics.color_flows[0] +helicity = runtime.physics.helicities[0] + +# One physical flow, summed over every helicity. +flow_values = runtime.evaluate(points, color_flows=(flow,), precision=16) + +# One physical helicity, summed over every LC flow. +helicity_values = runtime.evaluate(points, helicities=(helicity,), precision=16) +``` + +Here `points` is a batch with shape +`[point][external particle][E, px, py, pz]`. The same calls accept stable string +IDs instead of the typed objects. The global selector arguments are sequences: +passing one entry selects one component, passing several selects and sums that +subset, and passing `None` sums the complete retained axis. + +Typed generation uses the same schema as a TOML card. For example, this is the +complete choice of LC layout and execution mode in Python: + +```python +from pyamplicol import Generator, ModelSource +from pyamplicol.config import resolve_config + +settings = resolve_config( + { + "schema_version": 1, + "action": "generate", + "color": { + "accuracy": "lc", + "lc_flow_layout": "all-flow-union", + }, + "evaluator": {"execution_mode": "recurrence"}, + } +) +Generator(settings).generate( + "d d~ > g z g", + "artifacts/ddbar_to_zgg_union", + model=ModelSource.built_in_sm(), +) +``` + +Replace `all-flow-union` by `topology-replay`, or `recurrence` by `compiled`, +`eager`, or `on-the-fly` as described below. For OTF, omit +`lc_flow_layout`: no materialized flow layout is used. + +### C11 + +The C ABI uses a null pointer with a zero count for an unselected axis. Given +stable `flow_id` and `helicity_id` strings, the two LC calls are: + +```c +const char *one_flow[] = {flow_id}; +int status = rusticol_runtime_evaluate_selected_f64( + handle, momenta, momentum_count, point_count, + NULL, 0, one_flow, 1, + NULL, 0, NULL, 0, + values, point_count); + +const char *one_helicity[] = {helicity_id}; +status = rusticol_runtime_evaluate_selected_f64( + handle, momenta, momentum_count, point_count, + one_helicity, 1, NULL, 0, + NULL, 0, NULL, 0, + values, point_count); +``` + +The arguments after the global string IDs are optional per-point **zero-based** +selector-index arrays. Do not supply both the global and per-point form for the +same axis. End the handle lifetime with `rusticol_runtime_free(handle)`. + +### C++17 + +The C++ wrapper uses empty vectors for an unselected axis: + +```cpp +auto flow_values = runtime.evaluate_selected( + momenta, point_count, {}, {flow_id}); +auto helicity_values = runtime.evaluate_selected( + momenta, point_count, {helicity_id}, {}); +``` + +The final two optional arguments, omitted here, are the per-point helicity and +flow index vectors. The `rusticol::Runtime` destructor releases the handle. + +### Fortran 2008 + +In the Fortran module, omit the optional axis that should be summed: + +```fortran +character(len=128) :: flow_ids(1), helicity_ids(1) +real(c_double), allocatable :: values(:) + +flow_ids(1) = flow_id +call runtime%evaluate_selected(momenta, point_count, values, & + color_ids=flow_ids, ierr=ierr) + +helicity_ids(1) = helicity_id +call runtime%evaluate_selected(momenta, point_count, values, & + helicity_ids=helicity_ids, ierr=ierr) +``` + +Use `call runtime%close()` when the loaded process is no longer needed. + +### Rust 2021 + +The installed safe wrapper represents omitted axes with `Selectors::all()`: + +```rust +let one_flow = Selectors::all().with_colors([flow_id]); +let flow_values = runtime.evaluate_selected_f64( + &momenta, point_count, &one_flow, None, None, +)?; + +let one_helicity = Selectors::all().with_helicities([helicity_id]); +let helicity_values = runtime.evaluate_selected_f64( + &momenta, point_count, &one_helicity, None, None, +)?; +``` + +The two `None` arguments are the optional per-point zero-based helicity and +flow index slices. Dropping `runtime` releases the native handle. + +Complete load, error-handling, metadata, memory-layout, and compiler examples +are in the [Native APIs guide](native-apis.md). Stable IDs can be read from that +metadata or copied from `pyamplicol inspect`; do not infer IDs from their +position except where the CLI explicitly accepts a one-based ordinal. + +## Generation specialization is not runtime selection + +Complete coverage gives one process output with reusable runtime selectors. A +runtime selection chooses among components that are already represented by +that output and can change from call to call. This is analogous to generating a +general standalone process once and choosing the desired component during the +event loop. + +Generation specialization instead permanently omits unneeded work while +constructing a recurrence, eager, or compiled output. It can be valuable when +a production run will use exactly one known flow or helicity, but that smaller +output cannot later answer a different selection. The relevant card fields are: + +```toml +[process] +# Flow-specialized packaged example: u u~ > Z g g g g g g. +reference_color_order = [2, 4, 5, 6, 7, 8, 9, 1, 3] +selected_color_sector_ids = [0] +``` + +or, in the separate helicity-specialized card: + +```toml +[process] +# Helicity-specialized packaged example: u u~ > Z g g g g g g. +selected_source_helicities = { "1" = -1, "2" = 1, "3" = -1, "4" = 1, "5" = -1, "6" = 1, "7" = -1, "8" = 1, "9" = -1 } +``` + +These are low-level generation inputs. `selected_color_sector_ids` are +zero-based internal construction-sector IDs, not the one-based physical flow +numbers accepted by `--color-flow`. A specialized-away flow or helicity cannot +be restored by a later runtime call. Use the maintained +`benchmark_z6g_generation_specialized_*.toml` cards as worked examples instead +of guessing sector IDs or a reference order. + +For reusable compiled and eager outputs, do not combine generation +specialization with `all-flow-union`; generation rejects that combination. +Recurrence can encode a valid specialized union construction, but its excluded +selectors are still absent, so it should be treated as an advanced, +non-reusable output. `process.max_color_sectors` is incompatible with +`all-flow-union` in every materialized mode. + +OTF takes the opposite approach: it rejects these generation-specialization +fields and keeps a compact list of all physical selectors. The first warm-up or +evaluation then constructs only the requested family in memory. + +## Four ways to organize the same calculation + +The execution mode is selected in `[evaluator]`: + +```toml +[evaluator] +execution_mode = "recurrence" # or compiled, eager, on-the-fly +``` + +or on the generation command line: + +```console +pyamplicol generate --card run.toml --execution-mode recurrence +``` + +In the descriptions below, a **kernel** is a small compiled numerical routine +for a model vertex or current operation. A **dependency graph** records which +currents must be computed before others; the common abbreviation DAG means a +directed acyclic graph. A **prepared model** is a reusable package of these +model-specific kernels, prepared once and shared by several process outputs. + +| Mode | What generation stores | What happens after loading | Natural starting point | +| --- | --- | --- | --- | +| `recurrence` | An ordered recipe for building and combining currents | The loaded calculator follows that stored recipe for each batch | General-purpose default with a prepared model | +| `compiled` | Machine-code stages compiled for the complete process graph | Those process-specific stages evaluate each batch | More generation work in exchange for the broadest process-wide optimization | +| `eager` | A compact list of calls to prepared model kernels | The loaded calculator makes those calls directly | Usually the quickest materialized output to generate and load | +| `on-the-fly` (OTF) | A compact process recipe, not a ready-to-run selected family | The first selected request constructs one family and retains it in memory | Compact LC output when one selected family dominates many later calls | + +The choice changes where work and storage occur, not the meaning of LC, NLC, +or full colour. Results for the same supported physics selection should agree +across modes within the stated numerical tolerance. + +### Recurrence + +Recurrence is the default and the closest stored representation to the +physical current recursion. Generation writes a compact, checked schedule of +which currents to build and in what order; numerical evaluation follows it +through the model's prepared kernels. It supports LC, NLC, and full colour, +reusable runtime selectors, and optional generation specialization. Large +point batches are divided into smaller tiles so temporary memory stays bounded. + +Choose recurrence first when a prepared model is available and the process +output should remain broadly reusable. + +### Compiled + +Compiled mode turns the complete process dependency graph into process-wide +machine-code stages during generation. That costs more generation work and +lets the compiler optimize across the broadest view of one process. Unlike +recurrence, eager, and OTF, it can start directly from a machine-readable +JSON/UFO model description without a separately prepared kernel package. It +supports LC, NLC, full colour, reusable selectors when coverage is complete, +and generation-specialized outputs. + +Choose it when process-local compilation is desirable, when comparing a +specialized baseline, or when no prepared model bundle exists. + +### Eager + +Eager mode stores compact tables saying which prepared model kernel to call, +with which inputs, and how to combine its result. It does less process-wide +compilation and is usually the quickest materialized mode to generate and +load. Like recurrence and compiled mode, it supports LC, NLC, full colour, +complete runtime selection, and optional generation specialization. + +Choose it when fast materialized generation/load matters and a prepared model +is available. + +### On-the-fly + +OTF stores a compact process recipe rather than a ready-to-run schedule for +every selector. It still needs a prepared model. For LC it can express both +physical request families: one flow summed over helicities, or all flows summed +at one helicity. It does not use the `topology-replay`/`all-flow-union` +generation setting because the first runtime request itself defines the family +to build. + +OTF is native `f64` only (`precision=16`), meaning ordinary IEEE double +precision. Its practical performance focus is LC with one selected flow and a +helicity sum. Contracted NLC and full-colour families are available for +low-multiplicity correctness work, but the initial construction grows rapidly +and is not intended as a practical high-multiplicity route. + +The release acceptance suite exercises catalogued contracted NLC and +full-colour OTF processes through `n <= 4` against recurrence or MadGraph as +appropriate. That establishes the low-multiplicity correctness surface; it is +not a claim that their cold construction is affordable at larger `n`. + +## The OTF warm-state lifecycle + +OTF moves a potentially expensive operation from generation to the first use +of a selected family. Treat warm-up as part of job planning, not as a small +extra numerical sample. + +A **family** here means one physical selection together with all currents +needed to evaluate it repeatedly. For example, “flow 1, summed over all +helicities” is one family, while “flow 2, summed over all helicities” is +another. A family is structural: it says what to calculate, not what numerical +value the matrix element takes at one phase-space point. + +### Cold, warm, replaced, and cleared + +One loaded OTF runtime follows this lifecycle: + +| Action | State afterwards | +| --- | --- | +| Load an OTF process | Cold: compact seed loaded, no selected family retained | +| `warm_up` selection A | A is retained, and exactly one `f64` point has also been evaluated | +| Evaluate selection A again | Reuses A's structural family; ordinary numerical execution remains | +| Successfully warm or evaluate selection B | B replaces A; only the most recent selected family is retained | +| Change model parameters, then evaluate the retained selection | The same structure is retained, and its numerical workspace is refreshed with the latest parameter values before execution | +| `runtime.save(path)` | The completed retained cache is written to disk; the current handle is unchanged | +| Load a matching process, then `runtime.load_cache(path)` | The saved family is restored without repeating current construction; numeric workspaces are initialized for the receiving handle | +| Python `runtime.clear()` | Cold again while the process output and current model parameters remain loaded | +| Close/free/drop the native handle | In-memory state is released; a later load starts cold unless a saved cache is explicitly restored | + +The in-memory family cache is **handle-local and last-family-only**. A handle +is simply one loaded `Runtime` object (or its C/Fortran/Rust/C++ equivalent). +A second `pyamplicol` CLI command starts a new process and loads a new handle, +so it does not inherit warm state from the first command. Keep one Python or +native runtime alive when same-selection reuse matters, or explicitly +[save and restore its cache](runtime-and-selectors.md#saving-an-otf-warm-cache) +between runs. A snapshot retains the current family, not previously evicted +selections, and does not checkpoint an unfinished cold construction. + +For a small complete Python example, including generation and a check that no +queries are rebuilt, see +[Saving an OTF warm cache](runtime-and-selectors.md#saving-an-otf-warm-cache). +The [native examples](native-apis.md#saving-and-restoring-an-otf-cache) show +`save` and `load_cache` in C, C++, Fortran and Rust. Restore the cache before +the first evaluation and use the same selectors as when it was saved. + +There is no public setting for retaining several OTF families: the most recent +successful family replaces the previous one. `evaluator.optimization.cores` +controls how many CPU workers may help with the initial construction; it does +not change this one-family retention rule or promise that later numerical +evaluation uses that many threads. `warm_up(..., n_cores=4)` overrides that +construction setting for one call, without discarding an already-warm family +or persisting the override in a saved cache. Query traces can be built in +parallel, with a bounded number of temporary traces; their shared-cache merge +and family finalization remain serial. + +### Explicit warm-up means exactly one binary64 point + +Python exposes the structural operation only on OTF runtimes: + +```python +flow = runtime.physics.color_flows[0] +one_point = (point,) + +result = runtime.warm_up( + one_point, + precision=16, + color_flows=(flow,), + n_cores=4, + progress=progress, +) +``` + +The outer batch must contain exactly one phase-space point, and the precision +must be `16` (native binary64). This point is genuinely evaluated after the +family is prepared. Passing a 128-point profiling batch, or asking for higher +precision, is rejected. A repeated `warm_up` for the same family reuses its +structure but still performs the required one-point evaluation; +`result.already_warm` and `result.warmed_query_count` distinguish reuse from a +cold construction. + +The optional progress callback (a function called whenever progress changes) +receives four stages: + +1. process preparation; +2. query-family construction; +3. family finalization; and +4. the first one-point evaluation. + +Progress includes completed/total work, elapsed time, worker count, and current +and peak resident memory (RAM physically held by the process) when the platform +supplies them. Without a callback or Python progress display, progress +reporting and its memory sampling are not enabled. The final first-evaluation +notification occurs after the warmed state has been committed and is not +cancellable. + +The packaged example supplies a coloured terminal progress display and result +table: + +```console +pyamplicol examples copy ./pyamplicol-examples +cd ./pyamplicol-examples +pyamplicol generate --card otf_pp_zjj.toml +python python/otf_pp_zjj_warm_up.py --n-cores 4 +pyamplicol profile --card otf_pp_zjj.toml +``` + +The Python program keeps one runtime loaded across `warm_up` and the timed +evaluations, so the expensive family construction is not accidentally repeated +by a second process. The final CLI `profile` command is a separate benchmark +run and therefore loads its own runtime; it is included to demonstrate the +standard profiling interface, not to reuse the Python program's in-memory +family. + +### The same warm-up in native languages + +All native interfaces use one flattened point in +`[external particle][E, px, py, pz]` order and stable selector IDs. Omitting an +axis sums it. + +In C, warm one flow and all helicities: + +```c +const char *one_flow[] = {flow_id}; +RusticolWarmUpResult result = {0}; +int status = rusticol_runtime_warm_up_f64_with_cores( + handle, point, momentum_count, + NULL, 0, one_flow, 1, + 2, report_progress, user_data, &result); +``` + +The callback has this signature and returns nonzero to continue: + +```c +int report_progress( + const RusticolWarmUpProgressEvent *event, + void *user_data); +``` + +The wrappers preserve the same selector order: + +```cpp +auto result = runtime.warm_up(point, {}, {flow_id}, report_progress, 2); +``` + +```fortran +call runtime%warm_up(point, result, color_ids=flow_ids, & + progress_callback=report_progress, progress_user_data=user_data, ierr=ierr, n_cores=2) +``` + +```rust +let selectors = Selectors::all().with_colors([flow_id]); +let result = runtime.warm_up_f64( + &point, &selectors, Some(2), Some(&mut report_progress), +)?; +``` + +These examples request up to two construction workers for this call only. +Use zero in C/C++, omit `n_cores` in Fortran, or pass `None` in Rust to retain +the process-output default. The original C `rusticol_runtime_warm_up_f64` +entry point keeps its argument list without `n_cores` and selects that default. +Explicit Fortran values below one and Rust +`Some(0)` are rejected. The override neither changes evaluation defaults nor +invalidates an existing warm family or saved structural cache. + +For all flows at one helicity, put one helicity ID in the helicity argument and +leave the colour argument empty. Passing no callback is valid. C++ releases at +destruction, Fortran exposes `runtime%close()`, Rust releases on `drop`, and C +uses `rusticol_runtime_free(handle)`. The native wrappers do not expose +Python's clear-without-unload convenience; close and reload to return to a +fully cold native handle. + +### Model-parameter updates do not keep stale numbers + +`runtime.set_model_parameters(...)` updates the loaded runtime atomically. On +the next warm-up or evaluation, an already retained OTF family refreshes its +parameter-dependent workspace before executing. Merely changing a numerical +model parameter does not require rebuilding the same structural selector +family, and the old parameter values are not silently reused. + +Python `runtime.clear()` discards the OTF family, prepared process state, and +temporary workspace while keeping the process output and the latest parameter +values loaded. In recurrence, compiled, and eager modes, `clear()` is a no-op +because they do not have the corresponding OTF family cache. + +## A practical decision sequence + +1. Decide the colour accuracy from the physics question: `lc`, `nlc`, or + `full`. +2. For recurrence, eager, or compiled LC output, decide which slice is repeated + most often: + one-flow/helicity-sum suggests `topology-replay`; all-flow/one-helicity + suggests `all-flow-union`. +3. Start with recurrence for a reusable prepared-model output. Use eager when + the time to generate and load a ready-to-run output is the priority, and + compiled when process-wide compilation or a raw JSON/UFO model is needed. +4. Consider OTF when a compact process output and one dominant LC selection + outweigh an expensive first construction. Keep the same loaded runtime + alive so later calls reuse it. +5. Keep generation coverage complete until a measured production workload + justifies a non-reusable specialized output. +6. Profile the exact process, selector, parameters, batch size, and target + machine used by the application. + +See [Configuration](configuration.md#color-accuracy-and-lc-layout) for every card +field, [Runtime and Selectors](runtime-and-selectors.md) for resolved output and +per-point selectors, and the [Native APIs](native-apis.md) for complete +buildable language examples. diff --git a/pyamplicol/docs/user/models-and-processes.md b/pyamplicol/docs/user/models-and-processes.md new file mode 100644 index 0000000000..c5c06bc9f0 --- /dev/null +++ b/pyamplicol/docs/user/models-and-processes.md @@ -0,0 +1,473 @@ +--- +title: "Models and Processes" +nav_order: 1 +parent: "Configuration" +--- + +# Models and Processes + +pyAmpliCol separates **model compilation** from **process generation**. A model +describes particles, parameters, interactions, propagators, Lorentz structures, +and color data. A process request selects external states and asks pyAmpliCol to +construct the supported tree amplitudes and executable runtime artifact. + +## Model source choices + +| Source | Typical use | Executes Python while loading? | +| --- | --- | --- | +| `built-in-sm` | Fast Standard Model compatibility workflows and prepared recurrence/eager/OTF kernels | No | +| `built-in-sm-heft` | Standard Model plus scalar Higgs-effective gluon interactions | No | +| Serialized JSON | Recommended portable external-model input | No | +| UFO directory | Trusted development input | **Yes** | +| Compiled model JSON | Reuse normalized model IR | No | +| `.pyamplicol-model` | Reuse model IR plus a prepared local-kernel backend | No | + +## Built-in Standard Model + +```toml +[model] +source = "built-in-sm" + +[process] +entries = [{ expression = "u u~ > g g", name = "uubar_gg" }] +``` + +Direct CLI: + +```console +pyamplicol generate 'u u~ > g g' artifacts/uubar_gg \ + --model built-in-sm +``` + +The built-in model selects the wheel-owned prepared JIT O2 bundle for default +recurrence or eager execution. It also retains compatibility aliases that an +arbitrary external model does not automatically inherit. + +## Built-in scalar HEFT model + +Select `built-in-sm-heft` for an independent model containing the packaged +Standard Model plus the scalar Hgg, Hggg, and Hgggg effective interactions: + +```console +pyamplicol model inspect built-in-sm-heft +pyamplicol generate 'g g > H g g' artifacts/gg_hgg \ + --model built-in-sm-heft \ + --coupling-order-policy explicit --max-coupling-order HIG=1 \ + --color-accuracy full --execution-mode recurrence +pyamplicol inspect artifacts/gg_hgg +``` + +The equivalent packaged run card is `builtin_sm_heft.toml`: + +```toml +[model] +source = "built-in-sm-heft" + +[process] +entries = [{ expression = "g g > H g g", name = "gg_hgg" }] +coupling_order_policy = "explicit" +max_coupling_orders = { HIG = 1 } +``` + +The model inherits the packaged SM parameter defaults, including `MH = 125` +GeV, and has no runtime dependency on a UFO checkout. The effective coupling +`GH` depends on model parameters (`G`, `vev`, `MH`, and `MT`), not event +momenta; the momentum factors in the Hgg and Hggg Lorentz tensors are retained. +Update those external inputs through a runtime parameter card rather than +treating the derived `GH` as an independent input. The built-in selector uses +its canonical default restriction and simplification policy. + +The wheel-owned portable JIT O2 pack makes `eager`, `recurrence`, and +`on-the-fly` execution available directly from this selector. Compiled mode +supports the `jit`, `asm`, and `cpp` evaluator backends. Explicitly prepared +HEFT bundles can likewise use any of those backends. `builtin_sm_heft`, +`sm-heft`, and `sm_heft` are accepted as configuration aliases. + +The canonical selector is also available through the typed API: + +```python +from pyamplicol import Generator, ModelSource + +model = ModelSource.built_in_sm_heft() +plan = Generator().plan("g g > H g g", model=model) +print(plan.concrete_processes) +``` + +### Trusted external HEFT UFOs + +A trusted scalar-HEFT UFO directory can be supplied through the ordinary UFO +path described below. Inspect it before starting process generation: + +```console +pyamplicol model inspect /path/to/trusted/ScalarHEFT_UFO +pyamplicol generate 'g g > H g g' artifacts/ufo_heft_hgg \ + --model /path/to/trusted/ScalarHEFT_UFO \ + --coupling-order-policy explicit --max-coupling-order HIG=1 \ + --color-accuracy full --execution-mode compiled --backend jit +``` + +UFO modules execute Python while loading. pyAmpliCol recognizes the supported +scalar one-/two-structure-constant contact families and rejects unsupported +Lorentz, colour, or form-factor structures during model preflight; arbitrary +HEFT UFOs are therefore not assumed compatible. +The separately downloaded FeynRules HEFT UFO used by developer acceptance is +not packaged, redistributed, or required at runtime. For portable user +workflows, prefer `built-in-sm-heft`. + +## Serialized JSON model + +Serialized JSON is the recommended portable external-model form: + +```toml +[model] +source = "models/json/sm/sm.json" +restriction = "default" +``` + +Copy the packaged JSON/UFO examples into an editable workspace: + +```console +pyamplicol examples copy ./pyamplicol-examples --force +cd pyamplicol-examples +pyamplicol model inspect models/json/sm/sm.json +``` + +The wheel includes serialized `sm`, `scalars`, and `scalar_gravity` examples. +They become ordinary filesystem paths under the copied workspace; no code needs +to know the package installation directory. + +If only the packaged models are wanted, the copied +`python/copy_packaged_models.py` helper can populate a separate empty model +workspace: + +```console +python python/copy_packaged_models.py ../pyamplicol-models +``` + +## Trusted UFO model + +A UFO directory uses the same source field: + +```toml +[model] +source = "/path/to/MyUFO" +restriction = "default" + +[process] +entries = [{ expression = "u u~ > z g" }] + +[generation] +output = "artifacts/my_ufo_process" + +[evaluator] +execution_mode = "compiled" +backend = "jit" +``` + +> UFO modules are Python code and execute during loading. Only load a UFO model +> from a source you trust. Serialize it for portable or automated workflows. + +In a TOML card, `restriction` is normally a loader name such as `default`, +`no_widths`, or `none`. With the typed Python API, an explicit restriction file +may be supplied. Relative typed-API restriction paths are resolved from the +model directory; card names select the conventional `restrict_.json` or +`restrict_.dat` file. + +## Model commands + +Inspect model capabilities and diagnostics: + +```console +pyamplicol model inspect models/json/sm/sm.json --restriction default +``` + +Compile portable normalized model IR: + +```console +pyamplicol model compile \ + models/json/sm/sm.json models/sm.pyAmplicol-model.json \ + --restriction default +``` + +Enumerate concrete processes without generating a process artifact: + +```console +pyamplicol model processes 'p p > Z j j' \ + --model models/sm.pyAmplicol-model.json \ + --multiparticle 'p=d,d~,g' \ + --multiparticle 'j=d,d~,g' \ + --flavor-scheme 2 --max-quark-lines 2 +``` + +Unsupported model features are reported during model preflight, before an +expensive process build. + +## Prepared kernel bundles + +A file ending in `.pyamplicol-model` contains normalized model IR, exact +expressions, and one prepared local-kernel backend. It enables recurrence, +eager, or on-the-fly process generation without compiling each local kernel +again. + +Create a JIT O2 prepared bundle: + +```console +pyamplicol model compile \ + models/json/sm/sm.json models/ufo-sm-jit-o2.pyamplicol-model \ + --backend jit --jit-optimization-level 2 +``` + +Use it for eager generation: + +```console +pyamplicol generate 'd d~ > z g g g' artifacts/ddbar_z3g_eager \ + --model models/ufo-sm-jit-o2.pyamplicol-model \ + --execution-mode eager +``` + +The three prepared-kernel lanes store different process-level structures: +eager writes compact invocation tables, recurrence writes compact current +schedules, and on-the-fly writes a compact process seed from which the selected +query-local recurrence family is built. Each completed process artifact carries +the referenced kernels and is standalone; the source prepared bundle is not +needed at runtime. + +At native `f64`, OTF supports LC helicity/flow selection and the singleton +contracted color axis used by NLC and full colour. Contracted color rejects LC +flow selectors and is intended as a low-multiplicity correctness capability, +not a high-multiplicity practicality promise. OTF is binary64-only; eager and +recurrence can retain the Symbolica-backed exact path when their artifact does. + +Prepared JIT state uses the exact-O2 portability contract across supported +64-bit little-endian macOS and Linux hosts. Process-local compiled JIT O1 and +O2 are also portable; C++/ASM and explicit O0/O3 JIT remain target-native. See +[Artifacts and Portability](artifacts-and-portability.md). +See [Generation Modes and Evaluators](generation-modes-and-evaluators.md) for +the generation and warm-state trade-offs. + +## Typed model API + +```python +from pyamplicol import ModelSource + +json_source = ModelSource.from_path("models/json/sm/sm.json") +ufo_source = ModelSource.from_path( + "/path/to/MyUFO", + restriction="restrict_default.dat", +) + +compiled = json_source.compile() +print(compiled.name) +print(compiled.capabilities.supported_color_accuracies) +print(compiled.supported) + +compiled.write("models/sm.pyAmplicol-model.json") +compiled.write_parameter_card("data/sm_parameters.json") +``` + +`CompiledModel` is an immutable public handle. Stable metadata is available +through `compiled.info`; compiler-owned expression/tensor IR remains private. +Compiled models are content-addressed by their source, restriction, +compiler/schema versions, and normalization/tensor policies. + +See [Python API](python-api.md) for generation with a compiled handle. + +## Parameter restrictions and runtime cards + +Model compilation restrictions determine the model baked into the artifact. +Mutable external UFO parameters may then be updated at runtime. + +A generated parameter card is a flat JSON mapping: + +```json +{ + "aS": 0.118, + "MZ": 91.1876, + "complex_parameter": [1.0, 0.0] +} +``` + +Each value is a finite real number or `[real, imaginary]`. Apply it through the +runtime CLI: + +```console +pyamplicol evaluate artifacts/pp_zjj \ + --process 'd d~ > g z g' \ + --model-parameters data/model_parameters.json \ + --momenta data/pp_zjj_momenta.json +``` + +The generated standalone API drivers also accept repeated +`--set-parameter NAME REAL IMAG` options; those overrides are applied after the +card and win over its values. The main `pyamplicol evaluate` command uses the +`--model-parameters` card, while Python callers can use +`Runtime.set_model_parameter()` or `Runtime.set_model_parameters()`. Updates +are atomic: unknown, immutable, invalid, or non-finite input rejects the whole +batch. + +## Process expressions + +A process has incoming and outgoing particles separated by `>`: + +```text +d d~ > Z g g +``` + +Named requests provide stable IDs: + +```toml +[process] +entries = [ + { expression = "u u~ > Z g", name = "uubar_Zg" }, + { expression = "u u~ > Z g g", name = "uubar_Zgg" }, +] +``` + +Repetition syntax such as `3*scalar_0` is supported for particle names. + +## Multiparticles + +```toml +[process] +entries = [{ expression = "p p > Z j j" }] +flavor_scheme = 2 +max_quark_lines = 2 + +[process.multiparticles] +p = ["d", "d~", "g"] +j = ["d", "d~", "g"] +``` + +Every model supplies `all`, in declaration order, containing its valid +propagating physical external states. Ghosts, Goldstones, non-propagating +records, and auxiliary particles are excluded. User labels merge over defaults; +an explicit `all` overrides the generic one. + +Example enumeration: + +```console +pyamplicol model processes 'p p > all all' \ + --model built-in-sm --flavor-scheme 1 --max-quark-lines 0 +``` + +`all` is intentionally broad and may produce a combinatorial expansion for a +large UFO model. Prefer a focused custom label when possible. + +With a UFO or other compiled model (and in `model processes`), expansion +enumerates each side of `>` as a multiset of particles, so two candidates +never differ only by an ordering within a side. Process IDs count these +candidates, so they differ from those of earlier pyAmpliCol versions, which +counted every ordered candidate: re-read them with `pyamplicol inspect` after +regenerating. The built-in SM catalogue keeps its own ordered enumeration. +When generating with a compiled model, candidates that violate an additive +charge conserved by every model vertex are dropped before the colour and +amplitude checks. The charges are derived from the vertex list itself: electric charge, +and quark or lepton numbers per generation whenever the vertices conserve +them (quark numbers reduce to baryon number with a non-diagonal CKM matrix). +With the shipped serialized Standard Model (`sm.json`, default +restriction) this leaves exactly the 385 tree-level subprocesses of +`p p > j j j j`. `model processes` applies the same filter, but not the +colour and amplitude checks. The built-in SM keeps its catalogue prefilters (`flavor_scheme`, +`max_quark_lines`). See +[Conserved charges in UFO expansion](process-grouping.md#conserved-charges-in-ufo-expansion). + +## Permutation-equivalent processes + +Multiprocess expansion stores one representative for candidates that differ +only by reordering within the incoming side and/or within the outgoing side. +At runtime, this is valid: + +```console +pyamplicol inspect artifacts/pp_zjj --process 'd d~ > g z g' +``` + +even when the representative is stored as `d d~ > Z g g`. Rusticol maps the +public particle order centrally across momenta, particle metadata, helicities, +LC flow words, selectors, reductions, and resolved values. + +Rules: + +- incoming particles may only permute with incoming particles; +- outgoing particles may only permute with outgoing particles; +- repeated identical particles use deterministic first-unused matching; +- ambiguous representative matches fail with candidate stable IDs; +- stable process and explicit alias IDs take precedence over inferred matching. + +The current [Born Correlations](../correlators.md) API is an exception: it +requires the generated external-leg ordering and rejects nonidentity +permutations. Declare spin and colour legs using that process's one-based +labels; a declaration used for a process set must be valid for every generated +member. Ordinary runtime permutation support is unchanged. + +## Flavour-blind generation + +`--flavor-blind` (`process.flavor_blind`) generates one subprocess per flavour +class: subprocesses related by a relabelling of flavours with equal spin, +colour, mass and width and, unless `--no-consider-charge-conjugation`, by +charge conjugation. For the built-in SM, `p p > j j j j` at `flavor_scheme = 5` +has 486 subprocesses, 33 flavour classes and 23 classes with charge +conjugation. + +Process IDs are assigned before the classes are formed, so a generated +representative has the same ID with or without the flag. The representative +is the canonical member: smaller `|PDG|` first and particles before +antiparticles, so `d d~ > g g` represents `u u~ > g g` and `d g > d g` +represents `u~ g > u~ g`. The other members are not generated; they are +recorded in `process-groups.json` with the leg permutation onto the +representative. A subprocess with an explicit `ProcessAlias` keeps its own +artifact entry. + +The equality is assumed, not verified. A member gets a warning, logged and +stored with it, when its relation is not a symmetry of the model vertices +that the representative is generated with: those within +`max_coupling_orders` and, under the default `minimal` coupling-order policy, +within the representative's minimal coupling-order envelope. A relabelling +must map each such vertex onto one with the same structure and coupling; +charge conjugation also requires every such vertex to couple fermions +vector-like. Pure-QCD jet classes therefore pass, while `p p > z j` warns: the +photon and Z couplings distinguish `u` from `d`, and the chiral Z coupling +breaks charge conjugation. The check looks at vertices rather than diagrams +and can warn about a relation that holds for the subprocess at hand. +[Process Grouping](process-grouping.md) covers flavour classes, warnings and +the numerical analysis in detail. + +## Tree-level support and omitted candidates + +Multiparticle expansion may find a syntactically valid process with no +model-supported tree-level amplitude. pyAmpliCol reports and omits it rather +than creating a zero or broken artifact. In the primary SM example, +`g g > Z g g` is loop-induced and therefore omitted. + +This is distinct from an unsupported model feature: model preflight rejects +unsupported theory structures with an actionable diagnostic. + +## Current generic UFO support + +The generic path covers the proven subset used by shipped tests and examples, +including: + +- scalar, Dirac-like fermion, vector, and proven massless spin-2 paths; +- SU(3) singlet, fundamental, antifundamental, and adjoint particles; +- implemented identity, gamma/gamma5, projector, sigma, metric, and momentum + Lorentz structures; +- trilinear kernels, color-singlet contact trees, and proof-gated colored + contacts; +- LC flows and contracted NLC/full SU(3) calculations. + +These general model capabilities do not imply LC/NLC or every execution mode +is available for correlations. The opt-in correlated Born path currently uses +full SU(3), with singlet, fundamental, antifundamental, and adjoint colour +representations, and generic compiled amplitudes. Spin replacements apply only +to four-component vector sources, not fermion or spin-2 sources. Supported +model preflight remains required; see [Born Correlations](../correlators.md) +for the narrower execution and API scope. + +Preflight rejects Majorana/FNV fermions, spin 3/2, sextets, epsilon color +tensors, multiple/non-SU(3) color groups, unknown form-factor functions, +unsupported custom propagator tensors, and unproven general colored +higher-point contacts. Massive spin-2 is experimental; the packaged massless +`scalar_gravity` model is a tested path. + +The exact, code-derived list of every condition, rejection and diagnostic +code is kept in [UFO Model Coverage](ufo-model-coverage.md). For the +validated release boundary, see [Release and Support](release-and-support.md). diff --git a/pyamplicol/docs/user/native-apis.md b/pyamplicol/docs/user/native-apis.md new file mode 100644 index 0000000000..03e5879020 --- /dev/null +++ b/pyamplicol/docs/user/native-apis.md @@ -0,0 +1,677 @@ +--- +title: "Native APIs" +nav_order: 2 +parent: "Python API" +--- + +# Native APIs + +Every binary pyAmpliCol wheel includes a target-specific Rusticol SDK and every +generated process artifact can include a ready-to-build API bundle. Python, +C11, C++17, Fortran 2008, and Rust 2021 select and perform ordinary total/resolved +evaluation of the same artifact through the same Rusticol core. C, C++, +Fortran, and the standalone Rust interface share the public C ABI v1; Python +uses the wheel's PyO3 binding for these native operations. + +For a shared matrix-element provider that can be loaded by MadSpace or linked +into another executable, see [UMAMI and MadSpace](umami-api.md). Its generated +`API/umami` Makefile builds `libumami.so` and a linked standalone driver using +the same installed Rusticol SDK. + +`rusticol-config` (also available as `python -m rusticol_config`) only reads SDK +paths, version and linker metadata. It does not initialize pyAmpliCol or +Symbolica, so build-system queries can run alongside numerical evaluations. + +FFT basis is a generation-time choice, not a native runtime flag. Generation +with `--color-accuracy full` or Python's `ColorConfig(accuracy="full")` +automatically tries adaptive adjoint FFT for recurrence/on-the-fly execution, +falling back to direct for unsupported FFT plans. Certified gluon trees and +single-insertion HEFT use DDM; quark processes use fundamental chains, while +uncertified interactions retain trace. +The saved `fft_basis_selection` reports the actual representation and reason; +the defaults are automatic contraction and adaptive adjoint selection. +C, C++, Fortran and Rust load either generated artifact and use their existing +total/resolved evaluation calls unchanged. Both FFT choices require NLC/full +colour in recurrence or on-the-fly mode. Automatic LC, compiled/eager, and +correlated generation use direct contraction and trace basis. Select +`--color-contraction direct` to opt out; `--fft trace` or `--fft adjoint` forces +FFT and retains unsupported-plan errors. Forced adjoint FFT rejects correlations. + +> **Prerequisites:** install a binary wheel as described in [Installation](installation.md), +> activate that environment, and generate the primary artifact from +> [Quick Start](quick-start.md). Native consumers need the corresponding language compiler; +> they do **not** need a Rust compiler unless the consumer itself is Rust. + +## Correlated Born evaluations + +The C, C++, Fortran and standalone Rust SDKs expose the same generation-time +colour catalogue, literal spin replacements and grouped correlated evaluations. +These additions are on the `correlators` development branch; use its matching +SDK and library, not an older installed release. Declare the operators with +`Generator.generate(..., correlators=CorrelatorConfig(...))` or +`generate ... --correlators FILE.json` as in [Born Correlations](../correlators.md). + +Native correlated evaluation is **binary64 only**, like the ordinary native +SDK. It runs in RustiCol without Python or Symbolica. The separate Python +correlated executor retains Symbolica-backed double-double and arbitrary +precision; its `precision=16` path also uses that executor. + +LC, NLC and full colour are selected at generation for each inserted colour +matrix, independently of connection order. The underlying amplitudes remain +complete and full-colour: ordinary `evaluate`/`evaluate_resolved` still return +that underlying process, while the correlated `"born"` request returns the +Born result at the requested correlated accuracy. Spin defaults affect only +correlated calls, never ordinary totals or resolved components. + +### Catalogue, grouped requests and result indices + +| Language | Catalogue | Grouped evaluation and component access | +| --- | --- | --- | +| C | `rusticol_runtime_color_correlation_count`, `_id`, `_catalogue_json` | `rusticol_runtime_evaluate_correlated_many_f64`; output offset `2*(request*point_count + point)` gives real then imaginary parts | +| C++ | `runtime.color_correlation_ids()`, `.color_correlation_catalogue_json()` | `runtime.evaluate_correlated_many(...)`; `result(request, point)` is `std::complex` | +| Fortran | `runtime%color_correlation_ids()`, `%color_correlation_catalogue_json()` | `runtime%evaluate_correlated_many(...)`; `values(point, request)` is `complex(c_double_complex)` | +| Rust | `runtime.color_correlation_ids()`, `.color_correlation_catalogue_json()` | `runtime.evaluate_correlated_many_f64(...)`; `result.get(request, point)` returns `Option` | + +The ID list and full JSON histories follow generation order, including +`"born"`. C/C++/Rust indices are zero-based; Fortran array indices are +one-based. Requests follow caller order and points follow input order. +Different requests share amplitudes and unchanged spin-dependent stages in +one call. All momenta use `[point][external particle][E,px,py,pz]`. + +For example, using the registered `"T13"` dipole from the correlation guide: + +```cpp +// runtime and flattened momenta have already been prepared. +std::vector vectors{ + {3, {{{0., 0., 1., 0.}}}}}; +std::vector physical; +auto values = runtime.evaluate_correlated_many(momenta, point_count, { + {"born", physical}, {"T13", physical}, {"T13", vectors}, +}); +std::complex first_spin_dipole = values(2, 0); +runtime.set_spin_correlation_vectors(vectors); +auto inherited = runtime.evaluate_correlated(momenta, point_count, "T13"); +runtime.set_spin_correlation_vectors(); // Clear defaults. +``` + +```fortran +type(rusticol_correlated_request) :: requests(2) +complex(c_double_complex), allocatable :: values(:, :) +! Unallocated spin_vectors inherits defaults; allocated size zero is physical. +requests(1)%color_correlation = "born" +allocate(requests(1)%spin_vectors(0), requests(2)%spin_vectors(1)) +requests(2)%color_correlation = "T13" +requests(2)%spin_vectors(1)%leg = 3_c_size_t +allocate(requests(2)%spin_vectors(1)%components(4, 1)) +requests(2)%spin_vectors(1)%components(:, 1) = cmplx([0,0,1,0], kind=c_double) +call runtime%evaluate_correlated_many(momenta, point_count, requests, values) +print *, values(1, 2) ! First point, spin-correlated dipole. +``` + +```rust +let mut physical = rusticol::CorrelatedRequest::new("born"); +physical.spin_vectors = Some(vec![]); +let mut spin = rusticol::CorrelatedRequest::new("T13"); +spin.spin_vectors = Some(vec![rusticol::SpinCorrelationVector { + leg: 3, + components: vec![[rusticol::Complex64::new(0., 0.), + rusticol::Complex64::new(0., 0.), + rusticol::Complex64::new(1., 0.), + rusticol::Complex64::new(0., 0.)]], +}]); +let values = runtime.evaluate_correlated_many_f64( + &momenta, point_count, &[physical, spin], &[])?; +let first_spin_dipole = values.get(1, 0).unwrap(); +``` + +Spin labels are **one-based public external legs** in every language. Supply +one complex four-vector to broadcast, or one per momentum point. Fortran uses +`components(4,points)`; C++ and Rust use a vector of complex four-vectors. The +C representation explicitly interleaves real/imaginary doubles, avoiding any +assumption about a language's complex-number memory layout. The nonempty set +of replaced legs must match a declared spin class. + +A C request sets `use_default_spin_vectors=1` with zero explicit vectors to +inherit the setter, or `0` to use its own vectors (zero means physical +helicities). C++ uses `std::nullopt` versus an explicit vector; Rust uses +`None` versus `Some(...)`; Fortran uses unallocated versus allocated +`spin_vectors`. Setters copy their input, and empty setter input clears it. +Optional helicity IDs select physical helicities of the unreplaced legs. + +Complete examples for the mixed adjoint/fundamental N3LO interference are in +[`examples/native/correlated.c`](https://github.com/MadGraphTeam/pyamplicol/blob/correlators/examples/native/correlated.c), +[`correlated.cpp`](https://github.com/MadGraphTeam/pyamplicol/blob/correlators/examples/native/correlated.cpp), +[`correlated.f90`](https://github.com/MadGraphTeam/pyamplicol/blob/correlators/examples/native/correlated.f90), and +[`correlated.rs`](https://github.com/MadGraphTeam/pyamplicol/blob/correlators/examples/native/correlated.rs). +The [generation example](../correlators.md#native-correlated-evaluations) +prepares their input. Build all four with `make -C examples/native correlated`; +each executable accepts `ARTIFACT PROCESS`. The generated ordinary standalone +drivers still demonstrate total/resolved evaluation, not correlations. + +## What the wheel provides + +The installed SDK is owned by the wheel: + +```text +pyamplicol/_sdk/ + include/rusticol.h + include/rusticol.hpp + rust/rusticol.rs + fortran/rusticol.f90 + lib/librusticol_capi.a + config.py + metadata.json + link.json +``` + +`rusticol-config` validates this SDK and emits correctly quoted paths and +target-specific link arguments: + +```console +rusticol-config --abi-version +rusticol-config --version +rusticol-config --target +rusticol-config --include-dir +rusticol-config --library +rusticol-config --fortran-source +rusticol-config --rust-source +rusticol-config --cflags +rusticol-config --libs +rusticol-config --rustflags +rusticol-config --cargo-rustflags +rusticol-config --json +``` + +For a 0.2.0 macOS arm64 release-candidate wheel, the first three commands print: + +```text +1 +0.2.0 +aarch64-apple-darwin +``` + +The target naturally differs on Intel macOS and manylinux. The ABI remains 1. + +Run these commands from the environment that contains pyAmpliCol: + +```console +python -m venv .venv +. .venv/bin/activate +python -m pip install pyamplicol +rusticol-config --json +``` + +If pyAmpliCol was invoked through an explicit path, either activate that +environment or put its `bin` directory on `PATH`. An explicit +`RUSTICOL_CONFIG=/path/to/rusticol-config` also works. + +In the source workflow, generated drivers below a checkout can find the nearest +`.venv/bin/rusticol-config` created by `just dev-install`. This is a +development convenience, not an embedded build-machine path. + +`--cflags`, `--libs`, and `--rustflags` emit shell-escaped argument streams. +By contrast, `--cargo-rustflags` emits the same linker arguments using Cargo's +unit-separator encoding for `CARGO_ENCODED_RUSTFLAGS`; it is not a shell +argument stream. `--json` exposes typed arrays when a program should avoid +parsing either textual form. + +## The generated API bundle + +With `generation.emit_api_bundle = true` (the default for ordinary +generation), an artifact contains: + +```text +artifacts/pp_zjj/API/ + validation_points.dat + python/check_standalone.py + c/check_standalone.c + rust/check_standalone.rs + cpp/check_standalone.cpp + fortran/check_standalone.f90 + umami/umami_driver.c + umami/metadata.json +``` + +The per-language Makefiles place binaries, objects, and Fortran modules in the +sibling `artifacts/.pyamplicol-api-build/` directory. The integrity-checked +artifact itself is not modified. + +The ordinary language drivers: + +- selects a process by stable ID or expression; +- accepts an optional JSON kinematic point; +- accepts a UFO-style JSON model-parameter card and direct overrides; +- evaluates all resolved components and sums them explicitly; +- compares the explicit sum with the optimized total; +- prints either a human result or JSON. + +The [UMAMI driver](umami-api.md#generate-and-build) instead demonstrates the +shared-library ABI and its channel/flavour selectors. Its Makefile places +libraries and executables outside the artifact in the same way. + +## One process expression in all five languages + +The following commands deliberately request `d d~ > g z g`, even though its +stored representative uses another outgoing order. Rusticol resolves the +unique representative and permutes momenta, particles, helicities, color +flows, reductions, and resolved output metadata consistently. + +```console +python artifacts/pp_zjj/API/python/check_standalone.py \ + --process 'd d~ > g z g' \ + --set-parameter aS 0.117 0 \ + --json + +make -C artifacts/pp_zjj/API/c run \ + ARGS='--process "d d~ > g z g" --set-parameter aS 0.117 0 --json' + +make -C artifacts/pp_zjj/API/rust run \ + ARGS='--process "d d~ > g z g" --set-parameter aS 0.117 0 --precision 16 --json' + +make -C artifacts/pp_zjj/API/cpp run \ + ARGS='--process "d d~ > g z g" --set-parameter aS 0.117 0 --json' + +make -C artifacts/pp_zjj/API/fortran run \ + ARGS='--process "d d~ > g z g" --set-parameter aS 0.117 0 --json' +``` + +No generated alias is required. Case and whitespace are normalized. Incoming +legs may permute only among incoming legs; outgoing legs may permute only among +outgoing legs. An ambiguous expression fails with the candidate stable IDs. + +The drivers also accept the representative stable ID: + +```console +--process p_p_to_z_j_j_3 +``` + +After loading, the API exposes the representative stable key and active +representative-to-public permutation. The public process expression remains +the ordering requested by the caller. + +The same selectors reach processes that a `--flavor-blind` generation listed +in `process-groups.json` without generating them. Rusticol evaluates the +representative, multiplies every matrix element by the member's exact factor, +and, for a charge-conjugated member, reverses the LC flow words (and maps flow +selectors back). Runtime metadata JSON reports `process_group_member_of`, +`process_group_factor` (`{"numerator", "denominator"}`), and +`charge_conjugated` in every language. The C ABI adds one getter: + +```c +uint64_t numerator, denominator; +int32_t conjugated; +rusticol_runtime_process_group_relation(runtime, &numerator, &denominator, &conjugated); +``` + +Selections other than non-generated members report `1/1` and `0`. See +[Process-group members](process-selection-and-permutations.md#process-group-members). + +## Custom kinematics + +All five drivers accept `--kinematics PATH`. The file contains exactly one +point, either directly as `[external][4]` or inside a singleton batch +`[[external][4]]`. Its leg order is the order written in `--process`. + +For `d d~ > g z g`, `my_sample_point.json` may be: + +```json +[ + ["250.0", "0", "0", "250.0"], + ["250.0", "0", "0", "-250.0"], + ["204.406", "204.406", "0", "0"], + ["91.188", "0", "0", "0"], + ["204.406", "-204.406", "0", "0"] +] +``` + +Each vector is `[E, px, py, pz]`. Components may be JSON numbers or decimal +strings. Multiple points, booleans, non-finite values, incorrect rank, or an +incorrect particle count are rejected. + +```console +python artifacts/pp_zjj/API/python/check_standalone.py \ + --process 'd d~ > g z g' \ + --kinematics my_sample_point.json \ + --precision 80 +``` + +When an artifact retains an exact evaluator, the Python driver keeps decimal +strings as `Decimal` values at non-f64 precision without an intermediate f64 +round trip. Native drivers convert the same input to f64 and accept only +precision 16. OTF artifacts are binary64-only through the Python driver too. + +When `--kinematics` is omitted, the bundled representative validation point is +reordered into the public process order automatically. + +## Model-parameter cards and overrides + +`--model-parameters PATH` reads one flat JSON object. A real external value is +a finite number; a complex external value is `[real, imaginary]`: + +```json +{ + "aS": 0.117, + "MZ": 91.188, + "complex_external_parameter": [1.0, -0.25] +} +``` + +Apply the card, then override one entry: + +```console +make -C artifacts/pp_zjj/API/cpp run \ + ARGS='--process "d d~ > g z g" \ + --model-parameters data/model_parameters.json \ + --set-parameter aS 0.1165 0 --json' +``` + +Direct overrides are applied after the card and win atomically. Unknown, +immutable, non-finite, or otherwise invalid entries reject the complete update. + +Compiled, eager, recurrence, and OTF artifacts use this same API surface when +the bundle is emitted. Eager artifacts carry compact invocation tables, +recurrence artifacts carry current schedules, and OTF artifacts carry the +referenced prepared kernels plus a compact process seed. A native caller never +needs the source `.pyamplicol-model` bundle used during generation. + +## OTF warm-up from native APIs + +OTF callers can make cold-path work explicit with +`rusticol_runtime_warm_up_f64_with_cores` in C, `rusticol::Runtime::warm_up` in C++, +`runtime%warm_up` in Fortran, or `Runtime::warm_up`/`warm_up_f64` in the safe +Rust wrapper. Each call accepts exactly one flattened binary64 point and +optional global helicity/color ID subsets, constructs and retains that family, +and performs its first evaluation. + +In C the entry point is: + +```c +const char *one_flow[] = {flow_id}; +RusticolWarmUpResult result = {0}; +int status = rusticol_runtime_warm_up_f64_with_cores( + handle, point, momentum_count, + NULL, 0, one_flow, 1, + 2, report_progress, user_data, &result); +``` + +The construction-core override is per call: C takes `size_t n_cores` before +the callback, with zero selecting the process-output default. The original +`rusticol_runtime_warm_up_f64` keeps its signature without `n_cores` and uses +that default. C++ takes a +trailing `std::size_t n_cores = 0`; Fortran accepts optional integer `n_cores` +and rejects an explicitly supplied value below one; Rust takes +`Option` before the callback, with `None` selecting the default and +`Some(0)` rejected. Positive values bound independent query-trace construction; +shared-cache merging and family finalization remain serial. The override does +not change later evaluation defaults or structural cache identity. Generated +API bundles use these same SDKs; their standalone drivers retain ordinary lazy +evaluation unless the caller explicitly invokes warm-up. + +The optional fixed-layout callback reports stage, completed and total query +counts, elapsed time, construction workers, and current/peak RSS when the +platform exposes it. Updates are throttled and callbacks run only on the +coordinating caller thread. Returning zero from the C/Fortran callback, or +`false` from the C++/Rust callback, cancels at the next pre-commit boundary; +the final post-commit first-evaluation notification cannot be cancelled. With no +observer, ordinary evaluation and warm-up perform no progress-callback or +memory-sampling work. + +The cache belongs to one runtime handle and retains only the last selected +family. Native wrappers do not expose Python's clear-without-unload +convenience: close/free/drop and reload to start fully cold. See the +[OTF lifecycle walkthrough](lc-workloads-and-execution-modes.md#the-otf-warm-state-lifecycle) +for the corresponding C++, Fortran, and Rust call fragments. + +## Saving and restoring an OTF cache + +All native APIs can save the completed structural cache of an OTF handle and +restore it into a handle loaded from the matching process output. The following +fragments use an OTF output for `g g > g g` at `artifacts/otf_gg_gg`; the +[Python example](runtime-and-selectors.md#saving-an-otf-warm-cache) gives the +generation command and explicit four-vector input. +Here `point` and `next_point` are two flattened binary64 phase-space points, +each containing 16 components in `[particle][E,px,py,pz]` order. They are arrays +in C/Fortran and vectors in C++/Rust. The first evaluation constructs the +recursion; after reopening, the second reuses it at different momenta. + +In C, `check(status)` denotes the application's usual error handler: stop on +a nonzero status and retrieve the message with `rusticol_last_error_message`. + +```c +RusticolRuntimeHandle *runtime = NULL; +double value; +check(rusticol_runtime_load("artifacts/otf_gg_gg", NULL, NULL, &runtime)); +check(rusticol_runtime_evaluate_f64(runtime, point, 16, 1, &value, 1)); +check(rusticol_runtime_save(runtime, "gg.otf-cache")); +check(rusticol_runtime_free(runtime)); + +check(rusticol_runtime_load("artifacts/otf_gg_gg", NULL, NULL, &runtime)); +check(rusticol_runtime_load_cache(runtime, "gg.otf-cache")); +check(rusticol_runtime_evaluate_f64(runtime, next_point, 16, 1, &value, 1)); +check(rusticol_runtime_free(runtime)); +``` + +```cpp +{ + rusticol::Runtime runtime("artifacts/otf_gg_gg"); + runtime.evaluate(point, 1); + runtime.save("gg.otf-cache"); +} // The original handle is closed. +rusticol::Runtime restored("artifacts/otf_gg_gg"); +restored.load_cache("gg.otf-cache"); +auto values = restored.evaluate(next_point, 1); +``` + +```fortran +! Within a procedure using iso_c_binding and the rusticol module: +type(rusticol_runtime) :: runtime +real(c_double), allocatable :: values(:) +call runtime%load("artifacts/otf_gg_gg") +call runtime%evaluate(point, 1_c_size_t, values) +call runtime%save("gg.otf-cache") +call runtime%close() + +call runtime%load("artifacts/otf_gg_gg") +call runtime%load_cache("gg.otf-cache") +call runtime%evaluate(next_point, 1_c_size_t, values) +call runtime%close() +``` + +```rust +{ + let mut runtime = rusticol::Runtime::load("artifacts/otf_gg_gg", None, None)?; + runtime.evaluate_f64(&point, 1)?; + runtime.save("gg.otf-cache")?; +} // The original handle is dropped. +let mut restored = rusticol::Runtime::load("artifacts/otf_gg_gg", None, None)?; +restored.load_cache("gg.otf-cache")?; +let values = restored.evaluate_f64(&next_point, 1)?; +``` + +The reopening block can run in a later invocation of the application. Unlike +the one-flow Python example, these examples use the default helicity/colour +sum; selected evaluations and explicit +`warm_up(...)` calls can be saved in exactly the same way. Reuse the same +selectors after loading to benefit from the saved family. Fortran reports +errors by stopping unless an optional `ierr=status` argument is supplied. + +The Rust core's `NativeRuntime` exposes the same `save` and `load_cache` +methods as the dependency-free Rust SDK. C, C++, Fortran, Rust and Python +use the same snapshot format, so a compatible runtime can restore a cache +written through another API. Usual SDK status/exception handling applies. + +The snapshot covers the currently retained selection, including LC sums and +contracted NLC/full-colour direct or FFT families. It stores structural rows +and reduction mappings, not machine addresses or evaluated currents. Loading +rebinds the original prepared kernels and allocates numeric workspaces; +the receiving handle's model parameters remain unchanged. The same saved +selection can then run at new phase-space points and batch sizes without +reconstructing its currents. Selecting a different family still follows OTF's +normal cache-replacement behavior. Non-OTF handles reject these operations. +See [Runtime and Selectors](runtime-and-selectors.md#saving-an-otf-warm-cache) +for the Python example and snapshot scope. + +## Selector support + +The C, C++, Fortran, and Rust total-evaluation entry points accept optional +zero-based selector arrays with one entry per point. They map to the physical +helicity and LC-flow order reported by runtime metadata. + +- Batch-global string-ID subsets and per-point selectors are mutually + exclusive on the same axis. +- LC supports physical helicity and color-flow selection. +- NLC/full supports helicity selection; color is contracted. +- Rusticol groups equal per-point selectors for contiguous execution and + restores the original point order on output. +- Rectangular resolved evaluation remains batch-global. + +The selector contract is consistent across compiled, eager, recurrence, and +OTF artifacts; each lane applies its own internal planner. For OTF, LC exposes +selectable flow families. NLC and full colour expose one contracted component +and reject LC-flow selectors. Those contracted families are correctness +capabilities without a high-multiplicity runtime-practicality promise. + +See [Runtime and Selectors](runtime-and-selectors.md) for the same behavior in Python. + +## C11 + +The C header exposes C ABI v1 as an opaque runtime handle, typed status codes, +metadata getters, parameter updates, warning access, explicit OTF warm-up, +OTF cache save/restore, total evaluation, and resolved evaluation. Use +`rusticol-config` rather than hard-coding include or library paths: + +```console +eval "set -- $(rusticol-config --cflags) $(rusticol-config --libs)" +cc -std=c11 my_runtime.c "$@" -o my_runtime +``` + +The shell `eval` converts the command's shell-escaped output into an argument +vector and preserves SDK paths containing spaces. + +## C++17 + +`rusticol.hpp` is a header-only RAII wrapper over C ABI v1. It exposes metadata, +parameters, selectors, warnings, one-point OTF warm-up, cache save/restore, +and total/resolved f64 evaluation: + +```cpp +#include + +#include +#include + +int main() { + rusticol::Runtime runtime("artifacts/pp_zjj", "d d~ > g z g"); + runtime.set_model_parameter("aS", 0.117); + + std::vector momenta = /* point-major [point][particle][4] */; + auto total = runtime.evaluate(momenta, 1); + auto resolved = runtime.evaluate_resolved(momenta, 1); + + std::cout << total.at(0) << "\n"; + std::cout << resolved.total().at(0) << "\n"; +} +``` + +Compile with: + +```console +eval "set -- $(rusticol-config --cflags) $(rusticol-config --libs)" +c++ -std=c++17 my_runtime.cpp "$@" -o my_runtime +``` + +## Fortran 2008 + +The wheel ships portable module source rather than a compiler-specific `.mod` +file: + +```console +RUSTICOL_FORTRAN="$(rusticol-config --fortran-source)" +eval "set -- $(rusticol-config --libs)" +gfortran -std=f2008 "$RUSTICOL_FORTRAN" my_runtime.f90 "$@" -o my_runtime +``` + +`type(rusticol_runtime)` owns load/close, metadata, parameter, warning, +one-point OTF warm-up, cache save/restore, and total/resolved f64 methods. +Resolved Fortran storage is `(color, helicity, point)`, the column-major view +of the C ABI sequence `(point, helicity, color)`. + +## Rust 2021 + +`rusticol.rs` is a dependency-free safe wrapper over C ABI v1. It owns the +handle, frees it on drop, exposes typed metadata, selectors, explicit +one-point OTF warm-up and cache save/restore, and remains bound to its creating +thread. + +The generated driver is compiled directly with `rustc`: + +```console +make -C artifacts/pp_zjj/API/rust check_standalone +make -C artifacts/pp_zjj/API/rust run \ + ARGS='--process "d d~ > g z g" --precision 16 --json' +``` + +For a hand-written source: + +```console +RUSTICOL_RUST_SOURCE="$(rusticol-config --rust-source)" +eval "set -- $(rusticol-config --rustflags)" +RUSTICOL_RUST_SOURCE="$RUSTICOL_RUST_SOURCE" \ + rustc --edition=2021 my_runtime.rs -o my_runtime "$@" +``` + +`rust-script` is optional and is not a pyAmpliCol runtime requirement. The +generated source includes its minimal Cargo header, so an installed +`rust-script` can run it directly: + +```console +RUSTICOL_RUST_SOURCE="$(rusticol-config --rust-source)" \ + CARGO_ENCODED_RUSTFLAGS="$(rusticol-config --cargo-rustflags)" \ + rust-script artifacts/pp_zjj/API/rust/check_standalone.rs -- \ + --process 'd d~ > g z g' --precision 16 --json +``` + +The generated Makefile exposes the same path as: + +```console +make -C artifacts/pp_zjj/API/rust run-script \ + ARGS='--process "d d~ > g z g" --precision 16 --json' +``` + +## Runtime and precision boundary + +All native APIs use the Symbolica-independent f64 runtime: + +- direct JIT artifacts load the separate MIT-licensed SymJIT application; +- eager and recurrence artifacts execute their prepared native schedules; +- OTF artifacts construct a selected recurrence family from their compact seed + and then execute the artifact-local prepared kernels; +- C++/ASM evaluator artifacts load only on a compatible target and CPU; +- no native call imports Symbolica or performs a Symbolica license check. + +Python is the only standalone driver that can request Symbolica-backed exact +precision when the artifact retains an exact evaluator. OTF does not retain +that path and rejects non-f64 precision. See +[Symbolica and Licensing](symbolica-and-licensing.md). +The separate Python correlated API is also Symbolica-backed at precision 16; +the native correlated entry points instead use the independent RustiCol +binary64 executor described above. + +## Common setup failures + +| Symptom | Resolution | +| --- | --- | +| `ModuleNotFoundError: pyamplicol` | Run the Python driver with the environment's Python or activate that environment. | +| `rusticol-config: command not found` | Activate the same environment, set `RUSTICOL_CONFIG`, or use the supported checkout-local `.venv` workflow. | +| `rusticol.h` / `rusticol.hpp` not found | Do not invoke the compiler without the flags emitted by `rusticol-config`. | +| Empty `RUSTICOL_RUST_SOURCE` or `RUSTICOL_FORTRAN` | Verify `rusticol-config --rust-source` / `--fortran-source` in the active environment. | +| Target incompatibility | Use a portable JIT artifact (compiled O1/O2, or eager/recurrence/OTF from a prepared JIT O2 pack), or regenerate C++/ASM/O0/O3 on the destination target. | + +See [Troubleshooting](troubleshooting.md) for a fuller decision tree. + +## Related pages + +- [Examples Gallery](examples-gallery.md) — complete copied examples and generated API commands. +- [Artifacts and Portability](artifacts-and-portability.md) — target rules and trusted-input boundary. +- [Runtime and Selectors](runtime-and-selectors.md) — process ordering and selector semantics. +- [Born Correlations](../correlators.md) — opt-in LC/NLC/full-colour correlations in Python and native SDKs. diff --git a/pyamplicol/docs/user/process-grouping.md b/pyamplicol/docs/user/process-grouping.md new file mode 100644 index 0000000000..89917daedd --- /dev/null +++ b/pyamplicol/docs/user/process-grouping.md @@ -0,0 +1,803 @@ +--- +title: "Process Grouping" +nav_order: 3.5 +parent: "Configuration" +--- + +# Process grouping + +A multiprocess request such as `p p > j j j j` expands into many subprocesses, +but many of them have the same matrix element once their legs are relabelled. +In the built-in Standard Model with five massless flavours, the 486 +subprocesses of `p p > j j j j` fall into 23 classes. Within a class, all +subprocesses are related by a permutation of quark flavours, and possibly by +charge conjugation. +pyAmpliCol records such relations in the artifact, can skip generating the +redundant subprocesses, can find relations numerically, and exports the +skipped subprocesses to UMAMI through their representative. + +| Mechanism | What it does | Status of the relation | Generated subprocesses | +| --- | --- | --- | --- | +| Every generation | Writes `process-groups.json`; each subprocess is its own group | Identity | All | +| `--flavor-blind` | Keeps one representative per flavour class, records the others as its members | **Assumed**, with a warning when the model may break it | Representatives only | +| `--analyze-process-groups numerical`, `pyamplicol group-processes` | Merges generated subprocesses whose resolved components agree up to a rational factor | Tested numerically at three points | All, unchanged | +| UFO conserved-charge filter | Drops expansion candidates that cannot have a tree amplitude | Exact | Not applicable | + +The sections below define the relation, explain how candidate subprocesses +and leg mappings are found, and describe each mechanism, the file format and +the UMAMI export. A [worked example](#worked-example-p-p--j-j-j-j) with +`p p > j j j j` closes the page. + +## The relation between two subprocesses + +A group has one **representative**, which is always a generated subprocess, +and any number of **members**. Each member carries a relation to the +representative: + +```text +ME_member(p[0], ..., p[n-1]) = factor * ME_representative(q[0], ..., q[n-1]) +q[r] = p[permutation[r]] +``` + +- Legs are counted from 0 in the public order of the process expression, + incoming legs first. Momenta are four-vectors in that order. +- `permutation[r]` is the member leg that plays the role of representative leg + `r`. This is the convention of `ProcessAlias.particle_permutation` and of + the UMAMI momentum permutation. +- A permutation never moves a leg across `>`. It may exchange the two incoming + legs, as between `d g > d g` and `g u > u g`. +- `factor` is a signed rational number whose numerator and denominator are at + most 99 in absolute value. `--flavor-blind` records factor 1; a + flavour-blind member gets another factor only when its group is later merged + numerically. A member that was not generated is evaluated through its + representative and must have a positive factor; a map that would give one a + non-positive factor is rejected when it is written. +- `conjugated` marks a relation that includes charge conjugation (see + [Charge conjugation](#charge-conjugation)). +- Helicities follow the legs: the member's helicity on leg `permutation[r]` + equals the representative's helicity on leg `r`. This also holds for + conjugated relations. + +For example, the relation between `u u~ > d u d~ u~` and its representative +`d d~ > d u d~ u~` has permutation `[0, 1, 3, 2, 5, 4]`: + +```text +ME(u u~ > d u d~ u~)(p0, p1, p2, p3, p4, p5) + = ME(d d~ > d u d~ u~)(p0, p1, p3, p2, p5, p4) +``` + +Representative leg 2 (the outgoing `d`) takes the momentum of member leg 3 +(the outgoing `u`), so the relabelling is `u <-> d`, applied to every leg. + +## Particle types + +A relabelling may only exchange particles that the dynamics cannot tell +apart. pyAmpliCol summarises everything a relabelling must preserve in a +**particle type**: + +| Component | Meaning | +| --- | --- | +| spin | `2S+1`: 1 for scalars, 2 for fermions, 3 for vectors | +| colour | dimension of the SU(3) representation: 1, 3 or 8 (triplet and antitriplet are not distinguished) | +| mass | the name of the mass parameter, or `ZERO` | +| width | the name of the width parameter, or `ZERO` | +| self-conjugate | whether the particle is its own antiparticle | + +A particle and its antiparticle share one type. Masses and widths are compared +by **parameter name**, after compile-time resolution. A parameter that the +model or its restriction fixes to zero at compile time counts as `ZERO`. Two +different parameters with equal numerical values are never identified. + +Examples: + +- Built-in SM: `d`, `u`, `s`, `c` and `b` share one type (massless fermion + triplet), and the top quark has its own (`mass_6`, `width_6`). +- Shipped `sm.json`, default restriction: `d`, `u`, `s` and `c` share one + type. The `b` quark (`MB`) and the top (`MT`, `WT`) each have their own. +- In `sm.json`, the massless leptons `e-` and `mu-` and the three neutrinos all + have the same type (massless colour-singlet fermion), while `ta-` (`MTA`) + has its own. `W+` and `W-` share a type. The photon and the `Z` do not, + because the `Z` mass and width differ from `ZERO`. + +The sorted list of particle types of each side is the **coarse key** of a +subprocess. Only subprocesses with equal coarse keys are ever compared. + +## Flavour profiles + +Equal types are necessary but not sufficient. The relabelling must also be +**consistent**: a flavour must be renamed the same way on every leg where it +appears. pyAmpliCol describes each flavour `f` of a subprocess by its +**profile**, the four counts + +```text +profile(f) = (initial particles, initial antiparticles, + final particles, final antiparticles) +``` + +Self-conjugate particles are counted as particles. Two subprocesses are +related by a type-preserving flavour relabelling exactly when they have the +same number of incoming legs and the same multiset of `(type, profile)` pairs. + +### Why sorting by PDG code fails + +**A related pair that a positional pairing breaks.** Compare +`u u~ > d u d~ u~` with `d d~ > d u d~ u~`: + +| Subprocess | profile of `u` | profile of `d` | +| --- | --- | --- | +| `u u~ > d u d~ u~` | (1, 1, 1, 1) | (0, 0, 1, 1) | +| `d d~ > d u d~ u~` | (0, 0, 1, 1) | (1, 1, 1, 1) | + +The `(type, profile)` multisets agree, so the relabelling `u <-> d` relates +the two. Ordering each side by type, then profile, then flavour, and pairing +the ordered legs gives the permutation `[0, 1, 3, 2, 5, 4]` shown above. +Sorting each side by signed PDG code instead pairs the incoming `u` with the +incoming `d`, but the outgoing `u` with the outgoing `u`. No relabelling does +that, and evaluating the representative at those momenta computes a different +function. + +**An unrelated pair that a type-only comparison merges.** `d d~ > u u~` and +`d d~ > d d~` have the same particle types on each side. The first has only +the s-channel gluon exchange. The second also has the t-channel exchange and +the interference between the two. Their profiles differ: in `d d~ > u u~`, +`d` has `(1, 1, 0, 0)` and `u` has `(0, 0, 1, 1)`, while in `d d~ > d d~`, `d` +has `(1, 1, 1, 1)`. `--flavor-blind` therefore never puts them in one class. + +When several flavours of a subprocess share a type and a profile (`s` and `c` +in `d d~ > s c s~ c~`), each pairing of them is a valid leg mapping. The first +mapping pairs them by increasing `|PDG|`. The numerical analysis tries at most +24 such mappings, and at most 24 conjugated ones. + +## Charge conjugation + +Charge conjugation replaces every particle by its antiparticle, which +exchanges the particle and antiparticle entries of each profile. With +`--consider-charge-conjugation` (the default), two subprocesses are also +related when one matches the conjugate of the other. Examples are +`u~ g > u~ g g g` and `d g > d g g g`, or `d u~ > d u u~ u~` and +`d u~ > d d d~ u~`. The relation is stored with `"conjugated": true`, and it +means: + +- helicity values move with the legs **unchanged**: no helicity is flipped; +- leading-colour (LC) flow words are relabelled and **reversed**, because + every quark line runs in the opposite direction. + +Charge conjugation with unchanged helicities is a symmetry of vector-like +interactions such as QCD and QED. It is not a symmetry of chiral couplings +such as those of the `W` and `Z`. Relations that also need a helicity flip +(CP-type relations) are not pursued. For example, `u d~ > W+ a` and +`d u~ > W- a` have equal helicity-summed matrix elements by CP, but their +helicity components differ, so the numerical analysis never groups them. +`--flavor-blind` does put them in one class, with a warning that a chiral +electroweak vertex breaks charge conjugation. + +`--no-consider-charge-conjugation` (`process.consider_charge_conjugation = +false`) disables conjugated relations. The setting only matters together with +`--flavor-blind` or the numerical analysis. + +## Flavour-blind generation + +`--flavor-blind` (`process.flavor_blind = true`) states an **assumption**: +every subprocess in a flavour class has the same matrix element as the class +representative. A flavour class contains the subprocesses related by a +type-preserving flavour relabelling and, unless +`--no-consider-charge-conjugation`, by charge conjugation. + +Generation then proceeds as follows: + +1. The request is expanded and checked exactly as without the flag. + Subprocesses are named at this stage, so a generated subprocess has the + same process ID with or without `--flavor-blind`. +2. The expanded subprocesses are partitioned into flavour classes. The + representative is the canonical member: it has the smallest `|PDG|` + codes, and particles come before antiparticles. For example, `d d~ > g g` + represents `u u~ > g g`, and `d g > d g` represents `u~ g > u~ g`. +3. Only representatives are generated. Each other member is recorded in + `process-groups.json` with factor 1, basis `flavor-blind`, + `"generated": false`, and the leg permutation of the first flavour mapping. + A non-conjugated mapping is preferred; a conjugated one is used only when + no non-conjugated mapping exists. + +A subprocess with an explicit `ProcessAlias` keeps its own artifact entry and +is not folded into a class. + +Generation logs the outcome of step 3 in one line, such as +`Flavor-blind grouping: 486 subprocesses -> 23 generated representatives; 463 +routed members (140 by charge conjugation); 0 relabelling warnings`, and the +summary table printed at the end has a `process grouping` row; see the +[worked example](#worked-example-p-p--j-j-j-j). + +Members stay selectable. Every SDK (`Runtime.load(..., process=...)`, +`pyamplicol evaluate --process`, `rusticol_runtime_load` and the C++, Fortran +and Rust wrappers) resolves a member ID or expression, in any side order, +through `process-groups.json` to its representative. The representative is +evaluated at the permuted momenta and every output is multiplied by the +recorded factor. Helicity and colour-flow IDs are reported in the member's leg +order; for a conjugated member each LC flow word is reversed, and colour-flow +selectors given in member IDs are translated back. See +[Process-group members](process-selection-and-permutations.md#process-group-members). + +```python +from pyamplicol import Runtime + +runtime = Runtime.load("artifacts/pp_4j_blind", process="d u~ > d u u~ u~") +runtime.representative_process_key # the generated representative +runtime.process_group_factor # Fraction(1, 1) +runtime.charge_conjugated # True for this member +values = runtime.evaluate(points) # points in the member's leg order +``` + +The UMAMI export applies this mapping automatically (see +[UMAMI export](#umami-export-of-flavour-blind-members)). + +### Warnings + +The assumption is not verified. Generation checks whether each relation is a +**symmetry of the model vertices that the representative is generated with**. +A vertex is allowed when its coupling orders fit `max_coupling_orders` and, +under the default `minimal` coupling-order policy, the representative's +minimal coupling-order envelope. The check works as follows: + +- The relabelling is completed to a permutation of the model's particles. + Flavours that the relabelling moves but that occur in only one of the two + subprocesses are paired by particle type. Every + allowed vertex that the permutation moves must have an allowed image with + the same particles in the same order and the same structure and coupling. + For compiled (UFO or JSON) models, structure and coupling mean the Lorentz + and colour expressions and the resolved coupling. For the built-in SM, they + mean the vertex kind and its coupling. +- A conjugated relation also requires that the conjugate of every allowed + vertex is allowed, and that no allowed vertex couples fermions chirally. A + Lorentz structure is chiral when exchanging its left- and right-handed + projectors changes it, or when it contains `gamma5`. A built-in electroweak + current is chiral when its left- and right-handed couplings differ. + +A member that fails the check gets a warning, stored as its `warning` field in +`process-groups.json`. The warnings are also logged once on the +`pyamplicol.generation` logger, with their count and up to three examples. +For `pyamplicol generate "p p > z j" OUT --model built-in-sm --flavor-blind`: + +```text +'u u~ > g z' assumed equal to 'd d~ > g z', but relabelling d->u, u->d changes +the allowed vertex [d a d] (coupling orders: QCD<=1, QED<=1) + +'u~ g > u~ z' assumed equal to 'd g > d z', but relabelling d->u, u->d changes +the allowed vertex [d a d] and charge conjugation is broken by the chiral +allowed vertex [d z d] (coupling orders: QCD<=1, QED<=1) +``` + +The check inspects vertices, not diagrams, and completes the relabelling in +one fixed way, so it can warn about a relation that holds. In the same run, +`s s~ > g z` is equal to `d d~ > g z` at tree level, but it still warns: +`d <-> s` alone moves the allowed `W` vertex `[d w+ u]`, although also +exchanging `u <-> c` would restore it. + +### When to use it + +Equal matrix elements within a class are **your responsibility**. The flag is +safe when the interactions that can appear are blind to the relabelled +flavours. The typical case is pure-QCD jets with massless quarks under the +default `minimal` coupling-order policy: `p p > j j`, `p p > j j j` and +`p p > j j j j` produce no warnings. + +It is not safe when photons, `Z` or `W` bosons can couple to the relabelled +flavours: + +- `p p > z j`, `p p > a j`, `p p > e+ e- j`: up- and down-type quarks have + different electric charges and `Z` couplings, and the chiral `Z` and `W` + couplings break charge conjugation; +- jets with electroweak orders allowed explicitly, for example + `--coupling-order-policy explicit --max-coupling-order QED=2`; +- requests that mix charged leptons and neutrinos: in massless-lepton models + they share a type, so `p p > e+ e-` and `p p > ve ve~` would fall into one + class. + +To check an assumption, generate the subprocesses in question without +`--flavor-blind` and run the +[numerical analysis](#numerical-analysis): it merges exactly the relations +that hold. For example, with `u u~ > a a` and `d d~ > a a` it finds +`ME(u u~ > a a) = 16 ME(d d~ > a a)`, the ratio `(Q_u / Q_d)^4` of the +quark charges. `--flavor-blind` would have assumed factor 1. + +## Conserved charges in UFO expansion + +When generating with a UFO or other compiled model, a multiparticle request is +expanded in four steps: + +1. Each side of `>` is filled as a **multiset** of particles, so two + candidates never differ only by an ordering within one side. +2. Candidates that violate an **additive charge conserved by every vertex** + are dropped. +3. The colour-readiness probe runs on the remaining candidates. +4. Tree amplitudes are constructed. With `--flavor-blind`, the flavour classes + are formed just before this step. + +A conserved charge assigns a rational value to every particle, the opposite +value to its antiparticle and zero to self-conjugate particles. The values on +every vertex term sum to zero. The conserved charges are a basis of the +rational nullspace of the vertex/particle incidence matrix, computed from the +model's vertex list alone. No quantum numbers are hard-coded. + +Every vertex conserves these charges, so every tree graph does too. A +candidate whose incoming and outgoing totals differ can have no tree +amplitude, whatever the coupling orders. The filter therefore removes nothing +that amplitude construction would have kept; it only drops early the +candidates that would have failed later. A request with no candidate left is +rejected with an error. For the shipped `sm.json` +with its default restriction, the basis has nine charges, spanning electric +charge, one quark number per generation (the CKM matrix is diagonal), one +lepton number per generation and two ghost numbers. With a non-diagonal CKM +matrix, the three quark numbers reduce to baryon number. + +The built-in SM keeps its own catalogue prefilters (`flavor_scheme`, +`max_quark_lines`). `pyamplicol model processes` applies the same charge +filter, but not the colour and amplitude checks. See +[Models and Processes](models-and-processes.md#multiparticles) +for the expansion syntax. + +## Numerical analysis + +`--analyze-process-groups numerical` (`process.analyze_process_groups = +"numerical"`) runs after the artifact is written. `pyamplicol group-processes` +runs the same analysis on an existing artifact. The analysis looks for +relations among the **generated group representatives** and merges the +groups it relates. Flavour-blind members are not analysed: they have no +runtime process of their own, and loading one only evaluates its +representative. + +### Procedure + +1. Representatives are visited in canonical order. Each one is compared with + the earlier representatives that kept their own group and have the same + coarse key and the same colour accuracy. +2. The candidate leg mappings are, in order: the flavour-profile mappings, + then (with charge conjugation) the conjugated profile mappings, then a + fallback that pairs the legs of each side by type and signed PDG code. +3. Three **phase-space points** are used, in f64. At each point, the model + inputs are varied independently and deterministically. Each mutable + parameter of kind `external` or `normalization`, other than a particle + mass or width, is multiplied by a factor in `[0.9, 1.1]`. The factor + depends only on the parameter name, the point and the seed, so both + processes of a comparison always see the same values. Couplings and other + derived parameters follow from the varied inputs. Masses and widths are + never varied. A derived mass, such as `MW` computed from `Gf`, `aEWM1` and + `MZ` in `sm.json`, is read back from the runtime after the variation. +4. Each point is **on shell** for the resolved masses. It is generated in the + centre-of-mass frame with the beams along the z axis (massive beams are + supported), at `sqrt(s) = max(1000, 2 * threshold)` in the model's energy + unit. The threshold is the larger of the sum of the beam masses and the sum + of the final-state masses. The final state is a massive RAMBO point. A + `2 > 1` process uses `sqrt(s)` equal to the final mass, and a decay is + evaluated at rest. +5. The representative is evaluated at its points, and the candidate member at + the permuted points `p[permutation[r]] = q[r]`. All surviving mappings are + evaluated in one batch per point. +6. Every resolved component is compared through the mapping. At LC, a + component is a helicity configuration times a colour flow. Helicity values + are permuted. Flow words are relabelled, and reversed for a conjugated + mapping; they are compared as sets of open colour lines, or as one adjoint + trace up to cyclic rotation. At NLC and full colour, a component is a + helicity configuration times a contracted colour component. + +A mapping is accepted when one common ratio `member / representative` +describes every component at all three points: + +| Quantity | Rule | +| --- | --- | +| zero components | below `1e-12` times the largest component at that point; a zero on one side must be a zero on the other | +| agreement of a non-zero component | relative `1e-9`, never tighter than `1e-9 * 1e-4` times the largest component | +| common ratio across points | relative `1e-9` | +| a point that vanishes entirely on either side | no relation | +| imaginary part of the ratio | at most `1e-9` of its modulus | +| recorded factor | rational within `1e-9` relative, numerator and denominator at most 99 in absolute value | +| `--only-unit-multiplier` | only factor 1 is accepted | +| seed | `20260917`, recorded in the map | + +The search stops at the first earlier group and mapping that pass. The +subprocess's group is then merged into that group with basis `numerical`. + +A representative is **skipped**, and stays its own group, in any of these +cases: + +- it was generated with selected helicities (incomplete helicity coverage); +- it was generated with selected colour flows or sectors; +- its masses leave no on-shell phase space; +- its runtime raises an error. + +Each skip is logged as a warning and listed in the summary. + +## The `group-processes` command + +```console +pyamplicol group-processes ARTIFACT \ + [--analyze-process-groups {none,numerical}] \ + [--only-unit-multiplier] \ + [--consider-charge-conjugation | --no-consider-charge-conjugation] +``` + +| Option | Default | Meaning | +| --- | --- | --- | +| `--analyze-process-groups` | `numerical` | `none` only writes the trivial map when the artifact has no `process-groups.json` | +| `--only-unit-multiplier` | off | accept only new relations with factor 1 | +| `--[no-]consider-charge-conjugation` | on | also try conjugated mappings | + +The `numerical` default belongs to the subcommand, and it takes precedence +over a `--card` value. Pass `--analyze-process-groups none` or +`--set process.analyze_process_groups=none` to change it. The schema default +of `process.analyze_process_groups`, used by generation, remains `none`. + +The command reads the stored map, analyses its representatives, merges the +new relations, and rewrites `process-groups.json` in place. The rewrite runs +under the lock that also serializes generation and appends. The file's +payload record in +`artifact.json` is updated, but the artifact ID is unchanged, because the +`process-groups` role is not part of the runtime identity. UMAMI files are +never touched. The command prints a summary; add `--json` for +machine-readable output. For the five subprocesses of the +[file example](#the-process-groupsjson-file), generated without analysis: + +```console +pyamplicol group-processes artifacts/grouping_demo --json +``` + +```json +{ + "analysis": "numerical", + "artifact": "/home/user/artifacts/grouping_demo", + "groups_after": 2, + "groups_before": 5, + "new_relations": [ + {"conjugated": false, "factor": "16", "permutation": [0, 1, 2, 3], + "process": "u u~ > a a", "process_id": "u_ubar_to_a_a", + "representative": "d_dbar_to_a_a"}, + {"conjugated": true, "factor": "1", "permutation": [0, 1, 2, 3], + "process": "d~ g > d~ g", "process_id": "dbar_g_to_dbar_g", + "representative": "d_g_to_d_g"}, + {"conjugated": false, "factor": "1", "permutation": [1, 0, 2, 3], + "process": "g u > u g", "process_id": "g_u_to_u_g", + "representative": "d_g_to_d_g"} + ], + "processes_analysed": 5, + "skipped": [] +} +``` + +`representative` is the group the subprocess joined. A second run reports +`groups_before: 2`, `processes_analysed: 2` and no new relation. + +When two groups merge, the canonically smaller representative stays. Every +member of the absorbed group, including its flavour-blind members, is +re-expressed through the new link: + +- factors multiply; +- permutations compose; +- the `conjugated` flags combine (two conjugations cancel); +- the basis becomes the least verified of the two links, in the order + `identity`, `numerical`, `flavor-blind`; +- a warning is kept. + +The analysis settings are appended to `analyses` unless an identical entry +is already there. Rerunning with the same settings is deterministic and adds +nothing. A later run with relaxed settings, for example without +`--only-unit-multiplier`, can add relations. + +The Python equivalent is `pyamplicol.group_processes` (also available as +`pyamplicol.api.group_processes`). It takes the same options as keyword +arguments: `analysis="numerical"`, `only_unit_multiplier=False` and +`consider_charge_conjugation=True`. See +[Python API](python-api.md#group-equivalent-subprocesses). + +## The `process-groups.json` file + +Every generation lane (compiled, eager, recurrence, on-the-fly) writes +`process-groups.json` at the artifact root, declared in `artifact.json` with +payload role `process-groups` and media type `application/json`. Without +`--flavor-blind` and without numerical analysis, each generated subprocess is +its own group with factor 1. An append keeps the stored groups and adds the +new subprocesses. A newly generated subprocess whose process ID matches an +ungenerated flavour-blind member replaces that member and becomes its own +group. + +The example below comes from + +```console +pyamplicol generate "d d~ > a a" artifacts/grouping_demo --model built-in-sm \ + --process "u u~ > a a" --process "d g > d g" --process "d~ g > d~ g" \ + --process "g u > u g" --analyze-process-groups numerical +``` + +(lists are folded onto one line for readability): + +```json +{ + "kind": "pyamplicol-process-groups", + "schema_version": 1, + "analyses": [ + {"variant": "numerical", "consider_charge_conjugation": true, + "only_unit_multiplier": false, "points": 3, "parameter_variation": 0.1, + "tolerance": 1e-09, "seed": 20260917} + ], + "groups": [ + { + "representative": {"process_id": "d_dbar_to_a_a", "process": "d d~ > a a"}, + "members": [ + {"process_id": "d_dbar_to_a_a", "process": "d d~ > a a", + "pdgs": [1, -1, 22, 22], "initial_count": 2, + "factor": {"numerator": 1, "denominator": 1}, + "permutation": [0, 1, 2, 3], "conjugated": false, + "basis": "identity", "generated": true}, + {"process_id": "u_ubar_to_a_a", "process": "u u~ > a a", + "pdgs": [2, -2, 22, 22], "initial_count": 2, + "factor": {"numerator": 16, "denominator": 1}, + "permutation": [0, 1, 2, 3], "conjugated": false, + "basis": "numerical", "generated": true} + ] + }, + { + "representative": {"process_id": "d_g_to_d_g", "process": "d g > d g"}, + "members": [ + {"process_id": "d_g_to_d_g", "process": "d g > d g", + "pdgs": [1, 21, 1, 21], "initial_count": 2, + "factor": {"numerator": 1, "denominator": 1}, + "permutation": [0, 1, 2, 3], "conjugated": false, + "basis": "identity", "generated": true}, + {"process_id": "dbar_g_to_dbar_g", "process": "d~ g > d~ g", + "pdgs": [-1, 21, -1, 21], "initial_count": 2, + "factor": {"numerator": 1, "denominator": 1}, + "permutation": [0, 1, 2, 3], "conjugated": true, + "basis": "numerical", "generated": true}, + {"process_id": "g_u_to_u_g", "process": "g u > u g", + "pdgs": [21, 2, 2, 21], "initial_count": 2, + "factor": {"numerator": 1, "denominator": 1}, + "permutation": [1, 0, 2, 3], "conjugated": false, + "basis": "numerical", "generated": true} + ] + } + ] +} +``` + +Reading it: + +- `u u~ > a a` is 16 times `d d~ > a a` at equal momenta. +- `d~ g > d~ g` is the charge conjugate of `d g > d g`, leg by leg. +- `g u > u g` at `(p0, p1, p2, p3)` equals `d g > d g` at + `(p1, p0, p2, p3)`: representative leg 0, the incoming quark, takes the + momentum of member leg 1. +- All five subprocesses remain generated: numerical grouping records + relations but removes nothing. + +| Field | Meaning | +| --- | --- | +| `kind`, `schema_version` | `pyamplicol-process-groups`, version 1 | +| `analyses` | one entry per distinct numerical-analysis setting applied | +| `groups` | sorted by representative process ID | +| `representative` | process ID and expression of the group's generated representative | +| `members` | the representative first (identity relation), then every member | +| `pdgs`, `initial_count` | signed PDG codes in public leg order, number of incoming legs | +| `factor` | `ME_member = factor * ME_representative`, as a reduced fraction | +| `permutation` | `q[r] = p[permutation[r]]`, representative leg to member leg | +| `conjugated` | the relation includes charge conjugation | +| `basis` | `identity`, `flavor-blind` (assumed) or `numerical` (tested) | +| `generated` | whether the member has its own runtime process in the artifact | +| `warning` | present only for a flavour-blind relation that may not be a model symmetry | + +Every subprocess of the artifact appears exactly once as a generated member, +and a representative is always generated. + +`pyamplicol inspect` shows the map in its `Process groups` section (colours +omitted here). For the five subprocesses above: + +```text +Process groups + ++--------+--------------+-----------+--------+------------+-------------+-------------+-----------+----------+ +| groups | subprocesses | generated | routed | conjugated | factor != 1 | bases | analyses | warnings | ++--------+--------------+-----------+--------+------------+-------------+-------------+-----------+----------+ +| 2 | 5 | 5 | 0 | 1 | 1 | numerical 3 | numerical | 0 | ++--------+--------------+-----------+--------+------------+-------------+-------------+-----------+----------+ + ++---------------------+-------------+----------------+--------+---+-------------+-----------+ +| process ID | process | role | factor | C | permutation | basis | ++---------------------+-------------+----------------+--------+---+-------------+-----------+ +| d_dbar_to_a_a | d d~ > a a | representative | | | | | +| `- u_ubar_to_a_a | u u~ > a a | generated | 16 | | | numerical | ++---------------------+-------------+----------------+--------+---+-------------+-----------+ +| d_g_to_d_g | d g > d g | representative | | | | | +| |- g_u_to_u_g | g u > u g | generated | 1 | | [1 0 2 3] | numerical | +| `- dbar_g_to_dbar_g | d~ g > d~ g | generated | 1 | C | | numerical | ++---------------------+-------------+----------------+--------+---+-------------+-----------+ + +C: charge-conjugated relation; factor F: ME = F x ME(representative) +permutation p: representative leg r takes the momentum of member leg p[r] +numerical analysis: 3 points, tolerance 1e-09 +``` + +A `generated` member has its own runtime process, and a `routed` member is +evaluated through its representative. The permutation column is empty for the +identity. The factor, `C` and permutation columns appear only when some member +needs them, and a `note` column with `[n]` references to the warnings printed +below the table appears when a member has a warning. Maps with more than 80 +subprocesses list each group's members as a grid of process strings instead; +see [Inspect and select a process](command-line-interface.md#inspect-and-select-a-process). +An artifact without grouping prints one line, `Process groups: none; each of +the N processes is its own group`. + +The summary printed by this `generate` command reports the same map in its +`process grouping` row, `5 subprocesses -> 2 groups: 5 generated, 0 routed`, +followed by `3 numerical relations (1 charge-conjugated, 1 with factor != 1); +0 warnings` and the analysis settings. Its `Generated Processes` table lists +`u u~ > a a` as `member of d_dbar_to_a_a (x16)` and `d~ g > d~ g` as +`member of d_g_to_d_g (C)`. + +## UMAMI export of flavour-blind members + +The UMAMI provider exports every flavour-blind member, although it has no +runtime process: + +- A member adds flavour selectors that evaluate its **representative's + runtime**, with no `runtime_processes` row of its own. Its + `members[].runtime` names the representative. +- The momentum permutation is the composition of the recorded relation with + the runtime ordering. It may exchange the incoming legs, but the selector + keeps the member's physical incoming PDG codes for the PDF weights, so this + is not a beam-exchange assumption. +- A conjugated member uses the representative's colour flows reversed. Its + LHE colour tags and helicity labels are those of the member. +- The member's factor (1 for flavour-blind relations) multiplies the runtime + factor. +- Every provider exporting such members records an additional + `grouping.assumptions` entry: + + ```text + flavor-blind matrix elements: ungenerated process-group members are + evaluated through their relabelled representative (assumed, not proven) + ``` + +A member whose relabelling moves unequal masses between legs, or maps a +restricted helicity selection onto a different set, is exported by a separate +provider that loads the representative's runtime. Warnings stay in +`process-groups.json`; they are not copied into the UMAMI metadata. +Numerically grouped subprocesses are generated and keep their ordinary +export; `group-processes` does not change the UMAMI files. See +[UMAMI and MadSpace](umami-api.md#flavour-blind-members) for the selector +layout. + +## Worked example: `p p > j j j j` + +Preview the flavour classes with a dry run, which prints the subprocesses +that would be generated: + +```console +pyamplicol generate "p p > j j j j" artifacts/pp_4j_blind \ + --model built-in-sm --flavor-blind --dry-run +``` + +With the built-in SM at the default `flavor_scheme = 5`, the request has: + +| Setting | Generated subprocesses | +| --- | --- | +| no `--flavor-blind` | 486 | +| `--flavor-blind --no-consider-charge-conjugation` | 33 | +| `--flavor-blind` | 23 | + +Charge conjugation merges, for example, the `d~ d~` and `d~ g` initial +states into the `d d` and `d g` classes, and `d u~ > d u u~ u~` into the +class of `d u~ > d d d~ u~`. Generate the artifact and analyse the +representatives in one step: + +```console +pyamplicol generate "p p > j j j j" artifacts/pp_4j_blind \ + --model built-in-sm --flavor-blind --analyze-process-groups numerical +``` + +Before generating, the command logs the flavour classes it found: + +```text +INFO pyamplicol.generation: Flavor-blind grouping: 486 subprocesses -> 23 generated representatives; 463 routed members (140 by charge conjugation); 0 relabelling warnings +``` + +and its final summary reads the written map back (abridged, colours omitted): + +```text +| processes | 23 requests | +| process grouping | 486 subprocesses -> 23 groups: 23 generated, 463 routed | +| | 463 flavor-blind relations (140 charge-conjugated); 0 warnings | +| | numerical analysis: 3 points, tolerance 1e-09 | +... ++--------------------+------------------+-----------------------------+ +| name | process | group | ++--------------------+------------------+-----------------------------+ +| p_p_to_j_j_j_j_1 | d d > d d d d~ | 10 subprocesses (9 routed) | +| p_p_to_j_j_j_j_2 | d d > d d u u~ | 40 subprocesses (39 routed) | +... +| p_p_to_j_j_j_j_486 | g g > g g g g | | ++--------------------+------------------+-----------------------------+ +``` + +The minimal coupling-order envelope is pure QCD, so no relation warns. The +artifact contains 23 runtime processes, and `process-groups.json` has 23 +groups covering all 486 subprocesses: 463 flavour-blind members, 140 of them +conjugated. The numerical analysis finds no further relation among the 23 +representatives. Some of the classes: + +| Representative | Process ID | Class size | Conjugated members | Incoming legs exchanged | +| --- | --- | --- | --- | --- | +| `d d~ > d u d~ u~` | `p_p_to_j_j_j_j_32` | 20 | 0 | 0 | +| `d u > d u s s~` | `p_p_to_j_j_j_j_9` | 60 | 30 | 0 | +| `d u~ > d d d~ u~` | `p_p_to_j_j_j_j_52` | 40 | 20 | 20 | +| `d g > d g g g` | `p_p_to_j_j_j_j_81` | 10 | 5 | 0 | +| `d d~ > g g g g` | `p_p_to_j_j_j_j_51` | 5 | 0 | 0 | +| `g g > g g g g` | `p_p_to_j_j_j_j_486` | 1 | 0 | 0 | + +Three members illustrate the relations: + +| Member | Representative | `permutation` | `conjugated` | +| --- | --- | --- | --- | +| `u u~ > d u d~ u~` | `d d~ > d u d~ u~` | `[0, 1, 3, 2, 5, 4]` | false | +| `d u~ > d u u~ u~` | `d u~ > d d d~ u~` | `[1, 0, 4, 5, 3, 2]` | true | +| `u~ g > u~ g g g` | `d g > d g g g` | `[0, 1, 2, 3, 4, 5]` | true | + +The second relation exchanges the incoming legs and conjugates. Its +representative leg 0, the incoming `d`, takes the momentum of member leg 1, +the incoming `u~`, which conjugation and the relabelling `u <-> d` turn into a +`d`. Loaded by its own expression, each of these three members agrees with a +separately generated copy of the same subprocess to about `1e-14`. + +`pyamplicol inspect artifacts/pp_4j_blind` summarises the 23 classes and lists +the members of each, grouped into non-conjugated and conjugated blocks, with +`g g > g g g g` listed as `ungrouped`. +`--process p_p_to_j_j_j_j_52` lists one class member by member, with +permutations, and `--process 'd u~ > d u u~ u~' --full-physics` adds a +`Process Group` table with that member's representative, factor, conjugation +and permutation. + +The UMAMI provider in `API/umami` has 23 `runtime_processes` rows. Its +`members` cover all 486 subprocess IDs, and its `grouping.assumptions` list +contains the flavour-blind entry. + +To check the assumption itself, generate all 486 subprocesses without +`--flavor-blind` and let the numerical analysis group them: + +```console +pyamplicol generate "p p > j j j j" artifacts/pp_4j \ + --model built-in-sm --analyze-process-groups numerical +``` + +This generates all 486 runtime processes, so it takes much longer. The +analysis then finds the same 23 groups, with the same representatives, +permutations and conjugation flags, all with factor 1, now with basis +`numerical`. + +## Limitations + +- `--flavor-blind` is an assumption. Its warning comes from a per-vertex check + and can be raised for a relation that holds (see [Warnings](#warnings)). +- A flavour-blind representative that later turns out to have no tree-level + amplitude is skipped as unsupported, and its members disappear with it. The + skip warning gives only their number, not their names. +- A routed member uses its representative's model parameters, so its value + follows the representative's flavours. Changing a parameter that + distinguishes their flavours (a mass, width or Yukawa coupling) invalidates + the relation; generate the process explicitly if it must follow independent + parameter values. +- Correlated evaluation through a member is supported only when the composed + permutation (relation and requested order) is the identity and the relation + is not conjugated; the factor is then applied. Other selections fail with an + error naming the member and its representative. +- For conjugated members with several open quark lines, the reversed LC flow + IDs may list the open lines in another order than a separate generation of + the same subprocess would; values and flow selection agree. +- The numerical analysis is a test at three f64 points, not a proof: two + matrix elements that agree at those points within `1e-9` are grouped. +- The search covers only relations with a rational factor whose numerator + and denominator are at most 99, a leg permutation within each side, and + optional charge conjugation with unchanged helicities. It does not cover + CP-type relations or crossing between the initial and final states. +- Subprocesses generated with selected helicities or colour flows are never + grouped numerically. +- Relations are only searched between subprocesses with equal particle types + per side, so two different mass parameters with equal values are never + identified. +- Numerical relations do not shrink the artifact or the UMAMI export: both + subprocesses stay generated. diff --git a/pyamplicol/docs/user/process-selection-and-permutations.md b/pyamplicol/docs/user/process-selection-and-permutations.md new file mode 100644 index 0000000000..6739b2a191 --- /dev/null +++ b/pyamplicol/docs/user/process-selection-and-permutations.md @@ -0,0 +1,342 @@ +--- +title: "Process Selection and Permutations" +nav_order: 3 +parent: "Configuration" +--- + + +# Process Selection and Permutations + +pyAmpliCol separates two related operations: + +- **generation requests** decide which process representatives an artifact + contains; +- **runtime selection** chooses one representative and the public particle + order in which the caller wants to use it. + +This lets one evaluator serve equivalent incoming and outgoing orderings +without asking users to memorize generated alias names. + +## A representative is not an output directory + +An artifact may contain one process or many. Each retained concrete process +has a stable ID, such as `p_p_to_z_j_j_3`; the artifact itself is still loaded +from one directory: + +```console +pyamplicol inspect artifacts/pp_zjj +pyamplicol inspect artifacts/pp_zjj --process p_p_to_z_j_j_3 +``` + +The same process can be selected with a readable expression: + +```console +pyamplicol inspect artifacts/pp_zjj --process 'd d~ > g z g' +``` + +Stable process IDs are selectors, not paths below `artifacts/pp_zjj`. + +## How a selector is resolved + +Rusticol applies a deterministic precedence order: + +1. stable process ID; +2. explicit alias ID; +3. exact stored process expression; +4. exact explicit-alias expression; +5. one unique permutation-equivalent expression; +6. a process-group member that was not generated: its stable ID, its exact + expression, then one unique permutation-equivalent expression (see + [Process-group members](#process-group-members)). + +Case and whitespace in a process expression are normalized. At the +permutation steps, the incoming and outgoing particle multisets are matched +independently. + +For example, an artifact may store the representative ordering +`d d~ > Z g g`, while the caller requests: + +```text +d d~ > g z g +``` + +The outgoing multisets are equal, so the request selects the same evaluator and +exposes the requested public ordering. No pre-generated alias is required. + +If more than one stored representative could match, selection fails and lists +the candidate stable IDs. pyAmpliCol never silently picks an ambiguous process. + +## The side-preserving rule + +Permutations are allowed **within** each side of `>`: + +```text +d d~ > Z g g -> d~ d > g Z g allowed +``` + +A leg may never cross the process boundary: + +```text +d d~ > Z g g -> d Z > d~ g g not equivalent +``` + +Repeated identical particles are matched by a deterministic first-unused +rule. This makes the mapping reproducible even when several `g` legs are +indistinguishable by particle name. + +## What is remapped + +The selected ordering is not cosmetic. One representative-to-public +permutation is applied centrally to: + +- input momenta and external-particle PDGs; +- external-particle metadata and momentum slots; +- helicity vectors, IDs, representatives, and selectors; +- LC color-flow words, IDs, replay labels, and selectors; +- reduction groups and resolved-output metadata; +- bundled validation kinematics; +- compiled, eager, recurrence, and on-the-fly f64 execution; +- Python exact execution when retained expressions support it. + +Consequently, all public inputs and outputs describe the ordering written in +the selector: + +```python +from pyamplicol import Runtime + +runtime = Runtime.load("artifacts/pp_zjj", process="d d~ > g z g") +print(runtime.physics.process) # d d~ > g Z g +print([particle.name for particle in runtime.physics.external_particles]) +``` + +The process key continues to identify the representative evaluator. Generic +permutation metadata records the active public mapping; explicit aliases also +retain their alias identity. + +## Process-group members + +An artifact generated with `--flavor-blind` stores one representative per +flavour class and lists the other processes of each class in its +`process-groups.json` payload. Every listed process stays selectable by its +stable ID or expression through `Runtime.load`, `pyamplicol evaluate`, and the +C, C++, Fortran, and Rust APIs, even though it has no evaluator of its own. + +Rusticol reads `process-groups.json` only when no stored process ID, alias, +or stored expression (exact or side-permuted) matches the selector, and +verifies its declared size and SHA-256 first. A member relation states + +```text +ME_member(p) = factor * ME_representative(q), q[r] = p[permutation[r]] +``` + +with an exact rational `factor`. Selecting a member, possibly in yet another +side order, therefore composes two permutations (requested order from member +order, member order from representative order) and evaluates the +representative with: + +- the composed permutation applied to momenta, helicities, LC flows, + reductions, and resolved metadata, exactly as for any permutation; +- the member's own flavours, particle names, process ID, and expression in + `runtime.physics`; +- every matrix element (totals, resolved helicity/flow components, selected + subsets, profiles, and exact `precision != 16` results) multiplied by + `factor`; +- for a charge-conjugated member (`conjugated: true`), every LC color-flow + word mapped through the permutation and then reversed. Flow IDs follow the + reversed words, and color-flow selectors given in the member's labels map + back to the representative's. NLC/full contracted color is unaffected. + With several open quark lines, a reversed word may list the lines in + another order than a separate generation of the member would, as with any + permutation; read flow IDs from the loaded runtime. + +For instance, an artifact storing `u g > u g` answers `u~ g > u~ g`; its flow +`flow:1,4,2,3` is the representative's `flow:3,2,4,1`: + +```python +runtime = Runtime.load("artifacts/qg", process="u~ g > u~ g") +print(runtime.representative_process_key) # u_g_to_u_g +print(runtime.external_permutation) # composed representative -> public +print(runtime.process_group_factor) # Fraction(1, 1) +print(runtime.charge_conjugated) # True +``` + +Native APIs report the same relation in their runtime metadata JSON as +`process_group_member_of`, `process_group_factor`, and `charge_conjugated`; +the C API also provides `rusticol_runtime_process_group_relation`. A +selection that is not a non-generated member reports factor `1/1`, no +conjugation, and keeps the unchanged fast path. To list a member's helicity +and flow IDs, run `pyamplicol inspect ARTIFACT --process MEMBER +--full-physics`; the compact inventory lists generated processes only. + +A member uses its representative's model parameters, and its factor was +established at generation time: changing a parameter that distinguishes the +member's flavours from the representative's (a mass, width, or Yukawa +coupling, for example) invalidates the relation. Generate such a process +explicitly when it must follow independent parameter values. + +Born correlations through a member are available only when the composed +permutation is the identity and the relation is not charge-conjugated; the +factor is then applied. Other cases fail with a compatibility error naming +the member and its representative: generate that process explicitly. + +## Kinematics follow the requested order + +Momenta always have shape: + +```text +[point][external particle][E, px, py, pz] +``` + +The external-particle dimension follows `runtime.physics.external_particles`, +which in turn follows the selected expression. For `d d~ > g z g`, a custom +five-particle point is therefore ordered as: + +```json +[ + ["250.0", "0", "0", "250.0"], + ["250.0", "0", "0", "-250.0"], + ["204.406", "204.406", "0", "0"], + ["91.188", "0", "0", "0"], + ["204.406", "-204.406", "0", "0"] +] +``` + +The first two rows are the incoming `d` and `d~`; the remaining rows are the +outgoing `g`, `z`, and `g` in exactly that order. + +Generated standalone drivers accept the file with `--kinematics`: + +```console +python artifacts/pp_zjj/API/python/check_standalone.py \ + --process 'd d~ > g z g' \ + --kinematics my_sample_point.json \ + --precision 80 +``` + +The file may contain one point as `[external][4]` or a singleton batch as +`[[external][4]]`. Components may be JSON numbers or decimal strings. The +Python exact path preserves decimal strings without an intermediate f64 +conversion; native drivers parse the same representation as f64. Booleans, +non-finite values, multiple points, and incorrect shapes are rejected. + +When `--kinematics` is omitted, the bundled representative validation point is +automatically reordered to the requested public expression. + +## Helicity and color selectors follow the requested order + +Always obtain stable IDs after selecting the desired process ordering: + +```python +runtime = Runtime.load("artifacts/pp_zjj", process="d d~ > g z g") + +helicity = runtime.physics.helicity_ids[0] +flow = runtime.physics.color_flow_ids[0] +value = runtime.evaluate( + momenta, + helicities=[helicity], + color_flows=[flow], +) +``` + +A helicity ID encodes values in the public external order. An LC flow word and +its stable ID likewise use public leg labels. Reduction metadata points to the +remapped public IDs, so `evaluate_resolved()` remains consistent with +`evaluate()` after any permitted permutation. + +Color-flow selection is available only at LC. NLC and full-color artifacts +have a contracted output axis and accept helicity selectors only. See +[Runtime and Selectors](runtime-and-selectors.md). + +## Generation-time multiprocess expansion + +One request may expand through multiparticle labels: + +```toml +[process] +entries = [{ expression = "p p > Z j j" }] +flavor_scheme = 2 +max_quark_lines = 2 + +[process.multiparticles] +p = ["d", "d~", "g"] +j = ["d", "d~", "g"] +``` + +For the shipped serialized Standard Model this request produces eight +candidates: each side is enumerated as a multiset, one candidate per +incoming/outgoing permutation class, and candidates violating a charge +conserved by every model vertex are dropped. Seven candidates have tree-level +amplitudes and are stored. The remaining class, `g g > Z g g`, is loop-induced +and is reported as unsupported at tree level rather than emitted as a zero +process. + +Those seven evaluators cover all 18 ordered tree-level channels, including +public orderings not stored as explicit aliases. + +With `--flavor-blind`, expansion keeps one representative per flavour class +as well (see +[Flavour-blind generation](models-and-processes.md#flavour-blind-generation)). +The other members are not stored as processes, but they stay selectable: +runtime selection routes a member ID or expression through +`process-groups.json` to its representative (see +[Process-group members](#process-group-members)), and UMAMI exports apply the +same mapping. + +## The model-generic `all` label + +Every model supplies `all` as its declaration-ordered valid physical external +states. It includes propagating, physical-spin particles and excludes ghosts, +Goldstones, non-propagating records, and auxiliary states. + +User definitions merge over model defaults, so defining `p` or `j` leaves +`all` available. Defining `all` explicitly replaces the default. + +Inspect a bounded built-in expansion before generating it: + +```console +pyamplicol model processes "p p > all all" \ + --model built-in-sm \ + --flavor-scheme 1 \ + --max-quark-lines 0 +``` + +`all` products can expand combinatorially, especially for large UFO models. +Use a narrower custom label for routine production work. + +## Explicit process sets and aliases in Python + +When stable names are useful, construct a `ProcessSet`: + +```python +from pyamplicol import ProcessRequest, ProcessSet + +processes = ProcessSet( + requests=( + ProcessRequest.parse("u u~ > Z g", name="uubar_Zg"), + ProcessRequest.parse("u u~ > Z g g", name="uubar_Zgg"), + ) +) +``` + +Explicit `ProcessAlias` records remain available for named public interfaces, +but ordinary callers do not need to enumerate every incoming or outgoing +ordering. Runtime inference uses the same side-preserving mapping. + +## Failure guide + +| Message or symptom | Meaning | Action | +| --- | --- | --- | +| Several candidate stable IDs are listed | The expression is permutation-equivalent to more than one stored representative | Select the intended stable ID explicitly | +| No process matches | The artifact does not contain the requested particle multisets, or a leg crossed `>` | Run `pyamplicol inspect ARTIFACT` and correct the request | +| Several process-group member IDs are listed | The expression matches more than one non-generated member | Select the intended member ID explicitly | +| `process-groups payload ... sha256` or size error | `process-groups.json` changed without its manifest record | Restore the original file, or regenerate the artifact | +| Kinematics shape mismatch | The point does not have one four-vector per public external particle | Follow `runtime.physics.external_particles` or the `--process` order | +| Unknown helicity or flow ID | The ID came from another ordering or process | Read IDs after loading the requested process ordering | +| Color-flow selector rejected | The artifact is NLC/full, not LC | Select helicities only, or generate an LC artifact | + +## See also + +- [Models and Processes](models-and-processes.md) +- [Runtime and Selectors](runtime-and-selectors.md) +- [Native APIs](native-apis.md) +- [Artifacts and Portability](artifacts-and-portability.md) diff --git a/pyamplicol/docs/user/profiling-and-benchmarking.md b/pyamplicol/docs/user/profiling-and-benchmarking.md new file mode 100644 index 0000000000..9d9040a2d5 --- /dev/null +++ b/pyamplicol/docs/user/profiling-and-benchmarking.md @@ -0,0 +1,334 @@ +--- +title: "Profiling and Benchmarking" +nav_order: 5 +has_children: true +--- + + +# Profiling and Benchmarking + +pyAmpliCol has three intentionally separate performance tools: + +- `pyamplicol profile` measures one generated artifact through the same + optimized total path as `Runtime.evaluate()`; +- a copied [profiling campaign](profiling-campaigns.md) coordinates many model, + process, color, and execution-mode cells and renders comparison PDFs; +- the source-checkout [FullColor FFT profiler](fullcolor-fft-profiling.md) + schedules resumable direct/FFT, reference, AmpliCol, and MadGraph comparison + cells under explicit per-child resource limits. + +Use the direct profiler for a focused runtime question. Use a copied campaign +for a reproducible general matrix. Use the FullColor FFT driver for the +dedicated exact-colour scaling comparison. + +## First profile + +From the copied example workspace, generate the primary artifact and profile +one public process ordering: + +```console +pyamplicol generate_pp_zjj_from_ufo_sm.toml + +pyamplicol profile artifacts/pp_zjj \ + --process 'd d~ > g z g' \ + --momenta data/pp_zjj_momenta.json \ + --target-runtime 1.0 \ + --batch-size 128 \ + --color-flow 1 \ + --precision 16 +``` + +The shipped shorthand card runs the same one-second workload: + +```console +pyamplicol benchmark.toml +``` + +`benchmark` is a compatibility alias for `profile`; both direct commands +accept the same options. TOML retains `action = "benchmark"`—there is no +separate `action = "profile"` card value. + +## What is timed + +At precision 16, the headline wall timer starts inside Rusticol after Python +has converted the input to the native momentum buffer. It covers the ordinary +optimized core evaluation, including the selected engine's scheduling and +reduction. This is the runtime shared by Python, C11, C++17, Fortran 2008, and +Rust 2021 callers; it deliberately excludes Python/NumPy conversion overhead. + +The profiler separately runs a paired native-attribution pass over the same +batch and repetition count. That pass explains where time was spent but never +replaces the ordinary headline wall sample. + +The main rows mean: + +| Metric | Interpretation | +| --- | --- | +| `wall time` | Repeated ordinary native runtime calls, normalized per point | +| `evaluator total` | Separately measured, minimally instrumented complete evaluator call | +| `Profile wall (paired profiled pass)` | Wall time of the attribution-enabled paired pass | +| `Native input pack` | Native-side input preparation after caller conversion | +| engine-specific runtime rows | Exclusive or explicitly labeled lane attribution | +| `Other Rusticol core` | Residual work closing the exclusive component sum against profile wall | + +For compiled Direct-Arena, the row otherwise represented by an orchestration +counter is labeled **Direct-Arena runtime envelope**. It includes arena setup, +generated kernel execution, reduction, normalization, and orchestration. It is +not a kernel-only time. The current uninstrumented Direct-Arena hot path cannot +derive a pure kernel duration from that bucket. + +Rows whose measured mean is exactly zero are omitted from the human timing +breakdown, which keeps the table focused on work actually observed for the +selected engine. + +OTF has one additional, separately reported preparation observation. Before +configured warmups, the profiler snapshots the compact native runtime census, +times exactly one requested-selector evaluation over the full benchmark batch, +and authenticates that the family is retained. It labels whether the starting +handle was cold or already retained. That observation is not a headline wall +sample and is ineligible for ratios or acceptance. It is also distinct from an +explicit public `warm_up(...)`, which always takes exactly one binary64 point, +never a profiling batch. + +## Sampling and uncertainty + +The profiler performs configured warmups, estimates one evaluation cost, and +calibrates: + +- the number of independent timed blocks; +- repetitions within each block; +- the batch of phase-space points used by every call. + +Fast evaluators receive more repetitions per block. Slow evaluators use fewer +blocks while never dropping below `--minimum-samples`. A suspiciously slow +first one-repetition calibration observation is confirmed once at the same +repetition count so a transient busy-host outlier does not force an unusable +sampling plan. + +The human report includes: + +- mean time per point; +- sample standard deviation; +- standard error of the mean; +- relative standard error (`standard error / mean`); +- sample and repetition counts; +- target and measured cumulative timing; +- process, mode, precision, batch, and selector workload. + +The uncertainty describes repeatability of the measured blocks on that host; +it is not a physics uncertainty. + +Interrupting with `Ctrl-C` stops after the current complete block and still +prints the usual tables from completed samples. The result is explicitly +marked partial. + +## Selectors change the workload + +Profile selectors are runtime choices. They do not require regenerating an +artifact that retained complete coverage: + +```console +pyamplicol profile artifacts/qq_z6g_recurrence_jit_o2 \ + --process 'u u~ > Z g g g g g g' \ + --color-flow 2 \ + --target-runtime 5 +``` + +or select one helicity: + +```console +pyamplicol profile artifacts/qq_z6g_recurrence_jit_o2 \ + --helicity 'h:-1,+1,-1,+1,+1,-1,+1,-1,+1' +``` + +For LC, match the flow layout to the workload: + +| Workload | Preferred generated layout | +| --- | --- | +| One selected flow, all helicities | Default `topology-replay` | +| All flows, one selected helicity | `all-flow-union` | + +When both profiling selector axes are omitted, pyAmpliCol chooses a +deterministic hot workload for the stored layout: one computed flow for +`topology-replay`, or one computed nonzero helicity for `all-flow-union`. +Explicit subsets and selected-axis lists are preserved; a complete summed-axis +list is normalized to equivalent omission. Any valid explicit shape outside +the stored layout's optimized workload emits at most one warning per loaded +process, before timing begins. This is a performance warning, not a correctness +failure; both layouts preserve complete physical flow and helicity coverage. +Ordinary unqualified evaluation still returns the complete matrix element. + +There is no `all` selector shorthand. A complete list on the layout's summed +axis is equivalent to omission and still permits hot-selector inference; a +complete list on its selected axis explicitly requests the broader all-entry +workload. Repeat `--color-flow` or `--helicity` for the complete stable-ID list +shown by `inspect`; in Python, pass `runtime.physics.color_ids` or +`runtime.physics.helicity_ids`. See +[Runtime and Selectors](runtime-and-selectors.md#omitted-selectors-and-requesting-every-entry) +for complete examples. + +NLC/full artifacts have contracted color and reject flow selectors. + +## Native timing attribution + +When the selected f64 runtime exposes native profiling, the report can include: + +- input packing and crossing; +- state preparation and clearing; +- source and momentum setup; +- model-parameter setup; +- stage or amplitude input/evaluator/output work; +- reduction, total materialization, and final output copies; +- selector planning, gather, and scatter; +- recurrence source, contribution, finalization, closure, and replay work; +- OTF query-family recurrence-schedule work, separately labelled from both the + complete evaluator envelope and recurrence mode's static schedule; +- eager initialization, gather, kernel calls, scatter, finalization, closure, + and copy-out; +- per-stage timing where applicable. + +Some internal rows are **attribution**, not additional top-level time. For +example, a full-stage evaluator envelope may already own its leaf-input gather. +The report labels these paths so they are not added twice. + +`Other Rusticol core` is the residual between the paired profile wall and the +exclusive component sum. A small nonzero value is ordinary unsplit bookkeeping; +it should not be interpreted as hidden Python overhead because the timer is +already inside Rusticol. + +## Native work counters + +The optional counter table makes a profile easier to interpret by reporting +work volume as well as duration. Depending on the engine it can include: + +- input bytes and native container counts; +- state, source, momentum, and model-parameter components; +- stage/amplitude movement and backend calls; +- reduction and materialized output values; +- selector gather/scatter volume; +- observed reusable-scratch capacity changes; +- explicit native output allocations; +- recurrence call and row counts; +- Direct-Arena engine/call and boundary-byte counts. + +Movement and materialization are normalized per profiled point. Backend-call +and allocation activity are normalized per runtime call. + +For the fused compiled Arena path, boundary input/current-output/amplitude- +output bytes must be zero and Arena calls must cover evaluator backend calls. +An invalid counter relationship is rejected rather than printed as a valid +optimized profile. + +## Progress and machine-readable output + +On a terminal, `--progress auto` selects a colored live progress display with +elapsed time, uncertainty, repetitions, and batch size. A redirected or +non-interactive run uses rate-limited log messages instead. + +Choose explicitly when scripting: + +```console +pyamplicol profile artifacts/pp_zjj --progress log +pyamplicol profile artifacts/pp_zjj --progress off --json > profile.json +``` + +JSON mode reserves stdout for the machine-readable result; diagnostics stay on +stderr. + +## Python API + +The typed API accepts either a loaded runtime or an artifact path: + +```python +import json +from pathlib import Path + +from pyamplicol import BenchmarkConfig, BenchmarkRunner, Runtime + +momenta = json.loads(Path("data/pp_zjj_momenta.json").read_text()) +runtime = Runtime.load("artifacts/pp_zjj", process="d d~ > g z g") +runner = BenchmarkRunner( + BenchmarkConfig( + target_runtime=1.0, + batch_size=128, + precision=16, + warmup_runs=2, + minimum_samples=5, + color_flow_ids=("1",), + ) +) +result = runner.run(runtime, points=momenta) + +print(result.wall_time_per_point) +print(result.uncertainty.standard_error) +print(result.repetitions_per_sample) +``` + +`BenchmarkResult.timing_breakdown` exposes typed component timings, stage +timings, and counter summaries when the backend supplies them. + +Python may profile a higher-precision evaluator when the artifact retains one, +but native component attribution is f64-only. A non-f64 run therefore reports +wall timing rather than the Rusticol f64 breakdown. OTF rejects non-f64 +precision altogether. See [Python API](python-api.md) and +[Symbolica and Licensing](symbolica-and-licensing.md). + +## Reproducible campaign reports + +To create an independent campaign workspace from an installed wheel: + +```console +pyamplicol profiling-campaign copy ./pyamplicol-profiling-campaign --force +cd pyamplicol-profiling-campaign +./steer_performance_campaign.py run \ + --workers 1 --table matrix --process-id 1 --multiplicity 1 \ + --color-approximation lc --generation-mode non-union-flow \ + --generation-engine recurrence --model built_in +./steer_performance_campaign.py refresh-pdf +``` + +That intentionally small smoke measures only the final-state-multiplicity-one +`d d~ > Z` recurrence cell. Broad campaign matrices belong on dedicated hosts. +See [Profiling Campaigns](profiling-campaigns.md) for continuation, selective +retry, artifact retention, optional original-AmpliCol comparison, and PDF +section controls. + +The repository publishes two rendered manual snapshots: + +- [MacBook M3 report](https://github.com/MadGraphTeam/pyamplicol/blob/main/docs/performance_reports/macbook_M3_pyAmpliCol.pdf) +- [AMD EPYC report](https://github.com/MadGraphTeam/pyamplicol/blob/main/docs/performance_reports/EPYC_pyAmpliCol.pdf) + +They are measurement-host results, not release-CI benchmarks. Raw attempts and +campaign workspaces remain local. + +## Comparing results responsibly + +- Keep process, model, parameter card, momenta, color accuracy, flow layout, + mode, backend, precision, selectors, and batch size fixed. +- Compare ordinary wall means with their standard errors; do not compare one + engine's internal component against another engine's complete wall time. +- Treat very small relative standard errors on a busy host cautiously; repeat + the full profile if system conditions materially changed. +- Use a fresh runtime after rebuilding an artifact. +- Record the exact artifact and package version, but do not infer physics + independence from agreement among APIs that all call Rusticol. + +## See also + +- [Runtime and Selectors](runtime-and-selectors.md) +- [Generation Modes and Evaluators](generation-modes-and-evaluators.md) +- [LC workloads and execution modes](lc-workloads-and-execution-modes.md) +- [Profiling Campaigns](profiling-campaigns.md) +- [FullColor FFT Profiling](fullcolor-fft-profiling.md) +- [Examples Gallery](examples-gallery.md) + + +```{toctree} +:hidden: + +profiling-campaigns +fullcolor-fft-profiling +troubleshooting +release-and-support +../performance_reports/README +``` diff --git a/pyamplicol/docs/user/profiling-campaign-walkthrough.md b/pyamplicol/docs/user/profiling-campaign-walkthrough.md new file mode 100644 index 0000000000..9c7a9bc995 --- /dev/null +++ b/pyamplicol/docs/user/profiling-campaign-walkthrough.md @@ -0,0 +1,715 @@ +--- +title: "Profiling campaign: from measurements to the PDF" +nav_order: 2 +parent: "Get started: a gentle walkthrough" +--- + + +# Profiling campaign: from measurements to the PDF + +The profiling campaign answers a practical physics-computing question: for a +fixed process, multiplicity, colour treatment, and workload, which pyAmpliCol +execution mode is both numerically validated and fastest on *this* machine? +It also records how that result compares with original AmpliCol and, where +appropriate, MadGraph. + +Think of a copied campaign as a self-contained electronic lab notebook. It +contains the catalogue of measurements to make, the report sources, and a +visible `campaign_artifacts/` directory in which every attempt and current +result is recorded, together with bounded diagnostics and any process output +that must be retained. The controller can stop and resume without turning a +long scan into one fragile all-or-nothing job. + +The dashboard and report use a few precise words: + +| Word | Plain-language meaning | +| --- | --- | +| **cell** | One measurement question: one process, multiplicity, model, colour treatment, workload, and execution mode | +| **controller** | The steering script that chooses cells, starts measurements, and records their outcome | +| **worker** | One supervised operating-system process doing one cell at a time; a worker may itself be allowed several CPU cores | +| **dependency** | Another cell whose numerical result is needed to validate the requested cell, such as recurrence or MadGraph | +| **process output** | The generated directory that can later evaluate phase-space points, roughly like a MadGraph standalone-process directory; internal files may call it an `artifact` | +| **current result** | The latest complete record that the next report will use, not a worker that happens to be running now | +| **provenance** | The recorded origin of a number: source revision, settings, machine, phase-space point, and validation link | +| **authenticated record** | A record whose identity and integrity fields the controller has checked; this is a data-integrity term, not a user login | +| **precision lane** | One supported numerical route, such as `p16` (ordinary double precision) or `p200` (about 200 decimal digits); “lane” does not mean another physics approximation | + +A new attempt replaces a cell's current-result pointer only after the complete +record has been written. An interrupted or half-written attempt therefore +cannot silently become the number shown in the PDF. + +The campaign is deliberately broader than the ordinary `pyamplicol profile` +command. `profile` times one process output that you already selected. The +campaign generates many outputs, checks them against independent numerical +references, profiles the accepted results under matched workloads, and fills +the tables in `pyAmpliCol.pdf`. + +## Create an independent campaign workspace + +Start from an installed pyAmpliCol environment and copy the packaged template +into a new or empty directory: + +```console +pyamplicol profiling-campaign copy ./my-profiling-campaign +cd ./my-profiling-campaign +./steer_performance_campaign.py --help +``` + +The copied directory is the unit to archive or move. All measurement state +stays below it; the controller does not consult an old repository-level +`.artifacts` directory. + +Two optional external programs extend the comparisons: + +```console +pyamplicol profiling-campaign copy ./my-full-campaign \ + --local-amplicol /path/to/clean/original-AmpliCol-checkout \ + --local-madgraph /path/to/MG5_aMC +``` + +- Original AmpliCol supplies a legacy numerical diagnostic and the performance + denominator in the “versus AmpliCol” tables. It is not a correctness + authority for pyAmpliCol. The checkout must be clean and complete and expose + the maintained profiling and colour-probe interface from PR #12. +- MadGraph standalone supplies the independent full-colour reference for the + UFO Standard Model comparison views. The path must contain executable + `bin/mg5_aMC` and the standard `models/sm` UFO model. + +The saved paths are defaults for that copy. A later `run --original-amplicol +PATH` or `run --madgraph PATH` overrides one invocation. + +The reset JSON and TeX tables shipped in a fresh copy are **templates**, not +benchmark predictions. Their cells begin unmeasured or `N/A`; they are filled +from results obtained on your machine. In particular, do not expect a reset +copy to contain the example numbers shown later on this page. + +## First preview, then run a small real cell + +Before using substantial CPU time, preview the selected work: + +```console +./steer_performance_campaign.py run \ + --dry-run \ + --table matrix \ + --process-id 1 \ + --multiplicity 1 \ + --color-approximation lc \ + --generation-mode non-union-flow \ + --generation-engine recurrence \ + --model built_in \ + --no-dependencies-added +``` + +The preview separates the cells you requested from any prerequisite or +numerical-reference cells that the controller would add. It also prints the +public pyAmpliCol recipe corresponding to each runnable cell, explicitly +labelling any pre-generation template or report-protocol exception. + +Remove `--dry-run` to perform the deliberately small installation smoke test: + +```console +./steer_performance_campaign.py run \ + --workers 1 \ + --table matrix \ + --process-id 1 \ + --multiplicity 1 \ + --color-approximation lc \ + --generation-mode non-union-flow \ + --generation-engine recurrence \ + --model built_in \ + --no-dependencies-added \ + --no-dashboard +``` + +This measures only the recurrence cell for `d d~ > Z`, at final-state +multiplicity one. It is an installation check, not a representative estimate +of the time needed for a broad high-multiplicity campaign. + +Release wheels omit the optional terminal-dashboard component (implemented +with Ratatui). Their campaigns continue **headlessly**, meaning without the +interactive dashboard, as though `--no-dashboard` had been supplied. A +contributor installation prepared with that optional component can show the +live coloured dashboard. + +## Select the physics question, not individual shell jobs + +The campaign catalogue is a matrix: each axis describes part of the physics or +measurement question, and their allowed combinations define the cells. Useful +selector dimensions include: + +| Selector | What it chooses | Examples | +| --- | --- | --- | +| `--table` | A report surface | `matrix`, `matrix_best`, `z_table`, `reference`, `scalar`, or an exact dataset ID | +| `--process-id` | One of the numbered process families | `1`, or a catalogue key or quoted process | +| `--multiplicity` | Number of final-state particles, `n` | `3 4` | +| `--color-approximation` | Colour treatment | `lc`, `nlc`, `full` | +| `--generation-mode` | Physical workload/layout | `non-union-flow`, `union-flow`, `contracted` | +| `--generation-engine` | Program or execution mode | `amplicol`, `recurrence`, `compiled`, `eager`, `on-the-fly` | +| `--model` | Model preparation used by the cell | `built_in`, `sm_ufo` | +| `--cell-id` / `--cell-id-file` | Exact canonical cells | IDs printed by `inspect` or the summary files | + +Multiple values within one selector are alternatives; different selector +dimensions are combined. For example: + +```console +./steer_performance_campaign.py run \ + --dry-run \ + --table matrix \ + --multiplicity 3 4 \ + --generation-engine recurrence compiled \ + --model built_in sm_ufo +``` + +means “multiplicity 3 **or** 4, recurrence **or** compiled, and built-in +**or** UFO-SM where those catalogue cells exist.” An omitted dimension means +all of its applicable values. Use `--help` to see the exact current choices and +aliases before scripting a large selection. + +The campaign names map to the physical LC calls as follows: + +| Campaign name | Repeated request | Generated layout in recurrence/eager/compiled | +| --- | --- | --- | +| `non-union-flow` | one selected flow, summed over all helicities | `topology-replay` | +| `union-flow` | all flows, one selected helicity | `all-flow-union` | +| `contracted` | the requested axes already summed/contracted | used for NLC and full-colour campaign cells | + +OTF accepts the same two LC requests, but does not write either layout during +generation. Its first selected request constructs the needed family in memory. +This is why “engine” and “generation mode” are separate selectors: the former +chooses *when and how* currents are prepared, while the latter chooses *which +physics workload* is timed. + +By default, the controller also schedules the numerical reference needed to +accept each requested candidate. The dashboard calls such a prerequisite a +dependency. `--no-dependencies-added` suppresses optional independent-reference +cells, but never removes unavoidable model, process, or selector prerequisites. +It is useful for a narrow smoke test; a publication campaign normally keeps +those independent checks. + +### Resource controls + +The safe defaults are one worker, one core per worker, a one-hour +generation/preparation limit, a one-hour whole-worker limit, a decimal 30 GB +RAM cap for each supervised worker process tree, a 64 MiB cap for each watched +attempt-output stream, and a 5 GiB free-disk reserve. The main controls are: + +```text +--workers N +--cores-per-worker N +--amplicol-build-jobs N +--generation-time-limit SECONDS +--worker-wall-limit SECONDS +--ram-limit BYTES +--campaign-ram-limit BYTES +--attempt-output-limit BYTES +--minimum-free-disk BYTES +--target-measurement-duration SECONDS +--minimum-samples N +--warmups N +--batch-size N +``` + +`--cores-per-worker` controls pyAmpliCol generation and runtime construction. +Original AmpliCol compilation has the separate `--amplicol-build-jobs` control, +which defaults to 1 even when pyAmpliCol receives more cores. Keep that serial +default for the maintained legacy checkout: its generator Make target is not +parallel-safe. + +`--ram-limit` is a cap for each worker tree—the worker plus every compiler or +external program it starts. The optional +`--campaign-ram-limit` is an aggregate ceiling: the controller conservatively +divides it by the requested worker count and uses the smaller of that share and +the per-worker cap. For example, ten workers with both limits set to decimal +30 GB receive at most 3 GB each, so they cannot collectively claim ten times +30 GB: + +```console +./steer_performance_campaign.py run \ + --workers 10 --ram-limit 30000000000 \ + --campaign-ram-limit 30000000000 \ + --table matrix --multiplicity 1 2 3 +``` + +The controller measures the worker and all of its descendants as one memory +tree. It guards resident RAM (RSS), and on macOS also checks the operating +system's physical-footprint measure and uses whichever is larger. This RAM +guard does **not** limit bytes written to disk: a runaway external-program log +can fill a volume while the process remains well below its RAM ceiling. +`--attempt-output-limit` therefore stops a worker when any watched attempt log +or output file crosses its per-file limit; the default is 64 MiB (67,108,864 +bytes). `--minimum-free-disk` stops it when free space on the campaign-artifact +volume falls below the reserve; the default is 5 GiB (5,368,709,120 bytes). +Both cases are recorded as explicit errors rather than being mistaken for +physics or timing results. + +Increase parallelism only when the machine has enough RAM for that many +independent worker trees. `--fail-fast` changes validation scheduling into +multiplicity waves and stops after the first final non-success, including a +required mismatch, error, timeout, or supervisor-confirmed resource cap. A +legacy AmpliCol disagreement remains a diagnostic rather than a stopping +condition because original AmpliCol is not a correctness authority. + +## What one campaign cell actually does + +For an ordinary successful cell, the controller: + +1. resolves the catalogue process, model, colour treatment, workload, and + execution mode; +2. prepares reusable model data when needed; +3. generates the process output and records generation time; +4. evaluates a shared deterministic phase-space point; +5. checks the resolved sum and the required cross-mode or MadGraph agreement; +6. profiles the accepted workload in repeated timing chunks; and +7. seals the attempt (marks the record complete and no longer editable) and + makes it the cell's current result in one replacement. + +Independent cells can run in parallel. Dependencies are explicit: for example, +the matching recurrence result checks compiled and eager results, while +MadGraph standalone supplies the independent full-colour reference for UFO-SM +results. For LC OTF cells through `n <= 4`, the matching recurrence cell checks +every resolved helicity/flow component, not only their final sum; no compiled +output is generated merely to perform that check. + +The common deterministic point matters. A timing ratio is published only when +the candidate and denominator are explicitly linked to the same phase-space +point and the required validation has passed. This prevents a visually +plausible ratio from accidentally joining unrelated measurements. + +## Watch, stop, and resume without losing completed work + +In the interactive dashboard, `Ctrl-C` stops the campaign safely. `Esc` first +closes an open command drawer; otherwise it performs the same orderly stop. In +headless mode, use `Ctrl-C`. The controller stops dispatch, terminates each +supervised worker and any child programs with its configured grace period, +preserves completed current results and compact interruption evidence, marks +the campaign as no longer live, writes the summary IDs, and exits with the +conventional interrupted status (code 130). + +There is no supported “hold” or in-memory pause command. To pause a long scan, +stop it orderly and later run the same command again. Compatible successful +results from the same source revision (code version) are reused automatically; +a cell with no compatible successful current is attempted again. If an older +successful current survived the interrupted attempt, the ordinary rerun reuses +that result; add `--force-refresh` only when you intentionally require a fresh +attempt. Historical results from another source revision participate only when +you explicitly pass `--continue-across-revisions`. Use that option only after +checking that the intervening edits cannot change the generated physics, +validation, or measured runtime path—for example, a report-layout-only change. +Each reused result still records its original revision, so the PDF's provenance +remains auditable rather than pretending that every cell came from one commit. +Resume and cross-revision reuse follow the sealed `current.json` and result +records; they do not depend on retaining a compiler or external-program build +workspace. + +With dashboard bindings installed, another terminal can take a read-only +snapshot of the newest live campaign: + +```console +./steer_performance_campaign.py \ + dashboard-snapshot --live --width 160 --height 48 +``` + +This reads a small status record; it cannot pause, resume, or stop the campaign. +Use `--instance ID_OR_PREFIX` when several campaign invocations are active. +Without the optional dashboard bindings, this subcommand explains how to +install them rather than attaching to a wheel-owned headless run. + +### Replay failures or policy caps + +After normal completion or an orderly interrupt, `campaign_summary_ids/` +contains one sorted text file per non-success status. Typical names include +`error.txt`, `blocked_dependency.txt`, `validation_failed.txt`, +`worker_timeout.txt`, `interrupted.txt`, and `unverified.txt`. Replay exact +cells with: + +```console +./steer_performance_campaign.py run \ + --cell-id-file campaign_summary_ids/error.txt \ + campaign_summary_ids/validation_failed.txt \ + --force-refresh +``` + +Replaying a blocked cell includes its required prerequisites. An unverified +timing result is not a successful current; replay `unverified.txt` without +`--force-refresh` once its authority is available. + +Alternatively, keep ordinary table/process selectors and filter them by their +latest state: + +```console +./steer_performance_campaign.py run \ + --table matrix \ + --generation-engine recurrence compiled \ + --rerun-failed --rerun-capped +``` + +`--rerun-failed` selects unsuccessful outcomes that are not authenticated +policy caps. `--rerun-capped` selects resource-policy caps. Together they take +the union. Successful, static, and never-attempted cells are not direct retry +targets. These retry flags cannot be combined with `--force-refresh`; a retry +selection with no matches exits successfully and leaves the existing summary +untouched. + +## What is retained as evidence + +The controller owns `campaign_artifacts/`. Here “artifact” means retained +campaign evidence, not only a generated pyAmpliCol process output. A cell +normally retains a tiny pointer to its current result and one or more uniquely +named attempt directories. Depending on the cell, an attempt can contain: + +- a manifest (an inventory identifying the completed attempt); +- `result.json` and `worker-result.json`; +- a bounded `worker.log`, streamed progress events, and a phase timeline; +- resource measurements and termination diagnostics; and +- when retained, an `artifact/` payload containing the generated process output + or an external-reference workspace used for the measurement. + +Prepared models, reusable model data (“model caches”), live status data, and a +temporary directory used while building the PDF (“publication staging”) also +stay below the campaign directory. Treat their exact subdirectory names as +controller internals; use `inspect`, the summary ID files, and the current +result JSON rather than editing pointers by hand. + +The default is **compact retention**. The controller keeps the result, +provenance, commands, progress and phase records, resource diagnostics, and +bounded final log tails. It keeps a generated pyAmpliCol process output when a +current result still needs it. For original AmpliCol it keeps the small +selected-flow library needed for replay and self-contained structural evidence, +but removes the disposable checkout/build workspace; disposable MadGraph and +other build workspaces are likewise removed after their measurements are +sealed. Heavy payloads from obsolete, failed, or cancelled attempts are also +removed. The old `--cleanup-artifacts` spelling is still accepted, but now +names this default policy rather than enabling a more aggressive one. + +When diagnosing an external generator, opt in to complete workspaces for that +run with: + +```console +./steer_performance_campaign.py run \ + --retain-workspaces \ + --workers 1 --table matrix --process-id 1 --multiplicity 1 \ + --color-approximation lc --generation-mode non-union-flow \ + --generation-engine recurrence --model built_in \ + --no-dependencies-added --no-dashboard +``` + +`--retain-workspaces` can consume disk quickly and is intended for a bounded +debugging session. It does not disable the attempt-output or minimum-free-disk +guards. Without it, a failed or fail-fast-cancelled attempt still retains the +compact diagnostics needed to understand and replay the cell, but not an +unbounded partial workspace. + +Temporary PDF-building directories and “live” markers are not historical +evidence. They are removed when their operation ends, whether it succeeds or +fails. Do not prune the campaign with broad shell cleanup commands. + +### Reset or branch a campaign safely + +Use another `profiling-campaign copy` for an independent machine, compiler, or +experimental branch. To reset one existing destination intentionally: + +```console +pyamplicol profiling-campaign copy ./my-profiling-campaign --force +``` + +Stop its active controller first. `--force` overwrites the managed template +files and resets only that destination's `campaign_artifacts/`, generated PDF, +summary-ID directory, measurement lineage, and known LaTeX byproducts. It +preserves unrelated files. It also preserves saved AmpliCol and MadGraph paths +unless new `--local-amplicol` or `--local-madgraph` values are supplied. + +This narrow reset is the supported replacement for manually deleting a broad +temporary directory or guessing which attempt files are safe to remove. + +## Turn current results into the report + +When the desired cells have finished, publish one internally consistent report +snapshot: + +```console +./steer_performance_campaign.py refresh-pdf +``` + +The controller first takes a stable snapshot—a read-only list of the complete +current results that belong in this version of the report. It verifies their +recorded origin and validation links, shows coloured progress, renders the JSON +and table TeX in a fresh temporary directory, and runs LaTeX there. Only after +all of that succeeds does it replace the published files and `pyAmpliCol.pdf` +as one unit. A failed render therefore leaves the previous PDF in place. + +Unresolved LaTeX references and compilation errors are fatal. Overfull boxes +are reported by interactive `refresh-pdf` so they can be reviewed. The final +maintainer audit is stricter: it authenticates the current results and physics +links, rebuilds the PDF, extracts its text, renders every page to an image, and +compares both text and pixels. Thus `refresh-pdf` is the normal campaign-owner +command; the release audit is the publication boundary rather than a second +measurement campaign. + +Useful report-only controls are: + +```console +./steer_performance_campaign.py refresh-pdf --list-sections +./steer_performance_campaign.py refresh-pdf \ + --remove-sections worked-zgg shared-current-dag +./steer_performance_campaign.py refresh-pdf --quiet +``` + +Removing sections affects only that staged PDF. It does not remove source, +measurements, result JSON, or table TeX; the next plain `refresh-pdf` restores +the complete report. + +## Find the matrices and their evidence + +Open `pyAmpliCol.pdf` and go to **Standard-Model Process Matrices**. The +“best measured mode versus AmpliCol” tables provide the compact overview. For +each process, multiplicity, and LC workload, the renderer independently chooses +the validated pyAmpliCol mode with the smallest measured wall time. It does not +declare one universally best mode. + +The generated TeX files beside the PDF are useful when a cell is too dense on +screen. For example, the LC overview is +`result_matrix_best_builtin_sm_lc_table.tex`. The corresponding detailed raw +tables are rendered from JSON caches below `results/`, such as +`matrix_compiled_builtin_sm_lc.json` and `reference_amplicol_lc.json`. + +For a human-readable view of current coverage and status, including OTF, use: + +```console +./steer_performance_campaign.py inspect \ + --color-approximation lc \ + --generation-engine recurrence compiled eager on-the-fly +``` + +Add `--json` for stable, uncoloured output suitable for scripts. Exact +cell IDs in `inspect`, the current JSON, and `campaign_summary_ids/` connect a +compact table status back to its retained attempt evidence. The PDF does not +turn every timing into a clickable evidence link; the result record stores the +source/settings (“provenance”), shared point, and validation-reference link. + +### Wall, evaluator, and recurrence clocks + +The report keeps several clocks separate instead of renaming whichever number +is available: + +- **Runtime** is the broad physics task of evaluating already generated output + at phase-space points. The **evaluator** is the loaded numerical calculator + that performs it. +- **Wall time** is elapsed real time observed around the complete timed call. + It is available across implementations and is therefore the primary number + used to choose the fastest validated mode. +- **Evaluator total** is a separately recorded accumulated clock inside the + complete warmed evaluator. Comparing it with wall time helps reveal time + spent in wrappers or in choosing and calling the numerical routine; it is + not copied from wall time. +- **Recurrence core** is the still narrower time spent replaying recurrence + schedules. It is useful for diagnosing recurrence itself, but it is not a + complete evaluator clock and is never relabelled or used as another mode's + denominator. + +The muted bracketed `xS` in a Best-vs-AmpliCol runtime cell first compares +compatible internal clocks that each implementation explicitly assigns to +numerical execution, excluding its documented surrounding work. If those are +unavailable, the table may use its explicitly documented evaluator-total +versus legacy-direct fallback. Either way `xS` is diagnostic. The coloured +outer `xW` is always the wall-time ratio and remains the performance +conclusion. + +`JIT` in row labels means “just-in-time”: numerical machine code is prepared +automatically for the current environment. `O2` and `O3` name compiler +optimization levels; they do not denote perturbative orders. + +### The Z-gluon ladder also includes OTF + +The built-in-SM and UFO-SM Z tables follow + +```text +d d~ -> Z + (n-1)g +``` + +through increasing final-state multiplicity. For each `n`, their setup rows now +include compiled variants, eager, recurrence, **and on-the-fly JIT O2**, beside +the original-AmpliCol reference. The two column groups remain the same physical +workloads used throughout the report: selected flow with a helicity sum, and +all flows at one helicity. + +An OTF generation cell in these detailed Z tables is printed as: + +```text +[G] G+W +``` + +Both numbers are absolute seconds. `G` is generation of the compact process +recipe; `G+W` adds the first cold evaluation of the complete campaign batch for +that workload. The muted brackets separate generation alone without hiding the +construction cost moved to first use. This `W` is not the public +single-point `Runtime.warm_up(...)` call: the campaign deliberately measures +its complete profiling batch. The adjacent `wall` column is the ordinary warm +wall time per point. The `eval` column is shown only when the mode supplied a +separately authenticated warmed evaluator-total clock; `not exposed` does not +mean that wall timing failed. + +## Read one real “Best vs AmpliCol” cell + +The following is the process-ID 1, `n = 2` cell from the current authoritative +campaign rendered for this release. It is a worked notation example, **not** a +performance promise for another machine, compiler, source revision, or model +preparation: + +```text +d d~ -> Z + (n-1)g = d d~ -> Z g +``` + +The LC table contains two aligned workloads in the fixed order +**selected flow, helicity sum | all flows, one helicity**: + +| Table row | Original AmpliCol | Best validated pyAmpliCol mode | +| --- | --- | --- | +| generation `[s]` | `3.66 | 0.000217` | `(o) ([x1.52] x1.52) | [5.57] 5.57 (o)` | +| runtime `[microseconds/point]` | `0.209 | 0.172` | `([x2.46] x2.51) | ([x0.888] x1.04)` | + +Here is every visible entry: + +- `3.66` seconds is original AmpliCol's selected-flow process-library + generation time. +- `0.000217` seconds is original AmpliCol's direct all-flow setup time. This is + a different setup boundary from pyAmpliCol process generation. +- `(o)` says that **on-the-fly JIT O2** had the smallest validated wall time + for that workload. The code is a label, not a ratio. The four possible codes + are `(r)`, `(c)`, `(e)`, and `(o)` for recurrence JIT O2, compiled JIT O3, + eager-DAG JIT O2, and on-the-fly JIT O2. +- In `([x1.52] x1.52)`, the muted bracketed multiplier uses compact OTF + process-output generation `G` alone. The coloured outer multiplier uses + `G+W`, where `W` is construction and first evaluation of the selected family + on the complete campaign batch. Both are divided by the matching 3.66 s + AmpliCol library-generation time. They round to the same value here because + the stored `G` was about 5.565 s and `W` about 0.00362 s. +- `[5.57] 5.57 (o)` is the all-flow OTF generation pair in absolute seconds: + muted `[G]` followed by `G+W`. It is intentionally not divided by + `0.000217`, because AmpliCol direct setup and pyAmpliCol process generation + are different operations. +- `0.209 | 0.172` are original AmpliCol wall times in microseconds per point + for the selected-flow and all-flow workloads, respectively. +- In `([x2.46] x2.51)`, the muted bracketed value compares the OTF + execution-attribution clock with AmpliCol's compatible direct-execution + clock. The coloured outer value independently compares wall time: about + 0.526 microseconds per point divided by 0.209. The outer ratio is the primary + figure used to choose a winner; generation time is not. +- Likewise, `([x0.888] x1.04)` contains the supplementary execution ratio and + the primary wall ratio for all flows at one helicity. The OTF wall time was + about 0.178 microseconds per point, versus about 0.172 for AmpliCol. + +Muted supplementary ratios help diagnose where time is spent, but they never +replace the outer wall ratio. In OTF generation notation, `W` excludes process +output loading and the conventional benchmark warm-up calls. It is also not +the public `Runtime.warm_up(...)` contract, which accepts exactly one +double-precision point for one selected family; the campaign times the first +complete batch required by its profiling protocol. + +## Colours, `N/A`, caps, and failures + +For a candidate/baseline multiplier `x`: + +- green means `x < 1`: the candidate was faster; +- orange means `1 <= x < 2`; +- red means `x >= 2`. + +The colours describe timing ratios, not physics accuracy. Accuracy is a +separate acceptance gate. An orange resource-cap token such as `>5h` or +`>40GB` means a configured policy boundary was reached; a red failure or +validation token means the measurement was not accepted. Muted `N/A` denotes +an unavailable entry, while `not applicable` means the process family itself +is not defined at that multiplicity. `not run` means it exists but was outside +the measured scope. `static N/A` marks a catalogue limitation known without +launching a worker, such as an original-AmpliCol surface outside its supported +open-quark-line scope. + +A compact token such as `N out.[5d35ae]` is a shortened final status, not a +number or speed ratio. Use `inspect` or the relevant file under +`campaign_summary_ids/` to recover the exact cell ID and outcome. `not exposed` +has a different meaning: the wall measurement succeeded, but that mode did not +provide the separately recorded internal clock requested by the detailed +table. It is not a missing or failed wall measurement. + +Unverified, capped, unsupported, and validation-failed candidates do not enter +“best” selection or performance summaries. A successful but unlinked candidate +can still be selected by its absolute wall time; it is displayed absolutely and +is excluded from timing ratios, mode-mix counts, and ratio summaries. + +## Read the summary rows + +Below each multiplicity block, the **mode mix** counts selected-flow wall-time +winners that also have the valid AmpliCol link needed for the generation +summary. The current authoritative `n = 2` LC block shows: + +```text +r:0 | c:2 | e:0 | o:4 +``` + +Among the six eligible process families, no winner was recurrence, two were +compiled, none was eager, and four were OTF. A zero is meaningful—it says that +the mode was measured but did not win this particular group. The order is +always `r | c | e | o`; it matches the four mode labels in the cells. + +Every five-number summary is ordered as: + +```text +minimum | maximum | median | arithmetic mean | weighted mean +``` + +The weighted mean is framed in the PDF. It is the ratio of summed candidate +time to summed baseline time, not the average of the individual ratios: + +```text +weighted mean = sum(candidate times) / sum(baseline times) +``` + +For `n = 2`, the same rendered campaign snapshot displays: + +| Summary row | `min | max | median | mean | weighted` | +| --- | --- | +| generation, selected-flow workload | `x0.801 | x1.52 | x1.34 | x1.24 | x1.23` | +| runtime, selected flow and helicity sum | `x2.00 | x4.12 | x2.46 | x2.89 | x2.65` | +| runtime, all flows and one helicity | `x0.754 | x1.43 | x1.00 | x1.05 | x1.05` | + +The generation summary uses only the comparable selected-flow **primary** +generation ratios; for an OTF winner this is `G+W`, not the muted `G`-only +ratio. Absolute all-flow generation is omitted. Runtime summaries use only the +primary wall ratios, not the muted supplementary ratios, and keep the two LC +workloads on separate lines. Structurally inapplicable, resource-limited, +unsupported, unverified, unlinked, and validation-failed cells are excluded. +This Best-vs summary follows the selected winner's primary token; a fixed-engine +OTF table instead keeps its generation summary as the diagnostic `G`-only +quantity while displaying both `G` and `G+W` in each row. + +The minimum answers “what was the best individual ratio?”, the maximum the +worst, the median the middle process, and the arithmetic mean treats every +included process equally. The framed weighted mean gives processes with larger +baseline times more influence, because it asks what would happen if the +included candidate times and baseline times were each added together. + +## A campaign-owner checklist + +1. Copy a fresh workspace for each machine or measurement environment. +2. Record external AmpliCol or MadGraph paths only when those comparisons are + required. +3. Preview a narrow selector with `run --dry-run`. +4. Confirm worker count, per-worker cores, wall-time limit, per-worker RAM cap, + any aggregate campaign RAM cap, attempt-output cap, and free-disk reserve + before broadening the scan. +5. Stop with `Ctrl-C` or dashboard `Esc`; resume by repeating the command. +6. Use `inspect` and `campaign_summary_ids/` to understand non-successes before + forcing a refresh. +7. Use compact retention by default. Add `--retain-workspaces` only for a + deliberate, disk-bounded debugging run. +8. Run `refresh-pdf` only after the desired current cells are in place. +9. Read ratios together with workload, mode code, validation status, and the + machine/source identity; no timing number is universal. + +For the exact controller contract and all current options, see the packaged +[campaign template README](https://github.com/MadGraphTeam/MadGraph7/blob/main/pyamplicol/src/pyamplicol/_profiling_campaign/README.md) +and [table-filling reference](https://github.com/MadGraphTeam/MadGraph7/blob/main/pyamplicol/src/pyamplicol/_profiling_campaign/TABLE_FILLING.md). +For the physical difference between the two LC workloads and the four +pyAmpliCol execution modes, continue with +[LC workloads and execution modes](lc-workloads-and-execution-modes.md). The +ordinary one-output timing workflow remains documented in +[Profiling and Benchmarking](profiling-and-benchmarking.md#what-is-timed). diff --git a/pyamplicol/docs/user/profiling-campaigns.md b/pyamplicol/docs/user/profiling-campaigns.md new file mode 100644 index 0000000000..6cdd69e5af --- /dev/null +++ b/pyamplicol/docs/user/profiling-campaigns.md @@ -0,0 +1,367 @@ +--- +title: "Profiling Campaigns" +nav_order: 1 +parent: "Profiling and Benchmarking" +--- + +# Profiling Campaigns + +The profiling-campaign command creates a complete, movable measurement +workspace from an installed pyAmpliCol wheel. It is intended for controlled +generation/runtime comparisons and for reproducing the report format used by +the published MacBook M3 and AMD EPYC PDFs. + +For a physicist-oriented end-to-end explanation of steering, interruption, +retained evidence, PDF rendering, and every entry in a concrete “Best vs +AmpliCol” matrix cell, follow +[Profiling campaign: from measurements to the PDF](profiling-campaign-walkthrough.md). + +> A campaign can create very large artifacts and long-running workers. Start +> with the single-cell smoke below. Broader multiplicities belong on a +> dedicated measurement host. + +## Create a fresh campaign + +```console +pyamplicol profiling-campaign copy ./pyamplicol-profiling-campaign --force +cd ./pyamplicol-profiling-campaign +./steer_performance_campaign.py --help +``` + +The copy is self-contained apart from the installed pyAmpliCol runtime. It +contains the controller, report sources, blank/reset data, generated-table +templates, and one visible state root: + +```text +pyamplicol-profiling-campaign/ + steer_performance_campaign.py + README.md + TABLE_FILLING.md + report-workspace.json + campaign_artifacts/ + cells/ + coordination/ + manual-reproductions/ + report-snapshot-builds/ + results/ + result_*.tex + pyAmpliCol.tex +``` + +All attempts, prepared artifacts, current pointers, logs, locks, leases, and +reproduction state live below `campaign_artifacts/`. The controller does not +consult an old checkout-level `.artifacts` directory. + +## A deliberately small real run + +This command measures only the final-state-multiplicity-one `d d~ > Z` +recurrence cell: + +```console +./steer_performance_campaign.py run \ + --workers 1 \ + --table matrix \ + --process-id 1 \ + --multiplicity 1 \ + --color-approximation lc \ + --generation-mode non-union-flow \ + --generation-engine recurrence \ + --model built_in +``` + +It is the recommended installed-wheel campaign smoke. Release wheels omit the +optional Ratatui bindings, so the controller continues headlessly. A +contributor checkout prepared with `just dev-install` can show the interactive +dashboard. + +Preview a broader selection without launching workers: + +```console +./steer_performance_campaign.py run \ + --dry-run \ + --table z_table \ + --multiplicity 3 4 \ + --model built_in sm_ufo +``` + +The dry run separates directly selected cells from dependency work and prints +a public-CLI reproduction recipe for each runnable cell. + +## Selection and numerical-authority closure + +The campaign catalog has stable canonical cell IDs. Ordinary selectors choose +direct work by table, process family, multiplicity, color approximation, +generation mode, engine, model, and variant. + +By default, the controller recursively adds each selected cell's available +active-source numerical-authority chain: + +```text +original AmpliCol → recurrence → compiled / eager +``` + +Added cells are dependency-only work. Independent process/multiplicity cohorts +remain parallel. Hard construction and selector/provider dependencies always +block until available. A terminal optional authority releases its candidate: +the candidate can still run, but a compiled/eager result without independent +authority is retained as an explicit unverified timing diagnostic rather than +published as a successful current. + +Use `--no-dependencies-added` only when you intentionally want to suppress +this optional numerical-authority expansion. It never removes hard +dependencies. + +## Original AmpliCol is optional + +All pyAmpliCol backends work without an original-AmpliCol checkout. The legacy +backend is enabled only when selected as a reference. + +The supported input is a **clean, complete checkout** exposing the profiling +probe sources and Make targets from +[rikkert-frederix/AmpliCol PR #12](https://github.com/rikkert-frederix/AmpliCol/pull/12). +It is not a single Fortran file and is not bundled in pyAmpliCol. + +Record a destination-local default while copying: + +```console +pyamplicol profiling-campaign copy ./campaign \ + --local-amplicol /path/to/clean/complete/AmpliCol-checkout +``` + +Or provide/override it for one run: + +```console +./campaign/steer_performance_campaign.py run \ + --original-amplicol /path/to/clean/complete/AmpliCol-checkout \ + --generation-engine amplicol recurrence \ + --table matrix --process-id 1 --multiplicity 1 \ + --color-approximation lc --model built_in +``` + +The copy stores the chosen path in `.pyamplicol-original-amplicol`. A later +`--force` reset preserves that pointer unless `--local-amplicol` explicitly +replaces it. Without a configured checkout, a selection that does not include +`amplicol` remains fully supported. + +## Reuse and source revisions + +Successful compatible currents are reused by default. The campaign's stored +attempts are not erased merely because pyAmpliCol's source revision changes. + +Without `--continue-across-revisions`, planning and report operations use the +active source cohort. With it, valid historical currents may remain in the +campaign while new cells and dependencies are measured at the active revision: + +```console +./steer_performance_campaign.py run \ + --continue-across-revisions \ + --workers 4 \ + --table matrix \ + --multiplicity 4 5 \ + --generation-engine recurrence compiled eager \ + --model built_in +``` + +This option reconciles compatible prior results; it does not reconstruct a +deleted campaign. Historical numerical authorities needed by current-revision +work are replanned at the active revision rather than silently reused across an +unsafe authority boundary. + +The operations that actually reset managed state are +`pyamplicol profiling-campaign copy DEST --force` and deliberate manual +deletion. Stop every active controller before using `--force`. + +## Move or rename a campaign + +Move the complete directory, not selected result files: + +```console +mv ./campaign-mac ./archive/macbook-M3-2026 +cd ./archive/macbook-M3-2026 +./steer_performance_campaign.py inspect +``` + +Portable current locators are interpreted relative to the campaign's new +`campaign_artifacts/` root. Two campaign directories with the same basename in +different parents do not share state. + +If a directory was renamed while a shell remained inside its old inode, start +a new shell or `cd` through the new absolute path before launching the copied +controller. The launcher checks logical and physical working directories and +fails instead of reading an unintended campaign. + +## Dashboard and progress + +Contributor environments with Ratatui bindings display active worker lanes, +direct/dependency work, completed/capped/error/unverified partitions, elapsed +time, and compact terminal reasons. + +Capture a read-only view from another terminal: + +```console +./steer_performance_campaign.py dashboard-snapshot --live \ + --width 160 --height 48 +``` + +The live snapshot reads only compact coordination leases. Use +`--instance ID_OR_PREFIX` when more than one controller is active. Omit +`--live` for the deterministic layout fixture. + +`Ctrl-C` or `Esc` stops dispatch, terminates supervised process trees, +preserves completed currents, writes the summary-ID files, and restores the +terminal. + +## Retry exactly what needs attention + +Every completed or orderly interrupted run replaces +`campaign_summary_ids/` with one sorted text file per non-success status, for +example: + +```text +campaign_summary_ids/ + blocked_dependency.txt + error.txt + unverified.txt + validation_failed.txt + worker_timeout.txt +``` + +Replay exact IDs: + +```console +./steer_performance_campaign.py run \ + --cell-id-file campaign_summary_ids/error.txt \ + campaign_summary_ids/validation_failed.txt \ + --force-refresh +``` + +Or filter an ordinary selection by structured state: + +```console +./steer_performance_campaign.py run \ + --table z_table \ + --generation-engine recurrence compiled \ + --rerun-failed --rerun-capped \ + --continue-across-revisions +``` + +- `--rerun-failed` selects latest terminal non-successes that are not + authenticated policy caps; unverified diagnostics are retryable here. +- `--rerun-capped` selects authenticated memory/time/generation policy caps. +- Combining them takes the union. +- Successful, structural-static, and never-attempted cells are not direct + retry targets. +- Normal hard and optional dependency closure still applies. +- The selective flags conflict with `--force-refresh`. +- No matches is a successful no-op and preserves the previous summary IDs. + +Replaying `blocked_dependency.txt` includes its prerequisites automatically. +An `unverified.txt` replay does not require `--force-refresh`: unverified is +not an authoritative successful current. + +## Attempt retention and cleanup + +Compact retention is the default. It keeps sealed results and current pointers, +provenance, commands, progress/timeline evidence, bounded log tails, and outputs +needed by currents or dependencies. Disposable copied AmpliCol, MadGraph, and +build workspaces are removed after publication. Cross-revision reuse reads the +sealed results rather than those workspaces. + +Pass `--retain-workspaces` only for a bounded debugging run that needs the full +workspaces: + +```console +./steer_performance_campaign.py run \ + --retain-workspaces \ + --table matrix --process-id 1 --multiplicity 1 \ + --generation-engine recurrence --model built_in +``` + +`--cleanup-artifacts` remains accepted as a compatibility spelling for the +default compact policy. Independently, `--attempt-output-limit` defaults to +67,108,864 bytes (64 MiB) per watched output/log file and +`--minimum-free-disk` reserves 5,368,709,120 bytes (5 GiB) on the artifact +volume. These storage guards complement, but do not replace, process-tree RAM +supervision. + +## Inspect and render + +Inspect coverage and AmpliCol-relative comparisons: + +```console +./steer_performance_campaign.py inspect \ + --color-approximation lc \ + --generation-engine recurrence compiled +``` + +Refresh all tables and atomically replace the PDF from one coherent current +snapshot: + +```console +./steer_performance_campaign.py refresh-pdf +``` + +Interactive refresh displays a colored progress bar while current records are +read and checked. It does not rescan every historical heavy artifact. + +List stable section IDs or omit sections from one PDF build: + +```console +./steer_performance_campaign.py refresh-pdf --list-sections +./steer_performance_campaign.py refresh-pdf \ + --remove-sections worked-zgg shared-current-dag +``` + +Section omission changes only the staged PDF. Measurements, JSON caches, table +TeX, and source remain; the next plain refresh restores the complete report. +`--quiet` suppresses scan progress and live LaTeX output. + +The repository retains only two rendered report snapshots: + +- [MacBook M3 report](https://github.com/MadGraphTeam/pyamplicol/blob/main/docs/performance_reports/macbook_M3_pyAmpliCol.pdf) +- [AMD EPYC report](https://github.com/MadGraphTeam/pyamplicol/blob/main/docs/performance_reports/EPYC_pyAmpliCol.pdf) + +Raw campaign results are intentionally not published in the repository. A +fresh copied campaign contains everything needed to reproduce the report +format directly. + +## What `--force` resets + +```console +pyamplicol profiling-campaign copy DEST --force +``` + +replaces only managed template files and resets: + +- `DEST/campaign_artifacts/`; +- the managed `pyAmpliCol.pdf`; +- `campaign_summary_ids/`; +- measurement lineage; +- known pyAmpliCol LaTeX build byproducts. + +It preserves unrelated destination files, a legacy `DEST/.artifacts` +directory (which is ignored), and the recorded original-AmpliCol pointer unless +explicitly replaced. An active campaign makes reset fail without removing +state. + +## Recommended operating habits + +1. Use one copied directory per host and campaign policy. +2. Run `--dry-run` before a broad selection. +3. Set worker and RAM limits for the actual host. +4. Use `--continue-across-revisions` when intentionally extending a campaign + across source updates. +5. Prefer selective retry flags over a broad force refresh. +6. Keep compact attempts by default; use `--retain-workspaces` only for a + bounded debugging run. +7. Never commit `campaign_artifacts/`, raw results, logs, locks, attempts, or + LaTeX auxiliary files. + +## Related pages + +- [Examples Gallery](examples-gallery.md) — ordinary generation/runtime examples. +- [Profiling campaign walkthrough](profiling-campaign-walkthrough.md) — the campaign idea, retained evidence, PDF lifecycle, and matrix interpretation. +- [Artifacts and Portability](artifacts-and-portability.md) — process-artifact portability and trust. +- [Troubleshooting](troubleshooting.md) — campaign state, source revision, and dashboard issues. +- [Packaged campaign guide](https://github.com/MadGraphTeam/pyamplicol/blob/main/src/pyamplicol/_profiling_campaign/TABLE_FILLING.md). +- [Published report index](../performance_reports/README.md). diff --git a/pyamplicol/docs/user/python-api.md b/pyamplicol/docs/user/python-api.md new file mode 100644 index 0000000000..aadbcd9c36 --- /dev/null +++ b/pyamplicol/docs/user/python-api.md @@ -0,0 +1,620 @@ +--- +title: "Python API" +nav_order: 4 +has_children: true +--- + +# Python API + +The public Python interface offers typed services for model compilation, +process planning/generation, runtime evaluation, Born correlations, selectors, +and profiling. +Importing `pyamplicol` is lightweight: Symbolica and model tooling are loaded +only when the requested operation needs them. + +```python +import pyamplicol + +print(pyamplicol.__version__) +``` + +## Public service map + +| Service | Purpose | +| --- | --- | +| `ModelSource` | Resolve and compile built-in, JSON, UFO, compiled, or prepared models. | +| `Generator` | Plan or generate schema-v3 process artifacts. | +| `Runtime` | Load one concrete process and evaluate totals/resolved components. | +| `CorrelatorConfig`, `ColorCorrelator` | Declare allowed spin replacements and named colour operators at generation. | +| `CorrelatedValue` | A correlated result with `Decimal` real and imaginary parts. | +| `BenchmarkRunner` | Profile an artifact or loaded runtime. | +| `group_processes` | Group the subprocesses of an artifact with equal matrix elements. | +| `generate`, `load`, `benchmark` | One-shot convenience functions. | + +The shipped scripts under +[`examples/python/`](https://github.com/MadGraphTeam/pyamplicol/tree/main/examples/python) +are complete runnable programs. + +## Plan and generate with the built-in model + +```python +from pyamplicol import GenerationConfig, Generator, ModelSource + +generator = Generator(GenerationConfig(workers=2)) +model = ModelSource.built_in_sm() + +plan = generator.plan("d d~ > z g", model=model) +for process in plan.concrete_processes: + print(process.name, process.expression) + +result = generator.generate( + "d d~ > z g", + "artifacts/builtin_ddbar_to_zg", + model=model, + mode="replace", +) +print(result.output) +``` + +`plan()` resolves the model/process/color/evaluator contract without writing a +process artifact. `generate()` returns an immutable `GenerationResult` with the +absolute output, stored process set, schema version, and file inventory. + +The packaged scalar HEFT model uses the same public selector: + +```python +from pyamplicol import Generator, ModelSource + +heft = ModelSource.built_in_sm_heft() +Generator().plan("g g > H g g", model=heft) +``` + +Use an explicit `HIG = 1` coupling-order limit in a resolved run configuration +when generating the process; see [Models and Processes](models-and-processes.md#built-in-scalar-heft-model) +and the packaged `builtin_sm_heft.toml` card. + +## Generate with adjoint FFT contraction + +The Python API uses the same `RunConfig` and `ColorConfig` fields as TOML and +the CLI. Defaults `contraction="auto"` and `fft_basis="adjoint"` try adaptive +exact FFT for NLC/full recurrence and on-the-fly execution, with direct fallback +for unsupported FFT plans. This certified pure Yang–Mills example uses DDM: + +```python +from pyamplicol import ColorConfig, Generator, ModelSource, RunConfig +from pyamplicol.config import EvaluatorConfig + +config = RunConfig( + action="generate", + color=ColorConfig(accuracy="full"), + evaluator=EvaluatorConfig(execution_mode="recurrence"), +) +result = Generator(config).generate( + "g g > g g g", + "artifacts/ggg_adjoint_fft", + model=ModelSource.built_in_sm(), +) +``` + +Select `accuracy="nlc"` for NLC contraction, or `execution_mode="on-the-fly"` +for a compact OTF artifact. The effective configuration can retain `"auto"` +until per-process selection. LC, compiled/eager execution, and `correlators=` +use direct contraction and the trace basis under the automatic policy. + +Use `ColorConfig(accuracy="full", contraction="direct")` to opt out of FFT; +direct contraction ignores `fft_basis`. To force FFT and retain errors for +unsupported plans, set `contraction="symmetric-group-fft"`, optionally with +`fft_basis="trace"` for the original representation. The string `"adjoint"` +is equivalent to `ColorFFTBasis.ADJOINT` (imported from `pyamplicol`). Explicit +FFT does not support compiled/eager modes or LC accuracy. + +For certified trees with `n` external gluons, DDM fixes two anchors and reduces +the trace set from `(n-1)!` orderings to `(n-2)!`, representing the same amplitude. +Certified single-insertion HEFT also uses DDM; specify explicit coupling orders +with `HIG = 1`, as in `examples/builtin_sm_heft.toml`. Quark processes keep exact +fundamental chains or their products, not a newly assumed JO primitive basis; +uncertified adjoint interactions retain trace tensors. The saved +`fft_basis_selection` reports `requested`, `actual_basis`, `name`, `reason`, +`permutation_degree`, and `tensor_count`, making that choice explicit. +Forced adjoint FFT cannot be combined with `correlators=`. Runtime totals, +resolved helicities and native SDK evaluation calls are unchanged. No universal +speedup is implied. +In the benchmarked pure-gluon family the adjoint basis was faster than trace +for six or more external gluons (3.9x per sample at ten gluons); benchmark +other multiplicities and selector workloads. + +## Generate a named process set + +```python +from pyamplicol import Generator, ModelSource, ProcessRequest, ProcessSet + +processes = ProcessSet( + requests=( + ProcessRequest.parse("u u~ > Z g", name="uubar_Zg"), + ProcessRequest.parse("u u~ > Z g g", name="uubar_Zgg"), + ) +) + +result = Generator().generate( + processes, + "artifacts/z_ladder", + model=ModelSource.built_in_sm(), + mode="replace", +) +``` + +Process names must be unique. For custom aliases and crossing metadata, use +`ProcessAlias`; runtime expression matching already handles unique +side-preserving permutations automatically. +The correlated reference path currently requires identity ordering; these +permutation facilities apply to ordinary evaluation. + +## Compile and reuse an external model + +```python +from pyamplicol import Generator, ModelSource + +source = ModelSource.from_path( + "models/json/sm/sm.json", + restriction="default", +) +model = source.compile() + +print(model.info.name) +print(model.capabilities.supported_color_accuracies) +print(model.supported) + +Generator().plan("d d~ > z g", model=model) +``` + +For a trusted UFO directory: + +```python +source = ModelSource.from_path( + "/path/to/MyUFO", + restriction="restrict_default.dat", +) +model = source.compile() +``` + +UFO modules execute Python while loading. See +[Models and Processes](models-and-processes.md) for trust, serialization, and +prepared bundles. + +## Use a fully resolved run card + +For complex configuration, resolve the same schema used by the CLI and pass the +result to `Generator`: + +```python +from pyamplicol import Generator, ModelSource, ProcessRequest, ProcessSet +from pyamplicol.config import resolve_config + +card = { + "schema_version": 1, + "action": "generate", + "model": {"source": "models/json/sm/sm.json", "restriction": "default"}, + "process": { + "entries": [{"expression": "p p > Z j j"}], + "multiparticles": {"p": ["d", "d~", "g"], "j": ["d", "d~", "g"]}, + "flavor_scheme": 2, + "max_quark_lines": 2, + }, + "color": {"accuracy": "lc"}, + "generation": {"output": "artifacts/pp_zjj", "mode": "replace"}, + "evaluator": {"execution_mode": "compiled", "backend": "jit"}, +} + +resolution = resolve_config(card) +model = ModelSource.from_config(resolution.effective.model).compile() +processes = ProcessSet( + tuple( + ProcessRequest.parse(entry.expression, name=entry.name) + for entry in resolution.effective.process.entries + ) +) +result = Generator(resolution).generate( + processes, + resolution.effective.generation.output, + model=model, + mode=resolution.effective.generation.mode, +) +``` + +The complete maintained implementation is +[`examples/python/typed_generation.py`](https://github.com/MadGraphTeam/pyamplicol/blob/main/examples/python/typed_generation.py). + +## Load a runtime + +```python +from pyamplicol import Runtime + +runtime = Runtime.load( + "artifacts/pp_zjj", + process="d d~ > g z g", + model_parameters={"aS": 0.117, "MZ": 91.1876}, +) + +print(runtime.artifact_id) +print(runtime.execution_mode) +print(runtime.physics.process_id) +print(runtime.physics.process) +print(runtime.representative_process_key) +print(runtime.external_permutation) +``` + +`process` may be a stable process ID, explicit alias ID, exact stored expression, +or unique permutation-equivalent expression within each side. The loaded +runtime exposes metadata in the requested public order. +For correlated evaluation, load the stored process in its generated ordering; +nonidentity aliases/permutations are not supported by that path yet. + +## Evaluate totals and resolved components + +```python +import json +import math +from pathlib import Path + +points = json.loads(Path("data/pp_zjj_momenta.json").read_text()) + +totals = runtime.evaluate(points) +resolved = runtime.evaluate_resolved(points) + +print(resolved.helicity_ids) +print(resolved.color_ids) +for optimized, explicit in zip(totals, resolved.total(), strict=True): + assert math.isclose(optimized.real, explicit.real, rel_tol=1e-12, abs_tol=1e-15) + assert math.isclose(optimized.imag, explicit.imag, rel_tol=1e-12, abs_tol=1e-15) +``` + +Input shape is: + +```text +(point, external particle, [E, px, py, pz]) +``` + +At LC, resolved shape is `(point, helicity, color_flow)`. At NLC/full, +the color dimension has length one because color is contracted. + +## Retain completed OTF preparation + +For an on-the-fly runtime, `runtime.save(path)` writes the completed cache +left by evaluation or explicit warm-up. In a later program, load the original +process output with `Runtime.load(...)`, then call `runtime.load_cache(path)` +before evaluating with the same selectors. Both methods return `None`; the +restored calculation accepts new momenta and batch sizes while retaining the +receiving runtime's model parameters. + +See the [complete four-gluon example](runtime-and-selectors.md#saving-an-otf-warm-cache) +for generation, saving and restoring, and a check that no queries are rebuilt. +The cache supplements the original process output; it does not replace it or +checkpoint an unfinished cold construction. + +## Evaluate Born correlations + +Correlations require an explicit `Generator.generate(..., correlators=...)` +declaration. They support **LC, NLC and full colour**, through a separate Python +direct, generic compiled exact executor. `color.accuracy` selects the correlated +approximation. Supplying `correlators` retains complete underlying amplitudes +and selects direct contraction and compiled generation. Ordinary generation +and evaluation remain unchanged. + +```python +from pyamplicol import ( + ColorCorrelator, CorrelatorConfig, Generator, ModelSource, Runtime, +) +from pyamplicol.config import ColorConfig, RunConfig + +declarations = CorrelatorConfig( + color_correlations=(ColorCorrelator.dipole("T13", 1, 3),), + spin_correlations=((3,), (3, 4)), +) +generated = Generator(RunConfig(color=ColorConfig(accuracy="full"))).generate( + "g g > g g", "artifacts/gg_correlated", + model=ModelSource.built_in_sm(), correlators=declarations, +) +correlated_runtime = Runtime.load(generated.output) +point = ( + (500, 0, 0, 500), (500, 0, 0, -500), + (500, 300, 0, 400), (500, -300, 0, -400), +) +colour = correlated_runtime.evaluate_correlated( + (point,), color_correlation="T13", precision=40, +)[0] +correlated_runtime.set_spin_correlation_vectors({3: (0, 0, 1, 0)}) +spin_colour = correlated_runtime.evaluate_correlated( + (point,), color_correlation="T13", precision=40, +)[0] +print(spin_colour.real, spin_colour.imag) # Decimal values, not a forced real part. +correlated_runtime.set_spin_correlation_vectors(None) +``` + +Instead of explicit colour requests, use +`CorrelatorConfig.all_color(through_order=k, spin_correlations=...)` before +generation to prepare every compatible directed pair of tree-soft connections +through any positive order `k`. The nonminimal catalogue includes emissions from +emitted partons and splitting of auxiliary gluons, and grows rapidly with order. +It adds no kinematic kernels, flavour sums or multiplicity weights. Inspect the +operators actually stored for a selected process with: + +```python +for entry in correlated_runtime.available_color_correlations(): + print(entry.id, entry.order, entry.bra, entry.ket) +``` + +This includes `"born"` and reads the catalogue without initializing amplitude +evaluation. See the [automatic catalogue guide](../correlators.md) +for generation and grouped evaluation. + +`evaluate_correlated(momenta, *, color_correlation="born", helicities=None, +precision=16, arithmetic="arbitrary")` returns one `CorrelatedValue` per point. The reserved ID +`"born"` uses the Born metric at the requested colour accuracy; other IDs are the declared +operators. Optional batch-global helicity selectors accept stable IDs or typed +`HelicityConfiguration` objects. There is no colour-flow or per-point selector +argument on this method. Normalization, initial-state averages, and +identical-particle factors are retained, and runtime model-parameter updates +also apply to correlations. + +For multiple combinations, `evaluate_correlated_many(points, requests)` takes +a mapping from your result labels to `CorrelatedRequest(color_correlation=..., +spin_vectors=...)`. Its result is a dictionary of tuples: +`result[request_label][point_index].real` or `.imag` selects one contraction +at one phase-space point. Label and point order are preserved. Identical spin +assignments share their amplitudes across colour operators, and unchanged +stages can be reused across different assignments. See the +[two-axis example](../correlators.md#several-requests-and-several-phase-space-points) +for request-centric and point-centric access. Ordinary `evaluate()` on the +underlying correlated output remains full-colour; use the correlated `"born"` +request when comparing its selected LC/NLC approximation. + +The spin-vector mapping uses one-based public leg labels. Each value is a +literal real or complex `(v0, vx, vy, vz)` vector, broadcast across the batch, +or one such vector per point. The nonempty key set must exactly match a +declared spin class: this example permits `{3}` and `{3, 4}`, but not `{4}`. +Vectors are neither normalized nor projected. They are copied into +instance-local state; a failed setter preserves the previous state. `None` or +`{}` resets to ordinary helicity sources; `clear()` releases warmed evaluators +without resetting vectors. The setter affects `evaluate_correlated` and +inherited requests in `evaluate_correlated_many`, not `evaluate` or +`evaluate_resolved`. + +`Decimal` vector components retain their digits; a `(Decimal(real), +Decimal(imaginary))` pair represents one complex component without first +rounding it through Python's binary64 `complex`. For example: + +```python +from decimal import Decimal + +zero = Decimal(0) +correlated_runtime.set_spin_correlation_vectors({3: ( + zero, zero, + (Decimal("1.0000000000000000000000001"), Decimal("2e-25")), + zero, +)}) +arb = correlated_runtime.evaluate_correlated((point,), precision=80) +dd = correlated_runtime.evaluate_correlated( + (point,), arithmetic="double-double", precision=31, +) +correlated_runtime.set_spin_correlation_vectors(None) +``` + +The default is arbitrary precision. Explicit `arithmetic="double-double"` +uses genuine DoubleFloat arithmetic throughout sources, evaluation stages, +normalization and reduction; at most 31 result digits are supported. The same +keyword and Decimal vector syntax apply to `evaluate_correlated_many` and its +explicit request vectors. See the [grouped precision example](../correlators.md#decimal-inputs-and-double-double-arithmetic). + +Artifacts without declarations, append generation, partial source coverage, +FFT, eager/recurrence/OTF correlated execution, replay reductions, and +nonidentity process permutations are not supported. Ordered `EmitGluon` and +`SplitGluon` operations have no fixed order cap; generic NkLO colour-connection +order is distinct from LC/NLC colour approximations and does not supply a +complete higher-order subtraction calculation. See [Born Correlations](../correlators.md) +for the bra-adjoint/ket convention, joint spin contractions, JSON declarations, +Ward checks, and current limitations. + +The same generated colour catalogue and spin classes are available to the +[C/C++/Fortran/Rust SDKs](native-apis.md#correlated-born-evaluations), whose +correlated evaluation is binary64 and independent of Python/Symbolica. +The Python methods described here retain their precision-controlled executor. + +## Select helicities and color flows + +Batch-global selectors accept stable IDs or the typed objects exposed by +`runtime.physics`: + +```python +selected = runtime.evaluate( + points, + helicities=[runtime.physics.helicities[0]], + color_flows=[runtime.physics.color_flows[0]], +) +``` + +For one selector per point: + +```python +mixed = runtime.evaluate( + points, + helicity_by_point=[ + runtime.physics.helicity_ids[index % 2] + for index in range(len(points)) + ], + color_flow_by_point=[ + runtime.physics.color_flow_ids[(index // 2) % 2] + for index in range(len(points)) + ], +) +``` + +Batch-global and per-point selectors are mutually exclusive on the same axis. +Per-point color selection is LC-only. Rusticol groups mixed selectors while +returning values in original point order. + +See [Runtime and Selectors](runtime-and-selectors.md) for the complete selector +contract. + +## Update model parameters + +```python +runtime.set_model_parameters({"aS": 0.1165, "MZ": 91.1876}) +runtime.set_model_parameter("MT", 172.5) +``` + +Updates are atomic and refresh dependent derived parameters. Unknown, +immutable, non-finite, or invalid values reject the entire update. + +`Runtime.load(..., model_parameters=...)` applies a complete mapping before the +runtime is returned. Complex inputs may be represented as Python complex values. + +## Group equivalent subprocesses + +```python +from pyamplicol.api import group_processes + +summary = group_processes("artifacts/pp_jj") +for relation in summary["new_relations"]: + print(relation["process"], "=", relation["factor"], "x", relation["representative"]) +``` + +`group_processes` evaluates each group representative stored in +`process-groups.json` in f64 at three on-shell points, each with its own +deterministic variation of up to 10% of the external and normalization model +parameters other than masses and widths. Two subprocesses with the same +external particle types are grouped when, through a leg permutation (and +charge conjugation, unless `consider_charge_conjugation=False`), every +resolved helicity and colour +component of one is a common rational factor times the other's; numerator and +denominator are at most 99 and `only_unit_multiplier=True` accepts only 1. +Subprocesses generated with a selected helicity or colour subset are never +grouped. The map is updated in place and rerunning adds nothing; the artifact +ID and UMAMI files are unchanged. `analysis="none"` only writes the trivial map +of an artifact that has none. This is the `pyamplicol group-processes` +command; see [Process Grouping](process-grouping.md) for the details. + +## Precision + +```python +f64 = runtime.evaluate(points, precision=16) +high_precision = runtime.evaluate(points, precision=80) +``` + +For ordinary `evaluate` and `evaluate_resolved`, precision 16 uses the native +Rusticol runtime without Symbolica computations. Package startup imports +Symbolica to attempt registration of the pyAmpliCol library key; synchronous +registration failures retain the personal-license and restricted-mode paths. +Other positive precision requests load retained exact evaluator state when +the artifact supports it. +Decimal input preserves the supplied decimal digits; +binary64 input cannot gain information merely by requesting more arithmetic +precision. + +`evaluate_correlated` always uses retained Symbolica evaluator states through +the separate Python exact executor, including at its default precision 16. +Its default `arithmetic="arbitrary"` is distinct from the explicit +`arithmetic="double-double", precision=31` mode; neither is selected by an +ordinary native SDK precision flag. +Its real and imaginary results remain `Decimal` values; `complex(value)` is an +explicit conversion to ordinary double precision. + +Generated C/C++/Fortran/Rust standalone APIs support f64 (`precision=16`) only. + +## Profile from Python + +```python +from pyamplicol import BenchmarkConfig, BenchmarkRunner + +config = BenchmarkConfig( + target_runtime=1.0, + batch_size=128, + precision=16, + warmup_runs=2, + minimum_samples=5, + color_flow_ids=("1",), +) + +result = BenchmarkRunner(config).run(runtime, points=points) +print(result.wall_time_per_point) +print(result.evaluator_time_per_point) +print(result.uncertainty.standard_error) +print(result.uncertainty.relative_standard_error) +``` + +`BenchmarkResult` also contains calibrated repetitions, sample count, +environment metadata, native timing breakdown, stage attribution, and work +counters when the runtime exposes them. + +The runnable reference is +[`examples/python/benchmark.py`](https://github.com/MadGraphTeam/pyamplicol/blob/main/examples/python/benchmark.py). + +## Convenience functions + +```python +from pyamplicol import benchmark, generate, load + +generated = generate( + "d d~ > z g", + "artifacts/builtin_ddbar_to_zg", + model=ModelSource.built_in_sm(), + mode="replace", +) +runtime = load(generated.output) +profile = benchmark(runtime, points=points) +``` + +The class-based services are preferable when reusing configuration, progress +sinks, compiled models, or loaded runtimes. +The one-shot `generate(...)` function also accepts the same +`correlators=CorrelatorConfig(...)` keyword as `Generator.generate(...)`. + +## Errors + +All public failures derive from `PyAmpliColError`: + +```python +from pyamplicol import EvaluationError, GenerationError, PyAmpliColError + +try: + runtime = Runtime.load("artifacts/pp_zjj", process="missing") +except EvaluationError as error: + print(f"could not load runtime: {error}") +``` + +Useful subclasses include configuration, model, generation, artifact, +compatibility, dependency, and evaluation errors. Catch the narrowest type you +can handle; catch `PyAmpliColError` at an application boundary. + +## API design notes + +- Public result and metadata objects are immutable dataclasses or typed facades. +- Artifact paths are normalized to absolute paths at service boundaries. +- `Generator.generate(..., mode="error")` refuses an existing destination. +- `Runtime` is bound to one selected process; load another instance for another + process in the same multiprocess artifact. +- Process artifacts are trusted executable inputs. Normal loading validates the + schema, path confinement, references, target compatibility, and runtime ABI. +- Use explicit payload validation only for an intentional whole-artifact + corruption audit; it is not required before every load. + +## Further reading + +- [Configuration](configuration.md) +- [Generation Modes and Evaluators](generation-modes-and-evaluators.md) +- [Runtime and Selectors](runtime-and-selectors.md) +- [Born Correlations](../correlators.md) +- [Correlation Conventions](../correlator-conventions.md) +- [Artifacts and Portability](artifacts-and-portability.md) +- [Native APIs](native-apis.md) + + +```{toctree} +:hidden: + +runtime-and-selectors +native-apis +artifacts-and-portability +umami-api +../correlators +../correlator-conventions +``` diff --git a/pyamplicol/docs/user/quick-start.md b/pyamplicol/docs/user/quick-start.md new file mode 100644 index 0000000000..4ffa5afc56 --- /dev/null +++ b/pyamplicol/docs/user/quick-start.md @@ -0,0 +1,213 @@ +--- +title: "Quick Start" +nav_order: 1 +parent: "Installation" +--- + +# Quick Start + +This tutorial follows the primary shipped example: generate a portable +leading-color `p p > Z j j` artifact, select the concrete process +`d d~ > g z g`, evaluate it, inspect its resolved components, and profile the +same runtime path. + +## 1. Install and copy the examples + +```console +python3 -m venv .venv +. .venv/bin/activate +python -m pip install pyamplicol + +pyamplicol examples copy ./pyamplicol-examples --force +cd pyamplicol-examples +``` + +The copied directory is self-contained: it includes run cards, model resources, +phase-space data, parameter cards, typed Python examples, and native driver +sources. + +List the available packaged examples at any time: + +```console +pyamplicol examples list +pyamplicol examples run --help +``` + +## 2. Generate `p p > Z j j` + +```console +pyamplicol generate_pp_zjj_from_ufo_sm.toml +``` + +The run card uses the packaged serialized UFO Standard Model, explicit +two-flavor `p` and `j` multiparticles, leading color, and process-local compiled +JIT O2 execution. The result is written to: + +```text +artifacts/pp_zjj/ +``` + +The inclusive request expands to ordered partonic candidates, merges candidates +that differ only by a permutation within the incoming or outgoing side, and +stores one representative per class. The loop-induced `g g > Z g g` candidate +has no supported tree-level amplitude and is reported rather than generated. + +> Generation can take appreciably longer than evaluation because it constructs, +> optimizes, validates, and serializes reusable evaluators. + +## 3. Inspect the artifact + +```console +pyamplicol inspect artifacts/pp_zjj +``` + +The default terminal report uses colored tables for the artifact, model, +runtime target, concrete processes, aliases, helicities, color flows, and +payload inventory. Select one process for deeper physics metadata: + +```console +pyamplicol inspect artifacts/pp_zjj --process 'd d~ > g z g' +``` + +Machine-readable output is available without changing the artifact: + +```console +pyamplicol inspect artifacts/pp_zjj --json > inspection.json +``` + +## 4. Evaluate totals and resolved components + +```console +pyamplicol evaluate_total.toml +pyamplicol evaluate_resolved.toml +``` + +Both cards load `artifacts/pp_zjj`, apply the UFO parameter card in +`data/model_parameters.json`, and read `data/pp_zjj_momenta.json`. + +- `evaluate_total.toml` calls the optimized fully summed runtime path. +- `evaluate_resolved.toml` shows the physical helicity/color components and an + explicit per-point sum. + +The two totals agree. Nonzero matrix elements are displayed in scientific +notation; exact zeros remain `0`. Add `--json` for scripts: + +```console +pyamplicol evaluate_total.toml --json +``` + +## 5. Understand process ordering + +The artifact stores a representative process, but the runtime accepts a unique +permutation within each side of the process arrow: + +```console +pyamplicol evaluate artifacts/pp_zjj \ + --process 'd d~ > g z g' \ + --momenta data/pp_zjj_momenta.json \ + --model-parameters data/model_parameters.json +``` + +The input point is interpreted in the exact public ordering written in +`--process`. Rusticol consistently remaps momenta, external-particle metadata, +helicities, LC flows, selectors, reductions, and resolved outputs. No leg may +cross `>`. + +If multiple stored representatives could match, selection fails with the +candidate stable IDs instead of guessing. A stable ID such as +`p_p_to_z_j_j_3` is always an unambiguous alternative. + +## 6. Profile the runtime + +```console +pyamplicol benchmark.toml +``` + +This short profile uses the same optimized total path, a 128-point batch, two +warmups, at least five measured blocks, and a one-second target. The human +report separates headline wall/evaluator timing from native component +attribution and work counters; rows with exactly zero measured time are omitted. + +The direct equivalent is: + +```console +pyamplicol profile artifacts/pp_zjj \ + --process 'd d~ > g z g' \ + --momenta data/pp_zjj_momenta.json \ + --target-runtime 1.0 --batch-size 128 --color-flow 1 --precision 16 +``` + +`pyamplicol benchmark` is a compatibility alias for `profile`. + +## 7. Use the Python runtime + +```python +import json +import math +from pathlib import Path + +from pyamplicol import Runtime + +points = json.loads(Path("data/pp_zjj_momenta.json").read_text()) +runtime = Runtime.load( + "artifacts/pp_zjj", + process="d d~ > g z g", + model_parameters={"aS": 0.117}, +) + +total = runtime.evaluate(points) +resolved = runtime.evaluate_resolved(points) + +print(runtime.physics.process) +print(total[0]) +for optimized, explicit in zip(total, resolved.total(), strict=True): + assert math.isclose(optimized.real, explicit.real, rel_tol=1e-12, abs_tol=1e-15) + assert math.isclose(optimized.imag, explicit.imag, rel_tol=1e-12, abs_tol=1e-15) +``` + +The runtime exposes stable selector IDs: + +```python +one_component = runtime.evaluate( + points, + helicities=[runtime.physics.helicity_ids[0]], + color_flows=[runtime.physics.color_flow_ids[0]], +) +``` + +See [Python API](python-api.md) and [Runtime and Selectors](runtime-and-selectors.md) +for typed results, per-point selectors, arbitrary precision, and benchmarking. + +## 8. Try a direct built-in-model command + +For a smaller single-process example: + +```console +pyamplicol generate 'd d~ > z g' artifacts/builtin_ddbar_to_zg \ + --model built-in-sm +pyamplicol inspect artifacts/builtin_ddbar_to_zg +``` + +The built-in Standard Model automatically uses the wheel-owned prepared JIT O2 +kernel pack for the default recurrence execution mode. + +## 9. Try generated language APIs + +The generated `artifacts/pp_zjj/API/` tree contains standalone Python, C11, +C++17, Fortran 2008, and Rust 2021 drivers. For example: + +```console +python artifacts/pp_zjj/API/python/check_standalone.py \ + --process 'd d~ > g z g' --set-parameter aS 0.117 0 --json +``` + +See [Native APIs](native-apis.md) for all five invocations, custom kinematics, and +SDK discovery. + +## Where to go next + +- [Examples Gallery](examples-gallery.md) — choose another shipped card. +- [Configuration](configuration.md) — adapt a TOML card or use direct CLI overrides. +- [Models and Processes](models-and-processes.md) — load your own JSON/UFO model. +- [Artifacts and Portability](artifacts-and-portability.md) — understand schema-v3 artifacts, portable compiled JIT O1/O2, and prepared O2 packs. +- [Profiling Campaigns](profiling-campaigns.md) — run reproducible multi-process measurements. diff --git a/pyamplicol/docs/user/release-and-support.md b/pyamplicol/docs/user/release-and-support.md new file mode 100644 index 0000000000..5e419d31d2 --- /dev/null +++ b/pyamplicol/docs/user/release-and-support.md @@ -0,0 +1,300 @@ +--- +title: "Release and Support" +nav_order: 5 +parent: "Profiling and Benchmarking" +--- + + +# Release and Support + +pyAmpliCol 1.0.0 is the software release accompanying the official arXiv +publication, *pyAmpliCol: fast tree-level matrix elements*. Binary wheels and a +source distribution are available on [PyPI](https://pypi.org/project/pyamplicol/). +This page records the supported release boundary and explains how to report a +problem. + +## Releases with MadGraph7 + +pyAmpliCol is now developed in the +[MadGraph7 repository](https://github.com/MadGraphTeam/MadGraph7/tree/main/pyamplicol) +and released with MadGraph7: every MadGraph7 release publishes pyAmpliCol to +PyPI with the same version number. These versions (0.2.x and later) sort below +the standalone 1.0.x releases described below, so a plain +`pip install pyamplicol` may still select 1.0.1; MadGraph7 always installs the +exact matching version, `pyamplicol==`. Report problems in +the [MadGraph7 issue tracker](https://github.com/MadGraphTeam/MadGraph7/issues). + +## Current release boundary + +Version `1.0.1` is represented by the immutable +[`v1.0.1` source snapshot](https://github.com/MadGraphTeam/pyamplicol/tree/v1.0.1), +the [GitHub release](https://github.com/MadGraphTeam/pyamplicol/releases/tag/v1.0.1), +and [PyPI release](https://pypi.org/project/pyamplicol/1.0.1/). Its validated +inventory is one source distribution and three `cp311-abi3` wheels: + +- macOS 11 or newer on Apple silicon; +- macOS 11 or newer on x86-64; +- manylinux 2.28 on x86-64. + +The [release workflow](https://github.com/MadGraphTeam/MadGraph7/actions/workflows/release.yml) +installs each wheel into a clean CPython 3.11 environment and exercises the +Python, C11, C++17, Fortran 2008, and Rust 2021 APIs. It also runs a CPython +3.14 abi3 smoke test, source preflight, and independent Fortran physics oracle. +Publication uploads these already validated files without rebuilding them. + +Version 1.0.1 uses published Symbolica 3.0.0, SymJIT 2.26.4 and +ufo-model-loader 1.0.0, as recorded in the dependency lockfiles. No local +dependency source patches or unpublished wheels are needed. + +## 1.0.1 release + +Version `1.0.1` adds process grouping for multiprocess outputs. Subprocesses +with equal matrix elements are recorded in a new `process-groups.json` +artifact payload, with their leg permutation, rational factor, and charge +conjugation. `--flavor-blind` generates one representative per flavour +class, optionally including charge conjugation, and every other flavour +remains selectable from Python, the CLI, and the native SDKs, which route it +to its representative. `--analyze-process-groups numerical` and +`pyamplicol group-processes` find further relations numerically. +`pyamplicol inspect` and the generation summary display the groups, and UMAMI +exports flavour-blind members through their representative. UFO multiparticle +expansion now enumerates multisets and drops candidates that violate a charge +conserved by every vertex, which renumbers the process IDs of compiled-model +multiprocess requests. Artifacts written by 1.0.1 declare the new payload and +require a 1.0.1 runtime. See [Process Grouping](process-grouping.md). + +## 1.0.0 release + +Version `1.0.0` preserves exact model constants, colour weights, and +normalization factors through higher-precision evaluation. It also corrects +native evaluation and retains the exact next-to-leading-colour generation +shortcut. The release keeps the same supported platforms and Python versions. +Tensor algebra and prepared models have been updated for the published +Symbolica 3.0.0 API, including exact complex constants in the SymJIT bridge. +Equivalent exact and floating-point coefficients are again recognized when +selecting optimized recurrence kernels, including in the bundled SM and HEFT models. +Batched recurrence evaluations now fill the already allocated SIMD-aligned +workspace, avoiding unnecessarily small batches at higher multiplicities. +Completed on-the-fly caches can be saved and restored through Python and every +native SDK, without reconstructing the retained currents. See +[saving an OTF warm cache](runtime-and-selectors.md#saving-an-otf-warm-cache). +Symbolica's package license removes the need for a personal key for pyAmpliCol +use; personal-license and restricted-mode fallbacks remain available. +Compiled JIT O2 generation selects compressed code by default. + +## 0.2.0 release + +Version `0.2.0` adds exact symmetric-group FFT full-colour contraction, +helicity-parametric recurrence and on-the-fly scheduling, built-in SM+HEFT and +trusted UFO HEFT support, and the resumable fixed-helicity/helicity-sum FFT +profiling workflow with its final comparison reports. +See the runnable [scalar HEFT workflow](models-and-processes.md#built-in-scalar-heft-model) +and the [FullColor FFT profiling guide](fullcolor-fft-profiling.md). + +## 0.1.4 release + +Version `0.1.4` adds on-the-fly generation and its explicit one-point +`warm_up(...)` lifecycle, while retaining compiled, eager, and recurrence +generation. It also ships progress-aware warm-up entry points for the Python, +C, C++, Fortran, and Rust APIs and the packaged `p p > Z j j` OTF example. + +## Install a release + +```console +python -m venv .venv +. .venv/bin/activate +python -m pip install pyamplicol +``` + +Binary wheels include the Rust runtime and native SDK. Wheel users do not need +a Rust compiler. A C, C++, Fortran, or Rust compiler is required only when +compiling a consumer in that language against the included SDK. + +Verify the environment with: + +```console +pyamplicol doctor +pyamplicol self-test +``` + +Then run the primary example from an editable copy: + +```console +pyamplicol examples copy ./pyamplicol-examples --force +cd pyamplicol-examples +pyamplicol generate_pp_zjj_from_ufo_sm.toml +pyamplicol evaluate_total.toml +``` + +See [Installation](installation.md) and [Quick Start](quick-start.md). + +## What a release contains + +The release set consists of: + +- one source distribution; +- supported-platform `cp311-abi3` wheels; +- the Python extension and target-specific static Rusticol SDK in each wheel; +- packaged model resources, examples, profiling-campaign templates, and user + documentation required by installed workflows. + +Source installation may work elsewhere, but a platform is not advertised as +supported until its installed Python and native API matrix has passed. + +Rendered performance PDFs are repository documents, not PyPI wheel payloads or +release-CI benchmarks. Their links intentionally point to the current `main` +branch: + +- [MacBook M3 report](https://github.com/MadGraphTeam/pyamplicol/blob/main/docs/performance_reports/macbook_M3_pyAmpliCol.pdf) +- [AMD EPYC report](https://github.com/MadGraphTeam/pyamplicol/blob/main/docs/performance_reports/EPYC_pyAmpliCol.pdf) + +## How artifacts are validated and published + +The authoritative release workflow builds and tests the source distribution +and platform wheels. Deployment tests install each wheel into a clean +environment and exercise the installed Python runtime and native SDK consumers. + +Publication then selects that already validated artifact run and uploads its +exact files through PyPI Trusted Publishing. Upload does not rebuild packages. +This keeps the tested and published bytes identical without adding a second +release build. + +GitHub workflow history is available under +[Actions](https://github.com/MadGraphTeam/pyamplicol/actions), and tagged source +snapshots under [Releases](https://github.com/MadGraphTeam/pyamplicol/releases). + +## Compatibility promises + +- The public Python API is versioned and documented in the repository's API + contract. +- The native public boundary is C ABI version 1; C++, Fortran, and safe Rust + wrappers sit on that ABI. +- Run cards use schema version 1 and reject unknown fields. +- Current process artifacts use schema version 3 and the current runtime + identity/ABI contract. +- Internal generated-artifact formats are not automatically migrated. An old + artifact that lacks the current contract fails with regeneration guidance. + +Regenerate process artifacts with the installed version when upgrading across +an artifact-contract change. Do not edit a manifest or executable payload by +hand. + +## Supported and intentionally separate components + +- pyAmpliCol has no LHAPDF dependency. +- Symbolica is required for model compilation, generation, and Python exact + paths; the default compatible f64 JIT runtime executes through SymJIT without + importing Symbolica. +- Original AmpliCol is optional campaign/developer comparison infrastructure, + not an installed-package runtime dependency. +- Source contributor builds and release builds have separate dependency + boundaries. Contributor candidate wheels are deliberately non-publishable. + +See [Symbolica and Licensing](symbolica-and-licensing.md), +[Profiling Campaigns](profiling-campaigns.md), and +[Artifacts and Portability](artifacts-and-portability.md). + +## Report a problem + +Use [GitHub Issues](https://github.com/MadGraphTeam/pyamplicol/issues) for a +reproducible bug, compatibility failure, or documentation gap. Search existing +issues first. + +Include the smallest information that distinguishes the failure: + +1. exact pyAmpliCol version and installation method; +2. operating system, architecture, and Python version; +3. the command or short Python snippet; +4. the complete error message; +5. whether the model is built-in, JSON, compiled, prepared, or trusted UFO; +6. execution mode, backend, color accuracy, and process expression; +7. output from `pyamplicol doctor` when installation or SDK discovery is + involved. + +Useful read-only captures are: + +```console +pyamplicol doctor --json > doctor.json +pyamplicol inspect ARTIFACT --json > inspect.json +pyamplicol config resolve RUN_CARD.toml --json > config.json +``` + +Review these files before attaching them. Do not publish private model data, +license keys, cluster paths, credentials, or an artifact you are not permitted +to redistribute. + +For a generated native-driver failure, also include: + +```console +rusticol-config --version +rusticol-config --target +rusticol-config --json +``` + +For a numerical report, provide the smallest kinematic point and parameter card +that reproduces it, the selected stable ID or process expression, precision, +and selectors. State whether the optimized total and explicit resolved sum +disagree. + +## Triage checklist + +Before filing, try the focused check matching the symptom: + +| Symptom | First check | +| --- | --- | +| CLI/import failure | `pyamplicol doctor` | +| Installation/runtime smoke | `pyamplicol self-test` | +| Native headers or linker flags missing | Activate the environment, then `rusticol-config --json` | +| Artifact target/ABI rejection | `pyamplicol inspect ARTIFACT` and regenerate with the current release | +| Process expression not found | Inspect stable IDs; see [Process Selection and Permutations](process-selection-and-permutations.md) | +| Selector rejected | Inspect the selected process's helicity/color capabilities | +| Raw UFO/JSON fails in recurrence/eager/OTF | Prepare a compatible `.pyamplicol-model` bundle, or choose compiled mode | +| High-precision Python failure | Confirm Symbolica availability and license; f64 is `--precision 16` | +| Profiling result is surprising | Keep process, layout, selectors, batch, and precision fixed; see [Profiling and Benchmarking](profiling-and-benchmarking.md) | + +More symptom-specific help is in [Troubleshooting](troubleshooting.md). + +## Feature requests + +A useful feature request explains: + +- the physics or deployment workflow; +- why existing model, generation, runtime, or selector APIs are insufficient; +- the smallest representative process/model; +- expected public behavior, including error behavior; +- platform or performance constraints. + +Avoid attaching a complete private project when a small public model/process +can demonstrate the need. + +## Contributing a fix + +Contributor setup is documented in [Installation](installation.md). +The repository policy favors focused tests for a demonstrated failure plus one +authoritative CI path. Changes should not add redundant manifests, repeated +hashing, dependency revalidation, or unrelated release ceremony. + +Public API changes require corresponding documentation and typing coverage. +Runtime or artifact changes require proportionate native tests; documentation- +only changes do not justify rebuilding the platform release matrix. + +## Security and artifact trust + +Process artifacts are executable inputs. JIT applications are lowered to +native code at load time, and C++/ASM artifacts may contain native libraries. +Manifest validation establishes internal consistency and path confinement, not +publisher identity. Generate artifacts yourself or obtain them through a +trusted channel. + +Do not post a suspected security vulnerability publicly before maintainers can +assess it. Use the repository's available private security-reporting channel if +enabled; otherwise contact the maintainers through the organization channels +without including exploit details in a public issue. + +## Links + +- [PyPI](https://pypi.org/project/pyamplicol/) +- [GitHub releases](https://github.com/MadGraphTeam/pyamplicol/releases) +- [Issue tracker](https://github.com/MadGraphTeam/pyamplicol/issues) +- [License](https://github.com/MadGraphTeam/pyamplicol/blob/main/LICENSE) +- [Third-party notices](https://github.com/MadGraphTeam/pyamplicol/blob/main/THIRD_PARTY_NOTICES.md) diff --git a/pyamplicol/docs/user/runtime-and-selectors.md b/pyamplicol/docs/user/runtime-and-selectors.md new file mode 100644 index 0000000000..03a0fffec6 --- /dev/null +++ b/pyamplicol/docs/user/runtime-and-selectors.md @@ -0,0 +1,708 @@ +--- +title: "Runtime and Selectors" +nav_order: 1 +parent: "Python API" +--- + +# Runtime and Selectors + +pyAmpliCol process artifacts are designed to be evaluated after generation, +without rebuilding the model or process DAG. The Python `Runtime` and all +native APIs use the same RustiCol execution layer, process resolver, selector +planner, model-parameter state, and numerical conventions for ordinary +evaluations. + +[Born Correlations](../correlators.md) use an opt-in, separate Python evaluator. +They accept generation-time colour-operator IDs and runtime spin vectors, not +the ordinary LC flow selector. This path currently supports full-colour SU(3) +only, and requires the original generated particle ordering (no nonidentity +aliases or automatic permutations). A process-group member is accepted only +with that ordering and without charge conjugation; its factor is applied. + +> **Start here:** follow [Quick Start](quick-start.md) to create the example workspace and +> generate `artifacts/pp_zjj`. See [Generation Modes and Evaluators](generation-modes-and-evaluators.md) for the generation +> side of the workflow and [Artifacts and Portability](artifacts-and-portability.md) before moving an +> artifact between machines. + +## Inspect before loading + +Inspection reads the artifact inventory without loading executable evaluator +state: + +```console +pyamplicol inspect artifacts/pp_zjj +``` + +The human view is a colored set of tables covering the artifact, model, +runtime target, concrete processes, crossing aliases, particle order, +helicities, color coverage, payload sizes, and dependencies. Use JSON for a +machine-readable inventory: + +```console +pyamplicol inspect artifacts/pp_zjj --json > pp_zjj-inventory.json +``` + +For one process, either a stable ID or a process expression is accepted: + +```console +pyamplicol inspect artifacts/pp_zjj --process p_p_to_z_j_j_3 +pyamplicol inspect artifacts/pp_zjj --process 'd d~ > g z g' +``` + +For an on-the-fly artifact, this compact inventory reports the physical +selector census, requested query-construction workers, and binary64-only +runtime without instantiating the runtime or enumerating a dense selector +axis. That metadata-only view is the safe default at high multiplicity. Ask +for the legacy fully materialized physics view only when its complete selector +catalog is genuinely needed: + +```console +pyamplicol inspect artifacts/otf_pp_zjj \ + --process 'd d~ > g z g' --full-physics +``` + +`--full-physics` loads executable evaluator state and can be factorially large +in the number of colored legs. + +## Load a process from Python + +```python +from pyamplicol import Runtime + +runtime = Runtime.load( + "artifacts/pp_zjj", + process="d d~ > g z g", +) + +print(runtime.execution_mode) # compiled, eager, recurrence, or on-the-fly +print(runtime.artifact_id) # 64 lowercase hexadecimal characters +print(runtime.physics.process) # requested public particle order +print(runtime.physics.external_particles) +print(runtime.physics.helicity_ids) +print(runtime.physics.color_flow_ids) +``` + +If an artifact contains only one process, `process=` may be omitted. For a +multiprocess artifact, make the selection explicit so the chosen public +particle order is obvious at the call site. + +## Process expressions and automatic permutations + +Selection is resolved in this order: + +1. stable process ID; +2. explicit alias ID; +3. exact stored process expression; +4. exact stored alias expression; +5. a unique permutation-equivalent expression. + +Incoming and outgoing particles are matched independently. A particle never +crosses the `>` boundary. Repeated identical particles use deterministic +first-unused matching. If several stored representatives would fit, loading +fails and reports their stable IDs instead of guessing. + +For example, the representative `p_p_to_z_j_j_3` is stored for one ordering, +but this is a valid public selection: + +```python +runtime = Runtime.load( + "artifacts/pp_zjj", + process="d d~ > g z g", +) +``` + +Rusticol then applies one central side-preserving permutation to all public +axes: + +- input four-momenta and external PDGs; +- external-particle metadata; +- helicity IDs, vectors, representatives, and selectors; +- leading-color flow words, IDs, replay labels, and selectors; +- reduction groups and resolved output metadata; +- compiled, eager, recurrence, on-the-fly, scalar/SIMD, and Python exact + execution. + +Consequently, momenta supplied to this runtime must follow `d, d~, g, z, g`, +not the representative's internal order. + +A sixth step resolves processes that a `--flavor-blind` generation did not +generate but listed in the artifact's process-group map. Such a member is +evaluated by its representative at the composed permutation, with every +matrix element multiplied by the member's exact factor. A charge-conjugated +member reverses each LC flow word, so its flow IDs and selectors are the +member's own: + +```python +runtime = Runtime.load("artifacts/qg", process="u~ g > u~ g") +print(runtime.representative_process_key) +print(runtime.process_group_factor, runtime.charge_conjugated) +``` + +Every other selection reports `process_group_factor == 1` and +`charge_conjugated is False`. See +[Process-group members](process-selection-and-permutations.md#process-group-members). + +## Momenta + +Python accepts a batch with shape +`(point, external particle, [E, px, py, pz])`. A single five-particle point is +therefore wrapped in one outer batch dimension: + +```python +momenta = [[ + [500.0, 0.0, 0.0, 500.0], + [500.0, 0.0, 0.0, -500.0], + [462.6501613061637, 14.340107538562991, + 155.76435943335707, -425.7484539710246], + [369.7738416261408, -17.479290785282917, + 2.0064955613504103, 369.3550355960509], + [167.57599706769557, 3.1391832467199254, + -157.77085499470743, 56.3934183749737], +]] +``` + +The fourth-vector convention is `[E, px, py, pz]`. The particle axis follows +the expression passed to `Runtime.load()`. + +The CLI reads the same shape from JSON: + +```console +pyamplicol evaluate artifacts/pp_zjj \ + --process 'd d~ > g z g' \ + --momenta data/pp_zjj_momenta.json +``` + +## Model parameters + +Inspect mutable external parameters and their defaults before applying an +update: + +```python +for parameter in runtime.physics.model_parameters: + if parameter.mutable: + print(parameter.name, parameter.default_real) +``` + +Mutable external model parameters can be set while loading: + +```python +runtime = Runtime.load( + "artifacts/pp_zjj", + process="d d~ > g z g", + model_parameters={"aS": 0.117, "MZ": 91.188}, +) +``` + +They can also be updated atomically later: + +```python +runtime.set_model_parameters({"aS": 0.1165, "MZ": 91.1876}) +runtime.set_model_parameter("MT", 172.5) +``` + +Derived couplings and dependent parameters are refreshed before the update is +committed. If any name is unknown or immutable, or any value is invalid or +non-finite, the complete update is rejected. + +A parameter card is a flat JSON object matching a serialized UFO restriction: + +```json +{ + "aS": 0.117, + "MZ": 91.188, + "complex_external_parameter": [1.0, -0.25] +} +``` + +The generated standalone API drivers apply card values first, then let repeated +`--set-parameter NAME REAL IMAG` options override them. The main CLI accepts +the card through `--model-parameters`; Python callers pass a mapping while +loading or use the setters shown above. See +[Models and Processes](models-and-processes.md) for model compilation and card +production. + +## Total and resolved evaluation + +The optimized total returns one value per input point: + +```python +total = runtime.evaluate(momenta) +``` + +The resolved path exposes the physical axes retained in the artifact: + +```python +resolved = runtime.evaluate_resolved(momenta) + +for optimized, explicit in zip(total, resolved.total(), strict=True): + scale = max(1.0, abs(optimized)) + assert abs(optimized - explicit) <= 1.0e-12 * scale + +print(resolved.shape) +print(resolved.helicity_ids) +print(resolved.color_flow_ids) +``` + +At leading color (LC), the resolved shape is `(point, helicity, physical +color flow)`. At NLC and full color, color is contracted and the final axis has +length one. That contracted singleton is output metadata, not a selectable LC +flow. + +Human CLI output uses colored tables and scientific notation; add `--json` only +when a machine-readable representation is wanted. + +## Batch-global selectors + +Stable IDs come from `runtime.physics`: + +```python +helicity = runtime.physics.helicity_ids[0] +flow = runtime.physics.color_flow_ids[0] + +selected_total = runtime.evaluate( + momenta, + helicities=[helicity], + color_flows=[flow], +) + +selected_resolved = runtime.evaluate_resolved( + momenta, + helicities=[helicity], + color_flows=[flow], +) +``` + +`--helicity` accepts a stable helicity ID. `--color-flow` accepts either a +stable flow ID or a one-based advertised position; for example, +`--color-flow 1` selects the first physical LC flow. + +Color-flow selection is LC-only. NLC/full accept helicity selectors and reject +color-flow selectors. + +## Stay on the generated LC layout's optimized workload + +LC artifacts retain the layout chosen during generation. Runtime selectors do +not switch an artifact from one layout to the other. Inspect the complete +artifact first; its process-execution table includes an `LC flow layout` row: + +```console +pyamplicol inspect artifacts/my_process +``` + +Then invoke `Runtime.evaluate()` with the selector pattern that matches that +layout: + +| Generated LC layout | Supply | Deliberately omit | Optimized total computed per point | +| --- | --- | --- | --- | +| `topology-replay` (non-union) | exactly one color-flow ID | helicity selector | selected flow, summed over all physical helicities | +| `all-flow-union` | exactly one helicity ID | color-flow selector | selected helicity, summed over all physical flows | + +For the default `topology-replay` layout: + +```python +flow = runtime.physics.color_flow_ids[0] + +single_flow_helicity_sum = runtime.evaluate( + momenta, + color_flows=(flow,), + precision=16, +) +``` + +The equivalent CLI call uses one stable flow ID, or the one-based position +shown by `inspect`: + +```console +pyamplicol evaluate artifacts/my_process \ + --process 'd d~ > g z g' \ + --momenta data/point.json \ + --precision 16 \ + --no-resolved \ + --color-flow 1 +``` + +For an artifact generated with `lc_flow_layout = "all-flow-union"`: + +```python +helicity = next( + item + for item in runtime.physics.helicities + if item.computed and not item.structural_zero +) + +all_flows_single_helicity = runtime.evaluate( + momenta, + helicities=(helicity,), + precision=16, +) +``` + +The CLI form uses the stable helicity ID printed by `inspect`: + +```console +pyamplicol evaluate artifacts/my_union_process \ + --process 'd d~ > g z g' \ + --momenta data/point.json \ + --precision 16 \ + --no-resolved \ + --helicity 'h:-1,+1,-1,+1,+1' +``` + +For a mixed batch, `color_flow_by_point=(...)` with no helicity selector is the +per-point form of the topology-replay workload; +`helicity_by_point=(...)` with no color-flow selector is the per-point form of +the all-flow-union workload. + +To sample one helicity and one LC flow from a helicity-generic output, select +both axes at runtime: + +```python +single_flow_single_helicity = runtime.evaluate( + momenta, + helicities=(helicity,), + color_flows=(flow,), + precision=16, +) +``` + +With `topology-replay`, recurrence prepares and caches the current dependencies +needed by the selected helicity and flow. Compiled evaluation selects the +corresponding colour and helicity sub-schedules instead of computing an +all-colour companion. The output still supports every generated helicity: +changing the runtime selectors requires no regeneration. Shared currents and +physical helicity/flow relabellings are preserved. Omitting the helicity +selector retains the existing helicity-summed evaluation path. + +Use the same selectors with the public profiler to time the warmed, batched +single-flow/single-helicity workload: + +```console +pyamplicol profile artifacts/my_process \ + --color-flow 1 --helicity 'h:-1,+1,-1,+1,+1' \ + --batch-size 128 --precision 16 +``` + +Other selector combinations remain valid when the output has the required +coverage. Omitting the layout's required selector asks for a broader sum. Use +`Runtime.evaluate()` at `precision=16` for the optimized native total; +`evaluate_resolved()` and higher precision are diagnostic/exact paths rather +than that benchmarked execution unit. + +These two stored layouts apply to recurrence, compiled, and eager artifacts. +Those modes can also fix flow IDs, helicity IDs, or both during generation when +a deliberately specialized artifact is preferable. OTF does not materialize a +layout or use generation-time specialization: one compact artifact builds the +family implied by the runtime selectors and retains only the last selected +family. Its practical high-multiplicity target is one selected LC flow summed +over all helicities. See +[LC workloads and execution modes](lc-workloads-and-execution-modes.md) for the +physical distinction and call signatures. + +### Omitted selectors and requesting every entry + +The meaning of omission depends on the operation: + +- `Runtime.evaluate(momenta)` omits both axes and therefore sums every retained + physical helicity and color flow: it is the complete matrix element. +- Profiling with both selector lists empty asks pyAmpliCol to choose the stored + layout's deterministic hot workload described above. +- A complete explicit list on the layout's summed axis is equivalent to + omission and still permits the optimized selector to be inferred. A subset + on that axis is preserved and prevents inference. +- An explicit list on the layout's selected axis is always intentional. A + complete list there requests the broader all-entry workload rather than + being replaced by one inferred selector. + +There is deliberately no magic `all` selector token. To profile every member +of an axis explicitly, pass the complete stable-ID tuple exposed by process +metadata. For example, either of these requests the all-helicity/all-flow LC +total while making the intent unambiguous to the profiler: + +```python +from pyamplicol import BenchmarkConfig, BenchmarkRunner + +# Natural explicit-all spelling for a topology-replay artifact. +topology_all = BenchmarkRunner( + BenchmarkConfig(color_flow_ids=runtime.physics.color_ids) +).run(runtime, points=momenta) + +# Natural explicit-all spelling for an all-flow-union artifact. +union_all = BenchmarkRunner( + BenchmarkConfig(helicity_ids=runtime.physics.helicity_ids) +).run(runtime, points=momenta) +``` + +Those are valid broader workloads and consequently emit the non-hot warning. +For CLI profiling, read the stable IDs from `pyamplicol inspect` and repeat the +option once per entry, for example +`--color-flow FLOW_1 --color-flow FLOW_2 ...` or +`--helicity HELICITY_1 --helicity HELICITY_2 ...`. Color-flow one-based +ordinals may be repeated instead of stable flow IDs. Supplying complete lists +on both axes is also valid, but redundant. The complete list on the selected +axis is the part that makes the all-entry profiling intent explicit. + +## One selector per point + +Complete-coverage artifacts may mix selectors inside one batch: + +```python +mixed = runtime.evaluate( + momenta_batch, + color_flow_by_point=[ + runtime.physics.color_flow_ids[i % 2] + for i in range(len(momenta_batch)) + ], + helicity_by_point=[ + runtime.physics.helicity_ids[(i // 2) % 2] + for i in range(len(momenta_batch)) + ], +) +``` + +Rusticol stably groups equal selectors to preserve contiguous SIMD work, then +restores caller order. A batch-global selector and a per-point selector cannot +be supplied on the same axis. Per-point selection is available for optimized +totals; rectangular `evaluate_resolved()` uses batch-global axes. + +## Explicit OTF warm-up + +OTF exposes its cold structural work through `warm_up(...)`. The call accepts +exactly one phase-space point at native binary64 precision and the same +batch-global selectors as `evaluate(...)`: + +```python +import sys + +from pyamplicol import Runtime +from pyamplicol.reporting import close_progress_sink, progress_sink + +runtime = Runtime.load("artifacts/otf_pp_zjj", process="d d~ > g z g") +point = points[0] # one [external particle][E, px, py, pz] point +flow = runtime.physics.color_flows[0] +progress = progress_sink("auto", stream=sys.stderr, color=True) +try: + result = runtime.warm_up( + (point,), + precision=16, + color_flows=(flow,), + n_cores=4, + progress=progress, + ) +finally: + close_progress_sink(progress) +``` + +Warm-up never uses the later profiling batch: passing 128 points, or any +precision other than `16`, is rejected. It builds the selected query family, +retains it on this runtime handle, and genuinely evaluates the supplied point. +Repeating the same request reuses the family but still performs that required +one-point evaluation. The returned `WarmUpResult` records elapsed time, total +and newly built query counts, cache reuse, and sampled memory. + +The optional positive integer `n_cores` bounds the workers constructing query +traces in parallel. Completed traces are merged into the shared cache serially; +family finalization and the first evaluation also remain serial. More workers +use additional temporary memory, and the speed-up depends on the selected +family. Omit `n_cores` (or pass `None`) to use the process output's +`evaluator.optimization.cores` setting. This override applies only to this +warm-up call: it is not saved in the cache, does not change subsequent +evaluation threading, and does not rebuild an already-warm family. + +The optional progress stream separates process preparation, query-family +construction, family finalization, and first evaluation. It includes completed +query counts, construction workers, and current/peak resident memory when the +platform exposes them. With no observer, warm-up performs no progress callback +or memory-sampling work. + +The cache is handle-local and last-family-only: successfully selecting another +flow or helicity replaces the previously warmed family. LC accepts flow and +helicity selectors; contracted NLC/full accepts helicity selectors only. +`runtime.clear()` returns a Python OTF handle to cold state while keeping its +artifact and current model parameters loaded. Closing/freeing a native handle +releases all of its warm state. C, C++, Fortran, and Rust expose the same four +stages through a fixed-layout optional callback; see +[Native APIs](native-apis.md) and the complete +[OTF lifecycle walkthrough](lc-workloads-and-execution-modes.md#the-otf-warm-state-lifecycle). + +## Saving an OTF warm cache + +`save(path)` writes the completed OTF cache; `load_cache(path)` restores it +into a runtime loaded from the matching process output. Both return `None`. +For a small example, generate a four-gluon process with generic helicity and +flow coverage: + +```console +pyamplicol generate "g g > g g" artifacts/otf_gg_gg \ + --model built-in-sm --color-accuracy lc --execution-mode on-the-fly +``` + +Then warm one LC flow, summed over all helicities, and save it: + +```python +from pyamplicol import Runtime + +point = ( + (500.0, 0.0, 0.0, 500.0), + (500.0, 0.0, 0.0, -500.0), + (500.0, 300.0, 0.0, 400.0), + (500.0, -300.0, 0.0, -400.0), +) +runtime = Runtime.load("artifacts/otf_gg_gg") +flow = runtime.physics.color_flows[0].id +selection = {"color_flows": (flow,)} # no helicity selector: sum helicities +runtime.warm_up((point,), **selection) +runtime.save("gg.otf-cache") + +# This can instead be done in a later Python session. +restored = Runtime.load("artifacts/otf_gg_gg") +restored.load_cache("gg.otf-cache") +report = restored.warm_up((point,), **selection) +assert report.already_warm and report.warmed_query_count == 0 +values = restored.evaluate((point, point), **selection) +print(values) +``` + +The example repeats its point only to keep the two-point batch short; a later +batch may contain different momenta. A successful ordinary `evaluate(...)` +also leaves saveable state, so explicit `warm_up(...)` is optional. The extra +warm-up after restoration above demonstrates cache reuse and can be omitted. +Keep the same selectors when reusing the saved family: `load_cache` restores +cached work, not new defaults for subsequent `evaluate` calls. + +Both methods are OTF-only. The same operations are available through the +[native APIs](native-apis.md#saving-and-restoring-an-otf-cache). +The snapshot contains the retained structural calculation, including its +helicity/colour selection and reduction mappings. It supports selected LC +requests, helicity/flow sums, and contracted NLC/full-colour families, with +either direct colour contraction or FFT where the original process supports it. + +Restoration skips current construction and binds the saved schedule to the +prepared kernels in the matching process output. It does not save evaluated +currents, native pointers or numeric workspaces. New momenta and batch sizes +are supported, and the receiving runtime keeps its current model parameters. +The original process output is still required; the cache is not a standalone +process export. Incompatible process outputs or snapshot formats are rejected +without replacing the receiving runtime's existing cache. + +The cache path names a file (its parent directory must already exist). Saving +again replaces that file; a failed save leaves any previous complete file +intact. Empty caches and completed zero-valued selections are also saveable. + +Saving does not change the running handle or its warmed evaluation path. +It saves the state currently retained, not a history of evicted selections. +A different selection after restoration follows the usual OTF construction +and replacement rules. This is a completed-state snapshot, not a checkpoint +of an unfinished warm-up. + +## Precision + +For ordinary evaluation, `precision=16` uses the native f64 RustiCol runtime. Direct JIT artifacts load +the separate MIT-licensed SymJIT runtime; compatible C++ and ASM artifacts load +their native evaluator libraries. None of these f64 paths imports Symbolica or +checks a Symbolica runtime license. + +The separate `evaluate_correlated` method uses retained Symbolica evaluator +states at every requested precision, including 16; see +[Born Correlations](../correlators.md). + +Python may request another positive decimal precision: + +```python +high_precision = runtime.evaluate(momenta, precision=80) +``` + +This higher-precision path exists only when the artifact retains an exact +evaluator. OTF artifacts are native-f64-only and reject every precision other +than `16` in Python as well as in the native APIs. + +That path lazily loads retained Symbolica evaluator state and is therefore +subject to Symbolica availability and licensing. Decimal inputs retain their +digits. Values first supplied as binary64 do not acquire new physical +information simply because a larger arithmetic precision is requested. +Results are rounded to the requested decimal precision after guard-digit +evaluation. Derived model parameters and couplings are reevaluated from their +retained symbolic definitions at this precision, including constants such as +pi. Coupling powers and combinatorial normalization factors are also evaluated +in the requested arithmetic, rather than copied from the native f64 result. +Generated full-colour and NLC contraction weights also retain their rational +coefficients for higher-precision evaluation; native evaluation continues to +use their binary64 values. +This matters when subtracting matrix elements at different multiplicities. + +After updating from a version that rounded these definitions during generation, +regenerate the prepared model and process output: increasing `precision` cannot +repair coefficients already rounded in an older output. The prepared-model +compiler version rejects incompatible bundled kernels. The public +`precision=32` call uses guarded arbitrary precision, not a dedicated +double-double runtime. See +[Symbolica and Licensing](symbolica-and-licensing.md). + +Native C, C++, Fortran, and Rust APIs expose f64 only. Their shared SDK is +covered in [Native APIs](native-apis.md). + +## Profiling one runtime + +Profile the same optimized total path used by `Runtime.evaluate()`: + +```console +pyamplicol profile artifacts/pp_zjj \ + --process 'd d~ > g z g' \ + --momenta data/pp_zjj_momenta.json \ + --target-runtime 1.0 \ + --batch-size 128 \ + --color-flow 1 \ + --precision 16 +``` + +If both profiling selector axes are omitted, pyAmpliCol derives one +deterministic selector matching the stored hot layout. Explicit subsets and +selected-axis lists are preserved; a complete summed-axis list is normalized +to equivalent omission as described above. A valid non-hot profile instead +emits at most one pre-loop warning per loaded process. This profiling +convenience does not change the complete-matrix-element default of +`Runtime.evaluate()`. + +For OTF, the profiler first snapshots the compact native runtime census, times +one requested-selector evaluation over the configured benchmark batch, and +then authenticates that the family is retained before ordinary configured +warmups begin. The result records whether the starting handle was cold or +already warm. This implicit profiling preparation is distinct from the public +one-point `warm_up(...)` API and is reported separately; neither preparation +time nor configured warmups are folded into the steady-state wall samples. + +A retained structural-zero request intentionally has no amplitude destination, +executor handle, semantic binding, or active family-union census. The profiler +recognizes that as a valid retained state instead of inventing executable work. +OTF native attribution labels the narrower recurrence-schedule core separately +from the complete evaluator envelope and from recurrence mode's statically +generated core. + +The headline wall time is sampled independently of the paired native +attribution pass. `evaluator total` is a minimally instrumented complete +evaluator call. Native component rows describe a separate profiled pass and +must not be subtracted from that headline as though they were measured in one +identical call. `pyamplicol benchmark` is a compatibility alias for `profile`. + +For controlled, multi-process comparisons and report generation, use +[Profiling Campaigns](profiling-campaigns.md) instead. + +## Handle ownership and warnings + +Parameter and warning state belongs to one runtime handle. Do not invoke the +same mutable handle concurrently. Independent handles may execute on separate +threads. + +Artifacts are trusted executable inputs. Generate them yourself or obtain +them through a trusted channel; content hashes establish internal consistency, +not publisher identity. See [Artifacts and Portability](artifacts-and-portability.md) for the fast load +boundary and optional explicit checksum audit. + +## Related pages + +- [Quick Start](quick-start.md) — generate and evaluate the primary example. +- [Python API](python-api.md) — typed generation, model, and benchmark interfaces. +- [Native APIs](native-apis.md) — C11, C++17, Fortran 2008, and Rust 2021. +- [LC workloads and execution modes](lc-workloads-and-execution-modes.md) — LC + call shapes and the OTF warm-state lifecycle. +- [Troubleshooting](troubleshooting.md) — missing artifacts, target mismatches, and stale source + environments. diff --git a/pyamplicol/docs/user/symbolica-and-licensing.md b/pyamplicol/docs/user/symbolica-and-licensing.md new file mode 100644 index 0000000000..7b70a189c9 --- /dev/null +++ b/pyamplicol/docs/user/symbolica-and-licensing.md @@ -0,0 +1,322 @@ +--- +title: "Symbolica and Licensing" +nav_order: 1 +parent: "Architecture Overview" +--- + +# Symbolica and Licensing + +pyAmpliCol uses Symbolica for symbolic model compilation and process +generation, but its default f64 deployment path is deliberately separated from +Symbolica's runtime. This page explains the technical boundary and summarizes +where to read the authoritative license terms. + +> This page is a technical guide, not legal advice. Upstream license terms and +> eligibility rules control. The complete notices distributed with pyAmpliCol +> are in +> [`THIRD_PARTY_NOTICES.md`](https://github.com/MadGraphTeam/pyamplicol/blob/main/THIRD_PARTY_NOTICES.md) +> and the repository's `licenses/` directory. + +## Package license at startup + +Importing pyAmpliCol imports Symbolica and calls `symbolica.set_library_key()` +with the key issued for the `pyamplicol` package, before any symbolic work. +When that key is accepted and valid, users do not need a personal Symbolica +license for calls made by pyAmpliCol, including parallel generation and +evaluator optimization. Registration is also attempted when a spawned Python +worker imports pyAmpliCol. + +```python +import sys +import pyamplicol + +print("symbolica" in sys.modules) # True + +from pyamplicol.licensing import detect_symbolica_license +print(detect_symbolica_license(suggest=False).licensed) # Actual license state +``` + +If Symbolica is not installed, lightweight configuration and source tooling +remain available; operations requiring Symbolica still report the missing +dependency. Broken native extensions or other unexpected import errors are +not suppressed. + +The key licenses calls within `pyamplicol`; it does not license unrelated +Symbolica code in the user's program. Personal license environment variables +are left unchanged. Model compilation and generation tooling remain lazy. + +If registration raises `ValueError` or `PermissionError`, or an older +Symbolica lacks `set_library_key()`, import continues. Symbolica's public +`is_licensed()` check then decides whether a valid personal license permits +normal operation or restricted resources are needed; a nonempty +`SYMBOLICA_LICENSE` alone is not proof of validity. Personal-license +configuration and the existing license-request helpers remain available. + +This fallback covers synchronous registration failures, not all later +revocations. Symbolica may terminate the process during asynchronous license +verification; pyAmpliCol cannot catch that upstream termination and switch +licenses in the same process. + +The startup import and registration temporarily suppress Symbolica's banner +before CLI output policy is known, then restore the caller's banner setting. +Normal reminder suppression and JSON output behavior remain unchanged. + +## Which operations need Symbolica? + +| Operation | Uses Symbolica? | Notes | +| --- | --- | --- | +| Import `pyamplicol` | Yes | Registers the package key; public exports remain lazy. | +| Inspect an artifact | Startup only | Reads metadata and indexes only. | +| Ordinary Python f64 evaluation (`precision=16`) | Startup only | Runs through Rusticol and the artifact's native evaluator. | +| C11/C++17/Fortran 2008/Rust 2021 runtime | No | Ordinary and opt-in correlated APIs are f64-only. | +| Direct JIT f64 load | Startup only in Python | Uses the separate MIT-licensed SymJIT runtime. | +| Compatible C++/ASM evaluator load | Startup only in Python | Uses the artifact's target-native library. | +| Compile a JSON/UFO model | Yes | Symbolic model construction. | +| Generate a process artifact | Yes | Symbolic DAG/recurrence construction and evaluator production. | +| Ordinary Python precision other than 16 | Yes | Lazily loads retained Symbolica evaluator state when supported. | +| Python `evaluate_correlated(...)` | Yes | Uses retained Symbolica evaluator state at every precision, including 16. | + +The absence of a Symbolica runtime dependency for f64 evaluation does not +change the terms governing Symbolica use during generation. + +## Direct-JIT f64 runtime + +The default JIT backend embeds a direct SymJIT application in the schema-v3 +artifact. Rusticol loads and lowers that application to native code without: + +- performing Symbolica computations (Python package startup registers the key); +- reading `SYMBOLICA_LICENSE`; +- applying Symbolica's generation-time worker clamp; +- linking the arbitrary-precision Symbolica/Rug/Malachite closure into the + wheel's f64 native SDK. + +This is the deployment path shared by ordinary Python evaluation at precision +16 and the C11, C++17, Fortran 2008, and Rust 2021 APIs. + +```python +from pyamplicol import Runtime + +runtime = Runtime.load("artifacts/pp_zjj", process="d d~ > g z g") +values = runtime.evaluate(momenta, precision=16) +``` + +Independent runtime handles can execute concurrently even when their artifact +was generated under restricted Symbolica conditions. Do not call one mutable +handle concurrently because its parameter and warning state is handle-local. + +See [Runtime and Selectors](runtime-and-selectors.md) and [Native APIs](native-apis.md). + +## Generation with a valid license + +pyAmpliCol calls `symbolica.is_licensed()` from inside the package to select +its generation resources. This detects a valid registered package key even +when `SYMBOLICA_LICENSE` is absent, or a valid personal license. A call from +unrelated user code can still return `False`, because the package key is scoped +to pyAmpliCol. + +With a valid license, automatic resource settings share one affinity-aware CPU +budget: + +```toml +[generation] +workers = "auto" + +[evaluator.optimization] +cores = "auto" +``` + +Concurrent process builds receive disjoint evaluator budgets. Explicit +requests are clamped when their product exceeds the available budget, and the +requested/effective difference is recorded in generation provenance. + +## Restricted generation + +Without a valid package or personal license, pyAmpliCol offers a +license-request reminder. Users whose work is eligible under Symbolica's +current terms can continue in restricted mode. Symbolica describes that mode +as non-commercial, one instance, and one core per device; commercial work +requires the applicable professional license path. + +pyAmpliCol enforces a technical clamp of one process worker and one Symbolica +core and records why the requested configuration changed. That clamp does not +grant eligibility or replace upstream terms. + +Suppress the request reminder and Symbolica startup banner when appropriate: + +```console +pyamplicol generate_pp_zjj_from_ufo_sm.toml \ + --no-symbolica-suggestion +``` + +or in a run card: + +```toml +[symbolica] +suggest_license = false +``` + +JSON CLI output suppresses the banner automatically so stdout remains valid +machine-readable JSON. + +## Requesting a personal license + +Personal licenses remain useful if the package key is unavailable or for +Symbolica work outside pyAmpliCol. Interactive helpers collect the fields, +show a confirmation, and submit through Symbolica's Python API: + +```console +pyamplicol request-symbolica-trial-license +pyamplicol request-symbolica-hobbyist-license +``` + +Complete noninteractive requests require all fields and `--yes`: + +```console +pyamplicol request-symbolica-trial-license \ + --name "Ada Lovelace" \ + --email ada@example.org \ + --organization "Example Institute" \ + --yes + +pyamplicol request-symbolica-hobbyist-license \ + --name "Ada Lovelace" \ + --email ada@example.org \ + --yes +``` + +pyAmpliCol does not retain the submitted identity fields and does not print the +returned key. Symbolica emails the issued key. Export it before generation: + +```console +export SYMBOLICA_LICENSE='your-issued-key' +``` + +Consult the +[official Symbolica installation and licensing guide](https://symbolica.io/docs/get_started.html) +before choosing a request type. + +## Python exact precision + +Python may request positive decimal precision other than 16: + +```python +exact = runtime.evaluate(momenta, precision=80) +``` + +This loads retained Symbolica evaluator state and requires the applicable +Symbolica package/runtime authorization. Decimal-string inputs can preserve +their source digits through the generated Python standalone driver: + +```console +python artifacts/pp_zjj/API/python/check_standalone.py \ + --process 'd d~ > g z g' \ + --kinematics my_sample_point.json \ + --precision 80 +``` + +A binary64 value converted to higher precision gains trailing arithmetic +digits, not additional physical input information. Use decimal strings in the +kinematics JSON when input precision matters. + +Native C, C++, Fortran, and Rust callers reject precision other than 16. + +The Python [Born Correlations API](../correlators.md) uses the same retained +Symbolica-state machinery through a separate exact executor at **all** +requested precisions, including 16. The C/C++/Fortran/Rust correlated SDK calls +instead use a separate RustiCol binary64 executor without importing or +licensing Symbolica at runtime. Both contract the selected LC/NLC/full colour +matrices prepared during generation, which still requires Symbolica. + +## SymJIT is a separate dependency + +SymJIT is the native JIT runtime used by Symbolica-generated applications. It +is distributed under the MIT License, not under the Symbolica proprietary +license. Release dependency metadata pins the official +[`siravan/symjit-crate`](https://github.com/siravan/symjit-crate) source and an +immutable revision. + +SymJIT compression shares repeated arithmetic sequences to reduce generated +code size. The extra calls can increase evaluation time, but the smaller +instruction footprint can make larger evaluators faster. Neither setting is +universally faster; use `profile` to compare them for your workload. + +The default is `compress = "auto"`: compression is enabled for compiled JIT +evaluators at optimization level O2 and disabled for other execution modes or +optimization levels. Explicit `true` or `false` settings override that policy. +To enable it explicitly: + +```toml +[evaluator.jit] +compress = true +``` + +The equivalent CLI option is: + +```console +pyamplicol generate --card run.toml --jit-compress +``` + +Prepared model bundles bake their compression/backend choice into the kernel +pack, so an incompatible request is adjusted to the prepared pack and reported +in the effective configuration. To change that choice, regenerate the prepared +model with `--jit-compress` or `--no-jit-compress`. + +## License and provenance map + +| Component | Role | License/provenance boundary | +| --- | --- | --- | +| pyAmpliCol and project Rusticol source | Public package/runtime implementation | BSD Zero Clause License (`0BSD`). | +| Symbolica | Symbolic generation and Python exact runtime | Proprietary Symbolica Software License Agreement; project redistribution permission is documented separately. | +| SymJIT | Native JIT application runtime | MIT License. | +| UFO model loader | External UFO loading | MIT License. | +| Bundled SM/scalar/scalar-gravity assets | Example/model data | Asset-specific provenance and reproduced terms in `PROVENANCE.toml` and `licenses/`. | +| Optional original Fortran AmpliCol checkout | Independent campaign oracle | Not bundled in wheels or sdists; its upstream metadata/terms remain separate. | + +The pyAmpliCol project has express authorization from the Symbolica licensor to +redistribute the Symbolica components required by pyAmpliCol's binary runtime. +That project-specific permission is not a general grant to redistribute +Symbolica separately. While valid, the embedded library key covers Symbolica +calls within pyAmpliCol; other Symbolica use requires its own applicable +authorization. + +Exact dependency versions and notices are included in release metadata and +[`THIRD_PARTY_NOTICES.md`](https://github.com/MadGraphTeam/pyamplicol/blob/main/THIRD_PARTY_NOTICES.md). + +## Original AmpliCol is not a pyAmpliCol runtime dependency + +The optional original Fortran AmpliCol checkout is used only as an independent +numerical/performance reference in [Profiling Campaigns](profiling-campaigns.md). It is not shipped +in pyAmpliCol wheels or source distributions and is unnecessary for ordinary +generation or evaluation. + +pyAmpliCol has no LHAPDF dependency. The supported campaign comparison checkout +contains the profiling interface described in +[rikkert-frederix/AmpliCol PR #12](https://github.com/rikkert-frederix/AmpliCol/pull/12). + +## Diagnose the active boundary + +```console +pyamplicol doctor +``` + +The human output is colored and names Python, model assets, Rusticol extension, +native SDK, Symbolica license status, and available compiler tools separately. +Use JSON when attaching diagnostics to an issue: + +```console +pyamplicol doctor --json +``` + +If ordinary or native correlated f64 evaluation fails, do not assume it is a Symbolica-license +problem: first run `pyamplicol self-test` and inspect the artifact target. If +generation, precision-80 evaluation, or Python correlated evaluation at any precision +fails, then inspect the Symbolica check. See +[Troubleshooting](troubleshooting.md). + +## Related pages + +- [Installation](installation.md) — wheel and source requirements. +- [Generation Modes and Evaluators](generation-modes-and-evaluators.md) — where symbolic work occurs. +- [Artifacts and Portability](artifacts-and-portability.md) — what the resulting f64 artifact carries. +- [Born Correlations](../correlators.md) — Python exact and native binary64 correlation paths. +- [Release and Support](release-and-support.md) — published dependency and validation boundary. diff --git a/pyamplicol/docs/user/troubleshooting.md b/pyamplicol/docs/user/troubleshooting.md new file mode 100644 index 0000000000..8d624f24fb --- /dev/null +++ b/pyamplicol/docs/user/troubleshooting.md @@ -0,0 +1,411 @@ +--- +title: "Troubleshooting" +nav_order: 4 +parent: "Profiling and Benchmarking" +--- + +# Troubleshooting + +This page starts with cheap, authoritative checks and then narrows common +generation, runtime, native-SDK, and profiling-campaign failures. Avoid broad +rebuilds until the failing boundary is known. + +## First checks + +Confirm which Python and installation are active: + +```console +command -v python +command -v pyamplicol +python -c 'import pyamplicol; print(pyamplicol.__version__); print(pyamplicol.__file__)' +``` + +Inspect the installed environment and compiler tools: + +```console +pyamplicol doctor +``` + +Run the packaged direct-runtime self-test: + +```console +pyamplicol self-test +``` + +On a healthy 0.2.0 macOS arm64 release-candidate installation, the colored +terminal tables report checks equivalent to this compact transcription: + +```text +python pass +model-assets pass 67 verified files +rusticol-python pass package 0.2.0, C ABI 1 +native-sdk pass aarch64-apple-darwin; ABI 1; librusticol_capi.a +physics-f64 pass d_dbar_to_z; shape (1, 12, 1); direct SymJIT +``` + +The Python version, target, paths, and package version naturally vary. A failure +in one named check points to a much smaller subsystem than a full rebuild. + +## `ModuleNotFoundError: No module named 'pyamplicol'` + +The generated Python driver uses the interpreter that launches it. Activate +the installed environment or invoke that environment's Python explicitly: + +```console +. /path/to/venv/bin/activate +python artifacts/pp_zjj/API/python/check_standalone.py \ + --process 'd d~ > g z g' --json +``` + +or: + +```console +/path/to/venv/bin/python \ + artifacts/pp_zjj/API/python/check_standalone.py \ + --process 'd d~ > g z g' --json +``` + +In the canonical source workflow, a copied workspace below a checkout can find +the checkout's `.venv` created by `just dev-install`. A workspace elsewhere +must use an active or explicit environment. + +## `rusticol-config: command not found` + +C, C++, Fortran, and Rust consumers discover their SDK through the installed +environment: + +```console +. /path/to/venv/bin/activate +rusticol-config --json +``` + +An explicit override is also supported: + +```console +export RUSTICOL_CONFIG=/path/to/venv/bin/rusticol-config +make -C artifacts/pp_zjj/API/c run \ + ARGS='--process "d d~ > g z g" --json' +``` + +Do not manually guess include, archive, framework, or system-library paths. +If `rusticol.h`, `rusticol.hpp`, the Fortran module source, or +`RUSTICOL_RUST_SOURCE` is missing, first check: + +```console +rusticol-config --include-dir +rusticol-config --library +rusticol-config --fortran-source +rusticol-config --rust-source +``` + +See [Native APIs](native-apis.md) for the complete environment contract. + +## `installed candidate wheel is stale for this checkout` + +Contributor imports bind the staged native extension to relevant checkout +sources. If Rust/native inputs or the source revision changed, refresh the +candidate once: + +```console +just dev-install +``` + +Do not copy an extension from another worktree or disable this check. Published +wheels do not use the checkout-candidate gate. + +## An example card cannot find its artifact + +Copied evaluation and benchmark cards use paths relative to the copied +workspace. Generate their prerequisite first and run from that workspace: + +```console +pyamplicol examples copy /tmp/pyamplicol-examples --force +cd /tmp/pyamplicol-examples + +pyamplicol generate_pp_zjj_from_ufo_sm.toml +pyamplicol evaluate_total.toml +pyamplicol evaluate_resolved.toml +pyamplicol benchmark.toml +``` + +Generating `qq_z6g_compiled_jit_o3.toml` creates a different artifact and does +not satisfy `benchmark.toml`'s `artifacts/pp_zjj` prerequisite. + +List the packaged names and their actions before running one: + +```console +pyamplicol examples list +pyamplicol examples run --help +``` + +See [Examples Gallery](examples-gallery.md). + +## “no model-supported tree-level amplitudes” + +pyAmpliCol generates tree-level amplitudes. The primary `p p > Z j j` +expansion encounters `g g > Z g g`, which is loop-induced in the Standard +Model. It is reported and omitted while the supported tree-level channels are +retained: + +```text +Skipped 1 concrete subprocess with no model-supported tree-level amplitudes: +p_p_to_z_j_j_8 (g g > Z g g) +``` + +That warning is expected for the inclusive example. A request containing only +the loop-induced process has no supported representative and fails. Choose a +tree-level process or a model that provides the required tree-level amplitude; +pyAmpliCol is not silently performing a loop calculation. + +## Recurrence, eager, or OTF says a prepared model is missing + +Recurrence, eager, and OTF use prepared local kernels. A raw UFO directory, +serialized model JSON, or compiled model IR is not itself a prepared kernel +bundle. + +Either compile a prepared bundle: + +```console +pyamplicol model compile models/json/sm/sm.json \ + models/sm-jit-o2.pyamplicol-model \ + --backend jit --jit-optimization-level 2 +``` + +and use it: + +```console +pyamplicol generate 'd d~ > z g g' artifacts/builtin_ddbar_to_zgg \ + --model models/sm-jit-o2.pyamplicol-model \ + --execution-mode recurrence +``` + +or select process-local compiled mode explicitly for raw/compiled model input: + +```console +pyamplicol generate --card run.toml --execution-mode compiled +``` + +The installed `built-in-sm` source automatically selects its packaged JIT O2 +prepared bundle. There is no hidden recurrence-to-compiled fallback. + +## A process expression does not resolve + +Inspect the stored representatives: + +```console +pyamplicol inspect artifacts/pp_zjj +``` + +The runtime permits a unique permutation within each side of `>`: + +```console +pyamplicol inspect artifacts/pp_zjj --process 'd d~ > g z g' +``` + +It does not move an incoming particle to the outgoing side. If several +representatives have the same incoming/outgoing multisets, inference is +ambiguous and the error lists their stable IDs. Select one of those IDs +explicitly. + +## Wrong kinematic shape or ordering + +Runtime arrays have shape `(point, external particle, [E, px, py, pz])`. +Standalone `--kinematics` files contain exactly one `[external][4]` point or a +singleton batch around it. Legs follow the expression passed in `--process`, +including inferred permutations. + +Use `runtime.physics.external_particles` or: + +```console +pyamplicol inspect artifacts/pp_zjj --process 'd d~ > g z g' +``` + +to confirm public order. JSON booleans, non-finite values, multiple points, and +wrong ranks are rejected. Decimal strings are preserved only by Python exact +evaluation; native APIs convert to f64. + +## Color-flow selection is rejected + +Physical color-flow selection is an LC capability. NLC and full color are +contracted and expose one output color slot per helicity, not a selectable LC +flow. + +```python +print(runtime.physics.selector_capabilities) +print(runtime.physics.color_flow_ids) +``` + +Use only helicity selectors for NLC/full. See [Runtime and Selectors](runtime-and-selectors.md). + +## Target or CPU-feature incompatibility + +Inspect the target: + +```console +pyamplicol inspect artifacts/my_process +rusticol-config --target +``` + +Compiled all-JIT O1/O2 process artifacts report `portable-64le` and can move +between supported x86-64 and arm64 little-endian hosts. Prepared eager and +recurrence artifacts use the exact-O2 portable pack contract. JIT O0/O3, C++, +and ASM artifacts are target-specific and may require particular CPU features. +Regenerate a target-specific artifact on the destination, or use a portable +JIT configuration. See [Artifacts and Portability](artifacts-and-portability.md). + +## Artifact loading or an explicit checksum audit fails + +Treat a size/digest/path mismatch as a changed or incomplete artifact. Do not +edit `artifact.json` or payloads by hand. Restore the complete directory from a +trusted channel or regenerate it. + +Normal runtime loading does not hash every payload. If you intentionally ran +the explicit full audit, remember it may read very large PACBIN files once: + +```python +from pyamplicol.artifacts import load_manifest, validate_payloads + +manifest = load_manifest("artifacts/my_process") +validate_payloads(manifest) +``` + +Do not put this full rehash in every evaluation loop. + +## Generation appears to repeat expensive validation + +Generation-time validation remains enabled and checks construction as it is +built. The separate **post-build** runtime reopen is off by default because it +does not change the completed artifact and can be expensive for large resolved +axes. + +Check the resolved card: + +```console +pyamplicol config resolve run.toml +``` + +`generation.validation.post_build_validation` should be `false` unless you +explicitly requested `--post-build-validation`. The sample count alone does not +enable the reopened pass. + +## Symbolica license or restricted-mode messages + +Run: + +```console +pyamplicol doctor +``` + +Importing pyAmpliCol and using direct f64 runtime evaluation do not import +Symbolica. Model compilation and generation use Symbolica lazily. Without a +valid license, eligible users may continue under Symbolica's current restricted +terms, and pyAmpliCol clamps generation to one worker and one Symbolica core. + +Python precision other than 16 also loads retained Symbolica state. Native +languages are f64-only and never perform a Symbolica license check. See +[Symbolica and Licensing](symbolica-and-licensing.md). + +## A profiling campaign seems to have lost earlier results + +Running without `--continue-across-revisions` does **not** delete old attempts. +It limits active planning to the current source cohort. To extend a campaign +across source changes: + +```console +./steer_performance_campaign.py run \ + --continue-across-revisions \ + ...ordinary selectors... +``` + +Compatible historical currents can then remain while new work uses the active +revision. Numerical authorities required by new current-revision work are +replanned safely. + +The command that resets state is: + +```console +pyamplicol profiling-campaign copy DEST --force +``` + +It resets only managed state below that destination, and refuses while a +campaign is active. See [Profiling Campaigns](profiling-campaigns.md). + +## The campaign reads the wrong directory after a rename + +All state is local to `DEST/campaign_artifacts/`. If a directory was renamed +while the shell remained inside the old inode, the prompt may show a logical +name that no longer matches the physical directory. + +```console +pwd +pwd -P +``` + +Start a new shell or `cd` through the campaign's current absolute path. Current +launchers reject a stale logical/physical mismatch before opening state. + +Move the whole campaign directory; do not copy individual currents or +coordination files. + +## A campaign reports `unverified` + +`unverified` is intentional for a compiled/eager timing measured without an +independent successful recurrence or original-AmpliCol authority. It is not an +OK current, is excluded from best-mode ratios/summaries, and is automatically +retryable after authority becomes available. + +By default, ordinary selection adds the available authority closure. If you +used `--no-dependencies-added`, rerun without it, or explicitly select the +authority and candidate. Replay exact IDs from: + +```console +./steer_performance_campaign.py run \ + --cell-id-file campaign_summary_ids/unverified.txt +``` + +## A campaign is slow to render its PDF + +`refresh-pdf` scans compact current records and displays a colored progress +bar. It does not need to hash or load every retained heavy attempt. + +```console +./steer_performance_campaign.py refresh-pdf +``` + +Use `--quiet` only when progress and LaTeX output should be suppressed. If the +scan is unexpectedly dominated by old payloads, confirm you are using the +current copied controller and that state is below `campaign_artifacts/`, not a +legacy campaign layout. + +## Memory use while loading a large JIT artifact + +PACBIN-backed runtimes snapshot the container into anonymous read-only memory. +The handle therefore needs RAM or swap comparable to the PACBIN size for its +lifetime. This protects a running evaluator from later path replacement or +truncation. + +If that footprint is unsuitable, generate a smaller process set or a more +specialized artifact. Do not assume the container will be lazily read from a +mutable file. + +## Reporting a reproducible issue + +Include only the smallest evidence that identifies the failing boundary: + +```console +python -c 'import pyamplicol; print(pyamplicol.__version__)' +pyamplicol doctor --json +pyamplicol inspect PATH_TO_ARTIFACT --json +``` + +Also include: + +- the exact command and run card; +- host OS/architecture and Python version; +- execution mode, backend, color accuracy, and target; +- the complete error, not only its final status code; +- for a campaign, the canonical cell ID and compact worker log/result path; +- whether the artifact is reproducible from a public example. + +Do **not** upload proprietary UFO code, license keys, or an untrusted native +artifact publicly. Open issues through [Release and Support](release-and-support.md). diff --git a/pyamplicol/docs/user/ufo-model-coverage.md b/pyamplicol/docs/user/ufo-model-coverage.md new file mode 100644 index 0000000000..ba27450c7c --- /dev/null +++ b/pyamplicol/docs/user/ufo-model-coverage.md @@ -0,0 +1,235 @@ +--- +title: "UFO Model Coverage" +nav_order: 2 +parent: "Configuration" +--- + +# UFO Model Coverage + +This page is the exact reference of what a generic UFO (or serialized JSON) +model may contain for pyAmpliCol, what is accepted only under a condition, and +what is rejected or ignored. Every row was derived from the code of +pyAmpliCol 1.0.0 with ufo-model-loader 1.0.0 and Symbolica 3.0.0; the paper +summarises it in one table. Read +[Models and Processes](models-and-processes.md) first for the workflow, and +use `pyamplicol model inspect ` to see the issues reported for your +own model. + +Status words used below: + +| Status | Meaning | +| --- | --- | +| supported | accepted without further condition | +| conditional | accepted when the stated condition holds, otherwise the stated diagnostic | +| experimental | accepted, but the path is not validated against a reference | +| rejected | refused with the stated diagnostic | +| ignored | loads without error but never affects generated amplitudes | + +## Summary + +| Area | Supported | Rejected or restricted | +| --- | --- | --- | +| Input | UFO directories (trusted Python), serialized JSON, compiled IR files, prepared kernel bundles | anything else; ufo-model-loader older than 1.0.0 | +| Fields | scalars, Dirac fermions, vectors, massless spin-2 (validated); massive spin-2 (experimental) | Majorana and fermion-number-violating fermions, spin 3/2, higher spins; ghosts and Goldstones only internally | +| Colour | one SU(3) with 1, 3, 3̄, 8; external coloured legs are triplet fermions or octet vectors | sextets, other groups, N_c other than 3, d^abc, epsilon and sextet tensors | +| Lorentz | Identity, Gamma, Gamma5, ProjM, ProjP, Sigma, Metric, P; PSlash in propagators | Epsilon, C, IdentityL, form factors, functions outside the registry | +| Three-point vertices | any spins with colour 1, delta, T^a, f^abc | d^abc; one- and two-point terms are ignored | +| Four-point vertices | colour-singlet (any Lorentz structure); the two-f gauge contact; HEFT Hggg | other coloured contacts (four-fermion operators, delta couplings, d^abc) | +| Five-point and higher | colour-singlet contacts of any valence (tested to ten scalars); HEFT Hgggg | every other coloured contact | +| Propagators | standard scalar, Dirac, vector and spin-2 kernels with fixed widths; custom UFO propagators that lower exactly | complex-mass scheme; gauge choices other than Feynman (massless) and unitary (massive) | +| Parameters | external and internal, real or complex, the fixed function registry, restriction cards, runtime cards for external parameters | custom function bodies, epsilon-expanded values (finite part kept), counterterm vertices (dropped) | +| Processes | 2 to N tree level, LC, NLC and full colour, FFT contraction in the trace or adjoint basis | loop-induced channels (omitted) | + +## Preflight diagnostic codes + +`pyamplicol model inspect` lists these codes; any error code makes the model +unsupported for generation, warnings do not. + +| Code | Severity | Trigger | +| --- | --- | --- | +| `unsupported-spin` | error | a particle spin code outside -1, 1, 2, 3, 5 | +| `unsupported-color-representation` | error | a particle colour code outside 1, 3, -3, 8 | +| `majorana-fermion` | error | a spin-1/2 particle whose name equals its antiname | +| `form-factors` | error | the model declares any form factor | +| `function-arity` | error | a declared function with a registry name but a different argument count | +| `unknown-functions` | error | a function name outside the registry and the UFO tensor heads in any parameter, coupling, propagator, Lorentz or colour expression | +| `unsupported-contact-color-lowering` | error | a vertex with more than three legs whose colour structure could not be decomposed into contact kernels (reported after tensor lowering) | +| `experimental-massive-spin-2` | warning | a spin-2 particle whose mass is not `ZERO` | + +Some rejections happen at model compilation rather than at preflight and +carry a `ValueError` message instead of a code; they are marked below. + +## Sources and loading + +| Feature | Status | Condition and consequence | +| --- | --- | --- | +| Source kinds | supported | `built-in-sm`, `built-in-sm-heft`, a UFO directory containing `__init__.py`, a `.json` model in ufo-model-loader's schema, a compiled IR file `*.pyAmplicol-model.json`, or a prepared bundle `*.pyamplicol-model`. A directory without `__init__.py`, a missing path or unreadable JSON is a `ValueError`. | +| Built-in selector spelling | conditional | Use `built-in-sm`; the `builtin_sm` spelling is accepted by the config resolver but not by the typed model layer. | +| UFO Python execution | supported | A UFO directory is imported as Python with your privileges; only load trusted models and prefer the JSON form for automation. During the import the `UFO_SCALARS_MODEL_*` and `UFO_GRAVITY_MODEL_*` environment variables are hidden and no `.pyc` files are written. | +| ufo-model-loader version | conditional | External UFO and JSON models require an installed release 1.0.0 or newer (development builds are refused with a `RuntimeError`); built-in models do not need it. | +| Required UFO attributes | conditional | `all_orders`, `all_parameters`, `all_particles`, `all_lorentz`, `all_couplings` and `all_vertices` must exist; `all_propagators`, `all_functions`, `all_form_factors` and `all_CTparameters` are optional; objects of class `CTVertex` are dropped silently. | +| Serialized JSON schema | conditional | The file must end in `.json` and provide `name`, `restriction`, `orders`, `parameters`, `particles`, `propagators`, `lorentz_structures`, `couplings` and `vertex_rules`. | +| Loader errors | conditional | Errors raised inside ufo-model-loader (missing restriction file, external parameter without default, malformed JSON) surface as raw Python tracebacks, not as pyAmpliCol `ModelError` messages. | +| Restriction selector | supported | `default` applies `restrict_default.dat` (UFO) or `restrict_default.json` (JSON) when present, otherwise the model defaults without zero-pruning; `none` applies no card; any other name needs `restrict_.dat` or `.json`. | +| Restriction file location | conditional | A restriction given as a path must be named `restrict_.dat` (UFO, inside the model directory) or `restrict_.json` (JSON, beside the model file); other names or locations are a `ValueError`. | +| What a restriction changes | supported | A card sets external parameter values only; `.dat` cards are SLHA parameter cards, `.json` cards map names to `[re, im]`. With `simplify=true` (default) parameters set to zero become constants and the couplings and vertices they kill are pruned. | +| Restrictions on built-in and precompiled sources | rejected | `restriction` and `simplify=false` apply only to UFO and JSON sources; built-in, compiled and prepared models refuse them. | +| Compilation cache | conditional | Compiled models are cached under `$PYAMPLICOL_CACHE_DIR/models` (or the platform cache) keyed by source contents, options and toolchain. Editing a `restrict_*.json` beside a JSON model does not change the key: pass `--no-model-cache` or the file path after editing it. | +| Dry run | conditional | `--dry-run` never compiles a UFO or JSON model; compile it or run `model inspect` first so the cache is populated. | +| Compiled IR files | conditional | Tied to the exact pyAmpliCol, Symbolica and ufo-model-loader versions that wrote them; regenerate after any upgrade. | +| Prepared bundles | conditional | Verified member by member (sizes and SHA-256) on every load; must match the current bundle schema and model-compiler version. | +| Parameter card | supported | `CompiledModel.write_parameter_card()` writes the runtime-adjustable external parameters after the restriction as `{name: [re, im]}`; parameters fixed to zero by the restriction are absent and immutable. | +| Model CLI | supported | `pyamplicol model inspect` loads even unsupported models and lists every issue; `model compile` and `model processes` refuse unsupported models. There is no separate preflight subcommand. | + +## Particles + +| Feature | Status | Condition and consequence | +| --- | --- | --- | +| Spin codes | supported | -1 (ghost), 1 (scalar), 2 (Dirac fermion), 3 (vector), 5 (spin-2); any other code is `unsupported-spin`. | +| Spin 3/2 and higher | rejected | `unsupported-spin`; no downstream path exists. | +| Scalars | supported | standard propagator i/(p^2 - m^2 + i m Gamma). | +| Dirac fermions | supported | particle and antiparticle must be distinct; standard massive or massless Dirac propagator. | +| Massless fermions | conditional | Treated as two-component Weyl spinors when the mass is literally `ZERO` or an internal parameter equal to zero and the propagator is the loader default; an external mass parameter that merely defaults to zero keeps four-component currents. | +| Majorana fermions | rejected | `majorana-fermion` when name equals antiname; fermion-number-violating flow is not implemented. | +| Vectors | supported | massless vectors get helicities -1, +1 and Feynman gauge; massive vectors get -1, 0, +1 and unitary gauge, decided from the restricted default mass at compile time. | +| Massless spin-2 | supported | 16-component tensor, helicities -2 and +2 only, de Donder propagator; validated by the packaged scalar-gravity model. A model without its own spin-2 propagator must define the `dim` parameter used by the loader default. | +| Massive spin-2 | experimental | accepted with `experimental-massive-spin-2`; the Fierz-Pauli propagator and five helicities are implemented but only the propagator projector is unit-tested; such a model must declare its own propagator because the loader has no default. | +| Ghosts | ignored | any particle with a nonzero ghost number loads, but it can never be external, is excluded from every current, and has no propagator kernel. | +| Spin -1 without ghost number | rejected | classified as a non-external auxiliary whose propagator has no contract; model preparation fails when it is needed. | +| Colour codes | supported | 1, 3, -3, 8; anything else is `unsupported-color-representation`. Colour always means SU(3) with N_c = 3. | +| Coloured external states | conditional | an external coloured leg must be a triplet or antitriplet fermion or an octet vector; coloured scalars, octet fermions and coloured tensors are refused when a process names them (`ValueError`). | +| External-state eligibility | supported | a particle can be named in a process only if it has positive spin, zero ghost number, is not a Goldstone, is propagating and is not a compiler auxiliary; names resolve exactly, then case-insensitively when unique. | +| Particle identity | supported | antiname must exist and be involutive, non-self-conjugate pairs use opposite-signed PDG codes, names and PDG codes are unique, self-conjugate particles are neutral. | +| Mass | conditional | `ZERO` or a model parameter; the massless or massive class (helicities, gauge, Weyl projection) is fixed once at compile time from the restricted default and cannot be changed by a runtime card. | +| Complex masses and widths | rejected | mass and width values must be real; there is no complex-mass scheme. | +| Width | supported | `ZERO` or a parameter; enters only the fixed-width denominator of massive propagators and can be overridden at runtime. | +| Goldstone bosons | conditional | must be propagating scalars; matched to the unique massive vector with identical colour, quantum numbers and mass and then absorbed into its unitary-gauge propagator; an ambiguous match is an error, an unmatched Goldstone stays an internal scalar. | +| Propagating flag | supported | non-propagating particles cannot be external but are compiled normally. | +| Electric charge | supported | recorded as a bookkeeping quantum number only; not used to check charge conservation. | +| Other attributes | ignored | `texname`, `antitexname`, `line`, `counterterm`, `LeptonNumber` and `Y` never affect amplitudes. | +| Custom propagator attribute | conditional | a particle whose propagator differs from the loader's Feynman default is used as written and loses the Weyl projection and Goldstone absorption. | + +## Parameters, couplings and functions + +| Feature | Status | Condition and consequence | +| --- | --- | --- | +| Parameter nature | supported | `external` (runtime input) or `internal` (substituted symbolically); the common `interal` typo is corrected; anything else fails in the loader. | +| Parameter type | supported | `real` or `complex`; complex external parameters are set as `[re, im]`; a real parameter can never receive an imaginary part. | +| External defaults | conditional | every external parameter needs a value in the UFO or in the selected restriction card. | +| Expression syntax | conditional | standard UFO spellings only: `cmath.sqrt`, `cmath.pi` or `pi`, `cmath.sin/cos/asin/acos`, `complex(x, y)`, `complexconjugate`, `cond`, `Theta`, `reglog`, bare `exp`, `log`, `tan`, `atan`, and `**`; other `cmath.`, `math.` or `numpy.` calls fail to load. | +| Scientific notation | rejected | a decimal mantissa with an exponent such as `1.5e-3` is mis-parsed by the loader; write `0.0015`, `15e-4` or a fraction. | +| Rational exponents | supported | `x**0.5` is evaluated exactly as `x^(1/2)`. | +| Parameter dependencies | supported | internal parameters may reference each other in any order; cycles are rejected. | +| Function registry | conditional | one-argument `Theta`, `abs`, `acos`, `acosh`, `acsc`, `asec`, `asin`, `asinh`, `atan`, `atanh`, `complexconjugate`, `conj`, `cos`, `cosh`, `csc`, `exp`, `im`, `log`, `log10`, `re`, `reglog`, `reglogm`, `reglogp`, `sec`, `sin`, `sinh`, `sqrt`, `tan`, `tanh`; two-argument `complex`, `pow`; three-argument `cond`, `if`. Anything else is `unknown-functions`. | +| Evaluable subset | conditional | `re`, `im`, `log10`, `pow`, `reglogp` and `reglogm` pass preflight but have no runtime evaluator and fail when a parameter that uses them is evaluated; all other registry functions compute. | +| Declared functions | conditional | a declared `Function` is accepted only if it is a registry name with the registry arity; its body is ignored in favour of the built-in meaning (`function-arity` otherwise). | +| Constants | conditional | `pi` stays exact until numerical evaluation and `complex(0, 1)` is the exact imaginary unit; a bare `I` symbol is not supported. | +| Epsilon-expanded values | ignored | dictionary-valued parameters and couplings keep only the order-0 entry, with one loader warning. | +| Counterterms and decays | ignored | counterterm parameters are ordinary parameters, `CTVertex` objects are dropped, the decays block is never read. | +| Coupling records | supported | any expression in the parameters, any number of coupling-order names per coupling. | +| Coupling matrix | supported | one row per colour structure and one column per Lorentz structure; `None` entries are skipped; each cell becomes its own term and pieces with different orders are counted separately. | +| Coupling orders | conditional | `hierarchy` weights the minimal coupling-order policy; `expansion_order` is stored but has no effect; undeclared order names default to weight 1. | +| Coupling-order policies | supported | `minimal` (default) keeps the lowest hierarchy-weighted order; `explicit` applies only `--max-coupling-order NAME=N`, and misspelled names are not detected. | +| Runtime parameter cards | supported | flat JSON objects of finite real or `[re, im]` values via `--model-parameters`, `Runtime.load(model_parameters=...)` or `set_model_parameters`; an invalid batch leaves the runtime unchanged. | +| Runtime mutability | conditional | only surviving external parameters are mutable; derived parameters and couplings are recomputed and read-only. In recurrence mode an external parameter that the process never uses may be rejected as not used. | +| Masses and widths at runtime | conditional | external mass and width parameters can be varied, but a particle cannot switch between the massless and massive class without regenerating. | +| Numerical precision | supported | model constants are kept exact in the compiled model; only user-supplied values are binary64. | + +## Lorentz structures and vertices + +| Feature | Status | Condition and consequence | +| --- | --- | --- | +| Normalized tensor heads | supported | `Identity(i, j)`, `Gamma(mu, i, j)`, `Gamma5(i, j)`, `ProjM(i, j)`, `ProjP(i, j)`, `Sigma(mu, nu, i, j)`, `Metric(mu, nu)`, `P(mu, k)` with the standard UFO argument conventions; `PSlash(i, j)` only in its two-argument propagator form. | +| Other tensor heads | rejected | `Epsilon`, `EpsilonBar`, `C`, `IdentityL`, three-argument `PSlash` and any accepted head with a wrong arity fail model compilation with `ValueError: ... contains unsupported UFO tensors`. | +| Form factors | rejected | `form-factors`. | +| Sigma normalisation | experimental | expanded as (i/2)(gamma^mu gamma^nu - gamma^nu gamma^mu); vertices using `Sigma` have not been validated against a reference implementation. | +| Chiral structures | supported | `ProjM`, `ProjP` and `Gamma5` are validated through the packaged Standard Model. | +| Index conventions | conditional | 1-based leg indices, the `1000*component + leg` notation for spin-2 fields and negative dummy indices are supported; inconsistent indices are a `ValueError`. | +| Several structures per vertex | supported | any number of colour and Lorentz structures through the coupling matrix. | +| Three-point vertices | supported | any combination of scalars, Dirac fermions, vectors and spin-2 fields, momentum-dependent or not, with colour 1, delta, T^a or f^abc. | +| One- and two-point terms | ignored | tadpole and two-point mixing vertices are dropped without a warning. | +| Four-point colour-singlet contacts | supported | momentum-independent ones through the exact component-basis split; momentum-dependent ones through balanced contact trees. | +| Four-point two-f gauge contacts | conditional | accepted when written as a numeric factor times `f(a, b, x) * f(x, c, d)` with one shared adjoint index and no momentum dependence; otherwise `unsupported-contact-color-lowering`. | +| Other coloured four-point contacts | rejected | four-fermion operators, delta couplings to singlets, `d^abc` and single-f shapes outside the HEFT family give `unsupported-contact-color-lowering`. | +| Higher-point colour-singlet contacts | supported | any valence, momentum-dependent or not, tested up to ten scalars and five-point spin-2 vertices. | +| Scalar-HEFT contacts | conditional | four-point (8, 8, 8, 1) with colour exactly `f(i, j, k)` and five-point (8, 8, 8, 8, 1) with colour exactly `f(...) * f(...)` sharing one index, without a numeric prefactor in the colour string. | +| Other coloured contacts with five or more legs | rejected | `unsupported-contact-color-lowering`. | +| Four-fermion contacts | experimental | not gated at compile time but not validated; treat as unsupported. | +| Counterterm and loop vertices | ignored | only tree vertices are used; `type` and `loop_particles` attributes are never read. | +| Ghost vertices | ignored | compiled but never used. | +| Custom propagator tensors | conditional | the vertex tensor heads plus `PSlash(i, j)`, `P(1)` and `P(1)^2` for the propagating momentum. | + +## Colour structures and colour accuracy + +| Feature | Status | Condition and consequence | +| --- | --- | --- | +| Colour group | rejected | one SU(3) with N_c = 3 only; a second group or SU(N) with N other than 3 is not representable. | +| `1` | conditional | accepted for any valence when every leg is colour-neutral; on a vertex with coloured legs it is rejected. | +| `Identity(i, j)` | conditional | both indices must point at coloured legs of dual (or equal adjoint) representations. | +| `T(a, i, j)` | supported | adjoint, fundamental, antifundamental index order. | +| `f(a, b, c)` | supported | three adjoint legs or dummies; a product of two `f` is the only coloured four-point structure. | +| `d(a, b, c)` | rejected | recognised, then refused at compilation as an unsupported trilinear tensor or contact lowering. | +| `Epsilon`, `EpsilonBar`, `K6`, `K6Bar`, `T6` | rejected | pass the preflight name check but fail model compilation with `ValueError: color expression contains unsupported UFO tensors`. | +| Three-point projection | conditional | every three-point colour structure must be exactly proportional to 1, delta, T^a or f^abc with matching leg representations; the constant may be any number. | +| Colour accuracies | supported | `lc`, `nlc` and `full` for any accepted model; the choice is a run-card setting. | +| NLC and full colour weights | supported | exact for any number of quark lines; pure-gluon processes are exact up to 40 gluons. | +| Colour-charge balance | conditional | after crossing, fundamental and antifundamental legs must balance; otherwise the process has no colour plan. | +| Sector truncation | conditional | `process.max_color_sectors` produces a partial sum marked `selected` with direct contraction and is refused by on-the-fly, FFT and correlators. | +| Colour-flow selectors | conditional | LC outputs only; NLC and full outputs return one contracted entry per helicity. | +| `all-flow-union` layout | conditional | LC only, with complete coverage. | +| FFT contraction | conditional | Default `contraction="auto"` tries FFT for `nlc` or `full` with `recurrence` or `on-the-fly`, falling back to direct contraction for unsupported plans. FFT needs at most ten permutable gluons whose colour sectors form complete permutation orbits. LC, compiled/eager execution, and correlators retain direct/trace; `--color-contraction direct` opts out. Explicit `--fft trace` or `--fft adjoint` forces FFT and retains unsupported-plan errors. | +| Adjoint FFT selection | conditional | Default `fft_basis="adjoint"` adaptively uses DDM for certified Yang–Mills or single-insertion scalar HEFT (`HIG = 1`), fundamental chains/products for quarks, and trace tensors otherwise. Saved `fft_basis_selection` reports the actual representation; no universal DDM or speedup claim. Forced adjoint FFT with correlators is rejected; direct contraction ignores the FFT basis. | +| Shared-trace optimisation | conditional | applied only to pure gauge-boson processes whose Yang-Mills structure the model certificate proves; results are exact either way. | +| Correlators | conditional | representations 1, 3, -3, 8 from complete full-colour plans in compiled execution. | + +## Propagators and gauge + +| Feature | Status | Condition and consequence | +| --- | --- | --- | +| Default versus custom | supported | a UFO propagator counts as custom only if its expression differs from the loader's Feynman default for that particle. | +| Massless vectors | supported | Feynman gauge -i g/p^2, fixed. | +| Massive vectors | supported | unitary gauge with a fixed-width denominator; matched Goldstones are absorbed. | +| Feynman-gauge massive vectors | not applicable | no such kernel exists; Goldstone exchange appears only if the UFO supplies a custom massive-vector propagator, which is then used verbatim. | +| Massless spin-2 | supported | de Donder propagator with dimension parameter `dim` (default 4). | +| Massive spin-2 | experimental | Fierz-Pauli propagator behind `experimental-massive-spin-2`. | +| Widths | supported | fixed-width Breit-Wigner denominators only; zero widths are fine; complex masses are rejected. | +| Mass class | conditional | fixed by the restriction used at generation; runtime cards change values within that form, and setting a massive particle's mass to zero at runtime is not guarded. | +| Custom propagators | conditional | numerator and denominator must both be defined, use only the accepted tensors and registered functions, and lower exactly to the particle's component count; no gauge conversion is applied. | +| Disabled shortcuts | conditional | a custom propagator switches off the Weyl projection, transverse Yang-Mills lowering, Fierz auxiliary currents, reflection certificates and Goldstone removal for that particle. | +| Custom propagator coverage | conditional | plumbed through all execution modes but not exercised end to end by the test suite. | +| Ghost propagators | ignored | none; ghosts never propagate in tree amplitudes. | +| Non-propagating particles | conditional | excluded from external states but not from internal lines. | +| Gauge selection | not applicable | no option selects a gauge; the UFO must supply its own propagator for another choice. | +| Contact auxiliaries | supported | synthesized auxiliary currents carry no propagator and introduce no pole. | + +## Process-level scope with a generic model + +| Feature | Status | Condition and consequence | +| --- | --- | --- | +| Model inputs at generation | supported | any accepted source kind; restriction and simplification choices apply only when compiling from UFO or JSON. | +| Preflight gate | rejected | a model with any error code cannot generate or enumerate processes. | +| Process shape | supported | `a b > c d ...` with exactly two incoming and at least one outgoing particle; several requests may be joined with `\|`. | +| Loop-induced channels | rejected | tree level only; a channel without tree amplitudes is skipped with a warning inside a multiparticle request and is an error when requested alone. | +| Multiparticle labels | conditional | `all` always exists; `p` and `j` exist only when the model has massless triplet fermions or octet vectors; define your own labels otherwise. | +| Repetition syntax | conditional | write `3*g`; the built-in shorthands `3g` and `[d g]` are not understood by generic expansion. | +| `flavor_scheme` | ignored | no effect on generic models; use multiparticle labels instead. | +| `max_quark_lines` | conditional | bounds the number of external quark-antiquark pairs per subprocess; channels above it are dropped like loop-induced ones. | +| Permutation representatives | supported | one generated representative serves every reordering within the incoming and within the outgoing side. | +| Basis reductions | conditional | trace-reflection folding and the shared single-trace basis are applied only to certified pure Yang-Mills gluon processes; the unreduced exact basis is used otherwise. | +| Execution modes | conditional | `compiled` works from a raw UFO or JSON model; `recurrence` (default), `eager` and `on-the-fly` need a prepared bundle: `pyamplicol model compile MODEL out.pyamplicol-model --backend jit`. | +| Prepared-bundle clamps | conditional | the bundle fixes the backend and code-shaping settings; differing run-card values are overridden with a warning. | +| Backends | supported | `jit`, `asm`, `cpp` for compiled mode; only JIT artifacts support arbitrary precision. | +| Precision | conditional | `precision=16` needs no Symbolica; higher precision needs the Symbolica package and a JIT-backed recurrence, compiled or eager artifact, never on-the-fly. | +| Symbolica at generation | supported | generation from any model needs Symbolica; double-precision evaluation of a finished artifact does not. | +| Helicity selectors | supported | any generated helicity, by stable ID, for every accuracy and mode. | +| On-the-fly | conditional | keeps every selector for runtime, double precision only, one-point warm-up required. | +| Correlated Born evaluations | conditional | compiled full-colour generation with complete coverage; spin replacements on vector legs only. | +| Numerical current reuse | conditional | off by default; `--numerical-current-reuse` enables the certified search for compiled, eager and recurrence generation. | + +## Related pages + +- [Models and Processes](models-and-processes.md) for the model workflow and + the `model` commands. +- [Configuration](configuration.md) for the run-card fields referenced above. +- [Release and Support](release-and-support.md) for the validated release + boundary. diff --git a/pyamplicol/docs/user/umami-api.md b/pyamplicol/docs/user/umami-api.md new file mode 100644 index 0000000000..bd9297ac3e --- /dev/null +++ b/pyamplicol/docs/user/umami-api.md @@ -0,0 +1,323 @@ +--- +title: "UMAMI and MadSpace" +nav_order: 3 +parent: "Python API" +--- + +# UMAMI and MadSpace + +The generated `API/umami` SDK exports a matrix-element shared library implementing +the [Unified MAtrix eleMent Interface (UMAMI)](http://docs.madgraph.org/latest/madspace/umami-api.html). +It lets a C/C++ executable or MadSpace evaluate a pyAmpliCol artifact through the +same Rusticol runtime used by the other [native APIs](native-apis.md). +The library needs no Python interpreter at evaluation time. + +This interface is being developed on the `umami` branch. Use that branch's +matching generation code and installed native SDK; it is not present in an +older released wheel. + +`git pull` updates the checkout, not a non-editable installed package. Check the +Python interpreter and exporter actually being imported with: + +```console +python -c "import sys; import pyamplicol.artifacts.umami as umami; print(sys.executable); print(umami.__file__)" +``` + +To rebuild and reinstall the current branch into `.venv`, run the repository's +existing `just dev-install` workflow from a clean checkout. Python-only changes +can instead use `env PYTHONPATH="$PWD/src" .venv/bin/python ...` from the repository +root when a matching native runtime is already staged; they do not require a +new Rusticol build. See the +[example README](https://github.com/MadGraphTeam/pyamplicol/tree/umami/umami_example#updating-a-checkout-or-stale-environment) +for complete commands. + +## Generate and build + +Ordinary generation includes the API bundle by default: + +```console +pyamplicol generate 'g g > g g g' ./artifacts/gg_ggg \ + --model built-in-sm --color-accuracy full +make -C artifacts/gg_ggg/API/umami +make -C artifacts/gg_ggg/API/umami run +``` + +The generated directory contains `umami.h`, the adapter and provider tables, a +Makefile, `umami_driver.c`, `metadata.json`, and usage/licensing information. +The Makefile obtains headers and native link arguments from the active +pyAmpliCol environment: + +```console +rusticol-config --cflags +rusticol-config --libs +``` + +If necessary, select the installation and build directory explicitly: + +```console +make -C /absolute/artifact/API/umami \ + RUSTICOL_CONFIG=/absolute/venv/bin/rusticol-config \ + BUILD_DIR=/absolute/build/umami +/absolute/build/umami/umami_driver /absolute/artifact +``` + +The default build directory is the sibling +`.pyamplicol-api-build//umami`, outside the immutable artifact. +A single provider produces **`libumami.so`** and **`umami_driver`** on both Linux +and macOS; the `.so` suffix is retained on macOS. Rusticol is statically included +in the provider and its symbols are hidden. The standalone driver links only +the UMAMI library, not another copy of Rusticol. It reads a saved validation +point and prints the matrix element as JSON. Optional arguments select a +zero-based channel and flavour: + +```console +/absolute/build/umami/umami_driver /absolute/artifact 0 0 +``` + +### Link your executable + +Include the generated `umami.h` and link your executable to the generated +library. The generated Makefile and driver are complete examples. For a +single-provider library and paths without shell quoting requirements: + +```sh +# Linux: place the executable beside libumami.so. +cc -I/absolute/artifact/API/umami my_driver.c \ + -L/absolute/build/umami -lumami -Wl,-rpath,'$ORIGIN' \ + -o /absolute/build/umami/my_driver + +# macOS: the filename remains libumami.so. +cc -I/absolute/artifact/API/umami my_driver.c \ + /absolute/build/umami/libumami.so -Wl,-rpath,@loader_path \ + -o /absolute/build/umami/my_driver +``` + +Keep the executable and provider together when relocating them, and retain the +whole generated artifact and any native evaluator dependencies. `rusticol-config` +is needed to **build the provider**, not to add a second Rusticol library to its +consumer. Distribute the generated `UMAMI_LICENSE` notice with the provider; +the upstream public header retains its license, independently of the adapter's +0BSD license. + +## Initialization, inputs and normalization + +This adapter intentionally gives `param_card_path` a different meaning from the +usual UMAMI convention: pass the **absolute artifact-directory path**, not a +parameter-card file. + +```c +#include "umami.h" + +UmamiHandle handle = NULL; +UmamiStatus status = umami_initialize(&handle, "/absolute/artifact"); +if (status != UMAMI_SUCCESS) { + /* Report the error; do not evaluate this handle. */ +} +/* Evaluate using umami_matrix_element, then release the handle. */ +umami_free(handle); +``` + +Each handle starts from the saved model defaults and owns independent mutable +state. Use a separate handle per thread. The provider checks that the loaded +artifact matches the one used to generate its tables. Metadata is available before +initialization because it is compiled into the library. + +Momenta have components `(E, px, py, pz)` and UMAMI's column-major layout: + +```text +momenta[event + stride * (particle + particle_count * component)] +``` + +`count` events are evaluated beginning at `offset`, with +`offset + count <= stride`. Scalar input and output arrays use the same event +indices. Values outside the requested output slice are untouched. Inputs must +be finite and indices valid. Check every returned `UmamiStatus`. + +| Input | Meaning | +| --- | --- | +| `UMAMI_IN_MOMENTA` | Required momenta in the physical external order from metadata. | +| `UMAMI_IN_CHANNEL_INDEX` | Zero-based index into `channels`; defaults to zero. | +| `UMAMI_IN_FLAVOR_INDEX` | Zero-based index into that channel's `processes`; defaults to zero. | +| `UMAMI_IN_ALPHA_S` | Optional per-event strong coupling, only when the model supplies an unambiguous declaration. | +| `UMAMI_IN_RANDOM_HELICITY` | A number in `[0,1)` selecting the returned helicity index with probability proportional to its contribution. | +| `UMAMI_IN_HELICITY_INDEX` | Evaluates only this exported helicity, an index into the metadata `helicities`. | + +The matrix-element output sums the artifact's **available helicities**, unless +`UMAMI_IN_HELICITY_INDEX` selects one of them. A +generation restricted to one helicity is therefore not an unpolarized provider; +consult `runtime_processes[].coverage` and `helicity_ids` in the metadata. +The result already includes Rusticol's incoming spin/colour averaging and +identical-final-state normalization. Do not apply these factors again. + +LC channels select additive physical colour contributions. NLC/full-colour +channels return the already contracted colour result, not a separately sampled +colour flow. In particular, the absence of an LHE colour-flow reference for a +contracted result is intentional. Metadata integration multiplicities are +**not** silently included in `UMAMI_OUT_MATRIX_ELEMENT`: the integrator applies +them once when assembling physical contributions. + +The adapter reports supported and required inputs and supported outputs through +`umami_supported_inputs`, `umami_required_inputs` and +`umami_supported_outputs`. Besides matrix elements, it supplies informational +helicity indices, and colour indices for LC only. Exported helicity indices +refer to the metadata `helicities` table, with the values in the physical +external order of the momenta, and the sampled index follows the helicity +distribution of the contribution. Contracted-colour providers +do not advertise a colour-flow count or colour-index output. Random colour +selection, +diagram-resolved outputs and GPU execution are currently unsupported. Query +capabilities rather than assuming every UMAMI operation is implemented. + +### Parameters + +`umami_set_parameter` and `umami_get_parameter` use the model's parameter names. +Updates use Rusticol's validated setter and refresh dependent parameters; +immutable or invalid values are rejected. Event-wise αs overrides are temporary +and restore the persistent handle value after evaluation. + +An αs input is advertised only for an explicit model mapping or a standard UFO +external `SMINPUTS[3]` declaration. It is not guessed from particle names or +coupling powers. The built-in demonstration model has no such declaration, so +its integration uses the saved coupling defaults; a suitable UFO model can +support the optional running-coupling workflow. + +`UMAMI_META_MASSES` contains the **saved default** external masses and is not +handle-specific. An integrator using these fixed masses must not change mass +parameters without also updating its kinematics and checking the grouping +assumptions. Regenerating the provider at the desired masses is the simplest +consistent workflow. + +## Providers and generated metadata + +UMAMI metadata is fixed for a library, so one library cannot transparently +switch between incompatible external sizes or mass patterns. Compatible +processes share a provider; otherwise the SDK emits `libumami_p0.so`, +`libumami_p1.so`, and corresponding `umami_driver_p0`, etc. The root +`metadata.json` is then a provider index pointing to +`providers/p0/metadata.json`, etc. Select a matching library and metadata entry; +do not sum alternative providers without checking their process selections. + +For example: + +```console +make -C /absolute/artifact/API/umami run PROVIDER=p0 +``` + +A provider's metadata contains: + +- `provider`: fixed external size, incoming count, masses, colour accuracy and + optional αs parameter. +- `channels[].processes`: the channel/flavour selectors consumed by UMAMI, with + physical PDGs and integration weights referenced by `matrix_elements`. +- `pdg_ids`, `color_orders`, `color_flows` and `helicities`: shared physical + configuration tables. Indices are zero-based and colour-flow references can + be absent for contracted colour. +- `runtime` and `members`: the representative evaluator, certified momentum + permutations and covered physical contributions. +- `grouping` and `runtime_processes`: the assumptions and available coverage + needed to interpret those contributions correctly. + +Physical incoming PDGs remain oriented for PDF evaluation. Aliases name the +same computation; they are not automatically extra integrable contributions. +Each exported contribution uses one phase-space ordering, rather than several +alternative maps whose naive sum would double count it. + +## Automatic symmetry grouping + +UMAMI always applies proven computation reuse and integration grouping, including +equivalent contributions with different physical final-state flavours. There is +no grouping setting. Using the compact integration contributions assumes +**permutation-invariant, flavour-blind cuts and observables**, as recorded in +`grouping.assumptions`. The `grouping` object also reports physical and exported +contribution counts; it has no mode selector. + +An exporter still accepting a `grouping` argument, or saved metadata containing +`grouping.mode`, belongs to the older contract. The example scripts reject such +metadata, and the orchestrator checks the installed exporter before optional +integration imports. After updating the Python code, regenerate into a new +`--output` directory without `--reuse` and build the matching shared library. +Old JSON and `.so` pairs cannot be repaired by editing metadata counts or +multiplicities: the provider tables are compiled into the library. The example +reports actual channel, representative and physical-member counts from its +metadata. + +Integration contributions are quotiented by permutations of identical +final-state PDGs, using physical **oriented** colour topology and preserving +the available helicity coverage. There is no additional reflection quotient or +incoming-beam exchange. With complete LC coverage, `g g > g g g` therefore +exports four representatives for its 24 physical colour contributions, with +integration multiplicity six each. The full-colour result is already contracted: +it remains one contribution, with no extra factorial +multiplier. This integration multiplicity is distinct from Rusticol's existing +identical-final-state normalization. + +Equivalence is derived from model expressions and generated computation, not +equal default parameters, particle categories or a hard-coded Standard Model +process list. Unproved relations remain separate, except for the explicit +`--flavor-blind` assumption described below. Grouping never assumes equal +PDFs, exchanges the two beams, or identifies unequal mass parameters. + +A representative's weighted integral reproduces its orbit +**only under the recorded assumptions**, including cuts invariant under the +recorded permutations and blind to final-state flavour. Multiplicity times a +representative is not the pointwise sum at a given labelled momentum point. +Metadata retains all physical `members` and their momentum mappings for +pointwise reconstruction. +Arbitrary labelled-leg or species-sensitive cuts require expanding those members +and applying each cut to its physical contribution; they cannot be applied to +the compact weighted representatives. Physical incoming PDGs and their PDF +weights remain distinct. Grouping metadata does not change the native evaluator +build identity. + +### Flavour-blind members + +With `--flavor-blind`, generation keeps one representative per flavour class +and records the other processes as ungenerated members in +`process-groups.json`. The provider still exports every member, without a +`runtime_processes` row of its own: its selectors evaluate the representative's +runtime through the recorded momentum permutation, and its `members[].runtime` +names the representative. A member with the incoming PDGs of an existing +selector evaluated identically becomes one more `matrix_elements` row of that +selector, like other final-state flavours; otherwise it is a selector of its +own. + +The permutation may exchange the incoming legs, as between `u g > u g` and +`g u > u g`. This relabels the member's momenta explicitly and is not a beam +exchange assumption: the selector keeps the member's physical incoming PDGs for +its PDF weights. Members related by charge conjugation, such as `u~ g > u~ g`, +use the representative's colour flows reversed, so their LHE colour tags and +helicity labels are those of the member. + +The equality of these matrix elements is the user's `--flavor-blind` +assumption, not a proof. Every provider exporting such members records it as an +additional `grouping.assumptions` entry; `process-groups.json` keeps any +warning issued when a relabelling is not a symmetry of the model. A member +whose relabelling moves unequal masses, or maps a restricted helicity selection +onto a different set, is exported by a separate provider that loads the +representative's runtime. Processes grouped numerically were generated and keep +their ordinary export. [Process Grouping](process-grouping.md) defines the +flavour classes, the recorded relations and their warnings. + +## MadSpace example + +The repository's +[`umami_example`](https://github.com/MadGraphTeam/pyamplicol/tree/umami/umami_example) +contains close adaptations of AmpliCol's loading and integration examples. They +use the real MadSpace matrix-element loader, phase-space mappings and +differential cross section, with VEGAS from MadNIS. The default is a bounded +partonic `g g > g g g` run for LC and full colour, with no PDF downloads: + +```console +python -m pip install -r umami_example/requirements.txt +python umami_example/run.py +``` + +The orchestrator generates both artifacts, compiles the libraries and linked +drivers, checks batched UMAMI/native numerical agreement, and writes integrated +cross sections with statistical uncertainties. It disables numerical +current-relation discovery during generation. Optional PDF and neural-flow +workflows, provider selection, cuts and all commands are explained in the +example README. Those dependencies are not part of pyAmpliCol's core install. +The integration example uses MadSpace's massless two-beam scattering flux and +rejects massive incoming legs; this is a limitation of that example, not of the +UMAMI provider. diff --git a/pyamplicol/examples/NLO_limits/README.md b/pyamplicol/examples/NLO_limits/README.md new file mode 100644 index 0000000000..0950318103 --- /dev/null +++ b/pyamplicol/examples/NLO_limits/README.md @@ -0,0 +1,144 @@ +# Local NLO limit stability + +`nlo_limit_stability_tests.py` is a standalone, serial example using the public +batched correlation API. It compares the full-colour `g g > g g g g` real +matrix element with correlated `g g > g g g` Born counterterms at 1 TeV and +`alpha_s = 1/8`. It does not import the paper's study scripts. + +From a checkout with pyAmpliCol, Symbolica and Matplotlib available: + +```sh +python examples/NLO_limits/nlo_limit_stability_tests.py --self-test +python examples/NLO_limits/nlo_limit_stability_tests.py --prepare-only +python examples/NLO_limits/nlo_limit_stability_tests.py --smoke \ + --formats double double-double --oracle-precision 160 \ + --output .artifacts/nlo-limits/smoke.json +python examples/NLO_limits/nlo_limit_stability_tests.py +python examples/NLO_limits/nlo_limit_stability_tests.py --render +``` + +Run the smoke test before the full scan. Evaluation and generation run under +the repository's process-tree memory watchdog, with a **10 GiB** default limit; +generation uses one worker and one optimization core. There are no native +builds. Existing compatible process artifacts and completed result batches +are reused. `--render` only reads retained results and creates the two-panel +PDF/PNG. Defaults are under `.artifacts/nlo-limits`; `--artifact-dir`, +`--output`, and `--plot` select different locations. + +The ordinary real process retains default certified current reuse and its +normal compiled colour/helicity replay. Correlated Born generation disables +those reductions to preserve the complete source basis. The isolated precision +adapter covers the real process's exact colour-replay arithmetic as well as +the correlated sources and contractions. + +`--formats`, `--limits`, and `--exponents` select a smaller scan. The reference +defaults to 1200 decimal digits (`--oracle-precision`); a smaller smoke oracle +is allowed when it exceeds every selected target precision. A checkpoint is saved +after each completed format/limit batch. A failed real-emission batch is +bisected diagnostically, retaining its valid points; this exceptional fallback +is recorded in `real_batch`. Measured failures are preserved on resume unless +`--retry-failed` is explicit. Ordinary successful evaluation is batched. +Completing a scan requires successful oracle measurements for every requested +point; missing or failed references cause a nonzero exit after checkpointing. +Target-precision failures remain stability data, and `--render` remains +available for partial results. +Results record the Git revision, artifact IDs, batch call counts, and elapsed +times. `--prepare-only` generates/reuses the two artifacts without starting a scan. + +## Named results, not scalar correlation loops + +For each approach-point batch the script makes one Born call containing all +requested dipoles, the ordinary Born, and the literal spin projection: + +```python +from pyamplicol import CorrelatedRequest + +requests = { + "born": CorrelatedRequest("born", spin_vectors={}), + "spin": CorrelatedRequest("born", spin_vectors={3: spin_vectors_by_point}), + "T13": CorrelatedRequest("T13", spin_vectors={}), + # The study also includes the other nine off-diagonal Born dipoles. +} +results = born.evaluate_correlated_many(born_points, requests, precision=1000) +real_part = results["T13"][point_index].real +imaginary_part = results["T13"][point_index].imag +point_components = {name: values[point_index] for name, values in results.items()} +``` + +Every tuple follows the input phase-space-point order. Request labels such as +`"spin"` are user-defined output names; `"born"` and `"T13"` inside each request +are generation-time colour IDs. Empty `spin_vectors={}` explicitly requests +ordinary physical helicities, independently of any setter state. The same +fixed Born is repeated across the trajectory; the real process is evaluated +as its own batch. Checkpointed components retain both decimal parts. + +## Counterterms and mappings + +The metric is `(+,-,-,-)`. The collinear trajectory uses the inverse final-final +Catani--Seymour map at fixed `z=3/5`, with `kT=delta*1000 GeV`. One generic, +rational transverse direction is used, **without azimuthal averaging**. With +`e=k_perp/kT`, `a=z/(1-z)+(1-z)/z`, and `CA=3`, + +```text +C_collinear = (8*pi*alpha_s/s_ij) * 2*CA * [a*B + 2*z*(1-z)*B(e)] +``` + +`B(e)` is the literal vector replacement on Born leg 3. A five-gluon Born is +used so this test resolves nontrivial azimuthal spin interference. The smoke +test checks that the selected spin projection is not simply `B/2`. + +The separate soft trajectory has `q=delta*r` along a fixed wide-angle null +direction. An inverse-map recoil keeps the mapped Born fixed and all real +momenta on shell. Using mapped Born momenta `P_a` consistently, + +```text +C_soft = -8*pi*alpha_s * sum_(a g g g g", "born": "g g > g g g"} +DIPOLES = tuple(combinations(range(1, 6), 2)) +ALPHA_S = "0.125" +STUDY = { + "version": 1, + "real": EXPRESSIONS["real"], + "born": EXPRESSIONS["born"], + "sqrt_s": "1000", + "alpha_s": ALPHA_S, + "z_collinear": "3/5", + "azimuth_cos_sin": ["3/5", "4/5"], + "normalization": "initial averaged; final symmetry removed: R*24, B*6", + "difference": "F=C-R, without azimuth averaging", + "soft_kernel_momenta": "mapped Born; crossed colour charges; no extra signs", + "relation_discovery": {"real": "certified-reuse", "born": "off"}, +} + + +def transport(value): + """Decimal transport must not make Symbolica infer a smaller precision.""" + value = Decimal(value) + with localcontext() as context: + context.prec = max(80, len(value.as_tuple().digits)) + return Decimal(format(value, ".79E")) + + +class Binary64(Decimal): + """Decimal-compatible transport; every scalar operation rounds to f64.""" + + @classmethod + def operation(cls, operation, *values): + args = [float(value) for value in values] + if operation == "identity": + result = args[0] + elif operation == "add": + result = args[0] + args[1] + elif operation == "sub": + result = args[0] - args[1] + elif operation == "mul": + result = args[0] * args[1] + elif operation == "div": + result = args[0] / args[1] + elif operation == "sqrt": + result = math.sqrt(args[0]) + else: + raise ValueError(f"unknown arithmetic operation: {operation}") + if not math.isfinite(result): + raise ArithmeticError(f"non-finite binary64 {operation}") + return Decimal.__new__(cls, Decimal.from_float(result)) + + def __new__(cls, value=0): + if isinstance(value, cls): + return value + return cls.operation("identity", Decimal(value)) + + @classmethod + def from_float(cls, value): + return cls(Decimal.from_float(value)) + + def __add__(self, other): + return self.operation("add", self, other) + + __radd__ = __add__ + + def __sub__(self, other): + return self.operation("sub", self, other) + + def __rsub__(self, other): + return self.operation("sub", other, self) + + def __mul__(self, other): + return self.operation("mul", self, other) + + __rmul__ = __mul__ + + def __truediv__(self, other): + return self.operation("div", self, other) + + def __rtruediv__(self, other): + return self.operation("div", other, self) + + def __neg__(self): + return Decimal.__new__(type(self), Decimal.copy_negate(self)) + + def __abs__(self): + return Decimal.__new__(type(self), Decimal.copy_abs(self)) + + def __pos__(self): + return self + + def __pow__(self, exponent, modulo=None): + if modulo is not None or int(exponent) != exponent: + raise TypeError("study arithmetic supports integer powers only") + exponent = int(exponent) + if exponent < 0: + return type(self)(1) / self ** (-exponent) + result, factor = type(self)(1), self + while exponent: + if exponent & 1: + result *= factor + exponent >>= 1 + if exponent: + factor *= factor + return result + + def sqrt(self, context=None): + return self.operation("sqrt", self) + + +class DoubleDouble(Binary64): + """Actual Symbolica DoubleFloat arithmetic, with 31-digit transport.""" + + evaluators: ClassVar[dict] = {} + + @classmethod + def operation(cls, operation, *values): + if operation not in cls.evaluators: + from symbolica import E, S + + expression, names = { + "identity": ("x", ["x"]), + "add": ("x+y", ["x", "y"]), + "sub": ("x-y", ["x", "y"]), + "mul": ("x*y", ["x", "y"]), + "div": ("x/y", ["x", "y"]), + "sqrt": ("sqrt(x)", ["x"]), + }[operation] + cls.evaluators[operation] = E(expression).evaluator( + [S(name) for name in names] + ) + result = cls.evaluators[operation].evaluate_with_prec( + [transport(value) for value in values], 32 + )[0] + if not result.is_finite(): + raise ArithmeticError(f"non-finite double-double {operation}") + return Decimal.__new__(cls, result) + + +@contextmanager +def arithmetic(format_name, precision): + """Study-local, restored patches: sources, kernels, coefficients, reduction. + + Explicit modules only, all imported before patching. This context must not + be used concurrently with another evaluator; the command is serial and + runs under its own guarded subprocess. Fresh runtimes are loaded inside it. + """ + modules = [ + importlib.import_module(f"pyamplicol.runtime.{name}") + for name in ( + "_color_weight_exact", + "_normalization_exact", + "symbolica_exact", + "correlated_exact", + "correlations", + "_color_topology_exact", + ) + ] + normalization = modules[1] + scalar = {"double": Binary64, "double-double": DoubleDouble}.get( + format_name, Decimal + ) + patches = [] + normalization._pi.cache_clear() + + def working_precision(requested): + return precision + + def upcast(value, requested): + return transport(value) + + def study_pi(requested): + return target_pi(scalar, precision) + + def unsupported_parameter_derivation(self, values, requested): + raise RuntimeError( + "This study's low-precision adapters require built-in SM kernels " + "without a derived model-parameter expression evaluator" + ) + + try: + if scalar is not Decimal: + for module in modules: + for name, value in list(vars(module).items()): + replacement = None + if value is Decimal: + replacement = scalar + elif type(value) is Decimal: + replacement = scalar(value) + elif name == "_working_precision": + replacement = working_precision + elif name == "_upcast_decimal": + replacement = upcast + if replacement is not None: + patches.append((module, name, value)) + setattr(module, name, replacement) + patches.append((normalization, "_pi", normalization._pi)) + normalization._pi = lru_cache(maxsize=1)(study_pi) + exact = modules[2] + expression_class = exact._ExactExpressionEvaluator + patches.append((expression_class, "evaluate", expression_class.evaluate)) + expression_class.evaluate = unsupported_parameter_derivation + if scalar is Binary64: + evaluator_class = exact._ExactEvaluator + original_stage = evaluator_class._evaluate_prepared + + def binary64_stage(self, values, requested): + if self.evaluator is None: + return original_stage(self, values, requested) + # with_prec(...,16) uses arbitrary Float, NOT binary64. + outputs = self.evaluator.evaluate_complex( + [complex(float(real), float(imag)) for real, imag in values] + )[0] + return tuple( + (Binary64(float(value.real)), Binary64(float(value.imag))) + for value in outputs + ) + + patches.append((evaluator_class, "_evaluate_prepared", original_stage)) + evaluator_class._evaluate_prepared = binary64_stage + with localcontext() as context: + context.prec = precision + 8 if scalar is Decimal else 80 + yield scalar + finally: + for module, name, value in reversed(patches): + setattr(module, name, value) + normalization._pi.cache_clear() + + +def arithmetic_smoke(format_name, *, kernels=False): + """Reject f64/MP mislabeled as DD before generating any study process.""" + scalar = {"double": Binary64, "double-double": DoubleDouble}[format_name] + tiny = "1e-20" + observed = (scalar(1) + scalar(tiny)) - scalar(1) + if format_name == "double": + assert observed == 0 + else: + assert abs(Decimal(observed) / Decimal(tiny) - 1) < Decimal("1e-10") + assert (scalar(1) + scalar("1e-40")) - scalar(1) == 0 + result = {"format": format_name, "one_plus_1e_20_minus_one": str(observed)} + if kernels: + from symbolica import E, S + + from pyamplicol.runtime.symbolica_exact import _ExactEvaluator + + evaluator = E("x+y").evaluator([S("x"), S("y")]) + precision = FORMATS[format_name][0] + exact = _ExactEvaluator(input_len=2, evaluator=evaluator) + with arithmetic(format_name, precision): + observed_kernel = Decimal( + exact.evaluate( + ((scalar(1), scalar(0)), (scalar(tiny), scalar(0))), precision + )[0][0] + ) + halfway = exact.evaluate( + ((scalar(1), scalar(0)), (scalar(2.0**-53), scalar(0))), precision + )[0][0] + if format_name == "double": + assert halfway == 1, "complex stage is not binary64 ties-to-even" + else: + assert halfway > 1, "complex stage lost the DoubleFloat half-ulp" + result["complex_kernel_one_plus_half_ulp"] = str(halfway) + with localcontext() as context: + context.prec = 80 + residual = observed_kernel - 1 + if format_name == "double": + assert residual == 0, "complex stage is not binary64" + else: + assert abs(residual / Decimal(tiny) - 1) < Decimal("1e-10"), ( + "complex stage lost the DoubleFloat correction" + ) + result["complex_kernel_one_plus_1e_20_minus_one"] = str(residual) + return result + + +def dot(a, b): + return a[0] * b[0] - sum(x * y for x, y in zip(a[1:], b[1:], strict=True)) + + +def linear(*terms): + return [sum(scale * vector[mu] for scale, vector in terms) for mu in range(4)] + + +def born_geometry(): + """The paper's rational generic 1 TeV Born point and transverse basis.""" + d, q = Decimal, Decimal(1000) + n = (d(2) / 3, d(1) / 3, d(2) / 3) + ta = (d(2) / 3, -d(2) / 3, -d(1) / 3) + tb = (d(1) / 3, d(2) / 3, -d(2) / 3) + incoming = [[q / 2, d(0), d(0), q / 2], [q / 2, d(0), d(0), -q / 2]] + parent = [4 * q / 9] + [4 * q * a / 9 for a in n] + spectator = [5 * q / 26] + [ + q * (-3 * a / 26 + 2 * b / 13) for a, b in zip(n, tb, strict=True) + ] + untouched = [85 * q / 234] + [ + q * (-77 * a / 234 - 2 * b / 13) for a, b in zip(n, tb, strict=True) + ] + e1 = [d(0), *ta] + e2 = [d(1) / 2] + [a / 2 + b for a, b in zip(n, tb, strict=True)] + return [*incoming, parent, spectator, untouched], e1, e2 + + +def constraint_checks(point): + q = Decimal(1000) + return { + "mass_shell_over_s": str(max(abs(dot(p, p)) for p in point) / q**2), + "conservation_over_Q": str( + max( + abs(point[0][mu] + point[1][mu] - sum(p[mu] for p in point[2:])) + for mu in range(4) + ) + / q + ), + "positive_energies": all(p[0] > 0 for p in point), + } + + +def forward_map(point): + pi, pj, pk = point[2:5] + ij, ik, jk = dot(pi, pj), dot(pi, pk), dot(pj, pk) + y = ij / (ij + ik + jk) + z = ik / (ik + jk) + mapped = [ + *point[:2], + linear((1, pi), (1, pj), (-y / (1 - y), pk)), + linear((1 / (1 - y), pk)), + point[5], + ] + return mapped, y, z, 2 * ij + + +def make_point(limit, exponent, digits=1280): + """One high-precision source point; no target arithmetic is used here.""" + if limit not in {"collinear", "soft"} or exponent < 1 or digits < 16: + raise ValueError("invalid limit, exponent, or source precision") + with localcontext() as context: + context.prec = max(digits + 80, 2 * exponent + 80) + born, e1, e2 = born_geometry() + parent, spectator = born[2:4] + delta, q = Decimal(10) ** -exponent, Decimal(1000) + e = linear((Decimal(3) / 5, e1), (Decimal(4) / 5, e2)) + if limit == "collinear": + z = Decimal(3) / 5 + kt = delta * q + transverse = linear((kt, e)) + y = kt**2 / (2 * z * (1 - z) * dot(parent, spectator)) + else: + r = [q / 2] + [q * a / 2 for a in e1[1:]] + alpha = dot(r, spectator) / dot(parent, spectator) + beta = dot(r, parent) / dot(parent, spectator) + r_perp = linear((1, r), (-alpha, parent), (-beta, spectator)) + z = 1 - delta * alpha + y = delta * beta / z + transverse = linear((-delta, r_perp)) + if not (0 < z < 1 and 0 < y < 1): + raise ValueError("trajectory outside the physical inverse-map domain") + pi = linear((z, parent), (y * (1 - z), spectator), (1, transverse)) + pj = linear((1 - z, parent), (y * z, spectator), (-1, transverse)) + pk = linear((1 - y, spectator)) + real = [*born[:2], pi, pj, pk, born[4]] + mapped, actual_y, actual_z, sij = forward_map(real) + checks = constraint_checks(real) + checks["mapped_born_over_Q"] = str( + max( + abs(a - b) + for p, r in zip(mapped, born, strict=True) + for a, b in zip(p, r, strict=True) + ) + / q + ) + checks["y_error"] = str(abs(actual_y - y)) + checks["z_error"] = str(abs(actual_z - z)) + with localcontext() as output: + output.prec = digits + + def strings(vectors): + return [[str(+value) for value in p] for p in vectors] + + return { + "limit": limit, + "exponent": exponent, + "delta": str(delta), + "real": strings(real), + "born": strings(born), + "spin": strings([e])[0], + "basis": strings([e1, e2]), + "s_ij": str(+sij), + "z": str(+z), + "y": str(+y), + "checks": checks, + } + + +@lru_cache(maxsize=8) +def target_pi(scalar, precision): + if scalar is Binary64: + return scalar(math.pi) + from symbolica import E + + # Pi is a constant input, rounded once into the target arithmetic. Asking + # Expression.evaluate(...,32) would perform arbitrary, not DD, arithmetic. + working = 80 if scalar is DoubleDouble else precision + 8 + return scalar(E("pi").evaluate({}, decimal_digit_precision=working)[0]) + + +def counterterm(source, values, scalar, pi): + """All inputs include public normalization; remove only final factorials.""" + b = scalar(6) * scalar(values["born"].real) + prefactor = scalar(8) * pi * scalar(ALPHA_S) + if source["limit"] == "collinear": + z, sij = scalar(source["z"]), scalar(source["s_ij"]) + spin = scalar(6) * scalar(values["spin"].real) + a = z / (scalar(1) - z) + (scalar(1) - z) / z + return ( + prefactor + / sij + * scalar(6) + * (a * b + scalar(2) * z * (scalar(1) - z) * spin) + ) + born = [[scalar(v) for v in p] for p in source["born"]] + soft = [scalar(v) for v in source["real"][3]] + total = scalar(0) + for a, b in DIPOLES: + pa, pb = born[a - 1], born[b - 1] + weight = dot(pa, pb) / (dot(pa, soft) * dot(pb, soft)) + total += weight * scalar(6) * scalar(values[f"T{a}{b}"].real) + return -prefactor * total + + +def declarations(): + from pyamplicol import ColorCorrelator, CorrelatorConfig + + return CorrelatorConfig( + color_correlations=tuple( + ColorCorrelator.dipole(f"T{a}{b}", a, b) for a, b in (*DIPOLES, (3, 3)) + ), + spin_correlations=((3,),), + ) + + +def correlation_requests(sources, diagnostics=False): + from pyamplicol import CorrelatedRequest + + requests = { + "born": CorrelatedRequest("born", spin_vectors={}), + "spin": CorrelatedRequest( + "born", spin_vectors={3: [source["spin"] for source in sources]} + ), + **{ + f"T{a}{b}": CorrelatedRequest(f"T{a}{b}", spin_vectors={}) + for a, b in DIPOLES + }, + } + if diagnostics: + requests["T33"] = CorrelatedRequest("T33", spin_vectors={}) + for name, key in (("basis1", 0), ("basis2", 1)): + requests[name] = CorrelatedRequest( + "born", spin_vectors={3: [source["basis"][key] for source in sources]} + ) + requests["ward"] = CorrelatedRequest( + "born", spin_vectors={3: [source["born"][2] for source in sources]} + ) + return requests + + +def generate_artifacts(directory): + """One standard generation per missing output; never overwrite an artifact.""" + import tomllib + + from pyamplicol import Generator, ModelSource + from pyamplicol.config import ( + ColorConfig, + EvaluatorConfig, + EvaluatorOptimizationConfig, + GenerationConfig, + RunConfig, + ) + + config = RunConfig( + action="generate", + color=ColorConfig(accuracy="full", contraction="direct"), + generation=GenerationConfig( + workers=1, + emit_api_bundle=False, + ), + evaluator=EvaluatorConfig( + execution_mode="compiled", + optimization=EvaluatorOptimizationConfig(cores=1), + ), + ) + paths = {name: directory / name for name in EXPRESSIONS} + identities = {} + for name, path in paths.items(): + if not (path / "artifact.json").exists(): + print(f"Generating {name}: {EXPRESSIONS[name]}", flush=True) + Generator(config).generate( + EXPRESSIONS[name], + path, + model=ModelSource.built_in_sm(), + correlators=declarations() if name == "born" else None, + ) + manifest = json.loads((path / "artifact.json").read_text()) + effective = tomllib.loads((path / "config" / "effective.toml").read_text()) + if ( + len(manifest["processes"]) != 1 + or manifest["processes"][0]["expression"] != EXPRESSIONS[name] + or effective["color"]["accuracy"] != "full" + or effective["color"]["contraction"] != "direct" + or effective["evaluator"]["execution_mode"] != "compiled" + or effective["generation"]["relation_discovery"]["mode"] + != STUDY["relation_discovery"][name] + ): + raise ValueError( + f"Incompatible study artifact {path}; choose a fresh directory" + ) + if name == "born": + catalogue = json.loads((path / "correlators.json").read_text()) + expected = declarations().to_json_dict() + if ( + catalogue["declarations"] != expected + or not catalogue.get("processes") + or any( + process.get("declarations") != expected + for process in catalogue["processes"].values() + ) + ): + raise ValueError( + f"Incompatible correlation declarations in {path}; " + "choose a fresh artifact directory" + ) + identities[name] = manifest["artifact_id"] + return paths, identities + + +def evaluate_real_batch(runtime, sources, format_name, precision, scalar): + """Normal path: one batch. Bisect only after a genuine batch exception.""" + points = [source["real"] for source in sources] + if format_name == "double": + points = [[[float(v) for v in p] for p in point] for point in points] + calls, errors = 0, {} + + def run(batch, start): + nonlocal calls + calls += 1 + try: + if format_name == "double": + values = runtime.evaluate(batch, precision=16) + result = [scalar(complex(value).real) for value in values] + else: + # Sum resolved DD values in DD, not an exact Decimal helper. + resolved = runtime.evaluate_resolved(batch, precision=precision) + result = [ + sum((scalar(v) for row in point for v in row), scalar(0)) + for point in resolved.values + ] + if len(result) != len(batch): + raise ValueError("real evaluator returned an inconsistent batch size") + for index, value in enumerate(result): + if not value.is_finite() or value <= 0: + errors[start + index] = "non-finite or nonpositive real result" + result[index] = None + return result + except Exception as exc: + if len(batch) == 1: + errors[start] = f"{type(exc).__name__}: {exc}" + return [None] + middle = len(batch) // 2 + return run(batch[:middle], start) + run(batch[middle:], start + middle) + + values = run(points, 0) + return values, {"calls": calls, "diagnostic_bisection": calls > 1, "errors": errors} + + +def validate_correlations(values, precision): + """Dimensionless checks; no high-multiplicity real ME is needed here.""" + with localcontext() as context: + context.prec = max(40, precision + 8) + b = Decimal(values["born"].real) + if b <= 0: + raise AssertionError("Born result must be positive") + tolerance = Decimal(10) ** (-min(20, max(8, precision - 6))) + checks = { + "completeness": abs( + (Decimal(values["basis1"].real) + Decimal(values["basis2"].real)) / b + - 1 + ), + "ward": abs(Decimal(values["ward"].real) / (b * Decimal(1000) ** 2)), + "casimir": abs(Decimal(values["T33"].real) / b - 3), + "imaginary": max(abs(Decimal(v.imag)) for v in values.values()) / b, + } + for leg in range(1, 6): + others = [ + tuple(sorted((leg, other))) for other in range(1, 6) if other != leg + ] + checks[f"coherence_{leg}"] = abs( + sum((Decimal(values[f"T{a}{b}"].real) for a, b in others), 3 * b) / b + ) + if any(value > tolerance for value in checks.values()): + raise AssertionError(f"Born correlation checks failed: {checks}") + anisotropy = abs(Decimal(values["spin"].real) / b - Decimal("0.5")) + if anisotropy < Decimal("1e-8"): + raise AssertionError("chosen Born/vector does not resolve spin anisotropy") + return { + **{key: str(value) for key, value in checks.items()}, + "spin_anisotropy": str(anisotropy), + } + + +def evaluate_batch(paths, sources, format_name, precision, *, diagnostics=False): + from pyamplicol import Runtime + + with arithmetic(format_name, precision) as scalar: + started = time.perf_counter() + real = Runtime.load( + paths["real"], + model_parameters={ + "alpha_s": float(ALPHA_S), + }, + ) + born = Runtime.load( + paths["born"], + model_parameters={ + "alpha_s": float(ALPHA_S), + }, + ) + requests = correlation_requests(sources, diagnostics=diagnostics) + loaded = time.perf_counter() + # One public call: every requested colour/spin contraction, every point. + results = born.evaluate_correlated_many( + [source["born"] for source in sources], + requests, + precision=precision, + ) + correlated_done = time.perf_counter() + # Public named access: results["T13"][point_index].real / .imag. + real_values, real_diagnostics = evaluate_real_batch( + real, + sources, + format_name, + precision, + scalar, + ) + real_done = time.perf_counter() + pi = target_pi(scalar, precision) + records = [] + for index, (source, real_value) in enumerate( + zip(sources, real_values, strict=True) + ): + components = {name: values[index] for name, values in results.items()} + record = { + "limit": source["limit"], + "exponent": source["exponent"], + "format": format_name, + "precision": precision, + "delta": source["delta"], + "source_checks": source["checks"], + "correlations": { + name: {"real": str(value.real), "imag": str(value.imag)} + for name, value in components.items() + }, + "real_batch": real_diagnostics, + } + if diagnostics: + record["correlation_checks"] = validate_correlations( + components, precision + ) + try: + if real_value is None: + raise ArithmeticError(real_diagnostics["errors"][index]) + r = scalar(24) * real_value + c = counterterm(source, components, scalar, pi) + f = c - r + if not all(value.is_finite() for value in (r, c, f)): + raise ArithmeticError("non-finite subtraction") + record.update( + { + "status": "ok", + "R": str(r), + "C": str(c), + "F": str(f), + "C_over_R": str(c / r), + "F_over_R": str(f / r), + } + ) + if source["limit"] == "collinear": + z, sij = scalar(source["z"]), scalar(source["s_ij"]) + pgg = scalar(6) * (z / (1 - z) + (1 - z) / z + z * (1 - z)) + averaged = scalar(8) * pi * scalar(ALPHA_S) / sij * pgg + averaged *= scalar(6) * scalar(components["born"].real) + record["spin_averaged_control_over_R"] = str((averaged - r) / r) + except (ArithmeticError, ValueError) as exc: + record.update({"status": "failed", "error": str(exc)}) + records.append(record) + batch = { + "point_count": len(sources), + "request_count": len(requests), + "public_correlated_many_calls": 1, + "public_real_calls": real_diagnostics["calls"], + "runtime_load_seconds": loaded - started, + "correlations_seconds": correlated_done - loaded, + "real_seconds": real_done - correlated_done, + "combination_seconds": time.perf_counter() - real_done, + } + for record in records: + record["batch"] = batch + # Adapter-specific caches cannot survive into the next format. + del born, real + return records + + +def record_key(record): + return f"{record['limit']}:{record['format']}:{record['exponent']}" + + +def pending_exponents(report, limit, format_name, exponents, *, retry_failed=False): + """A measured failure is data, not an implicit request for endless retries.""" + return [ + x + for x in exponents + if f"{limit}:{format_name}:{x}" not in report["records"] + or ( + retry_failed + and report["records"][f"{limit}:{format_name}:{x}"]["status"] != "ok" + ) + ] + + +def git_revision(): + try: + return subprocess.check_output( + ["git", "-C", str(ROOT), "rev-parse", "HEAD"], + text=True, + stderr=subprocess.DEVNULL, + ).strip() + except (OSError, subprocess.CalledProcessError): + return None + + +def compare_records(record, reference): + if reference is None or reference.get("status") != "ok": + return {"reference_status": "missing"} + if record.get("status") != "ok": + return {"reference_status": "ok", "stable_digits": "0", "stable_fraction": 0.0} + with localcontext() as context: + context.prec = int(reference["precision"]) + 30 + expected, observed = Decimal(reference["F"]), Decimal(record["F"]) + if expected == 0: + return {"reference_status": "zero-remainder"} + error = abs((observed - expected) / expected) + tracked = Decimal(str(FORMATS[record["format"]][1])) + digits = ( + tracked if error == 0 else min(tracked, max(Decimal(0), -error.log10())) + ) + return { + "reference_status": "ok", + "epsilon_F": str(error), + "stable_digits": str(digits), + "stable_fraction": float(digits / tracked), + } + + +def update_comparisons(report): + for record in report["records"].values(): + if record["format"] != "oracle": + reference = report["records"].get( + f"{record['limit']}:oracle:{record['exponent']}" + ) + record.update(compare_records(record, reference)) + + +def checkpoint(path, report): + path.parent.mkdir(parents=True, exist_ok=True) + temporary = path.with_suffix(path.suffix + ".tmp") + temporary.write_text( + json.dumps(report, indent=2, sort_keys=True, allow_nan=False) + "\n" + ) + temporary.replace(path) + + +def validate_factorization(report): + """Only the well-resolved oracle determines the physics convergence test.""" + for limit in ("collinear", "soft"): + points = sorted( + ( + r + for r in report["records"].values() + if r["limit"] == limit + and r["format"] == "oracle" + and r["status"] == "ok" + ), + key=lambda r: r["exponent"], + ) + if len(points) < 2: + continue + errors = [abs(Decimal(point["F_over_R"])) for point in points] + if errors[-1] >= errors[0] / 3 or errors[-1] >= Decimal("0.01"): + raise AssertionError(f"{limit} counterterm does not converge: {errors}") + + +def validate_requested_oracles(report, limits, exponents): + """A completed scan needs references; partial render-only requests do not.""" + missing = [ + key + for limit in limits + for exponent in exponents + if report["records"].get(key := f"{limit}:oracle:{exponent}", {}).get("status") + != "ok" + ] + if missing: + raise ValueError( + "Missing or failed requested oracle measurements: " + + ", ".join(missing) + + ". Checkpoints are retained; use --retry-failed to retry failures " + "or --render to redraw available results." + ) + + +def render(report, output): + import matplotlib + + matplotlib.use("Agg") + import matplotlib.pyplot as plt + + styles = { + "double": ("Double precision", "#cc6600", "--", "o"), + "double-double": ("DoubleFloat (31 digits)", "#2767ad", "-.", "s"), + "arbitrary-1000": ("1000 decimal digits", "#23834a", "-", "D"), + } + plt.rcParams.update( + { + "font.family": "serif", + "font.serif": ["DejaVu Serif"], + "mathtext.fontset": "cm", + "font.size": 10, + "axes.labelsize": 11, + "axes.titlesize": 11, + "xtick.labelsize": 9, + "ytick.labelsize": 9, + "axes.linewidth": 0.7, + "pdf.fonttype": 42, + "ps.fonttype": 42, + } + ) + figure, axes = plt.subplots( + 1, 2, figsize=(10.5, 4), sharey=True, layout="constrained" + ) + for axis, limit in zip(axes, ("collinear", "soft"), strict=True): + for name in FORMATS: + points = sorted( + ( + r + for r in report["records"].values() + if r["limit"] == limit + and r["format"] == name + and "stable_fraction" in r + and 1 <= r["exponent"] <= 15 + ), + key=lambda r: r["exponent"], + ) + if points: + label, color, linestyle, marker = styles[name] + axis.plot( + [p["exponent"] for p in points], + [p["stable_fraction"] for p in points], + color=color, + linestyle=linestyle, + label=label, + marker=marker, + markerfacecolor="white", + markersize=3.4, + linewidth=1.5, + markeredgewidth=0.8, + ) + axis.set( + title={"collinear": "Fixed-azimuth collinear", "soft": "Wide-angle soft"}[ + limit + ], + xlabel=r"$-\log_{10}\delta$", + xlim=(0.8, 15.2), + ylim=(-0.012, 1.03), + ) + axis.set_xticks(range(1, 16, 2)) + axis.set_yticks([0, 0.2, 0.4, 0.6, 0.8, 1]) + axis.grid(color="0.86", linewidth=0.45) + axis.tick_params(direction="in", top=True, right=True) + axes[0].set_ylabel("Stable-digit fraction") + axes[1].legend( + loc="center right", + bbox_to_anchor=(0.985, 0.54), + frameon=False, + fontsize=8.5, + handlelength=2.6, + ) + output.parent.mkdir(parents=True, exist_ok=True) + figure.savefig(output.with_suffix(".pdf")) + figure.savefig(output.with_suffix(".png"), dpi=170) + plt.close(figure) + + +def parser(): + result = argparse.ArgumentParser(description=__doc__) + result.add_argument( + "--artifact-dir", type=Path, default=Path(".artifacts/nlo-limits") + ) + result.add_argument( + "--output", type=Path, default=Path(".artifacts/nlo-limits/results.json") + ) + result.add_argument( + "--formats", nargs="+", choices=tuple(FORMATS), default=list(FORMATS) + ) + result.add_argument( + "--limits", + nargs="+", + choices=("collinear", "soft"), + default=["collinear", "soft"], + ) + result.add_argument("--exponents", nargs="+", type=int, default=list(range(1, 16))) + result.add_argument("--oracle-precision", type=int, default=1200) + result.add_argument("--memory-limit-gib", type=float, default=10.0) + result.add_argument( + "--prepare-only", + action="store_true", + help="Generate/reuse process artifacts without evaluating the scan", + ) + result.add_argument( + "--retry-failed", + action="store_true", + help="Explicitly retry measured failures; default preserves them", + ) + result.add_argument( + "--smoke", action="store_true", help="Only x=2,3,4; check physics identities" + ) + result.add_argument( + "--self-test", action="store_true", help="Kinematics only; no Symbolica" + ) + result.add_argument( + "--render", + action="store_true", + help="Render retained JSON only; no evaluations", + ) + result.add_argument( + "--plot", type=Path, help="PDF/PNG output stem; defaults next to results" + ) + result.add_argument("--_guard-child", action="store_true", help=argparse.SUPPRESS) + return result + + +def self_test(): + for limit in ("collinear", "soft"): + for exponent in (1, 4, 15, 100): + source = make_point(limit, exponent, digits=300) + for key, value in source["checks"].items(): + if key == "positive_energies": + assert value + else: + assert Decimal(value) < Decimal("1e-175"), (limit, exponent, key) + arithmetic_smoke("double") + print("Kinematics and binary64 arithmetic checks passed.") + + +def main(argv=None): + arguments = list(sys.argv[1:] if argv is None else argv) + args = parser().parse_args(arguments) + if args.self_test: + self_test() + return 0 + if args.render: + render( + json.loads(args.output.read_text()), + args.plot or args.output.with_suffix(""), + ) + return 0 + if ( + args.oracle_precision <= max(FORMATS[name][0] for name in args.formats) + or min(args.exponents) < 1 + ): + raise ValueError( + "oracle precision must exceed every selected target; " + "exponents must be positive" + ) + if not math.isfinite(args.memory_limit_gib) or args.memory_limit_gib <= 0: + raise ValueError("a positive finite process-tree memory guard is required") + if not args._guard_child: + watchdog = ROOT / "tools" / "ci" / "memory_watchdog.py" + return subprocess.call( + [ + sys.executable, + str(watchdog), + "--limit-gib", + str(args.memory_limit_gib), + "--", + sys.executable, + str(Path(__file__).resolve()), + *arguments, + "--_guard-child", + ] + ) + if args.prepare_only: + started = time.perf_counter() + _, identities = generate_artifacts(args.artifact_dir) + print( + json.dumps( + { + "artifacts": identities, + "git_revision": git_revision(), + "preparation_seconds": time.perf_counter() - started, + } + ), + flush=True, + ) + return 0 + arithmetic_checks = [] + for name in args.formats: + if name in ("double", "double-double"): + check = arithmetic_smoke(name, kernels=True) + arithmetic_checks.append(check) + print(json.dumps(check), flush=True) + exponents = [2, 3, 4] if args.smoke else sorted(set(args.exponents)) + report = {"study": STUDY, "oracle_precision": args.oracle_precision, "records": {}} + if args.output.exists(): + report = json.loads(args.output.read_text()) + if ( + report["study"] != STUDY + or report["oracle_precision"] != args.oracle_precision + ): + raise ValueError( + "Existing report has a different study definition; choose --output" + ) + report["arithmetic_checks"] = arithmetic_checks + report["git_revision"] = git_revision() + prepared = time.perf_counter() + paths, identities = generate_artifacts(args.artifact_dir) + report["preparation_seconds"] = time.perf_counter() - prepared + if report.get("artifacts", identities) != identities: + raise ValueError( + "Report belongs to different process artifacts; choose --output" + ) + report["artifacts"] = identities + for name in ["oracle", *args.formats]: + precision = args.oracle_precision if name == "oracle" else FORMATS[name][0] + for limit in args.limits: + missing = pending_exponents( + report, limit, name, exponents, retry_failed=args.retry_failed + ) + if not missing: + continue + print(f"Evaluating {limit}, {name}: x={missing}", flush=True) + sources = [ + make_point(limit, x, digits=args.oracle_precision + 80) for x in missing + ] + records = evaluate_batch( + paths, sources, name, precision, diagnostics=args.smoke + ) + for record in records: + record["git_revision"] = report["git_revision"] + report["records"][record_key(record)] = record + update_comparisons(report) + checkpoint(args.output, report) + validate_requested_oracles(report, args.limits, exponents) + validate_factorization(report) + render(report, args.plot or args.output.with_suffix("")) + print(f"Saved {args.output}", flush=True) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/pyamplicol/examples/README.md b/pyamplicol/examples/README.md new file mode 100644 index 0000000000..23c030c19f --- /dev/null +++ b/pyamplicol/examples/README.md @@ -0,0 +1,499 @@ + + +# Packaged Examples + +All TOML cards use schema version 1 and resolve paths relative to themselves. +Create an editable workspace whose model/data assets are independent of the +installation location with: + +```console +pyamplicol examples copy ./pyamplicol-examples +cd pyamplicol-examples +``` + +Keep the environment containing pyAmpliCol activated for top-level CLI and +Python commands below, or invoke its executables by explicit path. Generated +artifact API drivers additionally support the canonical source workflow: when +the copied workspace is below a checkout containing `.venv` from +`just dev-install`, the Python driver and C, C++, Fortran, and Rust Makefiles +find that environment automatically. Explicit `RUSTICOL_CONFIG` and an active +`PATH` still take precedence. + +The primary example uses the serialized external Standard Model and generates +seven tree-level representatives for `p p > Z j j`. They cover all 18 ordered +tree-level channels through automatic incoming/outgoing permutations. +Expansion also finds `g g > Z g g`, which is loop-induced and therefore +omitted: + +```console +pyamplicol generate_pp_zjj_from_ufo_sm.toml +pyamplicol evaluate_total.toml +pyamplicol evaluate_resolved.toml +pyamplicol benchmark.toml +``` + +`evaluate_total.toml` selects the concrete channel through the requested public +ordering `d d~ > g z g`. Its representative stable ID is +`p_p_to_z_j_j_3`. Its +parameter card updates the genuine UFO external inputs `aS` and `MZ`. +These three showcase cards print colorized terminal tables by default. Add +`--json` when a machine-readable result is wanted instead. + +## Run Cards + +| File | Coverage | +| --- | --- | +| `generate_pp_zjj_from_ufo_sm.toml` | Generate a portable compiled-JIT-O2 multiprocess `p p > Z j j` artifact from the serialized UFO SM | +| `evaluate_total.toml` | Optimized total for one `pp_zjj` subprocess | +| `evaluate_resolved.toml` | Helicity/color-resolved evaluation and explicit sum | +| `benchmark.toml` | Short topology-replay benchmark of the same subprocess at one selected flow | +| `qq_z6g_recurrence_jit_o2.toml` | `u u~ > Z + 6g` through the default recurrence schedule and prepared JIT O2 kernels | +| `qq_z6g_compiled_jit_o3.toml` | The same `Z + 6g` workload through process-local compiled-DAG JIT O3 execution | +| `qq_z6g_eager_jit_o2.toml` | The same `Z + 6g` workload through eager-DAG execution and prepared JIT O2 kernels | +| `benchmark_z6g_single_flow_helicity_sum.toml` | Profile the default topology-replay layout for runtime flow selection with a helicity sum in `u u~ > Z + 6g` | +| `benchmark_z6g_all_flows_single_helicity.toml` | Profile the explicit all-flow-union layout for an all-flow sum at one runtime-selected helicity in `u u~ > Z + 6g` | +| `benchmark_z6g_generation_specialized_flow_helicity_sum.toml` | Generation-specialized flow baseline for the reusable-selector comparison | +| `benchmark_z6g_generation_specialized_all_flows_single_helicity.toml` | Generation-specialized helicity baseline for the reusable-selector comparison | +| `process_set_mixed_multiplicity.toml` | Named UFO-SM 2-to-2 and 2-to-3 requests | +| `external_ufo_sm.toml` | Trusted UFO execution path | +| `external_json_scalars.toml` | Scalar contact model and repeated particles | +| `external_json_scalar_gravity.toml` | Proven massless spin-2 model path | +| `builtin_sm_lc.toml` | Built-in compatibility SM, default recurrence JIT O2, `u u~ > g g`, LC | +| `builtin_sm_nlc.toml` | Built-in compatibility SM, default recurrence JIT O2, contracted NLC | +| `builtin_sm_full.toml` | Built-in compatibility SM, explicit compiled C++, contracted full color | +| `builtin_sm_adjoint_fft.toml` | Certified pure-gluon tree, recurrence full color in the adjoint DDM FFT basis | +| `builtin_sm_heft.toml` | Packaged scalar HEFT `g g > H g g`, one insertion, recurrence JIT O2 with adjoint DDM FFT | +| `builtin_sm_eager.toml` | Built-in SM LC generation using the wheel-owned prepared JIT O2 pack | +| `builtin_sm_on_the_fly.toml` | Built-in SM LC generation as a compact on-the-fly seed using the same prepared JIT O2 pack | +| `otf_pp_zjj.toml` | Generate compact OTF LC `p p > Z j j`, then profile one flow with a helicity sum | +| `generate_pp_4j_flavor_blind.toml` | Flavour-blind built-in SM `p p > j j j j` at `QED = 0`, LC: 23 generated representatives for 486 subprocesses | +| `inspect_pp_4j.toml` | Summary of that artifact, including its process-group map | +| `evaluate_pp_4j_member.toml` | Evaluate the non-generated member `u~ g > u~ g g g` through its representative | +| `group_pp_4j.toml` | Numerical `group-processes` analysis of the generated representatives | +| `all_options.toml` | Every current schema field, active and commented | + +## Generate Pure Gluons With Adjoint FFT + +This is the primary FFT example for certified pure Yang–Mills trees; the card +selects adjoint explicitly rather than relying on the trace configuration +default. + +```console +pyamplicol generate --card builtin_sm_adjoint_fft.toml +pyamplicol inspect artifacts/builtin_sm_adjoint_fft +``` + +The card keeps the same amplitude in the two-anchor DDM basis: for its five +external gluons, it retains `3!` ordered amplitudes instead of the trace +basis's `4!`. To use the original trace FFT, pass `--fft trace`. Either basis +also supports `--execution-mode on-the-fly`; choose a different output path +when comparing artifacts. The adjoint request adaptively selects DDM for +certified Yang–Mills and single-insertion HEFT trees, exact fundamental +chains/products for quarks, and trace tensors otherwise. Inspect saved +`fft_basis_selection` for the actual representation and selection reason. +Correlated generation is not supported by this option. In the benchmarked pure-gluon +family the adjoint basis was faster than trace for six or more external gluons +(3.9x per sample at ten gluons); compare setup and warmed runtime for other +workloads. +The generated Python, C, C++, Fortran, and Rust drivers need no new runtime +option to evaluate it. + +## Generate Scalar HEFT Higgs Plus Two Jets + +The scalar HEFT card uses the packaged `built-in-sm-heft` model, so it does not +need an external UFO checkout: + +```console +pyamplicol generate --card builtin_sm_heft.toml +pyamplicol inspect artifacts/builtin_sm_heft +``` + +The explicit `HIG = 1` limit retains one effective Higgs-gluon insertion and +allows the certified DDM colour reduction. The four gluons in this example +have `2!` DDM orderings instead of `3!` trace orderings; the Higgs is not +counted as a coloured leg. The artifact uses full-colour recurrence over the +wheel-owned prepared JIT O2 kernels. Use `--fft trace` to compare the original +representation, without assuming a speedup at every multiplicity. See the website's +[Models and Processes](https://madgraph.org/pyamplicol/user/models-and-processes/) +guide for execution-mode and trusted-UFO details. + +## Process Grouping With `p p > j j j j` + +With five massless quark flavours, `p p > j j j j` expands into 486 +subprocesses. In pure QCD they fall into 23 classes whose members share one +matrix element, related by a relabelling of quark flavours, charge +conjugation, and a reordering of the legs within each side. Four cards +generate one representative per class, show the grouping, evaluate a +subprocess that was never generated, and run the numerical analysis: + +```console +pyamplicol generate_pp_4j_flavor_blind.toml +pyamplicol inspect_pp_4j.toml +pyamplicol evaluate_pp_4j_member.toml +pyamplicol group_pp_4j.toml +``` + +The same steps run in the private `examples run` workspace with +`pyamplicol examples run generate_pp_4j_flavor_blind`, then +`inspect_pp_4j`, `evaluate_pp_4j_member`, and `group_pp_4j`. + +**Generate.** The card uses the built-in SM with its default five-flavour `p` +and `j`, `process.max_coupling_orders = { QED = 0 }`, and LC recurrence over +the wheel-owned JIT O2 kernels. `process.flavor_blind = true` generates only +the 23 class representatives into `artifacts/pp_4j_flavor_blind`, which takes +about 30 seconds on an Apple M3 Pro laptop. The other 463 subprocesses, 140 +of them related by charge conjugation, are recorded in +`artifacts/pp_4j_flavor_blind/process-groups.json` as members of their class. +Equal matrix elements within a class are assumed, not verified. They hold for +massless pure-QCD jets, so this request produces no relabelling warning; with +photon, `Z` or `W` couplings allowed, generation warns about the relations +that may break. The card refuses an existing output; add +`--set generation.mode=replace` to regenerate. + +**Inspect.** `inspect_pp_4j.toml` is equivalent to +`pyamplicol inspect artifacts/pp_4j_flavor_blind`. After the 23 generated +processes it shows the process-group map: a summary of the 486 subprocesses, +then one row per representative with the members routed through it, the +charge-conjugated ones marked `C`. Select a representative to list its group +member by member, or a member with `--full-physics` to see how it is routed: + +```console +pyamplicol inspect artifacts/pp_4j_flavor_blind --process 'd g > d g g g' +pyamplicol inspect artifacts/pp_4j_flavor_blind \ + --process 'u~ g > u~ g g g' --full-physics +``` + +**Evaluate a member.** `u~ g > u~ g g g` is not in the artifact. The runtime +finds it in `process-groups.json` and evaluates its representative +`d g > d g g g` (relabelling `u -> d` plus charge conjugation) at the +correspondingly permuted momenta. `data/pp_4j_momenta.json` holds one massless +2-to-4 point at `sqrt(s) = 1 TeV` in the member's leg order. The total, +`7.271103230388e-08`, agrees with a separately generated +`u~ g > u~ g g g` to about `1e-14` relative. Any of the 486 subprocesses, in +any order of the legs within each side, can be selected the same way: + +```console +pyamplicol evaluate_pp_4j_member.toml \ + --set 'evaluation.process=u~ u > u~ u g g' +pyamplicol evaluate --card evaluate_pp_4j_member.toml --resolved \ + --helicity 'h:-1,-1,-1,-1,-1,-1' +``` + +The resolved table lists helicities and LC flows in the member's leg order; +for a conjugated member each flow word is reversed. Without `--helicity` it +lists all 64 helicities times 24 flows. Python selects members +with `Runtime.load("artifacts/pp_4j_flavor_blind", process="u~ g > u~ g g g")`, +whose `representative_process_key`, `process_group_factor` and +`charge_conjugated` describe the routing. The generated API drivers accept +the same selection, for example: + +```console +python artifacts/pp_4j_flavor_blind/API/python/check_standalone.py \ + --process 'u~ g > u~ g g g' --kinematics data/pp_4j_momenta.json --json +``` + +**Group numerically.** `group_pp_4j.toml` is equivalent to +`pyamplicol group-processes artifacts/pp_4j_flavor_blind`. It compares the 23 +generated representatives at random points, merges those whose resolved +values agree up to a rational factor, and rewrites `process-groups.json` in +place. The card sets `process.analyze_process_groups = "numerical"` because +run cards otherwise use the schema default `none`; the `group-processes` +subcommand defaults to `numerical`. Here the analysis takes about a second +and reports no new relation: the 23 classes are already distinct. To test +the flavour-blind assumption itself, generate all 486 subprocesses without +`flavor_blind` (much slower) and let the analysis rebuild the same 23 groups; +see the [process grouping guide](https://madgraph.org/pyamplicol/user/process-grouping/). + +## Run `q q~ > Z + 6g` With Recurrence, Compiled, And Eager + +Each matched card generates and profiles the same complete-selector, +topology-replay LC workload: + +```console +pyamplicol generate --card qq_z6g_recurrence_jit_o2.toml +pyamplicol profile --card qq_z6g_recurrence_jit_o2.toml + +pyamplicol generate --card qq_z6g_compiled_jit_o3.toml +pyamplicol profile --card qq_z6g_compiled_jit_o3.toml + +pyamplicol generate --card qq_z6g_eager_jit_o2.toml +pyamplicol profile --card qq_z6g_eager_jit_o2.toml +``` + +Recurrence JIT O2 is the default current-schedule lane, compiled JIT O3 builds +process-local DAG evaluators, and eager-DAG JIT O2 applies prepared kernels to +compact process tables. The explicit settings in these cards make the +materialized-mode comparison stable if project defaults change again. + +## Generate A Compact On-The-Fly LC Artifact + +The dedicated OTF walkthrough expands `p p > Z j j`, loads the concrete +ordering `d d~ > g z g`, warms one LC flow with a complete helicity sum, and +then profiles the same selector workload: + +```console +pyamplicol generate --card otf_pp_zjj.toml +python python/otf_pp_zjj_warm_up.py +pyamplicol profile --card otf_pp_zjj.toml +``` + +The Python step calls +`Runtime.warm_up((point,), precision=16, color_flows=(flow,))`: its input is +exactly one phase-space point, native `f64` is explicit, and omitting a +helicity selector means sum every physical helicity. The standard terminal +progress dashboard tracks query-family progress and live current/peak RSS. +It is followed by a colored PrettyTable containing the selected stable flow +ID, warm-up wall time, query counts, memory, and matrix element. Use +`--color always` when capturing the ANSI-colored table through a pipe, or +`--color-flow N` to choose another one-based physical flow. + +The `profile` command starts an independent runtime and prints pyAmpliCol's +colored human benchmark tables. Its 128-point batches are throughput and +benchmark-capacity measurements; they do not change the explicit +`warm_up(...)` API's one-point contract. + +To reuse completed OTF preparation in another Python session or native program, +call `runtime.save("process.otf-cache")` on the warmed handle. Load the same +process output normally in the new program, then call +`runtime.load_cache("process.otf-cache")` before evaluating with the same +selectors. This is explicit API functionality: the separate CLI `profile` +command above does not automatically read a saved cache. Small Python and +C/C++/Fortran/Rust examples are in the online manual's +[save/restore guide](https://madgraph.org/pyamplicol/user/runtime-and-selectors/#saving-an-otf-warm-cache) +and [native API guide](https://madgraph.org/pyamplicol/user/native-apis/#saving-and-restoring-an-otf-cache). + +The smaller single-process card remains useful for quick inspection and a CLI +profile without multiprocess expansion: + +```console +pyamplicol generate --card builtin_sm_on_the_fly.toml +pyamplicol inspect artifacts/builtin_sm_on_the_fly +pyamplicol profile --card builtin_sm_on_the_fly.toml \ + --color-flow 1 --batch-size 128 --target-runtime 5 +``` + +The inspection reports the physical helicity/color-flow census without +materializing either LC flow layout. The card's +`evaluator.optimization.cores = 4` requests four query-construction threads; it +does not promise four numerical-evaluation threads. Point-tile and workspace +settings are not currently applied by this mode. The profile measures one +runtime-selected LC flow with a helicity sum over 128-point batches. Generation +requires the wheel-owned prepared JIT O2 bundle selected for the built-in +model; the artifact carries the referenced kernels it needs at runtime. +The walkthrough intentionally targets the practical OTF workload: LC, +single-flow, helicity-sum execution at native `f64`. Contracted NLC and full +color are available for low multiplicities but are not showcased here because +their OTF construction cost grows quickly; higher-precision OTF execution is +not available. + +## Reproduce The Z-Ladder Workloads + +These acceptance cards use two separately generated compiled-mode artifacts so +their generation and profile logs remain independent. The first keeps the +default `topology-replay` LC layout, selects flow ordinal `1` only at profile +time, and sums all helicities. The second explicitly sets +`color.lc_flow_layout = "all-flow-union"`, sums all flows, and selects one +stable helicity ID only at profile time. The union shares currents across the +physical flows and is optimized for this second workload. + +Both artifacts retain all physical LC flows and all physical helicities. Either +can select any retained flow or helicity globally or per point at runtime; +neither card fixes a selector at generation time. Generate and profile them +independently: + +```console +pyamplicol generate --card benchmark_z6g_single_flow_helicity_sum.toml +pyamplicol profile --card benchmark_z6g_single_flow_helicity_sum.toml + +pyamplicol generate --card benchmark_z6g_all_flows_single_helicity.toml +pyamplicol profile --card benchmark_z6g_all_flows_single_helicity.toml +``` + +Both profiles use native Rusticol wall timing, compressed JIT O3, 64-point +batches, two warmups, at least five samples, and a twenty-second target. The +cards use `u u~ > Z g g g g g g` as requested; the PDF's literal reference +family uses `d d~ > Z + (n-1)g`, so replace `u u~` with `d d~` when reproducing +that exact row rather than the equivalent up-quark topology. + +The runtime selector can be changed without regenerating. For example: + +```console +pyamplicol profile \ + --card benchmark_z6g_single_flow_helicity_sum.toml \ + --color-flow 2 + +pyamplicol profile \ + --card benchmark_z6g_all_flows_single_helicity.toml \ + --helicity h:-1,+1,-1,+1,+1,-1,+1,-1,+1 +``` + +Python's `Runtime.evaluate(..., color_flow_by_point=..., helicity_by_point=...)` +accepts one physical selector per phase-space point. Homogeneous, alternating, +and randomized selector batches all use the same complete artifact. + +The layout is also a dedicated CLI option. For example, this creates a union +artifact from the otherwise topology-replay card: + +```console +pyamplicol generate \ + --card benchmark_z6g_single_flow_helicity_sum.toml \ + --lc-flow-layout all-flow-union \ + --set generation.output=artifacts/uubar_z6g_all_flows_override +``` + +`topology-replay` remains the global default because it is optimized for a +single runtime-selected flow with a helicity sum. `all-flow-union` is LC-only; +configuration rejects it for NLC/full and for LC requests with a +generation-selected color sector, a generation-selected helicity, or truncated +color coverage. + +The execution mode can be overridden directly at invocation time. Use a +different output so the eager artifact cannot collide with the compiled one: + +```console +pyamplicol generate \ + --card benchmark_z6g_single_flow_helicity_sum.toml \ + --execution-mode eager \ + --set generation.output=artifacts/uubar_z6g_single_flow_helicity_sum_eager +``` + +The eager override keeps the same complete reusable-selector contract and the +card's LC flow layout. Eager generation and execution apply flow and helicity +choices through Rusticol's selector schedule rather than generation-time +specialization. + +`examples copy` also materializes wheel-owned `sm`, `scalars`, and +`scalar_gravity` resources into `models/`. The included +`python/copy_packaged_models.py` helper performs the resource-only operation for +a separate workspace and refuses to merge into a non-empty destination unless +`--force` is supplied. The public model API accepts filesystem paths, so no +example relies on an installation directory. + +## Typed Python + +Plan or generate the primary process through the typed API: + +```console +python python/typed_generation.py artifacts/pp_zjj_typed --plan-only +python python/typed_generation.py artifacts/pp_zjj_typed +``` + +The script compiles the external JSON model before planning, carries explicit +`p`/`j` definitions in the resolved configuration, and writes the same root +API bundle as the CLI. + +Evaluate with a parameter card plus a direct override: + +```console +python python/runtime_evaluation.py \ + artifacts/pp_zjj data/pp_zjj_momenta.json \ + --process 'd d~ > g z g' \ + --parameters data/model_parameters.json \ + --set-parameter aS=0.1165 +``` + +The JSON output includes the resolved tensor `shape`, flattened row-major +`values`, its explicit `resolved_sum`, and the optimized +`compatibility_total`, in addition to the selected physics-axis IDs. + +Benchmark the selected process: + +```console +python python/benchmark.py artifacts/pp_zjj \ + --process 'd d~ > g z g' \ + --momenta data/pp_zjj_momenta.json \ + --color-flow 1 +``` + +The compact OTF counterpart uses the same phase-space point but explicitly +prepares just one LC flow before evaluating it: + +```console +python python/otf_pp_zjj_warm_up.py +``` + +`python/external_models.py` demonstrates explicit JSON and trusted-UFO +`ModelSource` construction: + +```console +python python/external_models.py models/json/sm/sm.json models/ufo/sm +``` + +## Generated Python, C, Rust, C++, And Fortran + +Every generated artifact contains one `API/` bundle. All drivers select a +process, accept JSON/direct model-parameter updates, evaluate resolved values, +sum them, and compare with the optimized total: + +```console +python artifacts/pp_zjj/API/python/check_standalone.py \ + --process 'd d~ > g z g' --set-parameter aS 0.117 0 --json +make -C artifacts/pp_zjj/API/c run \ + ARGS='--process "d d~ > g z g" --set-parameter aS 0.117 0 --json' +make -C artifacts/pp_zjj/API/rust run \ + ARGS='--process "d d~ > g z g" --set-parameter aS 0.117 0 --precision 16 --json' +make -C artifacts/pp_zjj/API/cpp run \ + ARGS='--process "d d~ > g z g" --set-parameter aS 0.117 0 --json' +make -C artifacts/pp_zjj/API/fortran run \ + ARGS='--process "d d~ > g z g" --set-parameter aS 0.117 0 --json' +``` + +The requested expression need not be a stored alias. Rusticol identifies a +unique generated representative with the same incoming and outgoing particle +multisets, then applies the side-preserving permutation to input momenta, +helicity and LC-flow selectors, and resolved output metadata. Incoming and +outgoing legs never cross `>`; an ambiguous match fails with the candidate +stable process IDs. + +All five drivers also accept `--kinematics PATH`. The JSON file contains one +point as `[external][4]`, or the same point wrapped as a singleton batch +`[[external][4]]`, in the exact particle order written in `--process`. Each +component may be a JSON number or a decimal string. Decimal strings avoid an +f64 round trip in the arbitrary-precision Python driver; native drivers parse +the same representation for f64. Multiple points, non-finite values, booleans, +and mismatched shapes are rejected. Without this option the driver reorders +the artifact's bundled validation point automatically. + +`--model-parameters data/model_parameters.json` applies a UFO-style flat JSON +object before evaluation. Each mutable external parameter maps to a finite +number or `[real, imaginary]`, matching a serialized UFO restriction card. +Repeated `--set-parameter NAME REAL IMAG` options are applied afterward and +therefore override the card atomically. + +In a copied workspace below a source checkout, this exact block works even in +a later shell where `.venv` has not been activated. Artifacts generated by an +older pyAmpliCol retain their older API files and must be regenerated once: + +```console +../.venv/bin/pyamplicol generate_pp_zjj_from_ufo_sm.toml \ + --set generation.mode=replace +``` + +The generated Rust source includes the wheel-owned safe wrapper located by +`rusticol-config --rust-source` and is compiled directly with `rustc` plus +`rusticol-config --rustflags`; no Rust crate dependency is needed. The Makefile +also has an optional `run-script` target for separately installed +`rust-script`, using `rusticol-config --cargo-rustflags`. C, Rust, C++, and +Fortran support f64 (`--precision 16`) only. At f64, direct SymJIT, +target-compatible ASM/C++, and eager JIT artifacts run without a Symbolica +runtime. The Python driver also exposes precision-controlled Symbolica +evaluation when exact expressions are available. + +## Hand-Written Native Examples + +`native/runtime.cpp`, `native/runtime.f90`, and `native/Makefile` consume only +the SDK discovered from an installed wheel: + +```console +make -C native +native/runtime_cpp artifacts/pp_zjj p_p_to_z_j_j_3 \ + data/model_parameters.json +native/runtime_fortran artifacts/pp_zjj p_p_to_z_j_j_3 \ + data/model_parameters.json +``` + +Both examples apply a direct `aS` override, evaluate the five-particle +validation point, and verify that resolved components reproduce the total. + +Current release validation and upload status are listed in +the [release and support guide](https://madgraph.org/pyamplicol/user/release-and-support/). diff --git a/pyamplicol/examples/all_options.toml b/pyamplicol/examples/all_options.toml new file mode 100644 index 0000000000..c0eb70f3a1 --- /dev/null +++ b/pyamplicol/examples/all_options.toml @@ -0,0 +1,268 @@ +# SPDX-License-Identifier: 0BSD +# Exhaustive TOML schema-v1 reference. Every current registry leaf is active. +# This card is for resolution/documentation, not as a recommended physics run. + +# action: Required command action. Choices are generate, evaluate, benchmark, +# inspect, model-inspect, model-compile, model-processes, and group-processes. +action = "generate" + +# schema_version: Configuration schema. The only current value is 1. +schema_version = 1 + +[model] +# model.source: "built-in-sm", "built-in-sm-heft", or a UFO +# directory/JSON/compiled-model path. +source = "models/json/sm/sm.json" +# model.restriction: Optional restriction name. Default is null, which applies +# a model-provided default restriction when one exists. +restriction = "default" +# model.simplify: Simplify an external model while compiling. Default true. +simplify = true +# model.cache: Reuse the content-addressed compiled-model cache. Default true. +cache = true +# model.cache_dir: Optional cache directory. Default is platform-specific/null. +cache_dir = "cache/models" + +[process] +# process.entries: Typed process expressions with optional unique stable names. +entries = [ + { expression = "p p > Z j j" }, +] +# process.flavor_scheme: Number of active quark flavors. Default 5. +flavor_scheme = 2 +# process.max_quark_lines: Optional maximum quark-line count. Default null. +max_quark_lines = 2 +# process.coupling_order_policy: "minimal" or "explicit". Default minimal. +coupling_order_policy = "explicit" +# The following four topology-extraction controls are unstable, developer-only +# implementation details. Do not rely on them in production run cards. +# process.max_color_sectors: Optional cap on generated color sectors. Default null. +max_color_sectors = 32 +# process.reference_color_order: Optional external-leg ordering used to identify +# a reference LC sector. Default is an empty list. +reference_color_order = [1, 2, 3, 4, 5] +# process.selected_color_sector_ids: Optional generated-sector subset. Default +# is an empty list, which keeps every generated sector. +selected_color_sector_ids = [0] +# process.selected_source_helicities: Optional fixed source helicities keyed by +# one-based external-leg label. Default is an empty table. +selected_source_helicities = { "1" = -1, "2" = 1 } +# process.flavor_blind: Generate one representative per flavour-relabelling +# class and record the others as unverified members in process-groups.json. +# Default false. +flavor_blind = false +# process.consider_charge_conjugation: Also relate processes by charge +# conjugation for flavor_blind and numerical grouping. Default true. +consider_charge_conjugation = true +# process.only_unit_multiplier: Accept only numerical relations with factor 1. +# Default false. +only_unit_multiplier = false +# process.analyze_process_groups: "none" or "numerical". After generation, +# "numerical" compares generated processes at random points and records equal +# matrix elements up to a rational factor. Default none. +analyze_process_groups = "none" + +# process.multiparticles: User-defined particle-name expansions, merged over +# model defaults. Every model supplies "all" for its valid propagating physical +# external states (excluding ghosts, Goldstones, and auxiliary/non-propagating +# records); an explicit all entry overrides that broad default. +[process.multiparticles] +p = ["d", "d~", "u", "u~", "g"] +j = ["d", "d~", "u", "u~", "g"] + +# process.max_coupling_orders: Non-negative limits keyed by coupling-order name. +[process.max_coupling_orders] +QCD = 4 +QED = 2 + +[color] +# color.accuracy: "lc", "nlc", or "full". Default lc. +accuracy = "lc" +# color.contraction: "direct" or "symmetric-group-fft". Default direct. +# The FFT method requires NLC/full with recurrence or on-the-fly execution. +contraction = "direct" +# color.fft_basis: "trace" or "adjoint". Default trace. +# Adjoint adaptively selects certified DDM (Yang-Mills or single-insertion +# HEFT), fundamental chains/products for quarks, or exact trace otherwise. +# Saved fft_basis_selection reports the actual representation and reason. +# It requires FFT contraction and does not support correlated generation. +# The schema default shown here remains trace. +fft_basis = "trace" +# color.lc_flow_layout: "topology-replay" or "all-flow-union". Default +# topology-replay. The all-flow union is LC-only and cannot be combined with +# generation-selected or truncated color/helicity coverage. +lc_flow_layout = "topology-replay" + +[generation] +# generation.output: Optional artifact output path. Default null. +output = "artifacts/pp_zjj_all_options" +# generation.mode: "error", "append", or "replace". Default error. +mode = "error" +# generation.workers: Positive integer or "auto". Default auto. +workers = "auto" +# generation.emit_api_bundle: Emit Python/Rust/C/C++/Fortran/UMAMI API bundle. Default true. +emit_api_bundle = true +[generation.validation] +# generation.validation.enabled: Run generation validation. Default true. +enabled = true +# generation.validation.samples: Positive sample count. Default 2. +samples = 2 +# generation.validation.seed: Non-negative random seed. Default 12345. +seed = 12345 +# generation.validation.relative_tolerance: Non-negative tolerance. Default 1e-12. +relative_tolerance = 1e-12 +# generation.validation.absolute_tolerance: Non-negative tolerance. Default 1e-300. +absolute_tolerance = 1e-300 +# generation.validation.post_build_validation: Re-open and numerically smoke-test +# the built evaluator. Default false; structural artifact checks always run. +post_build_validation = false + +[generation.relation_discovery] +# generation.relation_discovery.mode: "off", "diagnostic", or +# "certified-reuse". Exact proofs are preferred. Independently verified +# equal, opposite, and zero currents may also be applied with persisted +# numerical evidence and a warning. Default off, because the high-precision +# probes are costly at generation time; set "certified-reuse" or pass +# --numerical-current-reuse to search for additional reuse. +mode = "off" +# generation.relation_discovery.precision_digits: Decimal probe precision, +# at least 80. Default 96. +precision_digits = 96 +# generation.relation_discovery.probe_count: Deterministic candidate points, +# at least 2. Default 4. +probe_count = 4 +# generation.relation_discovery.verification_probe_count: Independent +# verification points, at least 2. Default 4. +verification_probe_count = 4 +# generation.relation_discovery.relative_tolerance: Non-negative verification +# tolerance. Default 1e-70. +relative_tolerance = 1e-70 +# generation.relation_discovery.absolute_tolerance: Non-negative verification +# tolerance. Default 1e-80. +absolute_tolerance = 1e-80 +# generation.relation_discovery.seed: Non-negative deterministic probe seed. +# Default 1348026701. +seed = 1348026701 + +[evaluator] +# evaluator.backend: "jit", "asm", or "cpp". Default jit. +backend = "jit" +# evaluator.execution_mode: "compiled", "eager", "recurrence", or +# "on-the-fly". Default recurrence. Eager, recurrence, and on-the-fly +# built-in-sm use the wheel-owned JIT O2 pack; other packs need an explicit +# .pyamplicol-model source. On-the-fly supports LC and contracted NLC/full in +# native f64. +execution_mode = "compiled" +# evaluator.batch_size: Positive evaluation batch size. Default 128. +batch_size = 128 +# evaluator.output_chunk_size: Positive size or null. Default 512. +output_chunk_size = 512 + +[evaluator.eager] +# evaluator.eager.point_tile_size: Maximum points per eager workspace tile. +# The runtime reduces this automatically when required. Default 1024. +# This eager section is not applied by on-the-fly execution. +point_tile_size = 1024 +# evaluator.eager.workspace_mib: Batch workspace target. Default 256; grows as +# needed to fit the minimum executable tile, rather than rejecting a process. +workspace_mib = 256 + +[evaluator.recurrence] +# evaluator.recurrence.point_tile_size: Maximum points per recurrence workspace +# tile. The runtime may reduce this automatically. Default 1024. +# On-the-fly records this request as provenance but does not currently apply it. +point_tile_size = 1024 +# evaluator.recurrence.workspace_mib: Batch workspace target; grows as needed +# to fit the minimum executable tile, rather than rejecting a process. +# Default 256. This target is not currently applied by on-the-fly execution. +workspace_mib = 256 + +[evaluator.optimization] +# evaluator.optimization.horner_iterations: Non-negative count. Default 10. +horner_iterations = 10 +# evaluator.optimization.cpe_iterations: Non-negative override. The field is +# omitted by default so Symbolica selects its backend default; this card uses 0. +cpe_iterations = 0 +# evaluator.optimization.cores: Positive integer or "auto". Default auto. In +# on-the-fly mode this is the requested query-construction thread count, not a +# numerical-evaluation thread guarantee. +cores = "auto" +# evaluator.optimization.max_horner_variables: Non-negative limit. Default 1000. +max_horner_variables = 1000 +# evaluator.optimization.max_common_pair_cache_entries: Default 5000000. +max_common_pair_cache_entries = 5000000 +# evaluator.optimization.max_common_pair_distance: Default 1000. +max_common_pair_distance = 1000 +# evaluator.optimization.collect_factors: "auto", true, or false. Default auto. +collect_factors = "auto" + +[evaluator.jit] +# evaluator.jit.optimization_level: Integer 0 through 3. Default 2. +optimization_level = 2 +# evaluator.jit.compress: Smaller generated code, potentially slower evaluation. +# "auto" enables compiled JIT O2 only; true/false explicitly override the default. +compress = "auto" + +[evaluator.cpp] +# evaluator.cpp.optimization: Compiler optimization token. Default "O3". +optimization = "O3" +# evaluator.cpp.compiler: Optional compiler executable. Default null. +compiler = "c++" +# evaluator.cpp.native_arch: Opt into recorded host CPU requirements. Default false. +native_arch = false +# evaluator.cpp.extra_flags: Allowlisted non-ISA compiler arguments. Default empty. +extra_flags = ["-fno-math-errno"] + +[evaluation] +# evaluation.artifact: Optional schema-v3 artifact path. Default null. +artifact = "artifacts/pp_zjj_all_options" +# evaluation.process: Optional stable ID/alias, concrete expression, or unique +# permutation within each side of a concrete process. Legs never cross ">". +# Default null. +process = "d d~ > g z g" +# evaluation.precision: Positive decimal-digit request. Default 16. +precision = 16 +# evaluation.resolved: Return physical resolved components. Default false. +resolved = true +# evaluation.helicity_ids: Stable physical helicity selectors. Default empty. +helicity_ids = [] +# evaluation.color_flow_ids: Stable LC flow selectors. Default empty. +color_flow_ids = [] +# evaluation.model_parameters: Optional flat UFO-style JSON parameter card. +# Values are finite numbers or [real, imaginary] pairs. Direct parameter +# overrides are applied afterward. Default null. +model_parameters = "data/model_parameters.json" +# evaluation.momenta: Optional JSON phase-space points path. Default null. +momenta = "data/pp_zjj_momenta.json" + +[benchmark] +# benchmark.target_runtime: Positive target duration in seconds. Default 10.0. +target_runtime = 10.0 +# benchmark.batch_size: Positive benchmark batch size. Default 128. +batch_size = 128 +# benchmark.precision: Positive decimal precision used by Python evaluation. Default 16. +precision = 16 +# benchmark.warmup_runs: Non-negative warmup count. Default 2. +warmup_runs = 2 +# benchmark.minimum_samples: Positive minimum measured samples. Default 5. +minimum_samples = 5 +# benchmark.helicity_ids: Optional benchmark helicity selectors. With both +# selector lists empty, profiling chooses the generated layout's hot workload. +helicity_ids = [] +# benchmark.color_flow_ids: Optional benchmark LC flow selectors. With both +# selector lists empty, profiling chooses the generated layout's hot workload. +color_flow_ids = [] + +[output] +# output.format: typed API/config provenance default. The CLI uses --json. +format = "human" +# output.color: "auto", "always", or "never". Default auto. +color = "auto" +# output.progress: "auto", "tty", "log", or "off". Default auto. +progress = "auto" +# output.log_level: "debug", "info", "warning", or "error". Default info. +log_level = "info" + +[symbolica] +# symbolica.suggest_license: Show one restricted-mode license reminder. Default true. +suggest_license = true diff --git a/pyamplicol/examples/benchmark.toml b/pyamplicol/examples/benchmark.toml new file mode 100644 index 0000000000..ad6b87cc24 --- /dev/null +++ b/pyamplicol/examples/benchmark.toml @@ -0,0 +1,25 @@ +# SPDX-License-Identifier: 0BSD +# Short benchmark of the optimized summed runtime path. + +schema_version = 1 +action = "benchmark" + +[evaluation] +artifact = "artifacts/pp_zjj" +process = "d d~ > g z g" +momenta = "data/pp_zjj_momenta.json" + +[benchmark] +target_runtime = 1.0 +batch_size = 128 +precision = 16 +warmup_runs = 2 +minimum_samples = 5 +helicity_ids = [] +# Topology-replay is optimized for one selected flow with the helicity sum. +color_flow_ids = ["1"] + +[output] +format = "human" +color = "auto" +progress = "log" diff --git a/pyamplicol/examples/benchmark_z6g_all_flows_single_helicity.toml b/pyamplicol/examples/benchmark_z6g_all_flows_single_helicity.toml new file mode 100644 index 0000000000..581b14be1e --- /dev/null +++ b/pyamplicol/examples/benchmark_z6g_all_flows_single_helicity.toml @@ -0,0 +1,76 @@ +# SPDX-License-Identifier: 0BSD +# Exercise the reusable compiled JIT O3 all-flows, single-helicity workload +# with the explicit all-flow-union recurrence layout. +# +# From a copied examples directory, generate and profile with: +# pyamplicol generate --card benchmark_z6g_all_flows_single_helicity.toml +# pyamplicol profile --card benchmark_z6g_all_flows_single_helicity.toml + +schema_version = 1 +action = "generate" + +[model] +source = "built-in-sm" +cache = true + +[process] +entries = [ + { expression = "u u~ > Z g g g g g g", name = "uubar_Z_6g" }, +] +# No generation-time helicity selection is used: the artifact retains every +# physical helicity and can select any helicity globally or per point at runtime. + +[color] +accuracy = "lc" +lc_flow_layout = "all-flow-union" + +[generation] +output = "artifacts/uubar_z6g_all_flows_single_helicity" +mode = "error" +workers = 1 +emit_api_bundle = true + +[generation.validation] +enabled = true +samples = 10 +seed = 12345 +relative_tolerance = 1e-12 +absolute_tolerance = 1e-300 +post_build_validation = false + +[evaluator] +backend = "jit" +execution_mode = "compiled" +batch_size = 64 +output_chunk_size = 512 + +[evaluator.optimization] +horner_iterations = 10 +cores = 1 +max_horner_variables = 1000 +max_common_pair_cache_entries = 5000000 +max_common_pair_distance = 1000 +collect_factors = "auto" + +[evaluator.jit] +optimization_level = 3 +compress = false + +[evaluation] +artifact = "artifacts/uubar_z6g_all_flows_single_helicity" +process = "u u~ > Z g g g g g g" +precision = 16 + +[benchmark] +target_runtime = 20.0 +batch_size = 64 +precision = 16 +warmup_runs = 2 +minimum_samples = 5 +helicity_ids = ["h:-1,+1,-1,+1,-1,+1,-1,+1,-1"] + +[output] +format = "human" +color = "auto" +progress = "auto" +log_level = "info" diff --git a/pyamplicol/examples/benchmark_z6g_generation_specialized_all_flows_single_helicity.toml b/pyamplicol/examples/benchmark_z6g_generation_specialized_all_flows_single_helicity.toml new file mode 100644 index 0000000000..a129cd668e --- /dev/null +++ b/pyamplicol/examples/benchmark_z6g_generation_specialized_all_flows_single_helicity.toml @@ -0,0 +1,74 @@ +# SPDX-License-Identifier: 0BSD +# Exercise compiled JIT O3 with one source helicity fixed at generation time +# and a complete LC-flow sum. This is the generation-specialized baseline for +# the reusable runtime-helicity selector comparison documented in README.md. + +schema_version = 1 +action = "generate" + +[model] +source = "built-in-sm" +cache = true + +[process] +entries = [ + { expression = "u u~ > Z g g g g g g", name = "uubar_Z_6g" }, +] +# Source labels are 1:u, 2:u~, 3:Z, and 4..9:the six gluons. Unlike the +# reusable example, this card intentionally fixes one helicity assignment +# during generation and cannot switch to another assignment at runtime. +selected_source_helicities = { "1" = -1, "2" = 1, "3" = -1, "4" = 1, "5" = -1, "6" = 1, "7" = -1, "8" = 1, "9" = -1 } + +[color] +accuracy = "lc" + +[generation] +output = "artifacts/uubar_z6g_generation_specialized_all_flows_single_helicity" +mode = "error" +workers = 1 +emit_api_bundle = true + +[generation.validation] +enabled = true +samples = 10 +seed = 12345 +relative_tolerance = 1e-12 +absolute_tolerance = 1e-300 +post_build_validation = false + +[evaluator] +backend = "jit" +execution_mode = "compiled" +batch_size = 64 +output_chunk_size = 512 + +[evaluator.optimization] +horner_iterations = 10 +cores = 1 +max_horner_variables = 1000 +max_common_pair_cache_entries = 5000000 +max_common_pair_distance = 1000 +collect_factors = "auto" + +[evaluator.jit] +optimization_level = 3 +compress = false + +[evaluation] +artifact = "artifacts/uubar_z6g_generation_specialized_all_flows_single_helicity" +process = "u u~ > Z g g g g g g" +precision = 16 + +[benchmark] +target_runtime = 20.0 +batch_size = 64 +precision = 16 +warmup_runs = 2 +minimum_samples = 5 +helicity_ids = ["h:-1,+1,-1,+1,-1,+1,-1,+1,-1"] + +[output] +format = "human" +color = "auto" +progress = "auto" +log_level = "info" diff --git a/pyamplicol/examples/benchmark_z6g_generation_specialized_flow_helicity_sum.toml b/pyamplicol/examples/benchmark_z6g_generation_specialized_flow_helicity_sum.toml new file mode 100644 index 0000000000..3c3807c195 --- /dev/null +++ b/pyamplicol/examples/benchmark_z6g_generation_specialized_flow_helicity_sum.toml @@ -0,0 +1,75 @@ +# SPDX-License-Identifier: 0BSD +# Exercise compiled JIT O3 with one LC flow fixed at generation time and a +# complete helicity sum. This is the specialized baseline for the reusable +# selector comparison documented in examples/README.md. + +schema_version = 1 +action = "generate" + +[model] +source = "built-in-sm" +cache = true + +[process] +entries = [ + { expression = "u u~ > Z g g g g g g", name = "uubar_Z_6g" }, +] +# Source labels are 1:u, 2:u~, 3:Z, and 4..9:the six gluons. Unlike the +# reusable examples, this card intentionally fixes one physical flow during +# generation and must therefore be labeled as generation-specialized. +reference_color_order = [2, 4, 5, 6, 7, 8, 9, 1, 3] +selected_color_sector_ids = [0] + +[color] +accuracy = "lc" + +[generation] +output = "artifacts/uubar_z6g_generation_specialized_flow_helicity_sum" +mode = "error" +workers = 1 +emit_api_bundle = true + +[generation.validation] +enabled = true +samples = 10 +seed = 12345 +relative_tolerance = 1e-12 +absolute_tolerance = 1e-300 +post_build_validation = false + +[evaluator] +backend = "jit" +execution_mode = "compiled" +batch_size = 64 +output_chunk_size = 512 + +[evaluator.optimization] +horner_iterations = 10 +cores = 1 +max_horner_variables = 1000 +max_common_pair_cache_entries = 5000000 +max_common_pair_distance = 1000 +collect_factors = "auto" + +[evaluator.jit] +optimization_level = 3 +compress = false + +[evaluation] +artifact = "artifacts/uubar_z6g_generation_specialized_flow_helicity_sum" +process = "u u~ > Z g g g g g g" +precision = 16 + +[benchmark] +target_runtime = 20.0 +batch_size = 64 +precision = 16 +warmup_runs = 2 +minimum_samples = 5 +color_flow_ids = ["1"] + +[output] +format = "human" +color = "auto" +progress = "auto" +log_level = "info" diff --git a/pyamplicol/examples/benchmark_z6g_single_flow_helicity_sum.toml b/pyamplicol/examples/benchmark_z6g_single_flow_helicity_sum.toml new file mode 100644 index 0000000000..a1816f7255 --- /dev/null +++ b/pyamplicol/examples/benchmark_z6g_single_flow_helicity_sum.toml @@ -0,0 +1,78 @@ +# SPDX-License-Identifier: 0BSD +# Exercise the reusable compiled JIT O3 single-flow, helicity-sum workload. +# +# From a copied examples directory, generate and profile with: +# pyamplicol generate --card benchmark_z6g_single_flow_helicity_sum.toml +# pyamplicol profile --card benchmark_z6g_single_flow_helicity_sum.toml + +schema_version = 1 +action = "generate" + +[model] +source = "built-in-sm" +cache = true + +[process] +entries = [ + { expression = "u u~ > Z g g g g g g", name = "uubar_Z_6g" }, +] +# No generation-time flow selection is used: the artifact retains every +# physical LC flow and can select any flow globally or per point at runtime. + +[color] +accuracy = "lc" +# The default topology-replay layout is optimized for selecting one physical +# flow at runtime while summing the retained helicities. + +[generation] +output = "artifacts/uubar_z6g_single_flow_helicity_sum" +mode = "error" +workers = 1 +emit_api_bundle = true + +[generation.validation] +enabled = true +samples = 10 +seed = 12345 +relative_tolerance = 1e-12 +absolute_tolerance = 1e-300 +post_build_validation = false + +[evaluator] +backend = "jit" +execution_mode = "compiled" +batch_size = 64 +output_chunk_size = 512 + +[evaluator.optimization] +horner_iterations = 10 +cores = 1 +max_horner_variables = 1000 +max_common_pair_cache_entries = 5000000 +max_common_pair_distance = 1000 +collect_factors = "auto" + +[evaluator.jit] +optimization_level = 3 +compress = false + +[evaluation] +artifact = "artifacts/uubar_z6g_single_flow_helicity_sum" +process = "u u~ > Z g g g g g g" +precision = 16 + +[benchmark] +target_runtime = 20.0 +batch_size = 64 +precision = 16 +warmup_runs = 2 +minimum_samples = 5 +# One-based ordinal 1 resolves to the first physical flow exposed by the +# artifact. Stable flow IDs such as "flow:..." are accepted as well. +color_flow_ids = ["1"] + +[output] +format = "human" +color = "auto" +progress = "auto" +log_level = "info" diff --git a/pyamplicol/examples/builtin_sm_adjoint_fft.toml b/pyamplicol/examples/builtin_sm_adjoint_fft.toml new file mode 100644 index 0000000000..390511ef36 --- /dev/null +++ b/pyamplicol/examples/builtin_sm_adjoint_fft.toml @@ -0,0 +1,31 @@ +# SPDX-License-Identifier: 0BSD +# Pure-gluon full-color generation in the two-anchor adjoint DDM FFT basis. +# Primary FFT example for certified Yang-Mills trees; adjoint is explicit below. +# Five gluons retain 3! ordered amplitudes instead of the trace basis's 4!. +# This smaller basis does not guarantee a runtime speedup at every multiplicity. + +schema_version = 1 +action = "generate" + +[model] +source = "built-in-sm" + +[process] +entries = [{ expression = "g g > g g g", name = "gg_ggg_adjoint_fft" }] + +[color] +accuracy = "full" +contraction = "symmetric-group-fft" +fft_basis = "adjoint" + +[generation] +output = "artifacts/builtin_sm_adjoint_fft" +mode = "error" +emit_api_bundle = true + +[evaluator] +# Both recurrence and on-the-fly support trace and adjoint FFT contraction. +execution_mode = "recurrence" + +[output] +progress = "log" diff --git a/pyamplicol/examples/builtin_sm_eager.toml b/pyamplicol/examples/builtin_sm_eager.toml new file mode 100644 index 0000000000..cb5481f415 --- /dev/null +++ b/pyamplicol/examples/builtin_sm_eager.toml @@ -0,0 +1,37 @@ +# SPDX-License-Identifier: 0BSD +# Built-in Standard Model generation with prepared eager-DAG JIT O2 kernels. +# Uses the wheel's automatically selected x86_64 or aarch64 prepared pack. + +schema_version = 1 +action = "generate" + +[model] +source = "built-in-sm" + +[process] +entries = [{ expression = "d d~ > z g g", name = "eager_zgg" }] + +[color] +accuracy = "lc" + +[generation] +output = "artifacts/builtin_sm_eager" +mode = "error" +emit_api_bundle = true + +[evaluator] +execution_mode = "eager" +backend = "jit" +batch_size = 128 + +[evaluator.jit] +optimization_level = 2 +compress = false + +[evaluator.eager] +point_tile_size = 1024 +workspace_mib = 256 + +[output] +progress = "auto" +log_level = "info" diff --git a/pyamplicol/examples/builtin_sm_full.toml b/pyamplicol/examples/builtin_sm_full.toml new file mode 100644 index 0000000000..11eb83738e --- /dev/null +++ b/pyamplicol/examples/builtin_sm_full.toml @@ -0,0 +1,33 @@ +# SPDX-License-Identifier: 0BSD +# Built-in Standard Model generation with the full color contraction. +# Resolved runtime output has one contracted color entry per helicity. + +schema_version = 1 +action = "generate" + +[model] +source = "built-in-sm" + +[process] +entries = [{ expression = "u u~ > g g", name = "uubar_gg_full" }] + +[color] +accuracy = "full" + +[generation] +output = "artifacts/builtin_sm_full" +mode = "error" +emit_api_bundle = true + +[evaluator] +backend = "cpp" +# The C++ example intentionally selects process-local compiled execution; +# the default recurrence mode uses the packaged portable JIT O2 kernel pack. +execution_mode = "compiled" + +[evaluator.cpp] +optimization = "O3" +native_arch = false + +[output] +progress = "log" diff --git a/pyamplicol/examples/builtin_sm_heft.toml b/pyamplicol/examples/builtin_sm_heft.toml new file mode 100644 index 0000000000..cf50b3d1c8 --- /dev/null +++ b/pyamplicol/examples/builtin_sm_heft.toml @@ -0,0 +1,33 @@ +# SPDX-License-Identifier: 0BSD +# Packaged scalar HEFT generation for g g > H g g at full colour. +# Uses the wheel-owned prepared JIT O2 kernel bundle and recurrence schedule. +# Explicit HIG=1 certifies a single insertion for adaptive adjoint DDM FFT. + +schema_version = 1 +action = "generate" + +[model] +source = "built-in-sm-heft" + +[process] +entries = [{ expression = "g g > H g g", name = "gg_hgg" }] +coupling_order_policy = "explicit" +max_coupling_orders = { HIG = 1 } + +[color] +accuracy = "full" +contraction = "symmetric-group-fft" +fft_basis = "adjoint" + +[generation] +output = "artifacts/builtin_sm_heft" +mode = "error" +emit_api_bundle = true + +[evaluator] +execution_mode = "recurrence" +backend = "jit" + +[output] +progress = "auto" +log_level = "info" diff --git a/pyamplicol/examples/builtin_sm_lc.toml b/pyamplicol/examples/builtin_sm_lc.toml new file mode 100644 index 0000000000..647de0093b --- /dev/null +++ b/pyamplicol/examples/builtin_sm_lc.toml @@ -0,0 +1,32 @@ +# SPDX-License-Identifier: 0BSD +# Built-in Standard Model generation at leading color with default recurrence. +# Writes a transactional schema-v3 artifact and root API bundle. + +schema_version = 1 +action = "generate" + +[model] +source = "built-in-sm" + +[process] +entries = [{ expression = "u u~ > g g", name = "uubar_gg" }] + +[color] +accuracy = "lc" + +[generation] +output = "artifacts/builtin_sm_lc" +mode = "error" +emit_api_bundle = true + +[evaluator] +backend = "jit" +batch_size = 128 + +[evaluator.jit] +optimization_level = 2 +compress = false + +[output] +progress = "auto" +log_level = "info" diff --git a/pyamplicol/examples/builtin_sm_nlc.toml b/pyamplicol/examples/builtin_sm_nlc.toml new file mode 100644 index 0000000000..7696f942ee --- /dev/null +++ b/pyamplicol/examples/builtin_sm_nlc.toml @@ -0,0 +1,30 @@ +# SPDX-License-Identifier: 0BSD +# Built-in Standard Model default-recurrence generation with NLC contraction. +# Resolved runtime output has one contracted color entry per helicity. + +schema_version = 1 +action = "generate" + +[model] +source = "built-in-sm" + +[process] +entries = [{ expression = "u u~ > g g", name = "uubar_gg_nlc" }] + +[color] +accuracy = "nlc" + +[generation] +output = "artifacts/builtin_sm_nlc" +mode = "error" +emit_api_bundle = true + +[evaluator] +backend = "jit" + +[evaluator.jit] +optimization_level = 2 +compress = false + +[output] +progress = "log" diff --git a/pyamplicol/examples/builtin_sm_on_the_fly.toml b/pyamplicol/examples/builtin_sm_on_the_fly.toml new file mode 100644 index 0000000000..6ac5a72932 --- /dev/null +++ b/pyamplicol/examples/builtin_sm_on_the_fly.toml @@ -0,0 +1,37 @@ +# SPDX-License-Identifier: 0BSD +# Built-in Standard Model LC generation with compact on-the-fly execution. +# Uses the wheel-owned prepared JIT O2 kernel pack at native f64 precision. + +schema_version = 1 +action = "generate" + +[model] +source = "built-in-sm" + +[process] +entries = [{ expression = "d d~ > z g g", name = "on_the_fly_zgg" }] + +[color] +accuracy = "lc" + +[generation] +output = "artifacts/builtin_sm_on_the_fly" +mode = "error" +emit_api_bundle = false + +[evaluator] +execution_mode = "on-the-fly" +backend = "jit" +batch_size = 128 + +[evaluator.optimization] +# Query-construction request; numerical evaluation is not promised four threads. +cores = 4 + +[evaluator.jit] +optimization_level = 2 +compress = false + +[output] +progress = "auto" +log_level = "info" diff --git a/pyamplicol/examples/data/model_parameters.json b/pyamplicol/examples/data/model_parameters.json new file mode 100644 index 0000000000..2f0fde26be --- /dev/null +++ b/pyamplicol/examples/data/model_parameters.json @@ -0,0 +1,4 @@ +{ + "aS": [0.117, 0.0], + "MZ": [91.188, 0.0] +} diff --git a/pyamplicol/examples/data/pp_4j_momenta.json b/pyamplicol/examples/data/pp_4j_momenta.json new file mode 100644 index 0000000000..d771c8208f --- /dev/null +++ b/pyamplicol/examples/data/pp_4j_momenta.json @@ -0,0 +1,10 @@ +[ + [ + [500.0, 0.0, 0.0, 500.0], + [500.0, 0.0, 0.0, -500.0], + [128.3787399982121, -71.23031764614652, 47.087854014801664, -95.86488791960448], + [334.43918837698175, 304.7133406265853, -93.4909089428669, 101.28573794704494], + [235.17705547671883, 1.8638452993958485, 234.86942331328228, 11.879709419251002], + [302.0050161480876, -235.34686827983452, -188.46636838521704, -17.30055944669148] + ] +] diff --git a/pyamplicol/examples/data/pp_zjj_momenta.json b/pyamplicol/examples/data/pp_zjj_momenta.json new file mode 100644 index 0000000000..9674ef5dbe --- /dev/null +++ b/pyamplicol/examples/data/pp_zjj_momenta.json @@ -0,0 +1,9 @@ +[ + [ + [500.0, 0.0, 0.0, 500.0], + [500.0, 0.0, 0.0, -500.0], + [369.7738416261408, -17.479290785282917, 2.0064955613504103, 369.3550355960509], + [462.6501613061637, 14.340107538562991, 155.76435943335707, -425.7484539710246], + [167.57599706769557, 3.1391832467199254, -157.77085499470743, 56.3934183749737] + ] +] diff --git a/pyamplicol/examples/evaluate_pp_4j_member.toml b/pyamplicol/examples/evaluate_pp_4j_member.toml new file mode 100644 index 0000000000..7a244a00f0 --- /dev/null +++ b/pyamplicol/examples/evaluate_pp_4j_member.toml @@ -0,0 +1,20 @@ +# SPDX-License-Identifier: 0BSD +# Evaluate u~ g > u~ g g g, a flavour-blind member that was not generated. +# It is routed through its representative d g > d g g g by the relabelling +# u -> d plus charge conjugation, at momenta given in the member's leg order. + +schema_version = 1 +action = "evaluate" + +[evaluation] +artifact = "artifacts/pp_4j_flavor_blind" +# Any member of the 486 subprocesses, in any order within each side of ">", +# is accepted, for example "g u~ > g g g u~" or "u~ u > u~ u g g". +process = "u~ g > u~ g g g" +precision = 16 +momenta = "data/pp_4j_momenta.json" + +[output] +format = "human" +color = "auto" +progress = "off" diff --git a/pyamplicol/examples/evaluate_resolved.toml b/pyamplicol/examples/evaluate_resolved.toml new file mode 100644 index 0000000000..9ef91566b7 --- /dev/null +++ b/pyamplicol/examples/evaluate_resolved.toml @@ -0,0 +1,21 @@ +# SPDX-License-Identifier: 0BSD +# Helicity/color-resolved evaluation of one UFO-SM pp -> Zjj subprocess. +# Empty selector arrays mean all physical IDs stored in the artifact. + +schema_version = 1 +action = "evaluate" + +[evaluation] +artifact = "artifacts/pp_zjj" +process = "d d~ > g z g" +precision = 16 +resolved = true +helicity_ids = [] +color_flow_ids = [] +model_parameters = "data/model_parameters.json" +momenta = "data/pp_zjj_momenta.json" + +[output] +format = "human" +color = "auto" +progress = "off" diff --git a/pyamplicol/examples/evaluate_total.toml b/pyamplicol/examples/evaluate_total.toml new file mode 100644 index 0000000000..5994536c57 --- /dev/null +++ b/pyamplicol/examples/evaluate_total.toml @@ -0,0 +1,18 @@ +# SPDX-License-Identifier: 0BSD +# Fully summed evaluation of one UFO-SM pp -> Zjj subprocess. + +schema_version = 1 +action = "evaluate" + +[evaluation] +artifact = "artifacts/pp_zjj" +process = "d d~ > g z g" +precision = 16 +resolved = false +model_parameters = "data/model_parameters.json" +momenta = "data/pp_zjj_momenta.json" + +[output] +format = "human" +color = "auto" +progress = "off" diff --git a/pyamplicol/examples/external_json_scalar_gravity.toml b/pyamplicol/examples/external_json_scalar_gravity.toml new file mode 100644 index 0000000000..df648698d4 --- /dev/null +++ b/pyamplicol/examples/external_json_scalar_gravity.toml @@ -0,0 +1,27 @@ +# SPDX-License-Identifier: 0BSD +# Serialized spin-2 scalar-gravity model named "scalar_gravity". +# `pyamplicol examples copy` materializes this model beside the card. + +schema_version = 1 +action = "generate" + +[model] +source = "models/json/scalar_gravity/scalar_gravity.json" +simplify = true + +[process] +entries = [ + { expression = "scalar_0 scalar_0 > graviton graviton", name = "scalar_gravity_2to2" }, + { expression = "scalar_0 scalar_0 > 3*graviton", name = "scalar_gravity_2to3" }, +] + +[color] +accuracy = "lc" + +[generation] +output = "artifacts/external_json_scalar_gravity" +emit_api_bundle = true + +[evaluator] +backend = "jit" +execution_mode = "compiled" diff --git a/pyamplicol/examples/external_json_scalars.toml b/pyamplicol/examples/external_json_scalars.toml new file mode 100644 index 0000000000..46e94e1a8a --- /dev/null +++ b/pyamplicol/examples/external_json_scalars.toml @@ -0,0 +1,30 @@ +# SPDX-License-Identifier: 0BSD +# Serialized scalar contact model named "scalars". +# `pyamplicol examples copy` materializes this model beside the card. + +schema_version = 1 +action = "generate" + +[model] +source = "models/json/scalars/scalars.json" +simplify = true + +[process] +entries = [ + { expression = "scalar_0 scalar_0 > scalar_0 scalar_0", name = "scalar_contact_2to2" }, + { expression = "scalar_0 scalar_0 > 3*scalar_0", name = "scalar_contact_2to3" }, +] + +[process.max_coupling_orders] +QED = 6 + +[color] +accuracy = "lc" + +[generation] +output = "artifacts/external_json_scalars" +emit_api_bundle = true + +[evaluator] +backend = "jit" +execution_mode = "compiled" diff --git a/pyamplicol/examples/external_ufo_sm.toml b/pyamplicol/examples/external_ufo_sm.toml new file mode 100644 index 0000000000..128df42b65 --- /dev/null +++ b/pyamplicol/examples/external_ufo_sm.toml @@ -0,0 +1,26 @@ +# SPDX-License-Identifier: 0BSD +# Trusted external UFO model named "sm". +# `pyamplicol examples copy` materializes models/ufo/sm beside this card. + +schema_version = 1 +action = "generate" + +[model] +source = "models/ufo/sm" +restriction = "default" +simplify = true +cache = true + +[process] +entries = [{ expression = "u u~ > Z g", name = "ufo_sm_uubar_Zg" }] + +[color] +accuracy = "lc" + +[generation] +output = "artifacts/external_ufo_sm" +emit_api_bundle = true + +[evaluator] +backend = "jit" +execution_mode = "compiled" diff --git a/pyamplicol/examples/generate_pp_4j_flavor_blind.toml b/pyamplicol/examples/generate_pp_4j_flavor_blind.toml new file mode 100644 index 0000000000..8c6d3d193c --- /dev/null +++ b/pyamplicol/examples/generate_pp_4j_flavor_blind.toml @@ -0,0 +1,44 @@ +# SPDX-License-Identifier: 0BSD +# Flavour-blind built-in SM p p > j j j j at QED = 0 and leading color. +# Generates 23 class representatives and records the other 463 of the 486 +# five-flavour subprocesses as callable members in process-groups.json. + +schema_version = 1 +action = "generate" + +[model] +source = "built-in-sm" + +[process] +entries = [{ expression = "p p > j j j j" }] +# Omitted flavor_scheme keeps the default five massless quark flavours; the +# model's default p and j multiparticles contain all of them and the gluon. +# One representative per flavour-relabelling class is generated. Equal matrix +# elements within a class are assumed, not verified; they hold for pure-QCD +# jets with massless quarks, so this request produces no relabelling warning. +flavor_blind = true +# Also relate classes by charge conjugation (the default): 23 instead of 33 +# representatives. +consider_charge_conjugation = true + +[process.max_coupling_orders] +# Pure QCD: no photon, Z, or W exchange between the quark lines. +QED = 0 + +[color] +accuracy = "lc" + +[generation] +output = "artifacts/pp_4j_flavor_blind" +mode = "error" +emit_api_bundle = true + +[evaluator] +# Omitted execution_mode uses recurrence over the wheel-owned prepared +# JIT O2 kernels of the built-in model. +backend = "jit" +batch_size = 128 + +[output] +progress = "auto" +log_level = "info" diff --git a/pyamplicol/examples/generate_pp_zjj_from_ufo_sm.toml b/pyamplicol/examples/generate_pp_zjj_from_ufo_sm.toml new file mode 100644 index 0000000000..fffb19506b --- /dev/null +++ b/pyamplicol/examples/generate_pp_zjj_from_ufo_sm.toml @@ -0,0 +1,39 @@ +# SPDX-License-Identifier: 0BSD +# Generate p p > Z j j from the serialized form of the UFO Standard Model. +# `pyamplicol examples copy` materializes models/json/sm beside this card. + +schema_version = 1 +action = "generate" + +[model] +source = "models/json/sm/sm.json" +restriction = "default" +simplify = true + +[process] +entries = [{ expression = "p p > Z j j" }] +flavor_scheme = 2 +max_quark_lines = 2 + +[process.multiparticles] +p = ["d", "d~", "g"] +j = ["d", "d~", "g"] + +[color] +accuracy = "lc" +# Omitted lc_flow_layout defaults to non-union "topology-replay". +# Set it to "all-flow-union" to generate the LC union-flow layout instead. + +[generation] +output = "artifacts/pp_zjj" +emit_api_bundle = true + +[evaluator] +backend = "jit" +# Raw portable model IR has no prepared recurrence pack. This example +# intentionally demonstrates process-local compiled DAG generation. +execution_mode = "compiled" + +[evaluator.jit] +# Omitted optimization_level uses the portable JIT O2 default. +compress = "auto" diff --git a/pyamplicol/examples/group_pp_4j.toml b/pyamplicol/examples/group_pp_4j.toml new file mode 100644 index 0000000000..85ae16213f --- /dev/null +++ b/pyamplicol/examples/group_pp_4j.toml @@ -0,0 +1,21 @@ +# SPDX-License-Identifier: 0BSD +# Numerically group the generated representatives of the flavour-blind +# p p > j j j j artifact and update its process-groups.json in place. + +schema_version = 1 +action = "group-processes" + +[process] +# Run cards use the schema default "none"; request the numerical analysis. +analyze_process_groups = "numerical" +only_unit_multiplier = false +consider_charge_conjugation = true + +[evaluation] +artifact = "artifacts/pp_4j_flavor_blind" + +[output] +format = "human" +color = "auto" +progress = "auto" +log_level = "info" diff --git a/pyamplicol/examples/inspect_pp_4j.toml b/pyamplicol/examples/inspect_pp_4j.toml new file mode 100644 index 0000000000..8a1907c93b --- /dev/null +++ b/pyamplicol/examples/inspect_pp_4j.toml @@ -0,0 +1,14 @@ +# SPDX-License-Identifier: 0BSD +# Summarize the flavour-blind p p > j j j j artifact: its 23 generated +# representatives and the process-group map of all 486 subprocesses. + +schema_version = 1 +action = "inspect" + +[evaluation] +artifact = "artifacts/pp_4j_flavor_blind" + +[output] +format = "human" +color = "auto" +progress = "off" diff --git a/pyamplicol/examples/native/Makefile b/pyamplicol/examples/native/Makefile new file mode 100644 index 0000000000..faf6409869 --- /dev/null +++ b/pyamplicol/examples/native/Makefile @@ -0,0 +1,58 @@ +# SPDX-License-Identifier: 0BSD + +CC ?= cc +CXX ?= c++ +RUSTC ?= rustc +ifeq ($(origin FC), default) +FC := gfortran +endif +FC ?= gfortran +CFLAGS ?= -O2 +CXXFLAGS ?= -O2 +FFLAGS ?= -O2 +RUSTICOL_CONFIG ?= rusticol-config +RUSTICOL_CONFIG_PATH := $(shell \ + if command -v "$(RUSTICOL_CONFIG)" >/dev/null 2>&1; then \ + command -v "$(RUSTICOL_CONFIG)"; \ + elif [ "$(RUSTICOL_CONFIG)" = rusticol-config ]; then \ + directory="$(CURDIR)"; \ + while [ "$$directory" != / ]; do \ + candidate="$$directory/.venv/bin/rusticol-config"; \ + if [ -x "$$candidate" ]; then printf '%s\n' "$$candidate"; break; fi; \ + directory=$$(dirname "$$directory"); \ + done; \ + fi) +ifeq ($(strip $(RUSTICOL_CONFIG_PATH)),) +$(error rusticol-config is unavailable; activate the pyAmpliCol environment or set RUSTICOL_CONFIG=/absolute/path) +endif + +RUSTICOL_CFLAGS := $(shell "$(RUSTICOL_CONFIG_PATH)" --cflags) +RUSTICOL_LIBS := $(shell "$(RUSTICOL_CONFIG_PATH)" --libs) +RUSTICOL_FORTRAN := $(shell "$(RUSTICOL_CONFIG_PATH)" --fortran-source) +RUSTICOL_RUST := $(shell "$(RUSTICOL_CONFIG_PATH)" --rust-source) +RUSTICOL_RUSTFLAGS := $(shell "$(RUSTICOL_CONFIG_PATH)" --rustflags) + +.PHONY: all correlated +all: runtime_cpp runtime_fortran +correlated: correlated_c correlated_cpp correlated_fortran correlated_rust + +runtime_cpp: runtime.cpp + $(CXX) $(CXXFLAGS) -std=c++17 $< $(RUSTICOL_CFLAGS) \ + $(RUSTICOL_LIBS) -o $@ + +runtime_fortran: runtime.f90 + $(FC) $(FFLAGS) -std=f2008 "$(RUSTICOL_FORTRAN)" $< \ + $(RUSTICOL_LIBS) -o $@ + +correlated_c: correlated.c + $(CC) $(CFLAGS) -std=c11 $< $(RUSTICOL_CFLAGS) $(RUSTICOL_LIBS) -o $@ + +correlated_cpp: correlated.cpp + $(CXX) $(CXXFLAGS) -std=c++17 $< $(RUSTICOL_CFLAGS) $(RUSTICOL_LIBS) -o $@ + +correlated_fortran: correlated.f90 + $(FC) $(FFLAGS) -std=f2008 "$(RUSTICOL_FORTRAN)" $< $(RUSTICOL_LIBS) -o $@ + +correlated_rust: correlated.rs + RUSTICOL_RUST_SOURCE="$(RUSTICOL_RUST)" $(RUSTC) --edition=2021 \ + -C opt-level=2 $< $(RUSTICOL_RUSTFLAGS) -o $@ diff --git a/pyamplicol/examples/native/correlated.c b/pyamplicol/examples/native/correlated.c new file mode 100644 index 0000000000..b1c85ec714 --- /dev/null +++ b/pyamplicol/examples/native/correlated.c @@ -0,0 +1,120 @@ +/* SPDX-License-Identifier: 0BSD */ +/* Generate the d d~ > d d~ g example in the correlator guide first. */ +#include +#include +#include +#include +#include + +static void check(int status) { + if (status != RUSTICOL_STATUS_OK) { + char error[2048]; + size_t required = 0; + rusticol_last_error_message(error, sizeof error, &required); + fprintf(stderr, "Rusticol status %d: %s\n", status, + required <= sizeof error ? error : "error message too long"); + exit(1); + } +} + +static void same(double actual, double expected) { + if (fabs(actual - expected) > 1.e-12 * fmax(fabs(expected), 1.e-12)) { + fprintf(stderr, "mismatch: %.17g versus %.17g\n", actual, expected); + exit(1); + } +} + +int main(int argc, char **argv) { + if (argc != 3) { + fprintf(stderr, "usage: correlated_c ARTIFACT PROCESS\n"); + return 2; + } + RusticolRuntimeHandle *runtime = NULL; + check(rusticol_runtime_load(argv[1], argv[2], NULL, &runtime)); + size_t count = 0, required = 0; + check(rusticol_runtime_color_correlation_count(runtime, &count)); + int found_born = 0, found_interference = 0; + for (size_t index = 0; index < count; ++index) { + check(rusticol_runtime_color_correlation_id(runtime, index, NULL, 0, &required)); + char *id = malloc(required); + if (!id) return 1; + check(rusticol_runtime_color_correlation_id(runtime, index, id, required, &required)); + found_born |= strcmp(id, "born") == 0; + found_interference |= strcmp(id, "cascade-interference") == 0; + free(id); + } + if (!found_born || !found_interference) return 1; + check(rusticol_runtime_color_correlation_catalogue_json(runtime, NULL, 0, &required)); + if (required < 2) return 1; + + const double momenta[] = { + 400,0,0,400, 400,0,0,-400, + 300,300,0,0, 250,-150,200,0, 250,-150,-200,0, + 400,0,0,400, 400,0,0,-400, + 300,0,300,0, 250,200,-150,0, 250,-200,-150,0, + }; + const double spin[] = {0,0, 0,0, 0,0, 1,0}; + const RusticolSpinCorrelationVector vector = {5, 1, spin, 8}; + const RusticolCorrelatedRequest requests[] = { + {"born", NULL, 0, 0}, + {"cascade-interference", NULL, 0, 0}, + {"cascade-interference", &vector, 1, 0}, + }; + double values[12], ordinary_before[2], ordinary_after[2]; + check(rusticol_runtime_evaluate_f64(runtime, momenta, 40, 2, ordinary_before, 2)); + check(rusticol_runtime_evaluate_correlated_many_f64( + runtime, momenta, 40, 2, requests, 3, NULL, 0, values, 12)); + for (size_t request = 0; request < 3; ++request) { + for (size_t point = 0; point < 2; ++point) { + size_t index = 2 * (2 * request + point); + printf("VALUE %zu %zu %.17g %.17g\n", request, point, + values[index], values[index + 1]); + } + } + check(rusticol_runtime_set_spin_correlation_vectors_f64(runtime, &vector, 1)); + RusticolCorrelatedRequest inherited = {"cascade-interference", NULL, 0, 1}; + double selected[4]; + check(rusticol_runtime_evaluate_correlated_many_f64( + runtime, momenta, 40, 2, &inherited, 1, NULL, 0, selected, 4)); + for (size_t index = 0; index < 4; ++index) same(selected[index], values[8 + index]); + check(rusticol_runtime_evaluate_f64(runtime, momenta, 40, 2, ordinary_after, 2)); + for (size_t point = 0; point < 2; ++point) same(ordinary_after[point], ordinary_before[point]); + check(rusticol_runtime_set_spin_correlation_vectors_f64(runtime, NULL, 0)); + check(rusticol_runtime_evaluate_correlated_many_f64( + runtime, momenta, 40, 2, &inherited, 1, NULL, 0, selected, 4)); + for (size_t index = 0; index < 4; ++index) same(selected[index], values[4 + index]); + /* Per-point complex scaling: |i|^2=1 and |2|^2=4. */ + const double scaled[] = {0,0, 0,0, 0,0, 0,1, 0,0, 0,0, 0,0, 2,0}; + RusticolSpinCorrelationVector per_point = {5, 2, scaled, 16}; + RusticolCorrelatedRequest varied = {"cascade-interference", &per_point, 1, 0}; + check(rusticol_runtime_evaluate_correlated_many_f64( + runtime, momenta, 40, 2, &varied, 1, NULL, 0, selected, 4)); + same(selected[0], values[8]); + same(selected[2], 4 * values[10]); + /* Ward identity: substitute the on-shell momentum on gluon leg 5. */ + double longitudinal[16]; + for (size_t point = 0; point < 2; ++point) { + for (size_t mu = 0; mu < 4; ++mu) { + longitudinal[8 * point + 2 * mu] = momenta[20 * point + 16 + mu]; + longitudinal[8 * point + 2 * mu + 1] = 0; + } + } + per_point.components = longitudinal; + check(rusticol_runtime_evaluate_correlated_many_f64( + runtime, momenta, 40, 2, &varied, 1, NULL, 0, selected, 4)); + for (size_t point = 0; point < 2; ++point) { + if (hypot(selected[2 * point], selected[2 * point + 1]) + > 1.e-15 * 800 * 800 * fabs(values[2 * point])) return 1; + } + /* Errors do not partially overwrite the caller's output. */ + selected[0] = 123; + if (rusticol_runtime_evaluate_correlated_many_f64( + runtime, momenta, 40, 2, requests, 3, NULL, 0, selected, 4) + != RUSTICOL_STATUS_BUFFER_TOO_SMALL || selected[0] != 123) return 1; + RusticolCorrelatedRequest missing = {"missing", NULL, 0, 0}; + if (rusticol_runtime_evaluate_correlated_many_f64( + runtime, momenta, 40, 2, &missing, 1, NULL, 0, selected, 4) + == RUSTICOL_STATUS_OK || selected[0] != 123) return 1; + check(rusticol_runtime_free(runtime)); + return 0; +} diff --git a/pyamplicol/examples/native/correlated.cpp b/pyamplicol/examples/native/correlated.cpp new file mode 100644 index 0000000000..5ae95c409b --- /dev/null +++ b/pyamplicol/examples/native/correlated.cpp @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: 0BSD +// Generate d d~ > d d~ g with the E.3 "cascade-interference" insertion first. +#include + +#include +#include +#include + +int main(int argc, char **argv) { + if (argc != 3) { + std::cerr << "usage: correlated_cpp ARTIFACT PROCESS\n"; + return 2; + } + try { + rusticol::Runtime runtime(argv[1], argv[2]); + const auto ids = runtime.color_correlation_ids(); + for (const auto &id : {"born", "cascade-interference"}) { + if (std::find(ids.begin(), ids.end(), id) == ids.end()) { + throw std::runtime_error("required colour correlation is not registered"); + } + } + if (runtime.color_correlation_catalogue_json().empty()) { + throw std::runtime_error("missing colour histories"); + } + const std::vector momenta{ + 400,0,0,400, 400,0,0,-400, 300,300,0,0, 250,-150,200,0, 250,-150,-200,0, + 400,0,0,400, 400,0,0,-400, 300,0,300,0, 250,200,-150,0, 250,-200,-150,0, + }; + const std::vector spins{ + {5, {{{0., 0., 0., 1.}}}}}, physical{}; + const std::vector requests{ + {"born", physical}, {"cascade-interference", physical}, + {"cascade-interference", spins}, + }; + const auto ordinary = runtime.evaluate(momenta, 2); + const auto result = runtime.evaluate_correlated_many(momenta, 2, requests); + runtime.set_spin_correlation_vectors(spins); + const auto inherited = runtime.evaluate_correlated(momenta, 2, "cascade-interference"); + const auto after = runtime.evaluate(momenta, 2); + runtime.set_spin_correlation_vectors(); + const auto cleared = runtime.evaluate_correlated(momenta, 2, "cascade-interference"); + const auto close = [](std::complex a, std::complex b) { + return std::abs(a - b) <= 1e-11 * std::max(1e-100, std::max(std::abs(a), std::abs(b))); + }; + for (std::size_t point = 0; point < 2; ++point) { + if (!close(inherited[point], result(2, point)) || + !close(cleared[point], result(1, point)) || + ordinary[point] != after[point]) { + throw std::runtime_error("setter or ordinary-evaluation isolation check failed"); + } + } + std::cout << std::setprecision(17); + for (std::size_t request = 0; request < requests.size(); ++request) { + for (std::size_t point = 0; point < 2; ++point) { + const auto value = result(request, point); + std::cout << "VALUE " << request << ' ' << point << ' ' + << value.real() << ' ' << value.imag() << '\n'; + } + } + } catch (const std::exception &error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/pyamplicol/examples/native/correlated.f90 b/pyamplicol/examples/native/correlated.f90 new file mode 100644 index 0000000000..7f4b57f6f0 --- /dev/null +++ b/pyamplicol/examples/native/correlated.f90 @@ -0,0 +1,69 @@ +! SPDX-License-Identifier: 0BSD +! Generate d d~ > d d~ g with the E.3 "cascade-interference" insertion first. +program correlated_fortran + use, intrinsic :: iso_c_binding + use rusticol + implicit none + type(rusticol_runtime) :: runtime + type(rusticol_correlated_request) :: requests(3), inherited_request + type(rusticol_spin_correlation_vector) :: spins(1) + type(rusticol_string), allocatable :: ids(:) + character(len=4096) :: artifact, process_key + character(len=:), allocatable :: catalogue + real(c_double), target :: momenta(40) + real(c_double), allocatable :: ordinary(:), after(:) + complex(c_double_complex), allocatable :: values(:, :), inherited(:), cleared(:) + integer :: point, request, index + logical :: has_born, has_cascade + + if (command_argument_count() /= 2) error stop "usage: correlated_fortran ARTIFACT PROCESS" + call get_command_argument(1, artifact) + call get_command_argument(2, process_key) + call runtime%load(trim(artifact), process_key=trim(process_key)) + ids = runtime%color_correlation_ids() + has_born = .false. + has_cascade = .false. + do index = 1, size(ids) + has_born = has_born .or. ids(index)%value == "born" + has_cascade = has_cascade .or. ids(index)%value == "cascade-interference" + end do + if (.not. has_born .or. .not. has_cascade) error stop "required colour correlation is not registered" + catalogue = runtime%color_correlation_catalogue_json() + if (len(catalogue) == 0) error stop "missing colour histories" + momenta = real([ & + 400,0,0,400, 400,0,0,-400, 300,300,0,0, 250,-150,200,0, 250,-150,-200,0, & + 400,0,0,400, 400,0,0,-400, 300,0,300,0, 250,200,-150,0, 250,-200,-150,0 ], c_double) + spins(1)%leg = 5_c_size_t + allocate(spins(1)%components(4, 1)) + spins(1)%components(:, 1) = cmplx([0, 0, 0, 1], kind=c_double) + requests(1)%color_correlation = "born" + requests(2)%color_correlation = "cascade-interference" + requests(3)%color_correlation = "cascade-interference" + allocate(requests(1)%spin_vectors(0), requests(2)%spin_vectors(0)) + requests(3)%spin_vectors = spins + call runtime%evaluate(momenta, 2_c_size_t, ordinary) + call runtime%evaluate_correlated_many(momenta, 2_c_size_t, requests, values) + call runtime%set_spin_correlation_vectors(spins) + inherited_request%color_correlation = "cascade-interference" + call runtime%evaluate_correlated(momenta, 2_c_size_t, inherited_request, inherited) + call runtime%evaluate(momenta, 2_c_size_t, after) + if (any(ordinary /= after)) error stop "spin setter changed ordinary evaluation" + call runtime%set_spin_correlation_vectors() + call runtime%evaluate_correlated(momenta, 2_c_size_t, inherited_request, cleared) + do point = 1, 2 + if (.not. close(inherited(point), values(point, 3))) error stop "default spin source mismatch" + if (.not. close(cleared(point), values(point, 2))) error stop "cleared spin source mismatch" + end do + do request = 1, size(requests) + do point = 1, 2 + write(*, '(A,1X,I0,1X,I0,2(1X,ES25.17E3))') "VALUE", request - 1, point - 1, & + real(values(point, request), c_double), aimag(values(point, request)) + end do + end do + call runtime%close() +contains + logical function close(a, b) + complex(c_double_complex), intent(in) :: a, b + close = abs(a - b) <= 1e-11_c_double * max(1e-100_c_double, abs(a), abs(b)) + end function close +end program correlated_fortran diff --git a/pyamplicol/examples/native/correlated.rs b/pyamplicol/examples/native/correlated.rs new file mode 100644 index 0000000000..e2047c4031 --- /dev/null +++ b/pyamplicol/examples/native/correlated.rs @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: 0BSD +// Set RUSTICOL_RUST_SOURCE to `rusticol-config --json`'s rust_source when compiling. +#[allow(dead_code)] +mod rusticol { + include!(env!("RUSTICOL_RUST_SOURCE")); +} + +use rusticol::{Complex64, CorrelatedRequest, Runtime, SpinCorrelationVector}; + +fn main() -> Result<(), Box> { + let args = std::env::args().collect::>(); + if args.len() != 3 { + return Err("usage: correlated_rust ARTIFACT PROCESS".into()); + } + let mut runtime = Runtime::load(&args[1], Some(&args[2]), None)?; + let ids = runtime.color_correlation_ids()?; + for id in ["born", "cascade-interference"] { + if !ids.iter().any(|item| item == id) { + return Err("required colour correlation is not registered".into()); + } + } + assert!(!runtime.color_correlation_catalogue_json()?.is_empty()); + let momenta = [ + 400., 0., 0., 400., 400., 0., 0., -400., 300., 300., 0., 0., 250., -150., 200., 0., 250., + -150., -200., 0., 400., 0., 0., 400., 400., 0., 0., -400., 300., 0., 300., 0., 250., 200., + -150., 0., 250., -200., -150., 0., + ]; + let spins = vec![SpinCorrelationVector { + leg: 5, + components: vec![[ + Complex64::default(), + Complex64::default(), + Complex64::default(), + Complex64::new(1., 0.), + ]], + }]; + let requests = [ + CorrelatedRequest { + color_correlation: "born".into(), + spin_vectors: Some(vec![]), + }, + CorrelatedRequest { + color_correlation: "cascade-interference".into(), + spin_vectors: Some(vec![]), + }, + CorrelatedRequest { + color_correlation: "cascade-interference".into(), + spin_vectors: Some(spins.clone()), + }, + ]; + let ordinary = runtime.evaluate_f64(&momenta, 2)?; + let result = runtime.evaluate_correlated_many_f64(&momenta, 2, &requests, &[])?; + runtime.set_spin_correlation_vectors(&spins)?; + let default_request = CorrelatedRequest::new("cascade-interference"); + let inherited = runtime.evaluate_correlated_f64(&momenta, 2, &default_request, &[])?; + assert_eq!(ordinary, runtime.evaluate_f64(&momenta, 2)?); + runtime.set_spin_correlation_vectors(&[])?; + let cleared = runtime.evaluate_correlated_f64(&momenta, 2, &default_request, &[])?; + let close = |a: Complex64, b: Complex64| { + (a.re - b.re).hypot(a.im - b.im) + <= 1e-11 * a.re.hypot(a.im).max(b.re.hypot(b.im)).max(1e-100) + }; + for point in 0..2 { + assert!(close(inherited[point], result.get(2, point).unwrap())); + assert!(close(cleared[point], result.get(1, point).unwrap())); + } + for request in 0..requests.len() { + for point in 0..2 { + let value = result.get(request, point).unwrap(); + println!( + "VALUE {request} {point} {:.17e} {:.17e}", + value.re, value.im + ); + } + } + Ok(()) +} diff --git a/pyamplicol/examples/native/runtime.cpp b/pyamplicol/examples/native/runtime.cpp new file mode 100644 index 0000000000..c85c1ba16e --- /dev/null +++ b/pyamplicol/examples/native/runtime.cpp @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: 0BSD + +#include + +#include +#include +#include +#include +#include +#include + +int main(int argc, char **argv) { + if (argc < 3 || argc > 4) { + std::cerr << "usage: runtime_cpp ARTIFACT PROCESS [PARAMETERS.json]\n"; + return 2; + } + + try { + rusticol::Runtime runtime(argv[1], argv[2]); + if (argc == 4) { + runtime.set_model_parameters_json(argv[3]); + } + runtime.set_model_parameter("aS", 0.117); + + // The external-SM d d~ > Z g g subprocess p_p_to_z_j_j_3, + // flattened as + // [point][particle][E,px,py,pz]. + const std::vector momenta{ + 500.0, 0.0, 0.0, 500.0, + 500.0, 0.0, 0.0, -500.0, + 462.6501613061637, 14.340107538562991, 155.76435943335707, + -425.7484539710246, + 369.7738416261408, -17.479290785282917, 2.0064955613504103, + 369.3550355960509, + 167.57599706769557, 3.1391832467199254, -157.77085499470743, + 56.3934183749737, + }; + constexpr std::size_t point_count = 1; + const auto totals = runtime.evaluate(momenta, point_count); + const auto resolved = runtime.evaluate_resolved(momenta, point_count); + const auto checked_totals = resolved.total(); + const double scale = std::max(1.0, std::abs(totals.at(0))); + if (std::abs(totals.at(0) - checked_totals.at(0)) > 1.0e-12 * scale) { + std::cerr << "resolved components do not reproduce the total\n"; + return 1; + } + + std::cout << std::setprecision(17) + << "process=" << runtime.process_key() << "\n" + << "color_accuracy=" << runtime.color_accuracy() << "\n" + << "total=" << totals.at(0) << "\n"; + } catch (const std::exception &error) { + std::cerr << "Rusticol error: " << error.what() << "\n"; + return 1; + } + return 0; +} diff --git a/pyamplicol/examples/native/runtime.f90 b/pyamplicol/examples/native/runtime.f90 new file mode 100644 index 0000000000..f30d5930d9 --- /dev/null +++ b/pyamplicol/examples/native/runtime.f90 @@ -0,0 +1,56 @@ +! SPDX-License-Identifier: 0BSD + +program runtime_fortran + use, intrinsic :: iso_c_binding, only: c_double, c_size_t + use rusticol, only: rusticol_runtime + implicit none + + type(rusticol_runtime) :: runtime + character(len=4096) :: artifact, process_key, parameters + real(c_double), target :: momenta(20) + real(c_double), allocatable :: totals(:), resolved(:, :, :) + real(c_double) :: checked_total, scale + + if (command_argument_count() < 2 .or. command_argument_count() > 3) then + error stop "usage: runtime_fortran ARTIFACT PROCESS [PARAMETERS.json]" + end if + call get_command_argument(1, artifact) + call get_command_argument(2, process_key) + parameters = "" + if (command_argument_count() == 3) call get_command_argument(3, parameters) + + if (len_trim(parameters) > 0) then + call runtime%load(trim(artifact), process_key=trim(process_key), & + model_parameters=trim(parameters)) + else + call runtime%load(trim(artifact), process_key=trim(process_key)) + end if + call runtime%set_model_parameter( & + "aS", 0.117_c_double) + + ! The external-SM d d~ > Z g g subprocess p_p_to_z_j_j_3, + ! flattened as + ! [point][particle][E,px,py,pz]. + momenta = [ & + 500.0_c_double, 0.0_c_double, 0.0_c_double, 500.0_c_double, & + 500.0_c_double, 0.0_c_double, 0.0_c_double, -500.0_c_double, & + 462.6501613061637_c_double, 14.340107538562991_c_double, & + 155.76435943335707_c_double, -425.7484539710246_c_double, & + 369.7738416261408_c_double, -17.479290785282917_c_double, & + 2.0064955613504103_c_double, 369.3550355960509_c_double, & + 167.57599706769557_c_double, 3.1391832467199254_c_double, & + -157.77085499470743_c_double, 56.3934183749737_c_double ] + + call runtime%evaluate(momenta, 1_c_size_t, totals) + call runtime%evaluate_resolved(momenta, 1_c_size_t, resolved) + checked_total = sum(resolved(:, :, 1)) + scale = max(1.0_c_double, abs(totals(1))) + if (abs(totals(1) - checked_total) > 1.0e-12_c_double * scale) then + error stop "resolved components do not reproduce the total" + end if + + write(*, '(A)') "process=" // runtime%process_key() + write(*, '(A)') "color_accuracy=" // runtime%color_accuracy() + write(*, '(A,ES24.16)') "total=", totals(1) + call runtime%close() +end program runtime_fortran diff --git a/pyamplicol/examples/otf_pp_zjj.toml b/pyamplicol/examples/otf_pp_zjj.toml new file mode 100644 index 0000000000..150e836119 --- /dev/null +++ b/pyamplicol/examples/otf_pp_zjj.toml @@ -0,0 +1,60 @@ +# SPDX-License-Identifier: 0BSD +# Compact OTF LC example for p p > Z j j. The first explicit warm_up or +# evaluation constructs only the selected flow with a complete helicity sum. + +schema_version = 1 +action = "generate" + +[model] +source = "built-in-sm" + +[process] +entries = [{ expression = "p p > Z j j" }] +flavor_scheme = 2 +max_quark_lines = 2 + +[process.multiparticles] +p = ["d", "d~", "g"] +j = ["d", "d~", "g"] + +[color] +accuracy = "lc" + +[generation] +output = "artifacts/otf_pp_zjj" +mode = "error" +emit_api_bundle = true + +[evaluator] +execution_mode = "on-the-fly" +backend = "jit" +batch_size = 128 + +[evaluator.optimization] +# This controls cold query construction, not warmed numerical execution. +cores = 4 + +[evaluator.jit] +optimization_level = 2 +compress = false + +[evaluation] +artifact = "artifacts/otf_pp_zjj" +process = "d d~ > g z g" +momenta = "data/pp_zjj_momenta.json" +precision = 16 + +[benchmark] +target_runtime = 2.0 +batch_size = 128 +precision = 16 +warmup_runs = 2 +minimum_samples = 5 +# CLI positions are one-based; this is one LC flow with a helicity sum. +color_flow_ids = ["1"] + +[output] +format = "human" +color = "auto" +progress = "auto" +log_level = "info" diff --git a/pyamplicol/examples/process_set_mixed_multiplicity.toml b/pyamplicol/examples/process_set_mixed_multiplicity.toml new file mode 100644 index 0000000000..938d342df2 --- /dev/null +++ b/pyamplicol/examples/process_set_mixed_multiplicity.toml @@ -0,0 +1,36 @@ +# SPDX-License-Identifier: 0BSD +# One UFO-SM artifact containing 2-to-2 and 2-to-3 processes. +# Generation can reuse proven final-state permutations within the set. + +schema_version = 1 +action = "generate" + +[model] +source = "models/json/sm/sm.json" +restriction = "default" + +[process] +entries = [ + { expression = "u u~ > Z g", name = "uubar_Zg" }, + { expression = "u u~ > Z g g", name = "uubar_Zgg" }, +] +flavor_scheme = 5 +max_quark_lines = 2 + +[color] +accuracy = "lc" + +[generation] +output = "artifacts/mixed_multiplicity" +workers = "auto" +emit_api_bundle = true + +[generation.validation] +enabled = true +samples = 3 +seed = 12345 + +[evaluator] +backend = "jit" +execution_mode = "compiled" +batch_size = 128 diff --git a/pyamplicol/examples/python/benchmark.py b/pyamplicol/examples/python/benchmark.py new file mode 100644 index 0000000000..7f12ec429e --- /dev/null +++ b/pyamplicol/examples/python/benchmark.py @@ -0,0 +1,68 @@ +# SPDX-License-Identifier: 0BSD +"""Benchmark an artifact through the typed service facade.""" + +from __future__ import annotations + +import argparse +import json +from pathlib import Path +from typing import Any + +from pyamplicol import BenchmarkConfig, BenchmarkRunner, Runtime + + +def _parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("artifact", type=Path) + parser.add_argument("--process") + parser.add_argument("--momenta", type=Path) + parser.add_argument("--target-runtime", type=float, default=1.0) + parser.add_argument("--batch-size", type=int, default=128) + parser.add_argument("--precision", type=int, default=16) + parser.add_argument("--warmup-runs", type=int, default=2) + parser.add_argument("--minimum-samples", type=int, default=5) + parser.add_argument("--helicity", action="append") + parser.add_argument("--color-flow", action="append") + return parser + + +def _read_points(path: Path | None) -> Any: + if path is None: + return None + return json.loads(path.read_text(encoding="utf-8")) + + +def main() -> int: + args = _parser().parse_args() + config = BenchmarkConfig( + target_runtime=args.target_runtime, + batch_size=args.batch_size, + precision=args.precision, + warmup_runs=args.warmup_runs, + minimum_samples=args.minimum_samples, + helicity_ids=tuple(args.helicity or ()), + color_flow_ids=tuple(args.color_flow or ()), + ) + runtime = Runtime.load(args.artifact, process=args.process) + result = BenchmarkRunner(config).run( + runtime, + points=_read_points(args.momenta), + ) + print( + json.dumps( + { + "sample_count": result.sample_count, + "wall_time_per_point": result.wall_time_per_point, + "evaluator_time_per_point": result.evaluator_time_per_point, + "relative_standard_error": (result.uncertainty.relative_standard_error), + "environment": dict(result.environment), + }, + indent=2, + sort_keys=True, + ) + ) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/pyamplicol/examples/python/copy_packaged_models.py b/pyamplicol/examples/python/copy_packaged_models.py new file mode 100644 index 0000000000..7118cec115 --- /dev/null +++ b/pyamplicol/examples/python/copy_packaged_models.py @@ -0,0 +1,61 @@ +# SPDX-License-Identifier: 0BSD +"""Copy pyAmpliCol's wheel-owned example models into an editable workspace.""" + +from __future__ import annotations + +import argparse +from importlib import resources +from importlib.resources.abc import Traversable +from pathlib import Path + + +def _parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "destination", + type=Path, + nargs="?", + default=Path("models"), + help="destination directory (default: ./models)", + ) + parser.add_argument( + "--force", + action="store_true", + help="merge into a non-empty destination without removing other files", + ) + return parser + + +def _copy_resource_tree(source: Traversable, destination: Path) -> None: + destination.mkdir(parents=True, exist_ok=True) + for child in source.iterdir(): + if child.name == "__pycache__": + continue + target = destination / child.name + if child.is_dir(): + _copy_resource_tree(child, target) + elif child.is_file(): + target.parent.mkdir(parents=True, exist_ok=True) + target.write_bytes(child.read_bytes()) + + +def main() -> int: + args = _parser().parse_args() + destination = args.destination.expanduser().resolve(strict=False) + if destination.exists() and not destination.is_dir(): + raise SystemExit(f"destination is not a directory: {destination}") + if destination.is_dir() and any(destination.iterdir()) and not args.force: + raise SystemExit( + f"destination is not empty: {destination}; pass --force to merge" + ) + + model_assets = resources.files("pyamplicol.assets").joinpath("models") + if not model_assets.is_dir(): + raise SystemExit("installed pyAmpliCol package contains no model assets") + _copy_resource_tree(model_assets, destination) + print(destination) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/pyamplicol/examples/python/external_models.py b/pyamplicol/examples/python/external_models.py new file mode 100644 index 0000000000..74238ca0cb --- /dev/null +++ b/pyamplicol/examples/python/external_models.py @@ -0,0 +1,34 @@ +# SPDX-License-Identifier: 0BSD +"""Resolve serialized-JSON and trusted-UFO Standard Model sources.""" + +from __future__ import annotations + +import argparse + +from pyamplicol import ModelSource + + +def _parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("sm_json", help="Path to the serialized SM JSON model") + parser.add_argument("sm_ufo", help="Path to a trusted UFO model named sm") + parser.add_argument("--ufo-restriction", default="restrict_default.dat") + return parser + + +def main() -> int: + args = _parser().parse_args() + sources = { + "sm-json": ModelSource.from_path(args.sm_json), + "sm-ufo": ModelSource.from_path( + args.sm_ufo, + restriction=args.ufo_restriction, + ), + } + for name, source in sources.items(): + print(f"{name}: kind={source.kind} path={source.path}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/pyamplicol/examples/python/otf_pp_zjj_warm_up.py b/pyamplicol/examples/python/otf_pp_zjj_warm_up.py new file mode 100644 index 0000000000..b80f76fd36 --- /dev/null +++ b/pyamplicol/examples/python/otf_pp_zjj_warm_up.py @@ -0,0 +1,209 @@ +# SPDX-License-Identifier: 0BSD +"""Warm and evaluate one LC flow of the packaged OTF ``p p > Z j j`` example.""" + +from __future__ import annotations + +import argparse +import json +import sys +from collections.abc import Sequence +from pathlib import Path + +from colorama import Fore, Style, just_fix_windows_console +from prettytable import HRuleStyle, PrettyTable + +from pyamplicol import ColorFlow, Runtime, WarmUpResult +from pyamplicol.reporting import ProgressSink, close_progress_sink, progress_sink + +WORKSPACE = Path(__file__).resolve().parents[1] +DEFAULT_ARTIFACT = WORKSPACE / "artifacts/otf_pp_zjj" +DEFAULT_MOMENTA = WORKSPACE / "data/pp_zjj_momenta.json" +DEFAULT_PROCESS = "d d~ > g z g" + +FourMomentum = tuple[float, float, float, float] +PhaseSpacePoint = tuple[FourMomentum, ...] + + +def _parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--artifact", type=Path, default=DEFAULT_ARTIFACT) + parser.add_argument("--momenta", type=Path, default=DEFAULT_MOMENTA) + parser.add_argument("--process", default=DEFAULT_PROCESS) + parser.add_argument( + "--n-cores", + type=int, + default=None, + metavar="N", + help="warm-up construction workers (default: process output setting)", + ) + parser.add_argument( + "--color-flow", + type=int, + default=1, + metavar="N", + help="one-based physical LC flow ordinal (default: 1)", + ) + parser.add_argument( + "--color", + choices=("auto", "always", "never"), + default="auto", + help="colored summary output (default: auto)", + ) + return parser + + +def _one_point(path: Path) -> PhaseSpacePoint: + payload = json.loads(path.read_text(encoding="utf-8")) + if not isinstance(payload, list) or len(payload) != 1: + raise ValueError( + "the explicit OTF warm-up example requires exactly one phase-space point" + ) + raw_point = payload[0] + if not isinstance(raw_point, list) or not raw_point: + raise ValueError("the phase-space point must contain external momenta") + point: list[FourMomentum] = [] + for raw_momentum in raw_point: + if not isinstance(raw_momentum, list) or len(raw_momentum) != 4: + raise ValueError("each external momentum must contain four components") + if not all( + isinstance(component, (int, float)) and not isinstance(component, bool) + for component in raw_momentum + ): + raise ValueError("momentum components must be real numbers") + components = tuple(float(component) for component in raw_momentum) + point.append((components[0], components[1], components[2], components[3])) + return tuple(point) + + +def _warm_and_evaluate( + runtime: Runtime, + point: PhaseSpacePoint, + flow: ColorFlow, + progress: ProgressSink, + n_cores: int | None = None, +) -> tuple[WarmUpResult, complex]: + # The structural warm-up contract is deliberately one point in native f64. + # Omitting ``helicities`` requests the complete physical helicity sum. + one_point = (point,) + warmed = runtime.warm_up( + one_point, + precision=16, + color_flows=(flow,), + progress=progress, + n_cores=n_cores, + ) + values = runtime.evaluate(one_point, precision=16, color_flows=(flow,)) + if len(values) != 1: + raise RuntimeError("one-point evaluation returned an unexpected result shape") + return warmed, complex(values[0]) + + +def _paint(text: str, color: str, *, enabled: bool) -> str: + return f"{color}{text}{Style.RESET_ALL}" if enabled else text + + +def _bytes_text(value: int | None) -> str: + if value is None: + return "unavailable" + units = ("B", "KiB", "MiB", "GiB") + scaled = float(value) + unit = units[0] + for unit in units: + if abs(scaled) < 1024.0 or unit == units[-1]: + break + scaled /= 1024.0 + return f"{scaled:.2f} {unit}" + + +def _value_text(value: complex) -> str: + if value.imag == 0.0: + return f"{value.real:.15e}" + return f"{value.real:.15e}{value.imag:+.15e}j" + + +def _summary( + *, + runtime: Runtime, + flow: ColorFlow, + physical_helicity_count: int, + warmed: WarmUpResult, + value: complex, + color: bool, +) -> str: + table = PrettyTable(("field", "value")) + table.title = _paint("OTF LC: one flow, helicity sum", Fore.CYAN, enabled=color) + table.align["field"] = "l" + table.align["value"] = "l" + table.hrules = HRuleStyle.FRAME + status = "already retained" if warmed.already_warm else "warmed now" + rows = ( + ("process", runtime.physics.process, Fore.CYAN), + ("precision", "f64 (16 digits)", Fore.GREEN), + ("selected LC flow", f"{flow.index + 1}: {flow.id}", Fore.MAGENTA), + ("helicities", f"sum of {physical_helicity_count}", Fore.MAGENTA), + ("warm-up status", status, Fore.GREEN), + ( + "queries", + f"{warmed.warmed_query_count} new / {warmed.query_count} selected", + Fore.YELLOW, + ), + ("warm-up wall", f"{warmed.elapsed_seconds:.3f} s", Fore.YELLOW), + ("current RSS", _bytes_text(warmed.current_rss_bytes), Fore.YELLOW), + ("peak RSS", _bytes_text(warmed.peak_rss_bytes), Fore.YELLOW), + ("matrix element", _value_text(value), Fore.GREEN), + ) + for field, text, shade in rows: + table.add_row((field, _paint(text, shade, enabled=color))) + return table.get_string() + + +def _uses_color(mode: str, stream: object) -> bool: + if mode == "always": + return True + if mode == "never": + return False + return bool(getattr(stream, "isatty", lambda: False)()) + + +def main(argv: Sequence[str] | None = None) -> int: + args = _parser().parse_args(argv) + point = _one_point(args.momenta) + runtime = Runtime.load(args.artifact, process=args.process) + physics = runtime.physics + if runtime.execution_mode != "on-the-fly" or physics.color_accuracy != "lc": + raise RuntimeError("this example requires an on-the-fly LC artifact") + if args.color_flow < 1 or args.color_flow > len(physics.color_flows): + raise ValueError( + f"--color-flow must be in 1..{len(physics.color_flows)} for this process" + ) + flow = physics.color_flows[args.color_flow - 1] + + summary_color = _uses_color(args.color, sys.stdout) + progress_color = _uses_color(args.color, sys.stderr) + if summary_color or progress_color: + just_fix_windows_console() + sink = progress_sink( + "auto", + stream=sys.stderr, + color=progress_color, + ) + try: + warmed, value = _warm_and_evaluate(runtime, point, flow, sink, args.n_cores) + finally: + close_progress_sink(sink) + + print( + _summary( + runtime=runtime, + flow=flow, + physical_helicity_count=len(physics.helicities), + warmed=warmed, + value=value, + color=summary_color, + ) + ) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/pyamplicol/examples/python/runtime_evaluation.py b/pyamplicol/examples/python/runtime_evaluation.py new file mode 100644 index 0000000000..1cadf027c0 --- /dev/null +++ b/pyamplicol/examples/python/runtime_evaluation.py @@ -0,0 +1,105 @@ +# SPDX-License-Identifier: 0BSD +"""Evaluate totals and resolved components through the public Runtime facade.""" + +from __future__ import annotations + +import argparse +import json +import math +from pathlib import Path +from typing import Any + +from pyamplicol import Runtime + + +def _parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("artifact", type=Path) + parser.add_argument("momenta", type=Path) + parser.add_argument("--process") + parser.add_argument("--parameters", type=Path) + parser.add_argument( + "--set-parameter", + action="append", + default=[], + metavar="NAME=VALUE", + help="apply a direct real or complex UFO-parameter override", + ) + parser.add_argument("--helicity", action="append") + parser.add_argument("--color-flow", action="append") + return parser + + +def _read_json(path: Path) -> Any: + return json.loads(path.read_text(encoding="utf-8")) + + +def _json_complex(value: complex) -> dict[str, float]: + return {"real": value.real, "imag": value.imag} + + +def _flatten(values: Any) -> list[complex]: + if isinstance(values, (list, tuple)): + return [item for value in values for item in _flatten(value)] + return [complex(values)] + + +def _parameter_overrides(values: list[str]) -> dict[str, complex]: + result: dict[str, complex] = {} + for value in values: + name, separator, raw = value.partition("=") + if not separator or not name or not raw: + raise SystemExit(f"invalid --set-parameter value: {value!r}") + try: + result[name] = complex(raw) + except ValueError as error: + raise SystemExit(f"invalid parameter value {raw!r}") from error + return result + + +def main() -> int: + args = _parser().parse_args() + parameters = _read_json(args.parameters) if args.parameters else None + momenta = _read_json(args.momenta) + runtime = Runtime.load( + args.artifact, + process=args.process, + model_parameters=parameters, + ) + if overrides := _parameter_overrides(args.set_parameter): + runtime.set_model_parameters(overrides) + selectors = { + "helicities": args.helicity, + "color_flows": args.color_flow, + } + compatibility_totals = runtime.evaluate(momenta, **selectors) + resolved = runtime.evaluate_resolved(momenta, **selectors) + resolved_totals = resolved.total() + if len(compatibility_totals) != len(resolved_totals) or any( + not math.isclose(total.real, check.real, rel_tol=1e-12, abs_tol=1e-15) + or not math.isclose(total.imag, check.imag, rel_tol=1e-12, abs_tol=1e-15) + for total, check in zip(compatibility_totals, resolved_totals, strict=True) + ): + raise RuntimeError("resolved components do not reproduce summed values") + + print( + json.dumps( + { + "color_accuracy": resolved.color_accuracy, + "helicity_ids": resolved.helicity_ids, + "color_flow_ids": resolved.color_flow_ids, + "shape": list(resolved.shape), + "values": [_json_complex(value) for value in _flatten(resolved.values)], + "resolved_sum": [_json_complex(value) for value in resolved_totals], + "compatibility_total": [ + _json_complex(value) for value in compatibility_totals + ], + }, + indent=2, + ) + ) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/pyamplicol/examples/python/typed_generation.py b/pyamplicol/examples/python/typed_generation.py new file mode 100644 index 0000000000..b2c9b86b98 --- /dev/null +++ b/pyamplicol/examples/python/typed_generation.py @@ -0,0 +1,119 @@ +# SPDX-License-Identifier: 0BSD +"""Plan or generate the external-JSON pp -> Zjj example with typed services.""" + +from __future__ import annotations + +import argparse +from pathlib import Path + +from pyamplicol import Generator, ModelSource, ProcessRequest, ProcessSet +from pyamplicol.config import resolve_config + + +def _parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("output", type=Path) + parser.add_argument( + "process", + nargs="*", + default=("p p > Z j j",), + help="quoted process expressions (default: 'p p > Z j j')", + ) + parser.add_argument( + "--model", + default="models/json/sm/sm.json", + help="built-in-sm, a trusted UFO directory, or a model JSON path", + ) + parser.add_argument("--restriction") + parser.add_argument( + "--multiparticle", + action="append", + default=["p=d,d~,g", "j=d,d~,g"], + metavar="NAME=ITEM[,ITEM...]", + ) + parser.add_argument("--flavor-scheme", type=int, default=2) + parser.add_argument("--max-quark-lines", type=int, default=2) + parser.add_argument("--color-accuracy", choices=("lc", "nlc", "full"), default="lc") + parser.add_argument( + "--mode", + choices=("error", "append", "replace"), + default="error", + ) + parser.add_argument("--plan-only", action="store_true") + return parser + + +def _multiparticles(values: list[str]) -> dict[str, list[str]]: + result: dict[str, list[str]] = {} + for value in values: + name, separator, members = value.partition("=") + entries = [member.strip() for member in members.split(",") if member.strip()] + if not separator or not name or not entries: + raise SystemExit(f"invalid --multiparticle value: {value!r}") + result[name] = entries + return result + + +def main() -> int: + args = _parser().parse_args() + model_config: dict[str, object] = {"source": args.model} + if args.restriction is not None: + model_config["restriction"] = args.restriction + card = { + "schema_version": 1, + "action": "generate", + "model": model_config, + "process": { + "entries": [{"expression": expression} for expression in args.process], + "multiparticles": _multiparticles(args.multiparticle), + "flavor_scheme": args.flavor_scheme, + "max_quark_lines": args.max_quark_lines, + }, + "color": {"accuracy": args.color_accuracy}, + "generation": {"output": args.output, "mode": args.mode}, + # Raw JSON/UFO model IR has no prepared recurrence-kernel bundle. + # Match the primary card by building process-local compiled JIT evaluators. + "evaluator": { + "backend": "jit", + "execution_mode": "compiled", + "jit": {"compress": True}, + }, + } + resolution = resolve_config(card) + config = resolution.effective + processes = ProcessSet( + tuple( + ProcessRequest.parse(entry.expression, name=entry.name) + for entry in config.process.entries + ) + ) + model_source = ( + ModelSource.built_in_sm() + if args.model == "built-in-sm" + else ModelSource.from_path( + args.model, + restriction=args.restriction, + simplify=config.model.simplify, + ) + ) + model = model_source.compile(use_cache=config.model.cache) + + generator = Generator(resolution) + plan = generator.plan(processes, model=model) + for process in plan.concrete_processes: + print(f"{process.name}: {process.expression}") + if args.plan_only: + return 0 + + result = generator.generate( + processes, + config.generation.output or args.output, + model=model, + mode=config.generation.mode, + ) + print(result.output) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/pyamplicol/examples/qq_z6g_compiled_jit_o3.toml b/pyamplicol/examples/qq_z6g_compiled_jit_o3.toml new file mode 100644 index 0000000000..b92258e0f2 --- /dev/null +++ b/pyamplicol/examples/qq_z6g_compiled_jit_o3.toml @@ -0,0 +1,63 @@ +# SPDX-License-Identifier: 0BSD +# q q~ > Z + 6g with process-local compiled-DAG JIT O3 execution. +# +# pyamplicol generate --card qq_z6g_compiled_jit_o3.toml +# pyamplicol profile --card qq_z6g_compiled_jit_o3.toml + +schema_version = 1 +action = "generate" + +[model] +source = "built-in-sm" +cache = true + +[process] +entries = [ + { expression = "u u~ > Z g g g g g g", name = "uubar_Z_6g_compiled" }, +] + +[color] +accuracy = "lc" + +[generation] +output = "artifacts/qq_z6g_compiled_jit_o3" +mode = "error" +workers = 1 +emit_api_bundle = true + +[generation.validation] +enabled = true +samples = 3 +seed = 12345 +relative_tolerance = 1e-12 +absolute_tolerance = 1e-300 +post_build_validation = false + +[evaluator] +backend = "jit" +execution_mode = "compiled" +batch_size = 64 +output_chunk_size = 512 + +[evaluator.jit] +optimization_level = 3 +compress = false + +[evaluation] +artifact = "artifacts/qq_z6g_compiled_jit_o3" +process = "u u~ > Z g g g g g g" +precision = 16 + +[benchmark] +target_runtime = 5.0 +batch_size = 64 +precision = 16 +warmup_runs = 2 +minimum_samples = 5 +color_flow_ids = ["1"] + +[output] +format = "human" +color = "auto" +progress = "auto" +log_level = "info" diff --git a/pyamplicol/examples/qq_z6g_eager_jit_o2.toml b/pyamplicol/examples/qq_z6g_eager_jit_o2.toml new file mode 100644 index 0000000000..57d5043e99 --- /dev/null +++ b/pyamplicol/examples/qq_z6g_eager_jit_o2.toml @@ -0,0 +1,67 @@ +# SPDX-License-Identifier: 0BSD +# q q~ > Z + 6g with eager-DAG execution and prepared JIT O2 kernels. +# +# pyamplicol generate --card qq_z6g_eager_jit_o2.toml +# pyamplicol profile --card qq_z6g_eager_jit_o2.toml + +schema_version = 1 +action = "generate" + +[model] +source = "built-in-sm" +cache = true + +[process] +entries = [ + { expression = "u u~ > Z g g g g g g", name = "uubar_Z_6g_eager" }, +] + +[color] +accuracy = "lc" + +[generation] +output = "artifacts/qq_z6g_eager_jit_o2" +mode = "error" +workers = 1 +emit_api_bundle = true + +[generation.validation] +enabled = true +samples = 3 +seed = 12345 +relative_tolerance = 1e-12 +absolute_tolerance = 1e-300 +post_build_validation = false + +[evaluator] +backend = "jit" +execution_mode = "eager" +batch_size = 64 +output_chunk_size = 512 + +[evaluator.jit] +optimization_level = 2 +compress = false + +[evaluator.eager] +point_tile_size = 1024 +workspace_mib = 256 + +[evaluation] +artifact = "artifacts/qq_z6g_eager_jit_o2" +process = "u u~ > Z g g g g g g" +precision = 16 + +[benchmark] +target_runtime = 5.0 +batch_size = 64 +precision = 16 +warmup_runs = 2 +minimum_samples = 5 +color_flow_ids = ["1"] + +[output] +format = "human" +color = "auto" +progress = "auto" +log_level = "info" diff --git a/pyamplicol/examples/qq_z6g_recurrence_jit_o2.toml b/pyamplicol/examples/qq_z6g_recurrence_jit_o2.toml new file mode 100644 index 0000000000..86c521cd94 --- /dev/null +++ b/pyamplicol/examples/qq_z6g_recurrence_jit_o2.toml @@ -0,0 +1,67 @@ +# SPDX-License-Identifier: 0BSD +# q q~ > Z + 6g with the default recurrence schedule and prepared JIT O2 kernels. +# +# pyamplicol generate --card qq_z6g_recurrence_jit_o2.toml +# pyamplicol profile --card qq_z6g_recurrence_jit_o2.toml + +schema_version = 1 +action = "generate" + +[model] +source = "built-in-sm" +cache = true + +[process] +entries = [ + { expression = "u u~ > Z g g g g g g", name = "uubar_Z_6g_recurrence" }, +] + +[color] +accuracy = "lc" + +[generation] +output = "artifacts/qq_z6g_recurrence_jit_o2" +mode = "error" +workers = 1 +emit_api_bundle = true + +[generation.validation] +enabled = true +samples = 3 +seed = 12345 +relative_tolerance = 1e-12 +absolute_tolerance = 1e-300 +post_build_validation = false + +[evaluator] +backend = "jit" +execution_mode = "recurrence" +batch_size = 64 +output_chunk_size = 512 + +[evaluator.jit] +optimization_level = 2 +compress = false + +[evaluator.recurrence] +point_tile_size = 1024 +workspace_mib = 256 + +[evaluation] +artifact = "artifacts/qq_z6g_recurrence_jit_o2" +process = "u u~ > Z g g g g g g" +precision = 16 + +[benchmark] +target_runtime = 5.0 +batch_size = 64 +precision = 16 +warmup_runs = 2 +minimum_samples = 5 +color_flow_ids = ["1"] + +[output] +format = "human" +color = "auto" +progress = "auto" +log_level = "info" diff --git a/pyamplicol/flake.lock b/pyamplicol/flake.lock new file mode 100644 index 0000000000..33a5783d09 --- /dev/null +++ b/pyamplicol/flake.lock @@ -0,0 +1,48 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1784120854, + "narHash": "sha256-KesHgItiZPgGX740axSiQLcIQ8D24MDqNpkKYWIek8k=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "753cc8a3a87467296ddd1fa93f0cc3e81120ee46", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs", + "rust-overlay": "rust-overlay" + } + }, + "rust-overlay": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1784216271, + "narHash": "sha256-bvaxxdk7mmMQ88AuXQGTUtk+PcUujHpgxqpryqTWvTU=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "1f69c7203034a4a974d7a4eac0f19b54af30804d", + "type": "github" + }, + "original": { + "owner": "oxalica", + "repo": "rust-overlay", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/pyamplicol/flake.nix b/pyamplicol/flake.nix new file mode 100644 index 0000000000..48494f0259 --- /dev/null +++ b/pyamplicol/flake.nix @@ -0,0 +1,134 @@ +# SPDX-License-Identifier: 0BSD + +{ + description = "pyAmpliCol contributor development environment"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + rust-overlay = { + url = "github:oxalica/rust-overlay"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + }; + + outputs = + { nixpkgs, rust-overlay, ... }: + let + systems = [ + "aarch64-darwin" + "aarch64-linux" + "x86_64-linux" + ]; + forAllSystems = nixpkgs.lib.genAttrs systems; + in + { + devShells = forAllSystems ( + system: + let + pkgs = import nixpkgs { + inherit system; + overlays = [ (import rust-overlay) ]; + }; + inherit (pkgs) lib stdenv; + + rustToolchain = pkgs.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml; + + # Keep the generic PEP 517 frontend for `just wheel`. Project, test, + # and candidate packages belong to the lock-controlled contributor venv. + python = pkgs.python311.withPackages ( + pythonPackages: with pythonPackages; [ + build + pip + setuptools + wheel + ] + ); + + nativeLibraries = with pkgs; [ + gmp + libffi + libmpc + mpfr + openssl + stdenv.cc.cc.lib + zlib + ]; + + platformTools = + lib.optionals stdenv.isLinux [ + pkgs.binutils + pkgs.lsof + pkgs.patchelf + pkgs.procps + ] + ++ lib.optionals stdenv.isDarwin [ + pkgs.darwin.cctools + pkgs.libiconv + ]; + in + { + default = pkgs.mkShell { + packages = + (with pkgs; [ + bash + cacert + cmake + coreutils + curl + diffutils + file + findutils + gh + ghostscript + git + gnumake + gnugrep + gnused + gnutar + gfortran + gzip + just + jq + m4 + ninja + openssh + patch + pkg-config + poppler-utils + python + ripgrep + rustToolchain + stdenv.cc + texliveFull + unzip + which + xz + zip + zstd + ]) + ++ platformTools; + + buildInputs = nativeLibraries; + + PYAMPLICOL_DEV_PYTHON = ".venv/bin/python"; + PIP_DISABLE_PIP_VERSION_CHECK = "1"; + PYTHONNOUSERSITE = "1"; + + # These paths support native wheels and generated SDK consumers in + # a Nix shell without changing release-artifact link metadata. + LD_LIBRARY_PATH = lib.optionalString stdenv.isLinux ( + lib.makeLibraryPath nativeLibraries + ); + NIX_LD_LIBRARY_PATH = lib.optionalString stdenv.isLinux ( + lib.makeLibraryPath nativeLibraries + ); + + shellHook = '' + echo "pyAmpliCol developer shell (Python 3.11, Rust 1.97.1, native SDK, TeX)" + echo "Run 'just dev-install' to create the pinned contributor environment." + ''; + }; + } + ); + }; +} diff --git a/pyamplicol/install.py b/pyamplicol/install.py new file mode 100644 index 0000000000..40b82b2bb0 --- /dev/null +++ b/pyamplicol/install.py @@ -0,0 +1,479 @@ +#!/usr/bin/env python3 +"""Install pyamplicol either using pre-compiled binaries or built from source. + +The pre-compiled PyPI wheel (--bin) is only offered/defaulted-to in an +official MadGraph release tarball, where it is guaranteed to match the +bundled source; a plain git checkout always builds from source. + +Interactive usage (no arguments): python install.py +Non-interactive examples: + python install.py --bin + python install.py --source -j 8 + python install.py --source --clean # rebuild from scratch + +By default everything goes into install/ next to this script, so the active +Python environment is left untouched. The source build needs a Rust +toolchain: the version pinned in rust-toolchain.toml is taken from rustup +when available, otherwise a project-local rustup is installed into build/. + +Rebuilds reuse the Cargo build tree in build/ and are therefore incremental. +--clean deletes build/ and install/ first; the saved settings survive it. + +The prepared-model packs shipped in the package are taken from the archive +attached to the MadGraph release (the matching one, else the latest). When +they are stale for this source tree, or cannot be downloaded, they are +regenerated locally (one extra build). +""" + +import argparse +import contextlib +import json +import os +import platform +import shutil +import subprocess +import sys +import tempfile +import tomllib +import urllib.request +from pathlib import Path + +SCRIPT_DIR = Path(__file__).parent.resolve() +INSTALL_DIR = SCRIPT_DIR / "install" +BUILD_DIR = SCRIPT_DIR / "build" +SETTINGS_FILE = SCRIPT_DIR / "install_settings.json" +RUST_DIR = BUILD_DIR / "rust" +CACHE_DIR = BUILD_DIR / "cache" +PREPARED_MODELS_DIR = BUILD_DIR / "prepared-models" +RELEASE_TOOLS = SCRIPT_DIR / "tools" / "release" + +PACKAGE_NAME = "pyamplicol" +RUSTUP_INIT_URL = "https://sh.rustup.rs" + + +# Interactive helpers (same behaviour as madspace/install.py: prompts go +# through MadGraph's cmd.ask when importable, else input()). + +_NONINTERACTIVE = (not sys.stdin.isatty()) or any( + a in ("-y", "--yes") for a in sys.argv[1:] +) + + +def _set_noninteractive(flag: bool) -> None: + global _NONINTERACTIVE + if flag: + _NONINTERACTIVE = True + + +def ask_yes_no(prompt: str, default: bool = True) -> bool: + default_s = "y" if default else "n" + answer = None + try: + from madgraph.interface.extended_cmd import Cmd + + answer = Cmd().ask( + prompt, default_s, choices=["y", "n"], timeout=0, force=_NONINTERACTIVE + ) + except Exception: + if not _NONINTERACTIVE: + with contextlib.suppress(EOFError): + answer = input(f"{prompt} [{default_s}]: ").strip() + answer = (answer or default_s).strip().lower() + return answer in ("y", "yes") if answer in ("y", "yes", "n", "no") else default + + +# Command execution + + +def run(cmd: list, env: dict | None = None, cwd: Path = SCRIPT_DIR) -> None: + display = " ".join(str(c) for c in cmd) + print(f"\n$ {display}\n", flush=True) + result = subprocess.run(cmd, cwd=cwd, env=env) + if result.returncode != 0: + sys.exit(result.returncode) + + +def default_jobs() -> int: + try: + return len(os.sched_getaffinity(0)) + except AttributeError: + return os.cpu_count() or 1 + + +# Versions and release information + + +def _pyamplicol_version() -> str: + """The MadGraph version, which pyamplicol is released in lockstep with.""" + for line in (SCRIPT_DIR.parent / "VERSION").read_text().splitlines(): + name, _, value = line.partition("=") + if name.strip() == "version": + return value.strip() + raise RuntimeError("no 'version' line in the MadGraph VERSION file") + + +def _release_info() -> dict[str, str]: + """Parse input/.release, written into MadGraph release tarballs.""" + marker = SCRIPT_DIR.parent / "input" / ".release" + if not marker.is_file(): + return {} + info = {} + for line in marker.read_text().splitlines(): + name, _, value = line.partition("=") + info[name.strip()] = value.strip() + return info + + +def _release_wheel_available() -> bool: + """Whether input/.release lists a pyamplicol wheel for this platform. + + The wheels are abi3 (cp311 and newer), so only the platform matters. + """ + wheels = [ + name + for name in _release_info().get("wheels", "").split(",") + if name.startswith(f"{PACKAGE_NAME}-") + ] + system, machine = platform.system(), platform.machine() + if system == "Linux" and machine in ("x86_64", "AMD64"): + platform_tags = ("manylinux", "x86_64") + elif system == "Darwin" and machine in ("arm64", "aarch64"): + platform_tags = ("macosx", "arm64") + elif system == "Darwin" and machine == "x86_64": + platform_tags = ("macosx", "x86_64") + else: + return False + return any(all(tag in name for tag in platform_tags) for name in wheels) + + +def load_settings() -> dict: + try: + with open(SETTINGS_FILE) as f: + return json.load(f) + except (FileNotFoundError, json.JSONDecodeError): + return {} + + +def save_settings(settings: dict) -> None: + with open(SETTINGS_FILE, "w") as f: + json.dump(settings, f, indent=2) + + +def clean_install_dirs() -> None: + for target in (BUILD_DIR, INSTALL_DIR): + if target.is_dir(): + shutil.rmtree(target) + print(f"Removed {target}") + + +# Rust toolchain + + +def _pinned_toolchain() -> str: + with open(SCRIPT_DIR / "rust-toolchain.toml", "rb") as f: + return tomllib.load(f)["toolchain"]["channel"] + + +def setup_rust(env: dict) -> dict: + """Make the pinned toolchain available through rustup and return env. + + A rustup on PATH is used as is; otherwise a project-local one is installed + into build/rust, leaving ~/.rustup and ~/.cargo alone. + """ + toolchain = _pinned_toolchain() + local_bin = RUST_DIR / "cargo" / "bin" + if (local_bin / "rustup").is_file(): + rustup = local_bin / "rustup" + elif shutil.which("rustup", path=env.get("PATH")): + rustup = Path(shutil.which("rustup", path=env.get("PATH"))) + else: + rustup = None + + # The build sets its own CARGO_HOME; rustup itself needs the local one. + rust_env = env + if rustup is None or rustup.parent == local_bin: + env["RUSTUP_HOME"] = str(RUST_DIR / "rustup") + env["PATH"] = os.pathsep.join([str(local_bin), env.get("PATH", "")]) + rust_env = dict( + env, CARGO_HOME=str(RUST_DIR / "cargo"), RUSTUP_INIT_SKIP_PATH_CHECK="yes" + ) + if rustup is None: + print(f"\nNo rustup found; installing Rust {toolchain} into {RUST_DIR}") + RUST_DIR.mkdir(parents=True, exist_ok=True) + script = RUST_DIR / "rustup-init.sh" + with urllib.request.urlopen(RUSTUP_INIT_URL, timeout=120) as response: + script.write_bytes(response.read()) + run( + [ + "sh", + str(script), + "-y", + "--no-modify-path", + "--profile", + "minimal", + "--default-toolchain", + toolchain, + ], + env=rust_env, + ) + else: + run( + [str(rustup), "toolchain", "install", toolchain, "--profile", "minimal"], + env=rust_env, + ) + return env + + +# Build + + +def install_build_deps(env: dict, system: bool) -> dict: + with open(SCRIPT_DIR / "pyproject.toml", "rb") as f: + requires = tomllib.load(f)["build-system"]["requires"] + print("\nInstalling build dependencies...") + cmd = [sys.executable, "-m", "pip", "install", "--upgrade", *requires] + if not system: + cmd.append(f"--target={INSTALL_DIR}") + run(cmd, env=env) + if not system: + env["PYTHONPATH"] = os.pathsep.join( + p for p in (str(INSTALL_DIR), env.get("PYTHONPATH")) if p + ) + # maturin's console script, needed by the build backend + env["PATH"] = os.pathsep.join([str(INSTALL_DIR / "bin"), env.get("PATH", "")]) + return env + + +def build_environment(jobs: int | None) -> dict: + env = os.environ.copy() + env["PYAMPLICOL_LOCAL_BUILD"] = "1" + env["PYAMPLICOL_BUILD_MODE"] = "release" + env["PYAMPLICOL_BUILD_CACHE_ROOT"] = str(CACHE_DIR) + jobs = default_jobs() if jobs is None or jobs <= 0 else jobs + env["PYAMPLICOL_BUILD_JOBS"] = str(jobs) + print(f"Compiling with {jobs} parallel job(s) (-j to change).") + return env + + +def _prepared_models_stale(env: dict) -> str | None: + """Validate the unpacked prepared models against this source tree. + + Returns None when they are usable, else the reason they are not. + """ + check = ( + "import sys; from pathlib import Path; " + f"sys.path.insert(0, {str(SCRIPT_DIR / 'build_backend')!r}); " + "from prepared_models import stage_packaged_prepared_models; " + f"stage_packaged_prepared_models(Path({str(SCRIPT_DIR)!r}), 'release')" + ) + result = subprocess.run( + [sys.executable, "-c", check], env=env, capture_output=True, text=True + ) + if result.returncode == 0: + return None + lines = (result.stderr or result.stdout).strip().splitlines() + return lines[-1] if lines else "validation failed" + + +def fetch_prepared_models(env: dict) -> bool: + """Download and unpack the release archive; True if the packs are usable.""" + archive_tool = RELEASE_TOOLS / "prepared_models_archive.py" + result = subprocess.run( + [ + sys.executable, + str(archive_tool), + "fetch", + "--version", + _pyamplicol_version(), + ], + env=env, + ) + if result.returncode != 0: + print("Could not download the prepared-model archive.") + return False + reason = _prepared_models_stale(env) + if reason is not None: + print(f"The released prepared models do not match this source tree: {reason}") + return False + return True + + +def regenerate_prepared_models(env: dict) -> None: + """Build a bootstrap wheel and produce the prepared models with it.""" + print("\nRegenerating the prepared models locally (one extra build)...") + PREPARED_MODELS_DIR.mkdir(parents=True, exist_ok=True) + run( + [ + sys.executable, + str(RELEASE_TOOLS / "prepared_models_archive.py"), + "regenerate", + "--work", + str(PREPARED_MODELS_DIR), + ], + env=env, + ) + + +def check_license(target: Path | None) -> None: + """Fail unless pyamplicol's Symbolica library key is accepted.""" + env = os.environ.copy() + env.pop("SYMBOLICA_LICENSE", None) + if target is not None: + env["PYTHONPATH"] = str(target) + code = ( + "import json, pyamplicol; " + "from pyamplicol.licensing import detect_symbolica_license; " + "print(json.dumps({'file': pyamplicol.__file__, " + "'licensed': detect_symbolica_license(suggest=False).licensed}))" + ) + # Run outside the MadGraph tree, where pyamplicol/ is a plain directory. + with tempfile.TemporaryDirectory() as cwd: + result = subprocess.run( + [sys.executable, "-I" if target is None else "-s", "-c", code], + cwd=cwd, + env=env, + capture_output=True, + text=True, + ) + try: + info = json.loads(result.stdout.strip().splitlines()[-1]) + except (IndexError, ValueError): + print(result.stdout + result.stderr) + sys.exit("ERROR: the installed pyamplicol cannot be imported.") + if target is not None and not Path(info["file"]).resolve().is_relative_to(target): + sys.exit(f"ERROR: imported pyamplicol from {info['file']}, not from {target}.") + if not info["licensed"]: + sys.exit( + "ERROR: Symbolica does not accept pyamplicol's library license key. " + "Model generation would run in Symbolica's restricted mode." + ) + print("Symbolica license: pyamplicol library key accepted.") + + +# Main + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter + ) + mode = parser.add_mutually_exclusive_group() + mode.add_argument( + "--bin", + action="store_true", + help="Install the pre-compiled package (default in release tarballs).", + ) + mode.add_argument( + "--source", action="store_true", help="Build and install from source." + ) + parser.add_argument( + "-y", + "--yes", + action="store_true", + help="Non-interactive, no prompts: reuse the previous install mode, else " + "the default. Combine with --source/--bin to force the install mode.", + ) + parser.add_argument( + "-j", + "--jobs", + type=int, + default=None, + metavar="N", + help="Number of parallel compilation jobs for the source build " + f"(default: every available core, {default_jobs()} here). MG5's " + "'nb_core' option is forwarded here by 'install pyamplicol'.", + ) + parser.add_argument( + "--system", + action="store_true", + help="Install into the active Python environment instead of install/.", + ) + parser.add_argument( + "--clean", + action="store_true", + help="Delete build/ and install/ first, for a rebuild from scratch. " + "The saved settings are kept.", + ) + parser.add_argument( + "--build-models", + action="store_true", + help="Regenerate the prepared models locally instead of downloading them.", + ) + return parser + + +def main(argv: list[str] | None = None) -> None: + args = build_parser().parse_args(argv) + _set_noninteractive(args.yes) + saved = load_settings() + if args.clean: + clean_install_dirs() + + release_version = _release_info().get("version") + is_release = release_version == _pyamplicol_version() + bin_available = is_release and _release_wheel_available() + + if args.bin: + if not bin_available: + print( + "WARNING: no pyamplicol wheel was published for this platform; " + "--bin will likely fail." + ) + from_source = False + elif args.source: + from_source = True + elif args.yes: + from_source = ( + saved.get("mode", "bin" if bin_available else "source") == "source" + ) + else: + print("Welcome to the pyAmpliCol installer") + from_source = not ( + bin_available + and ask_yes_no( + "Install pre-compiled package? (recommended)", + default=saved.get("mode", "bin") != "source", + ) + ) + + target = None if args.system else INSTALL_DIR + if not from_source: + cmd = [ + sys.executable, + "-m", + "pip", + "install", + f"{PACKAGE_NAME}=={_pyamplicol_version()}", + ] + if target is not None: + cmd.append(f"--target={target}") + run(cmd) + save_settings({"mode": "bin"}) + else: + env = build_environment(args.jobs) + env = setup_rust(env) + env = install_build_deps(env, args.system) + if args.build_models or not fetch_prepared_models(env): + regenerate_prepared_models(env) + cmd = [ + sys.executable, + "-m", + "pip", + "install", + "--upgrade", + "--no-build-isolation", + # A wheel for this host only; manylinux tags need a manylinux container. + "--config-settings=pyamplicol.host-wheel=true", + ".", + ] + if target is not None: + cmd.append(f"--target={target}") + run(cmd, env=env) + save_settings({"mode": "source"}) + + check_license(target) + print(f"\nInstalled to: {target}" if target else "\nInstalled system-wide.") + + +if __name__ == "__main__": + main() diff --git a/pyamplicol/justfile b/pyamplicol/justfile new file mode 100644 index 0000000000..048d34f0c5 --- /dev/null +++ b/pyamplicol/justfile @@ -0,0 +1,191 @@ +set shell := ["bash", "-euo", "pipefail", "-c"] + +python := env_var_or_default("PYTHON", "python3") +dev_python := env_var_or_default("PYAMPLICOL_DEV_PYTHON", ".venv/bin/python") +build_mode := env_var_or_default("PYAMPLICOL_BUILD_MODE", "release") +eager_builtin_pack := env_var_or_default("PYAMPLICOL_EAGER_BUILTIN_PACK", ".artifacts/eager-models/builtin-sm-jit-o3.pyamplicol-model") +eager_ufo_pack := env_var_or_default("PYAMPLICOL_EAGER_UFO_SM_PACK", ".artifacts/eager-models/ufo-sm-jit-o3.pyamplicol-model") +eager_ufo_source := env_var_or_default("PYAMPLICOL_EAGER_UFO_SM_SOURCE", "src/pyamplicol/assets/models/json/sm/sm.json") +dev_cache := ".artifacts/dev-install" +fft_performance_run_id := env_var_or_default("PYAMPLICOL_FFT_PERFORMANCE_RUN_ID", "manual") + +default: + @just --list + +_source-checkout: + @if ! git ls-files --error-unmatch pyproject.toml >/dev/null 2>&1 || [[ ! -f dependencies/contributor-lock.toml || ! -f dependencies/install_dependencies.py ]]; then \ + printf '%s\n' "error: this command requires a full pyAmpliCol Git source checkout; contributor commands are unavailable from a release source distribution" >&2; \ + exit 1; \ + fi + +build: + PYAMPLICOL_BUILD_MODE={{build_mode}} {{python}} -m build --wheel + +typing: + PYTHONPATH="$PWD/src" {{python}} -m ruff check tools/typing tests/typing + PYTHONPATH="$PWD/src" {{python}} tools/typing/check_public_typing.py + PYTHONPATH="$PWD/src" {{python}} -m pytest tests/typing/test_typing_gate.py -q + +dependency-gate: + @if [ "{{build_mode}}" = candidate ]; then \ + {{python}} tools/release/check_dependencies.py --candidate; \ + else \ + {{python}} tools/release/check_dependencies.py; \ + fi + +python-unit: + PYTHONPATH="$PWD/src" {{python}} -m pytest tests/unit -q + +python-release: + PYTHONPATH="$PWD/src" {{python}} -m pytest tests/release -q + +python-integration: + # The restricted child needs a parent that has not imported Symbolica. + PYTHONPATH="$PWD/src" PYAMPLICOL_REQUIRE_NATIVE_TESTS=1 {{python}} -m pytest tests/integration/test_spenso_restricted_mode.py -q + PYTHONPATH="$PWD/src" PYAMPLICOL_REQUIRE_NATIVE_TESTS=1 {{python}} -m pytest tests/integration --ignore=tests/integration/test_spenso_restricted_mode.py -q + +# Focused compiler regressions only; no pyAmpliCol/Symbolica build is required. +# Defaults to the release-lock SymJIT version; select a checkout with +# PYAMPLICOL_SYMJIT_SOURCE=/absolute/path/to/symjit. +symjit-regressions: + PYAMPLICOL_RUN_SYMJIT_REGRESSIONS=1 {{python}} -m pytest tests/integration/test_symjit_upstream_regressions.py -q + +python-physics: + PYTHONPATH="$PWD/src" PYAMPLICOL_REQUIRE_NATIVE_TESTS=1 {{python}} -m pytest tests/integration/test_schema_v3_generation_runtime.py tests/unit/test_reference_fixture_v2.py tests/unit/test_tracked_reference_fixture_v2.py tests/unit/test_color_contraction_safety.py -q + +# Explicit, fail-fast numerical authority gate. This is intentionally excluded +# from the default suite because it generates fifteen catalog ProcessSets plus +# four separate full-colour extra artifacts. +numerical-acceptance: _source-checkout + PYTHONPATH="$PWD/src" PYAMPLICOL_RUN_NUMERICAL_ACCEPTANCE=1 PYAMPLICOL_REQUIRE_NATIVE_TESTS=1 {{dev_python}} tools/ci/memory_watchdog.py --limit-gib 30 -- {{dev_python}} -m pytest tests/integration/test_numerical_acceptance.py -q -x + +# Dedicated FFT process-table parity and frozen-MadGraph replay. +# This expensive gate is absent from every default CI and release aggregate. +fft-numerical-acceptance: _source-checkout + mkdir -p .artifacts/fft-acceptance-env/tmp .artifacts/fft-acceptance-env/cargo-home .artifacts/fft-acceptance-env/cargo-target .artifacts/fft-acceptance-env/pip-cache .artifacts/fft-acceptance-env/xdg-cache .artifacts/fft-acceptance-env/python-cache .artifacts/fft-numerical-acceptance + TMPDIR="$PWD/.artifacts/fft-acceptance-env/tmp" CARGO_HOME="$PWD/.artifacts/fft-acceptance-env/cargo-home" CARGO_TARGET_DIR="$PWD/.artifacts/fft-acceptance-env/cargo-target" CARGO_NET_OFFLINE=true PIP_CACHE_DIR="$PWD/.artifacts/fft-acceptance-env/pip-cache" PIP_NO_INDEX=1 XDG_CACHE_HOME="$PWD/.artifacts/fft-acceptance-env/xdg-cache" PYTHONPYCACHEPREFIX="$PWD/.artifacts/fft-acceptance-env/python-cache" PYTHONPATH="$PWD/src" PYAMPLICOL_REQUIRE_NATIVE_TESTS=1 {{dev_python}} tools/ci/memory_watchdog.py --limit-gib 30 -- {{dev_python}} tools/developer/fft_numerical_acceptance.py --output-root "$PWD/.artifacts/fft-numerical-acceptance" + +# Native same-host pure-gluon timing, RSS, and cold-to-ready comparison. Prepare +# its reference with `just dev-install --with-reference-fft`; this gate is +# deliberately excluded from default CI/release runs. +fft-performance-acceptance: _source-checkout + mkdir -p .artifacts/fft-performance/env/tmp .artifacts/fft-performance/env/cargo-home .artifacts/fft-performance/env/cargo-target .artifacts/fft-performance/env/pip-cache .artifacts/fft-performance/env/xdg-cache .artifacts/fft-performance/env/python-cache + TMPDIR="$PWD/.artifacts/fft-performance/env/tmp" CARGO_HOME="$PWD/.artifacts/fft-performance/env/cargo-home" CARGO_TARGET_DIR="$PWD/.artifacts/fft-performance/env/cargo-target" CARGO_NET_OFFLINE=true PIP_CACHE_DIR="$PWD/.artifacts/fft-performance/env/pip-cache" PIP_NO_INDEX=1 XDG_CACHE_HOME="$PWD/.artifacts/fft-performance/env/xdg-cache" PYTHONPYCACHEPREFIX="$PWD/.artifacts/fft-performance/env/python-cache" PYTHONPATH="$PWD/src" PYAMPLICOL_REQUIRE_NATIVE_TESTS=1 {{dev_python}} tools/ci/memory_watchdog.py --limit-gib 30 -- {{dev_python}} tools/developer/fft_gluon_performance_acceptance.py --include-optional --run-id "{{fft_performance_run_id}}" + +# Reuse a current staged runtime, or build through the real backend and stage +# ignored native runtime/SDK resources for source-tree tests. +source-runtime: + PYAMPLICOL_BUILD_MODE={{build_mode}} {{python}} tools/developer/prepare_source_runtime.py --reuse-current + +# Developer-only independent Fortran oracle. Prepare its checkout with +# `just dev-install --with-legacy-amplicol` first. +legacy-physics: _source-checkout + {{python}} tools/developer/legacy_amplicol.py --jobs 5 + +legacy-physics-verify: _source-checkout + {{python}} tools/developer/legacy_amplicol.py --fixture tests/fixtures/reference/physics-v2.json --jobs 5 --check-output tests/fixtures/reference/legacy-fortran-v2.json + +# Release-facing numerical comparison against the pinned independent Fortran +# implementation. The CI job additionally applies a 30 GiB process limit. +independent-physics-oracle: _source-checkout + {{python}} tools/developer/legacy_amplicol.py --prepare-checkout --fixture tests/fixtures/reference/physics-v2.json --jobs 2 >/dev/null + +installed-smoke: + PYTHONPATH="$PWD/src" {{python}} -m pyamplicol.selftest + PYTHONPATH="$PWD/src" {{python}} -m pyamplicol self-test --json + PYTHONPATH="$PWD/src" {{python}} -m pyamplicol examples list --json + PYTHONPATH="$PWD/src" PYAMPLICOL_EXAMPLE_CACHE="$PWD/.artifacts/source-gate-example" {{python}} -m pyamplicol examples run builtin_sm_lc --set generation.mode=replace --json + +rust-check: + {{python}} tools/release/run_cargo.py --mode {{build_mode}} -- fmt --all --check + {{python}} tools/release/run_cargo.py --mode {{build_mode}} -- clippy --workspace --all-targets --locked + +rust-test: + {{python}} tools/release/run_cargo.py --mode {{build_mode}} -- test --workspace --locked + {{python}} tools/release/run_cargo.py --mode {{build_mode}} -- test --locked -p rusticol-core --no-default-features --features f64-compiled native_role_exports_load_and_remain_callable_while_owners_move + +# Complete source gate used before any release artifact is retained. +source-gate: + just dependency-gate + just source-runtime + just typing + just python-unit + just python-release + just python-integration + just python-physics + just rust-check + just rust-test + just installed-smoke + +check: + just typing + just dependency-gate + just python-unit + just rust-check + +test: + PYTHONPATH="$PWD/src" {{python}} -m pytest tests/integration/test_spenso_restricted_mode.py -q + PYTHONPATH="$PWD/src" {{python}} -m pytest --ignore=tests/integration/test_spenso_restricted_mode.py + just rust-test + +sdist: + PYAMPLICOL_BUILD_MODE={{build_mode}} {{python}} -m build --sdist + +wheel: + PYAMPLICOL_BUILD_MODE={{build_mode}} {{python}} -m build --wheel + +wheel-from-sdist: + {{python}} tools/release/build_from_sdist.py + +install-wheel PYTHON_ARG="": + @selected="{{PYTHON_ARG}}"; \ + if [[ "$selected" == PYTHON=* ]]; then \ + selected="$(printf '%s' "$selected" | cut -d= -f2-)"; \ + fi; \ + if [[ -z "$selected" ]]; then selected="{{python}}"; fi; \ + {{python}} tools/release/install_wheel.py --python "$selected" + +# Install published dependencies and a release-mode source runtime. Historical +# source candidates require --candidate; profiling references remain opt-in. +dev-install *INSTALLER_ARGS: _source-checkout + mkdir -p {{dev_cache}}/tmp {{dev_cache}}/cargo-home {{dev_cache}}/cargo-target {{dev_cache}}/pip-cache {{dev_cache}}/xdg-cache {{dev_cache}}/python-cache + TMPDIR="$PWD/{{dev_cache}}/tmp" CARGO_HOME="$PWD/{{dev_cache}}/cargo-home" CARGO_TARGET_DIR="$PWD/{{dev_cache}}/cargo-target" PIP_CACHE_DIR="$PWD/{{dev_cache}}/pip-cache" XDG_CACHE_HOME="$PWD/{{dev_cache}}/xdg-cache" PYTHONPYCACHEPREFIX="$PWD/{{dev_cache}}/python-cache" PYAMPLICOL_CANDIDATE_CACHE_ROOT="$PWD/{{dev_cache}}" {{python}} dependencies/install_dependencies.py {{INSTALLER_ARGS}} + +# Report/campaign prerequisite. pyAmpliCol is not released yet, so this keeps +# the explicit build entrypoint tied to the published-dependency environment. +dev-build: _source-checkout + just dev-install + {{dev_python}} -c 'import pyamplicol; import pyamplicol.api' + {{dev_python}} src/pyamplicol/_profiling_campaign/result_tables.py validate + +dev-test: _source-checkout + PYTHON={{dev_python}} PYAMPLICOL_BUILD_MODE={{build_mode}} just source-gate + @if [ "{{build_mode}}" = candidate ]; then \ + PYTHON={{dev_python}} PYAMPLICOL_BUILD_MODE=candidate just test-deployment-candidate; \ + else \ + PYTHON={{dev_python}} PYAMPLICOL_BUILD_MODE=release just test-deployment; \ + fi + +# Bounded built-in-SM eager gate under the 30 GiB memory guard. +eager-smoke: _source-checkout + {{dev_python}} tools/ci/memory_watchdog.py --limit-gib 30 -- {{dev_python}} tools/developer/eager_benchmark_matrix.py --suite smoke --models built-in --builtin-pack {{eager_builtin_pack}} --output-root .artifacts/eager-benchmark/smoke + +# Full built-in/UFO-SM eager milestone gate under the 30 GiB memory guard. +eager-milestone: _source-checkout + {{dev_python}} tools/ci/memory_watchdog.py --limit-gib 30 -- {{dev_python}} tools/developer/eager_benchmark_matrix.py --suite milestone --models built-in,ufo-sm --builtin-pack {{eager_builtin_pack}} --ufo-source {{eager_ufo_source}} --ufo-pack {{eager_ufo_pack}} --output-root .artifacts/eager-benchmark/milestone + +test-deployment-candidate: _source-checkout + PYAMPLICOL_BUILD_MODE=candidate {{python}} tools/release/test_deployment.py --candidate + +test-deployment: + PYAMPLICOL_BUILD_MODE=release {{python}} tools/release/test_deployment.py + +release-artifacts: _source-checkout + PYAMPLICOL_BUILD_MODE=release just source-gate + just independent-physics-oracle + PYAMPLICOL_BUILD_MODE=release {{python}} tools/release/build_release_artifacts.py + +publish-dry-run: _source-checkout + PYAMPLICOL_BUILD_MODE=release just source-gate + just independent-physics-oracle + PYAMPLICOL_BUILD_MODE=release {{python}} tools/release/publish_dry_run.py diff --git a/pyamplicol/licenses/GammaLoop-model-assets.txt b/pyamplicol/licenses/GammaLoop-model-assets.txt new file mode 100644 index 0000000000..f931655be4 --- /dev/null +++ b/pyamplicol/licenses/GammaLoop-model-assets.txt @@ -0,0 +1,16 @@ +GammaLoop Model Asset License + +Local SPDX identifier: LicenseRef-GammaLoop-No-Restrictions + +The bundled UFO source models and their derived JSON forms were distributed +as GammaLoop model assets. GammaLoop's accompanying LICENSE.md states: + +There are no usage restrictions for `GammaLoop`. + +The upstream license separately calls attention to Symbolica's license +restrictions. Symbolica is not part of these model assets and its terms are +recorded independently in licenses/Symbolica.txt. + +This notice applies only to the eligible GammaLoop-origin model assets listed +in src/pyamplicol/assets/models/PROVENANCE.toml. Files carrying their own +copyright or license notice retain that notice. diff --git a/pyamplicol/licenses/GammaLoop.txt b/pyamplicol/licenses/GammaLoop.txt new file mode 100644 index 0000000000..82d806ae99 --- /dev/null +++ b/pyamplicol/licenses/GammaLoop.txt @@ -0,0 +1,6 @@ +# Licence +There are no usage restrictions for `GammaLoop`. + +However, note the license restrictions from +[Symbolica](https://symbolica.io/license.html), which is a core dependency of +`GammaLoop`. diff --git a/pyamplicol/licenses/MadGraph5_aMCatNLO.txt b/pyamplicol/licenses/MadGraph5_aMCatNLO.txt new file mode 100644 index 0000000000..df1774f377 --- /dev/null +++ b/pyamplicol/licenses/MadGraph5_aMCatNLO.txt @@ -0,0 +1,34 @@ +The MadGraph5_aMC@NLO software comes with the following Open Source License, +adapted from the University of Illinois/NCSA license. + +Copyright (c) 2009, 2013, the MadTeam. All rights reserved. + +Developed by the "MadTeam", i.e. Johan ALWALL, Rikkert FREDERIX, Stefano +FRIXIONE, Michel HERQUET, Valentin HIRSCHI, Fabio MALTONI, Olivier MATTELAER, +Tim STELZER, Paolo TORRIELLI, and Marco ZARO. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +with the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimers. +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimers in the documentation + and/or other materials provided with the distribution. +3. Neither the name "MadTeam", the names of the authors, their institution, + nor the names of its contributors may be used to endorse or promote + products derived from this Software without specific prior written + permission. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + +IN NO EVENT SHALL THE CONTRIBUTORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE +OR OTHER DEALINGS WITH THE SOFTWARE. diff --git a/pyamplicol/licenses/SymJIT.txt b/pyamplicol/licenses/SymJIT.txt new file mode 100644 index 0000000000..fbf10f96ad --- /dev/null +++ b/pyamplicol/licenses/SymJIT.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 siravan + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/pyamplicol/licenses/Symbolica.txt b/pyamplicol/licenses/Symbolica.txt new file mode 100644 index 0000000000..38b64bd583 --- /dev/null +++ b/pyamplicol/licenses/Symbolica.txt @@ -0,0 +1,42 @@ +Symbolica Proprietary Terms And Redistribution Permission + +SPDX identifier: LicenseRef-Symbolica-Proprietary + +Upstream terms reproduced from Symbolica 2.2.0 License.md + +# Symbolica licenses + +The source code of Symbolica is publicly available. It is not permitted to +copy or distribute any part of the Symbolica code without express prior +permission. + +Visit https://symbolica.io/license/ to read more about the license philosophy +behind Symbolica or to acquire a license. + +## Hobbyist / student license + +For personal non-professional use of Symbolica, for example for studies or for +a hobby, a free hobbyist license can be acquired. + +## Professional non-commercial license + +For professional non-commercial use of Symbolica, for example for work at an +educational / research institute, you or your employer must acquire a +professional non-commercial license. + +## Commercial license + +For professional commercial use of Symbolica, you or your employer must +acquire a commercial license. + +Attribution And Project-Specific Permission + +Symbolica is developed by Ben Ruijl and contributors and is available from +https://symbolica.io/ and https://github.com/symbolica-dev/symbolica. + +Express redistribution permission for pyAmpliCol: the pyAmpliCol project has +express prior permission from the Symbolica licensor to redistribute the +Symbolica components required by pyAmpliCol's binary runtime. This permission +is specific to pyAmpliCol release artifacts. It does not grant recipients a +general right to copy or distribute Symbolica separately, and it does not +replace the use license appropriate to the recipient's circumstances. diff --git a/pyamplicol/licenses/rust/ahash-0.8.12/LICENSE-APACHE b/pyamplicol/licenses/rust/ahash-0.8.12/LICENSE-APACHE new file mode 100644 index 0000000000..16fe87b06e --- /dev/null +++ b/pyamplicol/licenses/rust/ahash-0.8.12/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/pyamplicol/licenses/rust/ahash-0.8.12/LICENSE-MIT b/pyamplicol/licenses/rust/ahash-0.8.12/LICENSE-MIT new file mode 100644 index 0000000000..cba20106dd --- /dev/null +++ b/pyamplicol/licenses/rust/ahash-0.8.12/LICENSE-MIT @@ -0,0 +1,25 @@ +Copyright (c) 2018 Tom Kaitchuck + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/pyamplicol/licenses/rust/allocator-api2-0.2.21/LICENSE-APACHE b/pyamplicol/licenses/rust/allocator-api2-0.2.21/LICENSE-APACHE new file mode 100644 index 0000000000..a2ce4d97c1 --- /dev/null +++ b/pyamplicol/licenses/rust/allocator-api2-0.2.21/LICENSE-APACHE @@ -0,0 +1,176 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS diff --git a/pyamplicol/licenses/rust/allocator-api2-0.2.21/LICENSE-MIT b/pyamplicol/licenses/rust/allocator-api2-0.2.21/LICENSE-MIT new file mode 100644 index 0000000000..458723b374 --- /dev/null +++ b/pyamplicol/licenses/rust/allocator-api2-0.2.21/LICENSE-MIT @@ -0,0 +1,23 @@ +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/pyamplicol/licenses/rust/anyhow-1.0.103/LICENSE-APACHE b/pyamplicol/licenses/rust/anyhow-1.0.103/LICENSE-APACHE new file mode 100644 index 0000000000..1b5ec8b78e --- /dev/null +++ b/pyamplicol/licenses/rust/anyhow-1.0.103/LICENSE-APACHE @@ -0,0 +1,176 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS diff --git a/pyamplicol/licenses/rust/anyhow-1.0.103/LICENSE-MIT b/pyamplicol/licenses/rust/anyhow-1.0.103/LICENSE-MIT new file mode 100644 index 0000000000..31aa79387f --- /dev/null +++ b/pyamplicol/licenses/rust/anyhow-1.0.103/LICENSE-MIT @@ -0,0 +1,23 @@ +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/pyamplicol/licenses/rust/autocfg-1.5.1/LICENSE-APACHE b/pyamplicol/licenses/rust/autocfg-1.5.1/LICENSE-APACHE new file mode 100644 index 0000000000..16fe87b06e --- /dev/null +++ b/pyamplicol/licenses/rust/autocfg-1.5.1/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/pyamplicol/licenses/rust/autocfg-1.5.1/LICENSE-MIT b/pyamplicol/licenses/rust/autocfg-1.5.1/LICENSE-MIT new file mode 100644 index 0000000000..44fbc4d8b9 --- /dev/null +++ b/pyamplicol/licenses/rust/autocfg-1.5.1/LICENSE-MIT @@ -0,0 +1,25 @@ +Copyright (c) 2018 Josh Stone + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/pyamplicol/licenses/rust/bincode-2.0.1/LICENSE.md b/pyamplicol/licenses/rust/bincode-2.0.1/LICENSE.md new file mode 100644 index 0000000000..26b1f00a27 --- /dev/null +++ b/pyamplicol/licenses/rust/bincode-2.0.1/LICENSE.md @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 Ty Overby + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/pyamplicol/licenses/rust/bincode_derive-2.0.1/LICENSE.md b/pyamplicol/licenses/rust/bincode_derive-2.0.1/LICENSE.md new file mode 100644 index 0000000000..26b1f00a27 --- /dev/null +++ b/pyamplicol/licenses/rust/bincode_derive-2.0.1/LICENSE.md @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 Ty Overby + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/pyamplicol/licenses/rust/bitflags-1.3.2/LICENSE-APACHE b/pyamplicol/licenses/rust/bitflags-1.3.2/LICENSE-APACHE new file mode 100644 index 0000000000..16fe87b06e --- /dev/null +++ b/pyamplicol/licenses/rust/bitflags-1.3.2/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/pyamplicol/licenses/rust/bitflags-1.3.2/LICENSE-MIT b/pyamplicol/licenses/rust/bitflags-1.3.2/LICENSE-MIT new file mode 100644 index 0000000000..39d4bdb5ac --- /dev/null +++ b/pyamplicol/licenses/rust/bitflags-1.3.2/LICENSE-MIT @@ -0,0 +1,25 @@ +Copyright (c) 2014 The Rust Project Developers + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/pyamplicol/licenses/rust/block-buffer-0.10.4/LICENSE-APACHE b/pyamplicol/licenses/rust/block-buffer-0.10.4/LICENSE-APACHE new file mode 100644 index 0000000000..78173fa2e7 --- /dev/null +++ b/pyamplicol/licenses/rust/block-buffer-0.10.4/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/pyamplicol/licenses/rust/block-buffer-0.10.4/LICENSE-MIT b/pyamplicol/licenses/rust/block-buffer-0.10.4/LICENSE-MIT new file mode 100644 index 0000000000..502cee6e85 --- /dev/null +++ b/pyamplicol/licenses/rust/block-buffer-0.10.4/LICENSE-MIT @@ -0,0 +1,25 @@ +Copyright (c) 2018-2019 The RustCrypto Project Developers + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/pyamplicol/licenses/rust/bytemuck-1.25.1/LICENSE-APACHE b/pyamplicol/licenses/rust/bytemuck-1.25.1/LICENSE-APACHE new file mode 100644 index 0000000000..136d900456 --- /dev/null +++ b/pyamplicol/licenses/rust/bytemuck-1.25.1/LICENSE-APACHE @@ -0,0 +1,61 @@ +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + + "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and + (b) You must cause any modified files to carry prominent notices stating that You changed the files; and + (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + + You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + +To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/pyamplicol/licenses/rust/bytemuck-1.25.1/LICENSE-MIT b/pyamplicol/licenses/rust/bytemuck-1.25.1/LICENSE-MIT new file mode 100644 index 0000000000..164045faad --- /dev/null +++ b/pyamplicol/licenses/rust/bytemuck-1.25.1/LICENSE-MIT @@ -0,0 +1,9 @@ +MIT License + +Copyright (c) 2019 Daniel "Lokathor" Gee. + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice (including the next paragraph) shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/pyamplicol/licenses/rust/bytemuck-1.25.1/LICENSE-ZLIB b/pyamplicol/licenses/rust/bytemuck-1.25.1/LICENSE-ZLIB new file mode 100644 index 0000000000..d70707c75a --- /dev/null +++ b/pyamplicol/licenses/rust/bytemuck-1.25.1/LICENSE-ZLIB @@ -0,0 +1,11 @@ +Copyright (c) 2019 Daniel "Lokathor" Gee. + +This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required. + +2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. + +3. This notice may not be removed or altered from any source distribution. diff --git a/pyamplicol/licenses/rust/cfg-if-1.0.4/LICENSE-APACHE b/pyamplicol/licenses/rust/cfg-if-1.0.4/LICENSE-APACHE new file mode 100644 index 0000000000..16fe87b06e --- /dev/null +++ b/pyamplicol/licenses/rust/cfg-if-1.0.4/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/pyamplicol/licenses/rust/cfg-if-1.0.4/LICENSE-MIT b/pyamplicol/licenses/rust/cfg-if-1.0.4/LICENSE-MIT new file mode 100644 index 0000000000..39e0ed6602 --- /dev/null +++ b/pyamplicol/licenses/rust/cfg-if-1.0.4/LICENSE-MIT @@ -0,0 +1,25 @@ +Copyright (c) 2014 Alex Crichton + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/pyamplicol/licenses/rust/cpufeatures-0.2.17/LICENSE-APACHE b/pyamplicol/licenses/rust/cpufeatures-0.2.17/LICENSE-APACHE new file mode 100644 index 0000000000..78173fa2e7 --- /dev/null +++ b/pyamplicol/licenses/rust/cpufeatures-0.2.17/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/pyamplicol/licenses/rust/cpufeatures-0.2.17/LICENSE-MIT b/pyamplicol/licenses/rust/cpufeatures-0.2.17/LICENSE-MIT new file mode 100644 index 0000000000..0cd648f76b --- /dev/null +++ b/pyamplicol/licenses/rust/cpufeatures-0.2.17/LICENSE-MIT @@ -0,0 +1,25 @@ +Copyright (c) 2020-2025 The RustCrypto Project Developers + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/pyamplicol/licenses/rust/crossbeam-deque-0.8.7/LICENSE-APACHE b/pyamplicol/licenses/rust/crossbeam-deque-0.8.7/LICENSE-APACHE new file mode 100644 index 0000000000..16fe87b06e --- /dev/null +++ b/pyamplicol/licenses/rust/crossbeam-deque-0.8.7/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/pyamplicol/licenses/rust/crossbeam-deque-0.8.7/LICENSE-MIT b/pyamplicol/licenses/rust/crossbeam-deque-0.8.7/LICENSE-MIT new file mode 100644 index 0000000000..068d491fd5 --- /dev/null +++ b/pyamplicol/licenses/rust/crossbeam-deque-0.8.7/LICENSE-MIT @@ -0,0 +1,27 @@ +The MIT License (MIT) + +Copyright (c) 2019 The Crossbeam Project Developers + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/pyamplicol/licenses/rust/crossbeam-epoch-0.9.20/LICENSE-APACHE b/pyamplicol/licenses/rust/crossbeam-epoch-0.9.20/LICENSE-APACHE new file mode 100644 index 0000000000..16fe87b06e --- /dev/null +++ b/pyamplicol/licenses/rust/crossbeam-epoch-0.9.20/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/pyamplicol/licenses/rust/crossbeam-epoch-0.9.20/LICENSE-MIT b/pyamplicol/licenses/rust/crossbeam-epoch-0.9.20/LICENSE-MIT new file mode 100644 index 0000000000..068d491fd5 --- /dev/null +++ b/pyamplicol/licenses/rust/crossbeam-epoch-0.9.20/LICENSE-MIT @@ -0,0 +1,27 @@ +The MIT License (MIT) + +Copyright (c) 2019 The Crossbeam Project Developers + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/pyamplicol/licenses/rust/crossbeam-utils-0.8.22/LICENSE-APACHE b/pyamplicol/licenses/rust/crossbeam-utils-0.8.22/LICENSE-APACHE new file mode 100644 index 0000000000..16fe87b06e --- /dev/null +++ b/pyamplicol/licenses/rust/crossbeam-utils-0.8.22/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/pyamplicol/licenses/rust/crossbeam-utils-0.8.22/LICENSE-MIT b/pyamplicol/licenses/rust/crossbeam-utils-0.8.22/LICENSE-MIT new file mode 100644 index 0000000000..068d491fd5 --- /dev/null +++ b/pyamplicol/licenses/rust/crossbeam-utils-0.8.22/LICENSE-MIT @@ -0,0 +1,27 @@ +The MIT License (MIT) + +Copyright (c) 2019 The Crossbeam Project Developers + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/pyamplicol/licenses/rust/crypto-common-0.1.7/LICENSE-APACHE b/pyamplicol/licenses/rust/crypto-common-0.1.7/LICENSE-APACHE new file mode 100644 index 0000000000..78173fa2e7 --- /dev/null +++ b/pyamplicol/licenses/rust/crypto-common-0.1.7/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/pyamplicol/licenses/rust/crypto-common-0.1.7/LICENSE-MIT b/pyamplicol/licenses/rust/crypto-common-0.1.7/LICENSE-MIT new file mode 100644 index 0000000000..efb0b5f8b0 --- /dev/null +++ b/pyamplicol/licenses/rust/crypto-common-0.1.7/LICENSE-MIT @@ -0,0 +1,25 @@ +Copyright (c) 2021 RustCrypto Developers + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/pyamplicol/licenses/rust/digest-0.10.7/LICENSE-APACHE b/pyamplicol/licenses/rust/digest-0.10.7/LICENSE-APACHE new file mode 100644 index 0000000000..78173fa2e7 --- /dev/null +++ b/pyamplicol/licenses/rust/digest-0.10.7/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/pyamplicol/licenses/rust/digest-0.10.7/LICENSE-MIT b/pyamplicol/licenses/rust/digest-0.10.7/LICENSE-MIT new file mode 100644 index 0000000000..8dcb85b302 --- /dev/null +++ b/pyamplicol/licenses/rust/digest-0.10.7/LICENSE-MIT @@ -0,0 +1,25 @@ +Copyright (c) 2017 Artyom Pavlov + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/pyamplicol/licenses/rust/either-1.16.0/LICENSE-APACHE b/pyamplicol/licenses/rust/either-1.16.0/LICENSE-APACHE new file mode 100644 index 0000000000..16fe87b06e --- /dev/null +++ b/pyamplicol/licenses/rust/either-1.16.0/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/pyamplicol/licenses/rust/either-1.16.0/LICENSE-MIT b/pyamplicol/licenses/rust/either-1.16.0/LICENSE-MIT new file mode 100644 index 0000000000..9203baa055 --- /dev/null +++ b/pyamplicol/licenses/rust/either-1.16.0/LICENSE-MIT @@ -0,0 +1,25 @@ +Copyright (c) 2015 + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/pyamplicol/licenses/rust/equivalent-1.0.2/LICENSE-APACHE b/pyamplicol/licenses/rust/equivalent-1.0.2/LICENSE-APACHE new file mode 100644 index 0000000000..16fe87b06e --- /dev/null +++ b/pyamplicol/licenses/rust/equivalent-1.0.2/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/pyamplicol/licenses/rust/equivalent-1.0.2/LICENSE-MIT b/pyamplicol/licenses/rust/equivalent-1.0.2/LICENSE-MIT new file mode 100644 index 0000000000..5ac40dae7f --- /dev/null +++ b/pyamplicol/licenses/rust/equivalent-1.0.2/LICENSE-MIT @@ -0,0 +1,25 @@ +Copyright (c) 2016--2023 + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/pyamplicol/licenses/rust/fixedbitset-0.5.7/LICENSE-APACHE b/pyamplicol/licenses/rust/fixedbitset-0.5.7/LICENSE-APACHE new file mode 100644 index 0000000000..16fe87b06e --- /dev/null +++ b/pyamplicol/licenses/rust/fixedbitset-0.5.7/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/pyamplicol/licenses/rust/fixedbitset-0.5.7/LICENSE-MIT b/pyamplicol/licenses/rust/fixedbitset-0.5.7/LICENSE-MIT new file mode 100644 index 0000000000..327cc6174c --- /dev/null +++ b/pyamplicol/licenses/rust/fixedbitset-0.5.7/LICENSE-MIT @@ -0,0 +1,25 @@ +Copyright (c) 2015-2017 + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/pyamplicol/licenses/rust/foldhash-0.1.5/LICENSE b/pyamplicol/licenses/rust/foldhash-0.1.5/LICENSE new file mode 100644 index 0000000000..2f65b0a366 --- /dev/null +++ b/pyamplicol/licenses/rust/foldhash-0.1.5/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2024 Orson Peters + +This software is provided 'as-is', without any express or implied warranty. In +no event will the authors be held liable for any damages arising from the use of +this software. + +Permission is granted to anyone to use this software for any purpose, including +commercial applications, and to alter it and redistribute it freely, subject to +the following restrictions: + +1. The origin of this software must not be misrepresented; you must not claim + that you wrote the original software. If you use this software in a product, + an acknowledgment in the product documentation would be appreciated but is + not required. + +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + +3. This notice may not be removed or altered from any source distribution. \ No newline at end of file diff --git a/pyamplicol/licenses/rust/foldhash-0.2.0/LICENSE b/pyamplicol/licenses/rust/foldhash-0.2.0/LICENSE new file mode 100644 index 0000000000..2f65b0a366 --- /dev/null +++ b/pyamplicol/licenses/rust/foldhash-0.2.0/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2024 Orson Peters + +This software is provided 'as-is', without any express or implied warranty. In +no event will the authors be held liable for any damages arising from the use of +this software. + +Permission is granted to anyone to use this software for any purpose, including +commercial applications, and to alter it and redistribute it freely, subject to +the following restrictions: + +1. The origin of this software must not be misrepresented; you must not claim + that you wrote the original software. If you use this software in a product, + an acknowledgment in the product documentation would be appreciated but is + not required. + +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + +3. This notice may not be removed or altered from any source distribution. \ No newline at end of file diff --git a/pyamplicol/licenses/rust/generic-array-0.14.7/LICENSE b/pyamplicol/licenses/rust/generic-array-0.14.7/LICENSE new file mode 100644 index 0000000000..6d318946df --- /dev/null +++ b/pyamplicol/licenses/rust/generic-array-0.14.7/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015 Bartłomiej Kamiński + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/pyamplicol/licenses/rust/getrandom-0.3.4/LICENSE-APACHE b/pyamplicol/licenses/rust/getrandom-0.3.4/LICENSE-APACHE new file mode 100644 index 0000000000..17d74680f8 --- /dev/null +++ b/pyamplicol/licenses/rust/getrandom-0.3.4/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + https://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/pyamplicol/licenses/rust/getrandom-0.3.4/LICENSE-MIT b/pyamplicol/licenses/rust/getrandom-0.3.4/LICENSE-MIT new file mode 100644 index 0000000000..e54440a9e5 --- /dev/null +++ b/pyamplicol/licenses/rust/getrandom-0.3.4/LICENSE-MIT @@ -0,0 +1,26 @@ +Copyright (c) 2018-2025 The rust-random Project Developers +Copyright (c) 2014 The Rust Project Developers + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/pyamplicol/licenses/rust/hashbrown-0.15.5/LICENSE-APACHE b/pyamplicol/licenses/rust/hashbrown-0.15.5/LICENSE-APACHE new file mode 100644 index 0000000000..16fe87b06e --- /dev/null +++ b/pyamplicol/licenses/rust/hashbrown-0.15.5/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/pyamplicol/licenses/rust/hashbrown-0.15.5/LICENSE-MIT b/pyamplicol/licenses/rust/hashbrown-0.15.5/LICENSE-MIT new file mode 100644 index 0000000000..5afc2a7b0a --- /dev/null +++ b/pyamplicol/licenses/rust/hashbrown-0.15.5/LICENSE-MIT @@ -0,0 +1,25 @@ +Copyright (c) 2016 Amanieu d'Antras + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/pyamplicol/licenses/rust/hashbrown-0.16.1/LICENSE-APACHE b/pyamplicol/licenses/rust/hashbrown-0.16.1/LICENSE-APACHE new file mode 100644 index 0000000000..16fe87b06e --- /dev/null +++ b/pyamplicol/licenses/rust/hashbrown-0.16.1/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/pyamplicol/licenses/rust/hashbrown-0.16.1/LICENSE-MIT b/pyamplicol/licenses/rust/hashbrown-0.16.1/LICENSE-MIT new file mode 100644 index 0000000000..5afc2a7b0a --- /dev/null +++ b/pyamplicol/licenses/rust/hashbrown-0.16.1/LICENSE-MIT @@ -0,0 +1,25 @@ +Copyright (c) 2016 Amanieu d'Antras + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/pyamplicol/licenses/rust/hashbrown-0.17.1/LICENSE-APACHE b/pyamplicol/licenses/rust/hashbrown-0.17.1/LICENSE-APACHE new file mode 100644 index 0000000000..16fe87b06e --- /dev/null +++ b/pyamplicol/licenses/rust/hashbrown-0.17.1/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/pyamplicol/licenses/rust/hashbrown-0.17.1/LICENSE-MIT b/pyamplicol/licenses/rust/hashbrown-0.17.1/LICENSE-MIT new file mode 100644 index 0000000000..5afc2a7b0a --- /dev/null +++ b/pyamplicol/licenses/rust/hashbrown-0.17.1/LICENSE-MIT @@ -0,0 +1,25 @@ +Copyright (c) 2016 Amanieu d'Antras + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/pyamplicol/licenses/rust/heck-0.5.0/LICENSE-APACHE b/pyamplicol/licenses/rust/heck-0.5.0/LICENSE-APACHE new file mode 100644 index 0000000000..16fe87b06e --- /dev/null +++ b/pyamplicol/licenses/rust/heck-0.5.0/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/pyamplicol/licenses/rust/heck-0.5.0/LICENSE-MIT b/pyamplicol/licenses/rust/heck-0.5.0/LICENSE-MIT new file mode 100644 index 0000000000..e69282e381 --- /dev/null +++ b/pyamplicol/licenses/rust/heck-0.5.0/LICENSE-MIT @@ -0,0 +1,25 @@ +Copyright (c) 2015 The Rust Project Developers + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/pyamplicol/licenses/rust/hugepage-rs-0.1.1/LICENSE-APACHE b/pyamplicol/licenses/rust/hugepage-rs-0.1.1/LICENSE-APACHE new file mode 100644 index 0000000000..1b5ec8b78e --- /dev/null +++ b/pyamplicol/licenses/rust/hugepage-rs-0.1.1/LICENSE-APACHE @@ -0,0 +1,176 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS diff --git a/pyamplicol/licenses/rust/hugepage-rs-0.1.1/LICENSE-MIT b/pyamplicol/licenses/rust/hugepage-rs-0.1.1/LICENSE-MIT new file mode 100644 index 0000000000..31aa79387f --- /dev/null +++ b/pyamplicol/licenses/rust/hugepage-rs-0.1.1/LICENSE-MIT @@ -0,0 +1,23 @@ +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/pyamplicol/licenses/rust/indexmap-2.14.0/LICENSE-APACHE b/pyamplicol/licenses/rust/indexmap-2.14.0/LICENSE-APACHE new file mode 100644 index 0000000000..16fe87b06e --- /dev/null +++ b/pyamplicol/licenses/rust/indexmap-2.14.0/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/pyamplicol/licenses/rust/indexmap-2.14.0/LICENSE-MIT b/pyamplicol/licenses/rust/indexmap-2.14.0/LICENSE-MIT new file mode 100644 index 0000000000..8b8181068b --- /dev/null +++ b/pyamplicol/licenses/rust/indexmap-2.14.0/LICENSE-MIT @@ -0,0 +1,25 @@ +Copyright (c) 2016--2017 + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/pyamplicol/licenses/rust/itoa-1.0.18/LICENSE-APACHE b/pyamplicol/licenses/rust/itoa-1.0.18/LICENSE-APACHE new file mode 100644 index 0000000000..1b5ec8b78e --- /dev/null +++ b/pyamplicol/licenses/rust/itoa-1.0.18/LICENSE-APACHE @@ -0,0 +1,176 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS diff --git a/pyamplicol/licenses/rust/itoa-1.0.18/LICENSE-MIT b/pyamplicol/licenses/rust/itoa-1.0.18/LICENSE-MIT new file mode 100644 index 0000000000..31aa79387f --- /dev/null +++ b/pyamplicol/licenses/rust/itoa-1.0.18/LICENSE-MIT @@ -0,0 +1,23 @@ +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/pyamplicol/licenses/rust/lazy_static-1.5.0/LICENSE-APACHE b/pyamplicol/licenses/rust/lazy_static-1.5.0/LICENSE-APACHE new file mode 100644 index 0000000000..16fe87b06e --- /dev/null +++ b/pyamplicol/licenses/rust/lazy_static-1.5.0/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/pyamplicol/licenses/rust/lazy_static-1.5.0/LICENSE-MIT b/pyamplicol/licenses/rust/lazy_static-1.5.0/LICENSE-MIT new file mode 100644 index 0000000000..25597d5838 --- /dev/null +++ b/pyamplicol/licenses/rust/lazy_static-1.5.0/LICENSE-MIT @@ -0,0 +1,25 @@ +Copyright (c) 2010 The Rust Project Developers + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/pyamplicol/licenses/rust/libc-0.2.186/LICENSE-APACHE b/pyamplicol/licenses/rust/libc-0.2.186/LICENSE-APACHE new file mode 100644 index 0000000000..1b5ec8b78e --- /dev/null +++ b/pyamplicol/licenses/rust/libc-0.2.186/LICENSE-APACHE @@ -0,0 +1,176 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS diff --git a/pyamplicol/licenses/rust/libc-0.2.186/LICENSE-MIT b/pyamplicol/licenses/rust/libc-0.2.186/LICENSE-MIT new file mode 100644 index 0000000000..f4095b2fa2 --- /dev/null +++ b/pyamplicol/licenses/rust/libc-0.2.186/LICENSE-MIT @@ -0,0 +1,25 @@ +Copyright (c) The Rust Project Developers + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/pyamplicol/licenses/rust/mach2-0.4.3/LICENSE-APACHE b/pyamplicol/licenses/rust/mach2-0.4.3/LICENSE-APACHE new file mode 100644 index 0000000000..1b5ec8b78e --- /dev/null +++ b/pyamplicol/licenses/rust/mach2-0.4.3/LICENSE-APACHE @@ -0,0 +1,176 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS diff --git a/pyamplicol/licenses/rust/mach2-0.4.3/LICENSE-BSD b/pyamplicol/licenses/rust/mach2-0.4.3/LICENSE-BSD new file mode 100644 index 0000000000..e02a2d2cfc --- /dev/null +++ b/pyamplicol/licenses/rust/mach2-0.4.3/LICENSE-BSD @@ -0,0 +1,23 @@ +Copyright (c) 2019 Nick Fitzgerald, 2021 Yuki Okushi +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/pyamplicol/licenses/rust/mach2-0.4.3/LICENSE-MIT b/pyamplicol/licenses/rust/mach2-0.4.3/LICENSE-MIT new file mode 100644 index 0000000000..08bda86c65 --- /dev/null +++ b/pyamplicol/licenses/rust/mach2-0.4.3/LICENSE-MIT @@ -0,0 +1,25 @@ +Copyright (c) 2019 Nick Fitzgerald, 2021 Yuki Okushi + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/pyamplicol/licenses/rust/matrixmultiply-0.3.11/LICENSE-APACHE b/pyamplicol/licenses/rust/matrixmultiply-0.3.11/LICENSE-APACHE new file mode 100644 index 0000000000..16fe87b06e --- /dev/null +++ b/pyamplicol/licenses/rust/matrixmultiply-0.3.11/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/pyamplicol/licenses/rust/matrixmultiply-0.3.11/LICENSE-MIT b/pyamplicol/licenses/rust/matrixmultiply-0.3.11/LICENSE-MIT new file mode 100644 index 0000000000..9015f77637 --- /dev/null +++ b/pyamplicol/licenses/rust/matrixmultiply-0.3.11/LICENSE-MIT @@ -0,0 +1,27 @@ +Copyright (c) 2016 - 2023 Ulrik Sverdrup "bluss" +Copyirhgt (c) 2018 R. Janis Goldschmidt +Copyright (c) 2021 DutchGhost [constparse.rs] + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/pyamplicol/licenses/rust/memchr-2.8.3/COPYING b/pyamplicol/licenses/rust/memchr-2.8.3/COPYING new file mode 100644 index 0000000000..bb9c20a094 --- /dev/null +++ b/pyamplicol/licenses/rust/memchr-2.8.3/COPYING @@ -0,0 +1,3 @@ +This project is dual-licensed under the Unlicense and MIT licenses. + +You may use this code under the terms of either license. diff --git a/pyamplicol/licenses/rust/memchr-2.8.3/LICENSE-MIT b/pyamplicol/licenses/rust/memchr-2.8.3/LICENSE-MIT new file mode 100644 index 0000000000..3b0a5dc09c --- /dev/null +++ b/pyamplicol/licenses/rust/memchr-2.8.3/LICENSE-MIT @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015 Andrew Gallant + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/pyamplicol/licenses/rust/memchr-2.8.3/UNLICENSE b/pyamplicol/licenses/rust/memchr-2.8.3/UNLICENSE new file mode 100644 index 0000000000..68a49daad8 --- /dev/null +++ b/pyamplicol/licenses/rust/memchr-2.8.3/UNLICENSE @@ -0,0 +1,24 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to diff --git a/pyamplicol/licenses/rust/ndarray-0.17.2/LICENSE-APACHE b/pyamplicol/licenses/rust/ndarray-0.17.2/LICENSE-APACHE new file mode 100644 index 0000000000..16fe87b06e --- /dev/null +++ b/pyamplicol/licenses/rust/ndarray-0.17.2/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/pyamplicol/licenses/rust/ndarray-0.17.2/LICENSE-MIT b/pyamplicol/licenses/rust/ndarray-0.17.2/LICENSE-MIT new file mode 100644 index 0000000000..d0af99b046 --- /dev/null +++ b/pyamplicol/licenses/rust/ndarray-0.17.2/LICENSE-MIT @@ -0,0 +1,27 @@ +Copyright (c) 2015 - 2021 Ulrik Sverdrup "bluss", + Jim Turner, + and ndarray developers + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/pyamplicol/licenses/rust/num-complex-0.4.6/LICENSE-APACHE b/pyamplicol/licenses/rust/num-complex-0.4.6/LICENSE-APACHE new file mode 100644 index 0000000000..16fe87b06e --- /dev/null +++ b/pyamplicol/licenses/rust/num-complex-0.4.6/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/pyamplicol/licenses/rust/num-complex-0.4.6/LICENSE-MIT b/pyamplicol/licenses/rust/num-complex-0.4.6/LICENSE-MIT new file mode 100644 index 0000000000..39d4bdb5ac --- /dev/null +++ b/pyamplicol/licenses/rust/num-complex-0.4.6/LICENSE-MIT @@ -0,0 +1,25 @@ +Copyright (c) 2014 The Rust Project Developers + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/pyamplicol/licenses/rust/num-integer-0.1.46/LICENSE-APACHE b/pyamplicol/licenses/rust/num-integer-0.1.46/LICENSE-APACHE new file mode 100644 index 0000000000..16fe87b06e --- /dev/null +++ b/pyamplicol/licenses/rust/num-integer-0.1.46/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/pyamplicol/licenses/rust/num-integer-0.1.46/LICENSE-MIT b/pyamplicol/licenses/rust/num-integer-0.1.46/LICENSE-MIT new file mode 100644 index 0000000000..39d4bdb5ac --- /dev/null +++ b/pyamplicol/licenses/rust/num-integer-0.1.46/LICENSE-MIT @@ -0,0 +1,25 @@ +Copyright (c) 2014 The Rust Project Developers + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/pyamplicol/licenses/rust/num-traits-0.2.19/LICENSE-APACHE b/pyamplicol/licenses/rust/num-traits-0.2.19/LICENSE-APACHE new file mode 100644 index 0000000000..16fe87b06e --- /dev/null +++ b/pyamplicol/licenses/rust/num-traits-0.2.19/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/pyamplicol/licenses/rust/num-traits-0.2.19/LICENSE-MIT b/pyamplicol/licenses/rust/num-traits-0.2.19/LICENSE-MIT new file mode 100644 index 0000000000..39d4bdb5ac --- /dev/null +++ b/pyamplicol/licenses/rust/num-traits-0.2.19/LICENSE-MIT @@ -0,0 +1,25 @@ +Copyright (c) 2014 The Rust Project Developers + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/pyamplicol/licenses/rust/numpy-0.29.0/LICENSE b/pyamplicol/licenses/rust/numpy-0.29.0/LICENSE new file mode 100644 index 0000000000..d5b45151c7 --- /dev/null +++ b/pyamplicol/licenses/rust/numpy-0.29.0/LICENSE @@ -0,0 +1,25 @@ +BSD 2-Clause License + +Copyright (c) 2017, Toshiki Teramura +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/pyamplicol/licenses/rust/once_cell-1.21.4/LICENSE-APACHE b/pyamplicol/licenses/rust/once_cell-1.21.4/LICENSE-APACHE new file mode 100644 index 0000000000..16fe87b06e --- /dev/null +++ b/pyamplicol/licenses/rust/once_cell-1.21.4/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/pyamplicol/licenses/rust/once_cell-1.21.4/LICENSE-MIT b/pyamplicol/licenses/rust/once_cell-1.21.4/LICENSE-MIT new file mode 100644 index 0000000000..31aa79387f --- /dev/null +++ b/pyamplicol/licenses/rust/once_cell-1.21.4/LICENSE-MIT @@ -0,0 +1,23 @@ +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/pyamplicol/licenses/rust/petgraph-0.8.3/LICENSE-APACHE b/pyamplicol/licenses/rust/petgraph-0.8.3/LICENSE-APACHE new file mode 100644 index 0000000000..16fe87b06e --- /dev/null +++ b/pyamplicol/licenses/rust/petgraph-0.8.3/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/pyamplicol/licenses/rust/petgraph-0.8.3/LICENSE-MIT b/pyamplicol/licenses/rust/petgraph-0.8.3/LICENSE-MIT new file mode 100644 index 0000000000..9203baa055 --- /dev/null +++ b/pyamplicol/licenses/rust/petgraph-0.8.3/LICENSE-MIT @@ -0,0 +1,25 @@ +Copyright (c) 2015 + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/pyamplicol/licenses/rust/petgraph-0.8.3/assets/images/LICENSE.md b/pyamplicol/licenses/rust/petgraph-0.8.3/assets/images/LICENSE.md new file mode 100644 index 0000000000..ba81e5248f --- /dev/null +++ b/pyamplicol/licenses/rust/petgraph-0.8.3/assets/images/LICENSE.md @@ -0,0 +1,7 @@ +Graphosaurus (c) by the petgraph project. + +Graphosaurus is licensed under a +Creative Commons Attribution-ShareAlike 4.0 International License. + +You should have received a copy of the license along with this +work. If not, see . \ No newline at end of file diff --git a/pyamplicol/licenses/rust/ppv-lite86-0.2.21/LICENSE-APACHE b/pyamplicol/licenses/rust/ppv-lite86-0.2.21/LICENSE-APACHE new file mode 100644 index 0000000000..1eb3215354 --- /dev/null +++ b/pyamplicol/licenses/rust/ppv-lite86-0.2.21/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright 2019 The CryptoCorrosion Contributors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/pyamplicol/licenses/rust/ppv-lite86-0.2.21/LICENSE-MIT b/pyamplicol/licenses/rust/ppv-lite86-0.2.21/LICENSE-MIT new file mode 100644 index 0000000000..d78c961bca --- /dev/null +++ b/pyamplicol/licenses/rust/ppv-lite86-0.2.21/LICENSE-MIT @@ -0,0 +1,25 @@ +Copyright (c) 2019 The CryptoCorrosion Contributors + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/pyamplicol/licenses/rust/proc-macro2-1.0.106/LICENSE-APACHE b/pyamplicol/licenses/rust/proc-macro2-1.0.106/LICENSE-APACHE new file mode 100644 index 0000000000..1b5ec8b78e --- /dev/null +++ b/pyamplicol/licenses/rust/proc-macro2-1.0.106/LICENSE-APACHE @@ -0,0 +1,176 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS diff --git a/pyamplicol/licenses/rust/proc-macro2-1.0.106/LICENSE-MIT b/pyamplicol/licenses/rust/proc-macro2-1.0.106/LICENSE-MIT new file mode 100644 index 0000000000..31aa79387f --- /dev/null +++ b/pyamplicol/licenses/rust/proc-macro2-1.0.106/LICENSE-MIT @@ -0,0 +1,23 @@ +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/pyamplicol/licenses/rust/pyo3-0.29.2/LICENSE-APACHE b/pyamplicol/licenses/rust/pyo3-0.29.2/LICENSE-APACHE new file mode 100644 index 0000000000..72207b851d --- /dev/null +++ b/pyamplicol/licenses/rust/pyo3-0.29.2/LICENSE-APACHE @@ -0,0 +1,178 @@ +Copyright (c) 2017-present PyO3 Project and Contributors. https://github.com/PyO3 + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS diff --git a/pyamplicol/licenses/rust/pyo3-0.29.2/LICENSE-MIT b/pyamplicol/licenses/rust/pyo3-0.29.2/LICENSE-MIT new file mode 100644 index 0000000000..cd0ad009a8 --- /dev/null +++ b/pyamplicol/licenses/rust/pyo3-0.29.2/LICENSE-MIT @@ -0,0 +1,25 @@ +Copyright (c) 2023-present PyO3 Project and Contributors. https://github.com/PyO3 + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/pyamplicol/licenses/rust/pyo3-0.29.2/pyo3-runtime/LICENSE-APACHE b/pyamplicol/licenses/rust/pyo3-0.29.2/pyo3-runtime/LICENSE-APACHE new file mode 100644 index 0000000000..72207b851d --- /dev/null +++ b/pyamplicol/licenses/rust/pyo3-0.29.2/pyo3-runtime/LICENSE-APACHE @@ -0,0 +1,178 @@ +Copyright (c) 2017-present PyO3 Project and Contributors. https://github.com/PyO3 + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS diff --git a/pyamplicol/licenses/rust/pyo3-0.29.2/pyo3-runtime/LICENSE-MIT b/pyamplicol/licenses/rust/pyo3-0.29.2/pyo3-runtime/LICENSE-MIT new file mode 100644 index 0000000000..cd0ad009a8 --- /dev/null +++ b/pyamplicol/licenses/rust/pyo3-0.29.2/pyo3-runtime/LICENSE-MIT @@ -0,0 +1,25 @@ +Copyright (c) 2023-present PyO3 Project and Contributors. https://github.com/PyO3 + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/pyamplicol/licenses/rust/pyo3-build-config-0.29.2/LICENSE-APACHE b/pyamplicol/licenses/rust/pyo3-build-config-0.29.2/LICENSE-APACHE new file mode 100644 index 0000000000..72207b851d --- /dev/null +++ b/pyamplicol/licenses/rust/pyo3-build-config-0.29.2/LICENSE-APACHE @@ -0,0 +1,178 @@ +Copyright (c) 2017-present PyO3 Project and Contributors. https://github.com/PyO3 + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS diff --git a/pyamplicol/licenses/rust/pyo3-build-config-0.29.2/LICENSE-MIT b/pyamplicol/licenses/rust/pyo3-build-config-0.29.2/LICENSE-MIT new file mode 100644 index 0000000000..cd0ad009a8 --- /dev/null +++ b/pyamplicol/licenses/rust/pyo3-build-config-0.29.2/LICENSE-MIT @@ -0,0 +1,25 @@ +Copyright (c) 2023-present PyO3 Project and Contributors. https://github.com/PyO3 + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/pyamplicol/licenses/rust/pyo3-ffi-0.29.2/LICENSE-APACHE b/pyamplicol/licenses/rust/pyo3-ffi-0.29.2/LICENSE-APACHE new file mode 100644 index 0000000000..72207b851d --- /dev/null +++ b/pyamplicol/licenses/rust/pyo3-ffi-0.29.2/LICENSE-APACHE @@ -0,0 +1,178 @@ +Copyright (c) 2017-present PyO3 Project and Contributors. https://github.com/PyO3 + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS diff --git a/pyamplicol/licenses/rust/pyo3-ffi-0.29.2/LICENSE-MIT b/pyamplicol/licenses/rust/pyo3-ffi-0.29.2/LICENSE-MIT new file mode 100644 index 0000000000..cd0ad009a8 --- /dev/null +++ b/pyamplicol/licenses/rust/pyo3-ffi-0.29.2/LICENSE-MIT @@ -0,0 +1,25 @@ +Copyright (c) 2023-present PyO3 Project and Contributors. https://github.com/PyO3 + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/pyamplicol/licenses/rust/pyo3-macros-0.29.2/LICENSE-APACHE b/pyamplicol/licenses/rust/pyo3-macros-0.29.2/LICENSE-APACHE new file mode 100644 index 0000000000..72207b851d --- /dev/null +++ b/pyamplicol/licenses/rust/pyo3-macros-0.29.2/LICENSE-APACHE @@ -0,0 +1,178 @@ +Copyright (c) 2017-present PyO3 Project and Contributors. https://github.com/PyO3 + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS diff --git a/pyamplicol/licenses/rust/pyo3-macros-0.29.2/LICENSE-MIT b/pyamplicol/licenses/rust/pyo3-macros-0.29.2/LICENSE-MIT new file mode 100644 index 0000000000..cd0ad009a8 --- /dev/null +++ b/pyamplicol/licenses/rust/pyo3-macros-0.29.2/LICENSE-MIT @@ -0,0 +1,25 @@ +Copyright (c) 2023-present PyO3 Project and Contributors. https://github.com/PyO3 + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/pyamplicol/licenses/rust/pyo3-macros-backend-0.29.2/LICENSE-APACHE b/pyamplicol/licenses/rust/pyo3-macros-backend-0.29.2/LICENSE-APACHE new file mode 100644 index 0000000000..72207b851d --- /dev/null +++ b/pyamplicol/licenses/rust/pyo3-macros-backend-0.29.2/LICENSE-APACHE @@ -0,0 +1,178 @@ +Copyright (c) 2017-present PyO3 Project and Contributors. https://github.com/PyO3 + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS diff --git a/pyamplicol/licenses/rust/pyo3-macros-backend-0.29.2/LICENSE-MIT b/pyamplicol/licenses/rust/pyo3-macros-backend-0.29.2/LICENSE-MIT new file mode 100644 index 0000000000..cd0ad009a8 --- /dev/null +++ b/pyamplicol/licenses/rust/pyo3-macros-backend-0.29.2/LICENSE-MIT @@ -0,0 +1,25 @@ +Copyright (c) 2023-present PyO3 Project and Contributors. https://github.com/PyO3 + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/pyamplicol/licenses/rust/quote-1.0.46/LICENSE-APACHE b/pyamplicol/licenses/rust/quote-1.0.46/LICENSE-APACHE new file mode 100644 index 0000000000..1b5ec8b78e --- /dev/null +++ b/pyamplicol/licenses/rust/quote-1.0.46/LICENSE-APACHE @@ -0,0 +1,176 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS diff --git a/pyamplicol/licenses/rust/quote-1.0.46/LICENSE-MIT b/pyamplicol/licenses/rust/quote-1.0.46/LICENSE-MIT new file mode 100644 index 0000000000..31aa79387f --- /dev/null +++ b/pyamplicol/licenses/rust/quote-1.0.46/LICENSE-MIT @@ -0,0 +1,23 @@ +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/pyamplicol/licenses/rust/rand-0.9.5/COPYRIGHT b/pyamplicol/licenses/rust/rand-0.9.5/COPYRIGHT new file mode 100644 index 0000000000..468d907caf --- /dev/null +++ b/pyamplicol/licenses/rust/rand-0.9.5/COPYRIGHT @@ -0,0 +1,12 @@ +Copyrights in the Rand project are retained by their contributors. No +copyright assignment is required to contribute to the Rand project. + +For full authorship information, see the version control history. + +Except as otherwise noted (below and/or in individual files), Rand is +licensed under the Apache License, Version 2.0 or + or the MIT license + or , at your option. + +The Rand project includes code from the Rust project +published under these same licenses. diff --git a/pyamplicol/licenses/rust/rand-0.9.5/LICENSE-APACHE b/pyamplicol/licenses/rust/rand-0.9.5/LICENSE-APACHE new file mode 100644 index 0000000000..494ad3bfdf --- /dev/null +++ b/pyamplicol/licenses/rust/rand-0.9.5/LICENSE-APACHE @@ -0,0 +1,176 @@ + Apache License + Version 2.0, January 2004 + https://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS diff --git a/pyamplicol/licenses/rust/rand-0.9.5/LICENSE-MIT b/pyamplicol/licenses/rust/rand-0.9.5/LICENSE-MIT new file mode 100644 index 0000000000..d93b5baf34 --- /dev/null +++ b/pyamplicol/licenses/rust/rand-0.9.5/LICENSE-MIT @@ -0,0 +1,26 @@ +Copyright 2018 Developers of the Rand project +Copyright (c) 2014 The Rust Project Developers + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/pyamplicol/licenses/rust/rand_chacha-0.9.0/COPYRIGHT b/pyamplicol/licenses/rust/rand_chacha-0.9.0/COPYRIGHT new file mode 100644 index 0000000000..468d907caf --- /dev/null +++ b/pyamplicol/licenses/rust/rand_chacha-0.9.0/COPYRIGHT @@ -0,0 +1,12 @@ +Copyrights in the Rand project are retained by their contributors. No +copyright assignment is required to contribute to the Rand project. + +For full authorship information, see the version control history. + +Except as otherwise noted (below and/or in individual files), Rand is +licensed under the Apache License, Version 2.0 or + or the MIT license + or , at your option. + +The Rand project includes code from the Rust project +published under these same licenses. diff --git a/pyamplicol/licenses/rust/rand_chacha-0.9.0/LICENSE-APACHE b/pyamplicol/licenses/rust/rand_chacha-0.9.0/LICENSE-APACHE new file mode 100644 index 0000000000..494ad3bfdf --- /dev/null +++ b/pyamplicol/licenses/rust/rand_chacha-0.9.0/LICENSE-APACHE @@ -0,0 +1,176 @@ + Apache License + Version 2.0, January 2004 + https://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS diff --git a/pyamplicol/licenses/rust/rand_chacha-0.9.0/LICENSE-MIT b/pyamplicol/licenses/rust/rand_chacha-0.9.0/LICENSE-MIT new file mode 100644 index 0000000000..d93b5baf34 --- /dev/null +++ b/pyamplicol/licenses/rust/rand_chacha-0.9.0/LICENSE-MIT @@ -0,0 +1,26 @@ +Copyright 2018 Developers of the Rand project +Copyright (c) 2014 The Rust Project Developers + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/pyamplicol/licenses/rust/rand_core-0.9.5/COPYRIGHT b/pyamplicol/licenses/rust/rand_core-0.9.5/COPYRIGHT new file mode 100644 index 0000000000..468d907caf --- /dev/null +++ b/pyamplicol/licenses/rust/rand_core-0.9.5/COPYRIGHT @@ -0,0 +1,12 @@ +Copyrights in the Rand project are retained by their contributors. No +copyright assignment is required to contribute to the Rand project. + +For full authorship information, see the version control history. + +Except as otherwise noted (below and/or in individual files), Rand is +licensed under the Apache License, Version 2.0 or + or the MIT license + or , at your option. + +The Rand project includes code from the Rust project +published under these same licenses. diff --git a/pyamplicol/licenses/rust/rand_core-0.9.5/LICENSE-APACHE b/pyamplicol/licenses/rust/rand_core-0.9.5/LICENSE-APACHE new file mode 100644 index 0000000000..455787c233 --- /dev/null +++ b/pyamplicol/licenses/rust/rand_core-0.9.5/LICENSE-APACHE @@ -0,0 +1,187 @@ + Apache License + Version 2.0, January 2004 + https://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. diff --git a/pyamplicol/licenses/rust/rand_core-0.9.5/LICENSE-MIT b/pyamplicol/licenses/rust/rand_core-0.9.5/LICENSE-MIT new file mode 100644 index 0000000000..d93b5baf34 --- /dev/null +++ b/pyamplicol/licenses/rust/rand_core-0.9.5/LICENSE-MIT @@ -0,0 +1,26 @@ +Copyright 2018 Developers of the Rand project +Copyright (c) 2014 The Rust Project Developers + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/pyamplicol/licenses/rust/rawpointer-0.2.1/LICENSE-APACHE b/pyamplicol/licenses/rust/rawpointer-0.2.1/LICENSE-APACHE new file mode 100644 index 0000000000..16fe87b06e --- /dev/null +++ b/pyamplicol/licenses/rust/rawpointer-0.2.1/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/pyamplicol/licenses/rust/rawpointer-0.2.1/LICENSE-MIT b/pyamplicol/licenses/rust/rawpointer-0.2.1/LICENSE-MIT new file mode 100644 index 0000000000..9203baa055 --- /dev/null +++ b/pyamplicol/licenses/rust/rawpointer-0.2.1/LICENSE-MIT @@ -0,0 +1,25 @@ +Copyright (c) 2015 + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/pyamplicol/licenses/rust/rayon-1.12.0/LICENSE-APACHE b/pyamplicol/licenses/rust/rayon-1.12.0/LICENSE-APACHE new file mode 100644 index 0000000000..16fe87b06e --- /dev/null +++ b/pyamplicol/licenses/rust/rayon-1.12.0/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/pyamplicol/licenses/rust/rayon-1.12.0/LICENSE-MIT b/pyamplicol/licenses/rust/rayon-1.12.0/LICENSE-MIT new file mode 100644 index 0000000000..25597d5838 --- /dev/null +++ b/pyamplicol/licenses/rust/rayon-1.12.0/LICENSE-MIT @@ -0,0 +1,25 @@ +Copyright (c) 2010 The Rust Project Developers + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/pyamplicol/licenses/rust/rayon-core-1.13.0/LICENSE-APACHE b/pyamplicol/licenses/rust/rayon-core-1.13.0/LICENSE-APACHE new file mode 100644 index 0000000000..16fe87b06e --- /dev/null +++ b/pyamplicol/licenses/rust/rayon-core-1.13.0/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/pyamplicol/licenses/rust/rayon-core-1.13.0/LICENSE-MIT b/pyamplicol/licenses/rust/rayon-core-1.13.0/LICENSE-MIT new file mode 100644 index 0000000000..25597d5838 --- /dev/null +++ b/pyamplicol/licenses/rust/rayon-core-1.13.0/LICENSE-MIT @@ -0,0 +1,25 @@ +Copyright (c) 2010 The Rust Project Developers + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/pyamplicol/licenses/rust/region-3.0.2/LICENSE b/pyamplicol/licenses/rust/region-3.0.2/LICENSE new file mode 100644 index 0000000000..6c34497dcc --- /dev/null +++ b/pyamplicol/licenses/rust/region-3.0.2/LICENSE @@ -0,0 +1,25 @@ +Copyright (c) 2016 Elliott Linder + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/pyamplicol/licenses/rust/rustc-hash-2.1.3/LICENSE-APACHE b/pyamplicol/licenses/rust/rustc-hash-2.1.3/LICENSE-APACHE new file mode 100644 index 0000000000..a7e77cb28d --- /dev/null +++ b/pyamplicol/licenses/rust/rustc-hash-2.1.3/LICENSE-APACHE @@ -0,0 +1,176 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS \ No newline at end of file diff --git a/pyamplicol/licenses/rust/rustc-hash-2.1.3/LICENSE-MIT b/pyamplicol/licenses/rust/rustc-hash-2.1.3/LICENSE-MIT new file mode 100644 index 0000000000..468cd79a8f --- /dev/null +++ b/pyamplicol/licenses/rust/rustc-hash-2.1.3/LICENSE-MIT @@ -0,0 +1,23 @@ +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/pyamplicol/licenses/rust/safe_arch-1.0.0/LICENSE-APACHE.md b/pyamplicol/licenses/rust/safe_arch-1.0.0/LICENSE-APACHE.md new file mode 100644 index 0000000000..136d900456 --- /dev/null +++ b/pyamplicol/licenses/rust/safe_arch-1.0.0/LICENSE-APACHE.md @@ -0,0 +1,61 @@ +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + + "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and + (b) You must cause any modified files to carry prominent notices stating that You changed the files; and + (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + + You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + +To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/pyamplicol/licenses/rust/safe_arch-1.0.0/LICENSE-MIT.md b/pyamplicol/licenses/rust/safe_arch-1.0.0/LICENSE-MIT.md new file mode 100644 index 0000000000..b61b394edd --- /dev/null +++ b/pyamplicol/licenses/rust/safe_arch-1.0.0/LICENSE-MIT.md @@ -0,0 +1,9 @@ +MIT License + +Copyright (c) 2023 Daniel "Lokathor" Gee. + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice (including the next paragraph) shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/pyamplicol/licenses/rust/safe_arch-1.0.0/LICENSE-ZLIB.md b/pyamplicol/licenses/rust/safe_arch-1.0.0/LICENSE-ZLIB.md new file mode 100644 index 0000000000..4991f7b1df --- /dev/null +++ b/pyamplicol/licenses/rust/safe_arch-1.0.0/LICENSE-ZLIB.md @@ -0,0 +1,11 @@ +Copyright (c) 2020 Daniel "Lokathor" Gee. + +This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required. + +2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. + +3. This notice may not be removed or altered from any source distribution. diff --git a/pyamplicol/licenses/rust/serde-1.0.228/LICENSE-APACHE b/pyamplicol/licenses/rust/serde-1.0.228/LICENSE-APACHE new file mode 100644 index 0000000000..1b5ec8b78e --- /dev/null +++ b/pyamplicol/licenses/rust/serde-1.0.228/LICENSE-APACHE @@ -0,0 +1,176 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS diff --git a/pyamplicol/licenses/rust/serde-1.0.228/LICENSE-MIT b/pyamplicol/licenses/rust/serde-1.0.228/LICENSE-MIT new file mode 100644 index 0000000000..31aa79387f --- /dev/null +++ b/pyamplicol/licenses/rust/serde-1.0.228/LICENSE-MIT @@ -0,0 +1,23 @@ +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/pyamplicol/licenses/rust/serde_core-1.0.228/LICENSE-APACHE b/pyamplicol/licenses/rust/serde_core-1.0.228/LICENSE-APACHE new file mode 100644 index 0000000000..1b5ec8b78e --- /dev/null +++ b/pyamplicol/licenses/rust/serde_core-1.0.228/LICENSE-APACHE @@ -0,0 +1,176 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS diff --git a/pyamplicol/licenses/rust/serde_core-1.0.228/LICENSE-MIT b/pyamplicol/licenses/rust/serde_core-1.0.228/LICENSE-MIT new file mode 100644 index 0000000000..31aa79387f --- /dev/null +++ b/pyamplicol/licenses/rust/serde_core-1.0.228/LICENSE-MIT @@ -0,0 +1,23 @@ +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/pyamplicol/licenses/rust/serde_derive-1.0.228/LICENSE-APACHE b/pyamplicol/licenses/rust/serde_derive-1.0.228/LICENSE-APACHE new file mode 100644 index 0000000000..1b5ec8b78e --- /dev/null +++ b/pyamplicol/licenses/rust/serde_derive-1.0.228/LICENSE-APACHE @@ -0,0 +1,176 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS diff --git a/pyamplicol/licenses/rust/serde_derive-1.0.228/LICENSE-MIT b/pyamplicol/licenses/rust/serde_derive-1.0.228/LICENSE-MIT new file mode 100644 index 0000000000..31aa79387f --- /dev/null +++ b/pyamplicol/licenses/rust/serde_derive-1.0.228/LICENSE-MIT @@ -0,0 +1,23 @@ +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/pyamplicol/licenses/rust/serde_json-1.0.150/LICENSE-APACHE b/pyamplicol/licenses/rust/serde_json-1.0.150/LICENSE-APACHE new file mode 100644 index 0000000000..1b5ec8b78e --- /dev/null +++ b/pyamplicol/licenses/rust/serde_json-1.0.150/LICENSE-APACHE @@ -0,0 +1,176 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS diff --git a/pyamplicol/licenses/rust/serde_json-1.0.150/LICENSE-MIT b/pyamplicol/licenses/rust/serde_json-1.0.150/LICENSE-MIT new file mode 100644 index 0000000000..31aa79387f --- /dev/null +++ b/pyamplicol/licenses/rust/serde_json-1.0.150/LICENSE-MIT @@ -0,0 +1,23 @@ +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/pyamplicol/licenses/rust/serde_spanned-1.1.1/LICENSE-APACHE b/pyamplicol/licenses/rust/serde_spanned-1.1.1/LICENSE-APACHE new file mode 100644 index 0000000000..8f71f43fee --- /dev/null +++ b/pyamplicol/licenses/rust/serde_spanned-1.1.1/LICENSE-APACHE @@ -0,0 +1,202 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/pyamplicol/licenses/rust/serde_spanned-1.1.1/LICENSE-MIT b/pyamplicol/licenses/rust/serde_spanned-1.1.1/LICENSE-MIT new file mode 100644 index 0000000000..a2d01088b6 --- /dev/null +++ b/pyamplicol/licenses/rust/serde_spanned-1.1.1/LICENSE-MIT @@ -0,0 +1,19 @@ +Copyright (c) Individual contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/pyamplicol/licenses/rust/sha2-0.10.9/LICENSE-APACHE b/pyamplicol/licenses/rust/sha2-0.10.9/LICENSE-APACHE new file mode 100644 index 0000000000..78173fa2e7 --- /dev/null +++ b/pyamplicol/licenses/rust/sha2-0.10.9/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/pyamplicol/licenses/rust/sha2-0.10.9/LICENSE-MIT b/pyamplicol/licenses/rust/sha2-0.10.9/LICENSE-MIT new file mode 100644 index 0000000000..66cf75563b --- /dev/null +++ b/pyamplicol/licenses/rust/sha2-0.10.9/LICENSE-MIT @@ -0,0 +1,27 @@ +Copyright (c) 2006-2009 Graydon Hoare +Copyright (c) 2009-2013 Mozilla Foundation +Copyright (c) 2016 Artyom Pavlov + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/pyamplicol/licenses/rust/spec_math-0.1.6/LICENSE-APACHE b/pyamplicol/licenses/rust/spec_math-0.1.6/LICENSE-APACHE new file mode 100644 index 0000000000..1b5ec8b78e --- /dev/null +++ b/pyamplicol/licenses/rust/spec_math-0.1.6/LICENSE-APACHE @@ -0,0 +1,176 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS diff --git a/pyamplicol/licenses/rust/spec_math-0.1.6/LICENSE-MIT b/pyamplicol/licenses/rust/spec_math-0.1.6/LICENSE-MIT new file mode 100644 index 0000000000..31aa79387f --- /dev/null +++ b/pyamplicol/licenses/rust/spec_math-0.1.6/LICENSE-MIT @@ -0,0 +1,23 @@ +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/pyamplicol/licenses/rust/symjit-2.22.0/LICENSE b/pyamplicol/licenses/rust/symjit-2.22.0/LICENSE new file mode 100644 index 0000000000..fbf10f96ad --- /dev/null +++ b/pyamplicol/licenses/rust/symjit-2.22.0/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 siravan + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/pyamplicol/licenses/rust/syn-2.0.119/LICENSE-APACHE b/pyamplicol/licenses/rust/syn-2.0.119/LICENSE-APACHE new file mode 100644 index 0000000000..1b5ec8b78e --- /dev/null +++ b/pyamplicol/licenses/rust/syn-2.0.119/LICENSE-APACHE @@ -0,0 +1,176 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS diff --git a/pyamplicol/licenses/rust/syn-2.0.119/LICENSE-MIT b/pyamplicol/licenses/rust/syn-2.0.119/LICENSE-MIT new file mode 100644 index 0000000000..31aa79387f --- /dev/null +++ b/pyamplicol/licenses/rust/syn-2.0.119/LICENSE-MIT @@ -0,0 +1,23 @@ +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/pyamplicol/licenses/rust/target-lexicon-0.13.5/LICENSE b/pyamplicol/licenses/rust/target-lexicon-0.13.5/LICENSE new file mode 100644 index 0000000000..f9d81955f4 --- /dev/null +++ b/pyamplicol/licenses/rust/target-lexicon-0.13.5/LICENSE @@ -0,0 +1,220 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +--- LLVM Exceptions to the Apache 2.0 License ---- + +As an exception, if, as a result of your compiling your source code, portions +of this Software are embedded into an Object form of such source code, you +may redistribute such embedded portions in such Object form without complying +with the conditions of Sections 4(a), 4(b) and 4(d) of the License. + +In addition, if you combine or link compiled forms of this Software with +software that is licensed under the GPLv2 ("Combined Software") and if a +court of competent jurisdiction determines that the patent provision (Section +3), the indemnity provision (Section 9) or other Section of the License +conflicts with the conditions of the GPLv2, you may retroactively and +prospectively choose to deem waived or otherwise exclude such Section(s) of +the License, but only in their entirety and only with respect to the Combined +Software. + diff --git a/pyamplicol/licenses/rust/thiserror-2.0.18/LICENSE-APACHE b/pyamplicol/licenses/rust/thiserror-2.0.18/LICENSE-APACHE new file mode 100644 index 0000000000..1b5ec8b78e --- /dev/null +++ b/pyamplicol/licenses/rust/thiserror-2.0.18/LICENSE-APACHE @@ -0,0 +1,176 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS diff --git a/pyamplicol/licenses/rust/thiserror-2.0.18/LICENSE-MIT b/pyamplicol/licenses/rust/thiserror-2.0.18/LICENSE-MIT new file mode 100644 index 0000000000..31aa79387f --- /dev/null +++ b/pyamplicol/licenses/rust/thiserror-2.0.18/LICENSE-MIT @@ -0,0 +1,23 @@ +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/pyamplicol/licenses/rust/thiserror-impl-2.0.18/LICENSE-APACHE b/pyamplicol/licenses/rust/thiserror-impl-2.0.18/LICENSE-APACHE new file mode 100644 index 0000000000..1b5ec8b78e --- /dev/null +++ b/pyamplicol/licenses/rust/thiserror-impl-2.0.18/LICENSE-APACHE @@ -0,0 +1,176 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS diff --git a/pyamplicol/licenses/rust/thiserror-impl-2.0.18/LICENSE-MIT b/pyamplicol/licenses/rust/thiserror-impl-2.0.18/LICENSE-MIT new file mode 100644 index 0000000000..31aa79387f --- /dev/null +++ b/pyamplicol/licenses/rust/thiserror-impl-2.0.18/LICENSE-MIT @@ -0,0 +1,23 @@ +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/pyamplicol/licenses/rust/toml-1.1.3+spec-1.1.0/LICENSE-APACHE b/pyamplicol/licenses/rust/toml-1.1.3+spec-1.1.0/LICENSE-APACHE new file mode 100644 index 0000000000..8f71f43fee --- /dev/null +++ b/pyamplicol/licenses/rust/toml-1.1.3+spec-1.1.0/LICENSE-APACHE @@ -0,0 +1,202 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/pyamplicol/licenses/rust/toml-1.1.3+spec-1.1.0/LICENSE-MIT b/pyamplicol/licenses/rust/toml-1.1.3+spec-1.1.0/LICENSE-MIT new file mode 100644 index 0000000000..a2d01088b6 --- /dev/null +++ b/pyamplicol/licenses/rust/toml-1.1.3+spec-1.1.0/LICENSE-MIT @@ -0,0 +1,19 @@ +Copyright (c) Individual contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/pyamplicol/licenses/rust/toml_datetime-1.1.1+spec-1.1.0/LICENSE-APACHE b/pyamplicol/licenses/rust/toml_datetime-1.1.1+spec-1.1.0/LICENSE-APACHE new file mode 100644 index 0000000000..8f71f43fee --- /dev/null +++ b/pyamplicol/licenses/rust/toml_datetime-1.1.1+spec-1.1.0/LICENSE-APACHE @@ -0,0 +1,202 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/pyamplicol/licenses/rust/toml_datetime-1.1.1+spec-1.1.0/LICENSE-MIT b/pyamplicol/licenses/rust/toml_datetime-1.1.1+spec-1.1.0/LICENSE-MIT new file mode 100644 index 0000000000..a2d01088b6 --- /dev/null +++ b/pyamplicol/licenses/rust/toml_datetime-1.1.1+spec-1.1.0/LICENSE-MIT @@ -0,0 +1,19 @@ +Copyright (c) Individual contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/pyamplicol/licenses/rust/toml_parser-1.1.2+spec-1.1.0/LICENSE-APACHE b/pyamplicol/licenses/rust/toml_parser-1.1.2+spec-1.1.0/LICENSE-APACHE new file mode 100644 index 0000000000..8f71f43fee --- /dev/null +++ b/pyamplicol/licenses/rust/toml_parser-1.1.2+spec-1.1.0/LICENSE-APACHE @@ -0,0 +1,202 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/pyamplicol/licenses/rust/toml_parser-1.1.2+spec-1.1.0/LICENSE-MIT b/pyamplicol/licenses/rust/toml_parser-1.1.2+spec-1.1.0/LICENSE-MIT new file mode 100644 index 0000000000..a2d01088b6 --- /dev/null +++ b/pyamplicol/licenses/rust/toml_parser-1.1.2+spec-1.1.0/LICENSE-MIT @@ -0,0 +1,19 @@ +Copyright (c) Individual contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/pyamplicol/licenses/rust/toml_writer-1.1.2+spec-1.1.0/LICENSE-APACHE b/pyamplicol/licenses/rust/toml_writer-1.1.2+spec-1.1.0/LICENSE-APACHE new file mode 100644 index 0000000000..8f71f43fee --- /dev/null +++ b/pyamplicol/licenses/rust/toml_writer-1.1.2+spec-1.1.0/LICENSE-APACHE @@ -0,0 +1,202 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/pyamplicol/licenses/rust/toml_writer-1.1.2+spec-1.1.0/LICENSE-MIT b/pyamplicol/licenses/rust/toml_writer-1.1.2+spec-1.1.0/LICENSE-MIT new file mode 100644 index 0000000000..a2d01088b6 --- /dev/null +++ b/pyamplicol/licenses/rust/toml_writer-1.1.2+spec-1.1.0/LICENSE-MIT @@ -0,0 +1,19 @@ +Copyright (c) Individual contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/pyamplicol/licenses/rust/typenum-1.20.1/LICENSE b/pyamplicol/licenses/rust/typenum-1.20.1/LICENSE new file mode 100644 index 0000000000..364b7731ca --- /dev/null +++ b/pyamplicol/licenses/rust/typenum-1.20.1/LICENSE @@ -0,0 +1 @@ +MIT OR Apache-2.0 \ No newline at end of file diff --git a/pyamplicol/licenses/rust/typenum-1.20.1/LICENSE-APACHE b/pyamplicol/licenses/rust/typenum-1.20.1/LICENSE-APACHE new file mode 100644 index 0000000000..4bf355c3e0 --- /dev/null +++ b/pyamplicol/licenses/rust/typenum-1.20.1/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright 2014 Paho Lurie-Gregg + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. \ No newline at end of file diff --git a/pyamplicol/licenses/rust/typenum-1.20.1/LICENSE-MIT b/pyamplicol/licenses/rust/typenum-1.20.1/LICENSE-MIT new file mode 100644 index 0000000000..e567a4d280 --- /dev/null +++ b/pyamplicol/licenses/rust/typenum-1.20.1/LICENSE-MIT @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 Paho Lurie-Gregg + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/pyamplicol/licenses/rust/unicode-ident-1.0.24/LICENSE-APACHE b/pyamplicol/licenses/rust/unicode-ident-1.0.24/LICENSE-APACHE new file mode 100644 index 0000000000..1b5ec8b78e --- /dev/null +++ b/pyamplicol/licenses/rust/unicode-ident-1.0.24/LICENSE-APACHE @@ -0,0 +1,176 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS diff --git a/pyamplicol/licenses/rust/unicode-ident-1.0.24/LICENSE-MIT b/pyamplicol/licenses/rust/unicode-ident-1.0.24/LICENSE-MIT new file mode 100644 index 0000000000..31aa79387f --- /dev/null +++ b/pyamplicol/licenses/rust/unicode-ident-1.0.24/LICENSE-MIT @@ -0,0 +1,23 @@ +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/pyamplicol/licenses/rust/unicode-ident-1.0.24/LICENSE-UNICODE b/pyamplicol/licenses/rust/unicode-ident-1.0.24/LICENSE-UNICODE new file mode 100644 index 0000000000..11f2842a30 --- /dev/null +++ b/pyamplicol/licenses/rust/unicode-ident-1.0.24/LICENSE-UNICODE @@ -0,0 +1,39 @@ +UNICODE LICENSE V3 + +COPYRIGHT AND PERMISSION NOTICE + +Copyright © 1991-2023 Unicode, Inc. + +NOTICE TO USER: Carefully read the following legal agreement. BY +DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR +SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE +TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT +DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of data files and any associated documentation (the "Data Files") or +software and any associated documentation (the "Software") to deal in the +Data Files or Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, and/or sell +copies of the Data Files or Software, and to permit persons to whom the +Data Files or Software are furnished to do so, provided that either (a) +this copyright and permission notice appear with all copies of the Data +Files or Software, or (b) this copyright and permission notice appear in +associated Documentation. + +THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF +THIRD PARTY RIGHTS. + +IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE +BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, +OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA +FILES OR SOFTWARE. + +Except as contained in this notice, the name of a copyright holder shall +not be used in advertising or otherwise to promote the sale, use or other +dealings in these Data Files or Software without prior written +authorization of the copyright holder. diff --git a/pyamplicol/licenses/rust/unty-0.0.4/LICENSE-APACHE b/pyamplicol/licenses/rust/unty-0.0.4/LICENSE-APACHE new file mode 100644 index 0000000000..28784f7584 --- /dev/null +++ b/pyamplicol/licenses/rust/unty-0.0.4/LICENSE-APACHE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2023 Bincode + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/pyamplicol/licenses/rust/unty-0.0.4/LICENSE-MIT b/pyamplicol/licenses/rust/unty-0.0.4/LICENSE-MIT new file mode 100644 index 0000000000..bfa1bb494f --- /dev/null +++ b/pyamplicol/licenses/rust/unty-0.0.4/LICENSE-MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Bincode + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/pyamplicol/licenses/rust/version_check-0.9.5/LICENSE-APACHE b/pyamplicol/licenses/rust/version_check-0.9.5/LICENSE-APACHE new file mode 100644 index 0000000000..16fe87b06e --- /dev/null +++ b/pyamplicol/licenses/rust/version_check-0.9.5/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/pyamplicol/licenses/rust/version_check-0.9.5/LICENSE-MIT b/pyamplicol/licenses/rust/version_check-0.9.5/LICENSE-MIT new file mode 100644 index 0000000000..dfc0e73b19 --- /dev/null +++ b/pyamplicol/licenses/rust/version_check-0.9.5/LICENSE-MIT @@ -0,0 +1,19 @@ +The MIT License (MIT) +Copyright (c) 2017-2018 Sergio Benitez + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/pyamplicol/licenses/rust/virtue-0.0.18/LICENSE.md b/pyamplicol/licenses/rust/virtue-0.0.18/LICENSE.md new file mode 100644 index 0000000000..4d5a490145 --- /dev/null +++ b/pyamplicol/licenses/rust/virtue-0.0.18/LICENSE.md @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2021 Victor Koenders + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/pyamplicol/licenses/rust/wasmtime-jit-icache-coherence-34.0.2/LICENSE b/pyamplicol/licenses/rust/wasmtime-jit-icache-coherence-34.0.2/LICENSE new file mode 100644 index 0000000000..f9d81955f4 --- /dev/null +++ b/pyamplicol/licenses/rust/wasmtime-jit-icache-coherence-34.0.2/LICENSE @@ -0,0 +1,220 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +--- LLVM Exceptions to the Apache 2.0 License ---- + +As an exception, if, as a result of your compiling your source code, portions +of this Software are embedded into an Object form of such source code, you +may redistribute such embedded portions in such Object form without complying +with the conditions of Sections 4(a), 4(b) and 4(d) of the License. + +In addition, if you combine or link compiled forms of this Software with +software that is licensed under the GPLv2 ("Combined Software") and if a +court of competent jurisdiction determines that the patent provision (Section +3), the indemnity provision (Section 9) or other Section of the License +conflicts with the conditions of the GPLv2, you may retroactively and +prospectively choose to deem waived or otherwise exclude such Section(s) of +the License, but only in their entirety and only with respect to the Combined +Software. + diff --git a/pyamplicol/licenses/rust/wide-1.5.0/LICENSE-ZLIB.md b/pyamplicol/licenses/rust/wide-1.5.0/LICENSE-ZLIB.md new file mode 100644 index 0000000000..4991f7b1df --- /dev/null +++ b/pyamplicol/licenses/rust/wide-1.5.0/LICENSE-ZLIB.md @@ -0,0 +1,11 @@ +Copyright (c) 2020 Daniel "Lokathor" Gee. + +This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required. + +2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. + +3. This notice may not be removed or altered from any source distribution. diff --git a/pyamplicol/licenses/rust/winnow-1.0.4/LICENSE-MIT b/pyamplicol/licenses/rust/winnow-1.0.4/LICENSE-MIT new file mode 100644 index 0000000000..c9b44cb8aa --- /dev/null +++ b/pyamplicol/licenses/rust/winnow-1.0.4/LICENSE-MIT @@ -0,0 +1,18 @@ +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/pyamplicol/licenses/rust/zerocopy-0.8.54/LICENSE-APACHE b/pyamplicol/licenses/rust/zerocopy-0.8.54/LICENSE-APACHE new file mode 100644 index 0000000000..2dc22c12fa --- /dev/null +++ b/pyamplicol/licenses/rust/zerocopy-0.8.54/LICENSE-APACHE @@ -0,0 +1,202 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2023 The Fuchsia Authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/pyamplicol/licenses/rust/zerocopy-0.8.54/LICENSE-BSD b/pyamplicol/licenses/rust/zerocopy-0.8.54/LICENSE-BSD new file mode 100644 index 0000000000..7ed244f42d --- /dev/null +++ b/pyamplicol/licenses/rust/zerocopy-0.8.54/LICENSE-BSD @@ -0,0 +1,24 @@ +Copyright 2019 The Fuchsia Authors. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/pyamplicol/licenses/rust/zerocopy-0.8.54/LICENSE-MIT b/pyamplicol/licenses/rust/zerocopy-0.8.54/LICENSE-MIT new file mode 100644 index 0000000000..26e15216cd --- /dev/null +++ b/pyamplicol/licenses/rust/zerocopy-0.8.54/LICENSE-MIT @@ -0,0 +1,26 @@ +Copyright 2023 The Fuchsia Authors + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. + diff --git a/pyamplicol/licenses/rust/zmij-1.0.23/LICENSE-MIT b/pyamplicol/licenses/rust/zmij-1.0.23/LICENSE-MIT new file mode 100644 index 0000000000..31aa79387f --- /dev/null +++ b/pyamplicol/licenses/rust/zmij-1.0.23/LICENSE-MIT @@ -0,0 +1,23 @@ +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/pyamplicol/licenses/ufo-model-loader.txt b/pyamplicol/licenses/ufo-model-loader.txt new file mode 100644 index 0000000000..7a3ea73b49 --- /dev/null +++ b/pyamplicol/licenses/ufo-model-loader.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2025 Valentin Hirschi + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/pyamplicol/pyproject.toml b/pyamplicol/pyproject.toml new file mode 100644 index 0000000000..ee39907fa0 --- /dev/null +++ b/pyamplicol/pyproject.toml @@ -0,0 +1,226 @@ +# SPDX-License-Identifier: 0BSD + +[build-system] +requires = [ + "maturin==1.15.0", + "packaging==26.3", +] +build-backend = "_pyamplicol_build" +backend-path = ["build_backend"] + +[project] +name = "pyamplicol" +dynamic = ["version"] +description = "Generator using current recursion to build fast color-ordered matrix-element evaluators" +readme = "README.md" +requires-python = ">=3.11" +license = "0BSD" +license-files = [ + "LICENSE", + "THIRD_PARTY_NOTICES.md", + "licenses/*", + "licenses/**/*", +] +authors = [ + { name = "Rikkert Frederix", email = "rikkert.frederix@thep.lu.se" }, + { name = "Valentin Hirschi", email = "valentin.hirschi@gmail.com" }, + { name = "Timea Vitos", email = "timea.vitos@thep.lu.se" }, +] +keywords = [ + "high-energy physics", + "matrix elements", + "UFO", + "Symbolica", +] +classifiers = [ + "Development Status :: 3 - Alpha", + "Intended Audience :: Science/Research", + "License :: OSI Approved :: Zero-Clause BSD (0BSD)", + "Operating System :: MacOS :: MacOS X", + "Operating System :: POSIX :: Linux", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", + "Programming Language :: Rust", + "Topic :: Scientific/Engineering :: Physics", + "Typing :: Typed", +] +dependencies = [ + "colorama==0.4.6", + "numpy==2.4.6", + "platformdirs==4.12.2", + "prettytable==3.18.0", + "progressbar2==4.6.0", + "six==1.17.0", + "symbolica==3.0.0", + "tomli-w==1.2.0", + "ufo-model-loader==1.0.0", +] + +[project.optional-dependencies] +test = [ + "build>=1.2,<2", + "jsonschema>=4.22,<5", + "maturin==1.15.0", + "mypy>=1.13,<2", + "pytest>=8.3,<9", + "ruff>=0.9,<1", + "twine>=6,<7", + "wheel>=0.45,<1", +] +docs = [ + "pypdf>=5,<6", +] +fft-profiling = [ + "matplotlib==3.11.2", + "reportlab==4.5.1", +] + +[project.scripts] +pyamplicol = "pyamplicol.cli:main" +rusticol-config = "rusticol_config:main" + +[project.urls] +Homepage = "https://github.com/MadGraphTeam/pyamplicol" +Documentation = "https://madgraph.org/pyamplicol/" +Issues = "https://github.com/MadGraphTeam/pyamplicol/issues" +Source = "https://github.com/MadGraphTeam/pyamplicol" + +[tool.maturin] +manifest-path = "rust/crates/rusticol-python/Cargo.toml" +python-source = "src" +python-packages = ["pyamplicol", "rusticol_config"] +module-name = "pyamplicol._rusticol" +bindings = "pyo3" +features = ["extension-module", "numpy"] +locked = true +strip = true +compatibility = "manylinux_2_28" +auditwheel = "repair" +include = [ + { path = "LICENSE", format = ["sdist"] }, + { path = "licenses/**/*", format = ["sdist"] }, + { path = "THIRD_PARTY_NOTICES.md", format = ["sdist"] }, + { path = "README.md", format = ["sdist"] }, + { path = "Cargo.lock", format = ["sdist"] }, + { path = "Cargo.toml", format = ["sdist"] }, + { path = "rust-toolchain.toml", format = ["sdist"] }, + { path = "justfile", format = ["sdist"] }, + { path = "build_backend/**/*", format = ["sdist"] }, + { path = "dependencies/release-lock.toml", format = ["sdist"] }, + { path = "docs/README.md", format = ["sdist"] }, + { path = "docs/correlators.md", format = ["sdist"] }, + { path = "docs/correlator-conventions.md", format = ["sdist"] }, + { path = "docs/user/**/*", format = ["sdist"] }, + { path = "examples/**/*", format = ["sdist"] }, + { path = "rust/**/*", format = ["sdist"] }, + { path = "schemas/**/*", format = ["sdist"] }, + { path = "source_hash.py", format = ["sdist"] }, + { path = "tests/**/*", format = ["sdist"] }, + { path = "tools/ci/__init__.py", format = ["sdist"] }, + { path = "tools/ci/memory_watchdog.py", format = ["sdist"] }, + { path = "tools/developer/**/*", format = ["sdist"] }, + { path = "tools/fft_profiling/**/*", format = ["sdist"] }, + { path = "tools/performance_report/**/*", format = ["sdist"] }, + { path = "tools/release/**/*", format = ["sdist"] }, + { path = "tools/typing/**/*", format = ["sdist"] }, + { path = "_sdk/**/*", from = "out-dir", to = "pyamplicol/", crate-name = "rusticol-python" }, +] +exclude = [ + "**/__pycache__/**", + "**/*.pyc", + "build_backend/python_lock.py", + "dependencies/checkouts/**", + "dependencies/contributor-lock.toml", + "dependencies/install_dependencies.py", + "dependencies/python-runtime-lock.toml", + ".pyamplicol-build-dependencies/**", + "flake.nix", + "outputs/**", + "PYPI_DEPLOYMENT_TEST/**", + "release_assets/**", + "target/**", +] + +[tool.maturin.sbom] +rust = false +auditwheel = false + +[tool.pyamplicol.native-build] +schema-version = 1 + +[tool.pyamplicol.native-build.rust-path-remapping] +candidate-checkouts = "/pyamplicol/dependencies" +checkout = "/pyamplicol/checkout" +source = "/pyamplicol/source" +build = "/pyamplicol/build" +sysroot = "/rust/sysroot" + +[tool.pyamplicol.native-build.macos] +cc = "/usr/bin/clang" +cxx = "/usr/bin/clang++" +deployment-target = "11.0" + +[tool.pyamplicol.native-build.sdk] +package = "rusticol-capi" +profile = "release" +# Fat LTO folds libstd into referenced static-library objects, which makes the +# archive impossible to link from a Rust consumer on ELF without duplicate symbols. +rustc-codegen-arguments = ["-C", "lto=off", "-C", "embed-bitcode=no"] + +[tool.pytest.ini_options] +addopts = "-ra --import-mode=importlib" +pythonpath = ["src"] +testpaths = ["tests"] + +[tool.ruff] +line-length = 88 +target-version = "py311" +exclude = [ + "src/pyamplicol/assets/models/ufo/**", +] + +[tool.ruff.lint] +select = ["E", "F", "I", "UP", "B", "SIM", "RUF"] + +[tool.mypy] +python_version = "3.11" +strict = true +mypy_path = "src" +explicit_package_bases = true +follow_imports = "silent" +files = [ + "src/pyamplicol/__init__.py", + "src/pyamplicol/api", + "src/pyamplicol/config/__init__.py", + "src/pyamplicol/config/errors.py", + "src/pyamplicol/config/models.py", + "src/pyamplicol/config/registry.py", + "src/pyamplicol/reporting", + "src/pyamplicol/runtime", + "tools/typing/check_public_typing.py", + "tests/typing/consumer_public_api.py", + "tests/typing/test_typing_gate.py", +] +exclude = [ + "^src/pyamplicol/assets/models/ufo/", + "^src/pyamplicol/assets/api_templates/", +] +show_error_codes = true +warn_unused_configs = true + +# Symbolica 3.0.0's FactorProjector overloads crash mypy 1.20.2. These +# implementation-only tensor bindings are exercised by numerical tests; +# keep checking all pyAmpliCol public types and the other Symbolica stubs. +[[tool.mypy.overrides]] +module = "symbolica.community.tensor" +follow_imports = "skip" +follow_imports_for_stubs = true + +[tool.coverage.run] +source = ["pyamplicol"] + +[tool.coverage.report] +show_missing = true diff --git a/pyamplicol/release_assets/prepared_models/README.md b/pyamplicol/release_assets/prepared_models/README.md new file mode 100644 index 0000000000..396ddc51e4 --- /dev/null +++ b/pyamplicol/release_assets/prepared_models/README.md @@ -0,0 +1,26 @@ + + +# Release Prepared-Model Store + +This source-only directory owns the architecture-specific portable JIT O2 +prepared-model pairs for both `built-in-sm` and `built-in-sm-heft`. It is +deliberately outside the Python package: contributor builds continue to use the +candidate assets under `src/pyamplicol/assets/prepared_models`. + +Generate the release pairs with the manual `release-prepared-models.yml` +workflow, then copy only these eight files from its architecture artifacts into +this directory: + +- `built-in-sm-heft-jit-o2-aarch64.metadata.json` +- `built-in-sm-heft-jit-o2-aarch64.pyamplicol-model` +- `built-in-sm-heft-jit-o2-x86_64.metadata.json` +- `built-in-sm-heft-jit-o2-x86_64.pyamplicol-model` +- `built-in-sm-jit-o2-aarch64.metadata.json` +- `built-in-sm-jit-o2-aarch64.pyamplicol-model` +- `built-in-sm-jit-o2-x86_64.metadata.json` +- `built-in-sm-jit-o2-x86_64.pyamplicol-model` + +Normal release builds fail until all eight files exist and match the active +release lock. The build overlay projects them into the canonical package asset +directory and removes this auxiliary store before creating a wheel or source +distribution. diff --git a/pyamplicol/run_fft_profiling_render.sh b/pyamplicol/run_fft_profiling_render.sh new file mode 100755 index 0000000000..78060834df --- /dev/null +++ b/pyamplicol/run_fft_profiling_render.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +set -euo pipefail + +if [[ -z "${IN_NIX_SHELL:-}" && -z "${PYAMPLICOL_FFT_NO_NIX:-}" ]]; then + exec nix develop --quiet --command "$0" "$@" +fi + +workspace_root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)" + +exec "$workspace_root/.venv/bin/python" \ + "$workspace_root/tools/fft_profiling/fft_profiling.py" \ + --output "$workspace_root/fft_profiling" \ + --render \ + "$@" diff --git a/pyamplicol/rust-toolchain.toml b/pyamplicol/rust-toolchain.toml new file mode 100644 index 0000000000..af72c57c0e --- /dev/null +++ b/pyamplicol/rust-toolchain.toml @@ -0,0 +1,6 @@ +# SPDX-License-Identifier: 0BSD + +[toolchain] +channel = "1.97.1" +profile = "minimal" +components = ["clippy", "rustfmt"] diff --git a/pyamplicol/rust/crates/rusticol-capi/Cargo.toml b/pyamplicol/rust/crates/rusticol-capi/Cargo.toml new file mode 100644 index 0000000000..a26b82216a --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-capi/Cargo.toml @@ -0,0 +1,19 @@ +# SPDX-License-Identifier: 0BSD + +[package] +name = "rusticol-capi" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +authors.workspace = true +repository.workspace = true +build = "build.rs" + +[lib] +name = "rusticol_capi" +crate-type = ["staticlib", "rlib"] + +[dependencies] +rusticol-core = { path = "../rusticol-core", default-features = false, features = ["f64-compiled", "f64-symjit"] } +serde_json.workspace = true diff --git a/pyamplicol/rust/crates/rusticol-capi/build.rs b/pyamplicol/rust/crates/rusticol-capi/build.rs new file mode 100644 index 0000000000..c79273d76a --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-capi/build.rs @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: 0BSD + +use std::env; +use std::fs; +use std::path::PathBuf; + +fn main() { + let output = PathBuf::from(env::var_os("OUT_DIR").expect("OUT_DIR")); + let profile = env::var("PROFILE").expect("PROFILE"); + let target = env::var("TARGET").expect("TARGET"); + let payload = format!( + "{{\n \"schema_version\": 1,\n \"target\": \"{target}\",\n \"profile\": \"{profile}\",\n \"native_static_libs\": []\n}}\n" + ); + fs::write(output.join("rusticol-link.json"), payload).expect("write link metadata"); +} diff --git a/pyamplicol/rust/crates/rusticol-capi/fortran/rusticol.f90 b/pyamplicol/rust/crates/rusticol-capi/fortran/rusticol.f90 new file mode 100644 index 0000000000..7967400c7f --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-capi/fortran/rusticol.f90 @@ -0,0 +1,1430 @@ +! SPDX-License-Identifier: 0BSD + +module rusticol + use, intrinsic :: iso_c_binding + use, intrinsic :: iso_fortran_env, only: error_unit + implicit none + private + + integer(c_int), parameter, public :: RUSTICOL_STATUS_OK = 0_c_int + integer(c_int), parameter, public :: RUSTICOL_STATUS_INVALID_ARGUMENT = 1_c_int + integer(c_int), parameter, public :: RUSTICOL_STATUS_BUFFER_TOO_SMALL = 2_c_int + integer(c_int), parameter, public :: RUSTICOL_STATUS_RUNTIME_ERROR = 3_c_int + integer(c_int), parameter, public :: RUSTICOL_STATUS_PANIC = 4_c_int + + integer(c_int32_t), parameter, public :: RUSTICOL_WARM_UP_EVENT_START = 0_c_int32_t + integer(c_int32_t), parameter, public :: RUSTICOL_WARM_UP_EVENT_UPDATE = 1_c_int32_t + integer(c_int32_t), parameter, public :: RUSTICOL_WARM_UP_EVENT_END = 2_c_int32_t + integer(c_int32_t), parameter, public :: RUSTICOL_WARM_UP_STAGE_PROCESS_PREPARATION = & + 0_c_int32_t + integer(c_int32_t), parameter, public :: RUSTICOL_WARM_UP_STAGE_QUERY_FAMILY = 1_c_int32_t + integer(c_int32_t), parameter, public :: RUSTICOL_WARM_UP_STAGE_FAMILY_FINALIZATION = & + 2_c_int32_t + integer(c_int32_t), parameter, public :: RUSTICOL_WARM_UP_STAGE_FIRST_EVALUATION = & + 3_c_int32_t + + type, bind(C), public :: rusticol_warm_up_progress_event + integer(c_int32_t) :: schema_version + integer(c_int32_t) :: kind + integer(c_int32_t) :: stage + integer(c_int32_t) :: reserved + integer(c_int64_t) :: completed + integer(c_int64_t) :: total + real(c_double) :: elapsed_seconds + integer(c_int64_t) :: current_rss_bytes + integer(c_int64_t) :: peak_rss_bytes + integer(c_int64_t) :: workers + integer(c_int32_t) :: current_rss_available + integer(c_int32_t) :: peak_rss_available + end type rusticol_warm_up_progress_event + + type, bind(C), public :: rusticol_warm_up_result + integer(c_int32_t) :: schema_version + integer(c_int32_t) :: reserved + real(c_double) :: elapsed_seconds + integer(c_int64_t) :: query_count + integer(c_int64_t) :: warmed_query_count + integer(c_int64_t) :: current_rss_bytes + integer(c_int64_t) :: peak_rss_bytes + integer(c_int32_t) :: current_rss_available + integer(c_int32_t) :: peak_rss_available + integer(c_int32_t) :: already_warm + integer(c_int32_t) :: first_evaluation_completed + end type rusticol_warm_up_result + + type, public :: rusticol_string + character(len=:), allocatable :: value + end type rusticol_string + + type, public :: rusticol_external_particle + integer(c_size_t) :: index = 0_c_size_t + integer(c_int32_t) :: pdg = 0_c_int32_t + end type rusticol_external_particle + + type, public :: rusticol_helicity_configuration + character(len=:), allocatable :: id + integer(c_int32_t), allocatable :: helicities(:) + end type rusticol_helicity_configuration + + type, public :: rusticol_color_component + character(len=:), allocatable :: id + character(len=:), allocatable :: kind + integer(c_size_t), allocatable :: word(:) + end type rusticol_color_component + + type, public :: rusticol_model_parameter + character(len=:), allocatable :: name + end type rusticol_model_parameter + + type, public :: rusticol_spin_correlation_vector + integer(c_size_t) :: leg = 0_c_size_t ! One-based public external leg. + ! (E,px,py,pz) by point; a single point broadcasts over the batch. + complex(c_double_complex), allocatable :: components(:, :) + end type rusticol_spin_correlation_vector + + type, public :: rusticol_correlated_request + character(len=:), allocatable :: color_correlation ! Unallocated means "born". + ! Unallocated inherits the setter; allocated size zero uses physical helicities. + type(rusticol_spin_correlation_vector), allocatable :: spin_vectors(:) + end type rusticol_correlated_request + + type, bind(C) :: c_spin_correlation_vector + integer(c_size_t) :: leg, point_count + type(c_ptr) :: components + integer(c_size_t) :: component_count + end type c_spin_correlation_vector + + type, bind(C) :: c_correlated_request + type(c_ptr) :: color_correlation, spin_vectors + integer(c_size_t) :: spin_vector_count + integer(c_int32_t) :: use_default_spin_vectors + end type c_correlated_request + + ! Explicit real/imaginary transport avoids assumptions about COMPLEX layout. + type :: spin_correlation_storage + real(c_double), allocatable :: components(:) + type(c_spin_correlation_vector), allocatable :: vectors(:) + character(kind=c_char), allocatable :: id(:) + end type spin_correlation_storage + + type, public :: rusticol_runtime + private + type(c_ptr) :: handle = c_null_ptr + contains + final :: rusticol_finalize + procedure, public :: load => rusticol_load + procedure, public :: save => rusticol_save + procedure, public :: load_cache => rusticol_load_cache + procedure, public :: close => rusticol_close + procedure, public :: is_loaded => rusticol_is_loaded + procedure, public :: process => rusticol_process + procedure, public :: process_key => rusticol_process_key + procedure, public :: representative_process_key => rusticol_representative_process_key + procedure, public :: color_accuracy => rusticol_color_accuracy + procedure, public :: execution_mode => rusticol_execution_mode + procedure, public :: metadata_json => rusticol_metadata_json + procedure, public :: physics_json => rusticol_physics_json + procedure, public :: external_particles => rusticol_external_particles + procedure, public :: external_permutation => rusticol_external_permutation + procedure, public :: load_kinematics_json => rusticol_load_kinematics_json + procedure, public :: helicities => rusticol_helicities + procedure, public :: colors => rusticol_colors + procedure, public :: model_parameters => rusticol_model_parameters + procedure, public :: evaluate => rusticol_evaluate + procedure, public :: color_correlation_ids => rusticol_color_correlation_ids + procedure, public :: color_correlation_catalogue_json => rusticol_color_correlation_catalogue_json + procedure, public :: set_spin_correlation_vectors => rusticol_set_spin_correlation_vectors + procedure, public :: evaluate_correlated_many => rusticol_evaluate_correlated_many + procedure, public :: evaluate_correlated => rusticol_evaluate_correlated + procedure, public :: warm_up => rusticol_warm_up + procedure, public :: evaluate_selected => rusticol_evaluate_selected + procedure, public :: evaluate_resolved => rusticol_evaluate_resolved + procedure, public :: set_model_parameter => rusticol_set_model_parameter + procedure, public :: set_model_parameters => rusticol_set_model_parameters + procedure, public :: set_model_parameters_json => rusticol_set_model_parameters_json + procedure, public :: mute_warnings => rusticol_mute_warnings + procedure, public :: unmute_warnings => rusticol_unmute_warnings + procedure, public :: take_warnings_json => rusticol_take_warnings_json + end type rusticol_runtime + + interface + function c_rusticol_abi_version() bind(C, name="rusticol_abi_version") result(version) + import :: c_int32_t + integer(c_int32_t) :: version + end function c_rusticol_abi_version + + function c_rusticol_last_error_message(buffer, capacity, required) & + bind(C, name="rusticol_last_error_message") result(status) + import :: c_ptr, c_size_t, c_int + type(c_ptr), value :: buffer + integer(c_size_t), value :: capacity + integer(c_size_t) :: required + integer(c_int) :: status + end function c_rusticol_last_error_message + + function c_rusticol_runtime_load(process_dir, process_key, model_parameters, output) & + bind(C, name="rusticol_runtime_load") result(status) + import :: c_ptr, c_int + type(c_ptr), value :: process_dir + type(c_ptr), value :: process_key + type(c_ptr), value :: model_parameters + type(c_ptr) :: output + integer(c_int) :: status + end function c_rusticol_runtime_load + + function c_rusticol_runtime_free(handle) bind(C, name="rusticol_runtime_free") result(status) + import :: c_ptr, c_int + type(c_ptr), value :: handle + integer(c_int) :: status + end function c_rusticol_runtime_free + + function c_rusticol_runtime_save(handle, path) bind(C, name="rusticol_runtime_save") result(status) + import :: c_ptr, c_int + type(c_ptr), value :: handle, path + integer(c_int) :: status + end function c_rusticol_runtime_save + + function c_rusticol_runtime_load_cache(handle, path) & + bind(C, name="rusticol_runtime_load_cache") result(status) + import :: c_ptr, c_int + type(c_ptr), value :: handle, path + integer(c_int) :: status + end function c_rusticol_runtime_load_cache + + function c_rusticol_runtime_metadata_json(handle, buffer, capacity, required) & + bind(C, name="rusticol_runtime_metadata_json") result(status) + import :: c_ptr, c_size_t, c_int + type(c_ptr), value :: handle, buffer + integer(c_size_t), value :: capacity + integer(c_size_t) :: required + integer(c_int) :: status + end function c_rusticol_runtime_metadata_json + + function c_rusticol_runtime_execution_mode(handle, buffer, capacity, required) & + bind(C, name="rusticol_runtime_execution_mode") result(status) + import :: c_ptr, c_size_t, c_int + type(c_ptr), value :: handle, buffer + integer(c_size_t), value :: capacity + integer(c_size_t) :: required + integer(c_int) :: status + end function c_rusticol_runtime_execution_mode + + function c_rusticol_runtime_physics_json(handle, buffer, capacity, required) & + bind(C, name="rusticol_runtime_physics_json") result(status) + import :: c_ptr, c_size_t, c_int + type(c_ptr), value :: handle, buffer + integer(c_size_t), value :: capacity + integer(c_size_t) :: required + integer(c_int) :: status + end function c_rusticol_runtime_physics_json + + function c_rusticol_runtime_process(handle, buffer, capacity, required) & + bind(C, name="rusticol_runtime_process") result(status) + import :: c_ptr, c_size_t, c_int + type(c_ptr), value :: handle, buffer + integer(c_size_t), value :: capacity + integer(c_size_t) :: required + integer(c_int) :: status + end function c_rusticol_runtime_process + + function c_rusticol_runtime_process_key(handle, buffer, capacity, required) & + bind(C, name="rusticol_runtime_process_key") result(status) + import :: c_ptr, c_size_t, c_int + type(c_ptr), value :: handle, buffer + integer(c_size_t), value :: capacity + integer(c_size_t) :: required + integer(c_int) :: status + end function c_rusticol_runtime_process_key + + function c_rusticol_runtime_representative_process_key(handle, buffer, capacity, required) & + bind(C, name="rusticol_runtime_representative_process_key") result(status) + import :: c_ptr, c_size_t, c_int + type(c_ptr), value :: handle, buffer + integer(c_size_t), value :: capacity + integer(c_size_t) :: required + integer(c_int) :: status + end function c_rusticol_runtime_representative_process_key + + function c_rusticol_runtime_color_accuracy(handle, buffer, capacity, required) & + bind(C, name="rusticol_runtime_color_accuracy") result(status) + import :: c_ptr, c_size_t, c_int + type(c_ptr), value :: handle, buffer + integer(c_size_t), value :: capacity + integer(c_size_t) :: required + integer(c_int) :: status + end function c_rusticol_runtime_color_accuracy + + function c_rusticol_runtime_external_count(handle, output) & + bind(C, name="rusticol_runtime_external_count") result(status) + import :: c_ptr, c_size_t, c_int + type(c_ptr), value :: handle + integer(c_size_t) :: output + integer(c_int) :: status + end function c_rusticol_runtime_external_count + + function c_rusticol_runtime_external_pdg(handle, index, output) & + bind(C, name="rusticol_runtime_external_pdg") result(status) + import :: c_ptr, c_size_t, c_int32_t, c_int + type(c_ptr), value :: handle + integer(c_size_t), value :: index + integer(c_int32_t) :: output + integer(c_int) :: status + end function c_rusticol_runtime_external_pdg + + function c_rusticol_runtime_external_permutation(handle, output, capacity, required) & + bind(C, name="rusticol_runtime_external_permutation") result(status) + import :: c_ptr, c_size_t, c_int + type(c_ptr), value :: handle, output + integer(c_size_t), value :: capacity + integer(c_size_t) :: required + integer(c_int) :: status + end function c_rusticol_runtime_external_permutation + + function c_rusticol_runtime_load_kinematics_json(handle, path, output, capacity, required) & + bind(C, name="rusticol_runtime_load_kinematics_json") result(status) + import :: c_ptr, c_size_t, c_int + type(c_ptr), value :: handle, path, output + integer(c_size_t), value :: capacity + integer(c_size_t) :: required + integer(c_int) :: status + end function c_rusticol_runtime_load_kinematics_json + + function c_rusticol_runtime_helicity_count(handle, output) & + bind(C, name="rusticol_runtime_helicity_count") result(status) + import :: c_ptr, c_size_t, c_int + type(c_ptr), value :: handle + integer(c_size_t) :: output + integer(c_int) :: status + end function c_rusticol_runtime_helicity_count + + function c_rusticol_runtime_helicity_id(handle, index, buffer, capacity, required) & + bind(C, name="rusticol_runtime_helicity_id") result(status) + import :: c_ptr, c_size_t, c_int + type(c_ptr), value :: handle, buffer + integer(c_size_t), value :: index, capacity + integer(c_size_t) :: required + integer(c_int) :: status + end function c_rusticol_runtime_helicity_id + + function c_rusticol_runtime_helicity_vector(handle, index, output, capacity, required) & + bind(C, name="rusticol_runtime_helicity_vector") result(status) + import :: c_ptr, c_size_t, c_int + type(c_ptr), value :: handle, output + integer(c_size_t), value :: index, capacity + integer(c_size_t) :: required + integer(c_int) :: status + end function c_rusticol_runtime_helicity_vector + + function c_rusticol_runtime_color_count(handle, output) & + bind(C, name="rusticol_runtime_color_count") result(status) + import :: c_ptr, c_size_t, c_int + type(c_ptr), value :: handle + integer(c_size_t) :: output + integer(c_int) :: status + end function c_rusticol_runtime_color_count + + function c_rusticol_runtime_color_id(handle, index, buffer, capacity, required) & + bind(C, name="rusticol_runtime_color_id") result(status) + import :: c_ptr, c_size_t, c_int + type(c_ptr), value :: handle, buffer + integer(c_size_t), value :: index, capacity + integer(c_size_t) :: required + integer(c_int) :: status + end function c_rusticol_runtime_color_id + + function c_rusticol_runtime_color_kind(handle, index, buffer, capacity, required) & + bind(C, name="rusticol_runtime_color_kind") result(status) + import :: c_ptr, c_size_t, c_int + type(c_ptr), value :: handle, buffer + integer(c_size_t), value :: index, capacity + integer(c_size_t) :: required + integer(c_int) :: status + end function c_rusticol_runtime_color_kind + + function c_rusticol_runtime_color_word(handle, index, output, capacity, required) & + bind(C, name="rusticol_runtime_color_word") result(status) + import :: c_ptr, c_size_t, c_int + type(c_ptr), value :: handle, output + integer(c_size_t), value :: index, capacity + integer(c_size_t) :: required + integer(c_int) :: status + end function c_rusticol_runtime_color_word + + function c_rusticol_runtime_model_parameter_count(handle, output) & + bind(C, name="rusticol_runtime_model_parameter_count") result(status) + import :: c_ptr, c_size_t, c_int + type(c_ptr), value :: handle + integer(c_size_t) :: output + integer(c_int) :: status + end function c_rusticol_runtime_model_parameter_count + + function c_rusticol_runtime_model_parameter_name(handle, index, buffer, capacity, required) & + bind(C, name="rusticol_runtime_model_parameter_name") result(status) + import :: c_ptr, c_size_t, c_int + type(c_ptr), value :: handle, buffer + integer(c_size_t), value :: index, capacity + integer(c_size_t) :: required + integer(c_int) :: status + end function c_rusticol_runtime_model_parameter_name + + function c_rusticol_runtime_resolved_shape(handle, helicity_ids, helicity_count, & + color_ids, color_count, output_helicity_count, output_color_count) & + bind(C, name="rusticol_runtime_resolved_shape") result(status) + import :: c_ptr, c_size_t, c_int + type(c_ptr), value :: handle, helicity_ids, color_ids + integer(c_size_t), value :: helicity_count, color_count + integer(c_size_t) :: output_helicity_count, output_color_count + integer(c_int) :: status + end function c_rusticol_runtime_resolved_shape + + function c_rusticol_runtime_warm_up_f64_with_cores(handle, momenta, momentum_count, & + helicity_ids, helicity_count, color_ids, color_count, n_cores, & + progress_callback, progress_user_data, output) & + bind(C, name="rusticol_runtime_warm_up_f64_with_cores") result(status) + import :: c_ptr, c_funptr, c_size_t, c_int, rusticol_warm_up_result + type(c_ptr), value :: handle, momenta, helicity_ids, color_ids + integer(c_size_t), value :: momentum_count, helicity_count, color_count, n_cores + type(c_funptr), value :: progress_callback + type(c_ptr), value :: progress_user_data + type(rusticol_warm_up_result) :: output + integer(c_int) :: status + end function c_rusticol_runtime_warm_up_f64_with_cores + + function c_rusticol_color_correlation_count(handle, output) & + bind(C, name="rusticol_runtime_color_correlation_count") result(status) + import :: c_ptr, c_size_t, c_int + type(c_ptr), value :: handle + integer(c_size_t) :: output + integer(c_int) :: status + end function c_rusticol_color_correlation_count + + function c_rusticol_color_correlation_id(handle, index, buffer, capacity, required) & + bind(C, name="rusticol_runtime_color_correlation_id") result(status) + import :: c_ptr, c_size_t, c_int + type(c_ptr), value :: handle, buffer + integer(c_size_t), value :: index, capacity + integer(c_size_t) :: required + integer(c_int) :: status + end function c_rusticol_color_correlation_id + + function c_rusticol_color_correlation_catalogue_json(handle, buffer, capacity, required) & + bind(C, name="rusticol_runtime_color_correlation_catalogue_json") result(status) + import :: c_ptr, c_size_t, c_int + type(c_ptr), value :: handle, buffer + integer(c_size_t), value :: capacity + integer(c_size_t) :: required + integer(c_int) :: status + end function c_rusticol_color_correlation_catalogue_json + + function c_rusticol_set_spin_correlation_vectors(handle, vectors, vector_count) & + bind(C, name="rusticol_runtime_set_spin_correlation_vectors_f64") result(status) + import :: c_ptr, c_size_t, c_int + type(c_ptr), value :: handle, vectors + integer(c_size_t), value :: vector_count + integer(c_int) :: status + end function c_rusticol_set_spin_correlation_vectors + + function c_rusticol_evaluate_correlated_many(handle, momenta, momentum_count, point_count, & + requests, request_count, helicity_ids, helicity_count, output, output_capacity) & + bind(C, name="rusticol_runtime_evaluate_correlated_many_f64") result(status) + import :: c_ptr, c_size_t, c_int + type(c_ptr), value :: handle, momenta, requests, helicity_ids, output + integer(c_size_t), value :: momentum_count, point_count, request_count + integer(c_size_t), value :: helicity_count, output_capacity + integer(c_int) :: status + end function c_rusticol_evaluate_correlated_many + + function c_rusticol_runtime_evaluate_f64(handle, momenta, momentum_count, point_count, & + output, output_capacity) bind(C, name="rusticol_runtime_evaluate_f64") result(status) + import :: c_ptr, c_size_t, c_int + type(c_ptr), value :: handle, momenta, output + integer(c_size_t), value :: momentum_count, point_count, output_capacity + integer(c_int) :: status + end function c_rusticol_runtime_evaluate_f64 + + function c_rusticol_runtime_evaluate_selected_f64(handle, momenta, momentum_count, & + point_count, helicity_ids, helicity_count, color_ids, color_count, & + helicity_by_point, helicity_by_point_count, color_flow_by_point, & + color_flow_by_point_count, & + output, output_capacity) bind(C, name="rusticol_runtime_evaluate_selected_f64") & + result(status) + import :: c_ptr, c_size_t, c_int + type(c_ptr), value :: handle, momenta, helicity_ids, color_ids + type(c_ptr), value :: helicity_by_point, color_flow_by_point, output + integer(c_size_t), value :: momentum_count, point_count, helicity_count, color_count + integer(c_size_t), value :: helicity_by_point_count, color_flow_by_point_count + integer(c_size_t), value :: output_capacity + integer(c_int) :: status + end function c_rusticol_runtime_evaluate_selected_f64 + + function c_rusticol_runtime_evaluate_resolved_f64(handle, momenta, momentum_count, & + point_count, helicity_ids, helicity_count, color_ids, color_count, output, & + output_capacity, output_helicity_count, output_color_count) & + bind(C, name="rusticol_runtime_evaluate_resolved_f64") result(status) + import :: c_ptr, c_size_t, c_int + type(c_ptr), value :: handle, momenta, helicity_ids, color_ids, output + integer(c_size_t), value :: momentum_count, point_count, helicity_count, color_count + integer(c_size_t), value :: output_capacity + integer(c_size_t) :: output_helicity_count, output_color_count + integer(c_int) :: status + end function c_rusticol_runtime_evaluate_resolved_f64 + + function c_rusticol_runtime_set_model_parameter(handle, name, real, imaginary) & + bind(C, name="rusticol_runtime_set_model_parameter") result(status) + import :: c_ptr, c_double, c_int + type(c_ptr), value :: handle, name + real(c_double), value :: real, imaginary + integer(c_int) :: status + end function c_rusticol_runtime_set_model_parameter + + function c_rusticol_runtime_set_model_parameters(handle, names, real, imaginary, count) & + bind(C, name="rusticol_runtime_set_model_parameters") result(status) + import :: c_ptr, c_size_t, c_int + type(c_ptr), value :: handle, names, real, imaginary + integer(c_size_t), value :: count + integer(c_int) :: status + end function c_rusticol_runtime_set_model_parameters + + function c_rusticol_runtime_set_model_parameters_json(handle, path) & + bind(C, name="rusticol_runtime_set_model_parameters_json") result(status) + import :: c_ptr, c_int + type(c_ptr), value :: handle, path + integer(c_int) :: status + end function c_rusticol_runtime_set_model_parameters_json + + function c_rusticol_runtime_mute_warnings(handle, muted) & + bind(C, name="rusticol_runtime_mute_warnings") result(status) + import :: c_ptr, c_int + type(c_ptr), value :: handle + integer(c_int), value :: muted + integer(c_int) :: status + end function c_rusticol_runtime_mute_warnings + + function c_rusticol_runtime_take_warnings_json(handle, buffer, capacity, required) & + bind(C, name="rusticol_runtime_take_warnings_json") result(status) + import :: c_ptr, c_size_t, c_int + type(c_ptr), value :: handle, buffer + integer(c_size_t), value :: capacity + integer(c_size_t) :: required + integer(c_int) :: status + end function c_rusticol_runtime_take_warnings_json + end interface + + abstract interface + function rusticol_warm_up_progress_callback(event, user_data) bind(C) result(keep_going) + import :: rusticol_warm_up_progress_event, c_ptr, c_int + type(rusticol_warm_up_progress_event), intent(in) :: event + type(c_ptr), value :: user_data + integer(c_int) :: keep_going + end function rusticol_warm_up_progress_callback + + function c_runtime_string_function(handle, buffer, capacity, required) result(status) + import :: c_ptr, c_size_t, c_int + type(c_ptr), value :: handle, buffer + integer(c_size_t), value :: capacity + integer(c_size_t) :: required + integer(c_int) :: status + end function c_runtime_string_function + + function c_runtime_indexed_string_function(handle, index, buffer, capacity, required) & + result(status) + import :: c_ptr, c_size_t, c_int + type(c_ptr), value :: handle, buffer + integer(c_size_t), value :: index, capacity + integer(c_size_t) :: required + integer(c_int) :: status + end function c_runtime_indexed_string_function + end interface + + public :: rusticol_abi_version, rusticol_last_error, rusticol_warm_up_progress_callback + +contains + + function rusticol_abi_version() result(version) + integer(c_int32_t) :: version + version = c_rusticol_abi_version() + end function rusticol_abi_version + + subroutine build_c_string(value, buffer) + character(len=*), intent(in) :: value + character(kind=c_char), allocatable, target, intent(out) :: buffer(:) + integer :: index, length + + length = len_trim(value) + allocate(buffer(length + 1)) + do index = 1, length + buffer(index) = value(index:index) + end do + buffer(length + 1) = c_null_char + end subroutine build_c_string + + function from_c_string(buffer) result(value) + character(kind=c_char), intent(in) :: buffer(:) + character(len=:), allocatable :: value + integer :: index, length + + length = 0 + do index = 1, size(buffer) + if (buffer(index) == c_null_char) exit + length = length + 1 + end do + allocate(character(len=length) :: value) + do index = 1, length + value(index:index) = buffer(index) + end do + end function from_c_string + + function rusticol_last_error() result(message) + character(len=:), allocatable :: message + character(kind=c_char), allocatable, target :: buffer(:) + integer(c_size_t) :: required + integer(c_int) :: status + + required = 0_c_size_t + status = c_rusticol_last_error_message(c_null_ptr, 0_c_size_t, required) + if (status /= RUSTICOL_STATUS_OK .or. required == 0_c_size_t) then + message = "unknown Rusticol error" + return + end if + allocate(buffer(required)) + status = c_rusticol_last_error_message(c_loc(buffer(1)), required, required) + if (status /= RUSTICOL_STATUS_OK) then + message = "could not retrieve Rusticol error" + else + message = from_c_string(buffer) + end if + end function rusticol_last_error + + logical function status_ok(status, ierr) + integer(c_int), intent(in) :: status + integer(c_int), intent(out), optional :: ierr + + if (present(ierr)) ierr = status + if (status /= RUSTICOL_STATUS_OK .and. .not. present(ierr)) then + write(error_unit, '(A)') rusticol_last_error() + error stop 1 + end if + status_ok = status == RUSTICOL_STATUS_OK + end function status_ok + + logical function argument_ok(condition, message, ierr) + logical, intent(in) :: condition + character(len=*), intent(in) :: message + integer(c_int), intent(out), optional :: ierr + + argument_ok = condition + if (condition) return + if (present(ierr)) then + ierr = RUSTICOL_STATUS_INVALID_ARGUMENT + else + write(error_unit, '(A)') message + error stop 1 + end if + end function argument_ok + + subroutine rusticol_load(self, process_dir, process_key, model_parameters, ierr) + class(rusticol_runtime), intent(inout) :: self + character(len=*), intent(in) :: process_dir + character(len=*), intent(in), optional :: process_key, model_parameters + integer(c_int), intent(out), optional :: ierr + character(kind=c_char), allocatable, target :: dir_c(:), key_c(:), model_c(:) + type(c_ptr) :: key_ptr, model_ptr + integer(c_int) :: status + + call self%close() + call build_c_string(process_dir, dir_c) + key_ptr = c_null_ptr + if (present(process_key)) then + if (len_trim(process_key) > 0) then + call build_c_string(process_key, key_c) + key_ptr = c_loc(key_c(1)) + end if + end if + model_ptr = c_null_ptr + if (present(model_parameters)) then + if (len_trim(model_parameters) > 0) then + call build_c_string(model_parameters, model_c) + model_ptr = c_loc(model_c(1)) + end if + end if + status = c_rusticol_runtime_load(c_loc(dir_c(1)), key_ptr, model_ptr, self%handle) + if (.not. status_ok(status, ierr)) self%handle = c_null_ptr + end subroutine rusticol_load + + ! Save completed OTF cache entries; the generated process output is still required. + subroutine rusticol_save(self, path, ierr) + class(rusticol_runtime), intent(in) :: self + character(len=*), intent(in) :: path + integer(c_int), intent(out), optional :: ierr + character(kind=c_char), allocatable, target :: path_c(:) + integer(c_int) :: status + + if (.not. argument_ok(index(path, achar(0)) == 0, "cache path contains a NUL byte", ierr)) return + call build_c_string(path, path_c) + status = c_rusticol_runtime_save(self%handle, c_loc(path_c(1))) + if (.not. status_ok(status, ierr)) return + end subroutine rusticol_save + + ! Load a saved OTF cache into an already-loaded matching process output. + subroutine rusticol_load_cache(self, path, ierr) + class(rusticol_runtime), intent(inout) :: self + character(len=*), intent(in) :: path + integer(c_int), intent(out), optional :: ierr + character(kind=c_char), allocatable, target :: path_c(:) + integer(c_int) :: status + + if (.not. argument_ok(index(path, achar(0)) == 0, "cache path contains a NUL byte", ierr)) return + call build_c_string(path, path_c) + status = c_rusticol_runtime_load_cache(self%handle, c_loc(path_c(1))) + if (.not. status_ok(status, ierr)) return + end subroutine rusticol_load_cache + + subroutine rusticol_close(self) + class(rusticol_runtime), intent(inout) :: self + integer(c_int) :: status + if (c_associated(self%handle)) then + status = c_rusticol_runtime_free(self%handle) + self%handle = c_null_ptr + end if + end subroutine rusticol_close + + subroutine rusticol_finalize(self) + type(rusticol_runtime), intent(inout) :: self + call self%close() + end subroutine rusticol_finalize + + logical function rusticol_is_loaded(self) + class(rusticol_runtime), intent(in) :: self + rusticol_is_loaded = c_associated(self%handle) + end function rusticol_is_loaded + + function runtime_string(self, function, ierr) result(value) + class(rusticol_runtime), intent(in) :: self + procedure(c_runtime_string_function) :: function + integer(c_int), intent(out), optional :: ierr + character(len=:), allocatable :: value + character(kind=c_char), allocatable, target :: buffer(:) + integer(c_size_t) :: required + integer(c_int) :: status + + required = 0_c_size_t + status = function(self%handle, c_null_ptr, 0_c_size_t, required) + if (.not. status_ok(status, ierr)) then + value = "" + return + end if + allocate(buffer(required)) + status = function(self%handle, c_loc(buffer(1)), required, required) + if (.not. status_ok(status, ierr)) then + value = "" + return + end if + value = from_c_string(buffer) + end function runtime_string + + function indexed_runtime_string(self, index, function, ierr) result(value) + class(rusticol_runtime), intent(in) :: self + integer(c_size_t), intent(in) :: index + procedure(c_runtime_indexed_string_function) :: function + integer(c_int), intent(out), optional :: ierr + character(len=:), allocatable :: value + character(kind=c_char), allocatable, target :: buffer(:) + integer(c_size_t) :: required + integer(c_int) :: status + + required = 0_c_size_t + status = function(self%handle, index, c_null_ptr, 0_c_size_t, required) + if (.not. status_ok(status, ierr)) then + value = "" + return + end if + allocate(buffer(required)) + status = function(self%handle, index, c_loc(buffer(1)), required, required) + if (.not. status_ok(status, ierr)) then + value = "" + return + end if + value = from_c_string(buffer) + end function indexed_runtime_string + + function rusticol_process(self, ierr) result(value) + class(rusticol_runtime), intent(in) :: self + integer(c_int), intent(out), optional :: ierr + character(len=:), allocatable :: value + value = runtime_string(self, c_rusticol_runtime_process, ierr) + end function rusticol_process + + function rusticol_process_key(self, ierr) result(value) + class(rusticol_runtime), intent(in) :: self + integer(c_int), intent(out), optional :: ierr + character(len=:), allocatable :: value + value = runtime_string(self, c_rusticol_runtime_process_key, ierr) + end function rusticol_process_key + + function rusticol_representative_process_key(self, ierr) result(value) + class(rusticol_runtime), intent(in) :: self + integer(c_int), intent(out), optional :: ierr + character(len=:), allocatable :: value + value = runtime_string(self, c_rusticol_runtime_representative_process_key, ierr) + end function rusticol_representative_process_key + + function rusticol_color_accuracy(self, ierr) result(value) + class(rusticol_runtime), intent(in) :: self + integer(c_int), intent(out), optional :: ierr + character(len=:), allocatable :: value + value = runtime_string(self, c_rusticol_runtime_color_accuracy, ierr) + end function rusticol_color_accuracy + + function rusticol_execution_mode(self, ierr) result(value) + class(rusticol_runtime), intent(in) :: self + integer(c_int), intent(out), optional :: ierr + character(len=:), allocatable :: value + value = runtime_string(self, c_rusticol_runtime_execution_mode, ierr) + end function rusticol_execution_mode + + function rusticol_metadata_json(self, ierr) result(value) + class(rusticol_runtime), intent(in) :: self + integer(c_int), intent(out), optional :: ierr + character(len=:), allocatable :: value + value = runtime_string(self, c_rusticol_runtime_metadata_json, ierr) + end function rusticol_metadata_json + + function rusticol_physics_json(self, ierr) result(value) + class(rusticol_runtime), intent(in) :: self + integer(c_int), intent(out), optional :: ierr + character(len=:), allocatable :: value + value = runtime_string(self, c_rusticol_runtime_physics_json, ierr) + end function rusticol_physics_json + + function rusticol_external_particles(self, ierr) result(particles) + class(rusticol_runtime), intent(in) :: self + integer(c_int), intent(out), optional :: ierr + type(rusticol_external_particle), allocatable :: particles(:) + integer(c_size_t) :: count, index + integer(c_int) :: status + + status = c_rusticol_runtime_external_count(self%handle, count) + if (.not. status_ok(status, ierr)) then + allocate(particles(0)) + return + end if + allocate(particles(count)) + do index = 1, count + particles(index)%index = index - 1 + status = c_rusticol_runtime_external_pdg(self%handle, index - 1, particles(index)%pdg) + if (.not. status_ok(status, ierr)) return + end do + end function rusticol_external_particles + + function rusticol_external_permutation(self, ierr) result(permutation) + class(rusticol_runtime), intent(in) :: self + integer(c_int), intent(out), optional :: ierr + integer(c_size_t), allocatable, target :: permutation(:) + integer(c_size_t) :: required + integer(c_int) :: status + + required = 0_c_size_t + status = c_rusticol_runtime_external_permutation( & + self%handle, c_null_ptr, 0_c_size_t, required) + if (.not. status_ok(status, ierr)) then + allocate(permutation(0)) + return + end if + allocate(permutation(required)) + if (required > 0) then + status = c_rusticol_runtime_external_permutation( & + self%handle, c_loc(permutation(1)), required, required) + if (.not. status_ok(status, ierr)) permutation = 0_c_size_t + end if + end function rusticol_external_permutation + + subroutine rusticol_load_kinematics_json(self, path, momenta, ierr) + class(rusticol_runtime), intent(in) :: self + character(len=*), intent(in) :: path + real(c_double), allocatable, target, intent(out) :: momenta(:) + integer(c_int), intent(out), optional :: ierr + character(kind=c_char), allocatable, target :: path_c(:) + integer(c_size_t) :: required + integer(c_int) :: status + + call build_c_string(path, path_c) + required = 0_c_size_t + status = c_rusticol_runtime_load_kinematics_json( & + self%handle, c_loc(path_c(1)), c_null_ptr, 0_c_size_t, required) + if (.not. status_ok(status, ierr)) then + allocate(momenta(0)) + return + end if + allocate(momenta(required)) + if (required > 0) then + status = c_rusticol_runtime_load_kinematics_json( & + self%handle, c_loc(path_c(1)), c_loc(momenta(1)), required, required) + if (.not. status_ok(status, ierr)) momenta = 0.0_c_double + end if + end subroutine rusticol_load_kinematics_json + + function rusticol_helicities(self, ierr) result(items) + class(rusticol_runtime), intent(in) :: self + integer(c_int), intent(out), optional :: ierr + type(rusticol_helicity_configuration), allocatable :: items(:) + integer(c_int32_t), allocatable, target :: helicity_values(:) + integer(c_size_t) :: count, index, required + integer(c_int) :: status + + status = c_rusticol_runtime_helicity_count(self%handle, count) + if (.not. status_ok(status, ierr)) then + allocate(items(0)) + return + end if + allocate(items(count)) + do index = 1, count + items(index)%id = indexed_runtime_string( & + self, index - 1, c_rusticol_runtime_helicity_id, ierr) + required = 0_c_size_t + status = c_rusticol_runtime_helicity_vector( & + self%handle, index - 1, c_null_ptr, 0_c_size_t, required) + if (.not. status_ok(status, ierr)) return + allocate(helicity_values(required)) + status = c_rusticol_runtime_helicity_vector( & + self%handle, index - 1, c_loc(helicity_values(1)), required, required) + if (.not. status_ok(status, ierr)) return + call move_alloc(helicity_values, items(index)%helicities) + end do + end function rusticol_helicities + + function rusticol_colors(self, ierr) result(items) + class(rusticol_runtime), intent(in) :: self + integer(c_int), intent(out), optional :: ierr + type(rusticol_color_component), allocatable :: items(:) + integer(c_size_t), allocatable, target :: word_values(:) + integer(c_size_t) :: count, index, required + integer(c_int) :: status + + status = c_rusticol_runtime_color_count(self%handle, count) + if (.not. status_ok(status, ierr)) then + allocate(items(0)) + return + end if + allocate(items(count)) + do index = 1, count + items(index)%id = indexed_runtime_string(self, index - 1, c_rusticol_runtime_color_id, ierr) + items(index)%kind = indexed_runtime_string( & + self, index - 1, c_rusticol_runtime_color_kind, ierr) + required = 0_c_size_t + status = c_rusticol_runtime_color_word( & + self%handle, index - 1, c_null_ptr, 0_c_size_t, required) + if (.not. status_ok(status, ierr)) return + allocate(word_values(required)) + if (required > 0) then + status = c_rusticol_runtime_color_word( & + self%handle, index - 1, c_loc(word_values(1)), required, required) + if (.not. status_ok(status, ierr)) return + end if + call move_alloc(word_values, items(index)%word) + end do + end function rusticol_colors + + function rusticol_model_parameters(self, ierr) result(items) + class(rusticol_runtime), intent(in) :: self + integer(c_int), intent(out), optional :: ierr + type(rusticol_model_parameter), allocatable :: items(:) + integer(c_size_t) :: count, index + integer(c_int) :: status + + status = c_rusticol_runtime_model_parameter_count(self%handle, count) + if (.not. status_ok(status, ierr)) then + allocate(items(0)) + return + end if + allocate(items(count)) + do index = 1, count + items(index)%name = indexed_runtime_string( & + self, index - 1, c_rusticol_runtime_model_parameter_name, ierr) + end do + end function rusticol_model_parameters + + subroutine build_c_string_array(values, storage, pointers) + character(len=*), intent(in), optional :: values(:) + character(kind=c_char), allocatable, target, intent(out) :: storage(:, :) + type(c_ptr), allocatable, target, intent(out) :: pointers(:) + integer :: count, index, character_index, max_length + + if (.not. present(values)) then + allocate(storage(1, 0), pointers(0)) + return + end if + count = size(values) + if (count == 0) then + allocate(storage(1, 0), pointers(0)) + return + end if + max_length = max(1, maxval(len_trim(values))) + allocate(storage(max_length + 1, count), pointers(count)) + storage = c_null_char + do index = 1, count + do character_index = 1, len_trim(values(index)) + storage(character_index, index) = values(index)(character_index:character_index) + end do + pointers(index) = c_loc(storage(1, index)) + end do + end subroutine build_c_string_array + + function rusticol_color_correlation_catalogue_json(self, ierr) result(value) + class(rusticol_runtime), intent(inout) :: self + integer(c_int), intent(out), optional :: ierr + character(len=:), allocatable :: value + value = runtime_string(self, c_rusticol_color_correlation_catalogue_json, ierr) + end function rusticol_color_correlation_catalogue_json + + function rusticol_color_correlation_ids(self, ierr) result(items) + class(rusticol_runtime), intent(inout) :: self + integer(c_int), intent(out), optional :: ierr + type(rusticol_string), allocatable :: items(:) + integer(c_size_t) :: count, index + integer(c_int) :: status + + status = c_rusticol_color_correlation_count(self%handle, count) + if (.not. status_ok(status, ierr)) then + allocate(items(0)) + return + end if + allocate(items(count)) + do index = 1, count + items(index)%value = indexed_runtime_string( & + self, index - 1, c_rusticol_color_correlation_id, status) + if (.not. status_ok(status, ierr)) return + end do + end function rusticol_color_correlation_ids + + logical function build_spin_storage(input, storage, ierr) result(ok) + type(rusticol_spin_correlation_vector), intent(in) :: input(:) + type(spin_correlation_storage), intent(out), target :: storage + integer(c_int), intent(out), optional :: ierr + integer(c_size_t) :: total, count, offset, first, item, point, component + + ok = .false. + total = 0_c_size_t + do item = 1, size(input, kind=c_size_t) + if (.not. argument_ok(allocated(input(item)%components), & + "Each spin vector requires allocated components(4,points)", ierr)) return + if (.not. argument_ok(size(input(item)%components, 1) == 4 .and. & + size(input(item)%components, 2) > 0, & + "Each spin vector requires components(4,points) with at least one point", ierr)) return + count = size(input(item)%components, 2, kind=c_size_t) + if (.not. argument_ok(count <= (huge(total) - total) / 8_c_size_t, & + "Spin-vector component count overflows", ierr)) return + total = total + 8_c_size_t * count + end do + allocate(storage%components(total), storage%vectors(size(input))) + offset = 1_c_size_t + do item = 1, size(input, kind=c_size_t) + first = offset + count = size(input(item)%components, 2, kind=c_size_t) + do point = 1, count + do component = 1, 4 + storage%components(offset) = real(input(item)%components(component, point), c_double) + storage%components(offset + 1) = aimag(input(item)%components(component, point)) + offset = offset + 2 + end do + end do + storage%vectors(item) = c_spin_correlation_vector(input(item)%leg, count, & + c_loc(storage%components(first)), 8_c_size_t * count) + end do + if (present(ierr)) ierr = RUSTICOL_STATUS_OK + ok = .true. + end function build_spin_storage + + subroutine rusticol_set_spin_correlation_vectors(self, vectors, ierr) + class(rusticol_runtime), intent(inout) :: self + type(rusticol_spin_correlation_vector), intent(in), optional :: vectors(:) + integer(c_int), intent(out), optional :: ierr + type(spin_correlation_storage), target :: storage + type(c_ptr) :: vector_pointer + integer(c_size_t) :: count + integer(c_int) :: status + + vector_pointer = c_null_ptr + count = 0_c_size_t + if (present(vectors)) then + if (.not. build_spin_storage(vectors, storage, ierr)) return + count = size(storage%vectors, kind=c_size_t) + if (count > 0) vector_pointer = c_loc(storage%vectors(1)) + end if + status = c_rusticol_set_spin_correlation_vectors(self%handle, vector_pointer, count) + if (.not. status_ok(status, ierr)) return + end subroutine rusticol_set_spin_correlation_vectors + + subroutine rusticol_evaluate_correlated_many(self, momenta, point_count, requests, values, & + helicity_ids, ierr) + class(rusticol_runtime), intent(inout) :: self + real(c_double), intent(in), contiguous, target :: momenta(:) + integer(c_size_t), intent(in) :: point_count + type(rusticol_correlated_request), intent(in) :: requests(:) + ! values(point,request): Fortran ordering matches request-major C output. + complex(c_double_complex), allocatable, intent(out) :: values(:, :) + character(len=*), intent(in), optional :: helicity_ids(:) + integer(c_int), intent(out), optional :: ierr + type(spin_correlation_storage), allocatable, target :: storage(:) + type(c_correlated_request), allocatable, target :: raw_requests(:) + character(kind=c_char), allocatable, target :: helicity_storage(:, :) + type(c_ptr), allocatable, target :: helicity_pointers(:) + type(c_ptr) :: helicity_pointer + real(c_double), allocatable, target :: raw(:) + integer(c_size_t) :: request_count, index, point, offset, count + integer(c_int) :: status + + allocate(values(0, 0)) + request_count = size(requests, kind=c_size_t) + if (.not. argument_ok(point_count > 0 .and. request_count > 0 .and. size(momenta) > 0, & + "Correlated evaluation requires positive point, request and momentum counts", ierr)) return + if (.not. argument_ok(point_count <= huge(point_count) / 2_c_size_t / request_count, & + "Correlated output dimensions overflow", ierr)) return + allocate(storage(request_count), raw_requests(request_count)) + do index = 1, request_count + if (allocated(requests(index)%color_correlation)) then + if (.not. argument_ok(scan(requests(index)%color_correlation, c_null_char) == 0, & + "Colour correlation ID contains a NUL byte", ierr)) return + end if + ! build_spin_storage initializes all storage fields, so assign the ID last. + raw_requests(index)%spin_vectors = c_null_ptr + raw_requests(index)%spin_vector_count = 0_c_size_t + raw_requests(index)%use_default_spin_vectors = 1_c_int32_t + if (allocated(requests(index)%spin_vectors)) then + if (.not. build_spin_storage(requests(index)%spin_vectors, storage(index), ierr)) return + count = size(storage(index)%vectors, kind=c_size_t) + raw_requests(index)%spin_vector_count = count + raw_requests(index)%use_default_spin_vectors = 0_c_int32_t + if (count > 0) raw_requests(index)%spin_vectors = c_loc(storage(index)%vectors(1)) + end if + if (allocated(requests(index)%color_correlation)) then + call build_c_string(requests(index)%color_correlation, storage(index)%id) + else + call build_c_string("born", storage(index)%id) + end if + raw_requests(index)%color_correlation = c_loc(storage(index)%id(1)) + end do + if (present(helicity_ids)) then + do index = 1, size(helicity_ids, kind=c_size_t) + if (.not. argument_ok(scan(helicity_ids(index), c_null_char) == 0, & + "Helicity ID contains a NUL byte", ierr)) return + end do + end if + call build_c_string_array(helicity_ids, helicity_storage, helicity_pointers) + helicity_pointer = c_null_ptr + if (size(helicity_pointers) > 0) helicity_pointer = c_loc(helicity_pointers(1)) + allocate(raw(2_c_size_t * point_count * request_count)) + status = c_rusticol_evaluate_correlated_many(self%handle, c_loc(momenta(1)), & + size(momenta, kind=c_size_t), point_count, c_loc(raw_requests(1)), request_count, & + helicity_pointer, size(helicity_pointers, kind=c_size_t), c_loc(raw(1)), & + size(raw, kind=c_size_t)) + if (.not. status_ok(status, ierr)) return + deallocate(values) + allocate(values(point_count, request_count)) + offset = 1_c_size_t + do index = 1, request_count + do point = 1, point_count + values(point, index) = cmplx(raw(offset), raw(offset + 1), kind=c_double) + offset = offset + 2 + end do + end do + end subroutine rusticol_evaluate_correlated_many + + subroutine rusticol_evaluate_correlated(self, momenta, point_count, request, values, helicity_ids, ierr) + class(rusticol_runtime), intent(inout) :: self + real(c_double), intent(in), contiguous, target :: momenta(:) + integer(c_size_t), intent(in) :: point_count + type(rusticol_correlated_request), intent(in) :: request + complex(c_double_complex), allocatable, intent(out) :: values(:) + character(len=*), intent(in), optional :: helicity_ids(:) + integer(c_int), intent(out), optional :: ierr + complex(c_double_complex), allocatable :: batch(:, :) + + call self%evaluate_correlated_many(momenta, point_count, [request], batch, helicity_ids, ierr) + if (size(batch, 2) == 0) then + allocate(values(0)) + else + values = batch(:, 1) + end if + end subroutine rusticol_evaluate_correlated + + subroutine rusticol_evaluate(self, momenta, point_count, values, ierr) + class(rusticol_runtime), intent(inout) :: self + real(c_double), intent(in), target :: momenta(:) + integer(c_size_t), intent(in) :: point_count + real(c_double), allocatable, intent(out), target :: values(:) + integer(c_int), intent(out), optional :: ierr + integer(c_int) :: status + + if (.not. argument_ok(point_count > 0_c_size_t .and. size(momenta) > 0, & + "Rusticol evaluation requires positive point and momentum counts", ierr)) then + allocate(values(0)) + return + end if + allocate(values(point_count)) + status = c_rusticol_runtime_evaluate_f64( & + self%handle, c_loc(momenta(1)), size(momenta, kind=c_size_t), point_count, & + c_loc(values(1)), size(values, kind=c_size_t)) + if (.not. status_ok(status, ierr)) values = 0.0_c_double + end subroutine rusticol_evaluate + + subroutine rusticol_warm_up(self, point, result, helicity_ids, color_ids, & + progress_callback, progress_user_data, ierr, n_cores) + class(rusticol_runtime), intent(inout) :: self + real(c_double), intent(in), target :: point(:) + type(rusticol_warm_up_result), intent(out) :: result + character(len=*), intent(in), optional :: helicity_ids(:), color_ids(:) + procedure(rusticol_warm_up_progress_callback), optional :: progress_callback + type(c_ptr), intent(in), optional :: progress_user_data + integer(c_int), intent(out), optional :: ierr + integer, intent(in), optional :: n_cores + character(kind=c_char), allocatable, target :: helicity_storage(:, :), color_storage(:, :) + type(c_ptr), allocatable, target :: helicity_pointers(:), color_pointers(:) + type(c_ptr) :: helicity_pointer, color_pointer, user_data_pointer + type(c_funptr) :: callback_pointer + integer(c_int) :: status + integer(c_size_t) :: construction_cores + + result = rusticol_warm_up_result( & + 0_c_int32_t, 0_c_int32_t, 0.0_c_double, 0_c_int64_t, 0_c_int64_t, & + 0_c_int64_t, 0_c_int64_t, 0_c_int32_t, 0_c_int32_t, 0_c_int32_t, 0_c_int32_t) + if (.not. argument_ok(size(point) > 0, & + "Rusticol warm_up requires exactly one non-empty binary64 point", ierr)) return + construction_cores = 0_c_size_t + if (present(n_cores)) then + if (.not. argument_ok(n_cores > 0, "Rusticol warm_up n_cores must be positive", ierr)) return + construction_cores = int(n_cores, c_size_t) + end if + + call build_c_string_array(helicity_ids, helicity_storage, helicity_pointers) + call build_c_string_array(color_ids, color_storage, color_pointers) + helicity_pointer = c_null_ptr + if (size(helicity_pointers) > 0) helicity_pointer = c_loc(helicity_pointers(1)) + color_pointer = c_null_ptr + if (size(color_pointers) > 0) color_pointer = c_loc(color_pointers(1)) + + callback_pointer = c_null_funptr + if (present(progress_callback)) callback_pointer = c_funloc(progress_callback) + user_data_pointer = c_null_ptr + if (present(progress_user_data)) user_data_pointer = progress_user_data + + status = c_rusticol_runtime_warm_up_f64_with_cores( & + self%handle, c_loc(point(1)), size(point, kind=c_size_t), & + helicity_pointer, size(helicity_pointers, kind=c_size_t), & + color_pointer, size(color_pointers, kind=c_size_t), & + construction_cores, callback_pointer, user_data_pointer, result) + if (.not. status_ok(status, ierr)) return + end subroutine rusticol_warm_up + + subroutine rusticol_evaluate_selected(self, momenta, point_count, values, helicity_ids, & + color_ids, helicity_by_point, color_flow_by_point, ierr) + class(rusticol_runtime), intent(inout) :: self + real(c_double), intent(in), target :: momenta(:) + integer(c_size_t), intent(in) :: point_count + real(c_double), allocatable, intent(out), target :: values(:) + character(len=*), intent(in), optional :: helicity_ids(:), color_ids(:) + integer(c_int32_t), intent(in), optional, target :: helicity_by_point(:) + integer(c_int32_t), intent(in), optional, target :: color_flow_by_point(:) + integer(c_int), intent(out), optional :: ierr + character(kind=c_char), allocatable, target :: helicity_storage(:, :), color_storage(:, :) + type(c_ptr), allocatable, target :: helicity_pointers(:), color_pointers(:) + type(c_ptr) :: helicity_pointer, color_pointer + type(c_ptr) :: helicity_by_point_pointer, color_flow_by_point_pointer + integer(c_size_t) :: helicity_by_point_count, color_flow_by_point_count + logical :: has_helicity_ids, has_color_ids + integer(c_int) :: status + + if (.not. argument_ok(point_count > 0_c_size_t .and. size(momenta) > 0, & + "Rusticol selected evaluation requires positive point and momentum counts", ierr)) then + allocate(values(0)) + return + end if + has_helicity_ids = .false. + if (present(helicity_ids)) has_helicity_ids = size(helicity_ids) > 0 + has_color_ids = .false. + if (present(color_ids)) has_color_ids = size(color_ids) > 0 + + helicity_by_point_count = 0_c_size_t + helicity_by_point_pointer = c_null_ptr + if (present(helicity_by_point)) then + helicity_by_point_count = size(helicity_by_point, kind=c_size_t) + if (.not. argument_ok(helicity_by_point_count == 0_c_size_t .or. & + helicity_by_point_count == point_count, & + "helicity_by_point must contain one zero-based selector per point", ierr)) then + allocate(values(0)) + return + end if + if (.not. argument_ok(helicity_by_point_count == 0_c_size_t .or. & + all(helicity_by_point >= 0_c_int32_t), & + "helicity_by_point selectors must be nonnegative", ierr)) then + allocate(values(0)) + return + end if + if (helicity_by_point_count > 0) helicity_by_point_pointer = c_loc(helicity_by_point(1)) + end if + + color_flow_by_point_count = 0_c_size_t + color_flow_by_point_pointer = c_null_ptr + if (present(color_flow_by_point)) then + color_flow_by_point_count = size(color_flow_by_point, kind=c_size_t) + if (.not. argument_ok(color_flow_by_point_count == 0_c_size_t .or. & + color_flow_by_point_count == point_count, & + "color_flow_by_point must contain one zero-based selector per point", ierr)) then + allocate(values(0)) + return + end if + if (.not. argument_ok(color_flow_by_point_count == 0_c_size_t .or. & + all(color_flow_by_point >= 0_c_int32_t), & + "color_flow_by_point selectors must be nonnegative", ierr)) then + allocate(values(0)) + return + end if + if (color_flow_by_point_count > 0) then + color_flow_by_point_pointer = c_loc(color_flow_by_point(1)) + end if + end if + + if (.not. argument_ok(.not. (has_helicity_ids .and. helicity_by_point_count > 0), & + "helicity_ids and helicity_by_point are mutually exclusive", ierr)) then + allocate(values(0)) + return + end if + if (.not. argument_ok(.not. (has_color_ids .and. color_flow_by_point_count > 0), & + "color_ids and color_flow_by_point are mutually exclusive", ierr)) then + allocate(values(0)) + return + end if + + call build_c_string_array(helicity_ids, helicity_storage, helicity_pointers) + call build_c_string_array(color_ids, color_storage, color_pointers) + helicity_pointer = c_null_ptr + if (size(helicity_pointers) > 0) helicity_pointer = c_loc(helicity_pointers(1)) + color_pointer = c_null_ptr + if (size(color_pointers) > 0) color_pointer = c_loc(color_pointers(1)) + + allocate(values(point_count)) + status = c_rusticol_runtime_evaluate_selected_f64( & + self%handle, c_loc(momenta(1)), size(momenta, kind=c_size_t), point_count, & + helicity_pointer, size(helicity_pointers, kind=c_size_t), color_pointer, & + size(color_pointers, kind=c_size_t), helicity_by_point_pointer, & + helicity_by_point_count, color_flow_by_point_pointer, & + color_flow_by_point_count, & + c_loc(values(1)), size(values, kind=c_size_t)) + if (.not. status_ok(status, ierr)) values = 0.0_c_double + end subroutine rusticol_evaluate_selected + + subroutine rusticol_evaluate_resolved(self, momenta, point_count, values, helicity_ids, & + color_ids, ierr) + class(rusticol_runtime), intent(inout) :: self + real(c_double), intent(in), target :: momenta(:) + integer(c_size_t), intent(in) :: point_count + real(c_double), allocatable, intent(out), target :: values(:, :, :) + character(len=*), intent(in), optional :: helicity_ids(:), color_ids(:) + integer(c_int), intent(out), optional :: ierr + character(kind=c_char), allocatable, target :: helicity_storage(:, :), color_storage(:, :) + type(c_ptr), allocatable, target :: helicity_pointers(:), color_pointers(:) + type(c_ptr) :: helicity_pointer, color_pointer + integer(c_size_t) :: helicity_count, color_count + integer(c_int) :: status + + if (.not. argument_ok(point_count > 0_c_size_t .and. size(momenta) > 0, & + "Rusticol resolved evaluation requires positive point and momentum counts", ierr)) then + allocate(values(0, 0, 0)) + return + end if + call build_c_string_array(helicity_ids, helicity_storage, helicity_pointers) + call build_c_string_array(color_ids, color_storage, color_pointers) + helicity_pointer = c_null_ptr + if (size(helicity_pointers) > 0) helicity_pointer = c_loc(helicity_pointers(1)) + color_pointer = c_null_ptr + if (size(color_pointers) > 0) color_pointer = c_loc(color_pointers(1)) + status = c_rusticol_runtime_resolved_shape( & + self%handle, helicity_pointer, size(helicity_pointers, kind=c_size_t), & + color_pointer, size(color_pointers, kind=c_size_t), helicity_count, color_count) + if (.not. status_ok(status, ierr)) then + allocate(values(0, 0, 0)) + return + end if + allocate(values(color_count, helicity_count, point_count)) + status = c_rusticol_runtime_evaluate_resolved_f64( & + self%handle, c_loc(momenta(1)), size(momenta, kind=c_size_t), point_count, & + helicity_pointer, size(helicity_pointers, kind=c_size_t), color_pointer, & + size(color_pointers, kind=c_size_t), c_loc(values(1, 1, 1)), & + size(values, kind=c_size_t), helicity_count, color_count) + if (.not. status_ok(status, ierr)) values = 0.0_c_double + end subroutine rusticol_evaluate_resolved + + subroutine rusticol_set_model_parameter(self, name, real, imaginary, ierr) + class(rusticol_runtime), intent(inout) :: self + character(len=*), intent(in) :: name + real(c_double), intent(in) :: real + real(c_double), intent(in), optional :: imaginary + integer(c_int), intent(out), optional :: ierr + character(kind=c_char), allocatable, target :: name_c(:) + real(c_double) :: imaginary_value + integer(c_int) :: status + + imaginary_value = 0.0_c_double + if (present(imaginary)) imaginary_value = imaginary + call build_c_string(name, name_c) + status = c_rusticol_runtime_set_model_parameter( & + self%handle, c_loc(name_c(1)), real, imaginary_value) + if (.not. status_ok(status, ierr)) return + end subroutine rusticol_set_model_parameter + + subroutine rusticol_set_model_parameters(self, names, real, imaginary, ierr) + class(rusticol_runtime), intent(inout) :: self + character(len=*), intent(in) :: names(:) + real(c_double), intent(in), target :: real(:), imaginary(:) + integer(c_int), intent(out), optional :: ierr + character(kind=c_char), allocatable, target :: storage(:, :) + type(c_ptr), allocatable, target :: pointers(:) + integer(c_int) :: status + + if (.not. argument_ok(size(names) > 0 .and. size(names) == size(real) .and. & + size(names) == size(imaginary), & + "Rusticol model parameter arrays must be non-empty and have equal lengths", ierr)) then + return + end if + call build_c_string_array(names, storage, pointers) + status = c_rusticol_runtime_set_model_parameters( & + self%handle, c_loc(pointers(1)), c_loc(real(1)), c_loc(imaginary(1)), & + size(names, kind=c_size_t)) + if (.not. status_ok(status, ierr)) return + end subroutine rusticol_set_model_parameters + + subroutine rusticol_set_model_parameters_json(self, path, ierr) + class(rusticol_runtime), intent(inout) :: self + character(len=*), intent(in) :: path + integer(c_int), intent(out), optional :: ierr + character(kind=c_char), allocatable, target :: path_c(:) + integer(c_int) :: status + + call build_c_string(path, path_c) + status = c_rusticol_runtime_set_model_parameters_json(self%handle, c_loc(path_c(1))) + if (.not. status_ok(status, ierr)) return + end subroutine rusticol_set_model_parameters_json + + subroutine rusticol_mute_warnings(self, ierr) + class(rusticol_runtime), intent(inout) :: self + integer(c_int), intent(out), optional :: ierr + integer(c_int) :: status + status = c_rusticol_runtime_mute_warnings(self%handle, 1_c_int) + if (.not. status_ok(status, ierr)) return + end subroutine rusticol_mute_warnings + + subroutine rusticol_unmute_warnings(self, ierr) + class(rusticol_runtime), intent(inout) :: self + integer(c_int), intent(out), optional :: ierr + integer(c_int) :: status + status = c_rusticol_runtime_mute_warnings(self%handle, 0_c_int) + if (.not. status_ok(status, ierr)) return + end subroutine rusticol_unmute_warnings + + function rusticol_take_warnings_json(self, ierr) result(value) + class(rusticol_runtime), intent(inout) :: self + integer(c_int), intent(out), optional :: ierr + character(len=:), allocatable :: value + value = runtime_string(self, c_rusticol_runtime_take_warnings_json, ierr) + end function rusticol_take_warnings_json + +end module rusticol diff --git a/pyamplicol/rust/crates/rusticol-capi/include/rusticol.h b/pyamplicol/rust/crates/rusticol-capi/include/rusticol.h new file mode 100644 index 0000000000..36e08c40d1 --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-capi/include/rusticol.h @@ -0,0 +1,458 @@ +/* SPDX-License-Identifier: 0BSD */ + +#ifndef RUSTICOL_H +#define RUSTICOL_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define RUSTICOL_ABI_VERSION 1u + +enum rusticol_status { + RUSTICOL_STATUS_OK = 0, + RUSTICOL_STATUS_INVALID_ARGUMENT = 1, + RUSTICOL_STATUS_BUFFER_TOO_SMALL = 2, + RUSTICOL_STATUS_RUNTIME_ERROR = 3, + RUSTICOL_STATUS_PANIC = 4 +}; + +typedef struct RusticolRuntimeHandle RusticolRuntimeHandle; + +/* One literal complex spin source on a one-based public external leg. + * components use [point][E,px,py,pz][real,imag], with 8 doubles per point. + * point_count=1 broadcasts; otherwise it must match the evaluation batch. + * Values are copied by the setter and borrowed only during an evaluation. + */ +typedef struct RusticolSpinCorrelationVector { + size_t leg; + size_t point_count; + const double *components; + size_t component_count; +} RusticolSpinCorrelationVector; + +/* A directed colour insertion, with optional spin replacements. + * use_default_spin_vectors=1 inherits the setter and requires an empty array; + * 0 uses this request's vectors (an empty array selects physical helicities). + */ +typedef struct RusticolCorrelatedRequest { + const char *color_correlation; + const RusticolSpinCorrelationVector *spin_vectors; + size_t spin_vector_count; + uint32_t use_default_spin_vectors; +} RusticolCorrelatedRequest; + +enum rusticol_warm_up_event_kind { + RUSTICOL_WARM_UP_EVENT_START = 0, + RUSTICOL_WARM_UP_EVENT_UPDATE = 1, + RUSTICOL_WARM_UP_EVENT_END = 2 +}; + +enum rusticol_warm_up_stage { + RUSTICOL_WARM_UP_STAGE_PROCESS_PREPARATION = 0, + RUSTICOL_WARM_UP_STAGE_QUERY_FAMILY = 1, + RUSTICOL_WARM_UP_STAGE_FAMILY_FINALIZATION = 2, + RUSTICOL_WARM_UP_STAGE_FIRST_EVALUATION = 3 +}; + +/* + * Fixed-layout progress snapshot. RSS fields are meaningful only when their + * matching *_available field is nonzero. The event pointer is borrowed for + * the callback invocation only. + */ +typedef struct RusticolWarmUpProgressEvent { + uint32_t schema_version; + uint32_t kind; + uint32_t stage; + uint32_t reserved; + uint64_t completed; + uint64_t total; + double elapsed_seconds; + uint64_t current_rss_bytes; + uint64_t peak_rss_bytes; + uint64_t workers; + uint32_t current_rss_available; + uint32_t peak_rss_available; +} RusticolWarmUpProgressEvent; + +typedef struct RusticolWarmUpResult { + uint32_t schema_version; + uint32_t reserved; + double elapsed_seconds; + uint64_t query_count; + uint64_t warmed_query_count; + uint64_t current_rss_bytes; + uint64_t peak_rss_bytes; + uint32_t current_rss_available; + uint32_t peak_rss_available; + uint32_t already_warm; + uint32_t first_evaluation_completed; +} RusticolWarmUpResult; + +/* Return nonzero to continue, or zero to request cancellation. */ +typedef int (*RusticolWarmUpProgressCallback)( + const RusticolWarmUpProgressEvent *event, + void *user_data +); + +/* + * A handle is mutable and must not be called concurrently. Independent handles + * may be used concurrently from separate threads. + * + * For string and variable-length metadata getters, callers may query the + * required capacity with a null buffer and zero capacity. String capacities + * include the trailing NUL. Query and short-buffer calls do not consume + * warning state; rusticol_runtime_take_warnings_json consumes warnings only + * after a successful copy. + */ + +uint32_t rusticol_abi_version(void); +int rusticol_supported_runtime_capabilities_json( + char *buffer, + size_t capacity, + size_t *required +); +int rusticol_last_error_message(char *buffer, size_t capacity, size_t *required); + +/* process_key may be a stable process/alias ID or a concrete expression. */ +int rusticol_runtime_load( + const char *process_dir, + const char *process_key, + const char *model_parameters_path, + RusticolRuntimeHandle **output +); +int rusticol_runtime_free(RusticolRuntimeHandle *handle); + +/* + * Save/restore the completed, retained on-the-fly recursion cache. These calls + * apply only to on-the-fly runtimes; load_cache requires an already-loaded + * matching process output. The cache file does not replace that process output. + * Saving does not warm new selections and cannot checkpoint an in-progress call. + * A later evaluation may populate selections not present in the saved cache. + * path must be non-null, NUL-terminated UTF-8; relative paths use the working directory. + */ +int rusticol_runtime_save(const RusticolRuntimeHandle *handle, const char *path); +int rusticol_runtime_load_cache(RusticolRuntimeHandle *handle, const char *path); + +/* Existing content identity of the loaded, authenticated artifact manifest. */ +int rusticol_runtime_artifact_id( + const RusticolRuntimeHandle *handle, + char *buffer, + size_t capacity, + size_t *required +); +int rusticol_runtime_metadata_json( + const RusticolRuntimeHandle *handle, + char *buffer, + size_t capacity, + size_t *required +); +int rusticol_runtime_execution_mode( + const RusticolRuntimeHandle *handle, + char *buffer, + size_t capacity, + size_t *required +); +int rusticol_runtime_physics_json( + const RusticolRuntimeHandle *handle, + char *buffer, + size_t capacity, + size_t *required +); +int rusticol_runtime_process( + const RusticolRuntimeHandle *handle, + char *buffer, + size_t capacity, + size_t *required +); +int rusticol_runtime_process_key( + const RusticolRuntimeHandle *handle, + char *buffer, + size_t capacity, + size_t *required +); +int rusticol_runtime_representative_process_key( + const RusticolRuntimeHandle *handle, + char *buffer, + size_t capacity, + size_t *required +); +int rusticol_runtime_color_accuracy( + const RusticolRuntimeHandle *handle, + char *buffer, + size_t capacity, + size_t *required +); + +int rusticol_runtime_external_count( + const RusticolRuntimeHandle *handle, + size_t *output +); +int rusticol_runtime_external_pdg( + const RusticolRuntimeHandle *handle, + size_t index, + int32_t *output +); +/* Representative-index to public/requested-index external-leg permutation. */ +int rusticol_runtime_external_permutation( + const RusticolRuntimeHandle *handle, + size_t *output, + size_t capacity, + size_t *required +); +/* Exact relation of the selected process to the representative evaluating it: + * ME = numerator / denominator * ME_representative, with reversed LC color-flow + * words when conjugated is 1. Processes other than non-generated members of the + * artifact's process-group map report 1/1 and 0. */ +int rusticol_runtime_process_group_relation( + const RusticolRuntimeHandle *handle, + uint64_t *numerator, + uint64_t *denominator, + int32_t *conjugated +); +/* Load one public-order [external][4] JSON point into a flat f64 buffer. */ +int rusticol_runtime_load_kinematics_json( + const RusticolRuntimeHandle *handle, + const char *path, + double *output, + size_t capacity, + size_t *required +); + +int rusticol_runtime_helicity_count( + const RusticolRuntimeHandle *handle, + size_t *output +); +int rusticol_runtime_helicity_id( + const RusticolRuntimeHandle *handle, + size_t index, + char *buffer, + size_t capacity, + size_t *required +); +int rusticol_runtime_helicity_vector( + const RusticolRuntimeHandle *handle, + size_t index, + int32_t *output, + size_t capacity, + size_t *required +); + +int rusticol_runtime_color_count( + const RusticolRuntimeHandle *handle, + size_t *output +); +int rusticol_runtime_color_id( + const RusticolRuntimeHandle *handle, + size_t index, + char *buffer, + size_t capacity, + size_t *required +); +int rusticol_runtime_color_kind( + const RusticolRuntimeHandle *handle, + size_t index, + char *buffer, + size_t capacity, + size_t *required +); +int rusticol_runtime_color_word( + const RusticolRuntimeHandle *handle, + size_t index, + size_t *output, + size_t capacity, + size_t *required +); + +int rusticol_runtime_model_parameter_count( + const RusticolRuntimeHandle *handle, + size_t *output +); +int rusticol_runtime_model_parameter_name( + const RusticolRuntimeHandle *handle, + size_t index, + char *buffer, + size_t capacity, + size_t *required +); + +int rusticol_runtime_resolved_shape( + const RusticolRuntimeHandle *handle, + const char *const *helicity_ids, + size_t helicity_count, + const char *const *color_ids, + size_t color_count, + size_t *output_helicity_count, + size_t *output_color_count +); + +/* + * Construct and retain one selected OTF query family, then evaluate exactly + * one binary64 point. Momenta use [external particle][E, px, py, pz]. Global + * selector IDs are optional; a null pointer with zero count sums that axis. + * Uses the process-output construction default. + * Progress callbacks run on this coordinating caller thread. The terminal + * FIRST_EVALUATION/END event is post-commit and cannot cancel the result. + */ +int rusticol_runtime_warm_up_f64( + RusticolRuntimeHandle *handle, + const double *momenta, + size_t momentum_count, + const char *const *helicity_ids, + size_t helicity_count, + const char *const *color_ids, + size_t color_count, + RusticolWarmUpProgressCallback progress_callback, + void *progress_user_data, + RusticolWarmUpResult *output +); + +/* Same warm-up contract, with an explicit construction-core override. + * n_cores=0 uses the process-output default; a positive value overrides it + * for this call only, without changing evaluation or cache identity. + */ +int rusticol_runtime_warm_up_f64_with_cores( + RusticolRuntimeHandle *handle, + const double *momenta, + size_t momentum_count, + const char *const *helicity_ids, + size_t helicity_count, + const char *const *color_ids, + size_t color_count, + size_t n_cores, + RusticolWarmUpProgressCallback progress_callback, + void *progress_user_data, + RusticolWarmUpResult *output +); + +/* Momenta use [point][external particle][E, px, py, pz]. */ +/* Correlation queries read the selected process's generation-time catalogue. + * They may lazily initialize correlated metadata, but do not evaluate an ME. + * String queries include the trailing NUL in required; NULL/0 queries size. + */ +int rusticol_runtime_color_correlation_count( + RusticolRuntimeHandle *handle, size_t *output +); +int rusticol_runtime_color_correlation_id( + RusticolRuntimeHandle *handle, size_t index, + char *buffer, size_t capacity, size_t *required +); +int rusticol_runtime_color_correlation_catalogue_json( + RusticolRuntimeHandle *handle, char *buffer, size_t capacity, size_t *required +); + +/* Empty vectors clear defaults. Ordinary evaluations never use these values. */ +int rusticol_runtime_set_spin_correlation_vectors_f64( + RusticolRuntimeHandle *handle, + const RusticolSpinCorrelationVector *vectors, size_t vector_count +); + +/* Grouped binary64 colour/spin insertions over a phase-space batch. + * output uses [request][point][real,imag], with capacity measured in doubles. + * helicity_ids optionally selects physical helicities of unreplaced legs. + * All request/input buffers are borrowed only for this call. Dimensions and + * selectors are validated before evaluation; output is written only on success. + */ +int rusticol_runtime_evaluate_correlated_many_f64( + RusticolRuntimeHandle *handle, + const double *momenta, size_t momentum_count, size_t point_count, + const RusticolCorrelatedRequest *requests, size_t request_count, + const char *const *helicity_ids, size_t helicity_count, + double *output, size_t output_capacity +); + +/* Ordinary momenta use [point][external particle][E, px, py, pz]. */ +int rusticol_runtime_evaluate_f64( + RusticolRuntimeHandle *handle, + const double *momenta, + size_t momentum_count, + size_t point_count, + double *output, + size_t output_capacity +); + +/* + * Evaluate one total per point with optional selectors. Global selectors are + * physical string IDs. Per-point selectors are zero-based physical indices and + * must have length point_count. Global and per-point selectors are mutually + * exclusive on the same axis. A null pointer with zero length omits that axis. + */ +int rusticol_runtime_evaluate_selected_f64( + RusticolRuntimeHandle *handle, + const double *momenta, + size_t momentum_count, + size_t point_count, + const char *const *helicity_ids, + size_t helicity_count, + const char *const *color_ids, + size_t color_count, + const uint32_t *helicity_by_point, + size_t helicity_by_point_count, + const uint32_t *color_flow_by_point, + size_t color_flow_by_point_count, + double *output, + size_t output_capacity +); + +/* Resolved output uses [point][helicity][color]. */ +int rusticol_runtime_evaluate_resolved_f64( + RusticolRuntimeHandle *handle, + const double *momenta, + size_t momentum_count, + size_t point_count, + const char *const *helicity_ids, + size_t helicity_count, + const char *const *color_ids, + size_t color_count, + double *output, + size_t output_capacity, + size_t *output_helicity_count, + size_t *output_color_count +); + +int rusticol_runtime_set_model_parameters( + RusticolRuntimeHandle *handle, + const char *const *names, + const double *real, + const double *imaginary, + size_t count +); +int rusticol_runtime_set_model_parameter( + RusticolRuntimeHandle *handle, + const char *name, + double real, + double imaginary +); +/* Read the current value (not its saved default), including derived parameters. + * Both output pointers must be non-null and writable for one double. + */ +int rusticol_runtime_get_model_parameter( + const RusticolRuntimeHandle *handle, + const char *name, + double *real, + double *imaginary +); +int rusticol_runtime_set_model_parameters_json( + RusticolRuntimeHandle *handle, + const char *path +); + +int rusticol_runtime_mute_warnings( + RusticolRuntimeHandle *handle, + int muted +); +int rusticol_runtime_take_warnings_json( + RusticolRuntimeHandle *handle, + char *buffer, + size_t capacity, + size_t *required +); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/pyamplicol/rust/crates/rusticol-capi/include/rusticol.hpp b/pyamplicol/rust/crates/rusticol-capi/include/rusticol.hpp new file mode 100644 index 0000000000..d437af6e0d --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-capi/include/rusticol.hpp @@ -0,0 +1,695 @@ +// SPDX-License-Identifier: 0BSD + +#ifndef RUSTICOL_HPP +#define RUSTICOL_HPP + +#include "rusticol.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace rusticol { + +class Error : public std::runtime_error { +public: + explicit Error(const std::string &message) : std::runtime_error(message) {} +}; + +inline std::string last_error() { + std::size_t required = 0; + rusticol_last_error_message(nullptr, 0, &required); + if (required == 0) { + return "unknown Rusticol error"; + } + std::vector buffer(required); + rusticol_last_error_message(buffer.data(), buffer.size(), &required); + return std::string(buffer.data()); +} + +inline void check(int status) { + if (status != RUSTICOL_STATUS_OK) { + throw Error(last_error()); + } +} + +inline std::string supported_runtime_capabilities_json() { + std::size_t required = 0; + check(rusticol_supported_runtime_capabilities_json(nullptr, 0, &required)); + std::vector buffer(required); + check(rusticol_supported_runtime_capabilities_json( + buffer.data(), buffer.size(), &required)); + return std::string(buffer.data()); +} + +struct ExternalParticle { + std::size_t index{}; + std::int32_t pdg{}; +}; + +struct HelicityConfiguration { + std::string id; + std::vector helicities; +}; + +struct ColorComponent { + std::string id; + std::string kind; + std::vector word; +}; + +struct ModelParameter { + std::string name; +}; + +struct SpinCorrelationVector { + std::size_t leg{}; // One-based public external leg. + // One four-vector broadcasts; otherwise supply one per momentum point. + std::vector, 4>> components; +}; + +struct CorrelatedRequest { + std::string color_correlation{"born"}; + // nullopt inherits the setter; an empty vector selects physical helicities. + std::optional> spin_vectors; +}; + +struct CorrelatedEvaluation { + std::vector> values; // [request][point] + std::size_t request_count{}; + std::size_t point_count{}; + + std::complex operator()(std::size_t request, std::size_t point) const { + if (request >= request_count || point >= point_count) { + throw std::out_of_range("correlated Rusticol result index is out of range"); + } + return values.at(request * point_count + point); + } +}; + +enum class WarmUpEventKind : std::uint32_t { + start = RUSTICOL_WARM_UP_EVENT_START, + update = RUSTICOL_WARM_UP_EVENT_UPDATE, + end = RUSTICOL_WARM_UP_EVENT_END, +}; + +enum class WarmUpStage : std::uint32_t { + process_preparation = RUSTICOL_WARM_UP_STAGE_PROCESS_PREPARATION, + query_family = RUSTICOL_WARM_UP_STAGE_QUERY_FAMILY, + family_finalization = RUSTICOL_WARM_UP_STAGE_FAMILY_FINALIZATION, + first_evaluation = RUSTICOL_WARM_UP_STAGE_FIRST_EVALUATION, +}; + +struct WarmUpProgress { + std::uint32_t schema_version{}; + WarmUpEventKind kind{}; + WarmUpStage stage{}; + std::uint64_t completed{}; + std::uint64_t total{}; + double elapsed_seconds{}; + std::uint64_t current_rss_bytes{}; + std::uint64_t peak_rss_bytes{}; + std::uint64_t workers{}; + bool current_rss_available{}; + bool peak_rss_available{}; +}; + +struct WarmUpResult { + std::uint32_t schema_version{}; + double elapsed_seconds{}; + std::uint64_t query_count{}; + std::uint64_t warmed_query_count{}; + std::uint64_t current_rss_bytes{}; + std::uint64_t peak_rss_bytes{}; + bool current_rss_available{}; + bool peak_rss_available{}; + bool already_warm{}; + bool first_evaluation_completed{}; +}; + +namespace detail { + +struct SpinCorrelationStorage { + std::vector> components; + std::vector vectors; + + explicit SpinCorrelationStorage(const std::vector &input) { + components.reserve(input.size()); + vectors.reserve(input.size()); + for (const auto &item : input) { + components.emplace_back(); + auto &flat = components.back(); + for (const auto &point : item.components) { + for (const auto &value : point) { + flat.push_back(value.real()); + flat.push_back(value.imag()); + } + } + vectors.push_back({item.leg, item.components.size(), + flat.empty() ? nullptr : flat.data(), flat.size()}); + } + } +}; + +inline WarmUpProgress warm_up_progress(const RusticolWarmUpProgressEvent &event) { + return { + event.schema_version, + static_cast(event.kind), + static_cast(event.stage), + event.completed, + event.total, + event.elapsed_seconds, + event.current_rss_bytes, + event.peak_rss_bytes, + event.workers, + event.current_rss_available != 0, + event.peak_rss_available != 0, + }; +} + +inline WarmUpResult warm_up_result(const RusticolWarmUpResult &result) { + return { + result.schema_version, + result.elapsed_seconds, + result.query_count, + result.warmed_query_count, + result.current_rss_bytes, + result.peak_rss_bytes, + result.current_rss_available != 0, + result.peak_rss_available != 0, + result.already_warm != 0, + result.first_evaluation_completed != 0, + }; +} + +struct WarmUpCallbackState { + const std::function *callback{}; + std::exception_ptr exception; +}; + +inline int warm_up_progress_callback( + const RusticolWarmUpProgressEvent *event, + void *user_data) noexcept { + if (event == nullptr || user_data == nullptr) { + return 0; + } + auto &state = *static_cast(user_data); + if (state.callback == nullptr) { + return 0; + } + try { + return (*state.callback)(warm_up_progress(*event)) ? 1 : 0; + } catch (...) { + const bool terminal = + event->kind == RUSTICOL_WARM_UP_EVENT_END && + event->stage == RUSTICOL_WARM_UP_STAGE_FIRST_EVALUATION; + if (!terminal) { + state.exception = std::current_exception(); + return 0; + } + // This event is emitted after the selected family and first evaluation + // have committed. Match the core contract by treating it as a + // non-cancellable notification. + return 1; + } +} + +} // namespace detail + +class ResolvedEvaluation { +public: + std::vector values; + std::size_t point_count{}; + std::vector helicities; + std::vector colors; + + double operator()(std::size_t point, std::size_t helicity, std::size_t color) const { + if (point >= point_count || helicity >= helicities.size() || color >= colors.size()) { + throw std::out_of_range("resolved Rusticol result index is out of range"); + } + return values[(point * helicities.size() + helicity) * colors.size() + color]; + } + + std::vector total() const { + const std::size_t stride = helicities.size() * colors.size(); + std::vector totals(point_count, 0.0); + for (std::size_t point = 0; point < point_count; ++point) { + for (std::size_t component = 0; component < stride; ++component) { + totals[point] += values[point * stride + component]; + } + } + return totals; + } +}; + +class Runtime { +public: + Runtime(const std::string &process_dir, + const std::string &process_key = {}, + const std::string &model_parameters = {}) { + RusticolRuntimeHandle *loaded = nullptr; + check(rusticol_runtime_load( + process_dir.c_str(), + process_key.empty() ? nullptr : process_key.c_str(), + model_parameters.empty() ? nullptr : model_parameters.c_str(), + &loaded)); + handle_ = loaded; + } + + ~Runtime() { + if (handle_ != nullptr) { + rusticol_runtime_free(handle_); + } + } + + Runtime(const Runtime &) = delete; + Runtime &operator=(const Runtime &) = delete; + + Runtime(Runtime &&other) noexcept : handle_(std::exchange(other.handle_, nullptr)) {} + + Runtime &operator=(Runtime &&other) noexcept { + if (this != &other) { + if (handle_ != nullptr) { + rusticol_runtime_free(handle_); + } + handle_ = std::exchange(other.handle_, nullptr); + } + return *this; + } + + // Save the completed, retained OTF cache, without warming new selections. + void save(const std::string &path) const { + if (path.find('\0') != std::string::npos) { + throw std::invalid_argument("cache path contains a NUL byte"); + } + check(rusticol_runtime_save(handle_, path.c_str())); + } + + // Restore an OTF cache after loading the matching process output normally. + void load_cache(const std::string &path) { + if (path.find('\0') != std::string::npos) { + throw std::invalid_argument("cache path contains a NUL byte"); + } + check(rusticol_runtime_load_cache(handle_, path.c_str())); + } + + std::string process() const { return get_string(rusticol_runtime_process); } + std::string process_key() const { return get_string(rusticol_runtime_process_key); } + std::string representative_process_key() const { + return get_string(rusticol_runtime_representative_process_key); + } + std::string color_accuracy() const { return get_string(rusticol_runtime_color_accuracy); } + std::string execution_mode() const { return get_string(rusticol_runtime_execution_mode); } + std::string metadata_json() const { return get_string(rusticol_runtime_metadata_json); } + std::string physics_json() const { return get_string(rusticol_runtime_physics_json); } + + std::vector external_particles() const { + std::size_t count = 0; + check(rusticol_runtime_external_count(handle_, &count)); + std::vector result; + result.reserve(count); + for (std::size_t index = 0; index < count; ++index) { + std::int32_t pdg = 0; + check(rusticol_runtime_external_pdg(handle_, index, &pdg)); + result.push_back({index, pdg}); + } + return result; + } + + std::vector external_permutation() const { + std::size_t required = 0; + check(rusticol_runtime_external_permutation( + handle_, nullptr, 0, &required)); + std::vector result(required); + if (required != 0) { + check(rusticol_runtime_external_permutation( + handle_, result.data(), result.size(), &required)); + } + return result; + } + + std::vector load_kinematics_json(const std::string &path) const { + std::size_t required = 0; + check(rusticol_runtime_load_kinematics_json( + handle_, path.c_str(), nullptr, 0, &required)); + std::vector result(required); + if (required != 0) { + check(rusticol_runtime_load_kinematics_json( + handle_, path.c_str(), result.data(), result.size(), &required)); + } + return result; + } + + std::vector helicities() const { + std::size_t count = 0; + check(rusticol_runtime_helicity_count(handle_, &count)); + std::vector result; + result.reserve(count); + for (std::size_t index = 0; index < count; ++index) { + std::size_t vector_size = 0; + check(rusticol_runtime_helicity_vector(handle_, index, nullptr, 0, &vector_size)); + std::vector vector(vector_size); + check(rusticol_runtime_helicity_vector( + handle_, index, vector.data(), vector.size(), &vector_size)); + result.push_back({get_indexed_string(rusticol_runtime_helicity_id, index), vector}); + } + return result; + } + + std::vector colors() const { + std::size_t count = 0; + check(rusticol_runtime_color_count(handle_, &count)); + std::vector result; + result.reserve(count); + for (std::size_t index = 0; index < count; ++index) { + std::size_t word_size = 0; + check(rusticol_runtime_color_word(handle_, index, nullptr, 0, &word_size)); + std::vector word(word_size); + check(rusticol_runtime_color_word( + handle_, index, word.data(), word.size(), &word_size)); + result.push_back({ + get_indexed_string(rusticol_runtime_color_id, index), + get_indexed_string(rusticol_runtime_color_kind, index), + word, + }); + } + return result; + } + + std::vector model_parameters() const { + std::size_t count = 0; + check(rusticol_runtime_model_parameter_count(handle_, &count)); + std::vector result; + result.reserve(count); + for (std::size_t index = 0; index < count; ++index) { + result.push_back({get_indexed_string(rusticol_runtime_model_parameter_name, index)}); + } + return result; + } + + std::vector evaluate(const std::vector &momenta, + std::size_t point_count) { + std::vector values(point_count); + check(rusticol_runtime_evaluate_f64( + handle_, momenta.data(), momenta.size(), point_count, values.data(), values.size())); + return values; + } + + std::string color_correlation_catalogue_json() { + return get_string_mut(rusticol_runtime_color_correlation_catalogue_json); + } + + std::vector color_correlation_ids() { + std::size_t count = 0; + check(rusticol_runtime_color_correlation_count(handle_, &count)); + std::vector result; + result.reserve(count); + for (std::size_t index = 0; index < count; ++index) { + std::size_t required = 0; + check(rusticol_runtime_color_correlation_id(handle_, index, nullptr, 0, &required)); + std::vector buffer(required); + check(rusticol_runtime_color_correlation_id( + handle_, index, buffer.data(), buffer.size(), &required)); + result.emplace_back(buffer.data()); + } + return result; + } + + // Empty input clears defaults; this does not affect ordinary evaluate(). + void set_spin_correlation_vectors(const std::vector &vectors = {}) { + const detail::SpinCorrelationStorage storage(vectors); + check(rusticol_runtime_set_spin_correlation_vectors_f64( + handle_, storage.vectors.empty() ? nullptr : storage.vectors.data(), + storage.vectors.size())); + } + + CorrelatedEvaluation evaluate_correlated_many( + const std::vector &momenta, + std::size_t point_count, + const std::vector &requests, + const std::vector &helicity_ids = {}) { + if (point_count == 0 || requests.empty() || + point_count > std::numeric_limits::max() / 2 / requests.size()) { + throw std::invalid_argument("invalid correlated result dimensions"); + } + std::vector storage; + std::vector raw_requests; + const std::vector physical_helicities; + storage.reserve(requests.size()); + raw_requests.reserve(requests.size()); + for (const auto &request : requests) { + if (request.color_correlation.find('\0') != std::string::npos) { + throw std::invalid_argument("colour correlation ID contains a NUL byte"); + } + storage.emplace_back(request.spin_vectors ? *request.spin_vectors : physical_helicities); + const auto &vectors = storage.back().vectors; + raw_requests.push_back({request.color_correlation.c_str(), + vectors.empty() ? nullptr : vectors.data(), vectors.size(), + request.spin_vectors ? 0u : 1u}); + } + for (const auto &id : helicity_ids) { + if (id.find('\0') != std::string::npos) { + throw std::invalid_argument("helicity ID contains a NUL byte"); + } + } + const auto helicity_ptrs = c_string_pointers(helicity_ids); + std::vector raw(requests.size() * point_count * 2); + check(rusticol_runtime_evaluate_correlated_many_f64( + handle_, momenta.data(), momenta.size(), point_count, + raw_requests.data(), raw_requests.size(), + helicity_ptrs.empty() ? nullptr : helicity_ptrs.data(), helicity_ptrs.size(), + raw.data(), raw.size())); + CorrelatedEvaluation result{{}, requests.size(), point_count}; + result.values.reserve(raw.size() / 2); + for (std::size_t index = 0; index < raw.size(); index += 2) { + result.values.emplace_back(raw[index], raw[index + 1]); + } + return result; + } + + std::vector> evaluate_correlated( + const std::vector &momenta, + std::size_t point_count, + const std::string &color_correlation = "born", + const std::optional> &spin_vectors = std::nullopt, + const std::vector &helicity_ids = {}) { + return evaluate_correlated_many( + momenta, point_count, {{color_correlation, spin_vectors}}, helicity_ids).values; + } + + WarmUpResult warm_up( + const std::vector &point, + const std::vector &helicity_ids = {}, + const std::vector &color_ids = {}, + const std::function &progress = {}, + std::size_t n_cores = 0) { + const auto helicity_ptrs = c_string_pointers(helicity_ids); + const auto color_ptrs = c_string_pointers(color_ids); + RusticolWarmUpResult result{}; + detail::WarmUpCallbackState callback_state{progress ? &progress : nullptr, {}}; + const int status = rusticol_runtime_warm_up_f64_with_cores( + handle_, + point.data(), + point.size(), + helicity_ptrs.empty() ? nullptr : helicity_ptrs.data(), + helicity_ptrs.size(), + color_ptrs.empty() ? nullptr : color_ptrs.data(), + color_ptrs.size(), + n_cores, + progress ? detail::warm_up_progress_callback : nullptr, + progress ? static_cast(&callback_state) : nullptr, + &result); + if (callback_state.exception) { + std::rethrow_exception(callback_state.exception); + } + check(status); + const auto converted = detail::warm_up_result(result); + if (converted.schema_version != 1 || !converted.first_evaluation_completed) { + throw Error("Rusticol returned an invalid successful warm-up result"); + } + return converted; + } + + std::vector evaluate_selected( + const std::vector &momenta, + std::size_t point_count, + const std::vector &helicity_ids = {}, + const std::vector &color_ids = {}, + const std::vector &helicity_by_point = {}, + const std::vector &color_flow_by_point = {}) { + const auto helicity_ptrs = c_string_pointers(helicity_ids); + const auto color_ptrs = c_string_pointers(color_ids); + std::vector values(point_count); + check(rusticol_runtime_evaluate_selected_f64( + handle_, + momenta.data(), + momenta.size(), + point_count, + helicity_ptrs.empty() ? nullptr : helicity_ptrs.data(), + helicity_ptrs.size(), + color_ptrs.empty() ? nullptr : color_ptrs.data(), + color_ptrs.size(), + helicity_by_point.empty() ? nullptr : helicity_by_point.data(), + helicity_by_point.size(), + color_flow_by_point.empty() ? nullptr : color_flow_by_point.data(), + color_flow_by_point.size(), + values.data(), + values.size())); + return values; + } + + ResolvedEvaluation evaluate_resolved( + const std::vector &momenta, + std::size_t point_count, + const std::vector &helicity_ids = {}, + const std::vector &color_ids = {}) { + const auto helicity_ptrs = c_string_pointers(helicity_ids); + const auto color_ptrs = c_string_pointers(color_ids); + std::size_t helicity_count = 0; + std::size_t color_count = 0; + check(rusticol_runtime_resolved_shape( + handle_, + helicity_ptrs.empty() ? nullptr : helicity_ptrs.data(), + helicity_ptrs.size(), + color_ptrs.empty() ? nullptr : color_ptrs.data(), + color_ptrs.size(), + &helicity_count, + &color_count)); + std::vector values(point_count * helicity_count * color_count); + check(rusticol_runtime_evaluate_resolved_f64( + handle_, + momenta.data(), + momenta.size(), + point_count, + helicity_ptrs.empty() ? nullptr : helicity_ptrs.data(), + helicity_ptrs.size(), + color_ptrs.empty() ? nullptr : color_ptrs.data(), + color_ptrs.size(), + values.data(), + values.size(), + &helicity_count, + &color_count)); + + auto all_helicities = helicities(); + auto all_colors = colors(); + return { + std::move(values), + point_count, + select_helicities(all_helicities, helicity_ids), + select_colors(all_colors, color_ids), + }; + } + + void set_model_parameter(const std::string &name, double real, double imaginary = 0.0) { + check(rusticol_runtime_set_model_parameter(handle_, name.c_str(), real, imaginary)); + } + + void set_model_parameters(const std::vector &names, + const std::vector &real, + const std::vector &imaginary) { + if (names.size() != real.size() || names.size() != imaginary.size()) { + throw std::invalid_argument("model parameter arrays have different lengths"); + } + const auto pointers = c_string_pointers(names); + check(rusticol_runtime_set_model_parameters( + handle_, pointers.data(), real.data(), imaginary.data(), names.size())); + } + + void set_model_parameters_json(const std::string &path) { + check(rusticol_runtime_set_model_parameters_json(handle_, path.c_str())); + } + + void mute_warnings() { check(rusticol_runtime_mute_warnings(handle_, 1)); } + void unmute_warnings() { check(rusticol_runtime_mute_warnings(handle_, 0)); } + std::string take_warnings_json() { + return get_string_mut(rusticol_runtime_take_warnings_json); + } + +private: + using StringFunction = int (*)(const RusticolRuntimeHandle *, char *, std::size_t, std::size_t *); + using MutableStringFunction = int (*)(RusticolRuntimeHandle *, char *, std::size_t, std::size_t *); + using IndexedStringFunction = int (*)(const RusticolRuntimeHandle *, std::size_t, char *, std::size_t, std::size_t *); + + std::string get_string(StringFunction function) const { + std::size_t required = 0; + check(function(handle_, nullptr, 0, &required)); + std::vector buffer(required); + check(function(handle_, buffer.data(), buffer.size(), &required)); + return std::string(buffer.data()); + } + + std::string get_string_mut(MutableStringFunction function) { + std::size_t required = 0; + check(function(handle_, nullptr, 0, &required)); + std::vector buffer(required); + check(function(handle_, buffer.data(), buffer.size(), &required)); + return std::string(buffer.data()); + } + + std::string get_indexed_string(IndexedStringFunction function, std::size_t index) const { + std::size_t required = 0; + check(function(handle_, index, nullptr, 0, &required)); + std::vector buffer(required); + check(function(handle_, index, buffer.data(), buffer.size(), &required)); + return std::string(buffer.data()); + } + + static std::vector c_string_pointers(const std::vector &values) { + std::vector pointers; + pointers.reserve(values.size()); + for (const auto &value : values) { + pointers.push_back(value.c_str()); + } + return pointers; + } + + static std::vector select_helicities( + const std::vector &available, + const std::vector &selected) { + if (selected.empty()) { + return available; + } + std::vector result; + for (const auto &item : available) { + for (const auto &id : selected) { + if (item.id == id) { + result.push_back(item); + } + } + } + return result; + } + + static std::vector select_colors( + const std::vector &available, + const std::vector &selected) { + if (selected.empty()) { + return available; + } + std::vector result; + for (const auto &item : available) { + for (const auto &id : selected) { + if (item.id == id) { + result.push_back(item); + } + } + } + return result; + } + + RusticolRuntimeHandle *handle_{}; +}; + +} // namespace rusticol + +#endif diff --git a/pyamplicol/rust/crates/rusticol-capi/src/correlated.rs b/pyamplicol/rust/crates/rusticol-capi/src/correlated.rs new file mode 100644 index 0000000000..caab0efe1f --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-capi/src/correlated.rs @@ -0,0 +1,283 @@ +// SPDX-License-Identifier: 0BSD +//! Additive correlated evaluation ABI. Ordinary calls do not enter this module. + +use super::*; + +unsafe fn read_spin_vectors( + vectors: *const RusticolSpinCorrelationVector, + count: usize, +) -> AbiResult { + let mut result = NativeSpinCorrelationVectors::new(); + if count == 0 { + return Ok(result); + } + if vectors.is_null() { + return Err(invalid("spin-vector array is null")); + } + // SAFETY: The caller supplies count readable records. + for vector in unsafe { slice::from_raw_parts(vectors, count) } { + if vector.leg == 0 || vector.point_count == 0 { + return Err(invalid("spin-vector leg and point count must be positive")); + } + let required = vector + .point_count + .checked_mul(8) + .ok_or_else(|| invalid("spin-vector size overflow"))?; + if vector.component_count != required { + return Err(invalid( + "spin-vector components must contain eight doubles per point", + )); + } + // SAFETY: This record's component buffer is readable for component_count doubles. + let flat = unsafe { + read_f64_slice(vector.components, vector.component_count, "spin components") + }?; + if flat.iter().any(|value| !value.is_finite()) { + return Err(invalid("spin-vector components must be finite")); + } + let points = flat + .chunks_exact(8) + .map(|point| std::array::from_fn(|mu| [point[2 * mu], point[2 * mu + 1]])) + .collect(); + if result.insert(vector.leg, points).is_some() { + return Err(invalid("a spin-vector array repeats an external leg")); + } + } + Ok(result) +} + +unsafe fn read_requests( + requests: *const RusticolCorrelatedRequest, + count: usize, +) -> AbiResult> { + if count == 0 || requests.is_null() { + return Err(invalid( + "a correlated evaluation requires a nonempty request array", + )); + } + // SAFETY: The caller supplies count readable records and their nested buffers. + unsafe { slice::from_raw_parts(requests, count) } + .iter() + .map(|request| { + let identifier = + unsafe { required_c_string(request.color_correlation, "colour correlation ID") }?; + if identifier.is_empty() { + return Err(invalid("colour correlation ID must not be empty")); + } + let spin_vectors = match request.use_default_spin_vectors { + 0 => Some(unsafe { + read_spin_vectors(request.spin_vectors, request.spin_vector_count) + }?), + 1 if request.spin_vector_count == 0 => None, + 1 => { + return Err(invalid( + "inherited spin vectors require an empty explicit array", + )); + } + _ => return Err(invalid("use_default_spin_vectors must be zero or one")), + }; + Ok(NativeCorrelatedRequest { + color_correlation: identifier.to_owned(), + spin_vectors, + }) + }) + .collect() +} + +/// Enumerate the selected process's generation-time correlation IDs. +/// +/// # Safety +/// handle must be live and exclusively accessible; output must be writable. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn rusticol_runtime_color_correlation_count( + handle: *mut RusticolRuntimeHandle, + output: *mut size_t, +) -> c_int { + guard(|| { + let handle = unsafe { required_handle_mut(handle) }?; + let ids = handle.runtime.color_correlation_ids()?; + unsafe { write_size(ids.len(), output, "colour correlation count") } + }) +} + +/// Copy one correlation ID. NULL/zero buffer queries its NUL-inclusive size. +/// +/// # Safety +/// handle must be live/exclusive; required and buffer follow write_string's contract. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn rusticol_runtime_color_correlation_id( + handle: *mut RusticolRuntimeHandle, + index: size_t, + buffer: *mut c_char, + capacity: size_t, + required: *mut size_t, +) -> c_int { + guard(|| { + let handle = unsafe { required_handle_mut(handle) }?; + let ids = handle.runtime.color_correlation_ids()?; + let value = ids + .get(index) + .ok_or_else(|| invalid("colour correlation index out of range"))?; + unsafe { write_string(value, buffer, capacity, required) } + }) +} + +/// Copy the full ordered catalogue as JSON, including generation-time histories. +/// +/// # Safety +/// Pointer requirements match rusticol_runtime_color_correlation_id. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn rusticol_runtime_color_correlation_catalogue_json( + handle: *mut RusticolRuntimeHandle, + buffer: *mut c_char, + capacity: size_t, + required: *mut size_t, +) -> c_int { + guard(|| { + let handle = unsafe { required_handle_mut(handle) }?; + let value = handle.runtime.available_color_correlations_json()?; + unsafe { write_string(&value, buffer, capacity, required) } + }) +} + +/// Copy literal spin-vector defaults. An empty array clears them. +/// +/// # Safety +/// handle must be live/exclusive; vectors and nested component buffers must be readable. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn rusticol_runtime_set_spin_correlation_vectors_f64( + handle: *mut RusticolRuntimeHandle, + vectors: *const RusticolSpinCorrelationVector, + vector_count: size_t, +) -> c_int { + guard(|| { + let handle = unsafe { required_handle_mut(handle) }?; + let vectors = unsafe { read_spin_vectors(vectors, vector_count) }?; + handle.runtime.set_spin_correlation_vectors_f64(vectors)?; + Ok(()) + }) +} + +/// Evaluate grouped colour/spin requests; result doubles are [request][point][real,imag]. +/// +/// # Safety +/// handle must be live/exclusive. All arrays must be readable for their stated counts; +/// nested request/vector/string buffers must remain live. output must be writable for +/// output_capacity doubles and must not overlap input buffers. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn rusticol_runtime_evaluate_correlated_many_f64( + handle: *mut RusticolRuntimeHandle, + momenta: *const c_double, + momentum_count: size_t, + point_count: size_t, + requests: *const RusticolCorrelatedRequest, + request_count: size_t, + helicity_ids: *const *const c_char, + helicity_count: size_t, + output: *mut c_double, + output_capacity: size_t, +) -> c_int { + guard(|| { + let required = request_count + .checked_mul(point_count) + .and_then(|n| n.checked_mul(2)) + .ok_or_else(|| invalid("correlated result size overflow"))?; + if point_count == 0 || request_count == 0 { + return Err(invalid( + "correlated request and point counts must be positive", + )); + } + validate_f64_output(output, output_capacity, required, "correlated output")?; + let handle = unsafe { required_handle_mut(handle) }?; + let momenta = unsafe { read_f64_slice(momenta, momentum_count, "momenta") }?; + let requests = unsafe { read_requests(requests, request_count) }?; + let helicities = + unsafe { read_selector_ids(helicity_ids, helicity_count, "helicity IDs") }?; + let values = handle.runtime.evaluate_correlated_many_f64( + momenta, + point_count, + &requests, + helicities.as_deref(), + )?; + if values.point_count != point_count + || values.request_count != request_count + || values.values.len().checked_mul(2) != Some(required) + { + return Err(abi_error( + RUSTICOL_STATUS_RUNTIME_ERROR, + "correlated runtime returned inconsistent dimensions", + )); + } + let flat: Vec = values.values.into_iter().flatten().collect(); + unsafe { write_f64_slice(&flat, output, output_capacity, "correlated output") } + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn spin_buffers_preserve_complex_layout_and_reject_duplicate_legs() { + let components = [1., 2., 3., 4., 5., 6., 7., 8.]; + let vector = RusticolSpinCorrelationVector { + leg: 3, + point_count: 1, + components: components.as_ptr(), + component_count: 8, + }; + let result = unsafe { read_spin_vectors(&vector, 1) }.ok().unwrap(); + assert_eq!(result[&3], vec![[[1., 2.], [3., 4.], [5., 6.], [7., 8.]]]); + let duplicated = [vector, vector]; + assert!(unsafe { read_spin_vectors(duplicated.as_ptr(), 2) }.is_err()); + assert!( + unsafe { read_spin_vectors(ptr::null(), 0) } + .ok() + .unwrap() + .is_empty() + ); + assert!(unsafe { read_spin_vectors(ptr::null(), 1) }.is_err()); + let wrong = RusticolSpinCorrelationVector { + component_count: 7, + ..vector + }; + assert!(unsafe { read_spin_vectors(&wrong, 1) }.is_err()); + } + + #[test] + fn requests_distinguish_defaults_from_physical_helicities() { + let request = RusticolCorrelatedRequest { + color_correlation: c"born".as_ptr(), + spin_vectors: ptr::null(), + spin_vector_count: 0, + use_default_spin_vectors: 1, + }; + assert!( + unsafe { read_requests(&request, 1) }.ok().unwrap()[0] + .spin_vectors + .is_none() + ); + let explicit = RusticolCorrelatedRequest { + use_default_spin_vectors: 0, + ..request + }; + assert!( + unsafe { read_requests(&explicit, 1) }.ok().unwrap()[0] + .spin_vectors + .as_ref() + .unwrap() + .is_empty() + ); + let invalid = RusticolCorrelatedRequest { + use_default_spin_vectors: 2, + ..request + }; + assert!(unsafe { read_requests(&invalid, 1) }.is_err()); + let mixed = RusticolCorrelatedRequest { + spin_vector_count: 1, + ..request + }; + assert!(unsafe { read_requests(&mixed, 1) }.is_err()); + assert!(unsafe { read_requests(ptr::null(), 0) }.is_err()); + } +} diff --git a/pyamplicol/rust/crates/rusticol-capi/src/lib.rs b/pyamplicol/rust/crates/rusticol-capi/src/lib.rs new file mode 100644 index 0000000000..2997422086 --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-capi/src/lib.rs @@ -0,0 +1,1960 @@ +// SPDX-License-Identifier: 0BSD + +use rusticol_core::{ + NativeCorrelatedRequest, NativeOnTheFlyWarmUpEvent, NativeOnTheFlyWarmUpEventKind, + NativeOnTheFlyWarmUpResult, NativeOnTheFlyWarmUpStage, NativeRuntime, + NativeSpinCorrelationVectors, RusticolError, RusticolErrorKind, supported_runtime_capabilities, +}; +use std::any::Any; +use std::cell::RefCell; +use std::collections::BTreeMap; +use std::ffi::{CStr, CString, c_char, c_double, c_int, c_void}; +use std::panic::{AssertUnwindSafe, catch_unwind}; +use std::path::Path; +use std::ptr; +use std::slice; + +mod correlated; + +#[allow(non_camel_case_types)] +type size_t = usize; + +pub const RUSTICOL_STATUS_OK: c_int = 0; +pub const RUSTICOL_STATUS_INVALID_ARGUMENT: c_int = 1; +pub const RUSTICOL_STATUS_BUFFER_TOO_SMALL: c_int = 2; +pub const RUSTICOL_STATUS_RUNTIME_ERROR: c_int = 3; +pub const RUSTICOL_STATUS_PANIC: c_int = 4; + +pub const RUSTICOL_WARM_UP_EVENT_START: u32 = 0; +pub const RUSTICOL_WARM_UP_EVENT_UPDATE: u32 = 1; +pub const RUSTICOL_WARM_UP_EVENT_END: u32 = 2; + +pub const RUSTICOL_WARM_UP_STAGE_PROCESS_PREPARATION: u32 = 0; +pub const RUSTICOL_WARM_UP_STAGE_QUERY_FAMILY: u32 = 1; +pub const RUSTICOL_WARM_UP_STAGE_FAMILY_FINALIZATION: u32 = 2; +pub const RUSTICOL_WARM_UP_STAGE_FIRST_EVALUATION: u32 = 3; + +/// Fixed-layout progress snapshot passed to native warm-up observers. +/// +/// `current_rss_bytes` and `peak_rss_bytes` are meaningful only when the +/// corresponding `*_available` field is nonzero. The event is borrowed and is +/// valid only for the duration of the callback. +#[repr(C)] +#[derive(Clone, Copy, Debug, Default, PartialEq)] +pub struct RusticolWarmUpProgressEvent { + pub schema_version: u32, + pub kind: u32, + pub stage: u32, + pub reserved: u32, + pub completed: u64, + pub total: u64, + pub elapsed_seconds: c_double, + pub current_rss_bytes: u64, + pub peak_rss_bytes: u64, + pub workers: u64, + pub current_rss_available: u32, + pub peak_rss_available: u32, +} + +/// Fixed-layout result from one explicit binary64 on-the-fly warm-up. +#[repr(C)] +#[derive(Clone, Copy, Debug, Default, PartialEq)] +pub struct RusticolWarmUpResult { + pub schema_version: u32, + pub reserved: u32, + pub elapsed_seconds: c_double, + pub query_count: u64, + pub warmed_query_count: u64, + pub current_rss_bytes: u64, + pub peak_rss_bytes: u64, + pub current_rss_available: u32, + pub peak_rss_available: u32, + pub already_warm: u32, + pub first_evaluation_completed: u32, +} + +/// Return nonzero to continue or zero to request cancellation. +/// +/// The callback runs on the coordinating caller thread. The terminal +/// first-evaluation `END` event is an informational post-commit notification; +/// returning zero for that one event does not undo a completed warm-up. +pub type RusticolWarmUpProgressCallback = Option< + unsafe extern "C" fn( + event: *const RusticolWarmUpProgressEvent, + user_data: *mut c_void, + ) -> c_int, +>; + +pub struct RusticolRuntimeHandle { + runtime: NativeRuntime, +} + +/// One borrowed literal complex spin-vector batch; see rusticol.h for layout. +#[repr(C)] +#[derive(Clone, Copy, Debug)] +pub struct RusticolSpinCorrelationVector { + pub leg: size_t, + pub point_count: size_t, + pub components: *const c_double, + pub component_count: size_t, +} + +/// One ordered colour insertion and either explicit or inherited spin vectors. +#[repr(C)] +#[derive(Clone, Copy, Debug)] +pub struct RusticolCorrelatedRequest { + pub color_correlation: *const c_char, + pub spin_vectors: *const RusticolSpinCorrelationVector, + pub spin_vector_count: size_t, + pub use_default_spin_vectors: u32, +} + +struct AbiError { + status: c_int, + message: String, +} + +type AbiResult = Result; + +impl From for AbiError { + fn from(error: RusticolError) -> Self { + let status = match error.kind() { + RusticolErrorKind::InvalidArgument + | RusticolErrorKind::Selector + | RusticolErrorKind::ModelParameter + | RusticolErrorKind::UnsupportedPrecision => RUSTICOL_STATUS_INVALID_ARGUMENT, + _ => RUSTICOL_STATUS_RUNTIME_ERROR, + }; + Self { + status, + message: error.to_string(), + } + } +} + +thread_local! { + static LAST_ERROR: RefCell = RefCell::new(CString::new("").expect("empty CString")); +} + +fn sanitize_c_string(message: impl AsRef) -> CString { + let sanitized = message.as_ref().replace('\0', "\\0"); + CString::new(sanitized) + .unwrap_or_else(|_| CString::new("Rusticol error").expect("literal CString")) +} + +fn set_last_error(message: impl AsRef) { + LAST_ERROR.with(|slot| *slot.borrow_mut() = sanitize_c_string(message)); +} + +fn guard(operation: impl FnOnce() -> AbiResult<()>) -> c_int { + finish_guard(catch_unwind(AssertUnwindSafe(operation))) +} + +fn finish_guard(result: Result, Box>) -> c_int { + match result { + Ok(Ok(())) => RUSTICOL_STATUS_OK, + Ok(Err(error)) => { + set_last_error(&error.message); + error.status + } + Err(payload) => { + let message = payload + .downcast_ref::<&str>() + .copied() + .or_else(|| payload.downcast_ref::().map(String::as_str)) + .unwrap_or("unknown Rust panic"); + set_last_error(format!("Rusticol panic: {message}")); + RUSTICOL_STATUS_PANIC + } + } +} + +fn abi_error(status: c_int, message: impl Into) -> AbiError { + AbiError { + status, + message: message.into(), + } +} + +fn invalid(message: impl Into) -> AbiError { + abi_error(RUSTICOL_STATUS_INVALID_ARGUMENT, message) +} + +fn buffer_too_small(message: impl Into) -> AbiError { + abi_error(RUSTICOL_STATUS_BUFFER_TOO_SMALL, message) +} + +unsafe fn required_handle<'a>( + handle: *const RusticolRuntimeHandle, +) -> AbiResult<&'a RusticolRuntimeHandle> { + if handle.is_null() { + return Err(invalid("Rusticol runtime handle is null")); + } + // SAFETY: The caller promises that a non-null handle was returned by rusticol_runtime_load + // and remains alive for the duration of this call. + Ok(unsafe { &*handle }) +} + +unsafe fn required_handle_mut<'a>( + handle: *mut RusticolRuntimeHandle, +) -> AbiResult<&'a mut RusticolRuntimeHandle> { + if handle.is_null() { + return Err(invalid("Rusticol runtime handle is null")); + } + // SAFETY: The ABI documents handles as mutable and non-concurrently callable. + Ok(unsafe { &mut *handle }) +} + +unsafe fn optional_c_string<'a>(value: *const c_char) -> AbiResult> { + if value.is_null() { + return Ok(None); + } + // SAFETY: The caller supplies a NUL-terminated string for non-null pointers. + let value = unsafe { CStr::from_ptr(value) } + .to_str() + .map_err(|error| invalid(format!("C string is not valid UTF-8: {error}")))?; + Ok(Some(value)) +} + +unsafe fn required_c_string<'a>(value: *const c_char, name: &str) -> AbiResult<&'a str> { + unsafe { optional_c_string(value) }?.ok_or_else(|| invalid(format!("{name} is null"))) +} + +unsafe fn write_size(value: usize, output: *mut size_t, name: &str) -> AbiResult<()> { + if output.is_null() { + return Err(invalid(format!("{name} is null"))); + } + // SAFETY: The caller supplies writable storage for one size_t. + unsafe { *output = value as size_t }; + Ok(()) +} + +unsafe fn write_i32(value: i32, output: *mut i32, name: &str) -> AbiResult<()> { + if output.is_null() { + return Err(invalid(format!("{name} is null"))); + } + // SAFETY: The caller supplies writable storage for one i32. + unsafe { *output = value }; + Ok(()) +} + +unsafe fn write_string( + value: &str, + buffer: *mut c_char, + capacity: size_t, + required: *mut size_t, +) -> AbiResult<()> { + let bytes = value.as_bytes(); + let required_capacity = bytes + .len() + .checked_add(1) + .ok_or_else(|| invalid("string length overflow"))?; + unsafe { write_size(required_capacity, required, "required string capacity")? }; + if buffer.is_null() { + if capacity == 0 { + return Ok(()); + } + return Err(invalid("string output buffer is null")); + } + if capacity < required_capacity { + return Err(buffer_too_small(format!( + "string output buffer has capacity {capacity}, requires {required_capacity}" + ))); + } + // SAFETY: Capacity was checked and the source does not overlap the caller's buffer. + unsafe { + ptr::copy_nonoverlapping(bytes.as_ptr(), buffer.cast::(), bytes.len()); + *buffer.add(bytes.len()) = 0; + } + Ok(()) +} + +unsafe fn read_selector_ids( + values: *const *const c_char, + count: size_t, + name: &str, +) -> AbiResult>> { + if count == 0 { + return Ok(None); + } + if values.is_null() { + return Err(invalid(format!("{name} array is null"))); + } + // SAFETY: The caller supplies count string pointers. + let values = unsafe { slice::from_raw_parts(values, count) }; + let mut result = Vec::with_capacity(count); + for (index, value) in values.iter().enumerate() { + result.push(unsafe { required_c_string(*value, &format!("{name}[{index}]")) }?.to_string()); + } + Ok(Some(result)) +} + +unsafe fn read_f64_slice<'a>( + values: *const c_double, + count: size_t, + name: &str, +) -> AbiResult<&'a [f64]> { + if count == 0 { + return Err(invalid(format!("{name} must not be empty"))); + } + if values.is_null() { + return Err(invalid(format!("{name} is null"))); + } + // SAFETY: The caller supplies count readable f64 values. + Ok(unsafe { slice::from_raw_parts(values, count) }) +} + +unsafe fn read_optional_u32_slice<'a>( + values: *const u32, + count: size_t, + expected_count: usize, + name: &str, +) -> AbiResult> { + if count == 0 { + return Ok(None); + } + if values.is_null() { + return Err(invalid(format!("{name} is null"))); + } + if count != expected_count { + return Err(invalid(format!( + "{name} has length {count}, expected {expected_count} (one selector per point)" + ))); + } + // SAFETY: The caller supplies count readable u32 values. + Ok(Some(unsafe { slice::from_raw_parts(values, count) })) +} + +unsafe fn write_f64_slice( + values: &[f64], + output: *mut c_double, + capacity: size_t, + name: &str, +) -> AbiResult<()> { + if output.is_null() { + return Err(invalid(format!("{name} is null"))); + } + if capacity < values.len() { + return Err(buffer_too_small(format!( + "{name} has capacity {capacity}, requires {}", + values.len() + ))); + } + // SAFETY: Capacity was checked and the source cannot overlap the caller-owned output. + unsafe { ptr::copy_nonoverlapping(values.as_ptr(), output, values.len()) }; + Ok(()) +} + +fn validate_f64_output( + output: *mut c_double, + capacity: size_t, + required: usize, + name: &str, +) -> AbiResult<()> { + if output.is_null() { + return Err(invalid(format!("{name} is null"))); + } + if capacity < required { + return Err(buffer_too_small(format!( + "{name} has capacity {capacity}, requires {required}" + ))); + } + Ok(()) +} + +fn warm_up_event_kind(kind: NativeOnTheFlyWarmUpEventKind) -> u32 { + match kind { + NativeOnTheFlyWarmUpEventKind::Start => RUSTICOL_WARM_UP_EVENT_START, + NativeOnTheFlyWarmUpEventKind::Update => RUSTICOL_WARM_UP_EVENT_UPDATE, + NativeOnTheFlyWarmUpEventKind::End => RUSTICOL_WARM_UP_EVENT_END, + } +} + +fn warm_up_stage(stage: NativeOnTheFlyWarmUpStage) -> u32 { + match stage { + NativeOnTheFlyWarmUpStage::ProcessPreparation => RUSTICOL_WARM_UP_STAGE_PROCESS_PREPARATION, + NativeOnTheFlyWarmUpStage::QueryFamily => RUSTICOL_WARM_UP_STAGE_QUERY_FAMILY, + NativeOnTheFlyWarmUpStage::FamilyFinalization => RUSTICOL_WARM_UP_STAGE_FAMILY_FINALIZATION, + NativeOnTheFlyWarmUpStage::FirstEvaluation => RUSTICOL_WARM_UP_STAGE_FIRST_EVALUATION, + } +} + +fn warm_up_progress_event(event: &NativeOnTheFlyWarmUpEvent) -> RusticolWarmUpProgressEvent { + RusticolWarmUpProgressEvent { + schema_version: event.schema_version, + kind: warm_up_event_kind(event.kind), + stage: warm_up_stage(event.stage), + reserved: 0, + completed: event.completed, + total: event.total, + elapsed_seconds: event.elapsed_seconds, + current_rss_bytes: event.current_rss_bytes.unwrap_or_default(), + peak_rss_bytes: event.peak_rss_bytes.unwrap_or_default(), + workers: event.workers, + current_rss_available: u32::from(event.current_rss_bytes.is_some()), + peak_rss_available: u32::from(event.peak_rss_bytes.is_some()), + } +} + +fn warm_up_result(result: NativeOnTheFlyWarmUpResult) -> RusticolWarmUpResult { + RusticolWarmUpResult { + schema_version: result.schema_version, + reserved: 0, + elapsed_seconds: result.elapsed_seconds, + query_count: result.query_count, + warmed_query_count: result.warmed_query_count, + current_rss_bytes: result.current_rss_bytes.unwrap_or_default(), + peak_rss_bytes: result.peak_rss_bytes.unwrap_or_default(), + current_rss_available: u32::from(result.current_rss_bytes.is_some()), + peak_rss_available: u32::from(result.peak_rss_bytes.is_some()), + already_warm: u32::from(result.already_warm), + first_evaluation_completed: u32::from(result.first_evaluation_completed), + } +} + +#[unsafe(no_mangle)] +pub extern "C" fn rusticol_abi_version() -> u32 { + catch_unwind(|| NativeRuntime::ABI_VERSION).unwrap_or(0) +} + +/// Returns the supported runtime capabilities as JSON. +/// +/// # Safety +/// +/// If non-null, `required` must be writable for one `size_t`. If non-null, `buffer` must be +/// writable for `capacity` bytes. A null `buffer` is valid only for a zero-capacity query. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn rusticol_supported_runtime_capabilities_json( + buffer: *mut c_char, + capacity: size_t, + required: *mut size_t, +) -> c_int { + guard(|| { + let json = serde_json::to_string(&supported_runtime_capabilities()).map_err(|error| { + AbiError::from(RusticolError::serialization(format!( + "could not serialize supported runtime capabilities: {error}" + ))) + })?; + // SAFETY: Pointer validation and copying are performed by write_string. + unsafe { write_string(&json, buffer, capacity, required) } + }) +} + +/// Copies the calling thread's last Rusticol error message. +/// +/// # Safety +/// +/// If non-null, `required` must be writable for one `size_t`. If non-null, `buffer` must be +/// writable for `capacity` bytes. A null `buffer` is valid only for a zero-capacity query. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn rusticol_last_error_message( + buffer: *mut c_char, + capacity: size_t, + required: *mut size_t, +) -> c_int { + match catch_unwind(AssertUnwindSafe(|| { + LAST_ERROR.with(|slot| { + let value = slot.borrow(); + // SAFETY: Pointer validation and copying are performed by write_string. + unsafe { + write_string( + value.to_str().unwrap_or("Rusticol error"), + buffer, + capacity, + required, + ) + } + }) + })) { + Ok(Ok(())) => RUSTICOL_STATUS_OK, + Ok(Err(error)) => { + set_last_error(&error.message); + error.status + } + Err(_) => { + set_last_error("Rusticol panic while retrieving the last error message"); + RUSTICOL_STATUS_PANIC + } + } +} + +/// Loads a Rusticol runtime and returns an owning opaque handle. +/// +/// # Safety +/// +/// Every non-null string pointer must reference a readable NUL-terminated string for the duration +/// of the call. If non-null, `output` must be writable for one handle pointer. A handle returned +/// through `output` must eventually be released exactly once with [`rusticol_runtime_free`]. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn rusticol_runtime_load( + process_dir: *const c_char, + process_key: *const c_char, + model_parameters_path: *const c_char, + output: *mut *mut RusticolRuntimeHandle, +) -> c_int { + guard(|| { + if output.is_null() { + return Err(invalid("runtime output handle is null")); + } + // SAFETY: The caller supplies writable handle storage. + unsafe { *output = ptr::null_mut() }; + // SAFETY: String pointers follow the ABI contract; output is checked above. + let process_dir = unsafe { required_c_string(process_dir, "process_dir") }?; + let process_key = unsafe { optional_c_string(process_key) }?; + let model_parameters = unsafe { optional_c_string(model_parameters_path) }?; + let runtime = + NativeRuntime::load(process_dir, process_key, model_parameters.map(Path::new))?; + let boxed = Box::new(RusticolRuntimeHandle { runtime }); + // SAFETY: output points to writable handle storage supplied by the caller. + unsafe { *output = Box::into_raw(boxed) }; + Ok(()) + }) +} + +/// Saves the completed, retained OTF recursion cache to a file. +/// +/// # Safety +/// +/// `handle` must be a live runtime handle, and `path` must reference a readable +/// NUL-terminated UTF-8 string. No other call on this handle may overlap this call. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn rusticol_runtime_save( + handle: *const RusticolRuntimeHandle, + path: *const c_char, +) -> c_int { + guard(|| { + // SAFETY: The handle and string follow the ABI contract above. + let runtime = unsafe { required_handle(handle) }?; + let path = unsafe { required_c_string(path, "cache path") }?; + runtime.runtime.save(path)?; + Ok(()) + }) +} + +/// Restores a saved OTF recursion cache into an already-loaded matching process. +/// +/// # Safety +/// +/// `handle` must be a live runtime handle, and `path` must reference a readable +/// NUL-terminated UTF-8 string. No other call on this handle may overlap this call. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn rusticol_runtime_load_cache( + handle: *mut RusticolRuntimeHandle, + path: *const c_char, +) -> c_int { + guard(|| { + // SAFETY: The handle and string follow the ABI contract above. + let runtime = unsafe { required_handle_mut(handle) }?; + let path = unsafe { required_c_string(path, "cache path") }?; + runtime.runtime.load_cache(path)?; + Ok(()) + }) +} + +/// Releases a Rusticol runtime handle. +/// +/// # Safety +/// +/// `handle` must be null or a live handle returned by [`rusticol_runtime_load`] that has not +/// already been freed. No other access to a non-null handle may overlap this call. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn rusticol_runtime_free(handle: *mut RusticolRuntimeHandle) -> c_int { + guard(|| { + if !handle.is_null() { + // SAFETY: The caller must free a live handle exactly once. + unsafe { drop(Box::from_raw(handle)) }; + } + Ok(()) + }) +} + +unsafe fn runtime_string( + handle: *const RusticolRuntimeHandle, + buffer: *mut c_char, + capacity: size_t, + required: *mut size_t, + get: impl FnOnce(&NativeRuntime) -> AbiResult, +) -> c_int { + guard(|| { + // SAFETY: Handle and output pointers are validated by helpers. + let handle = unsafe { required_handle(handle) }?; + let value = get(&handle.runtime)?; + unsafe { write_string(&value, buffer, capacity, required) } + }) +} + +/// Copies the existing content identity of the authenticated artifact manifest. +/// +/// # Safety +/// +/// A non-null `handle` must remain live and available for shared access during the call. If +/// non-null, `required` must be writable for one `size_t`, and `buffer` must be writable for +/// `capacity` bytes. A null `buffer` is valid only for a zero-capacity query. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn rusticol_runtime_artifact_id( + handle: *const RusticolRuntimeHandle, + buffer: *mut c_char, + capacity: size_t, + required: *mut size_t, +) -> c_int { + // SAFETY: The caller upholds this function's pointer contract. + unsafe { + runtime_string(handle, buffer, capacity, required, |runtime| { + Ok(runtime.artifact_id().to_owned()) + }) + } +} + +/// Copies the runtime metadata as JSON. +/// +/// # Safety +/// +/// A non-null `handle` must remain live and available for shared access during the call. If +/// non-null, `required` must be writable for one `size_t`, and `buffer` must be writable for +/// `capacity` bytes. A null `buffer` is valid only for a zero-capacity query. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn rusticol_runtime_metadata_json( + handle: *const RusticolRuntimeHandle, + buffer: *mut c_char, + capacity: size_t, + required: *mut size_t, +) -> c_int { + // SAFETY: The caller upholds this function's pointer contract. + unsafe { + runtime_string(handle, buffer, capacity, required, |runtime| { + Ok(runtime.metadata_json()?) + }) + } +} + +/// Copies the runtime physics metadata as JSON. +/// +/// # Safety +/// +/// A non-null `handle` must remain live and available for shared access during the call. If +/// non-null, `required` must be writable for one `size_t`, and `buffer` must be writable for +/// `capacity` bytes. A null `buffer` is valid only for a zero-capacity query. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn rusticol_runtime_physics_json( + handle: *const RusticolRuntimeHandle, + buffer: *mut c_char, + capacity: size_t, + required: *mut size_t, +) -> c_int { + // SAFETY: The caller upholds this function's pointer contract. + unsafe { + runtime_string(handle, buffer, capacity, required, |runtime| { + Ok(runtime.physics_json()?) + }) + } +} + +/// Copies the runtime process name. +/// +/// # Safety +/// +/// A non-null `handle` must remain live and available for shared access during the call. If +/// non-null, `required` must be writable for one `size_t`, and `buffer` must be writable for +/// `capacity` bytes. A null `buffer` is valid only for a zero-capacity query. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn rusticol_runtime_process( + handle: *const RusticolRuntimeHandle, + buffer: *mut c_char, + capacity: size_t, + required: *mut size_t, +) -> c_int { + // SAFETY: The caller upholds this function's pointer contract. + unsafe { + runtime_string(handle, buffer, capacity, required, |runtime| { + Ok(runtime.metadata().process) + }) + } +} + +/// Copies the runtime process key. +/// +/// # Safety +/// +/// A non-null `handle` must remain live and available for shared access during the call. If +/// non-null, `required` must be writable for one `size_t`, and `buffer` must be writable for +/// `capacity` bytes. A null `buffer` is valid only for a zero-capacity query. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn rusticol_runtime_process_key( + handle: *const RusticolRuntimeHandle, + buffer: *mut c_char, + capacity: size_t, + required: *mut size_t, +) -> c_int { + // SAFETY: The caller upholds this function's pointer contract. + unsafe { + runtime_string(handle, buffer, capacity, required, |runtime| { + Ok(runtime.metadata().process_key) + }) + } +} + +/// Copies the stable representative process key implementing this selection. +/// +/// # Safety +/// +/// Pointer requirements are identical to [`rusticol_runtime_process_key`]. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn rusticol_runtime_representative_process_key( + handle: *const RusticolRuntimeHandle, + buffer: *mut c_char, + capacity: size_t, + required: *mut size_t, +) -> c_int { + // SAFETY: The caller upholds this function's pointer contract. + unsafe { + runtime_string(handle, buffer, capacity, required, |runtime| { + Ok(runtime.metadata().representative_process_key) + }) + } +} + +/// Copies the runtime color-accuracy label. +/// +/// # Safety +/// +/// A non-null `handle` must remain live and available for shared access during the call. If +/// non-null, `required` must be writable for one `size_t`, and `buffer` must be writable for +/// `capacity` bytes. A null `buffer` is valid only for a zero-capacity query. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn rusticol_runtime_color_accuracy( + handle: *const RusticolRuntimeHandle, + buffer: *mut c_char, + capacity: size_t, + required: *mut size_t, +) -> c_int { + // SAFETY: The caller upholds this function's pointer contract. + unsafe { + runtime_string(handle, buffer, capacity, required, |runtime| { + Ok(runtime.metadata().color_accuracy) + }) + } +} + +/// Writes the number of external particles. +/// +/// # Safety +/// +/// A non-null `handle` must remain live and available for shared access during the call. If +/// non-null, `output` must be writable for one `size_t`. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn rusticol_runtime_external_count( + handle: *const RusticolRuntimeHandle, + output: *mut size_t, +) -> c_int { + guard(|| { + // SAFETY: Helpers validate pointers. + let handle = unsafe { required_handle(handle) }?; + unsafe { write_size(handle.runtime.external_count(), output, "external count") } + }) +} + +/// Copies the runtime execution mode (`compiled`, `eager`, or `recurrence`). +/// +/// # Safety +/// +/// `handle` must be a live handle returned by [`rusticol_runtime_load`]. If non-null, +/// `required` must be writable for one `size_t`. If non-null, `buffer` must be writable for +/// `capacity` bytes. A null `buffer` is valid only for a zero-capacity query. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn rusticol_runtime_execution_mode( + handle: *const RusticolRuntimeHandle, + buffer: *mut c_char, + capacity: size_t, + required: *mut size_t, +) -> c_int { + guard(|| { + // SAFETY: The ABI requires a live handle for this call. + let handle = unsafe { required_handle(handle) }?; + let execution_mode = handle.runtime.metadata().execution_mode; + // SAFETY: Pointer validation and copying are performed by write_string. + unsafe { write_string(&execution_mode, buffer, capacity, required) } + }) +} + +/// Writes the PDG identifier for one external particle. +/// +/// # Safety +/// +/// A non-null `handle` must remain live and available for shared access during the call. If +/// non-null, `output` must be writable for one `i32`. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn rusticol_runtime_external_pdg( + handle: *const RusticolRuntimeHandle, + index: size_t, + output: *mut i32, +) -> c_int { + guard(|| { + // SAFETY: Helpers validate pointers. + let handle = unsafe { required_handle(handle) }?; + let particles = handle.runtime.external_particles()?; + let particle = particles + .get(index) + .ok_or_else(|| invalid(format!("external particle index {index} is out of range")))?; + unsafe { write_i32(particle.pdg, output, "external PDG output") } + }) +} + +/// Copies the representative-index to public-index external permutation. +/// +/// # Safety +/// +/// A non-null `handle` must remain live. If non-null, `required` must be writable for one +/// `size_t`; `output` must be writable for `capacity` `size_t` values. A null output is valid +/// only for a zero-capacity query. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn rusticol_runtime_external_permutation( + handle: *const RusticolRuntimeHandle, + output: *mut size_t, + capacity: size_t, + required: *mut size_t, +) -> c_int { + guard(|| { + // SAFETY: Helpers validate pointers. + let handle = unsafe { required_handle(handle) }?; + let permutation = handle.runtime.metadata().external_permutation; + unsafe { write_size(permutation.len(), required, "external permutation length")? }; + if output.is_null() { + if capacity == 0 { + return Ok(()); + } + return Err(invalid("external permutation output is null")); + } + if capacity < permutation.len() { + return Err(buffer_too_small(format!( + "external permutation capacity {capacity} is smaller than {}", + permutation.len() + ))); + } + // SAFETY: Capacity was checked and usize matches C size_t. + unsafe { ptr::copy_nonoverlapping(permutation.as_ptr(), output, permutation.len()) }; + Ok(()) + }) +} + +/// Writes the exact factor and charge conjugation relating the selected process to the +/// representative that evaluates it: `ME = numerator / denominator * ME_representative`. +/// Selections other than non-generated process-group members report `1/1` and `0`. +/// +/// # Safety +/// +/// A non-null `handle` must remain live. Each output must be writable for one value. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn rusticol_runtime_process_group_relation( + handle: *const RusticolRuntimeHandle, + numerator: *mut u64, + denominator: *mut u64, + conjugated: *mut i32, +) -> c_int { + guard(|| { + // SAFETY: Helpers validate pointers. + let handle = unsafe { required_handle(handle) }?; + if numerator.is_null() || denominator.is_null() { + return Err(invalid("process-group factor output is null")); + } + let factor = handle.runtime.process_group_factor(); + // SAFETY: The caller supplies writable storage for one u64 each. + unsafe { + *numerator = factor.numerator; + *denominator = factor.denominator; + } + unsafe { + write_i32( + i32::from(handle.runtime.charge_conjugated()), + conjugated, + "charge-conjugation output", + ) + } + }) +} + +/// Loads one JSON kinematic point in public/requested external order. +/// +/// # Safety +/// +/// `path` must reference a readable NUL-terminated string. Other pointer requirements match +/// [`rusticol_runtime_external_permutation`], with `output` containing f64 values. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn rusticol_runtime_load_kinematics_json( + handle: *const RusticolRuntimeHandle, + path: *const c_char, + output: *mut c_double, + capacity: size_t, + required: *mut size_t, +) -> c_int { + guard(|| { + // SAFETY: Helpers validate pointers. + let handle = unsafe { required_handle(handle) }?; + let value_count = handle + .runtime + .external_count() + .checked_mul(4) + .ok_or_else(|| invalid("kinematics value count overflows size_t"))?; + unsafe { write_size(value_count, required, "kinematics value count")? }; + if output.is_null() { + if capacity == 0 { + return Ok(()); + } + return Err(invalid("kinematics output is null")); + } + if capacity < value_count { + return Err(buffer_too_small(format!( + "kinematics capacity {capacity} is smaller than {}", + value_count + ))); + } + let path = unsafe { required_c_string(path, "kinematics path") }?; + let values = handle.runtime.load_kinematics_json(Path::new(path))?; + if values.len() != value_count { + return Err(invalid(format!( + "kinematics JSON contains {} values, expected {value_count}", + values.len() + ))); + } + // SAFETY: Capacity was checked. + unsafe { ptr::copy_nonoverlapping(values.as_ptr(), output, value_count) }; + Ok(()) + }) +} + +/// Writes the number of helicity configurations. +/// +/// # Safety +/// +/// A non-null `handle` must remain live and available for shared access during the call. If +/// non-null, `output` must be writable for one `size_t`. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn rusticol_runtime_helicity_count( + handle: *const RusticolRuntimeHandle, + output: *mut size_t, +) -> c_int { + guard(|| { + // SAFETY: Helpers validate pointers. + let handle = unsafe { required_handle(handle) }?; + unsafe { write_size(handle.runtime.helicities()?.len(), output, "helicity count") } + }) +} + +/// Copies a helicity identifier. +/// +/// # Safety +/// +/// A non-null `handle` must remain live and available for shared access during the call. If +/// non-null, `required` must be writable for one `size_t`, and `buffer` must be writable for +/// `capacity` bytes. A null `buffer` is valid only for a zero-capacity query. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn rusticol_runtime_helicity_id( + handle: *const RusticolRuntimeHandle, + index: size_t, + buffer: *mut c_char, + capacity: size_t, + required: *mut size_t, +) -> c_int { + // SAFETY: The caller upholds this function's pointer contract. + unsafe { + runtime_string(handle, buffer, capacity, required, |runtime| { + runtime + .helicities()? + .get(index) + .map(|item| item.id.clone()) + .ok_or_else(|| invalid(format!("helicity index {index} is out of range"))) + }) + } +} + +/// Copies one helicity vector. +/// +/// # Safety +/// +/// A non-null `handle` must remain live and available for shared access during the call. If +/// non-null, `required` must be writable for one `size_t`, and `output` must be writable for +/// `capacity` `i32` values. A null `output` is valid only for a zero-capacity query. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn rusticol_runtime_helicity_vector( + handle: *const RusticolRuntimeHandle, + index: size_t, + output: *mut i32, + capacity: size_t, + required: *mut size_t, +) -> c_int { + guard(|| { + // SAFETY: Helpers validate pointers. + let handle = unsafe { required_handle(handle) }?; + let helicities = handle.runtime.helicities()?; + let item = helicities + .get(index) + .ok_or_else(|| invalid(format!("helicity index {index} is out of range")))?; + unsafe { write_size(item.helicities.len(), required, "helicity vector length")? }; + if output.is_null() { + if capacity == 0 { + return Ok(()); + } + return Err(invalid("helicity vector output is null")); + } + if capacity < item.helicities.len() { + return Err(buffer_too_small(format!( + "helicity vector capacity {capacity} is smaller than {}", + item.helicities.len() + ))); + } + // SAFETY: Capacity was checked. + unsafe { + ptr::copy_nonoverlapping(item.helicities.as_ptr(), output, item.helicities.len()) + }; + Ok(()) + }) +} + +/// Writes the number of color components. +/// +/// # Safety +/// +/// A non-null `handle` must remain live and available for shared access during the call. If +/// non-null, `output` must be writable for one `size_t`. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn rusticol_runtime_color_count( + handle: *const RusticolRuntimeHandle, + output: *mut size_t, +) -> c_int { + guard(|| { + // SAFETY: Helpers validate pointers. + let handle = unsafe { required_handle(handle) }?; + unsafe { + write_size( + handle.runtime.color_components()?.len(), + output, + "color count", + ) + } + }) +} + +/// Copies a color-component identifier. +/// +/// # Safety +/// +/// A non-null `handle` must remain live and available for shared access during the call. If +/// non-null, `required` must be writable for one `size_t`, and `buffer` must be writable for +/// `capacity` bytes. A null `buffer` is valid only for a zero-capacity query. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn rusticol_runtime_color_id( + handle: *const RusticolRuntimeHandle, + index: size_t, + buffer: *mut c_char, + capacity: size_t, + required: *mut size_t, +) -> c_int { + // SAFETY: The caller upholds this function's pointer contract. + unsafe { + runtime_string(handle, buffer, capacity, required, |runtime| { + runtime + .color_components()? + .get(index) + .map(|item| item.id.clone()) + .ok_or_else(|| invalid(format!("color index {index} is out of range"))) + }) + } +} + +/// Copies a color-component kind. +/// +/// # Safety +/// +/// A non-null `handle` must remain live and available for shared access during the call. If +/// non-null, `required` must be writable for one `size_t`, and `buffer` must be writable for +/// `capacity` bytes. A null `buffer` is valid only for a zero-capacity query. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn rusticol_runtime_color_kind( + handle: *const RusticolRuntimeHandle, + index: size_t, + buffer: *mut c_char, + capacity: size_t, + required: *mut size_t, +) -> c_int { + // SAFETY: The caller upholds this function's pointer contract. + unsafe { + runtime_string(handle, buffer, capacity, required, |runtime| { + runtime + .color_components()? + .get(index) + .map(|item| item.kind.clone()) + .ok_or_else(|| invalid(format!("color index {index} is out of range"))) + }) + } +} + +/// Copies one color word. +/// +/// # Safety +/// +/// A non-null `handle` must remain live and available for shared access during the call. If +/// non-null, `required` must be writable for one `size_t`, and `output` must be writable for +/// `capacity` `size_t` values. A null `output` is valid only for a zero-capacity query. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn rusticol_runtime_color_word( + handle: *const RusticolRuntimeHandle, + index: size_t, + output: *mut size_t, + capacity: size_t, + required: *mut size_t, +) -> c_int { + guard(|| { + // SAFETY: Helpers validate pointers. + let handle = unsafe { required_handle(handle) }?; + let colors = handle.runtime.color_components()?; + let item = colors + .get(index) + .ok_or_else(|| invalid(format!("color index {index} is out of range")))?; + unsafe { write_size(item.word.len(), required, "color word length")? }; + if output.is_null() { + if capacity == 0 { + return Ok(()); + } + return Err(invalid("color word output is null")); + } + if capacity < item.word.len() { + return Err(buffer_too_small(format!( + "color word capacity {capacity} is smaller than {}", + item.word.len() + ))); + } + // SAFETY: Capacity was checked and usize matches C size_t. + unsafe { ptr::copy_nonoverlapping(item.word.as_ptr(), output, item.word.len()) }; + Ok(()) + }) +} + +/// Writes the number of model parameters. +/// +/// # Safety +/// +/// A non-null `handle` must remain live and available for shared access during the call. If +/// non-null, `output` must be writable for one `size_t`. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn rusticol_runtime_model_parameter_count( + handle: *const RusticolRuntimeHandle, + output: *mut size_t, +) -> c_int { + guard(|| { + // SAFETY: Helpers validate pointers. + let handle = unsafe { required_handle(handle) }?; + unsafe { + write_size( + handle.runtime.model_parameters()?.len(), + output, + "model parameter count", + ) + } + }) +} + +/// Copies a model-parameter name. +/// +/// # Safety +/// +/// A non-null `handle` must remain live and available for shared access during the call. If +/// non-null, `required` must be writable for one `size_t`, and `buffer` must be writable for +/// `capacity` bytes. A null `buffer` is valid only for a zero-capacity query. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn rusticol_runtime_model_parameter_name( + handle: *const RusticolRuntimeHandle, + index: size_t, + buffer: *mut c_char, + capacity: size_t, + required: *mut size_t, +) -> c_int { + // SAFETY: The caller upholds this function's pointer contract. + unsafe { + runtime_string(handle, buffer, capacity, required, |runtime| { + runtime + .model_parameters()? + .get(index) + .map(|item| item.name.clone()) + .ok_or_else(|| invalid(format!("model parameter index {index} is out of range"))) + }) + } +} + +/// Writes the resolved helicity and color dimensions for a selector set. +/// +/// # Safety +/// +/// A non-null `handle` must remain live and available for shared access during the call. For each +/// nonzero selector count, the corresponding pointer must reference that many readable string +/// pointers, and every non-null string pointer must reference a readable NUL-terminated string. +/// Non-null output pointers must each be writable for one `size_t`. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn rusticol_runtime_resolved_shape( + handle: *const RusticolRuntimeHandle, + helicity_ids: *const *const c_char, + helicity_count: size_t, + color_ids: *const *const c_char, + color_count: size_t, + output_helicity_count: *mut size_t, + output_color_count: *mut size_t, +) -> c_int { + guard(|| { + // SAFETY: Helpers validate pointers and arrays. + let handle = unsafe { required_handle(handle) }?; + let helicities = + unsafe { read_selector_ids(helicity_ids, helicity_count, "helicity ids") }?; + let colors = unsafe { read_selector_ids(color_ids, color_count, "color ids") }?; + let (helicity_count, color_count) = handle + .runtime + .resolved_shape(helicities.as_deref(), colors.as_deref())?; + unsafe { + write_size( + helicity_count, + output_helicity_count, + "resolved helicity count", + )?; + write_size(color_count, output_color_count, "resolved color count") + } + }) +} + +/// Warms one binary64 point using the process-output construction default. +/// +/// This preserves the original C ABI and forwards to +/// [`rusticol_runtime_warm_up_f64_with_cores`] with `n_cores=0`. +/// +/// # Safety +/// +/// The same pointer, storage, and callback requirements as +/// [`rusticol_runtime_warm_up_f64_with_cores`] apply. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn rusticol_runtime_warm_up_f64( + handle: *mut RusticolRuntimeHandle, + momenta: *const c_double, + momentum_count: size_t, + helicity_ids: *const *const c_char, + helicity_count: size_t, + color_ids: *const *const c_char, + color_count: size_t, + progress_callback: RusticolWarmUpProgressCallback, + progress_user_data: *mut c_void, + output: *mut RusticolWarmUpResult, +) -> c_int { + // SAFETY: This forwards the caller's storage and callback unchanged. + unsafe { + rusticol_runtime_warm_up_f64_with_cores( + handle, + momenta, + momentum_count, + helicity_ids, + helicity_count, + color_ids, + color_count, + 0, + progress_callback, + progress_user_data, + output, + ) + } +} + +/// Explicitly warms one binary64 point for an on-the-fly runtime selection. +/// +/// This constructs and retains the selected OTF query family, performs exactly one evaluation, +/// and returns a fixed-layout summary. Global selector arrays contain physical string IDs; an +/// omitted selector axis is summed over every component retained by the artifact. Progress is +/// reported only from the coordinating caller thread and is throttled by the core runtime. +/// Returning zero from the callback requests cancellation at a cancellable cold-path boundary. +/// The terminal first-evaluation `END` event is post-commit and informational. +/// A zero `n_cores` uses the process-output construction default; a positive value overrides +/// construction workers for this warm-up only, without changing evaluation or cache identity. +/// +/// # Safety +/// +/// A non-null `handle` must remain live and exclusively accessible during the call. `momenta` +/// must reference `momentum_count` readable `f64` values containing exactly one point in +/// `[external particle][E, px, py, pz]` order. For each nonzero selector count, the corresponding +/// pointer must reference that many readable NUL-terminated string pointers. `output` must be +/// writable for one [`RusticolWarmUpResult`]. If supplied, `progress_callback` must remain callable +/// for the duration of this function and may use `progress_user_data`; it must not call the same +/// runtime handle recursively. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn rusticol_runtime_warm_up_f64_with_cores( + handle: *mut RusticolRuntimeHandle, + momenta: *const c_double, + momentum_count: size_t, + helicity_ids: *const *const c_char, + helicity_count: size_t, + color_ids: *const *const c_char, + color_count: size_t, + n_cores: size_t, + progress_callback: RusticolWarmUpProgressCallback, + progress_user_data: *mut c_void, + output: *mut RusticolWarmUpResult, +) -> c_int { + guard(|| { + if output.is_null() { + return Err(invalid("warm-up result output is null")); + } + // SAFETY: The caller supplies writable result storage. Initialize it before any + // fallible operation so failures never leave stale success data behind. + unsafe { *output = RusticolWarmUpResult::default() }; + + // SAFETY: Helpers validate the handle and all input arrays. + let handle = unsafe { required_handle_mut(handle) }?; + let momenta = unsafe { read_f64_slice(momenta, momentum_count, "warm-up momenta") }?; + let helicities = + unsafe { read_selector_ids(helicity_ids, helicity_count, "helicity ids") }?; + let colors = unsafe { read_selector_ids(color_ids, color_count, "color ids") }?; + let n_cores = (n_cores != 0).then_some(n_cores); + + let result = if let Some(callback) = progress_callback { + let mut observer = |event: &NativeOnTheFlyWarmUpEvent| { + let event = warm_up_progress_event(event); + // SAFETY: The caller guarantees that the callback and user-data pointer remain + // valid for this call. The borrowed event lives through the callback only. + Ok(unsafe { callback(&event, progress_user_data) } != 0) + }; + handle.runtime.warm_up_f64( + momenta, + helicities.as_deref(), + colors.as_deref(), + n_cores, + Some(&mut observer), + ) + } else { + handle.runtime.warm_up_f64( + momenta, + helicities.as_deref(), + colors.as_deref(), + n_cores, + None, + ) + } + .map_err(AbiError::from)?; + + // SAFETY: The non-null output was validated before invoking the runtime. + unsafe { *output = warm_up_result(result) }; + Ok(()) + }) +} + +/// Evaluates total f64 matrix elements for a batch of momentum points. +/// +/// # Safety +/// +/// A non-null `handle` must remain live and exclusively accessible during the call. If non-null, +/// `momenta` must reference `momentum_count` readable `f64` values and `output` must reference +/// `output_capacity` writable `f64` values. The input and output regions must not overlap. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn rusticol_runtime_evaluate_f64( + handle: *mut RusticolRuntimeHandle, + momenta: *const c_double, + momentum_count: size_t, + point_count: size_t, + output: *mut c_double, + output_capacity: size_t, +) -> c_int { + guard(|| { + // SAFETY: Helpers validate all pointers. + let handle = unsafe { required_handle_mut(handle) }?; + let momenta = unsafe { read_f64_slice(momenta, momentum_count, "momenta") }?; + validate_f64_output(output, output_capacity, point_count, "total output")?; + // SAFETY: The output pointer is non-null and has at least point_count writable values. + // Restrict the mutable slice to the exact result prefix so excess caller capacity and + // trailing canaries remain untouched. + let output = unsafe { slice::from_raw_parts_mut(output, point_count) }; + handle + .runtime + .evaluate_f64_into(momenta, point_count, output) + .map_err(AbiError::from) + }) +} + +/// Evaluates total f64 matrix elements with optional global or per-point selectors. +/// +/// Global selector arrays contain physical helicity/color string IDs and retain subset/sum +/// semantics. Per-point selector arrays contain zero-based physical-axis indices and must contain +/// exactly one selector for every input point. Global and per-point selectors are mutually +/// exclusive on the same axis. An omitted axis is summed over every component retained by the +/// artifact. +/// +/// # Safety +/// +/// A non-null `handle` must remain live and exclusively accessible during the call. `momenta` must +/// reference `momentum_count` readable `f64` values and `output` must reference `output_capacity` +/// writable `f64` values; those regions must not overlap. For each nonzero global selector count, +/// the corresponding pointer must reference that many readable string pointers and each string +/// must be NUL-terminated. For each nonzero per-point selector count, the corresponding pointer +/// must reference exactly `point_count` readable `u32` values. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn rusticol_runtime_evaluate_selected_f64( + handle: *mut RusticolRuntimeHandle, + momenta: *const c_double, + momentum_count: size_t, + point_count: size_t, + helicity_ids: *const *const c_char, + helicity_count: size_t, + color_ids: *const *const c_char, + color_count: size_t, + helicity_by_point: *const u32, + helicity_by_point_count: size_t, + color_flow_by_point: *const u32, + color_flow_by_point_count: size_t, + output: *mut c_double, + output_capacity: size_t, +) -> c_int { + guard(|| { + // SAFETY: Helpers validate all pointers and arrays. + let handle = unsafe { required_handle_mut(handle) }?; + if point_count == 0 { + return Err(invalid("point_count must be positive")); + } + let momenta = unsafe { read_f64_slice(momenta, momentum_count, "momenta") }?; + let helicities = + unsafe { read_selector_ids(helicity_ids, helicity_count, "helicity ids") }?; + let colors = unsafe { read_selector_ids(color_ids, color_count, "color ids") }?; + let helicity_by_point = unsafe { + read_optional_u32_slice( + helicity_by_point, + helicity_by_point_count, + point_count, + "helicity_by_point", + ) + }?; + let color_flow_by_point = unsafe { + read_optional_u32_slice( + color_flow_by_point, + color_flow_by_point_count, + point_count, + "color_flow_by_point", + ) + }?; + if helicities.is_some() && helicity_by_point.is_some() { + return Err(invalid( + "helicity ids and helicity_by_point are mutually exclusive", + )); + } + if colors.is_some() && color_flow_by_point.is_some() { + return Err(invalid( + "color ids and color_flow_by_point are mutually exclusive", + )); + } + validate_f64_output(output, output_capacity, point_count, "total output")?; + // SAFETY: The output pointer is non-null and has at least point_count writable values. + // Restrict the mutable slice to the exact result prefix so excess caller capacity and + // trailing canaries remain untouched. + let output = unsafe { slice::from_raw_parts_mut(output, point_count) }; + handle + .runtime + .evaluate_f64_into_with_selectors( + momenta, + point_count, + helicities.as_deref(), + colors.as_deref(), + helicity_by_point, + color_flow_by_point, + output, + ) + .map_err(AbiError::from) + }) +} + +/// Evaluates resolved f64 matrix elements for a batch of momentum points. +/// +/// # Safety +/// +/// A non-null `handle` must remain live and exclusively accessible during the call. If non-null, +/// `momenta` must reference `momentum_count` readable `f64` values and `output` must reference +/// `output_capacity` writable `f64` values; those regions must not overlap. For each nonzero +/// selector count, the corresponding pointer must reference that many readable string pointers, +/// and every non-null string pointer must reference a readable NUL-terminated string. Non-null +/// shape outputs must each be writable for one `size_t`. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn rusticol_runtime_evaluate_resolved_f64( + handle: *mut RusticolRuntimeHandle, + momenta: *const c_double, + momentum_count: size_t, + point_count: size_t, + helicity_ids: *const *const c_char, + helicity_count: size_t, + color_ids: *const *const c_char, + color_count: size_t, + output: *mut c_double, + output_capacity: size_t, + output_helicity_count: *mut size_t, + output_color_count: *mut size_t, +) -> c_int { + guard(|| { + // SAFETY: Helpers validate all pointers and arrays. + let handle = unsafe { required_handle_mut(handle) }?; + let momenta = unsafe { read_f64_slice(momenta, momentum_count, "momenta") }?; + let helicities = + unsafe { read_selector_ids(helicity_ids, helicity_count, "helicity ids") }?; + let colors = unsafe { read_selector_ids(color_ids, color_count, "color ids") }?; + if output_helicity_count.is_null() { + return Err(invalid("resolved helicity count output is null")); + } + if output_color_count.is_null() { + return Err(invalid("resolved color count output is null")); + } + if point_count == 0 { + return Err(invalid("point_count must be positive")); + } + let (expected_helicity_count, expected_color_count) = handle + .runtime + .resolved_shape(helicities.as_deref(), colors.as_deref())?; + let required = point_count + .checked_mul(expected_helicity_count) + .and_then(|value| value.checked_mul(expected_color_count)) + .ok_or_else(|| invalid("resolved output shape overflow"))?; + validate_f64_output(output, output_capacity, required, "resolved output")?; + let resolved = handle.runtime.evaluate_resolved_f64( + momenta, + point_count, + helicities.as_deref(), + colors.as_deref(), + )?; + let (_, helicity_count, color_count) = resolved.shape(); + if helicity_count != expected_helicity_count || color_count != expected_color_count { + return Err(abi_error( + RUSTICOL_STATUS_RUNTIME_ERROR, + "resolved evaluation shape changed after buffer validation", + )); + } + unsafe { + write_size( + helicity_count, + output_helicity_count, + "resolved helicity count", + )?; + write_size(color_count, output_color_count, "resolved color count")?; + write_f64_slice(&resolved.values, output, output_capacity, "resolved output") + } + }) +} + +/// Updates multiple runtime model parameters atomically. +/// +/// # Safety +/// +/// A non-null `handle` must remain live and exclusively accessible during the call. For nonzero +/// `count`, `names` must reference `count` readable string pointers, every non-null string pointer +/// must reference a readable NUL-terminated string, and `real` and `imaginary` must each reference +/// `count` readable `f64` values. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn rusticol_runtime_set_model_parameters( + handle: *mut RusticolRuntimeHandle, + names: *const *const c_char, + real: *const c_double, + imaginary: *const c_double, + count: size_t, +) -> c_int { + guard(|| { + if count == 0 { + return Err(invalid("model parameter update must not be empty")); + } + // SAFETY: Helpers and explicit checks validate all pointers. + let handle = unsafe { required_handle_mut(handle) }?; + let names = unsafe { read_selector_ids(names, count, "model parameter names") }? + .expect("positive count returns names"); + let real = unsafe { read_f64_slice(real, count, "model parameter real values") }?; + let imaginary = + unsafe { read_f64_slice(imaginary, count, "model parameter imaginary values") }?; + let mut values = BTreeMap::new(); + for index in 0..count { + if values + .insert(names[index].clone(), (real[index], imaginary[index])) + .is_some() + { + return Err(invalid(format!( + "duplicate model parameter update {:?}", + names[index] + ))); + } + } + Ok(handle.runtime.set_model_parameters(&values)?) + }) +} + +/// Updates one runtime model parameter. +/// +/// # Safety +/// +/// A non-null `handle` must remain live and exclusively accessible during the call. A non-null +/// `name` must reference a readable NUL-terminated string for the duration of the call. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn rusticol_runtime_set_model_parameter( + handle: *mut RusticolRuntimeHandle, + name: *const c_char, + real: c_double, + imaginary: c_double, +) -> c_int { + guard(|| { + // SAFETY: Helpers validate pointers. + let handle = unsafe { required_handle_mut(handle) }?; + let name = unsafe { required_c_string(name, "model parameter name") }?; + Ok(handle.runtime.set_model_parameter(name, real, imaginary)?) + }) +} + +/// Reads a current runtime model parameter, including refreshed derived values. +/// +/// # Safety +/// +/// The handle must remain live and shared-accessible; name must be a readable +/// NUL-terminated string. Both outputs must be writable for one double. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn rusticol_runtime_get_model_parameter( + handle: *const RusticolRuntimeHandle, + name: *const c_char, + real: *mut c_double, + imaginary: *mut c_double, +) -> c_int { + guard(|| { + if real.is_null() || imaginary.is_null() { + return Err(invalid("model parameter outputs must be non-null")); + } + // SAFETY: The helpers validate the pointer arguments. + let handle = unsafe { required_handle(handle) }?; + let name = unsafe { required_c_string(name, "model parameter name") }?; + let value = handle.runtime.model_parameter_value(name)?; + // SAFETY: Non-null output storage is guaranteed by the caller contract. + unsafe { + *real = value.0; + *imaginary = value.1; + } + Ok(()) + }) +} + +/// Updates runtime model parameters from a JSON file. +/// +/// # Safety +/// +/// A non-null `handle` must remain live and exclusively accessible during the call. A non-null +/// `path` must reference a readable NUL-terminated string for the duration of the call. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn rusticol_runtime_set_model_parameters_json( + handle: *mut RusticolRuntimeHandle, + path: *const c_char, +) -> c_int { + guard(|| { + // SAFETY: Helpers validate pointers. + let handle = unsafe { required_handle_mut(handle) }?; + let path = unsafe { required_c_string(path, "model parameter JSON path") }?; + Ok(handle.runtime.set_model_parameters_json(Path::new(path))?) + }) +} + +/// Mutes or unmutes runtime warnings. +/// +/// # Safety +/// +/// A non-null `handle` must remain live and exclusively accessible during the call. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn rusticol_runtime_mute_warnings( + handle: *mut RusticolRuntimeHandle, + muted: c_int, +) -> c_int { + guard(|| { + // SAFETY: Helper validates the handle. + let handle = unsafe { required_handle_mut(handle) }?; + if muted == 0 { + handle.runtime.unmute_warnings(); + } else { + handle.runtime.mute_warnings(); + } + Ok(()) + }) +} + +/// Copies and consumes the runtime's pending warnings as JSON. +/// +/// # Safety +/// +/// A non-null `handle` must remain live and exclusively accessible during the call. If non-null, +/// `required` must be writable for one `size_t`, and `buffer` must be writable for `capacity` +/// bytes. A null `buffer` is valid only for a zero-capacity query. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn rusticol_runtime_take_warnings_json( + handle: *mut RusticolRuntimeHandle, + buffer: *mut c_char, + capacity: size_t, + required: *mut size_t, +) -> c_int { + guard(|| { + // SAFETY: Helpers validate pointers. + let handle = unsafe { required_handle_mut(handle) }?; + let json = handle.runtime.pending_warnings_json()?; + unsafe { write_string(&json, buffer, capacity, required)? }; + if !buffer.is_null() { + handle.runtime.clear_pending_warnings(); + } + Ok(()) + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn every_abi_error_is_thread_local_and_nul_terminated() { + set_last_error("one\0two"); + LAST_ERROR.with(|slot| { + assert_eq!(slot.borrow().to_str().unwrap(), "one\\0two"); + }); + } + + #[test] + fn null_handle_is_reported_without_panicking() { + let mut count = 0; + // SAFETY: A null handle is explicitly accepted and reported as an ABI error. + let status = unsafe { rusticol_runtime_external_count(ptr::null(), &mut count) }; + assert_eq!(status, RUSTICOL_STATUS_INVALID_ARGUMENT); + } + + #[test] + fn current_parameter_getter_rejects_null_outputs_without_writing() { + let name = CString::new("aS").unwrap(); + let mut value = 42.0; + // SAFETY: Null arguments are explicitly validated, the other output is live. + let status = unsafe { + rusticol_runtime_get_model_parameter( + ptr::null(), + name.as_ptr(), + &mut value, + ptr::null_mut(), + ) + }; + assert_eq!(status, RUSTICOL_STATUS_INVALID_ARGUMENT); + assert_eq!(value, 42.0); + } + + #[test] + fn short_string_buffer_has_a_distinct_status() { + set_last_error("a message larger than one byte"); + let mut byte = 0_i8; + let mut required = 0; + // SAFETY: Both outputs point to writable storage of the declared capacity. + let status = unsafe { rusticol_last_error_message(&mut byte, 1, &mut required) }; + assert_eq!(status, RUSTICOL_STATUS_BUFFER_TOO_SMALL); + assert!(required > 1); + } + + #[test] + fn invalid_last_error_output_reports_invalid_argument() { + // SAFETY: Null outputs are explicitly accepted and reported as an ABI error. + let status = unsafe { rusticol_last_error_message(ptr::null_mut(), 0, ptr::null_mut()) }; + assert_eq!(status, RUSTICOL_STATUS_INVALID_ARGUMENT); + } + + #[test] + fn null_string_buffer_is_only_valid_for_a_zero_capacity_query() { + let mut required = 0; + let query = unsafe { write_string("value", ptr::null_mut(), 0, &mut required) }; + assert!(query.is_ok()); + assert_eq!(required, 6); + + let error = unsafe { write_string("value", ptr::null_mut(), 1, &mut required) } + .expect_err("nonzero null buffer must fail"); + assert_eq!(error.status, RUSTICOL_STATUS_INVALID_ARGUMENT); + } + + #[test] + fn f64_output_capacity_is_validated_before_copying() { + let values = [1.0, 2.0]; + let mut output = [99.0]; + + let error = unsafe { write_f64_slice(&values, output.as_mut_ptr(), 1, "test output") } + .expect_err("short output must fail"); + + assert_eq!(error.status, RUSTICOL_STATUS_BUFFER_TOO_SMALL); + assert_eq!(output, [99.0]); + } + + #[test] + fn failed_load_clears_the_output_handle() { + let path = CString::new("/path/that/does/not/exist").unwrap(); + let mut output = usize::MAX as *mut RusticolRuntimeHandle; + + // SAFETY: The path is NUL-terminated and output points to writable handle storage. + let status = + unsafe { rusticol_runtime_load(path.as_ptr(), ptr::null(), ptr::null(), &mut output) }; + + assert_eq!(status, RUSTICOL_STATUS_RUNTIME_ERROR); + assert!(output.is_null()); + } + + #[test] + fn cache_calls_reject_null_handles() { + let path = CString::new("cache.bin").unwrap(); + // SAFETY: A null handle is explicitly validated by both entry points. + assert_eq!( + unsafe { rusticol_runtime_save(ptr::null(), path.as_ptr()) }, + RUSTICOL_STATUS_INVALID_ARGUMENT + ); + assert_eq!( + unsafe { rusticol_runtime_load_cache(ptr::null_mut(), path.as_ptr()) }, + RUSTICOL_STATUS_INVALID_ARGUMENT + ); + } + + #[test] + fn static_c_api_advertises_all_symbolica_free_f64_capabilities() { + let mut required = 0; + // SAFETY: This is a zero-capacity query with writable required-size storage. + let status = unsafe { + rusticol_supported_runtime_capabilities_json(ptr::null_mut(), 0, &mut required) + }; + assert_eq!(status, RUSTICOL_STATUS_OK); + assert!(required > 1); + + let mut buffer = vec![0_i8; required]; + // SAFETY: The output buffer and required-size storage are valid for the declared sizes. + let status = unsafe { + rusticol_supported_runtime_capabilities_json( + buffer.as_mut_ptr(), + buffer.len(), + &mut required, + ) + }; + assert_eq!(status, RUSTICOL_STATUS_OK); + let json = unsafe { CStr::from_ptr(buffer.as_ptr()) }.to_str().unwrap(); + assert_eq!( + serde_json::from_str::>(json).unwrap(), + vec![ + "compiled-plane-arena-v1".to_string(), + "eager-direct-arena-v1".to_string(), + "rusticol.color-contraction.symmetric-group-fft.v1".to_string(), + "rusticol.compiled.color-contraction-walsh-c2k.v1".to_string(), + "rusticol.compiled.color-contraction-walsh.v1".to_string(), + "rusticol.compiled.color-topology-lanes.v1".to_string(), + "rusticol.compiled.helicity-dual-lane.v1".to_string(), + "rusticol.compiled.helicity-primary-recurrence.v1".to_string(), + "rusticol.compiled.helicity-selector-union.v1".to_string(), + "rusticol.compiled.runtime-selectors.v1".to_string(), + "rusticol.eager-runtime-layout.complex-f64.v1".to_string(), + "rusticol.on-the-fly.complex-f64.v1".to_string(), + "rusticol.on-the-fly.contracted-color.v1".to_string(), + "rusticol.on-the-fly.lc-color.v1".to_string(), + "rusticol.recurrence-color.contracted.v1".to_string(), + "rusticol.recurrence-color.lc.v1".to_string(), + "rusticol.recurrence-direct-arena.complex-f64.v1".to_string(), + "rusticol.recurrence-helicity-selector-companion.v2".to_string(), + "symbolica.compiled-asm.complex-f64.v1".to_string(), + "symbolica.compiled-cpp.complex-f64.v1".to_string(), + "symjit.application.complex-f64.v1".to_string(), + ] + ); + } + + #[test] + fn native_argument_errors_map_to_the_c_argument_status() { + let error = AbiError::from(RusticolError::selector("bad selector")); + assert_eq!(error.status, RUSTICOL_STATUS_INVALID_ARGUMENT); + } + + #[test] + fn warm_up_pods_preserve_schema_stage_memory_and_completion_state() { + assert_eq!(std::mem::size_of::(), 72); + assert_eq!(std::mem::size_of::(), 64); + let event = NativeOnTheFlyWarmUpEvent { + schema_version: 1, + kind: NativeOnTheFlyWarmUpEventKind::Update, + stage: NativeOnTheFlyWarmUpStage::QueryFamily, + completed: 7, + total: 11, + elapsed_seconds: 0.25, + current_rss_bytes: Some(1024), + peak_rss_bytes: None, + workers: 3, + message: Some("not copied across the fixed C ABI".to_string()), + }; + let pod = warm_up_progress_event(&event); + assert_eq!(pod.schema_version, 1); + assert_eq!(pod.kind, RUSTICOL_WARM_UP_EVENT_UPDATE); + assert_eq!(pod.stage, RUSTICOL_WARM_UP_STAGE_QUERY_FAMILY); + assert_eq!((pod.completed, pod.total), (7, 11)); + assert_eq!(pod.current_rss_bytes, 1024); + assert_eq!(pod.current_rss_available, 1); + assert_eq!(pod.peak_rss_bytes, 0); + assert_eq!(pod.peak_rss_available, 0); + assert_eq!(pod.workers, 3); + + let result = warm_up_result(NativeOnTheFlyWarmUpResult { + schema_version: 1, + elapsed_seconds: 0.5, + query_count: 19, + warmed_query_count: 17, + current_rss_bytes: None, + peak_rss_bytes: Some(4096), + already_warm: false, + first_evaluation_completed: true, + }); + assert_eq!((result.query_count, result.warmed_query_count), (19, 17)); + assert_eq!(result.current_rss_available, 0); + assert_eq!(result.peak_rss_available, 1); + assert_eq!(result.peak_rss_bytes, 4096); + assert_eq!(result.already_warm, 0); + assert_eq!(result.first_evaluation_completed, 1); + } + + #[test] + fn failed_warm_up_clears_result_before_validating_the_handle() { + let point = [0.0; 8]; + let mut result = RusticolWarmUpResult { + schema_version: u32::MAX, + query_count: u64::MAX, + first_evaluation_completed: 1, + ..RusticolWarmUpResult::default() + }; + // SAFETY: The point and result are valid; a null handle is deliberately rejected. + let status = unsafe { + rusticol_runtime_warm_up_f64( + ptr::null_mut(), + point.as_ptr(), + point.len(), + ptr::null(), + 0, + ptr::null(), + 0, + None, + ptr::null_mut(), + &mut result, + ) + }; + assert_eq!(status, RUSTICOL_STATUS_INVALID_ARGUMENT); + assert_eq!(result, RusticolWarmUpResult::default()); + } + + #[test] + fn panic_payloads_map_to_the_panic_status() { + let payload: Box = Box::new(String::from("contained test panic")); + let status = finish_guard(Err(payload)); + assert_eq!(status, RUSTICOL_STATUS_PANIC); + LAST_ERROR.with(|slot| { + assert!( + slot.borrow() + .to_str() + .unwrap() + .contains("contained test panic") + ); + }); + } + + #[test] + fn guard_contains_panics_before_they_cross_the_c_boundary() { + let status = guard(|| -> AbiResult<()> { panic!("contained ABI panic") }); + + assert_eq!(status, RUSTICOL_STATUS_PANIC); + LAST_ERROR.with(|slot| { + assert!( + slot.borrow() + .to_str() + .unwrap() + .contains("contained ABI panic") + ); + }); + } +} diff --git a/pyamplicol/rust/crates/rusticol-capi/tests/eager_artifact.rs b/pyamplicol/rust/crates/rusticol-capi/tests/eager_artifact.rs new file mode 100644 index 0000000000..37d00d6fe5 --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-capi/tests/eager_artifact.rs @@ -0,0 +1,320 @@ +// SPDX-License-Identifier: 0BSD + +use rusticol_capi::{ + RUSTICOL_STATUS_OK, RusticolRuntimeHandle, rusticol_last_error_message, + rusticol_runtime_artifact_id, rusticol_runtime_color_id, rusticol_runtime_evaluate_f64, + rusticol_runtime_evaluate_resolved_f64, rusticol_runtime_execution_mode, rusticol_runtime_free, + rusticol_runtime_get_model_parameter, rusticol_runtime_helicity_id, rusticol_runtime_load, + rusticol_runtime_resolved_shape, rusticol_runtime_set_model_parameter, +}; +use std::ffi::{CStr, CString, c_char}; +use std::path::Path; +use std::ptr; + +fn last_error() -> String { + let mut required = 0; + // SAFETY: This is a zero-capacity query with writable required-size storage. + let status = unsafe { rusticol_last_error_message(ptr::null_mut(), 0, &mut required) }; + if status != RUSTICOL_STATUS_OK || required == 0 { + return format!("could not query Rusticol error (status {status})"); + } + let mut buffer = vec![0 as c_char; required]; + // SAFETY: The output buffer has the queried capacity. + let status = + unsafe { rusticol_last_error_message(buffer.as_mut_ptr(), buffer.len(), &mut required) }; + if status != RUSTICOL_STATUS_OK { + return format!("could not copy Rusticol error (status {status})"); + } + // SAFETY: A successful Rusticol string getter always writes a trailing NUL. + unsafe { CStr::from_ptr(buffer.as_ptr()) } + .to_string_lossy() + .into_owned() +} + +fn load_artifact(root: &Path) -> *mut RusticolRuntimeHandle { + let root = CString::new(root.to_string_lossy().as_bytes()).expect("artifact C path"); + let mut handle: *mut RusticolRuntimeHandle = ptr::null_mut(); + // SAFETY: The path and output handle storage remain valid for the call. + let status = + unsafe { rusticol_runtime_load(root.as_ptr(), ptr::null(), ptr::null(), &mut handle) }; + assert_eq!(status, RUSTICOL_STATUS_OK, "{}", last_error()); + assert!(!handle.is_null()); + handle +} + +fn execution_mode(handle: *mut RusticolRuntimeHandle) -> String { + let mut required = 0; + // SAFETY: This is a zero-capacity query against a live handle. + let status = + unsafe { rusticol_runtime_execution_mode(handle, ptr::null_mut(), 0, &mut required) }; + assert_eq!(status, RUSTICOL_STATUS_OK, "{}", last_error()); + let mut mode = vec![0 as c_char; required]; + // SAFETY: The output buffer has the queried capacity and the handle remains live. + let status = unsafe { + rusticol_runtime_execution_mode(handle, mode.as_mut_ptr(), mode.len(), &mut required) + }; + assert_eq!(status, RUSTICOL_STATUS_OK, "{}", last_error()); + // SAFETY: A successful Rusticol string getter always writes a trailing NUL. + unsafe { CStr::from_ptr(mode.as_ptr()) } + .to_str() + .unwrap() + .to_owned() +} + +fn indexed_string( + handle: *mut RusticolRuntimeHandle, + index: usize, + getter: unsafe extern "C" fn( + *const RusticolRuntimeHandle, + usize, + *mut c_char, + usize, + *mut usize, + ) -> i32, +) -> CString { + let mut required = 0; + // SAFETY: This is a zero-capacity query against a live handle. + let status = unsafe { getter(handle, index, ptr::null_mut(), 0, &mut required) }; + assert_eq!(status, RUSTICOL_STATUS_OK, "{}", last_error()); + let mut buffer = vec![0 as c_char; required]; + // SAFETY: The output buffer has the queried capacity and the handle remains live. + let status = unsafe { + getter( + handle, + index, + buffer.as_mut_ptr(), + buffer.len(), + &mut required, + ) + }; + assert_eq!(status, RUSTICOL_STATUS_OK, "{}", last_error()); + // SAFETY: A successful Rusticol string getter always writes a trailing NUL. + unsafe { CStr::from_ptr(buffer.as_ptr()) }.to_owned() +} + +fn free_runtime(handle: *mut RusticolRuntimeHandle) { + // SAFETY: The handle is live and is consumed exactly once here. + assert_eq!( + unsafe { rusticol_runtime_free(handle) }, + RUSTICOL_STATUS_OK, + "{}", + last_error() + ); +} + +#[test] +fn generated_compiled_artifact_reports_compiled_execution_mode() { + let Some(root) = std::env::var_os("RUSTICOL_COMPILED_ARTIFACT") else { + return; + }; + let handle = load_artifact(Path::new(&root)); + assert_eq!(execution_mode(handle), "compiled"); + free_runtime(handle); +} + +#[test] +fn generated_eager_artifact_loads_and_evaluates_through_the_c_abi() { + let Some(root) = std::env::var_os("RUSTICOL_EAGER_ARTIFACT") else { + return; + }; + let root = Path::new(&root); + let manifest: serde_json::Value = serde_json::from_slice( + &std::fs::read(root.join("artifact.json")).expect("read eager artifact manifest"), + ) + .expect("parse eager artifact manifest"); + let process_id = manifest["default_process_id"] + .as_str() + .expect("eager artifact default process"); + let validation: serde_json::Value = serde_json::from_slice( + &std::fs::read( + root.join("processes") + .join(process_id) + .join("validation-momenta.json"), + ) + .expect("read eager validation momenta"), + ) + .expect("parse eager validation momenta"); + let momenta = validation["points"][0] + .as_array() + .expect("validation point") + .iter() + .flat_map(|leg| { + leg["momentum"] + .as_array() + .expect("validation momentum") + .iter() + .map(|value| { + value + .as_str() + .expect("string momentum component") + .parse::() + .expect("f64 momentum component") + }) + }) + .collect::>(); + + let handle = load_artifact(root); + assert_eq!(execution_mode(handle), "eager"); + + let mut required = 0; + // SAFETY: The handle is live and required-size storage is writable. + assert_eq!( + unsafe { rusticol_runtime_artifact_id(handle, ptr::null_mut(), 0, &mut required) }, + RUSTICOL_STATUS_OK + ); + let mut identity = vec![0 as c_char; required]; + // SAFETY: The output buffer has exactly the queried capacity. + assert_eq!( + unsafe { + rusticol_runtime_artifact_id( + handle, + identity.as_mut_ptr(), + identity.len(), + &mut required, + ) + }, + RUSTICOL_STATUS_OK + ); + // SAFETY: A successful string getter includes a trailing NUL. + let identity = unsafe { CStr::from_ptr(identity.as_ptr()) } + .to_str() + .unwrap(); + assert_eq!(identity, manifest["artifact_id"].as_str().unwrap()); + + let mut output = [f64::NAN]; + // SAFETY: Every input and output buffer remains live and has the declared length. + let status = unsafe { + rusticol_runtime_evaluate_f64( + handle, + momenta.as_ptr(), + momenta.len(), + 1, + output.as_mut_ptr(), + output.len(), + ) + }; + assert_eq!(status, RUSTICOL_STATUS_OK, "{}", last_error()); + assert!(output[0].is_finite()); + + let mut helicity_count = 0; + let mut color_count = 0; + // SAFETY: Null selector arrays request complete coverage and shape outputs are writable. + let status = unsafe { + rusticol_runtime_resolved_shape( + handle, + ptr::null(), + 0, + ptr::null(), + 0, + &mut helicity_count, + &mut color_count, + ) + }; + assert_eq!(status, RUSTICOL_STATUS_OK, "{}", last_error()); + assert!(helicity_count > 0); + assert!(color_count > 0); + let mut resolved = vec![f64::NAN; helicity_count * color_count]; + // SAFETY: Every input, output, and shape buffer remains live and has the declared length. + let status = unsafe { + rusticol_runtime_evaluate_resolved_f64( + handle, + momenta.as_ptr(), + momenta.len(), + 1, + ptr::null(), + 0, + ptr::null(), + 0, + resolved.as_mut_ptr(), + resolved.len(), + &mut helicity_count, + &mut color_count, + ) + }; + assert_eq!(status, RUSTICOL_STATUS_OK, "{}", last_error()); + assert!(resolved.iter().all(|value| value.is_finite())); + let resolved_sum = resolved.iter().sum::(); + let tolerance = 1.0e-15 + 1.0e-12 * output[0].abs(); + assert!( + (resolved_sum - output[0]).abs() <= tolerance, + "resolved sum {resolved_sum:.17e} does not reproduce total {:.17e}", + output[0], + ); + + let helicity = indexed_string(handle, 0, rusticol_runtime_helicity_id); + let color = indexed_string(handle, 0, rusticol_runtime_color_id); + let helicity_ids = [helicity.as_ptr()]; + let color_ids = [color.as_ptr()]; + let mut selected_helicity_count = 0; + let mut selected_color_count = 0; + // SAFETY: Selector arrays and shape outputs remain valid for the call. + let status = unsafe { + rusticol_runtime_resolved_shape( + handle, + helicity_ids.as_ptr(), + helicity_ids.len(), + color_ids.as_ptr(), + color_ids.len(), + &mut selected_helicity_count, + &mut selected_color_count, + ) + }; + assert_eq!(status, RUSTICOL_STATUS_OK, "{}", last_error()); + assert_eq!((selected_helicity_count, selected_color_count), (1, 1)); + let mut selected = [f64::NAN]; + // SAFETY: Every input, selector, output, and shape buffer has the declared length. + let status = unsafe { + rusticol_runtime_evaluate_resolved_f64( + handle, + momenta.as_ptr(), + momenta.len(), + 1, + helicity_ids.as_ptr(), + helicity_ids.len(), + color_ids.as_ptr(), + color_ids.len(), + selected.as_mut_ptr(), + selected.len(), + &mut selected_helicity_count, + &mut selected_color_count, + ) + }; + assert_eq!(status, RUSTICOL_STATUS_OK, "{}", last_error()); + assert!(selected[0].is_finite()); + + let baseline = output[0]; + let parameter = CString::new("normalization.alpha_ew").unwrap(); + // SAFETY: The parameter name and live runtime handle remain valid for the call. + let status = + unsafe { rusticol_runtime_set_model_parameter(handle, parameter.as_ptr(), 0.006, 0.0) }; + assert_eq!(status, RUSTICOL_STATUS_OK, "{}", last_error()); + let mut current_real = f64::NAN; + let mut current_imaginary = f64::NAN; + // SAFETY: Both outputs and the parameter name remain live for this call. + let status = unsafe { + rusticol_runtime_get_model_parameter( + handle, + parameter.as_ptr(), + &mut current_real, + &mut current_imaginary, + ) + }; + assert_eq!(status, RUSTICOL_STATUS_OK, "{}", last_error()); + assert_eq!((current_real, current_imaginary), (0.006, 0.0)); + // SAFETY: Every input and output buffer remains live and has the declared length. + let status = unsafe { + rusticol_runtime_evaluate_f64( + handle, + momenta.as_ptr(), + momenta.len(), + 1, + output.as_mut_ptr(), + output.len(), + ) + }; + assert_eq!(status, RUSTICOL_STATUS_OK, "{}", last_error()); + assert!(output[0].is_finite()); + assert_ne!(output[0], baseline); + + free_runtime(handle); +} diff --git a/pyamplicol/rust/crates/rusticol-capi/tests/runtime_selectors.rs b/pyamplicol/rust/crates/rusticol-capi/tests/runtime_selectors.rs new file mode 100644 index 0000000000..036c7f6287 --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-capi/tests/runtime_selectors.rs @@ -0,0 +1,1064 @@ +// SPDX-License-Identifier: 0BSD + +use rusticol_capi::{ + RUSTICOL_STATUS_BUFFER_TOO_SMALL, RUSTICOL_STATUS_INVALID_ARGUMENT, RUSTICOL_STATUS_OK, + RUSTICOL_WARM_UP_EVENT_END, RUSTICOL_WARM_UP_STAGE_FIRST_EVALUATION, RusticolRuntimeHandle, + RusticolWarmUpProgressEvent, RusticolWarmUpResult, rusticol_last_error_message, + rusticol_runtime_color_count, rusticol_runtime_color_id, rusticol_runtime_evaluate_f64, + rusticol_runtime_evaluate_resolved_f64, rusticol_runtime_evaluate_selected_f64, + rusticol_runtime_free, rusticol_runtime_helicity_count, rusticol_runtime_helicity_id, + rusticol_runtime_load, rusticol_runtime_warm_up_f64, rusticol_runtime_warm_up_f64_with_cores, +}; +use std::ffi::{CStr, CString, c_char, c_int, c_void}; +use std::path::{Path, PathBuf}; +use std::ptr; + +fn fixture_root() -> Option { + std::env::var_os("RUSTICOL_SELECTOR_ARTIFACT").map(PathBuf::from) +} + +fn contracted_fixture_root() -> Option { + std::env::var_os("RUSTICOL_CONTRACTED_SELECTOR_ARTIFACT").map(PathBuf::from) +} + +fn otf_fixture_root() -> Option { + std::env::var_os("RUSTICOL_OTF_ARTIFACT").map(PathBuf::from) +} + +fn last_error() -> String { + let mut required = 0; + // SAFETY: This is a zero-capacity query with writable required-size storage. + let status = unsafe { rusticol_last_error_message(ptr::null_mut(), 0, &mut required) }; + if status != RUSTICOL_STATUS_OK || required == 0 { + return format!("could not query Rusticol error (status {status})"); + } + let mut buffer = vec![0 as c_char; required]; + // SAFETY: The output buffer has the queried capacity. + let status = + unsafe { rusticol_last_error_message(buffer.as_mut_ptr(), buffer.len(), &mut required) }; + if status != RUSTICOL_STATUS_OK { + return format!("could not copy Rusticol error (status {status})"); + } + // SAFETY: Successful Rusticol string getters write a trailing NUL. + unsafe { CStr::from_ptr(buffer.as_ptr()) } + .to_string_lossy() + .into_owned() +} + +fn load_fixture(root: &Path) -> *mut RusticolRuntimeHandle { + let root = CString::new(root.to_string_lossy().as_bytes()).expect("fixture C path"); + let mut handle = ptr::null_mut(); + // SAFETY: The path and output handle storage remain valid for the call. + let status = + unsafe { rusticol_runtime_load(root.as_ptr(), ptr::null(), ptr::null(), &mut handle) }; + assert_eq!(status, RUSTICOL_STATUS_OK, "{}", last_error()); + assert!(!handle.is_null()); + handle +} + +fn validation_momenta(root: &Path) -> Vec { + let manifest: serde_json::Value = serde_json::from_slice( + &std::fs::read(root.join("artifact.json")).expect("read artifact manifest"), + ) + .expect("parse artifact manifest"); + let process_id = manifest["default_process_id"] + .as_str() + .expect("default process id"); + let payload: serde_json::Value = serde_json::from_slice( + &std::fs::read( + root.join("processes") + .join(process_id) + .join("validation-momenta.json"), + ) + .expect("read validation momenta"), + ) + .expect("parse validation momenta"); + payload["points"][0] + .as_array() + .expect("validation point") + .iter() + .flat_map(|leg| { + leg["momentum"] + .as_array() + .expect("validation momentum") + .iter() + .map(|value| { + value + .as_str() + .expect("string momentum component") + .parse::() + .expect("f64 momentum component") + }) + }) + .collect() +} + +fn repeated_momenta(root: &Path, point_count: usize) -> Vec { + let point = validation_momenta(root); + point.repeat(point_count) +} + +#[derive(Default)] +struct WarmUpTrace { + event_count: usize, + saw_terminal_first_evaluation: bool, + max_workers: u64, +} + +unsafe extern "C" fn record_warm_up_progress( + event: *const RusticolWarmUpProgressEvent, + user_data: *mut c_void, +) -> c_int { + if event.is_null() || user_data.is_null() { + return 0; + } + // SAFETY: The test passes a live trace for this synchronous callback. + let trace = unsafe { &mut *user_data.cast::() }; + // SAFETY: Rusticol supplies a borrowed event for the callback invocation. + let event = unsafe { &*event }; + trace.event_count += 1; + trace.max_workers = trace.max_workers.max(event.workers); + trace.saw_terminal_first_evaluation |= event.kind == RUSTICOL_WARM_UP_EVENT_END + && event.stage == RUSTICOL_WARM_UP_STAGE_FIRST_EVALUATION; + 1 +} + +fn physical_counts(handle: *mut RusticolRuntimeHandle) -> (usize, usize) { + let mut helicity_count = 0; + let mut color_count = 0; + // SAFETY: Both outputs are writable and the runtime handle remains live. + assert_eq!( + unsafe { rusticol_runtime_helicity_count(handle, &mut helicity_count) }, + RUSTICOL_STATUS_OK, + "{}", + last_error(), + ); + // SAFETY: Both outputs are writable and the runtime handle remains live. + assert_eq!( + unsafe { rusticol_runtime_color_count(handle, &mut color_count) }, + RUSTICOL_STATUS_OK, + "{}", + last_error(), + ); + (helicity_count, color_count) +} + +fn helicity_id(handle: *mut RusticolRuntimeHandle, index: usize) -> CString { + let mut required = 0; + // SAFETY: This is a zero-capacity query against a live runtime. + assert_eq!( + unsafe { rusticol_runtime_helicity_id(handle, index, ptr::null_mut(), 0, &mut required) }, + RUSTICOL_STATUS_OK, + "{}", + last_error(), + ); + let mut buffer = vec![0 as c_char; required]; + // SAFETY: The buffer has the queried capacity. + assert_eq!( + unsafe { + rusticol_runtime_helicity_id( + handle, + index, + buffer.as_mut_ptr(), + buffer.len(), + &mut required, + ) + }, + RUSTICOL_STATUS_OK, + "{}", + last_error(), + ); + // SAFETY: A successful getter writes a trailing NUL. + unsafe { CStr::from_ptr(buffer.as_ptr()) }.to_owned() +} + +fn color_id(handle: *mut RusticolRuntimeHandle, index: usize) -> CString { + let mut required = 0; + // SAFETY: This is a zero-capacity query against a live runtime. + assert_eq!( + unsafe { rusticol_runtime_color_id(handle, index, ptr::null_mut(), 0, &mut required) }, + RUSTICOL_STATUS_OK, + "{}", + last_error(), + ); + let mut buffer = vec![0 as c_char; required]; + // SAFETY: The buffer has the queried capacity. + assert_eq!( + unsafe { + rusticol_runtime_color_id( + handle, + index, + buffer.as_mut_ptr(), + buffer.len(), + &mut required, + ) + }, + RUSTICOL_STATUS_OK, + "{}", + last_error(), + ); + // SAFETY: A successful getter writes a trailing NUL. + unsafe { CStr::from_ptr(buffer.as_ptr()) }.to_owned() +} + +fn resolved_values( + handle: *mut RusticolRuntimeHandle, + momenta: &[f64], + point_count: usize, + helicity_count: usize, + color_count: usize, +) -> Vec { + let mut output_helicity_count = 0; + let mut output_color_count = 0; + let mut values = vec![f64::NAN; point_count * helicity_count * color_count]; + // SAFETY: Every buffer remains live and has the declared length. Null selectors request all + // physical components. + let status = unsafe { + rusticol_runtime_evaluate_resolved_f64( + handle, + momenta.as_ptr(), + momenta.len(), + point_count, + ptr::null(), + 0, + ptr::null(), + 0, + values.as_mut_ptr(), + values.len(), + &mut output_helicity_count, + &mut output_color_count, + ) + }; + assert_eq!(status, RUSTICOL_STATUS_OK, "{}", last_error()); + assert_eq!(output_helicity_count, helicity_count); + assert_eq!(output_color_count, color_count); + values +} + +fn expected_component( + resolved: &[f64], + point: usize, + helicity: usize, + color: usize, + helicity_count: usize, + color_count: usize, +) -> f64 { + resolved[(point * helicity_count + helicity) * color_count + color] +} + +fn expected_selected_sum( + resolved: &[f64], + point: usize, + helicity: Option, + color: Option, + helicity_count: usize, + color_count: usize, +) -> f64 { + (0..helicity_count) + .filter(|index| helicity.is_none_or(|selected| *index == selected)) + .flat_map(|helicity_index| { + (0..color_count) + .filter(move |index| color.is_none_or(|selected| *index == selected)) + .map(move |color_index| { + expected_component( + resolved, + point, + helicity_index, + color_index, + helicity_count, + color_count, + ) + }) + }) + .sum() +} + +#[test] +fn direct_total_outputs_preserve_bits_canaries_and_capacity_errors() { + let Some(root) = fixture_root() else { + return; + }; + let handle = load_fixture(&root); + let point_count = 4; + let momenta = repeated_momenta(&root, point_count); + let (helicity_count, color_count) = physical_counts(handle); + let resolved = resolved_values(handle, &momenta, point_count, helicity_count, color_count); + let leading_canary = f64::from_bits(0x7ff8_1234_5678_9abc); + let trailing_canary = f64::from_bits(0x7ff8_cafe_babe_f00d); + + let mut totals = vec![f64::NAN; point_count + 2]; + totals[0] = leading_canary; + totals[point_count + 1] = trailing_canary; + // SAFETY: The output pointer starts after the leading canary and has point_count + 1 + // writable values; the runtime must write only the exact point_count-value prefix. + let status = unsafe { + rusticol_runtime_evaluate_f64( + handle, + momenta.as_ptr(), + momenta.len(), + point_count, + totals.as_mut_ptr().add(1), + point_count + 1, + ) + }; + assert_eq!(status, RUSTICOL_STATUS_OK, "{}", last_error()); + assert_eq!(totals[0].to_bits(), leading_canary.to_bits()); + assert_eq!(totals[point_count + 1].to_bits(), trailing_canary.to_bits()); + for point in 0..point_count { + let expected = + expected_selected_sum(&resolved, point, None, None, helicity_count, color_count); + assert_eq!(totals[point + 1].to_bits(), expected.to_bits()); + } + + let mut selected = vec![f64::NAN; point_count + 2]; + selected[0] = leading_canary; + selected[point_count + 1] = trailing_canary; + // SAFETY: All selector arrays are omitted and the output has one excess tail value. + let status = unsafe { + rusticol_runtime_evaluate_selected_f64( + handle, + momenta.as_ptr(), + momenta.len(), + point_count, + ptr::null(), + 0, + ptr::null(), + 0, + ptr::null(), + 0, + ptr::null(), + 0, + selected.as_mut_ptr().add(1), + point_count + 1, + ) + }; + assert_eq!(status, RUSTICOL_STATUS_OK, "{}", last_error()); + assert_eq!(selected[0].to_bits(), leading_canary.to_bits()); + assert_eq!( + selected[point_count + 1].to_bits(), + trailing_canary.to_bits() + ); + for point in 0..point_count { + assert_eq!(selected[point + 1].to_bits(), totals[point + 1].to_bits()); + } + + let short_sentinel = f64::from_bits(0x7ff8_dead_beef_0001); + let mut short_total = vec![short_sentinel; point_count + 2]; + let unchanged_total = short_total.clone(); + // SAFETY: The storage is writable, but its declared capacity is intentionally too short. + let status = unsafe { + rusticol_runtime_evaluate_f64( + handle, + momenta.as_ptr(), + momenta.len(), + point_count, + short_total.as_mut_ptr().add(1), + point_count - 1, + ) + }; + assert_eq!(status, RUSTICOL_STATUS_BUFFER_TOO_SMALL); + assert_eq!( + short_total + .iter() + .map(|value| value.to_bits()) + .collect::>(), + unchanged_total + .iter() + .map(|value| value.to_bits()) + .collect::>() + ); + + let mut short_selected = vec![short_sentinel; point_count + 2]; + let unchanged_selected = short_selected.clone(); + // SAFETY: The storage is writable, but its declared capacity is intentionally too short. + let status = unsafe { + rusticol_runtime_evaluate_selected_f64( + handle, + momenta.as_ptr(), + momenta.len(), + point_count, + ptr::null(), + 0, + ptr::null(), + 0, + ptr::null(), + 0, + ptr::null(), + 0, + short_selected.as_mut_ptr().add(1), + point_count - 1, + ) + }; + assert_eq!(status, RUSTICOL_STATUS_BUFFER_TOO_SMALL); + assert_eq!( + short_selected + .iter() + .map(|value| value.to_bits()) + .collect::>(), + unchanged_selected + .iter() + .map(|value| value.to_bits()) + .collect::>() + ); + + // SAFETY: The handle is consumed exactly once after all evaluations finish. + assert_eq!( + unsafe { rusticol_runtime_free(handle) }, + RUSTICOL_STATUS_OK, + "{}", + last_error(), + ); +} + +#[test] +fn otf_warm_up_c_boundary_reports_progress_and_completes_one_f64_point() { + let Some(root) = otf_fixture_root() else { + return; + }; + let handle = load_fixture(&root); + let point = validation_momenta(&root); + let mut trace = WarmUpTrace::default(); + let mut result = RusticolWarmUpResult::default(); + + // SAFETY: Every pointer refers to live storage for this synchronous call; + // null selector arrays request the complete retained axes. + let status = unsafe { + rusticol_runtime_warm_up_f64_with_cores( + handle, + point.as_ptr(), + point.len(), + ptr::null(), + 0, + ptr::null(), + 0, + 2, + Some(record_warm_up_progress), + (&mut trace as *mut WarmUpTrace).cast(), + &mut result, + ) + }; + assert_eq!(status, RUSTICOL_STATUS_OK, "{}", last_error()); + assert_eq!(result.schema_version, 1); + assert_eq!(result.first_evaluation_completed, 1); + assert!(result.query_count > 0); + assert!(result.warmed_query_count <= result.query_count); + assert!(trace.event_count >= 2); + assert!(trace.saw_terminal_first_evaluation); + assert!((1..=2).contains(&trace.max_workers)); + + // Changing construction cores back to the process-output default must + // reuse the same warm family instead of changing its cache identity. + let mut repeated = RusticolWarmUpResult::default(); + // SAFETY: The handle, point, and result storage remain live. + let status = unsafe { + rusticol_runtime_warm_up_f64( + handle, + point.as_ptr(), + point.len(), + ptr::null(), + 0, + ptr::null(), + 0, + None, + ptr::null_mut(), + &mut repeated, + ) + }; + assert_eq!(status, RUSTICOL_STATUS_OK, "{}", last_error()); + assert_eq!(repeated.already_warm, 1); + assert_eq!(repeated.warmed_query_count, 0); + + let mut value = f64::NAN; + // SAFETY: The same exactly-one-point input and scalar output remain live. + let status = unsafe { + rusticol_runtime_evaluate_f64(handle, point.as_ptr(), point.len(), 1, &mut value, 1) + }; + assert_eq!(status, RUSTICOL_STATUS_OK, "{}", last_error()); + assert!(value.is_finite()); + + // SAFETY: The handle is consumed exactly once after warm-up and evaluation. + assert_eq!(unsafe { rusticol_runtime_free(handle) }, RUSTICOL_STATUS_OK); +} + +#[test] +fn homogeneous_and_alternating_per_point_selectors_match_resolved_components() { + let Some(root) = fixture_root() else { + return; + }; + let handle = load_fixture(&root); + let point_count = 4; + let momenta = repeated_momenta(&root, point_count); + let (helicity_count, color_count) = physical_counts(handle); + assert!( + helicity_count >= 2, + "fixture must expose at least two helicities" + ); + assert!( + color_count >= 2, + "fixture must expose at least two color flows" + ); + let resolved = resolved_values(handle, &momenta, point_count, helicity_count, color_count); + + let helicity = helicity_id(handle, 0); + let color = color_id(handle, 0); + let global_helicities = [helicity.as_ptr()]; + let global_colors = [color.as_ptr()]; + let mut global = vec![f64::NAN; point_count]; + // SAFETY: All selector, momentum, and output arrays have their declared lengths. + let status = unsafe { + rusticol_runtime_evaluate_selected_f64( + handle, + momenta.as_ptr(), + momenta.len(), + point_count, + global_helicities.as_ptr(), + global_helicities.len(), + global_colors.as_ptr(), + global_colors.len(), + ptr::null(), + 0, + ptr::null(), + 0, + global.as_mut_ptr(), + global.len(), + ) + }; + assert_eq!(status, RUSTICOL_STATUS_OK, "{}", last_error()); + for (point, value) in global.iter().enumerate() { + assert_eq!( + value.to_bits(), + expected_component(&resolved, point, 0, 0, helicity_count, color_count).to_bits(), + ); + } + + let mut global_helicity_only = vec![f64::NAN; point_count]; + // SAFETY: All selector, momentum, and output arrays have their declared lengths. + let status = unsafe { + rusticol_runtime_evaluate_selected_f64( + handle, + momenta.as_ptr(), + momenta.len(), + point_count, + global_helicities.as_ptr(), + global_helicities.len(), + ptr::null(), + 0, + ptr::null(), + 0, + ptr::null(), + 0, + global_helicity_only.as_mut_ptr(), + global_helicity_only.len(), + ) + }; + assert_eq!(status, RUSTICOL_STATUS_OK, "{}", last_error()); + for (point, value) in global_helicity_only.iter().enumerate() { + let expected = + expected_selected_sum(&resolved, point, Some(0), None, helicity_count, color_count); + assert_eq!(value.to_bits(), expected.to_bits()); + } + + let mut global_color_only = vec![f64::NAN; point_count]; + // SAFETY: All selector, momentum, and output arrays have their declared lengths. + let status = unsafe { + rusticol_runtime_evaluate_selected_f64( + handle, + momenta.as_ptr(), + momenta.len(), + point_count, + ptr::null(), + 0, + global_colors.as_ptr(), + global_colors.len(), + ptr::null(), + 0, + ptr::null(), + 0, + global_color_only.as_mut_ptr(), + global_color_only.len(), + ) + }; + assert_eq!(status, RUSTICOL_STATUS_OK, "{}", last_error()); + for (point, value) in global_color_only.iter().enumerate() { + let expected = + expected_selected_sum(&resolved, point, None, Some(0), helicity_count, color_count); + assert_eq!(value.to_bits(), expected.to_bits()); + } + + let homogeneous_helicities = vec![0_u32; point_count]; + let homogeneous_colors = vec![0_u32; point_count]; + let mut homogeneous = vec![f64::NAN; point_count]; + // SAFETY: All selector, momentum, and output arrays have their declared lengths. + let status = unsafe { + rusticol_runtime_evaluate_selected_f64( + handle, + momenta.as_ptr(), + momenta.len(), + point_count, + ptr::null(), + 0, + ptr::null(), + 0, + homogeneous_helicities.as_ptr(), + homogeneous_helicities.len(), + homogeneous_colors.as_ptr(), + homogeneous_colors.len(), + homogeneous.as_mut_ptr(), + homogeneous.len(), + ) + }; + assert_eq!(status, RUSTICOL_STATUS_OK, "{}", last_error()); + for (point, value) in homogeneous.iter().enumerate() { + assert_eq!( + value.to_bits(), + expected_component(&resolved, point, 0, 0, helicity_count, color_count).to_bits(), + ); + } + + let pooled_helicities = [0_u32, 0, 1, 1]; + let pooled_colors = [1_u32, 1, 0, 0]; + let mut pooled = vec![f64::NAN; point_count]; + // SAFETY: All selector, momentum, and output arrays have their declared lengths. + let status = unsafe { + rusticol_runtime_evaluate_selected_f64( + handle, + momenta.as_ptr(), + momenta.len(), + point_count, + ptr::null(), + 0, + ptr::null(), + 0, + pooled_helicities.as_ptr(), + pooled_helicities.len(), + pooled_colors.as_ptr(), + pooled_colors.len(), + pooled.as_mut_ptr(), + pooled.len(), + ) + }; + assert_eq!(status, RUSTICOL_STATUS_OK, "{}", last_error()); + for (point, value) in pooled.iter().enumerate() { + assert_eq!( + value.to_bits(), + expected_component( + &resolved, + point, + pooled_helicities[point] as usize, + pooled_colors[point] as usize, + helicity_count, + color_count, + ) + .to_bits(), + ); + } + + let alternating_helicities = [0_u32, 1, 0, 1]; + let alternating_colors = [1_u32, 0, 1, 0]; + let mut alternating = vec![f64::NAN; point_count]; + // SAFETY: All selector, momentum, and output arrays have their declared lengths. + let status = unsafe { + rusticol_runtime_evaluate_selected_f64( + handle, + momenta.as_ptr(), + momenta.len(), + point_count, + ptr::null(), + 0, + ptr::null(), + 0, + alternating_helicities.as_ptr(), + alternating_helicities.len(), + alternating_colors.as_ptr(), + alternating_colors.len(), + alternating.as_mut_ptr(), + alternating.len(), + ) + }; + assert_eq!(status, RUSTICOL_STATUS_OK, "{}", last_error()); + for (point, value) in alternating.iter().enumerate() { + let helicity = alternating_helicities[point] as usize; + let color = alternating_colors[point] as usize; + assert_eq!( + value.to_bits(), + expected_component( + &resolved, + point, + helicity, + color, + helicity_count, + color_count, + ) + .to_bits(), + ); + } + + // First four entries from a selector stream seeded with 0xC0FFEE. + let random_helicities = [0_u32, 0, 0, 1]; + let random_colors = [1_u32, 1, 0, 1]; + let mut seeded_random = vec![f64::NAN; point_count]; + // SAFETY: All selector, momentum, and output arrays have their declared lengths. + let status = unsafe { + rusticol_runtime_evaluate_selected_f64( + handle, + momenta.as_ptr(), + momenta.len(), + point_count, + ptr::null(), + 0, + ptr::null(), + 0, + random_helicities.as_ptr(), + random_helicities.len(), + random_colors.as_ptr(), + random_colors.len(), + seeded_random.as_mut_ptr(), + seeded_random.len(), + ) + }; + assert_eq!(status, RUSTICOL_STATUS_OK, "{}", last_error()); + for (point, value) in seeded_random.iter().enumerate() { + assert_eq!( + value.to_bits(), + expected_component( + &resolved, + point, + random_helicities[point] as usize, + random_colors[point] as usize, + helicity_count, + color_count, + ) + .to_bits(), + ); + } + + let mut helicity_only = vec![f64::NAN; point_count]; + // SAFETY: All selector, momentum, and output arrays have their declared lengths. + let status = unsafe { + rusticol_runtime_evaluate_selected_f64( + handle, + momenta.as_ptr(), + momenta.len(), + point_count, + ptr::null(), + 0, + ptr::null(), + 0, + alternating_helicities.as_ptr(), + alternating_helicities.len(), + ptr::null(), + 0, + helicity_only.as_mut_ptr(), + helicity_only.len(), + ) + }; + assert_eq!(status, RUSTICOL_STATUS_OK, "{}", last_error()); + for (point, value) in helicity_only.iter().enumerate() { + let expected = expected_selected_sum( + &resolved, + point, + Some(alternating_helicities[point] as usize), + None, + helicity_count, + color_count, + ); + assert_eq!(value.to_bits(), expected.to_bits()); + } + + let mut color_only = vec![f64::NAN; point_count]; + // SAFETY: All selector, momentum, and output arrays have their declared lengths. + let status = unsafe { + rusticol_runtime_evaluate_selected_f64( + handle, + momenta.as_ptr(), + momenta.len(), + point_count, + ptr::null(), + 0, + ptr::null(), + 0, + ptr::null(), + 0, + alternating_colors.as_ptr(), + alternating_colors.len(), + color_only.as_mut_ptr(), + color_only.len(), + ) + }; + assert_eq!(status, RUSTICOL_STATUS_OK, "{}", last_error()); + for (point, value) in color_only.iter().enumerate() { + let expected = expected_selected_sum( + &resolved, + point, + None, + Some(alternating_colors[point] as usize), + helicity_count, + color_count, + ); + assert_eq!(value.to_bits(), expected.to_bits()); + } + + // SAFETY: The handle is consumed exactly once after all evaluations finish. + assert_eq!( + unsafe { rusticol_runtime_free(handle) }, + RUSTICOL_STATUS_OK, + "{}", + last_error(), + ); +} + +#[test] +fn malformed_selector_buffers_fail_without_touching_output() { + let Some(root) = fixture_root() else { + return; + }; + let handle = load_fixture(&root); + let point_count = 2; + let momenta = repeated_momenta(&root, point_count); + let valid = [0_u32, 0]; + let mut output = [123.0, 456.0]; + + // SAFETY: A nonzero length with a null selector pointer is intentionally malformed. + let status = unsafe { + rusticol_runtime_evaluate_selected_f64( + handle, + momenta.as_ptr(), + momenta.len(), + point_count, + ptr::null(), + 0, + ptr::null(), + 0, + ptr::null(), + point_count, + ptr::null(), + 0, + output.as_mut_ptr(), + output.len(), + ) + }; + assert_eq!(status, RUSTICOL_STATUS_INVALID_ARGUMENT); + assert!(last_error().contains("helicity_by_point is null")); + assert_eq!(output, [123.0, 456.0]); + + // SAFETY: The selector pointer is readable, but its declared length is intentionally short. + let status = unsafe { + rusticol_runtime_evaluate_selected_f64( + handle, + momenta.as_ptr(), + momenta.len(), + point_count, + ptr::null(), + 0, + ptr::null(), + 0, + valid.as_ptr(), + 1, + ptr::null(), + 0, + output.as_mut_ptr(), + output.len(), + ) + }; + assert_eq!(status, RUSTICOL_STATUS_INVALID_ARGUMENT); + assert!(last_error().contains("expected 2")); + assert_eq!(output, [123.0, 456.0]); + + let helicity = helicity_id(handle, 0); + let global_helicities = [helicity.as_ptr()]; + // SAFETY: Buffers are valid, but the same selector axis is intentionally supplied twice. + let status = unsafe { + rusticol_runtime_evaluate_selected_f64( + handle, + momenta.as_ptr(), + momenta.len(), + point_count, + global_helicities.as_ptr(), + global_helicities.len(), + ptr::null(), + 0, + valid.as_ptr(), + valid.len(), + ptr::null(), + 0, + output.as_mut_ptr(), + output.len(), + ) + }; + assert_eq!(status, RUSTICOL_STATUS_INVALID_ARGUMENT); + assert!(last_error().contains("mutually exclusive")); + assert_eq!(output, [123.0, 456.0]); + + let color = color_id(handle, 0); + let global_colors = [color.as_ptr()]; + // SAFETY: Buffers are valid, but the same color selector axis is supplied twice. + let status = unsafe { + rusticol_runtime_evaluate_selected_f64( + handle, + momenta.as_ptr(), + momenta.len(), + point_count, + ptr::null(), + 0, + global_colors.as_ptr(), + global_colors.len(), + ptr::null(), + 0, + valid.as_ptr(), + valid.len(), + output.as_mut_ptr(), + output.len(), + ) + }; + assert_eq!(status, RUSTICOL_STATUS_INVALID_ARGUMENT); + assert!(last_error().contains("mutually exclusive")); + assert_eq!(output, [123.0, 456.0]); + + let out_of_range = [u32::MAX, u32::MAX]; + // SAFETY: Buffers are valid; selector values are intentionally outside the physical domain. + let status = unsafe { + rusticol_runtime_evaluate_selected_f64( + handle, + momenta.as_ptr(), + momenta.len(), + point_count, + ptr::null(), + 0, + ptr::null(), + 0, + out_of_range.as_ptr(), + out_of_range.len(), + ptr::null(), + 0, + output.as_mut_ptr(), + output.len(), + ) + }; + assert_eq!(status, RUSTICOL_STATUS_INVALID_ARGUMENT); + assert!(last_error().contains("out of range")); + assert_eq!(output, [123.0, 456.0]); + + // SAFETY: The color selector is readable, but its declared length is intentionally short. + let status = unsafe { + rusticol_runtime_evaluate_selected_f64( + handle, + momenta.as_ptr(), + momenta.len(), + point_count, + ptr::null(), + 0, + ptr::null(), + 0, + ptr::null(), + 0, + valid.as_ptr(), + 1, + output.as_mut_ptr(), + output.len(), + ) + }; + assert_eq!(status, RUSTICOL_STATUS_INVALID_ARGUMENT); + assert!(last_error().contains("expected 2")); + assert_eq!(output, [123.0, 456.0]); + + // SAFETY: Buffers are valid; color selector values are outside the physical domain. + let status = unsafe { + rusticol_runtime_evaluate_selected_f64( + handle, + momenta.as_ptr(), + momenta.len(), + point_count, + ptr::null(), + 0, + ptr::null(), + 0, + ptr::null(), + 0, + out_of_range.as_ptr(), + out_of_range.len(), + output.as_mut_ptr(), + output.len(), + ) + }; + assert_eq!(status, RUSTICOL_STATUS_INVALID_ARGUMENT); + assert!(last_error().contains("out of range")); + assert_eq!(output, [123.0, 456.0]); + + // SAFETY: The handle is consumed exactly once after all error cases finish. + assert_eq!( + unsafe { rusticol_runtime_free(handle) }, + RUSTICOL_STATUS_OK, + "{}", + last_error(), + ); +} + +#[test] +fn contracted_color_rejects_global_and_per_point_flow_selectors() { + let Some(root) = contracted_fixture_root() else { + return; + }; + let handle = load_fixture(&root); + let point_count = 2; + let momenta = repeated_momenta(&root, point_count); + let color = color_id(handle, 0); + let global_colors = [color.as_ptr()]; + let point_colors = [0_u32, 0]; + let mut output = [123.0, 456.0]; + + // SAFETY: Buffers are valid; a global physical-flow selection is intentionally requested + // from a contracted NLC/full color axis. + let status = unsafe { + rusticol_runtime_evaluate_selected_f64( + handle, + momenta.as_ptr(), + momenta.len(), + point_count, + ptr::null(), + 0, + global_colors.as_ptr(), + global_colors.len(), + ptr::null(), + 0, + ptr::null(), + 0, + output.as_mut_ptr(), + output.len(), + ) + }; + assert_eq!(status, RUSTICOL_STATUS_INVALID_ARGUMENT); + assert!(last_error().contains("LC color-flow selection is unavailable")); + assert_eq!(output, [123.0, 456.0]); + + // SAFETY: Buffers are valid; per-point physical-flow selection is intentionally requested + // from the same contracted color axis. + let status = unsafe { + rusticol_runtime_evaluate_selected_f64( + handle, + momenta.as_ptr(), + momenta.len(), + point_count, + ptr::null(), + 0, + ptr::null(), + 0, + ptr::null(), + 0, + point_colors.as_ptr(), + point_colors.len(), + output.as_mut_ptr(), + output.len(), + ) + }; + assert_eq!(status, RUSTICOL_STATUS_INVALID_ARGUMENT); + assert!(last_error().contains("LC color-flow selection is unavailable")); + assert_eq!(output, [123.0, 456.0]); + + // SAFETY: The handle is consumed exactly once after both rejection cases finish. + assert_eq!( + unsafe { rusticol_runtime_free(handle) }, + RUSTICOL_STATUS_OK, + "{}", + last_error(), + ); +} diff --git a/pyamplicol/rust/crates/rusticol-core/Cargo.toml b/pyamplicol/rust/crates/rusticol-core/Cargo.toml new file mode 100644 index 0000000000..e68cf17e2e --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/Cargo.toml @@ -0,0 +1,36 @@ +# SPDX-License-Identifier: 0BSD + +[package] +name = "rusticol-core" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +authors.workspace = true +repository.workspace = true + +[features] +default = ["f64-compiled", "f64-symjit"] +f64-compiled = ["dep:libloading", "dep:num-complex", "dep:symjit"] +f64-symjit = ["dep:num-complex", "dep:symjit"] +# Unstable cross-crate bridge used only by the private PyO3 extension during +# artifact generation. It is intentionally absent from the default feature set. +python-generation-bridge = ["f64-symjit"] +# Non-default parity seam for the private Python development harness. This +# feature is deliberately absent from production/default builds. +on-the-fly-test-support = ["f64-symjit"] +symbolica-runtime = ["dep:symbolica"] + +[dependencies] +bincode.workspace = true +libloading = { version = "0.9", optional = true } +libc.workspace = true +num-complex = { version = "0.4", optional = true } +rayon.workspace = true +serde.workspace = true +serde_json.workspace = true +sha2.workspace = true +symbolica = { version = "=3.0.0", default-features = false, features = ["bincode", "integer-gmp", "float-mpfr", "native_code_generation"], optional = true } +symjit = { version = "=2.26.4", default-features = false, optional = true } +thiserror.workspace = true +wide.workspace = true diff --git a/pyamplicol/rust/crates/rusticol-core/src/artifact.rs b/pyamplicol/rust/crates/rusticol-core/src/artifact.rs new file mode 100644 index 0000000000..d7b6d61a29 --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/artifact.rs @@ -0,0 +1,3928 @@ +// SPDX-License-Identifier: 0BSD + +use crate::pacbin::{PacbinMemberKind, PacbinReader}; +use crate::{ + ARTIFACT_MANIFEST_FILE, C_ABI_VERSION, COMPILED_MODEL_SCHEMA_VERSION, + PROCESS_ARTIFACT_SCHEMA_VERSION, PYTHON_API_VERSION, RUNTIME_PHYSICS_SCHEMA_VERSION, + RuntimeCapability, RusticolError, RusticolResult, TOML_SCHEMA_VERSION, +}; +use bincode::Decode; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use sha2::{Digest, Sha256}; +use std::borrow::Cow; +use std::collections::{BTreeMap, BTreeSet}; +use std::fmt::Write as _; +use std::fs::{self, File, OpenOptions}; +use std::io::{Read, Write}; +use std::path::{Path, PathBuf}; +use std::sync::Arc; +#[cfg(feature = "f64-compiled")] +use std::sync::Mutex; +#[cfg(feature = "f64-compiled")] +use std::sync::atomic::{AtomicU64, Ordering}; +#[cfg(feature = "f64-compiled")] +use std::time::{SystemTime, UNIX_EPOCH}; + +const ARTIFACT_IDENTITY_EXTENSION: &str = "artifact_identity"; +const ARTIFACT_IDENTITY_KIND: &str = "pyamplicol-runtime-payload-identity"; +const ARTIFACT_IDENTITY_SCHEMA_VERSION: u64 = 1; +#[cfg(test)] +const RUNTIME_IDENTITY_PAYLOAD_ROLES: [&str; 5] = [ + "compiled-model", + "evaluator-manifest", + "evaluator-state", + "model-parameters", + "runtime-physics", +]; +const EVALUATOR_PAYLOAD_CONTAINER_EXTENSION: &str = "evaluator_payload_container"; +const EVALUATOR_PAYLOAD_CONTAINER_KIND: &str = "pyamplicol-evaluator-payload-container"; +const EVALUATOR_PAYLOAD_CONTAINER_STORAGE_ABI: &str = "pacbin-v1"; +#[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] +const RECURRENCE_BOOTSTRAP_FILENAME: &str = "recurrence-bootstrap.bin"; +const SUPPORTED_ARTIFACT_TARGETS: [&str; 3] = [ + "aarch64-apple-darwin", + "x86_64-apple-darwin", + "x86_64-unknown-linux-gnu", +]; + +pub(crate) const PORTABLE_64LE_ARTIFACT_TARGET: &str = "portable-64le"; +#[cfg(feature = "f64-compiled")] +const NATIVE_LIBRARY_SNAPSHOT_ATTEMPTS: u64 = 128; +#[cfg(feature = "f64-compiled")] +static NEXT_NATIVE_LIBRARY_SNAPSHOT: AtomicU64 = AtomicU64::new(0); + +fn rusticol_package_version() -> &'static str { + option_env!("PYAMPLICOL_PACKAGE_VERSION").unwrap_or(env!("CARGO_PKG_VERSION")) +} + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "kebab-case")] +pub enum ArtifactKind { + PyamplicolProcess, + PyamplicolProcessSet, +} + +#[derive(Clone, Debug, Decode, Deserialize, Eq, PartialEq, Serialize)] +#[cfg_attr( + any(feature = "python-generation-bridge", test), + derive(bincode::Encode) +)] +#[serde(deny_unknown_fields)] +pub struct VersionSet { + pub python_api: u32, + pub toml: u32, + pub compiled_model: u32, + pub process_artifact: u32, + pub runtime_physics: u32, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub symbolica_serialization: Option, + pub c_abi: u32, +} + +#[derive(Clone, Debug, Decode, Deserialize, Eq, PartialEq, Serialize)] +#[cfg_attr( + any(feature = "python-generation-bridge", test), + derive(bincode::Encode) +)] +#[serde(deny_unknown_fields)] +pub struct Target { + pub triple: String, + pub cpu_features: Vec, +} + +#[derive(Clone, Debug, Decode, Deserialize, Eq, PartialEq, Serialize)] +#[cfg_attr( + any(feature = "python-generation-bridge", test), + derive(bincode::Encode) +)] +#[serde(deny_unknown_fields)] +pub struct Producer { + pub distribution: String, + pub version: String, + pub versions: VersionSet, + pub target: Target, + #[serde(default)] + pub git_revision: Option, + #[serde(default)] + pub native_build_inputs_sha256: Option, +} + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "kebab-case")] +pub enum ModelSourceKind { + BuiltInSm, + BuiltInSmHeft, + Ufo, + UfoJson, + CompiledModel, +} + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct ArtifactModel { + pub name: String, + pub source_kind: ModelSourceKind, + pub content_sha256: String, + pub compiled_schema_version: u32, + #[serde(default)] + pub restriction: Option, +} + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct ConfigurationAdjustment { + pub path: String, + pub reason: String, +} + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct ArtifactConfiguration { + pub toml_schema_version: u32, + pub requested_path: String, + pub effective_path: String, + pub adjustments: Vec, +} + +#[derive(Clone, Debug, Decode, Deserialize, Eq, PartialEq, Serialize)] +#[cfg_attr( + any(feature = "python-generation-bridge", test), + derive(bincode::Encode) +)] +#[serde(deny_unknown_fields)] +pub struct ProcessAlias { + pub id: String, + pub expression: String, + pub external_pdgs: Vec, + pub external_permutation: Vec, +} + +#[derive(Clone, Debug, Decode, Deserialize, Eq, PartialEq, Serialize)] +#[cfg_attr( + any(feature = "python-generation-bridge", test), + derive(bincode::Encode) +)] +#[serde(deny_unknown_fields)] +pub struct ArtifactProcess { + pub id: String, + pub expression: String, + pub color_accuracy: String, + pub external_pdgs: Vec, + pub physics_path: String, + pub required_runtime_capabilities: Vec, + pub aliases: Vec, +} + +#[derive(Clone, Debug, Decode, Deserialize, Eq, PartialEq, Serialize)] +#[cfg_attr( + any(feature = "python-generation-bridge", test), + derive(bincode::Encode) +)] +#[serde(deny_unknown_fields)] +pub struct ArtifactRuntime { + pub engine: String, + pub engine_version: String, + pub evaluator_manifest_path: String, + pub required_runtime_capabilities: Vec, + #[serde(default)] + pub api_bundle_path: Option, +} + +#[derive(Clone, Copy, Debug, Decode, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)] +#[cfg_attr( + any(feature = "python-generation-bridge", test), + derive(bincode::Encode) +)] +#[serde(rename_all = "kebab-case")] +pub enum PayloadRole { + ConfigurationRequested, + ConfigurationEffective, + CompiledModel, + RuntimePhysics, + EvaluatorManifest, + EvaluatorState, + ModelParameters, + ValidationMomenta, + StructuralSourceProof, + ApiSource, + ApiBuildFile, + SdkMetadata, + ProcessGroups, +} + +#[derive(Clone, Debug, Decode, Deserialize, Eq, PartialEq, Serialize)] +#[cfg_attr( + any(feature = "python-generation-bridge", test), + derive(bincode::Encode) +)] +#[serde(deny_unknown_fields)] +pub struct Payload { + pub path: String, + pub role: PayloadRole, + pub media_type: String, + pub size_bytes: u64, + pub sha256: String, + pub executable: bool, + #[serde(default)] + pub target: Option, + #[serde(default)] + pub process_id: Option, +} + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct Dependency { + pub name: String, + pub version: String, + pub source: String, + pub license: String, + #[serde(default)] + pub content_sha256: Option, + #[serde(default)] + pub revision: Option, + #[serde(default)] + pub patch_sha256: Option, +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct ArtifactManifest { + pub schema_version: u32, + pub kind: ArtifactKind, + pub artifact_id: String, + pub created_utc: String, + pub producer: Producer, + pub model: ArtifactModel, + pub configuration: ArtifactConfiguration, + pub processes: Vec, + #[serde(default)] + pub default_process_id: Option, + pub runtime: ArtifactRuntime, + pub payloads: Vec, + pub dependencies: Vec, + pub extensions: BTreeMap, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ArtifactSelection { + pub process: ArtifactProcess, + pub requested_id: String, + pub alias: Option, + /// Canonical public process expression selected by the caller. + pub public_expression: String, + /// Public external-particle order selected by the caller. + pub external_pdgs: Vec, + /// Representative external index to public external index. + pub external_permutation: Vec, + /// Whether the permutation was inferred from a concrete expression rather + /// than persisted as an explicit artifact alias. + pub inferred_permutation: bool, + /// Present when the caller selected a process that was not generated and + /// is routed through `process-groups.json` to `process`. + pub process_group: Option, +} + +impl ArtifactSelection { + /// Whether public metadata differs from the stored representative. + pub fn remaps_public_process(&self) -> bool { + self.alias.is_some() || self.inferred_permutation || self.process_group.is_some() + } +} + +/// Relation of a selected process-group member to its generated representative: +/// `ME_member(p) = numerator / denominator * ME_representative(q)` with +/// `q[r] = p[external_permutation[r]]`. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ProcessGroupRelation { + pub factor_numerator: u64, + pub factor_denominator: u64, + /// The relation includes global charge conjugation: public LC color-flow + /// words are the reversed words of the permuted representative. + pub conjugated: bool, + /// Member particle names in the public external order. + pub external_particles: Vec, +} + +impl ProcessGroupRelation { + pub fn has_unit_factor(&self) -> bool { + self.factor_numerator == self.factor_denominator + } +} + +#[derive(Clone, Debug)] +pub struct VerifiedArtifact { + root: PathBuf, + manifest_path: PathBuf, + manifest: ArtifactManifest, + payloads: Arc>, + evaluator_payload_container: Option>, + #[cfg(feature = "f64-compiled")] + native_library_cache: Arc>>>, +} + +/// Authenticated inputs for the narrow recurrence-only loading path. +/// +/// The returned `VerifiedArtifact` owns the same checked payload inventory and +/// evaluator container used by the generic loader. Only the broad evaluator +/// set and generation-oriented extension trees are skipped. +#[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] +pub(crate) struct RecurrenceBootstrapArtifact { + pub(crate) artifact: VerifiedArtifact, + pub(crate) selection: ArtifactSelection, + pub(crate) ready_execution: crate::engine::RecurrenceReadyExecutionV1, + pub(crate) physics: crate::ProcessPhysics, + pub(crate) evaluator_payload_container: EvaluatorPayloadContainerExtension, +} + +#[derive(Clone, Debug, Decode, Deserialize)] +#[cfg_attr( + any(feature = "python-generation-bridge", test), + derive(bincode::Encode) +)] +#[serde(deny_unknown_fields)] +pub(crate) struct EvaluatorPayloadContainerExtension { + pub(crate) kind: String, + pub(crate) schema_version: u32, + pub(crate) storage_abi: String, + pub(crate) path: String, + pub(crate) member_count: u64, + pub(crate) unpacked_size_bytes: u64, + pub(crate) index_sha256: String, +} + +/// One evaluator payload resolved from a legacy loose file or a packed member. +#[derive(Clone, Debug)] +pub(crate) enum EvaluatorPayloadSource { + File(PathBuf), + AuthenticatedFile { + root: PathBuf, + payload: Payload, + }, + Packed { + container: Arc, + logical_path: String, + }, +} + +/// A dynamic library loaded only from process-private snapshot bytes. +/// +/// The original artifact path is retained solely for diagnostics. Symbol +/// lookup always addresses `library`, which was opened from an authenticated +/// private materialization that is unlinked immediately on Unix. +pub(crate) struct PinnedNativeLibrary { + #[cfg(feature = "f64-compiled")] + library: Option, + display_path: PathBuf, + #[cfg(feature = "f64-compiled")] + temporary: Option, +} + +impl std::fmt::Debug for PinnedNativeLibrary { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter + .debug_struct("PinnedNativeLibrary") + .field("display_path", &self.display_path) + .finish_non_exhaustive() + } +} + +#[cfg(feature = "f64-compiled")] +impl std::ops::Deref for PinnedNativeLibrary { + type Target = libloading::Library; + + fn deref(&self) -> &Self::Target { + self.library + .as_ref() + .expect("pinned native library is live until Drop") + } +} + +impl PinnedNativeLibrary { + #[cfg(feature = "f64-compiled")] + fn from_bytes(bytes: &[u8], display_path: PathBuf) -> RusticolResult> { + let mut failures = Vec::new(); + for root in native_library_snapshot_roots() { + let temporary = match TemporaryNativeLibrary::create(bytes, &display_path, &root) { + Ok(temporary) => temporary, + Err(error) => { + failures.push(format!("{}: {error}", root.display())); + continue; + } + }; + let library = match unsafe { libloading::Library::new(&temporary.path) } { + Ok(library) => library, + Err(error) => { + failures.push(format!("{}: {error}", root.display())); + continue; + } + }; + #[cfg(unix)] + let temporary = { + temporary.remove_now()?; + None + }; + #[cfg(not(unix))] + let temporary = Some(temporary); + return Ok(Arc::new(Self { + library: Some(library), + display_path, + temporary, + })); + } + Err(RusticolError::evaluation(format!( + "could not load authenticated native evaluator library {}; snapshot roots failed: {}. \ + The snapshot filesystem must be writable and executable (not mounted noexec); set \ + PYAMPLICOL_NATIVE_SNAPSHOT_ROOT to a private executable filesystem", + display_path.display(), + failures.join("; ") + ))) + } + + pub(crate) fn display_path(&self) -> &Path { + &self.display_path + } + + #[cfg(all(test, feature = "f64-compiled"))] + pub(crate) fn from_test_path(path: &Path) -> RusticolResult> { + let bytes = fs::read(path).map_err(|error| { + RusticolError::artifact(format!( + "could not read native-library test fixture {}: {error}", + path.display() + )) + })?; + Self::from_bytes(&bytes, path.to_path_buf()) + } +} + +#[cfg(feature = "f64-compiled")] +impl Drop for PinnedNativeLibrary { + fn drop(&mut self) { + // Windows does not permit removal while the library is loaded. Drop + // the handle first, then let the retained materialization guard clean + // up. Unix snapshots are already unlinked after successful dlopen. + drop(self.library.take()); + drop(self.temporary.take()); + } +} + +#[cfg(feature = "f64-compiled")] +struct TemporaryNativeLibrary { + directory: PathBuf, + path: PathBuf, + active: bool, +} + +#[cfg(feature = "f64-compiled")] +impl TemporaryNativeLibrary { + fn create(bytes: &[u8], display_path: &Path, root: &Path) -> RusticolResult { + if !root.is_dir() { + return Err(RusticolError::artifact(format!( + "native-library snapshot root {} is not a directory", + root.display() + ))); + } + let suffix = display_path + .extension() + .and_then(|value| value.to_str()) + .filter(|value| !value.is_empty()) + .unwrap_or("bin"); + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_nanos(); + for _ in 0..NATIVE_LIBRARY_SNAPSHOT_ATTEMPTS { + let sequence = NEXT_NATIVE_LIBRARY_SNAPSHOT.fetch_add(1, Ordering::Relaxed); + let directory = root.join(format!( + ".pyamplicol-native-{}-{now}-{sequence}", + std::process::id() + )); + let mut builder = fs::DirBuilder::new(); + #[cfg(unix)] + { + use std::os::unix::fs::DirBuilderExt as _; + builder.mode(0o700); + } + match builder.create(&directory) { + Ok(()) => {} + Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => continue, + Err(error) => { + return Err(RusticolError::artifact(format!( + "could not create private native-library snapshot directory {}: {error}", + directory.display() + ))); + } + } + let path = directory.join(format!("payload.{suffix}")); + let result = (|| { + let mut options = OpenOptions::new(); + options.create_new(true).write(true); + #[cfg(unix)] + { + use std::os::unix::fs::OpenOptionsExt as _; + options.mode(0o600); + } + let mut file = options.open(&path).map_err(|error| { + RusticolError::artifact(format!( + "could not create native-library snapshot {}: {error}", + path.display() + )) + })?; + file.write_all(bytes).map_err(|error| { + RusticolError::artifact(format!( + "could not write native-library snapshot {}: {error}", + path.display() + )) + })?; + file.flush().map_err(|error| { + RusticolError::artifact(format!( + "could not flush native-library snapshot {}: {error}", + path.display() + )) + })?; + drop(file); + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt as _; + fs::set_permissions(&path, fs::Permissions::from_mode(0o500)).map_err( + |error| { + RusticolError::artifact(format!( + "could not protect native-library snapshot {}: {error}", + path.display() + )) + }, + )?; + } + Ok(Self { + directory: directory.clone(), + path: path.clone(), + active: true, + }) + })(); + if result.is_err() { + let _ = fs::remove_file(&path); + let _ = fs::remove_dir(&directory); + } + return result; + } + Err(RusticolError::artifact( + "could not allocate a unique private native-library snapshot", + )) + } + + #[cfg(unix)] + fn remove_now(mut self) -> RusticolResult<()> { + fs::remove_file(&self.path).map_err(|error| { + RusticolError::artifact(format!( + "could not unlink loaded native-library snapshot {}: {error}", + self.path.display() + )) + })?; + fs::remove_dir(&self.directory).map_err(|error| { + RusticolError::artifact(format!( + "could not remove native-library snapshot directory {}: {error}", + self.directory.display() + )) + })?; + self.active = false; + Ok(()) + } +} + +#[cfg(feature = "f64-compiled")] +fn native_library_snapshot_roots() -> Vec { + let mut roots = Vec::new(); + if let Some(configured) = + std::env::var_os("PYAMPLICOL_NATIVE_SNAPSHOT_ROOT").filter(|value| !value.is_empty()) + { + let configured = PathBuf::from(configured); + if !roots.contains(&configured) { + roots.push(configured); + } + } + let system = std::env::temp_dir(); + if !roots.contains(&system) { + roots.push(system); + } + roots +} + +#[cfg(feature = "f64-compiled")] +impl Drop for TemporaryNativeLibrary { + fn drop(&mut self) { + if self.active { + let _ = fs::remove_file(&self.path); + let _ = fs::remove_dir(&self.directory); + } + } +} + +impl EvaluatorPayloadSource { + pub(crate) fn read(&self) -> RusticolResult> { + match self { + Self::File(path) => fs::read(path).map(Cow::Owned).map_err(|error| { + RusticolError::artifact(format!( + "could not read evaluator payload {}: {error}", + path.display() + )) + }), + Self::AuthenticatedFile { root, payload } => { + read_declared_payload(root, payload).map(Cow::Owned) + } + Self::Packed { + container, + logical_path, + } => container.member_bytes(logical_path).map(Cow::Borrowed), + } + } + + pub(crate) fn display_name(&self) -> String { + match self { + Self::File(path) => path.display().to_string(), + Self::AuthenticatedFile { root, payload } => { + root.join(&payload.path).display().to_string() + } + Self::Packed { logical_path, .. } => format!("evaluators.pacbin:{logical_path}"), + } + } +} + +/// A path-scoped resolver shared by compiled and eager evaluator loaders. +#[derive(Clone, Debug)] +pub(crate) struct EvaluatorPayloadStore { + artifact_root: PathBuf, + relative_root: PathBuf, + container: Option>, + payloads: Option>>, + #[cfg(feature = "f64-compiled")] + native_library_cache: Arc>>>, +} + +impl EvaluatorPayloadStore { + pub(crate) fn directory(root: &Path) -> Self { + Self { + artifact_root: root.to_path_buf(), + relative_root: root.to_path_buf(), + container: None, + payloads: None, + #[cfg(feature = "f64-compiled")] + native_library_cache: Arc::new(Mutex::new(BTreeMap::new())), + } + } + + pub(crate) fn logical_path(&self, value: &str) -> RusticolResult { + let relative = confined_evaluator_path(value)?; + artifact_logical_path(&self.artifact_root, &self.relative_root.join(relative)) + } + + pub(crate) fn source(&self, value: &str) -> RusticolResult { + let logical_path = self.logical_path(value)?; + let path = self.artifact_root.join(&logical_path); + if let Some(container) = &self.container + && container.member(&logical_path).is_ok() + { + return Ok(EvaluatorPayloadSource::Packed { + container: container.clone(), + logical_path, + }); + } + if let Some(payloads) = &self.payloads { + let payload = payloads.get(&logical_path).ok_or_else(|| { + RusticolError::security(format!( + "evaluator payload {logical_path:?} is not declared by the verified artifact" + )) + })?; + if payload.role != PayloadRole::EvaluatorState { + return Err(RusticolError::security(format!( + "evaluator payload {logical_path:?} has role {:?}, expected evaluator-state", + payload.role + ))); + } + return Ok(EvaluatorPayloadSource::AuthenticatedFile { + root: self.artifact_root.clone(), + payload: payload.clone(), + }); + } + Ok(EvaluatorPayloadSource::File(path)) + } + + pub(crate) fn packed_member_bytes( + &self, + value: &str, + expected_kind: PacbinMemberKind, + ) -> RusticolResult<&[u8]> { + let logical_path = self.logical_path(value)?; + let container = self.container.as_ref().ok_or_else(|| { + RusticolError::compatibility(format!( + "typed evaluator payload {logical_path:?} requires evaluators.pacbin storage" + )) + })?; + let member = container.member(&logical_path)?; + if member.kind() != expected_kind { + return Err(RusticolError::integrity(format!( + "evaluator payload {logical_path:?} has pacbin member kind {:?}, expected {:?}", + member.kind(), + expected_kind, + ))); + } + container.member_bytes(&logical_path) + } + + pub(crate) fn load_native_library( + &self, + value: &str, + ) -> RusticolResult> { + #[cfg(not(feature = "f64-compiled"))] + { + let _ = value; + return Err(RusticolError::compatibility( + "native evaluator libraries require the f64-compiled feature", + )); + } + #[cfg(feature = "f64-compiled")] + { + let relative = confined_evaluator_path(value)?; + let path = self.relative_root.join(relative); + let logical_path = artifact_logical_path(&self.artifact_root, &path)?; + if let Some(library) = self + .native_library_cache + .lock() + .map_err(|_| { + RusticolError::evaluation("native evaluator library cache is poisoned") + })? + .get(&logical_path) + .cloned() + { + return Ok(library); + } + let source = self.source(value)?; + if let EvaluatorPayloadSource::Packed { logical_path, .. } = &source { + return Err(RusticolError::compatibility(format!( + "native evaluator library {logical_path:?} cannot be loaded from pacbin storage" + ))); + } + let display_path = PathBuf::from(source.display_name()); + let bytes = source.read()?; + let loaded = PinnedNativeLibrary::from_bytes(bytes.as_ref(), display_path)?; + let mut cache = self.native_library_cache.lock().map_err(|_| { + RusticolError::evaluation("native evaluator library cache is poisoned") + })?; + Ok(cache + .entry(logical_path) + .or_insert_with(|| Arc::clone(&loaded)) + .clone()) + } + } +} + +impl VerifiedArtifact { + /// Try the authenticated recurrence-only bootstrap without materializing + /// broad generation/reporting extension trees. + /// + /// An absent conventional sidecar is not an error: callers may fall back + /// to the generic loader for artifacts produced before this optimization. + /// File presence opts a stable representative process into this compact + /// path; every subsequent failure is reported rather than falling back to + /// less constrained inputs. + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + #[cfg_attr(target_vendor = "apple", unsafe(link_section = "__TEXT,__rcl_load"))] + #[cfg_attr(target_vendor = "apple", inline(never))] + pub(crate) fn try_open_recurrence_bootstrap( + path: impl AsRef, + requested_process: Option<&str>, + ) -> RusticolResult> { + // Explicit aliases and expressions retain the generic selector path. + // An omitted selector can use the image only when the conventional + // single-process tree contains exactly one stable-ID sidecar. + if requested_process.is_some_and(|process_id| { + validate_public_id(process_id, "requested process id").is_err() + }) { + return Ok(None); + } + let requested = path.as_ref(); + reject_symlink_chain(requested)?; + let (root, manifest_path) = locate_manifest(requested)?; + reject_symlink_chain(&manifest_path)?; + let discovered_process_id = if requested_process.is_none() { + discover_single_recurrence_bootstrap_process(&root)? + } else { + None + }; + let discovered_default = discovered_process_id.is_some(); + let Some(process_id) = requested_process.or(discovered_process_id.as_deref()) else { + return Ok(None); + }; + let bootstrap_path = format!("processes/{}/{}", process_id, RECURRENCE_BOOTSTRAP_FILENAME); + let bootstrap_file = root.join(&bootstrap_path); + let bootstrap_bytes = match read_bounded_recurrence_bootstrap(&root, &bootstrap_file)? { + None if discovered_default => { + return Err(RusticolError::integrity(format!( + "discovered default recurrence bootstrap {bootstrap_path:?} disappeared before opening" + ))); + } + None => return Ok(None), + Some(bytes) => bytes, + }; + let decoded = crate::engine::decode_recurrence_bootstrap_image_v1(&bootstrap_bytes)?; + if decoded.process.id != process_id { + return Err(RusticolError::integrity(format!( + "recurrence bootstrap at {bootstrap_path:?} belongs to process {:?}", + decoded.process.id + ))); + } + validate_recurrence_bootstrap_metadata(&decoded.producer, &decoded.runtime)?; + + let bootstrap_size = u64::try_from(bootstrap_bytes.len()).map_err(|_| { + RusticolError::artifact("recurrence bootstrap size exceeds the u64 domain") + })?; + let bootstrap_payload = Payload { + path: bootstrap_path.clone(), + role: PayloadRole::EvaluatorState, + media_type: "application/octet-stream".to_string(), + size_bytes: bootstrap_size, + sha256: format!("{:x}", Sha256::digest(&bootstrap_bytes)), + executable: false, + target: Some(decoded.producer.target.clone()), + process_id: Some(process_id.to_string()), + }; + let mut declared = decoded.payloads; + let mut insertion_index = declared.len(); + let mut previous_path: Option<&str> = None; + for (index, payload) in declared.iter().enumerate() { + if previous_path.is_some_and(|previous| previous >= payload.path.as_str()) { + return Err(RusticolError::integrity( + "recurrence bootstrap payload inventory must be strictly path-sorted", + )); + } + if payload.path == bootstrap_path { + return Err(RusticolError::integrity(format!( + "recurrence bootstrap inventory illegally contains its own record {bootstrap_path:?}" + ))); + } + if insertion_index == declared.len() && payload.path.as_str() > bootstrap_path.as_str() + { + insertion_index = index; + } + previous_path = Some(&payload.path); + } + declared.insert(insertion_index, bootstrap_payload); + let manifest_artifact_id = read_canonical_manifest_artifact_id(&manifest_path)?; + let computed_artifact_id = compute_payload_artifact_id(&declared)?; + if computed_artifact_id != manifest_artifact_id { + return Err(RusticolError::integrity(format!( + "recurrence bootstrap payload identity {computed_artifact_id} disagrees with artifact manifest identity {manifest_artifact_id}" + ))); + } + + let process = decoded.process; + validate_recurrence_bootstrap_process(&process)?; + let selection = representative_selection(&process); + let manifest = recurrence_bootstrap_manifest( + manifest_artifact_id, + decoded.producer, + decoded.runtime, + process, + declared, + ); + let payloads = validated_payload_declarations(&manifest)?; + for payload in payloads.values() { + if let Some(target) = &payload.target { + validate_payload_target(&manifest.producer.target, target, &payload.path)?; + validate_target(target, &format!("payload {}", payload.path))?; + } + } + validate_recurrence_bootstrap_source_payload( + &payloads, + &decoded.execution_path, + &decoded.execution_sha256, + PayloadRole::EvaluatorManifest, + process_id, + )?; + validate_recurrence_bootstrap_source_payload( + &payloads, + &decoded.physics_path, + &decoded.physics_sha256, + PayloadRole::RuntimePhysics, + process_id, + )?; + if decoded.physics_path != selection.process.physics_path { + return Err(RusticolError::integrity( + "recurrence bootstrap physics path disagrees with the selected process", + )); + } + validate_evaluator_payload_container_declaration( + &decoded.evaluator_payload_container, + &payloads, + )?; + let artifact = Self { + root, + manifest_path, + manifest, + payloads: Arc::new(payloads), + // The process binding says whether this process has any JIT/native + // leaves. Avoid opening and retaining the model-wide evaluator + // PACBIN for source/intrinsic-only recurrence processes. + evaluator_payload_container: None, + #[cfg(feature = "f64-compiled")] + native_library_cache: Arc::new(Mutex::new(BTreeMap::new())), + }; + Ok(Some(RecurrenceBootstrapArtifact { + artifact, + selection, + ready_execution: decoded.ready_execution, + physics: decoded.physics, + evaluator_payload_container: decoded.evaluator_payload_container, + })) + } + + /// Open a trusted artifact directory or a direct v3 manifest path. + /// + /// Schema, references, and confined paths are checked. Payload digests are + /// deliberately not recomputed on the normal runtime path. + pub fn open(path: impl AsRef) -> RusticolResult { + Self::open_with_manifest_preflight(path, |_| Ok(())) + } + + pub(crate) fn open_with_manifest_preflight( + path: impl AsRef, + preflight: impl FnOnce(&ArtifactManifest) -> RusticolResult<()>, + ) -> RusticolResult { + let requested = path.as_ref(); + reject_symlink_chain(requested)?; + let (root, manifest_path) = locate_manifest(requested)?; + reject_symlink_chain(&manifest_path)?; + let metadata = fs::metadata(&manifest_path).map_err(|error| { + RusticolError::artifact(format!( + "could not inspect artifact manifest {}: {error}", + manifest_path.display() + )) + })?; + if !metadata.is_file() { + return Err(RusticolError::security(format!( + "artifact manifest {} is not a regular file", + manifest_path.display() + ))); + } + let bytes = fs::read(&manifest_path).map_err(|error| { + RusticolError::artifact(format!( + "could not read artifact manifest {}: {error}", + manifest_path.display() + )) + })?; + let header: Value = serde_json::from_slice(&bytes).map_err(|error| { + RusticolError::serialization(format!( + "could not parse artifact manifest {} as JSON: {error}", + manifest_path.display() + )) + })?; + let schema_version = header + .get("schema_version") + .and_then(Value::as_u64) + .unwrap_or(0); + if matches!(schema_version, 1 | 2) { + return Err(RusticolError::compatibility(format!( + "process artifact schema v{schema_version} is unsupported and unsafe to migrate; regenerate it with `pyamplicol generate` to produce schema v3" + ))); + } + if schema_version != u64::from(PROCESS_ARTIFACT_SCHEMA_VERSION) { + return Err(RusticolError::compatibility(format!( + "unsupported process artifact schema {schema_version}; this runtime requires schema v{PROCESS_ARTIFACT_SCHEMA_VERSION}" + ))); + } + validate_artifact_identity_header(&header)?; + reject_forbidden_nulls(&header)?; + let manifest: ArtifactManifest = serde_json::from_slice(&bytes).map_err(|error| { + RusticolError::serialization(format!( + "artifact manifest {} does not conform to schema v3: {error}", + manifest_path.display() + )) + })?; + validate_manifest(&manifest)?; + preflight(&manifest)?; + + let payloads = validated_payload_declarations(&manifest)?; + validate_references(&manifest, &payloads)?; + let evaluator_payload_container = + load_evaluator_payload_container(&root, &manifest, &payloads)?; + let payloads = Arc::new(payloads); + Ok(Self { + root, + manifest_path, + manifest, + payloads, + evaluator_payload_container, + #[cfg(feature = "f64-compiled")] + native_library_cache: Arc::new(Mutex::new(BTreeMap::new())), + }) + } + + pub fn root(&self) -> &Path { + &self.root + } + + pub fn manifest_path(&self) -> &Path { + &self.manifest_path + } + + pub fn manifest(&self) -> &ArtifactManifest { + &self.manifest + } + + /// Resolve a selector against the manifest, then against non-generated + /// members of the declared process-group map. The map is read only when + /// no stored process ID, alias, or stored expression (exact or + /// side-permuted) matches. + pub fn select_process(&self, requested: Option<&str>) -> RusticolResult { + self.manifest + .select_process_with_groups(requested, || self.process_groups()) + } + + fn process_groups(&self) -> RusticolResult> { + let mut declared = self + .payloads + .values() + .filter(|payload| payload.role == PayloadRole::ProcessGroups); + let Some(payload) = declared.next() else { + return Ok(None); + }; + if declared.next().is_some() { + return Err(RusticolError::artifact( + "artifact declares more than one process-groups payload", + )); + } + let bytes = read_declared_payload(&self.root, payload)?; + if format!("{:x}", Sha256::digest(&bytes)) != payload.sha256 { + return Err(RusticolError::integrity(format!( + "process-groups payload {:?} does not match its declared sha256", + payload.path + ))); + } + ProcessGroups::from_json(&bytes, &self.manifest).map(Some) + } + + pub fn payload(&self, path: &str) -> RusticolResult<&Payload> { + self.payloads.get(path).ok_or_else(|| { + RusticolError::security(format!("artifact path {path:?} is not a declared payload")) + }) + } + + pub fn read_payload(&self, path: &str) -> RusticolResult> { + let payload = self.payload(path)?; + read_declared_payload(&self.root, payload) + } + + /// Open one declared loose payload and pin the checked file description. + /// + /// Callers that stream or map a payload must authenticate this returned + /// file, rather than reopening `path`, so path replacement cannot change + /// the bytes between declaration checks and use. + pub(crate) fn open_payload_file(&self, path: &str) -> RusticolResult { + let payload = self.payload(path)?; + open_checked_payload(&self.root, payload) + } + + pub(crate) fn payload_path(&self, path: &str) -> RusticolResult { + let payload = self.payload(path)?; + drop(open_checked_payload(&self.root, payload)?); + Ok(self.root.join(path)) + } + + pub(crate) fn evaluator_payload_store( + &self, + relative_root: &Path, + ) -> RusticolResult { + if !relative_root.starts_with(&self.root) { + return Err(RusticolError::security( + "evaluator payload root escapes the artifact root", + )); + } + Ok(EvaluatorPayloadStore { + artifact_root: self.root.clone(), + relative_root: relative_root.to_path_buf(), + container: self.evaluator_payload_container.clone(), + payloads: Some(self.payloads.clone()), + #[cfg(feature = "f64-compiled")] + native_library_cache: self.native_library_cache.clone(), + }) + } + + pub(crate) fn install_evaluator_payload_container( + &mut self, + extension: &EvaluatorPayloadContainerExtension, + ) -> RusticolResult<()> { + if self.evaluator_payload_container.is_none() { + self.evaluator_payload_container = Some( + load_evaluator_payload_container_extension(&self.root, extension, &self.payloads)? + .ok_or_else(|| { + RusticolError::internal( + "declared evaluator payload container resolved to no reader", + ) + })?, + ); + } + Ok(()) + } + + pub(crate) fn has_evaluator_payload(&self, path: &str) -> RusticolResult { + if let Some(payload) = self.payloads.get(path) { + return Ok(payload.role == PayloadRole::EvaluatorState); + } + Ok(self + .evaluator_payload_container + .as_ref() + .is_some_and(|container| container.member(path).is_ok())) + } +} + +#[cfg_attr(target_vendor = "apple", unsafe(link_section = "__TEXT,__rcl_load"))] +#[cfg_attr(target_vendor = "apple", inline(never))] +fn validated_payload_declarations( + manifest: &ArtifactManifest, +) -> RusticolResult> { + let mut payloads = BTreeMap::new(); + let mut portable_paths = BTreeSet::new(); + for payload in &manifest.payloads { + validate_payload_declaration(payload)?; + validate_relative_path(&payload.path, "payload path")?; + if payload.path == ARTIFACT_MANIFEST_FILE { + return Err(RusticolError::security(format!( + "{ARTIFACT_MANIFEST_FILE} is reserved for the artifact manifest" + ))); + } + let portable = payload.path.to_ascii_lowercase(); + if !portable_paths.insert(portable) { + return Err(RusticolError::security(format!( + "duplicate or case-colliding payload path {:?}", + payload.path + ))); + } + if payloads + .insert(payload.path.clone(), payload.clone()) + .is_some() + { + return Err(RusticolError::security(format!( + "duplicate payload path {:?}", + payload.path + ))); + } + } + Ok(payloads) +} + +#[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] +#[cfg_attr(target_vendor = "apple", unsafe(link_section = "__TEXT,__rcl_load"))] +#[cfg_attr(target_vendor = "apple", inline(never))] +fn read_bounded_recurrence_bootstrap(root: &Path, path: &Path) -> RusticolResult>> { + reject_symlink_chain(path)?; + let metadata = match fs::symlink_metadata(path) { + Ok(metadata) => metadata, + Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None), + Err(error) => { + return Err(RusticolError::artifact(format!( + "could not inspect recurrence bootstrap {}: {error}", + path.display() + ))); + } + }; + if metadata.file_type().is_symlink() || !metadata.is_file() { + return Err(RusticolError::security(format!( + "recurrence bootstrap {} must be a regular non-symlink file", + path.display() + ))); + } + let size = usize::try_from(metadata.len()).map_err(|_| { + RusticolError::artifact("recurrence bootstrap size exceeds this platform's usize domain") + })?; + if size == 0 { + return Err(RusticolError::artifact(format!( + "recurrence bootstrap {} is empty", + path.display() + ))); + } + #[cfg(unix)] + if metadata_is_executable(&metadata) { + return Err(RusticolError::security(format!( + "recurrence bootstrap {} must not be executable", + path.display() + ))); + } + let canonical = path.canonicalize().map_err(|error| { + RusticolError::security(format!( + "could not resolve recurrence bootstrap {}: {error}", + path.display() + )) + })?; + if !canonical.starts_with(root) { + return Err(RusticolError::security(format!( + "recurrence bootstrap {} escapes the artifact root", + path.display() + ))); + } + #[cfg(unix)] + let file = { + use std::os::unix::fs::OpenOptionsExt as _; + OpenOptions::new() + .read(true) + .custom_flags(libc::O_CLOEXEC | libc::O_NOFOLLOW) + .open(path) + }; + #[cfg(not(unix))] + let file = OpenOptions::new().read(true).open(path); + let file = file.map_err(|error| { + RusticolError::artifact(format!( + "could not open recurrence bootstrap {}: {error}", + path.display() + )) + })?; + let pinned = file.metadata().map_err(|error| { + RusticolError::security(format!( + "could not inspect opened recurrence bootstrap {}: {error}", + path.display() + )) + })?; + if !pinned.is_file() || pinned.len() != metadata.len() { + return Err(RusticolError::integrity(format!( + "recurrence bootstrap {} changed while being opened", + path.display() + ))); + } + reject_symlink_chain(path)?; + let path_metadata = fs::symlink_metadata(path).map_err(|error| { + RusticolError::security(format!( + "could not re-inspect recurrence bootstrap {}: {error}", + path.display() + )) + })?; + if path_metadata.file_type().is_symlink() || !path_metadata.is_file() { + return Err(RusticolError::security(format!( + "recurrence bootstrap {} is not a regular non-symlink file", + path.display() + ))); + } + #[cfg(unix)] + { + use std::os::unix::fs::MetadataExt as _; + if pinned.dev() != path_metadata.dev() || pinned.ino() != path_metadata.ino() { + return Err(RusticolError::security(format!( + "recurrence bootstrap {} was replaced while being opened", + path.display() + ))); + } + } + let mut bytes = Vec::new(); + bytes.try_reserve_exact(size).map_err(|error| { + RusticolError::artifact(format!( + "could not allocate {size} recurrence bootstrap bytes: {error}" + )) + })?; + // Read at most one byte beyond the pinned size. This detects a concurrent + // append without allowing a growing file to bypass the allocation bound. + let mut bounded = file.take(metadata.len().saturating_add(1)); + bounded.read_to_end(&mut bytes).map_err(|error| { + RusticolError::artifact(format!( + "could not read recurrence bootstrap {}: {error}", + path.display() + )) + })?; + if bytes.len() != size { + return Err(RusticolError::integrity(format!( + "recurrence bootstrap {} changed while being read", + path.display() + ))); + } + Ok(Some(bytes)) +} + +#[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] +#[cfg_attr(target_vendor = "apple", unsafe(link_section = "__TEXT,__rcl_load"))] +#[cfg_attr(target_vendor = "apple", inline(never))] +fn discover_single_recurrence_bootstrap_process(root: &Path) -> RusticolResult> { + let processes = root.join("processes"); + let metadata = match fs::symlink_metadata(&processes) { + Ok(metadata) => metadata, + Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None), + Err(error) => { + return Err(RusticolError::artifact(format!( + "could not inspect recurrence process root {}: {error}", + processes.display() + ))); + } + }; + if metadata.file_type().is_symlink() || !metadata.is_dir() { + return Err(RusticolError::security(format!( + "recurrence process root {} must be a regular non-symlink directory", + processes.display() + ))); + } + + let entries = fs::read_dir(&processes).map_err(|error| { + RusticolError::artifact(format!( + "could not enumerate recurrence process root {}: {error}", + processes.display() + )) + })?; + let mut selected: Option = None; + for entry in entries { + let entry = entry.map_err(|error| { + RusticolError::artifact(format!( + "could not enumerate recurrence process root {}: {error}", + processes.display() + )) + })?; + let Ok(process_id) = entry.file_name().into_string() else { + continue; + }; + if validate_public_id(&process_id, "recurrence process directory").is_err() { + continue; + } + let file_type = entry.file_type().map_err(|error| { + RusticolError::artifact(format!( + "could not inspect recurrence process directory {}: {error}", + entry.path().display() + )) + })?; + if file_type.is_symlink() { + return Err(RusticolError::security(format!( + "recurrence process directory {} must not be a symlink", + entry.path().display() + ))); + } + if !file_type.is_dir() { + continue; + } + let sidecar = entry.path().join(RECURRENCE_BOOTSTRAP_FILENAME); + let sidecar_metadata = match fs::symlink_metadata(&sidecar) { + Ok(metadata) => metadata, + Err(error) if error.kind() == std::io::ErrorKind::NotFound => continue, + Err(error) => { + return Err(RusticolError::artifact(format!( + "could not inspect recurrence bootstrap {}: {error}", + sidecar.display() + ))); + } + }; + if sidecar_metadata.file_type().is_symlink() || !sidecar_metadata.is_file() { + return Err(RusticolError::security(format!( + "recurrence bootstrap {} must be a regular non-symlink file", + sidecar.display() + ))); + } + if selected.replace(process_id).is_some() { + return Err(RusticolError::integrity( + "default recurrence bootstrap selection is ambiguous", + )); + } + } + Ok(selected) +} + +#[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] +#[cfg_attr(target_vendor = "apple", unsafe(link_section = "__TEXT,__rcl_load"))] +#[cfg_attr(target_vendor = "apple", inline(never))] +fn read_canonical_manifest_artifact_id(path: &Path) -> RusticolResult { + const PREFIX: &[u8] = b"{\"artifact_id\":\""; + const SUFFIX: &[u8] = b"\","; + let read_len = PREFIX.len() + 64 + SUFFIX.len(); + #[cfg(unix)] + let file = { + use std::os::unix::fs::OpenOptionsExt as _; + OpenOptions::new() + .read(true) + .custom_flags(libc::O_CLOEXEC | libc::O_NOFOLLOW) + .open(path) + }; + #[cfg(not(unix))] + let mut file = OpenOptions::new().read(true).open(path); + let mut file = file.map_err(|error| { + RusticolError::artifact(format!( + "could not open artifact manifest {}: {error}", + path.display() + )) + })?; + if !file + .metadata() + .map_err(|error| { + RusticolError::security(format!( + "could not inspect artifact manifest {}: {error}", + path.display() + )) + })? + .is_file() + { + return Err(RusticolError::security(format!( + "artifact manifest {} is not a regular file", + path.display() + ))); + } + let mut prefix = vec![0_u8; read_len]; + file.read_exact(&mut prefix).map_err(|error| { + RusticolError::serialization(format!( + "artifact manifest {} has no canonical artifact-ID prefix: {error}", + path.display() + )) + })?; + let digest = &prefix[PREFIX.len()..PREFIX.len() + 64]; + if !prefix.starts_with(PREFIX) + || &prefix[PREFIX.len() + 64..] != SUFFIX + || !digest + .iter() + .all(|byte| byte.is_ascii_digit() || matches!(byte, b'a'..=b'f')) + { + return Err(RusticolError::integrity(format!( + "artifact manifest {} is not canonical at its artifact-ID prefix", + path.display() + ))); + } + String::from_utf8(digest.to_vec()).map_err(|error| { + RusticolError::internal(format!( + "validated ASCII artifact ID was not UTF-8: {error}" + )) + }) +} + +#[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] +#[cfg_attr(target_vendor = "apple", unsafe(link_section = "__TEXT,__rcl_load"))] +#[cfg_attr(target_vendor = "apple", inline(never))] +fn compute_payload_artifact_id(payloads: &[Payload]) -> RusticolResult { + let mut canonical = String::new(); + canonical + .try_reserve(payloads.len().checked_mul(256).unwrap_or_default()) + .map_err(|error| { + RusticolError::artifact(format!( + "could not allocate recurrence artifact identity buffer: {error}" + )) + })?; + canonical.push_str("{\"kind\":\"pyamplicol-runtime-payload-identity\",\"payloads\":["); + let mut previous_path: Option<&str> = None; + let mut first = true; + for payload in payloads.iter().filter(|payload| { + matches!( + payload.role, + PayloadRole::CompiledModel + | PayloadRole::EvaluatorManifest + | PayloadRole::EvaluatorState + | PayloadRole::ModelParameters + | PayloadRole::RuntimePhysics + ) + }) { + if previous_path.is_some_and(|previous| previous >= payload.path.as_str()) { + return Err(RusticolError::integrity( + "recurrence bootstrap payload inventory must be strictly path-sorted", + )); + } + previous_path = Some(&payload.path); + if !first { + canonical.push(','); + } + first = false; + canonical.push_str("{\"executable\":"); + canonical.push_str(if payload.executable { "true" } else { "false" }); + canonical.push_str(",\"media_type\":"); + write_python_json_string(&payload.media_type, &mut canonical); + canonical.push_str(",\"path\":"); + write_python_json_string(&payload.path, &mut canonical); + if let Some(process_id) = &payload.process_id { + canonical.push_str(",\"process_id\":"); + write_python_json_string(process_id, &mut canonical); + } + canonical.push_str(",\"role\":"); + write_python_json_string(payload_role_name(payload.role), &mut canonical); + canonical.push_str(",\"sha256\":"); + write_python_json_string(&payload.sha256, &mut canonical); + canonical.push_str(",\"size_bytes\":"); + let _ = write!(canonical, "{}", payload.size_bytes); + if let Some(target) = &payload.target { + canonical.push_str(",\"target\":{\"cpu_features\":["); + for (feature_index, feature) in target.cpu_features.iter().enumerate() { + if feature_index != 0 { + canonical.push(','); + } + write_python_json_string(feature, &mut canonical); + } + canonical.push_str("],\"triple\":"); + write_python_json_string(&target.triple, &mut canonical); + canonical.push('}'); + } + canonical.push('}'); + } + canonical.push_str("],\"schema_version\":1}\n"); + Ok(format!("{:x}", Sha256::digest(canonical.as_bytes()))) +} + +fn payload_role_name(role: PayloadRole) -> &'static str { + match role { + PayloadRole::ConfigurationRequested => "configuration-requested", + PayloadRole::ConfigurationEffective => "configuration-effective", + PayloadRole::CompiledModel => "compiled-model", + PayloadRole::RuntimePhysics => "runtime-physics", + PayloadRole::EvaluatorManifest => "evaluator-manifest", + PayloadRole::EvaluatorState => "evaluator-state", + PayloadRole::ModelParameters => "model-parameters", + PayloadRole::ValidationMomenta => "validation-momenta", + PayloadRole::StructuralSourceProof => "structural-source-proof", + PayloadRole::ApiSource => "api-source", + PayloadRole::ApiBuildFile => "api-build-file", + PayloadRole::SdkMetadata => "sdk-metadata", + PayloadRole::ProcessGroups => "process-groups", + } +} + +#[cfg_attr(target_vendor = "apple", unsafe(link_section = "__TEXT,__rcl_load"))] +#[cfg_attr(target_vendor = "apple", inline(never))] +fn validate_recurrence_bootstrap_metadata( + producer: &Producer, + runtime: &ArtifactRuntime, +) -> RusticolResult<()> { + if producer.distribution != "pyamplicol" + || runtime.engine != "rusticol" + || runtime.engine_version != producer.version + || !compatible_distribution_version(&producer.version) + { + return Err(RusticolError::compatibility(format!( + "recurrence bootstrap producer/runtime {}/{}/{} is incompatible with Rusticol {}", + producer.distribution, + producer.version, + runtime.engine_version, + rusticol_package_version() + ))); + } + let versions = &producer.versions; + for (name, found, expected) in [ + ("python API", versions.python_api, PYTHON_API_VERSION), + ("TOML", versions.toml, TOML_SCHEMA_VERSION), + ( + "compiled model", + versions.compiled_model, + COMPILED_MODEL_SCHEMA_VERSION, + ), + ( + "process artifact", + versions.process_artifact, + PROCESS_ARTIFACT_SCHEMA_VERSION, + ), + ( + "runtime physics", + versions.runtime_physics, + RUNTIME_PHYSICS_SCHEMA_VERSION, + ), + ("C ABI", versions.c_abi, C_ABI_VERSION), + ] { + if found != expected { + return Err(RusticolError::compatibility(format!( + "recurrence bootstrap {name} version {found} is incompatible with {expected}" + ))); + } + } + if let Some(revision) = &producer.git_revision + && (revision.len() != 40 + || !revision + .bytes() + .all(|byte| byte.is_ascii_hexdigit() && !byte.is_ascii_uppercase())) + { + return Err(RusticolError::artifact( + "recurrence bootstrap producer git revision is not canonical", + )); + } + if let Some(identity) = &producer.native_build_inputs_sha256 { + validate_sha256(identity, "recurrence bootstrap native build identity")?; + } + validate_relative_path( + &runtime.evaluator_manifest_path, + "recurrence bootstrap evaluator manifest path", + )?; + if let Some(path) = &runtime.api_bundle_path { + validate_relative_path(path, "recurrence bootstrap API bundle path")?; + } + let capabilities = validate_runtime_capabilities( + &runtime.required_runtime_capabilities, + "recurrence bootstrap runtime capabilities", + )?; + validate_target(&producer.target, "recurrence bootstrap producer")?; + validate_portable_runtime_capabilities(&producer.target, &capabilities) +} + +#[cfg_attr(target_vendor = "apple", unsafe(link_section = "__TEXT,__rcl_load"))] +#[cfg_attr(target_vendor = "apple", inline(never))] +fn validate_recurrence_bootstrap_process(process: &ArtifactProcess) -> RusticolResult<()> { + validate_public_id(&process.id, "recurrence bootstrap process id")?; + if process.expression.is_empty() + || process.external_pdgs.len() < 3 + || !matches!(process.color_accuracy.as_str(), "lc" | "nlc" | "full") + { + return Err(RusticolError::artifact(format!( + "recurrence bootstrap process {:?} has invalid public metadata", + process.id + ))); + } + let expected_physics = format!("processes/{}/physics.json", process.id); + if process.physics_path != expected_physics { + return Err(RusticolError::security(format!( + "recurrence bootstrap process {:?} physics path must be {expected_physics:?}", + process.id + ))); + } + validate_runtime_capabilities( + &process.required_runtime_capabilities, + "recurrence bootstrap process capabilities", + )?; + if process.aliases.is_empty() { + return Ok(()); + } + let initial_count = parse_process_expression(&process.expression) + .map(|tokens| tokens.initial.len()) + .ok_or_else(|| { + RusticolError::artifact(format!( + "recurrence bootstrap process expression {:?} is invalid", + process.expression + )) + })?; + let mut aliases = BTreeSet::new(); + for alias in &process.aliases { + validate_public_id(&alias.id, "recurrence bootstrap alias id")?; + if !aliases.insert(alias.id.as_str()) + || alias.expression.is_empty() + || alias.external_pdgs.len() != process.external_pdgs.len() + { + return Err(RusticolError::artifact(format!( + "recurrence bootstrap alias {:?} is invalid", + alias.id + ))); + } + validate_permutation( + &alias.external_permutation, + process.external_pdgs.len(), + &format!("alias {:?}", alias.id), + )?; + if !permutation_preserves_process_sides(&alias.external_permutation, initial_count) { + return Err(RusticolError::artifact(format!( + "recurrence bootstrap alias {:?} crosses process sides", + alias.id + ))); + } + } + Ok(()) +} + +#[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] +#[cfg_attr(target_vendor = "apple", unsafe(link_section = "__TEXT,__rcl_load"))] +#[cfg_attr(target_vendor = "apple", inline(never))] +fn validate_recurrence_bootstrap_source_payload( + payloads: &BTreeMap, + path: &str, + sha256: &str, + role: PayloadRole, + process_id: &str, +) -> RusticolResult<()> { + let payload = payloads.get(path).ok_or_else(|| { + RusticolError::security(format!( + "recurrence bootstrap source {path:?} is not a declared payload" + )) + })?; + if payload.role != role + || payload.media_type != "application/json" + || payload.process_id.as_deref() != Some(process_id) + || payload.executable + || payload.sha256 != sha256 + { + return Err(RusticolError::integrity(format!( + "recurrence bootstrap source {path:?} disagrees with its payload declaration" + ))); + } + Ok(()) +} + +fn recurrence_bootstrap_manifest( + artifact_id: String, + producer: Producer, + runtime: ArtifactRuntime, + process: ArtifactProcess, + payloads: Vec, +) -> ArtifactManifest { + let process_id = process.id.clone(); + ArtifactManifest { + schema_version: PROCESS_ARTIFACT_SCHEMA_VERSION, + kind: ArtifactKind::PyamplicolProcess, + artifact_id, + created_utc: "1970-01-01T00:00:00Z".to_string(), + producer, + model: ArtifactModel { + name: "recurrence-bootstrap".to_string(), + source_kind: ModelSourceKind::CompiledModel, + content_sha256: "0".repeat(64), + compiled_schema_version: COMPILED_MODEL_SCHEMA_VERSION, + restriction: None, + }, + configuration: ArtifactConfiguration { + toml_schema_version: TOML_SCHEMA_VERSION, + requested_path: "config/requested.toml".to_string(), + effective_path: "config/effective.toml".to_string(), + adjustments: Vec::new(), + }, + processes: vec![process], + default_process_id: Some(process_id), + runtime, + payloads, + dependencies: Vec::new(), + extensions: BTreeMap::new(), + } +} + +/// Construct the same narrowly-scoped artifact view while generation still +/// owns the private staging directory. This is used only to lower the ready +/// recipe through the authoritative PACBIN/binding loaders; the staging path +/// itself is never serialized. +#[cfg(feature = "python-generation-bridge")] +pub(crate) fn open_staged_recurrence_bootstrap_artifact( + root: PathBuf, + producer: Producer, + runtime: ArtifactRuntime, + process: ArtifactProcess, + payloads: Vec, + evaluator_payload_container: EvaluatorPayloadContainerExtension, +) -> RusticolResult { + let root = fs::canonicalize(&root).map_err(|error| { + RusticolError::artifact(format!( + "could not resolve recurrence bootstrap staging root {}: {error}", + root.display() + )) + })?; + if !root.is_dir() { + return Err(RusticolError::artifact(format!( + "recurrence bootstrap staging root {} is not a directory", + root.display() + ))); + } + validate_recurrence_bootstrap_metadata(&producer, &runtime)?; + validate_recurrence_bootstrap_process(&process)?; + let manifest = + recurrence_bootstrap_manifest("0".repeat(64), producer, runtime, process, payloads); + let payloads = validated_payload_declarations(&manifest)?; + for payload in payloads.values() { + if let Some(target) = &payload.target { + validate_payload_target(&manifest.producer.target, target, &payload.path)?; + validate_target(target, &format!("payload {}", payload.path))?; + } + } + validate_evaluator_payload_container_declaration(&evaluator_payload_container, &payloads)?; + Ok(VerifiedArtifact { + manifest_path: root.join(ARTIFACT_MANIFEST_FILE), + root, + manifest, + payloads: Arc::new(payloads), + // Process-ready lowering reads only loose plan/binding/color payloads. + evaluator_payload_container: None, + #[cfg(feature = "f64-compiled")] + native_library_cache: Arc::new(Mutex::new(BTreeMap::new())), + }) +} + +fn load_evaluator_payload_container( + root: &Path, + manifest: &ArtifactManifest, + payloads: &BTreeMap, +) -> RusticolResult>> { + let Some(raw) = manifest + .extensions + .get(EVALUATOR_PAYLOAD_CONTAINER_EXTENSION) + else { + return Ok(None); + }; + let extension: EvaluatorPayloadContainerExtension = serde_json::from_value(raw.clone()) + .map_err(|error| { + RusticolError::artifact(format!( + "artifact extension {EVALUATOR_PAYLOAD_CONTAINER_EXTENSION:?} is invalid: {error}" + )) + })?; + load_evaluator_payload_container_extension(root, &extension, payloads) +} + +fn load_evaluator_payload_container_extension( + root: &Path, + extension: &EvaluatorPayloadContainerExtension, + payloads: &BTreeMap, +) -> RusticolResult>> { + let payload = validate_evaluator_payload_container_declaration(extension, payloads)?; + let expected_payload_sha = parse_sha256( + &payload.sha256, + "evaluator payload container payload SHA-256", + )?; + let container_path = root.join(&extension.path); + let container_file = open_checked_payload(root, payload)?; + let reader = PacbinReader::open_file_with_sha256( + container_file, + &container_path, + &expected_payload_sha, + )?; + let index = reader.index(); + if index.version() != 1 + || u64::try_from(index.members().len()).unwrap_or(u64::MAX) != extension.member_count + || hex_digest(index.index_sha256()) != extension.index_sha256 + { + return Err(RusticolError::integrity( + "evaluator payload container metadata disagrees with its authenticated index", + )); + } + let unpacked_size = index.members().iter().try_fold(0_u64, |total, member| { + total + .checked_add(member.length()) + .ok_or_else(|| RusticolError::integrity("packed evaluator size exceeds u64")) + })?; + if unpacked_size != extension.unpacked_size_bytes { + return Err(RusticolError::integrity( + "evaluator payload container unpacked size disagrees with its index", + )); + } + for member in index.members() { + validate_relative_path(member.logical_path(), "packed evaluator logical path")?; + if payloads.contains_key(member.logical_path()) + || member.logical_path() == extension.path + || !matches!( + member.kind(), + PacbinMemberKind::SymjitApplication + | PacbinMemberKind::SymbolicaExactState + | PacbinMemberKind::ColorContraction + ) + { + return Err(RusticolError::integrity(format!( + "invalid packed evaluator member {:?}", + member.logical_path() + ))); + } + } + Ok(Some(Arc::new(reader))) +} + +#[cfg_attr(target_vendor = "apple", unsafe(link_section = "__TEXT,__rcl_load"))] +#[cfg_attr(target_vendor = "apple", inline(never))] +fn validate_evaluator_payload_container_declaration<'a>( + extension: &EvaluatorPayloadContainerExtension, + payloads: &'a BTreeMap, +) -> RusticolResult<&'a Payload> { + if extension.kind != EVALUATOR_PAYLOAD_CONTAINER_KIND + || extension.schema_version != 1 + || extension.storage_abi != EVALUATOR_PAYLOAD_CONTAINER_STORAGE_ABI + { + return Err(RusticolError::compatibility(format!( + "unsupported evaluator payload container kind/version/ABI: {:?}/{}/{}", + extension.kind, extension.schema_version, extension.storage_abi + ))); + } + validate_relative_path(&extension.path, "evaluator payload container path")?; + validate_sha256( + &extension.index_sha256, + "evaluator payload container index_sha256", + )?; + let payload = payloads.get(&extension.path).ok_or_else(|| { + RusticolError::security(format!( + "evaluator payload container {:?} is not a declared payload", + extension.path + )) + })?; + if payload.role != PayloadRole::EvaluatorState + || payload.media_type != "application/octet-stream" + || payload.process_id.is_some() + { + return Err(RusticolError::artifact( + "evaluator payload container must be a root evaluator-state octet-stream payload", + )); + } + parse_sha256( + &payload.sha256, + "evaluator payload container payload SHA-256", + )?; + Ok(payload) +} + +fn confined_evaluator_path(value: &str) -> RusticolResult<&Path> { + let path = Path::new(value); + if value.is_empty() + || value.contains('\\') + || path.is_absolute() + || path + .components() + .any(|component| !matches!(component, std::path::Component::Normal(_))) + { + return Err(RusticolError::security(format!( + "evaluator payload path {value:?} is not a confined relative path" + ))); + } + Ok(path) +} + +fn artifact_logical_path(root: &Path, path: &Path) -> RusticolResult { + let relative = path + .strip_prefix(root) + .map_err(|_| RusticolError::security("evaluator payload path escapes the artifact root"))?; + let logical = relative + .to_str() + .ok_or_else(|| RusticolError::security("evaluator payload path is not valid UTF-8"))?; + Ok(logical.replace(std::path::MAIN_SEPARATOR, "/")) +} + +fn hex_digest(bytes: &[u8]) -> String { + let mut result = String::with_capacity(bytes.len() * 2); + for byte in bytes { + use std::fmt::Write as _; + let _ = write!(result, "{byte:02x}"); + } + result +} + +impl ArtifactManifest { + pub(crate) fn select_process( + &self, + requested: Option<&str>, + ) -> RusticolResult { + self.select_process_with_groups(requested, || Ok(None)) + } + + /// Resolve `requested` in precedence order. `groups` is called at most + /// once, and only after every stored process, alias, and side permutation + /// of their expressions failed to match. + pub(crate) fn select_process_with_groups( + &self, + requested: Option<&str>, + groups: impl FnOnce() -> RusticolResult>, + ) -> RusticolResult { + let selected_id = if let Some(requested) = requested { + requested + } else if let Some(default) = self.default_process_id.as_deref() { + default + } else if self.processes.len() == 1 { + self.processes[0].id.as_str() + } else { + return Err(RusticolError::selector( + "this process-set artifact has no default process; select a process or alias id", + )); + }; + for process in &self.processes { + if process.id == selected_id { + return Ok(representative_selection(process)); + } + if let Some(alias) = process.aliases.iter().find(|alias| alias.id == selected_id) { + return Ok(alias_selection(process, alias)); + } + } + + let requested_expression = normalize_process_expression(selected_id); + let mut process_expression_matches = self + .processes + .iter() + .filter(|process| { + normalize_process_expression(&process.expression) == requested_expression + }) + .map(representative_selection) + .collect::>(); + match process_expression_matches.len() { + 1 => { + return Ok(process_expression_matches + .pop() + .expect("one process expression match")); + } + count if count > 1 => { + let mut matching_ids = process_expression_matches + .iter() + .map(|selection| selection.requested_id.as_str()) + .collect::>(); + matching_ids.sort_unstable(); + return Err(RusticolError::selector(format!( + "process expression {selected_id:?} is ambiguous; select one of these stable ids: {}", + matching_ids.join(", ") + ))); + } + _ => {} + } + let mut alias_expression_matches = self + .processes + .iter() + .flat_map(|process| { + process + .aliases + .iter() + .filter(|alias| { + normalize_process_expression(&alias.expression) == requested_expression + }) + .map(|alias| alias_selection(process, alias)) + }) + .collect::>(); + match alias_expression_matches.len() { + 1 => { + return Ok(alias_expression_matches + .pop() + .expect("one alias expression match")); + } + count if count > 1 => { + let mut matching_ids = alias_expression_matches + .iter() + .map(|selection| selection.requested_id.as_str()) + .collect::>(); + matching_ids.sort_unstable(); + return Err(RusticolError::selector(format!( + "process expression {selected_id:?} is ambiguous; select one of these stable ids: {}", + matching_ids.join(", ") + ))); + } + _ => {} + } + + let Some(requested_tokens) = parse_process_expression(selected_id) else { + if let Some(groups) = groups()? + && let Some(selection) = groups.select(self, selected_id, None)? + { + return Ok(selection); + } + if selected_id.contains('>') { + return Err(RusticolError::selector(format!( + "concrete process expression {selected_id:?} must contain one '>' with at least one particle on each side" + ))); + } + return Err(RusticolError::selector(format!( + "unknown process id or alias id {selected_id:?}" + ))); + }; + let mut permutation_matches = Vec::new(); + let mut crossing_only_matches = Vec::new(); + for process in &self.processes { + let Some(representative_tokens) = parse_process_expression(&process.expression) else { + continue; + }; + if let Some(permutation) = + side_preserving_process_permutation(&representative_tokens, &requested_tokens) + { + let mut external_pdgs = vec![0; process.external_pdgs.len()]; + for (representative_index, public_index) in permutation.iter().copied().enumerate() + { + external_pdgs[public_index] = process.external_pdgs[representative_index]; + } + permutation_matches.push(ArtifactSelection { + process: process.clone(), + // An inferred expression has no persisted alias identity; + // retain the representative stable process id. + requested_id: process.id.clone(), + alias: None, + public_expression: requested_tokens.canonical.clone(), + external_pdgs, + external_permutation: permutation, + inferred_permutation: true, + process_group: None, + }); + } else if process_tokens_are_permutation_equivalent_ignoring_sides( + &representative_tokens, + &requested_tokens, + ) { + crossing_only_matches.push(process.id.as_str()); + } + } + if permutation_matches.len() > 1 { + // Multiprocess artifacts commonly retain both incoming orderings + // of the same physical channel. Prefer the representative that + // already agrees with the requested ordering in the most slots; + // only genuinely tied representatives remain ambiguous. + let minimum_displacement = permutation_matches + .iter() + .map(|selection| permutation_displacement(&selection.external_permutation)) + .min() + .expect("more than one permutation match"); + permutation_matches.retain(|selection| { + permutation_displacement(&selection.external_permutation) == minimum_displacement + }); + } + match permutation_matches.len() { + 1 => return Ok(permutation_matches.pop().expect("one permutation match")), + count if count > 1 => { + let mut matching_ids = permutation_matches + .iter() + .map(|selection| selection.process.id.as_str()) + .collect::>(); + matching_ids.sort_unstable(); + matching_ids.dedup(); + return Err(RusticolError::selector(format!( + "process expression {selected_id:?} is permutation-ambiguous; select one of these stable ids: {}", + matching_ids.join(", ") + ))); + } + _ => {} + } + if let Some(groups) = groups()? + && let Some(selection) = groups.select(self, selected_id, Some(&requested_tokens))? + { + return Ok(selection); + } + if !crossing_only_matches.is_empty() { + crossing_only_matches.sort_unstable(); + crossing_only_matches.dedup(); + return Err(RusticolError::selector(format!( + "process expression {selected_id:?} would move a particle across the '>' boundary; only permutations within the incoming and outgoing sides are supported (matching stable ids: {})", + crossing_only_matches.join(", ") + ))); + } + Err(RusticolError::selector(format!( + "unknown process id, alias id, or concrete process expression {selected_id:?}" + ))) + } +} + +fn representative_selection(process: &ArtifactProcess) -> ArtifactSelection { + ArtifactSelection { + process: process.clone(), + requested_id: process.id.clone(), + alias: None, + public_expression: process.expression.clone(), + external_pdgs: process.external_pdgs.clone(), + external_permutation: (0..process.external_pdgs.len()).collect(), + inferred_permutation: false, + process_group: None, + } +} + +fn alias_selection(process: &ArtifactProcess, alias: &ProcessAlias) -> ArtifactSelection { + ArtifactSelection { + process: process.clone(), + requested_id: alias.id.clone(), + alias: Some(alias.clone()), + public_expression: alias.expression.clone(), + external_pdgs: alias.external_pdgs.clone(), + external_permutation: alias.external_permutation.clone(), + inferred_permutation: false, + process_group: None, + } +} + +#[derive(Clone, Debug)] +struct ProcessExpressionTokens { + initial: Vec, + final_state: Vec, + canonical: String, +} + +fn parse_process_expression(expression: &str) -> Option { + let tokens = expression + .split_whitespace() + .map(str::to_lowercase) + .collect::>(); + let separators = tokens + .iter() + .enumerate() + .filter_map(|(index, token)| (token == ">").then_some(index)) + .collect::>(); + let [separator] = separators.as_slice() else { + return None; + }; + if *separator == 0 || *separator + 1 == tokens.len() { + return None; + } + let initial = tokens[..*separator].to_vec(); + let final_state = tokens[*separator + 1..].to_vec(); + Some(ProcessExpressionTokens { + canonical: format!("{} > {}", initial.join(" "), final_state.join(" ")), + initial, + final_state, + }) +} + +fn side_preserving_process_permutation( + representative: &ProcessExpressionTokens, + public: &ProcessExpressionTokens, +) -> Option> { + if representative.initial.len() != public.initial.len() + || representative.final_state.len() != public.final_state.len() + { + return None; + } + let initial = deterministic_side_permutation(&representative.initial, &public.initial, 0)?; + let final_offset = representative.initial.len(); + let final_state = deterministic_side_permutation( + &representative.final_state, + &public.final_state, + final_offset, + )?; + Some(initial.into_iter().chain(final_state).collect()) +} + +/// Match repeated particles deterministically by assigning each representative +/// occurrence to the first still-unused equal public occurrence. +fn deterministic_side_permutation( + representative: &[String], + public: &[String], + offset: usize, +) -> Option> { + let mut used = vec![false; public.len()]; + representative + .iter() + .map(|particle| { + let public_index = public.iter().enumerate().find_map(|(index, candidate)| { + (!used[index] && candidate == particle).then_some(index) + })?; + used[public_index] = true; + Some(offset + public_index) + }) + .collect() +} + +fn permutation_displacement(permutation: &[usize]) -> usize { + permutation + .iter() + .copied() + .enumerate() + .filter(|(representative_index, public_index)| representative_index != public_index) + .count() +} + +fn process_tokens_are_permutation_equivalent_ignoring_sides( + representative: &ProcessExpressionTokens, + public: &ProcessExpressionTokens, +) -> bool { + let mut representative = representative + .initial + .iter() + .chain(&representative.final_state) + .cloned() + .collect::>(); + let mut public = public + .initial + .iter() + .chain(&public.final_state) + .cloned() + .collect::>(); + representative.sort_unstable(); + public.sort_unstable(); + representative == public +} + +fn normalize_process_expression(expression: &str) -> String { + expression + .split_whitespace() + .map(str::to_lowercase) + .collect::>() + .join(" ") +} + +const PROCESS_GROUPS_KIND: &str = "pyamplicol-process-groups"; +const PROCESS_GROUPS_SCHEMA_VERSION: u64 = 1; + +// Unknown keys (`analyses`, `basis`, `warning`, ...) are ignored on purpose. +#[derive(Deserialize)] +struct ProcessGroupsDocument { + kind: String, + schema_version: u64, + groups: Vec, +} + +#[derive(Deserialize)] +struct ProcessGroupRecord { + representative: ProcessGroupRepresentativeRecord, + members: Vec, +} + +#[derive(Deserialize)] +struct ProcessGroupRepresentativeRecord { + process_id: String, +} + +#[derive(Deserialize)] +struct ProcessGroupMemberRecord { + process_id: String, + process: String, + pdgs: Vec, + initial_count: usize, + factor: ProcessGroupFactorRecord, + permutation: Vec, + conjugated: bool, + generated: bool, +} + +#[derive(Deserialize)] +struct ProcessGroupFactorRecord { + numerator: i64, + denominator: i64, +} + +/// The non-generated members of a validated `process-groups.json`. +#[derive(Clone, Debug)] +pub(crate) struct ProcessGroups { + members: Vec, +} + +#[derive(Clone, Debug)] +struct ProcessGroupMember { + id: String, + expression: String, + tokens: ProcessExpressionTokens, + /// Particle names and PDGs in the member's own order. + particles: Vec, + pdgs: Vec, + representative_id: String, + /// Representative external index to member external index. + permutation: Vec, + factor_numerator: u64, + factor_denominator: u64, + conjugated: bool, +} + +impl ProcessGroups { + pub(crate) fn from_json(bytes: &[u8], manifest: &ArtifactManifest) -> RusticolResult { + let document: ProcessGroupsDocument = serde_json::from_slice(bytes).map_err(|error| { + RusticolError::serialization(format!( + "could not parse the process-groups payload: {error}" + )) + })?; + if document.kind != PROCESS_GROUPS_KIND + || document.schema_version != PROCESS_GROUPS_SCHEMA_VERSION + { + return Err(RusticolError::compatibility(format!( + "unsupported process-groups payload kind {:?} schema {}", + document.kind, document.schema_version + ))); + } + let stored_ids = manifest + .processes + .iter() + .flat_map(|process| { + std::iter::once(process.id.as_str()) + .chain(process.aliases.iter().map(|alias| alias.id.as_str())) + }) + .collect::>(); + let mut member_ids = BTreeSet::new(); + let mut members = Vec::new(); + for group in document.groups { + let representative_id = group.representative.process_id; + let process = manifest + .processes + .iter() + .find(|process| process.id == representative_id) + .ok_or_else(|| { + RusticolError::artifact(format!( + "process-group representative {representative_id:?} is not a stored process" + )) + })?; + let count = process.external_pdgs.len(); + let initial_count = parse_process_expression(&process.expression) + .map(|tokens| tokens.initial.len()) + .ok_or_else(|| { + RusticolError::artifact(format!( + "process {representative_id:?} has no incoming and outgoing particles" + )) + })?; + let identity = group.members.first().is_some_and(|member| { + member.process_id == representative_id + && member.generated + && !member.conjugated + && member.factor.numerator == member.factor.denominator + && member.permutation.iter().copied().eq(0..count) + }); + if !identity { + return Err(RusticolError::artifact(format!( + "process group {representative_id:?} must list its representative first with the identity relation" + ))); + } + for member in group.members { + let id = member.process_id; + validate_public_id(&id, "process-group member id")?; + if !member_ids.insert(id.clone()) { + return Err(RusticolError::artifact(format!( + "process-group member {id:?} is listed more than once" + ))); + } + if member.generated { + if !manifest.processes.iter().any(|process| process.id == id) { + return Err(RusticolError::artifact(format!( + "generated process-group member {id:?} is not a stored process" + ))); + } + continue; + } + if stored_ids.contains(id.as_str()) { + return Err(RusticolError::artifact(format!( + "non-generated process-group member {id:?} reuses a stored process or alias id" + ))); + } + let tokens = parse_process_expression(&member.process) + .filter(|tokens| { + tokens.initial.len() == initial_count + && tokens.initial.len() + tokens.final_state.len() == count + }) + .ok_or_else(|| { + RusticolError::artifact(format!( + "process-group member {id:?} expression {:?} does not have the incoming and outgoing particle counts of {representative_id:?}", + member.process + )) + })?; + if member.pdgs.len() != count || member.initial_count != initial_count { + return Err(RusticolError::artifact(format!( + "process-group member {id:?} PDGs or initial count disagree with {representative_id:?}" + ))); + } + validate_permutation( + &member.permutation, + count, + &format!("process-group member {id:?}"), + )?; + if !permutation_preserves_process_sides(&member.permutation, initial_count) { + return Err(RusticolError::artifact(format!( + "process-group member {id:?} permutation moves a particle across the '>' boundary" + ))); + } + let (factor_numerator, factor_denominator) = positive_rational( + member.factor.numerator, + member.factor.denominator, + ) + .ok_or_else(|| { + RusticolError::artifact(format!( + "process-group member {id:?} factor {}/{} is not a positive rational", + member.factor.numerator, member.factor.denominator + )) + })?; + members.push(ProcessGroupMember { + particles: member + .process + .split_whitespace() + .filter(|token| *token != ">") + .map(str::to_string) + .collect(), + id, + expression: member.process, + tokens, + pdgs: member.pdgs, + representative_id: representative_id.clone(), + permutation: member.permutation, + factor_numerator, + factor_denominator, + conjugated: member.conjugated, + }); + } + } + Ok(Self { members }) + } + + /// Match a member by stable ID, exact expression, or side permutation of + /// its expression, mirroring the precedence used for stored processes. + fn select( + &self, + manifest: &ArtifactManifest, + selected_id: &str, + requested: Option<&ProcessExpressionTokens>, + ) -> RusticolResult> { + if let Some(member) = self.members.iter().find(|member| member.id == selected_id) { + return member.selection(manifest, None).map(Some); + } + let Some(requested) = requested else { + return Ok(None); + }; + let exact = self + .members + .iter() + .filter(|member| member.tokens.canonical == requested.canonical) + .collect::>(); + match exact.as_slice() { + [] => {} + [member] => return member.selection(manifest, None).map(Some), + _ => { + return Err(ambiguous_member_selection( + selected_id, + "is ambiguous", + exact.iter().map(|member| member.id.as_str()), + )); + } + } + let mut matches = self + .members + .iter() + .filter_map(|member| { + side_preserving_process_permutation(&member.tokens, requested) + .map(|permutation| (member, permutation)) + }) + .collect::>(); + if let Some(minimum) = matches + .iter() + .map(|(_, permutation)| permutation_displacement(permutation)) + .min() + { + matches.retain(|(_, permutation)| permutation_displacement(permutation) == minimum); + } + match matches.as_slice() { + [] => Ok(None), + [(member, permutation)] => member + .selection(manifest, Some((permutation, &requested.canonical))) + .map(Some), + _ => Err(ambiguous_member_selection( + selected_id, + "is permutation-ambiguous", + matches.iter().map(|(member, _)| member.id.as_str()), + )), + } + } +} + +impl ProcessGroupMember { + /// `public` is the member-index to public-index permutation and the + /// public expression of an inferred ordering; `None` keeps the member's. + fn selection( + &self, + manifest: &ArtifactManifest, + public: Option<(&[usize], &str)>, + ) -> RusticolResult { + let process = manifest + .processes + .iter() + .find(|process| process.id == self.representative_id) + .ok_or_else(|| { + RusticolError::internal("process-group representative left the manifest") + })?; + let count = self.pdgs.len(); + let identity = (0..count).collect::>(); + let (member_to_public, public_expression) = + public.unwrap_or((&identity, self.expression.as_str())); + let mut external_pdgs = vec![0; count]; + let mut particles = vec![String::new(); count]; + for (member_index, public_index) in member_to_public.iter().copied().enumerate() { + external_pdgs[public_index] = self.pdgs[member_index]; + particles[public_index].clone_from(&self.particles[member_index]); + } + Ok(ArtifactSelection { + process: process.clone(), + requested_id: self.id.clone(), + alias: None, + public_expression: public_expression.to_string(), + external_pdgs, + // Representative -> member -> public. + external_permutation: self + .permutation + .iter() + .map(|member_index| member_to_public[*member_index]) + .collect(), + inferred_permutation: public.is_some(), + process_group: Some(ProcessGroupRelation { + factor_numerator: self.factor_numerator, + factor_denominator: self.factor_denominator, + conjugated: self.conjugated, + external_particles: particles, + }), + }) + } +} + +fn ambiguous_member_selection<'a>( + selected_id: &str, + problem: &str, + ids: impl Iterator, +) -> RusticolError { + let mut ids = ids.collect::>(); + ids.sort_unstable(); + RusticolError::selector(format!( + "process expression {selected_id:?} {problem}; select one of these process-group member ids: {}", + ids.join(", ") + )) +} + +/// Reduce `numerator / denominator` and return it when it is positive. +fn positive_rational(numerator: i64, denominator: i64) -> Option<(u64, u64)> { + if numerator == 0 || denominator == 0 || (numerator < 0) != (denominator < 0) { + return None; + } + let (mut numerator, mut denominator) = (numerator.unsigned_abs(), denominator.unsigned_abs()); + let (mut left, mut right) = (numerator, denominator); + while right != 0 { + (left, right) = (right, left % right); + } + numerator /= left; + denominator /= left; + Some((numerator, denominator)) +} + +#[cfg(test)] +fn compute_artifact_id(manifest: &Value) -> RusticolResult { + let payloads = manifest + .get("payloads") + .and_then(Value::as_array) + .ok_or_else(|| RusticolError::artifact("payloads must be an array"))?; + let mut runtime_records = Vec::new(); + for (index, payload) in payloads.iter().enumerate() { + let role = payload.get("role").and_then(Value::as_str).ok_or_else(|| { + RusticolError::artifact(format!("payloads[{index}].role must be a non-empty string")) + })?; + if RUNTIME_IDENTITY_PAYLOAD_ROLES.contains(&role) { + runtime_records.push(payload.clone()); + } + } + runtime_records.sort_by(|left, right| { + left.get("path") + .and_then(Value::as_str) + .unwrap_or_default() + .cmp( + right + .get("path") + .and_then(Value::as_str) + .unwrap_or_default(), + ) + }); + let content = serde_json::json!({ + "kind": "pyamplicol-runtime-payload-identity", + "schema_version": 1, + "payloads": runtime_records, + }); + let mut canonical = String::new(); + write_python_canonical_json(&content, &mut canonical)?; + canonical.push('\n'); + Ok(format!("{:x}", Sha256::digest(canonical.as_bytes()))) +} + +#[cfg(test)] +fn write_python_canonical_json(value: &Value, output: &mut String) -> RusticolResult<()> { + match value { + Value::Null => output.push_str("null"), + Value::Bool(true) => output.push_str("true"), + Value::Bool(false) => output.push_str("false"), + Value::Number(number) => output.push_str(&python_number(number)), + Value::String(value) => write_python_json_string(value, output), + Value::Array(values) => { + output.push('['); + for (index, value) in values.iter().enumerate() { + if index != 0 { + output.push(','); + } + write_python_canonical_json(value, output)?; + } + output.push(']'); + } + Value::Object(values) => { + output.push('{'); + let mut keys = values.keys().collect::>(); + keys.sort_unstable(); + for (index, key) in keys.iter().enumerate() { + if index != 0 { + output.push(','); + } + write_python_json_string(key, output); + output.push(':'); + write_python_canonical_json(&values[*key], output)?; + } + output.push('}'); + } + } + Ok(()) +} + +#[cfg(test)] +fn python_number(number: &serde_json::Number) -> String { + let rendered = number.to_string(); + let Some((mantissa, exponent)) = rendered.split_once('e') else { + return rendered; + }; + let (sign, digits) = if let Some(digits) = exponent.strip_prefix('-') { + ('-', digits) + } else if let Some(digits) = exponent.strip_prefix('+') { + ('+', digits) + } else { + ('+', exponent) + }; + format!("{mantissa}e{sign}{digits:0>2}") +} + +fn write_python_json_string(value: &str, output: &mut String) { + output.push('"'); + for character in value.chars() { + match character { + '"' => output.push_str("\\\""), + '\\' => output.push_str("\\\\"), + '\u{0008}' => output.push_str("\\b"), + '\u{000c}' => output.push_str("\\f"), + '\n' => output.push_str("\\n"), + '\r' => output.push_str("\\r"), + '\t' => output.push_str("\\t"), + '\u{0020}'..='\u{007e}' => output.push(character), + character if u32::from(character) <= 0xffff => { + let _ = write!(output, "\\u{:04x}", u32::from(character)); + } + character => { + let scalar = u32::from(character) - 0x1_0000; + let high = 0xd800 + (scalar >> 10); + let low = 0xdc00 + (scalar & 0x3ff); + let _ = write!(output, "\\u{high:04x}\\u{low:04x}"); + } + } + } + output.push('"'); +} + +fn reject_forbidden_nulls(manifest: &Value) -> RusticolResult<()> { + if manifest + .get("runtime") + .and_then(Value::as_object) + .is_some_and(|runtime| !runtime.contains_key("api_bundle_path")) + { + return Err(RusticolError::artifact( + "runtime.api_bundle_path is required and must be a relative path or null", + )); + } + for (collection, keys) in [ + ("payloads", &["target", "process_id"] as &[&str]), + ( + "dependencies", + &["content_sha256", "revision", "patch_sha256"] as &[&str], + ), + ] { + let Some(items) = manifest.get(collection).and_then(Value::as_array) else { + continue; + }; + for (index, item) in items.iter().enumerate() { + for key in keys { + if item.get(*key).is_some_and(Value::is_null) { + return Err(RusticolError::artifact(format!( + "{collection}[{index}].{key} may be omitted but may not be null" + ))); + } + } + } + } + Ok(()) +} + +fn locate_manifest(requested: &Path) -> RusticolResult<(PathBuf, PathBuf)> { + let metadata = fs::metadata(requested).map_err(|error| { + RusticolError::artifact(format!( + "could not inspect artifact path {}: {error}", + requested.display() + )) + })?; + if metadata.is_file() { + if requested.file_name().and_then(|name| name.to_str()) != Some(ARTIFACT_MANIFEST_FILE) { + return Err(RusticolError::artifact(format!( + "artifact manifest must be named {ARTIFACT_MANIFEST_FILE}" + ))); + } + let manifest = requested.canonicalize().map_err(|error| { + RusticolError::artifact(format!( + "could not resolve artifact manifest {}: {error}", + requested.display() + )) + })?; + let root = manifest.parent().ok_or_else(|| { + RusticolError::artifact("artifact manifest has no containing directory") + })?; + return Ok((root.to_path_buf(), manifest)); + } + if !metadata.is_dir() { + return Err(RusticolError::security(format!( + "artifact path {} is neither a regular file nor a directory", + requested.display() + ))); + } + let root = requested.canonicalize().map_err(|error| { + RusticolError::artifact(format!( + "could not resolve artifact directory {}: {error}", + requested.display() + )) + })?; + let manifest = root.join(ARTIFACT_MANIFEST_FILE); + if manifest.exists() { + Ok((root, manifest)) + } else { + Err(RusticolError::artifact(format!( + "artifact directory does not contain {ARTIFACT_MANIFEST_FILE}" + ))) + } +} + +fn validate_manifest(manifest: &ArtifactManifest) -> RusticolResult<()> { + validate_sha256(&manifest.artifact_id, "artifact_id")?; + validate_datetime(&manifest.created_utc)?; + validate_artifact_identity_extension(&manifest.extensions)?; + if manifest.producer.distribution != "pyamplicol" { + return Err(RusticolError::compatibility(format!( + "unsupported artifact producer {:?}; expected pyamplicol", + manifest.producer.distribution + ))); + } + if !compatible_distribution_version(&manifest.producer.version) + || !compatible_distribution_version(&manifest.runtime.engine_version) + { + return Err(RusticolError::compatibility(format!( + "artifact producer/runtime version {}/{} is incompatible with Rusticol {}", + manifest.producer.version, + manifest.runtime.engine_version, + rusticol_package_version() + ))); + } + let versions = &manifest.producer.versions; + let expected = [ + ("python API", versions.python_api, PYTHON_API_VERSION), + ("TOML", versions.toml, TOML_SCHEMA_VERSION), + ( + "compiled model", + versions.compiled_model, + COMPILED_MODEL_SCHEMA_VERSION, + ), + ( + "process artifact", + versions.process_artifact, + PROCESS_ARTIFACT_SCHEMA_VERSION, + ), + ( + "runtime physics", + versions.runtime_physics, + RUNTIME_PHYSICS_SCHEMA_VERSION, + ), + ("C ABI", versions.c_abi, C_ABI_VERSION), + ]; + for (name, found, required) in expected { + if found != required { + return Err(RusticolError::compatibility(format!( + "artifact {name} version {found} is incompatible with required version {required}; regenerate the artifact" + ))); + } + } + if manifest.runtime.engine != "rusticol" { + return Err(RusticolError::compatibility(format!( + "unsupported runtime engine {:?}", + manifest.runtime.engine + ))); + } + validate_target(&manifest.producer.target, "producer")?; + for payload in &manifest.payloads { + if let Some(target) = &payload.target { + validate_payload_target(&manifest.producer.target, target, &payload.path)?; + validate_target(target, &format!("payload {}", payload.path))?; + } + } + if let Some(revision) = &manifest.producer.git_revision + && (revision.len() != 40 + || !revision + .bytes() + .all(|byte| byte.is_ascii_hexdigit() && !byte.is_ascii_uppercase())) + { + return Err(RusticolError::artifact( + "producer.git_revision must be 40 lowercase hexadecimal characters", + )); + } + if let Some(native_inputs) = &manifest.producer.native_build_inputs_sha256 { + validate_sha256(native_inputs, "producer.native_build_inputs_sha256")?; + } + validate_sha256(&manifest.model.content_sha256, "model content_sha256")?; + if manifest.model.name.is_empty() + || manifest + .model + .restriction + .as_ref() + .is_some_and(String::is_empty) + { + return Err(RusticolError::artifact( + "model name and any model restriction must be non-empty", + )); + } + if manifest.model.compiled_schema_version != COMPILED_MODEL_SCHEMA_VERSION { + return Err(RusticolError::compatibility(format!( + "compiled model schema {} is incompatible with required schema {}", + manifest.model.compiled_schema_version, COMPILED_MODEL_SCHEMA_VERSION + ))); + } + if manifest.configuration.toml_schema_version != TOML_SCHEMA_VERSION { + return Err(RusticolError::compatibility(format!( + "configuration TOML schema {} is incompatible with required schema {}", + manifest.configuration.toml_schema_version, TOML_SCHEMA_VERSION + ))); + } + validate_relative_path( + &manifest.configuration.requested_path, + "requested configuration path", + )?; + validate_relative_path( + &manifest.configuration.effective_path, + "effective configuration path", + )?; + for (index, adjustment) in manifest.configuration.adjustments.iter().enumerate() { + if adjustment.path.is_empty() || adjustment.reason.is_empty() { + return Err(RusticolError::artifact(format!( + "configuration adjustment {index} requires non-empty path and reason" + ))); + } + } + validate_relative_path( + &manifest.runtime.evaluator_manifest_path, + "evaluator manifest path", + )?; + let runtime_capabilities = validate_runtime_capabilities( + &manifest.runtime.required_runtime_capabilities, + "runtime.required_runtime_capabilities", + )?; + if let Some(path) = &manifest.runtime.api_bundle_path { + validate_relative_path(path, "API bundle path")?; + } + if manifest.processes.is_empty() { + return Err(RusticolError::artifact( + "artifact must contain at least one process", + )); + } + match manifest.kind { + ArtifactKind::PyamplicolProcess if manifest.processes.len() != 1 => { + return Err(RusticolError::artifact( + "pyamplicol-process artifacts must contain exactly one process", + )); + } + _ => {} + } + let mut public_ids = BTreeSet::new(); + let mut process_capabilities = BTreeSet::new(); + for process in &manifest.processes { + validate_public_id(&process.id, "process id")?; + validate_relative_path(&process.physics_path, "runtime physics path")?; + let process_expression = parse_process_expression(&process.expression); + if process.expression.is_empty() || process.external_pdgs.len() < 3 { + return Err(RusticolError::artifact(format!( + "process {} has an empty expression or fewer than three external particles", + process.id + ))); + } + let process_expression = process_expression.ok_or_else(|| { + RusticolError::artifact(format!( + "process {:?} expression must contain one '>' with particles on both sides", + process.id + )) + })?; + if process_expression.initial.len() + process_expression.final_state.len() + != process.external_pdgs.len() + { + return Err(RusticolError::artifact(format!( + "process {:?} expression particle count does not match external_pdgs", + process.id + ))); + } + if !matches!(process.color_accuracy.as_str(), "lc" | "nlc" | "full") { + return Err(RusticolError::artifact(format!( + "process {} has unsupported color accuracy {:?}", + process.id, process.color_accuracy + ))); + } + process_capabilities.extend(validate_runtime_capabilities( + &process.required_runtime_capabilities, + &format!("process {:?}.required_runtime_capabilities", process.id), + )?); + if !public_ids.insert(&process.id) { + return Err(RusticolError::artifact(format!( + "duplicate public process id {:?}", + process.id + ))); + } + for alias in &process.aliases { + validate_public_id(&alias.id, "process alias id")?; + if alias.expression.is_empty() || !public_ids.insert(&alias.id) { + return Err(RusticolError::artifact(format!( + "duplicate or invalid process alias id {:?}", + alias.id + ))); + } + validate_permutation( + &alias.external_permutation, + process.external_pdgs.len(), + &format!("alias {:?}", alias.id), + )?; + let alias_expression = + parse_process_expression(&alias.expression).ok_or_else(|| { + RusticolError::artifact(format!( + "alias {:?} expression must contain one '>' with particles on both sides", + alias.id + )) + })?; + if alias_expression.initial.len() != process_expression.initial.len() + || alias_expression.final_state.len() != process_expression.final_state.len() + || !permutation_preserves_process_sides( + &alias.external_permutation, + process_expression.initial.len(), + ) + { + return Err(RusticolError::artifact(format!( + "alias {:?} may only permute particles within the incoming and outgoing sides", + alias.id + ))); + } + let mut expected_external_pdgs = vec![0; process.external_pdgs.len()]; + for (representative_index, alias_index) in + alias.external_permutation.iter().copied().enumerate() + { + expected_external_pdgs[alias_index] = process.external_pdgs[representative_index]; + } + if alias.external_pdgs != expected_external_pdgs { + return Err(RusticolError::artifact(format!( + "alias {:?} external_pdgs {:?} does not match external_permutation {:?}; expected {:?}", + alias.id, + alias.external_pdgs, + alias.external_permutation, + expected_external_pdgs, + ))); + } + } + } + if runtime_capabilities != process_capabilities { + return Err(RusticolError::artifact( + "runtime.required_runtime_capabilities must equal the union of process capability declarations", + )); + } + validate_portable_runtime_capabilities(&manifest.producer.target, &runtime_capabilities)?; + if let Some(default) = &manifest.default_process_id + && !public_ids.contains(default) + { + return Err(RusticolError::artifact(format!( + "default process id {default:?} does not identify a process or alias" + ))); + } + if manifest.payloads.is_empty() { + return Err(RusticolError::artifact( + "artifact must declare at least one payload", + )); + } + let mut dependencies = BTreeSet::new(); + for dependency in &manifest.dependencies { + if dependency.name.is_empty() + || dependency.version.is_empty() + || dependency.source.is_empty() + || dependency.license.is_empty() + || !dependencies.insert(dependency.name.to_ascii_lowercase()) + { + return Err(RusticolError::artifact(format!( + "invalid or duplicate dependency {:?}", + dependency.name + ))); + } + for (name, value) in [ + ("content_sha256", dependency.content_sha256.as_deref()), + ("patch_sha256", dependency.patch_sha256.as_deref()), + ] { + if let Some(value) = value { + validate_sha256(value, &format!("dependency {} {name}", dependency.name))?; + } + } + if dependency.revision.as_ref().is_some_and(String::is_empty) { + return Err(RusticolError::artifact(format!( + "dependency {} revision must be non-empty when present", + dependency.name + ))); + } + } + Ok(()) +} + +fn validate_artifact_identity_extension( + extensions: &BTreeMap, +) -> RusticolResult<()> { + let Some(policy) = extensions + .get(ARTIFACT_IDENTITY_EXTENSION) + .and_then(Value::as_object) + else { + return Err(RusticolError::compatibility( + "artifact predates the required runtime-payload identity contract; \ + regenerate it with the current pyAmpliCol", + )); + }; + let valid = policy.len() == 2 + && policy.get("kind").and_then(Value::as_str) == Some(ARTIFACT_IDENTITY_KIND) + && policy.get("schema_version").and_then(Value::as_u64) + == Some(ARTIFACT_IDENTITY_SCHEMA_VERSION); + if !valid { + return Err(RusticolError::compatibility( + "artifact uses an unsupported artifact identity contract; \ + regenerate it with the current pyAmpliCol", + )); + } + Ok(()) +} + +fn validate_artifact_identity_header(manifest: &Value) -> RusticolResult<()> { + let Some(extensions) = manifest.get("extensions").and_then(Value::as_object) else { + return Err(RusticolError::compatibility( + "artifact predates the required runtime-payload identity contract; \ + regenerate it with the current pyAmpliCol", + )); + }; + let ordered = extensions + .iter() + .map(|(key, value)| (key.clone(), value.clone())) + .collect(); + validate_artifact_identity_extension(&ordered) +} + +fn compatible_distribution_version(version: &str) -> bool { + #[cfg(test)] + if std::env::var_os("RUSTICOL_TEST_ALLOW_VERSION_MISMATCH").is_some() { + return true; + } + canonical_distribution_version(version) + == canonical_distribution_version(rusticol_package_version()) +} + +fn canonical_distribution_version(version: &str) -> String { + let canonical = version.replace("-dev.", ".dev"); + // Candidate build hashes identify native builds, not process formats. + // Explicit schema, ABI, and evaluator-storage versions are checked when + // loading the output; a runtime fix must not require process regeneration. + if let Some((base, fingerprint)) = canonical.split_once("+candidate.") + && base.contains(".dev") + && fingerprint.len() == 12 + && fingerprint + .bytes() + .all(|byte| byte.is_ascii_hexdigit() && !byte.is_ascii_uppercase()) + { + return base.to_owned(); + } + canonical +} + +fn validate_references( + manifest: &ArtifactManifest, + payloads: &BTreeMap, +) -> RusticolResult<()> { + let process_ids = manifest + .processes + .iter() + .map(|process| process.id.as_str()) + .collect::>(); + require_payload_role( + payloads, + &manifest.configuration.requested_path, + PayloadRole::ConfigurationRequested, + None, + )?; + require_payload_role( + payloads, + &manifest.configuration.effective_path, + PayloadRole::ConfigurationEffective, + None, + )?; + require_payload_role( + payloads, + &manifest.runtime.evaluator_manifest_path, + PayloadRole::EvaluatorManifest, + None, + )?; + for process in &manifest.processes { + require_payload_role( + payloads, + &process.physics_path, + PayloadRole::RuntimePhysics, + Some(&process.id), + )?; + } + for payload in payloads + .values() + .filter(|payload| payload.role == PayloadRole::StructuralSourceProof) + { + if let Some(process_id) = payload.process_id.as_deref() + && !process_ids.contains(process_id) + { + return Err(RusticolError::artifact(format!( + "structural-source-proof payload {:?} belongs to unknown process {process_id:?}", + payload.path + ))); + } + } + if let Some(api_path) = &manifest.runtime.api_bundle_path { + let prefix = format!("{}/", api_path.trim_end_matches('/')); + if !payloads.keys().any(|path| path.starts_with(&prefix)) { + return Err(RusticolError::artifact(format!( + "API bundle path {api_path:?} contains no declared payload" + ))); + } + } + Ok(()) +} + +fn require_payload_role( + payloads: &BTreeMap, + path: &str, + role: PayloadRole, + process_id: Option<&str>, +) -> RusticolResult<()> { + let payload = payloads.get(path).ok_or_else(|| { + RusticolError::security(format!( + "referenced artifact path {path:?} is not a declared payload" + )) + })?; + if payload.role != role { + return Err(RusticolError::security(format!( + "referenced payload {path:?} has role {:?}, expected {:?}", + payload.role, role + ))); + } + if let (Some(expected), Some(found)) = (process_id, payload.process_id.as_deref()) + && expected != found + { + return Err(RusticolError::artifact(format!( + "payload {path:?} belongs to process {found:?}, expected {expected:?}" + ))); + } + Ok(()) +} + +fn metadata_is_executable(metadata: &fs::Metadata) -> bool { + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + metadata.permissions().mode() & 0o111 != 0 + } + #[cfg(not(unix))] + { + let _ = metadata; + false + } +} + +#[cfg_attr(target_vendor = "apple", unsafe(link_section = "__TEXT,__rcl_load"))] +#[cfg_attr(target_vendor = "apple", inline(never))] +fn read_declared_payload(root: &Path, payload: &Payload) -> RusticolResult> { + validate_payload_declaration(payload)?; + let mut file = open_checked_payload(root, payload)?; + let expected_size = usize::try_from(payload.size_bytes).map_err(|_| { + RusticolError::artifact(format!( + "payload {:?} is too large for this platform", + payload.path + )) + })?; + let mut bytes = Vec::new(); + bytes.try_reserve_exact(expected_size).map_err(|error| { + RusticolError::artifact(format!( + "could not allocate {} bytes for payload {:?}: {error}", + payload.size_bytes, payload.path + )) + })?; + file.read_to_end(&mut bytes).map_err(|error| { + RusticolError::artifact(format!( + "could not read payload {:?}: {error}", + payload.path + )) + })?; + if bytes.len() != expected_size { + return Err(RusticolError::integrity(format!( + "payload {:?} changed while being read: expected {} bytes, read {}", + payload.path, + payload.size_bytes, + bytes.len() + ))); + } + Ok(bytes) +} + +fn validate_payload_declaration(payload: &Payload) -> RusticolResult<()> { + validate_sha256(&payload.sha256, &format!("payload {} sha256", payload.path))?; + if payload.media_type.is_empty() { + return Err(RusticolError::artifact(format!( + "payload {:?} has an empty media type", + payload.path + ))); + } + if let Some(process_id) = &payload.process_id { + validate_public_id(process_id, "payload process id")?; + } + if payload.role == PayloadRole::EvaluatorState && payload.target.is_none() { + return Err(RusticolError::artifact(format!( + "evaluator-state payload {:?} is missing required target metadata", + payload.path + ))); + } + if payload.role == PayloadRole::StructuralSourceProof { + let process_id = payload.process_id.as_deref().ok_or_else(|| { + RusticolError::artifact(format!( + "structural-source-proof payload {:?} is missing required process id", + payload.path + )) + })?; + let expected_path = format!("processes/{process_id}/structural-source-proof.json"); + if payload.path != expected_path { + return Err(RusticolError::artifact(format!( + "structural-source-proof payload {:?} must use exact path {expected_path:?}", + payload.path + ))); + } + if payload.media_type != "application/json" { + return Err(RusticolError::artifact(format!( + "structural-source-proof payload {:?} must use media type application/json", + payload.path + ))); + } + if payload.target.is_some() { + return Err(RusticolError::artifact(format!( + "structural-source-proof payload {:?} may not declare target metadata", + payload.path + ))); + } + } + if payload.executable + && !matches!( + payload.role, + PayloadRole::EvaluatorState | PayloadRole::ApiSource | PayloadRole::ApiBuildFile + ) + { + return Err(RusticolError::security(format!( + "payload {:?} has role {:?}, which may not be executable", + payload.path, payload.role + ))); + } + Ok(()) +} + +#[cfg_attr(target_vendor = "apple", unsafe(link_section = "__TEXT,__rcl_load"))] +#[cfg_attr(target_vendor = "apple", inline(never))] +fn open_checked_payload(root: &Path, payload: &Payload) -> RusticolResult { + let path = root.join(&payload.path); + reject_symlink_chain(&path)?; + let canonical = path.canonicalize().map_err(|error| { + RusticolError::security(format!( + "could not resolve payload {:?}: {error}", + payload.path + )) + })?; + if !canonical.starts_with(root) { + return Err(RusticolError::security(format!( + "payload {:?} escapes the artifact root", + payload.path + ))); + } + #[cfg(unix)] + let file = { + use std::os::unix::fs::OpenOptionsExt as _; + OpenOptions::new() + .read(true) + .custom_flags(libc::O_CLOEXEC | libc::O_NOFOLLOW) + .open(&path) + }; + #[cfg(not(unix))] + let file = OpenOptions::new().read(true).open(&path); + let file = file.map_err(|error| { + RusticolError::artifact(format!( + "could not open payload {:?}: {error}", + payload.path + )) + })?; + let metadata = file.metadata().map_err(|error| { + RusticolError::security(format!( + "could not inspect payload {:?}: {error}", + payload.path + )) + })?; + if !metadata.is_file() { + return Err(RusticolError::security(format!( + "payload {:?} is not a regular file", + payload.path + ))); + } + if metadata.len() != payload.size_bytes { + return Err(RusticolError::integrity(format!( + "payload {:?} has size {}, expected {}", + payload.path, + metadata.len(), + payload.size_bytes + ))); + } + #[cfg(unix)] + { + let executable = metadata_is_executable(&metadata); + if executable != payload.executable { + return Err(RusticolError::security(format!( + "payload {:?} executable mode is {}, but the manifest declares {}", + payload.path, executable, payload.executable + ))); + } + } + reject_symlink_chain(&path)?; + let path_metadata = fs::symlink_metadata(&path).map_err(|error| { + RusticolError::security(format!( + "could not re-inspect payload {:?}: {error}", + payload.path + )) + })?; + if path_metadata.file_type().is_symlink() || !path_metadata.file_type().is_file() { + return Err(RusticolError::security(format!( + "payload {:?} is not a regular non-symlink file", + payload.path + ))); + } + #[cfg(unix)] + { + use std::os::unix::fs::MetadataExt as _; + if metadata.dev() != path_metadata.dev() || metadata.ino() != path_metadata.ino() { + return Err(RusticolError::security(format!( + "payload {:?} was replaced while being opened", + payload.path + ))); + } + } + Ok(file) +} + +fn validate_relative_path(value: &str, description: &str) -> RusticolResult<()> { + if value.is_empty() + || value.starts_with('/') + || value.ends_with('/') + || value.contains('\\') + || value.contains('\0') + || value + .split('/') + .any(|part| part.is_empty() || part == "." || part == "..") + || Path::new(value).is_absolute() + { + return Err(RusticolError::security(format!( + "{description} {value:?} is not a normalized confined relative path" + ))); + } + Ok(()) +} + +#[cfg_attr(target_vendor = "apple", unsafe(link_section = "__TEXT,__rcl_load"))] +#[cfg_attr(target_vendor = "apple", inline(never))] +fn reject_symlink_chain(path: &Path) -> RusticolResult<()> { + let mut ancestors = path.ancestors().collect::>(); + ancestors.reverse(); + for ancestor in ancestors { + let Ok(metadata) = fs::symlink_metadata(ancestor) else { + continue; + }; + if metadata.file_type().is_symlink() { + return Err(RusticolError::security(format!( + "artifact path {} contains a symlink at {}", + path.display(), + ancestor.display() + ))); + } + } + Ok(()) +} + +fn validate_sha256(value: &str, description: &str) -> RusticolResult<()> { + if value.len() != 64 + || !value + .bytes() + .all(|byte| byte.is_ascii_hexdigit() && !byte.is_ascii_uppercase()) + { + return Err(RusticolError::artifact(format!( + "{description} must be 64 lowercase hexadecimal characters" + ))); + } + Ok(()) +} + +fn parse_sha256(value: &str, description: &str) -> RusticolResult<[u8; 32]> { + validate_sha256(value, description)?; + let mut digest = [0_u8; 32]; + for (index, pair) in value.as_bytes().chunks_exact(2).enumerate() { + digest[index] = (hex_nibble(pair[0]) << 4) | hex_nibble(pair[1]); + } + Ok(digest) +} + +fn hex_nibble(value: u8) -> u8 { + match value { + b'0'..=b'9' => value - b'0', + b'a'..=b'f' => value - b'a' + 10, + _ => unreachable!("SHA-256 validation precedes hexadecimal decoding"), + } +} + +fn validate_public_id(value: &str, description: &str) -> RusticolResult<()> { + let valid = !value.is_empty() + && value.len() <= 255 + && value.bytes().enumerate().all(|(index, byte)| { + byte.is_ascii_alphanumeric() + || (index > 0 && matches!(byte, b'.' | b'_' | b':' | b'+' | b',' | b'~' | b'-')) + }); + if !valid { + return Err(RusticolError::artifact(format!( + "invalid {description} {value:?}" + ))); + } + Ok(()) +} + +fn validate_datetime(value: &str) -> RusticolResult<()> { + let bytes = value.as_bytes(); + let digits = |start: usize, length: usize| -> Option { + let slice = bytes.get(start..start + length)?; + if !slice.iter().all(u8::is_ascii_digit) { + return None; + } + slice.iter().try_fold(0_u32, |value, digit| { + value.checked_mul(10)?.checked_add(u32::from(*digit - b'0')) + }) + }; + let year = digits(0, 4); + let month = digits(5, 2); + let day = digits(8, 2); + let hour = digits(11, 2); + let minute = digits(14, 2); + let second = digits(17, 2); + let date_valid = match (year, month, day) { + (Some(year), Some(month @ 1..=12), Some(day)) => { + let leap = year % 4 == 0 && (year % 100 != 0 || year % 400 == 0); + let maximum = match month { + 2 if leap => 29, + 2 => 28, + 4 | 6 | 9 | 11 => 30, + _ => 31, + }; + (1..=maximum).contains(&day) + } + _ => false, + }; + let time_valid = matches!(hour, Some(0..=23)) + && matches!(minute, Some(0..=59)) + && matches!(second, Some(0..=60)); + let separators_valid = bytes.get(4) == Some(&b'-') + && bytes.get(7) == Some(&b'-') + && matches!(bytes.get(10), Some(b'T' | b't')) + && bytes.get(13) == Some(&b':') + && bytes.get(16) == Some(&b':'); + let mut offset = 19; + if bytes.get(offset) == Some(&b'.') { + offset += 1; + let fractional_start = offset; + while bytes.get(offset).is_some_and(u8::is_ascii_digit) { + offset += 1; + } + if offset == fractional_start { + offset = bytes.len() + 1; + } + } + let zone_valid = match bytes.get(offset) { + Some(b'Z' | b'z') => offset + 1 == bytes.len(), + Some(b'+' | b'-') => { + offset + 6 == bytes.len() + && bytes.get(offset + 3) == Some(&b':') + && matches!(digits(offset + 1, 2), Some(0..=23)) + && matches!(digits(offset + 4, 2), Some(0..=59)) + } + _ => false, + }; + let valid = bytes.len() >= 20 && date_valid && time_valid && separators_valid && zone_valid; + if !valid { + return Err(RusticolError::artifact(format!( + "created_utc {value:?} is not an RFC 3339 date-time" + ))); + } + Ok(()) +} + +fn validate_permutation(values: &[usize], size: usize, owner: &str) -> RusticolResult<()> { + if values.len() != size { + return Err(RusticolError::artifact(format!( + "{owner} permutation has length {}, expected {size}", + values.len() + ))); + } + let found = values.iter().copied().collect::>(); + let expected = (0..size).collect::>(); + if found != expected { + return Err(RusticolError::artifact(format!( + "{owner} permutation is not a complete zero-based permutation" + ))); + } + Ok(()) +} + +fn permutation_preserves_process_sides(values: &[usize], initial_count: usize) -> bool { + values + .iter() + .copied() + .enumerate() + .all(|(representative_index, public_index)| { + (representative_index < initial_count) == (public_index < initial_count) + }) +} + +fn validate_runtime_capabilities( + values: &[String], + description: &str, +) -> RusticolResult> { + if values.is_empty() { + return Err(RusticolError::artifact(format!( + "{description} must contain at least one capability" + ))); + } + let capabilities = values.iter().cloned().collect::>(); + if capabilities.len() != values.len() { + return Err(RusticolError::artifact(format!( + "{description} must not contain duplicates" + ))); + } + if values.windows(2).any(|pair| pair[0] >= pair[1]) { + return Err(RusticolError::artifact(format!( + "{description} must be sorted" + ))); + } + let known = [ + RuntimeCapability::CompiledColorContractionWalshC2kV1, + RuntimeCapability::CompiledColorContractionWalshV1, + RuntimeCapability::CompiledColorTopologyLanesV1, + RuntimeCapability::CompiledHelicityDualLaneV1, + RuntimeCapability::CompiledHelicityPrimaryRecurrenceV1, + RuntimeCapability::CompiledHelicitySelectorUnionV1, + RuntimeCapability::CompiledPlaneArenaV1, + RuntimeCapability::CompiledRuntimeSelectorsV1, + RuntimeCapability::EagerDagComplexF64V1, + RuntimeCapability::EagerDirectArenaV1, + RuntimeCapability::EagerRuntimeLayoutComplexF64V1, + RuntimeCapability::EagerLcTopologyReplayComplexF64V1, + RuntimeCapability::RecurrenceRuntimeComplexF64V1, + RuntimeCapability::RecurrenceLcColorV1, + RuntimeCapability::RecurrenceContractedColorV1, + RuntimeCapability::RecurrenceHelicitySelectorCompanionV2, + RuntimeCapability::SymmetricGroupFftColorContractionV1, + RuntimeCapability::OnTheFlyRuntimeComplexF64V1, + RuntimeCapability::OnTheFlyContractedColorV1, + RuntimeCapability::OnTheFlyLcColorV1, + RuntimeCapability::SymjitApplicationComplexF64V1, + RuntimeCapability::SymbolicaLegacyJitContainerComplexF64V1, + RuntimeCapability::SymbolicaCompiledCppComplexF64V1, + RuntimeCapability::SymbolicaCompiledAsmComplexF64V1, + ] + .map(RuntimeCapability::as_str) + .into_iter() + .collect::>(); + let unknown = capabilities + .iter() + .filter(|capability| !known.contains(capability.as_str())) + .cloned() + .collect::>(); + if !unknown.is_empty() { + return Err(RusticolError::artifact(format!( + "{description} contains unsupported capabilities: {}", + unknown.join(", ") + ))); + } + Ok(capabilities) +} + +fn validate_target(target: &Target, description: &str) -> RusticolResult<()> { + let current = current_target_triple(); + if !SUPPORTED_ARTIFACT_TARGETS.contains(¤t) { + return Err(RusticolError::compatibility(format!( + "Rusticol process artifacts are not supported on runtime target {current:?}" + ))); + } + let required = normalized_cpu_features(target, description)?; + if target.triple == PORTABLE_64LE_ARTIFACT_TARGET { + if usize::BITS != 64 || !cfg!(target_endian = "little") { + return Err(RusticolError::compatibility(format!( + "{description} target {PORTABLE_64LE_ARTIFACT_TARGET:?} requires a 64-bit little-endian runtime" + ))); + } + if !required.is_empty() { + return Err(RusticolError::compatibility(format!( + "{description} target {PORTABLE_64LE_ARTIFACT_TARGET:?} must not require CPU features" + ))); + } + return Ok(()); + } + if target.triple != current { + return Err(RusticolError::compatibility(format!( + "{description} target {:?} is incompatible with runtime target {current:?}", + target.triple + ))); + } + let available = detected_cpu_features().into_iter().collect::>(); + let unavailable = required.difference(&available).cloned().collect::>(); + if !unavailable.is_empty() { + return Err(RusticolError::compatibility(format!( + "{description} requires unavailable CPU features {unavailable:?}" + ))); + } + Ok(()) +} + +fn validate_portable_runtime_capabilities( + target: &Target, + capabilities: &BTreeSet, +) -> RusticolResult<()> { + if target.triple != PORTABLE_64LE_ARTIFACT_TARGET { + return Ok(()); + } + let forbidden = [ + RuntimeCapability::SymbolicaLegacyJitContainerComplexF64V1.as_str(), + RuntimeCapability::SymbolicaCompiledCppComplexF64V1.as_str(), + RuntimeCapability::SymbolicaCompiledAsmComplexF64V1.as_str(), + ]; + if forbidden + .iter() + .any(|capability| capabilities.contains(*capability)) + { + return Err(RusticolError::compatibility( + "portable-64le process artifacts cannot contain C++, ASM, or legacy JIT evaluators; those evaluator families remain target-specific", + )); + } + Ok(()) +} + +fn validate_payload_target( + producer: &Target, + payload: &Target, + payload_path: &str, +) -> RusticolResult<()> { + if payload.triple != producer.triple { + return Err(RusticolError::compatibility(format!( + "payload {payload_path:?} target {:?} does not match producer target {:?}", + payload.triple, producer.triple + ))); + } + let producer_features = normalized_cpu_features(producer, "producer")?; + let payload_features = normalized_cpu_features(payload, &format!("payload {payload_path}"))?; + if payload_features != producer_features { + return Err(RusticolError::compatibility(format!( + "payload {payload_path:?} CPU features {payload_features:?} do not match producer CPU features {producer_features:?}" + ))); + } + Ok(()) +} + +fn normalized_cpu_features(target: &Target, description: &str) -> RusticolResult> { + let mut features = BTreeSet::new(); + let mut previous: Option<&str> = None; + for feature in &target.cpu_features { + let canonical = !feature.is_empty() + && feature.bytes().enumerate().all(|(index, byte)| { + byte.is_ascii_lowercase() + || byte.is_ascii_digit() + || (index > 0 && matches!(byte, b'.' | b'-')) + }); + if !canonical { + return Err(RusticolError::artifact(format!( + "{description} target CPU feature {feature:?} is not a canonical feature ID" + ))); + } + if previous.is_some_and(|value| value >= feature.as_str()) { + return Err(RusticolError::artifact(format!( + "{description} target CPU features must be sorted and unique" + ))); + } + previous = Some(feature); + features.insert(feature.clone()); + } + Ok(features) +} + +/// Return the current Rusticol target and every CPU feature it can verify at runtime. +/// +/// An empty feature list on an artifact means the architecture's baseline ISA. Native +/// evaluator producers use this detected, canonical list as a conservative requirement. +pub fn runtime_target_info() -> Target { + Target { + triple: current_target_triple().to_string(), + cpu_features: detected_cpu_features(), + } +} + +fn current_target_triple() -> &'static str { + #[cfg(all(target_arch = "aarch64", target_os = "macos"))] + { + "aarch64-apple-darwin" + } + #[cfg(all(target_arch = "x86_64", target_os = "macos"))] + { + "x86_64-apple-darwin" + } + #[cfg(all(target_arch = "x86_64", target_os = "linux", target_env = "gnu"))] + { + "x86_64-unknown-linux-gnu" + } + #[cfg(not(any( + all(target_arch = "aarch64", target_os = "macos"), + all(target_arch = "x86_64", target_os = "macos"), + all(target_arch = "x86_64", target_os = "linux", target_env = "gnu") + )))] + { + "unsupported-target" + } +} + +fn detected_cpu_features() -> Vec { + let mut features = Vec::new(); + #[cfg(target_arch = "x86_64")] + { + let detected = [ + ("adx", std::is_x86_feature_detected!("adx")), + ("aes", std::is_x86_feature_detected!("aes")), + ("avx", std::is_x86_feature_detected!("avx")), + ("avx2", std::is_x86_feature_detected!("avx2")), + ("avx512bf16", std::is_x86_feature_detected!("avx512bf16")), + ( + "avx512bitalg", + std::is_x86_feature_detected!("avx512bitalg"), + ), + ("avx512bw", std::is_x86_feature_detected!("avx512bw")), + ("avx512cd", std::is_x86_feature_detected!("avx512cd")), + ("avx512dq", std::is_x86_feature_detected!("avx512dq")), + ("avx512f", std::is_x86_feature_detected!("avx512f")), + ("avx512ifma", std::is_x86_feature_detected!("avx512ifma")), + ("avx512vbmi", std::is_x86_feature_detected!("avx512vbmi")), + ("avx512vbmi2", std::is_x86_feature_detected!("avx512vbmi2")), + ("avx512vl", std::is_x86_feature_detected!("avx512vl")), + ("avx512vnni", std::is_x86_feature_detected!("avx512vnni")), + ( + "avx512vpopcntdq", + std::is_x86_feature_detected!("avx512vpopcntdq"), + ), + ("bmi1", std::is_x86_feature_detected!("bmi1")), + ("bmi2", std::is_x86_feature_detected!("bmi2")), + ("cmpxchg16b", std::is_x86_feature_detected!("cmpxchg16b")), + ("f16c", std::is_x86_feature_detected!("f16c")), + ("fma", std::is_x86_feature_detected!("fma")), + ("fxsr", std::is_x86_feature_detected!("fxsr")), + ("gfni", std::is_x86_feature_detected!("gfni")), + ("lzcnt", std::is_x86_feature_detected!("lzcnt")), + ("movbe", std::is_x86_feature_detected!("movbe")), + ("pclmulqdq", std::is_x86_feature_detected!("pclmulqdq")), + ("popcnt", std::is_x86_feature_detected!("popcnt")), + ("rdrand", std::is_x86_feature_detected!("rdrand")), + ("rdseed", std::is_x86_feature_detected!("rdseed")), + ("rtm", std::is_x86_feature_detected!("rtm")), + ("sha", std::is_x86_feature_detected!("sha")), + ("sse", std::is_x86_feature_detected!("sse")), + ("sse2", std::is_x86_feature_detected!("sse2")), + ("sse3", std::is_x86_feature_detected!("sse3")), + ("sse4.1", std::is_x86_feature_detected!("sse4.1")), + ("sse4.2", std::is_x86_feature_detected!("sse4.2")), + ("ssse3", std::is_x86_feature_detected!("ssse3")), + ("vaes", std::is_x86_feature_detected!("vaes")), + ("vpclmulqdq", std::is_x86_feature_detected!("vpclmulqdq")), + ("xsave", std::is_x86_feature_detected!("xsave")), + ("xsavec", std::is_x86_feature_detected!("xsavec")), + ("xsaveopt", std::is_x86_feature_detected!("xsaveopt")), + ("xsaves", std::is_x86_feature_detected!("xsaves")), + ]; + features.extend( + detected + .into_iter() + .filter(|(_, available)| *available) + .map(|(name, _)| name.to_string()), + ); + } + #[cfg(target_arch = "aarch64")] + { + let detected = [ + ("aes", std::arch::is_aarch64_feature_detected!("aes")), + ("bf16", std::arch::is_aarch64_feature_detected!("bf16")), + ("bti", std::arch::is_aarch64_feature_detected!("bti")), + ("crc", std::arch::is_aarch64_feature_detected!("crc")), + ("dit", std::arch::is_aarch64_feature_detected!("dit")), + ( + "dotprod", + std::arch::is_aarch64_feature_detected!("dotprod"), + ), + ("dpb", std::arch::is_aarch64_feature_detected!("dpb")), + ("dpb2", std::arch::is_aarch64_feature_detected!("dpb2")), + ("f32mm", std::arch::is_aarch64_feature_detected!("f32mm")), + ("f64mm", std::arch::is_aarch64_feature_detected!("f64mm")), + ("fcma", std::arch::is_aarch64_feature_detected!("fcma")), + ("fhm", std::arch::is_aarch64_feature_detected!("fhm")), + ("flagm", std::arch::is_aarch64_feature_detected!("flagm")), + ("fp", std::arch::is_aarch64_feature_detected!("fp")), + ("fp16", std::arch::is_aarch64_feature_detected!("fp16")), + ( + "frintts", + std::arch::is_aarch64_feature_detected!("frintts"), + ), + ("i8mm", std::arch::is_aarch64_feature_detected!("i8mm")), + ("jsconv", std::arch::is_aarch64_feature_detected!("jsconv")), + ("lse", std::arch::is_aarch64_feature_detected!("lse")), + ("lse2", std::arch::is_aarch64_feature_detected!("lse2")), + ("mte", std::arch::is_aarch64_feature_detected!("mte")), + ("neon", std::arch::is_aarch64_feature_detected!("neon")), + ("paca", std::arch::is_aarch64_feature_detected!("paca")), + ("pacg", std::arch::is_aarch64_feature_detected!("pacg")), + ("rand", std::arch::is_aarch64_feature_detected!("rand")), + ("rcpc", std::arch::is_aarch64_feature_detected!("rcpc")), + ("rcpc2", std::arch::is_aarch64_feature_detected!("rcpc2")), + ("rdm", std::arch::is_aarch64_feature_detected!("rdm")), + ("sb", std::arch::is_aarch64_feature_detected!("sb")), + ("sha2", std::arch::is_aarch64_feature_detected!("sha2")), + ("sha3", std::arch::is_aarch64_feature_detected!("sha3")), + ("sm4", std::arch::is_aarch64_feature_detected!("sm4")), + ("ssbs", std::arch::is_aarch64_feature_detected!("ssbs")), + ("sve", std::arch::is_aarch64_feature_detected!("sve")), + ("sve2", std::arch::is_aarch64_feature_detected!("sve2")), + ( + "sve2-aes", + std::arch::is_aarch64_feature_detected!("sve2-aes"), + ), + ( + "sve2-bitperm", + std::arch::is_aarch64_feature_detected!("sve2-bitperm"), + ), + ( + "sve2-sha3", + std::arch::is_aarch64_feature_detected!("sve2-sha3"), + ), + ( + "sve2-sm4", + std::arch::is_aarch64_feature_detected!("sve2-sm4"), + ), + ("tme", std::arch::is_aarch64_feature_detected!("tme")), + ]; + features.extend( + detected + .into_iter() + .filter(|(_, available)| *available) + .map(|(name, _)| name.to_string()), + ); + } + features.sort(); + features +} + +#[cfg(test)] +#[path = "artifact_tests.rs"] +pub(crate) mod tests; diff --git a/pyamplicol/rust/crates/rusticol-core/src/artifact_tests.rs b/pyamplicol/rust/crates/rusticol-core/src/artifact_tests.rs new file mode 100644 index 0000000000..2b852031ec --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/artifact_tests.rs @@ -0,0 +1,3040 @@ +// SPDX-License-Identifier: 0BSD + +use super::*; +use crate::pacbin::{PacbinWriteMember, PacbinWriteOptions, write_pacbin_atomic}; +use serde_json::{Value, json}; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::time::{SystemTime, UNIX_EPOCH}; + +static TEST_ARTIFACT_COUNTER: AtomicU64 = AtomicU64::new(0); + +#[test] +fn walsh_color_contraction_is_a_known_artifact_capability() { + let capability = crate::engine::COMPILED_COLOR_CONTRACTION_WALSH_CAPABILITY.to_string(); + let validated = + validate_runtime_capabilities(std::slice::from_ref(&capability), "test capabilities") + .expect("Walsh color-contraction capability"); + + assert_eq!(validated, BTreeSet::from([capability])); +} + +#[test] +fn c2k_walsh_color_contraction_is_a_known_artifact_capability() { + let capability = crate::engine::COMPILED_COLOR_CONTRACTION_WALSH_C2K_CAPABILITY.to_string(); + let validated = + validate_runtime_capabilities(std::slice::from_ref(&capability), "test capabilities") + .expect("C2^k Walsh color-contraction capability"); + + assert_eq!(validated, BTreeSet::from([capability])); +} + +#[test] +fn compiled_plane_arena_is_a_known_artifact_capability() { + let capability = crate::engine::COMPILED_PLANE_ARENA_RUNTIME_CAPABILITY.to_string(); + let validated = + validate_runtime_capabilities(std::slice::from_ref(&capability), "test capabilities") + .expect("compiled plane-arena capability"); + + assert_eq!(validated, BTreeSet::from([capability])); +} + +#[test] +fn typed_compiled_color_member_is_required_and_kind_checked() { + let mut artifact = TestArtifact::new(); + let destination = artifact.root.join("evaluators.pacbin"); + let index = write_pacbin_atomic( + &destination, + vec![ + PacbinWriteMember::from_bytes( + "processes/p0/compiled-color.pacrclr3", + PacbinMemberKind::ColorContraction, + b"color-payload", + ) + .unwrap(), + PacbinWriteMember::from_bytes( + "processes/p0/wrong-kind.pacrclr3", + PacbinMemberKind::SymbolicaExactState, + b"wrong-kind", + ) + .unwrap(), + ], + PacbinWriteOptions::default(), + ) + .unwrap(); + let container = fs::read(&destination).expect("read evaluator container"); + add_test_payload( + &mut artifact, + "evaluators.pacbin", + "evaluator-state", + &container, + None, + true, + ); + artifact.manifest["extensions"]["evaluator_payload_container"] = json!({ + "kind": "pyamplicol-evaluator-payload-container", + "schema_version": 1, + "storage_abi": "pacbin-v1", + "path": "evaluators.pacbin", + "member_count": index.members().len(), + "unpacked_size_bytes": index.members().iter().map(|member| member.length()).sum::(), + "index_sha256": hex_digest(index.index_sha256()), + }); + artifact.write_manifest(); + + let verified = VerifiedArtifact::open(&artifact.root).expect("typed color artifact"); + let store = verified + .evaluator_payload_store(&artifact.root.join("processes/p0")) + .expect("process evaluator payload store"); + + assert_eq!( + store + .packed_member_bytes( + "compiled-color.pacrclr3", + PacbinMemberKind::ColorContraction, + ) + .unwrap(), + b"color-payload", + ); + let wrong_kind = store + .packed_member_bytes("wrong-kind.pacrclr3", PacbinMemberKind::ColorContraction) + .unwrap_err(); + assert_eq!(wrong_kind.kind(), crate::RusticolErrorKind::Integrity); + assert!(wrong_kind.message().contains("member kind")); + let missing = store + .packed_member_bytes("missing.pacrclr3", PacbinMemberKind::ColorContraction) + .unwrap_err(); + assert_eq!(missing.kind(), crate::RusticolErrorKind::InvalidArgument); + assert!(missing.message().contains("unknown pacbin member")); + + let unpacked = EvaluatorPayloadStore::directory(&artifact.root); + let no_pack = unpacked + .packed_member_bytes( + "compiled-color.pacrclr3", + PacbinMemberKind::ColorContraction, + ) + .unwrap_err(); + assert_eq!(no_pack.kind(), crate::RusticolErrorKind::Compatibility); + assert!(no_pack.message().contains("requires evaluators.pacbin")); +} + +#[cfg(feature = "f64-symjit")] +const PYTHON_PACBIN_GOLDEN_HEX: &str = concat!( + "50414342494e000001004000000000004000000000000000c000000000000000", + "0200000000000000000000000000000000000000000000000000000000000000", + "6a69742d76310000000000000000000000000000000000000000000000000000", + "0000000000000000000000000000000000000000000000000000000000000000", + "65786163742d7374617465000000000000000000000000000000000000000000", + "0000000000000000000000000000000000000000000000000000000000000000", + "5041434944580000010020000000000002000000000000000000000000000000", + "0c0000000100000040000000000000000600000000000000e9a628134c31e4a5", + "7771ecd7a1dd51c5c5bc7daca10a2553c83f8af1652d2587612f6a69742e7379", + "6d6a6974000000000b0000000200000080000000000000000b00000000000000", + "a9f46d72eb6f1c7e5d62e97d3d8e67b80605aeba2df022a8a25b51ca3fc89805", + "7a2f65786163742e62696e0000000000504143454e4400000100400000000000", + "c0000000000000000200000000000000263b30eb85e5b2a82600201822438767", + "c5f66d9eecf2e08a0202383aafe0e3c1", +); + +#[test] +fn distribution_version_normalization_is_narrow() { + assert_eq!( + canonical_distribution_version("0.1.0-dev.0+candidate.0123"), + "0.1.0.dev0+candidate.0123" + ); + assert_eq!(canonical_distribution_version("0.1.0"), "0.1.0"); + assert_eq!( + canonical_distribution_version("0.2.1-dev.0+candidate.3112b2494ca7"), + canonical_distribution_version("0.2.1.dev0+candidate.42ec2237ad19") + ); + for other in [ + "0.2.1", + "0.2.2.dev0+candidate.3112b2494ca7", + "0.2.1.dev0+other.3112b2494ca7", + ] { + assert_ne!( + canonical_distribution_version("0.2.1.dev0+candidate.3112b2494ca7"), + canonical_distribution_version(other) + ); + } + assert_ne!( + canonical_distribution_version("0.1.0-dev.0+candidate.0123"), + canonical_distribution_version("0.1.0-dev.0+candidate.4567") + ); +} + +#[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] +#[test] +fn compact_payload_identity_matches_the_python_canonical_contract() { + let artifact = TestArtifact::new(); + let typed: ArtifactManifest = + serde_json::from_value(artifact.manifest.clone()).expect("typed artifact manifest"); + assert_eq!( + compute_payload_artifact_id(&typed.payloads).expect("typed payload identity"), + artifact.manifest["artifact_id"] + .as_str() + .expect("manifest artifact ID") + ); +} + +#[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] +#[test] +fn default_recurrence_bootstrap_discovery_requires_exactly_one_stable_process() { + let artifact = TestArtifact::new(); + let processes = artifact.root.join("processes"); + fs::create_dir(&processes).expect("create process root"); + assert_eq!( + discover_single_recurrence_bootstrap_process(&artifact.root).unwrap(), + None + ); + + let first = processes.join("p0"); + fs::create_dir(&first).expect("create first process"); + fs::write(first.join(RECURRENCE_BOOTSTRAP_FILENAME), b"image").expect("write first bootstrap"); + assert_eq!( + discover_single_recurrence_bootstrap_process(&artifact.root).unwrap(), + Some("p0".to_string()) + ); + + let second = processes.join("p1"); + fs::create_dir(&second).expect("create second process"); + fs::write(second.join(RECURRENCE_BOOTSTRAP_FILENAME), b"image") + .expect("write second bootstrap"); + let error = discover_single_recurrence_bootstrap_process(&artifact.root).unwrap_err(); + assert_eq!(error.kind(), crate::RusticolErrorKind::Integrity); + assert!(error.to_string().contains("ambiguous")); +} + +struct TestArtifact { + root: PathBuf, + manifest: Value, +} + +impl TestArtifact { + fn new() -> Self { + let nonce = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("clock after epoch") + .as_nanos(); + let temp_root = std::env::temp_dir() + .canonicalize() + .expect("canonical temporary directory"); + let root = temp_root.join(format!( + "pyamplicol-rusticol-manifest-{}-{nonce}-{}", + std::process::id(), + TEST_ARTIFACT_COUNTER.fetch_add(1, Ordering::Relaxed) + )); + fs::create_dir(&root).expect("create test artifact"); + let files = [ + ("requested.toml", b"action = \"generate\"\n".as_slice()), + ("effective.toml", b"action = \"generate\"\n".as_slice()), + ("evaluator.json", b"{}".as_slice()), + ("physics.json", b"{}".as_slice()), + ]; + let mut payloads = Vec::new(); + for (path, bytes) in files { + fs::write(root.join(path), bytes).expect("write payload"); + let role = match path { + "requested.toml" => "configuration-requested", + "effective.toml" => "configuration-effective", + "evaluator.json" => "evaluator-manifest", + "physics.json" => "runtime-physics", + _ => unreachable!(), + }; + let mut payload = json!({ + "path": path, + "role": role, + "media_type": "application/octet-stream", + "size_bytes": bytes.len(), + "sha256": sha256(bytes), + "executable": false, + }); + if path == "physics.json" { + payload["process_id"] = json!("p0"); + } + payloads.push(payload); + } + let manifest = json!({ + "schema_version": 3, + "kind": "pyamplicol-process", + "artifact_id": "0".repeat(64), + "created_utc": "2026-07-15T00:00:00Z", + "producer": { + "distribution": "pyamplicol", + "version": env!("CARGO_PKG_VERSION"), + "versions": { + "python_api": PYTHON_API_VERSION, + "toml": TOML_SCHEMA_VERSION, + "compiled_model": COMPILED_MODEL_SCHEMA_VERSION, + "process_artifact": PROCESS_ARTIFACT_SCHEMA_VERSION, + "runtime_physics": RUNTIME_PHYSICS_SCHEMA_VERSION, + "symbolica_serialization": crate::SYMBOLICA_SERIALIZATION_ABI, + "c_abi": C_ABI_VERSION, + }, + "target": {"triple": current_target_triple(), "cpu_features": []}, + }, + "model": { + "name": "test-model", + "source_kind": "compiled-model", + "content_sha256": "1".repeat(64), + "compiled_schema_version": COMPILED_MODEL_SCHEMA_VERSION, + }, + "configuration": { + "toml_schema_version": TOML_SCHEMA_VERSION, + "requested_path": "requested.toml", + "effective_path": "effective.toml", + "adjustments": [], + }, + "processes": [{ + "id": "p0", + "expression": "a b > c", + "color_accuracy": "lc", + "external_pdgs": [1, -1, 22], + "physics_path": "physics.json", + "required_runtime_capabilities": [ + "symjit.application.complex-f64.v1" + ], + "aliases": [], + }], + "default_process_id": "p0", + "runtime": { + "engine": "rusticol", + "engine_version": env!("CARGO_PKG_VERSION"), + "evaluator_manifest_path": "evaluator.json", + "required_runtime_capabilities": [ + "symjit.application.complex-f64.v1" + ], + "api_bundle_path": null, + }, + "payloads": payloads, + "dependencies": [], + "extensions": { + "artifact_identity": { + "kind": ARTIFACT_IDENTITY_KIND, + "schema_version": ARTIFACT_IDENTITY_SCHEMA_VERSION, + }, + }, + }); + let mut artifact = Self { root, manifest }; + artifact.write_manifest(); + artifact + } + + fn write_manifest(&mut self) { + if self.manifest["schema_version"] == json!(PROCESS_ARTIFACT_SCHEMA_VERSION) { + self.manifest["artifact_id"] = + json!(compute_artifact_id(&self.manifest).expect("compute artifact identity")); + } + let mut bytes = String::new(); + write_python_canonical_json(&self.manifest, &mut bytes) + .expect("serialize canonical manifest"); + bytes.push('\n'); + fs::write(self.root.join(ARTIFACT_MANIFEST_FILE), bytes).expect("write manifest"); + } +} + +impl Drop for TestArtifact { + fn drop(&mut self) { + let _ = fs::remove_dir_all(&self.root); + } +} + +fn sha256(bytes: &[u8]) -> String { + format!("{:x}", Sha256::digest(bytes)) +} + +fn add_structural_source_proof(artifact: &mut TestArtifact) { + let path = "processes/p0/structural-source-proof.json"; + let bytes = br#"{"schema":"pyamplicol-generation-structural-source-proof-v1"}"#; + let output = artifact.root.join(path); + fs::create_dir_all(output.parent().expect("structural proof parent")) + .expect("create structural proof parent"); + fs::write(output, bytes).expect("write structural proof"); + artifact.manifest["payloads"] + .as_array_mut() + .expect("payload array") + .push(json!({ + "path": path, + "role": "structural-source-proof", + "media_type": "application/json", + "size_bytes": bytes.len(), + "sha256": sha256(bytes), + "executable": false, + "process_id": "p0", + })); + artifact.write_manifest(); +} + +#[cfg(feature = "f64-symjit")] +fn decode_hex(value: &str) -> Vec { + assert_eq!(value.len() % 2, 0); + value + .as_bytes() + .chunks_exact(2) + .map(|pair| { + let text = std::str::from_utf8(pair).expect("ASCII hex pair"); + u8::from_str_radix(text, 16).expect("valid hex pair") + }) + .collect() +} + +fn add_test_payload( + artifact: &mut TestArtifact, + path: &str, + role: &str, + bytes: &[u8], + process_id: Option<&str>, + targeted: bool, +) { + let output = artifact.root.join(path); + fs::create_dir_all(output.parent().expect("payload parent")).expect("create payload parent"); + fs::write(&output, bytes).expect("write mixed-runtime payload"); + let mut payload = json!({ + "path": path, + "role": role, + "media_type": "application/octet-stream", + "size_bytes": bytes.len(), + "sha256": sha256(bytes), + "executable": false, + }); + if let Some(process_id) = process_id { + payload["process_id"] = json!(process_id); + } + if targeted { + payload["target"] = json!({ + "triple": current_target_triple(), + "cpu_features": [], + }); + } + artifact.manifest["payloads"] + .as_array_mut() + .expect("payload array") + .push(payload); +} + +#[cfg(feature = "f64-symjit")] +fn replace_test_payload(artifact: &mut TestArtifact, path: &str, bytes: &[u8]) { + fs::write(artifact.root.join(path), bytes).expect("replace test payload"); + let payload = artifact.manifest["payloads"] + .as_array_mut() + .expect("payload array") + .iter_mut() + .find(|payload| payload["path"] == path) + .expect("declared test payload"); + payload["size_bytes"] = json!(bytes.len()); + payload["sha256"] = json!(sha256(bytes)); +} + +#[cfg(feature = "f64-symjit")] +fn add_test_evaluator_container(artifact: &mut TestArtifact) { + let bytes = decode_hex(PYTHON_PACBIN_GOLDEN_HEX); + add_test_payload( + artifact, + "evaluators.pacbin", + "evaluator-state", + &bytes, + None, + true, + ); + artifact.manifest["extensions"]["evaluator_payload_container"] = json!({ + "kind": "pyamplicol-evaluator-payload-container", + "schema_version": 1, + "storage_abi": "pacbin-v1", + "path": "evaluators.pacbin", + "member_count": 2, + "unpacked_size_bytes": 17, + "index_sha256": "263b30eb85e5b2a82600201822438767c5f66d9eecf2e08a0202383aafe0e3c1", + }); + artifact.write_manifest(); +} + +#[cfg(feature = "f64-symjit")] +const DIRECT_SYMJIT_PROGRAM: &str = r#"[[{"Add":[{"Out":0},[{"Param":0},{"Param":1}],0]}],1,[]]"#; + +#[cfg(feature = "f64-symjit")] +fn direct_symjit_application_bytes(input_len: usize) -> Vec { + use symjit::{Compiler, CompilerType, Config, Storage}; + + let mut config = Config::new(CompilerType::Native, 0).expect("native SymJIT config"); + config.set_complex(true); + config.set_symbolica(true); + config.set_opt_level(3); + config.set_simd(true); + let mut compiler = Compiler::with_config(config); + let application = compiler + .translate(DIRECT_SYMJIT_PROGRAM.to_string(), input_len) + .expect("translate direct SymJIT test application"); + let mut bytes = Vec::new(); + application + .save(&mut bytes) + .expect("serialize direct SymJIT test application"); + bytes +} + +#[cfg(feature = "f64-symjit")] +fn direct_symjit_plane_application_bytes(input_len: usize) -> Vec { + crate::engine::compile_symbolica_program_to_plane_application_bytes( + DIRECT_SYMJIT_PROGRAM, + input_len, + 1, + 3, + false, + ) + .expect("translate direct SymJIT test plane application") +} + +#[cfg(feature = "f64-symjit")] +pub(crate) fn minimal_runtime_physics( + process_id: &str, + process: &str, + final_particle: &str, +) -> Value { + json!({ + "schema_version": RUNTIME_PHYSICS_SCHEMA_VERSION, + "kind": "pyamplicol-resolved-physics", + "process_id": process_id, + "process": process, + "color_accuracy": "lc", + "coverage": { + "helicities": "complete", + "color": "complete", + "color_kind": "physical-lc-flows", + "structural_zero_helicity_count": 0, + }, + "external_particles": [ + { + "index": 0, + "label": 1, + "particle": "a", + "pdg": 1, + "role": "initial", + "momentum_slot": 0, + "momentum_components": ["E", "px", "py", "pz"], + }, + { + "index": 1, + "label": 2, + "particle": "b", + "pdg": -1, + "role": "initial", + "momentum_slot": 1, + "momentum_components": ["E", "px", "py", "pz"], + }, + { + "index": 2, + "label": 3, + "particle": final_particle, + "pdg": 22, + "role": "final", + "momentum_slot": 2, + "momentum_components": ["E", "px", "py", "pz"], + }, + ], + "helicities": [{ + "id": "h:+0,+0,+0", + "index": 0, + "values": [0, 0, 0], + "computed": true, + "structural_zero": false, + "representative_id": "h:+0,+0,+0", + "coefficient": 1.0, + }], + "color_components": [{ + "kind": "lc-flow", + "id": "flow:singlet", + "index": 0, + "word": [], + "computed": true, + "representative_id": "flow:singlet", + "coefficient": 1.0, + }], + "reduction": { + "kind": "lc-diagonal", + "groups": [{ + "id": "group:7", + "representative_helicity_id": "h:+0,+0,+0", + "representative_color_id": "flow:singlet", + "physical_helicity_ids": ["h:+0,+0,+0"], + "physical_color_ids": ["flow:singlet"], + }], + }, + "model_parameters": [], + "selectors": { + "helicity": true, + "color_flow": true, + "contracted_color": false, + }, + "extensions": {}, + }) +} + +#[cfg(feature = "f64-symjit")] +pub(crate) fn direct_evaluator_manifest(application_path: &str) -> Value { + json!({ + "kind": "symjit-application-evaluator", + "runtime_capability": "symjit.application.complex-f64.v1", + "application_path": application_path, + "application_abi": crate::engine::SYMJIT_APPLICATION_STORAGE_ABI, + "input_len": 14, + "output_len": 1, + "element_layout": "complex-f64", + "batch_layout": "row-major", + "compiler_type": "native", + "translation_mode": "indirect", + "optimization_level": 3, + "word_bits": 64, + "endianness": "little", + "required_defuns": [], + "evaluator_state_path": null, + "evaluator_state_runtime_capability": null, + }) +} + +#[cfg(feature = "f64-symjit")] +fn compiled_evaluator_manifest(library_path: &str, runtime_capability: &str) -> Value { + json!({ + "kind": "compiled-complex-evaluator", + "runtime_capability": runtime_capability, + "function_name": "rusticol_test_evaluator", + "input_len": 14, + "output_len": 1, + "library_path": library_path, + "evaluator_state_path": null, + "number_type": "complex-f64", + }) +} + +#[cfg(feature = "f64-symjit")] +pub(crate) fn minimal_execution_manifest( + process_id: &str, + process: &str, + capability: &str, + evaluator: Value, +) -> Value { + let value_slot = |value_slot_id: usize, current_id: usize, component: usize| { + json!({ + "value_slot_id": value_slot_id, + "current_id": current_id, + "variant": "source", + "component_start": component, + "component_stop": component + 1, + "dimension": 1, + }) + }; + let slot = |current_id: usize, component: usize| { + json!({ + "current_id": current_id, + "component_start": component, + "component_stop": component + 1, + "dimension": 1, + }) + }; + let crossing_ir = || { + json!({ + "momentum_transform": "negate-four-momentum", + "helicity_factor": 1, + "chirality_factor": 1, + "spin_state_factor": 1, + "phase": [1.0, 0.0], + }) + }; + let source_ir = |particle_id: i32, anti_particle_id: i32, orientation: &str| { + json!({ + "identity": { + "canonical_id": format!("model:minimal:state:{particle_id}"), + "species_id": "model:minimal:species:1", + "anti_canonical_id": format!("model:minimal:state:{anti_particle_id}"), + "display_name": format!("state_{particle_id}"), + "anti_display_name": format!("state_{anti_particle_id}"), + "pdg_label": particle_id, + "anti_pdg_label": anti_particle_id, + "orientation": orientation, + "self_conjugate": false, + }, + "statistics": "boson", + "wavefunction_family": "scalar", + "component_dimension": 1, + "states": [{"helicity": 0, "chirality": 0, "spin_state": 1}], + "crossing": crossing_ir(), + "basis": "scalar", + "mass_parameter": null, + "width_parameter": null, + }) + }; + let propagator_ir = |particle_id: i32, anti_particle_id: i32, orientation: &str| { + json!({ + "identity": { + "canonical_id": format!("model:minimal:state:{particle_id}"), + "species_id": "model:minimal:species:1", + "anti_canonical_id": format!( + "model:minimal:state:{anti_particle_id}" + ), + "display_name": format!("state_{particle_id}"), + "anti_display_name": format!("state_{anti_particle_id}"), + "pdg_label": particle_id, + "anti_pdg_label": anti_particle_id, + "orientation": orientation, + "self_conjugate": false, + }, + "particle_id": particle_id, + "chirality": 0, + "kind": "scalar", + "backend": "test", + "basis": "scalar", + "applies_propagator": true, + "kernel": "test_scalar", + "full_tensor_network_ready": true, + "mass_class": "massless", + "gauge": null, + "numerator": "i", + "denominator": "momentum_squared-mass_squared+i*mass*width", + "mass_parameter": null, + "width_parameter": null, + "custom_source": null, + "auxiliary_policy": null, + "goldstone_policy": "not-applicable", + "description": "test scalar propagator", + }) + }; + let real_inputs = (2..14).collect::>(); + let current_storage = json!({ + "component_count": 2, + "number_type": "complex", + "metadata_compacted": true, + "current_slots": [ + { + "current_id": 0, "component_start": 0, "component_stop": 1, + "dimension": 1, "is_source": true, "particle_id": 1, + "external_mask": 1, "external_labels": [], "helicity_ancestry": null, + "chirality": 0, "spin_state": null, "flavour_flow": [], + "color_state": null, "momentum_mask": 1, + "auxiliary_kind": null + }, + { + "current_id": 1, "component_start": 1, "component_stop": 2, + "dimension": 1, "is_source": true, "particle_id": -1, + "external_mask": 2, "external_labels": [], "helicity_ancestry": null, + "chirality": 0, "spin_state": null, "flavour_flow": [], + "color_state": null, "momentum_mask": 2, + "auxiliary_kind": null + } + ], + }); + let value_storage = json!({ + "component_count": 2, + "number_type": "complex", + "metadata_compacted": true, + "value_slots": [ + { + "value_slot_id": 0, "current_id": 0, "variant": "source", + "component_start": 0, "component_stop": 1, "dimension": 1, + "current_component_start": 0, "current_component_stop": 1, + "is_source": true, "applies_propagator": false, "particle_id": 1, + "external_mask": 1, "external_labels": [], "momentum_mask": 1, + "chirality": 0, + "propagator": propagator_ir(1, -1, "particle") + }, + { + "value_slot_id": 1, "current_id": 1, "variant": "source", + "component_start": 1, "component_stop": 2, "dimension": 1, + "current_component_start": 1, "current_component_stop": 2, + "is_source": true, "applies_propagator": false, "particle_id": -1, + "external_mask": 2, "external_labels": [], "momentum_mask": 2, + "chirality": 0, + "propagator": propagator_ir(-1, 1, "antiparticle") + } + ], + }); + let source_fill = json!({ + "source_count": 2, + "sources": [ + { + "source_id": 0, "current_id": 0, + "current_component_start": 0, "current_component_stop": 1, + "value_slot": value_slot(0, 0, 0), + "source_parameter_start": 0, "source_parameter_stop": 1, + "leg_label": 1, "input_momentum_slot": 0, "side": "initial", + "crossing": "negate-incoming-momentum", "physical_pdg": 1, "outgoing_pdg": 1, + "particle_id": 1, "anti_particle_id": -1, + "source_kind": "external-wavefunction", + "wavefunction_kind": "scalar", "source_orientation": "particle", + "source_basis": "scalar", "source_ir": source_ir(1, -1, "particle"), + "applied_crossing": crossing_ir(), + "source_helicity": 0, + "chirality": 0, "spin_state": 1, "dimension": 1, + "helicity_ancestry": 1, "color_state": {} + }, + { + "source_id": 1, "current_id": 1, + "current_component_start": 1, "current_component_stop": 2, + "value_slot": value_slot(1, 1, 1), + "source_parameter_start": 1, "source_parameter_stop": 2, + "leg_label": 2, "input_momentum_slot": 1, "side": "initial", + "crossing": "negate-incoming-momentum", "physical_pdg": -1, "outgoing_pdg": -1, + "particle_id": -1, "anti_particle_id": 1, + "source_kind": "external-wavefunction", + "wavefunction_kind": "scalar", "source_orientation": "antiparticle", + "source_basis": "scalar", "source_ir": source_ir(-1, 1, "antiparticle"), + "applied_crossing": crossing_ir(), + "source_helicity": 0, + "chirality": 0, "spin_state": 1, "dimension": 1, + "helicity_ancestry": 1, "color_state": {} + } + ], + }); + let amplitude_stage = json!({ + "stage_kind": "amplitude-roots", + "output_count": 1, + "color_contraction": null, + "roots": [{ + "output_index": 0, "root_id": 0, "kind": "direct-contraction", + "left_current_id": 0, "right_current_id": 1, + "left_slot": slot(0, 0), "right_slot": slot(1, 1), + "left_value_slot": value_slot(0, 0, 0), + "right_value_slot": value_slot(1, 1, 1), + "vertex_kind": null, "vertex_particles": null, + "coupling": [1.0, 0.0], "color_weight": [1.0, 0.0], + "color_sector_id": 0, "contraction": "minimal-test", + "contraction_ir": { + "name": "minimal-test", + "left_basis": "scalar", + "right_basis": "scalar", + "coefficients": [[1.0, 0.0]], + "chirality_relation": "any", + "metric_signature": null + }, + "coherent_group_id": 7, "helicity_weight": 1.0, + "all_sector_weight": 1.0 + }], + }); + let runtime_schema = json!({ + "schema_version": PROCESS_ARTIFACT_SCHEMA_VERSION, + "kind": "pyamplicol-runtime-execution-plan", + "process_key": process_id, + "process": process, + "external_particles": [ + {"label": 1, "index": 0, "pdg": 1, "outgoing_pdg": 1, "role": "initial", "momentum_slot": 0}, + {"label": 2, "index": 1, "pdg": -1, "outgoing_pdg": -1, "role": "initial", "momentum_slot": 1}, + {"label": 3, "index": 2, "pdg": 22, "outgoing_pdg": 22, "role": "final", "momentum_slot": 2}, + ], + "model": null, + "model_parameters": [], + "normalization": null, + "parameter_layout": { + "source_component_parameter_count": 2, + "momentum_parameter_count": 12, + "model_parameter_count": 0, + "parameter_count_if_flattened": 14, + "value_component_count": 2, + "source_components_complex": true, + "momentum_components_real": true, + "real_valued_inputs": real_inputs, + }, + "current_storage": current_storage, + "value_storage": value_storage, + "source_fill": source_fill, + "momentum_slots": [ + {"momentum_slot_id": 0, "momentum_mask": 1, "external_labels": [1], "component_start": 0, "component_stop": 4, "real_valued": true}, + {"momentum_slot_id": 1, "momentum_mask": 2, "external_labels": [2], "component_start": 4, "component_stop": 8, "real_valued": true}, + {"momentum_slot_id": 2, "momentum_mask": 4, "external_labels": [3], "component_start": 8, "component_stop": 12, "real_valued": true}, + ], + "stages": [], + "amplitude_stage": amplitude_stage, + }); + let serialized_amplitude = json!({ + "stage_index": 0, + "stage_kind": "amplitude-roots", + "subset_size": null, + "evaluator_label": "minimal_amplitude", + "parameter_layout": "global-value-momentum", + "output_length": 1, + "output_slots": [{ + "value_slot_id": -1, + "current_id": -1, + "variant": "amplitude-root", + "component_start": 0, + "component_stop": 1, + "output_start": 0, + "output_stop": 1, + }], + "input_value_slot_ids": [0, 1], + "output_value_slot_ids": [], + "interaction_ids": [], + "input_components": [], + "parameter_count": 14, + "value_parameter_count": 2, + "momentum_parameter_count": 12, + "model_parameter_count": 0, + "real_valued_inputs": (2..14).collect::>(), + "expression_ready": true, + "blockers": [], + "evaluator": evaluator, + }); + let compiled = json!({ + "kind": "generic-dag-stage-blueprint", + "runtime_available": true, + "runtime_unavailable_message": null, + "model_parameter_evaluator": null, + "stage_evaluators": { + "kind": "generic-dag-stage-evaluator-artifacts", + "required_runtime_capabilities": [capability], + "runtime_available": true, + "runtime_unavailable_message": null, + "parameter_count": 14, + "value_parameter_count": 2, + "momentum_parameter_count": 12, + "model_parameter_count": 0, + "real_valued_inputs": (2..14).collect::>(), + "parameter_layout": "global-value-momentum", + "stage_count": 1, + "stages": [], + "amplitude_stage": serialized_amplitude, + }, + }); + json!({ + "schema_version": PROCESS_ARTIFACT_SCHEMA_VERSION, + "kind": "pyamplicol-runtime-execution", + "required_runtime_capabilities": [capability], + "process": process, + "key": process_id, + "color_accuracy": "lc", + "external_pdg_order": [1, -1, 22], + "compiled": compiled, + "dag_summary": { + "current_count": 2, + "source_count": 2, + "interaction_count": 0, + "interaction_evaluation_count": 0, + "amplitude_root_count": 1, + "truncated": false, + }, + "materialization_census": { + "abi": "pyamplicol-fully-resident-materialization-census-v1", + "basis": "immutable-fully-resident-compiled-dag", + "final": { + "current_count": 2, + "source_count": 2, + "interaction_count": 0, + "interaction_evaluation_count": 0, + "amplitude_root_count": 1, + }, + "peak": { + "current_count": 2, + "source_count": 2, + "interaction_count": 0, + "interaction_evaluation_count": 0, + "amplitude_root_count": 1, + }, + "final_equals_peak": true, + }, + "runtime_schema": runtime_schema, + }) +} + +#[cfg(feature = "f64-symjit")] +pub(crate) fn mock_helicity_materialization() -> Value { + json!({ + "kind": "pyamplicol-helicity-recurrence", + "contract_version": 1, + "proof_algorithm": "canonical-source-transition-dependency-shape-v1", + "current_count": 2, + "amplitude_root_count": 1, + "proof_counts": { + "recurrence_class_count": 0, + "optimized_recurrence_class_count": 0, + "optimized_current_count": 0, + "residual_current_count": 2, + "amplitude_class_count": 0, + "optimized_amplitude_class_count": 0, + "residual_amplitude_count": 1, + "source_state_mapping_count": 0, + "physical_helicity_count": 2, + "structural_zero_helicity_count": 0, + }, + "selector_domains": [ + {"id": 0, "complete": true, "source_states": []}, + {"id": 1, "complete": true, "source_states": []}, + ], + "source_state_mappings": [], + "recurrence_classes": [], + "amplitude_classes": [], + "residual_current_ids": [0, 1], + "residual_root_ids": [0], + "structural_zero_selector_domain_ids": [], + "diagnostics": [], + "materialization": { + "kind": "pyamplicol-helicity-recurrence-materialization", + "contract_version": 1, + "strategy": "quotient", + "proof_current_count": 2, + "proof_root_count": 1, + "materialized_current_count": 2, + "materialized_root_count": 1, + "proof_to_materialized_current": [0, 1], + "source_routes": [], + "amplitude_routes": [], + "selector_schedules": [], + }, + }) +} + +#[cfg(feature = "f64-symjit")] +pub(crate) fn minimal_helicity_selector_lane_execution() -> Value { + const DIRECT: &str = "symjit.application.complex-f64.v1"; + let mut execution = minimal_execution_manifest( + "p0", + "a b > c", + DIRECT, + direct_evaluator_manifest("evaluators/direct.symjit"), + ); + let mut lane = execution.clone(); + lane["physics_reduction"] = minimal_runtime_physics("p0", "a b > c", "c")["reduction"].clone(); + execution["runtime_schema"]["helicity_recurrence"] = mock_helicity_materialization(); + execution["required_runtime_capabilities"] = json!([ + crate::engine::COMPILED_HELICITY_PRIMARY_RECURRENCE_CAPABILITY, + crate::engine::COMPILED_HELICITY_SELECTOR_UNION_CAPABILITY, + crate::engine::COMPILED_RUNTIME_SELECTORS_CAPABILITY, + DIRECT, + ]); + execution["helicity_selector_executions"] = json!([{ + "selector_domain_ids": [0, 1], + "execution": lane, + }]); + execution +} + +#[cfg(feature = "f64-symjit")] +fn helicity_selector_lane_artifact(execution: Value) -> TestArtifact { + const DIRECT: &str = "symjit.application.complex-f64.v1"; + let mut artifact = TestArtifact::new(); + let capabilities = json!([ + crate::engine::COMPILED_HELICITY_PRIMARY_RECURRENCE_CAPABILITY, + crate::engine::COMPILED_HELICITY_SELECTOR_UNION_CAPABILITY, + crate::engine::COMPILED_RUNTIME_SELECTORS_CAPABILITY, + DIRECT, + ]); + artifact.manifest["processes"][0]["required_runtime_capabilities"] = capabilities.clone(); + artifact.manifest["runtime"]["required_runtime_capabilities"] = capabilities; + replace_test_payload( + &mut artifact, + "evaluator.json", + &serde_json::to_vec(&execution).expect("serialize execution fixture"), + ); + replace_test_payload( + &mut artifact, + "physics.json", + &serde_json::to_vec(&minimal_runtime_physics("p0", "a b > c", "c")) + .expect("serialize physics fixture"), + ); + add_test_payload( + &mut artifact, + "evaluators/direct.symjit", + "evaluator-state", + &direct_symjit_application_bytes(14), + Some("p0"), + true, + ); + artifact.write_manifest(); + artifact +} + +#[cfg(feature = "f64-symjit")] +fn helicity_selector_lane_load_error(execution: Value) -> crate::RusticolError { + let artifact = helicity_selector_lane_artifact(execution); + match crate::NativeRuntime::load(&artifact.root, Some("p0"), None) { + Ok(_) => panic!("invalid compiled helicity selector lane unexpectedly loaded"), + Err(error) => error, + } +} + +#[cfg(feature = "f64-symjit")] +#[test] +fn compiled_helicity_selector_lanes_reject_duplicate_domains() { + let mut execution = minimal_helicity_selector_lane_execution(); + let mut duplicate = execution["helicity_selector_executions"][0].clone(); + duplicate["selector_domain_ids"] = json!([1]); + execution["helicity_selector_executions"] + .as_array_mut() + .unwrap() + .push(duplicate); + + let error = helicity_selector_lane_load_error(execution); + + assert_eq!(error.kind(), crate::RusticolErrorKind::Integrity, "{error}"); + assert!( + error.to_string().contains("duplicate selector domain 1"), + "{error}" + ); +} + +#[cfg(feature = "f64-symjit")] +#[test] +fn compiled_helicity_selector_lanes_reject_unknown_domains() { + let mut execution = minimal_helicity_selector_lane_execution(); + execution["helicity_selector_executions"][0]["selector_domain_ids"] = json!([2]); + + let error = helicity_selector_lane_load_error(execution); + + assert_eq!(error.kind(), crate::RusticolErrorKind::Integrity, "{error}"); + assert!( + error.to_string().contains("unknown selector domain 2"), + "{error}" + ); +} + +#[cfg(feature = "f64-symjit")] +#[test] +fn compiled_helicity_selector_lanes_reject_nested_auxiliaries() { + for nested_field in ["helicity_sum_execution", "color_selector_executions"] { + let mut execution = minimal_helicity_selector_lane_execution(); + let lane = &mut execution["helicity_selector_executions"][0]["execution"]; + let nested = lane.clone(); + lane[nested_field] = match nested_field { + "helicity_sum_execution" => nested, + _ => json!([{"materialized_sector_id": 0, "execution": nested}]), + }; + + let error = helicity_selector_lane_load_error(execution); + + assert_eq!(error.kind(), crate::RusticolErrorKind::Integrity, "{error}"); + assert!( + error.to_string().contains( + "helicity-selector execution contains an unsupported auxiliary execution" + ), + "{nested_field}: {error}" + ); + } +} + +#[cfg(feature = "f64-symjit")] +#[test] +fn parent_closure_helicity_selector_lane_rejects_nested_closure() { + let mut execution = minimal_helicity_selector_lane_execution(); + let lane = &mut execution["helicity_selector_executions"][0]["execution"]; + let nested = lane.clone(); + lane["helicity_selector_executions"] = + json!([{"selector_domain_ids": [0], "execution": nested}]); + + let error = helicity_selector_lane_load_error(execution); + + assert_eq!(error.kind(), crate::RusticolErrorKind::Integrity, "{error}"); + assert!( + error + .to_string() + .contains("only a nested-runtime helicity-selector execution"), + "{error}" + ); +} + +#[cfg(feature = "f64-symjit")] +#[test] +fn compiled_helicity_selector_lanes_reject_nested_materialization() { + let mut execution = minimal_helicity_selector_lane_execution(); + execution["helicity_selector_executions"][0]["execution"]["runtime_schema"]["helicity_recurrence"] = + mock_helicity_materialization(); + + let error = helicity_selector_lane_load_error(execution); + + assert_eq!(error.kind(), crate::RusticolErrorKind::Integrity, "{error}"); + assert!( + error + .to_string() + .contains("cannot contain helicity materialization"), + "{error}" + ); +} + +#[cfg(feature = "f64-symjit")] +#[test] +fn nested_runtime_helicity_selector_lane_requires_materialization() { + let mut execution = minimal_helicity_selector_lane_execution(); + execution["helicity_selector_executions"][0]["schedule_mode"] = json!("nested-runtime"); + + let error = helicity_selector_lane_load_error(execution); + + assert_eq!(error.kind(), crate::RusticolErrorKind::Integrity, "{error}"); + assert!( + error.to_string().contains( + "nested-runtime helicity-selector execution requires helicity materialization" + ), + "{error}" + ); +} + +#[cfg(feature = "f64-symjit")] +#[test] +fn compiled_helicity_selector_lanes_verify_nested_payloads() { + let mut execution = minimal_helicity_selector_lane_execution(); + execution["helicity_selector_executions"][0]["execution"]["compiled"]["stage_evaluators"]["amplitude_stage"] + ["evaluator"]["application_path"] = json!("evaluators/undeclared-selector-lane.symjit"); + + let error = helicity_selector_lane_load_error(execution); + + assert_eq!(error.kind(), crate::RusticolErrorKind::Security, "{error}"); + assert!( + error + .to_string() + .contains("undeclared-selector-lane.symjit"), + "{error}" + ); +} + +#[cfg(feature = "f64-symjit")] +fn minimal_color_topology_lane_execution() -> Value { + const DIRECT: &str = "symjit.application.complex-f64.v1"; + let mut execution = minimal_execution_manifest( + "p0", + "a b > c", + DIRECT, + direct_evaluator_manifest("evaluators/direct.symjit"), + ); + let mut lane = execution.clone(); + lane["physics_reduction"] = minimal_runtime_physics("p0", "a b > c", "c")["reduction"].clone(); + execution["required_runtime_capabilities"] = json!([ + crate::engine::COMPILED_COLOR_TOPOLOGY_LANES_CAPABILITY, + crate::engine::COMPILED_RUNTIME_SELECTORS_CAPABILITY, + DIRECT, + ]); + execution["color_selector_executions"] = json!([{ + "materialized_sector_id": 0, + "execution": lane, + }]); + execution +} + +#[cfg(feature = "f64-symjit")] +fn color_topology_lane_artifact(execution: Value) -> TestArtifact { + const DIRECT: &str = "symjit.application.complex-f64.v1"; + let mut artifact = TestArtifact::new(); + let capabilities = json!([ + crate::engine::COMPILED_COLOR_TOPOLOGY_LANES_CAPABILITY, + crate::engine::COMPILED_RUNTIME_SELECTORS_CAPABILITY, + DIRECT, + ]); + artifact.manifest["processes"][0]["required_runtime_capabilities"] = capabilities.clone(); + artifact.manifest["runtime"]["required_runtime_capabilities"] = capabilities; + replace_test_payload( + &mut artifact, + "evaluator.json", + &serde_json::to_vec(&execution).expect("serialize execution fixture"), + ); + replace_test_payload( + &mut artifact, + "physics.json", + &serde_json::to_vec(&minimal_runtime_physics("p0", "a b > c", "c")) + .expect("serialize physics fixture"), + ); + add_test_payload( + &mut artifact, + "evaluators/direct.symjit", + "evaluator-state", + &direct_symjit_application_bytes(14), + Some("p0"), + true, + ); + artifact.write_manifest(); + artifact +} + +#[cfg(feature = "f64-symjit")] +fn color_topology_lane_load_error(execution: Value) -> crate::RusticolError { + let artifact = color_topology_lane_artifact(execution); + match crate::NativeRuntime::load(&artifact.root, Some("p0"), None) { + Ok(_) => panic!("invalid compiled color topology lane unexpectedly loaded"), + Err(error) => error, + } +} + +#[cfg(feature = "f64-symjit")] +#[test] +fn compiled_color_topology_lane_rejects_duplicate_materialized_sector_ids() { + let mut execution = minimal_color_topology_lane_execution(); + let duplicate = execution["color_selector_executions"][0].clone(); + execution["color_selector_executions"] + .as_array_mut() + .unwrap() + .push(duplicate); + + let error = color_topology_lane_load_error(execution); + + assert_eq!(error.kind(), crate::RusticolErrorKind::Integrity, "{error}"); + assert!( + error + .to_string() + .contains("duplicate materialized sector 0") + ); +} + +#[cfg(feature = "f64-symjit")] +#[test] +fn compiled_color_topology_lane_rejects_recursive_auxiliary_executions() { + for nested_field in [ + "helicity_sum_execution", + "helicity_selector_executions", + "color_selector_executions", + ] { + let mut execution = minimal_color_topology_lane_execution(); + let lane = &mut execution["color_selector_executions"][0]["execution"]; + let nested = lane.clone(); + lane[nested_field] = match nested_field { + "helicity_sum_execution" => nested, + "helicity_selector_executions" => { + json!([{"selector_domain_ids": [0], "execution": nested}]) + } + _ => json!([{"materialized_sector_id": 1, "execution": nested}]), + }; + + let error = color_topology_lane_load_error(execution); + + assert_eq!(error.kind(), crate::RusticolErrorKind::Integrity, "{error}"); + assert!( + error + .to_string() + .contains("color-selector execution cannot contain auxiliary executions"), + "{nested_field}: {error}" + ); + } +} + +#[cfg(feature = "f64-symjit")] +#[test] +fn compiled_color_topology_lane_verifies_nested_evaluator_payloads() { + let mut execution = minimal_color_topology_lane_execution(); + execution["color_selector_executions"][0]["execution"]["compiled"]["stage_evaluators"]["amplitude_stage"] + ["evaluator"]["application_path"] = json!("evaluators/undeclared-lane.symjit"); + + let error = color_topology_lane_load_error(execution); + + assert_eq!(error.kind(), crate::RusticolErrorKind::Security, "{error}"); + assert!( + error.to_string().contains("undeclared-lane.symjit"), + "{error}" + ); +} + +#[cfg(feature = "f64-symjit")] +fn mixed_backend_runtime_artifact() -> TestArtifact { + const DIRECT: &str = "symjit.application.complex-f64.v1"; + const ARENA: &str = crate::engine::COMPILED_PLANE_ARENA_RUNTIME_CAPABILITY; + const ASM: &str = "symbolica.compiled-asm.complex-f64.v1"; + const CPP: &str = "symbolica.compiled-cpp.complex-f64.v1"; + let mut artifact = TestArtifact::new(); + artifact.manifest["kind"] = json!("pyamplicol-process-set"); + artifact.manifest["default_process_id"] = json!("direct"); + artifact.manifest["processes"] = json!([ + { + "id": "direct", + "expression": "a b > c", + "color_accuracy": "lc", + "external_pdgs": [1, -1, 22], + "physics_path": "processes/direct/physics.json", + "required_runtime_capabilities": [ARENA, DIRECT], + "aliases": [], + }, + { + "id": "asm", + "expression": "a b > e", + "color_accuracy": "lc", + "external_pdgs": [1, -1, 22], + "physics_path": "processes/asm/physics.json", + "required_runtime_capabilities": [ASM], + "aliases": [], + }, + { + "id": "cpp", + "expression": "a b > d", + "color_accuracy": "lc", + "external_pdgs": [1, -1, 22], + "physics_path": "processes/cpp/physics.json", + "required_runtime_capabilities": [CPP], + "aliases": [], + } + ]); + artifact.manifest["runtime"]["evaluator_manifest_path"] = json!("processes/evaluators.json"); + artifact.manifest["runtime"]["required_runtime_capabilities"] = + json!([ARENA, ASM, CPP, DIRECT]); + + let direct_application = direct_symjit_application_bytes(14); + let direct_plane_application = direct_symjit_plane_application_bytes(14); + let mut direct_execution = minimal_execution_manifest( + "direct", + "a b > c", + DIRECT, + direct_evaluator_manifest("evaluators/direct.symjit"), + ); + direct_execution["compiled"]["momentum_slot_ids"] = json!([0, 1, 2]); + let input_binding = |parameter_index: usize| { + let (kind, source_id, component, global_component, real_valued) = if parameter_index < 2 { + ("value", parameter_index, 0, parameter_index, false) + } else { + ( + "momentum", + (parameter_index - 2) / 4, + (parameter_index - 2) % 4, + parameter_index, + true, + ) + }; + json!({ + "parameter_index": parameter_index, + "kind": kind, + "source_id": source_id, + "component": component, + "global_component": global_component, + "real_valued": real_valued, + }) + }; + let input_bindings = (0..14).map(input_binding).collect::>(); + let direct_stage = &mut direct_execution["compiled"]["stage_evaluators"]["amplitude_stage"]; + direct_stage["parameter_layout"] = json!("stage-local-value-momentum"); + direct_stage["input_components"] = json!(input_bindings); + direct_stage["evaluator"]["plane_application"] = json!({ + "application_path": "evaluators/direct.plane.symjit", + "application_abi": crate::engine::SYMJIT_PLANE_APPLICATION_ABI, + "storage_abi": crate::engine::SYMJIT_APPLICATION_STORAGE_ABI, + "element_layout": "split-complex-plane-major", + "descriptor_order": "inputs-re-im-then-outputs-re-im", + "input_complex_count": 14, + "output_complex_count": 1, + "input_plane_count": 28, + "output_plane_count": 2, + "compiler_type": "native", + "translation_mode": "symbolica-structured-instructions", + "optimization_level": 3, + "simd": true, + "complex": true, + "fast_math": true, + "fast_complex": false, + "compression": false, + "threading": false, + "direct_arena": true, + "source_digest": sha256(DIRECT_SYMJIT_PROGRAM.as_bytes()), + "target": { + "word_bits": 64, + "endianness": "little", + "triple": "test-native", + "cpu_features": [], + }, + }); + direct_stage["evaluator"]["evaluator_state_path"] = json!("evaluators/direct.exact.symbolica"); + direct_stage["evaluator"]["evaluator_state_runtime_capability"] = + json!(crate::engine::SYMBOLICA_LEGACY_JIT_RUNTIME_CAPABILITY); + direct_stage["compiled_plane_arena"] = json!({ + "schema_version": 1, + "kind": "compiled-plane-arena-stage", + "application_abi": crate::engine::COMPILED_PLANE_DIRECT_APPLICATION_ABI, + "source_application_abi": crate::engine::COMPILED_PLANE_SOURCE_APPLICATION_ABI, + "element_layout": "split-complex-component-major", + "output_operation": "overwrite", + "output_factor": "identity", + "input_output_aliasing": "forbidden", + "output_output_aliasing": "forbidden", + "input_bindings": (0..14).map(input_binding).collect::>(), + "output_bindings": [{ + "output_index": 0, + "arena": "amplitude", + "component": 0, + }], + "leaves": [{ + "application_path": "evaluators/direct.plane.symjit", + "source_application_abi": crate::engine::COMPILED_PLANE_SOURCE_APPLICATION_ABI, + "optimization_level": 3, + "direct_codegen_optimization_level": 3, + "input_len": 14, + "output_len": 1, + "input_indices": (0..14).collect::>(), + "output_start": 0, + "output_stop": 1, + }], + }); + direct_execution["compiled"]["stage_evaluators"]["parameter_layout"] = + json!("stage-local-value-momentum"); + direct_execution["compiled"]["stage_evaluators"]["parameter_count"] = json!(0); + direct_execution["compiled"]["stage_evaluators"]["value_parameter_count"] = json!(0); + direct_execution["compiled"]["stage_evaluators"]["momentum_parameter_count"] = json!(0); + direct_execution["compiled"]["stage_evaluators"]["model_parameter_count"] = json!(0); + direct_execution["compiled"]["stage_evaluators"]["real_valued_inputs"] = json!([]); + direct_execution["compiled"]["stage_evaluators"]["required_runtime_capabilities"] = + json!([ARENA, DIRECT]); + direct_execution["required_runtime_capabilities"] = json!([ARENA, DIRECT]); + let cpp_execution = minimal_execution_manifest( + "cpp", + "a b > d", + CPP, + compiled_evaluator_manifest("evaluators/fake-cpp.dylib", CPP), + ); + let asm_execution = minimal_execution_manifest( + "asm", + "a b > e", + ASM, + compiled_evaluator_manifest("evaluators/fake-asm.dylib", ASM), + ); + let execution_set = json!({ + "schema_version": PROCESS_ARTIFACT_SCHEMA_VERSION, + "kind": "pyamplicol-runtime-execution-set", + "required_runtime_capabilities": [ARENA, ASM, CPP, DIRECT], + "processes": [ + { + "process_id": "direct", + "manifest_path": "direct/execution.json", + "required_runtime_capabilities": [ARENA, DIRECT] + }, + { + "process_id": "asm", + "manifest_path": "asm/execution.json", + "required_runtime_capabilities": [ASM] + }, + { + "process_id": "cpp", + "manifest_path": "cpp/execution.json", + "required_runtime_capabilities": [CPP] + } + ] + }); + for (path, role, value, process_id) in [ + ( + "processes/evaluators.json", + "evaluator-manifest", + execution_set, + None, + ), + ( + "processes/direct/execution.json", + "evaluator-manifest", + direct_execution, + Some("direct"), + ), + ( + "processes/asm/execution.json", + "evaluator-manifest", + asm_execution, + Some("asm"), + ), + ( + "processes/cpp/execution.json", + "evaluator-manifest", + cpp_execution, + Some("cpp"), + ), + ( + "processes/direct/physics.json", + "runtime-physics", + minimal_runtime_physics("direct", "a b > c", "c"), + Some("direct"), + ), + ( + "processes/asm/physics.json", + "runtime-physics", + minimal_runtime_physics("asm", "a b > e", "e"), + Some("asm"), + ), + ( + "processes/cpp/physics.json", + "runtime-physics", + minimal_runtime_physics("cpp", "a b > d", "d"), + Some("cpp"), + ), + ] { + let bytes = serde_json::to_vec(&value).expect("serialize mixed-runtime JSON"); + add_test_payload(&mut artifact, path, role, &bytes, process_id, false); + } + add_test_payload( + &mut artifact, + "processes/direct/evaluators/direct.symjit", + "evaluator-state", + &direct_application, + Some("direct"), + true, + ); + add_test_payload( + &mut artifact, + "processes/direct/evaluators/direct.plane.symjit", + "evaluator-state", + &direct_plane_application, + Some("direct"), + true, + ); + add_test_payload( + &mut artifact, + "processes/direct/evaluators/direct.exact.symbolica", + "evaluator-state", + b"deferred exact-evaluator test state", + Some("direct"), + true, + ); + add_test_payload( + &mut artifact, + "processes/asm/evaluators/fake-asm.dylib", + "evaluator-state", + b"unselected fake ASM evaluator", + Some("asm"), + true, + ); + add_test_payload( + &mut artifact, + "processes/cpp/evaluators/fake-cpp.dylib", + "evaluator-state", + b"unselected fake C++ evaluator", + Some("cpp"), + true, + ); + artifact.write_manifest(); + artifact +} + +#[test] +fn artifact_identity_tracks_only_runtime_payload_records() { + let mut value = json!({ + "artifact_id": "0".repeat(64), + "created_utc": "2026-07-31T00:00:00Z", + "payloads": [ + { + "path": "config/effective.toml", + "role": "configuration-effective", + "media_type": "application/toml", + "size_bytes": 1, + "sha256": "1".repeat(64), + "executable": false, + }, + { + "path": "processes/p0/validation-momenta.json", + "role": "validation-momenta", + "media_type": "application/json", + "size_bytes": 1, + "sha256": "2".repeat(64), + "executable": false, + }, + { + "path": "processes/p0/physics.json", + "role": "runtime-physics", + "media_type": "application/json", + "size_bytes": 1, + "sha256": "3".repeat(64), + "executable": false, + "process_id": "p0", + }, + ], + }); + let identity = compute_artifact_id(&value).unwrap(); + + value["created_utc"] = json!("2030-01-01T00:00:00Z"); + value["payloads"][0]["sha256"] = json!("a".repeat(64)); + value["payloads"][1]["sha256"] = json!("b".repeat(64)); + assert_eq!(compute_artifact_id(&value).unwrap(), identity); + + value["payloads"][2]["sha256"] = json!("c".repeat(64)); + assert_ne!(compute_artifact_id(&value).unwrap(), identity); +} + +#[test] +fn valid_manifest_opens_trusted_payload_catalog() { + let artifact = TestArtifact::new(); + let verified = VerifiedArtifact::open(&artifact.root).expect("valid artifact"); + + assert_eq!(verified.manifest().schema_version, 3); + assert_eq!(verified.select_process(None).unwrap().requested_id, "p0"); + assert_eq!( + verified + .select_process(Some(" A b > C ")) + .expect("select process by case- and whitespace-normalized expression") + .requested_id, + "p0" + ); +} + +#[test] +fn valid_manifest_larger_than_legacy_16_mib_guard_opens() { + let mut artifact = TestArtifact::new(); + artifact.manifest["extensions"]["large_valid_metadata"] = json!("x".repeat(16 * 1024 * 1024)); + artifact.write_manifest(); + let manifest_path = artifact.root.join(ARTIFACT_MANIFEST_FILE); + assert!( + fs::metadata(&manifest_path) + .expect("large manifest metadata") + .len() + > 16 * 1024 * 1024 + ); + + let verified = VerifiedArtifact::open(&artifact.root).expect("large valid artifact manifest"); + assert_eq!( + verified + .manifest() + .extensions + .get("large_valid_metadata") + .and_then(Value::as_str) + .map(str::len), + Some(16 * 1024 * 1024) + ); +} + +#[test] +fn artifact_without_current_identity_contract_fails_closed() { + let mut artifact = TestArtifact::new(); + artifact.manifest["extensions"] + .as_object_mut() + .unwrap() + .remove(ARTIFACT_IDENTITY_EXTENSION); + artifact.write_manifest(); + + let error = VerifiedArtifact::open(&artifact.root).unwrap_err(); + assert_eq!(error.kind(), crate::RusticolErrorKind::Compatibility); + assert!(error.to_string().contains("regenerate")); +} + +#[cfg(feature = "f64-symjit")] +#[test] +fn verified_artifact_resolves_packed_evaluator_payloads() { + let mut artifact = TestArtifact::new(); + add_test_evaluator_container(&mut artifact); + + let verified = VerifiedArtifact::open(&artifact.root).expect("packed artifact"); + assert!(verified.has_evaluator_payload("a/jit.symjit").unwrap()); + assert!(verified.has_evaluator_payload("z/exact.bin").unwrap()); + assert!(!verified.has_evaluator_payload("missing.symjit").unwrap()); + + let store = verified + .evaluator_payload_store(verified.root()) + .expect("root evaluator store"); + let application = store + .source("a/jit.symjit") + .expect("packed application source"); + assert_eq!(application.read().unwrap().as_ref(), b"jit-v1"); + assert_eq!(application.display_name(), "evaluators.pacbin:a/jit.symjit"); + assert_eq!( + store + .source("z/exact.bin") + .expect("packed exact source") + .read() + .unwrap() + .as_ref(), + b"exact-state" + ); +} + +#[test] +fn evaluator_payload_store_resolves_process_relative_paths_and_rejects_escape() { + let mut artifact = TestArtifact::new(); + let declared = "processes/p0/on-the-fly-runtime.pacbin"; + add_test_payload( + &mut artifact, + declared, + "evaluator-state", + b"authenticated on-the-fly container", + Some("p0"), + true, + ); + artifact.write_manifest(); + + let verified = VerifiedArtifact::open(&artifact.root).expect("process payload artifact"); + let process_root = verified.root().join("processes/p0"); + let store = verified + .evaluator_payload_store(&process_root) + .expect("process evaluator store"); + let logical = store + .logical_path("on-the-fly-runtime.pacbin") + .expect("process-relative payload path"); + assert_eq!(logical, declared); + assert_eq!(verified.payload(&logical).unwrap().path, declared); + + let error = store.logical_path("../escape.pacbin").unwrap_err(); + assert_eq!(error.kind(), crate::RusticolErrorKind::Security); + + let missing = store + .logical_path("missing.pacbin") + .expect("confined undeclared path"); + let error = verified.payload(&missing).unwrap_err(); + assert_eq!(error.kind(), crate::RusticolErrorKind::Security); + assert!( + error.to_string().contains("not a declared payload"), + "{error}" + ); +} + +#[cfg(all(unix, feature = "f64-symjit"))] +#[test] +fn trusted_loose_evaluator_source_reads_replacement_bytes() { + let mut artifact = TestArtifact::new(); + add_test_payload( + &mut artifact, + "loose.symjit", + "evaluator-state", + b"jit-v1", + Some("p0"), + true, + ); + artifact.write_manifest(); + let verified = VerifiedArtifact::open(&artifact.root).expect("loose evaluator artifact"); + let source = verified + .evaluator_payload_store(verified.root()) + .expect("verified evaluator store") + .source("loose.symjit") + .expect("loose evaluator source"); + let replacement_path = artifact.root.join("loose.replacement"); + fs::write(&replacement_path, b"bad-v1").expect("write replacement evaluator"); + fs::rename(&replacement_path, artifact.root.join("loose.symjit")) + .expect("replace loose evaluator path"); + + assert_eq!( + source.read().expect("read trusted replacement").as_ref(), + b"bad-v1" + ); +} + +#[cfg(all(unix, feature = "f64-symjit"))] +#[test] +fn evaluator_container_replacement_after_validation_is_reauthenticated() { + let mut artifact = TestArtifact::new(); + add_test_evaluator_container(&mut artifact); + let verified = VerifiedArtifact::open(&artifact.root).expect("packed artifact"); + let container_path = artifact.root.join("evaluators.pacbin"); + let replacement_path = artifact.root.join("evaluators.replacement"); + let mut replacement = decode_hex(PYTHON_PACBIN_GOLDEN_HEX); + let payload_offset = replacement + .windows(b"jit-v1".len()) + .position(|window| window == b"jit-v1") + .expect("golden JIT member"); + replacement[payload_offset..payload_offset + b"jit-v1".len()].copy_from_slice(b"bad-v1"); + fs::write(&replacement_path, replacement).expect("write replacement PACBIN"); + fs::rename(&replacement_path, &container_path).expect("replace validated PACBIN path"); + + let error = + load_evaluator_payload_container(&verified.root, &verified.manifest, &verified.payloads) + .expect_err("replacement PACBIN bytes must not inherit prior validation"); + + assert_eq!(error.kind(), crate::RusticolErrorKind::Integrity); + assert!(error.to_string().contains("payload digest mismatch")); +} + +#[cfg(feature = "f64-symjit")] +#[test] +fn packed_evaluator_metadata_and_physical_collisions_are_rejected() { + let mut count_mismatch = TestArtifact::new(); + add_test_evaluator_container(&mut count_mismatch); + count_mismatch.manifest["extensions"]["evaluator_payload_container"]["member_count"] = json!(3); + count_mismatch.write_manifest(); + let error = VerifiedArtifact::open(&count_mismatch.root).unwrap_err(); + assert_eq!(error.kind(), crate::RusticolErrorKind::Integrity); + assert!(error.to_string().contains("metadata disagrees")); + + let mut collision = TestArtifact::new(); + add_test_evaluator_container(&mut collision); + add_test_payload( + &mut collision, + "a/jit.symjit", + "evaluator-state", + b"physical collision", + Some("p0"), + true, + ); + collision.write_manifest(); + let error = VerifiedArtifact::open(&collision.root).unwrap_err(); + assert_eq!(error.kind(), crate::RusticolErrorKind::Integrity); + assert!( + error + .to_string() + .contains("invalid packed evaluator member") + ); +} + +#[test] +fn ambiguous_process_expression_lists_stable_ids() { + let artifact = TestArtifact::new(); + let verified = VerifiedArtifact::open(&artifact.root).expect("valid artifact"); + let mut manifest = verified.manifest().clone(); + let mut duplicate = manifest.processes[0].clone(); + duplicate.id = "p1".to_string(); + manifest.processes.push(duplicate); + + let error = manifest + .select_process(Some("a b > c")) + .expect_err("duplicate expressions must be ambiguous"); + + assert_eq!(error.kind(), crate::RusticolErrorKind::Selector); + assert!(error.to_string().contains("ambiguous")); + assert!(error.to_string().contains("p0, p1")); +} + +#[test] +fn direct_runtime_defers_symbolica_serialization_abi_validation() { + let mut artifact = TestArtifact::new(); + artifact.manifest["producer"]["versions"]["symbolica_serialization"] = + json!("a-future-symbolica-serialization-abi"); + artifact.write_manifest(); + + let verified = VerifiedArtifact::open(&artifact.root) + .expect("outer artifact validation must not require an unused Symbolica serialization ABI"); + + assert_eq!( + verified + .manifest() + .producer + .versions + .symbolica_serialization, + Some("a-future-symbolica-serialization-abi".to_string()) + ); + + let mut no_symbolica_abi = TestArtifact::new(); + no_symbolica_abi.manifest["producer"]["versions"] + .as_object_mut() + .unwrap() + .remove("symbolica_serialization"); + no_symbolica_abi.write_manifest(); + + let verified = VerifiedArtifact::open(&no_symbolica_abi.root) + .expect("direct f64 artifact must not require Symbolica ABI metadata"); + assert_eq!( + verified + .manifest() + .producer + .versions + .symbolica_serialization, + None + ); +} + +#[test] +fn artifact_identity_is_trusted_during_normal_loading() { + let artifact = TestArtifact::new(); + let path = artifact.root.join(ARTIFACT_MANIFEST_FILE); + let mut manifest: Value = + serde_json::from_slice(&fs::read(&path).unwrap()).expect("parse fixture manifest"); + manifest["model"]["name"] = json!("tampered-model"); + fs::write(&path, serde_json::to_vec_pretty(&manifest).unwrap()).unwrap(); + + let opened = VerifiedArtifact::open(&artifact.root).expect("open trusted manifest"); + assert_eq!(opened.manifest().model.name, "tampered-model"); +} + +#[test] +fn unknown_nested_manifest_keys_are_rejected() { + let mut artifact = TestArtifact::new(); + artifact.manifest["producer"]["target"]["unexpected"] = json!(true); + artifact.write_manifest(); + + let error = VerifiedArtifact::open(&artifact.root).unwrap_err(); + + assert_eq!(error.kind(), crate::RusticolErrorKind::Serialization); + assert!(error.to_string().contains("unknown field")); +} + +#[test] +fn only_canonical_artifact_filename_is_accepted() { + let artifact = TestArtifact::new(); + let legacy = artifact.root.join("manifest.json"); + fs::rename(artifact.root.join(ARTIFACT_MANIFEST_FILE), &legacy) + .expect("rename canonical manifest"); + + let directory_error = VerifiedArtifact::open(&artifact.root).unwrap_err(); + let direct_error = VerifiedArtifact::open(&legacy).unwrap_err(); + + assert_eq!(directory_error.kind(), crate::RusticolErrorKind::Artifact); + assert_eq!(direct_error.kind(), crate::RusticolErrorKind::Artifact); + assert!(directory_error.to_string().contains("artifact.json")); + assert!(direct_error.to_string().contains("artifact.json")); +} + +#[test] +fn legacy_schema_reports_actionable_regeneration_error() { + let mut artifact = TestArtifact::new(); + artifact.manifest = json!({"schema_version": 2, "kind": "legacy"}); + artifact.write_manifest(); + + let error = VerifiedArtifact::open(&artifact.root).unwrap_err(); + + assert_eq!(error.kind(), crate::RusticolErrorKind::Compatibility); + assert!(error.to_string().contains("regenerate")); + assert!(error.to_string().contains("schema v3")); +} + +#[test] +fn traversal_and_duplicate_payload_paths_are_rejected() { + let mut traversal = TestArtifact::new(); + traversal.manifest["payloads"][3]["path"] = json!("../physics.json"); + traversal.write_manifest(); + let error = VerifiedArtifact::open(&traversal.root).unwrap_err(); + assert_eq!(error.kind(), crate::RusticolErrorKind::Security); + + let mut duplicate = TestArtifact::new(); + duplicate.manifest["payloads"][1]["path"] = json!("requested.toml"); + duplicate.write_manifest(); + let error = VerifiedArtifact::open(&duplicate.root).unwrap_err(); + assert_eq!(error.kind(), crate::RusticolErrorKind::Security); + assert!(error.to_string().contains("duplicate")); +} + +#[test] +fn payload_size_is_checked_when_payload_is_read() { + let artifact = TestArtifact::new(); + fs::write(artifact.root.join("physics.json"), b"tampered").unwrap(); + + let opened = VerifiedArtifact::open(&artifact.root).expect("open trusted manifest"); + let error = opened.read_payload("physics.json").unwrap_err(); + + assert_eq!(error.kind(), crate::RusticolErrorKind::Integrity); + assert!(error.to_string().contains("size")); +} + +#[test] +fn manifest_preflight_runs_without_payload_verification() { + let mut artifact = TestArtifact::new(); + artifact.manifest["processes"][0]["required_runtime_capabilities"] = + json!(["symbolica.compiled-cpp.complex-f64.v1"]); + artifact.manifest["runtime"]["required_runtime_capabilities"] = + json!(["symbolica.compiled-cpp.complex-f64.v1"]); + artifact.write_manifest(); + fs::write(artifact.root.join("physics.json"), b"tampered").unwrap(); + + let error = VerifiedArtifact::open_with_manifest_preflight(&artifact.root, |manifest| { + assert_eq!( + manifest.runtime.required_runtime_capabilities, + ["symbolica.compiled-cpp.complex-f64.v1"] + ); + Err(RusticolError::unsupported_runtime_capability( + &manifest.runtime.required_runtime_capabilities[0], + "test runtime supports direct SymJIT only", + )) + }) + .unwrap_err(); + + assert_eq!( + error.kind(), + crate::RusticolErrorKind::UnsupportedRuntimeCapability + ); +} + +#[test] +fn process_capabilities_are_strict_and_form_the_runtime_union() { + for (capabilities, message) in [ + (json!([]), "at least one"), + ( + json!([ + "symjit.application.complex-f64.v1", + "symjit.application.complex-f64.v1" + ]), + "duplicates", + ), + ( + json!([ + "symjit.application.complex-f64.v1", + "symbolica.compiled-cpp.complex-f64.v1" + ]), + "sorted", + ), + ] { + let mut artifact = TestArtifact::new(); + artifact.manifest["processes"][0]["required_runtime_capabilities"] = capabilities.clone(); + artifact.manifest["runtime"]["required_runtime_capabilities"] = capabilities; + artifact.write_manifest(); + + let error = VerifiedArtifact::open(&artifact.root).unwrap_err(); + assert_eq!(error.kind(), crate::RusticolErrorKind::Artifact); + assert!(error.to_string().contains(message), "{error}"); + } + + let mut mismatch = TestArtifact::new(); + mismatch.manifest["runtime"]["required_runtime_capabilities"] = + json!(["symbolica.compiled-cpp.complex-f64.v1"]); + mismatch.write_manifest(); + let error = VerifiedArtifact::open(&mismatch.root).unwrap_err(); + assert_eq!(error.kind(), crate::RusticolErrorKind::Artifact); + assert!(error.to_string().contains("union")); + + let mut mixed = TestArtifact::new(); + mixed.manifest["kind"] = json!("pyamplicol-process-set"); + mixed.manifest["processes"] + .as_array_mut() + .unwrap() + .push(json!({ + "id": "p1", + "expression": "a b > d", + "color_accuracy": "lc", + "external_pdgs": [1, -1, 23], + "physics_path": "physics.json", + "required_runtime_capabilities": [ + "symbolica.compiled-cpp.complex-f64.v1" + ], + "aliases": [], + })); + mixed.manifest["runtime"]["required_runtime_capabilities"] = json!([ + "symbolica.compiled-cpp.complex-f64.v1", + "symjit.application.complex-f64.v1" + ]); + let parsed: ArtifactManifest = + serde_json::from_value(mixed.manifest.clone()).expect("parse mixed manifest"); + validate_manifest(&parsed).expect("mixed process capability union validates"); +} + +#[test] +fn on_the_fly_fft_capabilities_are_known_and_the_catalog_remains_closed() { + let capabilities = json!([ + crate::engine::SYMMETRIC_GROUP_FFT_COLOR_RUNTIME_CAPABILITY, + "rusticol.on-the-fly.complex-f64.v1", + "rusticol.on-the-fly.contracted-color.v1", + "rusticol.on-the-fly.lc-color.v1" + ]); + let mut artifact = TestArtifact::new(); + artifact.manifest["processes"][0]["required_runtime_capabilities"] = capabilities.clone(); + artifact.manifest["runtime"]["required_runtime_capabilities"] = capabilities; + artifact.write_manifest(); + + VerifiedArtifact::open(&artifact.root) + .expect("on-the-fly capabilities pass outer artifact validation"); + + let unknown = json!(["rusticol.on-the-fly.unknown.v1"]); + artifact.manifest["processes"][0]["required_runtime_capabilities"] = unknown.clone(); + artifact.manifest["runtime"]["required_runtime_capabilities"] = unknown; + artifact.write_manifest(); + + let error = VerifiedArtifact::open(&artifact.root).unwrap_err(); + assert_eq!(error.kind(), crate::RusticolErrorKind::Artifact); + assert!( + error.to_string().contains("unsupported capabilities"), + "{error}" + ); +} + +#[test] +fn recurrence_capabilities_are_part_of_the_outer_artifact_catalog() { + let capabilities = json!([ + "rusticol.recurrence-color.lc.v1", + "rusticol.recurrence-direct-arena.complex-f64.v1" + ]); + let mut artifact = TestArtifact::new(); + artifact.manifest["processes"][0]["required_runtime_capabilities"] = capabilities.clone(); + artifact.manifest["runtime"]["required_runtime_capabilities"] = capabilities; + artifact.write_manifest(); + + VerifiedArtifact::open(&artifact.root) + .expect("recurrence capabilities pass outer artifact validation"); +} + +#[cfg(all( + feature = "f64-symjit", + not(feature = "f64-compiled"), + not(feature = "symbolica-runtime") +))] +#[test] +fn selected_unsupported_process_capability_preflights_before_payloads() { + let mut artifact = TestArtifact::new(); + artifact.manifest["processes"][0]["required_runtime_capabilities"] = + json!(["symbolica.compiled-cpp.complex-f64.v1"]); + artifact.manifest["runtime"]["required_runtime_capabilities"] = + json!(["symbolica.compiled-cpp.complex-f64.v1"]); + artifact.write_manifest(); + fs::write(artifact.root.join("physics.json"), b"tampered").unwrap(); + + let error = match crate::NativeRuntime::load(&artifact.root, Some("p0"), None) { + Ok(_) => panic!("unsupported selected process unexpectedly loaded"), + Err(error) => error, + }; + + assert_eq!( + error.kind(), + crate::RusticolErrorKind::UnsupportedRuntimeCapability + ); + assert!(error.to_string().contains("compiled-cpp")); +} + +#[cfg(feature = "f64-symjit")] +#[test] +fn mixed_backend_process_set_loads_selected_direct_symjit_process() { + let artifact = mixed_backend_runtime_artifact(); + + let runtime = crate::NativeRuntime::load(&artifact.root, Some("direct"), None) + .expect("selected direct-SymJIT process loads despite unselected C++/ASM processes"); + + assert_eq!(runtime.metadata().process_key, "direct"); + assert_eq!(runtime.metadata().external_pdg_order, vec![1, -1, 22]); +} + +#[cfg(all( + feature = "f64-symjit", + not(feature = "f64-compiled"), + not(feature = "symbolica-runtime") +))] +#[test] +fn mixed_backend_process_set_rejects_unsupported_selection_before_tampered_payload() { + let artifact = mixed_backend_runtime_artifact(); + fs::write( + artifact.root.join("processes/direct/physics.json"), + b"tampered after manifest creation", + ) + .unwrap(); + + for (process_id, capability) in [("asm", "compiled-asm"), ("cpp", "compiled-cpp")] { + let error = match crate::NativeRuntime::load(&artifact.root, Some(process_id), None) { + Ok(_) => panic!("unsupported selected {process_id} process unexpectedly loaded"), + Err(error) => error, + }; + + assert_eq!( + error.kind(), + crate::RusticolErrorKind::UnsupportedRuntimeCapability + ); + assert!(error.to_string().contains(capability)); + } +} + +#[test] +fn required_nullable_and_target_fields_remain_strict() { + let mut missing_nullable = TestArtifact::new(); + missing_nullable.manifest["runtime"] + .as_object_mut() + .unwrap() + .remove("api_bundle_path"); + missing_nullable.write_manifest(); + let error = VerifiedArtifact::open(&missing_nullable.root).unwrap_err(); + assert_eq!(error.kind(), crate::RusticolErrorKind::Artifact); + assert!(error.to_string().contains("api_bundle_path")); + + let mut missing_target = TestArtifact::new(); + let state = b"serialized state"; + fs::write(missing_target.root.join("state.bin"), state).unwrap(); + missing_target.manifest["payloads"] + .as_array_mut() + .unwrap() + .push(json!({ + "path": "state.bin", + "role": "evaluator-state", + "media_type": "application/octet-stream", + "size_bytes": state.len(), + "sha256": sha256(state), + "executable": false, + })); + missing_target.write_manifest(); + let error = VerifiedArtifact::open(&missing_target.root).unwrap_err(); + assert_eq!(error.kind(), crate::RusticolErrorKind::Artifact); + assert!(error.to_string().contains("target")); +} + +#[test] +fn clean_source_structural_proof_payload_roundtrips_with_exact_declaration() { + let mut valid = TestArtifact::new(); + valid.manifest["producer"]["git_revision"] = json!("a".repeat(40)); + valid.manifest["producer"]["native_build_inputs_sha256"] = json!("b".repeat(64)); + add_structural_source_proof(&mut valid); + let verified = VerifiedArtifact::open(&valid.root).expect("valid structural proof artifact"); + assert_eq!( + verified.manifest.producer.git_revision.as_deref(), + Some("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") + ); + assert_eq!( + verified + .manifest + .producer + .native_build_inputs_sha256 + .as_deref(), + Some("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb") + ); + let payload = verified + .payloads + .get("processes/p0/structural-source-proof.json") + .expect("verified structural proof payload"); + assert_eq!(payload.role, PayloadRole::StructuralSourceProof); + assert_eq!(payload.media_type, "application/json"); + assert_eq!(payload.process_id.as_deref(), Some("p0")); + assert!(payload.target.is_none()); + + for (field, value, message) in [ + ("path", json!("structural-source-proof.json"), "exact path"), + ( + "media_type", + json!("application/octet-stream"), + "media type", + ), + ("process_id", json!("missing"), "unknown process"), + ( + "target", + json!({"triple": current_target_triple(), "cpu_features": []}), + "target metadata", + ), + ] { + let mut malformed = TestArtifact::new(); + add_structural_source_proof(&mut malformed); + { + let payload = malformed.manifest["payloads"] + .as_array_mut() + .expect("payload array") + .last_mut() + .expect("structural proof payload"); + payload[field] = value; + if field == "process_id" { + payload["path"] = json!("processes/missing/structural-source-proof.json"); + } + } + if field == "process_id" { + let destination = malformed + .root + .join("processes/missing/structural-source-proof.json"); + fs::create_dir_all(destination.parent().expect("structural proof parent")) + .expect("create structural proof parent"); + fs::rename( + malformed + .root + .join("processes/p0/structural-source-proof.json"), + destination, + ) + .expect("move structural proof to its exact declaration"); + } + malformed.write_manifest(); + let error = VerifiedArtifact::open(&malformed.root).unwrap_err(); + assert!( + error.to_string().contains(message), + "{field} rejection did not mention {message:?}: {error}" + ); + } + + let mut missing_process = TestArtifact::new(); + add_structural_source_proof(&mut missing_process); + missing_process.manifest["payloads"] + .as_array_mut() + .expect("payload array") + .last_mut() + .expect("structural proof payload") + .as_object_mut() + .expect("payload object") + .remove("process_id"); + missing_process.write_manifest(); + let error = VerifiedArtifact::open(&missing_process.root).unwrap_err(); + assert!(error.to_string().contains("required process id")); + + let mut malformed_native_identity = TestArtifact::new(); + malformed_native_identity.manifest["producer"]["git_revision"] = json!("a".repeat(40)); + malformed_native_identity.manifest["producer"]["native_build_inputs_sha256"] = + json!("not-a-sha256"); + malformed_native_identity.write_manifest(); + let error = VerifiedArtifact::open(&malformed_native_identity.root).unwrap_err(); + assert!( + error + .to_string() + .contains("producer.native_build_inputs_sha256") + ); + + let mut native_identity_without_revision = TestArtifact::new(); + native_identity_without_revision.manifest["producer"]["native_build_inputs_sha256"] = + json!("b".repeat(64)); + native_identity_without_revision.write_manifest(); + let verified = VerifiedArtifact::open(&native_identity_without_revision.root) + .expect("native build-input identity is valid without a source revision"); + assert_eq!(verified.manifest.producer.git_revision, None); + assert_eq!( + verified + .manifest + .producer + .native_build_inputs_sha256 + .as_deref(), + Some("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb") + ); +} + +#[test] +fn producer_and_payload_cpu_features_are_checked() { + let mut unavailable = TestArtifact::new(); + unavailable.manifest["producer"]["target"]["cpu_features"] = + json!(["definitely-not-a-real-cpu-feature"]); + unavailable.write_manifest(); + fs::write(unavailable.root.join("physics.json"), b"tampered payload").unwrap(); + let error = VerifiedArtifact::open(&unavailable.root).unwrap_err(); + assert_eq!(error.kind(), crate::RusticolErrorKind::Compatibility); + assert!(error.to_string().contains("CPU feature")); + + let mut mismatch = TestArtifact::new(); + mismatch.manifest["payloads"][2]["target"] = json!({ + "triple": current_target_triple(), + "cpu_features": ["baseline"], + }); + mismatch.write_manifest(); + let error = VerifiedArtifact::open(&mismatch.root).unwrap_err(); + assert_eq!(error.kind(), crate::RusticolErrorKind::Compatibility); + assert!( + error + .to_string() + .contains("do not match producer CPU features") + ); +} + +#[test] +fn portable_64le_target_accepts_only_feature_free_symjit_artifacts() { + let mut portable = TestArtifact::new(); + portable.manifest["producer"]["target"] = json!({ + "triple": PORTABLE_64LE_ARTIFACT_TARGET, + "cpu_features": [], + }); + portable.write_manifest(); + VerifiedArtifact::open(&portable.root).expect("portable O2 SymJIT target"); + + let mut feature_bound = TestArtifact::new(); + feature_bound.manifest["producer"]["target"] = json!({ + "triple": PORTABLE_64LE_ARTIFACT_TARGET, + "cpu_features": ["avx2"], + }); + feature_bound.write_manifest(); + let error = VerifiedArtifact::open(&feature_bound.root).unwrap_err(); + assert_eq!(error.kind(), crate::RusticolErrorKind::Compatibility); + assert!(error.to_string().contains("must not require CPU features")); + + for capability in [ + RuntimeCapability::SymbolicaLegacyJitContainerComplexF64V1.as_str(), + RuntimeCapability::SymbolicaCompiledCppComplexF64V1.as_str(), + RuntimeCapability::SymbolicaCompiledAsmComplexF64V1.as_str(), + ] { + let mut target_specific = TestArtifact::new(); + target_specific.manifest["producer"]["target"] = json!({ + "triple": PORTABLE_64LE_ARTIFACT_TARGET, + "cpu_features": [], + }); + target_specific.manifest["runtime"]["required_runtime_capabilities"] = json!([capability]); + target_specific.manifest["processes"][0]["required_runtime_capabilities"] = + json!([capability]); + target_specific.write_manifest(); + let error = VerifiedArtifact::open(&target_specific.root).unwrap_err(); + assert_eq!(error.kind(), crate::RusticolErrorKind::Compatibility); + assert!(error.to_string().contains("remain target-specific")); + } + + for capability in [ + RuntimeCapability::EagerDirectArenaV1.as_str(), + RuntimeCapability::RecurrenceRuntimeComplexF64V1.as_str(), + ] { + let mut direct = TestArtifact::new(); + direct.manifest["producer"]["target"] = json!({ + "triple": PORTABLE_64LE_ARTIFACT_TARGET, + "cpu_features": [], + }); + direct.manifest["runtime"]["required_runtime_capabilities"] = json!([capability]); + direct.manifest["processes"][0]["required_runtime_capabilities"] = json!([capability]); + direct.write_manifest(); + VerifiedArtifact::open(&direct.root) + .expect("portable eager/recurrence outer capability is pack-authenticated later"); + } +} + +#[cfg(feature = "f64-symjit")] +#[test] +fn packaged_portable_64le_o2_artifact_loads_through_native_runtime() { + let source = PathBuf::from(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../../../src/pyamplicol/assets/selftest/portable-64le/artifact" + )); + let mut staged = TestArtifact::new(); + fs::remove_dir_all(&staged.root).expect("remove synthetic test artifact"); + copy_test_artifact_tree(&source, &staged.root); + staged.manifest = serde_json::from_slice( + &fs::read(staged.root.join(ARTIFACT_MANIFEST_FILE)).expect("read portable manifest"), + ) + .expect("parse portable manifest"); + let source_artifact_id = staged.manifest["artifact_id"].clone(); + staged.manifest["producer"]["version"] = json!(env!("CARGO_PKG_VERSION")); + staged.manifest["runtime"]["engine_version"] = json!(env!("CARGO_PKG_VERSION")); + staged.write_manifest(); + assert_eq!(staged.manifest["artifact_id"], source_artifact_id); + + let artifact = + VerifiedArtifact::open(&staged.root).expect("open staged portable source artifact"); + assert_eq!( + artifact.manifest().producer.target.triple, + PORTABLE_64LE_ARTIFACT_TARGET + ); + + let runtime = crate::NativeRuntime::load(&staged.root, Some("d_dbar_to_z"), None) + .expect("load portable O2 artifact through the authoritative runtime"); + assert_eq!(runtime.metadata().execution_mode, "compiled"); +} + +#[cfg(feature = "f64-symjit")] +fn copy_test_artifact_tree(source: &Path, destination: &Path) { + fs::create_dir(destination).expect("create staged portable artifact"); + for entry in fs::read_dir(source).expect("read portable artifact tree") { + let entry = entry.expect("read portable artifact entry"); + let source_path = entry.path(); + let destination_path = destination.join(entry.file_name()); + let file_type = entry.file_type().expect("read portable artifact file type"); + if file_type.is_dir() { + copy_test_artifact_tree(&source_path, &destination_path); + } else { + assert!(file_type.is_file(), "portable artifact contains a symlink"); + fs::copy(&source_path, &destination_path).expect("copy portable artifact payload"); + } + } +} + +#[test] +fn target_metadata_requires_canonical_sorted_cpu_features() { + let target = runtime_target_info(); + assert_eq!(target.triple, current_target_triple()); + assert!(target.cpu_features.windows(2).all(|pair| pair[0] < pair[1])); + validate_target(&target, "runtime target").expect("runtime target validates"); + + let mut unsorted = TestArtifact::new(); + unsorted.manifest["producer"]["target"]["cpu_features"] = json!(["z-feature", "a-feature"]); + unsorted.write_manifest(); + let error = VerifiedArtifact::open(&unsorted.root).unwrap_err(); + assert_eq!(error.kind(), crate::RusticolErrorKind::Artifact); + assert!(error.to_string().contains("sorted and unique")); + + let mut noncanonical = TestArtifact::new(); + noncanonical.manifest["producer"]["target"]["cpu_features"] = json!(["+avx2"]); + noncanonical.write_manifest(); + let error = VerifiedArtifact::open(&noncanonical.root).unwrap_err(); + assert_eq!(error.kind(), crate::RusticolErrorKind::Artifact); + assert!(error.to_string().contains("canonical feature ID")); +} + +#[test] +fn malformed_timestamp_and_crossing_alias_are_rejected() { + let mut timestamp = TestArtifact::new(); + timestamp.manifest["created_utc"] = json!("2026-02-30T25:00:00Z"); + timestamp.write_manifest(); + let error = VerifiedArtifact::open(×tamp.root).unwrap_err(); + assert_eq!(error.kind(), crate::RusticolErrorKind::Artifact); + + let mut alias = TestArtifact::new(); + alias.manifest["processes"][0]["aliases"] = json!([{ + "id": "alias0", + "expression": "b a > c", + "external_pdgs": [-1, 1, 22], + "external_permutation": [1, 0, 2], + }]); + alias.write_manifest(); + VerifiedArtifact::open(&alias.root) + .expect("incoming particles may be permuted within their side"); + + alias.manifest["processes"][0]["aliases"] = json!([{ + "id": "alias0", + "expression": "a c > b", + "external_pdgs": [1, 22, -1], + "external_permutation": [0, 2, 1], + }]); + alias.write_manifest(); + let error = VerifiedArtifact::open(&alias.root).unwrap_err(); + assert_eq!(error.kind(), crate::RusticolErrorKind::Artifact); + assert!(error.to_string().contains("incoming and outgoing sides")); +} + +#[test] +fn concrete_process_expression_infers_a_deterministic_side_preserving_permutation() { + let mut artifact = TestArtifact::new(); + artifact.manifest["processes"][0]["expression"] = json!("d d~ > z g g"); + artifact.manifest["processes"][0]["external_pdgs"] = json!([1, -1, 23, 21, 21]); + artifact.write_manifest(); + + let verified = VerifiedArtifact::open(&artifact.root).unwrap(); + let selection = verified + .select_process(Some(" d~ d > g z g ")) + .expect("unique permutation-equivalent expression"); + assert_eq!(selection.requested_id, "p0"); + assert!(selection.alias.is_none()); + assert!(selection.inferred_permutation); + assert_eq!(selection.public_expression, "d~ d > g z g"); + assert_eq!(selection.external_permutation, [1, 0, 3, 2, 4]); + assert_eq!(selection.external_pdgs, [-1, 1, 21, 23, 21]); + + let crossing = verified.select_process(Some("d z > d~ g g")).unwrap_err(); + assert_eq!(crossing.kind(), crate::RusticolErrorKind::Selector); + assert!(crossing.to_string().contains("across the '>' boundary")); +} + +#[test] +fn inferred_expression_prefers_the_least_reordered_representative() { + let artifact = TestArtifact::new(); + let mut manifest: ArtifactManifest = serde_json::from_value(artifact.manifest.clone()).unwrap(); + manifest.processes[0].expression = "d d~ > z g g".to_string(); + manifest.processes[0].external_pdgs = vec![1, -1, 23, 21, 21]; + let mut reversed = manifest.processes[0].clone(); + reversed.id = "p1".to_string(); + reversed.expression = "d~ d > z g g".to_string(); + reversed.external_pdgs = vec![-1, 1, 23, 21, 21]; + manifest.processes.push(reversed); + + let forward = manifest.select_process(Some("d d~ > g z g")).unwrap(); + assert_eq!(forward.process.id, "p0"); + assert_eq!(forward.external_permutation, [0, 1, 3, 2, 4]); + + let reversed = manifest.select_process(Some("d~ d > g z g")).unwrap(); + assert_eq!(reversed.process.id, "p1"); + assert_eq!(reversed.external_permutation, [0, 1, 3, 2, 4]); +} + +#[test] +fn exact_representative_expression_precedes_an_equal_alias_expression() { + let mut artifact = TestArtifact::new(); + artifact.manifest["processes"][0]["aliases"] = json!([{ + "id": "same-expression-alias", + "expression": "a b > c", + "external_pdgs": [1, -1, 22], + "external_permutation": [0, 1, 2], + }]); + artifact.write_manifest(); + + let verified = VerifiedArtifact::open(&artifact.root).unwrap(); + let selection = verified.select_process(Some("a b > c")).unwrap(); + + assert_eq!(selection.requested_id, "p0"); + assert!(selection.alias.is_none()); +} + +#[test] +fn inferred_process_expression_fails_on_multiple_representatives() { + let artifact = TestArtifact::new(); + let mut manifest: ArtifactManifest = serde_json::from_value(artifact.manifest.clone()).unwrap(); + manifest.processes[0].expression = "d d~ > z g g".to_string(); + manifest.processes[0].external_pdgs = vec![1, -1, 23, 21, 21]; + let mut duplicate = manifest.processes[0].clone(); + duplicate.id = "p1".to_string(); + manifest.processes.push(duplicate); + + let error = manifest.select_process(Some("d d~ > g z g")).unwrap_err(); + assert_eq!(error.kind(), crate::RusticolErrorKind::Selector); + assert!(error.to_string().contains("permutation-ambiguous")); + assert!(error.to_string().contains("p0, p1")); +} + +#[test] +fn non_self_inverse_alias_pdg_order_is_verified() { + let mut valid = TestArtifact::new(); + valid.manifest["processes"][0]["expression"] = json!("d d~ > z g a"); + valid.manifest["processes"][0]["external_pdgs"] = json!([1, -1, 23, 21, 22]); + valid.manifest["processes"][0]["aliases"] = json!([{ + "id": "cycled", + "expression": "d d~ > a z g", + "external_pdgs": [1, -1, 22, 23, 21], + "external_permutation": [0, 1, 3, 4, 2], + }]); + valid.write_manifest(); + + let verified = VerifiedArtifact::open(&valid.root).expect("valid three-cycle alias"); + let selected = verified + .select_process(Some("cycled")) + .expect("select three-cycle alias"); + assert_eq!( + selected.process.required_runtime_capabilities, + ["symjit.application.complex-f64.v1"] + ); + assert_eq!( + selected.alias.expect("alias metadata").external_pdgs, + vec![1, -1, 22, 23, 21] + ); + let selected_by_expression = verified + .select_process(Some("d d~ > a z g")) + .expect("select three-cycle alias by expression"); + assert_eq!(selected_by_expression.requested_id, "cycled"); + assert_eq!( + selected_by_expression + .alias + .expect("alias metadata selected by expression") + .external_pdgs, + vec![1, -1, 22, 23, 21] + ); + + let mut inconsistent = TestArtifact::new(); + inconsistent.manifest["processes"][0]["expression"] = json!("d d~ > z g a"); + inconsistent.manifest["processes"][0]["external_pdgs"] = json!([1, -1, 23, 21, 22]); + inconsistent.manifest["processes"][0]["aliases"] = json!([{ + "id": "cycled", + "expression": "d d~ > a z g", + "external_pdgs": [1, -1, 21, 23, 22], + "external_permutation": [0, 1, 3, 4, 2], + }]); + inconsistent.write_manifest(); + + let error = VerifiedArtifact::open(&inconsistent.root).unwrap_err(); + assert_eq!(error.kind(), crate::RusticolErrorKind::Artifact); + assert!( + error + .to_string() + .contains("does not match external_permutation") + ); +} + +#[cfg(unix)] +#[test] +fn referenced_symlink_payload_is_rejected_when_read() { + use std::os::unix::fs::symlink; + + let artifact = TestArtifact::new(); + fs::rename( + artifact.root.join("physics.json"), + artifact.root.join("physics-target.json"), + ) + .unwrap(); + symlink("physics-target.json", artifact.root.join("physics.json")).unwrap(); + + let opened = VerifiedArtifact::open(&artifact.root).expect("open trusted manifest"); + let error = opened.read_payload("physics.json").unwrap_err(); + + assert_eq!(error.kind(), crate::RusticolErrorKind::Security); + assert!(error.to_string().contains("symlink")); +} + +#[cfg(unix)] +#[test] +fn undeclared_tree_entries_are_ignored_for_trusted_artifacts() { + use std::os::unix::fs::{PermissionsExt, symlink}; + + let symlink_artifact = TestArtifact::new(); + fs::create_dir(symlink_artifact.root.join("extra")).unwrap(); + symlink( + "../physics.json", + symlink_artifact.root.join("extra/undeclared-link"), + ) + .unwrap(); + VerifiedArtifact::open(&symlink_artifact.root).expect("ignore undeclared symlink"); + + let executable_artifact = TestArtifact::new(); + let executable = executable_artifact.root.join("undeclared-tool"); + fs::write(&executable, b"#!/bin/sh\n").unwrap(); + let mut permissions = fs::metadata(&executable).unwrap().permissions(); + permissions.set_mode(0o755); + fs::set_permissions(&executable, permissions).unwrap(); + VerifiedArtifact::open(&executable_artifact.root).expect("ignore undeclared executable"); +} + +fn process_group_member( + process_id: &str, + process: &str, + pdgs: Value, + permutation: Value, + factor: (i64, i64), + conjugated: bool, +) -> Value { + json!({ + "process_id": process_id, + "process": process, + "pdgs": pdgs, + "initial_count": 2, + "factor": {"numerator": factor.0, "denominator": factor.1}, + "permutation": permutation, + "conjugated": conjugated, + "basis": "flavor-blind", + "generated": false, + }) +} + +fn process_groups_document(members: Vec) -> Value { + let mut all_members = vec![json!({ + "process_id": "p0", + "process": "d d~ > a g", + "pdgs": [1, -1, 22, 21], + "initial_count": 2, + "factor": {"numerator": 1, "denominator": 1}, + "permutation": [0, 1, 2, 3], + "conjugated": false, + "basis": "identity", + "generated": true, + })]; + all_members.extend(members); + json!({ + "kind": "pyamplicol-process-groups", + "schema_version": 1, + "analyses": [{"kind": "future-analysis", "anything": [1, 2]}], + "groups": [{ + "representative": {"process_id": "p0", "process": "d d~ > a g"}, + "members": all_members, + }], + }) +} + +fn process_group_artifact(document: &Value) -> TestArtifact { + let mut artifact = TestArtifact::new(); + artifact.manifest["processes"][0]["expression"] = json!("d d~ > a g"); + artifact.manifest["processes"][0]["external_pdgs"] = json!([1, -1, 22, 21]); + let bytes = serde_json::to_vec_pretty(document).expect("serialize process groups"); + add_test_payload( + &mut artifact, + "process-groups.json", + "process-groups", + &bytes, + None, + false, + ); + artifact.write_manifest(); + artifact +} + +fn default_process_groups() -> Value { + process_groups_document(vec![ + process_group_member( + "u_ubar_flipped", + "u~ u > a g", + json!([-2, 2, 22, 21]), + json!([1, 0, 2, 3]), + (8, 2), + false, + ), + json!({ + "process_id": "s_sbar_conjugate", + "process": "s s~ > a g", + "pdgs": [3, -3, 22, 21], + "initial_count": 2, + "factor": {"numerator": 1, "denominator": 1}, + "permutation": [1, 0, 2, 3], + "conjugated": true, + "basis": "numerical", + "generated": false, + "warning": "assumed, not verified", + "future_key": {"ignored": true}, + }), + ]) +} + +#[test] +fn process_group_members_route_to_their_representative() { + let artifact = process_group_artifact(&default_process_groups()); + let verified = VerifiedArtifact::open(&artifact.root).unwrap(); + + let by_id = verified.select_process(Some("u_ubar_flipped")).unwrap(); + assert_eq!(by_id.process.id, "p0"); + assert_eq!(by_id.requested_id, "u_ubar_flipped"); + assert!(by_id.alias.is_none()); + assert!(!by_id.inferred_permutation); + assert!(by_id.remaps_public_process()); + assert_eq!(by_id.public_expression, "u~ u > a g"); + assert_eq!(by_id.external_pdgs, [-2, 2, 22, 21]); + assert_eq!(by_id.external_permutation, [1, 0, 2, 3]); + let relation = by_id + .process_group + .as_ref() + .expect("process-group relation"); + assert_eq!( + (relation.factor_numerator, relation.factor_denominator), + (4, 1) + ); + assert!(!relation.conjugated); + assert!(!relation.has_unit_factor()); + assert_eq!(relation.external_particles, ["u~", "u", "a", "g"]); + + let by_expression = verified.select_process(Some(" U~ u > A g ")).unwrap(); + assert_eq!(by_expression.requested_id, "u_ubar_flipped"); + assert_eq!(by_expression.external_permutation, [1, 0, 2, 3]); + assert!(!by_expression.inferred_permutation); + + // Requested public order <- member order <- representative order. + let permuted = verified.select_process(Some("u~ u > g a")).unwrap(); + assert_eq!(permuted.requested_id, "u_ubar_flipped"); + assert!(permuted.inferred_permutation); + assert_eq!(permuted.public_expression, "u~ u > g a"); + assert_eq!(permuted.external_pdgs, [-2, 2, 21, 22]); + assert_eq!(permuted.external_permutation, [1, 0, 3, 2]); + assert_eq!( + permuted.process_group.expect("relation").external_particles, + ["u~", "u", "g", "a"] + ); + + let swapped = verified.select_process(Some("u u~ > a g")).unwrap(); + assert_eq!(swapped.external_permutation, [0, 1, 2, 3]); + assert_eq!(swapped.external_pdgs, [2, -2, 22, 21]); + + let conjugate = verified.select_process(Some("s~ s > g a")).unwrap(); + assert_eq!(conjugate.requested_id, "s_sbar_conjugate"); + assert_eq!(conjugate.external_permutation, [0, 1, 3, 2]); + assert_eq!(conjugate.external_pdgs, [-3, 3, 21, 22]); + let relation = conjugate.process_group.expect("conjugate relation"); + assert!(relation.conjugated); + assert!(relation.has_unit_factor()); + + // Stored processes keep precedence and never carry a relation. + let stored = verified.select_process(Some("d~ d > g a")).unwrap(); + assert_eq!(stored.requested_id, "p0"); + assert!(stored.process_group.is_none()); + assert_eq!(stored.external_permutation, [1, 0, 3, 2]); + + let unknown = verified.select_process(Some("c c~ > a g")).unwrap_err(); + assert_eq!(unknown.kind(), crate::RusticolErrorKind::Selector); + let crossing = verified.select_process(Some("u~ a > u g")).unwrap_err(); + assert_eq!(crossing.kind(), crate::RusticolErrorKind::Selector); +} + +#[test] +fn process_group_member_permutation_composes_through_member_order() { + // Three final-state legs make side permutations non-commuting, so only + // `public[member[r]]` puts the representative photon on the requested one. + let mut document = process_groups_document(vec![process_group_member( + "u_ubar_member", + "u~ u > g a g", + json!([-2, 2, 21, 22, 21]), + json!([1, 0, 3, 2, 4]), + (4, 1), + false, + )]); + document["groups"][0]["representative"]["process"] = json!("d d~ > a g g"); + let representative = &mut document["groups"][0]["members"][0]; + representative["process"] = json!("d d~ > a g g"); + representative["pdgs"] = json!([1, -1, 22, 21, 21]); + representative["permutation"] = json!([0, 1, 2, 3, 4]); + let mut artifact = TestArtifact::new(); + artifact.manifest["processes"][0]["expression"] = json!("d d~ > a g g"); + artifact.manifest["processes"][0]["external_pdgs"] = json!([1, -1, 22, 21, 21]); + let bytes = serde_json::to_vec_pretty(&document).expect("serialize process groups"); + add_test_payload( + &mut artifact, + "process-groups.json", + "process-groups", + &bytes, + None, + false, + ); + artifact.write_manifest(); + let verified = VerifiedArtifact::open(&artifact.root).unwrap(); + + let selection = verified.select_process(Some("u u~ > g g a")).unwrap(); + assert_eq!(selection.requested_id, "u_ubar_member"); + assert_eq!(selection.external_pdgs, [2, -2, 21, 21, 22]); + // Representative legs d, d~, a, g, g sit at requested u, u~, a, g, g. + assert_eq!(selection.external_permutation, [0, 1, 4, 2, 3]); +} + +#[test] +fn process_group_map_is_read_only_after_stored_selection_fails() { + let mut artifact = process_group_artifact(&default_process_groups()); + fs::write( + artifact.root.join("process-groups.json"), + b"{\"tampered\": true}", + ) + .expect("tamper process groups"); + artifact.write_manifest(); + let verified = VerifiedArtifact::open(&artifact.root).unwrap(); + + assert_eq!( + verified.select_process(Some("p0")).unwrap().requested_id, + "p0" + ); + assert_eq!( + verified + .select_process(Some("d d~ > g a")) + .unwrap() + .requested_id, + "p0" + ); + assert_eq!(verified.select_process(None).unwrap().requested_id, "p0"); + let error = verified.select_process(Some("u_ubar_flipped")).unwrap_err(); + assert!(matches!( + error.kind(), + crate::RusticolErrorKind::Integrity | crate::RusticolErrorKind::Artifact + )); +} + +#[test] +fn process_group_payload_digest_is_verified() { + let artifact = process_group_artifact(&default_process_groups()); + let mut document = default_process_groups(); + document["groups"][0]["members"][1]["factor"]["numerator"] = json!(9); + let mut bytes = serde_json::to_vec_pretty(&document).unwrap(); + let declared = fs::metadata(artifact.root.join("process-groups.json")) + .unwrap() + .len() as usize; + bytes.resize(declared, b' '); + fs::write(artifact.root.join("process-groups.json"), &bytes).unwrap(); + + let verified = VerifiedArtifact::open(&artifact.root).unwrap(); + let error = verified.select_process(Some("u_ubar_flipped")).unwrap_err(); + assert_eq!(error.kind(), crate::RusticolErrorKind::Integrity); + assert!(error.to_string().contains("sha256")); +} + +#[test] +fn invalid_process_group_maps_are_rejected() { + let member = |permutation: Value, factor: (i64, i64)| { + process_groups_document(vec![process_group_member( + "u_ubar", + "u u~ > a g", + json!([2, -2, 22, 21]), + permutation, + factor, + false, + )]) + }; + let mut wrong_representative = member(json!([0, 1, 2, 3]), (1, 1)); + wrong_representative["groups"][0]["representative"]["process_id"] = json!("missing"); + let mut representative_not_first = member(json!([0, 1, 2, 3]), (1, 1)); + representative_not_first["groups"][0]["members"] + .as_array_mut() + .unwrap() + .reverse(); + let mut reused_id = member(json!([0, 1, 2, 3]), (1, 1)); + reused_id["groups"][0]["members"][1]["process_id"] = json!("p0"); + let mut wrong_kind = member(json!([0, 1, 2, 3]), (1, 1)); + wrong_kind["kind"] = json!("something-else"); + let mut wrong_count = member(json!([0, 1, 2, 3]), (1, 1)); + wrong_count["groups"][0]["members"][1]["process"] = json!("u > u~ a g"); + let cases = [ + (member(json!([2, 1, 0, 3]), (1, 1)), "'>' boundary"), + (member(json!([0, 0, 2, 3]), (1, 1)), "complete zero-based"), + (member(json!([0, 1, 2]), (1, 1)), "has length 3"), + (member(json!([0, 1, 2, 3]), (1, 0)), "positive rational"), + (member(json!([0, 1, 2, 3]), (-1, 2)), "positive rational"), + (wrong_representative, "not a stored process"), + (representative_not_first, "identity relation"), + (reused_id, "listed more than once"), + (wrong_kind, "unsupported process-groups payload"), + (wrong_count, "particle counts"), + ]; + for (document, expected) in cases { + let artifact = process_group_artifact(&document); + let verified = VerifiedArtifact::open(&artifact.root).unwrap(); + let error = verified.select_process(Some("u_ubar")).unwrap_err(); + assert!( + error.to_string().contains(expected), + "{expected:?} not in {error}" + ); + } + + let mut collision = TestArtifact::new(); + collision.manifest["processes"][0]["expression"] = json!("d d~ > a g"); + collision.manifest["processes"][0]["external_pdgs"] = json!([1, -1, 22, 21]); + collision.manifest["processes"][0]["aliases"] = json!([{ + "id": "u_ubar", + "expression": "d~ d > a g", + "external_pdgs": [-1, 1, 22, 21], + "external_permutation": [1, 0, 2, 3], + }]); + let bytes = serde_json::to_vec(&member(json!([0, 1, 2, 3]), (1, 1))).unwrap(); + add_test_payload( + &mut collision, + "process-groups.json", + "process-groups", + &bytes, + None, + false, + ); + collision.write_manifest(); + let verified = VerifiedArtifact::open(&collision.root).unwrap(); + let error = verified.select_process(Some("u u~ > a g")).unwrap_err(); + assert!( + error + .to_string() + .contains("reuses a stored process or alias id") + ); +} + +#[test] +fn ambiguous_process_group_member_expressions_list_member_ids() { + let document = process_groups_document(vec![ + process_group_member( + "first", + "u u~ > a g", + json!([2, -2, 22, 21]), + json!([0, 1, 2, 3]), + (4, 1), + false, + ), + process_group_member( + "second", + "u u~ > a g", + json!([2, -2, 22, 21]), + json!([0, 1, 2, 3]), + (4, 1), + false, + ), + ]); + let artifact = process_group_artifact(&document); + let verified = VerifiedArtifact::open(&artifact.root).unwrap(); + let error = verified.select_process(Some("u u~ > a g")).unwrap_err(); + assert_eq!(error.kind(), crate::RusticolErrorKind::Selector); + assert!(error.to_string().contains("first, second")); + assert_eq!( + verified + .select_process(Some("second")) + .unwrap() + .requested_id, + "second" + ); +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/direct_arena/abi.rs b/pyamplicol/rust/crates/rusticol-core/src/direct_arena/abi.rs new file mode 100644 index 0000000000..7c28f854ad --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/direct_arena/abi.rs @@ -0,0 +1,731 @@ +// SPDX-License-Identifier: 0BSD + +//! ABI-stable lane-neutral Direct-Arena views. + +use std::mem::{align_of, size_of}; +use std::ops::Range; + +use crate::{RusticolError, RusticolResult}; + +use super::DIRECT_ARENA_ALIGNMENT; + +fn invalid(message: impl Into) -> RusticolError { + RusticolError::invalid_argument(message) +} + +/// Shape of one component-major, point-contiguous plane bundle. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct DirectPlaneShape { + scalar_len: u64, + point_stride: u32, +} + +impl DirectPlaneShape { + pub fn new(scalar_len: u64, point_stride: u32, label: &str) -> RusticolResult { + if point_stride == 0 { + return Err(invalid(format!( + "direct {label} point stride must be positive" + ))); + } + if !scalar_len.is_multiple_of(u64::from(point_stride)) { + return Err(invalid(format!( + "direct {label} scalar length {scalar_len} is not a whole number of \ + point-contiguous planes with stride {point_stride}" + ))); + } + Ok(Self { + scalar_len, + point_stride, + }) + } + + pub const fn scalar_len(self) -> u64 { + self.scalar_len + } + + pub const fn point_stride(self) -> u32 { + self.point_stride + } + + pub fn component_count(self) -> RusticolResult { + u32::try_from(self.scalar_len / u64::from(self.point_stride)) + .map_err(|_| invalid("direct plane component count exceeds u32")) + } + + pub fn checked_scalar_index( + self, + component: u32, + point: u32, + label: &str, + ) -> RusticolResult { + if point >= self.point_stride { + return Err(invalid(format!( + "direct {label} point {point} exceeds stride {}", + self.point_stride + ))); + } + if component >= self.component_count()? { + return Err(invalid(format!( + "direct {label} component {component} is out of bounds" + ))); + } + let index = u64::from(component) + .checked_mul(u64::from(self.point_stride)) + .and_then(|base| base.checked_add(u64::from(point))) + .ok_or_else(|| invalid(format!("direct {label} scalar index overflows u64")))?; + usize::try_from(index) + .map_err(|_| invalid(format!("direct {label} scalar index exceeds usize"))) + } + + pub fn checked_component_range( + self, + component_base: u32, + component_count: u32, + label: &str, + ) -> RusticolResult> { + let component_stop = component_base + .checked_add(component_count) + .ok_or_else(|| invalid(format!("direct {label} component range overflows u32")))?; + if component_stop > self.component_count()? { + return Err(invalid(format!( + "direct {label} component range {component_base}..{component_stop} is out of bounds" + ))); + } + let start = u64::from(component_base) + .checked_mul(u64::from(self.point_stride)) + .ok_or_else(|| invalid(format!("direct {label} scalar range overflows u64")))?; + let stop = u64::from(component_stop) + .checked_mul(u64::from(self.point_stride)) + .ok_or_else(|| invalid(format!("direct {label} scalar range overflows u64")))?; + Ok(usize::try_from(start) + .map_err(|_| invalid(format!("direct {label} scalar range exceeds usize")))? + ..usize::try_from(stop) + .map_err(|_| invalid(format!("direct {label} scalar range exceeds usize")))?) + } +} + +#[repr(C)] +#[derive(Clone, Copy, Debug)] +pub struct DirectArenaView { + pub current_re: *mut f64, + pub current_im: *mut f64, + pub current_scalar_len: u64, + pub amplitude_re: *mut f64, + pub amplitude_im: *mut f64, + pub amplitude_scalar_len: u64, + /// Physical component-plane pitch, not the semantic active point count. + pub point_stride: u32, +} + +impl DirectArenaView { + pub fn validate(self) -> RusticolResult<()> { + let _ = self.current_shape()?.component_count()?; + let _ = self.amplitude_shape()?.component_count()?; + if self.current_scalar_len != 0 || self.amplitude_scalar_len != 0 { + require_plane_stride_alignment(self.point_stride, "arena")?; + } + require_split_pair( + self.current_re, + self.current_im, + self.current_scalar_len, + "current", + )?; + require_split_pair( + self.amplitude_re, + self.amplitude_im, + self.amplitude_scalar_len, + "amplitude", + )?; + require_arena_alignment( + self.current_re.cast_const(), + self.current_scalar_len, + "current real", + )?; + require_arena_alignment( + self.current_im.cast_const(), + self.current_scalar_len, + "current imaginary", + )?; + require_arena_alignment( + self.amplitude_re.cast_const(), + self.amplitude_scalar_len, + "amplitude real", + )?; + require_arena_alignment( + self.amplitude_im.cast_const(), + self.amplitude_scalar_len, + "amplitude imaginary", + )?; + require_disjoint_ranges(&self.mutable_ranges()?)?; + Ok(()) + } + + pub fn current_shape(self) -> RusticolResult { + DirectPlaneShape::new(self.current_scalar_len, self.point_stride, "current arena") + } + + pub fn amplitude_shape(self) -> RusticolResult { + DirectPlaneShape::new( + self.amplitude_scalar_len, + self.point_stride, + "amplitude arena", + ) + } + + fn mutable_ranges(self) -> RusticolResult<[Option; 4]> { + Ok([ + checked_pointer_range( + self.current_re.cast_const(), + self.current_scalar_len, + "current real", + )?, + checked_pointer_range( + self.current_im.cast_const(), + self.current_scalar_len, + "current imaginary", + )?, + checked_pointer_range( + self.amplitude_re.cast_const(), + self.amplitude_scalar_len, + "amplitude real", + )?, + checked_pointer_range( + self.amplitude_im.cast_const(), + self.amplitude_scalar_len, + "amplitude imaginary", + )?, + ]) + } +} + +#[repr(C)] +#[derive(Clone, Copy, Debug)] +pub struct DirectMomentumView { + pub values: *const f64, + pub scalar_len: u64, + pub form_count: u32, + pub lorentz_component_count: u16, + /// Physical component-plane pitch, shared with the arena view. + pub point_stride: u32, +} + +impl DirectMomentumView { + pub fn validate(self) -> RusticolResult<()> { + if self.form_count == 0 || self.lorentz_component_count == 0 { + return Err(invalid("direct momentum view has an empty dimension")); + } + let required = u64::from(self.form_count) + .checked_mul(u64::from(self.lorentz_component_count)) + .and_then(|planes| planes.checked_mul(u64::from(self.point_stride))) + .ok_or_else(|| invalid("direct momentum view dimensions overflow u64"))?; + let shape = DirectPlaneShape::new(self.scalar_len, self.point_stride, "momentum")?; + require_plane_stride_alignment(self.point_stride, "momentum")?; + if required != self.scalar_len { + return Err(invalid(format!( + "direct momentum view requires exactly {required} scalars but exposes {}", + self.scalar_len + ))); + } + if self.values.is_null() { + return Err(invalid("direct momentum view has a null base")); + } + require_arena_alignment(self.values, self.scalar_len, "momentum")?; + if shape.component_count()? + != self + .form_count + .checked_mul(u32::from(self.lorentz_component_count)) + .ok_or_else(|| invalid("direct momentum plane count overflows u32"))? + { + return Err(invalid("direct momentum plane shape is inconsistent")); + } + let _ = checked_pointer_range(self.values, self.scalar_len, "momentum")?; + Ok(()) + } +} + +#[repr(C)] +#[derive(Clone, Copy, Debug)] +pub struct DirectParameterView { + pub values_re: *const f64, + pub values_im: *const f64, + pub value_count: u32, +} + +impl DirectParameterView { + pub fn validate(self) -> RusticolResult<()> { + require_split_pair( + self.values_re.cast_mut(), + self.values_im.cast_mut(), + u64::from(self.value_count), + "parameter", + )?; + require_disjoint_ranges(&[ + checked_pointer_range( + self.values_re, + u64::from(self.value_count), + "parameter real", + )?, + checked_pointer_range( + self.values_im, + u64::from(self.value_count), + "parameter imaginary", + )?, + ]) + } +} + +#[repr(C)] +#[derive(Clone, Copy, Debug)] +pub struct DirectFactorView { + pub values_re: *const f64, + pub values_im: *const f64, + pub value_count: u32, +} + +impl DirectFactorView { + pub fn validate(self) -> RusticolResult<()> { + require_split_pair( + self.values_re.cast_mut(), + self.values_im.cast_mut(), + u64::from(self.value_count), + "factor", + )?; + require_disjoint_ranges(&[ + checked_pointer_range(self.values_re, u64::from(self.value_count), "factor real")?, + checked_pointer_range( + self.values_im, + u64::from(self.value_count), + "factor imaginary", + )?, + ]) + } +} + +/// Validate one complete direct-call descriptor bundle. +/// +/// Read-only inputs may alias each other, but none may overlap a mutable arena. +pub fn validate_direct_views( + arena: DirectArenaView, + momenta: DirectMomentumView, + parameters: DirectParameterView, + factors: DirectFactorView, +) -> RusticolResult<()> { + arena.validate()?; + momenta.validate()?; + parameters.validate()?; + factors.validate()?; + if arena.point_stride != momenta.point_stride { + return Err(invalid(format!( + "direct arena pitch {} does not match momentum pitch {}", + arena.point_stride, momenta.point_stride + ))); + } + + let mutable_ranges = arena.mutable_ranges()?; + let read_only_ranges = [ + checked_pointer_range(momenta.values, momenta.scalar_len, "momentum")?, + checked_pointer_range( + parameters.values_re, + u64::from(parameters.value_count), + "parameter real", + )?, + checked_pointer_range( + parameters.values_im, + u64::from(parameters.value_count), + "parameter imaginary", + )?, + checked_pointer_range( + factors.values_re, + u64::from(factors.value_count), + "factor real", + )?, + checked_pointer_range( + factors.values_im, + u64::from(factors.value_count), + "factor imaginary", + )?, + ]; + for mutable in mutable_ranges.iter().flatten() { + for read_only in read_only_ranges.iter().flatten() { + if mutable.overlaps(*read_only) { + return Err(invalid(format!( + "mutable direct {} range overlaps read-only direct {} range", + mutable.label, read_only.label + ))); + } + } + } + Ok(()) +} + +fn require_split_pair( + values_re: *mut f64, + values_im: *mut f64, + scalar_len: u64, + label: &str, +) -> RusticolResult<()> { + if scalar_len != 0 && (values_re.is_null() || values_im.is_null()) { + return Err(invalid(format!( + "nonempty direct {label} view has a null split base" + ))); + } + Ok(()) +} + +fn require_arena_alignment(values: *const f64, scalar_len: u64, label: &str) -> RusticolResult<()> { + if scalar_len != 0 && !(values as usize).is_multiple_of(DIRECT_ARENA_ALIGNMENT) { + return Err(invalid(format!( + "direct {label} base is not {DIRECT_ARENA_ALIGNMENT}-byte aligned" + ))); + } + Ok(()) +} + +fn require_plane_stride_alignment(point_stride: u32, label: &str) -> RusticolResult<()> { + // Packed singleton views are a separate, explicit ABI layout. Every + // other pitch must retain per-plane alignment for tiled SIMD execution. + if point_stride == 1 { + return Ok(()); + } + let stride_bytes = usize::try_from(point_stride) + .ok() + .and_then(|stride| stride.checked_mul(size_of::())) + .ok_or_else(|| { + invalid(format!( + "direct {label} point stride byte count overflows usize" + )) + })?; + if !stride_bytes.is_multiple_of(DIRECT_ARENA_ALIGNMENT) { + return Err(invalid(format!( + "direct {label} point stride does not preserve {DIRECT_ARENA_ALIGNMENT}-byte plane alignment" + ))); + } + Ok(()) +} + +#[derive(Clone, Copy, Debug)] +struct DeclaredRange { + start: usize, + stop: usize, + label: &'static str, +} + +impl DeclaredRange { + const fn overlaps(self, other: Self) -> bool { + self.start < other.stop && other.start < self.stop + } +} + +fn checked_pointer_range( + values: *const f64, + scalar_len: u64, + label: &'static str, +) -> RusticolResult> { + if scalar_len == 0 { + return Ok(None); + } + if values.is_null() { + return Err(invalid(format!( + "nonempty direct {label} range has a null base" + ))); + } + let byte_len = usize::try_from(scalar_len) + .ok() + .and_then(|len| len.checked_mul(size_of::())) + .ok_or_else(|| invalid(format!("direct {label} byte length overflows usize")))?; + if byte_len > isize::MAX as usize { + return Err(invalid(format!( + "direct {label} byte length exceeds isize::MAX" + ))); + } + let start = values as usize; + if !start.is_multiple_of(align_of::()) { + return Err(invalid(format!( + "direct {label} base is not binary64 aligned" + ))); + } + let stop = start + .checked_add(byte_len) + .ok_or_else(|| invalid(format!("direct {label} address range overflows usize")))?; + Ok(Some(DeclaredRange { start, stop, label })) +} + +fn require_disjoint_ranges(ranges: &[Option]) -> RusticolResult<()> { + for (index, left) in ranges.iter().copied().enumerate() { + let Some(left) = left else { + continue; + }; + for right in ranges[index + 1..].iter().copied().flatten() { + if left.overlaps(right) { + return Err(invalid(format!( + "direct {} and {} ranges overlap", + left.label, right.label + ))); + } + } + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::super::AlignedF64Buffer; + use super::*; + + #[test] + fn component_major_shape_checks_projection() { + assert!(DirectPlaneShape::new(15, 8, "test").is_err()); + let shape = DirectPlaneShape::new(24, 8, "test").unwrap(); + assert_eq!(shape.component_count().unwrap(), 3); + assert_eq!(shape.checked_scalar_index(2, 7, "test").unwrap(), 23); + assert_eq!(shape.checked_component_range(1, 2, "test").unwrap(), 8..24); + assert!(shape.checked_scalar_index(3, 0, "test").is_err()); + assert!(shape.checked_scalar_index(0, 8, "test").is_err()); + } + + #[test] + fn views_fail_closed_on_aliases_partial_planes_and_unaligned_pitch() { + let mut values_re = AlignedF64Buffer::zeroed(24, "test real").unwrap(); + let mut values_im = AlignedF64Buffer::zeroed(24, "test imag").unwrap(); + let alias = DirectArenaView { + current_re: values_re.as_mut_ptr(), + current_im: values_re.as_mut_ptr(), + current_scalar_len: 16, + amplitude_re: std::ptr::null_mut(), + amplitude_im: std::ptr::null_mut(), + amplitude_scalar_len: 0, + point_stride: 8, + }; + assert!(alias.validate().is_err()); + let partial = DirectArenaView { + current_re: values_re.as_mut_ptr(), + current_im: values_im.as_mut_ptr(), + current_scalar_len: 15, + amplitude_re: std::ptr::null_mut(), + amplitude_im: std::ptr::null_mut(), + amplitude_scalar_len: 0, + point_stride: 8, + }; + assert!(partial.validate().is_err()); + let bad_pitch = DirectArenaView { + current_re: values_re.as_mut_ptr(), + current_im: values_im.as_mut_ptr(), + current_scalar_len: 6, + amplitude_re: std::ptr::null_mut(), + amplitude_im: std::ptr::null_mut(), + amplitude_scalar_len: 0, + point_stride: 3, + }; + assert!(bad_pitch.validate().is_err()); + } + + #[test] + fn packed_singleton_views_are_validated_as_an_explicit_layout() { + let mut current_re = AlignedF64Buffer::zeroed(2, "packed current real").unwrap(); + let mut current_im = AlignedF64Buffer::zeroed(2, "packed current imag").unwrap(); + DirectArenaView { + current_re: current_re.as_mut_ptr(), + current_im: current_im.as_mut_ptr(), + current_scalar_len: 2, + amplitude_re: std::ptr::null_mut(), + amplitude_im: std::ptr::null_mut(), + amplitude_scalar_len: 0, + point_stride: 1, + } + .validate() + .unwrap(); + } + + #[test] + fn complete_bundle_rejects_mutable_read_only_overlap() { + let mut current_re = AlignedF64Buffer::zeroed(24, "current real").unwrap(); + let mut current_im = AlignedF64Buffer::zeroed(24, "current imag").unwrap(); + let parameters_im = AlignedF64Buffer::zeroed(8, "parameter imag").unwrap(); + let arena = DirectArenaView { + current_re: current_re.as_mut_ptr(), + current_im: current_im.as_mut_ptr(), + current_scalar_len: 16, + amplitude_re: std::ptr::null_mut(), + amplitude_im: std::ptr::null_mut(), + amplitude_scalar_len: 0, + point_stride: 8, + }; + let momenta = DirectMomentumView { + values: unsafe { current_re.as_ptr().add(8) }, + scalar_len: 8, + form_count: 1, + lorentz_component_count: 1, + point_stride: 8, + }; + let parameters = DirectParameterView { + values_re: std::ptr::null(), + values_im: std::ptr::null(), + value_count: 0, + }; + let factors = DirectFactorView { + values_re: parameters_im.as_ptr(), + values_im: parameters_im.as_ptr(), + value_count: 0, + }; + assert!(validate_direct_views(arena, momenta, parameters, factors).is_err()); + } + + #[test] + fn partial_split_range_overlap_is_rejected() { + let mut values = AlignedF64Buffer::zeroed(40, "overlap").unwrap(); + let overlap = DirectArenaView { + current_re: values.as_mut_ptr(), + current_im: unsafe { values.as_mut_ptr().add(8) }, + current_scalar_len: 16, + amplitude_re: std::ptr::null_mut(), + amplitude_im: std::ptr::null_mut(), + amplitude_scalar_len: 0, + point_stride: 8, + }; + assert!(overlap.validate().is_err()); + let parameter_overlap = DirectParameterView { + values_re: values.as_ptr(), + values_im: unsafe { values.as_ptr().add(8) }, + value_count: 16, + }; + assert!(parameter_overlap.validate().is_err()); + } + + #[test] + fn read_only_inputs_may_share_authenticated_storage() { + let mut current_re = AlignedF64Buffer::zeroed(8, "current real").unwrap(); + let mut current_im = AlignedF64Buffer::zeroed(8, "current imag").unwrap(); + let momentum = AlignedF64Buffer::zeroed(8, "momentum").unwrap(); + let shared_re = AlignedF64Buffer::zeroed(8, "shared real").unwrap(); + let shared_im = AlignedF64Buffer::zeroed(8, "shared imag").unwrap(); + let arena = DirectArenaView { + current_re: current_re.as_mut_ptr(), + current_im: current_im.as_mut_ptr(), + current_scalar_len: 8, + amplitude_re: std::ptr::null_mut(), + amplitude_im: std::ptr::null_mut(), + amplitude_scalar_len: 0, + point_stride: 8, + }; + let momenta = DirectMomentumView { + values: momentum.as_ptr(), + scalar_len: 8, + form_count: 1, + lorentz_component_count: 1, + point_stride: 8, + }; + let parameters = DirectParameterView { + values_re: shared_re.as_ptr(), + values_im: shared_im.as_ptr(), + value_count: 8, + }; + let factors = DirectFactorView { + values_re: shared_re.as_ptr(), + values_im: shared_im.as_ptr(), + value_count: 8, + }; + validate_direct_views(arena, momenta, parameters, factors).unwrap(); + } + + #[test] + fn empty_views_ignore_pointer_values_while_momentum_dimensions_are_exact() { + let unaligned = std::ptr::without_provenance::(3); + let empty = DirectArenaView { + current_re: unaligned.cast_mut(), + current_im: std::ptr::null_mut(), + current_scalar_len: 0, + amplitude_re: std::ptr::null_mut(), + amplitude_im: unaligned.cast_mut(), + amplitude_scalar_len: 0, + point_stride: 1, + }; + empty.validate().unwrap(); + + let values = AlignedF64Buffer::zeroed(24, "momentum").unwrap(); + let extra_plane = DirectMomentumView { + values: values.as_ptr(), + scalar_len: 24, + form_count: 2, + lorentz_component_count: 1, + point_stride: 8, + }; + assert!(extra_plane.validate().is_err()); + let partial = DirectMomentumView { + values: values.as_ptr(), + scalar_len: 15, + form_count: 2, + lorentz_component_count: 1, + point_stride: 8, + }; + assert!(partial.validate().is_err()); + } + + #[test] + fn declared_pointer_range_overflow_fails_closed() { + let near_limit = + std::ptr::without_provenance::(usize::MAX - (DIRECT_ARENA_ALIGNMENT - 1)); + assert!(checked_pointer_range(near_limit, 16, "overflow").is_err()); + assert!(checked_pointer_range(near_limit, u64::MAX, "overflow").is_err()); + let over_isize = (isize::MAX as usize / size_of::()) + 1; + assert!( + checked_pointer_range( + std::ptr::without_provenance::(DIRECT_ARENA_ALIGNMENT), + over_isize as u64, + "oversized", + ) + .is_err() + ); + } + + #[test] + fn split_read_only_views_require_binary64_alignment() { + let unaligned = std::ptr::without_provenance::(1); + let aligned = std::ptr::without_provenance::(DIRECT_ARENA_ALIGNMENT); + let parameters = DirectParameterView { + values_re: unaligned, + values_im: aligned, + value_count: 1, + }; + assert!(parameters.validate().is_err()); + let factors = DirectFactorView { + values_re: aligned, + values_im: unaligned, + value_count: 1, + }; + assert!(factors.validate().is_err()); + } + + #[test] + fn complete_bundle_requires_one_shared_physical_pitch() { + let mut current_re = AlignedF64Buffer::zeroed(8, "current real").unwrap(); + let mut current_im = AlignedF64Buffer::zeroed(8, "current imag").unwrap(); + let momentum = AlignedF64Buffer::zeroed(16, "momentum").unwrap(); + let arena = DirectArenaView { + current_re: current_re.as_mut_ptr(), + current_im: current_im.as_mut_ptr(), + current_scalar_len: 8, + amplitude_re: std::ptr::null_mut(), + amplitude_im: std::ptr::null_mut(), + amplitude_scalar_len: 0, + point_stride: 8, + }; + let momenta = DirectMomentumView { + values: momentum.as_ptr(), + scalar_len: 16, + form_count: 1, + lorentz_component_count: 1, + point_stride: 16, + }; + let parameters = DirectParameterView { + values_re: std::ptr::null(), + values_im: std::ptr::null(), + value_count: 0, + }; + let factors = DirectFactorView { + values_re: std::ptr::null(), + values_im: std::ptr::null(), + value_count: 0, + }; + assert!(validate_direct_views(arena, momenta, parameters, factors).is_err()); + } +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/direct_arena/layout.rs b/pyamplicol/rust/crates/rusticol-core/src/direct_arena/layout.rs new file mode 100644 index 0000000000..d8cf8faf94 --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/direct_arena/layout.rs @@ -0,0 +1,503 @@ +// SPDX-License-Identifier: 0BSD + +//! Deterministic variable-width interval allocation for arena planes. + +use std::collections::{BTreeMap, BTreeSet}; + +use crate::{RusticolError, RusticolResult}; + +fn invalid(message: impl Into) -> RusticolError { + RusticolError::invalid_argument(message) +} + +/// Inclusive liveness interval and required component-plane width. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct DirectArenaInterval { + pub semantic_value_id: u32, + pub first_use: u64, + pub last_use: u64, + pub component_count: u32, +} + +impl DirectArenaInterval { + pub fn new( + semantic_value_id: u32, + first_use: u64, + last_use: u64, + component_count: u32, + ) -> RusticolResult { + if first_use > last_use { + return Err(invalid(format!( + "direct-arena value {semantic_value_id} starts at {first_use} after its last use {last_use}" + ))); + } + if component_count == 0 { + return Err(invalid(format!( + "direct-arena value {semantic_value_id} has no components" + ))); + } + Ok(Self { + semantic_value_id, + first_use, + last_use, + component_count, + }) + } +} + +/// One semantic value's physical component range. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct DirectArenaAssignment { + pub semantic_value_id: u32, + pub component_base: u32, + pub component_count: u32, + pub first_use: u64, + pub last_use: u64, +} + +impl DirectArenaAssignment { + pub fn component_stop(self) -> RusticolResult { + self.component_base + .checked_add(self.component_count) + .ok_or_else(|| invalid("direct-arena component range overflows u32")) + } +} + +/// Deterministic interval-coloring result. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct DirectArenaLayout { + assignments: Box<[DirectArenaAssignment]>, + component_count: u32, + total_semantic_components: u64, + reused_semantic_components: u64, +} + +impl DirectArenaLayout { + pub fn assignments(&self) -> &[DirectArenaAssignment] { + &self.assignments + } + + pub const fn component_count(&self) -> u32 { + self.component_count + } + + pub const fn total_semantic_components(&self) -> u64 { + self.total_semantic_components + } + + pub const fn reused_semantic_components(&self) -> u64 { + self.reused_semantic_components + } + + pub fn assignment(&self, semantic_value_id: u32) -> Option { + self.assignments + .get(semantic_value_id as usize) + .copied() + .filter(|assignment| assignment.semantic_value_id == semantic_value_id) + } + + pub fn validate(&self) -> RusticolResult<()> { + if self.assignments.is_empty() { + if self.component_count != 0 + || self.total_semantic_components != 0 + || self.reused_semantic_components != 0 + { + return Err(invalid("empty direct-arena layout has nonempty accounting")); + } + return Ok(()); + } + + let mut semantic_components = 0_u64; + for (index, assignment) in self.assignments.iter().copied().enumerate() { + if assignment.semantic_value_id != index as u32 { + return Err(invalid(format!( + "direct-arena assignment row {index} has semantic value ID {}", + assignment.semantic_value_id + ))); + } + if assignment.component_count == 0 || assignment.first_use > assignment.last_use { + return Err(invalid(format!( + "direct-arena assignment {index} has invalid width or liveness" + ))); + } + if assignment.component_stop()? > self.component_count { + return Err(invalid(format!( + "direct-arena assignment {index} exceeds the physical arena" + ))); + } + semantic_components = semantic_components + .checked_add(u64::from(assignment.component_count)) + .ok_or_else(|| invalid("direct-arena semantic component count overflows u64"))?; + } + if semantic_components != self.total_semantic_components { + return Err(invalid(format!( + "direct-arena semantic component total {semantic_components} does not match recorded {}", + self.total_semantic_components + ))); + } + if self + .total_semantic_components + .checked_sub(u64::from(self.component_count)) + != Some(self.reused_semantic_components) + { + return Err(invalid( + "direct-arena reused component accounting is inconsistent", + )); + } + + for (left_index, left) in self.assignments.iter().copied().enumerate() { + for right in self.assignments[left_index + 1..].iter().copied() { + let lifetimes_overlap = + left.first_use <= right.last_use && right.first_use <= left.last_use; + let ranges_overlap = left.component_base < right.component_stop()? + && right.component_base < left.component_stop()?; + if lifetimes_overlap && ranges_overlap { + return Err(invalid(format!( + "live direct-arena values {} and {} overlap physical components", + left.semantic_value_id, right.semantic_value_id + ))); + } + } + } + Ok(()) + } +} + +#[derive(Default)] +struct FreeRanges { + by_base: BTreeMap, + by_size: BTreeSet<(u32, u32)>, +} + +impl FreeRanges { + fn take_best_fit(&mut self, required: u32) -> RusticolResult> { + let Some(&(component_count, component_base)) = self.by_size.range((required, 0)..).next() + else { + return Ok(None); + }; + self.remove(component_base, component_count)?; + Ok(Some((component_base, component_count))) + } + + fn insert(&mut self, mut component_base: u32, mut component_count: u32) -> RusticolResult<()> { + if component_count == 0 { + return Err(invalid("cannot release an empty direct-arena range")); + } + + if let Some((&previous_base, &previous_count)) = + self.by_base.range(..component_base).next_back() + { + let previous_stop = previous_base + .checked_add(previous_count) + .ok_or_else(|| invalid("direct-arena free range overflows u32"))?; + if previous_stop > component_base { + return Err(invalid("released direct-arena ranges overlap")); + } + if previous_stop == component_base { + self.remove(previous_base, previous_count)?; + component_base = previous_base; + component_count = component_count + .checked_add(previous_count) + .ok_or_else(|| invalid("direct-arena merged range overflows u32"))?; + } + } + + let component_stop = component_base + .checked_add(component_count) + .ok_or_else(|| invalid("direct-arena free range overflows u32"))?; + if let Some((&next_base, &next_count)) = self.by_base.range(component_base..).next() { + if component_stop > next_base { + return Err(invalid("released direct-arena ranges overlap")); + } + if component_stop == next_base { + self.remove(next_base, next_count)?; + component_count = component_count + .checked_add(next_count) + .ok_or_else(|| invalid("direct-arena merged range overflows u32"))?; + } + } + + if self + .by_base + .insert(component_base, component_count) + .is_some() + || !self.by_size.insert((component_count, component_base)) + { + return Err(invalid("released direct-arena range repeats its base")); + } + Ok(()) + } + + fn remove(&mut self, component_base: u32, component_count: u32) -> RusticolResult<()> { + if self.by_base.remove(&component_base) != Some(component_count) + || !self.by_size.remove(&(component_count, component_base)) + { + return Err(invalid("direct-arena free-range indexes disagree")); + } + Ok(()) + } +} + +/// Assign variable-width semantic intervals to reusable physical planes. +/// +/// Inputs use dense semantic value IDs. Intervals are processed by +/// `(first_use, semantic_value_id)`. The allocator releases ranges whose +/// inclusive lifetime ended before the next value starts, then chooses the +/// smallest fitting range with the lowest base as a deterministic tie-breaker. +pub fn assign_direct_arena(intervals: &[DirectArenaInterval]) -> RusticolResult { + if intervals.is_empty() { + return Ok(DirectArenaLayout { + assignments: Box::new([]), + component_count: 0, + total_semantic_components: 0, + reused_semantic_components: 0, + }); + } + + let mut ordered = intervals.to_vec(); + ordered.sort_by_key(|interval| (interval.first_use, interval.semantic_value_id)); + let mut releases = ordered.clone(); + releases.sort_by_key(|interval| (interval.last_use, interval.semantic_value_id)); + let mut seen = vec![false; intervals.len()]; + for interval in &ordered { + let id = interval.semantic_value_id as usize; + if id >= seen.len() || seen[id] { + return Err(invalid( + "direct-arena semantic value IDs must be dense and unique", + )); + } + seen[id] = true; + if interval.component_count == 0 || interval.first_use > interval.last_use { + return Err(invalid(format!( + "direct-arena value {} has invalid width or liveness", + interval.semantic_value_id + ))); + } + } + if seen.contains(&false) { + return Err(invalid( + "direct-arena semantic value IDs must cover zero through count minus one", + )); + } + + let mut assignments = vec![None::; intervals.len()]; + let mut next_release = 0; + let mut free = FreeRanges::default(); + let mut arena_stop = 0_u32; + let mut total_semantic_components = 0_u64; + + for interval in ordered { + while releases + .get(next_release) + .is_some_and(|released| released.last_use < interval.first_use) + { + let released = releases[next_release]; + let assignment = assignments[released.semantic_value_id as usize].ok_or_else(|| { + invalid("direct-arena interval was released before it was assigned") + })?; + free.insert(assignment.component_base, assignment.component_count)?; + next_release += 1; + } + + let (component_base, available_count) = free + .take_best_fit(interval.component_count)? + .unwrap_or((arena_stop, 0)); + + if available_count == 0 { + arena_stop = arena_stop + .checked_add(interval.component_count) + .ok_or_else(|| invalid("direct-arena physical component count exceeds u32"))?; + } else { + let remaining = available_count - interval.component_count; + if remaining != 0 { + free.insert( + component_base + .checked_add(interval.component_count) + .ok_or_else(|| invalid("direct-arena free range overflows u32"))?, + remaining, + )?; + } + } + + let assignment = DirectArenaAssignment { + semantic_value_id: interval.semantic_value_id, + component_base, + component_count: interval.component_count, + first_use: interval.first_use, + last_use: interval.last_use, + }; + assignments[interval.semantic_value_id as usize] = Some(assignment); + total_semantic_components = total_semantic_components + .checked_add(u64::from(interval.component_count)) + .ok_or_else(|| invalid("direct-arena semantic component count exceeds u64"))?; + } + + let assignments = assignments + .into_iter() + .enumerate() + .map(|(id, assignment)| { + assignment.ok_or_else(|| { + invalid(format!( + "direct-arena value {id} was not assigned a physical range" + )) + }) + }) + .collect::>>()?; + Ok(DirectArenaLayout { + assignments: assignments.into_boxed_slice(), + component_count: arena_stop, + total_semantic_components, + reused_semantic_components: total_semantic_components - u64::from(arena_stop), + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn interval(id: u32, first: u64, last: u64, count: u32) -> DirectArenaInterval { + DirectArenaInterval::new(id, first, last, count).unwrap() + } + + fn assign_naively(intervals: &[DirectArenaInterval]) -> DirectArenaLayout { + let mut ordered = intervals.to_vec(); + ordered.sort_by_key(|row| (row.first_use, row.semantic_value_id)); + let mut assignments = vec![None::; intervals.len()]; + let mut arena_stop = 0_u32; + let mut total_semantic_components = 0_u64; + for row in ordered { + let mut occupied = vec![false; arena_stop as usize]; + for assignment in assignments.iter().flatten() { + if assignment.last_use < row.first_use { + continue; + } + let stop = assignment.component_base + assignment.component_count; + occupied[assignment.component_base as usize..stop as usize].fill(true); + } + let mut best = None::<(u32, u32)>; + let mut cursor = 0usize; + while cursor < occupied.len() { + if occupied[cursor] { + cursor += 1; + continue; + } + let start = cursor; + while cursor < occupied.len() && !occupied[cursor] { + cursor += 1; + } + let count = (cursor - start) as u32; + let candidate = (count, start as u32); + if count >= row.component_count && best.is_none_or(|current| candidate < current) { + best = Some(candidate); + } + } + let component_base = best.map_or(arena_stop, |(_, base)| base); + if best.is_none() { + arena_stop += row.component_count; + } + assignments[row.semantic_value_id as usize] = Some(DirectArenaAssignment { + semantic_value_id: row.semantic_value_id, + component_base, + component_count: row.component_count, + first_use: row.first_use, + last_use: row.last_use, + }); + total_semantic_components += u64::from(row.component_count); + } + DirectArenaLayout { + assignments: assignments.into_iter().map(Option::unwrap).collect(), + component_count: arena_stop, + total_semantic_components, + reused_semantic_components: total_semantic_components - u64::from(arena_stop), + } + } + + #[test] + fn non_overlapping_values_reuse_the_same_range() { + let layout = assign_direct_arena(&[ + interval(0, 0, 2, 4), + interval(1, 3, 5, 4), + interval(2, 6, 8, 4), + ]) + .unwrap(); + assert_eq!(layout.component_count(), 4); + assert_eq!(layout.total_semantic_components(), 12); + assert_eq!(layout.reused_semantic_components(), 8); + assert!( + layout + .assignments() + .iter() + .all(|row| row.component_base == 0) + ); + } + + #[test] + fn inclusive_lifetimes_do_not_reuse_at_the_same_event() { + let layout = assign_direct_arena(&[interval(0, 0, 3, 2), interval(1, 3, 4, 3)]).unwrap(); + assert_eq!(layout.component_count(), 5); + assert_eq!(layout.assignment(0).unwrap().component_base, 0); + assert_eq!(layout.assignment(1).unwrap().component_base, 2); + } + + #[test] + fn allocator_chooses_the_smallest_fitting_range_deterministically() { + let layout = assign_direct_arena(&[ + interval(0, 0, 1, 8), + interval(1, 0, 5, 2), + interval(2, 0, 1, 3), + interval(3, 2, 4, 2), + interval(4, 2, 4, 7), + ]) + .unwrap(); + assert_eq!(layout.assignment(3).unwrap().component_base, 10); + assert_eq!(layout.assignment(4).unwrap().component_base, 0); + assert_eq!(layout.component_count(), 13); + } + + #[test] + fn assignment_is_stable_for_unsorted_input() { + let intervals = [ + interval(3, 8, 9, 2), + interval(0, 0, 3, 4), + interval(2, 4, 7, 3), + interval(1, 1, 2, 1), + ]; + let expected = assign_direct_arena(&intervals).unwrap(); + for _ in 0..32 { + assert_eq!(assign_direct_arena(&intervals).unwrap(), expected); + } + } + + #[test] + fn indexed_allocator_matches_naive_best_fit_oracle() { + for seed in 0..48_u32 { + let mut intervals = (0..32_u32) + .map(|id| { + let first = u64::from((id * 7 + seed * 3) % 11); + interval( + id, + first, + first + u64::from((id * 5 + seed) % 6), + 1 + (id * 3 + seed * 5) % 7, + ) + }) + .collect::>(); + let length = intervals.len(); + intervals.rotate_left(seed as usize % length); + if seed % 2 != 0 { + intervals.reverse(); + } + assert_eq!( + assign_direct_arena(&intervals).unwrap(), + assign_naively(&intervals) + ); + } + } + + #[test] + fn malformed_ids_and_component_overflow_fail_closed() { + assert!(assign_direct_arena(&[interval(1, 0, 1, 1)]).is_err()); + assert!(assign_direct_arena(&[interval(0, 0, 1, 1), interval(0, 2, 3, 1)]).is_err()); + assert!(assign_direct_arena(&[interval(0, 0, 1, u32::MAX), interval(1, 0, 1, 1)]).is_err()); + } +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/direct_arena/mod.rs b/pyamplicol/rust/crates/rusticol-core/src/direct_arena/mod.rs new file mode 100644 index 0000000000..b128f0825e --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/direct_arena/mod.rs @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: 0BSD + +//! Lane-neutral Direct-Arena building blocks. +//! +//! This module contains no recurrence-current, eager-row, or compiled-stage +//! semantics. Each execution lane owns its authenticated plan, event schedule, +//! selectors, reductions, and hot loop. + +mod abi; +mod layout; +mod profile; +mod reduction; +mod storage; + +pub use abi::{ + DirectArenaView, DirectFactorView, DirectMomentumView, DirectParameterView, DirectPlaneShape, + validate_direct_views, +}; +pub use layout::{ + DirectArenaAssignment, DirectArenaInterval, DirectArenaLayout, assign_direct_arena, +}; +pub use profile::{DirectArenaTrafficCounters, DirectArenaTrafficKind}; +pub use reduction::DirectAmplitudePlanes; +pub use storage::{ + AlignedF64Buffer, DIRECT_ARENA_ALIGNMENT, DIRECT_ARENA_LOCALITY_POINT_CAP, + DirectArenaAllocationCounters, DirectArenaWorkspace, DirectPointTile, DirectPointTiles, + checked_aligned_point_stride, checked_plane_scalar_len, clear_split_active_range, + deterministic_point_tile_size, deterministic_point_tile_size_with_cache_footprint, +}; diff --git a/pyamplicol/rust/crates/rusticol-core/src/direct_arena/profile.rs b/pyamplicol/rust/crates/rusticol-core/src/direct_arena/profile.rs new file mode 100644 index 0000000000..3dd976960e --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/direct_arena/profile.rs @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: 0BSD + +//! Lane-neutral direct-call and forbidden-materialization accounting. + +use crate::{RusticolError, RusticolResult}; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum DirectArenaTrafficKind { + PacketInput, + PacketOutput, + Gather, + Scatter, + Remap, +} + +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub struct DirectArenaTrafficCounters { + pub calls: u64, + pub rows: u64, + pub points: u64, + pub packet_input_bytes: u64, + pub packet_output_bytes: u64, + pub gather_bytes: u64, + pub scatter_bytes: u64, + pub remap_bytes: u64, +} + +impl DirectArenaTrafficCounters { + pub fn record_call(&mut self, rows: u32, points: u32) { + self.calls = self.calls.saturating_add(1); + self.rows = self.rows.saturating_add(u64::from(rows)); + self.points = self.points.saturating_add(u64::from(points)); + } + + pub fn record_traffic(&mut self, kind: DirectArenaTrafficKind, bytes: u64) { + let counter = match kind { + DirectArenaTrafficKind::PacketInput => &mut self.packet_input_bytes, + DirectArenaTrafficKind::PacketOutput => &mut self.packet_output_bytes, + DirectArenaTrafficKind::Gather => &mut self.gather_bytes, + DirectArenaTrafficKind::Scatter => &mut self.scatter_bytes, + DirectArenaTrafficKind::Remap => &mut self.remap_bytes, + }; + *counter = counter.saturating_add(bytes); + } + + pub fn validate_direct(&self) -> RusticolResult<()> { + if self.packet_input_bytes + | self.packet_output_bytes + | self.gather_bytes + | self.scatter_bytes + | self.remap_bytes + != 0 + { + return Err(RusticolError::integrity( + "Direct-Arena execution recorded forbidden packet/gather/scatter/remap traffic", + )); + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn forbidden_traffic_fails_closed() { + let mut counters = DirectArenaTrafficCounters::default(); + counters.record_call(7, 128); + assert!(counters.validate_direct().is_ok()); + counters.record_traffic(DirectArenaTrafficKind::Gather, 8); + assert!(counters.validate_direct().is_err()); + } +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/direct_arena/reduction.rs b/pyamplicol/rust/crates/rusticol-core/src/direct_arena/reduction.rs new file mode 100644 index 0000000000..8c382f7a7f --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/direct_arena/reduction.rs @@ -0,0 +1,132 @@ +// SPDX-License-Identifier: 0BSD + +//! Borrowed split-complex amplitude planes for lane-neutral reductions. + +use std::mem::size_of; + +use crate::{RusticolError, RusticolResult}; + +use super::{DIRECT_ARENA_ALIGNMENT, DirectPlaneShape}; + +fn invalid(message: impl Into) -> RusticolError { + RusticolError::invalid_argument(message) +} + +/// Read-only component-major amplitude planes for one active point tile. +/// +/// Each component occupies `point_stride` consecutive scalars in both halves +/// of the split-complex storage. Only `0..point_count` is semantically active; +/// the remaining physical pitch is padding retained by the persistent arena. +#[derive(Clone, Copy, Debug)] +pub struct DirectAmplitudePlanes<'a> { + values_re: &'a [f64], + values_im: &'a [f64], + shape: DirectPlaneShape, + point_count: u32, +} + +impl<'a> DirectAmplitudePlanes<'a> { + pub fn new( + values_re: &'a [f64], + values_im: &'a [f64], + point_stride: u32, + point_count: u32, + ) -> RusticolResult { + if values_re.len() != values_im.len() { + return Err(invalid( + "direct amplitude real and imaginary plane lengths disagree", + )); + } + if point_count == 0 || point_count > point_stride { + return Err(invalid( + "direct amplitude active point count is outside its physical pitch", + )); + } + let scalar_len = u64::try_from(values_re.len()) + .map_err(|_| invalid("direct amplitude scalar length exceeds u64"))?; + let shape = DirectPlaneShape::new(scalar_len, point_stride, "amplitude reduction")?; + let alignment_scalars = u32::try_from(DIRECT_ARENA_ALIGNMENT / size_of::()) + .map_err(|_| invalid("direct amplitude alignment exceeds u32"))?; + if !point_stride.is_multiple_of(alignment_scalars) { + return Err(invalid( + "direct amplitude point stride does not preserve per-plane alignment", + )); + } + for (values, label) in [(values_re, "real"), (values_im, "imaginary")] { + if values.is_empty() + || !(values.as_ptr() as usize).is_multiple_of(DIRECT_ARENA_ALIGNMENT) + { + return Err(invalid(format!( + "direct amplitude {label} plane base is not 64-byte aligned" + ))); + } + } + let _ = shape.component_count()?; + Ok(Self { + values_re, + values_im, + shape, + point_count, + }) + } + + pub const fn point_count(self) -> u32 { + self.point_count + } + + pub const fn point_stride(self) -> u32 { + self.shape.point_stride() + } + + pub fn component_count(self) -> RusticolResult { + self.shape.component_count() + } + + /// Return one active plane without exposing its physical pitch padding. + /// + /// Callers validate the component catalog once before entering their hot + /// reduction loop. Keeping points contiguous here lets reducers traverse + /// terms/components outside and points inside without reconstructing + /// point-major rows. + #[allow(dead_code)] + #[inline(always)] + pub(crate) fn plane_unchecked(self, component: usize) -> (&'a [f64], &'a [f64]) { + debug_assert!(component < self.shape.component_count().unwrap_or(0) as usize); + let start = component * self.shape.point_stride() as usize; + let stop = start + self.point_count as usize; + (&self.values_re[start..stop], &self.values_im[start..stop]) + } + + #[inline(always)] + pub(crate) fn value_unchecked(self, component: usize, point: usize) -> (f64, f64) { + debug_assert!(component < self.shape.component_count().unwrap_or(0) as usize); + debug_assert!(point < self.point_count as usize); + let index = component * self.shape.point_stride() as usize + point; + (self.values_re[index], self.values_im[index]) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::direct_arena::DirectArenaWorkspace; + + #[test] + fn validates_alignment_pitch_and_active_tail() { + let mut workspace = DirectArenaWorkspace::new(0, 3, 129).unwrap(); + workspace.begin_tile(127).unwrap(); + let stride = workspace.point_stride(); + let (values_re, values_im) = workspace.amplitude_slices(); + let planes = DirectAmplitudePlanes::new(values_re, values_im, stride, 127).unwrap(); + assert_eq!(planes.component_count().unwrap(), 3); + assert_eq!(planes.point_count(), 127); + assert_eq!(planes.point_stride(), 136); + let (plane_re, plane_im) = planes.plane_unchecked(1); + assert_eq!(plane_re.len(), 127); + assert_eq!(plane_im.len(), 127); + assert_eq!(plane_re.as_ptr(), values_re[136..].as_ptr()); + assert_eq!(plane_im.as_ptr(), values_im[136..].as_ptr()); + assert!(DirectAmplitudePlanes::new(values_re, values_im, stride, 137).is_err()); + assert!(DirectAmplitudePlanes::new(&values_re[1..], &values_im[1..], stride, 1).is_err()); + } +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/direct_arena/storage.rs b/pyamplicol/rust/crates/rusticol-core/src/direct_arena/storage.rs new file mode 100644 index 0000000000..a8b6320f27 --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/direct_arena/storage.rs @@ -0,0 +1,690 @@ +// SPDX-License-Identifier: 0BSD + +//! Aligned storage and deterministic, shape-derived point tiling. + +use std::mem::size_of; + +use crate::{RusticolError, RusticolResult}; + +use super::DirectArenaView; + +pub const DIRECT_ARENA_ALIGNMENT: usize = 64; +const DIRECT_ARENA_ALIGNMENT_SCALARS: u32 = (DIRECT_ARENA_ALIGNMENT / size_of::()) as u32; + +fn invalid(message: impl Into) -> RusticolError { + RusticolError::invalid_argument(message) +} + +/// Binary64 storage exposing an exactly-sized 64-byte-aligned logical range. +/// +/// The small alignment reserve belongs to the allocator and is intentionally +/// outside any caller-authenticated logical workspace budget. +pub struct AlignedF64Buffer { + storage: Vec, + start: usize, + len: usize, +} + +impl AlignedF64Buffer { + pub fn zeroed(len: usize, label: &str) -> RusticolResult { + if len == 0 { + return Ok(Self { + storage: Vec::new(), + start: 0, + len: 0, + }); + } + let alignment_values = DIRECT_ARENA_ALIGNMENT + .checked_div(size_of::()) + .filter(|value| *value != 0) + .ok_or_else(|| invalid("arena alignment is invalid for binary64 storage"))?; + let storage_len = len + .checked_add(alignment_values - 1) + .ok_or_else(|| invalid(format!("{label} arena length overflows usize")))?; + let mut storage = Vec::new(); + storage.try_reserve_exact(storage_len).map_err(|error| { + RusticolError::internal(format!( + "could not allocate Direct-Arena {label} storage: {error}" + )) + })?; + storage.resize(storage_len, 0.0); + let address = storage.as_ptr() as usize; + let byte_offset = + (DIRECT_ARENA_ALIGNMENT - address % DIRECT_ARENA_ALIGNMENT) % DIRECT_ARENA_ALIGNMENT; + if !byte_offset.is_multiple_of(size_of::()) { + return Err(RusticolError::internal( + "Direct-Arena allocation cannot provide binary64 alignment", + )); + } + let start = byte_offset / size_of::(); + if start.checked_add(len).is_none_or(|end| end > storage.len()) { + return Err(RusticolError::internal( + "aligned Direct-Arena range exceeds its allocation", + )); + } + Ok(Self { + storage, + start, + len, + }) + } + + pub const fn len(&self) -> usize { + self.len + } + + pub const fn is_empty(&self) -> bool { + self.len == 0 + } + + pub fn as_ptr(&self) -> *const f64 { + self.as_slice().as_ptr() + } + + pub fn as_mut_ptr(&mut self) -> *mut f64 { + self.as_mut_slice().as_mut_ptr() + } + + pub fn as_slice(&self) -> &[f64] { + &self.storage[self.start..self.start + self.len] + } + + pub fn as_mut_slice(&mut self) -> &mut [f64] { + &mut self.storage[self.start..self.start + self.len] + } + + pub fn allocation_counters(&self) -> DirectArenaAllocationCounters { + DirectArenaAllocationCounters { + allocation_requests: u64::from(!self.is_empty()), + requested_bytes: u64::try_from(self.storage.len().saturating_mul(size_of::())) + .unwrap_or(u64::MAX), + } + } +} + +/// Logical allocation requests made for persistent Direct-Arena buffers. +/// +/// `requested_bytes` describes the buffer lengths requested from `Vec`; an +/// allocator may reserve more physical capacity. Use a counting allocator +/// when actual allocator calls or resident bytes are required. +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub struct DirectArenaAllocationCounters { + pub allocation_requests: u64, + pub requested_bytes: u64, +} + +impl DirectArenaAllocationCounters { + pub fn checked_add(self, other: Self) -> RusticolResult { + Ok(Self { + allocation_requests: self + .allocation_requests + .checked_add(other.allocation_requests) + .ok_or_else(|| invalid("Direct-Arena allocation request count overflows u64"))?, + requested_bytes: self + .requested_bytes + .checked_add(other.requested_bytes) + .ok_or_else(|| invalid("Direct-Arena requested byte count overflows u64"))?, + }) + } +} + +/// Persistent split-complex current and amplitude storage for one lane. +pub struct DirectArenaWorkspace { + current_re: AlignedF64Buffer, + current_im: AlignedF64Buffer, + amplitude_re: AlignedF64Buffer, + amplitude_im: AlignedF64Buffer, + current_plane_count: u32, + amplitude_plane_count: u32, + tile_capacity: u32, + point_stride: u32, + active_point_count: u32, + allocation_counters: DirectArenaAllocationCounters, +} + +impl DirectArenaWorkspace { + pub fn new( + current_plane_count: u32, + amplitude_plane_count: u32, + tile_capacity: u32, + ) -> RusticolResult { + if tile_capacity == 0 { + return Err(invalid( + "Direct-Arena workspace tile capacity must be positive", + )); + } + let point_stride = checked_aligned_point_stride(tile_capacity)?; + Self::new_with_layout( + current_plane_count, + amplitude_plane_count, + tile_capacity, + point_stride, + ) + } + + /// Allocate the explicit packed layout used only for a proven-generic + /// singleton direct execution. Ordinary capacity-one workspaces remain + /// aligned through [`Self::new`]. + pub(crate) fn new_packed_singleton( + current_plane_count: u32, + amplitude_plane_count: u32, + ) -> RusticolResult { + Self::new_with_layout(current_plane_count, amplitude_plane_count, 1, 1) + } + + fn new_with_layout( + current_plane_count: u32, + amplitude_plane_count: u32, + tile_capacity: u32, + point_stride: u32, + ) -> RusticolResult { + let current_len = + checked_plane_scalar_len(current_plane_count, point_stride, "current arena")?; + let amplitude_len = + checked_plane_scalar_len(amplitude_plane_count, point_stride, "amplitude arena")?; + let current_re = AlignedF64Buffer::zeroed(current_len, "current real")?; + let current_im = AlignedF64Buffer::zeroed(current_len, "current imaginary")?; + let amplitude_re = AlignedF64Buffer::zeroed(amplitude_len, "amplitude real")?; + let amplitude_im = AlignedF64Buffer::zeroed(amplitude_len, "amplitude imaginary")?; + let allocation_counters = [¤t_re, ¤t_im, &litude_re, &litude_im] + .into_iter() + .try_fold(DirectArenaAllocationCounters::default(), |total, buffer| { + total.checked_add(buffer.allocation_counters()) + })?; + Ok(Self { + current_re, + current_im, + amplitude_re, + amplitude_im, + current_plane_count, + amplitude_plane_count, + tile_capacity, + point_stride, + active_point_count: 0, + allocation_counters, + }) + } + + pub const fn tile_capacity(&self) -> u32 { + self.tile_capacity + } + + pub const fn point_stride(&self) -> u32 { + self.point_stride + } + + pub const fn active_point_count(&self) -> u32 { + self.active_point_count + } + + pub const fn allocation_counters(&self) -> DirectArenaAllocationCounters { + self.allocation_counters + } + + pub fn current_slices(&self) -> (&[f64], &[f64]) { + (self.current_re.as_slice(), self.current_im.as_slice()) + } + + pub fn amplitude_slices(&self) -> (&[f64], &[f64]) { + (self.amplitude_re.as_slice(), self.amplitude_im.as_slice()) + } + + pub fn split_slices_mut(&mut self) -> (&mut [f64], &mut [f64], &mut [f64], &mut [f64]) { + ( + self.current_re.as_mut_slice(), + self.current_im.as_mut_slice(), + self.amplitude_re.as_mut_slice(), + self.amplitude_im.as_mut_slice(), + ) + } + + /// Bind the persistent storage to one active tile without modifying it. + /// + /// The physical pitch and pointer identity remain stable across full and + /// tail tiles. Only `0..point_count` in each plane is semantically active. + pub fn begin_tile(&mut self, point_count: u32) -> RusticolResult<()> { + if point_count == 0 || point_count > self.tile_capacity { + return Err(invalid(format!( + "Direct-Arena workspace point count {point_count} is outside 1..={}", + self.tile_capacity + ))); + } + self.active_point_count = point_count; + Ok(()) + } + + pub fn clear_current_active( + &mut self, + component_base: u32, + component_count: u32, + ) -> RusticolResult<()> { + clear_split_active_range( + self.current_re.as_mut_slice(), + self.current_im.as_mut_slice(), + self.current_plane_count, + self.point_stride, + self.active_point_count, + component_base, + component_count, + "current arena", + ) + } + + pub fn clear_amplitude_active( + &mut self, + component_base: u32, + component_count: u32, + ) -> RusticolResult<()> { + clear_split_active_range( + self.amplitude_re.as_mut_slice(), + self.amplitude_im.as_mut_slice(), + self.amplitude_plane_count, + self.point_stride, + self.active_point_count, + component_base, + component_count, + "amplitude arena", + ) + } + + pub fn view(&mut self) -> RusticolResult { + if self.active_point_count == 0 { + return Err(invalid( + "Direct-Arena workspace view requires an active tile", + )); + } + let view = DirectArenaView { + current_re: self.current_re.as_mut_ptr(), + current_im: self.current_im.as_mut_ptr(), + current_scalar_len: u64::try_from(self.current_re.len()) + .map_err(|_| invalid("current arena view length exceeds u64"))?, + amplitude_re: self.amplitude_re.as_mut_ptr(), + amplitude_im: self.amplitude_im.as_mut_ptr(), + amplitude_scalar_len: u64::try_from(self.amplitude_re.len()) + .map_err(|_| invalid("amplitude arena view length exceeds u64"))?, + point_stride: self.point_stride, + }; + view.validate()?; + Ok(view) + } + + pub fn point_tiles( + &self, + requested_points: u32, + tile_points: u32, + ) -> RusticolResult { + if requested_points == 0 || tile_points == 0 || tile_points > self.tile_capacity { + return Err(invalid(format!( + "Direct-Arena point tiling requires positive counts and tile size at most {}", + self.tile_capacity + ))); + } + Ok(DirectPointTiles { + next_point: 0, + requested_points, + tile_points, + }) + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct DirectPointTile { + pub point_start: u32, + pub point_count: u32, +} + +#[derive(Clone, Copy, Debug)] +pub struct DirectPointTiles { + next_point: u32, + requested_points: u32, + tile_points: u32, +} + +impl Iterator for DirectPointTiles { + type Item = DirectPointTile; + + fn next(&mut self) -> Option { + if self.next_point == self.requested_points { + return None; + } + let point_count = (self.requested_points - self.next_point).min(self.tile_points); + let tile = DirectPointTile { + point_start: self.next_point, + point_count, + }; + self.next_point += point_count; + Some(tile) + } +} + +pub fn checked_aligned_point_stride(tile_capacity: u32) -> RusticolResult { + if DIRECT_ARENA_ALIGNMENT_SCALARS == 0 { + return Err(RusticolError::internal( + "Direct-Arena alignment is smaller than binary64 storage", + )); + } + let remainder = tile_capacity % DIRECT_ARENA_ALIGNMENT_SCALARS; + if remainder == 0 { + return Ok(tile_capacity); + } + tile_capacity + .checked_add(DIRECT_ARENA_ALIGNMENT_SCALARS - remainder) + .ok_or_else(|| invalid("aligned Direct-Arena point stride exceeds u32")) +} + +/// Clear only active points in a validated contiguous split-complex plane range. +#[allow(clippy::too_many_arguments)] +pub fn clear_split_active_range( + values_re: &mut [f64], + values_im: &mut [f64], + plane_count: u32, + point_stride: u32, + active_point_count: u32, + component_base: u32, + component_count: u32, + label: &str, +) -> RusticolResult<()> { + if active_point_count == 0 || active_point_count > point_stride { + return Err(invalid(format!( + "direct {label} clear requires an active count within its physical pitch" + ))); + } + if component_count == 0 { + return Err(invalid(format!( + "direct {label} clear range must contain a component" + ))); + } + let expected_len = checked_plane_scalar_len(plane_count, point_stride, label)?; + if values_re.len() != expected_len || values_im.len() != expected_len { + return Err(invalid(format!( + "direct {label} split storage has an inconsistent length" + ))); + } + let component_stop = component_base + .checked_add(component_count) + .filter(|stop| *stop <= plane_count) + .ok_or_else(|| invalid(format!("direct {label} clear range is out of bounds")))?; + let point_stride = point_stride as usize; + let active_point_count = active_point_count as usize; + for component in component_base..component_stop { + let start = (component as usize) + .checked_mul(point_stride) + .ok_or_else(|| invalid(format!("direct {label} clear range overflows usize")))?; + let stop = start + .checked_add(active_point_count) + .ok_or_else(|| invalid(format!("direct {label} clear range overflows usize")))?; + values_re[start..stop].fill(0.0); + values_im[start..stop].fill(0.0); + } + Ok(()) +} + +pub fn checked_plane_scalar_len( + plane_count: u32, + point_stride: u32, + label: &str, +) -> RusticolResult { + usize::try_from(plane_count) + .ok() + .and_then(|planes| planes.checked_mul(point_stride as usize)) + .ok_or_else(|| invalid(format!("{label} length overflows usize"))) +} + +/// Portable upper bound for the physical point pitch of row-outer Direct-Arena lanes. +/// +/// Table callables revisit their parent component planes for every row. A +/// larger pitch spreads those planes farther apart and expands the live parent +/// footprint even when the active point count is smaller than the allocated +/// capacity. Keeping the pitch bounded preserves locality without consulting +/// the host architecture or runtime timings. Lanes remain free to request a +/// smaller tile or apply a smaller workspace target. +pub const DIRECT_ARENA_LOCALITY_POINT_CAP: u32 = 64; + +/// Select a semantic tile capacity using its padded footprint. +/// +/// `scalar_values_per_point` includes every physically pitched plane and both +/// halves of split-complex planes. The workspace target excludes the +/// allocator's small base-alignment reserve and grows to fit one aligned tile. +/// It controls batching, not which processes can run. `cache_target_bytes` +/// is a separate locality target; lanes may pass `usize::MAX` and apply their +/// own cache heuristic. +pub fn deterministic_point_tile_size( + requested_points: u32, + workspace_bytes: usize, + cache_target_bytes: usize, + scalar_values_per_point: usize, +) -> RusticolResult { + deterministic_point_tile_size_with_cache_footprint( + requested_points, + workspace_bytes, + cache_target_bytes, + scalar_values_per_point, + scalar_values_per_point, + ) +} + +/// Select a semantic tile using distinct physical and cache-local footprints. +/// +/// The workspace target uses `physical_scalar_values_per_point`, including +/// the final aligned pitch. The cache target instead uses the maximum +/// phase-local working set. Keeping +/// the two inputs separate prevents cold persistent planes from shrinking a +/// tile whose hot kernels touch only a bounded subset. +pub fn deterministic_point_tile_size_with_cache_footprint( + requested_points: u32, + workspace_bytes: usize, + cache_target_bytes: usize, + physical_scalar_values_per_point: usize, + cache_scalar_values_per_point: usize, +) -> RusticolResult { + if requested_points == 0 + || physical_scalar_values_per_point == 0 + || cache_scalar_values_per_point == 0 + { + return Err(invalid( + "Direct-Arena tiling requires positive points and physical/cache per-point shapes", + )); + } + let physical_bytes_per_point = physical_scalar_values_per_point + .checked_mul(size_of::()) + .ok_or_else(|| invalid("Direct-Arena per-point byte count overflows usize"))?; + let cache_bytes_per_point = cache_scalar_values_per_point + .checked_mul(size_of::()) + .ok_or_else(|| invalid("Direct-Arena cache footprint overflows usize"))?; + let minimum_physical_bytes = physical_bytes_per_point + .checked_mul(DIRECT_ARENA_ALIGNMENT_SCALARS as usize) + .ok_or_else(|| invalid("minimum aligned Direct-Arena tile byte count overflows usize"))?; + let workspace_bytes = workspace_bytes.max(minimum_physical_bytes); + let largest_aligned_u32 = u32::MAX - u32::MAX % DIRECT_ARENA_ALIGNMENT_SCALARS; + let workspace_stride = (workspace_bytes / physical_bytes_per_point) + .min(largest_aligned_u32 as usize) + / DIRECT_ARENA_ALIGNMENT_SCALARS as usize + * DIRECT_ARENA_ALIGNMENT_SCALARS as usize; + let workspace_capacity = u32::try_from(workspace_stride) + .map_err(|_| invalid("aligned Direct-Arena workspace capacity exceeds u32"))?; + let cache_points = + greatest_power_of_two_not_exceeding((cache_target_bytes / cache_bytes_per_point).max(1)); + let tile_capacity = requested_points + .min(workspace_capacity) + .min(u32::try_from(cache_points).unwrap_or(u32::MAX).max(1)); + let point_stride = checked_aligned_point_stride(tile_capacity)?; + let physical_bytes = (point_stride as usize) + .checked_mul(physical_bytes_per_point) + .ok_or_else(|| invalid("padded Direct-Arena tile byte count overflows usize"))?; + if physical_bytes > workspace_bytes { + return Err(invalid(format!( + "padded Direct-Arena tile accounting mismatch: {physical_bytes} bytes \ + exceeds derived workspace size {workspace_bytes}" + ))); + } + Ok(tile_capacity) +} + +fn greatest_power_of_two_not_exceeding(value: usize) -> usize { + if value == 0 { + return 1; + } + 1_usize << (usize::BITS - 1 - value.leading_zeros()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn storage_and_each_physical_plane_are_aligned() { + let values = AlignedF64Buffer::zeroed(129, "test").unwrap(); + assert_eq!(values.len(), 129); + assert_eq!((values.as_ptr() as usize) % DIRECT_ARENA_ALIGNMENT, 0); + for (capacity, stride) in [(1, 8), (7, 8), (127, 128), (129, 136)] { + let mut workspace = DirectArenaWorkspace::new(3, 2, capacity).unwrap(); + assert_eq!(workspace.point_stride(), stride); + workspace.begin_tile(capacity).unwrap(); + let view = workspace.view().unwrap(); + for component in 0..3_usize { + assert_eq!( + unsafe { view.current_re.add(component * stride as usize) } as usize + % DIRECT_ARENA_ALIGNMENT, + 0 + ); + } + } + } + + #[test] + fn tiling_uses_padded_physical_workspace_target() { + assert_eq!( + deterministic_point_tile_size(129, 2064, usize::MAX, 2).unwrap(), + 128 + ); + assert_eq!( + deterministic_point_tile_size(129, 2176, usize::MAX, 2).unwrap(), + 129 + ); + assert_eq!( + deterministic_point_tile_size(129, 2175, usize::MAX, 2).unwrap(), + 128 + ); + assert_eq!( + deterministic_point_tile_size(1, 127, usize::MAX, 2).unwrap(), + 1 + ); + assert_eq!( + deterministic_point_tile_size(1, 128, usize::MAX, 2).unwrap(), + 1 + ); + } + + #[test] + fn tiling_separates_physical_and_cache_footprints() { + assert_eq!( + deterministic_point_tile_size_with_cache_footprint(32, 32 * 1024, 4 * 1024, 128, 8,) + .unwrap(), + 32, + "cold physical planes must not consume the phase-local cache target" + ); + assert_eq!( + deterministic_point_tile_size_with_cache_footprint(64, 16 * 1024, usize::MAX, 128, 1,) + .unwrap(), + 16, + "the padded physical allocation controls the tile size" + ); + assert_eq!( + deterministic_point_tile_size_with_cache_footprint(64, usize::MAX, 4 * 1024, 1, 128,) + .unwrap(), + 4, + "the cache bound follows the phase-local footprint" + ); + assert!( + deterministic_point_tile_size_with_cache_footprint(1, usize::MAX, usize::MAX, 1, 0,) + .is_err() + ); + } + + #[test] + fn observed_large_minimum_pitch_can_exceed_the_workspace_target() { + const PHYSICAL_SCALARS_PER_POINT: usize = 4_249_960; + const REQUIRED_BYTES: usize = 271_997_440; + + assert_eq!( + deterministic_point_tile_size_with_cache_footprint( + 1024, + REQUIRED_BYTES, + 4 * 1024 * 1024, + PHYSICAL_SCALARS_PER_POINT, + 1, + ) + .unwrap(), + 8 + ); + assert_eq!( + deterministic_point_tile_size_with_cache_footprint( + 1024, + REQUIRED_BYTES - 1, + 4 * 1024 * 1024, + PHYSICAL_SCALARS_PER_POINT, + 1, + ) + .unwrap(), + 8 + ); + } + + #[test] + fn allocator_alignment_reserve_is_outside_the_logical_budget() { + assert_eq!( + deterministic_point_tile_size(1, 128, usize::MAX, 2).unwrap(), + 1 + ); + let workspace = DirectArenaWorkspace::new(1, 0, 1).unwrap(); + assert_eq!(workspace.point_stride(), 8); + assert_eq!(workspace.allocation_counters().allocation_requests, 2); + assert!( + workspace.allocation_counters().requested_bytes > 128, + "allocator-owned base-alignment reserve must not reduce logical capacity" + ); + } + + #[test] + fn packed_singleton_is_explicit_and_does_not_change_aligned_capacity_one() { + assert_eq!( + DirectArenaWorkspace::new(2, 1, 1).unwrap().point_stride(), + 8 + ); + let packed = DirectArenaWorkspace::new_packed_singleton(2, 1).unwrap(); + assert_eq!(packed.tile_capacity(), 1); + assert_eq!(packed.point_stride(), 1); + assert_eq!(packed.current_slices().0.len(), 2); + assert_eq!(packed.amplitude_slices().0.len(), 1); + } + + #[test] + fn selective_clear_preserves_padding_and_other_planes() { + let mut workspace = DirectArenaWorkspace::new(2, 1, 9).unwrap(); + workspace.begin_tile(9).unwrap(); + { + let (current_re, current_im, amplitude_re, amplitude_im) = workspace.split_slices_mut(); + current_re.fill(7.0); + current_im.fill(-7.0); + amplitude_re.fill(3.0); + amplitude_im.fill(-3.0); + } + workspace.begin_tile(7).unwrap(); + workspace.clear_current_active(1, 1).unwrap(); + workspace.clear_amplitude_active(0, 1).unwrap(); + let stride = workspace.point_stride() as usize; + let (current_re, current_im) = workspace.current_slices(); + assert!(current_re[..stride].iter().all(|value| *value == 7.0)); + assert!( + current_re[stride..stride + 7] + .iter() + .all(|value| *value == 0.0) + ); + assert_eq!(current_re[stride + 7], 7.0); + assert_eq!(current_im[stride + 7], -7.0); + let (amplitude_re, amplitude_im) = workspace.amplitude_slices(); + assert!(amplitude_re[..7].iter().all(|value| *value == 0.0)); + assert_eq!(amplitude_re[7], 3.0); + assert_eq!(amplitude_im[7], -3.0); + } +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/eager_layout.rs b/pyamplicol/rust/crates/rusticol-core/src/eager_layout.rs new file mode 100644 index 0000000000..e9a999b443 --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/eager_layout.rs @@ -0,0 +1,370 @@ +// SPDX-License-Identifier: 0BSD + +//! Fixed-width wire contracts for compact eager runtime artifacts. +//! +//! This module defines only the versioned storage foundation. Eager lowering +//! and runtime loading are implemented separately and consume these validated +//! little-endian sections. + +use crate::{RusticolError, RusticolResult}; + +pub const EAGER_LOWERING_INPUT_ABI: &str = "pyamplicol-eager-lowering-input-v1"; +pub const EAGER_PLAN_ABI: &str = "pyamplicol-eager-plan-v3"; +pub const EAGER_RUNTIME_LAYOUT_ABI: &str = "pyamplicol-eager-runtime-layout-v1"; +pub const EAGER_RUNTIME_CAPABILITY: &str = "rusticol.eager-runtime-layout.complex-f64.v1"; +pub const EAGER_DIRECT_ARENA_RUNTIME_CAPABILITY: &str = "eager-direct-arena-v1"; +pub const EAGER_DIRECT_SOURCE_APPLICATION_ABI: &str = "pyamplicol-symjit-plane-application-v2"; +pub const EAGER_NATIVE_DIRECT_TABLE_APPLICATION_ABI: &str = + "pyamplicol-eager-native-direct-table-v1"; +pub const EAGER_DIRECT_TABLE_DESCRIPTOR_ABI: &str = "pyamplicol-eager-plane-table-descriptor-v1"; +pub const EAGER_DIRECT_TABLE_BINDING_ABI: &str = "pyamplicol-eager-plane-table-binding-v2"; +pub const EAGER_RUNTIME_CONTAINER_KIND: &str = "pyamplicol-eager-runtime-container"; +pub const EAGER_RUNTIME_CONTAINER_SCHEMA: u16 = 1; +pub const EAGER_SECTION_SCHEMA: u16 = 1; +pub const EAGER_SECTION_HEADER_SIZE: usize = 64; +pub const EAGER_SECTION_REFERENCE_SIZE: usize = 24; + +const SECTION_MAGIC: &[u8; 8] = b"PACERT\0\0"; +const SUPPORTED_FLAGS: u16 = 0; + +/// Stable section identifiers used by `eager-runtime.pacbin` members. +#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] +#[repr(u16)] +pub enum EagerSectionKind { + Metadata = 1, + CurrentLayout = 2, + ValueLayout = 3, + MomentumLayout = 4, + SourceFill = 5, + ParameterLayout = 6, + Stages = 7, + Couplings = 8, + Invocations = 9, + Attachments = 10, + Finalizations = 11, + Closures = 12, + SelectorDomains = 13, + SelectorMemberships = 14, + ReductionGroups = 15, + ReductionEntries = 16, + ExactFactors = 17, + InspectionSummary = 18, +} + +impl EagerSectionKind { + fn parse(value: u16) -> RusticolResult { + match value { + 1 => Ok(Self::Metadata), + 2 => Ok(Self::CurrentLayout), + 3 => Ok(Self::ValueLayout), + 4 => Ok(Self::MomentumLayout), + 5 => Ok(Self::SourceFill), + 6 => Ok(Self::ParameterLayout), + 7 => Ok(Self::Stages), + 8 => Ok(Self::Couplings), + 9 => Ok(Self::Invocations), + 10 => Ok(Self::Attachments), + 11 => Ok(Self::Finalizations), + 12 => Ok(Self::Closures), + 13 => Ok(Self::SelectorDomains), + 14 => Ok(Self::SelectorMemberships), + 15 => Ok(Self::ReductionGroups), + 16 => Ok(Self::ReductionEntries), + 17 => Ok(Self::ExactFactors), + 18 => Ok(Self::InspectionSummary), + _ => Err(RusticolError::compatibility(format!( + "unknown eager runtime section kind: {value}" + ))), + } + } +} + +/// Header preceding every fixed-width eager runtime section. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct EagerSectionHeader { + kind: EagerSectionKind, + record_size: u32, + record_count: u64, + payload_length: u64, +} + +impl EagerSectionHeader { + pub fn new( + kind: EagerSectionKind, + record_size: u32, + record_count: u64, + ) -> RusticolResult { + if record_size == 0 { + return Err(RusticolError::invalid_argument( + "eager section record size must be positive", + )); + } + let payload_length = u64::from(record_size) + .checked_mul(record_count) + .ok_or_else(|| { + RusticolError::invalid_argument("eager section payload length exceeds u64") + })?; + Ok(Self { + kind, + record_size, + record_count, + payload_length, + }) + } + + pub fn kind(&self) -> EagerSectionKind { + self.kind + } + + pub fn record_size(&self) -> u32 { + self.record_size + } + + pub fn record_count(&self) -> u64 { + self.record_count + } + + pub fn payload_length(&self) -> u64 { + self.payload_length + } + + pub fn encode(&self) -> [u8; EAGER_SECTION_HEADER_SIZE] { + let mut bytes = [0_u8; EAGER_SECTION_HEADER_SIZE]; + bytes[0..8].copy_from_slice(SECTION_MAGIC); + put_u16(&mut bytes, 8, EAGER_SECTION_SCHEMA); + put_u16(&mut bytes, 10, EAGER_SECTION_HEADER_SIZE as u16); + put_u16(&mut bytes, 12, self.kind as u16); + put_u16(&mut bytes, 14, SUPPORTED_FLAGS); + put_u32(&mut bytes, 16, self.record_size); + put_u64(&mut bytes, 24, self.record_count); + put_u64(&mut bytes, 32, EAGER_SECTION_HEADER_SIZE as u64); + put_u64(&mut bytes, 40, self.payload_length); + bytes + } + + /// Decode and validate one complete section, returning its record payload. + pub fn decode(section: &[u8]) -> RusticolResult<(Self, &[u8])> { + if section.len() < EAGER_SECTION_HEADER_SIZE { + return Err(RusticolError::integrity( + "truncated eager runtime section header", + )); + } + let header = §ion[..EAGER_SECTION_HEADER_SIZE]; + if &header[0..8] != SECTION_MAGIC { + return Err(RusticolError::integrity( + "invalid eager runtime section magic", + )); + } + let schema = read_u16(header, 8, "eager section schema")?; + if schema != EAGER_SECTION_SCHEMA { + return Err(RusticolError::compatibility(format!( + "unsupported eager runtime section schema: {schema}" + ))); + } + let header_size = read_u16(header, 10, "eager section header size")?; + if usize::from(header_size) != EAGER_SECTION_HEADER_SIZE { + return Err(RusticolError::compatibility(format!( + "unsupported eager runtime section header size: {header_size}" + ))); + } + let kind = EagerSectionKind::parse(read_u16(header, 12, "eager section kind")?)?; + let flags = read_u16(header, 14, "eager section flags")?; + if flags != SUPPORTED_FLAGS { + return Err(RusticolError::compatibility(format!( + "unknown eager runtime section flags: {flags}" + ))); + } + let record_size = read_u32(header, 16, "eager section record size")?; + if record_size == 0 { + return Err(RusticolError::integrity( + "eager section record size must be positive", + )); + } + if read_u32(header, 20, "eager section reserved field")? != 0 + || header[48..64].iter().any(|value| *value != 0) + { + return Err(RusticolError::integrity( + "eager section reserved fields must be zero", + )); + } + let record_count = read_u64(header, 24, "eager section record count")?; + let payload_offset = read_u64(header, 32, "eager section payload offset")?; + if payload_offset != EAGER_SECTION_HEADER_SIZE as u64 { + return Err(RusticolError::integrity( + "eager section payload offset is not canonical", + )); + } + let payload_length = read_u64(header, 40, "eager section payload length")?; + let expected_payload_length = u64::from(record_size) + .checked_mul(record_count) + .ok_or_else(|| RusticolError::integrity("eager section payload length exceeds u64"))?; + if payload_length != expected_payload_length { + return Err(RusticolError::integrity( + "eager section payload length disagrees with record shape", + )); + } + let expected_total = payload_offset + .checked_add(payload_length) + .ok_or_else(|| RusticolError::integrity("eager section size exceeds u64"))?; + let actual_total = u64::try_from(section.len()) + .map_err(|_| RusticolError::integrity("eager section size exceeds u64"))?; + if actual_total < expected_total { + return Err(RusticolError::integrity( + "truncated eager runtime section payload", + )); + } + if actual_total > expected_total { + return Err(RusticolError::integrity( + "eager runtime section has trailing bytes", + )); + } + let payload_start = usize::try_from(payload_offset).map_err(|_| { + RusticolError::integrity("eager section offset exceeds platform bounds") + })?; + Ok(( + Self { + kind, + record_size, + record_count, + payload_length, + }, + §ion[payload_start..], + )) + } +} + +/// Fixed-width reference to a contiguous record range in another section. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct EagerSectionReference { + kind: EagerSectionKind, + start: u64, + count: u64, +} + +impl EagerSectionReference { + pub fn new(kind: EagerSectionKind, start: u64, count: u64) -> RusticolResult { + start.checked_add(count).ok_or_else(|| { + RusticolError::invalid_argument("eager section reference exceeds u64") + })?; + Ok(Self { kind, start, count }) + } + + pub fn kind(&self) -> EagerSectionKind { + self.kind + } + + pub fn start(&self) -> u64 { + self.start + } + + pub fn count(&self) -> u64 { + self.count + } + + pub fn encode(&self) -> [u8; EAGER_SECTION_REFERENCE_SIZE] { + let mut bytes = [0_u8; EAGER_SECTION_REFERENCE_SIZE]; + put_u16(&mut bytes, 0, self.kind as u16); + put_u16(&mut bytes, 2, SUPPORTED_FLAGS); + put_u64(&mut bytes, 8, self.start); + put_u64(&mut bytes, 16, self.count); + bytes + } + + pub fn decode(bytes: &[u8]) -> RusticolResult { + if bytes.len() < EAGER_SECTION_REFERENCE_SIZE { + return Err(RusticolError::integrity( + "truncated eager section reference", + )); + } + if bytes.len() > EAGER_SECTION_REFERENCE_SIZE { + return Err(RusticolError::integrity( + "eager section reference has trailing bytes", + )); + } + let kind = EagerSectionKind::parse(read_u16(bytes, 0, "eager reference kind")?)?; + let flags = read_u16(bytes, 2, "eager reference flags")?; + if flags != SUPPORTED_FLAGS { + return Err(RusticolError::compatibility(format!( + "unknown eager section reference flags: {flags}" + ))); + } + if read_u32(bytes, 4, "eager reference reserved field")? != 0 { + return Err(RusticolError::integrity( + "eager section reference reserved field must be zero", + )); + } + let start = read_u64(bytes, 8, "eager reference start")?; + let count = read_u64(bytes, 16, "eager reference count")?; + Self::new(kind, start, count).map_err(|error| RusticolError::integrity(error.to_string())) + } + + pub fn validate_against(&self, target: &EagerSectionHeader) -> RusticolResult<()> { + if self.kind != target.kind { + return Err(RusticolError::integrity(format!( + "eager section reference kind {:?} does not match target {:?}", + self.kind, target.kind + ))); + } + let end = self + .start + .checked_add(self.count) + .ok_or_else(|| RusticolError::integrity("eager section reference exceeds u64"))?; + if self.start > target.record_count || end > target.record_count { + return Err(RusticolError::integrity( + "eager section reference exceeds target record bounds", + )); + } + Ok(()) + } +} + +fn read_u16(bytes: &[u8], offset: usize, label: &str) -> RusticolResult { + let value: [u8; 2] = checked_slice(bytes, offset, 2, label)? + .try_into() + .map_err(|_| RusticolError::integrity(format!("truncated {label}")))?; + Ok(u16::from_le_bytes(value)) +} + +fn read_u32(bytes: &[u8], offset: usize, label: &str) -> RusticolResult { + let value: [u8; 4] = checked_slice(bytes, offset, 4, label)? + .try_into() + .map_err(|_| RusticolError::integrity(format!("truncated {label}")))?; + Ok(u32::from_le_bytes(value)) +} + +fn read_u64(bytes: &[u8], offset: usize, label: &str) -> RusticolResult { + let value: [u8; 8] = checked_slice(bytes, offset, 8, label)? + .try_into() + .map_err(|_| RusticolError::integrity(format!("truncated {label}")))?; + Ok(u64::from_le_bytes(value)) +} + +fn checked_slice<'a>( + bytes: &'a [u8], + offset: usize, + length: usize, + label: &str, +) -> RusticolResult<&'a [u8]> { + let end = offset + .checked_add(length) + .ok_or_else(|| RusticolError::integrity(format!("{label} range overflow")))?; + bytes + .get(offset..end) + .ok_or_else(|| RusticolError::integrity(format!("truncated {label}"))) +} + +fn put_u16(bytes: &mut [u8], offset: usize, value: u16) { + bytes[offset..offset + 2].copy_from_slice(&value.to_le_bytes()); +} + +fn put_u32(bytes: &mut [u8], offset: usize, value: u32) { + bytes[offset..offset + 4].copy_from_slice(&value.to_le_bytes()); +} + +fn put_u64(bytes: &mut [u8], offset: usize, value: u64) { + bytes[offset..offset + 8].copy_from_slice(&value.to_le_bytes()); +} + +#[cfg(test)] +#[path = "eager_layout_tests.rs"] +mod tests; diff --git a/pyamplicol/rust/crates/rusticol-core/src/eager_layout_tests.rs b/pyamplicol/rust/crates/rusticol-core/src/eager_layout_tests.rs new file mode 100644 index 0000000000..e3ec767fb9 --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/eager_layout_tests.rs @@ -0,0 +1,233 @@ +// SPDX-License-Identifier: 0BSD + +use super::*; +use crate::RusticolErrorKind; + +fn section(kind: EagerSectionKind, record_size: u32, record_count: u64) -> Vec { + let header = EagerSectionHeader::new(kind, record_size, record_count).unwrap(); + let mut bytes = header.encode().to_vec(); + bytes.resize( + EAGER_SECTION_HEADER_SIZE + header.payload_length() as usize, + 0x5a, + ); + bytes +} + +fn assert_error(bytes: &[u8], kind: RusticolErrorKind, message: &str) { + let error = EagerSectionHeader::decode(bytes).unwrap_err(); + assert_eq!(error.kind(), kind); + assert!( + error.to_string().contains(message), + "expected {:?} to contain {message:?}", + error.to_string() + ); +} + +#[test] +fn public_contract_constants_are_stable() { + assert_eq!( + EAGER_LOWERING_INPUT_ABI, + "pyamplicol-eager-lowering-input-v1" + ); + assert_eq!(EAGER_PLAN_ABI, "pyamplicol-eager-plan-v3"); + assert_eq!( + EAGER_RUNTIME_LAYOUT_ABI, + "pyamplicol-eager-runtime-layout-v1" + ); + assert_eq!( + EAGER_RUNTIME_CAPABILITY, + "rusticol.eager-runtime-layout.complex-f64.v1" + ); + assert_eq!( + EAGER_RUNTIME_CONTAINER_KIND, + "pyamplicol-eager-runtime-container" + ); + assert_eq!(EAGER_RUNTIME_CONTAINER_SCHEMA, 1); +} + +#[test] +fn section_header_is_deterministic_little_endian_and_round_trips() { + let bytes = section(EagerSectionKind::Invocations, 40, 3); + assert_eq!(&bytes[0..8], b"PACERT\0\0"); + assert_eq!(&bytes[8..10], &1_u16.to_le_bytes()); + assert_eq!(&bytes[10..12], &64_u16.to_le_bytes()); + assert_eq!(&bytes[12..14], &9_u16.to_le_bytes()); + assert_eq!(&bytes[16..20], &40_u32.to_le_bytes()); + assert_eq!(&bytes[24..32], &3_u64.to_le_bytes()); + assert_eq!(&bytes[32..40], &64_u64.to_le_bytes()); + assert_eq!(&bytes[40..48], &120_u64.to_le_bytes()); + let (header, payload) = EagerSectionHeader::decode(&bytes).unwrap(); + assert_eq!(header.kind(), EagerSectionKind::Invocations); + assert_eq!(header.record_size(), 40); + assert_eq!(header.record_count(), 3); + assert_eq!(header.payload_length(), 120); + assert_eq!(payload, vec![0x5a; 120]); + assert_eq!(header.encode(), bytes[..EAGER_SECTION_HEADER_SIZE]); +} + +#[test] +fn zero_record_sections_are_valid_but_zero_width_and_overflow_are_rejected() { + let bytes = section(EagerSectionKind::Metadata, 16, 0); + let (header, payload) = EagerSectionHeader::decode(&bytes).unwrap(); + assert_eq!(header.record_count(), 0); + assert!(payload.is_empty()); + + let zero = EagerSectionHeader::new(EagerSectionKind::Metadata, 0, 1).unwrap_err(); + assert_eq!(zero.kind(), RusticolErrorKind::InvalidArgument); + assert!(zero.to_string().contains("record size")); + let overflow = + EagerSectionHeader::new(EagerSectionKind::Metadata, u32::MAX, u64::MAX).unwrap_err(); + assert_eq!(overflow.kind(), RusticolErrorKind::InvalidArgument); + assert!(overflow.to_string().contains("exceeds u64")); +} + +#[test] +fn section_header_rejects_contract_corruption_and_noncanonical_lengths() { + let golden = section(EagerSectionKind::Attachments, 24, 2); + for (offset, replacement, kind, message) in [ + (0, vec![b'X'], RusticolErrorKind::Integrity, "magic"), + ( + 8, + 2_u16.to_le_bytes().to_vec(), + RusticolErrorKind::Compatibility, + "schema", + ), + ( + 10, + 32_u16.to_le_bytes().to_vec(), + RusticolErrorKind::Compatibility, + "header size", + ), + ( + 12, + 99_u16.to_le_bytes().to_vec(), + RusticolErrorKind::Compatibility, + "section kind", + ), + ( + 14, + 1_u16.to_le_bytes().to_vec(), + RusticolErrorKind::Compatibility, + "flags", + ), + ( + 16, + 0_u32.to_le_bytes().to_vec(), + RusticolErrorKind::Integrity, + "record size", + ), + ( + 20, + 1_u32.to_le_bytes().to_vec(), + RusticolErrorKind::Integrity, + "reserved", + ), + ( + 32, + 63_u64.to_le_bytes().to_vec(), + RusticolErrorKind::Integrity, + "payload offset", + ), + ( + 40, + 47_u64.to_le_bytes().to_vec(), + RusticolErrorKind::Integrity, + "payload length", + ), + ] { + let mut bytes = golden.clone(); + bytes[offset..offset + replacement.len()].copy_from_slice(&replacement); + assert_error(&bytes, kind, message); + } + let mut reserved = golden.clone(); + reserved[63] = 1; + assert_error(&reserved, RusticolErrorKind::Integrity, "reserved fields"); + assert_error( + &golden[..golden.len() - 1], + RusticolErrorKind::Integrity, + "truncated", + ); + let mut trailing = golden; + trailing.push(0); + assert_error(&trailing, RusticolErrorKind::Integrity, "trailing bytes"); +} + +#[test] +fn section_references_round_trip_and_validate_bounds_and_kind() { + let target = EagerSectionHeader::new(EagerSectionKind::Invocations, 40, 100).unwrap(); + let reference = EagerSectionReference::new(EagerSectionKind::Invocations, 10, 20).unwrap(); + let bytes = reference.encode(); + assert_eq!(&bytes[0..2], &9_u16.to_le_bytes()); + assert_eq!(&bytes[8..16], &10_u64.to_le_bytes()); + assert_eq!(&bytes[16..24], &20_u64.to_le_bytes()); + let decoded = EagerSectionReference::decode(&bytes).unwrap(); + assert_eq!(decoded, reference); + decoded.validate_against(&target).unwrap(); + + EagerSectionReference::new(EagerSectionKind::Invocations, 100, 0) + .unwrap() + .validate_against(&target) + .unwrap(); + let out_of_bounds = EagerSectionReference::new(EagerSectionKind::Invocations, 99, 2) + .unwrap() + .validate_against(&target) + .unwrap_err(); + assert_eq!(out_of_bounds.kind(), RusticolErrorKind::Integrity); + assert!(out_of_bounds.to_string().contains("bounds")); + let wrong_kind = EagerSectionReference::new(EagerSectionKind::Closures, 0, 1) + .unwrap() + .validate_against(&target) + .unwrap_err(); + assert_eq!(wrong_kind.kind(), RusticolErrorKind::Integrity); + assert!(wrong_kind.to_string().contains("does not match")); + let overflow = + EagerSectionReference::new(EagerSectionKind::Invocations, u64::MAX, 1).unwrap_err(); + assert_eq!(overflow.kind(), RusticolErrorKind::InvalidArgument); +} + +#[test] +fn section_reference_rejects_corruption() { + let golden = EagerSectionReference::new(EagerSectionKind::Closures, 2, 3) + .unwrap() + .encode(); + for (offset, replacement, kind, message) in [ + ( + 0, + 99_u16.to_le_bytes().to_vec(), + RusticolErrorKind::Compatibility, + "kind", + ), + ( + 2, + 1_u16.to_le_bytes().to_vec(), + RusticolErrorKind::Compatibility, + "flags", + ), + ( + 4, + 1_u32.to_le_bytes().to_vec(), + RusticolErrorKind::Integrity, + "reserved", + ), + ] { + let mut bytes = golden; + bytes[offset..offset + replacement.len()].copy_from_slice(&replacement); + let error = EagerSectionReference::decode(&bytes).unwrap_err(); + assert_eq!(error.kind(), kind); + assert!(error.to_string().contains(message)); + } + assert!( + EagerSectionReference::decode(&golden[..golden.len() - 1]) + .unwrap_err() + .to_string() + .contains("truncated") + ); + let mut trailing = golden.to_vec(); + trailing.push(0); + assert!( + EagerSectionReference::decode(&trailing) + .unwrap_err() + .to_string() + .contains("trailing") + ); +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/eager_lowering_v3.rs b/pyamplicol/rust/crates/rusticol-core/src/eager_lowering_v3.rs new file mode 100644 index 0000000000..8c973d4d9e --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/eager_lowering_v3.rs @@ -0,0 +1,4124 @@ +// SPDX-License-Identifier: 0BSD + +//! Validated columnar input and deterministic lowering for eager plan v3. +//! +//! This module deliberately stops at an owned, fixed-width in-memory plan. It +//! neither writes PACBIN nor replaces the plan-v2 execution runtime. The Python +//! binding can borrow contiguous NumPy columns through the views below, build a +//! validated owned input, release the GIL, and call [`lower_eager_plan_v3`]. + +use crate::eager_layout::{ + EAGER_LOWERING_INPUT_ABI, EAGER_PLAN_ABI, EagerSectionHeader, EagerSectionKind, +}; +use crate::{MISSING_U32, RusticolError, RusticolResult}; +use std::cmp::Ordering; +use std::collections::{BTreeMap, BTreeSet}; + +const EXACT_SOURCE_BINARY64: u8 = 0; +const EXACT_SOURCE_CANONICAL_IR: u8 = 1; +const OUTPUT_FACTOR_NONE: u8 = 0; +const OUTPUT_FACTOR_COUPLING_REAL: u8 = 1; +const OUTPUT_FACTOR_COUPLING_IMAG: u8 = 2; + +const CURRENT_FLAG_SOURCE: u32 = 1 << 0; + +/// Complete, sorted table set defined by `pyamplicol-eager-lowering-input-v1`. +/// +/// The binding supplies retained fragments for every table. Columns consumed +/// by typed views may be omitted from a fragment; all other columns are copied +/// bit-for-bit into the owned input and plan. +pub const EAGER_LOWERING_V1_TABLE_NAMES: &[&str] = &[ + "bitset_ranges", + "bitset_words", + "coherent_groups", + "color_contraction_entries", + "color_contraction_metadata", + "color_open_lines", + "color_plan_diagnostics", + "color_replay_contraction_component_groups", + "color_replay_contraction_entries", + "color_replay_contraction_metadata", + "color_replay_mappings", + "color_replay_metadata", + "color_replay_permutations", + "color_replay_physical_groups", + "color_replay_routes", + "color_sectors", + "color_selectors", + "contraction_coefficients", + "coupling_orders", + "couplings", + "currents", + "exact_factors", + "helicity_amplitude_classes", + "helicity_amplitude_member_domains", + "helicity_amplitude_members", + "helicity_domain_states", + "helicity_domains", + "helicity_materialization_metadata", + "helicity_materialized_amplitude_domains", + "helicity_materialized_amplitude_routes", + "helicity_materialized_current_map", + "helicity_materialized_schedules", + "helicity_materialized_source_routes", + "helicity_proof_diagnostics", + "helicity_proof_metadata", + "helicity_recurrence_classes", + "helicity_recurrence_members", + "helicity_residual_currents", + "helicity_residual_roots", + "helicity_selectors", + "helicity_source_mappings", + "helicity_structural_zero_domains", + "i32_sequence_ranges", + "i32_sequence_values", + "interaction_group_members", + "interaction_groups", + "interactions", + "lc_replay_diagnostics", + "lc_replay_members", + "lc_replay_metadata", + "lc_replay_partitions", + "lc_replay_permutations", + "lc_replay_residual_sectors", + "metadata", + "model_parameters", + "momentum_masks", + "quantum_flows", + "reduction_members", + "roots", + "selected_color_sectors", + "selected_source_helicities", + "sources", + "u32_sequence_ranges", + "u32_sequence_values", +]; + +/// Borrowed primitive storage for a retained column fragment. +#[derive(Clone, Copy, Debug)] +pub enum EagerPrimitiveColumnView<'a> { + U8(&'a [u8]), + U32(&'a [u32]), + U64(&'a [u64]), + I32(&'a [i32]), + F64(&'a [f64]), +} + +impl EagerPrimitiveColumnView<'_> { + fn len(self) -> usize { + match self { + Self::U8(values) => values.len(), + Self::U32(values) => values.len(), + Self::U64(values) => values.len(), + Self::I32(values) => values.len(), + Self::F64(values) => values.len(), + } + } +} + +/// One borrowed fixed-shape column not consumed by a typed lowering view. +#[derive(Clone, Copy, Debug)] +pub struct EagerRetainedColumnView<'a> { + pub name: &'a str, + /// Number of primitive values per table row, including trailing shape. + pub elements_per_row: u32, + pub values: EagerPrimitiveColumnView<'a>, +} + +/// Retained fragment for one source table. +#[derive(Clone, Copy, Debug)] +pub struct EagerRetainedTableView<'a> { + pub name: &'a str, + pub row_count: u64, + pub columns: &'a [EagerRetainedColumnView<'a>], +} + +/// Owned primitive storage for retained metadata/proof columns. +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum EagerOwnedPrimitiveColumn { + U8(Vec), + U32(Vec), + U64(Vec), + I32(Vec), + /// Raw IEEE-754 bits preserve signed zero exactly. + F64Bits(Vec), +} + +impl EagerOwnedPrimitiveColumn { + pub fn element_count(&self) -> usize { + match self { + Self::U8(values) => values.len(), + Self::U32(values) => values.len(), + Self::U64(values) | Self::F64Bits(values) => values.len(), + Self::I32(values) => values.len(), + } + } +} + +/// Owned retained column with a deterministic primitive shape. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct EagerOwnedRetainedColumn { + name: Box, + elements_per_row: u32, + values: EagerOwnedPrimitiveColumn, +} + +impl EagerOwnedRetainedColumn { + pub fn name(&self) -> &str { + &self.name + } + + pub fn elements_per_row(&self) -> u32 { + self.elements_per_row + } + + pub fn values(&self) -> &EagerOwnedPrimitiveColumn { + &self.values + } +} + +/// Owned retained table fragment carried unchanged into plan-v3. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct EagerOwnedRetainedTable { + name: Box, + row_count: u64, + columns: Vec, +} + +impl EagerOwnedRetainedTable { + pub fn name(&self) -> &str { + &self.name + } + + pub fn row_count(&self) -> u64 { + self.row_count + } + + pub fn columns(&self) -> &[EagerOwnedRetainedColumn] { + &self.columns + } +} + +/// Borrowed columns from the `currents` input table. +#[derive(Clone, Copy, Debug)] +pub struct EagerCurrentColumns<'a> { + pub id: &'a [u32], + pub dimension: &'a [u32], + pub is_source: &'a [u8], + pub momentum_mask_bitset_id: &'a [u32], + pub propagator_ir_id: &'a [u32], + pub propagator_kernel_id: &'a [u32], +} + +/// Borrowed columns from the `sources` input table. +#[derive(Clone, Copy, Debug)] +pub struct EagerSourceColumns<'a> { + pub source_id: &'a [u32], + pub current_id: &'a [u32], + pub external_label: &'a [u32], + pub input_momentum_slot: &'a [u32], + pub source_ir_id: &'a [u32], + pub crossing_ir_id: &'a [u32], + pub crossing_factor_id: &'a [u32], + pub declared_state_index: &'a [u32], +} + +/// Borrowed columns from the `interactions` input table. +#[derive(Clone, Copy, Debug)] +pub struct EagerInteractionColumns<'a> { + pub id: &'a [u32], + pub stage_subset_size: &'a [u32], + pub left_current_id: &'a [u32], + pub right_current_id: &'a [u32], + pub result_current_id: &'a [u32], + pub coupling_id: &'a [u32], + pub coupling_factor_id: &'a [u32], + pub color_factor_id: &'a [u32], + pub evaluation_factor_id: &'a [u32], + pub evaluation_group_id: &'a [u32], + pub full_tensor_network_ready: &'a [u8], + pub kernel_id: &'a [u32], + pub canonical_input_order: &'a [[u8; 2]], + pub kernel_normalization_factor_id: &'a [u32], + pub output_factor_source: &'a [u8], +} + +/// Borrowed columns from the `roots` input table. +#[derive(Clone, Copy, Debug)] +pub struct EagerRootColumns<'a> { + pub id: &'a [u32], + pub left_current_id: &'a [u32], + pub right_current_id: &'a [u32], + pub color_factor_id: &'a [u32], + pub contraction_ir_id: &'a [u32], + pub coupling_id: &'a [u32], + pub coupling_factor_id: &'a [u32], + pub helicity_weight_factor_id: &'a [u32], + pub coherent_group_id: &'a [u32], + pub kernel_id: &'a [u32], + pub canonical_input_order: &'a [[u8; 2]], + pub kernel_normalization_factor_id: &'a [u32], + pub output_factor_source: &'a [u8], +} + +/// Borrowed columns from `interaction_groups` and +/// `interaction_group_members`. +#[derive(Clone, Copy, Debug)] +pub struct EagerInteractionGroupColumns<'a> { + pub representative_interaction_id: &'a [u32], + pub member_start: &'a [u64], + pub member_count: &'a [u64], + pub member_interaction_id: &'a [u32], +} + +/// Borrowed columns from the `momentum_masks` table. +#[derive(Clone, Copy, Debug)] +pub struct EagerMomentumMaskColumns<'a> { + pub slot_id: &'a [u32], + pub bitset_id: &'a [u32], +} + +/// Borrowed arbitrary-width bitset catalog. +#[derive(Clone, Copy, Debug)] +pub struct EagerBitsetColumns<'a> { + pub start: &'a [u64], + pub count: &'a [u64], + pub bit_count: &'a [u64], + pub words: &'a [u64], +} + +/// Borrowed unsigned sequence catalog. +#[derive(Clone, Copy, Debug)] +pub struct EagerU32SequenceColumns<'a> { + pub start: &'a [u64], + pub count: &'a [u64], + pub values: &'a [u32], +} + +/// Borrowed signed sequence catalog. +#[derive(Clone, Copy, Debug)] +pub struct EagerI32SequenceColumns<'a> { + pub start: &'a [u64], + pub count: &'a [u64], + pub values: &'a [i32], +} + +/// Borrowed columns from the `exact_factors` table. +#[derive(Clone, Copy, Debug)] +pub struct EagerExactFactorColumns<'a> { + pub real: &'a [f64], + pub imaginary: &'a [f64], + pub canonical_string_id: &'a [u32], + pub exact_source: &'a [u8], + pub exact_ir_id: &'a [u32], + pub source_ir_id: &'a [u32], +} + +/// Borrowed columns from the `couplings` table. +#[derive(Clone, Copy, Debug)] +pub struct EagerCouplingColumns<'a> { + pub constant_factor_id: &'a [u32], + pub parameter_name_ids_sequence_id: &'a [u32], +} + +/// Borrowed columns from the `model_parameters` table. +#[derive(Clone, Copy, Debug)] +pub struct EagerModelParameterColumns<'a> { + pub name_string_id: &'a [u32], + pub kind_string_id: &'a [u32], + pub default_value: &'a [f64], + pub default_factor_id: &'a [u32], + pub runtime_name_string_id: &'a [u32], + pub complex_component: &'a [i32], + pub derived: &'a [u8], +} + +/// Borrowed columns from the `contraction_coefficients` table. +#[derive(Clone, Copy, Debug)] +pub struct EagerContractionCoefficientColumns<'a> { + pub contraction_ir_id: &'a [u32], + pub component_index: &'a [u32], + pub factor_id: &'a [u32], +} + +/// Borrowed columns from the `coherent_groups` table. +#[derive(Clone, Copy, Debug)] +pub struct EagerCoherentGroupColumns<'a> { + pub id: &'a [u32], + pub helicity_weight_factor_id: &'a [u32], + pub all_sector_weight_factor_id: &'a [u32], +} + +/// Borrowed columns from the `helicity_selectors` table. +#[derive(Clone, Copy, Debug)] +pub struct EagerHelicitySelectorColumns<'a> { + pub values_sequence_id: &'a [u32], + pub representative_sequence_id: &'a [u32], + pub coefficient_factor_id: &'a [u32], + pub computed: &'a [u8], + pub structural_zero: &'a [u8], +} + +/// Borrowed columns from the `color_selectors` table. +#[derive(Clone, Copy, Debug)] +pub struct EagerColorSelectorColumns<'a> { + pub word_sequence_id: &'a [u32], + pub representative_word_sequence_id: &'a [u32], + pub coefficient_factor_id: &'a [u32], + pub computed: &'a [u8], +} + +/// Borrowed columns from the `reduction_members` table. +#[derive(Clone, Copy, Debug)] +pub struct EagerReductionMemberColumns<'a> { + pub coherent_group_id: &'a [u32], + pub helicity_selector_id: &'a [u32], + pub color_selector_id: &'a [u32], +} + +/// Scalar row from `color_contraction_metadata`. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct EagerColorContractionMetadata { + pub present: u8, + pub supported: u8, + pub group_count: u64, + pub includes_color_factor: u8, +} + +/// Borrowed columns from `color_contraction_entries`. +#[derive(Clone, Copy, Debug)] +pub struct EagerColorContractionEntryColumns<'a> { + pub left_group_id: &'a [u32], + pub right_group_id: &'a [u32], + pub weight_factor_id: &'a [u32], + pub symmetry_factor_id: &'a [u32], +} + +/// Complete borrowed core view of `pyamplicol-eager-lowering-input-v1`. +#[derive(Clone, Copy, Debug)] +pub struct EagerLoweringInputV1View<'a> { + pub abi: &'a str, + pub process_key: &'a str, + pub model_name: &'a str, + pub string_catalog: &'a [&'a str], + pub canonical_ir_catalog: &'a [&'a str], + pub semantic_limitations: &'a [&'a str], + /// Exact v1 table inventory with unconsumed columns retained by the writer. + pub retained_tables: &'a [EagerRetainedTableView<'a>], + /// Tables the binding cannot represent. Any entry makes construction fail. + pub unsupported_tables: &'a [&'a str], + pub currents: EagerCurrentColumns<'a>, + pub sources: EagerSourceColumns<'a>, + pub interactions: EagerInteractionColumns<'a>, + pub roots: EagerRootColumns<'a>, + pub interaction_groups: EagerInteractionGroupColumns<'a>, + pub momentum_masks: EagerMomentumMaskColumns<'a>, + pub bitsets: EagerBitsetColumns<'a>, + pub u32_sequences: EagerU32SequenceColumns<'a>, + pub i32_sequences: EagerI32SequenceColumns<'a>, + pub exact_factors: EagerExactFactorColumns<'a>, + pub couplings: EagerCouplingColumns<'a>, + pub model_parameters: EagerModelParameterColumns<'a>, + pub contraction_coefficients: EagerContractionCoefficientColumns<'a>, + pub coherent_groups: EagerCoherentGroupColumns<'a>, + pub helicity_selectors: EagerHelicitySelectorColumns<'a>, + pub color_selectors: EagerColorSelectorColumns<'a>, + pub reduction_members: EagerReductionMemberColumns<'a>, + pub color_contraction: EagerColorContractionMetadata, + pub color_contraction_entries: EagerColorContractionEntryColumns<'a>, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +struct InputCurrent { + id: u32, + dimension: u32, + is_source: bool, + momentum_mask_bitset_id: u32, + propagator_ir_id: u32, + propagator_kernel_id: u32, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +struct InputSource { + source_id: u32, + current_id: u32, + external_label: u32, + input_momentum_slot: u32, + source_ir_id: u32, + crossing_ir_id: u32, + crossing_factor_id: u32, + declared_state_index: u32, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +struct InputInteraction { + id: u32, + stage_subset_size: u32, + left_current_id: u32, + right_current_id: u32, + result_current_id: u32, + coupling_id: u32, + coupling_factor_id: u32, + color_factor_id: u32, + evaluation_factor_id: u32, + evaluation_group_id: u32, + kernel_id: u32, + canonical_input_order: [u8; 2], + kernel_normalization_factor_id: u32, + output_factor_source: u8, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +struct InputRoot { + id: u32, + left_current_id: u32, + right_current_id: u32, + color_factor_id: u32, + contraction_ir_id: u32, + coupling_id: u32, + coupling_factor_id: u32, + helicity_weight_factor_id: u32, + coherent_group_id: u32, + kernel_id: u32, + canonical_input_order: [u8; 2], + kernel_normalization_factor_id: u32, + output_factor_source: u8, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +struct InputRange { + start: u64, + count: u64, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +struct InputInteractionGroup { + representative_interaction_id: u32, + members: InputRange, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +struct InputBitsetRange { + range: InputRange, + bit_count: u64, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +struct InputExactFactor { + real_bits: u64, + imaginary_bits: u64, + canonical_string_id: u32, + exact_source: u8, + exact_ir_id: u32, + source_ir_id: u32, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +struct InputCoupling { + constant_factor_id: u32, + parameter_name_ids_sequence_id: u32, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +struct InputModelParameter { + name_string_id: u32, + kind_string_id: u32, + default_bits: u64, + default_factor_id: u32, + runtime_name_string_id: u32, + complex_component: i32, + derived: bool, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +struct InputContractionCoefficient { + contraction_ir_id: u32, + component_index: u32, + factor_id: u32, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +struct InputCoherentGroup { + id: u32, + helicity_weight_factor_id: u32, + all_sector_weight_factor_id: u32, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +struct InputHelicitySelector { + values_sequence_id: u32, + representative_sequence_id: u32, + coefficient_factor_id: u32, + computed: bool, + structural_zero: bool, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +struct InputColorSelector { + word_sequence_id: u32, + representative_word_sequence_id: u32, + coefficient_factor_id: u32, + computed: bool, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +struct InputReductionMember { + coherent_group_id: u32, + helicity_selector_id: u32, + color_selector_id: u32, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +struct InputColorContractionEntry { + left_group_id: u32, + right_group_id: u32, + weight_factor_id: u32, + symmetry_factor_id: u32, +} + +/// Owned, fully validated core representation of the v1 lowering input. +/// +/// Fields are private so an instance cannot be put back into an invalid state. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct EagerLoweringInputV1 { + process_key: String, + model_name: String, + string_catalog: Vec>, + canonical_ir_catalog: Vec>, + semantic_limitations: Vec>, + retained_tables: Vec, + string_count: u32, + canonical_ir_count: u32, + currents: Vec, + sources: Vec, + interactions: Vec, + roots: Vec, + interaction_groups: Vec, + interaction_group_members: Vec, + momentum_masks: Vec<(u32, u32)>, + bitset_ranges: Vec, + bitset_words: Vec, + u32_sequence_ranges: Vec, + u32_sequence_values: Vec, + i32_sequence_ranges: Vec, + i32_sequence_values: Vec, + exact_factors: Vec, + couplings: Vec, + model_parameters: Vec, + contraction_coefficients: Vec, + coherent_groups: Vec, + helicity_selectors: Vec, + color_selectors: Vec, + reduction_members: Vec, + color_contraction: EagerColorContractionMetadata, + color_contraction_entries: Vec, +} + +impl EagerLoweringInputV1 { + /// Validate borrowed columns and copy them into a compact owned form. + /// + /// Bindings should construct the view directly over borrowed NumPy columns + /// while holding the GIL, then call this method exactly once. The returned + /// input owns every payload and may be lowered after releasing the GIL. + pub fn try_from_view(view: EagerLoweringInputV1View<'_>) -> RusticolResult { + if view.abi != EAGER_LOWERING_INPUT_ABI { + return Err(invalid(format!( + "unsupported eager lowering input ABI {:?}", + view.abi + ))); + } + if view.process_key.is_empty() { + return Err(invalid("eager lowering process key must not be empty")); + } + if view.model_name.is_empty() { + return Err(invalid("eager lowering model name must not be empty")); + } + if view.semantic_limitations.is_empty() { + return Err(invalid( + "eager lowering exact-factor limitations must not be empty", + )); + } + if !view.unsupported_tables.is_empty() { + return Err(invalid(format!( + "eager lowering input contains unsupported tables: {}", + view.unsupported_tables.join(", ") + ))); + } + let string_catalog = copy_string_catalog(view.string_catalog, "string catalog")?; + let canonical_ir_catalog = + copy_string_catalog(view.canonical_ir_catalog, "canonical IR catalog")?; + let semantic_limitations = + copy_string_list(view.semantic_limitations, "semantic limitations")?; + let retained_tables = copy_retained_tables(view.retained_tables)?; + let string_count = usize_u32(string_catalog.len(), "string catalog count")?; + let canonical_ir_count = + usize_u32(canonical_ir_catalog.len(), "canonical IR catalog count")?; + + let current_count = equal_column_lengths( + "currents", + &[ + ("id", view.currents.id.len()), + ("dimension", view.currents.dimension.len()), + ("is_source", view.currents.is_source.len()), + ( + "momentum_mask_bitset_id", + view.currents.momentum_mask_bitset_id.len(), + ), + ("propagator_ir_id", view.currents.propagator_ir_id.len()), + ( + "propagator_kernel_id", + view.currents.propagator_kernel_id.len(), + ), + ], + )?; + let mut currents = reserved(current_count, "currents")?; + for row in 0..current_count { + currents.push(InputCurrent { + id: view.currents.id[row], + dimension: view.currents.dimension[row], + is_source: bool_column(view.currents.is_source[row], "currents.is_source", row)?, + momentum_mask_bitset_id: view.currents.momentum_mask_bitset_id[row], + propagator_ir_id: view.currents.propagator_ir_id[row], + propagator_kernel_id: view.currents.propagator_kernel_id[row], + }); + } + + let source_count = equal_column_lengths( + "sources", + &[ + ("source_id", view.sources.source_id.len()), + ("current_id", view.sources.current_id.len()), + ("external_label", view.sources.external_label.len()), + ( + "input_momentum_slot", + view.sources.input_momentum_slot.len(), + ), + ("source_ir_id", view.sources.source_ir_id.len()), + ("crossing_ir_id", view.sources.crossing_ir_id.len()), + ("crossing_factor_id", view.sources.crossing_factor_id.len()), + ( + "declared_state_index", + view.sources.declared_state_index.len(), + ), + ], + )?; + let mut sources = reserved(source_count, "sources")?; + for row in 0..source_count { + sources.push(InputSource { + source_id: view.sources.source_id[row], + current_id: view.sources.current_id[row], + external_label: view.sources.external_label[row], + input_momentum_slot: view.sources.input_momentum_slot[row], + source_ir_id: view.sources.source_ir_id[row], + crossing_ir_id: view.sources.crossing_ir_id[row], + crossing_factor_id: view.sources.crossing_factor_id[row], + declared_state_index: view.sources.declared_state_index[row], + }); + } + + let interaction_count = equal_column_lengths( + "interactions", + &[ + ("id", view.interactions.id.len()), + ( + "stage_subset_size", + view.interactions.stage_subset_size.len(), + ), + ("left_current_id", view.interactions.left_current_id.len()), + ("right_current_id", view.interactions.right_current_id.len()), + ( + "result_current_id", + view.interactions.result_current_id.len(), + ), + ("coupling_id", view.interactions.coupling_id.len()), + ( + "coupling_factor_id", + view.interactions.coupling_factor_id.len(), + ), + ("color_factor_id", view.interactions.color_factor_id.len()), + ( + "evaluation_factor_id", + view.interactions.evaluation_factor_id.len(), + ), + ( + "evaluation_group_id", + view.interactions.evaluation_group_id.len(), + ), + ( + "full_tensor_network_ready", + view.interactions.full_tensor_network_ready.len(), + ), + ("kernel_id", view.interactions.kernel_id.len()), + ( + "canonical_input_order", + view.interactions.canonical_input_order.len(), + ), + ( + "kernel_normalization_factor_id", + view.interactions.kernel_normalization_factor_id.len(), + ), + ( + "output_factor_source", + view.interactions.output_factor_source.len(), + ), + ], + )?; + let mut interactions = reserved(interaction_count, "interactions")?; + for row in 0..interaction_count { + if !bool_column( + view.interactions.full_tensor_network_ready[row], + "interactions.full_tensor_network_ready", + row, + )? { + return Err(invalid(format!( + "interaction {row} is not ready for tensor-network lowering" + ))); + } + interactions.push(InputInteraction { + id: view.interactions.id[row], + stage_subset_size: view.interactions.stage_subset_size[row], + left_current_id: view.interactions.left_current_id[row], + right_current_id: view.interactions.right_current_id[row], + result_current_id: view.interactions.result_current_id[row], + coupling_id: view.interactions.coupling_id[row], + coupling_factor_id: view.interactions.coupling_factor_id[row], + color_factor_id: view.interactions.color_factor_id[row], + evaluation_factor_id: view.interactions.evaluation_factor_id[row], + evaluation_group_id: view.interactions.evaluation_group_id[row], + kernel_id: view.interactions.kernel_id[row], + canonical_input_order: view.interactions.canonical_input_order[row], + kernel_normalization_factor_id: view.interactions.kernel_normalization_factor_id + [row], + output_factor_source: view.interactions.output_factor_source[row], + }); + } + + let root_count = equal_column_lengths( + "roots", + &[ + ("id", view.roots.id.len()), + ("left_current_id", view.roots.left_current_id.len()), + ("right_current_id", view.roots.right_current_id.len()), + ("color_factor_id", view.roots.color_factor_id.len()), + ("contraction_ir_id", view.roots.contraction_ir_id.len()), + ("coupling_id", view.roots.coupling_id.len()), + ("coupling_factor_id", view.roots.coupling_factor_id.len()), + ( + "helicity_weight_factor_id", + view.roots.helicity_weight_factor_id.len(), + ), + ("coherent_group_id", view.roots.coherent_group_id.len()), + ("kernel_id", view.roots.kernel_id.len()), + ( + "canonical_input_order", + view.roots.canonical_input_order.len(), + ), + ( + "kernel_normalization_factor_id", + view.roots.kernel_normalization_factor_id.len(), + ), + ( + "output_factor_source", + view.roots.output_factor_source.len(), + ), + ], + )?; + let mut roots = reserved(root_count, "roots")?; + for row in 0..root_count { + roots.push(InputRoot { + id: view.roots.id[row], + left_current_id: view.roots.left_current_id[row], + right_current_id: view.roots.right_current_id[row], + color_factor_id: view.roots.color_factor_id[row], + contraction_ir_id: view.roots.contraction_ir_id[row], + coupling_id: view.roots.coupling_id[row], + coupling_factor_id: view.roots.coupling_factor_id[row], + helicity_weight_factor_id: view.roots.helicity_weight_factor_id[row], + coherent_group_id: view.roots.coherent_group_id[row], + kernel_id: view.roots.kernel_id[row], + canonical_input_order: view.roots.canonical_input_order[row], + kernel_normalization_factor_id: view.roots.kernel_normalization_factor_id[row], + output_factor_source: view.roots.output_factor_source[row], + }); + } + + let group_count = equal_column_lengths( + "interaction_groups", + &[ + ( + "representative_interaction_id", + view.interaction_groups.representative_interaction_id.len(), + ), + ("member_start", view.interaction_groups.member_start.len()), + ("member_count", view.interaction_groups.member_count.len()), + ], + )?; + let mut interaction_groups = reserved(group_count, "interaction groups")?; + for row in 0..group_count { + interaction_groups.push(InputInteractionGroup { + representative_interaction_id: view + .interaction_groups + .representative_interaction_id[row], + members: InputRange { + start: view.interaction_groups.member_start[row], + count: view.interaction_groups.member_count[row], + }, + }); + } + + let momentum_count = equal_column_lengths( + "momentum_masks", + &[ + ("slot_id", view.momentum_masks.slot_id.len()), + ("bitset_id", view.momentum_masks.bitset_id.len()), + ], + )?; + let mut momentum_masks = reserved(momentum_count, "momentum masks")?; + momentum_masks.extend( + view.momentum_masks + .slot_id + .iter() + .copied() + .zip(view.momentum_masks.bitset_id.iter().copied()), + ); + + let bitset_count = equal_column_lengths( + "bitset_ranges", + &[ + ("start", view.bitsets.start.len()), + ("count", view.bitsets.count.len()), + ("bit_count", view.bitsets.bit_count.len()), + ], + )?; + let mut bitset_ranges = reserved(bitset_count, "bitset ranges")?; + for row in 0..bitset_count { + bitset_ranges.push(InputBitsetRange { + range: InputRange { + start: view.bitsets.start[row], + count: view.bitsets.count[row], + }, + bit_count: view.bitsets.bit_count[row], + }); + } + + let u32_sequence_count = equal_column_lengths( + "u32_sequence_ranges", + &[ + ("start", view.u32_sequences.start.len()), + ("count", view.u32_sequences.count.len()), + ], + )?; + let mut u32_sequence_ranges = reserved(u32_sequence_count, "u32 sequences")?; + for row in 0..u32_sequence_count { + u32_sequence_ranges.push(InputRange { + start: view.u32_sequences.start[row], + count: view.u32_sequences.count[row], + }); + } + + let i32_sequence_count = equal_column_lengths( + "i32_sequence_ranges", + &[ + ("start", view.i32_sequences.start.len()), + ("count", view.i32_sequences.count.len()), + ], + )?; + let mut i32_sequence_ranges = reserved(i32_sequence_count, "i32 sequences")?; + for row in 0..i32_sequence_count { + i32_sequence_ranges.push(InputRange { + start: view.i32_sequences.start[row], + count: view.i32_sequences.count[row], + }); + } + + let factor_count = equal_column_lengths( + "exact_factors", + &[ + ("real", view.exact_factors.real.len()), + ("imaginary", view.exact_factors.imaginary.len()), + ( + "canonical_string_id", + view.exact_factors.canonical_string_id.len(), + ), + ("exact_source", view.exact_factors.exact_source.len()), + ("exact_ir_id", view.exact_factors.exact_ir_id.len()), + ("source_ir_id", view.exact_factors.source_ir_id.len()), + ], + )?; + let mut exact_factors = reserved(factor_count, "exact factors")?; + for row in 0..factor_count { + let real = view.exact_factors.real[row]; + let imaginary = view.exact_factors.imaginary[row]; + if !real.is_finite() || !imaginary.is_finite() { + return Err(invalid(format!("exact factor {row} is not finite"))); + } + exact_factors.push(InputExactFactor { + real_bits: real.to_bits(), + imaginary_bits: imaginary.to_bits(), + canonical_string_id: view.exact_factors.canonical_string_id[row], + exact_source: view.exact_factors.exact_source[row], + exact_ir_id: view.exact_factors.exact_ir_id[row], + source_ir_id: view.exact_factors.source_ir_id[row], + }); + } + + let coupling_count = equal_column_lengths( + "couplings", + &[ + ( + "constant_factor_id", + view.couplings.constant_factor_id.len(), + ), + ( + "parameter_name_ids_sequence_id", + view.couplings.parameter_name_ids_sequence_id.len(), + ), + ], + )?; + let mut couplings = reserved(coupling_count, "couplings")?; + for row in 0..coupling_count { + couplings.push(InputCoupling { + constant_factor_id: view.couplings.constant_factor_id[row], + parameter_name_ids_sequence_id: view.couplings.parameter_name_ids_sequence_id[row], + }); + } + + let parameter_count = equal_column_lengths( + "model_parameters", + &[ + ("name_string_id", view.model_parameters.name_string_id.len()), + ("kind_string_id", view.model_parameters.kind_string_id.len()), + ("default_value", view.model_parameters.default_value.len()), + ( + "default_factor_id", + view.model_parameters.default_factor_id.len(), + ), + ( + "runtime_name_string_id", + view.model_parameters.runtime_name_string_id.len(), + ), + ( + "complex_component", + view.model_parameters.complex_component.len(), + ), + ("derived", view.model_parameters.derived.len()), + ], + )?; + let mut model_parameters = reserved(parameter_count, "model parameters")?; + for row in 0..parameter_count { + let default = view.model_parameters.default_value[row]; + if !default.is_finite() { + return Err(invalid(format!( + "model parameter {row} has a non-finite default" + ))); + } + model_parameters.push(InputModelParameter { + name_string_id: view.model_parameters.name_string_id[row], + kind_string_id: view.model_parameters.kind_string_id[row], + default_bits: default.to_bits(), + default_factor_id: view.model_parameters.default_factor_id[row], + runtime_name_string_id: view.model_parameters.runtime_name_string_id[row], + complex_component: view.model_parameters.complex_component[row], + derived: bool_column( + view.model_parameters.derived[row], + "model_parameters.derived", + row, + )?, + }); + } + + let coefficient_count = equal_column_lengths( + "contraction_coefficients", + &[ + ( + "contraction_ir_id", + view.contraction_coefficients.contraction_ir_id.len(), + ), + ( + "component_index", + view.contraction_coefficients.component_index.len(), + ), + ("factor_id", view.contraction_coefficients.factor_id.len()), + ], + )?; + let mut contraction_coefficients = reserved(coefficient_count, "contraction coefficients")?; + for row in 0..coefficient_count { + contraction_coefficients.push(InputContractionCoefficient { + contraction_ir_id: view.contraction_coefficients.contraction_ir_id[row], + component_index: view.contraction_coefficients.component_index[row], + factor_id: view.contraction_coefficients.factor_id[row], + }); + } + + let coherent_count = equal_column_lengths( + "coherent_groups", + &[ + ("id", view.coherent_groups.id.len()), + ( + "helicity_weight_factor_id", + view.coherent_groups.helicity_weight_factor_id.len(), + ), + ( + "all_sector_weight_factor_id", + view.coherent_groups.all_sector_weight_factor_id.len(), + ), + ], + )?; + let mut coherent_groups = reserved(coherent_count, "coherent groups")?; + for row in 0..coherent_count { + coherent_groups.push(InputCoherentGroup { + id: view.coherent_groups.id[row], + helicity_weight_factor_id: view.coherent_groups.helicity_weight_factor_id[row], + all_sector_weight_factor_id: view.coherent_groups.all_sector_weight_factor_id[row], + }); + } + + let helicity_selector_count = equal_column_lengths( + "helicity_selectors", + &[ + ( + "values_sequence_id", + view.helicity_selectors.values_sequence_id.len(), + ), + ( + "representative_sequence_id", + view.helicity_selectors.representative_sequence_id.len(), + ), + ( + "coefficient_factor_id", + view.helicity_selectors.coefficient_factor_id.len(), + ), + ("computed", view.helicity_selectors.computed.len()), + ( + "structural_zero", + view.helicity_selectors.structural_zero.len(), + ), + ], + )?; + let mut helicity_selectors = reserved(helicity_selector_count, "helicity selectors")?; + for row in 0..helicity_selector_count { + helicity_selectors.push(InputHelicitySelector { + values_sequence_id: view.helicity_selectors.values_sequence_id[row], + representative_sequence_id: view.helicity_selectors.representative_sequence_id[row], + coefficient_factor_id: view.helicity_selectors.coefficient_factor_id[row], + computed: bool_column( + view.helicity_selectors.computed[row], + "helicity_selectors.computed", + row, + )?, + structural_zero: bool_column( + view.helicity_selectors.structural_zero[row], + "helicity_selectors.structural_zero", + row, + )?, + }); + } + + let color_selector_count = equal_column_lengths( + "color_selectors", + &[ + ( + "word_sequence_id", + view.color_selectors.word_sequence_id.len(), + ), + ( + "representative_word_sequence_id", + view.color_selectors.representative_word_sequence_id.len(), + ), + ( + "coefficient_factor_id", + view.color_selectors.coefficient_factor_id.len(), + ), + ("computed", view.color_selectors.computed.len()), + ], + )?; + let mut color_selectors = reserved(color_selector_count, "color selectors")?; + for row in 0..color_selector_count { + color_selectors.push(InputColorSelector { + word_sequence_id: view.color_selectors.word_sequence_id[row], + representative_word_sequence_id: view + .color_selectors + .representative_word_sequence_id[row], + coefficient_factor_id: view.color_selectors.coefficient_factor_id[row], + computed: bool_column( + view.color_selectors.computed[row], + "color_selectors.computed", + row, + )?, + }); + } + + let reduction_count = equal_column_lengths( + "reduction_members", + &[ + ( + "coherent_group_id", + view.reduction_members.coherent_group_id.len(), + ), + ( + "helicity_selector_id", + view.reduction_members.helicity_selector_id.len(), + ), + ( + "color_selector_id", + view.reduction_members.color_selector_id.len(), + ), + ], + )?; + let mut reduction_members = reserved(reduction_count, "reduction members")?; + for row in 0..reduction_count { + reduction_members.push(InputReductionMember { + coherent_group_id: view.reduction_members.coherent_group_id[row], + helicity_selector_id: view.reduction_members.helicity_selector_id[row], + color_selector_id: view.reduction_members.color_selector_id[row], + }); + } + + let contraction_entry_count = equal_column_lengths( + "color_contraction_entries", + &[ + ( + "left_group_id", + view.color_contraction_entries.left_group_id.len(), + ), + ( + "right_group_id", + view.color_contraction_entries.right_group_id.len(), + ), + ( + "weight_factor_id", + view.color_contraction_entries.weight_factor_id.len(), + ), + ( + "symmetry_factor_id", + view.color_contraction_entries.symmetry_factor_id.len(), + ), + ], + )?; + let mut color_contraction_entries = + reserved(contraction_entry_count, "color contraction entries")?; + for row in 0..contraction_entry_count { + color_contraction_entries.push(InputColorContractionEntry { + left_group_id: view.color_contraction_entries.left_group_id[row], + right_group_id: view.color_contraction_entries.right_group_id[row], + weight_factor_id: view.color_contraction_entries.weight_factor_id[row], + symmetry_factor_id: view.color_contraction_entries.symmetry_factor_id[row], + }); + } + + let input = Self { + process_key: view.process_key.to_owned(), + model_name: view.model_name.to_owned(), + string_catalog, + canonical_ir_catalog, + semantic_limitations, + retained_tables, + string_count, + canonical_ir_count, + currents, + sources, + interactions, + roots, + interaction_groups, + interaction_group_members: copy_slice( + view.interaction_groups.member_interaction_id, + "interaction group members", + )?, + momentum_masks, + bitset_ranges, + bitset_words: copy_slice(view.bitsets.words, "bitset words")?, + u32_sequence_ranges, + u32_sequence_values: copy_slice(view.u32_sequences.values, "u32 sequence values")?, + i32_sequence_ranges, + i32_sequence_values: copy_slice(view.i32_sequences.values, "i32 sequence values")?, + exact_factors, + couplings, + model_parameters, + contraction_coefficients, + coherent_groups, + helicity_selectors, + color_selectors, + reduction_members, + color_contraction: view.color_contraction, + color_contraction_entries, + }; + input.validate()?; + Ok(input) + } + + pub fn process_key(&self) -> &str { + &self.process_key + } + + pub fn model_name(&self) -> &str { + &self.model_name + } + + pub fn string_catalog(&self) -> &[Box] { + &self.string_catalog + } + + pub fn canonical_ir_catalog(&self) -> &[Box] { + &self.canonical_ir_catalog + } + + pub fn semantic_limitations(&self) -> &[Box] { + &self.semantic_limitations + } + + pub fn retained_tables(&self) -> &[EagerOwnedRetainedTable] { + &self.retained_tables + } + + pub fn current_count(&self) -> usize { + self.currents.len() + } + + pub fn interaction_count(&self) -> usize { + self.interactions.len() + } + + pub fn root_count(&self) -> usize { + self.roots.len() + } + + pub fn exact_factor_count(&self) -> usize { + self.exact_factors.len() + } + + fn validate(&self) -> RusticolResult<()> { + validate_retained_table_counts(self)?; + if self.currents.is_empty() { + return Err(invalid("eager lowering input has no currents")); + } + if self.roots.is_empty() { + return Err(invalid("eager lowering input has no amplitude roots")); + } + validate_dense_ids( + self.currents.iter().map(|row| row.id), + self.currents.len(), + "current", + )?; + validate_dense_ids( + self.interactions.iter().map(|row| row.id), + self.interactions.len(), + "interaction", + )?; + validate_dense_ids( + self.roots.iter().map(|row| row.id), + self.roots.len(), + "root", + )?; + validate_dense_ids( + self.sources.iter().map(|row| row.source_id), + self.sources.len(), + "source", + )?; + validate_dense_ids( + self.coherent_groups.iter().map(|row| row.id), + self.coherent_groups.len(), + "coherent group", + )?; + + validate_flat_ranges( + self.bitset_ranges.iter().map(|row| row.range), + self.bitset_words.len(), + "bitset catalog", + )?; + for (bitset_id, row) in self.bitset_ranges.iter().enumerate() { + let words = range_slice(&self.bitset_words, row.range, "bitset words")?; + let actual_count = words.iter().try_fold(0_u64, |total, word| { + total + .checked_add(u64::from(word.count_ones())) + .ok_or_else(|| { + invalid(format!("bitset {bitset_id} population count exceeds u64")) + }) + })?; + if actual_count != row.bit_count { + return Err(invalid(format!( + "bitset {bitset_id} declares {} set bits, found {actual_count}", + row.bit_count + ))); + } + if words.last().is_some_and(|word| *word == 0) { + return Err(invalid(format!( + "bitset {bitset_id} has a noncanonical trailing zero word" + ))); + } + } + validate_flat_ranges( + self.u32_sequence_ranges.iter().copied(), + self.u32_sequence_values.len(), + "u32 sequence catalog", + )?; + validate_flat_ranges( + self.i32_sequence_ranges.iter().copied(), + self.i32_sequence_values.len(), + "i32 sequence catalog", + )?; + + let mut previous_momentum_bitset = None; + let mut momentum_by_bitset = vec![MISSING_U32; self.bitset_ranges.len()]; + for (row, (slot_id, bitset_id)) in self.momentum_masks.iter().copied().enumerate() { + expect_dense_id(slot_id, row, "momentum slot")?; + let bitset_index = index(bitset_id, self.bitset_ranges.len(), "momentum bitset")?; + if momentum_by_bitset[bitset_index] != MISSING_U32 { + return Err(invalid(format!( + "momentum bitset {bitset_id} is assigned more than once" + ))); + } + if let Some(previous) = previous_momentum_bitset + && compare_bitsets(self, previous, bitset_id)? != Ordering::Less + { + return Err(invalid( + "momentum masks must be unique and ordered by population then value", + )); + } + previous_momentum_bitset = Some(bitset_id); + momentum_by_bitset[bitset_index] = slot_id; + } + + let mut source_seen = vec![false; self.currents.len()]; + for source in &self.sources { + let current_index = index(source.current_id, self.currents.len(), "source current")?; + if !self.currents[current_index].is_source { + return Err(invalid(format!( + "source {} references non-source current {}", + source.source_id, source.current_id + ))); + } + if std::mem::replace(&mut source_seen[current_index], true) { + return Err(invalid(format!( + "source current {} is listed more than once", + source.current_id + ))); + } + required_index(source.source_ir_id, self.canonical_ir_count, "source IR")?; + required_index( + source.crossing_ir_id, + self.canonical_ir_count, + "source crossing IR", + )?; + required_index( + source.crossing_factor_id, + self.exact_factors.len(), + "source crossing factor", + )?; + } + for (current_id, current) in self.currents.iter().enumerate() { + if current.dimension == 0 { + return Err(invalid(format!("current {current_id} has zero dimension"))); + } + required_index( + current.momentum_mask_bitset_id, + self.bitset_ranges.len(), + "current momentum bitset", + )?; + if momentum_by_bitset[current.momentum_mask_bitset_id as usize] == MISSING_U32 { + return Err(invalid(format!( + "current {current_id} momentum bitset {} has no momentum slot", + current.momentum_mask_bitset_id + ))); + } + required_index( + current.propagator_ir_id, + self.canonical_ir_count, + "current propagator IR", + )?; + if current.is_source != source_seen[current_id] { + return Err(invalid(format!( + "current {current_id} source flag disagrees with the source table" + ))); + } + } + + for (factor_id, factor) in self.exact_factors.iter().enumerate() { + required_index( + factor.canonical_string_id, + self.string_count, + "exact factor canonical string", + )?; + required_index( + factor.exact_ir_id, + self.canonical_ir_count, + "exact factor IR", + )?; + optional_index( + factor.source_ir_id, + self.canonical_ir_count, + "exact factor source IR", + )?; + if !matches!( + factor.exact_source, + EXACT_SOURCE_BINARY64 | EXACT_SOURCE_CANONICAL_IR + ) { + return Err(invalid(format!( + "exact factor {factor_id} has unsupported provenance {}", + factor.exact_source + ))); + } + } + + for (parameter_id, parameter) in self.model_parameters.iter().enumerate() { + required_index( + parameter.name_string_id, + self.string_count, + "parameter name", + )?; + required_index( + parameter.kind_string_id, + self.string_count, + "parameter kind", + )?; + optional_index( + parameter.runtime_name_string_id, + self.string_count, + "parameter runtime name", + )?; + let factor = required_row( + &self.exact_factors, + parameter.default_factor_id, + "parameter default factor", + )?; + if factor.real_bits != parameter.default_bits || !factor_is_real_zero(factor) { + return Err(invalid(format!( + "model parameter {parameter_id} default disagrees with its exact factor" + ))); + } + if !matches!(parameter.complex_component, -1..=1) { + return Err(invalid(format!( + "model parameter {parameter_id} has invalid complex component {}", + parameter.complex_component + ))); + } + let _ = parameter.derived; + } + + for (coupling_id, coupling) in self.couplings.iter().enumerate() { + required_index( + coupling.constant_factor_id, + self.exact_factors.len(), + "coupling constant factor", + )?; + let names = self.u32_sequence(coupling.parameter_name_ids_sequence_id)?; + if names.len() > 2 { + return Err(invalid(format!( + "coupling {coupling_id} has more than two parameter names" + ))); + } + for name in names.iter().copied().filter(|name| *name != MISSING_U32) { + required_index(name, self.string_count, "coupling parameter name")?; + } + } + + let mut result_stage = vec![None; self.currents.len()]; + for (interaction_id, interaction) in self.interactions.iter().enumerate() { + if interaction.stage_subset_size == 0 { + return Err(invalid(format!( + "interaction {interaction_id} has zero stage subset size" + ))); + } + let left = index( + interaction.left_current_id, + self.currents.len(), + "interaction left current", + )?; + let right = index( + interaction.right_current_id, + self.currents.len(), + "interaction right current", + )?; + let result = index( + interaction.result_current_id, + self.currents.len(), + "interaction result current", + )?; + if self.currents[result].is_source { + return Err(invalid(format!( + "interaction {interaction_id} writes source current {result}" + ))); + } + for input in [left, right] { + if !self.currents[input].is_source + && result_stage[input] + .is_some_and(|stage| stage >= interaction.stage_subset_size) + { + return Err(invalid(format!( + "interaction {interaction_id} does not follow DAG stage order" + ))); + } + } + match result_stage[result] { + Some(stage) if stage != interaction.stage_subset_size => { + return Err(invalid(format!( + "current {result} is produced in multiple stages" + ))); + } + None => result_stage[result] = Some(interaction.stage_subset_size), + _ => {} + } + required_index( + interaction.coupling_id, + self.couplings.len(), + "interaction coupling", + )?; + for (name, factor_id) in [ + ("coupling", interaction.coupling_factor_id), + ("color", interaction.color_factor_id), + ("evaluation", interaction.evaluation_factor_id), + ( + "kernel normalization", + interaction.kernel_normalization_factor_id, + ), + ] { + required_index(factor_id, self.exact_factors.len(), name)?; + } + if interaction.coupling_factor_id + != self.couplings[interaction.coupling_id as usize].constant_factor_id + { + return Err(invalid(format!( + "interaction {interaction_id} coupling factor disagrees with coupling {}", + interaction.coupling_id + ))); + } + validate_kernel_transform( + interaction.kernel_id, + interaction.canonical_input_order, + interaction.output_factor_source, + false, + &format!("interaction {interaction_id}"), + )?; + } + + for (current_id, current) in self.currents.iter().enumerate() { + if !current.is_source && result_stage[current_id].is_none() { + return Err(invalid(format!( + "non-source current {current_id} has no producing interaction" + ))); + } + } + + validate_flat_ranges( + self.interaction_groups.iter().map(|row| row.members), + self.interaction_group_members.len(), + "interaction groups", + )?; + if self.interaction_group_members.len() != self.interactions.len() { + return Err(invalid( + "interaction groups must contain every interaction exactly once", + )); + } + let mut grouped = vec![false; self.interactions.len()]; + for (group_id, group) in self.interaction_groups.iter().enumerate() { + if group.members.count == 0 { + return Err(invalid(format!("interaction group {group_id} is empty"))); + } + let members = range_slice( + &self.interaction_group_members, + group.members, + "interaction group members", + )?; + if members[0] != group.representative_interaction_id { + return Err(invalid(format!( + "interaction group {group_id} representative is not its first member" + ))); + } + let representative = required_row( + &self.interactions, + group.representative_interaction_id, + "interaction group representative", + )?; + if representative.evaluation_group_id != group_id as u32 { + return Err(invalid(format!( + "interaction group {group_id} representative has a different group ID" + ))); + } + let representative_is_zero = factor_is_complex_zero( + &self.exact_factors[representative.evaluation_factor_id as usize], + ); + let mut group_has_nonzero_member = false; + for member_id in members { + let member_index = index(*member_id, self.interactions.len(), "group member")?; + if std::mem::replace(&mut grouped[member_index], true) { + return Err(invalid(format!( + "interaction {member_id} appears in more than one group" + ))); + } + let member = &self.interactions[member_index]; + group_has_nonzero_member |= !factor_is_complex_zero( + &self.exact_factors[member.evaluation_factor_id as usize], + ); + if member.evaluation_group_id != group_id as u32 { + return Err(invalid(format!( + "interaction {member_id} has group {}, expected {group_id}", + member.evaluation_group_id + ))); + } + validate_group_signature(representative, member, group_id)?; + } + if representative_is_zero && group_has_nonzero_member { + return Err(invalid(format!( + "interaction group {group_id} places a zero evaluation factor \ + before a live member" + ))); + } + } + if grouped.iter().any(|seen| !seen) { + return Err(invalid("interaction group partition is incomplete")); + } + + for (root_id, root) in self.roots.iter().enumerate() { + required_index( + root.left_current_id, + self.currents.len(), + "root left current", + )?; + required_index( + root.right_current_id, + self.currents.len(), + "root right current", + )?; + required_index( + root.color_factor_id, + self.exact_factors.len(), + "root color factor", + )?; + required_index( + root.contraction_ir_id, + self.canonical_ir_count, + "root contraction IR", + )?; + required_index( + root.coupling_factor_id, + self.exact_factors.len(), + "root coupling factor", + )?; + required_index( + root.helicity_weight_factor_id, + self.exact_factors.len(), + "root helicity weight", + )?; + required_index( + root.kernel_normalization_factor_id, + self.exact_factors.len(), + "root kernel normalization", + )?; + required_index( + root.coherent_group_id, + self.coherent_groups.len(), + "root coherent group", + )?; + if root.coupling_id != MISSING_U32 { + let coupling = required_row(&self.couplings, root.coupling_id, "root coupling")?; + if coupling.constant_factor_id != root.coupling_factor_id { + return Err(invalid(format!( + "root {root_id} coupling factor disagrees with coupling {}", + root.coupling_id + ))); + } + } + validate_kernel_transform( + root.kernel_id, + root.canonical_input_order, + root.output_factor_source, + true, + &format!("root {root_id}"), + )?; + } + + let mut previous_coefficient: Option<(u32, u32)> = None; + for (row, coefficient) in self.contraction_coefficients.iter().enumerate() { + required_index( + coefficient.contraction_ir_id, + self.canonical_ir_count, + "contraction coefficient IR", + )?; + required_index( + coefficient.factor_id, + self.exact_factors.len(), + "contraction coefficient factor", + )?; + match previous_coefficient { + Some((previous_ir, _)) if coefficient.contraction_ir_id < previous_ir => { + return Err(invalid(format!( + "contraction coefficient row {row} is not canonically ordered" + ))); + } + Some((previous_ir, previous_component)) + if coefficient.contraction_ir_id == previous_ir => + { + let expected_component = + previous_component.checked_add(1).ok_or_else(|| { + invalid(format!( + "contraction IR {} component index exceeds u32", + coefficient.contraction_ir_id + )) + })?; + if coefficient.component_index != expected_component { + return Err(invalid(format!( + "contraction coefficient row {row} is not canonically ordered" + ))); + } + } + _ if coefficient.component_index != 0 => { + return Err(invalid(format!( + "contraction IR {} starts at component {}", + coefficient.contraction_ir_id, coefficient.component_index + ))); + } + _ => {} + } + previous_coefficient = + Some((coefficient.contraction_ir_id, coefficient.component_index)); + } + + for group in &self.coherent_groups { + required_index( + group.helicity_weight_factor_id, + self.exact_factors.len(), + "coherent-group helicity weight", + )?; + required_index( + group.all_sector_weight_factor_id, + self.exact_factors.len(), + "coherent-group all-sector weight", + )?; + } + for (selector_id, selector) in self.helicity_selectors.iter().enumerate() { + let values = self.i32_sequence(selector.values_sequence_id)?; + let representative = self.i32_sequence(selector.representative_sequence_id)?; + if values.len() != representative.len() { + return Err(invalid(format!( + "helicity selector {selector_id} alias has a different width" + ))); + } + required_index( + selector.coefficient_factor_id, + self.exact_factors.len(), + "helicity selector coefficient", + )?; + if selector.computed && selector.structural_zero { + return Err(invalid(format!( + "helicity selector {selector_id} is both computed and structural zero" + ))); + } + if selector.structural_zero + && !factor_is_complex_zero( + &self.exact_factors[selector.coefficient_factor_id as usize], + ) + { + return Err(invalid(format!( + "structural-zero helicity selector {selector_id} has a nonzero coefficient" + ))); + } + } + for (selector_id, selector) in self.color_selectors.iter().enumerate() { + let values = self.u32_sequence(selector.word_sequence_id)?; + let representative = self.u32_sequence(selector.representative_word_sequence_id)?; + if values.len() != representative.len() { + return Err(invalid(format!( + "color selector {selector_id} alias has a different width" + ))); + } + required_index( + selector.coefficient_factor_id, + self.exact_factors.len(), + "color selector coefficient", + )?; + } + let mut reduction_keys = BTreeSet::new(); + let mut group_has_reduction = vec![false; self.coherent_groups.len()]; + for member in &self.reduction_members { + let group = index( + member.coherent_group_id, + self.coherent_groups.len(), + "reduction coherent group", + )?; + required_index( + member.helicity_selector_id, + self.helicity_selectors.len(), + "reduction helicity selector", + )?; + required_index( + member.color_selector_id, + self.color_selectors.len(), + "reduction color selector", + )?; + if !reduction_keys.insert(( + member.coherent_group_id, + member.helicity_selector_id, + member.color_selector_id, + )) { + return Err(invalid("duplicate selector reduction member")); + } + group_has_reduction[group] = true; + } + if group_has_reduction.iter().any(|present| !present) { + return Err(invalid("every coherent group must have reduction metadata")); + } + + let contraction = self.color_contraction; + validate_bool_scalar(contraction.present, "color contraction present")?; + validate_bool_scalar(contraction.supported, "color contraction supported")?; + validate_bool_scalar( + contraction.includes_color_factor, + "color contraction includes-color-factor", + )?; + if contraction.present == 0 { + if !self.color_contraction_entries.is_empty() || contraction.group_count != 0 { + return Err(invalid( + "absent color contraction has entries or a nonzero group count", + )); + } + } else { + if contraction.supported == 0 { + return Err(invalid("unsupported color contraction cannot be lowered")); + } + if contraction.group_count != self.coherent_groups.len() as u64 { + return Err(invalid( + "color contraction group count disagrees with coherent groups", + )); + } + if self.color_contraction_entries.is_empty() { + return Err(invalid("present color contraction has no entries")); + } + } + for entry in &self.color_contraction_entries { + required_index( + entry.left_group_id, + self.coherent_groups.len(), + "color contraction left group", + )?; + required_index( + entry.right_group_id, + self.coherent_groups.len(), + "color contraction right group", + )?; + required_index( + entry.weight_factor_id, + self.exact_factors.len(), + "color contraction weight", + )?; + required_index( + entry.symmetry_factor_id, + self.exact_factors.len(), + "color contraction symmetry", + )?; + } + Ok(()) + } + + fn u32_sequence(&self, id: u32) -> RusticolResult<&[u32]> { + let range = required_row(&self.u32_sequence_ranges, id, "u32 sequence")?; + range_slice(&self.u32_sequence_values, *range, "u32 sequence values") + } + + fn i32_sequence(&self, id: u32) -> RusticolResult<&[i32]> { + let range = required_row(&self.i32_sequence_ranges, id, "i32 sequence")?; + range_slice(&self.i32_sequence_values, *range, "i32 sequence values") + } +} + +/// Value-slot role in the compact plan. +#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] +#[repr(u8)] +pub enum EagerValueSlotKind { + Source = 1, + Unpropagated = 2, + Propagated = 3, +} + +/// One logical current and its global component range. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct EagerPlanCurrentRow { + pub current_id: u32, + pub component_start: u64, + pub component_count: u32, + pub momentum_slot_id: u32, + pub flags: u32, +} + +impl EagerPlanCurrentRow { + pub const ENCODED_LEN: u32 = 24; +} + +/// One value slot and its global component range. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct EagerPlanValueRow { + pub value_slot_id: u32, + pub current_id: u32, + pub component_start: u64, + pub component_count: u32, + pub kind: EagerValueSlotKind, +} + +impl EagerPlanValueRow { + pub const ENCODED_LEN: u32 = 24; +} + +/// One canonical arbitrary-width momentum mask. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct EagerPlanMomentumRow { + pub momentum_slot_id: u32, + pub bitset_id: u32, + pub component_start: u64, + pub component_count: u32, +} + +impl EagerPlanMomentumRow { + pub const ENCODED_LEN: u32 = 24; +} + +/// Source initialization metadata retained for exact execution. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct EagerPlanSourceFillRow { + pub source_id: u32, + pub current_id: u32, + pub value_slot_id: u32, + pub external_label: u32, + pub input_momentum_slot: u32, + pub source_ir_id: u32, + pub crossing_ir_id: u32, + pub crossing_factor_id: u32, + pub declared_state_index: u32, +} + +impl EagerPlanSourceFillRow { + pub const ENCODED_LEN: u32 = 40; +} + +/// Model-parameter metadata for the compact parameter layout. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct EagerPlanParameterRow { + pub parameter_id: u32, + pub name_string_id: u32, + pub kind_string_id: u32, + pub default_factor_id: u32, + pub runtime_name_string_id: u32, + pub complex_component: i32, + pub flags: u32, +} + +impl EagerPlanParameterRow { + pub const ENCODED_LEN: u32 = 32; +} + +/// One stage and its contiguous ranges in the execution tables. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct EagerPlanStageRow { + pub stage_index: u32, + pub subset_size: u32, + pub invocation_start: u64, + pub invocation_count: u64, + pub attachment_start: u64, + pub attachment_count: u64, + pub finalization_start: u64, + pub finalization_count: u64, +} + +impl EagerPlanStageRow { + pub const ENCODED_LEN: u32 = 56; +} + +/// Runtime coupling with exact constant provenance and optional parameter slots. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct EagerPlanCouplingRow { + pub coupling_id: u32, + pub real_parameter_id: u32, + pub imaginary_parameter_id: u32, + pub constant_factor_id: u32, +} + +impl EagerPlanCouplingRow { + pub const ENCODED_LEN: u32 = 16; +} + +/// One prepared vertex invocation. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct EagerPlanInvocationRow { + pub evaluation_group_id: u32, + pub kernel_id: u32, + pub left_value_slot_id: u32, + pub right_value_slot_id: u32, + pub left_momentum_slot_id: u32, + pub right_momentum_slot_id: u32, + pub coupling_slot_id: u32, + pub output_factor_source: u8, + pub attachment_start: u64, + pub attachment_count: u64, + pub selector_domain_id: u32, +} + +impl EagerPlanInvocationRow { + pub const ENCODED_LEN: u32 = 56; +} + +/// One invocation fan-out attachment. +/// +/// Its exact factor is `color * evaluation * normalization / +/// representative_evaluation`. Keeping all four IDs avoids an irreversible +/// f64 collapse during lowering. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct EagerPlanAttachmentRow { + pub interaction_id: u32, + pub result_current_id: u32, + pub color_factor_id: u32, + pub evaluation_factor_id: u32, + pub normalization_factor_id: u32, + pub representative_evaluation_factor_id: u32, + pub selector_domain_id: u32, +} + +impl EagerPlanAttachmentRow { + pub const ENCODED_LEN: u32 = 28; +} + +/// One current finalization and optional propagator application. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct EagerPlanFinalizationRow { + pub kernel_id: u32, + pub current_id: u32, + pub unpropagated_value_slot_id: u32, + pub propagated_value_slot_id: u32, + pub momentum_slot_id: u32, + pub unpropagated_selector_domain_id: u32, + pub propagated_selector_domain_id: u32, +} + +impl EagerPlanFinalizationRow { + pub const ENCODED_LEN: u32 = 28; +} + +/// One amplitude closure. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct EagerPlanClosureRow { + pub root_id: u32, + pub kernel_id: u32, + pub left_value_slot_id: u32, + pub right_value_slot_id: u32, + pub amplitude_index: u32, + pub coherent_group_id: u32, + pub coupling_slot_id: u32, + pub coupling_factor_id: u32, + pub output_factor_source: u8, + pub color_factor_id: u32, + pub normalization_factor_id: u32, + pub direct_coefficient_start: u64, + pub direct_coefficient_count: u64, + pub selector_domain_id: u32, +} + +impl EagerPlanClosureRow { + pub const ENCODED_LEN: u32 = 64; +} + +/// Exact coefficient for one direct closure component. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct EagerPlanDirectCoefficientRow { + pub contraction_ir_id: u32, + pub component_index: u32, + pub factor_id: u32, +} + +/// One interned coherent-group selector domain. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct EagerPlanSelectorDomainRow { + pub member_start: u64, + pub member_count: u64, +} + +impl EagerPlanSelectorDomainRow { + pub const ENCODED_LEN: u32 = 16; +} + +/// Logical currents, kernel evaluations, and routes executed for one selector. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct EagerPlanActiveWorkCounts { + pub current_count: u64, + pub evaluation_count: u64, + pub attachment_count: u64, +} + +/// Worst exact runtime work for each report selector axis. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct EagerPlanSelectorWorkSummary { + pub selected_flow_selector_count: u64, + pub selected_flow_worst: EagerPlanActiveWorkCounts, + pub all_flow_selector_count: u64, + pub all_flow_worst: EagerPlanActiveWorkCounts, + pub contracted: EagerPlanActiveWorkCounts, +} + +/// Public helicity selector, including aliases and structural zeros. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct EagerPlanHelicitySelectorRow { + pub selector_id: u32, + pub values_sequence_id: u32, + pub representative_sequence_id: u32, + pub coefficient_factor_id: u32, + pub computed: u8, + pub structural_zero: u8, +} + +/// Public color selector and representative alias. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct EagerPlanColorSelectorRow { + pub selector_id: u32, + pub word_sequence_id: u32, + pub representative_word_sequence_id: u32, + pub coefficient_factor_id: u32, + pub computed: u8, +} + +/// One coherent reduction group and its flat entry ranges. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct EagerPlanReductionGroupRow { + pub coherent_group_id: u32, + pub amplitude_entry_start: u64, + pub amplitude_entry_count: u64, + pub selector_entry_start: u64, + pub selector_entry_count: u64, + pub helicity_weight_factor_id: u32, + pub all_sector_weight_factor_id: u32, +} + +impl EagerPlanReductionGroupRow { + pub const ENCODED_LEN: u32 = 48; +} + +/// Tagged fixed-width reduction entry kind. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[repr(u8)] +pub enum EagerPlanReductionEntryKind { + AmplitudeMember = 1, + SelectorMember = 2, + ColorContraction = 3, +} + +/// Tagged fixed-width reduction entry. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct EagerPlanReductionEntryRow { + pub kind: EagerPlanReductionEntryKind, + pub owner_id: u32, + pub left_id: u32, + pub right_id: u32, + pub factor_id: u32, + pub auxiliary_factor_id: u32, +} + +impl EagerPlanReductionEntryRow { + pub const ENCODED_LEN: u32 = 24; +} + +/// Exact-factor catalog row with bit-exact binary64 fallback. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct EagerPlanExactFactorRow { + pub factor_id: u32, + pub real_bits: u64, + pub imaginary_bits: u64, + pub canonical_string_id: u32, + pub exact_source: u8, + pub exact_ir_id: u32, + pub source_ir_id: u32, +} + +/// Canonical range into a flat fixed-width catalog. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct EagerPlanCatalogRangeRow { + pub start: u64, + pub count: u64, +} + +impl EagerPlanCatalogRangeRow { + pub const ENCODED_LEN: u32 = 16; +} + +impl EagerPlanExactFactorRow { + pub const ENCODED_LEN: u32 = 40; +} + +/// A section shape ready to be wrapped by the common eager section header. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct EagerPlanSectionShape { + pub kind: EagerSectionKind, + pub record_size: u32, + pub record_count: u64, +} + +impl EagerPlanSectionShape { + pub fn header(self) -> RusticolResult { + EagerSectionHeader::new(self.kind, self.record_size, self.record_count) + } +} + +/// Flat, deterministic plan-v3 representation. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct EagerPlanV3 { + process_key: String, + model_name: String, + string_catalog: Vec>, + canonical_ir_catalog: Vec>, + semantic_limitations: Vec>, + retained_tables: Vec, + currents: Vec, + values: Vec, + momenta: Vec, + sources: Vec, + parameters: Vec, + stages: Vec, + couplings: Vec, + invocations: Vec, + attachments: Vec, + finalizations: Vec, + closures: Vec, + direct_coefficients: Vec, + selector_domains: Vec, + selector_memberships: Vec, + helicity_selectors: Vec, + color_selectors: Vec, + reduction_groups: Vec, + reduction_entries: Vec, + exact_factors: Vec, + bitset_ranges: Vec, + bitset_populations: Vec, + bitset_words: Vec, + u32_sequence_ranges: Vec, + u32_sequence_values: Vec, + i32_sequence_ranges: Vec, + i32_sequence_values: Vec, + color_contraction_entry_start: u64, + color_contraction_entry_count: u64, + current_component_count: u64, + value_component_count: u64, + momentum_component_count: u64, +} + +macro_rules! plan_slice_getter { + ($name:ident, $field:ident, $row:ty) => { + pub fn $name(&self) -> &[$row] { + &self.$field + } + }; +} + +fn selector_domain_active( + plan: &EagerPlanV3, + domain_id: u32, + active_groups: &BTreeSet, +) -> RusticolResult { + if domain_id == MISSING_U32 { + return Ok(true); + } + let domain = plan + .selector_domains + .get(domain_id as usize) + .ok_or_else(|| invalid("eager selector-work census references an unknown domain"))?; + let members = checked_usize_range( + domain.member_start, + domain.member_count, + plan.selector_memberships.len(), + "selector-work domain memberships", + )?; + Ok(plan.selector_memberships[members] + .iter() + .any(|member| active_groups.contains(member))) +} + +fn active_selector_work( + plan: &EagerPlanV3, + active_groups: &BTreeSet, +) -> RusticolResult { + let mut currents = plan + .sources + .iter() + .map(|source| source.current_id) + .collect::>(); + let mut evaluations = 0_u64; + let mut attachments_count = 0_u64; + for invocation in &plan.invocations { + if !selector_domain_active(plan, invocation.selector_domain_id, active_groups)? { + continue; + } + let attachments = checked_usize_range( + invocation.attachment_start, + invocation.attachment_count, + plan.attachments.len(), + "selector-work invocation attachments", + )?; + let mut invocation_active = false; + for attachment in &plan.attachments[attachments] { + if !selector_domain_active(plan, attachment.selector_domain_id, active_groups)? { + continue; + } + invocation_active = true; + currents.insert(attachment.result_current_id); + attachments_count = attachments_count + .checked_add(1) + .ok_or_else(|| invalid("selector-work attachment count exceeds u64"))?; + } + if invocation_active { + evaluations = evaluations + .checked_add(1) + .ok_or_else(|| invalid("selector-work evaluation count exceeds u64"))?; + } + } + Ok(EagerPlanActiveWorkCounts { + current_count: usize_u64(currents.len(), "selector-work current count")?, + evaluation_count: evaluations, + attachment_count: attachments_count, + }) +} + +fn worst_selector_work<'a>( + plan: &EagerPlanV3, + selectors: impl Iterator>, + context: &str, +) -> RusticolResult { + let mut worst = None::; + for groups in selectors { + let counts = active_selector_work(plan, groups)?; + worst = Some(match worst { + None => counts, + Some(previous) => EagerPlanActiveWorkCounts { + current_count: previous.current_count.max(counts.current_count), + evaluation_count: previous.evaluation_count.max(counts.evaluation_count), + attachment_count: previous.attachment_count.max(counts.attachment_count), + }, + }); + } + worst.ok_or_else(|| invalid(format!("eager selector-work census has no {context}"))) +} + +impl EagerPlanV3 { + pub fn abi(&self) -> &'static str { + EAGER_PLAN_ABI + } + + pub fn process_key(&self) -> &str { + &self.process_key + } + + pub fn model_name(&self) -> &str { + &self.model_name + } + + pub fn string_catalog(&self) -> &[Box] { + &self.string_catalog + } + + pub fn canonical_ir_catalog(&self) -> &[Box] { + &self.canonical_ir_catalog + } + + pub fn semantic_limitations(&self) -> &[Box] { + &self.semantic_limitations + } + + pub fn retained_tables(&self) -> &[EagerOwnedRetainedTable] { + &self.retained_tables + } + + plan_slice_getter!(currents, currents, EagerPlanCurrentRow); + plan_slice_getter!(values, values, EagerPlanValueRow); + plan_slice_getter!(momenta, momenta, EagerPlanMomentumRow); + plan_slice_getter!(sources, sources, EagerPlanSourceFillRow); + plan_slice_getter!(parameters, parameters, EagerPlanParameterRow); + plan_slice_getter!(stages, stages, EagerPlanStageRow); + plan_slice_getter!(couplings, couplings, EagerPlanCouplingRow); + plan_slice_getter!(invocations, invocations, EagerPlanInvocationRow); + plan_slice_getter!(attachments, attachments, EagerPlanAttachmentRow); + plan_slice_getter!(finalizations, finalizations, EagerPlanFinalizationRow); + plan_slice_getter!(closures, closures, EagerPlanClosureRow); + plan_slice_getter!( + direct_coefficients, + direct_coefficients, + EagerPlanDirectCoefficientRow + ); + plan_slice_getter!( + selector_domains, + selector_domains, + EagerPlanSelectorDomainRow + ); + plan_slice_getter!( + helicity_selectors, + helicity_selectors, + EagerPlanHelicitySelectorRow + ); + plan_slice_getter!(color_selectors, color_selectors, EagerPlanColorSelectorRow); + plan_slice_getter!( + reduction_groups, + reduction_groups, + EagerPlanReductionGroupRow + ); + plan_slice_getter!( + reduction_entries, + reduction_entries, + EagerPlanReductionEntryRow + ); + plan_slice_getter!(exact_factors, exact_factors, EagerPlanExactFactorRow); + + pub fn selector_memberships(&self) -> &[u32] { + &self.selector_memberships + } + + /// Count the exact rows admitted by the same selector domains as runtime. + pub fn selector_work_summary(&self) -> RusticolResult { + let mut groups_by_helicity = BTreeMap::>::new(); + let mut groups_by_color = BTreeMap::>::new(); + for entry in &self.reduction_entries { + if entry.kind != EagerPlanReductionEntryKind::SelectorMember { + continue; + } + groups_by_helicity + .entry(entry.left_id) + .or_default() + .insert(entry.owner_id); + groups_by_color + .entry(entry.right_id) + .or_default() + .insert(entry.owner_id); + } + if groups_by_helicity.is_empty() || groups_by_color.is_empty() { + return Err(invalid( + "eager selector-work census has no physical selector memberships", + )); + } + let selected_flow_worst = + worst_selector_work(self, groups_by_color.values(), "color selector")?; + let all_flow_worst = + worst_selector_work(self, groups_by_helicity.values(), "helicity selector")?; + Ok(EagerPlanSelectorWorkSummary { + selected_flow_selector_count: usize_u64( + groups_by_color.len(), + "selected-flow selector count", + )?, + selected_flow_worst, + all_flow_selector_count: usize_u64( + groups_by_helicity.len(), + "all-flow selector count", + )?, + all_flow_worst, + contracted: EagerPlanActiveWorkCounts { + current_count: usize_u64(self.currents.len(), "contracted current count")?, + evaluation_count: usize_u64(self.invocations.len(), "contracted evaluation count")?, + attachment_count: usize_u64(self.attachments.len(), "contracted attachment count")?, + }, + }) + } + + pub fn bitset_ranges(&self) -> &[EagerPlanCatalogRangeRow] { + &self.bitset_ranges + } + + pub fn bitset_populations(&self) -> &[u64] { + &self.bitset_populations + } + + pub fn bitset_words(&self) -> &[u64] { + &self.bitset_words + } + + pub fn u32_sequence_ranges(&self) -> &[EagerPlanCatalogRangeRow] { + &self.u32_sequence_ranges + } + + pub fn u32_sequence_values(&self) -> &[u32] { + &self.u32_sequence_values + } + + pub fn i32_sequence_ranges(&self) -> &[EagerPlanCatalogRangeRow] { + &self.i32_sequence_ranges + } + + pub fn i32_sequence_values(&self) -> &[i32] { + &self.i32_sequence_values + } + + pub fn current_component_count(&self) -> u64 { + self.current_component_count + } + + pub fn value_component_count(&self) -> u64 { + self.value_component_count + } + + pub fn momentum_component_count(&self) -> u64 { + self.momentum_component_count + } + + pub fn color_contraction_entry_range(&self) -> (u64, u64) { + ( + self.color_contraction_entry_start, + self.color_contraction_entry_count, + ) + } + + pub fn section_shapes(&self) -> RusticolResult> { + let mut shapes = reserved(17, "eager section shapes")?; + let mut add = |kind, record_size, count: usize| -> RusticolResult<()> { + shapes.push(EagerPlanSectionShape { + kind, + record_size, + record_count: usize_u64(count, "eager section record count")?, + }); + Ok(()) + }; + add(EagerSectionKind::Metadata, 64, 1)?; + add( + EagerSectionKind::CurrentLayout, + EagerPlanCurrentRow::ENCODED_LEN, + self.currents.len(), + )?; + add( + EagerSectionKind::ValueLayout, + EagerPlanValueRow::ENCODED_LEN, + self.values.len(), + )?; + add( + EagerSectionKind::MomentumLayout, + EagerPlanMomentumRow::ENCODED_LEN, + self.momenta.len(), + )?; + add( + EagerSectionKind::SourceFill, + EagerPlanSourceFillRow::ENCODED_LEN, + self.sources.len(), + )?; + add( + EagerSectionKind::ParameterLayout, + EagerPlanParameterRow::ENCODED_LEN, + self.parameters.len(), + )?; + add( + EagerSectionKind::Stages, + EagerPlanStageRow::ENCODED_LEN, + self.stages.len(), + )?; + add( + EagerSectionKind::Couplings, + EagerPlanCouplingRow::ENCODED_LEN, + self.couplings.len(), + )?; + add( + EagerSectionKind::Invocations, + EagerPlanInvocationRow::ENCODED_LEN, + self.invocations.len(), + )?; + add( + EagerSectionKind::Attachments, + EagerPlanAttachmentRow::ENCODED_LEN, + self.attachments.len(), + )?; + add( + EagerSectionKind::Finalizations, + EagerPlanFinalizationRow::ENCODED_LEN, + self.finalizations.len(), + )?; + add( + EagerSectionKind::Closures, + EagerPlanClosureRow::ENCODED_LEN, + self.closures.len(), + )?; + add( + EagerSectionKind::SelectorDomains, + EagerPlanSelectorDomainRow::ENCODED_LEN, + self.selector_domains.len(), + )?; + add( + EagerSectionKind::SelectorMemberships, + 4, + self.selector_memberships.len(), + )?; + add( + EagerSectionKind::ReductionGroups, + EagerPlanReductionGroupRow::ENCODED_LEN, + self.reduction_groups.len(), + )?; + add( + EagerSectionKind::ReductionEntries, + EagerPlanReductionEntryRow::ENCODED_LEN, + self.reduction_entries.len(), + )?; + add( + EagerSectionKind::ExactFactors, + EagerPlanExactFactorRow::ENCODED_LEN, + self.exact_factors.len(), + )?; + Ok(shapes) + } +} + +#[derive(Clone, Copy, Debug, Default)] +struct ValueSlots { + source: Option, + unpropagated: Option, + propagated: Option, + input: Option, +} + +/// Deterministically lower one validated v1 input into a flat plan-v3. +/// +/// The input is consumed so catalogs and retained column payloads move into the +/// plan without a second allocation or copy. +pub fn lower_eager_plan_v3(input: EagerLoweringInputV1) -> RusticolResult { + let (currents, current_component_count, momentum_by_bitset) = lower_currents(&input)?; + let (values, value_slots, value_component_count) = lower_values(&input)?; + let (momenta, momentum_component_count) = lower_momenta(&input)?; + let sources = lower_sources(&input, &value_slots)?; + let parameters = lower_parameters(&input)?; + let couplings = lower_couplings(&input)?; + let (stages, mut invocations, mut attachments, mut finalizations, producer_stage_by_current) = + lower_stages(&input, &value_slots, &momentum_by_bitset)?; + let (mut closures, direct_coefficients) = lower_closures(&input, &value_slots)?; + let (selector_domains, selector_memberships) = lower_selector_domains( + &stages, + &mut invocations, + &mut attachments, + &mut finalizations, + &mut closures, + values.len(), + input.currents.len(), + )?; + validate_stage_dependencies(&input, &producer_stage_by_current)?; + let helicity_selectors = lower_helicity_selectors(&input)?; + let color_selectors = lower_color_selectors(&input)?; + let ( + reduction_groups, + reduction_entries, + color_contraction_entry_start, + color_contraction_entry_count, + ) = lower_reductions(&input)?; + let exact_factors = input + .exact_factors + .iter() + .enumerate() + .map(|(factor_id, factor)| { + Ok(EagerPlanExactFactorRow { + factor_id: usize_u32(factor_id, "exact factor ID")?, + real_bits: factor.real_bits, + imaginary_bits: factor.imaginary_bits, + canonical_string_id: factor.canonical_string_id, + exact_source: factor.exact_source, + exact_ir_id: factor.exact_ir_id, + source_ir_id: factor.source_ir_id, + }) + }) + .collect::>>()?; + let bitset_ranges = input + .bitset_ranges + .iter() + .map(|row| EagerPlanCatalogRangeRow { + start: row.range.start, + count: row.range.count, + }) + .collect(); + let bitset_populations = input + .bitset_ranges + .iter() + .map(|row| row.bit_count) + .collect(); + let u32_sequence_ranges = input + .u32_sequence_ranges + .iter() + .map(|row| EagerPlanCatalogRangeRow { + start: row.start, + count: row.count, + }) + .collect(); + let i32_sequence_ranges = input + .i32_sequence_ranges + .iter() + .map(|row| EagerPlanCatalogRangeRow { + start: row.start, + count: row.count, + }) + .collect(); + + Ok(EagerPlanV3 { + process_key: input.process_key, + model_name: input.model_name, + string_catalog: input.string_catalog, + canonical_ir_catalog: input.canonical_ir_catalog, + semantic_limitations: input.semantic_limitations, + retained_tables: input.retained_tables, + currents, + values, + momenta, + sources, + parameters, + stages, + couplings, + invocations, + attachments, + finalizations, + closures, + direct_coefficients, + selector_domains, + selector_memberships, + helicity_selectors, + color_selectors, + reduction_groups, + reduction_entries, + exact_factors, + bitset_ranges, + bitset_populations, + bitset_words: input.bitset_words, + u32_sequence_ranges, + u32_sequence_values: input.u32_sequence_values, + i32_sequence_ranges, + i32_sequence_values: input.i32_sequence_values, + color_contraction_entry_start, + color_contraction_entry_count, + current_component_count, + value_component_count, + momentum_component_count, + }) +} + +fn lower_currents( + input: &EagerLoweringInputV1, +) -> RusticolResult<(Vec, u64, Vec)> { + let mut momentum_by_bitset = vec![MISSING_U32; input.bitset_ranges.len()]; + for (slot_id, bitset_id) in &input.momentum_masks { + momentum_by_bitset[*bitset_id as usize] = *slot_id; + } + let mut rows = reserved(input.currents.len(), "current layout")?; + let mut component_start = 0_u64; + for current in &input.currents { + rows.push(EagerPlanCurrentRow { + current_id: current.id, + component_start, + component_count: current.dimension, + momentum_slot_id: momentum_by_bitset[current.momentum_mask_bitset_id as usize], + flags: if current.is_source { + CURRENT_FLAG_SOURCE + } else { + 0 + }, + }); + component_start = component_start + .checked_add(u64::from(current.dimension)) + .ok_or_else(|| invalid("current component layout exceeds u64"))?; + } + Ok((rows, component_start, momentum_by_bitset)) +} + +fn lower_values( + input: &EagerLoweringInputV1, +) -> RusticolResult<(Vec, Vec, u64)> { + let mut used_as_input = vec![false; input.currents.len()]; + let mut used_as_root = vec![false; input.currents.len()]; + for interaction in &input.interactions { + used_as_input[interaction.left_current_id as usize] = true; + used_as_input[interaction.right_current_id as usize] = true; + } + for root in &input.roots { + used_as_root[root.left_current_id as usize] = true; + used_as_root[root.right_current_id as usize] = true; + } + + let capacity = input + .currents + .len() + .checked_mul(2) + .ok_or_else(|| invalid("value slot capacity exceeds usize"))?; + let mut rows = reserved(capacity, "value layout")?; + let mut slots = vec![ValueSlots::default(); input.currents.len()]; + let mut component_start = 0_u64; + for current in &input.currents { + let current_index = current.id as usize; + if current.is_source { + let slot = push_value_row( + &mut rows, + current.id, + current.dimension, + EagerValueSlotKind::Source, + &mut component_start, + )?; + slots[current_index].source = Some(slot); + if used_as_input[current_index] { + slots[current_index].input = Some(slot); + } + continue; + } + let needs_propagated = + used_as_input[current_index] && current.propagator_kernel_id != MISSING_U32; + let needs_unpropagated = used_as_root[current_index] || !needs_propagated; + if needs_unpropagated { + slots[current_index].unpropagated = Some(push_value_row( + &mut rows, + current.id, + current.dimension, + EagerValueSlotKind::Unpropagated, + &mut component_start, + )?); + } + if needs_propagated { + slots[current_index].propagated = Some(push_value_row( + &mut rows, + current.id, + current.dimension, + EagerValueSlotKind::Propagated, + &mut component_start, + )?); + } + if used_as_input[current_index] { + slots[current_index].input = if needs_propagated { + slots[current_index].propagated + } else { + slots[current_index].unpropagated + }; + } + } + Ok((rows, slots, component_start)) +} + +fn push_value_row( + rows: &mut Vec, + current_id: u32, + component_count: u32, + kind: EagerValueSlotKind, + component_start: &mut u64, +) -> RusticolResult { + let value_slot_id = usize_u32(rows.len(), "value slot ID")?; + rows.push(EagerPlanValueRow { + value_slot_id, + current_id, + component_start: *component_start, + component_count, + kind, + }); + *component_start = component_start + .checked_add(u64::from(component_count)) + .ok_or_else(|| invalid("value component layout exceeds u64"))?; + Ok(value_slot_id) +} + +fn lower_momenta(input: &EagerLoweringInputV1) -> RusticolResult<(Vec, u64)> { + let mut rows = reserved(input.momentum_masks.len(), "momentum layout")?; + let mut component_start = 0_u64; + for (slot_id, bitset_id) in &input.momentum_masks { + rows.push(EagerPlanMomentumRow { + momentum_slot_id: *slot_id, + bitset_id: *bitset_id, + component_start, + component_count: 4, + }); + component_start = component_start + .checked_add(4) + .ok_or_else(|| invalid("momentum component layout exceeds u64"))?; + } + Ok((rows, component_start)) +} + +fn lower_sources( + input: &EagerLoweringInputV1, + value_slots: &[ValueSlots], +) -> RusticolResult> { + let mut rows = reserved(input.sources.len(), "source fill")?; + for source in &input.sources { + let value_slot_id = value_slots[source.current_id as usize] + .source + .ok_or_else(|| { + invalid(format!( + "source {} has no source value slot", + source.source_id + )) + })?; + rows.push(EagerPlanSourceFillRow { + source_id: source.source_id, + current_id: source.current_id, + value_slot_id, + external_label: source.external_label, + input_momentum_slot: source.input_momentum_slot, + source_ir_id: source.source_ir_id, + crossing_ir_id: source.crossing_ir_id, + crossing_factor_id: source.crossing_factor_id, + declared_state_index: source.declared_state_index, + }); + } + Ok(rows) +} + +fn lower_parameters(input: &EagerLoweringInputV1) -> RusticolResult> { + let mut rows = reserved(input.model_parameters.len(), "parameter layout")?; + for (parameter_id, parameter) in input.model_parameters.iter().enumerate() { + rows.push(EagerPlanParameterRow { + parameter_id: usize_u32(parameter_id, "parameter ID")?, + name_string_id: parameter.name_string_id, + kind_string_id: parameter.kind_string_id, + default_factor_id: parameter.default_factor_id, + runtime_name_string_id: parameter.runtime_name_string_id, + complex_component: parameter.complex_component, + flags: u32::from(parameter.derived), + }); + } + Ok(rows) +} + +fn lower_couplings(input: &EagerLoweringInputV1) -> RusticolResult> { + let mut rows = reserved(input.couplings.len(), "coupling layout")?; + for (coupling_id, coupling) in input.couplings.iter().enumerate() { + let names = input.u32_sequence(coupling.parameter_name_ids_sequence_id)?; + let mut real = MISSING_U32; + let mut imaginary = MISSING_U32; + let first = names.first().copied().filter(|id| *id != MISSING_U32); + if let Some(logical_name) = first { + let logical: Vec<_> = input + .model_parameters + .iter() + .enumerate() + .filter(|(_, parameter)| parameter.runtime_name_string_id == logical_name) + .collect(); + if !logical.is_empty() { + for (parameter_id, parameter) in logical { + let parameter_id = usize_u32(parameter_id, "coupling parameter ID")?; + match parameter.complex_component { + 0 if real == MISSING_U32 => real = parameter_id, + 1 if imaginary == MISSING_U32 => imaginary = parameter_id, + _ => { + return Err(invalid(format!( + "coupling {coupling_id} has ambiguous logical parameter components" + ))); + } + } + } + if real == MISSING_U32 || imaginary == MISSING_U32 { + return Err(invalid(format!( + "coupling {coupling_id} logical parameter lacks real/imaginary slots" + ))); + } + } + } + if real == MISSING_U32 && imaginary == MISSING_U32 { + for (component, name) in names.iter().copied().enumerate() { + if name == MISSING_U32 { + continue; + } + let matches = input + .model_parameters + .iter() + .enumerate() + .filter(|(_, parameter)| parameter.name_string_id == name) + .map(|(id, _)| id) + .collect::>(); + if matches.len() != 1 { + return Err(invalid(format!( + "coupling {coupling_id} parameter name {name} is not unique" + ))); + } + let parameter_id = usize_u32(matches[0], "coupling parameter ID")?; + if component == 0 { + real = parameter_id; + } else { + imaginary = parameter_id; + } + } + } + rows.push(EagerPlanCouplingRow { + coupling_id: usize_u32(coupling_id, "coupling ID")?, + real_parameter_id: real, + imaginary_parameter_id: imaginary, + constant_factor_id: coupling.constant_factor_id, + }); + } + Ok(rows) +} + +type LoweredStages = ( + Vec, + Vec, + Vec, + Vec, + Vec, +); + +fn lower_stages( + input: &EagerLoweringInputV1, + value_slots: &[ValueSlots], + momentum_by_bitset: &[u32], +) -> RusticolResult { + let stage_sizes = input + .interactions + .iter() + .map(|row| row.stage_subset_size) + .collect::>() + .into_iter() + .collect::>(); + let mut stage_position_by_size = BTreeMap::new(); + for (position, subset_size) in stage_sizes.iter().copied().enumerate() { + stage_position_by_size.insert(subset_size, position); + } + let mut groups_by_stage = vec![Vec::new(); stage_sizes.len()]; + for (group_id, group) in input.interaction_groups.iter().enumerate() { + let representative = &input.interactions[group.representative_interaction_id as usize]; + let position = stage_position_by_size[&representative.stage_subset_size]; + groups_by_stage[position].push(group_id); + } + let mut outputs_by_stage = vec![BTreeSet::new(); stage_sizes.len()]; + let mut producer_stage_by_current = vec![MISSING_U32; input.currents.len()]; + for interaction in &input.interactions { + let position = stage_position_by_size[&interaction.stage_subset_size]; + outputs_by_stage[position].insert(interaction.result_current_id); + producer_stage_by_current[interaction.result_current_id as usize] = + usize_u32(position, "producer stage")?; + } + + let mut stages = reserved(stage_sizes.len(), "stages")?; + let mut invocations = reserved(input.interaction_groups.len(), "invocations")?; + let mut attachments = reserved(input.interactions.len(), "attachments")?; + let finalization_capacity = outputs_by_stage.iter().map(BTreeSet::len).sum(); + let mut finalizations = reserved(finalization_capacity, "finalizations")?; + for (stage_position, subset_size) in stage_sizes.iter().copied().enumerate() { + let invocation_start = usize_u64(invocations.len(), "invocation start")?; + let attachment_start = usize_u64(attachments.len(), "attachment start")?; + for group_id in &groups_by_stage[stage_position] { + let group = input.interaction_groups[*group_id]; + let representative = input.interactions[group.representative_interaction_id as usize]; + if factor_is_complex_zero( + &input.exact_factors[representative.evaluation_factor_id as usize], + ) { + continue; + } + let members = range_slice( + &input.interaction_group_members, + group.members, + "interaction group members", + )?; + let ordered_currents = match representative.canonical_input_order { + [0, 1] => [ + representative.left_current_id, + representative.right_current_id, + ], + [1, 0] => [ + representative.right_current_id, + representative.left_current_id, + ], + _ => unreachable!("validated canonical input order"), + }; + let invocation_attachment_start_index = attachments.len(); + let invocation_attachment_start = usize_u64( + invocation_attachment_start_index, + "invocation attachment start", + )?; + for member_id in members { + let member = input.interactions[*member_id as usize]; + if factor_is_complex_zero( + &input.exact_factors[member.evaluation_factor_id as usize], + ) { + continue; + } + attachments.push(EagerPlanAttachmentRow { + interaction_id: member.id, + result_current_id: member.result_current_id, + color_factor_id: member.color_factor_id, + evaluation_factor_id: member.evaluation_factor_id, + normalization_factor_id: representative.kernel_normalization_factor_id, + representative_evaluation_factor_id: representative.evaluation_factor_id, + selector_domain_id: MISSING_U32, + }); + } + let invocation_attachment_count = usize_u64( + attachments.len() - invocation_attachment_start_index, + "invocation attachment count", + )?; + invocations.push(EagerPlanInvocationRow { + evaluation_group_id: *group_id as u32, + kernel_id: representative.kernel_id, + left_value_slot_id: value_slots[ordered_currents[0] as usize] + .input + .ok_or_else(|| invalid("invocation left current has no input value slot"))?, + right_value_slot_id: value_slots[ordered_currents[1] as usize] + .input + .ok_or_else(|| invalid("invocation right current has no input value slot"))?, + left_momentum_slot_id: momentum_by_bitset + [input.currents[ordered_currents[0] as usize].momentum_mask_bitset_id as usize], + right_momentum_slot_id: momentum_by_bitset + [input.currents[ordered_currents[1] as usize].momentum_mask_bitset_id as usize], + coupling_slot_id: representative.coupling_id, + output_factor_source: representative.output_factor_source, + attachment_start: invocation_attachment_start, + attachment_count: invocation_attachment_count, + selector_domain_id: MISSING_U32, + }); + } + let finalization_start = usize_u64(finalizations.len(), "finalization start")?; + for current_id in &outputs_by_stage[stage_position] { + let current = input.currents[*current_id as usize]; + let slots = value_slots[*current_id as usize]; + if slots.unpropagated.is_none() && slots.propagated.is_none() { + return Err(invalid(format!( + "stage output current {current_id} has no output value slot" + ))); + } + if slots.propagated.is_some() && current.propagator_kernel_id == MISSING_U32 { + return Err(invalid(format!( + "propagated current {current_id} has no prepared propagator kernel" + ))); + } + finalizations.push(EagerPlanFinalizationRow { + kernel_id: if slots.propagated.is_some() { + current.propagator_kernel_id + } else { + MISSING_U32 + }, + current_id: *current_id, + unpropagated_value_slot_id: slots.unpropagated.unwrap_or(MISSING_U32), + propagated_value_slot_id: slots.propagated.unwrap_or(MISSING_U32), + momentum_slot_id: momentum_by_bitset[current.momentum_mask_bitset_id as usize], + unpropagated_selector_domain_id: MISSING_U32, + propagated_selector_domain_id: MISSING_U32, + }); + } + stages.push(EagerPlanStageRow { + stage_index: usize_u32(stage_position + 1, "stage index")?, + subset_size, + invocation_start, + invocation_count: usize_u64(invocations.len(), "invocation stop")? + .checked_sub(invocation_start) + .ok_or_else(|| invalid("invocation range underflow"))?, + attachment_start, + attachment_count: usize_u64(attachments.len(), "attachment stop")? + .checked_sub(attachment_start) + .ok_or_else(|| invalid("attachment range underflow"))?, + finalization_start, + finalization_count: usize_u64(finalizations.len(), "finalization stop")? + .checked_sub(finalization_start) + .ok_or_else(|| invalid("finalization range underflow"))?, + }); + } + Ok(( + stages, + invocations, + attachments, + finalizations, + producer_stage_by_current, + )) +} + +fn lower_closures( + input: &EagerLoweringInputV1, + value_slots: &[ValueSlots], +) -> RusticolResult<(Vec, Vec)> { + let mut coefficient_ranges = BTreeMap::::new(); + let mut coefficients = reserved( + input.contraction_coefficients.len(), + "direct closure coefficients", + )?; + let mut cursor = 0; + while cursor < input.contraction_coefficients.len() { + let ir_id = input.contraction_coefficients[cursor].contraction_ir_id; + let start = coefficients.len(); + while cursor < input.contraction_coefficients.len() + && input.contraction_coefficients[cursor].contraction_ir_id == ir_id + { + let row = input.contraction_coefficients[cursor]; + coefficients.push(EagerPlanDirectCoefficientRow { + contraction_ir_id: row.contraction_ir_id, + component_index: row.component_index, + factor_id: row.factor_id, + }); + cursor += 1; + } + coefficient_ranges.insert( + ir_id, + InputRange { + start: usize_u64(start, "direct coefficient start")?, + count: usize_u64(coefficients.len() - start, "direct coefficient count")?, + }, + ); + } + let mut closures = reserved(input.roots.len(), "closures")?; + for root in &input.roots { + let unordered = [root.left_current_id, root.right_current_id]; + let ordered = [ + unordered[root.canonical_input_order[0] as usize], + unordered[root.canonical_input_order[1] as usize], + ]; + let value_slot = |current_id: u32| -> RusticolResult { + let current = input.currents[current_id as usize]; + let slots = value_slots[current_id as usize]; + if current.is_source { + slots.source + } else { + slots.unpropagated + } + .ok_or_else(|| { + invalid(format!( + "root current {current_id} has no amplitude value slot" + )) + }) + }; + let direct_range = if root.kernel_id == MISSING_U32 { + coefficient_ranges + .get(&root.contraction_ir_id) + .copied() + .ok_or_else(|| { + invalid(format!( + "direct root {} has no contraction coefficients", + root.id + )) + })? + } else { + InputRange { start: 0, count: 0 } + }; + closures.push(EagerPlanClosureRow { + root_id: root.id, + kernel_id: root.kernel_id, + left_value_slot_id: value_slot(ordered[0])?, + right_value_slot_id: value_slot(ordered[1])?, + amplitude_index: root.id, + coherent_group_id: root.coherent_group_id, + coupling_slot_id: root.coupling_id, + coupling_factor_id: root.coupling_factor_id, + output_factor_source: root.output_factor_source, + color_factor_id: root.color_factor_id, + normalization_factor_id: root.kernel_normalization_factor_id, + direct_coefficient_start: direct_range.start, + direct_coefficient_count: direct_range.count, + selector_domain_id: MISSING_U32, + }); + } + Ok((closures, coefficients)) +} + +#[derive(Default)] +struct DomainInterner { + by_members: BTreeMap, u32>, + members: Vec>, +} + +impl DomainInterner { + fn intern(&mut self, members: &[u32]) -> RusticolResult { + if let Some(existing) = self.by_members.get(members) { + return Ok(*existing); + } + let id = usize_present_u32(self.members.len(), "selector domain ID")?; + let owned = copy_slice(members, "selector domain members")?; + self.by_members.insert(owned.clone(), id); + self.members.push(owned); + Ok(id) + } +} + +fn lower_selector_domains( + stages: &[EagerPlanStageRow], + invocations: &mut [EagerPlanInvocationRow], + attachments: &mut [EagerPlanAttachmentRow], + finalizations: &mut [EagerPlanFinalizationRow], + closures: &mut [EagerPlanClosureRow], + value_count: usize, + current_count: usize, +) -> RusticolResult<(Vec, Vec)> { + let mut interner = DomainInterner::default(); + let empty_domain = interner.intern(&[])?; + let mut value_domains = vec![Vec::::new(); value_count]; + for closure in closures.iter_mut() { + let members = [closure.coherent_group_id]; + closure.selector_domain_id = interner.intern(&members)?; + union_sorted_into( + &mut value_domains[closure.left_value_slot_id as usize], + &members, + )?; + union_sorted_into( + &mut value_domains[closure.right_value_slot_id as usize], + &members, + )?; + } + + let mut current_domains = vec![Vec::::new(); current_count]; + for stage in stages.iter().rev() { + let finalization_range = checked_usize_range( + stage.finalization_start, + stage.finalization_count, + finalizations.len(), + "stage finalizations", + )?; + for finalization in &mut finalizations[finalization_range.clone()] { + let unpropagated = if finalization.unpropagated_value_slot_id == MISSING_U32 { + Vec::new() + } else { + value_domains[finalization.unpropagated_value_slot_id as usize].clone() + }; + let propagated = if finalization.propagated_value_slot_id == MISSING_U32 { + Vec::new() + } else { + value_domains[finalization.propagated_value_slot_id as usize].clone() + }; + finalization.unpropagated_selector_domain_id = interner.intern(&unpropagated)?; + finalization.propagated_selector_domain_id = interner.intern(&propagated)?; + let mut current = unpropagated; + union_sorted_into(&mut current, &propagated)?; + current_domains[finalization.current_id as usize] = current; + if finalization.unpropagated_value_slot_id != MISSING_U32 { + value_domains[finalization.unpropagated_value_slot_id as usize].clear(); + } + if finalization.propagated_value_slot_id != MISSING_U32 { + value_domains[finalization.propagated_value_slot_id as usize].clear(); + } + } + + let attachment_range = checked_usize_range( + stage.attachment_start, + stage.attachment_count, + attachments.len(), + "stage attachments", + )?; + for attachment in &mut attachments[attachment_range] { + attachment.selector_domain_id = + interner.intern(¤t_domains[attachment.result_current_id as usize])?; + } + + let invocation_range = checked_usize_range( + stage.invocation_start, + stage.invocation_count, + invocations.len(), + "stage invocations", + )?; + for invocation in &mut invocations[invocation_range] { + let range = checked_usize_range( + invocation.attachment_start, + invocation.attachment_count, + attachments.len(), + "invocation attachments", + )?; + let mut domain = Vec::new(); + for attachment in &attachments[range] { + union_sorted_into( + &mut domain, + ¤t_domains[attachment.result_current_id as usize], + )?; + } + invocation.selector_domain_id = interner.intern(&domain)?; + union_sorted_into( + &mut value_domains[invocation.left_value_slot_id as usize], + &domain, + )?; + union_sorted_into( + &mut value_domains[invocation.right_value_slot_id as usize], + &domain, + )?; + } + for finalization in &finalizations[finalization_range] { + current_domains[finalization.current_id as usize].clear(); + } + } + + let mut order = (0..interner.members.len()).collect::>(); + order.sort_by(|left, right| { + compare_domain_members(&interner.members[*left], &interner.members[*right]) + }); + let mut remap = vec![0_u32; order.len()]; + let mut domain_rows = reserved(order.len(), "selector domains")?; + let member_capacity = interner + .members + .iter() + .try_fold(0_usize, |total, members| total.checked_add(members.len())) + .ok_or_else(|| invalid("selector membership count exceeds usize"))?; + let mut memberships = reserved(member_capacity, "selector memberships")?; + for (final_id, provisional_id) in order.into_iter().enumerate() { + remap[provisional_id] = usize_present_u32(final_id, "final selector domain ID")?; + let members = &interner.members[provisional_id]; + domain_rows.push(EagerPlanSelectorDomainRow { + member_start: usize_u64(memberships.len(), "selector member start")?, + member_count: usize_u64(members.len(), "selector member count")?, + }); + memberships.extend_from_slice(members); + } + let remap_id = |id: &mut u32| { + debug_assert_ne!(*id, MISSING_U32); + *id = remap[*id as usize]; + }; + for row in invocations { + remap_id(&mut row.selector_domain_id); + } + for row in attachments { + remap_id(&mut row.selector_domain_id); + } + for row in finalizations { + remap_id(&mut row.unpropagated_selector_domain_id); + remap_id(&mut row.propagated_selector_domain_id); + } + for row in closures { + remap_id(&mut row.selector_domain_id); + } + let _ = empty_domain; + Ok((domain_rows, memberships)) +} + +fn lower_helicity_selectors( + input: &EagerLoweringInputV1, +) -> RusticolResult> { + input + .helicity_selectors + .iter() + .enumerate() + .map(|(selector_id, row)| { + Ok(EagerPlanHelicitySelectorRow { + selector_id: usize_u32(selector_id, "helicity selector ID")?, + values_sequence_id: row.values_sequence_id, + representative_sequence_id: row.representative_sequence_id, + coefficient_factor_id: row.coefficient_factor_id, + computed: u8::from(row.computed), + structural_zero: u8::from(row.structural_zero), + }) + }) + .collect() +} + +fn lower_color_selectors( + input: &EagerLoweringInputV1, +) -> RusticolResult> { + input + .color_selectors + .iter() + .enumerate() + .map(|(selector_id, row)| { + Ok(EagerPlanColorSelectorRow { + selector_id: usize_u32(selector_id, "color selector ID")?, + word_sequence_id: row.word_sequence_id, + representative_word_sequence_id: row.representative_word_sequence_id, + coefficient_factor_id: row.coefficient_factor_id, + computed: u8::from(row.computed), + }) + }) + .collect() +} + +type LoweredReductions = ( + Vec, + Vec, + u64, + u64, +); + +fn lower_reductions(input: &EagerLoweringInputV1) -> RusticolResult { + let mut amplitudes_by_group = vec![Vec::new(); input.coherent_groups.len()]; + for root in &input.roots { + amplitudes_by_group[root.coherent_group_id as usize].push(root.id); + } + if amplitudes_by_group.iter().any(Vec::is_empty) { + return Err(invalid("every coherent group must own an amplitude root")); + } + let mut selectors_by_group = vec![Vec::new(); input.coherent_groups.len()]; + for member in &input.reduction_members { + selectors_by_group[member.coherent_group_id as usize].push(*member); + } + let base_entry_count = amplitudes_by_group + .iter() + .map(Vec::len) + .chain(selectors_by_group.iter().map(Vec::len)) + .try_fold(0_usize, |total, count| total.checked_add(count)) + .ok_or_else(|| invalid("reduction entry count exceeds usize"))?; + let contraction_count = if input.color_contraction.present != 0 { + input.color_contraction_entries.len() + } else { + input.coherent_groups.len() + }; + let capacity = base_entry_count + .checked_add(contraction_count) + .ok_or_else(|| invalid("reduction entry count exceeds usize"))?; + let mut groups = reserved(input.coherent_groups.len(), "reduction groups")?; + let mut entries = reserved(capacity, "reduction entries")?; + for group in &input.coherent_groups { + let group_index = group.id as usize; + let amplitude_start = usize_u64(entries.len(), "amplitude reduction start")?; + for amplitude_id in &litudes_by_group[group_index] { + entries.push(EagerPlanReductionEntryRow { + kind: EagerPlanReductionEntryKind::AmplitudeMember, + owner_id: group.id, + left_id: *amplitude_id, + right_id: MISSING_U32, + factor_id: MISSING_U32, + auxiliary_factor_id: MISSING_U32, + }); + } + let amplitude_count = usize_u64( + amplitudes_by_group[group_index].len(), + "amplitude reduction count", + )?; + let selector_start = usize_u64(entries.len(), "selector reduction start")?; + for selector in &selectors_by_group[group_index] { + entries.push(EagerPlanReductionEntryRow { + kind: EagerPlanReductionEntryKind::SelectorMember, + owner_id: group.id, + left_id: selector.helicity_selector_id, + right_id: selector.color_selector_id, + factor_id: MISSING_U32, + auxiliary_factor_id: MISSING_U32, + }); + } + groups.push(EagerPlanReductionGroupRow { + coherent_group_id: group.id, + amplitude_entry_start: amplitude_start, + amplitude_entry_count: amplitude_count, + selector_entry_start: selector_start, + selector_entry_count: usize_u64( + selectors_by_group[group_index].len(), + "selector reduction count", + )?, + helicity_weight_factor_id: group.helicity_weight_factor_id, + all_sector_weight_factor_id: group.all_sector_weight_factor_id, + }); + } + let contraction_start = usize_u64(entries.len(), "color contraction start")?; + if input.color_contraction.present != 0 { + for entry in &input.color_contraction_entries { + entries.push(EagerPlanReductionEntryRow { + kind: EagerPlanReductionEntryKind::ColorContraction, + owner_id: MISSING_U32, + left_id: entry.left_group_id, + right_id: entry.right_group_id, + factor_id: entry.weight_factor_id, + auxiliary_factor_id: entry.symmetry_factor_id, + }); + } + } else { + for group in &input.coherent_groups { + entries.push(EagerPlanReductionEntryRow { + kind: EagerPlanReductionEntryKind::ColorContraction, + owner_id: MISSING_U32, + left_id: group.id, + right_id: group.id, + factor_id: group.all_sector_weight_factor_id, + auxiliary_factor_id: MISSING_U32, + }); + } + } + let contraction_entry_count = usize_u64(entries.len(), "color contraction stop")? + .checked_sub(contraction_start) + .ok_or_else(|| invalid("color contraction range underflow"))?; + Ok((groups, entries, contraction_start, contraction_entry_count)) +} + +fn validate_stage_dependencies( + input: &EagerLoweringInputV1, + producer_stage_by_current: &[u32], +) -> RusticolResult<()> { + let mut stage_by_size = BTreeMap::new(); + for size in input + .interactions + .iter() + .map(|row| row.stage_subset_size) + .collect::>() + { + let index = usize_u32(stage_by_size.len(), "stage position")?; + stage_by_size.insert(size, index); + } + for interaction in &input.interactions { + let stage = stage_by_size[&interaction.stage_subset_size]; + for current_id in [interaction.left_current_id, interaction.right_current_id] { + if !input.currents[current_id as usize].is_source + && producer_stage_by_current[current_id as usize] >= stage + { + return Err(invalid(format!( + "interaction {} consumes current {current_id} before it is finalized", + interaction.id + ))); + } + } + } + Ok(()) +} + +fn validate_group_signature( + representative: &InputInteraction, + member: &InputInteraction, + group_id: usize, +) -> RusticolResult<()> { + // Evaluation-group identity is the compiler's exact proof that these + // oriented interactions share one representative kernel evaluation. The + // attachment rows retain each member's factors and output contract. Only + // crossing a stage boundary would make the shared invocation unschedulable. + if representative.stage_subset_size != member.stage_subset_size { + return Err(invalid(format!( + "interaction group {group_id} crosses recursion stages" + ))); + } + Ok(()) +} + +fn validate_kernel_transform( + kernel_id: u32, + order: [u8; 2], + factor_source: u8, + kernel_optional: bool, + context: &str, +) -> RusticolResult<()> { + if kernel_id == MISSING_U32 && !kernel_optional { + return Err(invalid(format!("{context} has no prepared kernel"))); + } + if !matches!(order, [0, 1] | [1, 0]) { + return Err(invalid(format!( + "{context} canonical input order is not a permutation" + ))); + } + if !matches!( + factor_source, + OUTPUT_FACTOR_NONE | OUTPUT_FACTOR_COUPLING_REAL | OUTPUT_FACTOR_COUPLING_IMAG + ) { + return Err(invalid(format!( + "{context} has unsupported output factor source {factor_source}" + ))); + } + if kernel_id == MISSING_U32 && factor_source != OUTPUT_FACTOR_NONE { + return Err(invalid(format!( + "direct {context} cannot use a prepared-kernel output factor" + ))); + } + Ok(()) +} + +fn compare_domain_members(left: &[u32], right: &[u32]) -> Ordering { + left.len() + .cmp(&right.len()) + .then_with(|| left.iter().rev().cmp(right.iter().rev())) +} + +fn union_sorted_into(target: &mut Vec, source: &[u32]) -> RusticolResult<()> { + if source.is_empty() { + return Ok(()); + } + if target.is_empty() { + target + .try_reserve_exact(source.len()) + .map_err(|error| invalid(format!("could not reserve selector domain: {error}")))?; + target.extend_from_slice(source); + return Ok(()); + } + let capacity = target + .len() + .checked_add(source.len()) + .ok_or_else(|| invalid("selector domain union exceeds usize"))?; + let mut merged = reserved(capacity, "selector domain union")?; + let (mut left, mut right) = (0, 0); + while left < target.len() || right < source.len() { + let value = match (target.get(left), source.get(right)) { + (Some(a), Some(b)) if a < b => { + left += 1; + *a + } + (Some(a), Some(b)) if b < a => { + right += 1; + *b + } + (Some(a), Some(_)) => { + left += 1; + right += 1; + *a + } + (Some(a), None) => { + left += 1; + *a + } + (None, Some(b)) => { + right += 1; + *b + } + (None, None) => break, + }; + merged.push(value); + } + *target = merged; + Ok(()) +} + +fn compare_bitsets( + input: &EagerLoweringInputV1, + left_id: u32, + right_id: u32, +) -> RusticolResult { + let left = required_row(&input.bitset_ranges, left_id, "left bitset")?; + let right = required_row(&input.bitset_ranges, right_id, "right bitset")?; + let by_population = left.bit_count.cmp(&right.bit_count); + if by_population != Ordering::Equal { + return Ok(by_population); + } + let left_words = range_slice(&input.bitset_words, left.range, "left bitset words")?; + let right_words = range_slice(&input.bitset_words, right.range, "right bitset words")?; + Ok(left_words + .len() + .cmp(&right_words.len()) + .then_with(|| left_words.iter().rev().cmp(right_words.iter().rev()))) +} + +fn factor_is_complex_zero(factor: &InputExactFactor) -> bool { + f64::from_bits(factor.real_bits) == 0.0 && f64::from_bits(factor.imaginary_bits) == 0.0 +} + +fn factor_is_real_zero(factor: &InputExactFactor) -> bool { + f64::from_bits(factor.imaginary_bits) == 0.0 +} + +fn equal_column_lengths(context: &str, columns: &[(&str, usize)]) -> RusticolResult { + let expected = columns.first().map(|(_, length)| *length).unwrap_or(0); + for (name, length) in columns { + if *length != expected { + return Err(invalid(format!( + "{context}.{name} has {length} rows, expected {expected}" + ))); + } + } + usize_u32(expected, &format!("{context} row count"))?; + Ok(expected) +} + +fn bool_column(value: u8, context: &str, row: usize) -> RusticolResult { + validate_bool_scalar(value, &format!("{context}[{row}]"))?; + Ok(value != 0) +} + +fn validate_bool_scalar(value: u8, context: &str) -> RusticolResult<()> { + if value > 1 { + return Err(invalid(format!("{context} must be zero or one"))); + } + Ok(()) +} + +fn validate_dense_ids( + ids: impl Iterator, + count: usize, + context: &str, +) -> RusticolResult<()> { + for (expected, actual) in ids.enumerate() { + expect_dense_id(actual, expected, context)?; + } + usize_u32(count, &format!("{context} count"))?; + Ok(()) +} + +fn expect_dense_id(actual: u32, expected: usize, context: &str) -> RusticolResult<()> { + let expected = usize_u32(expected, &format!("{context} ID"))?; + if actual != expected { + return Err(invalid(format!( + "{context} IDs must be dense: found {actual}, expected {expected}" + ))); + } + Ok(()) +} + +fn validate_flat_ranges( + ranges: impl Iterator, + flat_len: usize, + context: &str, +) -> RusticolResult<()> { + let mut cursor = 0_u64; + for (row, range) in ranges.enumerate() { + if range.start != cursor { + return Err(invalid(format!( + "{context} range {row} starts at {}, expected {cursor}", + range.start + ))); + } + cursor = range + .start + .checked_add(range.count) + .ok_or_else(|| invalid(format!("{context} range {row} exceeds u64")))?; + } + if cursor != usize_u64(flat_len, &format!("{context} flat length"))? { + return Err(invalid(format!( + "{context} ranges cover {cursor} rows, expected {flat_len}" + ))); + } + Ok(()) +} + +fn range_slice<'a, T>( + values: &'a [T], + range: InputRange, + context: &str, +) -> RusticolResult<&'a [T]> { + let bounds = checked_usize_range(range.start, range.count, values.len(), context)?; + Ok(&values[bounds]) +} + +fn checked_usize_range( + start: u64, + count: u64, + length: usize, + context: &str, +) -> RusticolResult> { + let end = start + .checked_add(count) + .ok_or_else(|| invalid(format!("{context} range exceeds u64")))?; + let start = usize::try_from(start) + .map_err(|_| invalid(format!("{context} start exceeds platform bounds")))?; + let end = usize::try_from(end) + .map_err(|_| invalid(format!("{context} end exceeds platform bounds")))?; + if start > length || end > length { + return Err(invalid(format!("{context} range exceeds table bounds"))); + } + Ok(start..end) +} + +fn required_row<'a, T>(values: &'a [T], id: u32, context: &str) -> RusticolResult<&'a T> { + let index = index(id, values.len(), context)?; + Ok(&values[index]) +} + +fn required_index(id: u32, count: impl TryInto, context: &str) -> RusticolResult { + let count = count + .try_into() + .map_err(|_| invalid(format!("{context} count exceeds platform bounds")))?; + index(id, count, context) +} + +fn optional_index(id: u32, count: impl TryInto, context: &str) -> RusticolResult<()> { + if id != MISSING_U32 { + required_index(id, count, context)?; + } + Ok(()) +} + +fn index(id: u32, count: usize, context: &str) -> RusticolResult { + let index = usize::try_from(id) + .map_err(|_| invalid(format!("{context} ID {id} exceeds platform bounds")))?; + if index >= count { + return Err(invalid(format!( + "{context} ID {id} references an absent row (count {count})" + ))); + } + Ok(index) +} + +#[cfg(test)] +fn count_u32(count: u64, context: &str) -> RusticolResult { + u32::try_from(count).map_err(|_| invalid(format!("{context} count exceeds u32"))) +} + +fn usize_u32(value: usize, context: &str) -> RusticolResult { + u32::try_from(value).map_err(|_| invalid(format!("{context} exceeds u32"))) +} + +fn usize_present_u32(value: usize, context: &str) -> RusticolResult { + let value = usize_u32(value, context)?; + if value == MISSING_U32 { + return Err(invalid(format!( + "{context} collides with the missing-ID sentinel" + ))); + } + Ok(value) +} + +fn usize_u64(value: usize, context: &str) -> RusticolResult { + u64::try_from(value).map_err(|_| invalid(format!("{context} exceeds u64"))) +} + +fn reserved(capacity: usize, context: &str) -> RusticolResult> { + let mut result = Vec::new(); + result.try_reserve_exact(capacity).map_err(|error| { + invalid(format!( + "could not reserve {context} ({capacity} rows): {error}" + )) + })?; + Ok(result) +} + +fn copy_slice(values: &[T], context: &str) -> RusticolResult> { + let mut result = reserved(values.len(), context)?; + result.extend_from_slice(values); + Ok(result) +} + +fn copy_retained_tables( + tables: &[EagerRetainedTableView<'_>], +) -> RusticolResult> { + if tables.len() != EAGER_LOWERING_V1_TABLE_NAMES.len() { + return Err(invalid(format!( + "eager lowering retained table inventory has {} entries, expected {}", + tables.len(), + EAGER_LOWERING_V1_TABLE_NAMES.len() + ))); + } + let mut owned_tables = reserved(tables.len(), "retained eager tables")?; + for (table_index, (table, expected_name)) in + tables.iter().zip(EAGER_LOWERING_V1_TABLE_NAMES).enumerate() + { + if table.name != *expected_name { + return Err(invalid(format!( + "eager lowering retained table {table_index} is {:?}, expected {:?}", + table.name, expected_name + ))); + } + let row_count = usize::try_from(table.row_count).map_err(|_| { + invalid(format!( + "retained table {:?} row count exceeds platform bounds", + table.name + )) + })?; + usize_u32( + row_count, + &format!("retained table {:?} row count", table.name), + )?; + let mut seen_columns = BTreeSet::new(); + let mut columns = reserved(table.columns.len(), "retained eager columns")?; + for (column_index, column) in table.columns.iter().enumerate() { + if column.name.is_empty() { + return Err(invalid(format!( + "retained table {:?} column {column_index} has an empty name", + table.name + ))); + } + if !seen_columns.insert(column.name) { + return Err(invalid(format!( + "retained table {:?} repeats column {:?}", + table.name, column.name + ))); + } + if column.elements_per_row == 0 { + return Err(invalid(format!( + "retained column {}.{} has zero elements per row", + table.name, column.name + ))); + } + let expected_elements = table + .row_count + .checked_mul(u64::from(column.elements_per_row)) + .ok_or_else(|| { + invalid(format!( + "retained column {}.{} shape exceeds u64", + table.name, column.name + )) + })?; + if usize_u64(column.values.len(), "retained column element count")? != expected_elements + { + return Err(invalid(format!( + "retained column {}.{} has {} elements, expected {expected_elements}", + table.name, + column.name, + column.values.len() + ))); + } + let values = match column.values { + EagerPrimitiveColumnView::U8(values) => { + EagerOwnedPrimitiveColumn::U8(copy_slice(values, "retained u8 column")?) + } + EagerPrimitiveColumnView::U32(values) => { + EagerOwnedPrimitiveColumn::U32(copy_slice(values, "retained u32 column")?) + } + EagerPrimitiveColumnView::U64(values) => { + EagerOwnedPrimitiveColumn::U64(copy_slice(values, "retained u64 column")?) + } + EagerPrimitiveColumnView::I32(values) => { + EagerOwnedPrimitiveColumn::I32(copy_slice(values, "retained i32 column")?) + } + EagerPrimitiveColumnView::F64(values) => { + let mut bits = reserved(values.len(), "retained f64 column")?; + for (row, value) in values.iter().copied().enumerate() { + if !value.is_finite() { + return Err(invalid(format!( + "retained column {}.{} element {row} is not finite", + table.name, column.name + ))); + } + bits.push(value.to_bits()); + } + EagerOwnedPrimitiveColumn::F64Bits(bits) + } + }; + columns.push(EagerOwnedRetainedColumn { + name: copy_boxed_str(column.name, "retained column name", column_index)?, + elements_per_row: column.elements_per_row, + values, + }); + } + owned_tables.push(EagerOwnedRetainedTable { + name: copy_boxed_str(table.name, "retained table name", table_index)?, + row_count: table.row_count, + columns, + }); + } + Ok(owned_tables) +} + +fn validate_retained_table_counts(input: &EagerLoweringInputV1) -> RusticolResult<()> { + for table in &input.retained_tables { + let expected = match table.name() { + "bitset_ranges" => Some(input.bitset_ranges.len()), + "bitset_words" => Some(input.bitset_words.len()), + "coherent_groups" => Some(input.coherent_groups.len()), + "color_contraction_entries" => Some(input.color_contraction_entries.len()), + "color_contraction_metadata" + | "metadata" + | "lc_replay_metadata" + | "helicity_proof_metadata" + | "helicity_materialization_metadata" => Some(1), + "color_selectors" => Some(input.color_selectors.len()), + "contraction_coefficients" => Some(input.contraction_coefficients.len()), + "couplings" => Some(input.couplings.len()), + "currents" => Some(input.currents.len()), + "exact_factors" => Some(input.exact_factors.len()), + "helicity_selectors" => Some(input.helicity_selectors.len()), + "i32_sequence_ranges" => Some(input.i32_sequence_ranges.len()), + "i32_sequence_values" => Some(input.i32_sequence_values.len()), + "interaction_group_members" => Some(input.interaction_group_members.len()), + "interaction_groups" => Some(input.interaction_groups.len()), + "interactions" => Some(input.interactions.len()), + "model_parameters" => Some(input.model_parameters.len()), + "momentum_masks" => Some(input.momentum_masks.len()), + "reduction_members" => Some(input.reduction_members.len()), + "roots" => Some(input.roots.len()), + "sources" => Some(input.sources.len()), + "u32_sequence_ranges" => Some(input.u32_sequence_ranges.len()), + "u32_sequence_values" => Some(input.u32_sequence_values.len()), + _ => None, + }; + if let Some(expected) = expected + && table.row_count != usize_u64(expected, "retained table expected row count")? + { + return Err(invalid(format!( + "retained table {:?} has {} rows, expected {expected}", + table.name(), + table.row_count + ))); + } + } + Ok(()) +} + +fn copy_string_catalog(values: &[&str], context: &str) -> RusticolResult>> { + let mut seen = BTreeSet::new(); + for (index, value) in values.iter().copied().enumerate() { + if !seen.insert(value) { + return Err(invalid(format!( + "{context} contains duplicate value at row {index}" + ))); + } + } + let mut result = reserved(values.len(), context)?; + for (index, value) in values.iter().copied().enumerate() { + result.push(copy_boxed_str(value, context, index)?); + } + Ok(result) +} + +fn copy_string_list(values: &[&str], context: &str) -> RusticolResult>> { + let mut result = reserved(values.len(), context)?; + for (index, value) in values.iter().copied().enumerate() { + if value.is_empty() { + return Err(invalid(format!("{context} row {index} must not be empty"))); + } + result.push(copy_boxed_str(value, context, index)?); + } + Ok(result) +} + +fn copy_boxed_str(value: &str, context: &str, index: usize) -> RusticolResult> { + let mut owned = String::new(); + owned.try_reserve_exact(value.len()).map_err(|error| { + invalid(format!( + "could not reserve {context} row {index} ({} bytes): {error}", + value.len() + )) + })?; + owned.push_str(value); + Ok(owned.into_boxed_str()) +} + +fn invalid(message: impl Into) -> RusticolError { + RusticolError::invalid_argument(message) +} + +#[cfg(test)] +#[path = "eager_lowering_v3_tests.rs"] +mod tests; diff --git a/pyamplicol/rust/crates/rusticol-core/src/eager_lowering_v3_tests.rs b/pyamplicol/rust/crates/rusticol-core/src/eager_lowering_v3_tests.rs new file mode 100644 index 0000000000..58e991aa3d --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/eager_lowering_v3_tests.rs @@ -0,0 +1,692 @@ +// SPDX-License-Identifier: 0BSD + +use super::*; +use crate::RusticolErrorKind; + +#[derive(Clone)] +struct Fixture { + current_id: Vec, + current_dimension: Vec, + current_is_source: Vec, + current_momentum_bitset: Vec, + current_propagator_ir: Vec, + current_propagator_kernel: Vec, + source_id: Vec, + source_current: Vec, + source_external_label: Vec, + source_input_momentum: Vec, + source_ir: Vec, + source_crossing_ir: Vec, + source_crossing_factor: Vec, + source_state: Vec, + interaction_id: Vec, + interaction_stage_size: Vec, + interaction_left: Vec, + interaction_right: Vec, + interaction_result: Vec, + interaction_coupling: Vec, + interaction_coupling_factor: Vec, + interaction_color_factor: Vec, + interaction_evaluation_factor: Vec, + interaction_group: Vec, + interaction_ready: Vec, + interaction_kernel: Vec, + interaction_order: Vec<[u8; 2]>, + interaction_normalization: Vec, + interaction_output_factor: Vec, + root_id: Vec, + root_left: Vec, + root_right: Vec, + root_color_factor: Vec, + root_contraction_ir: Vec, + root_coupling: Vec, + root_coupling_factor: Vec, + root_helicity_weight: Vec, + root_group: Vec, + root_kernel: Vec, + root_order: Vec<[u8; 2]>, + root_normalization: Vec, + root_output_factor: Vec, + group_representative: Vec, + group_start: Vec, + group_count: Vec, + group_members: Vec, + momentum_slot: Vec, + momentum_bitset: Vec, + bitset_start: Vec, + bitset_count: Vec, + bitset_population: Vec, + bitset_words: Vec, + u32_sequence_start: Vec, + u32_sequence_count: Vec, + u32_sequence_values: Vec, + i32_sequence_start: Vec, + i32_sequence_count: Vec, + i32_sequence_values: Vec, + factor_real: Vec, + factor_imaginary: Vec, + factor_string: Vec, + factor_source: Vec, + factor_ir: Vec, + factor_source_ir: Vec, + coupling_factor: Vec, + coupling_names: Vec, + parameter_name: Vec, + parameter_kind: Vec, + parameter_default: Vec, + parameter_default_factor: Vec, + parameter_runtime_name: Vec, + parameter_component: Vec, + parameter_derived: Vec, + coefficient_ir: Vec, + coefficient_component: Vec, + coefficient_factor: Vec, + coherent_id: Vec, + coherent_helicity_weight: Vec, + coherent_all_weight: Vec, + helicity_values: Vec, + helicity_representative: Vec, + helicity_coefficient: Vec, + helicity_computed: Vec, + helicity_zero: Vec, + color_word: Vec, + color_representative: Vec, + color_coefficient: Vec, + color_computed: Vec, + reduction_group: Vec, + reduction_helicity: Vec, + reduction_color: Vec, + contraction_metadata: EagerColorContractionMetadata, + contraction_left: Vec, + contraction_right: Vec, + contraction_weight: Vec, + contraction_symmetry: Vec, + string_catalog: Vec<&'static str>, + ir_catalog: Vec<&'static str>, + semantic_limitations: Vec<&'static str>, +} + +impl Fixture { + fn valid() -> Self { + Self { + current_id: vec![0, 1, 2, 3], + current_dimension: vec![1; 4], + current_is_source: vec![1, 1, 0, 0], + current_momentum_bitset: vec![0, 1, 2, 2], + current_propagator_ir: vec![0; 4], + current_propagator_kernel: vec![MISSING_U32, MISSING_U32, 30, MISSING_U32], + source_id: vec![0, 1], + source_current: vec![0, 1], + source_external_label: vec![1, 2], + source_input_momentum: vec![0, 1], + source_ir: vec![0, 0], + source_crossing_ir: vec![0, 0], + source_crossing_factor: vec![0, 0], + source_state: vec![0, 0], + interaction_id: vec![0, 1], + interaction_stage_size: vec![2, 3], + interaction_left: vec![0, 2], + interaction_right: vec![1, 1], + interaction_result: vec![2, 3], + interaction_coupling: vec![0, 0], + interaction_coupling_factor: vec![0, 0], + interaction_color_factor: vec![1, 1], + interaction_evaluation_factor: vec![0, 0], + interaction_group: vec![0, 1], + interaction_ready: vec![1, 1], + interaction_kernel: vec![10, 11], + interaction_order: vec![[0, 1], [1, 0]], + interaction_normalization: vec![0, 0], + interaction_output_factor: vec![0, 0], + root_id: vec![0], + root_left: vec![3], + root_right: vec![0], + root_color_factor: vec![1], + root_contraction_ir: vec![0], + root_coupling: vec![0], + root_coupling_factor: vec![0], + root_helicity_weight: vec![0], + root_group: vec![0], + root_kernel: vec![40], + root_order: vec![[0, 1]], + root_normalization: vec![0], + root_output_factor: vec![0], + group_representative: vec![0, 1], + group_start: vec![0, 1], + group_count: vec![1, 1], + group_members: vec![0, 1], + momentum_slot: vec![0, 1, 2], + momentum_bitset: vec![0, 1, 2], + bitset_start: vec![0, 1, 2], + bitset_count: vec![1, 1, 1], + bitset_population: vec![1, 1, 2], + bitset_words: vec![1, 2, 3], + u32_sequence_start: vec![0], + u32_sequence_count: vec![0], + u32_sequence_values: vec![], + i32_sequence_start: vec![0], + i32_sequence_count: vec![2], + i32_sequence_values: vec![-1, 1], + factor_real: vec![1.0, 2.0, -0.0], + factor_imaginary: vec![0.0; 3], + factor_string: vec![0, 1, 2], + factor_source: vec![0; 3], + factor_ir: vec![0, 1, 2], + factor_source_ir: vec![MISSING_U32; 3], + coupling_factor: vec![0], + coupling_names: vec![0], + parameter_name: vec![], + parameter_kind: vec![], + parameter_default: vec![], + parameter_default_factor: vec![], + parameter_runtime_name: vec![], + parameter_component: vec![], + parameter_derived: vec![], + coefficient_ir: vec![], + coefficient_component: vec![], + coefficient_factor: vec![], + coherent_id: vec![0], + coherent_helicity_weight: vec![0], + coherent_all_weight: vec![0], + helicity_values: vec![0, 0], + helicity_representative: vec![0, 0], + helicity_coefficient: vec![0, 2], + helicity_computed: vec![1, 0], + helicity_zero: vec![0, 1], + color_word: vec![0], + color_representative: vec![0], + color_coefficient: vec![0], + color_computed: vec![1], + reduction_group: vec![0], + reduction_helicity: vec![0], + reduction_color: vec![0], + contraction_metadata: EagerColorContractionMetadata { + present: 0, + supported: 0, + group_count: 0, + includes_color_factor: 0, + }, + contraction_left: vec![], + contraction_right: vec![], + contraction_weight: vec![], + contraction_symmetry: vec![], + string_catalog: vec!["factor-0", "factor-1", "factor-2"], + ir_catalog: vec!["{\"id\":0}", "{\"id\":1}", "{\"id\":2}"], + semantic_limitations: vec!["binary64 source factors retain exact payload bits"], + } + } + + fn retained_row_count(&self, name: &str) -> u64 { + match name { + "bitset_ranges" => self.bitset_start.len() as u64, + "bitset_words" => self.bitset_words.len() as u64, + "coherent_groups" => self.coherent_id.len() as u64, + "color_contraction_entries" => self.contraction_left.len() as u64, + "color_contraction_metadata" + | "metadata" + | "lc_replay_metadata" + | "helicity_proof_metadata" + | "helicity_materialization_metadata" => 1, + "color_selectors" => self.color_word.len() as u64, + "contraction_coefficients" => self.coefficient_ir.len() as u64, + "couplings" => self.coupling_factor.len() as u64, + "currents" => self.current_id.len() as u64, + "exact_factors" => self.factor_real.len() as u64, + "helicity_selectors" => self.helicity_values.len() as u64, + "i32_sequence_ranges" => self.i32_sequence_start.len() as u64, + "i32_sequence_values" => self.i32_sequence_values.len() as u64, + "interaction_group_members" => self.group_members.len() as u64, + "interaction_groups" => self.group_start.len() as u64, + "interactions" => self.interaction_id.len() as u64, + "model_parameters" => self.parameter_name.len() as u64, + "momentum_masks" => self.momentum_slot.len() as u64, + "reduction_members" => self.reduction_group.len() as u64, + "roots" => self.root_id.len() as u64, + "sources" => self.source_id.len() as u64, + "u32_sequence_ranges" => self.u32_sequence_start.len() as u64, + "u32_sequence_values" => self.u32_sequence_values.len() as u64, + _ => 0, + } + } + + fn retained_views(&self) -> Vec> { + EAGER_LOWERING_V1_TABLE_NAMES + .iter() + .map(|name| EagerRetainedTableView { + name, + row_count: self.retained_row_count(name), + columns: &[], + }) + .collect() + } + + fn view<'a>( + &'a self, + retained_tables: &'a [EagerRetainedTableView<'a>], + ) -> EagerLoweringInputV1View<'a> { + EagerLoweringInputV1View { + abi: EAGER_LOWERING_INPUT_ABI, + process_key: "fixture", + model_name: "fixture-model", + string_catalog: &self.string_catalog, + canonical_ir_catalog: &self.ir_catalog, + semantic_limitations: &self.semantic_limitations, + retained_tables, + unsupported_tables: &[], + currents: EagerCurrentColumns { + id: &self.current_id, + dimension: &self.current_dimension, + is_source: &self.current_is_source, + momentum_mask_bitset_id: &self.current_momentum_bitset, + propagator_ir_id: &self.current_propagator_ir, + propagator_kernel_id: &self.current_propagator_kernel, + }, + sources: EagerSourceColumns { + source_id: &self.source_id, + current_id: &self.source_current, + external_label: &self.source_external_label, + input_momentum_slot: &self.source_input_momentum, + source_ir_id: &self.source_ir, + crossing_ir_id: &self.source_crossing_ir, + crossing_factor_id: &self.source_crossing_factor, + declared_state_index: &self.source_state, + }, + interactions: EagerInteractionColumns { + id: &self.interaction_id, + stage_subset_size: &self.interaction_stage_size, + left_current_id: &self.interaction_left, + right_current_id: &self.interaction_right, + result_current_id: &self.interaction_result, + coupling_id: &self.interaction_coupling, + coupling_factor_id: &self.interaction_coupling_factor, + color_factor_id: &self.interaction_color_factor, + evaluation_factor_id: &self.interaction_evaluation_factor, + evaluation_group_id: &self.interaction_group, + full_tensor_network_ready: &self.interaction_ready, + kernel_id: &self.interaction_kernel, + canonical_input_order: &self.interaction_order, + kernel_normalization_factor_id: &self.interaction_normalization, + output_factor_source: &self.interaction_output_factor, + }, + roots: EagerRootColumns { + id: &self.root_id, + left_current_id: &self.root_left, + right_current_id: &self.root_right, + color_factor_id: &self.root_color_factor, + contraction_ir_id: &self.root_contraction_ir, + coupling_id: &self.root_coupling, + coupling_factor_id: &self.root_coupling_factor, + helicity_weight_factor_id: &self.root_helicity_weight, + coherent_group_id: &self.root_group, + kernel_id: &self.root_kernel, + canonical_input_order: &self.root_order, + kernel_normalization_factor_id: &self.root_normalization, + output_factor_source: &self.root_output_factor, + }, + interaction_groups: EagerInteractionGroupColumns { + representative_interaction_id: &self.group_representative, + member_start: &self.group_start, + member_count: &self.group_count, + member_interaction_id: &self.group_members, + }, + momentum_masks: EagerMomentumMaskColumns { + slot_id: &self.momentum_slot, + bitset_id: &self.momentum_bitset, + }, + bitsets: EagerBitsetColumns { + start: &self.bitset_start, + count: &self.bitset_count, + bit_count: &self.bitset_population, + words: &self.bitset_words, + }, + u32_sequences: EagerU32SequenceColumns { + start: &self.u32_sequence_start, + count: &self.u32_sequence_count, + values: &self.u32_sequence_values, + }, + i32_sequences: EagerI32SequenceColumns { + start: &self.i32_sequence_start, + count: &self.i32_sequence_count, + values: &self.i32_sequence_values, + }, + exact_factors: EagerExactFactorColumns { + real: &self.factor_real, + imaginary: &self.factor_imaginary, + canonical_string_id: &self.factor_string, + exact_source: &self.factor_source, + exact_ir_id: &self.factor_ir, + source_ir_id: &self.factor_source_ir, + }, + couplings: EagerCouplingColumns { + constant_factor_id: &self.coupling_factor, + parameter_name_ids_sequence_id: &self.coupling_names, + }, + model_parameters: EagerModelParameterColumns { + name_string_id: &self.parameter_name, + kind_string_id: &self.parameter_kind, + default_value: &self.parameter_default, + default_factor_id: &self.parameter_default_factor, + runtime_name_string_id: &self.parameter_runtime_name, + complex_component: &self.parameter_component, + derived: &self.parameter_derived, + }, + contraction_coefficients: EagerContractionCoefficientColumns { + contraction_ir_id: &self.coefficient_ir, + component_index: &self.coefficient_component, + factor_id: &self.coefficient_factor, + }, + coherent_groups: EagerCoherentGroupColumns { + id: &self.coherent_id, + helicity_weight_factor_id: &self.coherent_helicity_weight, + all_sector_weight_factor_id: &self.coherent_all_weight, + }, + helicity_selectors: EagerHelicitySelectorColumns { + values_sequence_id: &self.helicity_values, + representative_sequence_id: &self.helicity_representative, + coefficient_factor_id: &self.helicity_coefficient, + computed: &self.helicity_computed, + structural_zero: &self.helicity_zero, + }, + color_selectors: EagerColorSelectorColumns { + word_sequence_id: &self.color_word, + representative_word_sequence_id: &self.color_representative, + coefficient_factor_id: &self.color_coefficient, + computed: &self.color_computed, + }, + reduction_members: EagerReductionMemberColumns { + coherent_group_id: &self.reduction_group, + helicity_selector_id: &self.reduction_helicity, + color_selector_id: &self.reduction_color, + }, + color_contraction: self.contraction_metadata, + color_contraction_entries: EagerColorContractionEntryColumns { + left_group_id: &self.contraction_left, + right_group_id: &self.contraction_right, + weight_factor_id: &self.contraction_weight, + symmetry_factor_id: &self.contraction_symmetry, + }, + } + } + + fn input(&self) -> EagerLoweringInputV1 { + let retained_tables = self.retained_views(); + EagerLoweringInputV1::try_from_view(self.view(&retained_tables)).unwrap() + } +} + +fn assert_invalid(fixture: &Fixture, message: &str) { + let retained_tables = fixture.retained_views(); + let error = EagerLoweringInputV1::try_from_view(fixture.view(&retained_tables)).unwrap_err(); + assert_eq!(error.kind(), RusticolErrorKind::InvalidArgument); + assert!( + error.to_string().contains(message), + "expected {:?} to contain {message:?}", + error.to_string() + ); +} + +#[test] +fn owned_input_and_lowering_preserve_semantics_and_exact_factor_ids() { + let input = Fixture::valid().input(); + assert_eq!(input.process_key(), "fixture"); + assert_eq!(input.model_name(), "fixture-model"); + assert_eq!(input.string_catalog().len(), 3); + assert_eq!(input.canonical_ir_catalog().len(), 3); + assert_eq!(input.semantic_limitations().len(), 1); + assert_eq!(input.current_count(), 4); + assert_eq!(input.interaction_count(), 2); + assert_eq!(input.root_count(), 1); + assert_eq!(input.exact_factor_count(), 3); + + let plan = lower_eager_plan_v3(input).unwrap(); + assert_eq!(plan.abi(), "pyamplicol-eager-plan-v3"); + assert_eq!(plan.current_component_count(), 4); + assert_eq!(plan.value_component_count(), 4); + assert_eq!(plan.momentum_component_count(), 12); + assert_eq!(plan.stages().len(), 2); + assert_eq!(plan.invocations().len(), 2); + assert_eq!(plan.attachments().len(), 2); + assert_eq!(plan.finalizations().len(), 2); + assert_eq!(plan.closures().len(), 1); + assert_eq!( + plan.selector_work_summary().unwrap(), + EagerPlanSelectorWorkSummary { + selected_flow_selector_count: 1, + selected_flow_worst: EagerPlanActiveWorkCounts { + current_count: 4, + evaluation_count: 2, + attachment_count: 2, + }, + all_flow_selector_count: 1, + all_flow_worst: EagerPlanActiveWorkCounts { + current_count: 4, + evaluation_count: 2, + attachment_count: 2, + }, + contracted: EagerPlanActiveWorkCounts { + current_count: 4, + evaluation_count: 2, + attachment_count: 2, + }, + } + ); + + assert_eq!( + plan.values() + .iter() + .map(|row| (row.current_id, row.kind)) + .collect::>(), + vec![ + (0, EagerValueSlotKind::Source), + (1, EagerValueSlotKind::Source), + (2, EagerValueSlotKind::Propagated), + (3, EagerValueSlotKind::Unpropagated), + ] + ); + assert_eq!(plan.invocations()[1].left_value_slot_id, 1); + assert_eq!(plan.invocations()[1].right_value_slot_id, 2); + assert_eq!(plan.finalizations()[0].kernel_id, 30); + assert_eq!(plan.finalizations()[0].propagated_value_slot_id, 2); + assert_eq!( + plan.finalizations()[0].unpropagated_value_slot_id, + MISSING_U32 + ); + + let attachment = plan.attachments()[0]; + assert_eq!(attachment.color_factor_id, 1); + assert_eq!(attachment.evaluation_factor_id, 0); + assert_eq!(attachment.normalization_factor_id, 0); + assert_eq!(attachment.representative_evaluation_factor_id, 0); + assert_eq!(plan.exact_factors()[2].real_bits, (-0.0_f64).to_bits()); + + assert_eq!( + plan.selector_domains(), + &[ + EagerPlanSelectorDomainRow { + member_start: 0, + member_count: 0, + }, + EagerPlanSelectorDomainRow { + member_start: 0, + member_count: 1, + }, + ] + ); + assert_eq!(plan.selector_memberships(), &[0]); + assert_eq!(plan.closures()[0].selector_domain_id, 1); + assert_eq!(plan.helicity_selectors()[1].structural_zero, 1); + + let group = plan.reduction_groups()[0]; + assert_eq!(group.amplitude_entry_count, 1); + assert_eq!(group.selector_entry_count, 1); + assert_eq!(plan.reduction_entries().len(), 3); + assert_eq!( + plan.reduction_entries()[2], + EagerPlanReductionEntryRow { + kind: EagerPlanReductionEntryKind::ColorContraction, + owner_id: MISSING_U32, + left_id: 0, + right_id: 0, + factor_id: 0, + auxiliary_factor_id: MISSING_U32, + } + ); + assert_eq!(plan.color_contraction_entry_range(), (2, 1)); + + let shapes = plan.section_shapes().unwrap(); + assert_eq!(shapes.len(), 17); + for shape in shapes { + let header = shape.header().unwrap(); + assert_eq!(header.kind(), shape.kind); + assert_eq!(header.record_size(), shape.record_size); + assert_eq!(header.record_count(), shape.record_count); + } +} + +#[test] +fn lowering_is_byte_order_and_hash_independent_deterministic() { + let input = Fixture::valid().input(); + let first = lower_eager_plan_v3(input.clone()).unwrap(); + let second = lower_eager_plan_v3(input).unwrap(); + assert_eq!(first, second); +} + +#[test] +fn authenticated_zero_evaluation_groups_emit_no_runtime_invocation() { + let mut fixture = Fixture::valid(); + fixture.interaction_evaluation_factor[0] = 2; + + let plan = lower_eager_plan_v3(fixture.input()).unwrap(); + + assert_eq!(plan.stages().len(), 2); + assert_eq!(plan.invocations().len(), 1); + assert_eq!(plan.attachments().len(), 1); + assert_eq!(plan.finalizations().len(), 2); + assert_eq!(plan.invocations()[0].evaluation_group_id, 1); + assert_eq!(plan.attachments()[0].interaction_id, 1); +} + +#[test] +fn malformed_dense_and_cross_table_ids_fail_closed() { + let mut dense = Fixture::valid(); + dense.current_id[2] = 99; + assert_invalid(&dense, "current IDs must be dense"); + + let mut reference = Fixture::valid(); + reference.interaction_left[0] = 99; + assert_invalid(&reference, "interaction left current ID 99"); + + let mut factor = Fixture::valid(); + factor.interaction_color_factor[0] = 99; + assert_invalid(&factor, "color ID 99"); +} + +#[test] +fn malformed_group_partition_and_kernel_permutation_fail_closed() { + let mut group = Fixture::valid(); + group.group_members[1] = 0; + assert_invalid(&group, "representative is not its first member"); + + let mut order = Fixture::valid(); + order.interaction_order[0] = [0, 0]; + assert_invalid(&order, "not a permutation"); +} + +#[test] +fn equivalent_group_members_may_use_different_logical_current_ids() { + let input = Fixture::valid().input(); + let representative = input.interactions[0]; + let mut equivalent = representative; + equivalent.left_current_id = 2; + equivalent.right_current_id = 0; + validate_group_signature(&representative, &equivalent, 0).unwrap(); + + equivalent.kernel_id += 1; + equivalent.coupling_id += 1; + equivalent.canonical_input_order = [1, 0]; + validate_group_signature(&representative, &equivalent, 0).unwrap(); + + equivalent.stage_subset_size += 1; + let error = validate_group_signature(&representative, &equivalent, 0).unwrap_err(); + assert!(error.to_string().contains("crosses recursion stages")); +} + +#[test] +fn retained_table_inventory_and_payloads_are_fail_closed_and_bit_exact() { + let fixture = Fixture::valid(); + let retained_value = [-0.0_f64]; + let retained_columns = [EagerRetainedColumnView { + name: "probe", + elements_per_row: 1, + values: EagerPrimitiveColumnView::F64(&retained_value), + }]; + let mut retained_tables = fixture.retained_views(); + let metadata_index = EAGER_LOWERING_V1_TABLE_NAMES + .iter() + .position(|name| *name == "metadata") + .unwrap(); + retained_tables[metadata_index] = EagerRetainedTableView { + name: "metadata", + row_count: 1, + columns: &retained_columns, + }; + let input = EagerLoweringInputV1::try_from_view(fixture.view(&retained_tables)).unwrap(); + let metadata = input.retained_tables()[metadata_index].clone(); + assert_eq!(metadata.name(), "metadata"); + assert_eq!(metadata.row_count(), 1); + assert_eq!(metadata.columns()[0].elements_per_row(), 1); + assert_eq!( + metadata.columns()[0].values(), + &EagerOwnedPrimitiveColumn::F64Bits(vec![(-0.0_f64).to_bits()]) + ); + let plan = lower_eager_plan_v3(input).unwrap(); + assert_eq!(plan.retained_tables()[metadata_index], metadata); + + let missing = &retained_tables[..retained_tables.len() - 1]; + let error = EagerLoweringInputV1::try_from_view(fixture.view(missing)).unwrap_err(); + assert!(error.to_string().contains("retained table inventory")); + + let unsupported = ["future-proof-table"]; + let mut view = fixture.view(&retained_tables); + view.unsupported_tables = &unsupported; + let error = EagerLoweringInputV1::try_from_view(view).unwrap_err(); + assert!(error.to_string().contains("future-proof-table")); +} + +#[test] +fn forward_stage_dependency_is_rejected_during_lowering() { + let mut fixture = Fixture::valid(); + fixture.interaction_left[0] = 3; + let input = fixture.input(); + let error = lower_eager_plan_v3(input).unwrap_err(); + assert_eq!(error.kind(), RusticolErrorKind::InvalidArgument); + assert!(error.to_string().contains("before it is finalized")); +} + +#[test] +fn structural_zero_requires_an_exact_zero_factor() { + let mut fixture = Fixture::valid(); + fixture.helicity_coefficient[1] = 1; + assert_invalid(&fixture, "structural-zero helicity selector"); +} + +#[test] +fn checked_u32_and_u64_overflow_are_rejected() { + let error = count_u32(u64::from(u32::MAX) + 1, "string catalog").unwrap_err(); + assert!( + error + .to_string() + .contains("string catalog count exceeds u32") + ); + + let mut ranges = Fixture::valid(); + ranges.bitset_start = vec![0, u64::MAX, 0]; + ranges.bitset_count = vec![u64::MAX, 1, 1]; + assert_invalid(&ranges, "bitset catalog range 1 exceeds u64"); + + let error = checked_usize_range(u64::MAX, 1, 0, "overflow probe").unwrap_err(); + assert!(error.to_string().contains("exceeds u64")); + + let error = usize_present_u32(u32::MAX as usize, "selector domain ID").unwrap_err(); + assert!(error.to_string().contains("missing-ID sentinel")); +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/eager_plan_v3_pacbin.rs b/pyamplicol/rust/crates/rusticol-core/src/eager_plan_v3_pacbin.rs new file mode 100644 index 0000000000..5fa0b1bac2 --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/eager_plan_v3_pacbin.rs @@ -0,0 +1,1570 @@ +// SPDX-License-Identifier: 0BSD + +//! Deterministic, bounded-memory serialization for compact eager plan-v3. +//! +//! Every PACBIN member starts with an [`EagerSectionHeader`]. Fixed-width +//! tables are encoded one row at a time, while string and retained-column +//! catalogs are streamed from borrowed segments. The serializer therefore +//! never materializes an aggregate eager runtime payload in memory. + +use crate::eager_layout::{ + EAGER_LOWERING_INPUT_ABI, EAGER_PLAN_ABI, EAGER_RUNTIME_CAPABILITY, + EAGER_RUNTIME_CONTAINER_KIND, EAGER_RUNTIME_CONTAINER_SCHEMA, EAGER_RUNTIME_LAYOUT_ABI, + EAGER_SECTION_HEADER_SIZE, EagerSectionHeader, EagerSectionKind, +}; +use crate::eager_lowering_v3::{ + EagerOwnedPrimitiveColumn, EagerOwnedRetainedTable, EagerPlanAttachmentRow, + EagerPlanCatalogRangeRow, EagerPlanClosureRow, EagerPlanColorSelectorRow, EagerPlanCouplingRow, + EagerPlanCurrentRow, EagerPlanDirectCoefficientRow, EagerPlanExactFactorRow, + EagerPlanFinalizationRow, EagerPlanHelicitySelectorRow, EagerPlanInvocationRow, + EagerPlanMomentumRow, EagerPlanParameterRow, EagerPlanReductionEntryRow, + EagerPlanReductionGroupRow, EagerPlanSelectorDomainRow, EagerPlanSourceFillRow, + EagerPlanStageRow, EagerPlanV3, EagerPlanValueRow, +}; +use crate::pacbin::{PacbinMemberKind, PacbinWriteMember, PacbinWriteOptions, write_pacbin_atomic}; +use crate::{RusticolError, RusticolResult}; +use sha2::{Digest, Sha256}; +use std::fs::File; +use std::io::{self, Read}; +use std::path::Path; + +const SERIALIZATION_SCHEMA: u32 = 1; +const METADATA_RECORD_SIZE: u32 = 224; +const INSPECTION_RECORD_SIZE: u32 = 160; +const RETAINED_TABLE_RECORD_SIZE: u32 = 40; +const RETAINED_COLUMN_RECORD_SIZE: u32 = 40; +const DIRECT_COEFFICIENT_RECORD_SIZE: u32 = 16; +const HELICITY_SELECTOR_RECORD_SIZE: u32 = 24; +const COLOR_SELECTOR_RECORD_SIZE: u32 = 24; +const MAX_ENCODED_RECORD_SIZE: usize = METADATA_RECORD_SIZE as usize; +const FILE_HASH_BUFFER_SIZE: usize = 1024 * 1024; + +const IDENTITY_LOWERING_ABI: u32 = 0; +const IDENTITY_PLAN_ABI: u32 = 1; +const IDENTITY_RUNTIME_LAYOUT_ABI: u32 = 2; +const IDENTITY_RUNTIME_CAPABILITY: u32 = 3; +const IDENTITY_CONTAINER_KIND: u32 = 4; +const IDENTITY_PROCESS_KEY: u32 = 5; +const IDENTITY_MODEL_NAME: u32 = 6; + +const RETAINED_U8: u8 = 1; +const RETAINED_U32: u8 = 2; +const RETAINED_U64: u8 = 3; +const RETAINED_I32: u8 = 4; +const RETAINED_F64_BITS: u8 = 5; + +/// Bounded publication metadata returned after the complete container is +/// atomically installed. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct EagerPlanV3PacbinMetadata { + pub member_count: u64, + pub unpacked_bytes: u64, + pub index_sha256: [u8; 32], + pub file_size: u64, + pub file_sha256: [u8; 32], +} + +/// Atomically write one complete `eager-runtime.pacbin` from an eager plan-v3. +pub fn write_eager_plan_v3_pacbin( + plan: &EagerPlanV3, + destination: impl AsRef, +) -> RusticolResult { + let view = PlanView::from(plan); + write_plan_view(&view, destination.as_ref()) +} + +#[derive(Clone, Copy)] +struct PlanView<'a> { + abi: &'a str, + process_key: &'a str, + model_name: &'a str, + string_catalog: &'a [Box], + canonical_ir_catalog: &'a [Box], + semantic_limitations: &'a [Box], + retained_tables: &'a [EagerOwnedRetainedTable], + currents: &'a [EagerPlanCurrentRow], + values: &'a [EagerPlanValueRow], + momenta: &'a [EagerPlanMomentumRow], + sources: &'a [EagerPlanSourceFillRow], + parameters: &'a [EagerPlanParameterRow], + stages: &'a [EagerPlanStageRow], + couplings: &'a [EagerPlanCouplingRow], + invocations: &'a [EagerPlanInvocationRow], + attachments: &'a [EagerPlanAttachmentRow], + finalizations: &'a [EagerPlanFinalizationRow], + closures: &'a [EagerPlanClosureRow], + direct_coefficients: &'a [EagerPlanDirectCoefficientRow], + selector_domains: &'a [EagerPlanSelectorDomainRow], + selector_memberships: &'a [u32], + helicity_selectors: &'a [EagerPlanHelicitySelectorRow], + color_selectors: &'a [EagerPlanColorSelectorRow], + reduction_groups: &'a [EagerPlanReductionGroupRow], + reduction_entries: &'a [EagerPlanReductionEntryRow], + exact_factors: &'a [EagerPlanExactFactorRow], + bitset_ranges: &'a [EagerPlanCatalogRangeRow], + bitset_populations: &'a [u64], + bitset_words: &'a [u64], + u32_sequence_ranges: &'a [EagerPlanCatalogRangeRow], + u32_sequence_values: &'a [u32], + i32_sequence_ranges: &'a [EagerPlanCatalogRangeRow], + i32_sequence_values: &'a [i32], + current_component_count: u64, + value_component_count: u64, + momentum_component_count: u64, + color_contraction_entry_start: u64, + color_contraction_entry_count: u64, +} + +impl<'a> From<&'a EagerPlanV3> for PlanView<'a> { + fn from(plan: &'a EagerPlanV3) -> Self { + let (color_contraction_entry_start, color_contraction_entry_count) = + plan.color_contraction_entry_range(); + Self { + abi: plan.abi(), + process_key: plan.process_key(), + model_name: plan.model_name(), + string_catalog: plan.string_catalog(), + canonical_ir_catalog: plan.canonical_ir_catalog(), + semantic_limitations: plan.semantic_limitations(), + retained_tables: plan.retained_tables(), + currents: plan.currents(), + values: plan.values(), + momenta: plan.momenta(), + sources: plan.sources(), + parameters: plan.parameters(), + stages: plan.stages(), + couplings: plan.couplings(), + invocations: plan.invocations(), + attachments: plan.attachments(), + finalizations: plan.finalizations(), + closures: plan.closures(), + direct_coefficients: plan.direct_coefficients(), + selector_domains: plan.selector_domains(), + selector_memberships: plan.selector_memberships(), + helicity_selectors: plan.helicity_selectors(), + color_selectors: plan.color_selectors(), + reduction_groups: plan.reduction_groups(), + reduction_entries: plan.reduction_entries(), + exact_factors: plan.exact_factors(), + bitset_ranges: plan.bitset_ranges(), + bitset_populations: plan.bitset_populations(), + bitset_words: plan.bitset_words(), + u32_sequence_ranges: plan.u32_sequence_ranges(), + u32_sequence_values: plan.u32_sequence_values(), + i32_sequence_ranges: plan.i32_sequence_ranges(), + i32_sequence_values: plan.i32_sequence_values(), + current_component_count: plan.current_component_count(), + value_component_count: plan.value_component_count(), + momentum_component_count: plan.momentum_component_count(), + color_contraction_entry_start, + color_contraction_entry_count, + } + } +} + +fn write_plan_view( + plan: &PlanView<'_>, + destination: &Path, +) -> RusticolResult { + let context = SerializationContext::new(plan)?; + let mut sections = context.sections(plan)?; + let mut members = Vec::with_capacity(sections.len()); + for section in &mut sections { + members.push(PacbinWriteMember::from_reader( + section.logical_path, + section.member_kind, + &mut section.reader, + )?); + } + let index = write_pacbin_atomic(destination, members, PacbinWriteOptions::default())?; + let unpacked_bytes = index.members().iter().try_fold(0_u64, |total, member| { + total.checked_add(member.length()).ok_or_else(|| { + RusticolError::serialization("eager PACBIN unpacked byte count exceeds u64") + }) + })?; + let file_sha256 = hash_file(destination)?; + Ok(EagerPlanV3PacbinMetadata { + member_count: u64::try_from(index.members().len()) + .map_err(|_| RusticolError::serialization("eager PACBIN member count exceeds u64"))?, + unpacked_bytes, + index_sha256: *index.index_sha256(), + file_size: index.file_size(), + file_sha256, + }) +} + +fn hash_file(path: &Path) -> RusticolResult<[u8; 32]> { + let mut file = File::open(path).map_err(|error| { + RusticolError::artifact(format!( + "could not open published eager PACBIN {} for hashing: {error}", + path.display() + )) + })?; + let mut digest = Sha256::new(); + let mut buffer = vec![0_u8; FILE_HASH_BUFFER_SIZE]; + loop { + let count = file.read(&mut buffer).map_err(|error| { + RusticolError::artifact(format!( + "could not hash published eager PACBIN {}: {error}", + path.display() + )) + })?; + if count == 0 { + break; + } + digest.update(&buffer[..count]); + } + Ok(digest.finalize().into()) +} + +struct SerializationContext<'a> { + identity: TextCatalog<'a>, + strings: TextCatalog<'a>, + exact_ir: TextCatalog<'a>, + limitations: TextCatalog<'a>, + retained: RetainedCatalog<'a>, + metadata: MetadataRecord, + inspection: InspectionRecord, +} + +impl<'a> SerializationContext<'a> { + fn new(plan: &PlanView<'a>) -> RusticolResult { + if plan.abi != EAGER_PLAN_ABI { + return Err(RusticolError::compatibility(format!( + "cannot serialize unsupported eager plan ABI {:?}", + plan.abi + ))); + } + let identity = TextCatalog::new([ + EAGER_LOWERING_INPUT_ABI.as_bytes(), + plan.abi.as_bytes(), + EAGER_RUNTIME_LAYOUT_ABI.as_bytes(), + EAGER_RUNTIME_CAPABILITY.as_bytes(), + EAGER_RUNTIME_CONTAINER_KIND.as_bytes(), + plan.process_key.as_bytes(), + plan.model_name.as_bytes(), + ])?; + let retained = RetainedCatalog::new(plan.retained_tables)?; + let metadata = MetadataRecord::new(plan, &retained)?; + let inspection = InspectionRecord::new(plan)?; + Ok(Self { + identity, + strings: TextCatalog::new(plan.string_catalog.iter().map(|value| value.as_bytes()))?, + exact_ir: TextCatalog::new( + plan.canonical_ir_catalog + .iter() + .map(|value| value.as_bytes()), + )?, + limitations: TextCatalog::new( + plan.semantic_limitations + .iter() + .map(|value| value.as_bytes()), + )?, + retained, + metadata, + inspection, + }) + } + + fn sections<'b>(&'b self, plan: &'b PlanView<'a>) -> RusticolResult>> + where + 'a: 'b, + { + let mut sections = Vec::with_capacity(48); + push_fixed( + &mut sections, + "metadata/core.bin", + PacbinMemberKind::EagerRuntimeMetadata, + EagerSectionKind::Metadata, + METADATA_RECORD_SIZE, + std::slice::from_ref(&self.metadata), + encode_metadata, + )?; + push_text_catalog( + &mut sections, + "metadata/identity", + EagerSectionKind::Metadata, + &self.identity, + )?; + push_text_catalog( + &mut sections, + "catalogs/strings", + EagerSectionKind::Metadata, + &self.strings, + )?; + push_text_catalog( + &mut sections, + "catalogs/exact-ir", + EagerSectionKind::Metadata, + &self.exact_ir, + )?; + push_text_catalog( + &mut sections, + "catalogs/semantic-limitations", + EagerSectionKind::Metadata, + &self.limitations, + )?; + self.retained.push_sections(&mut sections)?; + + push_fixed_table( + &mut sections, + "tables/currents.bin", + EagerSectionKind::CurrentLayout, + EagerPlanCurrentRow::ENCODED_LEN, + plan.currents, + encode_current, + )?; + push_fixed_table( + &mut sections, + "tables/values.bin", + EagerSectionKind::ValueLayout, + EagerPlanValueRow::ENCODED_LEN, + plan.values, + encode_value, + )?; + push_fixed_table( + &mut sections, + "tables/momenta.bin", + EagerSectionKind::MomentumLayout, + EagerPlanMomentumRow::ENCODED_LEN, + plan.momenta, + encode_momentum, + )?; + push_fixed_table( + &mut sections, + "tables/sources.bin", + EagerSectionKind::SourceFill, + EagerPlanSourceFillRow::ENCODED_LEN, + plan.sources, + encode_source, + )?; + push_fixed_table( + &mut sections, + "tables/parameters.bin", + EagerSectionKind::ParameterLayout, + EagerPlanParameterRow::ENCODED_LEN, + plan.parameters, + encode_parameter, + )?; + push_fixed_table( + &mut sections, + "tables/stages.bin", + EagerSectionKind::Stages, + EagerPlanStageRow::ENCODED_LEN, + plan.stages, + encode_stage, + )?; + push_fixed_table( + &mut sections, + "tables/couplings.bin", + EagerSectionKind::Couplings, + EagerPlanCouplingRow::ENCODED_LEN, + plan.couplings, + encode_coupling, + )?; + push_fixed_table( + &mut sections, + "tables/invocations.bin", + EagerSectionKind::Invocations, + EagerPlanInvocationRow::ENCODED_LEN, + plan.invocations, + encode_invocation, + )?; + push_fixed_table( + &mut sections, + "tables/attachments.bin", + EagerSectionKind::Attachments, + EagerPlanAttachmentRow::ENCODED_LEN, + plan.attachments, + encode_attachment, + )?; + push_fixed_table( + &mut sections, + "tables/finalizations.bin", + EagerSectionKind::Finalizations, + EagerPlanFinalizationRow::ENCODED_LEN, + plan.finalizations, + encode_finalization, + )?; + push_fixed_table( + &mut sections, + "tables/closures.bin", + EagerSectionKind::Closures, + EagerPlanClosureRow::ENCODED_LEN, + plan.closures, + encode_closure, + )?; + push_fixed_table( + &mut sections, + "tables/direct-coefficients.bin", + EagerSectionKind::Closures, + DIRECT_COEFFICIENT_RECORD_SIZE, + plan.direct_coefficients, + encode_direct_coefficient, + )?; + push_fixed_table( + &mut sections, + "selectors/domains.bin", + EagerSectionKind::SelectorDomains, + EagerPlanSelectorDomainRow::ENCODED_LEN, + plan.selector_domains, + encode_selector_domain, + )?; + push_primitive( + &mut sections, + "selectors/memberships.bin", + EagerSectionKind::SelectorMemberships, + plan.selector_memberships, + )?; + push_fixed_table( + &mut sections, + "selectors/helicities.bin", + EagerSectionKind::SelectorDomains, + HELICITY_SELECTOR_RECORD_SIZE, + plan.helicity_selectors, + encode_helicity_selector, + )?; + push_fixed_table( + &mut sections, + "selectors/colors.bin", + EagerSectionKind::SelectorDomains, + COLOR_SELECTOR_RECORD_SIZE, + plan.color_selectors, + encode_color_selector, + )?; + push_fixed_table( + &mut sections, + "reductions/groups.bin", + EagerSectionKind::ReductionGroups, + EagerPlanReductionGroupRow::ENCODED_LEN, + plan.reduction_groups, + encode_reduction_group, + )?; + push_fixed_table( + &mut sections, + "reductions/entries.bin", + EagerSectionKind::ReductionEntries, + EagerPlanReductionEntryRow::ENCODED_LEN, + plan.reduction_entries, + encode_reduction_entry, + )?; + push_fixed_table( + &mut sections, + "catalogs/exact-factors.bin", + EagerSectionKind::ExactFactors, + EagerPlanExactFactorRow::ENCODED_LEN, + plan.exact_factors, + encode_exact_factor, + )?; + push_fixed_table( + &mut sections, + "catalogs/bitsets/ranges.bin", + EagerSectionKind::Metadata, + EagerPlanCatalogRangeRow::ENCODED_LEN, + plan.bitset_ranges, + encode_catalog_range, + )?; + push_primitive( + &mut sections, + "catalogs/bitsets/populations.bin", + EagerSectionKind::Metadata, + plan.bitset_populations, + )?; + push_primitive( + &mut sections, + "catalogs/bitsets/words.bin", + EagerSectionKind::Metadata, + plan.bitset_words, + )?; + push_fixed_table( + &mut sections, + "catalogs/u32-sequences/ranges.bin", + EagerSectionKind::Metadata, + EagerPlanCatalogRangeRow::ENCODED_LEN, + plan.u32_sequence_ranges, + encode_catalog_range, + )?; + push_primitive( + &mut sections, + "catalogs/u32-sequences/values.bin", + EagerSectionKind::Metadata, + plan.u32_sequence_values, + )?; + push_fixed_table( + &mut sections, + "catalogs/i32-sequences/ranges.bin", + EagerSectionKind::Metadata, + EagerPlanCatalogRangeRow::ENCODED_LEN, + plan.i32_sequence_ranges, + encode_catalog_range, + )?; + push_primitive( + &mut sections, + "catalogs/i32-sequences/values.bin", + EagerSectionKind::Metadata, + plan.i32_sequence_values, + )?; + push_fixed( + &mut sections, + "inspection/summary.bin", + PacbinMemberKind::EagerRuntimeMetadata, + EagerSectionKind::InspectionSummary, + INSPECTION_RECORD_SIZE, + std::slice::from_ref(&self.inspection), + encode_inspection, + )?; + Ok(sections) + } +} + +#[derive(Clone, Copy)] +struct MetadataRecord { + retained_column_count: u64, + current_component_count: u64, + value_component_count: u64, + momentum_component_count: u64, + color_contraction_entry_start: u64, + color_contraction_entry_count: u64, + string_count: u64, + exact_ir_count: u64, + limitation_count: u64, + retained_table_count: u64, + current_count: u64, + value_count: u64, + momentum_count: u64, + source_count: u64, + stage_count: u64, + invocation_count: u64, + closure_count: u64, + direct_coefficient_count: u64, + helicity_selector_count: u64, + color_selector_count: u64, + exact_factor_count: u64, +} + +impl MetadataRecord { + fn new(plan: &PlanView<'_>, retained: &RetainedCatalog<'_>) -> RusticolResult { + Ok(Self { + retained_column_count: usize_u64(retained.columns.len(), "retained column count")?, + current_component_count: plan.current_component_count, + value_component_count: plan.value_component_count, + momentum_component_count: plan.momentum_component_count, + color_contraction_entry_start: plan.color_contraction_entry_start, + color_contraction_entry_count: plan.color_contraction_entry_count, + string_count: usize_u64(plan.string_catalog.len(), "string catalog count")?, + exact_ir_count: usize_u64(plan.canonical_ir_catalog.len(), "exact IR count")?, + limitation_count: usize_u64( + plan.semantic_limitations.len(), + "semantic limitation count", + )?, + retained_table_count: usize_u64(plan.retained_tables.len(), "retained table count")?, + current_count: usize_u64(plan.currents.len(), "current count")?, + value_count: usize_u64(plan.values.len(), "value count")?, + momentum_count: usize_u64(plan.momenta.len(), "momentum count")?, + source_count: usize_u64(plan.sources.len(), "source count")?, + stage_count: usize_u64(plan.stages.len(), "stage count")?, + invocation_count: usize_u64(plan.invocations.len(), "invocation count")?, + closure_count: usize_u64(plan.closures.len(), "closure count")?, + direct_coefficient_count: usize_u64( + plan.direct_coefficients.len(), + "direct coefficient count", + )?, + helicity_selector_count: usize_u64( + plan.helicity_selectors.len(), + "helicity selector count", + )?, + color_selector_count: usize_u64(plan.color_selectors.len(), "color selector count")?, + exact_factor_count: usize_u64(plan.exact_factors.len(), "exact factor count")?, + }) + } +} + +#[derive(Clone, Copy)] +struct InspectionRecord { + counts: [u64; 20], +} + +impl InspectionRecord { + fn new(plan: &PlanView<'_>) -> RusticolResult { + Ok(Self { + counts: [ + usize_u64(plan.currents.len(), "inspection current count")?, + usize_u64(plan.values.len(), "inspection value count")?, + usize_u64(plan.momenta.len(), "inspection momentum count")?, + usize_u64(plan.sources.len(), "inspection source count")?, + usize_u64(plan.parameters.len(), "inspection parameter count")?, + usize_u64(plan.stages.len(), "inspection stage count")?, + usize_u64(plan.couplings.len(), "inspection coupling count")?, + usize_u64(plan.invocations.len(), "inspection invocation count")?, + usize_u64(plan.attachments.len(), "inspection attachment count")?, + usize_u64(plan.finalizations.len(), "inspection finalization count")?, + usize_u64(plan.closures.len(), "inspection closure count")?, + usize_u64( + plan.direct_coefficients.len(), + "inspection direct coefficient count", + )?, + usize_u64( + plan.selector_domains.len(), + "inspection selector domain count", + )?, + usize_u64( + plan.selector_memberships.len(), + "inspection selector membership count", + )?, + usize_u64( + plan.helicity_selectors.len(), + "inspection helicity selector count", + )?, + usize_u64( + plan.color_selectors.len(), + "inspection color selector count", + )?, + usize_u64( + plan.reduction_groups.len(), + "inspection reduction group count", + )?, + usize_u64( + plan.reduction_entries.len(), + "inspection reduction entry count", + )?, + usize_u64(plan.exact_factors.len(), "inspection exact factor count")?, + usize_u64( + plan.retained_tables.len(), + "inspection retained table count", + )?, + ], + }) + } +} + +struct TextCatalog<'a> { + ranges: Vec, + segments: Vec<&'a [u8]>, + byte_count: u64, +} + +impl<'a> TextCatalog<'a> { + fn new(values: impl IntoIterator) -> RusticolResult { + let values = values.into_iter(); + let (minimum, _) = values.size_hint(); + let mut ranges = Vec::with_capacity(minimum); + let mut segments = Vec::with_capacity(minimum); + let mut byte_count = 0_u64; + for value in values { + let count = usize_u64(value.len(), "text catalog entry length")?; + ranges.push(EagerPlanCatalogRangeRow { + start: byte_count, + count, + }); + byte_count = byte_count.checked_add(count).ok_or_else(|| { + RusticolError::serialization("text catalog byte count exceeds u64") + })?; + segments.push(value); + } + Ok(Self { + ranges, + segments, + byte_count, + }) + } +} + +#[derive(Clone, Copy)] +struct RetainedTableRecord { + table_id: u32, + name_id: u32, + row_count: u64, + column_start: u64, + column_count: u64, +} + +#[derive(Clone, Copy)] +struct RetainedColumnRecord { + table_id: u32, + column_id: u32, + name_id: u32, + primitive_kind: u8, + elements_per_row: u32, + value_start: u64, + value_count: u64, +} + +struct RetainedCatalog<'a> { + names: TextCatalog<'a>, + tables: Vec, + columns: Vec, + u8_segments: Vec<&'a [u8]>, + u32_segments: Vec<&'a [u32]>, + u64_segments: Vec<&'a [u64]>, + i32_segments: Vec<&'a [i32]>, + f64_bits_segments: Vec<&'a [u64]>, + u8_count: u64, + u32_count: u64, + u64_count: u64, + i32_count: u64, + f64_bits_count: u64, +} + +impl<'a> RetainedCatalog<'a> { + fn new(tables: &'a [EagerOwnedRetainedTable]) -> RusticolResult { + let column_capacity = tables.iter().try_fold(0_usize, |total, table| { + total + .checked_add(table.columns().len()) + .ok_or_else(|| RusticolError::serialization("retained column count exceeds usize")) + })?; + let names = TextCatalog::new(tables.iter().flat_map(|table| { + std::iter::once(table.name().as_bytes()).chain( + table + .columns() + .iter() + .map(|column| column.name().as_bytes()), + ) + }))?; + let mut records = Vec::with_capacity(tables.len()); + let mut columns = Vec::with_capacity(column_capacity); + let mut u8_segments = Vec::new(); + let mut u32_segments = Vec::new(); + let mut u64_segments = Vec::new(); + let mut i32_segments = Vec::new(); + let mut f64_bits_segments = Vec::new(); + let mut name_id = 0_u32; + let mut u8_count = 0_u64; + let mut u32_count = 0_u64; + let mut u64_count = 0_u64; + let mut i32_count = 0_u64; + let mut f64_bits_count = 0_u64; + for (table_index, table) in tables.iter().enumerate() { + let table_id = usize_u32(table_index, "retained table ID")?; + let column_start = usize_u64(columns.len(), "retained column start")?; + let table_name_id = name_id; + name_id = name_id + .checked_add(1) + .ok_or_else(|| RusticolError::serialization("retained name count exceeds u32"))?; + for (column_index, column) in table.columns().iter().enumerate() { + let column_id = usize_u32(column_index, "retained column ID")?; + let column_name_id = name_id; + name_id = name_id.checked_add(1).ok_or_else(|| { + RusticolError::serialization("retained name count exceeds u32") + })?; + let (primitive_kind, value_start, value_count) = match column.values() { + EagerOwnedPrimitiveColumn::U8(values) => { + append_segment(&mut u8_segments, &mut u8_count, values, RETAINED_U8)? + } + EagerOwnedPrimitiveColumn::U32(values) => { + append_segment(&mut u32_segments, &mut u32_count, values, RETAINED_U32)? + } + EagerOwnedPrimitiveColumn::U64(values) => { + append_segment(&mut u64_segments, &mut u64_count, values, RETAINED_U64)? + } + EagerOwnedPrimitiveColumn::I32(values) => { + append_segment(&mut i32_segments, &mut i32_count, values, RETAINED_I32)? + } + EagerOwnedPrimitiveColumn::F64Bits(values) => append_segment( + &mut f64_bits_segments, + &mut f64_bits_count, + values, + RETAINED_F64_BITS, + )?, + }; + columns.push(RetainedColumnRecord { + table_id, + column_id, + name_id: column_name_id, + primitive_kind, + elements_per_row: column.elements_per_row(), + value_start, + value_count, + }); + } + records.push(RetainedTableRecord { + table_id, + name_id: table_name_id, + row_count: table.row_count(), + column_start, + column_count: usize_u64(table.columns().len(), "retained table column count")?, + }); + } + Ok(Self { + names, + tables: records, + columns, + u8_segments, + u32_segments, + u64_segments, + i32_segments, + f64_bits_segments, + u8_count, + u32_count, + u64_count, + i32_count, + f64_bits_count, + }) + } + + fn push_sections<'b>(&'b self, sections: &mut Vec>) -> RusticolResult<()> { + push_text_catalog( + sections, + "retained/names", + EagerSectionKind::Metadata, + &self.names, + )?; + push_fixed_table( + sections, + "retained/tables.bin", + EagerSectionKind::Metadata, + RETAINED_TABLE_RECORD_SIZE, + &self.tables, + encode_retained_table, + )?; + push_fixed_table( + sections, + "retained/columns.bin", + EagerSectionKind::Metadata, + RETAINED_COLUMN_RECORD_SIZE, + &self.columns, + encode_retained_column, + )?; + push_segmented_primitive( + sections, + "retained/values-u8.bin", + EagerSectionKind::Metadata, + &self.u8_segments, + self.u8_count, + )?; + push_segmented_primitive( + sections, + "retained/values-u32.bin", + EagerSectionKind::Metadata, + &self.u32_segments, + self.u32_count, + )?; + push_segmented_primitive( + sections, + "retained/values-u64.bin", + EagerSectionKind::Metadata, + &self.u64_segments, + self.u64_count, + )?; + push_segmented_primitive( + sections, + "retained/values-i32.bin", + EagerSectionKind::Metadata, + &self.i32_segments, + self.i32_count, + )?; + push_segmented_primitive( + sections, + "retained/values-f64-bits.bin", + EagerSectionKind::Metadata, + &self.f64_bits_segments, + self.f64_bits_count, + )?; + Ok(()) + } +} + +fn append_segment<'a, T>( + segments: &mut Vec<&'a [T]>, + total: &mut u64, + values: &'a [T], + primitive_kind: u8, +) -> RusticolResult<(u8, u64, u64)> { + let start = *total; + let count = usize_u64(values.len(), "retained value count")?; + *total = total + .checked_add(count) + .ok_or_else(|| RusticolError::serialization("retained value count exceeds u64"))?; + segments.push(values); + Ok((primitive_kind, start, count)) +} + +fn usize_u64(value: usize, description: &str) -> RusticolResult { + u64::try_from(value) + .map_err(|_| RusticolError::serialization(format!("{description} exceeds u64"))) +} + +fn usize_u32(value: usize, description: &str) -> RusticolResult { + u32::try_from(value) + .map_err(|_| RusticolError::serialization(format!("{description} exceeds u32"))) +} + +struct NamedSection<'a> { + logical_path: &'static str, + member_kind: PacbinMemberKind, + reader: EagerSectionStream<'a>, +} + +struct EagerSectionStream<'a> { + header: [u8; EAGER_SECTION_HEADER_SIZE], + header_position: usize, + payload: Box, + payload_remaining: u64, +} + +impl<'a> EagerSectionStream<'a> { + fn new( + kind: EagerSectionKind, + record_size: u32, + record_count: u64, + payload: impl Read + 'a, + ) -> RusticolResult { + let header = EagerSectionHeader::new(kind, record_size, record_count)?; + Ok(Self { + header: header.encode(), + header_position: 0, + payload: Box::new(payload), + payload_remaining: header.payload_length(), + }) + } +} + +impl Read for EagerSectionStream<'_> { + fn read(&mut self, output: &mut [u8]) -> io::Result { + if output.is_empty() { + return Ok(0); + } + let mut written = 0; + if self.header_position < self.header.len() { + let count = (self.header.len() - self.header_position).min(output.len()); + output[..count] + .copy_from_slice(&self.header[self.header_position..self.header_position + count]); + self.header_position += count; + written += count; + } + if written == output.len() || self.payload_remaining == 0 { + return Ok(written); + } + let maximum = usize::try_from(self.payload_remaining) + .unwrap_or(usize::MAX) + .min(output.len() - written); + let count = self.payload.read(&mut output[written..written + maximum])?; + if count == 0 { + return Err(io::Error::new( + io::ErrorKind::UnexpectedEof, + "eager section payload ended before its declared length", + )); + } + let count_u64 = u64::try_from(count) + .map_err(|_| io::Error::other("eager section read count exceeds u64"))?; + self.payload_remaining -= count_u64; + written += count; + Ok(written) + } +} + +struct FixedRowsReader<'a, T> { + rows: &'a [T], + record_size: usize, + total_bytes: u64, + byte_position: u64, + encoder: fn(&T, &mut [u8]), + scratch: [u8; MAX_ENCODED_RECORD_SIZE], +} + +impl<'a, T> FixedRowsReader<'a, T> { + fn new(rows: &'a [T], record_size: u32, encoder: fn(&T, &mut [u8])) -> RusticolResult { + let record_size_u64 = u64::from(record_size); + let record_size = usize::try_from(record_size).map_err(|_| { + RusticolError::serialization("eager record size exceeds platform usize") + })?; + if record_size == 0 || record_size > MAX_ENCODED_RECORD_SIZE { + return Err(RusticolError::serialization(format!( + "eager record size {record_size} exceeds streaming scratch bound {MAX_ENCODED_RECORD_SIZE}" + ))); + } + let row_count = usize_u64(rows.len(), "fixed eager table row count")?; + let total_bytes = row_count + .checked_mul(record_size_u64) + .ok_or_else(|| RusticolError::serialization("fixed eager table size exceeds u64"))?; + Ok(Self { + rows, + record_size, + total_bytes, + byte_position: 0, + encoder, + scratch: [0; MAX_ENCODED_RECORD_SIZE], + }) + } +} + +impl Read for FixedRowsReader<'_, T> { + fn read(&mut self, output: &mut [u8]) -> io::Result { + if self.byte_position >= self.total_bytes || output.is_empty() { + return Ok(0); + } + let mut written = 0; + while written < output.len() && self.byte_position < self.total_bytes { + let record_size_u64 = u64::try_from(self.record_size) + .map_err(|_| io::Error::other("eager record size exceeds u64"))?; + let row_index = usize::try_from(self.byte_position / record_size_u64) + .map_err(|_| io::Error::other("eager row index exceeds usize"))?; + let row_offset = usize::try_from(self.byte_position % record_size_u64) + .map_err(|_| io::Error::other("eager row offset exceeds usize"))?; + self.scratch[..self.record_size].fill(0); + (self.encoder)(&self.rows[row_index], &mut self.scratch[..self.record_size]); + let count = (self.record_size - row_offset).min(output.len() - written); + output[written..written + count] + .copy_from_slice(&self.scratch[row_offset..row_offset + count]); + written += count; + self.byte_position = self + .byte_position + .checked_add( + u64::try_from(count) + .map_err(|_| io::Error::other("eager read count exceeds u64"))?, + ) + .ok_or_else(|| io::Error::other("eager byte position exceeds u64"))?; + } + Ok(written) + } +} + +trait LittleEndianPrimitive: Copy { + const ENCODED_LEN: u32; + fn encode(self, output: &mut [u8]); +} + +impl LittleEndianPrimitive for u8 { + const ENCODED_LEN: u32 = 1; + + fn encode(self, output: &mut [u8]) { + output[0] = self; + } +} + +impl LittleEndianPrimitive for u32 { + const ENCODED_LEN: u32 = 4; + + fn encode(self, output: &mut [u8]) { + output.copy_from_slice(&self.to_le_bytes()); + } +} + +impl LittleEndianPrimitive for i32 { + const ENCODED_LEN: u32 = 4; + + fn encode(self, output: &mut [u8]) { + output.copy_from_slice(&self.to_le_bytes()); + } +} + +impl LittleEndianPrimitive for u64 { + const ENCODED_LEN: u32 = 8; + + fn encode(self, output: &mut [u8]) { + output.copy_from_slice(&self.to_le_bytes()); + } +} + +struct SegmentedPrimitiveReader<'a, T> { + segments: &'a [&'a [T]], + segment_index: usize, + value_index: usize, + pending: [u8; 8], + pending_position: usize, + pending_length: usize, +} + +impl<'a, T> SegmentedPrimitiveReader<'a, T> { + fn new(segments: &'a [&'a [T]]) -> Self { + Self { + segments, + segment_index: 0, + value_index: 0, + pending: [0; 8], + pending_position: 0, + pending_length: 0, + } + } +} + +impl Read for SegmentedPrimitiveReader<'_, T> { + fn read(&mut self, output: &mut [u8]) -> io::Result { + if output.is_empty() { + return Ok(0); + } + let mut written = 0; + while written < output.len() { + if self.pending_position < self.pending_length { + let count = + (self.pending_length - self.pending_position).min(output.len() - written); + output[written..written + count].copy_from_slice( + &self.pending[self.pending_position..self.pending_position + count], + ); + self.pending_position += count; + written += count; + continue; + } + while self.segment_index < self.segments.len() + && self.value_index == self.segments[self.segment_index].len() + { + self.segment_index += 1; + self.value_index = 0; + } + if self.segment_index == self.segments.len() { + break; + } + self.pending.fill(0); + let value = self.segments[self.segment_index][self.value_index]; + value.encode(&mut self.pending[..T::ENCODED_LEN as usize]); + self.pending_position = 0; + self.pending_length = T::ENCODED_LEN as usize; + self.value_index += 1; + } + Ok(written) + } +} + +fn push_fixed_table<'a, T>( + sections: &mut Vec>, + logical_path: &'static str, + section_kind: EagerSectionKind, + record_size: u32, + rows: &'a [T], + encoder: fn(&T, &mut [u8]), +) -> RusticolResult<()> { + push_fixed( + sections, + logical_path, + PacbinMemberKind::EagerRuntimeTable, + section_kind, + record_size, + rows, + encoder, + ) +} + +fn push_fixed<'a, T>( + sections: &mut Vec>, + logical_path: &'static str, + member_kind: PacbinMemberKind, + section_kind: EagerSectionKind, + record_size: u32, + rows: &'a [T], + encoder: fn(&T, &mut [u8]), +) -> RusticolResult<()> { + let count = usize_u64(rows.len(), "eager section row count")?; + sections.push(NamedSection { + logical_path, + member_kind, + reader: EagerSectionStream::new( + section_kind, + record_size, + count, + FixedRowsReader::new(rows, record_size, encoder)?, + )?, + }); + Ok(()) +} + +fn push_primitive<'a, T: LittleEndianPrimitive + 'a>( + sections: &mut Vec>, + logical_path: &'static str, + section_kind: EagerSectionKind, + values: &'a [T], +) -> RusticolResult<()> { + push_fixed( + sections, + logical_path, + PacbinMemberKind::EagerRuntimeTable, + section_kind, + T::ENCODED_LEN, + values, + encode_primitive::, + ) +} + +fn encode_primitive(value: &T, output: &mut [u8]) { + value.encode(output); +} + +fn push_segmented_primitive<'a, T: LittleEndianPrimitive + 'a>( + sections: &mut Vec>, + logical_path: &'static str, + section_kind: EagerSectionKind, + segments: &'a [&'a [T]], + count: u64, +) -> RusticolResult<()> { + let actual_count = segments.iter().try_fold(0_u64, |total, values| { + total + .checked_add(usize_u64(values.len(), "segmented primitive count")?) + .ok_or_else(|| RusticolError::serialization("segmented primitive count exceeds u64")) + })?; + if actual_count != count { + return Err(RusticolError::serialization(format!( + "segmented primitive count mismatch: declared {count}, encoded {actual_count}" + ))); + } + sections.push(NamedSection { + logical_path, + member_kind: PacbinMemberKind::EagerRuntimeTable, + reader: EagerSectionStream::new( + section_kind, + T::ENCODED_LEN, + count, + SegmentedPrimitiveReader::new(segments), + )?, + }); + Ok(()) +} + +fn push_text_catalog<'a>( + sections: &mut Vec>, + prefix: &'static str, + section_kind: EagerSectionKind, + catalog: &'a TextCatalog<'a>, +) -> RusticolResult<()> { + let (ranges_path, bytes_path) = match prefix { + "metadata/identity" => ( + "metadata/identity-ranges.bin", + "metadata/identity-bytes.bin", + ), + "catalogs/strings" => ("catalogs/strings/ranges.bin", "catalogs/strings/bytes.bin"), + "catalogs/exact-ir" => ( + "catalogs/exact-ir/ranges.bin", + "catalogs/exact-ir/bytes.bin", + ), + "catalogs/semantic-limitations" => ( + "catalogs/semantic-limitations/ranges.bin", + "catalogs/semantic-limitations/bytes.bin", + ), + "retained/names" => ("retained/name-ranges.bin", "retained/name-bytes.bin"), + _ => { + return Err(RusticolError::internal(format!( + "unknown eager text catalog prefix: {prefix}" + ))); + } + }; + push_fixed( + sections, + ranges_path, + PacbinMemberKind::EagerRuntimeMetadata, + section_kind, + EagerPlanCatalogRangeRow::ENCODED_LEN, + &catalog.ranges, + encode_catalog_range, + )?; + sections.push(NamedSection { + logical_path: bytes_path, + member_kind: PacbinMemberKind::EagerRuntimeMetadata, + reader: EagerSectionStream::new( + section_kind, + 1, + catalog.byte_count, + SegmentedPrimitiveReader::new(&catalog.segments), + )?, + }); + Ok(()) +} + +fn encode_metadata(row: &MetadataRecord, output: &mut [u8]) { + put_u32(output, 0, SERIALIZATION_SCHEMA); + put_u16(output, 4, EAGER_RUNTIME_CONTAINER_SCHEMA); + put_u32(output, 8, IDENTITY_LOWERING_ABI); + put_u32(output, 12, IDENTITY_PLAN_ABI); + put_u32(output, 16, IDENTITY_RUNTIME_LAYOUT_ABI); + put_u32(output, 20, IDENTITY_RUNTIME_CAPABILITY); + put_u32(output, 24, IDENTITY_CONTAINER_KIND); + put_u32(output, 28, IDENTITY_PROCESS_KEY); + put_u32(output, 32, IDENTITY_MODEL_NAME); + put_u32(output, 36, 7); + let values = [ + row.retained_column_count, + row.current_component_count, + row.value_component_count, + row.momentum_component_count, + row.color_contraction_entry_start, + row.color_contraction_entry_count, + row.string_count, + row.exact_ir_count, + row.limitation_count, + row.retained_table_count, + row.current_count, + row.value_count, + row.momentum_count, + row.source_count, + row.stage_count, + row.invocation_count, + row.closure_count, + row.direct_coefficient_count, + row.helicity_selector_count, + row.color_selector_count, + row.exact_factor_count, + ]; + for (index, value) in values.into_iter().enumerate() { + put_u64(output, 40 + index * 8, value); + } +} + +fn encode_inspection(row: &InspectionRecord, output: &mut [u8]) { + for (index, value) in row.counts.into_iter().enumerate() { + put_u64(output, index * 8, value); + } +} + +fn encode_retained_table(row: &RetainedTableRecord, output: &mut [u8]) { + put_u32(output, 0, row.table_id); + put_u32(output, 4, row.name_id); + put_u64(output, 8, row.row_count); + put_u64(output, 16, row.column_start); + put_u64(output, 24, row.column_count); +} + +fn encode_retained_column(row: &RetainedColumnRecord, output: &mut [u8]) { + put_u32(output, 0, row.table_id); + put_u32(output, 4, row.column_id); + put_u32(output, 8, row.name_id); + put_u8(output, 12, row.primitive_kind); + put_u32(output, 16, row.elements_per_row); + put_u64(output, 24, row.value_start); + put_u64(output, 32, row.value_count); +} + +fn encode_current(row: &EagerPlanCurrentRow, output: &mut [u8]) { + put_u32(output, 0, row.current_id); + put_u64(output, 4, row.component_start); + put_u32(output, 12, row.component_count); + put_u32(output, 16, row.momentum_slot_id); + put_u32(output, 20, row.flags); +} + +fn encode_value(row: &EagerPlanValueRow, output: &mut [u8]) { + put_u32(output, 0, row.value_slot_id); + put_u32(output, 4, row.current_id); + put_u64(output, 8, row.component_start); + put_u32(output, 16, row.component_count); + put_u8(output, 20, row.kind as u8); +} + +fn encode_momentum(row: &EagerPlanMomentumRow, output: &mut [u8]) { + put_u32(output, 0, row.momentum_slot_id); + put_u32(output, 4, row.bitset_id); + put_u64(output, 8, row.component_start); + put_u32(output, 16, row.component_count); +} + +fn encode_source(row: &EagerPlanSourceFillRow, output: &mut [u8]) { + let values = [ + row.source_id, + row.current_id, + row.value_slot_id, + row.external_label, + row.input_momentum_slot, + row.source_ir_id, + row.crossing_ir_id, + row.crossing_factor_id, + row.declared_state_index, + ]; + put_u32_values(output, &values); +} + +fn encode_parameter(row: &EagerPlanParameterRow, output: &mut [u8]) { + put_u32(output, 0, row.parameter_id); + put_u32(output, 4, row.name_string_id); + put_u32(output, 8, row.kind_string_id); + put_u32(output, 12, row.default_factor_id); + put_u32(output, 16, row.runtime_name_string_id); + put_i32(output, 20, row.complex_component); + put_u32(output, 24, row.flags); +} + +fn encode_stage(row: &EagerPlanStageRow, output: &mut [u8]) { + put_u32(output, 0, row.stage_index); + put_u32(output, 4, row.subset_size); + let values = [ + row.invocation_start, + row.invocation_count, + row.attachment_start, + row.attachment_count, + row.finalization_start, + row.finalization_count, + ]; + for (index, value) in values.into_iter().enumerate() { + put_u64(output, 8 + index * 8, value); + } +} + +fn encode_coupling(row: &EagerPlanCouplingRow, output: &mut [u8]) { + put_u32_values( + output, + &[ + row.coupling_id, + row.real_parameter_id, + row.imaginary_parameter_id, + row.constant_factor_id, + ], + ); +} + +fn encode_invocation(row: &EagerPlanInvocationRow, output: &mut [u8]) { + put_u32_values( + output, + &[ + row.evaluation_group_id, + row.kernel_id, + row.left_value_slot_id, + row.right_value_slot_id, + row.left_momentum_slot_id, + row.right_momentum_slot_id, + row.coupling_slot_id, + ], + ); + put_u8(output, 28, row.output_factor_source); + put_u64(output, 32, row.attachment_start); + put_u64(output, 40, row.attachment_count); + put_u32(output, 48, row.selector_domain_id); +} + +fn encode_attachment(row: &EagerPlanAttachmentRow, output: &mut [u8]) { + put_u32_values( + output, + &[ + row.interaction_id, + row.result_current_id, + row.color_factor_id, + row.evaluation_factor_id, + row.normalization_factor_id, + row.representative_evaluation_factor_id, + row.selector_domain_id, + ], + ); +} + +fn encode_finalization(row: &EagerPlanFinalizationRow, output: &mut [u8]) { + put_u32_values( + output, + &[ + row.kernel_id, + row.current_id, + row.unpropagated_value_slot_id, + row.propagated_value_slot_id, + row.momentum_slot_id, + row.unpropagated_selector_domain_id, + row.propagated_selector_domain_id, + ], + ); +} + +fn encode_closure(row: &EagerPlanClosureRow, output: &mut [u8]) { + put_u32_values( + output, + &[ + row.root_id, + row.kernel_id, + row.left_value_slot_id, + row.right_value_slot_id, + row.amplitude_index, + row.coherent_group_id, + row.coupling_slot_id, + row.coupling_factor_id, + ], + ); + put_u8(output, 32, row.output_factor_source); + put_u32(output, 36, row.color_factor_id); + put_u32(output, 40, row.normalization_factor_id); + put_u64(output, 44, row.direct_coefficient_start); + put_u64(output, 52, row.direct_coefficient_count); + put_u32(output, 60, row.selector_domain_id); +} + +fn encode_direct_coefficient(row: &EagerPlanDirectCoefficientRow, output: &mut [u8]) { + put_u32_values( + output, + &[row.contraction_ir_id, row.component_index, row.factor_id], + ); +} + +fn encode_selector_domain(row: &EagerPlanSelectorDomainRow, output: &mut [u8]) { + put_u64(output, 0, row.member_start); + put_u64(output, 8, row.member_count); +} + +fn encode_helicity_selector(row: &EagerPlanHelicitySelectorRow, output: &mut [u8]) { + put_u32_values( + output, + &[ + row.selector_id, + row.values_sequence_id, + row.representative_sequence_id, + row.coefficient_factor_id, + ], + ); + put_u8(output, 16, row.computed); + put_u8(output, 17, row.structural_zero); +} + +fn encode_color_selector(row: &EagerPlanColorSelectorRow, output: &mut [u8]) { + put_u32_values( + output, + &[ + row.selector_id, + row.word_sequence_id, + row.representative_word_sequence_id, + row.coefficient_factor_id, + ], + ); + put_u8(output, 16, row.computed); +} + +fn encode_reduction_group(row: &EagerPlanReductionGroupRow, output: &mut [u8]) { + put_u32(output, 0, row.coherent_group_id); + put_u64(output, 8, row.amplitude_entry_start); + put_u64(output, 16, row.amplitude_entry_count); + put_u64(output, 24, row.selector_entry_start); + put_u64(output, 32, row.selector_entry_count); + put_u32(output, 40, row.helicity_weight_factor_id); + put_u32(output, 44, row.all_sector_weight_factor_id); +} + +fn encode_reduction_entry(row: &EagerPlanReductionEntryRow, output: &mut [u8]) { + put_u8(output, 0, row.kind as u8); + put_u32(output, 4, row.owner_id); + put_u32(output, 8, row.left_id); + put_u32(output, 12, row.right_id); + put_u32(output, 16, row.factor_id); + put_u32(output, 20, row.auxiliary_factor_id); +} + +fn encode_exact_factor(row: &EagerPlanExactFactorRow, output: &mut [u8]) { + put_u32(output, 0, row.factor_id); + put_u64(output, 8, row.real_bits); + put_u64(output, 16, row.imaginary_bits); + put_u32(output, 24, row.canonical_string_id); + put_u8(output, 28, row.exact_source); + put_u32(output, 32, row.exact_ir_id); + put_u32(output, 36, row.source_ir_id); +} + +fn encode_catalog_range(row: &EagerPlanCatalogRangeRow, output: &mut [u8]) { + put_u64(output, 0, row.start); + put_u64(output, 8, row.count); +} + +fn put_u32_values(output: &mut [u8], values: &[u32]) { + for (index, value) in values.iter().copied().enumerate() { + put_u32(output, index * 4, value); + } +} + +fn put_u8(output: &mut [u8], offset: usize, value: u8) { + output[offset] = value; +} + +fn put_u16(output: &mut [u8], offset: usize, value: u16) { + output[offset..offset + 2].copy_from_slice(&value.to_le_bytes()); +} + +fn put_u32(output: &mut [u8], offset: usize, value: u32) { + output[offset..offset + 4].copy_from_slice(&value.to_le_bytes()); +} + +fn put_i32(output: &mut [u8], offset: usize, value: i32) { + output[offset..offset + 4].copy_from_slice(&value.to_le_bytes()); +} + +fn put_u64(output: &mut [u8], offset: usize, value: u64) { + output[offset..offset + 8].copy_from_slice(&value.to_le_bytes()); +} + +#[cfg(test)] +#[path = "eager_plan_v3_pacbin_tests.rs"] +mod tests; diff --git a/pyamplicol/rust/crates/rusticol-core/src/eager_plan_v3_pacbin_tests.rs b/pyamplicol/rust/crates/rusticol-core/src/eager_plan_v3_pacbin_tests.rs new file mode 100644 index 0000000000..1e9d22319b --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/eager_plan_v3_pacbin_tests.rs @@ -0,0 +1,522 @@ +// SPDX-License-Identifier: 0BSD + +use super::*; +use crate::RusticolErrorKind; +use crate::eager_lowering_v3::{EagerPlanReductionEntryKind, EagerValueSlotKind}; +use crate::pacbin::PacbinReader; +use std::fs; +use std::sync::atomic::{AtomicU64, Ordering}; + +static NEXT_DIRECTORY: AtomicU64 = AtomicU64::new(0); + +const EXPECTED_MEMBERS: &[&str] = &[ + "catalogs/bitsets/populations.bin", + "catalogs/bitsets/ranges.bin", + "catalogs/bitsets/words.bin", + "catalogs/exact-factors.bin", + "catalogs/exact-ir/bytes.bin", + "catalogs/exact-ir/ranges.bin", + "catalogs/i32-sequences/ranges.bin", + "catalogs/i32-sequences/values.bin", + "catalogs/semantic-limitations/bytes.bin", + "catalogs/semantic-limitations/ranges.bin", + "catalogs/strings/bytes.bin", + "catalogs/strings/ranges.bin", + "catalogs/u32-sequences/ranges.bin", + "catalogs/u32-sequences/values.bin", + "inspection/summary.bin", + "metadata/core.bin", + "metadata/identity-bytes.bin", + "metadata/identity-ranges.bin", + "reductions/entries.bin", + "reductions/groups.bin", + "retained/columns.bin", + "retained/name-bytes.bin", + "retained/name-ranges.bin", + "retained/tables.bin", + "retained/values-f64-bits.bin", + "retained/values-i32.bin", + "retained/values-u32.bin", + "retained/values-u64.bin", + "retained/values-u8.bin", + "selectors/colors.bin", + "selectors/domains.bin", + "selectors/helicities.bin", + "selectors/memberships.bin", + "tables/attachments.bin", + "tables/closures.bin", + "tables/couplings.bin", + "tables/currents.bin", + "tables/direct-coefficients.bin", + "tables/finalizations.bin", + "tables/invocations.bin", + "tables/momenta.bin", + "tables/parameters.bin", + "tables/sources.bin", + "tables/stages.bin", + "tables/values.bin", +]; + +struct Fixture { + strings: Vec>, + exact_ir: Vec>, + limitations: Vec>, + retained: Vec, + currents: Vec, + values: Vec, + momenta: Vec, + sources: Vec, + parameters: Vec, + stages: Vec, + couplings: Vec, + invocations: Vec, + attachments: Vec, + finalizations: Vec, + closures: Vec, + direct_coefficients: Vec, + selector_domains: Vec, + selector_memberships: Vec, + helicity_selectors: Vec, + color_selectors: Vec, + reduction_groups: Vec, + reduction_entries: Vec, + exact_factors: Vec, + bitset_ranges: Vec, + bitset_populations: Vec, + bitset_words: Vec, + u32_ranges: Vec, + u32_values: Vec, + i32_ranges: Vec, + i32_values: Vec, +} + +impl Fixture { + fn populated() -> Self { + Self { + strings: vec!["one".into(), "two".into()], + exact_ir: vec!["{\"ir\":1}".into()], + limitations: vec!["exact limitation".into()], + retained: vec![], + currents: vec![EagerPlanCurrentRow { + current_id: 11, + component_start: 12, + component_count: 13, + momentum_slot_id: 14, + flags: 15, + }], + values: vec![EagerPlanValueRow { + value_slot_id: 21, + current_id: 22, + component_start: 23, + component_count: 24, + kind: EagerValueSlotKind::Propagated, + }], + momenta: vec![EagerPlanMomentumRow { + momentum_slot_id: 31, + bitset_id: 32, + component_start: 33, + component_count: 34, + }], + sources: vec![EagerPlanSourceFillRow { + source_id: 41, + current_id: 42, + value_slot_id: 43, + external_label: 44, + input_momentum_slot: 45, + source_ir_id: 46, + crossing_ir_id: 47, + crossing_factor_id: 48, + declared_state_index: 49, + }], + parameters: vec![EagerPlanParameterRow { + parameter_id: 51, + name_string_id: 52, + kind_string_id: 53, + default_factor_id: 54, + runtime_name_string_id: 55, + complex_component: -56, + flags: 57, + }], + stages: vec![EagerPlanStageRow { + stage_index: 61, + subset_size: 62, + invocation_start: 63, + invocation_count: 64, + attachment_start: 65, + attachment_count: 66, + finalization_start: 67, + finalization_count: 68, + }], + couplings: vec![EagerPlanCouplingRow { + coupling_id: 71, + real_parameter_id: 72, + imaginary_parameter_id: 73, + constant_factor_id: 74, + }], + invocations: vec![EagerPlanInvocationRow { + evaluation_group_id: 81, + kernel_id: 82, + left_value_slot_id: 83, + right_value_slot_id: 84, + left_momentum_slot_id: 85, + right_momentum_slot_id: 86, + coupling_slot_id: 87, + output_factor_source: 88, + attachment_start: 89, + attachment_count: 90, + selector_domain_id: 91, + }], + attachments: vec![EagerPlanAttachmentRow { + interaction_id: 101, + result_current_id: 102, + color_factor_id: 103, + evaluation_factor_id: 104, + normalization_factor_id: 105, + representative_evaluation_factor_id: 106, + selector_domain_id: 107, + }], + finalizations: vec![EagerPlanFinalizationRow { + kernel_id: 111, + current_id: 112, + unpropagated_value_slot_id: 113, + propagated_value_slot_id: 114, + momentum_slot_id: 115, + unpropagated_selector_domain_id: 116, + propagated_selector_domain_id: 117, + }], + closures: vec![EagerPlanClosureRow { + root_id: 121, + kernel_id: 122, + left_value_slot_id: 123, + right_value_slot_id: 124, + amplitude_index: 125, + coherent_group_id: 126, + coupling_slot_id: 127, + coupling_factor_id: 128, + output_factor_source: 129, + color_factor_id: 130, + normalization_factor_id: 131, + direct_coefficient_start: 132, + direct_coefficient_count: 133, + selector_domain_id: 134, + }], + direct_coefficients: vec![EagerPlanDirectCoefficientRow { + contraction_ir_id: 141, + component_index: 142, + factor_id: 143, + }], + selector_domains: vec![EagerPlanSelectorDomainRow { + member_start: 151, + member_count: 152, + }], + selector_memberships: vec![153], + helicity_selectors: vec![EagerPlanHelicitySelectorRow { + selector_id: 161, + values_sequence_id: 162, + representative_sequence_id: 163, + coefficient_factor_id: 164, + computed: 1, + structural_zero: 0, + }], + color_selectors: vec![EagerPlanColorSelectorRow { + selector_id: 171, + word_sequence_id: 172, + representative_word_sequence_id: 173, + coefficient_factor_id: 174, + computed: 1, + }], + reduction_groups: vec![EagerPlanReductionGroupRow { + coherent_group_id: 181, + amplitude_entry_start: 182, + amplitude_entry_count: 183, + selector_entry_start: 184, + selector_entry_count: 185, + helicity_weight_factor_id: 186, + all_sector_weight_factor_id: 187, + }], + reduction_entries: vec![EagerPlanReductionEntryRow { + kind: EagerPlanReductionEntryKind::ColorContraction, + owner_id: 191, + left_id: 192, + right_id: 193, + factor_id: 194, + auxiliary_factor_id: 195, + }], + exact_factors: vec![EagerPlanExactFactorRow { + factor_id: 201, + real_bits: (-0.0_f64).to_bits(), + imaginary_bits: 2.5_f64.to_bits(), + canonical_string_id: 202, + exact_source: 203, + exact_ir_id: 204, + source_ir_id: 205, + }], + bitset_ranges: vec![EagerPlanCatalogRangeRow { + start: 211, + count: 212, + }], + bitset_populations: vec![213], + bitset_words: vec![214], + u32_ranges: vec![EagerPlanCatalogRangeRow { + start: 221, + count: 222, + }], + u32_values: vec![223], + i32_ranges: vec![EagerPlanCatalogRangeRow { + start: 231, + count: 232, + }], + i32_values: vec![-233], + } + } + + fn view(&self) -> PlanView<'_> { + PlanView { + abi: EAGER_PLAN_ABI, + process_key: "fixture-process", + model_name: "fixture-model", + string_catalog: &self.strings, + canonical_ir_catalog: &self.exact_ir, + semantic_limitations: &self.limitations, + retained_tables: &self.retained, + currents: &self.currents, + values: &self.values, + momenta: &self.momenta, + sources: &self.sources, + parameters: &self.parameters, + stages: &self.stages, + couplings: &self.couplings, + invocations: &self.invocations, + attachments: &self.attachments, + finalizations: &self.finalizations, + closures: &self.closures, + direct_coefficients: &self.direct_coefficients, + selector_domains: &self.selector_domains, + selector_memberships: &self.selector_memberships, + helicity_selectors: &self.helicity_selectors, + color_selectors: &self.color_selectors, + reduction_groups: &self.reduction_groups, + reduction_entries: &self.reduction_entries, + exact_factors: &self.exact_factors, + bitset_ranges: &self.bitset_ranges, + bitset_populations: &self.bitset_populations, + bitset_words: &self.bitset_words, + u32_sequence_ranges: &self.u32_ranges, + u32_sequence_values: &self.u32_values, + i32_sequence_ranges: &self.i32_ranges, + i32_sequence_values: &self.i32_values, + current_component_count: 301, + value_component_count: 302, + momentum_component_count: 303, + color_contraction_entry_start: 304, + color_contraction_entry_count: 305, + } + } +} + +fn temporary_directory(label: &str) -> std::path::PathBuf { + let path = std::env::temp_dir().join(format!( + "rusticol-eager-v3-pacbin-{label}-{}-{}", + std::process::id(), + NEXT_DIRECTORY.fetch_add(1, Ordering::Relaxed) + )); + fs::create_dir(&path).unwrap(); + path +} + +fn u32_at(bytes: &[u8], offset: usize) -> u32 { + u32::from_le_bytes(bytes[offset..offset + 4].try_into().unwrap()) +} + +fn u64_at(bytes: &[u8], offset: usize) -> u64 { + u64::from_le_bytes(bytes[offset..offset + 8].try_into().unwrap()) +} + +fn section_payload<'a>(reader: &'a PacbinReader, path: &str) -> &'a [u8] { + let bytes = reader.member_bytes(path).unwrap(); + let (_, payload) = EagerSectionHeader::decode(bytes).unwrap(); + payload +} + +#[test] +fn eager_plan_v3_pacbin_is_byte_identical_and_metadata_is_bounded() { + let directory = temporary_directory("deterministic"); + let first = directory.join("first.pacbin"); + let second = directory.join("second.pacbin"); + let fixture = Fixture::populated(); + let first_metadata = write_plan_view(&fixture.view(), &first).unwrap(); + let second_metadata = write_plan_view(&fixture.view(), &second).unwrap(); + assert_eq!(fs::read(&first).unwrap(), fs::read(&second).unwrap()); + assert_eq!(first_metadata, second_metadata); + assert_eq!(first_metadata.member_count, EXPECTED_MEMBERS.len() as u64); + assert_eq!( + first_metadata.file_size, + fs::metadata(&first).unwrap().len() + ); + let expected_file_digest: [u8; 32] = Sha256::digest(fs::read(&first).unwrap()).into(); + assert_eq!(first_metadata.file_sha256, expected_file_digest); + fs::remove_dir_all(directory).unwrap(); +} + +#[test] +fn reader_authenticates_allowlisted_members_and_every_plan_field() { + let directory = temporary_directory("coverage"); + let destination = directory.join("eager-runtime.pacbin"); + let fixture = Fixture::populated(); + write_plan_view(&fixture.view(), &destination).unwrap(); + let reader = PacbinReader::open(&destination).unwrap(); + reader.verify_payloads().unwrap(); + let actual: Vec<_> = reader + .members() + .iter() + .map(|member| member.logical_path()) + .collect(); + assert_eq!(actual, EXPECTED_MEMBERS); + for member in reader.members() { + let expected_kind = if member.logical_path().starts_with("metadata/") + || member.logical_path().starts_with("inspection/") + || member.logical_path().ends_with("/bytes.bin") + || member.logical_path().ends_with("/ranges.bin") + && (member.logical_path().starts_with("catalogs/strings") + || member.logical_path().starts_with("catalogs/exact-ir") + || member + .logical_path() + .starts_with("catalogs/semantic-limitations")) + || member.logical_path().starts_with("retained/name-") + { + PacbinMemberKind::EagerRuntimeMetadata + } else { + PacbinMemberKind::EagerRuntimeTable + }; + assert_eq!(member.kind(), expected_kind, "{}", member.logical_path()); + let (header, payload) = + EagerSectionHeader::decode(reader.member_bytes(member.logical_path()).unwrap()) + .unwrap(); + assert_eq!(payload.len() as u64, header.payload_length()); + } + + let metadata = section_payload(&reader, "metadata/core.bin"); + assert_eq!(u32_at(metadata, 0), SERIALIZATION_SCHEMA); + assert_eq!(u32_at(metadata, 8), IDENTITY_LOWERING_ABI); + assert_eq!(u32_at(metadata, 32), IDENTITY_MODEL_NAME); + assert_eq!(u64_at(metadata, 48), 301); + assert_eq!(u64_at(metadata, 56), 302); + assert_eq!(u64_at(metadata, 64), 303); + assert_eq!(u64_at(metadata, 72), 304); + assert_eq!(u64_at(metadata, 80), 305); + + let current = section_payload(&reader, "tables/currents.bin"); + assert_eq!(u32_at(current, 0), 11); + assert_eq!(u64_at(current, 4), 12); + assert_eq!(u32_at(current, 20), 15); + let closure = section_payload(&reader, "tables/closures.bin"); + assert_eq!(u32_at(closure, 0), 121); + assert_eq!(u64_at(closure, 44), 132); + assert_eq!(u32_at(closure, 60), 134); + let direct = section_payload(&reader, "tables/direct-coefficients.bin"); + assert_eq!( + (u32_at(direct, 0), u32_at(direct, 4), u32_at(direct, 8)), + (141, 142, 143) + ); + let helicity = section_payload(&reader, "selectors/helicities.bin"); + assert_eq!( + &helicity[..18], + &[161, 0, 0, 0, 162, 0, 0, 0, 163, 0, 0, 0, 164, 0, 0, 0, 1, 0] + ); + let exact = section_payload(&reader, "catalogs/exact-factors.bin"); + assert_eq!(u64_at(exact, 8), (-0.0_f64).to_bits()); + assert_eq!(u64_at(exact, 16), 2.5_f64.to_bits()); + assert_eq!(u32_at(exact, 36), 205); + let identity = section_payload(&reader, "metadata/identity-bytes.bin"); + let identity_text = std::str::from_utf8(identity).unwrap(); + assert!(identity_text.contains(EAGER_PLAN_ABI)); + assert!(identity_text.contains("fixture-process")); + assert!(identity_text.contains("fixture-model")); + fs::remove_dir_all(directory).unwrap(); +} + +#[test] +fn malformed_destination_preserves_existing_target_and_cleans_staging() { + let directory = temporary_directory("rollback"); + let destination = directory.join("eager-runtime.pacbin"); + fs::create_dir(&destination).unwrap(); + fs::write(destination.join("sentinel"), b"keep").unwrap(); + let fixture = Fixture::populated(); + let error = write_plan_view(&fixture.view(), &destination).unwrap_err(); + assert_eq!(error.kind(), RusticolErrorKind::Artifact); + assert_eq!(fs::read(destination.join("sentinel")).unwrap(), b"keep"); + let names: Vec<_> = fs::read_dir(&directory) + .unwrap() + .map(|entry| entry.unwrap().file_name()) + .collect(); + assert_eq!( + names, + vec![std::ffi::OsString::from("eager-runtime.pacbin")] + ); + fs::remove_dir_all(directory).unwrap(); +} + +#[test] +fn large_catalog_serialization_keeps_payload_independent_scratch() { + let directory = temporary_directory("bounded"); + let destination = directory.join("eager-runtime.pacbin"); + let mut fixture = Fixture::populated(); + fixture.u32_values = vec![0x1234_5678; 1_000_000]; + fixture.u32_ranges = vec![EagerPlanCatalogRangeRow { + start: 0, + count: fixture.u32_values.len() as u64, + }]; + let metadata = write_plan_view(&fixture.view(), &destination).unwrap(); + assert!(metadata.unpacked_bytes > 4_000_000); + assert_eq!(MAX_ENCODED_RECORD_SIZE, 224); + assert!( + std::mem::size_of::>() + < MAX_ENCODED_RECORD_SIZE + 128 + ); + assert!( + std::mem::size_of::>() < 128, + "segmented reader must retain only cursors and one primitive" + ); + let reader = PacbinReader::open(&destination).unwrap(); + let values = section_payload(&reader, "catalogs/u32-sequences/values.bin"); + assert_eq!(values.len(), 4_000_000); + assert_eq!(u32_at(values, values.len() - 4), 0x1234_5678); + fs::remove_dir_all(directory).unwrap(); +} + +#[test] +fn retained_table_and_column_records_preserve_every_descriptor_field() { + let table = RetainedTableRecord { + table_id: 1, + name_id: 2, + row_count: 3, + column_start: 4, + column_count: 5, + }; + let mut table_bytes = [0_u8; RETAINED_TABLE_RECORD_SIZE as usize]; + encode_retained_table(&table, &mut table_bytes); + assert_eq!(u32_at(&table_bytes, 0), 1); + assert_eq!(u32_at(&table_bytes, 4), 2); + assert_eq!(u64_at(&table_bytes, 8), 3); + assert_eq!(u64_at(&table_bytes, 16), 4); + assert_eq!(u64_at(&table_bytes, 24), 5); + + let column = RetainedColumnRecord { + table_id: 6, + column_id: 7, + name_id: 8, + primitive_kind: RETAINED_F64_BITS, + elements_per_row: 9, + value_start: 10, + value_count: 11, + }; + let mut column_bytes = [0_u8; RETAINED_COLUMN_RECORD_SIZE as usize]; + encode_retained_column(&column, &mut column_bytes); + assert_eq!(u32_at(&column_bytes, 0), 6); + assert_eq!(u32_at(&column_bytes, 4), 7); + assert_eq!(u32_at(&column_bytes, 8), 8); + assert_eq!(column_bytes[12], RETAINED_F64_BITS); + assert_eq!(u32_at(&column_bytes, 16), 9); + assert_eq!(u64_at(&column_bytes, 24), 10); + assert_eq!(u64_at(&column_bytes, 32), 11); +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/eager_runtime.rs b/pyamplicol/rust/crates/rusticol-core/src/eager_runtime.rs new file mode 100644 index 0000000000..2f8902c0aa --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/eager_runtime.rs @@ -0,0 +1,242 @@ +// SPDX-License-Identifier: 0BSD + +use crate::{RusticolError, RusticolResult}; +use num_complex::Complex; +use std::time::Duration; + +pub type EagerComplex64 = Complex; + +pub const DEFAULT_EAGER_POINT_TILE_SIZE: usize = 1024; +pub const DEFAULT_EAGER_WORKSPACE_MIB: usize = 256; +pub const EAGER_HOMOGENEOUS_LINEAR_CURRENT_PROOF: &str = + "prepared-kernel-homogeneous-complex-linear-current-v1"; +pub const EAGER_INDEPENDENT_BLOCK_SIZE: u32 = 4; + +#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] +pub enum EagerKernelRole { + Vertex, + Finalization, + Closure, +} + +/// One prepared evaluator input in its exact, deterministic parameter order. +#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] +pub enum EagerKernelInput { + FirstCurrentComponent(u32), + SecondCurrentComponent(u32), + FirstMomentumComponent(u32), + SecondMomentumComponent(u32), + CouplingReal, + CouplingImag, + ModelParameter(u32), +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct EagerKernelSpec { + pub kernel_id: u32, + pub role: EagerKernelRole, + pub inputs: Vec, + pub output_component_count: u32, + pub homogeneous_linear_first_current: bool, + /// Prepared evaluator width for independent invocation blocks. + /// + /// A value greater than one is valid only for vertex kernels whose + /// prepared pack contains the correspondingly widened evaluator. The + /// scheduler retains the scalar evaluator for incomplete tails. + pub independent_block_size: u32, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct EagerPlanDimensions { + pub value_slot_component_counts: Vec, + pub momentum_slot_component_counts: Vec, + pub current_component_counts: Vec, + pub parameter_count: u32, + pub amplitude_count: u32, +} + +#[derive(Clone, Debug, PartialEq)] +pub struct EagerDirectClosureSpec { + pub closure_index: u32, + pub coefficients: Vec, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct EagerReductionGroup { + pub coherent_group_id: u32, + pub amplitude_indices: Vec, +} + +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct EagerReductionEntry { + pub left_group_index: u32, + pub right_group_index: u32, + pub coefficient: EagerComplex64, +} + +#[derive(Clone, Debug, PartialEq)] +pub struct EagerPlanDefinition { + pub dimensions: EagerPlanDimensions, + pub kernels: Vec, + pub direct_closures: Vec, + pub reduction_groups: Vec, + pub reduction_entries: Vec, +} + +#[derive(Clone, Copy, Debug)] +pub struct EagerStagePayload<'a> { + pub stage_index: u32, + pub invocations: &'a [u8], + pub attachments: &'a [u8], + pub finalizations: &'a [u8], +} + +#[derive(Clone, Copy, Debug)] +pub struct EagerSelectorStagePayload<'a> { + pub stage_index: u32, + pub invocation_domains: &'a [u8], + pub attachment_domains: &'a [u8], + pub unpropagated_finalization_domains: &'a [u8], + pub propagated_finalization_domains: &'a [u8], +} + +#[derive(Clone, Copy, Debug)] +pub struct EagerSelectorPayloads<'a> { + pub domains: &'a [u8], + pub domain_group_ids: &'a [u8], + pub stages: &'a [EagerSelectorStagePayload<'a>], + pub closure_domains: &'a [u8], +} + +#[derive(Clone, Copy, Debug)] +pub struct EagerPlanPayloads<'a> { + pub couplings: &'a [u8], + pub stages: &'a [EagerStagePayload<'a>], + pub closures: &'a [u8], + pub selector_domains: Option>, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct EagerRuntimeOptions { + pub point_tile_size: usize, + /// Batch workspace target; one executable point may require more memory. + pub workspace_bytes: usize, +} + +impl EagerRuntimeOptions { + pub fn from_mib(point_tile_size: usize, workspace_mib: usize) -> RusticolResult { + let workspace_bytes = workspace_mib.checked_mul(1024 * 1024).ok_or_else(|| { + RusticolError::invalid_argument("eager workspace size overflows bytes") + })?; + Ok(Self { + point_tile_size, + workspace_bytes, + }) + } +} + +impl Default for EagerRuntimeOptions { + fn default() -> Self { + Self { + point_tile_size: DEFAULT_EAGER_POINT_TILE_SIZE, + workspace_bytes: DEFAULT_EAGER_WORKSPACE_MIB * 1024 * 1024, + } + } +} + +pub struct EagerKernelCall<'a> { + pub kernel_id: u32, + pub independent_block_size: u32, + pub lane_count: usize, + pub input_component_count: usize, + pub output_component_count: usize, + pub inputs: &'a [EagerComplex64], + pub outputs: &'a mut [EagerComplex64], +} + +/// Evaluates row-major packets whose lanes are ordered by invocation, then point. +/// +/// The scheduler sorts invocations stably by kernel id while loading a plan. Each packet +/// therefore contains one kernel only; within that packet, all points for the first +/// invocation precede all points for the next invocation. Input components follow the +/// exact order in [`EagerKernelSpec::inputs`], so component `c` of lane `l` is stored at +/// `l * input_component_count + c`. Outputs use the same lane-major row layout. +pub trait EagerKernelBackend { + fn evaluate_batch(&mut self, call: EagerKernelCall<'_>) -> RusticolResult<()>; +} + +/// Opt-in wall-clock accounting for one eager scheduler evaluation. +/// +/// Every field is an aggregate over all point tiles. The phases are mutually +/// exclusive, so [`Self::accounted`] can be compared directly with +/// [`Self::total`]. Timer bookkeeping and validation account for the small +/// unclassified remainder. +#[derive(Clone, Copy, Debug, Default, PartialEq)] +pub(crate) struct EagerExecutionProfile { + pub(crate) initialize: Duration, + pub(crate) gather: Duration, + pub(crate) kernel_call: Duration, + pub(crate) invocation_scatter: Duration, + pub(crate) finalization: Duration, + pub(crate) closure: Duration, + pub(crate) reduction: Duration, + pub(crate) copy_out: Duration, + pub(crate) total: Duration, + pub(crate) backend_call_count: u64, + /// P-kernel output scratch reads and writes wholly inside the eager arena. + /// This is diagnostic traffic, never arena-boundary materialization. + pub(crate) internal_scratch_bytes: u64, + /// Point-independent coupling and parameter plane refreshes inside the + /// persistent eager arena. This is not arena-boundary traffic. + pub(crate) internal_broadcast_bytes: u64, +} + +impl EagerExecutionProfile { + pub(crate) fn accounted(self) -> Duration { + self.initialize + + self.gather + + self.kernel_call + + self.invocation_scatter + + self.finalization + + self.closure + + self.reduction + + self.copy_out + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub(crate) struct EagerScheduleAuditRow { + pub(crate) stage_index: Option, + pub(crate) role: &'static str, + pub(crate) kernel_id: Option, + pub(crate) call_count: usize, + pub(crate) row_count: usize, + pub(crate) destination_count: usize, +} + +#[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] +mod direct_execution_arena; +#[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] +mod direct_invocation_arena; +#[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] +pub(crate) use direct_execution_arena::EagerDirectExecutionRuntime; +#[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] +pub(crate) use direct_invocation_arena::{ + EagerDirectPreparedApplication, EagerDirectPreparedKernel, +}; +#[cfg(all(test, any(feature = "f64-compiled", feature = "f64-symjit")))] +pub(crate) use direct_invocation_arena::{ + run_retained_multistage_oracle, select_retained_multistage_candidate, +}; +mod execute; +mod plan; +mod plan_v3; +mod profile; +mod runtime; + +#[cfg(test)] +mod plan_v3_tests; + +pub use plan::EagerExecutionPlan; +pub(crate) use plan_v3::EagerPlanV3Sections; +pub use runtime::EagerExecutionRuntime; diff --git a/pyamplicol/rust/crates/rusticol-core/src/eager_runtime/direct_execution_arena.rs b/pyamplicol/rust/crates/rusticol-core/src/eager_runtime/direct_execution_arena.rs new file mode 100644 index 0000000000..7ff6959312 --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/eager_runtime/direct_execution_arena.rs @@ -0,0 +1,3242 @@ +// SPDX-License-Identifier: 0BSD + +//! Developer-gated whole-plan eager Direct-Arena execution. +//! +//! The packet scheduler remains an explicit validation oracle while this lane +//! is gated. Production-selected direct execution never falls back to it: +//! authenticated table callables consume persistent planes for every vertex, +//! propagator, and kernel closure, while copies, direct contractions, and +//! reductions operate on the same canonical storage. + +#![allow(dead_code)] + +use std::collections::{BTreeMap, BTreeSet}; +use std::mem::size_of; +use std::sync::Arc; +use std::time::Instant; + +use crate::direct_arena::{DIRECT_ARENA_LOCALITY_POINT_CAP, DirectArenaLayout}; +use crate::engine::symjit_eager_direct::{ + EAGER_DIRECT_SOURCE_APPLICATION_ABI, EAGER_DIRECT_TABLE_BINDING_ABI, + EAGER_DIRECT_TABLE_DESCRIPTOR_ABI, EagerDirectArenaPlaneBinding, EagerDirectTableRows, + EagerDirectTableWorkspace, LoadedSymjitEagerDirectTable, +}; +use crate::{ + EAGER_OUTPUT_FACTOR_COUPLING_IMAG, EAGER_OUTPUT_FACTOR_COUPLING_REAL, EAGER_OUTPUT_FACTOR_NONE, + MISSING_U32, RusticolError, RusticolResult, +}; + +use super::direct_invocation_arena::{ + EagerDirectPreparedApplication, EagerDirectPreparedKernel, SemanticCatalog, assigned_component, + count_u32, derive_event_layout, invalid, push_u32, resolve_coupling, row_active, + validate_active_groups, validate_callable_identity, validate_component_buffer, +}; +use super::execute::AccumulationFactor; +use super::plan::{ + ComponentRange, EagerExecutionPlan, EagerStagePlan, ScheduledClosure, ScheduledFinalization, + ScheduledInvocation, +}; +use super::{ + EagerComplex64, EagerKernelInput, EagerKernelRole, EagerPlanV3Sections, EagerRuntimeOptions, + EagerScheduleAuditRow, +}; + +struct DirectCall { + kernel_id: u32, + application: Arc, + rows: EagerDirectTableRows, +} + +#[derive(Clone, Copy)] +struct DirectFactorSpec { + factor: EagerComplex64, + coupling_slot_id: Option, + output_factor_source: u32, +} + +#[derive(Clone, Copy)] +struct DirectCopy { + source_component_base: u32, + destination_component_base: u32, + component_count: u32, +} + +struct DirectStage { + stage_index: u32, + invocation_calls: Box<[DirectCall]>, + unpropagated_copies: Box<[DirectCopy]>, + finalization_calls: Box<[DirectCall]>, +} + +struct DirectCoefficientClosure { + left_component_base: u32, + right_component_base: u32, + component_count: u32, + amplitude_index: u32, + factor: AccumulationFactor, + initializes_amplitude: bool, + coefficients: Box<[EagerComplex64]>, +} + +struct DirectSchedule { + stages: Box<[DirectStage]>, + closure_calls: Box<[DirectCall]>, + direct_closures: Box<[DirectCoefficientClosure]>, + factor_specs: Box<[DirectFactorSpec]>, + active_amplitude_indices: Box<[usize]>, + active_reduction_group_indices: Box<[usize]>, + active_reduction_entry_indices: Box<[usize]>, + active_reduction_group_ids: Box<[u32]>, +} + +struct SelectedSchedule { + active_groups: Box<[u32]>, + schedule: DirectSchedule, +} + +#[derive(Clone, Copy)] +struct PointWorkGroup { + schedule_index: Option, + point_start: usize, + point_count: usize, +} + +#[derive(Default)] +struct PointSelectedSchedules { + signature_offsets: Vec, + signature_groups: Vec, + point_work_group_ids: Vec, + grouped_point_indices: Vec, + cursors: Vec, + work_groups: Vec, + schedules: Vec, +} + +/// Whole-plan eager Direct-Arena runtime. +/// +/// Construction owns all lowered callables and immutable row tables. The +/// warmed unprofiled methods mutate only persistent planes, factors, reduction +/// scratch, and caller-provided outputs. +pub(crate) struct EagerDirectExecutionRuntime { + plan: EagerExecutionPlan, + catalog: SemanticCatalog, + layout: DirectArenaLayout, + workspace: EagerDirectTableWorkspace, + applications: BTreeMap>, + full_schedule: DirectSchedule, + selected_schedule: Option, + point_selected: PointSelectedSchedules, + initial_value_slots: Box<[u32]>, + value_ranges: Box<[ComponentRange]>, + momentum_ranges: Box<[ComponentRange]>, + reduction_groups: Vec, + reduced_tile: Vec, + broadcast_parameter_bits: Box<[[u64; 2]]>, + broadcasts_initialized: bool, + shared_zero_plane_index: Option, + elide_structural_zero_fills: bool, + internal_broadcast_bytes: u64, + workspace_bytes: usize, +} + +impl EagerDirectExecutionRuntime { + pub(crate) fn from_plan_v3_sections( + sections: EagerPlanV3Sections<'_>, + prepared: &[EagerDirectPreparedKernel<'_>], + options: EagerRuntimeOptions, + ) -> RusticolResult { + if options.point_tile_size == 0 { + return Err(invalid( + "eager direct whole-plan point tile size must be positive", + )); + } + let plan = EagerExecutionPlan::from_plan_v3_sections(sections)?; + let catalog = SemanticCatalog::new(sections)?; + let layout = derive_event_layout(sections, catalog, &plan)?; + let requested_tile = u32::try_from(options.point_tile_size) + .map_err(|_| invalid("eager direct point tile size exceeds u32"))? + .min(DIRECT_ARENA_LOCALITY_POINT_CAP); + + let applications = load_applications(&plan, prepared)?; + let shared_zero_plane_index = + shared_zero_plane_index(sections, catalog, &layout, &applications)?; + let elide_structural_zero_fills = shared_zero_plane_index.is_some() + && applications + .values() + .all(|application| application.is_symjit()); + let full_schedule = build_direct_schedule( + &plan, + catalog, + &layout, + &applications, + shared_zero_plane_index, + None, + )?; + let factor_capacity = full_schedule.factor_specs.len().max(1); + let (symjit_descriptor_capacity, symjit_scratch_plane_count) = applications + .values() + .filter_map(|application| application.symjit_workspace_shape()) + .fold((0usize, 0usize), |maximum, shape| { + (maximum.0.max(shape.0), maximum.1.max(shape.1)) + }); + let broadcast_cache_bytes = plan + .parameter_count + .checked_mul(std::mem::size_of::<[u64; 2]>()) + .ok_or_else(|| invalid("eager direct broadcast cache size overflows"))?; + let descriptor_bytes = + EagerDirectTableWorkspace::symjit_descriptor_storage_bytes(symjit_descriptor_capacity)?; + let factor_bytes = factor_capacity + .checked_mul(2 * size_of::()) + .ok_or_else(|| invalid("eager direct factor workspace size overflows"))?; + let fixed_workspace_bytes = broadcast_cache_bytes + .checked_add(descriptor_bytes) + .and_then(|bytes| bytes.checked_add(factor_bytes)) + .ok_or_else(|| invalid("eager direct fixed workspace size overflows"))?; + let arena_workspace_budget = options + .workspace_bytes + .saturating_sub(fixed_workspace_bytes); + let tile_capacity = direct_tile_capacity( + requested_tile, + arena_workspace_budget, + layout.component_count(), + count_u32(plan.amplitude_count, "amplitude planes")?, + count_u32(plan.reduction_groups.len(), "reduction groups")?, + symjit_scratch_plane_count, + )?; + + let value_ranges = (0..sections.values.len()) + .map(|id| plan.values.get(id as u32, "eager direct value")) + .collect::>>()? + .into_boxed_slice(); + let momentum_ranges = (0..sections.momenta.len()) + .map(|id| plan.momenta.get(id as u32, "eager direct momentum")) + .collect::>>()? + .into_boxed_slice(); + let mut produced_values = vec![false; sections.values.len()]; + for row in sections.finalizations { + for value in [row.unpropagated_value_slot_id, row.propagated_value_slot_id] { + if value != MISSING_U32 { + produced_values[value as usize] = true; + } + } + } + let initial_value_slots = produced_values + .iter() + .enumerate() + .filter_map(|(id, produced)| (!produced).then_some(id as u32)) + .collect::>() + .into_boxed_slice(); + + let plane_bindings = plane_bindings(&layout, plan.amplitude_count)?; + let mut workspace = EagerDirectTableWorkspace::new( + layout.component_count(), + count_u32(plan.amplitude_count, "amplitude planes")?, + tile_capacity, + &plane_bindings, + shared_zero_plane_index, + Vec::new(), + vec![0.0; factor_capacity], + vec![0.0; factor_capacity], + )?; + workspace.begin_tile(tile_capacity)?; + validate_schedule_calls( + &full_schedule, + &mut workspace, + shared_zero_plane_index, + tile_capacity, + )?; + + let stride = workspace.arena().point_stride() as usize; + let reduction_groups = + vec![EagerComplex64::new(0.0, 0.0); plan.reduction_groups.len() * stride]; + let reduced_tile = vec![0.0; stride]; + let broadcast_parameter_bits = vec![[0, 0]; plan.parameter_count].into_boxed_slice(); + let workspace_bytes = direct_workspace_bytes( + &workspace, + reduction_groups.len(), + reduced_tile.len(), + factor_capacity, + )? + .checked_add(broadcast_cache_bytes) + .ok_or_else(|| invalid("eager direct workspace byte accounting overflows"))?; + + Ok(Self { + plan, + catalog, + layout, + workspace, + applications, + full_schedule, + selected_schedule: None, + point_selected: PointSelectedSchedules::default(), + initial_value_slots, + value_ranges, + momentum_ranges, + reduction_groups, + reduced_tile, + broadcast_parameter_bits, + broadcasts_initialized: false, + shared_zero_plane_index, + elide_structural_zero_fills, + internal_broadcast_bytes: 0, + workspace_bytes, + }) + } + + pub(crate) fn plan(&self) -> &EagerExecutionPlan { + &self.plan + } + + pub(crate) fn effective_point_tile_size(&self) -> usize { + self.workspace.arena().tile_capacity() as usize + } + + pub(crate) const fn workspace_bytes(&self) -> usize { + self.workspace_bytes + } + + pub(crate) const fn internal_scratch_bytes(&self) -> u64 { + self.workspace.internal_scratch_bytes() + } + + pub(crate) const fn internal_broadcast_bytes(&self) -> u64 { + self.internal_broadcast_bytes + } + + fn refresh_broadcast_planes( + &mut self, + model_parameters: &[EagerComplex64], + ) -> RusticolResult<()> { + fn parameter_changed( + cached: &[[u64; 2]], + parameters: &[EagerComplex64], + parameter: usize, + ) -> bool { + let value = parameters[parameter]; + cached[parameter] != [value.re.to_bits(), value.im.to_bits()] + } + + let initializing = !self.broadcasts_initialized; + if !initializing + && !(0..model_parameters.len()).any(|parameter| { + parameter_changed(&self.broadcast_parameter_bits, model_parameters, parameter) + }) + { + return Ok(()); + } + + let stride = self.workspace.arena().point_stride() as usize; + let (real, imaginary, _, _) = self.workspace.split_arena_slices_mut(); + let mut written_planes = 0_u64; + for (coupling_id, row) in self.plan.couplings.iter().copied().enumerate() { + let coupling_changed = initializing + || (row.real_parameter_id != MISSING_U32 + && parameter_changed( + &self.broadcast_parameter_bits, + model_parameters, + row.real_parameter_id as usize, + )) + || (row.imag_parameter_id != MISSING_U32 + && parameter_changed( + &self.broadcast_parameter_bits, + model_parameters, + row.imag_parameter_id as usize, + )); + if !coupling_changed { + continue; + } + let coupling = resolve_coupling(row, model_parameters); + let real_component = assigned_component( + &self.layout, + self.catalog + .coupling_real(count_u32(coupling_id, "coupling")?)?, + 0, + )?; + let imaginary_component = assigned_component( + &self.layout, + self.catalog + .coupling_imag(count_u32(coupling_id, "coupling")?)?, + 0, + )?; + if self.elide_structural_zero_fills { + fill_real_input_plane(real, stride, real_component, stride, coupling.re); + fill_real_input_plane(real, stride, imaginary_component, stride, coupling.im); + written_planes = written_planes.saturating_add(2); + } else { + fill_input_plane( + real, + imaginary, + stride, + real_component, + stride, + coupling.re, + 0.0, + ); + fill_input_plane( + real, + imaginary, + stride, + imaginary_component, + stride, + coupling.im, + 0.0, + ); + written_planes = written_planes.saturating_add(4); + } + } + for (parameter, value) in model_parameters.iter().copied().enumerate() { + if !initializing + && !parameter_changed(&self.broadcast_parameter_bits, model_parameters, parameter) + { + continue; + } + fill_input_plane( + real, + imaginary, + stride, + assigned_component( + &self.layout, + self.catalog + .parameter(count_u32(parameter, "model parameter")?)?, + 0, + )?, + stride, + value.re, + value.im, + ); + written_planes = written_planes.saturating_add(2); + self.broadcast_parameter_bits[parameter] = [value.re.to_bits(), value.im.to_bits()]; + } + self.broadcasts_initialized = true; + self.internal_broadcast_bytes = self.internal_broadcast_bytes.saturating_add( + written_planes + .saturating_mul(stride as u64) + .saturating_mul(std::mem::size_of::() as u64), + ); + Ok(()) + } + + /// Execute and reduce directly from arena amplitude planes without + /// materializing the amplitude matrix in caller memory. + #[allow(clippy::too_many_arguments)] + pub(crate) fn evaluate_totals_into( + &mut self, + point_count: usize, + initial_values: &[EagerComplex64], + momenta: &[f64], + model_parameters: &[EagerComplex64], + reduced: &mut [f64], + ) -> RusticolResult<()> { + validate_io( + &self.plan, + point_count, + initial_values, + momenta, + model_parameters, + None, + Some(reduced), + )?; + self.refresh_broadcast_planes(model_parameters)?; + let tile_capacity = self.effective_point_tile_size(); + let mut tile_start = 0usize; + while tile_start < point_count { + let tile_points = tile_capacity.min(point_count - tile_start); + execute_contiguous_tile( + &self.plan, + self.catalog, + &self.layout, + &mut self.workspace, + &self.initial_value_slots, + &self.value_ranges, + &self.momentum_ranges, + self.elide_structural_zero_fills, + &self.full_schedule, + &mut self.reduction_groups, + point_count, + tile_start, + tile_points, + initial_values, + momenta, + model_parameters, + )?; + reduce_full_tile( + &self.plan, + &self.workspace, + &mut self.reduction_groups, + &mut self.reduced_tile, + tile_points, + ); + reduced[tile_start..tile_start + tile_points] + .copy_from_slice(&self.reduced_tile[..tile_points]); + tile_start += tile_points; + } + Ok(()) + } + + #[allow(clippy::too_many_arguments)] + pub(crate) fn evaluate_into( + &mut self, + point_count: usize, + initial_values: &[EagerComplex64], + momenta: &[f64], + model_parameters: &[EagerComplex64], + amplitudes: &mut [EagerComplex64], + reduced: &mut [f64], + ) -> RusticolResult<()> { + validate_io( + &self.plan, + point_count, + initial_values, + momenta, + model_parameters, + Some(amplitudes), + Some(reduced), + )?; + self.refresh_broadcast_planes(model_parameters)?; + let tile_capacity = self.effective_point_tile_size(); + let mut tile_start = 0usize; + while tile_start < point_count { + let tile_points = tile_capacity.min(point_count - tile_start); + execute_contiguous_tile( + &self.plan, + self.catalog, + &self.layout, + &mut self.workspace, + &self.initial_value_slots, + &self.value_ranges, + &self.momentum_ranges, + self.elide_structural_zero_fills, + &self.full_schedule, + &mut self.reduction_groups, + point_count, + tile_start, + tile_points, + initial_values, + momenta, + model_parameters, + )?; + reduce_full_tile( + &self.plan, + &self.workspace, + &mut self.reduction_groups, + &mut self.reduced_tile, + tile_points, + ); + copy_amplitude_tile( + &self.workspace, + 0..self.plan.amplitude_count, + point_count, + tile_start, + tile_points, + amplitudes, + ); + reduced[tile_start..tile_start + tile_points] + .copy_from_slice(&self.reduced_tile[..tile_points]); + tile_start += tile_points; + } + Ok(()) + } + + #[allow(clippy::too_many_arguments)] + pub(crate) fn evaluate_profile_into( + &mut self, + point_count: usize, + initial_values: &[EagerComplex64], + momenta: &[f64], + model_parameters: &[EagerComplex64], + amplitudes: &mut [EagerComplex64], + reduced: &mut [f64], + ) -> RusticolResult { + validate_io( + &self.plan, + point_count, + initial_values, + momenta, + model_parameters, + Some(amplitudes), + Some(reduced), + )?; + let total_start = Instant::now(); + let internal_scratch_before = self.workspace.internal_scratch_bytes(); + let internal_broadcast_before = self.internal_broadcast_bytes; + let mut profile = super::EagerExecutionProfile::default(); + let phase = Instant::now(); + self.refresh_broadcast_planes(model_parameters)?; + profile.gather += phase.elapsed(); + let tile_capacity = self.effective_point_tile_size(); + let mut tile_start = 0usize; + while tile_start < point_count { + let tile_points = tile_capacity.min(point_count - tile_start); + + let phase = Instant::now(); + self.workspace + .begin_tile(count_u32(tile_points, "active point tile")?)?; + self.workspace + .clear_amplitude_active(0, count_u32(self.plan.amplitude_count, "amplitudes")?)?; + profile.initialize += phase.elapsed(); + + let phase = Instant::now(); + fill_contiguous_tile_inputs( + &self.plan, + self.catalog, + &self.layout, + &mut self.workspace, + &self.initial_value_slots, + &self.value_ranges, + &self.momentum_ranges, + self.elide_structural_zero_fills, + point_count, + tile_start, + tile_points, + initial_values, + momenta, + model_parameters, + )?; + fill_schedule_factors( + &self.plan, + &self.full_schedule, + &mut self.workspace, + model_parameters, + )?; + profile.gather += phase.elapsed(); + + execute_schedule_profiled( + &self.full_schedule, + &mut self.workspace, + &mut self.reduction_groups, + count_u32(tile_points, "active point tile")?, + &mut profile, + )?; + + let phase = Instant::now(); + reduce_full_tile( + &self.plan, + &self.workspace, + &mut self.reduction_groups, + &mut self.reduced_tile, + tile_points, + ); + profile.reduction += phase.elapsed(); + + let phase = Instant::now(); + copy_amplitude_tile( + &self.workspace, + 0..self.plan.amplitude_count, + point_count, + tile_start, + tile_points, + amplitudes, + ); + reduced[tile_start..tile_start + tile_points] + .copy_from_slice(&self.reduced_tile[..tile_points]); + profile.copy_out += phase.elapsed(); + tile_start += tile_points; + } + profile.total = total_start.elapsed(); + profile.internal_scratch_bytes = self + .workspace + .internal_scratch_bytes() + .saturating_sub(internal_scratch_before); + profile.internal_broadcast_bytes = self + .internal_broadcast_bytes + .saturating_sub(internal_broadcast_before); + Ok(profile) + } + + pub(crate) fn full_schedule_call_counts(&self) -> (u64, u64, u64) { + let mut calls = 0_u64; + let mut invocations = 0_u64; + let mut destinations = 0_u64; + for stage in &self.full_schedule.stages { + for call in stage + .invocation_calls + .iter() + .chain(stage.finalization_calls.iter()) + { + calls += 1; + invocations += u64::from(call.rows.invocation_count()); + destinations += u64::from(call.rows.attachment_count()); + } + } + for call in &self.full_schedule.closure_calls { + calls += 1; + invocations += u64::from(call.rows.invocation_count()); + destinations += u64::from(call.rows.attachment_count()); + } + (calls, invocations, destinations) + } + + pub(crate) fn schedule_audit( + &mut self, + active_groups: Option<&[u32]>, + ) -> RusticolResult> { + if let Some(groups) = active_groups { + self.prepare_selected_schedule(groups)?; + } + let schedule = if active_groups.is_some() { + &self + .selected_schedule + .as_ref() + .ok_or_else(|| RusticolError::internal("eager direct selected schedule is absent"))? + .schedule + } else { + &self.full_schedule + }; + let mut rows = Vec::new(); + for stage in &schedule.stages { + for call in &stage.invocation_calls { + rows.push(EagerScheduleAuditRow { + stage_index: Some(stage.stage_index), + role: "invocation", + kernel_id: Some(call.kernel_id), + call_count: 1, + row_count: call.rows.invocation_count() as usize, + destination_count: call.rows.attachment_count() as usize, + }); + } + if !stage.unpropagated_copies.is_empty() { + rows.push(EagerScheduleAuditRow { + stage_index: Some(stage.stage_index), + role: "copy", + kernel_id: None, + call_count: 0, + row_count: stage.unpropagated_copies.len(), + destination_count: stage + .unpropagated_copies + .iter() + .map(|copy| copy.component_count as usize) + .sum(), + }); + } + for call in &stage.finalization_calls { + rows.push(EagerScheduleAuditRow { + stage_index: Some(stage.stage_index), + role: "finalization", + kernel_id: Some(call.kernel_id), + call_count: 1, + row_count: call.rows.invocation_count() as usize, + destination_count: call.rows.attachment_count() as usize, + }); + } + } + for call in &schedule.closure_calls { + rows.push(EagerScheduleAuditRow { + stage_index: None, + role: "closure", + kernel_id: Some(call.kernel_id), + call_count: 1, + row_count: call.rows.invocation_count() as usize, + destination_count: call.rows.attachment_count() as usize, + }); + } + if !schedule.direct_closures.is_empty() { + rows.push(EagerScheduleAuditRow { + stage_index: None, + role: "direct-closure", + kernel_id: None, + call_count: 0, + row_count: schedule.direct_closures.len(), + destination_count: schedule.direct_closures.len(), + }); + } + Ok(rows) + } + + #[allow(clippy::too_many_arguments)] + pub(crate) fn evaluate_selected_profile_into( + &mut self, + active_groups: &[u32], + point_count: usize, + initial_values: &[EagerComplex64], + momenta: &[f64], + model_parameters: &[EagerComplex64], + amplitudes: &mut [EagerComplex64], + ) -> RusticolResult { + validate_io( + &self.plan, + point_count, + initial_values, + momenta, + model_parameters, + Some(amplitudes), + None, + )?; + let total_start = Instant::now(); + let internal_scratch_before = self.workspace.internal_scratch_bytes(); + let internal_broadcast_before = self.internal_broadcast_bytes; + let phase = Instant::now(); + self.prepare_selected_schedule(active_groups)?; + self.refresh_broadcast_planes(model_parameters)?; + let mut profile = super::EagerExecutionProfile { + initialize: phase.elapsed(), + ..super::EagerExecutionProfile::default() + }; + let selected = self + .selected_schedule + .as_ref() + .ok_or_else(|| RusticolError::internal("eager direct selected schedule is absent"))?; + let tile_capacity = self.effective_point_tile_size(); + let mut tile_start = 0usize; + while tile_start < point_count { + let tile_points = tile_capacity.min(point_count - tile_start); + + let phase = Instant::now(); + self.workspace + .begin_tile(count_u32(tile_points, "active point tile")?)?; + self.workspace + .clear_amplitude_active(0, count_u32(self.plan.amplitude_count, "amplitudes")?)?; + profile.initialize += phase.elapsed(); + + let phase = Instant::now(); + fill_contiguous_tile_inputs( + &self.plan, + self.catalog, + &self.layout, + &mut self.workspace, + &self.initial_value_slots, + &self.value_ranges, + &self.momentum_ranges, + self.elide_structural_zero_fills, + point_count, + tile_start, + tile_points, + initial_values, + momenta, + model_parameters, + )?; + fill_schedule_factors( + &self.plan, + &selected.schedule, + &mut self.workspace, + model_parameters, + )?; + profile.gather += phase.elapsed(); + + execute_schedule_profiled( + &selected.schedule, + &mut self.workspace, + &mut self.reduction_groups, + count_u32(tile_points, "active point tile")?, + &mut profile, + )?; + + let phase = Instant::now(); + copy_selected_amplitude_tile( + &self.workspace, + &selected.schedule.active_amplitude_indices, + point_count, + tile_start, + tile_points, + amplitudes, + ); + profile.copy_out += phase.elapsed(); + tile_start += tile_points; + } + profile.total = total_start.elapsed(); + profile.internal_scratch_bytes = self + .workspace + .internal_scratch_bytes() + .saturating_sub(internal_scratch_before); + profile.internal_broadcast_bytes = self + .internal_broadcast_bytes + .saturating_sub(internal_broadcast_before); + Ok(profile) + } + + #[allow(clippy::too_many_arguments)] + pub(crate) fn evaluate_selected_active_amplitudes_into( + &mut self, + active_groups: &[u32], + point_count: usize, + initial_values: &[EagerComplex64], + momenta: &[f64], + model_parameters: &[EagerComplex64], + amplitudes: &mut [EagerComplex64], + ) -> RusticolResult<()> { + validate_io( + &self.plan, + point_count, + initial_values, + momenta, + model_parameters, + Some(amplitudes), + None, + )?; + self.prepare_selected_schedule(active_groups)?; + self.refresh_broadcast_planes(model_parameters)?; + let selected = self + .selected_schedule + .as_ref() + .ok_or_else(|| RusticolError::internal("eager direct selected schedule is absent"))?; + let tile_capacity = self.workspace.arena().tile_capacity() as usize; + let mut tile_start = 0usize; + while tile_start < point_count { + let tile_points = tile_capacity.min(point_count - tile_start); + execute_contiguous_tile( + &self.plan, + self.catalog, + &self.layout, + &mut self.workspace, + &self.initial_value_slots, + &self.value_ranges, + &self.momentum_ranges, + self.elide_structural_zero_fills, + &selected.schedule, + &mut self.reduction_groups, + point_count, + tile_start, + tile_points, + initial_values, + momenta, + model_parameters, + )?; + copy_selected_amplitude_tile( + &self.workspace, + &selected.schedule.active_amplitude_indices, + point_count, + tile_start, + tile_points, + amplitudes, + ); + tile_start += tile_points; + } + Ok(()) + } + + #[allow(clippy::too_many_arguments)] + pub(crate) fn evaluate_point_selected_group_sets_into( + &mut self, + group_offsets: &[usize], + active_groups: &[u32], + active_group_weights: &[f64], + point_count: usize, + initial_values: &[EagerComplex64], + momenta: &[f64], + model_parameters: &[EagerComplex64], + reduced: &mut [f64], + ) -> RusticolResult<()> { + validate_io( + &self.plan, + point_count, + initial_values, + momenta, + model_parameters, + None, + Some(reduced), + )?; + validate_point_selectors( + &self.plan, + group_offsets, + active_groups, + active_group_weights, + point_count, + )?; + self.prepare_point_selected_schedules(group_offsets, active_groups)?; + self.refresh_broadcast_planes(model_parameters)?; + reduced.fill(0.0); + + let tile_capacity = self.workspace.arena().tile_capacity() as usize; + for work in self.point_selected.work_groups.iter().copied() { + let Some(schedule_index) = work.schedule_index else { + continue; + }; + let schedule = &self.point_selected.schedules[schedule_index].schedule; + let stop = work + .point_start + .checked_add(work.point_count) + .ok_or_else(|| invalid("eager direct point work range overflows"))?; + let points = self + .point_selected + .grouped_point_indices + .get(work.point_start..stop) + .ok_or_else(|| invalid("eager direct point work range is invalid"))?; + for point_tile in points.chunks(tile_capacity) { + execute_indexed_tile( + &self.plan, + self.catalog, + &self.layout, + &mut self.workspace, + &self.initial_value_slots, + &self.value_ranges, + &self.momentum_ranges, + self.elide_structural_zero_fills, + schedule, + &mut self.reduction_groups, + point_count, + point_tile, + initial_values, + momenta, + model_parameters, + )?; + reduce_selected_indexed_tile( + &self.plan, + schedule, + &self.workspace, + &mut self.reduction_groups, + &mut self.reduced_tile, + group_offsets, + active_groups, + active_group_weights, + point_tile, + )?; + for (tile_point, original_point) in point_tile.iter().copied().enumerate() { + reduced[original_point] = self.reduced_tile[tile_point]; + } + } + } + Ok(()) + } + + fn prepare_selected_schedule(&mut self, active_groups: &[u32]) -> RusticolResult<()> { + validate_active_groups(&self.plan, Some(active_groups))?; + if self + .selected_schedule + .as_ref() + .is_some_and(|selected| selected.active_groups.as_ref() == active_groups) + { + return Ok(()); + } + let schedule = build_direct_schedule( + &self.plan, + self.catalog, + &self.layout, + &self.applications, + self.shared_zero_plane_index, + Some(active_groups), + )?; + let tile_capacity = self.workspace.arena().tile_capacity(); + self.workspace.begin_tile(tile_capacity)?; + validate_schedule_calls( + &schedule, + &mut self.workspace, + self.shared_zero_plane_index, + tile_capacity, + )?; + self.selected_schedule = Some(SelectedSchedule { + active_groups: active_groups.into(), + schedule, + }); + Ok(()) + } + + fn prepare_point_selected_schedules( + &mut self, + offsets: &[usize], + groups: &[u32], + ) -> RusticolResult<()> { + if self.point_selected.signature_offsets == offsets + && self.point_selected.signature_groups == groups + { + return Ok(()); + } + self.point_selected.point_work_group_ids.clear(); + self.point_selected.work_groups.clear(); + self.point_selected + .point_work_group_ids + .try_reserve(offsets.len().saturating_sub(1)) + .map_err(|error| { + invalid(format!( + "could not reserve eager direct point selector work: {error}" + )) + })?; + for point in 0..offsets.len() - 1 { + let selected = &groups[offsets[point]..offsets[point + 1]]; + let schedule_index = if selected.is_empty() { + None + } else if let Some(index) = self + .point_selected + .schedules + .iter() + .position(|cached| cached.active_groups.as_ref() == selected) + { + Some(index) + } else { + let schedule = build_direct_schedule( + &self.plan, + self.catalog, + &self.layout, + &self.applications, + self.shared_zero_plane_index, + Some(selected), + )?; + let tile_capacity = self.workspace.arena().tile_capacity(); + self.workspace.begin_tile(tile_capacity)?; + validate_schedule_calls( + &schedule, + &mut self.workspace, + self.shared_zero_plane_index, + tile_capacity, + )?; + self.point_selected.schedules.push(SelectedSchedule { + active_groups: selected.into(), + schedule, + }); + Some(self.point_selected.schedules.len() - 1) + }; + let work_id = self + .point_selected + .work_groups + .iter() + .position(|work| work.schedule_index == schedule_index) + .unwrap_or_else(|| { + self.point_selected.work_groups.push(PointWorkGroup { + schedule_index, + point_start: 0, + point_count: 0, + }); + self.point_selected.work_groups.len() - 1 + }); + self.point_selected.work_groups[work_id].point_count += 1; + self.point_selected.point_work_group_ids.push(work_id); + } + self.point_selected.cursors.clear(); + let mut cursor = 0usize; + for work in &mut self.point_selected.work_groups { + work.point_start = cursor; + cursor = cursor + .checked_add(work.point_count) + .ok_or_else(|| invalid("eager direct grouped point count overflows"))?; + self.point_selected.cursors.push(work.point_start); + } + self.point_selected.grouped_point_indices.clear(); + self.point_selected.grouped_point_indices.resize(cursor, 0); + for (point, work_id) in self + .point_selected + .point_work_group_ids + .iter() + .copied() + .enumerate() + { + let target = self.point_selected.cursors[work_id]; + self.point_selected.grouped_point_indices[target] = point; + self.point_selected.cursors[work_id] += 1; + } + self.point_selected.signature_offsets.clear(); + self.point_selected + .signature_offsets + .extend_from_slice(offsets); + self.point_selected.signature_groups.clear(); + self.point_selected + .signature_groups + .extend_from_slice(groups); + Ok(()) + } +} + +fn load_applications( + plan: &EagerExecutionPlan, + prepared: &[EagerDirectPreparedKernel<'_>], +) -> RusticolResult>> { + let mut applications = BTreeMap::new(); + for artifact in prepared { + if applications.contains_key(&artifact.kernel_id) { + return Err(RusticolError::integrity(format!( + "eager direct prepared catalog repeats kernel {}", + artifact.kernel_id + ))); + } + let kernel = plan.kernels.get(&artifact.kernel_id).ok_or_else(|| { + RusticolError::integrity(format!( + "eager direct prepared catalog references unknown kernel {}", + artifact.kernel_id + )) + })?; + validate_callable_identity(kernel, artifact)?; + let application = match &artifact.application { + EagerDirectPreparedApplication::Symjit { + source_application, + descriptor, + expected_optimization_level, + expected_compression, + } => LoadedSymjitEagerDirectTable::load_prepared_application_bytes( + source_application, + descriptor, + artifact.display_path.clone(), + EAGER_DIRECT_SOURCE_APPLICATION_ABI, + EAGER_DIRECT_TABLE_DESCRIPTOR_ABI, + EAGER_DIRECT_TABLE_BINDING_ABI, + *expected_optimization_level, + *expected_compression, + )?, + EagerDirectPreparedApplication::Native { + library, + function_name, + source_application_abi, + invocation_stride, + attachment_stride, + target_triple, + evaluator_state_sha256, + simd_lane_width, + } => LoadedSymjitEagerDirectTable::load_native_application( + Arc::clone(library), + function_name, + artifact.display_path.clone(), + source_application_abi, + *invocation_stride, + *attachment_stride, + count_u32(artifact.inputs.len(), "native eager input width")?, + artifact.output_component_count, + target_triple, + evaluator_state_sha256, + *simd_lane_width, + )?, + }; + applications.insert(artifact.kernel_id, Arc::new(application)); + } + Ok(applications) +} + +fn plane_bindings( + layout: &DirectArenaLayout, + amplitude_count: usize, +) -> RusticolResult> { + let capacity = layout + .component_count() + .checked_add(count_u32(amplitude_count, "amplitude planes")?) + .and_then(|count| count.checked_mul(2)) + .ok_or_else(|| invalid("eager direct plane catalog overflows"))?; + let mut bindings = Vec::new(); + bindings + .try_reserve_exact(capacity as usize) + .map_err(|error| invalid(format!("could not reserve eager direct planes: {error}")))?; + for component in 0..layout.component_count() { + bindings.push(EagerDirectArenaPlaneBinding::CurrentReal(component)); + bindings.push(EagerDirectArenaPlaneBinding::CurrentImag(component)); + } + for amplitude in 0..count_u32(amplitude_count, "amplitude planes")? { + bindings.push(EagerDirectArenaPlaneBinding::AmplitudeReal(amplitude)); + bindings.push(EagerDirectArenaPlaneBinding::AmplitudeImag(amplitude)); + } + Ok(bindings) +} + +/// Reuse one already-allocated, event-pinned imaginary plane as the structural +/// zero required by real-only complex P-kernel inputs. +/// +/// `derive_event_layout` pins momenta, couplings, and parameters through the +/// final event. Momentum and split coupling semantics are real-only, so their +/// imaginary halves are initialized to zero and are never legal destinations. +/// No extra plane or artifact-ABI entry is required. +fn shared_zero_plane_index( + sections: EagerPlanV3Sections<'_>, + catalog: SemanticCatalog, + layout: &DirectArenaLayout, + applications: &BTreeMap>, +) -> RusticolResult> { + if !applications + .values() + .any(|application| application.is_symjit()) + { + return Ok(None); + } + let physical_component = if let Some(momentum) = sections + .momenta + .iter() + .find(|momentum| momentum.component_count != 0) + { + Some(assigned_component( + layout, + catalog.momentum(momentum.momentum_slot_id)?, + 0, + )?) + } else if let Some(coupling) = sections.couplings.first() { + Some(assigned_component( + layout, + catalog.coupling_real(coupling.coupling_id)?, + 0, + )?) + } else { + None + }; + physical_component + .map(|component| { + component + .checked_mul(2) + .and_then(|plane| plane.checked_add(1)) + .ok_or_else(|| invalid("eager direct shared-zero plane id overflows")) + }) + .transpose() +} + +fn build_direct_schedule( + plan: &EagerExecutionPlan, + catalog: SemanticCatalog, + layout: &DirectArenaLayout, + applications: &BTreeMap>, + shared_zero_plane_index: Option, + active_groups: Option<&[u32]>, +) -> RusticolResult { + validate_active_groups(plan, active_groups)?; + let mut factor_specs = Vec::new(); + let mut stages = Vec::with_capacity(plan.stages.len()); + for stage in &plan.stages { + stages.push(build_direct_stage( + plan, + stage, + catalog, + layout, + applications, + shared_zero_plane_index, + active_groups, + &mut factor_specs, + )?); + } + let (closure_calls, direct_closures) = build_direct_closures( + plan, + catalog, + layout, + applications, + shared_zero_plane_index, + active_groups, + &mut factor_specs, + )?; + let ( + active_amplitude_indices, + active_reduction_group_indices, + active_reduction_entry_indices, + active_reduction_group_ids, + ) = selected_reduction_metadata(plan, active_groups); + Ok(DirectSchedule { + stages: stages.into_boxed_slice(), + closure_calls: closure_calls.into_boxed_slice(), + direct_closures: direct_closures.into_boxed_slice(), + factor_specs: factor_specs.into_boxed_slice(), + active_amplitude_indices: active_amplitude_indices.into_boxed_slice(), + active_reduction_group_indices: active_reduction_group_indices.into_boxed_slice(), + active_reduction_entry_indices: active_reduction_entry_indices.into_boxed_slice(), + active_reduction_group_ids: active_reduction_group_ids.into_boxed_slice(), + }) +} + +#[allow(clippy::too_many_arguments)] +fn build_direct_stage( + plan: &EagerExecutionPlan, + stage: &EagerStagePlan, + catalog: SemanticCatalog, + layout: &DirectArenaLayout, + applications: &BTreeMap>, + shared_zero_plane_index: Option, + active_groups: Option<&[u32]>, + factor_specs: &mut Vec, +) -> RusticolResult { + let invocation_calls = build_invocation_calls( + plan, + stage, + catalog, + layout, + applications, + shared_zero_plane_index, + active_groups, + factor_specs, + )?; + let mut copies = Vec::new(); + for copy in &stage.finalization_copies { + if !row_active(plan, copy.selector_domain_id, active_groups)? { + continue; + } + let (current_id, _) = stage_current_id(stage, copy.current)?; + let source = assigned_component(layout, catalog.current(current_id)?, 0)?; + let value_id = value_id_for_range(plan, copy.unpropagated)?; + let destination = assigned_component(layout, catalog.value(value_id)?, 0)?; + copies.push(DirectCopy { + source_component_base: source, + destination_component_base: destination, + component_count: count_u32(copy.current.len, "finalization copy width")?, + }); + } + let finalization_calls = build_finalization_calls( + plan, + stage, + catalog, + layout, + applications, + shared_zero_plane_index, + active_groups, + factor_specs, + )?; + Ok(DirectStage { + stage_index: stage.stage_index, + invocation_calls: invocation_calls.into_boxed_slice(), + unpropagated_copies: copies.into_boxed_slice(), + finalization_calls: finalization_calls.into_boxed_slice(), + }) +} + +fn stage_current_id( + stage: &EagerStagePlan, + range: ComponentRange, +) -> RusticolResult<(u32, ComponentRange)> { + stage + .finalizations + .iter() + .find(|item| item.current == range) + .map(|item| (item.row.current_id, item.current)) + .or_else(|| { + stage.attachments.iter().find_map(|item| { + (item.current == range).then_some((item.row.result_current_id, item.current)) + }) + }) + .ok_or_else(|| RusticolError::internal("eager direct stage lost a compact current range")) +} + +fn value_id_for_range(plan: &EagerExecutionPlan, range: ComponentRange) -> RusticolResult { + plan.values + .id_for_range(range, "eager direct finalization value") +} + +#[allow(clippy::too_many_arguments)] +fn build_invocation_calls( + plan: &EagerExecutionPlan, + stage: &EagerStagePlan, + catalog: SemanticCatalog, + layout: &DirectArenaLayout, + applications: &BTreeMap>, + shared_zero_plane_index: Option, + active_groups: Option<&[u32]>, + factor_specs: &mut Vec, +) -> RusticolResult> { + let mut calls = Vec::new(); + let mut written_currents = BTreeSet::new(); + let mut cursor = 0usize; + while cursor < stage.invocations.len() { + let kernel_id = stage.invocations[cursor].row.kernel_id; + let stop = stage.invocations[cursor..] + .iter() + .position(|row| row.row.kernel_id != kernel_id) + .map_or(stage.invocations.len(), |offset| cursor + offset); + let kernel = plan.kernels.get(&kernel_id).ok_or_else(|| { + RusticolError::internal(format!("eager direct schedule lost kernel {kernel_id}")) + })?; + let kernel_shared_zero_plane_index = shared_zero_plane_index.filter(|_| { + applications + .get(&kernel_id) + .is_some_and(|application| application.is_symjit()) + }); + let mut invocation_bytes = Vec::new(); + let mut attachment_bytes = Vec::new(); + let mut local_attachment_count = 0_u32; + for invocation in &stage.invocations[cursor..stop] { + if !row_active(plan, invocation.selector_domain_id, active_groups)? { + continue; + } + let retained = stage.attachments[invocation.attachment_range.clone()] + .iter() + .map(|attachment| row_active(plan, attachment.selector_domain_id, active_groups)) + .collect::>>()?; + let active_attachment_count = retained.iter().filter(|active| **active).count(); + if active_attachment_count == 0 { + continue; + } + encode_invocation_inputs( + &mut invocation_bytes, + invocation, + &kernel.inputs, + catalog, + layout, + kernel_shared_zero_plane_index, + )?; + push_u32(&mut invocation_bytes, local_attachment_count); + push_u32( + &mut invocation_bytes, + count_u32(active_attachment_count, "active invocation attachments")?, + ); + for (attachment, active) in stage.attachments[invocation.attachment_range.clone()] + .iter() + .zip(retained) + { + if !active { + continue; + } + encode_current_destination( + &mut attachment_bytes, + attachment.row.result_current_id, + kernel.output_component_count, + catalog, + layout, + )?; + let factor_index = count_u32(factor_specs.len(), "eager direct factors")?; + factor_specs.push(DirectFactorSpec { + factor: EagerComplex64::new( + attachment.row.factor_real, + attachment.row.factor_imag, + ), + coupling_slot_id: Some(invocation.row.coupling_slot_id), + output_factor_source: invocation.row.output_factor_source, + }); + push_u32(&mut attachment_bytes, factor_index); + push_u32( + &mut attachment_bytes, + u32::from(!written_currents.insert(attachment.row.result_current_id)), + ); + local_attachment_count = local_attachment_count + .checked_add(1) + .ok_or_else(|| invalid("eager direct attachment count overflows"))?; + } + } + if !invocation_bytes.is_empty() { + calls.push(load_call( + applications, + kernel_id, + invocation_bytes, + attachment_bytes, + )?); + } + cursor = stop; + } + Ok(calls) +} + +#[allow(clippy::too_many_arguments)] +fn build_finalization_calls( + plan: &EagerExecutionPlan, + stage: &EagerStagePlan, + catalog: SemanticCatalog, + layout: &DirectArenaLayout, + applications: &BTreeMap>, + shared_zero_plane_index: Option, + active_groups: Option<&[u32]>, + factor_specs: &mut Vec, +) -> RusticolResult> { + let mut calls = Vec::new(); + let mut cursor = 0usize; + while cursor < stage.finalizations.len() { + let kernel_id = stage.finalizations[cursor].row.kernel_id; + let stop = stage.finalizations[cursor..] + .iter() + .position(|row| row.row.kernel_id != kernel_id) + .map_or(stage.finalizations.len(), |offset| cursor + offset); + let kernel = plan.kernels.get(&kernel_id).ok_or_else(|| { + RusticolError::internal(format!("eager direct finalization lost kernel {kernel_id}")) + })?; + if kernel.role != EagerKernelRole::Finalization { + return Err(RusticolError::integrity(format!( + "eager direct finalization kernel {kernel_id} has role {:?}", + kernel.role + ))); + } + let kernel_shared_zero_plane_index = shared_zero_plane_index.filter(|_| { + applications + .get(&kernel_id) + .is_some_and(|application| application.is_symjit()) + }); + let mut invocation_bytes = Vec::new(); + let mut attachment_bytes = Vec::new(); + let mut attachment_count = 0_u32; + for item in &stage.finalizations[cursor..stop] { + if !row_active(plan, item.selector_domain_id, active_groups)? { + continue; + } + encode_finalization_inputs( + &mut invocation_bytes, + item, + &kernel.inputs, + catalog, + layout, + kernel_shared_zero_plane_index, + )?; + push_u32(&mut invocation_bytes, attachment_count); + push_u32(&mut invocation_bytes, 1); + let propagated = item.propagated.ok_or_else(|| { + RusticolError::internal("eager direct finalization lost propagated output") + })?; + let value_id = value_id_for_range(plan, propagated)?; + encode_value_destination( + &mut attachment_bytes, + value_id, + kernel.output_component_count, + catalog, + layout, + )?; + let factor_index = count_u32(factor_specs.len(), "eager direct factors")?; + factor_specs.push(DirectFactorSpec { + factor: EagerComplex64::new(1.0, 0.0), + coupling_slot_id: None, + output_factor_source: EAGER_OUTPUT_FACTOR_NONE, + }); + push_u32(&mut attachment_bytes, factor_index); + push_u32(&mut attachment_bytes, 0); + attachment_count = attachment_count + .checked_add(1) + .ok_or_else(|| invalid("eager direct finalization count overflows"))?; + } + if !invocation_bytes.is_empty() { + calls.push(load_call( + applications, + kernel_id, + invocation_bytes, + attachment_bytes, + )?); + } + cursor = stop; + } + Ok(calls) +} + +#[allow(clippy::too_many_arguments)] +fn build_direct_closures( + plan: &EagerExecutionPlan, + catalog: SemanticCatalog, + layout: &DirectArenaLayout, + applications: &BTreeMap>, + shared_zero_plane_index: Option, + active_groups: Option<&[u32]>, + factor_specs: &mut Vec, +) -> RusticolResult<(Vec, Vec)> { + let mut calls = Vec::new(); + let mut written_amplitudes = BTreeSet::new(); + let mut cursor = 0usize; + while cursor < plan.closures.len() { + let kernel_id = plan.closures[cursor].row.kernel_id; + let stop = plan.closures[cursor..] + .iter() + .position(|row| row.row.kernel_id != kernel_id) + .map_or(plan.closures.len(), |offset| cursor + offset); + let kernel = plan.kernels.get(&kernel_id).ok_or_else(|| { + RusticolError::internal(format!("eager direct closure lost kernel {kernel_id}")) + })?; + if kernel.role != EagerKernelRole::Closure { + return Err(RusticolError::integrity(format!( + "eager direct closure kernel {kernel_id} has role {:?}", + kernel.role + ))); + } + let kernel_shared_zero_plane_index = shared_zero_plane_index.filter(|_| { + applications + .get(&kernel_id) + .is_some_and(|application| application.is_symjit()) + }); + let mut invocation_bytes = Vec::new(); + let mut attachment_bytes = Vec::new(); + let mut attachment_count = 0_u32; + for item in &plan.closures[cursor..stop] { + if !row_active(plan, item.selector_domain_id, active_groups)? { + continue; + } + encode_closure_inputs( + &mut invocation_bytes, + item, + &kernel.inputs, + catalog, + layout, + kernel_shared_zero_plane_index, + )?; + push_u32(&mut invocation_bytes, attachment_count); + push_u32(&mut invocation_bytes, 1); + encode_amplitude_destination( + &mut attachment_bytes, + item.row.amplitude_index, + kernel.output_component_count, + layout, + )?; + let factor_index = count_u32(factor_specs.len(), "eager direct factors")?; + factor_specs.push(DirectFactorSpec { + factor: EagerComplex64::new(item.row.factor_real, item.row.factor_imag), + coupling_slot_id: Some(item.row.coupling_slot_id), + output_factor_source: item.row.output_factor_source, + }); + push_u32(&mut attachment_bytes, factor_index); + push_u32( + &mut attachment_bytes, + u32::from(!written_amplitudes.insert(item.row.amplitude_index)), + ); + attachment_count = attachment_count + .checked_add(1) + .ok_or_else(|| invalid("eager direct closure count overflows"))?; + } + if !invocation_bytes.is_empty() { + calls.push(load_call( + applications, + kernel_id, + invocation_bytes, + attachment_bytes, + )?); + } + cursor = stop; + } + + let mut direct = Vec::new(); + for item in &plan.direct_closures { + if !row_active(plan, item.selector_domain_id, active_groups)? { + continue; + } + let left_id = value_id_for_range(plan, item.left_values)?; + let right_id = value_id_for_range(plan, item.right_values)?; + direct.push(DirectCoefficientClosure { + left_component_base: assigned_component(layout, catalog.value(left_id)?, 0)?, + right_component_base: assigned_component(layout, catalog.value(right_id)?, 0)?, + component_count: count_u32(item.coefficients.len(), "direct closure width")?, + amplitude_index: item.row.amplitude_index, + factor: AccumulationFactor::from_parts(item.row.factor_real, item.row.factor_imag), + initializes_amplitude: written_amplitudes.insert(item.row.amplitude_index), + coefficients: item.coefficients.clone().into_boxed_slice(), + }); + } + Ok((calls, direct)) +} + +fn load_call( + applications: &BTreeMap>, + kernel_id: u32, + invocation_bytes: Vec, + attachment_bytes: Vec, +) -> RusticolResult { + let application = applications.get(&kernel_id).cloned().ok_or_else(|| { + RusticolError::compatibility(format!( + "eager direct execution has no prepared table callable for kernel {kernel_id}" + )) + })?; + let rows = application.load_rows(invocation_bytes, attachment_bytes)?; + Ok(DirectCall { + kernel_id, + application, + rows, + }) +} + +fn encode_invocation_inputs( + output: &mut Vec, + invocation: &ScheduledInvocation, + descriptors: &[EagerKernelInput], + catalog: SemanticCatalog, + layout: &DirectArenaLayout, + shared_zero_plane_index: Option, +) -> RusticolResult<()> { + encode_inputs( + output, + descriptors, + |descriptor| match descriptor { + EagerKernelInput::FirstCurrentComponent(component) => Ok(( + catalog.value(invocation.row.left_value_slot_id)?, + *component, + false, + )), + EagerKernelInput::SecondCurrentComponent(component) => Ok(( + catalog.value(invocation.row.right_value_slot_id)?, + *component, + false, + )), + EagerKernelInput::FirstMomentumComponent(component) => Ok(( + catalog.momentum(invocation.row.left_momentum_slot_id)?, + *component, + true, + )), + EagerKernelInput::SecondMomentumComponent(component) => Ok(( + catalog.momentum(invocation.row.right_momentum_slot_id)?, + *component, + true, + )), + EagerKernelInput::CouplingReal => Ok(( + catalog.coupling_real(invocation.row.coupling_slot_id)?, + 0, + true, + )), + EagerKernelInput::CouplingImag => Ok(( + catalog.coupling_imag(invocation.row.coupling_slot_id)?, + 0, + true, + )), + EagerKernelInput::ModelParameter(parameter) => { + Ok((catalog.parameter(*parameter)?, 0, false)) + } + }, + layout, + shared_zero_plane_index, + ) +} + +fn encode_finalization_inputs( + output: &mut Vec, + item: &ScheduledFinalization, + descriptors: &[EagerKernelInput], + catalog: SemanticCatalog, + layout: &DirectArenaLayout, + shared_zero_plane_index: Option, +) -> RusticolResult<()> { + encode_inputs( + output, + descriptors, + |descriptor| match descriptor { + EagerKernelInput::FirstCurrentComponent(component) => { + Ok((catalog.current(item.row.current_id)?, *component, false)) + } + EagerKernelInput::FirstMomentumComponent(component) => Ok(( + catalog.momentum(item.row.momentum_slot_id)?, + *component, + true, + )), + EagerKernelInput::ModelParameter(parameter) => { + Ok((catalog.parameter(*parameter)?, 0, false)) + } + _ => Err(RusticolError::integrity( + "eager direct finalization has an invalid semantic input", + )), + }, + layout, + shared_zero_plane_index, + ) +} + +fn encode_closure_inputs( + output: &mut Vec, + item: &ScheduledClosure, + descriptors: &[EagerKernelInput], + catalog: SemanticCatalog, + layout: &DirectArenaLayout, + shared_zero_plane_index: Option, +) -> RusticolResult<()> { + encode_inputs( + output, + descriptors, + |descriptor| match descriptor { + EagerKernelInput::FirstCurrentComponent(component) => Ok(( + catalog.value(item.row.left_value_slot_id)?, + *component, + false, + )), + EagerKernelInput::SecondCurrentComponent(component) => Ok(( + catalog.value(item.row.right_value_slot_id)?, + *component, + false, + )), + EagerKernelInput::CouplingReal => { + Ok((catalog.coupling_real(item.row.coupling_slot_id)?, 0, true)) + } + EagerKernelInput::CouplingImag => { + Ok((catalog.coupling_imag(item.row.coupling_slot_id)?, 0, true)) + } + EagerKernelInput::ModelParameter(parameter) => { + Ok((catalog.parameter(*parameter)?, 0, false)) + } + _ => Err(RusticolError::integrity( + "eager direct closure has an invalid semantic input", + )), + }, + layout, + shared_zero_plane_index, + ) +} + +fn encode_inputs( + output: &mut Vec, + descriptors: &[EagerKernelInput], + mut resolve: impl FnMut(&EagerKernelInput) -> RusticolResult<(u32, u32, bool)>, + layout: &DirectArenaLayout, + shared_zero_plane_index: Option, +) -> RusticolResult<()> { + for descriptor in descriptors { + let (semantic, component, real_only) = resolve(descriptor)?; + let physical = assigned_component(layout, semantic, component)?; + if let Some(zero_plane) = shared_zero_plane_index.filter(|_| real_only) { + push_current_real_with_zero(output, physical, zero_plane)?; + } else { + push_current_plane_pair(output, physical)?; + } + } + Ok(()) +} + +fn encode_current_destination( + output: &mut Vec, + current_id: u32, + width: u32, + catalog: SemanticCatalog, + layout: &DirectArenaLayout, +) -> RusticolResult<()> { + let semantic = catalog.current(current_id)?; + for component in 0..width { + push_current_plane_pair(output, assigned_component(layout, semantic, component)?)?; + } + Ok(()) +} + +fn encode_value_destination( + output: &mut Vec, + value_id: u32, + width: u32, + catalog: SemanticCatalog, + layout: &DirectArenaLayout, +) -> RusticolResult<()> { + let semantic = catalog.value(value_id)?; + for component in 0..width { + push_current_plane_pair(output, assigned_component(layout, semantic, component)?)?; + } + Ok(()) +} + +fn encode_amplitude_destination( + output: &mut Vec, + amplitude_index: u32, + width: u32, + layout: &DirectArenaLayout, +) -> RusticolResult<()> { + if width != 1 { + return Err(RusticolError::integrity( + "eager direct closure output width must be one", + )); + } + let base = layout + .component_count() + .checked_mul(2) + .and_then(|value| value.checked_add(amplitude_index.checked_mul(2)?)) + .ok_or_else(|| invalid("eager direct amplitude plane id overflows"))?; + push_u32(output, base); + push_u32( + output, + base.checked_add(1) + .ok_or_else(|| invalid("eager direct amplitude plane id overflows"))?, + ); + Ok(()) +} + +fn push_current_plane_pair(output: &mut Vec, component: u32) -> RusticolResult<()> { + let real = component + .checked_mul(2) + .ok_or_else(|| invalid("eager direct current plane id overflows"))?; + push_u32(output, real); + push_u32( + output, + real.checked_add(1) + .ok_or_else(|| invalid("eager direct current plane id overflows"))?, + ); + Ok(()) +} + +fn push_current_real_with_zero( + output: &mut Vec, + component: u32, + zero_plane: u32, +) -> RusticolResult<()> { + let real = component + .checked_mul(2) + .ok_or_else(|| invalid("eager direct current plane id overflows"))?; + push_u32(output, real); + push_u32(output, zero_plane); + Ok(()) +} + +#[allow(clippy::type_complexity)] +fn selected_reduction_metadata( + plan: &EagerExecutionPlan, + active_groups: Option<&[u32]>, +) -> (Vec, Vec, Vec, Vec) { + let group_active = + |group_id: u32| active_groups.is_none_or(|groups| groups.binary_search(&group_id).is_ok()); + let active_reduction_group_indices = plan + .reduction_groups + .iter() + .enumerate() + .filter_map(|(index, group)| group_active(group.coherent_group_id).then_some(index)) + .collect::>(); + let mut active_amplitude_indices = active_reduction_group_indices + .iter() + .flat_map(|index| { + plan.reduction_groups[*index] + .amplitude_indices + .iter() + .copied() + }) + .map(|index| index as usize) + .collect::>(); + active_amplitude_indices.sort_unstable(); + active_amplitude_indices.dedup(); + let active_reduction_entry_indices = plan + .reduction_entries + .iter() + .enumerate() + .filter_map(|(index, entry)| { + let left = entry.left_group_index as usize; + let right = entry.right_group_index as usize; + (active_reduction_group_indices.binary_search(&left).is_ok() + && active_reduction_group_indices.binary_search(&right).is_ok()) + .then_some(index) + }) + .collect::>(); + let active_reduction_group_ids = active_reduction_group_indices + .iter() + .map(|index| plan.reduction_groups[*index].coherent_group_id) + .collect(); + ( + active_amplitude_indices, + active_reduction_group_indices, + active_reduction_entry_indices, + active_reduction_group_ids, + ) +} + +fn validate_schedule_calls( + schedule: &DirectSchedule, + workspace: &mut EagerDirectTableWorkspace, + shared_zero_plane_index: Option, + point_count: u32, +) -> RusticolResult<()> { + if let Some(zero_component) = shared_zero_plane_index.map(|plane| plane / 2) { + for stage in &schedule.stages { + for copy in &stage.unpropagated_copies { + let destination_stop = copy + .destination_component_base + .checked_add(copy.component_count) + .ok_or_else(|| invalid("eager direct copy destination range overflows"))?; + if (copy.destination_component_base..destination_stop).contains(&zero_component) { + return Err(RusticolError::integrity(format!( + "eager direct copy destination range {}..{destination_stop} overlaps \ + read-only shared-zero component {zero_component}", + copy.destination_component_base + ))); + } + } + } + } + for stage in &schedule.stages { + for call in stage + .invocation_calls + .iter() + .chain(stage.finalization_calls.iter()) + { + call.application + .validate_call(&call.rows, workspace, 0, point_count)?; + } + } + for call in &schedule.closure_calls { + call.application + .validate_call(&call.rows, workspace, 0, point_count)?; + } + Ok(()) +} + +#[allow(clippy::too_many_arguments)] +fn execute_contiguous_tile( + plan: &EagerExecutionPlan, + catalog: SemanticCatalog, + layout: &DirectArenaLayout, + workspace: &mut EagerDirectTableWorkspace, + initial_value_slots: &[u32], + value_ranges: &[ComponentRange], + momentum_ranges: &[ComponentRange], + elide_structural_zero_fills: bool, + schedule: &DirectSchedule, + closure_scratch: &mut [EagerComplex64], + source_point_count: usize, + tile_start: usize, + tile_points: usize, + initial_values: &[EagerComplex64], + momenta: &[f64], + model_parameters: &[EagerComplex64], +) -> RusticolResult<()> { + execute_tile( + plan, + catalog, + layout, + workspace, + initial_value_slots, + value_ranges, + momentum_ranges, + elide_structural_zero_fills, + schedule, + closure_scratch, + source_point_count, + tile_points, + initial_values, + momenta, + model_parameters, + |point| tile_start + point, + ) +} + +#[allow(clippy::too_many_arguments)] +fn execute_indexed_tile( + plan: &EagerExecutionPlan, + catalog: SemanticCatalog, + layout: &DirectArenaLayout, + workspace: &mut EagerDirectTableWorkspace, + initial_value_slots: &[u32], + value_ranges: &[ComponentRange], + momentum_ranges: &[ComponentRange], + elide_structural_zero_fills: bool, + schedule: &DirectSchedule, + closure_scratch: &mut [EagerComplex64], + source_point_count: usize, + point_indices: &[usize], + initial_values: &[EagerComplex64], + momenta: &[f64], + model_parameters: &[EagerComplex64], +) -> RusticolResult<()> { + execute_tile( + plan, + catalog, + layout, + workspace, + initial_value_slots, + value_ranges, + momentum_ranges, + elide_structural_zero_fills, + schedule, + closure_scratch, + source_point_count, + point_indices.len(), + initial_values, + momenta, + model_parameters, + |point| point_indices[point], + ) +} + +#[allow(clippy::too_many_arguments)] +fn execute_tile( + plan: &EagerExecutionPlan, + catalog: SemanticCatalog, + layout: &DirectArenaLayout, + workspace: &mut EagerDirectTableWorkspace, + initial_value_slots: &[u32], + value_ranges: &[ComponentRange], + momentum_ranges: &[ComponentRange], + elide_structural_zero_fills: bool, + schedule: &DirectSchedule, + closure_scratch: &mut [EagerComplex64], + source_point_count: usize, + tile_points: usize, + initial_values: &[EagerComplex64], + momenta: &[f64], + model_parameters: &[EagerComplex64], + source_point: impl Fn(usize) -> usize, +) -> RusticolResult<()> { + let point_count = count_u32(tile_points, "active point tile")?; + workspace.begin_tile(point_count)?; + workspace.clear_amplitude_active(0, count_u32(plan.amplitude_count, "amplitudes")?)?; + fill_tile_inputs( + plan, + catalog, + layout, + workspace, + initial_value_slots, + value_ranges, + momentum_ranges, + elide_structural_zero_fills, + source_point_count, + tile_points, + initial_values, + momenta, + model_parameters, + source_point, + )?; + fill_schedule_factors(plan, schedule, workspace, model_parameters)?; + execute_schedule(schedule, workspace, closure_scratch, point_count) +} + +#[allow(clippy::too_many_arguments)] +fn fill_contiguous_tile_inputs( + plan: &EagerExecutionPlan, + catalog: SemanticCatalog, + layout: &DirectArenaLayout, + workspace: &mut EagerDirectTableWorkspace, + initial_value_slots: &[u32], + value_ranges: &[ComponentRange], + momentum_ranges: &[ComponentRange], + elide_structural_zero_fills: bool, + source_point_count: usize, + tile_start: usize, + tile_points: usize, + initial_values: &[EagerComplex64], + momenta: &[f64], + model_parameters: &[EagerComplex64], +) -> RusticolResult<()> { + fill_tile_inputs( + plan, + catalog, + layout, + workspace, + initial_value_slots, + value_ranges, + momentum_ranges, + elide_structural_zero_fills, + source_point_count, + tile_points, + initial_values, + momenta, + model_parameters, + |point| tile_start + point, + ) +} + +#[allow(clippy::too_many_arguments)] +fn fill_tile_inputs( + _plan: &EagerExecutionPlan, + catalog: SemanticCatalog, + layout: &DirectArenaLayout, + workspace: &mut EagerDirectTableWorkspace, + initial_value_slots: &[u32], + value_ranges: &[ComponentRange], + momentum_ranges: &[ComponentRange], + elide_structural_zero_fills: bool, + source_point_count: usize, + tile_points: usize, + initial_values: &[EagerComplex64], + momenta: &[f64], + _model_parameters: &[EagerComplex64], + source_point: impl Fn(usize) -> usize, +) -> RusticolResult<()> { + let stride = workspace.arena().point_stride() as usize; + { + let (real, imaginary, _, _) = workspace.split_arena_slices_mut(); + for &slot in initial_value_slots { + let range = value_ranges[slot as usize]; + let semantic = catalog.value(slot)?; + for component in 0..range.len { + let target = assigned_component( + layout, + semantic, + count_u32(component, "initial value component")?, + )? as usize + * stride; + let source = (range.start + component) * source_point_count; + for point in 0..tile_points { + let value = initial_values[source + source_point(point)]; + real[target + point] = value.re; + imaginary[target + point] = value.im; + } + } + } + for (slot, range) in momentum_ranges.iter().copied().enumerate() { + let semantic = catalog.momentum(count_u32(slot, "momentum slot")?)?; + for component in 0..range.len { + let target = assigned_component( + layout, + semantic, + count_u32(component, "momentum component")?, + )? as usize + * stride; + let source = (range.start + component) * source_point_count; + for point in 0..tile_points { + real[target + point] = momenta[source + source_point(point)]; + } + if !elide_structural_zero_fills { + imaginary[target..target + tile_points].fill(0.0); + } + } + } + } + Ok(()) +} + +fn fill_input_plane( + real: &mut [f64], + imaginary: &mut [f64], + stride: usize, + component: u32, + points: usize, + value_re: f64, + value_im: f64, +) { + let start = component as usize * stride; + real[start..start + points].fill(value_re); + imaginary[start..start + points].fill(value_im); +} + +fn fill_real_input_plane( + real: &mut [f64], + stride: usize, + component: u32, + points: usize, + value: f64, +) { + let start = component as usize * stride; + real[start..start + points].fill(value); +} + +fn fill_schedule_factors( + plan: &EagerExecutionPlan, + schedule: &DirectSchedule, + workspace: &mut EagerDirectTableWorkspace, + model_parameters: &[EagerComplex64], +) -> RusticolResult<()> { + let (factor_re, factor_im) = workspace.factors_mut(); + if schedule.factor_specs.len() > factor_re.len() { + return Err(RusticolError::internal( + "eager direct selected factors exceed the persistent catalog", + )); + } + for (index, spec) in schedule.factor_specs.iter().copied().enumerate() { + let scale = if let Some(coupling_slot_id) = spec.coupling_slot_id { + let coupling = resolve_coupling( + *plan + .couplings + .get(coupling_slot_id as usize) + .ok_or_else(|| { + RusticolError::internal("eager direct factor lost its coupling") + })?, + model_parameters, + ); + match spec.output_factor_source { + EAGER_OUTPUT_FACTOR_NONE => 1.0, + EAGER_OUTPUT_FACTOR_COUPLING_REAL => coupling.re, + EAGER_OUTPUT_FACTOR_COUPLING_IMAG => coupling.im, + _ => { + return Err(RusticolError::integrity( + "eager direct factor has an invalid output-factor source", + )); + } + } + } else { + if spec.output_factor_source != EAGER_OUTPUT_FACTOR_NONE { + return Err(RusticolError::integrity( + "constant eager direct factor has a dynamic source", + )); + } + 1.0 + }; + factor_re[index] = spec.factor.re * scale; + factor_im[index] = spec.factor.im * scale; + } + Ok(()) +} + +fn execute_schedule( + schedule: &DirectSchedule, + workspace: &mut EagerDirectTableWorkspace, + closure_scratch: &mut [EagerComplex64], + point_count: u32, +) -> RusticolResult<()> { + for stage in &schedule.stages { + execute_calls(&stage.invocation_calls, workspace, point_count)?; + execute_copies(&stage.unpropagated_copies, workspace, point_count); + execute_calls(&stage.finalization_calls, workspace, point_count)?; + } + execute_calls(&schedule.closure_calls, workspace, point_count)?; + execute_direct_coefficient_closures( + &schedule.direct_closures, + workspace, + closure_scratch, + point_count as usize, + ); + Ok(()) +} + +fn execute_schedule_profiled( + schedule: &DirectSchedule, + workspace: &mut EagerDirectTableWorkspace, + closure_scratch: &mut [EagerComplex64], + point_count: u32, + profile: &mut super::EagerExecutionProfile, +) -> RusticolResult<()> { + for stage in &schedule.stages { + let phase = Instant::now(); + execute_calls(&stage.invocation_calls, workspace, point_count)?; + profile.kernel_call += phase.elapsed(); + profile.backend_call_count = profile + .backend_call_count + .saturating_add(stage.invocation_calls.len() as u64); + + let phase = Instant::now(); + execute_copies(&stage.unpropagated_copies, workspace, point_count); + profile.invocation_scatter += phase.elapsed(); + + let phase = Instant::now(); + execute_calls(&stage.finalization_calls, workspace, point_count)?; + profile.finalization += phase.elapsed(); + profile.backend_call_count = profile + .backend_call_count + .saturating_add(stage.finalization_calls.len() as u64); + } + let phase = Instant::now(); + execute_calls(&schedule.closure_calls, workspace, point_count)?; + execute_direct_coefficient_closures( + &schedule.direct_closures, + workspace, + closure_scratch, + point_count as usize, + ); + profile.closure += phase.elapsed(); + profile.backend_call_count = profile + .backend_call_count + .saturating_add(schedule.closure_calls.len() as u64); + Ok(()) +} + +fn execute_calls( + calls: &[DirectCall], + workspace: &mut EagerDirectTableWorkspace, + point_count: u32, +) -> RusticolResult<()> { + for call in calls { + // SAFETY: construction authenticated immutable rows, callable + // descriptors, plane catalogs, aliases, and the maximum point range. + unsafe { + call.application + .evaluate_validated_unchecked(&call.rows, workspace, 0, point_count)?; + } + } + Ok(()) +} + +fn execute_copies( + copies: &[DirectCopy], + workspace: &mut EagerDirectTableWorkspace, + point_count: u32, +) { + let stride = workspace.arena().point_stride() as usize; + let points = point_count as usize; + let (real, imaginary, _, _) = workspace.split_arena_slices_mut(); + for copy in copies { + for component in 0..copy.component_count as usize { + let source = (copy.source_component_base as usize + component) * stride; + let target = (copy.destination_component_base as usize + component) * stride; + for point in 0..points { + real[target + point] = real[source + point]; + imaginary[target + point] = imaginary[source + point]; + } + } + } +} + +fn execute_direct_coefficient_closures( + closures: &[DirectCoefficientClosure], + workspace: &mut EagerDirectTableWorkspace, + contraction_scratch: &mut [EagerComplex64], + point_count: usize, +) { + let stride = workspace.arena().point_stride() as usize; + debug_assert!(contraction_scratch.len() >= stride); + let (current_re, current_im, amplitude_re, amplitude_im) = workspace.split_arena_slices_mut(); + for closure in closures { + let target = closure.amplitude_index as usize * stride; + contraction_scratch[..point_count].fill(EagerComplex64::new(0.0, 0.0)); + // Component-outer, point-inner traversal keeps both source planes + // sequential. Each point still observes components in the original + // order, preserving its contraction summation semantics exactly. + for component in 0..closure.component_count as usize { + for (point, contraction) in contraction_scratch[..point_count].iter_mut().enumerate() { + let left = (closure.left_component_base as usize + component) * stride + point; + let right = (closure.right_component_base as usize + component) * stride + point; + *contraction += closure.coefficients[component] + * EagerComplex64::new(current_re[left], current_im[left]) + * EagerComplex64::new(current_re[right], current_im[right]); + } + } + for (point, contraction) in contraction_scratch[..point_count] + .iter() + .copied() + .enumerate() + { + let mut value = + EagerComplex64::new(amplitude_re[target + point], amplitude_im[target + point]); + if closure.initializes_amplitude { + closure.factor.assign(&mut value, contraction); + } else { + closure.factor.accumulate(&mut value, contraction); + } + amplitude_re[target + point] = value.re; + amplitude_im[target + point] = value.im; + } + } +} + +fn reduce_full_tile( + plan: &EagerExecutionPlan, + workspace: &EagerDirectTableWorkspace, + groups: &mut [EagerComplex64], + reduced: &mut [f64], + point_count: usize, +) { + let stride = workspace.arena().point_stride() as usize; + let (amplitude_re, amplitude_im) = workspace.arena().amplitude_slices(); + for (group_index, group) in plan.reduction_groups.iter().enumerate() { + let target = group_index * stride; + let (first, remaining) = group + .amplitude_indices + .split_first() + .expect("validated eager reduction group"); + let first_source = *first as usize * stride; + for point in 0..point_count { + groups[target + point] = EagerComplex64::new( + amplitude_re[first_source + point], + amplitude_im[first_source + point], + ); + } + for amplitude in remaining { + let source = *amplitude as usize * stride; + for point in 0..point_count { + groups[target + point] += + EagerComplex64::new(amplitude_re[source + point], amplitude_im[source + point]); + } + } + } + let (first, remaining) = plan + .reduction_entries + .split_first() + .expect("validated eager reduction entries"); + let left = first.left_group_index as usize * stride; + let right = first.right_group_index as usize * stride; + for point in 0..point_count { + reduced[point] = + (first.coefficient * groups[left + point] * groups[right + point].conj()).re; + } + for entry in remaining { + let left = entry.left_group_index as usize * stride; + let right = entry.right_group_index as usize * stride; + for point in 0..point_count { + reduced[point] += + (entry.coefficient * groups[left + point] * groups[right + point].conj()).re; + } + } +} + +#[allow(clippy::too_many_arguments)] +fn reduce_selected_indexed_tile( + plan: &EagerExecutionPlan, + schedule: &DirectSchedule, + workspace: &EagerDirectTableWorkspace, + groups: &mut [EagerComplex64], + reduced: &mut [f64], + group_offsets: &[usize], + selected_groups: &[u32], + selected_group_weights: &[f64], + point_indices: &[usize], +) -> RusticolResult<()> { + let stride = workspace.arena().point_stride() as usize; + let (amplitude_re, amplitude_im) = workspace.arena().amplitude_slices(); + reduced[..point_indices.len()].fill(0.0); + for group_index in schedule.active_reduction_group_indices.iter().copied() { + let group = &plan.reduction_groups[group_index]; + let target = group_index * stride; + let (first, remaining) = group + .amplitude_indices + .split_first() + .expect("validated eager reduction group"); + let source = *first as usize * stride; + for point in 0..point_indices.len() { + groups[target + point] = + EagerComplex64::new(amplitude_re[source + point], amplitude_im[source + point]); + } + for amplitude in remaining { + let source = *amplitude as usize * stride; + for point in 0..point_indices.len() { + groups[target + point] += + EagerComplex64::new(amplitude_re[source + point], amplitude_im[source + point]); + } + } + } + for entry_index in schedule.active_reduction_entry_indices.iter().copied() { + let entry = &plan.reduction_entries[entry_index]; + let left_group = entry.left_group_index as usize; + let right_group = entry.right_group_index as usize; + let left = left_group * stride; + let right = right_group * stride; + let left_position = schedule + .active_reduction_group_indices + .binary_search(&left_group) + .map_err(|_| RusticolError::internal("eager direct reduction lost left group"))?; + let right_position = schedule + .active_reduction_group_indices + .binary_search(&right_group) + .map_err(|_| RusticolError::internal("eager direct reduction lost right group"))?; + let left_id = schedule.active_reduction_group_ids[left_position]; + let right_id = schedule.active_reduction_group_ids[right_position]; + for (tile_point, original_point) in point_indices.iter().copied().enumerate() { + let selected = + &selected_groups[group_offsets[original_point]..group_offsets[original_point + 1]]; + let left_weight = selected + .binary_search(&left_id) + .ok() + .map(|position| selected_group_weights[group_offsets[original_point] + position]) + .ok_or_else(|| RusticolError::integrity("eager direct left weight is absent"))?; + let right_weight = selected + .binary_search(&right_id) + .ok() + .map(|position| selected_group_weights[group_offsets[original_point] + position]) + .ok_or_else(|| RusticolError::integrity("eager direct right weight is absent"))?; + if left_weight.to_bits() != right_weight.to_bits() { + return Err(RusticolError::integrity( + "eager direct selected contraction weights disagree", + )); + } + reduced[tile_point] += left_weight + * (entry.coefficient + * groups[left + tile_point] + * groups[right + tile_point].conj()) + .re; + } + } + Ok(()) +} + +fn copy_amplitude_tile( + workspace: &EagerDirectTableWorkspace, + amplitudes: std::ops::Range, + point_count: usize, + tile_start: usize, + tile_points: usize, + output: &mut [EagerComplex64], +) { + let stride = workspace.arena().point_stride() as usize; + let (real, imaginary) = workspace.arena().amplitude_slices(); + for amplitude in amplitudes { + let source = amplitude * stride; + let target = amplitude * point_count + tile_start; + for point in 0..tile_points { + output[target + point] = + EagerComplex64::new(real[source + point], imaginary[source + point]); + } + } +} + +fn copy_selected_amplitude_tile( + workspace: &EagerDirectTableWorkspace, + amplitude_indices: &[usize], + point_count: usize, + tile_start: usize, + tile_points: usize, + output: &mut [EagerComplex64], +) { + let stride = workspace.arena().point_stride() as usize; + let (real, imaginary) = workspace.arena().amplitude_slices(); + for amplitude in amplitude_indices { + let source = *amplitude * stride; + let target = *amplitude * point_count + tile_start; + for point in 0..tile_points { + output[target + point] = + EagerComplex64::new(real[source + point], imaginary[source + point]); + } + } +} + +#[allow(clippy::too_many_arguments)] +fn validate_io( + plan: &EagerExecutionPlan, + point_count: usize, + initial_values: &[EagerComplex64], + momenta: &[f64], + model_parameters: &[EagerComplex64], + amplitudes: Option<&[EagerComplex64]>, + reduced: Option<&[f64]>, +) -> RusticolResult<()> { + if point_count == 0 { + return Err(invalid( + "eager direct evaluation requires at least one point", + )); + } + let points = count_u32(point_count, "point")?; + validate_component_buffer( + "initial values", + initial_values.len(), + plan.values.component_count, + points, + )?; + validate_component_buffer( + "momenta", + momenta.len(), + plan.momenta.component_count, + points, + )?; + if model_parameters.len() != plan.parameter_count + || model_parameters + .iter() + .any(|value| !value.re.is_finite() || !value.im.is_finite()) + { + return Err(invalid( + "eager direct model parameters have an invalid shape or value", + )); + } + if let Some(amplitudes) = amplitudes { + let expected = plan + .amplitude_count + .checked_mul(point_count) + .ok_or_else(|| invalid("eager direct amplitude length overflows"))?; + if amplitudes.len() != expected { + return Err(invalid(format!( + "eager direct amplitudes have length {}, expected {expected}", + amplitudes.len() + ))); + } + } + if reduced.is_some_and(|values| values.len() != point_count) { + return Err(invalid("eager direct reduced output has the wrong length")); + } + Ok(()) +} + +fn validate_point_selectors( + plan: &EagerExecutionPlan, + offsets: &[usize], + groups: &[u32], + weights: &[f64], + point_count: usize, +) -> RusticolResult<()> { + if offsets.len() != point_count + 1 + || offsets.first() != Some(&0) + || offsets.last() != Some(&groups.len()) + || weights.len() != groups.len() + || offsets.windows(2).any(|pair| pair[0] > pair[1]) + { + return Err(invalid("eager direct per-point selector shape is invalid")); + } + let known = plan.selector_domains.as_ref().ok_or_else(|| { + RusticolError::compatibility( + "eager direct per-point selectors require selector-domain metadata", + ) + })?; + for point in 0..point_count { + let selected = &groups[offsets[point]..offsets[point + 1]]; + if selected.windows(2).any(|pair| pair[0] >= pair[1]) { + return Err(invalid(format!( + "eager direct selector groups for point {point} are not sorted and unique" + ))); + } + if let Some(unknown) = selected + .iter() + .find(|group| known.group_ids.binary_search(group).is_err()) + { + return Err(invalid(format!( + "eager direct point {point} references unknown group {unknown}" + ))); + } + } + if weights.iter().any(|weight| !weight.is_finite()) { + return Err(invalid("eager direct selector weights must be finite")); + } + Ok(()) +} + +fn direct_tile_capacity( + requested: u32, + workspace_bytes: usize, + current_planes: u32, + amplitude_planes: u32, + reduction_groups: u32, + symjit_scratch_planes: usize, +) -> RusticolResult { + for candidate in (1..=requested).rev() { + let stride = crate::direct_arena::checked_aligned_point_stride(candidate)? as usize; + let scalar_planes = usize::try_from( + current_planes + .checked_add(amplitude_planes) + .and_then(|count| count.checked_mul(2)) + .ok_or_else(|| invalid("eager direct workspace planes overflow"))?, + ) + .map_err(|_| invalid("eager direct workspace plane count exceeds usize"))? + .checked_add(symjit_scratch_planes) + .ok_or_else(|| invalid("eager direct scratch plane count overflows"))?; + let bytes = scalar_planes + .checked_mul(stride) + .and_then(|count| count.checked_mul(size_of::())) + .and_then(|bytes| { + (reduction_groups as usize) + .checked_mul(stride) + .and_then(|count| count.checked_mul(size_of::())) + .and_then(|groups| bytes.checked_add(groups)) + }) + .and_then(|bytes| { + stride + .checked_mul(size_of::()) + .and_then(|reduced| bytes.checked_add(reduced)) + }) + .ok_or_else(|| invalid("eager direct workspace size overflows"))?; + // The target may reduce batching, but must not reject a process whose + // minimum aligned tile is larger. Fill the already allocated pitch. + if bytes <= workspace_bytes + || candidate <= (crate::direct_arena::DIRECT_ARENA_ALIGNMENT / size_of::()) as u32 + { + return Ok(candidate); + } + } + Err(invalid( + "eager Direct-Arena workspace cannot hold one active point", + )) +} + +fn direct_workspace_bytes( + workspace: &EagerDirectTableWorkspace, + reduction_complex_count: usize, + reduced_count: usize, + factor_count: usize, +) -> RusticolResult { + let symjit_workspace_bytes = workspace.symjit_workspace_bytes()?; + usize::try_from(workspace.arena().allocation_counters().requested_bytes) + .ok() + .and_then(|arena| { + reduction_complex_count + .checked_mul(size_of::()) + .and_then(|reduction| arena.checked_add(reduction)) + }) + .and_then(|bytes| { + reduced_count + .checked_mul(size_of::()) + .and_then(|reduced| bytes.checked_add(reduced)) + }) + .and_then(|bytes| { + factor_count + .checked_mul(2 * size_of::()) + .and_then(|factors| bytes.checked_add(factors)) + }) + .and_then(|bytes| bytes.checked_add(symjit_workspace_bytes)) + .ok_or_else(|| invalid("eager direct workspace byte accounting overflows")) +} + +#[cfg(all(test, feature = "f64-symjit", target_arch = "aarch64"))] +mod tests { + use std::path::PathBuf; + + use super::super::plan_v3_tests::Fixture; + use super::*; + use crate::engine::compile_symbolica_program_to_plane_application_bytes; + use crate::engine::count_allocations; + use crate::engine::symjit_eager_direct::eager_direct_descriptor_for_source_application_bytes; + + fn source_and_descriptor() -> (Vec, Vec) { + let bytes = compile_symbolica_program_to_plane_application_bytes( + "([('add', ('temp', 0), [('param', 0), ('param', 1)], 0), \ + ('assign', ('out', 0), ('temp', 0))], 3, [])", + 3, + 1, + 2, + true, + ) + .expect("compile eager whole-plan P-kernel"); + let descriptor = eager_direct_descriptor_for_source_application_bytes( + &bytes, + 3, + 1, + std::path::Path::new("eager-whole-plan-test.symjit"), + ) + .expect("encode table descriptor"); + (bytes, descriptor) + } + + fn runtime(fixture: &Fixture, source: &[u8], descriptor: &[u8]) -> EagerDirectExecutionRuntime { + runtime_with_point_tile(fixture, source, descriptor, 128) + } + + fn runtime_with_point_tile( + fixture: &Fixture, + source: &[u8], + descriptor: &[u8], + point_tile_size: usize, + ) -> EagerDirectExecutionRuntime { + let inputs = [ + EagerKernelInput::FirstCurrentComponent(0), + EagerKernelInput::SecondCurrentComponent(0), + EagerKernelInput::CouplingReal, + ]; + let prepared = [ + EagerDirectPreparedKernel { + kernel_id: 10, + role: EagerKernelRole::Vertex, + inputs: &inputs, + output_component_count: 1, + application: EagerDirectPreparedApplication::Symjit { + source_application: source, + descriptor, + expected_optimization_level: 2, + expected_compression: true, + }, + display_path: PathBuf::from("whole-plan-vertex.symjit"), + }, + EagerDirectPreparedKernel { + kernel_id: 11, + role: EagerKernelRole::Closure, + inputs: &inputs, + output_component_count: 1, + application: EagerDirectPreparedApplication::Symjit { + source_application: source, + descriptor, + expected_optimization_level: 2, + expected_compression: true, + }, + display_path: PathBuf::from("whole-plan-closure.symjit"), + }, + ]; + EagerDirectExecutionRuntime::from_plan_v3_sections( + fixture.sections(), + &prepared, + EagerRuntimeOptions { + point_tile_size, + workspace_bytes: 16 * 1024 * 1024, + }, + ) + .expect("construct eager whole-plan Direct-Arena runtime") + } + + fn inputs(points: usize) -> (Vec, Vec) { + let mut values = vec![EagerComplex64::new(-91.0, 37.0); 3 * points]; + for point in 0..points { + values[point] = EagerComplex64::new(1.0 + point as f64 / 8.0, -0.25 * point as f64); + values[points + point] = + EagerComplex64::new(3.0 + point as f64 / 4.0, 0.5 * point as f64); + } + let momenta = (0..12 * points).map(|index| index as f64 / 16.0).collect(); + (values, momenta) + } + + fn expected(values: &[EagerComplex64], points: usize, point: usize) -> (EagerComplex64, f64) { + let current = (values[point] + values[points + point]) * 15.0; + let amplitude = (current + values[point]) * 77.0; + let reduced = (EagerComplex64::new(221.0, 0.0) * amplitude * amplitude.conj()).re; + (amplitude, reduced) + } + + fn assert_close(actual: f64, expected: f64, label: &str) { + let tolerance = 1.0e-15 + 1.0e-12 * actual.abs().max(expected.abs()); + assert!( + (actual - expected).abs() <= tolerance, + "{label}: {actual:.17e} != {expected:.17e} (tolerance {tolerance:.3e})" + ); + } + + #[test] + fn whole_plan_preserves_all_required_tile_tails_and_empty_point_selectors() { + let fixture = Fixture::new(); + let (source, descriptor) = source_and_descriptor(); + let mut direct = runtime(&fixture, &source, &descriptor); + assert_eq!(direct.effective_point_tile_size(), 64); + + for points in [1_usize, 7, 63, 64, 65, 127, 128, 129, 1023, 1024, 1025] { + let (values, momenta) = inputs(points); + let mut amplitudes = vec![EagerComplex64::new(-13.0, 29.0); points]; + let mut reduced = vec![-1.0; points]; + direct + .evaluate_into( + points, + &values, + &momenta, + &[], + &mut amplitudes, + &mut reduced, + ) + .expect("evaluate eager whole-plan tail"); + let mut totals_only = vec![-2.0; points]; + direct + .evaluate_totals_into(points, &values, &momenta, &[], &mut totals_only) + .expect("evaluate eager totals-only tail"); + for point in 0..points { + let (expected_amplitude, expected_reduced) = expected(&values, points, point); + assert_close( + amplitudes[point].re, + expected_amplitude.re, + "amplitude real", + ); + assert_close( + amplitudes[point].im, + expected_amplitude.im, + "amplitude imaginary", + ); + assert_close(reduced[point], expected_reduced, "reduced total"); + assert_eq!( + totals_only[point].to_bits(), + reduced[point].to_bits(), + "totals-only reduction order drifted at point {point}" + ); + } + } + + let points = 129; + let (values, momenta) = inputs(points); + let mut full_amplitudes = vec![EagerComplex64::new(0.0, 0.0); points]; + let mut full_reduced = vec![0.0; points]; + direct + .evaluate_into( + points, + &values, + &momenta, + &[], + &mut full_amplitudes, + &mut full_reduced, + ) + .expect("evaluate full eager selector reference"); + let profiled = direct + .evaluate_profile_into( + points, + &values, + &momenta, + &[], + &mut full_amplitudes, + &mut full_reduced, + ) + .expect("profile full eager selector reference"); + assert!( + profiled.internal_scratch_bytes > 0, + "profile must expose eager P-kernel scratch traffic separately" + ); + + let mut selected_amplitudes = vec![EagerComplex64::new(-13.0, 29.0); points]; + direct + .evaluate_selected_active_amplitudes_into( + &[7], + points, + &values, + &momenta, + &[], + &mut selected_amplitudes, + ) + .expect("evaluate selected eager amplitudes"); + for (selected, full) in selected_amplitudes.iter().zip(&full_amplitudes) { + assert_close(selected.re, full.re, "selected amplitude real"); + assert_close(selected.im, full.im, "selected amplitude imaginary"); + } + + let mut offsets = Vec::with_capacity(points + 1); + let mut groups = Vec::with_capacity(points / 2); + let mut weights = Vec::with_capacity(points / 2); + offsets.push(0); + for point in 0..points { + if point.is_multiple_of(2) { + groups.push(7); + weights.push(1.0); + } + offsets.push(groups.len()); + } + let mut point_selected = vec![-1.0; points]; + direct + .evaluate_point_selected_group_sets_into( + &offsets, + &groups, + &weights, + points, + &values, + &momenta, + &[], + &mut point_selected, + ) + .expect("evaluate mixed empty/non-empty eager point selectors"); + for point in 0..points { + let expected = if point.is_multiple_of(2) { + full_reduced[point] + } else { + 0.0 + }; + assert_close(point_selected[point], expected, "point-selected total"); + } + } + + #[test] + fn whole_plan_preserves_requested_tiles_below_the_locality_cap() { + let fixture = Fixture::new(); + let (source, descriptor) = source_and_descriptor(); + let direct = runtime_with_point_tile(&fixture, &source, &descriptor, 32); + assert_eq!(direct.effective_point_tile_size(), 32); + } + + #[test] + fn broadcast_planes_refresh_once_and_only_for_changed_parameters() { + let mut fixture = Fixture::new(); + fixture.prepared_parameter_count = 1; + let (source, descriptor) = source_and_descriptor(); + let mut direct = runtime(&fixture, &source, &descriptor); + let points = 7; + let (values, momenta) = inputs(points); + let mut amplitudes = vec![EagerComplex64::new(0.0, 0.0); points]; + let mut reduced = vec![0.0; points]; + let parameter = [EagerComplex64::new(2.0, -3.0)]; + + let first = direct + .evaluate_profile_into( + points, + &values, + &momenta, + ¶meter, + &mut amplitudes, + &mut reduced, + ) + .expect("initialize eager broadcast planes"); + assert!(direct.elide_structural_zero_fills); + let zero_plane = direct + .shared_zero_plane_index + .expect("SymJIT eager execution must reserve a shared-zero plane"); + assert_eq!(zero_plane % 2, 1); + let zero_component = (zero_plane / 2) as usize; + let point_stride = direct.workspace.arena().point_stride() as usize; + let (_, current_imaginary) = direct.workspace.arena().current_slices(); + assert!( + current_imaginary[zero_component * point_stride..(zero_component + 1) * point_stride] + .iter() + .all(|value| *value == 0.0), + "shared-zero plane changed during eager execution" + ); + let stride = direct.effective_point_tile_size() as u64; + assert_eq!( + first.internal_broadcast_bytes, + 4 * stride * std::mem::size_of::() as u64, + "initial refresh must fill the two real coupling planes and the split-complex parameter" + ); + for point in 0..points { + let (expected_amplitude, expected_reduced) = expected(&values, points, point); + assert_close( + amplitudes[point].re, + expected_amplitude.re, + "parameter-cache amplitude real", + ); + assert_close( + amplitudes[point].im, + expected_amplitude.im, + "parameter-cache amplitude imaginary", + ); + assert_close( + reduced[point], + expected_reduced, + "parameter-cache reduced total", + ); + } + + let unchanged = direct + .evaluate_profile_into( + points, + &values, + &momenta, + ¶meter, + &mut amplitudes, + &mut reduced, + ) + .expect("reuse eager broadcast planes"); + assert_eq!(unchanged.internal_broadcast_bytes, 0); + + let changed_parameter = [EagerComplex64::new(5.0, -3.0)]; + let changed = direct + .evaluate_profile_into( + points, + &values, + &momenta, + &changed_parameter, + &mut amplitudes, + &mut reduced, + ) + .expect("refresh changed eager parameter plane"); + assert_eq!( + changed.internal_broadcast_bytes, + 2 * stride * std::mem::size_of::() as u64, + "a changed unused parameter must not rewrite stationary coupling planes" + ); + + let (result, allocations, bytes) = count_allocations(|| { + direct.evaluate_into( + points, + &values, + &momenta, + &changed_parameter, + &mut amplitudes, + &mut reduced, + ) + }); + result.expect("repeat eager evaluation with cached parameter"); + assert_eq!( + (allocations, bytes), + (0, 0), + "cached eager broadcast execution allocated" + ); + } + + #[test] + fn warmed_whole_plan_and_selector_caches_allocate_zero() { + let fixture = Fixture::new(); + let (source, descriptor) = source_and_descriptor(); + let mut direct = runtime(&fixture, &source, &descriptor); + let points = 129; + let (values, momenta) = inputs(points); + let mut amplitudes = vec![EagerComplex64::new(0.0, 0.0); points]; + let mut reduced = vec![0.0; points]; + direct + .evaluate_into( + points, + &values, + &momenta, + &[], + &mut amplitudes, + &mut reduced, + ) + .expect("warm whole-plan execution"); + let (result, allocations, bytes) = count_allocations(|| { + direct.evaluate_into( + points, + &values, + &momenta, + &[], + &mut amplitudes, + &mut reduced, + ) + }); + result.expect("repeat whole-plan execution"); + assert_eq!( + (allocations, bytes), + (0, 0), + "warmed whole-plan execution allocated" + ); + + direct + .evaluate_totals_into(points, &values, &momenta, &[], &mut reduced) + .expect("warm totals-only execution"); + let (result, allocations, bytes) = count_allocations(|| { + direct.evaluate_totals_into(points, &values, &momenta, &[], &mut reduced) + }); + result.expect("repeat totals-only execution"); + assert_eq!( + (allocations, bytes), + (0, 0), + "warmed totals-only execution allocated" + ); + + direct + .evaluate_selected_active_amplitudes_into( + &[7], + points, + &values, + &momenta, + &[], + &mut amplitudes, + ) + .expect("warm global selector schedule"); + let (result, allocations, bytes) = count_allocations(|| { + direct.evaluate_selected_active_amplitudes_into( + &[7], + points, + &values, + &momenta, + &[], + &mut amplitudes, + ) + }); + result.expect("repeat global selector schedule"); + assert_eq!( + (allocations, bytes), + (0, 0), + "warmed global selector execution allocated" + ); + + let mut offsets = Vec::with_capacity(points + 1); + let mut groups = Vec::with_capacity(points); + let mut weights = Vec::with_capacity(points); + offsets.push(0); + for point in 0..points { + if !point.is_multiple_of(3) { + groups.push(7); + weights.push(1.0); + } + offsets.push(groups.len()); + } + direct + .evaluate_point_selected_group_sets_into( + &offsets, + &groups, + &weights, + points, + &values, + &momenta, + &[], + &mut reduced, + ) + .expect("warm per-point selector schedules"); + let (result, allocations, bytes) = count_allocations(|| { + direct.evaluate_point_selected_group_sets_into( + &offsets, + &groups, + &weights, + points, + &values, + &momenta, + &[], + &mut reduced, + ) + }); + result.expect("repeat per-point selector schedules"); + assert_eq!( + (allocations, bytes), + (0, 0), + "warmed per-point selector execution allocated" + ); + } +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/eager_runtime/direct_invocation_arena.rs b/pyamplicol/rust/crates/rusticol-core/src/eager_runtime/direct_invocation_arena.rs new file mode 100644 index 0000000000..bab98a4229 --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/eager_runtime/direct_invocation_arena.rs @@ -0,0 +1,2364 @@ +// SPDX-License-Identifier: 0BSD + +//! Developer-gated eager invocation/fanout Direct-Arena scheduler. +//! +//! This first scheduler slice deliberately stops at the stage boundary: +//! finalization and closure remain independent eager events. Unlike the +//! adapter-only ABI tests, this module consumes authenticated plan-v3 rows, +//! derives their event lifetimes, allocates physical planes with the shared +//! allocator, preserves selector pruning and invocation/fanout order, and +//! executes real prepared SymJIT applications through immutable binding-v2 +//! tables. + +#![allow(dead_code)] + +use std::collections::{BTreeMap, BTreeSet}; +use std::path::PathBuf; +use std::sync::Arc; + +use crate::artifact::PinnedNativeLibrary; +use crate::direct_arena::{ + DirectArenaInterval, DirectArenaLayout, DirectArenaTrafficCounters, assign_direct_arena, +}; +use crate::engine::symjit_eager_direct::{ + EAGER_DIRECT_SOURCE_APPLICATION_ABI, EAGER_DIRECT_TABLE_BINDING_ABI, + EAGER_DIRECT_TABLE_DESCRIPTOR_ABI, EagerDirectArenaPlaneBinding, EagerDirectTableRows, + EagerDirectTableWorkspace, LoadedSymjitEagerDirectTable, +}; +use crate::{ + EAGER_OUTPUT_FACTOR_COUPLING_IMAG, EAGER_OUTPUT_FACTOR_COUPLING_REAL, EAGER_OUTPUT_FACTOR_NONE, + MISSING_U32, RusticolError, RusticolResult, +}; + +use super::plan::{ComponentRange, EagerExecutionPlan, ScheduledAttachment, ScheduledInvocation}; +use super::{ + EagerComplex64, EagerKernelInput, EagerKernelRole, EagerKernelSpec, EagerPlanV3Sections, +}; + +/// One prepared source application and its portable eager table descriptor. +pub(crate) enum EagerDirectPreparedApplication<'a> { + Symjit { + source_application: &'a [u8], + descriptor: &'a [u8], + expected_optimization_level: u32, + expected_compression: bool, + }, + Native { + library: &'a Arc, + function_name: &'a str, + source_application_abi: &'a str, + invocation_stride: u32, + attachment_stride: u32, + target_triple: &'a str, + evaluator_state_sha256: &'a str, + simd_lane_width: u32, + }, +} + +pub(crate) struct EagerDirectPreparedKernel<'a> { + pub kernel_id: u32, + pub role: EagerKernelRole, + pub inputs: &'a [EagerKernelInput], + pub output_component_count: u32, + pub application: EagerDirectPreparedApplication<'a>, + pub display_path: PathBuf, +} + +struct EagerDirectStageCall { + application: LoadedSymjitEagerDirectTable, + rows: EagerDirectTableRows, +} + +#[derive(Clone, Copy)] +struct FactorSpec { + factor: EagerComplex64, + coupling_slot_id: u32, + output_factor_source: u32, +} + +#[derive(Clone, Copy)] +pub(super) struct SemanticCatalog { + value_start: u32, + current_start: u32, + momentum_start: u32, + coupling_real_start: u32, + coupling_imag_start: u32, + parameter_start: u32, + semantic_count: u32, +} + +impl SemanticCatalog { + pub(super) fn new(sections: EagerPlanV3Sections<'_>) -> RusticolResult { + let value_count = count_u32(sections.values.len(), "value slots")?; + let current_count = count_u32(sections.currents.len(), "currents")?; + let momentum_count = count_u32(sections.momenta.len(), "momentum slots")?; + let coupling_count = count_u32(sections.couplings.len(), "couplings")?; + let parameter_count = sections.prepared_parameter_count; + let value_start = 0; + let current_start = value_count; + let momentum_start = current_start + .checked_add(current_count) + .ok_or_else(|| invalid("eager direct semantic current range overflows"))?; + let coupling_real_start = momentum_start + .checked_add(momentum_count) + .ok_or_else(|| invalid("eager direct semantic momentum range overflows"))?; + let coupling_imag_start = coupling_real_start + .checked_add(coupling_count) + .ok_or_else(|| invalid("eager direct semantic coupling range overflows"))?; + let parameter_start = coupling_imag_start + .checked_add(coupling_count) + .ok_or_else(|| invalid("eager direct semantic coupling range overflows"))?; + let semantic_count = parameter_start + .checked_add(parameter_count) + .ok_or_else(|| invalid("eager direct semantic parameter range overflows"))?; + Ok(Self { + value_start, + current_start, + momentum_start, + coupling_real_start, + coupling_imag_start, + parameter_start, + semantic_count, + }) + } + + pub(super) fn value(self, id: u32) -> RusticolResult { + checked_semantic(self.value_start, id, self.current_start, "value") + } + + pub(super) fn current(self, id: u32) -> RusticolResult { + checked_semantic(self.current_start, id, self.momentum_start, "current") + } + + pub(super) fn momentum(self, id: u32) -> RusticolResult { + checked_semantic( + self.momentum_start, + id, + self.coupling_real_start, + "momentum", + ) + } + + pub(super) fn coupling_real(self, id: u32) -> RusticolResult { + checked_semantic( + self.coupling_real_start, + id, + self.coupling_imag_start, + "coupling-real", + ) + } + + pub(super) fn coupling_imag(self, id: u32) -> RusticolResult { + checked_semantic( + self.coupling_imag_start, + id, + self.parameter_start, + "coupling-imag", + ) + } + + pub(super) fn parameter(self, id: u32) -> RusticolResult { + checked_semantic(self.parameter_start, id, self.semantic_count, "parameter") + } +} + +/// Real plan-v3 eager invocation/fanout prototype. +/// +/// The production cutover will extend this envelope with finalization, +/// closure, and reduction event executors. Keeping this type separate from +/// [`super::EagerExecutionRuntime`] makes the temporary developer dual-run +/// explicit and prevents a packet fallback from being mistaken for a direct +/// lane. +pub(crate) struct EagerDirectInvocationPrototype { + plan: EagerExecutionPlan, + catalog: SemanticCatalog, + layout: DirectArenaLayout, + workspace: EagerDirectTableWorkspace, + calls: Box<[EagerDirectStageCall]>, + factor_specs: Box<[FactorSpec]>, + initial_value_slots: Box<[u32]>, + value_ranges: Box<[ComponentRange]>, + momentum_ranges: Box<[ComponentRange]>, + traffic: DirectArenaTrafficCounters, +} + +impl EagerDirectInvocationPrototype { + /// Construct one selector-specialized direct invocation schedule. + /// + /// `active_groups=None` retains every physical group. A sorted, unique + /// slice (including the empty slice) produces the same dependency-pruned + /// invocation and attachment order as the existing eager selector lane. + pub(crate) fn from_plan_v3_sections( + sections: EagerPlanV3Sections<'_>, + prepared: &[EagerDirectPreparedKernel<'_>], + active_groups: Option<&[u32]>, + tile_capacity: u32, + ) -> RusticolResult { + if tile_capacity == 0 { + return Err(invalid( + "eager direct invocation tile capacity must be positive", + )); + } + let plan = EagerExecutionPlan::from_plan_v3_sections(sections)?; + if plan.stages.len() != 1 { + return Err(RusticolError::compatibility(format!( + "the eager direct invocation prototype currently accepts one stage, found {}; \ + finalization and closure remain separate developer-oracle events", + plan.stages.len() + ))); + } + Self::from_validated_plan_stage( + sections, + plan, + prepared, + active_groups, + tile_capacity, + 0, + None, + ) + } + + #[allow(clippy::too_many_arguments)] + fn from_validated_plan_stage( + sections: EagerPlanV3Sections<'_>, + plan: EagerExecutionPlan, + prepared: &[EagerDirectPreparedKernel<'_>], + active_groups: Option<&[u32]>, + tile_capacity: u32, + stage_position: usize, + kernel_filter: Option, + ) -> RusticolResult { + validate_active_groups(&plan, active_groups)?; + let catalog = SemanticCatalog::new(sections)?; + let layout = derive_event_layout(sections, catalog, &plan)?; + let value_ranges = (0..sections.values.len()) + .map(|id| plan.values.get(id as u32, "eager direct value")) + .collect::>>()? + .into_boxed_slice(); + let momentum_ranges = (0..sections.momenta.len()) + .map(|id| plan.momenta.get(id as u32, "eager direct momentum")) + .collect::>>()? + .into_boxed_slice(); + let mut produced_values = vec![false; sections.values.len()]; + for row in sections.finalizations { + for value in [row.unpropagated_value_slot_id, row.propagated_value_slot_id] { + if value != MISSING_U32 { + produced_values[value as usize] = true; + } + } + } + let initial_value_slots = produced_values + .iter() + .enumerate() + .filter_map(|(id, produced)| (!produced).then_some(id as u32)) + .collect::>() + .into_boxed_slice(); + + let mut plane_bindings = Vec::new(); + plane_bindings + .try_reserve_exact(layout.component_count() as usize * 2) + .map_err(|error| { + RusticolError::internal(format!( + "could not reserve eager direct plane bindings: {error}" + )) + })?; + for component in 0..layout.component_count() { + plane_bindings.push(EagerDirectArenaPlaneBinding::CurrentReal(component)); + plane_bindings.push(EagerDirectArenaPlaneBinding::CurrentImag(component)); + } + + let stage = plan.stages.get(stage_position).ok_or_else(|| { + RusticolError::invalid_argument(format!( + "eager direct stage position {stage_position} is out of bounds" + )) + })?; + let mut applications = BTreeMap::new(); + for artifact in prepared { + if applications.contains_key(&artifact.kernel_id) { + return Err(RusticolError::integrity(format!( + "eager direct prepared catalog repeats kernel {}", + artifact.kernel_id + ))); + } + let kernel = plan.kernels.get(&artifact.kernel_id).ok_or_else(|| { + RusticolError::integrity(format!( + "eager direct prepared catalog references unknown kernel {}", + artifact.kernel_id + )) + })?; + validate_callable_identity(kernel, artifact)?; + let application = match &artifact.application { + EagerDirectPreparedApplication::Symjit { + source_application, + descriptor, + expected_optimization_level, + expected_compression, + } => LoadedSymjitEagerDirectTable::load_prepared_application_bytes( + source_application, + descriptor, + artifact.display_path.clone(), + EAGER_DIRECT_SOURCE_APPLICATION_ABI, + EAGER_DIRECT_TABLE_DESCRIPTOR_ABI, + EAGER_DIRECT_TABLE_BINDING_ABI, + *expected_optimization_level, + *expected_compression, + )?, + EagerDirectPreparedApplication::Native { + library, + function_name, + source_application_abi, + invocation_stride, + attachment_stride, + target_triple, + evaluator_state_sha256, + simd_lane_width, + } => LoadedSymjitEagerDirectTable::load_native_application( + Arc::clone(library), + function_name, + artifact.display_path.clone(), + source_application_abi, + *invocation_stride, + *attachment_stride, + count_u32(artifact.inputs.len(), "native eager input width")?, + artifact.output_component_count, + target_triple, + evaluator_state_sha256, + *simd_lane_width, + )?, + }; + applications.insert(artifact.kernel_id, application); + } + + let mut calls = Vec::new(); + let mut factor_specs = Vec::new(); + // Selector specialization changes which contribution is the first + // retained write to a current. Recompute overwrite/add semantics in + // exactly the retained, stable execution order rather than inheriting + // flags from the full schedule. + let mut selected_written_currents = BTreeSet::new(); + let mut cursor = 0usize; + while cursor < stage.invocations.len() { + let kernel_id = stage.invocations[cursor].row.kernel_id; + let run_stop = stage.invocations[cursor..] + .iter() + .position(|row| row.row.kernel_id != kernel_id) + .map_or(stage.invocations.len(), |offset| cursor + offset); + if kernel_filter.is_some_and(|selected| selected != kernel_id) { + cursor = run_stop; + continue; + } + let kernel = plan.kernels.get(&kernel_id).ok_or_else(|| { + RusticolError::internal(format!("eager direct schedule lost kernel {kernel_id}")) + })?; + let mut invocation_bytes = Vec::new(); + let mut attachment_bytes = Vec::new(); + let mut local_attachment_count = 0_u32; + for invocation in &stage.invocations[cursor..run_stop] { + if !row_active(&plan, invocation.selector_domain_id, active_groups)? { + continue; + } + let active_attachment_count = stage.attachments + [invocation.attachment_range.clone()] + .iter() + .map(|attachment| row_active(&plan, attachment.selector_domain_id, active_groups)) + .collect::>>()? + .into_iter() + .filter(|active| *active) + .count(); + if active_attachment_count == 0 { + continue; + } + encode_invocation_inputs( + &mut invocation_bytes, + invocation, + &kernel.inputs, + catalog, + &layout, + )?; + push_u32(&mut invocation_bytes, local_attachment_count); + push_u32( + &mut invocation_bytes, + count_u32(active_attachment_count, "active attachments")?, + ); + for attachment in &stage.attachments[invocation.attachment_range.clone()] { + if !row_active(&plan, attachment.selector_domain_id, active_groups)? { + continue; + } + encode_attachment_destinations( + &mut attachment_bytes, + attachment, + kernel.output_component_count, + catalog, + &layout, + )?; + let factor_index = count_u32(factor_specs.len(), "direct factors")?; + push_u32(&mut attachment_bytes, factor_index); + push_u32( + &mut attachment_bytes, + retained_attachment_operation( + &mut selected_written_currents, + attachment.row.result_current_id, + ), + ); + factor_specs.push(FactorSpec { + factor: EagerComplex64::new( + attachment.row.factor_real, + attachment.row.factor_imag, + ), + coupling_slot_id: invocation.row.coupling_slot_id, + output_factor_source: invocation.row.output_factor_source, + }); + local_attachment_count = local_attachment_count + .checked_add(1) + .ok_or_else(|| invalid("eager direct attachment count overflows"))?; + } + } + if !invocation_bytes.is_empty() { + let application = applications.remove(&kernel_id).ok_or_else(|| { + RusticolError::compatibility(format!( + "eager direct invocation schedule has no prepared table for kernel {kernel_id}" + )) + })?; + let rows = application.load_rows(invocation_bytes, attachment_bytes)?; + calls.push(EagerDirectStageCall { application, rows }); + } + cursor = run_stop; + } + + let factor_len = factor_specs.len().max(1); + let mut workspace = EagerDirectTableWorkspace::new( + layout.component_count(), + count_u32(plan.amplitude_count, "amplitude planes")?, + tile_capacity, + &plane_bindings, + None, + Vec::new(), + vec![0.0; factor_len], + vec![0.0; factor_len], + )?; + workspace.begin_tile(tile_capacity)?; + for call in &calls { + call.application + .validate_call(&call.rows, &mut workspace, 0, tile_capacity)?; + } + + Ok(Self { + plan, + catalog, + layout, + workspace, + calls: calls.into_boxed_slice(), + factor_specs: factor_specs.into_boxed_slice(), + initial_value_slots, + value_ranges, + momentum_ranges, + traffic: DirectArenaTrafficCounters::default(), + }) + } + + /// Extract one invocation-only stage slice from a fully validated + /// multistage plan for the retained-artifact oracle. This test-only entry + /// point does not relax the production constructor: finalization, closure, + /// and whole-plan multistage execution remain fail-closed. + #[cfg(test)] + pub(crate) fn from_multistage_stage_for_test( + sections: EagerPlanV3Sections<'_>, + prepared: &[EagerDirectPreparedKernel<'_>], + active_groups: Option<&[u32]>, + tile_capacity: u32, + stage_position: usize, + kernel_id: u32, + ) -> RusticolResult { + if tile_capacity == 0 { + return Err(invalid( + "eager direct invocation tile capacity must be positive", + )); + } + let plan = EagerExecutionPlan::from_plan_v3_sections(sections)?; + if plan.stages.len() < 2 { + return Err(RusticolError::compatibility( + "the retained-artifact stage oracle requires a genuine multistage eager plan", + )); + } + Self::from_validated_plan_stage( + sections, + plan, + prepared, + active_groups, + tile_capacity, + stage_position, + Some(kernel_id), + ) + } + + pub(crate) fn layout(&self) -> &DirectArenaLayout { + &self.layout + } + + pub(crate) const fn traffic(&self) -> DirectArenaTrafficCounters { + self.traffic + } + + pub(crate) fn current_value( + &self, + current_id: u32, + component: u32, + point: u32, + ) -> RusticolResult { + if point >= self.workspace.arena().active_point_count() { + return Err(invalid( + "eager direct current point is outside the active tile", + )); + } + let semantic = self.catalog.current(current_id)?; + let physical = assigned_component(&self.layout, semantic, component)?; + let stride = self.workspace.arena().point_stride() as usize; + let index = physical as usize * stride + point as usize; + let (real, imaginary) = self.workspace.arena().current_slices(); + Ok(EagerComplex64::new(real[index], imaginary[index])) + } + + #[allow(clippy::too_many_arguments)] + pub(crate) fn evaluate_invocations( + &mut self, + point_count: u32, + initial_values: &[EagerComplex64], + momenta: &[f64], + model_parameters: &[EagerComplex64], + ) -> RusticolResult<()> { + if point_count == 0 || point_count > self.workspace.arena().tile_capacity() { + return Err(invalid(format!( + "eager direct invocation point count {point_count} is outside 1..={}", + self.workspace.arena().tile_capacity() + ))); + } + validate_component_buffer( + "initial values", + initial_values.len(), + self.plan.values.component_count, + point_count, + )?; + validate_component_buffer( + "momenta", + momenta.len(), + self.plan.momenta.component_count, + point_count, + )?; + if model_parameters.len() != self.plan.parameter_count { + return Err(invalid(format!( + "eager direct model parameter count is {}, expected {}", + model_parameters.len(), + self.plan.parameter_count + ))); + } + if model_parameters + .iter() + .any(|value| !value.re.is_finite() || !value.im.is_finite()) + { + return Err(invalid("eager direct model parameters must be finite")); + } + + self.workspace.begin_tile(point_count)?; + self.workspace + .clear_current_active(0, self.layout.component_count())?; + let stride = self.workspace.arena().point_stride() as usize; + let points = point_count as usize; + { + let (real, imaginary, _, _) = self.workspace.split_arena_slices_mut(); + for &slot in &self.initial_value_slots { + let range = self.value_ranges[slot as usize]; + let semantic = self.catalog.value(slot)?; + for component in 0..range.len { + let physical = assigned_component( + &self.layout, + semantic, + count_u32(component, "value component")?, + )? as usize; + let target = physical * stride; + let source = (range.start + component) * points; + for point in 0..points { + let value = initial_values[source + point]; + real[target + point] = value.re; + imaginary[target + point] = value.im; + } + } + } + for (slot, range) in self.momentum_ranges.iter().copied().enumerate() { + let semantic = self.catalog.momentum(slot as u32)?; + for component in 0..range.len { + let physical = assigned_component( + &self.layout, + semantic, + count_u32(component, "momentum component")?, + )? as usize; + let target = physical * stride; + let source = (range.start + component) * points; + for point in 0..points { + real[target + point] = momenta[source + point]; + imaginary[target + point] = 0.0; + } + } + } + for (coupling_id, row) in self.plan.couplings.iter().copied().enumerate() { + let coupling = resolve_coupling(row, model_parameters); + fill_real_input_plane( + real, + imaginary, + stride, + assigned_component( + &self.layout, + self.catalog.coupling_real(coupling_id as u32)?, + 0, + )?, + points, + coupling.re, + ); + fill_real_input_plane( + real, + imaginary, + stride, + assigned_component( + &self.layout, + self.catalog.coupling_imag(coupling_id as u32)?, + 0, + )?, + points, + coupling.im, + ); + } + for (parameter_id, value) in model_parameters.iter().copied().enumerate() { + let physical = assigned_component( + &self.layout, + self.catalog.parameter(parameter_id as u32)?, + 0, + )? as usize; + let target = physical * stride; + real[target..target + points].fill(value.re); + imaginary[target..target + points].fill(value.im); + } + } + { + let (factor_re, factor_im) = self.workspace.factors_mut(); + for (index, spec) in self.factor_specs.iter().copied().enumerate() { + let coupling = resolve_coupling( + self.plan.couplings[spec.coupling_slot_id as usize], + model_parameters, + ); + let scale = match spec.output_factor_source { + EAGER_OUTPUT_FACTOR_NONE => 1.0, + EAGER_OUTPUT_FACTOR_COUPLING_REAL => coupling.re, + EAGER_OUTPUT_FACTOR_COUPLING_IMAG => coupling.im, + _ => { + return Err(RusticolError::integrity( + "eager direct factor references an invalid output-factor source", + )); + } + }; + factor_re[index] = spec.factor.re * scale; + factor_im[index] = spec.factor.im * scale; + } + } + self.execute_preinitialized_invocations(point_count) + } + + /// Execute immutable table rows after the caller has initialized the + /// arena and factor catalog for the active tile. + fn execute_preinitialized_invocations(&mut self, point_count: u32) -> RusticolResult<()> { + for call in &self.calls { + self.traffic + .record_call(call.rows.invocation_count(), point_count); + // SAFETY: constructor validation authenticates immutable rows, + // descriptors, plane catalogs, and aliases. This method changes + // only plane/factor contents and the checked active tail. + unsafe { + call.application.evaluate_validated_unchecked( + &call.rows, + &mut self.workspace, + 0, + point_count, + )?; + } + } + self.traffic.validate_direct()?; + Ok(()) + } +} + +#[cfg(test)] +#[derive(Clone, Copy, Debug)] +pub(crate) struct EagerDirectRetainedStageCandidate { + pub(crate) stage_position: usize, + pub(crate) stage_index: u32, + pub(crate) kernel_id: u32, + pub(crate) selector_group_id: u32, + pub(crate) full_invocation_count: usize, + pub(crate) full_attachment_count: usize, + pub(crate) selected_invocation_count: usize, + pub(crate) selected_attachment_count: usize, + pub(crate) distinct_destination_count: usize, +} + +#[cfg(test)] +#[derive(Clone, Copy, Debug)] +pub(crate) struct EagerDirectRetainedStageVariantEvidence { + pub(crate) invocation_count: usize, + pub(crate) attachment_count: usize, + pub(crate) comparison_count: usize, + pub(crate) bitwise_comparison_count: usize, + pub(crate) maximum_absolute_error: f64, + pub(crate) maximum_relative_error: f64, + pub(crate) direct_median_ns: f64, + pub(crate) direct_mad_ns: f64, + pub(crate) packet_median_ns: f64, + pub(crate) packet_mad_ns: f64, + pub(crate) direct_over_packet: f64, + pub(crate) direct_call_traffic: DirectArenaTrafficCounters, + pub(crate) arena_initialization_write_bytes: u64, + pub(crate) factor_fill_write_bytes: u64, + pub(crate) packet_input_materialization_bytes_per_call: u64, + pub(crate) packet_output_materialization_bytes_per_call: u64, + pub(crate) packet_scatter_bytes_per_call: u64, +} + +#[cfg(test)] +#[derive(Clone, Copy, Debug)] +pub(crate) struct EagerDirectRetainedStageEvidence { + pub(crate) candidate: EagerDirectRetainedStageCandidate, + pub(crate) full: EagerDirectRetainedStageVariantEvidence, + pub(crate) selected: EagerDirectRetainedStageVariantEvidence, +} + +/// Pick a retained-artifact kernel run that proves both a genuine multi-row, +/// multi-destination table and nontrivial runtime selector pruning. +#[cfg(test)] +pub(crate) fn select_retained_multistage_candidate( + sections: EagerPlanV3Sections<'_>, +) -> RusticolResult { + let plan = EagerExecutionPlan::from_plan_v3_sections(sections)?; + if plan.stages.len() < 2 { + return Err(RusticolError::compatibility( + "the retained eager stage oracle requires a multistage plan", + )); + } + let selector = plan.selector_domains.as_ref().ok_or_else(|| { + RusticolError::compatibility( + "the retained eager stage oracle requires runtime selector domains", + ) + })?; + let mut best = None; + for (stage_position, stage) in plan.stages.iter().enumerate() { + let mut cursor = 0; + while cursor < stage.invocations.len() { + let kernel_id = stage.invocations[cursor].row.kernel_id; + let stop = stage.invocations[cursor..] + .iter() + .position(|row| row.row.kernel_id != kernel_id) + .map_or(stage.invocations.len(), |offset| cursor + offset); + let run = &stage.invocations[cursor..stop]; + let full_attachment_count = run + .iter() + .map(|invocation| invocation.attachment_range.len()) + .sum::(); + let distinct_destinations = run + .iter() + .flat_map(|invocation| { + stage.attachments[invocation.attachment_range.clone()] + .iter() + .map(|attachment| attachment.row.result_current_id) + }) + .collect::>(); + if run.len() >= 2 && distinct_destinations.len() >= 2 { + for &group_id in &selector.group_ids { + let groups = [group_id]; + let (selected_invocations, selected_attachments) = + retained_run_counts(&plan, stage, run, Some(&groups))?; + if selected_invocations == 0 || selected_invocations >= run.len() { + continue; + } + let candidate = EagerDirectRetainedStageCandidate { + stage_position, + stage_index: stage.stage_index, + kernel_id, + selector_group_id: group_id, + full_invocation_count: run.len(), + full_attachment_count, + selected_invocation_count: selected_invocations, + selected_attachment_count: selected_attachments, + distinct_destination_count: distinct_destinations.len(), + }; + if best + .as_ref() + .is_none_or(|previous: &EagerDirectRetainedStageCandidate| { + ( + candidate.full_invocation_count, + candidate.distinct_destination_count, + candidate.selected_invocation_count, + ) > ( + previous.full_invocation_count, + previous.distinct_destination_count, + previous.selected_invocation_count, + ) + }) + { + best = Some(candidate); + } + } + } + cursor = stop; + } + } + best.ok_or_else(|| { + RusticolError::compatibility( + "retained eager plan has no multi-row/multi-destination kernel run with nontrivial selector pruning", + ) + }) +} + +#[cfg(test)] +fn retained_run_counts( + plan: &EagerExecutionPlan, + stage: &super::plan::EagerStagePlan, + run: &[ScheduledInvocation], + active_groups: Option<&[u32]>, +) -> RusticolResult<(usize, usize)> { + let mut invocations = 0; + let mut attachments = 0; + for invocation in run { + if !row_active(plan, invocation.selector_domain_id, active_groups)? { + continue; + } + let retained = stage.attachments[invocation.attachment_range.clone()] + .iter() + .map(|attachment| row_active(plan, attachment.selector_domain_id, active_groups)) + .collect::>>()? + .into_iter() + .filter(|active| *active) + .count(); + if retained != 0 { + invocations += 1; + attachments += retained; + } + } + Ok((invocations, attachments)) +} + +/// Execute one real retained multistage artifact's chosen invocation run +/// through both the Direct-Arena table ABI and the established packet +/// gather/evaluate/scatter implementation. +#[cfg(test)] +#[allow(clippy::too_many_arguments)] +pub(crate) fn run_retained_multistage_oracle( + sections: EagerPlanV3Sections<'_>, + prepared: &[EagerDirectPreparedKernel<'_>], + candidate: EagerDirectRetainedStageCandidate, + backend: &mut B, + model_parameters: &[EagerComplex64], + point_count: u32, + sample_count: usize, + repetitions: usize, +) -> RusticolResult { + if point_count == 0 || sample_count < 3 || sample_count.is_multiple_of(2) || repetitions == 0 { + return Err(invalid( + "retained eager stage oracle needs points, odd sample count >= 3, and repetitions", + )); + } + let selected_groups = [candidate.selector_group_id]; + let full = run_retained_stage_variant( + sections, + prepared, + candidate, + backend, + model_parameters, + point_count, + sample_count, + repetitions, + None, + )?; + let selected = run_retained_stage_variant( + sections, + prepared, + candidate, + backend, + model_parameters, + point_count, + sample_count, + repetitions, + Some(&selected_groups), + )?; + if full.invocation_count != candidate.full_invocation_count + || full.attachment_count != candidate.full_attachment_count + || selected.invocation_count != candidate.selected_invocation_count + || selected.attachment_count != candidate.selected_attachment_count + { + return Err(RusticolError::internal( + "retained eager stage oracle selection counts changed after candidate selection", + )); + } + Ok(EagerDirectRetainedStageEvidence { + candidate, + full, + selected, + }) +} + +#[cfg(test)] +#[allow(clippy::too_many_arguments)] +fn run_retained_stage_variant( + sections: EagerPlanV3Sections<'_>, + prepared: &[EagerDirectPreparedKernel<'_>], + candidate: EagerDirectRetainedStageCandidate, + backend: &mut B, + model_parameters: &[EagerComplex64], + point_count: u32, + sample_count: usize, + repetitions: usize, + active_groups: Option<&[u32]>, +) -> RusticolResult { + use std::hint::black_box; + + use super::execute::execute_stage; + use super::plan::{EagerStagePlan, mark_initial_current_writes}; + use super::runtime::{EagerWorkspace, KernelPacket, PacketRole, StageSchedule}; + + let packet_plan = EagerExecutionPlan::from_plan_v3_sections(sections)?; + let source_stage = packet_plan + .stages + .get(candidate.stage_position) + .ok_or_else(|| invalid("retained eager candidate stage is out of bounds"))?; + if source_stage.stage_index != candidate.stage_index { + return Err(RusticolError::internal( + "retained eager candidate stage identity changed", + )); + } + let kernel = packet_plan + .kernels + .get(&candidate.kernel_id) + .ok_or_else(|| invalid("retained eager candidate kernel is absent"))?; + + let mut invocations = Vec::new(); + let mut attachments = Vec::new(); + for invocation in &source_stage.invocations { + if invocation.row.kernel_id != candidate.kernel_id + || !row_active(&packet_plan, invocation.selector_domain_id, active_groups)? + { + continue; + } + let attachment_start = attachments.len(); + for attachment in &source_stage.attachments[invocation.attachment_range.clone()] { + if row_active(&packet_plan, attachment.selector_domain_id, active_groups)? { + attachments.push(*attachment); + } + } + if attachments.len() == attachment_start { + continue; + } + let mut retained = invocation.clone(); + retained.attachment_range = attachment_start..attachments.len(); + invocations.push(retained); + } + if invocations.is_empty() || attachments.is_empty() { + return Err(RusticolError::internal( + "retained eager stage variant unexpectedly has no work", + )); + } + let zero_current_ranges = mark_initial_current_writes(&invocations, &mut attachments, &[]); + let stage = EagerStagePlan { + stage_index: source_stage.stage_index, + current_component_count: source_stage.current_component_count, + invocations, + attachments, + finalization_copies: Vec::new(), + finalizations: Vec::new(), + zero_current_ranges, + }; + let packet = KernelPacket { + role: PacketRole::Invocation, + kernel_id: candidate.kernel_id, + independent_block_size: 1, + item_range: 0..stage.invocations.len(), + input_components: kernel.inputs.len(), + output_components: kernel.output_component_count as usize, + linear_finalization: None, + }; + let schedule = StageSchedule { + invocation_packets: vec![packet], + finalization_packets: Vec::new(), + }; + + let points = point_count as usize; + let values = deterministic_complex_components(packet_plan.values.component_count, points); + let momenta = deterministic_real_components(packet_plan.momenta.component_count, points); + let couplings = packet_plan + .couplings + .iter() + .copied() + .map(|row| resolve_coupling(row, model_parameters)) + .collect::>(); + let packet_complex_count = stage + .invocations + .len() + .checked_mul(points) + .and_then(|lanes| { + kernel + .inputs + .len() + .checked_add(kernel.output_component_count as usize) + .and_then(|width| lanes.checked_mul(width)) + }) + .ok_or_else(|| invalid("retained eager packet workspace overflows"))?; + let mut packet_workspace = EagerWorkspace { + tile_capacity: points, + values: values.clone(), + currents: vec![ + EagerComplex64::new(0.0, 0.0); + source_stage.current_component_count * points + ], + amplitudes: vec![EagerComplex64::new(0.0, 0.0); packet_plan.amplitude_count * points], + reduction_groups: vec![ + EagerComplex64::new(0.0, 0.0); + packet_plan.reduction_groups.len() * points + ], + couplings, + reduced: vec![0.0; points], + packet: vec![EagerComplex64::new(0.0, 0.0); packet_complex_count], + }; + + let mut direct = EagerDirectInvocationPrototype::from_multistage_stage_for_test( + sections, + prepared, + active_groups, + point_count, + candidate.stage_position, + candidate.kernel_id, + )?; + initialize_all_retained_stage_inputs( + &mut direct, + &stage, + point_count, + &values, + &momenta, + model_parameters, + )?; + direct.execute_preinitialized_invocations(point_count)?; + execute_stage( + &stage, + &schedule, + &packet_plan.kernels, + &mut packet_workspace, + backend, + points, + 0, + points, + &momenta, + model_parameters, + )?; + let mut comparison_count = 0; + let mut bitwise_comparison_count = 0; + let mut maximum_absolute_error = 0.0_f64; + let mut maximum_relative_error = 0.0_f64; + let mut compared = BTreeSet::new(); + for attachment in &stage.attachments { + for component in 0..attachment.current.len { + if !compared.insert((attachment.row.result_current_id, component)) { + continue; + } + for point in 0..points { + let actual = direct.current_value( + attachment.row.result_current_id, + component as u32, + point as u32, + )?; + let expected = packet_workspace.currents + [(attachment.current.start + component) * points + point]; + if (actual.re.to_bits(), actual.im.to_bits()) + == (expected.re.to_bits(), expected.im.to_bits()) + { + bitwise_comparison_count += 1; + } else { + for (actual_part, expected_part) in + [(actual.re, expected.re), (actual.im, expected.im)] + { + let absolute = (actual_part - expected_part).abs(); + let relative = absolute / expected_part.abs().max(f64::MIN_POSITIVE); + maximum_absolute_error = maximum_absolute_error.max(absolute); + maximum_relative_error = maximum_relative_error.max(relative); + if absolute > 1.0e-15 + 1.0e-12 * expected_part.abs() { + return Err(RusticolError::integrity(format!( + "retained eager Direct-Arena stage differs from packet execution for \ + current {} component {component} point {point}: \ + {actual:?} != {expected:?}", + attachment.row.result_current_id + ))); + } + } + } + comparison_count += 1; + } + } + } + + let mut direct_samples = Vec::with_capacity(sample_count); + let mut packet_samples = Vec::with_capacity(sample_count); + for sample in 0..sample_count { + if sample.is_multiple_of(2) { + direct_samples.push(measure_repeated(repetitions, || { + direct.execute_preinitialized_invocations(point_count) + })?); + packet_samples.push(measure_repeated(repetitions, || { + execute_stage( + &stage, + &schedule, + &packet_plan.kernels, + &mut packet_workspace, + backend, + points, + 0, + points, + &momenta, + model_parameters, + ) + })?); + } else { + packet_samples.push(measure_repeated(repetitions, || { + execute_stage( + &stage, + &schedule, + &packet_plan.kernels, + &mut packet_workspace, + backend, + points, + 0, + points, + &momenta, + model_parameters, + ) + })?); + direct_samples.push(measure_repeated(repetitions, || { + direct.execute_preinitialized_invocations(point_count) + })?); + } + } + black_box(direct.current_value(stage.attachments[0].row.result_current_id, 0, 0)?); + black_box(packet_workspace.currents[stage.attachments[0].current.start * points]); + let (direct_median_ns, direct_mad_ns) = median_mad(&mut direct_samples); + let (packet_median_ns, packet_mad_ns) = median_mad(&mut packet_samples); + let direct_call_traffic = direct.traffic(); + direct_call_traffic.validate_direct()?; + + let used_value_components = stage + .invocations + .iter() + .flat_map(|invocation| { + [ + invocation.row.left_value_slot_id, + invocation.row.right_value_slot_id, + ] + }) + .collect::>() + .into_iter() + .map(|slot| direct.value_ranges[slot as usize].len as u64) + .sum::(); + let arena_initialization_write_bytes = [ + direct.layout.component_count() as u64, + used_value_components, + packet_plan.momenta.component_count as u64, + (packet_plan.couplings.len() as u64).saturating_mul(2), + packet_plan.parameter_count as u64, + ] + .into_iter() + .sum::() + .saturating_mul(u64::from(point_count)) + .saturating_mul(16); + let factor_fill_write_bytes = (direct.factor_specs.len() as u64).saturating_mul(16); + let lane_count = (stage.invocations.len() as u64).saturating_mul(u64::from(point_count)); + let packet_input_materialization_bytes_per_call = lane_count + .saturating_mul(kernel.inputs.len() as u64) + .saturating_mul(16); + let packet_output_materialization_bytes_per_call = lane_count + .saturating_mul(u64::from(kernel.output_component_count)) + .saturating_mul(16); + let packet_scatter_bytes_per_call = (stage.attachments.len() as u64) + .saturating_mul(u64::from(point_count)) + .saturating_mul(u64::from(kernel.output_component_count)) + .saturating_mul(16); + + Ok(EagerDirectRetainedStageVariantEvidence { + invocation_count: stage.invocations.len(), + attachment_count: stage.attachments.len(), + comparison_count, + bitwise_comparison_count, + maximum_absolute_error, + maximum_relative_error, + direct_median_ns, + direct_mad_ns, + packet_median_ns, + packet_mad_ns, + direct_over_packet: direct_median_ns / packet_median_ns, + direct_call_traffic, + arena_initialization_write_bytes, + factor_fill_write_bytes, + packet_input_materialization_bytes_per_call, + packet_output_materialization_bytes_per_call, + packet_scatter_bytes_per_call, + }) +} + +#[cfg(test)] +fn deterministic_complex_components(components: usize, points: usize) -> Vec { + (0..components) + .flat_map(|component| { + (0..points).map(move |point| { + let seed = (component * points + point + 1) as f64; + EagerComplex64::new(seed.mul_add(0.000_976_562_5, 0.25), -seed / 2048.0) + }) + }) + .collect() +} + +#[cfg(test)] +fn deterministic_real_components(components: usize, points: usize) -> Vec { + (0..components) + .flat_map(|component| { + (0..points).map(move |point| { + let seed = (component * points + point + 1) as f64; + seed.mul_add(0.001_953_125, 0.5) + }) + }) + .collect() +} + +#[cfg(test)] +fn initialize_all_retained_stage_inputs( + direct: &mut EagerDirectInvocationPrototype, + stage: &super::plan::EagerStagePlan, + point_count: u32, + values: &[EagerComplex64], + momenta: &[f64], + model_parameters: &[EagerComplex64], +) -> RusticolResult<()> { + validate_component_buffer( + "retained-stage values", + values.len(), + direct.plan.values.component_count, + point_count, + )?; + validate_component_buffer( + "retained-stage momenta", + momenta.len(), + direct.plan.momenta.component_count, + point_count, + )?; + if model_parameters.len() != direct.plan.parameter_count { + return Err(invalid( + "retained-stage model parameter count does not match the plan", + )); + } + direct.workspace.begin_tile(point_count)?; + direct + .workspace + .clear_current_active(0, direct.layout.component_count())?; + let stride = direct.workspace.arena().point_stride() as usize; + let points = point_count as usize; + { + let (real, imaginary, _, _) = direct.workspace.split_arena_slices_mut(); + let used_value_slots = stage + .invocations + .iter() + .flat_map(|invocation| { + [ + invocation.row.left_value_slot_id, + invocation.row.right_value_slot_id, + ] + }) + .collect::>(); + for slot in used_value_slots { + let range = direct.value_ranges[slot as usize]; + let semantic = direct.catalog.value(slot)?; + for component in 0..range.len { + let physical = assigned_component( + &direct.layout, + semantic, + count_u32(component, "retained value component")?, + )? as usize; + let target = physical * stride; + let source = (range.start + component) * points; + for point in 0..points { + let value = values[source + point]; + real[target + point] = value.re; + imaginary[target + point] = value.im; + } + } + } + for (slot, range) in direct.momentum_ranges.iter().copied().enumerate() { + let semantic = direct.catalog.momentum(slot as u32)?; + for component in 0..range.len { + let physical = assigned_component( + &direct.layout, + semantic, + count_u32(component, "retained momentum component")?, + )? as usize; + let target = physical * stride; + let source = (range.start + component) * points; + for point in 0..points { + real[target + point] = momenta[source + point]; + imaginary[target + point] = 0.0; + } + } + } + for (coupling_id, row) in direct.plan.couplings.iter().copied().enumerate() { + let coupling = resolve_coupling(row, model_parameters); + fill_real_input_plane( + real, + imaginary, + stride, + assigned_component( + &direct.layout, + direct.catalog.coupling_real(coupling_id as u32)?, + 0, + )?, + points, + coupling.re, + ); + fill_real_input_plane( + real, + imaginary, + stride, + assigned_component( + &direct.layout, + direct.catalog.coupling_imag(coupling_id as u32)?, + 0, + )?, + points, + coupling.im, + ); + } + for (parameter_id, value) in model_parameters.iter().copied().enumerate() { + let physical = assigned_component( + &direct.layout, + direct.catalog.parameter(parameter_id as u32)?, + 0, + )? as usize; + let target = physical * stride; + real[target..target + points].fill(value.re); + imaginary[target..target + points].fill(value.im); + } + } + { + let (factor_re, factor_im) = direct.workspace.factors_mut(); + for (index, spec) in direct.factor_specs.iter().copied().enumerate() { + let coupling = resolve_coupling( + direct.plan.couplings[spec.coupling_slot_id as usize], + model_parameters, + ); + let scale = match spec.output_factor_source { + EAGER_OUTPUT_FACTOR_NONE => 1.0, + EAGER_OUTPUT_FACTOR_COUPLING_REAL => coupling.re, + EAGER_OUTPUT_FACTOR_COUPLING_IMAG => coupling.im, + _ => { + return Err(RusticolError::integrity( + "retained eager factor has an invalid output-factor source", + )); + } + }; + factor_re[index] = spec.factor.re * scale; + factor_im[index] = spec.factor.im * scale; + } + } + Ok(()) +} + +#[cfg(test)] +fn measure_repeated( + repetitions: usize, + mut operation: impl FnMut() -> RusticolResult<()>, +) -> RusticolResult { + use std::time::Instant; + + let started = Instant::now(); + for _ in 0..repetitions { + operation()?; + } + Ok(started.elapsed().as_nanos() as f64 / repetitions as f64) +} + +#[cfg(test)] +fn median_mad(values: &mut [f64]) -> (f64, f64) { + values.sort_by(f64::total_cmp); + let median = values[values.len() / 2]; + let mut deviations = values + .iter() + .map(|value| (value - median).abs()) + .collect::>(); + deviations.sort_by(f64::total_cmp); + (median, deviations[deviations.len() / 2]) +} + +pub(super) fn derive_event_layout( + sections: EagerPlanV3Sections<'_>, + catalog: SemanticCatalog, + plan: &EagerExecutionPlan, +) -> RusticolResult { + let count = catalog.semantic_count as usize; + let mut widths = vec![1_u32; count]; + let mut first = vec![None::; count]; + let mut last = vec![None::; count]; + + let mut produced_values = vec![false; sections.values.len()]; + for row in sections.finalizations { + for value in [row.unpropagated_value_slot_id, row.propagated_value_slot_id] { + if value != MISSING_U32 { + produced_values[value as usize] = true; + } + } + } + for row in sections.values { + widths[catalog.value(row.value_slot_id)? as usize] = row.component_count; + if !produced_values[row.value_slot_id as usize] { + define_semantic(&mut first, &mut last, catalog.value(row.value_slot_id)?, 0)?; + } + } + for row in sections.currents { + widths[catalog.current(row.current_id)? as usize] = row.component_count; + } + for row in sections.momenta { + let id = catalog.momentum(row.momentum_slot_id)?; + widths[id as usize] = row.component_count; + define_semantic(&mut first, &mut last, id, 0)?; + } + for coupling in 0..sections.couplings.len() { + define_semantic( + &mut first, + &mut last, + catalog.coupling_real(coupling as u32)?, + 0, + )?; + define_semantic( + &mut first, + &mut last, + catalog.coupling_imag(coupling as u32)?, + 0, + )?; + } + for parameter in 0..sections.prepared_parameter_count { + define_semantic(&mut first, &mut last, catalog.parameter(parameter)?, 0)?; + } + + let mut event = 1_u64; + for stage in &plan.stages { + // The validated plan has already applied the stable equal-kernel sort + // used by both packet and Direct-Arena execution. Liveness must follow + // this execution order rather than the producer table order. + for invocation in &stage.invocations { + let row = invocation.row; + use_semantic( + &first, + &mut last, + catalog.value(row.left_value_slot_id)?, + event, + )?; + use_semantic( + &first, + &mut last, + catalog.value(row.right_value_slot_id)?, + event, + )?; + use_semantic( + &first, + &mut last, + catalog.momentum(row.left_momentum_slot_id)?, + event, + )?; + use_semantic( + &first, + &mut last, + catalog.momentum(row.right_momentum_slot_id)?, + event, + )?; + use_semantic( + &first, + &mut last, + catalog.coupling_real(row.coupling_slot_id)?, + event, + )?; + use_semantic( + &first, + &mut last, + catalog.coupling_imag(row.coupling_slot_id)?, + event, + )?; + touch_kernel_parameters( + plan.kernels.get(&row.kernel_id), + catalog, + &first, + &mut last, + event, + )?; + for attachment in &stage.attachments[invocation.attachment_range.clone()] { + define_or_use_semantic( + &mut first, + &mut last, + catalog.current(attachment.row.result_current_id)?, + event, + )?; + } + event = event + .checked_add(1) + .ok_or_else(|| invalid("eager direct event index overflows"))?; + } + + // Packet execution performs every unpropagated copy before any + // propagated finalization call. Sharing current/value planes therefore + // requires distinct events in precisely that order. + for copy in &stage.finalization_copies { + let current_id = stage_current_id_for_layout(stage, copy.current)?; + let value_id = plan + .values + .id_for_range(copy.unpropagated, "eager direct finalization copy")?; + use_semantic(&first, &mut last, catalog.current(current_id)?, event)?; + define_semantic(&mut first, &mut last, catalog.value(value_id)?, event)?; + event = event + .checked_add(1) + .ok_or_else(|| invalid("eager direct event index overflows"))?; + } + for item in &stage.finalizations { + let row = item.row; + use_semantic(&first, &mut last, catalog.current(row.current_id)?, event)?; + use_semantic( + &first, + &mut last, + catalog.momentum(row.momentum_slot_id)?, + event, + )?; + let propagated = item.propagated.ok_or_else(|| { + RusticolError::internal("eager direct finalization lost propagated output") + })?; + let value_id = plan + .values + .id_for_range(propagated, "eager direct propagated finalization")?; + define_semantic(&mut first, &mut last, catalog.value(value_id)?, event)?; + touch_kernel_parameters( + plan.kernels.get(&row.kernel_id), + catalog, + &first, + &mut last, + event, + )?; + event = event + .checked_add(1) + .ok_or_else(|| invalid("eager direct event index overflows"))?; + } + } + for closure in &plan.closures { + let row = closure.row; + use_semantic( + &first, + &mut last, + catalog.value(row.left_value_slot_id)?, + event, + )?; + use_semantic( + &first, + &mut last, + catalog.value(row.right_value_slot_id)?, + event, + )?; + if row.coupling_slot_id != MISSING_U32 { + use_semantic( + &first, + &mut last, + catalog.coupling_real(row.coupling_slot_id)?, + event, + )?; + use_semantic( + &first, + &mut last, + catalog.coupling_imag(row.coupling_slot_id)?, + event, + )?; + } + touch_kernel_parameters( + plan.kernels.get(&row.kernel_id), + catalog, + &first, + &mut last, + event, + )?; + event = event + .checked_add(1) + .ok_or_else(|| invalid("eager direct event index overflows"))?; + } + for closure in &plan.direct_closures { + use_semantic( + &first, + &mut last, + catalog.value(closure.row.left_value_slot_id)?, + event, + )?; + use_semantic( + &first, + &mut last, + catalog.value(closure.row.right_value_slot_id)?, + event, + )?; + event = event + .checked_add(1) + .ok_or_else(|| invalid("eager direct event index overflows"))?; + } + let final_event = event.saturating_sub(1); + for id in catalog.momentum_start..catalog.semantic_count { + if first[id as usize].is_some() { + last[id as usize] = Some(final_event); + } + } + let intervals = widths + .into_iter() + .enumerate() + .map(|(id, width)| { + let first_use = first[id].unwrap_or(0); + let last_use = last[id].unwrap_or(final_event).max(first_use); + DirectArenaInterval::new(id as u32, first_use, last_use, width) + }) + .collect::>>()?; + assign_direct_arena(&intervals) +} + +fn stage_current_id_for_layout( + stage: &super::plan::EagerStagePlan, + range: ComponentRange, +) -> RusticolResult { + stage + .finalizations + .iter() + .find_map(|item| (item.current == range).then_some(item.row.current_id)) + .or_else(|| { + stage + .attachments + .iter() + .find_map(|item| (item.current == range).then_some(item.row.result_current_id)) + }) + .ok_or_else(|| RusticolError::internal("eager direct liveness lost a stage current")) +} + +fn touch_kernel_parameters( + kernel: Option<&super::EagerKernelSpec>, + catalog: SemanticCatalog, + first: &[Option], + last: &mut [Option], + event: u64, +) -> RusticolResult<()> { + let Some(kernel) = kernel else { + return Ok(()); + }; + for input in &kernel.inputs { + if let EagerKernelInput::ModelParameter(parameter) = *input { + use_semantic(first, last, catalog.parameter(parameter)?, event)?; + } + } + Ok(()) +} + +fn encode_invocation_inputs( + output: &mut Vec, + invocation: &ScheduledInvocation, + descriptors: &[EagerKernelInput], + catalog: SemanticCatalog, + layout: &DirectArenaLayout, +) -> RusticolResult<()> { + for descriptor in descriptors { + let (semantic, component) = match *descriptor { + EagerKernelInput::FirstCurrentComponent(component) => { + (catalog.value(invocation.row.left_value_slot_id)?, component) + } + EagerKernelInput::SecondCurrentComponent(component) => ( + catalog.value(invocation.row.right_value_slot_id)?, + component, + ), + EagerKernelInput::FirstMomentumComponent(component) => ( + catalog.momentum(invocation.row.left_momentum_slot_id)?, + component, + ), + EagerKernelInput::SecondMomentumComponent(component) => ( + catalog.momentum(invocation.row.right_momentum_slot_id)?, + component, + ), + EagerKernelInput::CouplingReal => { + (catalog.coupling_real(invocation.row.coupling_slot_id)?, 0) + } + EagerKernelInput::CouplingImag => { + (catalog.coupling_imag(invocation.row.coupling_slot_id)?, 0) + } + EagerKernelInput::ModelParameter(parameter) => (catalog.parameter(parameter)?, 0), + }; + let physical = assigned_component(layout, semantic, component)?; + push_u32( + output, + physical + .checked_mul(2) + .ok_or_else(|| invalid("eager direct plane catalog overflows"))?, + ); + push_u32( + output, + physical + .checked_mul(2) + .and_then(|value| value.checked_add(1)) + .ok_or_else(|| invalid("eager direct plane catalog overflows"))?, + ); + } + Ok(()) +} + +fn encode_attachment_destinations( + output: &mut Vec, + attachment: &ScheduledAttachment, + output_component_count: u32, + catalog: SemanticCatalog, + layout: &DirectArenaLayout, +) -> RusticolResult<()> { + if attachment.current.len != output_component_count as usize { + return Err(RusticolError::integrity( + "eager direct attachment width does not match its kernel output", + )); + } + let semantic = catalog.current(attachment.row.result_current_id)?; + for component in 0..output_component_count { + let physical = assigned_component(layout, semantic, component)?; + push_u32( + output, + physical + .checked_mul(2) + .ok_or_else(|| invalid("eager direct destination catalog overflows"))?, + ); + push_u32( + output, + physical + .checked_mul(2) + .and_then(|value| value.checked_add(1)) + .ok_or_else(|| invalid("eager direct destination catalog overflows"))?, + ); + } + Ok(()) +} + +pub(super) fn row_active( + plan: &EagerExecutionPlan, + domain: Option, + active_groups: Option<&[u32]>, +) -> RusticolResult { + let Some(active_groups) = active_groups else { + return Ok(true); + }; + // An explicitly empty runtime selector is a structural zero, including + // for legacy/domain-less rows. It must never fall through to an + // unconditional row. + if active_groups.is_empty() { + return Ok(false); + } + let Some(domain) = domain else { + return Ok(true); + }; + let selector = plan.selector_domains.as_ref().ok_or_else(|| { + RusticolError::integrity("eager direct selector row has no selector-domain plan") + })?; + let members = selector.memberships.get(domain as usize).ok_or_else(|| { + RusticolError::integrity("eager direct row references an unknown selector domain") + })?; + Ok(members + .iter() + .any(|member| active_groups.binary_search(member).is_ok())) +} + +fn retained_attachment_operation(written_currents: &mut BTreeSet, current_id: u32) -> u32 { + u32::from(!written_currents.insert(current_id)) +} + +pub(super) fn validate_active_groups( + plan: &EagerExecutionPlan, + active_groups: Option<&[u32]>, +) -> RusticolResult<()> { + let Some(active_groups) = active_groups else { + return Ok(()); + }; + if active_groups.windows(2).any(|pair| pair[0] >= pair[1]) { + return Err(invalid( + "eager direct active groups must be sorted and unique", + )); + } + let selector = plan.selector_domains.as_ref().ok_or_else(|| { + RusticolError::compatibility( + "eager direct selector specialization requires selector domains", + ) + })?; + if let Some(unknown) = active_groups + .iter() + .find(|group| selector.group_ids.binary_search(group).is_err()) + { + return Err(invalid(format!( + "eager direct selector references unknown group {unknown}" + ))); + } + Ok(()) +} + +pub(super) fn validate_callable_identity( + kernel: &EagerKernelSpec, + prepared: &EagerDirectPreparedKernel<'_>, +) -> RusticolResult<()> { + if prepared.role != kernel.role + || prepared.inputs != kernel.inputs + || prepared.output_component_count != kernel.output_component_count + { + return Err(RusticolError::integrity(format!( + "eager direct callable identity for kernel {} does not match its plan spec: \ + role {:?}/{:?}, ordered inputs {:?}/{:?}, output width {}/{}", + kernel.kernel_id, + prepared.role, + kernel.role, + prepared.inputs, + kernel.inputs, + prepared.output_component_count, + kernel.output_component_count, + ))); + } + Ok(()) +} + +pub(super) fn assigned_component( + layout: &DirectArenaLayout, + semantic: u32, + component: u32, +) -> RusticolResult { + let assignment = layout.assignment(semantic).ok_or_else(|| { + RusticolError::integrity(format!( + "eager direct layout lost semantic value {semantic}" + )) + })?; + if component >= assignment.component_count { + return Err(RusticolError::integrity(format!( + "eager direct semantic value {semantic} component {component} is out of bounds" + ))); + } + assignment + .component_base + .checked_add(component) + .ok_or_else(|| invalid("eager direct component assignment overflows")) +} + +pub(super) fn resolve_coupling( + row: crate::EagerCouplingRow, + parameters: &[EagerComplex64], +) -> EagerComplex64 { + let real = if row.real_parameter_id == MISSING_U32 { + row.constant_real + } else { + parameters[row.real_parameter_id as usize].re + }; + let imaginary = if row.imag_parameter_id == MISSING_U32 { + row.constant_imag + } else { + parameters[row.imag_parameter_id as usize].re + }; + EagerComplex64::new(real, imaginary) +} + +fn fill_real_input_plane( + real: &mut [f64], + imaginary: &mut [f64], + stride: usize, + physical: u32, + points: usize, + value: f64, +) { + let start = physical as usize * stride; + real[start..start + points].fill(value); + imaginary[start..start + points].fill(0.0); +} + +fn define_semantic( + first: &mut [Option], + last: &mut [Option], + id: u32, + event: u64, +) -> RusticolResult<()> { + let index = id as usize; + if first[index].replace(event).is_some() { + return Err(RusticolError::integrity(format!( + "eager direct semantic value {id} is defined more than once" + ))); + } + last[index] = Some(event); + Ok(()) +} + +fn define_or_use_semantic( + first: &mut [Option], + last: &mut [Option], + id: u32, + event: u64, +) -> RusticolResult<()> { + let index = id as usize; + if first[index].is_none() { + first[index] = Some(event); + } + last[index] = Some(event); + Ok(()) +} + +fn use_semantic( + first: &[Option], + last: &mut [Option], + id: u32, + event: u64, +) -> RusticolResult<()> { + if first[id as usize].is_none() { + return Err(RusticolError::integrity(format!( + "eager direct semantic value {id} is used before definition" + ))); + } + last[id as usize] = Some(event); + Ok(()) +} + +fn checked_semantic(start: u32, id: u32, stop: u32, label: &str) -> RusticolResult { + start + .checked_add(id) + .filter(|value| *value < stop) + .ok_or_else(|| invalid(format!("eager direct {label} id {id} is out of bounds"))) +} + +pub(super) fn validate_component_buffer( + label: &str, + actual: usize, + components: usize, + points: u32, +) -> RusticolResult<()> { + let expected = components + .checked_mul(points as usize) + .ok_or_else(|| invalid(format!("eager direct {label} length overflows")))?; + if actual != expected { + return Err(invalid(format!( + "eager direct {label} has length {actual}, expected {expected}" + ))); + } + Ok(()) +} + +fn row_range<'a, T>(rows: &'a [T], start: u64, count: u64, label: &str) -> RusticolResult<&'a [T]> { + let start = usize::try_from(start) + .map_err(|_| invalid(format!("eager direct {label} start exceeds usize")))?; + let count = usize::try_from(count) + .map_err(|_| invalid(format!("eager direct {label} count exceeds usize")))?; + let stop = start + .checked_add(count) + .ok_or_else(|| invalid(format!("eager direct {label} range overflows")))?; + rows.get(start..stop) + .ok_or_else(|| invalid(format!("eager direct {label} range is out of bounds"))) +} + +pub(super) fn count_u32(value: usize, label: &str) -> RusticolResult { + u32::try_from(value).map_err(|_| invalid(format!("eager direct {label} count exceeds u32"))) +} + +pub(super) fn push_u32(output: &mut Vec, value: u32) { + output.extend_from_slice(&value.to_le_bytes()); +} + +pub(super) fn invalid(message: impl Into) -> RusticolError { + RusticolError::invalid_argument(message) +} + +#[cfg(all(test, feature = "f64-symjit", target_arch = "aarch64"))] +mod tests { + use std::hint::black_box; + use std::time::Instant; + + use symjit::{Applet, Application, Compiler, Config, Expr}; + + use super::super::plan_v3_tests::Fixture; + use super::*; + use crate::engine::compile_symbolica_program_to_plane_application_bytes; + use crate::engine::count_allocations; + use crate::engine::symjit_eager_direct::eager_direct_descriptor_for_source_application_bytes; + + fn source_application() -> Application { + let mut config = Config::default(); + config.set_opt_level(2); + config.set_complex(true); + config.set_symbolica(true); + config.set_simd(true); + config.set_fast_complex(false); + let x = Expr::var("x"); + let y = Expr::var("y"); + let coupling = Expr::var("coupling"); + Compiler::with_config(config) + .compile_params(&[], &[&x + &y], &[x, y, coupling]) + .unwrap() + } + + fn ordinary_applet() -> Applet { + let mut source = source_application(); + source.prepare_simd(); + source.seal().unwrap() + } + + fn source_and_descriptor() -> (Vec, Vec) { + let bytes = compile_symbolica_program_to_plane_application_bytes( + "([('add', ('temp', 0), [('param', 0), ('param', 1)], 0), \ + ('assign', ('out', 0), ('temp', 0))], 3, [])", + 3, + 1, + 2, + true, + ) + .unwrap(); + let descriptor = eager_direct_descriptor_for_source_application_bytes( + &bytes, + 3, + 1, + std::path::Path::new("eager-invocation-test.symjit"), + ) + .unwrap(); + (bytes, descriptor) + } + + fn fanout_fixture() -> Fixture { + let mut fixture = Fixture::new(); + fixture.currents.push(crate::EagerPlanCurrentRow { + current_id: 3, + component_start: 3, + component_count: 1, + momentum_slot_id: 3, + flags: 0, + }); + fixture.values.push(crate::EagerPlanValueRow { + value_slot_id: 3, + current_id: 3, + component_start: 3, + component_count: 1, + kind: crate::EagerValueSlotKind::Unpropagated, + }); + fixture.momenta.push(crate::EagerPlanMomentumRow { + momentum_slot_id: 3, + bitset_id: 3, + component_start: 12, + component_count: 4, + }); + let mut second_attachment = fixture.attachments[0]; + second_attachment.result_current_id = 3; + fixture.attachments.push(second_attachment); + fixture.invocations[0].attachment_count = 2; + fixture.stages[0].attachment_count = 2; + fixture.stages[0].finalization_count = 2; + let mut second_finalization = fixture.finalizations[0]; + second_finalization.current_id = 3; + second_finalization.unpropagated_value_slot_id = 3; + second_finalization.momentum_slot_id = 3; + fixture.finalizations.push(second_finalization); + let mut second_closure = fixture.closures[0]; + second_closure.root_id = 1; + second_closure.left_value_slot_id = 3; + fixture.closures.push(second_closure); + fixture + } + + fn inputs(points: usize) -> (Vec, Vec) { + let mut values = vec![EagerComplex64::new(-91.0, 37.0); 4 * points]; + for point in 0..points { + values[point] = EagerComplex64::new(1.0 + point as f64 / 8.0, -0.25 * point as f64); + values[points + point] = + EagerComplex64::new(3.0 + point as f64 / 4.0, 0.5 * point as f64); + } + let momenta = (0..16 * points).map(|index| index as f64 / 16.0).collect(); + (values, momenta) + } + + struct PacketOracle { + applet: Applet, + inputs: Vec, + outputs: Vec, + currents: Vec, + } + + impl PacketOracle { + fn new(capacity: usize) -> Self { + Self { + applet: ordinary_applet(), + inputs: vec![EagerComplex64::new(0.0, 0.0); 3 * capacity], + outputs: vec![EagerComplex64::new(0.0, 0.0); capacity], + currents: vec![EagerComplex64::new(0.0, 0.0); capacity], + } + } + + fn evaluate(&mut self, points: usize, values: &[EagerComplex64]) { + for point in 0..points { + self.inputs[3 * point] = values[point]; + self.inputs[3 * point + 1] = values[points + point]; + self.inputs[3 * point + 2] = EagerComplex64::new(2.0, 3.0); + } + self.applet.evaluate_matrix( + &self.inputs[..3 * points], + &mut self.outputs[..points], + points, + ); + for point in 0..points { + let contribution = self.outputs[point] * EagerComplex64::new(15.0, 0.0); + self.currents[point] = contribution; + } + } + } + + fn prototype<'a>( + fixture: &Fixture, + source: &'a [u8], + descriptor: &'a [u8], + active_groups: Option<&[u32]>, + ) -> EagerDirectInvocationPrototype { + let inputs = [ + EagerKernelInput::FirstCurrentComponent(0), + EagerKernelInput::SecondCurrentComponent(0), + EagerKernelInput::CouplingReal, + ]; + let artifact = EagerDirectPreparedKernel { + kernel_id: 10, + role: EagerKernelRole::Vertex, + inputs: &inputs, + output_component_count: 1, + application: EagerDirectPreparedApplication::Symjit { + source_application: source, + descriptor, + expected_optimization_level: 2, + expected_compression: true, + }, + display_path: PathBuf::from("real-plan-v3-eager-k10.symjit"), + }; + EagerDirectInvocationPrototype::from_plan_v3_sections( + fixture.sections(), + &[artifact], + active_groups, + 129, + ) + .unwrap() + } + + #[test] + fn real_plan_v3_rows_match_packet_oracle_for_fanout_and_tails() { + let fixture = fanout_fixture(); + let (source, descriptor) = source_and_descriptor(); + let mut direct = prototype(&fixture, &source, &descriptor, None); + assert!( + direct.layout().reused_semantic_components() > 0, + "event-derived layout should reuse at least one dead semantic range" + ); + let mut packet = PacketOracle::new(129); + for points in [7_usize, 127, 128, 129] { + let (values, momenta) = inputs(points); + direct + .evaluate_invocations(points as u32, &values, &momenta, &[]) + .unwrap(); + packet.evaluate(points, &values); + for (point, expected) in packet.currents[..points].iter().copied().enumerate() { + let actual = direct.current_value(2, 0, point as u32).unwrap(); + assert_eq!( + (actual.re.to_bits(), actual.im.to_bits()), + (expected.re.to_bits(), expected.im.to_bits()), + "point {point} at tail {points}" + ); + let second = direct.current_value(3, 0, point as u32).unwrap(); + assert_eq!( + (second.re.to_bits(), second.im.to_bits()), + (expected.re.to_bits(), expected.im.to_bits()), + "second fanout destination at point {point} tail {points}" + ); + } + } + let traffic = direct.traffic(); + assert!(traffic.calls >= 4); + assert!(traffic.rows >= 4); + assert_eq!( + ( + traffic.packet_input_bytes, + traffic.packet_output_bytes, + traffic.gather_bytes, + traffic.scatter_bytes, + traffic.remap_bytes, + ), + (0, 0, 0, 0, 0) + ); + } + + #[test] + fn empty_runtime_selector_is_a_structural_zero_without_a_table_call() { + let fixture = fanout_fixture(); + let mut direct = EagerDirectInvocationPrototype::from_plan_v3_sections( + fixture.sections(), + &[], + Some(&[]), + 129, + ) + .unwrap(); + let (values, momenta) = inputs(129); + direct + .evaluate_invocations(129, &values, &momenta, &[]) + .unwrap(); + for point in 0..129 { + let actual = direct.current_value(2, 0, point).unwrap(); + assert_eq!((actual.re.to_bits(), actual.im.to_bits()), (0, 0)); + let second = direct.current_value(3, 0, point).unwrap(); + assert_eq!((second.re.to_bits(), second.im.to_bits()), (0, 0)); + } + assert_eq!(direct.traffic().calls, 0); + direct.traffic().validate_direct().unwrap(); + } + + #[test] + fn empty_runtime_selector_prunes_domainless_rows_too() { + let fixture = fanout_fixture(); + let plan = EagerExecutionPlan::from_plan_v3_sections(fixture.sections()).unwrap(); + assert!(!row_active(&plan, None, Some(&[])).unwrap()); + assert!(row_active(&plan, None, Some(&[7])).unwrap()); + } + + #[test] + fn selector_pruning_recomputes_first_retained_current_write() { + // The full schedule marked the first row as the initializer. Runtime + // selection prunes it and retains the second row, whose stale + // full-plan flag was "add". + let original_initializes = [true, false]; + let retained = [false, true]; + let mut written = BTreeSet::new(); + let operations = original_initializes + .into_iter() + .zip(retained) + .filter(|&(_original, keep)| keep) + .map(|_| retained_attachment_operation(&mut written, 23)) + .collect::>(); + assert_eq!(operations, [0], "first retained write must overwrite"); + + // Lock the bitwise reason this cannot reuse the stale flag: assigning + // -0 preserves its sign, while adding -0 to a cleared +0 loses it. + let contribution = -0.0_f64; + let mut recomputed = 0.0_f64; + if operations[0] == 0 { + recomputed = contribution; + } else { + recomputed += contribution; + } + let mut stale = 0.0_f64; + stale += contribution; + assert_eq!(recomputed.to_bits(), contribution.to_bits()); + assert_ne!(stale.to_bits(), contribution.to_bits()); + } + + #[test] + fn matching_dimension_callable_with_wrong_semantic_identity_is_rejected() { + let fixture = fanout_fixture(); + let (source, descriptor) = source_and_descriptor(); + let wrong_inputs = [ + EagerKernelInput::SecondCurrentComponent(0), + EagerKernelInput::FirstCurrentComponent(0), + EagerKernelInput::CouplingReal, + ]; + let artifact = EagerDirectPreparedKernel { + kernel_id: 10, + role: EagerKernelRole::Vertex, + inputs: &wrong_inputs, + output_component_count: 1, + application: EagerDirectPreparedApplication::Symjit { + source_application: &source, + descriptor: &descriptor, + expected_optimization_level: 2, + expected_compression: true, + }, + display_path: PathBuf::from("wrong-semantic-identity.symjit"), + }; + let result = EagerDirectInvocationPrototype::from_plan_v3_sections( + fixture.sections(), + &[artifact], + None, + 129, + ); + let Err(error) = result else { + panic!("matching dimensions must not authenticate the wrong callable"); + }; + assert!(error.to_string().contains("callable identity")); + assert!(error.to_string().contains("ordered inputs")); + } + + #[test] + fn warmed_real_plan_v3_invocation_slice_allocates_zero() { + let fixture = fanout_fixture(); + let (source, descriptor) = source_and_descriptor(); + let mut direct = prototype(&fixture, &source, &descriptor, None); + let (values, momenta) = inputs(129); + direct + .evaluate_invocations(129, &values, &momenta, &[]) + .unwrap(); + let (result, allocations, bytes) = + count_allocations(|| direct.evaluate_invocations(129, &values, &momenta, &[])); + result.unwrap(); + assert_eq!((allocations, bytes), (0, 0)); + } + + #[test] + #[ignore = "local interleaved native timing evidence; no timing assertion"] + fn benchmark_real_plan_v3_direct_slice_against_packet_execution() { + const SAMPLES: usize = 9; + const REPETITIONS: usize = 10_000; + const POINTS: usize = 129; + + let fixture = fanout_fixture(); + let (source, descriptor) = source_and_descriptor(); + let mut direct = prototype(&fixture, &source, &descriptor, None); + let (values, momenta) = inputs(POINTS); + let mut packet = PacketOracle::new(POINTS); + direct + .evaluate_invocations(POINTS as u32, &values, &momenta, &[]) + .unwrap(); + packet.evaluate(POINTS, &values); + + let mut direct_ns = Vec::with_capacity(SAMPLES); + let mut packet_ns = Vec::with_capacity(SAMPLES); + for sample in 0..SAMPLES { + if sample.is_multiple_of(2) { + direct_ns.push(measure_direct(&mut direct, REPETITIONS)); + packet_ns.push(measure_packet(&mut packet, &values, REPETITIONS)); + } else { + packet_ns.push(measure_packet(&mut packet, &values, REPETITIONS)); + direct_ns.push(measure_direct(&mut direct, REPETITIONS)); + } + } + direct_ns.sort_by(f64::total_cmp); + packet_ns.sort_by(f64::total_cmp); + let direct_median = direct_ns[SAMPLES / 2]; + let packet_median = packet_ns[SAMPLES / 2]; + eprintln!( + "eager real-plan-v3 invocation benchmark: samples={SAMPLES} \ + repetitions={REPETITIONS} points={POINTS} \ + direct_median_ns/call={direct_median:.3} \ + packet_median_ns/call={packet_median:.3} \ + direct_over_packet={:.6}", + direct_median / packet_median + ); + } + + fn measure_direct(direct: &mut EagerDirectInvocationPrototype, repetitions: usize) -> f64 { + let started = Instant::now(); + for _ in 0..repetitions { + direct.execute_preinitialized_invocations(129).unwrap(); + } + black_box(direct.current_value(2, 0, 0).unwrap()); + started.elapsed().as_nanos() as f64 / repetitions as f64 + } + + fn measure_packet( + packet: &mut PacketOracle, + values: &[EagerComplex64], + repetitions: usize, + ) -> f64 { + let started = Instant::now(); + for _ in 0..repetitions { + packet.evaluate(129, values); + } + black_box(packet.currents[0]); + started.elapsed().as_nanos() as f64 / repetitions as f64 + } +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/eager_runtime/execute.rs b/pyamplicol/rust/crates/rusticol-core/src/eager_runtime/execute.rs new file mode 100644 index 0000000000..8678cd0a2f --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/eager_runtime/execute.rs @@ -0,0 +1,2440 @@ +// SPDX-License-Identifier: 0BSD + +use super::plan::{ + ClosureExecutionRows, ComponentRange, EagerExecutionPlan, EagerStagePlan, ScheduledAttachment, + ScheduledClosure, ScheduledDirectClosure, ScheduledFinalization, ScheduledInvocation, +}; +use super::runtime::{ + EagerWorkspace, KernelPacket, LinearFinalizationPacket, PacketRole, StageSchedule, +}; +use super::{ + EagerComplex64, EagerKernelBackend, EagerKernelCall, EagerKernelInput, EagerKernelSpec, +}; +use crate::{ + EAGER_OUTPUT_FACTOR_COUPLING_IMAG, EAGER_OUTPUT_FACTOR_COUPLING_REAL, EAGER_OUTPUT_FACTOR_NONE, + RusticolError, RusticolResult, +}; +use std::collections::BTreeMap; + +#[derive(Clone, Copy, Debug, PartialEq)] +pub(super) enum AccumulationFactor { + One, + NegativeOne, + ImaginaryUnit, + NegativeImaginaryUnit, + Real(f64), + Imaginary(f64), + Generic(EagerComplex64), +} + +impl AccumulationFactor { + #[inline] + pub(super) fn from_parts(real: f64, imag: f64) -> Self { + match (real, imag) { + (1.0, 0.0) => Self::One, + (-1.0, 0.0) => Self::NegativeOne, + (0.0, 1.0) => Self::ImaginaryUnit, + (0.0, -1.0) => Self::NegativeImaginaryUnit, + (_, 0.0) => Self::Real(real), + (0.0, _) => Self::Imaginary(imag), + _ => Self::Generic(EagerComplex64::new(real, imag)), + } + } + + #[inline(always)] + pub(super) fn accumulate(self, target: &mut EagerComplex64, value: EagerComplex64) { + match self { + Self::One => *target += value, + Self::NegativeOne => *target -= value, + Self::ImaginaryUnit => { + target.re -= value.im; + target.im += value.re; + } + Self::NegativeImaginaryUnit => { + target.re += value.im; + target.im -= value.re; + } + Self::Real(factor) => { + target.re += factor * value.re; + target.im += factor * value.im; + } + Self::Imaginary(factor) => { + target.re -= factor * value.im; + target.im += factor * value.re; + } + Self::Generic(factor) => *target += factor * value, + } + } + + #[inline(always)] + pub(super) fn assign(self, target: &mut EagerComplex64, value: EagerComplex64) { + *target = match self { + Self::One => value, + Self::NegativeOne => -value, + Self::ImaginaryUnit => EagerComplex64::new(-value.im, value.re), + Self::NegativeImaginaryUnit => EagerComplex64::new(value.im, -value.re), + Self::Real(factor) => EagerComplex64::new(factor * value.re, factor * value.im), + Self::Imaginary(factor) => EagerComplex64::new(-factor * value.im, factor * value.re), + Self::Generic(factor) => factor * value, + }; + } + + #[inline] + #[allow(clippy::too_many_arguments)] + fn scatter_strided( + self, + initializes: bool, + source: &[EagerComplex64], + source_start: usize, + source_stride: usize, + target: &mut [EagerComplex64], + target_start: usize, + count: usize, + ) { + debug_assert!(count == 0 || source_start + (count - 1) * source_stride < source.len()); + debug_assert!(target_start + count <= target.len()); + // Bounds are established above from validated packet/current ranges. + // Specializing the factor outside the point loop avoids repeated enum + // dispatch in the eager scatter hot path. + unsafe { + let mut source_ptr = source.as_ptr().add(source_start); + let mut target_ptr = target.as_mut_ptr().add(target_start); + macro_rules! scatter { + ($operation:expr) => { + for _ in 0..count { + let value = *source_ptr; + $operation(&mut *target_ptr, value); + source_ptr = source_ptr.add(source_stride); + target_ptr = target_ptr.add(1); + } + }; + } + match (initializes, self) { + (true, Self::One) => { + scatter!(|target: &mut EagerComplex64, value: EagerComplex64| { + *target = value + }) + } + (true, Self::NegativeOne) => { + scatter!(|target: &mut EagerComplex64, value: EagerComplex64| { + *target = -value + }) + } + (true, Self::ImaginaryUnit) => { + scatter!(|target: &mut EagerComplex64, value: EagerComplex64| { + *target = EagerComplex64::new(-value.im, value.re) + }) + } + (true, Self::NegativeImaginaryUnit) => { + scatter!(|target: &mut EagerComplex64, value: EagerComplex64| { + *target = EagerComplex64::new(value.im, -value.re) + }) + } + (true, Self::Real(factor)) => { + scatter!(|target: &mut EagerComplex64, value: EagerComplex64| { + *target = EagerComplex64::new(factor * value.re, factor * value.im) + }) + } + (true, Self::Imaginary(factor)) => { + scatter!(|target: &mut EagerComplex64, value: EagerComplex64| { + *target = EagerComplex64::new(-factor * value.im, factor * value.re) + }) + } + (true, Self::Generic(factor)) => { + scatter!(|target: &mut EagerComplex64, value: EagerComplex64| { + *target = factor * value + }) + } + (false, Self::One) => { + scatter!(|target: &mut EagerComplex64, value: EagerComplex64| { + *target += value + }) + } + (false, Self::NegativeOne) => { + scatter!(|target: &mut EagerComplex64, value: EagerComplex64| { + *target -= value + }) + } + (false, Self::ImaginaryUnit) => { + scatter!(|target: &mut EagerComplex64, value: EagerComplex64| { + target.re -= value.im; + target.im += value.re; + }) + } + (false, Self::NegativeImaginaryUnit) => { + scatter!(|target: &mut EagerComplex64, value: EagerComplex64| { + target.re += value.im; + target.im -= value.re; + }) + } + (false, Self::Real(factor)) => { + scatter!(|target: &mut EagerComplex64, value: EagerComplex64| { + target.re += factor * value.re; + target.im += factor * value.im; + }) + } + (false, Self::Imaginary(factor)) => { + scatter!(|target: &mut EagerComplex64, value: EagerComplex64| { + target.re -= factor * value.im; + target.im += factor * value.re; + }) + } + (false, Self::Generic(factor)) => { + scatter!(|target: &mut EagerComplex64, value: EagerComplex64| { + *target += factor * value + }) + } + } + } + } + + #[inline] + #[allow(clippy::too_many_arguments)] + fn scatter_interleaved( + self, + initializes: bool, + source: &[EagerComplex64], + source_start: usize, + target: &mut [EagerComplex64], + target_component_start: usize, + target_component_stride: usize, + count: usize, + ) { + self.scatter_interleaved_strided::( + initializes, + source, + source_start, + COMPONENTS, + target, + target_component_start, + target_component_stride, + count, + ); + } + + #[inline] + #[allow(clippy::too_many_arguments)] + fn scatter_interleaved_strided( + self, + initializes: bool, + source: &[EagerComplex64], + source_start: usize, + source_row_stride: usize, + target: &mut [EagerComplex64], + target_component_start: usize, + target_component_stride: usize, + count: usize, + ) { + debug_assert!(COMPONENTS > 0); + debug_assert!(source_row_stride >= COMPONENTS); + debug_assert!( + count == 0 + || source_start + (count - 1) * source_row_stride + COMPONENTS <= source.len() + ); + debug_assert!( + count == 0 + || (target_component_start + COMPONENTS - 1) * target_component_stride + count + <= target.len() + ); + // SymJIT returns lane-major rows while eager current storage is + // component-major. Handle the common small output widths in one pass + // so source reads stay contiguous and each target component advances + // linearly. + unsafe { + let mut source_ptr = source.as_ptr().add(source_start); + let target_base = target.as_mut_ptr(); + let mut target_ptrs = [target_base; COMPONENTS]; + for (component, target_ptr) in target_ptrs.iter_mut().enumerate() { + *target_ptr = + target_base.add((target_component_start + component) * target_component_stride); + } + macro_rules! scatter { + ($operation:expr) => { + for _ in 0..count { + for component in 0..COMPONENTS { + let value = *source_ptr.add(component); + $operation(&mut *target_ptrs[component], value); + target_ptrs[component] = target_ptrs[component].add(1); + } + source_ptr = source_ptr.add(source_row_stride); + } + }; + } + match (initializes, self) { + (true, Self::One) => { + scatter!(|target: &mut EagerComplex64, value: EagerComplex64| *target = value) + } + (true, Self::NegativeOne) => { + scatter!(|target: &mut EagerComplex64, value: EagerComplex64| *target = -value) + } + (true, Self::ImaginaryUnit) => { + scatter!(|target: &mut EagerComplex64, value: EagerComplex64| { + *target = EagerComplex64::new(-value.im, value.re) + }) + } + (true, Self::NegativeImaginaryUnit) => { + scatter!(|target: &mut EagerComplex64, value: EagerComplex64| { + *target = EagerComplex64::new(value.im, -value.re) + }) + } + (true, Self::Real(factor)) => { + scatter!(|target: &mut EagerComplex64, value: EagerComplex64| { + *target = EagerComplex64::new(factor * value.re, factor * value.im) + }) + } + (true, Self::Imaginary(factor)) => { + scatter!(|target: &mut EagerComplex64, value: EagerComplex64| { + *target = EagerComplex64::new(-factor * value.im, factor * value.re) + }) + } + (true, Self::Generic(factor)) => scatter!( + |target: &mut EagerComplex64, value: EagerComplex64| *target = factor * value + ), + (false, Self::One) => { + scatter!(|target: &mut EagerComplex64, value: EagerComplex64| *target += value) + } + (false, Self::NegativeOne) => { + scatter!(|target: &mut EagerComplex64, value: EagerComplex64| *target -= value) + } + (false, Self::ImaginaryUnit) => { + scatter!(|target: &mut EagerComplex64, value: EagerComplex64| { + target.re -= value.im; + target.im += value.re; + }) + } + (false, Self::NegativeImaginaryUnit) => { + scatter!(|target: &mut EagerComplex64, value: EagerComplex64| { + target.re += value.im; + target.im -= value.re; + }) + } + (false, Self::Real(factor)) => { + scatter!(|target: &mut EagerComplex64, value: EagerComplex64| { + target.re += factor * value.re; + target.im += factor * value.im; + }) + } + (false, Self::Imaginary(factor)) => { + scatter!(|target: &mut EagerComplex64, value: EagerComplex64| { + target.re -= factor * value.im; + target.im += factor * value.re; + }) + } + (false, Self::Generic(factor)) => { + scatter!(|target: &mut EagerComplex64, value: EagerComplex64| { + *target += factor * value + }) + } + } + } + } +} + +#[inline(always)] +fn output_factor_scale(source: u32, coupling: EagerComplex64) -> f64 { + match source { + EAGER_OUTPUT_FACTOR_NONE => 1.0, + EAGER_OUTPUT_FACTOR_COUPLING_REAL => coupling.re, + EAGER_OUTPUT_FACTOR_COUPLING_IMAG => coupling.im, + _ => unreachable!("validated eager output factor source"), + } +} + +pub(super) fn initialize_tile( + plan: &EagerExecutionPlan, + workspace: &mut EagerWorkspace, + point_count: usize, + tile_start: usize, + tile_points: usize, + initial_values: &[EagerComplex64], + zero_amplitude_indices: &[usize], +) { + for range in &plan.initial_value_ranges { + for component in range.start..range.start + range.len { + let source = component * point_count + tile_start; + let target = component * workspace.tile_capacity; + workspace.values[target..target + tile_points] + .copy_from_slice(&initial_values[source..source + tile_points]); + } + } + for amplitude in zero_amplitude_indices { + let target = amplitude * workspace.tile_capacity; + workspace.amplitudes[target..target + tile_points].fill(EagerComplex64::new(0.0, 0.0)); + } +} + +#[allow(clippy::too_many_arguments)] +pub(super) fn execute_stage( + stage: &EagerStagePlan, + schedule: &StageSchedule, + kernels: &BTreeMap, + workspace: &mut EagerWorkspace, + backend: &mut B, + point_count: usize, + tile_start: usize, + tile_points: usize, + momenta: &[f64], + model_parameters: &[EagerComplex64], +) -> RusticolResult<()> { + for range in &stage.zero_current_ranges { + for component in range.start..range.start + range.len { + let target = component * workspace.tile_capacity; + workspace.currents[target..target + tile_points].fill(EagerComplex64::new(0.0, 0.0)); + } + } + for packet in &schedule.invocation_packets { + debug_assert_eq!(packet.role, PacketRole::Invocation); + let items = &stage.invocations[packet.item_range.clone()]; + let (lane_count, evaluator_input_components, evaluator_output_components) = + invocation_packet_shape(packet, items.len(), tile_points)?; + let input_len = evaluator_input_components * lane_count; + let output_len = evaluator_output_components * lane_count; + let (inputs, outputs) = packet_slices(&mut workspace.packet, input_len, output_len)?; + let kernel = kernels.get(&packet.kernel_id).ok_or_else(|| { + RusticolError::internal(format!( + "eager schedule lost invocation kernel {}", + packet.kernel_id + )) + })?; + if packet.independent_block_size == 1 { + gather_invocations( + items, + &kernel.inputs, + inputs, + packet.input_components, + point_count, + tile_start, + tile_points, + workspace.tile_capacity, + &workspace.values, + momenta, + &workspace.couplings, + model_parameters, + ); + } else { + gather_blocked_invocations( + items, + &kernel.inputs, + inputs, + packet.input_components, + packet.independent_block_size, + tile_points, + workspace.tile_capacity, + &workspace.values, + ); + } + backend.evaluate_batch(EagerKernelCall { + kernel_id: packet.kernel_id, + independent_block_size: packet.independent_block_size as u32, + lane_count, + input_component_count: evaluator_input_components, + output_component_count: evaluator_output_components, + inputs, + outputs, + })?; + if packet.independent_block_size == 1 { + scatter_invocations( + items, + &stage.attachments, + outputs, + lane_count, + tile_points, + workspace.tile_capacity, + &workspace.couplings, + &mut workspace.currents, + ); + } else { + scatter_blocked_invocations( + items, + &stage.attachments, + outputs, + packet.output_components, + packet.independent_block_size, + tile_points, + workspace.tile_capacity, + &workspace.couplings, + &mut workspace.currents, + ); + } + } + + for item in &stage.finalization_copies { + copy_component_range( + &workspace.currents, + item.current, + &mut workspace.values, + item.unpropagated, + workspace.tile_capacity, + tile_points, + ); + } + for packet in &schedule.finalization_packets { + debug_assert_eq!(packet.role, PacketRole::Finalization); + let items = &stage.finalizations[packet.item_range.clone()]; + let lane_count = packet + .linear_finalization + .as_ref() + .map_or(items.len(), LinearFinalizationPacket::lane_multiplier) + * tile_points; + let input_len = packet.input_components * lane_count; + let output_len = packet.output_components * lane_count; + let (inputs, outputs) = packet_slices(&mut workspace.packet, input_len, output_len)?; + let kernel = kernels.get(&packet.kernel_id).ok_or_else(|| { + RusticolError::internal(format!( + "eager schedule lost finalization kernel {}", + packet.kernel_id + )) + })?; + if let Some(linear) = &packet.linear_finalization { + gather_linear_finalizations( + linear, + &kernel.inputs, + inputs, + packet.input_components, + point_count, + tile_start, + tile_points, + momenta, + model_parameters, + ); + } else { + gather_finalizations( + items, + &kernel.inputs, + inputs, + packet.input_components, + point_count, + tile_start, + tile_points, + workspace.tile_capacity, + &workspace.currents, + momenta, + model_parameters, + ); + } + backend.evaluate_batch(EagerKernelCall { + kernel_id: packet.kernel_id, + independent_block_size: 1, + lane_count, + input_component_count: packet.input_components, + output_component_count: packet.output_components, + inputs, + outputs, + })?; + if let Some(linear) = &packet.linear_finalization { + scatter_linear_finalizations( + items, + linear, + outputs, + lane_count, + tile_points, + workspace.tile_capacity, + &workspace.currents, + &mut workspace.values, + )?; + } else { + scatter_finalizations( + items, + outputs, + lane_count, + tile_points, + workspace.tile_capacity, + &mut workspace.values, + )?; + } + } + Ok(()) +} + +pub(super) fn invocation_packet_shape( + packet: &KernelPacket, + item_count: usize, + tile_points: usize, +) -> RusticolResult<(usize, usize, usize)> { + let block_size = packet.independent_block_size; + if block_size == 0 || !item_count.is_multiple_of(block_size) { + return Err(RusticolError::internal( + "eager invocation packet has an invalid independent block shape", + )); + } + let lane_count = item_count + .checked_div(block_size) + .and_then(|value| value.checked_mul(tile_points)) + .ok_or_else(|| RusticolError::invalid_argument("eager invocation lanes overflow"))?; + let input_components = packet + .input_components + .checked_mul(block_size) + .ok_or_else(|| RusticolError::invalid_argument("eager blocked input width overflows"))?; + let output_components = packet + .output_components + .checked_mul(block_size) + .ok_or_else(|| RusticolError::invalid_argument("eager blocked output width overflows"))?; + Ok((lane_count, input_components, output_components)) +} + +#[allow(clippy::too_many_arguments)] +pub(super) fn gather_blocked_invocations( + items: &[ScheduledInvocation], + descriptors: &[EagerKernelInput], + inputs: &mut [EagerComplex64], + input_component_count: usize, + block_size: usize, + tile_points: usize, + tile_capacity: usize, + values: &[EagerComplex64], +) { + debug_assert_eq!(block_size, super::EAGER_INDEPENDENT_BLOCK_SIZE as usize); + debug_assert_eq!(items.len() % block_size, 0); + debug_assert!(descriptors.iter().all(|descriptor| matches!( + descriptor, + EagerKernelInput::FirstCurrentComponent(_) | EagerKernelInput::SecondCurrentComponent(_) + ))); + match input_component_count { + 6 => gather_current_only_blocked::<6, 4>( + items, + descriptors, + inputs, + tile_points, + tile_capacity, + values, + ), + 8 => gather_current_only_blocked::<8, 4>( + items, + descriptors, + inputs, + tile_points, + tile_capacity, + values, + ), + 10 => gather_current_only_blocked::<10, 4>( + items, + descriptors, + inputs, + tile_points, + tile_capacity, + values, + ), + _ => gather_current_only_blocked_generic( + items, + descriptors, + inputs, + block_size, + tile_points, + tile_capacity, + values, + ), + } +} + +#[allow(clippy::too_many_arguments)] +pub(super) fn gather_invocations( + items: &[ScheduledInvocation], + descriptors: &[EagerKernelInput], + inputs: &mut [EagerComplex64], + input_component_count: usize, + point_count: usize, + tile_start: usize, + tile_points: usize, + tile_capacity: usize, + values: &[EagerComplex64], + momenta: &[f64], + couplings: &[EagerComplex64], + model_parameters: &[EagerComplex64], +) { + if descriptors.iter().all(|descriptor| { + matches!( + descriptor, + EagerKernelInput::FirstCurrentComponent(_) + | EagerKernelInput::SecondCurrentComponent(_) + ) + }) { + match input_component_count { + 6 => { + gather_current_only_invocations::<6>( + items, + descriptors, + inputs, + tile_points, + tile_capacity, + values, + ); + return; + } + 8 => { + gather_current_only_invocations::<8>( + items, + descriptors, + inputs, + tile_points, + tile_capacity, + values, + ); + return; + } + 10 => { + gather_current_only_invocations::<10>( + items, + descriptors, + inputs, + tile_points, + tile_capacity, + values, + ); + return; + } + _ => {} + } + } + for (item_index, item) in items.iter().enumerate() { + let lane_start = item_index * tile_points; + let coupling = couplings[item.row.coupling_slot_id as usize]; + for (target_component, descriptor) in descriptors.iter().copied().enumerate() { + match descriptor { + EagerKernelInput::FirstCurrentComponent(component) => gather_complex_component( + values, + item.left_values, + component, + tile_capacity, + 0, + inputs, + target_component, + input_component_count, + lane_start, + tile_points, + ), + EagerKernelInput::SecondCurrentComponent(component) => gather_complex_component( + values, + item.right_values, + component, + tile_capacity, + 0, + inputs, + target_component, + input_component_count, + lane_start, + tile_points, + ), + EagerKernelInput::FirstMomentumComponent(component) => gather_real_component( + momenta, + item.left_momenta, + component, + point_count, + tile_start, + inputs, + target_component, + input_component_count, + lane_start, + tile_points, + ), + EagerKernelInput::SecondMomentumComponent(component) => gather_real_component( + momenta, + item.right_momenta, + component, + point_count, + tile_start, + inputs, + target_component, + input_component_count, + lane_start, + tile_points, + ), + EagerKernelInput::CouplingReal => fill_packet_component( + inputs, + target_component, + input_component_count, + lane_start, + tile_points, + EagerComplex64::new(coupling.re, 0.0), + ), + EagerKernelInput::CouplingImag => fill_packet_component( + inputs, + target_component, + input_component_count, + lane_start, + tile_points, + EagerComplex64::new(coupling.im, 0.0), + ), + EagerKernelInput::ModelParameter(parameter) => fill_packet_component( + inputs, + target_component, + input_component_count, + lane_start, + tile_points, + model_parameters[parameter as usize], + ), + } + } + } +} + +fn gather_current_only_invocations( + items: &[ScheduledInvocation], + descriptors: &[EagerKernelInput], + inputs: &mut [EagerComplex64], + tile_points: usize, + tile_capacity: usize, + values: &[EagerComplex64], +) { + debug_assert_eq!(descriptors.len(), COMPONENTS); + debug_assert_eq!(inputs.len(), items.len() * tile_points * COMPONENTS); + for (item_index, item) in items.iter().enumerate() { + let mut source_starts = [0usize; COMPONENTS]; + for (component, descriptor) in descriptors.iter().copied().enumerate() { + source_starts[component] = match descriptor { + EagerKernelInput::FirstCurrentComponent(source_component) => { + debug_assert!((source_component as usize) < item.left_values.len); + (item.left_values.start + source_component as usize) * tile_capacity + } + EagerKernelInput::SecondCurrentComponent(source_component) => { + debug_assert!((source_component as usize) < item.right_values.len); + (item.right_values.start + source_component as usize) * tile_capacity + } + _ => unreachable!("current-only eager gather descriptor"), + }; + debug_assert!(source_starts[component] + tile_points <= values.len()); + } + let target_start = item_index * tile_points * COMPONENTS; + debug_assert!(target_start + tile_points * COMPONENTS <= inputs.len()); + // Read each component stream linearly while writing complete SymJIT + // input rows contiguously. Const arities let LLVM unroll the small + // inner transpose used by the dominant prepared vertex kernels. + unsafe { + let source_base = values.as_ptr(); + let target_base = inputs.as_mut_ptr().add(target_start); + for point in 0..tile_points { + let target = target_base.add(point * COMPONENTS); + for (component, source_start) in source_starts.iter().copied().enumerate() { + *target.add(component) = *source_base.add(source_start + point); + } + } + } + } +} + +fn gather_current_only_blocked( + items: &[ScheduledInvocation], + descriptors: &[EagerKernelInput], + inputs: &mut [EagerComplex64], + tile_points: usize, + tile_capacity: usize, + values: &[EagerComplex64], +) { + debug_assert_eq!(descriptors.len(), COMPONENTS); + debug_assert_eq!(items.len() % BLOCK_SIZE, 0); + debug_assert_eq!(inputs.len(), items.len() * tile_points * COMPONENTS); + for (block_index, block) in items.chunks_exact(BLOCK_SIZE).enumerate() { + let mut source_starts = [[0usize; COMPONENTS]; BLOCK_SIZE]; + for (lane, item) in block.iter().enumerate() { + for (component, descriptor) in descriptors.iter().copied().enumerate() { + source_starts[lane][component] = match descriptor { + EagerKernelInput::FirstCurrentComponent(source_component) => { + debug_assert!((source_component as usize) < item.left_values.len); + (item.left_values.start + source_component as usize) * tile_capacity + } + EagerKernelInput::SecondCurrentComponent(source_component) => { + debug_assert!((source_component as usize) < item.right_values.len); + (item.right_values.start + source_component as usize) * tile_capacity + } + _ => unreachable!("validated blocked current-only descriptor"), + }; + } + } + let target_start = block_index * tile_points * BLOCK_SIZE * COMPONENTS; + unsafe { + let source_base = values.as_ptr(); + let target_base = inputs.as_mut_ptr().add(target_start); + for point in 0..tile_points { + let target = target_base.add(point * BLOCK_SIZE * COMPONENTS); + for (lane, lane_starts) in source_starts.iter().enumerate() { + let lane_target = target.add(lane * COMPONENTS); + for (component, source_start) in lane_starts.iter().copied().enumerate() { + *lane_target.add(component) = *source_base.add(source_start + point); + } + } + } + } + } +} + +fn gather_current_only_blocked_generic( + items: &[ScheduledInvocation], + descriptors: &[EagerKernelInput], + inputs: &mut [EagerComplex64], + block_size: usize, + tile_points: usize, + tile_capacity: usize, + values: &[EagerComplex64], +) { + let component_count = descriptors.len(); + debug_assert_eq!(items.len() % block_size, 0); + debug_assert_eq!(inputs.len(), items.len() * tile_points * component_count); + for (block_index, block) in items.chunks_exact(block_size).enumerate() { + for point in 0..tile_points { + let target_row = (block_index * tile_points + point) * block_size * component_count; + for (lane, item) in block.iter().enumerate() { + let lane_target = target_row + lane * component_count; + for (component, descriptor) in descriptors.iter().copied().enumerate() { + let (range, source_component) = match descriptor { + EagerKernelInput::FirstCurrentComponent(source_component) => { + (item.left_values, source_component) + } + EagerKernelInput::SecondCurrentComponent(source_component) => { + (item.right_values, source_component) + } + _ => unreachable!("validated blocked current-only descriptor"), + }; + inputs[lane_target + component] = + values[(range.start + source_component as usize) * tile_capacity + point]; + } + } + } + } +} + +#[allow(clippy::too_many_arguments)] +pub(super) fn scatter_invocations( + items: &[ScheduledInvocation], + attachments: &[ScheduledAttachment], + outputs: &[EagerComplex64], + lane_count: usize, + tile_points: usize, + tile_capacity: usize, + couplings: &[EagerComplex64], + currents: &mut [EagerComplex64], +) { + let output_components = outputs.len() / lane_count; + for (item_index, item) in items.iter().enumerate() { + let lane_start = item_index * tile_points; + let output_scale = output_factor_scale( + item.row.output_factor_source, + couplings[item.row.coupling_slot_id as usize], + ); + let item_attachments = &attachments[item.attachment_range.clone()]; + let source_start = lane_start * output_components; + let fused = match (item_attachments.len(), output_components) { + (2, 2) => scatter_signed_fanout::<2, 2>( + item_attachments, + outputs, + source_start, + output_scale, + currents, + tile_capacity, + tile_points, + ), + (2, 4) => scatter_signed_fanout::<4, 2>( + item_attachments, + outputs, + source_start, + output_scale, + currents, + tile_capacity, + tile_points, + ), + (2, 6) => scatter_signed_fanout::<6, 2>( + item_attachments, + outputs, + source_start, + output_scale, + currents, + tile_capacity, + tile_points, + ), + (4, 2) => scatter_signed_fanout::<2, 4>( + item_attachments, + outputs, + source_start, + output_scale, + currents, + tile_capacity, + tile_points, + ), + (4, 4) => scatter_signed_fanout::<4, 4>( + item_attachments, + outputs, + source_start, + output_scale, + currents, + tile_capacity, + tile_points, + ), + (4, 6) => scatter_signed_fanout::<6, 4>( + item_attachments, + outputs, + source_start, + output_scale, + currents, + tile_capacity, + tile_points, + ), + _ => false, + }; + if fused { + continue; + } + for attachment in item_attachments { + let factor = AccumulationFactor::from_parts( + attachment.row.factor_real * output_scale, + attachment.row.factor_imag * output_scale, + ); + debug_assert_eq!(attachment.current.len, output_components); + match output_components { + 2 => { + factor.scatter_interleaved::<2>( + attachment.initializes_current, + outputs, + source_start, + currents, + attachment.current.start, + tile_capacity, + tile_points, + ); + continue; + } + 4 => { + factor.scatter_interleaved::<4>( + attachment.initializes_current, + outputs, + source_start, + currents, + attachment.current.start, + tile_capacity, + tile_points, + ); + continue; + } + 6 => { + factor.scatter_interleaved::<6>( + attachment.initializes_current, + outputs, + source_start, + currents, + attachment.current.start, + tile_capacity, + tile_points, + ); + continue; + } + 8 => { + factor.scatter_interleaved::<8>( + attachment.initializes_current, + outputs, + source_start, + currents, + attachment.current.start, + tile_capacity, + tile_points, + ); + continue; + } + _ => {} + } + for component in 0..output_components { + let target = (attachment.current.start + component) * tile_capacity; + factor.scatter_strided( + attachment.initializes_current, + outputs, + source_start + component, + output_components, + currents, + target, + tile_points, + ); + } + } + } +} + +#[allow(clippy::too_many_arguments)] +pub(super) fn scatter_blocked_invocations( + items: &[ScheduledInvocation], + attachments: &[ScheduledAttachment], + outputs: &[EagerComplex64], + output_components: usize, + block_size: usize, + tile_points: usize, + tile_capacity: usize, + couplings: &[EagerComplex64], + currents: &mut [EagerComplex64], +) { + debug_assert_eq!(block_size, super::EAGER_INDEPENDENT_BLOCK_SIZE as usize); + debug_assert_eq!(items.len() % block_size, 0); + let source_row_stride = block_size * output_components; + debug_assert_eq!( + outputs.len(), + (items.len() / block_size) * tile_points * source_row_stride + ); + for (item_index, item) in items.iter().enumerate() { + let block_index = item_index / block_size; + let lane_index = item_index % block_size; + let source_start = + block_index * tile_points * source_row_stride + lane_index * output_components; + let output_scale = output_factor_scale( + item.row.output_factor_source, + couplings[item.row.coupling_slot_id as usize], + ); + for attachment in &attachments[item.attachment_range.clone()] { + let factor = AccumulationFactor::from_parts( + attachment.row.factor_real * output_scale, + attachment.row.factor_imag * output_scale, + ); + debug_assert_eq!(attachment.current.len, output_components); + match output_components { + 2 => factor.scatter_interleaved_strided::<2>( + attachment.initializes_current, + outputs, + source_start, + source_row_stride, + currents, + attachment.current.start, + tile_capacity, + tile_points, + ), + 4 => factor.scatter_interleaved_strided::<4>( + attachment.initializes_current, + outputs, + source_start, + source_row_stride, + currents, + attachment.current.start, + tile_capacity, + tile_points, + ), + 6 => factor.scatter_interleaved_strided::<6>( + attachment.initializes_current, + outputs, + source_start, + source_row_stride, + currents, + attachment.current.start, + tile_capacity, + tile_points, + ), + 8 => factor.scatter_interleaved_strided::<8>( + attachment.initializes_current, + outputs, + source_start, + source_row_stride, + currents, + attachment.current.start, + tile_capacity, + tile_points, + ), + _ => { + for component in 0..output_components { + factor.scatter_strided( + attachment.initializes_current, + outputs, + source_start + component, + source_row_stride, + currents, + (attachment.current.start + component) * tile_capacity, + tile_points, + ); + } + } + } + } + } +} + +#[allow(clippy::too_many_arguments)] +fn scatter_signed_fanout( + attachments: &[ScheduledAttachment], + outputs: &[EagerComplex64], + source_start: usize, + output_scale: f64, + currents: &mut [EagerComplex64], + tile_capacity: usize, + tile_points: usize, +) -> bool { + let Ok(attachments) = <&[ScheduledAttachment; FANOUT]>::try_from(attachments) else { + return false; + }; + let mut negative = [false; FANOUT]; + for (index, attachment) in attachments.iter().enumerate() { + if attachment.row.factor_imag != 0.0 + || (attachment.row.factor_real != 1.0 && attachment.row.factor_real != -1.0) + { + return false; + } + debug_assert_eq!(attachment.current.len, COMPONENTS); + negative[index] = attachment.row.factor_real == -1.0; + } + debug_assert!(source_start + tile_points * COMPONENTS <= outputs.len()); + unsafe { + let target_base = currents.as_mut_ptr(); + let mut target_ptrs = [[target_base; FANOUT]; COMPONENTS]; + for (attachment_index, attachment) in attachments.iter().enumerate() { + for (component, component_targets) in target_ptrs.iter_mut().enumerate() { + component_targets[attachment_index] = + target_base.add((attachment.current.start + component) * tile_capacity); + } + } + let mut source_ptr = outputs.as_ptr().add(source_start); + for _ in 0..tile_points { + for (component, component_targets) in target_ptrs.iter_mut().enumerate() { + let value = *source_ptr.add(component); + let scaled = match output_scale { + 1.0 => value, + -1.0 => -value, + scale => EagerComplex64::new(scale * value.re, scale * value.im), + }; + for (attachment_index, target_ptr) in component_targets.iter_mut().enumerate() { + let target = &mut **target_ptr; + let contribution = if negative[attachment_index] { + -scaled + } else { + scaled + }; + if attachments[attachment_index].initializes_current { + *target = contribution; + } else { + *target += contribution; + } + *target_ptr = target_ptr.add(1); + } + } + source_ptr = source_ptr.add(COMPONENTS); + } + } + true +} + +#[allow(clippy::too_many_arguments)] +pub(super) fn gather_finalizations( + items: &[ScheduledFinalization], + descriptors: &[EagerKernelInput], + inputs: &mut [EagerComplex64], + input_component_count: usize, + point_count: usize, + tile_start: usize, + tile_points: usize, + tile_capacity: usize, + currents: &[EagerComplex64], + momenta: &[f64], + model_parameters: &[EagerComplex64], +) { + if descriptors.len() == input_component_count { + match input_component_count { + 6 if finalization_inputs_are_current_momentum::<2>(descriptors) => { + gather_current_momentum_finalizations::<2, 6>( + items, + descriptors, + inputs, + point_count, + tile_start, + tile_points, + tile_capacity, + currents, + momenta, + ); + return; + } + 8 if finalization_inputs_are_current_momentum::<4>(descriptors) => { + gather_current_momentum_finalizations::<4, 8>( + items, + descriptors, + inputs, + point_count, + tile_start, + tile_points, + tile_capacity, + currents, + momenta, + ); + return; + } + _ => {} + } + } + for (item_index, item) in items.iter().enumerate() { + let lane_start = item_index * tile_points; + for (target_component, descriptor) in descriptors.iter().copied().enumerate() { + match descriptor { + EagerKernelInput::FirstCurrentComponent(component) => gather_complex_component( + currents, + item.current, + component, + tile_capacity, + 0, + inputs, + target_component, + input_component_count, + lane_start, + tile_points, + ), + EagerKernelInput::FirstMomentumComponent(component) => gather_real_component( + momenta, + item.momentum, + component, + point_count, + tile_start, + inputs, + target_component, + input_component_count, + lane_start, + tile_points, + ), + EagerKernelInput::ModelParameter(parameter) => fill_packet_component( + inputs, + target_component, + input_component_count, + lane_start, + tile_points, + model_parameters[parameter as usize], + ), + _ => unreachable!("validated eager finalization descriptor"), + } + } + } +} + +#[allow(clippy::too_many_arguments)] +pub(super) fn gather_linear_finalizations( + linear: &LinearFinalizationPacket, + descriptors: &[EagerKernelInput], + inputs: &mut [EagerComplex64], + input_component_count: usize, + point_count: usize, + tile_start: usize, + tile_points: usize, + momenta: &[f64], + model_parameters: &[EagerComplex64], +) { + debug_assert_eq!(descriptors.len(), input_component_count); + debug_assert_eq!( + inputs.len(), + linear.lane_multiplier() * tile_points * input_component_count + ); + let basis_count = linear.current_components.len(); + for (group_index, momentum) in linear.momentum_groups.iter().copied().enumerate() { + for basis_index in 0..basis_count { + let lane_start = (group_index * basis_count + basis_index) * tile_points; + for (target_component, descriptor) in descriptors.iter().copied().enumerate() { + match descriptor { + EagerKernelInput::FirstCurrentComponent(component) => { + let value = EagerComplex64::new( + if linear.current_components[basis_index] == component { + 1.0 + } else { + 0.0 + }, + 0.0, + ); + fill_packet_component( + inputs, + target_component, + input_component_count, + lane_start, + tile_points, + value, + ); + } + EagerKernelInput::FirstMomentumComponent(component) => gather_real_component( + momenta, + momentum, + component, + point_count, + tile_start, + inputs, + target_component, + input_component_count, + lane_start, + tile_points, + ), + EagerKernelInput::ModelParameter(parameter) => fill_packet_component( + inputs, + target_component, + input_component_count, + lane_start, + tile_points, + model_parameters[parameter as usize], + ), + _ => unreachable!("validated eager finalization descriptor"), + } + } + } + } +} + +fn finalization_inputs_are_current_momentum( + descriptors: &[EagerKernelInput], +) -> bool { + descriptors[..CURRENT_COMPONENTS] + .iter() + .all(|descriptor| matches!(descriptor, EagerKernelInput::FirstCurrentComponent(_))) + && descriptors[CURRENT_COMPONENTS..] + .iter() + .all(|descriptor| matches!(descriptor, EagerKernelInput::FirstMomentumComponent(_))) +} + +#[allow(clippy::too_many_arguments)] +fn gather_current_momentum_finalizations< + const CURRENT_COMPONENTS: usize, + const COMPONENTS: usize, +>( + items: &[ScheduledFinalization], + descriptors: &[EagerKernelInput], + inputs: &mut [EagerComplex64], + point_count: usize, + tile_start: usize, + tile_points: usize, + tile_capacity: usize, + currents: &[EagerComplex64], + momenta: &[f64], +) { + debug_assert_eq!(COMPONENTS, CURRENT_COMPONENTS + 4); + debug_assert_eq!(descriptors.len(), COMPONENTS); + debug_assert_eq!(inputs.len(), items.len() * tile_points * COMPONENTS); + for (item_index, item) in items.iter().enumerate() { + let mut current_starts = [0usize; CURRENT_COMPONENTS]; + for (component, descriptor) in descriptors[..CURRENT_COMPONENTS] + .iter() + .copied() + .enumerate() + { + let EagerKernelInput::FirstCurrentComponent(source_component) = descriptor else { + unreachable!("validated eager current/momentum finalization descriptor"); + }; + debug_assert!((source_component as usize) < item.current.len); + current_starts[component] = + (item.current.start + source_component as usize) * tile_capacity; + } + let mut momentum_starts = [0usize; 4]; + for (component, descriptor) in descriptors[CURRENT_COMPONENTS..] + .iter() + .copied() + .enumerate() + { + let EagerKernelInput::FirstMomentumComponent(source_component) = descriptor else { + unreachable!("validated eager current/momentum finalization descriptor"); + }; + debug_assert!((source_component as usize) < item.momentum.len); + momentum_starts[component] = + (item.momentum.start + source_component as usize) * point_count + tile_start; + } + let target_start = item_index * tile_points * COMPONENTS; + unsafe { + let current_base = currents.as_ptr(); + let momentum_base = momenta.as_ptr(); + let target_base = inputs.as_mut_ptr().add(target_start); + for point in 0..tile_points { + let target = target_base.add(point * COMPONENTS); + for (component, source_start) in current_starts.iter().copied().enumerate() { + *target.add(component) = *current_base.add(source_start + point); + } + for (component, source_start) in momentum_starts.iter().copied().enumerate() { + *target.add(CURRENT_COMPONENTS + component) = + EagerComplex64::new(*momentum_base.add(source_start + point), 0.0); + } + } + } + } +} + +pub(super) fn scatter_finalizations( + items: &[ScheduledFinalization], + outputs: &[EagerComplex64], + lane_count: usize, + tile_points: usize, + tile_capacity: usize, + values: &mut [EagerComplex64], +) -> RusticolResult<()> { + let output_components = outputs.len() / lane_count; + for (item_index, item) in items.iter().enumerate() { + let lane_start = item_index * tile_points; + let Some(target) = item.propagated else { + return Err(RusticolError::internal( + "eager finalization schedule lost its propagated output", + )); + }; + debug_assert_eq!(target.len, output_components); + let source_start = lane_start * output_components; + match output_components { + 2 => { + AccumulationFactor::One.scatter_interleaved::<2>( + true, + outputs, + source_start, + values, + target.start, + tile_capacity, + tile_points, + ); + continue; + } + 4 => { + AccumulationFactor::One.scatter_interleaved::<4>( + true, + outputs, + source_start, + values, + target.start, + tile_capacity, + tile_points, + ); + continue; + } + 6 => { + AccumulationFactor::One.scatter_interleaved::<6>( + true, + outputs, + source_start, + values, + target.start, + tile_capacity, + tile_points, + ); + continue; + } + 8 => { + AccumulationFactor::One.scatter_interleaved::<8>( + true, + outputs, + source_start, + values, + target.start, + tile_capacity, + tile_points, + ); + continue; + } + _ => {} + } + for component in 0..output_components { + let target_start = (target.start + component) * tile_capacity; + AccumulationFactor::One.scatter_strided( + true, + outputs, + source_start + component, + output_components, + values, + target_start, + tile_points, + ); + } + } + Ok(()) +} + +#[allow(clippy::too_many_arguments)] +pub(super) fn scatter_linear_finalizations( + items: &[ScheduledFinalization], + linear: &LinearFinalizationPacket, + outputs: &[EagerComplex64], + lane_count: usize, + tile_points: usize, + tile_capacity: usize, + currents: &[EagerComplex64], + values: &mut [EagerComplex64], +) -> RusticolResult<()> { + debug_assert_eq!(linear.item_group_indices.len(), items.len()); + let output_components = outputs.len() / lane_count; + let basis_stride = linear.current_components.len(); + for (item_index, item) in items.iter().enumerate() { + let Some(target) = item.propagated else { + return Err(RusticolError::internal( + "eager linear finalization lost its propagated output", + )); + }; + debug_assert_eq!(target.len, output_components); + let group_start = linear.item_group_indices[item_index] * basis_stride * tile_points; + for output_component in 0..output_components { + let target_start = (target.start + output_component) * tile_capacity; + for (basis_index, source_component) in + linear.current_components.iter().copied().enumerate() + { + let basis_start = (group_start + basis_index * tile_points) * output_components + + output_component; + let source_start = (item.current.start + source_component as usize) * tile_capacity; + if basis_index == 0 { + for point in 0..tile_points { + let coefficient = outputs[basis_start + point * output_components]; + values[target_start + point] = coefficient * currents[source_start + point]; + } + } else { + for point in 0..tile_points { + let coefficient = outputs[basis_start + point * output_components]; + values[target_start + point] += + coefficient * currents[source_start + point]; + } + } + } + } + } + Ok(()) +} + +pub(super) fn execute_closures( + rows: ClosureExecutionRows<'_>, + packets: &[KernelPacket], + workspace: &mut EagerWorkspace, + backend: &mut B, + tile_points: usize, + model_parameters: &[EagerComplex64], +) -> RusticolResult<()> { + for packet in packets { + debug_assert_eq!(packet.role, PacketRole::Closure); + let items = &rows.closures[packet.item_range.clone()]; + let lane_count = items.len() * tile_points; + let input_len = packet.input_components * lane_count; + let output_len = packet.output_components * lane_count; + let (inputs, outputs) = packet_slices(&mut workspace.packet, input_len, output_len)?; + let kernel = rows.kernels.get(&packet.kernel_id).ok_or_else(|| { + RusticolError::internal(format!( + "eager schedule lost closure kernel {}", + packet.kernel_id + )) + })?; + gather_closures( + items, + &kernel.inputs, + inputs, + packet.input_components, + tile_points, + workspace.tile_capacity, + &workspace.values, + &workspace.couplings, + model_parameters, + ); + backend.evaluate_batch(EagerKernelCall { + kernel_id: packet.kernel_id, + independent_block_size: 1, + lane_count, + input_component_count: packet.input_components, + output_component_count: packet.output_components, + inputs, + outputs, + })?; + scatter_closures( + items, + outputs, + lane_count, + tile_points, + workspace.tile_capacity, + &workspace.couplings, + &mut workspace.amplitudes, + ); + } + execute_direct_closures( + rows.direct_closures, + &workspace.values, + workspace.tile_capacity, + tile_points, + &mut workspace.amplitudes, + ); + Ok(()) +} + +pub(super) fn copy_tile_amplitudes( + plan: &EagerExecutionPlan, + workspace: &EagerWorkspace, + point_count: usize, + tile_start: usize, + tile_points: usize, + amplitudes: &mut [EagerComplex64], +) { + for amplitude in 0..plan.amplitude_count { + let source = amplitude * workspace.tile_capacity; + let target = amplitude * point_count + tile_start; + amplitudes[target..target + tile_points] + .copy_from_slice(&workspace.amplitudes[source..source + tile_points]); + } +} + +pub(super) fn copy_selected_tile_amplitudes( + amplitude_indices: &[usize], + workspace: &EagerWorkspace, + point_count: usize, + tile_start: usize, + tile_points: usize, + amplitudes: &mut [EagerComplex64], +) { + for amplitude in amplitude_indices { + let source = amplitude * workspace.tile_capacity; + let target = amplitude * point_count + tile_start; + amplitudes[target..target + tile_points] + .copy_from_slice(&workspace.amplitudes[source..source + tile_points]); + } +} + +#[allow(clippy::too_many_arguments)] +pub(super) fn gather_closures( + items: &[ScheduledClosure], + descriptors: &[EagerKernelInput], + inputs: &mut [EagerComplex64], + input_component_count: usize, + tile_points: usize, + tile_capacity: usize, + values: &[EagerComplex64], + couplings: &[EagerComplex64], + model_parameters: &[EagerComplex64], +) { + for (item_index, item) in items.iter().enumerate() { + let lane_start = item_index * tile_points; + let coupling = couplings[item.row.coupling_slot_id as usize]; + for (target_component, descriptor) in descriptors.iter().copied().enumerate() { + match descriptor { + EagerKernelInput::FirstCurrentComponent(component) => gather_complex_component( + values, + item.left_values, + component, + tile_capacity, + 0, + inputs, + target_component, + input_component_count, + lane_start, + tile_points, + ), + EagerKernelInput::SecondCurrentComponent(component) => gather_complex_component( + values, + item.right_values, + component, + tile_capacity, + 0, + inputs, + target_component, + input_component_count, + lane_start, + tile_points, + ), + EagerKernelInput::CouplingReal => fill_packet_component( + inputs, + target_component, + input_component_count, + lane_start, + tile_points, + EagerComplex64::new(coupling.re, 0.0), + ), + EagerKernelInput::CouplingImag => fill_packet_component( + inputs, + target_component, + input_component_count, + lane_start, + tile_points, + EagerComplex64::new(coupling.im, 0.0), + ), + EagerKernelInput::ModelParameter(parameter) => fill_packet_component( + inputs, + target_component, + input_component_count, + lane_start, + tile_points, + model_parameters[parameter as usize], + ), + _ => unreachable!("validated eager closure descriptor"), + } + } + } +} + +pub(super) fn scatter_closures( + items: &[ScheduledClosure], + outputs: &[EagerComplex64], + lane_count: usize, + tile_points: usize, + tile_capacity: usize, + couplings: &[EagerComplex64], + amplitudes: &mut [EagerComplex64], +) { + let output_components = outputs.len() / lane_count; + for (item_index, item) in items.iter().enumerate() { + let lane_start = item_index * tile_points; + let output_scale = output_factor_scale( + item.row.output_factor_source, + couplings[item.row.coupling_slot_id as usize], + ); + let factor = AccumulationFactor::from_parts( + item.row.factor_real * output_scale, + item.row.factor_imag * output_scale, + ); + let target = item.row.amplitude_index as usize * tile_capacity; + factor.scatter_strided( + item.initializes_amplitude, + outputs, + lane_start * output_components, + output_components, + amplitudes, + target, + tile_points, + ); + } +} + +pub(super) fn execute_direct_closures( + closures: &[ScheduledDirectClosure], + values: &[EagerComplex64], + tile_capacity: usize, + tile_points: usize, + amplitudes: &mut [EagerComplex64], +) { + for closure in closures { + let factor = + AccumulationFactor::from_parts(closure.row.factor_real, closure.row.factor_imag); + let target = closure.row.amplitude_index as usize * tile_capacity; + debug_assert_eq!(closure.left_values.len, closure.coefficients.len()); + debug_assert_eq!(closure.right_values.len, closure.coefficients.len()); + debug_assert!(target + tile_points <= amplitudes.len()); + // Preserve the exact per-point contraction order while replacing two + // component-index multiplications per term with linear pointer walks. + // Direct closures are numerous for small complete-color workloads. + unsafe { + let left_base = values + .as_ptr() + .add(closure.left_values.start * tile_capacity); + let right_base = values + .as_ptr() + .add(closure.right_values.start * tile_capacity); + let mut target_ptr = amplitudes.as_mut_ptr().add(target); + for point in 0..tile_points { + let mut left_ptr = left_base.add(point); + let mut right_ptr = right_base.add(point); + let mut contraction = EagerComplex64::new(0.0, 0.0); + for coefficient in &closure.coefficients { + contraction += *coefficient * *left_ptr * *right_ptr; + left_ptr = left_ptr.add(tile_capacity); + right_ptr = right_ptr.add(tile_capacity); + } + if closure.initializes_amplitude { + factor.assign(&mut *target_ptr, contraction); + } else { + factor.accumulate(&mut *target_ptr, contraction); + } + target_ptr = target_ptr.add(1); + } + } + } +} + +pub(super) fn reduce_tile( + plan: &EagerExecutionPlan, + workspace: &mut EagerWorkspace, + tile_points: usize, +) { + for (group_index, group) in plan.reduction_groups.iter().enumerate() { + let target = group_index * workspace.tile_capacity; + let (first_amplitude, remaining_amplitudes) = group + .amplitude_indices + .split_first() + .expect("validated eager reduction group is nonempty"); + let first_source = *first_amplitude as usize * workspace.tile_capacity; + workspace.reduction_groups[target..target + tile_points] + .copy_from_slice(&workspace.amplitudes[first_source..first_source + tile_points]); + for amplitude_index in remaining_amplitudes { + let source = *amplitude_index as usize * workspace.tile_capacity; + for point in 0..tile_points { + workspace.reduction_groups[target + point] += workspace.amplitudes[source + point]; + } + } + } + let (first_entry, remaining_entries) = plan + .reduction_entries + .split_first() + .expect("validated eager reduction table is nonempty"); + let left = first_entry.left_group_index as usize * workspace.tile_capacity; + let right = first_entry.right_group_index as usize * workspace.tile_capacity; + for point in 0..tile_points { + let product = workspace.reduction_groups[left + point] + * workspace.reduction_groups[right + point].conj(); + workspace.reduced[point] = (first_entry.coefficient * product).re; + } + for entry in remaining_entries { + let left = entry.left_group_index as usize * workspace.tile_capacity; + let right = entry.right_group_index as usize * workspace.tile_capacity; + for point in 0..tile_points { + let product = workspace.reduction_groups[left + point] + * workspace.reduction_groups[right + point].conj(); + workspace.reduced[point] += (entry.coefficient * product).re; + } + } +} + +pub(super) fn reduce_selected_tile( + plan: &EagerExecutionPlan, + active_group_indices: &[usize], + active_entry_indices: &[usize], + selected_group_weights: &[f64], + workspace: &mut EagerWorkspace, + tile_points: usize, +) -> RusticolResult<()> { + if selected_group_weights.len() != active_group_indices.len() * tile_points { + return Err(RusticolError::invalid_argument( + "eager selected reduction group weights have an inconsistent shape", + )); + } + workspace.reduced[..tile_points].fill(0.0); + for group_index in active_group_indices { + let group = &plan.reduction_groups[*group_index]; + let target = *group_index * workspace.tile_capacity; + let (first_amplitude, remaining_amplitudes) = group + .amplitude_indices + .split_first() + .expect("validated eager reduction group is nonempty"); + let first_source = *first_amplitude as usize * workspace.tile_capacity; + workspace.reduction_groups[target..target + tile_points] + .copy_from_slice(&workspace.amplitudes[first_source..first_source + tile_points]); + for amplitude_index in remaining_amplitudes { + let source = *amplitude_index as usize * workspace.tile_capacity; + for point in 0..tile_points { + workspace.reduction_groups[target + point] += workspace.amplitudes[source + point]; + } + } + } + for entry_index in active_entry_indices { + let entry = &plan.reduction_entries[*entry_index]; + let left_group_index = entry.left_group_index as usize; + let right_group_index = entry.right_group_index as usize; + let left = left_group_index * workspace.tile_capacity; + let right = entry.right_group_index as usize * workspace.tile_capacity; + let left_position = active_group_indices + .binary_search(&left_group_index) + .map_err(|_| { + RusticolError::integrity( + "eager selected reduction entry references an inactive left group", + ) + })?; + let right_position = active_group_indices + .binary_search(&right_group_index) + .map_err(|_| { + RusticolError::integrity( + "eager selected reduction entry references an inactive right group", + ) + })?; + for point in 0..tile_points { + let weight = selected_group_weights[left_position * tile_points + point]; + let right_weight = selected_group_weights[right_position * tile_points + point]; + if weight.to_bits() != right_weight.to_bits() { + return Err(RusticolError::integrity( + "eager selected contraction groups have inconsistent physical weights", + )); + } + let product = workspace.reduction_groups[left + point] + * workspace.reduction_groups[right + point].conj(); + workspace.reduced[point] += weight * (entry.coefficient * product).re; + } + } + Ok(()) +} + +#[allow(clippy::too_many_arguments)] +pub(super) fn copy_tile_results( + plan: &EagerExecutionPlan, + workspace: &EagerWorkspace, + point_count: usize, + tile_start: usize, + tile_points: usize, + amplitudes: &mut [EagerComplex64], + reduced: &mut [f64], +) { + copy_tile_amplitudes( + plan, + workspace, + point_count, + tile_start, + tile_points, + amplitudes, + ); + reduced[tile_start..tile_start + tile_points] + .copy_from_slice(&workspace.reduced[..tile_points]); +} + +fn packet_slices( + packet: &mut [EagerComplex64], + input_len: usize, + output_len: usize, +) -> RusticolResult<(&mut [EagerComplex64], &mut [EagerComplex64])> { + let total = input_len + .checked_add(output_len) + .ok_or_else(|| RusticolError::internal("eager active packet size overflows"))?; + if total > packet.len() { + return Err(RusticolError::internal(format!( + "eager active packet needs {total} elements, workspace has {}", + packet.len() + ))); + } + let active = &mut packet[..total]; + Ok(active.split_at_mut(input_len)) +} + +#[allow(clippy::too_many_arguments)] +fn gather_complex_component( + source: &[EagerComplex64], + range: ComponentRange, + component: u32, + source_stride: usize, + source_point_start: usize, + target: &mut [EagerComplex64], + target_component: usize, + target_component_count: usize, + target_lane_start: usize, + point_count: usize, +) { + let component = component as usize; + debug_assert!(component < range.len); + let source_start = (range.start + component) * source_stride + source_point_start; + let target_start = target_lane_start * target_component_count + target_component; + debug_assert!(source_start + point_count <= source.len()); + debug_assert!( + point_count == 0 + || target_start + (point_count - 1) * target_component_count < target.len() + ); + // The plan validator and packet sizing above establish both bounds. Raw + // pointers avoid repeated checks and index multiplication in this hot + // component-major to row-major transpose. + unsafe { + let mut source_ptr = source.as_ptr().add(source_start); + let mut target_ptr = target.as_mut_ptr().add(target_start); + for _ in 0..point_count { + *target_ptr = *source_ptr; + source_ptr = source_ptr.add(1); + target_ptr = target_ptr.add(target_component_count); + } + } +} + +#[allow(clippy::too_many_arguments)] +fn gather_real_component( + source: &[f64], + range: ComponentRange, + component: u32, + source_stride: usize, + source_point_start: usize, + target: &mut [EagerComplex64], + target_component: usize, + target_component_count: usize, + target_lane_start: usize, + point_count: usize, +) { + let component = component as usize; + debug_assert!(component < range.len); + let source_start = (range.start + component) * source_stride + source_point_start; + let target_start = target_lane_start * target_component_count + target_component; + debug_assert!(source_start + point_count <= source.len()); + debug_assert!( + point_count == 0 + || target_start + (point_count - 1) * target_component_count < target.len() + ); + // Safety follows from the validated source range and active packet size. + unsafe { + let mut source_ptr = source.as_ptr().add(source_start); + let mut target_ptr = target.as_mut_ptr().add(target_start); + for _ in 0..point_count { + *target_ptr = EagerComplex64::new(*source_ptr, 0.0); + source_ptr = source_ptr.add(1); + target_ptr = target_ptr.add(target_component_count); + } + } +} + +fn fill_packet_component( + target: &mut [EagerComplex64], + target_component: usize, + target_component_count: usize, + target_lane_start: usize, + point_count: usize, + value: EagerComplex64, +) { + let target_start = target_lane_start * target_component_count + target_component; + debug_assert!( + point_count == 0 + || target_start + (point_count - 1) * target_component_count < target.len() + ); + // Safety follows from the active packet size established by packet_slices. + unsafe { + let mut target_ptr = target.as_mut_ptr().add(target_start); + for _ in 0..point_count { + *target_ptr = value; + target_ptr = target_ptr.add(target_component_count); + } + } +} + +fn copy_component_range( + source: &[EagerComplex64], + source_range: ComponentRange, + target: &mut [EagerComplex64], + target_range: ComponentRange, + stride: usize, + point_count: usize, +) { + debug_assert_eq!(source_range.len, target_range.len); + for component in 0..source_range.len { + let source_start = (source_range.start + component) * stride; + let target_start = (target_range.start + component) * stride; + target[target_start..target_start + point_count] + .copy_from_slice(&source[source_start..source_start + point_count]); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{EagerAttachmentRow, EagerFinalizationRow, EagerInvocationRow, MISSING_U32}; + + fn c64(real: f64, imag: f64) -> EagerComplex64 { + EagerComplex64::new(real, imag) + } + + #[test] + fn packet_gather_uses_lane_major_component_rows() { + let complex_source = [ + c64(1.0, 10.0), + c64(2.0, 20.0), + c64(3.0, 30.0), + c64(4.0, 40.0), + c64(5.0, 50.0), + c64(6.0, 60.0), + ]; + let real_source = [11.0, 12.0, 13.0, 14.0, 15.0, 16.0]; + let mut packet = vec![c64(0.0, 0.0); 4 * 3]; + + gather_complex_component( + &complex_source, + ComponentRange { start: 1, len: 1 }, + 0, + 3, + 0, + &mut packet, + 1, + 3, + 1, + 2, + ); + gather_real_component( + &real_source, + ComponentRange { start: 0, len: 2 }, + 1, + 3, + 0, + &mut packet, + 0, + 3, + 1, + 2, + ); + fill_packet_component(&mut packet, 2, 3, 1, 2, c64(-1.0, 2.0)); + + assert_eq!( + packet, + vec![ + c64(0.0, 0.0), + c64(0.0, 0.0), + c64(0.0, 0.0), + c64(14.0, 0.0), + c64(4.0, 40.0), + c64(-1.0, 2.0), + c64(15.0, 0.0), + c64(5.0, 50.0), + c64(-1.0, 2.0), + c64(0.0, 0.0), + c64(0.0, 0.0), + c64(0.0, 0.0), + ] + ); + } + + #[test] + fn selected_amplitude_copy_leaves_inactive_slots_untouched() { + let workspace = EagerWorkspace { + tile_capacity: 3, + values: Vec::new(), + currents: Vec::new(), + amplitudes: vec![ + c64(1.0, 0.0), + c64(2.0, 0.0), + c64(3.0, 0.0), + c64(10.0, 0.0), + c64(20.0, 0.0), + c64(30.0, 0.0), + c64(100.0, 0.0), + c64(200.0, 0.0), + c64(300.0, 0.0), + ], + reduction_groups: Vec::new(), + couplings: Vec::new(), + reduced: Vec::new(), + packet: Vec::new(), + }; + let sentinel = c64(-99.0, 1.0); + let mut output = vec![sentinel; 3 * 5]; + + copy_selected_tile_amplitudes(&[1], &workspace, 5, 2, 2, &mut output); + + assert!(output[..5].iter().all(|value| *value == sentinel)); + assert_eq!( + output[5..10], + [sentinel, sentinel, c64(10.0, 0.0), c64(20.0, 0.0), sentinel] + ); + assert!(output[10..].iter().all(|value| *value == sentinel)); + } + + #[test] + fn proven_linear_finalization_reuses_shared_momentum_dependence() { + let linear = LinearFinalizationPacket { + momentum_groups: vec![ComponentRange { start: 0, len: 1 }], + item_group_indices: vec![0, 0], + current_components: vec![0], + }; + let descriptors = [ + EagerKernelInput::FirstCurrentComponent(0), + EagerKernelInput::FirstMomentumComponent(0), + EagerKernelInput::ModelParameter(0), + ]; + let mut inputs = vec![c64(0.0, 0.0); linear.lane_multiplier() * 2 * 3]; + gather_linear_finalizations( + &linear, + &descriptors, + &mut inputs, + 3, + 2, + 0, + 2, + &[2.0, 3.0], + &[c64(5.0, 0.0)], + ); + assert_eq!( + inputs, + [ + c64(1.0, 0.0), + c64(2.0, 0.0), + c64(5.0, 0.0), + c64(1.0, 0.0), + c64(3.0, 0.0), + c64(5.0, 0.0), + ] + ); + + let rows = [ + ScheduledFinalization { + row: EagerFinalizationRow { + kernel_id: 1, + current_id: 0, + unpropagated_value_slot_id: MISSING_U32, + propagated_value_slot_id: 0, + momentum_slot_id: 0, + }, + current: ComponentRange { start: 0, len: 1 }, + propagated: Some(ComponentRange { start: 0, len: 1 }), + momentum: ComponentRange { start: 0, len: 1 }, + selector_domain_id: None, + }, + ScheduledFinalization { + row: EagerFinalizationRow { + kernel_id: 1, + current_id: 1, + unpropagated_value_slot_id: MISSING_U32, + propagated_value_slot_id: 1, + momentum_slot_id: 0, + }, + current: ComponentRange { start: 1, len: 1 }, + propagated: Some(ComponentRange { start: 1, len: 1 }), + momentum: ComponentRange { start: 0, len: 1 }, + selector_domain_id: None, + }, + ]; + // f(current, momentum, parameter) = current * (momentum + parameter). + let outputs = [c64(7.0, 0.0), c64(8.0, 0.0)]; + let currents = [ + c64(10.0, 0.0), + c64(20.0, 0.0), + c64(30.0, 0.0), + c64(40.0, 0.0), + ]; + let mut values = vec![c64(0.0, 0.0); 4]; + scatter_linear_finalizations(&rows, &linear, &outputs, 2, 2, 2, ¤ts, &mut values) + .unwrap(); + assert_eq!( + values, + [ + c64(70.0, 0.0), + c64(160.0, 0.0), + c64(210.0, 0.0), + c64(320.0, 0.0), + ] + ); + } + + #[test] + fn row_major_scatter_specializes_exact_unit_factors() { + let invocation = ScheduledInvocation { + row: EagerInvocationRow { + kernel_id: 0, + left_value_slot_id: 0, + right_value_slot_id: 0, + left_momentum_slot_id: 0, + right_momentum_slot_id: 0, + coupling_slot_id: 0, + output_factor_source: EAGER_OUTPUT_FACTOR_NONE, + attachment_start: 0, + attachment_count: 4, + }, + left_values: ComponentRange { start: 0, len: 1 }, + right_values: ComponentRange { start: 0, len: 1 }, + left_momenta: ComponentRange { start: 0, len: 1 }, + right_momenta: ComponentRange { start: 0, len: 1 }, + attachment_range: 0..4, + selector_domain_id: None, + }; + let factors = [(1.0, 0.0), (-1.0, 0.0), (0.0, 1.0), (0.0, -1.0)]; + let attachments = factors + .into_iter() + .enumerate() + .map(|(index, (factor_real, factor_imag))| ScheduledAttachment { + row: EagerAttachmentRow { + result_current_id: index as u32, + factor_real, + factor_imag, + }, + current: ComponentRange { + start: index * 2, + len: 2, + }, + selector_domain_id: None, + initializes_current: true, + }) + .collect::>(); + let outputs = [c64(1.0, 2.0), c64(3.0, 4.0), c64(5.0, 6.0), c64(7.0, 8.0)]; + let mut currents = vec![c64(0.0, 0.0); 8 * 2]; + + scatter_invocations( + &[invocation], + &attachments, + &outputs, + 2, + 2, + 2, + &[c64(1.0, 0.0)], + &mut currents, + ); + + for (factor_index, (real, imag)) in factors.into_iter().enumerate() { + let factor = c64(real, imag); + for component in 0..2 { + for point in 0..2 { + let expected = factor * outputs[point * 2 + component]; + let actual = currents[(factor_index * 2 + component) * 2 + point]; + assert_eq!(actual, expected); + } + } + } + } + + #[test] + fn factor_specialization_keeps_generic_complex_fallback() { + let initial = c64(2.0, -3.0); + let value = c64(5.0, 7.0); + for (real, imag) in [ + (1.0, 0.0), + (-1.0, 0.0), + (0.0, 1.0), + (0.0, -1.0), + (2.5, -0.75), + ] { + let mut actual = initial; + AccumulationFactor::from_parts(real, imag).accumulate(&mut actual, value); + assert_eq!(actual, initial + c64(real, imag) * value); + } + } + + #[test] + fn signed_fanout_scales_once_and_preserves_accumulation_mode() { + let attachments = [ + ScheduledAttachment { + row: EagerAttachmentRow { + result_current_id: 0, + factor_real: 1.0, + factor_imag: 0.0, + }, + current: ComponentRange { start: 0, len: 2 }, + selector_domain_id: None, + initializes_current: true, + }, + ScheduledAttachment { + row: EagerAttachmentRow { + result_current_id: 1, + factor_real: -1.0, + factor_imag: 0.0, + }, + current: ComponentRange { start: 2, len: 2 }, + selector_domain_id: None, + initializes_current: false, + }, + ]; + let outputs = [c64(1.0, 2.0), c64(3.0, 4.0), c64(5.0, 6.0), c64(7.0, 8.0)]; + let mut currents = vec![c64(0.0, 0.0); 8]; + currents[4..].fill(c64(10.0, -10.0)); + + assert!(scatter_signed_fanout::<2, 2>( + &attachments, + &outputs, + 0, + 2.5, + &mut currents, + 2, + 2, + )); + + assert_eq!( + currents, + [ + c64(2.5, 5.0), + c64(12.5, 15.0), + c64(7.5, 10.0), + c64(17.5, 20.0), + c64(7.5, -15.0), + c64(-2.5, -25.0), + c64(2.5, -20.0), + c64(-7.5, -30.0), + ] + ); + } + + #[test] + fn invocation_output_factor_uses_live_coupling_component() { + let invocation = ScheduledInvocation { + row: EagerInvocationRow { + kernel_id: 0, + left_value_slot_id: 0, + right_value_slot_id: 0, + left_momentum_slot_id: 0, + right_momentum_slot_id: 0, + coupling_slot_id: 0, + output_factor_source: EAGER_OUTPUT_FACTOR_COUPLING_REAL, + attachment_start: 0, + attachment_count: 1, + }, + left_values: ComponentRange { start: 0, len: 1 }, + right_values: ComponentRange { start: 0, len: 1 }, + left_momenta: ComponentRange { start: 0, len: 1 }, + right_momenta: ComponentRange { start: 0, len: 1 }, + attachment_range: 0..1, + selector_domain_id: None, + }; + let attachments = [ScheduledAttachment { + row: EagerAttachmentRow { + result_current_id: 0, + factor_real: 0.0, + factor_imag: 1.0, + }, + current: ComponentRange { start: 0, len: 2 }, + selector_domain_id: None, + initializes_current: true, + }]; + let outputs = [c64(1.0, 2.0), c64(3.0, 4.0)]; + let mut currents = vec![c64(0.0, 0.0); 2]; + + scatter_invocations( + &[invocation], + &attachments, + &outputs, + 1, + 1, + 1, + &[c64(2.5, 7.0)], + &mut currents, + ); + + assert_eq!(currents, [c64(-5.0, 2.5), c64(-10.0, 7.5)]); + } +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/eager_runtime/plan.rs b/pyamplicol/rust/crates/rusticol-core/src/eager_runtime/plan.rs new file mode 100644 index 0000000000..680bb81096 --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/eager_runtime/plan.rs @@ -0,0 +1,1531 @@ +// SPDX-License-Identifier: 0BSD + +use super::{ + EagerComplex64, EagerDirectClosureSpec, EagerKernelInput, EagerKernelRole, EagerKernelSpec, + EagerPlanDefinition, EagerPlanPayloads, EagerReductionEntry, EagerReductionGroup, + EagerSelectorPayloads, EagerStagePayload, +}; +use crate::{ + EAGER_OUTPUT_FACTOR_NONE, EagerAttachmentRow, EagerClosureRow, EagerCouplingRow, + EagerFinalizationRow, EagerInvocationRow, EagerSelectorDomainIdRow, EagerSelectorDomainRow, + EagerSelectorGroupRow, MISSING_U32, RusticolError, RusticolResult, +}; +use std::collections::{BTreeMap, BTreeSet}; +use std::ops::Range; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(super) struct ComponentRange { + pub(super) start: usize, + pub(super) len: usize, +} + +#[derive(Clone, Debug)] +pub(super) struct ComponentLayout { + slots: Vec, + pub(super) component_count: usize, +} + +impl ComponentLayout { + pub(super) fn new(name: &str, component_counts: &[u32]) -> RusticolResult { + let mut slots = Vec::new(); + slots + .try_reserve_exact(component_counts.len()) + .map_err(|error| { + RusticolError::artifact(format!( + "could not reserve eager {name} slot layout: {error}" + )) + })?; + let mut start = 0usize; + for (slot_id, count) in component_counts.iter().copied().enumerate() { + let len = usize::try_from(count).map_err(|_| { + RusticolError::artifact(format!( + "eager {name} slot {slot_id} component count does not fit usize" + )) + })?; + if len == 0 { + return Err(RusticolError::artifact(format!( + "eager {name} slot {slot_id} has zero components" + ))); + } + slots.push(ComponentRange { start, len }); + start = start.checked_add(len).ok_or_else(|| { + RusticolError::artifact(format!("eager {name} component layout overflows usize")) + })?; + } + Ok(Self { + slots, + component_count: start, + }) + } + + pub(super) fn get(&self, id: u32, context: &str) -> RusticolResult { + let index = usize::try_from(id).map_err(|_| { + RusticolError::artifact(format!("{context} id {id} does not fit usize")) + })?; + self.slots.get(index).copied().ok_or_else(|| { + RusticolError::artifact(format!("{context} references unknown slot {id}")) + }) + } + + pub(super) fn id_for_range(&self, range: ComponentRange, context: &str) -> RusticolResult { + self.slots + .iter() + .position(|candidate| *candidate == range) + .and_then(|id| u32::try_from(id).ok()) + .ok_or_else(|| { + RusticolError::internal(format!( + "{context} has no matching semantic component range" + )) + }) + } +} + +#[derive(Clone, Debug)] +pub(super) struct ScheduledInvocation { + pub(super) row: EagerInvocationRow, + pub(super) left_values: ComponentRange, + pub(super) right_values: ComponentRange, + pub(super) left_momenta: ComponentRange, + pub(super) right_momenta: ComponentRange, + pub(super) attachment_range: Range, + pub(super) selector_domain_id: Option, +} + +#[derive(Clone, Copy, Debug)] +pub(super) struct ScheduledAttachment { + pub(super) row: EagerAttachmentRow, + pub(super) current: ComponentRange, + pub(super) selector_domain_id: Option, + pub(super) initializes_current: bool, +} + +#[derive(Clone, Copy, Debug)] +pub(super) struct FinalizationCopy { + pub(super) current: ComponentRange, + pub(super) unpropagated: ComponentRange, + pub(super) selector_domain_id: Option, +} + +#[derive(Clone, Debug)] +pub(super) struct ScheduledFinalization { + pub(super) row: EagerFinalizationRow, + pub(super) current: ComponentRange, + pub(super) propagated: Option, + pub(super) momentum: ComponentRange, + pub(super) selector_domain_id: Option, +} + +#[derive(Clone, Debug)] +pub(super) struct ScheduledClosure { + pub(super) row: EagerClosureRow, + pub(super) left_values: ComponentRange, + pub(super) right_values: ComponentRange, + pub(super) selector_domain_id: Option, + pub(super) initializes_amplitude: bool, +} + +#[derive(Clone, Debug)] +pub(super) struct ScheduledDirectClosure { + pub(super) row: EagerClosureRow, + pub(super) left_values: ComponentRange, + pub(super) right_values: ComponentRange, + pub(super) coefficients: Vec, + pub(super) selector_domain_id: Option, + pub(super) initializes_amplitude: bool, +} + +#[derive(Clone, Copy)] +pub(super) struct ClosureExecutionRows<'a> { + pub(super) closures: &'a [ScheduledClosure], + pub(super) direct_closures: &'a [ScheduledDirectClosure], + pub(super) kernels: &'a BTreeMap, +} + +#[derive(Clone, Debug)] +pub(super) struct SelectorDomainPlan { + pub(super) memberships: Vec>, + pub(super) group_ids: Vec, +} + +#[derive(Clone, Debug)] +struct DecodedSelectorStage { + stage_index: u32, + invocation_domains: Vec, + attachment_domains: Vec, + unpropagated_finalization_domains: Vec, + propagated_finalization_domains: Vec, +} + +#[derive(Clone, Debug)] +struct DecodedSelectorPayload { + plan: SelectorDomainPlan, + stages: Vec, + closure_domains: Vec, +} + +#[derive(Clone, Debug)] +pub(super) struct EagerStagePlan { + pub(super) stage_index: u32, + pub(super) current_component_count: usize, + pub(super) invocations: Vec, + pub(super) attachments: Vec, + pub(super) finalization_copies: Vec, + pub(super) finalizations: Vec, + pub(super) zero_current_ranges: Vec, +} + +#[derive(Clone, Debug)] +pub struct EagerExecutionPlan { + pub(super) values: ComponentLayout, + pub(super) momenta: ComponentLayout, + pub(super) currents: ComponentLayout, + pub(super) parameter_count: usize, + pub(super) amplitude_count: usize, + pub(super) kernels: BTreeMap, + pub(super) couplings: Vec, + pub(super) stages: Vec, + pub(super) closures: Vec, + pub(super) direct_closures: Vec, + pub(super) reduction_groups: Vec, + pub(super) reduction_entries: Vec, + pub(super) selector_domains: Option, + pub(super) initial_value_ranges: Vec, + pub(super) zero_amplitude_indices: Vec, +} + +impl EagerExecutionPlan { + pub fn from_payloads( + definition: EagerPlanDefinition, + payloads: EagerPlanPayloads<'_>, + ) -> RusticolResult { + let values = + ComponentLayout::new("value", &definition.dimensions.value_slot_component_counts)?; + let momenta = ComponentLayout::new( + "momentum", + &definition.dimensions.momentum_slot_component_counts, + )?; + let currents = + ComponentLayout::new("current", &definition.dimensions.current_component_counts)?; + let parameter_count = usize::try_from(definition.dimensions.parameter_count) + .map_err(|_| RusticolError::artifact("eager parameter count does not fit usize"))?; + let amplitude_count = usize::try_from(definition.dimensions.amplitude_count) + .map_err(|_| RusticolError::artifact("eager amplitude count does not fit usize"))?; + if amplitude_count == 0 { + return Err(RusticolError::artifact( + "eager execution plan has no amplitude outputs", + )); + } + + let kernels = validate_kernel_specs(&definition.kernels, parameter_count)?; + let couplings = EagerCouplingRow::decode_table(payloads.couplings)?; + validate_couplings(&couplings, parameter_count)?; + let selector_payload = + decode_selector_payloads(payloads.selector_domains, payloads.stages)?; + + let mut stages = Vec::new(); + stages + .try_reserve_exact(payloads.stages.len()) + .map_err(|error| { + RusticolError::artifact(format!("could not reserve eager stages: {error}")) + })?; + let mut previous_stage = None; + let mut finalized_currents = BTreeSet::new(); + let mut stored_value_slots = BTreeSet::new(); + for (stage_position, payload) in payloads.stages.iter().enumerate() { + if previous_stage.is_some_and(|previous| payload.stage_index <= previous) { + return Err(RusticolError::artifact( + "eager stage indices must be strictly increasing", + )); + } + previous_stage = Some(payload.stage_index); + stages.push(load_stage( + *payload, + selector_payload + .as_ref() + .map(|selector| &selector.stages[stage_position]), + selector_payload.as_ref().map(|selector| &selector.plan), + &values, + &momenta, + ¤ts, + &kernels, + couplings.len(), + parameter_count, + &mut finalized_currents, + &mut stored_value_slots, + )?); + } + + let closure_rows = EagerClosureRow::decode_table(payloads.closures)?; + let closure_domains = selector_payload + .as_ref() + .map(|selector| selector.closure_domains.as_slice()); + if closure_domains.is_some_and(|domains| domains.len() != closure_rows.len()) { + return Err(RusticolError::artifact( + "eager closure selector-domain count does not match the closure table", + )); + } + let (mut closures, mut direct_closures) = load_closures( + &closure_rows, + closure_domains, + &definition.direct_closures, + &values, + &kernels, + couplings.len(), + amplitude_count, + parameter_count, + )?; + validate_reduction_plan( + &definition.reduction_groups, + &definition.reduction_entries, + amplitude_count, + )?; + if let Some(selector) = selector_payload.as_ref() { + validate_selector_dependency_proof( + selector, + payloads.stages, + &closure_rows, + &definition.reduction_groups, + amplitude_count, + )?; + } + + let initial_value_ranges = values + .slots + .iter() + .enumerate() + .filter_map(|(slot_id, range)| { + (!stored_value_slots.contains(&(slot_id as u32))).then_some(*range) + }) + .collect(); + let zero_amplitude_indices = + mark_initial_amplitude_writes(&mut closures, &mut direct_closures, amplitude_count); + + Ok(Self { + values, + momenta, + currents, + parameter_count, + amplitude_count, + kernels, + couplings, + stages, + closures, + direct_closures, + reduction_groups: definition.reduction_groups, + reduction_entries: definition.reduction_entries, + selector_domains: selector_payload.map(|selector| selector.plan), + initial_value_ranges, + zero_amplitude_indices, + }) + } + + pub fn value_component_count(&self) -> usize { + self.values.component_count + } + + pub fn momentum_component_count(&self) -> usize { + self.momenta.component_count + } + + pub fn current_component_count(&self) -> usize { + self.currents.component_count + } + + pub(super) fn current_workspace_component_count(&self) -> usize { + self.stages + .iter() + .map(|stage| stage.current_component_count) + .max() + .unwrap_or(0) + } + + pub fn parameter_count(&self) -> usize { + self.parameter_count + } + + pub fn amplitude_count(&self) -> usize { + self.amplitude_count + } + + pub fn stage_count(&self) -> usize { + self.stages.len() + } + + pub fn stage_indices(&self) -> impl ExactSizeIterator + '_ { + self.stages.iter().map(|stage| stage.stage_index) + } + + pub fn invocation_count(&self) -> usize { + self.stages + .iter() + .map(|stage| stage.invocations.len()) + .sum() + } + + pub fn attachment_count(&self) -> usize { + self.stages + .iter() + .map(|stage| stage.attachments.len()) + .sum() + } + + pub fn closure_count(&self) -> usize { + self.closures.len() + self.direct_closures.len() + } + + pub fn reduction_group_count(&self) -> usize { + self.reduction_groups.len() + } + + pub fn reduction_entry_count(&self) -> usize { + self.reduction_entries.len() + } + + pub fn has_selector_domains(&self) -> bool { + self.selector_domains.is_some() + } + + pub(crate) fn selector_group_ids(&self) -> Option<&[u32]> { + Some(&self.selector_domains.as_ref()?.group_ids) + } +} + +fn decode_selector_payloads( + payloads: Option>, + execution_stages: &[EagerStagePayload<'_>], +) -> RusticolResult> { + let Some(payloads) = payloads else { + return Ok(None); + }; + if payloads.stages.len() != execution_stages.len() { + return Err(RusticolError::artifact(format!( + "eager selector domains cover {} stages, expected {}", + payloads.stages.len(), + execution_stages.len() + ))); + } + + let domain_rows = EagerSelectorDomainRow::decode_table(payloads.domains)?; + let group_rows = EagerSelectorGroupRow::decode_table(payloads.domain_group_ids)?; + if domain_rows.is_empty() { + return Err(RusticolError::artifact( + "eager selector-domain table is empty", + )); + } + let mut memberships = Vec::new(); + memberships + .try_reserve_exact(domain_rows.len()) + .map_err(|error| { + RusticolError::artifact(format!("could not reserve eager selector domains: {error}")) + })?; + let mut cursor = 0usize; + let mut unique_memberships = BTreeSet::new(); + for (domain_id, row) in domain_rows.into_iter().enumerate() { + let start = usize::try_from(row.member_start).map_err(|_| { + RusticolError::artifact(format!( + "eager selector domain {domain_id} start does not fit usize" + )) + })?; + let count = usize::try_from(row.member_count).map_err(|_| { + RusticolError::artifact(format!( + "eager selector domain {domain_id} count does not fit usize" + )) + })?; + if start != cursor { + return Err(RusticolError::artifact(format!( + "eager selector domain {domain_id} starts at {start}, expected {cursor}" + ))); + } + let stop = start.checked_add(count).ok_or_else(|| { + RusticolError::artifact(format!( + "eager selector domain {domain_id} range overflows usize" + )) + })?; + if stop > group_rows.len() { + return Err(RusticolError::artifact(format!( + "eager selector domain {domain_id} exceeds its membership table" + ))); + } + let members = group_rows[start..stop] + .iter() + .map(|row| row.coherent_group_id) + .collect::>(); + if members.windows(2).any(|pair| pair[0] >= pair[1]) { + return Err(RusticolError::artifact(format!( + "eager selector domain {domain_id} members are not sorted and unique" + ))); + } + if !unique_memberships.insert(members.clone()) { + return Err(RusticolError::artifact(format!( + "eager selector domain {domain_id} duplicates an earlier membership" + ))); + } + memberships.push(members); + cursor = stop; + } + if cursor != group_rows.len() { + return Err(RusticolError::artifact( + "eager selector domains do not cover their membership table", + )); + } + if !memberships.iter().any(Vec::is_empty) { + return Err(RusticolError::artifact( + "eager selector domains do not define the empty domain", + )); + } + let domain_count = memberships.len(); + let mut group_ids = memberships + .iter() + .flat_map(|members| members.iter().copied()) + .collect::>(); + group_ids.sort_unstable(); + group_ids.dedup(); + + let mut stages = Vec::new(); + stages + .try_reserve_exact(payloads.stages.len()) + .map_err(|error| { + RusticolError::artifact(format!("could not reserve eager selector stages: {error}")) + })?; + for (position, (selector, execution)) in + payloads.stages.iter().zip(execution_stages).enumerate() + { + if selector.stage_index != execution.stage_index { + return Err(RusticolError::artifact(format!( + "eager selector stage {} at position {position} does not match execution stage {}", + selector.stage_index, execution.stage_index + ))); + } + stages.push(DecodedSelectorStage { + stage_index: selector.stage_index, + invocation_domains: decode_domain_ids( + selector.invocation_domains, + domain_count, + "invocation", + )?, + attachment_domains: decode_domain_ids( + selector.attachment_domains, + domain_count, + "attachment", + )?, + unpropagated_finalization_domains: decode_domain_ids( + selector.unpropagated_finalization_domains, + domain_count, + "unpropagated finalization", + )?, + propagated_finalization_domains: decode_domain_ids( + selector.propagated_finalization_domains, + domain_count, + "propagated finalization", + )?, + }); + } + let closure_domains = decode_domain_ids(payloads.closure_domains, domain_count, "closure")?; + Ok(Some(DecodedSelectorPayload { + plan: SelectorDomainPlan { + memberships, + group_ids, + }, + stages, + closure_domains, + })) +} + +fn decode_domain_ids( + payload: &[u8], + domain_count: usize, + context: &str, +) -> RusticolResult> { + let rows = EagerSelectorDomainIdRow::decode_table(payload)?; + let mut result = Vec::with_capacity(rows.len()); + for (index, row) in rows.into_iter().enumerate() { + if usize::try_from(row.domain_id) + .ok() + .is_none_or(|domain_id| domain_id >= domain_count) + { + return Err(RusticolError::artifact(format!( + "eager {context} selector row {index} references unknown domain {}", + row.domain_id + ))); + } + result.push(row.domain_id); + } + Ok(result) +} + +fn validate_selector_dependency_proof( + selector: &DecodedSelectorPayload, + stage_payloads: &[EagerStagePayload<'_>], + closures: &[EagerClosureRow], + reduction_groups: &[EagerReductionGroup], + amplitude_count: usize, +) -> RusticolResult<()> { + let mut group_by_amplitude = vec![None; amplitude_count]; + let known_groups = reduction_groups + .iter() + .map(|group| group.coherent_group_id) + .collect::>(); + for group in reduction_groups { + for amplitude_index in &group.amplitude_indices { + let index = usize::try_from(*amplitude_index).map_err(|_| { + RusticolError::artifact("eager reduction amplitude index does not fit usize") + })?; + group_by_amplitude[index] = Some(group.coherent_group_id); + } + } + for members in &selector.plan.memberships { + if let Some(unknown) = members.iter().find(|group| !known_groups.contains(group)) { + return Err(RusticolError::artifact(format!( + "eager selector domain references unknown coherent group {unknown}" + ))); + } + } + + let mut value_domains = BTreeMap::>::new(); + for (index, closure) in closures.iter().enumerate() { + let amplitude_index = required_index( + closure.amplitude_index, + amplitude_count, + "eager closure amplitude", + )?; + let group_id = group_by_amplitude[amplitude_index].ok_or_else(|| { + RusticolError::artifact(format!( + "eager closure amplitude {amplitude_index} has no coherent group" + )) + })?; + let expected = BTreeSet::from([group_id]); + validate_selector_domain_members( + &selector.plan, + selector.closure_domains[index], + &expected, + &format!("eager closure {index}"), + )?; + value_domains + .entry(closure.left_value_slot_id) + .or_default() + .insert(group_id); + value_domains + .entry(closure.right_value_slot_id) + .or_default() + .insert(group_id); + } + + for (payload, selector_stage) in stage_payloads.iter().zip(&selector.stages).rev() { + let invocations = EagerInvocationRow::decode_table(payload.invocations)?; + let attachments = EagerAttachmentRow::decode_table(payload.attachments)?; + let finalizations = EagerFinalizationRow::decode_table(payload.finalizations)?; + let mut current_domains = BTreeMap::>::new(); + + for (index, finalization) in finalizations.iter().enumerate() { + let unpropagated = if finalization.stores_unpropagated() { + value_domains + .get(&finalization.unpropagated_value_slot_id) + .cloned() + .unwrap_or_default() + } else { + BTreeSet::new() + }; + validate_selector_domain_members( + &selector.plan, + selector_stage.unpropagated_finalization_domains[index], + &unpropagated, + &format!( + "eager stage {} unpropagated finalization {index}", + payload.stage_index + ), + )?; + let propagated = if finalization.stores_propagated() { + value_domains + .get(&finalization.propagated_value_slot_id) + .cloned() + .unwrap_or_default() + } else { + BTreeSet::new() + }; + validate_selector_domain_members( + &selector.plan, + selector_stage.propagated_finalization_domains[index], + &propagated, + &format!( + "eager stage {} propagated finalization {index}", + payload.stage_index + ), + )?; + let mut current = unpropagated; + current.extend(propagated); + current_domains.insert(finalization.current_id, current); + } + + let mut attachment_domains = Vec::with_capacity(attachments.len()); + for (index, attachment) in attachments.iter().enumerate() { + let expected = current_domains + .get(&attachment.result_current_id) + .ok_or_else(|| { + RusticolError::artifact(format!( + "eager stage {} attachment {index} has no finalized current", + payload.stage_index + )) + })?; + validate_selector_domain_members( + &selector.plan, + selector_stage.attachment_domains[index], + expected, + &format!("eager stage {} attachment {index}", payload.stage_index), + )?; + attachment_domains.push(expected.clone()); + } + + for (index, invocation) in invocations.iter().enumerate() { + let start = usize::try_from(invocation.attachment_start).map_err(|_| { + RusticolError::artifact("eager invocation attachment start does not fit usize") + })?; + let count = usize::try_from(invocation.attachment_count).map_err(|_| { + RusticolError::artifact("eager invocation attachment count does not fit usize") + })?; + let stop = start.checked_add(count).ok_or_else(|| { + RusticolError::artifact("eager invocation attachment range overflows usize") + })?; + let mut expected = BTreeSet::new(); + for domain in attachment_domains.get(start..stop).ok_or_else(|| { + RusticolError::artifact("eager invocation attachment range exceeds its table") + })? { + expected.extend(domain); + } + validate_selector_domain_members( + &selector.plan, + selector_stage.invocation_domains[index], + &expected, + &format!("eager stage {} invocation {index}", payload.stage_index), + )?; + value_domains + .entry(invocation.left_value_slot_id) + .or_default() + .extend(&expected); + value_domains + .entry(invocation.right_value_slot_id) + .or_default() + .extend(expected); + } + } + Ok(()) +} + +fn validate_selector_domain_members( + plan: &SelectorDomainPlan, + domain_id: u32, + expected: &BTreeSet, + context: &str, +) -> RusticolResult<()> { + let actual = plan + .memberships + .get(domain_id as usize) + .ok_or_else(|| RusticolError::internal("selector domain escaped payload validation"))?; + if !actual.iter().copied().eq(expected.iter().copied()) { + return Err(RusticolError::artifact(format!( + "{context} selector domain does not match its proven dependency closure" + ))); + } + Ok(()) +} + +fn validate_kernel_specs( + specs: &[EagerKernelSpec], + parameter_count: usize, +) -> RusticolResult> { + let mut kernels = BTreeMap::new(); + for (index, spec) in specs.iter().cloned().enumerate() { + if spec.kernel_id == MISSING_U32 { + return Err(RusticolError::artifact(format!( + "eager kernel spec {index} uses the reserved missing id" + ))); + } + if spec.inputs.is_empty() || spec.output_component_count == 0 { + return Err(RusticolError::artifact(format!( + "eager kernel {} has a zero input or output width", + spec.kernel_id + ))); + } + if spec.independent_block_size != 1 { + if spec.independent_block_size != super::EAGER_INDEPENDENT_BLOCK_SIZE { + return Err(RusticolError::artifact(format!( + "eager kernel {} has unsupported independent block size {}", + spec.kernel_id, spec.independent_block_size + ))); + } + if spec.role != EagerKernelRole::Vertex + || spec.inputs.iter().any(|input| { + !matches!( + input, + EagerKernelInput::FirstCurrentComponent(_) + | EagerKernelInput::SecondCurrentComponent(_) + ) + }) + { + return Err(RusticolError::artifact(format!( + "eager kernel {} applies an independent block to a non-current vertex contract", + spec.kernel_id + ))); + } + } + let mut unique_inputs = BTreeSet::new(); + for input in &spec.inputs { + if !unique_inputs.insert(*input) { + return Err(RusticolError::artifact(format!( + "eager kernel {} repeats input descriptor {input:?}", + spec.kernel_id + ))); + } + if let EagerKernelInput::ModelParameter(parameter_id) = input { + required_index( + *parameter_id, + parameter_count, + &format!("eager kernel {} model parameter", spec.kernel_id), + )?; + } + } + let kernel_id = spec.kernel_id; + if kernels.insert(kernel_id, spec).is_some() { + return Err(RusticolError::artifact(format!( + "duplicate eager kernel id {kernel_id}" + ))); + } + } + Ok(kernels) +} + +fn validate_couplings(rows: &[EagerCouplingRow], parameter_count: usize) -> RusticolResult<()> { + for (index, row) in rows.iter().enumerate() { + validate_optional_index( + row.real_parameter_id, + parameter_count, + &format!("eager coupling {index} real parameter"), + )?; + validate_optional_index( + row.imag_parameter_id, + parameter_count, + &format!("eager coupling {index} imaginary parameter"), + )?; + } + Ok(()) +} + +#[allow(clippy::too_many_arguments)] +fn load_stage( + payload: EagerStagePayload<'_>, + selector_stage: Option<&DecodedSelectorStage>, + selector_plan: Option<&SelectorDomainPlan>, + values: &ComponentLayout, + momenta: &ComponentLayout, + currents: &ComponentLayout, + kernels: &BTreeMap, + coupling_count: usize, + parameter_count: usize, + globally_finalized_currents: &mut BTreeSet, + globally_stored_value_slots: &mut BTreeSet, +) -> RusticolResult { + let rows = EagerInvocationRow::decode_table(payload.invocations)?; + let attachments = EagerAttachmentRow::decode_table(payload.attachments)?; + let finalization_rows = EagerFinalizationRow::decode_table(payload.finalizations)?; + if let Some(selector) = selector_stage { + if selector.stage_index != payload.stage_index { + return Err(RusticolError::artifact(format!( + "eager selector stage {} does not match execution stage {}", + selector.stage_index, payload.stage_index + ))); + } + for (kind, actual, expected) in [ + ("invocation", selector.invocation_domains.len(), rows.len()), + ( + "attachment", + selector.attachment_domains.len(), + attachments.len(), + ), + ( + "unpropagated finalization", + selector.unpropagated_finalization_domains.len(), + finalization_rows.len(), + ), + ( + "propagated finalization", + selector.propagated_finalization_domains.len(), + finalization_rows.len(), + ), + ] { + if actual != expected { + return Err(RusticolError::artifact(format!( + "eager stage {} {kind} selector-domain count is {actual}, expected {expected}", + payload.stage_index + ))); + } + } + } + let mut invocations = Vec::new(); + invocations.try_reserve_exact(rows.len()).map_err(|error| { + RusticolError::artifact(format!("could not reserve eager invocations: {error}")) + })?; + let mut attachment_cursor = 0usize; + let mut attached_currents = BTreeSet::new(); + let mut scheduled_attachments = Vec::new(); + scheduled_attachments + .try_reserve_exact(attachments.len()) + .map_err(|error| { + RusticolError::artifact(format!("could not reserve eager attachments: {error}")) + })?; + + for (index, row) in rows.into_iter().enumerate() { + let kernel = require_kernel( + kernels, + row.kernel_id, + EagerKernelRole::Vertex, + "invocation", + )?; + let left_values = values.get(row.left_value_slot_id, "eager invocation left value")?; + let right_values = values.get(row.right_value_slot_id, "eager invocation right value")?; + let left_momenta = + momenta.get(row.left_momentum_slot_id, "eager invocation left momentum")?; + let right_momenta = momenta.get( + row.right_momentum_slot_id, + "eager invocation right momentum", + )?; + let coupling_index = required_index( + row.coupling_slot_id, + coupling_count, + "eager invocation coupling", + )?; + let _ = coupling_index; + let attachment_start = usize::try_from(row.attachment_start).map_err(|_| { + RusticolError::artifact(format!( + "eager invocation {index} attachment start does not fit usize" + )) + })?; + let attachment_count = usize::try_from(row.attachment_count).map_err(|_| { + RusticolError::artifact(format!( + "eager invocation {index} attachment count does not fit usize" + )) + })?; + if attachment_count == 0 { + return Err(RusticolError::artifact(format!( + "eager invocation {index} has no attachments" + ))); + } + if attachment_start != attachment_cursor { + return Err(RusticolError::artifact(format!( + "eager invocation {index} attachment range is not contiguous" + ))); + } + let attachment_end = attachment_start + .checked_add(attachment_count) + .ok_or_else(|| { + RusticolError::artifact(format!( + "eager invocation {index} attachment range overflows usize" + )) + })?; + if attachment_end > attachments.len() { + return Err(RusticolError::artifact(format!( + "eager invocation {index} attachment range exceeds the table" + ))); + } + validate_kernel_inputs( + kernel, + KernelInputBounds { + first_current: left_values.len, + second_current: right_values.len, + first_momentum: left_momenta.len, + second_momentum: right_momenta.len, + has_coupling: true, + parameter_count, + }, + )?; + if let (Some(selector), Some(domains)) = (selector_stage, selector_plan) { + let expected_members = selector.attachment_domains[attachment_start..attachment_end] + .iter() + .flat_map(|domain_id| domains.memberships[*domain_id as usize].iter().copied()) + .collect::>(); + let invocation_members = domains.memberships + [selector.invocation_domains[index] as usize] + .iter() + .copied() + .collect::>(); + if invocation_members != expected_members { + return Err(RusticolError::artifact(format!( + "eager invocation {index} selector domain does not equal its attachment union" + ))); + } + } + for (attachment_index, attachment) in attachments[attachment_start..attachment_end] + .iter() + .enumerate() + { + let attachment_index = attachment_start + attachment_index; + let current = currents.get( + attachment.result_current_id, + "eager attachment result current", + )?; + if current.len + != usize::try_from(kernel.output_component_count).map_err(|_| { + RusticolError::artifact("eager kernel output width does not fit usize") + })? + { + return Err(RusticolError::artifact(format!( + "eager invocation kernel {} output width does not match current {}", + kernel.kernel_id, attachment.result_current_id + ))); + } + attached_currents.insert(attachment.result_current_id); + scheduled_attachments.push(ScheduledAttachment { + row: *attachment, + current, + selector_domain_id: selector_stage + .map(|selector| selector.attachment_domains[attachment_index]), + initializes_current: false, + }); + } + invocations.push(ScheduledInvocation { + row, + left_values, + right_values, + left_momenta, + right_momenta, + attachment_range: attachment_start..attachment_end, + selector_domain_id: selector_stage.map(|selector| selector.invocation_domains[index]), + }); + attachment_cursor = attachment_end; + } + if attachment_cursor != attachments.len() { + return Err(RusticolError::artifact( + "eager invocation ranges do not cover the attachment table", + )); + } + + let mut finalizations = Vec::new(); + let mut finalization_copies = Vec::new(); + let mut stage_current_ranges = BTreeMap::new(); + let mut current_component_count = 0usize; + finalizations + .try_reserve_exact(finalization_rows.len()) + .map_err(|error| { + RusticolError::artifact(format!("could not reserve eager finalizations: {error}")) + })?; + let mut finalized_currents = BTreeSet::new(); + for (index, row) in finalization_rows.into_iter().enumerate() { + if !finalized_currents.insert(row.current_id) { + return Err(RusticolError::artifact(format!( + "eager current {} is finalized more than once in stage {}", + row.current_id, payload.stage_index + ))); + } + if !globally_finalized_currents.insert(row.current_id) { + return Err(RusticolError::artifact(format!( + "eager current {} is finalized in more than one stage", + row.current_id + ))); + } + if row.unpropagated_value_slot_id != MISSING_U32 + && row.unpropagated_value_slot_id == row.propagated_value_slot_id + { + return Err(RusticolError::artifact(format!( + "eager finalization {index} aliases propagated and unpropagated outputs" + ))); + } + for value_slot_id in [row.unpropagated_value_slot_id, row.propagated_value_slot_id] { + if value_slot_id != MISSING_U32 && !globally_stored_value_slots.insert(value_slot_id) { + return Err(RusticolError::artifact(format!( + "eager value slot {value_slot_id} is written by more than one finalization" + ))); + } + } + let current = currents.get(row.current_id, "eager finalization current")?; + let local_current = ComponentRange { + start: current_component_count, + len: current.len, + }; + current_component_count = current_component_count + .checked_add(current.len) + .ok_or_else(|| { + RusticolError::artifact(format!( + "eager stage {} current workspace overflows usize", + payload.stage_index + )) + })?; + stage_current_ranges.insert(row.current_id, local_current); + let unpropagated = optional_component_range( + values, + row.unpropagated_value_slot_id, + "eager unpropagated value", + )?; + let propagated = optional_component_range( + values, + row.propagated_value_slot_id, + "eager propagated value", + )?; + if unpropagated.is_none() && propagated.is_none() { + return Err(RusticolError::artifact(format!( + "eager finalization {index} stores no current value" + ))); + } + for output in [unpropagated, propagated].into_iter().flatten() { + if output.len != current.len { + return Err(RusticolError::artifact(format!( + "eager finalization {index} output width does not match current width" + ))); + } + } + let momentum = momenta.get(row.momentum_slot_id, "eager finalization momentum")?; + if let Some(unpropagated) = unpropagated { + let selector_domain_id = + selector_stage.map(|selector| selector.unpropagated_finalization_domains[index]); + finalization_copies.push(FinalizationCopy { + current: local_current, + unpropagated, + selector_domain_id, + }); + } else if let (Some(selector), Some(domains)) = (selector_stage, selector_plan) + && !domains.memberships[selector.unpropagated_finalization_domains[index] as usize] + .is_empty() + { + return Err(RusticolError::artifact(format!( + "missing eager unpropagated output {index} has a nonempty selector domain" + ))); + } + if row.applies_kernel() { + let kernel = require_kernel( + kernels, + row.kernel_id, + EagerKernelRole::Finalization, + "finalization", + )?; + if propagated.is_none() { + return Err(RusticolError::artifact(format!( + "eager finalization {index} applies a kernel without a propagated output" + ))); + } + validate_kernel_inputs( + kernel, + KernelInputBounds { + first_current: current.len, + second_current: 0, + first_momentum: momentum.len, + second_momentum: 0, + has_coupling: false, + parameter_count, + }, + )?; + if usize::try_from(kernel.output_component_count).ok() != Some(current.len) { + return Err(RusticolError::artifact(format!( + "eager finalization kernel {} output width does not match current width", + kernel.kernel_id + ))); + } + finalizations.push(ScheduledFinalization { + row, + current: local_current, + propagated, + momentum, + selector_domain_id: selector_stage + .map(|selector| selector.propagated_finalization_domains[index]), + }); + } else if propagated.is_some() { + return Err(RusticolError::artifact(format!( + "eager finalization {index} has a propagated output but no kernel" + ))); + } else if let (Some(selector), Some(domains)) = (selector_stage, selector_plan) + && !domains.memberships[selector.propagated_finalization_domains[index] as usize] + .is_empty() + { + return Err(RusticolError::artifact(format!( + "missing eager propagated output {index} has a nonempty selector domain" + ))); + } + } + if !attached_currents.is_subset(&finalized_currents) { + let missing = attached_currents + .difference(&finalized_currents) + .next() + .copied() + .unwrap_or(MISSING_U32); + return Err(RusticolError::artifact(format!( + "eager stage {} does not finalize attached current {missing}", + payload.stage_index + ))); + } + if !finalized_currents.is_subset(&attached_currents) { + let unwritten = finalized_currents + .difference(&attached_currents) + .next() + .copied() + .unwrap_or(MISSING_U32); + return Err(RusticolError::artifact(format!( + "eager stage {} finalizes unwritten current {unwritten}", + payload.stage_index + ))); + } + + for attachment in &mut scheduled_attachments { + attachment.current = *stage_current_ranges + .get(&attachment.row.result_current_id) + .ok_or_else(|| { + RusticolError::internal(format!( + "eager stage {} lost current {} while compacting its workspace", + payload.stage_index, attachment.row.result_current_id + )) + })?; + } + + // Stable sorting makes equal-kernel rows contiguous for packetization while + // preserving artifact order, which defines deterministic lane ordering. + invocations.sort_by_key(|item| item.row.kernel_id); + finalizations.sort_by_key(|item| item.row.kernel_id); + let zero_current_ranges = + mark_initial_current_writes(&invocations, &mut scheduled_attachments, &finalizations); + Ok(EagerStagePlan { + stage_index: payload.stage_index, + current_component_count, + invocations, + attachments: scheduled_attachments, + finalization_copies, + finalizations, + zero_current_ranges, + }) +} + +#[allow(clippy::too_many_arguments)] +fn load_closures( + rows: &[EagerClosureRow], + selector_domain_ids: Option<&[u32]>, + direct_specs: &[EagerDirectClosureSpec], + values: &ComponentLayout, + kernels: &BTreeMap, + coupling_count: usize, + amplitude_count: usize, + parameter_count: usize, +) -> RusticolResult<(Vec, Vec)> { + let mut direct_by_index = BTreeMap::new(); + for spec in direct_specs { + let index = usize::try_from(spec.closure_index).map_err(|_| { + RusticolError::artifact("eager direct closure index does not fit usize") + })?; + if spec.coefficients.is_empty() { + return Err(RusticolError::artifact(format!( + "eager direct closure {index} has no coefficients" + ))); + } + if spec + .coefficients + .iter() + .any(|coefficient| !complex_is_finite(*coefficient)) + { + return Err(RusticolError::artifact(format!( + "eager direct closure {index} has a non-finite coefficient" + ))); + } + if direct_by_index.insert(index, spec).is_some() { + return Err(RusticolError::artifact(format!( + "duplicate eager direct closure specification {index}" + ))); + } + } + + let mut closures = Vec::new(); + let mut direct_closures = Vec::new(); + for (index, row) in rows.iter().copied().enumerate() { + let left_values = values.get(row.left_value_slot_id, "eager closure left value")?; + let right_values = values.get(row.right_value_slot_id, "eager closure right value")?; + required_index( + row.amplitude_index, + amplitude_count, + "eager closure amplitude", + )?; + if row.kernel_id == MISSING_U32 { + if row.coupling_slot_id != MISSING_U32 { + return Err(RusticolError::artifact(format!( + "eager direct closure {index} unexpectedly references a coupling" + ))); + } + if row.output_factor_source != EAGER_OUTPUT_FACTOR_NONE { + return Err(RusticolError::artifact(format!( + "eager direct closure {index} has a dynamic output factor" + ))); + } + let spec = direct_by_index.remove(&index).ok_or_else(|| { + RusticolError::artifact(format!( + "eager direct closure {index} lacks contraction coefficients" + )) + })?; + if left_values.len != right_values.len || left_values.len != spec.coefficients.len() { + return Err(RusticolError::artifact(format!( + "eager direct closure {index} component widths do not match" + ))); + } + direct_closures.push(ScheduledDirectClosure { + row, + left_values, + right_values, + coefficients: spec.coefficients.clone(), + selector_domain_id: selector_domain_ids.map(|domains| domains[index]), + initializes_amplitude: false, + }); + continue; + } + if direct_by_index.contains_key(&index) { + return Err(RusticolError::artifact(format!( + "eager kernel closure {index} has direct-contraction metadata" + ))); + } + let kernel = require_kernel(kernels, row.kernel_id, EagerKernelRole::Closure, "closure")?; + required_index( + row.coupling_slot_id, + coupling_count, + "eager closure coupling", + )?; + validate_kernel_inputs( + kernel, + KernelInputBounds { + first_current: left_values.len, + second_current: right_values.len, + first_momentum: 0, + second_momentum: 0, + has_coupling: true, + parameter_count, + }, + )?; + if kernel.output_component_count != 1 { + return Err(RusticolError::artifact(format!( + "eager closure kernel {} must produce one component", + kernel.kernel_id + ))); + } + closures.push(ScheduledClosure { + row, + left_values, + right_values, + selector_domain_id: selector_domain_ids.map(|domains| domains[index]), + initializes_amplitude: false, + }); + } + if let Some(index) = direct_by_index.keys().next() { + return Err(RusticolError::artifact(format!( + "eager direct closure specification {index} has no matching row" + ))); + } + // Keep the same stable equal-kernel ordering contract as stage invocations. + closures.sort_by_key(|item| item.row.kernel_id); + Ok((closures, direct_closures)) +} + +pub(super) fn mark_initial_current_writes( + invocations: &[ScheduledInvocation], + attachments: &mut [ScheduledAttachment], + finalizations: &[ScheduledFinalization], +) -> Vec { + let mut written = BTreeSet::new(); + for invocation in invocations { + for attachment in &mut attachments[invocation.attachment_range.clone()] { + attachment.initializes_current = written.insert(attachment.row.result_current_id); + } + } + finalizations + .iter() + .filter_map(|item| (!written.contains(&item.row.current_id)).then_some(item.current)) + .collect() +} + +pub(super) fn mark_initial_amplitude_writes( + closures: &mut [ScheduledClosure], + direct_closures: &mut [ScheduledDirectClosure], + amplitude_count: usize, +) -> Vec { + let mut written = BTreeSet::new(); + for closure in closures { + closure.initializes_amplitude = written.insert(closure.row.amplitude_index); + } + for closure in direct_closures { + closure.initializes_amplitude = written.insert(closure.row.amplitude_index); + } + (0..amplitude_count) + .filter(|index| !written.contains(&(*index as u32))) + .collect() +} + +fn validate_reduction_plan( + groups: &[EagerReductionGroup], + entries: &[EagerReductionEntry], + amplitude_count: usize, +) -> RusticolResult<()> { + if groups.is_empty() || entries.is_empty() { + return Err(RusticolError::artifact( + "eager reduction requires nonempty groups and entries", + )); + } + let mut covered = BTreeSet::new(); + let mut coherent_group_ids = BTreeSet::new(); + for (group_index, group) in groups.iter().enumerate() { + if !coherent_group_ids.insert(group.coherent_group_id) { + return Err(RusticolError::artifact(format!( + "eager coherent reduction-group ID {} is duplicated", + group.coherent_group_id + ))); + } + if group.amplitude_indices.is_empty() { + return Err(RusticolError::artifact(format!( + "eager reduction group {group_index} is empty" + ))); + } + for amplitude_index in &group.amplitude_indices { + required_index( + *amplitude_index, + amplitude_count, + "eager reduction amplitude", + )?; + if !covered.insert(*amplitude_index) { + return Err(RusticolError::artifact(format!( + "eager amplitude {amplitude_index} belongs to more than one reduction group" + ))); + } + } + } + if covered.len() != amplitude_count { + return Err(RusticolError::artifact(format!( + "eager reduction groups cover {} of {amplitude_count} amplitudes", + covered.len() + ))); + } + for (index, entry) in entries.iter().enumerate() { + required_index( + entry.left_group_index, + groups.len(), + "eager reduction left group", + )?; + required_index( + entry.right_group_index, + groups.len(), + "eager reduction right group", + )?; + if !complex_is_finite(entry.coefficient) { + return Err(RusticolError::artifact(format!( + "eager reduction entry {index} has a non-finite coefficient" + ))); + } + } + Ok(()) +} + +#[derive(Clone, Copy)] +struct KernelInputBounds { + first_current: usize, + second_current: usize, + first_momentum: usize, + second_momentum: usize, + has_coupling: bool, + parameter_count: usize, +} + +fn validate_kernel_inputs( + kernel: &EagerKernelSpec, + bounds: KernelInputBounds, +) -> RusticolResult<()> { + for input in &kernel.inputs { + let (allowed, index, count) = match *input { + EagerKernelInput::FirstCurrentComponent(index) => { + (true, Some(index), bounds.first_current) + } + EagerKernelInput::SecondCurrentComponent(index) => ( + kernel.role != EagerKernelRole::Finalization, + Some(index), + bounds.second_current, + ), + EagerKernelInput::FirstMomentumComponent(index) => ( + kernel.role != EagerKernelRole::Closure, + Some(index), + bounds.first_momentum, + ), + EagerKernelInput::SecondMomentumComponent(index) => ( + kernel.role == EagerKernelRole::Vertex, + Some(index), + bounds.second_momentum, + ), + EagerKernelInput::CouplingReal | EagerKernelInput::CouplingImag => { + (bounds.has_coupling, None, 0) + } + EagerKernelInput::ModelParameter(index) => (true, Some(index), bounds.parameter_count), + }; + if !allowed { + return Err(RusticolError::artifact(format!( + "eager {:?} kernel {} cannot use input descriptor {input:?}", + kernel.role, kernel.kernel_id + ))); + } + if let Some(index) = index { + let index = usize::try_from(index).map_err(|_| { + RusticolError::artifact(format!( + "eager kernel {} input descriptor {input:?} does not fit usize", + kernel.kernel_id + )) + })?; + if index >= count { + return Err(RusticolError::artifact(format!( + "eager kernel {} input descriptor {input:?} is outside 0..{count}", + kernel.kernel_id + ))); + } + } + } + Ok(()) +} + +fn require_kernel<'a>( + kernels: &'a BTreeMap, + kernel_id: u32, + role: EagerKernelRole, + context: &str, +) -> RusticolResult<&'a EagerKernelSpec> { + let kernel = kernels.get(&kernel_id).ok_or_else(|| { + RusticolError::artifact(format!( + "eager {context} references unknown kernel {kernel_id}" + )) + })?; + if kernel.role != role { + return Err(RusticolError::artifact(format!( + "eager {context} kernel {kernel_id} has role {:?}, expected {role:?}", + kernel.role + ))); + } + Ok(kernel) +} + +fn optional_component_range( + layout: &ComponentLayout, + id: u32, + context: &str, +) -> RusticolResult> { + if id == MISSING_U32 { + return Ok(None); + } + layout.get(id, context).map(Some) +} + +fn validate_optional_index(id: u32, count: usize, context: &str) -> RusticolResult<()> { + if id == MISSING_U32 { + return Ok(()); + } + required_index(id, count, context).map(|_| ()) +} + +fn required_index(id: u32, count: usize, context: &str) -> RusticolResult { + if id == MISSING_U32 { + return Err(RusticolError::artifact(format!( + "{context} uses the reserved missing id" + ))); + } + let index = usize::try_from(id) + .map_err(|_| RusticolError::artifact(format!("{context} id does not fit usize")))?; + if index >= count { + return Err(RusticolError::artifact(format!( + "{context} index {index} is outside 0..{count}" + ))); + } + Ok(index) +} + +pub(super) fn complex_is_finite(value: EagerComplex64) -> bool { + value.re.is_finite() && value.im.is_finite() +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/eager_runtime/plan_v3.rs b/pyamplicol/rust/crates/rusticol-core/src/eager_runtime/plan_v3.rs new file mode 100644 index 0000000000..5d98ea5c87 --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/eager_runtime/plan_v3.rs @@ -0,0 +1,1794 @@ +// SPDX-License-Identifier: 0BSD + +//! Direct plan-v3 fixed-row adapter for the eager f64 runtime. +//! +//! This module intentionally depends only on the public plan-v3 row model. It +//! does not depend on PACBIN, manifests, engine loader types, JSON, or encoded +//! plan-v2 tables. + +use super::plan::{ + ComponentLayout, ComponentRange, EagerStagePlan, FinalizationCopy, ScheduledAttachment, + ScheduledClosure, ScheduledDirectClosure, ScheduledFinalization, ScheduledInvocation, + SelectorDomainPlan, mark_initial_amplitude_writes, mark_initial_current_writes, +}; +use super::{ + EAGER_INDEPENDENT_BLOCK_SIZE, EagerComplex64, EagerExecutionPlan, EagerKernelInput, + EagerKernelRole, EagerKernelSpec, EagerReductionEntry, EagerReductionGroup, +}; +use crate::{ + EAGER_OUTPUT_FACTOR_NONE, EagerAttachmentRow, EagerClosureRow, EagerCouplingRow, + EagerFinalizationRow, EagerInvocationRow, EagerPlanAttachmentRow, EagerPlanClosureRow, + EagerPlanCouplingRow, EagerPlanCurrentRow, EagerPlanDirectCoefficientRow, + EagerPlanExactFactorRow, EagerPlanFinalizationRow, EagerPlanInvocationRow, + EagerPlanMomentumRow, EagerPlanParameterRow, EagerPlanReductionEntryKind, + EagerPlanReductionEntryRow, EagerPlanReductionGroupRow, EagerPlanSelectorDomainRow, + EagerPlanStageRow, EagerPlanValueRow, EagerValueSlotKind, MISSING_U32, RusticolError, + RusticolResult, +}; +use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; +use std::fmt; + +/// Borrowed execution-relevant sections of a plan-v3 artifact. +/// +/// Catalogs used only for source filling, public selector aliases, inspection, +/// or exact provenance are deliberately absent. `exact_factors` retains the +/// bit-exact binary64 values needed to materialize final f64 runtime factors. +#[derive(Clone, Copy, Debug)] +pub struct EagerPlanV3Sections<'a> { + pub kernels: &'a [EagerKernelSpec], + /// Final prepared-pack evaluator parameter width after runtime projection + /// and synthetic coupling-slot insertion. + pub prepared_parameter_count: u32, + pub currents: &'a [EagerPlanCurrentRow], + pub values: &'a [EagerPlanValueRow], + pub momenta: &'a [EagerPlanMomentumRow], + pub parameters: &'a [EagerPlanParameterRow], + pub stages: &'a [EagerPlanStageRow], + pub couplings: &'a [EagerPlanCouplingRow], + pub invocations: &'a [EagerPlanInvocationRow], + pub attachments: &'a [EagerPlanAttachmentRow], + pub finalizations: &'a [EagerPlanFinalizationRow], + pub closures: &'a [EagerPlanClosureRow], + pub direct_coefficients: &'a [EagerPlanDirectCoefficientRow], + pub selector_domains: &'a [EagerPlanSelectorDomainRow], + pub selector_memberships: &'a [u32], + pub reduction_groups: &'a [EagerPlanReductionGroupRow], + pub reduction_entries: &'a [EagerPlanReductionEntryRow], + pub exact_factors: &'a [EagerPlanExactFactorRow], + pub color_contraction_entry_start: u64, + pub color_contraction_entry_count: u64, +} + +impl EagerPlanV3Sections<'_> { + pub fn to_owned(self) -> EagerOwnedPlanV3Sections { + EagerOwnedPlanV3Sections { + kernels: self.kernels.to_vec(), + prepared_parameter_count: self.prepared_parameter_count, + currents: self.currents.to_vec(), + values: self.values.to_vec(), + momenta: self.momenta.to_vec(), + parameters: self.parameters.to_vec(), + stages: self.stages.to_vec(), + couplings: self.couplings.to_vec(), + invocations: self.invocations.to_vec(), + attachments: self.attachments.to_vec(), + finalizations: self.finalizations.to_vec(), + closures: self.closures.to_vec(), + direct_coefficients: self.direct_coefficients.to_vec(), + selector_domains: self.selector_domains.to_vec(), + selector_memberships: self.selector_memberships.to_vec(), + reduction_groups: self.reduction_groups.to_vec(), + reduction_entries: self.reduction_entries.to_vec(), + exact_factors: self.exact_factors.to_vec(), + color_contraction_entry_start: self.color_contraction_entry_start, + color_contraction_entry_count: self.color_contraction_entry_count, + } + } +} + +/// Owned counterpart of [`EagerPlanV3Sections`]. +#[derive(Clone, Debug, PartialEq)] +pub struct EagerOwnedPlanV3Sections { + pub kernels: Vec, + pub prepared_parameter_count: u32, + pub currents: Vec, + pub values: Vec, + pub momenta: Vec, + pub parameters: Vec, + pub stages: Vec, + pub couplings: Vec, + pub invocations: Vec, + pub attachments: Vec, + pub finalizations: Vec, + pub closures: Vec, + pub direct_coefficients: Vec, + pub selector_domains: Vec, + pub selector_memberships: Vec, + pub reduction_groups: Vec, + pub reduction_entries: Vec, + pub exact_factors: Vec, + pub color_contraction_entry_start: u64, + pub color_contraction_entry_count: u64, +} + +impl EagerOwnedPlanV3Sections { + pub fn as_borrowed(&self) -> EagerPlanV3Sections<'_> { + EagerPlanV3Sections { + kernels: &self.kernels, + prepared_parameter_count: self.prepared_parameter_count, + currents: &self.currents, + values: &self.values, + momenta: &self.momenta, + parameters: &self.parameters, + stages: &self.stages, + couplings: &self.couplings, + invocations: &self.invocations, + attachments: &self.attachments, + finalizations: &self.finalizations, + closures: &self.closures, + direct_coefficients: &self.direct_coefficients, + selector_domains: &self.selector_domains, + selector_memberships: &self.selector_memberships, + reduction_groups: &self.reduction_groups, + reduction_entries: &self.reduction_entries, + exact_factors: &self.exact_factors, + color_contraction_entry_start: self.color_contraction_entry_start, + color_contraction_entry_count: self.color_contraction_entry_count, + } + } + + pub fn into_execution_plan(self) -> RusticolResult { + EagerExecutionPlan::from_plan_v3_sections(self.as_borrowed()) + } +} + +impl EagerExecutionPlan { + /// Validate plan-v3 fixed rows and materialize final eager f64 semantics. + pub fn from_plan_v3_sections(input: EagerPlanV3Sections<'_>) -> RusticolResult { + PlanBuilder::new(input)?.build() + } +} + +struct PlanBuilder<'a> { + input: EagerPlanV3Sections<'a>, + values: ComponentLayout, + momenta: ComponentLayout, + currents: ComponentLayout, + factors: Vec, + kernels: BTreeMap, + parameter_count: usize, + amplitude_count: usize, + selector_plan: SelectorDomainPlan, +} + +impl<'a> PlanBuilder<'a> { + fn new(input: EagerPlanV3Sections<'a>) -> RusticolResult { + let current_counts = validate_current_layout(input.currents)?; + let value_counts = validate_value_layout(input.values, input.currents)?; + let momentum_counts = validate_momentum_layout(input.momenta)?; + validate_dense_ids( + input.parameters.iter().map(|row| row.parameter_id), + "eager plan-v3 parameter", + )?; + let parameter_count = usize::try_from(input.prepared_parameter_count) + .map_err(|_| artifact("eager prepared parameter count does not fit usize"))?; + let factors = load_factors(input.exact_factors)?; + let kernels = validate_kernel_specs(input.kernels, parameter_count)?; + let amplitude_count = validate_amplitude_layout(input.closures)?; + let selector_plan = load_selector_plan( + input.selector_domains, + input.selector_memberships, + input.reduction_groups, + )?; + Ok(Self { + input, + values: ComponentLayout::new("value", &value_counts)?, + momenta: ComponentLayout::new("momentum", &momentum_counts)?, + currents: ComponentLayout::new("current", ¤t_counts)?, + factors, + kernels, + parameter_count, + amplitude_count, + selector_plan, + }) + } + + fn build(self) -> RusticolResult { + let couplings = self.load_couplings()?; + let (reduction_groups, reduction_entries) = self.load_reductions()?; + self.validate_selector_dependency_proof(&reduction_groups)?; + let (stages, stored_value_slots) = self.load_stages(couplings.len())?; + let (mut closures, mut direct_closures) = self.load_closures(couplings.len())?; + + let mut initial_value_ranges = Vec::new(); + for slot_id in 0..self.input.values.len() { + let slot_id = usize_u32(slot_id, "eager value slot")?; + if !stored_value_slots[slot_id as usize] { + initial_value_ranges.push(self.values.get(slot_id, "eager initial value")?); + } + } + let zero_amplitude_indices = mark_initial_amplitude_writes( + &mut closures, + &mut direct_closures, + self.amplitude_count, + ); + + Ok(EagerExecutionPlan { + values: self.values, + momenta: self.momenta, + currents: self.currents, + parameter_count: self.parameter_count, + amplitude_count: self.amplitude_count, + kernels: self.kernels, + couplings, + stages, + closures, + direct_closures, + reduction_groups, + reduction_entries, + selector_domains: Some(self.selector_plan), + initial_value_ranges, + zero_amplitude_indices, + }) + } + + fn load_couplings(&self) -> RusticolResult> { + validate_dense_ids( + self.input.couplings.iter().map(|row| row.coupling_id), + "eager plan-v3 coupling", + )?; + self.input + .couplings + .iter() + .enumerate() + .map(|(index, row)| { + validate_optional_index( + row.real_parameter_id, + self.parameter_count, + &format!("eager coupling {index} real parameter"), + )?; + validate_optional_index( + row.imaginary_parameter_id, + self.parameter_count, + &format!("eager coupling {index} imaginary parameter"), + )?; + let constant = self.factor(row.constant_factor_id, "eager coupling constant")?; + Ok(EagerCouplingRow { + real_parameter_id: row.real_parameter_id, + imag_parameter_id: row.imaginary_parameter_id, + constant_real: constant.re, + constant_imag: constant.im, + }) + }) + .collect() + } + + fn load_stages( + &self, + coupling_count: usize, + ) -> RusticolResult<(Vec, Vec)> { + validate_stage_ranges( + self.input.stages, + self.input.invocations.len(), + self.input.attachments.len(), + self.input.finalizations.len(), + )?; + let mut stages = Vec::with_capacity(self.input.stages.len()); + let mut finalized_currents = vec![false; self.input.currents.len()]; + let mut stored_value_slots = vec![false; self.input.values.len()]; + let mut previous_stage = None; + for stage in self.input.stages { + if previous_stage.is_some_and(|previous| stage.stage_index <= previous) { + return Err(artifact("eager stage indices must be strictly increasing")); + } + previous_stage = Some(stage.stage_index); + stages.push(self.load_stage( + stage, + coupling_count, + &mut finalized_currents, + &mut stored_value_slots, + )?); + } + Ok((stages, stored_value_slots)) + } + + fn load_stage( + &self, + stage: &EagerPlanStageRow, + coupling_count: usize, + globally_finalized_currents: &mut [bool], + globally_stored_value_slots: &mut [bool], + ) -> RusticolResult { + let invocation_range = checked_range( + self.input.invocations, + stage.invocation_start, + stage.invocation_count, + "eager stage invocations", + )?; + let attachment_range = checked_range( + self.input.attachments, + stage.attachment_start, + stage.attachment_count, + "eager stage attachments", + )?; + let finalization_range = checked_range( + self.input.finalizations, + stage.finalization_start, + stage.finalization_count, + "eager stage finalizations", + )?; + let stage_attachment_start = usize_count(stage.attachment_start, "attachment start")?; + + let mut invocations = Vec::with_capacity(invocation_range.len()); + let mut attachments = Vec::with_capacity(attachment_range.len()); + let mut attached_currents = vec![false; self.input.currents.len()]; + let mut attachment_cursor = stage_attachment_start; + for (index, row) in invocation_range.iter().enumerate() { + let kernel = require_kernel( + &self.kernels, + row.kernel_id, + EagerKernelRole::Vertex, + "invocation", + )?; + let left_values = self + .values + .get(row.left_value_slot_id, "eager invocation left value")?; + let right_values = self + .values + .get(row.right_value_slot_id, "eager invocation right value")?; + let left_momenta = self + .momenta + .get(row.left_momentum_slot_id, "eager invocation left momentum")?; + let right_momenta = self.momenta.get( + row.right_momentum_slot_id, + "eager invocation right momentum", + )?; + required_index( + row.coupling_slot_id, + coupling_count, + "eager invocation coupling", + )?; + let output_factor_source = validate_output_factor( + row.output_factor_source, + row.coupling_slot_id, + "eager invocation", + )?; + validate_kernel_inputs( + kernel, + KernelInputBounds { + first_current: left_values.len, + second_current: right_values.len, + first_momentum: left_momenta.len, + second_momentum: right_momenta.len, + has_coupling: true, + parameter_count: self.parameter_count, + }, + )?; + let global_start = usize_count(row.attachment_start, "invocation attachment start")?; + let count = usize_count(row.attachment_count, "invocation attachment count")?; + if count == 0 || global_start != attachment_cursor { + return Err(artifact(format!( + "eager invocation {index} has an empty or noncontiguous attachment range" + ))); + } + let global_stop = global_start + .checked_add(count) + .ok_or_else(|| artifact("eager invocation attachment range overflows usize"))?; + let stage_stop = stage_attachment_start + .checked_add(attachment_range.len()) + .ok_or_else(|| artifact("eager stage attachment range overflows usize"))?; + if global_stop > stage_stop { + return Err(artifact(format!( + "eager invocation {index} attachment range exceeds its stage" + ))); + } + let local_start = global_start - stage_attachment_start; + let local_stop = global_stop - stage_attachment_start; + self.validate_domain_id(row.selector_domain_id, "eager invocation selector")?; + for (offset, attachment) in attachment_range[local_start..local_stop].iter().enumerate() + { + let current = self.currents.get( + attachment.result_current_id, + "eager attachment result current", + )?; + if usize::try_from(kernel.output_component_count).ok() != Some(current.len) { + return Err(artifact(format!( + "eager invocation kernel {} output width does not match current {}", + kernel.kernel_id, attachment.result_current_id + ))); + } + let factor = self.attachment_factor(attachment)?; + self.validate_domain_id( + attachment.selector_domain_id, + "eager attachment selector", + )?; + attached_currents[attachment.result_current_id as usize] = true; + attachments.push(ScheduledAttachment { + row: EagerAttachmentRow { + result_current_id: attachment.result_current_id, + factor_real: factor.re, + factor_imag: factor.im, + }, + current, + selector_domain_id: Some(attachment.selector_domain_id), + initializes_current: false, + }); + let expected_position = local_start + offset; + if attachments.len() != expected_position + 1 { + return Err(artifact("eager attachment ranges overlap")); + } + } + invocations.push(ScheduledInvocation { + row: EagerInvocationRow { + kernel_id: row.kernel_id, + left_value_slot_id: row.left_value_slot_id, + right_value_slot_id: row.right_value_slot_id, + left_momentum_slot_id: row.left_momentum_slot_id, + right_momentum_slot_id: row.right_momentum_slot_id, + coupling_slot_id: row.coupling_slot_id, + output_factor_source, + attachment_start: local_start as u64, + attachment_count: count as u64, + }, + left_values, + right_values, + left_momenta, + right_momenta, + attachment_range: local_start..local_stop, + selector_domain_id: Some(row.selector_domain_id), + }); + attachment_cursor = global_stop; + } + if attachments.len() != attachment_range.len() { + return Err(artifact( + "eager invocation ranges do not cover the stage attachment table", + )); + } + + let mut finalizations = Vec::new(); + let mut finalization_copies = Vec::new(); + let mut stage_current_ranges = vec![None; self.input.currents.len()]; + let mut current_component_count = 0usize; + let mut stage_finalized_currents = vec![false; self.input.currents.len()]; + for (index, row) in finalization_range.iter().enumerate() { + let current_index = required_index( + row.current_id, + self.input.currents.len(), + "eager finalization current", + )?; + if std::mem::replace(&mut stage_finalized_currents[current_index], true) + || std::mem::replace(&mut globally_finalized_currents[current_index], true) + { + return Err(artifact(format!( + "eager current {} is finalized more than once", + row.current_id + ))); + } + if row.unpropagated_value_slot_id != MISSING_U32 + && row.unpropagated_value_slot_id == row.propagated_value_slot_id + { + return Err(artifact(format!( + "eager finalization {index} aliases its outputs" + ))); + } + for value_slot_id in [row.unpropagated_value_slot_id, row.propagated_value_slot_id] { + if value_slot_id != MISSING_U32 { + let value_index = required_index( + value_slot_id, + self.input.values.len(), + "eager finalization value", + )?; + if std::mem::replace(&mut globally_stored_value_slots[value_index], true) { + return Err(artifact(format!( + "eager value slot {value_slot_id} is stored more than once" + ))); + } + } + } + let global_current = self + .currents + .get(row.current_id, "eager finalization current")?; + let local_current = ComponentRange { + start: current_component_count, + len: global_current.len, + }; + current_component_count = current_component_count + .checked_add(global_current.len) + .ok_or_else(|| artifact("eager stage current workspace overflows usize"))?; + stage_current_ranges[current_index] = Some(local_current); + let unpropagated = self.optional_value( + row.unpropagated_value_slot_id, + EagerValueSlotKind::Unpropagated, + "eager unpropagated finalization value", + )?; + let propagated = self.optional_value( + row.propagated_value_slot_id, + EagerValueSlotKind::Propagated, + "eager propagated finalization value", + )?; + if unpropagated.is_none() && propagated.is_none() { + return Err(artifact(format!( + "eager finalization {index} stores no current value" + ))); + } + for output in [unpropagated, propagated].into_iter().flatten() { + if output.len != local_current.len { + return Err(artifact(format!( + "eager finalization {index} output width does not match current width" + ))); + } + } + let momentum = self + .momenta + .get(row.momentum_slot_id, "eager finalization momentum")?; + self.validate_domain_id( + row.unpropagated_selector_domain_id, + "eager unpropagated finalization selector", + )?; + self.validate_domain_id( + row.propagated_selector_domain_id, + "eager propagated finalization selector", + )?; + if let Some(unpropagated) = unpropagated { + finalization_copies.push(FinalizationCopy { + current: local_current, + unpropagated, + selector_domain_id: Some(row.unpropagated_selector_domain_id), + }); + } else { + self.require_empty_domain( + row.unpropagated_selector_domain_id, + "missing eager unpropagated output", + )?; + } + if row.kernel_id == MISSING_U32 { + if propagated.is_some() { + return Err(artifact(format!( + "eager finalization {index} has a propagated output but no kernel" + ))); + } + self.require_empty_domain( + row.propagated_selector_domain_id, + "missing eager propagated output", + )?; + } else { + let kernel = require_kernel( + &self.kernels, + row.kernel_id, + EagerKernelRole::Finalization, + "finalization", + )?; + if propagated.is_none() { + return Err(artifact(format!( + "eager finalization {index} applies a kernel without an output" + ))); + } + validate_kernel_inputs( + kernel, + KernelInputBounds { + first_current: local_current.len, + second_current: 0, + first_momentum: momentum.len, + second_momentum: 0, + has_coupling: false, + parameter_count: self.parameter_count, + }, + )?; + if usize::try_from(kernel.output_component_count).ok() != Some(local_current.len) { + return Err(artifact(format!( + "eager finalization kernel {} output width does not match current width", + kernel.kernel_id + ))); + } + finalizations.push(ScheduledFinalization { + row: EagerFinalizationRow { + kernel_id: row.kernel_id, + current_id: row.current_id, + unpropagated_value_slot_id: row.unpropagated_value_slot_id, + propagated_value_slot_id: row.propagated_value_slot_id, + momentum_slot_id: row.momentum_slot_id, + }, + current: local_current, + propagated, + momentum, + selector_domain_id: Some(row.propagated_selector_domain_id), + }); + } + } + if attached_currents != stage_finalized_currents { + return Err(artifact(format!( + "eager stage {} attached and finalized current sets differ", + stage.stage_index + ))); + } + for attachment in &mut attachments { + attachment.current = stage_current_ranges + .get(attachment.row.result_current_id as usize) + .and_then(|range| *range) + .ok_or_else(|| artifact("eager attachment has no stage-local current"))?; + } + invocations.sort_by_key(|item| item.row.kernel_id); + finalizations.sort_by_key(|item| item.row.kernel_id); + let zero_current_ranges = + mark_initial_current_writes(&invocations, &mut attachments, &finalizations); + Ok(EagerStagePlan { + stage_index: stage.stage_index, + current_component_count, + invocations, + attachments, + finalization_copies, + finalizations, + zero_current_ranges, + }) + } + + fn load_closures( + &self, + coupling_count: usize, + ) -> RusticolResult<(Vec, Vec)> { + let mut closures = Vec::new(); + let mut direct_closures = Vec::new(); + for (index, row) in self.input.closures.iter().enumerate() { + let left_values = self + .values + .get(row.left_value_slot_id, "eager closure left value")?; + let right_values = self + .values + .get(row.right_value_slot_id, "eager closure right value")?; + required_index( + row.amplitude_index, + self.amplitude_count, + "eager closure amplitude", + )?; + self.validate_domain_id(row.selector_domain_id, "eager closure selector")?; + let color = self.factor(row.color_factor_id, "eager closure color factor")?; + self.factor(row.coupling_factor_id, "eager closure coupling factor")?; + self.factor( + row.normalization_factor_id, + "eager closure normalization factor", + )?; + let direct = checked_range( + self.input.direct_coefficients, + row.direct_coefficient_start, + row.direct_coefficient_count, + "eager direct closure coefficients", + )?; + if row.kernel_id == MISSING_U32 { + if row.coupling_slot_id != MISSING_U32 + || row.output_factor_source != EAGER_OUTPUT_FACTOR_NONE as u8 + || direct.is_empty() + { + return Err(artifact(format!( + "eager direct closure {index} has inconsistent kernel metadata" + ))); + } + if left_values.len != right_values.len || left_values.len != direct.len() { + return Err(artifact(format!( + "eager direct closure {index} component widths do not match" + ))); + } + let mut coefficients = Vec::with_capacity(direct.len()); + for (component, coefficient) in direct.iter().enumerate() { + if usize::try_from(coefficient.component_index).ok() != Some(component) { + return Err(artifact(format!( + "eager direct closure {index} coefficients are not component ordered" + ))); + } + coefficients.push( + self.factor(coefficient.factor_id, "eager direct closure coefficient")?, + ); + } + direct_closures.push(ScheduledDirectClosure { + row: EagerClosureRow { + kernel_id: MISSING_U32, + left_value_slot_id: row.left_value_slot_id, + right_value_slot_id: row.right_value_slot_id, + amplitude_index: row.amplitude_index, + coupling_slot_id: MISSING_U32, + output_factor_source: EAGER_OUTPUT_FACTOR_NONE, + factor_real: color.re, + factor_imag: color.im, + }, + left_values, + right_values, + coefficients, + selector_domain_id: Some(row.selector_domain_id), + initializes_amplitude: false, + }); + continue; + } + if !direct.is_empty() { + return Err(artifact(format!( + "eager kernel closure {index} also has direct coefficients" + ))); + } + let kernel = require_kernel( + &self.kernels, + row.kernel_id, + EagerKernelRole::Closure, + "closure", + )?; + required_index( + row.coupling_slot_id, + coupling_count, + "eager closure coupling", + )?; + self.validate_coupling_factor(row)?; + let output_factor_source = validate_output_factor( + row.output_factor_source, + row.coupling_slot_id, + "eager closure", + )?; + validate_kernel_inputs( + kernel, + KernelInputBounds { + first_current: left_values.len, + second_current: right_values.len, + first_momentum: 0, + second_momentum: 0, + has_coupling: true, + parameter_count: self.parameter_count, + }, + )?; + if kernel.output_component_count != 1 { + return Err(artifact(format!( + "eager closure kernel {} must produce one component", + kernel.kernel_id + ))); + } + // plan-v2 lowering multiplies color by prepared-kernel normalization. + let factor = color + * self.factor( + row.normalization_factor_id, + "eager closure normalization factor", + )?; + require_finite(factor, "eager closure combined factor")?; + closures.push(ScheduledClosure { + row: EagerClosureRow { + kernel_id: row.kernel_id, + left_value_slot_id: row.left_value_slot_id, + right_value_slot_id: row.right_value_slot_id, + amplitude_index: row.amplitude_index, + coupling_slot_id: row.coupling_slot_id, + output_factor_source, + factor_real: factor.re, + factor_imag: factor.im, + }, + left_values, + right_values, + selector_domain_id: Some(row.selector_domain_id), + initializes_amplitude: false, + }); + } + closures.sort_by_key(|item| item.row.kernel_id); + Ok((closures, direct_closures)) + } + + fn load_reductions( + &self, + ) -> RusticolResult<(Vec, Vec)> { + if self.input.reduction_groups.is_empty() { + return Err(artifact("eager reduction requires nonempty groups")); + } + let mut group_index_by_id = HashMap::with_capacity(self.input.reduction_groups.len()); + let mut covered_entries = vec![false; self.input.reduction_entries.len()]; + let mut covered_amplitudes = vec![false; self.amplitude_count]; + let mut covered_amplitude_count = 0usize; + let mut groups = Vec::with_capacity(self.input.reduction_groups.len()); + for (group_index, group) in self.input.reduction_groups.iter().enumerate() { + if group_index_by_id + .insert(group.coherent_group_id, group_index) + .is_some() + { + return Err(artifact(format!( + "duplicate eager coherent group {}", + group.coherent_group_id + ))); + } + self.factor( + group.helicity_weight_factor_id, + "eager reduction helicity weight", + )?; + self.factor( + group.all_sector_weight_factor_id, + "eager reduction all-sector weight", + )?; + let amplitudes = self.reduction_owned_range( + group.amplitude_entry_start, + group.amplitude_entry_count, + EagerPlanReductionEntryKind::AmplitudeMember, + group.coherent_group_id, + &mut covered_entries, + "eager amplitude reduction entries", + )?; + if amplitudes.is_empty() { + return Err(artifact(format!( + "eager reduction group {group_index} has no amplitudes" + ))); + } + let mut amplitude_indices = Vec::with_capacity(amplitudes.len()); + for entry in amplitudes { + let amplitude_index = required_index( + entry.left_id, + self.amplitude_count, + "eager reduction amplitude", + )?; + if std::mem::replace(&mut covered_amplitudes[amplitude_index], true) { + return Err(artifact(format!( + "eager amplitude {} belongs to multiple groups", + entry.left_id + ))); + } + covered_amplitude_count += 1; + amplitude_indices.push(entry.left_id); + } + self.reduction_owned_range( + group.selector_entry_start, + group.selector_entry_count, + EagerPlanReductionEntryKind::SelectorMember, + group.coherent_group_id, + &mut covered_entries, + "eager selector reduction entries", + )?; + groups.push(EagerReductionGroup { + coherent_group_id: group.coherent_group_id, + amplitude_indices, + }); + } + if covered_amplitude_count != self.amplitude_count { + return Err(artifact(format!( + "eager reduction groups cover {} of {} amplitudes", + covered_amplitude_count, self.amplitude_count + ))); + } + let contraction = checked_range( + self.input.reduction_entries, + self.input.color_contraction_entry_start, + self.input.color_contraction_entry_count, + "eager color contraction entries", + )?; + if contraction.is_empty() { + return Err(artifact("eager reduction requires contraction entries")); + } + let contraction_start = usize_count( + self.input.color_contraction_entry_start, + "eager color contraction start", + )?; + let mut entries = Vec::with_capacity(contraction.len()); + for (offset, entry) in contraction.iter().enumerate() { + if entry.kind != EagerPlanReductionEntryKind::ColorContraction { + return Err(artifact( + "eager color contraction range contains another entry kind", + )); + } + mark_covered(&mut covered_entries, contraction_start + offset)?; + let left_group_index = *group_index_by_id.get(&entry.left_id).ok_or_else(|| { + artifact(format!( + "eager contraction references unknown left group {}", + entry.left_id + )) + })?; + let right_group_index = *group_index_by_id.get(&entry.right_id).ok_or_else(|| { + artifact(format!( + "eager contraction references unknown right group {}", + entry.right_id + )) + })?; + let mut coefficient = self.factor(entry.factor_id, "eager color contraction factor")?; + if entry.auxiliary_factor_id != MISSING_U32 { + coefficient *= self.factor( + entry.auxiliary_factor_id, + "eager color contraction symmetry factor", + )?; + } + require_finite(coefficient, "eager color contraction coefficient")?; + entries.push(EagerReductionEntry { + left_group_index: usize_u32(left_group_index, "eager reduction group")?, + right_group_index: usize_u32(right_group_index, "eager reduction group")?, + coefficient, + }); + } + if covered_entries.iter().any(|covered| !covered) { + return Err(artifact( + "eager reduction entry catalog contains unowned rows", + )); + } + Ok((groups, entries)) + } + + fn reduction_owned_range<'b>( + &self, + start: u64, + count: u64, + kind: EagerPlanReductionEntryKind, + owner_id: u32, + covered: &mut [bool], + context: &str, + ) -> RusticolResult<&'b [EagerPlanReductionEntryRow]> + where + 'a: 'b, + { + let entries = checked_range(self.input.reduction_entries, start, count, context)?; + let start = usize_count(start, context)?; + for (offset, entry) in entries.iter().enumerate() { + if entry.kind != kind || entry.owner_id != owner_id { + return Err(artifact(format!("{context} have inconsistent tags/owners"))); + } + mark_covered(covered, start + offset)?; + } + Ok(entries) + } + + fn validate_selector_dependency_proof( + &self, + groups: &[EagerReductionGroup], + ) -> RusticolResult<()> { + let mut group_by_amplitude = vec![None; self.amplitude_count]; + for group in groups { + for amplitude in &group.amplitude_indices { + group_by_amplitude[*amplitude as usize] = Some(group.coherent_group_id); + } + } + let mut proof = SelectorProofScratch::new(&self.selector_plan)?; + let mut value_domain_sources = vec![Vec::::new(); self.input.values.len()]; + for (index, closure) in self.input.closures.iter().enumerate() { + let amplitude = required_index( + closure.amplitude_index, + self.amplitude_count, + "eager closure amplitude", + )?; + let group = group_by_amplitude[amplitude] + .ok_or_else(|| artifact("eager closure amplitude has no coherent group"))?; + if closure.coherent_group_id != group { + return Err(artifact(format!( + "eager closure {index} coherent group disagrees with reduction ownership" + ))); + } + proof.validate_singleton( + closure.selector_domain_id, + group, + ProofContext::closure(index), + )?; + push_value_domain_source( + &mut value_domain_sources, + closure.left_value_slot_id, + closure.selector_domain_id, + "eager closure left value", + )?; + push_value_domain_source( + &mut value_domain_sources, + closure.right_value_slot_id, + closure.selector_domain_id, + "eager closure right value", + )?; + } + for stage in self.input.stages.iter().rev() { + let invocations = checked_range( + self.input.invocations, + stage.invocation_start, + stage.invocation_count, + "eager selector stage invocations", + )?; + let attachments = checked_range( + self.input.attachments, + stage.attachment_start, + stage.attachment_count, + "eager selector stage attachments", + )?; + let finalizations = checked_range( + self.input.finalizations, + stage.finalization_start, + stage.finalization_count, + "eager selector stage finalizations", + )?; + let mut current_domain_sources = vec![None::<[u32; 2]>; self.input.currents.len()]; + for (index, finalization) in finalizations.iter().enumerate() { + let unpropagated = domain_sources_for_optional_value( + &value_domain_sources, + finalization.unpropagated_value_slot_id, + "eager unpropagated finalization value", + )?; + proof.validate_union( + finalization.unpropagated_selector_domain_id, + unpropagated.iter().copied(), + ProofContext::stage(stage.stage_index, "unpropagated finalization", index), + )?; + let propagated = domain_sources_for_optional_value( + &value_domain_sources, + finalization.propagated_value_slot_id, + "eager propagated finalization value", + )?; + proof.validate_union( + finalization.propagated_selector_domain_id, + propagated.iter().copied(), + ProofContext::stage(stage.stage_index, "propagated finalization", index), + )?; + let current_index = required_index( + finalization.current_id, + current_domain_sources.len(), + "eager selector finalization current", + )?; + current_domain_sources[current_index] = Some([ + finalization.unpropagated_selector_domain_id, + finalization.propagated_selector_domain_id, + ]); + } + for (index, attachment) in attachments.iter().enumerate() { + let current_index = required_index( + attachment.result_current_id, + current_domain_sources.len(), + "eager selector attachment current", + )?; + let sources = current_domain_sources + .get(current_index) + .and_then(|sources| *sources) + .ok_or_else(|| { + artifact(format!( + "eager stage {} attachment {index} has no finalized current", + stage.stage_index + )) + })?; + proof.validate_union( + attachment.selector_domain_id, + sources, + ProofContext::stage(stage.stage_index, "attachment", index), + )?; + } + let stage_attachment_start = + usize_count(stage.attachment_start, "eager stage attachment start")?; + for (index, invocation) in invocations.iter().enumerate() { + let start = usize_count( + invocation.attachment_start, + "eager invocation attachment start", + )?; + let count = usize_count( + invocation.attachment_count, + "eager invocation attachment count", + )?; + if start < stage_attachment_start { + return Err(artifact("eager invocation attachment precedes its stage")); + } + let local_start = start - stage_attachment_start; + let local_stop = local_start + .checked_add(count) + .ok_or_else(|| artifact("eager invocation attachment range overflows"))?; + let invocation_attachments = + attachments.get(local_start..local_stop).ok_or_else(|| { + artifact("eager invocation attachment range exceeds its stage") + })?; + proof.validate_union( + invocation.selector_domain_id, + invocation_attachments + .iter() + .map(|attachment| attachment.selector_domain_id), + ProofContext::stage(stage.stage_index, "invocation", index), + )?; + push_value_domain_source( + &mut value_domain_sources, + invocation.left_value_slot_id, + invocation.selector_domain_id, + "eager invocation left value", + )?; + push_value_domain_source( + &mut value_domain_sources, + invocation.right_value_slot_id, + invocation.selector_domain_id, + "eager invocation right value", + )?; + } + } + Ok(()) + } + + fn attachment_factor(&self, row: &EagerPlanAttachmentRow) -> RusticolResult { + let color = self.factor(row.color_factor_id, "eager attachment color factor")?; + let evaluation = self.factor( + row.evaluation_factor_id, + "eager attachment evaluation factor", + )?; + let normalization = self.factor( + row.normalization_factor_id, + "eager attachment normalization factor", + )?; + let representative = self.factor( + row.representative_evaluation_factor_id, + "eager attachment representative factor", + )?; + if representative == EagerComplex64::new(0.0, 0.0) { + return Err(artifact( + "eager attachment representative evaluation factor is zero", + )); + } + // Match plan-v2 lowering: color * evaluation * (normalization / representative). + let factor = color * evaluation * (normalization / representative); + require_finite(factor, "eager attachment combined factor")?; + Ok(factor) + } + + fn validate_coupling_factor(&self, closure: &EagerPlanClosureRow) -> RusticolResult<()> { + let coupling = self + .input + .couplings + .get(closure.coupling_slot_id as usize) + .ok_or_else(|| artifact("eager closure references unknown coupling"))?; + if coupling.constant_factor_id != closure.coupling_factor_id { + return Err(artifact( + "eager closure coupling factor disagrees with its coupling row", + )); + } + Ok(()) + } + + fn factor(&self, id: u32, context: &str) -> RusticolResult { + let index = required_index(id, self.factors.len(), context)?; + Ok(self.factors[index]) + } + + fn optional_value( + &self, + id: u32, + expected_kind: EagerValueSlotKind, + context: &str, + ) -> RusticolResult> { + if id == MISSING_U32 { + return Ok(None); + } + let index = required_index(id, self.input.values.len(), context)?; + if self.input.values[index].kind != expected_kind { + return Err(artifact(format!("{context} has the wrong value-slot kind"))); + } + self.values.get(id, context).map(Some) + } + + fn validate_domain_id(&self, id: u32, context: &str) -> RusticolResult { + required_index(id, self.selector_plan.memberships.len(), context) + } + + fn require_empty_domain(&self, id: u32, context: &str) -> RusticolResult<()> { + let index = self.validate_domain_id(id, context)?; + if !self.selector_plan.memberships[index].is_empty() { + return Err(artifact(format!( + "{context} has a nonempty selector domain" + ))); + } + Ok(()) + } +} + +#[derive(Clone, Copy)] +struct ProofContext { + stage_index: Option, + kind: &'static str, + row_index: usize, +} + +impl ProofContext { + fn closure(row_index: usize) -> Self { + Self { + stage_index: None, + kind: "closure", + row_index, + } + } + + fn stage(stage_index: u32, kind: &'static str, row_index: usize) -> Self { + Self { + stage_index: Some(stage_index), + kind, + row_index, + } + } +} + +impl fmt::Display for ProofContext { + fn fmt(&self, output: &mut fmt::Formatter<'_>) -> fmt::Result { + if let Some(stage_index) = self.stage_index { + write!( + output, + "eager stage {stage_index} {} {}", + self.kind, self.row_index + ) + } else { + write!(output, "eager {} {}", self.kind, self.row_index) + } + } +} + +enum GroupIndex { + Dense, + Sparse(HashMap), +} + +impl GroupIndex { + fn resolve(&self, group_id: u32, group_count: usize) -> Option { + match self { + Self::Dense => usize::try_from(group_id) + .ok() + .filter(|index| *index < group_count), + Self::Sparse(indices) => indices.get(&group_id).copied(), + } + } +} + +struct SelectorProofScratch<'a> { + memberships: &'a [Vec], + group_index: GroupIndex, + marks: Vec, + epoch: u32, +} + +impl<'a> SelectorProofScratch<'a> { + fn new(plan: &'a SelectorDomainPlan) -> RusticolResult { + let dense = plan + .group_ids + .iter() + .enumerate() + .all(|(index, group_id)| usize::try_from(*group_id).ok() == Some(index)); + let group_index = if dense { + GroupIndex::Dense + } else { + let mut indices = HashMap::with_capacity(plan.group_ids.len()); + for (index, group_id) in plan.group_ids.iter().copied().enumerate() { + if indices.insert(group_id, index).is_some() { + return Err(artifact("eager selector proof repeats coherent group IDs")); + } + } + GroupIndex::Sparse(indices) + }; + Ok(Self { + memberships: &plan.memberships, + group_index, + marks: vec![0; plan.group_ids.len()], + epoch: 0, + }) + } + + fn validate_singleton( + &self, + domain_id: u32, + expected: u32, + context: ProofContext, + ) -> RusticolResult<()> { + let index = required_index( + domain_id, + self.memberships.len(), + "eager selector proof domain", + )?; + if self.memberships[index].as_slice() != [expected] { + return Err(artifact(format!( + "{context} selector domain does not match its dependency closure" + ))); + } + Ok(()) + } + + fn validate_union( + &mut self, + domain_id: u32, + sources: impl IntoIterator, + context: ProofContext, + ) -> RusticolResult<()> { + let target_index = required_index( + domain_id, + self.memberships.len(), + "eager selector proof target domain", + )?; + if self.epoch > u32::MAX - 2 { + self.marks.fill(0); + self.epoch = 2; + } else { + self.epoch += 2; + } + let expected_mark = self.epoch; + let covered_mark = expected_mark + 1; + let target = &self.memberships[target_index]; + for group_id in target { + let group_index = self + .group_index + .resolve(*group_id, self.marks.len()) + .ok_or_else(|| artifact("eager selector proof references an unknown group"))?; + self.marks[group_index] = expected_mark; + } + + let mut covered = 0usize; + for source_id in sources { + let source_index = required_index( + source_id, + self.memberships.len(), + "eager selector proof source domain", + )?; + for group_id in &self.memberships[source_index] { + let group_index = self + .group_index + .resolve(*group_id, self.marks.len()) + .ok_or_else(|| artifact("eager selector proof references an unknown group"))?; + match self.marks[group_index] { + mark if mark == expected_mark => { + self.marks[group_index] = covered_mark; + covered += 1; + } + mark if mark == covered_mark => {} + _ => { + return Err(artifact(format!( + "{context} selector domain does not match its dependency closure" + ))); + } + } + } + } + if covered != target.len() { + return Err(artifact(format!( + "{context} selector domain does not match its dependency closure" + ))); + } + Ok(()) + } +} + +fn validate_current_layout(rows: &[EagerPlanCurrentRow]) -> RusticolResult> { + validate_component_rows( + rows.iter().enumerate().map(|(index, row)| { + if row.flags & !1 != 0 { + return Err(artifact(format!( + "eager current {index} has unsupported flags" + ))); + } + Ok((row.current_id, row.component_start, row.component_count)) + }), + "current", + ) +} + +fn validate_value_layout( + rows: &[EagerPlanValueRow], + currents: &[EagerPlanCurrentRow], +) -> RusticolResult> { + let counts = validate_component_rows( + rows.iter() + .map(|row| Ok((row.value_slot_id, row.component_start, row.component_count))), + "value", + )?; + for (index, row) in rows.iter().enumerate() { + let current = required_index(row.current_id, currents.len(), "eager value current")?; + if currents[current].component_count != row.component_count { + return Err(artifact(format!( + "eager value {index} width does not match current {}", + row.current_id + ))); + } + } + Ok(counts) +} + +fn validate_momentum_layout(rows: &[EagerPlanMomentumRow]) -> RusticolResult> { + validate_component_rows( + rows.iter().map(|row| { + Ok(( + row.momentum_slot_id, + row.component_start, + row.component_count, + )) + }), + "momentum", + ) +} + +fn validate_component_rows( + rows: impl Iterator>, + name: &str, +) -> RusticolResult> { + let mut counts = Vec::new(); + let mut component_start = 0u64; + for (index, row) in rows.enumerate() { + let (id, start, count) = row?; + if usize::try_from(id).ok() != Some(index) { + return Err(artifact(format!("eager {name} IDs are not dense"))); + } + if start != component_start || count == 0 { + return Err(artifact(format!( + "eager {name} {index} has a noncontiguous or empty component range" + ))); + } + component_start = component_start + .checked_add(u64::from(count)) + .ok_or_else(|| artifact(format!("eager {name} component range overflows")))?; + counts.push(count); + } + Ok(counts) +} + +fn validate_amplitude_layout(rows: &[EagerPlanClosureRow]) -> RusticolResult { + validate_dense_ids( + rows.iter().map(|row| row.root_id), + "eager plan-v3 closure root", + )?; + if rows.is_empty() { + return Err(artifact("eager execution plan has no amplitude outputs")); + } + let amplitudes = rows + .iter() + .map(|row| row.amplitude_index) + .collect::>(); + if amplitudes + .iter() + .copied() + .ne(0..usize_u32(amplitudes.len(), "eager amplitude count")?) + { + return Err(artifact("eager closure amplitude indices are not dense")); + } + Ok(amplitudes.len()) +} + +fn load_factors(rows: &[EagerPlanExactFactorRow]) -> RusticolResult> { + validate_dense_ids( + rows.iter().map(|row| row.factor_id), + "eager plan-v3 exact factor", + )?; + rows.iter() + .enumerate() + .map(|(index, row)| { + if row.exact_source > 1 { + return Err(artifact(format!( + "eager exact factor {index} has an unsupported source" + ))); + } + let value = EagerComplex64::new( + f64::from_bits(row.real_bits), + f64::from_bits(row.imaginary_bits), + ); + require_finite(value, &format!("eager exact factor {index}"))?; + Ok(value) + }) + .collect() +} + +fn load_selector_plan( + rows: &[EagerPlanSelectorDomainRow], + memberships: &[u32], + groups: &[EagerPlanReductionGroupRow], +) -> RusticolResult { + if rows.is_empty() { + return Err(artifact("eager selector-domain table is empty")); + } + let mut known_groups = HashSet::with_capacity(groups.len()); + let mut group_ids = Vec::with_capacity(groups.len()); + for group in groups { + if !known_groups.insert(group.coherent_group_id) { + return Err(artifact("eager reduction groups repeat coherent group IDs")); + } + group_ids.push(group.coherent_group_id); + } + group_ids.sort_unstable(); + let mut result = Vec::with_capacity(rows.len()); + let mut cursor = 0usize; + let mut unique = HashSet::<&[u32]>::with_capacity(rows.len()); + for (domain_id, row) in rows.iter().enumerate() { + let start = usize_count(row.member_start, "eager selector member start")?; + let count = usize_count(row.member_count, "eager selector member count")?; + if start != cursor { + return Err(artifact(format!( + "eager selector domain {domain_id} is not contiguous" + ))); + } + let stop = start + .checked_add(count) + .ok_or_else(|| artifact("eager selector membership range overflows"))?; + let members = memberships + .get(start..stop) + .ok_or_else(|| artifact("eager selector domain exceeds its membership table"))?; + if members.windows(2).any(|pair| pair[0] >= pair[1]) { + return Err(artifact(format!( + "eager selector domain {domain_id} is not sorted and unique" + ))); + } + if let Some(unknown) = members.iter().find(|member| !known_groups.contains(member)) { + return Err(artifact(format!( + "eager selector domain references unknown coherent group {unknown}" + ))); + } + if !unique.insert(members) { + return Err(artifact(format!( + "eager selector domain {domain_id} duplicates an earlier domain" + ))); + } + result.push(members.to_vec()); + cursor = stop; + } + if cursor != memberships.len() || !result.iter().any(Vec::is_empty) { + return Err(artifact( + "eager selector domains do not exactly cover memberships and an empty domain", + )); + } + Ok(SelectorDomainPlan { + memberships: result, + group_ids, + }) +} + +fn validate_stage_ranges( + stages: &[EagerPlanStageRow], + invocation_count: usize, + attachment_count: usize, + finalization_count: usize, +) -> RusticolResult<()> { + let mut invocations = 0u64; + let mut attachments = 0u64; + let mut finalizations = 0u64; + for (index, stage) in stages.iter().enumerate() { + if stage.invocation_start != invocations + || stage.attachment_start != attachments + || stage.finalization_start != finalizations + { + return Err(artifact(format!( + "eager stage {index} ranges are not contiguous" + ))); + } + invocations = checked_add( + stage.invocation_start, + stage.invocation_count, + "invocations", + )?; + attachments = checked_add( + stage.attachment_start, + stage.attachment_count, + "attachments", + )?; + finalizations = checked_add( + stage.finalization_start, + stage.finalization_count, + "finalizations", + )?; + } + if usize::try_from(invocations).ok() != Some(invocation_count) + || usize::try_from(attachments).ok() != Some(attachment_count) + || usize::try_from(finalizations).ok() != Some(finalization_count) + { + return Err(artifact("eager stage ranges do not cover execution tables")); + } + Ok(()) +} + +fn validate_kernel_specs( + specs: &[EagerKernelSpec], + parameter_count: usize, +) -> RusticolResult> { + let mut kernels = BTreeMap::new(); + for (index, spec) in specs.iter().cloned().enumerate() { + if spec.kernel_id == MISSING_U32 + || spec.inputs.is_empty() + || spec.output_component_count == 0 + { + return Err(artifact(format!("invalid eager kernel spec {index}"))); + } + if spec.independent_block_size != 1 + && (spec.independent_block_size != EAGER_INDEPENDENT_BLOCK_SIZE + || spec.role != EagerKernelRole::Vertex + || spec.inputs.iter().any(|input| { + !matches!( + input, + EagerKernelInput::FirstCurrentComponent(_) + | EagerKernelInput::SecondCurrentComponent(_) + ) + })) + { + return Err(artifact(format!( + "eager kernel {} has an invalid independent block contract", + spec.kernel_id + ))); + } + let mut inputs = BTreeSet::new(); + for input in &spec.inputs { + if !inputs.insert(*input) { + return Err(artifact(format!( + "eager kernel {} repeats an input descriptor", + spec.kernel_id + ))); + } + if let EagerKernelInput::ModelParameter(parameter) = input { + required_index( + *parameter, + parameter_count, + &format!("eager kernel {} model parameter", spec.kernel_id), + )?; + } + } + let kernel_id = spec.kernel_id; + if kernels.insert(kernel_id, spec).is_some() { + return Err(artifact(format!("duplicate eager kernel ID {kernel_id}"))); + } + } + Ok(kernels) +} + +#[derive(Clone, Copy)] +struct KernelInputBounds { + first_current: usize, + second_current: usize, + first_momentum: usize, + second_momentum: usize, + has_coupling: bool, + parameter_count: usize, +} + +fn validate_kernel_inputs( + kernel: &EagerKernelSpec, + bounds: KernelInputBounds, +) -> RusticolResult<()> { + for input in &kernel.inputs { + let (allowed, index, count) = match *input { + EagerKernelInput::FirstCurrentComponent(index) => { + (true, Some(index), bounds.first_current) + } + EagerKernelInput::SecondCurrentComponent(index) => ( + kernel.role != EagerKernelRole::Finalization, + Some(index), + bounds.second_current, + ), + EagerKernelInput::FirstMomentumComponent(index) => ( + kernel.role != EagerKernelRole::Closure, + Some(index), + bounds.first_momentum, + ), + EagerKernelInput::SecondMomentumComponent(index) => ( + kernel.role == EagerKernelRole::Vertex, + Some(index), + bounds.second_momentum, + ), + EagerKernelInput::CouplingReal | EagerKernelInput::CouplingImag => { + (bounds.has_coupling, None, 0) + } + EagerKernelInput::ModelParameter(index) => (true, Some(index), bounds.parameter_count), + }; + if !allowed + || index.is_some_and(|index| { + usize::try_from(index) + .ok() + .is_none_or(|index| index >= count) + }) + { + return Err(artifact(format!( + "eager {:?} kernel {} cannot use input descriptor {input:?}", + kernel.role, kernel.kernel_id + ))); + } + } + Ok(()) +} + +fn require_kernel<'a>( + kernels: &'a BTreeMap, + kernel_id: u32, + role: EagerKernelRole, + context: &str, +) -> RusticolResult<&'a EagerKernelSpec> { + let kernel = kernels.get(&kernel_id).ok_or_else(|| { + artifact(format!( + "eager {context} references unknown kernel {kernel_id}" + )) + })?; + if kernel.role != role { + return Err(artifact(format!( + "eager {context} kernel {kernel_id} has role {:?}, expected {role:?}", + kernel.role + ))); + } + Ok(kernel) +} + +fn validate_output_factor(source: u8, coupling: u32, context: &str) -> RusticolResult { + let source = u32::from(source); + if source > 2 || (source != EAGER_OUTPUT_FACTOR_NONE && coupling == MISSING_U32) { + return Err(artifact(format!( + "{context} has an invalid output-factor source" + ))); + } + Ok(source) +} + +fn domain_sources_for_optional_value<'a>( + domains: &'a [Vec], + value_slot_id: u32, + context: &str, +) -> RusticolResult<&'a [u32]> { + if value_slot_id == MISSING_U32 { + Ok(&[]) + } else { + let index = required_index(value_slot_id, domains.len(), context)?; + Ok(&domains[index]) + } +} + +fn push_value_domain_source( + domains: &mut [Vec], + value_slot_id: u32, + domain_id: u32, + context: &str, +) -> RusticolResult<()> { + let index = required_index(value_slot_id, domains.len(), context)?; + if domains[index].last().copied() != Some(domain_id) { + domains[index].push(domain_id); + } + Ok(()) +} + +fn validate_dense_ids(ids: impl Iterator, context: &str) -> RusticolResult<()> { + for (index, id) in ids.enumerate() { + if usize::try_from(id).ok() != Some(index) { + return Err(artifact(format!("{context} IDs are not dense"))); + } + } + Ok(()) +} + +fn validate_optional_index(id: u32, count: usize, context: &str) -> RusticolResult<()> { + if id == MISSING_U32 { + Ok(()) + } else { + required_index(id, count, context).map(|_| ()) + } +} + +fn required_index(id: u32, count: usize, context: &str) -> RusticolResult { + if id == MISSING_U32 { + return Err(artifact(format!("{context} uses the reserved missing ID"))); + } + let index = + usize::try_from(id).map_err(|_| artifact(format!("{context} index does not fit usize")))?; + if index >= count { + return Err(artifact(format!( + "{context} index {index} is outside 0..{count}" + ))); + } + Ok(index) +} + +fn checked_range<'a, T>( + values: &'a [T], + start: u64, + count: u64, + context: &str, +) -> RusticolResult<&'a [T]> { + let start = usize_count(start, context)?; + let count = usize_count(count, context)?; + let stop = start + .checked_add(count) + .ok_or_else(|| artifact(format!("{context} range overflows usize")))?; + values + .get(start..stop) + .ok_or_else(|| artifact(format!("{context} range exceeds its table"))) +} + +fn mark_covered(covered: &mut [bool], index: usize) -> RusticolResult<()> { + let slot = covered + .get_mut(index) + .ok_or_else(|| artifact("eager reduction range exceeds its table"))?; + if std::mem::replace(slot, true) { + return Err(artifact("eager reduction entry belongs to multiple ranges")); + } + Ok(()) +} + +fn checked_add(start: u64, count: u64, context: &str) -> RusticolResult { + start + .checked_add(count) + .ok_or_else(|| artifact(format!("eager {context} range overflows u64"))) +} + +fn usize_count(value: u64, context: &str) -> RusticolResult { + usize::try_from(value).map_err(|_| artifact(format!("{context} does not fit usize"))) +} + +fn usize_u32(value: usize, context: &str) -> RusticolResult { + u32::try_from(value).map_err(|_| artifact(format!("{context} exceeds u32"))) +} + +fn require_finite(value: EagerComplex64, context: &str) -> RusticolResult<()> { + if !value.re.is_finite() || !value.im.is_finite() { + return Err(artifact(format!("{context} is not finite"))); + } + Ok(()) +} + +fn artifact(message: impl Into) -> RusticolError { + RusticolError::artifact(message) +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/eager_runtime/plan_v3_tests.rs b/pyamplicol/rust/crates/rusticol-core/src/eager_runtime/plan_v3_tests.rs new file mode 100644 index 0000000000..27254490d6 --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/eager_runtime/plan_v3_tests.rs @@ -0,0 +1,350 @@ +// SPDX-License-Identifier: 0BSD + +use super::plan_v3::{EagerOwnedPlanV3Sections, EagerPlanV3Sections}; +use super::{EagerExecutionPlan, EagerKernelInput, EagerKernelRole, EagerKernelSpec}; +use crate::{ + EagerPlanAttachmentRow, EagerPlanClosureRow, EagerPlanCouplingRow, EagerPlanCurrentRow, + EagerPlanDirectCoefficientRow, EagerPlanExactFactorRow, EagerPlanFinalizationRow, + EagerPlanInvocationRow, EagerPlanMomentumRow, EagerPlanReductionEntryKind, + EagerPlanReductionEntryRow, EagerPlanReductionGroupRow, EagerPlanSelectorDomainRow, + EagerPlanStageRow, EagerPlanValueRow, EagerValueSlotKind, MISSING_U32, +}; + +#[derive(Clone)] +pub(super) struct Fixture { + kernels: Vec, + pub(super) prepared_parameter_count: u32, + pub(super) currents: Vec, + pub(super) values: Vec, + pub(super) momenta: Vec, + pub(super) stages: Vec, + couplings: Vec, + pub(super) invocations: Vec, + pub(super) attachments: Vec, + pub(super) finalizations: Vec, + pub(super) closures: Vec, + direct_coefficients: Vec, + selector_domains: Vec, + selector_memberships: Vec, + reduction_groups: Vec, + reduction_entries: Vec, + exact_factors: Vec, +} + +impl Fixture { + pub(super) fn new() -> Self { + let exact_factors = [ + (1.0, 0.0), + (2.0, 3.0), + (2.0, 0.0), + (3.0, 0.0), + (5.0, 0.0), + (2.0, 0.0), + (7.0, 0.0), + (11.0, 0.0), + (13.0, 0.0), + (17.0, 0.0), + (19.0, 0.0), + ] + .into_iter() + .enumerate() + .map(|(factor_id, (real, imaginary))| EagerPlanExactFactorRow { + factor_id: factor_id as u32, + real_bits: f64::to_bits(real), + imaginary_bits: f64::to_bits(imaginary), + canonical_string_id: 0, + exact_source: 0, + exact_ir_id: MISSING_U32, + source_ir_id: MISSING_U32, + }) + .collect(); + Self { + kernels: vec![ + EagerKernelSpec { + kernel_id: 10, + role: EagerKernelRole::Vertex, + inputs: vec![ + EagerKernelInput::FirstCurrentComponent(0), + EagerKernelInput::SecondCurrentComponent(0), + EagerKernelInput::CouplingReal, + ], + output_component_count: 1, + homogeneous_linear_first_current: false, + independent_block_size: 1, + }, + EagerKernelSpec { + kernel_id: 11, + role: EagerKernelRole::Closure, + inputs: vec![ + EagerKernelInput::FirstCurrentComponent(0), + EagerKernelInput::SecondCurrentComponent(0), + EagerKernelInput::CouplingReal, + ], + output_component_count: 1, + homogeneous_linear_first_current: false, + independent_block_size: 1, + }, + ], + prepared_parameter_count: 0, + currents: (0..3) + .map(|current_id| EagerPlanCurrentRow { + current_id, + component_start: u64::from(current_id), + component_count: 1, + momentum_slot_id: current_id, + flags: u32::from(current_id < 2), + }) + .collect(), + values: vec![ + EagerPlanValueRow { + value_slot_id: 0, + current_id: 0, + component_start: 0, + component_count: 1, + kind: EagerValueSlotKind::Source, + }, + EagerPlanValueRow { + value_slot_id: 1, + current_id: 1, + component_start: 1, + component_count: 1, + kind: EagerValueSlotKind::Source, + }, + EagerPlanValueRow { + value_slot_id: 2, + current_id: 2, + component_start: 2, + component_count: 1, + kind: EagerValueSlotKind::Unpropagated, + }, + ], + momenta: (0..3) + .map(|momentum_slot_id| EagerPlanMomentumRow { + momentum_slot_id, + bitset_id: momentum_slot_id, + component_start: u64::from(momentum_slot_id) * 4, + component_count: 4, + }) + .collect(), + stages: vec![EagerPlanStageRow { + stage_index: 1, + subset_size: 2, + invocation_start: 0, + invocation_count: 1, + attachment_start: 0, + attachment_count: 1, + finalization_start: 0, + finalization_count: 1, + }], + couplings: vec![EagerPlanCouplingRow { + coupling_id: 0, + real_parameter_id: MISSING_U32, + imaginary_parameter_id: MISSING_U32, + constant_factor_id: 1, + }], + invocations: vec![EagerPlanInvocationRow { + evaluation_group_id: 0, + kernel_id: 10, + left_value_slot_id: 0, + right_value_slot_id: 1, + left_momentum_slot_id: 0, + right_momentum_slot_id: 1, + coupling_slot_id: 0, + output_factor_source: 0, + attachment_start: 0, + attachment_count: 1, + selector_domain_id: 1, + }], + attachments: vec![EagerPlanAttachmentRow { + interaction_id: 0, + result_current_id: 2, + color_factor_id: 2, + evaluation_factor_id: 3, + normalization_factor_id: 4, + representative_evaluation_factor_id: 5, + selector_domain_id: 1, + }], + finalizations: vec![EagerPlanFinalizationRow { + kernel_id: MISSING_U32, + current_id: 2, + unpropagated_value_slot_id: 2, + propagated_value_slot_id: MISSING_U32, + momentum_slot_id: 2, + unpropagated_selector_domain_id: 1, + propagated_selector_domain_id: 0, + }], + closures: vec![EagerPlanClosureRow { + root_id: 0, + kernel_id: 11, + left_value_slot_id: 2, + right_value_slot_id: 0, + amplitude_index: 0, + coherent_group_id: 7, + coupling_slot_id: 0, + coupling_factor_id: 1, + output_factor_source: 0, + color_factor_id: 6, + normalization_factor_id: 7, + direct_coefficient_start: 0, + direct_coefficient_count: 0, + selector_domain_id: 1, + }], + direct_coefficients: Vec::new(), + selector_domains: vec![ + EagerPlanSelectorDomainRow { + member_start: 0, + member_count: 0, + }, + EagerPlanSelectorDomainRow { + member_start: 0, + member_count: 1, + }, + ], + selector_memberships: vec![7], + reduction_groups: vec![EagerPlanReductionGroupRow { + coherent_group_id: 7, + amplitude_entry_start: 0, + amplitude_entry_count: 1, + selector_entry_start: 1, + selector_entry_count: 0, + helicity_weight_factor_id: 0, + all_sector_weight_factor_id: 0, + }], + reduction_entries: vec![ + EagerPlanReductionEntryRow { + kind: EagerPlanReductionEntryKind::AmplitudeMember, + owner_id: 7, + left_id: 0, + right_id: MISSING_U32, + factor_id: MISSING_U32, + auxiliary_factor_id: MISSING_U32, + }, + EagerPlanReductionEntryRow { + kind: EagerPlanReductionEntryKind::ColorContraction, + owner_id: MISSING_U32, + left_id: 7, + right_id: 7, + factor_id: 8, + auxiliary_factor_id: 9, + }, + ], + exact_factors, + } + } + + pub(super) fn sections(&self) -> EagerPlanV3Sections<'_> { + EagerPlanV3Sections { + kernels: &self.kernels, + prepared_parameter_count: self.prepared_parameter_count, + currents: &self.currents, + values: &self.values, + momenta: &self.momenta, + parameters: &[], + stages: &self.stages, + couplings: &self.couplings, + invocations: &self.invocations, + attachments: &self.attachments, + finalizations: &self.finalizations, + closures: &self.closures, + direct_coefficients: &self.direct_coefficients, + selector_domains: &self.selector_domains, + selector_memberships: &self.selector_memberships, + reduction_groups: &self.reduction_groups, + reduction_entries: &self.reduction_entries, + exact_factors: &self.exact_factors, + color_contraction_entry_start: 1, + color_contraction_entry_count: 1, + } + } +} + +#[test] +fn builds_final_f64_semantics_without_plan_v2_payloads() { + let fixture = Fixture::new(); + let owned: EagerOwnedPlanV3Sections = fixture.sections().to_owned(); + let plan = owned.into_execution_plan().unwrap(); + + assert_eq!(plan.value_component_count(), 3); + assert_eq!(plan.momentum_component_count(), 12); + assert_eq!(plan.current_component_count(), 3); + assert_eq!(plan.stage_indices().collect::>(), vec![1]); + assert!(plan.has_selector_domains()); + assert_eq!(plan.couplings[0].constant_real, 2.0); + assert_eq!(plan.couplings[0].constant_imag, 3.0); + assert_eq!(plan.stages[0].attachments[0].row.factor_real, 15.0); + assert_eq!(plan.stages[0].attachments[0].row.factor_imag, 0.0); + assert_eq!(plan.closures[0].row.factor_real, 77.0); + assert_eq!(plan.reduction_groups[0].amplitude_indices, vec![0]); + assert_eq!(plan.reduction_entries[0].coefficient.re, 221.0); + assert_eq!(plan.initial_value_ranges.len(), 2); +} + +#[test] +fn preserves_direct_closure_coefficients_and_color_factor() { + let mut fixture = Fixture::new(); + fixture.closures[0].kernel_id = MISSING_U32; + fixture.closures[0].coupling_slot_id = MISSING_U32; + fixture.closures[0].direct_coefficient_count = 1; + fixture.direct_coefficients = vec![EagerPlanDirectCoefficientRow { + contraction_ir_id: 0, + component_index: 0, + factor_id: 10, + }]; + + let plan = EagerExecutionPlan::from_plan_v3_sections(fixture.sections()).unwrap(); + assert!(plan.closures.is_empty()); + assert_eq!(plan.direct_closures[0].row.factor_real, 7.0); + assert_eq!(plan.direct_closures[0].coefficients[0].re, 19.0); +} + +#[test] +fn prepared_parameter_domain_is_independent_of_decoded_parameter_rows() { + let mut fixture = Fixture::new(); + fixture.prepared_parameter_count = 1; + fixture.couplings[0].real_parameter_id = 0; + fixture.kernels[0] + .inputs + .push(EagerKernelInput::ModelParameter(0)); + + let plan = EagerExecutionPlan::from_plan_v3_sections(fixture.sections()).unwrap(); + assert_eq!(plan.parameter_count(), 1); + assert_eq!(plan.couplings[0].real_parameter_id, 0); + + fixture.prepared_parameter_count = 0; + let error = EagerExecutionPlan::from_plan_v3_sections(fixture.sections()).unwrap_err(); + assert!(error.to_string().contains("model parameter")); +} + +#[test] +fn rejects_prepared_kernel_role_mismatches() { + let mut fixture = Fixture::new(); + fixture.kernels[0].role = EagerKernelRole::Closure; + + let error = EagerExecutionPlan::from_plan_v3_sections(fixture.sections()).unwrap_err(); + assert!(error.to_string().contains("expected Vertex")); +} + +#[test] +fn rejects_duplicate_selector_domains() { + let mut fixture = Fixture::new(); + fixture.selector_domains.push(EagerPlanSelectorDomainRow { + member_start: 1, + member_count: 0, + }); + + let error = EagerExecutionPlan::from_plan_v3_sections(fixture.sections()).unwrap_err(); + assert!(error.to_string().contains("duplicates an earlier domain")); +} + +#[test] +fn rejects_selector_union_missing_an_attachment_dependency() { + let mut fixture = Fixture::new(); + fixture.invocations[0].selector_domain_id = 0; + + let error = EagerExecutionPlan::from_plan_v3_sections(fixture.sections()).unwrap_err(); + assert!( + error + .to_string() + .contains("selector domain does not match its dependency closure") + ); +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/eager_runtime/profile.rs b/pyamplicol/rust/crates/rusticol-core/src/eager_runtime/profile.rs new file mode 100644 index 0000000000..b32ada94b1 --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/eager_runtime/profile.rs @@ -0,0 +1,342 @@ +// SPDX-License-Identifier: 0BSD + +//! Instrumented eager execution used only by the explicit native profile path. +//! +//! The ordinary scheduler deliberately stays in `execute.rs` without timer +//! branches or `Instant` calls. These routines mirror its row-major packet +//! contract while placing timers exactly at the phase boundaries exposed to +//! users. + +use super::plan::{ClosureExecutionRows, ComponentRange, EagerStagePlan}; +use super::runtime::{ + EagerWorkspace, KernelPacket, LinearFinalizationPacket, PacketRole, StageSchedule, +}; +use super::{ + EagerComplex64, EagerExecutionProfile, EagerKernelBackend, EagerKernelCall, EagerKernelSpec, +}; +use crate::{RusticolError, RusticolResult}; +use std::collections::BTreeMap; +use std::time::Instant; + +#[allow(clippy::too_many_arguments)] +#[allow(dead_code)] // Retained by the explicit packet-scheduler profiling oracle. +pub(super) fn execute_stage_profiled( + stage: &EagerStagePlan, + schedule: &StageSchedule, + kernels: &BTreeMap, + workspace: &mut EagerWorkspace, + backend: &mut B, + point_count: usize, + tile_start: usize, + tile_points: usize, + momenta: &[f64], + model_parameters: &[EagerComplex64], + profile: &mut EagerExecutionProfile, +) -> RusticolResult<()> { + let clear_started = Instant::now(); + for range in &stage.zero_current_ranges { + for component in range.start..range.start + range.len { + let target = component * workspace.tile_capacity; + workspace.currents[target..target + tile_points].fill(EagerComplex64::new(0.0, 0.0)); + } + } + profile.invocation_scatter += clear_started.elapsed(); + for packet in &schedule.invocation_packets { + debug_assert_eq!(packet.role, PacketRole::Invocation); + let items = &stage.invocations[packet.item_range.clone()]; + let (lane_count, evaluator_input_components, evaluator_output_components) = + super::execute::invocation_packet_shape(packet, items.len(), tile_points)?; + let input_len = evaluator_input_components * lane_count; + let output_len = evaluator_output_components * lane_count; + let (inputs, outputs) = packet_slices(&mut workspace.packet, input_len, output_len)?; + let kernel = kernels.get(&packet.kernel_id).ok_or_else(|| { + RusticolError::internal(format!( + "eager schedule lost invocation kernel {}", + packet.kernel_id + )) + })?; + + let gather_started = Instant::now(); + if packet.independent_block_size == 1 { + super::execute::gather_invocations( + items, + &kernel.inputs, + inputs, + packet.input_components, + point_count, + tile_start, + tile_points, + workspace.tile_capacity, + &workspace.values, + momenta, + &workspace.couplings, + model_parameters, + ); + } else { + super::execute::gather_blocked_invocations( + items, + &kernel.inputs, + inputs, + packet.input_components, + packet.independent_block_size, + tile_points, + workspace.tile_capacity, + &workspace.values, + ); + } + profile.gather += gather_started.elapsed(); + + let kernel_started = Instant::now(); + backend.evaluate_batch(EagerKernelCall { + kernel_id: packet.kernel_id, + independent_block_size: packet.independent_block_size as u32, + lane_count, + input_component_count: evaluator_input_components, + output_component_count: evaluator_output_components, + inputs, + outputs, + })?; + profile.kernel_call += kernel_started.elapsed(); + profile.backend_call_count = profile.backend_call_count.saturating_add(1); + + let scatter_started = Instant::now(); + if packet.independent_block_size == 1 { + super::execute::scatter_invocations( + items, + &stage.attachments, + outputs, + lane_count, + tile_points, + workspace.tile_capacity, + &workspace.couplings, + &mut workspace.currents, + ); + } else { + super::execute::scatter_blocked_invocations( + items, + &stage.attachments, + outputs, + packet.output_components, + packet.independent_block_size, + tile_points, + workspace.tile_capacity, + &workspace.couplings, + &mut workspace.currents, + ); + } + profile.invocation_scatter += scatter_started.elapsed(); + } + + let copies_started = Instant::now(); + for item in &stage.finalization_copies { + copy_component_range( + &workspace.currents, + item.current, + &mut workspace.values, + item.unpropagated, + workspace.tile_capacity, + tile_points, + ); + } + profile.finalization += copies_started.elapsed(); + + for packet in &schedule.finalization_packets { + debug_assert_eq!(packet.role, PacketRole::Finalization); + let items = &stage.finalizations[packet.item_range.clone()]; + let lane_count = packet + .linear_finalization + .as_ref() + .map_or(items.len(), LinearFinalizationPacket::lane_multiplier) + * tile_points; + let input_len = packet.input_components * lane_count; + let output_len = packet.output_components * lane_count; + let (inputs, outputs) = packet_slices(&mut workspace.packet, input_len, output_len)?; + let kernel = kernels.get(&packet.kernel_id).ok_or_else(|| { + RusticolError::internal(format!( + "eager schedule lost finalization kernel {}", + packet.kernel_id + )) + })?; + + let gather_started = Instant::now(); + if let Some(linear) = &packet.linear_finalization { + super::execute::gather_linear_finalizations( + linear, + &kernel.inputs, + inputs, + packet.input_components, + point_count, + tile_start, + tile_points, + momenta, + model_parameters, + ); + } else { + super::execute::gather_finalizations( + items, + &kernel.inputs, + inputs, + packet.input_components, + point_count, + tile_start, + tile_points, + workspace.tile_capacity, + &workspace.currents, + momenta, + model_parameters, + ); + } + profile.finalization += gather_started.elapsed(); + + let kernel_started = Instant::now(); + backend.evaluate_batch(EagerKernelCall { + kernel_id: packet.kernel_id, + independent_block_size: 1, + lane_count, + input_component_count: packet.input_components, + output_component_count: packet.output_components, + inputs, + outputs, + })?; + profile.kernel_call += kernel_started.elapsed(); + profile.backend_call_count = profile.backend_call_count.saturating_add(1); + + let scatter_started = Instant::now(); + if let Some(linear) = &packet.linear_finalization { + super::execute::scatter_linear_finalizations( + items, + linear, + outputs, + lane_count, + tile_points, + workspace.tile_capacity, + &workspace.currents, + &mut workspace.values, + )?; + } else { + super::execute::scatter_finalizations( + items, + outputs, + lane_count, + tile_points, + workspace.tile_capacity, + &mut workspace.values, + )?; + } + profile.finalization += scatter_started.elapsed(); + } + Ok(()) +} + +#[allow(dead_code)] // Retained by the explicit packet-scheduler profiling oracle. +pub(super) fn execute_closures_profiled( + rows: ClosureExecutionRows<'_>, + packets: &[KernelPacket], + workspace: &mut EagerWorkspace, + backend: &mut B, + tile_points: usize, + model_parameters: &[EagerComplex64], + profile: &mut EagerExecutionProfile, +) -> RusticolResult<()> { + for packet in packets { + debug_assert_eq!(packet.role, PacketRole::Closure); + let items = &rows.closures[packet.item_range.clone()]; + let lane_count = items.len() * tile_points; + let input_len = packet.input_components * lane_count; + let output_len = packet.output_components * lane_count; + let (inputs, outputs) = packet_slices(&mut workspace.packet, input_len, output_len)?; + let kernel = rows.kernels.get(&packet.kernel_id).ok_or_else(|| { + RusticolError::internal(format!( + "eager schedule lost closure kernel {}", + packet.kernel_id + )) + })?; + + let gather_started = Instant::now(); + super::execute::gather_closures( + items, + &kernel.inputs, + inputs, + packet.input_components, + tile_points, + workspace.tile_capacity, + &workspace.values, + &workspace.couplings, + model_parameters, + ); + profile.closure += gather_started.elapsed(); + + let kernel_started = Instant::now(); + backend.evaluate_batch(EagerKernelCall { + kernel_id: packet.kernel_id, + independent_block_size: 1, + lane_count, + input_component_count: packet.input_components, + output_component_count: packet.output_components, + inputs, + outputs, + })?; + profile.kernel_call += kernel_started.elapsed(); + profile.backend_call_count = profile.backend_call_count.saturating_add(1); + + let scatter_started = Instant::now(); + super::execute::scatter_closures( + items, + outputs, + lane_count, + tile_points, + workspace.tile_capacity, + &workspace.couplings, + &mut workspace.amplitudes, + ); + profile.closure += scatter_started.elapsed(); + } + + let direct_started = Instant::now(); + super::execute::execute_direct_closures( + rows.direct_closures, + &workspace.values, + workspace.tile_capacity, + tile_points, + &mut workspace.amplitudes, + ); + profile.closure += direct_started.elapsed(); + Ok(()) +} + +#[allow(dead_code)] // Shared only by the retained profiled packet-scheduler path. +fn packet_slices( + packet: &mut [EagerComplex64], + input_len: usize, + output_len: usize, +) -> RusticolResult<(&mut [EagerComplex64], &mut [EagerComplex64])> { + let total = input_len + .checked_add(output_len) + .ok_or_else(|| RusticolError::internal("eager active packet size overflows"))?; + if total > packet.len() { + return Err(RusticolError::internal(format!( + "eager active packet needs {total} elements, workspace has {}", + packet.len() + ))); + } + let active = &mut packet[..total]; + Ok(active.split_at_mut(input_len)) +} + +#[allow(dead_code)] // Shared only by the retained profiled packet-scheduler path. +fn copy_component_range( + source: &[EagerComplex64], + source_range: ComponentRange, + target: &mut [EagerComplex64], + target_range: ComponentRange, + stride: usize, + point_count: usize, +) { + debug_assert_eq!(source_range.len, target_range.len); + for component in 0..source_range.len { + let source_start = (source_range.start + component) * stride; + let target_start = (target_range.start + component) * stride; + target[target_start..target_start + point_count] + .copy_from_slice(&source[source_start..source_start + point_count]); + } +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/eager_runtime/runtime.rs b/pyamplicol/rust/crates/rusticol-core/src/eager_runtime/runtime.rs new file mode 100644 index 0000000000..c77fc9238b --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/eager_runtime/runtime.rs @@ -0,0 +1,2824 @@ +// SPDX-License-Identifier: 0BSD + +use super::execute::{ + copy_selected_tile_amplitudes, copy_tile_amplitudes, copy_tile_results, execute_closures, + execute_stage, initialize_tile, reduce_selected_tile, reduce_tile, +}; +use super::plan::{ + ClosureExecutionRows, ComponentRange, EagerExecutionPlan, EagerStagePlan, ScheduledClosure, + ScheduledDirectClosure, ScheduledFinalization, complex_is_finite, +}; +use super::profile::{execute_closures_profiled, execute_stage_profiled}; +use super::{ + EagerComplex64, EagerExecutionProfile, EagerKernelBackend, EagerKernelSpec, + EagerRuntimeOptions, EagerScheduleAuditRow, +}; +use crate::{EagerCouplingRow, MISSING_U32, RusticolError, RusticolResult}; +use std::cmp::min; +use std::collections::BTreeMap; +use std::mem::size_of; +use std::ops::Range; +use std::time::Instant; + +// `workspace_bytes` controls batching, not the maximum process size. Keeping +// one gather/evaluate/scatter packet cache-sized avoids turning otherwise +// linear eager execution into a memory-bandwidth workload when many +// invocations share one prepared kernel. +const MAX_PACKET_BUFFER_BYTES: usize = 4 * 1024 * 1024; +const MAX_PERSISTENT_TILE_BYTES: usize = 16 * 1024 * 1024; +const PREFERRED_TILE_ALIGNMENT: usize = 8; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(super) enum PacketRole { + Invocation, + Finalization, + Closure, +} + +#[derive(Clone, Debug)] +pub(super) struct KernelPacket { + pub(super) role: PacketRole, + pub(super) kernel_id: u32, + pub(super) independent_block_size: usize, + pub(super) item_range: Range, + pub(super) input_components: usize, + pub(super) output_components: usize, + pub(super) linear_finalization: Option, +} + +#[derive(Clone, Debug)] +pub(super) struct LinearFinalizationPacket { + pub(super) momentum_groups: Vec, + pub(super) item_group_indices: Vec, + pub(super) current_components: Vec, +} + +impl LinearFinalizationPacket { + pub(super) fn lane_multiplier(&self) -> usize { + self.momentum_groups.len() * self.current_components.len() + } +} + +#[derive(Clone, Debug)] +pub(super) struct StageSchedule { + pub(super) invocation_packets: Vec, + pub(super) finalization_packets: Vec, +} + +#[derive(Clone, Debug)] +pub(super) struct ExecutionSchedule { + pub(super) stages: Vec, + pub(super) closure_packets: Vec, + packet_buffer_len: usize, +} + +#[derive(Clone, Debug)] +struct SelectedExecution { + active_groups: Vec, + active_amplitude_indices: Vec, + active_reduction_group_indices: Vec, + active_reduction_group_ids: Vec, + active_reduction_entry_indices: Vec, + stages: Vec, + closures: Vec, + direct_closures: Vec, + zero_amplitude_indices: Vec, + active_zero_amplitude_indices: Vec, + schedule: ExecutionSchedule, +} + +#[derive(Clone, Copy, Debug)] +struct PointSelectorWorkGroup { + execution_index: Option, + point_start: usize, + point_count: usize, +} + +#[derive(Debug, Default)] +struct PointSelectedExecution { + signature_offsets: Vec, + signature_groups: Vec, + point_work_group_ids: Vec, + grouped_point_indices: Vec, + work_group_cursors: Vec, + work_groups: Vec, + executions: Vec, + gathered_initial_values: Vec, + gathered_momenta: Vec, + gathered_group_weights: Vec, +} + +#[allow(dead_code)] +enum PointSelectorInput<'a> { + OneGroupPerPoint(&'a [u32]), + GroupSets { + offsets: &'a [usize], + groups: &'a [u32], + }, +} + +impl PointSelectorInput<'_> { + fn point_count(&self) -> usize { + match self { + Self::OneGroupPerPoint(groups) => groups.len(), + Self::GroupSets { offsets, .. } => offsets.len().saturating_sub(1), + } + } + + fn groups_for_point(&self, point: usize) -> &[u32] { + match self { + Self::OneGroupPerPoint(groups) => &groups[point..point + 1], + Self::GroupSets { offsets, groups } => &groups[offsets[point]..offsets[point + 1]], + } + } + + fn group_offset_for_point(&self, point: usize) -> usize { + match self { + Self::OneGroupPerPoint(_) => point, + Self::GroupSets { offsets, .. } => offsets[point], + } + } + + fn group_entry_count(&self) -> usize { + match self { + Self::OneGroupPerPoint(groups) => groups.len(), + Self::GroupSets { groups, .. } => groups.len(), + } + } + + fn matches_signature(&self, signature_offsets: &[usize], signature_groups: &[u32]) -> bool { + match self { + Self::OneGroupPerPoint(groups) => { + signature_groups == *groups + && signature_offsets.len() == groups.len() + 1 + && signature_offsets.iter().copied().eq(0..=groups.len()) + } + Self::GroupSets { offsets, groups } => { + signature_offsets == *offsets && signature_groups == *groups + } + } + } + + fn store_signature(&self, offsets: &mut Vec, groups: &mut Vec) { + offsets.clear(); + groups.clear(); + match self { + Self::OneGroupPerPoint(selected) => { + offsets.extend(0..=selected.len()); + groups.extend_from_slice(selected); + } + Self::GroupSets { + offsets: selected_offsets, + groups: selected_groups, + } => { + offsets.extend_from_slice(selected_offsets); + groups.extend_from_slice(selected_groups); + } + } + } +} + +#[derive(Debug)] +pub(super) struct EagerWorkspace { + pub(super) tile_capacity: usize, + pub(super) values: Vec, + pub(super) currents: Vec, + pub(super) amplitudes: Vec, + pub(super) reduction_groups: Vec, + pub(super) couplings: Vec, + pub(super) reduced: Vec, + pub(super) packet: Vec, +} + +#[derive(Debug)] +pub struct EagerExecutionRuntime { + plan: EagerExecutionPlan, + schedule: ExecutionSchedule, + workspace: EagerWorkspace, + workspace_bytes: usize, + packet_budget: usize, + selected: Option, + point_selected: PointSelectedExecution, +} + +impl EagerExecutionRuntime { + pub fn new(plan: EagerExecutionPlan, options: EagerRuntimeOptions) -> RusticolResult { + if options.point_tile_size == 0 { + return Err(RusticolError::invalid_argument( + "eager point tile size must be positive", + )); + } + let complex_bytes = size_of::(); + let static_bytes = plan + .couplings + .len() + .checked_mul(complex_bytes) + .ok_or_else(|| RusticolError::invalid_argument("eager coupling workspace overflows"))?; + let persistent_complex_components = plan + .values + .component_count + .checked_add(plan.current_workspace_component_count()) + .and_then(|value| value.checked_add(plan.amplitude_count)) + .and_then(|value| value.checked_add(plan.reduction_groups.len())) + .ok_or_else(|| { + RusticolError::invalid_argument("eager persistent workspace overflows") + })?; + let persistent_bytes_per_point = persistent_complex_components + .checked_mul(complex_bytes) + .and_then(|value| value.checked_add(size_of::())) + .ok_or_else(|| RusticolError::invalid_argument("eager point workspace overflows"))?; + let largest_kernel_io = used_kernel_io_components(&plan)?; + let minimum_bytes_per_point = largest_kernel_io + .checked_mul(complex_bytes) + .and_then(|value| value.checked_add(persistent_bytes_per_point)) + .ok_or_else(|| RusticolError::invalid_argument("eager packet workspace overflows"))?; + let dynamic_bytes = options + .workspace_bytes + .saturating_sub(static_bytes) + .max(minimum_bytes_per_point); + let workspace_maximum_tile = dynamic_bytes + .checked_div(minimum_bytes_per_point) + .unwrap_or(0); + let tile_capacity = effective_tile_capacity( + options.point_tile_size, + workspace_maximum_tile, + persistent_bytes_per_point, + ); + let persistent_bytes = persistent_bytes_per_point + .checked_mul(tile_capacity) + .ok_or_else(|| { + RusticolError::invalid_argument("eager persistent workspace overflows") + })?; + let packet_budget = dynamic_bytes + .checked_sub(persistent_bytes) + .ok_or_else(|| { + RusticolError::internal("eager workspace accounting lost its packet budget") + })? + .min( + MAX_PACKET_BUFFER_BYTES.max( + largest_kernel_io + .checked_mul(complex_bytes) + .and_then(|bytes| bytes.checked_mul(tile_capacity)) + .ok_or_else(|| { + RusticolError::invalid_argument("eager packet size overflows") + })?, + ), + ); + let schedule = build_schedule(&plan, tile_capacity, packet_budget)?; + let workspace = allocate_workspace(&plan, tile_capacity, schedule.packet_buffer_len)?; + let workspace_bytes = static_bytes + .checked_add(persistent_bytes) + .and_then(|value| { + schedule + .packet_buffer_len + .checked_mul(complex_bytes) + .and_then(|packet| value.checked_add(packet)) + }) + .ok_or_else(|| RusticolError::invalid_argument("eager workspace size overflows"))?; + Ok(Self { + plan, + schedule, + workspace, + workspace_bytes, + packet_budget, + selected: None, + point_selected: PointSelectedExecution::default(), + }) + } + + pub fn plan(&self) -> &EagerExecutionPlan { + &self.plan + } + + pub fn effective_point_tile_size(&self) -> usize { + self.workspace.tile_capacity + } + + pub fn workspace_bytes(&self) -> usize { + self.workspace_bytes + } + + pub fn packet_count(&self) -> usize { + self.schedule + .stages + .iter() + .map(|stage| stage.invocation_packets.len() + stage.finalization_packets.len()) + .sum::() + + self.schedule.closure_packets.len() + } + + #[allow(dead_code)] // Retained for the opt-in Direct-Arena/packet schedule oracle. + pub(crate) fn schedule_audit( + &mut self, + active_groups: Option<&[u32]>, + ) -> RusticolResult> { + if let Some(groups) = active_groups { + self.prepare_selected_execution(groups)?; + } + let (stages, closures, direct_closures, _, schedule) = selected_execution_rows( + &self.plan, + &self.schedule, + self.selected.as_ref(), + active_groups.is_some(), + ); + let mut rows = Vec::new(); + for (stage, stage_schedule) in stages.iter().zip(&schedule.stages) { + for packet in &stage_schedule.invocation_packets { + let items = &stage.invocations[packet.item_range.clone()]; + rows.push(EagerScheduleAuditRow { + stage_index: Some(stage.stage_index), + role: "invocation", + kernel_id: Some(packet.kernel_id), + call_count: 1, + row_count: items.len(), + destination_count: items.iter().map(|item| item.attachment_range.len()).sum(), + }); + } + if !stage.finalization_copies.is_empty() { + rows.push(EagerScheduleAuditRow { + stage_index: Some(stage.stage_index), + role: "copy", + kernel_id: None, + call_count: 0, + row_count: stage.finalization_copies.len(), + destination_count: stage + .finalization_copies + .iter() + .map(|item| item.current.len) + .sum(), + }); + } + for packet in &stage_schedule.finalization_packets { + let items = &stage.finalizations[packet.item_range.clone()]; + rows.push(EagerScheduleAuditRow { + stage_index: Some(stage.stage_index), + role: "finalization", + kernel_id: Some(packet.kernel_id), + call_count: 1, + row_count: items.len(), + destination_count: items.len(), + }); + } + } + for packet in &schedule.closure_packets { + let items = &closures[packet.item_range.clone()]; + rows.push(EagerScheduleAuditRow { + stage_index: None, + role: "closure", + kernel_id: Some(packet.kernel_id), + call_count: 1, + row_count: items.len(), + destination_count: items.len(), + }); + } + if !direct_closures.is_empty() { + rows.push(EagerScheduleAuditRow { + stage_index: None, + role: "direct-closure", + kernel_id: None, + call_count: 0, + row_count: direct_closures.len(), + destination_count: direct_closures.len(), + }); + } + Ok(rows) + } + + pub fn selector_group_ids(&self) -> Option> { + let domains = self.plan.selector_domains.as_ref()?; + Some(domains.group_ids.clone()) + } + + /// Evaluates one already-resolved coherent selector group per logical point. + /// + /// This is an internal bridge for the native runtime selector planner. It + /// deliberately accepts coherent group IDs rather than public helicity or + /// colour labels; the engine resolves those labels before entering the + /// eager scheduler. + #[allow(clippy::too_many_arguments)] + #[allow(dead_code)] + pub(crate) fn evaluate_point_selected_groups_into( + &mut self, + backend: &mut B, + groups_by_point: &[u32], + active_group_weights: &[f64], + point_count: usize, + initial_values: &[EagerComplex64], + momenta: &[f64], + model_parameters: &[EagerComplex64], + reduced: &mut [f64], + ) -> RusticolResult<()> { + self.evaluate_point_selected_impl( + backend, + PointSelectorInput::OneGroupPerPoint(groups_by_point), + active_group_weights, + point_count, + initial_values, + momenta, + model_parameters, + reduced, + ) + } + + /// Evaluates one pre-resolved active coherent-group set per logical point. + /// + /// `group_offsets` has `point_count + 1` entries and slices + /// `active_groups`. Every non-empty set must be sorted and unique. Empty + /// sets are valid and represent a certified structural-zero selector. + #[allow(clippy::too_many_arguments)] + #[allow(dead_code)] // Retained as the order-preserving benchmark reference lane. + pub(crate) fn evaluate_point_selected_group_sets_into( + &mut self, + backend: &mut B, + group_offsets: &[usize], + active_groups: &[u32], + active_group_weights: &[f64], + point_count: usize, + initial_values: &[EagerComplex64], + momenta: &[f64], + model_parameters: &[EagerComplex64], + reduced: &mut [f64], + ) -> RusticolResult<()> { + self.evaluate_point_selected_impl( + backend, + PointSelectorInput::GroupSets { + offsets: group_offsets, + groups: active_groups, + }, + active_group_weights, + point_count, + initial_values, + momenta, + model_parameters, + reduced, + ) + } + + #[allow(clippy::too_many_arguments)] + fn evaluate_point_selected_impl( + &mut self, + backend: &mut B, + selectors: PointSelectorInput<'_>, + active_group_weights: &[f64], + point_count: usize, + initial_values: &[EagerComplex64], + momenta: &[f64], + model_parameters: &[EagerComplex64], + reduced: &mut [f64], + ) -> RusticolResult<()> { + validate_point_selected_execution_buffers( + &self.plan, + &selectors, + active_group_weights, + point_count, + initial_values, + momenta, + model_parameters, + reduced, + )?; + validate_model_parameters(model_parameters)?; + self.prepare_point_selected_execution(&selectors)?; + resolve_couplings( + &self.plan.couplings, + model_parameters, + &mut self.workspace.couplings, + ); + reduced.fill(0.0); + + let Self { + plan, + workspace, + point_selected, + .. + } = self; + let PointSelectedExecution { + grouped_point_indices, + work_groups, + executions, + gathered_initial_values, + gathered_momenta, + gathered_group_weights, + .. + } = point_selected; + let tile_capacity = workspace.tile_capacity; + for work_group in work_groups.iter().copied() { + let Some(execution_index) = work_group.execution_index else { + continue; + }; + let execution = executions.get(execution_index).ok_or_else(|| { + RusticolError::internal("eager point selector lost its cached execution") + })?; + let point_stop = work_group + .point_start + .checked_add(work_group.point_count) + .ok_or_else(|| RusticolError::internal("eager point work range overflows"))?; + let points = grouped_point_indices + .get(work_group.point_start..point_stop) + .ok_or_else(|| RusticolError::internal("eager point work range is invalid"))?; + for point_tile in points.chunks(tile_capacity) { + gather_point_components( + initial_values, + plan.values.component_count, + point_count, + point_tile, + gathered_initial_values, + )?; + gather_point_components( + momenta, + plan.momenta.component_count, + point_count, + point_tile, + gathered_momenta, + )?; + let tile_points = point_tile.len(); + gather_selected_group_weights( + &selectors, + active_group_weights, + &execution.active_reduction_group_ids, + point_tile, + gathered_group_weights, + )?; + initialize_tile( + plan, + workspace, + tile_points, + 0, + tile_points, + gathered_initial_values, + &execution.active_zero_amplitude_indices, + ); + for (stage, stage_schedule) in + execution.stages.iter().zip(&execution.schedule.stages) + { + execute_stage( + stage, + stage_schedule, + &plan.kernels, + workspace, + backend, + tile_points, + 0, + tile_points, + gathered_momenta, + model_parameters, + )?; + } + execute_closures( + ClosureExecutionRows { + closures: &execution.closures, + direct_closures: &execution.direct_closures, + kernels: &plan.kernels, + }, + &execution.schedule.closure_packets, + workspace, + backend, + tile_points, + model_parameters, + )?; + reduce_selected_tile( + plan, + &execution.active_reduction_group_indices, + &execution.active_reduction_entry_indices, + gathered_group_weights, + workspace, + tile_points, + )?; + for (tile_point, original_point) in point_tile.iter().copied().enumerate() { + reduced[original_point] = workspace.reduced[tile_point]; + } + } + } + Ok(()) + } + + #[allow(clippy::too_many_arguments)] + pub fn evaluate_selected_amplitudes_into( + &mut self, + backend: &mut B, + active_groups: &[u32], + point_count: usize, + initial_values: &[EagerComplex64], + momenta: &[f64], + model_parameters: &[EagerComplex64], + amplitudes: &mut [EagerComplex64], + ) -> RusticolResult<()> { + self.evaluate_selected_amplitudes_impl( + backend, + active_groups, + point_count, + initial_values, + momenta, + model_parameters, + amplitudes, + false, + ) + } + + #[allow(clippy::too_many_arguments)] + #[allow(dead_code)] // Retained for the packet-scheduler selector oracle. + pub(crate) fn evaluate_selected_active_amplitudes_into( + &mut self, + backend: &mut B, + active_groups: &[u32], + point_count: usize, + initial_values: &[EagerComplex64], + momenta: &[f64], + model_parameters: &[EagerComplex64], + amplitudes: &mut [EagerComplex64], + ) -> RusticolResult<()> { + self.evaluate_selected_amplitudes_impl( + backend, + active_groups, + point_count, + initial_values, + momenta, + model_parameters, + amplitudes, + true, + ) + } + + #[allow(clippy::too_many_arguments)] + fn evaluate_selected_amplitudes_impl( + &mut self, + backend: &mut B, + active_groups: &[u32], + point_count: usize, + initial_values: &[EagerComplex64], + momenta: &[f64], + model_parameters: &[EagerComplex64], + amplitudes: &mut [EagerComplex64], + active_amplitudes_only: bool, + ) -> RusticolResult<()> { + validate_selected_execution_buffers( + &self.plan, + point_count, + initial_values, + momenta, + model_parameters, + amplitudes, + )?; + validate_model_parameters(model_parameters)?; + self.prepare_selected_execution(active_groups)?; + let (plan, full_schedule, selected, workspace) = ( + &self.plan, + &self.schedule, + &self.selected, + &mut self.workspace, + ); + resolve_couplings(&plan.couplings, model_parameters, &mut workspace.couplings); + let (stages, closures, direct_closures, zero_amplitudes, schedule) = + selected_execution_rows( + plan, + full_schedule, + selected.as_ref(), + active_amplitudes_only, + ); + let active_amplitude_indices = selected + .as_ref() + .map(|selected| selected.active_amplitude_indices.as_slice()); + let tile_capacity = workspace.tile_capacity; + let mut tile_start = 0usize; + while tile_start < point_count { + let tile_points = min(tile_capacity, point_count - tile_start); + initialize_tile( + plan, + workspace, + point_count, + tile_start, + tile_points, + initial_values, + zero_amplitudes, + ); + for (stage, stage_schedule) in stages.iter().zip(&schedule.stages) { + execute_stage( + stage, + stage_schedule, + &plan.kernels, + workspace, + backend, + point_count, + tile_start, + tile_points, + momenta, + model_parameters, + )?; + } + execute_closures( + ClosureExecutionRows { + closures, + direct_closures, + kernels: &plan.kernels, + }, + &schedule.closure_packets, + workspace, + backend, + tile_points, + model_parameters, + )?; + if active_amplitudes_only { + copy_selected_tile_amplitudes( + active_amplitude_indices.unwrap_or(&[]), + workspace, + point_count, + tile_start, + tile_points, + amplitudes, + ); + } else { + copy_tile_amplitudes( + plan, + workspace, + point_count, + tile_start, + tile_points, + amplitudes, + ); + } + tile_start += tile_points; + } + Ok(()) + } + + #[allow(clippy::too_many_arguments)] + #[allow(dead_code)] // Retained for the packet-scheduler profiling oracle. + pub(crate) fn evaluate_selected_amplitudes_profile_into( + &mut self, + backend: &mut B, + active_groups: &[u32], + point_count: usize, + initial_values: &[EagerComplex64], + momenta: &[f64], + model_parameters: &[EagerComplex64], + amplitudes: &mut [EagerComplex64], + ) -> RusticolResult { + validate_selected_execution_buffers( + &self.plan, + point_count, + initial_values, + momenta, + model_parameters, + amplitudes, + )?; + validate_model_parameters(model_parameters)?; + let total_started = Instant::now(); + self.prepare_selected_execution(active_groups)?; + let (plan, full_schedule, selected, workspace) = ( + &self.plan, + &self.schedule, + &self.selected, + &mut self.workspace, + ); + let initialize_started = Instant::now(); + resolve_couplings(&plan.couplings, model_parameters, &mut workspace.couplings); + let mut profile = EagerExecutionProfile { + initialize: initialize_started.elapsed(), + ..EagerExecutionProfile::default() + }; + let (stages, closures, direct_closures, zero_amplitudes, schedule) = + selected_execution_rows(plan, full_schedule, selected.as_ref(), true); + let active_amplitude_indices = selected + .as_ref() + .map(|selected| selected.active_amplitude_indices.as_slice()); + let tile_capacity = workspace.tile_capacity; + let mut tile_start = 0usize; + while tile_start < point_count { + let tile_points = min(tile_capacity, point_count - tile_start); + let initialize_started = Instant::now(); + initialize_tile( + plan, + workspace, + point_count, + tile_start, + tile_points, + initial_values, + zero_amplitudes, + ); + profile.initialize += initialize_started.elapsed(); + for (stage, stage_schedule) in stages.iter().zip(&schedule.stages) { + execute_stage_profiled( + stage, + stage_schedule, + &plan.kernels, + workspace, + backend, + point_count, + tile_start, + tile_points, + momenta, + model_parameters, + &mut profile, + )?; + } + execute_closures_profiled( + ClosureExecutionRows { + closures, + direct_closures, + kernels: &plan.kernels, + }, + &schedule.closure_packets, + workspace, + backend, + tile_points, + model_parameters, + &mut profile, + )?; + let copy_started = Instant::now(); + copy_selected_tile_amplitudes( + active_amplitude_indices.unwrap_or(&[]), + workspace, + point_count, + tile_start, + tile_points, + amplitudes, + ); + profile.copy_out += copy_started.elapsed(); + tile_start += tile_points; + } + profile.total = total_started.elapsed(); + Ok(profile) + } + + fn prepare_selected_execution(&mut self, active_groups: &[u32]) -> RusticolResult<()> { + if active_groups.windows(2).any(|pair| pair[0] >= pair[1]) { + return Err(RusticolError::invalid_argument( + "eager active selector groups must be sorted and unique", + )); + } + if self.plan.selector_domains.is_none() { + self.selected = None; + return Ok(()); + } + let selector_domains = self + .plan + .selector_domains + .as_ref() + .expect("selector domains were checked above"); + if let Some(unknown) = active_groups + .iter() + .find(|group| selector_domains.group_ids.binary_search(group).is_err()) + { + return Err(RusticolError::invalid_argument(format!( + "eager selector references unknown coherent group {unknown}" + ))); + } + if self + .selected + .as_ref() + .is_some_and(|selected| selected.active_groups == active_groups) + { + return Ok(()); + } + self.selected = Some(build_selected_execution( + &self.plan, + active_groups, + self.workspace.tile_capacity, + self.packet_budget, + )?); + Ok(()) + } + + fn prepare_point_selected_execution( + &mut self, + selectors: &PointSelectorInput<'_>, + ) -> RusticolResult<()> { + if selectors.matches_signature( + &self.point_selected.signature_offsets, + &self.point_selected.signature_groups, + ) { + return Ok(()); + } + let selector_domains = self.plan.selector_domains.as_ref().ok_or_else(|| { + RusticolError::invalid_argument( + "eager per-point selectors require selector-domain metadata", + ) + })?; + + self.point_selected.point_work_group_ids.clear(); + self.point_selected.work_groups.clear(); + self.point_selected + .point_work_group_ids + .try_reserve(selectors.point_count()) + .map_err(|error| { + RusticolError::invalid_argument(format!( + "could not reserve eager point selector work items: {error}" + )) + })?; + + for point in 0..selectors.point_count() { + let groups = selectors.groups_for_point(point); + validate_point_group_set(groups, &selector_domains.group_ids, point)?; + let execution_index = if groups.is_empty() { + None + } else if let Some(index) = self + .point_selected + .executions + .iter() + .position(|execution| execution.active_groups == groups) + { + Some(index) + } else { + let execution = build_selected_execution( + &self.plan, + groups, + self.workspace.tile_capacity, + self.packet_budget, + )?; + self.point_selected.executions.push(execution); + Some(self.point_selected.executions.len() - 1) + }; + let work_group_id = self + .point_selected + .work_groups + .iter() + .position(|work| work.execution_index == execution_index) + .unwrap_or_else(|| { + self.point_selected + .work_groups + .push(PointSelectorWorkGroup { + execution_index, + point_start: 0, + point_count: 0, + }); + self.point_selected.work_groups.len() - 1 + }); + self.point_selected.work_groups[work_group_id].point_count += 1; + self.point_selected.point_work_group_ids.push(work_group_id); + } + + let mut cursor = 0usize; + self.point_selected.work_group_cursors.clear(); + self.point_selected + .work_group_cursors + .try_reserve(self.point_selected.work_groups.len()) + .map_err(|error| { + RusticolError::invalid_argument(format!( + "could not reserve eager point selector cursors: {error}" + )) + })?; + for work in &mut self.point_selected.work_groups { + work.point_start = cursor; + cursor = cursor.checked_add(work.point_count).ok_or_else(|| { + RusticolError::invalid_argument("eager point selector work range overflows") + })?; + self.point_selected + .work_group_cursors + .push(work.point_start); + } + self.point_selected.grouped_point_indices.clear(); + self.point_selected + .grouped_point_indices + .try_reserve(cursor) + .map_err(|error| { + RusticolError::invalid_argument(format!( + "could not reserve eager grouped point indices: {error}" + )) + })?; + self.point_selected.grouped_point_indices.resize(cursor, 0); + for (point, work_group_id) in self + .point_selected + .point_work_group_ids + .iter() + .copied() + .enumerate() + { + let target = self.point_selected.work_group_cursors[work_group_id]; + self.point_selected.grouped_point_indices[target] = point; + self.point_selected.work_group_cursors[work_group_id] += 1; + } + selectors.store_signature( + &mut self.point_selected.signature_offsets, + &mut self.point_selected.signature_groups, + ); + Ok(()) + } + + #[allow(clippy::too_many_arguments)] + pub fn evaluate_into( + &mut self, + backend: &mut B, + point_count: usize, + initial_values: &[EagerComplex64], + momenta: &[f64], + model_parameters: &[EagerComplex64], + amplitudes: &mut [EagerComplex64], + reduced: &mut [f64], + ) -> RusticolResult<()> { + validate_execution_buffers( + &self.plan, + point_count, + initial_values, + momenta, + model_parameters, + amplitudes, + reduced, + )?; + if model_parameters + .iter() + .any(|value| !complex_is_finite(*value)) + { + return Err(RusticolError::invalid_argument( + "eager model parameters must be finite", + )); + } + resolve_couplings( + &self.plan.couplings, + model_parameters, + &mut self.workspace.couplings, + ); + let tile_capacity = self.workspace.tile_capacity; + let mut tile_start = 0usize; + while tile_start < point_count { + let tile_points = min(tile_capacity, point_count - tile_start); + initialize_tile( + &self.plan, + &mut self.workspace, + point_count, + tile_start, + tile_points, + initial_values, + &self.plan.zero_amplitude_indices, + ); + for (stage, schedule) in self.plan.stages.iter().zip(&self.schedule.stages) { + execute_stage( + stage, + schedule, + &self.plan.kernels, + &mut self.workspace, + backend, + point_count, + tile_start, + tile_points, + momenta, + model_parameters, + )?; + } + execute_closures( + ClosureExecutionRows { + closures: &self.plan.closures, + direct_closures: &self.plan.direct_closures, + kernels: &self.plan.kernels, + }, + &self.schedule.closure_packets, + &mut self.workspace, + backend, + tile_points, + model_parameters, + )?; + reduce_tile(&self.plan, &mut self.workspace, tile_points); + copy_tile_results( + &self.plan, + &self.workspace, + point_count, + tile_start, + tile_points, + amplitudes, + reduced, + ); + tile_start += tile_points; + } + Ok(()) + } + + #[allow(clippy::too_many_arguments)] + #[allow(dead_code)] // Retained for the packet-scheduler profiling oracle. + pub(crate) fn evaluate_profile_into( + &mut self, + backend: &mut B, + point_count: usize, + initial_values: &[EagerComplex64], + momenta: &[f64], + model_parameters: &[EagerComplex64], + amplitudes: &mut [EagerComplex64], + reduced: &mut [f64], + ) -> RusticolResult { + validate_execution_buffers( + &self.plan, + point_count, + initial_values, + momenta, + model_parameters, + amplitudes, + reduced, + )?; + if model_parameters + .iter() + .any(|value| !complex_is_finite(*value)) + { + return Err(RusticolError::invalid_argument( + "eager model parameters must be finite", + )); + } + + let total_started = Instant::now(); + let initialize_started = Instant::now(); + resolve_couplings( + &self.plan.couplings, + model_parameters, + &mut self.workspace.couplings, + ); + let mut profile = EagerExecutionProfile { + initialize: initialize_started.elapsed(), + ..EagerExecutionProfile::default() + }; + let tile_capacity = self.workspace.tile_capacity; + let mut tile_start = 0usize; + while tile_start < point_count { + let tile_points = min(tile_capacity, point_count - tile_start); + + let initialize_started = Instant::now(); + initialize_tile( + &self.plan, + &mut self.workspace, + point_count, + tile_start, + tile_points, + initial_values, + &self.plan.zero_amplitude_indices, + ); + profile.initialize += initialize_started.elapsed(); + + for (stage, schedule) in self.plan.stages.iter().zip(&self.schedule.stages) { + execute_stage_profiled( + stage, + schedule, + &self.plan.kernels, + &mut self.workspace, + backend, + point_count, + tile_start, + tile_points, + momenta, + model_parameters, + &mut profile, + )?; + } + execute_closures_profiled( + ClosureExecutionRows { + closures: &self.plan.closures, + direct_closures: &self.plan.direct_closures, + kernels: &self.plan.kernels, + }, + &self.schedule.closure_packets, + &mut self.workspace, + backend, + tile_points, + model_parameters, + &mut profile, + )?; + + let reduction_started = Instant::now(); + reduce_tile(&self.plan, &mut self.workspace, tile_points); + profile.reduction += reduction_started.elapsed(); + + let copy_started = Instant::now(); + copy_tile_results( + &self.plan, + &self.workspace, + point_count, + tile_start, + tile_points, + amplitudes, + reduced, + ); + profile.copy_out += copy_started.elapsed(); + tile_start += tile_points; + } + profile.total = total_started.elapsed(); + debug_assert!(profile.accounted() <= profile.total); + Ok(profile) + } +} + +fn effective_tile_capacity( + requested_tile: usize, + workspace_maximum_tile: usize, + persistent_bytes_per_point: usize, +) -> usize { + let cache_maximum_tile = (MAX_PERSISTENT_TILE_BYTES / persistent_bytes_per_point).max(1); + let maximum_tile = workspace_maximum_tile.min(cache_maximum_tile); + let mut tile_capacity = min(requested_tile, maximum_tile); + if tile_capacity < requested_tile && tile_capacity >= PREFERRED_TILE_ALIGNMENT { + tile_capacity -= tile_capacity % PREFERRED_TILE_ALIGNMENT; + } + tile_capacity +} + +fn used_kernel_io_components(plan: &EagerExecutionPlan) -> RusticolResult { + let mut largest = 0usize; + let mut record = |kernel_id: u32| -> RusticolResult<()> { + let kernel = plan.kernels.get(&kernel_id).ok_or_else(|| { + RusticolError::internal(format!("eager schedule lost kernel {kernel_id}")) + })?; + let count = usize::try_from(kernel.output_component_count) + .ok() + .and_then(|output| kernel.inputs.len().checked_add(output)) + .ok_or_else(|| RusticolError::artifact("eager kernel width overflows usize"))?; + largest = largest.max(count); + Ok(()) + }; + for stage in &plan.stages { + for item in &stage.invocations { + record(item.row.kernel_id)?; + } + for item in &stage.finalizations { + record(item.row.kernel_id)?; + } + } + for item in &plan.closures { + record(item.row.kernel_id)?; + } + Ok(largest.max(1)) +} + +fn build_schedule( + plan: &EagerExecutionPlan, + tile_capacity: usize, + packet_budget: usize, +) -> RusticolResult { + build_schedule_for_rows( + &plan.kernels, + &plan.stages, + &plan.closures, + tile_capacity, + packet_budget, + ) +} + +fn build_schedule_for_rows( + kernels: &BTreeMap, + stage_rows: &[EagerStagePlan], + closure_rows: &[ScheduledClosure], + tile_capacity: usize, + packet_budget: usize, +) -> RusticolResult { + let mut stages = Vec::new(); + stages + .try_reserve_exact(stage_rows.len()) + .map_err(|error| { + RusticolError::invalid_argument(format!("could not reserve eager schedule: {error}")) + })?; + let mut packet_buffer_len = 0usize; + for stage in stage_rows { + let invocation_packets = packetize( + PacketRole::Invocation, + &stage + .invocations + .iter() + .map(|item| item.row.kernel_id) + .collect::>(), + kernels, + tile_capacity, + packet_budget, + &mut packet_buffer_len, + )?; + let finalization_ids = stage + .finalizations + .iter() + .map(|item| item.row.kernel_id) + .collect::>(); + let mut finalization_packets = packetize( + PacketRole::Finalization, + &finalization_ids, + kernels, + tile_capacity, + packet_budget, + &mut packet_buffer_len, + )?; + configure_linear_finalization_packets( + &mut finalization_packets, + &stage.finalizations, + kernels, + )?; + stages.push(StageSchedule { + invocation_packets, + finalization_packets, + }); + } + let closure_ids = closure_rows + .iter() + .map(|item| item.row.kernel_id) + .collect::>(); + let closure_packets = packetize( + PacketRole::Closure, + &closure_ids, + kernels, + tile_capacity, + packet_budget, + &mut packet_buffer_len, + )?; + Ok(ExecutionSchedule { + stages, + closure_packets, + packet_buffer_len, + }) +} + +fn build_selected_execution( + plan: &EagerExecutionPlan, + active_groups: &[u32], + tile_capacity: usize, + packet_budget: usize, +) -> RusticolResult { + let domains = plan.selector_domains.as_ref().ok_or_else(|| { + RusticolError::internal("cannot build a selected eager plan without selector domains") + })?; + let domain_active = domains + .memberships + .iter() + .map(|members| { + members + .iter() + .any(|group| active_groups.binary_search(group).is_ok()) + }) + .collect::>(); + let active = |domain_id: Option| -> RusticolResult { + let Some(domain_id) = domain_id else { + return Ok(true); + }; + domain_active + .get(domain_id as usize) + .copied() + .ok_or_else(|| RusticolError::internal("eager selector domain id escaped validation")) + }; + + let mut stages = Vec::with_capacity(plan.stages.len()); + for stage in &plan.stages { + let mut invocations = Vec::new(); + let mut attachments = Vec::new(); + for invocation in &stage.invocations { + if !active(invocation.selector_domain_id)? { + continue; + } + let attachment_start = attachments.len(); + for attachment in &stage.attachments[invocation.attachment_range.clone()] { + if active(attachment.selector_domain_id)? { + attachments.push(*attachment); + } + } + let attachment_count = attachments.len() - attachment_start; + if attachment_count == 0 { + return Err(RusticolError::internal( + "active eager invocation has no active attachment", + )); + } + let mut invocation = invocation.clone(); + invocation.attachment_range = attachment_start..attachment_start + attachment_count; + invocations.push(invocation); + } + let mut finalization_copies = Vec::new(); + for item in &stage.finalization_copies { + if active(item.selector_domain_id)? { + finalization_copies.push(*item); + } + } + let mut finalizations = Vec::new(); + for item in &stage.finalizations { + if active(item.selector_domain_id)? { + finalizations.push(item.clone()); + } + } + stages.push(EagerStagePlan { + stage_index: stage.stage_index, + current_component_count: stage.current_component_count, + invocations, + attachments, + finalization_copies, + finalizations, + zero_current_ranges: Vec::new(), + }); + } + let mut closures = Vec::new(); + for item in &plan.closures { + if active(item.selector_domain_id)? { + closures.push(item.clone()); + } + } + let mut direct_closures = Vec::new(); + for item in &plan.direct_closures { + if active(item.selector_domain_id)? { + direct_closures.push(item.clone()); + } + } + for stage in &mut stages { + stage.zero_current_ranges = super::plan::mark_initial_current_writes( + &stage.invocations, + &mut stage.attachments, + &stage.finalizations, + ); + } + let zero_amplitude_indices = super::plan::mark_initial_amplitude_writes( + &mut closures, + &mut direct_closures, + plan.amplitude_count, + ); + let mut active_amplitude_indices = plan + .reduction_groups + .iter() + .filter(|group| { + active_groups + .binary_search(&group.coherent_group_id) + .is_ok() + }) + .flat_map(|group| group.amplitude_indices.iter().copied()) + .map(|index| index as usize) + .collect::>(); + active_amplitude_indices.sort_unstable(); + active_amplitude_indices.dedup(); + let active_reduction_group_indices = plan + .reduction_groups + .iter() + .enumerate() + .filter_map(|(index, group)| { + active_groups + .binary_search(&group.coherent_group_id) + .is_ok() + .then_some(index) + }) + .collect::>(); + let active_reduction_entry_indices = plan + .reduction_entries + .iter() + .enumerate() + .filter_map(|(index, entry)| { + let left = entry.left_group_index as usize; + let right = entry.right_group_index as usize; + (active_reduction_group_indices.binary_search(&left).is_ok() + && active_reduction_group_indices.binary_search(&right).is_ok()) + .then_some(index) + }) + .collect::>(); + let active_reduction_group_ids = active_reduction_group_indices + .iter() + .map(|index| plan.reduction_groups[*index].coherent_group_id) + .collect(); + let active_zero_amplitude_indices = zero_amplitude_indices + .iter() + .copied() + .filter(|index| active_amplitude_indices.binary_search(index).is_ok()) + .collect::>(); + let schedule = build_schedule_for_rows( + &plan.kernels, + &stages, + &closures, + tile_capacity, + packet_budget, + )?; + Ok(SelectedExecution { + active_groups: active_groups.to_vec(), + active_amplitude_indices, + active_reduction_group_indices, + active_reduction_group_ids, + active_reduction_entry_indices, + stages, + closures, + direct_closures, + zero_amplitude_indices, + active_zero_amplitude_indices, + schedule, + }) +} + +fn selected_execution_rows<'a>( + plan: &'a EagerExecutionPlan, + full_schedule: &'a ExecutionSchedule, + selected: Option<&'a SelectedExecution>, + active_amplitudes_only: bool, +) -> ( + &'a [EagerStagePlan], + &'a [ScheduledClosure], + &'a [ScheduledDirectClosure], + &'a [usize], + &'a ExecutionSchedule, +) { + if let Some(selected) = selected { + return ( + &selected.stages, + &selected.closures, + &selected.direct_closures, + if active_amplitudes_only { + &selected.active_zero_amplitude_indices + } else { + &selected.zero_amplitude_indices + }, + &selected.schedule, + ); + } + ( + &plan.stages, + &plan.closures, + &plan.direct_closures, + &plan.zero_amplitude_indices, + full_schedule, + ) +} + +fn packetize( + role: PacketRole, + kernel_ids: &[u32], + kernels: &BTreeMap, + tile_capacity: usize, + packet_budget: usize, + packet_buffer_len: &mut usize, +) -> RusticolResult> { + // Plan loading stably sorts each role by kernel id. Packetization therefore + // scans contiguous equal-kernel runs and never needs a hot-loop dispatch map. + let mut packets = Vec::new(); + let mut start = 0usize; + while start < kernel_ids.len() { + let kernel_id = kernel_ids[start]; + let mut run_end = start + 1; + while run_end < kernel_ids.len() && kernel_ids[run_end] == kernel_id { + run_end += 1; + } + let kernel = kernels.get(&kernel_id).ok_or_else(|| { + RusticolError::internal(format!("eager schedule lost kernel {kernel_id}")) + })?; + let input_components = kernel.inputs.len(); + let output_components = usize::try_from(kernel.output_component_count) + .map_err(|_| RusticolError::artifact("eager kernel output width does not fit usize"))?; + let bytes_per_invocation = input_components + .checked_add(output_components) + .and_then(|value| value.checked_mul(tile_capacity)) + .and_then(|value| value.checked_mul(size_of::())) + .ok_or_else(|| RusticolError::invalid_argument("eager packet size overflows"))?; + let capacity = packet_budget.checked_div(bytes_per_invocation).unwrap_or(0); + if capacity == 0 { + return Err(RusticolError::invalid_argument(format!( + "eager workspace cannot hold one kernel {kernel_id} packet" + ))); + } + let requested_block_size = if role == PacketRole::Invocation { + usize::try_from(kernel.independent_block_size) + .map_err(|_| RusticolError::artifact("eager block size does not fit usize"))? + } else { + 1 + }; + let block_size = if requested_block_size > 1 && capacity >= requested_block_size { + requested_block_size + } else { + 1 + }; + let blocked_end = if block_size > 1 { + start + ((run_end - start) / block_size) * block_size + } else { + start + }; + let mut packet_start = start; + let blocked_capacity = (capacity / block_size) * block_size; + while packet_start < blocked_end { + let packet_end = min(blocked_end, packet_start + blocked_capacity); + let invocation_count = packet_end - packet_start; + let elements = input_components + .checked_add(output_components) + .and_then(|value| value.checked_mul(invocation_count)) + .and_then(|value| value.checked_mul(tile_capacity)) + .ok_or_else(|| RusticolError::invalid_argument("eager packet size overflows"))?; + *packet_buffer_len = (*packet_buffer_len).max(elements); + packets.push(KernelPacket { + role, + kernel_id, + independent_block_size: block_size, + item_range: packet_start..packet_end, + input_components, + output_components, + linear_finalization: None, + }); + packet_start = packet_end; + } + while packet_start < run_end { + let packet_end = min(run_end, packet_start + capacity); + let invocation_count = packet_end - packet_start; + let elements = input_components + .checked_add(output_components) + .and_then(|value| value.checked_mul(invocation_count)) + .and_then(|value| value.checked_mul(tile_capacity)) + .ok_or_else(|| RusticolError::invalid_argument("eager packet size overflows"))?; + *packet_buffer_len = (*packet_buffer_len).max(elements); + packets.push(KernelPacket { + role, + kernel_id, + independent_block_size: 1, + item_range: packet_start..packet_end, + input_components, + output_components, + linear_finalization: None, + }); + packet_start = packet_end; + } + start = run_end; + } + Ok(packets) +} + +fn configure_linear_finalization_packets( + packets: &mut [KernelPacket], + rows: &[ScheduledFinalization], + kernels: &BTreeMap, +) -> RusticolResult<()> { + for packet in packets { + let items = &rows[packet.item_range.clone()]; + let kernel = kernels.get(&packet.kernel_id).ok_or_else(|| { + RusticolError::internal(format!( + "eager schedule lost finalization kernel {}", + packet.kernel_id + )) + })?; + if !kernel.homogeneous_linear_first_current { + continue; + } + let current_components = kernel + .inputs + .iter() + .filter_map(|input| match input { + super::EagerKernelInput::FirstCurrentComponent(component) => Some(*component), + _ => None, + }) + .collect::>(); + if current_components.is_empty() { + continue; + } + + let mut momentum_groups = Vec::::new(); + let mut item_group_indices = Vec::with_capacity(items.len()); + for item in items { + let group_index = momentum_groups + .iter() + .position(|momentum| *momentum == item.momentum) + .unwrap_or_else(|| { + momentum_groups.push(item.momentum); + momentum_groups.len() - 1 + }); + item_group_indices.push(group_index); + } + let lane_multiplier = momentum_groups + .len() + .checked_mul(current_components.len()) + .ok_or_else(|| { + RusticolError::invalid_argument("eager linear finalization overflows") + })?; + // Basis evaluation is useful only when shared momentum dependence + // reduces the number of backend lanes. Otherwise retain the direct + // prepared-kernel path and its simpler scatter. + if lane_multiplier >= items.len() { + continue; + } + packet.linear_finalization = Some(LinearFinalizationPacket { + momentum_groups, + item_group_indices, + current_components, + }); + } + Ok(()) +} + +fn allocate_workspace( + plan: &EagerExecutionPlan, + tile_capacity: usize, + packet_buffer_len: usize, +) -> RusticolResult { + Ok(EagerWorkspace { + tile_capacity, + values: zeroed_complex_workspace(plan.values.component_count, tile_capacity, "values")?, + currents: zeroed_complex_workspace( + plan.current_workspace_component_count(), + tile_capacity, + "currents", + )?, + amplitudes: zeroed_complex_workspace(plan.amplitude_count, tile_capacity, "amplitudes")?, + reduction_groups: zeroed_complex_workspace( + plan.reduction_groups.len(), + tile_capacity, + "reduction groups", + )?, + couplings: zeroed_complex_values(plan.couplings.len(), "couplings")?, + reduced: zeroed_real_values(tile_capacity, "reduction")?, + packet: zeroed_complex_values(packet_buffer_len, "packet")?, + }) +} + +fn zeroed_complex_workspace( + component_count: usize, + tile_capacity: usize, + name: &str, +) -> RusticolResult> { + let len = component_count.checked_mul(tile_capacity).ok_or_else(|| { + RusticolError::invalid_argument(format!("eager {name} workspace overflows")) + })?; + zeroed_complex_values(len, name) +} + +fn zeroed_complex_values(len: usize, name: &str) -> RusticolResult> { + let mut values = Vec::new(); + values.try_reserve_exact(len).map_err(|error| { + RusticolError::invalid_argument(format!("could not reserve eager {name}: {error}")) + })?; + values.resize(len, EagerComplex64::new(0.0, 0.0)); + Ok(values) +} + +fn zeroed_real_values(len: usize, name: &str) -> RusticolResult> { + let mut values = Vec::new(); + values.try_reserve_exact(len).map_err(|error| { + RusticolError::invalid_argument(format!("could not reserve eager {name}: {error}")) + })?; + values.resize(len, 0.0); + Ok(values) +} + +fn validate_point_group_set( + groups: &[u32], + known_groups: &[u32], + point: usize, +) -> RusticolResult<()> { + for pair in groups.windows(2) { + if pair[0] == pair[1] { + return Err(RusticolError::invalid_argument(format!( + "eager per-point selector {point} duplicates coherent group {}", + pair[0] + ))); + } + if pair[0] > pair[1] { + return Err(RusticolError::invalid_argument(format!( + "eager per-point selector {point} groups must be sorted" + ))); + } + } + if let Some(unknown) = groups + .iter() + .find(|group| known_groups.binary_search(group).is_err()) + { + return Err(RusticolError::invalid_argument(format!( + "eager per-point selector {point} references unknown coherent group {unknown}" + ))); + } + Ok(()) +} + +fn gather_point_components( + source: &[T], + component_count: usize, + point_count: usize, + point_indices: &[usize], + target: &mut Vec, +) -> RusticolResult<()> { + let target_len = component_count + .checked_mul(point_indices.len()) + .ok_or_else(|| RusticolError::invalid_argument("eager point gather length overflows"))?; + if target_len == 0 { + target.clear(); + return Ok(()); + } + let fill = source.first().copied().ok_or_else(|| { + RusticolError::internal("eager nonempty point gather has an empty source buffer") + })?; + target.resize(target_len, fill); + for component in 0..component_count { + let source_start = component * point_count; + let target_start = component * point_indices.len(); + for (target_point, source_point) in point_indices.iter().copied().enumerate() { + target[target_start + target_point] = source[source_start + source_point]; + } + } + Ok(()) +} + +fn gather_selected_group_weights( + selectors: &PointSelectorInput<'_>, + active_group_weights: &[f64], + execution_groups: &[u32], + point_indices: &[usize], + target: &mut Vec, +) -> RusticolResult<()> { + let target_len = execution_groups + .len() + .checked_mul(point_indices.len()) + .ok_or_else(|| { + RusticolError::invalid_argument("eager selected group weight gather overflows") + })?; + target.resize(target_len, 0.0); + for (group_position, group_id) in execution_groups.iter().copied().enumerate() { + let target_start = group_position * point_indices.len(); + for (target_point, source_point) in point_indices.iter().copied().enumerate() { + let groups = selectors.groups_for_point(source_point); + let source_position = groups.binary_search(&group_id).map_err(|_| { + RusticolError::integrity( + "eager grouped selector point is missing an execution group", + ) + })?; + let source_index = selectors.group_offset_for_point(source_point) + source_position; + target[target_start + target_point] = active_group_weights[source_index]; + } + } + Ok(()) +} + +#[allow(clippy::too_many_arguments)] +fn validate_point_selected_execution_buffers( + plan: &EagerExecutionPlan, + selectors: &PointSelectorInput<'_>, + active_group_weights: &[f64], + point_count: usize, + initial_values: &[EagerComplex64], + momenta: &[f64], + model_parameters: &[EagerComplex64], + reduced: &[f64], +) -> RusticolResult<()> { + if selectors.point_count() != point_count { + return Err(RusticolError::invalid_argument(format!( + "eager per-point selector count is {}, expected {point_count}", + selectors.point_count() + ))); + } + if active_group_weights.len() != selectors.group_entry_count() { + return Err(RusticolError::invalid_argument(format!( + "eager active group weight count is {}, expected {}", + active_group_weights.len(), + selectors.group_entry_count(), + ))); + } + if active_group_weights + .iter() + .any(|weight| !weight.is_finite() || *weight < 0.0) + { + return Err(RusticolError::invalid_argument( + "eager active group weights must be nonnegative and finite", + )); + } + if let PointSelectorInput::GroupSets { offsets, groups } = selectors { + if offsets.len() != point_count + 1 { + return Err(RusticolError::invalid_argument(format!( + "eager per-point selector offsets have length {}, expected {}", + offsets.len(), + point_count + 1 + ))); + } + if offsets.first().copied() != Some(0) { + return Err(RusticolError::invalid_argument( + "eager per-point selector offsets must start at zero", + )); + } + if offsets.windows(2).any(|pair| pair[0] > pair[1]) { + return Err(RusticolError::invalid_argument( + "eager per-point selector offsets must be nondecreasing", + )); + } + if offsets.last().copied() != Some(groups.len()) { + return Err(RusticolError::invalid_argument(format!( + "eager per-point selector offsets end at {}, expected {}", + offsets.last().copied().unwrap_or(0), + groups.len() + ))); + } + } + validate_buffer_len( + "initial value", + initial_values.len(), + plan.values.component_count, + point_count, + )?; + validate_buffer_len( + "momentum", + momenta.len(), + plan.momenta.component_count, + point_count, + )?; + if model_parameters.len() != plan.parameter_count { + return Err(RusticolError::invalid_argument(format!( + "eager model parameter buffer has length {}, expected {}", + model_parameters.len(), + plan.parameter_count + ))); + } + if reduced.len() != point_count { + return Err(RusticolError::invalid_argument(format!( + "eager reduced output has length {}, expected {point_count}", + reduced.len() + ))); + } + Ok(()) +} + +#[allow(clippy::too_many_arguments)] +fn validate_execution_buffers( + plan: &EagerExecutionPlan, + point_count: usize, + initial_values: &[EagerComplex64], + momenta: &[f64], + model_parameters: &[EagerComplex64], + amplitudes: &[EagerComplex64], + reduced: &[f64], +) -> RusticolResult<()> { + validate_buffer_len( + "initial value", + initial_values.len(), + plan.values.component_count, + point_count, + )?; + validate_buffer_len( + "momentum", + momenta.len(), + plan.momenta.component_count, + point_count, + )?; + if model_parameters.len() != plan.parameter_count { + return Err(RusticolError::invalid_argument(format!( + "eager model parameter buffer has length {}, expected {}", + model_parameters.len(), + plan.parameter_count + ))); + } + validate_buffer_len( + "amplitude output", + amplitudes.len(), + plan.amplitude_count, + point_count, + )?; + if reduced.len() != point_count { + return Err(RusticolError::invalid_argument(format!( + "eager reduced output has length {}, expected {point_count}", + reduced.len() + ))); + } + Ok(()) +} + +fn validate_selected_execution_buffers( + plan: &EagerExecutionPlan, + point_count: usize, + initial_values: &[EagerComplex64], + momenta: &[f64], + model_parameters: &[EagerComplex64], + amplitudes: &[EagerComplex64], +) -> RusticolResult<()> { + validate_buffer_len( + "initial value", + initial_values.len(), + plan.values.component_count, + point_count, + )?; + validate_buffer_len( + "momentum", + momenta.len(), + plan.momenta.component_count, + point_count, + )?; + if model_parameters.len() != plan.parameter_count { + return Err(RusticolError::invalid_argument(format!( + "eager model parameter buffer has length {}, expected {}", + model_parameters.len(), + plan.parameter_count + ))); + } + validate_buffer_len( + "amplitude output", + amplitudes.len(), + plan.amplitude_count, + point_count, + ) +} + +fn validate_model_parameters(parameters: &[EagerComplex64]) -> RusticolResult<()> { + if parameters.iter().any(|value| !complex_is_finite(*value)) { + return Err(RusticolError::invalid_argument( + "eager model parameters must be finite", + )); + } + Ok(()) +} + +fn validate_buffer_len( + name: &str, + actual: usize, + component_count: usize, + point_count: usize, +) -> RusticolResult<()> { + let expected = component_count.checked_mul(point_count).ok_or_else(|| { + RusticolError::invalid_argument(format!("eager {name} buffer length overflows")) + })?; + if actual != expected { + return Err(RusticolError::invalid_argument(format!( + "eager {name} buffer has length {actual}, expected {expected}" + ))); + } + Ok(()) +} + +fn resolve_couplings( + rows: &[EagerCouplingRow], + parameters: &[EagerComplex64], + output: &mut [EagerComplex64], +) { + for (row, value) in rows.iter().zip(output) { + let real = if row.real_parameter_id == MISSING_U32 { + row.constant_real + } else { + parameters[row.real_parameter_id as usize].re + }; + let imag = if row.imag_parameter_id == MISSING_U32 { + row.constant_imag + } else { + parameters[row.imag_parameter_id as usize].re + }; + *value = EagerComplex64::new(real, imag); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + EagerAttachmentRow, EagerClosureRow, EagerCouplingRow, EagerDirectClosureSpec, + EagerFinalizationRow, EagerInvocationRow, EagerKernelInput, EagerKernelRole, + EagerPlanDefinition, EagerPlanDimensions, EagerPlanPayloads, EagerReductionEntry, + EagerReductionGroup, EagerSelectorDomainIdRow, EagerSelectorDomainRow, + EagerSelectorGroupRow, EagerSelectorPayloads, EagerSelectorStagePayload, EagerStagePayload, + MISSING_U32, + }; + + #[derive(Default)] + struct AddBackend; + + impl EagerKernelBackend for AddBackend { + fn evaluate_batch( + &mut self, + call: super::super::EagerKernelCall<'_>, + ) -> RusticolResult<()> { + assert_eq!(call.kernel_id, 7); + assert_eq!(call.input_component_count, 3); + assert_eq!(call.output_component_count, 1); + for lane in 0..call.lane_count { + let row = lane * call.input_component_count; + call.outputs[lane] = call.inputs[row] + call.inputs[row + 1] + call.inputs[row + 2]; + } + Ok(()) + } + } + + #[derive(Default)] + struct RecordingAddBackend { + lane_counts: Vec, + } + + impl EagerKernelBackend for RecordingAddBackend { + fn evaluate_batch( + &mut self, + call: super::super::EagerKernelCall<'_>, + ) -> RusticolResult<()> { + assert_eq!(call.kernel_id, 7); + assert_eq!(call.input_component_count, 3); + assert_eq!(call.output_component_count, 1); + self.lane_counts.push(call.lane_count); + for lane in 0..call.lane_count { + let row = lane * call.input_component_count; + call.outputs[lane] = call.inputs[row] + call.inputs[row + 1] + call.inputs[row + 2]; + } + Ok(()) + } + } + + fn selector_runtime() -> EagerExecutionRuntime { + let definition = EagerPlanDefinition { + dimensions: EagerPlanDimensions { + value_slot_component_counts: vec![1, 1, 1, 1], + momentum_slot_component_counts: vec![1], + current_component_counts: vec![1, 1], + parameter_count: 0, + amplitude_count: 2, + }, + kernels: vec![EagerKernelSpec { + kernel_id: 7, + role: EagerKernelRole::Vertex, + inputs: vec![ + EagerKernelInput::FirstCurrentComponent(0), + EagerKernelInput::SecondCurrentComponent(0), + EagerKernelInput::FirstMomentumComponent(0), + ], + output_component_count: 1, + homogeneous_linear_first_current: false, + independent_block_size: 1, + }], + direct_closures: vec![ + EagerDirectClosureSpec { + closure_index: 0, + coefficients: vec![EagerComplex64::new(1.0, 0.0)], + }, + EagerDirectClosureSpec { + closure_index: 1, + coefficients: vec![EagerComplex64::new(1.0, 0.0)], + }, + ], + reduction_groups: vec![ + EagerReductionGroup { + coherent_group_id: 10, + amplitude_indices: vec![0], + }, + EagerReductionGroup { + coherent_group_id: 20, + amplitude_indices: vec![1], + }, + ], + reduction_entries: vec![ + EagerReductionEntry { + left_group_index: 0, + right_group_index: 0, + coefficient: EagerComplex64::new(2.0, 0.0), + }, + EagerReductionEntry { + left_group_index: 1, + right_group_index: 1, + coefficient: EagerComplex64::new(3.0, 0.0), + }, + ], + }; + let couplings = EagerCouplingRow::encode_table(&[EagerCouplingRow { + real_parameter_id: MISSING_U32, + imag_parameter_id: MISSING_U32, + constant_real: 1.0, + constant_imag: 0.0, + }]) + .expect("coupling table"); + let invocations = EagerInvocationRow::encode_table(&[ + EagerInvocationRow { + kernel_id: 7, + left_value_slot_id: 0, + right_value_slot_id: 0, + left_momentum_slot_id: 0, + right_momentum_slot_id: 0, + coupling_slot_id: 0, + output_factor_source: crate::EAGER_OUTPUT_FACTOR_NONE, + attachment_start: 0, + attachment_count: 1, + }, + EagerInvocationRow { + kernel_id: 7, + left_value_slot_id: 1, + right_value_slot_id: 1, + left_momentum_slot_id: 0, + right_momentum_slot_id: 0, + coupling_slot_id: 0, + output_factor_source: crate::EAGER_OUTPUT_FACTOR_NONE, + attachment_start: 1, + attachment_count: 1, + }, + ]) + .expect("invocation table"); + let attachments = EagerAttachmentRow::encode_table(&[ + EagerAttachmentRow { + result_current_id: 0, + factor_real: 1.0, + factor_imag: 0.0, + }, + EagerAttachmentRow { + result_current_id: 1, + factor_real: 1.0, + factor_imag: 0.0, + }, + ]) + .expect("attachment table"); + let finalizations = EagerFinalizationRow::encode_table(&[ + EagerFinalizationRow { + kernel_id: MISSING_U32, + current_id: 0, + unpropagated_value_slot_id: 2, + propagated_value_slot_id: MISSING_U32, + momentum_slot_id: 0, + }, + EagerFinalizationRow { + kernel_id: MISSING_U32, + current_id: 1, + unpropagated_value_slot_id: 3, + propagated_value_slot_id: MISSING_U32, + momentum_slot_id: 0, + }, + ]) + .expect("finalization table"); + let closures = EagerClosureRow::encode_table(&[ + EagerClosureRow { + kernel_id: MISSING_U32, + left_value_slot_id: 2, + right_value_slot_id: 0, + amplitude_index: 0, + coupling_slot_id: MISSING_U32, + output_factor_source: crate::EAGER_OUTPUT_FACTOR_NONE, + factor_real: 1.0, + factor_imag: 0.0, + }, + EagerClosureRow { + kernel_id: MISSING_U32, + left_value_slot_id: 3, + right_value_slot_id: 0, + amplitude_index: 1, + coupling_slot_id: MISSING_U32, + output_factor_source: crate::EAGER_OUTPUT_FACTOR_NONE, + factor_real: 1.0, + factor_imag: 0.0, + }, + ]) + .expect("closure table"); + let domains = EagerSelectorDomainRow::encode_table(&[ + EagerSelectorDomainRow { + member_start: 0, + member_count: 0, + }, + EagerSelectorDomainRow { + member_start: 0, + member_count: 1, + }, + EagerSelectorDomainRow { + member_start: 1, + member_count: 1, + }, + ]) + .expect("selector domains"); + let domain_group_ids = EagerSelectorGroupRow::encode_table(&[ + EagerSelectorGroupRow { + coherent_group_id: 10, + }, + EagerSelectorGroupRow { + coherent_group_id: 20, + }, + ]) + .expect("selector group ids"); + let invocation_domains = EagerSelectorDomainIdRow::encode_table(&[ + EagerSelectorDomainIdRow { domain_id: 1 }, + EagerSelectorDomainIdRow { domain_id: 2 }, + ]) + .expect("invocation selector domains"); + let attachment_domains = invocation_domains.clone(); + let unpropagated_finalization_domains = invocation_domains.clone(); + let propagated_finalization_domains = EagerSelectorDomainIdRow::encode_table(&[ + EagerSelectorDomainIdRow { domain_id: 0 }, + EagerSelectorDomainIdRow { domain_id: 0 }, + ]) + .expect("propagated selector domains"); + let closure_domains = invocation_domains.clone(); + let stage = EagerStagePayload { + stage_index: 1, + invocations: &invocations, + attachments: &attachments, + finalizations: &finalizations, + }; + let selector_stage = EagerSelectorStagePayload { + stage_index: 1, + invocation_domains: &invocation_domains, + attachment_domains: &attachment_domains, + unpropagated_finalization_domains: &unpropagated_finalization_domains, + propagated_finalization_domains: &propagated_finalization_domains, + }; + let plan = EagerExecutionPlan::from_payloads( + definition, + EagerPlanPayloads { + couplings: &couplings, + stages: &[stage], + closures: &closures, + selector_domains: Some(EagerSelectorPayloads { + domains: &domains, + domain_group_ids: &domain_group_ids, + stages: &[selector_stage], + closure_domains: &closure_domains, + }), + }, + ) + .expect("selector plan"); + EagerExecutionRuntime::new( + plan, + EagerRuntimeOptions { + point_tile_size: 8, + workspace_bytes: 64 * 1024, + }, + ) + .expect("selector runtime") + } + + fn selector_inputs() -> (Vec, Vec) { + ( + vec![ + EagerComplex64::new(1.0, 0.0), + EagerComplex64::new(2.0, 0.0), + EagerComplex64::new(3.0, 0.0), + EagerComplex64::new(4.0, 0.0), + EagerComplex64::new(10.0, 0.0), + EagerComplex64::new(20.0, 0.0), + EagerComplex64::new(30.0, 0.0), + EagerComplex64::new(40.0, 0.0), + EagerComplex64::new(0.0, 0.0), + EagerComplex64::new(0.0, 0.0), + EagerComplex64::new(0.0, 0.0), + EagerComplex64::new(0.0, 0.0), + EagerComplex64::new(0.0, 0.0), + EagerComplex64::new(0.0, 0.0), + EagerComplex64::new(0.0, 0.0), + EagerComplex64::new(0.0, 0.0), + ], + vec![0.5, 1.0, 1.5, 2.0], + ) + } + + fn expected_selector_value(group: u32, point: usize) -> f64 { + let x = (point + 1) as f64; + let momentum = 0.5 * (point + 1) as f64; + match group { + 10 => { + let amplitude = (2.0 * x + momentum) * x; + 2.0 * amplitude * amplitude + } + 20 => { + let y = 10.0 * (point + 1) as f64; + let amplitude = (2.0 * y + momentum) * x; + 3.0 * amplitude * amplitude + } + _ => unreachable!("test selector group"), + } + } + + fn profiled_runtime() -> EagerExecutionRuntime { + let definition = EagerPlanDefinition { + dimensions: EagerPlanDimensions { + value_slot_component_counts: vec![1, 1, 1], + momentum_slot_component_counts: vec![1], + current_component_counts: vec![1], + parameter_count: 0, + amplitude_count: 1, + }, + kernels: vec![EagerKernelSpec { + kernel_id: 7, + role: EagerKernelRole::Vertex, + inputs: vec![ + EagerKernelInput::FirstCurrentComponent(0), + EagerKernelInput::SecondCurrentComponent(0), + EagerKernelInput::FirstMomentumComponent(0), + ], + output_component_count: 1, + homogeneous_linear_first_current: false, + independent_block_size: 1, + }], + direct_closures: vec![EagerDirectClosureSpec { + closure_index: 0, + coefficients: vec![EagerComplex64::new(1.0, 0.0)], + }], + reduction_groups: vec![EagerReductionGroup { + coherent_group_id: 0, + amplitude_indices: vec![0], + }], + reduction_entries: vec![EagerReductionEntry { + left_group_index: 0, + right_group_index: 0, + coefficient: EagerComplex64::new(1.0, 0.0), + }], + }; + let couplings = EagerCouplingRow::encode_table(&[EagerCouplingRow { + real_parameter_id: MISSING_U32, + imag_parameter_id: MISSING_U32, + constant_real: 1.0, + constant_imag: 0.0, + }]) + .expect("coupling table"); + let invocations = EagerInvocationRow::encode_table(&[EagerInvocationRow { + kernel_id: 7, + left_value_slot_id: 0, + right_value_slot_id: 1, + left_momentum_slot_id: 0, + right_momentum_slot_id: 0, + coupling_slot_id: 0, + output_factor_source: crate::EAGER_OUTPUT_FACTOR_NONE, + attachment_start: 0, + attachment_count: 1, + }]) + .expect("invocation table"); + let attachments = EagerAttachmentRow::encode_table(&[EagerAttachmentRow { + result_current_id: 0, + factor_real: 1.0, + factor_imag: 0.0, + }]) + .expect("attachment table"); + let finalizations = EagerFinalizationRow::encode_table(&[EagerFinalizationRow { + kernel_id: MISSING_U32, + current_id: 0, + unpropagated_value_slot_id: 2, + propagated_value_slot_id: MISSING_U32, + momentum_slot_id: 0, + }]) + .expect("finalization table"); + let closures = EagerClosureRow::encode_table(&[EagerClosureRow { + kernel_id: MISSING_U32, + left_value_slot_id: 2, + right_value_slot_id: 0, + amplitude_index: 0, + coupling_slot_id: MISSING_U32, + output_factor_source: crate::EAGER_OUTPUT_FACTOR_NONE, + factor_real: 1.0, + factor_imag: 0.0, + }]) + .expect("closure table"); + let stage = EagerStagePayload { + stage_index: 1, + invocations: &invocations, + attachments: &attachments, + finalizations: &finalizations, + }; + let plan = EagerExecutionPlan::from_payloads( + definition, + EagerPlanPayloads { + couplings: &couplings, + stages: &[stage], + closures: &closures, + selector_domains: None, + }, + ) + .expect("profile plan"); + EagerExecutionRuntime::new( + plan, + EagerRuntimeOptions { + point_tile_size: 2, + workspace_bytes: 4096, + }, + ) + .expect("profile runtime") + } + + #[test] + fn profiled_execution_matches_hot_path_and_accounts_for_every_phase() { + let point_count = 4; + let initial_values = [ + EagerComplex64::new(1.0, 0.0), + EagerComplex64::new(2.0, 0.0), + EagerComplex64::new(3.0, 0.0), + EagerComplex64::new(4.0, 0.0), + EagerComplex64::new(10.0, 0.0), + EagerComplex64::new(20.0, 0.0), + EagerComplex64::new(30.0, 0.0), + EagerComplex64::new(40.0, 0.0), + EagerComplex64::new(0.0, 0.0), + EagerComplex64::new(0.0, 0.0), + EagerComplex64::new(0.0, 0.0), + EagerComplex64::new(0.0, 0.0), + ]; + let momenta = [0.5, 1.0, 1.5, 2.0]; + let mut hot = profiled_runtime(); + let mut profiled = profiled_runtime(); + let mut hot_amplitudes = vec![EagerComplex64::new(0.0, 0.0); point_count]; + let mut hot_reduced = vec![0.0; point_count]; + let mut profiled_amplitudes = hot_amplitudes.clone(); + let mut profiled_reduced = hot_reduced.clone(); + + hot.evaluate_into( + &mut AddBackend, + point_count, + &initial_values, + &momenta, + &[], + &mut hot_amplitudes, + &mut hot_reduced, + ) + .expect("hot eager execution"); + let profile = profiled + .evaluate_profile_into( + &mut AddBackend, + point_count, + &initial_values, + &momenta, + &[], + &mut profiled_amplitudes, + &mut profiled_reduced, + ) + .expect("profile eager execution"); + + assert_eq!(profiled_amplitudes, hot_amplitudes); + assert_eq!(profiled_reduced, hot_reduced); + assert!(!profile.initialize.is_zero()); + assert!(!profile.gather.is_zero()); + assert!(!profile.kernel_call.is_zero()); + assert!(!profile.invocation_scatter.is_zero()); + assert!(!profile.finalization.is_zero()); + assert!(!profile.closure.is_zero()); + assert!(!profile.reduction.is_zero()); + assert!(!profile.copy_out.is_zero()); + assert!(profile.accounted() <= profile.total); + } + + #[test] + fn point_selectors_group_homogeneous_and_alternating_rows() { + let (initial_values, momenta) = selector_inputs(); + let mut homogeneous = selector_runtime(); + let mut homogeneous_backend = RecordingAddBackend::default(); + let mut homogeneous_output = vec![0.0; 4]; + homogeneous + .evaluate_point_selected_groups_into( + &mut homogeneous_backend, + &[10, 10, 10, 10], + &[1.0; 4], + 4, + &initial_values, + &momenta, + &[], + &mut homogeneous_output, + ) + .expect("homogeneous point-selected execution"); + assert_eq!(homogeneous_backend.lane_counts, vec![4]); + assert_eq!( + homogeneous_output, + (0..4) + .map(|point| expected_selector_value(10, point)) + .collect::>() + ); + + let selectors = [10, 20, 10, 20]; + let mut alternating = selector_runtime(); + let mut alternating_backend = RecordingAddBackend::default(); + let mut alternating_output = vec![0.0; 4]; + alternating + .evaluate_point_selected_groups_into( + &mut alternating_backend, + &selectors, + &[1.0; 4], + 4, + &initial_values, + &momenta, + &[], + &mut alternating_output, + ) + .expect("alternating point-selected execution"); + assert_eq!(alternating_backend.lane_counts, vec![2, 2]); + assert_eq!( + alternating_output, + selectors + .iter() + .copied() + .enumerate() + .map(|(point, group)| expected_selector_value(group, point)) + .collect::>() + ); + assert!( + alternating + .point_selected + .executions + .iter() + .all(|execution| { + execution.stages.len() == 1 + && execution.stages[0].invocations.len() == 1 + && execution.stages[0].finalization_copies.len() == 1 + && execution.direct_closures.len() == 1 + }) + ); + + let randomized_selectors = [20, 10, 10, 20]; + let mut randomized = selector_runtime(); + let mut randomized_backend = RecordingAddBackend::default(); + let mut randomized_output = vec![0.0; 4]; + randomized + .evaluate_point_selected_groups_into( + &mut randomized_backend, + &randomized_selectors, + &[1.0; 4], + 4, + &initial_values, + &momenta, + &[], + &mut randomized_output, + ) + .expect("randomized point-selected execution"); + assert_eq!(randomized_backend.lane_counts, vec![2, 2]); + assert_eq!( + randomized_output, + randomized_selectors + .iter() + .copied() + .enumerate() + .map(|(point, group)| expected_selector_value(group, point)) + .collect::>() + ); + } + + #[test] + fn point_selector_sets_preserve_reduction_weights_and_structural_zeros() { + let (initial_values, momenta) = selector_inputs(); + let mut runtime = selector_runtime(); + let mut backend = RecordingAddBackend::default(); + let mut output = vec![-1.0; 4]; + let active_group_weights = [0.5, 1.0, 2.0, 0.25]; + // point 0 selects group 10, point 1 is a certified structural zero, + // point 2 selects both groups, and point 3 selects group 20. + runtime + .evaluate_point_selected_group_sets_into( + &mut backend, + &[0, 1, 1, 3, 4], + &[10, 10, 20, 20], + &active_group_weights, + 4, + &initial_values, + &momenta, + &[], + &mut output, + ) + .expect("point-selected active-set execution"); + assert_eq!(output[0], 0.5 * expected_selector_value(10, 0)); + assert_eq!(output[1], 0.0); + assert_eq!( + output[2], + expected_selector_value(10, 2) + 2.0 * expected_selector_value(20, 2) + ); + assert_eq!(output[3], 0.25 * expected_selector_value(20, 3)); + assert_eq!(backend.lane_counts, vec![1, 2, 1]); + } + + #[test] + fn point_selector_validation_reports_lengths_unknown_groups_and_duplicates() { + let (initial_values, momenta) = selector_inputs(); + let mut runtime = selector_runtime(); + let mut backend = RecordingAddBackend::default(); + let mut output = vec![0.0; 4]; + + let error = runtime + .evaluate_point_selected_groups_into( + &mut backend, + &[10, 20], + &[1.0; 4], + 4, + &initial_values, + &momenta, + &[], + &mut output, + ) + .unwrap_err(); + assert!( + error + .to_string() + .contains("selector count is 2, expected 4") + ); + + let error = runtime + .evaluate_point_selected_group_sets_into( + &mut backend, + &[0, 1, 2, 3, 4], + &[10, 20, 99, 10], + &[1.0; 4], + 4, + &initial_values, + &momenta, + &[], + &mut output, + ) + .unwrap_err(); + assert!(error.to_string().contains("unknown coherent group 99")); + + let error = runtime + .evaluate_point_selected_group_sets_into( + &mut backend, + &[0, 2, 2, 2, 2], + &[10, 10], + &[1.0; 2], + 4, + &initial_values, + &momenta, + &[], + &mut output, + ) + .unwrap_err(); + assert!(error.to_string().contains("duplicates coherent group 10")); + } + + #[test] + fn warmed_point_selector_execution_reuses_plans_and_buffers() { + let (initial_values, momenta) = selector_inputs(); + let selectors = [10, 20, 10, 20]; + let mut runtime = selector_runtime(); + let mut backend = RecordingAddBackend::default(); + let mut output = vec![0.0; 4]; + runtime + .evaluate_point_selected_groups_into( + &mut backend, + &selectors, + &[1.0; 4], + 4, + &initial_values, + &momenta, + &[], + &mut output, + ) + .expect("warm point selector caches"); + let fingerprint = [ + runtime.point_selected.executions.len(), + runtime.point_selected.work_groups.len(), + runtime.point_selected.signature_offsets.as_ptr() as usize, + runtime.point_selected.signature_offsets.capacity(), + runtime.point_selected.signature_groups.as_ptr() as usize, + runtime.point_selected.signature_groups.capacity(), + runtime.point_selected.point_work_group_ids.as_ptr() as usize, + runtime.point_selected.point_work_group_ids.capacity(), + runtime.point_selected.grouped_point_indices.as_ptr() as usize, + runtime.point_selected.grouped_point_indices.capacity(), + runtime.point_selected.gathered_initial_values.as_ptr() as usize, + runtime.point_selected.gathered_initial_values.capacity(), + runtime.point_selected.gathered_momenta.as_ptr() as usize, + runtime.point_selected.gathered_momenta.capacity(), + ]; + + backend.lane_counts.clear(); + output.fill(0.0); + runtime + .evaluate_point_selected_groups_into( + &mut backend, + &selectors, + &[1.0; 4], + 4, + &initial_values, + &momenta, + &[], + &mut output, + ) + .expect("reuse warmed point selector caches"); + let repeated = [ + runtime.point_selected.executions.len(), + runtime.point_selected.work_groups.len(), + runtime.point_selected.signature_offsets.as_ptr() as usize, + runtime.point_selected.signature_offsets.capacity(), + runtime.point_selected.signature_groups.as_ptr() as usize, + runtime.point_selected.signature_groups.capacity(), + runtime.point_selected.point_work_group_ids.as_ptr() as usize, + runtime.point_selected.point_work_group_ids.capacity(), + runtime.point_selected.grouped_point_indices.as_ptr() as usize, + runtime.point_selected.grouped_point_indices.capacity(), + runtime.point_selected.gathered_initial_values.as_ptr() as usize, + runtime.point_selected.gathered_initial_values.capacity(), + runtime.point_selected.gathered_momenta.as_ptr() as usize, + runtime.point_selected.gathered_momenta.capacity(), + ]; + assert_eq!(repeated, fingerprint); + assert_eq!(backend.lane_counts, vec![2, 2]); + } + + #[test] + fn effective_tile_uses_requested_size_as_an_upper_bound() { + assert_eq!(effective_tile_capacity(32, 4096, 63_240), 32); + assert_eq!(effective_tile_capacity(1024, 4096, 63_240), 264); + assert_eq!(effective_tile_capacity(1024, 4096, usize::MAX), 1); + } + + #[test] + fn shared_linear_finalization_requires_an_exact_kernel_proof() { + let rows = [0_u32, 1_u32].map(|current_id| ScheduledFinalization { + row: EagerFinalizationRow { + kernel_id: 7, + current_id, + unpropagated_value_slot_id: MISSING_U32, + propagated_value_slot_id: current_id, + momentum_slot_id: 0, + }, + current: ComponentRange { + start: current_id as usize, + len: 1, + }, + propagated: Some(ComponentRange { + start: current_id as usize, + len: 1, + }), + momentum: ComponentRange { start: 0, len: 1 }, + selector_domain_id: None, + }); + let packet = KernelPacket { + role: PacketRole::Finalization, + kernel_id: 7, + independent_block_size: 1, + item_range: 0..2, + input_components: 2, + output_components: 1, + linear_finalization: None, + }; + let kernel = |proved| EagerKernelSpec { + kernel_id: 7, + role: EagerKernelRole::Finalization, + inputs: vec![ + EagerKernelInput::FirstCurrentComponent(0), + EagerKernelInput::FirstMomentumComponent(0), + ], + output_component_count: 1, + homogeneous_linear_first_current: proved, + independent_block_size: 1, + }; + + let mut packets = vec![packet.clone()]; + configure_linear_finalization_packets( + &mut packets, + &rows, + &BTreeMap::from([(7, kernel(false))]), + ) + .unwrap(); + assert!(packets[0].linear_finalization.is_none()); + + configure_linear_finalization_packets( + &mut packets, + &rows, + &BTreeMap::from([(7, kernel(true))]), + ) + .unwrap(); + assert_eq!( + packets[0] + .linear_finalization + .as_ref() + .unwrap() + .lane_multiplier(), + 1 + ); + } +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/eager_tables.rs b/pyamplicol/rust/crates/rusticol-core/src/eager_tables.rs new file mode 100644 index 0000000000..6fd41c4f3b --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/eager_tables.rs @@ -0,0 +1,877 @@ +// SPDX-License-Identifier: 0BSD + +use crate::{RusticolError, RusticolResult}; +use std::mem::size_of; + +pub const EAGER_PLAN_ABI: &str = "pyamplicol-eager-plan-v2"; +pub const EAGER_KERNEL_ABI: &str = "pyamplicol-eager-kernel-v1"; +pub const EAGER_SELECTOR_DOMAINS_ABI: &str = "pyamplicol-eager-selector-domains-v1"; +pub const EAGER_RUNTIME_CAPABILITY: &str = "rusticol.eager-dag.complex-f64.v1"; +pub const EAGER_LC_TOPOLOGY_REPLAY_RUNTIME_CAPABILITY: &str = + "rusticol.eager-dag.lc-topology-replay.v1"; +pub const MISSING_U32: u32 = u32::MAX; +pub const EAGER_OUTPUT_FACTOR_NONE: u32 = 0; +pub const EAGER_OUTPUT_FACTOR_COUPLING_REAL: u32 = 1; +pub const EAGER_OUTPUT_FACTOR_COUPLING_IMAG: u32 = 2; + +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct EagerInvocationRow { + pub kernel_id: u32, + pub left_value_slot_id: u32, + pub right_value_slot_id: u32, + pub left_momentum_slot_id: u32, + pub right_momentum_slot_id: u32, + pub coupling_slot_id: u32, + pub output_factor_source: u32, + pub attachment_start: u64, + pub attachment_count: u64, +} + +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct EagerAttachmentRow { + pub result_current_id: u32, + pub factor_real: f64, + pub factor_imag: f64, +} + +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct EagerCouplingRow { + pub real_parameter_id: u32, + pub imag_parameter_id: u32, + pub constant_real: f64, + pub constant_imag: f64, +} + +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct EagerFinalizationRow { + pub kernel_id: u32, + pub current_id: u32, + pub unpropagated_value_slot_id: u32, + pub propagated_value_slot_id: u32, + pub momentum_slot_id: u32, +} + +impl EagerFinalizationRow { + pub fn applies_kernel(self) -> bool { + self.kernel_id != MISSING_U32 + } + + pub fn stores_unpropagated(self) -> bool { + self.unpropagated_value_slot_id != MISSING_U32 + } + + pub fn stores_propagated(self) -> bool { + self.propagated_value_slot_id != MISSING_U32 + } +} + +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct EagerClosureRow { + pub kernel_id: u32, + pub left_value_slot_id: u32, + pub right_value_slot_id: u32, + pub amplitude_index: u32, + pub coupling_slot_id: u32, + pub output_factor_source: u32, + pub factor_real: f64, + pub factor_imag: f64, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct EagerSelectorDomainRow { + pub member_start: u64, + pub member_count: u64, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct EagerSelectorGroupRow { + pub coherent_group_id: u32, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct EagerSelectorDomainIdRow { + pub domain_id: u32, +} + +trait FixedWidthRow: Sized { + const NAME: &'static str; + const WIDTH: usize; + + fn validate_for_encoding(&self, row_index: usize) -> RusticolResult<()>; + fn encode_into(&self, output: &mut Vec); + fn decode_from(bytes: &[u8], row_index: usize) -> RusticolResult; +} + +macro_rules! impl_table_api { + ($row:ty) => { + impl $row { + pub const ENCODED_LEN: usize = ::WIDTH; + + pub fn encode_table(rows: &[Self]) -> RusticolResult> { + encode_rows(rows) + } + + pub fn decode_table(payload: &[u8]) -> RusticolResult> { + decode_rows(payload) + } + } + }; +} + +impl FixedWidthRow for EagerInvocationRow { + const NAME: &'static str = "invocation"; + const WIDTH: usize = 7 * size_of::() + 2 * size_of::(); + + fn validate_for_encoding(&self, row_index: usize) -> RusticolResult<()> { + validate_output_factor_source_for_encoding(Self::NAME, row_index, self.output_factor_source) + } + + fn encode_into(&self, output: &mut Vec) { + push_u32(output, self.kernel_id); + push_u32(output, self.left_value_slot_id); + push_u32(output, self.right_value_slot_id); + push_u32(output, self.left_momentum_slot_id); + push_u32(output, self.right_momentum_slot_id); + push_u32(output, self.coupling_slot_id); + push_u32(output, self.output_factor_source); + push_u64(output, self.attachment_start); + push_u64(output, self.attachment_count); + } + + fn decode_from(bytes: &[u8], row_index: usize) -> RusticolResult { + let mut reader = RowReader::new(Self::NAME, row_index, bytes); + let row = Self { + kernel_id: reader.read_u32()?, + left_value_slot_id: reader.read_u32()?, + right_value_slot_id: reader.read_u32()?, + left_momentum_slot_id: reader.read_u32()?, + right_momentum_slot_id: reader.read_u32()?, + coupling_slot_id: reader.read_u32()?, + output_factor_source: reader.read_u32()?, + attachment_start: reader.read_u64()?, + attachment_count: reader.read_u64()?, + }; + reader.finish()?; + validate_output_factor_source_from_payload( + Self::NAME, + row_index, + row.output_factor_source, + )?; + Ok(row) + } +} + +impl FixedWidthRow for EagerAttachmentRow { + const NAME: &'static str = "attachment"; + const WIDTH: usize = size_of::() + 2 * size_of::(); + + fn validate_for_encoding(&self, row_index: usize) -> RusticolResult<()> { + validate_finite_for_encoding(Self::NAME, row_index, "factor_real", self.factor_real)?; + validate_finite_for_encoding(Self::NAME, row_index, "factor_imag", self.factor_imag) + } + + fn encode_into(&self, output: &mut Vec) { + push_u32(output, self.result_current_id); + push_f64(output, self.factor_real); + push_f64(output, self.factor_imag); + } + + fn decode_from(bytes: &[u8], row_index: usize) -> RusticolResult { + let mut reader = RowReader::new(Self::NAME, row_index, bytes); + let row = Self { + result_current_id: reader.read_u32()?, + factor_real: reader.read_f64()?, + factor_imag: reader.read_f64()?, + }; + reader.finish()?; + validate_finite_from_payload(Self::NAME, row_index, "factor_real", row.factor_real)?; + validate_finite_from_payload(Self::NAME, row_index, "factor_imag", row.factor_imag)?; + Ok(row) + } +} + +impl FixedWidthRow for EagerCouplingRow { + const NAME: &'static str = "coupling"; + const WIDTH: usize = 2 * size_of::() + 2 * size_of::(); + + fn validate_for_encoding(&self, row_index: usize) -> RusticolResult<()> { + validate_finite_for_encoding(Self::NAME, row_index, "constant_real", self.constant_real)?; + validate_finite_for_encoding(Self::NAME, row_index, "constant_imag", self.constant_imag) + } + + fn encode_into(&self, output: &mut Vec) { + push_u32(output, self.real_parameter_id); + push_u32(output, self.imag_parameter_id); + push_f64(output, self.constant_real); + push_f64(output, self.constant_imag); + } + + fn decode_from(bytes: &[u8], row_index: usize) -> RusticolResult { + let mut reader = RowReader::new(Self::NAME, row_index, bytes); + let row = Self { + real_parameter_id: reader.read_u32()?, + imag_parameter_id: reader.read_u32()?, + constant_real: reader.read_f64()?, + constant_imag: reader.read_f64()?, + }; + reader.finish()?; + validate_finite_from_payload(Self::NAME, row_index, "constant_real", row.constant_real)?; + validate_finite_from_payload(Self::NAME, row_index, "constant_imag", row.constant_imag)?; + Ok(row) + } +} + +impl FixedWidthRow for EagerFinalizationRow { + const NAME: &'static str = "finalization"; + const WIDTH: usize = 5 * size_of::(); + + fn validate_for_encoding(&self, _row_index: usize) -> RusticolResult<()> { + Ok(()) + } + + fn encode_into(&self, output: &mut Vec) { + push_u32(output, self.kernel_id); + push_u32(output, self.current_id); + push_u32(output, self.unpropagated_value_slot_id); + push_u32(output, self.propagated_value_slot_id); + push_u32(output, self.momentum_slot_id); + } + + fn decode_from(bytes: &[u8], row_index: usize) -> RusticolResult { + let mut reader = RowReader::new(Self::NAME, row_index, bytes); + let row = Self { + kernel_id: reader.read_u32()?, + current_id: reader.read_u32()?, + unpropagated_value_slot_id: reader.read_u32()?, + propagated_value_slot_id: reader.read_u32()?, + momentum_slot_id: reader.read_u32()?, + }; + reader.finish()?; + Ok(row) + } +} + +impl FixedWidthRow for EagerClosureRow { + const NAME: &'static str = "closure"; + const WIDTH: usize = 6 * size_of::() + 2 * size_of::(); + + fn validate_for_encoding(&self, row_index: usize) -> RusticolResult<()> { + validate_output_factor_source_for_encoding( + Self::NAME, + row_index, + self.output_factor_source, + )?; + validate_finite_for_encoding(Self::NAME, row_index, "factor_real", self.factor_real)?; + validate_finite_for_encoding(Self::NAME, row_index, "factor_imag", self.factor_imag) + } + + fn encode_into(&self, output: &mut Vec) { + push_u32(output, self.kernel_id); + push_u32(output, self.left_value_slot_id); + push_u32(output, self.right_value_slot_id); + push_u32(output, self.amplitude_index); + push_u32(output, self.coupling_slot_id); + push_u32(output, self.output_factor_source); + push_f64(output, self.factor_real); + push_f64(output, self.factor_imag); + } + + fn decode_from(bytes: &[u8], row_index: usize) -> RusticolResult { + let mut reader = RowReader::new(Self::NAME, row_index, bytes); + let row = Self { + kernel_id: reader.read_u32()?, + left_value_slot_id: reader.read_u32()?, + right_value_slot_id: reader.read_u32()?, + amplitude_index: reader.read_u32()?, + coupling_slot_id: reader.read_u32()?, + output_factor_source: reader.read_u32()?, + factor_real: reader.read_f64()?, + factor_imag: reader.read_f64()?, + }; + reader.finish()?; + validate_output_factor_source_from_payload( + Self::NAME, + row_index, + row.output_factor_source, + )?; + validate_finite_from_payload(Self::NAME, row_index, "factor_real", row.factor_real)?; + validate_finite_from_payload(Self::NAME, row_index, "factor_imag", row.factor_imag)?; + Ok(row) + } +} + +impl FixedWidthRow for EagerSelectorDomainRow { + const NAME: &'static str = "selector-domain"; + const WIDTH: usize = 2 * size_of::(); + + fn validate_for_encoding(&self, _row_index: usize) -> RusticolResult<()> { + Ok(()) + } + + fn encode_into(&self, output: &mut Vec) { + push_u64(output, self.member_start); + push_u64(output, self.member_count); + } + + fn decode_from(bytes: &[u8], row_index: usize) -> RusticolResult { + let mut reader = RowReader::new(Self::NAME, row_index, bytes); + let row = Self { + member_start: reader.read_u64()?, + member_count: reader.read_u64()?, + }; + reader.finish()?; + Ok(row) + } +} + +impl FixedWidthRow for EagerSelectorGroupRow { + const NAME: &'static str = "selector-domain-group"; + const WIDTH: usize = size_of::(); + + fn validate_for_encoding(&self, _row_index: usize) -> RusticolResult<()> { + Ok(()) + } + + fn encode_into(&self, output: &mut Vec) { + push_u32(output, self.coherent_group_id); + } + + fn decode_from(bytes: &[u8], row_index: usize) -> RusticolResult { + let mut reader = RowReader::new(Self::NAME, row_index, bytes); + let row = Self { + coherent_group_id: reader.read_u32()?, + }; + reader.finish()?; + Ok(row) + } +} + +impl FixedWidthRow for EagerSelectorDomainIdRow { + const NAME: &'static str = "selector-domain-id"; + const WIDTH: usize = size_of::(); + + fn validate_for_encoding(&self, _row_index: usize) -> RusticolResult<()> { + Ok(()) + } + + fn encode_into(&self, output: &mut Vec) { + push_u32(output, self.domain_id); + } + + fn decode_from(bytes: &[u8], row_index: usize) -> RusticolResult { + let mut reader = RowReader::new(Self::NAME, row_index, bytes); + let row = Self { + domain_id: reader.read_u32()?, + }; + reader.finish()?; + Ok(row) + } +} + +impl_table_api!(EagerInvocationRow); +impl_table_api!(EagerAttachmentRow); +impl_table_api!(EagerCouplingRow); +impl_table_api!(EagerFinalizationRow); +impl_table_api!(EagerClosureRow); +impl_table_api!(EagerSelectorDomainRow); +impl_table_api!(EagerSelectorGroupRow); +impl_table_api!(EagerSelectorDomainIdRow); + +fn encode_rows(rows: &[Row]) -> RusticolResult> { + let byte_count = rows.len().checked_mul(Row::WIDTH).ok_or_else(|| { + RusticolError::invalid_argument(format!( + "eager {} table row count overflows its encoded length", + Row::NAME + )) + })?; + let mut output = Vec::new(); + output.try_reserve_exact(byte_count).map_err(|error| { + RusticolError::invalid_argument(format!( + "could not reserve {byte_count} bytes for eager {} table: {error}", + Row::NAME + )) + })?; + for (row_index, row) in rows.iter().enumerate() { + row.validate_for_encoding(row_index)?; + row.encode_into(&mut output); + } + if output.len() != byte_count { + return Err(RusticolError::internal(format!( + "eager {} encoder produced {} bytes instead of {byte_count}", + Row::NAME, + output.len() + ))); + } + Ok(output) +} + +fn decode_rows(payload: &[u8]) -> RusticolResult> { + if !payload.len().is_multiple_of(Row::WIDTH) { + return Err(RusticolError::artifact(format!( + "eager {} table has {} bytes, not a multiple of {}", + Row::NAME, + payload.len(), + Row::WIDTH + ))); + } + let row_count = payload.len() / Row::WIDTH; + let expected_len = row_count.checked_mul(Row::WIDTH).ok_or_else(|| { + RusticolError::artifact(format!( + "eager {} table row count overflows its encoded length", + Row::NAME + )) + })?; + if expected_len != payload.len() { + return Err(RusticolError::artifact(format!( + "eager {} table length is inconsistent", + Row::NAME + ))); + } + + let mut rows = Vec::new(); + rows.try_reserve_exact(row_count).map_err(|error| { + RusticolError::artifact(format!( + "could not reserve {row_count} eager {} rows: {error}", + Row::NAME + )) + })?; + for (row_index, bytes) in payload.chunks_exact(Row::WIDTH).enumerate() { + rows.push(Row::decode_from(bytes, row_index)?); + } + Ok(rows) +} + +fn push_u32(output: &mut Vec, value: u32) { + output.extend_from_slice(&value.to_le_bytes()); +} + +fn push_u64(output: &mut Vec, value: u64) { + output.extend_from_slice(&value.to_le_bytes()); +} + +fn push_f64(output: &mut Vec, value: f64) { + output.extend_from_slice(&value.to_le_bytes()); +} + +fn output_factor_source_is_valid(source: u32) -> bool { + matches!( + source, + EAGER_OUTPUT_FACTOR_NONE + | EAGER_OUTPUT_FACTOR_COUPLING_REAL + | EAGER_OUTPUT_FACTOR_COUPLING_IMAG + ) +} + +fn validate_output_factor_source_for_encoding( + table: &str, + row_index: usize, + source: u32, +) -> RusticolResult<()> { + if !output_factor_source_is_valid(source) { + return Err(RusticolError::invalid_argument(format!( + "eager {table} row {row_index} has unsupported output factor source {source}" + ))); + } + Ok(()) +} + +fn validate_output_factor_source_from_payload( + table: &str, + row_index: usize, + source: u32, +) -> RusticolResult<()> { + if !output_factor_source_is_valid(source) { + return Err(RusticolError::artifact(format!( + "eager {table} row {row_index} has unsupported output factor source {source}" + ))); + } + Ok(()) +} + +fn validate_finite_for_encoding( + table: &str, + row_index: usize, + field: &str, + value: f64, +) -> RusticolResult<()> { + if !value.is_finite() { + return Err(RusticolError::invalid_argument(format!( + "eager {table} row {row_index} has non-finite {field}" + ))); + } + Ok(()) +} + +fn validate_finite_from_payload( + table: &str, + row_index: usize, + field: &str, + value: f64, +) -> RusticolResult<()> { + if !value.is_finite() { + return Err(RusticolError::artifact(format!( + "eager {table} row {row_index} has non-finite {field}" + ))); + } + Ok(()) +} + +struct RowReader<'a> { + table: &'static str, + row_index: usize, + bytes: &'a [u8], + offset: usize, +} + +impl<'a> RowReader<'a> { + fn new(table: &'static str, row_index: usize, bytes: &'a [u8]) -> Self { + Self { + table, + row_index, + bytes, + offset: 0, + } + } + + fn read_u32(&mut self) -> RusticolResult { + Ok(u32::from_le_bytes(self.read_array()?)) + } + + fn read_u64(&mut self) -> RusticolResult { + Ok(u64::from_le_bytes(self.read_array()?)) + } + + fn read_f64(&mut self) -> RusticolResult { + Ok(f64::from_le_bytes(self.read_array()?)) + } + + fn read_array(&mut self) -> RusticolResult<[u8; N]> { + let end = self.offset.checked_add(N).ok_or_else(|| { + RusticolError::artifact(format!( + "eager {} row {} byte offset overflow", + self.table, self.row_index + )) + })?; + let bytes = self.bytes.get(self.offset..end).ok_or_else(|| { + RusticolError::artifact(format!( + "eager {} row {} is truncated at byte {}", + self.table, self.row_index, self.offset + )) + })?; + let value = bytes.try_into().map_err(|_| { + RusticolError::internal(format!( + "could not decode eager {} row {}", + self.table, self.row_index + )) + })?; + self.offset = end; + Ok(value) + } + + fn finish(&self) -> RusticolResult<()> { + if self.offset != self.bytes.len() { + return Err(RusticolError::artifact(format!( + "eager {} row {} has {} trailing bytes", + self.table, + self.row_index, + self.bytes.len() - self.offset + ))); + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::RusticolErrorKind; + + #[test] + fn constants_match_python_contract() { + assert_eq!(EAGER_PLAN_ABI, "pyamplicol-eager-plan-v2"); + assert_eq!(EAGER_KERNEL_ABI, "pyamplicol-eager-kernel-v1"); + assert_eq!( + EAGER_RUNTIME_CAPABILITY, + "rusticol.eager-dag.complex-f64.v1" + ); + assert_eq!(MISSING_U32, u32::MAX); + } + + #[test] + fn invocation_matches_python_little_endian_fixture() { + let row = EagerInvocationRow { + kernel_id: 0x0102_0304, + left_value_slot_id: 0x1112_1314, + right_value_slot_id: 0x2122_2324, + left_momentum_slot_id: 0x3132_3334, + right_momentum_slot_id: 0x4142_4344, + coupling_slot_id: 0x5152_5354, + output_factor_source: EAGER_OUTPUT_FACTOR_COUPLING_IMAG, + attachment_start: 0x0102_0304_0506_0708, + attachment_count: 0x1112_1314_1516_1718, + }; + let expected = [ + 0x04, 0x03, 0x02, 0x01, 0x14, 0x13, 0x12, 0x11, 0x24, 0x23, 0x22, 0x21, 0x34, 0x33, + 0x32, 0x31, 0x44, 0x43, 0x42, 0x41, 0x54, 0x53, 0x52, 0x51, 0x02, 0x00, 0x00, 0x00, + 0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01, 0x18, 0x17, 0x16, 0x15, 0x14, 0x13, + 0x12, 0x11, + ]; + + assert_eq!(EagerInvocationRow::ENCODED_LEN, 44); + assert_eq!(EagerInvocationRow::encode_table(&[row]).unwrap(), expected); + assert_eq!(EagerInvocationRow::decode_table(&expected).unwrap(), [row]); + } + + #[test] + fn attachment_matches_python_little_endian_fixture() { + let row = EagerAttachmentRow { + result_current_id: 0x0102_0304, + factor_real: 1.5, + factor_imag: -2.25, + }; + let expected = [ + 0x04, 0x03, 0x02, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf8, 0x3f, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x02, 0xc0, + ]; + + assert_eq!(EagerAttachmentRow::ENCODED_LEN, 20); + assert_eq!(EagerAttachmentRow::encode_table(&[row]).unwrap(), expected); + assert_eq!(EagerAttachmentRow::decode_table(&expected).unwrap(), [row]); + } + + #[test] + fn coupling_matches_python_little_endian_fixture() { + let row = EagerCouplingRow { + real_parameter_id: MISSING_U32, + imag_parameter_id: 0x0102_0304, + constant_real: 3.5, + constant_imag: -0.5, + }; + let expected = [ + 0xff, 0xff, 0xff, 0xff, 0x04, 0x03, 0x02, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x0c, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe0, 0xbf, + ]; + + assert_eq!(EagerCouplingRow::ENCODED_LEN, 24); + assert_eq!(EagerCouplingRow::encode_table(&[row]).unwrap(), expected); + assert_eq!(EagerCouplingRow::decode_table(&expected).unwrap(), [row]); + } + + #[test] + fn finalization_matches_python_little_endian_fixture() { + let row = EagerFinalizationRow { + kernel_id: MISSING_U32, + current_id: 0x0102_0304, + unpropagated_value_slot_id: 0x1112_1314, + propagated_value_slot_id: MISSING_U32, + momentum_slot_id: 0x2122_2324, + }; + let expected = [ + 0xff, 0xff, 0xff, 0xff, 0x04, 0x03, 0x02, 0x01, 0x14, 0x13, 0x12, 0x11, 0xff, 0xff, + 0xff, 0xff, 0x24, 0x23, 0x22, 0x21, + ]; + + assert_eq!(EagerFinalizationRow::ENCODED_LEN, 20); + assert_eq!( + EagerFinalizationRow::encode_table(&[row]).unwrap(), + expected + ); + assert_eq!( + EagerFinalizationRow::decode_table(&expected).unwrap(), + [row] + ); + assert!(!row.applies_kernel()); + assert!(row.stores_unpropagated()); + assert!(!row.stores_propagated()); + } + + #[test] + fn closure_matches_python_little_endian_fixture() { + let row = EagerClosureRow { + kernel_id: 0x0102_0304, + left_value_slot_id: 0x1112_1314, + right_value_slot_id: 0x2122_2324, + amplitude_index: 0x3132_3334, + coupling_slot_id: 0x4142_4344, + output_factor_source: EAGER_OUTPUT_FACTOR_COUPLING_REAL, + factor_real: 1.5, + factor_imag: -2.25, + }; + let expected = [ + 0x04, 0x03, 0x02, 0x01, 0x14, 0x13, 0x12, 0x11, 0x24, 0x23, 0x22, 0x21, 0x34, 0x33, + 0x32, 0x31, 0x44, 0x43, 0x42, 0x41, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0xf8, 0x3f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xc0, + ]; + + assert_eq!(EagerClosureRow::ENCODED_LEN, 40); + assert_eq!(EagerClosureRow::encode_table(&[row]).unwrap(), expected); + assert_eq!(EagerClosureRow::decode_table(&expected).unwrap(), [row]); + } + + #[test] + fn round_trips_multiple_rows_and_boundary_values() { + let invocations = [ + EagerInvocationRow { + kernel_id: 0, + left_value_slot_id: MISSING_U32, + right_value_slot_id: 0, + left_momentum_slot_id: MISSING_U32, + right_momentum_slot_id: 0, + coupling_slot_id: MISSING_U32, + output_factor_source: EAGER_OUTPUT_FACTOR_NONE, + attachment_start: 0, + attachment_count: u64::MAX, + }, + EagerInvocationRow { + kernel_id: MISSING_U32, + left_value_slot_id: 0, + right_value_slot_id: MISSING_U32, + left_momentum_slot_id: 0, + right_momentum_slot_id: MISSING_U32, + coupling_slot_id: 0, + output_factor_source: EAGER_OUTPUT_FACTOR_COUPLING_REAL, + attachment_start: u64::MAX, + attachment_count: 0, + }, + ]; + let attachments = [EagerAttachmentRow { + result_current_id: MISSING_U32, + factor_real: f64::MAX, + factor_imag: f64::MIN, + }]; + let couplings = [EagerCouplingRow { + real_parameter_id: MISSING_U32, + imag_parameter_id: MISSING_U32, + constant_real: -0.0, + constant_imag: f64::MIN_POSITIVE, + }]; + let finalizations = [EagerFinalizationRow { + kernel_id: MISSING_U32, + current_id: MISSING_U32, + unpropagated_value_slot_id: MISSING_U32, + propagated_value_slot_id: MISSING_U32, + momentum_slot_id: MISSING_U32, + }]; + let closures = [EagerClosureRow { + kernel_id: MISSING_U32, + left_value_slot_id: MISSING_U32, + right_value_slot_id: MISSING_U32, + amplitude_index: MISSING_U32, + coupling_slot_id: MISSING_U32, + output_factor_source: EAGER_OUTPUT_FACTOR_NONE, + factor_real: f64::MIN_POSITIVE, + factor_imag: -0.0, + }]; + + assert_eq!( + EagerInvocationRow::decode_table( + &EagerInvocationRow::encode_table(&invocations).unwrap() + ) + .unwrap(), + invocations + ); + assert_eq!( + EagerAttachmentRow::decode_table( + &EagerAttachmentRow::encode_table(&attachments).unwrap() + ) + .unwrap(), + attachments + ); + assert_eq!( + EagerCouplingRow::decode_table(&EagerCouplingRow::encode_table(&couplings).unwrap()) + .unwrap(), + couplings + ); + assert_eq!( + EagerFinalizationRow::decode_table( + &EagerFinalizationRow::encode_table(&finalizations).unwrap() + ) + .unwrap(), + finalizations + ); + assert_eq!( + EagerClosureRow::decode_table(&EagerClosureRow::encode_table(&closures).unwrap()) + .unwrap(), + closures + ); + } + + #[test] + fn empty_tables_round_trip() { + assert!(EagerInvocationRow::encode_table(&[]).unwrap().is_empty()); + assert!(EagerInvocationRow::decode_table(&[]).unwrap().is_empty()); + assert!(EagerCouplingRow::decode_table(&[]).unwrap().is_empty()); + } + + #[test] + fn truncated_payloads_are_artifact_errors() { + let cases = [ + EagerInvocationRow::decode_table(&[0; EagerInvocationRow::ENCODED_LEN - 1]) + .unwrap_err(), + EagerAttachmentRow::decode_table(&[0; EagerAttachmentRow::ENCODED_LEN - 1]) + .unwrap_err(), + EagerCouplingRow::decode_table(&[0; EagerCouplingRow::ENCODED_LEN - 1]).unwrap_err(), + EagerFinalizationRow::decode_table(&[0; EagerFinalizationRow::ENCODED_LEN - 1]) + .unwrap_err(), + EagerClosureRow::decode_table(&[0; EagerClosureRow::ENCODED_LEN - 1]).unwrap_err(), + ]; + + for error in cases { + assert_eq!(error.kind(), RusticolErrorKind::Artifact); + assert!(error.to_string().contains("not a multiple")); + } + } + + #[test] + fn non_finite_outgoing_factors_are_invalid_arguments() { + let attachment = EagerAttachmentRow { + result_current_id: 0, + factor_real: f64::NAN, + factor_imag: 0.0, + }; + let coupling = EagerCouplingRow { + real_parameter_id: MISSING_U32, + imag_parameter_id: MISSING_U32, + constant_real: 0.0, + constant_imag: f64::INFINITY, + }; + let closure = EagerClosureRow { + kernel_id: 0, + left_value_slot_id: 0, + right_value_slot_id: 0, + amplitude_index: 0, + coupling_slot_id: 0, + output_factor_source: EAGER_OUTPUT_FACTOR_NONE, + factor_real: f64::NEG_INFINITY, + factor_imag: 0.0, + }; + + for error in [ + EagerAttachmentRow::encode_table(&[attachment]).unwrap_err(), + EagerCouplingRow::encode_table(&[coupling]).unwrap_err(), + EagerClosureRow::encode_table(&[closure]).unwrap_err(), + ] { + assert_eq!(error.kind(), RusticolErrorKind::InvalidArgument); + assert!(error.to_string().contains("non-finite")); + } + } + + #[test] + fn non_finite_payload_factors_are_artifact_errors() { + let mut attachment = vec![0; EagerAttachmentRow::ENCODED_LEN]; + attachment[4..12].copy_from_slice(&f64::INFINITY.to_le_bytes()); + let mut coupling = vec![0; EagerCouplingRow::ENCODED_LEN]; + coupling[8..16].copy_from_slice(&f64::NAN.to_le_bytes()); + let mut closure = vec![0; EagerClosureRow::ENCODED_LEN]; + closure[24..32].copy_from_slice(&f64::NEG_INFINITY.to_le_bytes()); + + for error in [ + EagerAttachmentRow::decode_table(&attachment).unwrap_err(), + EagerCouplingRow::decode_table(&coupling).unwrap_err(), + EagerClosureRow::decode_table(&closure).unwrap_err(), + ] { + assert_eq!(error.kind(), RusticolErrorKind::Artifact); + assert!(error.to_string().contains("non-finite")); + } + } +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/engine.rs b/pyamplicol/rust/crates/rusticol-core/src/engine.rs new file mode 100644 index 0000000000..eaad9c6d30 --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/engine.rs @@ -0,0 +1,4875 @@ +// SPDX-License-Identifier: 0BSD + +#[cfg(not(feature = "symbolica-runtime"))] +use num_complex::Complex; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use std::collections::{BTreeMap, BTreeSet}; +use std::fs; +use std::path::{Path, PathBuf}; +use std::sync::Arc; +use std::time::{Duration, Instant}; +#[cfg(feature = "symbolica-runtime")] +use symbolica::evaluate::JITCompiledEvaluator; +#[cfg(feature = "symbolica-runtime")] +use symbolica::prelude::{ + BatchEvaluator, Complex, DoubleFloat, EvaluationDomain, ExpressionEvaluator, Float, + JITCompilationSettings, Rational, Real, RealLike, +}; + +#[cfg(feature = "symbolica-runtime")] +use crate::artifact::EvaluatorPayloadSource; +use crate::artifact::EvaluatorPayloadStore; +use crate::{ + ColorComponent as PhysicsColorComponentV1, PROCESS_ARTIFACT_SCHEMA_VERSION, PayloadRole, + ProcessPhysics as ProcessPhysicsV1, RusticolError, RusticolResult, VerifiedArtifact, +}; + +// Keep replay state within the useful cache working set. Larger batches make +// stage-local gather/scatter substantially slower for high-flow LC workloads. +const MAX_LC_TOPOLOGY_REPLAY_EXPANDED_POINTS: usize = 2048; +const LC_SECTOR_SELECTOR_PARAMETER: &str = "runtime.lc_sector_id"; +const HELICITY_RECURRENCE_CONTRACT_VERSION: u32 = 1; +const HELICITY_RECURRENCE_KIND: &str = "pyamplicol-helicity-recurrence"; +const HELICITY_RECURRENCE_PROOF_ALGORITHM: &str = "canonical-source-transition-dependency-shape-v1"; +const HELICITY_MATERIALIZATION_CONTRACT_VERSION: u32 = 1; +const HELICITY_MATERIALIZATION_KIND: &str = "pyamplicol-helicity-recurrence-materialization"; + +type LcTopologyReplayMappings = Vec>; + +#[derive(Clone, Debug, Default)] +struct LcTopologyReplayData { + mappings: LcTopologyReplayMappings, + routes: Vec>, + materialized_sector_ids: BTreeSet, +} + +#[derive(Clone, Debug)] +struct LcTopologyReplaySectorRoute { + physical_sector_id: i64, + materialized_sector_id: i64, + weight: f64, + sign: i8, + amplitude_factor: [f64; 2], + residual: bool, +} + +impl LcTopologyReplaySectorRoute { + fn squared_reduction_weight(&self) -> f64 { + // The replay sign is an amplitude-level relation. LC resolved output + // is diagonal in the physical flow, so sign^2 = 1. Multiplying the + // signed factor by the sign retains that convention explicitly. + self.amplitude_factor[0] * f64::from(self.sign) + } +} + +#[derive(Clone, Copy, Debug)] +struct LcMaterializedSector { + color_index: usize, + reduction_weight: f64, +} + +#[derive(Clone, Debug)] +struct LcResolvedReplayRoute { + source_index: usize, + target_index: usize, + weight: f64, +} + +#[derive(Clone, Debug)] +struct LcResolvedReplayEntry { + routes: Vec, +} + +#[derive(Clone, Debug)] +struct LcResolvedReplayPlan { + #[cfg(test)] + entries: Vec, + routes_by_target: Vec>, + color_count: usize, +} + +#[derive(Clone, Debug)] +struct LcResolvedReplayTargetRoute { + mapping_index: usize, + source_index: usize, + weight: f64, +} + +#[derive(Clone, Debug)] +struct LcResolvedReplaySelection { + #[cfg(test)] + mapping_indices: Vec, + #[cfg(test)] + entries: Vec, + #[cfg(test)] + source_helicity_indices: Vec>, + #[cfg(test)] + source_color_indices: Vec>, + source_groups: Vec, + helicity_indices: Vec, + color_indices: Vec, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +struct LcResolvedReplaySelectionKey { + helicity_indices: Option>, + color_indices: Option>, +} + +#[derive(Clone, Debug)] +struct LcResolvedReplaySourceGroup { + mapping_indices: Vec, + entries: Vec, + helicity_ids: BTreeSet, + color_ids: BTreeSet, + materialized_sector_ids: Option>, + direct_total_plan: Option, + source_component_count: usize, +} + +#[derive(Clone, Copy, Debug)] +struct LcDirectTotalPlan { + mapping_index: usize, + materialized_sector_id: i64, + scale: f64, +} + +fn uniform_lc_replay_total_scale( + replay_entry: &LcResolvedReplayEntry, + source_component_count: usize, + target_component_count: usize, +) -> Option { + if source_component_count == 0 { + return None; + } + let mut source_scales = vec![0.0; source_component_count]; + for route in &replay_entry.routes { + if route.source_index >= source_component_count + || route.target_index >= target_component_count + || !route.weight.is_finite() + { + return None; + } + source_scales[route.source_index] += route.weight; + } + let scale = source_scales[0]; + if !scale.is_finite() || source_scales[1..].iter().any(|value| *value != scale) { + return None; + } + Some(scale) +} + +pub const SYMJIT_APPLICATION_RUNTIME_CAPABILITY: &str = "symjit.application.complex-f64.v1"; +pub const SYMBOLICA_LEGACY_JIT_RUNTIME_CAPABILITY: &str = + "symbolica.legacy-jit-container.complex-f64.v1"; +pub const SYMBOLICA_COMPILED_CPP_RUNTIME_CAPABILITY: &str = "symbolica.compiled-cpp.complex-f64.v1"; +pub const SYMBOLICA_COMPILED_ASM_RUNTIME_CAPABILITY: &str = "symbolica.compiled-asm.complex-f64.v1"; +pub const EAGER_DAG_RUNTIME_CAPABILITY: &str = crate::EAGER_RUNTIME_CAPABILITY; +pub const EAGER_RUNTIME_LAYOUT_CAPABILITY: &str = crate::eager_layout::EAGER_RUNTIME_CAPABILITY; +pub const EAGER_DIRECT_ARENA_RUNTIME_CAPABILITY: &str = + crate::eager_layout::EAGER_DIRECT_ARENA_RUNTIME_CAPABILITY; +pub const EAGER_LC_TOPOLOGY_REPLAY_RUNTIME_CAPABILITY: &str = + crate::EAGER_LC_TOPOLOGY_REPLAY_RUNTIME_CAPABILITY; +pub const RECURRENCE_RUNTIME_CAPABILITY: &str = crate::recurrence::RECURRENCE_RUNTIME_CAPABILITY; +pub const RECURRENCE_LC_COLOR_RUNTIME_CAPABILITY: &str = + crate::recurrence::RECURRENCE_LC_COLOR_CAPABILITY; +pub const RECURRENCE_CONTRACTED_COLOR_RUNTIME_CAPABILITY: &str = + crate::recurrence::RECURRENCE_CONTRACTED_COLOR_CAPABILITY; +pub const RECURRENCE_HELICITY_SELECTOR_COMPANION_RUNTIME_CAPABILITY: &str = + "rusticol.recurrence-helicity-selector-companion.v2"; +pub const ON_THE_FLY_RUNTIME_CAPABILITY: &str = "rusticol.on-the-fly.complex-f64.v1"; +pub const ON_THE_FLY_CONTRACTED_COLOR_RUNTIME_CAPABILITY: &str = + "rusticol.on-the-fly.contracted-color.v1"; +pub const ON_THE_FLY_LC_COLOR_RUNTIME_CAPABILITY: &str = "rusticol.on-the-fly.lc-color.v1"; +pub const SYMMETRIC_GROUP_FFT_COLOR_RUNTIME_CAPABILITY: &str = + "rusticol.color-contraction.symmetric-group-fft.v1"; +pub const COMPILED_RUNTIME_SELECTORS_CAPABILITY: &str = "rusticol.compiled.runtime-selectors.v1"; +pub const COMPILED_PLANE_ARENA_RUNTIME_CAPABILITY: &str = "compiled-plane-arena-v1"; +pub const COMPILED_PLANE_DIRECT_APPLICATION_ABI: &str = "pyamplicol-compiled-plane-kernel-v2"; +pub const COMPILED_PLANE_SOURCE_APPLICATION_ABI: &str = "pyamplicol-symjit-plane-application-v2"; +pub const COMPILED_HELICITY_DUAL_LANE_CAPABILITY: &str = "rusticol.compiled.helicity-dual-lane.v1"; +pub const COMPILED_HELICITY_SELECTOR_UNION_CAPABILITY: &str = + "rusticol.compiled.helicity-selector-union.v1"; +pub const COMPILED_HELICITY_PRIMARY_RECURRENCE_CAPABILITY: &str = + "rusticol.compiled.helicity-primary-recurrence.v1"; +pub const COMPILED_COLOR_CONTRACTION_WALSH_CAPABILITY: &str = + "rusticol.compiled.color-contraction-walsh.v1"; +pub const COMPILED_COLOR_CONTRACTION_WALSH_C2K_CAPABILITY: &str = + "rusticol.compiled.color-contraction-walsh-c2k.v1"; +pub const COMPILED_COLOR_TOPOLOGY_LANES_CAPABILITY: &str = + "rusticol.compiled.color-topology-lanes.v1"; +pub const SYMJIT_APPLICATION_STORAGE_ABI: &str = "symjit-application-storage-v3"; +pub const SYMJIT_PLANE_APPLICATION_ABI: &str = "pyamplicol-symjit-plane-application-v2"; + +#[doc(hidden)] +pub fn preflight_prepared_kernel_pack( + manifest_path: &Path, + payload_root: &Path, +) -> RusticolResult { + let bytes = fs::read(manifest_path).map_err(|error| { + RusticolError::artifact(format!( + "could not read prepared kernel pack {}: {error}", + manifest_path.display() + )) + })?; + let pack: PreparedKernelPackManifest = serde_json::from_slice(&bytes).map_err(|error| { + RusticolError::serialization(format!( + "could not parse prepared kernel pack {}: {error}", + manifest_path.display() + )) + })?; + pack.validate()?; + PreparedEvaluatorBackend::preflight_all(&pack, payload_root) +} + +#[cfg(feature = "f64-symjit")] +pub fn eager_direct_descriptor_for_source_application_bytes( + source_bytes: &[u8], + input_complex_count: u32, + output_complex_count: u32, + display_path: &Path, +) -> RusticolResult> { + evaluator::symjit_eager_direct::eager_direct_descriptor_for_source_application_bytes( + source_bytes, + input_complex_count, + output_complex_count, + display_path, + ) +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)] +#[serde(rename_all = "kebab-case")] +pub enum RuntimeCapability { + CompiledColorContractionWalshC2kV1, + CompiledColorContractionWalshV1, + CompiledColorTopologyLanesV1, + CompiledHelicityDualLaneV1, + CompiledHelicityPrimaryRecurrenceV1, + CompiledHelicitySelectorUnionV1, + CompiledPlaneArenaV1, + CompiledRuntimeSelectorsV1, + EagerDagComplexF64V1, + EagerDirectArenaV1, + EagerRuntimeLayoutComplexF64V1, + EagerLcTopologyReplayComplexF64V1, + RecurrenceRuntimeComplexF64V1, + RecurrenceLcColorV1, + RecurrenceContractedColorV1, + RecurrenceHelicitySelectorCompanionV2, + SymmetricGroupFftColorContractionV1, + OnTheFlyRuntimeComplexF64V1, + OnTheFlyContractedColorV1, + OnTheFlyLcColorV1, + SymjitApplicationComplexF64V1, + SymbolicaLegacyJitContainerComplexF64V1, + SymbolicaCompiledCppComplexF64V1, + SymbolicaCompiledAsmComplexF64V1, +} + +impl RuntimeCapability { + pub const fn as_str(self) -> &'static str { + match self { + Self::CompiledColorContractionWalshC2kV1 => { + COMPILED_COLOR_CONTRACTION_WALSH_C2K_CAPABILITY + } + Self::CompiledColorContractionWalshV1 => COMPILED_COLOR_CONTRACTION_WALSH_CAPABILITY, + Self::CompiledColorTopologyLanesV1 => COMPILED_COLOR_TOPOLOGY_LANES_CAPABILITY, + Self::CompiledHelicityDualLaneV1 => COMPILED_HELICITY_DUAL_LANE_CAPABILITY, + Self::CompiledHelicityPrimaryRecurrenceV1 => { + COMPILED_HELICITY_PRIMARY_RECURRENCE_CAPABILITY + } + Self::CompiledHelicitySelectorUnionV1 => COMPILED_HELICITY_SELECTOR_UNION_CAPABILITY, + Self::CompiledPlaneArenaV1 => COMPILED_PLANE_ARENA_RUNTIME_CAPABILITY, + Self::CompiledRuntimeSelectorsV1 => COMPILED_RUNTIME_SELECTORS_CAPABILITY, + Self::EagerDagComplexF64V1 => EAGER_DAG_RUNTIME_CAPABILITY, + Self::EagerDirectArenaV1 => EAGER_DIRECT_ARENA_RUNTIME_CAPABILITY, + Self::EagerRuntimeLayoutComplexF64V1 => EAGER_RUNTIME_LAYOUT_CAPABILITY, + Self::EagerLcTopologyReplayComplexF64V1 => EAGER_LC_TOPOLOGY_REPLAY_RUNTIME_CAPABILITY, + Self::RecurrenceRuntimeComplexF64V1 => RECURRENCE_RUNTIME_CAPABILITY, + Self::RecurrenceLcColorV1 => RECURRENCE_LC_COLOR_RUNTIME_CAPABILITY, + Self::RecurrenceContractedColorV1 => RECURRENCE_CONTRACTED_COLOR_RUNTIME_CAPABILITY, + Self::RecurrenceHelicitySelectorCompanionV2 => { + RECURRENCE_HELICITY_SELECTOR_COMPANION_RUNTIME_CAPABILITY + } + Self::SymmetricGroupFftColorContractionV1 => { + SYMMETRIC_GROUP_FFT_COLOR_RUNTIME_CAPABILITY + } + Self::OnTheFlyRuntimeComplexF64V1 => ON_THE_FLY_RUNTIME_CAPABILITY, + Self::OnTheFlyContractedColorV1 => ON_THE_FLY_CONTRACTED_COLOR_RUNTIME_CAPABILITY, + Self::OnTheFlyLcColorV1 => ON_THE_FLY_LC_COLOR_RUNTIME_CAPABILITY, + Self::SymjitApplicationComplexF64V1 => SYMJIT_APPLICATION_RUNTIME_CAPABILITY, + Self::SymbolicaLegacyJitContainerComplexF64V1 => { + SYMBOLICA_LEGACY_JIT_RUNTIME_CAPABILITY + } + Self::SymbolicaCompiledCppComplexF64V1 => SYMBOLICA_COMPILED_CPP_RUNTIME_CAPABILITY, + Self::SymbolicaCompiledAsmComplexF64V1 => SYMBOLICA_COMPILED_ASM_RUNTIME_CAPABILITY, + } + } +} + +pub fn supported_runtime_capabilities() -> Vec<&'static str> { + let mut capabilities = vec![ + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + COMPILED_COLOR_CONTRACTION_WALSH_C2K_CAPABILITY, + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + COMPILED_COLOR_CONTRACTION_WALSH_CAPABILITY, + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + COMPILED_COLOR_TOPOLOGY_LANES_CAPABILITY, + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + COMPILED_HELICITY_DUAL_LANE_CAPABILITY, + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + COMPILED_HELICITY_PRIMARY_RECURRENCE_CAPABILITY, + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + COMPILED_HELICITY_SELECTOR_UNION_CAPABILITY, + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + COMPILED_PLANE_ARENA_RUNTIME_CAPABILITY, + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + COMPILED_RUNTIME_SELECTORS_CAPABILITY, + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + EAGER_DIRECT_ARENA_RUNTIME_CAPABILITY, + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + EAGER_RUNTIME_LAYOUT_CAPABILITY, + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + RECURRENCE_RUNTIME_CAPABILITY, + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + RECURRENCE_LC_COLOR_RUNTIME_CAPABILITY, + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + RECURRENCE_CONTRACTED_COLOR_RUNTIME_CAPABILITY, + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + RECURRENCE_HELICITY_SELECTOR_COMPANION_RUNTIME_CAPABILITY, + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + SYMMETRIC_GROUP_FFT_COLOR_RUNTIME_CAPABILITY, + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + ON_THE_FLY_RUNTIME_CAPABILITY, + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + ON_THE_FLY_CONTRACTED_COLOR_RUNTIME_CAPABILITY, + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + ON_THE_FLY_LC_COLOR_RUNTIME_CAPABILITY, + #[cfg(feature = "f64-symjit")] + SYMJIT_APPLICATION_RUNTIME_CAPABILITY, + #[cfg(feature = "symbolica-runtime")] + SYMBOLICA_LEGACY_JIT_RUNTIME_CAPABILITY, + #[cfg(feature = "f64-compiled")] + SYMBOLICA_COMPILED_CPP_RUNTIME_CAPABILITY, + #[cfg(feature = "f64-compiled")] + SYMBOLICA_COMPILED_ASM_RUNTIME_CAPABILITY, + ]; + capabilities.sort_unstable(); + capabilities +} + +pub(crate) fn ensure_runtime_capabilities_supported<'a>( + capabilities: impl IntoIterator, +) -> RusticolResult<()> { + for capability in capabilities { + if !runtime_capability_is_supported(capability) { + let supported = supported_runtime_capabilities(); + return Err(RusticolError::unsupported_runtime_capability( + capability, + format!("this Rusticol build supports {supported:?}"), + )); + } + } + Ok(()) +} + +/// Test one capability on the successful runtime-load path without allocating +/// and sorting a complete capability set. The public reporting helper above +/// still returns the complete sorted list when diagnostics need it. +fn runtime_capability_is_supported(capability: &str) -> bool { + match capability { + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + COMPILED_COLOR_CONTRACTION_WALSH_C2K_CAPABILITY + | COMPILED_COLOR_CONTRACTION_WALSH_CAPABILITY + | COMPILED_COLOR_TOPOLOGY_LANES_CAPABILITY + | COMPILED_HELICITY_DUAL_LANE_CAPABILITY + | COMPILED_HELICITY_PRIMARY_RECURRENCE_CAPABILITY + | COMPILED_HELICITY_SELECTOR_UNION_CAPABILITY + | COMPILED_PLANE_ARENA_RUNTIME_CAPABILITY + | COMPILED_RUNTIME_SELECTORS_CAPABILITY + | EAGER_DIRECT_ARENA_RUNTIME_CAPABILITY + | EAGER_RUNTIME_LAYOUT_CAPABILITY + | RECURRENCE_RUNTIME_CAPABILITY + | RECURRENCE_LC_COLOR_RUNTIME_CAPABILITY + | RECURRENCE_CONTRACTED_COLOR_RUNTIME_CAPABILITY + | RECURRENCE_HELICITY_SELECTOR_COMPANION_RUNTIME_CAPABILITY + | SYMMETRIC_GROUP_FFT_COLOR_RUNTIME_CAPABILITY + | ON_THE_FLY_RUNTIME_CAPABILITY + | ON_THE_FLY_CONTRACTED_COLOR_RUNTIME_CAPABILITY + | ON_THE_FLY_LC_COLOR_RUNTIME_CAPABILITY => true, + #[cfg(feature = "f64-symjit")] + SYMJIT_APPLICATION_RUNTIME_CAPABILITY => true, + #[cfg(feature = "symbolica-runtime")] + SYMBOLICA_LEGACY_JIT_RUNTIME_CAPABILITY => true, + #[cfg(feature = "f64-compiled")] + SYMBOLICA_COMPILED_CPP_RUNTIME_CAPABILITY | SYMBOLICA_COMPILED_ASM_RUNTIME_CAPABILITY => { + true + } + _ => false, + } +} + +#[derive(Clone, Debug, Deserialize)] +struct ExecutionManifestHeader { + #[serde(default)] + schema_version: u32, + #[serde(default)] + kind: String, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct ExecutionSetManifest { + schema_version: u32, + kind: String, + #[serde(default)] + required_runtime_capabilities: Vec, + processes: Vec, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct ExecutionSetEntry { + process_id: String, + manifest_path: String, + #[serde(default)] + required_runtime_capabilities: Vec, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct InputCrossingMapEntry { + target_index: usize, + source_index: usize, + sign: f64, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct ExecutionManifest { + schema_version: u32, + kind: String, + #[serde(default)] + required_runtime_capabilities: Vec, + process: String, + key: String, + color_accuracy: String, + external_pdg_order: Vec, + compiled: EvaluatorSetManifest, + dag_summary: ExecutionSummary, + materialization_census: ExecutionMaterializationCensus, + runtime_schema: ExecutionPlan, + #[serde(default)] + color_contraction_payload: Option, + #[serde(default)] + physics_reduction: Option, + #[serde(default)] + helicity_sum_execution: Option>, + #[serde(default)] + helicity_selector_executions: Vec, + #[serde(default)] + color_selector_executions: Vec, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct CompiledColorContractionPayloadManifest { + path: String, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct HelicitySelectorExecutionManifest { + selector_domain_ids: Vec, + #[serde(default)] + schedule_mode: HelicitySelectorScheduleMode, + execution: Box, +} + +#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq)] +#[serde(rename_all = "kebab-case")] +enum HelicitySelectorScheduleMode { + #[default] + ParentClosure, + NestedRuntime, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct ColorSelectorExecutionManifest { + materialized_sector_id: i64, + execution: Box, +} + +#[derive(Clone, Debug)] +struct EvaluatorSetManifest { + kind: String, + runtime_available: bool, + runtime_unavailable_message: Option, + lc_topology_replay: Option, + color_topology_replay: Option, + model_parameter_evaluator: Option, + stage_evaluators: Option, + momentum_slot_ids: Option>, +} + +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct EvaluatorSetManifestWire { + kind: String, + runtime_available: bool, + runtime_unavailable_message: Option, + #[serde(default)] + lc_topology_replay: Option, + #[serde(default)] + color_topology_replay: Option, + // Current Python artifacts mirror this additive contract under `compiled`. + // Runtime loading uses the authoritative runtime_schema copy below. + #[serde(default)] + helicity_recurrence: Option, + #[serde(default)] + model_parameter_evaluator: Option, + stage_evaluators: Option, + #[serde(default)] + momentum_slot_ids: Option>, +} + +impl<'de> Deserialize<'de> for EvaluatorSetManifest { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let wire = EvaluatorSetManifestWire::deserialize(deserializer)?; + let _validated_additive_mirror = wire.helicity_recurrence; + Ok(Self { + kind: wire.kind, + runtime_available: wire.runtime_available, + runtime_unavailable_message: wire.runtime_unavailable_message, + lc_topology_replay: wire.lc_topology_replay, + color_topology_replay: wire.color_topology_replay, + model_parameter_evaluator: wire.model_parameter_evaluator, + stage_evaluators: wire.stage_evaluators, + momentum_slot_ids: wire.momentum_slot_ids, + }) + } +} + +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +#[serde(deny_unknown_fields)] +struct ColorTopologyReplayManifest { + #[serde(default)] + enabled: bool, + #[serde(default)] + mode: String, + #[serde(default)] + contract_version: Option, + #[serde(default)] + color_accuracy: String, + #[serde(default)] + physical_sector_count: Option, + #[serde(default)] + replayed_sector_count: usize, + #[serde(default)] + materialized_sector_ids: Vec, + #[serde(default)] + residual_sector_ids: Vec, + #[serde(default)] + groups: Vec, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct GenericModelParameterEvaluatorManifest { + kind: String, + #[serde(default)] + required_runtime_capabilities: Vec, + input_parameter_indices: Vec, + outputs: Vec, + evaluator: EvaluatorManifest, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct GenericDerivedParameterOutputManifest { + runtime_name: String, + output_index: usize, + real_parameter_index: usize, + imag_parameter_index: Option, +} + +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +#[serde(deny_unknown_fields)] +struct LcTopologyReplayManifest { + #[serde(default)] + enabled: bool, + #[serde(default)] + mode: String, + #[serde(default)] + contract_version: Option, + #[serde(default)] + physical_sector_count: Option, + #[serde(default)] + replayed_sector_count: usize, + #[serde(default)] + materialized_sector_ids: Vec, + #[serde(default)] + residual_sector_ids: Vec, + #[serde(default)] + groups: Vec, +} + +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +#[serde(deny_unknown_fields)] +struct LcTopologyReplayGroupManifest { + representative_sector_id: i64, + materialized_sector_id: i64, + #[serde(default)] + active_sector_ids: Vec, + #[serde(default)] + proof: Option, + #[serde(default)] + sector_permutations: Vec, +} + +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +#[serde(deny_unknown_fields)] +struct LcTopologyReplayProofManifest { + #[serde(default)] + status: String, + #[serde(default)] + algorithm: Option, + #[serde(default)] + digest: Option, +} + +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +#[serde(deny_unknown_fields)] +struct LcTopologyReplaySectorPermutationManifest { + sector_id: i64, + #[serde(default = "default_lc_topology_replay_weight")] + weight: f64, + #[serde(default = "default_lc_topology_replay_sign")] + sign: i8, + #[serde(default)] + factor: Option>, + #[serde(default)] + label_permutation: Vec, +} + +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +#[serde(deny_unknown_fields)] +struct LcTopologyReplayLabelPermutationManifest { + representative_label: usize, + sector_label: usize, +} + +fn default_lc_topology_replay_weight() -> f64 { + 1.0 +} + +fn default_lc_topology_replay_sign() -> i8 { + 1 +} + +#[derive(Clone, Debug, Deserialize, PartialEq)] +#[serde(deny_unknown_fields)] +struct HelicityRecurrenceManifest { + kind: String, + contract_version: u32, + proof_algorithm: String, + current_count: usize, + amplitude_root_count: usize, + proof_counts: HelicityRecurrenceProofCountsManifest, + selector_domains: Vec, + source_state_mappings: Vec, + recurrence_classes: Vec, + amplitude_classes: Vec, + residual_current_ids: Vec, + residual_root_ids: Vec, + structural_zero_selector_domain_ids: Vec, + diagnostics: Vec, + #[serde(default)] + materialization: Option, +} + +#[derive(Clone, Debug, Deserialize, PartialEq)] +#[serde(deny_unknown_fields)] +struct HelicityRecurrenceProofCountsManifest { + recurrence_class_count: usize, + optimized_recurrence_class_count: usize, + optimized_current_count: usize, + residual_current_count: usize, + amplitude_class_count: usize, + optimized_amplitude_class_count: usize, + residual_amplitude_count: usize, + source_state_mapping_count: usize, + physical_helicity_count: usize, + structural_zero_helicity_count: usize, +} + +#[derive(Clone, Debug, Deserialize, PartialEq)] +#[serde(deny_unknown_fields)] +struct HelicitySelectorDomainManifest { + id: usize, + complete: bool, + source_states: Vec, +} + +#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd)] +#[serde(deny_unknown_fields)] +struct HelicitySelectorSourceStateManifest { + external_label: usize, + helicity: i32, +} + +#[derive(Clone, Debug, Deserialize, PartialEq)] +#[serde(deny_unknown_fields)] +struct HelicityCurrentReplayMemberManifest { + current_id: usize, + selector_domain_id: usize, + factor: [f64; 2], +} + +#[derive(Clone, Debug, Deserialize, PartialEq)] +#[serde(deny_unknown_fields)] +struct HelicityRecurrenceClassManifest { + class_id: String, + representative_current_id: usize, + external_labels: Vec, + source_class: bool, + members: Vec, + proof: HelicityRecurrenceProofManifest, +} + +#[derive(Clone, Debug, Deserialize, PartialEq)] +#[serde(deny_unknown_fields)] +struct HelicityRecurrenceProofManifest { + status: String, + algorithm: String, + digest: String, + transition_contract_ids: Vec, +} + +#[derive(Clone, Debug, Deserialize, PartialEq)] +#[serde(deny_unknown_fields)] +struct HelicitySourceStateMappingManifest { + current_id: usize, + external_label: usize, + helicity: i32, + chirality: i32, + spin_state: GenericSourceSpinStateManifest, + declared_state_index: usize, + selector_domain_id: usize, + recurrence_class_id: String, + representative_current_id: usize, + source_contract_digest: String, + factor: [f64; 2], +} + +#[derive(Clone, Debug, Deserialize, PartialEq)] +#[serde(deny_unknown_fields)] +struct HelicityAmplitudeReplayMemberManifest { + root_id: usize, + selector_domain_ids: Vec, + factor: [f64; 2], +} + +#[derive(Clone, Debug, Deserialize, PartialEq)] +#[serde(deny_unknown_fields)] +struct HelicityAmplitudeReplayClassManifest { + class_id: String, + representative_root_id: usize, + members: Vec, + proof: HelicityRecurrenceProofManifest, +} + +#[derive(Clone, Debug, Deserialize, PartialEq)] +#[serde(deny_unknown_fields)] +struct HelicityRecurrenceMaterializationManifest { + kind: String, + contract_version: u32, + #[serde(default)] + strategy: HelicityMaterializationStrategy, + proof_current_count: usize, + proof_root_count: usize, + materialized_current_count: usize, + materialized_root_count: usize, + proof_to_materialized_current: Vec, + source_routes: Vec, + amplitude_routes: Vec, + selector_schedules: Vec, +} + +#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "kebab-case")] +enum HelicityMaterializationStrategy { + #[default] + Quotient, + RetainedProofGraph, +} + +#[derive(Clone, Debug, Deserialize, PartialEq)] +#[serde(deny_unknown_fields)] +struct HelicityMaterializedSourceRouteManifest { + materialized_current_id: usize, + external_label: usize, + helicity: i32, + chirality: i32, + spin_state: GenericSourceSpinStateManifest, + declared_state_index: usize, + selector_domain_id: usize, + factor: [f64; 2], +} + +#[derive(Clone, Debug, Deserialize, PartialEq)] +#[serde(deny_unknown_fields)] +struct HelicityMaterializedAmplitudeRouteManifest { + materialized_root_id: usize, + selector_domain_ids: Vec, + factor: [f64; 2], + residual: bool, +} + +#[derive(Clone, Debug, Deserialize, PartialEq)] +#[serde(deny_unknown_fields)] +struct HelicityMaterializedSelectorScheduleManifest { + selector_domain_id: usize, + active_current_ids: Vec, + active_root_ids: Vec, + structural_zero: bool, +} + +#[allow(dead_code)] +#[derive(Clone, Debug)] +struct HelicityRecurrenceRuntime { + selector_domains: Vec, + source_state_mappings: Vec, + recurrence_classes: Vec, + amplitude_classes: Vec, + residual_current_ids: Vec, + residual_root_ids: Vec, + structural_zero_selector_domain_ids: Vec, + materialization: Option, +} + +#[allow(dead_code)] +#[derive(Clone, Debug)] +struct HelicitySelectorDomainRuntime { + complete: bool, + source_states: Vec<(usize, i32)>, +} + +#[allow(dead_code)] +#[derive(Clone, Debug)] +struct HelicityCurrentReplayMemberRuntime { + current_id: usize, + selector_domain_id: usize, + factor: [f64; 2], +} + +#[allow(dead_code)] +#[derive(Clone, Debug)] +struct HelicityRecurrenceClassRuntime { + representative_current_id: usize, + external_labels: Vec, + source_class: bool, + members: Vec, +} + +#[allow(dead_code)] +#[derive(Clone, Debug)] +struct HelicitySourceStateMappingRuntime { + current_id: usize, + external_index: usize, + helicity: i32, + chirality: i32, + spin_state: GenericSourceSpinStateManifest, + declared_state_index: usize, + selector_domain_id: usize, + recurrence_class_index: usize, + representative_current_id: usize, + factor: [f64; 2], +} + +#[allow(dead_code)] +#[derive(Clone, Debug)] +struct HelicityAmplitudeReplayMemberRuntime { + root_id: usize, + selector_domain_ids: Vec, + factor: [f64; 2], +} + +#[allow(dead_code)] +#[derive(Clone, Debug)] +struct HelicityAmplitudeReplayClassRuntime { + representative_root_id: usize, + members: Vec, +} + +#[allow(dead_code)] +#[derive(Clone, Debug)] +struct HelicityRecurrenceMaterializationRuntime { + strategy: HelicityMaterializationStrategy, + proof_to_materialized_current: Vec, + source_routes: Vec, + amplitude_routes: Vec, + selector_schedules: Vec, +} + +#[allow(dead_code)] +#[derive(Clone, Debug)] +struct HelicityMaterializedSourceRouteRuntime { + materialized_current_id: usize, + external_index: usize, + helicity: i32, + chirality: i32, + spin_state: GenericSourceSpinStateManifest, + declared_state_index: usize, + selector_domain_id: usize, + factor: [f64; 2], +} + +#[allow(dead_code)] +#[derive(Clone, Debug)] +struct HelicityMaterializedAmplitudeRouteRuntime { + materialized_root_id: usize, + selector_domain_ids: Vec, + factor: [f64; 2], + residual: bool, +} + +#[allow(dead_code)] +#[derive(Clone, Debug)] +struct HelicityMaterializedSelectorScheduleRuntime { + selector_domain_id: usize, + active_current_ids: Vec, + active_root_ids: Vec, + active_stage_chunk_indices: Vec>, + active_amplitude_chunk_indices: Vec, + structural_zero: bool, +} + +#[derive(Clone, Debug)] +struct CompiledColorSelectorSchedule { + active_stage_chunk_indices: Vec>, + active_amplitude_chunk_indices: Vec, +} + +#[derive(Clone, Debug)] +struct CompiledColorExecutionPlan { + schedules_by_materialized_sector: BTreeMap>, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct GenericStageEvaluatorArtifactsManifest { + kind: String, + #[serde(default)] + required_runtime_capabilities: Vec, + runtime_available: bool, + runtime_unavailable_message: Option, + parameter_count: usize, + value_parameter_count: usize, + momentum_parameter_count: usize, + #[serde(default)] + model_parameter_count: usize, + real_valued_inputs: Vec, + parameter_layout: String, + stage_count: usize, + stages: Vec, + amplitude_stage: GenericSerializedStageEvaluatorManifest, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct GenericSerializedStageEvaluatorManifest { + stage_index: usize, + stage_kind: String, + subset_size: Option, + evaluator_label: String, + parameter_layout: String, + output_length: usize, + output_slots: Vec, + input_value_slot_ids: Vec, + output_value_slot_ids: Vec, + interaction_ids: Vec, + #[serde(default)] + input_components: Vec, + #[serde(default)] + parameter_count: usize, + #[serde(default)] + value_parameter_count: usize, + #[serde(default)] + momentum_parameter_count: usize, + #[serde(default)] + model_parameter_count: usize, + #[serde(default)] + real_valued_inputs: Vec, + expression_ready: bool, + blockers: Vec, + evaluator: EvaluatorManifest, + #[serde(default)] + compiled_plane_arena: Option, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct CompiledPlaneArenaStageManifest { + schema_version: u32, + kind: String, + application_abi: String, + source_application_abi: String, + element_layout: String, + output_operation: String, + output_factor: String, + input_output_aliasing: String, + output_output_aliasing: String, + input_bindings: Vec, + output_bindings: Vec, + leaves: Vec, +} + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq)] +#[serde(deny_unknown_fields)] +struct CompiledPlaneInputBindingManifest { + parameter_index: usize, + kind: String, + source_id: usize, + component: usize, + global_component: usize, + #[serde(default)] + real_valued: bool, +} + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq)] +#[serde(deny_unknown_fields)] +struct CompiledPlaneOutputBindingManifest { + output_index: usize, + arena: String, + component: usize, +} + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq)] +#[serde(deny_unknown_fields)] +struct CompiledPlaneLeafManifest { + application_path: String, + source_application_abi: String, + optimization_level: u8, + direct_codegen_optimization_level: u8, + input_len: usize, + output_len: usize, + input_indices: Vec, + output_start: usize, + output_stop: usize, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct GenericStageInputComponentManifest { + kind: String, + source_id: usize, + component: usize, + global_component: usize, + parameter_index: usize, + #[serde(default)] + real_valued: bool, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct GenericStageOutputSlotManifest { + value_slot_id: isize, + current_id: isize, + variant: String, + component_start: usize, + component_stop: usize, + output_start: usize, + output_stop: usize, + #[serde(default)] + color_selector_domain_ids: Vec, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct ExecutionSummary { + current_count: usize, + source_count: usize, + interaction_count: usize, + interaction_evaluation_count: usize, + amplitude_root_count: usize, + truncated: bool, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct ExecutionMaterializationCensus { + abi: String, + basis: String, + r#final: BTreeMap, + peak: BTreeMap, + final_equals_peak: bool, +} + +impl ExecutionMaterializationCensus { + fn from_summary(summary: &ExecutionSummary) -> Self { + let counts = BTreeMap::from([ + ( + "amplitude_root_count".to_string(), + summary.amplitude_root_count, + ), + ("current_count".to_string(), summary.current_count), + ("interaction_count".to_string(), summary.interaction_count), + ( + "interaction_evaluation_count".to_string(), + summary.interaction_evaluation_count, + ), + ("source_count".to_string(), summary.source_count), + ]); + Self { + abi: "pyamplicol-fully-resident-materialization-census-v1".to_string(), + basis: "immutable-fully-resident-compiled-dag".to_string(), + r#final: counts.clone(), + peak: counts, + final_equals_peak: true, + } + } +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct ExecutionPlan { + schema_version: u32, + kind: String, + process_key: String, + process: String, + external_particles: Vec, + #[serde(default)] + model: Option, + #[serde(default)] + model_parameters: Vec, + #[serde(default)] + normalization: Option, + parameter_layout: GenericParameterLayoutManifest, + current_storage: GenericCurrentStorageManifest, + value_storage: GenericValueStorageManifest, + source_fill: GenericSourceFillManifest, + momentum_slots: Vec, + stages: Vec, + amplitude_stage: GenericAmplitudeStageManifest, + #[serde(default)] + helicity_recurrence: Option, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct GenericExternalParticleManifest { + label: usize, + index: usize, + pdg: i32, + outgoing_pdg: i32, + role: String, + momentum_slot: usize, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct GenericRuntimeModelManifest { + #[serde(default)] + particles: Vec, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(deny_unknown_fields)] +struct GenericRuntimeModelParameterManifest { + name: String, + kind: String, + parameter_index: usize, + #[serde(default)] + default: f64, + #[serde(default)] + pdg: Option, + #[serde(default)] + runtime_name: Option, + #[serde(default)] + complex_component: Option, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct GenericRuntimeParticleManifest { + pdg: i32, + #[serde(default)] + mass: f64, + #[serde(default)] + mass_parameter: Option, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct GenericRuntimeNormalizationManifest { + #[serde(default = "default_one_f64")] + color_factor: f64, + #[serde(default = "default_one_f64")] + global_coupling_factor: f64, + #[serde(default = "default_one_f64")] + average_factor: f64, + #[serde(default = "default_one_f64")] + identical_factor: f64, + #[serde(default)] + qcd_coupling_power: usize, + #[serde(default)] + electroweak_coupling_power: usize, +} + +fn default_one_f64() -> f64 { + 1.0 +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct GenericParameterLayoutManifest { + source_component_parameter_count: usize, + momentum_parameter_count: usize, + #[serde(default)] + model_parameter_count: usize, + parameter_count_if_flattened: usize, + value_component_count: usize, + source_components_complex: bool, + momentum_components_real: bool, + real_valued_inputs: Vec, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct GenericCurrentStorageManifest { + component_count: usize, + number_type: String, + #[serde(default)] + metadata_compacted: bool, + current_slots: Vec, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct GenericCurrentSlotManifest { + current_id: usize, + component_start: usize, + component_stop: usize, + dimension: usize, + is_source: bool, + particle_id: i32, + external_mask: u64, + #[serde(default)] + external_labels: Vec, + #[serde(default)] + helicity_ancestry: Value, + chirality: i32, + #[serde(default)] + spin_state: Value, + #[serde(default)] + flavour_flow: Vec, + #[serde(default)] + color_state: Value, + momentum_mask: u64, + auxiliary_kind: Option, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct GenericValueStorageManifest { + component_count: usize, + number_type: String, + #[serde(default)] + metadata_compacted: bool, + value_slots: Vec, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct GenericValueSlotManifest { + value_slot_id: usize, + current_id: usize, + variant: String, + component_start: usize, + component_stop: usize, + dimension: usize, + current_component_start: usize, + current_component_stop: usize, + is_source: bool, + applies_propagator: bool, + particle_id: i32, + external_mask: u64, + #[serde(default)] + external_labels: Vec, + momentum_mask: u64, + chirality: i32, + propagator: GenericPropagatorIrManifest, +} + +#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq)] +#[serde(rename_all = "kebab-case")] +enum GenericPropagatorKindManifest { + Identity, + Scalar, + WeylFermion, + DiracFermion, + Vector, + Spin2, + Custom, + Unsupported, +} + +#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq)] +#[serde(rename_all = "kebab-case")] +enum GenericPropagatorMassClassManifest { + Massless, + Massive, + NotApplicable, +} + +#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq)] +#[serde(rename_all = "kebab-case")] +enum GenericPropagatorGaugeManifest { + Feynman, + Unitary, + DeDonder, + FierzPauli, + ModelSupplied, +} + +#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq)] +#[serde(rename_all = "kebab-case")] +enum GenericGoldstonePolicyManifest { + NotApplicable, + Absorbed, + Explicit, + ModelSupplied, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct GenericPropagatorIrManifest { + identity: GenericParticleIdentityIrManifest, + particle_id: i32, + chirality: i32, + kind: GenericPropagatorKindManifest, + backend: String, + basis: String, + applies_propagator: bool, + kernel: String, + full_tensor_network_ready: bool, + mass_class: GenericPropagatorMassClassManifest, + gauge: Option, + numerator: Option, + denominator: Option, + mass_parameter: Option, + width_parameter: Option, + custom_source: Option, + auxiliary_policy: Option, + goldstone_policy: GenericGoldstonePolicyManifest, + description: String, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct GenericSourceFillManifest { + source_count: usize, + sources: Vec, +} + +#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "kebab-case")] +enum GenericSourceOrientationManifest { + Particle, + Antiparticle, + SelfConjugate, +} + +#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "lowercase")] +enum GenericParticleStatisticsManifest { + Boson, + Fermion, + Ghost, + Auxiliary, +} + +#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "lowercase")] +enum GenericWavefunctionFamilyManifest { + Scalar, + Fermion, + Vector, + Spin2, + Ghost, + Auxiliary, +} + +impl GenericWavefunctionFamilyManifest { + const fn as_str(self) -> &'static str { + match self { + Self::Scalar => "scalar", + Self::Fermion => "fermion", + Self::Vector => "vector", + Self::Spin2 => "spin2", + Self::Ghost => "ghost", + Self::Auxiliary => "auxiliary", + } + } +} + +#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "kebab-case")] +enum GenericMomentumTransformManifest { + Identity, + NegateFourMomentum, +} + +impl GenericMomentumTransformManifest { + const fn legacy_projection(self) -> &'static str { + match self { + Self::Identity => "identity", + Self::NegateFourMomentum => "negate-incoming-momentum", + } + } +} + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[serde(untagged)] +enum GenericSourceSpinStateManifest { + Scalar(i32), + Components(Vec), +} + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +struct GenericSourceStateIrManifest { + helicity: i32, + chirality: i32, + spin_state: GenericSourceSpinStateManifest, +} + +impl GenericSourceStateIrManifest { + fn transformed(&self, crossing: &GenericCrossingIrManifest) -> Result { + let helicity = self + .helicity + .checked_mul(crossing.helicity_factor) + .ok_or("source crossing overflows the helicity state")?; + let chirality = self + .chirality + .checked_mul(crossing.chirality_factor) + .ok_or("source crossing overflows the chirality state")?; + let spin_state = match (&self.spin_state, crossing.spin_state_factor) { + (state, 1) => state.clone(), + (GenericSourceSpinStateManifest::Scalar(state), factor) => { + GenericSourceSpinStateManifest::Scalar( + state + .checked_mul(factor) + .ok_or("source crossing overflows the spin state")?, + ) + } + (GenericSourceSpinStateManifest::Components(_), _) => { + return Err("crossing cannot multiply a structured source spin state"); + } + }; + Ok(Self { + helicity, + chirality, + spin_state, + }) + } +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +struct GenericCrossingIrManifest { + momentum_transform: GenericMomentumTransformManifest, + helicity_factor: i32, + chirality_factor: i32, + spin_state_factor: i32, + phase: [f64; 2], +} + +impl GenericCrossingIrManifest { + fn is_identity(&self) -> bool { + self.momentum_transform == GenericMomentumTransformManifest::Identity + && self.helicity_factor == 1 + && self.chirality_factor == 1 + && self.spin_state_factor == 1 + && self.phase == [1.0, 0.0] + } +} + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +struct GenericParticleIdentityIrManifest { + canonical_id: String, + species_id: String, + anti_canonical_id: String, + display_name: String, + anti_display_name: String, + pdg_label: i32, + anti_pdg_label: i32, + orientation: GenericSourceOrientationManifest, + self_conjugate: bool, +} + +fn deserialize_required_nullable_string<'de, D>(deserializer: D) -> Result, D::Error> +where + D: serde::Deserializer<'de>, +{ + match Value::deserialize(deserializer)? { + Value::Null => Ok(None), + Value::String(value) => Ok(Some(value)), + _ => Err(::custom( + "expected a string or null", + )), + } +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +struct GenericSourceIrManifest { + identity: GenericParticleIdentityIrManifest, + statistics: GenericParticleStatisticsManifest, + wavefunction_family: GenericWavefunctionFamilyManifest, + component_dimension: usize, + states: Vec, + crossing: GenericCrossingIrManifest, + basis: String, + #[serde(deserialize_with = "deserialize_required_nullable_string")] + mass_parameter: Option, + #[serde(deserialize_with = "deserialize_required_nullable_string")] + width_parameter: Option, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(deny_unknown_fields)] +struct GenericSourceRecordManifest { + source_id: usize, + current_id: usize, + current_component_start: usize, + current_component_stop: usize, + value_slot: GenericValueSlotRefManifest, + source_parameter_start: usize, + source_parameter_stop: usize, + leg_label: usize, + input_momentum_slot: usize, + side: String, + crossing: String, + physical_pdg: i32, + outgoing_pdg: i32, + particle_id: i32, + anti_particle_id: i32, + source_kind: String, + wavefunction_kind: String, + source_orientation: GenericSourceOrientationManifest, + source_basis: String, + source_ir: GenericSourceIrManifest, + applied_crossing: GenericCrossingIrManifest, + source_helicity: i32, + chirality: i32, + spin_state: Value, + dimension: usize, + helicity_ancestry: Value, + color_state: Value, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(deny_unknown_fields)] +struct GenericMomentumSlotManifest { + momentum_slot_id: usize, + momentum_mask: u64, + external_labels: Vec, + component_start: usize, + component_stop: usize, + real_valued: bool, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct GenericStageManifest { + stage_index: usize, + stage_kind: String, + subset_size: usize, + input_current_ids: Vec, + output_current_ids: Vec, + input_value_slot_ids: Vec, + output_value_slot_ids: Vec, + interaction_count: usize, + #[serde(default)] + interactions_compacted: bool, + #[serde(default)] + interaction_ids: Vec, + #[serde(default)] + interactions: Vec, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct GenericInteractionManifest { + interaction_id: usize, + vertex_kind: i32, + vertex_particles: Vec, + left_current_id: usize, + right_current_id: usize, + result_current_id: usize, + left_slot: GenericSlotRefManifest, + right_slot: GenericSlotRefManifest, + result_slot: GenericSlotRefManifest, + left_value_slot: GenericValueSlotRefManifest, + right_value_slot: GenericValueSlotRefManifest, + result_value_slots: Vec, + result_requires_propagated_value: bool, + result_requires_unpropagated_value: bool, + momentum_slots: GenericInteractionMomentumSlotsManifest, + coupling: Vec, + color_weight: Vec, + accumulation: String, + lowering: GenericLoweringManifest, + full_tensor_network_ready: bool, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct GenericLoweringManifest { + kind: i32, + backend: String, + tensor_names: Vec, + expression_head: String, + full_tensor_network_ready: bool, + description: String, + kernel: String, + input_roles: Vec, + output_role: String, + coupling_mode: String, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct GenericInteractionMomentumSlotsManifest { + left: usize, + right: usize, + result: usize, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct GenericSlotRefManifest { + current_id: usize, + component_start: usize, + component_stop: usize, + dimension: usize, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct GenericAmplitudeStageManifest { + stage_kind: String, + output_count: usize, + #[serde(default)] + color_contraction: Option, + #[serde(default)] + color_topology_replay: Option, + roots: Vec, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct GenericColorTopologyReplayAmplitudeManifest { + contract_version: u32, + physical_group_count: usize, + physical_groups: Vec, + mappings: Vec, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct GenericColorTopologyReplayPhysicalGroupManifest { + group_id: i64, + helicities: Vec, + color_sector_id: i64, + color_word: Vec, + helicity_weight: f64, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct GenericColorTopologyReplayMappingManifest { + label_permutation: Vec, + group_routes: Vec, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct GenericColorTopologyReplayGroupRouteManifest { + source_group_id: i64, + target_group_id: i64, + factor: Vec, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct GenericColorContractionManifest { + supported: bool, + #[serde(default)] + reason: Option, + group_count: usize, + #[serde(default)] + includes_color_factor: bool, + entries: Vec, + #[serde(default)] + repeated_block: Option, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct GenericColorContractionEntryManifest { + left_group_id: i64, + right_group_id: i64, + weight: Vec, + // Retained for Python arbitrary precision; native contraction keeps its f64 weight. + #[serde(default, rename = "exact_weight")] + _exact_weight: Option<[String; 4]>, + #[serde(default = "default_symmetry_factor")] + symmetry_factor: f64, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct GenericRepeatedColorContractionBlockManifest { + component_count: usize, + component_group_ids: Vec, + entries: Vec, + #[serde(default)] + factorized_block: Option, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(tag = "kind", deny_unknown_fields)] +enum GenericFactorizedColorContractionBlockManifest { + #[serde(rename = "klein-four-walsh")] + KleinFourWalsh { cosets: Vec<[usize; 4]> }, + #[serde(rename = "elementary-abelian-walsh")] + ElementaryAbelianWalsh { + rank: usize, + cosets: Vec>, + }, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct GenericRepeatedColorContractionEntryManifest { + left_group_index: usize, + right_group_index: usize, + weight: Vec, + #[serde(default, rename = "exact_weight")] + _exact_weight: Option<[String; 4]>, + #[serde(default = "default_symmetry_factor")] + symmetry_factor: f64, +} + +fn default_symmetry_factor() -> f64 { + 1.0 +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct GenericAmplitudeRootManifest { + output_index: usize, + root_id: usize, + kind: String, + left_current_id: usize, + right_current_id: usize, + left_slot: GenericSlotRefManifest, + right_slot: GenericSlotRefManifest, + left_value_slot: GenericValueSlotRefManifest, + right_value_slot: GenericValueSlotRefManifest, + vertex_kind: Option, + vertex_particles: Option>, + coupling: Vec, + color_weight: Vec, + #[serde(default)] + color_sector_id: Option, + contraction: String, + contraction_ir: GenericContractionIrManifest, + coherent_group_id: Option, + helicity_weight: f64, + #[serde(default)] + all_sector_weight: Option, +} + +#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq)] +#[serde(rename_all = "lowercase")] +enum GenericContractionChiralityRelationManifest { + Any, + Equal, + Opposite, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct GenericContractionIrManifest { + name: String, + left_basis: String, + right_basis: String, + coefficients: Vec<[f64; 2]>, + chirality_relation: GenericContractionChiralityRelationManifest, + #[serde(deserialize_with = "deserialize_required_nullable_string")] + metric_signature: Option, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(deny_unknown_fields)] +struct GenericValueSlotRefManifest { + value_slot_id: usize, + current_id: usize, + variant: String, + component_start: usize, + component_stop: usize, + dimension: usize, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct NativeCompiledDirectTargetManifest { + triple: String, + cpu_features: Vec, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct NativeCompiledDirectApplicationManifest { + application_abi: String, + function_name: String, + source_path: String, + library_path: String, + target: NativeCompiledDirectTargetManifest, + evaluator_state_sha256: String, + instruction_count: u32, + temporary_count: u32, + input_plane_count: u32, + scalar_input_count: u32, + output_plane_count: u32, + simd_lane_width: u32, + logical_stack_bytes: u32, + output_semantics: String, +} + +impl NativeCompiledDirectApplicationManifest { + fn validate( + &self, + expected_function_name: &str, + input_len: usize, + output_len: usize, + ) -> RusticolResult<()> { + const APPLICATION_ABI: &str = "pyamplicol-native-compiled-direct-application-v1"; + const OUTPUT_SEMANTICS: &str = "factor-free-overwrite"; + const MAXIMUM_LOGICAL_STACK_BYTES: u32 = 64 * 1024; + + if self.application_abi != APPLICATION_ABI { + return Err(RusticolError::compatibility(format!( + "native compiled DirectApplication declares ABI {:?}, expected {APPLICATION_ABI:?}", + self.application_abi + ))); + } + if self.function_name != expected_function_name { + return Err(RusticolError::integrity( + "native compiled DirectApplication function identity does not match its evaluator", + )); + } + if self.source_path.is_empty() + || self.library_path.is_empty() + || self.source_path == self.library_path + || self.target.triple.is_empty() + || self.target.triple.contains('\0') + { + return Err(RusticolError::integrity( + "native compiled DirectApplication paths or target are invalid", + )); + } + if self + .target + .cpu_features + .windows(2) + .any(|pair| pair[0] >= pair[1]) + || self + .target + .cpu_features + .iter() + .any(|feature| feature.is_empty() || feature.contains('\0')) + { + return Err(RusticolError::integrity( + "native compiled DirectApplication CPU features are not sorted and unique", + )); + } + if self.evaluator_state_sha256.len() != 64 + || !self + .evaluator_state_sha256 + .bytes() + .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte)) + { + return Err(RusticolError::integrity( + "native compiled DirectApplication evaluator-state digest is invalid", + )); + } + if self.instruction_count == 0 + || self.input_plane_count == 0 + || !matches!(self.simd_lane_width, 2 | 4) + || self.logical_stack_bytes == 0 + || self.logical_stack_bytes > MAXIMUM_LOGICAL_STACK_BYTES + || self.output_semantics != OUTPUT_SEMANTICS + { + return Err(RusticolError::integrity( + "native compiled DirectApplication execution shape is invalid", + )); + } + + let expected_output_planes = u32::try_from(output_len.checked_mul(2).ok_or_else(|| { + RusticolError::integrity("native compiled DirectApplication output shape overflows") + })?) + .map_err(|_| { + RusticolError::integrity("native compiled DirectApplication output shape exceeds u32") + })?; + if self.output_plane_count != expected_output_planes { + return Err(RusticolError::integrity( + "native compiled DirectApplication output planes do not match its evaluator", + )); + } + + let descriptor_count = self + .input_plane_count + .checked_add(self.scalar_input_count) + .ok_or_else(|| { + RusticolError::integrity( + "native compiled DirectApplication input descriptor count overflows", + ) + })?; + let minimum_descriptors = u32::try_from(input_len).map_err(|_| { + RusticolError::integrity( + "native compiled DirectApplication logical input count exceeds u32", + ) + })?; + let maximum_descriptors = minimum_descriptors.checked_mul(2).ok_or_else(|| { + RusticolError::integrity( + "native compiled DirectApplication maximum descriptor count overflows", + ) + })?; + if descriptor_count < minimum_descriptors || descriptor_count > maximum_descriptors { + return Err(RusticolError::integrity( + "native compiled DirectApplication descriptors do not cover evaluator inputs", + )); + } + + let expected_stack_bytes = self + .temporary_count + .checked_add(u32::try_from(output_len).map_err(|_| { + RusticolError::integrity( + "native compiled DirectApplication output count exceeds u32", + ) + })?) + .and_then(|count| count.checked_mul(2)) + .and_then(|count| count.checked_mul(std::mem::size_of::() as u32)) + .and_then(|bytes| bytes.checked_mul(self.simd_lane_width)) + .ok_or_else(|| { + RusticolError::integrity( + "native compiled DirectApplication logical stack shape overflows", + ) + })?; + if self.logical_stack_bytes != expected_stack_bytes { + return Err(RusticolError::integrity( + "native compiled DirectApplication logical stack metadata is inconsistent", + )); + } + Ok(()) + } +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +#[serde(tag = "kind")] +enum EvaluatorManifest { + #[serde(rename = "symjit-application-evaluator")] + SymjitApplication { + runtime_capability: String, + application_path: String, + application_abi: String, + input_len: usize, + output_len: usize, + element_layout: String, + batch_layout: String, + compiler_type: String, + translation_mode: String, + optimization_level: u8, + word_bits: u8, + endianness: String, + required_defuns: Vec, + #[serde(default)] + plane_application: Option, + evaluator_state_path: Option, + evaluator_state_runtime_capability: Option, + }, + #[serde(rename = "jit-symbolica-evaluator")] + Jit { + runtime_capability: String, + input_len: usize, + output_len: usize, + evaluator_state_path: String, + }, + #[serde(rename = "compiled-complex-evaluator")] + CompiledComplex { + runtime_capability: String, + function_name: String, + input_len: usize, + output_len: usize, + library_path: String, + evaluator_state_path: Option, + number_type: String, + #[serde(default)] + native_direct_application: Option, + }, + #[serde(rename = "chunked-symbolica-evaluator")] + Chunked { + required_runtime_capabilities: Vec, + #[serde(default)] + input_len: Option, + #[serde(default)] + chunk_input_indices: Option>>, + chunks: Vec, + }, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct SymjitPlaneApplicationManifest { + application_path: String, + application_abi: String, + storage_abi: String, + element_layout: String, + descriptor_order: String, + input_complex_count: usize, + output_complex_count: usize, + input_plane_count: usize, + output_plane_count: usize, + compiler_type: String, + translation_mode: String, + optimization_level: u8, + simd: bool, + complex: bool, + fast_math: bool, + fast_complex: bool, + compression: bool, + threading: bool, + direct_arena: bool, + source_digest: String, + target: Value, +} + +impl SymjitPlaneApplicationManifest { + fn validate( + &self, + input_complex_count: usize, + output_complex_count: usize, + optimization_level: u8, + ) -> RusticolResult<()> { + let expected_inputs = input_complex_count + .checked_mul(2) + .ok_or_else(|| RusticolError::integrity("SymJIT plane input count overflows usize"))?; + let expected_outputs = output_complex_count + .checked_mul(2) + .ok_or_else(|| RusticolError::integrity("SymJIT plane output count overflows usize"))?; + let digest_valid = self.source_digest.len() == 64 + && self + .source_digest + .bytes() + .all(|value| value.is_ascii_hexdigit() && !value.is_ascii_uppercase()); + if self.application_path.is_empty() + || self.application_abi != SYMJIT_PLANE_APPLICATION_ABI + || self.storage_abi != SYMJIT_APPLICATION_STORAGE_ABI + || self.element_layout != "split-complex-plane-major" + || self.descriptor_order != "inputs-re-im-then-outputs-re-im" + || self.input_complex_count != input_complex_count + || self.output_complex_count != output_complex_count + || self.input_plane_count != expected_inputs + || self.output_plane_count != expected_outputs + || self.compiler_type != "native" + || self.translation_mode != "symbolica-structured-instructions" + || self.optimization_level != optimization_level + || !self.simd + || !self.complex + || !self.fast_math + || self.fast_complex + || self.threading + || !self.direct_arena + || !digest_valid + || !valid_symjit_plane_target(&self.target) + { + return Err(RusticolError::compatibility( + "SymJIT plane application metadata is incompatible; regenerate the artifact", + )); + } + Ok(()) + } +} + +fn valid_symjit_plane_target(target: &Value) -> bool { + let Some(target) = target.as_object() else { + return false; + }; + if target.keys().any(|key| { + !matches!( + key.as_str(), + "word_bits" | "endianness" | "triple" | "cpu_features" + ) + }) || target.get("word_bits").and_then(Value::as_u64) != Some(64) + || target.get("endianness").and_then(Value::as_str) != Some("little") + { + return false; + } + if let Some(triple) = target.get("triple") { + let Some(triple) = triple.as_str() else { + return false; + }; + if triple.is_empty() || triple.contains('\0') { + return false; + } + } + if let Some(features) = target.get("cpu_features") { + let Some(features) = features.as_array() else { + return false; + }; + let mut previous: Option<&str> = None; + for feature in features { + let Some(feature) = feature.as_str() else { + return false; + }; + if feature.is_empty() + || feature.contains('\0') + || previous.is_some_and(|previous| previous >= feature) + { + return false; + } + previous = Some(feature); + } + } + true +} + +/// One leaf in the canonical evaluator preorder used by loading, selector +/// coverage, and Direct-Arena lowering. +/// +/// Keeping the composed root-input map and output range together prevents +/// independently implemented flattening walks from silently assigning +/// selector chunk indices to different leaves. +struct EvaluatorLeafLayout<'a> { + evaluator: &'a EvaluatorManifest, + input_indices: Vec, + output_range: std::ops::Range, +} + +impl EvaluatorManifest { + fn io_len(&self) -> RusticolResult<(usize, usize)> { + match self { + Self::SymjitApplication { + input_len, + output_len, + .. + } + | Self::Jit { + input_len, + output_len, + .. + } => Ok((*input_len, *output_len)), + Self::CompiledComplex { + function_name, + input_len, + output_len, + .. + } => { + if let Self::CompiledComplex { + native_direct_application: Some(application), + .. + } = self + { + application.validate(function_name, *input_len, *output_len)?; + } + Ok((*input_len, *output_len)) + } + Self::Chunked { + input_len, + chunk_input_indices, + chunks, + .. + } => { + if chunks.is_empty() { + return Err(RusticolError::artifact( + "generic serialized evaluator chunk list is empty", + )); + } + let mut child_layouts = Vec::with_capacity(chunks.len()); + let mut output_len = 0usize; + for chunk in chunks { + let layout = chunk.io_len()?; + output_len = output_len.checked_add(layout.1).ok_or_else(|| { + RusticolError::artifact( + "generic serialized evaluator output length overflows usize", + ) + })?; + child_layouts.push(layout); + } + match (input_len, chunk_input_indices) { + (None, None) => { + let parent_input_len = child_layouts[0].0; + if child_layouts + .iter() + .any(|(child_input_len, _)| *child_input_len != parent_input_len) + { + return Err(RusticolError::artifact( + "legacy chunked evaluator children have inconsistent input lengths", + )); + } + Ok((parent_input_len, output_len)) + } + (Some(parent_input_len), Some(input_indices)) => { + if input_indices.len() != chunks.len() { + return Err(RusticolError::artifact( + "chunked evaluator input maps do not match evaluator chunks", + )); + } + for (indices, (child_input_len, _)) in + input_indices.iter().zip(&child_layouts) + { + if indices.len() != *child_input_len + || indices.iter().any(|index| *index >= *parent_input_len) + || indices.windows(2).any(|pair| pair[0] >= pair[1]) + { + return Err(RusticolError::artifact( + "chunked evaluator input map is inconsistent with child inputs", + )); + } + } + Ok((*parent_input_len, output_len)) + } + _ => Err(RusticolError::artifact( + "chunked evaluator input metadata is incomplete", + )), + } + } + } + } + + fn leaf_layout(&self) -> RusticolResult>> { + fn append_leaf_layouts<'a>( + evaluator: &'a EvaluatorManifest, + parent_inputs: &[usize], + output_cursor: &mut usize, + leaves: &mut Vec>, + ) -> RusticolResult<()> { + match evaluator { + EvaluatorManifest::Chunked { + input_len, + chunk_input_indices, + chunks, + .. + } => { + evaluator.io_len()?; + if let Some(input_len) = input_len + && *input_len != parent_inputs.len() + { + return Err(RusticolError::artifact( + "chunked evaluator parent input mapping has an inconsistent length", + )); + } + match chunk_input_indices { + Some(chunk_inputs) => { + for (chunk, indices) in chunks.iter().zip(chunk_inputs) { + let mapped = indices + .iter() + .map(|index| { + parent_inputs.get(*index).copied().ok_or_else(|| { + RusticolError::artifact( + "chunked evaluator input map references an absent parent input", + ) + }) + }) + .collect::>>()?; + append_leaf_layouts(chunk, &mapped, output_cursor, leaves)?; + } + } + None => { + for chunk in chunks { + append_leaf_layouts(chunk, parent_inputs, output_cursor, leaves)?; + } + } + } + } + _ => { + let (input_len, output_len) = evaluator.io_len()?; + if input_len != parent_inputs.len() { + return Err(RusticolError::artifact( + "evaluator leaf input mapping has an inconsistent length", + )); + } + if output_len == 0 { + return Err(RusticolError::artifact( + "evaluator leaf has an empty output range", + )); + } + let output_stop = output_cursor.checked_add(output_len).ok_or_else(|| { + RusticolError::artifact("evaluator leaf output range overflows usize") + })?; + leaves.push(EvaluatorLeafLayout { + evaluator, + input_indices: parent_inputs.to_vec(), + output_range: *output_cursor..output_stop, + }); + *output_cursor = output_stop; + } + } + Ok(()) + } + + let (root_input_len, root_output_len) = self.io_len()?; + let root_inputs = (0..root_input_len).collect::>(); + let mut output_cursor = 0; + let mut leaves = Vec::new(); + append_leaf_layouts(self, &root_inputs, &mut output_cursor, &mut leaves)?; + if output_cursor != root_output_len { + return Err(RusticolError::artifact( + "evaluator leaf output ranges do not cover the root output", + )); + } + Ok(leaves) + } +} + +struct EvaluatorGroup { + evaluators: Vec, + input_len: usize, + input_mappings: Vec>>, + input_mapping_spans: Vec>, + output_len: usize, + chunk_parameter_scratch_f64: Vec>, + chunk_scratch_f64: Vec>, + chunk_parameter_scratch_aosoa_f64: Vec, + chunk_scratch_aosoa_f64: Vec, + chunk_input_mapping_scratch: Vec, +} + +// Evaluator groups already own these values behind a Vec allocation. Boxing +// only the SymJIT variant would add an indirection to every hot kernel call. +#[allow(clippy::large_enum_variant)] +enum F64Evaluator { + #[cfg(feature = "symbolica-runtime")] + ExactOnly, + #[cfg(feature = "f64-symjit")] + SymjitApplication(SymjitApplicationEvaluator), + #[cfg(feature = "f64-compiled")] + Compiled(CompiledComplexF64Evaluator), + #[cfg(feature = "symbolica-runtime")] + Jit(JITCompiledEvaluator>), +} + +struct LoadedEvaluator { + eval: F64Evaluator, + #[cfg(feature = "symbolica-runtime")] + exact_eval: Option>>, + #[cfg(feature = "symbolica-runtime")] + exact_eval_source: Option, + #[cfg(feature = "symbolica-runtime")] + double_eval: Option>>, + #[cfg(feature = "symbolica-runtime")] + arb_eval: Option<(u32, ExpressionEvaluator>)>, + input_len: usize, + output_len: usize, +} + +#[cfg(feature = "symbolica-runtime")] +trait RusticolHighPrecisionNumber: + Real + RealLike + From + PartialOrd + Clone + EvaluationDomain +where + Complex: Real + EvaluationDomain, +{ + fn evaluate_loaded( + evaluator: &mut LoadedEvaluator, + params: &[Complex], + out: &mut [Complex], + binary_precision: Option, + ) -> RusticolResult<()>; +} + +#[cfg(feature = "symbolica-runtime")] +impl RusticolHighPrecisionNumber for DoubleFloat { + fn evaluate_loaded( + evaluator: &mut LoadedEvaluator, + params: &[Complex], + out: &mut [Complex], + _binary_precision: Option, + ) -> RusticolResult<()> { + if evaluator.double_eval.is_none() { + let exact = evaluator.exact_evaluator()?.clone(); + evaluator.double_eval = + Some(exact.map_coeff(&|c| { + Complex::new(DoubleFloat::from(&c.re), DoubleFloat::from(&c.im)) + })); + } + evaluator + .double_eval + .as_mut() + .expect("double evaluator initialized") + .evaluate(params, out); + Ok(()) + } +} + +#[cfg(feature = "symbolica-runtime")] +impl RusticolHighPrecisionNumber for Float { + fn evaluate_loaded( + evaluator: &mut LoadedEvaluator, + params: &[Complex], + out: &mut [Complex], + binary_precision: Option, + ) -> RusticolResult<()> { + let binary_precision = binary_precision.ok_or_else(|| { + RusticolError::invalid_argument( + "arbitrary-precision evaluation needs a binary precision", + ) + })?; + let rebuild = evaluator + .arb_eval + .as_ref() + .map(|(precision, _)| *precision != binary_precision) + .unwrap_or(true); + if rebuild { + let exact = evaluator.exact_evaluator()?.clone(); + evaluator.arb_eval = Some(( + binary_precision, + exact.map_coeff_with_prec( + &|c| { + Complex::new( + c.re.to_multi_prec_float(binary_precision), + c.im.to_multi_prec_float(binary_precision), + ) + }, + binary_precision, + ), + )); + } + evaluator + .arb_eval + .as_mut() + .expect("arbitrary-precision evaluator initialized") + .1 + .evaluate(params, out); + Ok(()) + } +} + +struct RawSumGroup { + id: i64, + indices: Vec, + weight: f64, + all_sector_weight: f64, + sector_ids: Vec, +} + +struct ColorContractionRuntime { + group_count: usize, + entries: Vec, + repeated_block: Option, + symmetric_group: Option, + group_scratch_f64: Vec>, +} + +struct CompiledSymmetricGroupColorContraction { + plan: crate::recurrence::RecurrenceColorContraction, + ordered_group_indices: Vec, + workspace: Option, +} + +#[derive(Clone, Copy)] +struct ColorContractionEntry { + left_group_index: usize, + right_group_index: usize, + weight_re: f64, + weight_im: f64, + symmetry_factor: f64, +} + +/// One color matrix shared by several disconnected contraction components. +/// +/// Helicity-summed NLC/full-color plans contain one isomorphic color block per +/// physical helicity. Keeping only one canonical block avoids streaming the +/// same sparse matrix metadata once per helicity and lays out group values so +/// that the repeated component dimension is contiguous. +struct RepeatedColorContractionBlock { + component_count: usize, + component_group_indices: Vec, + singleton_output_indices: Option>, + entries: Vec, + all_weights_real: bool, + walsh_block: Option, + c2k_walsh_block: Option, +} + +/// Four real symmetric blocks obtained from a normalized C2 x C2 Walsh basis. +/// +/// The cosets map canonical local color groups to each four-point input +/// transform. Entries address the transformed character/coset axis and retain +/// a contiguous repeated-component dimension. +struct WalshColorContractionBlock { + cosets: Vec<[usize; 4]>, + entries: Vec, +} + +/// Real symmetric blocks diagonalized in an unnormalized C2^k Walsh basis. +/// +/// Each coset is ordered by the generator bitmask, so the group product is +/// integer XOR. The transformed entries carry the single inverse-subgroup-order +/// factor. Rank three uses a dedicated eight-point butterfly; higher ranks use +/// the same in-place radix-two transform over reusable scratch. +struct C2kWalshColorContractionBlock { + subgroup_order: usize, + cosets: Vec>, + entries: Vec, +} + +impl ColorContractionRuntime { + fn new(groups: &[RawSumGroup], entries: Vec) -> Self { + let repeated_block = repeated_color_contraction_block(groups, &entries); + Self { + group_count: groups.len(), + entries, + repeated_block, + symmetric_group: None, + group_scratch_f64: Vec::new(), + } + } + + fn from_repeated_block( + groups: &[RawSumGroup], + component_count: usize, + component_group_indices: Vec, + entries: Vec, + walsh_block: Option, + c2k_walsh_block: Option, + ) -> Self { + let singleton_output_indices = component_group_indices + .iter() + .map( + |group_index| match groups[*group_index].indices.as_slice() { + [output_index] => Some(*output_index), + _ => None, + }, + ) + .collect::>>(); + let repeated_block = RepeatedColorContractionBlock { + component_count, + component_group_indices, + singleton_output_indices, + all_weights_real: entries.iter().all(|entry| entry.weight_im == 0.0), + entries, + walsh_block, + c2k_walsh_block, + }; + Self { + group_count: groups.len(), + entries: Vec::new(), + repeated_block: Some(repeated_block), + symmetric_group: None, + group_scratch_f64: Vec::new(), + } + } + + fn from_symmetric_group( + groups: &[RawSumGroup], + plan: crate::recurrence::RecurrenceColorContraction, + ordered_group_indices: Vec, + ) -> Self { + Self { + group_count: groups.len(), + entries: Vec::new(), + repeated_block: None, + symmetric_group: Some(CompiledSymmetricGroupColorContraction { + plan, + ordered_group_indices, + workspace: None, + }), + group_scratch_f64: Vec::new(), + } + } + + const fn is_symmetric_group(&self) -> bool { + self.symmetric_group.is_some() + } + + fn logical_entry_count(&self) -> RusticolResult { + if self.is_symmetric_group() { + return Err(RusticolError::compatibility( + "compiled symmetric-group FFT diagnostic contraction has no dense logical-entry view", + )); + } + if self.entries.is_empty() { + let Some(block) = self.repeated_block.as_ref() else { + return Ok(0); + }; + block + .component_count + .checked_mul(block.entries.len()) + .ok_or_else(|| { + RusticolError::invalid_argument( + "repeated colour contraction logical entry count overflows", + ) + }) + } else { + Ok(self.entries.len()) + } + } + + fn logical_entries(&self) -> ColorContractionEntries<'_> { + if self.entries.is_empty() + && let Some(block) = self.repeated_block.as_ref() + { + return ColorContractionEntries::Repeated { + block, + component_index: 0, + entry_index: 0, + }; + } + ColorContractionEntries::Expanded(self.entries.iter().copied()) + } +} + +enum ColorContractionEntries<'a> { + Expanded(std::iter::Copied>), + Repeated { + block: &'a RepeatedColorContractionBlock, + component_index: usize, + entry_index: usize, + }, +} + +impl Iterator for ColorContractionEntries<'_> { + type Item = ColorContractionEntry; + + fn next(&mut self) -> Option { + match self { + Self::Expanded(entries) => entries.next(), + Self::Repeated { + block, + component_index, + entry_index, + } => { + if *component_index >= block.component_count || block.entries.is_empty() { + return None; + } + let entry = block.entries[*entry_index]; + let component = *component_index; + let left_group_index = block.component_group_indices + [entry.left_group_index * block.component_count + component]; + let right_group_index = block.component_group_indices + [entry.right_group_index * block.component_count + component]; + *entry_index += 1; + if *entry_index == block.entries.len() { + *entry_index = 0; + *component_index += 1; + } + Some(ColorContractionEntry { + left_group_index, + right_group_index, + ..entry + }) + } + } + } +} + +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +struct CanonicalColorContractionEntry { + left_group_index: usize, + right_group_index: usize, + weight_re_bits: u64, + weight_im_bits: u64, + symmetry_factor_bits: u64, +} + +fn color_component_root(parent: &mut [usize], mut index: usize) -> usize { + while parent[index] != index { + parent[index] = parent[parent[index]]; + index = parent[index]; + } + index +} + +fn repeated_color_contraction_block( + groups: &[RawSumGroup], + entries: &[ColorContractionEntry], +) -> Option { + if groups.len() < 2 || entries.is_empty() { + return None; + } + + let mut parent = (0..groups.len()).collect::>(); + let mut component_size = vec![1usize; groups.len()]; + for entry in entries { + let mut left = color_component_root(&mut parent, entry.left_group_index); + let mut right = color_component_root(&mut parent, entry.right_group_index); + if left == right { + continue; + } + if component_size[left] < component_size[right] { + std::mem::swap(&mut left, &mut right); + } + parent[right] = left; + component_size[left] += component_size[right]; + } + + let mut components_by_root = BTreeMap::>::new(); + for group_index in 0..groups.len() { + let root = color_component_root(&mut parent, group_index); + components_by_root + .entry(root) + .or_default() + .push(group_index); + } + let mut components = components_by_root.into_values().collect::>(); + components.sort_by_key(|component| component.iter().copied().min().unwrap_or(usize::MAX)); + if components.len() < 2 { + return None; + } + let groups_per_component = components[0].len(); + if groups_per_component == 0 + || components + .iter() + .any(|component| component.len() != groups_per_component) + { + return None; + } + + let component_maps = components + .iter() + .map(|component| { + let mut by_sector_signature = BTreeMap::, usize>::new(); + for group_index in component { + let mut signature = groups[*group_index].sector_ids.clone(); + signature.sort_unstable(); + if by_sector_signature + .insert(signature, *group_index) + .is_some() + { + return None; + } + } + Some(by_sector_signature) + }) + .collect::>>()?; + let canonical_signatures = component_maps[0].keys().cloned().collect::>(); + if component_maps + .iter() + .skip(1) + .any(|component| component.keys().ne(canonical_signatures.iter())) + { + return None; + } + + let mut component_index_by_group = vec![usize::MAX; groups.len()]; + let mut local_index_by_group = vec![usize::MAX; groups.len()]; + let mut component_group_indices = Vec::with_capacity(groups.len()); + for (local_index, signature) in canonical_signatures.iter().enumerate() { + for (component_index, component) in component_maps.iter().enumerate() { + let group_index = component[signature]; + component_index_by_group[group_index] = component_index; + local_index_by_group[group_index] = local_index; + component_group_indices.push(group_index); + } + } + + let mut entries_by_component = + vec![Vec::::new(); components.len()]; + for entry in entries { + let component_index = component_index_by_group[entry.left_group_index]; + if component_index == usize::MAX + || component_index != component_index_by_group[entry.right_group_index] + { + return None; + } + entries_by_component[component_index].push(CanonicalColorContractionEntry { + left_group_index: local_index_by_group[entry.left_group_index], + right_group_index: local_index_by_group[entry.right_group_index], + weight_re_bits: entry.weight_re.to_bits(), + weight_im_bits: entry.weight_im.to_bits(), + symmetry_factor_bits: entry.symmetry_factor.to_bits(), + }); + } + for component_entries in &mut entries_by_component { + component_entries.sort_unstable(); + } + if entries_by_component + .iter() + .skip(1) + .any(|component_entries| component_entries != &entries_by_component[0]) + { + return None; + } + + let entries = entries_by_component[0] + .iter() + .map(|entry| ColorContractionEntry { + left_group_index: entry.left_group_index, + right_group_index: entry.right_group_index, + weight_re: f64::from_bits(entry.weight_re_bits), + weight_im: f64::from_bits(entry.weight_im_bits), + symmetry_factor: f64::from_bits(entry.symmetry_factor_bits), + }) + .collect::>(); + let singleton_output_indices = component_group_indices + .iter() + .map( + |group_index| match groups[*group_index].indices.as_slice() { + [output_index] => Some(*output_index), + _ => None, + }, + ) + .collect::>>(); + Some(RepeatedColorContractionBlock { + component_count: components.len(), + component_group_indices, + singleton_output_indices, + all_weights_real: entries.iter().all(|entry| entry.weight_im == 0.0), + entries, + walsh_block: None, + c2k_walsh_block: None, + }) +} + +#[derive(Clone, Copy, Debug, Default)] +struct EvaluatorBatchProfile { + leaf_input_pack_s: f64, + legacy_evaluator_call_s: f64, + evaluator_call_s: f64, + output_gather_s: f64, + leaf_input_copy_component_count: u64, + output_gather_component_count: u64, + backend_call_count: u64, + scratch_reallocation_count: u64, +} + +#[derive(Clone, Copy, Debug, Default)] +struct StageEvaluationProfile { + input_pack_s: f64, + evaluator: EvaluatorBatchProfile, + output_assign_s: f64, + input_copy_component_count: u64, + output_assign_component_count: u64, + scratch_reallocation_count: u64, +} + +#[derive(Clone, Copy, Debug, Default)] +struct AmplitudeEvaluationProfile { + input_pack_s: f64, + evaluator: EvaluatorBatchProfile, + output_remap_s: f64, + input_copy_component_count: u64, + output_remap_component_count: u64, + scratch_reallocation_count: u64, +} + +#[derive(Clone, Debug, Default)] +struct RuntimeProfile { + orchestration_s: f64, + state_prepare_s: f64, + state_clear_s: f64, + source_fill_s: f64, + momentum_input_setup_s: f64, + momentum_setup_s: f64, + model_parameter_setup_s: f64, + stage_input_pack_s: f64, + stage_leaf_input_pack_s: f64, + stage_evaluator_call_s: f64, + stage_backend_call_s: f64, + stage_evaluator_output_gather_s: f64, + stage_evaluator_s: f64, + output_assign_s: f64, + amplitude_input_pack_s: f64, + amplitude_leaf_input_pack_s: f64, + amplitude_evaluator_call_s: f64, + amplitude_backend_call_s: f64, + amplitude_evaluator_output_gather_s: f64, + amplitude_output_remap_s: f64, + amplitude_evaluator_s: f64, + reduction_s: f64, + resolved_reduction_materialization_s: f64, + total_materialization_s: f64, + final_output_copy_s: f64, + total_s: f64, + stage_input_pack_by_stage_s: Vec, + stage_leaf_input_pack_by_stage_s: Vec, + stage_evaluator_call_by_stage_s: Vec, + stage_backend_call_by_stage_s: Vec, + stage_evaluator_output_gather_by_stage_s: Vec, + stage_output_assign_by_stage_s: Vec, + state_component_count: u64, + state_clear_component_count: u64, + source_component_count: u64, + momentum_component_count: u64, + model_parameter_component_count: u64, + stage_input_copy_component_count: u64, + stage_leaf_input_copy_component_count: u64, + stage_evaluator_output_gather_component_count: u64, + stage_output_assign_component_count: u64, + amplitude_input_copy_component_count: u64, + amplitude_leaf_input_copy_component_count: u64, + amplitude_evaluator_output_gather_component_count: u64, + amplitude_output_remap_component_count: u64, + evaluator_backend_call_count: u64, + compiled_direct_arena_engine_count: u64, + compiled_direct_arena_call_count: u64, + compiled_direct_arena_minimum_effective_tile_capacity: u64, + compiled_direct_arena_maximum_physical_scalar_values_per_point: u64, + compiled_direct_arena_maximum_hot_scalar_values_per_point: u64, + compiled_direct_arena_maximum_source_scalar_values_per_point: u64, + compiled_direct_arena_maximum_reduction_scalar_values_per_point: u64, + compiled_direct_arena_boundary_input_bytes: u64, + compiled_direct_arena_boundary_current_output_bytes: u64, + compiled_direct_arena_boundary_amplitude_output_bytes: u64, + /// Broadcast-plane refresh traffic internal to compiled P-kernels. + compiled_direct_arena_internal_broadcast_bytes: u64, + reduction_input_component_count: u64, + resolved_materialized_component_count: u64, + total_materialized_value_count: u64, + final_output_copy_value_count: u64, + scratch_reallocation_count: u64, + eager_initialize_s: f64, + eager_gather_s: f64, + eager_kernel_call_s: f64, + eager_invocation_scatter_s: f64, + eager_finalization_s: f64, + eager_scatter_finalization_s: f64, + eager_closure_s: f64, + eager_reduction_s: f64, + eager_copy_out_s: f64, + /// P-kernel scratch reads and writes wholly inside eager Direct-Arena execution. + eager_internal_scratch_bytes: u64, + /// Point-independent coupling and parameter refreshes wholly inside eager + /// Direct-Arena execution. + eager_internal_broadcast_bytes: u64, + recurrence_momentum_fill_s: f64, + recurrence_union_source_fill_s: f64, + /// Inclusive top-level recurrence schedule envelope. The role-specific + /// recurrence fields below are internal attribution and must not be added + /// to top-level accounting. + recurrence_schedule_s: f64, + recurrence_source_kernel_s: f64, + recurrence_contribution_kernel_s: f64, + recurrence_finalization_s: f64, + recurrence_closure_s: f64, + recurrence_replay_output_mapping_s: f64, + recurrence_momentum_scalar_value_count: u64, + recurrence_schedule_execution_count: u64, + recurrence_replay_schedule_execution_count: u64, + recurrence_union_schedule_execution_count: u64, + recurrence_union_source_row_count: u64, + recurrence_replay_output_value_count: u64, + recurrence_source_call_count: u64, + recurrence_source_row_count: u64, + recurrence_contribution_call_count: u64, + recurrence_contribution_row_count: u64, + recurrence_finalization_call_count: u64, + recurrence_finalization_row_count: u64, + recurrence_closure_call_count: u64, + recurrence_closure_row_count: u64, + /// Legacy recurrence Direct-Arena packet counters. These remain private + /// because they authenticate the generated-kernel boundary rather than + /// the public diagnostic profiler's input/output materialization. + recurrence_direct_packed_input_bytes: u64, + recurrence_direct_packed_output_bytes: u64, + recurrence_direct_scatter_bytes: u64, + /// Lane-neutral Direct-Arena traffic counters observed by recurrence. + recurrence_direct_packet_input_bytes: u64, + recurrence_direct_packet_output_bytes: u64, + recurrence_direct_gather_bytes: u64, + recurrence_direct_traffic_scatter_bytes: u64, + recurrence_direct_remap_bytes: u64, + /// P-kernel output scratch traffic entirely inside recurrence execution. + recurrence_internal_scratch_bytes: u64, + /// P-kernel broadcast-plane traffic entirely inside recurrence execution. + recurrence_internal_broadcast_bytes: u64, +} + +// Saved SymJIT applications are native payloads and do not currently guarantee +// Rust's AArch64 callee-saved FP registers. Keep elapsed values integer-backed +// until the payload has returned, then use an opaque conversion boundary so +// LLVM cannot retain an f64 across that call. +#[inline(never)] +fn profile_duration_seconds(duration: Duration) -> f64 { + std::hint::black_box(duration).as_secs_f64() +} + +impl RuntimeProfile { + fn add_sector(&mut self, sector: &RuntimeProfile) { + self.orchestration_s += sector.orchestration_s; + self.state_prepare_s += sector.state_prepare_s; + self.state_clear_s += sector.state_clear_s; + self.source_fill_s += sector.source_fill_s; + self.momentum_input_setup_s += sector.momentum_input_setup_s; + self.momentum_setup_s += sector.momentum_setup_s; + self.model_parameter_setup_s += sector.model_parameter_setup_s; + self.stage_input_pack_s += sector.stage_input_pack_s; + self.stage_leaf_input_pack_s += sector.stage_leaf_input_pack_s; + self.stage_evaluator_call_s += sector.stage_evaluator_call_s; + self.stage_backend_call_s += sector.stage_backend_call_s; + self.stage_evaluator_output_gather_s += sector.stage_evaluator_output_gather_s; + self.stage_evaluator_s += sector.stage_evaluator_s; + self.output_assign_s += sector.output_assign_s; + self.amplitude_input_pack_s += sector.amplitude_input_pack_s; + self.amplitude_leaf_input_pack_s += sector.amplitude_leaf_input_pack_s; + self.amplitude_evaluator_call_s += sector.amplitude_evaluator_call_s; + self.amplitude_backend_call_s += sector.amplitude_backend_call_s; + self.amplitude_evaluator_output_gather_s += sector.amplitude_evaluator_output_gather_s; + self.amplitude_output_remap_s += sector.amplitude_output_remap_s; + self.amplitude_evaluator_s += sector.amplitude_evaluator_s; + self.reduction_s += sector.reduction_s; + self.resolved_reduction_materialization_s += sector.resolved_reduction_materialization_s; + self.total_materialization_s += sector.total_materialization_s; + self.final_output_copy_s += sector.final_output_copy_s; + self.state_component_count += sector.state_component_count; + self.state_clear_component_count += sector.state_clear_component_count; + self.source_component_count += sector.source_component_count; + self.momentum_component_count += sector.momentum_component_count; + self.model_parameter_component_count += sector.model_parameter_component_count; + self.stage_input_copy_component_count += sector.stage_input_copy_component_count; + self.stage_leaf_input_copy_component_count += sector.stage_leaf_input_copy_component_count; + self.stage_evaluator_output_gather_component_count += + sector.stage_evaluator_output_gather_component_count; + self.stage_output_assign_component_count += sector.stage_output_assign_component_count; + self.amplitude_input_copy_component_count += sector.amplitude_input_copy_component_count; + self.amplitude_leaf_input_copy_component_count += + sector.amplitude_leaf_input_copy_component_count; + self.amplitude_evaluator_output_gather_component_count += + sector.amplitude_evaluator_output_gather_component_count; + self.amplitude_output_remap_component_count += + sector.amplitude_output_remap_component_count; + self.evaluator_backend_call_count += sector.evaluator_backend_call_count; + self.compiled_direct_arena_engine_count += sector.compiled_direct_arena_engine_count; + self.compiled_direct_arena_call_count += sector.compiled_direct_arena_call_count; + self.compiled_direct_arena_minimum_effective_tile_capacity = match ( + self.compiled_direct_arena_minimum_effective_tile_capacity, + sector.compiled_direct_arena_minimum_effective_tile_capacity, + ) { + (0, value) | (value, 0) => value, + (left, right) => left.min(right), + }; + self.compiled_direct_arena_maximum_physical_scalar_values_per_point = self + .compiled_direct_arena_maximum_physical_scalar_values_per_point + .max(sector.compiled_direct_arena_maximum_physical_scalar_values_per_point); + self.compiled_direct_arena_maximum_hot_scalar_values_per_point = self + .compiled_direct_arena_maximum_hot_scalar_values_per_point + .max(sector.compiled_direct_arena_maximum_hot_scalar_values_per_point); + self.compiled_direct_arena_maximum_source_scalar_values_per_point = self + .compiled_direct_arena_maximum_source_scalar_values_per_point + .max(sector.compiled_direct_arena_maximum_source_scalar_values_per_point); + self.compiled_direct_arena_maximum_reduction_scalar_values_per_point = self + .compiled_direct_arena_maximum_reduction_scalar_values_per_point + .max(sector.compiled_direct_arena_maximum_reduction_scalar_values_per_point); + self.compiled_direct_arena_boundary_input_bytes += + sector.compiled_direct_arena_boundary_input_bytes; + self.compiled_direct_arena_boundary_current_output_bytes += + sector.compiled_direct_arena_boundary_current_output_bytes; + self.compiled_direct_arena_boundary_amplitude_output_bytes += + sector.compiled_direct_arena_boundary_amplitude_output_bytes; + self.compiled_direct_arena_internal_broadcast_bytes += + sector.compiled_direct_arena_internal_broadcast_bytes; + self.reduction_input_component_count += sector.reduction_input_component_count; + self.resolved_materialized_component_count += sector.resolved_materialized_component_count; + self.total_materialized_value_count += sector.total_materialized_value_count; + self.final_output_copy_value_count += sector.final_output_copy_value_count; + self.scratch_reallocation_count += sector.scratch_reallocation_count; + self.eager_initialize_s += sector.eager_initialize_s; + self.eager_gather_s += sector.eager_gather_s; + self.eager_kernel_call_s += sector.eager_kernel_call_s; + self.eager_invocation_scatter_s += sector.eager_invocation_scatter_s; + self.eager_finalization_s += sector.eager_finalization_s; + self.eager_scatter_finalization_s += sector.eager_scatter_finalization_s; + self.eager_closure_s += sector.eager_closure_s; + self.eager_reduction_s += sector.eager_reduction_s; + self.eager_copy_out_s += sector.eager_copy_out_s; + self.eager_internal_scratch_bytes += sector.eager_internal_scratch_bytes; + self.eager_internal_broadcast_bytes += sector.eager_internal_broadcast_bytes; + self.recurrence_momentum_fill_s += sector.recurrence_momentum_fill_s; + self.recurrence_union_source_fill_s += sector.recurrence_union_source_fill_s; + self.recurrence_schedule_s += sector.recurrence_schedule_s; + self.recurrence_source_kernel_s += sector.recurrence_source_kernel_s; + self.recurrence_contribution_kernel_s += sector.recurrence_contribution_kernel_s; + self.recurrence_finalization_s += sector.recurrence_finalization_s; + self.recurrence_closure_s += sector.recurrence_closure_s; + self.recurrence_replay_output_mapping_s += sector.recurrence_replay_output_mapping_s; + self.recurrence_momentum_scalar_value_count += + sector.recurrence_momentum_scalar_value_count; + self.recurrence_schedule_execution_count += sector.recurrence_schedule_execution_count; + self.recurrence_replay_schedule_execution_count += + sector.recurrence_replay_schedule_execution_count; + self.recurrence_union_schedule_execution_count += + sector.recurrence_union_schedule_execution_count; + self.recurrence_union_source_row_count += sector.recurrence_union_source_row_count; + self.recurrence_replay_output_value_count += sector.recurrence_replay_output_value_count; + self.recurrence_source_call_count += sector.recurrence_source_call_count; + self.recurrence_source_row_count += sector.recurrence_source_row_count; + self.recurrence_contribution_call_count += sector.recurrence_contribution_call_count; + self.recurrence_contribution_row_count += sector.recurrence_contribution_row_count; + self.recurrence_finalization_call_count += sector.recurrence_finalization_call_count; + self.recurrence_finalization_row_count += sector.recurrence_finalization_row_count; + self.recurrence_closure_call_count += sector.recurrence_closure_call_count; + self.recurrence_closure_row_count += sector.recurrence_closure_row_count; + self.recurrence_direct_packed_input_bytes += sector.recurrence_direct_packed_input_bytes; + self.recurrence_direct_packed_output_bytes += sector.recurrence_direct_packed_output_bytes; + self.recurrence_direct_scatter_bytes += sector.recurrence_direct_scatter_bytes; + self.recurrence_direct_packet_input_bytes += sector.recurrence_direct_packet_input_bytes; + self.recurrence_direct_packet_output_bytes += sector.recurrence_direct_packet_output_bytes; + self.recurrence_direct_gather_bytes += sector.recurrence_direct_gather_bytes; + self.recurrence_direct_traffic_scatter_bytes += + sector.recurrence_direct_traffic_scatter_bytes; + self.recurrence_direct_remap_bytes += sector.recurrence_direct_remap_bytes; + self.recurrence_internal_scratch_bytes += sector.recurrence_internal_scratch_bytes; + self.recurrence_internal_broadcast_bytes += sector.recurrence_internal_broadcast_bytes; + add_profile_vector( + &mut self.stage_input_pack_by_stage_s, + §or.stage_input_pack_by_stage_s, + ); + add_profile_vector( + &mut self.stage_leaf_input_pack_by_stage_s, + §or.stage_leaf_input_pack_by_stage_s, + ); + add_profile_vector( + &mut self.stage_evaluator_call_by_stage_s, + §or.stage_evaluator_call_by_stage_s, + ); + add_profile_vector( + &mut self.stage_backend_call_by_stage_s, + §or.stage_backend_call_by_stage_s, + ); + add_profile_vector( + &mut self.stage_evaluator_output_gather_by_stage_s, + §or.stage_evaluator_output_gather_by_stage_s, + ); + add_profile_vector( + &mut self.stage_output_assign_by_stage_s, + §or.stage_output_assign_by_stage_s, + ); + } + + fn validate_recurrence_direct_boundary_traffic(&self) -> RusticolResult<()> { + if self.recurrence_direct_packed_input_bytes + | self.recurrence_direct_packed_output_bytes + | self.recurrence_direct_scatter_bytes + != 0 + { + return Err(RusticolError::integrity( + "recurrence Direct-Arena execution recorded legacy pack/scatter boundary traffic", + )); + } + if self.recurrence_direct_packet_input_bytes + | self.recurrence_direct_packet_output_bytes + | self.recurrence_direct_gather_bytes + | self.recurrence_direct_traffic_scatter_bytes + | self.recurrence_direct_remap_bytes + != 0 + { + return Err(RusticolError::integrity( + "recurrence Direct-Arena execution recorded packet/gather/scatter/remap traffic", + )); + } + Ok(()) + } +} + +fn add_profile_vector(target: &mut Vec, source: &[f64]) { + if target.len() < source.len() { + target.resize(source.len(), 0.0); + } + for (index, value) in source.iter().enumerate() { + target[index] += value; + } +} + +struct ExecutionRuntime { + process: String, + key: String, + color_accuracy: String, + external_pdg_order: Vec, + external_count: usize, + parameter_count: usize, + value_parameter_count: usize, + momentum_parameter_count: usize, + current_count: usize, + source_count: usize, + interaction_count: usize, + stage_count: usize, + amplitude_output_count: usize, + lc_topology_replay_enabled: bool, + lc_topology_replay_mappings: Arc, + lc_topology_replay_public_mappings: LcTopologyReplayMappings, + lc_topology_replay_routes: Vec>, + lc_topology_replay_materialized_sector_ids: BTreeSet, + lc_resolved_replay_plan: Option>, + lc_resolved_replay_selection_cache: + Option<(LcResolvedReplaySelectionKey, Arc)>, + lc_replay_flat_momenta_scratch: Vec, + lc_replay_target_components_scratch: Vec, + color_topology_replay_enabled: bool, + color_topology_replay_mappings: Arc, + color_replay_flat_momenta_scratch: Vec, + #[allow(dead_code)] // Loaded now and consumed by the subsequent selector-execution milestone. + helicity_recurrence: Option, + compiled_helicity_execution_plan: Option, + compiled_color_execution_plan: Option, + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + compiled_direct_runtime: Option, + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + compiled_direct_color_schedules: + BTreeMap, + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + compiled_direct_helicity_schedules: + BTreeMap, + helicity_sum_runtime: Option>, + // Lane runtimes are large recursive owners; boxing keeps their addresses stable and avoids + // moving them when this selector index grows. + #[allow(clippy::vec_box)] + helicity_selector_runtimes: Vec>, + helicity_selector_runtime_schedule_modes: Vec, + helicity_selector_lane_by_domain: BTreeMap, + color_selector_runtimes: BTreeMap>, + runtime_unavailable_message: Option, + sources: Vec, + // Iterable fill list, not indexed by canonical momentum-slot ID. Compiled + // lanes retain only generated live slots, with their global offsets intact. + momentum_slots: Vec, + external_is_initial: Vec, + particle_masses: BTreeMap, + particle_mass_parameter_names: BTreeMap, + normalization_factor: f64, + normalization_color_factor: f64, + normalization_average_factor: f64, + normalization_identical_factor: f64, + normalization_qcd_coupling_power: usize, + normalization_electroweak_coupling_power: usize, + model_parameters: Vec, + model_parameter_name_to_index: BTreeMap, + model_parameter_runtime_slots: BTreeMap, + model_parameter_values_f64: Vec, + model_parameter_evaluator: Option, + physics_reduction_override: Option, + physics: Option>, + stages: Option>, + amplitude_stage: Option, + state_scratch_f64: Vec>, + state_scratch_f64_requires_clear: bool, + values_scratch_f64: Vec, +} + +#[derive(Clone)] +struct PhysicsRuntime { + binding_id: u64, + manifest: ProcessPhysicsV1, + helicity_index_by_id: BTreeMap, + helicity_members_by_representative: Vec>, + color_index_by_id: BTreeMap, + reduction_by_group_id: BTreeMap, + numeric_reduction_by_group_id: BTreeMap, +} + +#[derive(Clone)] +struct NumericReductionGroup { + physical_helicity_indices: Vec, + helicity_membership: Vec, + normalized_helicity_weights: Vec<(usize, f64)>, + normalized_color_weights: Vec<(usize, f64)>, + normalized_member_weights: Vec<(usize, usize, f64)>, +} + +impl NumericReductionGroup { + #[inline(always)] + fn contains_helicity(&self, helicity_index: usize) -> bool { + self.helicity_membership + .get(helicity_index) + .copied() + .unwrap_or(false) + } +} + +#[derive(Clone, Debug)] +struct ResolvedValues { + values: Vec, + point_count: usize, + helicity_indices: Vec, + color_indices: Vec, +} + +#[derive(Clone, Debug, Serialize)] +pub struct NativeRuntimeMetadata { + pub abi_version: u32, + pub schema_version: u32, + pub execution_mode: String, + pub prepared_backend: Option, + pub eager_effective_point_tile_size: Option, + pub eager_workspace_bytes: Option, + pub on_the_fly_requested_query_construction_threads: Option, + pub on_the_fly_effective_query_construction_threads: Option, + pub compiled_direct_minimum_effective_tile_capacity: Option, + pub compiled_direct_maximum_physical_scalar_values_per_point: Option, + pub compiled_direct_maximum_hot_scalar_values_per_point: Option, + pub compiled_direct_maximum_source_scalar_values_per_point: Option, + pub compiled_direct_maximum_reduction_scalar_values_per_point: Option, + pub process: String, + pub process_key: String, + pub representative_process: String, + pub representative_process_key: String, + /// Full representative-index to public-index external permutation. + pub external_permutation: Vec, + /// Representative process key when a stored or inferred permutation is active. + pub permutation_alias_of: Option, + pub final_state_permutation_alias_of: Option, + /// Representative process key when the selected process was not generated + /// and is routed through the artifact's process-group map. + pub process_group_member_of: Option, + /// Exact factor applied to every representative matrix element. + pub process_group_factor: NativeRationalFactor, + /// Whether the selected process is the charge conjugate of its + /// representative; public LC color-flow words are then reversed. + pub charge_conjugated: bool, + pub color_accuracy: String, + pub external_pdg_order: Vec, + pub external_count: usize, + pub current_count: usize, + pub source_count: usize, + pub interaction_count: usize, + pub stage_count: usize, + pub amplitude_output_count: usize, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)] +pub struct NativeRationalFactor { + pub numerator: u64, + pub denominator: u64, +} + +/// Public relabelling of a process-group member evaluated by its representative. +#[derive(Clone, Debug)] +struct ProcessGroupRouting { + representative_process_key: String, + factor: NativeRationalFactor, + conjugated: bool, +} + +/// Prepared batch-global selectors for allocation-free recurrence evaluation. +/// +/// Selector resolution and validation may allocate while this handle is +/// created. Reusing it with +/// [`NativeRuntime::evaluate_f64_into_with_recurrence_selector_plan`] borrows +/// the retained selector sets and caller-owned input/output storage. +#[derive(Clone, Debug)] +pub struct NativeRecurrenceSelectorPlan { + artifact_root: PathBuf, + process_key: String, + external_permutation: Vec, + selected_helicities: Option>, + selected_colors: Option>, +} + +impl NativeRecurrenceSelectorPlan { + fn ensure_matches(&self, runtime: &NativeRuntime) -> RusticolResult<()> { + if self.artifact_root != runtime.root + || self.process_key != runtime.process_key + || self.external_permutation != runtime.external_permutation + { + return Err(RusticolError::selector( + "recurrence selector plan belongs to a different artifact, process, or external ordering", + )); + } + Ok(()) + } +} + +/// Exact-required sections decoded lazily from one authenticated eager plan-v3 +/// artifact. This bridge is intentionally private to pyAmpliCol's Python exact +/// executor; f64 execution never constructs it. +#[derive(Debug)] +pub struct NativeEagerExactSections { + pub process_id: String, + pub exact_schema: Value, + pub reduction_groups: Value, + pub selector_group_ids: Vec, + pub selector_domains: Vec>, + pub couplings: Vec, + pub stages: Vec, + pub invocations: Vec, + pub attachments: Vec, + pub finalizations: Vec, + pub closures: Vec, +} + +#[derive(Debug)] +pub struct NativeEagerExactCoupling { + pub real_parameter_id: u32, + pub imaginary_parameter_id: u32, + pub constant_real: String, + pub constant_imaginary: String, +} + +#[derive(Clone, Copy, Debug)] +pub struct NativeEagerExactStage { + pub stage_index: u32, + pub invocation_start: u64, + pub invocation_count: u64, + pub attachment_start: u64, + pub attachment_count: u64, + pub finalization_start: u64, + pub finalization_count: u64, +} + +#[derive(Clone, Copy, Debug)] +pub struct NativeEagerExactInvocation { + pub kernel_id: u32, + pub left_value_slot_id: u32, + pub right_value_slot_id: u32, + pub left_momentum_slot_id: u32, + pub right_momentum_slot_id: u32, + pub coupling_slot_id: u32, + pub output_factor_source: u8, + pub attachment_start: u64, + pub attachment_count: u64, + pub selector_domain_id: u32, +} + +#[derive(Debug)] +pub struct NativeEagerExactAttachment { + pub result_current_id: u32, + pub factor_numerators: Vec<[String; 2]>, + pub factor_denominator: Option<[String; 2]>, + pub selector_domain_id: u32, +} + +#[derive(Clone, Copy, Debug)] +pub struct NativeEagerExactFinalization { + pub kernel_id: u32, + pub current_id: u32, + pub unpropagated_value_slot_id: u32, + pub propagated_value_slot_id: u32, + pub momentum_slot_id: u32, + pub unpropagated_selector_domain_id: u32, + pub propagated_selector_domain_id: u32, +} + +#[derive(Debug)] +pub struct NativeEagerExactClosure { + pub kernel_id: u32, + pub left_value_slot_id: u32, + pub right_value_slot_id: u32, + pub amplitude_index: u32, + pub coupling_slot_id: u32, + pub output_factor_source: u8, + pub factor_numerators: Vec<[String; 2]>, + pub factor_denominator: Option<[String; 2]>, + pub direct_coefficients: Option>, + pub coherent_group_id: u32, + pub selector_domain_id: u32, +} + +/// Exact-required sections decoded from one authenticated recurrence plan-v2 +/// artifact. Python consumes these immutable rows with the retained exact +/// evaluator payloads; the Direct-Arena f64 runtime never constructs them. +#[derive(Debug)] +pub struct NativeRecurrenceExactSections { + pub process_id: String, + pub strategy: String, + pub semantic_digest: String, + pub runtime_layout_digest: String, + pub current_arena_components: u32, + pub amplitude_destination_count: u32, + pub parameter_value_count: u32, + pub external_source_count: u32, + pub currents: Vec, + pub sources: Vec, + pub contributions: Vec, + pub finalizations: Vec, + pub closures: Vec, + pub row_groups: Vec, + pub momentum_forms: Vec, + pub momentum_terms: Vec, + pub replay_targets: Vec, + pub source_permutations: Vec, + pub replay_momentum_signs: Vec, + pub replay_helicity_map: Vec, + pub amplitude_destinations: Vec, + pub resolved_helicities: Vec, + pub public_helicities: Vec, + pub source_state_assignments: Vec, + pub source_dispatch_variants: Vec, + pub source_embeddings: Vec, + pub source_projections: Vec, + pub resolved_source_selections: Vec, + pub exact_factors: Vec, + pub public_flow_ids: Vec, + pub executors: Vec, +} + +#[derive(Debug)] +pub struct NativeRecurrenceExactFactor { + pub real_numerator: String, + pub real_denominator: String, + pub imaginary_numerator: String, + pub imaginary_denominator: String, +} + +#[derive(Clone, Debug)] +pub struct NativeRecurrenceExactExecutor { + pub direct_executor_id: u32, + pub role: String, + pub destination_operation: String, + pub parent_component_counts: Vec, + pub destination_component_count: u32, + pub momentum_operand_count: u32, + pub prepared_kernel_id: Option, + pub runtime_template: Option, +} + +#[derive(Clone, Debug, Serialize)] +pub struct NativeRuntimeProfile { + pub native_input_pack_s: f64, + pub native_input_crossing_s: f64, + pub orchestration_s: f64, + pub state_prepare_s: f64, + pub state_clear_s: f64, + pub source_fill_s: f64, + /// Exclusive momentum-input setup. Unlike `momentum_setup_s`, this does + /// not include model-parameter setup. + pub momentum_input_setup_s: f64, + /// Backward-compatible aggregate of momentum-input and model-parameter + /// setup. + pub momentum_setup_s: f64, + pub model_parameter_setup_s: f64, + /// Top-level stage input envelope. In composed selected-chunk paths this + /// owns the leaf gather; in full-stage paths it owns only the parent + /// stage-input gather. + pub stage_input_pack_s: f64, + /// Internal attribution. This is owned by `stage_input_pack_s` for + /// composed selected-chunk paths and by `stage_evaluator_call_s` for + /// full-stage paths, so it must not be added to the top-level sum. + pub stage_leaf_input_pack_s: f64, + /// Top-level evaluator envelope. This includes leaf gathering for + /// full-stage paths and excludes it for composed selected-chunk paths. + pub stage_evaluator_call_s: f64, + pub stage_backend_call_s: f64, + pub stage_evaluator_output_gather_s: f64, + pub stage_evaluator_s: f64, + pub output_assign_s: f64, + /// Top-level amplitude input envelope. In composed selected-chunk paths + /// this owns the amplitude leaf gather. + pub amplitude_input_pack_s: f64, + /// Internal attribution owned either by `amplitude_input_pack_s` or + /// `amplitude_evaluator_call_s`; never an additional top-level phase. + pub amplitude_leaf_input_pack_s: f64, + /// Top-level amplitude evaluator envelope. Full-stage paths include leaf + /// gathering; composed selected-chunk paths exclude it. + pub amplitude_evaluator_call_s: f64, + pub amplitude_backend_call_s: f64, + pub amplitude_evaluator_output_gather_s: f64, + pub amplitude_output_remap_s: f64, + pub amplitude_evaluator_s: f64, + pub reduction_s: f64, + /// Inclusive attribution: the resolved-result construction occurs inside + /// `reduction_s` and must not be added to exclusive top-level phases. + pub resolved_reduction_materialization_s: f64, + pub total_materialization_s: f64, + pub final_output_copy_s: f64, + pub total_s: f64, + pub stage_input_pack_by_stage_s: Vec, + pub stage_leaf_input_pack_by_stage_s: Vec, + pub stage_evaluator_call_by_stage_s: Vec, + pub stage_backend_call_by_stage_s: Vec, + pub stage_evaluator_output_gather_by_stage_s: Vec, + pub stage_output_assign_by_stage_s: Vec, + pub eager_initialize_s: f64, + pub eager_gather_s: f64, + pub eager_kernel_call_s: f64, + pub eager_invocation_scatter_s: f64, + pub eager_finalization_s: f64, + pub eager_scatter_finalization_s: f64, + pub eager_closure_s: f64, + pub eager_reduction_s: f64, + pub eager_copy_out_s: f64, + pub recurrence_momentum_fill_s: f64, + pub recurrence_union_source_fill_s: f64, + /// Inclusive top-level recurrence schedule envelope. + pub recurrence_schedule_s: f64, + /// Internal attribution owned by `recurrence_schedule_s`. + pub recurrence_source_kernel_s: f64, + /// Internal attribution owned by `recurrence_schedule_s`. + pub recurrence_contribution_kernel_s: f64, + /// Internal attribution owned by `recurrence_schedule_s`. + pub recurrence_finalization_s: f64, + /// Internal attribution owned by `recurrence_schedule_s`. + pub recurrence_closure_s: f64, + pub recurrence_replay_output_mapping_s: f64, + pub recurrence_momentum_scalar_value_count: u64, + pub recurrence_schedule_execution_count: u64, + pub recurrence_replay_schedule_execution_count: u64, + pub recurrence_union_schedule_execution_count: u64, + pub recurrence_union_source_row_count: u64, + pub recurrence_replay_output_value_count: u64, + pub recurrence_source_call_count: u64, + pub recurrence_source_row_count: u64, + pub recurrence_contribution_call_count: u64, + pub recurrence_contribution_row_count: u64, + pub recurrence_finalization_call_count: u64, + pub recurrence_finalization_row_count: u64, + pub recurrence_closure_call_count: u64, + pub recurrence_closure_row_count: u64, + pub selector_planner_s: f64, + pub selector_gather_s: f64, + pub selector_scatter_s: f64, + pub selector_plan_kind: String, + pub selector_group_sizes: Vec, + pub selector_reordered_point_count: usize, + pub selector_simd_lane_width: usize, + pub selector_simd_occupancy: f64, + pub native_input_component_count: u64, + pub native_input_pack_bytes: u64, + pub native_input_crossing_bytes: u64, + /// Explicit nested native-input containers allocated for this call. + pub native_input_container_allocation_count: u64, + pub state_component_count: u64, + pub state_clear_component_count: u64, + pub source_component_count: u64, + pub momentum_component_count: u64, + pub model_parameter_component_count: u64, + pub stage_input_copy_component_count: u64, + pub stage_leaf_input_copy_component_count: u64, + pub stage_evaluator_output_gather_component_count: u64, + pub stage_output_assign_component_count: u64, + pub amplitude_input_copy_component_count: u64, + pub amplitude_leaf_input_copy_component_count: u64, + pub amplitude_evaluator_output_gather_component_count: u64, + pub amplitude_output_remap_component_count: u64, + pub evaluator_backend_call_count: u64, + /// Number of compiled Direct-Arena engines authenticated for this profile. + pub compiled_direct_arena_engine_count: u64, + /// Calls observed directly on compiled Direct-Arena evaluator leaves. + pub compiled_direct_arena_call_count: u64, + /// Minimum effective tile across the compiled Direct engine fleet. + pub compiled_direct_arena_minimum_effective_tile_capacity: u64, + /// Maximum authenticated physical scalar-plane footprint per point. + pub compiled_direct_arena_maximum_physical_scalar_values_per_point: u64, + /// Maximum phase-local cache footprint per point. + pub compiled_direct_arena_maximum_hot_scalar_values_per_point: u64, + pub compiled_direct_arena_maximum_source_scalar_values_per_point: u64, + pub compiled_direct_arena_maximum_reduction_scalar_values_per_point: u64, + /// Developer-adapter traffic is forbidden in the production Arena lane. + pub compiled_direct_arena_boundary_input_bytes: u64, + pub compiled_direct_arena_boundary_current_output_bytes: u64, + pub compiled_direct_arena_boundary_amplitude_output_bytes: u64, + pub reduction_input_component_count: u64, + pub selector_gather_point_count: u64, + pub selector_gather_bytes: u64, + pub selector_scatter_value_count: u64, + pub resolved_materialized_component_count: u64, + pub total_materialized_value_count: u64, + pub final_output_copy_value_count: u64, + /// Capacity-changing reallocations observed in instrumented reusable hot + /// buffers. This is intentionally not a process-wide allocation count. + pub observed_scratch_reallocation_count: u64, + /// Explicit final native output vector allocated for this call. + pub native_output_allocation_count: u64, +} + +impl From for NativeRuntimeProfile { + fn from(profile: RuntimeProfile) -> Self { + Self { + native_input_pack_s: 0.0, + native_input_crossing_s: 0.0, + orchestration_s: profile.orchestration_s, + state_prepare_s: profile.state_prepare_s, + state_clear_s: profile.state_clear_s, + source_fill_s: profile.source_fill_s, + momentum_input_setup_s: profile.momentum_input_setup_s, + momentum_setup_s: profile.momentum_setup_s, + model_parameter_setup_s: profile.model_parameter_setup_s, + stage_input_pack_s: profile.stage_input_pack_s, + stage_leaf_input_pack_s: profile.stage_leaf_input_pack_s, + stage_evaluator_call_s: profile.stage_evaluator_call_s, + stage_backend_call_s: profile.stage_backend_call_s, + stage_evaluator_output_gather_s: profile.stage_evaluator_output_gather_s, + stage_evaluator_s: profile.stage_evaluator_s, + output_assign_s: profile.output_assign_s, + amplitude_input_pack_s: profile.amplitude_input_pack_s, + amplitude_leaf_input_pack_s: profile.amplitude_leaf_input_pack_s, + amplitude_evaluator_call_s: profile.amplitude_evaluator_call_s, + amplitude_backend_call_s: profile.amplitude_backend_call_s, + amplitude_evaluator_output_gather_s: profile.amplitude_evaluator_output_gather_s, + amplitude_output_remap_s: profile.amplitude_output_remap_s, + amplitude_evaluator_s: profile.amplitude_evaluator_s, + reduction_s: profile.reduction_s, + resolved_reduction_materialization_s: profile.resolved_reduction_materialization_s, + total_materialization_s: profile.total_materialization_s, + final_output_copy_s: profile.final_output_copy_s, + total_s: profile.total_s, + stage_input_pack_by_stage_s: profile.stage_input_pack_by_stage_s, + stage_leaf_input_pack_by_stage_s: profile.stage_leaf_input_pack_by_stage_s, + stage_evaluator_call_by_stage_s: profile.stage_evaluator_call_by_stage_s, + stage_backend_call_by_stage_s: profile.stage_backend_call_by_stage_s, + stage_evaluator_output_gather_by_stage_s: profile + .stage_evaluator_output_gather_by_stage_s, + stage_output_assign_by_stage_s: profile.stage_output_assign_by_stage_s, + eager_initialize_s: profile.eager_initialize_s, + eager_gather_s: profile.eager_gather_s, + eager_kernel_call_s: profile.eager_kernel_call_s, + eager_invocation_scatter_s: profile.eager_invocation_scatter_s, + eager_finalization_s: profile.eager_finalization_s, + eager_scatter_finalization_s: profile.eager_scatter_finalization_s, + eager_closure_s: profile.eager_closure_s, + eager_reduction_s: profile.eager_reduction_s, + eager_copy_out_s: profile.eager_copy_out_s, + recurrence_momentum_fill_s: profile.recurrence_momentum_fill_s, + recurrence_union_source_fill_s: profile.recurrence_union_source_fill_s, + recurrence_schedule_s: profile.recurrence_schedule_s, + recurrence_source_kernel_s: profile.recurrence_source_kernel_s, + recurrence_contribution_kernel_s: profile.recurrence_contribution_kernel_s, + recurrence_finalization_s: profile.recurrence_finalization_s, + recurrence_closure_s: profile.recurrence_closure_s, + recurrence_replay_output_mapping_s: profile.recurrence_replay_output_mapping_s, + recurrence_momentum_scalar_value_count: profile.recurrence_momentum_scalar_value_count, + recurrence_schedule_execution_count: profile.recurrence_schedule_execution_count, + recurrence_replay_schedule_execution_count: profile + .recurrence_replay_schedule_execution_count, + recurrence_union_schedule_execution_count: profile + .recurrence_union_schedule_execution_count, + recurrence_union_source_row_count: profile.recurrence_union_source_row_count, + recurrence_replay_output_value_count: profile.recurrence_replay_output_value_count, + recurrence_source_call_count: profile.recurrence_source_call_count, + recurrence_source_row_count: profile.recurrence_source_row_count, + recurrence_contribution_call_count: profile.recurrence_contribution_call_count, + recurrence_contribution_row_count: profile.recurrence_contribution_row_count, + recurrence_finalization_call_count: profile.recurrence_finalization_call_count, + recurrence_finalization_row_count: profile.recurrence_finalization_row_count, + recurrence_closure_call_count: profile.recurrence_closure_call_count, + recurrence_closure_row_count: profile.recurrence_closure_row_count, + selector_planner_s: 0.0, + selector_gather_s: 0.0, + selector_scatter_s: 0.0, + selector_plan_kind: "none".to_string(), + selector_group_sizes: Vec::new(), + selector_reordered_point_count: 0, + selector_simd_lane_width: 1, + selector_simd_occupancy: 1.0, + native_input_component_count: 0, + native_input_pack_bytes: 0, + native_input_crossing_bytes: 0, + native_input_container_allocation_count: 0, + state_component_count: profile.state_component_count, + state_clear_component_count: profile.state_clear_component_count, + source_component_count: profile.source_component_count, + momentum_component_count: profile.momentum_component_count, + model_parameter_component_count: profile.model_parameter_component_count, + stage_input_copy_component_count: profile.stage_input_copy_component_count, + stage_leaf_input_copy_component_count: profile.stage_leaf_input_copy_component_count, + stage_evaluator_output_gather_component_count: profile + .stage_evaluator_output_gather_component_count, + stage_output_assign_component_count: profile.stage_output_assign_component_count, + amplitude_input_copy_component_count: profile.amplitude_input_copy_component_count, + amplitude_leaf_input_copy_component_count: profile + .amplitude_leaf_input_copy_component_count, + amplitude_evaluator_output_gather_component_count: profile + .amplitude_evaluator_output_gather_component_count, + amplitude_output_remap_component_count: profile.amplitude_output_remap_component_count, + evaluator_backend_call_count: profile.evaluator_backend_call_count, + compiled_direct_arena_engine_count: profile.compiled_direct_arena_engine_count, + compiled_direct_arena_call_count: profile.compiled_direct_arena_call_count, + compiled_direct_arena_minimum_effective_tile_capacity: profile + .compiled_direct_arena_minimum_effective_tile_capacity, + compiled_direct_arena_maximum_physical_scalar_values_per_point: profile + .compiled_direct_arena_maximum_physical_scalar_values_per_point, + compiled_direct_arena_maximum_hot_scalar_values_per_point: profile + .compiled_direct_arena_maximum_hot_scalar_values_per_point, + compiled_direct_arena_maximum_source_scalar_values_per_point: profile + .compiled_direct_arena_maximum_source_scalar_values_per_point, + compiled_direct_arena_maximum_reduction_scalar_values_per_point: profile + .compiled_direct_arena_maximum_reduction_scalar_values_per_point, + compiled_direct_arena_boundary_input_bytes: profile + .compiled_direct_arena_boundary_input_bytes, + compiled_direct_arena_boundary_current_output_bytes: profile + .compiled_direct_arena_boundary_current_output_bytes, + compiled_direct_arena_boundary_amplitude_output_bytes: profile + .compiled_direct_arena_boundary_amplitude_output_bytes, + reduction_input_component_count: profile.reduction_input_component_count, + selector_gather_point_count: 0, + selector_gather_bytes: 0, + selector_scatter_value_count: 0, + resolved_materialized_component_count: profile.resolved_materialized_component_count, + total_materialized_value_count: profile.total_materialized_value_count, + final_output_copy_value_count: profile.final_output_copy_value_count, + observed_scratch_reallocation_count: profile.scratch_reallocation_count, + native_output_allocation_count: 0, + } + } +} + +impl NativeRuntimeProfile { + fn validate_eager_top_level_accounting(&self) -> RusticolResult<()> { + self.validate_top_level_accounting(true) + } + + fn validate_compiled_top_level_accounting(&self) -> RusticolResult<()> { + self.validate_top_level_accounting(false) + } + + fn validate_recurrence_top_level_accounting(&self) -> RusticolResult<()> { + let phases = [ + ("native input pack", self.native_input_pack_s), + ("native input crossing", self.native_input_crossing_s), + ("runtime orchestration", self.orchestration_s), + ("state preparation", self.state_prepare_s), + ("state clearing", self.state_clear_s), + ("source preparation", self.source_fill_s), + ("external momentum flatten", self.momentum_input_setup_s), + ("model parameter setup", self.model_parameter_setup_s), + ( + "recurrence momentum-form fill", + self.recurrence_momentum_fill_s, + ), + ( + "recurrence union source fill", + self.recurrence_union_source_fill_s, + ), + ("inclusive recurrence schedule", self.recurrence_schedule_s), + ( + "recurrence replay output mapping", + self.recurrence_replay_output_mapping_s, + ), + ("reduction", self.reduction_s), + ("total materialization", self.total_materialization_s), + ("final output copy", self.final_output_copy_s), + ("selector planning", self.selector_planner_s), + ("selector gather", self.selector_gather_s), + ("selector scatter", self.selector_scatter_s), + ]; + self.validate_exclusive_phases(&phases)?; + + let schedule_attribution = [ + ("source kernels", self.recurrence_source_kernel_s), + ( + "contribution kernels", + self.recurrence_contribution_kernel_s, + ), + ("current finalization", self.recurrence_finalization_s), + ("amplitude closure", self.recurrence_closure_s), + ]; + for (label, value) in &schedule_attribution { + if !value.is_finite() || *value < 0.0 { + return Err(RusticolError::internal(format!( + "native recurrence profile has invalid {label} time {value:.9e}s" + ))); + } + } + let attributed = schedule_attribution + .iter() + .map(|(_, value)| value) + .sum::(); + let tolerance = 1.0e-9_f64.max(self.recurrence_schedule_s * 1.0e-12); + if attributed > self.recurrence_schedule_s + tolerance { + return Err(RusticolError::internal(format!( + "native recurrence schedule sub-attribution accounts for {attributed:.9e}s, exceeding inclusive recurrence schedule time {schedule:.9e}s", + schedule = self.recurrence_schedule_s, + ))); + } + Ok(()) + } + + fn validate_exclusive_phases(&self, phases: &[(&str, f64)]) -> RusticolResult<()> { + if !self.total_s.is_finite() || self.total_s < 0.0 { + return Err(RusticolError::internal(format!( + "native profile has invalid wall time {:.9e}s", + self.total_s, + ))); + } + for (label, value) in phases { + if !value.is_finite() || *value < 0.0 { + return Err(RusticolError::internal(format!( + "native profile has invalid {label} time {value:.9e}s" + ))); + } + } + let accounted = phases.iter().map(|(_, value)| value).sum::(); + let tolerance = 1.0e-9_f64.max(self.total_s * 1.0e-12); + if accounted > self.total_s + tolerance { + return Err(RusticolError::internal(format!( + "native profile exclusive top-level phases account for {accounted:.9e}s, exceeding wall time {wall:.9e}s", + wall = self.total_s, + ))); + } + Ok(()) + } + + fn validate_top_level_accounting(&self, eager: bool) -> RusticolResult<()> { + let mut phases = vec![ + ("native input pack", self.native_input_pack_s), + ("native input crossing", self.native_input_crossing_s), + ("runtime orchestration", self.orchestration_s), + ("state preparation", self.state_prepare_s), + ("state clearing", self.state_clear_s), + ("source fill", self.source_fill_s), + ("momentum input setup", self.momentum_input_setup_s), + ("model parameter setup", self.model_parameter_setup_s), + ]; + if eager { + phases.push(("inclusive eager execution", self.stage_evaluator_call_s)); + } else { + phases.extend([ + ("stage input pack", self.stage_input_pack_s), + ("stage evaluator calls", self.stage_evaluator_call_s), + ("stage output assignment", self.output_assign_s), + ("amplitude input pack", self.amplitude_input_pack_s), + ("amplitude evaluator calls", self.amplitude_evaluator_call_s), + ("reduction", self.reduction_s), + ]); + } + phases.extend([ + ("total materialization", self.total_materialization_s), + ("final output copy", self.final_output_copy_s), + ("selector planning", self.selector_planner_s), + ("selector gather", self.selector_gather_s), + ("selector scatter", self.selector_scatter_s), + ]); + self.validate_exclusive_phases(&phases) + } + + #[inline(never)] + fn accumulate(&mut self, other: &Self) { + self.native_input_pack_s += other.native_input_pack_s; + self.native_input_crossing_s += other.native_input_crossing_s; + self.orchestration_s += other.orchestration_s; + self.state_prepare_s += other.state_prepare_s; + self.state_clear_s += other.state_clear_s; + self.source_fill_s += other.source_fill_s; + self.momentum_input_setup_s += other.momentum_input_setup_s; + self.momentum_setup_s += other.momentum_setup_s; + self.model_parameter_setup_s += other.model_parameter_setup_s; + self.stage_input_pack_s += other.stage_input_pack_s; + self.stage_leaf_input_pack_s += other.stage_leaf_input_pack_s; + self.stage_evaluator_call_s += other.stage_evaluator_call_s; + self.stage_backend_call_s += other.stage_backend_call_s; + self.stage_evaluator_output_gather_s += other.stage_evaluator_output_gather_s; + self.stage_evaluator_s += other.stage_evaluator_s; + self.output_assign_s += other.output_assign_s; + self.amplitude_input_pack_s += other.amplitude_input_pack_s; + self.amplitude_leaf_input_pack_s += other.amplitude_leaf_input_pack_s; + self.amplitude_evaluator_call_s += other.amplitude_evaluator_call_s; + self.amplitude_backend_call_s += other.amplitude_backend_call_s; + self.amplitude_evaluator_output_gather_s += other.amplitude_evaluator_output_gather_s; + self.amplitude_output_remap_s += other.amplitude_output_remap_s; + self.amplitude_evaluator_s += other.amplitude_evaluator_s; + self.reduction_s += other.reduction_s; + self.resolved_reduction_materialization_s += other.resolved_reduction_materialization_s; + self.total_materialization_s += other.total_materialization_s; + self.final_output_copy_s += other.final_output_copy_s; + self.total_s += other.total_s; + accumulate_profile_stages( + &mut self.stage_input_pack_by_stage_s, + &other.stage_input_pack_by_stage_s, + ); + accumulate_profile_stages( + &mut self.stage_leaf_input_pack_by_stage_s, + &other.stage_leaf_input_pack_by_stage_s, + ); + accumulate_profile_stages( + &mut self.stage_evaluator_call_by_stage_s, + &other.stage_evaluator_call_by_stage_s, + ); + accumulate_profile_stages( + &mut self.stage_backend_call_by_stage_s, + &other.stage_backend_call_by_stage_s, + ); + accumulate_profile_stages( + &mut self.stage_evaluator_output_gather_by_stage_s, + &other.stage_evaluator_output_gather_by_stage_s, + ); + accumulate_profile_stages( + &mut self.stage_output_assign_by_stage_s, + &other.stage_output_assign_by_stage_s, + ); + self.eager_initialize_s += other.eager_initialize_s; + self.eager_gather_s += other.eager_gather_s; + self.eager_kernel_call_s += other.eager_kernel_call_s; + self.eager_invocation_scatter_s += other.eager_invocation_scatter_s; + self.eager_finalization_s += other.eager_finalization_s; + self.eager_scatter_finalization_s += other.eager_scatter_finalization_s; + self.eager_closure_s += other.eager_closure_s; + self.eager_reduction_s += other.eager_reduction_s; + self.eager_copy_out_s += other.eager_copy_out_s; + self.recurrence_momentum_fill_s += other.recurrence_momentum_fill_s; + self.recurrence_union_source_fill_s += other.recurrence_union_source_fill_s; + self.recurrence_schedule_s += other.recurrence_schedule_s; + self.recurrence_source_kernel_s += other.recurrence_source_kernel_s; + self.recurrence_contribution_kernel_s += other.recurrence_contribution_kernel_s; + self.recurrence_finalization_s += other.recurrence_finalization_s; + self.recurrence_closure_s += other.recurrence_closure_s; + self.recurrence_replay_output_mapping_s += other.recurrence_replay_output_mapping_s; + self.recurrence_momentum_scalar_value_count += other.recurrence_momentum_scalar_value_count; + self.recurrence_schedule_execution_count += other.recurrence_schedule_execution_count; + self.recurrence_replay_schedule_execution_count += + other.recurrence_replay_schedule_execution_count; + self.recurrence_union_schedule_execution_count += + other.recurrence_union_schedule_execution_count; + self.recurrence_union_source_row_count += other.recurrence_union_source_row_count; + self.recurrence_replay_output_value_count += other.recurrence_replay_output_value_count; + self.recurrence_source_call_count += other.recurrence_source_call_count; + self.recurrence_source_row_count += other.recurrence_source_row_count; + self.recurrence_contribution_call_count += other.recurrence_contribution_call_count; + self.recurrence_contribution_row_count += other.recurrence_contribution_row_count; + self.recurrence_finalization_call_count += other.recurrence_finalization_call_count; + self.recurrence_finalization_row_count += other.recurrence_finalization_row_count; + self.recurrence_closure_call_count += other.recurrence_closure_call_count; + self.recurrence_closure_row_count += other.recurrence_closure_row_count; + self.selector_planner_s += other.selector_planner_s; + self.selector_gather_s += other.selector_gather_s; + self.selector_scatter_s += other.selector_scatter_s; + self.native_input_component_count += other.native_input_component_count; + self.native_input_pack_bytes += other.native_input_pack_bytes; + self.native_input_crossing_bytes += other.native_input_crossing_bytes; + self.native_input_container_allocation_count += + other.native_input_container_allocation_count; + self.state_component_count += other.state_component_count; + self.state_clear_component_count += other.state_clear_component_count; + self.source_component_count += other.source_component_count; + self.momentum_component_count += other.momentum_component_count; + self.model_parameter_component_count += other.model_parameter_component_count; + self.stage_input_copy_component_count += other.stage_input_copy_component_count; + self.stage_leaf_input_copy_component_count += other.stage_leaf_input_copy_component_count; + self.stage_evaluator_output_gather_component_count += + other.stage_evaluator_output_gather_component_count; + self.stage_output_assign_component_count += other.stage_output_assign_component_count; + self.amplitude_input_copy_component_count += other.amplitude_input_copy_component_count; + self.amplitude_leaf_input_copy_component_count += + other.amplitude_leaf_input_copy_component_count; + self.amplitude_evaluator_output_gather_component_count += + other.amplitude_evaluator_output_gather_component_count; + self.amplitude_output_remap_component_count += other.amplitude_output_remap_component_count; + self.evaluator_backend_call_count += other.evaluator_backend_call_count; + self.compiled_direct_arena_engine_count += other.compiled_direct_arena_engine_count; + self.compiled_direct_arena_call_count += other.compiled_direct_arena_call_count; + self.compiled_direct_arena_minimum_effective_tile_capacity = match ( + self.compiled_direct_arena_minimum_effective_tile_capacity, + other.compiled_direct_arena_minimum_effective_tile_capacity, + ) { + (0, value) | (value, 0) => value, + (left, right) => left.min(right), + }; + self.compiled_direct_arena_maximum_physical_scalar_values_per_point = self + .compiled_direct_arena_maximum_physical_scalar_values_per_point + .max(other.compiled_direct_arena_maximum_physical_scalar_values_per_point); + self.compiled_direct_arena_maximum_hot_scalar_values_per_point = self + .compiled_direct_arena_maximum_hot_scalar_values_per_point + .max(other.compiled_direct_arena_maximum_hot_scalar_values_per_point); + self.compiled_direct_arena_maximum_source_scalar_values_per_point = self + .compiled_direct_arena_maximum_source_scalar_values_per_point + .max(other.compiled_direct_arena_maximum_source_scalar_values_per_point); + self.compiled_direct_arena_maximum_reduction_scalar_values_per_point = self + .compiled_direct_arena_maximum_reduction_scalar_values_per_point + .max(other.compiled_direct_arena_maximum_reduction_scalar_values_per_point); + self.compiled_direct_arena_boundary_input_bytes += + other.compiled_direct_arena_boundary_input_bytes; + self.compiled_direct_arena_boundary_current_output_bytes += + other.compiled_direct_arena_boundary_current_output_bytes; + self.compiled_direct_arena_boundary_amplitude_output_bytes += + other.compiled_direct_arena_boundary_amplitude_output_bytes; + self.reduction_input_component_count += other.reduction_input_component_count; + self.selector_gather_point_count += other.selector_gather_point_count; + self.selector_gather_bytes += other.selector_gather_bytes; + self.selector_scatter_value_count += other.selector_scatter_value_count; + self.resolved_materialized_component_count += other.resolved_materialized_component_count; + self.total_materialized_value_count += other.total_materialized_value_count; + self.final_output_copy_value_count += other.final_output_copy_value_count; + self.observed_scratch_reallocation_count += other.observed_scratch_reallocation_count; + self.native_output_allocation_count += other.native_output_allocation_count; + if self.selector_plan_kind == "none" && other.selector_plan_kind != "none" { + self.selector_plan_kind + .clone_from(&other.selector_plan_kind); + self.selector_group_sizes + .clone_from(&other.selector_group_sizes); + self.selector_reordered_point_count = other.selector_reordered_point_count; + self.selector_simd_lane_width = other.selector_simd_lane_width; + self.selector_simd_occupancy = other.selector_simd_occupancy; + } + } +} + +fn accumulate_profile_stages(target: &mut Vec, source: &[f64]) { + if target.len() < source.len() { + target.resize(source.len(), 0.0); + } + for (target, source) in target.iter_mut().zip(source) { + *target += source; + } +} + +#[derive(Clone, Debug, Serialize)] +pub struct NativeProfiledEvaluation { + pub values: Vec, + pub profile: NativeRuntimeProfile, +} + +#[derive(Clone, Debug, Serialize)] +pub struct NativeResolvedEvaluation { + /// Row-major storage with layout `[point][helicity][color]`. + pub values: Vec, + pub point_count: usize, + pub helicity_ids: Vec, + pub color_ids: Vec, +} + +#[derive(Clone, Debug, Serialize)] +pub struct NativeDecimalEvaluation { + pub values: Vec, + pub decimal_digits: u32, +} + +#[derive(Clone, Debug, Serialize)] +pub struct NativeDecimalResolvedEvaluation { + /// Row-major storage with layout `[point][helicity][color]`. + pub values: Vec, + pub totals: Vec, + pub point_count: usize, + pub helicity_ids: Vec, + pub color_ids: Vec, + pub decimal_digits: u32, +} + +#[derive(Clone, Debug, Serialize)] +pub struct NativeExternalParticle { + pub label: usize, + pub index: usize, + pub side: String, + pub role: String, + pub particle: String, + pub outgoing_particle: String, + pub pdg: i32, + pub outgoing_pdg: i32, + pub particle_class: String, + pub momentum_slot: usize, +} + +#[derive(Clone, Debug, Serialize)] +pub struct NativeHelicityConfiguration { + pub id: String, + pub index: usize, + pub helicities: Vec, + pub representative_id: String, + pub computed: bool, + pub structural_zero: bool, + pub coefficient: f64, +} + +#[derive(Clone, Debug, Serialize)] +pub struct NativeColorComponent { + pub id: String, + pub index: usize, + pub kind: String, + pub word: Vec, + pub representative_id: String, + pub computed: bool, + pub coefficient: f64, +} + +#[derive(Clone, Debug, Serialize)] +pub struct NativeModelParameter { + pub name: String, + pub kind: String, + pub parameter_index: usize, + pub default: f64, + pub default_imaginary: f64, + pub mutable: bool, +} + +impl NativeResolvedEvaluation { + pub fn shape(&self) -> (usize, usize, usize) { + ( + self.point_count, + self.helicity_ids.len(), + self.color_ids.len(), + ) + } + + pub fn totals(&self) -> Vec { + let component_count = self.helicity_ids.len() * self.color_ids.len(); + self.values + .chunks(component_count) + .map(|point| point.iter().sum()) + .collect() + } +} + +impl NativeDecimalResolvedEvaluation { + pub fn shape(&self) -> (usize, usize, usize) { + ( + self.point_count, + self.helicity_ids.len(), + self.color_ids.len(), + ) + } +} + +#[cfg(feature = "on-the-fly-test-support")] +#[derive(Clone, Debug)] +#[doc(hidden)] +pub struct NativeOnTheFlyCurrentProbeV1 { + pub semantic_digest: String, + pub component_count: u32, + /// Point-major, then component-major split-complex values. + pub values: Vec<[f64; 2]>, +} + +#[cfg(feature = "on-the-fly-test-support")] +#[derive(Clone, Debug)] +#[doc(hidden)] +pub struct NativeOnTheFlyFamilyQueryProbeV1 { + pub selected_public_flow_id: u32, + pub public_helicities: Vec, + pub query_digest: String, + pub raw_amplitudes: Vec<[f64; 2]>, + pub normalized_values: Vec, +} + +#[cfg(feature = "on-the-fly-test-support")] +#[derive(Clone, Debug)] +#[doc(hidden)] +pub struct NativeOnTheFlyFamilyProbeV1 { + pub queries: Vec, + pub census: crate::recurrence::OnTheFlyQueryFamilyCensusV1, + pub execution_cache_hit: bool, + pub execution_source_calls: u32, + pub execution_source_rows: u32, + pub execution_contribution_calls: u32, + pub execution_contribution_rows: u32, + pub execution_finalization_calls: u32, + pub execution_finalization_rows: u32, + pub execution_closure_calls: u32, + pub execution_closure_rows: u32, + pub cold_prepare_seconds: f64, + pub benchmark_warmup_repetitions: u32, + pub benchmark_repetitions: u32, + pub benchmark_elapsed_seconds: Option, + pub benchmark_seconds_per_point: Option, +} + +#[cfg(feature = "on-the-fly-test-support")] +#[derive(Clone, Debug)] +#[doc(hidden)] +pub struct NativeOnTheFlyArtifactProbeV1 { + pub artifact_id: String, + pub process_id: String, + pub seed_digest: String, + pub query_digest: String, + pub trace_digest: String, + pub point_count: u32, + pub raw_amplitudes: Vec<[f64; 2]>, + pub normalized_values: Vec, + pub query_family: Option, + pub normalization_factor: f64, + pub currents: Vec, + pub projection_enabled: bool, + pub projection_applied: bool, + pub projection_counts: [[u32; 3]; 2], + pub work_census_basis: String, + pub logical_current_count: u32, + pub resident_current_count: u32, + pub resident_current_component_count: u32, + pub source_operation_count: u32, + pub contribution_operation_count: u32, + pub finalization_operation_count: u32, + pub closure_operation_count: u32, + pub total_kernel_application_count: u32, + pub semantic_executor_binding_count: u32, + pub distinct_prepared_executor_count: u32, + pub trace_build_count: u32, + pub trace_cache_hit_count: u32, + pub momentum_fill_count: u32, + pub benchmark_warmup_repetitions: u32, + pub benchmark_repetitions: u32, + pub benchmark_elapsed_seconds: Option, + pub benchmark_seconds_per_point: Option, + pub direct_plan_load_attempts: u32, + pub direct_plan_decode_attempts: u32, + pub direct_plan_materialization_attempts: u32, + pub established_builder_attempts: u32, +} + +#[cfg(feature = "on-the-fly-test-support")] +#[derive(Clone, Debug)] +#[doc(hidden)] +pub struct NativeOnTheFlyExecutionComponentV1 { + pub dependency_depth: u32, + pub semantic_digest: String, + pub component: u32, + pub on_the_fly: [f64; 2], + pub recurrence: [f64; 2], + pub on_the_fly_bits: [u64; 2], + pub recurrence_bits: [u64; 2], + pub absolute_delta: [f64; 2], +} + +#[cfg(feature = "on-the-fly-test-support")] +#[derive(Clone, Debug)] +#[doc(hidden)] +pub struct NativeOnTheFlyExecutionDiagnosticV1 { + pub on_the_fly_artifact_id: String, + pub on_the_fly_process_id: String, + pub recurrence_artifact_id: String, + pub recurrence_process_id: String, + pub seed_digest: String, + pub query_digest: String, + pub trace_digest: String, + pub direct_plan_semantic_digest: String, + pub direct_runtime_layout_digest: String, + pub public_flow_id: u32, + pub representative_flow_id: u32, + pub public_helicities: Vec, + pub public_direct_helicity_id: u32, + pub representative_direct_helicity_id: u32, + pub replay_phase: [f64; 2], + pub replay_multiplicity: u32, + pub replay_scale: [f64; 2], + pub on_the_fly_public_amplitude: [f64; 2], + pub recurrence_representative_amplitude: [f64; 2], + pub recurrence_public_amplitude: [f64; 2], + pub amplitude_absolute_delta: [f64; 2], + pub current_components: Vec, + pub first_raw_bit_difference: Option, + pub raw_bit_difference_count: u32, + pub compared_current_count: u32, + pub excluded_direct_current_count: u32, +} + +/// Python-independent schema-v3 process runtime. +/// +/// The input momentum layout is `[point][external particle][E, px, py, pz]`. +/// Instances are mutable and must not be called concurrently; independent +/// instances can be used from separate threads. +pub struct NativeRuntime { + root: PathBuf, + artifact_id: String, + runtime: ExecutionRuntime, + execution_lane: NativeExecutionLane, + process: String, + process_key: String, + representative_process_id: String, + external_permutation: Vec, + input_crossing_map: Option>, + permutation_alias_of: Option, + final_state_permutation_alias_of: Option, + // Absent for stored processes, aliases, and permutations of either. + process_group: Option>, + physics_v1: native_runtime::LazyProcessPhysicsV1, + warnings_muted: bool, + warned_kinds: BTreeSet, + pending_warnings: Vec, + point_selector_scratch: PointSelectorExecutionScratch, + selector_simd_lane_width: usize, + // Cold, opt-in metadata and vector defaults; ordinary evaluation never reads it. + correlated: Option, +} + +enum NativeExecutionLane { + Compiled, + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + Eager(Box), + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + Recurrence(Box), + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + OnTheFly(Box), +} + +impl NativeExecutionLane { + #[cfg(feature = "symbolica-runtime")] + const fn is_eager(&self) -> bool { + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + { + matches!(self, Self::Eager(_)) + } + #[cfg(not(any(feature = "f64-compiled", feature = "f64-symjit")))] + { + false + } + } + + #[cfg(feature = "symbolica-runtime")] + const fn is_recurrence(&self) -> bool { + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + { + matches!(self, Self::Recurrence(_)) + } + #[cfg(not(any(feature = "f64-compiled", feature = "f64-symjit")))] + { + false + } + } +} + +#[derive(Clone, Copy, Debug)] +struct RuntimeParameterSlots { + real: usize, + imaginary: Option, +} + +struct ModelParameterEvaluatorRuntime { + input_parameter_indices: Vec, + outputs: Vec, + evaluator: EvaluatorGroup, +} + +struct StageRuntime { + outputs: Vec<(usize, usize)>, + output_spans: Vec<(usize, usize, usize)>, + chunk_outputs: Vec>, + chunk_output_spans: Vec>, + input_components: Option>, + input_spans: Vec<(usize, usize, usize)>, + parameter_scratch_f64: Vec>, + output_scratch_f64: Vec>, + evaluator: EvaluatorGroup, +} + +#[derive(Default)] +struct RoutedReductionScratch { + helicity_indices: Vec, + color_indices: Vec, + helicity_positions: Vec>, + color_positions: Vec>, + selected_member_weights: Vec<(usize, usize, f64)>, + selected_member_weight_ranges: Vec>, + direct_group_re: Vec, + direct_group_im: Vec, + direct_source_components: Vec, + direct_target_components: Vec, + direct_totals: Vec, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +struct MaterializedHelicityDirectTotalPlanKey { + physics_binding_id: u64, + helicity_index: usize, + root_factor_bits: Vec<(usize, [u64; 2])>, + color_indices: Vec, +} + +#[derive(Clone, Copy, Debug)] +struct MaterializedHelicityDirectTotalRoot { + output_index: usize, + factor: Complex, +} + +#[derive(Clone, Debug)] +struct MaterializedHelicityDirectTotalGroup { + root_range: std::ops::Range, + all_sector_weight: f64, + identity_output_index: Option, +} + +#[derive(Clone, Copy, Debug)] +struct MaterializedHelicityDirectTotalColorGroup { + group_index: usize, + weight: f64, +} + +#[derive(Clone, Copy, Debug)] +struct MaterializedHelicityDirectTotalContractionEntry { + left_group_index: usize, + right_group_index: usize, + weight_re: f64, + weight_im: f64, + symmetry_factor: f64, +} + +#[derive(Clone, Copy, Debug)] +struct MaterializedHelicityDirectDefaultPlanCacheEntry { + physics_binding_id: u64, + helicity_index: usize, + plan_index: usize, +} + +#[derive(Clone, Debug)] +enum MaterializedHelicityDirectTotalReduction { + Lc { + color_group_ranges: Vec>, + color_groups: Vec, + }, + Contracted { + entries: Vec, + }, +} + +#[derive(Clone, Debug)] +struct MaterializedHelicityDirectTotalPlan { + key: MaterializedHelicityDirectTotalPlanKey, + roots: Vec, + groups: Vec, + reduction: MaterializedHelicityDirectTotalReduction, +} + +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +struct CompiledDirectRoutedReductionFootprint { + maximum_source_component_count: usize, + maximum_target_component_count: usize, +} + +impl CompiledDirectRoutedReductionFootprint { + fn merge(self, other: Self) -> Self { + Self { + maximum_source_component_count: self + .maximum_source_component_count + .max(other.maximum_source_component_count), + maximum_target_component_count: self + .maximum_target_component_count + .max(other.maximum_target_component_count), + } + } + + const fn is_empty(self) -> bool { + self.maximum_source_component_count == 0 && self.maximum_target_component_count == 0 + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum CompiledDirectReducerKind { + Plain, + Coherent, + Contracted { group_count: usize }, + SymmetricGroup { workspace_scalars_per_lane: usize }, + ColorTopologyReplay { physical_group_count: usize }, +} + +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +enum CompiledDirectReductionFootprint { + /// The maximum reducer working set was derived entirely from authenticated + /// amplitude, physics, and replay metadata. + Authenticated { hot_scalar_values_per_point: usize }, + /// A future/legacy execution shape did not expose enough static metadata. + /// Production Direct tiling must fail closed to one point. + #[default] + Unauthenticated, +} + +impl CompiledDirectReductionFootprint { + fn authenticated( + kind: CompiledDirectReducerKind, + maximum_group_component_count: usize, + maximum_resolved_component_count: usize, + routed: CompiledDirectRoutedReductionFootprint, + ) -> RusticolResult { + let maximum_group_plane_scalars = maximum_group_component_count + .max(1) + .checked_mul(2) + .ok_or_else(|| { + RusticolError::integrity( + "compiled Direct-Arena reduction group footprint overflows", + ) + })?; + let (amplitude_input_scalars, reducer_workspace_scalars, supports_resolved) = match kind { + CompiledDirectReducerKind::Plain => (2, 0, false), + CompiledDirectReducerKind::Coherent => (maximum_group_plane_scalars, 2, true), + CompiledDirectReducerKind::Contracted { group_count } => ( + maximum_group_plane_scalars, + group_count.checked_mul(2).ok_or_else(|| { + RusticolError::integrity( + "compiled Direct-Arena contracted reduction footprint overflows", + ) + })?, + true, + ), + CompiledDirectReducerKind::SymmetricGroup { + workspace_scalars_per_lane, + } => ( + maximum_group_plane_scalars, + workspace_scalars_per_lane, + false, + ), + CompiledDirectReducerKind::ColorTopologyReplay { + physical_group_count, + } => ( + maximum_group_plane_scalars, + physical_group_count.checked_mul(2).ok_or_else(|| { + RusticolError::integrity( + "compiled Direct-Arena topology-replay footprint overflows", + ) + })?, + true, + ), + }; + // One real total plane is live in every reducer. Reducer storage is a + // cache-local working-set estimate only; none of it belongs to the + // Direct workspace batching target. + let total_phase = amplitude_input_scalars + .checked_add(reducer_workspace_scalars) + .and_then(|value| value.checked_add(1)) + .ok_or_else(|| { + RusticolError::integrity( + "compiled Direct-Arena total-reduction footprint overflows", + ) + })?; + let resolved_phase = if supports_resolved { + total_phase + .checked_add(maximum_resolved_component_count) + .ok_or_else(|| { + RusticolError::integrity( + "compiled Direct-Arena resolved-reduction footprint overflows", + ) + })? + } else { + total_phase + }; + let routed_phase = if routed.is_empty() { + 0 + } else { + // Routed reducers materialize a split-complex group, one real + // totals plane, and authenticated source/target component planes. + // The target may be owned by an outer public-batch replay, but its + // tile slice is still part of the phase-local cache working set. + amplitude_input_scalars + .checked_add(2) + .and_then(|value| value.checked_add(1)) + .and_then(|value| value.checked_add(routed.maximum_source_component_count)) + .and_then(|value| value.checked_add(routed.maximum_target_component_count)) + .ok_or_else(|| { + RusticolError::integrity( + "compiled Direct-Arena routed-reduction footprint overflows", + ) + })? + }; + Ok(Self::Authenticated { + hot_scalar_values_per_point: total_phase.max(resolved_phase).max(routed_phase), + }) + } + + const fn hot_scalar_values_per_point(self) -> Option { + match self { + Self::Authenticated { + hot_scalar_values_per_point, + } => Some(hot_scalar_values_per_point), + Self::Unauthenticated => None, + } + } +} + +struct AmplitudeRuntime { + output_length: usize, + raw_sum_weights: Vec, + raw_sum_all_sector_weights: Vec, + raw_sum_color_sector_ids: Vec>, + raw_sum_groups: Vec, + has_coherent_groups: bool, + color_contraction: Option, + color_topology_replay: Option, + input_components: Option>, + input_spans: Vec<(usize, usize, usize)>, + parameter_scratch_f64: Vec>, + evaluator_output_scratch_f64: Vec>, + output_scratch_f64: Vec>, + resolved_source_row_scratch_f64: Vec, + resolved_target_row_scratch_f64: Vec, + routed_reduction_scratch: RoutedReductionScratch, + materialized_helicity_direct_total_plans: Vec, + materialized_helicity_direct_total_plan_capacity: usize, + materialized_helicity_direct_total_next_replacement: usize, + materialized_helicity_direct_default_plan_by_selector_domain: + Vec>, + evaluator_output_order: Option>, + evaluator: Option, +} + +struct ColorTopologyReplayAmplitudeRuntime { + mappings: Vec, + physical_groups: Vec, + physical_group_helicities: Vec>, + color_contraction: Option, + unit_weights: Vec, + no_sector_ids: Vec>, + physical_group_scratch_f64: Vec>, + covered_groups: Vec, +} + +struct ColorTopologyReplayAmplitudeMapping { + label_permutation: Vec<(usize, usize)>, + group_routes: Vec, +} + +struct ColorTopologyReplayAmplitudeGroupRoute { + source_group_index: usize, + target_group_index: usize, + factor: Complex, +} + +mod runtime_load; +use runtime_load::*; + +mod model_parameters; +use model_parameters::*; + +mod evaluation; +use evaluation::resolved_f64_totals; +#[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] +mod compiled_direct_prototype; +mod helicity_lane; +use helicity_lane::*; +mod momentum; +use momentum::*; +mod sources; + +mod validation; +use validation::*; + +mod correlated; +mod native_runtime; +pub use correlated::{ + NativeCorrelatedEvaluation, NativeCorrelatedRequest, NativeSpinCorrelationVectors, +}; + +#[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] +mod on_the_fly_warm_up; +#[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] +pub use on_the_fly_warm_up::{ + NativeOnTheFlyWarmUpEvent, NativeOnTheFlyWarmUpEventKind, NativeOnTheFlyWarmUpObserver, + NativeOnTheFlyWarmUpResult, NativeOnTheFlyWarmUpStage, +}; + +mod artifact_load; +use artifact_load::*; + +#[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] +mod eager_backend; +#[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] +use eager_backend::*; + +#[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] +mod recurrence_backend; +#[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] +mod recurrence_lane; +#[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] +use recurrence_lane::*; + +#[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] +mod recurrence_load; +#[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] +use recurrence_load::*; + +#[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] +pub(crate) mod recurrence_manifest; +#[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] +use recurrence_manifest::*; + +#[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] +mod recurrence_bootstrap; +#[cfg(feature = "python-generation-bridge")] +pub(crate) use recurrence_bootstrap::build_recurrence_bootstrap_image_v1; +#[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] +pub(crate) use recurrence_bootstrap::{ + RecurrenceReadyExecutionV1, decode_recurrence_bootstrap_image_v1, +}; + +#[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] +mod recurrence_bootstrap_physics; +#[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] +use recurrence_bootstrap_physics::*; + +#[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] +mod recurrence_process_pack; + +#[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] +mod on_the_fly_lane; +#[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] +mod on_the_fly_load; +#[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] +mod on_the_fly_manifest; +#[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] +mod on_the_fly_public_metadata; +#[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] +mod on_the_fly_selectors; + +#[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] +#[allow(dead_code)] +mod eager_manifest; +#[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] +use eager_manifest::*; + +#[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] +mod eager_v3_common; +#[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] +mod eager_v3_decode; +#[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] +mod eager_v3_load; +#[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] +mod eager_v3_manifest; + +#[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] +#[allow(dead_code)] +mod eager_load; +#[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] +use eager_load::*; + +#[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] +mod eager_lane; +#[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] +use eager_lane::*; + +mod physics; + +mod point_selectors; +use point_selectors::*; + +#[path = "evaluator.rs"] +mod evaluator; +#[cfg(all( + test, + feature = "f64-compiled", + not(feature = "f64-symjit"), + any(target_os = "linux", target_os = "macos") +))] +pub(crate) use evaluator::native_direct::tests::count_allocations; +pub(crate) use evaluator::recurrence_closure_direct::execute_closure_reduce_rows; +pub(crate) use evaluator::recurrence_intrinsic_direct::execute_interaction_terminal_closures; +#[cfg(all(test, feature = "f64-symjit"))] +pub(crate) use evaluator::symjit_direct::tests::count_allocations; +#[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] +pub(crate) use evaluator::symjit_eager_direct; +#[cfg(any(test, feature = "python-generation-bridge"))] +pub(crate) use evaluator::symjit_plane::compile_symbolica_program_to_plane_application_bytes; +use evaluator::*; + +#[path = "wavefunctions.rs"] +mod wavefunctions; +use wavefunctions::*; + +#[cfg(test)] +#[path = "engine_tests.rs"] +mod tests; + +#[cfg(test)] +#[path = "engine/source_metadata_tests.rs"] +mod source_metadata_tests; + +#[cfg(test)] +#[path = "engine/quantum_number_flow_tests.rs"] +mod quantum_number_flow_tests; + +#[cfg(test)] +#[path = "engine/contraction_metadata_tests.rs"] +mod contraction_metadata_tests; + +#[cfg(all(test, any(feature = "f64-compiled", feature = "f64-symjit")))] +#[path = "engine/eager_integration_tests.rs"] +mod eager_integration_tests; + +#[cfg(all(test, any(feature = "f64-compiled", feature = "f64-symjit")))] +#[path = "engine/recurrence_integration_tests.rs"] +mod recurrence_integration_tests; + +#[cfg(all(test, any(feature = "f64-compiled", feature = "f64-symjit")))] +#[path = "engine/eager_v3_manifest_tests.rs"] +mod eager_v3_manifest_tests; diff --git a/pyamplicol/rust/crates/rusticol-core/src/engine/artifact_load.rs b/pyamplicol/rust/crates/rusticol-core/src/engine/artifact_load.rs new file mode 100644 index 0000000000..91d7f36e48 --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/engine/artifact_load.rs @@ -0,0 +1,1126 @@ +// SPDX-License-Identifier: 0BSD + +#[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] +use super::eager_v3_manifest::{EagerV3ExecutionManifest, parse_eager_v3_execution_manifest}; +#[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] +use super::on_the_fly_manifest::{ + ON_THE_FLY_EXECUTION_KIND, OnTheFlyExecutionManifest, parse_on_the_fly_execution_manifest, +}; +use super::*; +#[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] +use crate::recurrence::RECURRENCE_RUNTIME_KIND; +use std::borrow::Cow; + +pub(super) enum LoadedExecutionManifest { + Compiled(Box), + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + EagerV3(Box), + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + Recurrence(Box), + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + OnTheFly(Box), +} + +impl LoadedExecutionManifest { + fn key(&self) -> &str { + match self { + Self::Compiled(value) => &value.key, + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + Self::EagerV3(value) => &value.key, + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + Self::Recurrence(value) => &value.key, + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + Self::OnTheFly(value) => &value.key, + } + } + + fn process(&self) -> &str { + match self { + Self::Compiled(value) => &value.process, + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + Self::EagerV3(value) => &value.process, + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + Self::Recurrence(value) => &value.process, + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + Self::OnTheFly(value) => &value.process, + } + } + + fn color_accuracy(&self) -> &str { + match self { + Self::Compiled(value) => &value.color_accuracy, + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + Self::EagerV3(value) => &value.color_accuracy, + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + Self::Recurrence(value) => &value.color_accuracy, + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + Self::OnTheFly(value) => &value.color_accuracy, + } + } + + fn external_pdg_order(&self) -> &[i32] { + match self { + Self::Compiled(value) => &value.external_pdg_order, + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + Self::EagerV3(value) => &value.external_pdg_order, + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + Self::Recurrence(value) => &value.external_pdg_order, + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + Self::OnTheFly(value) => &value.external_pdg_order, + } + } + + fn required_runtime_capabilities(&self) -> &[String] { + match self { + Self::Compiled(value) => &value.required_runtime_capabilities, + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + Self::EagerV3(value) => &value.required_runtime_capabilities, + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + Self::Recurrence(value) => &value.required_runtime_capabilities, + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + Self::OnTheFly(value) => &value.required_runtime_capabilities, + } + } + + fn validate_portable_execution(&self) -> RusticolResult<()> { + match self { + Self::Compiled(value) => validate_compiled_portable_jit(value).map(|_| ()), + // Eager and recurrence loaders authenticate their prepared backend, + // portable flag, target marker, and exact O2 level while loading + // the kernel pack. Their compact execution headers intentionally do + // not duplicate that backend identity. + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + Self::EagerV3(_) | Self::Recurrence(_) | Self::OnTheFly(_) => Ok(()), + } + } +} + +pub(super) fn load_verified_evaluator( + artifact: &VerifiedArtifact, + selection: &crate::ArtifactSelection, +) -> RusticolResult<(LoadedExecutionManifest, PathBuf)> { + let root_manifest_path = &artifact.manifest().runtime.evaluator_manifest_path; + if artifact.payload(root_manifest_path)?.role != PayloadRole::EvaluatorManifest { + return Err(RusticolError::security(format!( + "runtime evaluator path {root_manifest_path:?} is not an evaluator-manifest payload" + ))); + } + let bytes = artifact.read_payload(root_manifest_path)?; + let header: ExecutionManifestHeader = serde_json::from_slice(&bytes).map_err(|error| { + RusticolError::serialization(format!( + "could not parse evaluator manifest {root_manifest_path:?}: {error}" + )) + })?; + if matches!(header.schema_version, 1 | 2) { + return Err(RusticolError::compatibility(format!( + "internal evaluator schema v{} is unsupported and unsafe to migrate; regenerate the artifact with `pyamplicol generate`", + header.schema_version + ))); + } + if header.schema_version != PROCESS_ARTIFACT_SCHEMA_VERSION { + return Err(RusticolError::compatibility(format!( + "unsupported internal evaluator schema {}; this runtime requires schema v{}", + header.schema_version, PROCESS_ARTIFACT_SCHEMA_VERSION + ))); + } + if header.kind == "pyamplicol-runtime-execution" + || header.kind == "pyamplicol-runtime-eager-execution" + || header.kind == RECURRENCE_RUNTIME_KIND + || header.kind == ON_THE_FLY_EXECUTION_KIND + { + if artifact.manifest().processes.len() != 1 { + return Err(RusticolError::integrity( + "a direct execution manifest cannot represent multiple outer processes", + )); + } + let outer = &artifact.manifest().processes[0]; + let manifest = parse_execution_payload_variant(&bytes, root_manifest_path, outer)?; + if manifest.key() != outer.id + || manifest.process() != outer.expression + || manifest.color_accuracy() != outer.color_accuracy + || manifest.external_pdg_order() != outer.external_pdgs + { + return Err(RusticolError::integrity(format!( + "execution manifest {root_manifest_path:?} does not match outer process {:?}", + outer.id + ))); + } + validate_capability_list_match( + &artifact.manifest().runtime.required_runtime_capabilities, + manifest.required_runtime_capabilities(), + "outer runtime and direct execution manifest", + )?; + validate_capability_list_match( + &outer.required_runtime_capabilities, + manifest.required_runtime_capabilities(), + "outer process and direct execution manifest", + )?; + validate_portable_execution_contract(artifact, &manifest)?; + let path = artifact.payload_path(root_manifest_path)?; + let root = path.parent().ok_or_else(|| { + RusticolError::artifact("evaluator manifest has no containing directory") + })?; + validate_loaded_execution_references(artifact, root, &manifest)?; + return Ok((manifest, root.to_path_buf())); + } + if header.kind != "pyamplicol-runtime-execution-set" { + return Err(RusticolError::compatibility(format!( + "unsupported internal evaluator manifest kind {:?}", + header.kind + ))); + } + let process_set: ExecutionSetManifest = serde_json::from_slice(&bytes).map_err(|error| { + RusticolError::serialization(format!( + "could not parse evaluator process-set manifest {root_manifest_path:?}: {error}" + )) + })?; + if process_set.schema_version != PROCESS_ARTIFACT_SCHEMA_VERSION + || process_set.kind != "pyamplicol-runtime-execution-set" + || process_set.processes.is_empty() + { + return Err(RusticolError::compatibility( + "invalid schema-v3 execution-set manifest; regenerate the artifact", + )); + } + let mut process_ids = BTreeSet::new(); + let mut manifest_paths = BTreeSet::new(); + let mut entry_capabilities = BTreeSet::new(); + for entry in &process_set.processes { + if !process_ids.insert(entry.process_id.as_str()) { + return Err(RusticolError::integrity(format!( + "execution-set manifest contains duplicate process id {:?}", + entry.process_id + ))); + } + let path = execution_manifest_path(root_manifest_path, &entry.manifest_path)?; + if !manifest_paths.insert(path.to_ascii_lowercase()) { + return Err(RusticolError::integrity(format!( + "execution-set manifest reuses manifest path {path:?}" + ))); + } + if artifact.payload(&path)?.role != PayloadRole::EvaluatorManifest { + return Err(RusticolError::security(format!( + "internal evaluator path {path:?} is not an evaluator-manifest payload" + ))); + } + let capabilities = entry + .required_runtime_capabilities + .iter() + .cloned() + .collect::>(); + if capabilities.len() != entry.required_runtime_capabilities.len() { + return Err(RusticolError::integrity(format!( + "execution-set entry {:?} contains duplicate runtime capabilities", + entry.process_id + ))); + } + entry_capabilities.extend(capabilities); + } + validate_capability_set_match( + &process_set.required_runtime_capabilities, + &entry_capabilities, + "execution-set manifest", + )?; + validate_capability_list_match( + &artifact.manifest().runtime.required_runtime_capabilities, + &process_set.required_runtime_capabilities, + "outer runtime and execution-set manifest", + )?; + let outer_process_ids = artifact + .manifest() + .processes + .iter() + .map(|process| process.id.as_str()) + .collect::>(); + if process_ids != outer_process_ids { + return Err(RusticolError::integrity( + "execution-set process ids do not exactly match the outer schema-v3 manifest", + )); + } + + let mut selected = None; + for entry in &process_set.processes { + let manifest_path = execution_manifest_path(root_manifest_path, &entry.manifest_path)?; + let bytes = artifact.read_payload(&manifest_path)?; + let outer = artifact + .manifest() + .processes + .iter() + .find(|process| process.id == entry.process_id) + .expect("execution-set ids were matched to outer processes"); + let manifest = parse_execution_payload_variant(&bytes, &manifest_path, outer)?; + validate_capability_list_match( + &outer.required_runtime_capabilities, + &entry.required_runtime_capabilities, + &format!( + "outer process {:?} and execution-set entry", + entry.process_id + ), + )?; + if manifest.key() != outer.id + || manifest.process() != outer.expression + || manifest.color_accuracy() != outer.color_accuracy + || manifest.external_pdg_order() != outer.external_pdgs + { + return Err(RusticolError::integrity(format!( + "execution manifest {manifest_path:?} does not match outer process {:?}", + outer.id + ))); + } + validate_capability_list_match( + &entry.required_runtime_capabilities, + manifest.required_runtime_capabilities(), + &format!("execution-set entry {:?}", entry.process_id), + )?; + validate_portable_execution_contract(artifact, &manifest)?; + let path = artifact.payload_path(&manifest_path)?; + let evaluator_root = path.parent().ok_or_else(|| { + RusticolError::artifact("evaluator manifest has no containing directory") + })?; + validate_loaded_execution_references(artifact, evaluator_root, &manifest)?; + if entry.process_id == selection.process.id { + selected = Some((manifest, evaluator_root.to_path_buf())); + } + } + selected.ok_or_else(|| { + RusticolError::integrity(format!( + "execution set does not contain selected outer process {:?}", + selection.process.id + )) + }) +} + +fn validate_portable_execution_contract( + artifact: &VerifiedArtifact, + manifest: &LoadedExecutionManifest, +) -> RusticolResult<()> { + if artifact.manifest().producer.target.triple == crate::artifact::PORTABLE_64LE_ARTIFACT_TARGET + { + manifest.validate_portable_execution()?; + } + Ok(()) +} + +fn validate_compiled_portable_jit(manifest: &ExecutionManifest) -> RusticolResult { + let mut leaf_count = 0usize; + if let Some(model_parameters) = &manifest.compiled.model_parameter_evaluator { + leaf_count = leaf_count + .checked_add(model_parameters.evaluator.validate_portable_jit()?) + .ok_or_else(|| RusticolError::integrity("portable evaluator leaf count overflows"))?; + } + if let Some(stages) = &manifest.compiled.stage_evaluators { + for stage in stages + .stages + .iter() + .chain(std::iter::once(&stages.amplitude_stage)) + { + leaf_count = leaf_count + .checked_add(stage.evaluator.validate_portable_jit()?) + .ok_or_else(|| { + RusticolError::integrity("portable evaluator leaf count overflows") + })?; + if stage.compiled_plane_arena.as_ref().is_some_and(|arena| { + arena + .leaves + .iter() + .any(|leaf| !matches!(leaf.optimization_level, 1 | 2)) + }) { + return Err(nonportable_compiled_evaluator_error()); + } + } + } + for nested in manifest + .helicity_sum_execution + .iter() + .map(Box::as_ref) + .chain( + manifest + .helicity_selector_executions + .iter() + .map(|record| record.execution.as_ref()), + ) + .chain( + manifest + .color_selector_executions + .iter() + .map(|record| record.execution.as_ref()), + ) + { + leaf_count = leaf_count + .checked_add(validate_compiled_portable_jit(nested)?) + .ok_or_else(|| RusticolError::integrity("portable evaluator leaf count overflows"))?; + } + if leaf_count == 0 && manifest.compiled.runtime_available { + return Err(nonportable_compiled_evaluator_error()); + } + Ok(leaf_count) +} + +impl EvaluatorManifest { + fn validate_portable_jit(&self) -> RusticolResult { + match self { + Self::SymjitApplication { + optimization_level, + plane_application, + .. + } if matches!(*optimization_level, 1 | 2) + && plane_application + .as_ref() + .is_none_or(|application| matches!(application.optimization_level, 1 | 2)) => + { + Ok(1) + } + Self::Chunked { chunks, .. } => chunks.iter().try_fold(0usize, |count, chunk| { + count + .checked_add(chunk.validate_portable_jit()?) + .ok_or_else(|| { + RusticolError::integrity("portable evaluator leaf count overflows") + }) + }), + _ => Err(nonportable_compiled_evaluator_error()), + } + } +} + +fn nonportable_compiled_evaluator_error() -> RusticolError { + RusticolError::compatibility( + "portable-64le compiled artifacts require only canonical O1/O2 SymJIT evaluators; O0, O3, C++, ASM, and legacy JIT evaluators remain target-specific", + ) +} + +pub(super) fn validate_capability_list_match( + left: &[String], + right: &[String], + context: &str, +) -> RusticolResult<()> { + // Artifact and execution validators require canonical sorted capability + // lists. Preserve the order-insensitive fallback for other callers while + // keeping the ordinary authenticated load path allocation-free. + if left == right + && left.windows(2).all(|pair| pair[0] < pair[1]) + && right.windows(2).all(|pair| pair[0] < pair[1]) + { + return Ok(()); + } + let right_set = right.iter().cloned().collect::>(); + if right_set.len() != right.len() { + return Err(RusticolError::integrity(format!( + "{context} contains duplicate runtime capabilities" + ))); + } + validate_capability_set_match(left, &right_set, context) +} + +fn validate_capability_set_match( + declared: &[String], + actual: &BTreeSet, + context: &str, +) -> RusticolResult<()> { + let declared_set = declared.iter().cloned().collect::>(); + if declared_set.len() != declared.len() || &declared_set != actual { + return Err(RusticolError::integrity(format!( + "{context} runtime capabilities {declared:?} do not match {actual:?}" + ))); + } + Ok(()) +} + +fn parse_execution_payload(bytes: &[u8], path: &str) -> RusticolResult { + let header: ExecutionManifestHeader = serde_json::from_slice(bytes).map_err(|error| { + RusticolError::serialization(format!( + "could not parse evaluator manifest {path:?}: {error}" + )) + })?; + if matches!(header.schema_version, 1 | 2) { + return Err(RusticolError::compatibility(format!( + "internal evaluator schema v{} is unsupported and unsafe to migrate; regenerate the artifact with `pyamplicol generate`", + header.schema_version + ))); + } + if header.schema_version != PROCESS_ARTIFACT_SCHEMA_VERSION { + return Err(RusticolError::compatibility(format!( + "unsupported internal evaluator schema {}; this runtime requires schema v{}", + header.schema_version, PROCESS_ARTIFACT_SCHEMA_VERSION + ))); + } + if header.kind != "pyamplicol-runtime-execution" { + return Err(RusticolError::compatibility(format!( + "unsupported internal evaluator manifest kind {:?}", + header.kind + ))); + } + serde_json::from_slice(bytes).map_err(|error| execution_manifest_parse_error(path, error)) +} + +fn parse_execution_payload_variant( + bytes: &[u8], + path: &str, + outer: &crate::ArtifactProcess, +) -> RusticolResult { + let header: ExecutionManifestHeader = serde_json::from_slice(bytes).map_err(|error| { + RusticolError::serialization(format!( + "could not parse evaluator manifest {path:?}: {error}" + )) + })?; + match header.kind.as_str() { + "pyamplicol-runtime-execution" => parse_execution_payload(bytes, path) + .map(Box::new) + .map(LoadedExecutionManifest::Compiled), + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + EAGER_EXECUTION_KIND => parse_eager_v3_execution_manifest(bytes, outer) + .map(Box::new) + .map(LoadedExecutionManifest::EagerV3), + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + RECURRENCE_RUNTIME_KIND => parse_recurrence_execution_manifest(bytes, path, outer) + .map(Box::new) + .map(LoadedExecutionManifest::Recurrence), + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + ON_THE_FLY_EXECUTION_KIND => parse_on_the_fly_execution_manifest(bytes, outer) + .map(Box::new) + .map(LoadedExecutionManifest::OnTheFly), + _ => Err(RusticolError::compatibility(format!( + "unsupported internal evaluator manifest kind {:?}", + header.kind + ))), + } +} + +fn validate_loaded_execution_references( + artifact: &VerifiedArtifact, + evaluator_root: &Path, + manifest: &LoadedExecutionManifest, +) -> RusticolResult<()> { + match manifest { + LoadedExecutionManifest::Compiled(manifest) => { + validate_evaluator_payload_references(artifact, evaluator_root, manifest) + } + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + LoadedExecutionManifest::EagerV3(manifest) => { + let relative_root = evaluator_root.strip_prefix(artifact.root()).map_err(|_| { + RusticolError::security("eager plan-v3 root escapes the verified artifact") + })?; + let relative = relative_root.join(&manifest.plan.runtime_container.path); + let relative = relative.to_str().ok_or_else(|| { + RusticolError::security("eager plan-v3 container path is not valid UTF-8") + })?; + if artifact.payload(relative)?.role != PayloadRole::EvaluatorState { + return Err(RusticolError::security( + "eager plan-v3 runtime container is not an evaluator-state payload", + )); + } + Ok(()) + } + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + LoadedExecutionManifest::Recurrence(manifest) => { + let schedule = artifact.payload(&manifest.plan.runtime_schedule.path)?; + if schedule.role != PayloadRole::EvaluatorState || schedule.process_id.is_some() { + return Err(RusticolError::security( + "recurrence root schedule is not an unbound evaluator-state payload", + )); + } + let relative_root = evaluator_root.strip_prefix(artifact.root()).map_err(|_| { + RusticolError::security("recurrence process root escapes the verified artifact") + })?; + let binding = relative_root.join(&manifest.plan.process_binding.path); + let binding = binding.to_str().ok_or_else(|| { + RusticolError::security("recurrence process-binding path is not valid UTF-8") + })?; + let binding = artifact.payload(binding)?; + if binding.role != PayloadRole::EvaluatorState + || binding.process_id.as_deref() != Some(manifest.key.as_str()) + { + return Err(RusticolError::security( + "recurrence process binding is not owned by its process", + )); + } + Ok(()) + } + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + LoadedExecutionManifest::OnTheFly(manifest) => { + let relative_root = evaluator_root.strip_prefix(artifact.root()).map_err(|_| { + RusticolError::security("on-the-fly process root escapes the verified artifact") + })?; + let relative = relative_root.join(&manifest.runtime_container.path); + let relative = relative.to_str().ok_or_else(|| { + RusticolError::security("on-the-fly container path is not valid UTF-8") + })?; + let payload = artifact.payload(relative)?; + if payload.role != PayloadRole::EvaluatorState + || payload.process_id.as_deref() != Some(manifest.key.as_str()) + { + return Err(RusticolError::security( + "on-the-fly runtime container is not owned evaluator state", + )); + } + Ok(()) + } + } +} + +pub(super) fn execution_manifest_parse_error( + path: &str, + error: serde_json::Error, +) -> RusticolError { + let detail = error.to_string(); + if ["source_ir", "applied_crossing", "source_basis"] + .iter() + .any(|field| detail.contains(&format!("missing field `{field}`"))) + { + return RusticolError::compatibility(format!( + "schema-v3 evaluator manifest {path:?} predates typed source metadata; regenerate the artifact with `pyamplicol generate`" + )); + } + if detail.contains("missing field `contraction_ir`") { + return RusticolError::compatibility(format!( + "schema-v3 evaluator manifest {path:?} predates typed amplitude-contraction metadata; regenerate the artifact with `pyamplicol generate`" + )); + } + RusticolError::serialization(format!( + "could not parse schema-v3 evaluator manifest {path:?}: {detail}" + )) +} + +pub(super) fn execution_manifest_path( + parent_manifest: &str, + entry_path: &str, +) -> RusticolResult { + let base = Path::new(parent_manifest) + .parent() + .unwrap_or_else(|| Path::new("")); + let entry = confined_internal_path(entry_path, "evaluator process path")?; + let path = base.join(entry); + path.to_str() + .map(str::to_string) + .ok_or_else(|| RusticolError::security("evaluator process path is not valid UTF-8")) +} + +pub(super) fn validate_evaluator_payload_references( + artifact: &VerifiedArtifact, + evaluator_root: &Path, + manifest: &ExecutionManifest, +) -> RusticolResult<()> { + let relative_root = evaluator_root.strip_prefix(artifact.root()).map_err(|_| { + RusticolError::security("evaluator root escapes the verified artifact root") + })?; + if let Some(parameters) = &manifest.compiled.model_parameter_evaluator { + validate_evaluator_reference(artifact, relative_root, ¶meters.evaluator)?; + } + if let Some(stages) = &manifest.compiled.stage_evaluators { + for stage in &stages.stages { + validate_evaluator_reference(artifact, relative_root, &stage.evaluator)?; + } + validate_evaluator_reference(artifact, relative_root, &stages.amplitude_stage.evaluator)?; + } + if let Some(sum_manifest) = manifest.helicity_sum_execution.as_deref() { + validate_evaluator_payload_references(artifact, evaluator_root, sum_manifest)?; + } + for record in &manifest.helicity_selector_executions { + validate_evaluator_payload_references(artifact, evaluator_root, record.execution.as_ref())?; + } + for record in &manifest.color_selector_executions { + validate_evaluator_payload_references(artifact, evaluator_root, record.execution.as_ref())?; + } + Ok(()) +} + +pub(super) fn validate_evaluator_reference( + artifact: &VerifiedArtifact, + relative_root: &Path, + evaluator: &EvaluatorManifest, +) -> RusticolResult<()> { + match evaluator { + EvaluatorManifest::SymjitApplication { + application_path, + plane_application, + evaluator_state_path, + .. + } => { + validate_evaluator_state_path(artifact, relative_root, application_path)?; + if let Some(plane) = plane_application { + validate_evaluator_state_path(artifact, relative_root, &plane.application_path)?; + } + if let Some(path) = evaluator_state_path { + validate_evaluator_state_path(artifact, relative_root, path)?; + } + Ok(()) + } + EvaluatorManifest::Jit { + evaluator_state_path, + .. + } => validate_evaluator_state_path(artifact, relative_root, evaluator_state_path), + EvaluatorManifest::CompiledComplex { + function_name, + input_len, + output_len, + library_path, + evaluator_state_path, + native_direct_application, + .. + } => { + validate_evaluator_state_path(artifact, relative_root, library_path)?; + if let Some(path) = evaluator_state_path { + validate_evaluator_state_path(artifact, relative_root, path)?; + } + if let Some(application) = native_direct_application { + application.validate(function_name, *input_len, *output_len)?; + validate_evaluator_state_path(artifact, relative_root, &application.source_path)?; + validate_evaluator_state_path(artifact, relative_root, &application.library_path)?; + } + Ok(()) + } + EvaluatorManifest::Chunked { chunks, .. } => { + for chunk in chunks { + validate_evaluator_reference(artifact, relative_root, chunk)?; + } + Ok(()) + } + } +} + +pub(super) fn validate_evaluator_state_path( + artifact: &VerifiedArtifact, + relative_root: &Path, + value: &str, +) -> RusticolResult<()> { + let path = relative_root.join(confined_internal_path(value, "evaluator-state path")?); + let path = path + .to_str() + .ok_or_else(|| RusticolError::security("evaluator-state path is not valid UTF-8"))?; + if artifact.has_evaluator_payload(path)? { + Ok(()) + } else { + Err(RusticolError::security(format!( + "evaluator reference {path:?} is not a declared loose or packed evaluator payload" + ))) + } +} + +pub(super) fn confined_internal_path<'a>( + value: &'a str, + description: &str, +) -> RusticolResult<&'a Path> { + let path = Path::new(value); + if value.is_empty() + || value.contains('\\') + || path.is_absolute() + || path + .components() + .any(|component| !matches!(component, std::path::Component::Normal(_))) + { + return Err(RusticolError::security(format!( + "{description} {value:?} is not a confined relative path" + ))); + } + Ok(path) +} + +pub(super) fn parse_reduction_group_id(value: &str) -> RusticolResult { + value + .parse::() + .or_else(|_| value.rsplit(':').next().unwrap_or(value).parse::()) + .map_err(|_| { + RusticolError::artifact(format!( + "reduction group id {value:?} does not contain the evaluator's numeric group id" + )) + }) +} + +pub(super) fn apply_process_permutation_metadata( + mut physics: ProcessPhysicsV1, + selection: &crate::ArtifactSelection, +) -> RusticolResult { + physics.validate()?; + let permutation = &selection.external_permutation; + let particle_count = physics.external_particles.len(); + if permutation.len() != particle_count + || selection.external_pdgs.len() != particle_count + || permutation.iter().copied().collect::>() + != (0..particle_count).collect::>() + { + return Err(RusticolError::integrity(format!( + "process selection {:?} does not define a complete particle permutation and PDG order", + selection.public_expression + ))); + } + if permutation + .iter() + .copied() + .enumerate() + .any(|(representative_index, public_index)| { + physics.external_particles[representative_index].role + != physics.external_particles[public_index].role + }) + { + return Err(RusticolError::compatibility(format!( + "process selection {:?} moves a particle across the incoming/outgoing boundary", + selection.public_expression + ))); + } + let mut expected_alias_pdgs = vec![0; particle_count]; + for (representative_index, alias_index) in permutation.iter().copied().enumerate() { + expected_alias_pdgs[alias_index] = physics.external_particles[representative_index].pdg; + } + // A process-group member keeps the representative flavours here; they + // are relabelled by `apply_process_group_metadata`. + if selection.process_group.is_none() && selection.external_pdgs != expected_alias_pdgs { + return Err(RusticolError::integrity(format!( + "process selection {:?} PDG order {:?} does not match the representative physics payload and permutation; expected {:?}", + selection.public_expression, selection.external_pdgs, expected_alias_pdgs + ))); + } + + let mut particles = vec![None; particle_count]; + for (representative_index, alias_index) in permutation.iter().copied().enumerate() { + let mut particle = physics.external_particles[representative_index].clone(); + particle.index = alias_index; + particle.label = alias_index + 1; + particle.momentum_slot = alias_index; + particles[alias_index] = Some(particle); + } + physics.external_particles = particles + .into_iter() + .map(|particle| particle.expect("validated permutation is complete")) + .collect(); + + let mut helicity_id_map = BTreeMap::new(); + let mut helicity_updates = Vec::with_capacity(physics.helicities.len()); + for helicity in &physics.helicities { + let mut alias_values = vec![0; particle_count]; + for (representative_index, alias_index) in permutation.iter().copied().enumerate() { + alias_values[alias_index] = helicity.values[representative_index]; + } + let alias_id = canonical_helicity_id(&alias_values); + helicity_id_map.insert(helicity.id.clone(), alias_id.clone()); + helicity_updates.push((alias_values, alias_id)); + } + for (helicity, (values, id)) in physics.helicities.iter_mut().zip(helicity_updates) { + let representative_id = remapped_alias_id( + &helicity_id_map, + &helicity.representative_id, + "helicity representative", + )?; + helicity.values = values; + helicity.id = id; + helicity.representative_id = representative_id; + } + + let mut color_id_map = BTreeMap::new(); + let mut color_updates = Vec::with_capacity(physics.color_components.len()); + for color in &physics.color_components { + match color { + PhysicsColorComponentV1::LcFlow(flow) => { + let alias_word = flow + .word + .iter() + .map(|label| { + let representative_index = label.checked_sub(1).ok_or_else(|| { + RusticolError::artifact("LC color-flow labels must be one-based") + })?; + let alias_index = + permutation.get(representative_index).ok_or_else(|| { + RusticolError::artifact( + "LC color-flow label exceeds alias permutation", + ) + })?; + Ok(alias_index + 1) + }) + .collect::>>()?; + let alias_id = canonical_color_id(&alias_word); + color_id_map.insert(flow.id.clone(), alias_id.clone()); + color_updates.push((Some(alias_word), alias_id)); + } + PhysicsColorComponentV1::ContractedColor(color) => { + color_id_map.insert(color.id.clone(), color.id.clone()); + color_updates.push((None, color.id.clone())); + } + } + } + for (color, (word, id)) in physics.color_components.iter_mut().zip(color_updates) { + match color { + PhysicsColorComponentV1::LcFlow(flow) => { + let representative_id = remapped_alias_id( + &color_id_map, + &flow.representative_id, + "color representative", + )?; + flow.word = word.expect("LC color update includes a word"); + flow.id = id; + flow.representative_id = representative_id; + } + PhysicsColorComponentV1::ContractedColor(color) => { + if word.is_some() { + return Err(RusticolError::integrity( + "contracted color alias update unexpectedly contains an LC word", + )); + } + color.id = id; + } + } + } + + for group in &mut physics.reduction.groups { + let representative_helicity_id = group.representative_helicity_id.clone(); + let representative_color_id = group.representative_color_id.clone(); + group.representative_helicity_id = remapped_alias_id( + &helicity_id_map, + &representative_helicity_id, + "reduction representative helicity", + )?; + group.representative_color_id = remapped_alias_id( + &color_id_map, + &representative_color_id, + "reduction representative color", + )?; + for id in &mut group.physical_helicity_ids { + *id = remapped_alias_id(&helicity_id_map, id, "reduction physical helicity")?; + } + for id in &mut group.physical_color_ids { + *id = remapped_alias_id(&color_id_map, id, "reduction physical color")?; + } + } + + physics.process_id = selection.requested_id.clone(); + physics.process = selection.public_expression.clone(); + if physics + .external_particles + .iter() + .map(|particle| particle.pdg) + .ne(expected_alias_pdgs.iter().copied()) + { + return Err(RusticolError::integrity(format!( + "process selection {:?} external particle metadata does not match its public PDG order", + selection.public_expression + ))); + } + physics.validate()?; + Ok(physics) +} + +/// Relabel public metadata of a selected process-group member. +/// +/// `physics` is the representative metadata already remapped to the public +/// order by [`apply_process_permutation_metadata`]; the execution lanes keep +/// using it. The returned copy carries the member's flavours and, for a +/// charge-conjugated relation, reversed LC words whose IDs map to the +/// execution IDs through [`charge_conjugate_color_id`]. +pub(super) fn apply_process_group_metadata( + physics: &ProcessPhysicsV1, + selection: &crate::ArtifactSelection, +) -> RusticolResult { + let mut physics = physics.clone(); + let Some(group) = &selection.process_group else { + return Ok(physics); + }; + if group.external_particles.len() != physics.external_particles.len() + || selection.external_pdgs.len() != physics.external_particles.len() + { + return Err(RusticolError::integrity(format!( + "process-group member {:?} does not have one flavour per external particle", + selection.requested_id + ))); + } + for ((particle, name), pdg) in physics + .external_particles + .iter_mut() + .zip(&group.external_particles) + .zip(&selection.external_pdgs) + { + particle.particle.clone_from(name); + particle.pdg = *pdg; + } + if group.conjugated { + let conjugate = |id: &mut String| *id = charge_conjugate_color_id(id).into_owned(); + for color in &mut physics.color_components { + if let PhysicsColorComponentV1::LcFlow(flow) = color { + flow.word.reverse(); + conjugate(&mut flow.id); + conjugate(&mut flow.representative_id); + } + } + for reduction in &mut physics.reduction.groups { + conjugate(&mut reduction.representative_color_id); + reduction.physical_color_ids.iter_mut().for_each(conjugate); + } + } + physics.validate()?; + Ok(physics) +} + +/// Map an LC color-flow ID through global charge conjugation, which reverses +/// the flow word. The map is an involution; other IDs are unchanged. +pub(super) fn charge_conjugate_color_id(id: &str) -> Cow<'_, str> { + match id.strip_prefix("flow:") { + Some(word) if word != "singlet" && word.contains(',') => Cow::Owned(format!( + "flow:{}", + word.rsplit(',').collect::>().join(",") + )), + _ => Cow::Borrowed(id), + } +} + +fn canonical_helicity_id(values: &[i32]) -> String { + format!( + "h:{}", + values + .iter() + .map(|value| format!("{value:+}")) + .collect::>() + .join(",") + ) +} + +fn canonical_color_id(word: &[usize]) -> String { + if word.is_empty() { + return "flow:singlet".to_string(); + } + format!( + "flow:{}", + word.iter() + .map(usize::to_string) + .collect::>() + .join(",") + ) +} + +fn remapped_alias_id( + ids: &BTreeMap, + representative_id: &str, + description: &str, +) -> RusticolResult { + ids.get(representative_id).cloned().ok_or_else(|| { + RusticolError::integrity(format!( + "{description} {representative_id:?} is absent from alias remapping" + )) + }) +} + +pub(super) fn selector_set( + ids: Option<&[String]>, + kind: &str, +) -> Result>, RusticolError> { + let Some(ids) = ids else { + return Ok(None); + }; + if ids.is_empty() { + return Err(RusticolError::selector(format!( + "resolved {kind} selection must not be empty" + ))); + } + let selected = ids.iter().cloned().collect::>(); + if selected.len() != ids.len() { + return Err(RusticolError::selector(format!( + "resolved {kind} selection contains duplicate ids" + ))); + } + Ok(Some(selected)) +} + +#[cfg(test)] +mod portable_execution_tests { + use super::*; + + fn portable_fixture_value() -> Value { + serde_json::from_slice(include_bytes!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../../../src/pyamplicol/assets/selftest/portable-64le/artifact/processes/", + "d_dbar_to_z/execution.json" + ))) + .expect("parse portable compiled execution fixture") + } + + fn portable_fixture() -> ExecutionManifest { + serde_json::from_value(portable_fixture_value()) + .expect("decode portable compiled execution fixture") + } + + fn replace_symjit_optimization_level(value: &mut Value, level: u8) { + match value { + Value::Array(values) => { + for value in values { + replace_symjit_optimization_level(value, level); + } + } + Value::Object(object) => { + if object.get("kind").and_then(Value::as_str) + == Some("symjit-application-evaluator") + { + object.insert("optimization_level".to_string(), level.into()); + if let Some(plane) = object + .get_mut("plane_application") + .and_then(Value::as_object_mut) + { + plane.insert("optimization_level".to_string(), level.into()); + } + } + if let Some(leaves) = object.get_mut("leaves").and_then(Value::as_array_mut) { + for leaf in leaves { + if let Some(leaf) = leaf.as_object_mut() + && leaf.contains_key("optimization_level") + { + leaf.insert("optimization_level".to_string(), level.into()); + } + } + } + for value in object.values_mut() { + replace_symjit_optimization_level(value, level); + } + } + _ => {} + } + } + + #[test] + fn compiled_portable_contract_accepts_o1_o2_and_rejects_o0_o3() { + validate_compiled_portable_jit(&portable_fixture()) + .expect("canonical O2 fixture is portable"); + + let fixture = portable_fixture_value(); + let mut o1 = fixture.clone(); + replace_symjit_optimization_level(&mut o1, 1); + let o1: ExecutionManifest = serde_json::from_value(o1).expect("parse modified O1 fixture"); + validate_compiled_portable_jit(&o1).expect("canonical O1 fixture is portable"); + + for level in [0, 3] { + let mut value = fixture.clone(); + replace_symjit_optimization_level(&mut value, level); + let manifest: ExecutionManifest = + serde_json::from_value(value).expect("parse modified fixture"); + let error = validate_compiled_portable_jit(&manifest).unwrap_err(); + assert_eq!(error.kind(), crate::RusticolErrorKind::Compatibility); + assert!(error.to_string().contains("O0, O3, C++, ASM")); + } + } +} + +#[cfg(all(test, any(feature = "f64-compiled", feature = "f64-symjit")))] +mod recurrence_dispatch_tests { + use super::*; + + #[test] + fn generic_execution_dispatch_accepts_the_strict_recurrence_manifest() { + let value = super::super::recurrence_manifest::tests::manifest(); + let outer = super::super::recurrence_manifest::tests::outer(); + let bytes = serde_json::to_vec(&value).unwrap(); + + let parsed = + parse_execution_payload_variant(&bytes, "processes/x_to_x/execution.json", &outer) + .unwrap(); + + let LoadedExecutionManifest::Recurrence(manifest) = parsed else { + panic!("recurrence execution kind was dispatched to another manifest parser"); + }; + assert_eq!(manifest.key, outer.id); + assert_eq!(manifest.process, outer.expression); + assert_eq!( + manifest.required_runtime_capabilities, + outer.required_runtime_capabilities + ); + ensure_runtime_capabilities_supported( + manifest + .required_runtime_capabilities + .iter() + .map(String::as_str), + ) + .unwrap(); + } +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/engine/compiled_direct_prototype.rs b/pyamplicol/rust/crates/rusticol-core/src/engine/compiled_direct_prototype.rs new file mode 100644 index 0000000000..b532dabfbc --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/engine/compiled_direct_prototype.rs @@ -0,0 +1,5782 @@ +// SPDX-License-Identifier: 0BSD + +//! Production compiled Direct-Arena execution. +//! +//! This module consumes the existing stage/chunk manifests, lowers each +//! already-fused SymJIT O3 leaf to DirectApplication v3 at load time, and +//! executes the unchanged leaf schedule against persistent component-major +//! planes. Canonical manifest component IDs are cold-bound to a stage-granular +//! physical layout: values with disjoint whole-stage lifetimes may reuse a +//! plane while every Arena-v1 stage retains its no-alias contract. + +#![allow(dead_code)] + +use std::cmp::Reverse; +use std::collections::BinaryHeap; + +#[cfg(feature = "f64-symjit")] +use std::path::PathBuf; +use std::ptr; + +#[cfg(feature = "f64-compiled")] +use super::evaluator::native_compiled_direct::{ + BoundNativeCompiledDirectStage, LoadedNativeCompiledDirectStage, + NativeCompiledDirectArenaPlane, NativeCompiledDirectOutputBinding, + NativeCompiledDirectPlaneBinding, NativeCompiledDirectScalarBinding, +}; +#[cfg(feature = "f64-symjit")] +use super::evaluator::symjit_compiled_direct::{ + BoundSymjitCompiledDirectStage, CompiledDirectArenaPlane, CompiledDirectOutputBinding, + CompiledDirectPlaneBinding, CompiledDirectScalarBinding, CompiledDirectSourceInputBinding, + LoadedSymjitCompiledDirectStage, +}; +#[cfg(feature = "f64-symjit")] +use super::evaluator::{SymjitApplicationMetadata, validate_manifest_metadata}; +use super::sources::{ResolvedSourceWavefunction, RuntimeSourceState}; +use super::*; +#[cfg(test)] +use crate::direct_arena::checked_aligned_point_stride; +use crate::direct_arena::{ + AlignedF64Buffer, DirectAmplitudePlanes, DirectArenaAllocationCounters, + DirectArenaTrafficCounters, DirectArenaWorkspace, DirectMomentumView, DirectParameterView, + deterministic_point_tile_size_with_cache_footprint, +}; + +// Fused applications repeatedly traverse many split-complex parent/output +// planes. Keeping a small, fixed power-of-two point tile preserves that +// cross-stage locality on both cache-rich and cache-poor CPUs without an +// online host-specific tuner. +const COMPILED_DIRECT_LOCALITY_POINT_CAP: u32 = 32; +const COMPILED_DIRECT_CACHE_TARGET_BYTES: usize = 4 * 1024 * 1024; + +fn compiled_direct_cache_target_bytes() -> usize { + #[cfg(test)] + if let Some(value) = std::env::var_os("RUSTICOL_TEST_COMPILED_DIRECT_CACHE_TARGET_BYTES") { + let value = value + .to_str() + .expect("compiled Direct test cache target must be UTF-8"); + if value == "hard-budget" { + return usize::MAX; + } + return value + .parse() + .expect("compiled Direct test cache target must be bytes or hard-budget"); + } + COMPILED_DIRECT_CACHE_TARGET_BYTES +} + +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub(crate) struct CompiledDirectPrototypeTraffic { + /// Direct source-wavefunction writes into canonical current planes. + pub(crate) source_fill_bytes: u64, + /// Direct crossed-momentum writes into canonical momentum planes. + pub(crate) momentum_fill_bytes: u64, + /// Point-independent model-scalar writes. + pub(crate) parameter_fill_bytes: u64, + /// Internal persistent P-kernel broadcast-plane refresh traffic. + pub(crate) scalar_broadcast_fill_bytes: u64, + /// Stale-safety clears of amplitude planes not overwritten by a schedule. + pub(crate) amplitude_clear_bytes: u64, + /// Developer-oracle row-major to arena boundary traffic. + pub(crate) boundary_input_bytes: u64, + /// Explicit developer-only current extraction traffic. + pub(crate) boundary_current_output_bytes: u64, + /// Explicit developer-only amplitude extraction traffic. + pub(crate) boundary_amplitude_output_bytes: u64, + /// Hot compiled-leaf calls. Its forbidden traffic byte fields must remain + /// zero even though call/row/point counts advance structurally. + pub(crate) leaf: DirectArenaTrafficCounters, +} + +#[derive(Clone, Debug)] +struct DirectLeafPlan { + input_current_components: Box<[usize]>, + output_current_components: Box<[usize]>, + output_amplitude_components: Box<[usize]>, + scalar_values_per_point: usize, + persistent_scalar_values_per_point: usize, +} + +/// Call-local memo for correlated spin assignments at identical momenta and +/// model parameters. It is never read or allocated by ordinary evaluation. +#[derive(Default)] +pub(crate) struct CorrelatedStageMemo { + leaves: Vec>, + #[cfg(test)] + reused_leaves: usize, +} + +struct CorrelatedLeafMemo { + inputs: Vec, + current_outputs: Vec<[f64; 2]>, + amplitude_outputs: Vec<[f64; 2]>, +} + +// Leaves are cold-built and kept inline to avoid a separate allocation and +// indirection for each warmed stage call. +#[allow(clippy::large_enum_variant)] +enum LoadedCompiledDirectLeaf { + #[cfg(feature = "f64-symjit")] + Symjit(LoadedSymjitCompiledDirectStage), + #[cfg(feature = "f64-compiled")] + Native(LoadedNativeCompiledDirectStage), +} + +#[allow(clippy::large_enum_variant)] +enum BoundCompiledDirectLeaf { + #[cfg(feature = "f64-symjit")] + Symjit(BoundSymjitCompiledDirectStage), + #[cfg(feature = "f64-compiled")] + Native(BoundNativeCompiledDirectStage), +} + +impl LoadedCompiledDirectLeaf { + fn persistent_scalar_values_per_point(&self) -> usize { + match self { + #[cfg(feature = "f64-symjit")] + Self::Symjit(leaf) => leaf.broadcast_plane_count(), + #[cfg(feature = "f64-compiled")] + Self::Native(_) => 0, + } + } + + unsafe fn bind( + self, + arena: crate::direct_arena::DirectArenaView, + momenta: DirectMomentumView, + parameters: DirectParameterView, + _zero_plane: &AlignedF64Buffer, + current_component_map: &[u32], + ) -> RusticolResult { + match self { + #[cfg(feature = "f64-symjit")] + Self::Symjit(leaf) => { + // SAFETY: forwarded from the engine's fixed-allocation bind. + unsafe { + leaf.bind_with_current_map( + arena, + momenta, + parameters, + _zero_plane, + current_component_map, + ) + } + .map(BoundCompiledDirectLeaf::Symjit) + } + #[cfg(feature = "f64-compiled")] + Self::Native(leaf) => { + // SAFETY: forwarded from the engine's fixed-allocation bind. + unsafe { + leaf.bind_with_current_map(arena, momenta, parameters, current_component_map) + } + .map(BoundCompiledDirectLeaf::Native) + } + } + } +} + +impl BoundCompiledDirectLeaf { + fn allocation_counters(&self) -> DirectArenaAllocationCounters { + match self { + #[cfg(feature = "f64-symjit")] + Self::Symjit(leaf) => leaf.allocation_counters(), + #[cfg(feature = "f64-compiled")] + Self::Native(_) => DirectArenaAllocationCounters::default(), + } + } + + fn evaluate(&mut self, point_start: u32, point_count: u32) -> RusticolResult { + match self { + #[cfg(feature = "f64-symjit")] + Self::Symjit(leaf) => leaf.evaluate(point_start, point_count), + #[cfg(feature = "f64-compiled")] + Self::Native(leaf) => { + leaf.evaluate(point_start, point_count)?; + Ok(0) + } + } + } +} + +struct LoadedStage { + leaves: Vec, + leaf_plans: Vec, +} + +struct BoundStage { + leaves: Vec, + leaf_plans: Vec, +} + +const UNMAPPED_CURRENT_COMPONENT: u32 = u32::MAX; + +#[derive(Clone, Debug)] +struct CompiledDirectCurrentLayout { + canonical_to_physical: Box<[u32]>, + physical_component_count: u32, + live_component_count: usize, +} + +impl CompiledDirectCurrentLayout { + fn identity(canonical_component_count: usize) -> RusticolResult { + let physical_component_count = u32::try_from(canonical_component_count).map_err(|_| { + RusticolError::integrity("compiled Direct-Arena current plane count exceeds u32") + })?; + Ok(Self { + canonical_to_physical: (0..physical_component_count) + .collect::>() + .into_boxed_slice(), + physical_component_count, + live_component_count: canonical_component_count, + }) + } + + fn physical_component(&self, canonical_component: usize) -> RusticolResult { + self.canonical_to_physical + .get(canonical_component) + .copied() + .filter(|component| *component != UNMAPPED_CURRENT_COMPONENT) + .ok_or_else(|| { + RusticolError::integrity(format!( + "compiled Direct-Arena canonical current component {canonical_component} has \ + no physical liveness assignment" + )) + }) + } + + fn physical_map(&self) -> &[u32] { + &self.canonical_to_physical + } + + fn is_identity(&self) -> bool { + self.live_component_count == self.canonical_to_physical.len() + && self.physical_component_count as usize == self.canonical_to_physical.len() + && self + .canonical_to_physical + .iter() + .copied() + .enumerate() + .all(|(canonical, physical)| physical as usize == canonical) + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum CompiledDirectCurrentLayoutPolicy { + Identity { tile_capacity: u32 }, + StageLivenessProduction, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) struct CompiledDirectTileSizing { + pub(crate) effective_tile_capacity: usize, + pub(crate) reduction_tile_capacity: usize, + pub(crate) physical_scalar_values_per_point: usize, + pub(crate) persistent_scalar_values_per_point: usize, + pub(crate) hot_scalar_values_per_point: usize, + execution_scalar_values_per_point: usize, + pub(crate) source_scalar_values_per_point: usize, + pub(crate) reduction_scalar_values_per_point: usize, +} + +impl CompiledDirectTileSizing { + fn reduction_tile_capacity_for_footprint( + self, + reduction_footprint: CompiledDirectReductionFootprint, + ) -> RusticolResult { + let Some(reduction_scalars) = reduction_footprint.hot_scalar_values_per_point() else { + return Ok(self.reduction_tile_capacity); + }; + // A selected routed total only touches its selected source/target + // components. The all-selector footprint remains the default for + // other reducers. Never exceed the already allocated execution tile: + // this changes cache locality, not the arena's allocation bound. + let capacity = u32::try_from(self.effective_tile_capacity).map_err(|_| { + RusticolError::integrity("compiled Direct-Arena tile capacity exceeds u32") + })?; + Ok(deterministic_point_tile_size_with_cache_footprint( + capacity, + usize::MAX, + compiled_direct_cache_target_bytes(), + self.physical_scalar_values_per_point, + self.execution_scalar_values_per_point + .max(reduction_scalars), + )? as usize) + } +} + +trait DirectStagePlan { + fn leaf_plans(&self) -> &[DirectLeafPlan]; +} + +impl DirectStagePlan for LoadedStage { + fn leaf_plans(&self) -> &[DirectLeafPlan] { + &self.leaf_plans + } +} + +impl DirectStagePlan for BoundStage { + fn leaf_plans(&self) -> &[DirectLeafPlan] { + &self.leaf_plans + } +} + +#[allow(clippy::too_many_arguments)] +fn compiled_direct_tile_sizing( + stages: &[LoadedStage], + amplitude: &LoadedStage, + current_layout: &CompiledDirectCurrentLayout, + source_components: &[usize], + momentum_component_count: usize, + amplitude_component_count: usize, + reduction_footprint: CompiledDirectReductionFootprint, + layout_policy: CompiledDirectCurrentLayoutPolicy, +) -> RusticolResult { + let persistent_scalar_values_per_point = stages + .iter() + .flat_map(|stage| stage.leaf_plans.iter()) + .chain(amplitude.leaf_plans.iter()) + .try_fold(0usize, |total, leaf| { + total + .checked_add(leaf.persistent_scalar_values_per_point) + .ok_or_else(|| { + RusticolError::integrity( + "compiled Direct-Arena persistent scalar footprint overflows", + ) + }) + })?; + let physical_scalar_values_per_point = usize::try_from(current_layout.physical_component_count) + .ok() + .and_then(|value| value.checked_mul(2)) + .and_then(|value| value.checked_add(momentum_component_count)) + .and_then(|value| { + amplitude_component_count + .checked_mul(2) + .and_then(|amplitudes| value.checked_add(amplitudes)) + }) + // The shared zero plane is point-pitched even though every leaf + // aliases the same immutable storage. + .and_then(|value| value.checked_add(1)) + .and_then(|value| value.checked_add(persistent_scalar_values_per_point)) + .ok_or_else(|| { + RusticolError::integrity("compiled Direct-Arena physical per-point shape overflows") + })?; + let source_scalar_values_per_point = source_components + .len() + .checked_mul(2) + .and_then(|value| value.checked_add(momentum_component_count)) + .and_then(|value| value.checked_add(1)) + .ok_or_else(|| { + RusticolError::integrity("compiled Direct-Arena source working set overflows") + })?; + let maximum_leaf_scalar_values_per_point = stages + .iter() + .flat_map(|stage| stage.leaf_plans.iter()) + .chain(amplitude.leaf_plans.iter()) + .map(|leaf| leaf.scalar_values_per_point) + .max() + .ok_or_else(|| { + RusticolError::integrity( + "compiled Direct-Arena production schedule has no evaluator leaves", + ) + })?; + let authenticated_reduction_scalar_values_per_point = + reduction_footprint.hot_scalar_values_per_point(); + let execution_scalar_values_per_point = + maximum_leaf_scalar_values_per_point.max(source_scalar_values_per_point); + let hot_scalar_values_per_point = execution_scalar_values_per_point + .max(authenticated_reduction_scalar_values_per_point.unwrap_or(0)); + let (effective_tile_capacity, reduction_tile_capacity) = match layout_policy { + // This developer/test contract remains exact: the adaptive locality + // policy does not change it. + CompiledDirectCurrentLayoutPolicy::Identity { tile_capacity } => { + let capacity = usize::try_from(tile_capacity).map_err(|_| { + RusticolError::integrity("compiled Direct-Arena identity tile exceeds usize") + })?; + (capacity, capacity) + } + CompiledDirectCurrentLayoutPolicy::StageLivenessProduction => { + if authenticated_reduction_scalar_values_per_point.is_none() { + // Adaptive production tiling is only valid when every + // reducer reachable from this engine has a static footprint. + // Still route the one-point fallback through the common + // selector so its padded physical allocation remains checked. + let capacity = deterministic_point_tile_size_with_cache_footprint( + 1, + usize::MAX, + usize::MAX, + physical_scalar_values_per_point, + hot_scalar_values_per_point, + )? as usize; + (capacity, capacity) + } else { + let execution_capacity = deterministic_point_tile_size_with_cache_footprint( + COMPILED_DIRECT_LOCALITY_POINT_CAP, + usize::MAX, + compiled_direct_cache_target_bytes(), + physical_scalar_values_per_point, + execution_scalar_values_per_point, + )? as usize; + let reduction_capacity = deterministic_point_tile_size_with_cache_footprint( + COMPILED_DIRECT_LOCALITY_POINT_CAP, + usize::MAX, + compiled_direct_cache_target_bytes(), + physical_scalar_values_per_point, + hot_scalar_values_per_point, + )? as usize; + (execution_capacity, reduction_capacity) + } + } + }; + Ok(CompiledDirectTileSizing { + effective_tile_capacity, + reduction_tile_capacity, + physical_scalar_values_per_point, + persistent_scalar_values_per_point, + hot_scalar_values_per_point, + execution_scalar_values_per_point, + source_scalar_values_per_point, + reduction_scalar_values_per_point: authenticated_reduction_scalar_values_per_point + .unwrap_or(0), + }) +} + +/// Cold-bound canonical preorder leaf schedule. +/// +/// Fields remain private so arbitrary caller indices cannot bypass the +/// production runtime coverage validation that creates selector schedules. +pub(crate) struct CompiledDirectValidatedSchedule { + active_stage_leaves: Box<[Box<[usize]>]>, + active_amplitude_leaves: Box<[usize]>, + inactive_amplitude_components: Box<[usize]>, +} + +/// Persistent prototype executor for one compiled schedule. +/// +/// The descriptor objects contain pointers into the boxed arena and aligned +/// side buffers. Those allocations never resize or move after construction. +pub(crate) struct CompiledDirectEnginePrototype { + // Bound descriptors must drop before the allocations they address. Rust + // drops fields in declaration order, so keep these owners first. + stages: Vec, + amplitude: BoundStage, + arena: Box, + momenta: Box, + parameter_re: Box, + parameter_im: Box, + zero_plane: Box, + full_schedule: CompiledDirectValidatedSchedule, + helicity_color_schedules: + BTreeMap, Arc>>, + source_components: Box<[usize]>, + default_source_states: Box<[GenericSourceStateIrManifest]>, + source_wavefunction_scratch: Vec>, + current_layout: CompiledDirectCurrentLayout, + tile_sizing: CompiledDirectTileSizing, + value_component_count: usize, + momentum_component_count: usize, + model_parameter_count: usize, + amplitude_component_count: usize, + traffic: CompiledDirectPrototypeTraffic, + allocation_counters: DirectArenaAllocationCounters, +} + +impl CompiledDirectEnginePrototype { + #[allow(clippy::too_many_arguments)] + pub(crate) fn load( + stages: &[GenericSerializedStageEvaluatorManifest], + amplitude: &GenericSerializedStageEvaluatorManifest, + payloads: &EvaluatorPayloadStore, + source_components: &[usize], + source_scratch_len: usize, + value_component_count: usize, + momentum_component_count: usize, + model_parameter_count: usize, + amplitude_component_count: usize, + tile_capacity: u32, + ) -> RusticolResult { + Self::load_with_current_layout_policy( + stages, + amplitude, + payloads, + source_components, + source_scratch_len, + value_component_count, + momentum_component_count, + model_parameter_count, + amplitude_component_count, + CompiledDirectReductionFootprint::default(), + CompiledDirectCurrentLayoutPolicy::Identity { tile_capacity }, + ) + } + + #[allow(clippy::too_many_arguments)] + fn load_with_current_layout_policy( + stages: &[GenericSerializedStageEvaluatorManifest], + amplitude: &GenericSerializedStageEvaluatorManifest, + payloads: &EvaluatorPayloadStore, + source_components: &[usize], + source_scratch_len: usize, + value_component_count: usize, + momentum_component_count: usize, + model_parameter_count: usize, + amplitude_component_count: usize, + reduction_footprint: CompiledDirectReductionFootprint, + layout_policy: CompiledDirectCurrentLayoutPolicy, + ) -> RusticolResult { + if value_component_count == 0 || amplitude_component_count == 0 { + return Err(RusticolError::integrity( + "compiled Direct-Arena prototype requires current and amplitude planes", + )); + } + if !momentum_component_count.is_multiple_of(4) { + return Err(RusticolError::integrity( + "compiled Direct-Arena momentum component count must be a multiple of four", + )); + } + if source_components.is_empty() || source_scratch_len == 0 { + return Err(RusticolError::integrity( + "compiled Direct-Arena requires canonical source-component ownership", + )); + } + let mut previous_source = None; + for &component in source_components { + if component >= value_component_count + || previous_source.is_some_and(|previous| previous >= component) + { + return Err(RusticolError::integrity( + "compiled Direct-Arena source components are not sorted, unique, and in bounds", + )); + } + previous_source = Some(component); + } + let structural_zero_components = + canonical_structural_zero_components(stages, source_components, value_component_count)?; + let loaded_stages = stages + .iter() + .map(|stage| { + load_stage( + stage, + false, + payloads, + value_component_count, + momentum_component_count, + amplitude_component_count, + &structural_zero_components, + ) + }) + .collect::>>()?; + let loaded_amplitude = load_stage( + amplitude, + true, + payloads, + value_component_count, + momentum_component_count, + amplitude_component_count, + &structural_zero_components, + )?; + + let full_stage_leaves = loaded_stages + .iter() + .map(|stage| (0..stage.leaves.len()).collect::>()) + .collect::>(); + let full_amplitude_leaves = (0..loaded_amplitude.leaves.len()).collect::>(); + let full_schedule = validate_schedule( + &loaded_stages, + &loaded_amplitude, + value_component_count, + source_components, + amplitude_component_count, + &full_stage_leaves, + &full_amplitude_leaves, + )?; + let current_layout = match layout_policy { + CompiledDirectCurrentLayoutPolicy::Identity { .. } => { + CompiledDirectCurrentLayout::identity(value_component_count)? + } + CompiledDirectCurrentLayoutPolicy::StageLivenessProduction => { + derive_stage_current_layout( + &loaded_stages, + &loaded_amplitude, + source_components, + value_component_count, + )? + } + }; + let tile_sizing = compiled_direct_tile_sizing( + &loaded_stages, + &loaded_amplitude, + ¤t_layout, + source_components, + momentum_component_count, + amplitude_component_count, + reduction_footprint, + layout_policy, + )?; + let tile_capacity = u32::try_from(tile_sizing.effective_tile_capacity).map_err(|_| { + RusticolError::integrity("compiled Direct-Arena tile capacity exceeds u32") + })?; + let mut arena = Box::new(DirectArenaWorkspace::new( + current_layout.physical_component_count, + u32::try_from(amplitude_component_count).map_err(|_| { + RusticolError::integrity("compiled Direct-Arena amplitude plane count exceeds u32") + })?, + tile_capacity, + )?); + arena.begin_tile(tile_capacity)?; + let stride = arena.point_stride() as usize; + let momenta_len = momentum_component_count + .checked_mul(stride) + .ok_or_else(|| RusticolError::integrity("compiled momentum arena length overflows"))?; + let mut momenta = Box::new(AlignedF64Buffer::zeroed( + momenta_len, + "compiled prototype momenta", + )?); + let mut parameter_re = Box::new(AlignedF64Buffer::zeroed( + model_parameter_count, + "compiled prototype parameter real", + )?); + let mut parameter_im = Box::new(AlignedF64Buffer::zeroed( + model_parameter_count, + "compiled prototype parameter imaginary", + )?); + let zero_plane = Box::new(AlignedF64Buffer::zeroed( + stride, + "compiled prototype shared zero plane", + )?); + + let arena_view = arena.view()?; + let momentum_view = DirectMomentumView { + values: momenta.as_mut_ptr(), + scalar_len: momenta.len() as u64, + form_count: u32::try_from(momentum_component_count / 4).map_err(|_| { + RusticolError::integrity("compiled momentum form count exceeds u32") + })?, + lorentz_component_count: 4, + point_stride: arena_view.point_stride, + }; + let parameter_view = DirectParameterView { + values_re: if parameter_re.is_empty() { + ptr::null() + } else { + parameter_re.as_mut_ptr() + }, + values_im: if parameter_im.is_empty() { + ptr::null() + } else { + parameter_im.as_mut_ptr() + }, + value_count: u32::try_from(model_parameter_count).map_err(|_| { + RusticolError::integrity("compiled model parameter count exceeds u32") + })?, + }; + + // SAFETY: arena and side buffers are fixed-size heap allocations owned + // by the returned executor. No field resizes them, and evaluate methods + // require exclusive access to the executor. + let bind_stage = |loaded: LoadedStage| -> RusticolResult { + Ok(BoundStage { + leaves: loaded + .leaves + .into_iter() + .map(|leaf| unsafe { + leaf.bind( + arena_view, + momentum_view, + parameter_view, + &zero_plane, + current_layout.physical_map(), + ) + }) + .collect::>>()?, + leaf_plans: loaded.leaf_plans, + }) + }; + let stages = loaded_stages + .into_iter() + .map(bind_stage) + .collect::>>()?; + let amplitude = bind_stage(loaded_amplitude)?; + let allocation_counters = [ + arena.allocation_counters(), + momenta.allocation_counters(), + parameter_re.allocation_counters(), + parameter_im.allocation_counters(), + zero_plane.allocation_counters(), + DirectArenaAllocationCounters { + allocation_requests: 1, + requested_bytes: u64::try_from( + source_scratch_len.saturating_mul(std::mem::size_of::>()), + ) + .unwrap_or(u64::MAX), + }, + ] + .into_iter() + .try_fold( + DirectArenaAllocationCounters::default(), + |total, counters| total.checked_add(counters), + )?; + let allocation_counters = stages + .iter() + .flat_map(|stage| stage.leaves.iter()) + .chain(amplitude.leaves.iter()) + .try_fold(allocation_counters, |total, leaf| { + total.checked_add(leaf.allocation_counters()) + })?; + + Ok(Self { + stages, + amplitude, + arena, + momenta, + parameter_re, + parameter_im, + zero_plane, + full_schedule, + helicity_color_schedules: BTreeMap::new(), + source_components: source_components.to_vec().into_boxed_slice(), + default_source_states: Box::new([]), + source_wavefunction_scratch: vec![c64(0.0, 0.0); source_scratch_len], + current_layout, + tile_sizing, + value_component_count, + momentum_component_count, + model_parameter_count, + amplitude_component_count, + traffic: CompiledDirectPrototypeTraffic::default(), + allocation_counters, + }) + } + + #[allow(clippy::too_many_arguments)] + pub(crate) fn load_production( + stages: &[GenericSerializedStageEvaluatorManifest], + amplitude: &GenericSerializedStageEvaluatorManifest, + payloads: &EvaluatorPayloadStore, + sources: &[GenericSourceRecordManifest], + value_component_count: usize, + momentum_component_count: usize, + model_parameter_count: usize, + amplitude_component_count: usize, + reduction_footprint: CompiledDirectReductionFootprint, + ) -> RusticolResult { + let (source_components, source_scratch_len) = + canonical_source_layout(sources, value_component_count)?; + let mut direct = Self::load_with_current_layout_policy( + stages, + amplitude, + payloads, + &source_components, + source_scratch_len, + value_component_count, + momentum_component_count, + model_parameter_count, + amplitude_component_count, + reduction_footprint, + CompiledDirectCurrentLayoutPolicy::StageLivenessProduction, + )?; + direct.default_source_states = sources + .iter() + .map(ExecutionRuntime::default_runtime_source_state) + .collect::>>()? + .into_boxed_slice(); + Ok(direct) + } + + pub(crate) const fn traffic(&self) -> CompiledDirectPrototypeTraffic { + self.traffic + } + + pub(crate) const fn allocation_counters(&self) -> DirectArenaAllocationCounters { + self.allocation_counters + } + + pub(crate) const fn tile_capacity(&self) -> usize { + self.arena.tile_capacity() as usize + } + + pub(crate) const fn reduction_tile_capacity(&self) -> usize { + self.tile_sizing.reduction_tile_capacity + } + + pub(crate) fn reduction_tile_capacity_for_footprint( + &self, + reduction_footprint: CompiledDirectReductionFootprint, + ) -> RusticolResult { + self.tile_sizing + .reduction_tile_capacity_for_footprint(reduction_footprint) + } + + pub(crate) const fn tile_sizing(&self) -> CompiledDirectTileSizing { + self.tile_sizing + } + + /// Fill one active tile directly from the public borrowed momentum view + /// and immutable runtime metadata. No point-major global state exists. + #[allow(clippy::too_many_arguments)] + pub(crate) fn begin_tile_from_inputs( + &mut self, + batch: F64MomentumBatchView<'_>, + sources: &[GenericSourceRecordManifest], + source_states: Option<&[RuntimeSourceState]>, + external_count: usize, + particle_masses: &BTreeMap, + momentum_slots: &[GenericMomentumSlotManifest], + external_is_initial: &[bool], + model_parameter_values: &[f64], + ) -> RusticolResult<()> { + let point_count = batch.point_count(); + if point_count == 0 || point_count > self.tile_capacity() { + return Err(RusticolError::invalid_argument(format!( + "compiled Direct-Arena input tile has {point_count} points, expected 1..={}", + self.tile_capacity() + ))); + } + if batch.external_count() != external_count { + return Err(RusticolError::invalid_argument(format!( + "compiled Direct-Arena input has {} external legs, expected {external_count}", + batch.external_count() + ))); + } + if let Some(states) = source_states + && states.len() != sources.len() + { + return Err(RusticolError::invalid_argument(format!( + "compiled Direct-Arena source-state count {} does not match source count {}", + states.len(), + sources.len() + ))); + } + if source_states.is_none() && self.default_source_states.len() != sources.len() { + return Err(RusticolError::integrity(format!( + "compiled Direct-Arena default source-state count {} does not match source count {}", + self.default_source_states.len(), + sources.len() + ))); + } + if model_parameter_values.len() != self.model_parameter_count { + return Err(RusticolError::integrity(format!( + "compiled Direct-Arena model-scalar count {} does not match {}", + model_parameter_values.len(), + self.model_parameter_count + ))); + } + + self.arena + .begin_tile(u32::try_from(point_count).map_err(|_| { + RusticolError::invalid_argument("compiled Direct-Arena point count exceeds u32") + })?)?; + let stride = self.arena.point_stride() as usize; + { + let (current_re, current_im, _, _) = self.arena.split_slices_mut(); + let scratch = &mut self.source_wavefunction_scratch; + for (source_index, source) in sources.iter().enumerate() { + let start = source.value_slot.component_start; + let stop = source.value_slot.component_stop; + let dimension = stop.checked_sub(start).ok_or_else(|| { + RusticolError::integrity( + "compiled Direct-Arena source component range underflows", + ) + })?; + if dimension == 0 || stop > self.value_component_count || dimension > scratch.len() + { + return Err(RusticolError::integrity(format!( + "compiled Direct-Arena source {} has an invalid component range", + source.source_id + ))); + } + // Load-time current-layout validation covers every source; + // the physical mapping is immutable across tiles. + let physical_components = &self.current_layout.physical_map()[start..stop]; + let runtime_state = source_states.map(|states| &states[source_index]); + let factor = runtime_state.map_or(c64(1.0, 0.0), |state| state.factor); + if factor == c64(0.0, 0.0) { + for &component in physical_components { + let target = component as usize * stride; + current_re[target..target + point_count].fill(0.0); + current_im[target..target + point_count].fill(0.0); + } + continue; + } + let state = runtime_state.map_or_else( + || &self.default_source_states[source_index], + |state| &state.state, + ); + let output = &mut scratch[..dimension]; + let wavefunction = ResolvedSourceWavefunction::new( + source, + state, + external_count, + particle_masses, + dimension, + )?; + for point_index in 0..point_count { + let point = batch.point(point_index); + wavefunction.write(&point, output)?; + if factor != c64(1.0, 0.0) { + for value in output.iter_mut() { + *value *= factor; + } + } + for (&component, value) in physical_components.iter().zip(output.iter()) { + let target = component as usize * stride + point_index; + current_re[target] = value.re; + current_im[target] = value.im; + } + } + } + } + + fill_compiled_momentum_planes( + batch, + momentum_slots, + external_is_initial, + self.momentum_component_count, + stride, + self.momenta.as_mut_slice(), + )?; + for (index, value) in model_parameter_values.iter().copied().enumerate() { + self.parameter_re.as_mut_slice()[index] = value; + self.parameter_im.as_mut_slice()[index] = 0.0; + } + + self.traffic.source_fill_bytes = self.traffic.source_fill_bytes.saturating_add( + point_count + .saturating_mul(self.source_components.len()) + .saturating_mul(2 * std::mem::size_of::()) as u64, + ); + self.traffic.momentum_fill_bytes = self.traffic.momentum_fill_bytes.saturating_add( + point_count + .saturating_mul(momentum_slots.len().saturating_mul(4)) + .saturating_mul(std::mem::size_of::()) as u64, + ); + self.traffic.parameter_fill_bytes = self.traffic.parameter_fill_bytes.saturating_add( + self.model_parameter_count + .saturating_mul(2 * std::mem::size_of::()) as u64, + ); + Ok(()) + } + + /// Transpose the outer row-major state once at the engine boundary. + /// + /// This is not leaf gather traffic: every compiled leaf subsequently + /// consumes and produces the authoritative arena planes directly. + pub(crate) fn begin_tile_from_state( + &mut self, + point_count: usize, + global_parameter_count: usize, + state: &[Complex], + ) -> RusticolResult<()> { + if !self.current_layout.is_identity() { + return Err(RusticolError::invalid_argument( + "compiled Direct-Arena full-state developer input requires an identity current \ + layout", + )); + } + if point_count == 0 + || point_count > self.arena.tile_capacity() as usize + || state.len() != point_count.saturating_mul(global_parameter_count) + { + return Err(RusticolError::invalid_argument( + "compiled Direct-Arena prototype state shape is invalid", + )); + } + let required_parameters = self + .value_component_count + .checked_add(self.momentum_component_count) + .and_then(|value| value.checked_add(self.model_parameter_count)) + .ok_or_else(|| RusticolError::integrity("compiled parameter layout overflows"))?; + if global_parameter_count < required_parameters { + return Err(RusticolError::invalid_argument( + "compiled Direct-Arena prototype state omits required parameters", + )); + } + + self.arena.begin_tile(point_count as u32)?; + let stride = self.arena.point_stride() as usize; + { + let (current_re, current_im, amplitude_re, amplitude_im) = + self.arena.split_slices_mut(); + for component in 0..self.value_component_count { + let plane = component * stride; + for point in 0..point_count { + let value = state[point * global_parameter_count + component]; + current_re[plane + point] = value.re; + current_im[plane + point] = value.im; + } + } + for component in 0..self.amplitude_component_count { + let plane = component * stride; + amplitude_re[plane..plane + point_count].fill(0.0); + amplitude_im[plane..plane + point_count].fill(0.0); + } + } + let momentum_start = self.value_component_count; + for component in 0..self.momentum_component_count { + let plane = component * stride; + for point in 0..point_count { + self.momenta.as_mut_slice()[plane + point] = + state[point * global_parameter_count + momentum_start + component].re; + } + } + let parameter_start = momentum_start + self.momentum_component_count; + if self.model_parameter_count != 0 { + for index in 0..self.model_parameter_count { + let value = state[parameter_start + index]; + self.parameter_re.as_mut_slice()[index] = value.re; + self.parameter_im.as_mut_slice()[index] = value.im; + for point in 1..point_count { + if state[point * global_parameter_count + parameter_start + index] != value { + return Err(RusticolError::invalid_argument( + "compiled Direct-Arena model parameters vary across a point tile", + )); + } + } + } + } + let input_scalars = point_count + .checked_mul( + self.value_component_count + .checked_mul(2) + .and_then(|value| value.checked_add(self.momentum_component_count)) + .ok_or_else(|| { + RusticolError::integrity("compiled boundary input traffic overflows") + })?, + ) + .and_then(|value| value.checked_add(self.model_parameter_count.saturating_mul(2))) + .and_then(|value| value.checked_mul(std::mem::size_of::())) + .ok_or_else(|| RusticolError::integrity("compiled boundary input bytes overflow"))?; + self.traffic.boundary_input_bytes = self + .traffic + .boundary_input_bytes + .saturating_add(input_scalars as u64); + Ok(()) + } + + /// Test-only row-major source boundary for the production liveness layout. + /// + /// A liveness arena cannot represent arbitrary intermediate values at one + /// instant. It can, however, be initialized exactly like production by + /// clearing every physical plane and scattering only canonical sources. + #[cfg(test)] + fn begin_tile_from_state_sources_for_test( + &mut self, + point_count: usize, + global_parameter_count: usize, + state: &[Complex], + ) -> RusticolResult<()> { + if point_count == 0 + || point_count > self.arena.tile_capacity() as usize + || state.len() != point_count.saturating_mul(global_parameter_count) + { + return Err(RusticolError::invalid_argument( + "compiled Direct-Arena production test state shape is invalid", + )); + } + let required_parameters = self + .value_component_count + .checked_add(self.momentum_component_count) + .and_then(|value| value.checked_add(self.model_parameter_count)) + .ok_or_else(|| RusticolError::integrity("compiled parameter layout overflows"))?; + if global_parameter_count < required_parameters { + return Err(RusticolError::invalid_argument( + "compiled Direct-Arena production test state omits required parameters", + )); + } + self.arena.begin_tile(point_count as u32)?; + self.arena + .clear_current_active(0, self.current_layout.physical_component_count)?; + self.arena + .clear_amplitude_active(0, self.amplitude_component_count as u32)?; + let stride = self.arena.point_stride() as usize; + { + let (current_re, current_im, _, _) = self.arena.split_slices_mut(); + for canonical_component in self.source_components.iter().copied() { + let physical_component = + self.current_layout + .physical_component(canonical_component)? as usize; + let plane = physical_component * stride; + for point in 0..point_count { + let value = state[point * global_parameter_count + canonical_component]; + current_re[plane + point] = value.re; + current_im[plane + point] = value.im; + } + } + } + let momentum_start = self.value_component_count; + for component in 0..self.momentum_component_count { + let plane = component * stride; + for point in 0..point_count { + self.momenta.as_mut_slice()[plane + point] = + state[point * global_parameter_count + momentum_start + component].re; + } + } + let parameter_start = momentum_start + self.momentum_component_count; + for index in 0..self.model_parameter_count { + let value = state[parameter_start + index]; + self.parameter_re.as_mut_slice()[index] = value.re; + self.parameter_im.as_mut_slice()[index] = value.im; + for point in 1..point_count { + if state[point * global_parameter_count + parameter_start + index] != value { + return Err(RusticolError::invalid_argument( + "compiled Direct-Arena model parameters vary across a production test tile", + )); + } + } + } + Ok(()) + } + + /// Execute the existing full schedule. No input packet, gather, output + /// scatter, remap, or allocation occurs in this method. + pub(crate) fn evaluate_all(&mut self, point_count: usize) -> RusticolResult<()> { + evaluate_validated_schedule( + &mut self.stages, + &mut self.amplitude, + &self.full_schedule, + point_count_u32(point_count, self.arena.active_point_count())?, + &mut self.traffic, + ) + } + + /// Replace every retained source slot of a declared leg by the literal + /// public vector. Apply only the same SourceIR crossing phase as ordinary + /// wavefunctions; no projection, normalization or conjugation is applied. + pub(crate) fn replace_correlated_sources( + &mut self, + sources: &[GenericSourceRecordManifest], + vectors: &correlated::NativeSpinCorrelationVectors, + point_start: usize, + point_count: usize, + ) -> RusticolResult<()> { + point_count_u32(point_count, self.arena.active_point_count())?; + let stride = self.arena.point_stride() as usize; + let (re, im, _, _) = self.arena.split_slices_mut(); + for source in sources { + let Some(batch) = vectors.get(&source.leg_label) else { + continue; + }; + let [pr, pi] = source.applied_crossing.phase; + // Traverse Lorentz components in arena order, not the outer batch + // axis: each point below selects its own vector (or broadcasts it). + #[allow(clippy::needless_range_loop)] + for component in 0..4 { + let physical = self + .current_layout + .physical_component(source.value_slot.component_start + component)? + as usize; + for point in 0..point_count { + let [vr, vi] = batch[if batch.len() == 1 { + 0 + } else { + point_start + point + }][component]; + re[physical * stride + point] = vr * pr - vi * pi; + im[physical * stride + point] = vr * pi + vi * pr; + } + } + } + Ok(()) + } + + /// Reuse a leaf only when all of its current inputs are bit-identical. + /// Momenta and model parameters must stay fixed for the memo's lifetime; + /// the correlated caller creates a fresh memo for each point tile. Saved + /// outputs are restored because the production arena reuses dead slots. + pub(crate) fn evaluate_correlated_reusing( + &mut self, + point_count: usize, + memo: &mut CorrelatedStageMemo, + ) -> RusticolResult<()> { + let count = point_count_u32(point_count, self.arena.active_point_count())?; + let leaf_count = self + .stages + .iter() + .map(|stage| stage.leaves.len()) + .sum::() + + self.amplitude.leaves.len(); + if memo.leaves.is_empty() { + memo.leaves.resize_with(leaf_count, || None); + } + if memo.leaves.len() != leaf_count { + return Err(RusticolError::internal( + "correlated stage memo belongs to a different compiled plan", + )); + } + let stride = self.arena.point_stride() as usize; + let mut index = 0; + for stage in self + .stages + .iter_mut() + .chain(std::iter::once(&mut self.amplitude)) + { + for (leaf, plan) in stage.leaves.iter_mut().zip(&stage.leaf_plans) { + let input_components = plan + .input_current_components + .iter() + .map(|&component| self.current_layout.physical_component(component)) + .collect::>>()?; + let output_components = plan + .output_current_components + .iter() + .map(|&component| self.current_layout.physical_component(component)) + .collect::>>()?; + let (re, im) = self.arena.current_slices(); + let inputs: Vec<_> = input_components + .iter() + .flat_map(|&component| { + (0..point_count).flat_map(move |point| { + [ + re[component as usize * stride + point].to_bits(), + im[component as usize * stride + point].to_bits(), + ] + }) + }) + .collect(); + if let Some(saved) = &memo.leaves[index] + && saved.inputs == inputs + { + #[cfg(test)] + { + memo.reused_leaves += 1; + } + let (re, im, ar, ai) = self.arena.split_slices_mut(); + for (position, &component) in output_components.iter().enumerate() { + for point in 0..point_count { + let value = saved.current_outputs[position * point_count + point]; + re[component as usize * stride + point] = value[0]; + im[component as usize * stride + point] = value[1]; + } + } + for (position, &component) in + plan.output_amplitude_components.iter().enumerate() + { + for point in 0..point_count { + let value = saved.amplitude_outputs[position * point_count + point]; + ar[component * stride + point] = value[0]; + ai[component * stride + point] = value[1]; + } + } + } else { + leaf.evaluate(0, count)?; + let (re, im) = self.arena.current_slices(); + let current_outputs = output_components + .iter() + .flat_map(|&component| { + (0..point_count).map(move |point| { + [ + re[component as usize * stride + point], + im[component as usize * stride + point], + ] + }) + }) + .collect(); + let (re, im) = self.arena.amplitude_slices(); + let amplitude_outputs = plan + .output_amplitude_components + .iter() + .flat_map(|&component| { + (0..point_count).map(move |point| { + [ + re[component * stride + point], + im[component * stride + point], + ] + }) + }) + .collect(); + memo.leaves[index] = Some(CorrelatedLeafMemo { + inputs, + current_outputs, + amplitude_outputs, + }); + } + index += 1; + } + } + Ok(()) + } + + /// Bind an already producer-validated compiled color schedule to this + /// concrete direct plan. This remains cold-path validation: hot execution + /// accepts only the opaque returned schedule. + pub(crate) fn bind_color_schedule( + &self, + schedule: &CompiledColorSelectorSchedule, + ) -> RusticolResult { + validate_schedule( + &self.stages, + &self.amplitude, + self.value_component_count, + &self.source_components, + self.amplitude_component_count, + &schedule.active_stage_chunk_indices, + &schedule.active_amplitude_chunk_indices, + ) + } + + /// Bind an already producer-validated compiled helicity schedule to this + /// concrete direct plan. + pub(crate) fn bind_helicity_schedule( + &self, + schedule: &CompiledHelicitySelectorSchedule, + ) -> RusticolResult { + validate_schedule( + &self.stages, + &self.amplitude, + self.value_component_count, + &self.source_components, + self.amplitude_component_count, + &schedule.active_stage_chunk_indices, + &schedule.active_amplitude_chunk_indices, + ) + } + + /// Reuse the closure common to a helicity and a union of requested colours. + /// Binding is done once per selection, never per tile or phase-space point. + pub(crate) fn bind_helicity_color_schedule( + &mut self, + selector_domain_id: usize, + helicity: &CompiledDirectValidatedSchedule, + selected_sector_ids: &BTreeSet, + color_schedules: &BTreeMap, + ) -> RusticolResult> { + if let Some(schedule) = self + .helicity_color_schedules + .get(&selector_domain_id) + .and_then(|colors| colors.get(selected_sector_ids)) + { + return Ok(Arc::clone(schedule)); + } + if selected_sector_ids.is_empty() { + return Err(RusticolError::integrity( + "compiled Direct-Arena colour selection is empty", + )); + } + let colors = selected_sector_ids + .iter() + .map(|id| { + color_schedules.get(id).ok_or_else(|| { + RusticolError::integrity(format!( + "compiled Direct-Arena colour schedule {id} is missing" + )) + }) + }) + .collect::>>()?; + let (stages, amplitude) = intersect_helicity_color_schedules(helicity, &colors)?; + let schedule = Arc::new(validate_schedule( + &self.stages, + &self.amplitude, + self.value_component_count, + &self.source_components, + self.amplitude_component_count, + &stages, + &litude, + )?); + self.helicity_color_schedules + .entry(selector_domain_id) + .or_default() + .insert(selected_sector_ids.clone(), Arc::clone(&schedule)); + Ok(schedule) + } + + /// Execute a cold-bound canonical preorder leaf schedule. + pub(crate) fn evaluate_validated( + &mut self, + point_count: usize, + schedule: &CompiledDirectValidatedSchedule, + ) -> RusticolResult<()> { + self.clear_inactive_amplitude_components(schedule)?; + evaluate_validated_schedule( + &mut self.stages, + &mut self.amplitude, + schedule, + point_count_u32(point_count, self.arena.active_point_count())?, + &mut self.traffic, + ) + } + + /// Execute an authenticated materialized-helicity schedule without + /// clearing amplitude planes outside its active root closure. + /// + /// Materialized-helicity reducers consume only roots carried by the same + /// authenticated selector schedule. Inactive planes may therefore retain + /// values from an earlier tile and must not impose an all-helicity memory + /// pass on a singleton-helicity execution. + pub(crate) fn evaluate_validated_materialized_helicity( + &mut self, + point_count: usize, + schedule: &CompiledDirectValidatedSchedule, + ) -> RusticolResult<()> { + evaluate_validated_schedule( + &mut self.stages, + &mut self.amplitude, + schedule, + point_count_u32(point_count, self.arena.active_point_count())?, + &mut self.traffic, + ) + } + + fn clear_inactive_amplitude_components( + &mut self, + schedule: &CompiledDirectValidatedSchedule, + ) -> RusticolResult<()> { + let mut cursor = 0; + while cursor < schedule.inactive_amplitude_components.len() { + let start = schedule.inactive_amplitude_components[cursor]; + let mut stop = start + 1; + cursor += 1; + while cursor < schedule.inactive_amplitude_components.len() + && schedule.inactive_amplitude_components[cursor] == stop + { + stop += 1; + cursor += 1; + } + self.arena.clear_amplitude_active( + u32::try_from(start).map_err(|_| { + RusticolError::integrity("compiled amplitude clear start exceeds u32") + })?, + u32::try_from(stop - start).map_err(|_| { + RusticolError::integrity("compiled amplitude clear length exceeds u32") + })?, + )?; + self.traffic.amplitude_clear_bytes = self.traffic.amplitude_clear_bytes.saturating_add( + (stop - start) + .saturating_mul(self.arena.active_point_count() as usize) + .saturating_mul(2 * std::mem::size_of::()) as u64, + ); + } + Ok(()) + } + + pub(crate) fn copy_current_to_state( + &mut self, + point_count: usize, + global_parameter_count: usize, + state: &mut [Complex], + ) -> RusticolResult<()> { + if !self.current_layout.is_identity() { + return Err(RusticolError::invalid_argument( + "compiled Direct-Arena full-state developer output requires an identity current \ + layout", + )); + } + if state.len() != point_count.saturating_mul(global_parameter_count) { + return Err(RusticolError::invalid_argument( + "compiled Direct-Arena output state shape is invalid", + )); + } + let stride = self.arena.point_stride() as usize; + let (current_re, current_im) = self.arena.current_slices(); + for component in 0..self.value_component_count { + let plane = component * stride; + for point in 0..point_count { + state[point * global_parameter_count + component] = + Complex::new(current_re[plane + point], current_im[plane + point]); + } + } + self.traffic.boundary_current_output_bytes = + self.traffic.boundary_current_output_bytes.saturating_add( + point_count + .saturating_mul(self.value_component_count) + .saturating_mul(2 * std::mem::size_of::()) as u64, + ); + Ok(()) + } + + /// Borrow the canonical split-complex planes directly for the plane-native + /// reducer. No amplitude packing or traffic accounting is involved. + pub(crate) fn amplitude_planes(&self) -> RusticolResult> { + let (amplitude_re, amplitude_im) = self.arena.amplitude_slices(); + DirectAmplitudePlanes::new( + amplitude_re, + amplitude_im, + self.arena.point_stride(), + self.arena.active_point_count(), + ) + } + + /// Explicit developer-only AoS extraction, excluded from measured leaf and + /// plane-native reduction paths. + pub(crate) fn extract_amplitudes_row_major( + &mut self, + point_count: usize, + output: &mut [Complex], + ) -> RusticolResult<()> { + if output.len() != point_count.saturating_mul(self.amplitude_component_count) { + return Err(RusticolError::invalid_argument( + "compiled Direct-Arena amplitude output shape is invalid", + )); + } + let stride = self.arena.point_stride() as usize; + let (amplitude_re, amplitude_im) = self.arena.amplitude_slices(); + for component in 0..self.amplitude_component_count { + let plane = component * stride; + for point in 0..point_count { + output[point * self.amplitude_component_count + component] = + Complex::new(amplitude_re[plane + point], amplitude_im[plane + point]); + } + } + self.traffic.boundary_amplitude_output_bytes = + self.traffic.boundary_amplitude_output_bytes.saturating_add( + point_count + .saturating_mul(self.amplitude_component_count) + .saturating_mul(2 * std::mem::size_of::()) as u64, + ); + Ok(()) + } +} + +#[cfg(feature = "f64-symjit")] +pub(crate) fn compiled_direct_symjit_supported( + evaluators: &GenericStageEvaluatorArtifactsManifest, +) -> RusticolResult { + for stage in evaluators + .stages + .iter() + .chain(std::iter::once(&evaluators.amplitude_stage)) + { + for leaf in stage.evaluator.leaf_layout()? { + if !matches!(leaf.evaluator, EvaluatorManifest::SymjitApplication { .. }) { + return Ok(false); + } + } + } + Ok(true) +} + +/// Execute the momentum sums already recorded in the process output. Only +/// the point loop is transposed: label order, crossings and the initial +/// positive-zero addition remain identical to the scalar implementation. +fn fill_compiled_momentum_planes( + batch: F64MomentumBatchView<'_>, + slots: &[GenericMomentumSlotManifest], + external_is_initial: &[bool], + component_count: usize, + stride: usize, + momenta: &mut [f64], +) -> RusticolResult<()> { + let point_count = batch.point_count(); + if stride < point_count || component_count.checked_mul(stride) != Some(momenta.len()) { + return Err(RusticolError::integrity( + "compiled Direct-Arena momentum plane shape is invalid", + )); + } + for slot in slots { + if slot.component_stop.checked_sub(slot.component_start) != Some(4) + || slot.component_stop > component_count + { + return Err(RusticolError::integrity(format!( + "compiled Direct-Arena momentum slot {} has an invalid component range", + slot.momentum_slot_id + ))); + } + if slot.external_labels.is_empty() { + for component in slot.component_start..slot.component_stop { + let start = component * stride; + momenta[start..start + point_count].fill(0.0); + } + continue; + } + if point_count == 1 { + // A singleton has no point-plane loop to vectorize. Read all four + // components and resolve its public crossing once per term, as in + // the original scalar accumulator, after static slot pruning. + let point = batch.point(0); + let mut value = [0.0; 4]; + for &label in &slot.external_labels { + let (external_index, sign) = compiled_momentum_term( + label, + slot.momentum_slot_id, + batch.external_count(), + external_is_initial, + )?; + let momentum = point + .momentum(external_index) + .ok_or_else(|| RusticolError::integrity("compiled momentum input is absent"))?; + for (sum, component) in value.iter_mut().zip(momentum) { + *sum += sign * component; + } + } + for (component, value) in value.into_iter().enumerate() { + momenta[(slot.component_start + component) * stride] = value; + } + continue; + } + for (term, label) in slot.external_labels.iter().enumerate() { + let (external_index, sign) = compiled_momentum_term( + *label, + slot.momentum_slot_id, + batch.external_count(), + external_is_initial, + )?; + for component in 0..4 { + let start = (slot.component_start + component) * stride; + let output = &mut momenta[start..start + point_count]; + if term == 0 { + // Avoid a separate zero-fill pass, especially for the + // common single-external-momentum slots. + batch.accumulate_component_into::( + external_index, + component, + sign, + output, + )?; + } else { + batch.accumulate_component_into::( + external_index, + component, + sign, + output, + )?; + } + } + } + } + Ok(()) +} + +#[inline] +fn compiled_momentum_term( + label: usize, + momentum_slot_id: usize, + external_count: usize, + external_is_initial: &[bool], +) -> RusticolResult<(usize, f64)> { + let external_index = label.checked_sub(1).ok_or_else(|| { + RusticolError::integrity("compiled Direct-Arena momentum labels are one-based") + })?; + if external_index >= external_count { + return Err(RusticolError::integrity(format!( + "compiled Direct-Arena momentum slot {momentum_slot_id} references absent external leg {label}" + ))); + } + let sign = if *external_is_initial.get(external_index).ok_or_else(|| { + RusticolError::integrity("compiled Direct-Arena external-side metadata is incomplete") + })? { + -1.0 + } else { + 1.0 + }; + Ok((external_index, sign)) +} + +fn canonical_source_layout( + sources: &[GenericSourceRecordManifest], + value_component_count: usize, +) -> RusticolResult<(Vec, usize)> { + if sources.is_empty() { + return Err(RusticolError::integrity( + "compiled Direct-Arena runtime schema has no sources", + )); + } + let mut source_components = Vec::new(); + let mut max_dimension = 0usize; + for (source_index, source) in sources.iter().enumerate() { + if source.source_id != source_index + || source.value_slot.current_id != source.current_id + || source.value_slot.component_start != source.current_component_start + || source.value_slot.component_stop != source.current_component_stop + { + return Err(RusticolError::integrity(format!( + "compiled Direct-Arena source {source_index} does not preserve canonical current ownership" + ))); + } + let start = source.value_slot.component_start; + let stop = source.value_slot.component_stop; + let dimension = stop + .checked_sub(start) + .filter(|value| *value != 0) + .ok_or_else(|| { + RusticolError::integrity(format!( + "compiled Direct-Arena source {source_index} has an empty or reversed range" + )) + })?; + if stop > value_component_count || dimension != source.dimension { + return Err(RusticolError::integrity(format!( + "compiled Direct-Arena source {source_index} exceeds canonical current storage" + ))); + } + source_components.extend(start..stop); + max_dimension = max_dimension.max(dimension); + } + if source_components + .windows(2) + .any(|window| window[0] >= window[1]) + { + return Err(RusticolError::integrity( + "compiled Direct-Arena source component ownership overlaps or is unordered", + )); + } + Ok((source_components, max_dimension)) +} + +fn canonical_structural_zero_components( + stages: &[GenericSerializedStageEvaluatorManifest], + source_components: &[usize], + value_component_count: usize, +) -> RusticolResult> { + let sources = source_components.iter().copied().collect::>(); + let mut produced = BTreeSet::new(); + for stage in stages { + for slot in &stage.output_slots { + let output_len = slot + .output_stop + .checked_sub(slot.output_start) + .ok_or_else(|| { + RusticolError::integrity("compiled Direct-Arena output slot range underflows") + })?; + if slot.component_stop.checked_sub(slot.component_start) != Some(output_len) + || slot.component_stop > value_component_count + { + return Err(RusticolError::integrity(format!( + "compiled Direct-Arena stage {:?} has an invalid canonical output range", + stage.evaluator_label + ))); + } + for component in slot.component_start..slot.component_stop { + if sources.contains(&component) { + return Err(RusticolError::integrity(format!( + "compiled Direct-Arena stage {:?} writes canonical source component \ + {component}", + stage.evaluator_label + ))); + } + produced.insert(component); + } + } + } + Ok((0..value_component_count) + .filter(|component| !sources.contains(component) && !produced.contains(component)) + .collect()) +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +struct CompiledDirectCurrentLifetime { + canonical_component: usize, + first_event: u64, + last_event: u64, +} + +fn derive_stage_current_layout( + stages: &[LoadedStage], + amplitude: &LoadedStage, + source_components: &[usize], + canonical_component_count: usize, +) -> RusticolResult { + let mut first_events = vec![None; canonical_component_count]; + let mut last_events = vec![None; canonical_component_count]; + for &component in source_components { + define_current_component( + &mut first_events, + &mut last_events, + component, + 0, + "source fill", + )?; + } + + for (stage_index, stage) in stages.iter().enumerate() { + let event = u64::try_from(stage_index) + .ok() + .and_then(|value| value.checked_add(1)) + .ok_or_else(|| { + RusticolError::integrity("compiled Direct-Arena stage-liveness event exceeds u64") + })?; + for leaf in &stage.leaf_plans { + for &component in &leaf.input_current_components { + use_current_component( + &first_events, + &mut last_events, + component, + event, + "current stage input", + )?; + } + } + // Arena-v1 declares no input/output or output/output aliasing for the + // complete fused stage, not merely for one leaf. Give every output the + // same inclusive definition event so the allocator preserves that + // whole-stage contract. + for leaf in &stage.leaf_plans { + for &component in &leaf.output_current_components { + define_current_component( + &mut first_events, + &mut last_events, + component, + event, + "current stage output", + )?; + } + } + } + + let amplitude_event = u64::try_from(stages.len()) + .ok() + .and_then(|value| value.checked_add(1)) + .ok_or_else(|| { + RusticolError::integrity("compiled Direct-Arena amplitude-liveness event exceeds u64") + })?; + for leaf in &litude.leaf_plans { + for &component in &leaf.input_current_components { + use_current_component( + &first_events, + &mut last_events, + component, + amplitude_event, + "amplitude stage input", + )?; + } + } + + let mut lifetimes = first_events + .into_iter() + .zip(last_events) + .enumerate() + .filter_map(|(canonical_component, (first_event, last_event))| { + first_event.map(|first_event| { + last_event + .map(|last_event| CompiledDirectCurrentLifetime { + canonical_component, + first_event, + last_event, + }) + .ok_or_else(|| { + RusticolError::integrity(format!( + "compiled Direct-Arena current component {canonical_component} has a \ + definition but no lifetime end" + )) + }) + }) + }) + .collect::>>()?; + if lifetimes.is_empty() { + return Err(RusticolError::integrity( + "compiled Direct-Arena stage liveness has no live current components", + )); + } + lifetimes.sort_unstable_by_key(|lifetime| (lifetime.first_event, lifetime.canonical_component)); + + // Unit-width interval coloring in O(N log N). At every definition event, + // release only ranges whose inclusive lifetime ended strictly earlier and + // deterministically reuse the lowest free physical plane. + let mut active = BinaryHeap::>::new(); + let mut free = BinaryHeap::>::new(); + let mut next_physical = 0_u32; + let mut canonical_to_physical = vec![UNMAPPED_CURRENT_COMPONENT; canonical_component_count]; + for lifetime in &lifetimes { + while let Some(Reverse((last_event, physical_component))) = active.peek().copied() { + if last_event >= lifetime.first_event { + break; + } + active.pop(); + free.push(Reverse(physical_component)); + } + let physical_component = if let Some(Reverse(component)) = free.pop() { + component + } else { + let component = next_physical; + next_physical = next_physical.checked_add(1).ok_or_else(|| { + RusticolError::integrity( + "compiled Direct-Arena physical current plane count exceeds u32", + ) + })?; + component + }; + canonical_to_physical[lifetime.canonical_component] = physical_component; + active.push(Reverse((lifetime.last_event, physical_component))); + } + + let layout = CompiledDirectCurrentLayout { + canonical_to_physical: canonical_to_physical.into_boxed_slice(), + physical_component_count: next_physical, + live_component_count: lifetimes.len(), + }; + validate_compiled_current_layout(&layout, &lifetimes, stages, amplitude)?; + Ok(layout) +} + +fn define_current_component( + first_events: &mut [Option], + last_events: &mut [Option], + component: usize, + event: u64, + context: &str, +) -> RusticolResult<()> { + let first = first_events.get_mut(component).ok_or_else(|| { + RusticolError::integrity(format!( + "compiled Direct-Arena {context} component {component} is out of bounds" + )) + })?; + let last = last_events.get_mut(component).ok_or_else(|| { + RusticolError::integrity(format!( + "compiled Direct-Arena {context} component {component} is out of bounds" + )) + })?; + if first.replace(event).is_some() || last.replace(event).is_some() { + return Err(RusticolError::integrity(format!( + "compiled Direct-Arena {context} component {component} has multiple producers" + ))); + } + Ok(()) +} + +fn use_current_component( + first_events: &[Option], + last_events: &mut [Option], + component: usize, + event: u64, + context: &str, +) -> RusticolResult<()> { + let first = first_events + .get(component) + .copied() + .flatten() + .ok_or_else(|| { + RusticolError::integrity(format!( + "compiled Direct-Arena {context} component {component} is absent or used before \ + its producer" + )) + })?; + if first > event { + return Err(RusticolError::integrity(format!( + "compiled Direct-Arena {context} component {component} precedes its producer" + ))); + } + let last = last_events.get_mut(component).ok_or_else(|| { + RusticolError::integrity(format!( + "compiled Direct-Arena {context} component {component} is out of bounds" + )) + })?; + *last = Some(last.unwrap_or(first).max(event)); + Ok(()) +} + +fn validate_compiled_current_layout( + layout: &CompiledDirectCurrentLayout, + lifetimes: &[CompiledDirectCurrentLifetime], + stages: &[LoadedStage], + amplitude: &LoadedStage, +) -> RusticolResult<()> { + if layout.physical_component_count == 0 + || layout.live_component_count != lifetimes.len() + || layout.canonical_to_physical.is_empty() + { + return Err(RusticolError::integrity( + "compiled Direct-Arena current-liveness accounting is inconsistent", + )); + } + let mut physical_chains = + vec![Vec::::new(); layout.physical_component_count as usize]; + for lifetime in lifetimes { + if lifetime.first_event > lifetime.last_event { + return Err(RusticolError::integrity(format!( + "compiled Direct-Arena current component {} has a reversed lifetime", + lifetime.canonical_component + ))); + } + let physical = layout.physical_component(lifetime.canonical_component)?; + physical_chains + .get_mut(physical as usize) + .ok_or_else(|| { + RusticolError::integrity( + "compiled Direct-Arena liveness assignment exceeds physical storage", + ) + })? + .push(*lifetime); + } + for chain in &mut physical_chains { + chain.sort_unstable_by_key(|lifetime| (lifetime.first_event, lifetime.canonical_component)); + for pair in chain.windows(2) { + if pair[0].last_event >= pair[1].first_event { + return Err(RusticolError::integrity(format!( + "compiled Direct-Arena live current components {} and {} alias physical plane", + pair[0].canonical_component, pair[1].canonical_component + ))); + } + } + } + for (stage_index, stage) in stages.iter().enumerate() { + validate_stage_physical_aliases( + layout, + stage, + &format!("compiled Direct-Arena stage {stage_index}"), + )?; + } + validate_stage_physical_aliases(layout, amplitude, "compiled Direct-Arena amplitude stage")?; + Ok(()) +} + +fn validate_stage_physical_aliases( + layout: &CompiledDirectCurrentLayout, + stage: &LoadedStage, + context: &str, +) -> RusticolResult<()> { + let mut canonical_inputs = BTreeSet::new(); + let mut canonical_outputs = BTreeSet::new(); + for leaf in &stage.leaf_plans { + for &component in &leaf.input_current_components { + canonical_inputs.insert(component); + } + for &component in &leaf.output_current_components { + if !canonical_outputs.insert(component) { + return Err(RusticolError::integrity(format!( + "{context} repeats an output current component" + ))); + } + } + } + let mut inputs = BTreeSet::new(); + for component in canonical_inputs { + if !inputs.insert(layout.physical_component(component)?) { + return Err(RusticolError::integrity(format!( + "{context} aliases distinct live input current components" + ))); + } + } + let mut outputs = BTreeSet::new(); + for component in canonical_outputs { + if !outputs.insert(layout.physical_component(component)?) { + return Err(RusticolError::integrity(format!( + "{context} aliases distinct output current components" + ))); + } + } + if inputs.iter().any(|component| outputs.contains(component)) { + return Err(RusticolError::integrity(format!( + "{context} aliases an input and output current component" + ))); + } + Ok(()) +} + +fn point_count_u32(point_count: usize, active: u32) -> RusticolResult { + let point_count = u32::try_from(point_count) + .map_err(|_| RusticolError::invalid_argument("compiled point count exceeds u32"))?; + if point_count == 0 || point_count != active { + return Err(RusticolError::invalid_argument( + "compiled Direct-Arena point count does not match the active tile", + )); + } + Ok(point_count) +} + +fn evaluate_bound_stage_leaves( + stage: &mut BoundStage, + point_count: u32, + selected: &[usize], + traffic: &mut CompiledDirectPrototypeTraffic, +) -> RusticolResult<()> { + for &leaf_index in selected { + let broadcast_bytes = stage.leaves[leaf_index].evaluate(0, point_count)?; + traffic.scalar_broadcast_fill_bytes = traffic + .scalar_broadcast_fill_bytes + .checked_add(broadcast_bytes) + .ok_or_else(|| { + RusticolError::internal("compiled scalar broadcast traffic counter overflows u64") + })?; + traffic.leaf.record_call(1, point_count); + } + Ok(()) +} + +fn intersect_helicity_color_schedules( + helicity: &CompiledDirectValidatedSchedule, + colors: &[&CompiledDirectValidatedSchedule], +) -> RusticolResult<(Vec>, Vec)> { + if colors + .iter() + .any(|color| color.active_stage_leaves.len() != helicity.active_stage_leaves.len()) + { + return Err(RusticolError::integrity( + "compiled Direct-Arena selector schedules have different stage counts", + )); + } + let intersect = |selected: &[usize], union: BTreeSet| { + selected + .iter() + .copied() + .filter(|id| union.contains(id)) + .collect() + }; + let stages = helicity + .active_stage_leaves + .iter() + .enumerate() + .map(|(index, leaves)| { + intersect( + leaves, + colors + .iter() + .flat_map(|color| color.active_stage_leaves[index].iter().copied()) + .collect(), + ) + }) + .collect(); + let amplitude = intersect( + &helicity.active_amplitude_leaves, + colors + .iter() + .flat_map(|color| color.active_amplitude_leaves.iter().copied()) + .collect(), + ); + Ok((stages, amplitude)) +} + +fn validate_schedule( + stages: &[S], + amplitude: &S, + value_component_count: usize, + source_components: &[usize], + amplitude_component_count: usize, + active_stage_leaves: &[Vec], + active_amplitude_leaves: &[usize], +) -> RusticolResult { + if active_stage_leaves.len() != stages.len() { + return Err(RusticolError::integrity( + "compiled Direct-Arena schedule has the wrong stage count", + )); + } + let mut produced = BTreeSet::new(); + for (stage_index, stage) in stages.iter().enumerate() { + for (leaf_index, leaf) in stage.leaf_plans().iter().enumerate() { + for &component in &leaf.output_current_components { + if !produced.insert(component) { + return Err(RusticolError::integrity(format!( + "compiled Direct-Arena stage {stage_index} leaf {leaf_index} duplicates \ + producer for current component {component}" + ))); + } + } + } + } + let declared_sources = source_components.iter().copied().collect::>(); + if declared_sources.len() != source_components.len() + || declared_sources + .iter() + .any(|component| *component >= value_component_count || produced.contains(component)) + { + return Err(RusticolError::integrity( + "compiled Direct-Arena source ownership aliases or exceeds produced current storage", + )); + } + // Value storage is a stable global numbering space and can contain + // components omitted by closure pruning. Such holes are neither sources + // nor produced values. They are valid only while no selected leaf reads + // them; the ordered prerequisite checks below enforce that invariant. + let mut available = declared_sources; + let mut validated_stages = Vec::with_capacity(stages.len()); + for (stage_index, (stage, selected)) in stages.iter().zip(active_stage_leaves).enumerate() { + validate_sorted_leaf_indices( + selected, + stage.leaf_plans().len(), + &format!("compiled Direct-Arena stage {stage_index} schedule"), + )?; + let mut stage_outputs = BTreeSet::new(); + for &leaf_index in selected { + let leaf = &stage.leaf_plans()[leaf_index]; + let missing = leaf + .input_current_components + .iter() + .copied() + .filter(|component| !available.contains(component)) + .collect::>(); + if !missing.is_empty() { + return Err(RusticolError::integrity(format!( + "compiled Direct-Arena stage {stage_index} leaf {leaf_index} is missing \ + prerequisite current components {missing:?}" + ))); + } + stage_outputs.extend(leaf.output_current_components.iter().copied()); + } + available.extend(stage_outputs); + validated_stages.push(selected.clone().into_boxed_slice()); + } + validate_sorted_leaf_indices( + active_amplitude_leaves, + amplitude.leaf_plans().len(), + "compiled Direct-Arena amplitude schedule", + )?; + let mut selected_amplitudes = BTreeSet::new(); + for &leaf_index in active_amplitude_leaves { + let leaf = &litude.leaf_plans()[leaf_index]; + let missing = leaf + .input_current_components + .iter() + .copied() + .filter(|component| !available.contains(component)) + .collect::>(); + if !missing.is_empty() { + return Err(RusticolError::integrity(format!( + "compiled Direct-Arena amplitude leaf {leaf_index} is missing prerequisite \ + current components {missing:?}" + ))); + } + selected_amplitudes.extend(leaf.output_amplitude_components.iter().copied()); + } + if selected_amplitudes + .iter() + .any(|component| *component >= amplitude_component_count) + { + return Err(RusticolError::integrity( + "compiled Direct-Arena amplitude schedule exceeds canonical output planes", + )); + } + let inactive_amplitude_components = (0..amplitude_component_count) + .filter(|component| !selected_amplitudes.contains(component)) + .collect::>() + .into_boxed_slice(); + Ok(CompiledDirectValidatedSchedule { + active_stage_leaves: validated_stages.into_boxed_slice(), + active_amplitude_leaves: active_amplitude_leaves.to_vec().into_boxed_slice(), + inactive_amplitude_components, + }) +} + +fn validate_sorted_leaf_indices( + selected: &[usize], + leaf_count: usize, + label: &str, +) -> RusticolResult<()> { + let mut previous = None; + for &index in selected { + if index >= leaf_count || previous.is_some_and(|value| value >= index) { + return Err(RusticolError::integrity(format!( + "{label} is not sorted, unique, and in bounds" + ))); + } + previous = Some(index); + } + Ok(()) +} + +fn evaluate_validated_schedule( + stages: &mut [BoundStage], + amplitude: &mut BoundStage, + schedule: &CompiledDirectValidatedSchedule, + point_count: u32, + traffic: &mut CompiledDirectPrototypeTraffic, +) -> RusticolResult<()> { + for (stage, selected) in stages.iter_mut().zip(&schedule.active_stage_leaves) { + evaluate_bound_stage_leaves(stage, point_count, selected, traffic)?; + } + evaluate_bound_stage_leaves( + amplitude, + point_count, + &schedule.active_amplitude_leaves, + traffic, + )?; + traffic.leaf.validate_direct() +} + +fn load_stage( + stage: &GenericSerializedStageEvaluatorManifest, + is_amplitude: bool, + payloads: &EvaluatorPayloadStore, + value_component_count: usize, + momentum_component_count: usize, + amplitude_component_count: usize, + structural_zero_components: &BTreeSet, +) -> RusticolResult { + let direct = stage.compiled_plane_arena.as_ref().ok_or_else(|| { + RusticolError::integrity(format!( + "compiled Direct-Arena stage {:?} has no serialized plane bindings", + stage.evaluator_label + )) + })?; + if stage.parameter_layout != "stage-local-value-momentum" + || direct.input_bindings.len() != stage.parameter_count + { + return Err(RusticolError::compatibility(format!( + "compiled Direct-Arena stage {:?} requires complete serialized input bindings", + stage.evaluator_label + ))); + } + let mut components = vec![None; stage.parameter_count]; + for component in &direct.input_bindings { + if component.parameter_index >= components.len() + || components[component.parameter_index] + .replace(component) + .is_some() + { + return Err(RusticolError::integrity(format!( + "compiled Direct-Arena stage {:?} has invalid serialized input bindings", + stage.evaluator_label + ))); + } + } + if components.iter().any(Option::is_none) { + return Err(RusticolError::integrity(format!( + "compiled Direct-Arena stage {:?} has incomplete serialized input bindings", + stage.evaluator_label + ))); + } + + let output_limit = if is_amplitude { + amplitude_component_count + } else { + value_component_count + }; + let mut output_components = vec![None; stage.output_length]; + let mut seen_output_components = BTreeSet::new(); + let expected_arena = if is_amplitude { "amplitude" } else { "current" }; + for binding in &direct.output_bindings { + if binding.output_index >= output_components.len() + || binding.component >= output_limit + || binding.arena != expected_arena + { + return Err(RusticolError::integrity(format!( + "compiled Direct-Arena stage {:?} has an invalid serialized output binding", + stage.evaluator_label + ))); + } + if !seen_output_components.insert(binding.component) { + return Err(RusticolError::integrity(format!( + "compiled Direct-Arena stage {:?} aliases output component {}", + stage.evaluator_label, binding.component + ))); + } + if output_components[binding.output_index] + .replace(binding) + .is_some() + { + return Err(RusticolError::integrity(format!( + "compiled Direct-Arena stage {:?} has duplicate serialized output indices", + stage.evaluator_label + ))); + } + } + if output_components.iter().any(Option::is_none) { + return Err(RusticolError::integrity(format!( + "compiled Direct-Arena stage {:?} does not cover every output", + stage.evaluator_label + ))); + } + if is_amplitude + && (stage.output_length != amplitude_component_count + || seen_output_components.len() != amplitude_component_count + || seen_output_components + .iter() + .copied() + .ne(0..amplitude_component_count)) + { + return Err(RusticolError::integrity(format!( + "compiled Direct-Arena amplitude stage {:?} is not a complete canonical output \ + permutation", + stage.evaluator_label + ))); + } + + let leaf_layout = stage.evaluator.leaf_layout()?; + let mut loaded = Vec::with_capacity(leaf_layout.len()); + let mut leaf_plans = Vec::with_capacity(leaf_layout.len()); + let mut output_cursor = 0usize; + for (leaf_index, leaf) in leaf_layout.into_iter().enumerate() { + let (input_len, output_len) = leaf.evaluator.io_len()?; + let direct_leaf = direct.leaves.get(leaf_index).ok_or_else(|| { + RusticolError::integrity("compiled plane-arena leaf bindings are incomplete") + })?; + let direct_input_indices = direct_leaf.input_indices.as_slice(); + let direct_output_range = direct_leaf.output_start..direct_leaf.output_stop; + let direct_input_len = direct_leaf.input_len; + let direct_output_len = direct_leaf.output_len; + if direct_input_indices.len() != direct_input_len { + return Err(RusticolError::integrity(format!( + "compiled Direct-Arena stage {:?} leaf input map has the wrong length", + stage.evaluator_label + ))); + } + if direct_input_len != input_len + || direct_output_len != output_len + || direct_input_indices != leaf.input_indices + || direct_output_range != leaf.output_range + { + return Err(RusticolError::integrity(format!( + "compiled Direct-Arena stage {:?} leaf bindings disagree with canonical layout", + stage.evaluator_label + ))); + } + if direct_output_range.start != output_cursor + || direct_output_range.end + != output_cursor + .checked_add(direct_output_len) + .ok_or_else(|| { + RusticolError::integrity( + "compiled Direct-Arena leaf output range overflows", + ) + })? + || direct_output_range.end > output_components.len() + { + return Err(RusticolError::integrity( + "compiled Direct-Arena canonical leaf output range disagrees with the stage", + )); + } + let output_stop = direct_output_range.end; + let scalar_values_per_point = direct_input_len + .checked_add(direct_output_len) + .and_then(|value| value.checked_mul(2)) + .ok_or_else(|| { + RusticolError::integrity("compiled Direct-Arena leaf-local footprint overflows") + })?; + + let mut input_currents = BTreeSet::new(); + let mut input_components = Vec::with_capacity(direct_input_len); + for ¶meter_index in direct_input_indices { + let component = components + .get(parameter_index) + .and_then(|value| *value) + .ok_or_else(|| { + RusticolError::integrity( + "compiled Direct-Arena leaf references an absent stage input", + ) + })?; + if component.kind == "value" + && !structural_zero_components.contains(&component.global_component) + { + input_currents.insert(component.global_component); + } + input_components.push(component); + } + let canonical_outputs = output_components[output_cursor..output_stop] + .iter() + .map(|value| value.expect("output coverage validated")) + .collect::>(); + + let loaded_leaf = match leaf.evaluator { + #[cfg(feature = "f64-symjit")] + EvaluatorManifest::SymjitApplication { + runtime_capability, + application_abi, + element_layout, + batch_layout, + compiler_type, + translation_mode, + optimization_level, + word_bits, + endianness, + required_defuns, + plane_application, + .. + } => { + if *optimization_level > 3 { + return Err(RusticolError::compatibility( + "compiled-plane-arena-v1 supports compiled JIT optimization levels 0 \ + through 3", + )); + } + validate_manifest_metadata(&SymjitApplicationMetadata { + runtime_capability, + application_abi, + input_len, + output_len, + element_layout, + batch_layout, + compiler_type, + translation_mode, + optimization_level: *optimization_level, + word_bits: *word_bits, + endianness, + required_defuns, + })?; + let plane_application = plane_application.as_ref().ok_or_else(|| { + RusticolError::compatibility( + "compiled SymJIT leaf predates the standard plane \ + application ABI; regenerate the artifact", + ) + })?; + plane_application.validate(input_len, output_len, *optimization_level)?; + if direct_leaf.application_path != plane_application.application_path + || direct_leaf.source_application_abi != plane_application.application_abi + || direct_leaf.source_application_abi != direct.source_application_abi + || direct.source_application_abi != SYMJIT_PLANE_APPLICATION_ABI + || direct.application_abi != COMPILED_PLANE_DIRECT_APPLICATION_ABI + || direct_leaf.optimization_level != *optimization_level + || direct_leaf.direct_codegen_optimization_level != *optimization_level + { + return Err(RusticolError::integrity( + "compiled SymJIT Direct-Arena leaf identity is inconsistent", + )); + } + let mut source_inputs = Vec::with_capacity(direct_input_len * 2); + let mut plane_bindings = Vec::new(); + let mut scalar_bindings = Vec::new(); + for component in &input_components { + append_component_bindings( + component, + value_component_count, + momentum_component_count, + structural_zero_components, + &mut source_inputs, + &mut plane_bindings, + &mut scalar_bindings, + )?; + } + let outputs = symjit_output_bindings(&canonical_outputs)?; + let source = payloads.source(&plane_application.application_path)?; + let bytes = source.read()?; + LoadedCompiledDirectLeaf::Symjit( + LoadedSymjitCompiledDirectStage::load_source_bytes( + bytes.as_ref(), + PathBuf::from(source.display_name()), + &direct_leaf.source_application_abi, + direct_leaf.optimization_level, + plane_application.compression, + source_inputs, + plane_bindings, + scalar_bindings, + outputs, + )?, + ) + } + #[cfg(feature = "f64-compiled")] + EvaluatorManifest::CompiledComplex { + runtime_capability, + function_name, + number_type, + native_direct_application: Some(application), + .. + } => { + if !matches!( + runtime_capability.as_str(), + SYMBOLICA_COMPILED_CPP_RUNTIME_CAPABILITY + | SYMBOLICA_COMPILED_ASM_RUNTIME_CAPABILITY + ) || number_type != "complex" + { + return Err(RusticolError::compatibility( + "compiled native Direct-Arena leaf has an incompatible backend", + )); + } + application.validate(function_name, input_len, output_len)?; + if direct_leaf.optimization_level != 3 + || direct_leaf.direct_codegen_optimization_level != 3 + || direct_leaf.application_path != application.library_path + || direct_leaf.source_application_abi != application.application_abi + || direct.source_application_abi != application.application_abi + || direct.application_abi != application.application_abi + { + return Err(RusticolError::integrity( + "compiled native Direct-Arena leaf identity is inconsistent", + )); + } + let mut plane_bindings = Vec::new(); + let mut scalar_bindings = Vec::new(); + for component in &input_components { + append_native_component_bindings( + component, + value_component_count, + momentum_component_count, + structural_zero_components, + &mut plane_bindings, + &mut scalar_bindings, + )?; + } + let outputs = native_output_bindings(&canonical_outputs)?; + let library = payloads.load_native_library(&application.library_path)?; + LoadedCompiledDirectLeaf::Native(LoadedNativeCompiledDirectStage::load( + library, + function_name, + &application.application_abi, + plane_bindings, + scalar_bindings, + outputs, + )?) + } + #[cfg(feature = "f64-compiled")] + EvaluatorManifest::CompiledComplex { + native_direct_application: None, + .. + } => { + return Err(RusticolError::compatibility( + "compiled native Direct-Arena leaf has no plane-native DirectApplication; \ + regenerate the artifact", + )); + } + _ => { + return Err(RusticolError::compatibility(format!( + "compiled Direct-Arena stage {:?} contains an unsupported leaf", + stage.evaluator_label + ))); + } + }; + let persistent_scalar_values_per_point = loaded_leaf.persistent_scalar_values_per_point(); + loaded.push(loaded_leaf); + leaf_plans.push(DirectLeafPlan { + input_current_components: input_currents + .into_iter() + .collect::>() + .into_boxed_slice(), + output_current_components: if is_amplitude { + Box::new([]) + } else { + output_components[output_cursor..output_stop] + .iter() + .map(|value| value.expect("output coverage validated").component) + .collect::>() + .into_boxed_slice() + }, + output_amplitude_components: if is_amplitude { + output_components[output_cursor..output_stop] + .iter() + .map(|value| value.expect("output coverage validated").component) + .collect::>() + .into_boxed_slice() + } else { + Box::new([]) + }, + scalar_values_per_point, + persistent_scalar_values_per_point, + }); + output_cursor = output_stop; + } + if output_cursor != stage.output_length { + return Err(RusticolError::integrity(format!( + "compiled Direct-Arena stage {:?} leaf outputs do not cover the stage", + stage.evaluator_label + ))); + } + Ok(LoadedStage { + leaves: loaded, + leaf_plans, + }) +} + +#[cfg(feature = "f64-symjit")] +fn symjit_output_bindings( + outputs: &[&CompiledPlaneOutputBindingManifest], +) -> RusticolResult> { + let mut bindings = Vec::with_capacity(outputs.len() * 2); + for output in outputs { + let component = u32::try_from(output.component) + .map_err(|_| RusticolError::integrity("compiled output plane index exceeds u32"))?; + let plane = |imaginary| match output.arena.as_str() { + "current" => Ok(CompiledDirectArenaPlane::Current { + component, + imaginary, + }), + "amplitude" => Ok(CompiledDirectArenaPlane::Amplitude { + component, + imaginary, + }), + _ => Err(RusticolError::integrity( + "compiled output binding names an unsupported arena", + )), + }; + bindings.push(CompiledDirectOutputBinding(plane(false)?)); + bindings.push(CompiledDirectOutputBinding(plane(true)?)); + } + Ok(bindings) +} + +#[cfg(feature = "f64-compiled")] +fn native_output_bindings( + outputs: &[&CompiledPlaneOutputBindingManifest], +) -> RusticolResult> { + let mut bindings = Vec::with_capacity(outputs.len() * 2); + for output in outputs { + let component = u32::try_from(output.component) + .map_err(|_| RusticolError::integrity("compiled output plane index exceeds u32"))?; + let plane = |imaginary| match output.arena.as_str() { + "current" => Ok(NativeCompiledDirectArenaPlane::Current { + component, + imaginary, + }), + "amplitude" => Ok(NativeCompiledDirectArenaPlane::Amplitude { + component, + imaginary, + }), + _ => Err(RusticolError::integrity( + "compiled output binding names an unsupported arena", + )), + }; + bindings.push(NativeCompiledDirectOutputBinding(plane(false)?)); + bindings.push(NativeCompiledDirectOutputBinding(plane(true)?)); + } + Ok(bindings) +} + +#[cfg(feature = "f64-compiled")] +fn append_native_component_bindings( + component: &CompiledPlaneInputBindingManifest, + value_component_count: usize, + momentum_component_count: usize, + structural_zero_components: &BTreeSet, + planes: &mut Vec, + scalars: &mut Vec, +) -> RusticolResult<()> { + match component.kind.as_str() { + "value" => { + if component.global_component >= value_component_count { + return Err(RusticolError::integrity( + "compiled Direct-Arena value input is outside current planes", + )); + } + if structural_zero_components.contains(&component.global_component) { + planes.push(NativeCompiledDirectPlaneBinding::Zero); + if !component.real_valued { + planes.push(NativeCompiledDirectPlaneBinding::Zero); + } + return Ok(()); + } + let current = |imaginary| { + NativeCompiledDirectPlaneBinding::Arena(NativeCompiledDirectArenaPlane::Current { + component: component.global_component as u32, + imaginary, + }) + }; + planes.push(current(false)); + if !component.real_valued { + planes.push(current(true)); + } + } + "momentum" => { + let local = component + .global_component + .checked_sub(value_component_count) + .filter(|index| *index < momentum_component_count) + .ok_or_else(|| { + RusticolError::integrity( + "compiled Direct-Arena momentum input is outside momentum planes", + ) + })?; + planes.push(NativeCompiledDirectPlaneBinding::Momentum { + form: u32::try_from(local / 4) + .map_err(|_| RusticolError::integrity("compiled momentum form exceeds u32"))?, + lorentz_component: (local % 4) as u16, + }); + if !component.real_valued { + planes.push(NativeCompiledDirectPlaneBinding::Zero); + } + } + "model_parameter" => { + let index = u32::try_from(component.source_id).map_err(|_| { + RusticolError::integrity("compiled model parameter index exceeds u32") + })?; + scalars.push(NativeCompiledDirectScalarBinding::Parameter { + index, + imaginary: false, + }); + if !component.real_valued { + scalars.push(NativeCompiledDirectScalarBinding::Parameter { + index, + imaginary: true, + }); + } + } + kind => { + return Err(RusticolError::compatibility(format!( + "compiled Direct-Arena does not support input kind {kind:?}" + ))); + } + } + Ok(()) +} + +#[cfg(feature = "f64-symjit")] +fn append_component_bindings( + component: &CompiledPlaneInputBindingManifest, + value_component_count: usize, + momentum_component_count: usize, + structural_zero_components: &BTreeSet, + source_inputs: &mut Vec, + planes: &mut Vec, + scalars: &mut Vec, +) -> RusticolResult<()> { + fn push_plane( + source_inputs: &mut Vec, + planes: &mut Vec, + binding: CompiledDirectPlaneBinding, + ) -> RusticolResult<()> { + let index = u32::try_from(planes.len()).map_err(|_| { + RusticolError::integrity("compiled Direct-Arena plane binding count exceeds u32") + })?; + source_inputs.push(CompiledDirectSourceInputBinding::Plane(index)); + planes.push(binding); + Ok(()) + } + fn push_scalar( + source_inputs: &mut Vec, + scalars: &mut Vec, + binding: CompiledDirectScalarBinding, + ) -> RusticolResult<()> { + let index = u32::try_from(scalars.len()).map_err(|_| { + RusticolError::integrity("compiled Direct-Arena scalar binding count exceeds u32") + })?; + source_inputs.push(CompiledDirectSourceInputBinding::Scalar(index)); + scalars.push(binding); + Ok(()) + } + + match component.kind.as_str() { + "value" => { + if component.global_component >= value_component_count { + return Err(RusticolError::integrity( + "compiled Direct-Arena value input is outside current planes", + )); + } + if structural_zero_components.contains(&component.global_component) { + push_plane(source_inputs, planes, CompiledDirectPlaneBinding::Zero)?; + push_plane(source_inputs, planes, CompiledDirectPlaneBinding::Zero)?; + return Ok(()); + } + let current = |imaginary| { + CompiledDirectPlaneBinding::Arena(CompiledDirectArenaPlane::Current { + component: component.global_component as u32, + imaginary, + }) + }; + push_plane(source_inputs, planes, current(false))?; + push_plane(source_inputs, planes, current(true))?; + } + "momentum" => { + let local = component + .global_component + .checked_sub(value_component_count) + .filter(|index| *index < momentum_component_count) + .ok_or_else(|| { + RusticolError::integrity( + "compiled Direct-Arena momentum input is outside momentum planes", + ) + })?; + push_plane( + source_inputs, + planes, + CompiledDirectPlaneBinding::Momentum { + form: u32::try_from(local / 4).map_err(|_| { + RusticolError::integrity("compiled momentum form exceeds u32") + })?, + lorentz_component: (local % 4) as u16, + }, + )?; + push_plane(source_inputs, planes, CompiledDirectPlaneBinding::Zero)?; + } + "model_parameter" => { + let index = u32::try_from(component.source_id).map_err(|_| { + RusticolError::integrity("compiled model parameter index exceeds u32") + })?; + push_scalar( + source_inputs, + scalars, + CompiledDirectScalarBinding::Parameter { + index, + imaginary: false, + }, + )?; + push_scalar( + source_inputs, + scalars, + CompiledDirectScalarBinding::Parameter { + index, + imaginary: true, + }, + )?; + } + kind => { + return Err(RusticolError::compatibility(format!( + "compiled Direct-Arena does not support input kind {kind:?}" + ))); + } + } + Ok(()) +} + +#[cfg(all(test, feature = "f64-symjit"))] +mod tests { + fn momentum_slot(start: usize, labels: &[usize]) -> GenericMomentumSlotManifest { + GenericMomentumSlotManifest { + momentum_slot_id: start / 4, + momentum_mask: 0, + external_labels: labels.to_vec(), + component_start: start, + component_stop: start + 4, + real_valued: true, + } + } + + #[test] + fn compiled_momentum_planes_preserve_scalar_order_crossing_and_partial_tiles() { + let nested = (0..19) + .map(|point| { + let offset = point as f64; + vec![ + [1e16, -0.0, 3.0 + offset, -1e16], + [-1e16, 0.0, 5.0 - offset, 1.0], + [1.0, -0.0, -7.0, 1e16], + ] + }) + .collect::>(); + let flat = nested + .iter() + .flatten() + .flatten() + .copied() + .collect::>(); + let crossing = [ + InputCrossingMapEntry { + target_index: 0, + source_index: 2, + sign: -1.0, + }, + InputCrossingMapEntry { + target_index: 1, + source_index: 0, + sign: 1.0, + }, + InputCrossingMapEntry { + target_index: 2, + source_index: 1, + sign: -1.0, + }, + ]; + let views = [ + F64MomentumBatchView::from_nested(&nested, 3).unwrap(), + F64MomentumBatchView::from_contiguous_prevalidated(&flat, 19, 3, None).unwrap(), + F64MomentumBatchView::from_contiguous_prevalidated(&flat, 19, 3, Some(&crossing)) + .unwrap(), + ]; + let slots = [ + momentum_slot(0, &[1]), + momentum_slot(4, &[3, 1, 2]), + momentum_slot(8, &[3, 2, 1, 3]), + momentum_slot(16, &[]), + ]; + let initial = [true, false, true]; + let stride = 24; + let mut actual = vec![23.0_f64; 20 * stride]; + let pointer = actual.as_ptr(); + for view in views { + for count in [1, 17, 3, 1] { + let batch = view.subview(1, 1 + count).unwrap(); + let mut expected = actual.clone(); + for slot in &slots { + for point_index in 0..count { + let point = batch.point(point_index); + let mut momentum = [0.0; 4]; + for &label in &slot.external_labels { + let external = label - 1; + let sign = if initial[external] { -1.0 } else { 1.0 }; + for (target, value) in + momentum.iter_mut().zip(point.momentum(external).unwrap()) + { + *target += sign * value; + } + } + for (component, value) in momentum.into_iter().enumerate() { + expected[(slot.component_start + component) * stride + point_index] = + value; + } + } + } + fill_compiled_momentum_planes(batch, &slots, &initial, 20, stride, &mut actual) + .unwrap(); + assert_eq!(actual.as_ptr(), pointer); + for (&actual, expected) in actual.iter().zip(expected) { + assert_eq!(actual.to_bits(), expected.to_bits()); + } + } + } + } + + #[test] + fn compiled_momentum_planes_reject_invalid_generated_ranges_and_labels() { + let values = [0.0; 24]; + let batch = + F64MomentumBatchView::from_contiguous_prevalidated(&values, 2, 3, None).unwrap(); + let mut output = [0.0; 32]; + let valid = momentum_slot(0, &[1]); + for slot in [ + momentum_slot(8, &[1]), + momentum_slot(0, &[0]), + momentum_slot(0, &[4]), + GenericMomentumSlotManifest { + component_stop: 3, + ..valid.clone() + }, + GenericMomentumSlotManifest { + component_stop: 0, + component_start: 4, + ..valid.clone() + }, + ] { + for input in [batch, batch.subview(0, 1).unwrap()] { + assert!( + fill_compiled_momentum_planes( + input, + std::slice::from_ref(&slot), + &[true; 3], + 8, + 4, + &mut output, + ) + .is_err() + ); + } + } + assert!( + fill_compiled_momentum_planes( + batch, + std::slice::from_ref(&valid), + &[], + 8, + 4, + &mut output, + ) + .is_err() + ); + assert!( + fill_compiled_momentum_planes( + batch, + std::slice::from_ref(&valid), + &[true; 3], + 8, + 4, + &mut output[..31], + ) + .is_err() + ); + assert!( + fill_compiled_momentum_planes(batch, &[valid], &[true; 3], 8, 1, &mut output[..8],) + .is_err() + ); + } + + #[test] + fn joint_selection_intersects_helicity_with_union_of_colors_in_leaf_order() { + let schedule = + |stages: Vec>, amplitudes: Vec| CompiledDirectValidatedSchedule { + active_stage_leaves: stages.into_iter().map(Vec::into_boxed_slice).collect(), + active_amplitude_leaves: amplitudes.into_boxed_slice(), + inactive_amplitude_components: Box::new([]), + }; + let helicity = schedule(vec![vec![0, 1, 3], vec![0, 2]], vec![1, 2, 4]); + let first_color = schedule(vec![vec![0, 2], vec![0]], vec![1, 3]); + let second_color = schedule(vec![vec![0, 3], vec![1, 2]], vec![2, 3]); + let (stages, amplitudes) = + intersect_helicity_color_schedules(&helicity, &[&first_color, &second_color]).unwrap(); + assert_eq!(stages, vec![vec![0, 3], vec![0, 2]]); + assert_eq!(amplitudes, vec![1, 2]); + let wrong_stages = schedule(vec![], vec![]); + assert!(intersect_helicity_color_schedules(&helicity, &[&wrong_stages]).is_err()); + } + use super::super::evaluator::count_test_allocations; + use super::*; + use sha2::{Digest, Sha256}; + use std::fs; + use std::process::Command; + use std::sync::atomic::{AtomicU64, Ordering}; + use std::time::Instant; + use symjit::{Compiler, Config, Expr, Storage}; + + static TEST_DIRECTORY_ID: AtomicU64 = AtomicU64::new(0); + + fn liveness_leaf(inputs: &[usize], outputs: &[usize]) -> DirectLeafPlan { + DirectLeafPlan { + input_current_components: inputs.to_vec().into_boxed_slice(), + output_current_components: outputs.to_vec().into_boxed_slice(), + output_amplitude_components: Box::new([]), + scalar_values_per_point: 1, + persistent_scalar_values_per_point: 0, + } + } + + fn liveness_stage(leaves: Vec) -> LoadedStage { + LoadedStage { + leaves: Vec::new(), + leaf_plans: leaves, + } + } + + #[test] + fn stage_liveness_reuses_only_strictly_dead_current_planes() { + let stages = vec![ + liveness_stage(vec![liveness_leaf(&[0], &[1])]), + liveness_stage(vec![liveness_leaf(&[1], &[2])]), + ]; + let amplitude = liveness_stage(vec![liveness_leaf(&[2], &[])]); + let expected = derive_stage_current_layout(&stages, &litude, &[0], 5).unwrap(); + + assert_eq!(expected.physical_component_count, 2); + assert_eq!(expected.live_component_count, 3); + assert_eq!(expected.physical_component(0).unwrap(), 0); + assert_eq!(expected.physical_component(1).unwrap(), 1); + assert_eq!(expected.physical_component(2).unwrap(), 0); + assert_eq!( + expected.canonical_to_physical[3..], + [UNMAPPED_CURRENT_COMPONENT, UNMAPPED_CURRENT_COMPONENT] + ); + for _ in 0..32 { + let actual = derive_stage_current_layout(&stages, &litude, &[0], 5).unwrap(); + assert_eq!(actual.canonical_to_physical, expected.canonical_to_physical); + assert_eq!( + actual.physical_component_count, + expected.physical_component_count + ); + } + } + + #[test] + fn stage_liveness_preserves_arena_v1_whole_stage_no_aliasing() { + let stages = vec![ + liveness_stage(vec![liveness_leaf(&[0], &[1]), liveness_leaf(&[0], &[2])]), + liveness_stage(vec![liveness_leaf(&[1, 2], &[3])]), + ]; + let amplitude = liveness_stage(vec![liveness_leaf(&[3], &[])]); + let layout = derive_stage_current_layout(&stages, &litude, &[0], 4).unwrap(); + + let first_stage_planes = [0, 1, 2] + .into_iter() + .map(|component| layout.physical_component(component).unwrap()) + .collect::>(); + assert_eq!( + first_stage_planes.len(), + 3, + "same-stage input and every output must remain disjoint" + ); + assert_ne!( + layout.physical_component(1).unwrap(), + layout.physical_component(2).unwrap(), + "outputs from separate leaves still share the fused-stage no-alias contract" + ); + } + + #[test] + fn stage_liveness_rejects_a_current_use_without_a_producer() { + let stages = vec![liveness_stage(vec![liveness_leaf(&[1], &[2])])]; + let amplitude = liveness_stage(vec![liveness_leaf(&[2], &[])]); + let error = derive_stage_current_layout(&stages, &litude, &[0], 3).unwrap_err(); + assert!(error.message().contains("used before its producer")); + } + + fn test_payload_directory() -> PathBuf { + let target = std::env::var_os("CARGO_TARGET_DIR") + .map(PathBuf::from) + .unwrap_or_else(|| PathBuf::from("target")); + target.join(format!( + "compiled-direct-engine-test-{}-{}", + std::process::id(), + TEST_DIRECTORY_ID.fetch_add(1, Ordering::Relaxed) + )) + } + + fn source_application(inputs: &[Expr], outputs: &[Expr]) -> Vec { + let mut config = Config::default(); + config.set_opt_level(3); + config.set_complex(true); + config.set_symbolica(true); + config.set_simd(true); + config.set_fast_complex(false); + config.set_compress(true); + let application = Compiler::with_config(config) + .compile_params(&[], outputs, inputs) + .unwrap(); + assert_eq!(application.count_states, 0); + assert_eq!(application.count_params, inputs.len() * 2); + assert_eq!(application.count_obs, outputs.len() * 2); + let mut bytes = Vec::new(); + application.save(&mut bytes).unwrap(); + bytes + } + + fn evaluator( + path: &str, + plane_path: &str, + plane_program: &str, + input_len: usize, + output_len: usize, + ) -> EvaluatorManifest { + EvaluatorManifest::SymjitApplication { + runtime_capability: SYMJIT_APPLICATION_RUNTIME_CAPABILITY.to_string(), + application_path: path.to_string(), + application_abi: SYMJIT_APPLICATION_STORAGE_ABI.to_string(), + input_len, + output_len, + element_layout: "complex-f64".to_string(), + batch_layout: "row-major".to_string(), + compiler_type: "native".to_string(), + translation_mode: "indirect".to_string(), + optimization_level: 3, + word_bits: 64, + endianness: "little".to_string(), + required_defuns: Vec::new(), + plane_application: Some(SymjitPlaneApplicationManifest { + application_path: plane_path.to_string(), + application_abi: SYMJIT_PLANE_APPLICATION_ABI.to_string(), + storage_abi: SYMJIT_APPLICATION_STORAGE_ABI.to_string(), + element_layout: "split-complex-plane-major".to_string(), + descriptor_order: "inputs-re-im-then-outputs-re-im".to_string(), + input_complex_count: input_len, + output_complex_count: output_len, + input_plane_count: input_len * 2, + output_plane_count: output_len * 2, + compiler_type: "native".to_string(), + translation_mode: "symbolica-structured-instructions".to_string(), + optimization_level: 3, + simd: true, + complex: true, + fast_math: true, + fast_complex: false, + compression: true, + threading: false, + direct_arena: true, + source_digest: format!("{:x}", Sha256::digest(plane_program.as_bytes())), + target: serde_json::json!({ + "word_bits": 64, + "endianness": "little", + "triple": "test-native", + "cpu_features": [], + }), + }), + evaluator_state_path: None, + evaluator_state_runtime_capability: None, + } + } + + fn native_direct_fixture() -> (PathBuf, String) { + let directory = test_payload_directory(); + fs::create_dir_all(&directory).unwrap(); + let source = directory.join("native_direct_leaf.cpp"); + let library_name = if cfg!(target_os = "macos") { + "libnative_direct_leaf.dylib" + } else { + "libnative_direct_leaf.so" + }; + let library = directory.join(library_name); + fs::write( + &source, + r#"#include +struct Metadata { + std::uint32_t abi_version; + std::uint32_t struct_size; + std::uint32_t flags; + std::uint32_t input_plane_count; + std::uint32_t scalar_input_count; + std::uint32_t output_plane_count; + std::uint32_t simd_lane_width; + std::uint32_t reserved; +}; +struct InputPlane { const double* values; }; +struct OutputPlane { double* values; }; +struct Scalar { const double* value; }; +extern "C" Metadata native_direct_leaf_direct_application_v1_metadata() noexcept { + return Metadata{1u, sizeof(Metadata), 63u, 2u, 0u, 2u, 2u, 0u}; +} +extern "C" int native_direct_leaf_direct_application_v1( + const InputPlane* inputs, std::uint32_t input_count, + const Scalar*, std::uint32_t scalar_count, + const OutputPlane* outputs, std::uint32_t output_count, + std::uint32_t point_start, std::uint32_t point_count) noexcept { + if (inputs == nullptr || outputs == nullptr || input_count != 2u || + scalar_count != 0u || output_count != 2u || point_count == 0u) return 2; + for (std::uint32_t offset = 0; offset < point_count; ++offset) { + const std::uint32_t point = point_start + offset; + outputs[0].values[point] = 2.0 * inputs[0].values[point]; + outputs[1].values[point] = 2.0 * inputs[1].values[point]; + } + return 0; +} +"#, + ) + .unwrap(); + let compiler = std::env::var("CXX").unwrap_or_else(|_| "c++".to_string()); + let mut command = Command::new(compiler); + command.args(["-std=c++17", "-O3"]); + if cfg!(target_os = "macos") { + command.arg("-dynamiclib"); + } else { + command.args(["-shared", "-fPIC"]); + } + let output = command + .arg(&source) + .arg("-o") + .arg(&library) + .output() + .unwrap(); + assert!( + output.status.success(), + "could not compile native plane fixture: {}", + String::from_utf8_lossy(&output.stderr) + ); + (directory, library_name.to_string()) + } + + fn native_evaluator(library_path: &str, runtime_capability: &str) -> EvaluatorManifest { + EvaluatorManifest::CompiledComplex { + runtime_capability: runtime_capability.to_string(), + function_name: "native_direct_leaf".to_string(), + input_len: 1, + output_len: 1, + library_path: "absent-dense-library".to_string(), + evaluator_state_path: None, + number_type: "complex".to_string(), + native_direct_application: Some(NativeCompiledDirectApplicationManifest { + application_abi: + super::super::evaluator::native_compiled_direct:: + NATIVE_COMPILED_DIRECT_APPLICATION_ABI + .to_string(), + function_name: "native_direct_leaf".to_string(), + source_path: "native_direct_leaf.cpp".to_string(), + library_path: library_path.to_string(), + target: NativeCompiledDirectTargetManifest { + triple: crate::runtime_target_info().triple, + cpu_features: Vec::new(), + }, + evaluator_state_sha256: "a".repeat(64), + instruction_count: 1, + temporary_count: 0, + input_plane_count: 2, + scalar_input_count: 0, + output_plane_count: 2, + simd_lane_width: 2, + logical_stack_bytes: 32, + output_semantics: "factor-free-overwrite".to_string(), + }), + } + } + + fn input_component( + kind: &str, + source_id: usize, + component: usize, + global_component: usize, + parameter_index: usize, + real_valued: bool, + ) -> GenericStageInputComponentManifest { + GenericStageInputComponentManifest { + kind: kind.to_string(), + source_id, + component, + global_component, + parameter_index, + real_valued, + } + } + + fn output_slot( + component: usize, + current_id: isize, + output_column: usize, + ) -> GenericStageOutputSlotManifest { + GenericStageOutputSlotManifest { + value_slot_id: current_id, + current_id, + variant: "test".to_string(), + component_start: component, + component_stop: component + 1, + output_start: output_column, + output_stop: output_column + 1, + color_selector_domain_ids: Vec::new(), + } + } + + fn stage_manifest( + label: &str, + inputs: Vec, + output_components: &[usize], + evaluator: EvaluatorManifest, + amplitude: bool, + ) -> GenericSerializedStageEvaluatorManifest { + let leaf_layout = evaluator.leaf_layout().unwrap(); + let mut direct_application_abi = None; + let mut direct_source_abi = None; + let direct_leaves = leaf_layout + .iter() + .map(|leaf| { + let (application_path, application_abi, optimization_level, input_len, output_len) = + match leaf.evaluator { + EvaluatorManifest::SymjitApplication { + plane_application: Some(plane_application), + optimization_level, + input_len, + output_len, + .. + } => ( + &plane_application.application_path, + &plane_application.application_abi, + *optimization_level, + *input_len, + *output_len, + ), + EvaluatorManifest::CompiledComplex { + input_len, + output_len, + native_direct_application: Some(application), + .. + } => ( + &application.library_path, + &application.application_abi, + 3, + *input_len, + *output_len, + ), + _ => panic!("compiled Direct-Arena test leaf must be direct-capable"), + }; + if let Some(expected) = direct_source_abi.as_ref() { + assert_eq!(expected, application_abi); + } else { + direct_source_abi = Some(application_abi.clone()); + }; + if direct_application_abi.is_none() { + direct_application_abi = + Some(if application_abi == SYMJIT_PLANE_APPLICATION_ABI { + COMPILED_PLANE_DIRECT_APPLICATION_ABI.to_string() + } else { + application_abi.clone() + }); + } + CompiledPlaneLeafManifest { + application_path: application_path.clone(), + source_application_abi: application_abi.clone(), + optimization_level, + direct_codegen_optimization_level: optimization_level, + input_len, + output_len, + input_indices: leaf.input_indices.clone(), + output_start: leaf.output_range.start, + output_stop: leaf.output_range.end, + } + }) + .collect(); + let direct_inputs = inputs + .iter() + .map(|input| CompiledPlaneInputBindingManifest { + parameter_index: input.parameter_index, + kind: input.kind.clone(), + source_id: input.source_id, + component: input.component, + global_component: input.global_component, + real_valued: input.real_valued, + }) + .collect(); + let direct_outputs = output_components + .iter() + .copied() + .enumerate() + .map( + |(output_index, component)| CompiledPlaneOutputBindingManifest { + output_index, + arena: if amplitude { "amplitude" } else { "current" }.to_string(), + component, + }, + ) + .collect(); + let compiled_plane_arena = Some(CompiledPlaneArenaStageManifest { + schema_version: 1, + kind: "compiled-plane-arena-stage".to_string(), + application_abi: direct_application_abi.unwrap(), + source_application_abi: direct_source_abi.unwrap(), + element_layout: "split-complex-component-major".to_string(), + output_operation: "overwrite".to_string(), + output_factor: "identity".to_string(), + input_output_aliasing: "forbidden".to_string(), + output_output_aliasing: "forbidden".to_string(), + input_bindings: direct_inputs, + output_bindings: direct_outputs, + leaves: direct_leaves, + }); + let value_parameter_count = inputs.iter().filter(|item| item.kind == "value").count(); + let momentum_parameter_count = inputs.iter().filter(|item| item.kind == "momentum").count(); + let model_parameter_count = inputs + .iter() + .filter(|item| item.kind == "model_parameter") + .count(); + let real_valued_inputs = inputs + .iter() + .filter_map(|item| item.real_valued.then_some(item.parameter_index)) + .collect(); + GenericSerializedStageEvaluatorManifest { + stage_index: usize::from(amplitude), + stage_kind: if amplitude { + "amplitude".to_string() + } else { + "interaction".to_string() + }, + subset_size: None, + evaluator_label: label.to_string(), + parameter_layout: "stage-local-value-momentum".to_string(), + output_length: output_components.len(), + output_slots: output_components + .iter() + .copied() + .enumerate() + .map(|(column, component)| { + output_slot( + component, + if amplitude { -1 } else { component as isize }, + column, + ) + }) + .collect(), + input_value_slot_ids: Vec::new(), + output_value_slot_ids: Vec::new(), + interaction_ids: Vec::new(), + parameter_count: inputs.len(), + input_components: inputs, + value_parameter_count, + momentum_parameter_count, + model_parameter_count, + real_valued_inputs, + expression_ready: true, + blockers: Vec::new(), + evaluator, + compiled_plane_arena, + } + } + + fn assert_close(actual: Complex, expected: Complex) { + let expected_norm = expected.re.hypot(expected.im); + let delta = actual - expected; + let delta_norm = delta.re.hypot(delta.im); + let tolerance = 1.0e-15 + 1.0e-12 * expected_norm; + assert!( + delta_norm <= tolerance, + "actual={actual:?} expected={expected:?} tolerance={tolerance}" + ); + } + + fn assert_close_real(actual: f64, expected: f64, context: &str) { + let tolerance = 1.0e-15 + 1.0e-12 * actual.abs().max(expected.abs()); + assert!( + (actual - expected).abs() <= tolerance, + "{context}: actual={actual:.17e} expected={expected:.17e} tolerance={tolerance:.17e}" + ); + } + + #[test] + fn cpp_and_asm_native_leaves_execute_only_through_canonical_planes() { + let (payload_root, library_name) = native_direct_fixture(); + let payloads = EvaluatorPayloadStore::directory(&payload_root); + for runtime_capability in [ + SYMBOLICA_COMPILED_CPP_RUNTIME_CAPABILITY, + SYMBOLICA_COMPILED_ASM_RUNTIME_CAPABILITY, + ] { + let amplitude = stage_manifest( + runtime_capability, + vec![input_component("value", 0, 0, 0, 0, false)], + &[0], + native_evaluator(&library_name, runtime_capability), + true, + ); + let mut direct = CompiledDirectEnginePrototype::load( + &[], + &litude, + &payloads, + &[0], + 1, + 1, + 4, + 0, + 1, + 129, + ) + .unwrap(); + let input = (0..129) + .map(|point| Complex::new(point as f64 + 0.25, 0.5 - point as f64)) + .collect::>(); + let state = input + .iter() + .flat_map(|value| { + [ + *value, + Complex::new(0.0, 0.0), + Complex::new(0.0, 0.0), + Complex::new(0.0, 0.0), + Complex::new(0.0, 0.0), + ] + }) + .collect::>(); + direct.begin_tile_from_state(129, 5, &state).unwrap(); + direct.evaluate_all(129).unwrap(); + let mut output = vec![Complex::new(0.0, 0.0); 129]; + direct + .extract_amplitudes_row_major(129, &mut output) + .unwrap(); + for (actual, input) in output.into_iter().zip(&input) { + assert_eq!(actual, *input * 2.0); + } + assert_eq!(direct.traffic().leaf.calls, 1); + direct.traffic().leaf.validate_direct().unwrap(); + } + fs::remove_dir_all(payload_root).unwrap(); + } + + fn disable_compiled_direct_recursive(runtime: &mut ExecutionRuntime) { + runtime.compiled_direct_runtime = None; + runtime.compiled_direct_color_schedules.clear(); + runtime.compiled_direct_helicity_schedules.clear(); + if let Some(sum_runtime) = runtime.helicity_sum_runtime.as_deref_mut() { + disable_compiled_direct_recursive(sum_runtime); + } + for selector_runtime in &mut runtime.helicity_selector_runtimes { + disable_compiled_direct_recursive(selector_runtime); + } + for selector_runtime in runtime.color_selector_runtimes.values_mut() { + disable_compiled_direct_recursive(selector_runtime); + } + } + + fn compiled_direct_runtime_summary(runtime: &ExecutionRuntime) -> (usize, u64, u64, usize) { + let (mut engine_count, mut requested_bytes, mut hot_calls, mut minimum_tile_capacity) = + (0usize, 0u64, 0u64, usize::MAX); + if let Some(direct) = runtime.compiled_direct_runtime.as_ref() { + engine_count += 1; + requested_bytes = + requested_bytes.saturating_add(direct.allocation_counters().requested_bytes); + hot_calls = hot_calls.saturating_add(direct.traffic().leaf.calls); + minimum_tile_capacity = minimum_tile_capacity.min(direct.tile_capacity()); + direct + .traffic() + .leaf + .validate_direct() + .expect("production compiled leaf traffic remains direct"); + assert_eq!(direct.traffic().boundary_input_bytes, 0); + assert_eq!(direct.traffic().boundary_current_output_bytes, 0); + assert_eq!(direct.traffic().boundary_amplitude_output_bytes, 0); + } + let children = runtime + .helicity_sum_runtime + .iter() + .map(Box::as_ref) + .chain(runtime.helicity_selector_runtimes.iter().map(Box::as_ref)) + .chain(runtime.color_selector_runtimes.values().map(Box::as_ref)); + for child in children { + let (child_engines, child_bytes, child_calls, child_tile) = + compiled_direct_runtime_summary(child); + engine_count += child_engines; + requested_bytes = requested_bytes.saturating_add(child_bytes); + hot_calls = hot_calls.saturating_add(child_calls); + minimum_tile_capacity = minimum_tile_capacity.min(child_tile); + } + ( + engine_count, + requested_bytes, + hot_calls, + minimum_tile_capacity, + ) + } + + fn assert_compiled_direct_tile_geometry(runtime: &ExecutionRuntime) { + if let Some(direct) = runtime.compiled_direct_runtime.as_ref() { + let sizing = direct.tile_sizing(); + let stride = direct.arena.point_stride() as usize; + let logical_tile_bytes = stride + .checked_mul(sizing.physical_scalar_values_per_point) + .and_then(|value| value.checked_mul(std::mem::size_of::())) + .expect("retained Direct logical tile bytes fit usize"); + assert!(logical_tile_bytes > 0); + if sizing.reduction_scalar_values_per_point == 0 { + assert_eq!( + sizing.effective_tile_capacity, 1, + "unauthenticated retained reducer did not fail closed" + ); + } + } + let children = runtime + .helicity_sum_runtime + .iter() + .map(Box::as_ref) + .chain(runtime.helicity_selector_runtimes.iter().map(Box::as_ref)) + .chain(runtime.color_selector_runtimes.values().map(Box::as_ref)); + for child in children { + assert_compiled_direct_tile_geometry(child); + } + } + + fn retained_validation_point(runtime: &NativeRuntime) -> Vec { + let path = runtime + .root() + .join("processes") + .join(&runtime.metadata().representative_process_key) + .join("validation-momenta.json"); + let validation: serde_json::Value = + serde_json::from_slice(&fs::read(&path).expect("read retained validation momenta")) + .expect("parse retained validation momenta"); + validation["points"][0] + .as_array() + .expect("one retained validation point") + .iter() + .flat_map(|leg| { + leg["momentum"] + .as_array() + .expect("four retained momentum components") + .iter() + .map(|value| { + value + .as_str() + .expect("decimal retained momentum string") + .parse::() + .expect("retained f64 momentum") + }) + }) + .collect() + } + + #[test] + fn compiled_direct_reducer_shapes_are_statically_authenticated() { + let routed = CompiledDirectRoutedReductionFootprint::default(); + let hot = + |kind, maximum_group_component_count, maximum_resolved_component_count, routed| { + CompiledDirectReductionFootprint::authenticated( + kind, + maximum_group_component_count, + maximum_resolved_component_count, + routed, + ) + .unwrap() + .hot_scalar_values_per_point() + .unwrap() + }; + assert_eq!(hot(CompiledDirectReducerKind::Plain, 1, 0, routed), 3); + assert_eq!(hot(CompiledDirectReducerKind::Coherent, 5, 7, routed), 20); + assert_eq!( + hot( + CompiledDirectReducerKind::Contracted { group_count: 11 }, + 5, + 7, + routed, + ), + 40 + ); + assert_eq!( + hot( + CompiledDirectReducerKind::ColorTopologyReplay { + physical_group_count: 13, + }, + 5, + 7, + routed, + ), + 44 + ); + assert_eq!( + hot( + CompiledDirectReducerKind::Coherent, + 5, + 7, + CompiledDirectRoutedReductionFootprint { + maximum_source_component_count: 512, + maximum_target_component_count: 1, + }, + ), + 526, + "authenticated replay source components can dominate the reducer" + ); + assert_eq!( + hot( + CompiledDirectReducerKind::Coherent, + 5, + 7, + CompiledDirectRoutedReductionFootprint { + maximum_source_component_count: 1, + maximum_target_component_count: 1_024, + }, + ), + 1_038, + "authenticated replay target components can dominate the reducer" + ); + } + + #[test] + fn selected_routed_reduction_tile_uses_actual_components_within_allocated_capacity() { + let mut leaf = liveness_leaf(&[0], &[1]); + leaf.scalar_values_per_point = 4_320; + let stages = vec![liveness_stage(vec![leaf])]; + let amplitude = liveness_stage(vec![liveness_leaf(&[1], &[])]); + let layout = CompiledDirectCurrentLayout::identity(100_000).unwrap(); + let sizing = compiled_direct_tile_sizing( + &stages, + &litude, + &layout, + &[0, 1, 2, 3], + 24, + 32, + CompiledDirectReductionFootprint::Authenticated { + hot_scalar_values_per_point: 188_167, + }, + CompiledDirectCurrentLayoutPolicy::StageLivenessProduction, + ) + .unwrap(); + assert_eq!(sizing.effective_tile_capacity, 32); + assert_eq!(sizing.reduction_tile_capacity, 2); + let selected_footprint = |source, target| { + CompiledDirectReductionFootprint::authenticated( + CompiledDirectReducerKind::Coherent, + 1, + 0, + CompiledDirectRoutedReductionFootprint { + maximum_source_component_count: source, + maximum_target_component_count: target, + }, + ) + .unwrap() + }; + assert_eq!( + sizing + .reduction_tile_capacity_for_footprint(selected_footprint(64, 512)) + .unwrap(), + 32, + "one selected flow must not inherit the full color-axis working set" + ); + assert_eq!( + sizing + .reduction_tile_capacity_for_footprint(selected_footprint(64, 100_000)) + .unwrap(), + 4, + "a genuinely large selected target still limits cache locality" + ); + assert_eq!( + sizing + .reduction_tile_capacity_for_footprint(selected_footprint(100_000, 64)) + .unwrap(), + 4, + "selected source scratch is included as well as target scratch" + ); + let smaller_allocation = CompiledDirectTileSizing { + effective_tile_capacity: 8, + ..sizing + }; + assert_eq!( + smaller_allocation + .reduction_tile_capacity_for_footprint(selected_footprint(1, 1)) + .unwrap(), + 8, + "selected reduction must not enlarge the already allocated arena" + ); + assert_eq!( + sizing + .reduction_tile_capacity_for_footprint( + CompiledDirectReductionFootprint::Unauthenticated, + ) + .unwrap(), + sizing.reduction_tile_capacity, + "unknown reducer shapes retain the existing conservative capacity" + ); + } + + #[test] + fn compiled_direct_tile_policy_uses_authenticated_phase_local_footprints() { + let mut leaf = liveness_leaf(&[0], &[1]); + leaf.scalar_values_per_point = 4_320; + leaf.persistent_scalar_values_per_point = 17; + let stages = vec![liveness_stage(vec![leaf])]; + let amplitude = liveness_stage(vec![liveness_leaf(&[1], &[])]); + let layout = CompiledDirectCurrentLayout::identity(100_000).unwrap(); + let reduction = CompiledDirectReductionFootprint::Authenticated { + hot_scalar_values_per_point: 4_096, + }; + let expected = compiled_direct_tile_sizing( + &stages, + &litude, + &layout, + &[0, 1, 2, 3], + 24, + 8_192, + reduction, + CompiledDirectCurrentLayoutPolicy::StageLivenessProduction, + ) + .unwrap(); + assert_eq!(expected.effective_tile_capacity, 32); + assert_eq!(expected.hot_scalar_values_per_point, 4_320); + assert_eq!(expected.source_scalar_values_per_point, 33); + assert_eq!(expected.reduction_scalar_values_per_point, 4_096); + assert_eq!(expected.persistent_scalar_values_per_point, 17); + assert_eq!( + expected.physical_scalar_values_per_point, + 200_000 + 24 + 16_384 + 1 + 17, + "the hard footprint includes every persistent point-pitched scalar plane" + ); + let expected_stride = + checked_aligned_point_stride(expected.effective_tile_capacity as u32).unwrap(); + let expected_direct_bytes = expected_stride as usize + * expected.physical_scalar_values_per_point + * std::mem::size_of::(); + assert!( + expected_direct_bytes > COMPILED_DIRECT_CACHE_TARGET_BYTES, + "the soft cache target must not reject a larger physical arena" + ); + for _ in 0..32 { + assert_eq!( + compiled_direct_tile_sizing( + &stages, + &litude, + &layout, + &[0, 1, 2, 3], + 24, + 8_192, + reduction, + CompiledDirectCurrentLayoutPolicy::StageLivenessProduction, + ) + .unwrap(), + expected, + "authenticated sizing must be deterministic" + ); + } + + let many_sources = (0..20_000).collect::>(); + let source_limited = compiled_direct_tile_sizing( + &stages, + &litude, + &layout, + &many_sources, + 24, + 1, + CompiledDirectReductionFootprint::authenticated( + CompiledDirectReducerKind::Plain, + 1, + 0, + CompiledDirectRoutedReductionFootprint::default(), + ) + .unwrap(), + CompiledDirectCurrentLayoutPolicy::StageLivenessProduction, + ) + .unwrap(); + assert_eq!(source_limited.source_scalar_values_per_point, 40_025); + assert_eq!(source_limited.hot_scalar_values_per_point, 40_025); + assert_eq!( + source_limited.effective_tile_capacity, 8, + "authenticated source preparation can be the cache-limiting phase" + ); + + let large_reduction = CompiledDirectReductionFootprint::Authenticated { + hot_scalar_values_per_point: 32_768, + }; + let reduction_limited = compiled_direct_tile_sizing( + &stages, + &litude, + &layout, + &[0], + 4, + 1, + large_reduction, + CompiledDirectCurrentLayoutPolicy::StageLivenessProduction, + ) + .unwrap(); + assert_eq!( + reduction_limited.effective_tile_capacity, 32, + "a cold resolved-reduction shape must not fragment total evaluation" + ); + assert_eq!( + reduction_limited.reduction_tile_capacity, 16, + "resolved and routed reductions retain their smaller cache-local tile" + ); + + let hard_limited_layout = CompiledDirectCurrentLayout::identity(1_000_000).unwrap(); + assert_eq!( + compiled_direct_tile_sizing( + &stages, + &litude, + &hard_limited_layout, + &[0], + 4, + 1, + CompiledDirectReductionFootprint::authenticated( + CompiledDirectReducerKind::Plain, + 1, + 0, + CompiledDirectRoutedReductionFootprint::default(), + ) + .unwrap(), + CompiledDirectCurrentLayoutPolicy::StageLivenessProduction, + ) + .unwrap() + .effective_tile_capacity, + 32, + "large physical arenas retain the locality-limited tile" + ); + let hard_limited = compiled_direct_tile_sizing( + &stages, + &litude, + &hard_limited_layout, + &[0], + 4, + 1, + CompiledDirectReductionFootprint::authenticated( + CompiledDirectReducerKind::Plain, + 1, + 0, + CompiledDirectRoutedReductionFootprint::default(), + ) + .unwrap(), + CompiledDirectCurrentLayoutPolicy::StageLivenessProduction, + ) + .unwrap(); + let hard_stride = + checked_aligned_point_stride(hard_limited.effective_tile_capacity as u32).unwrap(); + let hard_direct_bytes = hard_stride as usize + * hard_limited.physical_scalar_values_per_point + * std::mem::size_of::(); + assert!( + hard_direct_bytes > 256 * 1024 * 1024, + "the regression geometry must exceed the removed fixed workspace bound" + ); + assert_eq!( + compiled_direct_tile_sizing( + &stages, + &litude, + &layout, + &[0], + 4, + 1, + CompiledDirectReductionFootprint::Unauthenticated, + CompiledDirectCurrentLayoutPolicy::StageLivenessProduction, + ) + .unwrap() + .effective_tile_capacity, + 1, + "production adaptive tiling fails closed without a static reducer footprint" + ); + let unauthenticated_over_budget_layout = CompiledDirectCurrentLayout { + canonical_to_physical: vec![0].into_boxed_slice(), + physical_component_count: 2_100_000, + live_component_count: 1, + }; + assert_eq!( + compiled_direct_tile_sizing( + &stages, + &litude, + &unauthenticated_over_budget_layout, + &[0], + 4, + 1, + CompiledDirectReductionFootprint::Unauthenticated, + CompiledDirectCurrentLayoutPolicy::StageLivenessProduction, + ) + .unwrap() + .effective_tile_capacity, + 1, + "the unauthenticated fallback remains one point without a fixed byte ceiling" + ); + assert_eq!( + compiled_direct_tile_sizing( + &stages, + &litude, + &hard_limited_layout, + &[0], + 4, + 1, + CompiledDirectReductionFootprint::default(), + CompiledDirectCurrentLayoutPolicy::Identity { tile_capacity: 129 }, + ) + .unwrap() + .effective_tile_capacity, + 129, + "explicit developer/test identity capacity remains exact" + ); + } + + #[test] + fn manifest_driven_engine_matches_row_major_compiled_stages_at_all_tail_boundaries() { + const VALUE_COMPONENTS: usize = 4; + const MOMENTUM_COMPONENTS: usize = 4; + const MODEL_PARAMETERS: usize = 1; + const GLOBAL_PARAMETERS: usize = VALUE_COMPONENTS + MOMENTUM_COMPONENTS + MODEL_PARAMETERS; + const PRODUCT_PROGRAM: &str = + "([('mul', ('out', 0), [('param', 0), ('param', 1)], 0)], 2, [])"; + const SHIFTED_PROGRAM: &str = + "([('add', ('out', 0), [('param', 0), ('param', 1)], 0)], 2, [])"; + + let payload_root = test_payload_directory(); + fs::create_dir_all(&payload_root).unwrap(); + let x = Expr::var("x"); + let energy = Expr::var("energy"); + let coupling = Expr::var("coupling"); + let product = &x * &energy; + let shifted = &x + &coupling; + let product_bytes = + source_application(&[x.clone(), energy.clone()], std::slice::from_ref(&product)); + let shifted_bytes = source_application( + &[x.clone(), coupling.clone()], + std::slice::from_ref(&shifted), + ); + fs::write(payload_root.join("product.symjit"), &product_bytes).unwrap(); + fs::write(payload_root.join("shifted.symjit"), &shifted_bytes).unwrap(); + let left = Expr::var("left"); + let right = Expr::var("right"); + let amplitude_value = &left * &right; + let amplitude_bytes = + source_application(&[left, right], std::slice::from_ref(&litude_value)); + fs::write(payload_root.join("amplitude.symjit"), &litude_bytes).unwrap(); + for (path, program) in [ + ("product.plane.symjit", PRODUCT_PROGRAM), + ("shifted.plane.symjit", SHIFTED_PROGRAM), + ("amplitude.plane.symjit", PRODUCT_PROGRAM), + ] { + fs::write( + payload_root.join(path), + compile_symbolica_program_to_plane_application_bytes(program, 2, 1, 3, true) + .unwrap(), + ) + .unwrap(); + } + + let chunked = EvaluatorManifest::Chunked { + required_runtime_capabilities: vec![SYMJIT_APPLICATION_RUNTIME_CAPABILITY.to_string()], + input_len: Some(3), + chunk_input_indices: Some(vec![vec![0, 1], vec![0, 2]]), + chunks: vec![ + evaluator( + "product.symjit", + "product.plane.symjit", + PRODUCT_PROGRAM, + 2, + 1, + ), + evaluator( + "shifted.symjit", + "shifted.plane.symjit", + SHIFTED_PROGRAM, + 2, + 1, + ), + ], + }; + let stage = stage_manifest( + "real-fused-stage", + vec![ + input_component("value", 0, 0, 0, 0, false), + input_component("momentum", 0, 0, VALUE_COMPONENTS, 1, true), + input_component( + "model_parameter", + 0, + 0, + VALUE_COMPONENTS + MOMENTUM_COMPONENTS, + 2, + true, + ), + ], + &[1, 2], + chunked, + false, + ); + let amplitude = stage_manifest( + "real-amplitude-stage", + vec![ + input_component("value", 1, 0, 1, 0, false), + input_component("value", 2, 0, 2, 1, false), + ], + &[0], + evaluator( + "amplitude.symjit", + "amplitude.plane.symjit", + PRODUCT_PROGRAM, + 2, + 1, + ), + true, + ); + let legacy_amplitude = stage_manifest( + "legacy-amplitude-stage", + vec![ + input_component("value", 1, 0, 1, 0, false), + input_component("value", 2, 0, 2, 1, false), + ], + &[3], + evaluator( + "amplitude.symjit", + "amplitude.plane.symjit", + PRODUCT_PROGRAM, + 2, + 1, + ), + false, + ); + let payloads = EvaluatorPayloadStore::directory(&payload_root); + let mut legacy_stage = StageRuntime::load(&stage, &payloads).unwrap(); + let mut legacy_amplitude_stage = StageRuntime::load(&legacy_amplitude, &payloads).unwrap(); + let mut direct = CompiledDirectEnginePrototype::load( + std::slice::from_ref(&stage), + &litude, + &payloads, + &[0], + 1, + VALUE_COMPONENTS, + MOMENTUM_COMPONENTS, + MODEL_PARAMETERS, + 1, + 129, + ) + .unwrap(); + let existing_selector = CompiledColorSelectorSchedule { + active_stage_chunk_indices: vec![vec![0, 1]], + active_amplitude_chunk_indices: vec![0], + }; + let direct_selector = direct.bind_color_schedule(&existing_selector).unwrap(); + let color_schedules = + BTreeMap::from([(7, direct.bind_color_schedule(&existing_selector).unwrap())]); + let selected_colors = BTreeSet::from([7]); + let joint = direct + .bind_helicity_color_schedule(11, &direct_selector, &selected_colors, &color_schedules) + .unwrap(); + let cached = direct + .bind_helicity_color_schedule(11, &direct_selector, &selected_colors, &color_schedules) + .unwrap(); + assert!(Arc::ptr_eq(&joint, &cached)); + assert_eq!( + joint.active_stage_leaves, + direct_selector.active_stage_leaves + ); + assert_eq!( + joint.active_amplitude_leaves, + direct_selector.active_amplitude_leaves + ); + assert!( + direct + .bind_helicity_color_schedule( + 11, + &direct_selector, + &BTreeSet::new(), + &color_schedules, + ) + .is_err() + ); + assert!( + direct + .bind_helicity_color_schedule( + 11, + &direct_selector, + &BTreeSet::from([8]), + &color_schedules, + ) + .is_err() + ); + let initial_state = |point_count: usize| { + let mut initial = vec![Complex::new(0.0, 0.0); point_count * GLOBAL_PARAMETERS]; + for point in 0..point_count { + let row = point * GLOBAL_PARAMETERS; + initial[row] = + Complex::new(0.25 + point as f64 / 128.0, -0.5 + point as f64 / 512.0); + initial[row + VALUE_COMPONENTS] = Complex::new(20.0 + point as f64 / 64.0, 0.0); + initial[row + VALUE_COMPONENTS + MOMENTUM_COMPONENTS] = Complex::new(1.25, 0.0); + } + initial + }; + + for point_count in [7_usize, 127, 128, 129] { + let initial = initial_state(point_count); + + let mut legacy = initial.clone(); + legacy_stage + .evaluate_f64_into_state(point_count, GLOBAL_PARAMETERS, &mut legacy) + .unwrap(); + legacy_amplitude_stage + .evaluate_f64_into_state(point_count, GLOBAL_PARAMETERS, &mut legacy) + .unwrap(); + + direct + .begin_tile_from_state(point_count, GLOBAL_PARAMETERS, &initial) + .unwrap(); + direct + .evaluate_validated(point_count, &direct_selector) + .unwrap(); + let (allocation_result, allocations, allocated_bytes) = + count_test_allocations(|| direct.evaluate_validated(point_count, &direct_selector)); + allocation_result.unwrap(); + assert_eq!(allocations, 0, "warmed direct engine call allocated"); + assert_eq!( + allocated_bytes, 0, + "warmed direct engine call allocated bytes" + ); + let traffic_before_extract = direct.traffic(); + assert!(traffic_before_extract.boundary_input_bytes > 0); + assert!(traffic_before_extract.leaf.calls > 0); + assert!(traffic_before_extract.leaf.points > 0); + traffic_before_extract.leaf.validate_direct().unwrap(); + let planes = direct.amplitude_planes().unwrap(); + assert_eq!(planes.component_count().unwrap(), 1); + assert_eq!(planes.point_count(), point_count as u32); + + let mut direct_state = initial.clone(); + direct + .copy_current_to_state(point_count, GLOBAL_PARAMETERS, &mut direct_state) + .unwrap(); + let mut direct_amplitude = vec![Complex::new(f64::NAN, f64::NAN); point_count]; + direct + .extract_amplitudes_row_major(point_count, &mut direct_amplitude) + .unwrap(); + let traffic_after_extract = direct.traffic(); + assert_eq!( + traffic_after_extract.boundary_current_output_bytes + - traffic_before_extract.boundary_current_output_bytes, + (point_count * VALUE_COMPONENTS * 2 * std::mem::size_of::()) as u64 + ); + assert_eq!( + traffic_after_extract.boundary_amplitude_output_bytes + - traffic_before_extract.boundary_amplitude_output_bytes, + (point_count * 2 * std::mem::size_of::()) as u64 + ); + traffic_after_extract.leaf.validate_direct().unwrap(); + for (point, direct_value) in direct_amplitude.iter().enumerate() { + let row = point * GLOBAL_PARAMETERS; + assert_close(direct_state[row + 1], legacy[row + 1]); + assert_close(direct_state[row + 2], legacy[row + 2]); + assert_close(*direct_value, legacy[row + 3]); + } + + // Correlated memo reuse must restore overwritten planes, and a + // genuinely changed source must invalidate the dependent leaves. + let mut memo = CorrelatedStageMemo::default(); + for repeat in 0..3 { + let mut correlated_state = initial.clone(); + if repeat == 2 { + correlated_state[0] += Complex::new(0.125, -0.25); + } + direct + .begin_tile_from_state(point_count, GLOBAL_PARAMETERS, &correlated_state) + .unwrap(); + direct + .evaluate_correlated_reusing(point_count, &mut memo) + .unwrap(); + let mut correlated_amplitude = vec![Complex::new(0.0, 0.0); point_count]; + direct + .extract_amplitudes_row_major(point_count, &mut correlated_amplitude) + .unwrap(); + legacy_stage + .evaluate_f64_into_state(point_count, GLOBAL_PARAMETERS, &mut correlated_state) + .unwrap(); + legacy_amplitude_stage + .evaluate_f64_into_state(point_count, GLOBAL_PARAMETERS, &mut correlated_state) + .unwrap(); + for (point, value) in correlated_amplitude.iter().enumerate() { + assert_close(*value, correlated_state[point * GLOBAL_PARAMETERS + 3]); + } + if repeat == 1 { + assert_eq!(memo.reused_leaves, 3); + } + if repeat == 2 { + assert_eq!( + memo.reused_leaves, 3, + "changed current inputs must invalidate the leaf" + ); + } + } + + if point_count == 129 { + const SAMPLES: usize = 7; + const REPEATS: usize = 2_000; + let mut direct_ns = [0_u128; SAMPLES]; + let mut legacy_ns = [0_u128; SAMPLES]; + for sample in 0..SAMPLES { + let mut time_direct = || { + let start = Instant::now(); + for _ in 0..REPEATS { + direct + .evaluate_validated(point_count, &direct_selector) + .unwrap(); + } + start.elapsed().as_nanos() / REPEATS as u128 + }; + let mut time_legacy = || { + let start = Instant::now(); + for _ in 0..REPEATS { + legacy_stage + .evaluate_f64_into_state( + point_count, + GLOBAL_PARAMETERS, + &mut legacy, + ) + .unwrap(); + legacy_amplitude_stage + .evaluate_f64_into_state( + point_count, + GLOBAL_PARAMETERS, + &mut legacy, + ) + .unwrap(); + } + start.elapsed().as_nanos() / REPEATS as u128 + }; + if sample.is_multiple_of(2) { + direct_ns[sample] = time_direct(); + legacy_ns[sample] = time_legacy(); + } else { + legacy_ns[sample] = time_legacy(); + direct_ns[sample] = time_direct(); + } + let direct_planes = direct.amplitude_planes().unwrap(); + std::hint::black_box((&legacy, direct_planes)); + } + direct_ns.sort_unstable(); + legacy_ns.sort_unstable(); + let direct_median = direct_ns[SAMPLES / 2]; + let legacy_median = legacy_ns[SAMPLES / 2]; + eprintln!( + "compiled-direct-engine-prototype points={point_count} samples={SAMPLES} \ + repeats={REPEATS} direct_ns={direct_ns:?} legacy_ns={legacy_ns:?} \ + direct_median_ns={direct_median} legacy_median_ns={legacy_median} \ + speedup={:.6}", + legacy_median as f64 / direct_median as f64 + ); + } + } + + let mut tiled_direct = CompiledDirectEnginePrototype::load_with_current_layout_policy( + std::slice::from_ref(&stage), + &litude, + &payloads, + &[0], + 1, + VALUE_COMPONENTS, + MOMENTUM_COMPONENTS, + MODEL_PARAMETERS, + 1, + CompiledDirectReductionFootprint::authenticated( + CompiledDirectReducerKind::Plain, + 1, + 0, + CompiledDirectRoutedReductionFootprint::default(), + ) + .unwrap(), + CompiledDirectCurrentLayoutPolicy::StageLivenessProduction, + ) + .unwrap(); + let production_tile = tiled_direct.tile_capacity(); + assert_eq!(production_tile, 32); + let tiled_selector = tiled_direct + .bind_color_schedule(&existing_selector) + .unwrap(); + for point_count in [ + 1_usize, + production_tile - 1, + production_tile, + production_tile + 1, + production_tile * 2 + 1, + production_tile * 4 + 1, + ] { + let initial = initial_state(point_count); + let mut legacy = initial.clone(); + legacy_stage + .evaluate_f64_into_state(point_count, GLOBAL_PARAMETERS, &mut legacy) + .unwrap(); + legacy_amplitude_stage + .evaluate_f64_into_state(point_count, GLOBAL_PARAMETERS, &mut legacy) + .unwrap(); + let mut tiled_amplitudes = vec![Complex::new(f64::NAN, f64::NAN); point_count]; + let tiles = tiled_direct + .arena + .point_tiles(point_count as u32, tiled_direct.tile_capacity() as u32) + .unwrap(); + for tile in tiles { + let point_start = tile.point_start as usize; + let tile_points = tile.point_count as usize; + let point_stop = point_start + tile_points; + tiled_direct + .begin_tile_from_state_sources_for_test( + tile_points, + GLOBAL_PARAMETERS, + &initial[point_start * GLOBAL_PARAMETERS..point_stop * GLOBAL_PARAMETERS], + ) + .unwrap(); + tiled_direct + .evaluate_validated(tile_points, &tiled_selector) + .unwrap(); + tiled_direct + .extract_amplitudes_row_major( + tile_points, + &mut tiled_amplitudes[point_start..point_stop], + ) + .unwrap(); + } + for (point, actual) in tiled_amplitudes.iter().copied().enumerate() { + assert_close(actual, legacy[point * GLOBAL_PARAMETERS + 3]); + } + } + + direct + .begin_tile_from_state( + 7, + GLOBAL_PARAMETERS, + &vec![Complex::new(0.0, 0.0); 7 * GLOBAL_PARAMETERS], + ) + .unwrap(); + direct + .evaluate_validated(7, &direct_selector) + .expect("runtime-validated selector schedule must execute"); + let duplicate = CompiledColorSelectorSchedule { + active_stage_chunk_indices: vec![vec![0, 0]], + active_amplitude_chunk_indices: vec![0], + }; + assert!(direct.bind_color_schedule(&duplicate).is_err()); + let missing_prerequisite = CompiledColorSelectorSchedule { + active_stage_chunk_indices: vec![Vec::new()], + active_amplitude_chunk_indices: vec![0], + }; + assert!(direct.bind_color_schedule(&missing_prerequisite).is_err()); + } + + #[test] + fn retained_compiled_o3_artifact_sizing_is_independently_deterministic() { + let Some(root) = std::env::var_os("RUSTICOL_COMPILED_DIRECT_ARTIFACT") else { + return; + }; + let root = PathBuf::from(root); + let first = + NativeRuntime::load(&root, None, None).expect("load first retained Direct runtime"); + let second = + NativeRuntime::load(&root, None, None).expect("load second retained Direct runtime"); + let first_summary = compiled_direct_runtime_summary(&first.runtime); + let second_summary = compiled_direct_runtime_summary(&second.runtime); + assert_eq!( + first_summary, second_summary, + "independent loads derived different nested Direct configurations" + ); + assert!( + first_summary.0 > 0, + "retained sizing fixture has no Direct engine" + ); + assert_eq!( + first.metadata_json().unwrap(), + second.metadata_json().unwrap(), + "independent loads reported different nested Direct provenance" + ); + assert_compiled_direct_tile_geometry(&first.runtime); + assert_compiled_direct_tile_geometry(&second.runtime); + eprintln!( + "retained-compiled-direct-sizing engines={} requested_bytes={} \ + minimum_tile={} metadata={}", + first_summary.0, + first_summary.1, + first_summary.3, + first.metadata_json().unwrap() + ); + } + + #[test] + fn retained_compiled_o3_artifact_dual_runs_canonical_leaf_schedule() { + let Some(root) = std::env::var_os("RUSTICOL_COMPILED_DIRECT_ARTIFACT") else { + return; + }; + let artifact = VerifiedArtifact::open(PathBuf::from(root)) + .expect("open retained compiled Direct-Arena artifact"); + let selection = artifact + .select_process(None) + .expect("select retained process"); + let (loaded, evaluator_root) = + load_verified_evaluator(&artifact, &selection).expect("load retained manifest"); + let LoadedExecutionManifest::Compiled(execution) = loaded else { + panic!("retained Direct-Arena fixture is not a compiled execution"); + }; + let stages = execution + .compiled + .stage_evaluators + .as_ref() + .expect("retained compiled execution has generic stages"); + let payloads = artifact + .evaluator_payload_store(&evaluator_root) + .expect("open retained evaluator payloads"); + let layout = &execution.runtime_schema.parameter_layout; + let value_component_count = layout.value_component_count; + let momentum_component_count = layout.momentum_parameter_count; + let model_parameter_count = layout.model_parameter_count; + let amplitude_component_count = stages.amplitude_stage.output_length; + + let mut legacy_stages = stages + .stages + .iter() + .map(|stage| StageRuntime::load(stage, &payloads)) + .collect::>>() + .expect("load retained row-major stages"); + let mut legacy_amplitude = AmplitudeRuntime::load( + &execution.runtime_schema.amplitude_stage, + &stages.amplitude_stage, + &payloads, + ) + .expect("load retained row-major amplitude stage"); + let (source_components, source_scratch_len) = canonical_source_layout( + &execution.runtime_schema.source_fill.sources, + value_component_count, + ) + .expect("derive retained canonical sources"); + let mut direct = CompiledDirectEnginePrototype::load( + &stages.stages, + &stages.amplitude_stage, + &payloads, + &source_components, + source_scratch_len, + value_component_count, + momentum_component_count, + model_parameter_count, + amplitude_component_count, + 129, + ) + .expect("lower retained compiled O3 stages"); + + let color_plan = build_compiled_color_execution_plan( + stages, + &execution.runtime_schema, + &BTreeSet::new(), + ) + .expect("validate retained selector coverage") + .expect("retained LC artifact has selector coverage"); + let (_, retained_schedule) = color_plan + .schedules_by_materialized_sector + .iter() + .next() + .expect("retained LC artifact has a materialized sector"); + let direct_schedule = direct + .bind_color_schedule(retained_schedule) + .expect("bind production-validated retained selector schedule"); + + let parameter_count = + value_component_count + momentum_component_count + model_parameter_count; + for point_count in [7_usize, 127, 128, 129] { + let mut initial = vec![Complex::new(0.0, 0.0); point_count * parameter_count]; + for point in 0..point_count { + let row = point * parameter_count; + for component in 0..value_component_count { + initial[row + component] = Complex::new( + 0.25 + component as f64 / 97.0 + point as f64 / 4096.0, + -0.125 + component as f64 / 211.0 - point as f64 / 8192.0, + ); + } + for component in 0..momentum_component_count { + initial[row + value_component_count + component] = + Complex::new(2.0 + component as f64 / 17.0 + point as f64 / 1024.0, 0.0); + } + for component in 0..model_parameter_count { + initial[row + value_component_count + momentum_component_count + component] = + Complex::new(0.75 + component as f64 / 31.0, 0.0); + } + } + + let mut legacy = initial.clone(); + for (stage_index, stage) in legacy_stages.iter_mut().enumerate() { + stage + .evaluate_active_chunks_f64_into_state( + point_count, + parameter_count, + &mut legacy, + &retained_schedule.active_stage_chunk_indices[stage_index], + ) + .expect("evaluate retained selected row-major stage"); + } + legacy_amplitude + .evaluate_active_chunks_f64_into_scratch( + point_count, + &legacy, + &retained_schedule.active_amplitude_chunk_indices, + ) + .expect("evaluate retained selected row-major amplitude stage"); + + direct + .begin_tile_from_state(point_count, parameter_count, &initial) + .expect("transpose retained tile"); + direct + .evaluate_validated(point_count, &direct_schedule) + .expect("evaluate retained Direct-Arena selector schedule"); + let (result, allocations, allocated_bytes) = + count_test_allocations(|| direct.evaluate_validated(point_count, &direct_schedule)); + result.expect("repeat retained Direct-Arena selector schedule"); + assert_eq!(allocations, 0, "retained warmed direct call allocated"); + assert_eq!( + allocated_bytes, 0, + "retained warmed direct call allocated bytes" + ); + direct.traffic().leaf.validate_direct().unwrap(); + + let mut direct_amplitudes = + vec![Complex::new(f64::NAN, f64::NAN); point_count * amplitude_component_count]; + direct + .extract_amplitudes_row_major(point_count, &mut direct_amplitudes) + .expect("extract retained amplitudes for oracle comparison"); + assert_eq!( + legacy_amplitude.output_scratch_f64.len(), + direct_amplitudes.len() + ); + for (direct_value, legacy_value) in direct_amplitudes + .iter() + .copied() + .zip(legacy_amplitude.output_scratch_f64.iter().copied()) + { + assert_close(direct_value, legacy_value); + } + + if point_count == 129 { + const SAMPLES: usize = 7; + const REPEATS: usize = 200; + let mut direct_ns = [0_u128; SAMPLES]; + let mut legacy_ns = [0_u128; SAMPLES]; + for sample in 0..SAMPLES { + let mut time_direct = || { + let start = Instant::now(); + for _ in 0..REPEATS { + direct + .evaluate_validated(point_count, &direct_schedule) + .unwrap(); + } + start.elapsed().as_nanos() / REPEATS as u128 + }; + let mut time_legacy = || { + let start = Instant::now(); + for _ in 0..REPEATS { + for (stage_index, stage) in legacy_stages.iter_mut().enumerate() { + stage + .evaluate_active_chunks_f64_into_state( + point_count, + parameter_count, + &mut legacy, + &retained_schedule.active_stage_chunk_indices[stage_index], + ) + .unwrap(); + } + legacy_amplitude + .evaluate_active_chunks_f64_into_scratch( + point_count, + &legacy, + &retained_schedule.active_amplitude_chunk_indices, + ) + .unwrap(); + } + start.elapsed().as_nanos() / REPEATS as u128 + }; + if sample.is_multiple_of(2) { + direct_ns[sample] = time_direct(); + legacy_ns[sample] = time_legacy(); + } else { + legacy_ns[sample] = time_legacy(); + direct_ns[sample] = time_direct(); + } + std::hint::black_box(( + direct.amplitude_planes().unwrap(), + &legacy_amplitude.output_scratch_f64, + )); + } + direct_ns.sort_unstable(); + legacy_ns.sort_unstable(); + let direct_median = direct_ns[SAMPLES / 2]; + let legacy_median = legacy_ns[SAMPLES / 2]; + eprintln!( + "retained-compiled-direct points={point_count} samples={SAMPLES} \ + repeats={REPEATS} direct_ns={direct_ns:?} legacy_ns={legacy_ns:?} \ + direct_median_ns={direct_median} legacy_median_ns={legacy_median} \ + speedup={:.6}", + legacy_median as f64 / direct_median as f64 + ); + } + } + } + + #[test] + fn retained_compiled_o3_artifact_native_runtime_direct_matches_legacy_contract() { + let Some(root) = std::env::var_os("RUSTICOL_COMPILED_DIRECT_ARTIFACT") else { + return; + }; + let root = PathBuf::from(root); + let mut direct = NativeRuntime::load(&root, None, None) + .expect("load retained production Direct runtime"); + let mut legacy = + NativeRuntime::load(&root, None, None).expect("load retained legacy oracle runtime"); + assert_eq!( + compiled_direct_runtime_summary(&direct.runtime), + compiled_direct_runtime_summary(&legacy.runtime), + "independent loads must derive the same nested Direct configuration" + ); + assert_eq!( + direct.metadata_json().unwrap(), + legacy.metadata_json().unwrap(), + "independent loads must report deterministic nested Direct provenance" + ); + disable_compiled_direct_recursive(&mut legacy.runtime); + + let (engine_count, requested_bytes, _, minimum_tile_capacity) = + compiled_direct_runtime_summary(&direct.runtime); + assert!(engine_count > 0, "retained runtime has one Direct engine"); + assert!(requested_bytes > 0, "retained Direct arenas own storage"); + assert!( + minimum_tile_capacity > 0, + "retained Direct engines have a nonzero tile" + ); + eprintln!( + "retained-compiled-direct-production engines={engine_count} \ + requested_bytes={requested_bytes} minimum_tile_capacity={minimum_tile_capacity}" + ); + let metadata = direct.metadata(); + assert_eq!( + metadata.compiled_direct_minimum_effective_tile_capacity, + Some(minimum_tile_capacity) + ); + assert!( + metadata + .compiled_direct_maximum_physical_scalar_values_per_point + .is_some_and(|value| value > 0) + ); + assert!( + metadata + .compiled_direct_maximum_hot_scalar_values_per_point + .is_some_and(|value| value > 0) + ); + assert!( + metadata + .compiled_direct_maximum_source_scalar_values_per_point + .is_some_and(|value| value > 0) + ); + assert!( + metadata + .compiled_direct_maximum_reduction_scalar_values_per_point + .is_some_and(|value| value > 0) + ); + assert_eq!( + direct.metadata_json().unwrap(), + direct.metadata_json().unwrap(), + "compiled Direct effective configuration provenance must be deterministic" + ); + + let point = retained_validation_point(&direct); + for point_count in [1usize, 7, 63, 64, 65, 127, 128, 129, 1023, 1024, 1025] { + let momenta = point.repeat(point_count); + let mut direct_values = vec![f64::NAN; point_count]; + let mut legacy_values = vec![f64::NAN; point_count]; + direct + .evaluate_f64_into(&momenta, point_count, &mut direct_values) + .expect("evaluate retained production Direct totals"); + legacy + .evaluate_f64_into(&momenta, point_count, &mut legacy_values) + .expect("evaluate retained legacy totals"); + for (point_index, (direct_value, legacy_value)) in direct_values + .iter() + .copied() + .zip(legacy_values.iter().copied()) + .enumerate() + { + assert_close_real( + direct_value, + legacy_value, + &format!("unselected total point={point_index} count={point_count}"), + ); + } + } + + let helicities = direct.helicities().expect("retained helicity metadata"); + let computed_helicities = helicities + .iter() + .filter(|helicity| helicity.computed && !helicity.structural_zero) + .collect::>(); + assert!( + !computed_helicities.is_empty(), + "retained artifact has one nonzero computed helicity" + ); + let selected_helicity = computed_helicities[0].id.clone(); + let colors = direct.color_components().expect("retained color metadata"); + assert!(!colors.is_empty(), "retained artifact has one color"); + // Prefer a non-representative physical flow so the test exercises a + // real topology-label permutation rather than only the identity map. + let selected_color = colors + .last() + .expect("retained artifact has one color") + .id + .clone(); + + let selector_points = 129usize; + let selector_momenta = point.repeat(selector_points); + let mut selector_cases = vec![( + "all-flows-single-helicity", + Some(std::slice::from_ref(&selected_helicity)), + None, + )]; + if direct.metadata().color_accuracy == "lc" { + selector_cases.push(( + "single-flow-helicity-sum", + None, + Some(std::slice::from_ref(&selected_color)), + )); + } + for (label, selected_helicities, selected_colors) in selector_cases { + let direct_values = direct + .evaluate_f64_with_selectors( + &selector_momenta, + selector_points, + selected_helicities, + selected_colors, + None, + None, + ) + .expect("evaluate retained Direct global selector"); + let legacy_values = legacy + .evaluate_f64_with_selectors( + &selector_momenta, + selector_points, + selected_helicities, + selected_colors, + None, + None, + ) + .expect("evaluate retained legacy global selector"); + let resolved = direct + .evaluate_resolved_f64( + &selector_momenta, + selector_points, + selected_helicities, + selected_colors, + ) + .expect("resolve retained Direct global selector"); + let resolved_totals = resolved.totals(); + for point_index in 0..selector_points { + assert_close_real( + direct_values[point_index], + legacy_values[point_index], + &format!("{label} direct/legacy point={point_index}"), + ); + assert_close_real( + direct_values[point_index], + resolved_totals[point_index], + &format!("{label} total/resolved point={point_index}"), + ); + } + } + + let compiled_symmetric_group = direct + .runtime + .amplitude_stage + .as_ref() + .and_then(|amplitude| amplitude.color_contraction.as_ref()) + .is_some_and(ColorContractionRuntime::is_symmetric_group); + if helicities.len() == 1 + && direct.metadata().color_accuracy == "full" + && !compiled_symmetric_group + { + let sole_helicity = [selected_helicity.clone()]; + let explicit_total = direct + .evaluate_f64_with_selectors( + &selector_momenta, + selector_points, + Some(&sole_helicity), + None, + None, + None, + ) + .expect("evaluate selected FullColour explicit sole-helicity total"); + let unselected_total = direct + .evaluate_f64(&selector_momenta, selector_points) + .expect("evaluate selected FullColour implicit sole-helicity total"); + let resolved = direct + .evaluate_resolved_f64( + &selector_momenta, + selector_points, + Some(&sole_helicity), + None, + ) + .expect("resolve selected FullColour sole-helicity values"); + assert_eq!(resolved.helicity_ids.as_slice(), sole_helicity.as_slice()); + assert_eq!(resolved.color_ids.len(), 1); + assert_eq!(resolved.point_count, selector_points); + assert_eq!(resolved.values.len(), selector_points); + let resolved_total = resolved.totals(); + for (point_index, ((explicit, unselected), resolved)) in explicit_total + .iter() + .copied() + .zip(unselected_total.iter().copied()) + .zip(resolved_total.iter().copied()) + .enumerate() + { + assert_close_real( + explicit, + unselected, + &format!("sole-helicity explicit/unselected total point={point_index}"), + ); + assert_close_real( + explicit, + resolved, + &format!("sole-helicity total/resolved point={point_index}"), + ); + } + + let sole_helicity_index = + u32::try_from(helicities[0].index).expect("sole public helicity index fits u32"); + let helicity_by_point = vec![sole_helicity_index; selector_points]; + let mut per_point_total = vec![f64::NAN; selector_points]; + direct + .evaluate_f64_into_with_selectors( + &selector_momenta, + selector_points, + None, + None, + Some(&helicity_by_point), + None, + &mut per_point_total, + ) + .expect("warm selected FullColour per-point sole-helicity total"); + let (result, allocations, allocated_bytes) = count_test_allocations(|| { + direct.evaluate_f64_into_with_selectors( + &selector_momenta, + selector_points, + None, + None, + Some(&helicity_by_point), + None, + &mut per_point_total, + ) + }); + result.expect("repeat selected FullColour per-point sole-helicity total"); + assert_eq!(allocations, 0, "warmed sole-helicity total allocated"); + assert_eq!( + allocated_bytes, 0, + "warmed sole-helicity total allocated bytes" + ); + for (point_index, (per_point, explicit)) in per_point_total + .iter() + .copied() + .zip(explicit_total.iter().copied()) + .enumerate() + { + assert_close_real( + per_point, + explicit, + &format!("sole-helicity per-point/global total point={point_index}"), + ); + } + } + + if direct.runtime.lc_topology_replay_enabled { + let selector_batch = F64MomentumBatchView::from_contiguous_prevalidated( + &selector_momenta, + selector_points, + direct.runtime.external_count, + direct.input_crossing_map.as_deref(), + ) + .expect("borrow retained selector batch"); + for (label, selected_helicities, selected_colors) in [ + ("all-components", None, None), + ( + "all-flows-single-helicity", + Some(BTreeSet::from([selected_helicity.clone()])), + None, + ), + ] { + let mut candidate_values = vec![f64::NAN; selector_points]; + let mut oracle_values = vec![f64::NAN; selector_points]; + direct + .runtime + .run_f64_selected_into_unprofiled( + selector_batch, + selected_helicities.as_ref(), + selected_colors.as_ref(), + &mut candidate_values, + ) + .expect("evaluate allocation-free replay candidate"); + direct + .runtime + .run_f64_selected_into_resolved_replay_oracle( + selector_batch, + selected_helicities.as_ref(), + selected_colors.as_ref(), + &mut oracle_values, + ) + .expect("evaluate resolved replay oracle"); + for (point_index, (candidate_value, oracle_value)) in candidate_values + .iter() + .copied() + .zip(oracle_values.iter().copied()) + .enumerate() + { + assert_close_real( + candidate_value, + oracle_value, + &format!("{label} replay candidate/oracle point={point_index}"), + ); + } + } + } + + if direct.metadata().color_accuracy == "lc" + && computed_helicities.len() >= 2 + && colors.len() >= 2 + { + let point_count = 129usize; + let momenta = point.repeat(point_count); + let helicity_by_point = (0..point_count) + .map(|point_index| { + computed_helicities[point_index % 2] + .index + .try_into() + .expect("physical helicity index fits u32") + }) + .collect::>(); + let color_by_point = (0..point_count) + .map(|point_index| { + colors[point_index % 2] + .index + .try_into() + .expect("physical color index fits u32") + }) + .collect::>(); + let direct_values = direct + .evaluate_f64_with_selectors( + &momenta, + point_count, + None, + None, + Some(&helicity_by_point), + Some(&color_by_point), + ) + .expect("evaluate retained Direct per-point selectors"); + let legacy_values = legacy + .evaluate_f64_with_selectors( + &momenta, + point_count, + None, + None, + Some(&helicity_by_point), + Some(&color_by_point), + ) + .expect("evaluate retained legacy per-point selectors"); + for (point_index, (direct_value, legacy_value)) in direct_values + .iter() + .copied() + .zip(legacy_values.iter().copied()) + .enumerate() + { + assert_close_real( + direct_value, + legacy_value, + &format!("per-point selector point={point_index}"), + ); + } + } + + let parameters = direct + .model_parameters() + .expect("retained model parameter metadata"); + if let Some(parameter) = parameters.iter().find(|parameter| parameter.mutable) { + let changed_real = if parameter.default == 0.0 { + 0.125 + } else { + parameter.default * 1.01 + }; + direct + .set_model_parameter(¶meter.name, changed_real, parameter.default_imaginary) + .expect("update retained Direct parameter"); + legacy + .set_model_parameter(¶meter.name, changed_real, parameter.default_imaginary) + .expect("update retained legacy parameter"); + let direct_changed = direct + .evaluate_f64(&point, 1) + .expect("evaluate retained Direct changed parameter"); + let legacy_changed = legacy + .evaluate_f64(&point, 1) + .expect("evaluate retained legacy changed parameter"); + assert_close_real( + direct_changed[0], + legacy_changed[0], + "changed model parameter", + ); + direct + .set_model_parameter( + ¶meter.name, + parameter.default, + parameter.default_imaginary, + ) + .expect("restore retained Direct parameter"); + legacy + .set_model_parameter( + ¶meter.name, + parameter.default, + parameter.default_imaginary, + ) + .expect("restore retained legacy parameter"); + } + + if !cfg!(debug_assertions) { + let mut workloads = vec![ + ("all-components", None, None), + ( + "all-flows-single-helicity", + Some(vec![selected_helicity.clone()]), + None, + ), + ]; + if direct.metadata().color_accuracy == "lc" { + workloads.push(( + "single-flow-helicity-sum", + None, + Some(vec![selected_color.clone()]), + )); + } + for point_count in [128usize, 1024] { + let momenta = point.repeat(point_count); + let repeats = if point_count == 128 { 32 } else { 8 }; + for (label, helicities, colors) in &workloads { + let mut direct_output = vec![f64::NAN; point_count]; + let mut legacy_output = vec![f64::NAN; point_count]; + direct + .evaluate_f64_into_with_selectors( + &momenta, + point_count, + helicities.as_deref(), + colors.as_deref(), + None, + None, + &mut direct_output, + ) + .expect("warm retained Direct timing workload"); + legacy + .evaluate_f64_into_with_selectors( + &momenta, + point_count, + helicities.as_deref(), + colors.as_deref(), + None, + None, + &mut legacy_output, + ) + .expect("warm retained legacy timing workload"); + let mut direct_samples = [0u128; 7]; + let mut legacy_samples = [0u128; 7]; + for sample in 0usize..7 { + let mut time_direct = || { + let start = Instant::now(); + for _ in 0..repeats { + direct + .evaluate_f64_into_with_selectors( + &momenta, + point_count, + helicities.as_deref(), + colors.as_deref(), + None, + None, + &mut direct_output, + ) + .expect("time retained Direct workload"); + } + start.elapsed().as_nanos() / repeats + }; + let mut time_legacy = || { + let start = Instant::now(); + for _ in 0..repeats { + legacy + .evaluate_f64_into_with_selectors( + &momenta, + point_count, + helicities.as_deref(), + colors.as_deref(), + None, + None, + &mut legacy_output, + ) + .expect("time retained legacy workload"); + } + start.elapsed().as_nanos() / repeats + }; + if sample.is_multiple_of(2) { + direct_samples[sample] = time_direct(); + legacy_samples[sample] = time_legacy(); + } else { + legacy_samples[sample] = time_legacy(); + direct_samples[sample] = time_direct(); + } + std::hint::black_box((&direct_output, &legacy_output)); + } + direct_samples.sort_unstable(); + legacy_samples.sort_unstable(); + let direct_median = direct_samples[3]; + let legacy_median = legacy_samples[3]; + eprintln!( + "retained-compiled-direct-wall color_accuracy={} workload={label} \ + points={point_count} samples=7 repeats={repeats} \ + direct_samples_ns={direct_samples:?} \ + legacy_samples_ns={legacy_samples:?} \ + direct_median_ns={direct_median} legacy_median_ns={legacy_median} \ + direct_ns_per_point={:.3} legacy_ns_per_point={:.3} speedup={:.6}", + direct.metadata().color_accuracy, + direct_median as f64 / point_count as f64, + legacy_median as f64 / point_count as f64, + legacy_median as f64 / direct_median as f64, + ); + } + } + } + + if !cfg!(debug_assertions) + && std::env::var_os("RUSTICOL_TEST_BENCHMARK_COMPILED_REPLAY_TOTALS").is_some() + && direct.metadata().color_accuracy == "lc" + { + let selected_helicity_set = BTreeSet::from([selected_helicity.clone()]); + for point_count in [128usize, 1024] { + let momenta = point.repeat(point_count); + let batch = F64MomentumBatchView::from_contiguous_prevalidated( + &momenta, + point_count, + direct.runtime.external_count, + direct.input_crossing_map.as_deref(), + ) + .expect("borrow retained replay timing batch"); + let repeats = if point_count == 128 { 64 } else { 8 }; + for (label, helicities) in [ + ("all-components", None), + ("all-flows-single-helicity", Some(&selected_helicity_set)), + ] { + let mut candidate_output = vec![f64::NAN; point_count]; + let mut oracle_output = vec![f64::NAN; point_count]; + direct + .runtime + .run_f64_selected_into_unprofiled( + batch, + helicities, + None, + &mut candidate_output, + ) + .expect("warm allocation-free replay candidate"); + direct + .runtime + .run_f64_selected_into_resolved_replay_oracle( + batch, + helicities, + None, + &mut oracle_output, + ) + .expect("warm resolved replay oracle"); + let mut candidate_samples = [0u128; 7]; + let mut oracle_samples = [0u128; 7]; + for sample in 0usize..7 { + if sample.is_multiple_of(2) { + let start = Instant::now(); + for _ in 0..repeats { + direct + .runtime + .run_f64_selected_into_unprofiled( + batch, + helicities, + None, + &mut candidate_output, + ) + .expect("time allocation-free replay candidate"); + } + candidate_samples[sample] = start.elapsed().as_nanos() / repeats; + let start = Instant::now(); + for _ in 0..repeats { + direct + .runtime + .run_f64_selected_into_resolved_replay_oracle( + batch, + helicities, + None, + &mut oracle_output, + ) + .expect("time resolved replay oracle"); + } + oracle_samples[sample] = start.elapsed().as_nanos() / repeats; + } else { + let start = Instant::now(); + for _ in 0..repeats { + direct + .runtime + .run_f64_selected_into_resolved_replay_oracle( + batch, + helicities, + None, + &mut oracle_output, + ) + .expect("time resolved replay oracle"); + } + oracle_samples[sample] = start.elapsed().as_nanos() / repeats; + let start = Instant::now(); + for _ in 0..repeats { + direct + .runtime + .run_f64_selected_into_unprofiled( + batch, + helicities, + None, + &mut candidate_output, + ) + .expect("time allocation-free replay candidate"); + } + candidate_samples[sample] = start.elapsed().as_nanos() / repeats; + } + std::hint::black_box((&candidate_output, &oracle_output)); + } + candidate_samples.sort_unstable(); + oracle_samples.sort_unstable(); + let candidate_median = candidate_samples[3]; + let oracle_median = oracle_samples[3]; + let mut candidate_deviations = + candidate_samples.map(|value| value.abs_diff(candidate_median)); + let mut oracle_deviations = + oracle_samples.map(|value| value.abs_diff(oracle_median)); + candidate_deviations.sort_unstable(); + oracle_deviations.sort_unstable(); + eprintln!( + "retained-compiled-replay-wall workload={label} points={point_count} \ + samples=7 repeats={repeats} candidate_samples_ns={candidate_samples:?} \ + oracle_samples_ns={oracle_samples:?} \ + candidate_median_ns={candidate_median} \ + candidate_mad_ns={} oracle_median_ns={oracle_median} oracle_mad_ns={} \ + candidate_ns_per_point={:.3} oracle_ns_per_point={:.3} speedup={:.6}", + candidate_deviations[3], + oracle_deviations[3], + candidate_median as f64 / point_count as f64, + oracle_median as f64 / point_count as f64, + oracle_median as f64 / candidate_median as f64, + ); + } + } + } + + let allocation_points = 128usize; + let allocation_momenta = point.repeat(allocation_points); + let mut allocation_output = vec![f64::NAN; allocation_points]; + direct + .evaluate_f64_into( + &allocation_momenta, + allocation_points, + &mut allocation_output, + ) + .expect("warm retained Direct evaluate-into"); + let (result, allocations, allocated_bytes) = count_test_allocations(|| { + direct.evaluate_f64_into( + &allocation_momenta, + allocation_points, + &mut allocation_output, + ) + }); + result.expect("repeat retained Direct evaluate-into"); + eprintln!( + "retained-compiled-direct-public-allocation allocations={allocations} \ + allocated_bytes={allocated_bytes}" + ); + assert_eq!( + allocations, 0, + "warmed public compiled Direct evaluation allocated" + ); + assert_eq!( + allocated_bytes, 0, + "warmed public compiled Direct evaluation allocated bytes" + ); + let arena_profile = direct + .evaluate_f64_arena_profile_repeated( + &allocation_momenta, + allocation_points, + 3, + None, + None, + ) + .expect("profile warmed retained Direct evaluate-into boundary"); + assert_eq!(arena_profile.values.len(), allocation_points); + for (point_index, (profiled, expected)) in arena_profile + .values + .iter() + .copied() + .zip(&allocation_output) + .enumerate() + { + assert_close_real( + profiled, + *expected, + &format!("warmed retained Direct profile point={point_index}"), + ); + } + assert_eq!( + arena_profile + .profile + .native_input_container_allocation_count, + 0 + ); + assert_eq!(arena_profile.profile.native_output_allocation_count, 0); + assert_eq!(arena_profile.profile.observed_scratch_reallocation_count, 0); + assert!( + arena_profile.profile.compiled_direct_arena_engine_count > 0, + "warmed Direct profile did not authenticate an Arena engine" + ); + assert_eq!( + arena_profile.profile.compiled_direct_arena_call_count, + arena_profile.profile.evaluator_backend_call_count, + ); + assert!( + arena_profile.profile.compiled_direct_arena_call_count > 0, + "warmed Direct profile observed no evaluator calls" + ); + assert_eq!( + arena_profile + .profile + .compiled_direct_arena_minimum_effective_tile_capacity, + metadata + .compiled_direct_minimum_effective_tile_capacity + .unwrap() as u64 + ); + assert_eq!( + arena_profile + .profile + .compiled_direct_arena_maximum_physical_scalar_values_per_point, + metadata + .compiled_direct_maximum_physical_scalar_values_per_point + .unwrap() as u64 + ); + assert_eq!( + arena_profile + .profile + .compiled_direct_arena_maximum_hot_scalar_values_per_point, + metadata + .compiled_direct_maximum_hot_scalar_values_per_point + .unwrap() as u64 + ); + assert_eq!( + arena_profile + .profile + .compiled_direct_arena_maximum_source_scalar_values_per_point, + metadata + .compiled_direct_maximum_source_scalar_values_per_point + .unwrap() as u64 + ); + assert_eq!( + arena_profile + .profile + .compiled_direct_arena_maximum_reduction_scalar_values_per_point, + metadata + .compiled_direct_maximum_reduction_scalar_values_per_point + .unwrap() as u64 + ); + + let allocation_batch = F64MomentumBatchView::from_contiguous_prevalidated( + &allocation_momenta, + allocation_points, + direct.runtime.external_count, + direct.input_crossing_map.as_deref(), + ) + .expect("borrow retained allocation batch"); + direct + .runtime + .run_f64_selected_into_unprofiled(allocation_batch, None, None, &mut allocation_output) + .expect("warm retained Direct runtime"); + let (result, runtime_allocations, runtime_allocated_bytes) = count_test_allocations(|| { + direct.runtime.run_f64_selected_into_unprofiled( + allocation_batch, + None, + None, + &mut allocation_output, + ) + }); + result.expect("repeat retained Direct runtime"); + eprintln!( + "retained-compiled-direct-runtime-allocation allocations={runtime_allocations} \ + allocated_bytes={runtime_allocated_bytes}" + ); + assert_eq!( + runtime_allocations, 0, + "warmed compiled Direct runtime allocated" + ); + assert_eq!( + runtime_allocated_bytes, 0, + "warmed compiled Direct runtime allocated bytes" + ); + + let selected_color_ids = [selected_color.clone()]; + let selected_helicity_ids = [selected_helicity.clone()]; + let selected_colors = BTreeSet::from([selected_color]); + let selected_helicities = BTreeSet::from([selected_helicity]); + let mut allocation_cases = vec![("selected-helicity", Some(&selected_helicities), None)]; + if direct.metadata().color_accuracy == "lc" { + allocation_cases.push(("selected-flow", None, Some(&selected_colors))); + } + for (allocation_label, allocation_helicities, allocation_colors) in allocation_cases { + direct + .runtime + .run_f64_selected_into_unprofiled( + allocation_batch, + allocation_helicities, + allocation_colors, + &mut allocation_output, + ) + .expect("warm retained Direct selected runtime"); + let (result, selected_allocations, selected_allocated_bytes) = + count_test_allocations(|| { + direct.runtime.run_f64_selected_into_unprofiled( + allocation_batch, + allocation_helicities, + allocation_colors, + &mut allocation_output, + ) + }); + result.expect("repeat retained Direct selected runtime"); + eprintln!( + "retained-compiled-direct-{allocation_label}-allocation \ + allocations={selected_allocations} allocated_bytes={selected_allocated_bytes}" + ); + assert_eq!( + selected_allocations, 0, + "warmed selected Direct arena execution allocated" + ); + assert_eq!( + selected_allocated_bytes, 0, + "warmed selected Direct arena execution allocated bytes" + ); + } + let mut profile_cases = vec![( + "selected-helicity", + Some(selected_helicity_ids.as_slice()), + None, + )]; + if direct.metadata().color_accuracy == "lc" { + profile_cases.push(("selected-flow", None, Some(selected_color_ids.as_slice()))); + } + for (label, helicities, colors) in profile_cases { + direct + .evaluate_f64_into_with_selectors( + &allocation_momenta, + allocation_points, + helicities, + colors, + None, + None, + &mut allocation_output, + ) + .unwrap_or_else(|error| panic!("evaluate retained Direct {label} oracle: {error}")); + let selected_profile = direct + .evaluate_f64_arena_profile_repeated( + &allocation_momenta, + allocation_points, + 3, + helicities, + colors, + ) + .unwrap_or_else(|error| panic!("profile retained Direct {label}: {error}")); + for (point_index, (profiled, expected)) in selected_profile + .values + .iter() + .copied() + .zip(&allocation_output) + .enumerate() + { + assert_close_real( + profiled, + *expected, + &format!("profiled retained Direct {label} point={point_index}"), + ); + } + assert_eq!( + selected_profile + .profile + .native_input_container_allocation_count, + 0, + "profiled retained Direct {label} input allocations" + ); + assert_eq!( + selected_profile.profile.native_output_allocation_count, 0, + "profiled retained Direct {label} output allocations" + ); + assert_eq!( + selected_profile.profile.observed_scratch_reallocation_count, 0, + "profiled retained Direct {label} scratch reallocations" + ); + } + + let (engine_count, requested_bytes, hot_calls, minimum_tile_capacity) = + compiled_direct_runtime_summary(&direct.runtime); + assert!(hot_calls > 0, "retained production Direct leaves executed"); + eprintln!( + "retained-compiled-direct-production-final engines={engine_count} \ + requested_bytes={requested_bytes} hot_calls={hot_calls} \ + minimum_tile_capacity={minimum_tile_capacity}" + ); + } + + #[test] + fn retained_compiled_replay_totals_quick_contract() { + let Some(root) = std::env::var_os("RUSTICOL_TEST_COMPILED_REPLAY_QUICK_ARTIFACT") else { + return; + }; + let mut runtime = + NativeRuntime::load(PathBuf::from(root), None, None).expect("load replay artifact"); + assert_eq!( + runtime.metadata().color_accuracy, + "lc", + "quick replay contract requires LC" + ); + let point = retained_validation_point(&runtime); + let helicities = runtime.helicities().expect("retained helicity metadata"); + let selected_helicity = helicities + .iter() + .find(|helicity| helicity.computed && !helicity.structural_zero) + .expect("retained artifact has a nonzero computed helicity") + .id + .clone(); + let colors = runtime.color_components().expect("retained color metadata"); + let selected_color = colors + .last() + .expect("retained artifact has one physical color") + .id + .clone(); + let selected_helicities = BTreeSet::from([selected_helicity]); + let selected_colors = BTreeSet::from([selected_color]); + + let parity_points = 7usize; + let parity_momenta = point.repeat(parity_points); + let parity_batch = F64MomentumBatchView::from_contiguous_prevalidated( + &parity_momenta, + parity_points, + runtime.runtime.external_count, + runtime.input_crossing_map.as_deref(), + ) + .expect("borrow replay parity batch"); + for (label, helicities, colors) in [ + ("all-components", None, None), + ( + "all-flows-single-helicity", + Some(&selected_helicities), + None, + ), + ("single-flow-helicity-sum", None, Some(&selected_colors)), + ] { + let mut candidate = vec![f64::NAN; parity_points]; + let mut oracle = vec![f64::NAN; parity_points]; + runtime + .runtime + .run_f64_selected_into_unprofiled(parity_batch, helicities, colors, &mut candidate) + .expect("evaluate allocation-free replay candidate"); + runtime + .runtime + .run_f64_selected_into_resolved_replay_oracle( + parity_batch, + helicities, + colors, + &mut oracle, + ) + .expect("evaluate resolved replay oracle"); + for (point_index, (candidate, oracle)) in + candidate.iter().copied().zip(oracle).enumerate() + { + assert_close_real( + candidate, + oracle, + &format!("{label} quick replay point={point_index}"), + ); + } + } + + let allocation_points = 128usize; + let allocation_momenta = point.repeat(allocation_points); + let allocation_batch = F64MomentumBatchView::from_contiguous_prevalidated( + &allocation_momenta, + allocation_points, + runtime.runtime.external_count, + runtime.input_crossing_map.as_deref(), + ) + .expect("borrow replay allocation batch"); + let mut output = vec![f64::NAN; allocation_points]; + for (label, helicities, colors) in [ + ("all-components", None, None), + ( + "all-flows-single-helicity", + Some(&selected_helicities), + None, + ), + ("single-flow-helicity-sum", None, Some(&selected_colors)), + ] { + runtime + .runtime + .run_f64_selected_into_unprofiled(allocation_batch, helicities, colors, &mut output) + .expect("warm replay allocation workload"); + let (result, allocations, allocated_bytes) = count_test_allocations(|| { + runtime.runtime.run_f64_selected_into_unprofiled( + allocation_batch, + helicities, + colors, + &mut output, + ) + }); + result.expect("repeat replay allocation workload"); + eprintln!( + "retained-compiled-replay-quick-allocation workload={label} \ + allocations={allocations} allocated_bytes={allocated_bytes}" + ); + assert_eq!(allocations, 0, "warmed replay workload {label} allocated"); + assert_eq!( + allocated_bytes, 0, + "warmed replay workload {label} allocated bytes" + ); + } + } +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/engine/contraction_metadata_tests.rs b/pyamplicol/rust/crates/rusticol-core/src/engine/contraction_metadata_tests.rs new file mode 100644 index 0000000000..418d451636 --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/engine/contraction_metadata_tests.rs @@ -0,0 +1,388 @@ +// SPDX-License-Identifier: 0BSD + +use super::*; +use serde_json::{Value, json}; + +fn contraction_value() -> Value { + json!({ + "name": "weyl", + "left_basis": "weyl-chiral", + "right_basis": "weyl-chiral", + "coefficients": [[1.0, 0.0], [1.0, 0.0]], + "chirality_relation": "opposite", + "metric_signature": null, + }) +} + +fn slot(current_id: usize, start: usize) -> Value { + json!({ + "current_id": current_id, + "component_start": start, + "component_stop": start + 2, + "dimension": 2, + }) +} + +fn value_slot(value_slot_id: usize, current_id: usize, start: usize) -> Value { + json!({ + "value_slot_id": value_slot_id, + "current_id": current_id, + "variant": "source", + "component_start": start, + "component_stop": start + 2, + "dimension": 2, + }) +} + +fn root_value() -> Value { + json!({ + "output_index": 0, + "root_id": 0, + "kind": "direct-contraction", + "left_current_id": 0, + "right_current_id": 1, + "left_slot": slot(0, 0), + "right_slot": slot(1, 2), + "left_value_slot": value_slot(0, 0, 0), + "right_value_slot": value_slot(1, 1, 2), + "vertex_kind": null, + "vertex_particles": null, + "coupling": [1.0, 0.0], + "color_weight": [1.0, 0.0], + "color_sector_id": 0, + "contraction": "weyl", + "contraction_ir": contraction_value(), + "coherent_group_id": 0, + "helicity_weight": 1.0, + "all_sector_weight": 1.0, + }) +} + +fn current_storage() -> GenericCurrentStorageManifest { + serde_json::from_value(json!({ + "component_count": 4, + "number_type": "complex", + "metadata_compacted": true, + "current_slots": [ + { + "current_id": 0, + "component_start": 0, + "component_stop": 2, + "dimension": 2, + "is_source": true, + "particle_id": 1, + "external_mask": 1, + "external_labels": [1], + "helicity_ancestry": 1, + "chirality": 1, + "spin_state": 1, + "flavour_flow": [], + "color_state": {}, + "momentum_mask": 1, + "auxiliary_kind": null + }, + { + "current_id": 1, + "component_start": 2, + "component_stop": 4, + "dimension": 2, + "is_source": true, + "particle_id": -1, + "external_mask": 2, + "external_labels": [2], + "helicity_ancestry": 1, + "chirality": -1, + "spin_state": -1, + "flavour_flow": [], + "color_state": {}, + "momentum_mask": 2, + "auxiliary_kind": null + } + ] + })) + .expect("current storage") +} + +fn propagator(particle_id: i32, anti_particle_id: i32, orientation: &str) -> Value { + json!({ + "identity": { + "canonical_id": format!("model:test:state:{particle_id}"), + "species_id": "model:test:species:fermion", + "anti_canonical_id": format!("model:test:state:{anti_particle_id}"), + "display_name": format!("state_{particle_id}"), + "anti_display_name": format!("state_{anti_particle_id}"), + "pdg_label": particle_id, + "anti_pdg_label": anti_particle_id, + "orientation": orientation, + "self_conjugate": false + }, + "particle_id": particle_id, + "chirality": if particle_id > 0 { 1 } else { -1 }, + "kind": "weyl-fermion", + "backend": "spenso", + "basis": "weyl-chiral", + "applies_propagator": true, + "kernel": "test-weyl", + "full_tensor_network_ready": true, + "mass_class": "massless", + "gauge": null, + "numerator": "test", + "denominator": "test", + "mass_parameter": null, + "width_parameter": null, + "custom_source": null, + "auxiliary_policy": null, + "goldstone_policy": "not-applicable", + "description": "test" + }) +} + +fn stored_value_slot( + value_slot_id: usize, + current_id: usize, + start: usize, + particle_id: i32, +) -> Value { + json!({ + "value_slot_id": value_slot_id, + "current_id": current_id, + "variant": "source", + "component_start": start, + "component_stop": start + 2, + "dimension": 2, + "current_component_start": start, + "current_component_stop": start + 2, + "is_source": true, + "applies_propagator": false, + "particle_id": particle_id, + "external_mask": 1_u64 << current_id, + "external_labels": [current_id + 1], + "momentum_mask": 1_u64 << current_id, + "chirality": if particle_id > 0 { 1 } else { -1 }, + "propagator": propagator( + particle_id, + -particle_id, + if particle_id > 0 { "particle" } else { "antiparticle" }, + ) + }) +} + +fn value_storage() -> GenericValueStorageManifest { + serde_json::from_value(json!({ + "component_count": 4, + "number_type": "complex", + "metadata_compacted": true, + "value_slots": [ + stored_value_slot(0, 0, 0, 1), + stored_value_slot(1, 1, 2, -1) + ] + })) + .expect("value storage") +} + +fn root() -> GenericAmplitudeRootManifest { + serde_json::from_value(root_value()).expect("amplitude root") +} + +#[test] +fn contraction_ir_requires_strict_complete_metadata() { + let parsed: GenericContractionIrManifest = + serde_json::from_value(contraction_value()).expect("ContractionIR"); + assert_eq!(parsed.coefficients.len(), 2); + + let mut missing_nullable = contraction_value(); + missing_nullable + .as_object_mut() + .expect("contraction object") + .remove("metric_signature"); + let error = + serde_json::from_value::(missing_nullable).unwrap_err(); + assert!(error.to_string().contains("metric_signature")); + + let mut malformed_pair = contraction_value(); + malformed_pair["coefficients"] = json!([[1.0], [1.0, 0.0]]); + assert!(serde_json::from_value::(malformed_pair).is_err()); + + let mut unknown = contraction_value(); + unknown["inferred_from_dimension"] = json!(true); + let error = serde_json::from_value::(unknown).unwrap_err(); + assert!(error.to_string().contains("unknown field")); +} + +#[test] +fn pre_contraction_ir_schema_v3_reports_actionable_regeneration_error() { + let mut missing = root_value(); + missing + .as_object_mut() + .expect("root object") + .remove("contraction_ir"); + let parse_error = serde_json::from_value::(missing).unwrap_err(); + + let error = execution_manifest_parse_error("processes/example/execution.json", parse_error); + + assert_eq!(error.kind(), crate::RusticolErrorKind::Compatibility); + assert!( + error + .to_string() + .contains("typed amplitude-contraction metadata") + ); + assert!(error.to_string().contains("regenerate")); +} + +#[test] +fn direct_contraction_ir_matches_projection_slots_and_chirality() { + let currents = current_storage(); + let values = value_storage(); + validate_amplitude_contraction(0, &root(), ¤ts, &values) + .expect("valid typed contraction"); + + let mut projection_mismatch = root(); + projection_mismatch.contraction = "display-only-mismatch".to_string(); + assert!( + validate_amplitude_contraction(0, &projection_mismatch, ¤ts, &values) + .unwrap_err() + .to_string() + .contains("projection") + ); + + let mut basis_mismatch = root(); + basis_mismatch.contraction_ir.left_basis = "dirac".to_string(); + assert!( + validate_amplitude_contraction(0, &basis_mismatch, ¤ts, &values) + .unwrap_err() + .to_string() + .contains("value slots") + ); + + let mut dimension_mismatch = root(); + dimension_mismatch.contraction_ir.coefficients.pop(); + assert!( + validate_amplitude_contraction(0, &dimension_mismatch, ¤ts, &values) + .unwrap_err() + .to_string() + .contains("value slots") + ); + + let mut chirality_mismatch = root(); + chirality_mismatch.contraction_ir.chirality_relation = + GenericContractionChiralityRelationManifest::Equal; + assert!( + validate_amplitude_contraction(0, &chirality_mismatch, ¤ts, &values) + .unwrap_err() + .to_string() + .contains("chirality relation") + ); +} + +#[test] +fn contraction_ir_rejects_nonfinite_zero_and_invalid_vertex_projection() { + let currents = current_storage(); + let values = value_storage(); + + let mut nonfinite = root(); + nonfinite.contraction_ir.coefficients[0][0] = f64::NAN; + assert!( + validate_amplitude_contraction(0, &nonfinite, ¤ts, &values) + .unwrap_err() + .to_string() + .contains("invalid typed contraction") + ); + + let mut zero = root(); + zero.contraction_ir.coefficients = vec![[0.0, 0.0], [0.0, 0.0]]; + assert!( + validate_amplitude_contraction(0, &zero, ¤ts, &values) + .unwrap_err() + .to_string() + .contains("invalid typed contraction") + ); + + let mut vertex = root(); + vertex.kind = "vertex-closure".to_string(); + vertex.vertex_kind = Some(1); + vertex.vertex_particles = Some(vec![1, -1, 25]); + vertex.contraction = "scalar".to_string(); + vertex.contraction_ir = serde_json::from_value(json!({ + "name": "scalar", + "left_basis": "scalar", + "right_basis": "scalar", + "coefficients": [[1.0, 0.0]], + "chirality_relation": "any", + "metric_signature": null + })) + .expect("scalar projection"); + validate_amplitude_contraction(0, &vertex, ¤ts, &values) + .expect("valid scalar projection"); + + vertex.contraction_ir.right_basis = "lorentz-vector".to_string(); + assert!( + validate_amplitude_contraction(0, &vertex, ¤ts, &values) + .unwrap_err() + .to_string() + .contains("scalar projection") + ); +} + +#[test] +fn amplitude_slots_match_storage_and_extreme_chiralities_are_safe() { + let currents = current_storage(); + let valid_root = root(); + validate_slot_ref(&valid_root.left_slot, 0, ¤ts).expect("matching current slot"); + + let mut mismatched = valid_root.left_slot; + mismatched.component_start += 1; + mismatched.component_stop += 1; + assert!( + validate_slot_ref(&mismatched, 0, ¤ts) + .unwrap_err() + .to_string() + .contains("does not match current storage") + ); + + assert!(!valid_current_chirality(i32::MIN)); + assert!(!contraction_chiralities_match( + GenericContractionChiralityRelationManifest::Opposite, + 0, + i32::MIN, + )); +} + +#[test] +fn amplitude_root_kind_is_discriminated_and_matches_current_particles() { + let currents = current_storage(); + let direct = root(); + validate_amplitude_root_kind(0, &direct, ¤ts).expect("direct contraction"); + + let mut stale_direct = root(); + stale_direct.vertex_kind = Some(1); + stale_direct.vertex_particles = Some(vec![1, -1, 25]); + assert!( + validate_amplitude_root_kind(0, &stale_direct, ¤ts) + .unwrap_err() + .to_string() + .contains("carries vertex metadata") + ); + + let mut vertex = root(); + vertex.kind = "vertex-closure".to_string(); + vertex.vertex_kind = Some(1); + vertex.vertex_particles = Some(vec![1, -1, 25]); + validate_amplitude_root_kind(0, &vertex, ¤ts).expect("matching vertex closure"); + + vertex.vertex_kind = Some(-1); + assert!( + validate_amplitude_root_kind(0, &vertex, ¤ts) + .unwrap_err() + .to_string() + .contains("invalid vertex kind") + ); + + vertex.vertex_kind = Some(1); + vertex.vertex_particles = Some(vec![-1, 1, 25]); + assert!( + validate_amplitude_root_kind(0, &vertex, ¤ts) + .unwrap_err() + .to_string() + .contains("does not match its input currents") + ); +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/engine/correlated.rs b/pyamplicol/rust/crates/rusticol-core/src/engine/correlated.rs new file mode 100644 index 0000000000..84fe838504 --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/engine/correlated.rs @@ -0,0 +1,852 @@ +// SPDX-License-Identifier: 0BSD + +//! Opt-in coherent Born correlations. Ordinary reductions never enter this lane. + +use super::*; + +/// One literal complex four-vector broadcasts; otherwise supply one per point. +pub type NativeSpinCorrelationVectors = BTreeMap>; + +#[derive(Clone, Debug)] +pub struct NativeCorrelatedRequest { + pub color_correlation: String, + /// None inherits the runtime setter; an empty map restores physical sources. + pub spin_vectors: Option, +} + +impl Default for NativeCorrelatedRequest { + fn default() -> Self { + Self { + color_correlation: "born".into(), + spin_vectors: None, + } + } +} + +#[derive(Clone, Debug)] +pub struct NativeCorrelatedEvaluation { + pub point_count: usize, + pub request_count: usize, + /// Request-major: values[request * point_count + point]. + pub values: Vec<[f64; 2]>, +} + +#[derive(Deserialize)] +struct Catalogue { + schema_version: u32, + complete_source_basis: bool, + processes: BTreeMap, +} + +#[derive(Deserialize)] +struct ProcessCatalogue { + declarations: Declarations, + spin_legs: Vec, + matrices: Vec, + coherent_groups: Vec, +} + +#[derive(Deserialize)] +struct Declarations { + color_correlations: Vec, + spin_correlations: Vec>, +} + +#[derive(Deserialize)] +struct Declaration { + id: String, + bra: Vec, + ket: Vec, +} + +#[derive(Deserialize)] +struct GroupRecord { + group_id: i64, + helicities: Vec, + color_sector_id: i64, + color_word: Vec, +} + +#[derive(Deserialize)] +struct MatrixRecord { + id: String, + order: usize, + bra: Vec, + ket: Vec, + convention: String, + color_accuracy: String, + output_legs: Value, + sector_ids: Vec, + storage: String, + includes_color_factor: bool, + entries: Vec, +} + +#[derive(Deserialize)] +struct EntryRecord { + left_sector_id: i64, + right_sector_id: i64, + weight: WeightRecord, +} + +#[derive(Deserialize)] +struct WeightRecord { + real: [Value; 2], + imag: [Value; 2], +} + +struct Matrix { + id: String, + sectors: BTreeSet, + entries: Vec<(i64, i64, [f64; 2])>, +} + +struct Group { + helicity_id: String, + helicities: Vec, + sector: i64, + roots: Vec, +} + +pub(super) struct CorrelatedRuntime { + catalogue_json: String, + matrices: Vec, + spin_classes: BTreeSet>, + known_helicities: BTreeSet, + groups: Vec, + vectors: NativeSpinCorrelationVectors, +} + +fn invalid(message: impl Into) -> RusticolError { + RusticolError::invalid_argument(message) +} + +fn malformed(message: impl Into) -> RusticolError { + RusticolError::integrity(format!( + "invalid correlated process output: {}", + message.into() + )) +} + +fn validate_catalogue_artifact_id(loaded: &str, reopened: &str) -> RusticolResult<()> { + if loaded != reopened { + return Err(malformed( + "process output changed since the runtime was loaded; reload the runtime before correlated evaluation", + )); + } + Ok(()) +} + +fn rational_f64(parts: &[Value; 2]) -> RusticolResult { + let integer = |value: &Value| -> RusticolResult { + let text = match value { + Value::String(text) => text.clone(), + Value::Number(number) if number.is_i64() || number.is_u64() => number.to_string(), + _ => return Err(malformed("matrix coefficient is not an integer")), + }; + let digits = text.strip_prefix('-').unwrap_or(&text); + if digits.is_empty() || !digits.bytes().all(|value| value.is_ascii_digit()) { + return Err(malformed("matrix coefficient is not an integer")); + } + text.parse::() + .ok() + .filter(|value| value.is_finite()) + .ok_or_else(|| malformed("matrix coefficient exceeds binary64 range")) + }; + let denominator = integer(&parts[1])?; + if denominator == 0.0 { + return Err(malformed("zero matrix denominator")); + } + let value = integer(&parts[0])? / denominator; + if !value.is_finite() { + return Err(malformed("matrix coefficient exceeds binary64 range")); + } + Ok(value) +} + +impl CorrelatedRuntime { + fn load(native: &NativeRuntime) -> RusticolResult { + let identity = native + .external_permutation + .iter() + .copied() + .eq(0..native.runtime.external_count); + if let Some(group) = native.process_group.as_deref() + && (group.conjugated || !identity) + { + return Err(RusticolError::compatibility(format!( + "correlated evaluation of process-group member {:?} through its representative {:?} is available only for an identity leg order without charge conjugation; generate the process explicitly", + native.process_key, group.representative_process_key + ))); + } + if !matches!(native.execution_lane, NativeExecutionLane::Compiled) || !identity { + return Err(RusticolError::compatibility( + "correlations require compiled process output with identity external ordering", + )); + } + let artifact = VerifiedArtifact::open(&native.root)?; + validate_catalogue_artifact_id(&native.artifact_id, &artifact.manifest().artifact_id)?; + let extension = artifact + .manifest() + .extensions + .get("correlators") + .ok_or_else(|| { + RusticolError::compatibility("correlations must be declared at generation") + })?; + if extension.get("schema_version").and_then(Value::as_u64) != Some(1) { + return Err(malformed("unsupported catalogue version")); + } + let path = extension + .get("path") + .and_then(Value::as_str) + .ok_or_else(|| malformed("missing catalogue path"))?; + let mut payload: Catalogue = serde_json::from_slice(&artifact.read_payload(path)?) + .map_err(|error| malformed(format!("catalogue: {error}")))?; + if payload.schema_version != 1 || !payload.complete_source_basis { + return Err(malformed("correlations require a complete source basis")); + } + let process = payload + .processes + .remove(&native.representative_process_id) + .ok_or_else(|| malformed("selected process is absent from catalogue"))?; + Self::from_process(process, &native.runtime, native.process_physics()?) + } + + fn from_process( + process: ProcessCatalogue, + runtime: &ExecutionRuntime, + physics: &ProcessPhysicsV1, + ) -> RusticolResult { + if runtime.lc_topology_replay_enabled + || runtime.color_topology_replay_enabled + || runtime.helicity_recurrence.is_some() + || runtime.helicity_sum_runtime.is_some() + { + return Err(malformed( + "correlated amplitudes must not use physical-helicity reductions", + )); + } + let spin_classes: BTreeSet<_> = + process.declarations.spin_correlations.into_iter().collect(); + let mut spin_legs = BTreeSet::new(); + for class in &spin_classes { + if class.is_empty() + || class.windows(2).any(|pair| pair[0] >= pair[1]) + || class + .iter() + .any(|&leg| leg == 0 || leg > runtime.external_count) + { + return Err(malformed("invalid joint spin-correlation class")); + } + spin_legs.extend(class.iter().copied()); + } + if spin_legs.iter().copied().collect::>() != process.spin_legs { + return Err(malformed("inconsistent declared spin legs")); + } + for leg in spin_legs { + let sources: Vec<_> = runtime + .sources + .iter() + .filter(|source| source.leg_label == leg) + .collect(); + if sources.is_empty() + || sources.iter().any(|source| { + !matches!( + source.source_ir.wavefunction_family, + GenericWavefunctionFamilyManifest::Vector + ) || source.source_ir.component_dimension != 4 + || source + .value_slot + .component_stop + .checked_sub(source.value_slot.component_start) + != Some(4) + }) + { + return Err(malformed( + "spin correlation requires retained four-component vector sources", + )); + } + } + let mut declarations = BTreeMap::from([("born".to_owned(), (Vec::new(), Vec::new()))]); + for declaration in process.declarations.color_correlations { + if declaration.id.is_empty() + || declarations + .insert(declaration.id, (declaration.bra, declaration.ket)) + .is_some() + { + return Err(malformed("repeated correlation declaration")); + } + } + let mut matrices = Vec::new(); + let mut catalogue = Vec::new(); + for matrix in process.matrices { + if matrix.convention != "su3-literal-tau-physical-charge-all-outgoing-v1" + || !["lc", "nlc", "full"].contains(&matrix.color_accuracy.as_str()) + || matrix.storage != "sparse-directed" + || !matrix.includes_color_factor + || matrix.order != matrix.bra.len() + || matrix.order != matrix.ket.len() + || declarations.remove(&matrix.id) != Some((matrix.bra.clone(), matrix.ket.clone())) + { + return Err(malformed("unsupported or inconsistent colour matrix")); + } + let sectors: BTreeSet<_> = matrix.sector_ids.iter().copied().collect(); + if sectors.is_empty() + || sectors.len() != matrix.sector_ids.len() + || sectors.iter().any(|&id| id < 0) + { + return Err(malformed("invalid matrix sector IDs")); + } + let mut pairs = BTreeSet::new(); + let mut entries = Vec::new(); + for entry in matrix.entries { + if !sectors.contains(&entry.left_sector_id) + || !sectors.contains(&entry.right_sector_id) + || !pairs.insert((entry.left_sector_id, entry.right_sector_id)) + { + return Err(malformed("invalid or repeated matrix entry")); + } + entries.push(( + entry.left_sector_id, + entry.right_sector_id, + [ + rational_f64(&entry.weight.real)?, + rational_f64(&entry.weight.imag)?, + ], + )); + } + catalogue.push(serde_json::json!({"id":matrix.id,"order":matrix.order,"bra":matrix.bra,"ket":matrix.ket, + "color_accuracy":matrix.color_accuracy,"output_legs":matrix.output_legs,"sector_ids":matrix.sector_ids})); + matrices.push(Matrix { + id: matrix.id, + sectors, + entries, + }); + } + if !declarations.is_empty() { + return Err(malformed("missing colour matrices")); + } + let mut known_helicities = BTreeSet::new(); + let mut helicities = BTreeMap::new(); + let mut computed = BTreeSet::new(); + for helicity in &physics.helicities { + if helicity.id != helicity.representative_id + || helicity.values.len() != runtime.external_count + || helicity.computed == helicity.structural_zero + || helicity.coefficient != if helicity.computed { 1.0 } else { 0.0 } + || !known_helicities.insert(helicity.id.clone()) + || helicities + .insert(helicity.values.clone(), helicity.id.clone()) + .is_some() + { + return Err(malformed( + "correlations require complete unquotiented helicities", + )); + } + if helicity.computed { + computed.insert(helicity.id.clone()); + } + } + let amplitude = runtime + .amplitude_stage + .as_ref() + .ok_or_else(|| malformed("missing amplitude stage"))?; + if amplitude + .raw_sum_weights + .iter() + .any(|&weight| weight != 1.0) + || amplitude + .raw_sum_all_sector_weights + .iter() + .any(|&weight| weight != 1.0) + { + return Err(malformed("correlated roots have folded weights")); + } + let raw: BTreeMap<_, _> = amplitude + .raw_sum_groups + .iter() + .map(|group| (group.id, group)) + .collect(); + let mut seen = BTreeSet::new(); + let mut roots = BTreeSet::new(); + let mut groups: Vec = Vec::new(); + let mut physical = BTreeMap::new(); + let mut words = BTreeMap::new(); + for record in process.coherent_groups { + let helicity_id = helicities + .get(&record.helicities) + .filter(|id| computed.contains(*id)) + .ok_or_else(|| malformed("unknown coherent helicity"))?; + let group = raw + .get(&record.group_id) + .ok_or_else(|| malformed("unknown coherent group"))?; + if !seen.insert(record.group_id) + || group.weight != 1.0 + || group.all_sector_weight != 1.0 + { + return Err(malformed("repeated or folded coherent group")); + } + if group + .indices + .iter() + .any(|&index| index >= runtime.amplitude_output_count || !roots.insert(index)) + { + return Err(malformed("invalid coherent root mapping")); + } + let key = (record.helicities.clone(), record.color_sector_id); + let position = if let Some(&position) = physical.get(&key) { + if words.get(&key) != Some(&record.color_word) { + return Err(malformed("inconsistent coherent colour word")); + } + position + } else { + let position = groups.len(); + physical.insert(key.clone(), position); + words.insert(key, record.color_word); + groups.push(Group { + helicity_id: helicity_id.clone(), + helicities: record.helicities, + sector: record.color_sector_id, + roots: Vec::new(), + }); + position + }; + groups[position].roots.extend(group.indices.iter().copied()); + } + if seen.len() != raw.len() + || roots.len() != runtime.amplitude_output_count + || groups + .iter() + .map(|group| group.helicity_id.clone()) + .collect::>() + != computed + { + return Err(malformed("incomplete coherent amplitude coverage")); + } + let mut basis: BTreeMap<&str, BTreeSet> = BTreeMap::new(); + for group in &groups { + basis + .entry(&group.helicity_id) + .or_default() + .insert(group.sector); + } + if matrices + .iter() + .any(|matrix| basis.values().any(|sectors| sectors != &matrix.sectors)) + { + return Err(malformed( + "coherent groups do not cover the colour matrices", + )); + } + let catalogue_json = + serde_json::to_string(&catalogue).map_err(|error| malformed(error.to_string()))?; + Ok(Self { + catalogue_json, + matrices, + spin_classes, + known_helicities, + groups, + vectors: BTreeMap::new(), + }) + } + + fn validate_vectors( + &self, + vectors: &NativeSpinCorrelationVectors, + point_count: Option, + ) -> RusticolResult<()> { + let legs: Vec<_> = vectors.keys().copied().collect(); + if !legs.is_empty() && !self.spin_classes.contains(&legs) { + return Err(invalid( + "this joint spin-correlation class was not declared at generation", + )); + } + for batch in vectors.values() { + if batch.is_empty() + || point_count.is_some_and(|count| batch.len() != 1 && batch.len() != count) + || batch + .iter() + .flatten() + .flatten() + .any(|value| !value.is_finite()) + { + return Err(invalid( + "spin vectors must be finite, with one vector or one per phase-space point", + )); + } + } + Ok(()) + } + + fn selected_groups( + &self, + ids: Option<&[String]>, + vectors: &NativeSpinCorrelationVectors, + ) -> Vec { + let selected = |group: &Group| ids.is_none_or(|ids| ids.contains(&group.helicity_id)); + let spectator = |group: &Group| { + group + .helicities + .iter() + .enumerate() + .filter(|(leg, _)| !vectors.contains_key(&(leg + 1))) + .map(|(_, &value)| value) + .collect::>() + }; + let mut representatives: BTreeMap, &Vec> = BTreeMap::new(); + for group in self.groups.iter().filter(|group| selected(group)) { + representatives + .entry(spectator(group)) + .and_modify(|values| { + if group.helicities < **values { + *values = &group.helicities; + } + }) + .or_insert(&group.helicities); + } + self.groups + .iter() + .enumerate() + .filter(|(_, group)| { + selected(group) + && representatives.get(&spectator(group)) == Some(&&group.helicities) + }) + .map(|(index, _)| index) + .collect() + } +} + +impl NativeRuntime { + fn ensure_correlated(&mut self) -> RusticolResult<()> { + if self.correlated.is_none() { + self.correlated = Some(CorrelatedRuntime::load(self)?); + } + Ok(()) + } + + pub fn available_color_correlations_json(&mut self) -> RusticolResult { + self.ensure_correlated()?; + Ok(self + .correlated + .as_ref() + .expect("initialized") + .catalogue_json + .clone()) + } + + pub fn color_correlation_ids(&mut self) -> RusticolResult> { + self.ensure_correlated()?; + Ok(self + .correlated + .as_ref() + .expect("initialized") + .matrices + .iter() + .map(|matrix| matrix.id.clone()) + .collect()) + } + + pub fn set_spin_correlation_vectors_f64( + &mut self, + vectors: NativeSpinCorrelationVectors, + ) -> RusticolResult<()> { + self.ensure_correlated()?; + let correlated = self.correlated.as_mut().expect("initialized"); + correlated.validate_vectors(&vectors, None)?; + correlated.vectors = vectors; + Ok(()) + } + + /// Evaluate all requested complex bilinears, sharing each spin assignment's amplitudes. + pub fn evaluate_correlated_many_f64( + &mut self, + momenta: &[f64], + point_count: usize, + requests: &[NativeCorrelatedRequest], + helicity_ids: Option<&[String]>, + ) -> RusticolResult { + self.ensure_correlated()?; + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + { + let mut evaluation = evaluate_many(self, momenta, point_count, requests, helicity_ids)?; + self.apply_process_group_factor(evaluation.values.as_flattened_mut()); + Ok(evaluation) + } + #[cfg(not(any(feature = "f64-compiled", feature = "f64-symjit")))] + { + let _ = (momenta, point_count, requests, helicity_ids); + Err(RusticolError::compatibility( + "native correlations require binary64 compiled-stage support", + )) + } + } +} + +#[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] +fn evaluate_many( + native: &mut NativeRuntime, + momenta: &[f64], + point_count: usize, + requests: &[NativeCorrelatedRequest], + helicity_ids: Option<&[String]>, +) -> RusticolResult { + let expected = point_count + .checked_mul(native.runtime.external_count) + .and_then(|n| n.checked_mul(4)); + if point_count == 0 + || expected != Some(momenta.len()) + || momenta.iter().any(|value| !value.is_finite()) + { + return Err(invalid( + "correlated momenta must be finite with shape [point][external leg][4]", + )); + } + let correlated = native.correlated.as_ref().expect("initialized"); + if helicity_ids.is_some_and(|ids| { + ids.iter() + .any(|id| !correlated.known_helicities.contains(id)) + }) { + return Err(invalid("unknown correlated helicity ID")); + } + let size = point_count + .checked_mul(requests.len()) + .ok_or_else(|| invalid("correlated result shape overflow"))?; + let mut assignments: Vec<&NativeSpinCorrelationVectors> = Vec::new(); + let mut keys = BTreeMap::new(); + let mut routes = Vec::new(); + for request in requests { + let matrix = correlated + .matrices + .iter() + .position(|matrix| matrix.id == request.color_correlation) + .ok_or_else(|| { + invalid(format!( + "unknown colour correlation ID {:?}", + request.color_correlation + )) + })?; + let vectors = request.spin_vectors.as_ref().unwrap_or(&correlated.vectors); + correlated.validate_vectors(vectors, Some(point_count))?; + // Canonicalize broadcast and per-point forms; preserve signed zero. + let key: Vec<_> = vectors + .iter() + .map(|(&leg, batch)| { + ( + leg, + (0..point_count) + .flat_map(|point| { + batch[if batch.len() == 1 { 0 } else { point }] + .into_iter() + .flatten() + .map(f64::to_bits) + }) + .collect::>(), + ) + }) + .collect(); + let assignment = *keys.entry(key).or_insert_with(|| { + assignments.push(vectors); + assignments.len() - 1 + }); + routes.push((assignment, matrix)); + } + let mut values = vec![[0.0; 2]; size]; + if requests.is_empty() { + return Ok(NativeCorrelatedEvaluation { + point_count, + request_count: 0, + values, + }); + } + let selected: Vec<_> = assignments + .iter() + .map(|vectors| correlated.selected_groups(helicity_ids, vectors)) + .collect(); + let runtime = &mut native.runtime; + let direct = runtime.compiled_direct_runtime.as_mut().ok_or_else(|| { + RusticolError::compatibility( + "native correlations require retained generic Direct-Arena compiled stages", + ) + })?; + let batch = F64MomentumBatchView::from_contiguous_prevalidated( + momenta, + point_count, + runtime.external_count, + None, + )?; + let mut point_start = 0; + while point_start < point_count { + let point_stop = (point_start + direct.tile_capacity()).min(point_count); + let count = point_stop - point_start; + // Memo lifetime is exactly one point tile: momenta and parameters cannot change. + let mut memo = compiled_direct_prototype::CorrelatedStageMemo::default(); + for (assignment, vectors) in assignments.iter().enumerate() { + direct.begin_tile_from_inputs( + batch.subview(point_start, point_stop)?, + &runtime.sources, + None, + runtime.external_count, + &runtime.particle_masses, + &runtime.momentum_slots, + &runtime.external_is_initial, + &runtime.model_parameter_values_f64, + )?; + direct.replace_correlated_sources(&runtime.sources, vectors, point_start, count)?; + if assignments.len() > 1 { + direct.evaluate_correlated_reusing(count, &mut memo)?; + } else { + direct.evaluate_all(count)?; + } + let planes = direct.amplitude_planes()?; + let mut amplitudes = vec![[0.0; 2]; selected[assignment].len() * count]; + let mut by_helicity: BTreeMap<&str, BTreeMap> = BTreeMap::new(); + for (position, &index) in selected[assignment].iter().enumerate() { + let group = &correlated.groups[index]; + by_helicity + .entry(&group.helicity_id) + .or_default() + .insert(group.sector, position); + for &root in &group.roots { + let (re, im) = planes.plane_unchecked(root); + for point in 0..count { + amplitudes[position * count + point][0] += re[point]; + amplitudes[position * count + point][1] += im[point]; + } + } + } + for (request, &(_, matrix)) in routes + .iter() + .enumerate() + .filter(|(_, (id, _))| *id == assignment) + { + for sectors in by_helicity.values() { + for &(left, right, weight) in &correlated.matrices[matrix].entries { + let a = sectors[&left] * count; + let b = sectors[&right] * count; + for point in 0..count { + let [ar, ai] = amplitudes[a + point]; + let [br, bi] = amplitudes[b + point]; + let wr = ar * weight[0] + ai * weight[1]; + let wi = ar * weight[1] - ai * weight[0]; + let value = &mut values[request * point_count + point_start + point]; + value[0] += wr * br - wi * bi; + value[1] += wr * bi + wi * br; + } + } + } + for value in &mut values + [request * point_count + point_start..request * point_count + point_stop] + { + value[0] *= runtime.normalization_factor; + value[1] *= runtime.normalization_factor; + } + } + } + point_start = point_stop; + } + Ok(NativeCorrelatedEvaluation { + point_count, + request_count: requests.len(), + values, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn lazy_catalogue_cannot_bind_to_replaced_process_output() { + assert!(validate_catalogue_artifact_id("loaded-output", "loaded-output").is_ok()); + assert!(validate_catalogue_artifact_id("loaded-output", "replacement-output").is_err()); + } + + #[test] + fn exact_weights_reject_lossy_or_nonfinite_encodings() { + assert_eq!( + rational_f64(&[Value::from("-3"), Value::from("2")]).unwrap(), + -1.5 + ); + for parts in [ + [Value::from("1e2"), Value::from(1)], + [Value::from(1.2), Value::from(1)], + [Value::from(1), Value::from(0)], + ] { + assert!(rational_f64(&parts).is_err()); + } + } + + #[test] + fn spin_validation_is_joint_finite_and_shape_checked() { + let runtime = CorrelatedRuntime { + catalogue_json: "[]".into(), + matrices: vec![], + spin_classes: BTreeSet::from([vec![3], vec![3, 4]]), + known_helicities: BTreeSet::new(), + groups: vec![], + vectors: BTreeMap::new(), + }; + let vector = [[1.0, 0.0]; 4]; + assert!( + runtime + .validate_vectors(&BTreeMap::from([(3, vec![vector])]), Some(2)) + .is_ok() + ); + assert!( + runtime + .validate_vectors(&BTreeMap::from([(4, vec![vector])]), Some(2)) + .is_err() + ); + assert!( + runtime + .validate_vectors(&BTreeMap::from([(3, vec![vector; 3])]), Some(2)) + .is_err() + ); + assert!( + runtime + .validate_vectors(&BTreeMap::from([(3, vec![[[f64::NAN, 0.0]; 4]])]), None) + .is_err() + ); + } + + #[test] + fn spin_replacements_select_one_placeholder_and_sum_spectators() { + let mut runtime = CorrelatedRuntime { + catalogue_json: "[]".into(), + matrices: vec![], + spin_classes: BTreeSet::from([vec![1]]), + known_helicities: BTreeSet::new(), + groups: vec![], + vectors: BTreeMap::new(), + }; + for h1 in [-1, 1] { + for h2 in [-1, 1] { + for sector in [0, 2] { + runtime.groups.push(Group { + helicity_id: format!("{h1},{h2}"), + helicities: vec![h1, h2], + sector, + roots: vec![], + }); + } + } + } + assert_eq!(runtime.selected_groups(None, &BTreeMap::new()).len(), 8); + let vectors = BTreeMap::from([(1, vec![[[0.0, 0.0]; 4]])]); + let selected = runtime.selected_groups(None, &vectors); + assert_eq!(selected.len(), 4); + assert!( + selected + .iter() + .all(|&index| runtime.groups[index].helicities[0] == -1) + ); + let ids = vec!["1,-1".to_owned()]; + let selected = runtime.selected_groups(Some(&ids), &vectors); + assert_eq!(selected.len(), 2); + assert!( + selected + .iter() + .all(|&index| runtime.groups[index].helicities == [1, -1]) + ); + } +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/engine/eager_backend.rs b/pyamplicol/rust/crates/rusticol-core/src/engine/eager_backend.rs new file mode 100644 index 0000000000..8992a841d4 --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/engine/eager_backend.rs @@ -0,0 +1,234 @@ +// SPDX-License-Identifier: 0BSD + +use super::*; +use crate::{EagerKernelBackend, EagerKernelCall}; + +pub(super) struct PreparedEvaluatorBackend { + kernels: BTreeMap<(u32, u32), PreparedEvaluatorKernel>, +} + +struct PreparedEvaluatorKernel { + evaluator: EvaluatorGroup, + #[cfg(feature = "symbolica-runtime")] + input_scratch: Vec>, + output_scratch: Vec>, +} + +impl PreparedEvaluatorBackend { + pub(super) fn load( + pack: &PreparedKernelPackManifest, + payload_root: &Path, + ) -> RusticolResult { + let payloads = EvaluatorPayloadStore::directory(payload_root); + Self::load_from_store(pack, &payloads) + } + + pub(super) fn load_from_store( + pack: &PreparedKernelPackManifest, + payloads: &EvaluatorPayloadStore, + ) -> RusticolResult { + let expected_capability = match pack.backend.as_str() { + "jit" => SYMJIT_APPLICATION_RUNTIME_CAPABILITY, + "asm" => SYMBOLICA_COMPILED_ASM_RUNTIME_CAPABILITY, + "cpp" => SYMBOLICA_COMPILED_CPP_RUNTIME_CAPABILITY, + other => { + return Err(RusticolError::artifact(format!( + "unsupported prepared backend {other:?}" + ))); + } + }; + let mut kernels = BTreeMap::new(); + for kernel in &pack.kernels { + if kernel.contract_kind == "model-parameter" { + continue; + } + let evaluator_manifest = kernel.runtime_evaluator_manifest()?; + let actual = evaluator_runtime_capabilities(&evaluator_manifest)?; + if actual != BTreeSet::from([expected_capability.to_string()]) { + return Err(RusticolError::integrity(format!( + "prepared kernel {} backend {:?} declares evaluator capabilities {actual:?}", + kernel.kernel_id, pack.backend + ))); + } + ensure_evaluator_capabilities_supported(&evaluator_manifest)?; + let (input_len, output_len) = evaluator_manifest.io_len()?; + if input_len != kernel.input_arity + || output_len != usize::try_from(kernel.output_arity).unwrap_or(usize::MAX) + { + return Err(RusticolError::integrity(format!( + "prepared kernel {} evaluator I/O ({input_len}, {output_len}) does not match ({}, {})", + kernel.kernel_id, kernel.input_arity, kernel.output_arity + ))); + } + let evaluator = EvaluatorGroup::load_from_store(&evaluator_manifest, payloads)?; + if kernels + .insert( + (kernel.kernel_id, 1), + PreparedEvaluatorKernel { + evaluator, + #[cfg(feature = "symbolica-runtime")] + input_scratch: Vec::new(), + output_scratch: Vec::new(), + }, + ) + .is_some() + { + return Err(RusticolError::integrity(format!( + "prepared evaluator backend repeats kernel {}", + kernel.kernel_id + ))); + } + } + for variant in &pack.kernel_variants { + let evaluator_manifest = variant.runtime_evaluator_manifest()?; + let actual = evaluator_runtime_capabilities(&evaluator_manifest)?; + if actual != BTreeSet::from([SYMJIT_APPLICATION_RUNTIME_CAPABILITY.to_string()]) { + return Err(RusticolError::integrity(format!( + "prepared kernel {} block variant declares evaluator capabilities {actual:?}", + variant.base_kernel_id + ))); + } + ensure_evaluator_capabilities_supported(&evaluator_manifest)?; + let (input_len, output_len) = evaluator_manifest.io_len()?; + if input_len != variant.input_arity || output_len != variant.output_arity { + return Err(RusticolError::integrity(format!( + "prepared kernel {} block evaluator I/O ({input_len}, {output_len}) does not match ({}, {})", + variant.base_kernel_id, variant.input_arity, variant.output_arity + ))); + } + let evaluator = EvaluatorGroup::load_from_store(&evaluator_manifest, payloads)?; + if kernels + .insert( + (variant.base_kernel_id, variant.block_size), + PreparedEvaluatorKernel { + evaluator, + #[cfg(feature = "symbolica-runtime")] + input_scratch: Vec::new(), + output_scratch: Vec::new(), + }, + ) + .is_some() + { + return Err(RusticolError::integrity(format!( + "prepared evaluator backend repeats kernel {} block size {}", + variant.base_kernel_id, variant.block_size + ))); + } + } + Ok(Self { kernels }) + } + + pub(super) fn preflight_all( + pack: &PreparedKernelPackManifest, + payload_root: &Path, + ) -> RusticolResult { + let backend = Self::load(pack, payload_root)?; + let mut loaded = backend.kernels.len(); + for kernel in &pack.kernels { + if kernel.contract_kind != "model-parameter" { + continue; + } + let evaluator_manifest = kernel.runtime_evaluator_manifest()?; + ensure_evaluator_capabilities_supported(&evaluator_manifest)?; + let (input_len, output_len) = evaluator_manifest.io_len()?; + if input_len != kernel.input_arity + || output_len != usize::try_from(kernel.output_arity).unwrap_or(usize::MAX) + { + return Err(RusticolError::integrity(format!( + "prepared model-parameter kernel {} evaluator I/O ({input_len}, {output_len}) does not match ({}, {})", + kernel.kernel_id, kernel.input_arity, kernel.output_arity + ))); + } + let _evaluator = EvaluatorGroup::load(&evaluator_manifest, payload_root)?; + loaded += 1; + } + Ok(loaded) + } +} + +impl EagerKernelBackend for PreparedEvaluatorBackend { + fn evaluate_batch(&mut self, call: EagerKernelCall<'_>) -> RusticolResult<()> { + let kernel = self + .kernels + .get_mut(&(call.kernel_id, call.independent_block_size)) + .ok_or_else(|| { + RusticolError::artifact(format!( + "eager plan references unloaded prepared kernel {} block size {}", + call.kernel_id, call.independent_block_size + )) + })?; + if call.input_component_count != kernel.evaluator.input_len + || call.output_component_count != kernel.evaluator.output_len + { + return Err(RusticolError::integrity(format!( + "eager call for kernel {} has I/O ({}, {}), expected ({}, {})", + call.kernel_id, + call.input_component_count, + call.output_component_count, + kernel.evaluator.input_len, + kernel.evaluator.output_len, + ))); + } + let input_len = call + .lane_count + .checked_mul(call.input_component_count) + .ok_or_else(|| RusticolError::invalid_argument("eager input packet overflows"))?; + let output_len = call + .lane_count + .checked_mul(call.output_component_count) + .ok_or_else(|| RusticolError::invalid_argument("eager output packet overflows"))?; + if call.inputs.len() != input_len || call.outputs.len() != output_len { + return Err(RusticolError::internal( + "eager scheduler passed an inconsistent kernel packet", + )); + } + + // Prepared eager kernels normally contain one evaluator with the exact + // kernel input order. Keep that hot path entirely borrowed: EagerComplex64 + // is the same Complex representation consumed by every f64 backend. + // Chunked/mapped manifests remain supported through EvaluatorGroup's + // reusable fallback scratch. + #[cfg(not(feature = "symbolica-runtime"))] + if kernel.evaluator.evaluators.len() == 1 + && kernel.evaluator.input_mappings[0].is_none() + && kernel.evaluator.evaluators[0].output_len == kernel.evaluator.output_len + { + return kernel.evaluator.evaluators[0].evaluate_f64_batch( + call.lane_count, + call.inputs, + call.outputs, + ); + } + + #[cfg(feature = "symbolica-runtime")] + let evaluator_inputs = { + kernel + .input_scratch + .resize(input_len, Complex::new(0.0, 0.0)); + for (target, source) in kernel.input_scratch.iter_mut().zip(call.inputs.iter()) { + *target = Complex::new(source.re, source.im); + } + kernel.input_scratch.as_slice() + }; + #[cfg(not(feature = "symbolica-runtime"))] + let evaluator_inputs = call.inputs; + + kernel.evaluator.evaluate_batch_into_padded_simd_tail( + call.lane_count, + evaluator_inputs, + &mut kernel.output_scratch, + )?; + if kernel.output_scratch.len() != output_len { + return Err(RusticolError::internal( + "prepared evaluator returned an inconsistent output packet", + )); + } + #[cfg(feature = "symbolica-runtime")] + for (target, source) in call.outputs.iter_mut().zip(kernel.output_scratch.iter()) { + *target = crate::EagerComplex64::new(source.re, source.im); + } + #[cfg(not(feature = "symbolica-runtime"))] + call.outputs.copy_from_slice(&kernel.output_scratch); + Ok(()) + } +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/engine/eager_integration_tests.rs b/pyamplicol/rust/crates/rusticol-core/src/engine/eager_integration_tests.rs new file mode 100644 index 0000000000..da10e058f4 --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/engine/eager_integration_tests.rs @@ -0,0 +1,1885 @@ +// SPDX-License-Identifier: 0BSD + +use super::*; +use crate::{ + EAGER_KERNEL_ABI, EagerClosureRow, EagerCouplingRow, EagerExecutionPlan, EagerExecutionRuntime, + EagerKernelInput, EagerKernelRole, EagerKernelSpec, EagerPlanDefinition, EagerPlanDimensions, + EagerPlanPayloads, EagerReductionEntry, EagerReductionGroup, EagerRuntimeOptions, MISSING_U32, +}; +use serde_json::json; + +const TEST_SYMJIT_APPLICATION_ABI: &str = "symjit-application-storage-v3"; +const TEST_SYMJIT_PLANE_APPLICATION_ABI: &str = "pyamplicol-symjit-plane-application-v2"; +const TEST_PREPARED_JIT_PORTABLE_OPTIMIZATION_LEVEL: u64 = 2; +const TEST_PREPARED_JIT_PORTABLE_TARGET: &str = "symjit-storage-v3-portable"; + +#[test] +fn eager_color_topology_replay_preserves_amplitude_major_batches() { + let materialized_groups = [ + RawSumGroup { + id: 10, + indices: vec![0, 1], + weight: 1.0, + all_sector_weight: 1.0, + sector_ids: vec![0], + }, + RawSumGroup { + id: 20, + indices: vec![2], + weight: 1.0, + all_sector_weight: 1.0, + sector_ids: vec![1], + }, + ]; + let replay = serde_json::from_value(json!({ + "contract_version": 1, + "physical_group_count": 2, + "physical_groups": [ + {"group_id": 0, "helicities": [1, -1], "color_sector_id": 0, + "color_word": [1, 2], "helicity_weight": 1.0}, + {"group_id": 1, "helicities": [1, -1], "color_sector_id": 1, + "color_word": [2, 1], "helicity_weight": 1.0}, + ], + "mappings": [{"label_permutation": [], "group_routes": [ + {"source_group_id": 10, "target_group_id": 0, "factor": [1.0, 0.0]}, + {"source_group_id": 20, "target_group_id": 1, "factor": [0.0, 1.0]}, + ]}], + })) + .expect("color topology replay fixture"); + let contraction = serde_json::from_value(json!({ + "supported": true, + "group_count": 2, + "includes_color_factor": true, + "entries": [ + {"left_group_id": 0, "right_group_id": 0, "weight": [2.0, 0.0]}, + {"left_group_id": 1, "right_group_id": 1, "weight": [3.0, 0.0]}, + {"left_group_id": 0, "right_group_id": 1, "weight": [1.0, 0.0], + "symmetry_factor": 2.0}, + ], + })) + .expect("color contraction fixture"); + let mut reducer = AmplitudeRuntime::color_topology_replay_reducer( + 3, + &materialized_groups, + &replay, + &contraction, + ) + .expect("color topology replay reducer"); + + // Include repeated and distinct points, and a batch crossing the normal + // eager point-tile boundary. Each amplitude occupies one point plane. + for point_count in [1, 2, 3, 65] { + let value = |amplitude: usize, point: usize| { + let point = (point / 2) as f64; + crate::EagerComplex64::new(1.0 + amplitude as f64 * 3.0 + point, point - 2.0) + }; + let amplitudes = (0..3) + .flat_map(|amplitude| (0..point_count).map(move |point| value(amplitude, point))) + .collect::>(); + reducer.begin_color_topology_replay(point_count).unwrap(); + reducer + .gather_color_topology_replay_eager_amplitudes(&litudes, point_count, 0) + .unwrap(); + let mut totals = vec![0.0; point_count]; + reducer + .reduce_color_topology_replay_f64_into(point_count, &mut totals) + .unwrap(); + for (point, total) in totals.iter().copied().enumerate() { + let left = value(0, point) + value(1, point); + let right = value(2, point) * crate::EagerComplex64::new(0.0, 1.0); + let expected = + (2.0 * left * left.conj() + 3.0 * right * right.conj() + 2.0 * left * right.conj()) + .re; + assert_eq!(total, expected, "batch {point_count}, point {point}"); + } + } +} + +fn symjit_manifest(application_path: &str, exact_state_path: &str, input_len: usize) -> Value { + let plane_application_path = application_path.strip_suffix(".symjit").map_or_else( + || format!("{application_path}.plane.symjit"), + |stem| format!("{stem}.plane.symjit"), + ); + json!({ + "kind": "symjit-application-evaluator", + "runtime_capability": SYMJIT_APPLICATION_RUNTIME_CAPABILITY, + "backend": "jit", + "label": "prepared_test_kernel", + "application_path": application_path, + "application_abi": TEST_SYMJIT_APPLICATION_ABI, + "plane_application": { + "application_path": plane_application_path, + "application_abi": TEST_SYMJIT_PLANE_APPLICATION_ABI, + "storage_abi": TEST_SYMJIT_APPLICATION_ABI, + "element_layout": "split-complex-plane-major", + "descriptor_order": "inputs-re-im-then-outputs-re-im", + "input_complex_count": input_len, + "output_complex_count": 1, + "input_plane_count": 2 * input_len, + "output_plane_count": 2, + "compiler_type": "native", + "translation_mode": "symbolica-structured-instructions", + "optimization_level": TEST_PREPARED_JIT_PORTABLE_OPTIMIZATION_LEVEL, + "simd": true, + "complex": true, + "fast_math": true, + "fast_complex": false, + "compression": false, + "threading": false, + "direct_arena": true, + "source_digest": "0".repeat(64), + "target": { + "word_bits": 64, + "endianness": "little", + }, + }, + "input_len": input_len, + "output_len": 1, + "element_layout": "complex-f64", + "batch_layout": "row-major", + "compiler_type": "native", + "translation_mode": "indirect", + "optimization_level": TEST_PREPARED_JIT_PORTABLE_OPTIMIZATION_LEVEL, + "word_bits": 64, + "endianness": "little", + "required_defuns": [], + "evaluator_state_path": exact_state_path, + "evaluator_state_runtime_capability": SYMBOLICA_LEGACY_JIT_RUNTIME_CAPABILITY, + "settings": { + "jit_optimization_level": TEST_PREPARED_JIT_PORTABLE_OPTIMIZATION_LEVEL, + }, + "build_timing": {"jit_materialize_s": 0.0}, + }) +} + +fn compiled_manifest(runtime_capability: &str, exact_state_path: &str, input_len: usize) -> Value { + json!({ + "kind": "compiled-complex-evaluator", + "runtime_capability": runtime_capability, + "backend": "compiled-complex", + "function_name": "prepared_test_kernel", + "library_path": "kernels/7/library-0", + "source_path": "kernels/7/source-0.cpp", + "input_len": input_len, + "output_len": 1, + "number_type": "complex-f64", + "evaluator_state_path": exact_state_path, + "settings": {"optimization_level": "o3"}, + "build_timing": {"compile_s": 0.0}, + "direct_table": { + "capability": crate::eager_layout::EAGER_DIRECT_ARENA_RUNTIME_CAPABILITY, + "source_application_abi": + crate::eager_layout::EAGER_NATIVE_DIRECT_TABLE_APPLICATION_ABI, + "descriptor_abi": crate::eager_layout::EAGER_DIRECT_TABLE_DESCRIPTOR_ABI, + "binding_abi": crate::eager_layout::EAGER_DIRECT_TABLE_BINDING_ABI, + "library_path": "kernels/7/library-0", + "function_name": "pyamplicol_eager_direct_table_k00000007_v1", + "evaluator_state_sha256": "b".repeat(64), + "input_complex_count": input_len, + "output_complex_count": 1, + "invocation_stride": (2 * input_len + 2) * 4, + "attachment_stride": 16, + "simd_lane_width": 2, + "instruction_count": 1, + "temporary_count": 0, + }, + }) +} + +fn input(role: &str, component: u32) -> PreparedKernelInputManifest { + PreparedKernelInputManifest { + role: role.to_string(), + component, + symbol: format!("pyamplicol::test::{role}::{component}"), + model_parameter_name: None, + model_parameter_index: None, + } +} + +fn model_parameter_input(name: &str, index: u32) -> PreparedKernelInputManifest { + PreparedKernelInputManifest { + role: "model-parameter".to_string(), + component: 0, + symbol: format!("pyamplicol::test::model-parameter::{name}"), + model_parameter_name: Some(name.to_string()), + model_parameter_index: Some(index), + } +} + +fn kernel( + kernel_id: u32, + contract_kind: &str, + inputs: Vec, + application_path: &str, +) -> PreparedKernelManifest { + let input_arity = inputs.len(); + let exact_evaluator_state_path = format!("kernels/{kernel_id}/exact.bin"); + PreparedKernelManifest { + kernel_id, + contract_kind: contract_kind.to_string(), + canonical_signature: format!("test-kernel-{kernel_id}"), + input_arity, + output_arity: 1, + input_layout: (0..input_arity) + .map(|index| format!("input-{index}")) + .collect(), + input_contracts: inputs, + output_layout: vec!["output-0".to_string()], + exact_expressions: vec!["test-expression".to_string()], + proof_classes: Vec::new(), + f64_evaluator_manifest: symjit_manifest( + application_path, + &exact_evaluator_state_path, + input_arity, + ), + exact_evaluator_state_path, + } +} + +fn add_direct_table_manifest(kernel: &mut PreparedKernelManifest) { + kernel + .f64_evaluator_manifest + .as_object_mut() + .expect("test evaluator object") + .insert( + "direct_table".to_string(), + json!({ + "capability": crate::eager_layout::EAGER_DIRECT_ARENA_RUNTIME_CAPABILITY, + "source_application_abi": + crate::eager_layout::EAGER_DIRECT_SOURCE_APPLICATION_ABI, + "descriptor_abi": crate::eager_layout::EAGER_DIRECT_TABLE_DESCRIPTOR_ABI, + "binding_abi": crate::eager_layout::EAGER_DIRECT_TABLE_BINDING_ABI, + "descriptor_path": format!( + "kernels/{}/eager-direct-table-descriptor-v1.bin", + kernel.kernel_id + ), + "descriptor_size_bytes": 128, + "descriptor_sha256": "a".repeat(64), + "input_complex_count": kernel.input_arity, + "output_complex_count": kernel.output_arity, + }), + ); +} + +#[test] +fn eager_direct_table_manifest_is_explicit_and_fail_closed() { + let mut prepared = kernel( + 50, + "closure", + vec![input("left-current", 0)], + "kernels/50/application.symjit", + ); + let missing = prepared + .eager_direct_table_manifest() + .expect_err("pre-arena kernel must fail"); + assert!(missing.to_string().contains("regenerate")); + + add_direct_table_manifest(&mut prepared); + let direct = prepared + .eager_direct_table_manifest() + .expect("valid direct table metadata"); + assert_eq!(direct.input_complex_count, 1); + assert_eq!(direct.output_complex_count, 1); + assert_eq!(direct.descriptor_size_bytes, Some(128)); + + let large_descriptor_size = (1_u64 << 30) + 1; + prepared.f64_evaluator_manifest["direct_table"]["descriptor_size_bytes"] = + json!(large_descriptor_size); + let direct = prepared + .eager_direct_table_manifest() + .expect("large writer-representable DirectTable descriptor metadata must remain valid"); + assert_eq!(direct.descriptor_size_bytes, Some(large_descriptor_size)); + + let mut predecessor = prepared.clone(); + predecessor.f64_evaluator_manifest["direct_table"]["descriptor_abi"] = + json!(["symjit-direct-table-", "descriptor-v1"].concat()); + predecessor.f64_evaluator_manifest["direct_table"]["binding_abi"] = + json!(["symjit-direct-table-", "binding-v1"].concat()); + let error = predecessor + .eager_direct_table_manifest() + .expect_err("predecessor DirectTable ABI must fail closed"); + assert!(error.to_string().contains("regenerate")); + + for (field, value) in [ + ("capability", json!("wrong-capability")), + ("descriptor_abi", json!("wrong-descriptor-abi")), + ("binding_abi", json!("wrong-binding-abi")), + ("descriptor_size_bytes", json!(0)), + ("descriptor_sha256", json!("A".repeat(64))), + ("input_complex_count", json!(2)), + ] { + let mut malformed = prepared.clone(); + malformed.f64_evaluator_manifest["direct_table"][field] = value; + assert!( + malformed.eager_direct_table_manifest().is_err(), + "malformed DirectTable field {field} must fail" + ); + } +} + +fn filtered_pack(kernels: Vec) -> PreparedKernelPackManifest { + PreparedKernelPackManifest { + eager_kernel_abi: EAGER_KERNEL_ABI.to_string(), + backend: "jit".to_string(), + optimization_settings: json!({ + "jit_optimization_level": TEST_PREPARED_JIT_PORTABLE_OPTIMIZATION_LEVEL, + }), + producer: json!({"distribution": "pyamplicol", "version": "test"}), + dependency_abis: json!({ + "symjit_application": TEST_SYMJIT_APPLICATION_ABI, + "symjit_plane_application": TEST_SYMJIT_PLANE_APPLICATION_ABI, + }), + provenance: json!({"compiled_model_digest": "test"}), + target: PreparedKernelTargetManifest { + portable: true, + word_bits: 64, + endianness: "little".to_string(), + target_triple: TEST_PREPARED_JIT_PORTABLE_TARGET.to_string(), + cpu_features: Vec::new(), + }, + resolver_manifest: json!({ + "abi": "pyamplicol-prepared-kernel-catalog-v1", + "model_name": "test-model", + "model_parameter_kernel_id": null, + "propagator_bindings": [ + {"key": {"particle_id": 22, "chirality": 0}, "applies_propagator": false, "kernel_id": null} + ], + "vertex_bindings": [], + "closure_bindings": [] + }), + kernels, + kernel_variants: Vec::new(), + recurrence_template: None, + recurrence_direct_template: None, + } +} + +#[test] +fn eager_parameter_projection_uses_active_and_derivation_dependencies_only() { + let pack = filtered_pack(vec![ + kernel( + 7, + "vertex", + vec![model_parameter_input("alpha", 3)], + "kernels/7/application.symjit", + ), + kernel( + 8, + "propagator", + vec![model_parameter_input("MW", 40)], + "kernels/8/application.symjit", + ), + kernel( + 9, + "model-parameter", + vec![model_parameter_input("Gf", 100)], + "kernels/9/application.symjit", + ), + ]); + let indices = super::eager_load::prepared_parameter_indices(&pack, &BTreeSet::from([7]), true) + .expect("project process-local prepared parameters"); + + assert_eq!( + indices, + BTreeMap::from([("Gf".to_string(), 100), ("alpha".to_string(), 3)]) + ); + assert!(!indices.contains_key("MW")); + + let indices_without_derived = + super::eager_load::prepared_parameter_indices(&pack, &BTreeSet::from([7]), false) + .expect("omit unused derivation inputs"); + assert_eq!( + indices_without_derived, + BTreeMap::from([("alpha".to_string(), 3)]) + ); + + let runtime_parameters = [GenericRuntimeModelParameterManifest { + name: "alpha".to_string(), + kind: "external_parameter".to_string(), + parameter_index: 0, + default: 1.0, + pdg: None, + runtime_name: None, + complex_component: None, + }]; + let payloads = EvaluatorPayloadStore::directory(Path::new(".")); + let (projection, _, evaluator) = super::eager_load::prepare_eager_parameter_state( + &pack, + &BTreeSet::from([7]), + &runtime_parameters, + &[], + &payloads, + ) + .expect("an unused derivation kernel must not load or impose its inputs"); + assert_eq!(projection.parameter_count, 4); + assert!(evaluator.is_none()); +} + +#[test] +fn eager_parameter_projection_preserves_full_pack_parameter_integrity() { + let pack = filtered_pack(vec![ + kernel( + 7, + "vertex", + vec![model_parameter_input("alpha", 3)], + "kernels/7/application.symjit", + ), + kernel( + 8, + "propagator", + vec![model_parameter_input("MW", 40)], + "kernels/8/application.symjit", + ), + kernel( + 9, + "closure", + vec![model_parameter_input("MW", 41)], + "kernels/9/application.symjit", + ), + ]); + + let error = super::eager_load::prepared_parameter_indices(&pack, &BTreeSet::from([7]), false) + .expect_err("inactive contracts must still share one authenticated namespace"); + assert!(error.to_string().contains("conflicting stable indices")); +} + +#[test] +fn eager_parameter_projection_ignores_inactive_missing_runtime_dependencies_but_not_active_ones() { + let pack = filtered_pack(vec![ + kernel( + 7, + "vertex", + vec![model_parameter_input("alpha", 3)], + "kernels/7/application.symjit", + ), + kernel( + 8, + "propagator", + vec![model_parameter_input("MW", 40)], + "kernels/8/application.symjit", + ), + ]); + let runtime_parameters = vec![GenericRuntimeModelParameterManifest { + name: "alpha".to_string(), + kind: "external_parameter".to_string(), + parameter_index: 0, + default: 1.0, + pdg: None, + runtime_name: None, + complex_component: None, + }]; + let payloads = EvaluatorPayloadStore::directory(Path::new(".")); + + let (projection, _, evaluator) = super::eager_load::prepare_eager_parameter_state( + &pack, + &BTreeSet::from([7]), + &runtime_parameters, + &[], + &payloads, + ) + .expect("inactive prepared dependencies do not belong to this process"); + assert_eq!(projection.parameter_count, 4); + assert_eq!(projection.entries.len(), 1); + assert_eq!(projection.entries[0].prepared_index, 3); + assert_eq!(projection.entries[0].runtime_real_index, 0); + assert!(evaluator.is_none()); + + let error = match super::eager_load::prepare_eager_parameter_state( + &pack, + &BTreeSet::from([8]), + &runtime_parameters, + &[], + &payloads, + ) { + Ok(_) => panic!("an active prepared dependency must exist in the runtime schema"), + Err(error) => error, + }; + assert!( + error + .to_string() + .contains("prepared parameter \"MW\" is absent") + ); +} + +fn portable_outer_target() -> crate::Target { + crate::Target { + triple: crate::artifact::PORTABLE_64LE_ARTIFACT_TARGET.to_string(), + cpu_features: Vec::new(), + } +} + +fn portable_jit_pack_for_outer_target_test() -> PreparedKernelPackManifest { + filtered_pack(vec![kernel( + 50, + "closure", + vec![input("left-current", 0)], + "kernels/50/application.symjit", + )]) +} + +fn assert_portable_outer_pack_boundary( + validate: impl Fn(&crate::Target, &PreparedKernelPackManifest) -> RusticolResult<()>, + context: &str, +) { + let target = portable_outer_target(); + let jit = portable_jit_pack_for_outer_target_test(); + jit.validate().expect("valid portable O2 JIT pack"); + validate(&target, &jit).unwrap_or_else(|error| { + panic!("portable O2 JIT pack must satisfy {context} outer target: {error}") + }); + + for (backend, capability) in [ + ("cpp", SYMBOLICA_COMPILED_CPP_RUNTIME_CAPABILITY), + ("asm", SYMBOLICA_COMPILED_ASM_RUNTIME_CAPABILITY), + ] { + let native = compiled_pack(backend, capability); + native + .validate() + .unwrap_or_else(|error| panic!("valid native {backend} pack: {error}")); + let error = validate(&target, &native) + .expect_err("portable outer artifact must reject a native prepared pack"); + assert!(error.to_string().contains("portable O2 SymJIT")); + assert!(error.to_string().contains(context)); + } +} + +#[test] +fn eager_portable_outer_target_requires_o2_jit_pack() { + assert_portable_outer_pack_boundary( + super::eager_v3_load::validate_eager_prepared_pack_outer_target, + "eager", + ); +} + +#[test] +fn recurrence_portable_outer_target_requires_o2_jit_pack() { + assert_portable_outer_pack_boundary( + super::recurrence_load::validate_recurrence_prepared_pack_outer_target, + "recurrence", + ); +} + +#[test] +fn prepared_jit_pack_rejects_nonportable_target_before_loading_payloads() { + let mut pack = filtered_pack(vec![kernel( + 50, + "closure", + vec![input("left-current", 0)], + "kernels/50/missing.symjit", + )]); + let other = if std::env::consts::ARCH == "aarch64" { + "x86_64" + } else { + "aarch64" + }; + pack.target.portable = false; + pack.target.target_triple = format!("symjit-storage-v3-{other}"); + + let error = pack + .validate() + .expect_err("nonportable prepared JIT pack must fail"); + assert!(error.to_string().contains("expected portable target")); +} + +#[test] +fn prepared_jit_pack_requires_plane_application_dependency_abi() { + let mut pack = filtered_pack(vec![kernel( + 50, + "closure", + vec![input("left-current", 0)], + "kernels/50/missing.symjit", + )]); + pack.dependency_abis + .as_object_mut() + .expect("test dependency ABI object") + .remove("symjit_plane_application"); + + let error = pack + .validate() + .expect_err("prepared JIT pack without plane application ABI must fail"); + assert!(error.to_string().contains("plane-application ABI")); +} + +#[test] +fn raw_prepared_jit_manifest_projects_plane_without_direct_table() { + let mut pack = filtered_pack(vec![kernel( + 50, + "closure", + vec![input("left-current", 0)], + "kernels/50/application.symjit", + )]); + pack.validate() + .expect("raw prepared JIT pack with a plane application"); + assert!( + pack.kernels[0] + .extra_evaluator_payload_paths() + .expect("raw JIT extra payload paths") + .is_empty(), + "a raw prepared JIT pack has no process-stage DirectTable payload" + ); + + let runtime = pack.kernels[0] + .runtime_evaluator_manifest() + .expect("project raw prepared evaluator"); + let EvaluatorManifest::SymjitApplication { + plane_application: Some(plane), + .. + } = runtime + else { + panic!("raw prepared evaluator must retain its plane application"); + }; + assert_eq!( + plane.application_path, + "kernels/50/application.plane.symjit" + ); + + add_direct_table_manifest(&mut pack.kernels[0]); + let runtime = pack.kernels[0] + .runtime_evaluator_manifest() + .expect("project process-stage prepared evaluator"); + assert!(matches!( + runtime, + EvaluatorManifest::SymjitApplication { + plane_application: Some(_), + .. + } + )); + assert_eq!( + pack.kernels[0] + .extra_evaluator_payload_paths() + .expect("process-stage extra payload paths"), + ["kernels/50/eager-direct-table-descriptor-v1.bin"] + ); +} + +#[test] +fn prepared_jit_plane_application_is_fail_closed() { + let pack = filtered_pack(vec![kernel( + 50, + "closure", + vec![input("left-current", 0)], + "kernels/50/application.symjit", + )]); + + let mut missing = pack.clone(); + missing.kernels[0] + .f64_evaluator_manifest + .as_object_mut() + .expect("test evaluator object") + .remove("plane_application"); + let error = missing + .validate() + .expect_err("missing plane application must fail"); + assert!(error.to_string().contains("regenerate")); + + for (field, value) in [ + ("application_abi", json!("wrong-plane-abi")), + ("input_plane_count", json!(3)), + ("target", Value::Null), + ] { + let mut malformed = pack.clone(); + malformed.kernels[0].f64_evaluator_manifest["plane_application"][field] = value; + let error = malformed + .validate() + .expect_err("malformed plane application must fail"); + assert!( + error.to_string().contains("regenerate"), + "malformed plane field {field} must include regeneration guidance: {error}" + ); + } +} + +fn compiled_pack(backend: &str, runtime_capability: &str) -> PreparedKernelPackManifest { + let exact_evaluator_state_path = "kernels/7/exact.bin".to_string(); + let kernel = PreparedKernelManifest { + kernel_id: 7, + contract_kind: "vertex".to_string(), + canonical_signature: format!("test-{backend}-kernel"), + input_arity: 1, + output_arity: 1, + input_layout: vec!["input-0".to_string()], + input_contracts: vec![input("left-current", 0)], + output_layout: vec!["output-0".to_string()], + exact_expressions: vec!["test-expression".to_string()], + proof_classes: Vec::new(), + f64_evaluator_manifest: compiled_manifest( + runtime_capability, + &exact_evaluator_state_path, + 1, + ), + exact_evaluator_state_path, + }; + let target = crate::runtime_target_info(); + PreparedKernelPackManifest { + eager_kernel_abi: EAGER_KERNEL_ABI.to_string(), + backend: backend.to_string(), + optimization_settings: json!({"optimization_level": "o3"}), + producer: json!({"distribution": "pyamplicol", "version": "test"}), + dependency_abis: json!({"compiled_complex": "test"}), + provenance: json!({"compiled_model_digest": "test"}), + target: PreparedKernelTargetManifest { + portable: false, + word_bits: 64, + endianness: "little".to_string(), + target_triple: target.triple, + cpu_features: target.cpu_features, + }, + resolver_manifest: json!({ + "abi": "pyamplicol-prepared-kernel-catalog-v1", + "model_name": "test-model", + "model_parameter_kernel_id": null, + "propagator_bindings": [], + "vertex_bindings": [], + "closure_bindings": [] + }), + kernels: vec![kernel], + kernel_variants: Vec::new(), + recurrence_template: None, + recurrence_direct_template: None, + } +} + +#[test] +fn runtime_options_are_positive_and_use_checked_platform_sizes() { + let options = EagerRuntimeOptionsManifest { + point_tile_size: 1024, + workspace_mib: 256, + } + .validate() + .expect("valid eager runtime options"); + assert_eq!(options.point_tile_size, 1024); + assert_eq!(options.workspace_bytes, 256 * 1024 * 1024); + + let above_old_reader_ceilings = EagerRuntimeOptionsManifest { + point_tile_size: 1_048_577, + workspace_mib: 4097, + } + .validate() + .expect("writer-valid eager runtime options above old reader ceilings"); + assert_eq!(above_old_reader_ceilings.point_tile_size, 1_048_577); + assert_eq!( + above_old_reader_ceilings.workspace_bytes, + 4097 * 1024 * 1024 + ); + + for invalid in [ + EagerRuntimeOptionsManifest { + point_tile_size: 0, + workspace_mib: 256, + }, + EagerRuntimeOptionsManifest { + point_tile_size: 1024, + workspace_mib: 0, + }, + ] { + assert!(invalid.validate().is_err()); + } +} + +#[test] +fn filtered_pack_accepts_only_referenced_kernel_ids_and_preserves_input_order() { + let pack = filtered_pack(vec![ + kernel( + 7, + "propagator", + vec![input("current", 0), input("momentum", 3)], + "kernels/7/application.symjit", + ), + kernel( + 78, + "vertex", + vec![ + input("right-current", 1), + input("left-current", 0), + input("right-momentum", 2), + input("coupling-imag", 0), + ], + "kernels/78/application.symjit", + ), + ]); + pack.validate().expect("filtered prepared pack"); + + let specs = pack.kernel_specs().expect("prepared kernel specs"); + assert_eq!( + specs.iter().map(|spec| spec.kernel_id).collect::>(), + [7, 78] + ); + assert_eq!(specs[0].role, EagerKernelRole::Finalization); + assert_eq!( + specs[1].inputs, + vec![ + EagerKernelInput::SecondCurrentComponent(1), + EagerKernelInput::FirstCurrentComponent(0), + EagerKernelInput::SecondMomentumComponent(2), + EagerKernelInput::CouplingImag, + ] + ); +} + +#[test] +fn prepared_pack_schema_requires_an_explicit_kernel_abi() { + let missing = json!({ + "backend": "jit", + "optimization_settings": {"jit_optimization_level": 3}, + "producer": {"distribution": "pyamplicol"}, + "dependency_abis": {"symjit_application": "test"}, + "provenance": {"compiled_model_digest": "test"}, + "target": { + "portable": true, + "word_bits": 64, + "endianness": "little", + "target_triple": "portable-symjit-mir", + "cpu_features": [] + }, + "resolver_manifest": {"abi": "pyamplicol-prepared-kernel-catalog-v1"}, + "kernels": [] + }); + assert!(serde_json::from_value::(missing.clone()).is_err()); + + let mut unknown = missing; + unknown["eager_kernel_abi"] = json!(EAGER_KERNEL_ABI); + unknown["unexpected"] = json!(true); + assert!(serde_json::from_value::(unknown).is_err()); +} + +#[test] +fn compiled_prepared_backends_validate_runtime_identity_tuples() { + compiled_pack("asm", SYMBOLICA_COMPILED_ASM_RUNTIME_CAPABILITY) + .validate() + .expect("ASM prepared evaluator tuple"); + compiled_pack("cpp", SYMBOLICA_COMPILED_CPP_RUNTIME_CAPABILITY) + .validate() + .expect("C++ prepared evaluator tuple"); + + let mut wrong_backend = compiled_pack("asm", SYMBOLICA_COMPILED_ASM_RUNTIME_CAPABILITY); + wrong_backend.kernels[0] + .f64_evaluator_manifest + .as_object_mut() + .expect("compiled evaluator object") + .insert("backend".to_string(), json!("asm")); + assert!(wrong_backend.validate().is_err()); + + let wrong_capability = compiled_pack("asm", SYMBOLICA_COMPILED_CPP_RUNTIME_CAPABILITY); + assert!(wrong_capability.validate().is_err()); +} + +#[cfg(feature = "f64-symjit")] +#[test] +fn prepared_symjit_backend_executes_a_filtered_eager_plan() { + use std::sync::atomic::{AtomicU64, Ordering}; + use symjit::{Compiler, CompilerType, Config, Storage}; + + static NEXT_DIRECTORY: AtomicU64 = AtomicU64::new(0); + let root = std::env::temp_dir().join(format!( + "rusticol-eager-backend-{}-{}", + std::process::id(), + NEXT_DIRECTORY.fetch_add(1, Ordering::Relaxed) + )); + fs::create_dir_all(root.join("kernels/50")).expect("create prepared-kernel test root"); + + let mut config = Config::new(CompilerType::Native, 0).expect("native SymJIT config"); + config.set_complex(true); + config.set_symbolica(true); + config.set_opt_level(TEST_PREPARED_JIT_PORTABLE_OPTIMIZATION_LEVEL as u8); + config.set_simd(true); + let mut compiler = Compiler::with_config(config); + let instructions = r#"[[{"Add":[{"Out":0},[{"Param":0},{"Param":1}],0]}],1,[]]"#; + let application = compiler + .translate(instructions.to_string(), 2) + .expect("translate prepared test application"); + let mut application_bytes = Vec::new(); + application + .save(&mut application_bytes) + .expect("serialize prepared test application"); + fs::write( + root.join("kernels/50/application.symjit"), + application_bytes, + ) + .expect("write prepared test application"); + let plane_application_bytes = + crate::engine::compile_symbolica_program_to_plane_application_bytes( + instructions, + 2, + 1, + TEST_PREPARED_JIT_PORTABLE_OPTIMIZATION_LEVEL as u8, + false, + ) + .expect("compile prepared test plane application"); + fs::write( + root.join("kernels/50/application.plane.symjit"), + plane_application_bytes, + ) + .expect("write prepared test plane application"); + + let pack = filtered_pack(vec![kernel( + 50, + "closure", + vec![input("left-current", 0), input("right-current", 0)], + "kernels/50/application.symjit", + )]); + pack.validate().expect("valid filtered test pack"); + assert_eq!( + PreparedEvaluatorBackend::preflight_all(&pack, &root) + .expect("preflight raw prepared JIT pack without a DirectTable"), + 1 + ); + let mut backend = PreparedEvaluatorBackend::load(&pack, &root) + .expect("load prepared SymJIT evaluator backend"); + let definition = EagerPlanDefinition { + dimensions: EagerPlanDimensions { + value_slot_component_counts: vec![1, 1], + momentum_slot_component_counts: Vec::new(), + current_component_counts: Vec::new(), + parameter_count: 0, + amplitude_count: 1, + }, + kernels: vec![EagerKernelSpec { + kernel_id: 50, + role: EagerKernelRole::Closure, + inputs: vec![ + EagerKernelInput::FirstCurrentComponent(0), + EagerKernelInput::SecondCurrentComponent(0), + ], + output_component_count: 1, + homogeneous_linear_first_current: false, + independent_block_size: 1, + }], + direct_closures: Vec::new(), + reduction_groups: vec![EagerReductionGroup { + coherent_group_id: 0, + amplitude_indices: vec![0], + }], + reduction_entries: vec![EagerReductionEntry { + left_group_index: 0, + right_group_index: 0, + coefficient: crate::EagerComplex64::new(1.0, 0.0), + }], + }; + let coupling_bytes = EagerCouplingRow::encode_table(&[EagerCouplingRow { + real_parameter_id: MISSING_U32, + imag_parameter_id: MISSING_U32, + constant_real: 1.0, + constant_imag: 0.0, + }]) + .expect("encode eager coupling table"); + let closure_bytes = EagerClosureRow::encode_table(&[EagerClosureRow { + kernel_id: 50, + left_value_slot_id: 0, + right_value_slot_id: 1, + amplitude_index: 0, + coupling_slot_id: 0, + output_factor_source: crate::EAGER_OUTPUT_FACTOR_NONE, + factor_real: 1.0, + factor_imag: 0.0, + }]) + .expect("encode eager closure table"); + let plan = EagerExecutionPlan::from_payloads( + definition, + EagerPlanPayloads { + couplings: &coupling_bytes, + stages: &[], + closures: &closure_bytes, + selector_domains: None, + }, + ) + .expect("build eager test plan"); + let mut runtime = EagerExecutionRuntime::new( + plan, + EagerRuntimeOptions { + point_tile_size: 2, + workspace_bytes: 4096, + }, + ) + .expect("build eager test runtime"); + let values = [ + crate::EagerComplex64::new(1.0, 0.0), + crate::EagerComplex64::new(2.0, 0.0), + crate::EagerComplex64::new(3.0, 0.0), + crate::EagerComplex64::new(10.0, 0.0), + crate::EagerComplex64::new(20.0, 0.0), + crate::EagerComplex64::new(30.0, 0.0), + ]; + let mut amplitudes = [crate::EagerComplex64::new(0.0, 0.0); 3]; + let mut reduced = [0.0; 3]; + runtime + .evaluate_into( + &mut backend, + 3, + &values, + &[], + &[], + &mut amplitudes, + &mut reduced, + ) + .expect("evaluate prepared eager plan"); + assert_eq!( + amplitudes, + [ + crate::EagerComplex64::new(11.0, 0.0), + crate::EagerComplex64::new(22.0, 0.0), + crate::EagerComplex64::new(33.0, 0.0), + ] + ); + assert_eq!(reduced, [121.0, 484.0, 1089.0]); + + amplitudes.fill(crate::EagerComplex64::new(0.0, 0.0)); + reduced.fill(0.0); + let profile = runtime + .evaluate_profile_into( + &mut backend, + 3, + &values, + &[], + &[], + &mut amplitudes, + &mut reduced, + ) + .expect("profile prepared eager plan"); + assert_eq!( + amplitudes, + [ + crate::EagerComplex64::new(11.0, 0.0), + crate::EagerComplex64::new(22.0, 0.0), + crate::EagerComplex64::new(33.0, 0.0), + ] + ); + assert_eq!(reduced, [121.0, 484.0, 1089.0]); + assert!(!profile.total.is_zero()); + assert!(!profile.initialize.is_zero()); + assert!(!profile.kernel_call.is_zero()); + assert!(!profile.closure.is_zero()); + assert!(!profile.reduction.is_zero()); + assert!(!profile.copy_out.is_zero()); + assert!(profile.accounted() <= profile.total); + + let _ = fs::remove_dir_all(root); +} + +#[cfg(all(feature = "f64-symjit", target_arch = "aarch64"))] +#[test] +#[ignore = "retained real-artifact Direct-Arena stage oracle and interleaved timing evidence"] +fn retained_ddbar_z3g_multistage_invocations_match_packet_execution() { + use crate::eager_runtime::{ + EagerDirectPreparedKernel, EagerPlanV3Sections, run_retained_multistage_oracle, + select_retained_multistage_candidate, + }; + const ARTIFACT_ENV: &str = "PYAMPLICOL_EAGER_DIRECT_REAL_ARTIFACT"; + const PROCESS_ID: &str = "d_dbar_to_z_g_g_g"; + const POINTS: u32 = 129; + const SAMPLES: usize = 7; + const REPETITIONS: usize = 100; + + let root = std::env::var_os(ARTIFACT_ENV) + .map(PathBuf::from) + .expect("set PYAMPLICOL_EAGER_DIRECT_REAL_ARTIFACT to the retained eager artifact"); + let native = NativeRuntime::load(&root, Some(PROCESS_ID), None) + .expect("load retained eager artifact through the production runtime"); + let artifact = VerifiedArtifact::open(&root).expect("verify retained eager artifact"); + let selection = artifact + .select_process(Some(PROCESS_ID)) + .expect("select retained eager process"); + let (loaded, evaluator_root) = + load_verified_evaluator(&artifact, &selection).expect("load eager execution manifest"); + let LoadedExecutionManifest::EagerV3(manifest) = loaded else { + panic!("retained artifact is not eager plan-v3"); + }; + let pack = super::eager_v3_load::load_eager_v3_prepared_pack(&artifact, &manifest) + .expect("load retained prepared kernel pack"); + let container = super::eager_v3_load::open_verified_eager_v3_runtime_container( + &artifact, + &evaluator_root, + &manifest, + ) + .expect("open retained eager runtime container"); + let decoded = + super::eager_v3_decode::decode_eager_v3_runtime(&container, &manifest, &pack.manifest) + .expect("decode retained eager plan-v3"); + let payloads = artifact + .evaluator_payload_store(&pack.payload_root) + .expect("open retained evaluator payload store"); + let (projection, couplings, _model_parameter_evaluator) = + super::eager_v3_load::prepare_plan_v3_parameter_state( + &pack.manifest, + &decoded, + &native.runtime.model_parameters, + &payloads, + ) + .expect("prepare retained eager parameter projection"); + let prepared_parameter_count = + u32::try_from(projection.parameter_count).expect("prepared parameter count fits u32"); + let sections = EagerPlanV3Sections { + kernels: &decoded.kernel_specs, + prepared_parameter_count, + currents: &decoded.currents, + values: &decoded.values, + momenta: &decoded.momenta, + parameters: &decoded.parameters, + stages: &decoded.stages, + couplings: &couplings, + invocations: &decoded.invocations, + attachments: &decoded.attachments, + finalizations: &decoded.finalizations, + closures: &decoded.closures, + direct_coefficients: &decoded.direct_coefficients, + selector_domains: &decoded.selector_domains, + selector_memberships: &decoded.selector_memberships, + reduction_groups: &decoded.reduction_groups, + reduction_entries: &decoded.reduction_entries, + exact_factors: &decoded.exact_factors, + color_contraction_entry_start: decoded.color_contraction_entry_start, + color_contraction_entry_count: decoded.color_contraction_entry_count, + }; + let candidate = + select_retained_multistage_candidate(sections).expect("select genuine retained stage run"); + let kernel = decoded + .kernel_specs + .iter() + .find(|kernel| kernel.kernel_id == candidate.kernel_id) + .expect("candidate kernel spec"); + let kernel_manifest = pack + .manifest + .kernels + .iter() + .find(|kernel| kernel.kernel_id == candidate.kernel_id) + .expect("candidate prepared kernel manifest"); + let plane_application = kernel_manifest + .f64_evaluator_manifest + .get("plane_application") + .and_then(Value::as_object) + .expect("candidate SymJIT plane application metadata"); + let application_path = plane_application + .get("application_path") + .and_then(Value::as_str) + .expect("candidate SymJIT plane application path"); + let application_source = payloads + .source(application_path) + .expect("resolve candidate SymJIT application"); + let source_bytes = application_source + .read() + .expect("read candidate SymJIT application") + .into_owned(); + let descriptor = symjit_eager_direct::eager_direct_descriptor_for_source_application_bytes( + &source_bytes, + u32::try_from(kernel.inputs.len()).expect("candidate input width fits u32"), + kernel.output_component_count, + Path::new(&application_source.display_name()), + ) + .expect("encode retained eager table descriptor"); + let prepared = EagerDirectPreparedKernel { + kernel_id: candidate.kernel_id, + role: kernel.role, + inputs: &kernel.inputs, + output_component_count: kernel.output_component_count, + application: crate::eager_runtime::EagerDirectPreparedApplication::Symjit { + source_application: &source_bytes, + descriptor: &descriptor, + expected_optimization_level: 2, + expected_compression: false, + }, + display_path: PathBuf::from(application_source.display_name()), + }; + + let mut scalar_pack = pack.manifest.clone(); + scalar_pack + .kernels + .retain(|entry| entry.kernel_id == candidate.kernel_id); + scalar_pack.kernel_variants.clear(); + let mut backend = PreparedEvaluatorBackend::load_from_store(&scalar_pack, &payloads) + .expect("load retained scalar packet oracle"); + let mut model_parameters = + vec![crate::EagerComplex64::new(0.0, 0.0); projection.parameter_count]; + for entry in &projection.entries { + let real = native.runtime.model_parameter_values_f64[entry.runtime_real_index]; + let imaginary = entry + .runtime_imaginary_index + .map(|index| native.runtime.model_parameter_values_f64[index]) + .unwrap_or(0.0); + model_parameters[entry.prepared_index] = crate::EagerComplex64::new(real, imaginary); + } + + let evidence = run_retained_multistage_oracle( + sections, + &[prepared], + candidate, + &mut backend, + &model_parameters, + POINTS, + SAMPLES, + REPETITIONS, + ) + .expect("retained Direct-Arena stage must match actual packet execution"); + for variant in [evidence.full, evidence.selected] { + variant + .direct_call_traffic + .validate_direct() + .expect("direct invocation slice has no forbidden materialization traffic"); + assert!(variant.comparison_count > 0); + } + eprintln!( + "retained eager Direct-Arena stage oracle: artifact={} process={} \ + stage_position={} stage_index={} kernel_id={} selector_group={} points={} \ + full_rows={}/{} destinations={} comparisons={} bitwise={}/{} \ + full_max_abs={:.6e} full_max_rel={:.6e} \ + full_direct_ns={:.3} full_direct_mad_ns={:.3} \ + full_packet_ns={:.3} full_packet_mad_ns={:.3} full_ratio={:.6} \ + full_init_write_bytes={} full_factor_fill_bytes={} \ + full_packet_input_bytes/call={} full_packet_output_bytes/call={} \ + full_packet_scatter_bytes/call={} \ + selected_rows={}/{} comparisons={} bitwise={}/{} \ + selected_max_abs={:.6e} selected_max_rel={:.6e} \ + selected_direct_ns={:.3} selected_direct_mad_ns={:.3} \ + selected_packet_ns={:.3} selected_packet_mad_ns={:.3} selected_ratio={:.6} \ + selected_init_write_bytes={} selected_factor_fill_bytes={} \ + selected_packet_input_bytes/call={} selected_packet_output_bytes/call={} \ + selected_packet_scatter_bytes/call={} \ + direct_call_traffic_full={:?} direct_call_traffic_selected={:?}", + root.display(), + PROCESS_ID, + evidence.candidate.stage_position, + evidence.candidate.stage_index, + evidence.candidate.kernel_id, + evidence.candidate.selector_group_id, + POINTS, + evidence.full.invocation_count, + evidence.full.attachment_count, + evidence.candidate.distinct_destination_count, + evidence.full.comparison_count, + evidence.full.bitwise_comparison_count, + evidence.full.comparison_count, + evidence.full.maximum_absolute_error, + evidence.full.maximum_relative_error, + evidence.full.direct_median_ns, + evidence.full.direct_mad_ns, + evidence.full.packet_median_ns, + evidence.full.packet_mad_ns, + evidence.full.direct_over_packet, + evidence.full.arena_initialization_write_bytes, + evidence.full.factor_fill_write_bytes, + evidence.full.packet_input_materialization_bytes_per_call, + evidence.full.packet_output_materialization_bytes_per_call, + evidence.full.packet_scatter_bytes_per_call, + evidence.selected.invocation_count, + evidence.selected.attachment_count, + evidence.selected.comparison_count, + evidence.selected.bitwise_comparison_count, + evidence.selected.comparison_count, + evidence.selected.maximum_absolute_error, + evidence.selected.maximum_relative_error, + evidence.selected.direct_median_ns, + evidence.selected.direct_mad_ns, + evidence.selected.packet_median_ns, + evidence.selected.packet_mad_ns, + evidence.selected.direct_over_packet, + evidence.selected.arena_initialization_write_bytes, + evidence.selected.factor_fill_write_bytes, + evidence + .selected + .packet_input_materialization_bytes_per_call, + evidence + .selected + .packet_output_materialization_bytes_per_call, + evidence.selected.packet_scatter_bytes_per_call, + evidence.full.direct_call_traffic, + evidence.selected.direct_call_traffic, + ); +} + +#[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] +#[test] +fn generated_eager_artifact_loads_when_fixture_is_supplied() { + let Some(root) = std::env::var_os("RUSTICOL_EAGER_ARTIFACT") else { + return; + }; + let mut runtime = NativeRuntime::load(PathBuf::from(root), None, None) + .expect("load generated eager artifact through NativeRuntime"); + assert_eq!(runtime.metadata().execution_mode, "eager"); + assert!(matches!( + runtime.metadata().prepared_backend.as_deref(), + Some("jit" | "asm" | "cpp") + )); + let validation_path = runtime + .root() + .join("processes") + .join(&runtime.metadata().representative_process_key) + .join("validation-momenta.json"); + let validation: Value = + serde_json::from_slice(&fs::read(&validation_path).expect("read eager validation momenta")) + .expect("parse eager validation momenta"); + let momenta = validation["points"][0] + .as_array() + .expect("one eager validation point") + .iter() + .flat_map(|leg| { + leg["momentum"] + .as_array() + .expect("four momentum components") + .iter() + .map(|value| { + value + .as_str() + .expect("decimal momentum string") + .parse::() + .expect("f64 validation momentum") + }) + }) + .collect::>(); + let values = runtime + .evaluate_f64(&momenta, 1) + .expect("evaluate generated eager artifact"); + assert_eq!(values.len(), 1); + assert!(values[0].is_finite()); + + let resolved = runtime + .evaluate_resolved_f64(&momenta, 1, None, None) + .expect("resolve generated eager artifact"); + let resolved_total = resolved.totals()[0]; + assert_close_f64(resolved_total, values[0], "eager resolved sum"); + + let selected_helicity = runtime + .helicities() + .expect("eager helicity metadata") + .into_iter() + .find(|helicity| helicity.computed) + .expect("one computed helicity") + .id; + let selected_color = runtime + .color_ids() + .expect("eager color metadata") + .into_iter() + .next() + .expect("one color component"); + let selected_colors = + (runtime.metadata().color_accuracy == "lc").then(|| std::slice::from_ref(&selected_color)); + let selected = runtime + .evaluate_resolved_f64( + &momenta, + 1, + Some(std::slice::from_ref(&selected_helicity)), + selected_colors, + ) + .expect("select eager resolved component"); + assert_eq!(selected.shape(), (1, 1, 1)); + assert!(selected.values[0].is_finite()); + + let selector_point_count = 4; + let selector_momenta = momenta.repeat(selector_point_count); + let all_components = runtime + .evaluate_resolved_f64(&selector_momenta, selector_point_count, None, None) + .expect("resolve eager selector fixture"); + let helicity_count = all_components.helicity_ids.len(); + let color_count = all_components.color_ids.len(); + assert!(helicity_count >= 2, "selector fixture needs two helicities"); + assert!( + color_count >= 1, + "selector fixture needs one color component" + ); + let helicity_by_point = [0_u32, 1, 0, 1]; + let color_by_point = [0_u32; 4]; + let color_by_point = + (runtime.metadata().color_accuracy == "lc").then_some(color_by_point.as_slice()); + let selected_by_point = runtime + .evaluate_f64_with_selectors( + &selector_momenta, + selector_point_count, + None, + None, + Some(&helicity_by_point), + color_by_point, + ) + .expect("evaluate eager per-point selectors"); + for point_index in 0..selector_point_count { + let expected_index = + (point_index * helicity_count + helicity_by_point[point_index] as usize) * color_count; + assert_close_f64( + selected_by_point[point_index], + all_components.values[expected_index], + "eager per-point selector", + ); + } + + { + for tail in [1_usize, 7, 63, 64, 65, 127, 128, 129, 1023, 1024, 1025] { + let tail_momenta = momenta.repeat(tail); + let tail_values = runtime + .evaluate_f64(&tail_momenta, tail) + .expect("evaluate required eager Direct-Arena tail"); + for value in tail_values { + assert_close_f64(value, values[0], "eager Direct-Arena repeated-point tail"); + } + } + + for tail in [129_usize, 1025] { + let tail_momenta = momenta.repeat(tail); + let helicity_by_point = (0..tail) + .map(|point| (point % helicity_count.min(2)) as u32) + .collect::>(); + let color_by_point = + (runtime.metadata().color_accuracy == "lc").then(|| vec![0_u32; tail]); + let selected_tail = runtime + .evaluate_f64_with_selectors( + &tail_momenta, + tail, + None, + None, + Some(&helicity_by_point), + color_by_point.as_deref(), + ) + .expect("evaluate required eager Direct-Arena selector tail"); + for (point, value) in selected_tail.into_iter().enumerate() { + let expected_index = helicity_by_point[point] as usize * color_count; + let expected = if color_by_point.is_some() { + resolved.values[expected_index] + } else { + resolved.values[expected_index..expected_index + color_count] + .iter() + .sum() + }; + assert_close_f64(value, expected, "eager Direct-Arena selector tail"); + } + } + } + + if let Some(compiled_root) = std::env::var_os("RUSTICOL_COMPILED_ARTIFACT") { + let mut compiled = NativeRuntime::load(PathBuf::from(compiled_root), None, None) + .expect("load matching compiled artifact"); + let compiled_values = compiled + .evaluate_f64(&momenta, 1) + .expect("evaluate matching compiled artifact"); + assert_close_f64(values[0], compiled_values[0], "eager/compiled total"); + let compiled_resolved = compiled + .evaluate_resolved_f64(&momenta, 1, None, None) + .expect("resolve matching compiled artifact"); + assert_eq!(resolved.helicity_ids, compiled_resolved.helicity_ids); + assert_eq!(resolved.color_ids, compiled_resolved.color_ids); + assert_eq!(resolved.values.len(), compiled_resolved.values.len()); + for (eager, compiled) in resolved.values.iter().zip(&compiled_resolved.values) { + assert_close_f64(*eager, *compiled, "eager/compiled resolved component"); + } + } + + let parameters = runtime.model_parameters().expect("eager model parameters"); + let candidates = if let Some(parameter) = parameters + .iter() + .find(|parameter| parameter.name == "aEWM1") + { + vec![( + BTreeMap::from([(parameter.name.clone(), (parameter.default * 1.05, 0.0))]), + BTreeMap::from([( + parameter.name.clone(), + (parameter.default, parameter.default_imaginary), + )]), + )] + } else { + let mut groups = BTreeMap::>::new(); + for parameter in parameters + .iter() + .filter(|parameter| parameter.mutable && parameter.name.starts_with("coupling.")) + { + let prefix = parameter + .name + .split_once(".component_") + .map(|(prefix, _)| prefix.to_string()) + .expect("coupling component suffix"); + groups.entry(prefix).or_default().push(( + parameter.name.clone(), + parameter.default, + parameter.default_imaginary, + )); + } + groups + .into_values() + .map(|selected| { + let changed = selected + .iter() + .map(|(name, _, _)| (name.clone(), (0.0, 0.0))) + .collect(); + let restored = selected + .iter() + .map(|(name, real, imaginary)| (name.clone(), (*real, *imaginary))) + .collect(); + (changed, restored) + }) + .collect::>() + }; + assert!(!candidates.is_empty(), "one mutable eager parameter group"); + let mut observed_parameter_effect = false; + for (changed, restored) in candidates { + runtime + .set_model_parameters(&changed) + .expect("update eager model parameters atomically"); + let changed_value = runtime + .evaluate_f64(&momenta, 1) + .expect("evaluate updated eager parameters")[0]; + runtime + .set_model_parameters(&restored) + .expect("restore eager model parameters atomically"); + let restored_value = runtime + .evaluate_f64(&momenta, 1) + .expect("evaluate restored eager parameters")[0]; + assert_eq!(restored_value.to_bits(), values[0].to_bits()); + if changed_value.to_bits() != values[0].to_bits() { + observed_parameter_effect = true; + break; + } + } + assert!( + observed_parameter_effect, + "one eager parameter affects output" + ); + + if parameters.iter().any(|parameter| parameter.name == "aEWM1") + && parameters.iter().any(|parameter| parameter.name == "MZ") + { + let before_failed_derivation = runtime + .exact_runtime_state_json() + .expect("eager state before failed derivation"); + assert!(runtime.set_model_parameter("MZ", 0.0, 0.0).is_err()); + assert_eq!( + runtime + .exact_runtime_state_json() + .expect("eager state after failed derivation"), + before_failed_derivation + ); + } + + if let Some(parameter) = parameters.iter().find(|parameter| !parameter.mutable) { + let before_derived_update = runtime + .exact_runtime_state_json() + .expect("eager parameter state before derived update"); + assert!( + runtime + .set_model_parameter( + ¶meter.name, + parameter.default, + parameter.default_imaginary, + ) + .is_err() + ); + assert_eq!( + runtime + .exact_runtime_state_json() + .expect("eager parameter state after derived update"), + before_derived_update + ); + } + + let before_failed_update = runtime + .exact_runtime_state_json() + .expect("eager parameter state before failed update"); + assert!( + runtime + .set_model_parameter("not-a-model-parameter", 1.0, 0.0) + .is_err() + ); + assert_eq!( + runtime + .exact_runtime_state_json() + .expect("eager parameter state after failed update"), + before_failed_update + ); +} + +#[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] +#[test] +fn retained_sparse_group_eager_artifact_decodes_when_fixture_is_supplied() { + let Some(root) = std::env::var_os("RUSTICOL_EAGER_SPARSE_GROUP_ARTIFACT") else { + return; + }; + let root = PathBuf::from(root); + let _native = NativeRuntime::load(&root, None, None) + .expect("load retained zero-first-group eager artifact through NativeRuntime"); + let artifact = VerifiedArtifact::open(&root).expect("verify retained eager artifact"); + let selection = artifact + .select_process(None) + .expect("select retained eager process"); + let (loaded, evaluator_root) = + load_verified_evaluator(&artifact, &selection).expect("load eager execution manifest"); + let LoadedExecutionManifest::EagerV3(manifest) = loaded else { + panic!("retained artifact is not eager plan-v3"); + }; + let pack = super::eager_v3_load::load_eager_v3_prepared_pack(&artifact, &manifest) + .expect("load retained prepared kernel pack"); + let container = super::eager_v3_load::open_verified_eager_v3_runtime_container( + &artifact, + &evaluator_root, + &manifest, + ) + .expect("open retained eager runtime container"); + let decoded = + super::eager_v3_decode::decode_eager_v3_runtime(&container, &manifest, &pack.manifest) + .expect("decode retained zero-first-group eager plan-v3"); + let group_ids = decoded + .invocations + .iter() + .map(|row| row.evaluation_group_id) + .collect::>(); + assert!(group_ids.windows(2).all(|pair| pair[0] < pair[1])); + assert!( + group_ids + .windows(2) + .any(|pair| pair[1] > pair[0].saturating_add(1)), + "retained eager failure fixture must contain a pruned group gap: {group_ids:?}" + ); +} + +#[cfg(all( + any(feature = "f64-compiled", feature = "f64-symjit"), + any(target_os = "linux", target_os = "macos") +))] +#[test] +fn generated_eager_native_into_is_warmed_allocation_free_when_fixture_is_supplied() { + let Some(root) = std::env::var_os("RUSTICOL_EAGER_ARTIFACT") else { + return; + }; + let mut runtime = NativeRuntime::load(PathBuf::from(root), None, None) + .expect("load generated eager allocation fixture"); + let validation_path = runtime + .root() + .join("processes") + .join(&runtime.metadata().representative_process_key) + .join("validation-momenta.json"); + let validation: Value = + serde_json::from_slice(&fs::read(&validation_path).expect("read eager validation momenta")) + .expect("parse eager validation momenta"); + let point = validation["points"][0] + .as_array() + .expect("one eager validation point") + .iter() + .flat_map(|leg| { + leg["momentum"] + .as_array() + .expect("four momentum components") + .iter() + .map(|value| { + value + .as_str() + .expect("decimal momentum string") + .parse::() + .expect("f64 validation momentum") + }) + }) + .collect::>(); + let point_count = 129usize; + let momenta = point.repeat(point_count); + let mut output = vec![-1.0; point_count]; + + runtime + .evaluate_f64_into(&momenta, point_count, &mut output) + .expect("warm eager borrowed-input totals"); + let (result, allocations, bytes) = + count_allocations(|| runtime.evaluate_f64_into(&momenta, point_count, &mut output)); + result.expect("repeat eager borrowed-input totals"); + assert_eq!( + (allocations, bytes), + (0, 0), + "warmed eager borrowed-input totals allocated" + ); + let arena_profile = runtime + .evaluate_f64_arena_profile_repeated(&momenta, point_count, 3, None, None) + .expect("profile warmed eager borrowed-input totals"); + assert_eq!(arena_profile.values, output); + assert_eq!( + arena_profile + .profile + .native_input_container_allocation_count, + 0 + ); + assert_eq!(arena_profile.profile.native_output_allocation_count, 0); + assert_eq!(arena_profile.profile.observed_scratch_reallocation_count, 0); + assert_eq!( + arena_profile.profile.native_input_component_count, + u64::try_from(momenta.len() * 3).expect("profile input count"), + ); + + let selected_helicity = runtime + .helicities() + .expect("eager helicity metadata") + .into_iter() + .find(|helicity| helicity.computed) + .expect("one computed eager helicity") + .id; + let selected_helicities = [selected_helicity]; + let selected_colors = (runtime.metadata().color_accuracy == "lc").then(|| { + [runtime + .color_ids() + .expect("eager color metadata") + .into_iter() + .next() + .expect("one eager color")] + }); + runtime + .evaluate_f64_into_with_selectors( + &momenta, + point_count, + Some(&selected_helicities), + selected_colors.as_ref().map(|ids| ids.as_slice()), + None, + None, + &mut output, + ) + .expect("warm eager global selectors"); + let (result, allocations, bytes) = count_allocations(|| { + runtime.evaluate_f64_into_with_selectors( + &momenta, + point_count, + Some(&selected_helicities), + selected_colors.as_ref().map(|ids| ids.as_slice()), + None, + None, + &mut output, + ) + }); + result.expect("repeat eager global selectors"); + assert_eq!( + (allocations, bytes), + (0, 0), + "warmed eager global selectors allocated" + ); + let mut profile_cases = vec![( + "selected-helicity", + Some(selected_helicities.as_slice()), + None, + )]; + if let Some(ids) = selected_colors.as_ref() { + profile_cases.push(("selected-flow", None, Some(ids.as_slice()))); + } + for (label, helicities, colors) in profile_cases { + runtime + .evaluate_f64_into_with_selectors( + &momenta, + point_count, + helicities, + colors, + None, + None, + &mut output, + ) + .unwrap_or_else(|error| panic!("evaluate eager {label} oracle: {error}")); + let selected_profile = runtime + .evaluate_f64_arena_profile_repeated(&momenta, point_count, 3, helicities, colors) + .unwrap_or_else(|error| panic!("profile eager {label}: {error}")); + assert_eq!( + selected_profile.values, output, + "profiled eager {label} values" + ); + assert_eq!( + selected_profile + .profile + .native_input_container_allocation_count, + 0, + "profiled eager {label} input allocations" + ); + assert_eq!( + selected_profile.profile.native_output_allocation_count, 0, + "profiled eager {label} output allocations" + ); + assert_eq!( + selected_profile.profile.observed_scratch_reallocation_count, 0, + "profiled eager {label} scratch reallocations" + ); + } + + let helicity_count = runtime + .helicities() + .expect("eager helicity metadata") + .len() + .min(2); + let helicity_by_point = (0..point_count) + .map(|point| (point % helicity_count) as u32) + .collect::>(); + let color_by_point = + (runtime.metadata().color_accuracy == "lc").then(|| vec![0_u32; point_count]); + runtime + .evaluate_f64_into_with_selectors( + &momenta, + point_count, + None, + None, + Some(&helicity_by_point), + color_by_point.as_deref(), + &mut output, + ) + .expect("warm eager alternating selectors"); + let (result, allocations, bytes) = count_allocations(|| { + runtime.evaluate_f64_into_with_selectors( + &momenta, + point_count, + None, + None, + Some(&helicity_by_point), + color_by_point.as_deref(), + &mut output, + ) + }); + result.expect("repeat eager alternating selectors"); + assert_eq!( + (allocations, bytes), + (0, 0), + "warmed eager alternating selectors allocated" + ); + + runtime + .evaluate_resolved_f64(&momenta, point_count, None, None) + .expect("warm eager borrowed-input resolved values"); + let (resolved, allocations, bytes) = + count_allocations(|| runtime.evaluate_resolved_f64(&momenta, point_count, None, None)); + let resolved = resolved.expect("repeat eager borrowed-input resolved values"); + let result_owned_allocation_bound = resolved.helicity_ids.len() + resolved.color_ids.len() + 12; + assert!( + allocations <= result_owned_allocation_bound, + "warmed eager resolved path retained point-sized input containers: \ + allocations={allocations} bytes={bytes} result_bound={result_owned_allocation_bound}" + ); + assert_eq!(resolved.point_count, point_count); + assert_eq!( + resolved.values.len(), + point_count * resolved.helicity_ids.len() * resolved.color_ids.len() + ); + + runtime + .evaluate_resolved_f64( + &momenta, + point_count, + Some(&selected_helicities), + selected_colors.as_ref().map(|ids| ids.as_slice()), + ) + .expect("warm eager selected borrowed-input resolved values"); + let (selected_resolved, allocations, bytes) = count_allocations(|| { + runtime.evaluate_resolved_f64( + &momenta, + point_count, + Some(&selected_helicities), + selected_colors.as_ref().map(|ids| ids.as_slice()), + ) + }); + let selected_resolved = + selected_resolved.expect("repeat eager selected borrowed-input resolved values"); + let result_owned_allocation_bound = + selected_resolved.helicity_ids.len() + selected_resolved.color_ids.len() + 12; + assert!( + allocations <= result_owned_allocation_bound, + "warmed selected eager resolved path retained point-sized input containers: \ + allocations={allocations} bytes={bytes} result_bound={result_owned_allocation_bound}" + ); + assert_eq!(selected_resolved.point_count, point_count); + assert_eq!( + selected_resolved.values.len(), + point_count * selected_resolved.helicity_ids.len() * selected_resolved.color_ids.len() + ); + + let mut sentinel = [37.0_f64, 41.0]; + assert!( + runtime + .evaluate_f64_into(&momenta, point_count, &mut sentinel) + .is_err() + ); + assert_eq!(sentinel, [37.0, 41.0]); +} + +fn assert_close_f64(actual: f64, expected: f64, context: &str) { + let tolerance = 1.0e-15 + 1.0e-12 * expected.abs(); + assert!( + (actual - expected).abs() <= tolerance, + "{context}: {actual:.17e} != {expected:.17e} (tolerance {tolerance:.3e})" + ); +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/engine/eager_lane.rs b/pyamplicol/rust/crates/rusticol-core/src/engine/eager_lane.rs new file mode 100644 index 0000000000..99e13bcf26 --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/engine/eager_lane.rs @@ -0,0 +1,2899 @@ +// SPDX-License-Identifier: 0BSD + +use super::evaluation::accumulate_selected_lc_replay_resolved_f64; +use super::*; + +#[derive(Clone, Copy, Debug)] +pub(super) struct EagerParameterProjectionEntry { + pub(super) prepared_index: usize, + pub(super) runtime_real_index: usize, + pub(super) runtime_imaginary_index: Option, +} + +#[derive(Debug)] +pub(super) struct EagerParameterProjection { + pub(super) parameter_count: usize, + pub(super) entries: Vec, +} + +#[allow(dead_code)] // Includes the order-preserving per-point benchmark reference buffers. +pub(super) struct EagerNativeRuntime { + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + direct_scheduler: Option, + backend_name: String, + amplitude_count: usize, + has_selector_domains: bool, + effective_point_tile_size: usize, + workspace_bytes: usize, + parameter_projection: EagerParameterProjection, + raw_sum_groups: Vec, + lc_resolved_replay_plan: Option>, + lc_resolved_replay_selection_cache: + Vec<(LcResolvedReplaySelectionKey, Arc)>, + lc_replay_expanded_batch: Vec>, + lc_replay_seen_labels: Vec, + lc_replay_flat_momenta: Vec, + lc_replay_materialized_values: Vec, + lc_replay_target_components: Vec, + color_topology_replay: Option, + color_contraction: Option, + initial_values: Vec, + momenta: Vec, + model_parameters: Vec, + amplitudes: Vec, + color_group_scratch: Vec, + reduced: Vec, + selected_groups: Vec, + point_selector_offsets: Vec, + point_selector_groups: Vec, + point_selector_group_weights: Vec, + point_selector_members: Option>>, + point_selector_pairs: Vec<(u32, f64)>, + selected_helicity_indices: Vec, + selected_color_indices: Vec, + helicity_position_scratch: Vec>, + color_position_scratch: Vec>, + helicity_weights_scratch: Vec<(usize, f64)>, + right_helicity_weights_scratch: Vec<(usize, f64)>, + member_weights_scratch: Vec<(usize, usize, f64)>, + source_schedule: Option, + source_wavefunction_scratch: Vec>, +} + +#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] +struct EagerSourceKey { + source_kind: String, + leg_label: usize, + family: u8, + dimension: usize, + pdg_label: i32, + anti_pdg_label: i32, + orientation: u8, + momentum_transform: u8, + phase_real: u64, + phase_imaginary: u64, + source_helicity: i32, + chirality: i32, +} + +#[derive(Debug)] +struct EagerSourceClass { + representative_index: usize, + target_component_starts: Vec, + dimension: usize, +} + +#[derive(Debug)] +struct EagerSourceSchedule { + classes: Vec, + maximum_dimension: usize, +} + +impl EagerNativeRuntime { + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + pub(super) fn new_direct( + scheduler: crate::eager_runtime::EagerDirectExecutionRuntime, + backend_name: String, + parameter_projection: EagerParameterProjection, + raw_sum_groups: Vec, + color_contraction: Option, + color_topology_replay: Option, + ) -> Self { + let amplitude_count = scheduler.plan().amplitude_count(); + let has_selector_domains = scheduler.plan().has_selector_domains(); + let effective_point_tile_size = scheduler.effective_point_tile_size(); + let workspace_bytes = scheduler.workspace_bytes(); + Self { + direct_scheduler: Some(scheduler), + backend_name, + amplitude_count, + has_selector_domains, + effective_point_tile_size, + workspace_bytes, + parameter_projection, + raw_sum_groups, + lc_resolved_replay_plan: None, + lc_resolved_replay_selection_cache: Vec::new(), + lc_replay_expanded_batch: Vec::new(), + lc_replay_seen_labels: Vec::new(), + lc_replay_flat_momenta: Vec::new(), + lc_replay_materialized_values: Vec::new(), + lc_replay_target_components: Vec::new(), + color_topology_replay, + color_contraction, + initial_values: Vec::new(), + momenta: Vec::new(), + model_parameters: Vec::new(), + amplitudes: Vec::new(), + color_group_scratch: Vec::new(), + reduced: Vec::new(), + selected_groups: Vec::new(), + point_selector_offsets: Vec::new(), + point_selector_groups: Vec::new(), + point_selector_group_weights: Vec::new(), + point_selector_members: None, + point_selector_pairs: Vec::new(), + selected_helicity_indices: Vec::new(), + selected_color_indices: Vec::new(), + helicity_position_scratch: Vec::new(), + color_position_scratch: Vec::new(), + helicity_weights_scratch: Vec::new(), + right_helicity_weights_scratch: Vec::new(), + member_weights_scratch: Vec::new(), + source_schedule: None, + source_wavefunction_scratch: Vec::new(), + } + } + + fn execute_full_scheduler(&mut self, point_count: usize) -> RusticolResult<()> { + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + { + self.direct_scheduler + .as_mut() + .ok_or_else(|| { + RusticolError::internal( + "production eager runtime has no Direct-Arena scheduler", + ) + })? + .evaluate_into( + point_count, + &self.initial_values, + &self.momenta, + &self.model_parameters, + &mut self.amplitudes, + &mut self.reduced, + ) + } + #[cfg(not(any(feature = "f64-compiled", feature = "f64-symjit")))] + { + let _ = point_count; + Err(RusticolError::compatibility( + "eager-direct-arena-v1 requires the f64-compiled or f64-symjit feature; regenerate or load \ + this artifact with a Direct-Arena-capable runtime", + )) + } + } + + fn execute_full_totals_scheduler(&mut self, point_count: usize) -> RusticolResult<()> { + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + { + self.direct_scheduler + .as_mut() + .ok_or_else(|| { + RusticolError::internal( + "production eager runtime has no Direct-Arena scheduler", + ) + })? + .evaluate_totals_into( + point_count, + &self.initial_values, + &self.momenta, + &self.model_parameters, + &mut self.reduced, + ) + } + #[cfg(not(any(feature = "f64-compiled", feature = "f64-symjit")))] + { + let _ = point_count; + Err(RusticolError::compatibility( + "eager-direct-arena-v1 requires the f64-compiled or f64-symjit feature; regenerate or load \ + this artifact with a Direct-Arena-capable runtime", + )) + } + } + + fn execute_selected_scheduler( + &mut self, + active_groups: &[u32], + point_count: usize, + ) -> RusticolResult<()> { + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + { + self.direct_scheduler + .as_mut() + .ok_or_else(|| { + RusticolError::internal( + "production eager runtime has no Direct-Arena scheduler", + ) + })? + .evaluate_selected_active_amplitudes_into( + active_groups, + point_count, + &self.initial_values, + &self.momenta, + &self.model_parameters, + &mut self.amplitudes, + ) + } + #[cfg(not(any(feature = "f64-compiled", feature = "f64-symjit")))] + { + let _ = (active_groups, point_count); + Err(RusticolError::compatibility( + "eager-direct-arena-v1 requires the f64-compiled or f64-symjit feature; regenerate or load \ + this artifact with a Direct-Arena-capable runtime", + )) + } + } + + #[allow(clippy::too_many_arguments)] + fn execute_point_selected_scheduler( + &mut self, + group_offsets: &[usize], + active_groups: &[u32], + active_group_weights: &[f64], + point_count: usize, + reduced: &mut [f64], + ) -> RusticolResult<()> { + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + { + self.direct_scheduler + .as_mut() + .ok_or_else(|| { + RusticolError::internal( + "production eager runtime has no Direct-Arena scheduler", + ) + })? + .evaluate_point_selected_group_sets_into( + group_offsets, + active_groups, + active_group_weights, + point_count, + &self.initial_values, + &self.momenta, + &self.model_parameters, + reduced, + ) + } + #[cfg(not(any(feature = "f64-compiled", feature = "f64-symjit")))] + { + let _ = ( + group_offsets, + active_groups, + active_group_weights, + point_count, + reduced, + ); + Err(RusticolError::compatibility( + "eager-direct-arena-v1 requires the f64-compiled or f64-symjit feature; regenerate or load \ + this artifact with a Direct-Arena-capable runtime", + )) + } + } + + fn execute_profile_scheduler( + &mut self, + active_groups: Option<&[u32]>, + point_count: usize, + ) -> RusticolResult { + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + { + let scheduler = self.direct_scheduler.as_mut().ok_or_else(|| { + RusticolError::internal("production eager runtime has no Direct-Arena scheduler") + })?; + if let Some(active_groups) = active_groups { + scheduler.evaluate_selected_profile_into( + active_groups, + point_count, + &self.initial_values, + &self.momenta, + &self.model_parameters, + &mut self.amplitudes, + ) + } else { + scheduler.evaluate_profile_into( + point_count, + &self.initial_values, + &self.momenta, + &self.model_parameters, + &mut self.amplitudes, + &mut self.reduced, + ) + } + } + #[cfg(not(any(feature = "f64-compiled", feature = "f64-symjit")))] + { + let _ = (active_groups, point_count); + Err(RusticolError::compatibility( + "eager-direct-arena-v1 requires the f64-compiled or f64-symjit feature; regenerate or load \ + this artifact with a Direct-Arena-capable runtime", + )) + } + } + + fn direct_internal_scratch_bytes(&self) -> u64 { + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + { + self.direct_scheduler + .as_ref() + .map_or(0, |scheduler| scheduler.internal_scratch_bytes()) + } + #[cfg(not(any(feature = "f64-compiled", feature = "f64-symjit")))] + { + 0 + } + } + + fn direct_internal_broadcast_bytes(&self) -> u64 { + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + { + self.direct_scheduler + .as_ref() + .map_or(0, |scheduler| scheduler.internal_broadcast_bytes()) + } + #[cfg(not(any(feature = "f64-compiled", feature = "f64-symjit")))] + { + 0 + } + } + + pub(super) fn backend_name(&self) -> &str { + &self.backend_name + } + + pub(super) fn effective_point_tile_size(&self) -> usize { + self.effective_point_tile_size + } + + pub(super) fn workspace_bytes(&self) -> usize { + self.workspace_bytes + } + + /// Execute the warmed eager Direct-Arena lane from a borrowed flat input. + /// + /// This is the ordinary native timing boundary: it performs no phase + /// clocks, nested momentum materialization, resolved-value construction, + /// or result allocation. + pub(super) fn run_f64_view_into_unprofiled( + &mut self, + common: &mut ExecutionRuntime, + batch: F64MomentumBatchView<'_>, + output: &mut [f64], + ) -> RusticolResult<()> { + let point_count = batch.point_count(); + if output.len() != point_count { + return Err(RusticolError::invalid_argument(format!( + "eager evaluation output has length {}, expected {point_count}", + output.len() + ))); + } + if common.color_topology_replay_enabled { + return self.run_f64_view_with_color_topology_replay_into_unprofiled( + common, batch, None, None, output, + ); + } + if common.lc_topology_replay_enabled { + return self.run_f64_view_with_lc_topology_replay_into_unprofiled( + common, batch, None, None, output, + ); + } + prepare_eager_inputs_view( + common, + batch, + &mut self.initial_values, + &mut self.momenta, + &mut self.source_schedule, + &mut self.source_wavefunction_scratch, + )?; + project_model_parameters( + &self.parameter_projection, + &common.model_parameter_values_f64, + &mut self.model_parameters, + )?; + self.reduced.resize(point_count, 0.0); + self.execute_full_totals_scheduler(point_count)?; + for (target, value) in output.iter_mut().zip(&self.reduced) { + *target = common.normalization_factor * *value; + } + Ok(()) + } + + /// Execute public global selectors directly into caller-sized totals. + /// + /// The selector weights feed the plane-native eager reducer, so LC, + /// compact NLC, and full-colour contractions avoid materializing the + /// resolved `(point, helicity, colour)` tensor. + pub(super) fn run_f64_view_selected_into_unprofiled( + &mut self, + common: &mut ExecutionRuntime, + batch: F64MomentumBatchView<'_>, + selected_helicity_ids: Option<&BTreeSet>, + selected_color_ids: Option<&BTreeSet>, + output: &mut [f64], + ) -> RusticolResult<()> { + let point_count = batch.point_count(); + if output.len() != point_count { + return Err(RusticolError::invalid_argument(format!( + "eager evaluation output has length {}, expected {point_count}", + output.len() + ))); + } + if selected_helicity_ids.is_none() && selected_color_ids.is_none() { + return self.run_f64_view_into_unprofiled(common, batch, output); + } + if common.color_topology_replay_enabled { + return self.run_f64_view_with_color_topology_replay_into_unprofiled( + common, + batch, + selected_helicity_ids, + selected_color_ids, + output, + ); + } + if common.lc_topology_replay_enabled { + return self.run_f64_view_with_lc_topology_replay_into_unprofiled( + common, + batch, + selected_helicity_ids, + selected_color_ids, + output, + ); + } + let physics = common.physics.clone().ok_or_else(|| { + RusticolError::artifact("selected eager evaluation requires physics metadata") + })?; + if self.point_selector_members.is_none() { + self.point_selector_members = Some(build_eager_selector_members( + &self.raw_sum_groups, + self.color_contraction.is_some(), + &physics, + )?); + } + fill_eager_group_sets_by_point( + &self.raw_sum_groups, + self.point_selector_members + .as_deref() + .expect("selector members initialized"), + &physics, + point_count, + selected_helicity_ids, + selected_color_ids, + None, + None, + &mut self.point_selector_offsets, + &mut self.point_selector_groups, + &mut self.point_selector_group_weights, + &mut self.point_selector_pairs, + &mut self.selected_helicity_indices, + &mut self.selected_color_indices, + )?; + prepare_eager_inputs_view( + common, + batch, + &mut self.initial_values, + &mut self.momenta, + &mut self.source_schedule, + &mut self.source_wavefunction_scratch, + )?; + project_model_parameters( + &self.parameter_projection, + &common.model_parameter_values_f64, + &mut self.model_parameters, + )?; + + // Keep caller output untouched if validation or execution fails. + let offsets = std::mem::take(&mut self.point_selector_offsets); + let groups = std::mem::take(&mut self.point_selector_groups); + let group_weights = std::mem::take(&mut self.point_selector_group_weights); + let mut reduced = std::mem::take(&mut self.reduced); + reduced.resize(point_count, 0.0); + let result = self.execute_point_selected_scheduler( + &offsets, + &groups, + &group_weights, + point_count, + &mut reduced, + ); + self.point_selector_offsets = offsets; + self.point_selector_groups = groups; + self.point_selector_group_weights = group_weights; + self.reduced = reduced; + result?; + for (target, value) in output.iter_mut().zip(&self.reduced) { + *target = common.normalization_factor * *value; + } + Ok(()) + } + + #[allow(clippy::too_many_arguments)] + fn run_resolved_f64_view_materialized_into_unprofiled( + &mut self, + common: &mut ExecutionRuntime, + batch: F64MomentumBatchView<'_>, + selected_helicity_ids: Option<&BTreeSet>, + selected_color_ids: Option<&BTreeSet>, + output: &mut Vec, + ) -> RusticolResult<()> { + let point_count = batch.point_count(); + prepare_eager_inputs_view( + common, + batch, + &mut self.initial_values, + &mut self.momenta, + &mut self.source_schedule, + &mut self.source_wavefunction_scratch, + )?; + project_model_parameters( + &self.parameter_projection, + &common.model_parameter_values_f64, + &mut self.model_parameters, + )?; + let amplitude_len = point_count + .checked_mul(self.amplitude_count) + .ok_or_else(|| RusticolError::invalid_argument("eager amplitudes overflow"))?; + self.amplitudes + .resize(amplitude_len, crate::EagerComplex64::new(0.0, 0.0)); + self.reduced.resize(point_count, 0.0); + let physics = common.physics.clone().ok_or_else(|| { + RusticolError::artifact("resolved eager evaluation requires physics metadata") + })?; + let has_selected_groups = if self.has_selector_domains + && (selected_helicity_ids.is_some() || selected_color_ids.is_some()) + { + fill_selected_eager_group_ids( + &self.raw_sum_groups, + &physics, + selected_helicity_ids, + selected_color_ids, + &mut self.selected_groups, + )?; + true + } else { + false + }; + if has_selected_groups { + let selected_groups = std::mem::take(&mut self.selected_groups); + let result = self.execute_selected_scheduler(&selected_groups, point_count); + self.selected_groups = selected_groups; + result?; + } else { + self.execute_full_scheduler(point_count)?; + } + reduce_eager_amplitudes_resolved_into( + &self.amplitudes, + point_count, + &self.raw_sum_groups, + self.color_contraction.as_mut(), + &mut self.color_group_scratch, + &physics, + common.normalization_factor, + selected_helicity_ids, + selected_color_ids, + output, + &mut self.selected_helicity_indices, + &mut self.selected_color_indices, + &mut self.helicity_position_scratch, + &mut self.color_position_scratch, + &mut self.helicity_weights_scratch, + &mut self.right_helicity_weights_scratch, + &mut self.member_weights_scratch, + ) + } + + /// Resolve eager components directly from a borrowed momentum view. + /// + /// The returned value and public-axis vectors are the only result-owned + /// allocations. Input crossing, eager source preparation, and topology + /// replay all consume the borrowed flat view without constructing nested + /// point or momentum containers. + pub(super) fn run_resolved_f64_view_unprofiled( + &mut self, + common: &mut ExecutionRuntime, + batch: F64MomentumBatchView<'_>, + selected_helicity_ids: Option<&BTreeSet>, + selected_color_ids: Option<&BTreeSet>, + ) -> RusticolResult> { + let point_count = batch.point_count(); + let mut values = Vec::new(); + if common.color_topology_replay_enabled { + return self.run_resolved_f64_view_with_color_topology_replay_unprofiled( + common, + batch, + selected_helicity_ids, + selected_color_ids, + ); + } + if common.lc_topology_replay_enabled { + let selection = self.run_resolved_f64_view_with_lc_topology_replay_into_unprofiled( + common, + batch, + selected_helicity_ids, + selected_color_ids, + &mut values, + )?; + return Ok(ResolvedValues { + values, + point_count, + helicity_indices: selection.helicity_indices.clone(), + color_indices: selection.color_indices.clone(), + }); + } + self.run_resolved_f64_view_materialized_into_unprofiled( + common, + batch, + selected_helicity_ids, + selected_color_ids, + &mut values, + )?; + Ok(ResolvedValues { + values, + point_count, + helicity_indices: self.selected_helicity_indices.clone(), + color_indices: self.selected_color_indices.clone(), + }) + } + + #[allow(clippy::too_many_arguments)] + fn run_f64_view_with_color_topology_replay_into_unprofiled( + &mut self, + common: &mut ExecutionRuntime, + batch: F64MomentumBatchView<'_>, + selected_helicity_ids: Option<&BTreeSet>, + selected_color_ids: Option<&BTreeSet>, + output: &mut [f64], + ) -> RusticolResult<()> { + let resolved = self.run_resolved_f64_view_with_color_topology_replay_unprofiled( + common, + batch, + selected_helicity_ids, + selected_color_ids, + )?; + if output.len() != resolved.point_count { + return Err(RusticolError::invalid_argument( + "eager color replay output has an invalid point count", + )); + } + let component_count = resolved.helicity_indices.len() * resolved.color_indices.len(); + if component_count == 0 { + output.fill(0.0); + } else { + for (target, components) in output + .iter_mut() + .zip(resolved.values.chunks_exact(component_count)) + { + *target = components.iter().sum(); + } + } + Ok(()) + } + + fn run_resolved_f64_view_with_color_topology_replay_unprofiled( + &mut self, + common: &mut ExecutionRuntime, + batch: F64MomentumBatchView<'_>, + selected_helicity_ids: Option<&BTreeSet>, + selected_color_ids: Option<&BTreeSet>, + ) -> RusticolResult> { + let point_count = batch.point_count(); + let physics = common.physics.clone().ok_or_else(|| { + RusticolError::artifact( + "eager color topology replay requires resolved physics metadata", + ) + })?; + let mappings = Arc::clone(&common.color_topology_replay_mappings); + let mut replay = self.color_topology_replay.take().ok_or_else(|| { + RusticolError::integrity("eager color topology replay has no amplitude gather runtime") + })?; + let mut flat_momenta = std::mem::take(&mut self.lc_replay_flat_momenta); + let result = (|| { + replay.begin_color_topology_replay(point_count)?; + for mapping_index in 0..mappings.len() { + let evaluation_view = if mappings[mapping_index].is_empty() { + batch + } else { + fill_lc_replay_expanded_flat_from_view( + &mut flat_momenta, + batch, + common.external_count, + &[mapping_index], + &mappings, + )?; + F64MomentumBatchView::from_contiguous_prevalidated( + &flat_momenta, + point_count, + common.external_count, + None, + )? + }; + prepare_eager_inputs_view( + common, + evaluation_view, + &mut self.initial_values, + &mut self.momenta, + &mut self.source_schedule, + &mut self.source_wavefunction_scratch, + )?; + project_model_parameters( + &self.parameter_projection, + &common.model_parameter_values_f64, + &mut self.model_parameters, + )?; + self.amplitudes.resize( + point_count + .checked_mul(self.amplitude_count) + .ok_or_else(|| { + RusticolError::invalid_argument( + "eager color replay amplitudes overflow", + ) + })?, + crate::EagerComplex64::new(0.0, 0.0), + ); + self.reduced.resize(point_count, 0.0); + self.execute_full_scheduler(point_count)?; + replay.gather_color_topology_replay_eager_amplitudes( + &self.amplitudes, + point_count, + mapping_index, + )?; + } + replay.reduce_color_topology_replay_f64_resolved( + point_count, + &physics, + common.normalization_factor, + selected_helicity_ids, + selected_color_ids, + ) + })(); + self.lc_replay_flat_momenta = flat_momenta; + self.color_topology_replay = Some(replay); + result + } + + #[allow(clippy::too_many_arguments)] + fn run_resolved_f64_view_with_lc_topology_replay_into_unprofiled( + &mut self, + common: &mut ExecutionRuntime, + batch: F64MomentumBatchView<'_>, + selected_helicity_ids: Option<&BTreeSet>, + selected_color_ids: Option<&BTreeSet>, + output: &mut Vec, + ) -> RusticolResult> { + let point_count = batch.point_count(); + let physics = common.physics.clone().ok_or_else(|| { + RusticolError::artifact("eager LC topology replay requires resolved physics metadata") + })?; + let replay_plan = if let Some(plan) = &self.lc_resolved_replay_plan { + Arc::clone(plan) + } else { + let materialized_sector_by_id = + common.lc_materialized_sectors_by_id_from_groups(&physics, &self.raw_sum_groups)?; + let plan = Arc::new(physics.lc_resolved_replay_plan( + &common.lc_topology_replay_public_mappings, + &common.lc_topology_replay_routes, + &materialized_sector_by_id, + )?); + self.lc_resolved_replay_plan = Some(Arc::clone(&plan)); + plan + }; + let selection = if let Some((_, selection)) = self + .lc_resolved_replay_selection_cache + .iter() + .find(|(key, _)| { + lc_replay_selection_key_matches( + key, + &physics, + selected_helicity_ids, + selected_color_ids, + ) + }) { + Arc::clone(selection) + } else { + let selection_key = physics + .lc_resolved_replay_selection_key(selected_helicity_ids, selected_color_ids)?; + let selection = Arc::new( + physics + .select_lc_resolved_replay_plan_for_key(replay_plan.as_ref(), &selection_key)?, + ); + self.lc_resolved_replay_selection_cache + .push((selection_key, Arc::clone(&selection))); + selection + }; + let target_component_count = selection + .helicity_indices + .len() + .checked_mul(selection.color_indices.len()) + .ok_or_else(|| RusticolError::invalid_argument("resolved eager shape overflows"))?; + let target_scalar_count = point_count + .checked_mul(target_component_count) + .ok_or_else(|| RusticolError::invalid_argument("resolved eager shape overflows"))?; + output.resize(target_scalar_count, 0.0); + output.fill(0.0); + if target_component_count == 0 { + return Ok(selection); + } + + let mappings = Arc::clone(&common.lc_topology_replay_mappings); + let mappings_per_chunk = replay_mappings_per_expanded_batch(point_count); + let mut flat_momenta = std::mem::take(&mut self.lc_replay_flat_momenta); + let mut materialized_values = std::mem::take(&mut self.lc_replay_materialized_values); + let result = (|| { + for source_group in &selection.source_groups { + for chunk_start in + (0..source_group.mapping_indices.len()).step_by(mappings_per_chunk) + { + let chunk_end = usize::min( + chunk_start + mappings_per_chunk, + source_group.mapping_indices.len(), + ); + let mapping_indices = &source_group.mapping_indices[chunk_start..chunk_end]; + let entry_chunk = &source_group.entries[chunk_start..chunk_end]; + let identity_mapping = + mapping_indices.len() == 1 && mappings[mapping_indices[0]].is_empty(); + let evaluation_view = if identity_mapping { + batch + } else { + fill_lc_replay_expanded_flat_from_view( + &mut flat_momenta, + batch, + common.external_count, + mapping_indices, + &mappings, + )?; + F64MomentumBatchView::from_contiguous_prevalidated( + &flat_momenta, + point_count * mapping_indices.len(), + common.external_count, + None, + )? + }; + self.run_resolved_f64_view_materialized_into_unprofiled( + common, + evaluation_view, + Some(&source_group.helicity_ids), + Some(&source_group.color_ids), + &mut materialized_values, + )?; + accumulate_selected_lc_replay_values_f64( + output, + point_count, + &materialized_values, + entry_chunk, + source_group.source_component_count, + target_component_count, + )?; + } + } + Ok(()) + })(); + self.lc_replay_flat_momenta = flat_momenta; + self.lc_replay_materialized_values = materialized_values; + result?; + Ok(selection) + } + + #[allow(clippy::too_many_arguments)] + fn run_f64_view_with_lc_topology_replay_into_unprofiled( + &mut self, + common: &mut ExecutionRuntime, + batch: F64MomentumBatchView<'_>, + selected_helicity_ids: Option<&BTreeSet>, + selected_color_ids: Option<&BTreeSet>, + output: &mut [f64], + ) -> RusticolResult<()> { + let point_count = batch.point_count(); + if output.len() != point_count { + return Err(RusticolError::invalid_argument(format!( + "eager topology-replay output has length {}, expected {point_count}", + output.len() + ))); + } + let mut target_components = std::mem::take(&mut self.lc_replay_target_components); + let result = self.run_resolved_f64_view_with_lc_topology_replay_into_unprofiled( + common, + batch, + selected_helicity_ids, + selected_color_ids, + &mut target_components, + ); + if let Ok(selection) = &result { + let target_component_count = + selection.helicity_indices.len() * selection.color_indices.len(); + if target_component_count == 0 { + output.fill(0.0); + } else { + for (target, components) in output + .iter_mut() + .zip(target_components.chunks_exact(target_component_count)) + { + *target = components.iter().sum(); + } + } + } + self.lc_replay_target_components = target_components; + result.map(|_| ()) + } + + #[allow(dead_code)] // Retained as the row-major eager benchmark oracle. + pub(super) fn run_f64( + &mut self, + common: &mut ExecutionRuntime, + batch: &[Vec<[f64; 4]>], + ) -> RusticolResult<(Vec, RuntimeProfile)> { + if common.color_topology_replay_enabled { + let (resolved, profile) = + self.run_resolved_f64_with_color_topology_replay(common, batch, None, None, false)?; + return Ok((sum_eager_resolved(&resolved), profile)); + } + if common.lc_topology_replay_enabled { + let (resolved, profile) = + self.run_resolved_f64_with_lc_topology_replay(common, batch, None, None, false)?; + return Ok((sum_eager_resolved(&resolved), profile)); + } + if batch.is_empty() { + return Err(RusticolError::invalid_argument( + "eager evaluation requires at least one point", + )); + } + let total_start = Instant::now(); + let point_count = batch.len(); + let (source_fill, momentum_setup) = prepare_eager_inputs( + common, + batch, + &mut self.initial_values, + &mut self.momenta, + &mut self.source_schedule, + &mut self.source_wavefunction_scratch, + )?; + project_model_parameters( + &self.parameter_projection, + &common.model_parameter_values_f64, + &mut self.model_parameters, + )?; + let amplitude_len = point_count + .checked_mul(self.amplitude_count) + .ok_or_else(|| RusticolError::invalid_argument("eager amplitudes overflow"))?; + self.amplitudes + .resize(amplitude_len, crate::EagerComplex64::new(0.0, 0.0)); + self.reduced.resize(point_count, 0.0); + + let execute_start = Instant::now(); + self.execute_full_scheduler(point_count)?; + let execute_s = execute_start.elapsed().as_secs_f64(); + for value in &mut self.reduced { + *value *= common.normalization_factor; + } + let values = self.reduced.clone(); + Ok(( + values, + RuntimeProfile { + source_fill_s: profile_duration_seconds(source_fill), + momentum_input_setup_s: profile_duration_seconds(momentum_setup), + momentum_setup_s: profile_duration_seconds(momentum_setup), + stage_evaluator_call_s: execute_s, + stage_evaluator_s: execute_s, + total_s: total_start.elapsed().as_secs_f64(), + ..RuntimeProfile::default() + }, + )) + } + + #[allow(clippy::too_many_arguments)] + #[allow(dead_code)] // Retained as the order-preserving benchmark reference lane. + pub(super) fn run_f64_by_point_selectors( + &mut self, + common: &mut ExecutionRuntime, + batch: &[Vec<[f64; 4]>], + selected_helicity_ids: Option<&BTreeSet>, + selected_color_ids: Option<&BTreeSet>, + helicity_by_point: Option<&[u32]>, + color_by_point: Option<&[u32]>, + ) -> RusticolResult<(Vec, RuntimeProfile)> { + if common.lc_topology_replay_enabled || common.color_topology_replay_enabled { + return Err(RusticolError::integrity( + "eager topology-replay per-point selectors must use the shared partition planner", + )); + } + let physics = common.physics.clone().ok_or_else(|| { + RusticolError::artifact("per-point eager selection requires physics metadata") + })?; + if self.point_selector_members.is_none() { + self.point_selector_members = Some(build_eager_selector_members( + &self.raw_sum_groups, + self.color_contraction.is_some(), + &physics, + )?); + } + fill_eager_group_sets_by_point( + &self.raw_sum_groups, + self.point_selector_members + .as_deref() + .expect("selector members initialized"), + &physics, + batch.len(), + selected_helicity_ids, + selected_color_ids, + helicity_by_point, + color_by_point, + &mut self.point_selector_offsets, + &mut self.point_selector_groups, + &mut self.point_selector_group_weights, + &mut self.point_selector_pairs, + &mut self.selected_helicity_indices, + &mut self.selected_color_indices, + )?; + let offsets = std::mem::take(&mut self.point_selector_offsets); + let groups = std::mem::take(&mut self.point_selector_groups); + let group_weights = std::mem::take(&mut self.point_selector_group_weights); + let mut reduced = std::mem::take(&mut self.reduced); + reduced.resize(batch.len(), 0.0); + let result = self.run_coherent_group_sets_by_point_f64_into( + common, + batch, + &offsets, + &groups, + &group_weights, + &mut reduced, + ); + self.point_selector_offsets = offsets; + self.point_selector_groups = groups; + self.point_selector_group_weights = group_weights; + self.reduced = reduced; + let profile = result?; + Ok((self.reduced.clone(), profile)) + } + + #[allow(dead_code)] // Retained as the order-preserving benchmark reference lane. + pub(super) fn run_coherent_group_sets_by_point_f64_into( + &mut self, + common: &mut ExecutionRuntime, + batch: &[Vec<[f64; 4]>], + group_offsets: &[usize], + active_groups: &[u32], + active_group_weights: &[f64], + reduced: &mut [f64], + ) -> RusticolResult { + if batch.is_empty() { + return Err(RusticolError::invalid_argument( + "eager evaluation requires at least one point", + )); + } + if reduced.len() != batch.len() { + return Err(RusticolError::invalid_argument(format!( + "eager per-point selector output has length {}, expected {}", + reduced.len(), + batch.len() + ))); + } + let total_start = Instant::now(); + let point_count = batch.len(); + let (source_fill, momentum_setup) = prepare_eager_inputs( + common, + batch, + &mut self.initial_values, + &mut self.momenta, + &mut self.source_schedule, + &mut self.source_wavefunction_scratch, + )?; + project_model_parameters( + &self.parameter_projection, + &common.model_parameter_values_f64, + &mut self.model_parameters, + )?; + let execute_start = Instant::now(); + self.execute_point_selected_scheduler( + group_offsets, + active_groups, + active_group_weights, + point_count, + reduced, + )?; + let execute_s = execute_start.elapsed().as_secs_f64(); + for value in reduced { + *value *= common.normalization_factor; + } + Ok(RuntimeProfile { + source_fill_s: profile_duration_seconds(source_fill), + momentum_input_setup_s: profile_duration_seconds(momentum_setup), + momentum_setup_s: profile_duration_seconds(momentum_setup), + stage_evaluator_call_s: execute_s, + stage_evaluator_s: execute_s, + total_s: total_start.elapsed().as_secs_f64(), + ..RuntimeProfile::default() + }) + } + + pub(super) fn run_f64_profile( + &mut self, + common: &mut ExecutionRuntime, + batch: &[Vec<[f64; 4]>], + ) -> RusticolResult<(Vec, RuntimeProfile)> { + if common.color_topology_replay_enabled { + let (resolved, profile) = + self.run_resolved_f64_with_color_topology_replay(common, batch, None, None, true)?; + return Ok((sum_eager_resolved(&resolved), profile)); + } + if common.lc_topology_replay_enabled { + let (resolved, profile) = + self.run_resolved_f64_with_lc_topology_replay(common, batch, None, None, true)?; + return Ok((sum_eager_resolved(&resolved), profile)); + } + if batch.is_empty() { + return Err(RusticolError::invalid_argument( + "eager evaluation requires at least one point", + )); + } + let total_start = Instant::now(); + let point_count = batch.len(); + let (source_fill, momentum_setup) = prepare_eager_inputs( + common, + batch, + &mut self.initial_values, + &mut self.momenta, + &mut self.source_schedule, + &mut self.source_wavefunction_scratch, + )?; + project_model_parameters( + &self.parameter_projection, + &common.model_parameter_values_f64, + &mut self.model_parameters, + )?; + let amplitude_len = point_count + .checked_mul(self.amplitude_count) + .ok_or_else(|| RusticolError::invalid_argument("eager amplitudes overflow"))?; + self.amplitudes + .resize(amplitude_len, crate::EagerComplex64::new(0.0, 0.0)); + self.reduced.resize(point_count, 0.0); + + let eager = self.execute_profile_scheduler(None, point_count)?; + validate_eager_profile_accounting(&eager)?; + for value in &mut self.reduced { + *value *= common.normalization_factor; + } + let values = self.reduced.clone(); + Ok(( + values, + RuntimeProfile { + source_fill_s: profile_duration_seconds(source_fill), + momentum_input_setup_s: profile_duration_seconds(momentum_setup), + momentum_setup_s: profile_duration_seconds(momentum_setup), + stage_evaluator_call_s: eager.total.as_secs_f64(), + stage_evaluator_s: eager.total.as_secs_f64(), + reduction_s: eager.reduction.as_secs_f64(), + total_s: total_start.elapsed().as_secs_f64(), + eager_initialize_s: eager.initialize.as_secs_f64(), + eager_gather_s: eager.gather.as_secs_f64(), + eager_kernel_call_s: eager.kernel_call.as_secs_f64(), + eager_invocation_scatter_s: eager.invocation_scatter.as_secs_f64(), + eager_finalization_s: eager.finalization.as_secs_f64(), + eager_scatter_finalization_s: (eager.invocation_scatter + eager.finalization) + .as_secs_f64(), + eager_closure_s: eager.closure.as_secs_f64(), + eager_reduction_s: eager.reduction.as_secs_f64(), + eager_copy_out_s: eager.copy_out.as_secs_f64(), + eager_internal_scratch_bytes: eager.internal_scratch_bytes, + eager_internal_broadcast_bytes: eager.internal_broadcast_bytes, + evaluator_backend_call_count: eager.backend_call_count, + ..RuntimeProfile::default() + }, + )) + } + + #[allow(dead_code)] // Retained as the row-major eager resolved-value oracle. + pub(super) fn run_resolved_f64( + &mut self, + common: &mut ExecutionRuntime, + batch: &[Vec<[f64; 4]>], + selected_helicity_ids: Option<&BTreeSet>, + selected_color_ids: Option<&BTreeSet>, + ) -> RusticolResult<(ResolvedValues, RuntimeProfile)> { + if common.color_topology_replay_enabled { + return self.run_resolved_f64_with_color_topology_replay( + common, + batch, + selected_helicity_ids, + selected_color_ids, + false, + ); + } + if common.lc_topology_replay_enabled { + return self.run_resolved_f64_with_lc_topology_replay( + common, + batch, + selected_helicity_ids, + selected_color_ids, + false, + ); + } + self.run_resolved_f64_materialized(common, batch, selected_helicity_ids, selected_color_ids) + } + + fn run_resolved_f64_materialized( + &mut self, + common: &mut ExecutionRuntime, + batch: &[Vec<[f64; 4]>], + selected_helicity_ids: Option<&BTreeSet>, + selected_color_ids: Option<&BTreeSet>, + ) -> RusticolResult<(ResolvedValues, RuntimeProfile)> { + if batch.is_empty() { + return Err(RusticolError::invalid_argument( + "eager evaluation requires at least one point", + )); + } + let total_start = Instant::now(); + let point_count = batch.len(); + let (source_fill, momentum_setup) = prepare_eager_inputs( + common, + batch, + &mut self.initial_values, + &mut self.momenta, + &mut self.source_schedule, + &mut self.source_wavefunction_scratch, + )?; + project_model_parameters( + &self.parameter_projection, + &common.model_parameter_values_f64, + &mut self.model_parameters, + )?; + let amplitude_len = point_count + .checked_mul(self.amplitude_count) + .ok_or_else(|| RusticolError::invalid_argument("eager amplitudes overflow"))?; + self.amplitudes + .resize(amplitude_len, crate::EagerComplex64::new(0.0, 0.0)); + self.reduced.resize(point_count, 0.0); + + let physics = common.physics.as_ref().ok_or_else(|| { + RusticolError::artifact("resolved eager evaluation requires physics metadata") + })?; + let has_selected_groups = if self.has_selector_domains + && (selected_helicity_ids.is_some() || selected_color_ids.is_some()) + { + fill_selected_eager_group_ids( + &self.raw_sum_groups, + physics, + selected_helicity_ids, + selected_color_ids, + &mut self.selected_groups, + )?; + true + } else { + false + }; + let execute_start = Instant::now(); + if has_selected_groups { + let selected_groups = std::mem::take(&mut self.selected_groups); + let result = self.execute_selected_scheduler(&selected_groups, point_count); + self.selected_groups = selected_groups; + result?; + } else { + self.execute_full_scheduler(point_count)?; + } + let execute_s = execute_start.elapsed().as_secs_f64(); + let reduction_start = Instant::now(); + let resolved = reduce_eager_amplitudes_resolved( + &self.amplitudes, + point_count, + &self.raw_sum_groups, + self.color_contraction.as_mut(), + &mut self.color_group_scratch, + physics, + common.normalization_factor, + selected_helicity_ids, + selected_color_ids, + )?; + let reduction_s = reduction_start.elapsed().as_secs_f64(); + Ok(( + resolved, + RuntimeProfile { + source_fill_s: profile_duration_seconds(source_fill), + momentum_input_setup_s: profile_duration_seconds(momentum_setup), + momentum_setup_s: profile_duration_seconds(momentum_setup), + stage_evaluator_call_s: execute_s, + stage_evaluator_s: execute_s, + reduction_s, + total_s: total_start.elapsed().as_secs_f64(), + ..RuntimeProfile::default() + }, + )) + } + + pub(super) fn run_resolved_f64_profile( + &mut self, + common: &mut ExecutionRuntime, + batch: &[Vec<[f64; 4]>], + selected_helicity_ids: Option<&BTreeSet>, + selected_color_ids: Option<&BTreeSet>, + ) -> RusticolResult<(ResolvedValues, RuntimeProfile)> { + if common.color_topology_replay_enabled { + return self.run_resolved_f64_with_color_topology_replay( + common, + batch, + selected_helicity_ids, + selected_color_ids, + true, + ); + } + if common.lc_topology_replay_enabled { + return self.run_resolved_f64_with_lc_topology_replay( + common, + batch, + selected_helicity_ids, + selected_color_ids, + true, + ); + } + self.run_resolved_f64_profile_materialized( + common, + batch, + selected_helicity_ids, + selected_color_ids, + ) + } + + fn run_resolved_f64_profile_materialized( + &mut self, + common: &mut ExecutionRuntime, + batch: &[Vec<[f64; 4]>], + selected_helicity_ids: Option<&BTreeSet>, + selected_color_ids: Option<&BTreeSet>, + ) -> RusticolResult<(ResolvedValues, RuntimeProfile)> { + if batch.is_empty() { + return Err(RusticolError::invalid_argument( + "eager evaluation requires at least one point", + )); + } + let total_start = Instant::now(); + let point_count = batch.len(); + let (source_fill, momentum_setup) = prepare_eager_inputs( + common, + batch, + &mut self.initial_values, + &mut self.momenta, + &mut self.source_schedule, + &mut self.source_wavefunction_scratch, + )?; + project_model_parameters( + &self.parameter_projection, + &common.model_parameter_values_f64, + &mut self.model_parameters, + )?; + let amplitude_len = point_count + .checked_mul(self.amplitude_count) + .ok_or_else(|| RusticolError::invalid_argument("eager amplitudes overflow"))?; + self.amplitudes + .resize(amplitude_len, crate::EagerComplex64::new(0.0, 0.0)); + self.reduced.resize(point_count, 0.0); + + let physics = common.physics.as_ref().ok_or_else(|| { + RusticolError::artifact("resolved eager evaluation requires physics metadata") + })?; + let has_selected_groups = if self.has_selector_domains + && (selected_helicity_ids.is_some() || selected_color_ids.is_some()) + { + fill_selected_eager_group_ids( + &self.raw_sum_groups, + physics, + selected_helicity_ids, + selected_color_ids, + &mut self.selected_groups, + )?; + true + } else { + false + }; + let eager = if has_selected_groups { + let selected_groups = std::mem::take(&mut self.selected_groups); + let result = self.execute_profile_scheduler(Some(&selected_groups), point_count); + self.selected_groups = selected_groups; + result? + } else { + self.execute_profile_scheduler(None, point_count)? + }; + validate_eager_profile_accounting(&eager)?; + let reduction_start = Instant::now(); + let resolved = reduce_eager_amplitudes_resolved( + &self.amplitudes, + point_count, + &self.raw_sum_groups, + self.color_contraction.as_mut(), + &mut self.color_group_scratch, + physics, + common.normalization_factor, + selected_helicity_ids, + selected_color_ids, + )?; + let resolved_reduction = reduction_start.elapsed(); + let eager_execution = eager.total + resolved_reduction; + let reduction = eager.reduction + resolved_reduction; + Ok(( + resolved, + RuntimeProfile { + source_fill_s: profile_duration_seconds(source_fill), + momentum_input_setup_s: profile_duration_seconds(momentum_setup), + momentum_setup_s: profile_duration_seconds(momentum_setup), + stage_evaluator_call_s: eager_execution.as_secs_f64(), + stage_evaluator_s: eager_execution.as_secs_f64(), + reduction_s: reduction.as_secs_f64(), + total_s: total_start.elapsed().as_secs_f64(), + eager_initialize_s: eager.initialize.as_secs_f64(), + eager_gather_s: eager.gather.as_secs_f64(), + eager_kernel_call_s: eager.kernel_call.as_secs_f64(), + eager_invocation_scatter_s: eager.invocation_scatter.as_secs_f64(), + eager_finalization_s: eager.finalization.as_secs_f64(), + eager_scatter_finalization_s: (eager.invocation_scatter + eager.finalization) + .as_secs_f64(), + eager_closure_s: eager.closure.as_secs_f64(), + eager_reduction_s: reduction.as_secs_f64(), + eager_copy_out_s: eager.copy_out.as_secs_f64(), + eager_internal_scratch_bytes: eager.internal_scratch_bytes, + eager_internal_broadcast_bytes: eager.internal_broadcast_bytes, + evaluator_backend_call_count: eager.backend_call_count, + ..RuntimeProfile::default() + }, + )) + } + + #[allow(clippy::too_many_arguments)] + fn run_resolved_f64_with_color_topology_replay( + &mut self, + common: &mut ExecutionRuntime, + batch: &[Vec<[f64; 4]>], + selected_helicity_ids: Option<&BTreeSet>, + selected_color_ids: Option<&BTreeSet>, + _profiled: bool, + ) -> RusticolResult<(ResolvedValues, RuntimeProfile)> { + if batch.is_empty() { + return Err(RusticolError::invalid_argument( + "eager color topology replay requires at least one point", + )); + } + let total_start = Instant::now(); + let eager_internal_scratch_before = self.direct_internal_scratch_bytes(); + let eager_internal_broadcast_before = self.direct_internal_broadcast_bytes(); + let point_count = batch.len(); + let physics = common.physics.clone().ok_or_else(|| { + RusticolError::artifact( + "eager color topology replay requires resolved physics metadata", + ) + })?; + let mappings = Arc::clone(&common.color_topology_replay_mappings); + let mut replay = self.color_topology_replay.take().ok_or_else(|| { + RusticolError::integrity("eager color topology replay has no amplitude gather runtime") + })?; + let mut expanded_batch = std::mem::take(&mut self.lc_replay_expanded_batch); + let mut seen_labels = std::mem::take(&mut self.lc_replay_seen_labels); + let result = (|| { + replay.begin_color_topology_replay(point_count)?; + let mut source_fill_s = 0.0; + let mut momentum_setup_s = 0.0; + let mut evaluator_s = 0.0; + for mapping_index in 0..mappings.len() { + let evaluation_batch = if mappings[mapping_index].is_empty() { + batch + } else { + let expanded_len = fill_lc_replay_expanded_batch( + &mut expanded_batch, + &mut seen_labels, + batch, + common.external_count, + &[mapping_index], + &mappings, + )?; + &expanded_batch[..expanded_len] + }; + let (source_fill, momentum_setup) = prepare_eager_inputs( + common, + evaluation_batch, + &mut self.initial_values, + &mut self.momenta, + &mut self.source_schedule, + &mut self.source_wavefunction_scratch, + )?; + source_fill_s += profile_duration_seconds(source_fill); + momentum_setup_s += profile_duration_seconds(momentum_setup); + project_model_parameters( + &self.parameter_projection, + &common.model_parameter_values_f64, + &mut self.model_parameters, + )?; + self.amplitudes.resize( + point_count + .checked_mul(self.amplitude_count) + .ok_or_else(|| { + RusticolError::invalid_argument( + "eager color replay amplitudes overflow", + ) + })?, + crate::EagerComplex64::new(0.0, 0.0), + ); + self.reduced.resize(point_count, 0.0); + let evaluator_start = Instant::now(); + self.execute_full_scheduler(point_count)?; + evaluator_s += evaluator_start.elapsed().as_secs_f64(); + replay.gather_color_topology_replay_eager_amplitudes( + &self.amplitudes, + point_count, + mapping_index, + )?; + } + let reduction_start = Instant::now(); + let resolved = replay.reduce_color_topology_replay_f64_resolved( + point_count, + &physics, + common.normalization_factor, + selected_helicity_ids, + selected_color_ids, + )?; + let reduction_s = reduction_start.elapsed().as_secs_f64(); + let eager_internal_scratch_bytes = self + .direct_internal_scratch_bytes() + .saturating_sub(eager_internal_scratch_before); + let eager_internal_broadcast_bytes = self + .direct_internal_broadcast_bytes() + .saturating_sub(eager_internal_broadcast_before); + Ok(( + resolved, + RuntimeProfile { + source_fill_s, + momentum_input_setup_s: momentum_setup_s, + momentum_setup_s, + stage_evaluator_call_s: evaluator_s, + stage_evaluator_s: evaluator_s, + reduction_s, + eager_reduction_s: reduction_s, + eager_internal_scratch_bytes, + eager_internal_broadcast_bytes, + total_s: total_start.elapsed().as_secs_f64(), + evaluator_backend_call_count: u64::try_from(mappings.len()).map_err(|_| { + RusticolError::invalid_argument( + "eager color replay mapping count exceeds u64", + ) + })?, + ..RuntimeProfile::default() + }, + )) + })(); + self.lc_replay_expanded_batch = expanded_batch; + self.lc_replay_seen_labels = seen_labels; + self.color_topology_replay = Some(replay); + result + } + + #[allow(clippy::too_many_arguments)] + fn run_resolved_f64_with_lc_topology_replay( + &mut self, + common: &mut ExecutionRuntime, + batch: &[Vec<[f64; 4]>], + selected_helicity_ids: Option<&BTreeSet>, + selected_color_ids: Option<&BTreeSet>, + profiled: bool, + ) -> RusticolResult<(ResolvedValues, RuntimeProfile)> { + if batch.is_empty() { + return Err(RusticolError::invalid_argument( + "eager evaluation requires at least one point", + )); + } + let physics = common.physics.clone().ok_or_else(|| { + RusticolError::artifact("eager LC topology replay requires resolved physics metadata") + })?; + let replay_plan = if let Some(plan) = &self.lc_resolved_replay_plan { + Arc::clone(plan) + } else { + let materialized_sector_by_id = + common.lc_materialized_sectors_by_id_from_groups(&physics, &self.raw_sum_groups)?; + let plan = Arc::new(physics.lc_resolved_replay_plan( + &common.lc_topology_replay_public_mappings, + &common.lc_topology_replay_routes, + &materialized_sector_by_id, + )?); + self.lc_resolved_replay_plan = Some(Arc::clone(&plan)); + plan + }; + let selection_key = + physics.lc_resolved_replay_selection_key(selected_helicity_ids, selected_color_ids)?; + let selection = if let Some((_, selection)) = self + .lc_resolved_replay_selection_cache + .iter() + .find(|(key, _)| key == &selection_key) + { + Arc::clone(selection) + } else { + let selection = Arc::new( + physics + .select_lc_resolved_replay_plan_for_key(replay_plan.as_ref(), &selection_key)?, + ); + self.lc_resolved_replay_selection_cache + .push((selection_key, Arc::clone(&selection))); + selection + }; + let total_start = Instant::now(); + let point_count = batch.len(); + let component_count = selection.helicity_indices.len() * selection.color_indices.len(); + let mut values = vec![0.0; point_count * component_count]; + // Saved SymJIT applications do not currently guarantee Rust's AArch64 + // callee-saved FP registers. Keep profile state produced by one call + // in memory until all calls have completed instead of retaining f64 + // accumulators across the next kernel invocation. + let mut sector_profiles = Vec::new(); + let mappings = common.lc_topology_replay_mappings.clone(); + let mappings_per_chunk = replay_mappings_per_expanded_batch(point_count); + let mut expanded_batch = std::mem::take(&mut self.lc_replay_expanded_batch); + let mut seen_labels = std::mem::take(&mut self.lc_replay_seen_labels); + let result = (|| { + for source_group in &selection.source_groups { + for chunk_start in + (0..source_group.mapping_indices.len()).step_by(mappings_per_chunk) + { + let chunk_end = usize::min( + chunk_start + mappings_per_chunk, + source_group.mapping_indices.len(), + ); + let mapping_indices = &source_group.mapping_indices[chunk_start..chunk_end]; + let entry_chunk = &source_group.entries[chunk_start..chunk_end]; + let identity_mapping = + mapping_indices.len() == 1 && mappings[mapping_indices[0]].is_empty(); + let evaluation_batch = if identity_mapping { + batch + } else { + let expanded_len = fill_lc_replay_expanded_batch( + &mut expanded_batch, + &mut seen_labels, + batch, + common.external_count, + mapping_indices, + &mappings, + )?; + &expanded_batch[..expanded_len] + }; + let (materialized, sector_profile) = if profiled { + self.run_resolved_f64_profile_materialized( + common, + evaluation_batch, + Some(&source_group.helicity_ids), + Some(&source_group.color_ids), + )? + } else { + self.run_resolved_f64_materialized( + common, + evaluation_batch, + Some(&source_group.helicity_ids), + Some(&source_group.color_ids), + )? + }; + let reduction_start = Instant::now(); + accumulate_selected_lc_replay_resolved_f64( + &mut values, + point_count, + &materialized, + entry_chunk, + source_group.source_component_count, + component_count, + )?; + sector_profiles.push((sector_profile, reduction_start.elapsed())); + } + } + let mut profile = RuntimeProfile::default(); + for (sector_profile, replay_reduction) in §or_profiles { + profile.add_sector(sector_profile); + let replay_reduction_s = profile_duration_seconds(*replay_reduction); + profile.stage_evaluator_call_s += replay_reduction_s; + profile.stage_evaluator_s += replay_reduction_s; + profile.reduction_s += replay_reduction_s; + profile.eager_reduction_s += replay_reduction_s; + } + profile.total_s = total_start.elapsed().as_secs_f64(); + Ok(( + ResolvedValues { + values, + point_count, + helicity_indices: selection.helicity_indices.clone(), + color_indices: selection.color_indices.clone(), + }, + profile, + )) + })(); + self.lc_replay_expanded_batch = expanded_batch; + self.lc_replay_seen_labels = seen_labels; + result + } +} + +fn fill_lc_replay_expanded_batch( + target: &mut Vec>, + seen_labels: &mut Vec, + batch: &[Vec<[f64; 4]>], + expected_legs: usize, + mapping_indices: &[usize], + mappings: &[Vec<(usize, usize)>], +) -> RusticolResult { + let required_rows = batch + .len() + .checked_mul(mapping_indices.len()) + .ok_or_else(|| RusticolError::invalid_argument("LC topology replay batch overflows"))?; + if target.len() < required_rows { + target.resize_with(required_rows, Vec::new); + } + seen_labels.resize(expected_legs, false); + let mut target_row = 0; + for mapping_index in mapping_indices { + let mapping = mappings.get(*mapping_index).ok_or_else(|| { + RusticolError::integrity("LC topology replay references an unknown mapping") + })?; + seen_labels.fill(false); + for (representative_index, sector_index) in mapping { + if *representative_index >= expected_legs || *sector_index >= expected_legs { + return Err(RusticolError::invalid_argument( + "LC topology replay label permutation references an out-of-range external leg", + )); + } + if seen_labels[*representative_index] { + return Err(RusticolError::invalid_argument( + "LC topology replay label permutation contains a duplicate representative label", + )); + } + seen_labels[*representative_index] = true; + } + for point in batch { + if point.len() != expected_legs { + return Err(RusticolError::invalid_argument(format!( + "LC topology replay point has {} external legs, expected {expected_legs}", + point.len(), + ))); + } + let mapped = &mut target[target_row]; + mapped.clear(); + mapped.extend_from_slice(point); + for (representative_index, sector_index) in mapping { + mapped[*representative_index] = point[*sector_index]; + } + target_row += 1; + } + } + Ok(required_rows) +} + +fn sum_eager_resolved(resolved: &ResolvedValues) -> Vec { + let component_count = resolved.helicity_indices.len() * resolved.color_indices.len(); + if component_count == 0 { + return vec![0.0; resolved.point_count]; + } + resolved + .values + .chunks_exact(component_count) + .map(|components| components.iter().sum()) + .collect() +} + +fn fill_selected_eager_group_ids( + groups: &[RawSumGroup], + physics: &PhysicsRuntime, + selected_helicity_ids: Option<&BTreeSet>, + selected_color_ids: Option<&BTreeSet>, + active: &mut Vec, +) -> RusticolResult<()> { + let intersects = |members: &[String], selected: Option<&BTreeSet>| { + selected.is_none_or(|selected| members.iter().any(|id| selected.contains(id))) + }; + active.clear(); + active.try_reserve_exact(groups.len()).map_err(|error| { + RusticolError::invalid_argument(format!( + "could not reserve eager active selector groups: {error}" + )) + })?; + for group in groups { + let reduction = physics + .reduction_by_group_id + .get(&group.id) + .ok_or_else(|| { + RusticolError::integrity(format!( + "resolved eager metadata is missing coherent group {}", + group.id + )) + })?; + if intersects(&reduction.physical_helicity_ids, selected_helicity_ids) + && intersects(&reduction.physical_color_ids, selected_color_ids) + { + active.push(u32::try_from(group.id).map_err(|_| { + RusticolError::integrity(format!( + "eager coherent group {} does not fit the selector-domain ABI", + group.id + )) + })?); + } + } + active.sort_unstable(); + active.dedup(); + Ok(()) +} + +#[allow(dead_code)] // Used by the order-preserving benchmark reference lane. +fn build_eager_selector_members( + groups: &[RawSumGroup], + contracted_color: bool, + physics: &PhysicsRuntime, +) -> RusticolResult>> { + groups + .iter() + .map(|group| { + let reduction = physics + .reduction_by_group_id + .get(&group.id) + .ok_or_else(|| { + RusticolError::integrity(format!( + "resolved eager metadata is missing coherent group {}", + group.id + )) + })?; + if contracted_color { + physics + .normalized_helicity_weights(reduction) + .map(|members| { + members + .into_iter() + .map(|(helicity, weight)| (helicity, 0, weight)) + .collect() + }) + } else { + physics.normalized_member_weights(reduction) + } + }) + .collect() +} + +#[allow(clippy::too_many_arguments)] +#[allow(dead_code)] // Used by the order-preserving benchmark reference lane. +fn fill_eager_group_sets_by_point( + groups: &[RawSumGroup], + selector_members: &[Vec<(usize, usize, f64)>], + physics: &PhysicsRuntime, + point_count: usize, + selected_helicity_ids: Option<&BTreeSet>, + selected_color_ids: Option<&BTreeSet>, + helicity_by_point: Option<&[u32]>, + color_by_point: Option<&[u32]>, + offsets: &mut Vec, + active_groups: &mut Vec, + active_group_weights: &mut Vec, + point_pairs: &mut Vec<(u32, f64)>, + selected_helicity_indices: &mut Vec, + selected_color_indices: &mut Vec, +) -> RusticolResult<()> { + if selector_members.len() != groups.len() { + return Err(RusticolError::integrity( + "eager selector memberships do not match coherent groups", + )); + } + physics.selected_helicity_indices_into(selected_helicity_ids, selected_helicity_indices)?; + physics.selected_color_indices_into(selected_color_ids, selected_color_indices)?; + let helicity_matches = |index: usize, point: usize| { + helicity_by_point + .map(|values| values[point] as usize == index) + .unwrap_or_else(|| selected_helicity_indices.binary_search(&index).is_ok()) + }; + let color_matches = |index: usize, point: usize| { + color_by_point + .map(|values| values[point] as usize == index) + .unwrap_or_else(|| selected_color_indices.binary_search(&index).is_ok()) + }; + + offsets.clear(); + active_groups.clear(); + active_group_weights.clear(); + offsets.try_reserve(point_count + 1).map_err(|error| { + RusticolError::invalid_argument(format!( + "could not reserve eager selector offsets: {error}" + )) + })?; + offsets.push(0); + point_pairs.clear(); + point_pairs.try_reserve(groups.len()).map_err(|error| { + RusticolError::invalid_argument(format!( + "could not reserve eager selector group scratch: {error}" + )) + })?; + for point in 0..point_count { + point_pairs.clear(); + for (group, members) in groups.iter().zip(selector_members) { + let weight = members + .iter() + .filter(|(helicity, color, _)| { + helicity_matches(*helicity, point) && color_matches(*color, point) + }) + .map(|(_, _, weight)| *weight) + .sum::(); + if weight == 0.0 { + continue; + } + let group_id = u32::try_from(group.id).map_err(|_| { + RusticolError::integrity(format!( + "eager coherent group {} does not fit the selector-domain ABI", + group.id + )) + })?; + point_pairs.push((group_id, weight)); + } + point_pairs.sort_unstable_by_key(|(group_id, _)| *group_id); + if point_pairs.windows(2).any(|pair| pair[0].0 == pair[1].0) { + return Err(RusticolError::integrity( + "eager physical selector maps duplicate coherent groups", + )); + } + active_groups.extend(point_pairs.iter().map(|(group_id, _)| *group_id)); + active_group_weights.extend(point_pairs.iter().map(|(_, weight)| *weight)); + offsets.push(active_groups.len()); + } + Ok(()) +} + +fn project_model_parameters( + projection: &EagerParameterProjection, + runtime_parameters: &[f64], + output: &mut Vec, +) -> RusticolResult<()> { + output.resize( + projection.parameter_count, + crate::EagerComplex64::new(0.0, 0.0), + ); + output.fill(crate::EagerComplex64::new(0.0, 0.0)); + for entry in &projection.entries { + let real = runtime_parameters + .get(entry.runtime_real_index) + .copied() + .ok_or_else(|| { + RusticolError::integrity("eager parameter projection has an invalid real slot") + })?; + let imaginary = entry + .runtime_imaginary_index + .map(|index| { + runtime_parameters.get(index).copied().ok_or_else(|| { + RusticolError::integrity( + "eager parameter projection has an invalid imaginary slot", + ) + }) + }) + .transpose()? + .unwrap_or(0.0); + output[entry.prepared_index] = crate::EagerComplex64::new(real, imaginary); + } + Ok(()) +} + +#[allow(clippy::too_many_arguments)] +fn reduce_eager_amplitudes_resolved_into( + amplitudes: &[crate::EagerComplex64], + point_count: usize, + groups: &[RawSumGroup], + contraction: Option<&mut ColorContractionRuntime>, + color_group_scratch: &mut Vec, + physics: &PhysicsRuntime, + normalization_factor: f64, + selected_helicity_ids: Option<&BTreeSet>, + selected_color_ids: Option<&BTreeSet>, + values: &mut Vec, + helicity_indices: &mut Vec, + color_indices: &mut Vec, + helicity_position: &mut Vec>, + color_position: &mut Vec>, + helicity_weights: &mut Vec<(usize, f64)>, + right_helicity_weights: &mut Vec<(usize, f64)>, + member_weights: &mut Vec<(usize, usize, f64)>, +) -> RusticolResult<()> { + physics.selected_helicity_indices_into(selected_helicity_ids, helicity_indices)?; + physics.selected_color_indices_into(selected_color_ids, color_indices)?; + let selected_component_count = helicity_indices + .len() + .checked_mul(color_indices.len()) + .ok_or_else(|| RusticolError::invalid_argument("resolved eager shape overflows"))?; + let value_count = point_count + .checked_mul(selected_component_count) + .ok_or_else(|| RusticolError::invalid_argument("resolved eager shape overflows"))?; + values.resize(value_count, 0.0); + values.fill(0.0); + + let amplitude_count = groups + .iter() + .flat_map(|group| group.indices.iter().copied()) + .max() + .map(|value| value + 1) + .unwrap_or(0); + let expected_amplitudes = point_count + .checked_mul(amplitude_count) + .ok_or_else(|| RusticolError::invalid_argument("eager amplitude shape overflows"))?; + if amplitudes.len() != expected_amplitudes { + return Err(RusticolError::integrity(format!( + "eager amplitude buffer has length {}, expected {expected_amplitudes}", + amplitudes.len(), + ))); + } + + helicity_position.resize(physics.manifest.helicities.len(), None); + helicity_position.fill(None); + for (position, index) in helicity_indices.iter().copied().enumerate() { + let slot = helicity_position.get_mut(index).ok_or_else(|| { + RusticolError::integrity("resolved eager helicity index is out of range") + })?; + *slot = Some(position); + } + color_position.resize(physics.manifest.color_components.len(), None); + color_position.fill(None); + for (position, index) in color_indices.iter().copied().enumerate() { + let slot = color_position.get_mut(index).ok_or_else(|| { + RusticolError::integrity("resolved eager color index is out of range") + })?; + *slot = Some(position); + } + + if let Some(contraction) = contraction { + if !physics.has_contracted_color_axis() + || physics.manifest.color_components.len() != 1 + || contraction.group_count != groups.len() + { + return Err(RusticolError::integrity( + "resolved eager color contraction does not match physics metadata", + )); + } + let Some(selected_color_position) = color_position.first().copied().flatten() else { + return Ok(()); + }; + color_group_scratch.resize( + point_count * groups.len(), + crate::EagerComplex64::new(0.0, 0.0), + ); + for (group_index, group) in groups.iter().enumerate() { + for point in 0..point_count { + let sum = group + .indices + .iter() + .fold(crate::EagerComplex64::new(0.0, 0.0), |sum, index| { + sum + amplitudes[*index * point_count + point] + }); + color_group_scratch[group_index * point_count + point] = sum; + } + } + for entry in contraction.logical_entries() { + let left_group = &groups[entry.left_group_index]; + let right_group = &groups[entry.right_group_index]; + let left_reduction = physics + .reduction_by_group_id + .get(&left_group.id) + .ok_or_else(|| { + RusticolError::integrity(format!( + "resolved eager metadata is missing coherent group {}", + left_group.id + )) + })?; + let right_reduction = physics + .reduction_by_group_id + .get(&right_group.id) + .ok_or_else(|| { + RusticolError::integrity(format!( + "resolved eager metadata is missing coherent group {}", + right_group.id + )) + })?; + if left_reduction.physical_helicity_ids != right_reduction.physical_helicity_ids { + return Err(RusticolError::integrity( + "resolved eager color contraction mixes distinct physical helicities", + )); + } + physics.normalized_helicity_weights_into(left_reduction, helicity_weights)?; + physics.normalized_helicity_weights_into(right_reduction, right_helicity_weights)?; + if helicity_weights.len() != right_helicity_weights.len() + || helicity_weights + .iter() + .zip(right_helicity_weights.iter()) + .any(|((left_index, left_weight), (right_index, right_weight))| { + left_index != right_index || left_weight.to_bits() != right_weight.to_bits() + }) + { + return Err(RusticolError::integrity( + "resolved eager color groups have inconsistent helicity weights", + )); + } + for point in 0..point_count { + let left = color_group_scratch[entry.left_group_index * point_count + point]; + let right = color_group_scratch[entry.right_group_index * point_count + point]; + let product = left * right.conj(); + let contribution = normalization_factor + * entry.symmetry_factor + * (entry.weight_re * product.re - entry.weight_im * product.im); + for (helicity_index, weight) in helicity_weights.iter().copied() { + let Some(selected_helicity_position) = helicity_position[helicity_index] else { + continue; + }; + let index = point * selected_component_count + + selected_helicity_position * color_indices.len() + + selected_color_position; + values[index] += contribution * weight; + } + } + } + } else { + for group in groups { + let reduction = physics + .reduction_by_group_id + .get(&group.id) + .ok_or_else(|| { + RusticolError::integrity(format!( + "resolved eager metadata is missing coherent group {}", + group.id + )) + })?; + physics.normalized_member_weights_into(reduction, member_weights)?; + for point in 0..point_count { + let sum = group + .indices + .iter() + .fold(crate::EagerComplex64::new(0.0, 0.0), |sum, index| { + sum + amplitudes[*index * point_count + point] + }); + let contribution = normalization_factor + * group.all_sector_weight + * (sum.re * sum.re + sum.im * sum.im); + for (helicity_index, color_index, weight) in member_weights.iter().copied() { + let Some(selected_helicity_position) = helicity_position[helicity_index] else { + continue; + }; + let Some(selected_color_position) = color_position[color_index] else { + continue; + }; + let index = point * selected_component_count + + selected_helicity_position * color_indices.len() + + selected_color_position; + values[index] += contribution * weight; + } + } + } + } + Ok(()) +} + +fn accumulate_selected_lc_replay_values_f64( + target: &mut [f64], + point_count: usize, + materialized_values: &[f64], + replay_entries: &[LcResolvedReplayEntry], + source_component_count: usize, + target_component_count: usize, +) -> RusticolResult<()> { + let source_scalar_count = point_count + .checked_mul(replay_entries.len()) + .and_then(|count| count.checked_mul(source_component_count)) + .ok_or_else(|| RusticolError::invalid_argument("LC replay source shape overflows"))?; + if materialized_values.len() != source_scalar_count { + return Err(RusticolError::integrity(format!( + "LC topology replay materialized values have length {}, expected {source_scalar_count}", + materialized_values.len() + ))); + } + let target_scalar_count = point_count + .checked_mul(target_component_count) + .ok_or_else(|| RusticolError::invalid_argument("LC replay target shape overflows"))?; + if target.len() != target_scalar_count { + return Err(RusticolError::integrity(format!( + "LC topology replay target has length {}, expected {target_scalar_count}", + target.len() + ))); + } + for (entry_index, entry) in replay_entries.iter().enumerate() { + for point_index in 0..point_count { + let source_row = (entry_index * point_count + point_index) * source_component_count; + let target_row = point_index * target_component_count; + for route in &entry.routes { + if route.source_index >= source_component_count + || route.target_index >= target_component_count + { + return Err(RusticolError::integrity( + "LC topology replay selected route is out of bounds", + )); + } + target[target_row + route.target_index] += + route.weight * materialized_values[source_row + route.source_index]; + } + } + } + Ok(()) +} + +fn lc_replay_selection_key_matches( + key: &LcResolvedReplaySelectionKey, + physics: &PhysicsRuntime, + selected_helicity_ids: Option<&BTreeSet>, + selected_color_ids: Option<&BTreeSet>, +) -> bool { + let helicities_match = match (&key.helicity_indices, selected_helicity_ids) { + (None, None) => true, + (Some(indices), Some(ids)) if indices.len() == ids.len() => indices.iter().all(|index| { + physics + .manifest + .helicities + .get(*index) + .is_some_and(|helicity| ids.contains(&helicity.id)) + }), + _ => false, + }; + let colors_match = match (&key.color_indices, selected_color_ids) { + (None, None) => true, + (Some(indices), Some(ids)) if indices.len() == ids.len() => indices.iter().all(|index| { + physics + .manifest + .color_components + .get(*index) + .is_some_and(|color| ids.contains(color.id())) + }), + _ => false, + }; + helicities_match && colors_match +} + +fn fill_lc_replay_expanded_flat_from_view( + target: &mut Vec, + batch: F64MomentumBatchView<'_>, + expected_legs: usize, + mapping_indices: &[usize], + mappings: &[Vec<(usize, usize)>], +) -> RusticolResult<()> { + if batch.external_count() != expected_legs { + return Err(RusticolError::invalid_argument(format!( + "LC topology replay input has {} external legs, expected {expected_legs}", + batch.external_count() + ))); + } + let values_per_point = expected_legs + .checked_mul(4) + .ok_or_else(|| RusticolError::invalid_argument("LC topology replay batch overflows"))?; + let row_count = batch + .point_count() + .checked_mul(mapping_indices.len()) + .ok_or_else(|| RusticolError::invalid_argument("LC topology replay batch overflows"))?; + let scalar_count = row_count + .checked_mul(values_per_point) + .ok_or_else(|| RusticolError::invalid_argument("LC topology replay batch overflows"))?; + target.resize(scalar_count, 0.0); + let mut target_row = 0usize; + for mapping_index in mapping_indices { + let mapping = mappings.get(*mapping_index).ok_or_else(|| { + RusticolError::integrity("LC topology replay references an unknown mapping") + })?; + for (position, (representative_index, sector_index)) in mapping.iter().enumerate() { + if *representative_index >= expected_legs || *sector_index >= expected_legs { + return Err(RusticolError::invalid_argument( + "LC topology replay label permutation references an out-of-range external leg", + )); + } + if mapping[..position] + .iter() + .any(|(previous, _)| previous == representative_index) + { + return Err(RusticolError::invalid_argument( + "LC topology replay label permutation contains a duplicate representative label", + )); + } + } + for point_index in 0..batch.point_count() { + let point = batch.point(point_index); + let row_start = target_row * values_per_point; + for external_index in 0..expected_legs { + let momentum = point.momentum(external_index).ok_or_else(|| { + RusticolError::integrity( + "validated momentum view is missing an external leg during topology replay", + ) + })?; + let start = row_start + external_index * 4; + target[start..start + 4].copy_from_slice(&momentum); + } + for (representative_index, sector_index) in mapping { + let momentum = point.momentum(*sector_index).ok_or_else(|| { + RusticolError::integrity( + "validated momentum view is missing a permuted external leg", + ) + })?; + let start = row_start + representative_index * 4; + target[start..start + 4].copy_from_slice(&momentum); + } + target_row += 1; + } + } + Ok(()) +} + +#[allow(clippy::too_many_arguments)] +fn reduce_eager_amplitudes_resolved( + amplitudes: &[crate::EagerComplex64], + point_count: usize, + groups: &[RawSumGroup], + contraction: Option<&mut ColorContractionRuntime>, + color_group_scratch: &mut Vec, + physics: &PhysicsRuntime, + normalization_factor: f64, + selected_helicity_ids: Option<&BTreeSet>, + selected_color_ids: Option<&BTreeSet>, +) -> RusticolResult> { + let helicity_indices = physics.selected_helicity_indices(selected_helicity_ids)?; + let color_indices = physics.selected_color_indices(selected_color_ids)?; + let selected_component_count = helicity_indices + .len() + .checked_mul(color_indices.len()) + .ok_or_else(|| RusticolError::invalid_argument("resolved eager shape overflows"))?; + let mut values = vec![0.0; point_count * selected_component_count]; + let amplitude_count = groups + .iter() + .flat_map(|group| group.indices.iter().copied()) + .max() + .map(|value| value + 1) + .unwrap_or(0); + if amplitudes.len() != point_count * amplitude_count { + return Err(RusticolError::integrity(format!( + "eager amplitude buffer has length {}, expected {}", + amplitudes.len(), + point_count * amplitude_count, + ))); + } + let helicity_position = selected_position_map( + physics.manifest.helicities.len(), + &helicity_indices, + "helicity", + )?; + let color_position = selected_position_map( + physics.manifest.color_components.len(), + &color_indices, + "color", + )?; + + if let Some(contraction) = contraction { + if !physics.has_contracted_color_axis() + || physics.manifest.color_components.len() != 1 + || contraction.group_count != groups.len() + { + return Err(RusticolError::integrity( + "resolved eager color contraction does not match physics metadata", + )); + } + let Some(selected_color_position) = color_position[0] else { + return Ok(ResolvedValues { + values, + point_count, + helicity_indices, + color_indices, + }); + }; + color_group_scratch.resize( + point_count * groups.len(), + crate::EagerComplex64::new(0.0, 0.0), + ); + for (group_index, group) in groups.iter().enumerate() { + for point in 0..point_count { + let sum = group + .indices + .iter() + .fold(crate::EagerComplex64::new(0.0, 0.0), |sum, index| { + sum + amplitudes[*index * point_count + point] + }); + color_group_scratch[group_index * point_count + point] = sum; + } + } + for entry in contraction.logical_entries() { + let left_group = &groups[entry.left_group_index]; + let right_group = &groups[entry.right_group_index]; + let left_reduction = physics + .reduction_by_group_id + .get(&left_group.id) + .ok_or_else(|| { + RusticolError::integrity(format!( + "resolved eager metadata is missing coherent group {}", + left_group.id + )) + })?; + let right_reduction = physics + .reduction_by_group_id + .get(&right_group.id) + .ok_or_else(|| { + RusticolError::integrity(format!( + "resolved eager metadata is missing coherent group {}", + right_group.id + )) + })?; + if left_reduction.physical_helicity_ids != right_reduction.physical_helicity_ids { + return Err(RusticolError::integrity( + "resolved eager color contraction mixes distinct physical helicities", + )); + } + let left_weights = physics.normalized_helicity_weights(left_reduction)?; + let right_weights = physics.normalized_helicity_weights(right_reduction)?; + if left_weights.len() != right_weights.len() + || left_weights.iter().zip(&right_weights).any( + |((left_index, left_weight), (right_index, right_weight))| { + left_index != right_index || left_weight.to_bits() != right_weight.to_bits() + }, + ) + { + return Err(RusticolError::integrity( + "resolved eager color groups have inconsistent helicity weights", + )); + } + let selected_weights = left_weights + .into_iter() + .filter_map(|(index, weight)| { + helicity_position[index].map(|position| (position, weight)) + }) + .collect::>(); + if selected_weights.is_empty() { + continue; + } + for point in 0..point_count { + let left = color_group_scratch[entry.left_group_index * point_count + point]; + let right = color_group_scratch[entry.right_group_index * point_count + point]; + let product = left * right.conj(); + let contribution = normalization_factor + * entry.symmetry_factor + * (entry.weight_re * product.re - entry.weight_im * product.im); + for (helicity_position, weight) in &selected_weights { + let index = point * selected_component_count + + *helicity_position * color_indices.len() + + selected_color_position; + values[index] += contribution * *weight; + } + } + } + } else { + for group in groups { + let reduction = physics + .reduction_by_group_id + .get(&group.id) + .ok_or_else(|| { + RusticolError::integrity(format!( + "resolved eager metadata is missing coherent group {}", + group.id + )) + })?; + let selected_members = physics + .normalized_member_weights(reduction)? + .into_iter() + .filter_map(|(helicity_index, color_index, weight)| { + Some(( + helicity_position[helicity_index]?, + color_position[color_index]?, + weight, + )) + }) + .collect::>(); + if selected_members.is_empty() { + continue; + } + for point in 0..point_count { + let sum = group + .indices + .iter() + .fold(crate::EagerComplex64::new(0.0, 0.0), |sum, index| { + sum + amplitudes[*index * point_count + point] + }); + let contribution = normalization_factor + * group.all_sector_weight + * (sum.re * sum.re + sum.im * sum.im); + for (helicity_position, color_position, weight) in &selected_members { + let index = point * selected_component_count + + *helicity_position * color_indices.len() + + *color_position; + values[index] += contribution * *weight; + } + } + } + } + + Ok(ResolvedValues { + values, + point_count, + helicity_indices, + color_indices, + }) +} + +fn selected_position_map( + physical_count: usize, + selected: &[usize], + kind: &str, +) -> RusticolResult>> { + let mut positions = vec![None; physical_count]; + for (position, index) in selected.iter().copied().enumerate() { + let slot = positions.get_mut(index).ok_or_else(|| { + RusticolError::integrity(format!("resolved eager {kind} index is out of range")) + })?; + *slot = Some(position); + } + Ok(positions) +} + +fn prepare_eager_inputs_view( + common: &mut ExecutionRuntime, + batch: F64MomentumBatchView<'_>, + initial_values: &mut Vec, + momenta: &mut Vec, + source_schedule: &mut Option, + source_wavefunction_scratch: &mut Vec>, +) -> RusticolResult<()> { + if batch.external_count() != common.external_count { + return Err(RusticolError::invalid_argument(format!( + "eager momentum input has {} external legs, expected {}", + batch.external_count(), + common.external_count + ))); + } + let point_count = batch.point_count(); + let value_len = point_count + .checked_mul(common.value_parameter_count) + .ok_or_else(|| RusticolError::invalid_argument("eager value input overflows"))?; + let momentum_len = point_count + .checked_mul(common.momentum_parameter_count) + .ok_or_else(|| RusticolError::invalid_argument("eager momentum input overflows"))?; + initial_values.resize(value_len, crate::EagerComplex64::new(0.0, 0.0)); + momenta.resize(momentum_len, 0.0); + + if source_schedule.is_none() { + *source_schedule = Some(build_eager_source_schedule( + &common.sources, + common.value_parameter_count, + )?); + } + let source_schedule = source_schedule + .as_ref() + .expect("eager source schedule was initialized above"); + source_wavefunction_scratch.resize(source_schedule.maximum_dimension, Complex::new(0.0, 0.0)); + + for point_index in 0..point_count { + let point = batch.point(point_index); + for class in &source_schedule.classes { + let source = &common.sources[class.representative_index]; + let wavefunction = &mut source_wavefunction_scratch[..class.dimension]; + ExecutionRuntime::write_source_wavefunction( + source, + common.external_count, + &common.particle_masses, + &point, + wavefunction, + )?; + for target_start in &class.target_component_starts { + for (component, value) in wavefunction.iter().enumerate() { + initial_values[(target_start + component) * point_count + point_index] = + crate::EagerComplex64::new(value.re, value.im); + } + } + } + } + + for point_index in 0..point_count { + let point = batch.point(point_index); + for slot in &common.momentum_slots { + let start = slot.component_start; + let stop = slot.component_stop; + if stop > common.momentum_parameter_count || stop < start || stop - start != 4 { + return Err(RusticolError::invalid_argument(format!( + "generic momentum slot {} has an invalid component range", + slot.momentum_slot_id + ))); + } + let mut momentum = [0.0; 4]; + for label in &slot.external_labels { + let index = label.checked_sub(1).ok_or_else(|| { + RusticolError::invalid_argument("generic momentum labels are one-based") + })?; + if index >= common.external_count || index >= common.external_is_initial.len() { + return Err(RusticolError::invalid_argument(format!( + "generic momentum slot {} refers to unknown external label {}", + slot.momentum_slot_id, label + ))); + } + let source_momentum = point.momentum(index).ok_or_else(|| { + RusticolError::integrity( + "validated eager momentum view is missing an external leg", + ) + })?; + let sign = if common.external_is_initial[index] { + -1.0 + } else { + 1.0 + }; + for (output, input) in momentum.iter_mut().zip(source_momentum) { + *output += sign * input; + } + } + for (component, value) in momentum.into_iter().enumerate() { + momenta[(start + component) * point_count + point_index] = value; + } + } + } + Ok(()) +} + +fn prepare_eager_inputs( + common: &mut ExecutionRuntime, + batch: &[Vec<[f64; 4]>], + initial_values: &mut Vec, + momenta: &mut Vec, + source_schedule: &mut Option, + source_wavefunction_scratch: &mut Vec>, +) -> RusticolResult<(Duration, Duration)> { + let point_count = batch.len(); + let value_len = point_count + .checked_mul(common.value_parameter_count) + .ok_or_else(|| RusticolError::invalid_argument("eager value input overflows"))?; + let momentum_len = point_count + .checked_mul(common.momentum_parameter_count) + .ok_or_else(|| RusticolError::invalid_argument("eager momentum input overflows"))?; + initial_values.resize(value_len, crate::EagerComplex64::new(0.0, 0.0)); + momenta.resize(momentum_len, 0.0); + + if source_schedule.is_none() { + *source_schedule = Some(build_eager_source_schedule( + &common.sources, + common.value_parameter_count, + )?); + } + let source_schedule = source_schedule + .as_ref() + .expect("eager source schedule was initialized above"); + source_wavefunction_scratch.resize(source_schedule.maximum_dimension, Complex::new(0.0, 0.0)); + + let source_started = Instant::now(); + for (point_index, point) in batch.iter().enumerate() { + for class in &source_schedule.classes { + let source = &common.sources[class.representative_index]; + let wavefunction = &mut source_wavefunction_scratch[..class.dimension]; + ExecutionRuntime::write_source_wavefunction( + source, + common.external_count, + &common.particle_masses, + point, + wavefunction, + )?; + for target_start in &class.target_component_starts { + for (component, value) in wavefunction.iter().enumerate() { + initial_values[(target_start + component) * point_count + point_index] = + crate::EagerComplex64::new(value.re, value.im); + } + } + } + } + let source_fill = source_started.elapsed(); + + let momentum_started = Instant::now(); + for (point_index, point) in batch.iter().enumerate() { + for slot in &common.momentum_slots { + let start = slot.component_start; + let stop = slot.component_stop; + if stop > common.momentum_parameter_count || stop < start || stop - start != 4 { + return Err(RusticolError::invalid_argument(format!( + "generic momentum slot {} has an invalid component range", + slot.momentum_slot_id + ))); + } + let mut momentum = [0.0; 4]; + for label in &slot.external_labels { + let index = label.checked_sub(1).ok_or_else(|| { + RusticolError::invalid_argument("generic momentum labels are one-based") + })?; + if index >= common.external_count || index >= common.external_is_initial.len() { + return Err(RusticolError::invalid_argument(format!( + "generic momentum slot {} refers to unknown external label {}", + slot.momentum_slot_id, label + ))); + } + let sign = if common.external_is_initial[index] { + -1.0 + } else { + 1.0 + }; + for (output, input) in momentum.iter_mut().zip(&point[index]) { + *output += sign * input; + } + } + for (component, value) in momentum.into_iter().enumerate() { + momenta[(start + component) * point_count + point_index] = value; + } + } + } + Ok((source_fill, momentum_started.elapsed())) +} + +fn build_eager_source_schedule( + sources: &[GenericSourceRecordManifest], + value_component_count: usize, +) -> RusticolResult { + let mut class_by_key: BTreeMap = BTreeMap::new(); + let mut classes: Vec = Vec::new(); + let mut maximum_dimension = 0usize; + for (source_index, source) in sources.iter().enumerate() { + let start = source.value_slot.component_start; + let stop = source.value_slot.component_stop; + if stop > value_component_count || stop < start || stop - start != source.dimension { + return Err(RusticolError::artifact(format!( + "eager source {} has an invalid value-slot range", + source.source_id + ))); + } + let key = eager_source_key(source); + if let Some(class_index) = class_by_key.get(&key).copied() { + let class = &mut classes[class_index]; + if class.dimension != source.dimension { + return Err(RusticolError::internal( + "equivalent eager sources have inconsistent dimensions", + )); + } + class.target_component_starts.push(start); + } else { + let class_index = classes.len(); + class_by_key.insert(key, class_index); + classes.push(EagerSourceClass { + representative_index: source_index, + target_component_starts: vec![start], + dimension: source.dimension, + }); + } + maximum_dimension = maximum_dimension.max(source.dimension); + } + Ok(EagerSourceSchedule { + classes, + maximum_dimension, + }) +} + +fn eager_source_key(source: &GenericSourceRecordManifest) -> EagerSourceKey { + EagerSourceKey { + source_kind: source.source_kind.clone(), + leg_label: source.leg_label, + family: match source.source_ir.wavefunction_family { + GenericWavefunctionFamilyManifest::Scalar => 0, + GenericWavefunctionFamilyManifest::Fermion => 1, + GenericWavefunctionFamilyManifest::Vector => 2, + GenericWavefunctionFamilyManifest::Spin2 => 3, + GenericWavefunctionFamilyManifest::Ghost => 4, + GenericWavefunctionFamilyManifest::Auxiliary => 5, + }, + dimension: source.source_ir.component_dimension, + pdg_label: source.source_ir.identity.pdg_label, + anti_pdg_label: source.source_ir.identity.anti_pdg_label, + orientation: match source.source_ir.identity.orientation { + GenericSourceOrientationManifest::Particle => 0, + GenericSourceOrientationManifest::Antiparticle => 1, + GenericSourceOrientationManifest::SelfConjugate => 2, + }, + momentum_transform: match source.applied_crossing.momentum_transform { + GenericMomentumTransformManifest::Identity => 0, + GenericMomentumTransformManifest::NegateFourMomentum => 1, + }, + phase_real: source.applied_crossing.phase[0].to_bits(), + phase_imaginary: source.applied_crossing.phase[1].to_bits(), + source_helicity: source.source_helicity, + chirality: source.chirality, + } +} + +fn validate_eager_profile_accounting( + profile: &crate::eager_runtime::EagerExecutionProfile, +) -> RusticolResult<()> { + let accounted = profile.accounted(); + let tolerance = Duration::from_nanos(1); + if accounted > profile.total.saturating_add(tolerance) { + return Err(RusticolError::internal(format!( + "eager runtime profile phases account for {accounted_s:.9e}s, exceeding the aggregate {total_s:.9e}s: {profile:?}", + accounted_s = accounted.as_secs_f64(), + total_s = profile.total.as_secs_f64(), + ))); + } + Ok(()) +} + +#[inline(always)] +#[cfg(test)] +fn store_complex_row_component_major( + row: &[Complex], + point_index: usize, + point_count: usize, + output: &mut [crate::EagerComplex64], +) { + debug_assert!(point_index < point_count); + debug_assert_eq!(output.len(), row.len() * point_count); + for (component, value) in row.iter().enumerate() { + output[component * point_count + point_index] = + crate::EagerComplex64::new(value.re, value.im); + } +} + +#[inline(always)] +#[cfg(test)] +fn store_real_row_component_major( + row: &[Complex], + point_index: usize, + point_count: usize, + output: &mut [f64], +) -> RusticolResult<()> { + debug_assert!(point_index < point_count); + debug_assert_eq!(output.len(), row.len() * point_count); + for (component, value) in row.iter().enumerate() { + if value.im != 0.0 { + return Err(RusticolError::internal( + "eager momentum setup produced an imaginary component", + )); + } + output[component * point_count + point_index] = value.re; + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn eager_input_rows_are_stored_component_major() { + let point_count = 2; + let mut complex = vec![crate::EagerComplex64::new(0.0, 0.0); 6]; + let mut real = vec![0.0; 6]; + for (point, values) in [ + [ + Complex::new(1.0, -1.0), + Complex::new(2.0, -2.0), + Complex::new(3.0, -3.0), + ], + [ + Complex::new(4.0, -4.0), + Complex::new(5.0, -5.0), + Complex::new(6.0, -6.0), + ], + ] + .iter() + .enumerate() + { + store_complex_row_component_major(values, point, point_count, &mut complex); + let real_values = values.map(|value| Complex::new(value.re, 0.0)); + store_real_row_component_major(&real_values, point, point_count, &mut real).unwrap(); + } + assert_eq!( + complex, + [ + crate::EagerComplex64::new(1.0, -1.0), + crate::EagerComplex64::new(4.0, -4.0), + crate::EagerComplex64::new(2.0, -2.0), + crate::EagerComplex64::new(5.0, -5.0), + crate::EagerComplex64::new(3.0, -3.0), + crate::EagerComplex64::new(6.0, -6.0), + ] + ); + assert_eq!(real, [1.0, 4.0, 2.0, 5.0, 3.0, 6.0]); + } + + #[test] + fn eager_real_input_rejects_imaginary_components() { + let error = store_real_row_component_major(&[Complex::new(1.0, 1.0)], 0, 1, &mut [0.0]) + .unwrap_err(); + assert_eq!(error.kind(), crate::RusticolErrorKind::Internal); + } + + #[test] + fn eager_profile_rejects_impossible_phase_totals() { + let valid = crate::eager_runtime::EagerExecutionProfile { + initialize: Duration::from_millis(2), + gather: Duration::from_millis(3), + kernel_call: Duration::from_millis(4), + total: Duration::from_millis(10), + ..crate::eager_runtime::EagerExecutionProfile::default() + }; + validate_eager_profile_accounting(&valid).unwrap(); + + let impossible = crate::eager_runtime::EagerExecutionProfile { + invocation_scatter: Duration::from_millis(20), + total: Duration::from_millis(10), + ..crate::eager_runtime::EagerExecutionProfile::default() + }; + let error = validate_eager_profile_accounting(&impossible).unwrap_err(); + assert_eq!(error.kind(), crate::RusticolErrorKind::Internal); + assert!(error.to_string().contains("exceeding the aggregate")); + } +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/engine/eager_load.rs b/pyamplicol/rust/crates/rusticol-core/src/engine/eager_load.rs new file mode 100644 index 0000000000..184f41bfed --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/engine/eager_load.rs @@ -0,0 +1,368 @@ +// SPDX-License-Identifier: 0BSD + +use super::*; +use crate::EagerCouplingRow; + +#[derive(Clone, Copy, Debug)] +struct RuntimeLogicalParameterSlots { + real: usize, + imaginary: Option, +} + +pub(super) fn prepare_eager_parameter_state( + pack: &PreparedKernelPackManifest, + active_kernel_ids: &BTreeSet, + runtime_parameters: &[GenericRuntimeModelParameterManifest], + coupling_bytes: &[u8], + payloads: &EvaluatorPayloadStore, +) -> RusticolResult<( + EagerParameterProjection, + Vec, + Option, +)> { + let logical_slots = eager_runtime_logical_parameter_slots(runtime_parameters)?; + let has_derived_parameters = runtime_parameters + .iter() + .any(|parameter| parameter.kind == "derived_parameter_component"); + let canonical_indices = + prepared_parameter_indices(pack, active_kernel_ids, has_derived_parameters)?; + let mut entries = Vec::with_capacity(canonical_indices.len()); + let mut base_parameter_count = 0usize; + for (name, prepared_index) in &canonical_indices { + let slots = logical_slots.get(name).ok_or_else(|| { + RusticolError::integrity(format!( + "prepared parameter {name:?} is absent from the process runtime schema" + )) + })?; + entries.push(EagerParameterProjectionEntry { + prepared_index: *prepared_index, + runtime_real_index: slots.real, + runtime_imaginary_index: slots.imaginary, + }); + base_parameter_count = base_parameter_count.max(prepared_index + 1); + } + + let mut couplings = EagerCouplingRow::decode_table(coupling_bytes)?; + let mut synthetic_by_runtime_index = BTreeMap::new(); + for row in &mut couplings { + for parameter_id in [&mut row.real_parameter_id, &mut row.imag_parameter_id] { + if *parameter_id == crate::MISSING_U32 { + continue; + } + let runtime_index = usize::try_from(*parameter_id).map_err(|_| { + RusticolError::artifact("eager coupling parameter index does not fit usize") + })?; + if runtime_index >= runtime_parameters.len() { + return Err(RusticolError::integrity(format!( + "eager coupling references runtime parameter {runtime_index}, but only {} exist", + runtime_parameters.len() + ))); + } + let prepared_index = if let Some(index) = synthetic_by_runtime_index.get(&runtime_index) + { + *index + } else { + let index = base_parameter_count + .checked_add(synthetic_by_runtime_index.len()) + .ok_or_else(|| { + RusticolError::artifact("eager synthetic coupling parameters overflow") + })?; + synthetic_by_runtime_index.insert(runtime_index, index); + entries.push(EagerParameterProjectionEntry { + prepared_index: index, + runtime_real_index: runtime_index, + runtime_imaginary_index: None, + }); + index + }; + *parameter_id = u32::try_from(prepared_index).map_err(|_| { + RusticolError::artifact("eager synthetic coupling parameter exceeds u32") + })?; + } + } + let parameter_count = base_parameter_count + .checked_add(synthetic_by_runtime_index.len()) + .ok_or_else(|| RusticolError::artifact("eager prepared parameter count overflows"))?; + entries.sort_unstable_by_key(|entry| entry.prepared_index); + let model_parameter_evaluator = load_prepared_model_parameter_evaluator( + pack, + runtime_parameters, + &logical_slots, + payloads, + )?; + Ok(( + EagerParameterProjection { + parameter_count, + entries, + }, + EagerCouplingRow::encode_table(&couplings)?, + model_parameter_evaluator, + )) +} + +fn eager_runtime_logical_parameter_slots( + runtime_parameters: &[GenericRuntimeModelParameterManifest], +) -> RusticolResult> { + let mut direct = BTreeMap::new(); + let mut complex = BTreeMap::, Option)>::new(); + for parameter in runtime_parameters { + if let Some(runtime_name) = ¶meter.runtime_name { + let slots = complex.entry(runtime_name.clone()).or_default(); + let target = match parameter.complex_component.as_deref() { + Some("real") => &mut slots.0, + Some("imag") => &mut slots.1, + other => { + return Err(RusticolError::integrity(format!( + "eager runtime parameter {runtime_name:?} has invalid component {other:?}" + ))); + } + }; + if target.replace(parameter.parameter_index).is_some() { + return Err(RusticolError::integrity(format!( + "eager runtime parameter {runtime_name:?} repeats a complex component" + ))); + } + } else if direct + .insert( + parameter.name.clone(), + RuntimeLogicalParameterSlots { + real: parameter.parameter_index, + imaginary: None, + }, + ) + .is_some() + { + return Err(RusticolError::integrity(format!( + "eager runtime parameter {:?} is duplicated", + parameter.name + ))); + } + } + for (name, (real, imaginary)) in complex { + let real = real.ok_or_else(|| { + RusticolError::integrity(format!( + "eager complex runtime parameter {name:?} lacks a real component" + )) + })?; + if direct + .insert( + name.clone(), + RuntimeLogicalParameterSlots { real, imaginary }, + ) + .is_some() + { + return Err(RusticolError::integrity(format!( + "eager runtime parameter {name:?} has conflicting scalar and complex records" + ))); + } + } + Ok(direct) +} + +pub(super) fn prepared_parameter_indices( + pack: &PreparedKernelPackManifest, + active_kernel_ids: &BTreeSet, + include_derivation_inputs: bool, +) -> RusticolResult> { + // Validate the stable parameter namespace across the complete authenticated + // pack before projecting it to this process. An inactive kernel must not + // weaken pack integrity, but neither may it impose runtime dependencies on + // a process that never invokes it. + let all_indices = all_prepared_parameter_indices(pack)?; + let mut selected_names = BTreeSet::new(); + let mut unresolved_active_ids = active_kernel_ids.clone(); + for kernel in &pack.kernels { + let is_active = active_kernel_ids.contains(&kernel.kernel_id); + if is_active { + unresolved_active_ids.remove(&kernel.kernel_id); + } + if !is_active && (!include_derivation_inputs || kernel.contract_kind != "model-parameter") { + continue; + } + for input in &kernel.input_contracts { + if input.role != "model-parameter" { + continue; + } + let name = input.model_parameter_name.as_ref().ok_or_else(|| { + RusticolError::integrity("prepared model-parameter input lacks its name") + })?; + selected_names.insert(name.clone()); + } + } + if let Some(kernel_id) = unresolved_active_ids.first() { + return Err(RusticolError::integrity(format!( + "active prepared kernel {kernel_id} is absent from the authenticated kernel pack" + ))); + } + Ok(all_indices + .into_iter() + .filter(|(name, _)| selected_names.contains(name)) + .collect()) +} + +fn all_prepared_parameter_indices( + pack: &PreparedKernelPackManifest, +) -> RusticolResult> { + let mut by_name = BTreeMap::new(); + let mut by_index = BTreeMap::new(); + for kernel in &pack.kernels { + for input in &kernel.input_contracts { + if input.role != "model-parameter" { + continue; + } + let name = input.model_parameter_name.as_ref().ok_or_else(|| { + RusticolError::integrity("prepared model-parameter input lacks its name") + })?; + let index = usize::try_from(input.model_parameter_index.ok_or_else(|| { + RusticolError::integrity("prepared model-parameter input lacks its stable index") + })?) + .map_err(|_| RusticolError::artifact("prepared parameter index does not fit usize"))?; + if by_name + .insert(name.clone(), index) + .is_some_and(|old| old != index) + { + return Err(RusticolError::integrity(format!( + "prepared parameter {name:?} has conflicting stable indices" + ))); + } + if by_index + .insert(index, name.clone()) + .is_some_and(|old| old != *name) + { + return Err(RusticolError::integrity(format!( + "prepared parameter index {index} names multiple parameters" + ))); + } + } + } + Ok(by_name) +} + +fn load_prepared_model_parameter_evaluator( + pack: &PreparedKernelPackManifest, + runtime_parameters: &[GenericRuntimeModelParameterManifest], + logical_slots: &BTreeMap, + payloads: &EvaluatorPayloadStore, +) -> RusticolResult> { + let mut kernels = pack + .kernels + .iter() + .filter(|kernel| kernel.contract_kind == "model-parameter"); + let kernel = kernels.next(); + if kernels.next().is_some() { + return Err(RusticolError::integrity( + "prepared kernel pack contains multiple model-parameter kernels", + )); + } + let derived_names = runtime_parameters + .iter() + .filter(|parameter| parameter.kind == "derived_parameter_component") + .map(|parameter| { + parameter + .runtime_name + .clone() + .unwrap_or_else(|| parameter.name.clone()) + }) + .collect::>(); + if derived_names.is_empty() { + return Ok(None); + } + let Some(kernel) = kernel else { + return Err(RusticolError::integrity( + "eager runtime schema contains derived parameters but its prepared pack has no derivation kernel", + )); + }; + + let mut input_parameter_indices = Vec::with_capacity(kernel.input_contracts.len()); + for input in &kernel.input_contracts { + let name = input.model_parameter_name.as_ref().ok_or_else(|| { + RusticolError::integrity("prepared derivation input lacks its parameter name") + })?; + let slots = logical_slots.get(name).ok_or_else(|| { + RusticolError::integrity(format!( + "prepared derivation input {name:?} is absent from the runtime schema" + )) + })?; + if slots.imaginary.is_some() { + return Err(RusticolError::compatibility(format!( + "prepared derivation input {name:?} is complex, but runtime schema v3 derivation inputs are scalar" + ))); + } + input_parameter_indices.push(slots.real); + } + + let output_index_by_name = kernel + .output_layout + .iter() + .enumerate() + .map(|(index, value)| { + let name = value.strip_prefix("model-parameter:").ok_or_else(|| { + RusticolError::integrity(format!( + "prepared derivation output layout {value:?} is invalid" + )) + })?; + Ok((name.to_string(), index)) + }) + .collect::>>()?; + let mut outputs = Vec::with_capacity(derived_names.len()); + for name in derived_names { + let slots = logical_slots.get(&name).ok_or_else(|| { + RusticolError::integrity(format!( + "derived runtime parameter {name:?} has no logical slots" + )) + })?; + let output_index = *output_index_by_name.get(&name).ok_or_else(|| { + RusticolError::integrity(format!( + "prepared derivation kernel does not output runtime parameter {name:?}" + )) + })?; + outputs.push(GenericDerivedParameterOutputManifest { + runtime_name: name, + output_index, + real_parameter_index: slots.real, + imag_parameter_index: slots.imaginary, + }); + } + let evaluator_manifest = kernel.runtime_evaluator_manifest()?; + let evaluator = EvaluatorGroup::load_from_store(&evaluator_manifest, payloads)?; + Ok(Some(ModelParameterEvaluatorRuntime { + input_parameter_indices, + outputs, + evaluator, + })) +} + +pub(super) fn load_prepared_model_parameter_evaluator_for_runtime( + pack: &PreparedKernelPackManifest, + runtime_parameters: &[GenericRuntimeModelParameterManifest], + payloads: &EvaluatorPayloadStore, +) -> RusticolResult> { + let logical_slots = eager_runtime_logical_parameter_slots(runtime_parameters)?; + load_prepared_model_parameter_evaluator(pack, runtime_parameters, &logical_slots, payloads) +} + +pub(super) fn validate_prepared_kernel_references( + artifact: &VerifiedArtifact, + payload_root: &Path, + pack: &PreparedKernelPackManifest, +) -> RusticolResult<()> { + let relative_root = payload_root.strip_prefix(artifact.root()).map_err(|_| { + RusticolError::security("prepared kernel payload root escapes the verified artifact") + })?; + for kernel in &pack.kernels { + validate_evaluator_state_path(artifact, relative_root, &kernel.exact_evaluator_state_path)?; + let evaluator = kernel.runtime_evaluator_manifest()?; + validate_evaluator_reference(artifact, relative_root, &evaluator)?; + for path in kernel.extra_evaluator_payload_paths()? { + validate_evaluator_state_path(artifact, relative_root, path)?; + } + } + for variant in &pack.kernel_variants { + let evaluator = variant.runtime_evaluator_manifest()?; + validate_evaluator_reference(artifact, relative_root, &evaluator)?; + for path in variant.extra_evaluator_payload_paths()? { + validate_evaluator_state_path(artifact, relative_root, path)?; + } + } + Ok(()) +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/engine/eager_manifest.rs b/pyamplicol/rust/crates/rusticol-core/src/engine/eager_manifest.rs new file mode 100644 index 0000000000..9e55eae53d --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/engine/eager_manifest.rs @@ -0,0 +1,2438 @@ +// SPDX-License-Identifier: 0BSD + +use super::evaluator::recurrence_intrinsic_direct::{ + FEYNMAN_VECTOR_PROPAGATOR_TEMPLATE, RecurrenceContributionIntrinsicKind, + WEYL_PROPAGATOR_NEGATIVE_TEMPLATE, WEYL_PROPAGATOR_POSITIVE_TEMPLATE, +}; +use super::*; +use crate::{ + EAGER_HOMOGENEOUS_LINEAR_CURRENT_PROOF, EAGER_INDEPENDENT_BLOCK_SIZE, EAGER_KERNEL_ABI, + EAGER_PLAN_ABI, EAGER_SELECTOR_DOMAINS_ABI, EagerDirectClosureSpec, EagerKernelInput, + EagerKernelRole, EagerKernelSpec, EagerPlanDefinition, EagerPlanDimensions, + EagerReductionEntry, EagerReductionGroup, +}; +use serde_json::value::RawValue; +use sha2::{Digest, Sha256}; +use std::fmt::Write as _; + +pub(super) const EAGER_EXECUTION_KIND: &str = "pyamplicol-runtime-eager-execution"; +const PREPARED_KERNEL_VARIANT_ABI: &str = "pyamplicol-prepared-kernel-variant-v2"; +const PREPARED_INDEPENDENT_BLOCK_VARIANT_ID: &str = "independent-block-4"; +const PREPARED_INDEPENDENT_BLOCK_PROOF: &str = "prepared-kernel-independent-current-block-v1"; +const SYMJIT_APPLICATION_STORAGE_V3_ABI: &str = "symjit-application-storage-v3"; +const PREPARED_JIT_PORTABLE_OPTIMIZATION_LEVEL: u64 = 2; +const PREPARED_JIT_PORTABLE_TARGET: &str = "symjit-storage-v3-portable"; +const RECURRENCE_DIRECT_TEMPLATE_ABI_V1: &str = "pyamplicol-recurrence-direct-template-v1"; +const RECURRENCE_DIRECT_BACKEND_ABI_V1: &str = "rusticol.recurrence-direct-backend.v1"; +const RECURRENCE_DIRECT_CANONICALIZATION_ABI_V1: &str = "pyamplicol-canonical-json-v1"; +const RECURRENCE_DIRECT_PAYLOAD_BINDING_ABI_V2: &str = "pyamplicol-recurrence-plane-binding-v2"; +const SYMJIT_PLANE_APPLICATION_V2_ABI: &str = "pyamplicol-symjit-plane-application-v2"; +const NATIVE_DIRECT_APPLICATION_V1_ABI: &str = "pyamplicol-recurrence-native-direct-library-v1"; + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(super) struct EagerExecutionManifest { + pub(super) schema_version: u32, + pub(super) kind: String, + #[serde(default)] + pub(super) required_runtime_capabilities: Vec, + pub(super) process: String, + pub(super) key: String, + pub(super) color_accuracy: String, + pub(super) external_pdg_order: Vec, + pub(super) eager_plan_abi: String, + pub(super) kernel_pack: EagerKernelPackReferenceManifest, + pub(super) runtime_options: EagerRuntimeOptionsManifest, + #[serde(default)] + pub(super) lc_topology_replay: Option, + pub(super) plan: EagerPlanManifest, + pub(super) dag_summary: ExecutionSummary, + pub(super) runtime_schema: ExecutionPlan, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(super) struct EagerKernelPackReferenceManifest { + pub(super) manifest_path: String, + pub(super) payload_root: String, +} + +#[derive(Clone, Copy, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(super) struct EagerRuntimeOptionsManifest { + pub(super) point_tile_size: usize, + pub(super) workspace_mib: usize, +} + +impl EagerRuntimeOptionsManifest { + pub(super) fn validate(self) -> RusticolResult { + if self.point_tile_size == 0 { + return Err(RusticolError::artifact( + "eager point_tile_size must be positive", + )); + } + if self.workspace_mib == 0 { + return Err(RusticolError::artifact( + "eager workspace_mib must be positive", + )); + } + crate::EagerRuntimeOptions::from_mib(self.point_tile_size, self.workspace_mib) + } +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(super) struct EagerPlanManifest { + pub(super) kind: String, + pub(super) eager_plan_abi: String, + #[serde(default)] + pub(super) required_runtime_capabilities: Vec, + pub(super) process_key: String, + pub(super) couplings: EagerTableManifest, + pub(super) stages: Vec, + pub(super) closures: EagerTableManifest, + #[serde(default)] + pub(super) selector_closures: Option, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(super) struct EagerStageTablesManifest { + pub(super) stage_index: u32, + pub(super) subset_size: usize, + pub(super) invocations: EagerTableManifest, + pub(super) attachments: EagerTableManifest, + pub(super) finalizations: EagerTableManifest, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(super) struct EagerSelectorDomainsManifest { + pub(super) abi: String, + pub(super) domains: EagerTableManifest, + pub(super) domain_group_ids: EagerTableManifest, + pub(super) stages: Vec, + pub(super) closure_domains: EagerTableManifest, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(super) struct EagerSelectorStageManifest { + pub(super) stage_index: u32, + pub(super) invocation_domains: EagerTableManifest, + pub(super) attachment_domains: EagerTableManifest, + pub(super) unpropagated_finalization_domains: EagerTableManifest, + pub(super) propagated_finalization_domains: EagerTableManifest, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(super) struct EagerTableManifest { + pub(super) path: String, + pub(super) count: usize, + pub(super) row_size: usize, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(super) struct PreparedKernelPackManifest { + pub(super) eager_kernel_abi: String, + pub(super) backend: String, + pub(super) optimization_settings: Value, + pub(super) producer: Value, + pub(super) dependency_abis: Value, + pub(super) provenance: Value, + pub(super) target: PreparedKernelTargetManifest, + pub(super) resolver_manifest: Value, + pub(super) kernels: Vec, + #[serde(default)] + pub(super) kernel_variants: Vec, + #[serde(default)] + pub(super) recurrence_template: Option>, + #[serde(default)] + pub(super) recurrence_direct_template: Option>, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(super) struct PreparedKernelTargetManifest { + pub(super) portable: bool, + pub(super) word_bits: u8, + pub(super) endianness: String, + pub(super) target_triple: String, + #[serde(default)] + pub(super) cpu_features: Vec, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(super) struct PreparedKernelManifest { + pub(super) kernel_id: u32, + pub(super) contract_kind: String, + pub(super) canonical_signature: String, + pub(super) input_arity: usize, + pub(super) output_arity: u32, + pub(super) input_layout: Vec, + pub(super) input_contracts: Vec, + pub(super) output_layout: Vec, + pub(super) exact_expressions: Vec, + #[serde(default)] + pub(super) proof_classes: Vec, + pub(super) exact_evaluator_state_path: String, + pub(super) f64_evaluator_manifest: Value, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(super) struct EagerDirectTableManifest { + pub(super) capability: String, + pub(super) source_application_abi: String, + pub(super) descriptor_abi: String, + pub(super) binding_abi: String, + #[serde(default)] + pub(super) descriptor_path: Option, + #[serde(default)] + pub(super) descriptor_size_bytes: Option, + #[serde(default)] + pub(super) descriptor_sha256: Option, + #[serde(default)] + pub(super) library_path: Option, + #[serde(default)] + pub(super) function_name: Option, + #[serde(default)] + pub(super) evaluator_state_sha256: Option, + #[serde(default)] + pub(super) invocation_stride: Option, + #[serde(default)] + pub(super) attachment_stride: Option, + #[serde(default)] + pub(super) simd_lane_width: Option, + #[serde(default)] + pub(super) instruction_count: Option, + #[serde(default)] + pub(super) temporary_count: Option, + pub(super) input_complex_count: u32, + pub(super) output_complex_count: u32, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(super) struct PreparedKernelVariantManifest { + pub(super) variant_id: String, + pub(super) variant_abi: String, + pub(super) kind: String, + pub(super) block_size: u32, + pub(super) lane_layout: String, + pub(super) base_kernel_id: u32, + pub(super) base_canonical_signature: String, + pub(super) base_expression_digest: String, + pub(super) base_input_contract_digest: String, + pub(super) base_output_contract_digest: String, + pub(super) backend: String, + pub(super) optimization_settings_digest: String, + pub(super) input_arity: usize, + pub(super) output_arity: usize, + pub(super) input_lane_stride: usize, + pub(super) output_lane_stride: usize, + pub(super) input_layout: Vec, + pub(super) output_layout: Vec, + pub(super) f64_evaluator_manifest: Value, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(super) struct PreparedKernelInputManifest { + pub(super) role: String, + pub(super) component: u32, + pub(super) symbol: String, + pub(super) model_parameter_name: Option, + pub(super) model_parameter_index: Option, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(super) struct RecurrenceDirectTemplateCatalogManifest { + pub(super) abi: String, + pub(super) backend_abi: String, + pub(super) canonicalization_abi: String, + pub(super) backend: String, + pub(super) target_triple: String, + pub(super) portable: bool, + pub(super) optimization_level: u32, + pub(super) compiled_model_digest: String, + pub(super) recurrence_template_catalog_digest: String, + pub(super) prepared_kernel_pack_digest: String, + pub(super) prepared_kernel_contract_digest: String, + pub(super) prepared_kernel_payload_digest: String, + pub(super) optimization_settings_digest: String, + pub(super) templates: Vec, + pub(super) catalog_digest: String, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(super) struct RecurrenceDirectTemplateManifest { + pub(super) abi: String, + pub(super) template_id: String, + pub(super) direct_executor_id: u32, + pub(super) evaluator_binding_id: u32, + pub(super) evaluator_resolver_key: String, + pub(super) role: String, + pub(super) parent_arity: u32, + pub(super) parent_component_counts: Vec, + pub(super) destination_component_count: u32, + pub(super) momentum_operand_count: u32, + pub(super) destination_operation: String, + pub(super) coupling_slot_count: u32, + pub(super) parameter_slot_count: u32, + pub(super) semantic_template_ids: Vec, + pub(super) exact_expression_digest: String, + pub(super) payload_binding: RecurrenceDirectPayloadBindingManifest, + pub(super) backend: String, + pub(super) target_triple: String, + pub(super) portable: bool, + pub(super) optimization_level: u32, + pub(super) alignment_bytes: u32, + pub(super) simd_axis: String, + pub(super) destination_aliasing: bool, + pub(super) semantic_digest: String, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(super) struct RecurrenceDirectPayloadBindingManifest { + pub(super) abi: String, + pub(super) kind: String, + pub(super) payload_digest: String, + pub(super) prepared_kernel_id: Option, + pub(super) runtime_template: Option, + pub(super) payload_paths: Vec, + pub(super) source_application_path: Option, + pub(super) source_application_sha256: Option, + pub(super) source_application_abi: Option, + pub(super) direct_application_abi: Option, + pub(super) native_entry_point: Option, + pub(super) role: Option, + pub(super) destination_operation: Option, + pub(super) exact_factor_scalar_slots: Vec, + pub(super) state_plane_indices: Vec, + pub(super) parameter_bindings: Vec, + pub(super) input_plane_count: u32, + pub(super) scalar_input_count: u32, + pub(super) output_alias_inputs: Vec, + pub(super) contribution_parent_permutation: Vec, + pub(super) input_plane_projections: Vec, + pub(super) scalar_projections: Vec, + pub(super) intrinsic_contract_digest: Option, + pub(super) prepared_template_semantic_digest: Option, +} + +#[derive(Clone, Copy, Debug, Deserialize)] +#[serde(tag = "kind", rename_all = "kebab-case", deny_unknown_fields)] +pub(super) enum RecurrenceDirectParameterBindingManifest { + Plane { index: u32 }, + Scalar { index: u32 }, +} + +#[derive(Clone, Copy, Debug, Deserialize)] +#[serde(tag = "kind", rename_all = "kebab-case", deny_unknown_fields)] +pub(super) enum RecurrenceDirectPlaneProjectionManifest { + ParentCurrent { + parent: u8, + component: u16, + imaginary: bool, + }, + Momentum { + operand: u8, + lorentz_component: u16, + }, + DestinationCurrent { + component: u16, + imaginary: bool, + }, + DestinationAmplitude { + component: u16, + imaginary: bool, + }, +} + +#[derive(Clone, Copy, Debug, Deserialize)] +#[serde(tag = "kind", rename_all = "kebab-case", deny_unknown_fields)] +pub(super) enum RecurrenceDirectScalarProjectionManifest { + ExactFactor { + imaginary: bool, + }, + Parameter { + index: u32, + imaginary: bool, + }, + Literal { + value: f64, + }, + #[serde(rename = "intrinsic-scale-v1")] + IntrinsicScale { + constant_real_bits: u64, + constant_imag_bits: u64, + parameter_index: Option, + }, +} + +impl EagerExecutionManifest { + pub(super) fn validate_header(&self) -> RusticolResult<()> { + if self.schema_version != PROCESS_ARTIFACT_SCHEMA_VERSION + || self.kind != EAGER_EXECUTION_KIND + { + return Err(RusticolError::compatibility(format!( + "unsupported eager execution kind {:?} schema {}; regenerate the artifact", + self.kind, self.schema_version + ))); + } + if self.eager_plan_abi != EAGER_PLAN_ABI + || self.plan.eager_plan_abi != EAGER_PLAN_ABI + || self.plan.kind != EAGER_EXECUTION_KIND + { + return Err(RusticolError::compatibility(format!( + "unsupported eager plan ABI {:?}", + self.eager_plan_abi + ))); + } + if self.plan.process_key != self.key { + return Err(RusticolError::integrity( + "eager plan process key does not match its execution manifest", + )); + } + if let Some(selector) = &self.plan.selector_closures { + if selector.abi != EAGER_SELECTOR_DOMAINS_ABI { + return Err(RusticolError::compatibility(format!( + "unsupported eager selector-domain ABI {:?}", + selector.abi + ))); + } + if selector.stages.len() != self.plan.stages.len() { + return Err(RusticolError::integrity( + "eager selector domains do not cover every execution stage", + )); + } + for (selector_stage, execution_stage) in selector.stages.iter().zip(&self.plan.stages) { + if selector_stage.stage_index != execution_stage.stage_index { + return Err(RusticolError::integrity( + "eager selector-domain stage index mismatch", + )); + } + } + } + validate_capability_list_match( + &self.required_runtime_capabilities, + &self.plan.required_runtime_capabilities, + "eager execution and plan", + )?; + let expected_capabilities = if self.lc_topology_replay.is_some() { + vec![ + EAGER_DAG_RUNTIME_CAPABILITY.to_string(), + EAGER_LC_TOPOLOGY_REPLAY_RUNTIME_CAPABILITY.to_string(), + ] + } else { + vec![EAGER_DAG_RUNTIME_CAPABILITY.to_string()] + }; + if self.required_runtime_capabilities != expected_capabilities { + return Err(RusticolError::compatibility( + "eager execution runtime capabilities do not match its topology-replay contract", + )); + } + self.runtime_options.validate()?; + Ok(()) + } + + pub(super) fn compiled_metadata_manifest(&self) -> ExecutionManifest { + let materialization_census = + ExecutionMaterializationCensus::from_summary(&self.dag_summary); + ExecutionManifest { + schema_version: self.schema_version, + kind: "pyamplicol-runtime-execution".to_string(), + required_runtime_capabilities: Vec::new(), + process: self.process.clone(), + key: self.key.clone(), + color_accuracy: self.color_accuracy.clone(), + external_pdg_order: self.external_pdg_order.clone(), + compiled: EvaluatorSetManifest { + kind: "eager-runtime-metadata".to_string(), + runtime_available: true, + runtime_unavailable_message: None, + lc_topology_replay: self.lc_topology_replay.clone(), + color_topology_replay: None, + model_parameter_evaluator: None, + stage_evaluators: None, + momentum_slot_ids: None, + }, + dag_summary: self.dag_summary.clone(), + materialization_census, + runtime_schema: self.runtime_schema.clone(), + color_contraction_payload: None, + physics_reduction: None, + helicity_sum_execution: None, + helicity_selector_executions: Vec::new(), + color_selector_executions: Vec::new(), + } + } + + pub(super) fn plan_definition( + &self, + pack: &PreparedKernelPackManifest, + prepared_parameter_count: u32, + ) -> RusticolResult { + let dimensions = EagerPlanDimensions { + value_slot_component_counts: contiguous_value_slot_widths(&self.runtime_schema)?, + momentum_slot_component_counts: contiguous_momentum_slot_widths(&self.runtime_schema)?, + current_component_counts: contiguous_current_slot_widths(&self.runtime_schema)?, + parameter_count: prepared_parameter_count, + amplitude_count: u32::try_from(self.runtime_schema.amplitude_stage.output_count) + .map_err(|_| RusticolError::artifact("eager amplitude count exceeds u32"))?, + }; + let kernels = pack.kernel_specs()?; + let direct_closures = self.direct_closure_specs()?; + let (reduction_groups, reduction_entries) = self.reduction_plan()?; + Ok(EagerPlanDefinition { + dimensions, + kernels, + direct_closures, + reduction_groups, + reduction_entries, + }) + } + + fn direct_closure_specs(&self) -> RusticolResult> { + self.runtime_schema + .amplitude_stage + .roots + .iter() + .enumerate() + .filter(|(_, root)| root.kind == "direct-contraction") + .map(|(index, root)| { + let coefficients = root + .contraction_ir + .coefficients + .iter() + .map(|value| crate::EagerComplex64::new(value[0], value[1])) + .collect(); + Ok(EagerDirectClosureSpec { + closure_index: u32::try_from(index).map_err(|_| { + RusticolError::artifact("eager direct closure index exceeds u32") + })?, + coefficients, + }) + }) + .collect() + } + + fn reduction_plan( + &self, + ) -> RusticolResult<(Vec, Vec)> { + let (groups, contraction) = self.raw_reduction_runtime()?; + let reduction_groups = groups + .iter() + .map(|group| { + Ok(EagerReductionGroup { + coherent_group_id: u32::try_from(group.id).map_err(|_| { + RusticolError::artifact("eager coherent reduction-group ID exceeds u32") + })?, + amplitude_indices: group + .indices + .iter() + .map(|index| { + u32::try_from(*index).map_err(|_| { + RusticolError::artifact( + "eager reduction amplitude index exceeds u32", + ) + }) + }) + .collect::>>()?, + }) + }) + .collect::>>()?; + let mut entries = Vec::new(); + if let Some(contraction) = contraction { + entries + .try_reserve_exact(contraction.logical_entry_count()?) + .map_err(|error| { + RusticolError::artifact(format!( + "cannot reserve eager color-contraction entries: {error}" + )) + })?; + for entry in contraction.logical_entries() { + let coefficient = crate::EagerComplex64::new(entry.weight_re, entry.weight_im) + * entry.symmetry_factor; + entries.push(EagerReductionEntry { + left_group_index: u32::try_from(entry.left_group_index).map_err(|_| { + RusticolError::artifact( + "eager color-contraction left group index exceeds u32", + ) + })?, + right_group_index: u32::try_from(entry.right_group_index).map_err(|_| { + RusticolError::artifact( + "eager color-contraction right group index exceeds u32", + ) + })?, + coefficient, + }); + } + } else { + entries.try_reserve_exact(groups.len()).map_err(|error| { + RusticolError::artifact(format!( + "cannot reserve eager diagonal reduction entries: {error}" + )) + })?; + for (group_index, group) in groups.iter().enumerate() { + let group_index = u32::try_from(group_index) + .map_err(|_| RusticolError::artifact("eager reduction group exceeds u32"))?; + entries.push(EagerReductionEntry { + left_group_index: group_index, + right_group_index: group_index, + coefficient: crate::EagerComplex64::new(group.all_sector_weight, 0.0), + }); + } + } + Ok((reduction_groups, entries)) + } + + pub(super) fn raw_reduction_runtime( + &self, + ) -> RusticolResult<(Vec, Option)> { + let roots = &self.runtime_schema.amplitude_stage.roots; + let output_count = self.runtime_schema.amplitude_stage.output_count; + let weights = roots + .iter() + .map(|root| root.helicity_weight) + .collect::>(); + let all_sector_weights = roots + .iter() + .map(|root| root.all_sector_weight.unwrap_or(root.helicity_weight)) + .collect::>(); + let color_sector_ids = roots + .iter() + .map(|root| root.color_sector_id) + .collect::>(); + let group_ids = roots + .iter() + .map(generic_root_group_id) + .collect::>>()?; + let groups = build_raw_sum_groups( + output_count, + &weights, + &all_sector_weights, + &group_ids, + &color_sector_ids, + )?; + let contraction = build_color_contraction_runtime( + self.runtime_schema + .amplitude_stage + .color_contraction + .as_ref(), + &groups, + )?; + Ok((groups, contraction)) + } +} + +impl PreparedKernelPackManifest { + pub(super) fn validate(&self) -> RusticolResult<()> { + if self.eager_kernel_abi != EAGER_KERNEL_ABI { + return Err(RusticolError::compatibility(format!( + "unsupported eager kernel ABI {:?}", + self.eager_kernel_abi + ))); + } + if !matches!(self.backend.as_str(), "jit" | "asm" | "cpp") { + return Err(RusticolError::artifact(format!( + "unsupported prepared backend {:?}", + self.backend + ))); + } + for (name, value) in [ + ("optimization_settings", &self.optimization_settings), + ("producer", &self.producer), + ("dependency_abis", &self.dependency_abis), + ("provenance", &self.provenance), + ("resolver_manifest", &self.resolver_manifest), + ] { + if value.as_object().is_none_or(|mapping| mapping.is_empty()) { + return Err(RusticolError::artifact(format!( + "prepared kernel pack {name} must be a nonempty object" + ))); + } + } + if self.resolver_manifest.get("abi").and_then(Value::as_str) + != Some("pyamplicol-prepared-kernel-catalog-v1") + { + return Err(RusticolError::compatibility( + "prepared kernel resolver manifest has an unsupported ABI", + )); + } + if self.kernels.is_empty() { + return Err(RusticolError::artifact("prepared kernel pack is empty")); + } + if self.target.word_bits != 64 || self.target.endianness != "little" { + return Err(RusticolError::compatibility( + "prepared eager kernels require a 64-bit little-endian target", + )); + } + if self + .target + .cpu_features + .windows(2) + .any(|pair| pair[0] >= pair[1]) + { + return Err(RusticolError::integrity( + "prepared target CPU features must be sorted and unique", + )); + } + if self.backend == "jit" { + match std::env::consts::ARCH { + "aarch64" | "x86_64" => {} + other => { + return Err(RusticolError::compatibility(format!( + "prepared JIT kernels do not support host architecture {other:?}" + ))); + } + } + if self + .dependency_abis + .get("symjit_application") + .and_then(Value::as_str) + != Some(SYMJIT_APPLICATION_STORAGE_V3_ABI) + { + return Err(RusticolError::compatibility( + "prepared JIT kernels declare an unsupported SymJIT application ABI", + )); + } + if self + .dependency_abis + .get("symjit_plane_application") + .and_then(Value::as_str) + != Some(SYMJIT_PLANE_APPLICATION_V2_ABI) + { + return Err(RusticolError::compatibility( + "prepared JIT kernels declare an unsupported SymJIT plane-application ABI; regenerate the prepared model", + )); + } + if !self.target.portable + || self.target.target_triple != PREPARED_JIT_PORTABLE_TARGET + || !self.target.cpu_features.is_empty() + { + return Err(RusticolError::compatibility(format!( + "prepared JIT kernels target {:?}, expected portable target {:?}", + self.target.target_triple, PREPARED_JIT_PORTABLE_TARGET, + ))); + } + if self + .optimization_settings + .get("jit_optimization_level") + .and_then(Value::as_u64) + != Some(PREPARED_JIT_PORTABLE_OPTIMIZATION_LEVEL) + { + return Err(RusticolError::compatibility(format!( + "prepared JIT kernels must use portable SymJIT optimization level {}", + PREPARED_JIT_PORTABLE_OPTIMIZATION_LEVEL, + ))); + } + } else { + let host = crate::runtime_target_info(); + let host_features = host.cpu_features.into_iter().collect::>(); + if self.target.portable + || self.target.target_triple != host.triple + || self + .target + .cpu_features + .iter() + .any(|feature| !host_features.contains(feature)) + { + return Err(RusticolError::compatibility(format!( + "prepared {} kernels target {:?} with features {:?}, incompatible with host {:?}", + self.backend, self.target.target_triple, self.target.cpu_features, host.triple, + ))); + } + } + let mut ids = BTreeSet::new(); + let mut signatures = BTreeSet::new(); + for kernel in &self.kernels { + if !ids.insert(kernel.kernel_id) { + return Err(RusticolError::integrity(format!( + "prepared kernel pack repeats kernel id {}", + kernel.kernel_id + ))); + } + if kernel.canonical_signature.is_empty() + || !signatures.insert(kernel.canonical_signature.as_str()) + { + return Err(RusticolError::integrity( + "prepared kernel signatures must be nonempty and unique", + )); + } + if kernel.input_arity != kernel.input_contracts.len() + || kernel.input_arity != kernel.input_layout.len() + || usize::try_from(kernel.output_arity).ok() != Some(kernel.output_layout.len()) + || usize::try_from(kernel.output_arity).ok() != Some(kernel.exact_expressions.len()) + { + return Err(RusticolError::integrity(format!( + "prepared kernel {} has inconsistent input/output arities", + kernel.kernel_id + ))); + } + if kernel.exact_evaluator_state_path.is_empty() { + return Err(RusticolError::integrity(format!( + "prepared kernel {} lacks exact evaluator state", + kernel.kernel_id + ))); + } + if kernel + .proof_classes + .windows(2) + .any(|pair| pair[0] >= pair[1]) + { + return Err(RusticolError::integrity(format!( + "prepared kernel {} proof classes must be sorted and unique", + kernel.kernel_id + ))); + } + if kernel + .proof_classes + .iter() + .any(|proof| proof == EAGER_HOMOGENEOUS_LINEAR_CURRENT_PROOF) + && kernel.contract_kind != "propagator" + { + return Err(RusticolError::integrity(format!( + "prepared kernel {} applies a current-linearity proof to {:?}", + kernel.kernel_id, kernel.contract_kind + ))); + } + if kernel + .proof_classes + .iter() + .any(|proof| proof == PREPARED_INDEPENDENT_BLOCK_PROOF) + && (kernel.contract_kind != "vertex" + || kernel.input_contracts.iter().any(|input| { + !matches!(input.role.as_str(), "left-current" | "right-current") + })) + { + return Err(RusticolError::integrity(format!( + "prepared kernel {} applies an independent-block proof to a non-current vertex", + kernel.kernel_id + ))); + } + kernel.validate_evaluator_metadata(self)?; + } + if self.backend != "jit" && !self.kernel_variants.is_empty() { + return Err(RusticolError::integrity( + "prepared C++/ASM packs cannot contain JIT block variants", + )); + } + let kernels_by_id = self + .kernels + .iter() + .map(|kernel| (kernel.kernel_id, kernel)) + .collect::>(); + let mut variant_keys = BTreeSet::new(); + let mut variant_bases = BTreeSet::new(); + for variant in &self.kernel_variants { + if !variant_keys.insert((variant.base_kernel_id, variant.variant_id.as_str())) { + return Err(RusticolError::integrity(format!( + "prepared kernel pack repeats variant {:?} for kernel {}", + variant.variant_id, variant.base_kernel_id + ))); + } + if !variant_bases.insert(variant.base_kernel_id) { + return Err(RusticolError::integrity(format!( + "prepared kernel {} has more than one block variant", + variant.base_kernel_id + ))); + } + let base = kernels_by_id.get(&variant.base_kernel_id).ok_or_else(|| { + RusticolError::integrity(format!( + "prepared variant {:?} references missing kernel {}", + variant.variant_id, variant.base_kernel_id + )) + })?; + variant.validate(self, base)?; + } + Ok(()) + } + + /// Parse and authenticate the optional Direct-Arena recurrence companion. + /// + /// Eager execution deliberately does not require this companion. Recurrence + /// loading calls this method with the digests authenticated by its process + /// manifest and plan before resolving any executable payload. + pub(super) fn recurrence_direct_template_catalog( + &self, + expected_prepared_pack_digest: &str, + expected_catalog_digest: &str, + ) -> RusticolResult { + let raw = self.recurrence_direct_template.as_ref().ok_or_else(|| { + RusticolError::compatibility( + "prepared model has no Direct-Arena recurrence template catalog; recompile the model", + ) + })?; + self.parse_recurrence_direct_template_catalog( + raw.get(), + expected_prepared_pack_digest, + expected_catalog_digest, + ) + } + + pub(super) fn take_recurrence_direct_template_catalog( + &mut self, + expected_prepared_pack_digest: &str, + expected_catalog_digest: &str, + ) -> RusticolResult { + let raw = self.recurrence_direct_template.take().ok_or_else(|| { + RusticolError::compatibility( + "prepared model has no Direct-Arena recurrence template catalog; recompile the model", + ) + })?; + self.parse_recurrence_direct_template_catalog( + raw.get(), + expected_prepared_pack_digest, + expected_catalog_digest, + ) + } + + fn parse_recurrence_direct_template_catalog( + &self, + raw: &str, + expected_prepared_pack_digest: &str, + expected_catalog_digest: &str, + ) -> RusticolResult { + let raw: Value = serde_json::from_str(raw).map_err(|error| { + RusticolError::serialization(format!( + "could not parse prepared Direct-Arena recurrence template catalog: {error}" + )) + })?; + let catalog = + RecurrenceDirectTemplateCatalogManifest::deserialize(&raw).map_err(|error| { + RusticolError::serialization(format!( + "could not parse prepared Direct-Arena recurrence template catalog: {error}" + )) + })?; + catalog.validate( + &raw, + self, + expected_prepared_pack_digest, + expected_catalog_digest, + )?; + Ok(catalog) + } + + pub(super) fn kernel_specs(&self) -> RusticolResult> { + let block_sizes = self + .kernel_variants + .iter() + .map(|variant| (variant.base_kernel_id, variant.block_size)) + .collect::>(); + self.kernels + .iter() + .filter(|kernel| kernel.contract_kind != "model-parameter") + .map(|kernel| { + let role = match kernel.contract_kind.as_str() { + "vertex" => EagerKernelRole::Vertex, + "propagator" => EagerKernelRole::Finalization, + "closure" => EagerKernelRole::Closure, + other => { + return Err(RusticolError::artifact(format!( + "unsupported prepared kernel contract kind {other:?}" + ))); + } + }; + let inputs = kernel + .input_contracts + .iter() + .map(PreparedKernelInputManifest::to_eager_input) + .collect::>>()?; + Ok(EagerKernelSpec { + kernel_id: kernel.kernel_id, + role, + inputs, + output_component_count: kernel.output_arity, + homogeneous_linear_first_current: kernel + .proof_classes + .iter() + .any(|proof| proof == EAGER_HOMOGENEOUS_LINEAR_CURRENT_PROOF), + independent_block_size: block_sizes + .get(&kernel.kernel_id) + .copied() + .unwrap_or(1), + }) + }) + .collect() + } + + pub(super) fn validate_portable_process_artifact_target( + &self, + outer_target: &crate::Target, + context: &str, + ) -> RusticolResult<()> { + if outer_target.triple != crate::artifact::PORTABLE_64LE_ARTIFACT_TARGET { + return Ok(()); + } + let optimization_level = self + .optimization_settings + .get("jit_optimization_level") + .and_then(Value::as_u64); + if !outer_target.cpu_features.is_empty() + || self.backend != "jit" + || !self.target.portable + || self.target.target_triple != PREPARED_JIT_PORTABLE_TARGET + || !self.target.cpu_features.is_empty() + || optimization_level != Some(PREPARED_JIT_PORTABLE_OPTIMIZATION_LEVEL) + { + return Err(RusticolError::compatibility(format!( + "portable-64le {context} artifacts require an authenticated portable O2 SymJIT prepared kernel pack; C++ and ASM packs remain target-specific" + ))); + } + Ok(()) + } +} + +impl RecurrenceDirectTemplateCatalogManifest { + fn validate( + &self, + raw: &Value, + pack: &PreparedKernelPackManifest, + expected_prepared_pack_digest: &str, + expected_catalog_digest: &str, + ) -> RusticolResult<()> { + if self.abi != RECURRENCE_DIRECT_TEMPLATE_ABI_V1 + || self.backend_abi != RECURRENCE_DIRECT_BACKEND_ABI_V1 + || self.canonicalization_abi != RECURRENCE_DIRECT_CANONICALIZATION_ABI_V1 + { + return Err(RusticolError::compatibility( + "prepared Direct-Arena recurrence catalog has an unsupported ABI", + )); + } + validate_sha256_text(&self.catalog_digest, "Direct-Arena catalog digest")?; + validate_sha256_text( + &self.prepared_kernel_pack_digest, + "Direct-Arena prepared-pack digest", + )?; + for (name, digest) in [ + ("compiled-model", self.compiled_model_digest.as_str()), + ( + "recurrence-template catalog", + self.recurrence_template_catalog_digest.as_str(), + ), + ( + "prepared-kernel contract", + self.prepared_kernel_contract_digest.as_str(), + ), + ( + "prepared-kernel payload", + self.prepared_kernel_payload_digest.as_str(), + ), + ( + "optimization-settings", + self.optimization_settings_digest.as_str(), + ), + ] { + validate_sha256_text(digest, &format!("Direct-Arena {name} digest"))?; + } + if self.catalog_digest != expected_catalog_digest + || self.prepared_kernel_pack_digest != expected_prepared_pack_digest + { + return Err(RusticolError::integrity( + "prepared Direct-Arena catalog identity does not match the recurrence artifact", + )); + } + if pack + .provenance + .get("prepared_kernel_pack_digest") + .and_then(Value::as_str) + != Some(expected_prepared_pack_digest) + || pack + .provenance + .get("direct_template_catalog_digest") + .and_then(Value::as_str) + != Some(expected_catalog_digest) + { + return Err(RusticolError::integrity( + "prepared kernel-pack provenance does not authenticate the Direct-Arena catalog", + )); + } + if self.backend != pack.backend + || self.target_triple != pack.target.target_triple + || self.portable != pack.target.portable + { + return Err(RusticolError::integrity( + "Direct-Arena catalog backend or target does not match its prepared kernel pack", + )); + } + if self.backend == "jit" { + if !self.portable + || self.optimization_level != PREPARED_JIT_PORTABLE_OPTIMIZATION_LEVEL as u32 + || self.target_triple != PREPARED_JIT_PORTABLE_TARGET + { + return Err(RusticolError::compatibility( + "Direct-Arena JIT templates require portable SymJIT O2 applications", + )); + } + } else if self.portable { + return Err(RusticolError::compatibility( + "Direct-Arena C++/ASM templates must be target-native", + )); + } + let mut semantic = raw.clone(); + semantic + .as_object_mut() + .ok_or_else(|| RusticolError::artifact("Direct-Arena catalog must be an object"))? + .remove("catalog_digest"); + if canonical_json_digest_exact(&semantic)? != self.catalog_digest { + return Err(RusticolError::integrity( + "Direct-Arena catalog digest does not match its canonical metadata", + )); + } + if self.templates.is_empty() { + return Err(RusticolError::artifact( + "Direct-Arena template catalog is empty", + )); + } + let raw_templates = raw + .get("templates") + .and_then(Value::as_array) + .ok_or_else(|| RusticolError::artifact("Direct-Arena templates must be an array"))?; + if raw_templates.len() != self.templates.len() { + return Err(RusticolError::integrity( + "Direct-Arena template count changed while parsing", + )); + } + let kernels = pack + .kernels + .iter() + .map(|kernel| (kernel.kernel_id, kernel)) + .collect::>(); + let mut template_ids = BTreeSet::new(); + let mut binding_keys = BTreeSet::new(); + for (expected_id, (template, raw_template)) in + self.templates.iter().zip(raw_templates).enumerate() + { + if template.direct_executor_id as usize != expected_id { + return Err(RusticolError::integrity( + "Direct-Arena executor IDs must be dense and zero-based", + )); + } + if !template_ids.insert(template.template_id.as_str()) + || !binding_keys.insert((template.role.as_str(), template.evaluator_binding_id)) + { + return Err(RusticolError::integrity( + "Direct-Arena template IDs and semantic binding keys must be unique", + )); + } + template.validate(raw_template, self, &kernels)?; + } + Ok(()) + } +} + +impl RecurrenceDirectTemplateManifest { + fn validate( + &self, + raw: &Value, + catalog: &RecurrenceDirectTemplateCatalogManifest, + kernels: &BTreeMap, + ) -> RusticolResult<()> { + if self.abi != RECURRENCE_DIRECT_TEMPLATE_ABI_V1 + || self.backend != catalog.backend + || self.target_triple != catalog.target_triple + || self.portable != catalog.portable + || self.optimization_level != catalog.optimization_level + { + return Err(RusticolError::integrity( + "Direct-Arena template policy does not match its catalog", + )); + } + if self.template_id.is_empty() + || self.evaluator_resolver_key.is_empty() + || self.semantic_template_ids.is_empty() + || self.parent_arity as usize != self.parent_component_counts.len() + || self.parent_component_counts.contains(&0) + || self.destination_component_count == 0 + || self.alignment_bytes == 0 + || !self.alignment_bytes.is_power_of_two() + || self.simd_axis != "points-contiguous" + { + return Err(RusticolError::artifact( + "Direct-Arena template has an invalid shape or SIMD contract", + )); + } + if self + .semantic_template_ids + .windows(2) + .any(|pair| pair[0] >= pair[1]) + { + return Err(RusticolError::integrity( + "Direct-Arena semantic template IDs must be sorted and unique", + )); + } + validate_sha256_text( + &self.exact_expression_digest, + "Direct-Arena exact-expression digest", + )?; + validate_sha256_text(&self.semantic_digest, "Direct-Arena template digest")?; + let expected_operation = expected_direct_operation(&self.role)?; + if self.destination_operation != expected_operation + || self.destination_aliasing != (self.role == "finalization") + { + return Err(RusticolError::integrity( + "Direct-Arena destination operation or aliasing does not match its role", + )); + } + self.payload_binding + .validate(self, raw.get("payload_binding"), kernels) + } +} + +impl RecurrenceDirectPayloadBindingManifest { + fn validate( + &self, + template: &RecurrenceDirectTemplateManifest, + raw: Option<&Value>, + kernels: &BTreeMap, + ) -> RusticolResult<()> { + if self.abi != RECURRENCE_DIRECT_PAYLOAD_BINDING_ABI_V2 { + return Err(RusticolError::compatibility(format!( + "Direct-Arena payload binding ABI {:?} is unsupported; regenerate the prepared model with this pyAmpliCol version", + self.abi + ))); + } + validate_sha256_text(&self.payload_digest, "Direct-Arena payload digest")?; + if !matches!( + self.contribution_parent_permutation.as_slice(), + [0, 1] | [1, 0] + ) || (self.contribution_parent_permutation.as_slice() != [0, 1] + && !(self.kind == "rusticol-intrinsic" && template.role == "contribution")) + { + return Err(RusticolError::integrity( + "Direct-Arena payload binding has an invalid parent permutation", + )); + } + match self.kind.as_str() { + "rusticol-intrinsic" => { + let runtime_template = self.runtime_template.as_deref().ok_or_else(|| { + RusticolError::artifact("Direct-Arena intrinsic has no runtime template") + })?; + let prepared_metadata_present = self.prepared_kernel_id.is_some() + || !self.payload_paths.is_empty() + || self.source_application_path.is_some() + || self.source_application_sha256.is_some() + || self.source_application_abi.is_some() + || self.direct_application_abi.is_some() + || self.native_entry_point.is_some() + || !self.exact_factor_scalar_slots.is_empty() + || !self.state_plane_indices.is_empty() + || !self.parameter_bindings.is_empty() + || self.input_plane_count != 0 + || !self.output_alias_inputs.is_empty() + || !self.input_plane_projections.is_empty() + || self.prepared_template_semantic_digest.is_some(); + if prepared_metadata_present { + return Err(RusticolError::integrity( + "Rusticol Direct-Arena intrinsics carry prepared-call metadata", + )); + } + if template.role == "contribution" { + if self.role.as_deref() != Some("contribution") + || self.destination_operation.as_deref() != Some("add") + || self.scalar_input_count != 1 + || self.scalar_projections.len() != 1 + || !matches!( + self.scalar_projections.first(), + Some(RecurrenceDirectScalarProjectionManifest::IntrinsicScale { .. }) + ) + { + return Err(RusticolError::integrity( + "Direct-Arena contribution intrinsic metadata is inconsistent", + )); + } + validate_sha256_text( + self.intrinsic_contract_digest + .as_deref() + .unwrap_or_default(), + "Direct-Arena intrinsic contract digest", + )?; + RecurrenceContributionIntrinsicKind::from_runtime_template(runtime_template)?; + } else if self.role.is_some() + || self.destination_operation.is_some() + || self.scalar_input_count != 0 + || !self.scalar_projections.is_empty() + || self.intrinsic_contract_digest.is_some() + { + return Err(RusticolError::integrity( + "non-contribution Direct-Arena intrinsic carries contribution metadata", + )); + } else if !match template.role.as_str() { + "source" => runtime_template.starts_with("rusticol.source-fill."), + "finalization" => matches!( + runtime_template, + "rusticol.identity-finalize-in-place.v1" + | WEYL_PROPAGATOR_POSITIVE_TEMPLATE + | WEYL_PROPAGATOR_NEGATIVE_TEMPLATE + | FEYNMAN_VECTOR_PROPAGATOR_TEMPLATE + ), + "closure" => runtime_template.starts_with("rusticol.closure-reduce.v1:"), + _ => false, + } { + return Err(RusticolError::compatibility(format!( + "unsupported Direct-Arena intrinsic {runtime_template:?} for role {:?}", + template.role + ))); + } + } + "prepared-direct-call" => { + if self.runtime_template.is_some() + || self.intrinsic_contract_digest.is_some() + || self.role.as_deref() != Some(template.role.as_str()) + || self.destination_operation.as_deref() + != Some(template.destination_operation.as_str()) + || self.exact_factor_scalar_slots != [0, 1] + || self.input_plane_count as usize != self.input_plane_projections.len() + || self.scalar_input_count as usize != self.scalar_projections.len() + { + return Err(RusticolError::integrity( + "prepared Direct-Arena callable metadata is inconsistent", + )); + } + let kernel_id = self.prepared_kernel_id.ok_or_else(|| { + RusticolError::artifact("prepared Direct-Arena callable has no kernel ID") + })?; + let kernel = kernels.get(&kernel_id).ok_or_else(|| { + RusticolError::integrity(format!( + "Direct-Arena callable references absent prepared kernel {kernel_id}" + )) + })?; + let source_path = self.source_application_path.as_deref().ok_or_else(|| { + RusticolError::artifact("Direct-Arena callable has no source application") + })?; + if self.payload_paths.len() != 1 || self.payload_paths[0] != source_path { + return Err(RusticolError::integrity( + "Direct-Arena callable must reference exactly its source application", + )); + } + validate_sha256_text( + self.source_application_sha256 + .as_deref() + .unwrap_or_default(), + "Direct-Arena source application digest", + )?; + validate_sha256_text( + self.prepared_template_semantic_digest + .as_deref() + .unwrap_or_default(), + "Direct-Arena prepared-template digest", + )?; + let evaluator = kernel.f64_evaluator_manifest.as_object().ok_or_else(|| { + RusticolError::artifact("prepared kernel evaluator metadata is not an object") + })?; + match template.backend.as_str() { + "jit" => { + let plane = evaluator + .get("plane_application") + .and_then(Value::as_object) + .ok_or_else(|| { + RusticolError::artifact( + "prepared JIT kernel has no SymJIT plane application", + ) + })?; + if self.direct_application_abi.as_deref() + != Some(SYMJIT_PLANE_APPLICATION_V2_ABI) + || self.source_application_abi.as_deref() + != Some(SYMJIT_PLANE_APPLICATION_V2_ABI) + || self.native_entry_point.is_some() + || plane.get("application_path").and_then(Value::as_str) + != Some(source_path) + || plane.get("application_abi").and_then(Value::as_str) + != self.source_application_abi.as_deref() + || plane.get("storage_abi").and_then(Value::as_str) + != Some(SYMJIT_APPLICATION_STORAGE_V3_ABI) + || plane.get("optimization_level").and_then(Value::as_u64) + != Some(PREPARED_JIT_PORTABLE_OPTIMIZATION_LEVEL) + || !self.state_plane_indices.is_empty() + || !self.parameter_bindings.len().is_multiple_of(2) + || !self.output_alias_inputs.len().is_multiple_of(2) + { + return Err(RusticolError::integrity( + "Direct-Arena callable source does not match its portable O2 SymJIT plane kernel", + )); + } + } + backend @ ("cpp" | "asm") => { + let expected_source_abi = match backend { + "cpp" => SYMBOLICA_COMPILED_CPP_RUNTIME_CAPABILITY, + "asm" => SYMBOLICA_COMPILED_ASM_RUNTIME_CAPABILITY, + _ => unreachable!("native Direct-Arena backend was matched above"), + }; + let expected_entry_point = format!( + "pyamplicol_recurrence_direct_{}_k{kernel_id:08x}_v1", + template.role + ); + if self.direct_application_abi.as_deref() + != Some(NATIVE_DIRECT_APPLICATION_V1_ABI) + || self.source_application_abi.as_deref() != Some(expected_source_abi) + || self.native_entry_point.as_deref() + != Some(expected_entry_point.as_str()) + || evaluator.get("kind").and_then(Value::as_str) + != Some("compiled-complex-evaluator") + || evaluator.get("library_path").and_then(Value::as_str) + != Some(source_path) + { + return Err(RusticolError::integrity( + "Direct-Arena callable source does not match its target-native prepared kernel", + )); + } + } + other => { + return Err(RusticolError::compatibility(format!( + "unsupported Direct-Arena prepared-call backend {other:?}" + ))); + } + } + if self + .state_plane_indices + .iter() + .chain(&self.output_alias_inputs) + .any(|index| *index >= self.input_plane_count) + { + return Err(RusticolError::integrity( + "Direct-Arena callable plane metadata is out of bounds", + )); + } + for binding in &self.parameter_bindings { + let (index, limit) = match *binding { + RecurrenceDirectParameterBindingManifest::Plane { index } => { + (index, self.input_plane_count) + } + RecurrenceDirectParameterBindingManifest::Scalar { index } => { + (index, self.scalar_input_count) + } + }; + if index >= limit { + return Err(RusticolError::integrity( + "Direct-Arena parameter binding is out of bounds", + )); + } + } + for projection in &self.input_plane_projections { + let valid = match *projection { + RecurrenceDirectPlaneProjectionManifest::ParentCurrent { + parent, + component, + .. + } => template + .parent_component_counts + .get(parent as usize) + .is_some_and(|count| u32::from(component) < *count), + RecurrenceDirectPlaneProjectionManifest::Momentum { + operand, + lorentz_component, + } => { + u32::from(operand) < template.momentum_operand_count + && lorentz_component < 4 + } + RecurrenceDirectPlaneProjectionManifest::DestinationCurrent { + component, + .. + } + | RecurrenceDirectPlaneProjectionManifest::DestinationAmplitude { + component, + .. + } => u32::from(component) < template.destination_component_count, + }; + if !valid { + return Err(RusticolError::integrity( + "Direct-Arena input-plane projection is outside its template shape", + )); + } + } + if !matches!( + self.scalar_projections.first(), + Some(RecurrenceDirectScalarProjectionManifest::ExactFactor { + imaginary: false + }) + ) || !matches!( + self.scalar_projections.get(1), + Some(RecurrenceDirectScalarProjectionManifest::ExactFactor { imaginary: true }) + ) { + return Err(RusticolError::integrity( + "Direct-Arena scalar slots 0 and 1 must project the exact complex factor", + )); + } + let _raw = raw.ok_or_else(|| { + RusticolError::artifact("Direct-Arena payload binding is absent") + })?; + } + "pending-direct-call-abi" => { + return Err(RusticolError::compatibility( + "prepared Direct-Arena callable is not executable; recompile with a supported backend", + )); + } + other => { + return Err(RusticolError::compatibility(format!( + "unsupported Direct-Arena payload binding kind {other:?}" + ))); + } + } + Ok(()) + } +} + +fn expected_direct_operation(role: &str) -> RusticolResult<&'static str> { + match role { + "source" => Ok("initialize"), + "contribution" => Ok("add"), + "finalization" => Ok("finalize-in-place"), + "closure" => Ok("closure-add"), + other => Err(RusticolError::compatibility(format!( + "unsupported Direct-Arena executor role {other:?}" + ))), + } +} + +fn validate_sha256_text(value: &str, label: &str) -> RusticolResult<()> { + if value.len() != 64 + || !value + .bytes() + .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte)) + { + return Err(RusticolError::artifact(format!( + "{label} must be a lowercase SHA-256 digest" + ))); + } + Ok(()) +} + +impl PreparedKernelManifest { + pub(super) fn runtime_evaluator_manifest(&self) -> RusticolResult { + let object = self.f64_evaluator_manifest.as_object().ok_or_else(|| { + RusticolError::artifact(format!( + "prepared kernel {} f64 evaluator manifest must be an object", + self.kernel_id + )) + })?; + let kind = object.get("kind").and_then(Value::as_str).ok_or_else(|| { + RusticolError::artifact(format!( + "prepared kernel {} f64 evaluator kind must be a nonempty string", + self.kernel_id + )) + })?; + let metadata_fields: &[&str] = match kind { + "symjit-application-evaluator" => &[ + "backend", + "label", + "settings", + "build_timing", + "direct_table", + ], + "compiled-complex-evaluator" => &[ + "backend", + "settings", + "source_path", + "build_timing", + "direct_table", + ], + other => { + return Err(RusticolError::compatibility(format!( + "prepared kernel {} has unsupported f64 evaluator kind {other:?}", + self.kernel_id + ))); + } + }; + validate_prepared_evaluator_keys(self.kernel_id, kind, object)?; + let mut runtime = object.clone(); + for field in metadata_fields { + runtime.remove(*field); + } + serde_json::from_value(Value::Object(runtime)).map_err(|error| { + RusticolError::serialization(format!( + "prepared kernel {} has invalid runtime evaluator metadata: {error}", + self.kernel_id + )) + }) + } + + pub(super) fn extra_evaluator_payload_paths(&self) -> RusticolResult> { + let object = self.f64_evaluator_manifest.as_object().ok_or_else(|| { + RusticolError::artifact("prepared f64 evaluator manifest must be an object") + })?; + match object.get("kind").and_then(Value::as_str) { + Some("symjit-application-evaluator") => { + let Some(direct) = object.get("direct_table") else { + return Ok(Vec::new()); + }; + let direct = direct.as_object().ok_or_else(|| { + RusticolError::artifact(format!( + "prepared kernel {} DirectTable metadata must be an object", + self.kernel_id + )) + })?; + if let Some(path) = direct.get("descriptor_path") { + Ok(vec![path.as_str().ok_or_else(|| { + RusticolError::artifact(format!( + "prepared kernel {} DirectTable descriptor path must be text", + self.kernel_id + )) + })?]) + } else if let Some(path) = direct.get("library_path") { + Ok(vec![path.as_str().ok_or_else(|| { + RusticolError::artifact(format!( + "prepared kernel {} DirectTable library path must be text", + self.kernel_id + )) + })?]) + } else { + Err(RusticolError::artifact(format!( + "prepared kernel {} DirectTable has no payload path", + self.kernel_id + ))) + } + } + Some("compiled-complex-evaluator") => { + let direct = object.get("direct_table").and_then(Value::as_object); + let mut paths = vec![required_nonempty_string( + object, + "source_path", + self.kernel_id, + )?]; + if let Some(direct) = direct { + paths.push(required_nonempty_string( + direct, + "library_path", + self.kernel_id, + )?); + } + Ok(paths) + } + _ => Err(RusticolError::compatibility(format!( + "prepared kernel {} has an unsupported f64 evaluator kind", + self.kernel_id + ))), + } + } + + pub(super) fn eager_direct_table_manifest(&self) -> RusticolResult { + let object = self.f64_evaluator_manifest.as_object().ok_or_else(|| { + RusticolError::artifact(format!( + "prepared kernel {} f64 evaluator manifest must be an object", + self.kernel_id + )) + })?; + let raw = object.get("direct_table").ok_or_else(|| { + RusticolError::compatibility(format!( + "prepared eager kernel {} predates {:?}; regenerate the artifact with the \ + current `pyamplicol generate`", + self.kernel_id, + crate::eager_layout::EAGER_DIRECT_ARENA_RUNTIME_CAPABILITY, + )) + })?; + let direct: EagerDirectTableManifest = + serde_json::from_value(raw.clone()).map_err(|error| { + RusticolError::serialization(format!( + "prepared eager kernel {} has invalid DirectTable metadata: {error}", + self.kernel_id + )) + })?; + if direct.capability != crate::eager_layout::EAGER_DIRECT_ARENA_RUNTIME_CAPABILITY + || !matches!( + direct.source_application_abi.as_str(), + crate::eager_layout::EAGER_DIRECT_SOURCE_APPLICATION_ABI + | crate::eager_layout::EAGER_NATIVE_DIRECT_TABLE_APPLICATION_ABI + ) + || direct.descriptor_abi != crate::eager_layout::EAGER_DIRECT_TABLE_DESCRIPTOR_ABI + || direct.binding_abi != crate::eager_layout::EAGER_DIRECT_TABLE_BINDING_ABI + { + return Err(RusticolError::compatibility(format!( + "prepared eager kernel {} has an unsupported DirectTable ABI contract; \ + regenerate the artifact with the current `pyamplicol generate`", + self.kernel_id + ))); + } + match direct.source_application_abi.as_str() { + crate::eager_layout::EAGER_DIRECT_SOURCE_APPLICATION_ABI => { + if direct.descriptor_path.as_deref().is_none_or(str::is_empty) { + return Err(RusticolError::artifact(format!( + "prepared eager kernel {} DirectTable descriptor path is empty", + self.kernel_id + ))); + } + let size = direct.descriptor_size_bytes.unwrap_or(0); + if size == 0 { + return Err(RusticolError::artifact(format!( + "prepared eager kernel {} DirectTable descriptor must not be empty", + self.kernel_id + ))); + } + validate_sha256_text( + direct.descriptor_sha256.as_deref().unwrap_or_default(), + "eager DirectTable descriptor digest", + )?; + } + crate::eager_layout::EAGER_NATIVE_DIRECT_TABLE_APPLICATION_ABI => { + if direct.library_path.as_deref().is_none_or(str::is_empty) + || direct.function_name.as_deref().is_none_or(str::is_empty) + || direct.invocation_stride.unwrap_or(0) == 0 + || direct.attachment_stride.unwrap_or(0) == 0 + || !matches!(direct.simd_lane_width, Some(2 | 4)) + { + return Err(RusticolError::artifact(format!( + "prepared eager kernel {} native DirectTable metadata is incomplete", + self.kernel_id + ))); + } + validate_sha256_text( + direct.evaluator_state_sha256.as_deref().unwrap_or_default(), + "eager native DirectTable evaluator-state digest", + )?; + } + _ => unreachable!("source ABI admitted above"), + } + if usize::try_from(direct.input_complex_count).ok() != Some(self.input_arity) + || direct.output_complex_count != self.output_arity + { + return Err(RusticolError::integrity(format!( + "prepared eager kernel {} DirectTable I/O ({}, {}) does not match ({}, {})", + self.kernel_id, + direct.input_complex_count, + direct.output_complex_count, + self.input_arity, + self.output_arity, + ))); + } + Ok(direct) + } + + fn validate_evaluator_metadata(&self, pack: &PreparedKernelPackManifest) -> RusticolResult<()> { + let object = self.f64_evaluator_manifest.as_object().ok_or_else(|| { + RusticolError::artifact(format!( + "prepared kernel {} f64 evaluator manifest must be an object", + self.kernel_id + )) + })?; + let kind = required_nonempty_string(object, "kind", self.kernel_id)?; + let backend = required_nonempty_string(object, "backend", self.kernel_id)?; + let (expected_kind, expected_backend, expected_capability) = match pack.backend.as_str() { + "jit" => ( + "symjit-application-evaluator", + "jit", + SYMJIT_APPLICATION_RUNTIME_CAPABILITY, + ), + "asm" => ( + "compiled-complex-evaluator", + "compiled-complex", + SYMBOLICA_COMPILED_ASM_RUNTIME_CAPABILITY, + ), + "cpp" => ( + "compiled-complex-evaluator", + "compiled-complex", + SYMBOLICA_COMPILED_CPP_RUNTIME_CAPABILITY, + ), + _ => unreachable!("prepared pack backend validated before its kernels"), + }; + if kind != expected_kind || backend != expected_backend { + return Err(RusticolError::integrity(format!( + "prepared kernel {} evaluator ({kind:?}, {backend:?}) does not match pack backend {:?}, expected ({expected_kind:?}, {expected_backend:?})", + self.kernel_id, pack.backend, + ))); + } + let settings = object.get("settings").ok_or_else(|| { + RusticolError::artifact(format!( + "prepared kernel {} evaluator lacks settings metadata", + self.kernel_id + )) + })?; + if settings != &pack.optimization_settings { + return Err(RusticolError::integrity(format!( + "prepared kernel {} evaluator settings do not match its pack", + self.kernel_id + ))); + } + let build_timing = object + .get("build_timing") + .and_then(Value::as_object) + .ok_or_else(|| { + RusticolError::artifact(format!( + "prepared kernel {} evaluator build_timing must be an object", + self.kernel_id + )) + })?; + if build_timing.values().any(|value| { + value + .as_f64() + .is_none_or(|seconds| !seconds.is_finite() || seconds < 0.0) + }) { + return Err(RusticolError::artifact(format!( + "prepared kernel {} evaluator build timings must be finite nonnegative numbers", + self.kernel_id + ))); + } + if kind == "symjit-application-evaluator" { + validate_prepared_symjit_plane_application( + self.kernel_id, + object, + self.input_arity, + usize::try_from(self.output_arity).unwrap_or(usize::MAX), + PREPARED_JIT_PORTABLE_OPTIMIZATION_LEVEL as u8, + )?; + } + let runtime = self.runtime_evaluator_manifest()?; + let capabilities = evaluator_runtime_capabilities(&runtime)?; + if capabilities != BTreeSet::from([expected_capability.to_string()]) { + return Err(RusticolError::integrity(format!( + "prepared kernel {} pack backend {:?} declares evaluator capabilities {capabilities:?}, expected {expected_capability:?}", + self.kernel_id, pack.backend, + ))); + } + let (input_len, output_len) = runtime.io_len()?; + if input_len != self.input_arity + || output_len != usize::try_from(self.output_arity).unwrap_or(usize::MAX) + { + return Err(RusticolError::integrity(format!( + "prepared kernel {} evaluator I/O ({input_len}, {output_len}) does not match ({}, {})", + self.kernel_id, self.input_arity, self.output_arity + ))); + } + match kind { + "symjit-application-evaluator" => { + if pack.backend != "jit" { + return Err(RusticolError::integrity(format!( + "prepared kernel {} uses SymJIT under backend {:?}", + self.kernel_id, pack.backend + ))); + } + if required_nonempty_string(object, "label", self.kernel_id)?.is_empty() { + return Err(RusticolError::artifact("prepared evaluator label is empty")); + } + let application_abi = + required_nonempty_string(object, "application_abi", self.kernel_id)?; + if pack + .dependency_abis + .get("symjit_application") + .and_then(Value::as_str) + != Some(application_abi) + { + return Err(RusticolError::compatibility(format!( + "prepared kernel {} SymJIT application ABI does not match its pack", + self.kernel_id + ))); + } + if object.get("optimization_level").and_then(Value::as_u64) + != Some(PREPARED_JIT_PORTABLE_OPTIMIZATION_LEVEL) + { + return Err(RusticolError::compatibility(format!( + "prepared kernel {} must use portable SymJIT optimization level {}", + self.kernel_id, PREPARED_JIT_PORTABLE_OPTIMIZATION_LEVEL, + ))); + } + if object.contains_key("direct_table") { + let direct = self.eager_direct_table_manifest()?; + let plane = object + .get("plane_application") + .and_then(Value::as_object) + .ok_or_else(|| { + RusticolError::compatibility(format!( + "prepared eager JIT kernel {} predates the SymJIT plane-application ABI; regenerate the prepared model", + self.kernel_id + )) + })?; + if direct.source_application_abi + != crate::eager_layout::EAGER_DIRECT_SOURCE_APPLICATION_ABI + || plane.get("application_abi").and_then(Value::as_str) + != Some(crate::eager_layout::EAGER_DIRECT_SOURCE_APPLICATION_ABI) + || plane.get("storage_abi").and_then(Value::as_str) + != Some(SYMJIT_APPLICATION_STORAGE_V3_ABI) + || plane + .get("application_path") + .and_then(Value::as_str) + .is_none_or(str::is_empty) + || plane.get("input_complex_count").and_then(Value::as_u64) + != u64::try_from(self.input_arity).ok() + || plane.get("output_complex_count").and_then(Value::as_u64) + != Some(u64::from(self.output_arity)) + || plane.get("optimization_level").and_then(Value::as_u64) + != Some(PREPARED_JIT_PORTABLE_OPTIMIZATION_LEVEL) + { + return Err(RusticolError::compatibility(format!( + "prepared eager JIT kernel {} has an incompatible SymJIT plane application", + self.kernel_id + ))); + } + } + } + "compiled-complex-evaluator" => { + if !matches!(pack.backend.as_str(), "asm" | "cpp") { + return Err(RusticolError::integrity(format!( + "prepared kernel {} uses a compiled evaluator under backend {:?}", + self.kernel_id, pack.backend + ))); + } + required_nonempty_string(object, "source_path", self.kernel_id)?; + let direct = self.eager_direct_table_manifest()?; + if direct.source_application_abi + != crate::eager_layout::EAGER_NATIVE_DIRECT_TABLE_APPLICATION_ABI + { + return Err(RusticolError::compatibility(format!( + "prepared native kernel {} does not provide a native eager DirectTable", + self.kernel_id + ))); + } + } + _ => unreachable!("runtime evaluator projection validated the kind"), + } + if object.get("evaluator_state_path").and_then(Value::as_str) + != Some(self.exact_evaluator_state_path.as_str()) + { + return Err(RusticolError::integrity(format!( + "prepared kernel {} exact evaluator state does not match its f64 evaluator fallback", + self.kernel_id + ))); + } + Ok(()) + } +} + +impl PreparedKernelVariantManifest { + pub(super) fn runtime_evaluator_manifest(&self) -> RusticolResult { + let object = self.f64_evaluator_manifest.as_object().ok_or_else(|| { + RusticolError::artifact(format!( + "prepared kernel {} variant {:?} evaluator manifest must be an object", + self.base_kernel_id, self.variant_id + )) + })?; + let kind = object.get("kind").and_then(Value::as_str).ok_or_else(|| { + RusticolError::artifact(format!( + "prepared kernel {} variant {:?} evaluator kind must be a nonempty string", + self.base_kernel_id, self.variant_id + )) + })?; + if kind != "symjit-application-evaluator" { + return Err(RusticolError::compatibility(format!( + "prepared kernel {} variant {:?} has unsupported evaluator kind {kind:?}", + self.base_kernel_id, self.variant_id + ))); + } + validate_prepared_evaluator_keys(self.base_kernel_id, kind, object)?; + let mut runtime = object.clone(); + for field in ["backend", "label", "settings", "build_timing"] { + runtime.remove(field); + } + serde_json::from_value(Value::Object(runtime)).map_err(|error| { + RusticolError::serialization(format!( + "prepared kernel {} variant {:?} has invalid runtime evaluator metadata: {error}", + self.base_kernel_id, self.variant_id + )) + }) + } + + pub(super) fn extra_evaluator_payload_paths(&self) -> RusticolResult> { + let object = self.f64_evaluator_manifest.as_object().ok_or_else(|| { + RusticolError::artifact("prepared block evaluator manifest must be an object") + })?; + if object.get("kind").and_then(Value::as_str) != Some("symjit-application-evaluator") { + return Err(RusticolError::compatibility(format!( + "prepared kernel {} variant {:?} has an unsupported evaluator kind", + self.base_kernel_id, self.variant_id + ))); + } + Ok(Vec::new()) + } + + fn validate( + &self, + pack: &PreparedKernelPackManifest, + base: &PreparedKernelManifest, + ) -> RusticolResult<()> { + if self.variant_abi != PREPARED_KERNEL_VARIANT_ABI + || self.variant_id != PREPARED_INDEPENDENT_BLOCK_VARIANT_ID + || self.kind != "independent-block" + || self.block_size != EAGER_INDEPENDENT_BLOCK_SIZE + || self.lane_layout != "lane-major" + { + return Err(RusticolError::compatibility(format!( + "prepared kernel {} has unsupported block variant metadata; \ + regenerate the artifact with the current `pyamplicol generate`", + self.base_kernel_id + ))); + } + if pack.backend != "jit" || self.backend != pack.backend { + return Err(RusticolError::integrity(format!( + "prepared kernel {} block variant backend does not match its JIT pack", + self.base_kernel_id + ))); + } + if base.contract_kind != "vertex" + || !base + .proof_classes + .iter() + .any(|proof| proof == PREPARED_INDEPENDENT_BLOCK_PROOF) + || base + .input_contracts + .iter() + .any(|input| !matches!(input.role.as_str(), "left-current" | "right-current")) + { + return Err(RusticolError::integrity(format!( + "prepared kernel {} block variant lacks its current-only vertex proof", + self.base_kernel_id + ))); + } + if self.base_canonical_signature != base.canonical_signature + || self.input_lane_stride != base.input_arity + || self.output_lane_stride != usize::try_from(base.output_arity).unwrap_or(usize::MAX) + || self.input_arity != self.input_lane_stride * self.block_size as usize + || self.output_arity != self.output_lane_stride * self.block_size as usize + { + return Err(RusticolError::integrity(format!( + "prepared kernel {} block variant does not match its scalar arities", + self.base_kernel_id + ))); + } + let expected_input_layout = (0..self.block_size) + .flat_map(|lane| { + base.input_layout + .iter() + .map(move |item| format!("lane:{lane}:{item}")) + }) + .collect::>(); + let expected_output_layout = (0..self.block_size) + .flat_map(|lane| { + base.output_layout + .iter() + .map(move |item| format!("lane:{lane}:{item}")) + }) + .collect::>(); + if self.input_layout != expected_input_layout + || self.output_layout != expected_output_layout + { + return Err(RusticolError::integrity(format!( + "prepared kernel {} block variant has an incompatible lane layout", + self.base_kernel_id + ))); + } + let input_contracts = base + .input_contracts + .iter() + .map(|input| { + serde_json::json!({ + "role": input.role, + "component": input.component, + "symbol": input.symbol, + "model_parameter_name": input.model_parameter_name, + "model_parameter_index": input.model_parameter_index, + }) + }) + .collect::>(); + let expected_expression_digest = canonical_json_digest(&serde_json::json!({ + "exact_expressions": base.exact_expressions, + }))?; + let expected_input_digest = canonical_json_digest(&serde_json::json!({ + "input_arity": base.input_arity, + "input_layout": base.input_layout, + "input_contracts": input_contracts, + }))?; + let expected_output_digest = canonical_json_digest(&serde_json::json!({ + "output_arity": base.output_arity, + "output_layout": base.output_layout, + }))?; + let expected_optimization_digest = canonical_json_digest(&pack.optimization_settings)?; + if self.base_expression_digest != expected_expression_digest + || self.base_input_contract_digest != expected_input_digest + || self.base_output_contract_digest != expected_output_digest + || self.optimization_settings_digest != expected_optimization_digest + { + return Err(RusticolError::integrity(format!( + "prepared kernel {} block variant digest does not match its scalar contract", + self.base_kernel_id + ))); + } + + let object = self.f64_evaluator_manifest.as_object().ok_or_else(|| { + RusticolError::artifact("prepared block evaluator manifest must be an object") + })?; + if required_nonempty_string(object, "kind", self.base_kernel_id)? + != "symjit-application-evaluator" + || required_nonempty_string(object, "backend", self.base_kernel_id)? != "jit" + || object.get("settings") != Some(&pack.optimization_settings) + { + return Err(RusticolError::integrity(format!( + "prepared kernel {} block evaluator does not match its JIT pack", + self.base_kernel_id + ))); + } + if object.get("optimization_level").and_then(Value::as_u64) + != Some(PREPARED_JIT_PORTABLE_OPTIMIZATION_LEVEL) + { + return Err(RusticolError::compatibility(format!( + "prepared kernel {} block evaluator must use portable SymJIT optimization level {}", + self.base_kernel_id, PREPARED_JIT_PORTABLE_OPTIMIZATION_LEVEL, + ))); + } + let build_timing = object + .get("build_timing") + .and_then(Value::as_object) + .ok_or_else(|| { + RusticolError::artifact("prepared block build_timing must be an object") + })?; + if build_timing.values().any(|value| { + value + .as_f64() + .is_none_or(|seconds| !seconds.is_finite() || seconds < 0.0) + }) { + return Err(RusticolError::artifact( + "prepared block build timings must be finite nonnegative numbers", + )); + } + required_nonempty_string(object, "label", self.base_kernel_id)?; + required_nonempty_string(object, "evaluator_state_path", self.base_kernel_id)?; + validate_prepared_symjit_plane_application( + self.base_kernel_id, + object, + self.input_arity, + self.output_arity, + PREPARED_JIT_PORTABLE_OPTIMIZATION_LEVEL as u8, + )?; + let application_abi = + required_nonempty_string(object, "application_abi", self.base_kernel_id)?; + if pack + .dependency_abis + .get("symjit_application") + .and_then(Value::as_str) + != Some(application_abi) + { + return Err(RusticolError::compatibility(format!( + "prepared kernel {} block evaluator SymJIT ABI does not match its pack", + self.base_kernel_id + ))); + } + let runtime = self.runtime_evaluator_manifest()?; + if evaluator_runtime_capabilities(&runtime)? + != BTreeSet::from([SYMJIT_APPLICATION_RUNTIME_CAPABILITY.to_string()]) + { + return Err(RusticolError::integrity(format!( + "prepared kernel {} block evaluator has incompatible capabilities", + self.base_kernel_id + ))); + } + let (input_len, output_len) = runtime.io_len()?; + if input_len != self.input_arity || output_len != self.output_arity { + return Err(RusticolError::integrity(format!( + "prepared kernel {} block evaluator I/O ({input_len}, {output_len}) does not match ({}, {})", + self.base_kernel_id, self.input_arity, self.output_arity + ))); + } + Ok(()) + } +} + +fn canonical_json_digest(value: &Value) -> RusticolResult { + let mut canonical = String::new(); + write_canonical_json(value, &mut canonical)?; + canonical.push('\n'); + Ok(format!("{:x}", Sha256::digest(canonical.as_bytes()))) +} + +fn canonical_json_digest_exact(value: &Value) -> RusticolResult { + let mut canonical = String::new(); + write_canonical_json(value, &mut canonical)?; + Ok(format!("{:x}", Sha256::digest(canonical.as_bytes()))) +} + +fn write_canonical_json(value: &Value, output: &mut String) -> RusticolResult<()> { + match value { + Value::Null => output.push_str("null"), + Value::Bool(value) => output.push_str(if *value { "true" } else { "false" }), + Value::Number(value) => output.push_str(&python_json_number(value)), + Value::String(value) => write_ascii_json_string(value, output), + Value::Array(values) => { + output.push('['); + for (index, value) in values.iter().enumerate() { + if index != 0 { + output.push(','); + } + write_canonical_json(value, output)?; + } + output.push(']'); + } + Value::Object(values) => { + output.push('{'); + let mut keys = values.keys().collect::>(); + keys.sort_unstable(); + for (index, key) in keys.iter().enumerate() { + if index != 0 { + output.push(','); + } + write_ascii_json_string(key, output); + output.push(':'); + write_canonical_json(&values[*key], output)?; + } + output.push('}'); + } + } + Ok(()) +} + +fn python_json_number(number: &serde_json::Number) -> String { + let rendered = number.to_string(); + let Some((mantissa, exponent)) = rendered.split_once('e') else { + return rendered; + }; + let (sign, digits) = if let Some(digits) = exponent.strip_prefix('-') { + ('-', digits) + } else if let Some(digits) = exponent.strip_prefix('+') { + ('+', digits) + } else { + ('+', exponent) + }; + format!("{mantissa}e{sign}{digits:0>2}") +} + +fn write_ascii_json_string(value: &str, output: &mut String) { + output.push('"'); + for character in value.chars() { + match character { + '"' => output.push_str("\\\""), + '\\' => output.push_str("\\\\"), + '\u{0008}' => output.push_str("\\b"), + '\u{000c}' => output.push_str("\\f"), + '\n' => output.push_str("\\n"), + '\r' => output.push_str("\\r"), + '\t' => output.push_str("\\t"), + '\u{0020}'..='\u{007e}' => output.push(character), + character if u32::from(character) <= 0xffff => { + let _ = write!(output, "\\u{:04x}", u32::from(character)); + } + character => { + let scalar = u32::from(character) - 0x1_0000; + let high = 0xd800 + (scalar >> 10); + let low = 0xdc00 + (scalar & 0x3ff); + let _ = write!(output, "\\u{high:04x}\\u{low:04x}"); + } + } + } + output.push('"'); +} + +fn validate_prepared_evaluator_keys( + kernel_id: u32, + kind: &str, + object: &serde_json::Map, +) -> RusticolResult<()> { + if kind == "symjit-application-evaluator" && !object.contains_key("plane_application") { + return Err(RusticolError::compatibility(format!( + "prepared JIT kernel {kernel_id} predates the SymJIT plane-application ABI; \ + regenerate the prepared model" + ))); + } + let expected = match kind { + "symjit-application-evaluator" => [ + "application_abi", + "application_path", + "backend", + "batch_layout", + "build_timing", + "compiler_type", + "direct_table", + "element_layout", + "endianness", + "evaluator_state_path", + "evaluator_state_runtime_capability", + "input_len", + "kind", + "label", + "optimization_level", + "output_len", + "plane_application", + "required_defuns", + "runtime_capability", + "settings", + "translation_mode", + "word_bits", + ] + .as_slice(), + "compiled-complex-evaluator" => [ + "backend", + "build_timing", + "direct_table", + "evaluator_state_path", + "function_name", + "input_len", + "kind", + "library_path", + "number_type", + "output_len", + "runtime_capability", + "settings", + "source_path", + ] + .as_slice(), + _ => { + return Err(RusticolError::compatibility(format!( + "prepared kernel {kernel_id} has unsupported evaluator kind {kind:?}" + ))); + } + }; + let actual = object.keys().map(String::as_str).collect::>(); + let expected = expected.iter().copied().collect::>(); + let expected_without_direct = expected + .iter() + .copied() + .filter(|field| *field != "direct_table") + .collect::>(); + if actual != expected && actual != expected_without_direct { + return Err(RusticolError::artifact(format!( + "prepared kernel {kernel_id} evaluator fields {actual:?} do not match {expected:?}" + ))); + } + Ok(()) +} + +fn validate_prepared_symjit_plane_application( + kernel_id: u32, + object: &serde_json::Map, + input_len: usize, + output_len: usize, + optimization_level: u8, +) -> RusticolResult<()> { + let raw = object.get("plane_application").ok_or_else(|| { + RusticolError::compatibility(format!( + "prepared JIT kernel {kernel_id} predates the SymJIT plane-application ABI; \ + regenerate the prepared model" + )) + })?; + let plane: SymjitPlaneApplicationManifest = + serde_json::from_value(raw.clone()).map_err(|error| { + RusticolError::compatibility(format!( + "prepared JIT kernel {kernel_id} has invalid SymJIT plane-application metadata; \ + regenerate the prepared model: {error}" + )) + })?; + plane.validate(input_len, output_len, optimization_level) +} + +fn required_nonempty_string<'a>( + object: &'a serde_json::Map, + field: &str, + kernel_id: u32, +) -> RusticolResult<&'a str> { + object + .get(field) + .and_then(Value::as_str) + .filter(|value| !value.is_empty()) + .ok_or_else(|| { + RusticolError::artifact(format!( + "prepared kernel {kernel_id} evaluator field {field:?} must be a nonempty string" + )) + }) +} + +impl PreparedKernelInputManifest { + fn to_eager_input(&self) -> RusticolResult { + if self.symbol.is_empty() { + return Err(RusticolError::integrity( + "prepared kernel input symbol must be nonempty", + )); + } + let input = match self.role.as_str() { + "left-current" => EagerKernelInput::FirstCurrentComponent(self.component), + "right-current" => EagerKernelInput::SecondCurrentComponent(self.component), + "left-momentum" => EagerKernelInput::FirstMomentumComponent(self.component), + "right-momentum" => EagerKernelInput::SecondMomentumComponent(self.component), + "current" => EagerKernelInput::FirstCurrentComponent(self.component), + "momentum" => EagerKernelInput::FirstMomentumComponent(self.component), + "coupling-real" => EagerKernelInput::CouplingReal, + "coupling-imag" => EagerKernelInput::CouplingImag, + "model-parameter" => { + EagerKernelInput::ModelParameter(self.model_parameter_index.ok_or_else(|| { + RusticolError::integrity( + "prepared model-parameter input lacks its stable index", + ) + })?) + } + other => { + return Err(RusticolError::artifact(format!( + "unsupported prepared kernel input role {other:?}" + ))); + } + }; + if self.role == "model-parameter" { + if self + .model_parameter_name + .as_deref() + .unwrap_or("") + .is_empty() + { + return Err(RusticolError::integrity( + "prepared model-parameter input lacks its name", + )); + } + } else if self.model_parameter_name.is_some() || self.model_parameter_index.is_some() { + return Err(RusticolError::integrity( + "only prepared model-parameter inputs may carry parameter metadata", + )); + } + Ok(input) + } +} + +fn contiguous_value_slot_widths(plan: &ExecutionPlan) -> RusticolResult> { + let pairs = plan + .value_storage + .value_slots + .iter() + .map(|slot| (slot.value_slot_id, slot.dimension)) + .collect::>(); + contiguous_widths("value", pairs) +} + +fn contiguous_momentum_slot_widths(plan: &ExecutionPlan) -> RusticolResult> { + let pairs = plan + .momentum_slots + .iter() + .map(|slot| { + let width = slot + .component_stop + .checked_sub(slot.component_start) + .ok_or_else(|| { + RusticolError::artifact("eager momentum slot has an inverted component range") + })?; + Ok((slot.momentum_slot_id, width)) + }) + .collect::>>()?; + contiguous_widths("momentum", pairs) +} + +fn contiguous_current_slot_widths(plan: &ExecutionPlan) -> RusticolResult> { + let pairs = plan + .current_storage + .current_slots + .iter() + .map(|slot| (slot.current_id, slot.dimension)) + .collect::>(); + contiguous_widths("current", pairs) +} + +fn contiguous_widths(name: &str, mut pairs: Vec<(usize, usize)>) -> RusticolResult> { + pairs.sort_unstable_by_key(|(id, _)| *id); + if pairs + .iter() + .enumerate() + .any(|(expected, (id, _))| expected != *id) + { + return Err(RusticolError::artifact(format!( + "eager {name} slot ids must be contiguous from zero" + ))); + } + pairs + .into_iter() + .map(|(id, width)| { + if width == 0 { + return Err(RusticolError::artifact(format!( + "eager {name} slot {id} has zero width" + ))); + } + u32::try_from(width) + .map_err(|_| RusticolError::artifact(format!("eager {name} width exceeds u32"))) + }) + .collect() +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/engine/eager_v3_common.rs b/pyamplicol/rust/crates/rusticol-core/src/engine/eager_v3_common.rs new file mode 100644 index 0000000000..47ca1c869b --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/engine/eager_v3_common.rs @@ -0,0 +1,1826 @@ +// SPDX-License-Identifier: 0BSD + +//! Checked construction of the common f64 state used by eager plan-v3. +//! +//! This adapter deliberately reads the compact typed plan, retained columns, +//! and canonical IR directly. It does not recreate the expanded +//! `runtime_schema` representation. + +use super::eager_v3_decode::{ + DecodedEagerPrimitiveColumn, DecodedEagerRetainedTable, DecodedEagerRuntimeV3, +}; +use super::eager_v3_manifest::EagerV3ExecutionManifest; +use super::*; +use serde::Deserialize; +use serde_json::{Value, json}; + +const MISSING_I32: i32 = i32::MIN; + +/// Validated fields that differ between eager artifacts while the compiled +/// lane state remains empty. +pub(super) struct EagerV3CommonParts { + process: String, + key: String, + color_accuracy: String, + external_pdg_order: Vec, + external_count: usize, + parameter_count: usize, + value_parameter_count: usize, + momentum_parameter_count: usize, + current_count: usize, + source_count: usize, + interaction_count: usize, + stage_count: usize, + amplitude_output_count: usize, + sources: Vec, + momentum_slots: Vec, + external_is_initial: Vec, + particle_masses: BTreeMap, + particle_mass_parameter_names: BTreeMap, + normalization_factor: f64, + normalization_color_factor: f64, + normalization_average_factor: f64, + normalization_identical_factor: f64, + normalization_qcd_coupling_power: usize, + normalization_electroweak_coupling_power: usize, + lc_topology_replay: Option, + lc_topology_replay_data: LcTopologyReplayData, + color_topology_replay: Option, + color_topology_replay_data: LcTopologyReplayData, + model_parameters: Vec, + model_parameter_name_to_index: BTreeMap, + model_parameter_runtime_slots: BTreeMap, + model_parameter_values_f64: Vec, + physics: Arc, +} + +/// One-line integration entry point for the plan-v3 loader. +pub(super) fn build_eager_v3_common_runtime( + decoded: &DecodedEagerRuntimeV3, + manifest: &EagerV3ExecutionManifest, + physics: ProcessPhysicsV1, +) -> RusticolResult { + EagerV3CommonParts::checked(decoded, manifest, physics).map(EagerV3CommonParts::into_runtime) +} + +/// Build the bounded schema fragment required by the lazy Python exact lane. +/// +/// This deliberately omits compiled-stage metadata and all high-cardinality +/// proof tables. Those remain authenticated in PACBIN and are exposed through +/// the dedicated exact row bridge only when non-f64 execution is requested. +pub(super) fn build_eager_v3_exact_schema( + decoded: &DecodedEagerRuntimeV3, + manifest: &EagerV3ExecutionManifest, + physics: ProcessPhysicsV1, + amplitude_stage: Value, +) -> RusticolResult { + EagerV3CommonParts::checked(decoded, manifest, physics)? + .into_exact_schema(decoded, amplitude_stage) +} + +impl EagerV3CommonParts { + fn checked( + decoded: &DecodedEagerRuntimeV3, + manifest: &EagerV3ExecutionManifest, + physics: ProcessPhysicsV1, + ) -> RusticolResult { + validate_identity(decoded, manifest, &physics)?; + let retained = RetainedTables::new(&decoded.retained_tables); + let metadata = retained.table("metadata")?; + let normalization_ir_id = retained.scalar_u32(metadata, "normalization_ir_id")?; + let normalization: CanonicalNormalization = + canonical_ir(decoded, normalization_ir_id, "runtime normalization")?; + normalization.validate()?; + let lc_topology_replay = build_lc_topology_replay_manifest(decoded, &retained)?; + let lc_topology_replay_data = + super::runtime_load::build_lc_topology_replay_data(lc_topology_replay.as_ref())?; + let color_topology_replay = + build_color_topology_replay_manifest(decoded, &retained, &manifest.color_accuracy)?; + let color_topology_replay_data = super::runtime_load::build_color_topology_replay_data( + color_topology_replay.as_ref(), + &manifest.color_accuracy, + )?; + + let model_parameters = build_model_parameters(decoded, &retained)?; + validate_physics_parameters(&model_parameters, &physics)?; + let model_parameter_values_f64 = model_parameters + .iter() + .map(|parameter| parameter.default) + .collect::>(); + let model_parameter_name_to_index = model_parameters + .iter() + .map(|parameter| (parameter.name.clone(), parameter.parameter_index)) + .collect::>(); + if model_parameter_name_to_index.len() != model_parameters.len() { + return Err(integrity("eager model-parameter names are not unique")); + } + let model_parameter_runtime_slots = runtime_parameter_slots(&model_parameters)?; + let (mut particle_masses, mut particle_mass_parameter_names) = + direct_particle_masses(&model_parameters)?; + + let momentum_slots = build_momentum_slots(decoded, physics.external_particles.len())?; + let sources = build_sources( + decoded, + &retained, + &physics, + &model_parameter_runtime_slots, + &model_parameter_values_f64, + &mut particle_masses, + &mut particle_mass_parameter_names, + )?; + let value_parameter_count = component_count(&decoded.values, "value")?; + let momentum_parameter_count = component_count(&decoded.momenta, "momentum")?; + let parameter_count = value_parameter_count + .checked_add(momentum_parameter_count) + .and_then(|count| count.checked_add(model_parameters.len())) + .ok_or_else(|| RusticolError::artifact("eager common parameter count overflows"))?; + + // Contracted-color reductions already carry the color contraction. + let color_factor = if physics.color_accuracy.as_str() == "lc" { + normalization.color_factor + } else { + 1.0 + }; + let normalization_factor = color_factor * normalization.global_coupling_factor + / (normalization.average_factor * normalization.identical_factor); + if !normalization_factor.is_finite() { + return Err(integrity("eager runtime normalization is not finite")); + } + let physics = Arc::new(PhysicsRuntime::new(physics)?); + + Ok(Self { + process: manifest.process.clone(), + key: manifest.key.clone(), + color_accuracy: manifest.color_accuracy.clone(), + external_pdg_order: manifest.external_pdg_order.clone(), + external_count: manifest.external_pdg_order.len(), + parameter_count, + value_parameter_count, + momentum_parameter_count, + current_count: decoded.currents.len(), + source_count: decoded.sources.len(), + interaction_count: usize_count( + manifest.dag_summary.interaction_count, + "interaction count", + )?, + stage_count: decoded.stages.len(), + amplitude_output_count: usize::try_from(decoded.dimensions.amplitude_count) + .map_err(|_| RusticolError::artifact("eager amplitude count exceeds usize"))?, + sources, + momentum_slots, + external_is_initial: physics + .manifest + .external_particles + .iter() + .map(|particle| particle.role == crate::ParticleRole::Initial) + .collect(), + particle_masses, + particle_mass_parameter_names, + normalization_factor, + normalization_color_factor: color_factor, + normalization_average_factor: normalization.average_factor, + normalization_identical_factor: normalization.identical_factor, + normalization_qcd_coupling_power: normalization.qcd_coupling_power, + normalization_electroweak_coupling_power: normalization.electroweak_coupling_power, + lc_topology_replay, + lc_topology_replay_data, + color_topology_replay, + color_topology_replay_data, + model_parameters, + model_parameter_name_to_index, + model_parameter_runtime_slots, + model_parameter_values_f64, + physics, + }) + } + + fn into_runtime(self) -> ExecutionRuntime { + let topology_replay_mappings = self.lc_topology_replay_data.mappings; + let topology_replay_public_mappings = topology_replay_mappings.clone(); + let color_topology_replay_mappings = self.color_topology_replay_data.mappings; + ExecutionRuntime { + process: self.process, + key: self.key, + color_accuracy: self.color_accuracy, + external_pdg_order: self.external_pdg_order, + external_count: self.external_count, + parameter_count: self.parameter_count, + value_parameter_count: self.value_parameter_count, + momentum_parameter_count: self.momentum_parameter_count, + current_count: self.current_count, + source_count: self.source_count, + interaction_count: self.interaction_count, + stage_count: self.stage_count, + amplitude_output_count: self.amplitude_output_count, + lc_topology_replay_enabled: !topology_replay_mappings.is_empty(), + lc_topology_replay_mappings: Arc::new(topology_replay_mappings), + lc_topology_replay_public_mappings: topology_replay_public_mappings, + lc_topology_replay_routes: self.lc_topology_replay_data.routes, + lc_topology_replay_materialized_sector_ids: self + .lc_topology_replay_data + .materialized_sector_ids, + lc_resolved_replay_plan: None, + lc_resolved_replay_selection_cache: None, + lc_replay_flat_momenta_scratch: Vec::new(), + lc_replay_target_components_scratch: Vec::new(), + color_topology_replay_enabled: !color_topology_replay_mappings.is_empty(), + color_topology_replay_mappings: Arc::new(color_topology_replay_mappings), + color_replay_flat_momenta_scratch: Vec::new(), + helicity_recurrence: None, + compiled_helicity_execution_plan: None, + compiled_color_execution_plan: None, + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + compiled_direct_runtime: None, + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + compiled_direct_color_schedules: BTreeMap::new(), + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + compiled_direct_helicity_schedules: BTreeMap::new(), + helicity_sum_runtime: None, + helicity_selector_runtimes: Vec::new(), + helicity_selector_runtime_schedule_modes: Vec::new(), + helicity_selector_lane_by_domain: BTreeMap::new(), + color_selector_runtimes: BTreeMap::new(), + runtime_unavailable_message: None, + sources: self.sources, + momentum_slots: self.momentum_slots, + external_is_initial: self.external_is_initial, + particle_masses: self.particle_masses, + particle_mass_parameter_names: self.particle_mass_parameter_names, + normalization_factor: self.normalization_factor, + normalization_color_factor: self.normalization_color_factor, + normalization_average_factor: self.normalization_average_factor, + normalization_identical_factor: self.normalization_identical_factor, + normalization_qcd_coupling_power: self.normalization_qcd_coupling_power, + normalization_electroweak_coupling_power: self.normalization_electroweak_coupling_power, + model_parameters: self.model_parameters, + model_parameter_name_to_index: self.model_parameter_name_to_index, + model_parameter_runtime_slots: self.model_parameter_runtime_slots, + model_parameter_values_f64: self.model_parameter_values_f64, + model_parameter_evaluator: None, + physics_reduction_override: None, + physics: Some(self.physics), + stages: None, + amplitude_stage: None, + state_scratch_f64: Vec::new(), + state_scratch_f64_requires_clear: false, + values_scratch_f64: Vec::new(), + } + } + + fn into_exact_schema( + self, + decoded: &DecodedEagerRuntimeV3, + amplitude_stage: Value, + ) -> RusticolResult { + let source_current_ids = self + .sources + .iter() + .map(|source| source.current_id) + .collect::>(); + let current_component_count = decoded + .dimensions + .current_component_counts + .iter() + .try_fold(0_usize, |total, count| { + total + .checked_add(*count as usize) + .ok_or_else(|| RusticolError::artifact("eager exact current layout overflows")) + })?; + let current_slots = decoded + .currents + .iter() + .map(|row| { + let start = usize_count(row.component_start, "exact current component start")?; + let dimension = row.component_count as usize; + Ok(json!({ + "current_id": row.current_id, + "component_start": start, + "component_stop": start + dimension, + "dimension": dimension, + "is_source": source_current_ids.contains(&(row.current_id as usize)), + })) + }) + .collect::>>()?; + let value_slots = decoded + .values + .iter() + .map(|row| { + let start = usize_count(row.component_start, "exact value component start")?; + let dimension = row.component_count as usize; + let variant = match row.kind { + crate::eager_lowering_v3::EagerValueSlotKind::Source => "source", + crate::eager_lowering_v3::EagerValueSlotKind::Unpropagated => "unpropagated", + crate::eager_lowering_v3::EagerValueSlotKind::Propagated => "propagated", + }; + Ok(json!({ + "value_slot_id": row.value_slot_id, + "current_id": row.current_id, + "variant": variant, + "component_start": start, + "component_stop": start + dimension, + "dimension": dimension, + })) + }) + .collect::>>()?; + let external_particles = self + .physics + .manifest + .external_particles + .iter() + .map(|particle| { + json!({ + "label": particle.label, + "index": particle.index, + "pdg": particle.pdg, + "role": match particle.role { + crate::ParticleRole::Initial => "initial", + crate::ParticleRole::Final => "final", + }, + "momentum_slot": particle.momentum_slot, + }) + }) + .collect::>(); + let model_particles = self + .particle_masses + .iter() + .map(|(pdg, mass)| { + json!({ + "pdg": pdg, + "mass": mass, + "mass_parameter": self.particle_mass_parameter_names.get(pdg), + }) + }) + .collect::>(); + let flattened_parameter_count = self + .value_parameter_count + .checked_add(self.momentum_parameter_count) + .and_then(|count| count.checked_add(self.model_parameters.len())) + .ok_or_else(|| RusticolError::artifact("eager exact parameter layout overflows"))?; + + Ok(json!({ + "kind": "pyamplicol-eager-exact-schema-v1", + "parameter_layout": { + "value_component_count": self.value_parameter_count, + "momentum_parameter_count": self.momentum_parameter_count, + "model_parameter_count": self.model_parameters.len(), + "parameter_count_if_flattened": flattened_parameter_count, + }, + "current_storage": { + "component_count": current_component_count, + "current_slots": current_slots, + }, + "value_storage": { + "component_count": self.value_parameter_count, + "value_slots": value_slots, + }, + "momentum_slots": self.momentum_slots, + "external_particles": external_particles, + "model": {"particles": model_particles}, + "model_parameters": self.model_parameters, + "source_fill": { + "source_count": self.sources.len(), + "sources": self.sources, + }, + "lc_topology_replay": self.lc_topology_replay, + "color_topology_replay": self.color_topology_replay, + "amplitude_stage": amplitude_stage, + })) + } +} + +fn build_lc_topology_replay_manifest( + decoded: &DecodedEagerRuntimeV3, + retained: &RetainedTables<'_>, +) -> RusticolResult> { + build_topology_replay_manifest(decoded, retained, 1) +} + +fn build_color_topology_replay_manifest( + decoded: &DecodedEagerRuntimeV3, + retained: &RetainedTables<'_>, + color_accuracy: &str, +) -> RusticolResult> { + let Some(compatibility) = build_topology_replay_manifest(decoded, retained, 2)? else { + return Ok(None); + }; + Ok(Some(ColorTopologyReplayManifest { + enabled: compatibility.enabled, + mode: compatibility.mode, + contract_version: Some(3), + color_accuracy: color_accuracy.to_string(), + physical_sector_count: compatibility.physical_sector_count, + replayed_sector_count: compatibility.replayed_sector_count, + materialized_sector_ids: compatibility.materialized_sector_ids, + residual_sector_ids: compatibility.residual_sector_ids, + groups: compatibility.groups, + })) +} + +pub(super) fn load_eager_color_topology_replay_manifests( + decoded: &DecodedEagerRuntimeV3, + include_exact_weights: bool, +) -> RusticolResult< + Option<( + GenericColorTopologyReplayAmplitudeManifest, + GenericColorContractionManifest, + )>, +> { + let retained = RetainedTables::new(&decoded.retained_tables); + let metadata = retained.table("color_replay_metadata")?; + let present = retained.scalar_u8(metadata, "present")?; + let contract_version = retained.scalar_u32(metadata, "contract_version")?; + let physical_group_count = usize_count( + retained.scalar_u64(metadata, "physical_group_count")?, + "physical group count", + )?; + let mapping_count = usize_count( + retained.scalar_u64(metadata, "mapping_count")?, + "mapping count", + )?; + let groups = retained.table("color_replay_physical_groups")?; + let mappings = retained.table("color_replay_mappings")?; + let permutations = retained.table("color_replay_permutations")?; + let routes = retained.table("color_replay_routes")?; + let contraction_metadata = retained.table("color_replay_contraction_metadata")?; + let contraction_component_groups = + retained.table("color_replay_contraction_component_groups")?; + let contraction_entries = retained.table("color_replay_contraction_entries")?; + if present == 0 { + if contract_version != 0 + || physical_group_count != 0 + || mapping_count != 0 + || groups.row_count != 0 + || mappings.row_count != 0 + || permutations.row_count != 0 + || routes.row_count != 0 + || retained.scalar_u8(contraction_metadata, "present")? != 0 + || retained.scalar_u64(contraction_metadata, "group_count")? != 0 + || retained.scalar_u8(contraction_metadata, "repeated")? != 0 + || retained.scalar_u64(contraction_metadata, "component_count")? != 0 + || contraction_component_groups.row_count != 0 + || contraction_entries.row_count != 0 + { + return Err(integrity( + "absent eager color replay contains retained payload rows", + )); + } + return Ok(None); + } + let contraction_repeated = retained.scalar_u8(contraction_metadata, "repeated")?; + let contraction_component_count = usize_count( + retained.scalar_u64(contraction_metadata, "component_count")?, + "color replay contraction component count", + )?; + if present != 1 + || contract_version != 1 + || physical_group_count == 0 + || groups.row_count as usize != physical_group_count + || mappings.row_count as usize != mapping_count + || mapping_count == 0 + || retained.scalar_u8(contraction_metadata, "present")? != 1 + || retained.scalar_u64(contraction_metadata, "group_count")? != physical_group_count as u64 + || retained.scalar_u8(contraction_metadata, "includes_color_factor")? != 1 + || contraction_repeated > 1 + || (contraction_repeated == 0 + && (contraction_component_count != 0 || contraction_component_groups.row_count != 0)) + || (contraction_repeated == 1 + && (contraction_component_count < 2 + || contraction_component_groups.row_count as usize != physical_group_count)) + { + return Err(integrity( + "eager color replay amplitude metadata is inconsistent", + )); + } + + let group_ids = retained.u32_column(groups, "group_id", 1)?; + let sector_ids = retained.u32_column(groups, "color_sector_id", 1)?; + let word_ids = retained.u32_column(groups, "color_word_sequence_id", 1)?; + let helicity_ids = retained.u32_column(groups, "helicity_values_sequence_id", 1)?; + let helicity_weight_ids = retained.u32_column(groups, "helicity_weight_factor_id", 1)?; + let physical_groups = (0..physical_group_count) + .map(|index| { + let helicity_weight = + eager_exact_real_factor(decoded, helicity_weight_ids[index], "helicity weight")?; + Ok(GenericColorTopologyReplayPhysicalGroupManifest { + group_id: i64::from(group_ids[index]), + helicities: i32_sequence(decoded, helicity_ids[index])?, + color_sector_id: i64::from(sector_ids[index]), + color_word: u32_sequence(decoded, word_ids[index], "color replay word")? + .into_iter() + .map(|value| value as usize) + .collect(), + helicity_weight, + }) + }) + .collect::>>()?; + + let permutation_starts = retained.u64_column(mappings, "permutation_start", 1)?; + let permutation_counts = retained.u64_column(mappings, "permutation_count", 1)?; + let route_starts = retained.u64_column(mappings, "route_start", 1)?; + let route_counts = retained.u64_column(mappings, "route_count", 1)?; + let representative_labels = retained.u32_column(permutations, "representative_label", 1)?; + let sector_labels = retained.u32_column(permutations, "sector_label", 1)?; + let source_group_ids = retained.u32_column(routes, "source_group_id", 1)?; + let target_group_ids = retained.u32_column(routes, "target_group_id", 1)?; + let route_factor_ids = retained.u32_column(routes, "factor_id", 1)?; + let mut permutation_cursor = 0usize; + let mut route_cursor = 0usize; + let mut replay_mappings = Vec::with_capacity(mapping_count); + for index in 0..mapping_count { + let permutation_start = + usize_count(permutation_starts[index], "color replay permutation start")?; + let permutation_count = + usize_count(permutation_counts[index], "color replay permutation count")?; + let route_start = usize_count(route_starts[index], "color replay route start")?; + let route_count = usize_count(route_counts[index], "color replay route count")?; + if permutation_start != permutation_cursor || route_start != route_cursor { + return Err(integrity( + "eager color replay mappings are not canonical and contiguous", + )); + } + let permutation_stop = permutation_start + .checked_add(permutation_count) + .ok_or_else(|| RusticolError::artifact("color replay permutation range overflows"))?; + let route_stop = route_start + .checked_add(route_count) + .ok_or_else(|| RusticolError::artifact("color replay route range overflows"))?; + if permutation_stop > permutations.row_count as usize + || route_stop > routes.row_count as usize + { + return Err(integrity( + "eager color replay mapping range is out of bounds", + )); + } + replay_mappings.push(GenericColorTopologyReplayMappingManifest { + label_permutation: (permutation_start..permutation_stop) + .map(|row| LcTopologyReplayLabelPermutationManifest { + representative_label: representative_labels[row] as usize, + sector_label: sector_labels[row] as usize, + }) + .collect(), + group_routes: (route_start..route_stop) + .map(|row| { + Ok(GenericColorTopologyReplayGroupRouteManifest { + source_group_id: i64::from(source_group_ids[row]), + target_group_id: i64::from(target_group_ids[row]), + factor: eager_exact_factor(decoded, route_factor_ids[row], "route factor")?, + }) + }) + .collect::>>()?, + }); + permutation_cursor = permutation_stop; + route_cursor = route_stop; + } + if permutation_cursor != permutations.row_count as usize + || route_cursor != routes.row_count as usize + { + return Err(integrity( + "eager color replay retained tables contain unreachable rows", + )); + } + + let left_ids = retained.u32_column(contraction_entries, "left_group_id", 1)?; + let right_ids = retained.u32_column(contraction_entries, "right_group_id", 1)?; + let left_indices = retained.u32_column(contraction_entries, "left_group_index", 1)?; + let right_indices = retained.u32_column(contraction_entries, "right_group_index", 1)?; + let weight_ids = retained.u32_column(contraction_entries, "weight_factor_id", 1)?; + let symmetry_ids = retained.u32_column(contraction_entries, "symmetry_factor_id", 1)?; + if contraction_entries.row_count == 0 { + return Err(integrity( + "eager color replay physical contraction is empty", + )); + } + let (entries, repeated_block) = if contraction_repeated == 0 { + if left_indices + .iter() + .chain(right_indices.iter()) + .any(|value| *value != u32::MAX) + { + return Err(integrity( + "expanded eager color replay contraction contains local indices", + )); + } + ( + (0..contraction_entries.row_count as usize) + .map(|index| { + Ok(GenericColorContractionEntryManifest { + _exact_weight: if include_exact_weights { + eager_exact_color_weight(decoded, weight_ids[index])? + } else { + None + }, + left_group_id: i64::from(left_ids[index]), + right_group_id: i64::from(right_ids[index]), + weight: eager_exact_factor( + decoded, + weight_ids[index], + "contraction weight", + )?, + symmetry_factor: eager_exact_real_factor( + decoded, + symmetry_ids[index], + "contraction symmetry", + )?, + }) + }) + .collect::>>()?, + None, + ) + } else { + if left_ids + .iter() + .chain(right_ids.iter()) + .any(|value| *value != u32::MAX) + { + return Err(integrity( + "repeated eager color replay contraction contains physical group IDs", + )); + } + let component_group_ids = retained + .u32_column(contraction_component_groups, "group_id", 1)? + .iter() + .map(|value| i64::from(*value)) + .collect(); + let repeated_entries = (0..contraction_entries.row_count as usize) + .map(|index| { + Ok(GenericRepeatedColorContractionEntryManifest { + _exact_weight: if include_exact_weights { + eager_exact_color_weight(decoded, weight_ids[index])? + } else { + None + }, + left_group_index: left_indices[index] as usize, + right_group_index: right_indices[index] as usize, + weight: eager_exact_factor(decoded, weight_ids[index], "contraction weight")?, + symmetry_factor: eager_exact_real_factor( + decoded, + symmetry_ids[index], + "contraction symmetry", + )?, + }) + }) + .collect::>>()?; + ( + Vec::new(), + Some(GenericRepeatedColorContractionBlockManifest { + component_count: contraction_component_count, + component_group_ids, + entries: repeated_entries, + factorized_block: None, + }), + ) + }; + let amplitude_manifest = GenericColorTopologyReplayAmplitudeManifest { + contract_version, + physical_group_count, + physical_groups, + mappings: replay_mappings, + }; + let contraction_manifest = GenericColorContractionManifest { + supported: true, + reason: None, + group_count: physical_group_count, + includes_color_factor: true, + entries, + repeated_block, + }; + Ok(Some((amplitude_manifest, contraction_manifest))) +} + +pub(super) fn build_eager_color_topology_replay_amplitude_runtime( + decoded: &DecodedEagerRuntimeV3, + materialized_groups: &[RawSumGroup], + output_count: usize, +) -> RusticolResult> { + let Some((amplitude_manifest, contraction_manifest)) = + load_eager_color_topology_replay_manifests(decoded, false)? + else { + return Ok(None); + }; + AmplitudeRuntime::color_topology_replay_reducer( + output_count, + materialized_groups, + &litude_manifest, + &contraction_manifest, + ) + .map(Some) +} + +pub(super) fn eager_exact_color_weight( + decoded: &DecodedEagerRuntimeV3, + factor_id: u32, +) -> RusticolResult> { + let factor = decoded + .exact_factors + .get(factor_id as usize) + .ok_or_else(|| integrity("eager exact colour weight is absent"))?; + if factor.exact_source != 1 { + return Ok(None); + } + let raw = decoded + .exact_ir + .get(factor.exact_ir_id as usize) + .ok_or_else(|| integrity("eager exact colour weight source is absent"))?; + let payload: serde_json::Value = serde_json::from_str(raw) + .map_err(|error| integrity(format!("invalid exact colour weight source: {error}")))?; + let source = &payload["source"]; + if source["kind"] != "rational-complex" { + return Ok(None); + } + serde_json::from_value(source["value"].clone()) + .map(Some) + .map_err(|error| integrity(format!("invalid rational colour weight: {error}"))) +} + +fn eager_exact_factor( + decoded: &DecodedEagerRuntimeV3, + factor_id: u32, + context: &str, +) -> RusticolResult> { + let factor = decoded + .exact_factors + .get(factor_id as usize) + .ok_or_else(|| integrity(format!("eager color replay {context} is absent")))?; + let real = f64::from_bits(factor.real_bits); + let imaginary = f64::from_bits(factor.imaginary_bits); + if !real.is_finite() || !imaginary.is_finite() { + return Err(integrity(format!( + "eager color replay {context} is not finite" + ))); + } + Ok(vec![real, imaginary]) +} + +fn eager_exact_real_factor( + decoded: &DecodedEagerRuntimeV3, + factor_id: u32, + context: &str, +) -> RusticolResult { + let factor = eager_exact_factor(decoded, factor_id, context)?; + if factor[1] != 0.0 { + return Err(integrity(format!( + "eager color replay {context} is not real" + ))); + } + Ok(factor[0]) +} + +fn build_topology_replay_manifest( + decoded: &DecodedEagerRuntimeV3, + retained: &RetainedTables<'_>, + expected_kind: u8, +) -> RusticolResult> { + let metadata = retained.table("lc_replay_metadata")?; + let present = retained.scalar_u8(metadata, "present")?; + let replay_kind = retained.scalar_u8(metadata, "replay_kind")?; + let physical_sequence_id = retained.scalar_u32(metadata, "physical_sector_ids_sequence_id")?; + let materialized_sequence_id = + retained.scalar_u32(metadata, "materialized_sector_ids_sequence_id")?; + let partitions = retained.table("lc_replay_partitions")?; + let members = retained.table("lc_replay_members")?; + let permutations = retained.table("lc_replay_permutations")?; + let residuals = retained.table("lc_replay_residual_sectors")?; + if present == 0 { + if replay_kind != 0 { + return Err(integrity( + "absent eager topology replay has a nonzero replay kind", + )); + } + if physical_sequence_id != crate::MISSING_U32 + || materialized_sequence_id != crate::MISSING_U32 + || partitions.row_count != 0 + || members.row_count != 0 + || permutations.row_count != 0 + || residuals.row_count != 0 + { + return Err(integrity( + "absent eager LC topology replay has retained payload rows", + )); + } + return Ok(None); + } + if replay_kind != expected_kind { + if replay_kind == 1 || replay_kind == 2 { + return Ok(None); + } + return Err(integrity( + "eager topology replay has an unknown replay kind", + )); + } + if present != 1 + || physical_sequence_id == crate::MISSING_U32 + || materialized_sequence_id == crate::MISSING_U32 + { + return Err(integrity( + "eager LC topology replay metadata has an invalid presence contract", + )); + } + + let physical_sector_ids = u32_sequence(decoded, physical_sequence_id, "LC physical sectors")?; + let materialized_sector_ids = + u32_sequence(decoded, materialized_sequence_id, "LC materialized sectors")?; + let representative_sector_ids = + retained.u32_column(partitions, "representative_sector_id", 1)?; + let partition_materialized_sector_ids = + retained.u32_column(partitions, "materialized_sector_id", 1)?; + let member_starts = retained.u64_column(partitions, "member_start", 1)?; + let member_counts = retained.u64_column(partitions, "member_count", 1)?; + let proof_algorithm_ids = retained.u32_column(partitions, "proof_algorithm_string_id", 1)?; + let proof_digest_ids = retained.u32_column(partitions, "proof_digest_string_id", 1)?; + + let member_sector_ids = retained.u32_column(members, "sector_id", 1)?; + let member_factor_ids = retained.u32_column(members, "factor_id", 1)?; + let member_signs = retained.i32_column(members, "sign", 1)?; + let permutation_starts = retained.u64_column(members, "permutation_start", 1)?; + let permutation_counts = retained.u64_column(members, "permutation_count", 1)?; + let representative_labels = retained.u32_column(permutations, "representative_label", 1)?; + let sector_labels = retained.u32_column(permutations, "sector_label", 1)?; + let residual_sector_ids = retained.u32_column(residuals, "sector_id", 1)?; + + let mut groups = Vec::with_capacity(partitions.row_count as usize); + let mut member_cursor = 0_usize; + let mut permutation_cursor = 0_usize; + for partition_index in 0..partitions.row_count as usize { + let member_start = usize_count(member_starts[partition_index], "LC replay member start")?; + let member_count = usize_count(member_counts[partition_index], "LC replay member count")?; + if member_start != member_cursor { + return Err(integrity( + "eager LC replay partition members are not canonical and contiguous", + )); + } + let member_stop = member_start + .checked_add(member_count) + .ok_or_else(|| RusticolError::artifact("LC replay member range overflows"))?; + if member_stop > members.row_count as usize { + return Err(integrity("eager LC replay member range is out of bounds")); + } + let mut active_sector_ids = Vec::with_capacity(member_count); + let mut sector_permutations = Vec::with_capacity(member_count); + for member_index in member_start..member_stop { + let permutation_start = usize_count( + permutation_starts[member_index], + "LC replay permutation start", + )?; + let permutation_count = usize_count( + permutation_counts[member_index], + "LC replay permutation count", + )?; + if permutation_start != permutation_cursor { + return Err(integrity( + "eager LC replay permutations are not canonical and contiguous", + )); + } + let permutation_stop = permutation_start + .checked_add(permutation_count) + .ok_or_else(|| RusticolError::artifact("LC replay permutation range overflows"))?; + if permutation_stop > permutations.row_count as usize { + return Err(integrity( + "eager LC replay permutation range is out of bounds", + )); + } + let sign = i8::try_from(member_signs[member_index]) + .map_err(|_| integrity("eager LC replay sign does not fit i8"))?; + let factor = decoded + .exact_factors + .get(member_factor_ids[member_index] as usize) + .ok_or_else(|| integrity("eager LC replay factor is absent"))?; + let amplitude_factor = [ + f64::from_bits(factor.real_bits), + f64::from_bits(factor.imaginary_bits), + ]; + let weight = amplitude_factor[0] * f64::from(sign); + let label_permutation = (permutation_start..permutation_stop) + .map(|index| LcTopologyReplayLabelPermutationManifest { + representative_label: representative_labels[index] as usize, + sector_label: sector_labels[index] as usize, + }) + .collect(); + let sector_id = i64::from(member_sector_ids[member_index]); + active_sector_ids.push(sector_id); + sector_permutations.push(LcTopologyReplaySectorPermutationManifest { + sector_id, + weight, + sign, + factor: Some(amplitude_factor.to_vec()), + label_permutation, + }); + permutation_cursor = permutation_stop; + } + groups.push(LcTopologyReplayGroupManifest { + representative_sector_id: i64::from(representative_sector_ids[partition_index]), + materialized_sector_id: i64::from(partition_materialized_sector_ids[partition_index]), + active_sector_ids, + proof: Some(LcTopologyReplayProofManifest { + status: "proven".to_string(), + algorithm: Some( + string( + decoded, + proof_algorithm_ids[partition_index], + "LC replay proof algorithm", + )? + .to_string(), + ), + digest: Some( + string( + decoded, + proof_digest_ids[partition_index], + "LC replay proof digest", + )? + .to_string(), + ), + }), + sector_permutations, + }); + member_cursor = member_stop; + } + if member_cursor != members.row_count as usize + || permutation_cursor != permutations.row_count as usize + { + return Err(integrity( + "eager LC replay retained tables contain unreachable rows", + )); + } + + Ok(Some(LcTopologyReplayManifest { + enabled: true, + mode: "external-label-permutation".to_string(), + contract_version: Some(2), + physical_sector_count: Some(physical_sector_ids.len()), + replayed_sector_count: members.row_count as usize, + materialized_sector_ids: materialized_sector_ids.into_iter().map(i64::from).collect(), + residual_sector_ids: residual_sector_ids.iter().copied().map(i64::from).collect(), + groups, + })) +} + +fn validate_identity( + decoded: &DecodedEagerRuntimeV3, + manifest: &EagerV3ExecutionManifest, + physics: &ProcessPhysicsV1, +) -> RusticolResult<()> { + physics.validate()?; + if decoded.process_key.as_ref() != manifest.key + || physics.process_id != manifest.key + || physics.process != manifest.process + || physics.color_accuracy.as_str() != manifest.color_accuracy + || physics.external_particles.len() != manifest.external_pdg_order.len() + || physics + .external_particles + .iter() + .map(|particle| particle.pdg) + .ne(manifest.external_pdg_order.iter().copied()) + { + return Err(integrity( + "eager plan-v3 identity does not match its manifest and resolved physics", + )); + } + if decoded.currents.len() != usize_count(manifest.dag_summary.current_count, "current count")? + || decoded.sources.len() != usize_count(manifest.dag_summary.source_count, "source count")? + || decoded.closures.is_empty() + { + return Err(integrity( + "eager plan-v3 decoded counts do not match its DAG summary", + )); + } + Ok(()) +} + +fn build_model_parameters( + decoded: &DecodedEagerRuntimeV3, + retained: &RetainedTables<'_>, +) -> RusticolResult> { + let table = retained.table("model_parameters")?; + let pdgs = retained.i32_column(table, "pdg", 1)?; + if pdgs.len() != decoded.parameters.len() { + return Err(integrity( + "retained model-parameter PDGs have the wrong row count", + )); + } + decoded + .parameters + .iter() + .zip(pdgs) + .map(|(row, pdg)| { + let index = usize::try_from(row.parameter_id) + .map_err(|_| RusticolError::artifact("parameter ID exceeds usize"))?; + let factor = decoded + .exact_factors + .get(row.default_factor_id as usize) + .ok_or_else(|| integrity("parameter default factor is absent"))?; + let default = f64::from_bits(factor.real_bits); + let imaginary = f64::from_bits(factor.imaginary_bits); + if !default.is_finite() || imaginary != 0.0 { + return Err(integrity( + "eager model-parameter default is not finite and real", + )); + } + Ok(GenericRuntimeModelParameterManifest { + name: string(decoded, row.name_string_id, "parameter name")?.to_owned(), + kind: string(decoded, row.kind_string_id, "parameter kind")?.to_owned(), + parameter_index: index, + default, + pdg: (*pdg != MISSING_I32).then_some(*pdg), + runtime_name: optional_string(decoded, row.runtime_name_string_id)? + .map(str::to_owned), + complex_component: match row.complex_component { + -1 => None, + 0 => Some("real".to_owned()), + 1 => Some("imag".to_owned()), + _ => return Err(integrity("invalid eager parameter complex component")), + }, + }) + }) + .collect() +} + +fn runtime_parameter_slots( + parameters: &[GenericRuntimeModelParameterManifest], +) -> RusticolResult> { + let mut result = BTreeMap::new(); + let mut complex = BTreeMap::, Option)>::new(); + for parameter in parameters { + if let Some(name) = ¶meter.runtime_name { + let slots = complex.entry(name.clone()).or_default(); + let slot = match parameter.complex_component.as_deref() { + Some("real") => &mut slots.0, + Some("imag") => &mut slots.1, + _ => { + return Err(integrity( + "complex eager parameter lacks component metadata", + )); + } + }; + if slot.replace(parameter.parameter_index).is_some() { + return Err(integrity("duplicate eager complex parameter component")); + } + } else if result + .insert( + parameter.name.clone(), + RuntimeParameterSlots { + real: parameter.parameter_index, + imaginary: None, + }, + ) + .is_some() + { + return Err(integrity("duplicate eager scalar parameter")); + } + } + for (name, (real, imaginary)) in complex { + let real = real.ok_or_else(|| integrity("complex eager parameter lacks a real slot"))?; + if result + .insert(name, RuntimeParameterSlots { real, imaginary }) + .is_some() + { + return Err(integrity("conflicting eager logical parameter")); + } + } + Ok(result) +} + +fn direct_particle_masses( + parameters: &[GenericRuntimeModelParameterManifest], +) -> RusticolResult<(BTreeMap, BTreeMap)> { + let mut masses = BTreeMap::new(); + let mut names = BTreeMap::new(); + for parameter in parameters { + if parameter.kind != "particle_mass" { + continue; + } + let pdg = parameter + .pdg + .ok_or_else(|| integrity("particle-mass parameter lacks a retained PDG"))?; + if parameter.default < 0.0 + || masses.insert(pdg, parameter.default).is_some() + || names.insert(pdg, parameter.name.clone()).is_some() + { + return Err(integrity("invalid or duplicate particle-mass parameter")); + } + } + Ok((masses, names)) +} + +#[allow(clippy::too_many_arguments)] +fn build_sources( + decoded: &DecodedEagerRuntimeV3, + retained: &RetainedTables<'_>, + physics: &ProcessPhysicsV1, + runtime_slots: &BTreeMap, + parameter_values: &[f64], + particle_masses: &mut BTreeMap, + mass_names: &mut BTreeMap, +) -> RusticolResult> { + let currents = retained.table("currents")?; + let particle_ids = retained.i32_column(currents, "particle_id", 1)?; + let source_labels = retained.u32_column(currents, "source_leg_label", 1)?; + let source_helicities = retained.i32_column(currents, "source_helicity", 1)?; + let chiralities = retained.i32_column(currents, "chirality", 1)?; + let spin_state_ids = retained.u32_column(currents, "spin_state_sequence_id", 1)?; + let ancestry_ids = retained.u32_column(currents, "helicity_ancestry_bitset_id", 1)?; + for column in [ + particle_ids.len(), + source_labels.len(), + source_helicities.len(), + chiralities.len(), + spin_state_ids.len(), + ancestry_ids.len(), + ] { + if column != decoded.currents.len() { + return Err(integrity( + "retained current metadata has the wrong row count", + )); + } + } + + let mut source_parameter_start = 0usize; + let mut source_ir_by_momentum_slot = BTreeMap::new(); + let mut source_ir_by_canonical_id = BTreeMap::new(); + decoded + .sources + .iter() + .enumerate() + .map(|(index, row)| { + let current_index = row.current_id as usize; + let current = decoded + .currents + .get(current_index) + .ok_or_else(|| integrity("eager source current is absent"))?; + let value = decoded + .values + .get(row.value_slot_id as usize) + .ok_or_else(|| integrity("eager source value slot is absent"))?; + let particle = physics + .external_particles + .get(row.input_momentum_slot as usize) + .ok_or_else(|| integrity("eager source external particle is absent"))?; + let source_ir: GenericSourceIrManifest = + canonical_ir(decoded, row.source_ir_id, "source IR")?; + let applied_crossing: GenericCrossingIrManifest = + canonical_ir(decoded, row.crossing_ir_id, "source crossing IR")?; + let declared = source_ir + .states + .get(row.declared_state_index as usize) + .ok_or_else(|| integrity("eager source declared state is absent"))?; + let state = declared + .transformed(&applied_crossing) + .map_err(|message| integrity(format!("invalid eager source state: {message}")))?; + let crossing_factor = decoded + .exact_factors + .get(row.crossing_factor_id as usize) + .ok_or_else(|| integrity("eager source crossing factor is absent"))?; + if crossing_factor.real_bits != applied_crossing.phase[0].to_bits() + || crossing_factor.imaginary_bits != applied_crossing.phase[1].to_bits() + { + return Err(integrity( + "eager source crossing factor disagrees with its IR", + )); + } + let expected_outgoing = match particle.role { + crate::ParticleRole::Initial => source_ir.identity.anti_pdg_label, + crate::ParticleRole::Final => source_ir.identity.pdg_label, + }; + if particle.label != row.external_label as usize + || particle.momentum_slot != row.input_momentum_slot as usize + || particle.pdg != expected_outgoing + || particle_ids[current_index] != source_ir.identity.pdg_label + || source_labels[current_index] != row.external_label + || source_helicities[current_index] != state.helicity + || chiralities[current_index] != state.chirality + || current.component_count as usize != source_ir.component_dimension + || value.component_count != current.component_count + || value.current_id != row.current_id + { + return Err(integrity( + "eager source metadata is internally inconsistent", + )); + } + let retained_spin = i32_sequence(decoded, spin_state_ids[current_index])?; + let expected_spin = match &state.spin_state { + GenericSourceSpinStateManifest::Scalar(value) => vec![*value], + GenericSourceSpinStateManifest::Components(values) => values.clone(), + }; + if retained_spin != expected_spin { + return Err(integrity( + "eager source spin state disagrees with retained metadata", + )); + } + bind_source_mass( + &source_ir, + runtime_slots, + parameter_values, + particle_masses, + mass_names, + )?; + let dimension = source_ir.component_dimension; + let current_start = usize_count(current.component_start, "source current start")?; + let value_start = usize_count(value.component_start, "source value start")?; + let source_parameter_stop = source_parameter_start + .checked_add(dimension) + .ok_or_else(|| RusticolError::artifact("source parameter layout overflows"))?; + let side = match particle.role { + crate::ParticleRole::Initial => "initial", + crate::ParticleRole::Final => "final", + }; + let source = GenericSourceRecordManifest { + source_id: index, + current_id: current_index, + current_component_start: current_start, + current_component_stop: current_start + dimension, + value_slot: GenericValueSlotRefManifest { + value_slot_id: row.value_slot_id as usize, + current_id: current_index, + variant: "source".to_owned(), + component_start: value_start, + component_stop: value_start + dimension, + dimension, + }, + source_parameter_start, + source_parameter_stop, + leg_label: row.external_label as usize, + input_momentum_slot: row.input_momentum_slot as usize, + side: side.to_owned(), + crossing: applied_crossing + .momentum_transform + .legacy_projection() + .to_owned(), + physical_pdg: particle.pdg, + outgoing_pdg: source_ir.identity.pdg_label, + particle_id: source_ir.identity.pdg_label, + anti_particle_id: source_ir.identity.anti_pdg_label, + source_kind: "external-wavefunction".to_owned(), + wavefunction_kind: source_ir.wavefunction_family.as_str().to_owned(), + source_orientation: source_ir.identity.orientation, + source_basis: source_ir.basis.clone(), + source_ir, + applied_crossing, + source_helicity: state.helicity, + chirality: state.chirality, + spin_state: spin_state_value(&state.spin_state), + dimension, + helicity_ancestry: Value::String(bitset_decimal( + decoded, + ancestry_ids[current_index], + )?), + color_state: json!({"accuracy": manifest_color_accuracy(physics)}), + }; + super::validation::validate_source_wavefunction_metadata(index, &source)?; + super::validation::validate_consistent_source_ir( + &mut source_ir_by_momentum_slot, + &mut source_ir_by_canonical_id, + index, + &source, + )?; + source_parameter_start = source_parameter_stop; + Ok(source) + }) + .collect() +} + +fn bind_source_mass( + source_ir: &GenericSourceIrManifest, + runtime_slots: &BTreeMap, + parameter_values: &[f64], + particle_masses: &mut BTreeMap, + mass_names: &mut BTreeMap, +) -> RusticolResult<()> { + let Some(name) = source_ir.mass_parameter.as_ref() else { + return Ok(()); + }; + let slots = runtime_slots.get(name).ok_or_else(|| { + integrity(format!( + "massive eager source references unbound mass parameter {name:?}" + )) + })?; + if slots + .imaginary + .and_then(|index| parameter_values.get(index)) + .is_some_and(|value| *value != 0.0) + { + return Err(integrity( + "eager source mass parameter has a nonzero imaginary component", + )); + } + let mass = *parameter_values + .get(slots.real) + .ok_or_else(|| integrity("eager source mass parameter slot is absent"))?; + if !mass.is_finite() || mass < 0.0 { + return Err(integrity("eager source mass parameter is invalid")); + } + for pdg in [ + source_ir.identity.pdg_label, + source_ir.identity.anti_pdg_label, + ] { + if particle_masses + .insert(pdg, mass) + .is_some_and(|previous| previous.to_bits() != mass.to_bits()) + || mass_names + .insert(pdg, name.clone()) + .is_some_and(|previous| previous != *name) + { + return Err(integrity("eager source has conflicting mass bindings")); + } + } + Ok(()) +} + +fn build_momentum_slots( + decoded: &DecodedEagerRuntimeV3, + external_count: usize, +) -> RusticolResult> { + decoded + .momenta + .iter() + .map(|row| { + if row.component_count != 4 { + return Err(integrity( + "eager momentum slot does not contain four components", + )); + } + let external_labels = bitset_labels(decoded, row.bitset_id, external_count)?; + if external_labels.is_empty() { + return Err(integrity("eager momentum slot has an empty external mask")); + } + let start = usize_count(row.component_start, "momentum component start")?; + Ok(GenericMomentumSlotManifest { + momentum_slot_id: row.momentum_slot_id as usize, + momentum_mask: bitset_u64(decoded, row.bitset_id)?, + external_labels, + component_start: start, + component_stop: start + 4, + real_valued: true, + }) + }) + .collect() +} + +fn validate_physics_parameters( + runtime: &[GenericRuntimeModelParameterManifest], + physics: &ProcessPhysicsV1, +) -> RusticolResult<()> { + let mut projected = BTreeMap::::new(); + for parameter in runtime { + let name = parameter + .runtime_name + .as_ref() + .unwrap_or(¶meter.name) + .clone(); + let kind = public_parameter_kind(¶meter.kind)?; + let entry = projected.entry(name).or_insert((kind, 0.0, 0.0)); + if entry.0 != kind { + return Err(integrity("eager logical parameter has inconsistent kinds")); + } + match parameter.complex_component.as_deref() { + Some("imag") => entry.2 = parameter.default, + _ => entry.1 = parameter.default, + } + } + if projected.len() != physics.model_parameters.len() { + return Err(integrity( + "eager runtime and resolved physics parameter counts disagree", + )); + } + for parameter in &physics.model_parameters { + let Some((kind, real, imaginary)) = projected.get(¶meter.name) else { + return Err(integrity( + "resolved physics parameter is absent from eager runtime", + )); + }; + if *kind != parameter.kind + || real.to_bits() != parameter.default_real.to_bits() + || imaginary.to_bits() != parameter.default_imaginary.to_bits() + { + return Err(integrity(format!( + "eager runtime parameter {:?} disagrees with resolved physics: \ + runtime kind={kind:?} defaults=({:#018x},{:#018x}), \ + physics kind={:?} defaults=({:#018x},{:#018x})", + parameter.name, + real.to_bits(), + imaginary.to_bits(), + parameter.kind, + parameter.default_real.to_bits(), + parameter.default_imaginary.to_bits(), + ))); + } + } + Ok(()) +} + +fn public_parameter_kind(kind: &str) -> RusticolResult { + match kind { + "normalization" => Ok(crate::ParameterKind::Normalization), + "particle_mass" => Ok(crate::ParameterKind::Mass), + "particle_width" => Ok(crate::ParameterKind::Width), + "coupling_component" => Ok(crate::ParameterKind::Coupling), + "external_parameter" | "external_parameter_component" => Ok(crate::ParameterKind::External), + "derived_parameter_component" => Ok(crate::ParameterKind::Derived), + _ => Err(RusticolError::compatibility(format!( + "unsupported eager model-parameter kind {kind:?}" + ))), + } +} + +#[derive(Deserialize)] +struct CanonicalNormalization { + #[serde(default = "one")] + color_factor: f64, + #[serde(default = "one")] + global_coupling_factor: f64, + #[serde(default = "one")] + average_factor: f64, + #[serde(default = "one")] + identical_factor: f64, + #[serde(default)] + qcd_coupling_power: usize, + #[serde(default)] + electroweak_coupling_power: usize, +} + +impl CanonicalNormalization { + fn validate(&self) -> RusticolResult<()> { + if !self.color_factor.is_finite() + || !self.global_coupling_factor.is_finite() + || !self.average_factor.is_finite() + || !self.identical_factor.is_finite() + || self.average_factor <= 0.0 + || self.identical_factor <= 0.0 + { + return Err(integrity("invalid eager runtime normalization IR")); + } + Ok(()) + } +} + +fn one() -> f64 { + 1.0 +} + +struct RetainedTables<'a> { + tables: &'a [DecodedEagerRetainedTable], +} + +impl<'a> RetainedTables<'a> { + fn new(tables: &'a [DecodedEagerRetainedTable]) -> Self { + Self { tables } + } + + fn table(&self, name: &str) -> RusticolResult<&'a DecodedEagerRetainedTable> { + let mut matches = self + .tables + .iter() + .filter(|table| table.name.as_ref() == name); + let table = matches + .next() + .ok_or_else(|| integrity(format!("retained eager table {name:?} is absent")))?; + if matches.next().is_some() { + return Err(integrity(format!( + "retained eager table {name:?} is duplicated" + ))); + } + Ok(table) + } + + fn scalar_u32(&self, table: &DecodedEagerRetainedTable, name: &str) -> RusticolResult { + let values = self.u32_column(table, name, 1)?; + if table.row_count != 1 || values.len() != 1 { + return Err(integrity(format!( + "retained eager scalar {}.{name} has the wrong shape", + table.name + ))); + } + Ok(values[0]) + } + + fn scalar_u8(&self, table: &DecodedEagerRetainedTable, name: &str) -> RusticolResult { + let values = self.u8_column(table, name, 1)?; + if table.row_count != 1 || values.len() != 1 { + return Err(integrity(format!( + "retained eager scalar {}.{name} has the wrong shape", + table.name + ))); + } + Ok(values[0]) + } + + fn scalar_u64(&self, table: &DecodedEagerRetainedTable, name: &str) -> RusticolResult { + let values = self.u64_column(table, name, 1)?; + if table.row_count != 1 || values.len() != 1 { + return Err(integrity(format!( + "retained eager scalar {}.{name} has the wrong shape", + table.name + ))); + } + Ok(values[0]) + } + + fn u8_column( + &self, + table: &'a DecodedEagerRetainedTable, + name: &str, + elements_per_row: u32, + ) -> RusticolResult<&'a [u8]> { + let column = retained_column(table, name, elements_per_row)?; + match &column.values { + DecodedEagerPrimitiveColumn::U8(values) => Ok(values), + _ => Err(integrity(format!( + "retained eager column {}.{name} is not u8", + table.name + ))), + } + } + + fn u32_column( + &self, + table: &'a DecodedEagerRetainedTable, + name: &str, + elements_per_row: u32, + ) -> RusticolResult<&'a [u32]> { + let column = retained_column(table, name, elements_per_row)?; + match &column.values { + DecodedEagerPrimitiveColumn::U32(values) => Ok(values), + _ => Err(integrity(format!( + "retained eager column {}.{name} is not u32", + table.name + ))), + } + } + + fn i32_column( + &self, + table: &'a DecodedEagerRetainedTable, + name: &str, + elements_per_row: u32, + ) -> RusticolResult<&'a [i32]> { + let column = retained_column(table, name, elements_per_row)?; + match &column.values { + DecodedEagerPrimitiveColumn::I32(values) => Ok(values), + _ => Err(integrity(format!( + "retained eager column {}.{name} is not i32", + table.name + ))), + } + } + + fn u64_column( + &self, + table: &'a DecodedEagerRetainedTable, + name: &str, + elements_per_row: u32, + ) -> RusticolResult<&'a [u64]> { + let column = retained_column(table, name, elements_per_row)?; + match &column.values { + DecodedEagerPrimitiveColumn::U64(values) => Ok(values), + _ => Err(integrity(format!( + "retained eager column {}.{name} is not u64", + table.name + ))), + } + } +} + +fn retained_column<'a>( + table: &'a DecodedEagerRetainedTable, + name: &str, + elements_per_row: u32, +) -> RusticolResult<&'a super::eager_v3_decode::DecodedEagerRetainedColumn> { + let mut matches = table + .columns + .iter() + .filter(|column| column.name.as_ref() == name); + let column = matches.next().ok_or_else(|| { + integrity(format!( + "retained eager column {}.{name} is absent", + table.name + )) + })?; + if matches.next().is_some() || column.elements_per_row != elements_per_row { + return Err(integrity(format!( + "retained eager column {}.{name} has a non-canonical shape", + table.name + ))); + } + Ok(column) +} + +fn canonical_ir Deserialize<'de>>( + decoded: &DecodedEagerRuntimeV3, + id: u32, + context: &str, +) -> RusticolResult { + let payload = decoded + .exact_ir + .get(id as usize) + .ok_or_else(|| integrity(format!("{context} references absent canonical IR {id}")))?; + serde_json::from_str(payload).map_err(|error| { + RusticolError::serialization(format!( + "could not parse eager {context} canonical IR {id}: {error}" + )) + }) +} + +fn string<'a>( + decoded: &'a DecodedEagerRuntimeV3, + id: u32, + context: &str, +) -> RusticolResult<&'a str> { + decoded + .strings + .get(id as usize) + .map(AsRef::as_ref) + .ok_or_else(|| integrity(format!("{context} string {id} is absent"))) +} + +fn optional_string(decoded: &DecodedEagerRuntimeV3, id: u32) -> RusticolResult> { + if id == crate::MISSING_U32 { + Ok(None) + } else { + string(decoded, id, "optional parameter").map(Some) + } +} + +fn i32_sequence(decoded: &DecodedEagerRuntimeV3, id: u32) -> RusticolResult> { + let range = decoded + .i32_sequence_ranges + .get(id as usize) + .ok_or_else(|| integrity("retained current references absent i32 sequence"))?; + checked_catalog_range( + &decoded.i32_sequence_values, + range.start, + range.count, + "i32 sequence", + ) + .map(<[i32]>::to_vec) +} + +fn u32_sequence( + decoded: &DecodedEagerRuntimeV3, + id: u32, + context: &str, +) -> RusticolResult> { + let range = decoded + .u32_sequence_ranges + .get(id as usize) + .ok_or_else(|| integrity(format!("eager metadata references absent {context}")))?; + checked_catalog_range( + &decoded.u32_sequence_values, + range.start, + range.count, + context, + ) + .map(<[u32]>::to_vec) +} + +fn bitset_words(decoded: &DecodedEagerRuntimeV3, id: u32) -> RusticolResult<&[u64]> { + let range = decoded + .bitset_ranges + .get(id as usize) + .ok_or_else(|| integrity("eager metadata references absent bitset"))?; + checked_catalog_range(&decoded.bitset_words, range.start, range.count, "bitset") +} + +fn bitset_u64(decoded: &DecodedEagerRuntimeV3, id: u32) -> RusticolResult { + let words = bitset_words(decoded, id)?; + if words.iter().skip(1).any(|word| *word != 0) { + return Err(RusticolError::compatibility( + "eager momentum metadata cannot project a bitset wider than 64 bits", + )); + } + Ok(words.first().copied().unwrap_or(0)) +} + +fn bitset_decimal(decoded: &DecodedEagerRuntimeV3, id: u32) -> RusticolResult { + Ok(bitset_words_decimal(bitset_words(decoded, id)?)) +} + +fn bitset_words_decimal(words: &[u64]) -> String { + const BASE: u128 = 1_000_000_000; + const WORD_RADIX: u128 = 1_u128 << 64; + + let mut decimal_limbs = vec![0_u32]; + for word in words.iter().rev().copied() { + let mut carry = u128::from(word); + for limb in &mut decimal_limbs { + let value = u128::from(*limb) * WORD_RADIX + carry; + *limb = (value % BASE) as u32; + carry = value / BASE; + } + while carry != 0 { + decimal_limbs.push((carry % BASE) as u32); + carry /= BASE; + } + } + + let mut limbs = decimal_limbs.iter().rev(); + let mut result = limbs.next().copied().unwrap_or(0).to_string(); + for limb in limbs { + use std::fmt::Write as _; + write!(&mut result, "{limb:09}").expect("writing to a string cannot fail"); + } + result +} + +fn bitset_labels( + decoded: &DecodedEagerRuntimeV3, + id: u32, + external_count: usize, +) -> RusticolResult> { + let mut labels = Vec::new(); + for (word_index, word) in bitset_words(decoded, id)?.iter().copied().enumerate() { + for bit in 0..64 { + if word & (1_u64 << bit) == 0 { + continue; + } + let index = word_index + .checked_mul(64) + .and_then(|start| start.checked_add(bit)) + .ok_or_else(|| RusticolError::artifact("eager momentum label overflows"))?; + if index >= external_count { + return Err(integrity( + "eager momentum mask exceeds the external-particle count", + )); + } + labels.push(index + 1); + } + } + Ok(labels) +} + +fn checked_catalog_range<'a, T>( + values: &'a [T], + start: u64, + count: u64, + context: &str, +) -> RusticolResult<&'a [T]> { + let start = usize_count(start, context)?; + let count = usize_count(count, context)?; + let stop = start + .checked_add(count) + .ok_or_else(|| RusticolError::artifact(format!("eager {context} range overflows")))?; + values + .get(start..stop) + .ok_or_else(|| integrity(format!("eager {context} range is out of bounds"))) +} + +fn component_count(rows: &[T], context: &str) -> RusticolResult +where + T: ComponentRow, +{ + rows.iter().try_fold(0usize, |total, row| { + total + .checked_add(row.component_count() as usize) + .ok_or_else(|| RusticolError::artifact(format!("eager {context} layout overflows"))) + }) +} + +trait ComponentRow { + fn component_count(&self) -> u32; +} + +impl ComponentRow for crate::eager_lowering_v3::EagerPlanValueRow { + fn component_count(&self) -> u32 { + self.component_count + } +} + +impl ComponentRow for crate::eager_lowering_v3::EagerPlanMomentumRow { + fn component_count(&self) -> u32 { + self.component_count + } +} + +fn spin_state_value(state: &GenericSourceSpinStateManifest) -> Value { + match state { + GenericSourceSpinStateManifest::Scalar(value) => Value::from(*value), + GenericSourceSpinStateManifest::Components(values) => json!(values), + } +} + +fn manifest_color_accuracy(physics: &ProcessPhysicsV1) -> &'static str { + match physics.color_accuracy { + crate::ColorAccuracy::Lc => "lc", + crate::ColorAccuracy::Nlc => "nlc", + crate::ColorAccuracy::Full => "full", + } +} + +fn usize_count(value: u64, context: &str) -> RusticolResult { + usize::try_from(value) + .map_err(|_| RusticolError::artifact(format!("eager {context} exceeds usize"))) +} + +fn integrity(message: impl Into) -> RusticolError { + RusticolError::integrity(message.into()) +} + +#[cfg(test)] +mod tests { + use super::bitset_words_decimal; + + #[test] + fn arbitrary_width_bitsets_project_to_decimal_strings() { + assert_eq!(bitset_words_decimal(&[]), "0"); + assert_eq!(bitset_words_decimal(&[1]), "1"); + assert_eq!(bitset_words_decimal(&[u64::MAX]), "18446744073709551615"); + assert_eq!(bitset_words_decimal(&[0, 1]), "18446744073709551616"); + assert_eq!(bitset_words_decimal(&[1, 2]), "36893488147419103233"); + } +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/engine/eager_v3_decode.rs b/pyamplicol/rust/crates/rusticol-core/src/engine/eager_v3_decode.rs new file mode 100644 index 0000000000..f5bd5d3532 --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/engine/eager_v3_decode.rs @@ -0,0 +1,3271 @@ +// SPDX-License-Identifier: 0BSD + +//! Compact decoder for authenticated eager plan-v3 PACBIN containers. +//! +//! The decoder borrows each PACBIN member while checking its fixed-width wire +//! contract, then allocates only the final typed runtime/exact structures. It +//! deliberately does not recreate the former expanded JSON `runtime_schema`. + +use super::eager_manifest::PreparedKernelPackManifest; +use super::eager_v3_manifest::EagerV3ExecutionManifest; +use crate::eager_layout::{ + EAGER_LOWERING_INPUT_ABI, EAGER_PLAN_ABI, EAGER_RUNTIME_CAPABILITY, + EAGER_RUNTIME_CONTAINER_KIND, EAGER_RUNTIME_CONTAINER_SCHEMA, EAGER_RUNTIME_LAYOUT_ABI, + EagerSectionHeader, EagerSectionKind, +}; +use crate::eager_lowering_v3::{ + EagerPlanAttachmentRow, EagerPlanCatalogRangeRow, EagerPlanClosureRow, + EagerPlanColorSelectorRow, EagerPlanCouplingRow, EagerPlanCurrentRow, + EagerPlanDirectCoefficientRow, EagerPlanExactFactorRow, EagerPlanFinalizationRow, + EagerPlanHelicitySelectorRow, EagerPlanInvocationRow, EagerPlanMomentumRow, + EagerPlanParameterRow, EagerPlanReductionEntryKind, EagerPlanReductionEntryRow, + EagerPlanReductionGroupRow, EagerPlanSelectorDomainRow, EagerPlanSourceFillRow, + EagerPlanStageRow, EagerPlanValueRow, EagerValueSlotKind, +}; +use crate::eager_runtime::{EagerKernelRole, EagerKernelSpec, EagerPlanDimensions}; +use crate::pacbin::PacbinReader; +use crate::{MISSING_U32, RusticolError, RusticolResult}; +use std::collections::{BTreeMap, BTreeSet, HashSet}; + +const SERIALIZATION_SCHEMA: u32 = 1; +const METADATA_RECORD_SIZE: u32 = 224; +const INSPECTION_RECORD_SIZE: u32 = 160; +const RETAINED_TABLE_RECORD_SIZE: u32 = 40; +const RETAINED_COLUMN_RECORD_SIZE: u32 = 40; +const DIRECT_COEFFICIENT_RECORD_SIZE: u32 = 16; +const HELICITY_SELECTOR_RECORD_SIZE: u32 = 24; +const COLOR_SELECTOR_RECORD_SIZE: u32 = 24; + +const RETAINED_U8: u8 = 1; +const RETAINED_U32: u8 = 2; +const RETAINED_U64: u8 = 3; +const RETAINED_I32: u8 = 4; +const RETAINED_F64_BITS: u8 = 5; + +const IDENTITY_PROCESS_KEY: usize = 5; +const IDENTITY_MODEL_NAME: usize = 6; +const IDENTITY_COUNT: usize = 7; + +/// One retained primitive column used by exact execution and inspection. +#[derive(Clone, Debug, Eq, PartialEq)] +pub(super) enum DecodedEagerPrimitiveColumn { + U8(Vec), + U32(Vec), + U64(Vec), + I32(Vec), + /// Raw binary64 bits preserve signed zero and NaN payloads. + F64Bits(Vec), + /// Authenticated and shape-validated retained data not needed by f64 load. + ValidatedOnly { + len: usize, + }, +} + +impl DecodedEagerPrimitiveColumn { + fn len(&self) -> usize { + match self { + Self::U8(values) => values.len(), + Self::U32(values) => values.len(), + Self::U64(values) | Self::F64Bits(values) => values.len(), + Self::I32(values) => values.len(), + Self::ValidatedOnly { len } => *len, + } + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub(super) struct DecodedEagerRetainedColumn { + pub(super) name: Box, + pub(super) elements_per_row: u32, + pub(super) values: DecodedEagerPrimitiveColumn, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub(super) struct DecodedEagerRetainedTable { + pub(super) name: Box, + pub(super) row_count: u64, + pub(super) columns: Vec, +} + +/// Final compact owned form consumed by the plan-v3 runtime adapter. +#[derive(Clone, Debug, PartialEq)] +pub(super) struct DecodedEagerRuntimeV3 { + pub(super) process_key: Box, + pub(super) model_name: Box, + pub(super) runtime_options: DecodedEagerRuntimeOptions, + pub(super) dimensions: EagerPlanDimensions, + pub(super) kernel_specs: Vec, + pub(super) strings: Vec>, + pub(super) exact_ir: Vec>, + pub(super) semantic_limitations: Vec>, + pub(super) retained_tables: Vec, + pub(super) currents: Vec, + pub(super) values: Vec, + pub(super) momenta: Vec, + pub(super) sources: Vec, + pub(super) parameters: Vec, + pub(super) stages: Vec, + pub(super) couplings: Vec, + pub(super) invocations: Vec, + pub(super) attachments: Vec, + pub(super) finalizations: Vec, + pub(super) closures: Vec, + pub(super) direct_coefficients: Vec, + pub(super) selector_domains: Vec, + pub(super) selector_memberships: Vec, + pub(super) helicity_selectors: Vec, + pub(super) color_selectors: Vec, + pub(super) reduction_groups: Vec, + pub(super) reduction_entries: Vec, + pub(super) exact_factors: Vec, + pub(super) bitset_ranges: Vec, + pub(super) bitset_populations: Vec, + pub(super) bitset_words: Vec, + pub(super) u32_sequence_ranges: Vec, + pub(super) u32_sequence_values: Vec, + pub(super) i32_sequence_ranges: Vec, + pub(super) i32_sequence_values: Vec, + pub(super) color_contraction_entry_start: u64, + pub(super) color_contraction_entry_count: u64, +} + +/// Reduction-only owned form used to hydrate compact public physics metadata. +/// +/// The enclosing PACBIN has already been authenticated in full. This view +/// materializes only reduction rows, selector rows, and selector-referenced +/// sequence values; all unrelated runtime tables and global sequence-catalog +/// entries are limited to borrowed fixed-width validation. +#[derive(Debug)] +pub(super) struct DecodedEagerReductionGroupsV3 { + pub(super) helicity_selectors: Vec, + pub(super) color_selectors: Vec, + pub(super) reduction_groups: Vec, + pub(super) reduction_entries: Vec, + pub(super) helicity_sequences: BTreeMap>, + pub(super) color_sequences: BTreeMap>, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(super) struct DecodedEagerRuntimeOptions { + pub(super) point_tile_size: usize, + pub(super) workspace_bytes: usize, +} + +#[derive(Clone, Copy, Debug)] +struct Metadata { + retained_column_count: u64, + current_component_count: u64, + value_component_count: u64, + momentum_component_count: u64, + color_contraction_entry_start: u64, + color_contraction_entry_count: u64, + string_count: u64, + exact_ir_count: u64, + limitation_count: u64, + retained_table_count: u64, + current_count: u64, + value_count: u64, + momentum_count: u64, + source_count: u64, + stage_count: u64, + invocation_count: u64, + closure_count: u64, + direct_coefficient_count: u64, + helicity_selector_count: u64, + color_selector_count: u64, + exact_factor_count: u64, +} + +#[derive(Clone, Copy, Debug)] +struct RetainedTableRecord { + table_id: u32, + name_id: u32, + row_count: u64, + column_start: u64, + column_count: u64, +} + +#[derive(Clone, Copy, Debug)] +struct RetainedColumnRecord { + table_id: u32, + column_id: u32, + name_id: u32, + primitive_kind: u8, + elements_per_row: u32, + value_start: u64, + value_count: u64, +} + +struct RetainedPrimitiveSections<'a> { + u8_values: DecodedSection<'a>, + u32_values: DecodedSection<'a>, + u64_values: DecodedSection<'a>, + i32_values: DecodedSection<'a>, + f64_values: DecodedSection<'a>, +} + +#[derive(Clone, Copy)] +struct CountedPlanSection { + path: &'static str, + kind: EagerSectionKind, + record_size: u32, +} + +const COUNTED_PLAN_SECTIONS: [CountedPlanSection; 20] = [ + CountedPlanSection { + path: "tables/currents.bin", + kind: EagerSectionKind::CurrentLayout, + record_size: EagerPlanCurrentRow::ENCODED_LEN, + }, + CountedPlanSection { + path: "tables/values.bin", + kind: EagerSectionKind::ValueLayout, + record_size: EagerPlanValueRow::ENCODED_LEN, + }, + CountedPlanSection { + path: "tables/momenta.bin", + kind: EagerSectionKind::MomentumLayout, + record_size: EagerPlanMomentumRow::ENCODED_LEN, + }, + CountedPlanSection { + path: "tables/sources.bin", + kind: EagerSectionKind::SourceFill, + record_size: EagerPlanSourceFillRow::ENCODED_LEN, + }, + CountedPlanSection { + path: "tables/parameters.bin", + kind: EagerSectionKind::ParameterLayout, + record_size: EagerPlanParameterRow::ENCODED_LEN, + }, + CountedPlanSection { + path: "tables/stages.bin", + kind: EagerSectionKind::Stages, + record_size: EagerPlanStageRow::ENCODED_LEN, + }, + CountedPlanSection { + path: "tables/couplings.bin", + kind: EagerSectionKind::Couplings, + record_size: EagerPlanCouplingRow::ENCODED_LEN, + }, + CountedPlanSection { + path: "tables/invocations.bin", + kind: EagerSectionKind::Invocations, + record_size: EagerPlanInvocationRow::ENCODED_LEN, + }, + CountedPlanSection { + path: "tables/attachments.bin", + kind: EagerSectionKind::Attachments, + record_size: EagerPlanAttachmentRow::ENCODED_LEN, + }, + CountedPlanSection { + path: "tables/finalizations.bin", + kind: EagerSectionKind::Finalizations, + record_size: EagerPlanFinalizationRow::ENCODED_LEN, + }, + CountedPlanSection { + path: "tables/closures.bin", + kind: EagerSectionKind::Closures, + record_size: EagerPlanClosureRow::ENCODED_LEN, + }, + CountedPlanSection { + path: "tables/direct-coefficients.bin", + kind: EagerSectionKind::Closures, + record_size: DIRECT_COEFFICIENT_RECORD_SIZE, + }, + CountedPlanSection { + path: "selectors/domains.bin", + kind: EagerSectionKind::SelectorDomains, + record_size: EagerPlanSelectorDomainRow::ENCODED_LEN, + }, + CountedPlanSection { + path: "selectors/memberships.bin", + kind: EagerSectionKind::SelectorMemberships, + record_size: 4, + }, + CountedPlanSection { + path: "selectors/helicities.bin", + kind: EagerSectionKind::SelectorDomains, + record_size: HELICITY_SELECTOR_RECORD_SIZE, + }, + CountedPlanSection { + path: "selectors/colors.bin", + kind: EagerSectionKind::SelectorDomains, + record_size: COLOR_SELECTOR_RECORD_SIZE, + }, + CountedPlanSection { + path: "reductions/groups.bin", + kind: EagerSectionKind::ReductionGroups, + record_size: EagerPlanReductionGroupRow::ENCODED_LEN, + }, + CountedPlanSection { + path: "reductions/entries.bin", + kind: EagerSectionKind::ReductionEntries, + record_size: EagerPlanReductionEntryRow::ENCODED_LEN, + }, + CountedPlanSection { + path: "catalogs/exact-factors.bin", + kind: EagerSectionKind::ExactFactors, + record_size: EagerPlanExactFactorRow::ENCODED_LEN, + }, + CountedPlanSection { + path: "retained/tables.bin", + kind: EagerSectionKind::Metadata, + record_size: RETAINED_TABLE_RECORD_SIZE, + }, +]; + +/// Decode only the authenticated rows required to hydrate native reduction +/// groups. Runtime-sized DAG tables are never converted into Rust rows. +pub(super) fn decode_eager_v3_reduction_groups( + reader: &PacbinReader, + manifest: &EagerV3ExecutionManifest, +) -> RusticolResult { + let metadata = decode_metadata(reader)?; + let inspection = decode_inspection(reader)?; + let identity = decode_text_catalog(reader, "metadata/identity", EagerSectionKind::Metadata)?; + validate_identity(&identity, manifest)?; + validate_reduction_plan_metadata(reader, manifest, metadata, &inspection)?; + + let helicity_selectors = decode_helicity_selectors(reader)?; + let color_selectors = decode_color_selectors(reader)?; + let reduction_groups = decode_reduction_groups(reader)?; + let reduction_entries = decode_reduction_entries(reader)?; + check_count( + "reduction-only helicity selector", + helicity_selectors.len(), + inspection[14], + )?; + check_count( + "reduction-only color selector", + color_selectors.len(), + inspection[15], + )?; + check_count( + "reduction-only reduction group", + reduction_groups.len(), + inspection[16], + )?; + check_count( + "reduction-only reduction entry", + reduction_entries.len(), + inspection[17], + )?; + + let helicity_sequence_ids = helicity_selectors + .iter() + .flat_map(|row| [row.values_sequence_id, row.representative_sequence_id]) + .collect::>(); + let color_sequence_ids = color_selectors + .iter() + .flat_map(|row| [row.word_sequence_id, row.representative_word_sequence_id]) + .collect::>(); + let helicity_sequences = decode_referenced_sequences( + reader, + "catalogs/i32-sequences/ranges.bin", + "catalogs/i32-sequences/values.bin", + &helicity_sequence_ids, + "helicity i32 sequence", + |row| read_i32(row, 0), + )?; + let color_sequences = decode_referenced_sequences( + reader, + "catalogs/u32-sequences/ranges.bin", + "catalogs/u32-sequences/values.bin", + &color_sequence_ids, + "color u32 sequence", + |row| read_u32(row, 0), + )?; + validate_reduction_subset_semantics( + metadata, + inspection[18], + &helicity_selectors, + &color_selectors, + &reduction_groups, + &reduction_entries, + &helicity_sequences, + &color_sequences, + )?; + + Ok(DecodedEagerReductionGroupsV3 { + helicity_selectors, + color_selectors, + reduction_groups, + reduction_entries, + helicity_sequences, + color_sequences, + }) +} + +/// Decode one already authenticated/preflighted eager plan-v3 container. +/// +/// `pack` must be the validated prepared pack referenced by the execution +/// manifest. The decoder resolves its final kernel specs and rejects every +/// plan reference whose prepared-kernel role is incompatible. +pub(super) fn decode_eager_v3_runtime( + reader: &PacbinReader, + manifest: &EagerV3ExecutionManifest, + pack: &PreparedKernelPackManifest, +) -> RusticolResult { + let metadata = decode_metadata(reader)?; + let inspection = decode_inspection(reader)?; + let identity = decode_text_catalog(reader, "metadata/identity", EagerSectionKind::Metadata)?; + validate_identity(&identity, manifest)?; + + let strings = decode_text_catalog(reader, "catalogs/strings", EagerSectionKind::Metadata)?; + let exact_ir = decode_text_catalog(reader, "catalogs/exact-ir", EagerSectionKind::Metadata)?; + let semantic_limitations = decode_text_catalog( + reader, + "catalogs/semantic-limitations", + EagerSectionKind::Metadata, + )?; + check_count("string catalog", strings.len(), metadata.string_count)?; + check_count("exact IR catalog", exact_ir.len(), metadata.exact_ir_count)?; + check_count( + "semantic limitation catalog", + semantic_limitations.len(), + metadata.limitation_count, + )?; + + let retained_tables = decode_retained_tables(reader)?; + check_count( + "retained table catalog", + retained_tables.len(), + metadata.retained_table_count, + )?; + let retained_column_count = retained_tables.iter().try_fold(0_usize, |total, table| { + total + .checked_add(table.columns.len()) + .ok_or_else(|| RusticolError::artifact("decoded retained column count exceeds usize")) + })?; + check_count( + "retained column catalog", + retained_column_count, + metadata.retained_column_count, + )?; + + let currents = decode_currents(reader)?; + let values = decode_values(reader)?; + let momenta = decode_momenta(reader)?; + let sources = decode_sources(reader)?; + let parameters = decode_parameters(reader)?; + let stages = decode_stages(reader)?; + let couplings = decode_couplings(reader)?; + let invocations = decode_invocations(reader)?; + let attachments = decode_attachments(reader)?; + let finalizations = decode_finalizations(reader)?; + let closures = decode_closures(reader)?; + let direct_coefficients = decode_direct_coefficients(reader)?; + let selector_domains = decode_selector_domains(reader)?; + let selector_memberships = decode_u32_section( + reader, + "selectors/memberships.bin", + EagerSectionKind::SelectorMemberships, + )?; + let helicity_selectors = decode_helicity_selectors(reader)?; + let color_selectors = decode_color_selectors(reader)?; + let reduction_groups = decode_reduction_groups(reader)?; + let reduction_entries = decode_reduction_entries(reader)?; + let exact_factors = decode_exact_factors(reader)?; + let bitset_ranges = decode_catalog_ranges(reader, "catalogs/bitsets/ranges.bin")?; + let bitset_populations = decode_u64_section( + reader, + "catalogs/bitsets/populations.bin", + EagerSectionKind::Metadata, + )?; + let bitset_words = decode_u64_section( + reader, + "catalogs/bitsets/words.bin", + EagerSectionKind::Metadata, + )?; + let u32_sequence_ranges = decode_catalog_ranges(reader, "catalogs/u32-sequences/ranges.bin")?; + let u32_sequence_values = decode_u32_section( + reader, + "catalogs/u32-sequences/values.bin", + EagerSectionKind::Metadata, + )?; + let i32_sequence_ranges = decode_catalog_ranges(reader, "catalogs/i32-sequences/ranges.bin")?; + let i32_sequence_values = decode_i32_section( + reader, + "catalogs/i32-sequences/values.bin", + EagerSectionKind::Metadata, + )?; + + validate_declared_counts( + manifest, + metadata, + &inspection, + ¤ts, + &values, + &momenta, + &sources, + ¶meters, + &stages, + &couplings, + &invocations, + &attachments, + &finalizations, + &closures, + &direct_coefficients, + &selector_domains, + &selector_memberships, + &helicity_selectors, + &color_selectors, + &reduction_groups, + &reduction_entries, + &exact_factors, + retained_tables.len(), + )?; + + validate_catalog_ranges("bitset", &bitset_ranges, bitset_words.len())?; + if bitset_ranges.len() != bitset_populations.len() { + return Err(integrity( + "bitset range and population catalogs have different lengths", + )); + } + for (index, (range, expected)) in bitset_ranges.iter().zip(&bitset_populations).enumerate() { + let words = checked_range(&bitset_words, range.start, range.count, "bitset words")?; + let actual = words + .iter() + .map(|word| u64::from(word.count_ones())) + .sum::(); + if actual != *expected { + return Err(integrity(format!( + "bitset {index} population {expected} does not match its {actual} set bits" + ))); + } + } + validate_catalog_ranges( + "u32 sequence", + &u32_sequence_ranges, + u32_sequence_values.len(), + )?; + validate_catalog_ranges( + "i32 sequence", + &i32_sequence_ranges, + i32_sequence_values.len(), + )?; + + let mut kernel_specs = pack.kernel_specs()?; + validate_semantics( + metadata, + manifest.dag_summary.interaction_count, + ¤ts, + &values, + &momenta, + &sources, + ¶meters, + &stages, + &couplings, + &invocations, + &attachments, + &finalizations, + &closures, + &direct_coefficients, + &selector_domains, + &selector_memberships, + &helicity_selectors, + &color_selectors, + &reduction_groups, + &reduction_entries, + &exact_factors, + &bitset_ranges, + &u32_sequence_ranges, + &i32_sequence_ranges, + &strings, + &exact_ir, + &kernel_specs, + )?; + let active_kernel_ids = active_kernel_ids( + invocations.iter().map(|row| row.kernel_id), + finalizations.iter().map(|row| row.kernel_id), + closures.iter().map(|row| row.kernel_id), + ); + kernel_specs.retain(|kernel| active_kernel_ids.contains(&kernel.kernel_id)); + + let amplitude_count = closures + .iter() + .map(|row| row.amplitude_index) + .max() + .map_or(Ok(0_u32), |maximum| { + maximum + .checked_add(1) + .ok_or_else(|| RusticolError::artifact("eager amplitude count exceeds u32")) + })?; + if amplitude_count == 0 { + return Err(integrity("eager plan-v3 has no amplitude closures")); + } + let dimensions = EagerPlanDimensions { + value_slot_component_counts: values.iter().map(|row| row.component_count).collect(), + momentum_slot_component_counts: momenta.iter().map(|row| row.component_count).collect(), + current_component_counts: currents.iter().map(|row| row.component_count).collect(), + parameter_count: usize_u32(parameters.len(), "parameter count")?, + amplitude_count, + }; + let workspace_mib = usize::try_from(manifest.runtime_options.workspace_mib) + .map_err(|_| RusticolError::artifact("eager workspace MiB does not fit usize"))?; + let workspace_bytes = workspace_mib + .checked_mul(1024 * 1024) + .ok_or_else(|| RusticolError::artifact("eager workspace byte count exceeds usize"))?; + + Ok(DecodedEagerRuntimeV3 { + process_key: identity[IDENTITY_PROCESS_KEY].clone(), + model_name: identity[IDENTITY_MODEL_NAME].clone(), + runtime_options: DecodedEagerRuntimeOptions { + point_tile_size: usize::try_from(manifest.runtime_options.point_tile_size) + .map_err(|_| RusticolError::artifact("eager point tile size does not fit usize"))?, + workspace_bytes, + }, + dimensions, + kernel_specs, + strings, + exact_ir, + semantic_limitations, + retained_tables, + currents, + values, + momenta, + sources, + parameters, + stages, + couplings, + invocations, + attachments, + finalizations, + closures, + direct_coefficients, + selector_domains, + selector_memberships, + helicity_selectors, + color_selectors, + reduction_groups, + reduction_entries, + exact_factors, + bitset_ranges, + bitset_populations, + bitset_words, + u32_sequence_ranges, + u32_sequence_values, + i32_sequence_ranges, + i32_sequence_values, + color_contraction_entry_start: metadata.color_contraction_entry_start, + color_contraction_entry_count: metadata.color_contraction_entry_count, + }) +} + +fn active_kernel_ids( + invocation_ids: impl IntoIterator, + finalization_ids: impl IntoIterator, + closure_ids: impl IntoIterator, +) -> BTreeSet { + invocation_ids + .into_iter() + .chain(finalization_ids) + .chain(closure_ids) + .filter(|kernel_id| *kernel_id != MISSING_U32) + .collect() +} + +fn decode_metadata(reader: &PacbinReader) -> RusticolResult { + let section = section( + reader, + "metadata/core.bin", + EagerSectionKind::Metadata, + METADATA_RECORD_SIZE, + )?; + require_count(§ion, 1, "eager metadata")?; + let row = section.payload; + if read_u32(row, 0)? != SERIALIZATION_SCHEMA + || read_u16(row, 4)? != EAGER_RUNTIME_CONTAINER_SCHEMA + { + return Err(RusticolError::compatibility( + "unsupported eager plan-v3 serialization schema", + )); + } + require_zero(row, 6, 8, "metadata reserved bytes")?; + let identity_ids = [ + read_u32(row, 8)?, + read_u32(row, 12)?, + read_u32(row, 16)?, + read_u32(row, 20)?, + read_u32(row, 24)?, + read_u32(row, 28)?, + read_u32(row, 32)?, + ]; + if identity_ids != [0, 1, 2, 3, 4, 5, 6] || read_u32(row, 36)? != 7 { + return Err(integrity( + "eager metadata identity catalog mapping is not canonical", + )); + } + let mut values = [0_u64; 21]; + for (index, value) in values.iter_mut().enumerate() { + *value = read_u64(row, 40 + index * 8)?; + } + require_zero(row, 208, 224, "metadata trailing reserved bytes")?; + Ok(Metadata { + retained_column_count: values[0], + current_component_count: values[1], + value_component_count: values[2], + momentum_component_count: values[3], + color_contraction_entry_start: values[4], + color_contraction_entry_count: values[5], + string_count: values[6], + exact_ir_count: values[7], + limitation_count: values[8], + retained_table_count: values[9], + current_count: values[10], + value_count: values[11], + momentum_count: values[12], + source_count: values[13], + stage_count: values[14], + invocation_count: values[15], + closure_count: values[16], + direct_coefficient_count: values[17], + helicity_selector_count: values[18], + color_selector_count: values[19], + exact_factor_count: values[20], + }) +} + +fn decode_inspection(reader: &PacbinReader) -> RusticolResult<[u64; 20]> { + let section = section( + reader, + "inspection/summary.bin", + EagerSectionKind::InspectionSummary, + INSPECTION_RECORD_SIZE, + )?; + require_count(§ion, 1, "eager inspection summary")?; + let mut values = [0_u64; 20]; + for (index, value) in values.iter_mut().enumerate() { + *value = read_u64(section.payload, index * 8)?; + } + Ok(values) +} + +fn validate_identity( + identity: &[Box], + manifest: &EagerV3ExecutionManifest, +) -> RusticolResult<()> { + if identity.len() != IDENTITY_COUNT { + return Err(integrity( + "eager identity catalog must contain exactly seven entries", + )); + } + let expected = [ + EAGER_LOWERING_INPUT_ABI, + EAGER_PLAN_ABI, + EAGER_RUNTIME_LAYOUT_ABI, + EAGER_RUNTIME_CAPABILITY, + EAGER_RUNTIME_CONTAINER_KIND, + manifest.key.as_str(), + manifest.plan.inspection_summary.model_name.as_str(), + ]; + for (index, expected) in expected.into_iter().enumerate() { + if identity[index].as_ref() != expected { + return Err(integrity(format!( + "eager identity entry {index} does not match its manifest contract" + ))); + } + } + Ok(()) +} + +fn decode_text_catalog( + reader: &PacbinReader, + base: &str, + kind: EagerSectionKind, +) -> RusticolResult>> { + let (ranges_path, bytes_path) = match base { + "metadata/identity" | "retained/name" => { + (format!("{base}-ranges.bin"), format!("{base}-bytes.bin")) + } + _ => (format!("{base}/ranges.bin"), format!("{base}/bytes.bin")), + }; + let ranges = decode_catalog_ranges(reader, &ranges_path)?; + let bytes = section(reader, &bytes_path, kind, 1)?; + validate_catalog_ranges(base, &ranges, bytes.payload.len())?; + let mut result = Vec::new(); + reserve(&mut result, ranges.len(), base)?; + for range in ranges { + let value = checked_range(bytes.payload, range.start, range.count, base)?; + let text = std::str::from_utf8(value).map_err(|error| { + RusticolError::integrity(format!("{base} contains invalid UTF-8: {error}")) + })?; + result.push(Box::::from(text)); + } + Ok(result) +} + +fn decode_retained_tables(reader: &PacbinReader) -> RusticolResult> { + let names = decode_text_catalog(reader, "retained/name", EagerSectionKind::Metadata)?; + let tables = decode_rows( + reader, + "retained/tables.bin", + EagerSectionKind::Metadata, + RETAINED_TABLE_RECORD_SIZE, + |row| { + require_zero(row, 32, 40, "retained table padding")?; + Ok(RetainedTableRecord { + table_id: read_u32(row, 0)?, + name_id: read_u32(row, 4)?, + row_count: read_u64(row, 8)?, + column_start: read_u64(row, 16)?, + column_count: read_u64(row, 24)?, + }) + }, + )?; + let columns = decode_rows( + reader, + "retained/columns.bin", + EagerSectionKind::Metadata, + RETAINED_COLUMN_RECORD_SIZE, + |row| { + require_zero(row, 13, 16, "retained column padding")?; + require_zero(row, 20, 24, "retained column padding")?; + Ok(RetainedColumnRecord { + table_id: read_u32(row, 0)?, + column_id: read_u32(row, 4)?, + name_id: read_u32(row, 8)?, + primitive_kind: read_u8(row, 12)?, + elements_per_row: read_u32(row, 16)?, + value_start: read_u64(row, 24)?, + value_count: read_u64(row, 32)?, + }) + }, + )?; + let primitive_sections = RetainedPrimitiveSections { + u8_values: section( + reader, + "retained/values-u8.bin", + EagerSectionKind::Metadata, + 1, + )?, + u32_values: section( + reader, + "retained/values-u32.bin", + EagerSectionKind::Metadata, + 4, + )?, + u64_values: section( + reader, + "retained/values-u64.bin", + EagerSectionKind::Metadata, + 8, + )?, + i32_values: section( + reader, + "retained/values-i32.bin", + EagerSectionKind::Metadata, + 4, + )?, + f64_values: section( + reader, + "retained/values-f64-bits.bin", + EagerSectionKind::Metadata, + 8, + )?, + }; + + let mut primitive_cursors = BTreeMap::from([ + (RETAINED_U8, 0_u64), + (RETAINED_U32, 0), + (RETAINED_U64, 0), + (RETAINED_I32, 0), + (RETAINED_F64_BITS, 0), + ]); + let mut result = Vec::new(); + reserve(&mut result, tables.len(), "retained tables")?; + let mut column_cursor = 0_u64; + let mut name_cursor = 0_u32; + for (table_index, table) in tables.iter().enumerate() { + if table.table_id != usize_u32(table_index, "retained table ID")? + || table.column_start != column_cursor + || table.name_id != name_cursor + { + return Err(integrity( + "retained table descriptors are not canonical and contiguous", + )); + } + let table_columns = checked_range( + &columns, + table.column_start, + table.column_count, + "retained table columns", + )?; + let mut decoded_columns = Vec::new(); + reserve( + &mut decoded_columns, + table_columns.len(), + "retained table columns", + )?; + name_cursor = name_cursor + .checked_add(1) + .ok_or_else(|| RusticolError::artifact("retained name count exceeds u32"))?; + let table_name: Box = + required_catalog(&names, table.name_id, "retained table name")?.into(); + for (column_index, column) in table_columns.iter().enumerate() { + if column.table_id != table.table_id + || column.column_id != usize_u32(column_index, "retained column ID")? + || column.name_id != name_cursor + || column.elements_per_row == 0 + { + return Err(integrity("retained column descriptor is not canonical")); + } + let expected_count = table + .row_count + .checked_mul(u64::from(column.elements_per_row)) + .ok_or_else(|| RusticolError::artifact("retained column shape exceeds u64"))?; + if expected_count != column.value_count { + return Err(integrity( + "retained column shape does not match its value count", + )); + } + let cursor = primitive_cursors + .get_mut(&column.primitive_kind) + .ok_or_else(|| { + RusticolError::compatibility(format!( + "unknown retained primitive kind {}", + column.primitive_kind + )) + })?; + if column.value_start != *cursor { + return Err(integrity("retained primitive ranges are not contiguous")); + } + let column_name: Box = + required_catalog(&names, column.name_id, "retained column name")?.into(); + let values = decode_retained_column_values( + &primitive_sections, + column, + retained_column_is_runtime_required(&table_name, &column_name), + )?; + if values.len() != usize_count(column.value_count, "retained value count")? { + return Err(integrity( + "retained decoded value count changed unexpectedly", + )); + } + decoded_columns.push(DecodedEagerRetainedColumn { + name: column_name, + elements_per_row: column.elements_per_row, + values, + }); + *cursor = cursor + .checked_add(column.value_count) + .ok_or_else(|| RusticolError::artifact("retained primitive cursor exceeds u64"))?; + name_cursor = name_cursor + .checked_add(1) + .ok_or_else(|| RusticolError::artifact("retained name count exceeds u32"))?; + } + column_cursor = column_cursor + .checked_add(table.column_count) + .ok_or_else(|| RusticolError::artifact("retained column cursor exceeds u64"))?; + result.push(DecodedEagerRetainedTable { + name: table_name, + row_count: table.row_count, + columns: decoded_columns, + }); + } + if usize_count(column_cursor, "retained column cursor")? != columns.len() + || usize::try_from(name_cursor).ok() != Some(names.len()) + || primitive_cursors[&RETAINED_U8] != primitive_sections.u8_values.count + || primitive_cursors[&RETAINED_U32] != primitive_sections.u32_values.count + || primitive_cursors[&RETAINED_U64] != primitive_sections.u64_values.count + || primitive_cursors[&RETAINED_I32] != primitive_sections.i32_values.count + || primitive_cursors[&RETAINED_F64_BITS] != primitive_sections.f64_values.count + { + return Err(integrity( + "retained catalogs contain unreachable trailing data", + )); + } + Ok(result) +} + +fn retained_column_is_runtime_required(table: &str, column: &str) -> bool { + if table.starts_with("lc_replay_") || table.starts_with("color_replay_") { + return true; + } + matches!( + (table, column), + ("metadata", "normalization_ir_id") + | ("coherent_groups", "color_sector_id") + | ("model_parameters", "pdg") + | ("currents", "particle_id") + | ("currents", "source_leg_label") + | ("currents", "source_helicity") + | ("currents", "chirality") + | ("currents", "spin_state_sequence_id") + | ("currents", "helicity_ancestry_bitset_id") + ) +} + +fn decode_retained_column_values( + sections: &RetainedPrimitiveSections<'_>, + column: &RetainedColumnRecord, + materialize: bool, +) -> RusticolResult { + let (section, width, context) = match column.primitive_kind { + RETAINED_U8 => (§ions.u8_values, 1_usize, "retained u8"), + RETAINED_U32 => (§ions.u32_values, 4, "retained u32"), + RETAINED_U64 => (§ions.u64_values, 8, "retained u64"), + RETAINED_I32 => (§ions.i32_values, 4, "retained i32"), + RETAINED_F64_BITS => (§ions.f64_values, 8, "retained f64 bits"), + _ => unreachable!("primitive kind checked before retained-value decoding"), + }; + let end = column + .value_start + .checked_add(column.value_count) + .ok_or_else(|| RusticolError::artifact(format!("{context} range exceeds u64")))?; + if end > section.count { + return Err(integrity(format!( + "{context} range exceeds its primitive catalog" + ))); + } + let count = usize_count(column.value_count, context)?; + if !materialize { + return Ok(DecodedEagerPrimitiveColumn::ValidatedOnly { len: count }); + } + let start = usize_count(column.value_start, context)?; + let byte_start = start + .checked_mul(width) + .ok_or_else(|| RusticolError::artifact(format!("{context} byte offset exceeds usize")))?; + let byte_count = count + .checked_mul(width) + .ok_or_else(|| RusticolError::artifact(format!("{context} byte count exceeds usize")))?; + let byte_end = byte_start + .checked_add(byte_count) + .ok_or_else(|| RusticolError::artifact(format!("{context} byte range exceeds usize")))?; + let payload = section.payload.get(byte_start..byte_end).ok_or_else(|| { + integrity(format!( + "{context} byte range exceeds its primitive payload" + )) + })?; + match column.primitive_kind { + RETAINED_U8 => Ok(DecodedEagerPrimitiveColumn::U8(payload.to_vec())), + RETAINED_U32 => Ok(DecodedEagerPrimitiveColumn::U32(decode_primitive_rows( + payload, + width, + count, + context, + |row| read_u32(row, 0), + )?)), + RETAINED_U64 => Ok(DecodedEagerPrimitiveColumn::U64(decode_primitive_rows( + payload, + width, + count, + context, + |row| read_u64(row, 0), + )?)), + RETAINED_I32 => Ok(DecodedEagerPrimitiveColumn::I32(decode_primitive_rows( + payload, + width, + count, + context, + |row| read_i32(row, 0), + )?)), + RETAINED_F64_BITS => Ok(DecodedEagerPrimitiveColumn::F64Bits(decode_primitive_rows( + payload, + width, + count, + context, + |row| read_u64(row, 0), + )?)), + _ => unreachable!("primitive kind checked before retained-value materialization"), + } +} + +fn decode_primitive_rows( + payload: &[u8], + width: usize, + count: usize, + context: &str, + mut decode: impl FnMut(&[u8]) -> RusticolResult, +) -> RusticolResult> { + let mut values = Vec::new(); + reserve(&mut values, count, context)?; + for row in payload.chunks_exact(width) { + values.push(decode(row)?); + } + if values.len() != count { + return Err(integrity(format!( + "{context} decoded row count changed unexpectedly" + ))); + } + Ok(values) +} + +fn decode_currents(reader: &PacbinReader) -> RusticolResult> { + decode_rows( + reader, + "tables/currents.bin", + EagerSectionKind::CurrentLayout, + EagerPlanCurrentRow::ENCODED_LEN, + |row| { + Ok(EagerPlanCurrentRow { + current_id: read_u32(row, 0)?, + component_start: read_u64(row, 4)?, + component_count: read_u32(row, 12)?, + momentum_slot_id: read_u32(row, 16)?, + flags: read_u32(row, 20)?, + }) + }, + ) +} + +fn decode_values(reader: &PacbinReader) -> RusticolResult> { + decode_rows( + reader, + "tables/values.bin", + EagerSectionKind::ValueLayout, + EagerPlanValueRow::ENCODED_LEN, + |row| { + require_zero(row, 21, 24, "value row padding")?; + let kind = match read_u8(row, 20)? { + 1 => EagerValueSlotKind::Source, + 2 => EagerValueSlotKind::Unpropagated, + 3 => EagerValueSlotKind::Propagated, + value => { + return Err(RusticolError::compatibility(format!( + "unknown eager value-slot kind {value}" + ))); + } + }; + Ok(EagerPlanValueRow { + value_slot_id: read_u32(row, 0)?, + current_id: read_u32(row, 4)?, + component_start: read_u64(row, 8)?, + component_count: read_u32(row, 16)?, + kind, + }) + }, + ) +} + +fn decode_momenta(reader: &PacbinReader) -> RusticolResult> { + decode_rows( + reader, + "tables/momenta.bin", + EagerSectionKind::MomentumLayout, + EagerPlanMomentumRow::ENCODED_LEN, + |row| { + require_zero(row, 20, 24, "momentum row padding")?; + Ok(EagerPlanMomentumRow { + momentum_slot_id: read_u32(row, 0)?, + bitset_id: read_u32(row, 4)?, + component_start: read_u64(row, 8)?, + component_count: read_u32(row, 16)?, + }) + }, + ) +} + +fn decode_sources(reader: &PacbinReader) -> RusticolResult> { + decode_rows( + reader, + "tables/sources.bin", + EagerSectionKind::SourceFill, + EagerPlanSourceFillRow::ENCODED_LEN, + |row| { + require_zero(row, 36, 40, "source row padding")?; + Ok(EagerPlanSourceFillRow { + source_id: read_u32(row, 0)?, + current_id: read_u32(row, 4)?, + value_slot_id: read_u32(row, 8)?, + external_label: read_u32(row, 12)?, + input_momentum_slot: read_u32(row, 16)?, + source_ir_id: read_u32(row, 20)?, + crossing_ir_id: read_u32(row, 24)?, + crossing_factor_id: read_u32(row, 28)?, + declared_state_index: read_u32(row, 32)?, + }) + }, + ) +} + +fn decode_parameters(reader: &PacbinReader) -> RusticolResult> { + decode_rows( + reader, + "tables/parameters.bin", + EagerSectionKind::ParameterLayout, + EagerPlanParameterRow::ENCODED_LEN, + |row| { + require_zero(row, 28, 32, "parameter row padding")?; + Ok(EagerPlanParameterRow { + parameter_id: read_u32(row, 0)?, + name_string_id: read_u32(row, 4)?, + kind_string_id: read_u32(row, 8)?, + default_factor_id: read_u32(row, 12)?, + runtime_name_string_id: read_u32(row, 16)?, + complex_component: read_i32(row, 20)?, + flags: read_u32(row, 24)?, + }) + }, + ) +} + +fn decode_stages(reader: &PacbinReader) -> RusticolResult> { + decode_rows( + reader, + "tables/stages.bin", + EagerSectionKind::Stages, + EagerPlanStageRow::ENCODED_LEN, + |row| { + Ok(EagerPlanStageRow { + stage_index: read_u32(row, 0)?, + subset_size: read_u32(row, 4)?, + invocation_start: read_u64(row, 8)?, + invocation_count: read_u64(row, 16)?, + attachment_start: read_u64(row, 24)?, + attachment_count: read_u64(row, 32)?, + finalization_start: read_u64(row, 40)?, + finalization_count: read_u64(row, 48)?, + }) + }, + ) +} + +fn decode_couplings(reader: &PacbinReader) -> RusticolResult> { + decode_rows( + reader, + "tables/couplings.bin", + EagerSectionKind::Couplings, + EagerPlanCouplingRow::ENCODED_LEN, + |row| { + Ok(EagerPlanCouplingRow { + coupling_id: read_u32(row, 0)?, + real_parameter_id: read_u32(row, 4)?, + imaginary_parameter_id: read_u32(row, 8)?, + constant_factor_id: read_u32(row, 12)?, + }) + }, + ) +} + +fn decode_invocations(reader: &PacbinReader) -> RusticolResult> { + decode_rows( + reader, + "tables/invocations.bin", + EagerSectionKind::Invocations, + EagerPlanInvocationRow::ENCODED_LEN, + |row| { + require_zero(row, 29, 32, "invocation row padding")?; + require_zero(row, 52, 56, "invocation row padding")?; + Ok(EagerPlanInvocationRow { + evaluation_group_id: read_u32(row, 0)?, + kernel_id: read_u32(row, 4)?, + left_value_slot_id: read_u32(row, 8)?, + right_value_slot_id: read_u32(row, 12)?, + left_momentum_slot_id: read_u32(row, 16)?, + right_momentum_slot_id: read_u32(row, 20)?, + coupling_slot_id: read_u32(row, 24)?, + output_factor_source: read_u8(row, 28)?, + attachment_start: read_u64(row, 32)?, + attachment_count: read_u64(row, 40)?, + selector_domain_id: read_u32(row, 48)?, + }) + }, + ) +} + +fn decode_attachments(reader: &PacbinReader) -> RusticolResult> { + decode_rows( + reader, + "tables/attachments.bin", + EagerSectionKind::Attachments, + EagerPlanAttachmentRow::ENCODED_LEN, + |row| { + Ok(EagerPlanAttachmentRow { + interaction_id: read_u32(row, 0)?, + result_current_id: read_u32(row, 4)?, + color_factor_id: read_u32(row, 8)?, + evaluation_factor_id: read_u32(row, 12)?, + normalization_factor_id: read_u32(row, 16)?, + representative_evaluation_factor_id: read_u32(row, 20)?, + selector_domain_id: read_u32(row, 24)?, + }) + }, + ) +} + +fn decode_finalizations(reader: &PacbinReader) -> RusticolResult> { + decode_rows( + reader, + "tables/finalizations.bin", + EagerSectionKind::Finalizations, + EagerPlanFinalizationRow::ENCODED_LEN, + |row| { + Ok(EagerPlanFinalizationRow { + kernel_id: read_u32(row, 0)?, + current_id: read_u32(row, 4)?, + unpropagated_value_slot_id: read_u32(row, 8)?, + propagated_value_slot_id: read_u32(row, 12)?, + momentum_slot_id: read_u32(row, 16)?, + unpropagated_selector_domain_id: read_u32(row, 20)?, + propagated_selector_domain_id: read_u32(row, 24)?, + }) + }, + ) +} + +fn decode_closures(reader: &PacbinReader) -> RusticolResult> { + decode_rows( + reader, + "tables/closures.bin", + EagerSectionKind::Closures, + EagerPlanClosureRow::ENCODED_LEN, + |row| { + require_zero(row, 33, 36, "closure row padding")?; + Ok(EagerPlanClosureRow { + root_id: read_u32(row, 0)?, + kernel_id: read_u32(row, 4)?, + left_value_slot_id: read_u32(row, 8)?, + right_value_slot_id: read_u32(row, 12)?, + amplitude_index: read_u32(row, 16)?, + coherent_group_id: read_u32(row, 20)?, + coupling_slot_id: read_u32(row, 24)?, + coupling_factor_id: read_u32(row, 28)?, + output_factor_source: read_u8(row, 32)?, + color_factor_id: read_u32(row, 36)?, + normalization_factor_id: read_u32(row, 40)?, + direct_coefficient_start: read_u64(row, 44)?, + direct_coefficient_count: read_u64(row, 52)?, + selector_domain_id: read_u32(row, 60)?, + }) + }, + ) +} + +fn decode_direct_coefficients( + reader: &PacbinReader, +) -> RusticolResult> { + decode_rows( + reader, + "tables/direct-coefficients.bin", + EagerSectionKind::Closures, + DIRECT_COEFFICIENT_RECORD_SIZE, + |row| { + require_zero(row, 12, 16, "direct coefficient row padding")?; + Ok(EagerPlanDirectCoefficientRow { + contraction_ir_id: read_u32(row, 0)?, + component_index: read_u32(row, 4)?, + factor_id: read_u32(row, 8)?, + }) + }, + ) +} + +fn decode_selector_domains( + reader: &PacbinReader, +) -> RusticolResult> { + decode_rows( + reader, + "selectors/domains.bin", + EagerSectionKind::SelectorDomains, + EagerPlanSelectorDomainRow::ENCODED_LEN, + |row| { + Ok(EagerPlanSelectorDomainRow { + member_start: read_u64(row, 0)?, + member_count: read_u64(row, 8)?, + }) + }, + ) +} + +fn decode_helicity_selectors( + reader: &PacbinReader, +) -> RusticolResult> { + decode_rows( + reader, + "selectors/helicities.bin", + EagerSectionKind::SelectorDomains, + HELICITY_SELECTOR_RECORD_SIZE, + |row| { + require_zero(row, 18, 24, "helicity selector padding")?; + Ok(EagerPlanHelicitySelectorRow { + selector_id: read_u32(row, 0)?, + values_sequence_id: read_u32(row, 4)?, + representative_sequence_id: read_u32(row, 8)?, + coefficient_factor_id: read_u32(row, 12)?, + computed: read_bool(row, 16, "helicity computed flag")?, + structural_zero: read_bool(row, 17, "helicity structural-zero flag")?, + }) + }, + ) +} + +fn decode_color_selectors(reader: &PacbinReader) -> RusticolResult> { + decode_rows( + reader, + "selectors/colors.bin", + EagerSectionKind::SelectorDomains, + COLOR_SELECTOR_RECORD_SIZE, + |row| { + require_zero(row, 17, 24, "color selector padding")?; + Ok(EagerPlanColorSelectorRow { + selector_id: read_u32(row, 0)?, + word_sequence_id: read_u32(row, 4)?, + representative_word_sequence_id: read_u32(row, 8)?, + coefficient_factor_id: read_u32(row, 12)?, + computed: read_bool(row, 16, "color computed flag")?, + }) + }, + ) +} + +fn decode_reduction_groups( + reader: &PacbinReader, +) -> RusticolResult> { + decode_rows( + reader, + "reductions/groups.bin", + EagerSectionKind::ReductionGroups, + EagerPlanReductionGroupRow::ENCODED_LEN, + |row| { + require_zero(row, 4, 8, "reduction group padding")?; + Ok(EagerPlanReductionGroupRow { + coherent_group_id: read_u32(row, 0)?, + amplitude_entry_start: read_u64(row, 8)?, + amplitude_entry_count: read_u64(row, 16)?, + selector_entry_start: read_u64(row, 24)?, + selector_entry_count: read_u64(row, 32)?, + helicity_weight_factor_id: read_u32(row, 40)?, + all_sector_weight_factor_id: read_u32(row, 44)?, + }) + }, + ) +} + +fn decode_reduction_entries( + reader: &PacbinReader, +) -> RusticolResult> { + decode_rows( + reader, + "reductions/entries.bin", + EagerSectionKind::ReductionEntries, + EagerPlanReductionEntryRow::ENCODED_LEN, + |row| { + require_zero(row, 1, 4, "reduction entry padding")?; + let kind = match read_u8(row, 0)? { + 1 => EagerPlanReductionEntryKind::AmplitudeMember, + 2 => EagerPlanReductionEntryKind::SelectorMember, + 3 => EagerPlanReductionEntryKind::ColorContraction, + value => { + return Err(RusticolError::compatibility(format!( + "unknown eager reduction entry kind {value}" + ))); + } + }; + Ok(EagerPlanReductionEntryRow { + kind, + owner_id: read_u32(row, 4)?, + left_id: read_u32(row, 8)?, + right_id: read_u32(row, 12)?, + factor_id: read_u32(row, 16)?, + auxiliary_factor_id: read_u32(row, 20)?, + }) + }, + ) +} + +fn decode_exact_factors(reader: &PacbinReader) -> RusticolResult> { + decode_rows( + reader, + "catalogs/exact-factors.bin", + EagerSectionKind::ExactFactors, + EagerPlanExactFactorRow::ENCODED_LEN, + |row| { + require_zero(row, 4, 8, "exact factor padding")?; + require_zero(row, 29, 32, "exact factor padding")?; + let exact_source = read_u8(row, 28)?; + if exact_source > 1 { + return Err(RusticolError::compatibility(format!( + "unknown exact-factor source {exact_source}" + ))); + } + Ok(EagerPlanExactFactorRow { + factor_id: read_u32(row, 0)?, + real_bits: read_u64(row, 8)?, + imaginary_bits: read_u64(row, 16)?, + canonical_string_id: read_u32(row, 24)?, + exact_source, + exact_ir_id: read_u32(row, 32)?, + source_ir_id: read_u32(row, 36)?, + }) + }, + ) +} + +fn decode_catalog_ranges( + reader: &PacbinReader, + path: &str, +) -> RusticolResult> { + decode_rows( + reader, + path, + EagerSectionKind::Metadata, + EagerPlanCatalogRangeRow::ENCODED_LEN, + |row| { + Ok(EagerPlanCatalogRangeRow { + start: read_u64(row, 0)?, + count: read_u64(row, 8)?, + }) + }, + ) +} + +#[allow(clippy::too_many_arguments)] +fn validate_declared_counts( + manifest: &EagerV3ExecutionManifest, + metadata: Metadata, + inspection: &[u64; 20], + currents: &[EagerPlanCurrentRow], + values: &[EagerPlanValueRow], + momenta: &[EagerPlanMomentumRow], + sources: &[EagerPlanSourceFillRow], + parameters: &[EagerPlanParameterRow], + stages: &[EagerPlanStageRow], + couplings: &[EagerPlanCouplingRow], + invocations: &[EagerPlanInvocationRow], + attachments: &[EagerPlanAttachmentRow], + finalizations: &[EagerPlanFinalizationRow], + closures: &[EagerPlanClosureRow], + direct_coefficients: &[EagerPlanDirectCoefficientRow], + selector_domains: &[EagerPlanSelectorDomainRow], + selector_memberships: &[u32], + helicity_selectors: &[EagerPlanHelicitySelectorRow], + color_selectors: &[EagerPlanColorSelectorRow], + reduction_groups: &[EagerPlanReductionGroupRow], + reduction_entries: &[EagerPlanReductionEntryRow], + exact_factors: &[EagerPlanExactFactorRow], + retained_table_count: usize, +) -> RusticolResult<()> { + let actual = [ + currents.len(), + values.len(), + momenta.len(), + sources.len(), + parameters.len(), + stages.len(), + couplings.len(), + invocations.len(), + attachments.len(), + finalizations.len(), + closures.len(), + direct_coefficients.len(), + selector_domains.len(), + selector_memberships.len(), + helicity_selectors.len(), + color_selectors.len(), + reduction_groups.len(), + reduction_entries.len(), + exact_factors.len(), + retained_table_count, + ]; + for (index, (actual, declared)) in actual.into_iter().zip(inspection).enumerate() { + check_count(&format!("inspection field {index}"), actual, *declared)?; + } + let metadata_counts = [ + ("metadata currents", currents.len(), metadata.current_count), + ("metadata values", values.len(), metadata.value_count), + ("metadata momenta", momenta.len(), metadata.momentum_count), + ("metadata sources", sources.len(), metadata.source_count), + ("metadata stages", stages.len(), metadata.stage_count), + ( + "metadata invocations", + invocations.len(), + metadata.invocation_count, + ), + ("metadata closures", closures.len(), metadata.closure_count), + ( + "metadata direct coefficients", + direct_coefficients.len(), + metadata.direct_coefficient_count, + ), + ( + "metadata helicity selectors", + helicity_selectors.len(), + metadata.helicity_selector_count, + ), + ( + "metadata color selectors", + color_selectors.len(), + metadata.color_selector_count, + ), + ( + "metadata exact factors", + exact_factors.len(), + metadata.exact_factor_count, + ), + ]; + for (name, actual, declared) in metadata_counts { + check_count(name, actual, declared)?; + } + let summary = &manifest.plan.inspection_summary; + let manifest_counts = [ + summary.current_count, + summary.value_count, + summary.momentum_count, + summary.source_count, + summary.parameter_count, + summary.stage_count, + summary.coupling_count, + summary.invocation_count, + summary.attachment_count, + summary.finalization_count, + summary.closure_count, + summary.selector_domain_count, + summary.reduction_group_count, + summary.reduction_entry_count, + summary.retained_table_count, + ]; + let inspection_indices = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 12, 16, 17, 19]; + for (manifest_count, index) in manifest_counts.into_iter().zip(inspection_indices) { + if manifest_count != inspection[index] { + return Err(integrity(format!( + "execution-manifest inspection count disagrees with PACBIN field {index}" + ))); + } + } + if summary.current_component_count != metadata.current_component_count + || summary.value_component_count != metadata.value_component_count + || summary.momentum_component_count != metadata.momentum_component_count + { + return Err(integrity( + "execution-manifest component counts disagree with PACBIN metadata", + )); + } + Ok(()) +} + +fn validate_reduction_plan_metadata( + reader: &PacbinReader, + manifest: &EagerV3ExecutionManifest, + metadata: Metadata, + inspection: &[u64; 20], +) -> RusticolResult<()> { + // The complete container was authenticated before this decoder was + // entered. Inspect the canonical shape/count header of every plan table + // without touching or allocating its payload rows. + for (index, expected) in COUNTED_PLAN_SECTIONS.iter().enumerate() { + let actual = section(reader, expected.path, expected.kind, expected.record_size)?.count; + if actual != inspection[index] { + return Err(integrity(format!( + "reduction-only plan section {:?} has {actual} rows, inspection field {index} declares {}", + expected.path, inspection[index] + ))); + } + } + + for (path, declared, context) in [ + ( + "catalogs/strings/ranges.bin", + metadata.string_count, + "string catalog", + ), + ( + "catalogs/exact-ir/ranges.bin", + metadata.exact_ir_count, + "exact IR catalog", + ), + ( + "catalogs/semantic-limitations/ranges.bin", + metadata.limitation_count, + "semantic limitation catalog", + ), + ( + "retained/columns.bin", + metadata.retained_column_count, + "retained column catalog", + ), + ] { + let record_size = if path == "retained/columns.bin" { + RETAINED_COLUMN_RECORD_SIZE + } else { + EagerPlanCatalogRangeRow::ENCODED_LEN + }; + let actual = section(reader, path, EagerSectionKind::Metadata, record_size)?.count; + if actual != declared { + return Err(integrity(format!( + "reduction-only {context} has {actual} rows, metadata declares {declared}" + ))); + } + } + + let metadata_counts = [ + ("currents", metadata.current_count, inspection[0]), + ("values", metadata.value_count, inspection[1]), + ("momenta", metadata.momentum_count, inspection[2]), + ("sources", metadata.source_count, inspection[3]), + ("stages", metadata.stage_count, inspection[5]), + ("invocations", metadata.invocation_count, inspection[7]), + ("closures", metadata.closure_count, inspection[10]), + ( + "direct coefficients", + metadata.direct_coefficient_count, + inspection[11], + ), + ( + "helicity selectors", + metadata.helicity_selector_count, + inspection[14], + ), + ( + "color selectors", + metadata.color_selector_count, + inspection[15], + ), + ("exact factors", metadata.exact_factor_count, inspection[18]), + ( + "retained tables", + metadata.retained_table_count, + inspection[19], + ), + ]; + for (context, declared, actual) in metadata_counts { + if declared != actual { + return Err(integrity(format!( + "reduction-only metadata {context} count {declared} disagrees with inspection count {actual}" + ))); + } + } + + let summary = &manifest.plan.inspection_summary; + let manifest_counts = [ + summary.current_count, + summary.value_count, + summary.momentum_count, + summary.source_count, + summary.parameter_count, + summary.stage_count, + summary.coupling_count, + summary.invocation_count, + summary.attachment_count, + summary.finalization_count, + summary.closure_count, + summary.selector_domain_count, + summary.reduction_group_count, + summary.reduction_entry_count, + summary.retained_table_count, + ]; + let inspection_indices = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 12, 16, 17, 19]; + for (manifest_count, index) in manifest_counts.into_iter().zip(inspection_indices) { + if manifest_count != inspection[index] { + return Err(integrity(format!( + "execution-manifest inspection count disagrees with PACBIN field {index}" + ))); + } + } + if summary.current_component_count != metadata.current_component_count + || summary.value_component_count != metadata.value_component_count + || summary.momentum_component_count != metadata.momentum_component_count + { + return Err(integrity( + "execution-manifest component counts disagree with PACBIN metadata", + )); + } + Ok(()) +} + +#[allow(clippy::too_many_arguments)] +fn validate_reduction_subset_semantics( + metadata: Metadata, + exact_factor_count: u64, + helicity_selectors: &[EagerPlanHelicitySelectorRow], + color_selectors: &[EagerPlanColorSelectorRow], + reduction_groups: &[EagerPlanReductionGroupRow], + reduction_entries: &[EagerPlanReductionEntryRow], + helicity_sequences: &BTreeMap>, + color_sequences: &BTreeMap>, +) -> RusticolResult<()> { + validate_dense_ids( + helicity_selectors.iter().map(|row| row.selector_id), + "helicity selector", + )?; + for row in helicity_selectors { + required_projected_sequence( + helicity_sequences, + row.values_sequence_id, + "helicity values sequence", + )?; + required_projected_sequence( + helicity_sequences, + row.representative_sequence_id, + "helicity representative sequence", + )?; + required_declared_index( + row.coefficient_factor_id, + exact_factor_count, + "helicity coefficient factor", + )?; + } + + validate_dense_ids( + color_selectors.iter().map(|row| row.selector_id), + "color selector", + )?; + for row in color_selectors { + required_projected_sequence(color_sequences, row.word_sequence_id, "color word sequence")?; + required_projected_sequence( + color_sequences, + row.representative_word_sequence_id, + "color representative sequence", + )?; + required_declared_index( + row.coefficient_factor_id, + exact_factor_count, + "color coefficient factor", + )?; + } + + let known_groups = reduction_groups + .iter() + .map(|group| group.coherent_group_id) + .collect::>(); + if known_groups.len() != reduction_groups.len() { + return Err(integrity("reduction groups repeat coherent-group IDs")); + } + let mut covered = Vec::new(); + reserve( + &mut covered, + reduction_entries.len(), + "reduction entry coverage", + )?; + covered.resize(reduction_entries.len(), false); + for group in reduction_groups { + for (start, count, expected_kind, context) in [ + ( + group.amplitude_entry_start, + group.amplitude_entry_count, + EagerPlanReductionEntryKind::AmplitudeMember, + "amplitude reduction entries", + ), + ( + group.selector_entry_start, + group.selector_entry_count, + EagerPlanReductionEntryKind::SelectorMember, + "selector reduction entries", + ), + ] { + let entries = checked_range(reduction_entries, start, count, context)?; + let start = usize_count(start, context)?; + for (offset, entry) in entries.iter().enumerate() { + if entry.kind != expected_kind || entry.owner_id != group.coherent_group_id { + return Err(integrity(format!( + "{context} have inconsistent tags/owners" + ))); + } + if entry.kind == EagerPlanReductionEntryKind::SelectorMember { + required_index( + entry.left_id, + helicity_selectors.len(), + "reduction helicity selector", + )?; + required_index( + entry.right_id, + color_selectors.len(), + "reduction color selector", + )?; + } + covered[start + offset] = true; + } + } + required_declared_index( + group.helicity_weight_factor_id, + exact_factor_count, + "reduction helicity weight", + )?; + required_declared_index( + group.all_sector_weight_factor_id, + exact_factor_count, + "reduction all-sector weight", + )?; + } + + let contraction = checked_range( + reduction_entries, + metadata.color_contraction_entry_start, + metadata.color_contraction_entry_count, + "color contraction entries", + )?; + let contraction_start = usize_count( + metadata.color_contraction_entry_start, + "color contraction entry start", + )?; + for (offset, entry) in contraction.iter().enumerate() { + if entry.kind != EagerPlanReductionEntryKind::ColorContraction { + return Err(integrity( + "color contraction range contains another entry kind", + )); + } + covered[contraction_start + offset] = true; + if !known_groups.contains(&entry.left_id) || !known_groups.contains(&entry.right_id) { + return Err(integrity( + "color contraction references an unknown coherent group", + )); + } + required_declared_index( + entry.factor_id, + exact_factor_count, + "color contraction factor", + )?; + optional_declared_index( + entry.auxiliary_factor_id, + exact_factor_count, + "color contraction symmetry factor", + )?; + } + if covered.iter().any(|covered| !covered) { + return Err(integrity("reduction entry catalog contains unowned rows")); + } + Ok(()) +} + +#[allow(clippy::too_many_arguments)] +fn validate_semantics( + metadata: Metadata, + invocation_group_upper_bound: u64, + currents: &[EagerPlanCurrentRow], + values: &[EagerPlanValueRow], + momenta: &[EagerPlanMomentumRow], + sources: &[EagerPlanSourceFillRow], + parameters: &[EagerPlanParameterRow], + stages: &[EagerPlanStageRow], + couplings: &[EagerPlanCouplingRow], + invocations: &[EagerPlanInvocationRow], + attachments: &[EagerPlanAttachmentRow], + finalizations: &[EagerPlanFinalizationRow], + closures: &[EagerPlanClosureRow], + direct_coefficients: &[EagerPlanDirectCoefficientRow], + selector_domains: &[EagerPlanSelectorDomainRow], + selector_memberships: &[u32], + helicity_selectors: &[EagerPlanHelicitySelectorRow], + color_selectors: &[EagerPlanColorSelectorRow], + reduction_groups: &[EagerPlanReductionGroupRow], + reduction_entries: &[EagerPlanReductionEntryRow], + exact_factors: &[EagerPlanExactFactorRow], + bitset_ranges: &[EagerPlanCatalogRangeRow], + u32_sequence_ranges: &[EagerPlanCatalogRangeRow], + i32_sequence_ranges: &[EagerPlanCatalogRangeRow], + strings: &[Box], + exact_ir: &[Box], + kernel_specs: &[EagerKernelSpec], +) -> RusticolResult<()> { + validate_component_layout( + "current", + currents.iter().enumerate().map(|(index, row)| { + ( + index, + row.current_id, + row.component_start, + row.component_count, + ) + }), + metadata.current_component_count, + )?; + validate_component_layout( + "value", + values.iter().enumerate().map(|(index, row)| { + ( + index, + row.value_slot_id, + row.component_start, + row.component_count, + ) + }), + metadata.value_component_count, + )?; + validate_component_layout( + "momentum", + momenta.iter().enumerate().map(|(index, row)| { + ( + index, + row.momentum_slot_id, + row.component_start, + row.component_count, + ) + }), + metadata.momentum_component_count, + )?; + for row in currents { + required_index(row.momentum_slot_id, momenta.len(), "current momentum slot")?; + if row.flags & !1 != 0 { + return Err(RusticolError::compatibility("unknown eager current flags")); + } + } + for row in values { + let current = required_index(row.current_id, currents.len(), "value current")?; + if currents[current].component_count != row.component_count { + return Err(integrity("value/current component counts disagree")); + } + } + for row in momenta { + required_index(row.bitset_id, bitset_ranges.len(), "momentum bitset")?; + } + validate_dense_ids(sources.iter().map(|row| row.source_id), "source")?; + for row in sources { + let current = required_index(row.current_id, currents.len(), "source current")?; + let value = required_index(row.value_slot_id, values.len(), "source value slot")?; + required_index( + row.input_momentum_slot, + momenta.len(), + "source input momentum", + )?; + required_index(row.source_ir_id, exact_ir.len(), "source exact IR")?; + required_index(row.crossing_ir_id, exact_ir.len(), "source crossing IR")?; + required_index( + row.crossing_factor_id, + exact_factors.len(), + "source crossing factor", + )?; + if currents[current].flags & 1 == 0 || values[value].kind != EagerValueSlotKind::Source { + return Err(integrity( + "source row does not reference source current/value slots", + )); + } + } + validate_dense_ids(parameters.iter().map(|row| row.parameter_id), "parameter")?; + for row in parameters { + required_index(row.name_string_id, strings.len(), "parameter name")?; + required_index(row.kind_string_id, strings.len(), "parameter kind")?; + optional_index( + row.runtime_name_string_id, + strings.len(), + "parameter runtime name", + )?; + required_index( + row.default_factor_id, + exact_factors.len(), + "parameter default factor", + )?; + if !(-1..=1).contains(&row.complex_component) || row.flags & !1 != 0 { + return Err(RusticolError::compatibility( + "unsupported eager parameter component or flags", + )); + } + } + validate_stages( + stages, + invocations.len(), + attachments.len(), + finalizations.len(), + )?; + validate_dense_ids(couplings.iter().map(|row| row.coupling_id), "coupling")?; + for row in couplings { + optional_index( + row.real_parameter_id, + parameters.len(), + "coupling real parameter", + )?; + optional_index( + row.imaginary_parameter_id, + parameters.len(), + "coupling imaginary parameter", + )?; + required_index( + row.constant_factor_id, + exact_factors.len(), + "coupling constant factor", + )?; + } + + let kernels = kernel_specs + .iter() + .map(|kernel| (kernel.kernel_id, kernel.role)) + .collect::>(); + if kernels.len() != kernel_specs.len() { + return Err(integrity("prepared kernel specs repeat a kernel ID")); + } + validate_ordered_sparse_ids( + invocations.iter().map(|row| row.evaluation_group_id), + invocation_group_upper_bound, + "invocation evaluation group", + )?; + for row in invocations { + require_kernel(&kernels, row.kernel_id, EagerKernelRole::Vertex)?; + required_index( + row.left_value_slot_id, + values.len(), + "invocation left value", + )?; + required_index( + row.right_value_slot_id, + values.len(), + "invocation right value", + )?; + required_index( + row.left_momentum_slot_id, + momenta.len(), + "invocation left momentum", + )?; + required_index( + row.right_momentum_slot_id, + momenta.len(), + "invocation right momentum", + )?; + optional_index(row.coupling_slot_id, couplings.len(), "invocation coupling")?; + validate_output_factor(row.output_factor_source, row.coupling_slot_id)?; + checked_range( + attachments, + row.attachment_start, + row.attachment_count, + "invocation attachments", + )?; + required_index( + row.selector_domain_id, + selector_domains.len(), + "invocation selector", + )?; + } + for row in attachments { + required_index( + row.result_current_id, + currents.len(), + "attachment result current", + )?; + for (factor, context) in [ + (row.color_factor_id, "attachment color factor"), + (row.evaluation_factor_id, "attachment evaluation factor"), + ( + row.normalization_factor_id, + "attachment normalization factor", + ), + ( + row.representative_evaluation_factor_id, + "attachment representative factor", + ), + ] { + required_index(factor, exact_factors.len(), context)?; + } + required_index( + row.selector_domain_id, + selector_domains.len(), + "attachment selector", + )?; + } + for row in finalizations { + required_index(row.current_id, currents.len(), "finalization current")?; + optional_index( + row.unpropagated_value_slot_id, + values.len(), + "finalization unpropagated value", + )?; + optional_index( + row.propagated_value_slot_id, + values.len(), + "finalization propagated value", + )?; + required_index(row.momentum_slot_id, momenta.len(), "finalization momentum")?; + required_index( + row.unpropagated_selector_domain_id, + selector_domains.len(), + "finalization unpropagated selector", + )?; + required_index( + row.propagated_selector_domain_id, + selector_domains.len(), + "finalization propagated selector", + )?; + if row.kernel_id == MISSING_U32 { + if row.propagated_value_slot_id != MISSING_U32 { + return Err(integrity("propagated finalization lacks a prepared kernel")); + } + } else { + require_kernel(&kernels, row.kernel_id, EagerKernelRole::Finalization)?; + } + } + validate_dense_ids(closures.iter().map(|row| row.root_id), "closure root")?; + let mut amplitudes = BTreeSet::new(); + for row in closures { + required_index(row.left_value_slot_id, values.len(), "closure left value")?; + required_index(row.right_value_slot_id, values.len(), "closure right value")?; + amplitudes.insert(row.amplitude_index); + optional_index(row.coupling_slot_id, couplings.len(), "closure coupling")?; + for (factor, context) in [ + (row.coupling_factor_id, "closure coupling factor"), + (row.color_factor_id, "closure color factor"), + (row.normalization_factor_id, "closure normalization factor"), + ] { + required_index(factor, exact_factors.len(), context)?; + } + validate_output_factor(row.output_factor_source, row.coupling_slot_id)?; + let direct = checked_range( + direct_coefficients, + row.direct_coefficient_start, + row.direct_coefficient_count, + "closure direct coefficients", + )?; + if row.kernel_id == MISSING_U32 { + if direct.is_empty() { + return Err(integrity("direct eager closure has no coefficients")); + } + } else { + require_kernel(&kernels, row.kernel_id, EagerKernelRole::Closure)?; + if !direct.is_empty() { + return Err(integrity( + "prepared eager closure also has direct coefficients", + )); + } + } + required_index( + row.selector_domain_id, + selector_domains.len(), + "closure selector", + )?; + } + if amplitudes + .iter() + .copied() + .ne(0..usize_u32(amplitudes.len(), "amplitude count")?) + { + return Err(integrity("closure amplitude indices are not dense")); + } + for row in direct_coefficients { + required_index( + row.contraction_ir_id, + exact_ir.len(), + "direct coefficient IR", + )?; + required_index( + row.factor_id, + exact_factors.len(), + "direct coefficient factor", + )?; + } + + let known_groups = reduction_groups + .iter() + .map(|group| group.coherent_group_id) + .collect::>(); + if known_groups.len() != reduction_groups.len() { + return Err(integrity("reduction groups repeat coherent-group IDs")); + } + let mut selector_cursor = 0_u64; + for domain in selector_domains { + if domain.member_start != selector_cursor { + return Err(integrity("selector-domain ranges are not contiguous")); + } + selector_cursor = checked_add( + domain.member_start, + domain.member_count, + "selector-domain memberships", + )?; + } + if selector_cursor != usize_u64(selector_memberships.len(), "selector memberships")? { + return Err(integrity( + "selector-domain ranges do not cover the membership catalog", + )); + } + for member in selector_memberships { + if !known_groups.contains(member) { + return Err(integrity( + "selector domain references an unknown coherent group", + )); + } + } + validate_dense_ids( + helicity_selectors.iter().map(|row| row.selector_id), + "helicity selector", + )?; + for row in helicity_selectors { + required_index( + row.values_sequence_id, + i32_sequence_ranges.len(), + "helicity values sequence", + )?; + required_index( + row.representative_sequence_id, + i32_sequence_ranges.len(), + "helicity representative sequence", + )?; + required_index( + row.coefficient_factor_id, + exact_factors.len(), + "helicity coefficient factor", + )?; + } + validate_dense_ids( + color_selectors.iter().map(|row| row.selector_id), + "color selector", + )?; + for row in color_selectors { + required_index( + row.word_sequence_id, + u32_sequence_ranges.len(), + "color word sequence", + )?; + required_index( + row.representative_word_sequence_id, + u32_sequence_ranges.len(), + "color representative sequence", + )?; + required_index( + row.coefficient_factor_id, + exact_factors.len(), + "color coefficient factor", + )?; + } + + let mut covered = vec![false; reduction_entries.len()]; + for group in reduction_groups { + for (start, count, expected_kind, context) in [ + ( + group.amplitude_entry_start, + group.amplitude_entry_count, + EagerPlanReductionEntryKind::AmplitudeMember, + "amplitude reduction entries", + ), + ( + group.selector_entry_start, + group.selector_entry_count, + EagerPlanReductionEntryKind::SelectorMember, + "selector reduction entries", + ), + ] { + let entries = checked_range(reduction_entries, start, count, context)?; + let start = usize_count(start, context)?; + for (offset, entry) in entries.iter().enumerate() { + if entry.kind != expected_kind || entry.owner_id != group.coherent_group_id { + return Err(integrity(format!( + "{context} have inconsistent tags/owners" + ))); + } + covered[start + offset] = true; + } + } + required_index( + group.helicity_weight_factor_id, + exact_factors.len(), + "reduction helicity weight", + )?; + required_index( + group.all_sector_weight_factor_id, + exact_factors.len(), + "reduction all-sector weight", + )?; + } + let contraction = checked_range( + reduction_entries, + metadata.color_contraction_entry_start, + metadata.color_contraction_entry_count, + "color contraction entries", + )?; + let contraction_start = usize_count( + metadata.color_contraction_entry_start, + "color contraction entry start", + )?; + for (offset, entry) in contraction.iter().enumerate() { + if entry.kind != EagerPlanReductionEntryKind::ColorContraction { + return Err(integrity( + "color contraction range contains another entry kind", + )); + } + covered[contraction_start + offset] = true; + if !known_groups.contains(&entry.left_id) || !known_groups.contains(&entry.right_id) { + return Err(integrity( + "color contraction references an unknown coherent group", + )); + } + required_index( + entry.factor_id, + exact_factors.len(), + "color contraction factor", + )?; + optional_index( + entry.auxiliary_factor_id, + exact_factors.len(), + "color contraction symmetry factor", + )?; + } + if covered.iter().any(|covered| !covered) { + return Err(integrity("reduction entry catalog contains unowned rows")); + } + + validate_dense_ids( + exact_factors.iter().map(|row| row.factor_id), + "exact factor", + )?; + for row in exact_factors { + required_index( + row.canonical_string_id, + strings.len(), + "exact factor canonical string", + )?; + optional_index(row.exact_ir_id, exact_ir.len(), "exact factor IR")?; + optional_index(row.source_ir_id, exact_ir.len(), "exact factor source IR")?; + } + Ok(()) +} + +fn validate_stages( + stages: &[EagerPlanStageRow], + invocation_count: usize, + attachment_count: usize, + finalization_count: usize, +) -> RusticolResult<()> { + let mut invocations = 0_u64; + let mut attachments = 0_u64; + let mut finalizations = 0_u64; + let mut previous_subset = 0_u32; + for (index, stage) in stages.iter().enumerate() { + if stage.stage_index != usize_u32(index + 1, "stage index")? + || stage.subset_size <= previous_subset + || stage.invocation_start != invocations + || stage.attachment_start != attachments + || stage.finalization_start != finalizations + { + return Err(integrity( + "eager stage descriptors are not canonical and contiguous", + )); + } + invocations = checked_add( + stage.invocation_start, + stage.invocation_count, + "stage invocations", + )?; + attachments = checked_add( + stage.attachment_start, + stage.attachment_count, + "stage attachments", + )?; + finalizations = checked_add( + stage.finalization_start, + stage.finalization_count, + "stage finalizations", + )?; + previous_subset = stage.subset_size; + } + if invocations != usize_u64(invocation_count, "invocations")? + || attachments != usize_u64(attachment_count, "attachments")? + || finalizations != usize_u64(finalization_count, "finalizations")? + { + return Err(integrity( + "eager stage ranges do not cover their complete tables", + )); + } + Ok(()) +} + +fn validate_output_factor(source: u8, coupling_id: u32) -> RusticolResult<()> { + if source > 2 || (coupling_id == MISSING_U32 && source != 0) { + return Err(RusticolError::compatibility( + "unsupported eager output-factor source", + )); + } + Ok(()) +} + +fn require_kernel( + kernels: &BTreeMap, + id: u32, + expected: EagerKernelRole, +) -> RusticolResult<()> { + match kernels.get(&id) { + Some(actual) if *actual == expected => Ok(()), + Some(actual) => Err(integrity(format!( + "prepared kernel {id} has role {actual:?}, expected {expected:?}" + ))), + None => Err(integrity(format!( + "eager plan references missing prepared kernel {id}" + ))), + } +} + +fn validate_component_layout( + name: &str, + rows: impl Iterator, + declared_components: u64, +) -> RusticolResult<()> { + let mut cursor = 0_u64; + for (index, id, start, count) in rows { + if id != usize_u32(index, &format!("{name} ID"))? || start != cursor || count == 0 { + return Err(integrity(format!( + "{name} component layout is not dense and contiguous" + ))); + } + cursor = cursor + .checked_add(u64::from(count)) + .ok_or_else(|| RusticolError::artifact(format!("{name} components exceed u64")))?; + } + if cursor != declared_components { + return Err(integrity(format!( + "{name} component layout total disagrees with metadata" + ))); + } + Ok(()) +} + +fn validate_dense_ids(ids: impl Iterator, context: &str) -> RusticolResult<()> { + for (index, id) in ids.enumerate() { + if id != usize_u32(index, &format!("{context} ID"))? { + return Err(integrity(format!("{context} IDs are not dense"))); + } + } + Ok(()) +} + +fn validate_ordered_sparse_ids( + ids: impl Iterator, + exclusive_upper_bound: u64, + context: &str, +) -> RusticolResult<()> { + let mut previous = None; + for id in ids { + if id == MISSING_U32 || u64::from(id) >= exclusive_upper_bound { + return Err(integrity(format!( + "{context} ID {id} is outside its authenticated domain" + ))); + } + if previous.is_some_and(|previous| id <= previous) { + return Err(integrity(format!( + "{context} IDs are not strictly increasing and unique" + ))); + } + previous = Some(id); + } + Ok(()) +} + +fn validate_catalog_ranges( + context: &str, + ranges: &[EagerPlanCatalogRangeRow], + value_count: usize, +) -> RusticolResult<()> { + let mut cursor = 0_u64; + for range in ranges { + if range.start != cursor { + return Err(integrity(format!("{context} ranges are not contiguous"))); + } + cursor = checked_add(range.start, range.count, context)?; + } + if cursor != usize_u64(value_count, context)? { + return Err(integrity(format!( + "{context} ranges do not cover their value catalog" + ))); + } + Ok(()) +} + +struct DecodedSection<'a> { + count: u64, + payload: &'a [u8], +} + +fn decode_referenced_sequences( + reader: &PacbinReader, + ranges_path: &str, + values_path: &str, + referenced_ids: &BTreeSet, + context: &str, + decode: impl FnMut(&[u8]) -> RusticolResult, +) -> RusticolResult>> { + let ranges = section( + reader, + ranges_path, + EagerSectionKind::Metadata, + EagerPlanCatalogRangeRow::ENCODED_LEN, + )?; + let values = section(reader, values_path, EagerSectionKind::Metadata, 4)?; + project_referenced_sequences(&ranges, &values, referenced_ids, context, decode) +} + +/// Validate the complete borrowed range catalog, then own only the values +/// reachable from `referenced_ids`. +fn project_referenced_sequences( + ranges: &DecodedSection<'_>, + values: &DecodedSection<'_>, + referenced_ids: &BTreeSet, + context: &str, + mut decode: impl FnMut(&[u8]) -> RusticolResult, +) -> RusticolResult>> { + let range_width = usize::try_from(EagerPlanCatalogRangeRow::ENCODED_LEN) + .map_err(|_| RusticolError::artifact(format!("{context} range width exceeds usize")))?; + let value_width = 4_usize; + let range_count = usize_count(ranges.count, context)?; + if ranges.payload.chunks_exact(range_width).len() != range_count { + return Err(integrity(format!( + "{context} range row count changed unexpectedly" + ))); + } + + // Authentication covers every byte before this point. This streaming pass + // additionally checks that even unreferenced ranges have canonical, + // gap-free coverage without allocating their rows. + let mut cursor = 0_u64; + for row in ranges.payload.chunks_exact(range_width) { + let start = read_u64(row, 0)?; + let count = read_u64(row, 8)?; + if start != cursor { + return Err(integrity(format!("{context} ranges are not contiguous"))); + } + cursor = checked_add(start, count, context)?; + } + if cursor != values.count { + return Err(integrity(format!( + "{context} ranges do not cover their value catalog" + ))); + } + + let mut projected = BTreeMap::new(); + for &sequence_id in referenced_ids { + required_declared_index(sequence_id, ranges.count, context)?; + let sequence_index = usize::try_from(sequence_id) + .map_err(|_| RusticolError::artifact(format!("{context} ID exceeds usize")))?; + let range_start = sequence_index + .checked_mul(range_width) + .ok_or_else(|| RusticolError::artifact(format!("{context} range offset overflows")))?; + let range_stop = range_start + .checked_add(range_width) + .ok_or_else(|| RusticolError::artifact(format!("{context} range offset overflows")))?; + let range = ranges.payload.get(range_start..range_stop).ok_or_else(|| { + integrity(format!( + "{context} {sequence_id} range row is out of bounds" + )) + })?; + let start = usize_count(read_u64(range, 0)?, context)?; + let count = usize_count(read_u64(range, 8)?, context)?; + let stop = start + .checked_add(count) + .ok_or_else(|| RusticolError::artifact(format!("{context} range exceeds usize")))?; + let byte_start = start + .checked_mul(value_width) + .ok_or_else(|| RusticolError::artifact(format!("{context} byte range overflows")))?; + let byte_stop = stop + .checked_mul(value_width) + .ok_or_else(|| RusticolError::artifact(format!("{context} byte range overflows")))?; + let encoded = values.payload.get(byte_start..byte_stop).ok_or_else(|| { + integrity(format!( + "{context} {sequence_id} value range is out of bounds" + )) + })?; + + let mut sequence = Vec::new(); + reserve(&mut sequence, count, context)?; + for row in encoded.chunks_exact(value_width) { + sequence.push(decode(row)?); + } + if sequence.len() != count { + return Err(integrity(format!( + "{context} {sequence_id} decoded length changed unexpectedly" + ))); + } + projected.insert(sequence_id, sequence); + } + Ok(projected) +} + +fn section<'a>( + reader: &'a PacbinReader, + path: &str, + kind: EagerSectionKind, + record_size: u32, +) -> RusticolResult> { + let bytes = reader.member_bytes(path)?; + let (header, payload) = EagerSectionHeader::decode(bytes)?; + if header.kind() != kind || header.record_size() != record_size { + return Err(RusticolError::compatibility(format!( + "eager PACBIN member {path:?} has shape ({:?}, {}), expected ({kind:?}, {record_size})", + header.kind(), + header.record_size(), + ))); + } + Ok(DecodedSection { + count: header.record_count(), + payload, + }) +} + +fn decode_rows( + reader: &PacbinReader, + path: &str, + kind: EagerSectionKind, + record_size: u32, + mut decode: impl FnMut(&[u8]) -> RusticolResult, +) -> RusticolResult> { + let section = section(reader, path, kind, record_size)?; + let count = usize_count(section.count, path)?; + let width = usize::try_from(record_size) + .map_err(|_| RusticolError::artifact(format!("{path} record size does not fit usize")))?; + let mut result = Vec::new(); + reserve(&mut result, count, path)?; + for row in section.payload.chunks_exact(width) { + result.push(decode(row)?); + } + if result.len() != count { + return Err(integrity(format!( + "{path} decoded row count changed unexpectedly" + ))); + } + Ok(result) +} + +fn decode_u32_section( + reader: &PacbinReader, + path: &str, + kind: EagerSectionKind, +) -> RusticolResult> { + decode_rows(reader, path, kind, 4, |row| read_u32(row, 0)) +} + +fn decode_i32_section( + reader: &PacbinReader, + path: &str, + kind: EagerSectionKind, +) -> RusticolResult> { + decode_rows(reader, path, kind, 4, |row| read_i32(row, 0)) +} + +fn decode_u64_section( + reader: &PacbinReader, + path: &str, + kind: EagerSectionKind, +) -> RusticolResult> { + decode_rows(reader, path, kind, 8, |row| read_u64(row, 0)) +} + +fn read_bool(row: &[u8], offset: usize, context: &str) -> RusticolResult { + let value = read_u8(row, offset)?; + if value > 1 { + return Err(integrity(format!( + "{context} must be encoded as zero or one" + ))); + } + Ok(value) +} + +fn read_u8(bytes: &[u8], offset: usize) -> RusticolResult { + bytes + .get(offset) + .copied() + .ok_or_else(|| integrity("truncated eager fixed-width record")) +} + +fn read_u16(bytes: &[u8], offset: usize) -> RusticolResult { + Ok(u16::from_le_bytes(read_array(bytes, offset)?)) +} + +fn read_u32(bytes: &[u8], offset: usize) -> RusticolResult { + Ok(u32::from_le_bytes(read_array(bytes, offset)?)) +} + +fn read_i32(bytes: &[u8], offset: usize) -> RusticolResult { + Ok(i32::from_le_bytes(read_array(bytes, offset)?)) +} + +fn read_u64(bytes: &[u8], offset: usize) -> RusticolResult { + Ok(u64::from_le_bytes(read_array(bytes, offset)?)) +} + +fn read_array(bytes: &[u8], offset: usize) -> RusticolResult<[u8; N]> { + let end = offset + .checked_add(N) + .ok_or_else(|| integrity("eager fixed-width record offset overflow"))?; + bytes + .get(offset..end) + .ok_or_else(|| integrity("truncated eager fixed-width record"))? + .try_into() + .map_err(|_| integrity("truncated eager fixed-width record")) +} + +fn require_zero(bytes: &[u8], start: usize, end: usize, context: &str) -> RusticolResult<()> { + if bytes + .get(start..end) + .ok_or_else(|| integrity(format!("truncated {context}")))? + .iter() + .any(|value| *value != 0) + { + return Err(integrity(format!("{context} must be zero"))); + } + Ok(()) +} + +fn require_count(section: &DecodedSection<'_>, expected: u64, context: &str) -> RusticolResult<()> { + if section.count != expected { + return Err(integrity(format!( + "{context} has {} records, expected {expected}", + section.count + ))); + } + Ok(()) +} + +fn required_catalog<'a>(values: &'a [Box], id: u32, context: &str) -> RusticolResult<&'a str> { + values + .get(required_index(id, values.len(), context)?) + .map(Box::as_ref) + .ok_or_else(|| integrity(format!("{context} is out of range"))) +} + +fn required_index(id: u32, length: usize, context: &str) -> RusticolResult { + let index = usize::try_from(id) + .map_err(|_| RusticolError::artifact(format!("{context} does not fit usize")))?; + if index >= length { + return Err(integrity(format!( + "{context} {id} is outside a catalog of length {length}" + ))); + } + Ok(index) +} + +fn required_declared_index(id: u32, length: u64, context: &str) -> RusticolResult<()> { + if u64::from(id) >= length { + return Err(integrity(format!( + "{context} {id} is outside a catalog of length {length}" + ))); + } + Ok(()) +} + +fn required_projected_sequence<'a, T>( + sequences: &'a BTreeMap>, + id: u32, + context: &str, +) -> RusticolResult<&'a [T]> { + sequences.get(&id).map(Vec::as_slice).ok_or_else(|| { + integrity(format!( + "{context} {id} is absent from the referenced catalog projection" + )) + }) +} + +fn optional_index(id: u32, length: usize, context: &str) -> RusticolResult<()> { + if id != MISSING_U32 { + required_index(id, length, context)?; + } + Ok(()) +} + +fn optional_declared_index(id: u32, length: u64, context: &str) -> RusticolResult<()> { + if id != MISSING_U32 { + required_declared_index(id, length, context)?; + } + Ok(()) +} + +fn checked_range<'a, T>( + values: &'a [T], + start: u64, + count: u64, + context: &str, +) -> RusticolResult<&'a [T]> { + let start = usize_count(start, context)?; + let count = usize_count(count, context)?; + let end = start + .checked_add(count) + .ok_or_else(|| RusticolError::artifact(format!("{context} range exceeds usize")))?; + values.get(start..end).ok_or_else(|| { + integrity(format!( + "{context} range {start}..{end} exceeds length {}", + values.len() + )) + }) +} + +fn checked_add(start: u64, count: u64, context: &str) -> RusticolResult { + start + .checked_add(count) + .ok_or_else(|| RusticolError::artifact(format!("{context} range exceeds u64"))) +} + +fn check_count(context: &str, actual: usize, declared: u64) -> RusticolResult<()> { + if usize_u64(actual, context)? != declared { + return Err(integrity(format!( + "{context} count {actual} does not match declared count {declared}" + ))); + } + Ok(()) +} + +fn usize_count(value: u64, context: &str) -> RusticolResult { + usize::try_from(value) + .map_err(|_| RusticolError::artifact(format!("{context} count does not fit usize"))) +} + +fn usize_u32(value: usize, context: &str) -> RusticolResult { + u32::try_from(value).map_err(|_| RusticolError::artifact(format!("{context} exceeds u32"))) +} + +fn usize_u64(value: usize, context: &str) -> RusticolResult { + u64::try_from(value).map_err(|_| RusticolError::artifact(format!("{context} exceeds u64"))) +} + +fn reserve(values: &mut Vec, count: usize, context: &str) -> RusticolResult<()> { + values.try_reserve_exact(count).map_err(|error| { + RusticolError::artifact(format!( + "could not reserve {context} ({count} rows): {error}" + )) + }) +} + +fn integrity(message: impl Into) -> RusticolError { + RusticolError::integrity(message.into()) +} + +#[cfg(test)] +mod reduction_subset_tests { + use super::*; + use crate::pacbin::{ + PacbinMemberKind, PacbinReader, PacbinWriteMember, PacbinWriteOptions, write_pacbin_atomic, + }; + use std::fs; + use std::sync::atomic::{AtomicU64, Ordering}; + + static NEXT_INVOCATION_FIXTURE: AtomicU64 = AtomicU64::new(0); + + fn invocation(evaluation_group_id: u32) -> EagerPlanInvocationRow { + EagerPlanInvocationRow { + evaluation_group_id, + kernel_id: 0, + left_value_slot_id: 0, + right_value_slot_id: 0, + left_momentum_slot_id: 0, + right_momentum_slot_id: 0, + coupling_slot_id: 0, + output_factor_source: 0, + attachment_start: 0, + attachment_count: 1, + selector_domain_id: MISSING_U32, + } + } + + fn put_u32(row: &mut [u8], offset: usize, value: u32) { + row[offset..offset + 4].copy_from_slice(&value.to_le_bytes()); + } + + fn put_u64(row: &mut [u8], offset: usize, value: u64) { + row[offset..offset + 8].copy_from_slice(&value.to_le_bytes()); + } + + fn decode_invocation_fixture( + rows: &[EagerPlanInvocationRow], + corrupt_padding: bool, + ) -> RusticolResult> { + let mut bytes = EagerSectionHeader::new( + EagerSectionKind::Invocations, + EagerPlanInvocationRow::ENCODED_LEN, + rows.len() as u64, + )? + .encode() + .to_vec(); + for (index, source) in rows.iter().enumerate() { + let mut row = [0_u8; EagerPlanInvocationRow::ENCODED_LEN as usize]; + for (offset, value) in [ + source.evaluation_group_id, + source.kernel_id, + source.left_value_slot_id, + source.right_value_slot_id, + source.left_momentum_slot_id, + source.right_momentum_slot_id, + source.coupling_slot_id, + ] + .into_iter() + .enumerate() + { + put_u32(&mut row, offset * 4, value); + } + row[28] = source.output_factor_source; + put_u64(&mut row, 32, source.attachment_start); + put_u64(&mut row, 40, source.attachment_count); + put_u32(&mut row, 48, source.selector_domain_id); + if corrupt_padding && index == 0 { + row[29] = 1; + } + bytes.extend_from_slice(&row); + } + + let root = std::env::temp_dir().join(format!( + "rusticol-eager-sparse-invocations-{}-{}", + std::process::id(), + NEXT_INVOCATION_FIXTURE.fetch_add(1, Ordering::Relaxed), + )); + fs::create_dir(&root).map_err(|error| { + RusticolError::artifact(format!("could not create test directory: {error}")) + })?; + let path = root.join("invocations.pacbin"); + let member = PacbinWriteMember::from_bytes( + "tables/invocations.bin", + PacbinMemberKind::EagerRuntimeTable, + &bytes, + )?; + write_pacbin_atomic(&path, [member], PacbinWriteOptions::default())?; + let reader = PacbinReader::open(&path)?; + let decoded = decode_invocations(&reader); + drop(reader); + let _ = fs::remove_dir_all(root); + decoded + } + + fn metadata() -> Metadata { + Metadata { + retained_column_count: 0, + current_component_count: 0, + value_component_count: 0, + momentum_component_count: 0, + color_contraction_entry_start: 2, + color_contraction_entry_count: 1, + string_count: 0, + exact_ir_count: 0, + limitation_count: 0, + retained_table_count: 0, + current_count: 0, + value_count: 0, + momentum_count: 0, + source_count: 0, + stage_count: 0, + invocation_count: 0, + closure_count: 0, + direct_coefficient_count: 0, + helicity_selector_count: 1, + color_selector_count: 1, + exact_factor_count: 2, + } + } + + fn color_selectors() -> Vec { + vec![EagerPlanColorSelectorRow { + selector_id: 0, + word_sequence_id: 0, + representative_word_sequence_id: 0, + coefficient_factor_id: 0, + computed: 1, + }] + } + + fn helicity_selectors() -> Vec { + vec![EagerPlanHelicitySelectorRow { + selector_id: 0, + values_sequence_id: 0, + representative_sequence_id: 0, + coefficient_factor_id: 0, + computed: 1, + structural_zero: 0, + }] + } + + fn groups() -> Vec { + vec![EagerPlanReductionGroupRow { + coherent_group_id: 7, + amplitude_entry_start: 0, + amplitude_entry_count: 1, + selector_entry_start: 1, + selector_entry_count: 1, + helicity_weight_factor_id: 0, + all_sector_weight_factor_id: 1, + }] + } + + fn entries() -> Vec { + vec![ + EagerPlanReductionEntryRow { + kind: EagerPlanReductionEntryKind::AmplitudeMember, + owner_id: 7, + left_id: 0, + right_id: MISSING_U32, + factor_id: MISSING_U32, + auxiliary_factor_id: MISSING_U32, + }, + EagerPlanReductionEntryRow { + kind: EagerPlanReductionEntryKind::SelectorMember, + owner_id: 7, + left_id: 0, + right_id: 0, + factor_id: MISSING_U32, + auxiliary_factor_id: MISSING_U32, + }, + EagerPlanReductionEntryRow { + kind: EagerPlanReductionEntryKind::ColorContraction, + owner_id: MISSING_U32, + left_id: 7, + right_id: 7, + factor_id: 1, + auxiliary_factor_id: MISSING_U32, + }, + ] + } + + fn validate(entries: &[EagerPlanReductionEntryRow]) -> RusticolResult<()> { + validate_reduction_subset_semantics( + metadata(), + 2, + &helicity_selectors(), + &color_selectors(), + &groups(), + entries, + &BTreeMap::from([(0, vec![1, -1, 1])]), + &BTreeMap::from([(0, vec![1, 2])]), + ) + } + + #[test] + fn bounded_reduction_semantics_accept_canonical_partition() { + validate(&entries()).expect("canonical reduction subset"); + } + + #[test] + fn active_kernel_census_uses_invocations_and_non_sentinel_terminal_rows() { + assert_eq!( + active_kernel_ids([7, 7], [MISSING_U32, 9], [11, MISSING_U32]), + BTreeSet::from([7, 9, 11]) + ); + } + + #[test] + fn pacbin_decode_accepts_sparse_invocation_groups_after_zero_first_group() { + let decoded = decode_invocation_fixture(&[invocation(1), invocation(3)], false) + .expect("decode authenticated sparse invocation rows"); + assert_eq!( + decoded + .iter() + .map(|row| row.evaluation_group_id) + .collect::>(), + [1, 3] + ); + validate_ordered_sparse_ids( + decoded.iter().map(|row| row.evaluation_group_id), + 4, + "invocation evaluation group", + ) + .expect("zero-first-group pruning retains authenticated sparse IDs"); + } + + #[test] + fn sparse_invocation_group_ids_reject_duplicates_order_and_domain_escape() { + for (ids, message) in [ + (&[1_u32, 1][..], "strictly increasing and unique"), + (&[2_u32, 1][..], "strictly increasing and unique"), + (&[1_u32, 4][..], "outside its authenticated domain"), + (&[MISSING_U32][..], "outside its authenticated domain"), + ] { + let rows = ids.iter().copied().map(invocation).collect::>(); + let decoded = decode_invocation_fixture(&rows, false) + .expect("fixed-width invocation rows remain decodable"); + let error = validate_ordered_sparse_ids( + decoded.iter().map(|row| row.evaluation_group_id), + 4, + "invocation evaluation group", + ) + .expect_err("malformed sparse group IDs must fail closed"); + assert!(error.to_string().contains(message), "{error}"); + } + } + + #[test] + fn pacbin_invocation_padding_mutation_fails_closed() { + let error = decode_invocation_fixture(&[invocation(1)], true) + .expect_err("mutated invocation padding must fail closed"); + assert!( + error + .to_string() + .contains("invocation row padding must be zero"), + "{error}" + ); + } + + #[test] + fn bounded_reduction_semantics_reject_corrupt_selector_and_coverage() { + let mut wrong_owner = entries(); + wrong_owner[1].owner_id = 8; + assert!( + validate(&wrong_owner) + .unwrap_err() + .to_string() + .contains("inconsistent tags/owners") + ); + + let mut wrong_color = entries(); + wrong_color[1].right_id = 1; + assert!( + validate(&wrong_color) + .unwrap_err() + .to_string() + .contains("reduction color selector") + ); + + let mut unowned = entries(); + unowned.push(EagerPlanReductionEntryRow { + kind: EagerPlanReductionEntryKind::AmplitudeMember, + owner_id: 7, + left_id: 1, + right_id: MISSING_U32, + factor_id: MISSING_U32, + auxiliary_factor_id: MISSING_U32, + }); + assert!( + validate(&unowned) + .unwrap_err() + .to_string() + .contains("unowned rows") + ); + } + + #[test] + fn large_irrelevant_catalog_projects_only_referenced_values() { + use std::cell::Cell; + + let catalog_size = 250_000_u32; + let mut range_bytes = Vec::with_capacity(catalog_size as usize * 16); + let mut value_bytes = Vec::with_capacity(catalog_size as usize * 4); + for id in 0..catalog_size { + range_bytes.extend_from_slice(&u64::from(id).to_le_bytes()); + range_bytes.extend_from_slice(&1_u64.to_le_bytes()); + value_bytes.extend_from_slice(&id.to_le_bytes()); + } + let ranges = DecodedSection { + count: u64::from(catalog_size), + payload: &range_bytes, + }; + let values = DecodedSection { + count: u64::from(catalog_size), + payload: &value_bytes, + }; + let last = catalog_size - 3; + let referenced = BTreeSet::from([7, last]); + let decoded_value_count = Cell::new(0_usize); + + let projected = project_referenced_sequences( + &ranges, + &values, + &referenced, + "bounded test sequence", + |row| { + decoded_value_count.set(decoded_value_count.get() + 1); + read_u32(row, 0) + }, + ) + .expect("project referenced catalog"); + + assert_eq!(projected.len(), 2); + assert_eq!(projected.values().map(Vec::len).sum::(), 2); + assert_eq!(decoded_value_count.get(), 2); + assert_eq!(projected[&7], [7]); + assert_eq!(projected[&last], [last]); + } + + #[test] + fn referenced_projection_validates_unreferenced_range_rows() { + let mut range_bytes = Vec::new(); + for (start, count) in [(0_u64, 1_u64), (9, 1), (2, 1)] { + range_bytes.extend_from_slice(&start.to_le_bytes()); + range_bytes.extend_from_slice(&count.to_le_bytes()); + } + let value_bytes = [0_u32, 1, 2] + .into_iter() + .flat_map(u32::to_le_bytes) + .collect::>(); + let ranges = DecodedSection { + count: 3, + payload: &range_bytes, + }; + let values = DecodedSection { + count: 3, + payload: &value_bytes, + }; + + let error = project_referenced_sequences( + &ranges, + &values, + &BTreeSet::from([0]), + "authenticated sequence", + |row| read_u32(row, 0), + ) + .unwrap_err(); + + assert!(error.to_string().contains("ranges are not contiguous")); + } +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/engine/eager_v3_load.rs b/pyamplicol/rust/crates/rusticol-core/src/engine/eager_v3_load.rs new file mode 100644 index 0000000000..18b93bc9cf --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/engine/eager_v3_load.rs @@ -0,0 +1,2167 @@ +// SPDX-License-Identifier: 0BSD + +//! Native loader for compact eager plan-v3 artifacts. + +use super::eager_manifest::PreparedKernelPackManifest; +use super::eager_v3_decode::DecodedEagerRuntimeV3; +use super::eager_v3_manifest::EagerV3ExecutionManifest; +use super::*; +use crate::eager_runtime::EagerPlanV3Sections; +use crate::{ + EagerCouplingRow, EagerPlanCouplingRow, EagerPlanReductionEntryKind, EagerRuntimeOptions, + MISSING_U32, +}; +use serde::Deserialize; +use serde_json::json; +#[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] +use sha2::{Digest, Sha256}; + +const NATIVE_REDUCTION_GROUPS_EXTENSION_KEY: &str = "native_reduction_groups"; +const NATIVE_REDUCTION_GROUPS_KIND: &str = "pyamplicol-eager-plan-v3-reduction-groups"; +const NATIVE_REDUCTION_GROUPS_SCHEMA_VERSION: u32 = 1; +const NATIVE_REDUCTION_GROUPS_STORAGE_ABI: &str = "pacbin-v1"; +const NATIVE_REDUCTION_GROUPS_RUNTIME_LAYOUT_ABI: &str = "pyamplicol-eager-runtime-layout-v1"; +const NATIVE_REDUCTION_GROUPS_CONTAINER_PATH: &str = "eager-runtime.pacbin"; +const NATIVE_REDUCTION_GROUPS_GROUP_MEMBER: &str = "reductions/groups.bin"; +const NATIVE_REDUCTION_GROUPS_ENTRY_MEMBER: &str = "reductions/entries.bin"; + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct NativeReductionGroupsDescriptor { + kind: String, + schema_version: u32, + storage_abi: String, + runtime_layout_abi: String, + container_path: String, + group_member: String, + entry_member: String, + group_count: u64, +} + +pub(super) struct EagerV3PreparedPack { + pub(super) manifest: PreparedKernelPackManifest, + pub(super) payload_root: PathBuf, +} + +pub(super) struct LoadedEagerV3Runtime { + pub(super) common: ExecutionRuntime, + pub(super) lane: EagerNativeRuntime, +} + +pub(super) fn load_eager_v3_prepared_pack( + artifact: &VerifiedArtifact, + manifest: &EagerV3ExecutionManifest, +) -> RusticolResult { + let manifest_path = confined_internal_path( + &manifest.kernel_pack.manifest_path, + "eager plan-v3 prepared kernel-pack manifest path", + )?; + let manifest_path = manifest_path.to_str().ok_or_else(|| { + RusticolError::security("prepared kernel-pack manifest path is not valid UTF-8") + })?; + if artifact.payload(manifest_path)?.role != PayloadRole::EvaluatorManifest { + return Err(RusticolError::security(format!( + "prepared kernel-pack path {manifest_path:?} is not an evaluator-manifest payload" + ))); + } + let bytes = artifact.read_payload(manifest_path)?; + let pack: PreparedKernelPackManifest = serde_json::from_slice(&bytes).map_err(|error| { + RusticolError::serialization(format!( + "could not parse prepared kernel pack {manifest_path:?}: {error}" + )) + })?; + pack.validate()?; + validate_eager_prepared_pack_outer_target(&artifact.manifest().producer.target, &pack)?; + let payload_root = confined_internal_path( + &manifest.kernel_pack.payload_root, + "eager plan-v3 prepared kernel payload root", + )?; + let payload_root = artifact.root().join(payload_root); + validate_prepared_kernel_references(artifact, &payload_root, &pack)?; + Ok(EagerV3PreparedPack { + manifest: pack, + payload_root, + }) +} + +pub(super) fn validate_eager_prepared_pack_outer_target( + outer_target: &crate::Target, + pack: &PreparedKernelPackManifest, +) -> RusticolResult<()> { + pack.validate_portable_process_artifact_target(outer_target, "eager") +} + +pub(super) fn load_eager_v3_native_runtime( + artifact: &VerifiedArtifact, + evaluator_root: &Path, + manifest: &EagerV3ExecutionManifest, + physics: &mut ProcessPhysicsV1, +) -> RusticolResult { + let pack = load_eager_v3_prepared_pack(artifact, manifest)?; + let container = open_verified_eager_v3_runtime_container(artifact, evaluator_root, manifest)?; + let decoded = + super::eager_v3_decode::decode_eager_v3_runtime(&container, manifest, &pack.manifest)?; + let color_selector_ids = native_color_selector_ids( + &decoded.color_selectors, + &decoded.u32_sequence_ranges, + &decoded.u32_sequence_values, + physics, + )?; + hydrate_native_reduction_groups( + physics, + &decoded.reduction_groups, + &decoded.reduction_entries, + &color_selector_ids, + )?; + let mut common = + super::eager_v3_common::build_eager_v3_common_runtime(&decoded, manifest, physics.clone())?; + + let kernel_payloads = artifact.evaluator_payload_store(&pack.payload_root)?; + let (parameter_projection, couplings, model_parameter_evaluator) = + prepare_plan_v3_parameter_state( + &pack.manifest, + &decoded, + &common.model_parameters, + &kernel_payloads, + )?; + common.model_parameter_evaluator = model_parameter_evaluator; + common.refresh_derived_model_parameters()?; + + let prepared_parameter_count = u32::try_from(parameter_projection.parameter_count) + .map_err(|_| RusticolError::artifact("prepared parameter count exceeds u32"))?; + let sections = EagerPlanV3Sections { + kernels: &decoded.kernel_specs, + prepared_parameter_count, + currents: &decoded.currents, + values: &decoded.values, + momenta: &decoded.momenta, + parameters: &decoded.parameters, + stages: &decoded.stages, + couplings: &couplings, + invocations: &decoded.invocations, + attachments: &decoded.attachments, + finalizations: &decoded.finalizations, + closures: &decoded.closures, + direct_coefficients: &decoded.direct_coefficients, + selector_domains: &decoded.selector_domains, + selector_memberships: &decoded.selector_memberships, + reduction_groups: &decoded.reduction_groups, + reduction_entries: &decoded.reduction_entries, + exact_factors: &decoded.exact_factors, + color_contraction_entry_start: decoded.color_contraction_entry_start, + color_contraction_entry_count: decoded.color_contraction_entry_count, + }; + let runtime_options = EagerRuntimeOptions { + point_tile_size: decoded.runtime_options.point_tile_size, + workspace_bytes: decoded.runtime_options.workspace_bytes, + }; + #[cfg(not(any(feature = "f64-compiled", feature = "f64-symjit")))] + { + let _ = (sections, runtime_options, parameter_projection); + return Err(RusticolError::compatibility( + "this artifact requires eager-direct-arena-v1, which is unavailable without the \ + f64-compiled/f64-symjit features; use a Direct-Arena-capable runtime", + )); + } + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + { + let direct = load_eager_v3_direct_scheduler( + &pack.manifest, + &kernel_payloads, + &decoded, + sections, + runtime_options, + )?; + let (raw_sum_groups, color_contraction) = reduction_runtime(&decoded, manifest)?; + let color_topology_replay = + super::eager_v3_common::build_eager_color_topology_replay_amplitude_runtime( + &decoded, + &raw_sum_groups, + usize::try_from(decoded.dimensions.amplitude_count) + .map_err(|_| RusticolError::artifact("eager amplitude count exceeds usize"))?, + )?; + if common.color_topology_replay_enabled != color_topology_replay.is_some() { + return Err(RusticolError::integrity( + "eager color replay proof and amplitude gather disagree", + )); + } + if let Some(replay) = color_topology_replay.as_ref() + && replay.color_topology_replay_mappings().as_deref() + != Some(common.color_topology_replay_mappings.as_slice()) + { + return Err(RusticolError::integrity( + "eager color replay proof mappings do not match amplitude gather mappings", + )); + } + if let Some(selector_group_ids) = direct.plan().selector_group_ids() { + let known_group_ids = raw_sum_groups + .iter() + .map(|group| { + u32::try_from(group.id).map_err(|_| { + RusticolError::integrity(format!( + "eager coherent group {} does not fit the selector-domain ABI", + group.id + )) + }) + }) + .collect::>>()?; + if let Some(unknown) = selector_group_ids + .iter() + .find(|group_id| !known_group_ids.contains(group_id)) + { + return Err(RusticolError::integrity(format!( + "eager selector domains reference unknown coherent group {unknown}" + ))); + } + } + let lane = EagerNativeRuntime::new_direct( + direct, + pack.manifest.backend, + parameter_projection, + raw_sum_groups, + color_contraction, + color_topology_replay, + ); + Ok(LoadedEagerV3Runtime { common, lane }) + } +} + +#[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] +fn load_eager_v3_direct_scheduler( + pack: &PreparedKernelPackManifest, + payloads: &EvaluatorPayloadStore, + decoded: &DecodedEagerRuntimeV3, + sections: EagerPlanV3Sections<'_>, + options: EagerRuntimeOptions, +) -> RusticolResult { + match pack.backend.as_str() { + "jit" => { + #[cfg(not(feature = "f64-symjit"))] + return Err(RusticolError::compatibility( + "eager Direct-Arena JIT execution requires the f64-symjit feature", + )); + } + "cpp" | "asm" => { + #[cfg(not(feature = "f64-compiled"))] + return Err(RusticolError::compatibility( + "eager Direct-Arena C++/ASM execution requires the f64-compiled feature", + )); + } + other => { + return Err(RusticolError::compatibility(format!( + "unsupported eager Direct-Arena prepared backend {other:?}" + ))); + } + } + + struct OwnedPrepared { + kernel_id: u32, + role: crate::EagerKernelRole, + inputs: Vec, + output_component_count: u32, + application: OwnedPreparedApplication, + display_path: PathBuf, + } + + enum OwnedPreparedApplication { + Symjit { + source: Vec, + descriptor: Vec, + expected_optimization_level: u32, + expected_compression: bool, + }, + Native { + library: Arc, + function_name: String, + source_application_abi: String, + invocation_stride: u32, + attachment_stride: u32, + target_triple: String, + evaluator_state_sha256: String, + simd_lane_width: u32, + }, + } + + let mut owned = Vec::with_capacity(decoded.kernel_specs.len()); + for kernel in &decoded.kernel_specs { + let manifest = pack + .kernels + .iter() + .find(|candidate| candidate.kernel_id == kernel.kernel_id) + .ok_or_else(|| { + RusticolError::integrity(format!( + "eager Direct-Arena kernel {} has no prepared manifest", + kernel.kernel_id + )) + })?; + let direct = manifest.eager_direct_table_manifest()?; + let (application, display_path) = match pack.backend.as_str() { + "jit" => { + if direct.source_application_abi + != crate::eager_layout::EAGER_DIRECT_SOURCE_APPLICATION_ABI + { + return Err(RusticolError::compatibility(format!( + "eager Direct-Arena JIT kernel {} has a native source ABI", + kernel.kernel_id + ))); + } + let plane_application = manifest + .f64_evaluator_manifest + .get("plane_application") + .and_then(serde_json::Value::as_object) + .ok_or_else(|| { + RusticolError::compatibility(format!( + "eager Direct-Arena kernel {} has no SymJIT plane application", + kernel.kernel_id + )) + })?; + let application_path = plane_application + .get("application_path") + .and_then(serde_json::Value::as_str) + .ok_or_else(|| { + RusticolError::artifact(format!( + "eager Direct-Arena kernel {} has no plane-application path", + kernel.kernel_id + )) + })?; + let expected_optimization_level = plane_application + .get("optimization_level") + .and_then(serde_json::Value::as_u64) + .and_then(|level| u32::try_from(level).ok()) + .ok_or_else(|| { + RusticolError::compatibility(format!( + "eager Direct-Arena kernel {} has no authenticated SymJIT \ + optimization level; regenerate the prepared model", + kernel.kernel_id + )) + })?; + let expected_compression = plane_application + .get("compression") + .and_then(serde_json::Value::as_bool) + .ok_or_else(|| { + RusticolError::compatibility(format!( + "eager Direct-Arena kernel {} has no authenticated SymJIT \ + compression setting; regenerate the prepared model", + kernel.kernel_id + )) + })?; + if plane_application + .get("application_abi") + .and_then(serde_json::Value::as_str) + != Some(crate::eager_layout::EAGER_DIRECT_SOURCE_APPLICATION_ABI) + { + return Err(RusticolError::compatibility(format!( + "eager Direct-Arena kernel {} has an incompatible plane-application ABI", + kernel.kernel_id + ))); + } + let source = payloads.source(application_path)?; + let source_bytes = source.read()?.into_owned(); + let display_path = PathBuf::from(source.display_name()); + let descriptor_path = direct.descriptor_path.as_deref().ok_or_else(|| { + RusticolError::artifact("eager JIT DirectTable has no descriptor path") + })?; + let descriptor_source = payloads.source(descriptor_path)?; + let descriptor = descriptor_source.read()?.into_owned(); + if u64::try_from(descriptor.len()).ok() != direct.descriptor_size_bytes { + return Err(RusticolError::integrity(format!( + "eager DirectTable descriptor for kernel {} has size {}, expected {:?}", + kernel.kernel_id, + descriptor.len(), + direct.descriptor_size_bytes, + ))); + } + let actual_descriptor_sha = format!("{:x}", Sha256::digest(&descriptor)); + if Some(actual_descriptor_sha.as_str()) != direct.descriptor_sha256.as_deref() { + return Err(RusticolError::integrity(format!( + "eager DirectTable descriptor digest mismatch for kernel {}", + kernel.kernel_id + ))); + } + ( + OwnedPreparedApplication::Symjit { + source: source_bytes, + descriptor, + expected_optimization_level, + expected_compression, + }, + display_path, + ) + } + "cpp" | "asm" => { + if direct.source_application_abi + != crate::eager_layout::EAGER_NATIVE_DIRECT_TABLE_APPLICATION_ABI + { + return Err(RusticolError::compatibility(format!( + "eager Direct-Arena native kernel {} has a JIT source ABI", + kernel.kernel_id + ))); + } + let library_path = direct.library_path.as_deref().ok_or_else(|| { + RusticolError::artifact("eager native DirectTable has no library path") + })?; + let library = payloads.load_native_library(library_path)?; + let display_path = library.display_path().to_path_buf(); + let function_name = direct.function_name.clone().ok_or_else(|| { + RusticolError::artifact("eager native DirectTable has no function name") + })?; + let expected = + format!("pyamplicol_eager_direct_table_k{:08x}_v1", kernel.kernel_id); + if function_name != expected { + return Err(RusticolError::integrity(format!( + "eager native DirectTable kernel {} exports {function_name:?}, expected {expected:?}", + kernel.kernel_id + ))); + } + let expected_state_sha = + direct.evaluator_state_sha256.as_deref().ok_or_else(|| { + RusticolError::artifact( + "eager native DirectTable has no evaluator-state digest", + ) + })?; + let exact_state = payloads.source(&manifest.exact_evaluator_state_path)?; + let exact_state_bytes = exact_state.read()?; + let actual_state_sha = format!("{:x}", Sha256::digest(exact_state_bytes.as_ref())); + if actual_state_sha != expected_state_sha { + return Err(RusticolError::integrity(format!( + "eager native DirectTable evaluator-state digest mismatch for kernel {}", + kernel.kernel_id + ))); + } + ( + OwnedPreparedApplication::Native { + library, + function_name, + source_application_abi: direct.source_application_abi.clone(), + invocation_stride: direct.invocation_stride.unwrap_or(0), + attachment_stride: direct.attachment_stride.unwrap_or(0), + target_triple: pack.target.target_triple.clone(), + evaluator_state_sha256: direct + .evaluator_state_sha256 + .clone() + .unwrap_or_default(), + simd_lane_width: direct.simd_lane_width.unwrap_or(0), + }, + display_path, + ) + } + _ => unreachable!("prepared backend validated"), + }; + if direct.input_complex_count + != count_u32_for_direct(kernel.inputs.len(), "kernel input width")? + || direct.output_complex_count != kernel.output_component_count + { + return Err(RusticolError::integrity(format!( + "eager DirectTable descriptor binding width mismatch for kernel {}", + kernel.kernel_id + ))); + } + owned.push(OwnedPrepared { + kernel_id: kernel.kernel_id, + role: kernel.role, + inputs: kernel.inputs.clone(), + output_component_count: kernel.output_component_count, + application, + display_path, + }); + } + let prepared = owned + .iter() + .map(|kernel| { + let application = match &kernel.application { + OwnedPreparedApplication::Symjit { + source, + descriptor, + expected_optimization_level, + expected_compression, + } => crate::eager_runtime::EagerDirectPreparedApplication::Symjit { + source_application: source, + descriptor, + expected_optimization_level: *expected_optimization_level, + expected_compression: *expected_compression, + }, + OwnedPreparedApplication::Native { + library, + function_name, + source_application_abi, + invocation_stride, + attachment_stride, + target_triple, + evaluator_state_sha256, + simd_lane_width, + } => crate::eager_runtime::EagerDirectPreparedApplication::Native { + library, + function_name, + source_application_abi, + invocation_stride: *invocation_stride, + attachment_stride: *attachment_stride, + target_triple, + evaluator_state_sha256, + simd_lane_width: *simd_lane_width, + }, + }; + crate::eager_runtime::EagerDirectPreparedKernel { + kernel_id: kernel.kernel_id, + role: kernel.role, + inputs: &kernel.inputs, + output_component_count: kernel.output_component_count, + application, + display_path: kernel.display_path.clone(), + } + }) + .collect::>(); + crate::eager_runtime::EagerDirectExecutionRuntime::from_plan_v3_sections( + sections, &prepared, options, + ) +} + +#[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] +fn count_u32_for_direct(value: usize, label: &str) -> RusticolResult { + u32::try_from(value) + .map_err(|_| RusticolError::invalid_argument(format!("eager direct {label} exceeds u32"))) +} + +pub(super) fn load_eager_v3_exact_sections( + artifact: &VerifiedArtifact, + evaluator_root: &Path, + manifest: &EagerV3ExecutionManifest, + physics: &mut ProcessPhysicsV1, +) -> RusticolResult { + let pack = load_eager_v3_prepared_pack(artifact, manifest)?; + let container = open_verified_eager_v3_runtime_container(artifact, evaluator_root, manifest)?; + let decoded = + super::eager_v3_decode::decode_eager_v3_runtime(&container, manifest, &pack.manifest)?; + let color_selector_ids = native_color_selector_ids( + &decoded.color_selectors, + &decoded.u32_sequence_ranges, + &decoded.u32_sequence_values, + physics, + )?; + hydrate_native_reduction_groups( + physics, + &decoded.reduction_groups, + &decoded.reduction_entries, + &color_selector_ids, + )?; + + let amplitude_stage = exact_amplitude_stage(&decoded, manifest)?; + let exact_schema = super::eager_v3_common::build_eager_v3_exact_schema( + &decoded, + manifest, + physics.clone(), + amplitude_stage, + )?; + let reduction_groups = serde_json::to_value(&physics.reduction.groups).map_err(|error| { + RusticolError::serialization(format!( + "could not serialize compact eager exact reduction groups: {error}" + )) + })?; + let selector_group_ids = decoded + .reduction_groups + .iter() + .map(|group| group.coherent_group_id) + .collect::>(); + let selector_domains = decoded + .selector_domains + .iter() + .map(|domain| { + Ok(table_range( + &decoded.selector_memberships, + domain.member_start, + domain.member_count, + "selector-domain members", + )? + .to_vec()) + }) + .collect::>>()?; + + let couplings = decoded + .couplings + .iter() + .map(|row| { + let [constant_real, constant_imaginary] = + exact_factor_pair(&decoded, row.constant_factor_id, "coupling")?; + Ok(NativeEagerExactCoupling { + real_parameter_id: row.real_parameter_id, + imaginary_parameter_id: row.imaginary_parameter_id, + constant_real, + constant_imaginary, + }) + }) + .collect::>>()?; + + let stages = decoded + .stages + .iter() + .map(|row| NativeEagerExactStage { + stage_index: row.stage_index, + invocation_start: row.invocation_start, + invocation_count: row.invocation_count, + attachment_start: row.attachment_start, + attachment_count: row.attachment_count, + finalization_start: row.finalization_start, + finalization_count: row.finalization_count, + }) + .collect(); + let invocations = decoded + .invocations + .iter() + .map(|row| NativeEagerExactInvocation { + kernel_id: row.kernel_id, + left_value_slot_id: row.left_value_slot_id, + right_value_slot_id: row.right_value_slot_id, + left_momentum_slot_id: row.left_momentum_slot_id, + right_momentum_slot_id: row.right_momentum_slot_id, + coupling_slot_id: row.coupling_slot_id, + output_factor_source: row.output_factor_source, + attachment_start: row.attachment_start, + attachment_count: row.attachment_count, + selector_domain_id: row.selector_domain_id, + }) + .collect(); + let attachments = decoded + .attachments + .iter() + .map(|row| { + let representative = exact_factor( + &decoded, + row.representative_evaluation_factor_id, + "attachment representative", + )?; + if representative == Complex::new(0.0, 0.0) { + return Err(RusticolError::integrity( + "eager exact attachment representative factor is zero", + )); + } + Ok(NativeEagerExactAttachment { + result_current_id: row.result_current_id, + factor_numerators: vec![ + exact_factor_pair(&decoded, row.color_factor_id, "attachment color")?, + exact_factor_pair(&decoded, row.evaluation_factor_id, "attachment evaluation")?, + exact_factor_pair( + &decoded, + row.normalization_factor_id, + "attachment normalization", + )?, + ], + factor_denominator: Some(exact_factor_pair( + &decoded, + row.representative_evaluation_factor_id, + "attachment representative", + )?), + selector_domain_id: row.selector_domain_id, + }) + }) + .collect::>>()?; + let finalizations = decoded + .finalizations + .iter() + .map(|row| NativeEagerExactFinalization { + kernel_id: row.kernel_id, + current_id: row.current_id, + unpropagated_value_slot_id: row.unpropagated_value_slot_id, + propagated_value_slot_id: row.propagated_value_slot_id, + momentum_slot_id: row.momentum_slot_id, + unpropagated_selector_domain_id: row.unpropagated_selector_domain_id, + propagated_selector_domain_id: row.propagated_selector_domain_id, + }) + .collect(); + let closures = decoded + .closures + .iter() + .map(|row| exact_closure(&decoded, row)) + .collect::>>()?; + + Ok(NativeEagerExactSections { + process_id: manifest.key.clone(), + exact_schema, + reduction_groups, + selector_group_ids, + selector_domains, + couplings, + stages, + invocations, + attachments, + finalizations, + closures, + }) +} + +pub(super) fn load_eager_v3_reduction_groups( + artifact: &VerifiedArtifact, + evaluator_root: &Path, + manifest: &EagerV3ExecutionManifest, + physics: &mut ProcessPhysicsV1, +) -> RusticolResult { + // Keep full artifact/container authentication, prepared-pack validation, + // and plan identity/count cross-checks, but materialize only the + // O(reduction) rows needed for public reduction groups. In particular, + // runtime-sized currents/stages/invocations/attachments/finalizations/ + // closures remain borrowed PACBIN payloads and are never converted. + let pack = load_eager_v3_prepared_pack(artifact, manifest)?; + drop(pack); + let container = open_verified_eager_v3_runtime_container(artifact, evaluator_root, manifest)?; + let decoded = super::eager_v3_decode::decode_eager_v3_reduction_groups(&container, manifest)?; + validate_native_helicity_selectors( + &decoded.helicity_selectors, + &decoded.helicity_sequences, + physics, + )?; + let color_selector_ids = native_projected_color_selector_ids( + &decoded.color_selectors, + &decoded.color_sequences, + physics, + )?; + hydrate_native_reduction_groups( + physics, + &decoded.reduction_groups, + &decoded.reduction_entries, + &color_selector_ids, + )?; + serde_json::to_value(&physics.reduction.groups).map_err(|error| { + RusticolError::serialization(format!( + "could not serialize compact eager reduction groups: {error}" + )) + }) +} + +fn exact_amplitude_stage( + decoded: &DecodedEagerRuntimeV3, + manifest: &EagerV3ExecutionManifest, +) -> RusticolResult { + let color_sector_by_group = coherent_group_color_sector_ids(decoded)?; + let group_weights = decoded + .reduction_groups + .iter() + .map(|group| { + exact_real_factor(decoded, group.helicity_weight_factor_id, "helicity weight")?; + exact_real_factor( + decoded, + group.all_sector_weight_factor_id, + "all-sector weight", + )?; + Ok(( + group.coherent_group_id, + ( + group.helicity_weight_factor_id, + group.all_sector_weight_factor_id, + ), + )) + }) + .collect::>>()?; + let roots = decoded + .closures + .iter() + .map(|root| { + let (helicity_weight_id, all_sector_weight_id) = group_weights + .get(&root.coherent_group_id) + .ok_or_else(|| { + RusticolError::integrity("eager exact closure references an unknown coherent group") + })?; + Ok(json!({ + "output_index": root.amplitude_index, + "root_id": root.root_id, + "kind": if root.kernel_id == MISSING_U32 { + "direct-contraction" + } else { + "kernel-closure" + }, + "coherent_group_id": root.coherent_group_id, + "color_sector_id": color_sector_by_group + .get(&root.coherent_group_id) + .copied() + .ok_or_else(|| RusticolError::integrity( + "eager exact closure references an unknown coherent-group color sector" + ))?, + "helicity_weight": exact_real_factor_number(decoded, *helicity_weight_id, "helicity weight")?, + "all_sector_weight": exact_real_factor_number(decoded, *all_sector_weight_id, "all-sector weight")?, + })) + }) + .collect::>>()?; + let materialized_color_contraction = if manifest.color_accuracy == "lc" { + Value::Null + } else { + let entries = reduction_range( + &decoded.reduction_entries, + decoded.color_contraction_entry_start, + decoded.color_contraction_entry_count, + "exact color contraction", + )? + .iter() + .map(|row| { + if row.kind != EagerPlanReductionEntryKind::ColorContraction { + return Err(RusticolError::integrity( + "eager exact color-contraction range contains another entry kind", + )); + } + let weight = exact_factor_pair(decoded, row.factor_id, "color-contraction weight")?; + let symmetry = if row.auxiliary_factor_id == MISSING_U32 { + exact_number(1.0) + } else { + exact_real_factor_number(decoded, row.auxiliary_factor_id, "color symmetry factor")? + }; + Ok(json!({ + "left_group_id": row.left_id, + "right_group_id": row.right_id, + "weight": weight, + "exact_weight": super::eager_v3_common::eager_exact_color_weight(decoded, row.factor_id)?, + "symmetry_factor": symmetry, + })) + }) + .collect::>>()?; + json!({"entries": entries}) + }; + let (color_topology_replay, color_contraction) = + if let Some((replay, contraction)) = exact_color_topology_replay(decoded)? { + (replay, contraction) + } else { + (Value::Null, materialized_color_contraction) + }; + Ok(json!({ + "stage_kind": "amplitude-roots", + "output_count": decoded.dimensions.amplitude_count, + "roots": roots, + "color_contraction": color_contraction, + "color_topology_replay": color_topology_replay, + })) +} + +fn exact_color_topology_replay( + decoded: &DecodedEagerRuntimeV3, +) -> RusticolResult> { + let Some((replay, contraction)) = + super::eager_v3_common::load_eager_color_topology_replay_manifests(decoded, true)? + else { + return Ok(None); + }; + let physical_groups = replay + .physical_groups + .iter() + .map(|group| { + if !group.helicity_weight.is_finite() || group.helicity_weight <= 0.0 { + return Err(RusticolError::integrity( + "eager exact color replay helicity weight is invalid", + )); + } + Ok(json!({ + "group_id": group.group_id, + "helicities": group.helicities, + "color_sector_id": group.color_sector_id, + "color_word": group.color_word, + "helicity_weight": exact_number(group.helicity_weight), + })) + }) + .collect::>>()?; + let mappings = replay + .mappings + .iter() + .map(|mapping| { + let routes = mapping + .group_routes + .iter() + .map(|route| { + Ok(json!({ + "source_group_id": route.source_group_id, + "target_group_id": route.target_group_id, + "factor": exact_f64_pair(&route.factor, "color replay route")?, + })) + }) + .collect::>>()?; + Ok(json!({ + "label_permutation": mapping.label_permutation, + "group_routes": routes, + })) + }) + .collect::>>()?; + let entries = contraction + .entries + .iter() + .map(|entry| { + if !entry.symmetry_factor.is_finite() { + return Err(RusticolError::integrity( + "eager exact color replay symmetry factor is invalid", + )); + } + Ok(json!({ + "left_group_id": entry.left_group_id, + "right_group_id": entry.right_group_id, + "weight": exact_f64_pair(&entry.weight, "color replay contraction")?, + "exact_weight": entry._exact_weight, + "symmetry_factor": exact_number(entry.symmetry_factor), + })) + }) + .collect::>>()?; + let repeated_block = contraction + .repeated_block + .as_ref() + .map(|block| { + let entries = block + .entries + .iter() + .map(|entry| { + if !entry.symmetry_factor.is_finite() { + return Err(RusticolError::integrity( + "eager exact repeated color symmetry factor is invalid", + )); + } + Ok(json!({ + "left_group_index": entry.left_group_index, + "right_group_index": entry.right_group_index, + "weight": exact_f64_pair( + &entry.weight, + "repeated color replay contraction", + )?, + "exact_weight": entry._exact_weight, + "symmetry_factor": exact_number(entry.symmetry_factor), + })) + }) + .collect::>>()?; + Ok(json!({ + "component_count": block.component_count, + "component_group_ids": block.component_group_ids, + "entries": entries, + })) + }) + .transpose()?; + let replay = json!({ + "contract_version": replay.contract_version, + "physical_group_count": replay.physical_group_count, + "physical_groups": physical_groups, + "mappings": mappings, + }); + let contraction = json!({ + "supported": contraction.supported, + "reason": contraction.reason, + "group_count": contraction.group_count, + "includes_color_factor": contraction.includes_color_factor, + "entries": entries, + "repeated_block": repeated_block, + }); + Ok(Some((replay, contraction))) +} + +fn exact_f64_pair(values: &[f64], context: &str) -> RusticolResult<[String; 2]> { + let [real, imaginary] = values else { + return Err(RusticolError::integrity(format!( + "eager exact {context} factor is not complex data" + ))); + }; + if !real.is_finite() || !imaginary.is_finite() { + return Err(RusticolError::integrity(format!( + "eager exact {context} factor is not finite" + ))); + } + Ok([exact_number(*real), exact_number(*imaginary)]) +} + +fn coherent_group_color_sector_ids( + decoded: &DecodedEagerRuntimeV3, +) -> RusticolResult> { + let table = decoded + .retained_tables + .iter() + .find(|table| table.name.as_ref() == "coherent_groups") + .ok_or_else(|| RusticolError::integrity("eager coherent-group table is absent"))?; + let column = table + .columns + .iter() + .find(|column| column.name.as_ref() == "color_sector_id") + .ok_or_else(|| { + RusticolError::integrity("eager coherent-group color-sector column is absent") + })?; + if column.elements_per_row != 1 { + return Err(RusticolError::integrity( + "eager coherent-group color-sector column is not scalar", + )); + } + let super::eager_v3_decode::DecodedEagerPrimitiveColumn::U32(color_sector_ids) = &column.values + else { + return Err(RusticolError::integrity( + "eager coherent-group color-sector column has the wrong primitive type", + )); + }; + if color_sector_ids.len() != decoded.reduction_groups.len() + || color_sector_ids.len() + != usize::try_from(table.row_count).map_err(|_| { + RusticolError::artifact("eager coherent-group row count exceeds usize") + })? + { + return Err(RusticolError::integrity( + "eager coherent-group color-sector coverage is inconsistent", + )); + } + decoded + .reduction_groups + .iter() + .zip(color_sector_ids) + .map(|(group, sector_id)| Ok((group.coherent_group_id, *sector_id))) + .collect() +} + +fn exact_closure( + decoded: &DecodedEagerRuntimeV3, + row: &crate::EagerPlanClosureRow, +) -> RusticolResult { + let color = exact_factor_pair(decoded, row.color_factor_id, "closure color")?; + let (factor_numerators, direct_coefficients) = if row.kernel_id == MISSING_U32 { + let coefficients = table_range( + &decoded.direct_coefficients, + row.direct_coefficient_start, + row.direct_coefficient_count, + "direct closure coefficients", + )? + .iter() + .enumerate() + .map(|(component, coefficient)| { + if coefficient.component_index as usize != component { + return Err(RusticolError::integrity( + "eager exact direct coefficients are not component ordered", + )); + } + exact_factor_pair(decoded, coefficient.factor_id, "direct coefficient") + }) + .collect::>>()?; + (vec![color], Some(coefficients)) + } else { + ( + vec![ + color, + exact_factor_pair( + decoded, + row.normalization_factor_id, + "closure normalization", + )?, + ], + None, + ) + }; + Ok(NativeEagerExactClosure { + kernel_id: row.kernel_id, + left_value_slot_id: row.left_value_slot_id, + right_value_slot_id: row.right_value_slot_id, + amplitude_index: row.amplitude_index, + coupling_slot_id: row.coupling_slot_id, + output_factor_source: row.output_factor_source, + factor_numerators, + factor_denominator: None, + direct_coefficients, + coherent_group_id: row.coherent_group_id, + selector_domain_id: row.selector_domain_id, + }) +} + +fn exact_number(value: f64) -> String { + format!("binary64:{:016x}", value.to_bits()) +} + +fn exact_factor_pair( + decoded: &DecodedEagerRuntimeV3, + factor_id: u32, + context: &str, +) -> RusticolResult<[String; 2]> { + let factor = decoded + .exact_factors + .get(factor_id as usize) + .ok_or_else(|| RusticolError::integrity(format!("eager {context} factor is absent")))?; + let value = exact_factor(decoded, factor_id, context)?; + if !value.re.is_finite() || !value.im.is_finite() { + return Err(RusticolError::integrity(format!( + "eager {context} factor is not finite" + ))); + } + Ok([ + format!("binary64:{:016x}", factor.real_bits), + format!("binary64:{:016x}", factor.imaginary_bits), + ]) +} + +fn exact_real_factor_number( + decoded: &DecodedEagerRuntimeV3, + factor_id: u32, + context: &str, +) -> RusticolResult { + exact_real_factor(decoded, factor_id, context)?; + Ok(exact_factor_pair(decoded, factor_id, context)?[0].clone()) +} + +pub(super) fn reject_native_reduction_groups_for_compiled( + physics: &ProcessPhysicsV1, +) -> RusticolResult<()> { + if physics + .extensions + .contains_key(NATIVE_REDUCTION_GROUPS_EXTENSION_KEY) + { + return Err(RusticolError::compatibility( + "compact PACBIN-backed reduction groups require eager plan-v3 execution; regenerate this compiled artifact", + )); + } + Ok(()) +} + +fn hydrate_native_reduction_groups( + physics: &mut ProcessPhysicsV1, + groups: &[crate::EagerPlanReductionGroupRow], + entries: &[crate::EagerPlanReductionEntryRow], + color_selector_ids: &[String], +) -> RusticolResult<()> { + let Some(value) = physics + .extensions + .get(NATIVE_REDUCTION_GROUPS_EXTENSION_KEY) + else { + return Ok(()); + }; + if !physics.reduction.groups.is_empty() { + return Err(RusticolError::integrity( + "compact native reduction metadata may not duplicate expanded reduction groups", + )); + } + let descriptor: NativeReductionGroupsDescriptor = serde_json::from_value(value.clone()) + .map_err(|error| { + RusticolError::serialization(format!( + "could not parse compact native reduction descriptor: {error}" + )) + })?; + descriptor.validate()?; + let declared_group_count = usize::try_from(descriptor.group_count).map_err(|_| { + RusticolError::artifact("compact native reduction group count exceeds usize") + })?; + if declared_group_count != groups.len() { + return Err(RusticolError::integrity(format!( + "compact native reduction descriptor declares {declared_group_count} groups, PACBIN contains {}", + groups.len() + ))); + } + + let mut hydrated = Vec::with_capacity(groups.len()); + let mut seen_group_ids = BTreeSet::new(); + for group in groups { + if !seen_group_ids.insert(group.coherent_group_id) { + return Err(RusticolError::integrity(format!( + "compact native reduction metadata contains duplicate coherent group {}", + group.coherent_group_id + ))); + } + let selectors = reduction_range( + entries, + group.selector_entry_start, + group.selector_entry_count, + "selector members", + )?; + let representative = selectors.first().ok_or_else(|| { + RusticolError::integrity(format!( + "compact native reduction group {} has no selector members", + group.coherent_group_id + )) + })?; + + let mut physical_helicity_ids = Vec::new(); + let mut physical_color_ids = Vec::new(); + let mut seen_helicities = BTreeSet::new(); + let mut seen_colors = BTreeSet::new(); + for selector in selectors { + if selector.kind != EagerPlanReductionEntryKind::SelectorMember + || selector.owner_id != group.coherent_group_id + { + return Err(RusticolError::integrity(format!( + "compact native reduction group {} contains an invalid selector member", + group.coherent_group_id + ))); + } + let helicity_index = usize::try_from(selector.left_id).map_err(|_| { + RusticolError::artifact("compact native helicity selector ID exceeds usize") + })?; + let color_index = usize::try_from(selector.right_id).map_err(|_| { + RusticolError::artifact("compact native color selector ID exceeds usize") + })?; + let helicity = physics.helicities.get(helicity_index).ok_or_else(|| { + RusticolError::integrity(format!( + "compact native reduction group {} references unknown helicity selector {}", + group.coherent_group_id, selector.left_id + )) + })?; + let color_id = color_selector_ids.get(color_index).ok_or_else(|| { + RusticolError::integrity(format!( + "compact native reduction group {} references unknown color selector {}", + group.coherent_group_id, selector.right_id + )) + })?; + if seen_helicities.insert(selector.left_id) { + physical_helicity_ids.push(helicity.id.clone()); + } + if seen_colors.insert(selector.right_id) { + physical_color_ids.push(color_id.clone()); + } + } + + let representative_helicity_id = physics + .helicities + .get(representative.left_id as usize) + .ok_or_else(|| { + RusticolError::integrity("compact native representative helicity is absent") + })? + .id + .clone(); + let representative_color_id = color_selector_ids + .get(representative.right_id as usize) + .ok_or_else(|| { + RusticolError::integrity("compact native representative color is absent") + })? + .clone(); + hydrated.push(crate::ReductionGroup { + id: format!("reduction:{}", group.coherent_group_id), + representative_helicity_id, + representative_color_id, + physical_helicity_ids, + physical_color_ids, + }); + } + physics.reduction.groups = hydrated; + physics.validate()?; + Ok(()) +} + +fn native_color_selector_ids( + color_selectors: &[crate::EagerPlanColorSelectorRow], + u32_sequence_ranges: &[crate::EagerPlanCatalogRangeRow], + u32_sequence_values: &[u32], + physics: &ProcessPhysicsV1, +) -> RusticolResult> { + let catalog = native_color_selector_catalog(physics)?; + color_selectors + .iter() + .map(|selector| { + let word = eager_u32_sequence( + u32_sequence_ranges, + u32_sequence_values, + selector.word_sequence_id, + "color selector word", + )?; + let representative_word = eager_u32_sequence( + u32_sequence_ranges, + u32_sequence_values, + selector.representative_word_sequence_id, + "color selector representative word", + )?; + resolve_native_color_selector_id( + &catalog, + word, + representative_word, + selector.computed != 0, + ) + }) + .collect() +} + +fn validate_native_helicity_selectors( + helicity_selectors: &[crate::EagerPlanHelicitySelectorRow], + helicity_sequences: &BTreeMap>, + physics: &ProcessPhysicsV1, +) -> RusticolResult<()> { + if helicity_selectors.len() != physics.helicities.len() { + return Err(RusticolError::integrity(format!( + "compact native plan contains {} helicity selectors, public physics contains {}", + helicity_selectors.len(), + physics.helicities.len() + ))); + } + let helicity_by_id = physics + .helicities + .iter() + .map(|helicity| (helicity.id.as_str(), helicity)) + .collect::>(); + + for (index, selector) in helicity_selectors.iter().enumerate() { + let expected_id = u32::try_from(index).map_err(|_| { + RusticolError::artifact("compact native helicity selector count exceeds u32") + })?; + if selector.selector_id != expected_id { + return Err(RusticolError::integrity( + "compact native helicity selector IDs are not dense", + )); + } + let helicity = &physics.helicities[index]; + let values = projected_sequence( + helicity_sequences, + selector.values_sequence_id, + "helicity selector values", + )?; + if values != helicity.values.as_slice() { + return Err(RusticolError::integrity(format!( + "compact native helicity selector {} values disagree with public helicity {:?}", + selector.selector_id, helicity.id + ))); + } + let representative = helicity_by_id + .get(helicity.representative_id.as_str()) + .ok_or_else(|| { + RusticolError::integrity(format!( + "public helicity {:?} references unknown representative {:?}", + helicity.id, helicity.representative_id + )) + })?; + let representative_values = projected_sequence( + helicity_sequences, + selector.representative_sequence_id, + "helicity selector representative", + )?; + if representative_values != representative.values.as_slice() { + return Err(RusticolError::integrity(format!( + "compact native helicity selector {} representative disagrees with public representative {:?}", + selector.selector_id, representative.id + ))); + } + if (selector.computed != 0) != helicity.computed + || (selector.structural_zero != 0) != helicity.structural_zero + { + return Err(RusticolError::integrity(format!( + "compact native helicity selector {} flags disagree with public helicity {:?}", + selector.selector_id, helicity.id + ))); + } + } + Ok(()) +} + +fn native_projected_color_selector_ids( + color_selectors: &[crate::EagerPlanColorSelectorRow], + color_sequences: &BTreeMap>, + physics: &ProcessPhysicsV1, +) -> RusticolResult> { + let catalog = native_color_selector_catalog(physics)?; + color_selectors + .iter() + .map(|selector| { + let word = projected_sequence( + color_sequences, + selector.word_sequence_id, + "color selector word", + )?; + let representative_word = projected_sequence( + color_sequences, + selector.representative_word_sequence_id, + "color selector representative word", + )?; + resolve_native_color_selector_id( + &catalog, + word, + representative_word, + selector.computed != 0, + ) + }) + .collect() +} + +fn projected_sequence<'a, T>( + sequences: &'a BTreeMap>, + sequence_id: u32, + context: &str, +) -> RusticolResult<&'a [T]> { + sequences + .get(&sequence_id) + .map(Vec::as_slice) + .ok_or_else(|| { + RusticolError::integrity(format!( + "compact native {context} references unknown projected sequence {sequence_id}" + )) + }) +} + +fn eager_u32_sequence<'a>( + ranges: &'a [crate::EagerPlanCatalogRangeRow], + values: &'a [u32], + sequence_id: u32, + context: &str, +) -> RusticolResult<&'a [u32]> { + let range = ranges.get(sequence_id as usize).ok_or_else(|| { + RusticolError::integrity(format!( + "compact native {context} references unknown sequence {sequence_id}" + )) + })?; + let start = usize::try_from(range.start).map_err(|_| { + RusticolError::artifact(format!("compact native {context} start exceeds usize")) + })?; + let count = usize::try_from(range.count).map_err(|_| { + RusticolError::artifact(format!("compact native {context} count exceeds usize")) + })?; + let stop = start.checked_add(count).ok_or_else(|| { + RusticolError::artifact(format!("compact native {context} range overflows")) + })?; + values.get(start..stop).ok_or_else(|| { + RusticolError::integrity(format!("compact native {context} range is out of bounds")) + }) +} + +type NativeColorSelectorCatalog = BTreeMap, (String, Vec, bool)>; + +fn native_color_selector_catalog( + physics: &ProcessPhysicsV1, +) -> RusticolResult { + let by_id = physics + .color_components + .iter() + .map(|component| (component.id(), component)) + .collect::>(); + let mut result = BTreeMap::new(); + for component in &physics.color_components { + let (word, representative_word, computed) = match component { + crate::ColorComponent::LcFlow(flow) => { + let representative = + by_id.get(flow.representative_id.as_str()).ok_or_else(|| { + RusticolError::integrity(format!( + "compact native color selector {:?} has no representative", + flow.id + )) + })?; + let crate::ColorComponent::LcFlow(representative) = representative else { + return Err(RusticolError::integrity(format!( + "compact native LC color selector {:?} has a contracted representative", + flow.id + ))); + }; + ( + u32_color_word(&flow.word, &flow.id)?, + u32_color_word(&representative.word, &representative.id)?, + flow.computed, + ) + } + crate::ColorComponent::ContractedColor(_) => (Vec::new(), Vec::new(), true), + }; + if result + .insert( + word.clone(), + (component.id().to_string(), representative_word, computed), + ) + .is_some() + { + return Err(RusticolError::integrity(format!( + "public physics contains duplicate color word {word:?}" + ))); + } + } + Ok(result) +} + +fn u32_color_word(word: &[usize], color_id: &str) -> RusticolResult> { + word.iter() + .copied() + .map(|label| { + u32::try_from(label).map_err(|_| { + RusticolError::artifact(format!( + "public color component {color_id:?} contains a label exceeding u32" + )) + }) + }) + .collect() +} + +fn resolve_native_color_selector_id( + catalog: &NativeColorSelectorCatalog, + word: &[u32], + representative_word: &[u32], + computed: bool, +) -> RusticolResult { + let (id, expected_representative, expected_computed) = catalog.get(word).ok_or_else(|| { + RusticolError::integrity(format!( + "compact native color selector word {word:?} is absent from public physics" + )) + })?; + if *expected_computed != computed || expected_representative != representative_word { + return Err(RusticolError::integrity(format!( + "compact native color selector {id:?} is inconsistent with public physics" + ))); + } + Ok(id.clone()) +} + +impl NativeReductionGroupsDescriptor { + fn validate(&self) -> RusticolResult<()> { + let valid = self.kind == NATIVE_REDUCTION_GROUPS_KIND + && self.schema_version == NATIVE_REDUCTION_GROUPS_SCHEMA_VERSION + && self.storage_abi == NATIVE_REDUCTION_GROUPS_STORAGE_ABI + && self.runtime_layout_abi == NATIVE_REDUCTION_GROUPS_RUNTIME_LAYOUT_ABI + && self.container_path == NATIVE_REDUCTION_GROUPS_CONTAINER_PATH + && self.group_member == NATIVE_REDUCTION_GROUPS_GROUP_MEMBER + && self.entry_member == NATIVE_REDUCTION_GROUPS_ENTRY_MEMBER; + if !valid { + return Err(RusticolError::compatibility( + "unsupported compact native reduction descriptor; regenerate the eager artifact", + )); + } + Ok(()) + } +} + +pub(super) fn open_verified_eager_v3_runtime_container( + artifact: &VerifiedArtifact, + evaluator_root: &Path, + manifest: &EagerV3ExecutionManifest, +) -> RusticolResult { + let container = &manifest.plan.runtime_container; + let path = evaluator_root.join(&container.path); + let relative = path.strip_prefix(artifact.root()).map_err(|_| { + RusticolError::security("eager runtime container escapes the verified artifact root") + })?; + let mut parts = Vec::new(); + for component in relative.components() { + let std::path::Component::Normal(part) = component else { + return Err(RusticolError::security( + "eager runtime container path is not canonical", + )); + }; + parts.push(part.to_str().ok_or_else(|| { + RusticolError::security("eager runtime container path is not valid UTF-8") + })?); + } + let logical_path = parts.join("/"); + let payload = artifact.payload(&logical_path)?; + if payload.role != crate::PayloadRole::EvaluatorState + || payload.media_type != "application/octet-stream" + || payload.process_id.as_deref() != Some(manifest.key.as_str()) + || payload.executable + { + return Err(RusticolError::integrity( + "eager runtime container has an invalid outer payload declaration", + )); + } + if payload.size_bytes != container.size_bytes || payload.sha256 != container.sha256 { + return Err(RusticolError::integrity( + "eager runtime container metadata disagrees with its authenticated outer payload", + )); + } + let container_file = artifact.open_payload_file(&logical_path)?; + super::eager_v3_manifest::open_eager_v3_runtime_container(container_file, &path, manifest) +} + +pub(super) fn prepare_plan_v3_parameter_state( + pack: &PreparedKernelPackManifest, + decoded: &DecodedEagerRuntimeV3, + runtime_parameters: &[GenericRuntimeModelParameterManifest], + payloads: &EvaluatorPayloadStore, +) -> RusticolResult<( + EagerParameterProjection, + Vec, + Option, +)> { + let legacy_rows = decoded + .couplings + .iter() + .map(|row| { + let constant = exact_factor(decoded, row.constant_factor_id, "coupling constant")?; + Ok(EagerCouplingRow { + real_parameter_id: row.real_parameter_id, + imag_parameter_id: row.imaginary_parameter_id, + constant_real: constant.re, + constant_imag: constant.im, + }) + }) + .collect::>>()?; + let encoded = EagerCouplingRow::encode_table(&legacy_rows)?; + let active_kernel_ids = decoded + .kernel_specs + .iter() + .map(|kernel| kernel.kernel_id) + .collect::>(); + let (projection, remapped, evaluator) = super::eager_load::prepare_eager_parameter_state( + pack, + &active_kernel_ids, + runtime_parameters, + &encoded, + payloads, + )?; + let remapped = EagerCouplingRow::decode_table(&remapped)?; + if remapped.len() != decoded.couplings.len() { + return Err(RusticolError::integrity( + "projected eager coupling count changed", + )); + } + let couplings = decoded + .couplings + .iter() + .zip(remapped) + .map(|(source, projected)| EagerPlanCouplingRow { + coupling_id: source.coupling_id, + real_parameter_id: projected.real_parameter_id, + imaginary_parameter_id: projected.imag_parameter_id, + constant_factor_id: source.constant_factor_id, + }) + .collect(); + Ok((projection, couplings, evaluator)) +} + +fn reduction_runtime( + decoded: &DecodedEagerRuntimeV3, + manifest: &EagerV3ExecutionManifest, +) -> RusticolResult<(Vec, Option)> { + let color_sector_by_group = (manifest.color_accuracy == "lc") + .then(|| coherent_group_color_sector_ids(decoded)) + .transpose()?; + let mut groups = Vec::with_capacity(decoded.reduction_groups.len()); + for group in &decoded.reduction_groups { + let amplitudes = reduction_range( + &decoded.reduction_entries, + group.amplitude_entry_start, + group.amplitude_entry_count, + "amplitude members", + )?; + let selectors = reduction_range( + &decoded.reduction_entries, + group.selector_entry_start, + group.selector_entry_count, + "selector members", + )?; + if amplitudes + .iter() + .any(|entry| entry.kind != EagerPlanReductionEntryKind::AmplitudeMember) + || selectors + .iter() + .any(|entry| entry.kind != EagerPlanReductionEntryKind::SelectorMember) + { + return Err(RusticolError::integrity( + "eager reduction group range has inconsistent entry kinds", + )); + } + let weight = + exact_real_factor(decoded, group.helicity_weight_factor_id, "helicity weight")?; + let all_sector_weight = exact_real_factor( + decoded, + group.all_sector_weight_factor_id, + "all-sector weight", + )?; + // Selector IDs index the sorted public color axis. Replay uses the + // original color-plan sector identity retained by each coherent group. + let sector_ids = eager_raw_sum_sector_ids( + manifest.color_accuracy.as_str(), + group.coherent_group_id, + color_sector_by_group.as_ref(), + )?; + groups.push(RawSumGroup { + id: i64::from(group.coherent_group_id), + indices: amplitudes + .iter() + .map(|entry| entry.left_id as usize) + .collect(), + weight, + all_sector_weight, + sector_ids, + }); + } + if manifest.color_accuracy == "lc" { + return Ok((groups, None)); + } + let group_index_by_id = groups + .iter() + .enumerate() + .map(|(index, group)| (group.id, index)) + .collect::>(); + let contraction_rows = reduction_range( + &decoded.reduction_entries, + decoded.color_contraction_entry_start, + decoded.color_contraction_entry_count, + "color contraction", + )?; + let mut entries = Vec::with_capacity(contraction_rows.len()); + for row in contraction_rows { + if row.kind != EagerPlanReductionEntryKind::ColorContraction { + return Err(RusticolError::integrity( + "eager color-contraction range contains another entry kind", + )); + } + let left_id = i64::from(row.left_id); + let right_id = i64::from(row.right_id); + let coefficient = exact_factor(decoded, row.factor_id, "color-contraction weight")?; + let symmetry_factor = if row.auxiliary_factor_id == MISSING_U32 { + 1.0 + } else { + exact_real_factor(decoded, row.auxiliary_factor_id, "color symmetry factor")? + }; + entries.push(ColorContractionEntry { + left_group_index: *group_index_by_id.get(&left_id).ok_or_else(|| { + RusticolError::integrity("color contraction references an unknown left group") + })?, + right_group_index: *group_index_by_id.get(&right_id).ok_or_else(|| { + RusticolError::integrity("color contraction references an unknown right group") + })?, + weight_re: coefficient.re, + weight_im: coefficient.im, + symmetry_factor, + }); + } + let contraction = ColorContractionRuntime::new(&groups, entries); + Ok((groups, Some(contraction))) +} + +fn eager_raw_sum_sector_ids( + color_accuracy: &str, + coherent_group_id: u32, + color_sector_by_group: Option<&BTreeMap>, +) -> RusticolResult> { + if color_accuracy != "lc" { + return Ok(Vec::new()); + } + let sector_id = color_sector_by_group + .and_then(|sectors| sectors.get(&coherent_group_id)) + .copied() + .ok_or_else(|| { + RusticolError::integrity(format!( + "eager LC coherent group {coherent_group_id} has no color-sector identity" + )) + })?; + Ok(vec![i64::from(sector_id)]) +} + +fn exact_factor( + decoded: &DecodedEagerRuntimeV3, + factor_id: u32, + context: &str, +) -> RusticolResult> { + let factor = decoded + .exact_factors + .get(factor_id as usize) + .ok_or_else(|| RusticolError::integrity(format!("eager {context} factor is absent")))?; + let value = Complex::new( + f64::from_bits(factor.real_bits), + f64::from_bits(factor.imaginary_bits), + ); + if !value.re.is_finite() || !value.im.is_finite() { + return Err(RusticolError::integrity(format!( + "eager {context} factor is not finite" + ))); + } + Ok(value) +} + +fn exact_real_factor( + decoded: &DecodedEagerRuntimeV3, + factor_id: u32, + context: &str, +) -> RusticolResult { + let value = exact_factor(decoded, factor_id, context)?; + if value.im != 0.0 { + return Err(RusticolError::integrity(format!( + "eager {context} factor is not real" + ))); + } + Ok(value.re) +} + +fn reduction_range<'a>( + entries: &'a [crate::EagerPlanReductionEntryRow], + start: u64, + count: u64, + context: &str, +) -> RusticolResult<&'a [crate::EagerPlanReductionEntryRow]> { + table_range(entries, start, count, context) +} + +fn table_range<'a, T>( + entries: &'a [T], + start: u64, + count: u64, + context: &str, +) -> RusticolResult<&'a [T]> { + let start = usize::try_from(start) + .map_err(|_| RusticolError::artifact(format!("eager {context} start exceeds usize")))?; + let count = usize::try_from(count) + .map_err(|_| RusticolError::artifact(format!("eager {context} count exceeds usize")))?; + let stop = start + .checked_add(count) + .ok_or_else(|| RusticolError::artifact(format!("eager {context} range overflows")))?; + entries + .get(start..stop) + .ok_or_else(|| RusticolError::integrity(format!("eager {context} range is out of bounds"))) +} + +#[cfg(test)] +mod compact_reduction_tests { + use super::*; + use crate::{ + ColorAccuracy, ColorComponent, ContractedColor, Coverage, ExternalParticle, Helicity, + LcColorFlow, ParticleRole, Reduction, ReductionKind, SelectorCapabilities, + }; + use serde_json::json; + + fn compact_physics() -> ProcessPhysicsV1 { + ProcessPhysicsV1 { + schema_version: crate::RUNTIME_PHYSICS_SCHEMA_VERSION, + kind: "pyamplicol-resolved-physics".to_string(), + process_id: "p0".to_string(), + process: "a b > c".to_string(), + color_accuracy: ColorAccuracy::Full, + coverage: Coverage { + helicities: "complete".to_string(), + color: "contracted".to_string(), + color_kind: "contracted-color".to_string(), + structural_zero_helicity_count: 0, + }, + external_particles: (0..3) + .map(|index| ExternalParticle { + index, + label: index + 1, + particle: format!("particle-{index}"), + pdg: index as i32 + 1, + role: if index < 2 { + ParticleRole::Initial + } else { + ParticleRole::Final + }, + momentum_slot: index, + momentum_components: [ + "E".to_string(), + "px".to_string(), + "py".to_string(), + "pz".to_string(), + ], + }) + .collect(), + helicities: vec![ + Helicity { + id: "helicity:0".to_string(), + index: 0, + values: vec![1, -1, 1], + computed: true, + structural_zero: false, + representative_id: "helicity:0".to_string(), + coefficient: 1.0, + }, + Helicity { + id: "helicity:1".to_string(), + index: 1, + values: vec![-1, 1, -1], + computed: true, + structural_zero: false, + representative_id: "helicity:1".to_string(), + coefficient: 1.0, + }, + ], + color_components: vec![ColorComponent::ContractedColor(ContractedColor { + id: "contracted".to_string(), + index: 0, + description: "coherent contracted color".to_string(), + })], + reduction: Reduction { + kind: ReductionKind::ContractedColor, + groups: Vec::new(), + }, + model_parameters: Vec::new(), + selectors: SelectorCapabilities { + helicity: true, + color_flow: false, + contracted_color: false, + }, + extensions: BTreeMap::from([( + NATIVE_REDUCTION_GROUPS_EXTENSION_KEY.to_string(), + json!({ + "kind": NATIVE_REDUCTION_GROUPS_KIND, + "schema_version": NATIVE_REDUCTION_GROUPS_SCHEMA_VERSION, + "storage_abi": NATIVE_REDUCTION_GROUPS_STORAGE_ABI, + "runtime_layout_abi": NATIVE_REDUCTION_GROUPS_RUNTIME_LAYOUT_ABI, + "container_path": NATIVE_REDUCTION_GROUPS_CONTAINER_PATH, + "group_member": NATIVE_REDUCTION_GROUPS_GROUP_MEMBER, + "entry_member": NATIVE_REDUCTION_GROUPS_ENTRY_MEMBER, + "group_count": 1, + }), + )]), + } + } + + fn group() -> crate::EagerPlanReductionGroupRow { + crate::EagerPlanReductionGroupRow { + coherent_group_id: 7, + amplitude_entry_start: 0, + amplitude_entry_count: 0, + selector_entry_start: 0, + selector_entry_count: 3, + helicity_weight_factor_id: 0, + all_sector_weight_factor_id: 0, + } + } + + fn helicity_selectors() -> Vec { + vec![ + crate::EagerPlanHelicitySelectorRow { + selector_id: 0, + values_sequence_id: 0, + representative_sequence_id: 0, + coefficient_factor_id: 0, + computed: 1, + structural_zero: 0, + }, + crate::EagerPlanHelicitySelectorRow { + selector_id: 1, + values_sequence_id: 1, + representative_sequence_id: 1, + coefficient_factor_id: 0, + computed: 1, + structural_zero: 0, + }, + ] + } + + fn helicity_sequences() -> BTreeMap> { + BTreeMap::from([(0, vec![1, -1, 1]), (1, vec![-1, 1, -1])]) + } + + fn selector( + owner_id: u32, + helicity_id: u32, + color_id: u32, + ) -> crate::EagerPlanReductionEntryRow { + crate::EagerPlanReductionEntryRow { + kind: EagerPlanReductionEntryKind::SelectorMember, + owner_id, + left_id: helicity_id, + right_id: color_id, + factor_id: MISSING_U32, + auxiliary_factor_id: MISSING_U32, + } + } + + #[test] + fn authenticates_compact_helicity_selectors_against_public_metadata() { + validate_native_helicity_selectors( + &helicity_selectors(), + &helicity_sequences(), + &compact_physics(), + ) + .expect("matching compact/public helicities"); + } + + #[test] + fn corrupted_compact_helicity_values_fail_public_metadata_differential() { + let mut sequences = helicity_sequences(); + sequences.insert(1, vec![1, 1, 1]); + + let error = validate_native_helicity_selectors( + &helicity_selectors(), + &sequences, + &compact_physics(), + ) + .unwrap_err(); + + assert!(error.to_string().contains("values disagree")); + } + + #[test] + fn corrupted_compact_helicity_representative_fails_public_metadata_differential() { + let mut physics = compact_physics(); + physics.helicities[1].computed = false; + physics.helicities[1].representative_id = "helicity:0".to_string(); + let mut selectors = helicity_selectors(); + selectors[1].computed = 0; + selectors[1].representative_sequence_id = 0; + validate_native_helicity_selectors(&selectors, &helicity_sequences(), &physics) + .expect("matching alias representative"); + + selectors[1].representative_sequence_id = 1; + let error = validate_native_helicity_selectors(&selectors, &helicity_sequences(), &physics) + .unwrap_err(); + + assert!(error.to_string().contains("representative disagrees")); + } + + #[test] + fn corrupted_compact_helicity_id_and_flags_fail_public_metadata_differential() { + let mut selectors = helicity_selectors(); + selectors[1].selector_id = 7; + let id_error = validate_native_helicity_selectors( + &selectors, + &helicity_sequences(), + &compact_physics(), + ) + .unwrap_err(); + assert!(id_error.to_string().contains("IDs are not dense")); + + let mut selectors = helicity_selectors(); + selectors[1].structural_zero = 1; + let flag_error = validate_native_helicity_selectors( + &selectors, + &helicity_sequences(), + &compact_physics(), + ) + .unwrap_err(); + assert!(flag_error.to_string().contains("flags disagree")); + } + + #[test] + fn hydrates_groups_from_selector_rows_in_first_seen_order() { + let mut physics = compact_physics(); + let entries = [selector(7, 0, 0), selector(7, 1, 0), selector(7, 0, 0)]; + + hydrate_native_reduction_groups( + &mut physics, + &[group()], + &entries, + &["contracted".to_string()], + ) + .unwrap(); + + let hydrated = &physics.reduction.groups[0]; + assert_eq!(hydrated.id, "reduction:7"); + assert_eq!(hydrated.representative_helicity_id, "helicity:0"); + assert_eq!(hydrated.representative_color_id, "contracted"); + assert_eq!(hydrated.physical_helicity_ids, ["helicity:0", "helicity:1"]); + assert_eq!(hydrated.physical_color_ids, ["contracted"]); + } + + #[test] + fn resolves_lc_selectors_by_numeric_word_instead_of_public_string_order() { + let mut physics = compact_physics(); + physics.color_accuracy = ColorAccuracy::Lc; + physics.coverage.color = "complete".to_string(); + physics.coverage.color_kind = "physical-lc-flows".to_string(); + physics.reduction.kind = ReductionKind::LcDiagonal; + physics.selectors.color_flow = true; + physics.external_particles = (0..10) + .map(|index| ExternalParticle { + index, + label: index + 1, + particle: format!("particle-{index}"), + pdg: index as i32 + 1, + role: if index < 2 { + ParticleRole::Initial + } else { + ParticleRole::Final + }, + momentum_slot: index, + momentum_components: [ + "E".to_string(), + "px".to_string(), + "py".to_string(), + "pz".to_string(), + ], + }) + .collect(); + physics.helicities[0].values = vec![1; 10]; + physics.helicities[1].values = vec![-1; 10]; + physics.color_components = vec![ + ColorComponent::LcFlow(LcColorFlow { + id: "flow:2,10,7,8,9,1".to_string(), + index: 0, + word: vec![2, 10, 7, 8, 9, 1], + computed: false, + representative_id: "flow:2,7,8,9,10,1".to_string(), + coefficient: 1.0, + }), + ColorComponent::LcFlow(LcColorFlow { + id: "flow:2,7,8,9,10,1".to_string(), + index: 1, + word: vec![2, 7, 8, 9, 10, 1], + computed: true, + representative_id: "flow:2,7,8,9,10,1".to_string(), + coefficient: 1.0, + }), + ]; + + let catalog = native_color_selector_catalog(&physics).unwrap(); + let computed = resolve_native_color_selector_id( + &catalog, + &[2, 7, 8, 9, 10, 1], + &[2, 7, 8, 9, 10, 1], + true, + ) + .unwrap(); + let alias = resolve_native_color_selector_id( + &catalog, + &[2, 10, 7, 8, 9, 1], + &[2, 7, 8, 9, 10, 1], + false, + ) + .unwrap(); + assert_eq!(computed, "flow:2,7,8,9,10,1"); + assert_eq!(alias, "flow:2,10,7,8,9,1"); + + let entries = [selector(7, 0, 0), selector(7, 1, 1), selector(7, 0, 0)]; + hydrate_native_reduction_groups(&mut physics, &[group()], &entries, &[computed, alias]) + .unwrap(); + let hydrated = &physics.reduction.groups[0]; + assert_eq!(hydrated.representative_color_id, "flow:2,7,8,9,10,1"); + assert_eq!( + hydrated.physical_color_ids, + ["flow:2,7,8,9,10,1", "flow:2,10,7,8,9,1"] + ); + } + + #[test] + fn rejects_descriptor_group_count_mismatch() { + let mut physics = compact_physics(); + physics + .extensions + .get_mut(NATIVE_REDUCTION_GROUPS_EXTENSION_KEY) + .unwrap()["group_count"] = json!(2); + + let error = + hydrate_native_reduction_groups(&mut physics, &[group()], &[], &["contracted".into()]) + .unwrap_err(); + + assert!(error.to_string().contains("declares 2 groups")); + } + + #[test] + fn rejects_unknown_descriptor_contract() { + let mut physics = compact_physics(); + physics + .extensions + .get_mut(NATIVE_REDUCTION_GROUPS_EXTENSION_KEY) + .unwrap()["storage_abi"] = json!("pacbin-v0"); + + let error = + hydrate_native_reduction_groups(&mut physics, &[group()], &[], &["contracted".into()]) + .unwrap_err(); + + assert_eq!(error.kind(), crate::RusticolErrorKind::Compatibility); + assert!(error.to_string().contains("regenerate")); + } + + #[test] + fn rejects_selector_owner_and_axis_mismatches() { + let mut physics = compact_physics(); + let owner_error = hydrate_native_reduction_groups( + &mut physics, + &[group()], + &[selector(6, 0, 0), selector(6, 1, 0), selector(6, 0, 0)], + &["contracted".into()], + ) + .unwrap_err(); + assert!(owner_error.to_string().contains("invalid selector member")); + + let mut physics = compact_physics(); + let axis_error = hydrate_native_reduction_groups( + &mut physics, + &[group()], + &[selector(7, 99, 0), selector(7, 0, 0), selector(7, 0, 0)], + &["contracted".into()], + ) + .unwrap_err(); + assert!( + axis_error + .to_string() + .contains("unknown helicity selector 99") + ); + } + + #[test] + fn lc_raw_sum_uses_retained_color_plan_sector_identity() { + let sectors = BTreeMap::from([(7, 1)]); + + assert_eq!( + eager_raw_sum_sector_ids("lc", 7, Some(§ors)).unwrap(), + vec![1] + ); + assert!(eager_raw_sum_sector_ids("lc", 8, Some(§ors)).is_err()); + assert!( + eager_raw_sum_sector_ids("full", 7, None) + .unwrap() + .is_empty() + ); + } + + #[test] + fn compiled_lane_rejects_native_reduction_marker() { + let physics = compact_physics(); + + let error = reject_native_reduction_groups_for_compiled(&physics).unwrap_err(); + + assert_eq!(error.kind(), crate::RusticolErrorKind::Compatibility); + assert!(error.to_string().contains("require eager plan-v3")); + } +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/engine/eager_v3_manifest.rs b/pyamplicol/rust/crates/rusticol-core/src/engine/eager_v3_manifest.rs new file mode 100644 index 0000000000..cad7957c3d --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/engine/eager_v3_manifest.rs @@ -0,0 +1,749 @@ +// SPDX-License-Identifier: 0BSD + +//! Bounded execution-manifest and container preflight for eager plan-v3. +//! +//! This module deliberately stops before decoding an `EagerExecutionPlan`. +//! It authenticates the small JSON summary and the complete PACBIN container, +//! then returns the mapped reader to the compact runtime loader. + +use crate::eager_layout::{ + EAGER_DIRECT_ARENA_RUNTIME_CAPABILITY, EAGER_LOWERING_INPUT_ABI, EAGER_PLAN_ABI, + EAGER_RUNTIME_CAPABILITY, EAGER_RUNTIME_CONTAINER_KIND, EAGER_RUNTIME_CONTAINER_SCHEMA, + EAGER_RUNTIME_LAYOUT_ABI, EagerSectionHeader, EagerSectionKind, +}; +use crate::pacbin::{PacbinMemberKind, PacbinReader}; +use crate::{ArtifactProcess, PROCESS_ARTIFACT_SCHEMA_VERSION, RusticolError, RusticolResult}; +use serde::Deserialize; +use std::collections::{BTreeMap, BTreeSet}; +use std::fs::File; +use std::path::Path; + +pub(super) const EAGER_EXECUTION_KIND: &str = "pyamplicol-runtime-eager-execution"; +pub(super) const EAGER_RUNTIME_STORAGE_ABI: &str = "pacbin-v1"; +pub(super) const EAGER_RUNTIME_CONTAINER_PATH: &str = "eager-runtime.pacbin"; +pub(super) const EAGER_KERNEL_PACK_MANIFEST_PATH: &str = "model/eager-kernel-pack.json"; +pub(super) const EAGER_KERNEL_PAYLOAD_ROOT: &str = "model/eager-kernels"; +const LEGACY_EAGER_PLAN_ABI: &str = "pyamplicol-eager-plan-v2"; +const LEGACY_EAGER_RUNTIME_CAPABILITY: &str = "rusticol.eager-dag.complex-f64.v1"; + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(super) struct EagerV3ExecutionManifest { + pub(super) schema_version: u32, + pub(super) kind: String, + pub(super) required_runtime_capabilities: Vec, + pub(super) process: String, + pub(super) key: String, + pub(super) color_accuracy: String, + pub(super) external_pdg_order: Vec, + pub(super) eager_plan_abi: String, + pub(super) kernel_pack: EagerV3KernelPackReference, + pub(super) runtime_options: EagerV3RuntimeOptions, + pub(super) plan: EagerV3PlanSummary, + pub(super) dag_summary: EagerV3DagSummary, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(super) struct EagerV3KernelPackReference { + pub(super) manifest_path: String, + pub(super) payload_root: String, +} + +#[derive(Clone, Copy, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(super) struct EagerV3RuntimeOptions { + pub(super) point_tile_size: u64, + pub(super) workspace_mib: u64, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(super) struct EagerV3PlanSummary { + pub(super) kind: String, + pub(super) eager_plan_abi: String, + pub(super) lowering_input_abi: String, + pub(super) lowering_input_sha256: String, + pub(super) runtime_layout_abi: String, + pub(super) required_runtime_capabilities: Vec, + pub(super) runtime_container: EagerV3RuntimeContainer, + pub(super) inspection_summary: EagerV3InspectionSummary, + pub(super) materialization_census: EagerV3MaterializationCensus, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(super) struct EagerV3RuntimeContainer { + pub(super) kind: String, + pub(super) schema_version: u16, + pub(super) storage_abi: String, + pub(super) path: String, + pub(super) size_bytes: u64, + pub(super) sha256: String, + pub(super) member_count: u64, + pub(super) unpacked_size_bytes: u64, + pub(super) index_sha256: String, +} + +#[derive(Clone, Copy, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(super) struct EagerV3DagSummary { + pub(super) current_count: u64, + pub(super) source_count: u64, + pub(super) interaction_count: u64, + pub(super) interaction_evaluation_count: u64, + pub(super) amplitude_root_count: u64, + pub(super) truncated: bool, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(super) struct EagerV3MaterializationCensus { + pub(super) abi: String, + pub(super) basis: String, + pub(super) r#final: BTreeMap, + pub(super) peak: BTreeMap, + pub(super) final_equals_peak: bool, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(super) struct EagerV3InspectionSummary { + pub(super) execution_mode: String, + pub(super) eager_plan_abi: String, + pub(super) runtime_layout_abi: String, + pub(super) process_id: String, + pub(super) model_name: String, + pub(super) current_count: u64, + pub(super) value_count: u64, + pub(super) momentum_count: u64, + pub(super) source_count: u64, + pub(super) parameter_count: u64, + pub(super) stage_count: u64, + pub(super) coupling_count: u64, + pub(super) invocation_count: u64, + pub(super) attachment_count: u64, + pub(super) finalization_count: u64, + pub(super) closure_count: u64, + pub(super) selector_domain_count: u64, + pub(super) reduction_group_count: u64, + pub(super) reduction_entry_count: u64, + pub(super) retained_table_count: u64, + pub(super) current_component_count: u64, + pub(super) value_component_count: u64, + pub(super) momentum_component_count: u64, + #[serde(default)] + pub(super) selector_work: Option, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(super) struct EagerV3SelectorWork { + pub(super) abi: String, + pub(super) selected_flow_selector_count: u64, + pub(super) selected_flow_current_count: u64, + pub(super) selected_flow_evaluation_count: u64, + pub(super) selected_flow_attachment_count: u64, + pub(super) all_flow_selector_count: u64, + pub(super) all_flow_current_count: u64, + pub(super) all_flow_evaluation_count: u64, + pub(super) all_flow_attachment_count: u64, + pub(super) contracted_current_count: u64, + pub(super) contracted_evaluation_count: u64, + pub(super) contracted_attachment_count: u64, +} + +impl EagerV3ExecutionManifest { + fn validate(&self, outer: &ArtifactProcess) -> RusticolResult<()> { + if self.schema_version != PROCESS_ARTIFACT_SCHEMA_VERSION + || self.kind != EAGER_EXECUTION_KIND + { + return Err(RusticolError::compatibility(format!( + "unsupported eager execution kind {:?} schema {}; regenerate the artifact", + self.kind, self.schema_version + ))); + } + if self.process != outer.expression + || self.key != outer.id + || self.color_accuracy != outer.color_accuracy + || self.external_pdg_order != outer.external_pdgs + { + return Err(RusticolError::integrity(format!( + "eager plan-v3 execution manifest does not match outer process {:?}", + outer.id + ))); + } + validate_direct_capabilities( + &self.required_runtime_capabilities, + "eager execution manifest", + )?; + validate_direct_capabilities(&outer.required_runtime_capabilities, "outer eager process")?; + if self.eager_plan_abi != EAGER_PLAN_ABI { + return Err(unsupported_abi("eager plan", &self.eager_plan_abi)); + } + if self.kernel_pack.manifest_path != EAGER_KERNEL_PACK_MANIFEST_PATH + || self.kernel_pack.payload_root != EAGER_KERNEL_PAYLOAD_ROOT + { + return Err(RusticolError::security( + "eager plan-v3 kernel-pack paths are not canonical", + )); + } + self.runtime_options.validate()?; + self.plan.validate(&self.key)?; + self.dag_summary.validate()?; + self.plan + .materialization_census + .validate(&self.plan.inspection_summary, &self.dag_summary)?; + if self.plan.inspection_summary.current_count != self.dag_summary.current_count + || self.plan.inspection_summary.source_count != self.dag_summary.source_count + || self.plan.inspection_summary.invocation_count + != self.dag_summary.interaction_evaluation_count + { + return Err(RusticolError::integrity( + "eager plan-v3 inspection counts disagree with the DAG summary", + )); + } + Ok(()) + } +} + +impl EagerV3RuntimeOptions { + fn validate(self) -> RusticolResult<()> { + if self.point_tile_size == 0 { + return Err(RusticolError::artifact( + "eager point_tile_size must be positive", + )); + } + if self.workspace_mib == 0 { + return Err(RusticolError::artifact( + "eager workspace_mib must be positive", + )); + } + Ok(()) + } +} + +impl EagerV3PlanSummary { + fn validate(&self, process_key: &str) -> RusticolResult<()> { + if self.kind != EAGER_EXECUTION_KIND { + return Err(RusticolError::compatibility(format!( + "unsupported eager plan kind {:?}", + self.kind + ))); + } + if self.eager_plan_abi != EAGER_PLAN_ABI { + return Err(unsupported_abi("nested eager plan", &self.eager_plan_abi)); + } + if self.lowering_input_abi != EAGER_LOWERING_INPUT_ABI { + return Err(unsupported_abi( + "eager lowering input", + &self.lowering_input_abi, + )); + } + if self.runtime_layout_abi != EAGER_RUNTIME_LAYOUT_ABI { + return Err(unsupported_abi( + "eager runtime layout", + &self.runtime_layout_abi, + )); + } + parse_sha256(&self.lowering_input_sha256, "eager lowering input")?; + validate_direct_capabilities(&self.required_runtime_capabilities, "nested eager plan")?; + self.runtime_container.validate()?; + self.inspection_summary.validate(process_key)?; + Ok(()) + } +} + +impl EagerV3RuntimeContainer { + fn validate(&self) -> RusticolResult<()> { + if self.kind != EAGER_RUNTIME_CONTAINER_KIND { + return Err(RusticolError::compatibility(format!( + "unsupported eager runtime container kind {:?}", + self.kind + ))); + } + if self.schema_version != EAGER_RUNTIME_CONTAINER_SCHEMA { + return Err(RusticolError::compatibility(format!( + "unsupported eager runtime container schema {}", + self.schema_version + ))); + } + if self.storage_abi != EAGER_RUNTIME_STORAGE_ABI { + return Err(unsupported_abi("eager runtime storage", &self.storage_abi)); + } + if self.path != EAGER_RUNTIME_CONTAINER_PATH { + return Err(RusticolError::security( + "eager runtime container path must be exactly eager-runtime.pacbin", + )); + } + if self.member_count != EXPECTED_EAGER_MEMBERS.len() as u64 { + return Err(RusticolError::integrity(format!( + "eager runtime container must declare exactly {} members", + EXPECTED_EAGER_MEMBERS.len() + ))); + } + if self.size_bytes == 0 { + return Err(RusticolError::artifact( + "eager runtime container must not be empty", + )); + } + if self.unpacked_size_bytes == 0 || self.unpacked_size_bytes > self.size_bytes { + return Err(RusticolError::artifact( + "eager runtime unpacked size must be nonzero and not exceed the container size", + )); + } + parse_sha256(&self.sha256, "eager runtime container")?; + parse_sha256(&self.index_sha256, "eager runtime index")?; + Ok(()) + } +} + +impl EagerV3DagSummary { + fn validate(self) -> RusticolResult<()> { + validate_counts( + "eager DAG summary", + &[ + self.current_count, + self.source_count, + self.interaction_count, + self.interaction_evaluation_count, + self.amplitude_root_count, + ], + )?; + if self.interaction_evaluation_count > self.interaction_count { + return Err(RusticolError::integrity( + "eager DAG interaction-evaluation count exceeds its interaction count", + )); + } + if self.truncated { + return Err(RusticolError::compatibility( + "truncated DAGs cannot be loaded as eager plan-v3 artifacts", + )); + } + Ok(()) + } +} + +impl EagerV3MaterializationCensus { + fn validate( + &self, + inspection: &EagerV3InspectionSummary, + dag: &EagerV3DagSummary, + ) -> RusticolResult<()> { + if self.abi != "pyamplicol-fully-resident-materialization-census-v1" + || self.basis != "immutable-fully-resident-eager-plan" + { + return Err(RusticolError::compatibility( + "eager materialization census ABI or basis is unsupported", + )); + } + let expected = BTreeMap::from([ + ("amplitude_root_count".to_string(), dag.amplitude_root_count), + ("attachment_count".to_string(), inspection.attachment_count), + ("closure_count".to_string(), inspection.closure_count), + ("current_count".to_string(), dag.current_count), + ( + "finalization_count".to_string(), + inspection.finalization_count, + ), + ("interaction_count".to_string(), dag.interaction_count), + ("invocation_count".to_string(), inspection.invocation_count), + ("source_count".to_string(), dag.source_count), + ]); + validate_counts( + "eager materialization census", + &self.r#final.values().copied().collect::>(), + )?; + validate_counts( + "eager materialization census peak", + &self.peak.values().copied().collect::>(), + )?; + if !self.final_equals_peak || self.r#final != expected || self.peak != expected { + return Err(RusticolError::integrity( + "eager materialization census disagrees with the persisted plan", + )); + } + Ok(()) + } +} + +impl EagerV3InspectionSummary { + fn validate(&self, process_key: &str) -> RusticolResult<()> { + if self.execution_mode != "eager" + || self.eager_plan_abi != EAGER_PLAN_ABI + || self.runtime_layout_abi != EAGER_RUNTIME_LAYOUT_ABI + { + return Err(RusticolError::compatibility( + "eager inspection summary ABI does not match plan-v3", + )); + } + if self.process_id != process_key { + return Err(RusticolError::integrity( + "eager inspection process id does not match its execution manifest", + )); + } + if self.model_name.is_empty() { + return Err(RusticolError::artifact( + "eager inspection model name must not be empty", + )); + } + validate_counts( + "eager inspection summary", + &[ + self.current_count, + self.value_count, + self.momentum_count, + self.source_count, + self.parameter_count, + self.stage_count, + self.coupling_count, + self.invocation_count, + self.attachment_count, + self.finalization_count, + self.closure_count, + self.selector_domain_count, + self.reduction_group_count, + self.reduction_entry_count, + self.retained_table_count, + self.current_component_count, + self.value_component_count, + self.momentum_component_count, + ], + )?; + if let Some(selector_work) = &self.selector_work { + selector_work.validate(self)?; + } + Ok(()) + } +} + +impl EagerV3SelectorWork { + fn validate(&self, summary: &EagerV3InspectionSummary) -> RusticolResult<()> { + if self.abi != "pyamplicol-eager-selector-work-v1" { + return Err(RusticolError::compatibility( + "eager selector-work summary ABI is unsupported", + )); + } + validate_counts( + "eager selector-work summary", + &[ + self.selected_flow_selector_count, + self.selected_flow_current_count, + self.selected_flow_evaluation_count, + self.selected_flow_attachment_count, + self.all_flow_selector_count, + self.all_flow_current_count, + self.all_flow_evaluation_count, + self.all_flow_attachment_count, + self.contracted_current_count, + self.contracted_evaluation_count, + self.contracted_attachment_count, + ], + )?; + if self.selected_flow_current_count > summary.current_count + || self.all_flow_current_count > summary.current_count + || self.contracted_current_count != summary.current_count + || self.selected_flow_evaluation_count > summary.invocation_count + || self.all_flow_evaluation_count > summary.invocation_count + || self.contracted_evaluation_count != summary.invocation_count + || self.selected_flow_attachment_count > summary.attachment_count + || self.all_flow_attachment_count > summary.attachment_count + || self.contracted_attachment_count != summary.attachment_count + { + return Err(RusticolError::integrity( + "eager selector-work counts exceed or disagree with the persisted plan", + )); + } + Ok(()) + } +} + +/// Parse and validate one eager plan-v3 `execution.json`. +/// +/// Legacy plan-v2 markers are detected before full +/// JSON deserialization, avoiding traversal of an expanded `runtime_schema`. +pub(super) fn parse_eager_v3_execution_manifest( + bytes: &[u8], + outer: &ArtifactProcess, +) -> RusticolResult { + reject_legacy_eager_manifest(bytes)?; + let manifest: EagerV3ExecutionManifest = serde_json::from_slice(bytes).map_err(|error| { + RusticolError::serialization(format!( + "could not parse eager plan-v3 execution manifest: {error}" + )) + })?; + manifest.validate(outer)?; + Ok(manifest) +} + +/// Open, authenticate, and structurally preflight `eager-runtime.pacbin`. +pub(super) fn open_eager_v3_runtime_container( + container_file: File, + display_path: &Path, + manifest: &EagerV3ExecutionManifest, +) -> RusticolResult { + manifest.plan.runtime_container.validate()?; + let container = &manifest.plan.runtime_container; + // A complete container digest authenticates every indexed member. Hashing + // and parsing use the exact checked file description supplied by the + // verified artifact, avoiding a path-replacement race. + // PACBIN still validates its index, canonical layout, and boundaries. + let expected_file_sha = parse_sha256(&container.sha256, "eager runtime container")?; + let reader = + PacbinReader::open_file_with_sha256(container_file, display_path, &expected_file_sha)?; + let index = reader.index(); + let mapped_size = u64::try_from(reader.container_size()) + .map_err(|_| RusticolError::integrity("eager runtime PACBIN size exceeds u64"))?; + if index.file_size() != container.size_bytes || mapped_size != container.size_bytes { + return Err(RusticolError::integrity( + "eager runtime PACBIN size does not match execution manifest", + )); + } + let expected_index_sha = parse_sha256(&container.index_sha256, "eager runtime index")?; + if index.index_sha256() != &expected_index_sha { + return Err(RusticolError::integrity( + "eager runtime PACBIN index digest mismatch", + )); + } + if index.members().len() as u64 != container.member_count { + return Err(RusticolError::integrity( + "eager runtime PACBIN member count does not match execution manifest", + )); + } + + let mut seen = BTreeSet::new(); + let mut unpacked_size = 0_u64; + for member in reader.members() { + let expected = expected_member(member.logical_path()).ok_or_else(|| { + RusticolError::security(format!( + "unexpected eager runtime PACBIN member path {:?}", + member.logical_path() + )) + })?; + if member.kind() != expected.member_kind { + return Err(RusticolError::integrity(format!( + "eager runtime PACBIN member {:?} has kind {:?}, expected {:?}", + member.logical_path(), + member.kind(), + expected.member_kind + ))); + } + let (header, _) = EagerSectionHeader::decode(reader.member_bytes(member.logical_path())?)?; + if header.kind() != expected.section_kind { + return Err(RusticolError::integrity(format!( + "eager runtime PACBIN member {:?} has section kind {:?}, expected {:?}", + member.logical_path(), + header.kind(), + expected.section_kind + ))); + } + unpacked_size = unpacked_size.checked_add(member.length()).ok_or_else(|| { + RusticolError::integrity("eager runtime PACBIN unpacked size exceeds u64") + })?; + seen.insert(member.logical_path()); + } + if seen.len() != EXPECTED_EAGER_MEMBERS.len() + || EXPECTED_EAGER_MEMBERS + .iter() + .any(|expected| !seen.contains(expected.path)) + { + return Err(RusticolError::integrity( + "eager runtime PACBIN is missing a required member", + )); + } + if unpacked_size != container.unpacked_size_bytes { + return Err(RusticolError::integrity( + "eager runtime PACBIN unpacked size does not match execution manifest", + )); + } + Ok(reader) +} + +fn reject_legacy_eager_manifest(bytes: &[u8]) -> RusticolResult<()> { + if contains_bytes(bytes, LEGACY_EAGER_PLAN_ABI.as_bytes()) + || contains_bytes(bytes, LEGACY_EAGER_RUNTIME_CAPABILITY.as_bytes()) + { + return Err(RusticolError::compatibility( + "legacy eager plan-v2 artifacts are unsupported by the compact eager runtime; regenerate the artifact with `pyamplicol generate`", + )); + } + Ok(()) +} + +fn contains_bytes(haystack: &[u8], needle: &[u8]) -> bool { + !needle.is_empty() + && haystack + .windows(needle.len()) + .any(|window| window == needle) +} + +fn validate_direct_capabilities(capabilities: &[String], context: &str) -> RusticolResult<()> { + if capabilities == [EAGER_RUNTIME_CAPABILITY] { + return Err(RusticolError::compatibility(format!( + "{context} predates {EAGER_DIRECT_ARENA_RUNTIME_CAPABILITY:?}; regenerate the eager \ + artifact with the current `pyamplicol generate` before loading it" + ))); + } + if capabilities + != [ + EAGER_DIRECT_ARENA_RUNTIME_CAPABILITY, + EAGER_RUNTIME_CAPABILITY, + ] + { + return Err(RusticolError::compatibility(format!( + "{context} must require exactly {:?}", + [ + EAGER_DIRECT_ARENA_RUNTIME_CAPABILITY, + EAGER_RUNTIME_CAPABILITY, + ] + ))); + } + Ok(()) +} + +fn validate_counts(context: &str, counts: &[u64]) -> RusticolResult<()> { + if counts.iter().any(|count| usize::try_from(*count).is_err()) { + return Err(RusticolError::artifact(format!( + "{context} count exceeds the platform index domain" + ))); + } + Ok(()) +} + +fn unsupported_abi(context: &str, value: &str) -> RusticolError { + RusticolError::compatibility(format!("unsupported {context} ABI {value:?}")) +} + +fn parse_sha256(value: &str, context: &str) -> RusticolResult<[u8; 32]> { + if value.len() != 64 + || value + .as_bytes() + .iter() + .any(|byte| !matches!(byte, b'0'..=b'9' | b'a'..=b'f')) + { + return Err(RusticolError::artifact(format!( + "{context} SHA-256 must be 64 lowercase hexadecimal characters" + ))); + } + let mut digest = [0_u8; 32]; + for (index, pair) in value.as_bytes().chunks_exact(2).enumerate() { + digest[index] = (hex_nibble(pair[0]) << 4) | hex_nibble(pair[1]); + } + Ok(digest) +} + +fn hex_nibble(value: u8) -> u8 { + match value { + b'0'..=b'9' => value - b'0', + b'a'..=b'f' => value - b'a' + 10, + _ => unreachable!("digest validation precedes hexadecimal decoding"), + } +} + +#[derive(Clone, Copy)] +pub(super) struct ExpectedEagerMember { + pub(super) path: &'static str, + pub(super) member_kind: PacbinMemberKind, + pub(super) section_kind: EagerSectionKind, +} + +const fn metadata(path: &'static str, section_kind: EagerSectionKind) -> ExpectedEagerMember { + ExpectedEagerMember { + path, + member_kind: PacbinMemberKind::EagerRuntimeMetadata, + section_kind, + } +} + +const fn table(path: &'static str, section_kind: EagerSectionKind) -> ExpectedEagerMember { + ExpectedEagerMember { + path, + member_kind: PacbinMemberKind::EagerRuntimeTable, + section_kind, + } +} + +pub(super) const EXPECTED_EAGER_MEMBERS: &[ExpectedEagerMember] = &[ + table( + "catalogs/bitsets/populations.bin", + EagerSectionKind::Metadata, + ), + table("catalogs/bitsets/ranges.bin", EagerSectionKind::Metadata), + table("catalogs/bitsets/words.bin", EagerSectionKind::Metadata), + table("catalogs/exact-factors.bin", EagerSectionKind::ExactFactors), + metadata("catalogs/exact-ir/bytes.bin", EagerSectionKind::Metadata), + metadata("catalogs/exact-ir/ranges.bin", EagerSectionKind::Metadata), + table( + "catalogs/i32-sequences/ranges.bin", + EagerSectionKind::Metadata, + ), + table( + "catalogs/i32-sequences/values.bin", + EagerSectionKind::Metadata, + ), + metadata( + "catalogs/semantic-limitations/bytes.bin", + EagerSectionKind::Metadata, + ), + metadata( + "catalogs/semantic-limitations/ranges.bin", + EagerSectionKind::Metadata, + ), + metadata("catalogs/strings/bytes.bin", EagerSectionKind::Metadata), + metadata("catalogs/strings/ranges.bin", EagerSectionKind::Metadata), + table( + "catalogs/u32-sequences/ranges.bin", + EagerSectionKind::Metadata, + ), + table( + "catalogs/u32-sequences/values.bin", + EagerSectionKind::Metadata, + ), + metadata( + "inspection/summary.bin", + EagerSectionKind::InspectionSummary, + ), + metadata("metadata/core.bin", EagerSectionKind::Metadata), + metadata("metadata/identity-bytes.bin", EagerSectionKind::Metadata), + metadata("metadata/identity-ranges.bin", EagerSectionKind::Metadata), + table("reductions/entries.bin", EagerSectionKind::ReductionEntries), + table("reductions/groups.bin", EagerSectionKind::ReductionGroups), + table("retained/columns.bin", EagerSectionKind::Metadata), + metadata("retained/name-bytes.bin", EagerSectionKind::Metadata), + metadata("retained/name-ranges.bin", EagerSectionKind::Metadata), + table("retained/tables.bin", EagerSectionKind::Metadata), + table("retained/values-f64-bits.bin", EagerSectionKind::Metadata), + table("retained/values-i32.bin", EagerSectionKind::Metadata), + table("retained/values-u32.bin", EagerSectionKind::Metadata), + table("retained/values-u64.bin", EagerSectionKind::Metadata), + table("retained/values-u8.bin", EagerSectionKind::Metadata), + table("selectors/colors.bin", EagerSectionKind::SelectorDomains), + table("selectors/domains.bin", EagerSectionKind::SelectorDomains), + table( + "selectors/helicities.bin", + EagerSectionKind::SelectorDomains, + ), + table( + "selectors/memberships.bin", + EagerSectionKind::SelectorMemberships, + ), + table("tables/attachments.bin", EagerSectionKind::Attachments), + table("tables/closures.bin", EagerSectionKind::Closures), + table("tables/couplings.bin", EagerSectionKind::Couplings), + table("tables/currents.bin", EagerSectionKind::CurrentLayout), + table("tables/direct-coefficients.bin", EagerSectionKind::Closures), + table("tables/finalizations.bin", EagerSectionKind::Finalizations), + table("tables/invocations.bin", EagerSectionKind::Invocations), + table("tables/momenta.bin", EagerSectionKind::MomentumLayout), + table("tables/parameters.bin", EagerSectionKind::ParameterLayout), + table("tables/sources.bin", EagerSectionKind::SourceFill), + table("tables/stages.bin", EagerSectionKind::Stages), + table("tables/values.bin", EagerSectionKind::ValueLayout), +]; + +fn expected_member(path: &str) -> Option<&'static ExpectedEagerMember> { + EXPECTED_EAGER_MEMBERS + .binary_search_by_key(&path, |member| member.path) + .ok() + .map(|index| &EXPECTED_EAGER_MEMBERS[index]) +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/engine/eager_v3_manifest_tests.rs b/pyamplicol/rust/crates/rusticol-core/src/engine/eager_v3_manifest_tests.rs new file mode 100644 index 0000000000..01268d852f --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/engine/eager_v3_manifest_tests.rs @@ -0,0 +1,704 @@ +// SPDX-License-Identifier: 0BSD + +use super::eager_v3_manifest::*; +use crate::eager_layout::{ + EAGER_DIRECT_ARENA_RUNTIME_CAPABILITY, EAGER_LOWERING_INPUT_ABI, EAGER_PLAN_ABI, + EAGER_RUNTIME_CAPABILITY, EAGER_RUNTIME_CONTAINER_KIND, EAGER_RUNTIME_CONTAINER_SCHEMA, + EAGER_RUNTIME_LAYOUT_ABI, EAGER_SECTION_HEADER_SIZE, EagerSectionHeader, EagerSectionKind, +}; +use crate::pacbin::{PacbinReader, PacbinWriteMember, PacbinWriteOptions, write_pacbin_atomic}; +use crate::{ArtifactProcess, PROCESS_ARTIFACT_SCHEMA_VERSION, RusticolErrorKind}; +use serde_json::{Value, json}; +use sha2::{Digest, Sha256}; +use std::fs; +use std::path::{Path, PathBuf}; +use std::sync::atomic::{AtomicU64, Ordering}; + +static NEXT_DIRECTORY: AtomicU64 = AtomicU64::new(0); + +struct Fixture { + root: PathBuf, + outer: ArtifactProcess, + manifest: Value, +} + +impl Fixture { + fn new() -> Self { + let root = temporary_directory(); + fs::create_dir_all(&root).unwrap(); + let metadata = write_container(&root, None, None); + let outer = outer_process(); + let manifest = manifest_value(&outer, &metadata); + Self { + root, + outer, + manifest, + } + } + + fn parse(&self) -> crate::RusticolResult { + parse_eager_v3_execution_manifest(&serde_json::to_vec(&self.manifest).unwrap(), &self.outer) + } +} + +impl Drop for Fixture { + fn drop(&mut self) { + let _ = fs::remove_dir_all(&self.root); + } +} + +fn open_fixture_runtime_container( + root: &Path, + manifest: &EagerV3ExecutionManifest, +) -> crate::RusticolResult { + let path = root.join(EAGER_RUNTIME_CONTAINER_PATH); + #[cfg(unix)] + let file = { + use std::os::unix::fs::OpenOptionsExt as _; + fs::OpenOptions::new() + .read(true) + .custom_flags(libc::O_CLOEXEC | libc::O_NOFOLLOW) + .open(&path) + }; + #[cfg(not(unix))] + let file = fs::File::open(&path); + let file = file.map_err(|error| { + crate::RusticolError::security(format!( + "could not open eager runtime container {}: {error}", + path.display() + )) + })?; + open_eager_v3_runtime_container(file, &path, manifest) +} + +#[derive(Clone)] +struct ContainerMetadata { + size_bytes: u64, + sha256: String, + member_count: u64, + unpacked_size_bytes: u64, + index_sha256: String, +} + +#[test] +fn valid_manifest_and_container_preflight() { + let fixture = Fixture::new(); + let manifest = fixture.parse().unwrap(); + let reader = open_fixture_runtime_container(&fixture.root, &manifest).unwrap(); + assert_eq!(reader.members().len(), EXPECTED_EAGER_MEMBERS.len()); +} + +#[cfg(unix)] +#[test] +fn opened_runtime_container_does_not_reopen_a_replaced_path() { + let fixture = Fixture::new(); + let manifest = fixture.parse().unwrap(); + let path = fixture.root.join(EAGER_RUNTIME_CONTAINER_PATH); + let displaced = fixture.root.join("authenticated-runtime.pacbin"); + let file = fs::File::open(&path).unwrap(); + fs::rename(&path, &displaced).unwrap(); + fs::write(&path, b"replacement bytes").unwrap(); + + let reader = open_eager_v3_runtime_container(file, &path, &manifest).unwrap(); + assert_eq!(reader.members().len(), EXPECTED_EAGER_MEMBERS.len()); +} + +#[test] +fn outer_process_identity_must_match() { + for (field, value) in [ + ("schema_version", json!(4)), + ("kind", json!("wrong-kind")), + ("process", json!("g g > g g")), + ("key", json!("wrong-key")), + ("color_accuracy", json!("lc")), + ("external_pdg_order", json!([21, 21, 21, 21])), + ] { + let mut fixture = Fixture::new(); + fixture.manifest[field] = value; + let error = fixture.parse().unwrap_err(); + assert!( + matches!( + error.kind(), + RusticolErrorKind::Compatibility | RusticolErrorKind::Integrity + ), + "field {field}: {error}" + ); + } +} + +#[test] +fn every_plan_abi_and_capability_mismatch_fails_closed() { + let mutations: &[(&[&str], Value)] = &[ + (&["eager_plan_abi"], json!("pyamplicol-eager-plan-v999")), + ( + &["plan", "eager_plan_abi"], + json!("pyamplicol-eager-plan-v999"), + ), + ( + &["plan", "lowering_input_abi"], + json!("pyamplicol-eager-lowering-input-v999"), + ), + ( + &["plan", "runtime_layout_abi"], + json!("pyamplicol-eager-runtime-layout-v999"), + ), + ( + &["required_runtime_capabilities"], + json!(["rusticol.eager-dag.complex-f64.v1"]), + ), + ( + &["plan", "required_runtime_capabilities"], + json!(["rusticol.eager-runtime-layout.complex-f64.v2"]), + ), + ]; + for (path, replacement) in mutations { + let mut fixture = Fixture::new(); + set_value(&mut fixture.manifest, path, replacement.clone()); + let error = fixture.parse().unwrap_err(); + assert_eq!(error.kind(), RusticolErrorKind::Compatibility, "{path:?}"); + } + + let mut fixture = Fixture::new(); + fixture.outer.required_runtime_capabilities = vec!["wrong-capability".to_string()]; + assert_eq!( + fixture.parse().unwrap_err().kind(), + RusticolErrorKind::Compatibility + ); +} + +#[test] +fn pre_arena_plan_v3_has_actionable_regeneration_error() { + let mut fixture = Fixture::new(); + let legacy = json!([EAGER_RUNTIME_CAPABILITY]); + fixture.manifest["required_runtime_capabilities"] = legacy.clone(); + fixture.manifest["plan"]["required_runtime_capabilities"] = legacy; + fixture.outer.required_runtime_capabilities = vec![EAGER_RUNTIME_CAPABILITY.to_string()]; + let error = fixture.parse().unwrap_err(); + assert_eq!(error.kind(), RusticolErrorKind::Compatibility); + assert!(error.to_string().contains("predates")); + assert!(error.to_string().contains("pyamplicol generate")); +} + +#[test] +fn container_contract_mismatches_fail_closed() { + let mutations: &[(&[&str], Value)] = &[ + ( + &["plan", "runtime_container", "kind"], + json!("wrong-container"), + ), + (&["plan", "runtime_container", "schema_version"], json!(2)), + ( + &["plan", "runtime_container", "storage_abi"], + json!("pacbin-v2"), + ), + ( + &["plan", "runtime_container", "path"], + json!("other.pacbin"), + ), + (&["plan", "runtime_container", "size_bytes"], json!(0)), + ( + &["plan", "runtime_container", "sha256"], + json!("A".repeat(64)), + ), + (&["plan", "runtime_container", "member_count"], json!(44)), + ( + &["plan", "runtime_container", "unpacked_size_bytes"], + json!(0), + ), + ( + &["plan", "runtime_container", "unpacked_size_bytes"], + json!(u64::MAX), + ), + ( + &["plan", "runtime_container", "index_sha256"], + json!("A".repeat(64)), + ), + ]; + for (path, replacement) in mutations { + let mut fixture = Fixture::new(); + set_value(&mut fixture.manifest, path, replacement.clone()); + assert!( + fixture.parse().is_err(), + "mutation unexpectedly passed: {path:?}" + ); + } +} + +#[test] +fn large_writer_representable_container_metadata_is_accepted() { + let mut fixture = Fixture::new(); + let size_bytes = 1_u64 << 40; + fixture.manifest["plan"]["runtime_container"]["size_bytes"] = json!(size_bytes); + fixture.manifest["plan"]["runtime_container"]["unpacked_size_bytes"] = json!(size_bytes - 4096); + + fixture.parse().unwrap(); +} + +#[test] +fn long_writer_valid_model_name_is_accepted() { + let mut fixture = Fixture::new(); + fixture.manifest["plan"]["inspection_summary"]["model_name"] = + json!("m".repeat(1024 * 1024 + 1)); + + fixture.parse().unwrap(); + + fixture.manifest["plan"]["inspection_summary"]["model_name"] = json!(""); + assert!(fixture.parse().is_err()); +} + +#[test] +fn runtime_options_require_positive_values_and_summary_relationships_hold() { + let mutations: &[(&[&str], Value)] = &[ + (&["runtime_options", "point_tile_size"], json!(0)), + (&["runtime_options", "workspace_mib"], json!(0)), + (&["dag_summary", "interaction_count"], json!(1_u64 << 49)), + ( + &["dag_summary", "interaction_evaluation_count"], + json!(1_u64 << 49), + ), + ( + &["plan", "inspection_summary", "invocation_count"], + json!(1_u64 << 49), + ), + ( + &[ + "plan", + "inspection_summary", + "selector_work", + "selected_flow_current_count", + ], + json!(12), + ), + ( + &[ + "plan", + "inspection_summary", + "selector_work", + "contracted_attachment_count", + ], + json!(18), + ), + ]; + for (path, replacement) in mutations { + let mut fixture = Fixture::new(); + set_value(&mut fixture.manifest, path, replacement.clone()); + assert!( + fixture.parse().is_err(), + "mutation unexpectedly passed: {path:?}" + ); + } +} + +#[test] +fn runtime_options_accept_writer_valid_values_above_old_reader_ceilings() { + let mut fixture = Fixture::new(); + fixture.manifest["runtime_options"]["point_tile_size"] = json!(1_048_577); + fixture.manifest["runtime_options"]["workspace_mib"] = json!(4097); + + fixture.parse().unwrap(); +} + +#[test] +fn interaction_evaluation_count_matches_the_persisted_plan() { + let mut fixture = Fixture::new(); + fixture.manifest["dag_summary"]["interaction_evaluation_count"] = json!(24); + let error = fixture.parse().unwrap_err(); + assert_eq!(error.kind(), RusticolErrorKind::Integrity); + assert!(error.to_string().contains("exceeds")); + + let mut fixture = Fixture::new(); + fixture.manifest["dag_summary"]["interaction_evaluation_count"] = json!(16); + let error = fixture.parse().unwrap_err(); + assert_eq!(error.kind(), RusticolErrorKind::Integrity); + assert!(error.to_string().contains("disagree")); +} + +#[test] +fn materialization_census_matches_the_persisted_plan() { + for (path, replacement) in [ + ( + &["plan", "materialization_census", "basis"][..], + json!("dynamic"), + ), + ( + &[ + "plan", + "materialization_census", + "final", + "invocation_count", + ][..], + json!(16), + ), + ( + &["plan", "materialization_census", "final_equals_peak"][..], + json!(false), + ), + ] { + let mut fixture = Fixture::new(); + set_value(&mut fixture.manifest, path, replacement); + assert!(fixture.parse().is_err(), "{path:?}"); + } +} + +#[test] +fn traversal_and_unknown_fields_are_rejected_during_manifest_parse() { + let mut fixture = Fixture::new(); + fixture.manifest["plan"]["runtime_container"]["path"] = json!("../eager-runtime.pacbin"); + assert_eq!( + fixture.parse().unwrap_err().kind(), + RusticolErrorKind::Security + ); + + let mut fixture = Fixture::new(); + fixture.manifest["plan"]["runtime_container"]["extra"] = json!(1); + assert_eq!( + fixture.parse().unwrap_err().kind(), + RusticolErrorKind::Serialization + ); +} + +#[cfg(unix)] +#[test] +fn symlinked_runtime_container_is_rejected() { + use std::os::unix::fs::symlink; + + let fixture = Fixture::new(); + let manifest = fixture.parse().unwrap(); + let path = fixture.root.join(EAGER_RUNTIME_CONTAINER_PATH); + let target = fixture.root.join("target.pacbin"); + fs::rename(&path, &target).unwrap(); + symlink(&target, &path).unwrap(); + let error = open_fixture_runtime_container(&fixture.root, &manifest).unwrap_err(); + assert_eq!(error.kind(), RusticolErrorKind::Security); +} + +#[test] +fn payload_digest_mismatch_is_rejected_before_container_use() { + let mut fixture = Fixture::new(); + fixture.manifest["plan"]["runtime_container"]["sha256"] = json!("0".repeat(64)); + let manifest = fixture.parse().unwrap(); + let error = open_fixture_runtime_container(&fixture.root, &manifest).unwrap_err(); + assert_eq!(error.kind(), RusticolErrorKind::Integrity); + assert!(error.to_string().contains("payload digest mismatch")); +} + +#[test] +fn index_and_unpacked_metadata_must_match_container() { + for (field, value) in [ + ("index_sha256", json!("0".repeat(64))), + ("unpacked_size_bytes", json!(1)), + ] { + let mut fixture = Fixture::new(); + fixture.manifest["plan"]["runtime_container"][field] = value; + let manifest = fixture.parse().unwrap(); + let error = open_fixture_runtime_container(&fixture.root, &manifest).unwrap_err(); + assert_eq!(error.kind(), RusticolErrorKind::Integrity, "{field}"); + } + + let mut fixture = Fixture::new(); + let declared = fixture.manifest["plan"]["runtime_container"]["size_bytes"] + .as_u64() + .unwrap(); + fixture.manifest["plan"]["runtime_container"]["size_bytes"] = json!(declared + 1); + let manifest = fixture.parse().unwrap(); + assert_eq!( + open_fixture_runtime_container(&fixture.root, &manifest) + .unwrap_err() + .kind(), + RusticolErrorKind::Integrity + ); +} + +#[test] +fn incorrect_section_kind_is_rejected() { + let mut fixture = Fixture::new(); + let metadata = write_container_with_section_override( + &fixture.root, + "tables/values.bin", + EagerSectionKind::CurrentLayout, + ); + fixture.manifest = manifest_value(&fixture.outer, &metadata); + let manifest = fixture.parse().unwrap(); + let error = open_fixture_runtime_container(&fixture.root, &manifest).unwrap_err(); + assert_eq!(error.kind(), RusticolErrorKind::Integrity); + assert!(error.to_string().contains("section kind")); +} + +#[test] +fn unexpected_member_path_is_rejected() { + let mut fixture = Fixture::new(); + let metadata = write_container( + &fixture.root, + Some(("tables/values.bin", "tables/unexpected.bin")), + None, + ); + fixture.manifest = manifest_value(&fixture.outer, &metadata); + let manifest = fixture.parse().unwrap(); + let error = open_fixture_runtime_container(&fixture.root, &manifest).unwrap_err(); + assert_eq!(error.kind(), RusticolErrorKind::Security); + assert!(error.to_string().contains("unexpected")); +} + +#[test] +fn incorrect_member_kind_is_rejected() { + let mut fixture = Fixture::new(); + let metadata = write_container( + &fixture.root, + None, + Some(( + "tables/values.bin", + crate::pacbin::PacbinMemberKind::EagerRuntimeMetadata, + )), + ); + fixture.manifest = manifest_value(&fixture.outer, &metadata); + let manifest = fixture.parse().unwrap(); + let error = open_fixture_runtime_container(&fixture.root, &manifest).unwrap_err(); + assert_eq!(error.kind(), RusticolErrorKind::Integrity); + assert!(error.to_string().contains("has kind")); +} + +#[test] +fn legacy_plan_v2_reports_regeneration_before_large_manifest_parse() { + let outer = outer_process(); + let mut bytes = br#"{"eager_plan_abi":"pyamplicol-eager-plan-v2","kind":"pyamplicol-runtime-eager-execution","runtime_schema":"# + .to_vec(); + bytes.extend(std::iter::repeat_n(b'x', 2 * 1024 * 1024)); + bytes.extend_from_slice(br#""}"#); + let error = parse_eager_v3_execution_manifest(&bytes, &outer).unwrap_err(); + assert_eq!(error.kind(), RusticolErrorKind::Compatibility); + assert!(error.to_string().contains("regenerate")); + assert!(error.to_string().contains("plan-v2")); +} + +fn outer_process() -> ArtifactProcess { + ArtifactProcess { + id: "d_dbar_to_z_g".to_string(), + expression: "d d~ > z g".to_string(), + color_accuracy: "full".to_string(), + external_pdgs: vec![1, -1, 23, 21], + physics_path: "physics.json".to_string(), + required_runtime_capabilities: direct_capabilities(), + aliases: Vec::new(), + } +} + +fn manifest_value(outer: &ArtifactProcess, metadata: &ContainerMetadata) -> Value { + let capabilities = json!(direct_capabilities()); + json!({ + "schema_version": PROCESS_ARTIFACT_SCHEMA_VERSION, + "kind": EAGER_EXECUTION_KIND, + "required_runtime_capabilities": capabilities, + "process": outer.expression, + "key": outer.id, + "color_accuracy": outer.color_accuracy, + "external_pdg_order": outer.external_pdgs, + "eager_plan_abi": EAGER_PLAN_ABI, + "kernel_pack": { + "manifest_path": EAGER_KERNEL_PACK_MANIFEST_PATH, + "payload_root": EAGER_KERNEL_PAYLOAD_ROOT, + }, + "runtime_options": {"point_tile_size": 1024, "workspace_mib": 256}, + "plan": { + "kind": EAGER_EXECUTION_KIND, + "eager_plan_abi": EAGER_PLAN_ABI, + "lowering_input_abi": EAGER_LOWERING_INPUT_ABI, + "lowering_input_sha256": "1".repeat(64), + "runtime_layout_abi": EAGER_RUNTIME_LAYOUT_ABI, + "required_runtime_capabilities": capabilities, + "runtime_container": { + "kind": EAGER_RUNTIME_CONTAINER_KIND, + "schema_version": EAGER_RUNTIME_CONTAINER_SCHEMA, + "storage_abi": EAGER_RUNTIME_STORAGE_ABI, + "path": EAGER_RUNTIME_CONTAINER_PATH, + "size_bytes": metadata.size_bytes, + "sha256": metadata.sha256, + "member_count": metadata.member_count, + "unpacked_size_bytes": metadata.unpacked_size_bytes, + "index_sha256": metadata.index_sha256, + }, + "inspection_summary": inspection_summary(outer), + "materialization_census": materialization_census(), + }, + "dag_summary": { + "current_count": 11, + "source_count": 4, + "interaction_count": 23, + "interaction_evaluation_count": 17, + "amplitude_root_count": 5, + "truncated": false, + }, + }) +} + +fn materialization_census() -> Value { + let counts = json!({ + "source_count": 4, + "current_count": 11, + "interaction_count": 23, + "amplitude_root_count": 5, + "invocation_count": 17, + "attachment_count": 19, + "closure_count": 5, + "finalization_count": 11, + }); + json!({ + "abi": "pyamplicol-fully-resident-materialization-census-v1", + "basis": "immutable-fully-resident-eager-plan", + "final": counts.clone(), + "peak": counts, + "final_equals_peak": true, + }) +} + +fn direct_capabilities() -> Vec { + vec![ + EAGER_DIRECT_ARENA_RUNTIME_CAPABILITY.to_string(), + EAGER_RUNTIME_CAPABILITY.to_string(), + ] +} + +fn inspection_summary(outer: &ArtifactProcess) -> Value { + json!({ + "execution_mode": "eager", + "eager_plan_abi": EAGER_PLAN_ABI, + "runtime_layout_abi": EAGER_RUNTIME_LAYOUT_ABI, + "process_id": outer.id, + "model_name": "built-in-sm", + "current_count": 11, + "value_count": 12, + "momentum_count": 13, + "source_count": 4, + "parameter_count": 5, + "stage_count": 3, + "coupling_count": 7, + "invocation_count": 17, + "attachment_count": 19, + "finalization_count": 11, + "closure_count": 5, + "selector_domain_count": 2, + "reduction_group_count": 3, + "reduction_entry_count": 9, + "retained_table_count": 21, + "current_component_count": 31, + "value_component_count": 37, + "momentum_component_count": 41, + "selector_work": { + "abi": "pyamplicol-eager-selector-work-v1", + "selected_flow_selector_count": 3, + "selected_flow_current_count": 7, + "selected_flow_evaluation_count": 9, + "selected_flow_attachment_count": 9, + "all_flow_selector_count": 5, + "all_flow_current_count": 8, + "all_flow_evaluation_count": 10, + "all_flow_attachment_count": 10, + "contracted_current_count": 11, + "contracted_evaluation_count": 17, + "contracted_attachment_count": 19, + }, + }) +} + +fn write_container( + root: &Path, + path_override: Option<(&str, &str)>, + kind_override: Option<(&str, crate::pacbin::PacbinMemberKind)>, +) -> ContainerMetadata { + let mut payloads = Vec::with_capacity(EXPECTED_EAGER_MEMBERS.len()); + for expected in EXPECTED_EAGER_MEMBERS { + payloads.push( + EagerSectionHeader::new(expected.section_kind, 1, 0) + .unwrap() + .encode() + .to_vec(), + ); + } + assert!( + payloads + .iter() + .all(|payload| payload.len() == EAGER_SECTION_HEADER_SIZE) + ); + let mut members = Vec::with_capacity(EXPECTED_EAGER_MEMBERS.len()); + for (expected, payload) in EXPECTED_EAGER_MEMBERS.iter().zip(&payloads) { + let path = path_override + .filter(|(original, _)| *original == expected.path) + .map_or(expected.path, |(_, replacement)| replacement); + let kind = kind_override + .filter(|(original, _)| *original == expected.path) + .map_or(expected.member_kind, |(_, replacement)| replacement); + members.push(PacbinWriteMember::from_bytes(path, kind, payload).unwrap()); + } + let path = root.join(EAGER_RUNTIME_CONTAINER_PATH); + let index = write_pacbin_atomic(&path, members, PacbinWriteOptions::default()).unwrap(); + let bytes = fs::read(&path).unwrap(); + ContainerMetadata { + size_bytes: bytes.len() as u64, + sha256: hex_sha256(&bytes), + member_count: index.members().len() as u64, + unpacked_size_bytes: index.members().iter().map(|member| member.length()).sum(), + index_sha256: hex_bytes(index.index_sha256()), + } +} + +fn write_container_with_section_override( + root: &Path, + override_path: &str, + override_kind: EagerSectionKind, +) -> ContainerMetadata { + let mut payloads = Vec::with_capacity(EXPECTED_EAGER_MEMBERS.len()); + for expected in EXPECTED_EAGER_MEMBERS { + let section_kind = if expected.path == override_path { + override_kind + } else { + expected.section_kind + }; + payloads.push( + EagerSectionHeader::new(section_kind, 1, 0) + .unwrap() + .encode() + .to_vec(), + ); + } + let members: Vec<_> = EXPECTED_EAGER_MEMBERS + .iter() + .zip(&payloads) + .map(|(expected, payload)| { + PacbinWriteMember::from_bytes(expected.path, expected.member_kind, payload).unwrap() + }) + .collect(); + let path = root.join(EAGER_RUNTIME_CONTAINER_PATH); + let index = write_pacbin_atomic(&path, members, PacbinWriteOptions::default()).unwrap(); + let bytes = fs::read(&path).unwrap(); + ContainerMetadata { + size_bytes: bytes.len() as u64, + sha256: hex_sha256(&bytes), + member_count: index.members().len() as u64, + unpacked_size_bytes: index.members().iter().map(|member| member.length()).sum(), + index_sha256: hex_bytes(index.index_sha256()), + } +} + +fn set_value(root: &mut Value, path: &[&str], replacement: Value) { + let (last, parents) = path.split_last().unwrap(); + let mut current = root; + for name in parents { + current = &mut current[*name]; + } + current[*last] = replacement; +} + +fn hex_sha256(bytes: &[u8]) -> String { + hex_bytes(&Sha256::digest(bytes)) +} + +fn hex_bytes(bytes: &[u8]) -> String { + bytes.iter().map(|byte| format!("{byte:02x}")).collect() +} + +fn temporary_directory() -> PathBuf { + let sequence = NEXT_DIRECTORY.fetch_add(1, Ordering::Relaxed); + std::env::temp_dir().join(format!( + "rusticol-eager-v3-manifest-{}-{sequence}", + std::process::id() + )) +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/engine/evaluation.rs b/pyamplicol/rust/crates/rusticol-core/src/engine/evaluation.rs new file mode 100644 index 0000000000..7bb044cdc1 --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/engine/evaluation.rs @@ -0,0 +1,3250 @@ +// SPDX-License-Identifier: 0BSD + +use super::*; + +#[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] +fn evaluate_direct_color_schedule( + direct: &mut compiled_direct_prototype::CompiledDirectEnginePrototype, + schedules: &BTreeMap, + selected_color_sector_ids: Option<&BTreeSet>, + point_count: usize, +) -> RusticolResult<()> { + let Some(selected) = selected_color_sector_ids else { + return direct.evaluate_all(point_count); + }; + if selected.is_empty() { + return Err(RusticolError::invalid_argument( + "compiled Direct-Arena color selection cannot be empty", + )); + } + if selected.len() == 1 { + let sector_id = *selected + .iter() + .next() + .expect("singleton color selection checked"); + let schedule = schedules.get(§or_id).ok_or_else(|| { + RusticolError::integrity(format!( + "compiled Direct-Arena has no cold-bound schedule for materialized sector {sector_id}" + )) + })?; + return direct.evaluate_validated(point_count, schedule); + } + for sector_id in selected { + if !schedules.contains_key(sector_id) { + return Err(RusticolError::integrity(format!( + "compiled Direct-Arena has no cold-bound schedule for materialized sector {sector_id}" + ))); + } + } + // A multi-sector union is uncommon and has no exponential subset cache. + // Executing the full authenticated schedule preserves semantics without a + // hot-path allocation; the reducer still selects exactly the requested + // materialized sectors. + direct.evaluate_all(point_count) +} + +impl ExecutionRuntime { + fn has_compiled_symmetric_group_diagnostic(&self) -> bool { + self.amplitude_stage.as_ref().is_some_and(|amplitude| { + amplitude + .color_contraction + .as_ref() + .is_some_and(ColorContractionRuntime::is_symmetric_group) + }) + } + + fn compiled_symmetric_group_unsupported(&self, operation: &str) -> RusticolError { + RusticolError::compatibility(format!( + "compiled symmetric-group FFT diagnostic does not support {operation}; use unselected f64 total evaluation" + )) + } + + fn use_helicity_sum_runtime_for_selection( + &self, + selected_helicity_ids: Option<&BTreeSet>, + ) -> bool { + self.helicity_sum_runtime.is_some() + && (selected_helicity_ids.is_none() + || self + .physics + .as_ref() + .is_some_and(|physics| physics.has_contracted_color_axis())) + } + + pub(super) fn set_lc_sector_selector(&mut self, sector_id: Option) -> Option { + let index = *self + .model_parameter_name_to_index + .get(LC_SECTOR_SELECTOR_PARAMETER)?; + let previous = *self.model_parameter_values_f64.get(index)?; + if let Some(value) = self.model_parameter_values_f64.get_mut(index) { + *value = sector_id.map(|id| id as f64).unwrap_or(-1.0); + } + Some(previous) + } + + pub(super) fn restore_lc_sector_selector(&mut self, previous: Option) { + let Some(previous) = previous else { + return; + }; + let Some(index) = self + .model_parameter_name_to_index + .get(LC_SECTOR_SELECTOR_PARAMETER) + .copied() + else { + return; + }; + if let Some(value) = self.model_parameter_values_f64.get_mut(index) { + *value = previous; + } + } + + #[allow(dead_code)] // Allocating compatibility wrapper around the into lane. + pub(super) fn run_f64_unprofiled( + &mut self, + batch: F64MomentumBatchView<'_>, + ) -> RusticolResult> { + let mut output = vec![0.0; batch.point_count()]; + self.run_f64_into_unprofiled(batch, &mut output)?; + Ok(output) + } + + pub(super) fn run_f64_into_unprofiled( + &mut self, + batch: F64MomentumBatchView<'_>, + output: &mut [f64], + ) -> RusticolResult<()> { + self.run_f64_selected_into_unprofiled(batch, None, None, output) + } + + /// Test-only oracle retaining the pre-optimization resolved topology-replay + /// path while using the same loaded compiled Direct engines. + #[cfg(test)] + pub(super) fn run_f64_selected_into_resolved_replay_oracle( + &mut self, + batch: F64MomentumBatchView<'_>, + selected_helicity_ids: Option<&BTreeSet>, + selected_color_ids: Option<&BTreeSet>, + output: &mut [f64], + ) -> RusticolResult<()> { + if output.len() != batch.point_count() { + return Err(RusticolError::invalid_argument(format!( + "evaluation output has length {}, expected {}", + output.len(), + batch.point_count() + ))); + } + if selected_helicity_ids.is_none() + && let Some(sum_runtime) = self.helicity_sum_runtime.as_mut() + { + return sum_runtime.run_f64_selected_into_resolved_replay_oracle( + batch, + None, + selected_color_ids, + output, + ); + } + if !self.lc_topology_replay_enabled { + return Err(RusticolError::invalid_argument( + "resolved topology-replay oracle requires an LC replay runtime", + )); + } + let resolved = self.run_resolved_f64_with_lc_topology_replay_unprofiled( + batch, + selected_helicity_ids, + selected_color_ids, + )?; + write_resolved_f64_totals(&resolved, output) + } + + pub(super) fn run_f64_selected_into_unprofiled( + &mut self, + batch: F64MomentumBatchView<'_>, + selected_helicity_ids: Option<&BTreeSet>, + selected_color_ids: Option<&BTreeSet>, + output: &mut [f64], + ) -> RusticolResult<()> { + if output.len() != batch.point_count() { + return Err(RusticolError::invalid_argument(format!( + "evaluation output has length {}, expected {}", + output.len(), + batch.point_count() + ))); + } + if self.has_compiled_symmetric_group_diagnostic() + && (selected_helicity_ids.is_some() || selected_color_ids.is_some()) + { + return Err(self.compiled_symmetric_group_unsupported("runtime selectors")); + } + let (selected_helicity_ids, selected_color_ids) = + self.physics + .as_ref() + .map_or((selected_helicity_ids, selected_color_ids), |physics| { + ( + physics.canonical_total_helicity_selector(selected_helicity_ids), + physics.canonical_total_color_selector(selected_color_ids), + ) + }); + if self.has_safe_singleton_helicity_execution(selected_helicity_ids, selected_color_ids)? + && self.try_run_f64_with_helicity_recurrence_into_unprofiled( + batch, + selected_helicity_ids, + selected_color_ids, + output, + )? + { + return Ok(()); + } + if self.use_helicity_sum_runtime_for_selection(selected_helicity_ids) + && let Some(sum_runtime) = self.helicity_sum_runtime.as_mut() + { + return sum_runtime.run_f64_selected_into_unprofiled( + batch, + selected_helicity_ids, + selected_color_ids, + output, + ); + } + if self.color_topology_replay_enabled { + if selected_helicity_ids.is_some() || selected_color_ids.is_some() { + let resolved = self.run_resolved_f64_with_color_topology_replay_unprofiled( + batch, + selected_helicity_ids, + selected_color_ids, + )?; + return write_resolved_f64_totals(&resolved, output); + } + return self.run_f64_with_color_topology_replay_into_unprofiled(batch, output); + } + if self.lc_topology_replay_enabled { + if selected_color_ids.is_some_and(|ids| ids.len() == 1) + && self.try_run_f64_with_single_lc_topology_replay_into_unprofiled( + batch, + selected_helicity_ids, + selected_color_ids, + output, + )? + { + return Ok(()); + } + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + if self.try_run_f64_with_lc_topology_replay_totals_into_unprofiled( + batch, + selected_helicity_ids, + selected_color_ids, + output, + )? { + return Ok(()); + } + let resolved = self.run_resolved_f64_with_lc_topology_replay_unprofiled( + batch, + selected_helicity_ids, + selected_color_ids, + )?; + return write_resolved_f64_totals(&resolved, output); + } + if self.has_compiled_helicity_execution_plan() { + if self.try_run_f64_with_helicity_recurrence_into_unprofiled( + batch, + selected_helicity_ids, + selected_color_ids, + output, + )? { + return Ok(()); + } + let resolved = self.run_resolved_f64_with_helicity_recurrence_unprofiled( + batch, + selected_helicity_ids, + selected_color_ids, + None, + )?; + return write_resolved_f64_totals(&resolved, output); + } + if selected_helicity_ids.is_some() || selected_color_ids.is_some() { + let resolved = + self.run_resolved_f64_unprofiled(batch, selected_helicity_ids, selected_color_ids)?; + return write_resolved_f64_totals(&resolved, output); + } + let previous = self.set_lc_sector_selector(None); + let result = self.run_f64_materialized_selected_into_unprofiled(batch, None, output); + self.restore_lc_sector_selector(previous); + result + } + + pub(super) fn run_resolved_f64_unprofiled( + &mut self, + batch: F64MomentumBatchView<'_>, + selected_helicity_ids: Option<&BTreeSet>, + selected_color_ids: Option<&BTreeSet>, + ) -> RusticolResult> { + if self.has_compiled_symmetric_group_diagnostic() { + return Err(self.compiled_symmetric_group_unsupported("resolved evaluation")); + } + if self.has_safe_singleton_helicity_execution(selected_helicity_ids, selected_color_ids)? { + return self.run_resolved_f64_with_helicity_recurrence_unprofiled( + batch, + selected_helicity_ids, + selected_color_ids, + None, + ); + } + if self.use_helicity_sum_runtime_for_selection(selected_helicity_ids) + && let Some(sum_runtime) = self.helicity_sum_runtime.as_mut() + { + return sum_runtime.run_resolved_f64_unprofiled( + batch, + selected_helicity_ids, + selected_color_ids, + ); + } + if self.color_topology_replay_enabled { + return self.run_resolved_f64_with_color_topology_replay_unprofiled( + batch, + selected_helicity_ids, + selected_color_ids, + ); + } + if self.lc_topology_replay_enabled { + return self.run_resolved_f64_with_lc_topology_replay_unprofiled( + batch, + selected_helicity_ids, + selected_color_ids, + ); + } + if self.has_compiled_helicity_execution_plan() { + return self.run_resolved_f64_with_helicity_recurrence_unprofiled( + batch, + selected_helicity_ids, + selected_color_ids, + None, + ); + } + let physics = self.physics.clone().ok_or_else(|| { + RusticolError::invalid_argument( + "schema-v3 artifact is missing resolved physics metadata; regenerate it with pyAmpliCol 0.1.0 or newer", + ) + })?; + let selected_color_lane = if selected_color_ids.is_some_and(|ids| ids.len() == 1) { + self.lc_materialized_sector_ids_for_color_ids( + physics.as_ref(), + selected_color_ids.expect("singleton selection checked"), + )? + .into_iter() + .next() + } else { + None + }; + if let Some(sector_id) = selected_color_lane + && let Some(selector_runtime) = self.color_selector_runtimes.get_mut(§or_id) + { + return selector_runtime.run_resolved_f64_unprofiled( + batch, + selected_helicity_ids, + selected_color_ids, + ); + } + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + if self.compiled_direct_runtime.is_some() { + return self.run_f64_materialized_selected_direct_resolved_unprofiled( + batch, + &physics, + selected_helicity_ids, + selected_color_ids, + None, + ); + } + self.run_f64_materialized_selected_for_resolved_unprofiled(batch, None)?; + self.amplitude_stage + .as_mut() + .expect("generic amplitude stage checked") + .reduce_scratch_f64_resolved( + batch.point_count(), + &physics, + self.normalization_factor, + selected_helicity_ids, + selected_color_ids, + ) + } + + fn run_f64_with_color_topology_replay_into_unprofiled( + &mut self, + batch: F64MomentumBatchView<'_>, + output: &mut [f64], + ) -> RusticolResult<()> { + if output.len() != batch.point_count() { + return Err(RusticolError::invalid_argument( + "color topology replay output shape is inconsistent", + )); + } + let mappings = Arc::clone(&self.color_topology_replay_mappings); + self.amplitude_stage + .as_mut() + .ok_or_else(|| { + RusticolError::integrity("color topology replay has no executable amplitude stage") + })? + .begin_color_topology_replay(batch.point_count())?; + let mut flat_momenta = std::mem::take(&mut self.color_replay_flat_momenta_scratch); + let result = (|| { + for (mapping_index, mapping) in mappings.iter().enumerate() { + let evaluation_view = if mapping.is_empty() { + batch + } else { + apply_lc_topology_label_permutation_from_view_into_flat( + batch, + self.external_count, + mapping, + &mut flat_momenta, + )?; + F64MomentumBatchView::from_contiguous_prevalidated( + &flat_momenta, + batch.point_count(), + self.external_count, + None, + )? + }; + self.evaluate_color_topology_replay_mapping_unprofiled( + evaluation_view, + mapping_index, + )?; + } + self.amplitude_stage + .as_mut() + .expect("color topology replay amplitude stage preflighted") + .reduce_color_topology_replay_f64_into(batch.point_count(), output)?; + if self.normalization_factor != 1.0 { + for value in output { + *value *= self.normalization_factor; + } + } + Ok(()) + })(); + self.color_replay_flat_momenta_scratch = flat_momenta; + result + } + + fn run_resolved_f64_with_color_topology_replay_unprofiled( + &mut self, + batch: F64MomentumBatchView<'_>, + selected_helicity_ids: Option<&BTreeSet>, + selected_color_ids: Option<&BTreeSet>, + ) -> RusticolResult> { + let physics = self.physics.clone().ok_or_else(|| { + RusticolError::artifact("color topology replay requires resolved physics metadata") + })?; + let mappings = Arc::clone(&self.color_topology_replay_mappings); + self.amplitude_stage + .as_mut() + .ok_or_else(|| { + RusticolError::integrity("color topology replay has no executable amplitude stage") + })? + .begin_color_topology_replay(batch.point_count())?; + let mut flat_momenta = std::mem::take(&mut self.color_replay_flat_momenta_scratch); + let result = (|| { + for (mapping_index, mapping) in mappings.iter().enumerate() { + let evaluation_view = if mapping.is_empty() { + batch + } else { + apply_lc_topology_label_permutation_from_view_into_flat( + batch, + self.external_count, + mapping, + &mut flat_momenta, + )?; + F64MomentumBatchView::from_contiguous_prevalidated( + &flat_momenta, + batch.point_count(), + self.external_count, + None, + )? + }; + self.evaluate_color_topology_replay_mapping_unprofiled( + evaluation_view, + mapping_index, + )?; + } + self.amplitude_stage + .as_mut() + .expect("color topology replay amplitude stage preflighted") + .reduce_color_topology_replay_f64_resolved( + batch.point_count(), + &physics, + self.normalization_factor, + selected_helicity_ids, + selected_color_ids, + ) + })(); + self.color_replay_flat_momenta_scratch = flat_momenta; + result + } + + fn evaluate_color_topology_replay_mapping_unprofiled( + &mut self, + batch: F64MomentumBatchView<'_>, + mapping_index: usize, + ) -> RusticolResult<()> { + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + if self.compiled_direct_runtime.is_some() { + let Self { + compiled_direct_runtime: Some(direct), + sources, + external_count, + particle_masses, + momentum_slots, + external_is_initial, + model_parameter_values_f64, + amplitude_stage: Some(amplitude), + .. + } = self + else { + return Err(RusticolError::integrity( + "compiled color replay selected an incomplete Direct-Arena runtime", + )); + }; + let tile_capacity = direct.tile_capacity(); + let mut point_start = 0usize; + while point_start < batch.point_count() { + let point_stop = (point_start + tile_capacity).min(batch.point_count()); + let tile = batch.subview(point_start, point_stop)?; + direct.begin_tile_from_inputs( + tile, + sources, + None, + *external_count, + particle_masses, + momentum_slots, + external_is_initial, + model_parameter_values_f64, + )?; + direct.evaluate_all(point_stop - point_start)?; + amplitude.gather_color_topology_replay_planes( + direct.amplitude_planes()?, + point_start, + mapping_index, + )?; + point_start = point_stop; + } + return Ok(()); + } + self.run_f64_materialized_selected_for_resolved_unprofiled(batch, None)?; + self.amplitude_stage + .as_mut() + .expect("color topology replay amplitude stage checked") + .gather_color_topology_replay_mapping(batch.point_count(), mapping_index) + } + + fn run_resolved_f64_with_lc_topology_replay_unprofiled( + &mut self, + batch: F64MomentumBatchView<'_>, + selected_helicity_ids: Option<&BTreeSet>, + selected_color_ids: Option<&BTreeSet>, + ) -> RusticolResult> { + let physics = self.physics.clone().ok_or_else(|| { + RusticolError::invalid_argument( + "schema-v3 artifact is missing resolved physics metadata; regenerate it with pyAmpliCol 0.1.0 or newer", + ) + })?; + let replay_plan = self.cached_lc_resolved_replay_plan(&physics)?; + let selection = self.cached_lc_resolved_replay_selection( + &physics, + replay_plan.as_ref(), + selected_helicity_ids, + selected_color_ids, + )?; + let n_points = batch.point_count(); + let component_count = selection.helicity_indices.len() * selection.color_indices.len(); + let mut full_values = vec![0.0; n_points * component_count]; + let mappings = self.lc_topology_replay_mappings.clone(); + let mappings_per_chunk = replay_mappings_per_expanded_batch(n_points); + for source_group in &selection.source_groups { + let materialized_sector_ids = + self.lc_materialized_sector_ids_for_color_ids(&physics, &source_group.color_ids)?; + for chunk_start in (0..source_group.mapping_indices.len()).step_by(mappings_per_chunk) { + let chunk_end = usize::min( + chunk_start + mappings_per_chunk, + source_group.mapping_indices.len(), + ); + let mapping_chunk = source_group.mapping_indices[chunk_start..chunk_end] + .iter() + .map(|mapping_index| mappings[*mapping_index].clone()) + .collect::>(); + let entry_chunk = &source_group.entries[chunk_start..chunk_end]; + let expanded_batch = if mapping_chunk.len() == 1 && mapping_chunk[0].is_empty() { + None + } else { + Some(apply_lc_topology_label_permutations_from_view( + batch, + self.external_count, + &mapping_chunk, + )?) + }; + let evaluation_view = if let Some(expanded_batch) = expanded_batch.as_deref() { + F64MomentumBatchView::from_nested(expanded_batch, self.external_count)? + } else { + batch + }; + let selected_lane_id = (materialized_sector_ids.len() == 1) + .then(|| materialized_sector_ids.iter().next().copied()) + .flatten(); + let materialized = if self.has_compiled_helicity_execution_plan() + && selected_helicity_ids.is_some() + { + self.run_resolved_f64_with_helicity_recurrence_unprofiled( + evaluation_view, + Some(&source_group.helicity_ids), + Some(&source_group.color_ids), + Some(&materialized_sector_ids), + )? + } else if let Some(selector_runtime) = selected_lane_id + .and_then(|sector_id| self.color_selector_runtimes.get_mut(§or_id)) + { + selector_runtime.run_resolved_f64_unprofiled( + evaluation_view, + Some(&source_group.helicity_ids), + Some(&source_group.color_ids), + )? + } else { + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + if self.compiled_direct_runtime.is_some() { + self.run_f64_materialized_selected_direct_resolved_unprofiled( + evaluation_view, + &physics, + Some(&source_group.helicity_ids), + Some(&source_group.color_ids), + Some(&materialized_sector_ids), + )? + } else { + self.run_f64_materialized_selected_for_resolved_unprofiled( + evaluation_view, + Some(&materialized_sector_ids), + )?; + self.amplitude_stage + .as_mut() + .expect("generic amplitude stage checked") + .reduce_scratch_f64_resolved( + evaluation_view.point_count(), + &physics, + self.normalization_factor, + Some(&source_group.helicity_ids), + Some(&source_group.color_ids), + )? + } + #[cfg(not(any(feature = "f64-compiled", feature = "f64-symjit")))] + { + self.run_f64_materialized_selected_for_resolved_unprofiled( + evaluation_view, + Some(&materialized_sector_ids), + )?; + self.amplitude_stage + .as_mut() + .expect("generic amplitude stage checked") + .reduce_scratch_f64_resolved( + evaluation_view.point_count(), + &physics, + self.normalization_factor, + Some(&source_group.helicity_ids), + Some(&source_group.color_ids), + )? + } + }; + accumulate_selected_lc_replay_resolved_f64( + &mut full_values, + n_points, + &materialized, + entry_chunk, + source_group.source_component_count, + component_count, + )?; + } + } + Ok(ResolvedValues { + values: full_values, + point_count: n_points, + helicity_indices: selection.helicity_indices.clone(), + color_indices: selection.color_indices.clone(), + }) + } + + /// Execute the dominant complete-layout selector shape without resolved + /// batch tensors: one requested public flow, replayed by one topology + /// mapping through one fused-helicity materialized-flow lane. + /// + /// Multiple mappings or recursive execution plans retain the resolved + /// fallback because their target cells must survive across evaluations to + /// preserve exact accumulation order. + fn try_run_f64_with_single_lc_topology_replay_into_unprofiled( + &mut self, + batch: F64MomentumBatchView<'_>, + selected_helicity_ids: Option<&BTreeSet>, + selected_color_ids: Option<&BTreeSet>, + output: &mut [f64], + ) -> RusticolResult { + // A fused colour lane computes the complete helicity axis. With an + // explicit helicity selection, retain the parent's pruned schedule + // and map the selected physical flow before executing it instead. + if selected_helicity_ids.is_some() && self.has_compiled_helicity_execution_plan() { + return Ok(false); + } + let physics = self.physics.clone().ok_or_else(|| { + RusticolError::invalid_argument( + "schema-v3 artifact is missing resolved physics metadata; regenerate it with pyAmpliCol 0.1.0 or newer", + ) + })?; + let replay_plan = self.cached_lc_resolved_replay_plan(&physics)?; + let selection = self.cached_lc_resolved_replay_selection( + &physics, + replay_plan.as_ref(), + selected_helicity_ids, + selected_color_ids, + )?; + let [source_group] = selection.source_groups.as_slice() else { + return Ok(false); + }; + let [mapping_index] = source_group.mapping_indices.as_slice() else { + return Ok(false); + }; + let [replay_entry] = source_group.entries.as_slice() else { + return Ok(false); + }; + let materialized_sector_ids = + source_group + .materialized_sector_ids + .as_ref() + .ok_or_else(|| { + RusticolError::integrity( + "LC topology replay selection is missing its materialized-sector cache", + ) + })?; + if materialized_sector_ids.len() != 1 { + return Ok(false); + } + let materialized_sector_id = *materialized_sector_ids + .iter() + .next() + .expect("singleton materialized sector checked"); + let Some(selector_runtime) = self.color_selector_runtimes.get(&materialized_sector_id) + else { + return Ok(false); + }; + if selector_runtime.helicity_sum_runtime.is_some() + || selector_runtime.lc_topology_replay_enabled + || selector_runtime.has_compiled_helicity_execution_plan() + || selector_runtime + .amplitude_stage + .as_ref() + .is_none_or(|amplitude| amplitude.color_contraction.is_some()) + { + return Ok(false); + } + + let target_component_count = selection + .helicity_indices + .len() + .checked_mul(selection.color_indices.len()) + .ok_or_else(|| RusticolError::invalid_argument("resolved shape overflows usize"))?; + let mappings = Arc::clone(&self.lc_topology_replay_mappings); + let mapping = mappings.get(*mapping_index).ok_or_else(|| { + RusticolError::integrity("LC topology replay selection references an unknown mapping") + })?; + let mut flat_momenta = std::mem::take(&mut self.lc_replay_flat_momenta_scratch); + let result = (|| { + let evaluation_view = if mapping.is_empty() { + batch + } else { + apply_lc_topology_label_permutation_from_view_into_flat( + batch, + self.external_count, + mapping, + &mut flat_momenta, + )?; + F64MomentumBatchView::from_contiguous_prevalidated( + &flat_momenta, + batch.point_count(), + self.external_count, + None, + )? + }; + if let Some(direct_plan) = source_group.direct_total_plan { + if direct_plan.mapping_index != *mapping_index + || direct_plan.materialized_sector_id != materialized_sector_id + { + return Err(RusticolError::integrity( + "LC topology replay direct-total plan disagrees with its selected source", + )); + } + self.color_selector_runtimes + .get_mut(&materialized_sector_id) + .expect("selector lane checked above") + .run_f64_into_unprofiled(evaluation_view, output)?; + if direct_plan.scale != 1.0 { + for value in output { + *value *= direct_plan.scale; + } + } + return Ok(true); + } + self.color_selector_runtimes + .get_mut(&materialized_sector_id) + .expect("selector lane checked above") + .run_f64_routed_materialized_into_unprofiled( + evaluation_view, + Some(&source_group.helicity_ids), + Some(&source_group.color_ids), + replay_entry, + source_group.source_component_count, + target_component_count, + output, + )?; + Ok(true) + })(); + self.lc_replay_flat_momenta_scratch = flat_momenta; + result + } + + /// Execute arbitrary LC topology-replay totals through compiled arena + /// planes without constructing expanded nested momenta or `ResolvedValues`. + /// + /// One mapping is streamed at a time. Persistent point-major target + /// components retain the resolved contract's mapping accumulation and + /// final H-major/C-minor fold exactly. + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + fn try_run_f64_with_lc_topology_replay_totals_into_unprofiled( + &mut self, + batch: F64MomentumBatchView<'_>, + selected_helicity_ids: Option<&BTreeSet>, + selected_color_ids: Option<&BTreeSet>, + output: &mut [f64], + ) -> RusticolResult { + let physics = self.physics.clone().ok_or_else(|| { + RusticolError::invalid_argument( + "schema-v3 artifact is missing resolved physics metadata; regenerate it with pyAmpliCol 0.1.0 or newer", + ) + })?; + let replay_plan = self.cached_lc_resolved_replay_plan(&physics)?; + let selection = self.cached_lc_resolved_replay_selection( + &physics, + replay_plan.as_ref(), + selected_helicity_ids, + selected_color_ids, + )?; + let mappings = Arc::clone(&self.lc_topology_replay_mappings); + let target_component_count = selection + .helicity_indices + .len() + .checked_mul(selection.color_indices.len()) + .ok_or_else(|| RusticolError::invalid_argument("resolved shape overflows usize"))?; + let target_scalar_count = batch + .point_count() + .checked_mul(target_component_count) + .ok_or_else(|| RusticolError::invalid_argument("resolved shape overflows usize"))?; + + // Preflight every source before touching output, so unsupported + // configurations can retain the resolved compatibility path. + for source_group in &selection.source_groups { + if source_group.mapping_indices.len() != source_group.entries.len() { + return Err(RusticolError::integrity( + "LC topology replay source has inconsistent mappings and routes", + )); + } + let materialized_sector_ids = source_group + .materialized_sector_ids + .as_ref() + .ok_or_else(|| { + RusticolError::integrity( + "LC topology replay selection is missing its materialized-sector cache", + ) + })?; + if self.has_compiled_helicity_execution_plan() && selected_helicity_ids.is_some() { + if self.compiled_direct_runtime.is_none() + || source_group.helicity_ids.len() != 1 + || materialized_sector_ids.is_empty() + { + return Ok(false); + } + continue; + } + let direct_selector_lane = (materialized_sector_ids.len() == 1) + .then(|| materialized_sector_ids.iter().next().copied()) + .flatten() + .and_then(|sector_id| self.color_selector_runtimes.get(§or_id)) + .is_some_and(|runtime| runtime.compiled_direct_runtime.is_some()); + if !direct_selector_lane && self.compiled_direct_runtime.is_none() { + return Ok(false); + } + } + + let mut flat_momenta = std::mem::take(&mut self.lc_replay_flat_momenta_scratch); + let mut target_components = std::mem::take(&mut self.lc_replay_target_components_scratch); + target_components.resize(target_scalar_count, 0.0); + target_components.fill(0.0); + let result = (|| { + for source_group in &selection.source_groups { + let materialized_sector_ids = source_group + .materialized_sector_ids + .as_ref() + .expect("topology-replay source cache preflighted"); + for (mapping_index, replay_entry) in source_group + .mapping_indices + .iter() + .copied() + .zip(&source_group.entries) + { + let mapping = mappings.get(mapping_index).ok_or_else(|| { + RusticolError::integrity( + "LC topology replay selection references an unknown mapping", + ) + })?; + let evaluation_view = if mapping.is_empty() { + batch + } else { + apply_lc_topology_label_permutation_from_view_into_flat( + batch, + self.external_count, + mapping, + &mut flat_momenta, + )?; + F64MomentumBatchView::from_contiguous_prevalidated( + &flat_momenta, + batch.point_count(), + self.external_count, + None, + )? + }; + + if self.has_compiled_helicity_execution_plan() + && selected_helicity_ids.is_some() + { + self.run_f64_materialized_helicity_direct_routed_components_add_unprofiled( + evaluation_view, + &physics, + &source_group.helicity_ids, + Some(&source_group.color_ids), + materialized_sector_ids, + replay_entry, + source_group.source_component_count, + target_component_count, + batch.point_count(), + &mut target_components, + )?; + continue; + } + + let selected_lane_id = (materialized_sector_ids.len() == 1) + .then(|| materialized_sector_ids.iter().next().copied()) + .flatten() + .filter(|sector_id| { + self.color_selector_runtimes + .get(sector_id) + .is_some_and(|runtime| runtime.compiled_direct_runtime.is_some()) + }); + if let Some(sector_id) = selected_lane_id { + self.color_selector_runtimes + .get_mut(§or_id) + .expect("direct selector lane preflighted") + .run_f64_routed_materialized_direct_components_add_unprofiled( + evaluation_view, + Some(&source_group.helicity_ids), + Some(&source_group.color_ids), + None, + replay_entry, + source_group.source_component_count, + target_component_count, + batch.point_count(), + &mut target_components, + )?; + } else { + self.run_f64_routed_materialized_direct_components_add_unprofiled( + evaluation_view, + Some(&source_group.helicity_ids), + Some(&source_group.color_ids), + Some(materialized_sector_ids), + replay_entry, + source_group.source_component_count, + target_component_count, + batch.point_count(), + &mut target_components, + )?; + } + } + } + for (target, components) in output + .iter_mut() + .zip(target_components.chunks_exact(target_component_count)) + { + *target = components.iter().sum(); + } + Ok(true) + })(); + self.lc_replay_flat_momenta_scratch = flat_momenta; + self.lc_replay_target_components_scratch = target_components; + result + } + + #[allow(clippy::too_many_arguments)] + fn run_f64_routed_materialized_into_unprofiled( + &mut self, + batch: F64MomentumBatchView<'_>, + selected_helicity_ids: Option<&BTreeSet>, + selected_color_ids: Option<&BTreeSet>, + replay_entry: &LcResolvedReplayEntry, + source_component_count: usize, + target_component_count: usize, + output: &mut [f64], + ) -> RusticolResult<()> { + if self.helicity_sum_runtime.is_some() + || self.lc_topology_replay_enabled + || self.has_compiled_helicity_execution_plan() + { + return Err(RusticolError::integrity( + "routed direct totals require a materialized leaf execution lane", + )); + } + let physics = self.physics.clone().ok_or_else(|| { + RusticolError::invalid_argument( + "schema-v3 artifact is missing resolved physics metadata; regenerate it with pyAmpliCol 0.1.0 or newer", + ) + })?; + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + if self.compiled_direct_runtime.is_some() { + return self.run_f64_routed_materialized_direct_into_unprofiled( + batch, + &physics, + selected_helicity_ids, + selected_color_ids, + replay_entry, + source_component_count, + target_component_count, + output, + ); + } + self.run_f64_materialized_selected_for_resolved_unprofiled(batch, None)?; + self.amplitude_stage + .as_mut() + .expect("materialized leaf amplitude stage checked") + .reduce_scratch_f64_routed_totals_into( + batch.point_count(), + &physics, + self.normalization_factor, + selected_helicity_ids, + selected_color_ids, + replay_entry, + source_component_count, + target_component_count, + output, + ) + } + + #[allow(dead_code)] // Allocating compatibility wrapper around the into lane. + fn run_f64_materialized_selected_unprofiled( + &mut self, + batch: F64MomentumBatchView<'_>, + selected_color_sector_ids: Option<&BTreeSet>, + ) -> RusticolResult> { + let mut output = vec![0.0; batch.point_count()]; + self.run_f64_materialized_selected_into_unprofiled( + batch, + selected_color_sector_ids, + &mut output, + )?; + Ok(output) + } + + fn run_f64_materialized_selected_for_resolved_unprofiled( + &mut self, + batch: F64MomentumBatchView<'_>, + selected_color_sector_ids: Option<&BTreeSet>, + ) -> RusticolResult<()> { + self.evaluate_f64_materialized_selected_unprofiled(batch, selected_color_sector_ids) + } + + fn run_f64_materialized_selected_into_unprofiled( + &mut self, + batch: F64MomentumBatchView<'_>, + selected_color_sector_ids: Option<&BTreeSet>, + output: &mut [f64], + ) -> RusticolResult<()> { + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + if self.compiled_direct_runtime.is_some() { + return self.run_f64_materialized_selected_direct_into_unprofiled( + batch, + selected_color_sector_ids, + output, + ); + } + let n_points = batch.point_count(); + if output.len() != n_points { + return Err(RusticolError::invalid_argument(format!( + "evaluation output has length {}, expected {n_points}", + output.len() + ))); + } + self.evaluate_f64_materialized_selected_unprofiled(batch, selected_color_sector_ids)?; + self.amplitude_stage + .as_mut() + .expect("generic amplitude stage checked") + .reduce_scratch_f64_into_selected_slice(n_points, output, selected_color_sector_ids)?; + for value in output { + *value *= self.normalization_factor; + } + Ok(()) + } + + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + fn run_f64_materialized_selected_direct_into_unprofiled( + &mut self, + batch: F64MomentumBatchView<'_>, + selected_color_sector_ids: Option<&BTreeSet>, + output: &mut [f64], + ) -> RusticolResult<()> { + let point_count = batch.point_count(); + if output.len() != point_count { + return Err(RusticolError::invalid_argument(format!( + "evaluation output has length {}, expected {point_count}", + output.len() + ))); + } + let Self { + compiled_direct_runtime: Some(direct), + compiled_direct_color_schedules, + sources, + external_count, + particle_masses, + momentum_slots, + external_is_initial, + model_parameter_values_f64, + amplitude_stage: Some(amplitude), + normalization_factor, + .. + } = self + else { + return Err(RusticolError::integrity( + "compiled Direct-Arena execution was selected without a complete runtime", + )); + }; + let tile_capacity = if amplitude + .color_contraction + .as_ref() + .is_some_and(ColorContractionRuntime::is_symmetric_group) + { + direct.reduction_tile_capacity() + } else { + direct.tile_capacity() + }; + let mut point_start = 0usize; + while point_start < point_count { + let point_stop = (point_start + tile_capacity).min(point_count); + let tile = batch.subview(point_start, point_stop)?; + direct.begin_tile_from_inputs( + tile, + sources, + None, + *external_count, + particle_masses, + momentum_slots, + external_is_initial, + model_parameter_values_f64, + )?; + evaluate_direct_color_schedule( + direct, + compiled_direct_color_schedules, + selected_color_sector_ids, + point_stop - point_start, + )?; + let planes = direct.amplitude_planes()?; + amplitude.reduce_planes_f64_into_selected_slice( + planes, + &mut output[point_start..point_stop], + selected_color_sector_ids, + )?; + if *normalization_factor != 1.0 { + for value in &mut output[point_start..point_stop] { + *value *= *normalization_factor; + } + } + point_start = point_stop; + } + Ok(()) + } + + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + #[allow(clippy::too_many_arguments)] + fn run_f64_materialized_selected_direct_resolved_unprofiled( + &mut self, + batch: F64MomentumBatchView<'_>, + physics: &PhysicsRuntime, + selected_helicity_ids: Option<&BTreeSet>, + selected_color_ids: Option<&BTreeSet>, + selected_color_sector_ids: Option<&BTreeSet>, + ) -> RusticolResult> { + let point_count = batch.point_count(); + let helicity_indices = physics.selected_helicity_indices(selected_helicity_ids)?; + let color_indices = physics.selected_color_indices(selected_color_ids)?; + let component_count = helicity_indices + .len() + .checked_mul(color_indices.len()) + .ok_or_else(|| RusticolError::invalid_argument("resolved shape overflows usize"))?; + let mut values = vec![0.0; point_count * component_count]; + let Self { + compiled_direct_runtime: Some(direct), + compiled_direct_color_schedules, + sources, + external_count, + particle_masses, + momentum_slots, + external_is_initial, + model_parameter_values_f64, + amplitude_stage: Some(amplitude), + normalization_factor, + .. + } = self + else { + return Err(RusticolError::integrity( + "compiled Direct-Arena resolved execution was selected without a complete runtime", + )); + }; + let tile_capacity = direct.reduction_tile_capacity(); + let mut point_start = 0usize; + while point_start < point_count { + let point_stop = (point_start + tile_capacity).min(point_count); + let tile = batch.subview(point_start, point_stop)?; + direct.begin_tile_from_inputs( + tile, + sources, + None, + *external_count, + particle_masses, + momentum_slots, + external_is_initial, + model_parameter_values_f64, + )?; + evaluate_direct_color_schedule( + direct, + compiled_direct_color_schedules, + selected_color_sector_ids, + point_stop - point_start, + )?; + let resolved = amplitude.reduce_planes_f64_resolved( + direct.amplitude_planes()?, + physics, + *normalization_factor, + selected_helicity_ids, + selected_color_ids, + )?; + if resolved.helicity_indices != helicity_indices + || resolved.color_indices != color_indices + || resolved.point_count != point_stop - point_start + { + return Err(RusticolError::integrity( + "compiled Direct-Arena reducer returned an inconsistent resolved shape", + )); + } + for tile_point in 0..resolved.point_count { + let source = tile_point * component_count; + let target = (point_start + tile_point) * component_count; + values[target..target + component_count] + .copy_from_slice(&resolved.values[source..source + component_count]); + } + point_start = point_stop; + } + Ok(ResolvedValues { + values, + point_count, + helicity_indices, + color_indices, + }) + } + + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + #[allow(clippy::too_many_arguments)] + fn run_f64_routed_materialized_direct_into_unprofiled( + &mut self, + batch: F64MomentumBatchView<'_>, + physics: &PhysicsRuntime, + selected_helicity_ids: Option<&BTreeSet>, + selected_color_ids: Option<&BTreeSet>, + replay_entry: &LcResolvedReplayEntry, + source_component_count: usize, + target_component_count: usize, + output: &mut [f64], + ) -> RusticolResult<()> { + let point_count = batch.point_count(); + if output.len() != point_count { + return Err(RusticolError::invalid_argument(format!( + "routed evaluation output has length {}, expected {point_count}", + output.len() + ))); + } + let Self { + compiled_direct_runtime: Some(direct), + sources, + external_count, + particle_masses, + momentum_slots, + external_is_initial, + model_parameter_values_f64, + amplitude_stage: Some(amplitude), + normalization_factor, + .. + } = self + else { + return Err(RusticolError::integrity( + "compiled Direct-Arena routed execution was selected without a complete runtime", + )); + }; + // This leaf is reached by a single selected LC flow. Size its routed + // scratch from the actual selected component axes, not the parent + // runtime's worst-case complete helicity/color Cartesian product. + let reduction_footprint = amplitude.compiled_direct_reduction_footprint( + 0, + CompiledDirectRoutedReductionFootprint { + maximum_source_component_count: source_component_count, + maximum_target_component_count: target_component_count, + }, + )?; + let tile_capacity = direct.reduction_tile_capacity_for_footprint(reduction_footprint)?; + let mut point_start = 0usize; + while point_start < point_count { + let point_stop = (point_start + tile_capacity).min(point_count); + let tile = batch.subview(point_start, point_stop)?; + direct.begin_tile_from_inputs( + tile, + sources, + None, + *external_count, + particle_masses, + momentum_slots, + external_is_initial, + model_parameter_values_f64, + )?; + direct.evaluate_all(point_stop - point_start)?; + amplitude.reduce_planes_f64_routed_totals_into( + direct.amplitude_planes()?, + physics, + *normalization_factor, + selected_helicity_ids, + selected_color_ids, + replay_entry, + source_component_count, + target_component_count, + &mut output[point_start..point_stop], + )?; + point_start = point_stop; + } + Ok(()) + } + + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + #[allow(clippy::too_many_arguments)] + fn run_f64_routed_materialized_direct_components_add_unprofiled( + &mut self, + batch: F64MomentumBatchView<'_>, + selected_helicity_ids: Option<&BTreeSet>, + selected_color_ids: Option<&BTreeSet>, + selected_color_sector_ids: Option<&BTreeSet>, + replay_entry: &LcResolvedReplayEntry, + source_component_count: usize, + target_component_count: usize, + target_point_count: usize, + target_components: &mut [f64], + ) -> RusticolResult<()> { + let point_count = batch.point_count(); + let physics = self.physics.clone().ok_or_else(|| { + RusticolError::invalid_argument( + "schema-v3 artifact is missing resolved physics metadata; regenerate it with pyAmpliCol 0.1.0 or newer", + ) + })?; + let Self { + compiled_direct_runtime: Some(direct), + compiled_direct_color_schedules, + sources, + external_count, + particle_masses, + momentum_slots, + external_is_initial, + model_parameter_values_f64, + amplitude_stage: Some(amplitude), + normalization_factor, + .. + } = self + else { + return Err(RusticolError::integrity( + "compiled Direct-Arena routed execution was selected without a complete runtime", + )); + }; + let tile_capacity = direct.reduction_tile_capacity(); + let mut point_start = 0usize; + while point_start < point_count { + let point_stop = (point_start + tile_capacity).min(point_count); + let tile = batch.subview(point_start, point_stop)?; + direct.begin_tile_from_inputs( + tile, + sources, + None, + *external_count, + particle_masses, + momentum_slots, + external_is_initial, + model_parameter_values_f64, + )?; + evaluate_direct_color_schedule( + direct, + compiled_direct_color_schedules, + selected_color_sector_ids, + point_stop - point_start, + )?; + amplitude.reduce_planes_f64_routed_components_add_into( + direct.amplitude_planes()?, + &physics, + *normalization_factor, + selected_helicity_ids, + selected_color_ids, + replay_entry, + source_component_count, + target_component_count, + target_point_count, + point_start, + target_components, + )?; + point_start = point_stop; + } + Ok(()) + } + + fn evaluate_f64_materialized_selected_unprofiled( + &mut self, + batch: F64MomentumBatchView<'_>, + selected_color_sector_ids: Option<&BTreeSet>, + ) -> RusticolResult<()> { + if self.stages.is_none() || self.amplitude_stage.is_none() { + return Err(self.execution_unavailable_error()); + } + let n_points = batch.point_count(); + let color_schedule = selected_color_sector_ids + .map(|sector_ids| self.compiled_color_selector_schedule(sector_ids)) + .transpose()? + .flatten(); + let state_len = n_points * self.parameter_count; + self.state_scratch_f64.resize(state_len, c64(0.0, 0.0)); + if self.state_scratch_f64_requires_clear { + self.state_scratch_f64.fill(c64(0.0, 0.0)); + } + self.state_scratch_f64_requires_clear = true; + let state = &mut self.state_scratch_f64; + let model_parameter_start = self.value_parameter_count + self.momentum_parameter_count; + for row in 0..n_points { + let point = batch.point(row); + let row_state = + &mut state[row * self.parameter_count..(row + 1) * self.parameter_count]; + Self::fill_sources_row( + &self.sources, + self.external_count, + &self.particle_masses, + row_state, + &point, + )?; + } + for row in 0..n_points { + let point = batch.point(row); + let row_state = + &mut state[row * self.parameter_count..(row + 1) * self.parameter_count]; + Self::fill_momenta_row( + &self.momentum_slots, + self.value_parameter_count, + self.external_count, + &self.external_is_initial, + row_state, + &point, + )?; + } + for row in 0..n_points { + let row_state = + &mut state[row * self.parameter_count..(row + 1) * self.parameter_count]; + Self::fill_model_parameters_row( + model_parameter_start, + &self.model_parameter_values_f64, + row_state, + )?; + } + for (stage_index, stage) in self + .stages + .as_mut() + .expect("generic stages checked") + .iter_mut() + .enumerate() + { + if let Some(schedule) = color_schedule.as_ref() { + stage.evaluate_active_chunks_f64_into_state( + n_points, + self.parameter_count, + state.as_mut_slice(), + &schedule.active_stage_chunk_indices[stage_index], + )?; + } else { + stage.evaluate_f64_into_state( + n_points, + self.parameter_count, + state.as_mut_slice(), + )?; + } + } + if let Some(schedule) = color_schedule.as_ref() { + self.amplitude_stage + .as_mut() + .expect("generic amplitude stage checked") + .evaluate_active_chunks_f64_into_scratch( + n_points, + state.as_slice(), + &schedule.active_amplitude_chunk_indices, + )?; + } else { + self.amplitude_stage + .as_mut() + .expect("generic amplitude stage checked") + .evaluate_f64_into_scratch(n_points, state.as_slice())?; + } + self.state_scratch_f64_requires_clear = color_schedule.is_some(); + Ok(()) + } + + pub(super) fn run_f64( + &mut self, + batch: &[Vec<[f64; 4]>], + ) -> RusticolResult<(Vec, RuntimeProfile)> { + if let Some(sum_runtime) = self.helicity_sum_runtime.as_mut() { + return sum_runtime.run_f64(batch); + } + if self.color_topology_replay_enabled { + return self.run_f64_with_color_topology_replay(batch); + } + if self.lc_topology_replay_enabled { + return self.run_f64_with_lc_topology_replay(batch); + } + if self.has_compiled_helicity_execution_plan() { + let (resolved, mut profile) = + self.run_resolved_f64_with_helicity_recurrence(batch, None, None, None)?; + let materialization_start = Instant::now(); + let values = resolved_f64_totals(&resolved)?; + profile.total_materialization_s += materialization_start.elapsed().as_secs_f64(); + profile.total_materialized_value_count += values.len() as u64; + return Ok((values, profile)); + } + self.run_f64_selected(batch, None) + } + + pub(super) fn run_f64_selected_totals( + &mut self, + batch: &[Vec<[f64; 4]>], + selected_helicity_ids: Option<&BTreeSet>, + selected_color_ids: Option<&BTreeSet>, + ) -> RusticolResult<(Vec, RuntimeProfile)> { + if self.has_compiled_symmetric_group_diagnostic() + && (selected_helicity_ids.is_some() || selected_color_ids.is_some()) + { + return Err(self.compiled_symmetric_group_unsupported("runtime selectors")); + } + let (selected_helicity_ids, selected_color_ids) = + self.physics + .as_ref() + .map_or((selected_helicity_ids, selected_color_ids), |physics| { + ( + physics.canonical_total_helicity_selector(selected_helicity_ids), + physics.canonical_total_color_selector(selected_color_ids), + ) + }); + if self.has_safe_singleton_helicity_execution(selected_helicity_ids, selected_color_ids)? { + let (resolved, mut profile) = self.run_resolved_f64_with_helicity_recurrence( + batch, + selected_helicity_ids, + selected_color_ids, + None, + )?; + let materialization_start = Instant::now(); + let values = resolved_f64_totals(&resolved)?; + profile.total_materialization_s += materialization_start.elapsed().as_secs_f64(); + profile.total_materialized_value_count += values.len() as u64; + return Ok((values, profile)); + } + if self.use_helicity_sum_runtime_for_selection(selected_helicity_ids) + && let Some(sum_runtime) = self.helicity_sum_runtime.as_mut() + { + return sum_runtime.run_f64_selected_totals( + batch, + selected_helicity_ids, + selected_color_ids, + ); + } + if self.color_topology_replay_enabled { + let (resolved, mut profile) = self.run_resolved_f64_with_color_topology_replay( + batch, + selected_helicity_ids, + selected_color_ids, + )?; + let materialization_start = Instant::now(); + let values = resolved_f64_totals(&resolved)?; + profile.total_materialization_s += materialization_start.elapsed().as_secs_f64(); + profile.total_materialized_value_count += values.len() as u64; + return Ok((values, profile)); + } + if self.lc_topology_replay_enabled + && selected_color_ids.is_some_and(|ids| ids.len() == 1) + && let Some(result) = self.try_run_f64_with_single_lc_topology_replay( + batch, + selected_helicity_ids, + selected_color_ids, + )? + { + return Ok(result); + } + let (resolved, mut profile) = + self.run_resolved_f64(batch, selected_helicity_ids, selected_color_ids)?; + let materialization_start = Instant::now(); + let values = resolved_f64_totals(&resolved)?; + profile.total_materialization_s += materialization_start.elapsed().as_secs_f64(); + profile.total_materialized_value_count += values.len() as u64; + Ok((values, profile)) + } + + fn try_run_f64_with_single_lc_topology_replay( + &mut self, + batch: &[Vec<[f64; 4]>], + selected_helicity_ids: Option<&BTreeSet>, + selected_color_ids: Option<&BTreeSet>, + ) -> RusticolResult, RuntimeProfile)>> { + let total_start = Instant::now(); + let physics = self.physics.clone().ok_or_else(|| { + RusticolError::invalid_argument( + "schema-v3 artifact is missing resolved physics metadata; regenerate it with pyAmpliCol 0.1.0 or newer", + ) + })?; + let replay_plan = self.cached_lc_resolved_replay_plan(&physics)?; + let selection = self.cached_lc_resolved_replay_selection( + &physics, + replay_plan.as_ref(), + selected_helicity_ids, + selected_color_ids, + )?; + let [source_group] = selection.source_groups.as_slice() else { + return Ok(None); + }; + let [mapping_index] = source_group.mapping_indices.as_slice() else { + return Ok(None); + }; + let [_replay_entry] = source_group.entries.as_slice() else { + return Ok(None); + }; + let Some(direct_plan) = source_group.direct_total_plan else { + return Ok(None); + }; + let materialized_sector_ids = + source_group + .materialized_sector_ids + .as_ref() + .ok_or_else(|| { + RusticolError::integrity( + "LC topology replay selection is missing its materialized-sector cache", + ) + })?; + if materialized_sector_ids.len() != 1 { + return Ok(None); + } + let materialized_sector_id = *materialized_sector_ids + .iter() + .next() + .expect("singleton materialized sector checked"); + if direct_plan.mapping_index != *mapping_index + || direct_plan.materialized_sector_id != materialized_sector_id + { + return Err(RusticolError::integrity( + "LC topology replay direct-total plan disagrees with its selected source", + )); + } + let Some(selector_runtime) = self.color_selector_runtimes.get(&materialized_sector_id) + else { + return Ok(None); + }; + if selected_helicity_ids.is_some() + || selector_runtime.helicity_sum_runtime.is_some() + || selector_runtime.lc_topology_replay_enabled + || selector_runtime.has_compiled_helicity_execution_plan() + || selector_runtime + .amplitude_stage + .as_ref() + .is_none_or(|amplitude| amplitude.color_contraction.is_some()) + { + return Ok(None); + } + let mapping = self + .lc_topology_replay_mappings + .get(*mapping_index) + .ok_or_else(|| { + RusticolError::integrity( + "LC topology replay selection references an unknown mapping", + ) + })?; + let expanded_batch = if mapping.is_empty() { + None + } else { + Some(apply_lc_topology_label_permutations( + batch, + self.external_count, + std::slice::from_ref(mapping), + )?) + }; + let evaluation_batch = expanded_batch.as_deref().unwrap_or(batch); + let (mut values, mut profile) = self + .color_selector_runtimes + .get_mut(&materialized_sector_id) + .expect("selector lane checked above") + .run_f64(evaluation_batch)?; + if direct_plan.scale != 1.0 { + for value in &mut values { + *value *= direct_plan.scale; + } + } + let total_s = total_start.elapsed().as_secs_f64(); + profile.orchestration_s += (total_s - profile.total_s).max(0.0); + profile.total_s = total_s; + Ok(Some((values, profile))) + } + + pub(super) fn run_resolved_f64( + &mut self, + batch: &[Vec<[f64; 4]>], + selected_helicity_ids: Option<&BTreeSet>, + selected_color_ids: Option<&BTreeSet>, + ) -> RusticolResult<(ResolvedValues, RuntimeProfile)> { + if self.has_safe_singleton_helicity_execution(selected_helicity_ids, selected_color_ids)? { + return self.run_resolved_f64_with_helicity_recurrence( + batch, + selected_helicity_ids, + selected_color_ids, + None, + ); + } + if self.use_helicity_sum_runtime_for_selection(selected_helicity_ids) + && let Some(sum_runtime) = self.helicity_sum_runtime.as_mut() + { + return sum_runtime.run_resolved_f64(batch, selected_helicity_ids, selected_color_ids); + } + if self.color_topology_replay_enabled { + return self.run_resolved_f64_with_color_topology_replay( + batch, + selected_helicity_ids, + selected_color_ids, + ); + } + if self.lc_topology_replay_enabled { + // A target-flow helicity generally maps to a different source + // helicity in the materialized topology. Always compose runtime + // helicity selection through the replay plan, even when a nested + // selector lane can evaluate the primary topology directly. + return self.run_resolved_f64_with_lc_topology_replay( + batch, + selected_helicity_ids, + selected_color_ids, + ); + } + if self.has_compiled_helicity_execution_plan() { + return self.run_resolved_f64_with_helicity_recurrence( + batch, + selected_helicity_ids, + selected_color_ids, + None, + ); + } + let physics = self.physics.clone().ok_or_else(|| { + RusticolError::invalid_argument( + "schema-v3 artifact is missing resolved physics metadata; regenerate it with pyAmpliCol 0.1.0 or newer", + ) + })?; + let selected_color_lane = if selected_color_ids.is_some_and(|ids| ids.len() == 1) { + self.lc_materialized_sector_ids_for_color_ids( + physics.as_ref(), + selected_color_ids.expect("singleton selection checked"), + )? + .into_iter() + .next() + } else { + None + }; + if let Some(sector_id) = selected_color_lane + && let Some(selector_runtime) = self.color_selector_runtimes.get_mut(§or_id) + { + return selector_runtime.run_resolved_f64( + batch, + selected_helicity_ids, + selected_color_ids, + ); + } + let (_summed, mut profile) = self.run_f64_materialized(batch)?; + let resolved_reduction_start = Instant::now(); + let resolved = self + .amplitude_stage + .as_mut() + .expect("generic amplitude stage checked") + .reduce_scratch_f64_resolved( + batch.len(), + &physics, + self.normalization_factor, + selected_helicity_ids, + selected_color_ids, + )?; + let resolved_reduction_s = resolved_reduction_start.elapsed().as_secs_f64(); + profile.reduction_s += resolved_reduction_s; + profile.resolved_reduction_materialization_s += resolved_reduction_s; + profile.resolved_materialized_component_count += resolved.values.len() as u64; + Ok((resolved, profile)) + } + + fn run_resolved_f64_with_lc_topology_replay( + &mut self, + batch: &[Vec<[f64; 4]>], + selected_helicity_ids: Option<&BTreeSet>, + selected_color_ids: Option<&BTreeSet>, + ) -> RusticolResult<(ResolvedValues, RuntimeProfile)> { + let physics = self.physics.clone().ok_or_else(|| { + RusticolError::invalid_argument( + "schema-v3 artifact is missing resolved physics metadata; regenerate it with pyAmpliCol 0.1.0 or newer", + ) + })?; + let replay_plan = self.cached_lc_resolved_replay_plan(&physics)?; + let selection = self.cached_lc_resolved_replay_selection( + &physics, + replay_plan.as_ref(), + selected_helicity_ids, + selected_color_ids, + )?; + let total_start = Instant::now(); + let initial_orchestration_start = Instant::now(); + let n_points = batch.len(); + let component_count = selection.helicity_indices.len() * selection.color_indices.len(); + let mut full_values = vec![0.0; n_points * component_count]; + let mut sector_profiles = Vec::new(); + let mut replay_reduction_elapsed = Duration::ZERO; + let mappings = self.lc_topology_replay_mappings.clone(); + let mappings_per_chunk = replay_mappings_per_expanded_batch(n_points); + let mut replay_orchestration_elapsed = initial_orchestration_start.elapsed(); + for source_group in &selection.source_groups { + let group_orchestration_start = Instant::now(); + let materialized_sector_ids = + self.lc_materialized_sector_ids_for_color_ids(&physics, &source_group.color_ids)?; + replay_orchestration_elapsed += group_orchestration_start.elapsed(); + for chunk_start in (0..source_group.mapping_indices.len()).step_by(mappings_per_chunk) { + let chunk_orchestration_start = Instant::now(); + let chunk_end = usize::min( + chunk_start + mappings_per_chunk, + source_group.mapping_indices.len(), + ); + let mapping_chunk = source_group.mapping_indices[chunk_start..chunk_end] + .iter() + .map(|mapping_index| mappings[*mapping_index].clone()) + .collect::>(); + let entry_chunk = &source_group.entries[chunk_start..chunk_end]; + let expanded_batch = if mapping_chunk.len() == 1 && mapping_chunk[0].is_empty() { + None + } else { + Some(apply_lc_topology_label_permutations( + batch, + self.external_count, + &mapping_chunk, + )?) + }; + let evaluation_batch = expanded_batch.as_deref().unwrap_or(batch); + let selected_lane_id = (materialized_sector_ids.len() == 1) + .then(|| materialized_sector_ids.iter().next().copied()) + .flatten(); + replay_orchestration_elapsed += chunk_orchestration_start.elapsed(); + let (materialized, sector_profile) = if self.has_compiled_helicity_execution_plan() + && selected_helicity_ids.is_some() + { + self.run_resolved_f64_with_helicity_recurrence( + evaluation_batch, + Some(&source_group.helicity_ids), + Some(&source_group.color_ids), + Some(&materialized_sector_ids), + )? + } else if let Some(selector_runtime) = selected_lane_id + .and_then(|sector_id| self.color_selector_runtimes.get_mut(§or_id)) + { + selector_runtime.run_resolved_f64( + evaluation_batch, + Some(&source_group.helicity_ids), + Some(&source_group.color_ids), + )? + } else { + let (_expanded_totals, mut sector_profile) = self + .run_f64_materialized_selected( + evaluation_batch, + Some(&materialized_sector_ids), + )?; + let resolved_reduction_start = Instant::now(); + let materialized = self + .amplitude_stage + .as_mut() + .expect("generic amplitude stage checked") + .reduce_scratch_f64_resolved( + evaluation_batch.len(), + &physics, + self.normalization_factor, + Some(&source_group.helicity_ids), + Some(&source_group.color_ids), + )?; + let resolved_reduction_s = resolved_reduction_start.elapsed().as_secs_f64(); + sector_profile.reduction_s += resolved_reduction_s; + sector_profile.resolved_reduction_materialization_s += resolved_reduction_s; + sector_profile.resolved_materialized_component_count += + materialized.values.len() as u64; + (materialized, sector_profile) + }; + let reduction_start = Instant::now(); + accumulate_selected_lc_replay_resolved_f64( + &mut full_values, + n_points, + &materialized, + entry_chunk, + source_group.source_component_count, + component_count, + )?; + replay_reduction_elapsed += reduction_start.elapsed(); + let post_reduction_orchestration_start = Instant::now(); + sector_profiles.push(sector_profile); + replay_orchestration_elapsed += post_reduction_orchestration_start.elapsed(); + } + } + let profile_fold_start = Instant::now(); + let mut profile = RuntimeProfile::default(); + for sector_profile in §or_profiles { + profile.add_sector(sector_profile); + } + replay_orchestration_elapsed += profile_fold_start.elapsed(); + profile.orchestration_s += profile_duration_seconds(replay_orchestration_elapsed); + profile.reduction_s += profile_duration_seconds(replay_reduction_elapsed); + profile.total_s = total_start.elapsed().as_secs_f64(); + Ok(( + ResolvedValues { + values: full_values, + point_count: n_points, + helicity_indices: selection.helicity_indices.clone(), + color_indices: selection.color_indices.clone(), + }, + profile, + )) + } + + pub(super) fn run_f64_selected( + &mut self, + batch: &[Vec<[f64; 4]>], + selected_color_sector_ids: Option<&BTreeSet>, + ) -> RusticolResult<(Vec, RuntimeProfile)> { + if self.has_compiled_symmetric_group_diagnostic() && selected_color_sector_ids.is_some() { + return Err(self.compiled_symmetric_group_unsupported("runtime selectors")); + } + if let Some(sum_runtime) = self.helicity_sum_runtime.as_mut() { + return sum_runtime.run_f64_selected(batch, selected_color_sector_ids); + } + if self.color_topology_replay_enabled { + if selected_color_sector_ids.is_some() { + return Err(RusticolError::selector( + "contracted-color topology replay does not expose physical sector selection", + )); + } + return self.run_f64_with_color_topology_replay(batch); + } + if self.lc_topology_replay_enabled { + if selected_color_sector_ids.is_some() { + return Err(RusticolError::invalid_argument( + "LC color-sector runtime selection is not available for topology-replay artifacts", + )); + } + return self.run_f64_with_lc_topology_replay(batch); + } + let Some(selected) = selected_color_sector_ids else { + let previous = self.set_lc_sector_selector(None); + let result = self.run_f64_materialized_selected(batch, None); + self.restore_lc_sector_selector(previous); + return result; + }; + if selected.is_empty() { + return Err(RusticolError::invalid_argument( + "LC color-sector runtime selection requires at least one sector id", + )); + } + if selected.len() == 1 + && let Some(selector_runtime) = selected + .iter() + .next() + .and_then(|sector_id| self.color_selector_runtimes.get_mut(sector_id)) + { + return selector_runtime.run_f64_materialized(batch); + } + if !self + .model_parameter_name_to_index + .contains_key(LC_SECTOR_SELECTOR_PARAMETER) + { + return self.run_f64_materialized_selected(batch, Some(selected)); + } + let total_start = Instant::now(); + let n_points = batch.len(); + let mut values = vec![0.0; n_points]; + let mut sector_profiles = Vec::with_capacity(selected.len()); + let previous = self.set_lc_sector_selector(None); + let result = (|| { + for sector_id in selected { + self.set_lc_sector_selector(Some(*sector_id)); + let mut singleton = BTreeSet::new(); + singleton.insert(*sector_id); + let (sector_values, sector_profile) = + self.run_f64_materialized_selected(batch, Some(&singleton))?; + for (value, sector_value) in values.iter_mut().zip(sector_values) { + *value += sector_value; + } + sector_profiles.push(sector_profile); + } + let mut profile = RuntimeProfile::default(); + for sector_profile in §or_profiles { + profile.add_sector(sector_profile); + } + profile.total_s = total_start.elapsed().as_secs_f64(); + Ok((values, profile)) + })(); + self.restore_lc_sector_selector(previous); + result + } + + #[cfg(feature = "symbolica-runtime")] + pub(super) fn run_double( + &mut self, + batch: &[Vec<[DoubleFloat; 4]>], + ) -> RusticolResult<(Vec, RuntimeProfile)> { + if self.has_compiled_symmetric_group_diagnostic() { + return Err( + self.compiled_symmetric_group_unsupported("exact/high-precision evaluation") + ); + } + if let Some(sum_runtime) = self.helicity_sum_runtime.as_mut() { + return sum_runtime.run_double(batch); + } + if self.color_topology_replay_enabled { + return Err(RusticolError::invalid_argument( + "compiled/eager contracted-color topology replay currently exposes f64; use the exact recurrence lane for precision certification", + )); + } + if self.lc_topology_replay_enabled { + return self.run_generic_with_lc_topology_replay(batch, None); + } + if self.has_compiled_helicity_execution_plan() { + return self.run_generic_with_helicity_recurrence(batch, None); + } + self.run_generic_materialized(batch, None) + } + + #[cfg(feature = "symbolica-runtime")] + pub(super) fn run_float( + &mut self, + batch: &[Vec<[Float; 4]>], + binary_precision: u32, + ) -> RusticolResult<(Vec, RuntimeProfile)> { + if self.has_compiled_symmetric_group_diagnostic() { + return Err( + self.compiled_symmetric_group_unsupported("exact/high-precision evaluation") + ); + } + if let Some(sum_runtime) = self.helicity_sum_runtime.as_mut() { + return sum_runtime.run_float(batch, binary_precision); + } + if self.color_topology_replay_enabled { + return Err(RusticolError::invalid_argument( + "compiled/eager contracted-color topology replay currently exposes f64; use the exact recurrence lane for precision certification", + )); + } + if self.lc_topology_replay_enabled { + return self.run_generic_with_lc_topology_replay(batch, Some(binary_precision)); + } + if self.has_compiled_helicity_execution_plan() { + return self.run_generic_with_helicity_recurrence(batch, Some(binary_precision)); + } + self.run_generic_materialized(batch, Some(binary_precision)) + } + + pub(super) fn run_f64_with_lc_topology_replay( + &mut self, + batch: &[Vec<[f64; 4]>], + ) -> RusticolResult<(Vec, RuntimeProfile)> { + let (resolved, profile) = + self.run_resolved_f64_with_lc_topology_replay(batch, None, None)?; + let mut profile = profile; + let materialization_start = Instant::now(); + let values = resolved_f64_totals(&resolved)?; + profile.total_materialization_s += materialization_start.elapsed().as_secs_f64(); + profile.total_materialized_value_count += values.len() as u64; + Ok((values, profile)) + } + + pub(super) fn run_f64_with_color_topology_replay( + &mut self, + batch: &[Vec<[f64; 4]>], + ) -> RusticolResult<(Vec, RuntimeProfile)> { + let total_start = Instant::now(); + let view = F64MomentumBatchView::from_nested(batch, self.external_count)?; + let mut values = vec![0.0; batch.len()]; + self.run_f64_with_color_topology_replay_into_unprofiled(view, &mut values)?; + let total_s = total_start.elapsed().as_secs_f64(); + Ok(( + values, + RuntimeProfile { + total_s, + total_materialized_value_count: batch.len() as u64, + ..RuntimeProfile::default() + }, + )) + } + + fn run_resolved_f64_with_color_topology_replay( + &mut self, + batch: &[Vec<[f64; 4]>], + selected_helicity_ids: Option<&BTreeSet>, + selected_color_ids: Option<&BTreeSet>, + ) -> RusticolResult<(ResolvedValues, RuntimeProfile)> { + let total_start = Instant::now(); + let view = F64MomentumBatchView::from_nested(batch, self.external_count)?; + let resolved = self.run_resolved_f64_with_color_topology_replay_unprofiled( + view, + selected_helicity_ids, + selected_color_ids, + )?; + let total_s = total_start.elapsed().as_secs_f64(); + Ok(( + resolved, + RuntimeProfile { + total_s, + ..RuntimeProfile::default() + }, + )) + } + + pub(super) fn run_f64_materialized( + &mut self, + batch: &[Vec<[f64; 4]>], + ) -> RusticolResult<(Vec, RuntimeProfile)> { + self.run_f64_materialized_selected(batch, None) + } + + pub(super) fn run_f64_materialized_selected( + &mut self, + batch: &[Vec<[f64; 4]>], + selected_color_sector_ids: Option<&BTreeSet>, + ) -> RusticolResult<(Vec, RuntimeProfile)> { + if self.stages.is_none() || self.amplitude_stage.is_none() { + return Err(self.execution_unavailable_error()); + } + let total_start = Instant::now(); + let n_points = batch.len(); + let state_prepare_start = Instant::now(); + let color_schedule = selected_color_sector_ids + .map(|sector_ids| self.compiled_color_selector_schedule(sector_ids)) + .transpose()? + .flatten(); + let state_len = n_points * self.parameter_count; + let state_capacity = self.state_scratch_f64.capacity(); + self.state_scratch_f64.resize(state_len, c64(0.0, 0.0)); + let state_prepare_elapsed = state_prepare_start.elapsed(); + let mut scratch_reallocation_count = + u64::from(self.state_scratch_f64.capacity() != state_capacity); + let clear_start = Instant::now(); + let mut state_clear_component_count = 0; + if self.state_scratch_f64_requires_clear { + self.state_scratch_f64.fill(c64(0.0, 0.0)); + state_clear_component_count = state_len as u64; + } + let state_clear_elapsed = clear_start.elapsed(); + // Keep this conservative while execution is in progress: an evaluator + // error can leave a partially written state. A complete schedule clears + // the flag below; selector-pruned schedules deliberately leave it set. + self.state_scratch_f64_requires_clear = true; + let state = &mut self.state_scratch_f64; + let sources = &self.sources; + let momentum_slots = &self.momentum_slots; + let external_count = self.external_count; + let external_is_initial = &self.external_is_initial; + let particle_masses = &self.particle_masses; + let value_parameter_count = self.value_parameter_count; + let model_parameter_start = self.value_parameter_count + self.momentum_parameter_count; + let model_parameter_values = &self.model_parameter_values_f64; + let source_start = Instant::now(); + for (row, point) in batch.iter().enumerate() { + let row_state = + &mut state[row * self.parameter_count..(row + 1) * self.parameter_count]; + Self::fill_sources_row(sources, external_count, particle_masses, row_state, point)?; + } + let source_fill_elapsed = source_start.elapsed(); + + let momentum_start = Instant::now(); + for (row, point) in batch.iter().enumerate() { + let row_state = + &mut state[row * self.parameter_count..(row + 1) * self.parameter_count]; + Self::fill_momenta_row( + momentum_slots, + value_parameter_count, + external_count, + external_is_initial, + row_state, + point, + )?; + } + let momentum_setup_elapsed = momentum_start.elapsed(); + + let model_parameter_start_time = Instant::now(); + for row in 0..n_points { + let row_state = + &mut state[row * self.parameter_count..(row + 1) * self.parameter_count]; + Self::fill_model_parameters_row( + model_parameter_start, + model_parameter_values, + row_state, + )?; + } + let model_parameter_setup_elapsed = model_parameter_start_time.elapsed(); + let mut stage_profiles = Vec::new(); + let mut stage_input_pack_by_stage_s = Vec::new(); + let mut stage_leaf_input_pack_by_stage_s = Vec::new(); + let mut stage_evaluator_call_by_stage_s = Vec::new(); + let mut stage_backend_call_by_stage_s = Vec::new(); + let mut stage_evaluator_output_gather_by_stage_s = Vec::new(); + let mut stage_output_assign_by_stage_s = Vec::new(); + for (stage_index, stage) in self + .stages + .as_mut() + .expect("generic stages checked") + .iter_mut() + .enumerate() + { + let stage_profile = if let Some(schedule) = color_schedule.as_ref() { + stage.evaluate_active_chunks_f64_into_state_profile( + n_points, + self.parameter_count, + state.as_mut_slice(), + &schedule.active_stage_chunk_indices[stage_index], + )? + } else { + stage.evaluate_f64_into_state_profile( + n_points, + self.parameter_count, + state.as_mut_slice(), + )? + }; + stage_input_pack_by_stage_s.push(stage_profile.input_pack_s); + stage_leaf_input_pack_by_stage_s.push(stage_profile.evaluator.leaf_input_pack_s); + stage_evaluator_call_by_stage_s.push(stage_profile.evaluator.legacy_evaluator_call_s); + stage_backend_call_by_stage_s.push(stage_profile.evaluator.evaluator_call_s); + stage_evaluator_output_gather_by_stage_s.push(stage_profile.evaluator.output_gather_s); + stage_output_assign_by_stage_s.push(stage_profile.output_assign_s); + stage_profiles.push(stage_profile); + } + + let amplitude_profile = if let Some(schedule) = color_schedule.as_ref() { + self.amplitude_stage + .as_mut() + .expect("generic amplitude stage checked") + .evaluate_active_chunks_f64_into_scratch_profile( + n_points, + state.as_slice(), + &schedule.active_amplitude_chunk_indices, + )? + } else { + self.amplitude_stage + .as_mut() + .expect("generic amplitude stage checked") + .evaluate_f64_into_scratch_profile(n_points, state.as_slice())? + }; + let amplitude_input_pack_s = amplitude_profile.input_pack_s; + let amplitude_evaluator_call_s = + amplitude_profile.evaluator.legacy_evaluator_call_s + amplitude_profile.output_remap_s; + let amplitude_evaluator_s = amplitude_input_pack_s + amplitude_evaluator_call_s; + + let reduction_start = Instant::now(); + let values_capacity = self.values_scratch_f64.capacity(); + self.amplitude_stage + .as_mut() + .expect("generic amplitude stage checked") + .reduce_scratch_f64_into_selected( + n_points, + &mut self.values_scratch_f64, + selected_color_sector_ids, + )?; + for value in &mut self.values_scratch_f64 { + *value *= self.normalization_factor; + } + let reduction_s = reduction_start.elapsed().as_secs_f64(); + scratch_reallocation_count += + u64::from(self.values_scratch_f64.capacity() != values_capacity); + self.state_scratch_f64_requires_clear = color_schedule.is_some(); + let stage_input_pack_s = stage_input_pack_by_stage_s.iter().sum::(); + let stage_evaluator_call_s = stage_evaluator_call_by_stage_s.iter().sum::(); + let stage_evaluator_s = stage_input_pack_s + stage_evaluator_call_s; + let output_assign_s = stage_output_assign_by_stage_s.iter().sum::(); + let final_output_copy_start = Instant::now(); + let values = self.values_scratch_f64.clone(); + let final_output_copy_s = final_output_copy_start.elapsed().as_secs_f64(); + Ok(( + values, + RuntimeProfile { + source_fill_s: profile_duration_seconds(source_fill_elapsed), + state_prepare_s: profile_duration_seconds(state_prepare_elapsed), + state_clear_s: profile_duration_seconds(state_clear_elapsed), + momentum_input_setup_s: profile_duration_seconds(momentum_setup_elapsed), + momentum_setup_s: profile_duration_seconds( + momentum_setup_elapsed + model_parameter_setup_elapsed, + ), + model_parameter_setup_s: profile_duration_seconds(model_parameter_setup_elapsed), + stage_input_pack_s, + stage_leaf_input_pack_s: stage_profiles + .iter() + .map(|profile| profile.evaluator.leaf_input_pack_s) + .sum(), + stage_evaluator_call_s, + stage_backend_call_s: stage_profiles + .iter() + .map(|profile| profile.evaluator.evaluator_call_s) + .sum(), + stage_evaluator_output_gather_s: stage_profiles + .iter() + .map(|profile| profile.evaluator.output_gather_s) + .sum(), + stage_evaluator_s, + output_assign_s, + amplitude_input_pack_s, + amplitude_leaf_input_pack_s: amplitude_profile.evaluator.leaf_input_pack_s, + amplitude_evaluator_call_s, + amplitude_backend_call_s: amplitude_profile.evaluator.evaluator_call_s, + amplitude_evaluator_output_gather_s: amplitude_profile.evaluator.output_gather_s, + amplitude_output_remap_s: amplitude_profile.output_remap_s, + amplitude_evaluator_s, + reduction_s, + total_s: total_start.elapsed().as_secs_f64(), + stage_input_pack_by_stage_s, + stage_leaf_input_pack_by_stage_s, + stage_evaluator_call_by_stage_s, + stage_backend_call_by_stage_s, + stage_evaluator_output_gather_by_stage_s, + stage_output_assign_by_stage_s, + state_component_count: state_len as u64, + state_clear_component_count, + source_component_count: (n_points * self.source_count) as u64, + momentum_component_count: (n_points * self.momentum_parameter_count) as u64, + model_parameter_component_count: (n_points * model_parameter_values.len()) as u64, + stage_input_copy_component_count: stage_profiles + .iter() + .map(|profile| profile.input_copy_component_count) + .sum(), + stage_leaf_input_copy_component_count: stage_profiles + .iter() + .map(|profile| profile.evaluator.leaf_input_copy_component_count) + .sum(), + stage_evaluator_output_gather_component_count: stage_profiles + .iter() + .map(|profile| profile.evaluator.output_gather_component_count) + .sum(), + stage_output_assign_component_count: stage_profiles + .iter() + .map(|profile| profile.output_assign_component_count) + .sum(), + amplitude_input_copy_component_count: amplitude_profile.input_copy_component_count, + amplitude_leaf_input_copy_component_count: amplitude_profile + .evaluator + .leaf_input_copy_component_count, + amplitude_evaluator_output_gather_component_count: amplitude_profile + .evaluator + .output_gather_component_count, + amplitude_output_remap_component_count: amplitude_profile + .output_remap_component_count, + evaluator_backend_call_count: stage_profiles + .iter() + .map(|profile| profile.evaluator.backend_call_count) + .sum::() + + amplitude_profile.evaluator.backend_call_count, + reduction_input_component_count: (n_points + * self + .amplitude_stage + .as_ref() + .expect("generic amplitude stage checked") + .output_length) as u64, + total_materialized_value_count: n_points as u64, + final_output_copy_s, + final_output_copy_value_count: self.values_scratch_f64.len() as u64, + scratch_reallocation_count: scratch_reallocation_count + + stage_profiles + .iter() + .map(|profile| { + profile.scratch_reallocation_count + + profile.evaluator.scratch_reallocation_count + }) + .sum::() + + amplitude_profile.scratch_reallocation_count + + amplitude_profile.evaluator.scratch_reallocation_count, + ..RuntimeProfile::default() + }, + )) + } + + #[cfg(feature = "symbolica-runtime")] + pub(super) fn run_generic_with_lc_topology_replay( + &mut self, + batch: &[Vec<[T; 4]>], + binary_precision: Option, + ) -> RusticolResult<(Vec, RuntimeProfile)> + where + T: RusticolHighPrecisionNumber, + Complex: Real + EvaluationDomain, + { + let (resolved, profile) = + self.run_resolved_generic_with_lc_topology_replay(batch, binary_precision, None, None)?; + let component_count = resolved.helicity_indices.len() * resolved.color_indices.len(); + let values = resolved + .values + .chunks_exact(component_count) + .map(|components| { + components + .iter() + .cloned() + .fold(T::new_zero(), |sum, value| sum + value) + }) + .collect(); + Ok((values, profile)) + } + + #[cfg(feature = "symbolica-runtime")] + fn run_generic_with_helicity_recurrence( + &mut self, + batch: &[Vec<[T; 4]>], + binary_precision: Option, + ) -> RusticolResult<(Vec, RuntimeProfile)> + where + T: RusticolHighPrecisionNumber, + Complex: Real + EvaluationDomain, + { + let (resolved, profile) = self.run_resolved_generic_with_helicity_recurrence( + batch, + binary_precision, + None, + None, + None, + )?; + let component_count = resolved.helicity_indices.len() * resolved.color_indices.len(); + let values = resolved + .values + .chunks_exact(component_count) + .map(|components| { + components + .iter() + .cloned() + .fold(T::new_zero(), |sum, value| sum + value) + }) + .collect(); + Ok((values, profile)) + } + + #[cfg(feature = "symbolica-runtime")] + pub(super) fn run_generic_materialized( + &mut self, + batch: &[Vec<[T; 4]>], + binary_precision: Option, + ) -> RusticolResult<(Vec, RuntimeProfile)> + where + T: RusticolHighPrecisionNumber, + Complex: Real + EvaluationDomain, + { + if self.stages.is_none() || self.amplitude_stage.is_none() { + return Err(self.execution_unavailable_error()); + } + let total_start = Instant::now(); + let n_points = batch.len(); + let mut state = vec![complex_zero::(); n_points * self.parameter_count]; + let sources = &self.sources; + let momentum_slots = &self.momentum_slots; + let external_count = self.external_count; + let external_is_initial = &self.external_is_initial; + let particle_masses = &self.particle_masses; + let value_parameter_count = self.value_parameter_count; + let model_parameter_start = self.value_parameter_count + self.momentum_parameter_count; + let model_parameter_values = &self.model_parameter_values_f64; + + let source_start = Instant::now(); + for (row, point) in batch.iter().enumerate() { + let row_state = + &mut state[row * self.parameter_count..(row + 1) * self.parameter_count]; + Self::fill_sources_row_generic( + sources, + external_count, + particle_masses, + row_state, + point, + )?; + } + let source_fill_elapsed = source_start.elapsed(); + + let momentum_start = Instant::now(); + for (row, point) in batch.iter().enumerate() { + let row_state = + &mut state[row * self.parameter_count..(row + 1) * self.parameter_count]; + Self::fill_momenta_row_generic( + momentum_slots, + value_parameter_count, + external_count, + external_is_initial, + row_state, + point, + )?; + } + let momentum_setup_elapsed = momentum_start.elapsed(); + + let model_parameter_start_time = Instant::now(); + for row in 0..n_points { + let row_state = + &mut state[row * self.parameter_count..(row + 1) * self.parameter_count]; + Self::fill_model_parameters_row_generic( + model_parameter_start, + model_parameter_values, + row_state, + )?; + } + let model_parameter_setup_elapsed = model_parameter_start_time.elapsed(); + + let mut stage_input_pack_s = 0.0; + let mut stage_evaluator_call_s = 0.0; + let mut stage_evaluator_s = 0.0; + let mut output_assign_s = 0.0; + let mut stage_input_pack_by_stage_s = Vec::new(); + let mut stage_evaluator_call_by_stage_s = Vec::new(); + let mut stage_output_assign_by_stage_s = Vec::new(); + for stage in self.stages.as_mut().expect("generic stages checked") { + let (pack_s, eval_s, assign_s) = stage.evaluate_generic_into_state( + n_points, + self.parameter_count, + state.as_mut_slice(), + binary_precision, + )?; + stage_input_pack_s += pack_s; + stage_evaluator_call_s += eval_s; + stage_evaluator_s += pack_s + eval_s; + output_assign_s += assign_s; + stage_input_pack_by_stage_s.push(pack_s); + stage_evaluator_call_by_stage_s.push(eval_s); + stage_output_assign_by_stage_s.push(assign_s); + } + + let (raw_sums, amplitude_input_pack_s, amplitude_evaluator_call_s) = self + .amplitude_stage + .as_mut() + .expect("generic amplitude stage checked") + .evaluate_raw_sums_generic(n_points, state.as_slice(), binary_precision)?; + let amplitude_evaluator_s = amplitude_input_pack_s + amplitude_evaluator_call_s; + + let reduction_start = Instant::now(); + let factor = T::from(self.normalization_factor); + let values = raw_sums + .into_iter() + .map(|value| value * factor.clone()) + .collect::>(); + let reduction_s = reduction_start.elapsed().as_secs_f64(); + Ok(( + values, + RuntimeProfile { + source_fill_s: profile_duration_seconds(source_fill_elapsed), + momentum_input_setup_s: profile_duration_seconds(momentum_setup_elapsed), + momentum_setup_s: profile_duration_seconds( + momentum_setup_elapsed + model_parameter_setup_elapsed, + ), + model_parameter_setup_s: profile_duration_seconds(model_parameter_setup_elapsed), + stage_input_pack_s, + stage_evaluator_call_s, + stage_evaluator_s, + output_assign_s, + amplitude_input_pack_s, + amplitude_evaluator_call_s, + amplitude_evaluator_s, + reduction_s, + total_s: total_start.elapsed().as_secs_f64(), + stage_input_pack_by_stage_s, + stage_evaluator_call_by_stage_s, + stage_output_assign_by_stage_s, + ..RuntimeProfile::default() + }, + )) + } + + #[cfg(feature = "symbolica-runtime")] + pub(super) fn run_resolved_generic( + &mut self, + batch: &[Vec<[T; 4]>], + binary_precision: Option, + selected_helicity_ids: Option<&BTreeSet>, + selected_color_ids: Option<&BTreeSet>, + ) -> RusticolResult<(ResolvedValues, RuntimeProfile)> + where + T: RusticolHighPrecisionNumber, + Complex: Real + EvaluationDomain, + { + if self.has_compiled_symmetric_group_diagnostic() { + return Err(self + .compiled_symmetric_group_unsupported("resolved exact/high-precision evaluation")); + } + if self.has_safe_singleton_helicity_execution(selected_helicity_ids, selected_color_ids)? { + return self.run_resolved_generic_with_helicity_recurrence( + batch, + binary_precision, + selected_helicity_ids, + selected_color_ids, + None, + ); + } + if self.use_helicity_sum_runtime_for_selection(selected_helicity_ids) + && let Some(sum_runtime) = self.helicity_sum_runtime.as_mut() + { + return sum_runtime.run_resolved_generic( + batch, + binary_precision, + selected_helicity_ids, + selected_color_ids, + ); + } + if self.lc_topology_replay_enabled { + return self.run_resolved_generic_with_lc_topology_replay( + batch, + binary_precision, + selected_helicity_ids, + selected_color_ids, + ); + } + if self.has_compiled_helicity_execution_plan() { + return self.run_resolved_generic_with_helicity_recurrence( + batch, + binary_precision, + selected_helicity_ids, + selected_color_ids, + None, + ); + } + self.run_resolved_generic_materialized( + batch, + binary_precision, + selected_helicity_ids, + selected_color_ids, + ) + } + + #[cfg(feature = "symbolica-runtime")] + fn run_resolved_generic_with_lc_topology_replay( + &mut self, + batch: &[Vec<[T; 4]>], + binary_precision: Option, + selected_helicity_ids: Option<&BTreeSet>, + selected_color_ids: Option<&BTreeSet>, + ) -> RusticolResult<(ResolvedValues, RuntimeProfile)> + where + T: RusticolHighPrecisionNumber, + Complex: Real + EvaluationDomain, + { + let physics = self.physics.clone().ok_or_else(|| { + RusticolError::invalid_argument( + "schema-v3 artifact is missing resolved physics metadata; regenerate it with pyAmpliCol 0.1.0 or newer", + ) + })?; + let replay_plan = self.cached_lc_resolved_replay_plan(&physics)?; + let selection = self.cached_lc_resolved_replay_selection( + &physics, + replay_plan.as_ref(), + selected_helicity_ids, + selected_color_ids, + )?; + let total_start = Instant::now(); + let initial_orchestration_start = Instant::now(); + let n_points = batch.len(); + let component_count = selection.helicity_indices.len() * selection.color_indices.len(); + let mut full_values = vec![T::new_zero(); n_points * component_count]; + let mut sector_profiles = Vec::new(); + let mut replay_reduction_elapsed = Duration::ZERO; + let mappings = self.lc_topology_replay_mappings.clone(); + let mappings_per_chunk = replay_mappings_per_expanded_batch(n_points); + let mut replay_orchestration_elapsed = initial_orchestration_start.elapsed(); + for source_group in &selection.source_groups { + for chunk_start in (0..source_group.mapping_indices.len()).step_by(mappings_per_chunk) { + let chunk_orchestration_start = Instant::now(); + let chunk_end = usize::min( + chunk_start + mappings_per_chunk, + source_group.mapping_indices.len(), + ); + let mapping_chunk = source_group.mapping_indices[chunk_start..chunk_end] + .iter() + .map(|mapping_index| mappings[*mapping_index].clone()) + .collect::>(); + let entry_chunk = &source_group.entries[chunk_start..chunk_end]; + let expanded_batch = if mapping_chunk.len() == 1 && mapping_chunk[0].is_empty() { + None + } else { + Some(apply_lc_topology_label_permutations_generic( + batch, + self.external_count, + &mapping_chunk, + )?) + }; + let evaluation_batch = expanded_batch.as_deref().unwrap_or(batch); + replay_orchestration_elapsed += chunk_orchestration_start.elapsed(); + let (materialized, sector_profile) = if self.has_compiled_helicity_execution_plan() + && selected_helicity_ids.is_some() + { + self.run_resolved_generic_with_helicity_recurrence( + evaluation_batch, + binary_precision, + Some(&source_group.helicity_ids), + Some(&source_group.color_ids), + source_group.materialized_sector_ids.as_ref(), + )? + } else { + self.run_resolved_generic_materialized( + evaluation_batch, + binary_precision, + Some(&source_group.helicity_ids), + Some(&source_group.color_ids), + )? + }; + let reduction_start = Instant::now(); + accumulate_selected_lc_replay_resolved_generic( + &mut full_values, + n_points, + &materialized, + entry_chunk, + source_group.source_component_count, + component_count, + )?; + replay_reduction_elapsed += reduction_start.elapsed(); + let post_reduction_orchestration_start = Instant::now(); + sector_profiles.push(sector_profile); + replay_orchestration_elapsed += post_reduction_orchestration_start.elapsed(); + } + } + let profile_fold_start = Instant::now(); + let mut profile = RuntimeProfile::default(); + for sector_profile in §or_profiles { + profile.add_sector(sector_profile); + } + replay_orchestration_elapsed += profile_fold_start.elapsed(); + profile.orchestration_s += profile_duration_seconds(replay_orchestration_elapsed); + profile.reduction_s += profile_duration_seconds(replay_reduction_elapsed); + profile.total_s = total_start.elapsed().as_secs_f64(); + Ok(( + ResolvedValues { + values: full_values, + point_count: n_points, + helicity_indices: selection.helicity_indices.clone(), + color_indices: selection.color_indices.clone(), + }, + profile, + )) + } + + #[cfg(feature = "symbolica-runtime")] + fn run_resolved_generic_materialized( + &mut self, + batch: &[Vec<[T; 4]>], + binary_precision: Option, + selected_helicity_ids: Option<&BTreeSet>, + selected_color_ids: Option<&BTreeSet>, + ) -> RusticolResult<(ResolvedValues, RuntimeProfile)> + where + T: RusticolHighPrecisionNumber, + Complex: Real + EvaluationDomain, + { + if self.stages.is_none() || self.amplitude_stage.is_none() { + return Err(self.execution_unavailable_error()); + } + let physics = self.physics.clone().ok_or_else(|| { + RusticolError::invalid_argument( + "schema-v3 artifact is missing resolved physics metadata; regenerate it with pyAmpliCol 0.1.0 or newer", + ) + })?; + let total_start = Instant::now(); + let n_points = batch.len(); + let mut state = vec![complex_zero::(); n_points * self.parameter_count]; + let sources = &self.sources; + let momentum_slots = &self.momentum_slots; + let external_count = self.external_count; + let external_is_initial = &self.external_is_initial; + let particle_masses = &self.particle_masses; + let value_parameter_count = self.value_parameter_count; + let model_parameter_start = self.value_parameter_count + self.momentum_parameter_count; + let model_parameter_values = &self.model_parameter_values_f64; + + let source_start = Instant::now(); + for (row, point) in batch.iter().enumerate() { + let row_state = + &mut state[row * self.parameter_count..(row + 1) * self.parameter_count]; + Self::fill_sources_row_generic( + sources, + external_count, + particle_masses, + row_state, + point, + )?; + } + let source_fill_elapsed = source_start.elapsed(); + + let momentum_start = Instant::now(); + for (row, point) in batch.iter().enumerate() { + let row_state = + &mut state[row * self.parameter_count..(row + 1) * self.parameter_count]; + Self::fill_momenta_row_generic( + momentum_slots, + value_parameter_count, + external_count, + external_is_initial, + row_state, + point, + )?; + } + let momentum_setup_elapsed = momentum_start.elapsed(); + + let model_parameter_start_time = Instant::now(); + for row in 0..n_points { + let row_state = + &mut state[row * self.parameter_count..(row + 1) * self.parameter_count]; + Self::fill_model_parameters_row_generic( + model_parameter_start, + model_parameter_values, + row_state, + )?; + } + let model_parameter_setup_elapsed = model_parameter_start_time.elapsed(); + + let mut stage_input_pack_s = 0.0; + let mut stage_evaluator_call_s = 0.0; + let mut stage_evaluator_s = 0.0; + let mut output_assign_s = 0.0; + let mut stage_input_pack_by_stage_s = Vec::new(); + let mut stage_evaluator_call_by_stage_s = Vec::new(); + let mut stage_output_assign_by_stage_s = Vec::new(); + for stage in self.stages.as_mut().expect("generic stages checked") { + let (pack_s, eval_s, assign_s) = stage.evaluate_generic_into_state( + n_points, + self.parameter_count, + state.as_mut_slice(), + binary_precision, + )?; + stage_input_pack_s += pack_s; + stage_evaluator_call_s += eval_s; + stage_evaluator_s += pack_s + eval_s; + output_assign_s += assign_s; + stage_input_pack_by_stage_s.push(pack_s); + stage_evaluator_call_by_stage_s.push(eval_s); + stage_output_assign_by_stage_s.push(assign_s); + } + + let reduction_start = Instant::now(); + let (resolved, amplitude_input_pack_s, amplitude_evaluator_call_s) = self + .amplitude_stage + .as_mut() + .expect("generic amplitude stage checked") + .evaluate_resolved_generic( + n_points, + state.as_slice(), + binary_precision, + &physics, + self.normalization_factor, + selected_helicity_ids, + selected_color_ids, + )?; + let reduction_s = reduction_start.elapsed().as_secs_f64(); + let amplitude_evaluator_s = amplitude_input_pack_s + amplitude_evaluator_call_s; + Ok(( + resolved, + RuntimeProfile { + source_fill_s: profile_duration_seconds(source_fill_elapsed), + momentum_input_setup_s: profile_duration_seconds(momentum_setup_elapsed), + momentum_setup_s: profile_duration_seconds( + momentum_setup_elapsed + model_parameter_setup_elapsed, + ), + model_parameter_setup_s: profile_duration_seconds(model_parameter_setup_elapsed), + stage_input_pack_s, + stage_evaluator_call_s, + stage_evaluator_s, + output_assign_s, + amplitude_input_pack_s, + amplitude_evaluator_call_s, + amplitude_evaluator_s, + reduction_s, + total_s: total_start.elapsed().as_secs_f64(), + stage_input_pack_by_stage_s, + stage_evaluator_call_by_stage_s, + stage_output_assign_by_stage_s, + ..RuntimeProfile::default() + }, + )) + } +} + +pub(super) fn write_resolved_f64_totals( + resolved: &ResolvedValues, + output: &mut [f64], +) -> RusticolResult<()> { + if output.len() != resolved.point_count { + return Err(RusticolError::invalid_argument(format!( + "evaluation output has length {}, expected {}", + output.len(), + resolved.point_count + ))); + } + let component_count = resolved + .helicity_indices + .len() + .checked_mul(resolved.color_indices.len()) + .ok_or_else(|| RusticolError::integrity("resolved component shape overflows usize"))?; + let expected_value_count = resolved + .point_count + .checked_mul(component_count) + .ok_or_else(|| RusticolError::integrity("resolved value shape overflows usize"))?; + if resolved.values.len() != expected_value_count { + return Err(RusticolError::integrity( + "resolved evaluation has an inconsistent component buffer length", + )); + } + if component_count == 0 { + output.fill(0.0); + return Ok(()); + } + for (target, components) in output + .iter_mut() + .zip(resolved.values.chunks_exact(component_count)) + { + *target = components.iter().sum(); + } + Ok(()) +} + +pub(super) fn resolved_f64_totals(resolved: &ResolvedValues) -> RusticolResult> { + let mut output = vec![0.0; resolved.point_count]; + write_resolved_f64_totals(resolved, &mut output)?; + Ok(output) +} + +pub(super) fn accumulate_selected_lc_replay_resolved_f64( + target: &mut [f64], + point_count: usize, + materialized: &ResolvedValues, + replay_entries: &[LcResolvedReplayEntry], + source_component_count: usize, + target_component_count: usize, +) -> RusticolResult<()> { + validate_materialized_replay_shape( + materialized, + point_count, + replay_entries.len(), + source_component_count, + )?; + if target.len() != point_count * target_component_count { + return Err(RusticolError::invalid_argument( + "LC topology replay target has an inconsistent resolved shape", + )); + } + for (entry_index, entry) in replay_entries.iter().enumerate() { + for point_index in 0..point_count { + let source_row = (entry_index * point_count + point_index) * source_component_count; + let target_row = point_index * target_component_count; + for route in &entry.routes { + if route.source_index >= source_component_count + || route.target_index >= target_component_count + { + return Err(RusticolError::integrity( + "LC topology replay selected route is out of bounds", + )); + } + target[target_row + route.target_index] += + route.weight * materialized.values[source_row + route.source_index]; + } + } + } + Ok(()) +} + +#[cfg(test)] +pub(super) fn accumulate_lc_replay_resolved_f64( + target: &mut [f64], + point_count: usize, + materialized: &ResolvedValues, + replay_entries: &[LcResolvedReplayEntry], + component_count: usize, +) -> RusticolResult<()> { + accumulate_selected_lc_replay_resolved_f64( + target, + point_count, + materialized, + replay_entries, + component_count, + component_count, + ) +} + +#[cfg(feature = "symbolica-runtime")] +fn accumulate_selected_lc_replay_resolved_generic( + target: &mut [T], + point_count: usize, + materialized: &ResolvedValues, + replay_entries: &[LcResolvedReplayEntry], + source_component_count: usize, + target_component_count: usize, +) -> RusticolResult<()> +where + T: RusticolHighPrecisionNumber, + Complex: Real + EvaluationDomain, +{ + validate_materialized_replay_shape( + materialized, + point_count, + replay_entries.len(), + source_component_count, + )?; + if target.len() != point_count * target_component_count { + return Err(RusticolError::invalid_argument( + "LC topology replay target has an inconsistent resolved shape", + )); + } + for (entry_index, entry) in replay_entries.iter().enumerate() { + for point_index in 0..point_count { + let source_row = (entry_index * point_count + point_index) * source_component_count; + let target_row = point_index * target_component_count; + for route in &entry.routes { + if route.source_index >= source_component_count + || route.target_index >= target_component_count + { + return Err(RusticolError::integrity( + "LC topology replay selected route is out of bounds", + )); + } + target[target_row + route.target_index] += + materialized.values[source_row + route.source_index].clone() + * T::from(route.weight); + } + } + } + Ok(()) +} + +fn validate_materialized_replay_shape( + materialized: &ResolvedValues, + point_count: usize, + replay_count: usize, + component_count: usize, +) -> RusticolResult<()> { + let expected_points = point_count.checked_mul(replay_count).ok_or_else(|| { + RusticolError::invalid_argument("LC topology replay point count overflowed") + })?; + let expected_values = expected_points + .checked_mul(component_count) + .ok_or_else(|| { + RusticolError::invalid_argument("LC topology replay resolved shape overflowed") + })?; + if materialized.point_count != expected_points + || materialized.values.len() != expected_values + || materialized.helicity_indices.len() * materialized.color_indices.len() != component_count + { + return Err(RusticolError::invalid_argument( + "materialized LC topology replay result has an inconsistent resolved shape", + )); + } + Ok(()) +} + +#[cfg(test)] +pub(super) fn select_resolved_values( + full_values: Vec, + point_count: usize, + physics: &PhysicsRuntime, + selected_helicity_ids: Option<&BTreeSet>, + selected_color_ids: Option<&BTreeSet>, +) -> RusticolResult> { + let helicity_count = physics.manifest.helicities.len(); + let color_count = physics.manifest.color_components.len(); + if full_values.len() != point_count * helicity_count * color_count { + return Err(RusticolError::invalid_argument( + "resolved LC topology replay result has an inconsistent public shape", + )); + } + let helicity_indices = physics.selected_helicity_indices(selected_helicity_ids)?; + let color_indices = physics.selected_color_indices(selected_color_ids)?; + let mut values = Vec::with_capacity(point_count * helicity_indices.len() * color_indices.len()); + for point_index in 0..point_count { + for helicity_index in &helicity_indices { + for color_index in &color_indices { + values.push( + full_values[(point_index * helicity_count + *helicity_index) * color_count + + *color_index] + .clone(), + ); + } + } + } + Ok(ResolvedValues { + values, + point_count, + helicity_indices, + color_indices, + }) +} + +#[cfg(test)] +mod selected_replay_tests { + use super::*; + + #[test] + fn recognizes_only_ordered_uniform_replay_totals() { + let entry = LcResolvedReplayEntry { + routes: vec![ + LcResolvedReplayRoute { + source_index: 0, + target_index: 1, + weight: 0.5, + }, + LcResolvedReplayRoute { + source_index: 1, + target_index: 0, + weight: 0.5, + }, + ], + }; + assert_eq!(uniform_lc_replay_total_scale(&entry, 2, 2), Some(0.5)); + + let mut nonuniform = entry.clone(); + nonuniform.routes[1].weight = 1.0; + assert_eq!(uniform_lc_replay_total_scale(&nonuniform, 2, 2), None); + + let mut reordered = entry.clone(); + reordered.routes.swap(0, 1); + assert_eq!(uniform_lc_replay_total_scale(&reordered, 2, 2), Some(0.5)); + + let mut out_of_bounds = entry; + out_of_bounds.routes[1].target_index = 2; + assert_eq!(uniform_lc_replay_total_scale(&out_of_bounds, 2, 2), None); + } + + #[test] + fn resolved_totals_validate_nonempty_and_empty_component_shapes() { + let resolved = ResolvedValues { + values: vec![1.0, 2.0, 3.0, 4.0], + point_count: 2, + helicity_indices: vec![0], + color_indices: vec![0, 1], + }; + assert_eq!(resolved_f64_totals(&resolved).unwrap(), vec![3.0, 7.0]); + + let mut truncated = resolved.clone(); + truncated.values.pop(); + assert!( + resolved_f64_totals(&truncated) + .unwrap_err() + .to_string() + .contains("inconsistent component buffer length") + ); + + let empty_axis = ResolvedValues { + values: Vec::new(), + point_count: 2, + helicity_indices: Vec::new(), + color_indices: vec![0], + }; + assert_eq!(resolved_f64_totals(&empty_axis).unwrap(), vec![0.0, 0.0]); + + let mut malformed_empty_axis = empty_axis; + malformed_empty_axis.values.push(1.0); + assert!( + resolved_f64_totals(&malformed_empty_axis) + .unwrap_err() + .to_string() + .contains("inconsistent component buffer length") + ); + } + + #[test] + fn accumulates_selected_routes_without_materializing_public_axis() { + let materialized = ResolvedValues { + values: vec![ + 0.0, 2.0, 0.0, 0.0, // mapping 0, point 0 + 0.0, 3.0, 0.0, 0.0, // mapping 0, point 1 + 0.0, 0.0, 0.0, 5.0, // mapping 1, point 0 + 0.0, 0.0, 0.0, 7.0, // mapping 1, point 1 + ], + point_count: 4, + helicity_indices: vec![0, 1], + color_indices: vec![0, 1], + }; + let entries = vec![ + LcResolvedReplayEntry { + routes: vec![LcResolvedReplayRoute { + source_index: 1, + target_index: 0, + weight: 1.0, + }], + }, + LcResolvedReplayEntry { + routes: vec![LcResolvedReplayRoute { + source_index: 3, + target_index: 0, + weight: 2.0, + }], + }, + ]; + let mut selected = vec![0.0; 2]; + accumulate_selected_lc_replay_resolved_f64(&mut selected, 2, &materialized, &entries, 4, 1) + .expect("selected replay accumulation"); + assert_eq!(selected, vec![12.0, 17.0]); + } + + #[test] + fn accumulates_two_replay_groups_and_one_residual_exactly_once() { + let materialized = ResolvedValues { + values: vec![ + 10.0, 0.0, 20.0, 0.0, 30.0, // identity mapping + 11.0, 0.0, 21.0, 0.0, 31.0, // shared swap mapping + ], + point_count: 2, + helicity_indices: vec![0], + color_indices: vec![0, 1, 2, 3, 4], + }; + let entries = vec![ + LcResolvedReplayEntry { + routes: vec![ + LcResolvedReplayRoute { + source_index: 0, + target_index: 0, + weight: 1.0, + }, + LcResolvedReplayRoute { + source_index: 2, + target_index: 2, + weight: 1.0, + }, + LcResolvedReplayRoute { + source_index: 4, + target_index: 4, + weight: 1.0, + }, + ], + }, + LcResolvedReplayEntry { + routes: vec![ + LcResolvedReplayRoute { + source_index: 0, + target_index: 1, + weight: 1.0, + }, + LcResolvedReplayRoute { + source_index: 2, + target_index: 3, + weight: 1.0, + }, + ], + }, + ]; + let mut target = vec![0.0; 5]; + + accumulate_selected_lc_replay_resolved_f64(&mut target, 1, &materialized, &entries, 5, 5) + .unwrap(); + + assert_eq!(target, vec![10.0, 11.0, 20.0, 21.0, 30.0]); + } +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/engine/helicity_lane.rs b/pyamplicol/rust/crates/rusticol-core/src/engine/helicity_lane.rs new file mode 100644 index 0000000000..9cd87b04d8 --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/engine/helicity_lane.rs @@ -0,0 +1,2373 @@ +// SPDX-License-Identifier: 0BSD + +use super::sources::RuntimeSourceState; +use super::*; + +#[derive(Clone, Debug)] +pub(super) struct CompiledHelicitySelectorSchedule { + pub(super) selector_domain_id: usize, + pub(super) physical_helicity_index: usize, + selected_helicity_ids: BTreeSet, + pub(super) source_states: Vec, + pub(super) active_stage_chunk_indices: Vec>, + pub(super) active_amplitude_chunk_indices: Vec, + pub(super) root_factors: Vec>>, + pub(super) structural_zero: bool, +} + +#[derive(Clone, Debug)] +pub(super) struct CompiledHelicityExecutionPlan { + schedules_by_physical_helicity: Vec>>, +} + +#[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] +fn bind_direct_helicity_color_schedule( + direct: &mut compiled_direct_prototype::CompiledDirectEnginePrototype, + helicity_schedules: &BTreeMap< + usize, + compiled_direct_prototype::CompiledDirectValidatedSchedule, + >, + color_schedules: &BTreeMap, + schedule: &CompiledHelicitySelectorSchedule, + selected: Option<&BTreeSet>, +) -> RusticolResult>> { + let Some(selected) = selected else { + return Ok(None); + }; + if selected.is_empty() { + return Err(RusticolError::invalid_argument( + "compiled Direct-Arena materialized-sector selection cannot be empty", + )); + } + let helicity = helicity_schedules + .get(&schedule.selector_domain_id) + .ok_or_else(|| { + RusticolError::integrity(format!( + "compiled Direct-Arena has no cold-bound helicity schedule for selector domain {}", + schedule.selector_domain_id + )) + })?; + direct + .bind_helicity_color_schedule( + schedule.selector_domain_id, + helicity, + selected, + color_schedules, + ) + .map(Some) +} + +#[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] +fn evaluate_direct_helicity_schedule( + direct: &mut compiled_direct_prototype::CompiledDirectEnginePrototype, + schedules: &BTreeMap, + schedule: &CompiledHelicitySelectorSchedule, + color_schedule: Option<&compiled_direct_prototype::CompiledDirectValidatedSchedule>, + execute_union: bool, + point_count: usize, +) -> RusticolResult<()> { + if execute_union { + return direct.evaluate_all(point_count); + } + let direct_schedule = color_schedule + .or_else(|| schedules.get(&schedule.selector_domain_id)) + .ok_or_else(|| { + RusticolError::integrity(format!( + "compiled Direct-Arena has no cold-bound helicity schedule for selector domain {}", + schedule.selector_domain_id + )) + })?; + direct.evaluate_validated_materialized_helicity(point_count, direct_schedule) +} + +impl ExecutionRuntime { + fn supports_materialized_helicity_totals(&self) -> bool { + let Some(amplitude) = self.amplitude_stage.as_ref() else { + return false; + }; + if amplitude.color_contraction.is_none() { + return true; + } + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + { + self.compiled_direct_runtime.is_some() + } + #[cfg(not(any(feature = "f64-compiled", feature = "f64-symjit")))] + { + false + } + } + + pub(super) fn initialize_compiled_helicity_execution_plan( + &mut self, + public_label_permutation: Option<&[usize]>, + ) -> RusticolResult<()> { + if let Some(sum_runtime) = self.helicity_sum_runtime.as_mut() { + sum_runtime.initialize_compiled_helicity_execution_plan(public_label_permutation)?; + } + for selector_runtime in &mut self.helicity_selector_runtimes { + selector_runtime + .initialize_compiled_helicity_execution_plan(public_label_permutation)?; + } + let Some(recurrence) = self.helicity_recurrence.as_ref() else { + self.compiled_helicity_execution_plan = None; + return Ok(()); + }; + let Some(materialization) = recurrence.materialization.as_ref() else { + self.compiled_helicity_execution_plan = None; + return Ok(()); + }; + let physics = self.physics.as_ref().ok_or_else(|| { + RusticolError::artifact( + "helicity recurrence materialization requires resolved physics metadata", + ) + })?; + if self.stages.is_none() || self.amplitude_stage.is_none() { + return Err(self.execution_unavailable_error()); + } + + let mut schedules_by_physical_helicity = vec![None; physics.manifest.helicities.len()]; + let mut schedule_by_domain = vec![None; recurrence.selector_domains.len()]; + let mut physical_helicity_by_domain = vec![None; recurrence.selector_domains.len()]; + for schedule in &materialization.selector_schedules { + let domain = recurrence + .selector_domains + .get(schedule.selector_domain_id) + .ok_or_else(|| { + RusticolError::integrity(format!( + "helicity schedule references unknown selector domain {}", + schedule.selector_domain_id + )) + })?; + if !domain.complete { + return Err(RusticolError::integrity(format!( + "helicity schedule domain {} is not complete", + schedule.selector_domain_id + ))); + } + let physical_helicity_index = + physical_helicity_index_for_domain(physics, domain, public_label_permutation)?; + if physical_helicity_by_domain[schedule.selector_domain_id] + .replace(physical_helicity_index) + .is_some() + { + return Err(RusticolError::integrity(format!( + "helicity selector domain {} has multiple recurrence schedules", + schedule.selector_domain_id + ))); + } + + let source_states = if schedule.structural_zero { + Vec::new() + } else { + build_materialized_source_states( + &self.sources, + &recurrence.selector_domains, + &materialization.source_routes, + domain, + &schedule.active_current_ids, + )? + }; + let root_factors = materialized_root_factors_for_domain( + self.amplitude_output_count, + &materialization.amplitude_routes, + schedule, + )?; + schedule_by_domain[schedule.selector_domain_id] = + Some(Arc::new(CompiledHelicitySelectorSchedule { + selector_domain_id: schedule.selector_domain_id, + physical_helicity_index, + selected_helicity_ids: BTreeSet::from([physics.manifest.helicities + [physical_helicity_index] + .id + .clone()]), + source_states, + active_stage_chunk_indices: schedule.active_stage_chunk_indices.clone(), + active_amplitude_chunk_indices: schedule.active_amplitude_chunk_indices.clone(), + root_factors, + structural_zero: schedule.structural_zero, + })); + } + let execution_domain_by_domain = helicity_execution_domain_by_domain( + recurrence.selector_domains.len(), + &materialization.amplitude_routes, + )?; + for (domain_id, execution_candidate) in schedule_by_domain.iter().enumerate() { + if execution_candidate.is_none() { + continue; + } + let physical_helicity_index = + physical_helicity_by_domain[domain_id].ok_or_else(|| { + RusticolError::integrity(format!( + "helicity selector domain {domain_id} has no physical-helicity mapping" + )) + })?; + let execution_domain_id = execution_domain_by_domain[domain_id]; + let execution_schedule = schedule_by_domain + .get(execution_domain_id) + .and_then(Option::as_ref) + .cloned() + .ok_or_else(|| { + RusticolError::integrity(format!( + "helicity selector domain {domain_id} maps to absent execution domain {execution_domain_id}" + )) + })?; + if schedules_by_physical_helicity[physical_helicity_index] + .replace(execution_schedule) + .is_some() + { + return Err(RusticolError::integrity(format!( + "physical helicity {} has multiple recurrence schedules", + physics.manifest.helicities[physical_helicity_index].id + ))); + } + } + if schedules_by_physical_helicity.iter().any(Option::is_none) { + return Err(RusticolError::integrity( + "helicity recurrence schedules do not cover every physical helicity", + )); + } + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + { + let mut direct_schedules = BTreeMap::new(); + if let Some(direct) = self.compiled_direct_runtime.as_ref() { + for schedule in schedules_by_physical_helicity.iter().flatten() { + if schedule.structural_zero + || direct_schedules.contains_key(&schedule.selector_domain_id) + { + continue; + } + direct_schedules.insert( + schedule.selector_domain_id, + direct.bind_helicity_schedule(schedule.as_ref())?, + ); + } + } + self.compiled_direct_helicity_schedules = direct_schedules; + } + self.compiled_helicity_execution_plan = Some(CompiledHelicityExecutionPlan { + schedules_by_physical_helicity, + }); + Ok(()) + } + + pub(super) fn has_compiled_helicity_execution_plan(&self) -> bool { + self.compiled_helicity_execution_plan.is_some() + } + + /// Whether a singleton physical-helicity request can bypass a compact + /// helicity-sum/topology-replay execution without losing physical colour. + /// + /// A nested runtime owns a complete non-replay colour DAG. When a proper + /// subset of LC flows is requested, prefer the compact topology replay: + /// that path maps both the flow and its helicity to the representative. + /// The complete-colour nested runtime remains useful for all-flow calls. + /// A local materialized schedule is safe only when this runtime is itself + /// not a replay representative. Parent closures are deliberately not + /// considered here: replay-backed closures bypass the physical topology + /// contraction. + pub(super) fn has_safe_singleton_helicity_execution( + &self, + selected_helicity_ids: Option<&BTreeSet>, + selected_color_ids: Option<&BTreeSet>, + ) -> RusticolResult { + let Some(selected_helicity_ids) = selected_helicity_ids else { + return Ok(false); + }; + if selected_helicity_ids.len() != 1 { + return Ok(false); + } + let Some(plan) = self.compiled_helicity_execution_plan.as_ref() else { + return Ok(false); + }; + let physics = self.physics.as_ref().ok_or_else(|| { + RusticolError::artifact( + "helicity recurrence materialization requires resolved physics metadata", + ) + })?; + let selected_helicity_id = selected_helicity_ids + .iter() + .next() + .expect("singleton helicity selection checked"); + let helicity_index = *physics + .helicity_index_by_id + .get(selected_helicity_id) + .ok_or_else(|| { + RusticolError::selector(format!( + "unknown resolved helicity id {selected_helicity_id:?}" + )) + })?; + let schedule = plan + .schedules_by_physical_helicity + .get(helicity_index) + .and_then(Option::as_ref) + .ok_or_else(|| { + RusticolError::integrity(format!( + "physical helicity {} has no compiled recurrence schedule", + physics.manifest.helicities[helicity_index].id + )) + })?; + if schedule.structural_zero { + return Ok(true); + } + if self.lc_topology_replay_enabled + && !physics.has_contracted_color_axis() + && physics + .canonical_total_color_selector(selected_color_ids) + .is_some() + { + return Ok(false); + } + if self + .helicity_selector_lane_by_domain + .get(&schedule.selector_domain_id) + .and_then(|lane_index| { + self.helicity_selector_runtime_schedule_modes + .get(*lane_index) + }) + == Some(&HelicitySelectorScheduleMode::NestedRuntime) + { + return Ok(true); + } + Ok(!self.color_topology_replay_enabled && !self.lc_topology_replay_enabled) + } + + pub(super) fn compiled_color_selector_schedule( + &self, + materialized_sector_ids: &BTreeSet, + ) -> RusticolResult> { + let Some(plan) = self.compiled_color_execution_plan.as_ref() else { + return Ok(None); + }; + if materialized_sector_ids.is_empty() { + return Err(RusticolError::invalid_argument( + "compiled color selection requires at least one materialized sector", + )); + } + let mut active_stage_chunks = vec![BTreeSet::new(); self.stage_count]; + let mut active_amplitude_chunks = BTreeSet::new(); + for sector_id in materialized_sector_ids { + let schedule = plan + .schedules_by_materialized_sector + .get(sector_id) + .ok_or_else(|| { + RusticolError::integrity(format!( + "compiled color selector has no schedule for materialized sector {sector_id}" + )) + })?; + if schedule.active_stage_chunk_indices.len() != active_stage_chunks.len() { + return Err(RusticolError::integrity(format!( + "compiled color selector sector {sector_id} has inconsistent stage coverage" + ))); + } + for (target, source) in active_stage_chunks + .iter_mut() + .zip(&schedule.active_stage_chunk_indices) + { + target.extend(source.iter().copied()); + } + active_amplitude_chunks.extend(schedule.active_amplitude_chunk_indices.iter().copied()); + } + Ok(Some(CompiledColorSelectorSchedule { + active_stage_chunk_indices: active_stage_chunks + .into_iter() + .map(|indices| indices.into_iter().collect()) + .collect(), + active_amplitude_chunk_indices: active_amplitude_chunks.into_iter().collect(), + })) + } + + /// Direct totals lane for the common runtime selector shape: one physical + /// helicity and any manifest-ordered subset of diagonal LC colours. + /// + /// Broader helicity sums retain the resolved fallback because adding a + /// pre-summed colour row to a previous helicity would change the flattened + /// H-major/C-minor floating-point fold. + pub(super) fn try_run_f64_with_helicity_recurrence_into_unprofiled( + &mut self, + batch: F64MomentumBatchView<'_>, + selected_helicity_ids: Option<&BTreeSet>, + selected_color_ids: Option<&BTreeSet>, + output: &mut [f64], + ) -> RusticolResult { + if output.len() != batch.point_count() { + return Err(RusticolError::invalid_argument(format!( + "evaluation output has length {}, expected {}", + output.len(), + batch.point_count() + ))); + } + let physics = self.physics.clone().ok_or_else(|| { + RusticolError::artifact( + "helicity recurrence materialization requires resolved physics metadata", + ) + })?; + let Some(selected_helicity_ids) = selected_helicity_ids else { + return Ok(false); + }; + if selected_helicity_ids.len() != 1 { + return Ok(false); + } + let selected_helicity_id = selected_helicity_ids + .iter() + .next() + .expect("singleton helicity selection checked"); + let helicity_index = *physics + .helicity_index_by_id + .get(selected_helicity_id) + .ok_or_else(|| { + RusticolError::selector(format!( + "unknown resolved helicity id {selected_helicity_id:?}" + )) + })?; + if let Some(selected_color_ids) = selected_color_ids { + for id in selected_color_ids { + if !physics.color_index_by_id.contains_key(id) { + return Err(RusticolError::selector(format!( + "unknown resolved color component id {id:?}" + ))); + } + } + } + let schedule = self + .compiled_helicity_execution_plan + .as_ref() + .and_then(|plan| { + plan.schedules_by_physical_helicity + .get(helicity_index) + .and_then(Option::as_ref) + }) + .cloned() + .ok_or_else(|| { + RusticolError::integrity(format!( + "physical helicity {} has no compiled recurrence schedule", + physics.manifest.helicities[helicity_index].id + )) + })?; + output.fill(0.0); + if schedule.structural_zero { + return Ok(true); + } + + let selector_lane_index = self + .helicity_selector_lane_by_domain + .get(&schedule.selector_domain_id) + .copied() + .filter(|lane_index| { + self.helicity_selector_runtime_schedule_modes + .get(*lane_index) + .is_some_and(|schedule_mode| { + *schedule_mode == HelicitySelectorScheduleMode::NestedRuntime + || (*schedule_mode == HelicitySelectorScheduleMode::ParentClosure + && selected_color_ids.is_none() + && !physics.has_contracted_color_axis() + && !self.color_topology_replay_enabled + && !self.lc_topology_replay_enabled) + }) + }); + if let Some(lane_index) = selector_lane_index { + let schedule_mode = self + .helicity_selector_runtime_schedule_modes + .get(lane_index) + .copied() + .ok_or_else(|| { + RusticolError::integrity(format!( + "helicity selector domain {} maps to missing schedule mode for lane {lane_index}", + schedule.selector_domain_id + )) + })?; + let selector_runtime = self + .helicity_selector_runtimes + .get_mut(lane_index) + .ok_or_else(|| { + RusticolError::integrity(format!( + "helicity selector domain {} maps to missing execution lane {lane_index}", + schedule.selector_domain_id + )) + })?; + match schedule_mode { + HelicitySelectorScheduleMode::ParentClosure => { + if !selector_runtime.supports_materialized_helicity_totals() { + return Ok(false); + } + selector_runtime.run_f64_materialized_helicity_schedule_add_into_unprofiled( + batch, &physics, None, None, &schedule, true, output, + )?; + } + HelicitySelectorScheduleMode::NestedRuntime => { + selector_runtime.run_f64_selected_into_unprofiled( + batch, + Some(&schedule.selected_helicity_ids), + selected_color_ids, + output, + )?; + } + } + return Ok(true); + } + + if self.color_topology_replay_enabled || self.lc_topology_replay_enabled { + return Ok(false); + } + if !self.supports_materialized_helicity_totals() { + return Ok(false); + } + self.run_f64_materialized_helicity_schedule_add_into_unprofiled( + batch, + &physics, + selected_color_ids, + None, + &schedule, + false, + output, + )?; + Ok(true) + } + + pub(super) fn run_resolved_f64_with_helicity_recurrence_unprofiled( + &mut self, + batch: F64MomentumBatchView<'_>, + selected_helicity_ids: Option<&BTreeSet>, + selected_color_ids: Option<&BTreeSet>, + selected_materialized_sector_ids: Option<&BTreeSet>, + ) -> RusticolResult> { + let physics = self.physics.clone().ok_or_else(|| { + RusticolError::artifact( + "helicity recurrence materialization requires resolved physics metadata", + ) + })?; + let helicity_indices = physics.selected_helicity_indices(selected_helicity_ids)?; + let color_indices = physics.selected_color_indices(selected_color_ids)?; + let point_count = batch.point_count(); + let component_count = helicity_indices + .len() + .checked_mul(color_indices.len()) + .ok_or_else(|| RusticolError::invalid_argument("resolved shape overflows usize"))?; + let mut values = vec![0.0; point_count * component_count]; + for (helicity_position, helicity_index) in helicity_indices.iter().copied().enumerate() { + let schedule = self + .compiled_helicity_execution_plan + .as_ref() + .and_then(|plan| { + plan.schedules_by_physical_helicity + .get(helicity_index) + .and_then(Option::as_ref) + }) + .cloned() + .ok_or_else(|| { + RusticolError::integrity(format!( + "physical helicity {} has no compiled recurrence schedule", + physics.manifest.helicities[helicity_index].id + )) + })?; + if schedule.structural_zero { + continue; + } + let selector_lane_index = self + .helicity_selector_lane_by_domain + .get(&schedule.selector_domain_id) + .copied() + .filter(|lane_index| { + self.helicity_selector_runtime_schedule_modes + .get(*lane_index) + .is_some_and(|schedule_mode| { + (*schedule_mode == HelicitySelectorScheduleMode::NestedRuntime + && selected_materialized_sector_ids.is_none()) + || (*schedule_mode == HelicitySelectorScheduleMode::ParentClosure + && selected_color_ids.is_none() + && selected_materialized_sector_ids.is_none() + && !physics.has_contracted_color_axis() + && !self.color_topology_replay_enabled + && !self.lc_topology_replay_enabled) + }) + }); + let mut selected = if let Some(lane_index) = selector_lane_index { + let schedule_mode = self + .helicity_selector_runtime_schedule_modes + .get(lane_index) + .copied() + .ok_or_else(|| { + RusticolError::integrity(format!( + "helicity selector domain {} maps to missing schedule mode for lane {lane_index}", + schedule.selector_domain_id + )) + })?; + let selector_runtime = self + .helicity_selector_runtimes + .get_mut(lane_index) + .ok_or_else(|| { + RusticolError::integrity(format!( + "helicity selector domain {} maps to missing execution lane {lane_index}", + schedule.selector_domain_id + )) + })?; + match schedule_mode { + HelicitySelectorScheduleMode::ParentClosure => selector_runtime + .run_f64_materialized_helicity_schedule_unprofiled( + batch, &physics, None, None, &schedule, true, + )?, + HelicitySelectorScheduleMode::NestedRuntime => { + let selected_helicity = BTreeSet::from([physics.manifest.helicities + [schedule.physical_helicity_index] + .id + .clone()]); + selector_runtime.run_resolved_f64_unprofiled( + batch, + Some(&selected_helicity), + selected_color_ids, + )? + } + } + } else { + if self.color_topology_replay_enabled + || (self.lc_topology_replay_enabled + && selected_materialized_sector_ids.is_none()) + { + return Err(RusticolError::integrity( + "topology-replay runtime cannot execute its representative materialized helicity schedule directly", + )); + } + self.run_f64_materialized_helicity_schedule_unprofiled( + batch, + &physics, + selected_color_ids, + selected_materialized_sector_ids, + &schedule, + false, + )? + }; + if selected.helicity_indices != [schedule.physical_helicity_index] + || selected.color_indices != color_indices + || selected.point_count != point_count + { + return Err(RusticolError::integrity( + "materialized helicity schedule returned an inconsistent resolved shape", + )); + } + selected.helicity_indices[0] = helicity_index; + if helicity_indices.len() == 1 { + return Ok(selected); + } + for point_index in 0..point_count { + let source_start = point_index * color_indices.len(); + let target_start = + point_index * component_count + helicity_position * color_indices.len(); + values[target_start..target_start + color_indices.len()].copy_from_slice( + &selected.values[source_start..source_start + color_indices.len()], + ); + } + } + Ok(ResolvedValues { + values, + point_count, + helicity_indices, + color_indices, + }) + } + + fn run_f64_materialized_helicity_schedule_unprofiled( + &mut self, + batch: F64MomentumBatchView<'_>, + physics: &PhysicsRuntime, + selected_color_ids: Option<&BTreeSet>, + selected_materialized_sector_ids: Option<&BTreeSet>, + schedule: &CompiledHelicitySelectorSchedule, + execute_union: bool, + ) -> RusticolResult> { + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + if self.compiled_direct_runtime.is_some() { + return self.run_f64_materialized_helicity_direct_resolved_unprofiled( + batch, + physics, + selected_color_ids, + selected_materialized_sector_ids, + schedule, + execute_union, + ); + } + self.evaluate_f64_materialized_helicity_schedule_unprofiled( + batch, + selected_materialized_sector_ids, + schedule, + execute_union, + )?; + self.amplitude_stage + .as_mut() + .expect("generic amplitude stage checked") + .reduce_scratch_f64_for_materialized_helicity( + batch.point_count(), + physics, + self.normalization_factor, + schedule.physical_helicity_index, + &schedule.root_factors, + selected_color_ids, + ) + } + + #[allow(clippy::too_many_arguments)] + fn run_f64_materialized_helicity_schedule_add_into_unprofiled( + &mut self, + batch: F64MomentumBatchView<'_>, + physics: &PhysicsRuntime, + selected_color_ids: Option<&BTreeSet>, + selected_materialized_sector_ids: Option<&BTreeSet>, + schedule: &CompiledHelicitySelectorSchedule, + execute_union: bool, + output: &mut [f64], + ) -> RusticolResult<()> { + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + if self.compiled_direct_runtime.is_some() { + return self.run_f64_materialized_helicity_direct_add_into_unprofiled( + batch, + physics, + selected_color_ids, + selected_materialized_sector_ids, + schedule, + execute_union, + output, + ); + } + self.evaluate_f64_materialized_helicity_schedule_unprofiled( + batch, + selected_materialized_sector_ids, + schedule, + execute_union, + )?; + self.amplitude_stage + .as_mut() + .expect("generic amplitude stage checked") + .reduce_scratch_f64_for_materialized_helicity_add_into( + batch.point_count(), + physics, + self.normalization_factor, + schedule.physical_helicity_index, + &schedule.root_factors, + selected_color_ids, + output, + ) + } + + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + #[allow(clippy::too_many_arguments)] + fn run_f64_materialized_helicity_direct_resolved_unprofiled( + &mut self, + batch: F64MomentumBatchView<'_>, + physics: &PhysicsRuntime, + selected_color_ids: Option<&BTreeSet>, + selected_materialized_sector_ids: Option<&BTreeSet>, + schedule: &CompiledHelicitySelectorSchedule, + execute_union: bool, + ) -> RusticolResult> { + let color_indices = physics.selected_color_indices(selected_color_ids)?; + let point_count = batch.point_count(); + let mut values = vec![0.0; point_count * color_indices.len()]; + let Self { + compiled_direct_runtime: Some(direct), + compiled_direct_color_schedules, + compiled_direct_helicity_schedules, + sources, + external_count, + particle_masses, + momentum_slots, + external_is_initial, + model_parameter_values_f64, + amplitude_stage: Some(amplitude), + normalization_factor, + .. + } = self + else { + return Err(RusticolError::integrity( + "compiled Direct-Arena helicity execution was selected without a complete runtime", + )); + }; + let color_schedule = bind_direct_helicity_color_schedule( + direct, + compiled_direct_helicity_schedules, + compiled_direct_color_schedules, + schedule, + selected_materialized_sector_ids, + )?; + let tile_capacity = direct.reduction_tile_capacity(); + let mut point_start = 0usize; + while point_start < point_count { + let point_stop = (point_start + tile_capacity).min(point_count); + direct.begin_tile_from_inputs( + batch.subview(point_start, point_stop)?, + sources, + Some(&schedule.source_states), + *external_count, + particle_masses, + momentum_slots, + external_is_initial, + model_parameter_values_f64, + )?; + evaluate_direct_helicity_schedule( + direct, + compiled_direct_helicity_schedules, + schedule, + color_schedule.as_deref(), + execute_union, + point_stop - point_start, + )?; + let resolved = amplitude.reduce_planes_f64_for_materialized_helicity( + direct.amplitude_planes()?, + physics, + *normalization_factor, + schedule.physical_helicity_index, + &schedule.root_factors, + selected_color_ids, + )?; + if resolved.helicity_indices != [schedule.physical_helicity_index] + || resolved.color_indices != color_indices + || resolved.point_count != point_stop - point_start + { + return Err(RusticolError::integrity( + "compiled Direct-Arena materialized-helicity reducer returned an inconsistent shape", + )); + } + for tile_point in 0..resolved.point_count { + let source = tile_point * color_indices.len(); + let target = (point_start + tile_point) * color_indices.len(); + values[target..target + color_indices.len()] + .copy_from_slice(&resolved.values[source..source + color_indices.len()]); + } + point_start = point_stop; + } + Ok(ResolvedValues { + values, + point_count, + helicity_indices: vec![schedule.physical_helicity_index], + color_indices, + }) + } + + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + #[allow(clippy::too_many_arguments)] + fn run_f64_materialized_helicity_direct_add_into_unprofiled( + &mut self, + batch: F64MomentumBatchView<'_>, + physics: &PhysicsRuntime, + selected_color_ids: Option<&BTreeSet>, + selected_materialized_sector_ids: Option<&BTreeSet>, + schedule: &CompiledHelicitySelectorSchedule, + execute_union: bool, + output: &mut [f64], + ) -> RusticolResult<()> { + let point_count = batch.point_count(); + if output.len() != point_count { + return Err(RusticolError::invalid_argument(format!( + "materialized-helicity output has length {}, expected {point_count}", + output.len() + ))); + } + let Self { + compiled_direct_runtime: Some(direct), + compiled_direct_color_schedules, + compiled_direct_helicity_schedules, + sources, + external_count, + particle_masses, + momentum_slots, + external_is_initial, + model_parameter_values_f64, + amplitude_stage: Some(amplitude), + normalization_factor, + .. + } = self + else { + return Err(RusticolError::integrity( + "compiled Direct-Arena helicity totals were selected without a complete runtime", + )); + }; + let color_schedule = bind_direct_helicity_color_schedule( + direct, + compiled_direct_helicity_schedules, + compiled_direct_color_schedules, + schedule, + selected_materialized_sector_ids, + )?; + let direct_total_plan = amplitude.bind_default_materialized_helicity_direct_total_plan( + schedule.selector_domain_id, + physics, + schedule.physical_helicity_index, + &schedule.root_factors, + selected_color_ids, + )?; + let tile_capacity = direct.tile_capacity(); + let mut point_start = 0usize; + while point_start < point_count { + let point_stop = (point_start + tile_capacity).min(point_count); + direct.begin_tile_from_inputs( + batch.subview(point_start, point_stop)?, + sources, + Some(&schedule.source_states), + *external_count, + particle_masses, + momentum_slots, + external_is_initial, + model_parameter_values_f64, + )?; + evaluate_direct_helicity_schedule( + direct, + compiled_direct_helicity_schedules, + schedule, + color_schedule.as_deref(), + execute_union, + point_stop - point_start, + )?; + amplitude.reduce_planes_f64_for_materialized_helicity_plan_add_into( + direct.amplitude_planes()?, + *normalization_factor, + direct_total_plan, + &mut output[point_start..point_stop], + )?; + point_start = point_stop; + } + Ok(()) + } + + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + #[allow(clippy::too_many_arguments)] + pub(super) fn run_f64_materialized_helicity_direct_routed_components_add_unprofiled( + &mut self, + batch: F64MomentumBatchView<'_>, + physics: &PhysicsRuntime, + selected_helicity_ids: &BTreeSet, + selected_color_ids: Option<&BTreeSet>, + selected_materialized_sector_ids: &BTreeSet, + replay_entry: &LcResolvedReplayEntry, + source_component_count: usize, + target_component_count: usize, + target_point_count: usize, + target_components: &mut [f64], + ) -> RusticolResult<()> { + if selected_helicity_ids.len() != 1 { + return Err(RusticolError::integrity( + "routed materialized-helicity execution requires one source helicity", + )); + } + let selected_helicity_id = selected_helicity_ids + .iter() + .next() + .expect("singleton source helicity checked"); + let helicity_index = *physics + .helicity_index_by_id + .get(selected_helicity_id) + .ok_or_else(|| { + RusticolError::selector(format!( + "unknown resolved helicity id {selected_helicity_id:?}" + )) + })?; + let schedule = self + .compiled_helicity_execution_plan + .as_ref() + .and_then(|plan| { + plan.schedules_by_physical_helicity + .get(helicity_index) + .and_then(Option::as_ref) + }) + .cloned() + .ok_or_else(|| { + RusticolError::integrity(format!( + "physical helicity {} has no compiled recurrence schedule", + physics.manifest.helicities[helicity_index].id + )) + })?; + if schedule.structural_zero { + return Ok(()); + } + let point_count = batch.point_count(); + let Self { + compiled_direct_runtime: Some(direct), + compiled_direct_color_schedules, + compiled_direct_helicity_schedules, + sources, + external_count, + particle_masses, + momentum_slots, + external_is_initial, + model_parameter_values_f64, + amplitude_stage: Some(amplitude), + normalization_factor, + .. + } = self + else { + return Err(RusticolError::integrity( + "compiled Direct-Arena routed helicity execution was selected without a complete runtime", + )); + }; + let color_schedule = bind_direct_helicity_color_schedule( + direct, + compiled_direct_helicity_schedules, + compiled_direct_color_schedules, + schedule.as_ref(), + Some(selected_materialized_sector_ids), + )?; + // A selected helicity/flow uses only these routed components. The + // complete-axis footprint can otherwise shrink even a singleton to + // one point per tile, despite the already allocated SIMD capacity. + let reduction_footprint = amplitude.compiled_direct_reduction_footprint( + 0, + CompiledDirectRoutedReductionFootprint { + maximum_source_component_count: source_component_count, + maximum_target_component_count: target_component_count, + }, + )?; + let tile_capacity = direct.reduction_tile_capacity_for_footprint(reduction_footprint)?; + let mut point_start = 0usize; + while point_start < point_count { + let point_stop = (point_start + tile_capacity).min(point_count); + direct.begin_tile_from_inputs( + batch.subview(point_start, point_stop)?, + sources, + Some(&schedule.source_states), + *external_count, + particle_masses, + momentum_slots, + external_is_initial, + model_parameter_values_f64, + )?; + evaluate_direct_helicity_schedule( + direct, + compiled_direct_helicity_schedules, + schedule.as_ref(), + color_schedule.as_deref(), + false, + point_stop - point_start, + )?; + amplitude.reduce_planes_f64_for_materialized_helicity_routed_components_add_into( + direct.amplitude_planes()?, + physics, + *normalization_factor, + schedule.physical_helicity_index, + &schedule.root_factors, + selected_color_ids, + replay_entry, + source_component_count, + target_component_count, + target_point_count, + point_start, + target_components, + )?; + point_start = point_stop; + } + Ok(()) + } + + fn evaluate_f64_materialized_helicity_schedule_unprofiled( + &mut self, + batch: F64MomentumBatchView<'_>, + selected_materialized_sector_ids: Option<&BTreeSet>, + schedule: &CompiledHelicitySelectorSchedule, + execute_union: bool, + ) -> RusticolResult<()> { + if self.stages.is_none() || self.amplitude_stage.is_none() { + return Err(self.execution_unavailable_error()); + } + let point_count = batch.point_count(); + let color_schedule = selected_materialized_sector_ids + .map(|sector_ids| self.compiled_color_selector_schedule(sector_ids)) + .transpose()? + .flatten(); + let state_len = point_count + .checked_mul(self.parameter_count) + .ok_or_else(|| RusticolError::invalid_argument("runtime state length overflows"))?; + self.state_scratch_f64.resize(state_len, c64(0.0, 0.0)); + self.state_scratch_f64_requires_clear = true; + let state = &mut self.state_scratch_f64; + let model_parameter_start = self.value_parameter_count + self.momentum_parameter_count; + for row in 0..point_count { + let point = batch.point(row); + let row_state = + &mut state[row * self.parameter_count..(row + 1) * self.parameter_count]; + Self::fill_sources_row_with_states( + &self.sources, + &schedule.source_states, + self.external_count, + &self.particle_masses, + row_state, + &point, + )?; + } + for row in 0..point_count { + let point = batch.point(row); + let row_state = + &mut state[row * self.parameter_count..(row + 1) * self.parameter_count]; + Self::fill_momenta_row( + &self.momentum_slots, + self.value_parameter_count, + self.external_count, + &self.external_is_initial, + row_state, + &point, + )?; + } + for row in 0..point_count { + let row_state = + &mut state[row * self.parameter_count..(row + 1) * self.parameter_count]; + Self::fill_model_parameters_row( + model_parameter_start, + &self.model_parameter_values_f64, + row_state, + )?; + } + let stages = self.stages.as_mut().expect("generic stages checked"); + if schedule.active_stage_chunk_indices.len() != stages.len() { + return Err(RusticolError::integrity(format!( + "helicity schedule {} has {} stage closures, expected {}", + schedule.selector_domain_id, + schedule.active_stage_chunk_indices.len(), + stages.len() + ))); + } + for (stage_index, (stage, active_chunks)) in stages + .iter_mut() + .zip(&schedule.active_stage_chunk_indices) + .enumerate() + { + if execute_union { + stage.evaluate_f64_into_state( + point_count, + self.parameter_count, + state.as_mut_slice(), + )?; + } else { + let combined_chunks; + let active_chunks = if let Some(color_schedule) = color_schedule.as_ref() { + combined_chunks = intersect_sorted_chunk_indices( + active_chunks, + &color_schedule.active_stage_chunk_indices[stage_index], + ); + &combined_chunks + } else { + active_chunks + }; + stage.evaluate_active_chunks_f64_into_state( + point_count, + self.parameter_count, + state.as_mut_slice(), + active_chunks, + )?; + } + } + let combined_amplitude_chunks; + let active_amplitude_chunks = if let Some(color_schedule) = color_schedule.as_ref() { + combined_amplitude_chunks = intersect_sorted_chunk_indices( + &schedule.active_amplitude_chunk_indices, + &color_schedule.active_amplitude_chunk_indices, + ); + &combined_amplitude_chunks + } else { + &schedule.active_amplitude_chunk_indices + }; + let amplitude = self + .amplitude_stage + .as_mut() + .expect("generic amplitude stage checked"); + if execute_union { + amplitude.evaluate_f64_into_scratch(point_count, state.as_slice())?; + } else { + amplitude.evaluate_active_chunks_f64_into_scratch( + point_count, + state.as_slice(), + active_amplitude_chunks, + )?; + } + Ok(()) + } + + pub(super) fn run_resolved_f64_with_helicity_recurrence( + &mut self, + batch: &[Vec<[f64; 4]>], + selected_helicity_ids: Option<&BTreeSet>, + selected_color_ids: Option<&BTreeSet>, + selected_materialized_sector_ids: Option<&BTreeSet>, + ) -> RusticolResult<(ResolvedValues, RuntimeProfile)> { + let total_start = Instant::now(); + let initial_orchestration_start = Instant::now(); + let physics = self.physics.clone().ok_or_else(|| { + RusticolError::artifact( + "helicity recurrence materialization requires resolved physics metadata", + ) + })?; + let helicity_indices = physics.selected_helicity_indices(selected_helicity_ids)?; + let color_indices = physics.selected_color_indices(selected_color_ids)?; + let point_count = batch.len(); + let component_count = helicity_indices + .len() + .checked_mul(color_indices.len()) + .ok_or_else(|| RusticolError::invalid_argument("resolved shape overflows usize"))?; + let mut values = vec![0.0; point_count * component_count]; + let mut schedule_profiles = Vec::with_capacity(helicity_indices.len()); + let mut orchestration_elapsed = initial_orchestration_start.elapsed(); + for (helicity_position, helicity_index) in helicity_indices.iter().copied().enumerate() { + let orchestration_start = Instant::now(); + let schedule = self + .compiled_helicity_execution_plan + .as_ref() + .and_then(|plan| { + plan.schedules_by_physical_helicity + .get(helicity_index) + .and_then(Option::as_ref) + }) + .cloned() + .ok_or_else(|| { + RusticolError::integrity(format!( + "physical helicity {} has no compiled recurrence schedule", + physics.manifest.helicities[helicity_index].id + )) + })?; + if schedule.structural_zero { + orchestration_elapsed += orchestration_start.elapsed(); + continue; + } + let selector_lane_index = self + .helicity_selector_lane_by_domain + .get(&schedule.selector_domain_id) + .copied() + .filter(|lane_index| { + // A nested union understands public color IDs, but not a + // topology-replay group's parent materialized-sector + // restriction. Use the parent schedule for those groups. + self.helicity_selector_runtime_schedule_modes + .get(*lane_index) + .is_some_and(|schedule_mode| { + (*schedule_mode == HelicitySelectorScheduleMode::NestedRuntime + && selected_materialized_sector_ids.is_none()) + || (*schedule_mode == HelicitySelectorScheduleMode::ParentClosure + && selected_color_ids.is_none() + && selected_materialized_sector_ids.is_none() + && !physics.has_contracted_color_axis() + && !self.color_topology_replay_enabled + && !self.lc_topology_replay_enabled) + }) + }); + orchestration_elapsed += orchestration_start.elapsed(); + let (mut selected, schedule_profile) = if let Some(lane_index) = selector_lane_index { + let schedule_mode = self + .helicity_selector_runtime_schedule_modes + .get(lane_index) + .copied() + .ok_or_else(|| { + RusticolError::integrity(format!( + "helicity selector domain {} maps to missing schedule mode for lane {lane_index}", + schedule.selector_domain_id + )) + })?; + let selector_runtime = self + .helicity_selector_runtimes + .get_mut(lane_index) + .ok_or_else(|| { + RusticolError::integrity(format!( + "helicity selector domain {} maps to missing execution lane {lane_index}", + schedule.selector_domain_id + )) + })?; + match schedule_mode { + HelicitySelectorScheduleMode::ParentClosure => selector_runtime + .run_f64_materialized_helicity_schedule( + batch, &physics, None, None, &schedule, true, + )?, + HelicitySelectorScheduleMode::NestedRuntime => { + let selected_helicity = BTreeSet::from([physics.manifest.helicities + [schedule.physical_helicity_index] + .id + .clone()]); + selector_runtime.run_resolved_f64( + batch, + Some(&selected_helicity), + selected_color_ids, + )? + } + } + } else { + if self.color_topology_replay_enabled + || (self.lc_topology_replay_enabled + && selected_materialized_sector_ids.is_none()) + { + return Err(RusticolError::integrity( + "topology-replay runtime cannot execute its representative materialized helicity schedule directly", + )); + } + self.run_f64_materialized_helicity_schedule( + batch, + &physics, + selected_color_ids, + selected_materialized_sector_ids, + &schedule, + false, + )? + }; + let post_execution_start = Instant::now(); + if selected.helicity_indices != [schedule.physical_helicity_index] + || selected.color_indices != color_indices + || selected.point_count != point_count + { + return Err(RusticolError::integrity( + "materialized helicity schedule returned an inconsistent resolved shape", + )); + } + selected.helicity_indices[0] = helicity_index; + if helicity_indices.len() == 1 { + let mut selected_profile = schedule_profile; + orchestration_elapsed += post_execution_start.elapsed(); + selected_profile.orchestration_s += profile_duration_seconds(orchestration_elapsed); + selected_profile.total_s = total_start.elapsed().as_secs_f64(); + return Ok((selected, selected_profile)); + } + for point_index in 0..point_count { + let source_start = point_index * color_indices.len(); + let target_start = + point_index * component_count + helicity_position * color_indices.len(); + values[target_start..target_start + color_indices.len()].copy_from_slice( + &selected.values[source_start..source_start + color_indices.len()], + ); + } + schedule_profiles.push(schedule_profile); + orchestration_elapsed += post_execution_start.elapsed(); + } + let mut profile = RuntimeProfile::default(); + for schedule_profile in &schedule_profiles { + profile.add_sector(schedule_profile); + } + profile.orchestration_s += profile_duration_seconds(orchestration_elapsed); + profile.total_s = total_start.elapsed().as_secs_f64(); + Ok(( + ResolvedValues { + values, + point_count, + helicity_indices, + color_indices, + }, + profile, + )) + } + + fn run_f64_materialized_helicity_schedule( + &mut self, + batch: &[Vec<[f64; 4]>], + physics: &PhysicsRuntime, + selected_color_ids: Option<&BTreeSet>, + selected_materialized_sector_ids: Option<&BTreeSet>, + schedule: &CompiledHelicitySelectorSchedule, + execute_union: bool, + ) -> RusticolResult<(ResolvedValues, RuntimeProfile)> { + if self.stages.is_none() || self.amplitude_stage.is_none() { + return Err(self.execution_unavailable_error()); + } + let total_start = Instant::now(); + let point_count = batch.len(); + let state_prepare_start = Instant::now(); + let color_schedule = selected_materialized_sector_ids + .map(|sector_ids| self.compiled_color_selector_schedule(sector_ids)) + .transpose()? + .flatten(); + let state_len = point_count + .checked_mul(self.parameter_count) + .ok_or_else(|| RusticolError::invalid_argument("runtime state length overflows"))?; + let state_capacity = self.state_scratch_f64.capacity(); + self.state_scratch_f64.resize(state_len, c64(0.0, 0.0)); + let state_prepare_elapsed = state_prepare_start.elapsed(); + let mut scratch_reallocation_count = + u64::from(self.state_scratch_f64.capacity() != state_capacity); + // Every source, momentum, model parameter, and recursive value read by + // this exact closure is overwritten below. Inactive parent-layout + // slots are never read, so clearing the full reusable state would only + // add a parent-size memory pass to a selector-specialized execution. + // A later full execution still clears untouched slots once. + self.state_scratch_f64_requires_clear = true; + let state = &mut self.state_scratch_f64; + let model_parameter_start = self.value_parameter_count + self.momentum_parameter_count; + + let source_start = Instant::now(); + for (row, point) in batch.iter().enumerate() { + let row_state = + &mut state[row * self.parameter_count..(row + 1) * self.parameter_count]; + Self::fill_sources_row_with_states( + &self.sources, + &schedule.source_states, + self.external_count, + &self.particle_masses, + row_state, + point, + )?; + } + let source_fill_elapsed = source_start.elapsed(); + + let momentum_start = Instant::now(); + for (row, point) in batch.iter().enumerate() { + let row_state = + &mut state[row * self.parameter_count..(row + 1) * self.parameter_count]; + Self::fill_momenta_row( + &self.momentum_slots, + self.value_parameter_count, + self.external_count, + &self.external_is_initial, + row_state, + point, + )?; + } + let momentum_setup_elapsed = momentum_start.elapsed(); + + let model_parameter_start_time = Instant::now(); + for row in 0..point_count { + let row_state = + &mut state[row * self.parameter_count..(row + 1) * self.parameter_count]; + Self::fill_model_parameters_row( + model_parameter_start, + &self.model_parameter_values_f64, + row_state, + )?; + } + let model_parameter_setup_elapsed = model_parameter_start_time.elapsed(); + + let stages = self.stages.as_mut().expect("generic stages checked"); + if schedule.active_stage_chunk_indices.len() != stages.len() { + return Err(RusticolError::integrity(format!( + "helicity schedule {} has {} stage closures, expected {}", + schedule.selector_domain_id, + schedule.active_stage_chunk_indices.len(), + stages.len() + ))); + } + let mut stage_profiles = Vec::with_capacity(stages.len()); + let mut stage_input_pack_by_stage_s = Vec::with_capacity(stages.len()); + let mut stage_leaf_input_pack_by_stage_s = Vec::with_capacity(stages.len()); + let mut stage_evaluator_call_by_stage_s = Vec::with_capacity(stages.len()); + let mut stage_backend_call_by_stage_s = Vec::with_capacity(stages.len()); + let mut stage_evaluator_output_gather_by_stage_s = Vec::with_capacity(stages.len()); + let mut stage_output_assign_by_stage_s = Vec::with_capacity(stages.len()); + for (stage_index, (stage, active_chunks)) in stages + .iter_mut() + .zip(&schedule.active_stage_chunk_indices) + .enumerate() + { + let stage_profile = if execute_union { + stage.evaluate_f64_into_state_profile( + point_count, + self.parameter_count, + state.as_mut_slice(), + )? + } else { + let combined_chunks; + let active_chunks = if let Some(color_schedule) = color_schedule.as_ref() { + combined_chunks = intersect_sorted_chunk_indices( + active_chunks, + &color_schedule.active_stage_chunk_indices[stage_index], + ); + &combined_chunks + } else { + active_chunks + }; + stage.evaluate_active_chunks_f64_into_state_profile( + point_count, + self.parameter_count, + state.as_mut_slice(), + active_chunks, + )? + }; + stage_input_pack_by_stage_s.push(stage_profile.input_pack_s); + stage_leaf_input_pack_by_stage_s.push(stage_profile.evaluator.leaf_input_pack_s); + stage_evaluator_call_by_stage_s.push(stage_profile.evaluator.legacy_evaluator_call_s); + stage_backend_call_by_stage_s.push(stage_profile.evaluator.evaluator_call_s); + stage_evaluator_output_gather_by_stage_s.push(stage_profile.evaluator.output_gather_s); + stage_output_assign_by_stage_s.push(stage_profile.output_assign_s); + stage_profiles.push(stage_profile); + } + + let combined_amplitude_chunks; + let active_amplitude_chunks = if let Some(color_schedule) = color_schedule.as_ref() { + combined_amplitude_chunks = intersect_sorted_chunk_indices( + &schedule.active_amplitude_chunk_indices, + &color_schedule.active_amplitude_chunk_indices, + ); + &combined_amplitude_chunks + } else { + &schedule.active_amplitude_chunk_indices + }; + let amplitude = self + .amplitude_stage + .as_mut() + .expect("generic amplitude stage checked"); + let amplitude_profile = if execute_union { + amplitude.evaluate_f64_into_scratch_profile(point_count, state.as_slice())? + } else { + amplitude.evaluate_active_chunks_f64_into_scratch_profile( + point_count, + state.as_slice(), + active_amplitude_chunks, + )? + }; + let amplitude_output_length = amplitude.output_length; + let amplitude_input_pack_s = amplitude_profile.input_pack_s; + let amplitude_evaluator_call_s = + amplitude_profile.evaluator.legacy_evaluator_call_s + amplitude_profile.output_remap_s; + let reduction_start = Instant::now(); + let resolved = self + .amplitude_stage + .as_mut() + .expect("generic amplitude stage checked") + .reduce_scratch_f64_for_materialized_helicity( + point_count, + physics, + self.normalization_factor, + schedule.physical_helicity_index, + &schedule.root_factors, + selected_color_ids, + )?; + let reduction_s = reduction_start.elapsed().as_secs_f64(); + let resolved_materialized_component_count = resolved.values.len() as u64; + let stage_input_pack_s = stage_input_pack_by_stage_s.iter().sum::(); + let stage_evaluator_call_s = stage_evaluator_call_by_stage_s.iter().sum::(); + let output_assign_s = stage_output_assign_by_stage_s.iter().sum::(); + scratch_reallocation_count += stage_profiles + .iter() + .map(|profile| { + profile.scratch_reallocation_count + profile.evaluator.scratch_reallocation_count + }) + .sum::() + + amplitude_profile.scratch_reallocation_count + + amplitude_profile.evaluator.scratch_reallocation_count; + Ok(( + resolved, + RuntimeProfile { + state_prepare_s: profile_duration_seconds(state_prepare_elapsed), + source_fill_s: profile_duration_seconds(source_fill_elapsed), + momentum_input_setup_s: profile_duration_seconds(momentum_setup_elapsed), + momentum_setup_s: profile_duration_seconds( + momentum_setup_elapsed + model_parameter_setup_elapsed, + ), + model_parameter_setup_s: profile_duration_seconds(model_parameter_setup_elapsed), + stage_input_pack_s, + stage_leaf_input_pack_s: stage_profiles + .iter() + .map(|profile| profile.evaluator.leaf_input_pack_s) + .sum(), + stage_evaluator_call_s, + stage_backend_call_s: stage_profiles + .iter() + .map(|profile| profile.evaluator.evaluator_call_s) + .sum(), + stage_evaluator_output_gather_s: stage_profiles + .iter() + .map(|profile| profile.evaluator.output_gather_s) + .sum(), + stage_evaluator_s: stage_input_pack_s + stage_evaluator_call_s, + output_assign_s, + amplitude_input_pack_s, + amplitude_leaf_input_pack_s: amplitude_profile.evaluator.leaf_input_pack_s, + amplitude_evaluator_call_s, + amplitude_backend_call_s: amplitude_profile.evaluator.evaluator_call_s, + amplitude_evaluator_output_gather_s: amplitude_profile.evaluator.output_gather_s, + amplitude_output_remap_s: amplitude_profile.output_remap_s, + amplitude_evaluator_s: amplitude_input_pack_s + amplitude_evaluator_call_s, + reduction_s, + resolved_reduction_materialization_s: reduction_s, + total_s: total_start.elapsed().as_secs_f64(), + stage_input_pack_by_stage_s, + stage_leaf_input_pack_by_stage_s, + stage_evaluator_call_by_stage_s, + stage_backend_call_by_stage_s, + stage_evaluator_output_gather_by_stage_s, + stage_output_assign_by_stage_s, + state_component_count: state_len as u64, + source_component_count: (point_count * schedule.source_states.len()) as u64, + momentum_component_count: (point_count * self.momentum_parameter_count) as u64, + model_parameter_component_count: (point_count + * self.model_parameter_values_f64.len()) + as u64, + stage_input_copy_component_count: stage_profiles + .iter() + .map(|profile| profile.input_copy_component_count) + .sum(), + stage_leaf_input_copy_component_count: stage_profiles + .iter() + .map(|profile| profile.evaluator.leaf_input_copy_component_count) + .sum(), + stage_evaluator_output_gather_component_count: stage_profiles + .iter() + .map(|profile| profile.evaluator.output_gather_component_count) + .sum(), + stage_output_assign_component_count: stage_profiles + .iter() + .map(|profile| profile.output_assign_component_count) + .sum(), + amplitude_input_copy_component_count: amplitude_profile.input_copy_component_count, + amplitude_leaf_input_copy_component_count: amplitude_profile + .evaluator + .leaf_input_copy_component_count, + amplitude_evaluator_output_gather_component_count: amplitude_profile + .evaluator + .output_gather_component_count, + amplitude_output_remap_component_count: amplitude_profile + .output_remap_component_count, + evaluator_backend_call_count: stage_profiles + .iter() + .map(|profile| profile.evaluator.backend_call_count) + .sum::() + + amplitude_profile.evaluator.backend_call_count, + reduction_input_component_count: (point_count * amplitude_output_length) as u64, + resolved_materialized_component_count, + scratch_reallocation_count, + ..RuntimeProfile::default() + }, + )) + } + + #[cfg(feature = "symbolica-runtime")] + pub(super) fn run_resolved_generic_with_helicity_recurrence( + &mut self, + batch: &[Vec<[T; 4]>], + binary_precision: Option, + selected_helicity_ids: Option<&BTreeSet>, + selected_color_ids: Option<&BTreeSet>, + selected_materialized_sector_ids: Option<&BTreeSet>, + ) -> RusticolResult<(ResolvedValues, RuntimeProfile)> + where + T: RusticolHighPrecisionNumber, + Complex: Real + EvaluationDomain, + { + let total_start = Instant::now(); + let physics = self.physics.clone().ok_or_else(|| { + RusticolError::artifact( + "helicity recurrence materialization requires resolved physics metadata", + ) + })?; + let helicity_indices = physics.selected_helicity_indices(selected_helicity_ids)?; + let color_indices = physics.selected_color_indices(selected_color_ids)?; + let point_count = batch.len(); + let component_count = helicity_indices + .len() + .checked_mul(color_indices.len()) + .ok_or_else(|| RusticolError::invalid_argument("resolved shape overflows usize"))?; + let mut values = vec![T::new_zero(); point_count * component_count]; + let mut schedule_profiles = Vec::with_capacity(helicity_indices.len()); + + for (helicity_position, helicity_index) in helicity_indices.iter().copied().enumerate() { + let schedule = self + .compiled_helicity_execution_plan + .as_ref() + .and_then(|plan| { + plan.schedules_by_physical_helicity + .get(helicity_index) + .and_then(Option::as_ref) + }) + .cloned() + .ok_or_else(|| { + RusticolError::integrity(format!( + "physical helicity {} has no compiled recurrence schedule", + physics.manifest.helicities[helicity_index].id + )) + })?; + if schedule.structural_zero { + continue; + } + let nested_lane_index = self + .helicity_selector_lane_by_domain + .get(&schedule.selector_domain_id) + .copied() + .filter(|lane_index| { + selected_materialized_sector_ids.is_none() + && self + .helicity_selector_runtime_schedule_modes + .get(*lane_index) + == Some(&HelicitySelectorScheduleMode::NestedRuntime) + }); + let (mut selected, schedule_profile) = if let Some(lane_index) = nested_lane_index { + let selected_helicity = BTreeSet::from([physics.manifest.helicities + [schedule.physical_helicity_index] + .id + .clone()]); + self.helicity_selector_runtimes + .get_mut(lane_index) + .ok_or_else(|| { + RusticolError::integrity(format!( + "helicity selector domain {} maps to missing exact execution lane {lane_index}", + schedule.selector_domain_id + )) + })? + .run_resolved_generic( + batch, + binary_precision, + Some(&selected_helicity), + selected_color_ids, + )? + } else { + self.run_generic_materialized_helicity_schedule( + batch, + binary_precision, + &physics, + selected_color_ids, + selected_materialized_sector_ids, + &schedule, + )? + }; + if selected.helicity_indices != [schedule.physical_helicity_index] + || selected.color_indices != color_indices + || selected.point_count != point_count + { + return Err(RusticolError::integrity( + "materialized helicity schedule returned an inconsistent resolved shape", + )); + } + selected.helicity_indices[0] = helicity_index; + for point_index in 0..point_count { + let source_start = point_index * color_indices.len(); + let target_start = + point_index * component_count + helicity_position * color_indices.len(); + values[target_start..target_start + color_indices.len()].clone_from_slice( + &selected.values[source_start..source_start + color_indices.len()], + ); + } + schedule_profiles.push(schedule_profile); + } + let mut profile = RuntimeProfile::default(); + for schedule_profile in &schedule_profiles { + profile.add_sector(schedule_profile); + } + profile.total_s = total_start.elapsed().as_secs_f64(); + Ok(( + ResolvedValues { + values, + point_count, + helicity_indices, + color_indices, + }, + profile, + )) + } + + #[cfg(feature = "symbolica-runtime")] + fn run_generic_materialized_helicity_schedule( + &mut self, + batch: &[Vec<[T; 4]>], + binary_precision: Option, + physics: &PhysicsRuntime, + selected_color_ids: Option<&BTreeSet>, + selected_materialized_sector_ids: Option<&BTreeSet>, + schedule: &CompiledHelicitySelectorSchedule, + ) -> RusticolResult<(ResolvedValues, RuntimeProfile)> + where + T: RusticolHighPrecisionNumber, + Complex: Real + EvaluationDomain, + { + if self.stages.is_none() || self.amplitude_stage.is_none() { + return Err(self.execution_unavailable_error()); + } + let total_start = Instant::now(); + let point_count = batch.len(); + let color_schedule = selected_materialized_sector_ids + .map(|sector_ids| self.compiled_color_selector_schedule(sector_ids)) + .transpose()? + .flatten(); + let state_len = point_count + .checked_mul(self.parameter_count) + .ok_or_else(|| RusticolError::invalid_argument("runtime state length overflows"))?; + let mut state = vec![complex_zero::(); state_len]; + let model_parameter_start = self.value_parameter_count + self.momentum_parameter_count; + + let source_start = Instant::now(); + for (row, point) in batch.iter().enumerate() { + let row_state = + &mut state[row * self.parameter_count..(row + 1) * self.parameter_count]; + Self::fill_sources_row_generic_with_states( + &self.sources, + &schedule.source_states, + self.external_count, + &self.particle_masses, + row_state, + point, + )?; + } + let source_fill_elapsed = source_start.elapsed(); + + let momentum_start = Instant::now(); + for (row, point) in batch.iter().enumerate() { + let row_state = + &mut state[row * self.parameter_count..(row + 1) * self.parameter_count]; + Self::fill_momenta_row_generic( + &self.momentum_slots, + self.value_parameter_count, + self.external_count, + &self.external_is_initial, + row_state, + point, + )?; + } + let momentum_setup_elapsed = momentum_start.elapsed(); + + let model_parameter_start_time = Instant::now(); + for row in 0..point_count { + let row_state = + &mut state[row * self.parameter_count..(row + 1) * self.parameter_count]; + Self::fill_model_parameters_row_generic( + model_parameter_start, + &self.model_parameter_values_f64, + row_state, + )?; + } + let model_parameter_setup_elapsed = model_parameter_start_time.elapsed(); + + let stages = self.stages.as_mut().expect("generic stages checked"); + if schedule.active_stage_chunk_indices.len() != stages.len() { + return Err(RusticolError::integrity(format!( + "helicity schedule {} has {} stage closures, expected {}", + schedule.selector_domain_id, + schedule.active_stage_chunk_indices.len(), + stages.len() + ))); + } + // The same certified closure applies independently of precision. + // Follow its chunks after intersecting the mapped colour selection. + let mut stage_input_pack_by_stage_s = Vec::with_capacity(stages.len()); + let mut stage_evaluator_call_by_stage_s = Vec::with_capacity(stages.len()); + let mut stage_output_assign_by_stage_s = Vec::with_capacity(stages.len()); + for (stage_index, (stage, active_chunks)) in stages + .iter_mut() + .zip(&schedule.active_stage_chunk_indices) + .enumerate() + { + let combined_chunks; + let active_chunks = if let Some(color_schedule) = color_schedule.as_ref() { + combined_chunks = intersect_sorted_chunk_indices( + active_chunks, + &color_schedule.active_stage_chunk_indices[stage_index], + ); + &combined_chunks + } else { + active_chunks + }; + let (pack_s, eval_s, assign_s) = stage.evaluate_selected_chunks_generic_into_state( + point_count, + self.parameter_count, + state.as_mut_slice(), + binary_precision, + Some(active_chunks), + )?; + stage_input_pack_by_stage_s.push(pack_s); + stage_evaluator_call_by_stage_s.push(eval_s); + stage_output_assign_by_stage_s.push(assign_s); + } + + let combined_amplitude_chunks; + let active_amplitude_chunks = if let Some(color_schedule) = color_schedule.as_ref() { + combined_amplitude_chunks = intersect_sorted_chunk_indices( + &schedule.active_amplitude_chunk_indices, + &color_schedule.active_amplitude_chunk_indices, + ); + &combined_amplitude_chunks + } else { + &schedule.active_amplitude_chunk_indices + }; + let (resolved, amplitude_input_pack_s, amplitude_evaluator_call_s, reduction_s) = self + .amplitude_stage + .as_mut() + .expect("generic amplitude stage checked") + .evaluate_materialized_helicity_generic( + point_count, + state.as_slice(), + binary_precision, + physics, + self.normalization_factor, + schedule.physical_helicity_index, + &schedule.root_factors, + selected_color_ids, + active_amplitude_chunks, + )?; + let stage_input_pack_s = stage_input_pack_by_stage_s.iter().sum::(); + let stage_evaluator_call_s = stage_evaluator_call_by_stage_s.iter().sum::(); + let output_assign_s = stage_output_assign_by_stage_s.iter().sum::(); + Ok(( + resolved, + RuntimeProfile { + source_fill_s: profile_duration_seconds(source_fill_elapsed), + momentum_input_setup_s: profile_duration_seconds(momentum_setup_elapsed), + momentum_setup_s: profile_duration_seconds( + momentum_setup_elapsed + model_parameter_setup_elapsed, + ), + model_parameter_setup_s: profile_duration_seconds(model_parameter_setup_elapsed), + stage_input_pack_s, + stage_evaluator_call_s, + stage_evaluator_s: stage_input_pack_s + stage_evaluator_call_s, + output_assign_s, + amplitude_input_pack_s, + amplitude_evaluator_call_s, + amplitude_evaluator_s: amplitude_input_pack_s + amplitude_evaluator_call_s, + reduction_s, + total_s: total_start.elapsed().as_secs_f64(), + stage_input_pack_by_stage_s, + stage_evaluator_call_by_stage_s, + stage_output_assign_by_stage_s, + ..RuntimeProfile::default() + }, + )) + } +} + +fn intersect_sorted_chunk_indices(left: &[usize], right: &[usize]) -> Vec { + let mut result = Vec::with_capacity(left.len().min(right.len())); + let (mut left_index, mut right_index) = (0, 0); + while left_index < left.len() && right_index < right.len() { + match left[left_index].cmp(&right[right_index]) { + std::cmp::Ordering::Less => left_index += 1, + std::cmp::Ordering::Greater => right_index += 1, + std::cmp::Ordering::Equal => { + result.push(left[left_index]); + left_index += 1; + right_index += 1; + } + } + } + result +} + +fn physical_helicity_index_for_domain( + physics: &PhysicsRuntime, + domain: &HelicitySelectorDomainRuntime, + public_label_permutation: Option<&[usize]>, +) -> RusticolResult { + let external_count = physics + .manifest + .helicities + .first() + .map(|helicity| helicity.values.len()) + .ok_or_else(|| RusticolError::artifact("physics metadata has no helicity axis"))?; + let mut values = vec![None; external_count]; + for (external_index, helicity) in &domain.source_states { + let value = values.get_mut(*external_index).ok_or_else(|| { + RusticolError::integrity("helicity selector domain has a stale external label") + })?; + if value.replace(*helicity).is_some() { + return Err(RusticolError::integrity( + "helicity selector domain repeats an external label", + )); + } + } + let mut values = values + .into_iter() + .collect::>>() + .ok_or_else(|| RusticolError::integrity("helicity selector domain is incomplete"))?; + if let Some(permutation) = public_label_permutation { + values = permute_representative_helicity_to_public_alias(&values, permutation)?; + } + physics + .manifest + .helicities + .iter() + .position(|helicity| helicity.values == values) + .ok_or_else(|| { + RusticolError::artifact(format!( + "helicity selector domain {values:?} has no physical metadata entry" + )) + }) +} + +fn permute_representative_helicity_to_public_alias( + representative: &[i32], + permutation: &[usize], +) -> RusticolResult> { + if permutation.len() != representative.len() + || permutation.iter().copied().collect::>() + != (0..representative.len()).collect::>() + { + return Err(RusticolError::integrity( + "process alias does not define a complete helicity-label permutation", + )); + } + let mut public = vec![0; representative.len()]; + for (representative_index, public_index) in permutation.iter().copied().enumerate() { + public[public_index] = representative[representative_index]; + } + Ok(public) +} + +fn build_materialized_source_states( + sources: &[GenericSourceRecordManifest], + selector_domains: &[HelicitySelectorDomainRuntime], + source_routes: &[HelicityMaterializedSourceRouteRuntime], + complete_domain: &HelicitySelectorDomainRuntime, + active_current_ids: &[usize], +) -> RusticolResult> { + let active_current_ids = active_current_ids.iter().copied().collect::>(); + let state_by_external = complete_domain + .source_states + .iter() + .copied() + .collect::>(); + let mut states = Vec::with_capacity(sources.len()); + for source in sources { + if !active_current_ids.contains(&source.current_id) { + states.push(ExecutionRuntime::inactive_runtime_source_state(source)?); + continue; + } + let external_index = source + .leg_label + .checked_sub(1) + .ok_or_else(|| RusticolError::integrity("runtime source labels must be one-based"))?; + let selected_helicity = state_by_external.get(&external_index).ok_or_else(|| { + RusticolError::integrity(format!( + "active materialized source {} has no state in its complete helicity domain", + source.source_id + )) + })?; + let mut matching = source_routes.iter().filter(|route| { + route.materialized_current_id == source.current_id + && route.external_index == external_index + && route.helicity == *selected_helicity + && selector_domains + .get(route.selector_domain_id) + .is_some_and(|domain| { + domain + .source_states + .iter() + .all(|state| complete_domain.source_states.contains(state)) + }) + }); + let route = matching.next(); + if matching.next().is_some() { + return Err(RusticolError::artifact(format!( + "active materialized source {} has multiple routes for helicity {}", + source.source_id, selected_helicity + ))); + } + states.push(if let Some(route) = route { + ExecutionRuntime::runtime_source_state( + source, + route.declared_state_index, + c64(route.factor[0], route.factor[1]), + )? + } else { + let declared_state_index = + ExecutionRuntime::runtime_source_state_index_for_helicity( + source, + *selected_helicity, + )? + .ok_or_else(|| { + RusticolError::artifact(format!( + "active materialized source {} has neither a recurrence route nor a declared state for helicity {}", + source.source_id, selected_helicity + )) + })?; + ExecutionRuntime::runtime_source_state( + source, + declared_state_index, + c64(1.0, 0.0), + )? + }); + } + Ok(states) +} + +fn materialized_root_factors_for_domain( + root_count: usize, + routes: &[HelicityMaterializedAmplitudeRouteRuntime], + schedule: &HelicityMaterializedSelectorScheduleRuntime, +) -> RusticolResult>>> { + let active_roots = schedule + .active_root_ids + .iter() + .copied() + .collect::>(); + let mut factors = vec![None; root_count]; + for route in routes { + if !route + .selector_domain_ids + .contains(&schedule.selector_domain_id) + { + continue; + } + let factor = c64(route.factor[0], route.factor[1]); + let target = factors.get_mut(route.materialized_root_id).ok_or_else(|| { + RusticolError::integrity("helicity amplitude route has a stale materialized root") + })?; + if target.replace(factor).is_some() { + return Err(RusticolError::artifact(format!( + "materialized amplitude root {} has multiple routes for selector domain {}", + route.materialized_root_id, schedule.selector_domain_id + ))); + } + } + let routed_roots = factors + .iter() + .enumerate() + .filter_map(|(index, factor)| factor.as_ref().map(|_| index)) + .collect::>(); + if routed_roots != active_roots { + return Err(RusticolError::artifact(format!( + "selector domain {} amplitude routes do not match its active-root closure", + schedule.selector_domain_id + ))); + } + Ok(factors) +} + +fn helicity_execution_domain_by_domain( + domain_count: usize, + routes: &[HelicityMaterializedAmplitudeRouteRuntime], +) -> RusticolResult> { + // The producer orders each proven route as its retained source state + // followed by synthetic global-flip aliases. Execute aliases through the + // retained source route and relabel the resolved result for the caller. + let mut execution_domain_by_domain = (0..domain_count).collect::>(); + let mut primary_domains = BTreeSet::new(); + for route in routes { + let Some((&primary_domain, replay_domains)) = route.selector_domain_ids.split_first() + else { + return Err(RusticolError::artifact( + "helicity materialized amplitude route has no selector domains", + )); + }; + if primary_domain >= domain_count { + return Err(RusticolError::integrity(format!( + "helicity amplitude route references stale primary domain {primary_domain}" + ))); + } + primary_domains.insert(primary_domain); + for replay_domain in replay_domains { + if *replay_domain >= domain_count { + return Err(RusticolError::integrity(format!( + "helicity amplitude route references stale replay domain {replay_domain}" + ))); + } + let previous = execution_domain_by_domain[*replay_domain]; + if previous != *replay_domain && previous != primary_domain { + return Err(RusticolError::artifact(format!( + "helicity replay domain {replay_domain} maps to inconsistent execution domains {previous} and {primary_domain}" + ))); + } + execution_domain_by_domain[*replay_domain] = primary_domain; + } + } + for primary_domain in primary_domains { + let execution_domain = execution_domain_by_domain[primary_domain]; + if execution_domain != primary_domain { + return Err(RusticolError::artifact(format!( + "helicity selector domain {primary_domain} is both a primary and replay-only domain" + ))); + } + } + Ok(execution_domain_by_domain) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn runtime_with_singleton_helicity_schedule( + structural_zero: bool, + ) -> (ExecutionRuntime, BTreeSet) { + let mut runtime = super::super::tests::empty_generic_runtime(); + let physics = Arc::new(super::super::tests::test_physics_runtime("full")); + let selected_helicity_id = physics.manifest.helicities[0].id.clone(); + let selected_helicity_ids = BTreeSet::from([selected_helicity_id.clone()]); + runtime.physics = Some(physics); + runtime.compiled_helicity_execution_plan = Some(CompiledHelicityExecutionPlan { + schedules_by_physical_helicity: vec![ + Some(Arc::new(CompiledHelicitySelectorSchedule { + selector_domain_id: 0, + physical_helicity_index: 0, + selected_helicity_ids: BTreeSet::from([selected_helicity_id]), + source_states: Vec::new(), + active_stage_chunk_indices: Vec::new(), + active_amplitude_chunk_indices: Vec::new(), + root_factors: Vec::new(), + structural_zero, + })), + None, + ], + }); + (runtime, selected_helicity_ids) + } + + #[test] + fn representative_helicity_is_permuted_into_public_alias_order() { + let values = + permute_representative_helicity_to_public_alias(&[1, -1, 0, -1, 1], &[0, 1, 3, 4, 2]) + .unwrap(); + assert_eq!(values, vec![1, -1, 1, 0, -1]); + } + + #[test] + fn invalid_public_alias_permutation_is_rejected() { + let error = + permute_representative_helicity_to_public_alias(&[1, -1, 0], &[0, 1, 1]).unwrap_err(); + assert!( + error + .to_string() + .contains("complete helicity-label permutation") + ); + } + + #[test] + fn global_flip_domains_execute_their_retained_partner_routes() { + let routes = vec![ + HelicityMaterializedAmplitudeRouteRuntime { + materialized_root_id: 0, + selector_domain_ids: vec![2, 7], + factor: [1.0, 0.0], + residual: false, + }, + HelicityMaterializedAmplitudeRouteRuntime { + materialized_root_id: 1, + selector_domain_ids: vec![3, 6], + factor: [1.0, 0.0], + residual: false, + }, + ]; + + let execution = helicity_execution_domain_by_domain(8, &routes).unwrap(); + + assert_eq!(execution[2], 2); + assert_eq!(execution[7], 2); + assert_eq!(execution[3], 3); + assert_eq!(execution[6], 3); + } + + #[test] + fn replay_owners_only_bypass_the_helicity_sum_through_nested_physical_runtimes() { + let (mut runtime, selected_helicity_ids) = runtime_with_singleton_helicity_schedule(false); + assert!( + runtime + .has_safe_singleton_helicity_execution(Some(&selected_helicity_ids), None) + .unwrap() + ); + + runtime.color_topology_replay_enabled = true; + assert!( + !runtime + .has_safe_singleton_helicity_execution(Some(&selected_helicity_ids), None) + .unwrap() + ); + + runtime + .helicity_selector_runtimes + .push(Box::new(super::super::tests::empty_generic_runtime())); + runtime + .helicity_selector_runtime_schedule_modes + .push(HelicitySelectorScheduleMode::NestedRuntime); + runtime.helicity_selector_lane_by_domain.insert(0, 0); + assert!( + runtime + .has_safe_singleton_helicity_execution(Some(&selected_helicity_ids), None) + .unwrap() + ); + + runtime.helicity_selector_runtime_schedule_modes[0] = + HelicitySelectorScheduleMode::ParentClosure; + assert!( + !runtime + .has_safe_singleton_helicity_execution(Some(&selected_helicity_ids), None) + .unwrap() + ); + } + + #[test] + fn structural_zero_singletons_are_safe_without_physical_replay() { + let (mut runtime, selected_helicity_ids) = runtime_with_singleton_helicity_schedule(true); + runtime.color_topology_replay_enabled = true; + + assert!( + runtime + .has_safe_singleton_helicity_execution(Some(&selected_helicity_ids), None) + .unwrap() + ); + assert!( + !runtime + .has_safe_singleton_helicity_execution(None, None) + .unwrap() + ); + assert!( + !runtime + .has_safe_singleton_helicity_execution( + Some(&BTreeSet::from([ + "hel:+-".to_string(), + "hel:-+".to_string(), + ])), + None, + ) + .unwrap() + ); + } + + #[test] + fn selected_lc_flows_keep_their_compact_replay_even_with_a_helicity_companion() { + let (mut runtime, selected_helicity_ids) = runtime_with_singleton_helicity_schedule(false); + runtime.physics = Some(Arc::new(super::super::tests::test_physics_runtime("lc"))); + runtime.lc_topology_replay_enabled = true; + runtime + .helicity_selector_runtimes + .push(Box::new(super::super::tests::empty_generic_runtime())); + runtime + .helicity_selector_runtime_schedule_modes + .push(HelicitySelectorScheduleMode::NestedRuntime); + runtime.helicity_selector_lane_by_domain.insert(0, 0); + + // Both the computed flow and a nontrivial replay alias must be mapped + // before selecting the helicity; neither belongs to the all-flow lane. + for flow_id in ["flow:0", "flow:1"] { + assert!( + !runtime + .has_safe_singleton_helicity_execution( + Some(&selected_helicity_ids), + Some(&BTreeSet::from([flow_id.to_string()])), + ) + .unwrap() + ); + } + assert!( + runtime + .has_safe_singleton_helicity_execution(Some(&selected_helicity_ids), None) + .unwrap() + ); + assert!( + runtime + .has_safe_singleton_helicity_execution( + Some(&selected_helicity_ids), + Some(&BTreeSet::from([ + "flow:0".to_string(), + "flow:1".to_string() + ])), + ) + .unwrap() + ); + assert!( + !runtime + .has_safe_singleton_helicity_execution( + None, + Some(&BTreeSet::from(["flow:0".to_string()])), + ) + .unwrap() + ); + } + + #[test] + fn contracted_color_and_structural_zero_requests_keep_the_singleton_shortcut() { + for accuracy in ["full", "nlc"] { + let (mut runtime, selected_helicity_ids) = + runtime_with_singleton_helicity_schedule(false); + runtime.physics = Some(Arc::new(super::super::tests::test_physics_runtime( + accuracy, + ))); + assert!( + runtime + .has_safe_singleton_helicity_execution( + Some(&selected_helicity_ids), + Some(&BTreeSet::from(["contracted".to_string()])), + ) + .unwrap() + ); + } + + let (mut runtime, selected_helicity_ids) = runtime_with_singleton_helicity_schedule(true); + runtime.physics = Some(Arc::new(super::super::tests::test_physics_runtime("lc"))); + runtime.lc_topology_replay_enabled = true; + assert!( + runtime + .has_safe_singleton_helicity_execution( + Some(&selected_helicity_ids), + Some(&BTreeSet::from(["flow:1".to_string()])), + ) + .unwrap() + ); + } +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/engine/model_parameters.rs b/pyamplicol/rust/crates/rusticol-core/src/engine/model_parameters.rs new file mode 100644 index 0000000000..0fe9425744 --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/engine/model_parameters.rs @@ -0,0 +1,384 @@ +// SPDX-License-Identifier: 0BSD + +use super::*; + +struct RuntimeParameterStateSnapshot { + values: Vec, + particle_masses: BTreeMap, + normalization_factor: f64, + helicity_sum: Option>, + helicity_selectors: Vec, + color_selectors: BTreeMap, +} + +pub(super) fn parse_complex_parameter_overrides( + text: &str, + path: &Path, +) -> RusticolResult> { + let value = serde_json::from_str::(text).map_err(|err| { + RusticolError::invalid_argument(format!( + "could not parse model-parameter JSON {}: {err}", + path.display() + )) + })?; + let object = value.as_object().ok_or_else(|| { + RusticolError::invalid_argument(format!( + "model-parameter JSON {} must contain an object", + path.display() + )) + })?; + let mut overrides = BTreeMap::new(); + for (name, value) in object { + let (real, imaginary) = if let Some(real) = value.as_f64() { + (real, 0.0) + } else { + let components = value.as_array().ok_or_else(|| { + RusticolError::invalid_argument(format!( + "model-parameter JSON entry {name:?} must be a real number or [real, imaginary]" + )) + })?; + if components.len() != 2 { + return Err(RusticolError::invalid_argument(format!( + "model-parameter JSON entry {name:?} must have exactly two components" + ))); + } + let real = components[0].as_f64().ok_or_else(|| { + RusticolError::invalid_argument(format!( + "model-parameter JSON entry {name:?} has a non-numeric real component" + )) + })?; + let imaginary = components[1].as_f64().ok_or_else(|| { + RusticolError::invalid_argument(format!( + "model-parameter JSON entry {name:?} has a non-numeric imaginary component" + )) + })?; + (real, imaginary) + }; + if !real.is_finite() || !imaginary.is_finite() { + return Err(RusticolError::invalid_argument(format!( + "model-parameter JSON entry {name:?} must contain finite values" + ))); + } + overrides.insert(name.clone(), (real, imaginary)); + } + Ok(overrides) +} + +impl ExecutionRuntime { + pub(super) fn current_model_parameter_value(&self, name: &str) -> RusticolResult<(f64, f64)> { + let mut real = None; + let mut imaginary = 0.0; + for parameter in &self.model_parameters { + if parameter.runtime_name.as_deref().unwrap_or(¶meter.name) != name { + continue; + } + let value = self + .model_parameter_values_f64 + .get(parameter.parameter_index) + .copied() + .ok_or_else(|| RusticolError::integrity("model-parameter value slot is absent"))?; + match parameter.complex_component.as_deref() { + Some("imag") => imaginary = value, + Some("real") | None => real = Some(value), + Some(_) => return Err(RusticolError::integrity("invalid parameter component")), + } + } + real.map(|value| (value, imaginary)).ok_or_else(|| { + RusticolError::model_parameter(format!("model parameter {name:?} has no runtime value")) + }) + } + + pub(super) fn apply_model_parameter_overrides( + &mut self, + overrides: &BTreeMap, + ) -> RusticolResult<()> { + let snapshot = self.parameter_state_snapshot(); + if let Err(error) = self.apply_model_parameter_overrides_recursive(overrides) { + self.restore_parameter_state(snapshot); + return Err(error); + } + Ok(()) + } + + fn apply_model_parameter_overrides_recursive( + &mut self, + overrides: &BTreeMap, + ) -> RusticolResult<()> { + self.apply_model_parameter_overrides_local(overrides)?; + if let Some(sum_runtime) = self.helicity_sum_runtime.as_mut() { + sum_runtime.apply_model_parameter_overrides_recursive(overrides)?; + } + for selector_runtime in &mut self.helicity_selector_runtimes { + selector_runtime.apply_model_parameter_overrides_recursive(overrides)?; + } + for selector_runtime in self.color_selector_runtimes.values_mut() { + selector_runtime.apply_model_parameter_overrides_recursive(overrides)?; + } + Ok(()) + } + + fn parameter_state_snapshot(&self) -> RuntimeParameterStateSnapshot { + RuntimeParameterStateSnapshot { + values: self.model_parameter_values_f64.clone(), + particle_masses: self.particle_masses.clone(), + normalization_factor: self.normalization_factor, + helicity_sum: self + .helicity_sum_runtime + .as_deref() + .map(ExecutionRuntime::parameter_state_snapshot) + .map(Box::new), + helicity_selectors: self + .helicity_selector_runtimes + .iter() + .map(|runtime| runtime.parameter_state_snapshot()) + .collect(), + color_selectors: self + .color_selector_runtimes + .iter() + .map(|(sector_id, runtime)| (*sector_id, runtime.parameter_state_snapshot())) + .collect(), + } + } + + fn restore_parameter_state(&mut self, snapshot: RuntimeParameterStateSnapshot) { + self.model_parameter_values_f64 = snapshot.values; + self.particle_masses = snapshot.particle_masses; + self.normalization_factor = snapshot.normalization_factor; + if let (Some(runtime), Some(snapshot)) = + (self.helicity_sum_runtime.as_mut(), snapshot.helicity_sum) + { + runtime.restore_parameter_state(*snapshot); + } + for (runtime, lane_snapshot) in self + .helicity_selector_runtimes + .iter_mut() + .zip(snapshot.helicity_selectors) + { + runtime.restore_parameter_state(lane_snapshot); + } + for (sector_id, lane_snapshot) in snapshot.color_selectors { + if let Some(runtime) = self.color_selector_runtimes.get_mut(§or_id) { + runtime.restore_parameter_state(lane_snapshot); + } + } + } + + fn apply_model_parameter_overrides_local( + &mut self, + overrides: &BTreeMap, + ) -> RusticolResult<()> { + let mut proposed = self.model_parameter_values_f64.clone(); + for (name, (real, imaginary)) in overrides { + if self.model_parameters.iter().any(|parameter| { + parameter.kind == "derived_parameter_component" + && parameter.runtime_name.as_deref().unwrap_or(¶meter.name) == name + }) { + return Err(RusticolError::invalid_argument(format!( + "derived model parameter {name:?} is immutable", + ))); + } + let Some(slots) = self.model_parameter_runtime_slots.get(name).copied() else { + return Err(RusticolError::invalid_argument(format!( + "model-parameter override {name:?} is not used by process {}", + self.process + ))); + }; + if slots.real >= self.model_parameter_values_f64.len() + || !real.is_finite() + || !imaginary.is_finite() + { + return Err(RusticolError::invalid_argument(format!( + "model-parameter override {name:?} has invalid value [{real}, {imaginary}]", + ))); + } + proposed[slots.real] = *real; + if let Some(index) = slots.imaginary { + if index >= self.model_parameter_values_f64.len() { + return Err(RusticolError::invalid_argument(format!( + "model-parameter override {name:?} has an invalid imaginary slot" + ))); + } + proposed[index] = *imaginary; + } else if *imaginary != 0.0 { + return Err(RusticolError::invalid_argument(format!( + "real model parameter {name:?} cannot receive a nonzero imaginary component" + ))); + } + } + refresh_derived_model_parameter_values( + self.model_parameter_evaluator.as_mut(), + &mut proposed, + )?; + let proposed_masses = self.particle_mass_parameters_for(&proposed); + validate_particle_mass_class_stability(&self.particle_masses, &proposed_masses)?; + self.model_parameter_values_f64 = proposed; + self.particle_masses = proposed_masses; + self.refresh_normalization_factor(); + Ok(()) + } + + pub(super) fn refresh_derived_model_parameters(&mut self) -> RusticolResult<()> { + refresh_derived_model_parameter_values( + self.model_parameter_evaluator.as_mut(), + &mut self.model_parameter_values_f64, + ) + } + + fn particle_mass_parameters_for(&self, values: &[f64]) -> BTreeMap { + let mut particle_masses = self.particle_masses.clone(); + for parameter in &self.model_parameters { + if parameter.kind == "particle_mass" + && let Some(pdg) = parameter.pdg + && let Some(value) = values.get(parameter.parameter_index) + { + particle_masses.insert(pdg, *value); + } + } + for (pdg, name) in &self.particle_mass_parameter_names { + let Some(slots) = self.model_parameter_runtime_slots.get(name) else { + continue; + }; + if let Some(value) = values.get(slots.real) { + particle_masses.insert(*pdg, *value); + } + } + particle_masses + } + + pub(super) fn refresh_normalization_factor(&mut self) { + let alpha_s = self + .model_parameter_name_to_index + .get("normalization.alpha_s_me_check") + .and_then(|index| self.model_parameter_values_f64.get(*index)) + .copied(); + let alpha_ew = self + .model_parameter_name_to_index + .get("normalization.alpha_ew") + .and_then(|index| self.model_parameter_values_f64.get(*index)) + .copied(); + let mut global_coupling_factor = 1.0; + if self.normalization_qcd_coupling_power > 0 { + let Some(alpha_s) = alpha_s else { + return; + }; + global_coupling_factor *= (4.0 * std::f64::consts::PI * alpha_s) + .powi(self.normalization_qcd_coupling_power as i32); + } + if self.normalization_electroweak_coupling_power > 0 { + let Some(alpha_ew) = alpha_ew else { + return; + }; + global_coupling_factor *= (2.0 * 4.0 * std::f64::consts::PI * alpha_ew) + .powi(self.normalization_electroweak_coupling_power as i32); + } + self.normalization_factor = self.normalization_color_factor * global_coupling_factor + / (self.normalization_average_factor * self.normalization_identical_factor); + } +} + +pub(super) fn refresh_derived_model_parameter_values( + runtime: Option<&mut ModelParameterEvaluatorRuntime>, + values: &mut [f64], +) -> RusticolResult<()> { + let Some(runtime) = runtime else { + return Ok(()); + }; + let parameters = runtime + .input_parameter_indices + .iter() + .map(|index| { + values + .get(*index) + .copied() + .map(|value| c64(value, 0.0)) + .ok_or_else(|| { + RusticolError::invalid_argument(format!( + "model-parameter evaluator input index {index} is out of range" + )) + }) + }) + .collect::>>()?; + let evaluated = runtime.evaluator.evaluate_single_row(¶meters)?; + for output in &runtime.outputs { + let value = evaluated.get(output.output_index).ok_or_else(|| { + RusticolError::invalid_argument(format!( + "model-parameter evaluator output {} for {:?} is absent", + output.output_index, output.runtime_name + )) + })?; + let Some(real) = values.get_mut(output.real_parameter_index) else { + return Err(RusticolError::invalid_argument(format!( + "derived model-parameter real slot {} is out of range", + output.real_parameter_index + ))); + }; + *real = value.re; + if let Some(index) = output.imag_parameter_index { + let imaginary = values.get_mut(index).ok_or_else(|| { + RusticolError::invalid_argument(format!( + "derived model-parameter imaginary slot {index} is out of range" + )) + })?; + *imaginary = value.im; + } + } + Ok(()) +} + +fn validate_particle_mass_class_stability( + previous: &BTreeMap, + proposed: &BTreeMap, +) -> RusticolResult<()> { + for (pdg, previous_mass) in previous { + let Some(proposed_mass) = proposed.get(pdg) else { + continue; + }; + if (*previous_mass == 0.0) != (*proposed_mass == 0.0) { + return Err(RusticolError::invalid_argument(format!( + "model-parameter update changes particle {pdg} from a {} to a {} mass class; regenerate the process artifact for that mass class", + if *previous_mass == 0.0 { + "massless" + } else { + "massive" + }, + if *proposed_mass == 0.0 { + "massless" + } else { + "massive" + }, + ))); + } + } + Ok(()) +} + +#[cfg(test)] +mod parameter_json_tests { + use super::parse_complex_parameter_overrides; + use std::path::Path; + + #[test] + fn parameter_json_accepts_real_scalars_and_complex_pairs() { + let parsed = parse_complex_parameter_overrides( + r#"{"aS": 0.117, "complex": [2.5, -0.25]}"#, + Path::new("parameters.json"), + ) + .expect("parameter card should parse"); + + assert_eq!(parsed.get("aS"), Some(&(0.117, 0.0))); + assert_eq!(parsed.get("complex"), Some(&(2.5, -0.25))); + } + + #[test] + fn parameter_json_rejects_non_numeric_values() { + let error = + parse_complex_parameter_overrides(r#"{"aS": "0.117"}"#, Path::new("parameters.json")) + .expect_err("string parameter must fail"); + + assert!( + error + .to_string() + .contains("must be a real number or [real, imaginary]") + ); + } +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/engine/momentum.rs b/pyamplicol/rust/crates/rusticol-core/src/engine/momentum.rs new file mode 100644 index 0000000000..e87fc15448 --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/engine/momentum.rs @@ -0,0 +1,498 @@ +// SPDX-License-Identifier: 0BSD + +use super::*; + +#[derive(Clone, Copy)] +enum F64MomentumBatchStorage<'a> { + Contiguous(&'a [f64]), + Nested(&'a [Vec<[f64; 4]>]), +} + +/// Borrowed row-major momentum input used by the ordinary f64 lane. +/// +/// Contiguous inputs have layout `[point][external][E, px, py, pz]`. An +/// optional crossing lookup is validated and normalized when the runtime is +/// loaded, so individual momentum reads need no allocation or map search. +#[derive(Clone, Copy)] +pub(super) struct F64MomentumBatchView<'a> { + storage: F64MomentumBatchStorage<'a>, + point_count: usize, + external_count: usize, + crossing_lookup: Option<&'a [InputCrossingMapEntry]>, +} + +#[derive(Clone, Copy)] +pub(super) enum F64MomentumPointView<'a> { + ContiguousIdentity { + values: &'a [f64], + external_count: usize, + }, + ContiguousCrossed { + values: &'a [f64], + external_count: usize, + crossing_lookup: &'a [InputCrossingMapEntry], + }, + Nested(&'a [[f64; 4]]), +} + +pub(super) trait F64MomentumPoint { + fn momentum(&self, external_index: usize) -> Option<[f64; 4]>; +} + +impl F64MomentumPoint for F64MomentumPointView<'_> { + #[inline(always)] + fn momentum(&self, external_index: usize) -> Option<[f64; 4]> { + match self { + Self::ContiguousIdentity { + values, + external_count, + } => { + if external_index >= *external_count { + return None; + } + let start = external_index * 4; + Some([ + values[start], + values[start + 1], + values[start + 2], + values[start + 3], + ]) + } + Self::ContiguousCrossed { + values, + external_count, + crossing_lookup, + } => { + if external_index >= *external_count { + return None; + } + let entry = &crossing_lookup[external_index]; + debug_assert_eq!(entry.target_index, external_index); + let start = entry.source_index * 4; + Some([ + entry.sign * values[start], + entry.sign * values[start + 1], + entry.sign * values[start + 2], + entry.sign * values[start + 3], + ]) + } + Self::Nested(values) => values.get(external_index).copied(), + } + } +} + +impl F64MomentumPoint for [[f64; 4]] { + #[inline(always)] + fn momentum(&self, external_index: usize) -> Option<[f64; 4]> { + self.get(external_index).copied() + } +} + +impl F64MomentumPoint for Vec<[f64; 4]> { + #[inline(always)] + fn momentum(&self, external_index: usize) -> Option<[f64; 4]> { + self.get(external_index).copied() + } +} + +impl F64MomentumPoint for [[f64; 4]; N] { + #[inline(always)] + fn momentum(&self, external_index: usize) -> Option<[f64; 4]> { + self.get(external_index).copied() + } +} + +impl<'a> F64MomentumBatchView<'a> { + pub(super) fn from_contiguous_prevalidated( + values: &'a [f64], + point_count: usize, + external_count: usize, + crossing_lookup: Option<&'a [InputCrossingMapEntry]>, + ) -> RusticolResult { + if point_count == 0 { + return Err(RusticolError::invalid_argument( + "point_count must be positive", + )); + } + let values_per_point = external_count + .checked_mul(4) + .ok_or_else(|| RusticolError::invalid_argument("momentum shape overflow"))?; + let expected = point_count + .checked_mul(values_per_point) + .ok_or_else(|| RusticolError::invalid_argument("momentum shape overflow"))?; + if values.len() != expected { + return Err(RusticolError::invalid_argument(format!( + "momenta contain {} values, expected {expected} for shape ({point_count}, {external_count}, 4)", + values.len() + ))); + } + if crossing_lookup.is_some_and(|lookup| lookup.len() != external_count) { + return Err(RusticolError::integrity( + "prevalidated input crossing lookup has an inconsistent length", + )); + } + Ok(Self { + storage: F64MomentumBatchStorage::Contiguous(values), + point_count, + external_count, + crossing_lookup, + }) + } + + pub(super) fn from_nested( + values: &'a [Vec<[f64; 4]>], + external_count: usize, + ) -> RusticolResult { + if values.is_empty() { + return Err(RusticolError::invalid_argument( + "point_count must be positive", + )); + } + if let Some(point) = values.iter().find(|point| point.len() != external_count) { + return Err(RusticolError::invalid_argument(format!( + "momentum point contains {} external legs, expected {external_count}", + point.len() + ))); + } + Ok(Self { + storage: F64MomentumBatchStorage::Nested(values), + point_count: values.len(), + external_count, + crossing_lookup: None, + }) + } + + pub(super) fn point_count(self) -> usize { + self.point_count + } + + pub(super) fn external_count(self) -> usize { + self.external_count + } + + /// Execute one term of an already-generated momentum sum into a point + /// plane. Resolve the public input layout and crossing once per term, not + /// once per point. `ADD = false` starts the sum with the same positive + /// zero as the scalar momentum accumulator, including for signed zeros. + #[inline] + pub(super) fn accumulate_component_into( + self, + external_index: usize, + component: usize, + coefficient: f64, + output: &mut [f64], + ) -> RusticolResult<()> { + if external_index >= self.external_count + || component >= 4 + || output.len() != self.point_count + { + return Err(RusticolError::integrity( + "momentum component plane has an invalid input or output range", + )); + } + #[inline(always)] + fn store(output: &mut f64, coefficient: f64, value: f64) { + let previous = if ADD { *output } else { 0.0 }; + *output = previous + coefficient * value; + } + match self.storage { + F64MomentumBatchStorage::Contiguous(values) => { + let point_stride = self.external_count * 4; + if let Some(crossing) = self.crossing_lookup { + let entry = &crossing[external_index]; + let input_start = entry.source_index * 4 + component; + for (target, &value) in output + .iter_mut() + .zip(values[input_start..].iter().step_by(point_stride)) + { + // Keep both multiplications in their scalar order; + // do not combine the crossing and sum coefficients. + store::(target, coefficient, entry.sign * value); + } + } else { + let input_start = external_index * 4 + component; + for (target, &value) in output + .iter_mut() + .zip(values[input_start..].iter().step_by(point_stride)) + { + store::(target, coefficient, value); + } + } + } + F64MomentumBatchStorage::Nested(values) => { + for (target, point) in output.iter_mut().zip(values) { + store::(target, coefficient, point[external_index][component]); + } + } + } + Ok(()) + } + + #[inline(always)] + pub(super) fn point(self, point_index: usize) -> F64MomentumPointView<'a> { + assert!(point_index < self.point_count); + match self.storage { + F64MomentumBatchStorage::Contiguous(values) => { + let values_per_point = self.external_count * 4; + let start = point_index * values_per_point; + let values = &values[start..start + values_per_point]; + match self.crossing_lookup { + Some(crossing_lookup) => F64MomentumPointView::ContiguousCrossed { + values, + external_count: self.external_count, + crossing_lookup, + }, + None => F64MomentumPointView::ContiguousIdentity { + values, + external_count: self.external_count, + }, + } + } + F64MomentumBatchStorage::Nested(values) => { + F64MomentumPointView::Nested(values[point_index].as_slice()) + } + } + } + + pub(super) fn subview(self, start: usize, end: usize) -> RusticolResult { + if start >= end || end > self.point_count { + return Err(RusticolError::invalid_argument( + "momentum batch subview has an invalid point range", + )); + } + match self.storage { + F64MomentumBatchStorage::Contiguous(values) => { + let values_per_point = self.external_count * 4; + Self::from_contiguous_prevalidated( + &values[start * values_per_point..end * values_per_point], + end - start, + self.external_count, + self.crossing_lookup, + ) + } + F64MomentumBatchStorage::Nested(values) => { + Self::from_nested(&values[start..end], self.external_count) + } + } + } + + pub(super) fn materialize_nested(self) -> Vec> { + (0..self.point_count) + .map(|point_index| { + let point = self.point(point_index); + (0..self.external_count) + .map(|external_index| { + point + .momentum(external_index) + .expect("validated momentum view covers every external leg") + }) + .collect() + }) + .collect() + } +} + +pub(super) fn prevalidate_input_crossing_lookup( + expected_legs: usize, + input_crossing_map: Option>, +) -> RusticolResult>> { + let Some(mut map) = input_crossing_map else { + return Ok(None); + }; + if map.len() != expected_legs { + return Err(RusticolError::invalid_argument(format!( + "input crossing map has {} entries, expected {expected_legs}", + map.len() + ))); + } + map.sort_unstable_by_key(|entry| entry.target_index); + for (target_index, entry) in map.iter().enumerate() { + if entry.target_index != target_index || entry.source_index >= expected_legs { + return Err(RusticolError::invalid_argument( + "input crossing map is not a complete in-range target lookup", + )); + } + } + Ok(Some(map)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn contiguous_view_matches_materialized_crossing_and_subviews() { + let values = [ + 1.0, 2.0, 3.0, 4.0, 10.0, 20.0, 30.0, 40.0, 100.0, 200.0, 300.0, 400.0, 5.0, 6.0, 7.0, + 8.0, 50.0, 60.0, 70.0, 80.0, 500.0, 600.0, 700.0, 800.0, + ]; + let lookup = prevalidate_input_crossing_lookup( + 3, + Some(vec![ + InputCrossingMapEntry { + target_index: 2, + source_index: 0, + sign: -1.0, + }, + InputCrossingMapEntry { + target_index: 0, + source_index: 2, + sign: 1.0, + }, + InputCrossingMapEntry { + target_index: 1, + source_index: 1, + sign: 1.0, + }, + ]), + ) + .unwrap() + .unwrap(); + let view = F64MomentumBatchView::from_contiguous_prevalidated(&values, 2, 3, Some(&lookup)) + .unwrap(); + let nested = vec![ + vec![ + [1.0, 2.0, 3.0, 4.0], + [10.0, 20.0, 30.0, 40.0], + [100.0, 200.0, 300.0, 400.0], + ], + vec![ + [5.0, 6.0, 7.0, 8.0], + [50.0, 60.0, 70.0, 80.0], + [500.0, 600.0, 700.0, 800.0], + ], + ]; + let expected = apply_input_crossing_map(nested, 3, Some(&lookup)).unwrap(); + + assert_eq!(view.materialize_nested(), expected); + assert_eq!( + view.point(0).momentum(0), + Some([100.0, 200.0, 300.0, 400.0]) + ); + assert_eq!(view.point(0).momentum(2), Some([-1.0, -2.0, -3.0, -4.0])); + assert_eq!( + view.subview(1, 2).unwrap().materialize_nested(), + expected[1..] + ); + } + + #[test] + fn contiguous_view_rejects_invalid_shapes() { + assert!(F64MomentumBatchView::from_contiguous_prevalidated(&[], 0, 2, None).is_err()); + assert!(F64MomentumBatchView::from_contiguous_prevalidated(&[0.0; 7], 1, 2, None).is_err()); + } + + #[test] + fn component_plane_accumulation_matches_scalar_bits_for_every_layout() { + let numbers = [ + 0.0, + -0.0, + 1.0, + -1.0, + f64::MAX, + f64::MIN_POSITIVE, + 1e16, + -1e16, + ]; + let nested = (0..19) + .map(|point| { + (0..3) + .map(|external| { + std::array::from_fn(|component| { + numbers[(point + external * 3 + component) % numbers.len()] + }) + }) + .collect::>() + }) + .collect::>(); + let flat = nested + .iter() + .flatten() + .flatten() + .copied() + .collect::>(); + let crossing = [ + InputCrossingMapEntry { + target_index: 0, + source_index: 2, + sign: 2.0, + }, + InputCrossingMapEntry { + target_index: 1, + source_index: 0, + sign: -1.0, + }, + InputCrossingMapEntry { + target_index: 2, + source_index: 1, + sign: 0.5, + }, + ]; + let views = [ + F64MomentumBatchView::from_nested(&nested, 3).unwrap(), + F64MomentumBatchView::from_contiguous_prevalidated(&flat, 19, 3, None).unwrap(), + F64MomentumBatchView::from_contiguous_prevalidated(&flat, 19, 3, Some(&crossing)) + .unwrap(), + ]; + for view in views { + for count in [1, 3, 17] { + let batch = view.subview(1, 1 + count).unwrap(); + for external in 0..3 { + for component in 0..4 { + for coefficient in [1.0, -1.0, 0.25, -0.0] { + let mut output = vec![23.0; count + 5]; + let pointer = output.as_ptr(); + batch + .accumulate_component_into::( + external, + component, + coefficient, + &mut output[..count], + ) + .unwrap(); + for (point, &actual) in output[..count].iter().enumerate() { + let value = + batch.point(point).momentum(external).unwrap()[component]; + let expected = 0.0 + coefficient * value; + assert_eq!(actual.to_bits(), expected.to_bits()); + } + let previous = output[..count].to_vec(); + batch + .accumulate_component_into::( + external, + component, + coefficient, + &mut output[..count], + ) + .unwrap(); + for (point, &actual) in output[..count].iter().enumerate() { + let value = + batch.point(point).momentum(external).unwrap()[component]; + let expected = previous[point] + coefficient * value; + assert_eq!(actual.to_bits(), expected.to_bits()); + } + assert_eq!(output.as_ptr(), pointer); + assert!(output[count..].iter().all(|&value| value == 23.0)); + } + } + } + } + } + } + + #[test] + fn component_plane_accumulation_rejects_invalid_ranges() { + let values = [0.0; 12]; + let batch = + F64MomentumBatchView::from_contiguous_prevalidated(&values, 1, 3, None).unwrap(); + for (external, component, len) in [(3, 0, 1), (0, 4, 1), (0, 0, 0), (0, 0, 2)] { + let mut output = vec![23.0; len]; + assert!( + batch + .accumulate_component_into::(external, component, 1.0, &mut output,) + .is_err() + ); + assert!(output.iter().all(|&value| value == 23.0)); + } + } +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/engine/native_runtime.rs b/pyamplicol/rust/crates/rusticol-core/src/engine/native_runtime.rs new file mode 100644 index 0000000000..40c931f570 --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/engine/native_runtime.rs @@ -0,0 +1,5168 @@ +// SPDX-License-Identifier: 0BSD + +use super::*; + +use std::borrow::Cow; +use std::sync::OnceLock; + +#[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] +#[path = "on_the_fly_cache.rs"] +mod cache; + +enum DeferredProcessPhysicsV1 { + #[expect( + dead_code, + reason = "retained for the lazy dense-metadata compatibility path" + )] + Dense { + artifact: Box, + selection: crate::ArtifactSelection, + }, + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + OnTheFly { + metadata: super::on_the_fly_public_metadata::OnTheFlyPublicMetadataV1, + selectors: super::on_the_fly_selectors::OnTheFlyCompactSelectorAdapterV1, + selection: Box, + }, +} + +/// Dense public process metadata is deliberately not part of ordinary +/// on-the-fly load/evaluation. Existing explicit metadata accessors retain +/// their API and initialize this cell on first use. +pub(super) struct LazyProcessPhysicsV1 { + value: OnceLock>, + deferred: Option, +} + +impl LazyProcessPhysicsV1 { + pub(super) fn loaded(value: ProcessPhysicsV1) -> Self { + let cell = OnceLock::new(); + cell.set(Ok(value)) + .expect("fresh process-physics cell must be empty"); + Self { + value: cell, + deferred: None, + } + } + + #[cfg(test)] + pub(super) const fn unavailable() -> Self { + Self { + value: OnceLock::new(), + deferred: None, + } + } + + #[expect( + dead_code, + reason = "retained for the lazy dense-metadata compatibility path" + )] + fn deferred(artifact: VerifiedArtifact, selection: crate::ArtifactSelection) -> Self { + Self { + value: OnceLock::new(), + deferred: Some(DeferredProcessPhysicsV1::Dense { + artifact: Box::new(artifact), + selection, + }), + } + } + + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + pub(super) fn deferred_on_the_fly( + metadata: super::on_the_fly_public_metadata::OnTheFlyPublicMetadataV1, + selectors: super::on_the_fly_selectors::OnTheFlyCompactSelectorAdapterV1, + selection: crate::ArtifactSelection, + ) -> Self { + Self { + value: OnceLock::new(), + deferred: Some(DeferredProcessPhysicsV1::OnTheFly { + metadata, + selectors, + selection: Box::new(selection), + }), + } + } + + pub(super) fn get(&self) -> RusticolResult<&ProcessPhysicsV1> { + let result = self.value.get_or_init(|| { + let deferred = self.deferred.as_ref().ok_or_else(|| { + RusticolError::internal("process-physics cell has no value or deferred source") + })?; + match deferred { + DeferredProcessPhysicsV1::Dense { + artifact, + selection, + } => { + let bytes = artifact.read_payload(&selection.process.physics_path)?; + let physics = + ProcessPhysicsV1::from_json(&bytes, &selection.process.physics_path)?; + validate_representative_physics(&physics, selection)?; + public_process_physics(physics, selection) + } + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + DeferredProcessPhysicsV1::OnTheFly { + metadata, + selectors, + selection, + } => { + let physics = metadata.synthesize(selectors)?; + validate_representative_physics(&physics, selection)?; + public_process_physics(physics, selection) + } + } + }); + result.as_ref().map_err(Clone::clone) + } +} + +/// Public metadata of `selection`: the representative physics remapped to the +/// requested order and, for a process-group member, relabelled as the member. +fn public_process_physics( + physics: ProcessPhysicsV1, + selection: &crate::ArtifactSelection, +) -> RusticolResult { + if !selection.remaps_public_process() { + return Ok(physics); + } + let physics = apply_process_permutation_metadata(physics, selection)?; + if selection.process_group.is_some() { + apply_process_group_metadata(&physics, selection) + } else { + Ok(physics) + } +} + +fn process_group_routing( + selection: &crate::ArtifactSelection, + representative_key: &str, +) -> Option> { + selection.process_group.as_ref().map(|group| { + Box::new(ProcessGroupRouting { + representative_process_key: representative_key.to_string(), + factor: NativeRationalFactor { + numerator: group.factor_numerator, + denominator: group.factor_denominator, + }, + conjugated: group.conjugated, + }) + }) +} + +fn validate_representative_physics( + physics: &ProcessPhysicsV1, + selection: &crate::ArtifactSelection, +) -> RusticolResult<()> { + if physics.process_id != selection.process.id + || physics.process != selection.process.expression + || physics.color_accuracy.as_str() != selection.process.color_accuracy + || physics + .external_particles + .iter() + .map(|particle| particle.pdg) + .ne(selection.process.external_pdgs.iter().copied()) + { + return Err(RusticolError::integrity(format!( + "runtime physics payload {:?} does not match process {:?}", + selection.process.physics_path, selection.process.id + ))); + } + Ok(()) +} + +#[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] +#[derive(Clone, Debug, Eq, PartialEq, bincode::Encode, bincode::Decode)] +struct OnTheFlySelectionIdentityV1 { + helicity_ordinals: Option>, + color_ordinals: Option>, +} + +#[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] +impl OnTheFlySelectionIdentityV1 { + fn from_slices(helicity_ordinals: Option<&[usize]>, color_ordinals: Option<&[usize]>) -> Self { + Self { + helicity_ordinals: helicity_ordinals.map(|values| values.to_vec().into_boxed_slice()), + color_ordinals: color_ordinals.map(|values| values.to_vec().into_boxed_slice()), + } + } + + fn selected_axis_matches( + ordinals: Option<&[usize]>, + prepared_ids: &[String], + selected_ids: Option<&BTreeSet>, + ) -> bool { + match (ordinals, selected_ids) { + (None, None) => true, + (Some(_), Some(selected)) => { + prepared_ids.len() == selected.len() + && prepared_ids.iter().all(|id| selected.contains(id)) + } + _ => false, + } + } + + fn matches_fixed_selection( + &self, + prepared_helicity_ids: &[String], + prepared_color_ids: &[String], + selected_helicities: Option<&BTreeSet>, + selected_colors: Option<&BTreeSet>, + ) -> bool { + Self::selected_axis_matches( + self.helicity_ordinals.as_deref(), + prepared_helicity_ids, + selected_helicities, + ) && Self::selected_axis_matches( + self.color_ordinals.as_deref(), + prepared_color_ids, + selected_colors, + ) + } +} + +#[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] +#[derive(Debug)] +struct OnTheFlyPreparedSelectionV1 { + identity: OnTheFlySelectionIdentityV1, + helicity_indices: Box<[usize]>, + color_indices: Box<[usize]>, + helicity_ids: Box<[String]>, + color_ids: Box<[String]>, + query_count: usize, +} + +#[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] +struct OnTheFlyWarmUpPreparedSelectionV1 { + helicity_count: usize, + color_count: usize, + query_count: usize, + already_warm: bool, +} + +#[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] +enum OnTheFlyReductionV1 { + Lc, + Contracted(Box), +} + +type OnTheFlySelectedOrdinalsV1 = (Option>, Option>); + +#[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] +pub(super) struct OnTheFlyExecutionRuntime { + lane: super::on_the_fly_lane::OnTheFlyNativeRuntime, + selectors: super::on_the_fly_selectors::OnTheFlyCompactSelectorAdapterV1, + reduction: OnTheFlyReductionV1, + prepared_selections: Vec, + last_prepared_selection: Option, + pending_prepared_selection: Option, + point_major_scratch: Vec, +} + +#[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] +impl OnTheFlyExecutionRuntime { + pub(super) fn new( + lane: super::on_the_fly_lane::OnTheFlyNativeRuntime, + selectors: super::on_the_fly_selectors::OnTheFlyCompactSelectorAdapterV1, + color_contraction: Option, + ) -> Self { + Self { + lane, + selectors, + reduction: color_contraction.map_or(OnTheFlyReductionV1::Lc, |plan| { + OnTheFlyReductionV1::Contracted(Box::new(plan)) + }), + prepared_selections: Vec::new(), + last_prepared_selection: None, + pending_prepared_selection: None, + point_major_scratch: Vec::new(), + } + } + + pub(super) fn clear(&mut self) -> RusticolResult<()> { + self.lane.clear()?; + self.pending_prepared_selection = None; + self.prepared_selections.clear(); + self.last_prepared_selection = None; + self.point_major_scratch = Vec::new(); + Ok(()) + } + + fn uses_contracted_color(&self) -> bool { + matches!(&self.reduction, OnTheFlyReductionV1::Contracted(_)) + } + + fn public_selector_counts(&self) -> RusticolResult<(usize, usize)> { + let selection = self.selectors.selection(self.lane.seed(), None, None)?; + Ok(( + selection.helicity_count(), + if self.uses_contracted_color() { + 1 + } else { + selection.color_count() + }, + )) + } + + #[cfg(test)] + pub(super) fn retained_family_count(&self) -> usize { + self.lane.retained_family_count() + } + + #[cfg(test)] + pub(super) fn retained_selection_count(&self) -> usize { + self.prepared_selections.len() + } + + #[cfg(test)] + pub(super) fn pending_family_count(&self) -> usize { + self.lane.pending_family_count() + } + + #[cfg(test)] + pub(super) fn fail_contracted_execution_at_for_test(&mut self, attempt: Option) { + self.lane.fail_contracted_execution_at_for_test(attempt); + } + + #[cfg(test)] + pub(super) const fn contracted_max_live_query_outcomes_for_test(&self) -> usize { + self.lane.contracted_max_live_query_outcomes_for_test() + } + + #[cfg(test)] + pub(super) fn semantic_executor_binding_count(&self) -> RusticolResult { + self.lane.semantic_executor_binding_count() + } + + #[cfg(test)] + pub(super) fn active_family_prepared_census( + &self, + ) -> Option { + self.lane.prepared_census() + } + + #[cfg(test)] + pub(super) fn point_major_scratch_state(&self) -> (usize, usize) { + ( + self.point_major_scratch.len(), + self.point_major_scratch.capacity(), + ) + } + + /// Private read-only production introspection of retained compact state. + pub(super) fn state_census(&self, process_id: &str) -> RusticolResult { + let retained = self.lane.retained_state_census(); + let active_family_union_census = self.lane.prepared_census().map(|census| { + serde_json::json!({ + "basis": "shared-query-family-union-v1", + "scope": "active-family-union", + "query_count": census.query_count, + "union_unique_current_count": census.union_unique_current_count, + "union_unique_current_component_count": ( + census.union_unique_current_component_count + ), + "union_source_rows": census.union_source_rows, + "union_contribution_rows": census.union_contribution_rows, + "union_finalization_rows": census.union_finalization_rows, + "union_closure_rows": census.union_closure_rows, + "union_amplitude_destination_count": ( + census.union_amplitude_destination_count + ), + "union_source_executor_call_groups": ( + census.union_source_executor_call_groups + ), + "union_contribution_executor_call_groups": ( + census.union_contribution_executor_call_groups + ), + "union_finalization_executor_call_groups": ( + census.union_finalization_executor_call_groups + ), + "union_closure_executor_call_groups": ( + census.union_closure_executor_call_groups + ), + }) + }); + Ok(serde_json::json!({ + "kind": "rusticol-on-the-fly-runtime-state-census-v1", + "process_id": process_id, + "family_cache_policy": "last-family-only", + "family_cache_limit": 1, + "process_preparation_count": self.lane.process_preparation_count(), + "retained_family_count": retained.family_count, + "pending_family_count": self.lane.pending_family_count(), + "retained_selection_count": self.prepared_selections.len(), + "retained_request_count": retained.request_count, + "retained_amplitude_destination_count": retained.amplitude_destination_count, + "retained_executor_handle_count": retained.executor_handle_count, + "retained_query_local_trace_count": retained.query_local_trace_count, + "retained_embedded_lookup_key_count": retained.embedded_lookup_key_count, + "semantic_executor_binding_count": self.lane.semantic_executor_binding_count()?, + "active_family_union_census": active_family_union_census, + })) + } + + fn fill_point_major(&mut self, batch: F64MomentumBatchView<'_>) -> RusticolResult { + let required = batch + .point_count() + .checked_mul(batch.external_count()) + .and_then(|value| value.checked_mul(4)) + .ok_or_else(|| { + RusticolError::invalid_argument("on-the-fly momentum shape exceeds usize") + })?; + if self.point_major_scratch.len() < required { + self.point_major_scratch + .try_reserve_exact(required - self.point_major_scratch.len()) + .map_err(|error| { + RusticolError::invalid_argument(format!( + "on-the-fly point-major workspace allocation failed: {error}" + )) + })?; + self.point_major_scratch.resize(required, 0.0); + } + for point_index in 0..batch.point_count() { + let point = batch.point(point_index); + for external_index in 0..batch.external_count() { + let momentum = point.momentum(external_index).ok_or_else(|| { + RusticolError::integrity("on-the-fly momentum view omits an external leg") + })?; + let start = (point_index * batch.external_count() + external_index) * 4; + self.point_major_scratch[start..start + 4].copy_from_slice(&momentum); + } + } + Ok(required) + } + + fn prepare_selection( + &mut self, + selected_helicities: Option<&BTreeSet>, + selected_colors: Option<&BTreeSet>, + point_count: usize, + ) -> RusticolResult<(usize, usize)> { + if matches!(&self.reduction, OnTheFlyReductionV1::Contracted(_)) + && selected_colors.is_some() + { + return Err(RusticolError::selector( + "on-the-fly contracted color does not expose a color selector", + )); + } + if let Some(shape) = + self.reuse_fixed_selection(selected_helicities, selected_colors, point_count)? + { + return Ok(shape); + } + let (helicity_ordinals, color_ordinals) = match &self.reduction { + OnTheFlyReductionV1::Lc => self + .selectors + .selected_ordinals(selected_helicities, selected_colors)?, + OnTheFlyReductionV1::Contracted(_) => { + let (helicity_ordinals, _) = self + .selectors + .selected_ordinals(selected_helicities, None)?; + (helicity_ordinals, None) + } + }; + self.prepare_selection_identity( + OnTheFlySelectionIdentityV1 { + helicity_ordinals, + color_ordinals, + }, + point_count, + ) + } + + /// Reuse the last committed fixed public selection before reparsing its + /// string IDs. The prepared public IDs already authenticate the exact + /// selector set, while the identity's `Option` markers distinguish an + /// omitted (sum-all) axis from an explicit complete-axis selection. + /// Matching therefore borrows both sets and allocates nothing on the + /// warmed ordinary-evaluation path. + fn reuse_fixed_selection( + &mut self, + selected_helicities: Option<&BTreeSet>, + selected_colors: Option<&BTreeSet>, + point_count: usize, + ) -> RusticolResult> { + let pending_matches = self + .pending_prepared_selection + .as_ref() + .is_some_and(|prepared| { + prepared.identity.matches_fixed_selection( + &prepared.helicity_ids, + &prepared.color_ids, + selected_helicities, + selected_colors, + ) + }); + let committed_index = self + .pending_prepared_selection + .is_none() + .then_some(self.last_prepared_selection) + .flatten() + .filter(|index| { + self.prepared_selections + .get(*index) + .is_some_and(|prepared| { + prepared.identity.matches_fixed_selection( + &prepared.helicity_ids, + &prepared.color_ids, + selected_helicities, + selected_colors, + ) + }) + }); + if !pending_matches && committed_index.is_none() { + return Ok(None); + } + let family_point_capacity = self.family_point_capacity(point_count)?; + self.reuse_current_queries(family_point_capacity)?; + let prepared = if pending_matches { + self.pending_prepared_selection + .as_ref() + .expect("matching pending on-the-fly selection disappeared") + } else { + &self.prepared_selections + [committed_index.expect("matching committed selection disappeared")] + }; + Ok(Some(( + prepared.helicity_indices.len(), + prepared.color_indices.len(), + ))) + } + + fn prepare_selection_from_ordinals( + &mut self, + helicity_ordinals: Option<&[usize]>, + color_ordinals: Option<&[usize]>, + point_count: usize, + ) -> RusticolResult<(usize, usize)> { + if matches!(&self.reduction, OnTheFlyReductionV1::Contracted(_)) && color_ordinals.is_some() + { + return Err(RusticolError::selector( + "on-the-fly contracted color does not expose per-point color ordinals", + )); + } + self.prepare_selection_identity( + OnTheFlySelectionIdentityV1::from_slices(helicity_ordinals, color_ordinals), + point_count, + ) + } + + fn prepare_selection_for_warm_up( + &mut self, + selected_helicities: Option<&BTreeSet>, + selected_colors: Option<&BTreeSet>, + query_construction_threads: usize, + progress: &mut super::on_the_fly_warm_up::OnTheFlyWarmUpProgress<'_>, + ) -> RusticolResult { + if self.uses_contracted_color() && selected_colors.is_some() { + return Err(RusticolError::selector( + "on-the-fly contracted color does not expose a color selector", + )); + } + let (helicity_ordinals, color_ordinals) = match &self.reduction { + OnTheFlyReductionV1::Lc => self + .selectors + .selected_ordinals(selected_helicities, selected_colors)?, + OnTheFlyReductionV1::Contracted(_) => { + let (helicity_ordinals, _) = self + .selectors + .selected_ordinals(selected_helicities, None)?; + (helicity_ordinals, None) + } + }; + self.prepare_selection_identity_impl( + OnTheFlySelectionIdentityV1 { + helicity_ordinals, + color_ordinals, + }, + 1, + Some(query_construction_threads), + Some(progress), + ) + } + + fn prepare_selection_identity( + &mut self, + identity: OnTheFlySelectionIdentityV1, + point_count: usize, + ) -> RusticolResult<(usize, usize)> { + let prepared = self.prepare_selection_identity_impl(identity, point_count, None, None)?; + Ok((prepared.helicity_count, prepared.color_count)) + } + + fn prepare_selection_identity_impl( + &mut self, + identity: OnTheFlySelectionIdentityV1, + point_count: usize, + query_construction_threads: Option, + mut progress: Option<&mut super::on_the_fly_warm_up::OnTheFlyWarmUpProgress<'_>>, + ) -> RusticolResult { + let family_point_capacity = self.family_point_capacity(point_count)?; + if self + .pending_prepared_selection + .as_ref() + .is_some_and(|prepared| prepared.identity == identity) + { + self.reuse_current_queries(family_point_capacity)?; + let prepared = self + .pending_prepared_selection + .as_ref() + .expect("matching pending on-the-fly selection disappeared"); + if let Some(progress) = progress.as_deref_mut() { + self.lane + .report_reused_family_for_warm_up(prepared.query_count, progress)?; + } + return Ok(OnTheFlyWarmUpPreparedSelectionV1 { + helicity_count: prepared.helicity_indices.len(), + color_count: prepared.color_indices.len(), + query_count: prepared.query_count, + already_warm: false, + }); + } + self.discard_pending_selection()?; + if let Some(index) = self.last_prepared_selection.filter(|index| { + self.prepared_selections + .get(*index) + .is_some_and(|prepared| prepared.identity == identity) + }) { + self.reuse_current_queries(family_point_capacity)?; + let prepared = &self.prepared_selections[index]; + if let Some(progress) = progress.as_deref_mut() { + self.lane + .report_reused_family_for_warm_up(prepared.query_count, progress)?; + } + return Ok(OnTheFlyWarmUpPreparedSelectionV1 { + helicity_count: prepared.helicity_indices.len(), + color_count: prepared.color_indices.len(), + query_count: prepared.query_count, + already_warm: true, + }); + } + + if self.prepared_selections.capacity() == 0 { + self.prepared_selections + .try_reserve_exact(1) + .map_err(|error| { + RusticolError::invalid_argument(format!( + "on-the-fly prepared-selection allocation failed: {error}" + )) + })?; + } + let selection = self.selectors.selection_from_ordinals( + self.lane.seed(), + identity.helicity_ordinals.as_deref(), + identity.color_ordinals.as_deref(), + )?; + let helicity_count = selection.helicity_count(); + let helicity_indices = (0..helicity_count) + .map(|position| selection.helicity_ordinal_at(position)) + .collect::>>()?; + let helicity_ids = (0..helicity_count) + .map(|position| selection.helicity_id_at(position)) + .collect::>>()?; + let (color_count, color_indices, color_ids, query_count) = match &self.reduction { + OnTheFlyReductionV1::Lc => { + let color_count = selection.color_count(); + let color_indices = (0..color_count) + .map(|position| selection.color_ordinal_at(position)) + .collect::>>()?; + let color_ids = (0..color_count) + .map(|position| selection.color_id_at(position)) + .collect::>>()?; + let requests = selection.iter().collect::>>()?; + if let Some(progress) = progress { + self.lane.prepare_lc_queries_for_warm_up( + &requests, + family_point_capacity, + query_construction_threads.expect("warm-up worker count is absent"), + progress, + )?; + } else { + self.lane + .prepare_lc_queries(&requests, family_point_capacity)?; + } + let query_count = requests.len(); + (color_count, color_indices, color_ids, query_count) + } + OnTheFlyReductionV1::Contracted(plan) => { + if identity.color_ordinals.is_some() { + return Err(RusticolError::selector( + "on-the-fly contracted color selection is unavailable", + )); + } + let structural_color_count = plan.destination_by_owner_ordinal.len(); + if structural_color_count != self.selectors.color_count() { + return Err(RusticolError::integrity( + "on-the-fly contracted owner basis disagrees with compact structural selectors", + )); + } + if let Some(progress) = progress { + self.lane.prepare_contracted_queries_for_warm_up( + &self.selectors, + &helicity_indices, + structural_color_count, + &plan.destination_by_owner_ordinal, + family_point_capacity, + query_construction_threads.expect("warm-up worker count is absent"), + progress, + )?; + } else { + self.lane.prepare_contracted_queries( + &self.selectors, + &helicity_indices, + structural_color_count, + &plan.destination_by_owner_ordinal, + family_point_capacity, + )?; + } + let query_count = helicity_indices + .len() + .checked_mul(structural_color_count) + .ok_or_else(|| { + RusticolError::invalid_argument( + "on-the-fly contracted query count exceeds usize", + ) + })?; + ( + 1, + vec![0], + vec!["color:contracted".to_string()], + query_count, + ) + } + }; + self.pending_prepared_selection = Some(OnTheFlyPreparedSelectionV1 { + identity, + helicity_indices: helicity_indices.into_boxed_slice(), + color_indices: color_indices.into_boxed_slice(), + helicity_ids: helicity_ids.into_boxed_slice(), + color_ids: color_ids.into_boxed_slice(), + query_count, + }); + Ok(OnTheFlyWarmUpPreparedSelectionV1 { + helicity_count, + color_count, + query_count, + already_warm: false, + }) + } + + fn family_point_capacity(&self, point_count: usize) -> RusticolResult { + let point_capacity = u32::try_from(point_count) + .map_err(|_| RusticolError::invalid_argument("on-the-fly point count exceeds u32"))?; + match &self.reduction { + OnTheFlyReductionV1::Lc => Ok(point_capacity), + OnTheFlyReductionV1::Contracted(plan) => { + u32::try_from(point_count.min(plan.point_tile_size)).map_err(|_| { + RusticolError::invalid_argument("on-the-fly contracted point tile exceeds u32") + }) + } + } + } + + fn current_prepared_selection(&self) -> RusticolResult<&OnTheFlyPreparedSelectionV1> { + self.pending_prepared_selection + .as_ref() + .or_else(|| { + self.last_prepared_selection + .and_then(|index| self.prepared_selections.get(index)) + }) + .ok_or_else(|| RusticolError::internal("on-the-fly selection was not retained")) + } + + fn discard_pending_selection(&mut self) -> RusticolResult<()> { + let aborting_prepared_contracted_selection = self.pending_prepared_selection.is_some() + && matches!(&self.reduction, OnTheFlyReductionV1::Contracted(_)); + if aborting_prepared_contracted_selection { + // A successful earlier tile commits the executor family before + // the wrapper commits its public selection. If a later tile + // fails, retaining either the old wrapper identity or the new + // executor family would make the two caches disagree. Clear the + // wrapper side even if native row-table invalidation itself fails. + let rollback = self.lane.abort_contracted_selection(); + self.pending_prepared_selection = None; + self.prepared_selections.clear(); + self.last_prepared_selection = None; + return rollback; + } + match &self.reduction { + OnTheFlyReductionV1::Lc => self.lane.discard_pending_lc_queries()?, + OnTheFlyReductionV1::Contracted(_) => self.lane.discard_pending_contracted_queries()?, + } + self.pending_prepared_selection = None; + Ok(()) + } + + fn reuse_current_queries(&mut self, point_capacity: u32) -> RusticolResult { + match &self.reduction { + OnTheFlyReductionV1::Lc => self.lane.reuse_current_lc_queries(point_capacity), + OnTheFlyReductionV1::Contracted(_) => { + self.lane.reuse_current_contracted_queries(point_capacity) + } + } + } + + fn commit_pending_selection(&mut self) { + let Some(selection) = self.pending_prepared_selection.take() else { + return; + }; + self.prepared_selections.clear(); + self.prepared_selections.push(selection); + self.last_prepared_selection = Some(0); + } + + fn selected_axis_ids(&self) -> RusticolResult<(Vec, Vec)> { + let prepared = self.current_prepared_selection()?; + Ok((prepared.helicity_ids.to_vec(), prepared.color_ids.to_vec())) + } + + #[allow(clippy::too_many_arguments)] + fn warm_up_f64<'observer>( + &mut self, + common: &ExecutionRuntime, + batch: F64MomentumBatchView<'_>, + selected_helicities: Option<&BTreeSet>, + selected_colors: Option<&BTreeSet>, + n_cores: Option, + observer: Option< + &'observer mut super::on_the_fly_warm_up::NativeOnTheFlyWarmUpObserver<'observer>, + >, + ) -> RusticolResult { + if batch.point_count() != 1 { + return Err(RusticolError::invalid_argument( + "on-the-fly warm-up requires exactly one binary64 phase-space point", + )); + } + let query_construction_threads = match n_cores { + Some(0) => { + return Err(RusticolError::invalid_argument( + "on-the-fly warm-up n_cores must be positive", + )); + } + Some(requested) => { + super::on_the_fly_load::effective_query_construction_threads(requested) + } + None => self.lane.effective_query_construction_threads(), + }; + let mut progress = super::on_the_fly_warm_up::OnTheFlyWarmUpProgress::new( + observer, + query_construction_threads, + )?; + let result = (|| { + self.lane.prepare_process_for_warm_up(&mut progress)?; + let prepared = self.prepare_selection_for_warm_up( + selected_helicities, + selected_colors, + query_construction_threads, + &mut progress, + )?; + let input_len = self.fill_point_major(batch)?; + progress.emit( + super::on_the_fly_warm_up::NativeOnTheFlyWarmUpEventKind::Start, + super::on_the_fly_warm_up::NativeOnTheFlyWarmUpStage::FirstEvaluation, + 0, + 1, + None, + )?; + let mut output = [0.0_f64]; + match &self.reduction { + OnTheFlyReductionV1::Lc => self.lane.run_f64_into_unprofiled( + &self.point_major_scratch[..input_len], + 1, + &common.model_parameter_values_f64, + common.normalization_factor, + &mut output, + )?, + OnTheFlyReductionV1::Contracted(plan) => { + self.lane.run_contracted_f64_into_unprofiled( + &self.point_major_scratch[..input_len], + 1, + &common.model_parameter_values_f64, + common.normalization_factor, + &plan.plan, + plan.point_tile_size, + &mut output, + )?; + } + } + Ok(prepared) + })(); + let prepared = match result { + Ok(prepared) => { + // The lane commits its executable family only after the first + // successful evaluation. Commit the matching public selector + // before publishing the terminal event so both cache layers + // remain coherent even if an embedding rejects that event. + self.commit_pending_selection(); + prepared + } + Err(error) => { + self.discard_pending_selection()?; + return Err(error); + } + }; + progress.emit_terminal_notification( + super::on_the_fly_warm_up::NativeOnTheFlyWarmUpStage::FirstEvaluation, + 1, + 1, + None, + ); + let query_count = u64::try_from(prepared.query_count) + .map_err(|_| RusticolError::invalid_argument("warm-up query count exceeds u64"))?; + Ok(super::on_the_fly_warm_up::NativeOnTheFlyWarmUpResult { + schema_version: 1, + elapsed_seconds: progress.elapsed_seconds(), + query_count, + warmed_query_count: if prepared.already_warm { + 0 + } else { + query_count + }, + current_rss_bytes: progress.current_rss_bytes(), + peak_rss_bytes: progress.peak_rss_bytes(), + already_warm: prepared.already_warm, + first_evaluation_completed: true, + }) + } + + pub(super) fn run_total_into_unprofiled( + &mut self, + common: &ExecutionRuntime, + batch: F64MomentumBatchView<'_>, + selected_helicities: Option<&BTreeSet>, + selected_colors: Option<&BTreeSet>, + output: &mut [f64], + ) -> RusticolResult<()> { + let result = (|| { + let point_count = batch.point_count(); + self.prepare_selection(selected_helicities, selected_colors, point_count)?; + let input_len = self.fill_point_major(batch)?; + let native_point_count = u32::try_from(point_count).map_err(|_| { + RusticolError::invalid_argument("on-the-fly point count exceeds u32") + })?; + match &self.reduction { + OnTheFlyReductionV1::Lc => self.lane.run_f64_into_unprofiled( + &self.point_major_scratch[..input_len], + native_point_count, + &common.model_parameter_values_f64, + common.normalization_factor, + output, + ), + OnTheFlyReductionV1::Contracted(plan) => { + self.lane.run_contracted_f64_into_unprofiled( + &self.point_major_scratch[..input_len], + native_point_count, + &common.model_parameter_values_f64, + common.normalization_factor, + &plan.plan, + plan.point_tile_size, + output, + ) + } + } + })(); + match result { + Ok(()) => { + self.commit_pending_selection(); + Ok(()) + } + Err(error) => { + self.discard_pending_selection()?; + Err(error) + } + } + } + + pub(super) fn run_total_into( + &mut self, + common: &ExecutionRuntime, + batch: F64MomentumBatchView<'_>, + selected_helicities: Option<&BTreeSet>, + selected_colors: Option<&BTreeSet>, + output: &mut [f64], + ) -> RusticolResult { + let result = (|| { + let point_count = batch.point_count(); + self.prepare_selection(selected_helicities, selected_colors, point_count)?; + let input_len = self.fill_point_major(batch)?; + let native_point_count = u32::try_from(point_count).map_err(|_| { + RusticolError::invalid_argument("on-the-fly point count exceeds u32") + })?; + let (_, profile) = match &self.reduction { + OnTheFlyReductionV1::Lc => self.lane.run_f64_into( + &self.point_major_scratch[..input_len], + native_point_count, + &common.model_parameter_values_f64, + common.normalization_factor, + output, + )?, + OnTheFlyReductionV1::Contracted(plan) => self.lane.run_contracted_f64_into( + &self.point_major_scratch[..input_len], + native_point_count, + &common.model_parameter_values_f64, + common.normalization_factor, + &plan.plan, + plan.point_tile_size, + output, + )?, + }; + Ok(profile) + })(); + match result { + Ok(profile) => { + self.commit_pending_selection(); + Ok(profile) + } + Err(error) => { + self.discard_pending_selection()?; + Err(error) + } + } + } + + fn run_total_into_by_ordinals( + &mut self, + common: &ExecutionRuntime, + batch: F64MomentumBatchView<'_>, + helicity_ordinals: Option<&[usize]>, + color_ordinals: Option<&[usize]>, + output: &mut [f64], + ) -> RusticolResult { + let result = (|| { + let point_count = batch.point_count(); + self.prepare_selection_from_ordinals(helicity_ordinals, color_ordinals, point_count)?; + let input_len = self.fill_point_major(batch)?; + let native_point_count = u32::try_from(point_count).map_err(|_| { + RusticolError::invalid_argument("on-the-fly point count exceeds u32") + })?; + let (_, profile) = match &self.reduction { + OnTheFlyReductionV1::Lc => self.lane.run_f64_into( + &self.point_major_scratch[..input_len], + native_point_count, + &common.model_parameter_values_f64, + common.normalization_factor, + output, + )?, + OnTheFlyReductionV1::Contracted(plan) => self.lane.run_contracted_f64_into( + &self.point_major_scratch[..input_len], + native_point_count, + &common.model_parameter_values_f64, + common.normalization_factor, + &plan.plan, + plan.point_tile_size, + output, + )?, + }; + Ok(profile) + })(); + match result { + Ok(profile) => { + self.commit_pending_selection(); + Ok(profile) + } + Err(error) => { + self.discard_pending_selection()?; + Err(error) + } + } + } + + fn run_total_into_by_ordinals_unprofiled( + &mut self, + common: &ExecutionRuntime, + batch: F64MomentumBatchView<'_>, + helicity_ordinals: Option<&[usize]>, + color_ordinals: Option<&[usize]>, + output: &mut [f64], + ) -> RusticolResult<()> { + let result = (|| { + let point_count = batch.point_count(); + self.prepare_selection_from_ordinals(helicity_ordinals, color_ordinals, point_count)?; + let input_len = self.fill_point_major(batch)?; + let native_point_count = u32::try_from(point_count).map_err(|_| { + RusticolError::invalid_argument("on-the-fly point count exceeds u32") + })?; + match &self.reduction { + OnTheFlyReductionV1::Lc => self.lane.run_f64_into_unprofiled( + &self.point_major_scratch[..input_len], + native_point_count, + &common.model_parameter_values_f64, + common.normalization_factor, + output, + ), + OnTheFlyReductionV1::Contracted(plan) => { + self.lane.run_contracted_f64_into_unprofiled( + &self.point_major_scratch[..input_len], + native_point_count, + &common.model_parameter_values_f64, + common.normalization_factor, + &plan.plan, + plan.point_tile_size, + output, + ) + } + } + })(); + match result { + Ok(()) => { + self.commit_pending_selection(); + Ok(()) + } + Err(error) => { + self.discard_pending_selection()?; + Err(error) + } + } + } + + fn run_resolved( + &mut self, + common: &ExecutionRuntime, + batch: F64MomentumBatchView<'_>, + selected_helicities: Option<&BTreeSet>, + selected_colors: Option<&BTreeSet>, + ) -> RusticolResult<(ResolvedValues, RuntimeProfile)> { + let result = (|| { + let point_count = batch.point_count(); + let (helicity_count, color_count) = + self.prepare_selection(selected_helicities, selected_colors, point_count)?; + let prepared = self.current_prepared_selection()?; + let helicity_indices = prepared.helicity_indices.to_vec(); + let color_indices = prepared.color_indices.to_vec(); + let value_count = point_count + .checked_mul(helicity_count) + .and_then(|value| value.checked_mul(color_count)) + .ok_or_else(|| { + RusticolError::invalid_argument("on-the-fly resolved shape exceeds usize") + })?; + let mut values = vec![0.0; value_count]; + let input_len = self.fill_point_major(batch)?; + let native_point_count = u32::try_from(point_count).map_err(|_| { + RusticolError::invalid_argument("on-the-fly point count exceeds u32") + })?; + let (_, profile) = match &self.reduction { + OnTheFlyReductionV1::Lc => self.lane.run_resolved_f64_into( + &self.point_major_scratch[..input_len], + native_point_count, + &common.model_parameter_values_f64, + common.normalization_factor, + helicity_count, + color_count, + &mut values, + )?, + OnTheFlyReductionV1::Contracted(plan) => { + if color_count != 1 { + return Err(RusticolError::integrity( + "on-the-fly contracted resolved color axis is not singleton", + )); + } + self.lane.run_contracted_resolved_f64_into( + &self.point_major_scratch[..input_len], + native_point_count, + &common.model_parameter_values_f64, + common.normalization_factor, + &plan.plan, + plan.point_tile_size, + helicity_count, + &mut values, + )? + } + }; + Ok(( + ResolvedValues { + values, + point_count, + helicity_indices, + color_indices, + }, + profile, + )) + })(); + match result { + Ok(result) => { + self.commit_pending_selection(); + Ok(result) + } + Err(error) => { + self.discard_pending_selection()?; + Err(error) + } + } + } +} + +struct PointSelectorProfileCounts { + gather_point_count: usize, + input_bytes_per_point: usize, + scatter_value_count: usize, +} + +type ProfiledPreparedF64Batch = (Vec>, Duration, Duration); + +fn attach_point_selector_profile( + profile: &mut NativeRuntimeProfile, + plan: &PointSelectorPlanProfile, + planner: Duration, + gather: Duration, + scatter: Duration, + counts: PointSelectorProfileCounts, +) { + profile.selector_planner_s = profile_duration_seconds(planner); + profile.selector_gather_s = profile_duration_seconds(gather); + profile.selector_scatter_s = profile_duration_seconds(scatter); + profile.selector_plan_kind = plan.kind.to_string(); + profile.selector_group_sizes.clone_from(&plan.group_sizes); + profile.selector_reordered_point_count = plan.reordered_point_count; + profile.selector_simd_lane_width = plan.simd_lane_width; + profile.selector_simd_occupancy = plan.simd_occupancy; + profile.selector_gather_point_count = counts.gather_point_count as u64; + profile.selector_gather_bytes = + (counts.gather_point_count * counts.input_bytes_per_point) as u64; + profile.selector_scatter_value_count = counts.scatter_value_count as u64; +} + +fn execution_uses_simd_jit(runtime: &ExecutionRuntime) -> bool { + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + if runtime.compiled_direct_runtime.is_some() { + return true; + } + runtime + .stages + .as_ref() + .is_some_and(|stages| stages.iter().any(|stage| stage.evaluator.uses_simd_jit())) + || runtime + .amplitude_stage + .as_ref() + .and_then(|stage| stage.evaluator.as_ref()) + .is_some_and(EvaluatorGroup::uses_simd_jit) + || runtime + .helicity_sum_runtime + .as_deref() + .is_some_and(execution_uses_simd_jit) + || runtime + .helicity_selector_runtimes + .iter() + .any(|lane| execution_uses_simd_jit(lane)) + || runtime + .color_selector_runtimes + .values() + .any(|lane| execution_uses_simd_jit(lane)) +} + +#[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] +#[derive(Clone, Copy, Debug, Default)] +struct CompiledDirectProfileSnapshot { + engine_count: usize, + minimum_effective_tile_capacity: usize, + maximum_physical_scalar_values_per_point: usize, + maximum_hot_scalar_values_per_point: usize, + maximum_source_scalar_values_per_point: usize, + maximum_reduction_scalar_values_per_point: usize, + source_fill_bytes: u64, + momentum_fill_bytes: u64, + parameter_fill_bytes: u64, + scalar_broadcast_fill_bytes: u64, + amplitude_clear_bytes: u64, + backend_call_count: u64, + boundary_input_bytes: u64, + boundary_current_output_bytes: u64, + boundary_amplitude_output_bytes: u64, +} + +#[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] +const fn minimum_nonzero(left: usize, right: usize) -> usize { + match (left, right) { + (0, value) | (value, 0) => value, + (left, right) => { + if left < right { + left + } else { + right + } + } + } +} + +#[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] +#[derive(Clone, Copy, Debug, Default)] +struct CompiledDirectConfigurationSnapshot { + engine_count: usize, + minimum_effective_tile_capacity: usize, + maximum_physical_scalar_values_per_point: usize, + maximum_hot_scalar_values_per_point: usize, + maximum_source_scalar_values_per_point: usize, + maximum_reduction_scalar_values_per_point: usize, +} + +#[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] +fn compiled_direct_configuration_snapshot( + runtime: &ExecutionRuntime, +) -> CompiledDirectConfigurationSnapshot { + let mut snapshot = CompiledDirectConfigurationSnapshot::default(); + if let Some(direct) = runtime.compiled_direct_runtime.as_ref() { + let sizing = direct.tile_sizing(); + snapshot.engine_count = 1; + snapshot.minimum_effective_tile_capacity = sizing.effective_tile_capacity; + snapshot.maximum_physical_scalar_values_per_point = sizing.physical_scalar_values_per_point; + snapshot.maximum_hot_scalar_values_per_point = sizing.hot_scalar_values_per_point; + snapshot.maximum_source_scalar_values_per_point = sizing.source_scalar_values_per_point; + snapshot.maximum_reduction_scalar_values_per_point = + sizing.reduction_scalar_values_per_point; + } + let children = runtime + .helicity_sum_runtime + .iter() + .map(Box::as_ref) + .chain(runtime.helicity_selector_runtimes.iter().map(Box::as_ref)) + .chain(runtime.color_selector_runtimes.values().map(Box::as_ref)); + for child in children { + let child = compiled_direct_configuration_snapshot(child); + snapshot.engine_count += child.engine_count; + snapshot.minimum_effective_tile_capacity = minimum_nonzero( + snapshot.minimum_effective_tile_capacity, + child.minimum_effective_tile_capacity, + ); + snapshot.maximum_physical_scalar_values_per_point = snapshot + .maximum_physical_scalar_values_per_point + .max(child.maximum_physical_scalar_values_per_point); + snapshot.maximum_hot_scalar_values_per_point = snapshot + .maximum_hot_scalar_values_per_point + .max(child.maximum_hot_scalar_values_per_point); + snapshot.maximum_source_scalar_values_per_point = snapshot + .maximum_source_scalar_values_per_point + .max(child.maximum_source_scalar_values_per_point); + snapshot.maximum_reduction_scalar_values_per_point = snapshot + .maximum_reduction_scalar_values_per_point + .max(child.maximum_reduction_scalar_values_per_point); + } + snapshot +} + +#[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] +fn compiled_direct_profile_snapshot( + runtime: &ExecutionRuntime, +) -> RusticolResult { + let mut snapshot = CompiledDirectProfileSnapshot::default(); + if let Some(direct) = runtime.compiled_direct_runtime.as_ref() { + let traffic = direct.traffic(); + traffic.leaf.validate_direct()?; + if traffic.boundary_input_bytes + | traffic.boundary_current_output_bytes + | traffic.boundary_amplitude_output_bytes + != 0 + { + return Err(RusticolError::integrity( + "production compiled Direct-Arena profile observed developer boundary traffic", + )); + } + let sizing = direct.tile_sizing(); + snapshot.engine_count += 1; + snapshot.minimum_effective_tile_capacity = sizing.effective_tile_capacity; + snapshot.maximum_physical_scalar_values_per_point = sizing.physical_scalar_values_per_point; + snapshot.maximum_hot_scalar_values_per_point = sizing.hot_scalar_values_per_point; + snapshot.maximum_source_scalar_values_per_point = sizing.source_scalar_values_per_point; + snapshot.maximum_reduction_scalar_values_per_point = + sizing.reduction_scalar_values_per_point; + snapshot.source_fill_bytes = traffic.source_fill_bytes; + snapshot.momentum_fill_bytes = traffic.momentum_fill_bytes; + snapshot.parameter_fill_bytes = traffic.parameter_fill_bytes; + snapshot.scalar_broadcast_fill_bytes = traffic.scalar_broadcast_fill_bytes; + snapshot.amplitude_clear_bytes = traffic.amplitude_clear_bytes; + snapshot.backend_call_count = traffic.leaf.calls; + snapshot.boundary_input_bytes = traffic.boundary_input_bytes; + snapshot.boundary_current_output_bytes = traffic.boundary_current_output_bytes; + snapshot.boundary_amplitude_output_bytes = traffic.boundary_amplitude_output_bytes; + } + let children = runtime + .helicity_sum_runtime + .iter() + .map(Box::as_ref) + .chain(runtime.helicity_selector_runtimes.iter().map(Box::as_ref)) + .chain(runtime.color_selector_runtimes.values().map(Box::as_ref)); + for child in children { + let child = compiled_direct_profile_snapshot(child)?; + snapshot.engine_count += child.engine_count; + snapshot.minimum_effective_tile_capacity = minimum_nonzero( + snapshot.minimum_effective_tile_capacity, + child.minimum_effective_tile_capacity, + ); + snapshot.maximum_physical_scalar_values_per_point = snapshot + .maximum_physical_scalar_values_per_point + .max(child.maximum_physical_scalar_values_per_point); + snapshot.maximum_hot_scalar_values_per_point = snapshot + .maximum_hot_scalar_values_per_point + .max(child.maximum_hot_scalar_values_per_point); + snapshot.maximum_source_scalar_values_per_point = snapshot + .maximum_source_scalar_values_per_point + .max(child.maximum_source_scalar_values_per_point); + snapshot.maximum_reduction_scalar_values_per_point = snapshot + .maximum_reduction_scalar_values_per_point + .max(child.maximum_reduction_scalar_values_per_point); + snapshot.source_fill_bytes = snapshot + .source_fill_bytes + .saturating_add(child.source_fill_bytes); + snapshot.momentum_fill_bytes = snapshot + .momentum_fill_bytes + .saturating_add(child.momentum_fill_bytes); + snapshot.parameter_fill_bytes = snapshot + .parameter_fill_bytes + .saturating_add(child.parameter_fill_bytes); + snapshot.scalar_broadcast_fill_bytes = snapshot + .scalar_broadcast_fill_bytes + .saturating_add(child.scalar_broadcast_fill_bytes); + snapshot.amplitude_clear_bytes = snapshot + .amplitude_clear_bytes + .saturating_add(child.amplitude_clear_bytes); + snapshot.backend_call_count = snapshot + .backend_call_count + .saturating_add(child.backend_call_count); + snapshot.boundary_input_bytes = snapshot + .boundary_input_bytes + .saturating_add(child.boundary_input_bytes); + snapshot.boundary_current_output_bytes = snapshot + .boundary_current_output_bytes + .saturating_add(child.boundary_current_output_bytes); + snapshot.boundary_amplitude_output_bytes = snapshot + .boundary_amplitude_output_bytes + .saturating_add(child.boundary_amplitude_output_bytes); + } + Ok(snapshot) +} + +#[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] +fn attach_compiled_direct_configuration( + profile: &mut RuntimeProfile, + snapshot: CompiledDirectProfileSnapshot, +) { + profile.compiled_direct_arena_minimum_effective_tile_capacity = + snapshot.minimum_effective_tile_capacity as u64; + profile.compiled_direct_arena_maximum_physical_scalar_values_per_point = + snapshot.maximum_physical_scalar_values_per_point as u64; + profile.compiled_direct_arena_maximum_hot_scalar_values_per_point = + snapshot.maximum_hot_scalar_values_per_point as u64; + profile.compiled_direct_arena_maximum_source_scalar_values_per_point = + snapshot.maximum_source_scalar_values_per_point as u64; + profile.compiled_direct_arena_maximum_reduction_scalar_values_per_point = + snapshot.maximum_reduction_scalar_values_per_point as u64; +} + +fn ensure_selected_runtime_capabilities_supported(capabilities: &[String]) -> RusticolResult<()> { + if capabilities.iter().any(|capability| { + capability == EAGER_DAG_RUNTIME_CAPABILITY + || capability == EAGER_LC_TOPOLOGY_REPLAY_RUNTIME_CAPABILITY + }) { + return Err(RusticolError::compatibility( + "legacy eager plan-v2 artifacts are no longer executable; regenerate the artifact \ + with the current `pyamplicol generate` before loading it", + )); + } + ensure_runtime_capabilities_supported(capabilities.iter().map(String::as_str)) +} + +impl NativeRuntime { + pub const ABI_VERSION: u32 = crate::C_ABI_VERSION; + + /// Save the currently retained, completed on-the-fly cache to a file. + /// The original process output is still required to load its kernels. + /// Numeric momenta, parameters, and workspaces are not serialized. + pub fn save(&self, path: impl AsRef) -> RusticolResult<()> { + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + if let NativeExecutionLane::OnTheFly(runtime) = &self.execution_lane { + return cache::save(self, runtime, path.as_ref()); + } + let _ = path; + Err(RusticolError::compatibility( + "save() requires an on-the-fly runtime", + )) + } + + /// Restore completed on-the-fly structural work against this loaded + /// process. The runner's current model parameters are kept. A failed load + /// leaves the previously usable cache intact. + pub fn load_cache(&mut self, path: impl AsRef) -> RusticolResult<()> { + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + if matches!(self.execution_lane, NativeExecutionLane::OnTheFly(_)) { + return cache::load(self, path.as_ref()); + } + let _ = path; + Err(RusticolError::compatibility( + "load_cache() requires an on-the-fly runtime", + )) + } + + /// Content identity of the authenticated artifact manifest that supplied + /// this in-memory runtime. + pub fn artifact_id(&self) -> &str { + &self.artifact_id + } + + /// Load only the authenticated physical reduction groups of an eager + /// plan-v3 artifact. + /// + /// Unlike the exact-section bridge, this does not materialize exact + /// invocations, attachments, finalizations, or closures that are unrelated + /// to reduction authentication. + #[doc(hidden)] + pub fn load_eager_reduction_groups( + artifact_path: impl AsRef, + process_id: &str, + ) -> Result { + let artifact = VerifiedArtifact::open_with_manifest_preflight(artifact_path, |manifest| { + let selection = manifest.select_process(Some(process_id))?; + ensure_selected_runtime_capabilities_supported( + &selection.process.required_runtime_capabilities, + ) + })?; + let selection = artifact.select_process(Some(process_id))?; + if selection.remaps_public_process() { + return Err(RusticolError::invalid_argument( + "compact eager reduction groups must be requested by representative process ID", + )); + } + let (manifest, evaluator_root) = load_verified_evaluator(&artifact, &selection)?; + let physics_bytes = artifact.read_payload(&selection.process.physics_path)?; + let mut physics = + ProcessPhysicsV1::from_json(&physics_bytes, &selection.process.physics_path)?; + if physics.process_id != selection.process.id + || physics.process != selection.process.expression + || physics.color_accuracy.as_str() != selection.process.color_accuracy + || physics + .external_particles + .iter() + .map(|particle| particle.pdg) + .ne(selection.process.external_pdgs.iter().copied()) + { + return Err(RusticolError::integrity(format!( + "runtime physics payload {:?} does not match process {:?}", + selection.process.physics_path, selection.process.id + ))); + } + match manifest { + LoadedExecutionManifest::Compiled(_) => Err(RusticolError::compatibility( + "compact reduction-group loading requires an eager plan-v3 artifact", + )), + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + LoadedExecutionManifest::EagerV3(manifest) => { + super::eager_v3_load::load_eager_v3_reduction_groups( + &artifact, + &evaluator_root, + &manifest, + &mut physics, + ) + } + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + LoadedExecutionManifest::Recurrence(_) => Err(RusticolError::compatibility( + "recurrence reduction-group loading is unavailable through the eager bridge", + )), + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + LoadedExecutionManifest::OnTheFly(_) => Err(RusticolError::compatibility( + "on-the-fly reduction-group loading is unavailable through the eager bridge", + )), + } + } + + /// Load only the exact-required sections of an eager plan-v3 artifact. + /// + /// This authenticates the artifact and compact runtime container but does + /// not instantiate the prepared f64 backend. It is intentionally separate + /// from the normal runtime hot path. + pub fn load_eager_exact_sections( + artifact_path: impl AsRef, + process_id: &str, + ) -> Result { + let artifact = VerifiedArtifact::open_with_manifest_preflight(artifact_path, |manifest| { + let selection = manifest.select_process(Some(process_id))?; + ensure_selected_runtime_capabilities_supported( + &selection.process.required_runtime_capabilities, + ) + })?; + let selection = artifact.select_process(Some(process_id))?; + if selection.remaps_public_process() { + return Err(RusticolError::invalid_argument( + "compact eager exact sections must be requested by representative process ID", + )); + } + let (manifest, evaluator_root) = load_verified_evaluator(&artifact, &selection)?; + let physics_bytes = artifact.read_payload(&selection.process.physics_path)?; + let mut physics = + ProcessPhysicsV1::from_json(&physics_bytes, &selection.process.physics_path)?; + if physics.process_id != selection.process.id + || physics.process != selection.process.expression + || physics.color_accuracy.as_str() != selection.process.color_accuracy + || physics + .external_particles + .iter() + .map(|particle| particle.pdg) + .ne(selection.process.external_pdgs.iter().copied()) + { + return Err(RusticolError::integrity(format!( + "runtime physics payload {:?} does not match process {:?}", + selection.process.physics_path, selection.process.id + ))); + } + match manifest { + LoadedExecutionManifest::Compiled(_) => Err(RusticolError::compatibility( + "compact exact-section loading requires an eager plan-v3 artifact", + )), + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + LoadedExecutionManifest::EagerV3(manifest) => { + super::eager_v3_load::load_eager_v3_exact_sections( + &artifact, + &evaluator_root, + &manifest, + &mut physics, + ) + } + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + LoadedExecutionManifest::Recurrence(_) => Err(RusticolError::compatibility( + "recurrence exact-section loading is not available in the initial f64 runtime slice", + )), + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + LoadedExecutionManifest::OnTheFly(_) => Err(RusticolError::compatibility( + "on-the-fly exact-section loading is unavailable through the eager bridge", + )), + } + } + + /// Load the immutable exact-execution sections of an authenticated + /// topology-replay recurrence artifact without instantiating its f64 + /// Direct-Arena backend. + pub fn load_recurrence_exact_sections( + artifact_path: impl AsRef, + process_id: &str, + ) -> Result { + let artifact = VerifiedArtifact::open_with_manifest_preflight(artifact_path, |manifest| { + let selection = manifest.select_process(Some(process_id))?; + ensure_selected_runtime_capabilities_supported( + &selection.process.required_runtime_capabilities, + ) + })?; + let selection = artifact.select_process(Some(process_id))?; + if selection.remaps_public_process() { + return Err(RusticolError::invalid_argument( + "compact recurrence exact sections must be requested by representative process ID", + )); + } + let (manifest, evaluator_root) = load_verified_evaluator(&artifact, &selection)?; + let physics_bytes = artifact.read_payload(&selection.process.physics_path)?; + let physics = ProcessPhysicsV1::from_json(&physics_bytes, &selection.process.physics_path)?; + if physics.process_id != selection.process.id + || physics.process != selection.process.expression + || physics.color_accuracy.as_str() != selection.process.color_accuracy + || physics + .external_particles + .iter() + .map(|particle| particle.pdg) + .ne(selection.process.external_pdgs.iter().copied()) + { + return Err(RusticolError::integrity(format!( + "runtime physics payload {:?} does not match process {:?}", + selection.process.physics_path, selection.process.id + ))); + } + match manifest { + LoadedExecutionManifest::Compiled(_) => Err(RusticolError::compatibility( + "compact recurrence exact-section loading requires a recurrence artifact", + )), + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + LoadedExecutionManifest::EagerV3(_) => Err(RusticolError::compatibility( + "compact recurrence exact-section loading requires a recurrence artifact", + )), + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + LoadedExecutionManifest::Recurrence(manifest) => { + load_recurrence_exact_sections(&artifact, &evaluator_root, &manifest, &physics) + } + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + LoadedExecutionManifest::OnTheFly(_) => Err(RusticolError::compatibility( + "compact recurrence exact-section loading is unavailable for on-the-fly artifacts", + )), + } + } + + #[cfg_attr(target_vendor = "apple", unsafe(link_section = "__TEXT,__rcl_load"))] + #[cfg_attr(target_vendor = "apple", inline(never))] + #[allow(clippy::too_many_arguments)] + fn finish_dense_load( + root: PathBuf, + artifact_id: String, + selection: crate::ArtifactSelection, + mut physics_v1: ProcessPhysicsV1, + representative_key: String, + mut runtime: ExecutionRuntime, + execution_lane: NativeExecutionLane, + model_parameters_path: Option<&Path>, + ) -> Result { + let process = selection.public_expression.clone(); + let process_key = selection.requested_id.clone(); + let representative_process_id = selection.process.id.clone(); + let public_remap = selection.remaps_public_process(); + let nonidentity_permutation = selection + .external_permutation + .iter() + .copied() + .enumerate() + .any(|(representative_index, public_index)| representative_index != public_index); + let initial_count = physics_v1 + .external_particles + .iter() + .take_while(|particle| particle.role == crate::ParticleRole::Initial) + .count(); + let final_state_only = public_remap + && selection + .external_permutation + .iter() + .take(initial_count) + .copied() + .enumerate() + .all(|(index, public_index)| index == public_index); + let input_crossing_map = if public_remap { + let representative_physics = physics_v1.clone(); + let public_physics = apply_process_permutation_metadata(physics_v1, &selection) + .map_err(|error| { + RusticolError::with_kind( + error.kind(), + format!("could not remap process physics metadata: {error}"), + ) + })?; + let helicity_id_map = representative_physics + .helicities + .iter() + .zip(&public_physics.helicities) + .map(|(representative, public)| (representative.id.clone(), public.id.clone())) + .collect::>(); + let color_id_map = representative_physics + .color_components + .iter() + .zip(&public_physics.color_components) + .map(|(representative, public)| { + (representative.id().to_string(), public.id().to_string()) + }) + .collect::>(); + runtime + .remap_lc_topology_replay_public_labels(&selection.external_permutation) + .map_err(|error| { + RusticolError::with_kind( + error.kind(), + format!("could not remap process execution metadata: {error}"), + ) + })?; + runtime.remap_physics_reduction_overrides(&helicity_id_map, &color_id_map)?; + physics_v1 = public_physics; + runtime.set_external_pdg_order_recursive(&selection.external_pdgs); + nonidentity_permutation.then(|| { + selection + .external_permutation + .iter() + .copied() + .enumerate() + .map(|(target_index, source_index)| InputCrossingMapEntry { + target_index, + source_index, + sign: 1.0, + }) + .collect() + }) + } else { + None + }; + let input_crossing_map = + prevalidate_input_crossing_lookup(runtime.external_count, input_crossing_map)?; + runtime.attach_physics(Arc::new(PhysicsRuntime::new(physics_v1.clone())?))?; + // Execution keeps the representative flavours and LC words; only the + // exposed metadata names the selected process-group member. + let physics_v1 = if selection.process_group.is_some() { + apply_process_group_metadata(&physics_v1, &selection)? + } else { + physics_v1 + }; + if matches!(&execution_lane, NativeExecutionLane::Compiled) { + runtime.initialize_compiled_helicity_execution_plan( + public_remap.then_some(selection.external_permutation.as_slice()), + )?; + } + let selector_simd_lane_width = { + let uses_simd_jit = match &execution_lane { + NativeExecutionLane::Compiled => execution_uses_simd_jit(&runtime), + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + NativeExecutionLane::Eager(runtime) => runtime.backend_name() == "jit", + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + NativeExecutionLane::Recurrence(runtime) => runtime.backend_name() == "jit", + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + NativeExecutionLane::OnTheFly(_) => false, + }; + if uses_simd_jit { + super::evaluator::native_f64_simd_lane_width() + } else { + 1 + } + }; + let mut loaded = Self { + root, + artifact_id, + runtime, + execution_lane, + process, + process_key, + representative_process_id, + external_permutation: selection.external_permutation.clone(), + input_crossing_map, + permutation_alias_of: public_remap.then(|| representative_key.clone()), + process_group: process_group_routing(&selection, &representative_key), + final_state_permutation_alias_of: final_state_only.then_some(representative_key), + physics_v1: LazyProcessPhysicsV1::loaded(physics_v1), + warnings_muted: false, + warned_kinds: BTreeSet::new(), + pending_warnings: Vec::new(), + point_selector_scratch: PointSelectorExecutionScratch::default(), + selector_simd_lane_width, + correlated: None, + }; + if let Some(path) = model_parameters_path { + loaded.set_model_parameters_json(path)?; + } + Ok(loaded) + } + + // Darwin reports each referenced clean executable page in process RSS. + // Keep the authenticated process-ready load path and its load-only callees + // in one Apple section; evaluation code remains in the ordinary text + // section and non-Apple builds retain their normal placement. + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + #[cfg_attr(target_vendor = "apple", unsafe(link_section = "__TEXT,__rcl_load"))] + #[cfg_attr(target_vendor = "apple", inline(never))] + fn load_recurrence_bootstrap( + bootstrap: crate::artifact::RecurrenceBootstrapArtifact, + model_parameters_path: Option<&Path>, + ) -> Result { + let crate::artifact::RecurrenceBootstrapArtifact { + mut artifact, + selection, + ready_execution, + physics: physics_v1, + evaluator_payload_container, + } = bootstrap; + ensure_selected_runtime_capabilities_supported( + &selection.process.required_runtime_capabilities, + )?; + validate_capability_list_match( + &artifact.manifest().runtime.required_runtime_capabilities, + &ready_execution.required_runtime_capabilities, + "outer runtime and recurrence process-ready recipe", + )?; + validate_representative_physics(&physics_v1, &selection)?; + + // The authenticated member uses the exact conventional execution path + // above, and public process IDs are already confined by the outer + // manifest validator. Avoid reopening the duplicate loose JSON merely + // to recover its parent directory. + let evaluator_root = artifact + .root() + .join("processes") + .join(&selection.process.id); + let representative_key = ready_execution.key.clone(); + let loaded = load_recurrence_ready_runtime( + &mut artifact, + &evaluator_root, + &ready_execution, + &physics_v1, + &evaluator_payload_container, + )?; + Self::finish_dense_load( + artifact.root().to_path_buf(), + artifact.manifest().artifact_id.clone(), + selection, + physics_v1, + representative_key, + loaded.common, + NativeExecutionLane::Recurrence(Box::new(loaded.lane)), + model_parameters_path, + ) + } + + #[cfg_attr(target_vendor = "apple", unsafe(link_section = "__TEXT,__rcl_load"))] + #[cfg_attr(target_vendor = "apple", inline(never))] + pub fn load( + artifact_path: impl AsRef, + process_id: Option<&str>, + model_parameters_path: Option<&Path>, + ) -> Result { + let artifact_path = artifact_path.as_ref(); + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + if let Some(bootstrap) = + VerifiedArtifact::try_open_recurrence_bootstrap(artifact_path, process_id)? + { + return Self::load_recurrence_bootstrap(bootstrap, model_parameters_path); + } + Self::load_generic(artifact_path, process_id, model_parameters_path) + } + + #[cold] + #[inline(never)] + fn load_generic( + artifact_path: &Path, + process_id: Option<&str>, + model_parameters_path: Option<&Path>, + ) -> Result { + let artifact = VerifiedArtifact::open_with_manifest_preflight(artifact_path, |manifest| { + match manifest.select_process(process_id) { + Ok(selection) => ensure_selected_runtime_capabilities_supported( + &selection.process.required_runtime_capabilities, + ), + // A process-group member resolves only after the map payload + // is read; its representative is checked below. + Err(_) + if manifest + .payloads + .iter() + .any(|payload| payload.role == crate::PayloadRole::ProcessGroups) => + { + Ok(()) + } + Err(error) => Err(error), + } + })?; + let artifact_id = artifact.manifest().artifact_id.clone(); + let selection = artifact.select_process(process_id)?; + if selection.process_group.is_some() { + ensure_selected_runtime_capabilities_supported( + &selection.process.required_runtime_capabilities, + )?; + } + let (manifest, evaluator_root) = load_verified_evaluator(&artifact, &selection)?; + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + if let LoadedExecutionManifest::OnTheFly(on_the_fly) = &manifest { + let public_remap = selection.remaps_public_process(); + let nonidentity_permutation = selection + .external_permutation + .iter() + .copied() + .enumerate() + .any(|(representative_index, public_index)| representative_index != public_index); + let initial_count = on_the_fly + .runtime_metadata + .external_legs + .iter() + .filter(|leg| leg.is_initial) + .count(); + let final_state_only = public_remap + && selection + .external_permutation + .iter() + .take(initial_count) + .copied() + .enumerate() + .all(|(index, public_index)| index == public_index); + let loaded = super::on_the_fly_load::load_on_the_fly_native_runtime( + &artifact, + &evaluator_root, + on_the_fly, + &selection, + )?; + let super::on_the_fly_load::LoadedOnTheFlyRuntime { + mut common, + lane, + selectors, + metadata_selectors, + public_metadata, + color_contraction, + } = loaded; + if public_remap { + common.set_external_pdg_order_recursive(&selection.external_pdgs); + } + let input_crossing_map = nonidentity_permutation.then(|| { + selection + .external_permutation + .iter() + .copied() + .enumerate() + .map(|(target_index, source_index)| InputCrossingMapEntry { + target_index, + source_index, + sign: 1.0, + }) + .collect::>() + }); + let input_crossing_map = + prevalidate_input_crossing_lookup(common.external_count, input_crossing_map)?; + let representative_key = on_the_fly.key.clone(); + let mut runtime = Self { + root: artifact.root().to_path_buf(), + artifact_id, + runtime: common, + execution_lane: NativeExecutionLane::OnTheFly(Box::new( + OnTheFlyExecutionRuntime::new(lane, selectors, color_contraction), + )), + process: selection.public_expression.clone(), + process_key: selection.requested_id.clone(), + representative_process_id: selection.process.id.clone(), + external_permutation: selection.external_permutation.clone(), + input_crossing_map, + permutation_alias_of: public_remap.then(|| representative_key.clone()), + process_group: process_group_routing(&selection, &representative_key), + final_state_permutation_alias_of: final_state_only.then_some(representative_key), + physics_v1: LazyProcessPhysicsV1::deferred_on_the_fly( + public_metadata, + metadata_selectors, + selection, + ), + warnings_muted: false, + warned_kinds: BTreeSet::new(), + pending_warnings: Vec::new(), + point_selector_scratch: PointSelectorExecutionScratch::default(), + selector_simd_lane_width: 1, + correlated: None, + }; + if let Some(path) = model_parameters_path { + runtime.set_model_parameters_json(path)?; + } + return Ok(runtime); + } + let physics_bytes = artifact.read_payload(&selection.process.physics_path)?; + let mut physics_v1 = + ProcessPhysicsV1::from_json(&physics_bytes, &selection.process.physics_path)?; + if physics_v1.process_id != selection.process.id + || physics_v1.process != selection.process.expression + || physics_v1.color_accuracy.as_str() != selection.process.color_accuracy + || physics_v1 + .external_particles + .iter() + .map(|particle| particle.pdg) + .ne(selection.process.external_pdgs.iter().copied()) + { + return Err(RusticolError::integrity(format!( + "runtime physics payload {:?} does not match process {:?}", + selection.process.physics_path, selection.process.id + ))); + } + let (_representative_process, representative_key, runtime, execution_lane) = match manifest + { + LoadedExecutionManifest::Compiled(manifest) => { + super::eager_v3_load::reject_native_reduction_groups_for_compiled(&physics_v1)?; + let representative_process = manifest.process.clone(); + let representative_key = manifest.key.clone(); + let evaluator_payloads = artifact.evaluator_payload_store(&evaluator_root)?; + // Tile sizing is bound to the representative physics + // before public alias remapping. Aliases preserve these + // component counts and the execution payload itself is + // authenticated against the representative identifiers. + let sizing_physics = PhysicsRuntime::new(physics_v1.clone())?; + let runtime = load_execution_manifest_with_store( + *manifest, + &evaluator_payloads, + &sizing_physics, + )?; + ( + representative_process, + representative_key, + runtime, + NativeExecutionLane::Compiled, + ) + } + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + LoadedExecutionManifest::EagerV3(manifest) => { + let representative_process = manifest.process.clone(); + let representative_key = manifest.key.clone(); + let loaded = super::eager_v3_load::load_eager_v3_native_runtime( + &artifact, + &evaluator_root, + &manifest, + &mut physics_v1, + )?; + ( + representative_process, + representative_key, + loaded.common, + NativeExecutionLane::Eager(Box::new(loaded.lane)), + ) + } + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + LoadedExecutionManifest::Recurrence(manifest) => { + let representative_process = manifest.process.clone(); + let representative_key = manifest.key.clone(); + let loaded = load_recurrence_native_runtime( + &artifact, + &evaluator_root, + &manifest, + &physics_v1, + &selection, + )?; + ( + representative_process, + representative_key, + loaded.common, + NativeExecutionLane::Recurrence(Box::new(loaded.lane)), + ) + } + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + LoadedExecutionManifest::OnTheFly(_) => { + unreachable!("on-the-fly manifests are dispatched before dense physics loading") + } + }; + Self::finish_dense_load( + artifact.root().to_path_buf(), + artifact_id, + selection, + physics_v1, + representative_key, + runtime, + execution_lane, + model_parameters_path, + ) + } + + pub fn metadata(&self) -> NativeRuntimeMetadata { + let ( + execution_mode, + prepared_backend, + eager_effective_point_tile_size, + eager_workspace_bytes, + on_the_fly_requested_query_construction_threads, + on_the_fly_effective_query_construction_threads, + ) = match &self.execution_lane { + NativeExecutionLane::Compiled => ("compiled", None, None, None, None, None), + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + NativeExecutionLane::Eager(runtime) => ( + "eager", + Some(runtime.backend_name().to_string()), + Some(runtime.effective_point_tile_size()), + Some(runtime.workspace_bytes()), + None, + None, + ), + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + NativeExecutionLane::Recurrence(runtime) => ( + "recurrence", + Some(runtime.backend_name().to_string()), + Some(runtime.effective_point_tile_size()), + None, + None, + None, + ), + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + NativeExecutionLane::OnTheFly(runtime) => ( + "on-the-fly", + None, + None, + None, + Some(runtime.lane.requested_query_construction_threads()), + Some(runtime.lane.effective_query_construction_threads()), + ), + }; + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + let compiled_direct_configuration = compiled_direct_configuration_snapshot(&self.runtime); + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + let ( + compiled_direct_minimum_effective_tile_capacity, + compiled_direct_maximum_physical_scalar_values_per_point, + compiled_direct_maximum_hot_scalar_values_per_point, + compiled_direct_maximum_source_scalar_values_per_point, + compiled_direct_maximum_reduction_scalar_values_per_point, + ) = if compiled_direct_configuration.engine_count == 0 { + (None, None, None, None, None) + } else { + ( + Some(compiled_direct_configuration.minimum_effective_tile_capacity), + Some(compiled_direct_configuration.maximum_physical_scalar_values_per_point), + Some(compiled_direct_configuration.maximum_hot_scalar_values_per_point), + Some(compiled_direct_configuration.maximum_source_scalar_values_per_point), + Some(compiled_direct_configuration.maximum_reduction_scalar_values_per_point), + ) + }; + #[cfg(not(any(feature = "f64-compiled", feature = "f64-symjit")))] + let ( + compiled_direct_minimum_effective_tile_capacity, + compiled_direct_maximum_physical_scalar_values_per_point, + compiled_direct_maximum_hot_scalar_values_per_point, + compiled_direct_maximum_source_scalar_values_per_point, + compiled_direct_maximum_reduction_scalar_values_per_point, + ) = (None, None, None, None, None); + NativeRuntimeMetadata { + abi_version: Self::ABI_VERSION, + schema_version: PROCESS_ARTIFACT_SCHEMA_VERSION, + execution_mode: execution_mode.to_string(), + prepared_backend, + eager_effective_point_tile_size, + eager_workspace_bytes, + on_the_fly_requested_query_construction_threads, + on_the_fly_effective_query_construction_threads, + compiled_direct_minimum_effective_tile_capacity, + compiled_direct_maximum_physical_scalar_values_per_point, + compiled_direct_maximum_hot_scalar_values_per_point, + compiled_direct_maximum_source_scalar_values_per_point, + compiled_direct_maximum_reduction_scalar_values_per_point, + process: self.process.clone(), + process_key: self.process_key.clone(), + representative_process: self.runtime.process.clone(), + representative_process_key: self.runtime.key.clone(), + external_permutation: self.external_permutation.clone(), + permutation_alias_of: self.permutation_alias_of.clone(), + final_state_permutation_alias_of: self.final_state_permutation_alias_of.clone(), + process_group_member_of: self + .process_group + .as_ref() + .map(|group| group.representative_process_key.clone()), + process_group_factor: self.process_group_factor(), + charge_conjugated: self.charge_conjugated(), + color_accuracy: self.runtime.color_accuracy.clone(), + external_pdg_order: self.runtime.external_pdg_order.clone(), + external_count: self.runtime.external_count, + current_count: self.runtime.current_count, + source_count: self.runtime.source_count, + interaction_count: self.runtime.interaction_count, + stage_count: self.runtime.stage_count, + amplitude_output_count: self.runtime.amplitude_output_count, + } + } + + fn selector_simd_lane_width(&self) -> usize { + self.selector_simd_lane_width + } + + pub fn metadata_json(&self) -> Result { + serde_json::to_string(&self.metadata()).map_err(|error| { + RusticolError::serialization(format!("could not serialize runtime metadata: {error}")) + }) + } + + pub fn physics_json(&self) -> Result { + serde_json::to_string(self.physics_v1.get()?).map_err(|error| { + RusticolError::serialization(format!("could not serialize physics metadata: {error}")) + }) + } + + /// Return the validated mutable state needed by the lazy Python + /// high-precision executor. + /// + /// The values have already passed Rusticol's atomic parameter update and + /// derived-parameter refresh logic. They are intentionally exposed only as + /// an internal bridge; f64 evaluation continues to execute entirely in the + /// Python-independent core. + pub fn exact_runtime_state_json(&self) -> Result { + serde_json::to_string(&serde_json::json!({ + "model_parameter_values": self.runtime.model_parameter_values_f64, + "normalization_factor": self.runtime.normalization_factor, + "representative_process_id": self.representative_process_id, + "representative_process_key": self.runtime.key, + "external_permutation": self.external_permutation, + "process_group_member_of": self + .process_group + .as_ref() + .map(|group| group.representative_process_key.as_str()), + "process_group_factor": self.process_group_factor(), + "charge_conjugated": self.charge_conjugated(), + })) + .map_err(|error| { + RusticolError::serialization(format!( + "could not serialize exact-runtime state: {error}" + )) + }) + } + + pub fn process_physics(&self) -> RusticolResult<&ProcessPhysicsV1> { + self.physics_v1.get() + } + + pub fn external_count(&self) -> usize { + self.runtime.external_count + } + + pub fn representative_process_key(&self) -> &str { + &self.runtime.key + } + + /// Full representative-index to public-index external permutation. + pub fn external_permutation(&self) -> &[usize] { + &self.external_permutation + } + + /// Exact factor multiplying every representative matrix element; one + /// unless the selection is a process-group member. + pub fn process_group_factor(&self) -> NativeRationalFactor { + self.process_group.as_ref().map_or( + NativeRationalFactor { + numerator: 1, + denominator: 1, + }, + |group| group.factor, + ) + } + + /// Whether the selection is the charge conjugate of its representative. + pub fn charge_conjugated(&self) -> bool { + self.process_group + .as_ref() + .is_some_and(|group| group.conjugated) + } + + /// Scale representative matrix elements to the selected member. + pub(super) fn apply_process_group_factor(&self, values: &mut [f64]) { + let Some(group) = self.process_group.as_deref() else { + return; + }; + let NativeRationalFactor { + numerator, + denominator, + } = group.factor; + if numerator != denominator { + let (numerator, denominator) = (numerator as f64, denominator as f64); + for value in values { + *value = *value * numerator / denominator; + } + } + } + + /// Translate caller color IDs to the IDs the execution lanes use. + pub(super) fn execution_color_ids<'a>( + &self, + ids: Option<&'a [String]>, + ) -> Option> { + let ids = ids?; + Some(if self.charge_conjugated() { + Cow::Owned( + ids.iter() + .map(|id| charge_conjugate_color_id(id).into_owned()) + .collect(), + ) + } else { + Cow::Borrowed(ids) + }) + } + + /// Translate execution color IDs back to the caller's labels. + pub(super) fn publish_color_ids(&self, ids: &mut [String]) { + if self.charge_conjugated() { + for id in ids { + *id = charge_conjugate_color_id(id).into_owned(); + } + } + } + + /// Load one public-order kinematic point from JSON. + /// + /// The accepted shapes are `[external][4]` and a singleton batch + /// `[[external][4]]`. Components may be JSON numbers or decimal strings. + /// The returned flat values retain the caller's public process ordering; + /// the ordinary evaluation entry points apply any active process + /// permutation when packing runtime inputs. + pub fn load_kinematics_json(&self, path: impl AsRef) -> Result, RusticolError> { + let path = path.as_ref(); + let bytes = fs::read(path).map_err(|error| { + RusticolError::invalid_argument(format!( + "could not read kinematics JSON {}: {error}", + path.display() + )) + })?; + let value: Value = serde_json::from_slice(&bytes).map_err(|error| { + RusticolError::invalid_argument(format!( + "could not parse kinematics JSON {}: {error}", + path.display() + )) + })?; + parse_public_kinematics_point(&value, self.external_count()) + } + + pub fn external_particles(&self) -> Result, RusticolError> { + Ok(self + .physics_v1 + .get()? + .external_particles + .iter() + .map(|item| NativeExternalParticle { + label: item.label, + index: item.index, + side: match item.role { + crate::ParticleRole::Initial => "initial", + crate::ParticleRole::Final => "final", + } + .to_string(), + role: match item.role { + crate::ParticleRole::Initial => "initial", + crate::ParticleRole::Final => "final", + } + .to_string(), + particle: item.particle.clone(), + outgoing_particle: item.particle.clone(), + pdg: item.pdg, + outgoing_pdg: item.pdg, + particle_class: String::new(), + momentum_slot: item.momentum_slot, + }) + .collect()) + } + + pub fn helicities(&self) -> Result, RusticolError> { + Ok(self + .physics_v1 + .get()? + .helicities + .iter() + .map(|item| NativeHelicityConfiguration { + id: item.id.clone(), + index: item.index, + helicities: item.values.clone(), + representative_id: item.representative_id.clone(), + computed: item.computed, + structural_zero: item.structural_zero, + coefficient: item.coefficient, + }) + .collect()) + } + + pub fn color_components(&self) -> Result, RusticolError> { + Ok(self + .physics_v1 + .get()? + .color_components + .iter() + .map(|item| match item { + PhysicsColorComponentV1::LcFlow(flow) => NativeColorComponent { + id: flow.id.clone(), + index: flow.index, + kind: "lc-flow".to_string(), + word: flow.word.clone(), + representative_id: flow.representative_id.clone(), + computed: flow.computed, + coefficient: flow.coefficient, + }, + PhysicsColorComponentV1::ContractedColor(color) => NativeColorComponent { + id: color.id.clone(), + index: color.index, + kind: "contracted-color".to_string(), + word: Vec::new(), + representative_id: color.id.clone(), + computed: true, + coefficient: 1.0, + }, + }) + .collect()) + } + + /// Return the current logical parameter value, including refreshed derived values. + pub fn model_parameter_value(&self, name: &str) -> Result<(f64, f64), RusticolError> { + if !self + .physics_v1 + .get()? + .model_parameters + .iter() + .any(|parameter| parameter.name == name) + { + return Err(RusticolError::model_parameter(format!( + "model parameter {name:?} is not declared by process {}", + self.process + ))); + } + self.runtime.current_model_parameter_value(name) + } + + pub fn model_parameters(&self) -> Result, RusticolError> { + Ok(self + .physics_v1 + .get()? + .model_parameters + .iter() + .enumerate() + .map(|(parameter_index, item)| NativeModelParameter { + name: item.name.clone(), + kind: format!("{:?}", item.kind).to_ascii_lowercase(), + parameter_index, + default: item.default_real, + default_imaginary: item.default_imaginary, + mutable: item.mutable, + }) + .collect()) + } + + pub fn helicity_ids(&self) -> Result, RusticolError> { + Ok(self + .physics_v1 + .get()? + .helicities + .iter() + .map(|item| item.id.clone()) + .collect()) + } + + pub fn color_ids(&self) -> Result, RusticolError> { + Ok(self + .physics_v1 + .get()? + .color_components + .iter() + .map(|item| item.id().to_string()) + .collect()) + } + + pub fn resolved_shape( + &self, + helicity_ids: Option<&[String]>, + color_ids: Option<&[String]>, + ) -> Result<(usize, usize), RusticolError> { + let color_ids = self.execution_color_ids(color_ids); + let color_ids = color_ids.as_deref(); + self.validate_selector_capabilities(helicity_ids.is_some(), color_ids.is_some())?; + let selected_helicities = selector_set(helicity_ids, "helicity")?; + let selected_colors = selector_set(color_ids, "color component")?; + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + if let NativeExecutionLane::OnTheFly(runtime) = &self.execution_lane { + let selection = runtime.selectors.selection( + runtime.lane.seed(), + selected_helicities.as_ref(), + if runtime.uses_contracted_color() { + None + } else { + selected_colors.as_ref() + }, + )?; + return Ok(( + selection.helicity_count(), + if runtime.uses_contracted_color() { + 1 + } else { + selection.color_count() + }, + )); + } + let physics = self.runtime.physics.as_ref().ok_or_else(|| { + RusticolError::artifact( + "schema-v3 artifact is missing resolved physics metadata; regenerate it with pyAmpliCol 0.1.0 or newer", + ) + })?; + let helicity_count = physics + .selected_helicity_indices(selected_helicities.as_ref()) + .map_err(|error| RusticolError::selector(error.to_string()))? + .len(); + let color_count = physics + .selected_color_indices(selected_colors.as_ref()) + .map_err(|error| RusticolError::selector(error.to_string()))? + .len(); + Ok((helicity_count, color_count)) + } + + /// Private compact context for the existing Python benchmark service. + /// Non-OTF lanes return `None`; OTF resolves optional one-based color + /// ordinals without opening process-wide physics metadata. + pub fn on_the_fly_benchmark_context_json( + &self, + requested_color_ids: Option<&[String]>, + ) -> RusticolResult> { + let requested_color_ids = self.execution_color_ids(requested_color_ids); + let requested_color_ids = requested_color_ids.as_deref(); + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + if let NativeExecutionLane::OnTheFly(runtime) = &self.execution_lane { + let selection = runtime + .selectors + .selection(runtime.lane.seed(), None, None)?; + let mut selected_color_ids = Vec::new(); + if let Some(requested) = requested_color_ids { + if runtime.uses_contracted_color() { + return Err(RusticolError::selector( + "on-the-fly contracted color does not expose benchmark color selectors", + )); + } + selected_color_ids + .try_reserve_exact(requested.len()) + .map_err(|error| { + RusticolError::invalid_argument(format!( + "on-the-fly benchmark selector allocation failed: {error}" + )) + })?; + for value in requested { + let resolved = match value.parse::() { + Ok(ordinal) if ordinal.to_string() == value.trim() => { + if ordinal == 0 || ordinal > selection.color_count() { + return Err(RusticolError::selector(format!( + "color-flow ordinal {value:?} is out of range; choose 1..{} or a stable color component ID", + selection.color_count(), + ))); + } + selection.color_id_at(ordinal - 1)? + } + _ => { + runtime.selectors.parse_color_id(value)?; + value.clone() + } + }; + selected_color_ids.push(resolved); + } + } + self.publish_color_ids(&mut selected_color_ids); + return serde_json::to_string(&serde_json::json!({ + "process_id": self.process_key, + "process_expression": self.process, + "color_accuracy": self.runtime.color_accuracy, + "helicity_count": selection.helicity_count(), + "color_count": if runtime.uses_contracted_color() { 1 } else { selection.color_count() }, + "selected_color_ids": selected_color_ids, + })) + .map(Some) + .map_err(|error| { + RusticolError::serialization(format!( + "could not serialize on-the-fly benchmark context: {error}" + )) + }); + } + Ok(None) + } + + /// Resolve existing public per-point selector IDs to compact public-axis + /// ordinals without materializing process-wide physics metadata. + pub fn on_the_fly_selector_ordinals_json( + &self, + helicity_ids: Option<&[String]>, + color_ids: Option<&[String]>, + ) -> RusticolResult> { + let color_ids = self.execution_color_ids(color_ids); + let color_ids = color_ids.as_deref(); + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + if let NativeExecutionLane::OnTheFly(runtime) = &self.execution_lane { + if runtime.uses_contracted_color() && color_ids.is_some() { + return Err(RusticolError::selector( + "on-the-fly contracted color does not expose color selector ordinals", + )); + } + let helicity_ordinals = helicity_ids + .map(|ids| { + ids.iter() + .map(|id| { + let selected = BTreeSet::from([id.clone()]); + runtime + .selectors + .selection(runtime.lane.seed(), Some(&selected), None)? + .helicity_ordinal_at(0) + }) + .collect::>>() + }) + .transpose()?; + let color_ordinals = color_ids + .map(|ids| { + ids.iter() + .map(|id| { + let selected = BTreeSet::from([id.clone()]); + runtime + .selectors + .selection(runtime.lane.seed(), None, Some(&selected))? + .color_ordinal_at(0) + }) + .collect::>>() + }) + .transpose()?; + return serde_json::to_string(&serde_json::json!({ + "helicity_ordinals": helicity_ordinals, + "color_ordinals": color_ordinals, + })) + .map(Some) + .map_err(|error| { + RusticolError::serialization(format!( + "could not serialize on-the-fly selector ordinals: {error}" + )) + }); + } + Ok(None) + } + + /// Return private read-only production introspection for the loaded compact + /// query-family lane. + /// + /// This observes the same retained selector/family caches used by public + /// evaluation. It does not construct selectors or open dense physics + /// metadata. A recurrence lane reports the same census only when it owns + /// the authenticated singleton-helicity companion; other lanes return + /// `None`. + pub fn on_the_fly_runtime_state_census_json(&self) -> RusticolResult> { + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + { + let census = match &self.execution_lane { + NativeExecutionLane::OnTheFly(runtime) => { + Some(runtime.state_census(&self.process_key)?) + } + NativeExecutionLane::Recurrence(runtime) => { + runtime.helicity_selector_companion_state_census(&self.process_key)? + } + _ => None, + }; + let Some(census) = census else { + return Ok(None); + }; + serde_json::to_string(&census).map(Some).map_err(|error| { + RusticolError::serialization(format!( + "could not serialize on-the-fly runtime state census: {error}" + )) + }) + } + #[cfg(not(any(feature = "f64-compiled", feature = "f64-symjit")))] + Ok(None) + } + + /// Explicitly construct and retain one selected OTF family, then execute + /// exactly one binary64 point before committing its selector identity. + /// Progress callbacks run only on the coordinating caller thread. + /// `n_cores` overrides cold query construction for this call only; `None` + /// uses the process-output default. Positive overrides are capped by host + /// availability and do not invalidate an already retained family. + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + pub fn warm_up_f64<'observer>( + &mut self, + momenta: &[f64], + helicity_ids: Option<&[String]>, + color_ids: Option<&[String]>, + n_cores: Option, + observer: Option< + &'observer mut super::on_the_fly_warm_up::NativeOnTheFlyWarmUpObserver<'observer>, + >, + ) -> RusticolResult { + self.warm_up_on_the_fly_f64_with_selectors( + momenta, + helicity_ids, + color_ids, + n_cores, + observer, + ) + } + + /// Explicitly named alias for [`Self::warm_up_f64`], retained for bindings + /// which make the on-the-fly-only capability visible in their method name. + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + pub fn warm_up_on_the_fly_f64_with_selectors<'observer>( + &mut self, + momenta: &[f64], + helicity_ids: Option<&[String]>, + color_ids: Option<&[String]>, + n_cores: Option, + observer: Option< + &'observer mut super::on_the_fly_warm_up::NativeOnTheFlyWarmUpObserver<'observer>, + >, + ) -> RusticolResult { + let color_ids = self.execution_color_ids(color_ids); + let color_ids = color_ids.as_deref(); + let expected = self.runtime.external_count.checked_mul(4).ok_or_else(|| { + RusticolError::invalid_argument("on-the-fly warm-up momentum shape exceeds usize") + })?; + if momenta.len() != expected { + return Err(RusticolError::invalid_argument(format!( + "on-the-fly warm-up requires exactly one point ({expected} binary64 momentum values), received {}", + momenta.len() + ))); + } + let crossing_lookup = std::mem::take(&mut self.input_crossing_map); + let mut selector_scratch = std::mem::take(&mut self.point_selector_scratch); + let result = (|| { + let selected_helicities = selector_scratch + .helicity_selector_sets + .resolve(helicity_ids, "helicity")?; + let selected_colors = selector_scratch + .color_selector_sets + .resolve(color_ids, "color component")?; + let batch = F64MomentumBatchView::from_contiguous_prevalidated( + momenta, + 1, + self.runtime.external_count, + crossing_lookup.as_deref(), + )?; + let NativeExecutionLane::OnTheFly(runtime) = &mut self.execution_lane else { + return Err(RusticolError::unsupported_runtime_capability( + ON_THE_FLY_RUNTIME_CAPABILITY, + "the explicit warm-up API requires an on-the-fly artifact", + )); + }; + runtime.warm_up_f64( + &self.runtime, + batch, + selected_helicities, + selected_colors, + n_cores, + observer, + ) + })(); + self.point_selector_scratch = selector_scratch; + self.input_crossing_map = crossing_lookup; + result + } + + /// Resolve and retain batch-global recurrence selectors once. + /// + /// The returned handle is tied to this artifact and representative + /// process. Handle creation may allocate; repeated evaluation through the + /// prepared-plan API does not rebuild string selector sets. + pub fn prepare_recurrence_selector_plan( + &self, + helicity_ids: Option<&[String]>, + color_ids: Option<&[String]>, + ) -> RusticolResult { + let NativeExecutionLane::Recurrence(runtime) = &self.execution_lane else { + return Err(RusticolError::invalid_argument( + "recurrence selector plans require a recurrence artifact", + )); + }; + let color_ids = self.execution_color_ids(color_ids); + let color_ids = color_ids.as_deref(); + self.validate_selector_capabilities(helicity_ids.is_some(), color_ids.is_some())?; + let selected_helicities = selector_set(helicity_ids, "helicity")?; + let selected_colors = selector_set(color_ids, "color component")?; + let physics = self.runtime.physics.as_ref().ok_or_else(|| { + RusticolError::artifact("recurrence execution requires physics metadata") + })?; + runtime.validate_global_selectors( + physics, + selected_helicities.as_ref(), + selected_colors.as_ref(), + )?; + Ok(NativeRecurrenceSelectorPlan { + artifact_root: self.root.clone(), + process_key: self.process_key.clone(), + external_permutation: self.external_permutation.clone(), + selected_helicities, + selected_colors, + }) + } + + pub fn evaluate_f64( + &mut self, + momenta: &[f64], + point_count: usize, + ) -> Result, RusticolError> { + validate_flat_momentum_shape(momenta.len(), point_count, self.runtime.external_count)?; + let mut output = vec![0.0; point_count]; + let crossing_lookup = std::mem::take(&mut self.input_crossing_map); + let result = (|| { + let batch = F64MomentumBatchView::from_contiguous_prevalidated( + momenta, + point_count, + self.runtime.external_count, + crossing_lookup.as_deref(), + )?; + self.run_f64_batch_into(batch, &mut output) + })(); + self.input_crossing_map = crossing_lookup; + result?; + self.apply_process_group_factor(&mut output); + Ok(output) + } + + pub fn evaluate_f64_into( + &mut self, + momenta: &[f64], + point_count: usize, + output: &mut [f64], + ) -> RusticolResult<()> { + validate_flat_momentum_shape(momenta.len(), point_count, self.runtime.external_count)?; + if output.len() != point_count { + return Err(RusticolError::invalid_argument(format!( + "evaluation output has length {}, expected {point_count}", + output.len() + ))); + } + let crossing_lookup = std::mem::take(&mut self.input_crossing_map); + let result = (|| { + let batch = F64MomentumBatchView::from_contiguous_prevalidated( + momenta, + point_count, + self.runtime.external_count, + crossing_lookup.as_deref(), + )?; + self.run_f64_batch_into(batch, output) + })(); + self.input_crossing_map = crossing_lookup; + result?; + self.apply_process_group_factor(output); + Ok(()) + } + + /// Evaluate recurrence totals with a previously resolved selector plan. + pub fn evaluate_f64_into_with_recurrence_selector_plan( + &mut self, + plan: &NativeRecurrenceSelectorPlan, + momenta: &[f64], + point_count: usize, + output: &mut [f64], + ) -> RusticolResult<()> { + if !matches!(&self.execution_lane, NativeExecutionLane::Recurrence(_)) { + return Err(RusticolError::invalid_argument( + "recurrence selector plans require a recurrence artifact", + )); + } + plan.ensure_matches(self)?; + validate_flat_momentum_shape(momenta.len(), point_count, self.runtime.external_count)?; + if output.len() != point_count { + return Err(RusticolError::invalid_argument(format!( + "evaluation output has length {}, expected {point_count}", + output.len() + ))); + } + let crossing_lookup = std::mem::take(&mut self.input_crossing_map); + let result = (|| { + let batch = F64MomentumBatchView::from_contiguous_prevalidated( + momenta, + point_count, + self.runtime.external_count, + crossing_lookup.as_deref(), + )?; + if plan.selected_helicities.is_some() || plan.selected_colors.is_some() { + self.run_selected_f64_batch_into( + batch, + plan.selected_helicities.as_ref(), + plan.selected_colors.as_ref(), + output, + ) + } else { + self.run_f64_batch_into(batch, output) + } + })(); + self.input_crossing_map = crossing_lookup; + result?; + self.apply_process_group_factor(output); + Ok(()) + } + + /// Evaluate one total per point with optional global or per-point selectors. + /// + /// Global selectors retain the existing subset semantics. Per-point + /// selectors are resolved physical-axis indices and contain exactly one + /// selector for every input point. The two forms are mutually exclusive on + /// the same axis. An omitted axis is summed over all components retained by + /// the artifact. + pub fn evaluate_f64_with_selectors( + &mut self, + momenta: &[f64], + point_count: usize, + helicity_ids: Option<&[String]>, + color_ids: Option<&[String]>, + helicity_by_point: Option<&[u32]>, + color_by_point: Option<&[u32]>, + ) -> Result, RusticolError> { + validate_flat_momentum_shape(momenta.len(), point_count, self.runtime.external_count)?; + let mut output = vec![0.0; point_count]; + self.evaluate_f64_into_with_selectors( + momenta, + point_count, + helicity_ids, + color_ids, + helicity_by_point, + color_by_point, + &mut output, + )?; + Ok(output) + } + + #[allow(clippy::too_many_arguments)] + pub fn evaluate_f64_into_with_selectors( + &mut self, + momenta: &[f64], + point_count: usize, + helicity_ids: Option<&[String]>, + color_ids: Option<&[String]>, + helicity_by_point: Option<&[u32]>, + color_by_point: Option<&[u32]>, + output: &mut [f64], + ) -> RusticolResult<()> { + let color_ids = self.execution_color_ids(color_ids); + self.evaluate_representative_f64_into_with_selectors( + momenta, + point_count, + helicity_ids, + color_ids.as_deref(), + helicity_by_point, + color_by_point, + output, + )?; + self.apply_process_group_factor(output); + Ok(()) + } + + #[allow(clippy::too_many_arguments)] + fn evaluate_representative_f64_into_with_selectors( + &mut self, + momenta: &[f64], + point_count: usize, + helicity_ids: Option<&[String]>, + color_ids: Option<&[String]>, + helicity_by_point: Option<&[u32]>, + color_by_point: Option<&[u32]>, + output: &mut [f64], + ) -> RusticolResult<()> { + validate_flat_momentum_shape(momenta.len(), point_count, self.runtime.external_count)?; + if output.len() != point_count { + return Err(RusticolError::invalid_argument(format!( + "evaluation output has length {}, expected {point_count}", + output.len() + ))); + } + if helicity_ids.is_some() && helicity_by_point.is_some() { + return Err(RusticolError::selector( + "helicities and helicity_by_point are mutually exclusive", + )); + } + if color_ids.is_some() && color_by_point.is_some() { + return Err(RusticolError::selector( + "color_flows and color_flow_by_point are mutually exclusive", + )); + } + self.validate_selector_capabilities( + helicity_ids.is_some() || helicity_by_point.is_some(), + color_ids.is_some() || color_by_point.is_some(), + )?; + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + if matches!(&self.execution_lane, NativeExecutionLane::OnTheFly(_)) { + return self.evaluate_on_the_fly_f64_into_with_selectors( + momenta, + point_count, + helicity_ids, + color_ids, + helicity_by_point, + color_by_point, + output, + ); + } + let physics = self.runtime.physics.clone().ok_or_else(|| { + RusticolError::artifact( + "schema-v3 artifact is missing resolved physics metadata; regenerate it with pyAmpliCol 0.1.0 or newer", + ) + })?; + let crossing_lookup = std::mem::take(&mut self.input_crossing_map); + let mut selector_scratch = std::mem::take(&mut self.point_selector_scratch); + let result = (|| { + if helicity_by_point.is_some() || color_by_point.is_some() { + selector_scratch.prepare_singletons(&physics); + } + let PointSelectorExecutionScratch { + planner, + gathered_batch, + partition_totals, + output_totals, + helicity_selector_sets, + color_selector_sets, + helicity_singletons, + color_singletons, + } = &mut selector_scratch; + let selected_helicities = helicity_selector_sets.resolve(helicity_ids, "helicity")?; + let selected_colors = color_selector_sets.resolve(color_ids, "color component")?; + let batch = F64MomentumBatchView::from_contiguous_prevalidated( + momenta, + point_count, + self.runtime.external_count, + crossing_lookup.as_deref(), + )?; + let plan = planner.build( + point_count, + helicity_by_point, + color_by_point, + physics.manifest.helicities.len(), + physics.manifest.color_components.len(), + )?; + if plan == PointSelectorPlan::None { + if selected_helicities.is_some() || selected_colors.is_some() { + return self.run_selected_f64_batch_into( + batch, + selected_helicities, + selected_colors, + output, + ); + } + return self.run_f64_batch_into(batch, output); + } + + self.record_resolved_warnings(helicity_ids, color_ids)?; + if let PointSelectorPlan::Homogeneous(key) = plan { + // Selecting the sole retained value of an axis is identical + // to leaving that axis unselected. Generation-specialized + // recurrence artifacts use exactly this shape in scalar + // performance and event-loop calls; route it through the + // ordinary borrowed-output path so we do not rescan singleton + // string sets or stage through a second output buffer. + let point_helicities = key.helicity_index.and_then(|index| { + (physics.manifest.helicities.len() != 1).then(|| &helicity_singletons[index]) + }); + let point_colors = key.color_index.and_then(|index| { + (physics.manifest.color_components.len() != 1).then(|| &color_singletons[index]) + }); + let effective_helicities = point_helicities.or(selected_helicities); + let effective_colors = point_colors.or(selected_colors); + if effective_helicities.is_none() && effective_colors.is_none() { + self.run_f64_batch_into(batch, output)?; + } else { + self.run_selected_f64_batch_into( + batch, + effective_helicities, + effective_colors, + output, + )?; + } + return Ok(()); + } + + output_totals.resize(point_count, 0.0); + output_totals.fill(0.0); + let partition_count = planner.partitions().len(); + for partition_index in 0..partition_count { + let partition = planner.partitions()[partition_index]; + let point_helicities = partition + .key + .helicity_index + .map(|index| &helicity_singletons[index]); + let point_colors = partition + .key + .color_index + .map(|index| &color_singletons[index]); + let effective_helicities = point_helicities.or(selected_helicities); + let effective_colors = point_colors.or(selected_colors); + match partition.rows { + PointSelectorRows::Contiguous { start, end } => { + self.run_selected_f64_batch_into( + batch.subview(start, end)?, + effective_helicities, + effective_colors, + &mut output_totals[start..end], + )?; + } + rows @ PointSelectorRows::Gathered { .. } => { + let point_indices = planner.gathered_rows(rows); + let gathered_batch = + fill_gathered_batch_from_view(gathered_batch, batch, point_indices)?; + partition_totals.resize(partition.rows.len(), 0.0); + partition_totals.fill(0.0); + self.run_selected_f64_batch_into( + F64MomentumBatchView::from_nested( + gathered_batch, + self.runtime.external_count, + )?, + effective_helicities, + effective_colors, + partition_totals, + )?; + scatter_partition_totals( + output_totals, + partition_totals, + partition.rows, + planner, + ); + } + } + } + output.copy_from_slice(output_totals); + Ok(()) + })(); + self.point_selector_scratch = selector_scratch; + self.input_crossing_map = crossing_lookup; + result + } + + /// Route ordinary public evaluation through the compact on-the-fly + /// selector domain. This must run before any dense `PhysicsRuntime` + /// access: on-the-fly artifacts intentionally leave that metadata lazy. + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + #[allow(clippy::too_many_arguments)] + fn evaluate_on_the_fly_f64_into_with_selectors( + &mut self, + momenta: &[f64], + point_count: usize, + helicity_ids: Option<&[String]>, + color_ids: Option<&[String]>, + helicity_by_point: Option<&[u32]>, + color_by_point: Option<&[u32]>, + output: &mut [f64], + ) -> RusticolResult<()> { + let crossing_lookup = std::mem::take(&mut self.input_crossing_map); + let mut selector_scratch = std::mem::take(&mut self.point_selector_scratch); + let result = (|| { + let PointSelectorExecutionScratch { + planner, + gathered_batch, + partition_totals, + output_totals, + helicity_selector_sets, + color_selector_sets, + .. + } = &mut selector_scratch; + let selected_helicities = helicity_selector_sets.resolve(helicity_ids, "helicity")?; + let selected_colors = color_selector_sets.resolve(color_ids, "color component")?; + if helicity_by_point.is_none() && color_by_point.is_none() { + let batch = F64MomentumBatchView::from_contiguous_prevalidated( + momenta, + point_count, + self.runtime.external_count, + crossing_lookup.as_deref(), + )?; + return self.run_selected_f64_batch_into( + batch, + selected_helicities, + selected_colors, + output, + ); + } + // Per-point routing retains its ordinal planner. Fixed global + // selection is handled above so its IDs are parsed only by the + // lane on a cold family and matched allocation-free once warm. + let (selected_helicity_ordinals, selected_color_ordinals) = + self.on_the_fly_selected_ordinals(selected_helicities, selected_colors)?; + let batch = F64MomentumBatchView::from_contiguous_prevalidated( + momenta, + point_count, + self.runtime.external_count, + crossing_lookup.as_deref(), + )?; + + let (helicity_count, color_count) = self.on_the_fly_selector_counts()?; + let plan = planner.build( + point_count, + helicity_by_point, + color_by_point, + helicity_count, + color_count, + )?; + self.record_resolved_warnings(helicity_ids, color_ids)?; + output_totals.resize(point_count, 0.0); + output_totals.fill(0.0); + if let PointSelectorPlan::Homogeneous(key) = plan { + let point_helicity = key.helicity_index.map(|index| [index]); + let point_color = key.color_index.map(|index| [index]); + self.run_on_the_fly_f64_batch_into_by_ordinals( + batch, + point_helicity + .as_ref() + .map(<[usize; 1]>::as_slice) + .or(selected_helicity_ordinals.as_deref()), + point_color + .as_ref() + .map(<[usize; 1]>::as_slice) + .or(selected_color_ordinals.as_deref()), + output_totals, + )?; + output.copy_from_slice(output_totals); + return Ok(()); + } + + let partition_count = planner.partitions().len(); + for partition_index in 0..partition_count { + let partition = planner.partitions()[partition_index]; + let point_helicity = partition.key.helicity_index.map(|index| [index]); + let point_color = partition.key.color_index.map(|index| [index]); + let effective_helicities = point_helicity + .as_ref() + .map(<[usize; 1]>::as_slice) + .or(selected_helicity_ordinals.as_deref()); + let effective_colors = point_color + .as_ref() + .map(<[usize; 1]>::as_slice) + .or(selected_color_ordinals.as_deref()); + match partition.rows { + PointSelectorRows::Contiguous { start, end } => { + self.run_on_the_fly_f64_batch_into_by_ordinals( + batch.subview(start, end)?, + effective_helicities, + effective_colors, + &mut output_totals[start..end], + )?; + } + rows @ PointSelectorRows::Gathered { .. } => { + let point_indices = planner.gathered_rows(rows); + let gathered_batch = + fill_gathered_batch_from_view(gathered_batch, batch, point_indices)?; + partition_totals.resize(partition.rows.len(), 0.0); + partition_totals.fill(0.0); + self.run_on_the_fly_f64_batch_into_by_ordinals( + F64MomentumBatchView::from_nested( + gathered_batch, + self.runtime.external_count, + )?, + effective_helicities, + effective_colors, + partition_totals, + )?; + scatter_partition_totals( + output_totals, + partition_totals, + partition.rows, + planner, + ); + } + } + } + output.copy_from_slice(output_totals); + Ok(()) + })(); + self.point_selector_scratch = selector_scratch; + self.input_crossing_map = crossing_lookup; + result + } + + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + fn on_the_fly_selector_counts(&self) -> RusticolResult<(usize, usize)> { + let NativeExecutionLane::OnTheFly(runtime) = &self.execution_lane else { + return Err(RusticolError::internal( + "compact selector counts require an on-the-fly execution lane", + )); + }; + runtime.public_selector_counts() + } + + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + fn on_the_fly_selected_ordinals( + &self, + selected_helicities: Option<&BTreeSet>, + selected_colors: Option<&BTreeSet>, + ) -> RusticolResult { + let NativeExecutionLane::OnTheFly(runtime) = &self.execution_lane else { + return Err(RusticolError::internal( + "compact selector ordinals require an on-the-fly execution lane", + )); + }; + if runtime.uses_contracted_color() { + if selected_colors.is_some() { + return Err(RusticolError::selector( + "on-the-fly contracted color does not expose color selector ordinals", + )); + } + let (helicity_ordinals, _) = runtime + .selectors + .selected_ordinals(selected_helicities, None)?; + Ok((helicity_ordinals, None)) + } else { + runtime + .selectors + .selected_ordinals(selected_helicities, selected_colors) + } + } + + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + fn run_on_the_fly_f64_batch_into_by_ordinals( + &mut self, + batch: F64MomentumBatchView<'_>, + helicity_ordinals: Option<&[usize]>, + color_ordinals: Option<&[usize]>, + output: &mut [f64], + ) -> RusticolResult<()> { + let NativeExecutionLane::OnTheFly(runtime) = &mut self.execution_lane else { + return Err(RusticolError::internal( + "compact selector ordinals require an on-the-fly execution lane", + )); + }; + runtime.run_total_into_by_ordinals_unprofiled( + &self.runtime, + batch, + helicity_ordinals, + color_ordinals, + output, + ) + } + + fn run_f64_batch_into( + &mut self, + batch: F64MomentumBatchView<'_>, + output: &mut [f64], + ) -> RusticolResult<()> { + match &mut self.execution_lane { + NativeExecutionLane::Compiled => self.runtime.run_f64_into_unprofiled(batch, output), + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + NativeExecutionLane::Eager(runtime) => { + runtime.run_f64_view_into_unprofiled(&mut self.runtime, batch, output) + } + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + NativeExecutionLane::Recurrence(runtime) => { + runtime.run_f64_view_into_unprofiled(&mut self.runtime, batch, None, None, output) + } + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + NativeExecutionLane::OnTheFly(runtime) => { + runtime.run_total_into_unprofiled(&self.runtime, batch, None, None, output) + } + } + } + + fn run_selected_f64_batch_into( + &mut self, + batch: F64MomentumBatchView<'_>, + selected_helicities: Option<&BTreeSet>, + selected_colors: Option<&BTreeSet>, + output: &mut [f64], + ) -> RusticolResult<()> { + match &mut self.execution_lane { + NativeExecutionLane::Compiled => self.runtime.run_f64_selected_into_unprofiled( + batch, + selected_helicities, + selected_colors, + output, + ), + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + NativeExecutionLane::Eager(runtime) => runtime.run_f64_view_selected_into_unprofiled( + &mut self.runtime, + batch, + selected_helicities, + selected_colors, + output, + ), + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + NativeExecutionLane::Recurrence(runtime) => runtime.run_f64_view_into_unprofiled( + &mut self.runtime, + batch, + selected_helicities, + selected_colors, + output, + ), + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + NativeExecutionLane::OnTheFly(runtime) => runtime.run_total_into_unprofiled( + &self.runtime, + batch, + selected_helicities, + selected_colors, + output, + ), + } + } + + fn run_resolved_f64_batch( + &mut self, + batch: F64MomentumBatchView<'_>, + selected_helicities: Option<&BTreeSet>, + selected_colors: Option<&BTreeSet>, + ) -> Result, RusticolError> { + match &mut self.execution_lane { + NativeExecutionLane::Compiled => self.runtime.run_resolved_f64_unprofiled( + batch, + selected_helicities, + selected_colors, + ), + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + NativeExecutionLane::Eager(runtime) => runtime.run_resolved_f64_view_unprofiled( + &mut self.runtime, + batch, + selected_helicities, + selected_colors, + ), + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + NativeExecutionLane::Recurrence(runtime) => { + let nested = batch.materialize_nested(); + runtime + .run_resolved_f64( + &mut self.runtime, + &nested, + selected_helicities, + selected_colors, + ) + .map(|(resolved, _profile)| resolved) + } + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + NativeExecutionLane::OnTheFly(runtime) => runtime + .run_resolved(&self.runtime, batch, selected_helicities, selected_colors) + .map(|(resolved, _profile)| resolved), + } + } + + fn run_selected_f64_batch_profile( + &mut self, + batch: &[Vec<[f64; 4]>], + selected_helicities: Option<&BTreeSet>, + selected_colors: Option<&BTreeSet>, + ) -> Result<(Vec, RuntimeProfile), RusticolError> { + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + if matches!(&self.execution_lane, NativeExecutionLane::Compiled) { + let before = compiled_direct_profile_snapshot(&self.runtime)?; + if before.engine_count != 0 { + let total_start = Instant::now(); + let mut values = vec![0.0; batch.len()]; + self.runtime.run_f64_selected_into_unprofiled( + F64MomentumBatchView::from_nested(batch, self.runtime.external_count)?, + selected_helicities, + selected_colors, + &mut values, + )?; + let total_s = total_start.elapsed().as_secs_f64(); + let after = compiled_direct_profile_snapshot(&self.runtime)?; + let f64_bytes = std::mem::size_of::() as u64; + let complex_bytes = 2 * f64_bytes; + let mut profile = RuntimeProfile { + // The direct hot path is deliberately uninstrumented. + // Until its three coarse phase clocks are added, keep + // the complete measured envelope in orchestration so + // top-level accounting remains exact rather than + // manufacturing dense-stage timing fields. + orchestration_s: total_s, + total_s, + source_component_count: after + .source_fill_bytes + .saturating_sub(before.source_fill_bytes) + / complex_bytes, + momentum_component_count: after + .momentum_fill_bytes + .saturating_sub(before.momentum_fill_bytes) + / f64_bytes, + model_parameter_component_count: after + .parameter_fill_bytes + .saturating_sub(before.parameter_fill_bytes) + / complex_bytes, + state_clear_component_count: after + .amplitude_clear_bytes + .saturating_sub(before.amplitude_clear_bytes) + / complex_bytes, + evaluator_backend_call_count: after + .backend_call_count + .saturating_sub(before.backend_call_count), + compiled_direct_arena_engine_count: after.engine_count as u64, + compiled_direct_arena_call_count: after + .backend_call_count + .saturating_sub(before.backend_call_count), + compiled_direct_arena_boundary_input_bytes: after.boundary_input_bytes, + compiled_direct_arena_boundary_current_output_bytes: after + .boundary_current_output_bytes, + compiled_direct_arena_boundary_amplitude_output_bytes: after + .boundary_amplitude_output_bytes, + compiled_direct_arena_internal_broadcast_bytes: after + .scalar_broadcast_fill_bytes + .saturating_sub(before.scalar_broadcast_fill_bytes), + total_materialized_value_count: batch.len() as u64, + ..RuntimeProfile::default() + }; + attach_compiled_direct_configuration(&mut profile, after); + return Ok((values, profile)); + } + } + match &mut self.execution_lane { + NativeExecutionLane::Compiled => { + self.runtime + .run_f64_selected_totals(batch, selected_helicities, selected_colors) + } + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + NativeExecutionLane::Eager(runtime) => { + let (resolved, mut profile) = runtime.run_resolved_f64_profile( + &mut self.runtime, + batch, + selected_helicities, + selected_colors, + )?; + let materialization_start = Instant::now(); + let values = resolved_f64_totals(&resolved)?; + profile.total_materialization_s += materialization_start.elapsed().as_secs_f64(); + profile.total_materialized_value_count += values.len() as u64; + Ok((values, profile)) + } + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + NativeExecutionLane::Recurrence(runtime) => { + let (resolved, mut profile) = runtime.run_resolved_f64( + &mut self.runtime, + batch, + selected_helicities, + selected_colors, + )?; + let materialization_start = Instant::now(); + let values = resolved_f64_totals(&resolved)?; + profile.total_materialization_s += materialization_start.elapsed().as_secs_f64(); + profile.total_materialized_value_count += values.len() as u64; + Ok((values, profile)) + } + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + NativeExecutionLane::OnTheFly(runtime) => { + let view = F64MomentumBatchView::from_nested(batch, self.runtime.external_count)?; + let mut values = vec![0.0; batch.len()]; + let mut profile = runtime.run_total_into( + &self.runtime, + view, + selected_helicities, + selected_colors, + &mut values, + )?; + profile.total_materialized_value_count += values.len() as u64; + Ok((values, profile)) + } + } + } + + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + fn run_on_the_fly_f64_batch_profile_by_ordinals( + &mut self, + batch: &[Vec<[f64; 4]>], + helicity_ordinals: Option<&[usize]>, + color_ordinals: Option<&[usize]>, + ) -> Result<(Vec, RuntimeProfile), RusticolError> { + let view = F64MomentumBatchView::from_nested(batch, self.runtime.external_count)?; + let mut values = vec![0.0; batch.len()]; + let NativeExecutionLane::OnTheFly(runtime) = &mut self.execution_lane else { + return Err(RusticolError::internal( + "compact selector ordinals require an on-the-fly execution lane", + )); + }; + let mut profile = runtime.run_total_into_by_ordinals( + &self.runtime, + view, + helicity_ordinals, + color_ordinals, + &mut values, + )?; + profile.total_materialized_value_count += values.len() as u64; + Ok((values, profile)) + } + + pub fn benchmark_f64_wall_time( + &mut self, + momenta: &[f64], + point_count: usize, + repetitions: usize, + helicity_ids: Option<&[String]>, + color_ids: Option<&[String]>, + ) -> Result { + self.benchmark_f64_wall_time_with_selectors( + momenta, + point_count, + repetitions, + helicity_ids, + color_ids, + None, + None, + ) + } + + #[allow(clippy::too_many_arguments)] + pub fn benchmark_f64_wall_time_with_selectors( + &mut self, + momenta: &[f64], + point_count: usize, + repetitions: usize, + helicity_ids: Option<&[String]>, + color_ids: Option<&[String]>, + helicity_by_point: Option<&[u32]>, + color_by_point: Option<&[u32]>, + ) -> Result { + if repetitions == 0 { + return Err(RusticolError::invalid_argument( + "benchmark repetitions must be positive", + )); + } + validate_flat_momentum_shape(momenta.len(), point_count, self.runtime.external_count)?; + let mut output = vec![0.0; point_count]; + let started = Instant::now(); + for _ in 0..repetitions { + self.evaluate_f64_into_with_selectors( + momenta, + point_count, + helicity_ids, + color_ids, + helicity_by_point, + color_by_point, + &mut output, + )?; + std::hint::black_box(&output); + } + Ok(started.elapsed().as_secs_f64()) + } + + /// Profile the warmed Direct-Arena timing boundary used by + /// [`Self::benchmark_f64_wall_time`] for batch-global selectors. + /// + /// The borrowed flat input and the caller-sized output are prepared once + /// outside the repeated region. Every measured repetition then uses the + /// allocation-free `evaluate_f64_into_with_selectors` path. This is kept + /// separate from the public diagnostic profiler below, whose materialized + /// input and owned result remain useful when inspecting legacy lanes. + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + pub fn evaluate_f64_arena_profile_repeated( + &mut self, + momenta: &[f64], + point_count: usize, + repetitions: usize, + helicity_ids: Option<&[String]>, + color_ids: Option<&[String]>, + ) -> Result { + if repetitions == 0 { + return Err(RusticolError::invalid_argument( + "arena profile repetitions must be positive", + )); + } + validate_flat_momentum_shape(momenta.len(), point_count, self.runtime.external_count)?; + let compiled_lane = match &self.execution_lane { + NativeExecutionLane::Compiled => { + let snapshot = compiled_direct_profile_snapshot(&self.runtime)?; + if snapshot.engine_count == 0 { + return Err(RusticolError::compatibility( + "warmed Arena profiling requires a compiled Direct-Arena artifact", + )); + } + true + } + NativeExecutionLane::Eager(_) => false, + NativeExecutionLane::Recurrence(_) => { + return Err(RusticolError::compatibility( + "warmed Arena profiling is available only for eager and compiled execution", + )); + } + NativeExecutionLane::OnTheFly(_) => false, + }; + let measured_points = point_count.checked_mul(repetitions).ok_or_else(|| { + RusticolError::invalid_argument("arena profile point count overflowed") + })?; + let measured_input_components = + momenta.len().checked_mul(repetitions).ok_or_else(|| { + RusticolError::invalid_argument( + "arena profile native input component count overflowed", + ) + })?; + let measured_points_u64 = u64::try_from(measured_points).map_err(|_| { + RusticolError::invalid_argument("arena profile point count does not fit in u64") + })?; + let measured_input_components_u64 = + u64::try_from(measured_input_components).map_err(|_| { + RusticolError::invalid_argument( + "arena profile native input component count does not fit in u64", + ) + })?; + let repetitions_u64 = u64::try_from(repetitions).map_err(|_| { + RusticolError::invalid_argument("arena profile repetition count does not fit in u64") + })?; + + // The result allocation is deliberately outside the measured repeated + // boundary, matching the native headline timer. Prime the exact + // selector and workspace route once before the counter snapshot so the + // measured region is independently warmed even when this private + // operation is called directly. + let mut values = vec![0.0; point_count]; + self.evaluate_f64_into_with_selectors( + momenta, + point_count, + helicity_ids, + color_ids, + None, + None, + &mut values, + )?; + let compiled_before = if compiled_lane { + Some(compiled_direct_profile_snapshot(&self.runtime)?) + } else { + None + }; + let started = Instant::now(); + for _ in 0..repetitions { + std::hint::black_box(&mut values); + self.evaluate_f64_into_with_selectors( + momenta, + point_count, + helicity_ids, + color_ids, + None, + None, + &mut values, + )?; + std::hint::black_box(&values); + } + let total_s = started.elapsed().as_secs_f64(); + let mut runtime_profile = RuntimeProfile { + // The warmed Direct-Arena boundary is deliberately free of phase + // clocks. Keep its complete measured envelope in orchestration. + // Callers must treat leaf-evaluator phase timing as unavailable, + // rather than interpreting the zero-valued phase fields as + // measurements below clock resolution. + orchestration_s: total_s, + total_s, + total_materialized_value_count: measured_points_u64, + ..RuntimeProfile::default() + }; + if let Some(before) = compiled_before { + let after = compiled_direct_profile_snapshot(&self.runtime)?; + let f64_bytes = std::mem::size_of::() as u64; + let complex_bytes = 2 * f64_bytes; + runtime_profile.source_component_count = after + .source_fill_bytes + .saturating_sub(before.source_fill_bytes) + / complex_bytes; + runtime_profile.momentum_component_count = after + .momentum_fill_bytes + .saturating_sub(before.momentum_fill_bytes) + / f64_bytes; + runtime_profile.model_parameter_component_count = after + .parameter_fill_bytes + .saturating_sub(before.parameter_fill_bytes) + / complex_bytes; + runtime_profile.state_clear_component_count = after + .amplitude_clear_bytes + .saturating_sub(before.amplitude_clear_bytes) + / complex_bytes; + runtime_profile.evaluator_backend_call_count = after + .backend_call_count + .saturating_sub(before.backend_call_count); + runtime_profile.compiled_direct_arena_engine_count = u64::try_from(before.engine_count) + .ok() + .and_then(|count| count.checked_mul(repetitions_u64)) + .ok_or_else(|| { + RusticolError::invalid_argument("compiled Direct-Arena engine count overflowed") + })?; + runtime_profile.compiled_direct_arena_call_count = + runtime_profile.evaluator_backend_call_count; + runtime_profile.compiled_direct_arena_boundary_input_bytes = after + .boundary_input_bytes + .saturating_sub(before.boundary_input_bytes); + runtime_profile.compiled_direct_arena_boundary_current_output_bytes = after + .boundary_current_output_bytes + .saturating_sub(before.boundary_current_output_bytes); + runtime_profile.compiled_direct_arena_boundary_amplitude_output_bytes = after + .boundary_amplitude_output_bytes + .saturating_sub(before.boundary_amplitude_output_bytes); + runtime_profile.compiled_direct_arena_internal_broadcast_bytes = after + .scalar_broadcast_fill_bytes + .saturating_sub(before.scalar_broadcast_fill_bytes); + attach_compiled_direct_configuration(&mut runtime_profile, after); + } + runtime_profile.validate_recurrence_direct_boundary_traffic()?; + let mut profile: NativeRuntimeProfile = runtime_profile.into(); + profile.native_input_component_count = measured_input_components_u64; + profile.native_input_pack_bytes = 0; + profile.native_input_crossing_bytes = 0; + profile.native_input_container_allocation_count = 0; + profile.native_output_allocation_count = 0; + self.validate_profile_accounting(&profile)?; + Ok(NativeProfiledEvaluation { values, profile }) + } + + #[cfg(not(any(feature = "f64-compiled", feature = "f64-symjit")))] + pub fn evaluate_f64_arena_profile_repeated( + &mut self, + _momenta: &[f64], + _point_count: usize, + _repetitions: usize, + _helicity_ids: Option<&[String]>, + _color_ids: Option<&[String]>, + ) -> Result { + Err(RusticolError::compatibility( + "warmed Arena profiling requires the f64-compiled or f64-symjit feature", + )) + } + + pub fn evaluate_f64_profile( + &mut self, + momenta: &[f64], + point_count: usize, + helicity_ids: Option<&[String]>, + color_ids: Option<&[String]>, + ) -> Result { + let color_ids = self.execution_color_ids(color_ids); + let mut evaluation = self.evaluate_representative_f64_profile( + momenta, + point_count, + helicity_ids, + color_ids.as_deref(), + )?; + self.apply_process_group_factor(&mut evaluation.values); + Ok(evaluation) + } + + fn evaluate_representative_f64_profile( + &mut self, + momenta: &[f64], + point_count: usize, + helicity_ids: Option<&[String]>, + color_ids: Option<&[String]>, + ) -> Result { + let total_start = Instant::now(); + let (batch, native_input_pack_elapsed, native_input_crossing_elapsed) = + self.prepare_f64_batch_profile(momenta, point_count)?; + let compiled_direct_profile = { + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + { + matches!(&self.execution_lane, NativeExecutionLane::Compiled) + && compiled_direct_profile_snapshot(&self.runtime)?.engine_count != 0 + } + #[cfg(not(any(feature = "f64-compiled", feature = "f64-symjit")))] + { + false + } + }; + let (values, profile) = if compiled_direct_profile { + if helicity_ids.is_some() || color_ids.is_some() { + self.validate_selector_capabilities(helicity_ids.is_some(), color_ids.is_some())?; + self.record_resolved_warnings(helicity_ids, color_ids)?; + } + let selected_helicities = selector_set(helicity_ids, "helicity")?; + let selected_colors = selector_set(color_ids, "color component")?; + self.run_selected_f64_batch_profile( + &batch, + selected_helicities.as_ref(), + selected_colors.as_ref(), + )? + } else if helicity_ids.is_some() || color_ids.is_some() { + self.validate_selector_capabilities(helicity_ids.is_some(), color_ids.is_some())?; + self.record_resolved_warnings(helicity_ids, color_ids)?; + let selected_helicities = selector_set(helicity_ids, "helicity")?; + let selected_colors = selector_set(color_ids, "color component")?; + let (values, profile) = match &mut self.execution_lane { + NativeExecutionLane::Compiled => self.runtime.run_f64_selected_totals( + &batch, + selected_helicities.as_ref(), + selected_colors.as_ref(), + )?, + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + NativeExecutionLane::Eager(runtime) => { + let (resolved, mut profile) = runtime.run_resolved_f64_profile( + &mut self.runtime, + &batch, + selected_helicities.as_ref(), + selected_colors.as_ref(), + )?; + let materialization_start = Instant::now(); + let values = resolved_f64_totals(&resolved)?; + profile.total_materialization_s += + materialization_start.elapsed().as_secs_f64(); + profile.total_materialized_value_count += point_count as u64; + (values, profile) + } + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + NativeExecutionLane::Recurrence(runtime) => { + let (resolved, mut profile) = runtime.run_resolved_f64( + &mut self.runtime, + &batch, + selected_helicities.as_ref(), + selected_colors.as_ref(), + )?; + let materialization_start = Instant::now(); + let values = resolved_f64_totals(&resolved)?; + profile.total_materialization_s += + materialization_start.elapsed().as_secs_f64(); + profile.total_materialized_value_count += point_count as u64; + (values, profile) + } + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + NativeExecutionLane::OnTheFly(runtime) => { + let view = + F64MomentumBatchView::from_nested(&batch, self.runtime.external_count)?; + let mut values = vec![0.0; point_count]; + let mut profile = runtime.run_total_into( + &self.runtime, + view, + selected_helicities.as_ref(), + selected_colors.as_ref(), + &mut values, + )?; + profile.total_materialized_value_count += point_count as u64; + (values, profile) + } + }; + (values, profile) + } else { + match &mut self.execution_lane { + NativeExecutionLane::Compiled => self.runtime.run_f64(&batch)?, + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + NativeExecutionLane::Eager(runtime) => { + runtime.run_f64_profile(&mut self.runtime, &batch)? + } + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + NativeExecutionLane::Recurrence(runtime) => { + runtime.run_f64(&mut self.runtime, &batch)? + } + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + NativeExecutionLane::OnTheFly(runtime) => { + let view = + F64MomentumBatchView::from_nested(&batch, self.runtime.external_count)?; + let mut values = vec![0.0; point_count]; + let profile = + runtime.run_total_into(&self.runtime, view, None, None, &mut values)?; + (values, profile) + } + } + }; + profile.validate_recurrence_direct_boundary_traffic()?; + let mut profile: NativeRuntimeProfile = profile.into(); + profile.native_input_pack_s = profile_duration_seconds(native_input_pack_elapsed); + profile.native_input_crossing_s = profile_duration_seconds(native_input_crossing_elapsed); + profile.native_input_component_count = momenta.len() as u64; + profile.native_input_pack_bytes = std::mem::size_of_val(momenta) as u64; + if self.input_crossing_map.is_some() { + profile.native_input_crossing_bytes = profile.native_input_pack_bytes; + } + profile.native_input_container_allocation_count = + (point_count + 1 + usize::from(self.input_crossing_map.is_some()) * (point_count + 2)) + as u64; + profile.native_output_allocation_count = 1; + profile.total_s = total_start.elapsed().as_secs_f64(); + self.validate_profile_accounting(&profile)?; + Ok(NativeProfiledEvaluation { values, profile }) + } + + #[allow(clippy::too_many_arguments)] + pub fn evaluate_f64_profile_with_selectors( + &mut self, + momenta: &[f64], + point_count: usize, + helicity_ids: Option<&[String]>, + color_ids: Option<&[String]>, + helicity_by_point: Option<&[u32]>, + color_by_point: Option<&[u32]>, + ) -> Result { + let color_ids = self.execution_color_ids(color_ids); + let mut evaluation = self.evaluate_representative_f64_profile_with_selectors( + momenta, + point_count, + helicity_ids, + color_ids.as_deref(), + helicity_by_point, + color_by_point, + )?; + self.apply_process_group_factor(&mut evaluation.values); + Ok(evaluation) + } + + #[allow(clippy::too_many_arguments)] + fn evaluate_representative_f64_profile_with_selectors( + &mut self, + momenta: &[f64], + point_count: usize, + helicity_ids: Option<&[String]>, + color_ids: Option<&[String]>, + helicity_by_point: Option<&[u32]>, + color_by_point: Option<&[u32]>, + ) -> Result { + if helicity_by_point.is_none() && color_by_point.is_none() { + return self.evaluate_representative_f64_profile( + momenta, + point_count, + helicity_ids, + color_ids, + ); + } + if helicity_ids.is_some() && helicity_by_point.is_some() { + return Err(RusticolError::selector( + "helicities and helicity_by_point are mutually exclusive", + )); + } + if color_ids.is_some() && color_by_point.is_some() { + return Err(RusticolError::selector( + "color_flows and color_flow_by_point are mutually exclusive", + )); + } + self.validate_selector_capabilities( + helicity_ids.is_some() || helicity_by_point.is_some(), + color_ids.is_some() || color_by_point.is_some(), + )?; + let total_start = Instant::now(); + let selected_helicities = selector_set(helicity_ids, "helicity")?; + let selected_colors = selector_set(color_ids, "color component")?; + let (batch, native_input_pack_elapsed, native_input_crossing_elapsed) = + self.prepare_f64_batch_profile(momenta, point_count)?; + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + let on_the_fly = matches!(&self.execution_lane, NativeExecutionLane::OnTheFly(_)); + #[cfg(not(any(feature = "f64-compiled", feature = "f64-symjit")))] + let on_the_fly = false; + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + let on_the_fly_selected_ordinals = if on_the_fly { + Some(self.on_the_fly_selected_ordinals( + selected_helicities.as_ref(), + selected_colors.as_ref(), + )?) + } else { + None + }; + let physics = if on_the_fly { + None + } else { + Some(self.runtime.physics.clone().ok_or_else(|| { + RusticolError::artifact( + "schema-v3 artifact is missing resolved physics metadata; regenerate it with pyAmpliCol 0.1.0 or newer", + ) + })?) + }; + let (helicity_count, color_count) = if on_the_fly { + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + { + self.on_the_fly_selector_counts()? + } + #[cfg(not(any(feature = "f64-compiled", feature = "f64-symjit")))] + { + unreachable!("on-the-fly lane requires an f64 evaluator feature") + } + } else { + let physics = physics + .as_ref() + .expect("non-on-the-fly profile has physics metadata"); + ( + physics.manifest.helicities.len(), + physics.manifest.color_components.len(), + ) + }; + let selector_simd_lane_width = self.selector_simd_lane_width(); + let mut selector_scratch = std::mem::take(&mut self.point_selector_scratch); + let result = (|| { + let planner_started = Instant::now(); + let plan = selector_scratch.planner.build( + point_count, + helicity_by_point, + color_by_point, + helicity_count, + color_count, + )?; + let plan_profile = + selector_scratch + .planner + .profile(plan, point_count, selector_simd_lane_width); + let planner = planner_started.elapsed(); + self.record_resolved_warnings(helicity_ids, color_ids)?; + + if let PointSelectorPlan::Homogeneous(key) = plan { + let (values, runtime_profile) = if on_the_fly { + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + { + let (selected_helicities, selected_colors) = on_the_fly_selected_ordinals + .as_ref() + .expect("on-the-fly selected ordinals are absent"); + let point_helicity = key.helicity_index.map(|index| [index]); + let point_color = key.color_index.map(|index| [index]); + self.run_on_the_fly_f64_batch_profile_by_ordinals( + &batch, + point_helicity + .as_ref() + .map(<[usize; 1]>::as_slice) + .or(selected_helicities.as_deref()), + point_color + .as_ref() + .map(<[usize; 1]>::as_slice) + .or(selected_colors.as_deref()), + )? + } + #[cfg(not(any(feature = "f64-compiled", feature = "f64-symjit")))] + { + unreachable!("on-the-fly lane requires an f64 evaluator feature") + } + } else { + let physics = physics + .as_ref() + .expect("non-on-the-fly profile has physics metadata"); + let point_helicities = key.helicity_index.map(|index| { + BTreeSet::from([physics.manifest.helicities[index].id.clone()]) + }); + let point_colors = key.color_index.map(|index| { + BTreeSet::from([physics.manifest.color_components[index].id().to_string()]) + }); + let effective_helicities = + point_helicities.as_ref().or(selected_helicities.as_ref()); + let effective_colors = point_colors.as_ref().or(selected_colors.as_ref()); + self.run_selected_f64_batch_profile( + &batch, + effective_helicities, + effective_colors, + )? + }; + runtime_profile.validate_recurrence_direct_boundary_traffic()?; + let mut profile: NativeRuntimeProfile = runtime_profile.into(); + attach_point_selector_profile( + &mut profile, + &plan_profile, + planner, + Duration::ZERO, + Duration::ZERO, + PointSelectorProfileCounts { + gather_point_count: 0, + input_bytes_per_point: self.runtime.external_count + * 4 + * std::mem::size_of::(), + scatter_value_count: 0, + }, + ); + profile.native_input_pack_s = profile_duration_seconds(native_input_pack_elapsed); + profile.native_input_crossing_s = + profile_duration_seconds(native_input_crossing_elapsed); + profile.native_input_component_count = momenta.len() as u64; + profile.native_input_pack_bytes = std::mem::size_of_val(momenta) as u64; + if self.input_crossing_map.is_some() { + profile.native_input_crossing_bytes = profile.native_input_pack_bytes; + } + profile.native_input_container_allocation_count = (point_count + + 1 + + usize::from(self.input_crossing_map.is_some()) * (point_count + 2)) + as u64; + profile.native_output_allocation_count = 1; + profile.total_s = total_start.elapsed().as_secs_f64(); + self.validate_profile_accounting(&profile)?; + return Ok(NativeProfiledEvaluation { values, profile }); + } + + let mut values = vec![0.0; point_count]; + let mut partition_profiles = Vec::new(); + let mut gather = Duration::ZERO; + let mut scatter = Duration::ZERO; + let mut gather_point_count = 0usize; + let partition_count = selector_scratch.planner.partitions().len(); + for partition_index in 0..partition_count { + let partition = selector_scratch.planner.partitions()[partition_index]; + let (partition_totals, partition_profile) = if on_the_fly { + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + { + let (selected_helicities, selected_colors) = on_the_fly_selected_ordinals + .as_ref() + .expect("on-the-fly selected ordinals are absent"); + let point_helicity = partition.key.helicity_index.map(|index| [index]); + let point_color = partition.key.color_index.map(|index| [index]); + let effective_helicities = point_helicity + .as_ref() + .map(<[usize; 1]>::as_slice) + .or(selected_helicities.as_deref()); + let effective_colors = point_color + .as_ref() + .map(<[usize; 1]>::as_slice) + .or(selected_colors.as_deref()); + match partition.rows { + PointSelectorRows::Contiguous { start, end } => self + .run_on_the_fly_f64_batch_profile_by_ordinals( + &batch[start..end], + effective_helicities, + effective_colors, + )?, + rows @ PointSelectorRows::Gathered { .. } => { + let gather_started = Instant::now(); + let point_indices = selector_scratch.planner.gathered_rows(rows); + gather_point_count += point_indices.len(); + let gathered_batch = fill_gathered_batch( + &mut selector_scratch.gathered_batch, + &batch, + point_indices, + ); + gather += gather_started.elapsed(); + self.run_on_the_fly_f64_batch_profile_by_ordinals( + gathered_batch, + effective_helicities, + effective_colors, + )? + } + } + } + #[cfg(not(any(feature = "f64-compiled", feature = "f64-symjit")))] + { + unreachable!("on-the-fly lane requires an f64 evaluator feature") + } + } else { + let physics = physics + .as_ref() + .expect("non-on-the-fly profile has physics metadata"); + let point_helicities = partition.key.helicity_index.map(|index| { + BTreeSet::from([physics.manifest.helicities[index].id.clone()]) + }); + let point_colors = partition.key.color_index.map(|index| { + BTreeSet::from([physics.manifest.color_components[index].id().to_string()]) + }); + let effective_helicities = + point_helicities.as_ref().or(selected_helicities.as_ref()); + let effective_colors = point_colors.as_ref().or(selected_colors.as_ref()); + match partition.rows { + PointSelectorRows::Contiguous { start, end } => self + .run_selected_f64_batch_profile( + &batch[start..end], + effective_helicities, + effective_colors, + )?, + rows @ PointSelectorRows::Gathered { .. } => { + let gather_started = Instant::now(); + let point_indices = selector_scratch.planner.gathered_rows(rows); + gather_point_count += point_indices.len(); + let gathered_batch = fill_gathered_batch( + &mut selector_scratch.gathered_batch, + &batch, + point_indices, + ); + gather += gather_started.elapsed(); + self.run_selected_f64_batch_profile( + gathered_batch, + effective_helicities, + effective_colors, + )? + } + } + }; + if partition_totals.len() != partition.rows.len() { + return Err(RusticolError::integrity( + "per-point selector partition returned the wrong number of values", + )); + } + let scatter_started = Instant::now(); + scatter_partition_totals( + &mut values, + &partition_totals, + partition.rows, + &selector_scratch.planner, + ); + scatter += scatter_started.elapsed(); + partition_profiles.push(partition_profile); + } + let mut runtime_profile = RuntimeProfile::default(); + for partition_profile in &partition_profiles { + runtime_profile.add_sector(partition_profile); + } + runtime_profile.validate_recurrence_direct_boundary_traffic()?; + let mut profile: NativeRuntimeProfile = runtime_profile.into(); + attach_point_selector_profile( + &mut profile, + &plan_profile, + planner, + gather, + scatter, + PointSelectorProfileCounts { + gather_point_count, + input_bytes_per_point: self.runtime.external_count + * 4 + * std::mem::size_of::(), + scatter_value_count: point_count, + }, + ); + profile.native_input_pack_s = profile_duration_seconds(native_input_pack_elapsed); + profile.native_input_crossing_s = + profile_duration_seconds(native_input_crossing_elapsed); + profile.native_input_component_count = momenta.len() as u64; + profile.native_input_pack_bytes = std::mem::size_of_val(momenta) as u64; + if self.input_crossing_map.is_some() { + profile.native_input_crossing_bytes = profile.native_input_pack_bytes; + } + profile.native_input_container_allocation_count = (point_count + + 1 + + usize::from(self.input_crossing_map.is_some()) * (point_count + 2)) + as u64; + profile.native_output_allocation_count = 1; + profile.total_s = total_start.elapsed().as_secs_f64(); + self.validate_profile_accounting(&profile)?; + Ok(NativeProfiledEvaluation { values, profile }) + })(); + self.point_selector_scratch = selector_scratch; + result + } + + pub fn evaluate_f64_profile_repeated( + &mut self, + momenta: &[f64], + point_count: usize, + repetitions: usize, + helicity_ids: Option<&[String]>, + color_ids: Option<&[String]>, + ) -> Result { + self.evaluate_f64_profile_repeated_with_selectors( + momenta, + point_count, + repetitions, + helicity_ids, + color_ids, + None, + None, + ) + } + + #[allow(clippy::too_many_arguments)] + pub fn evaluate_f64_profile_repeated_with_selectors( + &mut self, + momenta: &[f64], + point_count: usize, + repetitions: usize, + helicity_ids: Option<&[String]>, + color_ids: Option<&[String]>, + helicity_by_point: Option<&[u32]>, + color_by_point: Option<&[u32]>, + ) -> Result { + if repetitions == 0 { + return Err(RusticolError::invalid_argument( + "profile repetitions must be positive", + )); + } + let started = Instant::now(); + let mut profile: Option> = None; + let mut values = Vec::new(); + for _ in 0..repetitions { + // The saved evaluator payload may use the complete AArch64 + // floating-point register file. Keep the aggregate behind an + // opaque heap boundary so no accumulated f64 survives the next + // generated call in a register. + std::hint::black_box(&mut profile); + let profiled = self.evaluate_f64_profile_with_selectors( + momenta, + point_count, + helicity_ids, + color_ids, + helicity_by_point, + color_by_point, + )?; + std::hint::black_box(&profiled.values); + values = profiled.values; + if let Some(aggregate) = profile.as_deref_mut() { + aggregate.accumulate(&profiled.profile); + } else { + profile = Some(Box::new(profiled.profile)); + } + } + let mut profile = *profile.expect("positive repetitions checked"); + profile.total_s = started.elapsed().as_secs_f64(); + self.validate_profile_accounting(&profile)?; + Ok(NativeProfiledEvaluation { values, profile }) + } + + fn validate_profile_accounting(&self, profile: &NativeRuntimeProfile) -> RusticolResult<()> { + match &self.execution_lane { + NativeExecutionLane::Compiled => profile.validate_compiled_top_level_accounting(), + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + NativeExecutionLane::Eager(_) => profile.validate_eager_top_level_accounting(), + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + NativeExecutionLane::Recurrence(_) => { + profile.validate_recurrence_top_level_accounting() + } + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + NativeExecutionLane::OnTheFly(_) => profile.validate_recurrence_top_level_accounting(), + } + } + + pub fn evaluate_resolved_f64( + &mut self, + momenta: &[f64], + point_count: usize, + helicity_ids: Option<&[String]>, + color_ids: Option<&[String]>, + ) -> Result { + let color_ids = self.execution_color_ids(color_ids); + let mut resolved = self.evaluate_representative_resolved_f64( + momenta, + point_count, + helicity_ids, + color_ids.as_deref(), + )?; + self.apply_process_group_factor(&mut resolved.values); + self.publish_color_ids(&mut resolved.color_ids); + Ok(resolved) + } + + fn evaluate_representative_resolved_f64( + &mut self, + momenta: &[f64], + point_count: usize, + helicity_ids: Option<&[String]>, + color_ids: Option<&[String]>, + ) -> Result { + self.validate_selector_capabilities(helicity_ids.is_some(), color_ids.is_some())?; + self.record_resolved_warnings(helicity_ids, color_ids)?; + validate_flat_momentum_shape(momenta.len(), point_count, self.runtime.external_count)?; + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + if let NativeExecutionLane::OnTheFly(runtime) = &mut self.execution_lane { + let selected_helicities = selector_set(helicity_ids, "helicity")?; + let selected_colors = selector_set(color_ids, "color component")?; + let batch = F64MomentumBatchView::from_contiguous_prevalidated( + momenta, + point_count, + self.runtime.external_count, + self.input_crossing_map.as_deref(), + )?; + let (resolved, _profile) = runtime.run_resolved( + &self.runtime, + batch, + selected_helicities.as_ref(), + selected_colors.as_ref(), + )?; + let (helicity_ids, color_ids) = runtime.selected_axis_ids()?; + return Ok(NativeResolvedEvaluation { + values: resolved.values, + point_count: resolved.point_count, + helicity_ids, + color_ids, + }); + } + let physics = self.runtime.physics.clone().ok_or_else(|| { + RusticolError::artifact( + "schema-v3 artifact is missing resolved physics metadata; regenerate it with pyAmpliCol 0.1.0 or newer", + ) + })?; + let crossing_lookup = std::mem::take(&mut self.input_crossing_map); + let mut selector_scratch = std::mem::take(&mut self.point_selector_scratch); + let result = (|| { + physics.validate_helicity_id_slice(helicity_ids)?; + physics.validate_color_id_slice(color_ids)?; + let selected_helicities = selector_scratch + .helicity_selector_sets + .resolve(helicity_ids, "helicity")?; + let selected_colors = selector_scratch + .color_selector_sets + .resolve(color_ids, "color component")?; + let batch = F64MomentumBatchView::from_contiguous_prevalidated( + momenta, + point_count, + self.runtime.external_count, + crossing_lookup.as_deref(), + )?; + self.run_resolved_f64_batch(batch, selected_helicities, selected_colors) + })(); + self.point_selector_scratch = selector_scratch; + self.input_crossing_map = crossing_lookup; + let resolved = result?; + let helicity_ids = resolved + .helicity_indices + .iter() + .map(|index| physics.manifest.helicities[*index].id.clone()) + .collect(); + let color_ids = resolved + .color_indices + .iter() + .map(|index| physics.manifest.color_components[*index].id().to_string()) + .collect(); + Ok(NativeResolvedEvaluation { + values: resolved.values, + point_count: resolved.point_count, + helicity_ids, + color_ids, + }) + } + + #[cfg(feature = "symbolica-runtime")] + pub fn evaluate_with_precision( + &mut self, + momenta: &[String], + point_count: usize, + decimal_digits: u32, + ) -> Result { + if decimal_digits == 0 { + return Err(RusticolError::unsupported_precision( + "precision must be a positive number of decimal digits", + )); + } + if decimal_digits == 16 { + let values = momenta + .iter() + .map(|value| { + value.parse::().map_err(|error| { + RusticolError::invalid_argument(format!( + "could not parse f64 momentum component {value:?}: {error}" + )) + }) + }) + .collect::, _>>()?; + let values = self.evaluate_f64(&values, point_count)?; + return Ok(NativeDecimalEvaluation { + values: format_decimal_values(values, decimal_digits), + decimal_digits, + }); + } + if self.execution_lane.is_eager() { + return Err(eager_parity_pending("higher-precision evaluation")); + } + if self.execution_lane.is_recurrence() { + return Err(recurrence_parity_pending("higher-precision evaluation")); + } + if decimal_digits == 32 { + let batch = self.prepare_double_batch(momenta, point_count)?; + let (mut values, _profile) = self.runtime.run_double(&batch)?; + self.apply_process_group_factor_high_precision(&mut values); + return Ok(NativeDecimalEvaluation { + values: format_decimal_values(values, decimal_digits), + decimal_digits, + }); + } + let binary_precision = decimal_digits_to_bits(decimal_digits); + let batch = self.prepare_float_batch(momenta, point_count, binary_precision)?; + let (mut values, _profile) = self.runtime.run_float(&batch, binary_precision)?; + self.apply_process_group_factor_high_precision(&mut values); + Ok(NativeDecimalEvaluation { + values: format_decimal_values(values, decimal_digits), + decimal_digits, + }) + } + + #[cfg(feature = "symbolica-runtime")] + fn apply_process_group_factor_high_precision(&self, values: &mut [T]) + where + T: RusticolHighPrecisionNumber, + Complex: Real + EvaluationDomain, + { + let NativeRationalFactor { + numerator, + denominator, + } = self.process_group_factor(); + if numerator != denominator { + let numerator = T::from(numerator as f64); + let denominator = T::from(denominator as f64); + for value in values { + *value = value.clone() * numerator.clone() / denominator.clone(); + } + } + } + + #[cfg(feature = "symbolica-runtime")] + pub fn evaluate_resolved_with_precision( + &mut self, + momenta: &[String], + point_count: usize, + decimal_digits: u32, + helicity_ids: Option<&[String]>, + color_ids: Option<&[String]>, + ) -> Result { + if decimal_digits == 0 { + return Err(RusticolError::unsupported_precision( + "precision must be a positive number of decimal digits", + )); + } + let color_ids = self.execution_color_ids(color_ids); + let color_ids = color_ids.as_deref(); + self.validate_selector_capabilities(helicity_ids.is_some(), color_ids.is_some())?; + if self.execution_lane.is_eager() { + return Err(eager_parity_pending("resolved higher-precision evaluation")); + } + if self.execution_lane.is_recurrence() { + return Err(recurrence_parity_pending( + "resolved higher-precision evaluation", + )); + } + self.record_resolved_warnings(helicity_ids, color_ids)?; + let selected_helicities = selector_set(helicity_ids, "helicity")?; + let selected_colors = selector_set(color_ids, "color component")?; + if decimal_digits == 16 { + let values = momenta + .iter() + .map(|value| { + value.parse::().map_err(|error| { + RusticolError::invalid_argument(format!( + "could not parse f64 momentum component {value:?}: {error}" + )) + }) + }) + .collect::, _>>()?; + let mut resolved = self.evaluate_representative_resolved_f64( + &values, + point_count, + helicity_ids, + color_ids, + )?; + self.apply_process_group_factor(&mut resolved.values); + self.publish_color_ids(&mut resolved.color_ids); + let totals = resolved.totals(); + return Ok(NativeDecimalResolvedEvaluation { + values: format_decimal_values(resolved.values, decimal_digits), + totals: format_decimal_values(totals, decimal_digits), + point_count: resolved.point_count, + helicity_ids: resolved.helicity_ids, + color_ids: resolved.color_ids, + decimal_digits, + }); + } + let physics = + self.runtime.physics.clone().ok_or_else(|| { + RusticolError::artifact("resolved physics metadata is unavailable") + })?; + let mut evaluation = if decimal_digits == 32 { + let batch = self.prepare_double_batch(momenta, point_count)?; + let (mut resolved, _profile) = self.runtime.run_resolved_generic( + &batch, + None, + selected_helicities.as_ref(), + selected_colors.as_ref(), + )?; + self.apply_process_group_factor_high_precision(&mut resolved.values); + decimal_resolved_evaluation(resolved, &physics.manifest, decimal_digits)? + } else { + let binary_precision = decimal_digits_to_bits(decimal_digits); + let batch = self.prepare_float_batch(momenta, point_count, binary_precision)?; + let (mut resolved, _profile) = self.runtime.run_resolved_generic( + &batch, + Some(binary_precision), + selected_helicities.as_ref(), + selected_colors.as_ref(), + )?; + self.apply_process_group_factor_high_precision(&mut resolved.values); + decimal_resolved_evaluation(resolved, &physics.manifest, decimal_digits)? + }; + self.publish_color_ids(&mut evaluation.color_ids); + Ok(evaluation) + } + + pub fn set_model_parameters( + &mut self, + values: &BTreeMap, + ) -> Result<(), RusticolError> { + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + if matches!(self.execution_lane, NativeExecutionLane::OnTheFly(_)) { + return self + .runtime + .apply_model_parameter_overrides(values) + .map_err(|error| RusticolError::model_parameter(error.to_string())); + } + let physics = self.physics_v1.get()?; + for name in values.keys() { + let parameter = physics + .model_parameters + .iter() + .find(|parameter| parameter.name == *name) + .ok_or_else(|| { + RusticolError::model_parameter(format!( + "model parameter {name:?} is not declared by process {}", + self.process + )) + })?; + if !parameter.mutable { + return Err(RusticolError::model_parameter(format!( + "model parameter {name:?} is derived or immutable" + ))); + } + } + self.runtime + .apply_model_parameter_overrides(values) + .map_err(|error| RusticolError::model_parameter(error.to_string())) + } + + /// Drop selector-local warmed state without unloading the artifact or + /// changing runtime model parameters. Other execution modes have no + /// corresponding query-family cache and therefore treat this as a no-op. + pub fn clear(&mut self) -> RusticolResult<()> { + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + match &mut self.execution_lane { + NativeExecutionLane::OnTheFly(runtime) => runtime.clear()?, + NativeExecutionLane::Recurrence(runtime) => { + runtime.clear_helicity_selector_companion()? + } + _ => {} + } + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + if matches!( + &self.execution_lane, + NativeExecutionLane::OnTheFly(_) | NativeExecutionLane::Recurrence(_) + ) { + self.point_selector_scratch = PointSelectorExecutionScratch::default(); + } + Ok(()) + } + + pub fn set_model_parameter( + &mut self, + name: &str, + real: f64, + imaginary: f64, + ) -> Result<(), RusticolError> { + self.set_model_parameters(&BTreeMap::from([(name.to_string(), (real, imaginary))])) + } + + pub fn set_model_parameters_json(&mut self, path: &Path) -> Result<(), RusticolError> { + let text = fs::read_to_string(path).map_err(|error| { + RusticolError::model_parameter(format!( + "could not read model-parameter JSON {}: {error}", + path.display() + )) + })?; + let overrides = parse_complex_parameter_overrides(&text, path) + .map_err(|error| RusticolError::model_parameter(error.to_string()))?; + self.set_model_parameters(&overrides) + } + + pub fn mute_warnings(&mut self) { + self.warnings_muted = true; + } + + pub fn unmute_warnings(&mut self) { + self.warnings_muted = false; + } + + pub fn take_warnings(&mut self) -> Vec { + std::mem::take(&mut self.pending_warnings) + } + + pub fn pending_warnings_json(&self) -> Result { + serde_json::to_string(&self.pending_warnings).map_err(|error| { + RusticolError::serialization(format!("could not serialize warnings: {error}")) + }) + } + + pub fn clear_pending_warnings(&mut self) { + self.pending_warnings.clear(); + } + + pub fn take_warnings_json(&mut self) -> Result { + serde_json::to_string(&self.take_warnings()).map_err(|error| { + RusticolError::serialization(format!("could not serialize warnings: {error}")) + }) + } + + pub fn root(&self) -> &Path { + &self.root + } + + fn prepare_f64_batch_profile( + &self, + momenta: &[f64], + point_count: usize, + ) -> Result { + let pack_start = Instant::now(); + let batch = self.prepare_uncrossed_f64_batch(momenta, point_count)?; + let pack_elapsed = pack_start.elapsed(); + if self.input_crossing_map.is_none() { + return Ok((batch, pack_elapsed, Duration::ZERO)); + } + let crossing_start = Instant::now(); + let batch = apply_input_crossing_map( + batch, + self.runtime.external_count, + self.input_crossing_map.as_deref(), + )?; + Ok((batch, pack_elapsed, crossing_start.elapsed())) + } + + fn prepare_uncrossed_f64_batch( + &self, + momenta: &[f64], + point_count: usize, + ) -> Result>, RusticolError> { + if point_count == 0 { + return Err(RusticolError::invalid_argument( + "point_count must be positive", + )); + } + let values_per_point = self + .runtime + .external_count + .checked_mul(4) + .ok_or_else(|| RusticolError::invalid_argument("momentum shape overflow"))?; + let expected = point_count + .checked_mul(values_per_point) + .ok_or_else(|| RusticolError::invalid_argument("momentum shape overflow"))?; + if momenta.len() != expected { + return Err(RusticolError::invalid_argument(format!( + "momenta contain {} values, expected {expected} for shape ({point_count}, {}, 4)", + momenta.len(), + self.runtime.external_count + ))); + } + let mut batch = Vec::with_capacity(point_count); + for point_values in momenta.chunks_exact(values_per_point) { + let point = point_values + .chunks_exact(4) + .map(|components| [components[0], components[1], components[2], components[3]]) + .collect(); + batch.push(point); + } + Ok(batch) + } + + #[cfg(feature = "symbolica-runtime")] + fn prepare_double_batch( + &self, + momenta: &[String], + point_count: usize, + ) -> RusticolResult>> { + let floats = self.prepare_float_batch(momenta, point_count, 106)?; + Ok(floats + .into_iter() + .map(|point| { + point + .into_iter() + .map(|leg| { + [ + leg[0].to_double_float(), + leg[1].to_double_float(), + leg[2].to_double_float(), + leg[3].to_double_float(), + ] + }) + .collect() + }) + .collect()) + } + + #[cfg(feature = "symbolica-runtime")] + fn prepare_float_batch( + &self, + momenta: &[String], + point_count: usize, + binary_precision: u32, + ) -> RusticolResult>> { + let values_per_point = + validate_flat_momentum_shape(momenta.len(), point_count, self.runtime.external_count)?; + let mut batch = Vec::with_capacity(point_count); + for point_values in momenta.chunks_exact(values_per_point) { + let mut point = Vec::with_capacity(self.runtime.external_count); + for components in point_values.chunks_exact(4) { + let values = components + .iter() + .map(|value| { + Float::parse(value, Some(binary_precision)).map_err(|error| { + RusticolError::invalid_argument(format!( + "could not parse high-precision momentum component {value:?}: {error}" + )) + }) + }) + .collect::>>()?; + point.push([ + values[0].clone(), + values[1].clone(), + values[2].clone(), + values[3].clone(), + ]); + } + batch.push(point); + } + apply_input_crossing_map_generic( + &batch, + self.runtime.external_count, + self.input_crossing_map.as_deref(), + ) + } + + pub(super) fn record_resolved_warnings( + &mut self, + helicity_ids: Option<&[String]>, + color_ids: Option<&[String]>, + ) -> Result<(), RusticolError> { + if self.warnings_muted { + return Ok(()); + } + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + if matches!(self.execution_lane, NativeExecutionLane::OnTheFly(_)) { + // Compact selector construction authenticates complete LC + // coverage and materializes exact requested members directly; + // there are no representative-only rows to warn about here. + return Ok(()); + } + let physics = self.runtime.physics.as_ref().ok_or_else(|| { + RusticolError::artifact("resolved evaluation requires regenerated physics metadata") + })?; + let mut warnings = Vec::new(); + if physics.manifest.coverage.helicities != "complete" { + warnings.push(( + "incomplete-helicity-coverage", + "resolved evaluation contains only the helicities represented by this artifact", + )); + } + if physics.manifest.color_accuracy == crate::ColorAccuracy::Lc + && physics.manifest.coverage.color != "complete" + { + warnings.push(( + "incomplete-color-coverage", + "resolved evaluation contains only the color components represented by this artifact", + )); + } + let reduction_only_helicity = helicity_ids.is_some_and(|ids| { + ids.iter().any(|id| { + physics + .helicity_index_by_id + .get(id) + .and_then(|index| physics.manifest.helicities.get(*index)) + .is_some_and(|item| !item.computed) + }) + }); + let reduction_only_color = color_ids.is_some_and(|ids| { + ids.iter().any(|id| { + physics + .color_index_by_id + .get(id) + .is_some_and(|index| !physics.color_is_computed(*index)) + }) + }); + if reduction_only_helicity || reduction_only_color { + warnings.push(( + "reduction-only-selection", + "the selected resolved component reuses an exact symmetry representative", + )); + } + for (kind, message) in warnings { + if self.warned_kinds.insert(kind.to_string()) { + self.pending_warnings.push(message.to_string()); + } + } + Ok(()) + } + + fn validate_selector_capabilities( + &self, + helicity_requested: bool, + color_requested: bool, + ) -> Result<(), RusticolError> { + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + if matches!(self.execution_lane, NativeExecutionLane::OnTheFly(_)) { + if color_requested && self.runtime.color_accuracy != "lc" { + return Err(RusticolError::selector( + "on-the-fly color selection is available only for LC artifacts", + )); + } + return Ok(()); + } + let physics = self.physics_v1.get()?; + if helicity_requested && !physics.selectors.helicity { + return Err(RusticolError::selector( + "this artifact does not support physical helicity selection", + )); + } + if color_requested { + if self.runtime.color_accuracy != "lc" { + return Err(RusticolError::selector( + "LC color-flow selection is unavailable for NLC/full artifacts; their resolved color axis is contracted", + )); + } + if !physics.selectors.color_flow { + return Err(RusticolError::selector( + "this artifact does not support physical color-flow selection", + )); + } + } + Ok(()) + } +} + +fn validate_flat_momentum_shape( + value_count: usize, + point_count: usize, + external_count: usize, +) -> RusticolResult { + if point_count == 0 { + return Err(RusticolError::invalid_argument( + "point_count must be positive", + )); + } + let values_per_point = external_count + .checked_mul(4) + .ok_or_else(|| RusticolError::invalid_argument("momentum shape overflow"))?; + let expected = point_count + .checked_mul(values_per_point) + .ok_or_else(|| RusticolError::invalid_argument("momentum shape overflow"))?; + if value_count != expected { + return Err(RusticolError::invalid_argument(format!( + "momenta contain {value_count} values, expected {expected} for shape ({point_count}, {external_count}, 4)" + ))); + } + Ok(values_per_point) +} + +#[cfg(feature = "symbolica-runtime")] +fn format_decimal_values( + values: Vec, + decimal_digits: u32, +) -> Vec { + let digits = decimal_digits as usize; + values + .into_iter() + .map(|value| format!("{value:.digits$e}")) + .collect() +} + +#[cfg(feature = "symbolica-runtime")] +fn decimal_resolved_evaluation( + resolved: ResolvedValues, + physics: &ProcessPhysicsV1, + decimal_digits: u32, +) -> RusticolResult +where + T: RusticolHighPrecisionNumber + std::fmt::LowerExp, + Complex: Real + EvaluationDomain, +{ + let component_count = resolved.helicity_indices.len() * resolved.color_indices.len(); + if component_count == 0 { + return Err(RusticolError::internal( + "resolved evaluation produced an empty component axis", + )); + } + let mut totals = Vec::with_capacity(resolved.point_count); + for point in resolved.values.chunks(component_count) { + let mut total = T::new_zero(); + for value in point { + total += value.clone(); + } + totals.push(total); + } + let helicity_ids = resolved + .helicity_indices + .iter() + .map(|index| physics.helicities[*index].id.clone()) + .collect(); + let color_ids = resolved + .color_indices + .iter() + .map(|index| physics.color_components[*index].id().to_string()) + .collect(); + Ok(NativeDecimalResolvedEvaluation { + values: format_decimal_values(resolved.values, decimal_digits), + totals: format_decimal_values(totals, decimal_digits), + point_count: resolved.point_count, + helicity_ids, + color_ids, + decimal_digits, + }) +} + +pub(super) fn parse_public_kinematics_point( + value: &Value, + external_count: usize, +) -> RusticolResult> { + let outer = value.as_array().ok_or_else(|| { + RusticolError::invalid_argument( + "kinematics JSON must be one [external][4] point or a singleton batch", + ) + })?; + let point = if looks_like_kinematics_point(outer) { + outer.as_slice() + } else if outer.len() == 1 { + outer[0].as_array().map(Vec::as_slice).ok_or_else(|| { + RusticolError::invalid_argument( + "kinematics JSON singleton batch must contain one [external][4] point", + ) + })? + } else { + return Err(RusticolError::invalid_argument( + "kinematics JSON must contain exactly one point", + )); + }; + if point.len() != external_count { + return Err(RusticolError::invalid_argument(format!( + "kinematics point has {} external momenta, expected {external_count}", + point.len() + ))); + } + let mut flat = Vec::with_capacity(external_count.saturating_mul(4)); + for (external_index, momentum) in point.iter().enumerate() { + let components = momentum.as_array().ok_or_else(|| { + RusticolError::invalid_argument(format!( + "kinematics momentum {external_index} must be an array of four components" + )) + })?; + if components.len() != 4 { + return Err(RusticolError::invalid_argument(format!( + "kinematics momentum {external_index} has {} components, expected 4", + components.len() + ))); + } + for (component_index, component) in components.iter().enumerate() { + let parsed = match component { + Value::Number(number) => number.as_f64(), + Value::String(number) => number.parse::().ok(), + _ => None, + } + .filter(|number| number.is_finite()) + .ok_or_else(|| { + RusticolError::invalid_argument(format!( + "kinematics component [{external_index}][{component_index}] must be a finite JSON number or decimal string" + )) + })?; + flat.push(parsed); + } + } + Ok(flat) +} + +fn looks_like_kinematics_point(values: &[Value]) -> bool { + values.iter().all(|momentum| { + momentum.as_array().is_some_and(|components| { + components.len() == 4 + && components + .iter() + .all(|component| matches!(component, Value::Number(_) | Value::String(_))) + }) + }) +} + +#[cfg(feature = "symbolica-runtime")] +fn eager_parity_pending(feature: &str) -> RusticolError { + RusticolError::unsupported_runtime_capability( + EAGER_DIRECT_ARENA_RUNTIME_CAPABILITY, + format!( + "eager {feature} is not provided by the native f64 Direct-Arena ABI; \ + use the public Python Runtime exact-evaluation path for precision above 16 digits" + ), + ) +} + +#[cfg(feature = "symbolica-runtime")] +fn recurrence_parity_pending(feature: &str) -> RusticolError { + RusticolError::unsupported_runtime_capability( + RECURRENCE_RUNTIME_CAPABILITY, + format!( + "recurrence {feature} is not available in the initial f64 runtime slice; use compiled execution for this operation" + ), + ) +} + +#[cfg(test)] +mod runtime_capability_cutover_tests { + use super::*; + + #[test] + fn retired_eager_v2_capabilities_fail_with_regeneration_guidance() { + for capability in [ + EAGER_DAG_RUNTIME_CAPABILITY, + EAGER_LC_TOPOLOGY_REPLAY_RUNTIME_CAPABILITY, + ] { + let error = ensure_selected_runtime_capabilities_supported(&[capability.to_string()]) + .expect_err("retired eager capability must fail before generic preflight"); + assert_eq!(error.kind(), crate::RusticolErrorKind::Compatibility); + assert!(error.to_string().contains("legacy eager plan-v2")); + assert!(error.to_string().contains("regenerate")); + } + } +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/engine/on_the_fly_cache.rs b/pyamplicol/rust/crates/rusticol-core/src/engine/on_the_fly_cache.rs new file mode 100644 index 0000000000..6bce99f251 --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/engine/on_the_fly_cache.rs @@ -0,0 +1,303 @@ +// SPDX-License-Identifier: 0BSD + +//! One streamed snapshot of retained OTF structure, independent of numeric inputs. + +use super::super::on_the_fly_lane::OnTheFlyLaneSnapshotV1; +use super::*; +use sha2::{Digest, Sha256}; +use std::fs::{File, OpenOptions}; +use std::io::{BufReader, BufWriter, Read, Seek, SeekFrom, Write}; +use std::sync::atomic::{AtomicU64, Ordering}; + +const MAGIC: [u8; 8] = *b"PACOTFC1"; +const VERSION: u32 = 1; +static TEMP_ID: AtomicU64 = AtomicU64::new(0); + +#[derive(bincode::Encode, bincode::Decode, Debug, PartialEq)] +struct Header { + magic: [u8; 8], + version: u32, + artifact_id: String, + process_key: String, + external_permutation: Vec, +} + +impl Header { + fn for_runtime(runtime: &NativeRuntime) -> Self { + Self { + magic: MAGIC, + version: VERSION, + artifact_id: runtime.artifact_id.clone(), + process_key: runtime.process_key.clone(), + external_permutation: runtime.external_permutation.clone(), + } + } +} + +fn io_error(context: &str, error: impl std::fmt::Display) -> RusticolError { + RusticolError::serialization(format!("OTF cache {context}: {error}")) +} + +struct HashWriter { + inner: W, + hash: Sha256, +} + +impl Write for HashWriter { + fn write(&mut self, bytes: &[u8]) -> std::io::Result { + let written = self.inner.write(bytes)?; + self.hash.update(&bytes[..written]); + Ok(written) + } + + fn flush(&mut self) -> std::io::Result<()> { + self.inner.flush() + } +} + +fn encode(value: T, writer: &mut impl Write) -> RusticolResult<()> { + bincode::encode_into_std_write(value, writer, bincode::config::standard()) + .map(|_| ()) + .map_err(|error| io_error("encoding failed", error)) +} + +fn decode>(reader: &mut impl Read) -> RusticolResult { + bincode::decode_from_std_read(reader, bincode::config::standard()) + .map_err(|error| io_error("decoding failed", error)) +} + +/// Write into an owned sibling temporary file, then atomically replace the +/// destination. Failed saves do not truncate a previously usable snapshot. +fn write_snapshot( + path: &Path, + write_body: impl FnOnce(&mut HashWriter>) -> RusticolResult<()>, +) -> RusticolResult<()> { + let file_name = path + .file_name() + .ok_or_else(|| RusticolError::invalid_argument("OTF cache path must name a file"))?; + let mut temporary_name = file_name.to_os_string(); + temporary_name.push(format!( + ".otf-tmp-{}-{}", + std::process::id(), + TEMP_ID.fetch_add(1, Ordering::Relaxed) + )); + let temporary = path.with_file_name(temporary_name); + let file = OpenOptions::new() + .write(true) + .create_new(true) + .open(&temporary) + .map_err(|error| io_error("file creation failed", error))?; + let result = (|| { + let mut writer = HashWriter { + inner: BufWriter::new(file), + hash: Sha256::new(), + }; + write_body(&mut writer)?; + let digest = writer.hash.finalize(); + writer + .inner + .write_all(&digest) + .map_err(|error| io_error("checksum write failed", error))?; + writer + .inner + .flush() + .map_err(|error| io_error("flush failed", error))?; + drop(writer.inner); + fs::rename(&temporary, path).map_err(|error| io_error("file replacement failed", error)) + })(); + if result.is_err() { + let _ = fs::remove_file(&temporary); + } + result +} + +/// Verify the single body checksum before decoding native row indices. This +/// reads sequentially and never buffers a second copy of a large family. +fn open_snapshot(path: &Path) -> RusticolResult>> { + let file = File::open(path).map_err(|error| io_error("open failed", error))?; + let length = file + .metadata() + .map_err(|error| io_error("metadata failed", error))? + .len(); + let body_length = length + .checked_sub(32) + .ok_or_else(|| RusticolError::serialization("OTF cache is truncated"))?; + let mut reader = BufReader::new(file); + let mut hash = Sha256::new(); + let mut remaining = body_length; + let mut buffer = [0_u8; 64 * 1024]; + while remaining != 0 { + let size = usize::try_from(remaining.min(buffer.len() as u64)).expect("bounded read size"); + reader + .read_exact(&mut buffer[..size]) + .map_err(|error| io_error("body read failed", error))?; + hash.update(&buffer[..size]); + remaining -= size as u64; + } + let mut expected = [0_u8; 32]; + reader + .read_exact(&mut expected) + .map_err(|error| io_error("checksum read failed", error))?; + let actual: [u8; 32] = hash.finalize().into(); + if actual != expected { + return Err(RusticolError::integrity("OTF cache checksum mismatch")); + } + reader + .seek(SeekFrom::Start(0)) + .map_err(|error| io_error("rewind failed", error))?; + Ok(reader.take(body_length)) +} + +pub(super) fn save( + owner: &NativeRuntime, + runtime: &OnTheFlyExecutionRuntime, + path: &Path, +) -> RusticolResult<()> { + if runtime.prepared_selections.len() > 1 + || runtime.last_prepared_selection.is_some() && runtime.last_prepared_selection != Some(0) + { + return Err(RusticolError::internal( + "OTF selection retention policy changed; update the cache format", + )); + } + let selection = runtime + .prepared_selections + .first() + .map(|selected| &selected.identity); + let lane = runtime.lane.snapshot()?; + write_snapshot(path, |writer| { + encode(Header::for_runtime(owner), writer)?; + encode(selection, writer)?; + encode(lane, writer) + }) +} + +pub(super) fn load(owner: &mut NativeRuntime, path: &Path) -> RusticolResult<()> { + let mut reader = open_snapshot(path)?; + let header: Header = decode(&mut reader)?; + if header.magic != MAGIC || header.version != VERSION { + return Err(RusticolError::compatibility( + "unsupported OTF cache format; save it with this version", + )); + } + if header != Header::for_runtime(owner) { + return Err(RusticolError::compatibility( + "OTF cache belongs to a different process output or external ordering", + )); + } + let identity: Option = decode(&mut reader)?; + let lane: OnTheFlyLaneSnapshotV1 = decode(&mut reader)?; + if reader.limit() != 0 { + return Err(RusticolError::serialization("OTF cache has trailing data")); + } + if identity.is_some() != lane.has_family() { + return Err(RusticolError::integrity( + "OTF cache selection and family disagree", + )); + } + let NativeExecutionLane::OnTheFly(runtime) = &mut owner.execution_lane else { + return Err(RusticolError::compatibility( + "load_cache() requires an on-the-fly runtime", + )); + }; + runtime.restore_saved_selection(identity, lane) +} + +impl OnTheFlyExecutionRuntime { + fn restore_saved_selection( + &mut self, + identity: Option, + lane: OnTheFlyLaneSnapshotV1, + ) -> RusticolResult<()> { + let mut restored = Vec::new(); + let mut requests = None; + let mut contracted_count = None; + if let Some(identity) = identity { + let selected = self.selectors.selection_from_ordinals( + self.lane.seed(), + identity.helicity_ordinals.as_deref(), + identity.color_ordinals.as_deref(), + )?; + let helicity_indices = (0..selected.helicity_count()) + .map(|i| selected.helicity_ordinal_at(i)) + .collect::>>()?; + let helicity_ids = (0..selected.helicity_count()) + .map(|i| selected.helicity_id_at(i)) + .collect::>>()?; + let (color_indices, color_ids, query_count) = match &self.reduction { + OnTheFlyReductionV1::Lc => { + let indices = (0..selected.color_count()) + .map(|i| selected.color_ordinal_at(i)) + .collect::>>()?; + let ids = (0..selected.color_count()) + .map(|i| selected.color_id_at(i)) + .collect::>>()?; + let decoded = selected.iter().collect::>>()?; + let count = decoded.len(); + requests = Some(decoded); + (indices, ids, count) + } + OnTheFlyReductionV1::Contracted(plan) => { + if identity.color_ordinals.is_some() { + return Err(RusticolError::integrity( + "saved contracted OTF selection contains colour selectors", + )); + } + let count = plan.destination_by_owner_ordinal.len(); + contracted_count = Some(count); + let query_count = + helicity_indices.len().checked_mul(count).ok_or_else(|| { + RusticolError::integrity("saved OTF query count overflows") + })?; + (vec![0], vec!["color:contracted".to_string()], query_count) + } + }; + restored.push(OnTheFlyPreparedSelectionV1 { + identity, + helicity_indices: helicity_indices.into_boxed_slice(), + color_indices: color_indices.into_boxed_slice(), + helicity_ids: helicity_ids.into_boxed_slice(), + color_ids: color_ids.into_boxed_slice(), + query_count, + }); + } + let contracted = + contracted_count.map(|count| (restored[0].helicity_indices.as_ref(), count)); + self.lane.restore_snapshot(lane, requests, contracted)?; + self.last_prepared_selection = (!restored.is_empty()).then_some(0); + self.prepared_selections = restored; + self.pending_prepared_selection = None; + self.point_major_scratch = Vec::new(); + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn streamed_snapshot_checksum_rejects_damage_and_failed_save_keeps_previous_file() { + let path = std::env::temp_dir().join(format!( + "rusticol-otf-cache-{}-{}.bin", + std::process::id(), + TEMP_ID.fetch_add(1, Ordering::Relaxed) + )); + write_snapshot(&path, |writer| encode((42_u32, vec![1_u32, 2, 3]), writer)).unwrap(); + let mut reader = open_snapshot(&path).unwrap(); + let decoded: (u32, Vec) = decode(&mut reader).unwrap(); + assert_eq!(decoded, (42, vec![1, 2, 3])); + assert_eq!(reader.limit(), 0); + assert!( + write_snapshot(&path, |_| Err(RusticolError::serialization("test failure"))).is_err() + ); + assert!(open_snapshot(&path).is_ok()); + let mut bytes = fs::read(&path).unwrap(); + bytes[0] ^= 1; + fs::write(&path, &bytes).unwrap(); + assert!(open_snapshot(&path).is_err()); + fs::write(&path, &bytes[..8]).unwrap(); + assert!(open_snapshot(&path).is_err()); + fs::remove_file(path).unwrap(); + } +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/engine/on_the_fly_lane.rs b/pyamplicol/rust/crates/rusticol-core/src/engine/on_the_fly_lane.rs new file mode 100644 index 0000000000..d4b12ffc87 --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/engine/on_the_fly_lane.rs @@ -0,0 +1,3709 @@ +// SPDX-License-Identifier: 0BSD + +//! Private native LC lane for selector-local on-the-fly recurrence. +//! +//! This lane deliberately owns only the compact process seed, authenticated +//! model catalogs, and one warmed query-family executor. It never accepts or +//! materializes a [`crate::recurrence::DirectRecurrencePlan`], a dense flow +//! catalog, or a process-wide recurrence schedule. Public selectors are +//! decoded by the existing runtime adapter before they reach this module. +//! Likewise, load/evaluate/profile/benchmark need only this compact state. +//! The existing physics-metadata API can hang a separate lazy cache from +//! [`OnTheFlyNativeRuntime::seed`], deriving dense public axes only when a +//! caller explicitly requests introspection; that cache must not participate +//! in this lane's load or warmed-evaluation path. + +use super::on_the_fly_selectors::OnTheFlyCompactSelectorAdapterV1; +use super::on_the_fly_warm_up::{ + NativeOnTheFlyWarmUpEventKind, NativeOnTheFlyWarmUpStage, OnTheFlyWarmUpProgress, +}; +use super::recurrence_backend::NativeOnTheFlyPreparedExecutorResolver; +use super::recurrence_lane::{ + DirectProfileDelta, LcResolvedOutputLayout, PreparedParameterProjectionEntry, + accumulate_lc_diagonal_amplitude, direct_profile_from_delta, + projected_prepared_parameter_values, +}; +use super::recurrence_load::on_the_fly_source_major_momenta_into; +use super::*; +use crate::direct_arena::DirectArenaTrafficCounters; +#[cfg(feature = "on-the-fly-test-support")] +use crate::recurrence::AuthenticatedRecurrenceBuilderInput; +use crate::recurrence::direct_backend::DirectExecutionCounters; +use crate::recurrence::direct_runtime::DirectRuntimeActivityCounters; +#[cfg(any(test, feature = "on-the-fly-test-support"))] +use crate::recurrence::on_the_fly::OnTheFlyCouplingPolicyCensusV1; +#[cfg(feature = "on-the-fly-test-support")] +use crate::recurrence::on_the_fly::build_on_the_fly_selected_trace_against_seed_v1; +use crate::recurrence::on_the_fly::{ + DecodedLcQueryV1, OnTheFlyProcessSeedV1, OnTheFlyQueryConstructionPoolV1, + OnTheFlyQueryFamilyCensusV1, OnTheFlyQueryFamilyExecutionReportV1, + OnTheFlyQueryFamilyExecutorV1, OnTheFlyQueryFamilyHandleV1, OnTheFlyResolvedCouplingPolicyV1, + OnTheFlySelectedQueryOutcomeV1, PreparedOnTheFlyGrammarV1, QueryFamilyTraceInput, + build_selected_lc_query_family_v1, build_streamed_query_family_candidate_v1, + prepare_on_the_fly_process_v1, +}; +use crate::recurrence::template::ValidatedRecurrenceTemplateInput; +use crate::recurrence::{ + PreparedDirectExecutorCatalog, RecurrenceColorContraction, RuntimeColorContractionEntry, + RuntimeColorContractionReducer, RuntimeSymmetricGroupColorWorkspace, +}; +use std::time::{Duration, Instant}; + +#[path = "on_the_fly_lane_cache.rs"] +mod cache; +pub(super) use cache::OnTheFlyLaneSnapshotV1; + +#[cfg(feature = "on-the-fly-test-support")] +#[derive(Clone, Debug, PartialEq)] +pub(super) struct OnTheFlyExecutionDiagnosticCurrentV1 { + pub(super) semantic_digest: crate::recurrence::SemanticDigest, + pub(super) stage: u32, + pub(super) values: Vec<(f64, f64)>, +} + +#[cfg(feature = "on-the-fly-test-support")] +#[derive(Clone, Debug, PartialEq)] +pub(super) struct OnTheFlyExecutionDiagnosticSnapshotV1 { + pub(super) seed_digest: crate::recurrence::SemanticDigest, + pub(super) query_digest: crate::recurrence::SemanticDigest, + pub(super) trace_digest: crate::recurrence::SemanticDigest, + pub(super) raw_amplitude: (f64, f64), + pub(super) prepared_parameters: Vec<(f64, f64)>, + pub(super) currents: Vec, +} + +/// One public resolved-output destination represented by a selector-local +/// trace. The coefficient is the established helicity-orbit times LC color +/// weight; process normalization remains a call-local runtime input. +#[derive(Clone, Copy, Debug, PartialEq)] +pub(super) struct OnTheFlyLcReductionTargetV1 { + helicity_position: usize, + color_position: usize, + coefficient: f64, +} + +impl OnTheFlyLcReductionTargetV1 { + pub(super) fn new( + helicity_position: usize, + color_position: usize, + coefficient: f64, + ) -> RusticolResult { + if !coefficient.is_finite() || coefficient < 0.0 { + return Err(RusticolError::invalid_argument( + "on-the-fly LC reduction coefficient must be finite and nonnegative", + )); + } + Ok(Self { + helicity_position, + color_position, + coefficient, + }) + } +} + +/// Existing-runtime selector preparation expressed as one compact decoded LC +/// query plus its public resolved placements. A representative helicity may +/// legitimately address several physical orbit members. +#[derive(Clone, Debug, PartialEq)] +pub(super) struct OnTheFlyLcQueryRequestV1 { + query: DecodedLcQueryV1, + reduction_targets: Box<[OnTheFlyLcReductionTargetV1]>, +} + +impl OnTheFlyLcQueryRequestV1 { + pub(super) fn new( + query: DecodedLcQueryV1, + reduction_targets: Vec, + ) -> RusticolResult { + if reduction_targets.is_empty() { + return Err(RusticolError::invalid_argument( + "on-the-fly LC query has no selected reduction target", + )); + } + let mut coordinates = reduction_targets + .iter() + .map(|target| (target.helicity_position, target.color_position)) + .collect::>(); + coordinates.sort_unstable(); + if coordinates.windows(2).any(|pair| pair[0] == pair[1]) { + return Err(RusticolError::invalid_argument( + "on-the-fly LC query repeats a resolved reduction target", + )); + } + let total_coefficient = reduction_targets + .iter() + .try_fold(0.0_f64, |total, target| { + let total = total + target.coefficient; + total.is_finite().then_some(total).ok_or_else(|| { + RusticolError::invalid_argument( + "on-the-fly LC reduction coefficient sum is not finite", + ) + }) + })?; + if total_coefficient == 0.0 { + return Err(RusticolError::invalid_argument( + "on-the-fly LC query has zero total reduction weight", + )); + } + Ok(Self { + query, + reduction_targets: reduction_targets.into_boxed_slice(), + }) + } + + fn total_coefficient(&self) -> f64 { + self.reduction_targets + .iter() + .map(|target| target.coefficient) + .sum() + } +} + +struct PreparedOnTheFlyLcFamilyV1 { + requests: Box<[OnTheFlyLcQueryRequestV1]>, + amplitude_destinations: Box<[Option]>, + executor_handle: Option, + census: Option, + logical_point_capacity: u32, +} + +/// Retained contracted selection state. Decoded structural queries and +/// their traces are deliberately absent: after the union family is prepared, +/// execution needs only the selected public-helicity identity and the compact +/// H x S projection from authenticated metric destinations to union-family +/// amplitude destinations. +struct PreparedOnTheFlyContractedFamilyV1 { + helicity_ordinals: Box<[usize]>, + structural_color_count: usize, + amplitude_destinations: Box<[Option]>, + executor_handle: Option, + census: Option, + logical_point_capacity: u32, +} + +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub(super) struct OnTheFlyRetainedStateCensusV1 { + pub(super) family_count: usize, + pub(super) request_count: usize, + pub(super) amplitude_destination_count: usize, + pub(super) executor_handle_count: usize, + pub(super) query_local_trace_count: usize, + pub(super) embedded_lookup_key_count: usize, +} + +#[cfg(test)] +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub(super) struct OnTheFlyProcessPreparationCensusV1 { + pub(super) catalog_validation_count: u64, + pub(super) grammar_preparation_count: u64, +} + +/// Process-local production owner for the private on-the-fly lane. +/// +/// Field order is intentional: the family executor (and therefore every row +/// table borrowed by a prepared kernel) is dropped before the catalogs and +/// compact process seed from which its traces were constructed. +pub(super) struct OnTheFlyNativeRuntime { + executor: OnTheFlyQueryFamilyExecutorV1, + templates: ValidatedRecurrenceTemplateInput, + direct_catalog: PreparedDirectExecutorCatalog, + seed: OnTheFlyProcessSeedV1, + requested_query_construction_threads: usize, + effective_query_construction_threads: usize, + prepared_grammar: Option, + coupling_policy: Option, + coupling_policy_resolution: Option, + process_preparation_count: u64, + #[cfg(test)] + process_preparation_census: OnTheFlyProcessPreparationCensusV1, + parameter_defaults: Box<[crate::EagerComplex64]>, + parameter_projection: Box<[PreparedParameterProjectionEntry]>, + runtime_parameter_values: Box<[f64]>, + families: Vec, + last_family: Option, + pending_family: Option, + contracted_family: Option, + pending_contracted_family: Option, + #[cfg(test)] + contracted_test_execution_attempt: usize, + #[cfg(test)] + contracted_test_fail_execution_at: Option, + #[cfg(test)] + contracted_max_live_query_outcomes: usize, + source_momenta_scratch: Vec, + amplitude_scratch: Vec<(f64, f64)>, + symmetric_group_color_workspace: Option, +} + +fn emit_progress( + progress: Option<&mut OnTheFlyWarmUpProgress<'_>>, + kind: NativeOnTheFlyWarmUpEventKind, + stage: NativeOnTheFlyWarmUpStage, + completed: usize, + total: usize, + message: Option<&str>, +) -> RusticolResult<()> { + if let Some(progress) = progress { + progress.emit(kind, stage, completed, total, message)?; + } + Ok(()) +} + +/// Map one owner-ordered selector request into the group-ordered amplitude +/// projection retained by contracted execution. The inverse owner map is +/// authenticated at load, so every later reducer consumes this table directly +/// in normalized local-group order. +fn contracted_projection_index( + helicity_position: usize, + owner_ordinal: usize, + structural_color_count: usize, + destination_by_owner_ordinal: &[u32], +) -> RusticolResult { + let group = usize::try_from(*destination_by_owner_ordinal.get(owner_ordinal).ok_or_else( + || { + RusticolError::integrity( + "on-the-fly contracted owner is outside its destination mapping", + ) + }, + )?) + .map_err(|_| RusticolError::artifact("on-the-fly contracted destination exceeds usize"))?; + if group >= structural_color_count { + return Err(RusticolError::integrity( + "on-the-fly contracted destination is outside the structural group domain", + )); + } + helicity_position + .checked_mul(structural_color_count) + .and_then(|offset| offset.checked_add(group)) + .ok_or_else(|| { + RusticolError::invalid_argument("on-the-fly contracted projection exceeds usize") + }) +} + +fn emit_reused_family_progress( + mut progress: Option<&mut OnTheFlyWarmUpProgress<'_>>, + query_count: usize, +) -> RusticolResult<()> { + emit_progress( + progress.as_deref_mut(), + NativeOnTheFlyWarmUpEventKind::Start, + NativeOnTheFlyWarmUpStage::QueryFamily, + query_count, + query_count, + Some("query family already retained"), + )?; + emit_progress( + progress.as_deref_mut(), + NativeOnTheFlyWarmUpEventKind::End, + NativeOnTheFlyWarmUpStage::QueryFamily, + query_count, + query_count, + Some("query family reused"), + )?; + emit_progress( + progress.as_deref_mut(), + NativeOnTheFlyWarmUpEventKind::Start, + NativeOnTheFlyWarmUpStage::FamilyFinalization, + 1, + 1, + Some("family finalization already retained"), + )?; + emit_progress( + progress, + NativeOnTheFlyWarmUpEventKind::End, + NativeOnTheFlyWarmUpStage::FamilyFinalization, + 1, + 1, + Some("family finalization reused"), + ) +} + +impl OnTheFlyNativeRuntime { + #[allow(clippy::too_many_arguments)] + pub(super) fn new( + templates: ValidatedRecurrenceTemplateInput, + direct_catalog: PreparedDirectExecutorCatalog, + seed: OnTheFlyProcessSeedV1, + resolver: NativeOnTheFlyPreparedExecutorResolver, + requested_query_construction_threads: usize, + effective_query_construction_threads: usize, + parameter_defaults: Vec, + parameter_projection: Vec, + runtime_parameter_values: &[f64], + ) -> RusticolResult { + let summary = templates.summary(); + if seed.template_catalog_digest() != summary.catalog_digest + || seed.model_digest() != summary.compiled_model_digest + || seed.prepared_pack_digest() != summary.prepared_kernel_pack_digest + || seed.direct_catalog_digest() != direct_catalog.direct_template_catalog_digest() + { + return Err(RusticolError::integrity( + "on-the-fly native lane catalogs do not match its compact process seed", + )); + } + if parameter_defaults.len() + != usize::try_from(summary.parameter_count) + .map_err(|_| RusticolError::artifact("on-the-fly parameter count exceeds usize"))? + { + return Err(RusticolError::integrity( + "on-the-fly prepared parameter defaults have the wrong size", + )); + } + if runtime_parameter_values + .iter() + .any(|value| !value.is_finite()) + { + return Err(RusticolError::invalid_argument( + "on-the-fly runtime parameter value is not finite", + )); + } + if requested_query_construction_threads == 0 + || effective_query_construction_threads == 0 + || effective_query_construction_threads > requested_query_construction_threads + { + return Err(RusticolError::invalid_argument( + "on-the-fly effective query construction threads must be in the positive requested domain", + )); + } + + let prepared_parameters = projected_prepared_parameter_values( + ¶meter_defaults, + ¶meter_projection, + runtime_parameter_values, + )?; + let mut executor = OnTheFlyQueryFamilyExecutorV1::new(resolver); + executor.set_parameters(&prepared_parameters)?; + Ok(Self { + executor, + templates, + direct_catalog, + seed, + requested_query_construction_threads, + effective_query_construction_threads, + prepared_grammar: None, + coupling_policy: None, + coupling_policy_resolution: None, + process_preparation_count: 0, + #[cfg(test)] + process_preparation_census: OnTheFlyProcessPreparationCensusV1::default(), + parameter_defaults: parameter_defaults.into_boxed_slice(), + parameter_projection: parameter_projection.into_boxed_slice(), + runtime_parameter_values: runtime_parameter_values.to_vec().into_boxed_slice(), + families: Vec::new(), + last_family: None, + pending_family: None, + contracted_family: None, + pending_contracted_family: None, + #[cfg(test)] + contracted_test_execution_attempt: 0, + #[cfg(test)] + contracted_test_fail_execution_at: None, + #[cfg(test)] + contracted_max_live_query_outcomes: 0, + source_momenta_scratch: Vec::new(), + amplitude_scratch: Vec::new(), + symmetric_group_color_workspace: None, + }) + } + + pub(super) fn install_symmetric_group_color_workspace( + &mut self, + workspace: RuntimeSymmetricGroupColorWorkspace, + ) -> RusticolResult<()> { + if self.symmetric_group_color_workspace.is_some() { + return Err(RusticolError::internal( + "on-the-fly symmetric-group color workspace was installed twice", + )); + } + self.symmetric_group_color_workspace = Some(workspace); + Ok(()) + } + + pub(super) const fn seed(&self) -> &OnTheFlyProcessSeedV1 { + &self.seed + } + + pub(super) const fn requested_query_construction_threads(&self) -> usize { + self.requested_query_construction_threads + } + + pub(super) const fn effective_query_construction_threads(&self) -> usize { + self.effective_query_construction_threads + } + + /// Execute one retained public query through the production batched + /// query-family path and expose point-zero current values for diagnostics. + /// This remains absent from release builds and does not alter row grouping. + #[cfg(feature = "on-the-fly-test-support")] + pub(super) fn execution_diagnostic_v1( + &mut self, + authenticated: &AuthenticatedRecurrenceBuilderInput, + selected_public_flow_id: u32, + public_helicities: &[i32], + point_major_momenta: &[f64], + runtime_parameters: &[f64], + ) -> RusticolResult { + let selected = build_on_the_fly_selected_trace_against_seed_v1( + authenticated, + &self.direct_catalog, + &self.seed, + selected_public_flow_id, + public_helicities, + true, + self.symmetric_group_color_workspace.is_some(), + )?; + let request = OnTheFlyLcQueryRequestV1::new( + selected.query.clone(), + vec![OnTheFlyLcReductionTargetV1::new(0, 0, 1.0)?], + )?; + self.prepare_lc_queries(std::slice::from_ref(&request), 1)?; + let destination = self + .current_family() + .and_then(|family| family.amplitude_destinations.first()) + .copied() + .flatten() + .ok_or_else(|| { + RusticolError::invalid_argument( + "on-the-fly execution diagnostic selected a structural-zero query", + ) + })?; + let _ = self.execute_amplitudes(point_major_momenta, 1, runtime_parameters)?; + let raw_amplitude = *self + .amplitude_scratch + .get(destination) + .ok_or_else(|| RusticolError::integrity("on-the-fly diagnostic amplitude is absent"))?; + let currents = self + .executor + .observed_currents(0)? + .into_iter() + .map(|current| OnTheFlyExecutionDiagnosticCurrentV1 { + semantic_digest: current.semantic_digest, + stage: current.stage, + values: current.values, + }) + .collect(); + let prepared_parameters = projected_prepared_parameter_values( + &self.parameter_defaults, + &self.parameter_projection, + runtime_parameters, + )?; + Ok(OnTheFlyExecutionDiagnosticSnapshotV1 { + seed_digest: selected.seed.semantic_digest(), + query_digest: selected.query.semantic_digest(), + trace_digest: selected.trace.semantic_digest(), + raw_amplitude, + prepared_parameters, + currents, + }) + } + + #[cfg(any(test, feature = "on-the-fly-test-support"))] + pub(super) fn coupling_policy_census(&self) -> Option { + self.coupling_policy.as_ref().map(|policy| policy.census()) + } + + #[cfg(any(test, feature = "on-the-fly-test-support"))] + pub(super) const fn coupling_policy_resolution(&self) -> Option { + self.coupling_policy_resolution + } + + /// Private read-only production introspection of the existing preparation count. + pub(super) const fn process_preparation_count(&self) -> u64 { + self.process_preparation_count + } + + #[cfg(test)] + pub(super) const fn process_preparation_census(&self) -> OnTheFlyProcessPreparationCensusV1 { + self.process_preparation_census + } + + fn ensure_process_prepared(&mut self) -> RusticolResult<()> { + match (&self.prepared_grammar, &self.coupling_policy) { + (Some(_), Some(_)) => return Ok(()), + (None, None) => {} + _ => { + return Err(RusticolError::internal( + "on-the-fly process preparation is only partially initialized", + )); + } + } + let started = Instant::now(); + let (grammar, policy) = prepare_on_the_fly_process_v1(&self.templates, &self.seed)?; + #[cfg(test)] + { + self.process_preparation_census.catalog_validation_count = self + .process_preparation_census + .catalog_validation_count + .checked_add(1) + .ok_or_else(|| { + RusticolError::internal("on-the-fly catalog-validation count exceeds u64") + })?; + self.process_preparation_census.grammar_preparation_count = self + .process_preparation_census + .grammar_preparation_count + .checked_add(1) + .ok_or_else(|| { + RusticolError::internal("on-the-fly grammar-preparation count exceeds u64") + })?; + } + self.process_preparation_count = + self.process_preparation_count + .checked_add(1) + .ok_or_else(|| { + RusticolError::internal("on-the-fly process preparation count exceeds u64") + })?; + self.prepared_grammar = Some(grammar); + self.coupling_policy = Some(policy); + self.coupling_policy_resolution = Some(started.elapsed()); + Ok(()) + } + + pub(super) fn prepare_process_for_warm_up( + &mut self, + progress: &mut OnTheFlyWarmUpProgress<'_>, + ) -> RusticolResult { + let already_prepared = self.prepared_grammar.is_some() && self.coupling_policy.is_some(); + progress.emit( + NativeOnTheFlyWarmUpEventKind::Start, + NativeOnTheFlyWarmUpStage::ProcessPreparation, + usize::from(already_prepared), + 1, + already_prepared.then_some("process preparation already retained"), + )?; + self.ensure_process_prepared()?; + progress.emit( + NativeOnTheFlyWarmUpEventKind::End, + NativeOnTheFlyWarmUpStage::ProcessPreparation, + 1, + 1, + already_prepared.then_some("process preparation reused"), + )?; + Ok(already_prepared) + } + + pub(super) fn report_reused_family_for_warm_up( + &self, + query_count: usize, + progress: &mut OnTheFlyWarmUpProgress<'_>, + ) -> RusticolResult<()> { + emit_reused_family_progress(Some(progress), query_count) + } + + fn prepare_streamed_lc_candidate_for_warm_up( + &mut self, + requests: &[OnTheFlyLcQueryRequestV1], + logical_point_capacity: u32, + query_construction_threads: usize, + progress: &mut OnTheFlyWarmUpProgress<'_>, + ) -> RusticolResult<(bool, PreparedOnTheFlyLcFamilyV1)> { + let enable_cyclic_trace_reflection = self.symmetric_group_color_workspace.is_some(); + let coupling_policy = self.coupling_policy.as_ref().ok_or_else(|| { + RusticolError::internal("on-the-fly coupling policy disappeared after preparation") + })?; + let grammar = self.prepared_grammar.as_ref().ok_or_else(|| { + RusticolError::internal("on-the-fly grammar disappeared after preparation") + })?; + let worker_count = query_construction_threads.min(requests.len()).max(1); + let query_pool = OnTheFlyQueryConstructionPoolV1::new(worker_count)?; + let mut amplitude_destinations = Vec::new(); + amplitude_destinations + .try_reserve_exact(requests.len()) + .map_err(|error| { + RusticolError::invalid_argument(format!( + "on-the-fly LC warm-up projection allocation failed: {error}" + )) + })?; + amplitude_destinations.resize(requests.len(), None); + + let templates = &self.templates; + let direct_catalog = &self.direct_catalog; + let seed = &self.seed; + let executor = &mut self.executor; + let mut binding_started = false; + let mut processed_query_count = 0_usize; + let streamed = build_streamed_query_family_candidate_v1(direct_catalog, |consumer| { + while processed_query_count < requests.len() { + let chunk_end = processed_query_count + .saturating_add(query_pool.chunk_capacity()) + .min(requests.len()); + let expected_queries = requests[processed_query_count..chunk_end] + .iter() + .map(|request| grammar.canonicalize_query(seed, request.query.clone())) + .collect::>>()?; + let outcomes = query_pool.build_chunk( + templates, + direct_catalog, + seed, + coupling_policy, + grammar, + enable_cyclic_trace_reflection, + expected_queries.clone(), + )?; + if outcomes.len() != expected_queries.len() { + return Err(RusticolError::integrity( + "on-the-fly LC warm-up trace builder changed a chunk count", + )); + } + + let mut traces = Vec::new(); + let mut projection_slots = Vec::new(); + traces.try_reserve_exact(outcomes.len()).map_err(|error| { + RusticolError::invalid_argument(format!( + "on-the-fly LC warm-up trace-chunk allocation failed: {error}" + )) + })?; + projection_slots + .try_reserve_exact(outcomes.len()) + .map_err(|error| { + RusticolError::invalid_argument(format!( + "on-the-fly LC warm-up projection-chunk allocation failed: {error}" + )) + })?; + for (chunk_offset, (expected_query, outcome)) in + expected_queries.into_iter().zip(outcomes).enumerate() + { + let request_index = processed_query_count + chunk_offset; + match outcome { + OnTheFlySelectedQueryOutcomeV1::Trace(selected) => { + if selected.query != expected_query { + return Err(RusticolError::integrity( + "on-the-fly LC warm-up trace query changed during construction", + )); + } + projection_slots.push(request_index); + traces.push(selected); + } + OnTheFlySelectedQueryOutcomeV1::StructuralZero { query } => { + if query != expected_query { + return Err(RusticolError::integrity( + "on-the-fly LC warm-up structural-zero query changed during construction", + )); + } + } + } + } + if !traces.is_empty() { + if !binding_started { + executor + .resolver_mut() + .begin_on_the_fly_family_binding(templates, direct_catalog)?; + binding_started = true; + } + executor.resolver_mut().extend_on_the_fly_family_binding( + templates, + direct_catalog, + seed, + traces.iter_mut().map(|selected| &mut selected.trace), + )?; + let inputs = traces + .iter() + .map(|selected| QueryFamilyTraceInput { + trace: &selected.trace, + projection: selected.projection, + }) + .collect::>(); + let destinations = consumer.push_chunk(&inputs)?; + if destinations.len() != projection_slots.len() { + return Err(RusticolError::integrity( + "on-the-fly LC warm-up streamed destination range changed a chunk count", + )); + } + for (request_index, destination) in + projection_slots.into_iter().zip(destinations) + { + let slot = + amplitude_destinations + .get_mut(request_index) + .ok_or_else(|| { + RusticolError::integrity( + "on-the-fly LC warm-up projection slot is absent", + ) + })?; + if slot.replace(destination as usize).is_some() { + return Err(RusticolError::integrity( + "on-the-fly LC warm-up projection slot is repeated", + )); + } + } + } + processed_query_count = chunk_end; + progress.emit( + NativeOnTheFlyWarmUpEventKind::Update, + NativeOnTheFlyWarmUpStage::QueryFamily, + processed_query_count, + requests.len(), + None, + )?; + } + Ok(()) + })?; + if processed_query_count != requests.len() { + return Err(RusticolError::integrity( + "on-the-fly LC warm-up streamed construction omitted a selected query", + )); + } + progress.emit( + NativeOnTheFlyWarmUpEventKind::End, + NativeOnTheFlyWarmUpStage::QueryFamily, + requests.len(), + requests.len(), + None, + )?; + progress.emit( + NativeOnTheFlyWarmUpEventKind::Start, + NativeOnTheFlyWarmUpStage::FamilyFinalization, + 0, + 1, + None, + )?; + let (cache_hit, census) = if let Some(streamed) = streamed { + if !binding_started { + return Err(RusticolError::integrity( + "on-the-fly LC warm-up streamed family has no semantic binding transaction", + )); + } + let cache_hit = + executor.prepare_streamed_candidate(streamed, logical_point_capacity)?; + let census = executor.prepared_census().ok_or_else(|| { + RusticolError::integrity("on-the-fly LC warm-up family has no work census") + })?; + let projected_nonzero_count = amplitude_destinations + .iter() + .filter(|destination| destination.is_some()) + .count(); + if usize::try_from(census.union_amplitude_destination_count).ok() + != Some(projected_nonzero_count) + { + return Err(RusticolError::integrity( + "on-the-fly LC warm-up family does not retain one destination per nonzero query", + )); + } + (cache_hit, Some(census)) + } else { + if binding_started { + return Err(RusticolError::integrity( + "on-the-fly LC warm-up all-zero family retained semantic bindings", + )); + } + (false, None) + }; + progress.emit( + NativeOnTheFlyWarmUpEventKind::End, + NativeOnTheFlyWarmUpStage::FamilyFinalization, + 1, + 1, + None, + )?; + Ok(( + cache_hit, + PreparedOnTheFlyLcFamilyV1 { + requests: requests.to_vec().into_boxed_slice(), + amplitude_destinations: amplitude_destinations.into_boxed_slice(), + executor_handle: None, + census, + logical_point_capacity, + }, + )) + } + + /// Cold selector/trace construction. Repeating an identical public + /// selection reuses its retained requests, projections, and warmed row + /// family; increasing only the point capacity replaces numeric workspace. + pub(super) fn prepare_lc_queries( + &mut self, + requests: &[OnTheFlyLcQueryRequestV1], + logical_point_capacity: u32, + ) -> RusticolResult { + self.prepare_lc_queries_impl(requests, logical_point_capacity, None, None) + } + + pub(super) fn prepare_lc_queries_for_warm_up( + &mut self, + requests: &[OnTheFlyLcQueryRequestV1], + logical_point_capacity: u32, + query_construction_threads: usize, + progress: &mut OnTheFlyWarmUpProgress<'_>, + ) -> RusticolResult { + self.prepare_lc_queries_impl( + requests, + logical_point_capacity, + Some(query_construction_threads), + Some(progress), + ) + } + + fn prepare_lc_queries_impl( + &mut self, + requests: &[OnTheFlyLcQueryRequestV1], + logical_point_capacity: u32, + query_construction_threads: Option, + mut progress: Option<&mut OnTheFlyWarmUpProgress<'_>>, + ) -> RusticolResult { + if requests.is_empty() || logical_point_capacity == 0 { + return Err(RusticolError::invalid_argument( + "on-the-fly LC preparation requires queries and a nonzero point capacity", + )); + } + if self.contracted_family.is_some() || self.pending_contracted_family.is_some() { + return Err(RusticolError::internal( + "on-the-fly lane cannot mix LC and contracted retained families", + )); + } + if requests + .iter() + .any(|request| request.query.process_seed_digest() != self.seed.semantic_digest()) + { + return Err(RusticolError::integrity( + "on-the-fly LC query belongs to a different compact process seed", + )); + } + + // The common same-selector path compares the sole retained family + // directly. When the numeric + // workspace is already large enough it also avoids a redundant second + // identity walk inside the executor. + if self.pending_family.is_none() + && self.last_family.is_some_and(|index| { + self.families.get(index).is_some_and(|family| { + family.requests.as_ref() == requests + && logical_point_capacity <= family.logical_point_capacity + }) + }) + { + emit_reused_family_progress(progress.as_deref_mut(), requests.len())?; + return Ok(true); + } + if self + .pending_family + .as_ref() + .is_some_and(|family| family.requests.as_ref() == requests) + { + let (current_capacity, has_executable_family) = self + .pending_family + .as_ref() + .map(|family| (family.logical_point_capacity, family.census.is_some())) + .expect("matching on-the-fly pending family disappeared"); + if logical_point_capacity > current_capacity + && has_executable_family + && let Err(error) = self.executor.resize_active_family(logical_point_capacity) + { + self.discard_pending_lc_queries()?; + return Err(error); + } + let family = self + .pending_family + .as_mut() + .expect("matching on-the-fly pending family disappeared after resize"); + family.logical_point_capacity = + family.logical_point_capacity.max(logical_point_capacity); + emit_reused_family_progress(progress.as_deref_mut(), requests.len())?; + return Ok(false); + } + // A different uncommitted selector is a failed/superseded candidate, + // not another cache entry. + self.discard_pending_lc_queries()?; + + let retained = self.last_family.filter(|&index| { + self.families + .get(index) + .is_some_and(|family| family.requests.as_ref() == requests) + }); + if let Some(index) = retained { + let family = self + .families + .get_mut(index) + .expect("on-the-fly retained family index is absent"); + let cache_hit = if let Some(handle) = family.executor_handle { + self.executor + .activate_retained_family(handle, logical_point_capacity)? + } else { + true + }; + family.logical_point_capacity = + family.logical_point_capacity.max(logical_point_capacity); + self.pending_family = None; + self.last_family = Some(index); + emit_reused_family_progress(progress.as_deref_mut(), requests.len())?; + return Ok(cache_hit); + } + + if self.families.capacity() == 0 { + self.families.try_reserve_exact(1).map_err(|error| { + RusticolError::invalid_argument(format!( + "on-the-fly retained-family allocation failed: {error}" + )) + })?; + } + let candidate_result = (|| { + emit_progress( + progress.as_deref_mut(), + NativeOnTheFlyWarmUpEventKind::Start, + NativeOnTheFlyWarmUpStage::QueryFamily, + 0, + requests.len(), + None, + )?; + self.ensure_process_prepared()?; + if let Some(progress) = progress.as_deref_mut() { + return self.prepare_streamed_lc_candidate_for_warm_up( + requests, + logical_point_capacity, + query_construction_threads.expect("warm-up worker count is absent"), + progress, + ); + } + let enable_cyclic_trace_reflection = self.symmetric_group_color_workspace.is_some(); + let coupling_policy = self.coupling_policy.as_ref().ok_or_else(|| { + RusticolError::internal("on-the-fly coupling policy disappeared after preparation") + })?; + let grammar = self.prepared_grammar.as_ref().ok_or_else(|| { + RusticolError::internal("on-the-fly grammar disappeared after preparation") + })?; + let expected_queries = requests + .iter() + .map(|request| grammar.canonicalize_query(&self.seed, request.query.clone())) + .collect::>>()?; + let outcomes = build_selected_lc_query_family_v1( + &self.templates, + &self.direct_catalog, + &self.seed, + coupling_policy, + grammar, + enable_cyclic_trace_reflection, + self.effective_query_construction_threads, + expected_queries.iter().cloned(), + )?; + if outcomes.len() != requests.len() { + return Err(RusticolError::integrity( + "on-the-fly batch trace builder changed the query count", + )); + } + emit_progress( + progress.as_deref_mut(), + NativeOnTheFlyWarmUpEventKind::End, + NativeOnTheFlyWarmUpStage::QueryFamily, + requests.len(), + requests.len(), + None, + )?; + let mut traces = Vec::new(); + let mut amplitude_destinations = vec![None; requests.len()]; + traces.try_reserve_exact(requests.len()).map_err(|error| { + RusticolError::invalid_argument(format!( + "on-the-fly trace-family allocation failed: {error}" + )) + })?; + for (request_index, (expected_query, outcome)) in + expected_queries.into_iter().zip(outcomes).enumerate() + { + match outcome { + OnTheFlySelectedQueryOutcomeV1::Trace(selected) => { + if selected.query != expected_query { + return Err(RusticolError::integrity( + "on-the-fly trace query changed during construction", + )); + } + amplitude_destinations[request_index] = Some(traces.len()); + traces.push(selected); + } + OnTheFlySelectedQueryOutcomeV1::StructuralZero { query } => { + if query != expected_query { + return Err(RusticolError::integrity( + "on-the-fly structural-zero query changed during construction", + )); + } + } + } + } + emit_progress( + progress.as_deref_mut(), + NativeOnTheFlyWarmUpEventKind::Start, + NativeOnTheFlyWarmUpStage::FamilyFinalization, + 0, + 1, + None, + )?; + let (cache_hit, census) = if traces.is_empty() { + (false, None) + } else { + self.executor.resolver_mut().bind_on_the_fly_family( + &self.templates, + &self.direct_catalog, + &self.seed, + &mut traces, + )?; + let inputs = traces + .iter() + .map(|selected| QueryFamilyTraceInput { + trace: &selected.trace, + projection: selected.projection, + }) + .collect::>(); + let cache_hit = + self.executor + .prepare(&self.direct_catalog, &inputs, logical_point_capacity)?; + let census = self.executor.prepared_census().ok_or_else(|| { + RusticolError::integrity("on-the-fly prepared family has no work census") + })?; + if usize::try_from(census.union_amplitude_destination_count).ok() + != Some(traces.len()) + { + return Err(RusticolError::integrity( + "on-the-fly family does not retain one destination per nonzero query", + )); + } + if cache_hit && self.executor.active_retained_handle().is_none() { + return Err(RusticolError::integrity( + "on-the-fly cache hit has no retained executor handle", + )); + } + (cache_hit, Some(census)) + }; + emit_progress( + progress.as_deref_mut(), + NativeOnTheFlyWarmUpEventKind::End, + NativeOnTheFlyWarmUpStage::FamilyFinalization, + 1, + 1, + None, + )?; + Ok(( + cache_hit, + PreparedOnTheFlyLcFamilyV1 { + requests: requests.to_vec().into_boxed_slice(), + amplitude_destinations: amplitude_destinations.into_boxed_slice(), + executor_handle: None, + census, + logical_point_capacity, + }, + )) + })(); + let (cache_hit, candidate) = match candidate_result { + Ok(candidate) => candidate, + Err(error) => { + self.discard_pending_lc_queries()?; + return Err(error); + } + }; + // Selection-level state is committed only after the first successful + // evaluation, including structural-zero and executor-cache-hit cases. + self.pending_family = Some(candidate); + Ok(cache_hit) + } + + /// Prepare the transient H x S structural query family consumed by a + /// contracted NLC/full color metric. Only its compact identity and + /// amplitude projection survive this cold boundary. + #[allow(clippy::too_many_arguments)] + pub(super) fn prepare_contracted_queries( + &mut self, + selectors: &OnTheFlyCompactSelectorAdapterV1, + helicity_ordinals: &[usize], + structural_color_count: usize, + destination_by_owner_ordinal: &[u32], + logical_point_capacity: u32, + ) -> RusticolResult { + self.prepare_contracted_queries_impl( + selectors, + helicity_ordinals, + structural_color_count, + destination_by_owner_ordinal, + logical_point_capacity, + None, + None, + ) + } + + #[allow(clippy::too_many_arguments)] + pub(super) fn prepare_contracted_queries_for_warm_up( + &mut self, + selectors: &OnTheFlyCompactSelectorAdapterV1, + helicity_ordinals: &[usize], + structural_color_count: usize, + destination_by_owner_ordinal: &[u32], + logical_point_capacity: u32, + query_construction_threads: usize, + progress: &mut OnTheFlyWarmUpProgress<'_>, + ) -> RusticolResult { + self.prepare_contracted_queries_impl( + selectors, + helicity_ordinals, + structural_color_count, + destination_by_owner_ordinal, + logical_point_capacity, + Some(query_construction_threads), + Some(progress), + ) + } + + #[allow(clippy::too_many_arguments)] + fn prepare_contracted_queries_impl( + &mut self, + selectors: &OnTheFlyCompactSelectorAdapterV1, + helicity_ordinals: &[usize], + structural_color_count: usize, + destination_by_owner_ordinal: &[u32], + logical_point_capacity: u32, + query_construction_threads: Option, + mut progress: Option<&mut OnTheFlyWarmUpProgress<'_>>, + ) -> RusticolResult { + if helicity_ordinals.is_empty() + || structural_color_count == 0 + || logical_point_capacity == 0 + { + return Err(RusticolError::invalid_argument( + "on-the-fly contracted preparation requires helicities, structural colors, and a nonzero point capacity", + )); + } + if !self.families.is_empty() || self.pending_family.is_some() { + return Err(RusticolError::internal( + "on-the-fly lane cannot mix LC and contracted retained families", + )); + } + if helicity_ordinals.windows(2).any(|pair| pair[0] >= pair[1]) { + return Err(RusticolError::invalid_argument( + "on-the-fly contracted helicity ordinals are not strictly increasing", + )); + } + let query_count = helicity_ordinals + .len() + .checked_mul(structural_color_count) + .ok_or_else(|| { + RusticolError::invalid_argument("on-the-fly contracted query shape exceeds usize") + })?; + if destination_by_owner_ordinal.len() != structural_color_count + || selectors.color_count() != structural_color_count + { + return Err(RusticolError::integrity( + "on-the-fly contracted selector or destination shape is inconsistent", + )); + } + let mut destination_seen = vec![false; structural_color_count]; + for destination in destination_by_owner_ordinal { + let destination = usize::try_from(*destination).map_err(|_| { + RusticolError::artifact("on-the-fly contracted destination exceeds usize") + })?; + let seen = destination_seen.get_mut(destination).ok_or_else(|| { + RusticolError::integrity( + "on-the-fly contracted destination is outside the structural owner domain", + ) + })?; + if *seen { + return Err(RusticolError::integrity( + "on-the-fly contracted destination mapping is not one-to-one", + )); + } + *seen = true; + } + if destination_seen.contains(&false) { + return Err(RusticolError::integrity( + "on-the-fly contracted destination mapping is not complete", + )); + } + let identity_matches = |family: &PreparedOnTheFlyContractedFamilyV1| { + family.helicity_ordinals.as_ref() == helicity_ordinals + && family.structural_color_count == structural_color_count + }; + if self + .pending_contracted_family + .as_ref() + .is_some_and(identity_matches) + { + let (current_capacity, has_executable_family) = self + .pending_contracted_family + .as_ref() + .map(|family| (family.logical_point_capacity, family.census.is_some())) + .expect("matching contracted pending family disappeared"); + if logical_point_capacity > current_capacity + && has_executable_family + && let Err(error) = self.executor.resize_active_family(logical_point_capacity) + { + self.discard_pending_contracted_queries()?; + return Err(error); + } + let family = self + .pending_contracted_family + .as_mut() + .expect("matching contracted pending family disappeared after resize"); + family.logical_point_capacity = + family.logical_point_capacity.max(logical_point_capacity); + emit_reused_family_progress(progress.as_deref_mut(), query_count)?; + return Ok(false); + } + self.discard_pending_contracted_queries()?; + if self + .contracted_family + .as_ref() + .is_some_and(identity_matches) + { + let family = self + .contracted_family + .as_mut() + .expect("matching retained contracted family disappeared"); + let cache_hit = if let Some(handle) = family.executor_handle { + self.executor + .activate_retained_family(handle, logical_point_capacity)? + } else { + true + }; + family.logical_point_capacity = + family.logical_point_capacity.max(logical_point_capacity); + emit_reused_family_progress(progress.as_deref_mut(), query_count)?; + return Ok(cache_hit); + } + + let candidate_result = (|| { + emit_progress( + progress.as_deref_mut(), + NativeOnTheFlyWarmUpEventKind::Start, + NativeOnTheFlyWarmUpStage::QueryFamily, + 0, + query_count, + None, + )?; + self.ensure_process_prepared()?; + let enable_cyclic_trace_reflection = self.symmetric_group_color_workspace.is_some(); + let coupling_policy = self.coupling_policy.as_ref().ok_or_else(|| { + RusticolError::internal("on-the-fly coupling policy disappeared after preparation") + })?; + let grammar = self.prepared_grammar.as_ref().ok_or_else(|| { + RusticolError::internal("on-the-fly grammar disappeared after preparation") + })?; + let query_pool = OnTheFlyQueryConstructionPoolV1::new( + query_construction_threads.unwrap_or(self.effective_query_construction_threads), + )?; + let mut amplitude_destinations = Vec::new(); + amplitude_destinations + .try_reserve_exact(query_count) + .map_err(|error| { + RusticolError::invalid_argument(format!( + "on-the-fly contracted projection allocation failed: {error}" + )) + })?; + amplitude_destinations.resize(query_count, None); + + let templates = &self.templates; + let direct_catalog = &self.direct_catalog; + let seed = &self.seed; + let executor = &mut self.executor; + let mut binding_started = false; + let mut processed_query_count = 0_usize; + #[cfg(test)] + let mut max_live_query_outcomes = 0_usize; + let streamed = build_streamed_query_family_candidate_v1(direct_catalog, |consumer| { + while processed_query_count < query_count { + let chunk_end = processed_query_count + .saturating_add(query_pool.chunk_capacity()) + .min(query_count); + let mut queries = Vec::new(); + queries + .try_reserve_exact(chunk_end - processed_query_count) + .map_err(|error| { + RusticolError::invalid_argument(format!( + "on-the-fly contracted query-chunk allocation failed: {error}" + )) + })?; + for request_index in processed_query_count..chunk_end { + let helicity_position = request_index / structural_color_count; + let owner_ordinal = request_index % structural_color_count; + let query = selectors.decoded_query_at( + seed, + helicity_ordinals[helicity_position], + owner_ordinal, + )?; + queries.push(grammar.canonicalize_query(seed, query)?); + } + let expected_queries = queries.clone(); + let outcomes = query_pool.build_chunk( + templates, + direct_catalog, + seed, + coupling_policy, + grammar, + enable_cyclic_trace_reflection, + queries, + )?; + if outcomes.len() != expected_queries.len() { + return Err(RusticolError::integrity( + "on-the-fly contracted trace builder changed a chunk count", + )); + } + #[cfg(test)] + { + max_live_query_outcomes = max_live_query_outcomes.max(outcomes.len()); + } + + let mut traces = Vec::new(); + let mut projection_slots = Vec::new(); + traces.try_reserve_exact(outcomes.len()).map_err(|error| { + RusticolError::invalid_argument(format!( + "on-the-fly contracted trace-chunk allocation failed: {error}" + )) + })?; + projection_slots + .try_reserve_exact(outcomes.len()) + .map_err(|error| { + RusticolError::invalid_argument(format!( + "on-the-fly contracted projection-chunk allocation failed: {error}" + )) + })?; + for (chunk_offset, (expected_query, outcome)) in + expected_queries.into_iter().zip(outcomes).enumerate() + { + let request_index = processed_query_count + chunk_offset; + let helicity_position = request_index / structural_color_count; + let owner_ordinal = request_index % structural_color_count; + let projection_index = contracted_projection_index( + helicity_position, + owner_ordinal, + structural_color_count, + destination_by_owner_ordinal, + )?; + match outcome { + OnTheFlySelectedQueryOutcomeV1::Trace(selected) => { + if selected.query != expected_query { + return Err(RusticolError::integrity( + "on-the-fly contracted trace query changed during construction", + )); + } + projection_slots.push(projection_index); + traces.push(selected); + } + OnTheFlySelectedQueryOutcomeV1::StructuralZero { query: selected } => { + if selected != expected_query { + return Err(RusticolError::integrity( + "on-the-fly contracted structural-zero query changed during construction", + )); + } + } + } + } + if !traces.is_empty() { + if !binding_started { + executor + .resolver_mut() + .begin_on_the_fly_family_binding(templates, direct_catalog)?; + binding_started = true; + } + executor.resolver_mut().extend_on_the_fly_family_binding( + templates, + direct_catalog, + seed, + traces.iter_mut().map(|selected| &mut selected.trace), + )?; + let inputs = traces + .iter() + .map(|selected| QueryFamilyTraceInput { + trace: &selected.trace, + projection: selected.projection, + }) + .collect::>(); + let destinations = consumer.push_chunk(&inputs)?; + if destinations.len() != projection_slots.len() { + return Err(RusticolError::integrity( + "on-the-fly streamed destination range changed a chunk count", + )); + } + for (projection_index, destination) in + projection_slots.into_iter().zip(destinations) + { + let slot = amplitude_destinations + .get_mut(projection_index) + .ok_or_else(|| { + RusticolError::integrity( + "on-the-fly contracted projection slot is absent", + ) + })?; + if slot.replace(destination as usize).is_some() { + return Err(RusticolError::integrity( + "on-the-fly contracted projection slot is repeated", + )); + } + } + } + processed_query_count = chunk_end; + emit_progress( + progress.as_deref_mut(), + NativeOnTheFlyWarmUpEventKind::Update, + NativeOnTheFlyWarmUpStage::QueryFamily, + processed_query_count, + query_count, + None, + )?; + } + Ok(()) + })?; + #[cfg(test)] + { + self.contracted_max_live_query_outcomes = max_live_query_outcomes; + } + if processed_query_count != query_count { + return Err(RusticolError::integrity( + "on-the-fly contracted streamed construction omitted a structural query", + )); + } + emit_progress( + progress.as_deref_mut(), + NativeOnTheFlyWarmUpEventKind::End, + NativeOnTheFlyWarmUpStage::QueryFamily, + query_count, + query_count, + None, + )?; + emit_progress( + progress.as_deref_mut(), + NativeOnTheFlyWarmUpEventKind::Start, + NativeOnTheFlyWarmUpStage::FamilyFinalization, + 0, + 1, + None, + )?; + let (cache_hit, census) = if let Some(streamed) = streamed { + if !binding_started { + return Err(RusticolError::integrity( + "on-the-fly streamed family has no semantic binding transaction", + )); + } + let cache_hit = + executor.prepare_streamed_candidate(streamed, logical_point_capacity)?; + let census = executor.prepared_census().ok_or_else(|| { + RusticolError::integrity("on-the-fly contracted family has no work census") + })?; + let projected_nonzero_count = amplitude_destinations + .iter() + .filter(|destination| destination.is_some()) + .count(); + if usize::try_from(census.union_amplitude_destination_count).ok() + != Some(projected_nonzero_count) + { + return Err(RusticolError::integrity( + "on-the-fly contracted family does not retain one destination per nonzero query", + )); + } + (cache_hit, Some(census)) + } else { + if binding_started { + return Err(RusticolError::integrity( + "on-the-fly all-zero streamed family retained semantic bindings", + )); + } + (false, None) + }; + emit_progress( + progress.as_deref_mut(), + NativeOnTheFlyWarmUpEventKind::End, + NativeOnTheFlyWarmUpStage::FamilyFinalization, + 1, + 1, + None, + )?; + Ok(( + cache_hit, + PreparedOnTheFlyContractedFamilyV1 { + helicity_ordinals: helicity_ordinals.to_vec().into_boxed_slice(), + structural_color_count, + amplitude_destinations: amplitude_destinations.into_boxed_slice(), + executor_handle: None, + census, + logical_point_capacity, + }, + )) + })(); + let (cache_hit, candidate) = match candidate_result { + Ok(candidate) => candidate, + Err(error) => { + self.discard_pending_contracted_queries()?; + return Err(error); + } + }; + self.pending_contracted_family = Some(candidate); + Ok(cache_hit) + } + + /// Re-activate the family already selected by the runtime wrapper without + /// rebuilding its public selector requests. The common warmed call has + /// sufficient point capacity and therefore performs no allocation or + /// identity walk. Capacity growth remains explicit and fallible. + pub(super) fn reuse_current_lc_queries( + &mut self, + logical_point_capacity: u32, + ) -> RusticolResult { + if logical_point_capacity == 0 { + return Err(RusticolError::invalid_argument( + "on-the-fly LC preparation requires a nonzero point capacity", + )); + } + if let Some(family) = self.pending_family.as_mut() { + if logical_point_capacity <= family.logical_point_capacity { + return Ok(false); + } + let has_executable_family = family.census.is_some(); + if has_executable_family + && let Err(error) = self.executor.resize_active_family(logical_point_capacity) + { + self.discard_pending_lc_queries()?; + return Err(error); + } + self.pending_family + .as_mut() + .expect("pending on-the-fly family disappeared after resize") + .logical_point_capacity = logical_point_capacity; + return Ok(false); + } + let index = self.last_family.ok_or_else(|| { + RusticolError::internal( + "on-the-fly selector cache has no corresponding prepared family", + ) + })?; + let family = self.families.get_mut(index).ok_or_else(|| { + RusticolError::internal( + "on-the-fly selector cache references an absent prepared family", + ) + })?; + if logical_point_capacity <= family.logical_point_capacity { + return Ok(true); + } + let cache_hit = if let Some(handle) = family.executor_handle { + self.executor + .activate_retained_family(handle, logical_point_capacity)? + } else { + true + }; + family.logical_point_capacity = logical_point_capacity; + Ok(cache_hit) + } + + pub(super) fn reuse_current_contracted_queries( + &mut self, + logical_point_capacity: u32, + ) -> RusticolResult { + if logical_point_capacity == 0 { + return Err(RusticolError::invalid_argument( + "on-the-fly contracted preparation requires a nonzero point capacity", + )); + } + if let Some(family) = self.pending_contracted_family.as_mut() { + if logical_point_capacity <= family.logical_point_capacity { + return Ok(false); + } + if family.census.is_some() + && let Err(error) = self.executor.resize_active_family(logical_point_capacity) + { + self.discard_pending_contracted_queries()?; + return Err(error); + } + self.pending_contracted_family + .as_mut() + .expect("pending contracted family disappeared after resize") + .logical_point_capacity = logical_point_capacity; + return Ok(false); + } + let family = self.contracted_family.as_mut().ok_or_else(|| { + RusticolError::internal("on-the-fly contracted selector cache has no prepared family") + })?; + if logical_point_capacity <= family.logical_point_capacity { + return Ok(true); + } + let cache_hit = if let Some(handle) = family.executor_handle { + self.executor + .activate_retained_family(handle, logical_point_capacity)? + } else { + true + }; + family.logical_point_capacity = logical_point_capacity; + Ok(cache_hit) + } + + pub(super) fn prepared_census(&self) -> Option { + self.current_family() + .and_then(|family| family.census) + .or_else(|| { + self.current_contracted_family() + .and_then(|family| family.census) + }) + } + + #[cfg(test)] + pub(super) fn retained_family_count(&self) -> usize { + self.families.len() + usize::from(self.contracted_family.is_some()) + } + + pub(super) fn pending_family_count(&self) -> usize { + usize::from(self.pending_family.is_some()) + + usize::from(self.pending_contracted_family.is_some()) + } + + pub(super) fn semantic_executor_binding_count(&self) -> RusticolResult { + self.executor.resolver().semantic_executor_binding_count() + } + + /// Count retained production state without constructing or mutating it. + pub(super) fn retained_state_census(&self) -> OnTheFlyRetainedStateCensusV1 { + let mut census = OnTheFlyRetainedStateCensusV1::default(); + for family in &self.families { + census.family_count += 1; + census.request_count += family.requests.len(); + census.executor_handle_count += usize::from(family.executor_handle.is_some()); + for destination in &family.amplitude_destinations { + if destination.is_some() { + census.amplitude_destination_count += 1; + } + } + } + if let Some(family) = &self.contracted_family { + census.family_count += 1; + census.request_count += family.amplitude_destinations.len(); + census.executor_handle_count += usize::from(family.executor_handle.is_some()); + census.amplitude_destination_count += family + .amplitude_destinations + .iter() + .filter(|destination| destination.is_some()) + .count(); + } + // Query-local traces and embedded lookup keys are deliberately absent + // from the compact retained family type. Their zero counters keep that + // invariant observable without exposing private family fields. + census + } + + pub(super) fn clear(&mut self) -> RusticolResult<()> { + self.executor.clear_families()?; + self.executor.resolver_mut().clear_resolved_bindings(); + self.pending_family = None; + self.last_family = None; + self.families.clear(); + self.pending_contracted_family = None; + self.contracted_family = None; + self.prepared_grammar = None; + self.coupling_policy = None; + self.coupling_policy_resolution = None; + self.process_preparation_count = 0; + #[cfg(test)] + { + self.process_preparation_census = OnTheFlyProcessPreparationCensusV1::default(); + self.contracted_max_live_query_outcomes = 0; + } + self.source_momenta_scratch = Vec::new(); + self.amplitude_scratch = Vec::new(); + Ok(()) + } + + fn current_family(&self) -> Option<&PreparedOnTheFlyLcFamilyV1> { + self.pending_family + .as_ref() + .or_else(|| self.last_family.and_then(|index| self.families.get(index))) + } + + fn current_contracted_family(&self) -> Option<&PreparedOnTheFlyContractedFamilyV1> { + self.pending_contracted_family + .as_ref() + .or(self.contracted_family.as_ref()) + } + + pub(super) fn discard_pending_lc_queries(&mut self) -> RusticolResult<()> { + self.executor.discard_pending_family()?; + self.executor + .resolver_mut() + .discard_pending_resolved_bindings(); + self.pending_family = None; + Ok(()) + } + + pub(super) fn discard_pending_contracted_queries(&mut self) -> RusticolResult<()> { + self.executor.discard_pending_family()?; + self.executor + .resolver_mut() + .discard_pending_resolved_bindings(); + self.pending_contracted_family = None; + Ok(()) + } + + /// Roll back a contracted selection whose executor family may already + /// have committed on an earlier tile. The wrapper deliberately clears + /// its public selection cache in the same error path, so both halves of + /// the last-family-only identity remain coherent. + pub(super) fn abort_contracted_selection(&mut self) -> RusticolResult<()> { + self.executor.clear_families()?; + self.executor.resolver_mut().clear_resolved_bindings(); + self.pending_contracted_family = None; + self.contracted_family = None; + Ok(()) + } + + #[cfg(test)] + pub(super) fn fail_contracted_execution_at_for_test(&mut self, attempt: Option) { + self.contracted_test_execution_attempt = 0; + self.contracted_test_fail_execution_at = attempt; + } + + #[cfg(test)] + pub(super) const fn contracted_max_live_query_outcomes_for_test(&self) -> usize { + self.contracted_max_live_query_outcomes + } + + fn promote_pending_family(&mut self) -> RusticolResult<()> { + let Some(family) = self.pending_family.as_ref() else { + return Ok(()); + }; + let executor_handle = if family.census.is_some() { + let handle = self.executor.active_retained_handle().ok_or_else(|| { + RusticolError::internal( + "successful on-the-fly family has no retained executor handle", + ) + })?; + self.executor + .resolver_mut() + .commit_pending_resolved_bindings()?; + Some(handle) + } else { + // A structural-zero family replaces executable state only after + // its evaluation succeeds. Row-table owners disappear before the + // semantic bindings to their prepared contexts. + self.executor.clear_families()?; + self.executor.resolver_mut().clear_resolved_bindings(); + None + }; + let mut family = self + .pending_family + .take() + .expect("validated on-the-fly pending family disappeared during promotion"); + family.executor_handle = executor_handle; + self.retain_family(family); + Ok(()) + } + + fn retain_family(&mut self, family: PreparedOnTheFlyLcFamilyV1) { + self.families.clear(); + self.families.push(family); + self.last_family = Some(0); + } + + fn promote_pending_contracted_family(&mut self) -> RusticolResult<()> { + let Some(family) = self.pending_contracted_family.as_ref() else { + return Ok(()); + }; + let executor_handle = if family.census.is_some() { + let handle = self.executor.active_retained_handle().ok_or_else(|| { + RusticolError::internal( + "successful on-the-fly contracted family has no retained executor handle", + ) + })?; + self.executor + .resolver_mut() + .commit_pending_resolved_bindings()?; + Some(handle) + } else { + self.executor.clear_families()?; + self.executor.resolver_mut().clear_resolved_bindings(); + None + }; + let mut family = self + .pending_contracted_family + .take() + .expect("validated on-the-fly contracted pending family disappeared during promotion"); + family.executor_handle = executor_handle; + self.contracted_family = Some(family); + Ok(()) + } + + fn prepare_runtime_parameters(&mut self, runtime_values: &[f64]) -> RusticolResult<()> { + if runtime_values.iter().any(|value| !value.is_finite()) { + return Err(RusticolError::invalid_argument( + "on-the-fly runtime parameter value is not finite", + )); + } + if self.runtime_parameter_values.as_ref() == runtime_values { + return Ok(()); + } + let prepared = projected_prepared_parameter_values( + &self.parameter_defaults, + &self.parameter_projection, + runtime_values, + )?; + self.executor.set_parameters(&prepared)?; + self.runtime_parameter_values = runtime_values.to_vec().into_boxed_slice(); + Ok(()) + } + + fn ensure_point_capacity(&mut self, point_count: u32) -> RusticolResult<()> { + let Some(prepared) = self.current_family() else { + return Err(RusticolError::invalid_argument( + "on-the-fly evaluation requires prepared LC queries", + )); + }; + if point_count <= prepared.logical_point_capacity { + return Ok(()); + } + self.reuse_current_lc_queries(point_count)?; + Ok(()) + } + + fn ensure_amplitude_scratch(&mut self, point_count: u32) -> RusticolResult { + let query_count = self + .current_family() + .ok_or_else(|| { + RusticolError::invalid_argument( + "on-the-fly evaluation requires prepared LC queries", + ) + })? + .amplitude_destinations + .iter() + .filter(|destination| destination.is_some()) + .count(); + let required = query_count + .checked_mul(point_count as usize) + .ok_or_else(|| { + RusticolError::invalid_argument("on-the-fly amplitude shape exceeds usize") + })?; + if self.amplitude_scratch.len() < required { + self.amplitude_scratch + .try_reserve_exact(required - self.amplitude_scratch.len()) + .map_err(|error| { + RusticolError::invalid_argument(format!( + "on-the-fly amplitude workspace allocation failed: {error}" + )) + })?; + self.amplitude_scratch.resize(required, (0.0, 0.0)); + } + Ok(required) + } + + fn execute_amplitudes( + &mut self, + point_major_momenta: &[f64], + point_count: u32, + runtime_parameters: &[f64], + ) -> RusticolResult<( + OnTheFlyQueryFamilyExecutionReportV1, + Duration, + Duration, + Duration, + )> { + let candidate_pending = self.pending_family.is_some(); + let result = + self.execute_amplitudes_inner(point_major_momenta, point_count, runtime_parameters); + if result.is_err() && candidate_pending { + self.discard_pending_lc_queries()?; + } + result + } + + fn execute_amplitudes_inner( + &mut self, + point_major_momenta: &[f64], + point_count: u32, + runtime_parameters: &[f64], + ) -> RusticolResult<( + OnTheFlyQueryFamilyExecutionReportV1, + Duration, + Duration, + Duration, + )> { + if point_count == 0 { + return Err(RusticolError::invalid_argument( + "on-the-fly evaluation requires at least one point", + )); + } + self.ensure_point_capacity(point_count)?; + + let parameter_started = Instant::now(); + self.prepare_runtime_parameters(runtime_parameters)?; + let parameter_setup = parameter_started.elapsed(); + + if self + .current_family() + .is_some_and(|family| family.census.is_none()) + { + let cache_hit = self.pending_family.is_none(); + self.promote_pending_family()?; + return Ok(( + OnTheFlyQueryFamilyExecutionReportV1 { + cache_hit, + ..OnTheFlyQueryFamilyExecutionReportV1::default() + }, + parameter_setup, + Duration::ZERO, + Duration::ZERO, + )); + } + + let input_started = Instant::now(); + let source_major_len = on_the_fly_source_major_momenta_into( + &self.seed, + point_major_momenta, + point_count, + 4, + &mut self.source_momenta_scratch, + )?; + let input_setup = input_started.elapsed(); + + let output_len = self.ensure_amplitude_scratch(point_count)?; + let execution_started = Instant::now(); + let report = self.executor.execute_into( + &self.source_momenta_scratch[..source_major_len], + point_count, + &mut self.amplitude_scratch[..output_len], + )?; + let execution = execution_started.elapsed(); + // A cold family becomes reusable only after its first successful + // prepared-kernel execution. + self.promote_pending_family()?; + Ok((report, parameter_setup, input_setup, execution)) + } + + fn execute_amplitudes_unprofiled( + &mut self, + point_major_momenta: &[f64], + point_count: u32, + runtime_parameters: &[f64], + ) -> RusticolResult<()> { + let candidate_pending = self.pending_family.is_some(); + let result = self.execute_amplitudes_unprofiled_inner( + point_major_momenta, + point_count, + runtime_parameters, + ); + if result.is_err() && candidate_pending { + self.discard_pending_lc_queries()?; + } + result + } + + fn execute_amplitudes_unprofiled_inner( + &mut self, + point_major_momenta: &[f64], + point_count: u32, + runtime_parameters: &[f64], + ) -> RusticolResult<()> { + if point_count == 0 { + return Err(RusticolError::invalid_argument( + "on-the-fly evaluation requires at least one point", + )); + } + self.ensure_point_capacity(point_count)?; + self.prepare_runtime_parameters(runtime_parameters)?; + + if self + .current_family() + .is_some_and(|family| family.census.is_none()) + { + self.promote_pending_family()?; + return Ok(()); + } + + let source_major_len = on_the_fly_source_major_momenta_into( + &self.seed, + point_major_momenta, + point_count, + 4, + &mut self.source_momenta_scratch, + )?; + let output_len = self.ensure_amplitude_scratch(point_count)?; + self.executor.execute_into_unprofiled( + &self.source_momenta_scratch[..source_major_len], + point_count, + &mut self.amplitude_scratch[..output_len], + )?; + // A cold family becomes reusable only after its first successful + // prepared-kernel execution. + self.promote_pending_family()?; + Ok(()) + } + + fn ensure_contracted_point_capacity(&mut self, point_count: u32) -> RusticolResult<()> { + let Some(prepared) = self.current_contracted_family() else { + return Err(RusticolError::invalid_argument( + "on-the-fly contracted evaluation requires prepared queries", + )); + }; + if point_count > prepared.logical_point_capacity { + self.reuse_current_contracted_queries(point_count)?; + } + Ok(()) + } + + fn ensure_contracted_amplitude_scratch(&mut self, point_count: u32) -> RusticolResult { + let query_count = self + .current_contracted_family() + .ok_or_else(|| { + RusticolError::invalid_argument( + "on-the-fly contracted evaluation requires prepared queries", + ) + })? + .amplitude_destinations + .iter() + .filter(|destination| destination.is_some()) + .count(); + let required = query_count + .checked_mul(point_count as usize) + .ok_or_else(|| { + RusticolError::invalid_argument( + "on-the-fly contracted amplitude shape exceeds usize", + ) + })?; + if self.amplitude_scratch.len() < required { + self.amplitude_scratch + .try_reserve_exact(required - self.amplitude_scratch.len()) + .map_err(|error| { + RusticolError::invalid_argument(format!( + "on-the-fly contracted amplitude workspace allocation failed: {error}" + )) + })?; + self.amplitude_scratch.resize(required, (0.0, 0.0)); + } + Ok(required) + } + + fn execute_contracted_amplitudes( + &mut self, + point_major_momenta: &[f64], + point_count: u32, + runtime_parameters: &[f64], + ) -> RusticolResult<( + OnTheFlyQueryFamilyExecutionReportV1, + Duration, + Duration, + Duration, + )> { + #[cfg(test)] + { + let attempt = self.contracted_test_execution_attempt; + self.contracted_test_execution_attempt = self + .contracted_test_execution_attempt + .checked_add(1) + .expect("contracted test execution-attempt count exceeds usize"); + if self.contracted_test_fail_execution_at == Some(attempt) { + return Err(RusticolError::internal( + "injected on-the-fly contracted tile failure", + )); + } + } + let candidate_pending = self.pending_contracted_family.is_some(); + let result = self.execute_contracted_amplitudes_inner( + point_major_momenta, + point_count, + runtime_parameters, + ); + if result.is_err() && candidate_pending { + self.discard_pending_contracted_queries()?; + } + result + } + + fn execute_contracted_amplitudes_inner( + &mut self, + point_major_momenta: &[f64], + point_count: u32, + runtime_parameters: &[f64], + ) -> RusticolResult<( + OnTheFlyQueryFamilyExecutionReportV1, + Duration, + Duration, + Duration, + )> { + if point_count == 0 { + return Err(RusticolError::invalid_argument( + "on-the-fly contracted evaluation requires at least one point", + )); + } + self.ensure_contracted_point_capacity(point_count)?; + + let parameter_started = Instant::now(); + self.prepare_runtime_parameters(runtime_parameters)?; + let parameter_setup = parameter_started.elapsed(); + if self + .current_contracted_family() + .is_some_and(|family| family.census.is_none()) + { + let cache_hit = self.pending_contracted_family.is_none(); + self.promote_pending_contracted_family()?; + return Ok(( + OnTheFlyQueryFamilyExecutionReportV1 { + cache_hit, + ..OnTheFlyQueryFamilyExecutionReportV1::default() + }, + parameter_setup, + Duration::ZERO, + Duration::ZERO, + )); + } + + let input_started = Instant::now(); + let source_major_len = on_the_fly_source_major_momenta_into( + &self.seed, + point_major_momenta, + point_count, + 4, + &mut self.source_momenta_scratch, + )?; + let input_setup = input_started.elapsed(); + let output_len = self.ensure_contracted_amplitude_scratch(point_count)?; + let execution_started = Instant::now(); + let report = self.executor.execute_into( + &self.source_momenta_scratch[..source_major_len], + point_count, + &mut self.amplitude_scratch[..output_len], + )?; + let execution = execution_started.elapsed(); + self.promote_pending_contracted_family()?; + Ok((report, parameter_setup, input_setup, execution)) + } + + fn execute_contracted_amplitudes_unprofiled( + &mut self, + point_major_momenta: &[f64], + point_count: u32, + runtime_parameters: &[f64], + ) -> RusticolResult<()> { + #[cfg(test)] + { + let attempt = self.contracted_test_execution_attempt; + self.contracted_test_execution_attempt = self + .contracted_test_execution_attempt + .checked_add(1) + .expect("contracted test execution-attempt count exceeds usize"); + if self.contracted_test_fail_execution_at == Some(attempt) { + return Err(RusticolError::internal( + "injected on-the-fly contracted tile failure", + )); + } + } + let candidate_pending = self.pending_contracted_family.is_some(); + let result = self.execute_contracted_amplitudes_unprofiled_inner( + point_major_momenta, + point_count, + runtime_parameters, + ); + if result.is_err() && candidate_pending { + self.discard_pending_contracted_queries()?; + } + result + } + + fn execute_contracted_amplitudes_unprofiled_inner( + &mut self, + point_major_momenta: &[f64], + point_count: u32, + runtime_parameters: &[f64], + ) -> RusticolResult<()> { + if point_count == 0 { + return Err(RusticolError::invalid_argument( + "on-the-fly contracted evaluation requires at least one point", + )); + } + self.ensure_contracted_point_capacity(point_count)?; + self.prepare_runtime_parameters(runtime_parameters)?; + if self + .current_contracted_family() + .is_some_and(|family| family.census.is_none()) + { + self.promote_pending_contracted_family()?; + return Ok(()); + } + + let source_major_len = on_the_fly_source_major_momenta_into( + &self.seed, + point_major_momenta, + point_count, + 4, + &mut self.source_momenta_scratch, + )?; + let output_len = self.ensure_contracted_amplitude_scratch(point_count)?; + self.executor.execute_into_unprofiled( + &self.source_momenta_scratch[..source_major_len], + point_count, + &mut self.amplitude_scratch[..output_len], + )?; + self.promote_pending_contracted_family()?; + Ok(()) + } + + #[allow(clippy::too_many_arguments)] + fn reduce_current_contracted_color( + &mut self, + contraction: &RecurrenceColorContraction, + point_count: usize, + helicity_count: usize, + normalization_factor: f64, + accumulate: impl FnMut(usize, usize, f64), + ) -> RusticolResult<()> { + let Self { + contracted_family, + pending_contracted_family, + amplitude_scratch, + symmetric_group_color_workspace, + .. + } = self; + let family = pending_contracted_family + .as_ref() + .or(contracted_family.as_ref()) + .ok_or_else(|| { + RusticolError::internal( + "successful on-the-fly contracted execution lost its prepared family", + ) + })?; + reduce_contracted_color( + contraction, + symmetric_group_color_workspace.as_mut(), + &family.amplitude_destinations, + amplitude_scratch, + point_count, + helicity_count, + family.structural_color_count, + normalization_factor, + accumulate, + ) + } + + /// Evaluate a contracted NLC/full color metric without constructing a + /// diagnostic report or reading phase clocks. + #[allow(clippy::too_many_arguments)] + pub(super) fn run_contracted_f64_into_unprofiled( + &mut self, + point_major_momenta: &[f64], + point_count: u32, + runtime_parameters: &[f64], + normalization_factor: f64, + contraction: &RecurrenceColorContraction, + point_tile_size: usize, + output: &mut [f64], + ) -> RusticolResult<()> { + validate_contracted_run_shape( + point_major_momenta, + point_count, + normalization_factor, + contraction, + point_tile_size, + output.len(), + point_count as usize, + )?; + output.fill(0.0); + let point_stride = point_major_momenta.len() / point_count as usize; + let helicity_count = self + .current_contracted_family() + .ok_or_else(|| { + RusticolError::invalid_argument( + "on-the-fly contracted total requires prepared queries", + ) + })? + .helicity_ordinals + .len(); + for point_start in (0..point_count as usize).step_by(point_tile_size) { + let tile_count = point_tile_size.min(point_count as usize - point_start); + let input_start = point_start * point_stride; + let input_end = input_start + tile_count * point_stride; + self.execute_contracted_amplitudes_unprofiled( + &point_major_momenta[input_start..input_end], + u32::try_from(tile_count).map_err(|_| { + RusticolError::invalid_argument("on-the-fly contracted tile count exceeds u32") + })?, + runtime_parameters, + )?; + self.reduce_current_contracted_color( + contraction, + tile_count, + helicity_count, + normalization_factor, + |point, _helicity, value| output[point_start + point] += value, + )?; + } + Ok(()) + } + + /// Evaluate a contracted NLC/full color metric into one total per point. + /// The manifest tile size bounds both the prepared numeric family and the + /// H x S complex-amplitude scratch. + #[allow(clippy::too_many_arguments)] + pub(super) fn run_contracted_f64_into( + &mut self, + point_major_momenta: &[f64], + point_count: u32, + runtime_parameters: &[f64], + normalization_factor: f64, + contraction: &RecurrenceColorContraction, + point_tile_size: usize, + output: &mut [f64], + ) -> RusticolResult<(OnTheFlyQueryFamilyExecutionReportV1, RuntimeProfile)> { + validate_contracted_run_shape( + point_major_momenta, + point_count, + normalization_factor, + contraction, + point_tile_size, + output.len(), + point_count as usize, + )?; + output.fill(0.0); + let total_started = Instant::now(); + let mut aggregate_profile = RuntimeProfile::default(); + let mut aggregate_report = None; + let point_stride = point_major_momenta.len() / point_count as usize; + let helicity_count = self + .current_contracted_family() + .ok_or_else(|| { + RusticolError::invalid_argument( + "on-the-fly contracted total requires prepared queries", + ) + })? + .helicity_ordinals + .len(); + for point_start in (0..point_count as usize).step_by(point_tile_size) { + let tile_count = point_tile_size.min(point_count as usize - point_start); + let input_start = point_start.checked_mul(point_stride).ok_or_else(|| { + RusticolError::invalid_argument("on-the-fly contracted tile offset exceeds usize") + })?; + let input_end = input_start + .checked_add(tile_count.checked_mul(point_stride).ok_or_else(|| { + RusticolError::invalid_argument( + "on-the-fly contracted tile shape exceeds usize", + ) + })?) + .ok_or_else(|| { + RusticolError::invalid_argument( + "on-the-fly contracted tile range exceeds usize", + ) + })?; + let tile_started = Instant::now(); + let (report, parameter_setup, input_setup, execution) = self + .execute_contracted_amplitudes( + &point_major_momenta[input_start..input_end], + u32::try_from(tile_count).map_err(|_| { + RusticolError::invalid_argument( + "on-the-fly contracted tile count exceeds u32", + ) + })?, + runtime_parameters, + )?; + let reduction_started = Instant::now(); + self.reduce_current_contracted_color( + contraction, + tile_count, + helicity_count, + normalization_factor, + |point, _helicity, value| output[point_start + point] += value, + )?; + let reduction = reduction_started.elapsed(); + let tile_profile = profile_from_report( + tile_started.elapsed(), + parameter_setup, + input_setup, + execution, + reduction, + input_end - input_start, + u32::try_from(tile_count).expect("validated tile count"), + report, + ); + aggregate_profile.add_sector(&tile_profile); + merge_execution_report(&mut aggregate_report, report)?; + } + aggregate_profile.total_s = profile_duration_seconds(total_started.elapsed()); + Ok((aggregate_report.unwrap_or_default(), aggregate_profile)) + } + + /// Evaluate contracted values in `[point][selected_helicity][1]` order. + #[allow(clippy::too_many_arguments)] + pub(super) fn run_contracted_resolved_f64_into( + &mut self, + point_major_momenta: &[f64], + point_count: u32, + runtime_parameters: &[f64], + normalization_factor: f64, + contraction: &RecurrenceColorContraction, + point_tile_size: usize, + helicity_count: usize, + output: &mut [f64], + ) -> RusticolResult<(OnTheFlyQueryFamilyExecutionReportV1, RuntimeProfile)> { + let expected_output = (point_count as usize) + .checked_mul(helicity_count) + .ok_or_else(|| { + RusticolError::invalid_argument( + "on-the-fly contracted resolved shape exceeds usize", + ) + })?; + validate_contracted_run_shape( + point_major_momenta, + point_count, + normalization_factor, + contraction, + point_tile_size, + output.len(), + expected_output, + )?; + let prepared_helicity_count = self + .current_contracted_family() + .ok_or_else(|| { + RusticolError::invalid_argument( + "on-the-fly contracted resolved evaluation requires prepared queries", + ) + })? + .helicity_ordinals + .len(); + if helicity_count != prepared_helicity_count { + return Err(RusticolError::integrity( + "on-the-fly contracted resolved helicity shape disagrees with its prepared family", + )); + } + output.fill(0.0); + let total_started = Instant::now(); + let mut aggregate_profile = RuntimeProfile::default(); + let mut aggregate_report = None; + let point_stride = point_major_momenta.len() / point_count as usize; + for point_start in (0..point_count as usize).step_by(point_tile_size) { + let tile_count = point_tile_size.min(point_count as usize - point_start); + let input_start = point_start * point_stride; + let input_end = input_start + tile_count * point_stride; + let tile_started = Instant::now(); + let (report, parameter_setup, input_setup, execution) = self + .execute_contracted_amplitudes( + &point_major_momenta[input_start..input_end], + u32::try_from(tile_count).map_err(|_| { + RusticolError::invalid_argument( + "on-the-fly contracted tile count exceeds u32", + ) + })?, + runtime_parameters, + )?; + let reduction_started = Instant::now(); + self.reduce_current_contracted_color( + contraction, + tile_count, + helicity_count, + normalization_factor, + |point, helicity, value| { + let destination = (point_start + point) * helicity_count + helicity; + output[destination] += value; + }, + )?; + let reduction = reduction_started.elapsed(); + let tile_profile = profile_from_report( + tile_started.elapsed(), + parameter_setup, + input_setup, + execution, + reduction, + input_end - input_start, + u32::try_from(tile_count).expect("validated tile count"), + report, + ); + aggregate_profile.add_sector(&tile_profile); + merge_execution_report(&mut aggregate_report, report)?; + } + aggregate_profile.total_s = profile_duration_seconds(total_started.elapsed()); + Ok((aggregate_report.unwrap_or_default(), aggregate_profile)) + } + + /// Evaluate total LC matrix elements without constructing a diagnostic + /// report or reading phase clocks. + pub(super) fn run_f64_into_unprofiled( + &mut self, + point_major_momenta: &[f64], + point_count: u32, + runtime_parameters: &[f64], + normalization_factor: f64, + output: &mut [f64], + ) -> RusticolResult<()> { + if output.len() != point_count as usize + || !normalization_factor.is_finite() + || normalization_factor < 0.0 + { + return Err(RusticolError::invalid_argument( + "on-the-fly total output shape or normalization is invalid", + )); + } + self.execute_amplitudes_unprofiled(point_major_momenta, point_count, runtime_parameters)?; + output.fill(0.0); + let family = self + .current_family() + .expect("successful on-the-fly execution lost its prepared family"); + reduce_total_into( + &family.requests, + &family.amplitude_destinations, + &self.amplitude_scratch, + point_count as usize, + normalization_factor, + output, + )?; + Ok(()) + } + + /// Evaluate total LC matrix elements into the same one-value-per-point + /// shape consumed by the existing `Runtime.evaluate_into` path. + pub(super) fn run_f64_into( + &mut self, + point_major_momenta: &[f64], + point_count: u32, + runtime_parameters: &[f64], + normalization_factor: f64, + output: &mut [f64], + ) -> RusticolResult<(OnTheFlyQueryFamilyExecutionReportV1, RuntimeProfile)> { + if output.len() != point_count as usize + || !normalization_factor.is_finite() + || normalization_factor < 0.0 + { + return Err(RusticolError::invalid_argument( + "on-the-fly total output shape or normalization is invalid", + )); + } + let total_started = Instant::now(); + let (report, parameter_setup, input_setup, execution) = + self.execute_amplitudes(point_major_momenta, point_count, runtime_parameters)?; + output.fill(0.0); + let reduction_started = Instant::now(); + let family = self + .current_family() + .expect("successful on-the-fly execution lost its prepared family"); + reduce_total_into( + &family.requests, + &family.amplitude_destinations, + &self.amplitude_scratch, + point_count as usize, + normalization_factor, + output, + )?; + let reduction = reduction_started.elapsed(); + let profile = profile_from_report( + total_started.elapsed(), + parameter_setup, + input_setup, + execution, + reduction, + point_major_momenta.len(), + point_count, + report, + ); + Ok((report, profile)) + } + + /// Evaluate resolved LC values in the established + /// `[point][helicity][color]` order without creating a dense flow table. + #[allow(clippy::too_many_arguments)] + pub(super) fn run_resolved_f64_into( + &mut self, + point_major_momenta: &[f64], + point_count: u32, + runtime_parameters: &[f64], + normalization_factor: f64, + helicity_count: usize, + color_count: usize, + output: &mut [f64], + ) -> RusticolResult<(OnTheFlyQueryFamilyExecutionReportV1, RuntimeProfile)> { + if !normalization_factor.is_finite() || normalization_factor < 0.0 { + return Err(RusticolError::invalid_argument( + "on-the-fly resolved normalization is invalid", + )); + } + let layout = + LcResolvedOutputLayout::new(point_count as usize, helicity_count, color_count)?; + if output.len() != layout.output_len()? { + return Err(RusticolError::invalid_argument( + "on-the-fly resolved output has the wrong size", + )); + } + let requests = self.current_family().ok_or_else(|| { + RusticolError::invalid_argument( + "on-the-fly resolved evaluation requires prepared LC queries", + ) + })?; + for target in requests + .requests + .iter() + .flat_map(|request| request.reduction_targets.iter()) + { + if target.helicity_position >= helicity_count || target.color_position >= color_count { + return Err(RusticolError::integrity( + "on-the-fly resolved target is outside the selected public axes", + )); + } + } + + let total_started = Instant::now(); + let (report, parameter_setup, input_setup, execution) = + self.execute_amplitudes(point_major_momenta, point_count, runtime_parameters)?; + output.fill(0.0); + let reduction_started = Instant::now(); + let family = self + .current_family() + .expect("successful on-the-fly execution lost its prepared family"); + reduce_resolved_into( + &family.requests, + &family.amplitude_destinations, + &self.amplitude_scratch, + point_count as usize, + normalization_factor, + layout, + output, + )?; + let reduction = reduction_started.elapsed(); + let profile = profile_from_report( + total_started.elapsed(), + parameter_setup, + input_setup, + execution, + reduction, + point_major_momenta.len(), + point_count, + report, + ); + Ok((report, profile)) + } +} + +#[allow(clippy::too_many_arguments)] +fn validate_contracted_run_shape( + point_major_momenta: &[f64], + point_count: u32, + normalization_factor: f64, + contraction: &RecurrenceColorContraction, + point_tile_size: usize, + actual_output_len: usize, + expected_output_len: usize, +) -> RusticolResult<()> { + if point_count == 0 + || point_tile_size == 0 + || !point_major_momenta + .len() + .is_multiple_of(point_count as usize) + || !normalization_factor.is_finite() + || normalization_factor < 0.0 + || actual_output_len != expected_output_len + { + return Err(RusticolError::invalid_argument( + "on-the-fly contracted input, output, tile, or normalization shape is invalid", + )); + } + let supported_reducer = matches!( + contraction.runtime_reducer(), + None | Some(RuntimeColorContractionReducer::SymmetricGroupFourier(_)) + ); + if contraction.component_count() != 1 + || contraction.destination_count() == 0 + || !supported_reducer + { + return Err(RusticolError::integrity( + "on-the-fly contracted execution requires a supported one-component metric", + )); + } + Ok(()) +} + +/// Apply A-dagger C A independently for each selected physical helicity. +/// Structural-zero amplitudes are represented by absent projection entries +/// and therefore make every touching bilinear term exactly zero. +#[allow(clippy::too_many_arguments)] +fn reduce_contracted_color( + contraction: &RecurrenceColorContraction, + symmetric_group_workspace: Option<&mut RuntimeSymmetricGroupColorWorkspace>, + amplitude_destinations: &[Option], + amplitudes: &[(f64, f64)], + point_count: usize, + helicity_count: usize, + structural_color_count: usize, + normalization_factor: f64, + accumulate: impl FnMut(usize, usize, f64), +) -> RusticolResult<()> { + let projection_len = helicity_count + .checked_mul(structural_color_count) + .ok_or_else(|| { + RusticolError::invalid_argument("on-the-fly contracted projection shape exceeds usize") + })?; + if amplitude_destinations.len() != projection_len { + return Err(RusticolError::integrity( + "on-the-fly contracted amplitude projection has the wrong shape", + )); + } + match contraction.runtime_reducer() { + Some(RuntimeColorContractionReducer::SymmetricGroupFourier(reducer)) => { + let workspace = symmetric_group_workspace.ok_or_else(|| { + RusticolError::integrity("on-the-fly symmetric-group color workspace is absent") + })?; + return reduce_symmetric_group_contracted_color( + contraction, + reducer, + workspace, + amplitude_destinations, + amplitudes, + point_count, + helicity_count, + structural_color_count, + normalization_factor, + accumulate, + ); + } + Some(RuntimeColorContractionReducer::Walsh(_)) => { + return Err(RusticolError::integrity( + "on-the-fly contracted execution does not support Walsh color storage", + )); + } + None => {} + } + reduce_direct_contracted_color( + contraction.runtime_entries(), + amplitude_destinations, + amplitudes, + point_count, + helicity_count, + structural_color_count, + normalization_factor, + accumulate, + ) +} + +#[allow(clippy::too_many_arguments)] +fn reduce_direct_contracted_color( + entries: impl IntoIterator, + amplitude_destinations: &[Option], + amplitudes: &[(f64, f64)], + point_count: usize, + helicity_count: usize, + structural_color_count: usize, + normalization_factor: f64, + mut accumulate: impl FnMut(usize, usize, f64), +) -> RusticolResult<()> { + if amplitude_destinations.len() + != helicity_count + .checked_mul(structural_color_count) + .ok_or_else(|| { + RusticolError::invalid_argument( + "on-the-fly direct contracted projection shape exceeds usize", + ) + })? + { + return Err(RusticolError::integrity( + "on-the-fly direct contracted amplitude projection has the wrong shape", + )); + } + for entry in entries { + let left_color = usize::try_from(entry.left_destination_id).map_err(|_| { + RusticolError::artifact("on-the-fly contracted left destination exceeds usize") + })?; + let right_color = usize::try_from(entry.right_destination_id).map_err(|_| { + RusticolError::artifact("on-the-fly contracted right destination exceeds usize") + })?; + if left_color >= structural_color_count || right_color >= structural_color_count { + return Err(RusticolError::integrity( + "on-the-fly contracted metric destination is outside its structural basis", + )); + } + for helicity in 0..helicity_count { + let base = helicity * structural_color_count; + let (Some(left_destination), Some(right_destination)) = ( + amplitude_destinations[base + left_color], + amplitude_destinations[base + right_color], + ) else { + continue; + }; + let left_base = left_destination.checked_mul(point_count).ok_or_else(|| { + RusticolError::invalid_argument( + "on-the-fly contracted left amplitude offset exceeds usize", + ) + })?; + let right_base = right_destination.checked_mul(point_count).ok_or_else(|| { + RusticolError::invalid_argument( + "on-the-fly contracted right amplitude offset exceeds usize", + ) + })?; + if left_base + .checked_add(point_count) + .is_none_or(|end| end > amplitudes.len()) + || right_base + .checked_add(point_count) + .is_none_or(|end| end > amplitudes.len()) + { + return Err(RusticolError::integrity( + "on-the-fly contracted amplitude destination is absent", + )); + } + for point in 0..point_count { + let (left_re, left_im) = amplitudes[left_base + point]; + let (right_re, right_im) = amplitudes[right_base + point]; + let value = normalization_factor + * entry.contract_real_bilinear(left_re, left_im, right_re, right_im); + accumulate(point, helicity, value); + } + } + } + Ok(()) +} + +#[allow(clippy::too_many_arguments)] +fn reduce_symmetric_group_contracted_color( + contraction: &RecurrenceColorContraction, + reducer: &crate::recurrence::RuntimeSymmetricGroupColorContraction, + workspace: &mut RuntimeSymmetricGroupColorWorkspace, + amplitude_destinations: &[Option], + amplitudes: &[(f64, f64)], + point_count: usize, + helicity_count: usize, + structural_color_count: usize, + normalization_factor: f64, + mut accumulate: impl FnMut(usize, usize, f64), +) -> RusticolResult<()> { + if reducer.local_group_count() != contraction.local_group_count() as usize + || contraction.destination_by_group().len() != structural_color_count + || reducer.local_group_count() != structural_color_count + { + return Err(RusticolError::integrity( + "on-the-fly symmetric-group reducer disagrees with its structural owner basis", + )); + } + for helicity in 0..helicity_count { + let projection_base = helicity * structural_color_count; + reducer.reduce_lanes(workspace, point_count, |local_group, point| { + // Contracted query preparation already inverted the authenticated + // owner projection and retained this table in normalized group + // order. Applying destination_by_group here a second time would + // silently contract A(P^2 g) for a non-involutive permutation. + let Some(amplitude_destination) = amplitude_destinations[projection_base + local_group] + else { + return Ok((0.0, 0.0)); + }; + let amplitude_index = amplitude_destination + .checked_mul(point_count) + .and_then(|base| base.checked_add(point)) + .ok_or_else(|| { + RusticolError::invalid_argument( + "on-the-fly symmetric-group amplitude offset exceeds usize", + ) + })?; + amplitudes.get(amplitude_index).copied().ok_or_else(|| { + RusticolError::integrity( + "on-the-fly symmetric-group amplitude destination is absent", + ) + }) + })?; + for (point, value) in workspace.reduced(point_count)?.iter().copied().enumerate() { + accumulate(point, helicity, normalization_factor * value); + } + } + Ok(()) +} + +fn merge_execution_report( + aggregate: &mut Option, + report: OnTheFlyQueryFamilyExecutionReportV1, +) -> RusticolResult<()> { + let Some(target) = aggregate.as_mut() else { + *aggregate = Some(report); + return Ok(()); + }; + target.cache_hit &= report.cache_hit; + macro_rules! checked_add_report_field { + ($field:ident) => { + target.$field = target.$field.checked_add(report.$field).ok_or_else(|| { + RusticolError::invalid_argument(concat!( + "on-the-fly contracted execution report ", + stringify!($field), + " exceeds u32" + )) + })?; + }; + } + checked_add_report_field!(source_calls); + checked_add_report_field!(source_rows); + checked_add_report_field!(contribution_calls); + checked_add_report_field!(contribution_rows); + checked_add_report_field!(finalization_calls); + checked_add_report_field!(finalization_rows); + checked_add_report_field!(closure_calls); + checked_add_report_field!(closure_rows); + Ok(()) +} + +fn reduce_total_into( + requests: &[OnTheFlyLcQueryRequestV1], + amplitude_destinations: &[Option], + amplitudes: &[(f64, f64)], + point_count: usize, + normalization_factor: f64, + output: &mut [f64], +) -> RusticolResult<()> { + if amplitude_destinations.len() != requests.len() { + return Err(RusticolError::integrity( + "on-the-fly amplitude projection has the wrong request count", + )); + } + for (request, destination) in requests.iter().zip(amplitude_destinations) { + let Some(destination) = *destination else { + continue; + }; + let base = destination.checked_mul(point_count).ok_or_else(|| { + RusticolError::invalid_argument("on-the-fly amplitude destination offset exceeds usize") + })?; + let end = base.checked_add(point_count).ok_or_else(|| { + RusticolError::invalid_argument("on-the-fly amplitude range exceeds usize") + })?; + if end > amplitudes.len() { + return Err(RusticolError::integrity( + "on-the-fly amplitude destination is absent", + )); + } + let weight = request.total_coefficient() * normalization_factor; + accumulate_lc_diagonal_amplitude( + point_count, + weight, + |point| amplitudes[base + point], + |point, value| output[point] += value, + ); + } + Ok(()) +} + +fn reduce_resolved_into( + requests: &[OnTheFlyLcQueryRequestV1], + amplitude_destinations: &[Option], + amplitudes: &[(f64, f64)], + point_count: usize, + normalization_factor: f64, + layout: LcResolvedOutputLayout, + output: &mut [f64], +) -> RusticolResult<()> { + if amplitude_destinations.len() != requests.len() { + return Err(RusticolError::integrity( + "on-the-fly amplitude projection has the wrong request count", + )); + } + for (request, destination) in requests.iter().zip(amplitude_destinations) { + let Some(destination) = *destination else { + continue; + }; + let base = destination.checked_mul(point_count).ok_or_else(|| { + RusticolError::invalid_argument("on-the-fly amplitude destination offset exceeds usize") + })?; + let end = base.checked_add(point_count).ok_or_else(|| { + RusticolError::invalid_argument("on-the-fly amplitude range exceeds usize") + })?; + if end > amplitudes.len() { + return Err(RusticolError::integrity( + "on-the-fly amplitude destination is absent", + )); + } + for target in &request.reduction_targets { + let weight = target.coefficient * normalization_factor; + accumulate_lc_diagonal_amplitude( + point_count, + weight, + |point| amplitudes[base + point], + |point, value| { + let target_index = layout + .index(point, target.helicity_position, target.color_position) + .expect("validated on-the-fly resolved coordinate"); + output[target_index] += value; + }, + ); + } + } + Ok(()) +} + +#[allow(clippy::too_many_arguments)] +fn profile_from_report( + total: Duration, + parameter_setup: Duration, + input_setup: Duration, + execution: Duration, + reduction: Duration, + momentum_scalar_count: usize, + point_count: u32, + report: OnTheFlyQueryFamilyExecutionReportV1, +) -> RuntimeProfile { + let calls = u64::from(report.source_calls) + + u64::from(report.contribution_calls) + + u64::from(report.finalization_calls) + + u64::from(report.closure_calls); + let rows = u64::from(report.source_rows) + + u64::from(report.contribution_rows) + + u64::from(report.finalization_rows) + + u64::from(report.closure_rows); + direct_profile_from_delta( + total, + parameter_setup, + input_setup, + reduction, + DirectProfileDelta { + direct_execution: execution, + execution: DirectExecutionCounters { + source_calls: u64::from(report.source_calls), + source_rows: u64::from(report.source_rows), + contribution_calls: u64::from(report.contribution_calls), + contribution_rows: u64::from(report.contribution_rows), + finalization_calls: u64::from(report.finalization_calls), + finalization_rows: u64::from(report.finalization_rows), + closure_calls: u64::from(report.closure_calls), + closure_rows: u64::from(report.closure_rows), + ..DirectExecutionCounters::default() + }, + traffic: DirectArenaTrafficCounters { + calls, + rows, + points: calls.saturating_mul(u64::from(point_count)), + ..DirectArenaTrafficCounters::default() + }, + activity: DirectRuntimeActivityCounters { + momentum_fill_calls: 1, + momentum_scalar_values_filled: momentum_scalar_count as u64, + schedule_executions: 1, + ..DirectRuntimeActivityCounters::default() + }, + ..DirectProfileDelta::default() + }, + ) +} + +#[cfg(test)] +mod tests { + use super::super::recurrence_backend::on_the_fly_adapter_tests::{ + direct_catalog, prepared_pool, source_domains, + }; + use super::*; + use crate::recurrence::CheckedTableRange; + use crate::recurrence::SemanticDigest; + use crate::recurrence::on_the_fly::{OnTheFlyLcSelectorV1, scalar_adapter_test_seed}; + use crate::recurrence::template::{CouplingOrderTermRow, IndexedRangeRow}; + use crate::recurrence::validated_template_fixture; + + fn digest(byte: u8) -> SemanticDigest { + SemanticDigest::new([byte; 32]).unwrap() + } + + fn scalar_query() -> DecodedLcQueryV1 { + let seed = scalar_adapter_test_seed(digest(1), digest(2), digest(3), digest(4)).unwrap(); + DecodedLcQueryV1::new(&seed, vec![0, 1], &[0, 0], OnTheFlyLcSelectorV1::Singlet).unwrap() + } + + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + fn scalar_lane_with_threads(query_construction_threads: usize) -> OnTheFlyNativeRuntime { + let mut template_input = validated_template_fixture().into_input(); + template_input.coupling_order_ranges.push(IndexedRangeRow { + id: 1, + range: CheckedTableRange::new(0, 1), + }); + template_input + .coupling_order_terms + .push(CouplingOrderTermRow { + set_id: 1, + name_string_id: 0, + power: 1, + }); + let templates = template_input.validate().unwrap(); + let summary = templates.summary(); + let direct_digest = digest(40); + let direct = direct_catalog(direct_digest); + let seed = scalar_adapter_test_seed( + summary.compiled_model_digest, + summary.catalog_digest, + summary.prepared_kernel_pack_digest, + direct_digest, + ) + .unwrap(); + let pool = prepared_pool(&templates, direct_digest); + let sources = pool.bind_source_domains(source_domains()).unwrap(); + let resolver = pool.into_on_the_fly_resolver(sources); + let defaults = vec![ + crate::EagerComplex64::new(0.0, 0.0); + usize::try_from(summary.parameter_count).unwrap() + ]; + OnTheFlyNativeRuntime::new( + templates, + direct, + seed, + resolver, + query_construction_threads, + query_construction_threads, + defaults, + Vec::new(), + &[], + ) + .unwrap() + } + + fn scalar_lane() -> OnTheFlyNativeRuntime { + scalar_lane_with_threads(4) + } + + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + fn scalar_request(lane: &OnTheFlyNativeRuntime, coefficient: f64) -> OnTheFlyLcQueryRequestV1 { + let query = DecodedLcQueryV1::new( + lane.seed(), + vec![0, 1], + &[0, 0], + OnTheFlyLcSelectorV1::Singlet, + ) + .unwrap(); + OnTheFlyLcQueryRequestV1::new( + query, + vec![OnTheFlyLcReductionTargetV1::new(0, 0, coefficient).unwrap()], + ) + .unwrap() + } + + #[test] + fn lc_reduction_targets_reject_nonfinite_or_negative_coefficients() { + assert!(OnTheFlyLcReductionTargetV1::new(0, 0, -1.0).is_err()); + assert!(OnTheFlyLcReductionTargetV1::new(0, 0, f64::NAN).is_err()); + assert_eq!( + OnTheFlyLcReductionTargetV1::new(3, 5, 2.0) + .unwrap() + .coefficient, + 2.0 + ); + } + + #[test] + fn lc_query_rejects_duplicate_resolved_coordinates() { + let duplicate = vec![ + OnTheFlyLcReductionTargetV1::new(2, 3, 1.0).unwrap(), + OnTheFlyLcReductionTargetV1::new(2, 3, 2.0).unwrap(), + ]; + assert!(OnTheFlyLcQueryRequestV1::new(scalar_query(), duplicate).is_err()); + } + + #[test] + fn lc_query_rejects_zero_total_reduction_weight() { + let zero = vec![OnTheFlyLcReductionTargetV1::new(0, 0, 0.0).unwrap()]; + assert!(OnTheFlyLcQueryRequestV1::new(scalar_query(), zero).is_err()); + } + + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + #[test] + fn serial_and_bounded_parallel_query_construction_match() { + let mut serial = scalar_lane_with_threads(1); + let mut parallel = scalar_lane_with_threads(4); + let serial_requests = [scalar_request(&serial, 1.0), scalar_request(&serial, 2.0)]; + let parallel_requests = [ + scalar_request(¶llel, 1.0), + scalar_request(¶llel, 2.0), + ]; + + assert!(!serial.prepare_lc_queries(&serial_requests, 1).unwrap()); + assert!(!parallel.prepare_lc_queries(¶llel_requests, 1).unwrap()); + assert_eq!( + serial.retained_state_census(), + parallel.retained_state_census() + ); + + let mut serial_value = [f64::NAN]; + let mut parallel_value = [f64::NAN]; + serial + .run_f64_into(&[0.0; 8], 1, &[], 1.0, &mut serial_value) + .unwrap(); + parallel + .run_f64_into(&[0.0; 8], 1, &[], 1.0, &mut parallel_value) + .unwrap(); + assert_eq!(serial_value, parallel_value); + } + + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + #[test] + fn process_preparation_is_lazy_shared_compact_and_clearable() { + let mut lane = scalar_lane(); + assert_eq!(lane.process_preparation_count(), 0); + assert_eq!( + lane.process_preparation_census(), + OnTheFlyProcessPreparationCensusV1::default() + ); + assert_eq!(lane.coupling_policy_census(), None); + assert_eq!(lane.coupling_policy_resolution(), None); + assert_eq!(lane.retained_family_count(), 0); + + let first = scalar_request(&lane, 1.0); + assert!( + !lane + .prepare_lc_queries(std::slice::from_ref(&first), 1) + .unwrap() + ); + assert_eq!(lane.retained_state_census().family_count, 0); + assert_eq!(lane.pending_family_count(), 1); + assert_eq!(lane.process_preparation_count(), 1); + assert_eq!( + lane.process_preparation_census(), + OnTheFlyProcessPreparationCensusV1 { + catalog_validation_count: 1, + grammar_preparation_count: 1, + } + ); + assert!(lane.coupling_policy_census().is_some()); + assert!(lane.coupling_policy_resolution().is_some()); + let mut first_value = [0.0]; + lane.run_f64_into(&[0.0; 8], 1, &[], 1.0, &mut first_value) + .unwrap(); + // This compact fixture is an authenticated structural-zero query. It + // deliberately exercises the outer retained-family lifecycle without + // manufacturing an executor binding that the fixture's prepared pool + // does not own; nonzero A -> B -> A executor reuse is covered in the + // family executor tests below this public lane. + assert_eq!(first_value, [0.0]); + assert_eq!(lane.retained_state_census().family_count, 1); + assert_eq!(lane.pending_family_count(), 0); + assert!( + lane.prepare_lc_queries(std::slice::from_ref(&first), 1) + .unwrap() + ); + + let second = scalar_request(&lane, 2.0); + assert!( + !lane + .prepare_lc_queries(std::slice::from_ref(&second), 1) + .unwrap() + ); + let mut second_value = [0.0]; + lane.run_f64_into(&[0.0; 8], 1, &[], 1.0, &mut second_value) + .unwrap(); + assert_eq!(second_value, [2.0 * first_value[0]]); + + assert!( + !lane + .prepare_lc_queries(std::slice::from_ref(&first), 1) + .unwrap() + ); + let mut repeated = [0.0]; + lane.run_f64_into(&[0.0; 8], 1, &[], 1.0, &mut repeated) + .unwrap(); + assert_eq!(repeated, first_value); + + let batched = [first.clone(), second.clone()]; + assert!(!lane.prepare_lc_queries(&batched, 1).unwrap()); + let mut batched_value = [0.0]; + lane.run_f64_into(&[0.0; 8], 1, &[], 1.0, &mut batched_value) + .unwrap(); + assert_eq!(batched_value, [3.0 * first_value[0]]); + assert_eq!(lane.process_preparation_count(), 1); + assert_eq!( + lane.process_preparation_census(), + OnTheFlyProcessPreparationCensusV1 { + catalog_validation_count: 1, + grammar_preparation_count: 1, + } + ); + + let retained = lane.retained_state_census(); + assert_eq!(retained.family_count, 1); + assert_eq!(retained.request_count, 2); + assert_eq!(retained.amplitude_destination_count, 0); + assert_eq!(retained.executor_handle_count, 0); + assert_eq!(retained.query_local_trace_count, 0); + assert_eq!(retained.embedded_lookup_key_count, 0); + assert_eq!(lane.pending_family_count(), 0); + + lane.clear().unwrap(); + assert_eq!(lane.process_preparation_count(), 0); + assert_eq!( + lane.process_preparation_census(), + OnTheFlyProcessPreparationCensusV1::default() + ); + assert_eq!(lane.coupling_policy_census(), None); + assert_eq!(lane.coupling_policy_resolution(), None); + assert_eq!( + lane.retained_state_census(), + OnTheFlyRetainedStateCensusV1::default() + ); + assert_eq!(lane.pending_family_count(), 0); + assert!(lane.source_momenta_scratch.is_empty()); + assert_eq!(lane.source_momenta_scratch.capacity(), 0); + assert!(lane.amplitude_scratch.is_empty()); + assert_eq!(lane.amplitude_scratch.capacity(), 0); + + assert!(!lane.prepare_lc_queries(&[first], 1).unwrap()); + assert_eq!(lane.process_preparation_count(), 1); + assert_eq!( + lane.process_preparation_census(), + OnTheFlyProcessPreparationCensusV1 { + catalog_validation_count: 1, + grammar_preparation_count: 1, + } + ); + let mut rebuilt = [0.0]; + lane.run_f64_into(&[0.0; 8], 1, &[], 1.0, &mut rebuilt) + .unwrap(); + assert_eq!(rebuilt, first_value); + } + + #[test] + fn profile_maps_grouped_family_role_counters_without_dense_plan_counters() { + let profile = profile_from_report( + Duration::from_millis(11), + Duration::from_millis(1), + Duration::from_millis(2), + Duration::from_millis(7), + Duration::from_millis(1), + 96, + 3, + OnTheFlyQueryFamilyExecutionReportV1 { + cache_hit: true, + source_calls: 2, + source_rows: 4, + contribution_calls: 3, + contribution_rows: 17, + finalization_calls: 1, + finalization_rows: 5, + closure_calls: 2, + closure_rows: 7, + }, + ); + assert_eq!(profile.recurrence_schedule_execution_count, 1); + assert_eq!(profile.recurrence_momentum_scalar_value_count, 96); + assert_eq!(profile.recurrence_source_call_count, 2); + assert_eq!(profile.recurrence_source_row_count, 4); + assert_eq!(profile.recurrence_contribution_call_count, 3); + assert_eq!(profile.recurrence_contribution_row_count, 17); + assert_eq!(profile.recurrence_finalization_call_count, 1); + assert_eq!(profile.recurrence_finalization_row_count, 5); + assert_eq!(profile.recurrence_closure_call_count, 2); + assert_eq!(profile.recurrence_closure_row_count, 7); + } + + #[test] + fn source_major_workspace_is_reused_after_warmup() { + let seed = scalar_adapter_test_seed(digest(1), digest(2), digest(3), digest(4)).unwrap(); + let point_major = (0..16).map(f64::from).collect::>(); + let mut scratch = Vec::new(); + let output_len = + on_the_fly_source_major_momenta_into(&seed, &point_major, 2, 4, &mut scratch).unwrap(); + assert_eq!(output_len, 16); + assert_eq!(scratch[..4], [0.0, 8.0, 1.0, 9.0]); + let warmed_pointer = scratch.as_ptr(); + let warmed_capacity = scratch.capacity(); + on_the_fly_source_major_momenta_into(&seed, &point_major, 2, 4, &mut scratch).unwrap(); + assert_eq!(scratch.as_ptr(), warmed_pointer); + assert_eq!(scratch.capacity(), warmed_capacity); + } + + #[test] + fn source_major_workspace_preserves_values_across_partial_batches() { + use crate::recurrence::on_the_fly::{OnTheFlyExternalColorRoleV1, OnTheFlySourceAnchorV1}; + + let base = scalar_adapter_test_seed(digest(1), digest(2), digest(3), digest(4)).unwrap(); + let anchors = (0..3) + .map(|slot| { + OnTheFlySourceAnchorV1::new( + slot, + slot, + false, + OnTheFlyExternalColorRoleV1::Singlet, + false, + None, + base.source_anchors()[0].states().to_vec(), + ) + .unwrap() + }) + .collect(); + let seed = OnTheFlyProcessSeedV1::new( + base.process_digest(), + base.model_digest(), + base.template_catalog_digest(), + base.prepared_pack_digest(), + base.direct_catalog_digest(), + digest(92), + "raw-amplitude-test", + crate::recurrence::ExactComplexRational::ONE, + anchors, + vec![1, 2, 0], + base.coupling_order_policy(), + base.coupling_hierarchies().to_vec(), + base.explicit_coupling_limits().to_vec(), + Vec::new(), + ) + .unwrap(); + let mut scratch = vec![53.0; seed.external_permutation().len() * 4 * 17]; + let pointer = scratch.as_ptr(); + let capacity = scratch.capacity(); + for lorentz_count in [1, 3, 4] { + for point_count in [1, 17, 3, 8, 1] { + let point_width = seed.external_permutation().len() * lorentz_count; + let input = (0..point_width * point_count) + .map(|index| { + if index % 7 == 0 { + -0.0 + } else { + index as f64 / 11.0 + } + }) + .collect::>(); + scratch.fill(53.0); + let count = on_the_fly_source_major_momenta_into( + &seed, + &input, + point_count as u32, + lorentz_count as u16, + &mut scratch, + ) + .unwrap(); + assert_eq!(count, input.len()); + for (source, public) in seed.external_permutation().iter().copied().enumerate() { + for lorentz in 0..lorentz_count { + for point in 0..point_count { + assert_eq!( + scratch[(source * lorentz_count + lorentz) * point_count + point] + .to_bits(), + input[point * point_width + + public as usize * lorentz_count + + lorentz] + .to_bits(), + ); + } + } + } + assert!(scratch[count..].iter().all(|value| *value == 53.0)); + assert_eq!(scratch.as_ptr(), pointer); + assert_eq!(scratch.capacity(), capacity); + } + } + assert!(on_the_fly_source_major_momenta_into(&seed, &[1.0], 1, 4, &mut scratch).is_err()); + assert!(on_the_fly_source_major_momenta_into(&seed, &[], 0, 4, &mut scratch).is_err()); + } + + #[test] + fn mixed_structural_zero_and_trace_preserve_zero_resolved_slot() { + let query = scalar_query(); + let requests = vec![ + OnTheFlyLcQueryRequestV1::new( + query.clone(), + vec![OnTheFlyLcReductionTargetV1::new(0, 0, 1.0).unwrap()], + ) + .unwrap(), + OnTheFlyLcQueryRequestV1::new( + query, + vec![OnTheFlyLcReductionTargetV1::new(1, 0, 2.0).unwrap()], + ) + .unwrap(), + ]; + // Request zero is a structural zero and therefore has no amplitude + // destination. Request one is the sole nonzero trace destination. + let amplitude_destinations = [None, Some(0)]; + let amplitudes = [(3.0, 4.0)]; + + let mut total = [0.0]; + reduce_total_into( + &requests, + &litude_destinations, + &litudes, + 1, + 1.0, + &mut total, + ) + .unwrap(); + assert_eq!(total, [50.0]); + + let layout = LcResolvedOutputLayout::new(1, 2, 1).unwrap(); + let mut resolved = [0.0; 2]; + reduce_resolved_into( + &requests, + &litude_destinations, + &litudes, + 1, + 1.0, + layout, + &mut resolved, + ) + .unwrap(); + assert_eq!(resolved, [0.0, 50.0]); + } + + #[test] + fn all_structural_zero_family_reduces_to_exact_zero_without_amplitudes() { + let requests = [OnTheFlyLcQueryRequestV1::new( + scalar_query(), + vec![OnTheFlyLcReductionTargetV1::new(0, 0, 1.0).unwrap()], + ) + .unwrap()]; + let mut total = [0.0]; + reduce_total_into(&requests, &[None], &[], 1, 1.0, &mut total).unwrap(); + assert_eq!(total, [0.0]); + + let layout = LcResolvedOutputLayout::new(1, 1, 1).unwrap(); + let mut resolved = [0.0]; + reduce_resolved_into(&requests, &[None], &[], 1, 1.0, layout, &mut resolved).unwrap(); + assert_eq!(resolved, [0.0]); + } + + #[test] + fn symmetric_group_contracted_query_projection_reaches_final_reducer_in_group_order() { + let destination_by_group = (0..13) + .map(|group| ((group * 5) % 13) as u32) + .collect::>(); + let mut destination_by_owner_ordinal = vec![0_u32; 13]; + for (group, owner) in destination_by_group.iter().copied().enumerate() { + destination_by_owner_ordinal[owner as usize] = group as u32; + } + let contraction = RecurrenceColorContraction::symmetric_group_s3_for_runtime_test( + destination_by_group.clone(), + 13, + ); + let RuntimeColorContractionReducer::SymmetricGroupFourier(reducer) = + contraction.runtime_reducer().unwrap() + else { + panic!("expected symmetric-group runtime reducer") + }; + let covered_group_count = reducer.channel_count() * reducer.group_order(); + assert!(reducer.channel_count() > 0); + assert!(reducer.local_group_count() > covered_group_count); + assert!(reducer.residual_entries().iter().any(|entry| { + entry.left_group_index < covered_group_count as u32 + && entry.right_group_index >= covered_group_count as u32 + })); + let point_count = 3; + let helicity_count = 2; + let structural_color_count = 13; + let normalization = 1.75; + let mut projection = vec![None; helicity_count * structural_color_count]; + let mut amplitudes = Vec::new(); + let mut local_values = + vec![(0.0, 0.0); helicity_count * structural_color_count * point_count]; + // Query construction enumerates owners. Its production slot helper + // stores every outcome in the inverse-mapped Fourier-group slot. The + // complex values deliberately depend on the owner so a second + // application of this order-4 permutation cannot pass accidentally. + for helicity in 0..helicity_count { + for owner in 0..structural_color_count { + let group = destination_by_owner_ordinal[owner] as usize; + let structural_zero = helicity == 1 && group == 4; + if structural_zero { + continue; + } + let destination = amplitudes.len() / point_count; + let projection_index = contracted_projection_index( + helicity, + owner, + structural_color_count, + &destination_by_owner_ordinal, + ) + .unwrap(); + projection[projection_index] = Some(destination); + for point in 0..point_count { + let value = ( + (helicity * 19 + owner * 7 + point * 3) as f64 / 11.0 - 1.0, + (helicity * 5 + owner * 13 + point) as f64 / 17.0 - 0.5, + ); + amplitudes.push(value); + local_values + [(helicity * structural_color_count + group) * point_count + point] = value; + } + } + } + + let mut expected = vec![0.0; helicity_count * point_count]; + let helicity_stride = structural_color_count * point_count; + for helicity in 0..helicity_count { + let start = helicity * helicity_stride; + let dense = RecurrenceColorContraction::symmetric_group_s3_dense_for_runtime_test( + &local_values[start..start + helicity_stride], + point_count, + ); + for (point, value) in dense.into_iter().enumerate() { + expected[point * helicity_count + helicity] = normalization * value; + } + } + + // Install exactly the compact family state produced by query + // preparation, then enter the native lane's real final-reduction seam. + // No decoded queries or owner-indexed side table survives this point. + let mut lane = scalar_lane(); + lane.pending_contracted_family = Some(PreparedOnTheFlyContractedFamilyV1 { + helicity_ordinals: (0..helicity_count).collect::>().into_boxed_slice(), + structural_color_count, + amplitude_destinations: projection.into_boxed_slice(), + executor_handle: None, + census: None, + logical_point_capacity: point_count as u32, + }); + lane.amplitude_scratch = amplitudes; + lane.install_symmetric_group_color_workspace(reducer.workspace(point_count).unwrap()) + .unwrap(); + let mut actual = vec![0.0; expected.len()]; + lane.reduce_current_contracted_color( + &contraction, + point_count, + helicity_count, + normalization, + |point, helicity, value| actual[point * helicity_count + helicity] += value, + ) + .unwrap(); + for (actual, expected) in actual.into_iter().zip(expected) { + let scale = expected.abs().max(1.0); + assert!((actual - expected).abs() <= 2.0e-11 * scale); + } + } + + #[test] + fn contracted_metric_preserves_imaginary_off_diagonal_terms_per_helicity() { + let entries = [ + RuntimeColorContractionEntry { + left_destination_id: 0, + right_destination_id: 0, + coefficient_re: 1.0, + coefficient_im: 0.0, + }, + RuntimeColorContractionEntry { + left_destination_id: 0, + right_destination_id: 1, + coefficient_re: 2.0, + coefficient_im: 3.0, + }, + RuntimeColorContractionEntry { + left_destination_id: 1, + right_destination_id: 1, + coefficient_re: 0.5, + coefficient_im: 0.0, + }, + ]; + // H0 owns both structural amplitudes. H1's second structural query is + // an authenticated zero and every bilinear touching it must vanish. + let projection = [Some(0), Some(1), Some(2), None]; + let amplitudes = [(1.0, 2.0), (3.0, 4.0), (2.0, 0.0)]; + let mut resolved = [0.0; 2]; + reduce_direct_contracted_color( + entries, + &projection, + &litudes, + 1, + 2, + 2, + 2.0, + |point, helicity, value| resolved[point * 2 + helicity] += value, + ) + .unwrap(); + + // Re((2+3i) * (1+2i) * conj(3+4i)) = 16. The complete + // H0 metric is 5 + 16 + 12.5; H1 retains only |2|^2. + assert_eq!(resolved, [67.0, 8.0]); + assert_eq!(resolved.iter().sum::(), 75.0); + } + + #[test] + fn contracted_metric_rejects_projection_and_destination_shape_mismatches() { + let entry = RuntimeColorContractionEntry { + left_destination_id: 0, + right_destination_id: 1, + coefficient_re: 1.0, + coefficient_im: 0.0, + }; + assert!( + reduce_direct_contracted_color( + [entry], + &[Some(0)], + &[(1.0, 0.0)], + 1, + 1, + 2, + 1.0, + |_, _, _| {}, + ) + .is_err() + ); + assert!( + reduce_direct_contracted_color( + [RuntimeColorContractionEntry { + right_destination_id: 2, + ..entry + }], + &[Some(0), Some(1)], + &[(1.0, 0.0), (1.0, 0.0)], + 1, + 1, + 2, + 1.0, + |_, _, _| {}, + ) + .is_err() + ); + } +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/engine/on_the_fly_lane_cache.rs b/pyamplicol/rust/crates/rusticol-core/src/engine/on_the_fly_lane_cache.rs new file mode 100644 index 0000000000..1545d07d68 --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/engine/on_the_fly_lane_cache.rs @@ -0,0 +1,231 @@ +// SPDX-License-Identifier: 0BSD + +use super::*; +use crate::recurrence::on_the_fly::{ + OnTheFlyFamilySnapshotRefV1, OnTheFlyFamilySnapshotV1, restore_on_the_fly_grammar_v1, +}; + +#[derive(bincode::Encode, bincode::Decode)] +struct LcProjectionV1 { + amplitude_destinations: Box<[Option]>, + census: Option, + logical_point_capacity: u32, +} + +#[derive(bincode::Encode)] +struct LcProjectionRefV1<'a> { + amplitude_destinations: &'a [Option], + census: Option, + logical_point_capacity: u32, +} + +#[derive(bincode::Encode, bincode::Decode)] +struct ContractedProjectionV1 { + helicity_ordinals: Box<[usize]>, + structural_color_count: usize, + projection: LcProjectionV1, +} + +#[derive(bincode::Encode)] +struct ContractedProjectionRefV1<'a> { + helicity_ordinals: &'a [usize], + structural_color_count: usize, + projection: LcProjectionRefV1<'a>, +} + +#[derive(bincode::Decode)] +pub(in super::super) struct OnTheFlyLaneSnapshotV1 { + policy: Option, + process_preparation_count: u64, + lc: Option, + contracted: Option, + executor: Option, +} + +#[derive(bincode::Encode)] +pub(in super::super) struct OnTheFlyLaneSnapshotRefV1<'a> { + policy: Option<&'a OnTheFlyResolvedCouplingPolicyV1>, + process_preparation_count: u64, + lc: Option>, + contracted: Option>, + executor: Option>, +} + +impl OnTheFlyLaneSnapshotV1 { + pub(in super::super) fn has_family(&self) -> bool { + self.lc.is_some() || self.contracted.is_some() + } +} + +impl OnTheFlyNativeRuntime { + pub(in super::super) fn snapshot(&self) -> RusticolResult> { + if self.families.len() > 1 || (self.last_family.is_some() && self.last_family != Some(0)) { + return Err(RusticolError::internal( + "OTF retention policy changed; update the cache format", + )); + } + Ok(OnTheFlyLaneSnapshotRefV1 { + policy: self.coupling_policy.as_ref(), + process_preparation_count: self.process_preparation_count, + lc: self.families.first().map(|family| LcProjectionRefV1 { + amplitude_destinations: &family.amplitude_destinations, + census: family.census, + logical_point_capacity: family.logical_point_capacity, + }), + contracted: self + .contracted_family + .as_ref() + .map(|family| ContractedProjectionRefV1 { + helicity_ordinals: &family.helicity_ordinals, + structural_color_count: family.structural_color_count, + projection: LcProjectionRefV1 { + amplitude_destinations: &family.amplitude_destinations, + census: family.census, + logical_point_capacity: family.logical_point_capacity, + }, + }), + executor: self.executor.snapshot()?, + }) + } + + pub(in super::super) fn restore_snapshot( + &mut self, + snapshot: OnTheFlyLaneSnapshotV1, + requests: Option>, + contracted_selection: Option<(&[usize], usize)>, + ) -> RusticolResult<()> { + let OnTheFlyLaneSnapshotV1 { + policy, + process_preparation_count, + lc, + contracted, + executor, + } = snapshot; + if lc.is_some() && contracted.is_some() + || lc.is_some() != requests.is_some() + || contracted.is_some() != contracted_selection.is_some() + || (lc.is_some() || contracted.is_some()) && policy.is_none() + { + return Err(RusticolError::integrity( + "saved OTF family and selected workload disagree", + )); + } + let grammar = policy + .as_ref() + .map(|policy| restore_on_the_fly_grammar_v1(&self.templates, &self.seed, policy)) + .transpose()?; + let projection = lc + .as_ref() + .or_else(|| contracted.as_ref().map(|value| &value.projection)); + if let Some(projection) = projection { + let count = executor + .as_ref() + .map_or(0, OnTheFlyFamilySnapshotV1::amplitude_destination_count); + if projection.logical_point_capacity == 0 + || projection.census != executor.as_ref().map(OnTheFlyFamilySnapshotV1::census) + || projection + .amplitude_destinations + .iter() + .flatten() + .any(|&id| id >= count) + || executor.is_none() + && projection + .amplitude_destinations + .iter() + .any(Option::is_some) + { + return Err(RusticolError::integrity( + "saved OTF amplitude projection is inconsistent", + )); + } + } else if executor.is_some() { + return Err(RusticolError::integrity( + "saved OTF executor has no selected workload", + )); + } + if let (Some(lc), Some(requests)) = (&lc, &requests) + && lc.amplitude_destinations.len() != requests.len() + { + return Err(RusticolError::integrity( + "saved OTF LC projection has the wrong query count", + )); + } + if let (Some(contracted), Some((helicities, colors))) = (&contracted, contracted_selection) + && (contracted.helicity_ordinals.as_ref() != helicities + || contracted.structural_color_count != colors + || helicities.len().checked_mul(colors) + != Some(contracted.projection.amplitude_destinations.len())) + { + return Err(RusticolError::integrity( + "saved OTF contracted projection has the wrong selected axes", + )); + } + // Allocate all replacement metadata before committing a new executor. + let mut restored_lc = Vec::new(); + if lc.is_some() { + restored_lc.try_reserve_exact(1).map_err(|error| { + RusticolError::invalid_argument(format!( + "OTF restored selection allocation failed: {error}" + )) + })?; + } + let requests = requests.map(Vec::into_boxed_slice); + let capacity = projection.map_or(1, |value| value.logical_point_capacity); + let handle = if let Some(executor) = executor { + let restored = (|| { + self.executor.resolver_mut().bind_on_the_fly_saved_family( + &self.templates, + &self.direct_catalog, + executor.executor_keys(), + )?; + self.executor.restore_snapshot(executor, capacity) + })(); + match restored { + Ok(handle) => { + self.executor + .resolver_mut() + .commit_pending_resolved_bindings()?; + Some(handle) + } + Err(error) => { + self.executor + .resolver_mut() + .discard_pending_resolved_bindings(); + return Err(error); + } + } + } else { + self.executor.clear_families()?; + self.executor.resolver_mut().clear_resolved_bindings(); + None + }; + if let Some(lc) = lc { + restored_lc.push(PreparedOnTheFlyLcFamilyV1 { + requests: requests.expect("validated saved LC requests disappeared"), + amplitude_destinations: lc.amplitude_destinations, + executor_handle: handle, + census: lc.census, + logical_point_capacity: lc.logical_point_capacity, + }); + } + self.last_family = (!restored_lc.is_empty()).then_some(0); + self.families = restored_lc; + self.pending_family = None; + self.contracted_family = contracted.map(|value| PreparedOnTheFlyContractedFamilyV1 { + helicity_ordinals: value.helicity_ordinals, + structural_color_count: value.structural_color_count, + amplitude_destinations: value.projection.amplitude_destinations, + executor_handle: handle, + census: value.projection.census, + logical_point_capacity: value.projection.logical_point_capacity, + }); + self.pending_contracted_family = None; + self.prepared_grammar = grammar; + self.coupling_policy = policy; + self.coupling_policy_resolution = self.coupling_policy.as_ref().map(|_| Duration::ZERO); + self.process_preparation_count = process_preparation_count; + self.source_momenta_scratch = Vec::new(); + self.amplitude_scratch = Vec::new(); + Ok(()) + } +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/engine/on_the_fly_load.rs b/pyamplicol/rust/crates/rusticol-core/src/engine/on_the_fly_load.rs new file mode 100644 index 0000000000..888e6e7a8c --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/engine/on_the_fly_load.rs @@ -0,0 +1,975 @@ +// SPDX-License-Identifier: 0BSD + +//! Authenticated construction boundary for the private on-the-fly lane. +//! +//! Loading stops at the compact process seed and model-wide prepared catalogs. +//! It never opens or constructs a process-wide [`crate::recurrence::DirectRecurrencePlan`] +//! and it never materializes dense helicity/color axes. NLC/full authenticate +//! one recurrence-v3 color payload against the compact structural owner basis. +//! The existing runtime adapter owns public selector decoding after this boundary. + +use super::eager_manifest::PreparedKernelPackManifest; +use super::on_the_fly_lane::OnTheFlyNativeRuntime; +use super::on_the_fly_manifest::{ + ON_THE_FLY_PROCESS_SEED_MEMBER, OnTheFlyColorCoverage, OnTheFlyExecutionManifest, + OnTheFlyRuntimeContainer, +}; +use super::on_the_fly_public_metadata::{ + OnTheFlyPublicMetadataV1, parse_on_the_fly_public_metadata, +}; +use super::on_the_fly_selectors::{ + OnTheFlyCompactSelectorAdapterV1, OnTheFlyLcColorCoverageV1, OnTheFlyLcSelectorPolicyV1, +}; +use super::recurrence_backend::{ + NativeRecurrencePreparedExecutorPool, OnTheFlySourceDomainBinding, +}; +use super::recurrence_bootstrap::{ + RecurrenceReadyCompanionV1, RecurrenceReadyExecutionV1, RecurrenceReadyPlanV1, +}; +use super::recurrence_lane::{ + PreparedParameterProjectionEntry, RecurrencePersistedHelicitySelectorRuntime, +}; +use super::recurrence_load::{ + LoadedRecurrencePlan, build_direct_source_domains, build_on_the_fly_source_domains, + decode_ready_source_domains, decode_sha256, direct_helicity_to_physics, + load_color_contraction_reference, load_plan_summary, runtime_parameter_slots, + validate_ready_direct_helicity_to_physics, validate_recurrence_prepared_pack_outer_target, +}; +use super::*; +use crate::pacbin::{PacbinMemberKind, PacbinReader}; +use crate::recurrence::on_the_fly::{ + OnTheFlyExternalColorRoleV1, OnTheFlyProcessSeedV1, decode_on_the_fly_process_seed_v1, + validate_on_the_fly_source_mass_bindings_v1, +}; +use crate::recurrence::template_json::project_owned_recurrence_template_catalog_json_v1; +use crate::recurrence::{ + FactorizedColorContractionKind, RecurrenceColorAccuracy, RecurrenceColorContraction, + RecurrenceColorStorage, decode_recurrence_helicity_dispatch_v1, +}; +use std::rc::Rc; + +pub(super) struct LoadedOnTheFlyColorContractionV1 { + pub(super) plan: RecurrenceColorContraction, + pub(super) destination_by_owner_ordinal: Box<[u32]>, + pub(super) point_tile_size: usize, +} + +pub(super) struct LoadedOnTheFlyRuntime { + pub(super) common: ExecutionRuntime, + pub(super) lane: OnTheFlyNativeRuntime, + pub(super) selectors: OnTheFlyCompactSelectorAdapterV1, + pub(super) metadata_selectors: OnTheFlyCompactSelectorAdapterV1, + pub(super) public_metadata: OnTheFlyPublicMetadataV1, + pub(super) color_contraction: Option, +} + +pub(super) struct LoadedRecurrenceHelicitySelectorCompanion { + pub(super) runtime: RecurrencePersistedHelicitySelectorRuntime, +} + +fn clamp_query_construction_threads(requested: usize, available: usize) -> usize { + requested.min(available.max(1)) +} + +pub(super) fn effective_query_construction_threads(requested: usize) -> usize { + let available = std::thread::available_parallelism() + .map(std::num::NonZeroUsize::get) + .unwrap_or(1); + clamp_query_construction_threads(requested, available) +} + +fn validate_recurrence_companion_process_digest( + primary_process_digest: &str, + companion_process_digest: &str, + decoded_seed_process_digest: &str, +) -> RusticolResult<()> { + if primary_process_digest != companion_process_digest + || primary_process_digest != decoded_seed_process_digest + { + return Err(RusticolError::integrity( + "recurrence helicity-selector companion is bound to a different primary process", + )); + } + Ok(()) +} + +pub(super) fn load_on_the_fly_native_runtime( + artifact: &VerifiedArtifact, + evaluator_root: &Path, + manifest: &OnTheFlyExecutionManifest, + selection: &crate::ArtifactSelection, +) -> RusticolResult { + let public_metadata = load_public_metadata(artifact, manifest, &selection.process)?; + let seed = load_process_seed(artifact, evaluator_root, manifest)?; + if seed.identity() != manifest.runtime_metadata.process_seed_identity { + return Err(RusticolError::integrity( + "on-the-fly execution metadata does not identify its decoded process seed", + )); + } + let (mut pack, payload_root) = load_prepared_pack(artifact, manifest)?; + let raw_templates = pack.recurrence_template.take().ok_or_else(|| { + RusticolError::compatibility( + "on-the-fly execution requires the prepared recurrence template catalog", + ) + })?; + let raw_template_json = serde_json::from_str(raw_templates.get()).map_err(|error| { + RusticolError::serialization(format!( + "could not parse prepared recurrence template catalog: {error}" + )) + })?; + drop(raw_templates); + let templates = + project_owned_recurrence_template_catalog_json_v1(raw_template_json)?.validate()?; + let summary = templates.summary(); + if seed.template_catalog_digest() != summary.catalog_digest + || seed.model_digest() != summary.compiled_model_digest + || seed.prepared_pack_digest() != summary.prepared_kernel_pack_digest + { + return Err(RusticolError::integrity( + "on-the-fly process seed disagrees with the authenticated recurrence template catalog", + )); + } + let parameter_projection = manifest + .runtime_metadata + .parameter_projection + .iter() + .map(|row| { + ( + row.parameter_template_id, + row.prepared_parameter_id, + row.component, + ) + }) + .collect::>(); + validate_on_the_fly_source_mass_bindings_v1(&seed, &templates, ¶meter_projection)?; + + let payloads = artifact.evaluator_payload_store(&payload_root)?; + let pool = NativeRecurrencePreparedExecutorPool::load_without_plan_from_validated_pack( + &mut pack, + &payloads, + &seed.prepared_pack_digest().to_string(), + &seed.direct_catalog_digest().to_string(), + )?; + let direct_catalog = pool.prepared_direct_catalog()?; + if direct_catalog.direct_template_catalog_digest() != seed.direct_catalog_digest() { + return Err(RusticolError::integrity( + "on-the-fly prepared Direct-Arena catalog disagrees with the compact process seed", + )); + } + let source_domains = build_on_the_fly_source_domains(&seed, &templates)?; + let sources = pool.bind_source_domains(source_domains)?; + let resolver = pool.into_on_the_fly_resolver(sources); + + let (mut common, defaults, projection) = build_common_runtime(manifest)?; + common.model_parameter_evaluator = + super::eager_load::load_prepared_model_parameter_evaluator_for_runtime( + &pack, + &common.model_parameters, + &payloads, + )?; + common.refresh_derived_model_parameters()?; + + let metadata_selectors = OnTheFlyCompactSelectorAdapterV1::from_seed( + &seed, + selector_policy(&manifest.selector_policy), + )?; + let public_color_count = if manifest.uses_contracted_color() { + 1 + } else { + metadata_selectors.color_count() + }; + manifest + .selector_policy + .selector_census + .validate_against(metadata_selectors.helicity_count(), public_color_count)?; + let color_contraction = load_on_the_fly_color_contraction( + artifact, + evaluator_root, + OnTheFlyColorContractionLoadV1 { + process_key: &manifest.key, + color_accuracy: &manifest.color_accuracy, + requested_point_tile_size: manifest.runtime_options.point_tile_size, + reference: manifest.runtime_metadata.color_contraction.as_ref(), + seed: &seed, + selectors: &metadata_selectors, + }, + )?; + let selectors = metadata_selectors + .clone() + .with_public_permutation(&selection.external_permutation)?; + let requested_query_construction_threads = + usize::try_from(manifest.runtime_options.query_construction_threads).map_err(|_| { + RusticolError::artifact("on-the-fly query construction thread count exceeds usize") + })?; + let effective_query_construction_threads = + effective_query_construction_threads(requested_query_construction_threads); + let mut lane = OnTheFlyNativeRuntime::new( + templates, + direct_catalog, + seed, + resolver, + requested_query_construction_threads, + effective_query_construction_threads, + defaults, + projection, + &common.model_parameter_values_f64, + )?; + if let Some(workspace) = symmetric_group_workspace_for_loaded_color(color_contraction.as_ref())? + { + lane.install_symmetric_group_color_workspace(workspace)?; + } + Ok(LoadedOnTheFlyRuntime { + common, + lane, + selectors, + metadata_selectors, + public_metadata, + color_contraction, + }) +} + +#[allow(clippy::too_many_arguments)] +pub(super) fn load_recurrence_helicity_selector_companion( + artifact: &VerifiedArtifact, + evaluator_root: &Path, + manifest: &super::recurrence_manifest::RecurrenceExecutionManifest, + physics: &ProcessPhysicsV1, + payloads: &EvaluatorPayloadStore, + common: &ExecutionRuntime, +) -> RusticolResult { + let companion = manifest + .helicity_selector_companion + .as_ref() + .ok_or_else(|| { + RusticolError::integrity( + "recurrence companion loader was called without a companion descriptor", + ) + })?; + let LoadedRecurrencePlan { + plan, + process_executors, + } = load_plan_summary(artifact, evaluator_root, manifest, &companion.plan)?; + validate_recurrence_companion_process_digest( + &manifest.plan.process_binding.process_digest, + &companion.process_digest, + &companion.plan.process_binding.process_digest, + )?; + let dispatch = load_recurrence_companion_dispatch( + artifact, + &plan, + companion.plan.helicity_dispatch.as_ref(), + )?; + let source_domains = build_direct_source_domains( + &plan, + &manifest.runtime_metadata, + &common.model_parameter_runtime_slots, + )?; + let direct_helicity_to_physics = direct_helicity_to_physics(&plan, physics)?; + finish_recurrence_helicity_selector_companion( + process_executors, + plan, + dispatch, + payloads, + RecurrenceCompanionModelIdentities { + compiled_model_digest: &manifest.compiled_model_digest, + recurrence_template_catalog_digest: &manifest.recurrence_template_catalog_digest, + prepared_kernel_pack_digest: &manifest.prepared_kernel_pack_digest, + direct_template_catalog_digest: &manifest.direct_template_catalog_digest, + }, + source_domains, + direct_helicity_to_physics, + physics.helicities.len(), + ) +} + +#[allow(clippy::too_many_arguments)] +pub(super) fn load_recurrence_helicity_selector_companion_from_ready_parts( + artifact: &VerifiedArtifact, + loaded_plan: LoadedRecurrencePlan, + plan_reference: &RecurrenceReadyPlanV1, + companion: &RecurrenceReadyCompanionV1, + ready: &RecurrenceReadyExecutionV1, + physics: &ProcessPhysicsV1, + payloads: &EvaluatorPayloadStore, +) -> RusticolResult { + validate_recurrence_companion_process_digest( + &ready.primary_plan.process_binding.process_digest, + &companion.process_digest, + &plan_reference.process_binding.process_digest, + )?; + let LoadedRecurrencePlan { + plan, + process_executors, + } = loaded_plan; + let dispatch = load_recurrence_companion_dispatch( + artifact, + &plan, + plan_reference.helicity_dispatch.as_ref(), + )?; + let source_domains = decode_ready_source_domains(&companion.source_domains, &plan)?; + let direct_helicity_to_physics = validate_ready_direct_helicity_to_physics( + &plan, + &companion.direct_helicity_to_physics, + physics, + )?; + finish_recurrence_helicity_selector_companion( + process_executors, + plan, + dispatch, + payloads, + RecurrenceCompanionModelIdentities { + compiled_model_digest: &ready.compiled_model_digest, + recurrence_template_catalog_digest: &ready.recurrence_template_catalog_digest, + prepared_kernel_pack_digest: &ready.prepared_kernel_pack_digest, + direct_template_catalog_digest: &ready.direct_template_catalog_digest, + }, + source_domains, + direct_helicity_to_physics, + physics.helicities.len(), + ) +} + +fn load_recurrence_companion_dispatch( + artifact: &VerifiedArtifact, + plan: &crate::recurrence::DirectRecurrencePlan, + reference: Option<&super::recurrence_manifest::RecurrenceHelicityDispatchReference>, +) -> RusticolResult { + let reference = reference.ok_or_else(|| { + RusticolError::integrity("recurrence helicity-selector plan has no exact helicity dispatch") + })?; + let record = artifact.payload(&reference.path)?; + if record.role != PayloadRole::EvaluatorState + || record.media_type != "application/octet-stream" + || record.process_id.is_some() + || record.executable + || record.size_bytes != reference.size_bytes + || record.sha256 != reference.sha256 + { + return Err(RusticolError::integrity( + "recurrence helicity dispatch disagrees with its authenticated payload", + )); + } + let bytes = artifact.read_payload(&reference.path)?; + let dispatch = decode_recurrence_helicity_dispatch_v1(&bytes)?; + dispatch.validate_for_plan(plan)?; + if dispatch.runtime_layout_digest().to_string() != reference.base_runtime_layout_digest + || dispatch.resolved_helicity_count() as u64 != reference.resolved_helicity_count + { + return Err(RusticolError::integrity( + "decoded recurrence helicity dispatch disagrees with its bounded summary", + )); + } + Ok(dispatch) +} + +struct RecurrenceCompanionModelIdentities<'a> { + compiled_model_digest: &'a str, + recurrence_template_catalog_digest: &'a str, + prepared_kernel_pack_digest: &'a str, + direct_template_catalog_digest: &'a str, +} + +#[allow(clippy::too_many_arguments)] +fn finish_recurrence_helicity_selector_companion( + process_executors: super::recurrence_process_pack::ProcessDirectExecutorPack, + plan: crate::recurrence::DirectRecurrencePlan, + dispatch: crate::recurrence::DirectHelicityDispatch, + payloads: &EvaluatorPayloadStore, + identities: RecurrenceCompanionModelIdentities<'_>, + source_domains: Vec, + direct_helicity_to_physics: Vec, + physics_helicity_count: usize, +) -> RusticolResult { + let pool = Rc::new( + NativeRecurrencePreparedExecutorPool::load_for_direct_plan_from_process_pack( + &process_executors, + payloads, + &plan, + )?, + ); + pool.validate_model_identities( + identities.compiled_model_digest, + identities.recurrence_template_catalog_digest, + identities.prepared_kernel_pack_digest, + identities.direct_template_catalog_digest, + )?; + let sources = OnTheFlySourceDomainBinding::load_for_process_plan(&plan, source_domains)?; + let resolver = Rc::clone(&pool).into_shared_on_the_fly_resolver(sources); + let runtime = RecurrencePersistedHelicitySelectorRuntime::new( + plan, + dispatch, + resolver, + direct_helicity_to_physics, + physics_helicity_count, + )?; + Ok(LoadedRecurrenceHelicitySelectorCompanion { runtime }) +} + +fn symmetric_group_workspace_for_loaded_color( + loaded: Option<&LoadedOnTheFlyColorContractionV1>, +) -> RusticolResult> { + let Some((reducer, point_tile_size)) = loaded.and_then(|loaded| { + loaded + .plan + .runtime_reducer() + .and_then(|reducer| match reducer { + crate::recurrence::RuntimeColorContractionReducer::SymmetricGroupFourier(value) => { + Some((value, loaded.point_tile_size)) + } + _ => None, + }) + }) else { + return Ok(None); + }; + // `point_tile_size` was already clamped against the shared 512 MiB + // workspace budget while authenticating the color payload. Allocate the + // final capacity here so the first multi-point evaluation is warmed too. + Ok(Some(reducer.workspace(point_tile_size)?)) +} + +fn load_public_metadata( + artifact: &VerifiedArtifact, + manifest: &OnTheFlyExecutionManifest, + outer: &crate::ArtifactProcess, +) -> RusticolResult { + let record = artifact.payload(&outer.physics_path)?; + if record.role != PayloadRole::RuntimePhysics + || record.media_type != "application/json" + || record.executable + || record.process_id.as_deref() != Some(manifest.key.as_str()) + { + return Err(RusticolError::integrity( + "on-the-fly compact public metadata has the wrong authenticated payload role", + )); + } + let bytes = artifact.read_payload(&outer.physics_path)?; + parse_on_the_fly_public_metadata(&bytes, &outer.physics_path, outer, manifest) +} + +fn selector_policy( + policy: &super::on_the_fly_manifest::OnTheFlySelectorPolicy, +) -> OnTheFlyLcSelectorPolicyV1 { + let color_coverage = match policy.color_coverage { + OnTheFlyColorCoverage::Complete | OnTheFlyColorCoverage::Contracted => { + // NLC/full still use the compact complete LC structural basis + // internally; only the public axis is contracted. + OnTheFlyLcColorCoverageV1::Complete + } + }; + OnTheFlyLcSelectorPolicyV1 { + color_coverage, + color_basis: policy.color_basis, + reference_color_word: policy + .reference_color_word + .clone() + .map(Vec::into_boxed_slice), + trace_reflections_folded: policy.trace_reflections_folded, + } +} + +struct OnTheFlyColorContractionLoadV1<'a> { + process_key: &'a str, + color_accuracy: &'a str, + requested_point_tile_size: u32, + reference: Option<&'a super::recurrence_manifest::RecurrenceColorContractionReference>, + seed: &'a OnTheFlyProcessSeedV1, + selectors: &'a OnTheFlyCompactSelectorAdapterV1, +} + +fn load_on_the_fly_color_contraction( + artifact: &VerifiedArtifact, + evaluator_root: &Path, + request: OnTheFlyColorContractionLoadV1<'_>, +) -> RusticolResult> { + let OnTheFlyColorContractionLoadV1 { + process_key, + color_accuracy, + requested_point_tile_size, + reference, + seed, + selectors, + } = request; + let Some(reference) = reference else { + return Ok(None); + }; + let plan = load_color_contraction_reference(artifact, evaluator_root, process_key, reference)?; + let expected_accuracy = match color_accuracy { + "nlc" => RecurrenceColorAccuracy::Nlc, + "full" => RecurrenceColorAccuracy::Full, + _ => { + return Err(RusticolError::integrity( + "LC on-the-fly execution unexpectedly loaded contracted color", + )); + } + }; + let owner_count = selectors.color_count(); + let owner_count_u32 = u32::try_from(owner_count).map_err(|_| { + RusticolError::artifact("on-the-fly contracted structural owner count exceeds u32") + })?; + let fundamental_count = seed + .source_anchors() + .iter() + .filter(|anchor| anchor.color_role() == OnTheFlyExternalColorRoleV1::Fundamental) + .count(); + let alias_multiplicity = (1..=fundamental_count).try_fold(1usize, |value, factor| { + value.checked_mul(factor).ok_or_else(|| { + RusticolError::artifact( + "on-the-fly contracted open-line alias multiplicity exceeds usize", + ) + }) + })?; + let expected_sector_count = owner_count.checked_mul(alias_multiplicity).ok_or_else(|| { + RusticolError::artifact("on-the-fly contracted structural sector count exceeds usize") + })?; + let plan_sector_count = usize::try_from(plan.sector_count()).map_err(|_| { + RusticolError::artifact("on-the-fly contracted payload sector count exceeds usize") + })?; + let direct_storage = + plan.storage() == RecurrenceColorStorage::Expanded && plan.factorization().is_none(); + let symmetric_group_storage = plan.storage() == RecurrenceColorStorage::ConvolutionKernels + && plan.factorization().is_some_and(|factorization| { + factorization.kind() == FactorizedColorContractionKind::SymmetricGroupFourier + }); + if plan.accuracy() != expected_accuracy + || !(direct_storage || symmetric_group_storage) + || !plan.includes_color_factor() + || plan.component_count() != 1 + || plan.active_sector_count() != owner_count + || plan.group_count() != owner_count_u32 + || plan.destination_count() != owner_count_u32 + || plan_sector_count != expected_sector_count + || plan.owner_by_sector().contains(&u32::MAX) + { + return Err(RusticolError::integrity( + "on-the-fly contracted color payload disagrees with the compact structural owner basis", + )); + } + + let fixed_owner_sectors = plan + .owner_by_sector() + .iter() + .copied() + .enumerate() + .filter_map(|(sector, owner)| (owner as usize == sector).then_some(owner)) + .collect::>(); + if fixed_owner_sectors.len() != owner_count { + return Err(RusticolError::integrity( + "on-the-fly contracted color owner fixed points are incomplete", + )); + } + let owner_class_sizes = plan.owner_by_sector().iter().copied().fold( + BTreeMap::::new(), + |mut counts, owner| { + *counts.entry(owner).or_default() += 1; + counts + }, + ); + if owner_class_sizes.len() != owner_count + || owner_class_sizes + .values() + .any(|count| *count != alias_multiplicity) + { + return Err(RusticolError::integrity( + "on-the-fly contracted owner classes disagree with whole-open-line block aliases", + )); + } + let group_by_owner_sector = plan + .sector_by_group() + .iter() + .copied() + .zip(plan.component_by_group().iter().copied()) + .enumerate() + .map(|(group_id, (sector, component))| { + if component != 0 { + return Err(RusticolError::integrity( + "on-the-fly contracted group has a nonzero component", + )); + } + let group_id = u32::try_from(group_id).map_err(|_| { + RusticolError::artifact("on-the-fly contracted group ID exceeds u32") + })?; + Ok((sector, group_id)) + }) + .collect::>>()?; + if group_by_owner_sector.len() != owner_count { + return Err(RusticolError::integrity( + "on-the-fly contracted groups do not cover every structural owner once", + )); + } + let destination_by_owner_ordinal = if direct_storage { + let mut destinations = Vec::new(); + destinations + .try_reserve_exact(owner_count) + .map_err(|error| { + RusticolError::artifact(format!( + "could not reserve on-the-fly contracted destination map: {error}" + )) + })?; + for (owner_ordinal, owner_sector) in fixed_owner_sectors.iter().copied().enumerate() { + let group_id = group_by_owner_sector + .get(&owner_sector) + .copied() + .ok_or_else(|| { + RusticolError::integrity( + "on-the-fly contracted owner has no component-zero group", + ) + })?; + let destination = plan.destination_by_group()[group_id as usize]; + let expected_destination = u32::try_from(owner_ordinal).map_err(|_| { + RusticolError::artifact("on-the-fly contracted owner ordinal exceeds u32") + })?; + if destination != expected_destination + || plan.ordered_group_ids().get(owner_ordinal).copied() != Some(group_id) + { + return Err(RusticolError::integrity( + "on-the-fly contracted destination order disagrees with the compact structural owner basis", + )); + } + destinations.push(destination); + } + destinations + } else { + // Symmetric-group groups are channel/permutation ordered. Their + // authenticated destination projection maps that order back to the + // compact selector-owner ordinals, so query construction consumes the + // inverse map (owner ordinal -> Fourier group ID). + let mut destinations = vec![u32::MAX; owner_count]; + for (group_id, owner_ordinal) in plan.destination_by_group().iter().copied().enumerate() { + let owner_ordinal = usize::try_from(owner_ordinal).map_err(|_| { + RusticolError::artifact("on-the-fly contracted owner ordinal exceeds usize") + })?; + let slot = destinations.get_mut(owner_ordinal).ok_or_else(|| { + RusticolError::integrity( + "on-the-fly symmetric-group destination is outside the owner domain", + ) + })?; + if *slot != u32::MAX { + return Err(RusticolError::integrity( + "on-the-fly symmetric-group destination projection is not one-to-one", + )); + } + *slot = u32::try_from(group_id).map_err(|_| { + RusticolError::artifact("on-the-fly symmetric-group group ID exceeds u32") + })?; + } + if destinations.contains(&u32::MAX) { + return Err(RusticolError::integrity( + "on-the-fly symmetric-group destination projection is incomplete", + )); + } + for (owner_ordinal, owner_sector) in fixed_owner_sectors.iter().copied().enumerate() { + let expected_group = group_by_owner_sector + .get(&owner_sector) + .copied() + .ok_or_else(|| { + RusticolError::integrity( + "on-the-fly contracted owner has no component-zero group", + ) + })?; + if destinations[owner_ordinal] != expected_group { + return Err(RusticolError::integrity( + "on-the-fly symmetric-group projection disagrees with the authenticated owner sectors", + )); + } + } + destinations + }; + let requested_point_tile_size = usize::try_from(requested_point_tile_size).map_err(|_| { + RusticolError::artifact("on-the-fly contracted point tile size exceeds usize") + })?; + let point_tile_size = match plan.runtime_reducer() { + Some(crate::recurrence::RuntimeColorContractionReducer::SymmetricGroupFourier(reducer)) => { + reducer.bounded_lane_capacity(requested_point_tile_size)? + } + _ => requested_point_tile_size, + }; + Ok(Some(LoadedOnTheFlyColorContractionV1 { + plan, + destination_by_owner_ordinal: destination_by_owner_ordinal.into_boxed_slice(), + point_tile_size, + })) +} + +fn load_process_seed( + artifact: &VerifiedArtifact, + evaluator_root: &Path, + manifest: &OnTheFlyExecutionManifest, +) -> RusticolResult { + load_process_seed_from_container( + artifact, + evaluator_root, + &manifest.key, + &manifest.runtime_container, + ) +} + +fn load_process_seed_from_container( + artifact: &VerifiedArtifact, + evaluator_root: &Path, + process_key: &str, + container: &OnTheFlyRuntimeContainer, +) -> RusticolResult { + let payloads = artifact.evaluator_payload_store(evaluator_root)?; + let logical_path = payloads.logical_path(&container.path)?; + let record = artifact.payload(&logical_path)?; + if record.role != PayloadRole::EvaluatorState + || record.media_type != "application/octet-stream" + || record.executable + || record.process_id.as_deref() != Some(process_key) + { + return Err(RusticolError::integrity( + "on-the-fly runtime container has the wrong authenticated payload role", + )); + } + let path = artifact.root().join(&logical_path); + let file = artifact.open_payload_file(&logical_path)?; + let expected_sha = decode_sha256(&record.sha256)?; + let reader = PacbinReader::open_file_with_sha256(file, &path, &expected_sha)?; + if reader.members().len() != 1 { + return Err(RusticolError::integrity( + "on-the-fly runtime container must contain exactly its compact process seed", + )); + } + let member = reader.member(ON_THE_FLY_PROCESS_SEED_MEMBER)?; + if member.logical_path() != container.seed_member_path + || member.kind() != PacbinMemberKind::OnTheFlyProcessSeed + { + return Err(RusticolError::integrity( + "on-the-fly runtime container does not contain the canonical process-seed member", + )); + } + decode_on_the_fly_process_seed_v1(reader.member_bytes(ON_THE_FLY_PROCESS_SEED_MEMBER)?) +} + +fn load_prepared_pack( + artifact: &VerifiedArtifact, + manifest: &OnTheFlyExecutionManifest, +) -> RusticolResult<(PreparedKernelPackManifest, PathBuf)> { + let path = confined_internal_path( + &manifest.kernel_pack.manifest_path, + "on-the-fly prepared kernel-pack manifest path", + )?; + let logical = path.to_str().ok_or_else(|| { + RusticolError::security("on-the-fly prepared kernel-pack path is not UTF-8") + })?; + if artifact.payload(logical)?.role != PayloadRole::EvaluatorManifest { + return Err(RusticolError::security( + "on-the-fly prepared kernel-pack manifest has the wrong payload role", + )); + } + let bytes = artifact.read_payload(logical)?; + let pack: PreparedKernelPackManifest = serde_json::from_slice(&bytes).map_err(|error| { + RusticolError::serialization(format!( + "could not parse on-the-fly prepared kernel pack: {error}" + )) + })?; + pack.validate()?; + validate_recurrence_prepared_pack_outer_target(&artifact.manifest().producer.target, &pack)?; + let payload_root = artifact.root().join(confined_internal_path( + &manifest.kernel_pack.payload_root, + "on-the-fly prepared kernel payload root", + )?); + Ok((pack, payload_root)) +} + +fn build_common_runtime( + manifest: &OnTheFlyExecutionManifest, +) -> RusticolResult<( + ExecutionRuntime, + Vec, + Vec, +)> { + let metadata = &manifest.runtime_metadata; + let model_parameters = metadata + .runtime_parameters + .iter() + .map(|parameter| GenericRuntimeModelParameterManifest { + name: parameter.name.clone(), + kind: parameter.kind.clone(), + parameter_index: parameter.parameter_index as usize, + default: parameter.default, + pdg: None, + runtime_name: parameter.runtime_name.clone(), + complex_component: parameter.complex_component.clone(), + }) + .collect::>(); + let model_parameter_runtime_slots = runtime_parameter_slots(&model_parameters)?; + let model_parameter_values_f64 = model_parameters + .iter() + .map(|parameter| parameter.default) + .collect::>(); + let model_parameter_name_to_index = model_parameters + .iter() + .map(|parameter| (parameter.name.clone(), parameter.parameter_index)) + .collect::>(); + let defaults = metadata + .prepared_parameter_defaults + .iter() + .map(|[real, imaginary]| crate::EagerComplex64::new(*real, *imaginary)) + .collect::>(); + let projection = metadata + .parameter_projection + .iter() + .filter_map(|row| row.prepared_parameter_id.map(|prepared| (row, prepared))) + .map(|(row, prepared)| { + Ok(PreparedParameterProjectionEntry { + runtime_slot: usize::try_from(row.runtime_slot).map_err(|_| { + RusticolError::artifact("on-the-fly runtime parameter slot exceeds usize") + })?, + prepared_slot: usize::try_from(prepared).map_err(|_| { + RusticolError::artifact("on-the-fly prepared parameter slot exceeds usize") + })?, + component: u8::try_from(row.component).map_err(|_| { + RusticolError::artifact("on-the-fly parameter component exceeds u8") + })?, + }) + }) + .collect::>>()?; + + let normalization = &metadata.normalization; + if !normalization.couplings_in_stage_evaluators { + return Err(RusticolError::compatibility( + "on-the-fly execution requires local vertex couplings in prepared kernel calls", + )); + } + let color_factor = if metadata + .color_contraction + .as_ref() + .is_some_and(|contraction| contraction.includes_color_factor) + { + 1.0 + } else { + normalization.color_factor + }; + let normalization_factor = color_factor * normalization.global_coupling_factor + / (normalization.average_factor * normalization.identical_factor); + if !normalization_factor.is_finite() { + return Err(RusticolError::integrity( + "on-the-fly runtime normalization is not finite", + )); + } + let particle_masses = metadata + .particle_masses + .iter() + .map(|row| (row.outgoing_pdg, row.mass)) + .collect::>(); + let external_count = metadata.external_legs.len(); + let common = ExecutionRuntime { + process: manifest.process.clone(), + key: manifest.key.clone(), + color_accuracy: manifest.color_accuracy.clone(), + external_pdg_order: manifest.external_pdg_order.clone(), + external_count, + parameter_count: model_parameters.len(), + value_parameter_count: 0, + momentum_parameter_count: 0, + current_count: 0, + source_count: external_count, + interaction_count: 0, + stage_count: 0, + amplitude_output_count: 0, + lc_topology_replay_enabled: false, + lc_topology_replay_mappings: Arc::new(Vec::new()), + lc_topology_replay_public_mappings: Vec::new(), + lc_topology_replay_routes: Vec::new(), + lc_topology_replay_materialized_sector_ids: BTreeSet::new(), + lc_resolved_replay_plan: None, + lc_resolved_replay_selection_cache: None, + lc_replay_flat_momenta_scratch: Vec::new(), + lc_replay_target_components_scratch: Vec::new(), + color_topology_replay_enabled: false, + color_topology_replay_mappings: Arc::new(Vec::new()), + color_replay_flat_momenta_scratch: Vec::new(), + helicity_recurrence: None, + compiled_helicity_execution_plan: None, + compiled_color_execution_plan: None, + compiled_direct_runtime: None, + compiled_direct_color_schedules: BTreeMap::new(), + compiled_direct_helicity_schedules: BTreeMap::new(), + helicity_sum_runtime: None, + helicity_selector_runtimes: Vec::new(), + helicity_selector_runtime_schedule_modes: Vec::new(), + helicity_selector_lane_by_domain: BTreeMap::new(), + color_selector_runtimes: BTreeMap::new(), + runtime_unavailable_message: None, + sources: Vec::new(), + momentum_slots: Vec::new(), + external_is_initial: metadata + .external_legs + .iter() + .map(|leg| leg.is_initial) + .collect(), + particle_masses, + particle_mass_parameter_names: BTreeMap::new(), + normalization_factor, + normalization_color_factor: color_factor, + normalization_average_factor: normalization.average_factor, + normalization_identical_factor: normalization.identical_factor, + normalization_qcd_coupling_power: normalization.qcd_coupling_power.unwrap_or(0) as usize, + normalization_electroweak_coupling_power: normalization + .electroweak_coupling_power + .unwrap_or(0) as usize, + model_parameters, + model_parameter_name_to_index, + model_parameter_runtime_slots, + model_parameter_values_f64, + model_parameter_evaluator: None, + physics_reduction_override: None, + physics: None, + stages: None, + amplitude_stage: None, + state_scratch_f64: Vec::new(), + state_scratch_f64_requires_clear: false, + values_scratch_f64: Vec::new(), + }; + Ok((common, defaults, projection)) +} + +#[cfg(test)] +mod tests { + use super::{ + LoadedOnTheFlyColorContractionV1, clamp_query_construction_threads, + symmetric_group_workspace_for_loaded_color, validate_recurrence_companion_process_digest, + }; + use crate::recurrence::RecurrenceColorContraction; + + #[test] + fn query_construction_threads_are_capped_by_positive_host_availability() { + assert_eq!(clamp_query_construction_threads(8, 3), 3); + assert_eq!(clamp_query_construction_threads(2, 8), 2); + assert_eq!(clamp_query_construction_threads(4, 0), 1); + } + + #[test] + fn cross_process_companion_seed_swap_is_rejected_by_primary_binding_digest() { + let primary = "10".repeat(32); + let swapped = "20".repeat(32); + validate_recurrence_companion_process_digest(&primary, &primary, &primary).unwrap(); + + for (companion, decoded) in [ + (&swapped, &swapped), + (&primary, &swapped), + (&swapped, &primary), + ] { + let error = validate_recurrence_companion_process_digest(&primary, companion, decoded) + .unwrap_err(); + assert!(error.to_string().contains("different primary process")); + } + } + + #[test] + fn symmetric_group_workspace_is_installed_at_the_authenticated_tile_capacity() { + let loaded = LoadedOnTheFlyColorContractionV1 { + plan: RecurrenceColorContraction::symmetric_group_s3_for_runtime_test( + (0..13).collect(), + 13, + ), + destination_by_owner_ordinal: (0..13).collect::>().into_boxed_slice(), + point_tile_size: 5, + }; + let workspace = symmetric_group_workspace_for_loaded_color(Some(&loaded)) + .unwrap() + .expect("symmetric-group workspace"); + assert_eq!(workspace.lane_capacity(), 5); + } +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/engine/on_the_fly_manifest.rs b/pyamplicol/rust/crates/rusticol-core/src/engine/on_the_fly_manifest.rs new file mode 100644 index 0000000000..1db590b7f6 --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/engine/on_the_fly_manifest.rs @@ -0,0 +1,1123 @@ +// SPDX-License-Identifier: 0BSD + +//! Compact execution-manifest validation for the private on-the-fly lane. +//! +//! The verified outer artifact owns file sizes and digests, while PACBIN owns +//! its member index and payload digests. This manifest therefore records only +//! the identities and canonical paths needed to select the lane. + +use super::recurrence_manifest::{ + RecurrenceColorContractionReference, RecurrenceExternalLeg, RecurrenceNormalization, + RecurrenceParameterProjection, RecurrenceParticleMass, RecurrenceRuntimeParameter, +}; +use super::{ + ON_THE_FLY_CONTRACTED_COLOR_RUNTIME_CAPABILITY, ON_THE_FLY_LC_COLOR_RUNTIME_CAPABILITY, + ON_THE_FLY_RUNTIME_CAPABILITY, SYMMETRIC_GROUP_FFT_COLOR_RUNTIME_CAPABILITY, + confined_internal_path, +}; +use crate::recurrence::RECURRENCE_COLOR_CONTRACTION_CODEC_ABI; +use crate::recurrence::on_the_fly::{ + ON_THE_FLY_PROCESS_SEED_IDENTITY_ABI, OnTheFlyProcessSeedIdentityV1, +}; +use crate::{ArtifactProcess, PROCESS_ARTIFACT_SCHEMA_VERSION, RusticolError, RusticolResult}; +use serde::Deserialize; +use std::collections::BTreeSet; + +pub(super) const ON_THE_FLY_EXECUTION_KIND: &str = "pyamplicol-runtime-on-the-fly-execution"; +pub(super) const ON_THE_FLY_RUNTIME_CONTAINER_KIND: &str = + "pyamplicol-on-the-fly-runtime-container"; +pub(super) const ON_THE_FLY_RUNTIME_CONTAINER_SCHEMA: u16 = 1; +pub(super) const ON_THE_FLY_RUNTIME_STORAGE_ABI: &str = "pacbin-v1"; +pub(super) const ON_THE_FLY_RUNTIME_CONTAINER_PATH: &str = "on-the-fly-runtime.pacbin"; +pub(super) const ON_THE_FLY_PROCESS_SEED_MEMBER: &str = "on-the-fly/process-seed-v1.bin"; +pub(super) const ON_THE_FLY_KERNEL_PACK_MANIFEST_PATH: &str = "model/eager-kernel-pack.json"; +pub(super) const ON_THE_FLY_KERNEL_PAYLOAD_ROOT: &str = "model/eager-kernels"; +pub(super) const ON_THE_FLY_COLOR_CONTRACTION_PATH: &str = "on-the-fly-color.bin"; +#[expect( + dead_code, + reason = "retained to reject legacy v1 companion manifests explicitly" +)] +pub(super) const RECURRENCE_HELICITY_SELECTOR_COMPANION_KIND: &str = + "pyamplicol-recurrence-helicity-selector-companion-v1"; + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(super) struct OnTheFlyExecutionManifest { + pub(super) schema_version: u32, + pub(super) kind: String, + pub(super) required_runtime_capabilities: Vec, + pub(super) process: String, + pub(super) key: String, + pub(super) color_accuracy: String, + pub(super) external_pdg_order: Vec, + pub(super) kernel_pack: OnTheFlyKernelPackReference, + pub(super) runtime_options: OnTheFlyRuntimeOptions, + pub(super) selector_policy: OnTheFlySelectorPolicy, + pub(super) runtime_metadata: OnTheFlyRuntimeMetadata, + pub(super) runtime_container: OnTheFlyRuntimeContainer, +} + +/// Irreducible process/runtime state shared with the established recurrence +/// lane. Dense public color-flow catalogs and source-template replicas do not +/// belong to an on-the-fly artifact: selectors and source dispatch are derived +/// from the compact seed. NLC/full add only a bounded reference to their loose +/// authenticated color-contraction payload. +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(super) struct OnTheFlyRuntimeMetadata { + pub(super) runtime_parameters: Vec, + pub(super) prepared_parameter_defaults: Vec<[f64; 2]>, + pub(super) parameter_projection: Vec, + pub(super) external_legs: Vec, + pub(super) particle_masses: Vec, + pub(super) normalization: RecurrenceNormalization, + pub(super) process_seed_identity: OnTheFlyProcessSeedIdentityV1, + #[serde(default)] + pub(super) color_contraction: Option, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(super) struct OnTheFlyKernelPackReference { + pub(super) manifest_path: String, + pub(super) payload_root: String, +} + +#[derive(Clone, Copy, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(super) struct OnTheFlyRuntimeOptions { + pub(super) point_tile_size: u32, + pub(super) query_construction_threads: u32, +} + +#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq)] +#[serde(rename_all = "kebab-case")] +pub(super) enum OnTheFlyColorCoverage { + Complete, + Contracted, +} + +#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq)] +#[serde(rename_all = "kebab-case")] +pub(super) enum OnTheFlyColorBasis { + #[default] + Trace, + Adjoint, +} + +/// Compact facts that affect the internal structural color basis but cannot be +/// reconstructed from external color roles alone. +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(super) struct OnTheFlySelectorPolicy { + pub(super) color_coverage: OnTheFlyColorCoverage, + #[serde(default)] + pub(super) color_basis: OnTheFlyColorBasis, + #[serde(default)] + pub(super) reference_color_word: Option>, + pub(super) trace_reflections_folded: bool, + pub(super) selector_census: OnTheFlySelectorCensus, +} + +#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq)] +#[serde(deny_unknown_fields)] +pub(super) struct OnTheFlySelectorCensus { + pub(super) physical_helicity_count: u64, + pub(super) physical_color_flow_count: u64, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(super) struct OnTheFlyRuntimeContainer { + pub(super) kind: String, + pub(super) schema_version: u16, + pub(super) storage_abi: String, + pub(super) path: String, + pub(super) seed_member_path: String, +} + +/// Lane-only OTF state embedded by a complete contracted recurrence artifact. +/// +/// Runtime parameters, external legs, normalization, and point tiling remain +/// owned by the enclosing recurrence manifest. The companion retains only +/// the compact seed identity and policy needed to construct a selected query +/// family plus its independent one-component color reducer. +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +#[expect( + dead_code, + reason = "retained to reject legacy v1 companion manifests explicitly" +)] +pub(super) struct OnTheFlyHelicitySelectorCompanionManifest { + pub(super) schema_version: u32, + pub(super) kind: String, + pub(super) process_digest: String, + pub(super) process_seed_identity: OnTheFlyProcessSeedIdentityV1, + pub(super) query_construction_threads: u32, + pub(super) selector_policy: OnTheFlySelectorPolicy, + pub(super) runtime_container: OnTheFlyRuntimeContainer, + pub(super) color_contraction: RecurrenceColorContractionReference, +} + +impl OnTheFlyHelicitySelectorCompanionManifest { + #[expect( + dead_code, + reason = "retained to reject legacy v1 companion manifests explicitly" + )] + pub(super) fn validate( + &self, + process_digest: Option<&str>, + color_accuracy: &str, + ) -> RusticolResult<()> { + if self.schema_version != 1 || self.kind != RECURRENCE_HELICITY_SELECTOR_COMPANION_KIND { + return Err(RusticolError::compatibility( + "unsupported recurrence helicity-selector companion; regenerate the artifact", + )); + } + if process_digest != Some(self.process_digest.as_str()) + || self.process_seed_identity.process_digest != self.process_digest + { + return Err(RusticolError::integrity( + "recurrence helicity-selector companion disagrees with the process digest", + )); + } + if self.query_construction_threads == 0 { + return Err(RusticolError::artifact( + "recurrence helicity-selector companion query thread count must be positive", + )); + } + if !matches!(color_accuracy, "nlc" | "full") { + return Err(RusticolError::integrity( + "only contracted recurrence may carry a helicity-selector companion", + )); + } + self.selector_policy.validate(color_accuracy)?; + self.runtime_container.validate()?; + validate_contracted_color_reference(&self.color_contraction, color_accuracy)?; + if self.color_contraction.component_count != 1 { + return Err(RusticolError::integrity( + "recurrence helicity-selector companion color payload must have one component", + )); + } + Ok(()) + } +} + +impl OnTheFlyExecutionManifest { + fn validate(&self, outer: &ArtifactProcess) -> RusticolResult<()> { + if self.schema_version != PROCESS_ARTIFACT_SCHEMA_VERSION + || self.kind != ON_THE_FLY_EXECUTION_KIND + { + return Err(RusticolError::compatibility(format!( + "unsupported on-the-fly execution kind {:?} schema {}; regenerate the artifact", + self.kind, self.schema_version + ))); + } + if self.process != outer.expression + || self.key != outer.id + || self.color_accuracy != outer.color_accuracy + || self.external_pdg_order != outer.external_pdgs + { + return Err(RusticolError::integrity(format!( + "on-the-fly execution manifest does not match outer process {:?}", + outer.id + ))); + } + let actual = self + .required_runtime_capabilities + .iter() + .map(String::as_str) + .collect::>(); + let color_capability = match self.color_accuracy.as_str() { + "lc" => ON_THE_FLY_LC_COLOR_RUNTIME_CAPABILITY, + "nlc" | "full" => ON_THE_FLY_CONTRACTED_COLOR_RUNTIME_CAPABILITY, + _ => { + return Err(RusticolError::compatibility(format!( + "unsupported on-the-fly color accuracy {:?}", + self.color_accuracy + ))); + } + }; + let uses_symmetric_group_fft = self + .runtime_metadata + .color_contraction + .as_ref() + .and_then(|reference| reference.factorization.as_ref()) + .is_some_and(|factorization| factorization.kind == "symmetric-group-fourier"); + let mut expected = BTreeSet::from([ON_THE_FLY_RUNTIME_CAPABILITY, color_capability]); + if uses_symmetric_group_fft { + expected.insert(SYMMETRIC_GROUP_FFT_COLOR_RUNTIME_CAPABILITY); + } + if actual.len() != self.required_runtime_capabilities.len() || actual != expected { + return Err(RusticolError::integrity(format!( + "on-the-fly execution must require exactly {expected:?}" + ))); + } + if self.kernel_pack.manifest_path != ON_THE_FLY_KERNEL_PACK_MANIFEST_PATH + || self.kernel_pack.payload_root != ON_THE_FLY_KERNEL_PAYLOAD_ROOT + { + return Err(RusticolError::security( + "on-the-fly prepared-kernel paths are not canonical", + )); + } + if self.runtime_options.point_tile_size == 0 { + return Err(RusticolError::artifact( + "on-the-fly point_tile_size must be positive", + )); + } + if self.runtime_options.query_construction_threads == 0 { + return Err(RusticolError::artifact( + "on-the-fly query_construction_threads must be positive", + )); + } + self.selector_policy.validate(&self.color_accuracy)?; + self.runtime_metadata + .validate(&self.external_pdg_order, &self.color_accuracy)?; + self.runtime_container.validate() + } + + pub(super) fn uses_contracted_color(&self) -> bool { + matches!(self.color_accuracy.as_str(), "nlc" | "full") + } +} + +impl OnTheFlySelectorPolicy { + pub(super) fn validate(&self, color_accuracy: &str) -> RusticolResult<()> { + if let Some(word) = &self.reference_color_word + && (word.is_empty() + || word.contains(&0) + || word.iter().copied().collect::>().len() != word.len()) + { + return Err(RusticolError::artifact( + "on-the-fly reference color word must contain unique positive external labels", + )); + } + self.selector_census.validate()?; + let coverage_matches = matches!( + (color_accuracy, self.color_coverage), + ("lc", OnTheFlyColorCoverage::Complete) + | ("nlc" | "full", OnTheFlyColorCoverage::Contracted) + ); + if !coverage_matches + || self.color_basis == OnTheFlyColorBasis::Adjoint + && self.color_coverage != OnTheFlyColorCoverage::Contracted + || self.color_coverage == OnTheFlyColorCoverage::Contracted + && (self.trace_reflections_folded + || self.selector_census.physical_color_flow_count != 1) + { + return Err(RusticolError::integrity( + "on-the-fly selector policy disagrees with its color accuracy", + )); + } + Ok(()) + } +} + +impl OnTheFlySelectorCensus { + fn validate(&self) -> RusticolResult<()> { + if self.physical_helicity_count == 0 || self.physical_color_flow_count == 0 { + return Err(RusticolError::artifact( + "on-the-fly selector census counts must be positive", + )); + } + Ok(()) + } + + pub(super) fn validate_against( + &self, + physical_helicity_count: usize, + physical_color_flow_count: usize, + ) -> RusticolResult<()> { + let helicities = u64::try_from(physical_helicity_count).map_err(|_| { + RusticolError::artifact("on-the-fly physical helicity count exceeds u64") + })?; + let color_flows = u64::try_from(physical_color_flow_count).map_err(|_| { + RusticolError::artifact("on-the-fly physical color-flow count exceeds u64") + })?; + if self.physical_helicity_count != helicities + || self.physical_color_flow_count != color_flows + { + return Err(RusticolError::integrity( + "on-the-fly selector census disagrees with the authenticated compact seed", + )); + } + Ok(()) + } +} + +impl OnTheFlyRuntimeMetadata { + fn validate(&self, external_pdgs: &[i32], color_accuracy: &str) -> RusticolResult<()> { + for (context, len) in [ + ("runtime parameters", self.runtime_parameters.len()), + ( + "prepared parameter defaults", + self.prepared_parameter_defaults.len(), + ), + ("parameter projection", self.parameter_projection.len()), + ("external legs", self.external_legs.len()), + ("particle masses", self.particle_masses.len()), + ] { + u32::try_from(len).map_err(|_| { + RusticolError::artifact(format!( + "on-the-fly {context} count exceeds the u32 identifier domain" + )) + })?; + } + if self + .prepared_parameter_defaults + .iter() + .flatten() + .any(|component| !component.is_finite()) + { + return Err(RusticolError::artifact( + "on-the-fly prepared-parameter defaults must be finite complex-f64 values", + )); + } + if self.runtime_parameters.len() != self.parameter_projection.len() { + return Err(RusticolError::integrity( + "on-the-fly runtime parameters do not cover the parameter projection", + )); + } + let mut previous_projection_key: Option<(&str, u32)> = None; + let mut current_name: Option<&str> = None; + let mut current_template = None; + let mut current_prepared = None; + for (runtime_slot, row) in self.parameter_projection.iter().enumerate() { + if row.runtime_slot as usize != runtime_slot + || row.runtime_name.is_empty() + || row.component > 1 + || row.parameter_template_id as usize >= self.prepared_parameter_defaults.len() + || row + .prepared_parameter_id + .is_some_and(|id| id as usize >= self.prepared_parameter_defaults.len()) + { + return Err(RusticolError::integrity( + "on-the-fly parameter projection is outside its prepared-parameter domain", + )); + } + let key = (row.runtime_name.as_str(), row.component); + if previous_projection_key.is_some_and(|previous| previous >= key) { + return Err(RusticolError::artifact( + "on-the-fly parameter projection is not in strict name/component order", + )); + } + previous_projection_key = Some(key); + if current_name == Some(row.runtime_name.as_str()) { + if row.component != 1 + || current_template != Some(row.parameter_template_id) + || current_prepared != Some(row.prepared_parameter_id) + { + return Err(RusticolError::integrity( + "on-the-fly complex parameter projection rows are inconsistent", + )); + } + } else { + if row.component != 0 { + return Err(RusticolError::artifact( + "on-the-fly parameter projection must begin at component zero", + )); + } + current_name = Some(&row.runtime_name); + current_template = Some(row.parameter_template_id); + current_prepared = Some(row.prepared_parameter_id); + } + } + let mut previous_runtime_name: Option<&str> = None; + let mut previous_kind: Option<&str> = None; + for (parameter_index, (parameter, projection)) in self + .runtime_parameters + .iter() + .zip(&self.parameter_projection) + .enumerate() + { + if parameter.parameter_index as usize != parameter_index + || parameter.parameter_index != projection.runtime_slot + || parameter.name.is_empty() + || parameter.kind.is_empty() + || !parameter.default.is_finite() + { + return Err(RusticolError::integrity( + "on-the-fly runtime parameter identity/default is invalid", + )); + } + match (¶meter.runtime_name, ¶meter.complex_component) { + (Some(runtime_name), Some(component)) => { + let expected_component = match projection.component { + 0 => "real", + 1 => "imag", + _ => unreachable!("projection component was validated"), + }; + if runtime_name.is_empty() + || component != expected_component + || projection.runtime_name != *runtime_name + || parameter.name != format!("{runtime_name}.{component}") + || (previous_runtime_name == Some(runtime_name) + && previous_kind != Some(parameter.kind.as_str())) + { + return Err(RusticolError::integrity( + "on-the-fly complex runtime parameter disagrees with its projection", + )); + } + previous_runtime_name = Some(runtime_name); + previous_kind = Some(¶meter.kind); + } + (None, None) => { + if projection.component != 0 || parameter.name != projection.runtime_name { + return Err(RusticolError::integrity( + "on-the-fly real runtime parameter disagrees with its projection", + )); + } + previous_runtime_name = None; + previous_kind = None; + } + _ => { + return Err(RusticolError::artifact( + "on-the-fly runtime_name and complex_component must appear together", + )); + } + } + if let Some(prepared_id) = projection.prepared_parameter_id + && parameter.default + != self.prepared_parameter_defaults[prepared_id as usize] + [projection.component as usize] + { + return Err(RusticolError::integrity( + "on-the-fly runtime parameter default disagrees with its prepared default", + )); + } + } + + if self.external_legs.len() != external_pdgs.len() { + return Err(RusticolError::integrity( + "on-the-fly external legs do not match the execution PDG order", + )); + } + let mut public_labels = BTreeSet::new(); + let mut saw_final = false; + for (source_slot, (leg, expected_pdg)) in + self.external_legs.iter().zip(external_pdgs).enumerate() + { + if leg.source_slot as usize != source_slot + || leg.public_label == 0 + || !public_labels.insert(leg.public_label) + || leg.physical_pdg != *expected_pdg + || (saw_final && leg.is_initial) + { + return Err(RusticolError::integrity( + "on-the-fly external-leg order or identity is not canonical", + )); + } + saw_final |= !leg.is_initial; + } + let mut previous_mass_pdg = None; + for row in &self.particle_masses { + if previous_mass_pdg.is_some_and(|previous| previous >= row.outgoing_pdg) + || !row.mass.is_finite() + || row.mass < 0.0 + { + return Err(RusticolError::artifact( + "on-the-fly particle masses are not strictly ordered finite nonnegative values", + )); + } + previous_mass_pdg = Some(row.outgoing_pdg); + } + let normalization = &self.normalization; + if normalization.color_accuracy != color_accuracy + || [ + normalization.color_factor, + normalization.average_factor, + normalization.identical_factor, + normalization.global_coupling_factor, + ] + .iter() + .any(|value| !value.is_finite()) + || normalization.average_factor <= 0.0 + || normalization.identical_factor <= 0.0 + || normalization.qcd_coupling_power.is_some() + != normalization.electroweak_coupling_power.is_some() + || normalization.coupling_policy.is_empty() + { + return Err(RusticolError::integrity( + "on-the-fly normalization metadata is invalid", + )); + } + match (color_accuracy, self.color_contraction.as_ref()) { + ("lc", None) => {} + ("nlc" | "full", Some(reference)) => { + validate_contracted_color_reference(reference, color_accuracy)?; + } + ("lc", Some(_)) => { + return Err(RusticolError::integrity( + "LC on-the-fly execution must not carry a color-contraction companion", + )); + } + ("nlc" | "full", None) => { + return Err(RusticolError::integrity( + "NLC/full on-the-fly execution requires a color-contraction companion", + )); + } + _ => { + return Err(RusticolError::compatibility( + "unsupported on-the-fly color accuracy", + )); + } + } + self.validate_process_seed_identity()?; + Ok(()) + } + + fn validate_process_seed_identity(&self) -> RusticolResult<()> { + let identity = &self.process_seed_identity; + if identity.abi != ON_THE_FLY_PROCESS_SEED_IDENTITY_ABI { + return Err(RusticolError::compatibility( + "unsupported on-the-fly process-seed identity ABI; regenerate the artifact", + )); + } + for digest in [ + &identity.process_digest, + &identity.compiled_model_digest, + &identity.recurrence_template_catalog_digest, + &identity.prepared_kernel_pack_digest, + &identity.recurrence_direct_template_catalog_digest, + &identity.semantic_digest, + ] { + if digest.len() != 64 + || !digest + .bytes() + .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte)) + { + return Err(RusticolError::integrity( + "on-the-fly process-seed identity contains an invalid SHA-256", + )); + } + } + if identity.external_permutation.len() != self.external_legs.len() + || identity.external_sources.len() != self.external_legs.len() + { + return Err(RusticolError::integrity( + "on-the-fly process-seed identity has the wrong external domain", + )); + } + let mut permutation = identity.external_permutation.clone(); + permutation.sort_unstable(); + if permutation + .iter() + .enumerate() + .any(|(index, slot)| *slot as usize != index) + { + return Err(RusticolError::integrity( + "on-the-fly process-seed identity permutation is not dense", + )); + } + for (source_slot, (source, leg)) in identity + .external_sources + .iter() + .zip(&self.external_legs) + .enumerate() + { + if source.source_slot as usize != source_slot + || source.public_label != leg.public_label + || source.is_initial != leg.is_initial + || source.states.is_empty() + || source + .states + .windows(2) + .any(|pair| pair[0].state_index >= pair[1].state_index) + { + return Err(RusticolError::integrity( + "on-the-fly process-seed source identity disagrees with external legs", + )); + } + } + Ok(()) + } +} + +fn validate_contracted_color_reference( + reference: &RecurrenceColorContractionReference, + color_accuracy: &str, +) -> RusticolResult<()> { + let digest_is_canonical = |value: &str| { + value.len() == 64 + && value + .bytes() + .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte)) + }; + if reference.abi != RECURRENCE_COLOR_CONTRACTION_CODEC_ABI + || reference.path != ON_THE_FLY_COLOR_CONTRACTION_PATH + { + return Err(RusticolError::compatibility( + "unsupported on-the-fly color-contraction payload contract", + )); + } + if reference.color_accuracy != color_accuracy + || !matches!(color_accuracy, "nlc" | "full") + || !matches!( + reference.storage.as_str(), + "expanded" | "convolution-kernels" + ) + || !reference.includes_color_factor + || reference.component_count != 1 + || reference.group_count == 0 + || reference.group_count != reference.active_sector_count + || reference.group_count != reference.destination_count + || reference.sector_count < reference.active_sector_count + || reference + .materialized_destination_count + .is_some_and(|count| count != reference.destination_count) + || reference.entry_count == 0 + || reference.logical_entry_count != reference.entry_count + || reference.size_bytes == 0 + || reference.sha256 != reference.semantic_digest + || !digest_is_canonical(&reference.sha256) + || !digest_is_canonical(&reference.semantic_digest) + || [ + reference.group_count, + reference.sector_count, + reference.active_sector_count, + reference.component_count, + reference.destination_count, + ] + .into_iter() + .any(|value| u32::try_from(value).is_err()) + { + return Err(RusticolError::integrity( + "on-the-fly color-contraction summary is inconsistent", + )); + } + let symmetric_group_fft = reference + .factorization + .as_ref() + .is_some_and(|factorization| { + factorization.kind == "symmetric-group-fourier" + && (2..=10).contains(&factorization.rank) + && factorization.coset_count > 0 + }); + if reference.factorization.is_some() != symmetric_group_fft + || (reference.storage == "convolution-kernels") != symmetric_group_fft + { + return Err(RusticolError::integrity( + "on-the-fly convolution-kernel storage requires symmetric-group Fourier factorization", + )); + } + if symmetric_group_fft { + let factorization = reference + .factorization + .as_ref() + .expect("validated symmetric-group factorization"); + let provenance = reference.fft_provenance.as_ref().ok_or_else(|| { + RusticolError::integrity( + "on-the-fly symmetric-group Fourier summary is missing FFT provenance", + ) + })?; + reference.validate_fft_provenance(factorization, provenance)?; + } else if reference.fft_provenance.is_some() { + return Err(RusticolError::integrity( + "on-the-fly non-FFT color summary carries FFT provenance", + )); + } + confined_internal_path(&reference.path, "on-the-fly color-contraction payload")?; + Ok(()) +} + +impl OnTheFlyRuntimeContainer { + pub(super) fn validate(&self) -> RusticolResult<()> { + if self.kind != ON_THE_FLY_RUNTIME_CONTAINER_KIND + || self.schema_version != ON_THE_FLY_RUNTIME_CONTAINER_SCHEMA + || self.storage_abi != ON_THE_FLY_RUNTIME_STORAGE_ABI + { + return Err(RusticolError::compatibility( + "unsupported on-the-fly runtime-container contract", + )); + } + if self.path != ON_THE_FLY_RUNTIME_CONTAINER_PATH + || self.seed_member_path != ON_THE_FLY_PROCESS_SEED_MEMBER + { + return Err(RusticolError::security( + "on-the-fly runtime container or seed path is not canonical", + )); + } + confined_internal_path(&self.path, "on-the-fly runtime container")?; + confined_internal_path(&self.seed_member_path, "on-the-fly process seed")?; + Ok(()) + } +} + +pub(super) fn parse_on_the_fly_execution_manifest( + bytes: &[u8], + outer: &ArtifactProcess, +) -> RusticolResult { + let manifest: OnTheFlyExecutionManifest = serde_json::from_slice(bytes).map_err(|error| { + RusticolError::serialization(format!( + "could not parse on-the-fly execution manifest: {error}" + )) + })?; + manifest.validate(outer)?; + Ok(manifest) +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::{Value, json}; + + fn outer_with_accuracy(color_accuracy: &str) -> ArtifactProcess { + let color_capability = if color_accuracy == "lc" { + ON_THE_FLY_LC_COLOR_RUNTIME_CAPABILITY + } else { + ON_THE_FLY_CONTRACTED_COLOR_RUNTIME_CAPABILITY + }; + ArtifactProcess { + id: "d_dbar_to_t_tbar_g_g".into(), + expression: "d d~ > t t~ g g".into(), + color_accuracy: color_accuracy.into(), + external_pdgs: vec![1, -1, 6, -6, 21, 21], + physics_path: "processes/d_dbar_to_t_tbar_g_g/physics.json".into(), + required_runtime_capabilities: vec![ + color_capability.into(), + ON_THE_FLY_RUNTIME_CAPABILITY.into(), + ], + aliases: Vec::new(), + } + } + + fn outer() -> ArtifactProcess { + outer_with_accuracy("lc") + } + + fn manifest() -> Value { + let outer = outer(); + json!({ + "schema_version": PROCESS_ARTIFACT_SCHEMA_VERSION, + "kind": ON_THE_FLY_EXECUTION_KIND, + "required_runtime_capabilities": [ + ON_THE_FLY_LC_COLOR_RUNTIME_CAPABILITY, + ON_THE_FLY_RUNTIME_CAPABILITY, + ], + "process": outer.expression, + "key": outer.id, + "color_accuracy": "lc", + "external_pdg_order": outer.external_pdgs, + "kernel_pack": { + "manifest_path": ON_THE_FLY_KERNEL_PACK_MANIFEST_PATH, + "payload_root": ON_THE_FLY_KERNEL_PAYLOAD_ROOT, + }, + "runtime_options": { + "point_tile_size": 64, + "query_construction_threads": 4 + }, + "selector_policy": { + "color_coverage": "complete", + "reference_color_word": null, + "trace_reflections_folded": false, + "selector_census": { + "physical_helicity_count": 64, + "physical_color_flow_count": 6 + } + }, + "runtime_metadata": { + "runtime_parameters": [], + "prepared_parameter_defaults": [], + "parameter_projection": [], + "external_legs": [ + {"source_slot": 0, "public_label": 1, "physical_pdg": 1, "outgoing_pdg": -1, "is_initial": true}, + {"source_slot": 1, "public_label": 2, "physical_pdg": -1, "outgoing_pdg": 1, "is_initial": true}, + {"source_slot": 2, "public_label": 3, "physical_pdg": 6, "outgoing_pdg": 6, "is_initial": false}, + {"source_slot": 3, "public_label": 4, "physical_pdg": -6, "outgoing_pdg": -6, "is_initial": false}, + {"source_slot": 4, "public_label": 5, "physical_pdg": 21, "outgoing_pdg": 21, "is_initial": false}, + {"source_slot": 5, "public_label": 6, "physical_pdg": 21, "outgoing_pdg": 21, "is_initial": false} + ], + "particle_masses": [], + "normalization": { + "color_accuracy": "lc", + "color_factor": 1.0, + "average_factor": 1.0, + "identical_factor": 1.0, + "global_coupling_factor": 1.0, + "qcd_coupling_power": 0, + "electroweak_coupling_power": 0, + "couplings_in_stage_evaluators": true, + "coupling_policy": "local" + }, + "process_seed_identity": { + "abi": ON_THE_FLY_PROCESS_SEED_IDENTITY_ABI, + "process_digest": "11".repeat(32), + "compiled_model_digest": "22".repeat(32), + "recurrence_template_catalog_digest": "33".repeat(32), + "prepared_kernel_pack_digest": "44".repeat(32), + "recurrence_direct_template_catalog_digest": "55".repeat(32), + "semantic_digest": "66".repeat(32), + "external_permutation": [0, 1, 2, 3, 4, 5], + "external_sources": [ + {"source_slot": 0, "public_label": 1, "is_initial": true, "states": [{"state_index": 0, "public_helicity": -1, "prepared_mass_parameter_slot": null}]}, + {"source_slot": 1, "public_label": 2, "is_initial": true, "states": [{"state_index": 0, "public_helicity": 1, "prepared_mass_parameter_slot": null}]}, + {"source_slot": 2, "public_label": 3, "is_initial": false, "states": [{"state_index": 0, "public_helicity": -1, "prepared_mass_parameter_slot": null}]}, + {"source_slot": 3, "public_label": 4, "is_initial": false, "states": [{"state_index": 0, "public_helicity": 1, "prepared_mass_parameter_slot": null}]}, + {"source_slot": 4, "public_label": 5, "is_initial": false, "states": [{"state_index": 0, "public_helicity": -1, "prepared_mass_parameter_slot": null}]}, + {"source_slot": 5, "public_label": 6, "is_initial": false, "states": [{"state_index": 0, "public_helicity": 1, "prepared_mass_parameter_slot": null}]} + ] + } + }, + "runtime_container": { + "kind": ON_THE_FLY_RUNTIME_CONTAINER_KIND, + "schema_version": ON_THE_FLY_RUNTIME_CONTAINER_SCHEMA, + "storage_abi": ON_THE_FLY_RUNTIME_STORAGE_ABI, + "path": ON_THE_FLY_RUNTIME_CONTAINER_PATH, + "seed_member_path": ON_THE_FLY_PROCESS_SEED_MEMBER, + }, + }) + } + + fn parse(value: &Value) -> RusticolResult { + parse_on_the_fly_execution_manifest(&serde_json::to_vec(value).unwrap(), &outer()) + } + + fn contracted_manifest(color_accuracy: &str) -> Value { + let mut value = manifest(); + value["color_accuracy"] = json!(color_accuracy); + value["required_runtime_capabilities"] = json!([ + ON_THE_FLY_CONTRACTED_COLOR_RUNTIME_CAPABILITY, + ON_THE_FLY_RUNTIME_CAPABILITY, + ]); + value["selector_policy"]["color_coverage"] = json!("contracted"); + value["selector_policy"]["selector_census"]["physical_color_flow_count"] = json!(1); + value["runtime_metadata"]["normalization"]["color_accuracy"] = json!(color_accuracy); + value["runtime_metadata"]["color_contraction"] = json!({ + "abi": RECURRENCE_COLOR_CONTRACTION_CODEC_ABI, + "path": ON_THE_FLY_COLOR_CONTRACTION_PATH, + "size_bytes": 256, + "sha256": "77".repeat(32), + "color_accuracy": color_accuracy, + "storage": "expanded", + "includes_color_factor": true, + "group_count": 6, + "sector_count": 36, + "active_sector_count": 6, + "component_count": 1, + "destination_count": 6, + "entry_count": 12, + "logical_entry_count": 12, + "semantic_digest": "77".repeat(32), + "factorization": null, + }); + value + } + + fn parse_contracted( + value: &Value, + color_accuracy: &str, + ) -> RusticolResult { + parse_on_the_fly_execution_manifest( + &serde_json::to_vec(value).unwrap(), + &outer_with_accuracy(color_accuracy), + ) + } + + fn symmetric_group_contracted_manifest(color_accuracy: &str) -> Value { + let mut value = contracted_manifest(color_accuracy); + value["required_runtime_capabilities"] = json!([ + ON_THE_FLY_CONTRACTED_COLOR_RUNTIME_CAPABILITY, + ON_THE_FLY_RUNTIME_CAPABILITY, + SYMMETRIC_GROUP_FFT_COLOR_RUNTIME_CAPABILITY, + ]); + let reference = &mut value["runtime_metadata"]["color_contraction"]; + reference["storage"] = json!("convolution-kernels"); + reference["factorization"] = json!({ + "kind": "symmetric-group-fourier", + "rank": 2, + "coset_count": 3, + }); + reference["fft_provenance"] = json!({ + "method": "symmetric-group-fourier", + "degree": 2, + "channel_count": 3, + "covered_local_group_count": 6, + "residual_group_count": 0, + "residual_entry_count": 0, + "raw_kernel_bytes": 192, + "transformed_kernel_bytes": 96, + "capability": SYMMETRIC_GROUP_FFT_COLOR_RUNTIME_CAPABILITY, + }); + value + } + + #[test] + fn accepts_only_the_compact_lc_contract() { + let parsed = parse(&manifest()).unwrap(); + assert_eq!(parsed.runtime_options.point_tile_size, 64); + assert_eq!(parsed.runtime_options.query_construction_threads, 4); + assert_eq!( + parsed.selector_policy.selector_census, + OnTheFlySelectorCensus { + physical_helicity_count: 64, + physical_color_flow_count: 6, + } + ); + assert_eq!( + parsed.runtime_container.seed_member_path, + ON_THE_FLY_PROCESS_SEED_MEMBER + ); + + let mut nlc = manifest(); + nlc["color_accuracy"] = json!("nlc"); + assert!(parse(&nlc).is_err()); + + let mut extra_capability = manifest(); + extra_capability["required_runtime_capabilities"] + .as_array_mut() + .unwrap() + .push(json!("unrelated.capability.v1")); + assert!(parse(&extra_capability).is_err()); + + let mut zero_census = manifest(); + zero_census["selector_policy"]["selector_census"]["physical_helicity_count"] = json!(0); + assert!(parse(&zero_census).is_err()); + + let mut incomplete_census = manifest(); + incomplete_census["selector_policy"]["selector_census"] + .as_object_mut() + .unwrap() + .remove("physical_color_flow_count"); + assert!(parse(&incomplete_census).is_err()); + + let mut zero_query_construction_threads = manifest(); + zero_query_construction_threads["runtime_options"]["query_construction_threads"] = json!(0); + assert!(parse(&zero_query_construction_threads).is_err()); + } + + #[test] + fn contracted_color_requires_the_canonical_companion_and_public_census() { + for accuracy in ["nlc", "full"] { + let value = contracted_manifest(accuracy); + let parsed = parse_contracted(&value, accuracy).unwrap(); + assert!(parsed.uses_contracted_color()); + assert_eq!( + parsed + .runtime_metadata + .color_contraction + .as_ref() + .unwrap() + .path, + ON_THE_FLY_COLOR_CONTRACTION_PATH + ); + + let mut materialized = value.clone(); + materialized["runtime_metadata"]["color_contraction"]["materialized_destination_count"] = + json!(6); + parse_contracted(&materialized, accuracy).unwrap(); + + materialized["runtime_metadata"]["color_contraction"]["materialized_destination_count"] = + json!(5); + assert!(parse_contracted(&materialized, accuracy).is_err()); + + let mut missing = value.clone(); + missing["runtime_metadata"] + .as_object_mut() + .unwrap() + .remove("color_contraction"); + assert!(parse_contracted(&missing, accuracy).is_err()); + + let mut lc_capability = value.clone(); + lc_capability["required_runtime_capabilities"][0] = + json!(ON_THE_FLY_LC_COLOR_RUNTIME_CAPABILITY); + assert!(parse_contracted(&lc_capability, accuracy).is_err()); + + let mut public_flows = value.clone(); + public_flows["selector_policy"]["selector_census"]["physical_color_flow_count"] = + json!(6); + assert!(parse_contracted(&public_flows, accuracy).is_err()); + + let mut sparse_destinations = value; + sparse_destinations["runtime_metadata"]["color_contraction"]["destination_count"] = + json!(7); + assert!(parse_contracted(&sparse_destinations, accuracy).is_err()); + } + + let mut lc_with_companion = manifest(); + lc_with_companion["runtime_metadata"]["color_contraction"] = + contracted_manifest("full")["runtime_metadata"]["color_contraction"].clone(); + assert!(parse(&lc_with_companion).is_err()); + } + + #[test] + fn symmetric_group_contracted_color_keeps_the_exact_otf_owner_domain() { + let value = symmetric_group_contracted_manifest("full"); + parse_contracted(&value, "full").unwrap(); + + let mut retained_alias_destination = value.clone(); + retained_alias_destination["runtime_metadata"]["color_contraction"]["destination_count"] = + json!(7); + assert!(parse_contracted(&retained_alias_destination, "full").is_err()); + + let mut missing_capability = value.clone(); + missing_capability["required_runtime_capabilities"] = json!([ + ON_THE_FLY_CONTRACTED_COLOR_RUNTIME_CAPABILITY, + ON_THE_FLY_RUNTIME_CAPABILITY, + ]); + assert!(parse_contracted(&missing_capability, "full").is_err()); + + let mut stale_transformed_size = value; + stale_transformed_size["runtime_metadata"]["color_contraction"]["fft_provenance"]["transformed_kernel_bytes"] = + json!(88); + assert!(parse_contracted(&stale_transformed_size, "full").is_err()); + } + + #[test] + fn adjoint_basis_is_explicit_and_limited_to_contracted_color() { + assert_eq!( + parse(&manifest()).unwrap().selector_policy.color_basis, + OnTheFlyColorBasis::Trace + ); + for accuracy in ["nlc", "full"] { + let mut value = symmetric_group_contracted_manifest(accuracy); + value["selector_policy"]["color_basis"] = json!("adjoint"); + assert_eq!( + parse_contracted(&value, accuracy) + .unwrap() + .selector_policy + .color_basis, + OnTheFlyColorBasis::Adjoint + ); + value["selector_policy"]["color_basis"] = json!("unknown"); + assert!(parse_contracted(&value, accuracy).is_err()); + } + let mut lc = manifest(); + lc["selector_policy"]["color_basis"] = json!("adjoint"); + assert!(parse(&lc).is_err()); + } + + #[test] + fn selector_census_fails_closed_against_authenticated_adapter_counts() { + let census = parse(&manifest()).unwrap().selector_policy.selector_census; + census.validate_against(64, 6).unwrap(); + assert!(census.validate_against(63, 6).is_err()); + assert!(census.validate_against(64, 5).is_err()); + } + + #[test] + fn rejects_noncanonical_runtime_and_kernel_paths() { + let mut runtime_escape = manifest(); + runtime_escape["runtime_container"]["seed_member_path"] = json!("../seed.bin"); + assert!(parse(&runtime_escape).is_err()); + + let mut kernel_alias = manifest(); + kernel_alias["kernel_pack"]["manifest_path"] = json!("model/other.json"); + assert!(parse(&kernel_alias).is_err()); + } + + #[test] + fn rejects_unbound_process_seed_identity_metadata() { + let mut stale_abi = manifest(); + stale_abi["runtime_metadata"]["process_seed_identity"]["abi"] = json!("stale-v0"); + assert!(parse(&stale_abi).is_err()); + + let mut changed_anchor = manifest(); + changed_anchor["runtime_metadata"]["process_seed_identity"]["external_sources"][0]["public_label"] = + json!(9); + assert!(parse(&changed_anchor).is_err()); + + let mut unknown_field = manifest(); + unknown_field["runtime_metadata"]["process_seed_identity"]["opaque"] = json!(true); + assert!(parse(&unknown_field).is_err()); + } +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/engine/on_the_fly_public_metadata.rs b/pyamplicol/rust/crates/rusticol-core/src/engine/on_the_fly_public_metadata.rs new file mode 100644 index 0000000000..43dd2f8568 --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/engine/on_the_fly_public_metadata.rs @@ -0,0 +1,481 @@ +// SPDX-License-Identifier: 0BSD + +//! Compact public metadata for the on-the-fly lane. +//! +//! Ordinary evaluation needs only this O(externals + parameters) payload. +//! The complete public helicity axis and LC color axis remain in the compact +//! selector adapter and are materialized only when the established physics +//! metadata API is explicitly requested. NLC/full expose one contracted color. + +use super::on_the_fly_manifest::OnTheFlyExecutionManifest; +use super::on_the_fly_selectors::{ + OnTheFlyCompactSelectorAdapterV1, OnTheFlySelectorIntrospectionCacheV1, +}; +use crate::{ + ArtifactProcess, ColorAccuracy, ColorComponent, ContractedColor, Coverage, ExternalParticle, + Helicity, LcColorFlow, ModelParameter, ParameterKind, ProcessPhysics as ProcessPhysicsV1, + Reduction, ReductionKind, RusticolError, RusticolResult, SelectorCapabilities, +}; +use serde::Deserialize; +use std::collections::{BTreeMap, BTreeSet}; + +const ON_THE_FLY_PUBLIC_METADATA_KIND: &str = "pyamplicol-on-the-fly-public-metadata"; +const ON_THE_FLY_PUBLIC_METADATA_SCHEMA: u32 = 1; + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(super) struct OnTheFlyPublicMetadataV1 { + schema_version: u32, + kind: String, + process_id: String, + process: String, + color_accuracy: ColorAccuracy, + external_particles: Vec, + model_parameters: Vec, +} + +impl OnTheFlyPublicMetadataV1 { + #[cfg(test)] + pub(super) fn for_test( + process_id: &str, + process: &str, + external_particles: Vec, + ) -> Self { + Self { + schema_version: ON_THE_FLY_PUBLIC_METADATA_SCHEMA, + kind: ON_THE_FLY_PUBLIC_METADATA_KIND.to_string(), + process_id: process_id.to_string(), + process: process.to_string(), + color_accuracy: ColorAccuracy::Lc, + external_particles, + model_parameters: Vec::new(), + } + } + + fn validate( + &self, + path: &str, + outer: &ArtifactProcess, + manifest: &OnTheFlyExecutionManifest, + ) -> RusticolResult<()> { + if self.schema_version != ON_THE_FLY_PUBLIC_METADATA_SCHEMA + || self.kind != ON_THE_FLY_PUBLIC_METADATA_KIND + { + return Err(RusticolError::compatibility(format!( + "unsupported on-the-fly public metadata kind {:?} schema {} at {path:?}; regenerate the artifact", + self.kind, self.schema_version + ))); + } + if self.process_id != outer.id + || self.process != outer.expression + || self.process_id != manifest.key + || self.process != manifest.process + || self.color_accuracy.as_str() != outer.color_accuracy + || self.color_accuracy.as_str() != manifest.color_accuracy + { + return Err(RusticolError::integrity(format!( + "on-the-fly public metadata {path:?} does not match representative process {:?}", + outer.id + ))); + } + if self.external_particles.len() != outer.external_pdgs.len() + || self.external_particles.len() != manifest.runtime_metadata.external_legs.len() + || self.external_particles.len() < 3 + { + return Err(RusticolError::integrity(format!( + "on-the-fly public metadata {path:?} has inconsistent external-particle coverage" + ))); + } + let expected_components = ["E", "px", "py", "pz"]; + for (index, ((particle, expected_pdg), runtime_leg)) in self + .external_particles + .iter() + .zip(&outer.external_pdgs) + .zip(&manifest.runtime_metadata.external_legs) + .enumerate() + { + let expected_role = if runtime_leg.is_initial { + crate::ParticleRole::Initial + } else { + crate::ParticleRole::Final + }; + if particle.index != index + || particle.label != index + 1 + || particle.momentum_slot != index + || particle.particle.is_empty() + || particle.pdg != *expected_pdg + || particle.pdg != runtime_leg.physical_pdg + || particle.role != expected_role + || particle + .momentum_components + .iter() + .map(String::as_str) + .ne(expected_components) + { + return Err(RusticolError::integrity(format!( + "on-the-fly public metadata {path:?} has invalid external particle {index}" + ))); + } + } + let mut parameter_names = BTreeSet::new(); + for parameter in &self.model_parameters { + if parameter.name.is_empty() + || !parameter_names.insert(parameter.name.as_str()) + || !parameter.default_real.is_finite() + || !parameter.default_imaginary.is_finite() + || (parameter.kind == ParameterKind::Derived && parameter.mutable) + { + return Err(RusticolError::artifact(format!( + "on-the-fly public metadata {path:?} has invalid model parameter {:?}", + parameter.name + ))); + } + } + Ok(()) + } + + /// Materialize the existing public physics schema only for explicit + /// metadata access. Every selector is executed directly by this lane, so + /// each public member is its own exact representative with unit weight. + pub(super) fn synthesize( + &self, + selectors: &OnTheFlyCompactSelectorAdapterV1, + ) -> RusticolResult { + let mut introspection = OnTheFlySelectorIntrospectionCacheV1::default(); + let helicities = introspection + .helicities(selectors)? + .iter() + .map(|record| Helicity { + id: record.id.clone(), + index: record.index, + values: record.values.to_vec(), + computed: true, + structural_zero: false, + representative_id: record.id.clone(), + coefficient: 1.0, + }) + .collect(); + let (color_components, coverage_color, coverage_color_kind, reduction, selectors_metadata) = + match self.color_accuracy { + ColorAccuracy::Lc => { + let color_components = introspection + .colors(selectors)? + .iter() + .map(|record| { + Ok(ColorComponent::LcFlow(LcColorFlow { + id: record.id.clone(), + index: record.index, + word: record + .word + .iter() + .copied() + .map(|label| { + usize::try_from(label).map_err(|_| { + RusticolError::artifact( + "on-the-fly public color label exceeds usize", + ) + }) + }) + .collect::>>()?, + computed: true, + representative_id: record.id.clone(), + coefficient: 1.0, + })) + }) + .collect::>>()?; + ( + color_components, + "complete".to_string(), + "physical-lc-flows".to_string(), + Reduction { + kind: ReductionKind::LcDiagonal, + groups: Vec::new(), + }, + SelectorCapabilities { + helicity: true, + color_flow: true, + contracted_color: false, + }, + ) + } + ColorAccuracy::Nlc | ColorAccuracy::Full => ( + vec![ColorComponent::ContractedColor(ContractedColor { + id: "color:contracted".to_string(), + index: 0, + description: + "coherent sparse contraction of the complete ordered color basis" + .to_string(), + })], + "contracted".to_string(), + "contracted-color".to_string(), + Reduction { + kind: ReductionKind::ContractedColor, + groups: Vec::new(), + }, + SelectorCapabilities { + helicity: true, + color_flow: false, + contracted_color: false, + }, + ), + }; + let physics = ProcessPhysicsV1 { + schema_version: crate::RUNTIME_PHYSICS_SCHEMA_VERSION, + kind: "pyamplicol-resolved-physics".to_string(), + process_id: self.process_id.clone(), + process: self.process.clone(), + color_accuracy: self.color_accuracy, + coverage: Coverage { + helicities: "complete".to_string(), + color: coverage_color, + color_kind: coverage_color_kind, + structural_zero_helicity_count: 0, + }, + external_particles: self.external_particles.clone(), + helicities, + color_components, + // Unlike eager/recurrence, the on-the-fly lane has no + // pre-materialized public quotient groups to hydrate here. + reduction, + model_parameters: self.model_parameters.clone(), + selectors: selectors_metadata, + extensions: BTreeMap::new(), + }; + physics.validate()?; + Ok(physics) + } +} + +pub(super) fn parse_on_the_fly_public_metadata( + bytes: &[u8], + path: &str, + outer: &ArtifactProcess, + manifest: &OnTheFlyExecutionManifest, +) -> RusticolResult { + let metadata: OnTheFlyPublicMetadataV1 = serde_json::from_slice(bytes).map_err(|error| { + RusticolError::serialization(format!( + "could not parse on-the-fly public metadata {path:?}: {error}" + )) + })?; + metadata.validate(path, outer, manifest)?; + Ok(metadata) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::engine::on_the_fly_manifest::{ + ON_THE_FLY_EXECUTION_KIND, ON_THE_FLY_KERNEL_PACK_MANIFEST_PATH, + ON_THE_FLY_KERNEL_PAYLOAD_ROOT, ON_THE_FLY_PROCESS_SEED_MEMBER, + ON_THE_FLY_RUNTIME_CONTAINER_KIND, ON_THE_FLY_RUNTIME_CONTAINER_PATH, + ON_THE_FLY_RUNTIME_CONTAINER_SCHEMA, ON_THE_FLY_RUNTIME_STORAGE_ABI, + parse_on_the_fly_execution_manifest, + }; + use crate::engine::{ON_THE_FLY_LC_COLOR_RUNTIME_CAPABILITY, ON_THE_FLY_RUNTIME_CAPABILITY}; + use serde_json::{Value, json}; + + fn outer_with_accuracy(color_accuracy: &str) -> ArtifactProcess { + ArtifactProcess { + id: "d_dbar_to_z".into(), + expression: "d d~ > z".into(), + color_accuracy: color_accuracy.into(), + external_pdgs: vec![1, -1, 23], + physics_path: "processes/d_dbar_to_z/physics.json".into(), + required_runtime_capabilities: vec![ + ON_THE_FLY_LC_COLOR_RUNTIME_CAPABILITY.into(), + ON_THE_FLY_RUNTIME_CAPABILITY.into(), + ], + aliases: Vec::new(), + } + } + + fn outer() -> ArtifactProcess { + outer_with_accuracy("lc") + } + + fn execution() -> OnTheFlyExecutionManifest { + let outer = outer(); + let value = json!({ + "schema_version": crate::PROCESS_ARTIFACT_SCHEMA_VERSION, + "kind": ON_THE_FLY_EXECUTION_KIND, + "required_runtime_capabilities": [ + ON_THE_FLY_LC_COLOR_RUNTIME_CAPABILITY, + ON_THE_FLY_RUNTIME_CAPABILITY, + ], + "process": outer.expression, + "key": outer.id, + "color_accuracy": "lc", + "external_pdg_order": outer.external_pdgs, + "kernel_pack": { + "manifest_path": ON_THE_FLY_KERNEL_PACK_MANIFEST_PATH, + "payload_root": ON_THE_FLY_KERNEL_PAYLOAD_ROOT, + }, + "runtime_options": { + "point_tile_size": 64, + "query_construction_threads": 1 + }, + "selector_policy": { + "color_coverage": "complete", + "reference_color_word": null, + "trace_reflections_folded": false, + "selector_census": { + "physical_helicity_count": 1, + "physical_color_flow_count": 1, + }, + }, + "runtime_metadata": { + "runtime_parameters": [], + "prepared_parameter_defaults": [], + "parameter_projection": [], + "external_legs": [ + {"source_slot": 0, "public_label": 1, "physical_pdg": 1, "outgoing_pdg": -1, "is_initial": true}, + {"source_slot": 1, "public_label": 2, "physical_pdg": -1, "outgoing_pdg": 1, "is_initial": true}, + {"source_slot": 2, "public_label": 3, "physical_pdg": 23, "outgoing_pdg": 23, "is_initial": false}, + ], + "particle_masses": [], + "normalization": { + "color_accuracy": "lc", + "color_factor": 1.0, + "average_factor": 1.0, + "identical_factor": 1.0, + "global_coupling_factor": 1.0, + "qcd_coupling_power": 0, + "electroweak_coupling_power": 0, + "couplings_in_stage_evaluators": true, + "coupling_policy": "local", + }, + "process_seed_identity": { + "abi": crate::recurrence::on_the_fly::ON_THE_FLY_PROCESS_SEED_IDENTITY_ABI, + "process_digest": "11".repeat(32), + "compiled_model_digest": "22".repeat(32), + "recurrence_template_catalog_digest": "33".repeat(32), + "prepared_kernel_pack_digest": "44".repeat(32), + "recurrence_direct_template_catalog_digest": "55".repeat(32), + "semantic_digest": "66".repeat(32), + "external_permutation": [0, 1, 2], + "external_sources": [ + {"source_slot": 0, "public_label": 1, "is_initial": true, "states": [{"state_index": 0, "public_helicity": -1, "prepared_mass_parameter_slot": null}]}, + {"source_slot": 1, "public_label": 2, "is_initial": true, "states": [{"state_index": 0, "public_helicity": 1, "prepared_mass_parameter_slot": null}]}, + {"source_slot": 2, "public_label": 3, "is_initial": false, "states": [{"state_index": 0, "public_helicity": 0, "prepared_mass_parameter_slot": null}]} + ] + }, + }, + "runtime_container": { + "kind": ON_THE_FLY_RUNTIME_CONTAINER_KIND, + "schema_version": ON_THE_FLY_RUNTIME_CONTAINER_SCHEMA, + "storage_abi": ON_THE_FLY_RUNTIME_STORAGE_ABI, + "path": ON_THE_FLY_RUNTIME_CONTAINER_PATH, + "seed_member_path": ON_THE_FLY_PROCESS_SEED_MEMBER, + }, + }); + parse_on_the_fly_execution_manifest(&serde_json::to_vec(&value).unwrap(), &outer).unwrap() + } + + fn compact() -> Value { + json!({ + "schema_version": ON_THE_FLY_PUBLIC_METADATA_SCHEMA, + "kind": ON_THE_FLY_PUBLIC_METADATA_KIND, + "process_id": "d_dbar_to_z", + "process": "d d~ > z", + "color_accuracy": "lc", + "external_particles": [ + {"index": 0, "label": 1, "particle": "d", "pdg": 1, "role": "initial", "momentum_slot": 0, "momentum_components": ["E", "px", "py", "pz"]}, + {"index": 1, "label": 2, "particle": "d~", "pdg": -1, "role": "initial", "momentum_slot": 1, "momentum_components": ["E", "px", "py", "pz"]}, + {"index": 2, "label": 3, "particle": "z", "pdg": 23, "role": "final", "momentum_slot": 2, "momentum_components": ["E", "px", "py", "pz"]}, + ], + "model_parameters": [ + {"name": "alpha_s", "kind": "external", "default_real": 0.118, "default_imaginary": 0.0, "mutable": true}, + ], + }) + } + + fn parse(value: &Value) -> RusticolResult { + parse_on_the_fly_public_metadata( + &serde_json::to_vec(value).unwrap(), + &outer().physics_path, + &outer(), + &execution(), + ) + } + + fn parse_contracted( + value: &Value, + color_accuracy: &str, + ) -> RusticolResult { + let outer = outer_with_accuracy(color_accuracy); + let mut manifest = execution(); + manifest.color_accuracy = color_accuracy.to_string(); + parse_on_the_fly_public_metadata( + &serde_json::to_vec(value).unwrap(), + &outer.physics_path, + &outer, + &manifest, + ) + } + + #[test] + fn compact_metadata_authenticates_identity_externals_and_parameters() { + parse(&compact()).unwrap(); + + let mut cases = Vec::new(); + let mut wrong_kind = compact(); + wrong_kind["kind"] = json!("pyamplicol-resolved-physics"); + cases.push(wrong_kind); + let mut wrong_schema = compact(); + wrong_schema["schema_version"] = json!(2); + cases.push(wrong_schema); + let mut wrong_id = compact(); + wrong_id["process_id"] = json!("other"); + cases.push(wrong_id); + let mut wrong_process = compact(); + wrong_process["process"] = json!("d d~ > a"); + cases.push(wrong_process); + let mut wrong_accuracy = compact(); + wrong_accuracy["color_accuracy"] = json!("nlc"); + cases.push(wrong_accuracy); + + for invalid in cases { + assert!(parse(&invalid).is_err()); + } + } + + #[test] + fn compact_metadata_accepts_contracted_nlc_and_full_accuracy() { + for accuracy in ["nlc", "full"] { + let mut value = compact(); + value["color_accuracy"] = json!(accuracy); + let parsed = parse_contracted(&value, accuracy).unwrap(); + assert_eq!(parsed.color_accuracy.as_str(), accuracy); + + value["color_accuracy"] = json!("lc"); + assert!(parse_contracted(&value, accuracy).is_err()); + } + } + + #[test] + fn compact_metadata_tampering_fails_closed() { + let mut cases = Vec::new(); + let mut wrong_pdg = compact(); + wrong_pdg["external_particles"][2]["pdg"] = json!(22); + cases.push(wrong_pdg); + let mut wrong_role = compact(); + wrong_role["external_particles"][1]["role"] = json!("final"); + cases.push(wrong_role); + let mut wrong_slot = compact(); + wrong_slot["external_particles"][2]["momentum_slot"] = json!(1); + cases.push(wrong_slot); + let mut wrong_components = compact(); + wrong_components["external_particles"][0]["momentum_components"][0] = json!("energy"); + cases.push(wrong_components); + let mut invalid_parameter = compact(); + invalid_parameter["model_parameters"][0]["kind"] = json!("derived"); + cases.push(invalid_parameter); + let mut unknown_field = compact(); + unknown_field["dense_helicities"] = json!([]); + cases.push(unknown_field); + + for invalid in cases { + assert!(parse(&invalid).is_err()); + } + } +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/engine/on_the_fly_selectors.rs b/pyamplicol/rust/crates/rusticol-core/src/engine/on_the_fly_selectors.rs new file mode 100644 index 0000000000..d8bc4e5320 --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/engine/on_the_fly_selectors.rs @@ -0,0 +1,2034 @@ +// SPDX-License-Identifier: 0BSD + +//! Compact public-selector adapter for the private LC on-the-fly lane. +//! +//! The established runtime API addresses helicities as `h:+1,-1,...` and LC +//! components as `flow:` (or `flow:singlet`). This module +//! preserves those strings without loading a process-wide physics table. It +//! snapshots only O(external-state) seed data and derives individual axis +//! members by ordinal when a selector or explicit introspection call needs +//! them. +//! +//! Complete LC axes are factorial for general processes. Accordingly, +//! [`OnTheFlyLazySelectionV1`] is a lazy materializer: load, total evaluation, +//! profiling, and benchmarking need not create dense helicity/color metadata. +//! A caller which genuinely requests resolved metadata may explicitly +//! materialize it through [`OnTheFlySelectorIntrospectionCacheV1`]. + +use super::on_the_fly_lane::{OnTheFlyLcQueryRequestV1, OnTheFlyLcReductionTargetV1}; +use super::on_the_fly_manifest::OnTheFlyColorBasis; +use crate::recurrence::on_the_fly::{ + DecodedLcQueryV1, OnTheFlyExternalColorRoleV1, OnTheFlyLcSelectorV1, OnTheFlyProcessSeedV1, +}; +use crate::{RusticolError, RusticolResult}; +use std::collections::{BTreeMap, BTreeSet}; + +fn invalid(message: impl Into) -> RusticolError { + RusticolError::selector(format!("on-the-fly selector: {}", message.into())) +} + +fn integrity(message: impl Into) -> RusticolError { + RusticolError::integrity(format!("on-the-fly selector: {}", message.into())) +} + +#[derive(Clone, Debug, Eq, PartialEq)] +struct CompactExternalV1 { + source_slot: u32, + public_slot: u32, + public_label: u32, + is_initial: bool, + color_role: OnTheFlyExternalColorRoleV1, + public_helicities: Box<[i32]>, +} + +/// Compact seed-derived external record. Particle names and PDGs remain +/// owned by the existing process-selection metadata; this structural record +/// supplies the selector-relevant half of that existing API on demand. +#[derive(Clone, Debug, Eq, PartialEq)] +pub(super) struct OnTheFlyExternalSelectorRecordV1 { + pub(super) source_slot: u32, + pub(super) public_slot: u32, + pub(super) public_label: u32, + pub(super) is_initial: bool, + pub(super) color_role: OnTheFlyExternalColorRoleV1, +} + +/// One exact established public helicity ID, materialized only on request. +#[derive(Clone, Debug, Eq, PartialEq)] +pub(super) struct OnTheFlyHelicitySelectorRecordV1 { + pub(super) id: String, + pub(super) index: usize, + pub(super) values: Box<[i32]>, +} + +/// One exact established public LC color ID, materialized only on request. +#[derive(Clone, Debug, Eq, PartialEq)] +pub(super) struct OnTheFlyColorSelectorRecordV1 { + pub(super) id: String, + pub(super) index: usize, + pub(super) word: Box<[u32]>, + pub(super) selector: OnTheFlyLcSelectorV1, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum CompactColorDomainV1 { + Singlet, + SingleTrace, + OpenLines, +} + +/// Exact compact coverage marker supplied by generation. Incomplete color +/// coverage would require retaining the selected dense flow list, defeating +/// this lane's process-seed contract, so it is rejected at construction. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(super) enum OnTheFlyLcColorCoverageV1 { + Complete, + #[expect( + dead_code, + reason = "retained as the explicit rejected compatibility sentinel" + )] + Incomplete, +} + +/// Small generation-owned selector policy which cannot be reconstructed from +/// external roles. `reference_color_word` may reorder one ordinary complete +/// flow to the front; reflection folding changes pure-trace public ordering +/// while preserving both members as public aliases. Contracted adjoint colour +/// instead retains ordinary trace amplitudes with two fixed DDM endpoints. +#[derive(Clone, Debug, Eq, PartialEq)] +pub(super) struct OnTheFlyLcSelectorPolicyV1 { + pub(super) color_coverage: OnTheFlyLcColorCoverageV1, + pub(super) color_basis: OnTheFlyColorBasis, + pub(super) reference_color_word: Option>, + pub(super) trace_reflections_folded: bool, +} + +impl OnTheFlyLcSelectorPolicyV1 { + #[cfg(test)] + pub(super) fn complete( + reference_color_word: Option>, + trace_reflections_folded: bool, + ) -> Self { + Self { + color_coverage: OnTheFlyLcColorCoverageV1::Complete, + color_basis: OnTheFlyColorBasis::Trace, + reference_color_word: reference_color_word.map(Vec::into_boxed_slice), + trace_reflections_folded, + } + } +} + +type OnTheFlySelectedOrdinalsV1 = (Option>, Option>); + +/// O(external-state) selector owner retained beside the private native lane. +#[derive(Clone, Debug)] +pub(super) struct OnTheFlyCompactSelectorAdapterV1 { + external_permutation: Box<[u32]>, + representative_to_public: Box<[u32]>, + public_to_representative: Box<[u32]>, + public_label_by_representative_label: BTreeMap, + representative_label_by_public_label: BTreeMap, + externals_by_public_slot: Box<[CompactExternalV1]>, + public_slot_by_label: BTreeMap, + fundamental_labels: Box<[u32]>, + antifundamental_labels: Box<[u32]>, + adjoint_labels: Box<[u32]>, + color_domain: CompactColorDomainV1, + color_basis: OnTheFlyColorBasis, + reference_unfolded_color_ordinal: Option, + trace_reflections_folded: bool, + helicity_count: usize, + color_count: usize, +} + +impl OnTheFlyCompactSelectorAdapterV1 { + /// Snapshot only selector-relevant seed data. The accessors used here are + /// intentionally narrow; they do not expose the recurrence catalogs or a + /// dense public-flow table. + pub(super) fn from_seed( + seed: &OnTheFlyProcessSeedV1, + policy: OnTheFlyLcSelectorPolicyV1, + ) -> RusticolResult { + if policy.color_coverage != OnTheFlyLcColorCoverageV1::Complete { + return Err(invalid( + "on-the-fly LC requires complete public color coverage", + )); + } + let external_permutation = seed.external_permutation(); + let anchors = seed.source_anchors(); + if external_permutation.len() != anchors.len() { + return Err(integrity( + "external permutation and source-anchor domains differ", + )); + } + + let mut by_public = vec![None; anchors.len()]; + let mut public_slot_by_label = BTreeMap::new(); + for anchor in anchors { + let public_slot = *external_permutation + .get(anchor.source_slot() as usize) + .ok_or_else(|| integrity("source anchor is outside the gather map"))?; + let destination = by_public + .get_mut(public_slot as usize) + .ok_or_else(|| integrity("gather map public slot is out of bounds"))?; + if destination.is_some() { + return Err(integrity("gather map repeats a public external slot")); + } + if public_slot_by_label + .insert(anchor.external_label(), public_slot as usize) + .is_some() + { + return Err(integrity("compact seed repeats a public external label")); + } + let mut public_helicities = Vec::new(); + public_helicities + .try_reserve_exact(anchor.states().len()) + .map_err(|error| invalid(format!("helicity-domain allocation failed: {error}")))?; + for state in anchor.states() { + let helicity = state.public_helicity(); + if public_helicities.contains(&helicity) { + return Err(integrity(format!( + "source slot {} repeats public helicity {helicity}", + anchor.source_slot(), + ))); + } + public_helicities.push(helicity); + } + if public_helicities.is_empty() { + return Err(integrity("source anchor has no public helicity")); + } + *destination = Some(CompactExternalV1 { + source_slot: anchor.source_slot(), + public_slot, + public_label: anchor.external_label(), + is_initial: anchor.is_initial(), + color_role: anchor.color_role(), + public_helicities: public_helicities.into_boxed_slice(), + }); + } + let externals_by_public_slot = by_public + .into_iter() + .map(|entry| entry.ok_or_else(|| integrity("gather map omits a public slot"))) + .collect::>>()? + .into_boxed_slice(); + + // Pairing classes are not color-flow pairings: they authenticate Wick + // compatibility. Nevertheless, checking their endpoint coverage here + // ensures that every fundamental/antifundamental role used to split a + // public flow is backed by the same compact seed contract. + let mut pairing_endpoints = BTreeSet::new(); + for pairing_class in seed.pairing_classes() { + for endpoint in pairing_class + .fundamental_endpoints() + .iter() + .chain(pairing_class.antifundamental_endpoints()) + { + if !pairing_endpoints.insert(endpoint.source_slot) { + return Err(integrity("pairing classes repeat an endpoint")); + } + } + } + + let mut fundamental_labels = Vec::new(); + let mut antifundamental_labels = Vec::new(); + let mut adjoint_labels = Vec::new(); + let mut required_pairing_endpoints = BTreeSet::new(); + for external in externals_by_public_slot.iter() { + match external.color_role { + OnTheFlyExternalColorRoleV1::Singlet => {} + OnTheFlyExternalColorRoleV1::Fundamental => { + fundamental_labels.push(external.public_label); + required_pairing_endpoints.insert(external.source_slot); + } + OnTheFlyExternalColorRoleV1::Antifundamental => { + antifundamental_labels.push(external.public_label); + required_pairing_endpoints.insert(external.source_slot); + } + OnTheFlyExternalColorRoleV1::Adjoint => { + adjoint_labels.push(external.public_label); + } + } + } + if pairing_endpoints != required_pairing_endpoints { + return Err(integrity( + "pairing-class coverage differs from the open-line endpoint roles", + )); + } + fundamental_labels.sort_unstable(); + antifundamental_labels.sort_unstable(); + adjoint_labels.sort_unstable(); + if fundamental_labels.len() != antifundamental_labels.len() { + return Err(integrity( + "LC selector domain has unbalanced open-line endpoints", + )); + } + + let color_domain = if !fundamental_labels.is_empty() { + CompactColorDomainV1::OpenLines + } else if !adjoint_labels.is_empty() { + CompactColorDomainV1::SingleTrace + } else { + CompactColorDomainV1::Singlet + }; + if policy.color_basis == OnTheFlyColorBasis::Adjoint + && (adjoint_labels.len() < 2 + || !fundamental_labels.is_empty() + || policy.trace_reflections_folded) + { + return Err(integrity( + "adjoint color basis requires at least two adjoint external sources, no fundamental lines and no trace-reflection folding", + )); + } + let helicity_count = checked_product( + externals_by_public_slot + .iter() + .map(|external| external.public_helicities.len()), + "public helicity count", + )?; + let color_count = match color_domain { + CompactColorDomainV1::Singlet => 1, + CompactColorDomainV1::SingleTrace => { + single_trace_color_count(adjoint_labels.len(), policy.color_basis)? + } + CompactColorDomainV1::OpenLines => { + let line_count = fundamental_labels.len(); + let pairing_count = checked_factorial(line_count, "open-line pairing count")?; + let adjoint_order_count = + checked_factorial(adjoint_labels.len(), "adjoint order count")?; + let split_count = checked_binomial( + adjoint_labels + .len() + .checked_add(line_count.saturating_sub(1)) + .ok_or_else(|| invalid("open-line split domain exceeds usize"))?, + line_count.saturating_sub(1), + "open-line split count", + )?; + pairing_count + .checked_mul(adjoint_order_count) + .and_then(|value| value.checked_mul(split_count)) + .ok_or_else(|| invalid("LC open-line flow count exceeds usize"))? + } + }; + if policy.trace_reflections_folded && color_domain != CompactColorDomainV1::SingleTrace { + return Err(integrity( + "trace-reflection folding is valid only for a pure single-trace LC domain", + )); + } + let mut adapter = Self { + external_permutation: external_permutation.to_vec().into_boxed_slice(), + representative_to_public: (0..anchors.len() as u32) + .collect::>() + .into_boxed_slice(), + public_to_representative: (0..anchors.len() as u32) + .collect::>() + .into_boxed_slice(), + public_label_by_representative_label: public_slot_by_label + .keys() + .copied() + .map(|label| (label, label)) + .collect(), + representative_label_by_public_label: public_slot_by_label + .keys() + .copied() + .map(|label| (label, label)) + .collect(), + externals_by_public_slot, + public_slot_by_label, + fundamental_labels: fundamental_labels.into_boxed_slice(), + antifundamental_labels: antifundamental_labels.into_boxed_slice(), + adjoint_labels: adjoint_labels.into_boxed_slice(), + color_domain, + color_basis: policy.color_basis, + reference_unfolded_color_ordinal: None, + trace_reflections_folded: policy.trace_reflections_folded, + helicity_count, + color_count, + }; + if let Some(reference) = policy.reference_color_word { + if reference.is_empty() { + if adapter.color_domain != CompactColorDomainV1::Singlet { + return Err(invalid( + "empty reference color word is valid only for a singlet process", + )); + } + } else { + // Requiring a member of the ordinary complete axis excludes + // anti-endpoint-first and whole-block traversal aliases. Such + // references are useful generation hints, but are not distinct + // complete LC public components and cannot be retained without + // a dense exceptional-flow table. + adapter.selector_from_labels(&reference)?; + let ordinal = adapter.unfolded_color_ordinal(&reference)?; + let (actual, _) = adapter.unfolded_color_at(ordinal)?; + if actual.as_ref() != reference.as_ref() { + return Err(invalid( + "reference color word is not a member of the ordinary complete LC axis", + )); + } + if adapter.trace_reflections_folded { + let reflected = reflected_trace_word(&reference)?; + let reflected_ordinal = adapter.unfolded_color_ordinal(&reflected)?; + if ordinal > reflected_ordinal { + return Err(invalid( + "folded trace reference must be the canonical member of its reflection class", + )); + } + } + adapter.reference_unfolded_color_ordinal = Some(ordinal); + } + } + Ok(adapter) + } + + /// Project public selector IDs through an already authenticated artifact + /// alias without changing the representative compact seed or its query + /// identity. The mapping is representative public slot -> requested public + /// slot, matching the existing artifact-selection ABI. + pub(super) fn with_public_permutation(mut self, permutation: &[usize]) -> RusticolResult { + if permutation.len() != self.externals_by_public_slot.len() + || permutation.iter().copied().collect::>() + != (0..permutation.len()).collect::>() + { + return Err(integrity( + "artifact alias does not define a complete public external permutation", + )); + } + if permutation + .iter() + .copied() + .enumerate() + .all(|(representative, public)| representative == public) + { + return Ok(self); + } + let mut inverse = vec![u32::MAX; permutation.len()]; + let mut public_label_by_representative_label = BTreeMap::new(); + let mut representative_label_by_public_label = BTreeMap::new(); + for (representative_slot, public_slot) in permutation.iter().copied().enumerate() { + inverse[public_slot] = u32::try_from(representative_slot) + .map_err(|_| integrity("representative public slot exceeds u32"))?; + let external = self + .externals_by_public_slot + .get(representative_slot) + .ok_or_else(|| integrity("representative public slot is absent"))?; + let expected_label = u32::try_from(representative_slot + 1) + .map_err(|_| integrity("representative public label exceeds u32"))?; + if external.public_label != expected_label { + return Err(integrity( + "permuted public selection requires canonical one-based external labels", + )); + } + let public_label = u32::try_from(public_slot + 1) + .map_err(|_| integrity("requested public label exceeds u32"))?; + public_label_by_representative_label.insert(expected_label, public_label); + representative_label_by_public_label.insert(public_label, expected_label); + } + if inverse.contains(&u32::MAX) { + return Err(integrity("artifact alias omits a public external slot")); + } + self.representative_to_public = permutation + .iter() + .copied() + .map(|slot| u32::try_from(slot).map_err(|_| integrity("public slot exceeds u32"))) + .collect::>>()? + .into_boxed_slice(); + self.public_to_representative = inverse.into_boxed_slice(); + self.public_label_by_representative_label = public_label_by_representative_label; + self.representative_label_by_public_label = representative_label_by_public_label; + Ok(self) + } + + pub(super) const fn helicity_count(&self) -> usize { + self.helicity_count + } + + pub(super) const fn color_count(&self) -> usize { + self.color_count + } + + /// Decode one structural query directly from compact ordinals. + /// + /// Contracted NLC/full execution has a singleton public color axis, but + /// evaluates its authenticated structural owner basis before applying the + /// color metric. This cold adapter keeps those decoded queries transient + /// instead of retaining flow strings, traces, or a dense public table. + pub(super) fn decoded_query_at( + &self, + seed: &OnTheFlyProcessSeedV1, + helicity_ordinal: usize, + structural_color_ordinal: usize, + ) -> RusticolResult { + let public_helicities = self.helicity_at(helicity_ordinal)?; + let (_, selector) = self.color_at(structural_color_ordinal)?; + DecodedLcQueryV1::new( + seed, + self.external_permutation.to_vec(), + &public_helicities, + selector, + ) + } + + pub(super) fn parse_helicity_id(&self, value: &str) -> RusticolResult> { + let payload = value + .strip_prefix("h:") + .ok_or_else(|| invalid(format!("unknown helicity ID {value:?}")))?; + if payload.is_empty() { + return Err(invalid("helicity ID has no external values")); + } + let public_values = payload + .split(',') + .map(|part| { + let parsed = part.parse::().map_err(|_| { + invalid(format!("helicity ID {value:?} has an invalid integer")) + })?; + if format!("{parsed:+}") != part { + return Err(invalid(format!("helicity ID {value:?} is not canonical",))); + } + Ok(parsed) + }) + .collect::>>()?; + if public_values.len() != self.externals_by_public_slot.len() { + return Err(invalid(format!( + "helicity ID {value:?} has {} values, expected {}", + public_values.len(), + self.externals_by_public_slot.len(), + ))); + } + let mut values = vec![0; public_values.len()]; + for (public_slot, selected) in public_values.iter().copied().enumerate() { + let representative_slot = *self + .public_to_representative + .get(public_slot) + .ok_or_else(|| integrity("requested public helicity slot is absent"))?; + values[representative_slot as usize] = selected; + } + for (public_slot, (external, selected)) in self + .externals_by_public_slot + .iter() + .zip(values.iter().copied()) + .enumerate() + { + if !external.public_helicities.contains(&selected) { + return Err(invalid(format!( + "helicity ID {value:?} selects unavailable helicity {selected} at public slot {public_slot}", + ))); + } + } + Ok(values.into_boxed_slice()) + } + + pub(super) fn parse_color_id( + &self, + value: &str, + ) -> RusticolResult<(Box<[u32]>, OnTheFlyLcSelectorV1)> { + if value == "flow:singlet" { + if self.color_domain != CompactColorDomainV1::Singlet { + return Err(invalid( + "flow:singlet is invalid for colored external particles", + )); + } + return Ok((Box::new([]), OnTheFlyLcSelectorV1::Singlet)); + } + let payload = value + .strip_prefix("flow:") + .ok_or_else(|| invalid(format!("unknown LC color ID {value:?}")))?; + if payload.is_empty() { + return Err(invalid("LC color ID has no external labels")); + } + let labels = payload + .split(',') + .map(|part| { + let label = part + .parse::() + .map_err(|_| invalid(format!("LC color ID {value:?} has an invalid label")))?; + if label.to_string() != part { + return Err(invalid(format!("LC color ID {value:?} is not canonical",))); + } + Ok(label) + }) + .collect::>>()?; + let canonical = canonical_color_id(&labels); + if canonical != value { + return Err(invalid(format!("LC color ID {value:?} is not canonical"))); + } + let representative_labels = labels + .iter() + .map(|label| { + self.representative_label_by_public_label + .get(label) + .copied() + .ok_or_else(|| invalid("LC color ID contains an unknown external label")) + }) + .collect::>>()?; + let selector = self.selector_from_labels(&representative_labels)?; + Ok((representative_labels.into_boxed_slice(), selector)) + } + + fn selector_from_labels(&self, labels: &[u32]) -> RusticolResult { + let required = self + .externals_by_public_slot + .iter() + .filter(|external| external.color_role != OnTheFlyExternalColorRoleV1::Singlet) + .map(|external| external.public_label) + .collect::>(); + let observed = labels.iter().copied().collect::>(); + if observed.len() != labels.len() || observed != required { + return Err(invalid( + "LC color ID must cover every colored external label exactly once", + )); + } + let public_slots = labels + .iter() + .map(|label| { + self.public_slot_by_label + .get(label) + .copied() + .ok_or_else(|| invalid("LC color ID contains an unknown external label")) + .and_then(|slot| { + u32::try_from(slot).map_err(|_| invalid("public external slot exceeds u32")) + }) + }) + .collect::>>()?; + match self.color_domain { + CompactColorDomainV1::Singlet => { + Err(invalid("colored LC flow is invalid for a singlet process")) + } + CompactColorDomainV1::SingleTrace => { + if labels.iter().any(|label| { + let slot = self.public_slot_by_label[label]; + self.externals_by_public_slot[slot].color_role + != OnTheFlyExternalColorRoleV1::Adjoint + }) { + return Err(invalid("single-trace LC flow contains a non-adjoint label")); + } + if labels.first() != self.adjoint_labels.first() { + return Err(invalid( + "single-trace LC flow must use the minimum adjoint label as cyclic anchor", + )); + } + if self.color_basis == OnTheFlyColorBasis::Adjoint + && labels.last() != self.adjoint_labels.last() + { + return Err(invalid( + "adjoint color flow must preserve both fixed endpoint labels", + )); + } + Ok(OnTheFlyLcSelectorV1::single_trace(public_slots)) + } + CompactColorDomainV1::OpenLines => { + let mut blocks = Vec::>::new(); + let mut offset = 0; + while offset < labels.len() { + let start_slot = self.public_slot_by_label[&labels[offset]]; + if self.externals_by_public_slot[start_slot].color_role + != OnTheFlyExternalColorRoleV1::Fundamental + { + return Err(invalid( + "open-line LC flow block must start at a fundamental label", + )); + } + let mut end = offset + 1; + while end < labels.len() { + let role = self.externals_by_public_slot + [self.public_slot_by_label[&labels[end]]] + .color_role; + if role != OnTheFlyExternalColorRoleV1::Adjoint { + break; + } + end += 1; + } + if end >= labels.len() + || self.externals_by_public_slot[self.public_slot_by_label[&labels[end]]] + .color_role + != OnTheFlyExternalColorRoleV1::Antifundamental + { + return Err(invalid( + "open-line LC flow block must end at an antifundamental label", + )); + } + blocks.push(public_slots[offset..=end].to_vec()); + offset = end + 1; + } + if blocks.len() != self.fundamental_labels.len() { + return Err(invalid("LC flow has the wrong number of open-line blocks")); + } + // The established LC public plan orders blocks by the sorted + // fundamental external labels. Other block orders are valid + // traversal aliases internally, but are not distinct public IDs. + let starts = blocks + .iter() + .map(|block| { + let public_slot = block[0] as usize; + self.externals_by_public_slot[public_slot].public_label + }) + .collect::>(); + if starts.as_slice() != self.fundamental_labels.as_ref() { + return Err(invalid( + "open-line LC flow blocks are not in canonical fundamental-label order", + )); + } + Ok(OnTheFlyLcSelectorV1::open_lines(blocks)) + } + } + } + + fn helicity_at(&self, mut index: usize) -> RusticolResult> { + if index >= self.helicity_count { + return Err(invalid("public helicity ordinal is out of bounds")); + } + let mut values = vec![0; self.externals_by_public_slot.len()]; + for (slot, external) in self.externals_by_public_slot.iter().enumerate().rev() { + let radix = external.public_helicities.len(); + values[slot] = external.public_helicities[index % radix]; + index /= radix; + } + Ok(values.into_boxed_slice()) + } + + fn public_helicities_from_representative( + &self, + representative: &[i32], + ) -> RusticolResult> { + if representative.len() != self.representative_to_public.len() { + return Err(integrity( + "representative helicity domain differs from the public permutation", + )); + } + let mut public = vec![0; representative.len()]; + for (representative_slot, value) in representative.iter().copied().enumerate() { + let public_slot = self.representative_to_public[representative_slot] as usize; + public[public_slot] = value; + } + Ok(public.into_boxed_slice()) + } + + fn public_labels_from_representative( + &self, + representative: &[u32], + ) -> RusticolResult> { + representative + .iter() + .map(|label| { + self.public_label_by_representative_label + .get(label) + .copied() + .ok_or_else(|| integrity("representative LC label has no public alias")) + }) + .collect::>>() + .map(Vec::into_boxed_slice) + } + + fn helicity_ordinal(&self, values: &[i32]) -> RusticolResult { + let mut ordinal = 0usize; + for (external, selected) in self.externals_by_public_slot.iter().zip(values) { + let digit = external + .public_helicities + .iter() + .position(|candidate| candidate == selected) + .ok_or_else(|| invalid("public helicity is outside the compact seed"))?; + ordinal = ordinal + .checked_mul(external.public_helicities.len()) + .and_then(|value| value.checked_add(digit)) + .ok_or_else(|| invalid("public helicity ordinal exceeds usize"))?; + } + Ok(ordinal) + } + + fn color_at(&self, index: usize) -> RusticolResult<(Box<[u32]>, OnTheFlyLcSelectorV1)> { + if index >= self.color_count { + return Err(invalid("public LC color ordinal is out of bounds")); + } + let unfolded = self.unfolded_color_ordinal_at(index)?; + self.unfolded_color_at(unfolded) + } + + fn unfolded_color_at( + &self, + index: usize, + ) -> RusticolResult<(Box<[u32]>, OnTheFlyLcSelectorV1)> { + if index >= self.color_count { + return Err(invalid("unfolded LC color ordinal is out of bounds")); + } + let labels = match self.color_domain { + CompactColorDomainV1::Singlet => Vec::new(), + CompactColorDomainV1::SingleTrace => { + let first = *self + .adjoint_labels + .first() + .ok_or_else(|| integrity("single-trace domain has no adjoint anchor"))?; + let mut labels = vec![first]; + if self.color_basis == OnTheFlyColorBasis::Adjoint { + let last = self.adjoint_labels.len() - 1; + labels.extend(nth_permutation(&self.adjoint_labels[1..last], index)?); + labels.push(self.adjoint_labels[last]); + } else { + labels.extend(nth_permutation(&self.adjoint_labels[1..], index)?); + } + labels + } + CompactColorDomainV1::OpenLines => self.open_line_color_at(index)?, + }; + let selector = if labels.is_empty() { + OnTheFlyLcSelectorV1::Singlet + } else { + self.selector_from_labels(&labels)? + }; + Ok((labels.into_boxed_slice(), selector)) + } + + fn unfolded_color_ordinal_at(&self, public_index: usize) -> RusticolResult { + if public_index >= self.color_count { + return Err(invalid("public LC color ordinal is out of bounds")); + } + if self.trace_reflections_folded { + return self.folded_unfolded_color_ordinal_at(public_index); + } + match self.reference_unfolded_color_ordinal { + None => Ok(public_index), + Some(reference) if public_index == 0 => Ok(reference), + Some(reference) => { + let ordinary = public_index - 1; + Ok(if ordinary >= reference { + ordinary + 1 + } else { + ordinary + }) + } + } + } + + fn folded_unfolded_color_ordinal_at(&self, public_index: usize) -> RusticolResult { + let reference = self.reference_unfolded_color_ordinal; + let mut next_public = 0usize; + if let Some(reference) = reference { + let (word, _) = self.unfolded_color_at(reference)?; + let reflected = reflected_trace_word(&word)?; + let reflected_ordinal = self.unfolded_color_ordinal(&reflected)?; + for ordinal in [reference, reflected_ordinal] { + if next_public == public_index { + return Ok(ordinal); + } + next_public += 1; + if reflected_ordinal == reference { + break; + } + } + } + for ordinal in 0..self.color_count { + let (word, _) = self.unfolded_color_at(ordinal)?; + let reflected = reflected_trace_word(&word)?; + let reflected_ordinal = self.unfolded_color_ordinal(&reflected)?; + if ordinal > reflected_ordinal || reference == Some(ordinal) { + continue; + } + for member in [ordinal, reflected_ordinal] { + if next_public == public_index { + return Ok(member); + } + next_public += 1; + if reflected_ordinal == ordinal { + break; + } + } + } + Err(integrity( + "folded trace enumeration did not cover its public color axis", + )) + } + + fn open_line_color_at(&self, index: usize) -> RusticolResult> { + let line_count = self.fundamental_labels.len(); + let split_count = checked_binomial( + self.adjoint_labels + .len() + .checked_add(line_count.saturating_sub(1)) + .ok_or_else(|| invalid("open-line split domain exceeds usize"))?, + line_count.saturating_sub(1), + "open-line split count", + )?; + let adjoint_order_count = + checked_factorial(self.adjoint_labels.len(), "adjoint order count")?; + let allocation_count = adjoint_order_count + .checked_mul(split_count) + .ok_or_else(|| invalid("ordered adjoint allocation count exceeds usize"))?; + let pairing_index = index / allocation_count; + let allocation_index = index % allocation_count; + let adjoint_order_index = allocation_index / split_count; + let split_index = allocation_index % split_count; + let antifundamentals = nth_permutation(&self.antifundamental_labels, pairing_index)?; + let adjoints = nth_permutation(&self.adjoint_labels, adjoint_order_index)?; + let lengths = nth_weak_composition(adjoint_labels_len(&adjoints), line_count, split_index)?; + + let mut labels = Vec::new(); + labels + .try_reserve_exact( + line_count + .checked_mul(2) + .and_then(|value| value.checked_add(adjoints.len())) + .ok_or_else(|| invalid("LC color word length exceeds usize"))?, + ) + .map_err(|error| invalid(format!("LC color word allocation failed: {error}")))?; + let mut offset = 0_usize; + for line in 0..line_count { + labels.push(self.fundamental_labels[line]); + let end = offset + .checked_add(lengths[line]) + .ok_or_else(|| invalid("LC adjoint allocation exceeds usize"))?; + labels.extend_from_slice(&adjoints[offset..end]); + labels.push(antifundamentals[line]); + offset = end; + } + if offset != adjoints.len() { + return Err(integrity("LC adjoint allocation did not consume its word")); + } + Ok(labels) + } + + fn unfolded_color_ordinal(&self, labels: &[u32]) -> RusticolResult { + match self.color_domain { + CompactColorDomainV1::Singlet => { + if labels.is_empty() { + Ok(0) + } else { + Err(invalid("singlet color ordinal received a colored word")) + } + } + CompactColorDomainV1::SingleTrace => { + if labels.first() != self.adjoint_labels.first() { + return Err(invalid("single-trace cyclic anchor differs")); + } + if self.color_basis == OnTheFlyColorBasis::Adjoint { + if labels.len() != self.adjoint_labels.len() + || labels.last() != self.adjoint_labels.last() + { + return Err(invalid("adjoint color flow endpoints differ")); + } + let last = self.adjoint_labels.len() - 1; + permutation_rank(&self.adjoint_labels[1..last], &labels[1..last]) + } else { + permutation_rank(&self.adjoint_labels[1..], &labels[1..]) + } + } + CompactColorDomainV1::OpenLines => { + let mut antifundamentals = Vec::new(); + let mut adjoints = Vec::new(); + let mut lengths = Vec::new(); + let mut offset = 0; + for fundamental in self.fundamental_labels.iter().copied() { + if labels.get(offset).copied() != Some(fundamental) { + return Err(invalid("open-line block order differs")); + } + offset += 1; + let start = adjoints.len(); + while let Some(label) = labels.get(offset).copied() { + let role = self.externals_by_public_slot[self.public_slot_by_label[&label]] + .color_role; + if role != OnTheFlyExternalColorRoleV1::Adjoint { + break; + } + adjoints.push(label); + offset += 1; + } + let antifundamental = labels + .get(offset) + .copied() + .ok_or_else(|| invalid("open-line block has no terminal endpoint"))?; + antifundamentals.push(antifundamental); + offset += 1; + lengths.push(adjoints.len() - start); + } + if offset != labels.len() { + return Err(invalid("open-line flow has trailing labels")); + } + let pairing_index = + permutation_rank(&self.antifundamental_labels, &antifundamentals)?; + let adjoint_order_index = permutation_rank(&self.adjoint_labels, &adjoints)?; + let split_index = weak_composition_rank(&lengths)?; + let split_count = checked_binomial( + self.adjoint_labels + .len() + .checked_add(self.fundamental_labels.len().saturating_sub(1)) + .ok_or_else(|| invalid("open-line split domain exceeds usize"))?, + self.fundamental_labels.len().saturating_sub(1), + "open-line split count", + )?; + let allocation_count = + checked_factorial(self.adjoint_labels.len(), "adjoint order count")? + .checked_mul(split_count) + .ok_or_else(|| invalid("ordered adjoint allocation count exceeds usize"))?; + pairing_index + .checked_mul(allocation_count) + .and_then(|value| { + value.checked_add( + adjoint_order_index + .checked_mul(split_count) + .and_then(|inner| inner.checked_add(split_index))?, + ) + }) + .ok_or_else(|| invalid("LC color ordinal exceeds usize")) + } + } + } + + fn color_ordinal(&self, labels: &[u32]) -> RusticolResult { + let unfolded = self.unfolded_color_ordinal(labels)?; + if self.trace_reflections_folded { + for public in 0..self.color_count { + if self.folded_unfolded_color_ordinal_at(public)? == unfolded { + return Ok(public); + } + } + return Err(integrity( + "folded trace public axis omits an unfolded color member", + )); + } + Ok(match self.reference_unfolded_color_ordinal { + None => unfolded, + Some(reference) if unfolded == reference => 0, + Some(reference) if unfolded < reference => unfolded + 1, + Some(_) => unfolded, + }) + } + + pub(super) fn selected_ordinals( + &self, + selected_helicity_ids: Option<&BTreeSet>, + selected_color_ids: Option<&BTreeSet>, + ) -> RusticolResult { + let helicity_ordinals = match selected_helicity_ids { + None => None, + Some(ids) => { + if ids.is_empty() { + return Err(invalid("helicity selection is empty")); + } + let mut ordinals = ids + .iter() + .map(|id| { + let values = self.parse_helicity_id(id)?; + self.helicity_ordinal(&values) + }) + .collect::>>()?; + ordinals.sort_unstable(); + ordinals.dedup(); + Some(ordinals.into_boxed_slice()) + } + }; + let color_ordinals = match selected_color_ids { + None => None, + Some(ids) => { + if ids.is_empty() { + return Err(invalid("LC color selection is empty")); + } + let mut ordinals = ids + .iter() + .map(|id| { + let (labels, _) = self.parse_color_id(id)?; + self.color_ordinal(&labels) + }) + .collect::>>()?; + ordinals.sort_unstable(); + ordinals.dedup(); + Some(ordinals.into_boxed_slice()) + } + }; + Ok((helicity_ordinals, color_ordinals)) + } + + pub(super) fn selection<'a>( + &'a self, + seed: &'a OnTheFlyProcessSeedV1, + selected_helicity_ids: Option<&BTreeSet>, + selected_color_ids: Option<&BTreeSet>, + ) -> RusticolResult> { + let (helicity_ordinals, color_ordinals) = + self.selected_ordinals(selected_helicity_ids, selected_color_ids)?; + Ok(OnTheFlyLazySelectionV1 { + adapter: self, + seed, + helicity_ordinals, + color_ordinals, + }) + } + + pub(super) fn selection_from_ordinals<'a>( + &'a self, + seed: &'a OnTheFlyProcessSeedV1, + helicity_ordinals: Option<&[usize]>, + color_ordinals: Option<&[usize]>, + ) -> RusticolResult> { + Ok(OnTheFlyLazySelectionV1 { + adapter: self, + seed, + helicity_ordinals: owned_selected_ordinals( + helicity_ordinals, + self.helicity_count, + "helicity", + )?, + color_ordinals: owned_selected_ordinals(color_ordinals, self.color_count, "color")?, + }) + } +} + +/// Lazy selector Cartesian product. This is the explicit materializer seam +/// for factorial default-all flow coverage; callers may consume one request or +/// one bounded batch without ever allocating a dense public axis. +pub(super) struct OnTheFlyLazySelectionV1<'a> { + adapter: &'a OnTheFlyCompactSelectorAdapterV1, + seed: &'a OnTheFlyProcessSeedV1, + helicity_ordinals: Option>, + color_ordinals: Option>, +} + +impl OnTheFlyLazySelectionV1<'_> { + pub(super) fn helicity_count(&self) -> usize { + self.helicity_ordinals + .as_ref() + .map_or(self.adapter.helicity_count, |values| values.len()) + } + + pub(super) fn color_count(&self) -> usize { + self.color_ordinals + .as_ref() + .map_or(self.adapter.color_count, |values| values.len()) + } + + pub(super) fn helicity_ordinal_at(&self, position: usize) -> RusticolResult { + selected_ordinal( + self.helicity_ordinals.as_deref(), + self.adapter.helicity_count, + position, + "helicity", + ) + } + + pub(super) fn color_ordinal_at(&self, position: usize) -> RusticolResult { + selected_ordinal( + self.color_ordinals.as_deref(), + self.adapter.color_count, + position, + "color", + ) + } + + pub(super) fn request_count(&self) -> RusticolResult { + self.helicity_count() + .checked_mul(self.color_count()) + .ok_or_else(|| invalid("selected LC query count exceeds usize")) + } + + pub(super) fn helicity_id_at(&self, position: usize) -> RusticolResult { + let ordinal = selected_ordinal( + self.helicity_ordinals.as_deref(), + self.adapter.helicity_count, + position, + "helicity", + )?; + let representative = self.adapter.helicity_at(ordinal)?; + let public = self + .adapter + .public_helicities_from_representative(&representative)?; + Ok(canonical_helicity_id(&public)) + } + + pub(super) fn color_id_at(&self, position: usize) -> RusticolResult { + let ordinal = selected_ordinal( + self.color_ordinals.as_deref(), + self.adapter.color_count, + position, + "color", + )?; + let (representative, _) = self.adapter.color_at(ordinal)?; + let public = self + .adapter + .public_labels_from_representative(&representative)?; + Ok(canonical_color_id(&public)) + } + + pub(super) fn request_at(&self, position: usize) -> RusticolResult { + let color_count = self.color_count(); + if color_count == 0 || position >= self.request_count()? { + return Err(invalid("selected LC query ordinal is out of bounds")); + } + let helicity_position = position / color_count; + let color_position = position % color_count; + let helicity_ordinal = selected_ordinal( + self.helicity_ordinals.as_deref(), + self.adapter.helicity_count, + helicity_position, + "helicity", + )?; + let color_ordinal = selected_ordinal( + self.color_ordinals.as_deref(), + self.adapter.color_count, + color_position, + "color", + )?; + let query = self + .adapter + .decoded_query_at(self.seed, helicity_ordinal, color_ordinal)?; + OnTheFlyLcQueryRequestV1::new( + query, + vec![OnTheFlyLcReductionTargetV1::new( + helicity_position, + color_position, + 1.0, + )?], + ) + } + + pub(super) fn iter(&self) -> OnTheFlyLazyRequestIterV1<'_, '_> { + OnTheFlyLazyRequestIterV1 { + selection: self, + next: 0, + } + } +} + +pub(super) struct OnTheFlyLazyRequestIterV1<'selection, 'seed> { + selection: &'selection OnTheFlyLazySelectionV1<'seed>, + next: usize, +} + +impl Iterator for OnTheFlyLazyRequestIterV1<'_, '_> { + type Item = RusticolResult; + + fn next(&mut self) -> Option { + let count = match self.selection.request_count() { + Ok(value) => value, + Err(error) => { + self.next = usize::MAX; + return Some(Err(error)); + } + }; + if self.next >= count { + return None; + } + let position = self.next; + self.next += 1; + Some(self.selection.request_at(position)) + } + + fn size_hint(&self) -> (usize, Option) { + match self.selection.request_count() { + Ok(count) => { + let remaining = count.saturating_sub(self.next); + (remaining, Some(remaining)) + } + Err(_) => (1, Some(1)), + } + } +} + +/// Lane-local cache used only by explicit metadata calls. Evaluation and +/// profiling consume [`OnTheFlyLazySelectionV1`] directly and never fill it. +#[derive(Default)] +pub(super) struct OnTheFlySelectorIntrospectionCacheV1 { + #[expect( + dead_code, + reason = "retained for the private external-selector metadata compatibility API" + )] + externals: Option>, + helicities: Option>, + colors: Option>, +} + +impl OnTheFlySelectorIntrospectionCacheV1 { + #[expect( + dead_code, + reason = "retained for the private external-selector metadata compatibility API" + )] + pub(super) fn externals<'a>( + &'a mut self, + adapter: &OnTheFlyCompactSelectorAdapterV1, + ) -> &'a [OnTheFlyExternalSelectorRecordV1] { + self.externals.get_or_insert_with(|| { + adapter + .externals_by_public_slot + .iter() + .map(|external| OnTheFlyExternalSelectorRecordV1 { + source_slot: external.source_slot, + public_slot: external.public_slot, + public_label: external.public_label, + is_initial: external.is_initial, + color_role: external.color_role, + }) + .collect::>() + .into_boxed_slice() + }) + } + + pub(super) fn helicities<'a>( + &'a mut self, + adapter: &OnTheFlyCompactSelectorAdapterV1, + ) -> RusticolResult<&'a [OnTheFlyHelicitySelectorRecordV1]> { + if self.helicities.is_none() { + let mut records = Vec::new(); + records + .try_reserve_exact(adapter.helicity_count) + .map_err(|error| { + invalid(format!("helicity metadata allocation failed: {error}")) + })?; + for index in 0..adapter.helicity_count { + let values = adapter.helicity_at(index)?; + records.push(OnTheFlyHelicitySelectorRecordV1 { + id: canonical_helicity_id(&values), + index, + values, + }); + } + self.helicities = Some(records.into_boxed_slice()); + } + Ok(self + .helicities + .as_deref() + .expect("helicity cache was filled")) + } + + pub(super) fn colors<'a>( + &'a mut self, + adapter: &OnTheFlyCompactSelectorAdapterV1, + ) -> RusticolResult<&'a [OnTheFlyColorSelectorRecordV1]> { + if self.colors.is_none() { + let mut records = Vec::new(); + records + .try_reserve_exact(adapter.color_count) + .map_err(|error| invalid(format!("color metadata allocation failed: {error}")))?; + for index in 0..adapter.color_count { + let (word, selector) = adapter.color_at(index)?; + records.push(OnTheFlyColorSelectorRecordV1 { + id: canonical_color_id(&word), + index, + word, + selector, + }); + } + self.colors = Some(records.into_boxed_slice()); + } + Ok(self.colors.as_deref().expect("color cache was filled")) + } +} + +fn selected_ordinal( + selected: Option<&[usize]>, + all_count: usize, + position: usize, + label: &str, +) -> RusticolResult { + match selected { + Some(values) => values + .get(position) + .copied() + .ok_or_else(|| invalid(format!("selected {label} position is out of bounds"))), + None if position < all_count => Ok(position), + None => Err(invalid(format!("{label} position is out of bounds"))), + } +} + +fn owned_selected_ordinals( + selected: Option<&[usize]>, + all_count: usize, + label: &str, +) -> RusticolResult>> { + let Some(selected) = selected else { + return Ok(None); + }; + if selected.is_empty() { + return Err(invalid(format!("{label} selection is empty"))); + } + if selected.iter().any(|ordinal| *ordinal >= all_count) { + return Err(invalid(format!( + "selected {label} ordinal is out of bounds" + ))); + } + if selected.windows(2).any(|pair| pair[0] >= pair[1]) { + return Err(invalid(format!( + "selected {label} ordinals are not strictly increasing" + ))); + } + Ok(Some(selected.to_vec().into_boxed_slice())) +} + +fn canonical_helicity_id(values: &[i32]) -> String { + format!( + "h:{}", + values + .iter() + .map(|value| format!("{value:+}")) + .collect::>() + .join(",") + ) +} + +fn canonical_color_id(word: &[u32]) -> String { + if word.is_empty() { + "flow:singlet".to_string() + } else { + format!( + "flow:{}", + word.iter() + .map(u32::to_string) + .collect::>() + .join(",") + ) + } +} + +fn reflected_trace_word(word: &[u32]) -> RusticolResult> { + let Some((first, rest)) = word.split_first() else { + return Err(invalid("cannot reflect an empty trace word")); + }; + let mut reflected = Vec::new(); + reflected + .try_reserve_exact(word.len()) + .map_err(|error| invalid(format!("trace-reflection allocation failed: {error}")))?; + reflected.push(*first); + reflected.extend(rest.iter().rev().copied()); + Ok(reflected) +} + +fn checked_product(values: impl IntoIterator, label: &str) -> RusticolResult { + values.into_iter().try_fold(1usize, |total, value| { + total + .checked_mul(value) + .ok_or_else(|| invalid(format!("{label} exceeds usize"))) + }) +} + +fn single_trace_color_count(count: usize, basis: OnTheFlyColorBasis) -> RusticolResult { + let anchors = match basis { + OnTheFlyColorBasis::Trace => 1, + OnTheFlyColorBasis::Adjoint => 2, + }; + checked_factorial(count.saturating_sub(anchors), "single-trace count") +} + +fn checked_factorial(value: usize, label: &str) -> RusticolResult { + (2..=value).try_fold(1usize, |total, factor| { + total + .checked_mul(factor) + .ok_or_else(|| invalid(format!("{label} exceeds usize"))) + }) +} + +fn checked_binomial(n: usize, k: usize, label: &str) -> RusticolResult { + let k = k.min(n.saturating_sub(k)); + let mut value = 1usize; + for index in 0..k { + value = value + .checked_mul(n - index) + .ok_or_else(|| invalid(format!("{label} exceeds usize")))? + / (index + 1); + } + Ok(value) +} + +fn nth_permutation(values: &[u32], mut rank: usize) -> RusticolResult> { + if rank >= checked_factorial(values.len(), "permutation count")? { + return Err(invalid("permutation ordinal is out of bounds")); + } + let mut remaining = values.to_vec(); + let mut result = Vec::new(); + result + .try_reserve_exact(values.len()) + .map_err(|error| invalid(format!("permutation allocation failed: {error}")))?; + for width in (1..=values.len()).rev() { + let block = checked_factorial(width - 1, "permutation block")?; + let digit = rank / block; + rank %= block; + result.push(remaining.remove(digit)); + } + Ok(result) +} + +fn permutation_rank(reference: &[u32], selected: &[u32]) -> RusticolResult { + if reference.len() != selected.len() { + return Err(invalid("selected permutation has the wrong length")); + } + let mut remaining = reference.to_vec(); + let mut rank = 0usize; + for (index, selected) in selected.iter().copied().enumerate() { + let digit = remaining + .iter() + .position(|candidate| *candidate == selected) + .ok_or_else(|| invalid("selected values are not a permutation"))?; + rank = rank + .checked_add( + digit + .checked_mul(checked_factorial( + reference.len() - index - 1, + "permutation block", + )?) + .ok_or_else(|| invalid("permutation ordinal exceeds usize"))?, + ) + .ok_or_else(|| invalid("permutation ordinal exceeds usize"))?; + remaining.remove(digit); + } + Ok(rank) +} + +fn nth_weak_composition(total: usize, bins: usize, mut rank: usize) -> RusticolResult> { + if bins == 0 { + return if total == 0 && rank == 0 { + Ok(Vec::new()) + } else { + Err(invalid("nonempty sequence cannot be split into zero bins")) + }; + } + let count = checked_binomial( + total + .checked_add(bins - 1) + .ok_or_else(|| invalid("weak-composition domain exceeds usize"))?, + bins - 1, + "weak-composition count", + )?; + if rank >= count { + return Err(invalid("weak-composition ordinal is out of bounds")); + } + let mut remaining = total; + let mut result = Vec::with_capacity(bins); + for remaining_bins in (2..=bins).rev() { + let mut selected = None; + for head in 0..=remaining { + let suffix_count = checked_binomial( + remaining - head + remaining_bins - 2, + remaining_bins - 2, + "weak-composition suffix count", + )?; + if rank < suffix_count { + selected = Some(head); + break; + } + rank -= suffix_count; + } + let head = selected.ok_or_else(|| integrity("weak-composition unranking failed"))?; + result.push(head); + remaining -= head; + } + result.push(remaining); + Ok(result) +} + +fn weak_composition_rank(values: &[usize]) -> RusticolResult { + if values.is_empty() { + return Err(invalid("weak composition has no bins")); + } + let mut remaining = values + .iter() + .try_fold(0usize, |total, value| total.checked_add(*value)) + .ok_or_else(|| invalid("weak-composition total exceeds usize"))?; + let mut rank = 0usize; + for (index, selected) in values[..values.len() - 1].iter().copied().enumerate() { + if selected > remaining { + return Err(invalid("weak-composition value exceeds its remainder")); + } + let remaining_bins = values.len() - index; + for head in 0..selected { + rank = rank + .checked_add(checked_binomial( + remaining - head + remaining_bins - 2, + remaining_bins - 2, + "weak-composition suffix count", + )?) + .ok_or_else(|| invalid("weak-composition ordinal exceeds usize"))?; + } + remaining -= selected; + } + if values.last().copied() != Some(remaining) { + return Err(invalid("weak composition does not preserve its total")); + } + Ok(rank) +} + +const fn adjoint_labels_len(values: &[u32]) -> usize { + values.len() +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::recurrence::SemanticDigest; + use crate::recurrence::on_the_fly::scalar_adapter_test_seed; + + fn digest(byte: u8) -> SemanticDigest { + SemanticDigest::new([byte; 32]).unwrap() + } + + fn adapter( + roles: &[(u32, OnTheFlyExternalColorRoleV1, &[i32])], + ) -> OnTheFlyCompactSelectorAdapterV1 { + adapter_in_basis(roles, OnTheFlyColorBasis::Trace) + } + + fn adapter_in_basis( + roles: &[(u32, OnTheFlyExternalColorRoleV1, &[i32])], + color_basis: OnTheFlyColorBasis, + ) -> OnTheFlyCompactSelectorAdapterV1 { + let externals_by_public_slot = roles + .iter() + .enumerate() + .map(|(slot, (label, role, helicities))| CompactExternalV1 { + source_slot: slot as u32, + public_slot: slot as u32, + public_label: *label, + is_initial: slot < 2, + color_role: *role, + public_helicities: helicities.to_vec().into_boxed_slice(), + }) + .collect::>() + .into_boxed_slice(); + let public_slot_by_label = externals_by_public_slot + .iter() + .enumerate() + .map(|(slot, external)| (external.public_label, slot)) + .collect(); + let labels = |role| { + let mut labels = externals_by_public_slot + .iter() + .filter(|external| external.color_role == role) + .map(|external| external.public_label) + .collect::>(); + labels.sort_unstable(); + labels.into_boxed_slice() + }; + let fundamental_labels = labels(OnTheFlyExternalColorRoleV1::Fundamental); + let antifundamental_labels = labels(OnTheFlyExternalColorRoleV1::Antifundamental); + let adjoint_labels = labels(OnTheFlyExternalColorRoleV1::Adjoint); + let color_domain = if !fundamental_labels.is_empty() { + CompactColorDomainV1::OpenLines + } else if !adjoint_labels.is_empty() { + CompactColorDomainV1::SingleTrace + } else { + CompactColorDomainV1::Singlet + }; + let helicity_count = checked_product( + externals_by_public_slot + .iter() + .map(|external| external.public_helicities.len()), + "test helicities", + ) + .unwrap(); + let color_count = match color_domain { + CompactColorDomainV1::Singlet => 1, + CompactColorDomainV1::SingleTrace => { + single_trace_color_count(adjoint_labels.len(), color_basis).unwrap() + } + CompactColorDomainV1::OpenLines => { + checked_factorial(fundamental_labels.len(), "test pairings").unwrap() + * checked_factorial(adjoint_labels.len(), "test adjoints").unwrap() + * checked_binomial( + adjoint_labels.len() + fundamental_labels.len() - 1, + fundamental_labels.len() - 1, + "test splits", + ) + .unwrap() + } + }; + OnTheFlyCompactSelectorAdapterV1 { + external_permutation: (0..roles.len() as u32) + .collect::>() + .into_boxed_slice(), + representative_to_public: (0..roles.len() as u32) + .collect::>() + .into_boxed_slice(), + public_to_representative: (0..roles.len() as u32) + .collect::>() + .into_boxed_slice(), + public_label_by_representative_label: roles + .iter() + .map(|(label, _, _)| (*label, *label)) + .collect(), + representative_label_by_public_label: roles + .iter() + .map(|(label, _, _)| (*label, *label)) + .collect(), + externals_by_public_slot, + public_slot_by_label, + fundamental_labels, + antifundamental_labels, + adjoint_labels, + color_domain, + color_basis, + reference_unfolded_color_ordinal: None, + trace_reflections_folded: false, + helicity_count, + color_count, + } + } + + #[test] + fn permutation_and_split_ranks_are_exact_inverses() { + let values = [1, 3, 5, 7]; + for rank in 0..24 { + let selected = nth_permutation(&values, rank).unwrap(); + assert_eq!(permutation_rank(&values, &selected).unwrap(), rank); + } + for total in 0..6 { + for bins in 1..5 { + let count = checked_binomial(total + bins - 1, bins - 1, "test").unwrap(); + for rank in 0..count { + let selected = nth_weak_composition(total, bins, rank).unwrap(); + assert_eq!(selected.iter().sum::(), total); + assert_eq!(weak_composition_rank(&selected).unwrap(), rank); + } + } + } + } + + #[test] + fn adjoint_color_ordinals_permute_only_between_both_fixed_anchors() { + for (total, expected_count) in [(3, 1), (4, 2), (5, 6), (6, 24), (7, 120)] { + let roles = (1..=total) + .map(|label| (label, OnTheFlyExternalColorRoleV1::Adjoint, &[-1, 1][..])) + .collect::>(); + let selector = adapter_in_basis(&roles, OnTheFlyColorBasis::Adjoint); + assert_eq!(selector.color_count(), expected_count); + assert_eq!(selector.helicity_count(), 1 << total); + let mut words = BTreeSet::new(); + for index in 0..expected_count { + let (word, _) = selector.color_at(index).unwrap(); + assert_eq!(word.first(), Some(&1)); + assert_eq!(word.last(), Some(&total)); + assert_eq!(selector.color_ordinal(&word).unwrap(), index); + let (parsed, _) = selector.parse_color_id(&canonical_color_id(&word)).unwrap(); + assert_eq!(parsed, word); + assert!(words.insert(word)); + } + assert!(selector.color_at(expected_count).is_err()); + } + } + + #[test] + fn adjoint_reference_reordering_and_public_aliases_preserve_the_basis() { + let mut selector = adapter_in_basis( + &[ + (1, OnTheFlyExternalColorRoleV1::Adjoint, &[-1, 1]), + (2, OnTheFlyExternalColorRoleV1::Adjoint, &[-1, 1]), + (3, OnTheFlyExternalColorRoleV1::Adjoint, &[-1, 1]), + (4, OnTheFlyExternalColorRoleV1::Adjoint, &[-1, 1]), + ], + OnTheFlyColorBasis::Adjoint, + ); + assert!(selector.parse_color_id("flow:1,2,4,3").is_err()); + assert!(selector.unfolded_color_ordinal(&[1, 2, 4, 3]).is_err()); + assert!(selector.unfolded_color_ordinal(&[2, 1, 3, 4]).is_err()); + assert!(selector.unfolded_color_ordinal(&[1, 4]).is_err()); + selector.reference_unfolded_color_ordinal = Some(1); + for (index, expected) in [(0, [1, 3, 2, 4]), (1, [1, 2, 3, 4])] { + let (word, _) = selector.color_at(index).unwrap(); + assert_eq!(word.as_ref(), expected); + assert_eq!(selector.color_ordinal(&word).unwrap(), index); + } + let aliased = selector.with_public_permutation(&[3, 1, 2, 0]).unwrap(); + let (word, _) = aliased.parse_color_id("flow:4,3,2,1").unwrap(); + assert_eq!(word.as_ref(), [1, 3, 2, 4]); + assert_eq!(aliased.color_ordinal(&word).unwrap(), 0); + } + + #[test] + fn adjoint_basis_rejects_a_non_adjoint_seed_at_the_adapter_boundary() { + let seed = scalar_adapter_test_seed(digest(1), digest(2), digest(3), digest(4)).unwrap(); + let mut policy = OnTheFlyLcSelectorPolicyV1::complete(None, false); + policy.color_basis = OnTheFlyColorBasis::Adjoint; + assert!(OnTheFlyCompactSelectorAdapterV1::from_seed(&seed, policy).is_err()); + } + + #[test] + fn adjoint_color_ordinals_allow_certified_colourless_spectators() { + // Model/coupling-order certification is generation-side. Here only + // the structural selector domain is validated: singlets do not take + // part in the two-anchor colour permutation. + for gluon_count in [2, 3, 4] { + let mut sources: Vec<_> = (1..=gluon_count) + .map(|label| (label, OnTheFlyExternalColorRoleV1::Adjoint, &[-1, 1][..])) + .collect(); + sources.push((gluon_count + 1, OnTheFlyExternalColorRoleV1::Singlet, &[0])); + let selector = adapter_in_basis(&sources, OnTheFlyColorBasis::Adjoint); + assert_eq!(selector.helicity_count(), 1 << gluon_count); + assert_eq!( + selector.color_count(), + single_trace_color_count(gluon_count as usize, OnTheFlyColorBasis::Adjoint) + .unwrap() + ); + for index in 0..selector.color_count() { + let (word, _) = selector.color_at(index).unwrap(); + assert_eq!(word.len(), gluon_count as usize); + assert_eq!(word.first(), Some(&1)); + assert_eq!(word.last(), Some(&gluon_count)); + assert_eq!(selector.color_ordinal(&word).unwrap(), index); + } + } + } + + #[test] + fn public_ids_require_the_existing_canonical_spelling() { + assert_eq!(canonical_helicity_id(&[-1, 0, 1]), "h:-1,+0,+1"); + assert_eq!(canonical_color_id(&[]), "flow:singlet"); + assert_eq!(canonical_color_id(&[2, 5, 1]), "flow:2,5,1"); + } + + #[test] + fn direct_ordinal_decoder_matches_the_existing_lc_query_contract() { + let seed = scalar_adapter_test_seed(digest(1), digest(2), digest(3), digest(4)).unwrap(); + let selector = OnTheFlyCompactSelectorAdapterV1::from_seed( + &seed, + OnTheFlyLcSelectorPolicyV1::complete(None, false), + ) + .unwrap(); + let decoded = selector.decoded_query_at(&seed, 0, 0).unwrap(); + let expected = + DecodedLcQueryV1::new(&seed, vec![0, 1], &[0, 0], OnTheFlyLcSelectorV1::Singlet) + .unwrap(); + assert_eq!(decoded, expected); + } + + #[test] + fn six_fermion_compact_census_has_sixty_four_helicities_and_six_flows() { + let selector = adapter(&[ + (1, OnTheFlyExternalColorRoleV1::Fundamental, &[-1, 1]), + (2, OnTheFlyExternalColorRoleV1::Antifundamental, &[-1, 1]), + (3, OnTheFlyExternalColorRoleV1::Fundamental, &[-1, 1]), + (4, OnTheFlyExternalColorRoleV1::Antifundamental, &[-1, 1]), + (5, OnTheFlyExternalColorRoleV1::Fundamental, &[-1, 1]), + (6, OnTheFlyExternalColorRoleV1::Antifundamental, &[-1, 1]), + ]); + assert_eq!(selector.helicity_count(), 64); + assert_eq!(selector.color_count(), 6); + } + + #[test] + fn alias_permutation_round_trips_helicity_and_color_ids_without_changing_ordinals() { + let representative = adapter(&[ + (1, OnTheFlyExternalColorRoleV1::Fundamental, &[-1, 1]), + (2, OnTheFlyExternalColorRoleV1::Antifundamental, &[-1, 1]), + (3, OnTheFlyExternalColorRoleV1::Adjoint, &[-1, 1]), + (4, OnTheFlyExternalColorRoleV1::Adjoint, &[-1, 1]), + ]); + let representative_helicity = representative.parse_helicity_id("h:+1,-1,+1,-1").unwrap(); + let (representative_color, _) = representative.parse_color_id("flow:1,3,4,2").unwrap(); + let helicity_ordinal = representative + .helicity_ordinal(&representative_helicity) + .unwrap(); + let color_ordinal = representative.color_ordinal(&representative_color).unwrap(); + + let aliased = representative + .with_public_permutation(&[0, 1, 3, 2]) + .unwrap(); + let aliased_helicity = aliased.parse_helicity_id("h:+1,-1,-1,+1").unwrap(); + let (aliased_color, _) = aliased.parse_color_id("flow:1,4,3,2").unwrap(); + assert_eq!(aliased_helicity, representative_helicity); + assert_eq!(aliased_color, representative_color); + assert_eq!( + aliased.helicity_ordinal(&aliased_helicity).unwrap(), + helicity_ordinal + ); + assert_eq!( + aliased.color_ordinal(&aliased_color).unwrap(), + color_ordinal + ); + assert_eq!( + aliased + .public_helicities_from_representative(&representative_helicity) + .unwrap() + .as_ref(), + [1, -1, -1, 1] + ); + assert_eq!( + aliased + .public_labels_from_representative(&representative_color) + .unwrap() + .as_ref(), + [1, 4, 3, 2] + ); + } + + #[test] + fn deferred_public_physics_materializes_once_and_applies_alias_permutation_once() { + use crate::{ArtifactProcess, ArtifactSelection, ExternalParticle, ParticleRole}; + + let representative = adapter(&[ + (1, OnTheFlyExternalColorRoleV1::Fundamental, &[-1]), + (2, OnTheFlyExternalColorRoleV1::Antifundamental, &[1]), + (3, OnTheFlyExternalColorRoleV1::Adjoint, &[0]), + (4, OnTheFlyExternalColorRoleV1::Adjoint, &[2]), + ]); + let external_particles = [ + ("a", 1, ParticleRole::Initial), + ("b", -1, ParticleRole::Initial), + ("c", 21, ParticleRole::Final), + ("d", 22, ParticleRole::Final), + ] + .into_iter() + .enumerate() + .map(|(index, (particle, pdg, role))| ExternalParticle { + index, + label: index + 1, + particle: particle.to_string(), + pdg, + role, + momentum_slot: index, + momentum_components: ["E", "px", "py", "pz"].map(str::to_string), + }) + .collect(); + let metadata = super::super::on_the_fly_public_metadata::OnTheFlyPublicMetadataV1::for_test( + "a_b_to_c_d", + "a b > c d", + external_particles, + ); + let selection = ArtifactSelection { + process: ArtifactProcess { + id: "a_b_to_c_d".to_string(), + expression: "a b > c d".to_string(), + color_accuracy: "lc".to_string(), + external_pdgs: vec![1, -1, 21, 22], + physics_path: "processes/a_b_to_c_d/physics.json".to_string(), + required_runtime_capabilities: Vec::new(), + aliases: Vec::new(), + }, + requested_id: "a_b_to_d_c".to_string(), + alias: None, + public_expression: "a b > d c".to_string(), + external_pdgs: vec![1, -1, 22, 21], + external_permutation: vec![0, 1, 3, 2], + inferred_permutation: true, + process_group: None, + }; + let lazy = super::super::native_runtime::LazyProcessPhysicsV1::deferred_on_the_fly( + metadata, + representative, + selection, + ); + + let first = lazy.get().unwrap(); + let second = lazy.get().unwrap(); + assert!(std::ptr::eq(first, second)); + assert_eq!(first.process_id, "a_b_to_d_c"); + assert_eq!(first.process, "a b > d c"); + assert_eq!( + first + .external_particles + .iter() + .map(|particle| particle.pdg) + .collect::>(), + [1, -1, 22, 21] + ); + assert_eq!(first.helicities.len(), 1); + assert_eq!(first.helicities[0].id, "h:-1,+1,+2,+0"); + assert_eq!(first.helicities[0].values, [-1, 1, 2, 0]); + assert_eq!(first.color_components[0].id(), "flow:1,4,3,2"); + let crate::ColorComponent::LcFlow(flow) = &first.color_components[0] else { + panic!("on-the-fly public color axis changed kind") + }; + assert_eq!(flow.word, [1, 4, 3, 2]); + } + + #[test] + fn helicity_ids_validate_exact_seed_domains_and_canonical_signs() { + let adapter = adapter(&[ + (1, OnTheFlyExternalColorRoleV1::Singlet, &[-1, 1]), + (2, OnTheFlyExternalColorRoleV1::Singlet, &[0]), + ]); + assert_eq!(adapter.helicity_count(), 2); + assert_eq!(adapter.helicity_at(0).unwrap().as_ref(), [-1, 0]); + assert_eq!(adapter.helicity_at(1).unwrap().as_ref(), [1, 0]); + assert_eq!( + adapter.parse_helicity_id("h:+1,+0").unwrap().as_ref(), + [1, 0] + ); + assert!(adapter.parse_helicity_id("h:1,+0").is_err()); + assert!(adapter.parse_helicity_id("h:+1,+1").is_err()); + assert!(adapter.parse_helicity_id("h:+1").is_err()); + } + + #[test] + fn pure_adjoint_flows_keep_the_minimum_label_as_cyclic_anchor() { + let adapter = adapter(&[ + (3, OnTheFlyExternalColorRoleV1::Adjoint, &[-1, 1]), + (1, OnTheFlyExternalColorRoleV1::Adjoint, &[-1, 1]), + (2, OnTheFlyExternalColorRoleV1::Adjoint, &[-1, 1]), + ]); + assert_eq!(adapter.color_count(), 2); + assert_eq!(adapter.color_at(0).unwrap().0.as_ref(), [1, 2, 3]); + assert_eq!(adapter.color_at(1).unwrap().0.as_ref(), [1, 3, 2]); + assert!(adapter.parse_color_id("flow:1,3,2").is_ok()); + assert!(adapter.parse_color_id("flow:2,3,1").is_err()); + assert!(adapter.parse_color_id("flow:01,3,2").is_err()); + } + + #[test] + fn reference_word_reorders_one_complete_flow_without_changing_the_axis() { + let mut adapter = adapter(&[ + (1, OnTheFlyExternalColorRoleV1::Adjoint, &[-1, 1]), + (2, OnTheFlyExternalColorRoleV1::Adjoint, &[-1, 1]), + (3, OnTheFlyExternalColorRoleV1::Adjoint, &[-1, 1]), + ]); + adapter.reference_unfolded_color_ordinal = Some(1); + assert_eq!(adapter.color_at(0).unwrap().0.as_ref(), [1, 3, 2]); + assert_eq!(adapter.color_at(1).unwrap().0.as_ref(), [1, 2, 3]); + assert_eq!(adapter.color_ordinal(&[1, 3, 2]).unwrap(), 0); + assert_eq!(adapter.color_ordinal(&[1, 2, 3]).unwrap(), 1); + } + + #[test] + fn folded_trace_axis_retains_each_public_alias_in_generator_order() { + let mut adapter = adapter(&[ + (1, OnTheFlyExternalColorRoleV1::Adjoint, &[-1, 1]), + (2, OnTheFlyExternalColorRoleV1::Adjoint, &[-1, 1]), + (3, OnTheFlyExternalColorRoleV1::Adjoint, &[-1, 1]), + (4, OnTheFlyExternalColorRoleV1::Adjoint, &[-1, 1]), + ]); + adapter.trace_reflections_folded = true; + let expected = [ + [1, 2, 3, 4], + [1, 4, 3, 2], + [1, 2, 4, 3], + [1, 3, 4, 2], + [1, 3, 2, 4], + [1, 4, 2, 3], + ]; + for (index, word) in expected.iter().enumerate() { + assert_eq!(adapter.color_at(index).unwrap().0.as_ref(), word); + assert_eq!(adapter.color_ordinal(word).unwrap(), index); + } + + adapter.reference_unfolded_color_ordinal = Some(2); + let with_reference = [ + [1, 3, 2, 4], + [1, 4, 2, 3], + [1, 2, 3, 4], + [1, 4, 3, 2], + [1, 2, 4, 3], + [1, 3, 4, 2], + ]; + for (index, word) in with_reference.iter().enumerate() { + assert_eq!(adapter.color_at(index).unwrap().0.as_ref(), word); + assert_eq!(adapter.color_ordinal(word).unwrap(), index); + } + } + + #[test] + fn open_line_lazy_order_matches_the_established_generator() { + let adapter = adapter(&[ + (1, OnTheFlyExternalColorRoleV1::Antifundamental, &[-1, 1]), + (2, OnTheFlyExternalColorRoleV1::Fundamental, &[-1, 1]), + (3, OnTheFlyExternalColorRoleV1::Fundamental, &[-1, 1]), + (4, OnTheFlyExternalColorRoleV1::Antifundamental, &[-1, 1]), + (5, OnTheFlyExternalColorRoleV1::Adjoint, &[-1, 1]), + (6, OnTheFlyExternalColorRoleV1::Adjoint, &[-1, 1]), + ]); + assert_eq!(adapter.color_count(), 12); + let first = [ + [2, 1, 3, 5, 6, 4], + [2, 5, 1, 3, 6, 4], + [2, 5, 6, 1, 3, 4], + [2, 1, 3, 6, 5, 4], + ]; + for (index, expected) in first.iter().enumerate() { + let (actual, _) = adapter.color_at(index).unwrap(); + assert_eq!(actual.as_ref(), expected); + assert_eq!(adapter.color_ordinal(&actual).unwrap(), index); + assert!(adapter.parse_color_id(&canonical_color_id(&actual)).is_ok()); + } + assert!(adapter.parse_color_id("flow:3,5,6,4,2,1").is_err()); + assert!(adapter.parse_color_id("flow:2,5,1,4,6,3").is_err()); + } +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/engine/on_the_fly_warm_up.rs b/pyamplicol/rust/crates/rusticol-core/src/engine/on_the_fly_warm_up.rs new file mode 100644 index 0000000000..2913a992b2 --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/engine/on_the_fly_warm_up.rs @@ -0,0 +1,230 @@ +// SPDX-License-Identifier: 0BSD + +//! Structured progress for the explicit one-point on-the-fly warm-up API. +//! +//! Ordinary evaluation never constructs this state. In particular, resident +//! memory sampling is performed only while an explicit observer is installed, +//! so the warmed evaluation path acquires no clock, allocation, or syscall. + +use crate::{RusticolError, RusticolResult}; +use serde::Serialize; +use std::time::{Duration, Instant}; + +const PROGRESS_UPDATE_INTERVAL: Duration = Duration::from_millis(250); + +#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum NativeOnTheFlyWarmUpEventKind { + Start, + Update, + End, +} + +#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum NativeOnTheFlyWarmUpStage { + ProcessPreparation, + QueryFamily, + FamilyFinalization, + FirstEvaluation, +} + +/// One structured warm-up progress event. A renderer is deliberately not +/// part of rusticol-core; Python, a CLI, or another embedding chooses how to +/// display these snapshots. +#[derive(Clone, Debug, PartialEq, Serialize)] +pub struct NativeOnTheFlyWarmUpEvent { + pub schema_version: u32, + pub kind: NativeOnTheFlyWarmUpEventKind, + pub stage: NativeOnTheFlyWarmUpStage, + pub completed: u64, + pub total: u64, + pub elapsed_seconds: f64, + pub current_rss_bytes: Option, + pub peak_rss_bytes: Option, + pub workers: u64, + pub message: Option, +} + +/// Summary of an explicit one-point binary64 warm-up transaction. +#[derive(Clone, Debug, PartialEq, Serialize)] +pub struct NativeOnTheFlyWarmUpResult { + pub schema_version: u32, + pub elapsed_seconds: f64, + pub query_count: u64, + pub warmed_query_count: u64, + pub current_rss_bytes: Option, + pub peak_rss_bytes: Option, + pub already_warm: bool, + pub first_evaluation_completed: bool, +} + +/// Returning `false` requests cancellation at the next observable cold-path +/// boundary. Observer errors and cancellation are propagated through the +/// same pending-family rollback used by ordinary evaluation failures. The one +/// exception is the terminal `End` event for `FirstEvaluation`: it is an +/// informational notification delivered after commit, so its return value or +/// error cannot undo a completed warm-up. +pub type NativeOnTheFlyWarmUpObserver<'a> = + dyn FnMut(&NativeOnTheFlyWarmUpEvent) -> RusticolResult + 'a; + +pub(super) struct OnTheFlyWarmUpProgress<'a> { + observer: Option<&'a mut NativeOnTheFlyWarmUpObserver<'a>>, + started: Instant, + current_rss_bytes: Option, + peak_rss_bytes: Option, + workers: u64, + last_update_delivery: Option, +} + +impl<'a> OnTheFlyWarmUpProgress<'a> { + pub(super) fn new( + observer: Option<&'a mut NativeOnTheFlyWarmUpObserver<'a>>, + workers: usize, + ) -> RusticolResult { + let workers = u64::try_from(workers) + .map_err(|_| RusticolError::invalid_argument("warm-up worker count exceeds u64"))?; + Ok(Self { + observer, + started: Instant::now(), + current_rss_bytes: None, + peak_rss_bytes: None, + workers, + last_update_delivery: None, + }) + } + + pub(super) fn elapsed_seconds(&self) -> f64 { + self.started.elapsed().as_secs_f64() + } + + pub(super) const fn current_rss_bytes(&self) -> Option { + self.current_rss_bytes + } + + pub(super) const fn peak_rss_bytes(&self) -> Option { + self.peak_rss_bytes + } + + pub(super) fn emit( + &mut self, + kind: NativeOnTheFlyWarmUpEventKind, + stage: NativeOnTheFlyWarmUpStage, + completed: usize, + total: usize, + message: Option<&str>, + ) -> RusticolResult<()> { + if self.observer.is_none() { + return Ok(()); + } + if kind == NativeOnTheFlyWarmUpEventKind::Update { + let now = Instant::now(); + if self + .last_update_delivery + .is_some_and(|last| now.duration_since(last) < PROGRESS_UPDATE_INTERVAL) + { + return Ok(()); + } + self.last_update_delivery = Some(now); + } + self.deliver(kind, stage, completed, total, message, true) + } + + /// Publish the post-commit completion notification. The selected family + /// and its public identity are already committed at this boundary, so a + /// rejecting or failing observer cannot retroactively turn the completed + /// warm-up into an error. Embeddings may report such delivery failures as + /// unraisable diagnostics, but must not roll back physics state. + pub(super) fn emit_terminal_notification( + &mut self, + stage: NativeOnTheFlyWarmUpStage, + completed: usize, + total: usize, + message: Option<&str>, + ) { + let _ = self.deliver( + NativeOnTheFlyWarmUpEventKind::End, + stage, + completed, + total, + message, + false, + ); + } + + fn deliver( + &mut self, + kind: NativeOnTheFlyWarmUpEventKind, + stage: NativeOnTheFlyWarmUpStage, + completed: usize, + total: usize, + message: Option<&str>, + cancellable: bool, + ) -> RusticolResult<()> { + let Some(observer) = self.observer.as_deref_mut() else { + return Ok(()); + }; + let current_rss_bytes = current_process_rss_bytes(); + if let Some(current) = current_rss_bytes { + self.current_rss_bytes = Some(current); + self.peak_rss_bytes = Some(self.peak_rss_bytes.unwrap_or(0).max(current)); + } + let event = NativeOnTheFlyWarmUpEvent { + schema_version: 1, + kind, + stage, + completed: u64::try_from(completed).map_err(|_| { + RusticolError::invalid_argument("warm-up completed count exceeds u64") + })?, + total: u64::try_from(total) + .map_err(|_| RusticolError::invalid_argument("warm-up total exceeds u64"))?, + elapsed_seconds: self.started.elapsed().as_secs_f64(), + current_rss_bytes: self.current_rss_bytes, + peak_rss_bytes: self.peak_rss_bytes, + workers: self.workers, + message: message.map(str::to_owned), + }; + match observer(&event) { + Ok(true) => Ok(()), + Ok(false) if !cancellable => Ok(()), + Ok(false) => Err(RusticolError::evaluation( + "on-the-fly warm-up cancelled by its progress observer", + )), + Err(_) if !cancellable => Ok(()), + Err(error) => Err(error), + } + } +} + +#[cfg(target_os = "macos")] +fn current_process_rss_bytes() -> Option { + let mut info = std::mem::MaybeUninit::::zeroed(); + let size = std::mem::size_of::(); + let written = unsafe { + libc::proc_pidinfo( + libc::getpid(), + libc::PROC_PIDTASKINFO, + 0, + info.as_mut_ptr().cast(), + i32::try_from(size).ok()?, + ) + }; + if usize::try_from(written).ok()? != size { + return None; + } + Some(unsafe { info.assume_init() }.pti_resident_size) +} + +#[cfg(target_os = "linux")] +fn current_process_rss_bytes() -> Option { + let statm = std::fs::read_to_string("/proc/self/statm").ok()?; + let resident_pages = statm.split_whitespace().nth(1)?.parse::().ok()?; + let page_size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) }; + let page_size = u64::try_from(page_size).ok()?; + resident_pages.checked_mul(page_size) +} + +#[cfg(not(any(target_os = "linux", target_os = "macos")))] +const fn current_process_rss_bytes() -> Option { + None +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/engine/physics.rs b/pyamplicol/rust/crates/rusticol-core/src/engine/physics.rs new file mode 100644 index 0000000000..4ecdf95ff2 --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/engine/physics.rs @@ -0,0 +1,1370 @@ +// SPDX-License-Identifier: 0BSD + +use super::*; + +static NEXT_PHYSICS_RUNTIME_BINDING_ID: std::sync::atomic::AtomicU64 = + std::sync::atomic::AtomicU64::new(1); + +impl PhysicsRuntime { + #[cfg_attr(target_vendor = "apple", unsafe(link_section = "__TEXT,__rcl_load"))] + #[cfg_attr(target_vendor = "apple", inline(never))] + pub(super) fn new(manifest: ProcessPhysicsV1) -> RusticolResult { + manifest.validate()?; + let helicity_index_by_id = manifest + .helicities + .iter() + .enumerate() + .map(|(index, helicity)| (helicity.id.clone(), index)) + .collect::>(); + let helicity_representative_index = manifest + .helicities + .iter() + .map(|helicity| { + helicity_index_by_id + .get(&helicity.representative_id) + .copied() + .ok_or_else(|| { + RusticolError::artifact(format!( + "helicity {} references absent representative {}", + helicity.id, helicity.representative_id + )) + }) + }) + .collect::>>()?; + let mut helicity_members_by_representative = vec![Vec::new(); manifest.helicities.len()]; + for (physical_index, representative_index) in + helicity_representative_index.iter().copied().enumerate() + { + if !manifest.helicities[physical_index].structural_zero { + helicity_members_by_representative[representative_index].push(physical_index); + } + } + let color_index_by_id = manifest + .color_components + .iter() + .enumerate() + .map(|(index, color)| (color.id().to_string(), index)) + .collect::>(); + let mut reduction_by_group_id = BTreeMap::new(); + let mut numeric_reduction_by_group_id = BTreeMap::new(); + for group in &manifest.reduction.groups { + let group_id = parse_reduction_group_id(&group.id)?; + let mut expected_helicity_weight = None; + for id in &group.physical_helicity_ids { + let index = helicity_index_by_id[id]; + let coefficient = manifest.helicities[index].coefficient; + if let Some(expected) = expected_helicity_weight { + if coefficient.to_bits() != expected { + return Err(RusticolError::artifact(format!( + "resolved reduction group {} has inconsistent helicity weights", + group.id + ))); + } + } else { + expected_helicity_weight = Some(coefficient.to_bits()); + } + } + if reduction_by_group_id + .insert(group_id, group.clone()) + .is_some() + { + return Err(RusticolError::artifact(format!( + "resolved reduction groups map to duplicate evaluator id {group_id}" + ))); + } + if numeric_reduction_by_group_id + .insert( + group_id, + numeric_reduction_group( + group, + &manifest, + &helicity_index_by_id, + &color_index_by_id, + )?, + ) + .is_some() + { + return Err(RusticolError::artifact(format!( + "resolved reduction groups map to duplicate numeric evaluator id {group_id}" + ))); + } + } + Ok(Self { + binding_id: NEXT_PHYSICS_RUNTIME_BINDING_ID + .fetch_add(1, std::sync::atomic::Ordering::Relaxed), + manifest, + helicity_index_by_id, + helicity_members_by_representative, + color_index_by_id, + reduction_by_group_id, + numeric_reduction_by_group_id, + }) + } + + pub(super) fn selected_helicity_indices( + &self, + ids: Option<&BTreeSet>, + ) -> RusticolResult> { + let mut indices = Vec::new(); + self.select_indices_into(ids, &self.helicity_index_by_id, "helicity", &mut indices)?; + Ok(indices) + } + + pub(super) fn selected_color_indices( + &self, + ids: Option<&BTreeSet>, + ) -> RusticolResult> { + let mut indices = Vec::new(); + self.select_indices_into( + ids, + &self.color_index_by_id, + "color component", + &mut indices, + )?; + Ok(indices) + } + + pub(super) fn canonical_total_helicity_selector<'a>( + &self, + ids: Option<&'a BTreeSet>, + ) -> Option<&'a BTreeSet> { + canonical_total_axis_selector(ids, &self.helicity_index_by_id) + } + + pub(super) fn canonical_total_color_selector<'a>( + &self, + ids: Option<&'a BTreeSet>, + ) -> Option<&'a BTreeSet> { + canonical_total_axis_selector(ids, &self.color_index_by_id) + } + + pub(super) fn validate_helicity_id_slice(&self, ids: Option<&[String]>) -> RusticolResult<()> { + self.validate_id_slice(ids, &self.helicity_index_by_id, "helicity") + } + + pub(super) fn validate_color_id_slice(&self, ids: Option<&[String]>) -> RusticolResult<()> { + self.validate_id_slice(ids, &self.color_index_by_id, "color component") + } + + fn validate_id_slice( + &self, + ids: Option<&[String]>, + available: &BTreeMap, + kind: &str, + ) -> RusticolResult<()> { + if let Some(ids) = ids { + for id in ids { + if !available.contains_key(id) { + return Err(RusticolError::selector(format!( + "unknown resolved {kind} id {id:?}" + ))); + } + } + } + Ok(()) + } + + fn select_indices_into( + &self, + ids: Option<&BTreeSet>, + available: &BTreeMap, + kind: &str, + indices: &mut Vec, + ) -> RusticolResult<()> { + indices.clear(); + if let Some(ids) = ids { + for id in ids { + let index = available.get(id).ok_or_else(|| { + RusticolError::selector(format!("unknown resolved {kind} id {id:?}")) + })?; + indices.push(*index); + } + indices.sort_unstable(); + } else { + indices.extend(0..available.len()); + } + Ok(()) + } + + pub(super) fn selected_helicity_indices_into( + &self, + ids: Option<&BTreeSet>, + indices: &mut Vec, + ) -> RusticolResult<()> { + self.select_indices_into(ids, &self.helicity_index_by_id, "helicity", indices) + } + + pub(super) fn selected_color_indices_into( + &self, + ids: Option<&BTreeSet>, + indices: &mut Vec, + ) -> RusticolResult<()> { + self.select_indices_into(ids, &self.color_index_by_id, "color component", indices) + } + + pub(super) fn has_contracted_color_axis(&self) -> bool { + matches!( + self.manifest.color_components.as_slice(), + [PhysicsColorComponentV1::ContractedColor(_)] + ) + } + + pub(super) fn color_is_computed(&self, index: usize) -> bool { + match &self.manifest.color_components[index] { + PhysicsColorComponentV1::LcFlow(flow) => flow.computed, + PhysicsColorComponentV1::ContractedColor(_) => true, + } + } + + #[inline(always)] + pub(super) fn helicity_orbit_members(&self, representative: usize) -> &[usize] { + self.helicity_members_by_representative + .get(representative) + .map(Vec::as_slice) + .unwrap_or_default() + } + + pub(super) fn normalized_helicity_weights( + &self, + group: &crate::ReductionGroup, + ) -> RusticolResult> { + let mut weights = Vec::new(); + self.normalized_helicity_weights_into(group, &mut weights)?; + Ok(weights) + } + + pub(super) fn normalized_helicity_weights_into( + &self, + group: &crate::ReductionGroup, + weights: &mut Vec<(usize, f64)>, + ) -> RusticolResult<()> { + weights.clear(); + weights + .try_reserve(group.physical_helicity_ids.len()) + .map_err(|error| { + RusticolError::invalid_argument(format!( + "could not reserve normalized helicity weights: {error}" + )) + })?; + let mut total = 0.0; + for id in &group.physical_helicity_ids { + let index = self.helicity_index_by_id[id]; + let coefficient = self.manifest.helicities[index].coefficient; + total += coefficient; + weights.push((index, coefficient)); + } + if !total.is_finite() || total <= 0.0 { + return Err(RusticolError::artifact(format!( + "resolved reduction group {} has no positive helicity weight", + group.id + ))); + } + for (_, weight) in weights.iter_mut() { + *weight /= total; + } + Ok(()) + } + + pub(super) fn normalized_member_weights( + &self, + group: &crate::ReductionGroup, + ) -> RusticolResult> { + let mut weights = Vec::new(); + self.normalized_member_weights_into(group, &mut weights)?; + Ok(weights) + } + + pub(super) fn normalized_member_weights_into( + &self, + group: &crate::ReductionGroup, + weights: &mut Vec<(usize, usize, f64)>, + ) -> RusticolResult<()> { + weights.clear(); + let mut total = 0.0; + for helicity_id in &group.physical_helicity_ids { + let helicity_index = self.helicity_index_by_id[helicity_id]; + let helicity_weight = self.manifest.helicities[helicity_index].coefficient; + for color_id in &group.physical_color_ids { + let color_index = self.color_index_by_id[color_id]; + let weight = + helicity_weight * self.manifest.color_components[color_index].coefficient(); + total += weight; + weights.push((helicity_index, color_index, weight)); + } + } + if !total.is_finite() || total <= 0.0 { + return Err(RusticolError::artifact(format!( + "resolved reduction group {} has no positive member weight", + group.id + ))); + } + for (_, _, weight) in weights.iter_mut() { + *weight /= total; + } + Ok(()) + } + + pub(super) fn lc_resolved_replay_plan( + &self, + mappings: &LcTopologyReplayMappings, + sector_routes: &[Vec], + materialized_sector_by_id: &BTreeMap, + ) -> RusticolResult { + if self.manifest.color_accuracy.as_str() != "lc" { + return Err(RusticolError::artifact( + "LC topology replay requires LC resolved physics metadata", + )); + } + if mappings.len() != sector_routes.len() { + return Err(RusticolError::artifact( + "LC topology replay mappings and sector routes have inconsistent lengths", + )); + } + + let helicity_count = self.manifest.helicities.len(); + let color_count = self.manifest.color_components.len(); + let mut relevant_helicities = BTreeSet::new(); + let mut relevant_colors = BTreeSet::new(); + for reduction in self.reduction_by_group_id.values() { + for id in &reduction.physical_helicity_ids { + relevant_helicities.insert(self.helicity_index_by_id[id]); + } + for id in &reduction.physical_color_ids { + relevant_colors.insert(self.color_index_by_id[id]); + } + } + if relevant_helicities.is_empty() || relevant_colors.is_empty() { + return Err(RusticolError::artifact( + "LC topology replay has no materialized resolved members", + )); + } + + let helicity_index_by_values = self + .manifest + .helicities + .iter() + .enumerate() + .map(|(index, helicity)| (helicity.values.clone(), index)) + .collect::>(); + if helicity_index_by_values.len() != helicity_count { + return Err(RusticolError::artifact( + "resolved physics contains duplicate helicity vectors", + )); + } + let mut color_index_by_word = BTreeMap::new(); + for (index, color) in self.manifest.color_components.iter().enumerate() { + let PhysicsColorComponentV1::LcFlow(flow) = color else { + return Err(RusticolError::artifact( + "LC topology replay encountered a contracted color component", + )); + }; + if color_index_by_word + .insert(flow.word.clone(), index) + .is_some() + { + return Err(RusticolError::artifact( + "resolved physics contains duplicate LC flow words", + )); + } + } + + let mut entries = Vec::with_capacity(mappings.len()); + let mut target_sector_by_color = BTreeMap::new(); + for (mapping, mapping_routes) in mappings.iter().zip(sector_routes) { + validate_public_label_permutation(mapping, self.manifest.external_particles.len())?; + let mut helicity_targets = BTreeMap::new(); + for source_index in &relevant_helicities { + let source = &self.manifest.helicities[*source_index]; + let target_values = permute_public_helicity(&source.values, mapping); + let target_index = *helicity_index_by_values.get(&target_values).ok_or_else(|| { + RusticolError::compatibility(format!( + "resolved physics is missing replayed helicity vector {target_values:?}; regenerate the artifact with complete topology-replay reductions" + )) + })?; + let target = &self.manifest.helicities[target_index]; + if source.coefficient.to_bits() != target.coefficient.to_bits() { + return Err(RusticolError::artifact(format!( + "LC topology replay maps helicity {} to {} with a different reduction coefficient", + source.id, target.id + ))); + } + helicity_targets.insert(*source_index, target_index); + } + + let mut routes = Vec::new(); + for sector_route in mapping_routes { + let source_sector = *materialized_sector_by_id + .get(§or_route.materialized_sector_id) + .ok_or_else(|| { + RusticolError::artifact(format!( + "LC topology replay materialized sector {} has no computed physics component", + sector_route.materialized_sector_id + )) + })?; + let source_index = source_sector.color_index; + let PhysicsColorComponentV1::LcFlow(source) = + &self.manifest.color_components[source_index] + else { + return Err(RusticolError::artifact( + "LC topology replay encountered a contracted materialized color component", + )); + }; + if !source.computed || source.representative_id != source.id { + return Err(RusticolError::artifact(format!( + "LC topology replay sector {} is not bound to a self-representing computed color component", + sector_route.materialized_sector_id + ))); + } + let target_word = permute_public_color_word(&source.word, mapping); + let reduction_weight = if sector_route.residual { + source_sector.reduction_weight + } else { + sector_route.squared_reduction_weight() + }; + let target_words = replay_public_color_words(&target_word, reduction_weight)?; + let mut target_coefficients = BTreeMap::new(); + for word in target_words { + let target_index = *color_index_by_word.get(&word).ok_or_else(|| { + RusticolError::compatibility(format!( + "resolved physics is missing replayed LC flow word {word:?}; regenerate the artifact with replay-to-public-flow reductions" + )) + })?; + let coefficient = self.manifest.color_components[target_index].coefficient(); + if !coefficient.is_finite() || coefficient <= 0.0 { + return Err(RusticolError::artifact(format!( + "replayed LC flow {} has no positive reduction coefficient", + self.manifest.color_components[target_index].id() + ))); + } + let PhysicsColorComponentV1::LcFlow(target) = + &self.manifest.color_components[target_index] + else { + unreachable!("LC color axes checked above"); + }; + if target.representative_id != source.id { + return Err(RusticolError::artifact(format!( + "LC topology replay physical sector {} targets color {} represented by {}, expected {}", + sector_route.physical_sector_id, + target.id, + target.representative_id, + source.id + ))); + } + if let Some(previous_sector) = + target_sector_by_color.insert(target_index, sector_route.physical_sector_id) + && previous_sector != sector_route.physical_sector_id + { + return Err(RusticolError::artifact(format!( + "LC topology replay physical sectors {previous_sector} and {} overlap color {}", + sector_route.physical_sector_id, target.id + ))); + } + target_coefficients + .entry(target_index) + .or_insert(coefficient); + } + let total = target_coefficients.values().sum::(); + if !total.is_finite() || total <= 0.0 { + return Err(RusticolError::artifact( + "LC topology replay has no positive public-flow reduction weight", + )); + } + for (source_helicity, target_helicity) in &helicity_targets { + for (target_color, coefficient) in &target_coefficients { + routes.push(LcResolvedReplayRoute { + source_index: source_helicity * color_count + source_index, + target_index: target_helicity * color_count + target_color, + weight: reduction_weight * coefficient / total, + }); + } + } + } + if routes.is_empty() { + return Err(RusticolError::artifact( + "LC topology replay mapping contains no resolved component routes", + )); + } + entries.push(LcResolvedReplayEntry { routes }); + } + if target_sector_by_color.len() != color_count { + return Err(RusticolError::artifact(format!( + "LC topology replay covers {} of {color_count} physical color components", + target_sector_by_color.len() + ))); + } + let mut routes_by_target = vec![Vec::new(); helicity_count * color_count]; + for (mapping_index, entry) in entries.iter().enumerate() { + for route in &entry.routes { + routes_by_target[route.target_index].push(LcResolvedReplayTargetRoute { + mapping_index, + source_index: route.source_index, + weight: route.weight, + }); + } + } + Ok(LcResolvedReplayPlan { + #[cfg(test)] + entries, + routes_by_target, + color_count, + }) + } + + #[cfg(test)] + pub(super) fn select_lc_resolved_replay_plan( + &self, + plan: &LcResolvedReplayPlan, + selected_helicity_ids: Option<&BTreeSet>, + selected_color_ids: Option<&BTreeSet>, + ) -> RusticolResult { + let key = + self.lc_resolved_replay_selection_key(selected_helicity_ids, selected_color_ids)?; + self.select_lc_resolved_replay_plan_for_key(plan, &key) + } + + pub(super) fn lc_resolved_replay_selection_key( + &self, + selected_helicity_ids: Option<&BTreeSet>, + selected_color_ids: Option<&BTreeSet>, + ) -> RusticolResult { + Ok(LcResolvedReplaySelectionKey { + helicity_indices: selected_helicity_ids + .map(|ids| self.selected_helicity_indices(Some(ids))) + .transpose()?, + color_indices: selected_color_ids + .map(|ids| self.selected_color_indices(Some(ids))) + .transpose()?, + }) + } + + pub(super) fn select_lc_resolved_replay_plan_for_key( + &self, + plan: &LcResolvedReplayPlan, + key: &LcResolvedReplaySelectionKey, + ) -> RusticolResult { + let helicity_indices = key + .helicity_indices + .clone() + .unwrap_or_else(|| (0..self.manifest.helicities.len()).collect()); + let color_indices = key + .color_indices + .clone() + .unwrap_or_else(|| (0..self.manifest.color_components.len()).collect()); + let mut compact_target_by_full = BTreeMap::new(); + for (helicity_position, helicity_index) in helicity_indices.iter().enumerate() { + for (color_position, color_index) in color_indices.iter().enumerate() { + compact_target_by_full.insert( + helicity_index * plan.color_count + color_index, + helicity_position * color_indices.len() + color_position, + ); + } + } + + let mut routes_by_mapping = BTreeMap::>::new(); + for (target_full_index, target_compact_index) in &compact_target_by_full { + let routes = plan + .routes_by_target + .get(*target_full_index) + .ok_or_else(|| { + RusticolError::integrity( + "LC topology replay target index is outside its route index", + ) + })?; + for route in routes { + routes_by_mapping + .entry(route.mapping_index) + .or_default() + .push(LcResolvedReplayRoute { + source_index: route.source_index, + target_index: *target_compact_index, + weight: route.weight, + }); + } + } + + let mut mapping_indices = Vec::with_capacity(routes_by_mapping.len()); + let mut entries = Vec::with_capacity(routes_by_mapping.len()); + let mut source_helicity_indices = Vec::new(); + let mut source_color_indices = Vec::new(); + for (mapping_index, selected_routes) in routes_by_mapping { + let source_helicities = selected_routes + .iter() + .map(|route| route.source_index / plan.color_count) + .collect::>() + .into_iter() + .collect::>(); + let source_colors = selected_routes + .iter() + .map(|route| route.source_index % plan.color_count) + .collect::>() + .into_iter() + .collect::>(); + let compact_helicity = source_helicities + .iter() + .copied() + .enumerate() + .map(|(position, index)| (index, position)) + .collect::>(); + let compact_color = source_colors + .iter() + .copied() + .enumerate() + .map(|(position, index)| (index, position)) + .collect::>(); + let routes = selected_routes + .into_iter() + .map(|route| { + let source_helicity = route.source_index / plan.color_count; + let source_color = route.source_index % plan.color_count; + LcResolvedReplayRoute { + source_index: compact_helicity[&source_helicity] * source_colors.len() + + compact_color[&source_color], + target_index: route.target_index, + weight: route.weight, + } + }) + .collect(); + mapping_indices.push(mapping_index); + entries.push(LcResolvedReplayEntry { routes }); + source_helicity_indices.push(source_helicities); + source_color_indices.push(source_colors); + } + if entries.len() != mapping_indices.len() + || entries.len() != source_helicity_indices.len() + || entries.len() != source_color_indices.len() + { + return Err(RusticolError::integrity( + "LC topology replay selection has inconsistent mappings and entries", + )); + } + let mut entry_indices_by_source = BTreeMap::<(Vec, Vec), Vec>::new(); + for entry_index in 0..entries.len() { + entry_indices_by_source + .entry(( + source_helicity_indices[entry_index].clone(), + source_color_indices[entry_index].clone(), + )) + .or_default() + .push(entry_index); + } + let source_groups = entry_indices_by_source + .into_iter() + .map(|((helicity_indices, color_indices), entry_indices)| { + let helicity_ids = helicity_indices + .iter() + .map(|index| self.manifest.helicities[*index].id.clone()) + .collect(); + let color_ids = color_indices + .iter() + .map(|index| self.manifest.color_components[*index].id().to_string()) + .collect(); + LcResolvedReplaySourceGroup { + mapping_indices: entry_indices + .iter() + .map(|entry_index| mapping_indices[*entry_index]) + .collect(), + entries: entry_indices + .iter() + .map(|entry_index| entries[*entry_index].clone()) + .collect(), + helicity_ids, + color_ids, + materialized_sector_ids: None, + direct_total_plan: None, + source_component_count: helicity_indices.len() * color_indices.len(), + } + }) + .collect(); + Ok(LcResolvedReplaySelection { + #[cfg(test)] + mapping_indices, + #[cfg(test)] + entries, + #[cfg(test)] + source_helicity_indices, + #[cfg(test)] + source_color_indices, + source_groups, + helicity_indices, + color_indices, + }) + } +} + +fn canonical_total_axis_selector<'a>( + ids: Option<&'a BTreeSet>, + available: &BTreeMap, +) -> Option<&'a BTreeSet> { + ids.filter(|ids| { + ids.len() != available.len() || ids.iter().any(|id| !available.contains_key(id)) + }) +} + +fn numeric_reduction_group( + group: &crate::ReductionGroup, + manifest: &ProcessPhysicsV1, + helicity_index_by_id: &BTreeMap, + color_index_by_id: &BTreeMap, +) -> RusticolResult { + let mut physical_helicity_indices = Vec::with_capacity(group.physical_helicity_ids.len()); + let mut helicity_membership = vec![false; manifest.helicities.len()]; + let mut normalized_helicity_weights = Vec::with_capacity(group.physical_helicity_ids.len()); + let mut total_helicity_weight = 0.0; + for id in &group.physical_helicity_ids { + let index = *helicity_index_by_id.get(id).ok_or_else(|| { + RusticolError::artifact(format!( + "resolved reduction group {} references unknown helicity {id:?}", + group.id + )) + })?; + let coefficient = manifest.helicities[index].coefficient; + physical_helicity_indices.push(index); + helicity_membership[index] = true; + normalized_helicity_weights.push((index, coefficient)); + total_helicity_weight += coefficient; + } + if !total_helicity_weight.is_finite() || total_helicity_weight <= 0.0 { + return Err(RusticolError::artifact(format!( + "resolved reduction group {} has no positive helicity weight", + group.id + ))); + } + for (_, weight) in &mut normalized_helicity_weights { + *weight /= total_helicity_weight; + } + + let mut physical_color_weights = Vec::with_capacity(group.physical_color_ids.len()); + let mut total_color_weight = 0.0; + for id in &group.physical_color_ids { + let index = *color_index_by_id.get(id).ok_or_else(|| { + RusticolError::artifact(format!( + "resolved reduction group {} references unknown color {id:?}", + group.id + )) + })?; + let coefficient = manifest.color_components[index].coefficient(); + physical_color_weights.push((index, coefficient)); + total_color_weight += coefficient; + } + if !total_color_weight.is_finite() || total_color_weight <= 0.0 { + return Err(RusticolError::artifact(format!( + "resolved reduction group {} has no positive color weight", + group.id + ))); + } + let mut normalized_color_weights = physical_color_weights.clone(); + for (_, weight) in &mut normalized_color_weights { + *weight /= total_color_weight; + } + + let member_count = group + .physical_helicity_ids + .len() + .checked_mul(group.physical_color_ids.len()) + .ok_or_else(|| RusticolError::artifact("resolved reduction member count overflows"))?; + let mut normalized_member_weights = Vec::with_capacity(member_count); + let mut total_member_weight = 0.0; + for helicity_id in &group.physical_helicity_ids { + let helicity_index = helicity_index_by_id[helicity_id]; + let helicity_weight = manifest.helicities[helicity_index].coefficient; + for (color_index, color_weight) in &physical_color_weights { + let weight = helicity_weight * *color_weight; + total_member_weight += weight; + normalized_member_weights.push((helicity_index, *color_index, weight)); + } + } + if !total_member_weight.is_finite() || total_member_weight <= 0.0 { + return Err(RusticolError::artifact(format!( + "resolved reduction group {} has no positive member weight", + group.id + ))); + } + for (_, _, weight) in &mut normalized_member_weights { + *weight /= total_member_weight; + } + + Ok(NumericReductionGroup { + physical_helicity_indices, + helicity_membership, + normalized_helicity_weights, + normalized_color_weights, + normalized_member_weights, + }) +} + +impl ExecutionRuntime { + pub(super) fn set_external_pdg_order_recursive(&mut self, pdgs: &[i32]) { + self.external_pdg_order = pdgs.to_vec(); + if let Some(sum_runtime) = self.helicity_sum_runtime.as_mut() { + sum_runtime.set_external_pdg_order_recursive(pdgs); + } + for selector_runtime in &mut self.helicity_selector_runtimes { + selector_runtime.set_external_pdg_order_recursive(pdgs); + } + for selector_runtime in self.color_selector_runtimes.values_mut() { + selector_runtime.set_external_pdg_order_recursive(pdgs); + } + } + + pub(super) fn remap_physics_reduction_overrides( + &mut self, + helicity_id_map: &BTreeMap, + color_id_map: &BTreeMap, + ) -> RusticolResult<()> { + self.lc_resolved_replay_plan = None; + self.lc_resolved_replay_selection_cache = None; + if let Some(reduction) = self.physics_reduction_override.as_mut() { + remap_reduction_ids(reduction, helicity_id_map, color_id_map)?; + } + if let Some(sum_runtime) = self.helicity_sum_runtime.as_mut() { + sum_runtime.remap_physics_reduction_overrides(helicity_id_map, color_id_map)?; + } + for selector_runtime in &mut self.helicity_selector_runtimes { + selector_runtime.remap_physics_reduction_overrides(helicity_id_map, color_id_map)?; + } + for selector_runtime in self.color_selector_runtimes.values_mut() { + selector_runtime.remap_physics_reduction_overrides(helicity_id_map, color_id_map)?; + } + Ok(()) + } + + pub(super) fn attach_physics(&mut self, physics: Arc) -> RusticolResult<()> { + self.lc_resolved_replay_plan = None; + self.lc_resolved_replay_selection_cache = None; + self.physics = Some(Arc::clone(&physics)); + if let Some(sum_runtime) = self.helicity_sum_runtime.as_mut() { + let sum_physics = + if let Some(reduction) = sum_runtime.physics_reduction_override.as_ref() { + let mut manifest = physics.manifest.clone(); + manifest.reduction = reduction.clone(); + Arc::new(PhysicsRuntime::new(manifest)?) + } else { + Arc::clone(&physics) + }; + sum_runtime.attach_physics(sum_physics)?; + } + for selector_runtime in &mut self.helicity_selector_runtimes { + let selector_physics = + if let Some(reduction) = selector_runtime.physics_reduction_override.as_ref() { + let mut manifest = physics.manifest.clone(); + manifest.reduction = reduction.clone(); + Arc::new(PhysicsRuntime::new(manifest)?) + } else { + Arc::clone(&physics) + }; + selector_runtime.attach_physics(selector_physics)?; + } + for selector_runtime in self.color_selector_runtimes.values_mut() { + let selector_physics = + if let Some(reduction) = selector_runtime.physics_reduction_override.as_ref() { + let mut manifest = physics.manifest.clone(); + manifest.reduction = reduction.clone(); + Arc::new(PhysicsRuntime::new(manifest)?) + } else { + Arc::clone(&physics) + }; + selector_runtime.attach_physics(selector_physics)?; + } + Ok(()) + } + + pub(super) fn cached_lc_resolved_replay_plan( + &mut self, + physics: &PhysicsRuntime, + ) -> RusticolResult> { + if let Some(plan) = &self.lc_resolved_replay_plan { + return Ok(Arc::clone(plan)); + } + let materialized_sector_by_id = self.lc_materialized_sectors_by_id(physics)?; + let plan = Arc::new(physics.lc_resolved_replay_plan( + &self.lc_topology_replay_public_mappings, + &self.lc_topology_replay_routes, + &materialized_sector_by_id, + )?); + self.lc_resolved_replay_plan = Some(Arc::clone(&plan)); + Ok(plan) + } + + pub(super) fn cached_lc_resolved_replay_selection( + &mut self, + physics: &PhysicsRuntime, + plan: &LcResolvedReplayPlan, + selected_helicity_ids: Option<&BTreeSet>, + selected_color_ids: Option<&BTreeSet>, + ) -> RusticolResult> { + if let Some((cached_key, selection)) = &self.lc_resolved_replay_selection_cache + && selected_helicity_ids.is_none() + && cached_key.helicity_indices.is_none() + && let Some(color_ids) = selected_color_ids + && color_ids.len() == 1 + && let Some(color_id) = color_ids.iter().next() + && let Some(color_index) = physics.color_index_by_id.get(color_id) + && cached_key.color_indices.as_deref() == Some(std::slice::from_ref(color_index)) + { + return Ok(Arc::clone(selection)); + } + if let Some((cached_key, selection)) = &self.lc_resolved_replay_selection_cache + && selected_color_ids.is_none() + && cached_key.color_indices.is_none() + && let Some(helicity_ids) = selected_helicity_ids + && helicity_ids.len() == 1 + && let Some(helicity_id) = helicity_ids.iter().next() + && let Some(helicity_index) = physics.helicity_index_by_id.get(helicity_id) + && cached_key.helicity_indices.as_deref() == Some(std::slice::from_ref(helicity_index)) + { + return Ok(Arc::clone(selection)); + } + let key = + physics.lc_resolved_replay_selection_key(selected_helicity_ids, selected_color_ids)?; + if let Some((cached_key, selection)) = &self.lc_resolved_replay_selection_cache + && cached_key == &key + { + return Ok(Arc::clone(selection)); + } + let mut selection = physics.select_lc_resolved_replay_plan_for_key(plan, &key)?; + let sectors_by_id = self.lc_materialized_sectors_by_id(physics)?; + let sector_by_color_index = sectors_by_id + .iter() + .map(|(sector_id, sector)| (sector.color_index, *sector_id)) + .collect::>(); + let target_component_count = selection + .helicity_indices + .len() + .checked_mul(selection.color_indices.len()) + .ok_or_else(|| RusticolError::invalid_argument("resolved shape overflows usize"))?; + for source_group in &mut selection.source_groups { + let materialized_sector_ids = source_group + .color_ids + .iter() + .map(|color_id| { + let color_index = physics.color_index_by_id.get(color_id).ok_or_else(|| { + RusticolError::integrity(format!( + "LC topology replay selected unknown materialized color {color_id:?}" + )) + })?; + sector_by_color_index.get(color_index).copied().ok_or_else(|| { + RusticolError::integrity(format!( + "LC topology replay has no materialized sector for color {color_id:?}" + )) + }) + }) + .collect::>>()?; + source_group.direct_total_plan = None; + if selected_helicity_ids.is_none() + && let ([mapping_index], [replay_entry]) = ( + source_group.mapping_indices.as_slice(), + source_group.entries.as_slice(), + ) + && materialized_sector_ids.len() == 1 + && let Some(materialized_sector_id) = materialized_sector_ids.iter().next().copied() + && let Some(scale) = uniform_lc_replay_total_scale( + replay_entry, + source_group.source_component_count, + target_component_count, + ) + && self + .color_selector_runtimes + .get(&materialized_sector_id) + .is_some_and(|runtime| { + certifies_lc_direct_total_source( + runtime, + &source_group.helicity_ids, + &source_group.color_ids, + ) + }) + { + source_group.direct_total_plan = Some(LcDirectTotalPlan { + mapping_index: *mapping_index, + materialized_sector_id, + scale, + }); + } + source_group.materialized_sector_ids = Some(materialized_sector_ids); + } + let selection = Arc::new(selection); + self.lc_resolved_replay_selection_cache = Some((key, Arc::clone(&selection))); + Ok(selection) + } + + pub(super) fn lc_materialized_sectors_by_id( + &self, + physics: &PhysicsRuntime, + ) -> RusticolResult> { + let amplitude = self.amplitude_stage.as_ref().ok_or_else(|| { + RusticolError::artifact("LC topology replay requires a loaded compiled amplitude stage") + })?; + self.lc_materialized_sectors_by_id_from_groups(physics, &litude.raw_sum_groups) + } + + pub(super) fn lc_materialized_sector_ids_for_color_ids( + &self, + physics: &PhysicsRuntime, + color_ids: &BTreeSet, + ) -> RusticolResult> { + let sectors_by_id = self.lc_materialized_sectors_by_id(physics)?; + let sector_by_color_index = sectors_by_id + .iter() + .map(|(sector_id, sector)| (sector.color_index, *sector_id)) + .collect::>(); + color_ids + .iter() + .map(|color_id| { + let color_index = physics.color_index_by_id.get(color_id).ok_or_else(|| { + RusticolError::integrity(format!( + "LC topology replay selected unknown materialized color {color_id:?}" + )) + })?; + sector_by_color_index + .get(color_index) + .copied() + .ok_or_else(|| { + RusticolError::integrity(format!( + "LC topology replay color {color_id:?} has no materialized sector" + )) + }) + }) + .collect() + } + + pub(super) fn lc_materialized_sectors_by_id_from_groups( + &self, + physics: &PhysicsRuntime, + groups: &[RawSumGroup], + ) -> RusticolResult> { + let mut result = BTreeMap::new(); + for group in groups { + if group.sector_ids.is_empty() { + continue; + } + let reduction = physics + .reduction_by_group_id + .get(&group.id) + .ok_or_else(|| { + RusticolError::artifact(format!( + "LC topology replay is missing physics reduction group {}", + group.id + )) + })?; + let computed = reduction + .physical_color_ids + .iter() + .map(|id| physics.color_index_by_id[id]) + .filter(|index| physics.color_is_computed(*index)) + .collect::>(); + if computed.len() != 1 { + return Err(RusticolError::artifact(format!( + "LC topology replay coherent group {} has {} computed color representatives, expected one", + group.id, + computed.len() + ))); + } + let color_index = *computed.iter().next().expect("one computed color checked"); + let sector = LcMaterializedSector { + color_index, + // all_sector_weight folds independent helicity and color + // multiplicities. Resolved public-flow expansion must only + // distribute the color factor; helicity replay is handled by + // its own selector/reduction axis. + reduction_weight: lc_color_replay_weight(group)?, + }; + for sector_id in &group.sector_ids { + if let Some(previous) = result.insert(*sector_id, sector) + && (previous.color_index != sector.color_index + || previous.reduction_weight.to_bits() != sector.reduction_weight.to_bits()) + { + return Err(RusticolError::artifact(format!( + "LC topology replay materialized sector {sector_id} maps to inconsistent computed colors or weights" + ))); + } + } + } + for sector_id in &self.lc_topology_replay_materialized_sector_ids { + if !result.contains_key(sector_id) { + return Err(RusticolError::artifact(format!( + "LC topology replay materialized sector {sector_id} is absent from the amplitude groups" + ))); + } + } + Ok(result) + } + + pub(super) fn remap_lc_topology_replay_public_labels( + &mut self, + representative_to_public: &[usize], + ) -> RusticolResult<()> { + if representative_to_public.len() != self.external_count + || representative_to_public + .iter() + .copied() + .collect::>() + != (0..self.external_count).collect::>() + { + return Err(RusticolError::artifact( + "process alias has an invalid public-label permutation for LC topology replay", + )); + } + if let Some(sum_runtime) = self.helicity_sum_runtime.as_mut() { + sum_runtime.remap_lc_topology_replay_public_labels(representative_to_public)?; + } + for selector_runtime in &mut self.helicity_selector_runtimes { + selector_runtime.remap_lc_topology_replay_public_labels(representative_to_public)?; + } + if !self.lc_topology_replay_enabled { + return Ok(()); + } + self.lc_topology_replay_public_mappings = self + .lc_topology_replay_mappings + .iter() + .map(|mapping| { + mapping + .iter() + .map(|(representative, sector)| { + ( + representative_to_public[*representative], + representative_to_public[*sector], + ) + }) + .collect() + }) + .collect(); + self.lc_resolved_replay_plan = None; + self.lc_resolved_replay_selection_cache = None; + Ok(()) + } +} + +pub(super) fn certifies_lc_direct_total_source( + runtime: &ExecutionRuntime, + helicity_ids: &BTreeSet, + color_ids: &BTreeSet, +) -> bool { + if helicity_ids.is_empty() + || color_ids.is_empty() + || runtime.helicity_sum_runtime.is_some() + || runtime.lc_topology_replay_enabled + || runtime.has_compiled_helicity_execution_plan() + { + return false; + } + let Some(physics) = runtime.physics.as_deref() else { + return false; + }; + let Some(amplitude) = runtime.amplitude_stage.as_ref() else { + return false; + }; + if amplitude.color_contraction.is_some() + || physics.manifest.reduction.kind != crate::ReductionKind::LcDiagonal + { + return false; + } + let Some(expected_component_count) = helicity_ids.len().checked_mul(color_ids.len()) else { + return false; + }; + let groups = &physics.manifest.reduction.groups; + if groups.is_empty() + || amplitude.raw_sum_groups.len() != groups.len() + || runtime.amplitude_output_count != groups.len() + { + return false; + } + let raw_group_ids = amplitude + .raw_sum_groups + .iter() + .map(|group| group.id) + .collect::>(); + if raw_group_ids != physics.reduction_by_group_id.keys().copied().collect() { + return false; + } + let mut covered_members = BTreeSet::new(); + for group in groups { + if group.physical_helicity_ids.is_empty() + || group.physical_color_ids.is_empty() + || !group + .physical_helicity_ids + .contains(&group.representative_helicity_id) + || !group + .physical_color_ids + .contains(&group.representative_color_id) + { + return false; + } + for helicity_id in &group.physical_helicity_ids { + for color_id in &group.physical_color_ids { + if !helicity_ids.contains(helicity_id) + || !color_ids.contains(color_id) + || !covered_members.insert((helicity_id.as_str(), color_id.as_str())) + { + return false; + } + } + } + } + covered_members.len() == expected_component_count +} + +fn remap_reduction_ids( + reduction: &mut crate::Reduction, + helicity_id_map: &BTreeMap, + color_id_map: &BTreeMap, +) -> RusticolResult<()> { + for group in &mut reduction.groups { + group.representative_helicity_id = helicity_id_map + .get(&group.representative_helicity_id) + .cloned() + .ok_or_else(|| { + RusticolError::integrity(format!( + "reduction override representative helicity {:?} is absent from alias remapping", + group.representative_helicity_id + )) + })?; + group.representative_color_id = color_id_map + .get(&group.representative_color_id) + .cloned() + .ok_or_else(|| { + RusticolError::integrity(format!( + "reduction override representative color {:?} is absent from alias remapping", + group.representative_color_id + )) + })?; + for id in &mut group.physical_helicity_ids { + *id = helicity_id_map.get(id).cloned().ok_or_else(|| { + RusticolError::integrity(format!( + "reduction override physical helicity {id:?} is absent from alias remapping" + )) + })?; + } + for id in &mut group.physical_color_ids { + *id = color_id_map.get(id).cloned().ok_or_else(|| { + RusticolError::integrity(format!( + "reduction override physical color {id:?} is absent from alias remapping" + )) + })?; + } + } + Ok(()) +} + +fn lc_color_replay_weight(group: &RawSumGroup) -> RusticolResult { + if !group.weight.is_finite() + || group.weight <= 0.0 + || !group.all_sector_weight.is_finite() + || group.all_sector_weight <= 0.0 + { + return Err(RusticolError::artifact(format!( + "LC topology replay coherent group {} has no positive helicity/color weight", + group.id + ))); + } + let color_replay_weight = group.all_sector_weight / group.weight; + if !color_replay_weight.is_finite() || color_replay_weight <= 0.0 { + return Err(RusticolError::artifact(format!( + "LC topology replay coherent group {} has no positive color-only weight", + group.id + ))); + } + Ok(color_replay_weight) +} + +fn validate_public_label_permutation( + mapping: &[(usize, usize)], + external_count: usize, +) -> RusticolResult<()> { + let mut representatives = BTreeSet::new(); + let mut sectors = BTreeSet::new(); + for (representative, sector) in mapping { + if *representative >= external_count || *sector >= external_count { + return Err(RusticolError::artifact( + "LC topology replay public-label mapping references an out-of-range external leg", + )); + } + if !representatives.insert(*representative) || !sectors.insert(*sector) { + return Err(RusticolError::artifact( + "LC topology replay public-label mapping is not one-to-one", + )); + } + } + if representatives != sectors { + return Err(RusticolError::artifact( + "LC topology replay public-label mapping is not a permutation of its support", + )); + } + Ok(()) +} + +fn permute_public_helicity(values: &[i32], mapping: &[(usize, usize)]) -> Vec { + let mut target = values.to_vec(); + for (representative, sector) in mapping { + target[*sector] = values[*representative]; + } + target +} + +fn permute_public_color_word(word: &[usize], mapping: &[(usize, usize)]) -> Vec { + let labels = mapping.iter().copied().collect::>(); + word.iter() + .map(|label| { + label + .checked_sub(1) + .and_then(|index| labels.get(&index).copied()) + .map(|index| index + 1) + .unwrap_or(*label) + }) + .collect() +} + +fn replay_public_color_words( + word: &[usize], + replay_weight: f64, +) -> RusticolResult>> { + const WEIGHT_TOLERANCE: f64 = 1.0e-12; + if (replay_weight - 1.0).abs() <= WEIGHT_TOLERANCE { + return Ok(vec![word.to_vec()]); + } + if (replay_weight - 2.0).abs() > WEIGHT_TOLERANCE { + return Err(RusticolError::compatibility(format!( + "resolved LC topology replay cannot expand sector weight {replay_weight}; schema-v3 replay metadata defines public-flow expansion only for unit sectors and folded trace-reflection weight two" + ))); + } + if word.len() < 2 { + return Ok(vec![word.to_vec()]); + } + let mut reflected = Vec::with_capacity(word.len()); + reflected.push(word[0]); + reflected.extend(word[1..].iter().rev().copied()); + if reflected == word { + Ok(vec![word.to_vec()]) + } else { + Ok(vec![word.to_vec(), reflected]) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn color_replay_weight_excludes_helicity_multiplicity() { + let group = RawSumGroup { + id: 7, + indices: vec![0], + weight: 2.0, + all_sector_weight: 4.0, + sector_ids: vec![0], + }; + + assert_eq!(lc_color_replay_weight(&group).unwrap(), 2.0); + } + + #[test] + fn color_replay_weight_rejects_invalid_axes() { + let group = RawSumGroup { + id: 7, + indices: vec![0], + weight: 0.0, + all_sector_weight: 2.0, + sector_ids: vec![0], + }; + + assert!(lc_color_replay_weight(&group).is_err()); + } +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/engine/point_selectors.rs b/pyamplicol/rust/crates/rusticol-core/src/engine/point_selectors.rs new file mode 100644 index 0000000000..c47e56117d --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/engine/point_selectors.rs @@ -0,0 +1,724 @@ +// SPDX-License-Identifier: 0BSD + +use super::*; +use std::collections::HashMap; + +#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub(super) struct PointSelectorKey { + pub(super) helicity_index: Option, + pub(super) color_index: Option, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(super) struct PointSelectorPartition { + pub(super) key: PointSelectorKey, + pub(super) rows: PointSelectorRows, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(super) enum PointSelectorRows { + Contiguous { start: usize, end: usize }, + Gathered { start: usize, end: usize }, +} + +impl PointSelectorRows { + pub(super) fn len(self) -> usize { + match self { + Self::Contiguous { start, end } | Self::Gathered { start, end } => end - start, + } + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(super) enum PointSelectorPlan { + None, + Homogeneous(PointSelectorKey), + Partitioned, +} + +#[derive(Clone, Debug, PartialEq)] +pub(super) struct PointSelectorPlanProfile { + pub(super) kind: &'static str, + pub(super) group_sizes: Vec, + pub(super) reordered_point_count: usize, + pub(super) simd_lane_width: usize, + pub(super) simd_occupancy: f64, +} + +/// Reusable storage for stable per-point selector grouping. +/// +/// Partition order follows first occurrence and row order within a gathered +/// partition follows caller order. `HashMap` iteration order therefore never +/// affects execution order. Clearing the map and vectors retains their backing +/// allocations for subsequent calls of the same or smaller shape. +#[derive(Default)] +pub(super) struct PointSelectorPlanner { + partition_index_by_key: HashMap, + partitions: Vec, + partition_counts: Vec, + partition_cursors: Vec, + gathered_rows: Vec, +} + +impl PointSelectorPlanner { + pub(super) fn build( + &mut self, + point_count: usize, + helicity_by_point: Option<&[u32]>, + color_by_point: Option<&[u32]>, + helicity_count: usize, + color_count: usize, + ) -> RusticolResult { + self.clear_plan(); + if helicity_by_point.is_none() && color_by_point.is_none() { + return Ok(PointSelectorPlan::None); + } + validate_selector_axis( + helicity_by_point, + point_count, + helicity_count, + "helicity_by_point", + )?; + validate_selector_axis( + color_by_point, + point_count, + color_count, + "color_flow_by_point", + )?; + + let selector_key = |point_index: usize| PointSelectorKey { + helicity_index: helicity_by_point.map(|values| values[point_index] as usize), + color_index: color_by_point.map(|values| values[point_index] as usize), + }; + if point_count == 0 { + return Ok(PointSelectorPlan::Partitioned); + } + + let first = selector_key(0); + if (1..point_count).all(|point_index| selector_key(point_index) == first) { + return Ok(PointSelectorPlan::Homogeneous(first)); + } + + if self.build_contiguous_runs(point_count, &selector_key) { + return Ok(PointSelectorPlan::Partitioned); + } + + self.build_stable_gathered_partitions(point_count, &selector_key); + Ok(PointSelectorPlan::Partitioned) + } + + pub(super) fn partitions(&self) -> &[PointSelectorPartition] { + &self.partitions + } + + pub(super) fn gathered_rows(&self, rows: PointSelectorRows) -> &[usize] { + match rows { + PointSelectorRows::Gathered { start, end } => &self.gathered_rows[start..end], + PointSelectorRows::Contiguous { .. } => &[], + } + } + + pub(super) fn profile( + &self, + plan: PointSelectorPlan, + point_count: usize, + simd_lane_width: usize, + ) -> PointSelectorPlanProfile { + let (kind, group_sizes, reordered_point_count) = match plan { + PointSelectorPlan::None => ("none", Vec::new(), 0), + PointSelectorPlan::Homogeneous(_) => ("homogeneous", vec![point_count], 0), + PointSelectorPlan::Partitioned + if self.partitions.iter().all(|partition| { + matches!(partition.rows, PointSelectorRows::Contiguous { .. }) + }) => + { + ( + "contiguous", + self.partitions + .iter() + .map(|partition| partition.rows.len()) + .collect(), + 0, + ) + } + PointSelectorPlan::Partitioned => { + let reordered_point_count = self + .gathered_rows + .iter() + .copied() + .enumerate() + .filter(|(grouped_index, source_index)| grouped_index != source_index) + .count(); + ( + "stable-grouped", + self.partitions + .iter() + .map(|partition| partition.rows.len()) + .collect(), + reordered_point_count, + ) + } + }; + let simd_lane_width = simd_lane_width.max(1); + let padded_point_count = group_sizes + .iter() + .map(|size| size.div_ceil(simd_lane_width) * simd_lane_width) + .sum::(); + let simd_occupancy = if point_count == 0 || padded_point_count == 0 { + 1.0 + } else { + point_count as f64 / padded_point_count as f64 + }; + PointSelectorPlanProfile { + kind, + group_sizes, + reordered_point_count, + simd_lane_width, + simd_occupancy, + } + } + + fn clear_plan(&mut self) { + self.partition_index_by_key.clear(); + self.partitions.clear(); + self.partition_counts.clear(); + self.partition_cursors.clear(); + self.gathered_rows.clear(); + } + + fn build_contiguous_runs( + &mut self, + point_count: usize, + selector_key: &impl Fn(usize) -> PointSelectorKey, + ) -> bool { + let mut run_start = 0; + let mut run_key = selector_key(0); + for point_index in 1..=point_count { + let next_key = (point_index < point_count).then(|| selector_key(point_index)); + if next_key == Some(run_key) { + continue; + } + if self.partition_index_by_key.contains_key(&run_key) { + return false; + } + self.partition_index_by_key + .insert(run_key, self.partitions.len()); + self.partitions.push(PointSelectorPartition { + key: run_key, + rows: PointSelectorRows::Contiguous { + start: run_start, + end: point_index, + }, + }); + if let Some(key) = next_key { + run_start = point_index; + run_key = key; + } + } + true + } + + fn build_stable_gathered_partitions( + &mut self, + point_count: usize, + selector_key: &impl Fn(usize) -> PointSelectorKey, + ) { + self.partition_index_by_key.clear(); + self.partitions.clear(); + self.partition_counts.clear(); + + for point_index in 0..point_count { + let key = selector_key(point_index); + let partition_index = match self.partition_index_by_key.get(&key).copied() { + Some(index) => index, + None => { + let index = self.partitions.len(); + self.partition_index_by_key.insert(key, index); + self.partitions.push(PointSelectorPartition { + key, + rows: PointSelectorRows::Gathered { start: 0, end: 0 }, + }); + self.partition_counts.push(0); + index + } + }; + self.partition_counts[partition_index] += 1; + } + + self.partition_cursors.resize(self.partitions.len(), 0); + let mut offset = 0; + for (partition_index, partition) in self.partitions.iter_mut().enumerate() { + let end = offset + self.partition_counts[partition_index]; + partition.rows = PointSelectorRows::Gathered { start: offset, end }; + self.partition_cursors[partition_index] = offset; + offset = end; + } + self.gathered_rows.resize(point_count, 0); + for point_index in 0..point_count { + let partition_index = self.partition_index_by_key[&selector_key(point_index)]; + let cursor = &mut self.partition_cursors[partition_index]; + self.gathered_rows[*cursor] = point_index; + *cursor += 1; + } + } +} + +#[derive(Default)] +pub(super) struct SelectorSetCache { + entries: Vec, +} + +struct SelectorSetCacheEntry { + ids: Vec, + selected: BTreeSet, +} + +impl SelectorSetCache { + const MAX_ENTRIES: usize = 64; + + pub(super) fn resolve( + &mut self, + ids: Option<&[String]>, + kind: &str, + ) -> RusticolResult>> { + let Some(ids) = ids else { + return Ok(None); + }; + if ids.is_empty() { + return Err(RusticolError::selector(format!( + "resolved {kind} selection must not be empty" + ))); + } + if let Some(index) = self + .entries + .iter() + .position(|entry| entry.ids.as_slice() == ids) + { + return Ok(Some(&self.entries[index].selected)); + } + let selected = ids.iter().cloned().collect::>(); + if selected.len() != ids.len() { + return Err(RusticolError::selector(format!( + "resolved {kind} selection contains duplicate ids" + ))); + } + if self.entries.len() == Self::MAX_ENTRIES { + self.entries.remove(0); + } + self.entries.push(SelectorSetCacheEntry { + ids: ids.to_vec(), + selected, + }); + Ok(Some( + &self + .entries + .last() + .expect("selector cache entry was just appended") + .selected, + )) + } +} + +#[cfg(test)] +mod selector_set_cache_tests { + use super::*; + + #[test] + fn selector_set_cache_is_bounded_and_rejects_duplicates() { + let mut cache = SelectorSetCache::default(); + for index in 0..=SelectorSetCache::MAX_ENTRIES { + let ids = vec![format!("id-{index}")]; + cache.resolve(Some(&ids), "test").unwrap(); + } + assert_eq!(cache.entries.len(), SelectorSetCache::MAX_ENTRIES); + assert_eq!(cache.entries[0].ids, vec!["id-1"]); + + let duplicate = vec!["same".to_string(), "same".to_string()]; + assert!(cache.resolve(Some(&duplicate), "test").is_err()); + assert_eq!(cache.entries.len(), SelectorSetCache::MAX_ENTRIES); + } +} + +#[derive(Default)] +pub(super) struct PointSelectorExecutionScratch { + pub(super) planner: PointSelectorPlanner, + pub(super) gathered_batch: Vec>, + pub(super) partition_totals: Vec, + pub(super) output_totals: Vec, + pub(super) helicity_selector_sets: SelectorSetCache, + pub(super) color_selector_sets: SelectorSetCache, + pub(super) helicity_singletons: Vec>, + pub(super) color_singletons: Vec>, +} + +impl PointSelectorExecutionScratch { + pub(super) fn prepare_singletons(&mut self, physics: &PhysicsRuntime) { + if self.helicity_singletons.len() != physics.manifest.helicities.len() { + self.helicity_singletons.clear(); + self.helicity_singletons + .reserve(physics.manifest.helicities.len()); + self.helicity_singletons.extend( + physics + .manifest + .helicities + .iter() + .map(|helicity| BTreeSet::from([helicity.id.clone()])), + ); + } + if self.color_singletons.len() != physics.manifest.color_components.len() { + self.color_singletons.clear(); + self.color_singletons + .reserve(physics.manifest.color_components.len()); + self.color_singletons.extend( + physics + .manifest + .color_components + .iter() + .map(|color| BTreeSet::from([color.id().to_string()])), + ); + } + } +} + +pub(super) fn fill_gathered_batch<'a>( + gathered_batch: &'a mut Vec>, + batch: &[Vec<[f64; 4]>], + point_indices: &[usize], +) -> &'a [Vec<[f64; 4]>] { + if gathered_batch.len() < point_indices.len() { + gathered_batch.resize_with(point_indices.len(), Vec::new); + } + for (target, source_index) in gathered_batch.iter_mut().zip(point_indices.iter().copied()) { + target.clear(); + target.extend_from_slice(&batch[source_index]); + } + &gathered_batch[..point_indices.len()] +} + +pub(super) fn fill_gathered_batch_from_view<'a>( + gathered_batch: &'a mut Vec>, + batch: F64MomentumBatchView<'_>, + point_indices: &[usize], +) -> RusticolResult<&'a [Vec<[f64; 4]>]> { + if gathered_batch.len() < point_indices.len() { + gathered_batch.resize_with(point_indices.len(), Vec::new); + } + for (target, source_index) in gathered_batch.iter_mut().zip(point_indices.iter().copied()) { + if source_index >= batch.point_count() { + return Err(RusticolError::integrity( + "point-selector gather references an out-of-range input row", + )); + } + let point = batch.point(source_index); + target.clear(); + target.reserve(batch.external_count()); + for external_index in 0..batch.external_count() { + target.push(point.momentum(external_index).ok_or_else(|| { + RusticolError::integrity( + "validated momentum view is missing an external leg during selector gather", + ) + })?); + } + } + Ok(&gathered_batch[..point_indices.len()]) +} + +#[cfg(test)] +pub(super) fn write_partition_totals( + partition_totals: &mut Vec, + resolved: &ResolvedValues, +) { + let component_count = resolved.helicity_indices.len() * resolved.color_indices.len(); + partition_totals.clear(); + if component_count == 0 { + partition_totals.resize(resolved.point_count, 0.0); + } else { + partition_totals.reserve(resolved.point_count); + partition_totals.extend( + resolved + .values + .chunks(component_count) + .map(|point| point.iter().sum::()), + ); + } +} + +pub(super) fn scatter_partition_totals( + values: &mut [f64], + partition_totals: &[f64], + rows: PointSelectorRows, + planner: &PointSelectorPlanner, +) { + match rows { + PointSelectorRows::Contiguous { start, end } => { + values[start..end].copy_from_slice(partition_totals); + } + rows @ PointSelectorRows::Gathered { .. } => { + for (source, target) in partition_totals + .iter() + .copied() + .zip(planner.gathered_rows(rows).iter().copied()) + { + values[target] = source; + } + } + } +} + +fn validate_selector_axis( + values: Option<&[u32]>, + point_count: usize, + available_count: usize, + name: &str, +) -> RusticolResult<()> { + let Some(values) = values else { + return Ok(()); + }; + if values.len() != point_count { + return Err(RusticolError::selector(format!( + "{name} contains {} entries, expected one selector for each of {point_count} points", + values.len() + ))); + } + if let Some((point_index, selector)) = values + .iter() + .copied() + .enumerate() + .find(|(_, selector)| *selector as usize >= available_count) + { + return Err(RusticolError::selector(format!( + "{name}[{point_index}]={selector} is out of range; the artifact exposes {available_count} selectors on this axis" + ))); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn expanded_rows(planner: &PointSelectorPlanner) -> Vec<(PointSelectorKey, Vec, bool)> { + planner + .partitions() + .iter() + .map(|partition| match partition.rows { + PointSelectorRows::Contiguous { start, end } => { + (partition.key, (start..end).collect(), true) + } + rows @ PointSelectorRows::Gathered { .. } => { + (partition.key, planner.gathered_rows(rows).to_vec(), false) + } + }) + .collect() + } + + #[test] + fn stable_random_grouping_preserves_first_occurrence_and_caller_order() { + let colors = [2, 1, 2, 0, 1, 2, 0]; + let mut planner = PointSelectorPlanner::default(); + assert_eq!( + planner + .build(7, None, Some(&colors), 1, 3) + .expect("selector plan"), + PointSelectorPlan::Partitioned + ); + assert_eq!( + expanded_rows(&planner), + vec![ + ( + PointSelectorKey { + helicity_index: None, + color_index: Some(2), + }, + vec![0, 2, 5], + false, + ), + ( + PointSelectorKey { + helicity_index: None, + color_index: Some(1), + }, + vec![1, 4], + false, + ), + ( + PointSelectorKey { + helicity_index: None, + color_index: Some(0), + }, + vec![3, 6], + false, + ), + ] + ); + } + + #[test] + fn profile_distinguishes_zero_reorder_and_stable_grouped_plans() { + let mut planner = PointSelectorPlanner::default(); + let pooled = [0, 0, 1, 1]; + let plan = planner + .build(4, None, Some(&pooled), 1, 2) + .expect("pooled selector plan"); + let profile = planner.profile(plan, 4, 2); + assert_eq!(profile.kind, "contiguous"); + assert_eq!(profile.group_sizes, vec![2, 2]); + assert_eq!(profile.reordered_point_count, 0); + + let alternating = [0, 1, 0, 1]; + let plan = planner + .build(4, None, Some(&alternating), 1, 2) + .expect("alternating selector plan"); + let profile = planner.profile(plan, 4, 2); + assert_eq!(profile.kind, "stable-grouped"); + assert_eq!(profile.group_sizes, vec![2, 2]); + assert_eq!(profile.reordered_point_count, 2); + assert!((0.0..=1.0).contains(&profile.simd_occupancy)); + } + + #[test] + fn pooled_selector_runs_are_borrowed_without_gathering() { + let colors = [2, 2, 1, 1, 0]; + let mut planner = PointSelectorPlanner::default(); + assert_eq!( + planner + .build(5, None, Some(&colors), 1, 3) + .expect("selector plan"), + PointSelectorPlan::Partitioned + ); + assert_eq!( + expanded_rows(&planner), + vec![ + ( + PointSelectorKey { + helicity_index: None, + color_index: Some(2), + }, + vec![0, 1], + true, + ), + ( + PointSelectorKey { + helicity_index: None, + color_index: Some(1), + }, + vec![2, 3], + true, + ), + ( + PointSelectorKey { + helicity_index: None, + color_index: Some(0), + }, + vec![4], + true, + ), + ] + ); + assert!(planner.gathered_rows.is_empty()); + } + + #[test] + fn recognizes_homogeneous_selectors_without_partition_storage() { + let helicities = [1, 1, 1, 1]; + let colors = [2, 2, 2, 2]; + let mut planner = PointSelectorPlanner::default(); + assert_eq!( + planner + .build(4, Some(&helicities), Some(&colors), 2, 3) + .expect("selector plan"), + PointSelectorPlan::Homogeneous(PointSelectorKey { + helicity_index: Some(1), + color_index: Some(2), + }) + ); + assert!(planner.partitions().is_empty()); + } + + #[test] + fn reuses_planner_gather_and_partition_result_buffers() { + let colors = [2, 1, 2, 0, 1, 2, 0]; + let batch = (0..colors.len()) + .map(|point| vec![[point as f64, 0.0, 0.0, 0.0]; 3]) + .collect::>(); + let mut scratch = PointSelectorExecutionScratch::default(); + + scratch + .planner + .build(colors.len(), None, Some(&colors), 1, 3) + .expect("first selector plan"); + let partition_storage = scratch.planner.partitions.as_ptr(); + let gathered_row_storage = scratch.planner.gathered_rows.as_ptr(); + let first_rows = scratch.planner.partitions[0].rows; + let first_indices = scratch.planner.gathered_rows(first_rows).to_vec(); + fill_gathered_batch(&mut scratch.gathered_batch, &batch, &first_indices); + let gathered_batch_storage = scratch.gathered_batch.as_ptr(); + let gathered_point_storage = scratch.gathered_batch[0].as_ptr(); + let resolved = ResolvedValues { + values: vec![1.0, 2.0, 3.0], + point_count: 3, + helicity_indices: vec![0], + color_indices: vec![0], + }; + write_partition_totals(&mut scratch.partition_totals, &resolved); + let partition_total_storage = scratch.partition_totals.as_ptr(); + + scratch + .planner + .build(colors.len(), None, Some(&colors), 1, 3) + .expect("second selector plan"); + assert_eq!(scratch.planner.partitions.as_ptr(), partition_storage); + assert_eq!(scratch.planner.gathered_rows.as_ptr(), gathered_row_storage); + let first_rows = scratch.planner.partitions[0].rows; + let first_indices = scratch.planner.gathered_rows(first_rows).to_vec(); + fill_gathered_batch(&mut scratch.gathered_batch, &batch, &first_indices); + assert_eq!(scratch.gathered_batch.as_ptr(), gathered_batch_storage); + assert_eq!(scratch.gathered_batch[0].as_ptr(), gathered_point_storage); + write_partition_totals(&mut scratch.partition_totals, &resolved); + assert_eq!(scratch.partition_totals.as_ptr(), partition_total_storage); + } + + #[test] + fn gathered_partition_scatter_restores_caller_order() { + let colors = [2, 1, 2, 0, 1, 2, 0]; + let mut planner = PointSelectorPlanner::default(); + planner + .build(colors.len(), None, Some(&colors), 1, 3) + .expect("selector plan"); + + let mut values = vec![0.0; colors.len()]; + for partition in planner.partitions().iter().copied() { + let point_indices = planner.gathered_rows(partition.rows); + let partition_values = point_indices + .iter() + .map(|point_index| 100.0 + *point_index as f64) + .collect::>(); + scatter_partition_totals(&mut values, &partition_values, partition.rows, &planner); + } + + assert_eq!( + values, + (0..colors.len()) + .map(|point_index| 100.0 + point_index as f64) + .collect::>() + ); + } + + #[test] + fn validates_lengths_and_bounds() { + let mut planner = PointSelectorPlanner::default(); + let error = planner + .build(2, Some(&[0]), None, 1, 1) + .expect_err("length mismatch"); + assert!( + error + .to_string() + .contains("one selector for each of 2 points") + ); + + let error = planner + .build(2, None, Some(&[0, 1]), 1, 1) + .expect_err("out-of-range selector"); + assert!(error.to_string().contains("color_flow_by_point[1]=1")); + } +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/engine/quantum_number_flow_tests.rs b/pyamplicol/rust/crates/rusticol-core/src/engine/quantum_number_flow_tests.rs new file mode 100644 index 0000000000..53550bbd0d --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/engine/quantum_number_flow_tests.rs @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: 0BSD +use serde_json::{Value, json}; + +use super::GenericCurrentSlotManifest; + +fn compact_current_slot() -> Value { + json!({ + "current_id": 0, + "component_start": 0, + "component_stop": 1, + "dimension": 1, + "is_source": true, + "particle_id": 101, + "external_mask": 1, + "chirality": 0, + "momentum_mask": 1, + "auxiliary_kind": null + }) +} + +#[test] +fn compact_current_slot_has_no_quantum_number_payload() { + serde_json::from_value::(compact_current_slot()) + .expect("compact current slot should deserialize without quantum numbers"); + + for (field, value) in [ + ("charge_flow", json!(1)), + ("quantum_number_flow", json!([["electric_charge", "1/5"]])), + ] { + let mut payload = compact_current_slot(); + payload + .as_object_mut() + .expect("test current slot must be an object") + .insert(field.to_owned(), value); + let error = serde_json::from_value::(payload) + .expect_err("Rust execution current slots must reject quantum-number fields"); + assert!(error.to_string().contains("unknown field")); + } +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/engine/recurrence_backend.rs b/pyamplicol/rust/crates/rusticol-core/src/engine/recurrence_backend.rs new file mode 100644 index 0000000000..bf6591850b --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/engine/recurrence_backend.rs @@ -0,0 +1,2519 @@ +// SPDX-License-Identifier: 0BSD + +#[cfg(feature = "f64-compiled")] +use super::PreparedKernelManifest; +use super::PreparedKernelPackManifest; +#[cfg(feature = "f64-compiled")] +use super::evaluator::native_direct::LoadedNativeDirectExecutor; +use super::evaluator::recurrence_closure_direct::execute_closure_reduce_rows; +use super::evaluator::recurrence_intrinsic_direct::{ + FEYNMAN_VECTOR_PROPAGATOR_TEMPLATE, LoadedRecurrenceIntrinsicDirectExecutor, + RecurrenceIntrinsicScale, WEYL_PROPAGATOR_NEGATIVE_TEMPLATE, WEYL_PROPAGATOR_POSITIVE_TEMPLATE, + execute_identity_finalization_rows, +}; +use super::evaluator::recurrence_source_direct::{ + DirectSourceDispatchDomainSpec, LoadedDirectSourceExecutor, +}; +#[cfg(feature = "f64-symjit")] +use super::evaluator::symjit_direct::{ + LoadedSymjitDirectExecutor, SymjitDirectParameterBinding, SymjitDirectPlaneProjection, + SymjitDirectScalarProjection, +}; +#[cfg(feature = "f64-symjit")] +use super::recurrence_process_pack::ProcessJitExecutor; +#[cfg(feature = "f64-compiled")] +use super::recurrence_process_pack::ProcessNativeExecutor; +use super::recurrence_process_pack::{ + ProcessDirectExecutorBinding, ProcessDirectExecutorDescriptor, ProcessDirectExecutorPack, + ProcessIntrinsicExecutor, +}; +#[cfg(feature = "on-the-fly-test-support")] +use crate::VerifiedArtifact; +use crate::artifact::EvaluatorPayloadStore; +use crate::recurrence::direct_backend::{ + DirectContributionExecutionMetadata, DirectContributionFanoutExecutorHandle, + DirectExecutorCatalog, DirectExecutorHandle, DirectInteractionExecutorCapability, + DirectUnionSourceDispatchHandle, +}; +use crate::recurrence::on_the_fly::{ + OnTheFlyBoundUnionSourceProgram, OnTheFlyExecutorKeyV1, OnTheFlyPreparedExecutorResolver, + OnTheFlyProcessSeedV1, OnTheFlySelectedQueryTraceV1, OnTheFlyStructuralTraceV1, + OnTheFlyUnionSourceBindingView, ResolvedOnTheFlyExecutor, + authenticated_prepared_executor_binding, +}; +use crate::recurrence::template::ValidatedRecurrenceTemplateInput; +use crate::recurrence::{ + DIRECT_NONE_U32, DirectExecutorRole, DirectRecurrencePlan, PreparedDirectExecutorBinding, + PreparedDirectExecutorCatalog, RecurrenceStrategy, SemanticDigest, +}; +use crate::{RusticolError, RusticolResult}; +#[cfg(feature = "f64-symjit")] +use sha2::{Digest, Sha256}; +use std::collections::BTreeMap; +#[cfg(any(test, feature = "on-the-fly-test-support"))] +use std::collections::BTreeSet; +#[cfg(feature = "on-the-fly-test-support")] +use std::path::Path; +#[cfg(feature = "f64-symjit")] +use std::path::PathBuf; +use std::rc::Rc; + +#[cfg(feature = "f64-symjit")] +use super::eager_manifest::RecurrenceDirectPlaneProjectionManifest; +use super::eager_manifest::{ + RecurrenceDirectParameterBindingManifest, RecurrenceDirectScalarProjectionManifest, + RecurrenceDirectTemplateManifest, +}; + +/// Authenticated identity of one loaded Direct-Arena recurrence executor catalog. +#[derive(Clone, Debug, Eq, PartialEq)] +pub(super) struct NativeRecurrenceDirectBackendSummary { + pub(super) prepared_kernel_pack_digest: String, + pub(super) direct_template_catalog_digest: String, + pub(super) backend: String, + pub(super) target_triple: String, + pub(super) target_portable: bool, + pub(super) executor_count: usize, +} + +/// Context owners backing a loaded Direct-Arena executor catalog. +/// +/// Handles stored in `catalog` address immutable heap allocations owned by +/// `source`, `symjit`, and `native`. Those owners therefore must remain alive +/// for every direct runtime call. +pub(super) struct NativeRecurrenceDirectExecutorBackend { + catalog: DirectExecutorCatalog, + owners: NativeRecurrenceDirectExecutorOwners, +} + +/// Model-level ownership of one authenticated prepared-executor catalog. +/// +/// Source executors are deliberately absent: their contexts contain crossed +/// source domains and therefore belong to a process/lane binding. Every +/// other handle addresses immutable contexts owned by this pool and can be +/// shared by independent selector-local traces without constructing a direct +/// recurrence plan. +pub(super) struct NativeRecurrencePreparedExecutorPool { + handles: Box<[Option]>, + bindings: BTreeMap<(DirectExecutorRole, u32), NativePreparedExecutorBinding>, + executor_roles: Box<[Option]>, + contribution_metadata: Box<[Option]>, + contribution_fanout: Box<[Option]>, + packed_singleton_capabilities: Box<[bool]>, + identity_finalizer_id: Option, + direct_template_catalog_digest: SemanticDigest, + recurrence_template_catalog_digest: SemanticDigest, + compiled_model_digest: SemanticDigest, + prepared_kernel_pack_digest: SemanticDigest, + _intrinsics: Vec, + #[cfg(feature = "f64-symjit")] + _symjit: Vec, + #[cfg(feature = "f64-compiled")] + _native: Vec, + summary: NativeRecurrenceDirectBackendSummary, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +struct NativePreparedExecutorBinding { + executor_id: u32, + parent_permutation: [u8; 2], +} + +/// Process/lane-owned crossed-source dispatch binding. +/// +/// This value must outlive every source handle obtained from it and must be +/// dropped before the model-level prepared pool during lane teardown. +pub(super) struct OnTheFlySourceDomainBinding { + source: Option, +} + +impl OnTheFlySourceDomainBinding { + /// Bind source domains owned by a concrete process plan. + /// + /// All-flow plans execute source rows through the union-source dispatcher, + /// so their sparse process pack intentionally has no Source descriptor. + /// The plan-owned source rows, rather than the descriptor catalog, are the + /// authoritative signal that this binding is required. + pub(super) fn load_for_process_plan( + plan: &DirectRecurrencePlan, + source_domains: Vec, + ) -> RusticolResult { + if plan.sources().is_empty() { + return Err(RusticolError::integrity( + "process recurrence plan has no source rows", + )); + } + Ok(Self { + source: Some(LoadedDirectSourceExecutor::load(source_domains)?), + }) + } +} + +/// Owning semantic resolver used by the private on-the-fly interpreter. +/// +/// The copied direct handles address heap-stable contexts owned by `sources` +/// and `pool`. Declaration order is intentional: resolved handles disappear +/// first, then process-bound source contexts, then the model-level pool. +pub(super) struct NativeOnTheFlyPreparedExecutorResolver { + resolved: BTreeMap, + pending_resolved: Option>, + sources: OnTheFlySourceDomainBinding, + pool: Rc, +} + +impl NativeOnTheFlyPreparedExecutorResolver { + fn effective_resolved(&self) -> &BTreeMap { + self.pending_resolved.as_ref().unwrap_or(&self.resolved) + } + + pub(super) fn semantic_executor_binding_count(&self) -> RusticolResult { + u32::try_from(self.effective_resolved().len()).map_err(|_| { + RusticolError::integrity("on-the-fly semantic executor binding count exceeds u32") + }) + } +} + +#[cfg(any(test, feature = "on-the-fly-test-support"))] +impl NativeOnTheFlyPreparedExecutorResolver { + pub(super) fn distinct_prepared_executor_count(&self) -> RusticolResult { + let direct_executor_ids = self + .effective_resolved() + .values() + .map(|resolved| resolved.direct_executor_id) + .collect::>(); + u32::try_from(direct_executor_ids.len()) + .map_err(|_| RusticolError::integrity("on-the-fly prepared executor count exceeds u32")) + } +} + +/// Immutable context ownership retained beside the native recurrence scheduler. +pub(super) struct NativeRecurrenceDirectExecutorOwners { + // Field order documents the required destruction order. Rust drops fields + // in declaration order: process-bound source contexts before model pool. + source_binding: OnTheFlySourceDomainBinding, + pool: Rc, +} + +impl NativeRecurrenceDirectExecutorBackend { + #[cfg_attr(target_vendor = "apple", unsafe(link_section = "__TEXT,__rcl_load"))] + #[cfg_attr(target_vendor = "apple", inline(never))] + pub(super) fn load_from_process_pack( + pack: &ProcessDirectExecutorPack, + payloads: &EvaluatorPayloadStore, + plan: &DirectRecurrencePlan, + source_domains: Vec, + ) -> RusticolResult { + let pool = NativeRecurrencePreparedExecutorPool::load_for_direct_plan_from_process_pack( + pack, payloads, plan, + )?; + let source_binding = + OnTheFlySourceDomainBinding::load_for_process_plan(plan, source_domains)?; + let pool = Rc::new(pool); + let catalog = pool.bind_direct_plan(plan, &source_binding)?; + Ok(Self { + catalog, + owners: NativeRecurrenceDirectExecutorOwners { + source_binding, + pool, + }, + }) + } + + /// Load Direct-Arena executors from a verified artifact payload store. + /// + /// `source_domains` is derived from the process runtime metadata. Its + /// indices must match `DirectSourceRow::source_template_or_dispatch_domain`. + #[cfg(feature = "on-the-fly-test-support")] + #[allow(clippy::too_many_arguments)] + pub(super) fn load_from_verified_artifact( + pack: &mut PreparedKernelPackManifest, + artifact: &VerifiedArtifact, + payload_root: impl AsRef, + plan: &DirectRecurrencePlan, + expected_prepared_pack_digest: &str, + expected_catalog_digest: &str, + source_domains: Vec, + ) -> RusticolResult { + let payloads = artifact.evaluator_payload_store(payload_root.as_ref())?; + Self::load_from_store( + pack, + &payloads, + plan, + expected_prepared_pack_digest, + expected_catalog_digest, + source_domains, + ) + } + + #[cfg(feature = "on-the-fly-test-support")] + #[allow(clippy::too_many_arguments)] + pub(super) fn load_from_store( + pack: &mut PreparedKernelPackManifest, + payloads: &EvaluatorPayloadStore, + plan: &DirectRecurrencePlan, + expected_prepared_pack_digest: &str, + expected_catalog_digest: &str, + source_domains: Vec, + ) -> RusticolResult { + let pool = NativeRecurrencePreparedExecutorPool::load_for_direct_plan_from_validated_pack( + pack, + payloads, + plan, + expected_prepared_pack_digest, + expected_catalog_digest, + )?; + let pool = Rc::new(pool); + let source_binding = pool.bind_source_domains(source_domains)?; + let catalog = pool.bind_direct_plan(plan, &source_binding)?; + Ok(Self { + catalog, + owners: NativeRecurrenceDirectExecutorOwners { + source_binding, + pool, + }, + }) + } + + /// Split the lightweight handle catalog from the contexts that it addresses. + /// + /// The caller must retain `owners` for at least as long as `catalog` can be + /// invoked. Contexts are boxed, so moving either returned value cannot + /// invalidate a handle. + pub(super) fn into_parts( + self, + ) -> (DirectExecutorCatalog, NativeRecurrenceDirectExecutorOwners) { + (self.catalog, self.owners) + } +} + +impl NativeRecurrencePreparedExecutorPool { + pub(super) fn validate_model_identities( + &self, + compiled_model_digest: &str, + recurrence_template_catalog_digest: &str, + prepared_kernel_pack_digest: &str, + direct_template_catalog_digest: &str, + ) -> RusticolResult<()> { + if self.compiled_model_digest.to_string() != compiled_model_digest + || self.recurrence_template_catalog_digest.to_string() + != recurrence_template_catalog_digest + || self.prepared_kernel_pack_digest.to_string() != prepared_kernel_pack_digest + || self.direct_template_catalog_digest.to_string() != direct_template_catalog_digest + { + return Err(RusticolError::integrity( + "complete prepared executor pool disagrees with recurrence manifest identities", + )); + } + Ok(()) + } + + #[cfg_attr(target_vendor = "apple", unsafe(link_section = "__TEXT,__rcl_load"))] + #[cfg_attr(target_vendor = "apple", inline(never))] + pub(super) fn load_for_direct_plan_from_process_pack( + pack: &ProcessDirectExecutorPack, + payloads: &EvaluatorPayloadStore, + plan: &DirectRecurrencePlan, + ) -> RusticolResult { + #[cfg(not(any(feature = "f64-symjit", feature = "f64-compiled")))] + let _ = payloads; + if pack.catalog_executor_count != plan.direct_executor_count() + || pack.identities.direct_template_catalog_digest + != plan.direct_template_catalog_digest() + { + return Err(RusticolError::integrity( + "process Direct-Arena pack does not belong to the recurrence plan", + )); + } + let executor_count = usize::try_from(pack.catalog_executor_count).map_err(|_| { + RusticolError::artifact("process Direct-Arena executor count exceeds usize") + })?; + let mut intrinsics = Vec::new(); + #[cfg(feature = "f64-symjit")] + let mut symjit = Vec::new(); + #[cfg(feature = "f64-compiled")] + let mut native = Vec::new(); + let mut handles = vec![None; executor_count]; + let mut executor_roles = vec![None; executor_count]; + let mut contribution_metadata = vec![None; executor_count]; + let mut contribution_fanout = vec![None; executor_count]; + let mut packed_singleton_capabilities = vec![false; executor_count]; + for descriptor in &pack.descriptors { + let executor_id = usize::try_from(descriptor.direct_executor_id).map_err(|_| { + RusticolError::artifact("process Direct-Arena executor ID exceeds usize") + })?; + let role_slot = executor_roles.get_mut(executor_id).ok_or_else(|| { + RusticolError::integrity( + "process Direct-Arena executor is outside the complete catalog", + ) + })?; + if role_slot.replace(descriptor.role).is_some() { + return Err(RusticolError::integrity( + "process Direct-Arena pack repeats an executor", + )); + } + let mut fanout = None; + let (handle, packed_singleton_capable) = match &descriptor.binding { + ProcessDirectExecutorBinding::Source => (None, false), + ProcessDirectExecutorBinding::Intrinsic(intrinsic) => { + if descriptor.role == DirectExecutorRole::Contribution { + let loaded = load_process_contribution_intrinsic(intrinsic)?; + let handle = loaded.handle(); + fanout = loaded.contribution_fanout_handle(); + intrinsics.push(loaded); + (Some(handle), true) + } else { + ( + Some(load_process_intrinsic_handle( + descriptor.role, + &intrinsic.runtime_template, + )?), + true, + ) + } + } + ProcessDirectExecutorBinding::Jit(jit) => { + #[cfg(feature = "f64-symjit")] + { + let loaded = load_process_jit_executor(jit, descriptor.role, payloads)?; + let handle = loaded.handle(); + symjit.push(loaded); + (Some(handle), true) + } + #[cfg(not(feature = "f64-symjit"))] + { + let _ = jit; + return Err(RusticolError::compatibility( + "process Direct-Arena JIT execution requires f64-symjit", + )); + } + } + ProcessDirectExecutorBinding::Native(native_descriptor) => { + #[cfg(feature = "f64-compiled")] + { + let loaded = load_process_native_executor( + native_descriptor, + descriptor.role, + payloads, + )?; + let handle = loaded.handle(); + native.push(loaded); + (Some(handle), true) + } + #[cfg(not(feature = "f64-compiled"))] + { + let _ = native_descriptor; + return Err(RusticolError::compatibility( + "process Direct-Arena native execution requires f64-compiled", + )); + } + } + }; + if handle.is_some_and(|handle| handle.role() != descriptor.role) { + return Err(RusticolError::integrity(format!( + "process Direct-Arena executor {} resolved with the wrong role", + descriptor.direct_executor_id + ))); + } + handles[executor_id] = handle; + contribution_fanout[executor_id] = fanout; + packed_singleton_capabilities[executor_id] = packed_singleton_capable; + contribution_metadata[executor_id] = + process_contribution_execution_metadata(descriptor)?; + } + let summary = NativeRecurrenceDirectBackendSummary { + prepared_kernel_pack_digest: pack.identities.prepared_kernel_pack_digest.to_string(), + direct_template_catalog_digest: pack + .identities + .direct_template_catalog_digest + .to_string(), + backend: pack.target.backend.as_str().to_owned(), + target_triple: pack.target.target_triple.clone(), + target_portable: pack.target.portable, + executor_count, + }; + Ok(Self { + handles: handles.into_boxed_slice(), + bindings: BTreeMap::new(), + executor_roles: executor_roles.into_boxed_slice(), + contribution_metadata: contribution_metadata.into_boxed_slice(), + contribution_fanout: contribution_fanout.into_boxed_slice(), + packed_singleton_capabilities: packed_singleton_capabilities.into_boxed_slice(), + identity_finalizer_id: None, + direct_template_catalog_digest: pack.identities.direct_template_catalog_digest, + recurrence_template_catalog_digest: pack.identities.recurrence_template_catalog_digest, + compiled_model_digest: pack.identities.compiled_model_digest, + prepared_kernel_pack_digest: pack.identities.prepared_kernel_pack_digest, + _intrinsics: intrinsics, + #[cfg(feature = "f64-symjit")] + _symjit: symjit, + #[cfg(feature = "f64-compiled")] + _native: native, + summary, + }) + } + + /// Load and authenticate model-level prepared executors without binding a + /// process source domain or a recurrence plan. + #[cfg(feature = "on-the-fly-test-support")] + pub(super) fn load_from_store( + manifest_json: &[u8], + payloads: &EvaluatorPayloadStore, + expected_prepared_pack_digest: &str, + expected_catalog_digest: &str, + ) -> RusticolResult { + #[cfg(not(any(feature = "f64-symjit", feature = "f64-compiled")))] + let _ = payloads; + let mut pack: PreparedKernelPackManifest = + serde_json::from_slice(manifest_json).map_err(|error| { + RusticolError::serialization(format!( + "could not parse prepared Direct-Arena kernel pack: {error}" + )) + })?; + pack.validate()?; + Self::load_without_plan_from_validated_pack( + &mut pack, + payloads, + expected_prepared_pack_digest, + expected_catalog_digest, + ) + } + + /// Load a model-level pool from a pack already validated at the artifact boundary. + pub(super) fn load_without_plan_from_validated_pack( + pack: &mut PreparedKernelPackManifest, + payloads: &EvaluatorPayloadStore, + expected_prepared_pack_digest: &str, + expected_catalog_digest: &str, + ) -> RusticolResult { + Self::load_from_validated_pack( + pack, + payloads, + None, + expected_prepared_pack_digest, + expected_catalog_digest, + ) + } + + /// Load only the executor contexts referenced by one authenticated direct + /// plan while retaining the complete prepared-catalog identity and index + /// domain. The caller has already validated `pack` at the artifact boundary. + #[cfg(feature = "on-the-fly-test-support")] + fn load_for_direct_plan_from_validated_pack( + pack: &mut PreparedKernelPackManifest, + payloads: &EvaluatorPayloadStore, + plan: &DirectRecurrencePlan, + expected_prepared_pack_digest: &str, + expected_catalog_digest: &str, + ) -> RusticolResult { + Self::load_from_validated_pack( + pack, + payloads, + Some(plan), + expected_prepared_pack_digest, + expected_catalog_digest, + ) + } + + fn load_from_validated_pack( + pack: &mut PreparedKernelPackManifest, + payloads: &EvaluatorPayloadStore, + plan: Option<&DirectRecurrencePlan>, + expected_prepared_pack_digest: &str, + expected_catalog_digest: &str, + ) -> RusticolResult { + #[cfg(not(any(feature = "f64-symjit", feature = "f64-compiled")))] + let _ = payloads; + let direct = pack.take_recurrence_direct_template_catalog( + expected_prepared_pack_digest, + expected_catalog_digest, + )?; + let digest = semantic_digest(&direct.catalog_digest, "Direct-Arena catalog")?; + if plan.is_some_and(|plan| digest != plan.direct_template_catalog_digest()) { + return Err(RusticolError::integrity( + "prepared Direct-Arena catalog digest does not match the recurrence plan", + )); + } + let required_executors = plan + .map(|plan| required_direct_executor_mask(plan, direct.templates.len())) + .transpose()?; + let mut intrinsics = Vec::new(); + #[cfg(feature = "f64-symjit")] + let mut symjit = Vec::new(); + #[cfg(feature = "f64-compiled")] + let mut native = Vec::new(); + let mut handles = Vec::with_capacity(direct.templates.len()); + let mut bindings = BTreeMap::new(); + let mut executor_roles = Vec::with_capacity(direct.templates.len()); + let mut contribution_metadata = Vec::with_capacity(direct.templates.len()); + let mut contribution_fanout = Vec::with_capacity(direct.templates.len()); + let mut packed_singleton_capabilities = Vec::with_capacity(direct.templates.len()); + let mut identity_finalizer_id = None; + for template in &direct.templates { + let mut fanout = None; + let mut packed_singleton_capable = false; + let role = direct_role(&template.role)?; + let parent_permutation: [u8; 2] = template + .payload_binding + .contribution_parent_permutation + .as_slice() + .try_into() + .map_err(|_| { + RusticolError::integrity( + "prepared Direct-Arena executor has an invalid parent permutation", + ) + })?; + if bindings + .insert( + (role, template.evaluator_binding_id), + NativePreparedExecutorBinding { + executor_id: template.direct_executor_id, + parent_permutation, + }, + ) + .is_some() + { + return Err(RusticolError::integrity( + "prepared Direct-Arena catalog repeats a semantic executor binding", + )); + } + if template.payload_binding.kind == "rusticol-intrinsic" + && template.payload_binding.runtime_template.as_deref() + == Some("rusticol.identity-finalize-in-place.v1") + && identity_finalizer_id + .replace(template.direct_executor_id) + .is_some() + { + return Err(RusticolError::integrity( + "prepared Direct-Arena catalog repeats the identity finalizer", + )); + } + let handle = instantiate_selected_executor( + required_executors.as_deref(), + template.direct_executor_id, + || { + let handle = match template.payload_binding.kind.as_str() { + "rusticol-intrinsic" if template.role == "contribution" => { + let loaded = load_contribution_intrinsic(template)?; + let handle = loaded.handle(); + fanout = loaded.contribution_fanout_handle(); + packed_singleton_capable = true; + intrinsics.push(loaded); + Some(handle) + } + "rusticol-intrinsic" if template.role == "source" => None, + "rusticol-intrinsic" => { + let handle = load_intrinsic_handle(template)?; + packed_singleton_capable = true; + Some(handle) + } + "prepared-direct-call" => match template.backend.as_str() { + "jit" => { + #[cfg(feature = "f64-symjit")] + { + let loaded = load_symjit_executor(template, pack, payloads)?; + let handle = loaded.handle(); + packed_singleton_capable = true; + symjit.push(loaded); + Some(handle) + } + #[cfg(not(feature = "f64-symjit"))] + { + return Err(RusticolError::compatibility( + "Direct-Arena JIT recurrence execution requires the f64-symjit feature", + )); + } + } + "cpp" | "asm" => { + #[cfg(feature = "f64-compiled")] + { + let loaded = load_native_executor(template, pack, payloads)?; + let handle = loaded.handle(); + packed_singleton_capable = true; + native.push(loaded); + Some(handle) + } + #[cfg(not(feature = "f64-compiled"))] + { + return Err(RusticolError::compatibility( + "Direct-Arena C++/ASM recurrence execution requires the f64-compiled feature", + )); + } + } + other => { + return Err(RusticolError::compatibility(format!( + "unsupported Direct-Arena prepared-call backend {other:?}" + ))); + } + }, + other => { + return Err(RusticolError::compatibility(format!( + "unsupported Direct-Arena executor binding {other:?}" + ))); + } + }; + Ok(handle) + }, + )?; + if let Some(handle) = handle + && handle.role() != role + { + return Err(RusticolError::integrity(format!( + "Direct-Arena executor {} resolved with the wrong role", + template.direct_executor_id + ))); + } + handles.push(handle); + contribution_fanout.push(fanout); + packed_singleton_capabilities.push(packed_singleton_capable); + executor_roles.push(Some(role)); + contribution_metadata.push(if role == DirectExecutorRole::Contribution { + Some(DirectContributionExecutionMetadata::new( + template.destination_component_count, + !template + .payload_binding + .exact_factor_scalar_slots + .is_empty(), + )?) + } else { + None + }); + } + let recurrence_template_catalog_digest = semantic_digest( + &direct.recurrence_template_catalog_digest, + "recurrence template catalog", + )?; + let compiled_model_digest = + semantic_digest(&direct.compiled_model_digest, "compiled model")?; + let prepared_kernel_pack_digest = + semantic_digest(&direct.prepared_kernel_pack_digest, "prepared kernel pack")?; + let summary = NativeRecurrenceDirectBackendSummary { + prepared_kernel_pack_digest: direct.prepared_kernel_pack_digest.clone(), + direct_template_catalog_digest: direct.catalog_digest.clone(), + backend: direct.backend.clone(), + target_triple: direct.target_triple.clone(), + target_portable: direct.portable, + executor_count: direct.templates.len(), + }; + Ok(Self { + handles: handles.into_boxed_slice(), + bindings, + executor_roles: executor_roles.into_boxed_slice(), + contribution_metadata: contribution_metadata.into_boxed_slice(), + contribution_fanout: contribution_fanout.into_boxed_slice(), + packed_singleton_capabilities: packed_singleton_capabilities.into_boxed_slice(), + identity_finalizer_id, + direct_template_catalog_digest: digest, + recurrence_template_catalog_digest, + compiled_model_digest, + prepared_kernel_pack_digest, + _intrinsics: intrinsics, + #[cfg(feature = "f64-symjit")] + _symjit: symjit, + #[cfg(feature = "f64-compiled")] + _native: native, + summary, + }) + } + + pub(super) fn bind_source_domains( + &self, + source_domains: Vec, + ) -> RusticolResult { + let source_needed = self + .executor_roles + .contains(&Some(DirectExecutorRole::Source)); + let source = if source_needed { + Some(LoadedDirectSourceExecutor::load(source_domains)?) + } else { + if !source_domains.is_empty() { + return Err(RusticolError::integrity( + "source domains were supplied to a prepared catalog without source executors", + )); + } + None + }; + Ok(OnTheFlySourceDomainBinding { source }) + } + + /// Re-express the already authenticated prepared-pack mappings through + /// the shared semantic resolver used by selector-local lowering. This is + /// a compact model-level catalog, not a process recurrence plan. + pub(super) fn prepared_direct_catalog(&self) -> RusticolResult { + let mut bindings = self + .bindings + .iter() + .map(|(&(role, evaluator_binding_id), binding)| { + PreparedDirectExecutorBinding::evaluator_with_parent_permutation( + role, + evaluator_binding_id, + binding.executor_id, + binding.parent_permutation, + ) + }) + .collect::>(); + if let Some(executor_id) = self.identity_finalizer_id { + bindings.push(PreparedDirectExecutorBinding::identity_finalizer( + executor_id, + )); + } + PreparedDirectExecutorCatalog::new(self.direct_template_catalog_digest, bindings) + } + + pub(super) fn into_on_the_fly_resolver( + self, + sources: OnTheFlySourceDomainBinding, + ) -> NativeOnTheFlyPreparedExecutorResolver { + Rc::new(self).into_shared_on_the_fly_resolver(sources) + } + + pub(super) fn into_shared_on_the_fly_resolver( + self: Rc, + sources: OnTheFlySourceDomainBinding, + ) -> NativeOnTheFlyPreparedExecutorResolver { + NativeOnTheFlyPreparedExecutorResolver { + resolved: BTreeMap::new(), + pending_resolved: None, + sources, + pool: self, + } + } + + fn resolve_handle( + &self, + sources: &OnTheFlySourceDomainBinding, + role: DirectExecutorRole, + evaluator_binding_id: u32, + ) -> RusticolResult<(u32, DirectExecutorHandle, [u8; 2])> { + let binding = self + .bindings + .get(&(role, evaluator_binding_id)) + .copied() + .ok_or_else(|| { + RusticolError::integrity(format!( + "prepared recurrence catalog has no {role:?} semantic binding {evaluator_binding_id}" + )) + })?; + let handle = match role { + DirectExecutorRole::Source => sources.source_handle()?, + _ => self + .handles + .get(binding.executor_id as usize) + .and_then(|handle| *handle) + .ok_or_else(|| { + RusticolError::integrity(format!( + "prepared recurrence executor {} is not loaded", + binding.executor_id, + )) + })?, + }; + if handle.role() != role { + return Err(RusticolError::integrity(format!( + "prepared recurrence executor {} has role {:?}, expected {role:?}", + binding.executor_id, + handle.role() + ))); + } + Ok((binding.executor_id, handle, binding.parent_permutation)) + } + + fn resolve_direct_executor_id( + &self, + sources: &OnTheFlySourceDomainBinding, + direct_executor_id: u32, + role: DirectExecutorRole, + ) -> RusticolResult { + let expected_role = self + .executor_roles + .get(direct_executor_id as usize) + .copied() + .flatten() + .ok_or_else(|| { + RusticolError::integrity(format!( + "persisted direct executor {direct_executor_id} has no authenticated role" + )) + })?; + if expected_role != role { + return Err(RusticolError::integrity(format!( + "persisted direct executor {direct_executor_id} has role {expected_role:?}, expected {role:?}" + ))); + } + let handle = if role == DirectExecutorRole::Source { + sources.source_handle()? + } else { + self.handles + .get(direct_executor_id as usize) + .and_then(|handle| *handle) + .ok_or_else(|| { + RusticolError::integrity(format!( + "persisted direct executor {direct_executor_id} is not loaded" + )) + })? + }; + Ok(ResolvedOnTheFlyExecutor { + direct_executor_id, + handle, + // Persisted direct rows already carry the lowering-time parent + // permutation; no query-local remap remains at this boundary. + parent_permutation: [0, 1], + packed_singleton_capable: self.packed_singleton_capability( + sources, + direct_executor_id, + handle, + )?, + interaction_capability: self.interaction_capability(direct_executor_id, handle)?, + }) + } + + fn identity_finalizer_handle(&self) -> RusticolResult<(u32, DirectExecutorHandle)> { + let executor_id = self.identity_finalizer_id.ok_or_else(|| { + RusticolError::integrity("prepared recurrence catalog has no identity finalizer") + })?; + let handle = self + .handles + .get(executor_id as usize) + .and_then(|handle| *handle) + .ok_or_else(|| RusticolError::integrity("identity finalizer is not loaded"))?; + Ok((executor_id, handle)) + } + + fn interaction_capability( + &self, + executor_id: u32, + handle: DirectExecutorHandle, + ) -> RusticolResult> { + if handle.role() != DirectExecutorRole::Contribution { + return Ok(None); + } + let metadata = self + .contribution_metadata + .get(executor_id as usize) + .and_then(|metadata| *metadata) + .ok_or_else(|| { + RusticolError::integrity(format!( + "prepared contribution executor {executor_id} has no authenticated execution metadata" + )) + })?; + self.contribution_fanout + .get(executor_id as usize) + .and_then(|fanout| *fanout) + .map(|fanout| DirectInteractionExecutorCapability::new(handle, metadata, fanout)) + .transpose() + } + + fn packed_singleton_capability( + &self, + sources: &OnTheFlySourceDomainBinding, + executor_id: u32, + handle: DirectExecutorHandle, + ) -> RusticolResult { + let role = self + .executor_roles + .get(executor_id as usize) + .copied() + .flatten() + .ok_or_else(|| { + RusticolError::integrity(format!( + "prepared executor {executor_id} has no authenticated role" + )) + })?; + if handle.role() != role { + return Err(RusticolError::integrity(format!( + "prepared executor {executor_id} has role {:?}, expected {role:?}", + handle.role() + ))); + } + if role == DirectExecutorRole::Source { + return Ok(sources.packed_singleton_capability()); + } + if self + .handles + .get(executor_id as usize) + .and_then(|handle| *handle) + .is_none() + { + return Ok(false); + } + Ok(self + .packed_singleton_capabilities + .get(executor_id as usize) + .copied() + .unwrap_or(false)) + } + + fn bind_direct_plan( + &self, + plan: &DirectRecurrencePlan, + sources: &OnTheFlySourceDomainBinding, + ) -> RusticolResult { + if self.direct_template_catalog_digest != plan.direct_template_catalog_digest() { + return Err(RusticolError::integrity( + "prepared Direct-Arena catalog digest does not match the recurrence plan", + )); + } + let required_executors = required_direct_executor_mask(plan, self.handles.len())?; + let mut handles = Vec::with_capacity(self.handles.len()); + let source_capable = sources.packed_singleton_capability(); + let all_required_executors_are_stride_generic = required_executors.iter().any(|x| *x) + && required_executors + .iter() + .copied() + .enumerate() + .all(|(id, required)| { + !required + || match self.executor_roles.get(id).copied().flatten() { + Some(DirectExecutorRole::Source) => source_capable, + Some(_) => self + .packed_singleton_capabilities + .get(id) + .copied() + .unwrap_or(false), + None => false, + } + }) + && (plan.strategy() != RecurrenceStrategy::AllFlowUnion || source_capable); + for (executor_id, handle) in self.handles.iter().copied().enumerate() { + let selected = required_executors[executor_id]; + let handle = match (handle, selected) { + (Some(handle), _) => Some(handle), + (None, false) => None, + (None, true) => { + let role = self + .executor_roles + .get(executor_id) + .copied() + .flatten() + .ok_or_else(|| { + RusticolError::integrity("prepared executor role is absent") + })?; + if role != DirectExecutorRole::Source { + return Err(RusticolError::integrity(format!( + "referenced prepared non-source executor {executor_id} has no loaded handle" + ))); + } + Some(sources.source_handle()?) + } + }; + handles.push(handle); + } + let catalog = DirectExecutorCatalog::new_sparse_with_metadata_and_fanout( + plan, + self.direct_template_catalog_digest, + handles, + self.contribution_metadata.to_vec(), + self.contribution_fanout.to_vec(), + )?; + Ok(if all_required_executors_are_stride_generic { + catalog.mark_packed_singleton_capable() + } else { + catalog + }) + } +} + +fn process_contribution_execution_metadata( + descriptor: &ProcessDirectExecutorDescriptor, +) -> RusticolResult> { + if descriptor.role != DirectExecutorRole::Contribution { + return Ok(None); + } + DirectContributionExecutionMetadata::new( + descriptor.destination_component_count, + descriptor.uses_exact_factor, + ) + .map(Some) +} + +fn required_direct_executor_mask( + plan: &DirectRecurrencePlan, + catalog_executor_count: usize, +) -> RusticolResult> { + if plan.direct_executor_count() as usize != catalog_executor_count { + return Err(RusticolError::integrity(format!( + "prepared Direct-Arena catalog has {catalog_executor_count} executors, expected {}", + plan.direct_executor_count() + ))); + } + let mut required = vec![false; catalog_executor_count]; + for descriptor in plan.row_groups() { + if descriptor.direct_executor_id == DIRECT_NONE_U32 { + continue; + } + let slot = required + .get_mut(descriptor.direct_executor_id as usize) + .ok_or_else(|| { + RusticolError::integrity(format!( + "recurrence row group references absent prepared executor {}", + descriptor.direct_executor_id + )) + })?; + *slot = true; + } + Ok(required) +} + +fn instantiate_selected_executor( + required_executors: Option<&[bool]>, + executor_id: u32, + instantiate: impl FnOnce() -> RusticolResult>, +) -> RusticolResult> { + let selected = required_executors.map_or(Ok(true), |required| { + required.get(executor_id as usize).copied().ok_or_else(|| { + RusticolError::integrity(format!( + "prepared executor {executor_id} is outside the authenticated catalog" + )) + }) + })?; + if selected { instantiate() } else { Ok(None) } +} + +impl NativeOnTheFlyPreparedExecutorResolver { + pub(super) fn clear_resolved_bindings(&mut self) { + self.pending_resolved = None; + self.resolved.clear(); + } + + pub(super) fn discard_pending_resolved_bindings(&mut self) { + self.pending_resolved = None; + } + + pub(super) fn commit_pending_resolved_bindings(&mut self) -> RusticolResult<()> { + let pending = self.pending_resolved.take().ok_or_else(|| { + RusticolError::internal("on-the-fly family retention has no pending semantic bindings") + })?; + self.resolved = pending; + Ok(()) + } + + #[cfg(any(test, feature = "on-the-fly-test-support"))] + pub(super) fn bind_on_the_fly_trace( + &mut self, + templates: &ValidatedRecurrenceTemplateInput, + direct: &PreparedDirectExecutorCatalog, + seed: &OnTheFlyProcessSeedV1, + trace: &mut OnTheFlyStructuralTraceV1, + ) -> RusticolResult<()> { + self.begin_on_the_fly_family_binding(templates, direct)?; + self.extend_on_the_fly_family_binding(templates, direct, seed, std::iter::once(trace)) + } + + pub(super) fn bind_on_the_fly_family( + &mut self, + templates: &ValidatedRecurrenceTemplateInput, + direct: &PreparedDirectExecutorCatalog, + seed: &OnTheFlyProcessSeedV1, + selected: &mut [OnTheFlySelectedQueryTraceV1], + ) -> RusticolResult<()> { + self.begin_on_the_fly_family_binding(templates, direct)?; + self.extend_on_the_fly_family_binding( + templates, + direct, + seed, + selected.iter_mut().map(|selected| &mut selected.trace), + ) + } + + /// Begin a streamed semantic-binding transaction without disturbing the + /// last successfully committed family. + pub(super) fn begin_on_the_fly_family_binding( + &mut self, + templates: &ValidatedRecurrenceTemplateInput, + direct: &PreparedDirectExecutorCatalog, + ) -> RusticolResult<()> { + self.pending_resolved = None; + let summary = templates.summary(); + if summary.catalog_digest != self.pool.recurrence_template_catalog_digest + || summary.compiled_model_digest != self.pool.compiled_model_digest + || summary.prepared_kernel_pack_digest != self.pool.prepared_kernel_pack_digest + { + return Err(RusticolError::integrity( + "on-the-fly recurrence templates do not belong to the loaded prepared pack", + )); + } + if direct.direct_template_catalog_digest() != self.pool.direct_template_catalog_digest { + return Err(RusticolError::integrity( + "on-the-fly direct-template catalog does not match the loaded prepared pack", + )); + } + self.pending_resolved = Some(BTreeMap::new()); + Ok(()) + } + + /// Extend the current streamed transaction by one bounded trace chunk. + /// Failure drops only the uncommitted map; the active map remains intact. + pub(super) fn extend_on_the_fly_family_binding<'trace>( + &mut self, + templates: &ValidatedRecurrenceTemplateInput, + direct: &PreparedDirectExecutorCatalog, + seed: &OnTheFlyProcessSeedV1, + traces: impl IntoIterator, + ) -> RusticolResult<()> { + let mut resolved = self.pending_resolved.take().ok_or_else(|| { + RusticolError::internal( + "on-the-fly streamed binding extension has no pending transaction", + ) + })?; + let authenticate = authenticated_prepared_executor_binding(templates, direct)?; + let mut trace_count = 0_u32; + for trace in traces { + trace_count = trace_count.checked_add(1).ok_or_else(|| { + RusticolError::integrity("on-the-fly trace-family count exceeds u32") + })?; + if seed.template_catalog_digest() != self.pool.recurrence_template_catalog_digest + || seed.model_digest() != self.pool.compiled_model_digest + || seed.prepared_pack_digest() != self.pool.prepared_kernel_pack_digest + || seed.direct_catalog_digest() != self.pool.direct_template_catalog_digest + || trace.seed_digest() != seed.semantic_digest() + { + return Err(RusticolError::integrity( + "on-the-fly compact process identity does not match the loaded prepared pack", + )); + } + let mut parent_permutations = BTreeMap::new(); + for key in trace.executor_keys() { + let resolved_executor = if let Some(resolved) = resolved.get(&key).copied() { + resolved + } else { + let expected = authenticate(key)?; + let (direct_executor_id, handle, parent_permutation) = + if let Some(evaluator_binding_id) = key.evaluator_binding_id() { + self.pool.resolve_handle( + &self.sources, + key.role(), + evaluator_binding_id, + )? + } else { + let (executor_id, handle) = self.pool.identity_finalizer_handle()?; + (executor_id, handle, [0, 1]) + }; + if direct_executor_id != expected.direct_executor_id + || parent_permutation != expected.parent_permutation + { + return Err(RusticolError::integrity( + "prepared pool and authenticated direct-template mapping disagree", + )); + } + let resolved_executor = ResolvedOnTheFlyExecutor { + direct_executor_id, + handle, + parent_permutation, + packed_singleton_capable: self.pool.packed_singleton_capability( + &self.sources, + direct_executor_id, + handle, + )?, + interaction_capability: self + .pool + .interaction_capability(direct_executor_id, handle)?, + }; + resolved.insert(key, resolved_executor); + resolved_executor + }; + parent_permutations.insert(key, resolved_executor.parent_permutation); + } + trace.bind_prepared_executor_rows(&parent_permutations)?; + } + if trace_count == 0 { + return Err(RusticolError::invalid_argument( + "on-the-fly prepared executor family is empty", + )); + } + self.pending_resolved = Some(resolved); + Ok(()) + } + + /// Bind saved semantic operations to this runner's own prepared kernels. + /// Saved rows already include the authenticated parent permutation; no + /// trace or query reconstruction is involved here. + pub(super) fn bind_on_the_fly_saved_family( + &mut self, + templates: &ValidatedRecurrenceTemplateInput, + direct: &PreparedDirectExecutorCatalog, + keys: impl IntoIterator, + ) -> RusticolResult<()> { + self.begin_on_the_fly_family_binding(templates, direct)?; + let authenticate = authenticated_prepared_executor_binding(templates, direct)?; + let mut resolved = BTreeMap::new(); + for key in keys { + if resolved.contains_key(&key) { + continue; + } + let expected = authenticate(key)?; + let (direct_executor_id, handle, parent_permutation) = + if let Some(binding_id) = key.evaluator_binding_id() { + self.pool + .resolve_handle(&self.sources, key.role(), binding_id)? + } else { + let (id, handle) = self.pool.identity_finalizer_handle()?; + (id, handle, [0, 1]) + }; + if direct_executor_id != expected.direct_executor_id + || parent_permutation != expected.parent_permutation + { + return Err(RusticolError::integrity( + "saved on-the-fly operation differs from its prepared kernel", + )); + } + resolved.insert( + key, + ResolvedOnTheFlyExecutor { + direct_executor_id, + handle, + parent_permutation, + packed_singleton_capable: self.pool.packed_singleton_capability( + &self.sources, + direct_executor_id, + handle, + )?, + interaction_capability: self + .pool + .interaction_capability(direct_executor_id, handle)?, + }, + ); + } + self.pending_resolved = Some(resolved); + Ok(()) + } +} + +impl OnTheFlySourceDomainBinding { + fn packed_singleton_capability(&self) -> bool { + self.source.is_some() + } + + fn source_handle(&self) -> RusticolResult { + let source = self.source.as_ref().ok_or_else(|| { + RusticolError::integrity("on-the-fly source-domain binding is absent") + })?; + let handle = source.handle(); + Ok(DirectExecutorHandle::Source { + call: handle.call, + context: handle.context, + }) + } + + pub(super) fn union_source_dispatch(&self) -> RusticolResult { + self.source + .as_ref() + .map(LoadedDirectSourceExecutor::union_handle) + .ok_or_else(|| { + RusticolError::integrity( + "all-flow-union recurrence backend has no SourceIR dispatcher", + ) + }) + } + + fn bind_union_source_program( + &self, + view: OnTheFlyUnionSourceBindingView<'_>, + ) -> RusticolResult> { + self.source + .as_ref() + .ok_or_else(|| { + RusticolError::integrity( + "all-flow-union recurrence backend has no SourceIR dispatcher", + ) + })? + .bind_union_program(view) + } +} + +impl OnTheFlyPreparedExecutorResolver for NativeOnTheFlyPreparedExecutorResolver { + fn resolve(&self, key: OnTheFlyExecutorKeyV1) -> RusticolResult { + self.effective_resolved().get(&key).copied().ok_or_else(|| { + RusticolError::integrity( + "on-the-fly operation has no exact authenticated prepared executor", + ) + }) + } + + fn invalidate_row_tables(&self) -> RusticolResult<()> { + self.pool.invalidate_on_the_fly_row_tables() + } + + fn resolve_direct_executor( + &self, + direct_executor_id: u32, + role: DirectExecutorRole, + ) -> RusticolResult { + self.pool + .resolve_direct_executor_id(&self.sources, direct_executor_id, role) + } + + fn union_source_dispatch(&self) -> RusticolResult { + self.sources.union_source_dispatch() + } + + fn bind_union_source_program( + &self, + view: OnTheFlyUnionSourceBindingView<'_>, + ) -> RusticolResult> { + self.sources.bind_union_source_program(view) + } + + fn union_source_packed_singleton_capable(&self) -> bool { + self.sources.packed_singleton_capability() + } +} + +impl NativeRecurrencePreparedExecutorPool { + fn invalidate_on_the_fly_row_tables(&self) -> RusticolResult<()> { + #[cfg(feature = "f64-symjit")] + for executor in &self._symjit { + executor.invalidate_row_tables()?; + } + Ok(()) + } +} + +impl NativeRecurrenceDirectExecutorOwners { + pub(super) fn summary(&self) -> &NativeRecurrenceDirectBackendSummary { + &self.pool.summary + } + + pub(super) fn internal_traffic_bytes(&self) -> (u64, u64) { + #[cfg(feature = "f64-symjit")] + { + self.pool + ._symjit + .iter() + .map(LoadedSymjitDirectExecutor::internal_traffic_bytes) + .fold( + (0_u64, 0_u64), + |(scratch, broadcast), (next_scratch, next_broadcast)| { + ( + scratch.saturating_add(next_scratch), + broadcast.saturating_add(next_broadcast), + ) + }, + ) + } + #[cfg(not(feature = "f64-symjit"))] + { + (0, 0) + } + } + + pub(super) fn union_source_dispatch(&self) -> RusticolResult { + self.source_binding.union_source_dispatch() + } +} + +#[cfg(feature = "f64-compiled")] +fn load_process_native_executor( + descriptor: &ProcessNativeExecutor, + role: DirectExecutorRole, + payloads: &EvaluatorPayloadStore, +) -> RusticolResult { + LoadedNativeDirectExecutor::load( + payloads.load_native_library(&descriptor.library_path)?, + role, + descriptor.prepared_kernel_id, + &descriptor.native_entry_point, + descriptor.coupling, + ) +} + +#[cfg(feature = "f64-symjit")] +fn load_process_jit_executor( + descriptor: &ProcessJitExecutor, + role: DirectExecutorRole, + payloads: &EvaluatorPayloadStore, +) -> RusticolResult { + let source = payloads.source(&descriptor.source_application_path)?; + let bytes = source.read()?; + if Sha256::digest(bytes.as_ref()).as_slice() != descriptor.source_application_sha256.as_slice() + { + return Err(RusticolError::integrity(format!( + "process Direct-Arena source application {} has the wrong digest", + source.display_name() + ))); + } + let parameter_bindings = descriptor + .parameter_bindings + .iter() + .copied() + .map(|binding| match binding { + RecurrenceDirectParameterBindingManifest::Plane { index } => { + SymjitDirectParameterBinding::Plane { index } + } + RecurrenceDirectParameterBindingManifest::Scalar { index } => { + SymjitDirectParameterBinding::Broadcast { index } + } + }) + .collect(); + let input_planes = descriptor + .input_plane_projections + .iter() + .copied() + .map(plane_projection) + .collect(); + let scalars = descriptor + .scalar_projections + .iter() + .copied() + .map(scalar_projection) + .collect::>>()?; + LoadedSymjitDirectExecutor::load_prepared_application_bytes( + bytes.as_ref(), + PathBuf::from(source.display_name()), + &descriptor.source_application_abi, + descriptor.optimization_level, + descriptor.plane_compression, + role, + parameter_bindings, + input_planes, + scalars, + descriptor.output_alias_inputs.clone(), + ) +} + +fn load_process_contribution_intrinsic( + intrinsic: &ProcessIntrinsicExecutor, +) -> RusticolResult { + let scale = intrinsic.scale.ok_or_else(|| { + RusticolError::integrity("process contribution intrinsic has no certified scale") + })?; + LoadedRecurrenceIntrinsicDirectExecutor::load_runtime_template( + &intrinsic.runtime_template, + RecurrenceIntrinsicScale::new( + f64::from_bits(scale.constant_real_bits), + f64::from_bits(scale.constant_imag_bits), + scale.parameter_index, + )?, + ) +} + +fn load_process_intrinsic_handle( + role: DirectExecutorRole, + runtime_template: &str, +) -> RusticolResult { + match role { + DirectExecutorRole::Finalization + if runtime_template == "rusticol.identity-finalize-in-place.v1" => + { + Ok(DirectExecutorHandle::Finalization { + call: execute_identity_finalization_rows, + context: std::ptr::null::(), + }) + } + DirectExecutorRole::Finalization + if matches!( + runtime_template, + WEYL_PROPAGATOR_POSITIVE_TEMPLATE + | WEYL_PROPAGATOR_NEGATIVE_TEMPLATE + | FEYNMAN_VECTOR_PROPAGATOR_TEMPLATE + ) => + { + LoadedRecurrenceIntrinsicDirectExecutor::finalization_handle(runtime_template) + } + DirectExecutorRole::Closure + if runtime_template.starts_with("rusticol.closure-reduce.v1:") => + { + Ok(DirectExecutorHandle::Closure { + call: execute_closure_reduce_rows, + context: std::ptr::null::(), + }) + } + _ => Err(RusticolError::compatibility(format!( + "unsupported process Direct-Arena intrinsic {runtime_template:?} for {role:?}" + ))), + } +} + +#[cfg(feature = "f64-compiled")] +fn load_native_executor( + template: &RecurrenceDirectTemplateManifest, + pack: &PreparedKernelPackManifest, + payloads: &EvaluatorPayloadStore, +) -> RusticolResult { + let binding = &template.payload_binding; + let library_path = binding.source_application_path.as_deref().ok_or_else(|| { + RusticolError::artifact("native Direct-Arena prepared call has no library payload") + })?; + let exported_symbol = binding.native_entry_point.as_deref().ok_or_else(|| { + RusticolError::artifact("native Direct-Arena prepared call has no exported symbol") + })?; + let prepared_kernel_id = binding.prepared_kernel_id.ok_or_else(|| { + RusticolError::artifact("native Direct-Arena prepared call has no kernel ID") + })?; + let kernel = pack + .kernels + .iter() + .find(|kernel| kernel.kernel_id == prepared_kernel_id) + .ok_or_else(|| { + RusticolError::integrity(format!( + "native Direct-Arena prepared kernel {prepared_kernel_id} is absent" + )) + })?; + let binding_coupling = native_binding_coupling(binding, kernel)?; + LoadedNativeDirectExecutor::load( + payloads.load_native_library(library_path)?, + direct_role(&template.role)?, + prepared_kernel_id, + exported_symbol, + binding_coupling, + ) +} + +#[cfg(feature = "f64-compiled")] +fn native_binding_coupling( + binding: &super::eager_manifest::RecurrenceDirectPayloadBindingManifest, + kernel: &PreparedKernelManifest, +) -> RusticolResult> { + let expected_parameter_bindings = + kernel.input_contracts.len().checked_mul(2).ok_or_else(|| { + RusticolError::artifact("native Direct-Arena input count exceeds usize") + })?; + if binding.parameter_bindings.len() != expected_parameter_bindings { + return Err(RusticolError::integrity(format!( + "native Direct-Arena kernel {} has {} parameter bindings, expected {}", + kernel.kernel_id, + binding.parameter_bindings.len(), + expected_parameter_bindings, + ))); + } + + let mut coupling_re = None; + let mut coupling_im = None; + for (input_index, input) in kernel.input_contracts.iter().enumerate() { + let destination = match input.role.as_str() { + "coupling-real" => &mut coupling_re, + "coupling-imag" => &mut coupling_im, + _ => continue, + }; + if destination.is_some() { + return Err(RusticolError::integrity(format!( + "native Direct-Arena kernel {} repeats {:?}", + kernel.kernel_id, input.role + ))); + } + let base = input_index * 2; + *destination = Some(native_literal_binding( + binding, + base, + &format!("kernel {} {} input", kernel.kernel_id, input.role), + )?); + let imaginary = native_literal_binding( + binding, + base + 1, + &format!("kernel {} {} imaginary input", kernel.kernel_id, input.role), + )?; + if imaginary != 0.0 { + return Err(RusticolError::integrity(format!( + "native Direct-Arena kernel {} coupling input has a nonzero imaginary scalar lane", + kernel.kernel_id + ))); + } + } + if coupling_re.is_none() && coupling_im.is_none() { + return Ok(None); + } + Ok(Some(( + coupling_re.unwrap_or(0.0), + coupling_im.unwrap_or(0.0), + ))) +} + +#[cfg(feature = "f64-compiled")] +fn native_literal_binding( + binding: &super::eager_manifest::RecurrenceDirectPayloadBindingManifest, + parameter_index: usize, + label: &str, +) -> RusticolResult { + let scalar_index = match binding.parameter_bindings.get(parameter_index) { + Some(RecurrenceDirectParameterBindingManifest::Scalar { index }) => *index, + _ => { + return Err(RusticolError::integrity(format!( + "native Direct-Arena {label} is not bound to a scalar" + ))); + } + }; + let value = match binding.scalar_projections.get(scalar_index as usize) { + Some(RecurrenceDirectScalarProjectionManifest::Literal { value }) => *value, + _ => { + return Err(RusticolError::integrity(format!( + "native Direct-Arena {label} is not an immutable literal" + ))); + } + }; + if !value.is_finite() { + return Err(RusticolError::integrity(format!( + "native Direct-Arena {label} is non-finite" + ))); + } + Ok(value) +} + +fn load_contribution_intrinsic( + template: &RecurrenceDirectTemplateManifest, +) -> RusticolResult { + let runtime_template = template + .payload_binding + .runtime_template + .as_deref() + .ok_or_else(|| RusticolError::artifact("contribution intrinsic has no template"))?; + let scale = match template.payload_binding.scalar_projections.as_slice() { + [ + RecurrenceDirectScalarProjectionManifest::IntrinsicScale { + constant_real_bits, + constant_imag_bits, + parameter_index, + }, + ] => RecurrenceIntrinsicScale::new( + f64::from_bits(*constant_real_bits), + f64::from_bits(*constant_imag_bits), + *parameter_index, + )?, + _ => { + return Err(RusticolError::integrity( + "contribution intrinsic must carry exactly one intrinsic scale", + )); + } + }; + LoadedRecurrenceIntrinsicDirectExecutor::load_runtime_template(runtime_template, scale) +} + +fn load_intrinsic_handle( + template: &RecurrenceDirectTemplateManifest, +) -> RusticolResult { + let runtime_template = template + .payload_binding + .runtime_template + .as_deref() + .ok_or_else(|| RusticolError::artifact("Direct-Arena intrinsic has no runtime template"))?; + load_process_intrinsic_handle(direct_role(&template.role)?, runtime_template) +} + +#[cfg(feature = "f64-symjit")] +fn load_symjit_executor( + template: &RecurrenceDirectTemplateManifest, + pack: &PreparedKernelPackManifest, + payloads: &EvaluatorPayloadStore, +) -> RusticolResult { + let binding = &template.payload_binding; + let expected_compression = symjit_plane_compression(binding, pack)?; + let source_path = binding.source_application_path.as_deref().ok_or_else(|| { + RusticolError::artifact("Direct-Arena prepared call has no source application") + })?; + let source = payloads.source(source_path)?; + let bytes = source.read()?; + let expected_sha = binding + .source_application_sha256 + .as_deref() + .ok_or_else(|| RusticolError::artifact("Direct-Arena source application has no digest"))?; + let actual_sha = format!("{:x}", Sha256::digest(bytes.as_ref())); + if actual_sha != expected_sha { + return Err(RusticolError::integrity(format!( + "Direct-Arena source application {} has digest {actual_sha}, expected {expected_sha}", + source.display_name() + ))); + } + let role = direct_role(&template.role)?; + validate_direct_destination_policy(role, &template.destination_operation)?; + let parameter_bindings = binding + .parameter_bindings + .iter() + .map(|binding| match *binding { + RecurrenceDirectParameterBindingManifest::Plane { index } => { + SymjitDirectParameterBinding::Plane { index } + } + RecurrenceDirectParameterBindingManifest::Scalar { index } => { + SymjitDirectParameterBinding::Broadcast { index } + } + }) + .collect::>(); + let input_planes = binding + .input_plane_projections + .iter() + .copied() + .map(plane_projection) + .collect(); + let scalars = binding + .scalar_projections + .iter() + .copied() + .map(scalar_projection) + .collect::>>()?; + LoadedSymjitDirectExecutor::load_prepared_application_bytes( + bytes.as_ref(), + PathBuf::from(source.display_name()), + binding + .source_application_abi + .as_deref() + .unwrap_or_default(), + template.optimization_level, + expected_compression, + role, + parameter_bindings, + input_planes, + scalars, + binding.output_alias_inputs.clone(), + ) +} + +#[cfg(feature = "f64-symjit")] +fn symjit_plane_compression( + binding: &super::eager_manifest::RecurrenceDirectPayloadBindingManifest, + pack: &PreparedKernelPackManifest, +) -> RusticolResult { + let prepared_kernel_id = binding + .prepared_kernel_id + .ok_or_else(|| RusticolError::artifact("Direct-Arena prepared call has no kernel ID"))?; + let kernel = pack + .kernels + .iter() + .find(|kernel| kernel.kernel_id == prepared_kernel_id) + .ok_or_else(|| { + RusticolError::integrity(format!( + "Direct-Arena prepared kernel {prepared_kernel_id} is absent" + )) + })?; + kernel + .f64_evaluator_manifest + .get("plane_application") + .and_then(serde_json::Value::as_object) + .and_then(|plane| plane.get("compression")) + .and_then(serde_json::Value::as_bool) + .ok_or_else(|| { + RusticolError::compatibility(format!( + "Direct-Arena prepared JIT kernel {prepared_kernel_id} has no authenticated \ + SymJIT compression setting; regenerate the prepared model" + )) + }) +} + +#[cfg(feature = "f64-symjit")] +fn plane_projection( + projection: RecurrenceDirectPlaneProjectionManifest, +) -> SymjitDirectPlaneProjection { + match projection { + RecurrenceDirectPlaneProjectionManifest::ParentCurrent { + parent, + component, + imaginary, + } => SymjitDirectPlaneProjection::ParentCurrent { + parent, + component, + imaginary, + }, + RecurrenceDirectPlaneProjectionManifest::Momentum { + operand, + lorentz_component, + } => SymjitDirectPlaneProjection::Momentum { + operand, + lorentz_component, + }, + RecurrenceDirectPlaneProjectionManifest::DestinationCurrent { + component, + imaginary, + } => SymjitDirectPlaneProjection::DestinationCurrent { + component, + imaginary, + }, + RecurrenceDirectPlaneProjectionManifest::DestinationAmplitude { + component, + imaginary, + } => SymjitDirectPlaneProjection::DestinationAmplitude { + component, + imaginary, + }, + } +} + +#[cfg(feature = "f64-symjit")] +fn scalar_projection( + projection: RecurrenceDirectScalarProjectionManifest, +) -> RusticolResult { + Ok(match projection { + RecurrenceDirectScalarProjectionManifest::ExactFactor { imaginary } => { + SymjitDirectScalarProjection::ExactFactor { imaginary } + } + RecurrenceDirectScalarProjectionManifest::Parameter { index, imaginary } => { + SymjitDirectScalarProjection::Parameter { index, imaginary } + } + RecurrenceDirectScalarProjectionManifest::Literal { value } => { + if !value.is_finite() { + return Err(RusticolError::artifact( + "Direct-Arena literal scalar projection is not finite", + )); + } + SymjitDirectScalarProjection::Literal(value) + } + RecurrenceDirectScalarProjectionManifest::IntrinsicScale { .. } => { + return Err(RusticolError::integrity( + "intrinsic scale cannot be used by a prepared SymJIT callable", + )); + } + }) +} + +fn direct_role(role: &str) -> RusticolResult { + match role { + "source" => Ok(DirectExecutorRole::Source), + "contribution" => Ok(DirectExecutorRole::Contribution), + "finalization" => Ok(DirectExecutorRole::Finalization), + "closure" => Ok(DirectExecutorRole::Closure), + other => Err(RusticolError::compatibility(format!( + "unsupported Direct-Arena executor role {other:?}" + ))), + } +} + +#[cfg(feature = "f64-symjit")] +fn validate_direct_destination_policy(role: DirectExecutorRole, value: &str) -> RusticolResult<()> { + let expected = match role { + DirectExecutorRole::Source => "initialize", + DirectExecutorRole::Contribution => "add", + DirectExecutorRole::Finalization => "finalize-in-place", + DirectExecutorRole::Closure => "closure-add", + }; + if value == expected { + Ok(()) + } else { + Err(RusticolError::integrity(format!( + "Direct-Arena destination operation {value:?} does not match role {role:?}; expected {expected:?}" + ))) + } +} + +fn semantic_digest(value: &str, label: &str) -> RusticolResult { + let bytes = value.as_bytes(); + if bytes.len() != 64 { + return Err(RusticolError::artifact(format!( + "{label} is not a SHA-256 digest" + ))); + } + let mut digest = [0_u8; 32]; + for (index, pair) in bytes.chunks_exact(2).enumerate() { + let high = hex_nibble(pair[0]).ok_or_else(|| { + RusticolError::artifact(format!("{label} is not lowercase hexadecimal")) + })?; + let low = hex_nibble(pair[1]).ok_or_else(|| { + RusticolError::artifact(format!("{label} is not lowercase hexadecimal")) + })?; + digest[index] = (high << 4) | low; + } + SemanticDigest::new(digest) + .map_err(|_| RusticolError::artifact(format!("{label} must not be all zero"))) +} + +fn hex_nibble(value: u8) -> Option { + match value { + b'0'..=b'9' => Some(value - b'0'), + b'a'..=b'f' => Some(value - b'a' + 10), + _ => None, + } +} + +#[cfg(test)] +pub(in crate::engine) mod on_the_fly_adapter_tests { + use super::*; + use crate::engine::evaluator::recurrence_source_direct::{ + DirectSourceDispatchKey, DirectSourceDispatchVariantSpec, DirectSourceOrientation, + DirectSourceTemplateSpec, DirectSourceWavefunctionFamily, + }; + use crate::recurrence::on_the_fly::{ + OnTheFlyStructuralInterpreter, OnTheFlyWorkspaceV1, scalar_adapter_test_seed, + scalar_adapter_test_trace, + }; + use crate::recurrence::{ + DirectResolvedSourceSelection, DirectSourceDispatchVariantDescriptor, + DirectSourceEmbeddingRow, DirectSourceProjectionRow, PreparedDirectExecutorBinding, + PreparedDirectExecutorCatalog, valid_direct_plan_parts_fixture, validated_template_fixture, + }; + use std::cell::Cell; + use std::ptr; + + #[test] + fn selected_prepared_finalization_exact_factor_is_not_contribution_metadata() { + let descriptor = ProcessDirectExecutorDescriptor { + direct_executor_id: 574, + role: DirectExecutorRole::Finalization, + destination_component_count: 2, + uses_exact_factor: true, + binding: ProcessDirectExecutorBinding::Native( + crate::engine::recurrence_process_pack::ProcessNativeExecutor { + prepared_kernel_id: 17, + library_path: "prepared-finalizer.so".to_owned(), + native_entry_point: "prepared_finalizer".to_owned(), + coupling: None, + }, + ), + }; + + assert!(descriptor.uses_exact_factor); + assert!( + process_contribution_execution_metadata(&descriptor) + .unwrap() + .is_none() + ); + } + + #[test] + fn unreferenced_symjit_executor_loader_is_not_invoked_by_plan_filter() { + let required = [true, false, true]; + let calls = Cell::new(0_u32); + let skipped = instantiate_selected_executor(Some(&required), 1, || { + calls.set(calls.get() + 1); + Ok(Some(7_u32)) + }) + .unwrap(); + assert_eq!(skipped, None); + assert_eq!(calls.get(), 0); + + let loaded = instantiate_selected_executor(Some(&required), 2, || { + calls.set(calls.get() + 1); + Ok(Some(9_u32)) + }) + .unwrap(); + assert_eq!(loaded, Some(9)); + assert_eq!(calls.get(), 1); + + let full_catalog = instantiate_selected_executor(None, 99, || { + calls.set(calls.get() + 1); + Ok(Some(13_u32)) + }) + .unwrap(); + assert_eq!(full_catalog, Some(13)); + assert_eq!(calls.get(), 2); + + let propagated = instantiate_selected_executor(Some(&required), 0, || { + Err::, _>(RusticolError::artifact("selected payload is absent")) + }) + .unwrap_err(); + assert_eq!(propagated.kind(), crate::RusticolErrorKind::Artifact); + + let error = + instantiate_selected_executor(Some(&required), 3, || Ok(Some(11_u32))).unwrap_err(); + assert_eq!(error.kind(), crate::RusticolErrorKind::Integrity); + } + + #[test] + fn persisted_direct_ids_resolve_without_a_semantic_binding_catalog() { + let templates = validated_template_fixture(); + let direct_digest = digest(40); + let mut pool = prepared_pool(&templates, direct_digest); + // Process packs retain only plan-owned dense executor IDs. They do not + // carry the model-wide semantic binding map used by query-local OTF. + pool.bindings.clear(); + pool.identity_finalizer_id = None; + let sources = pool.bind_source_domains(source_domains()).unwrap(); + let resolver = pool.into_on_the_fly_resolver(sources); + + let source = resolver + .resolve_direct_executor(0, DirectExecutorRole::Source) + .unwrap(); + assert_eq!(source.direct_executor_id, 0); + assert_eq!(source.handle.role(), DirectExecutorRole::Source); + assert!(resolver.union_source_dispatch().is_ok()); + + let closure = resolver + .resolve_direct_executor(1, DirectExecutorRole::Closure) + .unwrap(); + assert_eq!(closure.direct_executor_id, 1); + assert_eq!(closure.handle.role(), DirectExecutorRole::Closure); + + let finalization = resolver + .resolve_direct_executor(2, DirectExecutorRole::Finalization) + .unwrap(); + assert_eq!(finalization.direct_executor_id, 2); + assert_eq!(finalization.handle.role(), DirectExecutorRole::Finalization); + + assert!( + resolver + .resolve_direct_executor(1, DirectExecutorRole::Finalization) + .is_err() + ); + assert!( + resolver + .resolve_direct_executor(3, DirectExecutorRole::Closure) + .is_err() + ); + } + + pub(in crate::engine) fn digest(seed: u8) -> SemanticDigest { + SemanticDigest::new([seed; 32]).unwrap() + } + + pub(in crate::engine) fn direct_catalog( + direct_digest: SemanticDigest, + ) -> PreparedDirectExecutorCatalog { + PreparedDirectExecutorCatalog::new( + direct_digest, + vec![ + PreparedDirectExecutorBinding::evaluator(DirectExecutorRole::Source, 0, 0), + PreparedDirectExecutorBinding::evaluator(DirectExecutorRole::Closure, 3, 1), + PreparedDirectExecutorBinding::identity_finalizer(2), + ], + ) + .unwrap() + } + + pub(in crate::engine) fn complete_scalar_direct_catalog( + direct_digest: SemanticDigest, + ) -> PreparedDirectExecutorCatalog { + PreparedDirectExecutorCatalog::new( + direct_digest, + vec![ + PreparedDirectExecutorBinding::evaluator(DirectExecutorRole::Source, 0, 0), + PreparedDirectExecutorBinding::evaluator(DirectExecutorRole::Closure, 3, 1), + PreparedDirectExecutorBinding::evaluator(DirectExecutorRole::Closure, 4, 1), + PreparedDirectExecutorBinding::identity_finalizer(2), + ], + ) + .unwrap() + } + + pub(in crate::engine) fn prepared_pool( + templates: &ValidatedRecurrenceTemplateInput, + direct_digest: SemanticDigest, + ) -> NativeRecurrencePreparedExecutorPool { + let summary = templates.summary(); + NativeRecurrencePreparedExecutorPool { + handles: vec![ + None, + Some(DirectExecutorHandle::Closure { + call: execute_closure_reduce_rows, + context: ptr::null(), + }), + Some(DirectExecutorHandle::Finalization { + call: execute_identity_finalization_rows, + context: ptr::null(), + }), + ] + .into_boxed_slice(), + bindings: BTreeMap::from([ + ( + (DirectExecutorRole::Source, 0), + NativePreparedExecutorBinding { + executor_id: 0, + parent_permutation: [0, 1], + }, + ), + ( + (DirectExecutorRole::Closure, 3), + NativePreparedExecutorBinding { + executor_id: 1, + parent_permutation: [0, 1], + }, + ), + ]), + executor_roles: vec![ + Some(DirectExecutorRole::Source), + Some(DirectExecutorRole::Closure), + Some(DirectExecutorRole::Finalization), + ] + .into_boxed_slice(), + contribution_metadata: vec![None, None, None].into_boxed_slice(), + contribution_fanout: vec![None, None, None].into_boxed_slice(), + packed_singleton_capabilities: vec![false, true, true].into_boxed_slice(), + identity_finalizer_id: Some(2), + direct_template_catalog_digest: direct_digest, + recurrence_template_catalog_digest: summary.catalog_digest, + compiled_model_digest: summary.compiled_model_digest, + prepared_kernel_pack_digest: summary.prepared_kernel_pack_digest, + _intrinsics: Vec::new(), + #[cfg(feature = "f64-symjit")] + _symjit: Vec::new(), + #[cfg(feature = "f64-compiled")] + _native: Vec::new(), + summary: NativeRecurrenceDirectBackendSummary { + prepared_kernel_pack_digest: summary.prepared_kernel_pack_digest.to_string(), + direct_template_catalog_digest: direct_digest.to_string(), + backend: "test-intrinsic".into(), + target_triple: "test".into(), + target_portable: true, + executor_count: 3, + }, + } + } + + pub(in crate::engine) fn complete_scalar_prepared_pool( + templates: &ValidatedRecurrenceTemplateInput, + direct_digest: SemanticDigest, + ) -> NativeRecurrencePreparedExecutorPool { + let mut pool = prepared_pool(templates, direct_digest); + pool.bindings.insert( + (DirectExecutorRole::Closure, 4), + NativePreparedExecutorBinding { + executor_id: 1, + parent_permutation: [0, 1], + }, + ); + pool + } + + pub(in crate::engine) fn source_domains() -> Vec { + vec![DirectSourceDispatchDomainSpec { + variants: vec![DirectSourceDispatchVariantSpec { + key: DirectSourceDispatchKey::SpinStateClass(50_000), + template: DirectSourceTemplateSpec { + spin_state_class: 50_000, + family: DirectSourceWavefunctionFamily::Scalar, + orientation: DirectSourceOrientation::SelfConjugate, + helicity: 0, + chirality: 0, + mass_parameter_index: None, + }, + }], + }] + } + + fn all_flow_process_plan_with_internal_source_group() -> DirectRecurrencePlan { + let mut parts = valid_direct_plan_parts_fixture(); + parts.strategy = RecurrenceStrategy::AllFlowUnion; + parts.runtime_helicity_contract_count = 1; + parts.runtime_helicity_variant_count = 1; + parts.sources[0].source_template_or_dispatch_domain = 0; + parts.row_groups[0].direct_executor_id = DIRECT_NONE_U32; + parts.replay_targets[0].helicity_map_count = 0; + parts.replay_helicity_map.clear(); + parts.amplitude_destinations[0].target_helicity_id_or_sentinel = DIRECT_NONE_U32; + parts.resolved_helicities[0].source_selection_count = 1; + parts.source_dispatch_variants = vec![DirectSourceDispatchVariantDescriptor { + embedding_start: 0, + projection_start: 0, + source_row_id: 0, + dispatch_domain_id: 0, + runtime_variant_id: 0, + source_state_index: 0, + source_template_id: 0, + source_state_template_id: 0, + crossed_state_template_id: 0, + crossed_spin_state_class: 0, + direct_executor_id: 0, + crossing_exact_factor_id: 0, + embedding_count: 2, + projection_count: 2, + }]; + parts.source_embeddings = vec![ + DirectSourceEmbeddingRow { + full_component: 0, + source_component_or_sentinel: 0, + exact_factor_id: 0, + }, + DirectSourceEmbeddingRow { + full_component: 1, + source_component_or_sentinel: 1, + exact_factor_id: 0, + }, + ]; + parts.source_projections = vec![ + DirectSourceProjectionRow { + source_component: 0, + full_component: 0, + }, + DirectSourceProjectionRow { + source_component: 1, + full_component: 1, + }, + ]; + parts.resolved_source_selections = vec![DirectResolvedSourceSelection { + source_slot: 0, + dispatch_variant_id: 0, + }]; + DirectRecurrencePlan::new(parts).unwrap() + } + + #[test] + fn sparse_all_flow_process_pack_binds_plan_owned_union_source() { + let plan = all_flow_process_plan_with_internal_source_group(); + assert_eq!(plan.strategy(), RecurrenceStrategy::AllFlowUnion); + assert_eq!(plan.row_groups()[0].direct_executor_id, DIRECT_NONE_U32); + + let templates = validated_template_fixture(); + let mut pool = prepared_pool(&templates, plan.direct_template_catalog_digest()); + // This is the authenticated sparse process-pack shape for AllFlow: + // the internal source group has no descriptor or prepared handle. + pool.executor_roles[0] = None; + pool.bindings.remove(&(DirectExecutorRole::Source, 0)); + assert!( + !pool + .executor_roles + .contains(&Some(DirectExecutorRole::Source)) + ); + assert!(pool.bind_source_domains(source_domains()).is_err()); + + let sources = + OnTheFlySourceDomainBinding::load_for_process_plan(&plan, source_domains()).unwrap(); + let resolver = pool.into_on_the_fly_resolver(sources); + assert!(resolver.union_source_dispatch().is_ok()); + } + + #[test] + fn genuine_scalar_sources_nonunit_identity_and_closure_execute_to_two() { + let templates = validated_template_fixture(); + let summary = templates.summary(); + let direct_digest = digest(40); + let direct = direct_catalog(direct_digest); + let seed = scalar_adapter_test_seed( + summary.compiled_model_digest, + summary.catalog_digest, + summary.prepared_kernel_pack_digest, + direct_digest, + ) + .unwrap(); + let mut trace = scalar_adapter_test_trace(&templates, &seed).unwrap(); + trace.test_insert_identity_finalizer(direct_digest); + let pool = prepared_pool(&templates, direct_digest); + let sources = pool.bind_source_domains(source_domains()).unwrap(); + let mut resolver = pool.into_on_the_fly_resolver(sources); + resolver + .bind_on_the_fly_trace(&templates, &direct, &seed, &mut trace) + .unwrap(); + let work = trace.execution_work_census().unwrap(); + assert_eq!(work.logical_current_count, 2); + assert_eq!(work.resident_current_count, 2); + assert_eq!(work.resident_current_component_count, 2); + assert_eq!(work.source_operation_count, 2); + assert_eq!(work.contribution_operation_count, 0); + assert_eq!(work.finalization_operation_count, 1); + assert_eq!(work.closure_operation_count, 1); + assert_eq!(work.total_kernel_application_count, 4); + assert_eq!(resolver.semantic_executor_binding_count().unwrap(), 3); + assert_eq!(resolver.distinct_prepared_executor_count().unwrap(), 3); + let packed_roles = trace + .executor_keys() + .map(|key| { + let resolved = resolver.resolve(key).unwrap(); + assert!(resolved.packed_singleton_capable); + resolved.handle.role() + }) + .collect::>(); + assert_eq!( + packed_roles, + BTreeSet::from([ + DirectExecutorRole::Source, + DirectExecutorRole::Finalization, + DirectExecutorRole::Closure, + ]) + ); + let mut workspace = OnTheFlyWorkspaceV1::new(&trace, 1).unwrap(); + workspace + .fill_momenta_from_external(&trace, &[0.0; 8], 1) + .unwrap(); + OnTheFlyStructuralInterpreter::execute(&trace, &resolver, &mut workspace, 1).unwrap(); + assert_eq!(workspace.amplitude(0).unwrap(), (2.0, 0.0)); + } + + #[test] + fn adapter_rejects_tampered_operation_and_identity_keys() { + let templates = validated_template_fixture(); + let summary = templates.summary(); + let direct_digest = digest(40); + let direct = direct_catalog(direct_digest); + let seed = scalar_adapter_test_seed( + summary.compiled_model_digest, + summary.catalog_digest, + summary.prepared_kernel_pack_digest, + direct_digest, + ) + .unwrap(); + let pool = prepared_pool(&templates, direct_digest); + let sources = pool.bind_source_domains(source_domains()).unwrap(); + let mut resolver = pool.into_on_the_fly_resolver(sources); + + let mut operation_tamper = scalar_adapter_test_trace(&templates, &seed).unwrap(); + operation_tamper.test_tamper_first_operation_semantic_digest(digest(99)); + assert!( + resolver + .bind_on_the_fly_trace(&templates, &direct, &seed, &mut operation_tamper) + .is_err() + ); + + let mut identity_tamper = scalar_adapter_test_trace(&templates, &seed).unwrap(); + identity_tamper.test_insert_identity_finalizer(digest(99)); + assert!( + resolver + .bind_on_the_fly_trace(&templates, &direct, &seed, &mut identity_tamper) + .is_err() + ); + assert_eq!(resolver.semantic_executor_binding_count().unwrap(), 0); + + let mut retained = scalar_adapter_test_trace(&templates, &seed).unwrap(); + resolver + .bind_on_the_fly_trace(&templates, &direct, &seed, &mut retained) + .unwrap(); + assert_eq!(resolver.semantic_executor_binding_count().unwrap(), 2); + } + + #[test] + fn semantic_binding_candidates_replace_exactly_only_after_commit() { + let templates = validated_template_fixture(); + let summary = templates.summary(); + let direct_digest = digest(40); + let direct = direct_catalog(direct_digest); + let seed = scalar_adapter_test_seed( + summary.compiled_model_digest, + summary.catalog_digest, + summary.prepared_kernel_pack_digest, + direct_digest, + ) + .unwrap(); + let pool = prepared_pool(&templates, direct_digest); + let sources = pool.bind_source_domains(source_domains()).unwrap(); + let mut resolver = pool.into_on_the_fly_resolver(sources); + + let mut first = scalar_adapter_test_trace(&templates, &seed).unwrap(); + resolver + .bind_on_the_fly_trace(&templates, &direct, &seed, &mut first) + .unwrap(); + assert_eq!(resolver.semantic_executor_binding_count().unwrap(), 2); + resolver.commit_pending_resolved_bindings().unwrap(); + + let mut second = scalar_adapter_test_trace(&templates, &seed).unwrap(); + second.test_insert_identity_finalizer(direct_digest); + resolver + .bind_on_the_fly_trace(&templates, &direct, &seed, &mut second) + .unwrap(); + assert_eq!(resolver.semantic_executor_binding_count().unwrap(), 3); + resolver.discard_pending_resolved_bindings(); + assert_eq!(resolver.semantic_executor_binding_count().unwrap(), 2); + + let mut malformed = scalar_adapter_test_trace(&templates, &seed).unwrap(); + malformed.test_insert_identity_finalizer(digest(99)); + assert!( + resolver + .bind_on_the_fly_trace(&templates, &direct, &seed, &mut malformed) + .is_err() + ); + assert_eq!(resolver.semantic_executor_binding_count().unwrap(), 2); + + let mut committed_second = scalar_adapter_test_trace(&templates, &seed).unwrap(); + committed_second.test_insert_identity_finalizer(direct_digest); + resolver + .bind_on_the_fly_trace(&templates, &direct, &seed, &mut committed_second) + .unwrap(); + resolver.commit_pending_resolved_bindings().unwrap(); + assert_eq!(resolver.semantic_executor_binding_count().unwrap(), 3); + + let mut revisited_first = scalar_adapter_test_trace(&templates, &seed).unwrap(); + resolver + .bind_on_the_fly_trace(&templates, &direct, &seed, &mut revisited_first) + .unwrap(); + resolver.commit_pending_resolved_bindings().unwrap(); + assert_eq!(resolver.semantic_executor_binding_count().unwrap(), 2); + } + + #[test] + fn streamed_semantic_binding_failure_rolls_back_every_prior_chunk() { + let templates = validated_template_fixture(); + let summary = templates.summary(); + let direct_digest = digest(40); + let direct = direct_catalog(direct_digest); + let seed = scalar_adapter_test_seed( + summary.compiled_model_digest, + summary.catalog_digest, + summary.prepared_kernel_pack_digest, + direct_digest, + ) + .unwrap(); + let pool = prepared_pool(&templates, direct_digest); + let sources = pool.bind_source_domains(source_domains()).unwrap(); + let mut resolver = pool.into_on_the_fly_resolver(sources); + + let mut committed = scalar_adapter_test_trace(&templates, &seed).unwrap(); + resolver + .bind_on_the_fly_trace(&templates, &direct, &seed, &mut committed) + .unwrap(); + resolver.commit_pending_resolved_bindings().unwrap(); + assert_eq!(resolver.semantic_executor_binding_count().unwrap(), 2); + + let mut first_chunk = scalar_adapter_test_trace(&templates, &seed).unwrap(); + first_chunk.test_insert_identity_finalizer(direct_digest); + resolver + .begin_on_the_fly_family_binding(&templates, &direct) + .unwrap(); + resolver + .extend_on_the_fly_family_binding( + &templates, + &direct, + &seed, + std::iter::once(&mut first_chunk), + ) + .unwrap(); + assert_eq!(resolver.semantic_executor_binding_count().unwrap(), 3); + + let mut malformed_second_chunk = scalar_adapter_test_trace(&templates, &seed).unwrap(); + malformed_second_chunk.test_insert_identity_finalizer(digest(99)); + let error = resolver + .extend_on_the_fly_family_binding( + &templates, + &direct, + &seed, + std::iter::once(&mut malformed_second_chunk), + ) + .unwrap_err(); + assert_eq!(error.kind(), crate::RusticolErrorKind::Integrity); + assert_eq!(resolver.semantic_executor_binding_count().unwrap(), 2); + assert!(resolver.commit_pending_resolved_bindings().is_err()); + assert_eq!(resolver.semantic_executor_binding_count().unwrap(), 2); + } + + #[test] + fn adapter_rejects_missing_binding_and_each_compact_catalog_identity_mismatch() { + let templates = validated_template_fixture(); + let summary = templates.summary(); + let direct_digest = digest(40); + let direct = direct_catalog(direct_digest); + let pool = prepared_pool(&templates, direct_digest); + let sources = pool.bind_source_domains(source_domains()).unwrap(); + let mut resolver = pool.into_on_the_fly_resolver(sources); + let seed = scalar_adapter_test_seed( + summary.compiled_model_digest, + summary.catalog_digest, + summary.prepared_kernel_pack_digest, + direct_digest, + ) + .unwrap(); + let mut trace = scalar_adapter_test_trace(&templates, &seed).unwrap(); + let missing = PreparedDirectExecutorCatalog::new( + direct_digest, + vec![ + PreparedDirectExecutorBinding::evaluator(DirectExecutorRole::Source, 0, 0), + PreparedDirectExecutorBinding::identity_finalizer(1), + ], + ) + .unwrap(); + assert!( + resolver + .bind_on_the_fly_trace(&templates, &missing, &seed, &mut trace) + .is_err() + ); + + for (model, template, pack, direct_id) in [ + ( + digest(90), + summary.catalog_digest, + summary.prepared_kernel_pack_digest, + direct_digest, + ), + ( + summary.compiled_model_digest, + digest(90), + summary.prepared_kernel_pack_digest, + direct_digest, + ), + ( + summary.compiled_model_digest, + summary.catalog_digest, + digest(90), + direct_digest, + ), + ( + summary.compiled_model_digest, + summary.catalog_digest, + summary.prepared_kernel_pack_digest, + digest(90), + ), + ] { + let mismatched = scalar_adapter_test_seed(model, template, pack, direct_id).unwrap(); + let mut mismatched_trace = scalar_adapter_test_trace(&templates, &mismatched).unwrap(); + assert!( + resolver + .bind_on_the_fly_trace(&templates, &direct, &mismatched, &mut mismatched_trace,) + .is_err() + ); + } + } +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/engine/recurrence_bootstrap.rs b/pyamplicol/rust/crates/rusticol-core/src/engine/recurrence_bootstrap.rs new file mode 100644 index 0000000000..ac50da55d1 --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/engine/recurrence_bootstrap.rs @@ -0,0 +1,570 @@ +// SPDX-License-Identifier: 0BSD + +//! Authenticated compact cold-load image for recurrence artifacts. +//! +//! The image deliberately contains no copy of its own digest or the final +//! artifact ID. Generation encodes the staged payload inventory, publishes +//! this image as one more ordinary evaluator-state payload, and lets normal +//! artifact finalization bind the image digest into the artifact ID. At load +//! time the image record can therefore be reconstructed from the checked file +//! itself without a self-hash cycle or a broad JSON-manifest parse. The +//! authenticated image is the runtime authority for producer, process, and +//! execution metadata; the outer manifest contributes only its canonical +//! payload artifact ID on this deliberately narrow path. + +use super::*; +use crate::artifact::{ArtifactRuntime, EvaluatorPayloadContainerExtension, Producer}; +use crate::{ArtifactProcess, Payload}; +use bincode::Decode; +#[cfg(any(feature = "python-generation-bridge", test))] +use bincode::Encode; +#[cfg(feature = "python-generation-bridge")] +use serde::Deserialize; +use sha2::{Digest, Sha256}; +#[cfg(feature = "python-generation-bridge")] +use std::path::PathBuf; + +pub(crate) const RECURRENCE_BOOTSTRAP_IMAGE_ABI: &str = "pyamplicol-recurrence-bootstrap-image-v1"; +const RECURRENCE_BOOTSTRAP_IMAGE_MAGIC: &[u8; 8] = b"PACRBIN1"; +const RECURRENCE_BOOTSTRAP_IMAGE_VERSION: u16 = 1; +const RECURRENCE_BOOTSTRAP_IMAGE_SCHEMA_VERSION: u16 = 1; +const RECURRENCE_BOOTSTRAP_IMAGE_HEADER_BYTES: usize = 64; + +#[cfg(feature = "python-generation-bridge")] +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct RecurrenceBootstrapBuildContextV1 { + schema_version: u16, + /// Private staging root used only while lowering the process-ready image. + /// It is deliberately not serialized into the artifact. + staged_root: PathBuf, + producer: Producer, + runtime: ArtifactRuntime, + process: ArtifactProcess, + payloads: Vec, + evaluator_payload_container: EvaluatorPayloadContainerExtension, +} + +/// Only the authenticated, operational pieces needed to reopen one direct +/// recurrence plan. The schedule PACBIN and process binding remain the sole +/// authorities; this value is a source-digest-bound load recipe, not a second +/// copy of the plan. +#[derive(Clone, Debug, Decode)] +#[cfg_attr(any(feature = "python-generation-bridge", test), derive(Encode))] +pub(super) struct RecurrenceReadyPlanV1 { + pub(super) runtime_schedule: RecurrenceRuntimeContainer, + pub(super) process_binding: RecurrenceProcessBinding, + pub(super) runtime_container_member: RecurrenceRuntimeContainerMember, + pub(super) color_projection_certificate: Option, + pub(super) helicity_dispatch: Option, +} + +impl RecurrenceReadyPlanV1 { + #[cfg(feature = "python-generation-bridge")] + pub(super) fn from_summary(summary: &RecurrencePlanSummary) -> Self { + Self { + runtime_schedule: summary.runtime_schedule.clone(), + process_binding: summary.process_binding.clone(), + runtime_container_member: summary.inspection_summary.runtime_container_member.clone(), + color_projection_certificate: summary + .inspection_summary + .color_projection_certificate + .clone(), + helicity_dispatch: summary.helicity_dispatch.clone(), + } + } +} + +#[derive(Clone, Copy, Debug, Decode)] +#[cfg_attr(any(feature = "python-generation-bridge", test), derive(Encode))] +pub(super) enum RecurrenceReadySourceFamilyV1 { + Scalar, + WeylFermion, + DiracFermion, + Vector, + Spin2, +} + +#[derive(Clone, Copy, Debug, Decode)] +#[cfg_attr(any(feature = "python-generation-bridge", test), derive(Encode))] +pub(super) enum RecurrenceReadySourceOrientationV1 { + Particle, + Antiparticle, + SelfConjugate, +} + +#[derive(Clone, Copy, Debug, Decode)] +#[cfg_attr(any(feature = "python-generation-bridge", test), derive(Encode))] +pub(super) enum RecurrenceReadySourceKeyV1 { + SpinStateClass(i32), + RuntimeVariant { + source_row_id: u32, + runtime_variant_id: u32, + }, +} + +#[derive(Clone, Copy, Debug, Decode)] +#[cfg_attr(any(feature = "python-generation-bridge", test), derive(Encode))] +pub(super) struct RecurrenceReadySourceTemplateV1 { + pub(super) spin_state_class: i32, + pub(super) family: RecurrenceReadySourceFamilyV1, + pub(super) orientation: RecurrenceReadySourceOrientationV1, + pub(super) helicity: i32, + pub(super) chirality: i32, + pub(super) mass_parameter_index: Option, +} + +#[derive(Clone, Copy, Debug, Decode)] +#[cfg_attr(any(feature = "python-generation-bridge", test), derive(Encode))] +pub(super) struct RecurrenceReadySourceVariantV1 { + pub(super) key: RecurrenceReadySourceKeyV1, + pub(super) template: RecurrenceReadySourceTemplateV1, +} + +#[derive(Clone, Debug, Decode)] +#[cfg_attr(any(feature = "python-generation-bridge", test), derive(Encode))] +pub(super) struct RecurrenceReadySourceDomainV1 { + pub(super) variants: Vec, +} + +#[derive(Clone, Debug, Decode)] +#[cfg_attr(any(feature = "python-generation-bridge", test), derive(Encode))] +pub(super) struct RecurrenceReadyRuntimeV1 { + pub(super) runtime_parameters: Vec, + pub(super) prepared_parameter_defaults: Vec<[f64; 2]>, + pub(super) parameter_projection: Vec, + pub(super) source_domains: Vec, + pub(super) external_is_initial: Vec, + pub(super) particle_masses: Vec<(i32, f64)>, + pub(super) particle_mass_parameter_names: Vec<(i32, String)>, + pub(super) normalization: RecurrenceNormalization, + /// Public physics-flow ordinal to the plan's construction sector. This + /// cannot be reconstructed for folded all-flow schedules. + pub(super) public_flow_ids: Vec, + /// Public IDs in the same order, retained so the compact decoder checks + /// the sector mapping against the authenticated physics axis. + pub(super) public_flow_public_ids: Vec, + pub(super) direct_helicity_to_physics: Vec, + pub(super) color_contraction: Option, +} + +#[derive(Clone, Debug, Decode)] +#[cfg_attr(any(feature = "python-generation-bridge", test), derive(Encode))] +pub(super) struct RecurrenceReadyCompanionV1 { + pub(super) process_digest: String, + pub(super) plan: RecurrenceReadyPlanV1, + pub(super) source_domains: Vec, + pub(super) direct_helicity_to_physics: Vec, +} + +#[derive(Clone, Debug, Decode)] +#[cfg_attr(any(feature = "python-generation-bridge", test), derive(Encode))] +pub(crate) struct RecurrenceReadyExecutionV1 { + pub(super) schema_version: u16, + pub(super) required_runtime_capabilities: Vec, + pub(super) process: String, + pub(super) key: String, + pub(super) color_accuracy: String, + pub(super) external_pdg_order: Vec, + pub(super) compiled_model_digest: String, + pub(super) recurrence_template_catalog_digest: String, + pub(super) prepared_kernel_pack_digest: String, + pub(super) direct_template_catalog_digest: String, + pub(super) kernel_pack: RecurrenceKernelPackReference, + pub(super) primary_plan: RecurrenceReadyPlanV1, + pub(super) runtime: RecurrenceReadyRuntimeV1, + pub(super) companion: Option, +} + +fn validate_ready_execution_outer( + ready: &RecurrenceReadyExecutionV1, + outer: &ArtifactProcess, +) -> RusticolResult<()> { + if ready.schema_version != RECURRENCE_BOOTSTRAP_IMAGE_SCHEMA_VERSION + || ready.process != outer.expression + || ready.key != outer.id + || ready.color_accuracy != outer.color_accuracy + || ready.external_pdg_order != outer.external_pdgs + { + return Err(RusticolError::integrity(format!( + "recurrence process-ready recipe does not match outer process {:?}", + outer.id + ))); + } + if ready.kernel_pack.manifest_path != RECURRENCE_KERNEL_PACK_MANIFEST_PATH + || ready.kernel_pack.payload_root != RECURRENCE_KERNEL_PAYLOAD_ROOT + { + return Err(RusticolError::security( + "recurrence process-ready recipe has non-canonical prepared-pack paths", + )); + } + for (label, digest) in [ + ("compiled model", &ready.compiled_model_digest), + ( + "recurrence-template catalog", + &ready.recurrence_template_catalog_digest, + ), + ("prepared-kernel pack", &ready.prepared_kernel_pack_digest), + ( + "direct-template catalog", + &ready.direct_template_catalog_digest, + ), + ] { + if digest.len() != 64 + || !digest + .bytes() + .all(|byte| byte.is_ascii_hexdigit() && !byte.is_ascii_uppercase()) + { + return Err(RusticolError::integrity(format!( + "recurrence process-ready {label} digest is not canonical SHA-256" + ))); + } + } + validate_capability_list_match( + &outer.required_runtime_capabilities, + &ready.required_runtime_capabilities, + "outer process and recurrence process-ready recipe", + ) +} + +#[derive(Debug, Decode)] +#[cfg_attr(any(feature = "python-generation-bridge", test), derive(Encode))] +struct RecurrenceBootstrapImageBodyV1 { + schema_version: u16, + producer: Producer, + runtime: ArtifactRuntime, + process: ArtifactProcess, + /// Ordinary payload declarations before this image is registered. + payloads: Vec, + evaluator_payload_container: EvaluatorPayloadContainerExtension, + execution_path: String, + execution_sha256: String, + physics_path: String, + physics_sha256: String, + ready_execution: RecurrenceReadyExecutionV1, + physics_binary: Vec, +} + +pub(crate) struct DecodedRecurrenceBootstrapV1 { + pub(crate) producer: Producer, + pub(crate) runtime: ArtifactRuntime, + pub(crate) process: ArtifactProcess, + pub(crate) payloads: Vec, + pub(crate) evaluator_payload_container: EvaluatorPayloadContainerExtension, + pub(crate) execution_path: String, + pub(crate) execution_sha256: String, + pub(crate) physics_path: String, + pub(crate) physics_sha256: String, + pub(crate) ready_execution: RecurrenceReadyExecutionV1, + pub(crate) physics: ProcessPhysicsV1, +} + +#[cfg(feature = "python-generation-bridge")] +pub(crate) fn build_recurrence_bootstrap_image_v1( + context_json: &[u8], + execution_json: &[u8], + physics_json: &[u8], +) -> RusticolResult> { + let context: RecurrenceBootstrapBuildContextV1 = + serde_json::from_slice(context_json).map_err(|error| { + RusticolError::serialization(format!( + "could not parse recurrence bootstrap build context: {error}" + )) + })?; + if context.schema_version != RECURRENCE_BOOTSTRAP_IMAGE_SCHEMA_VERSION { + return Err(RusticolError::compatibility(format!( + "unsupported recurrence bootstrap build-context schema {}; expected {}", + context.schema_version, RECURRENCE_BOOTSTRAP_IMAGE_SCHEMA_VERSION + ))); + } + let execution_path = format!("processes/{}/execution.json", context.process.id); + let execution = + parse_recurrence_execution_manifest(execution_json, &execution_path, &context.process)?; + let physics_path = context.process.physics_path.clone(); + let physics = ProcessPhysicsV1::from_json(physics_json, &physics_path)?; + validate_bootstrap_representative_physics(&physics, &context.process)?; + let execution_sha256 = require_build_source_payload( + &context.payloads, + &execution_path, + PayloadRole::EvaluatorManifest, + &context.process.id, + execution_json, + )? + .sha256 + .clone(); + let physics_sha256 = require_build_source_payload( + &context.payloads, + &physics_path, + PayloadRole::RuntimePhysics, + &context.process.id, + physics_json, + )? + .sha256 + .clone(); + let staged_artifact = crate::artifact::open_staged_recurrence_bootstrap_artifact( + context.staged_root, + context.producer.clone(), + context.runtime.clone(), + context.process.clone(), + context.payloads.clone(), + context.evaluator_payload_container.clone(), + )?; + let evaluator_root = staged_artifact + .root() + .join("processes") + .join(&context.process.id); + let ready_execution = build_recurrence_ready_execution_v1( + &staged_artifact, + &evaluator_root, + &execution, + &physics, + )?; + let body = RecurrenceBootstrapImageBodyV1 { + schema_version: RECURRENCE_BOOTSTRAP_IMAGE_SCHEMA_VERSION, + producer: context.producer, + runtime: context.runtime, + process: context.process, + payloads: context.payloads, + evaluator_payload_container: context.evaluator_payload_container, + execution_path, + execution_sha256, + physics_path, + physics_sha256, + ready_execution, + physics_binary: encode_recurrence_bootstrap_physics_v1(&physics)?, + }; + encode_image_body(&body) +} + +#[cfg_attr(target_vendor = "apple", unsafe(link_section = "__TEXT,__rcl_load"))] +#[cfg_attr(target_vendor = "apple", inline(never))] +pub(crate) fn decode_recurrence_bootstrap_image_v1( + bytes: &[u8], +) -> RusticolResult { + let body_bytes = checked_image_body(bytes)?; + let (body, consumed): (RecurrenceBootstrapImageBodyV1, usize) = + bincode::decode_from_slice(body_bytes, bincode::config::standard()).map_err(|error| { + RusticolError::serialization(format!( + "could not decode {RECURRENCE_BOOTSTRAP_IMAGE_ABI}: {error}" + )) + })?; + if consumed != body_bytes.len() { + return Err(RusticolError::serialization(format!( + "{RECURRENCE_BOOTSTRAP_IMAGE_ABI} contains {} trailing body bytes", + body_bytes.len() - consumed + ))); + } + if body.schema_version != RECURRENCE_BOOTSTRAP_IMAGE_SCHEMA_VERSION { + return Err(RusticolError::compatibility(format!( + "unsupported recurrence bootstrap body schema {}; expected {}", + body.schema_version, RECURRENCE_BOOTSTRAP_IMAGE_SCHEMA_VERSION + ))); + } + let physics = decode_recurrence_bootstrap_physics_v1(&body.physics_binary)?; + validate_bootstrap_representative_physics(&physics, &body.process)?; + validate_ready_execution_outer(&body.ready_execution, &body.process)?; + Ok(DecodedRecurrenceBootstrapV1 { + producer: body.producer, + runtime: body.runtime, + process: body.process, + payloads: body.payloads, + evaluator_payload_container: body.evaluator_payload_container, + execution_path: body.execution_path, + execution_sha256: body.execution_sha256, + physics_path: body.physics_path, + physics_sha256: body.physics_sha256, + ready_execution: body.ready_execution, + physics, + }) +} + +#[cfg(feature = "python-generation-bridge")] +fn require_build_source_payload<'a>( + payloads: &'a [Payload], + path: &str, + role: PayloadRole, + process_id: &str, + bytes: &[u8], +) -> RusticolResult<&'a Payload> { + let payload = payloads + .iter() + .find(|payload| payload.path == path) + .ok_or_else(|| { + RusticolError::integrity(format!( + "recurrence bootstrap source {path:?} is absent from the staged payload inventory" + )) + })?; + let size = u64::try_from(bytes.len()).map_err(|_| { + RusticolError::artifact(format!( + "recurrence bootstrap source {path:?} exceeds the u64 size domain" + )) + })?; + let digest = format!("{:x}", Sha256::digest(bytes)); + if payload.role != role + || payload.media_type != "application/json" + || payload.process_id.as_deref() != Some(process_id) + || payload.executable + || payload.size_bytes != size + || payload.sha256 != digest + { + return Err(RusticolError::integrity(format!( + "recurrence bootstrap source {path:?} disagrees with its staged payload declaration" + ))); + } + Ok(payload) +} + +fn validate_bootstrap_representative_physics( + physics: &ProcessPhysicsV1, + process: &ArtifactProcess, +) -> RusticolResult<()> { + if physics.process_id != process.id + || physics.process != process.expression + || physics.color_accuracy.as_str() != process.color_accuracy + || physics + .external_particles + .iter() + .map(|particle| particle.pdg) + .ne(process.external_pdgs.iter().copied()) + { + return Err(RusticolError::integrity(format!( + "recurrence bootstrap physics does not match process {:?}", + process.id + ))); + } + Ok(()) +} + +#[cfg(feature = "python-generation-bridge")] +fn encode_image_body(body: &RecurrenceBootstrapImageBodyV1) -> RusticolResult> { + let encoded = bincode::encode_to_vec(body, bincode::config::standard()).map_err(|error| { + RusticolError::serialization(format!( + "could not encode {RECURRENCE_BOOTSTRAP_IMAGE_ABI}: {error}" + )) + })?; + let body_len = u64::try_from(encoded.len()).map_err(|_| { + RusticolError::artifact(format!( + "{RECURRENCE_BOOTSTRAP_IMAGE_ABI} body length exceeds u64" + )) + })?; + let total_len = RECURRENCE_BOOTSTRAP_IMAGE_HEADER_BYTES + .checked_add(encoded.len()) + .ok_or_else(|| { + RusticolError::artifact(format!( + "{RECURRENCE_BOOTSTRAP_IMAGE_ABI} total length overflows usize" + )) + })?; + let mut image = Vec::new(); + image.try_reserve_exact(total_len).map_err(|error| { + RusticolError::artifact(format!( + "could not allocate {total_len} bytes for {RECURRENCE_BOOTSTRAP_IMAGE_ABI}: {error}" + )) + })?; + image.extend_from_slice(RECURRENCE_BOOTSTRAP_IMAGE_MAGIC); + image.extend_from_slice(&RECURRENCE_BOOTSTRAP_IMAGE_VERSION.to_le_bytes()); + image.extend_from_slice(&0_u16.to_le_bytes()); + image.extend_from_slice(&(RECURRENCE_BOOTSTRAP_IMAGE_HEADER_BYTES as u32).to_le_bytes()); + image.extend_from_slice(&body_len.to_le_bytes()); + image.extend_from_slice(&Sha256::digest(&encoded)); + image.extend_from_slice(&[0_u8; 8]); + debug_assert_eq!(image.len(), RECURRENCE_BOOTSTRAP_IMAGE_HEADER_BYTES); + image.extend_from_slice(&encoded); + Ok(image) +} + +fn checked_image_body(bytes: &[u8]) -> RusticolResult<&[u8]> { + let header = bytes + .get(..RECURRENCE_BOOTSTRAP_IMAGE_HEADER_BYTES) + .ok_or_else(|| { + RusticolError::serialization(format!( + "truncated {RECURRENCE_BOOTSTRAP_IMAGE_ABI} header" + )) + })?; + if header.get(..8) != Some(RECURRENCE_BOOTSTRAP_IMAGE_MAGIC.as_slice()) { + return Err(RusticolError::compatibility(format!( + "unsupported recurrence bootstrap magic; expected {RECURRENCE_BOOTSTRAP_IMAGE_ABI}" + ))); + } + let version = u16::from_le_bytes(header[8..10].try_into().expect("fixed version field")); + let flags = u16::from_le_bytes(header[10..12].try_into().expect("fixed flags field")); + let header_bytes = u32::from_le_bytes( + header[12..16] + .try_into() + .expect("fixed header-length field"), + ); + if version != RECURRENCE_BOOTSTRAP_IMAGE_VERSION + || flags != 0 + || header_bytes != RECURRENCE_BOOTSTRAP_IMAGE_HEADER_BYTES as u32 + || header[56..64].iter().any(|byte| *byte != 0) + { + return Err(RusticolError::compatibility(format!( + "unsupported {RECURRENCE_BOOTSTRAP_IMAGE_ABI} version/flags/header" + ))); + } + let body_len = usize::try_from(u64::from_le_bytes( + header[16..24].try_into().expect("fixed body-length field"), + )) + .map_err(|_| { + RusticolError::artifact(format!( + "{RECURRENCE_BOOTSTRAP_IMAGE_ABI} body length exceeds usize" + )) + })?; + let expected_len = RECURRENCE_BOOTSTRAP_IMAGE_HEADER_BYTES + .checked_add(body_len) + .ok_or_else(|| { + RusticolError::artifact(format!( + "{RECURRENCE_BOOTSTRAP_IMAGE_ABI} length overflows usize" + )) + })?; + if bytes.len() != expected_len { + return Err(RusticolError::serialization(format!( + "{RECURRENCE_BOOTSTRAP_IMAGE_ABI} length is {}, expected {expected_len}", + bytes.len() + ))); + } + let body = &bytes[RECURRENCE_BOOTSTRAP_IMAGE_HEADER_BYTES..]; + if Sha256::digest(body).as_slice() != &header[24..56] { + return Err(RusticolError::integrity(format!( + "{RECURRENCE_BOOTSTRAP_IMAGE_ABI} body SHA-256 mismatch" + ))); + } + Ok(body) +} + +#[cfg(test)] +mod image_tests { + use super::*; + + fn framed(body: &[u8]) -> Vec { + let mut image = Vec::with_capacity(RECURRENCE_BOOTSTRAP_IMAGE_HEADER_BYTES + body.len()); + image.extend_from_slice(RECURRENCE_BOOTSTRAP_IMAGE_MAGIC); + image.extend_from_slice(&RECURRENCE_BOOTSTRAP_IMAGE_VERSION.to_le_bytes()); + image.extend_from_slice(&0_u16.to_le_bytes()); + image.extend_from_slice(&(RECURRENCE_BOOTSTRAP_IMAGE_HEADER_BYTES as u32).to_le_bytes()); + image.extend_from_slice(&(body.len() as u64).to_le_bytes()); + image.extend_from_slice(&Sha256::digest(body)); + image.extend_from_slice(&[0_u8; 8]); + image.extend_from_slice(body); + image + } + + #[test] + fn outer_image_header_authenticates_body_and_exact_length() { + let image = framed(b"bincode-body"); + assert_eq!(checked_image_body(&image).unwrap(), b"bincode-body"); + + let mut corrupted = image.clone(); + *corrupted.last_mut().unwrap() ^= 1; + assert_eq!( + checked_image_body(&corrupted).unwrap_err().kind(), + crate::RusticolErrorKind::Integrity + ); + + let mut trailing = image; + trailing.push(0); + assert_eq!( + checked_image_body(&trailing).unwrap_err().kind(), + crate::RusticolErrorKind::Serialization + ); + } +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/engine/recurrence_bootstrap_physics.rs b/pyamplicol/rust/crates/rusticol-core/src/engine/recurrence_bootstrap_physics.rs new file mode 100644 index 0000000000..b2f2505754 --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/engine/recurrence_bootstrap_physics.rs @@ -0,0 +1,770 @@ +// SPDX-License-Identifier: 0BSD + +//! Compact native image of recurrence process physics. +//! +//! This is an internal cold-load cache, not a stable interchange format. The +//! enclosing PACBIN member authenticates its bytes; this codec supplies an +//! independently recognizable ABI, file-length checks, exact consumption, and +//! the same semantic validation as the canonical JSON path. + +use crate::{ + ColorAccuracy, ColorComponent, Coverage, ExternalParticle, Helicity, ModelParameter, + ProcessPhysics as ProcessPhysicsV1, Reduction, RusticolError, RusticolResult, + SelectorCapabilities, +}; +use bincode::Decode; +#[cfg(any(feature = "python-generation-bridge", test))] +use bincode::Encode; +use bincode::de::{Decoder, read::Reader}; +use bincode::error::{AllowedEnumVariants, DecodeError}; +use serde_json::{Map as JsonMap, Number as JsonNumber, Value as JsonValue}; +use std::collections::BTreeMap; + +/// Native, internal serialization ABI for one already-resolved physics image. +/// Artifacts carrying this image must be regenerated when this wire domain +/// changes. +pub(super) const RECURRENCE_BOOTSTRAP_PHYSICS_BINARY_ABI: &str = + "pyamplicol-recurrence-bootstrap-physics-v1"; + +const RECURRENCE_BOOTSTRAP_PHYSICS_BINARY_MAGIC: &[u8; 8] = b"PACRPHM1"; +const RECURRENCE_BOOTSTRAP_PHYSICS_BINARY_VERSION: u16 = 1; +const RECURRENCE_BOOTSTRAP_PHYSICS_BINARY_HEADER_BYTES: usize = 16; + +// serde_json's ordinary parser rejects nesting beyond 128 levels. Apply the +// same bound to the native representation so a recursive Decode cannot be used +// to exhaust the stack. +const COMPACT_JSON_MAX_DEPTH: usize = 128; + +#[derive(Clone, Debug, Decode)] +#[cfg_attr(any(feature = "python-generation-bridge", test), derive(Encode))] +#[bincode(decode_context = "CompactJsonDecodeContext")] +struct RecurrenceBootstrapPhysicsV1 { + schema_version: u32, + kind: String, + process_id: String, + process: String, + color_accuracy: ColorAccuracy, + coverage: Coverage, + external_particles: Vec, + helicities: Vec, + color_components: Vec, + reduction: Reduction, + model_parameters: Vec, + selectors: SelectorCapabilities, + extensions: CompactJsonObject, +} + +impl RecurrenceBootstrapPhysicsV1 { + #[cfg(any(feature = "python-generation-bridge", test))] + fn from_physics(physics: &ProcessPhysicsV1) -> RusticolResult { + Ok(Self { + schema_version: physics.schema_version, + kind: physics.kind.clone(), + process_id: physics.process_id.clone(), + process: physics.process.clone(), + color_accuracy: physics.color_accuracy, + coverage: physics.coverage.clone(), + external_particles: physics.external_particles.clone(), + helicities: physics.helicities.clone(), + color_components: physics.color_components.clone(), + reduction: physics.reduction.clone(), + model_parameters: physics.model_parameters.clone(), + selectors: physics.selectors.clone(), + extensions: CompactJsonObject::from_extensions(&physics.extensions)?, + }) + } + + fn into_physics(self) -> RusticolResult { + Ok(ProcessPhysicsV1 { + schema_version: self.schema_version, + kind: self.kind, + process_id: self.process_id, + process: self.process, + color_accuracy: self.color_accuracy, + coverage: self.coverage, + external_particles: self.external_particles, + helicities: self.helicities, + color_components: self.color_components, + reduction: self.reduction, + model_parameters: self.model_parameters, + selectors: self.selectors, + extensions: self.extensions.into_extensions()?, + }) + } +} + +#[derive(Clone, Debug)] +#[cfg_attr(any(feature = "python-generation-bridge", test), derive(Encode))] +struct CompactJsonObject(Vec<(String, CompactJsonValue)>); + +impl CompactJsonObject { + #[cfg(any(feature = "python-generation-bridge", test))] + fn from_extensions(extensions: &BTreeMap) -> RusticolResult { + Self::from_entries(extensions.iter(), 0) + } + + #[cfg(any(feature = "python-generation-bridge", test))] + fn from_json_map(values: &JsonMap, depth: usize) -> RusticolResult { + Self::from_entries(values.iter(), depth) + } + + #[cfg(any(feature = "python-generation-bridge", test))] + fn from_entries<'a>( + entries: impl ExactSizeIterator, + depth: usize, + ) -> RusticolResult { + let mut compact = Vec::new(); + compact.try_reserve_exact(entries.len()).map_err(|error| { + compact_json_bound_error(format!("could not allocate compact JSON object: {error}")) + })?; + for (key, value) in entries { + compact.push((key.clone(), CompactJsonValue::from_json(value, depth)?)); + } + compact.sort_unstable_by(|left, right| left.0.cmp(&right.0)); + Ok(Self(compact)) + } + + fn into_extensions(self) -> RusticolResult> { + let mut extensions = BTreeMap::new(); + for (key, value) in self.0 { + if extensions.insert(key, value.into_json()?).is_some() { + return Err(RusticolError::integrity( + "compact recurrence physics extensions contain a duplicate key", + )); + } + } + Ok(extensions) + } + + fn into_json_map(self) -> RusticolResult> { + let mut values = JsonMap::new(); + for (key, value) in self.0 { + if values.insert(key, value.into_json()?).is_some() { + return Err(RusticolError::integrity( + "compact recurrence physics JSON object contains a duplicate key", + )); + } + } + Ok(values) + } +} + +impl Decode for CompactJsonObject { + fn decode>( + decoder: &mut D, + ) -> Result { + let len = decode_compact_json_length(decoder, "object")?; + decoder.claim_container_read::<(String, CompactJsonValue)>(len)?; + let mut entries = Vec::new(); + entries.try_reserve_exact(len).map_err(|error| { + DecodeError::OtherString(format!("could not allocate compact JSON object: {error}")) + })?; + for _ in 0..len { + decoder.unclaim_bytes_read(std::mem::size_of::<(String, CompactJsonValue)>()); + let key = decode_compact_json_string(decoder, "object key")?; + if entries + .last() + .is_some_and(|(previous, _): &(String, CompactJsonValue)| previous >= &key) + { + return Err(DecodeError::Other( + "compact JSON object keys must be unique and strictly ordered", + )); + } + let value = CompactJsonValue::decode(decoder)?; + entries.push((key, value)); + } + Ok(Self(entries)) + } +} + +bincode::impl_borrow_decode_with_context!(CompactJsonObject, CompactJsonDecodeContext); + +#[derive(Clone, Debug)] +#[cfg_attr(any(feature = "python-generation-bridge", test), derive(Encode))] +enum CompactJsonValue { + Null, + Bool(bool), + U64(u64), + I64(i64), + F64Bits(u64), + String(String), + Array(Vec), + Object(CompactJsonObject), +} + +impl CompactJsonValue { + #[cfg(any(feature = "python-generation-bridge", test))] + fn from_json(value: &JsonValue, depth: usize) -> RusticolResult { + if depth > COMPACT_JSON_MAX_DEPTH { + return Err(compact_json_bound_error(format!( + "nesting exceeds the {COMPACT_JSON_MAX_DEPTH}-level limit" + ))); + } + match value { + JsonValue::Null => Ok(Self::Null), + JsonValue::Bool(value) => Ok(Self::Bool(*value)), + JsonValue::Number(value) if value.is_u64() => Ok(Self::U64( + value.as_u64().expect("u64 classification checked"), + )), + JsonValue::Number(value) if value.is_i64() => Ok(Self::I64( + value.as_i64().expect("i64 classification checked"), + )), + JsonValue::Number(value) => { + let value = value.as_f64().ok_or_else(|| { + RusticolError::artifact( + "compact recurrence physics extension has an unsupported JSON number", + ) + })?; + if !value.is_finite() { + return Err(RusticolError::artifact( + "compact recurrence physics extension has a non-finite JSON number", + )); + } + Ok(Self::F64Bits(value.to_bits())) + } + JsonValue::String(value) => Ok(Self::String(value.clone())), + JsonValue::Array(values) => { + let mut compact = Vec::new(); + compact.try_reserve_exact(values.len()).map_err(|error| { + compact_json_bound_error(format!( + "could not allocate compact JSON array: {error}" + )) + })?; + for value in values { + compact.push(Self::from_json(value, depth + 1)?); + } + Ok(Self::Array(compact)) + } + JsonValue::Object(values) => Ok(Self::Object(CompactJsonObject::from_json_map( + values, + depth + 1, + )?)), + } + } + + fn into_json(self) -> RusticolResult { + match self { + Self::Null => Ok(JsonValue::Null), + Self::Bool(value) => Ok(JsonValue::Bool(value)), + Self::U64(value) => Ok(JsonValue::Number(JsonNumber::from(value))), + Self::I64(value) => Ok(JsonValue::Number(JsonNumber::from(value))), + Self::F64Bits(bits) => { + let value = f64::from_bits(bits); + let number = JsonNumber::from_f64(value).ok_or_else(|| { + RusticolError::integrity( + "compact recurrence physics extension contains a non-finite JSON number", + ) + })?; + Ok(JsonValue::Number(number)) + } + Self::String(value) => Ok(JsonValue::String(value)), + Self::Array(values) => values + .into_iter() + .map(Self::into_json) + .collect::>>() + .map(JsonValue::Array), + Self::Object(values) => values.into_json_map().map(JsonValue::Object), + } + } +} + +impl Decode for CompactJsonValue { + fn decode>( + decoder: &mut D, + ) -> Result { + { + let context = decoder.context(); + if context.depth > COMPACT_JSON_MAX_DEPTH { + return Err(DecodeError::Other( + "compact JSON nesting exceeds its depth limit", + )); + } + if context.nodes_remaining == 0 { + return Err(DecodeError::Other( + "compact JSON node count cannot fit in the encoded payload", + )); + } + context.nodes_remaining -= 1; + } + + let variant = >::decode(decoder)?; + match variant { + 0 => Ok(Self::Null), + 1 => bool::decode(decoder).map(Self::Bool), + 2 => u64::decode(decoder).map(Self::U64), + 3 => i64::decode(decoder).map(Self::I64), + 4 => { + let bits = u64::decode(decoder)?; + if !f64::from_bits(bits).is_finite() { + return Err(DecodeError::Other("compact JSON number must be finite")); + } + Ok(Self::F64Bits(bits)) + } + 5 => decode_compact_json_string(decoder, "string").map(Self::String), + 6 => decode_compact_json_children(decoder).map(Self::Array), + 7 => decode_compact_json_object(decoder).map(Self::Object), + found => Err(DecodeError::UnexpectedVariant { + type_name: "CompactJsonValue", + allowed: &COMPACT_JSON_ALLOWED_VARIANTS, + found, + }), + } + } +} + +bincode::impl_borrow_decode_with_context!(CompactJsonValue, CompactJsonDecodeContext); + +static COMPACT_JSON_ALLOWED_VARIANTS: AllowedEnumVariants = + AllowedEnumVariants::Range { min: 0, max: 7 }; + +#[derive(Clone, Copy, Debug)] +struct CompactJsonDecodeContext { + depth: usize, + payload_bytes: usize, + nodes_remaining: usize, +} + +impl CompactJsonDecodeContext { + fn new(payload_bytes: usize) -> Self { + Self { + depth: 0, + payload_bytes, + // Each node needs at least one encoded variant byte. + nodes_remaining: payload_bytes, + } + } +} + +fn decode_compact_json_children>( + decoder: &mut D, +) -> Result, DecodeError> { + let len = decode_compact_json_length(decoder, "array")?; + decoder.claim_container_read::(len)?; + let mut values = Vec::new(); + values.try_reserve_exact(len).map_err(|error| { + DecodeError::OtherString(format!("could not allocate compact JSON array: {error}")) + })?; + let previous_depth = decoder.context().depth; + decoder.context().depth = previous_depth + 1; + let result = (|| { + for _ in 0..len { + decoder.unclaim_bytes_read(std::mem::size_of::()); + values.push(CompactJsonValue::decode(decoder)?); + } + Ok(values) + })(); + decoder.context().depth = previous_depth; + result +} + +fn decode_compact_json_object>( + decoder: &mut D, +) -> Result { + let previous_depth = decoder.context().depth; + decoder.context().depth = previous_depth + 1; + let result = CompactJsonObject::decode(decoder); + decoder.context().depth = previous_depth; + result +} + +fn decode_compact_json_length>( + decoder: &mut D, + description: &str, +) -> Result { + let wire_len = u64::decode(decoder)?; + let len = usize::try_from(wire_len).map_err(|_| DecodeError::OutsideUsizeRange(wire_len))?; + if len > decoder.context().payload_bytes { + return Err(DecodeError::OtherString(format!( + "compact JSON {description} count {len} cannot fit in the encoded payload" + ))); + } + Ok(len) +} + +fn decode_compact_json_string>( + decoder: &mut D, + description: &str, +) -> Result { + let wire_len = u64::decode(decoder)?; + let len = usize::try_from(wire_len).map_err(|_| DecodeError::OutsideUsizeRange(wire_len))?; + if len > decoder.context().payload_bytes { + return Err(DecodeError::OtherString(format!( + "compact JSON {description} length {len} cannot fit in the encoded payload" + ))); + } + decoder.claim_bytes_read(len)?; + let mut bytes = Vec::new(); + bytes.try_reserve_exact(len).map_err(|error| { + DecodeError::OtherString(format!( + "could not allocate compact JSON {description}: {error}" + )) + })?; + bytes.resize(len, 0); + decoder.reader().read(&mut bytes)?; + String::from_utf8(bytes).map_err(|error| DecodeError::Utf8 { + inner: error.utf8_error(), + }) +} + +#[cfg(any(feature = "python-generation-bridge", test))] +fn compact_json_bound_error(detail: impl std::fmt::Display) -> RusticolError { + RusticolError::artifact(format!( + "compact recurrence physics extension exceeds its bounds: {detail}" + )) +} + +/// Encode one typed physics payload for the native recurrence bootstrap. +#[cfg(any(feature = "python-generation-bridge", test))] +pub(super) fn encode_recurrence_bootstrap_physics_v1( + physics: &ProcessPhysicsV1, +) -> RusticolResult> { + let wire = RecurrenceBootstrapPhysicsV1::from_physics(physics)?; + encode_recurrence_bootstrap_physics_wire_v1(&wire) +} + +#[cfg(any(feature = "python-generation-bridge", test))] +fn encode_recurrence_bootstrap_physics_wire_v1( + physics: &RecurrenceBootstrapPhysicsV1, +) -> RusticolResult> { + let body = bincode::encode_to_vec(physics, bincode::config::standard()).map_err(|error| { + RusticolError::serialization(format!( + "could not encode {RECURRENCE_BOOTSTRAP_PHYSICS_BINARY_ABI}: {error}" + )) + })?; + let body_len = u32::try_from(body.len()).map_err(|_| { + RusticolError::artifact(format!( + "{RECURRENCE_BOOTSTRAP_PHYSICS_BINARY_ABI} body length exceeds its u32 wire domain" + )) + })?; + let encoded_len = RECURRENCE_BOOTSTRAP_PHYSICS_BINARY_HEADER_BYTES + .checked_add(body.len()) + .ok_or_else(|| { + RusticolError::artifact(format!( + "{RECURRENCE_BOOTSTRAP_PHYSICS_BINARY_ABI} encoded length overflows usize" + )) + })?; + let mut encoded = Vec::new(); + encoded.try_reserve_exact(encoded_len).map_err(|error| { + RusticolError::artifact(format!( + "could not allocate {encoded_len} bytes for {RECURRENCE_BOOTSTRAP_PHYSICS_BINARY_ABI}: {error}" + )) + })?; + encoded.extend_from_slice(RECURRENCE_BOOTSTRAP_PHYSICS_BINARY_MAGIC); + encoded.extend_from_slice(&RECURRENCE_BOOTSTRAP_PHYSICS_BINARY_VERSION.to_le_bytes()); + encoded.extend_from_slice(&0_u16.to_le_bytes()); + encoded.extend_from_slice(&body_len.to_le_bytes()); + encoded.extend_from_slice(&body); + Ok(encoded) +} + +/// Decode and semantically validate one native recurrence physics +/// payload. Exact consumption is part of the ABI. +#[cfg_attr(target_vendor = "apple", unsafe(link_section = "__TEXT,__rcl_load"))] +#[cfg_attr(target_vendor = "apple", inline(never))] +pub(super) fn decode_recurrence_bootstrap_physics_v1( + bytes: &[u8], +) -> RusticolResult { + let header = bytes + .get(..RECURRENCE_BOOTSTRAP_PHYSICS_BINARY_HEADER_BYTES) + .ok_or_else(|| { + RusticolError::serialization(format!( + "truncated {RECURRENCE_BOOTSTRAP_PHYSICS_BINARY_ABI} header" + )) + })?; + if header.get(..8) != Some(RECURRENCE_BOOTSTRAP_PHYSICS_BINARY_MAGIC.as_slice()) { + return Err(RusticolError::compatibility(format!( + "unsupported recurrence physics binary magic; expected {RECURRENCE_BOOTSTRAP_PHYSICS_BINARY_ABI}" + ))); + } + let version = u16::from_le_bytes( + header[8..10] + .try_into() + .expect("fixed recurrence physics version field"), + ); + if version != RECURRENCE_BOOTSTRAP_PHYSICS_BINARY_VERSION { + return Err(RusticolError::compatibility(format!( + "unsupported recurrence physics binary version {version}; expected {RECURRENCE_BOOTSTRAP_PHYSICS_BINARY_VERSION}" + ))); + } + let reserved = u16::from_le_bytes( + header[10..12] + .try_into() + .expect("fixed recurrence physics reserved field"), + ); + if reserved != 0 { + return Err(RusticolError::compatibility( + "recurrence physics binary reserved flags must be zero", + )); + } + let declared_body_len = usize::try_from(u32::from_le_bytes( + header[12..16] + .try_into() + .expect("fixed recurrence physics length field"), + )) + .map_err(|_| { + RusticolError::artifact(format!( + "{RECURRENCE_BOOTSTRAP_PHYSICS_BINARY_ABI} body length exceeds usize" + )) + })?; + let expected_len = RECURRENCE_BOOTSTRAP_PHYSICS_BINARY_HEADER_BYTES + .checked_add(declared_body_len) + .ok_or_else(|| { + RusticolError::artifact(format!( + "{RECURRENCE_BOOTSTRAP_PHYSICS_BINARY_ABI} declared length overflows usize" + )) + })?; + if bytes.len() != expected_len { + return Err(RusticolError::serialization(format!( + "{RECURRENCE_BOOTSTRAP_PHYSICS_BINARY_ABI} payload length is {}, expected {expected_len}", + bytes.len(), + ))); + } + + let body = &bytes[RECURRENCE_BOOTSTRAP_PHYSICS_BINARY_HEADER_BYTES..]; + let (wire, consumed): (RecurrenceBootstrapPhysicsV1, usize) = + bincode::decode_from_slice_with_context( + body, + bincode::config::standard(), + CompactJsonDecodeContext::new(body.len()), + ) + .map_err(|error| { + RusticolError::serialization(format!( + "could not decode {RECURRENCE_BOOTSTRAP_PHYSICS_BINARY_ABI}: {error}" + )) + })?; + if consumed != body.len() { + return Err(RusticolError::serialization(format!( + "{RECURRENCE_BOOTSTRAP_PHYSICS_BINARY_ABI} contains {} trailing bytes", + body.len() - consumed, + ))); + } + let physics = wire.into_physics()?; + physics.validate()?; + Ok(physics) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + ColorAccuracy, ColorComponent, ContractedColor, Coverage, ExternalParticle, Helicity, + ModelParameter, ParameterKind, ParticleRole, RUNTIME_PHYSICS_SCHEMA_VERSION, Reduction, + ReductionGroup, ReductionKind, SelectorCapabilities, + }; + use serde_json::json; + + fn physics_fixture() -> ProcessPhysicsV1 { + let mut extensions = BTreeMap::new(); + extensions.insert("array".to_owned(), json!([null, true, {"nested": [1, 2]}])); + extensions.insert("float".to_owned(), json!(0.125)); + extensions.insert("i64".to_owned(), json!(i64::MIN)); + extensions.insert("negative_zero".to_owned(), json!(-0.0)); + extensions.insert("string".to_owned(), json!("extension text")); + extensions.insert("u64".to_owned(), json!(u64::MAX)); + + ProcessPhysicsV1 { + schema_version: RUNTIME_PHYSICS_SCHEMA_VERSION, + kind: "pyamplicol-resolved-physics".to_owned(), + process_id: "p0".to_owned(), + process: "a b > c".to_owned(), + color_accuracy: ColorAccuracy::Full, + coverage: Coverage { + helicities: "complete".to_owned(), + color: "contracted".to_owned(), + color_kind: "contracted-color".to_owned(), + structural_zero_helicity_count: 0, + }, + external_particles: (0..3) + .map(|index| ExternalParticle { + index, + label: index + 1, + particle: format!("particle-{index}"), + pdg: index as i32 + 1, + role: if index < 2 { + ParticleRole::Initial + } else { + ParticleRole::Final + }, + momentum_slot: index, + momentum_components: [ + "E".to_owned(), + "px".to_owned(), + "py".to_owned(), + "pz".to_owned(), + ], + }) + .collect(), + helicities: vec![Helicity { + id: "helicity:0".to_owned(), + index: 0, + values: vec![1, -1, 1], + computed: true, + structural_zero: false, + representative_id: "helicity:0".to_owned(), + coefficient: 1.0, + }], + color_components: vec![ColorComponent::ContractedColor(ContractedColor { + id: "contracted".to_owned(), + index: 0, + description: "coherent contracted color".to_owned(), + })], + reduction: Reduction { + kind: ReductionKind::ContractedColor, + groups: vec![ReductionGroup { + id: "group:0".to_owned(), + representative_helicity_id: "helicity:0".to_owned(), + representative_color_id: "contracted".to_owned(), + physical_helicity_ids: vec!["helicity:0".to_owned()], + physical_color_ids: vec!["contracted".to_owned()], + }], + }, + model_parameters: vec![ModelParameter { + name: "coupling".to_owned(), + kind: ParameterKind::Coupling, + default_real: f64::from_bits(0x3fdd_8fdb_d004_403d), + default_imaginary: -0.0, + mutable: true, + }], + selectors: SelectorCapabilities { + helicity: true, + color_flow: false, + contracted_color: false, + }, + extensions, + } + } + + #[test] + fn compact_recurrence_physics_round_trips_every_json_value_exactly() { + let physics = physics_fixture(); + physics.validate().expect("valid physics fixture"); + let encoded = encode_recurrence_bootstrap_physics_v1(&physics) + .expect("encode compact recurrence physics"); + assert_eq!( + encoded.get(..8), + Some(RECURRENCE_BOOTSTRAP_PHYSICS_BINARY_MAGIC.as_slice()) + ); + + let decoded = decode_recurrence_bootstrap_physics_v1(&encoded) + .expect("decode compact recurrence physics"); + assert_eq!(decoded, physics); + assert_eq!( + decoded.extensions["negative_zero"] + .as_f64() + .expect("negative zero number") + .to_bits(), + (-0.0_f64).to_bits() + ); + assert_eq!( + encode_recurrence_bootstrap_physics_v1(&decoded) + .expect("deterministic compact recurrence physics re-encode"), + encoded + ); + } + + #[test] + fn compact_recurrence_physics_rejects_format_extensions_and_oversize() { + let encoded = encode_recurrence_bootstrap_physics_v1(&physics_fixture()) + .expect("encode compact recurrence physics"); + + let mut future_version = encoded.clone(); + future_version[8..10] + .copy_from_slice(&(RECURRENCE_BOOTSTRAP_PHYSICS_BINARY_VERSION + 1).to_le_bytes()); + assert!( + decode_recurrence_bootstrap_physics_v1(&future_version) + .unwrap_err() + .message() + .contains("version") + ); + + let mut future_flags = encoded.clone(); + future_flags[10..12].copy_from_slice(&1_u16.to_le_bytes()); + assert!( + decode_recurrence_bootstrap_physics_v1(&future_flags) + .unwrap_err() + .message() + .contains("reserved") + ); + + let mut trailing_body = encoded.clone(); + let body_len = u32::from_le_bytes(trailing_body[12..16].try_into().unwrap()); + trailing_body[12..16].copy_from_slice(&(body_len + 1).to_le_bytes()); + trailing_body.push(0); + assert!( + decode_recurrence_bootstrap_physics_v1(&trailing_body) + .unwrap_err() + .message() + .contains("trailing") + ); + + let mut oversized = encoded; + oversized[12..16].copy_from_slice(&u32::MAX.to_le_bytes()); + assert!( + decode_recurrence_bootstrap_physics_v1(&oversized) + .unwrap_err() + .message() + .contains("payload length") + ); + } + + #[test] + fn compact_recurrence_physics_rejects_noncanonical_and_deep_extensions() { + let mut wire = RecurrenceBootstrapPhysicsV1::from_physics(&physics_fixture()) + .expect("construct compact recurrence physics"); + wire.extensions = CompactJsonObject(vec![ + ("z".to_owned(), CompactJsonValue::Null), + ("a".to_owned(), CompactJsonValue::Null), + ]); + let encoded = encode_recurrence_bootstrap_physics_wire_v1(&wire) + .expect("encode noncanonical compact object"); + assert!( + decode_recurrence_bootstrap_physics_v1(&encoded) + .unwrap_err() + .message() + .contains("strictly ordered") + ); + + let mut value = CompactJsonValue::Null; + for _ in 0..=COMPACT_JSON_MAX_DEPTH { + value = CompactJsonValue::Array(vec![value]); + } + wire.extensions = CompactJsonObject(vec![("deep".to_owned(), value)]); + let encoded = encode_recurrence_bootstrap_physics_wire_v1(&wire) + .expect("encode over-deep compact extension"); + assert!( + decode_recurrence_bootstrap_physics_v1(&encoded) + .unwrap_err() + .message() + .contains("depth") + ); + } + + #[test] + fn compact_recurrence_physics_revalidates_typed_semantics_and_numbers() { + let mut wire = RecurrenceBootstrapPhysicsV1::from_physics(&physics_fixture()) + .expect("construct compact recurrence physics"); + wire.process.clear(); + let encoded = encode_recurrence_bootstrap_physics_wire_v1(&wire) + .expect("encode invalid compact recurrence physics"); + assert!( + decode_recurrence_bootstrap_physics_v1(&encoded) + .unwrap_err() + .message() + .contains("invalid process") + ); + + wire.process = "a b > c".to_owned(); + wire.extensions = CompactJsonObject(vec![( + "nan".to_owned(), + CompactJsonValue::F64Bits(f64::NAN.to_bits()), + )]); + let encoded = encode_recurrence_bootstrap_physics_wire_v1(&wire) + .expect("encode non-finite compact JSON number"); + assert!( + decode_recurrence_bootstrap_physics_v1(&encoded) + .unwrap_err() + .message() + .contains("finite") + ); + } +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/engine/recurrence_integration_tests.rs b/pyamplicol/rust/crates/rusticol-core/src/engine/recurrence_integration_tests.rs new file mode 100644 index 0000000000..9397f34647 --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/engine/recurrence_integration_tests.rs @@ -0,0 +1,253 @@ +// SPDX-License-Identifier: 0BSD + +//! Opt-in generated-artifact smoke tests for the recurrence execution lane. + +use super::*; + +fn assert_close(actual: f64, expected: f64, context: &str) { + let tolerance = 1.0e-15 + 1.0e-12 * expected.abs(); + assert!( + (actual - expected).abs() <= tolerance, + "{context}: {actual:.17e} != {expected:.17e}" + ); +} + +#[cfg(feature = "f64-symjit")] +fn generated_recurrence_validation_point(runtime: &NativeRuntime) -> Vec<[f64; 4]> { + let validation_path = runtime + .root() + .join("processes") + .join(&runtime.metadata().representative_process_key) + .join("validation-momenta.json"); + let validation: Value = serde_json::from_slice( + &fs::read(&validation_path).expect("read recurrence validation momenta"), + ) + .expect("parse recurrence validation momenta"); + validation["points"][0] + .as_array() + .expect("one recurrence validation point") + .iter() + .map(|leg| { + let components = leg["momentum"] + .as_array() + .expect("four momentum components"); + assert_eq!( + components.len(), + 4, + "recurrence momentum has four components" + ); + std::array::from_fn(|index| { + components[index] + .as_str() + .expect("decimal momentum string") + .parse::() + .expect("f64 validation momentum") + }) + }) + .collect() +} + +#[cfg(feature = "f64-symjit")] +fn assert_zero_recurrence_direct_boundary_traffic(profile: &RuntimeProfile, context: &str) { + for (label, observed) in [ + ( + "legacy packed input bytes", + profile.recurrence_direct_packed_input_bytes, + ), + ( + "legacy packed output bytes", + profile.recurrence_direct_packed_output_bytes, + ), + ( + "legacy scatter bytes", + profile.recurrence_direct_scatter_bytes, + ), + ( + "packet input bytes", + profile.recurrence_direct_packet_input_bytes, + ), + ( + "packet output bytes", + profile.recurrence_direct_packet_output_bytes, + ), + ("gather bytes", profile.recurrence_direct_gather_bytes), + ( + "traffic scatter bytes", + profile.recurrence_direct_traffic_scatter_bytes, + ), + ("remap bytes", profile.recurrence_direct_remap_bytes), + ] { + assert_eq!(observed, 0, "{context}: {label}"); + } + profile + .validate_recurrence_direct_boundary_traffic() + .unwrap_or_else(|error| panic!("{context}: {error}")); +} + +#[cfg(feature = "f64-symjit")] +#[test] +fn generated_recurrence_artifact_loads_when_fixture_is_supplied() { + let Some(root) = std::env::var_os("RUSTICOL_RECURRENCE_ARTIFACT") else { + return; + }; + let mut runtime = NativeRuntime::load(PathBuf::from(root), None, None) + .expect("load generated recurrence artifact through NativeRuntime"); + assert_eq!(runtime.metadata().execution_mode, "recurrence"); + + let point = generated_recurrence_validation_point(&runtime); + let momenta = point + .iter() + .flat_map(|momentum| momentum.iter().copied()) + .collect::>(); + + let values = runtime + .evaluate_f64(&momenta, 1) + .expect("evaluate generated recurrence artifact"); + assert_eq!(values.len(), 1); + assert!(values[0].is_finite()); + let mut direct_values = [f64::NAN]; + runtime + .evaluate_f64_into(&momenta, 1, &mut direct_values) + .expect("evaluate generated recurrence artifact into caller storage"); + assert_close( + direct_values[0], + values[0], + "recurrence direct-output total", + ); + + let resolved = runtime + .evaluate_resolved_f64(&momenta, 1, None, None) + .expect("resolve generated recurrence artifact"); + assert_eq!(resolved.point_count, 1); + assert_close(resolved.totals()[0], values[0], "recurrence resolved sum"); + + let selected_color = runtime + .color_ids() + .expect("recurrence color metadata") + .into_iter() + .next() + .expect("one recurrence color component"); + let selected = runtime + .evaluate_resolved_f64( + &momenta, + 1, + None, + Some(std::slice::from_ref(&selected_color)), + ) + .expect("select recurrence color component"); + assert_eq!(selected.color_ids, [selected_color]); + assert!(selected.values.iter().all(|value| value.is_finite())); + let selected_total = runtime + .evaluate_f64_with_selectors(&momenta, 1, None, Some(&selected.color_ids), None, None) + .expect("evaluate selected recurrence color component"); + assert_close( + selected_total[0], + selected.totals()[0], + "recurrence selected color total", + ); + let mut selected_direct = [f64::NAN]; + runtime + .evaluate_f64_into_with_selectors( + &momenta, + 1, + None, + Some(&selected.color_ids), + None, + None, + &mut selected_direct, + ) + .expect("evaluate selected recurrence component into caller storage"); + assert_close( + selected_direct[0], + selected_total[0], + "recurrence selected direct-output total", + ); +} + +#[cfg(feature = "f64-symjit")] +#[test] +fn generated_recurrence_odd_tails_report_zero_direct_boundary_traffic_when_fixtures_are_supplied() { + const ODD_TAIL_POINT_COUNTS: [usize; 4] = [127, 129, 1023, 1025]; + const FIXTURES: [(&str, &str); 2] = [ + ( + "RUSTICOL_RECURRENCE_TOPOLOGY_ALLOCATION_ARTIFACT", + "topology-replay", + ), + ( + "RUSTICOL_RECURRENCE_UNION_ALLOCATION_ARTIFACT", + "all-flow-union", + ), + ]; + + for (environment_name, layout) in FIXTURES { + let Some(root) = std::env::var_os(environment_name) else { + eprintln!( + "skipping genuine recurrence {layout} boundary gate: \ + {environment_name} is not set" + ); + continue; + }; + let mut runtime = NativeRuntime::load(PathBuf::from(root), None, None) + .unwrap_or_else(|error| panic!("load genuine recurrence {layout} artifact: {error}")); + let metadata = runtime.metadata(); + assert_eq!( + metadata.execution_mode, "recurrence", + "genuine {layout} fixture execution mode" + ); + assert_eq!( + metadata.prepared_backend.as_deref(), + Some("jit"), + "genuine {layout} fixture backend" + ); + let point = generated_recurrence_validation_point(&runtime); + + for point_count in ODD_TAIL_POINT_COUNTS { + let batch = vec![point.clone(); point_count]; + let flat_momenta = batch + .iter() + .flat_map(|momenta| momenta.iter().flat_map(|value| value.iter().copied())) + .collect::>(); + let mut caller_output = vec![f64::NAN; point_count]; + runtime + .evaluate_f64_into(&flat_momenta, point_count, &mut caller_output) + .unwrap_or_else(|error| { + panic!( + "warm genuine recurrence {layout} caller-output path at \ + {point_count} points: {error}" + ) + }); + + let (profiled_values, profile) = { + let (common, execution_lane) = (&mut runtime.runtime, &mut runtime.execution_lane); + let NativeExecutionLane::Recurrence(lane) = execution_lane else { + panic!("genuine {layout} fixture did not load the recurrence lane"); + }; + lane.run_f64(common, &batch).unwrap_or_else(|error| { + panic!( + "profile genuine recurrence {layout} Direct-Arena path at \ + {point_count} points: {error}" + ) + }) + }; + let context = format!("genuine recurrence {layout} odd tail {point_count}"); + assert_zero_recurrence_direct_boundary_traffic(&profile, &context); + assert_eq!( + profiled_values.len(), + caller_output.len(), + "{context}: profiled output length" + ); + for (index, (profiled, warmed)) in profiled_values + .iter() + .copied() + .zip(caller_output.iter().copied()) + .enumerate() + { + assert_close( + profiled, + warmed, + &format!("{context}: caller-output parity point {index}"), + ); + } + } + } +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/engine/recurrence_lane.rs b/pyamplicol/rust/crates/rusticol-core/src/engine/recurrence_lane.rs new file mode 100644 index 0000000000..38e2d08f59 --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/engine/recurrence_lane.rs @@ -0,0 +1,5317 @@ +// SPDX-License-Identifier: 0BSD + +//! Native adapter from public runtime inputs to Direct-Arena recurrence. + +use super::recurrence_backend::{ + NativeOnTheFlyPreparedExecutorResolver, NativeRecurrenceDirectExecutorOwners, +}; +use super::*; +use crate::direct_arena::DirectArenaTrafficCounters; +use crate::recurrence::direct_backend::{ + DirectExecutionCounters, DirectExecutionRoleTimings, DirectExecutorCatalog, +}; +use crate::recurrence::direct_runtime::{ + DirectRecurrenceExecutionRuntime, DirectRecurrenceTileOutput, DirectReplaySelectorPlan, + DirectRuntimeActivityCounters, DirectRuntimePhaseTimings, DirectUnionHelicitySelectorPlan, +}; +use crate::recurrence::on_the_fly::{ + PersistedHelicityAmplitudeTileV1, PersistedHelicityFamilyCacheCensusV1, + PersistedHelicityFamilyExecutionReportV1, PersistedHelicityFamilyExecutorV1, + PersistedHelicityFamilyInspectionCensusV1, +}; +use crate::recurrence::{ + DIRECT_NONE_U32, DirectAmplitudeDestinationDescriptor, DirectHelicityDispatch, + DirectRecurrencePlan, RecurrenceColorContraction, RecurrenceColorStorage, RecurrenceStrategy, + RuntimeColorContractionEntry, RuntimeColorContractionReducer, + RuntimeSymmetricGroupColorWorkspace, +}; +use std::collections::BTreeMap; +use std::sync::Arc; + +#[derive(Clone, Copy, Debug)] +pub(super) struct PreparedParameterProjectionEntry { + pub(super) runtime_slot: usize, + pub(super) prepared_slot: usize, + pub(super) component: u8, +} + +/// One process-local Direct-Arena runtime. +/// +/// `_backend_owner` keeps every immutable source/SymJIT context addressed by +/// the scheduler's lightweight function handles alive. All point-dependent +/// storage belongs to `scheduler` or the fixed-size external-momentum scratch. +pub(super) struct RecurrenceNativeRuntime { + scheduler: DirectRecurrenceExecutionRuntime, + _backend_owner: NativeRecurrenceDirectExecutorOwners, + backend_name: String, + selectors: RecurrenceNativeSelectors, + parameter_defaults: Vec, + parameter_projection: Vec, + external_source_count: usize, + effective_point_tile_size: usize, + external_momenta: Vec, + contracted_replay_re: Vec, + contracted_replay_im: Vec, + color_transform_re: Vec, + color_transform_im: Vec, + symmetric_group_color_workspace: Option, + replay_color_index_scratch: Vec, + selected_replay_cache: + BTreeMap, BTreeMap>>, + selected_replay_programs: BTreeMap<(u32, Vec), Arc>, + helicity_selector_companion: Option, +} + +pub(super) struct RecurrencePersistedHelicitySelectorRuntime { + plan: DirectRecurrencePlan, + dispatch: DirectHelicityDispatch, + executor: PersistedHelicityFamilyExecutorV1, + resolved_helicity_by_physics: Box<[u32]>, + primary_color_view: Option, +} + +struct PrimaryLocalColorViewV1 { + auxiliary_destination_by_local_group: Box<[u32]>, + local_group_by_primary_destination: Box<[u32]>, +} + +impl RecurrencePersistedHelicitySelectorRuntime { + pub(super) fn new( + plan: DirectRecurrencePlan, + dispatch: DirectHelicityDispatch, + resolver: NativeOnTheFlyPreparedExecutorResolver, + direct_helicity_to_physics: Vec, + physics_helicity_count: usize, + ) -> RusticolResult { + if plan.strategy() != RecurrenceStrategy::AllFlowUnion { + return Err(RusticolError::integrity( + "persisted recurrence helicity selector is not all-flow-union", + )); + } + dispatch.validate_for_plan(&plan)?; + if direct_helicity_to_physics.len() != plan.resolved_helicities().len() { + return Err(RusticolError::integrity( + "persisted selector helicity mapping does not cover its auxiliary plan", + )); + } + let mut resolved_helicity_by_physics = vec![DIRECT_NONE_U32; physics_helicity_count]; + for (resolved_helicity_id, physics_helicity_id) in + direct_helicity_to_physics.into_iter().enumerate() + { + let slot = resolved_helicity_by_physics + .get_mut(physics_helicity_id) + .ok_or_else(|| { + RusticolError::integrity( + "persisted selector helicity maps outside the public physics axis", + ) + })?; + if *slot != DIRECT_NONE_U32 { + return Err(RusticolError::integrity( + "persisted selector helicity mapping repeats a public helicity", + )); + } + *slot = u32::try_from(resolved_helicity_id).map_err(|_| { + RusticolError::artifact("persisted selector resolved helicity exceeds u32") + })?; + } + if resolved_helicity_by_physics.contains(&DIRECT_NONE_U32) { + return Err(RusticolError::integrity( + "persisted selector does not cover the complete public helicity axis", + )); + } + Ok(Self { + plan, + dispatch, + executor: PersistedHelicityFamilyExecutorV1::new(resolver), + resolved_helicity_by_physics: resolved_helicity_by_physics.into_boxed_slice(), + primary_color_view: None, + }) + } + + fn bind_primary_color_view( + &mut self, + contraction: &RecurrenceColorContraction, + ) -> RusticolResult<()> { + if self.primary_color_view.is_some() { + return Err(RusticolError::internal( + "persisted selector primary color view was bound twice", + )); + } + self.primary_color_view = Some(primary_local_color_destination_map( + contraction, + self.plan.amplitude_destinations(), + )?); + Ok(()) + } + + #[inline(always)] + fn resolved_helicity_id(&self, physics_helicity_id: usize) -> RusticolResult { + self.resolved_helicity_by_physics + .get(physics_helicity_id) + .copied() + .ok_or_else(|| { + RusticolError::integrity( + "persisted selector public helicity is outside its resolved map", + ) + }) + } + + fn clear(&mut self) -> RusticolResult<()> { + self.executor.clear_families() + } + + fn cache_census(&self) -> PersistedHelicityFamilyCacheCensusV1 { + self.executor.cache_census() + } + + fn active_family_inspection_census(&self) -> Option { + self.executor.active_family_inspection_census() + } +} + +fn primary_local_color_destination_map( + contraction: &RecurrenceColorContraction, + auxiliary_destinations: &[DirectAmplitudeDestinationDescriptor], +) -> RusticolResult { + if contraction.storage() == RecurrenceColorStorage::Expanded + && contraction.component_count() != 1 + { + return Err(RusticolError::compatibility( + "expanded persisted helicity color view must contain exactly one component", + )); + } + let component_count = contraction.component_count() as usize; + let local_group_count = match contraction.storage() { + RecurrenceColorStorage::Expanded => contraction.group_count() as usize, + RecurrenceColorStorage::Repeated | RecurrenceColorStorage::ConvolutionKernels => { + contraction.local_group_count() as usize + } + }; + if local_group_count == 0 + || component_count == 0 + || auxiliary_destinations.len() != local_group_count + { + return Err(RusticolError::integrity( + "persisted selector physical destinations disagree with the primary local color domain", + )); + } + + let active_owner_sectors = contraction + .owner_by_sector() + .iter() + .copied() + .enumerate() + .filter_map(|(sector, owner)| (owner as usize == sector).then_some(sector)) + .collect::>(); + if active_owner_sectors.len() != local_group_count { + return Err(RusticolError::integrity( + "primary local color groups do not match its canonical active owners", + )); + } + let mut dense_rank_by_sector = vec![DIRECT_NONE_U32; contraction.owner_by_sector().len()]; + for (rank, sector) in active_owner_sectors.into_iter().enumerate() { + dense_rank_by_sector[sector] = u32::try_from(rank) + .map_err(|_| RusticolError::artifact("primary color owner rank exceeds u32"))?; + } + + let mut auxiliary_destination_by_rank = vec![DIRECT_NONE_U32; local_group_count]; + for destination in auxiliary_destinations { + if destination.target_helicity_id_or_sentinel != DIRECT_NONE_U32 { + return Err(RusticolError::integrity( + "persisted selector auxiliary destination fixes a numerical helicity", + )); + } + let slot = auxiliary_destination_by_rank + .get_mut(destination.target_sector_id as usize) + .ok_or_else(|| { + RusticolError::integrity( + "persisted selector auxiliary destination has a non-dense physical sector", + ) + })?; + if *slot != DIRECT_NONE_U32 { + return Err(RusticolError::integrity( + "persisted selector repeats an auxiliary physical sector", + )); + } + *slot = destination.id; + } + if auxiliary_destination_by_rank.contains(&DIRECT_NONE_U32) { + return Err(RusticolError::integrity( + "persisted selector does not cover every dense auxiliary physical sector", + )); + } + + let ordered_groups = contraction.ordered_group_ids(); + let sectors = contraction.sector_by_group(); + let components = contraction.component_by_group(); + let destinations = contraction.destination_by_group(); + let mut auxiliary_destination_by_local_group = Vec::with_capacity(local_group_count); + let mut local_group_by_primary_destination = + vec![DIRECT_NONE_U32; contraction.destination_count() as usize]; + for local_group in 0..local_group_count { + let mut owner_sector = None; + for component in 0..component_count { + let ordered_index = local_group + .checked_mul(component_count) + .and_then(|base| base.checked_add(component)) + .ok_or_else(|| RusticolError::artifact("primary local color index overflows"))?; + let group_id = *ordered_groups + .get(ordered_index) + .ok_or_else(|| RusticolError::integrity("primary ordered color group is absent"))? + as usize; + let sector = *sectors.get(group_id).ok_or_else(|| { + RusticolError::integrity("primary color group has no physical sector") + })? as usize; + if components.get(group_id).copied() != Some(component as u32) + || contraction.owner_by_sector().get(sector).copied() != Some(sector as u32) + || owner_sector.is_some_and(|expected| expected != sector) + { + return Err(RusticolError::integrity( + "primary local color block mixes components or non-owner sectors", + )); + } + owner_sector = Some(sector); + let primary_destination = *destinations.get(group_id).ok_or_else(|| { + RusticolError::integrity("primary color group has no amplitude destination") + })? as usize; + let slot = local_group_by_primary_destination + .get_mut(primary_destination) + .ok_or_else(|| { + RusticolError::integrity( + "primary color destination is outside its authenticated domain", + ) + })?; + let local_group_u32 = u32::try_from(local_group) + .map_err(|_| RusticolError::artifact("primary local color group exceeds u32"))?; + if *slot != DIRECT_NONE_U32 && *slot != local_group_u32 { + return Err(RusticolError::integrity( + "primary color destination maps to multiple local groups", + )); + } + *slot = local_group_u32; + } + let owner_sector = owner_sector.expect("positive component count was validated"); + let dense_rank = *dense_rank_by_sector.get(owner_sector).ok_or_else(|| { + RusticolError::integrity("primary color owner has no dense canonical rank") + })?; + if dense_rank == DIRECT_NONE_U32 { + return Err(RusticolError::integrity( + "primary color local group references an inactive owner", + )); + } + auxiliary_destination_by_local_group + .push(auxiliary_destination_by_rank[dense_rank as usize]); + } + Ok(PrimaryLocalColorViewV1 { + auxiliary_destination_by_local_group: auxiliary_destination_by_local_group + .into_boxed_slice(), + local_group_by_primary_destination: local_group_by_primary_destination.into_boxed_slice(), + }) +} + +struct ContractedReplayRoute { + selector: DirectReplaySelectorPlan, + destination_copies: Vec<(u32, u32)>, +} + +// Boxing the contracted-color payload would add an indirection to every +// contracted recurrence tile; this enum is instantiated only once per runtime. +#[allow(clippy::large_enum_variant)] +enum RecurrenceNativeSelectors { + TopologyReplay { + replay_selectors: Vec, + direct_helicity_to_physics: Vec, + }, + AllFlowUnion { + helicity_selectors_by_physics: Vec>, + destination_by_public_flow: Vec, + }, + ContractedColorUnion { + contraction: RecurrenceColorContraction, + destination_physics_helicity: Vec, + replay_routes: Vec, + }, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum CompanionSingletonSelection { + StructuralZero, + Runnable { + physics_helicity_id: usize, + resolved_helicity_id: u32, + }, +} + +/// Borrowed LC selector and reduction state shared by topology replay and the +/// future compact on-the-fly lane. +/// +/// Constructing this view is allocation-free. It deliberately exposes only +/// the public axes and orbit weights needed after an amplitude is computed; +/// an on-the-fly runtime can therefore provide the same backing slices/maps +/// without materializing a dense [`ProcessPhysicsV1`]. +#[derive(Clone, Copy)] +pub(super) struct LcSelectorReductionView<'a> { + helicities: &'a [crate::Helicity], + color_components: &'a [crate::ColorComponent], + helicity_index_by_id: &'a BTreeMap, + color_index_by_id: &'a BTreeMap, + helicity_members_by_representative: &'a [Vec], +} + +impl<'a> LcSelectorReductionView<'a> { + pub(super) const fn from_parts( + helicities: &'a [crate::Helicity], + color_components: &'a [crate::ColorComponent], + helicity_index_by_id: &'a BTreeMap, + color_index_by_id: &'a BTreeMap, + helicity_members_by_representative: &'a [Vec], + ) -> Self { + Self { + helicities, + color_components, + helicity_index_by_id, + color_index_by_id, + helicity_members_by_representative, + } + } + + fn validate_selector_ids( + self, + selected_helicities: Option<&BTreeSet>, + selected_colors: Option<&BTreeSet>, + ) -> RusticolResult<()> { + if let Some(id) = selected_helicities.and_then(|ids| { + ids.iter() + .find(|id| !self.helicity_index_by_id.contains_key(*id)) + }) { + return Err(RusticolError::selector(format!( + "unknown resolved helicity id {id:?}" + ))); + } + if let Some(id) = selected_colors.and_then(|ids| { + ids.iter() + .find(|id| !self.color_index_by_id.contains_key(*id)) + }) { + return Err(RusticolError::selector(format!( + "unknown resolved color component id {id:?}" + ))); + } + Ok(()) + } + + fn selected_indices( + available: &BTreeMap, + ids: Option<&BTreeSet>, + kind: &str, + ) -> RusticolResult> { + let mut indices = if let Some(ids) = ids { + ids.iter() + .map(|id| { + available.get(id).copied().ok_or_else(|| { + RusticolError::selector(format!("unknown resolved {kind} id {id:?}")) + }) + }) + .collect::>>()? + } else { + (0..available.len()).collect() + }; + indices.sort_unstable(); + Ok(indices) + } + + fn selected_helicity_indices( + self, + ids: Option<&BTreeSet>, + ) -> RusticolResult> { + Self::selected_indices(self.helicity_index_by_id, ids, "helicity") + } + + fn selected_color_indices(self, ids: Option<&BTreeSet>) -> RusticolResult> { + Self::selected_indices(self.color_index_by_id, ids, "color component") + } + + fn retain_selected_color_indices( + self, + ids: &BTreeSet, + indices: &mut Vec, + ) -> RusticolResult<()> { + indices.clear(); + indices.reserve(ids.len()); + for id in ids { + indices.push(*self.color_index_by_id.get(id).ok_or_else(|| { + RusticolError::selector(format!("unknown resolved color component id {id:?}")) + })?); + } + indices.sort_unstable(); + Ok(()) + } + + #[inline(always)] + fn helicity(self, index: usize) -> &'a crate::Helicity { + &self.helicities[index] + } + + #[inline(always)] + fn color(self, index: usize) -> &'a crate::ColorComponent { + &self.color_components[index] + } + + #[inline(always)] + fn color_is_computed(self, index: usize) -> bool { + match self.color(index) { + crate::ColorComponent::LcFlow(flow) => flow.computed, + crate::ColorComponent::ContractedColor(_) => true, + } + } + + #[inline(always)] + fn helicity_orbit_members(self, representative: usize) -> &'a [usize] { + self.helicity_members_by_representative + .get(representative) + .map(Vec::as_slice) + .unwrap_or_default() + } + + #[inline(always)] + fn helicity_is_selected(self, selected: Option<&BTreeSet>, index: usize) -> bool { + selected.is_none_or(|ids| ids.contains(&self.helicity(index).id)) + } + + #[inline(always)] + fn helicity_orbit_weight( + self, + selected: Option<&BTreeSet>, + representative_index: usize, + ) -> f64 { + self.helicity_orbit_members(representative_index) + .iter() + .copied() + .filter(|index| self.helicity_is_selected(selected, *index)) + .map(|index| self.helicity(index).coefficient) + .sum() + } + + #[cfg(test)] + #[inline(always)] + fn color_is_selected(self, selected: Option<&BTreeSet>, index: usize) -> bool { + selected.is_none_or(|ids| ids.contains(self.color(index).id())) + } +} + +#[inline(always)] +fn visit_replay_color_indices( + color_count: usize, + selected: Option<&[usize]>, + mut visit: impl FnMut(usize) -> RusticolResult<()>, +) -> RusticolResult<()> { + if let Some(selected) = selected { + for &color_index in selected { + visit(color_index)?; + } + } else { + for color_index in 0..color_count { + visit(color_index)?; + } + } + Ok(()) +} + +impl PhysicsRuntime { + pub(super) fn lc_selector_reduction_view(&self) -> LcSelectorReductionView<'_> { + LcSelectorReductionView::from_parts( + &self.manifest.helicities, + &self.manifest.color_components, + &self.helicity_index_by_id, + &self.color_index_by_id, + &self.helicity_members_by_representative, + ) + } +} + +/// Apply the established LC diagonal reduction to one complex amplitude +/// stream. Both topology replay's split real/imaginary planes and the future +/// on-the-fly query-major output can provide their native storage through the +/// inlined accessor without copying or changing the reduction formula. +#[inline(always)] +pub(super) fn accumulate_lc_diagonal_amplitude( + point_count: usize, + weight: f64, + mut value: impl FnMut(usize) -> (f64, f64), + mut accumulate: impl FnMut(usize, f64), +) { + for point in 0..point_count { + let (real, imaginary) = value(point); + accumulate(point, weight * real.mul_add(real, imaginary * imaginary)); + } +} + +/// Initialize prepared complex parameters from authenticated defaults and the +/// mutable runtime projection shared by recurrence and on-the-fly execution. +/// Every fallible shape/index check completes before either destination plane +/// is modified. +pub(super) fn initialize_prepared_parameter_planes( + defaults: &[crate::EagerComplex64], + projection: &[PreparedParameterProjectionEntry], + runtime_values: &[f64], + destination_re: &mut [f64], + destination_im: &mut [f64], +) -> RusticolResult<()> { + if destination_re.len() != defaults.len() || destination_im.len() != defaults.len() { + return Err(RusticolError::integrity( + "prepared parameter workspace has the wrong size", + )); + } + for entry in projection { + if entry.prepared_slot >= defaults.len() + || entry.runtime_slot >= runtime_values.len() + || entry.component > 1 + { + return Err(RusticolError::integrity( + "runtime parameter projection is outside its prepared layout", + )); + } + } + for ((real, imaginary), default) in destination_re + .iter_mut() + .zip(destination_im.iter_mut()) + .zip(defaults) + { + *real = default.re; + *imaginary = default.im; + } + for entry in projection { + let value = runtime_values[entry.runtime_slot]; + if entry.component == 0 { + destination_re[entry.prepared_slot] = value; + } else { + destination_im[entry.prepared_slot] = value; + } + } + Ok(()) +} + +pub(super) fn projected_prepared_parameter_values( + defaults: &[crate::EagerComplex64], + projection: &[PreparedParameterProjectionEntry], + runtime_values: &[f64], +) -> RusticolResult> { + let mut real = vec![0.0; defaults.len()]; + let mut imaginary = vec![0.0; defaults.len()]; + initialize_prepared_parameter_planes( + defaults, + projection, + runtime_values, + &mut real, + &mut imaginary, + )?; + Ok(real.into_iter().zip(imaginary).collect()) +} + +/// Existing point-major resolved-output placement shared by recurrence and +/// on-the-fly LC execution. The selected axis order remains +/// `[point][helicity][color]` without requiring a persisted dense flow table. +#[derive(Clone, Copy, Debug)] +pub(super) struct LcResolvedOutputLayout { + point_count: usize, + helicity_count: usize, + color_count: usize, +} + +impl LcResolvedOutputLayout { + pub(super) fn new( + point_count: usize, + helicity_count: usize, + color_count: usize, + ) -> RusticolResult { + if point_count == 0 || helicity_count == 0 || color_count == 0 { + return Err(RusticolError::invalid_argument( + "resolved LC output shape is empty", + )); + } + let layout = Self { + point_count, + helicity_count, + color_count, + }; + layout.output_len()?; + Ok(layout) + } + + pub(super) fn output_len(self) -> RusticolResult { + self.point_count + .checked_mul(self.helicity_count) + .and_then(|count| count.checked_mul(self.color_count)) + .ok_or_else(|| RusticolError::invalid_argument("resolved LC output shape overflows")) + } + + #[inline(always)] + pub(super) fn index( + self, + point: usize, + helicity: usize, + color: usize, + ) -> RusticolResult { + if point >= self.point_count || helicity >= self.helicity_count || color >= self.color_count + { + return Err(RusticolError::integrity( + "resolved LC output coordinate is outside its selected axes", + )); + } + point + .checked_mul(self.helicity_count) + .and_then(|base| base.checked_add(helicity)) + .and_then(|base| base.checked_mul(self.color_count)) + .and_then(|base| base.checked_add(color)) + .ok_or_else(|| RusticolError::invalid_argument("resolved LC output index overflows")) + } +} + +impl RecurrenceNativeRuntime { + #[allow(clippy::too_many_arguments)] + #[cfg_attr(target_vendor = "apple", unsafe(link_section = "__TEXT,__rcl_load"))] + #[cfg_attr(target_vendor = "apple", inline(never))] + pub(super) fn new( + plan: DirectRecurrencePlan, + executors: DirectExecutorCatalog, + backend_owner: NativeRecurrenceDirectExecutorOwners, + parameter_defaults: Vec, + parameter_projection: Vec, + public_flow_ids: Vec, + direct_helicity_to_physics: Vec, + color_contraction: Option, + mut helicity_selector_companion: Option, + ) -> RusticolResult { + let parameter_count = usize::try_from(plan.parameter_value_count()) + .map_err(|_| RusticolError::artifact("recurrence parameter count exceeds usize"))?; + if parameter_defaults.len() != parameter_count { + return Err(RusticolError::integrity(format!( + "recurrence prepared defaults have length {}, expected {parameter_count}", + parameter_defaults.len() + ))); + } + for entry in ¶meter_projection { + if entry.prepared_slot >= parameter_count || entry.component > 1 { + return Err(RusticolError::artifact( + "recurrence parameter projection is outside the prepared layout", + )); + } + } + if direct_helicity_to_physics.len() != plan.resolved_helicities().len() { + return Err(RusticolError::integrity( + "recurrence resolved-helicity mapping does not cover the direct plan", + )); + } + let backend_name = backend_owner.summary().backend.clone(); + let strategy = plan.strategy(); + let has_color_contraction = color_contraction.is_some(); + if public_flow_ids.is_empty() && strategy != RecurrenceStrategy::ContractedColorUnion { + return Err(RusticolError::integrity( + "recurrence public color-flow mapping is empty", + )); + } + let scheduler = match strategy { + RecurrenceStrategy::TopologyReplay | RecurrenceStrategy::ContractedColorUnion => { + DirectRecurrenceExecutionRuntime::new(plan, executors, 4)? + } + RecurrenceStrategy::AllFlowUnion => { + let dispatch = backend_owner.union_source_dispatch()?; + DirectRecurrenceExecutionRuntime::new_with_union_source_dispatch( + plan, executors, 4, dispatch, + )? + } + }; + let point_tile_capacity = scheduler.point_tile_size() as usize; + let mut effective_point_tile_size = point_tile_capacity; + let mut color_transform_scratch_len = 0usize; + let mut symmetric_group_color_workspace = None; + if let Some(contraction) = color_contraction.as_ref() { + match contraction.runtime_reducer() { + Some(RuntimeColorContractionReducer::Walsh(_)) => { + color_transform_scratch_len = usize::try_from(contraction.local_group_count()) + .ok() + .and_then(|groups| groups.checked_mul(point_tile_capacity)) + .ok_or_else(|| { + RusticolError::artifact( + "recurrence factorized color workspace overflows usize", + ) + })?; + } + Some(RuntimeColorContractionReducer::SymmetricGroupFourier(reducer)) => { + (effective_point_tile_size, symmetric_group_color_workspace) = + lazy_symmetric_group_workspace_for_point_tile( + reducer, + point_tile_capacity, + )?; + } + None => {} + } + } + let selectors = match strategy { + RecurrenceStrategy::TopologyReplay => { + let replay_selectors = public_flow_ids + .into_iter() + .map(|public_flow_id| scheduler.prepare_replay_selector(public_flow_id)) + .collect::>>()?; + validate_replay_destination_helicity_mappings( + scheduler.plan(), + &replay_selectors, + &direct_helicity_to_physics, + )?; + RecurrenceNativeSelectors::TopologyReplay { + replay_selectors, + direct_helicity_to_physics, + } + } + RecurrenceStrategy::AllFlowUnion => { + let physics_helicity_count = direct_helicity_to_physics + .iter() + .copied() + .max() + .and_then(|value| value.checked_add(1)) + .ok_or_else(|| { + RusticolError::integrity( + "all-flow-union recurrence has no public helicities", + ) + })?; + let mut helicity_selectors_by_physics = vec![None; physics_helicity_count]; + for (direct_id, physics_id) in + direct_helicity_to_physics.iter().copied().enumerate() + { + let selector = scheduler.prepare_union_helicity_selector( + u32::try_from(direct_id).map_err(|_| { + RusticolError::artifact("recurrence direct helicity ID exceeds u32") + })?, + )?; + if helicity_selectors_by_physics[physics_id] + .replace(selector) + .is_some() + { + return Err(RusticolError::integrity( + "all-flow-union recurrence repeats a public helicity", + )); + } + } + if helicity_selectors_by_physics.iter().any(Option::is_none) { + return Err(RusticolError::integrity( + "all-flow-union recurrence does not cover the public helicity axis", + )); + } + let destination_by_public_flow = + union_destination_ids(scheduler.plan(), &public_flow_ids)?; + RecurrenceNativeSelectors::AllFlowUnion { + helicity_selectors_by_physics, + destination_by_public_flow, + } + } + RecurrenceStrategy::ContractedColorUnion => { + if !public_flow_ids.is_empty() { + return Err(RusticolError::integrity( + "contracted recurrence unexpectedly exposes public color flows", + )); + } + let contraction = color_contraction.ok_or_else(|| { + RusticolError::integrity( + "contracted recurrence has no color-contraction payload", + ) + })?; + let destination_physics_helicity = + contracted_destination_helicity_map(&contraction, &direct_helicity_to_physics)?; + let replay_routes = contracted_replay_routes(&scheduler, &contraction)?; + RecurrenceNativeSelectors::ContractedColorUnion { + contraction, + destination_physics_helicity, + replay_routes, + } + } + }; + if strategy != RecurrenceStrategy::ContractedColorUnion && has_color_contraction { + return Err(RusticolError::integrity( + "LC recurrence unexpectedly carries a color-contraction payload", + )); + } + if helicity_selector_companion.is_some() + && strategy != RecurrenceStrategy::ContractedColorUnion + { + return Err(RusticolError::integrity( + "recurrence helicity-selector companion requires contracted-color-union", + )); + } + if let Some(companion) = &mut helicity_selector_companion { + if companion.plan.parameter_value_count() != scheduler.plan().parameter_value_count() + || companion.plan.prepared_pack_digest() != scheduler.plan().prepared_pack_digest() + || companion.plan.direct_template_catalog_digest() + != scheduler.plan().direct_template_catalog_digest() + { + return Err(RusticolError::integrity( + "persisted selector prepared-parameter layout disagrees with the primary recurrence plan", + )); + } + let RecurrenceNativeSelectors::ContractedColorUnion { contraction, .. } = &selectors + else { + return Err(RusticolError::integrity( + "recurrence helicity-selector companion has no primary color contraction", + )); + }; + companion.bind_primary_color_view(contraction)?; + } + let external_source_count = usize::try_from(scheduler.plan().external_source_count()) + .map_err(|_| RusticolError::artifact("recurrence source count exceeds usize"))?; + scheduler + .point_tile_size() + .try_into() + .ok() + .and_then(|points: usize| points.checked_mul(external_source_count)) + .and_then(|values| values.checked_mul(4)) + .ok_or_else(|| { + RusticolError::artifact("recurrence external-momentum workspace overflows usize") + })?; + let mut external_momenta = Vec::new(); + ensure_external_momentum_workspace_capacity( + &mut external_momenta, + 1, + external_source_count, + effective_point_tile_size, + )?; + let contracted_replay_len = match &selectors { + RecurrenceNativeSelectors::ContractedColorUnion { + contraction, + replay_routes, + .. + } if !replay_routes.is_empty() => usize::try_from(contraction.destination_count()) + .ok() + .and_then(|destinations| { + usize::try_from(scheduler.point_tile_size()) + .ok() + .and_then(|points| destinations.checked_mul(points)) + }) + .ok_or_else(|| { + RusticolError::artifact("contracted replay amplitude workspace overflows usize") + })?, + _ => 0, + }; + + Ok(Self { + scheduler, + _backend_owner: backend_owner, + backend_name, + selectors, + parameter_defaults, + parameter_projection, + external_source_count, + effective_point_tile_size, + external_momenta, + contracted_replay_re: vec![0.0; contracted_replay_len], + contracted_replay_im: vec![0.0; contracted_replay_len], + color_transform_re: vec![0.0; color_transform_scratch_len], + color_transform_im: vec![0.0; color_transform_scratch_len], + symmetric_group_color_workspace, + replay_color_index_scratch: Vec::new(), + selected_replay_cache: BTreeMap::new(), + selected_replay_programs: BTreeMap::new(), + helicity_selector_companion, + }) + } + + pub(super) fn backend_name(&self) -> &str { + &self.backend_name + } + + pub(super) fn effective_point_tile_size(&self) -> usize { + self.effective_point_tile_size + } + + pub(super) fn clear_helicity_selector_companion(&mut self) -> RusticolResult<()> { + if let Some(companion) = &mut self.helicity_selector_companion { + companion.clear()?; + } + Ok(()) + } + + pub(super) fn helicity_selector_companion_state_census( + &self, + process_id: &str, + ) -> RusticolResult> { + let Some(companion) = &self.helicity_selector_companion else { + return Ok(None); + }; + Ok(Some(Self::persisted_helicity_selector_state_census( + process_id, + companion.cache_census(), + companion.active_family_inspection_census(), + ))) + } + + fn persisted_helicity_selector_state_census( + process_id: &str, + cache: PersistedHelicityFamilyCacheCensusV1, + active: Option, + ) -> serde_json::Value { + let active_family_union_census = active.map(|census| { + serde_json::json!({ + "basis": "shared-query-family-union-v1", + "scope": "active-family-union", + "query_count": census.query_count, + "union_unique_current_count": census.union_unique_current_count, + "union_unique_current_component_count": ( + census.union_unique_current_component_count + ), + "union_source_rows": census.union_source_rows, + "union_contribution_rows": census.union_contribution_rows, + "union_finalization_rows": census.union_finalization_rows, + "union_closure_rows": census.union_closure_rows, + "union_amplitude_destination_count": ( + census.union_amplitude_destination_count + ), + "union_source_executor_call_groups": ( + census.union_source_executor_call_groups + ), + "union_contribution_executor_call_groups": ( + census.union_contribution_executor_call_groups + ), + "union_finalization_executor_call_groups": ( + census.union_finalization_executor_call_groups + ), + "union_closure_executor_call_groups": ( + census.union_closure_executor_call_groups + ), + }) + }); + let retained_request_count = active.map_or(0, |census| census.query_count); + let retained_amplitude_destination_count = + active.map_or(0, |census| census.union_amplitude_destination_count); + let retained_executor_handle_count = cache.retained_family_count; + let semantic_executor_binding_count = + active.map_or(0, |census| census.semantic_executor_binding_count); + serde_json::json!({ + "kind": "rusticol-on-the-fly-runtime-state-census-v1", + "process_id": process_id, + "family_cache_policy": "last-family-only", + "family_cache_limit": 1, + "process_preparation_count": cache.retained_family_count, + "retained_family_count": cache.retained_family_count, + "pending_family_count": 0, + "retained_selection_count": cache.retained_family_count, + "retained_request_count": retained_request_count, + "retained_amplitude_destination_count": retained_amplitude_destination_count, + "retained_executor_handle_count": retained_executor_handle_count, + "retained_query_local_trace_count": 0, + "retained_embedded_lookup_key_count": 0, + "semantic_executor_binding_count": semantic_executor_binding_count, + "active_family_union_census": active_family_union_census, + }) + } + + pub(super) fn validate_global_selectors( + &self, + physics: &PhysicsRuntime, + selected_helicities: Option<&BTreeSet>, + selected_colors: Option<&BTreeSet>, + ) -> RusticolResult<()> { + self.validate_public_axis_lengths(physics)?; + if matches!( + &self.selectors, + RecurrenceNativeSelectors::TopologyReplay { .. } + ) { + physics + .lc_selector_reduction_view() + .validate_selector_ids(selected_helicities, selected_colors)?; + } else { + validate_recurrence_selector_ids(physics, selected_helicities, selected_colors)?; + } + if matches!( + &self.selectors, + RecurrenceNativeSelectors::ContractedColorUnion { .. } + ) { + reject_contracted_color_selector(selected_colors)?; + } + Ok(()) + } + + /// Classify the exact singleton selector served by the complete compact + /// companion. `Some(false)` is an authenticated structural zero and must + /// return without preparing either execution lane. + fn companion_singleton_selection( + &self, + physics: &PhysicsRuntime, + selected_helicities: Option<&BTreeSet>, + selected_colors: Option<&BTreeSet>, + ) -> RusticolResult> { + if self.helicity_selector_companion.is_none() + || !matches!( + &self.selectors, + RecurrenceNativeSelectors::ContractedColorUnion { .. } + ) + || selected_colors.is_some() + { + return Ok(None); + } + let Some(selected_helicities) = selected_helicities else { + return Ok(None); + }; + if selected_helicities.len() != 1 { + return Ok(None); + } + let selected_id = selected_helicities + .first() + .expect("singleton helicity selector disappeared"); + let helicity_index = physics + .helicity_index_by_id + .get(selected_id) + .copied() + .ok_or_else(|| { + RusticolError::selector(format!("unknown resolved helicity id {selected_id:?}")) + })?; + let helicity = &physics.manifest.helicities[helicity_index]; + if helicity.structural_zero || helicity.coefficient == 0.0 { + return Ok(Some(CompanionSingletonSelection::StructuralZero)); + } + let resolved_helicity_id = self + .helicity_selector_companion + .as_ref() + .expect("classified recurrence companion disappeared") + .resolved_helicity_id(helicity_index)?; + Ok(Some(CompanionSingletonSelection::Runnable { + physics_helicity_id: helicity_index, + resolved_helicity_id, + })) + } + + fn prepare_persisted_companion_parameters( + &mut self, + common: &ExecutionRuntime, + ) -> RusticolResult<()> { + self.prepare_parameters(common)?; + let companion = self + .helicity_selector_companion + .as_mut() + .ok_or_else(|| RusticolError::internal("persisted helicity companion disappeared"))?; + let (parameters_re, parameters_im) = self.scheduler.parameters_mut(); + companion + .executor + .set_parameter_planes(parameters_re, parameters_im) + } + + #[allow(clippy::too_many_arguments)] + fn run_persisted_companion_view_into_unprofiled( + &mut self, + physics: &PhysicsRuntime, + normalization_factor: f64, + batch: F64MomentumBatchView<'_>, + physics_helicity_id: usize, + resolved_helicity_id: u32, + output: &mut [f64], + ) -> RusticolResult<()> { + let mut tile_start = 0usize; + while tile_start < batch.point_count() { + let tile_stop = + (tile_start + self.effective_point_tile_size()).min(batch.point_count()); + let point_count = + self.flatten_external_tile_view::(batch.subview(tile_start, tile_stop)?)?; + self.ensure_primary_symmetric_group_color_workspace_for_capacity(point_count)?; + let input_len = self.external_tile_input_len(point_count)?; + let point_count_u32 = direct_point_count(point_count)?; + let Self { + selectors: RecurrenceNativeSelectors::ContractedColorUnion { contraction, .. }, + external_momenta, + color_transform_re, + color_transform_im, + symmetric_group_color_workspace, + helicity_selector_companion: Some(companion), + .. + } = self + else { + return Err(RusticolError::internal( + "persisted helicity companion lost its contracted recurrence lane", + )); + }; + let RecurrencePersistedHelicitySelectorRuntime { + plan, + dispatch, + executor, + primary_color_view: Some(color_view), + .. + } = companion + else { + return Err(RusticolError::integrity( + "persisted helicity companion has no primary local-color view", + )); + }; + executor.prepare(plan, dispatch, resolved_helicity_id, 4, point_count_u32)?; + let tile = executor + .execute_tile_unprofiled(&external_momenta[..input_len], point_count_u32)?; + contract_primary_local_color_tile( + tile, + contraction, + color_view, + physics, + physics_helicity_id, + normalization_factor, + color_transform_re, + color_transform_im, + symmetric_group_color_workspace.as_mut(), + |point, value| output[tile_start + point] += value, + )?; + tile_start = tile_stop; + } + Ok(()) + } + + #[allow(clippy::too_many_arguments)] + fn run_persisted_companion_profiled( + &mut self, + common: &ExecutionRuntime, + physics: &PhysicsRuntime, + batch: &[Vec<[f64; 4]>], + physics_helicity_id: usize, + resolved_helicity_id: u32, + mut accumulate: impl FnMut(usize, f64), + ) -> RusticolResult { + let total_started = Instant::now(); + let parameter_started = Instant::now(); + self.prepare_persisted_companion_parameters(common)?; + let parameter_setup = parameter_started.elapsed(); + let mut input_setup = Duration::ZERO; + let mut execution = Duration::ZERO; + let mut reduction = Duration::ZERO; + let mut report = persisted_execution_report_aggregate(); + let mut momentum_scalar_values = 0_u64; + let mut tile_start = 0usize; + while tile_start < batch.len() { + let tile_stop = (tile_start + self.effective_point_tile_size()).min(batch.len()); + let input_started = Instant::now(); + let point_count = self.flatten_external_tile::(&batch[tile_start..tile_stop])?; + input_setup += input_started.elapsed(); + self.ensure_primary_symmetric_group_color_workspace_for_capacity(point_count)?; + let input_len = self.external_tile_input_len(point_count)?; + let point_count_u32 = direct_point_count(point_count)?; + let Self { + selectors: RecurrenceNativeSelectors::ContractedColorUnion { contraction, .. }, + external_momenta, + color_transform_re, + color_transform_im, + symmetric_group_color_workspace, + helicity_selector_companion: Some(companion), + .. + } = self + else { + return Err(RusticolError::internal( + "persisted helicity companion lost its contracted recurrence lane", + )); + }; + let RecurrencePersistedHelicitySelectorRuntime { + plan, + dispatch, + executor, + primary_color_view: Some(color_view), + .. + } = companion + else { + return Err(RusticolError::integrity( + "persisted helicity companion has no primary local-color view", + )); + }; + let execution_started = Instant::now(); + executor.prepare(plan, dispatch, resolved_helicity_id, 4, point_count_u32)?; + let (tile, tile_report) = + executor.execute_tile_profiled(&external_momenta[..input_len], point_count_u32)?; + execution += execution_started.elapsed(); + add_persisted_execution_report(&mut report, tile_report); + momentum_scalar_values = momentum_scalar_values.saturating_add( + (plan.momentum_forms().len() as u64) + .saturating_mul(4) + .saturating_mul(point_count as u64), + ); + let reduction_started = Instant::now(); + contract_primary_local_color_tile( + tile, + contraction, + color_view, + physics, + physics_helicity_id, + common.normalization_factor, + color_transform_re, + color_transform_im, + symmetric_group_color_workspace.as_mut(), + |point, value| accumulate(tile_start + point, value), + )?; + reduction += reduction_started.elapsed(); + tile_start = tile_stop; + } + Ok(persisted_companion_profile( + total_started.elapsed(), + parameter_setup, + input_setup, + execution, + reduction, + momentum_scalar_values, + report, + )) + } + + pub(super) fn run_f64( + &mut self, + common: &mut ExecutionRuntime, + batch: &[Vec<[f64; 4]>], + ) -> RusticolResult<(Vec, RuntimeProfile)> { + self.run_f64_with_global_selectors(common, batch, None, None) + } + + /// Clock-free totals path used by the native f64 ABI. + /// + /// Momentum input and result storage remain borrowed throughout this + /// call. Selector membership is tested against the authenticated public + /// axes directly so a warmed evaluation does not allocate temporary + /// index vectors. + pub(super) fn run_f64_view_into_unprofiled( + &mut self, + common: &mut ExecutionRuntime, + batch: F64MomentumBatchView<'_>, + selected_helicities: Option<&BTreeSet>, + selected_colors: Option<&BTreeSet>, + output: &mut [f64], + ) -> RusticolResult<()> { + if output.len() != batch.point_count() { + return Err(RusticolError::invalid_argument(format!( + "recurrence output has length {}, expected {}", + output.len(), + batch.point_count() + ))); + } + if batch.external_count() != self.external_source_count { + return Err(RusticolError::invalid_argument(format!( + "recurrence input has {} external momenta, expected {}", + batch.external_count(), + self.external_source_count + ))); + } + let physics = common.physics.as_ref().ok_or_else(|| { + RusticolError::artifact("recurrence execution requires physics metadata") + })?; + self.validate_global_selectors(physics, selected_helicities, selected_colors)?; + output.fill(0.0); + if selected_helicities.is_some_and(BTreeSet::is_empty) { + return Ok(()); + } + if let Some(selection) = + self.companion_singleton_selection(physics, selected_helicities, selected_colors)? + { + match selection { + CompanionSingletonSelection::StructuralZero => return Ok(()), + CompanionSingletonSelection::Runnable { + physics_helicity_id, + resolved_helicity_id, + } => { + self.prepare_persisted_companion_parameters(common)?; + return self.run_persisted_companion_view_into_unprofiled( + physics, + common.normalization_factor, + batch, + physics_helicity_id, + resolved_helicity_id, + output, + ); + } + } + } + self.prepare_parameters(common)?; + + match &self.selectors { + RecurrenceNativeSelectors::TopologyReplay { .. } => self + .run_replay_view_into_unprofiled( + physics.lc_selector_reduction_view(), + common.normalization_factor, + batch, + selected_helicities, + selected_colors, + output, + ), + RecurrenceNativeSelectors::AllFlowUnion { .. } => self.run_union_view_into_unprofiled( + physics, + common.normalization_factor, + batch, + selected_helicities, + selected_colors, + output, + ), + RecurrenceNativeSelectors::ContractedColorUnion { .. } => self + .run_contracted_view_into_unprofiled( + physics, + common.normalization_factor, + batch, + selected_helicities, + selected_colors, + output, + ), + } + } + + fn run_replay_view_into_unprofiled( + &mut self, + reduction: LcSelectorReductionView<'_>, + normalization_factor: f64, + batch: F64MomentumBatchView<'_>, + selected_helicities: Option<&BTreeSet>, + selected_colors: Option<&BTreeSet>, + output: &mut [f64], + ) -> RusticolResult<()> { + let Some(selected_colors) = selected_colors else { + return visit_replay_color_indices( + reduction.color_components.len(), + None, + |color_index| { + self.run_replay_color_view_into_unprofiled( + reduction, + normalization_factor, + batch, + selected_helicities, + output, + color_index, + ) + }, + ); + }; + + let mut color_indices = std::mem::take(&mut self.replay_color_index_scratch); + let result = (|| { + reduction.retain_selected_color_indices(selected_colors, &mut color_indices)?; + visit_replay_color_indices( + reduction.color_components.len(), + Some(&color_indices), + |color_index| { + self.run_replay_color_view_into_unprofiled( + reduction, + normalization_factor, + batch, + selected_helicities, + output, + color_index, + ) + }, + ) + })(); + self.replay_color_index_scratch = color_indices; + result + } + + /// Restrict work only when the caller restricts the physical helicity + /// axis. The original helicity-summed selectors remain untouched. Plans + /// are prepared once per requested subset/flow, outside the point loop, + /// and retained across selector changes and intervening summed calls. + fn selected_replay_color_plan( + &mut self, + reduction: LcSelectorReductionView<'_>, + selected_helicities: Option<&BTreeSet>, + color_index: usize, + ) -> RusticolResult>> { + let Some(selected_helicities) = selected_helicities else { + return Ok(None); + }; + if selected_helicities.len() == reduction.helicity_index_by_id.len() { + // Selector IDs were validated at the public boundary. Spelling + // out the complete axis is the same work as omitting it. + return Ok(None); + } + if let Some(plan) = self + .selected_replay_cache + .get(selected_helicities) + .and_then(|plans| plans.get(&color_index)) + { + return Ok(Some(Arc::clone(plan))); + } + let RecurrenceNativeSelectors::TopologyReplay { + replay_selectors, + direct_helicity_to_physics, + } = &self.selectors + else { + return Err(RusticolError::internal( + "selected replay plan requires topology replay", + )); + }; + let selector = replay_selectors.get(color_index).ok_or_else(|| { + RusticolError::integrity("recurrence replay selector is outside the public color axis") + })?; + let destinations = selected_replay_destination_ids( + self.scheduler.plan().amplitude_destinations(), + selector.representative_flow_id(), + selector.helicity_map(), + direct_helicity_to_physics, + reduction, + selected_helicities, + )?; + let program_key = (selector.representative_flow_id(), destinations); + // Different public flows and physical aliases can select the same + // representative currents. Share their dependency program instead + // of retaining another copy for every flow/permutation. + let selected = if let Some(program) = self.selected_replay_programs.get(&program_key) { + Arc::new( + self.scheduler + .prepare_replay_selector_with_destination_program(selector, program)?, + ) + } else { + let selected = Arc::new( + self.scheduler + .prepare_replay_selector_for_destinations(selector, &program_key.1)?, + ); + self.selected_replay_programs + .insert(program_key, Arc::clone(&selected)); + selected + }; + self.selected_replay_cache + .entry(selected_helicities.clone()) + .or_default() + .insert(color_index, Arc::clone(&selected)); + Ok(Some(selected)) + } + + #[inline(always)] + #[allow(clippy::too_many_arguments)] + fn run_replay_color_view_into_unprofiled( + &mut self, + reduction: LcSelectorReductionView<'_>, + normalization_factor: f64, + batch: F64MomentumBatchView<'_>, + selected_helicities: Option<&BTreeSet>, + output: &mut [f64], + color_index: usize, + ) -> RusticolResult<()> { + if !reduction.color_is_computed(color_index) { + return Ok(()); + } + let color_weight = reduction.color(color_index).coefficient(); + let selected_replay = + self.selected_replay_color_plan(reduction, selected_helicities, color_index)?; + + let mut tile_start = 0usize; + while tile_start < batch.point_count() { + let tile_stop = + (tile_start + self.effective_point_tile_size()).min(batch.point_count()); + let point_count = + self.flatten_external_tile_view::(batch.subview(tile_start, tile_stop)?)?; + let input_len = self.external_tile_input_len(point_count)?; + let (replay_selector, direct_helicity_to_physics) = match &self.selectors { + RecurrenceNativeSelectors::TopologyReplay { + replay_selectors, + direct_helicity_to_physics, + } => ( + replay_selectors.get(color_index).ok_or_else(|| { + RusticolError::integrity( + "recurrence replay selector is outside the public color axis", + ) + })?, + direct_helicity_to_physics, + ), + RecurrenceNativeSelectors::AllFlowUnion { .. } => unreachable!(), + RecurrenceNativeSelectors::ContractedColorUnion { .. } => unreachable!(), + }; + let replay_selector = selected_replay.as_deref().unwrap_or(replay_selector); + let direct_output = self + .scheduler + .execute_replay_tile_from_external_planes::( + replay_selector, + direct_point_count(point_count)?, + &self.external_momenta[..input_len], + )?; + + for amplitude in direct_output.selected_amplitudes() { + let amplitude = amplitude?; + let helicity_index = replay_destination_physics_helicity( + replay_selector.helicity_map(), + direct_helicity_to_physics, + amplitude.destination.target_helicity_id_or_sentinel, + )?; + let helicity = reduction.helicity(helicity_index); + if !helicity.computed || helicity.structural_zero || helicity.coefficient == 0.0 { + continue; + } + let helicity_weight = + reduction.helicity_orbit_weight(selected_helicities, helicity_index); + if helicity_weight == 0.0 { + continue; + } + let weight = helicity_weight * color_weight * normalization_factor; + accumulate_lc_diagonal_amplitude( + point_count, + weight, + |point| (amplitude.real[point], amplitude.imaginary[point]), + |point, value| output[tile_start + point] += value, + ); + } + tile_start = tile_stop; + } + Ok(()) + } + + fn run_union_view_into_unprofiled( + &mut self, + physics: &PhysicsRuntime, + normalization_factor: f64, + batch: F64MomentumBatchView<'_>, + selected_helicities: Option<&BTreeSet>, + selected_colors: Option<&BTreeSet>, + output: &mut [f64], + ) -> RusticolResult<()> { + for helicity_index in 0..physics.manifest.helicities.len() { + if !recurrence_helicity_is_selected(physics, selected_helicities, helicity_index) { + continue; + } + let helicity = &physics.manifest.helicities[helicity_index]; + if !helicity.computed || helicity.structural_zero || helicity.coefficient == 0.0 { + continue; + } + let selector = self.union_helicity_selector(helicity_index)?; + let mut tile_start = 0usize; + while tile_start < batch.point_count() { + let tile_stop = + (tile_start + self.effective_point_tile_size()).min(batch.point_count()); + let point_count = + self.flatten_external_tile_view::(batch.subview(tile_start, tile_stop)?)?; + let input_len = self.external_tile_input_len(point_count)?; + let destination_by_public_flow = match &self.selectors { + RecurrenceNativeSelectors::AllFlowUnion { + destination_by_public_flow, + .. + } => destination_by_public_flow, + RecurrenceNativeSelectors::TopologyReplay { .. } => unreachable!(), + RecurrenceNativeSelectors::ContractedColorUnion { .. } => unreachable!(), + }; + let direct_output = self + .scheduler + .execute_union_tile_from_external_planes::( + &selector, + direct_point_count(point_count)?, + &self.external_momenta[..input_len], + )?; + + for color_index in 0..physics.manifest.color_components.len() { + if !physics.color_is_computed(color_index) + || !recurrence_color_is_selected(physics, selected_colors, color_index) + { + continue; + } + let destination_id = + *destination_by_public_flow.get(color_index).ok_or_else(|| { + RusticolError::integrity( + "all-flow-union color is outside retained public coverage", + ) + })?; + let values_re = + direct_output + .destination_re(destination_id) + .ok_or_else(|| { + RusticolError::integrity( + "all-flow-union amplitude destination is absent", + ) + })?; + let values_im = + direct_output + .destination_im(destination_id) + .ok_or_else(|| { + RusticolError::integrity( + "all-flow-union amplitude destination is absent", + ) + })?; + let color_weight = physics.manifest.color_components[color_index].coefficient(); + let weight = helicity.coefficient * color_weight * normalization_factor; + for point in 0..point_count { + output[tile_start + point] += weight + * values_re[point] + .mul_add(values_re[point], values_im[point] * values_im[point]); + } + } + tile_start = tile_stop; + } + } + Ok(()) + } + + fn run_contracted_view_into_unprofiled( + &mut self, + physics: &PhysicsRuntime, + normalization_factor: f64, + batch: F64MomentumBatchView<'_>, + selected_helicities: Option<&BTreeSet>, + selected_colors: Option<&BTreeSet>, + output: &mut [f64], + ) -> RusticolResult<()> { + reject_contracted_color_selector(selected_colors)?; + let mut tile_start = 0usize; + while tile_start < batch.point_count() { + let tile_stop = + (tile_start + self.effective_point_tile_size()).min(batch.point_count()); + let point_count = + self.flatten_external_tile_view::(batch.subview(tile_start, tile_stop)?)?; + let input_len = self.external_tile_input_len(point_count)?; + self.execute_and_contract_contracted_tile( + point_count, + input_len, + physics, + selected_helicities, + normalization_factor, + false, + |point, _helicity, value| output[tile_start + point] += value, + )?; + tile_start = tile_stop; + } + Ok(()) + } + + pub(super) fn run_f64_with_global_selectors( + &mut self, + common: &mut ExecutionRuntime, + batch: &[Vec<[f64; 4]>], + selected_helicities: Option<&BTreeSet>, + selected_colors: Option<&BTreeSet>, + ) -> RusticolResult<(Vec, RuntimeProfile)> { + match &self.selectors { + RecurrenceNativeSelectors::AllFlowUnion { .. } => { + return self.run_union_f64_with_global_selectors( + common, + batch, + selected_helicities, + selected_colors, + ); + } + RecurrenceNativeSelectors::ContractedColorUnion { .. } => { + return self.run_contracted_f64_with_global_selectors( + common, + batch, + selected_helicities, + selected_colors, + ); + } + RecurrenceNativeSelectors::TopologyReplay { .. } => {} + } + if batch.is_empty() { + return Err(RusticolError::invalid_argument( + "recurrence evaluation requires at least one point", + )); + } + let total_started = Instant::now(); + let physics = common.physics.clone().ok_or_else(|| { + RusticolError::artifact("recurrence execution requires physics metadata") + })?; + let reduction_view = physics.lc_selector_reduction_view(); + let helicity_indices = reduction_view.selected_helicity_indices(selected_helicities)?; + let color_indices = reduction_view.selected_color_indices(selected_colors)?; + self.validate_public_axes(&physics, &helicity_indices, &color_indices)?; + + let mut values = vec![0.0; batch.len()]; + let profile_before = DirectProfileSnapshot::capture(&self.scheduler, &self._backend_owner); + let parameter_started = Instant::now(); + self.prepare_parameters(common)?; + let parameter_setup = parameter_started.elapsed(); + let mut external_momentum_flatten = Duration::ZERO; + let mut reduction = Duration::ZERO; + + for color_index in color_indices { + if !reduction_view.color_is_computed(color_index) { + continue; + } + let color_weight = reduction_view.color(color_index).coefficient(); + let selected_replay = + self.selected_replay_color_plan(reduction_view, selected_helicities, color_index)?; + let mut tile_start = 0usize; + while tile_start < batch.len() { + let tile_stop = (tile_start + self.effective_point_tile_size()).min(batch.len()); + let flatten_started = Instant::now(); + let point_count = + self.flatten_external_tile::(&batch[tile_start..tile_stop])?; + external_momentum_flatten += flatten_started.elapsed(); + let input_len = point_count + .checked_mul(self.external_source_count) + .and_then(|values| values.checked_mul(4)) + .ok_or_else(|| { + RusticolError::invalid_argument( + "recurrence external-momentum tile length overflows", + ) + })?; + let (replay_selector, direct_helicity_to_physics) = match &self.selectors { + RecurrenceNativeSelectors::TopologyReplay { + replay_selectors, + direct_helicity_to_physics, + } => ( + replay_selectors.get(color_index).ok_or_else(|| { + RusticolError::integrity( + "recurrence replay selector is outside the public color axis", + ) + })?, + direct_helicity_to_physics, + ), + RecurrenceNativeSelectors::AllFlowUnion { .. } => unreachable!(), + RecurrenceNativeSelectors::ContractedColorUnion { .. } => unreachable!(), + }; + let replay_selector = selected_replay.as_deref().unwrap_or(replay_selector); + let output = self + .scheduler + .execute_replay_tile_from_external_planes::( + replay_selector, + u32::try_from(point_count).map_err(|_| { + RusticolError::invalid_argument( + "recurrence point tile exceeds the native u32 ABI", + ) + })?, + &self.external_momenta[..input_len], + )?; + + let reduction_started = Instant::now(); + for amplitude in output.selected_amplitudes() { + let amplitude = amplitude?; + let helicity_index = replay_destination_physics_helicity( + replay_selector.helicity_map(), + direct_helicity_to_physics, + amplitude.destination.target_helicity_id_or_sentinel, + )?; + let helicity = reduction_view.helicity(helicity_index); + if !helicity.computed || helicity.structural_zero || helicity.coefficient == 0.0 + { + continue; + } + let helicity_weight = + reduction_view.helicity_orbit_weight(selected_helicities, helicity_index); + if helicity_weight == 0.0 { + continue; + } + let weight = helicity_weight * color_weight * common.normalization_factor; + accumulate_lc_diagonal_amplitude( + point_count, + weight, + |point| (amplitude.real[point], amplitude.imaginary[point]), + |point, value| values[tile_start + point] += value, + ); + } + reduction += reduction_started.elapsed(); + tile_start = tile_stop; + } + } + + Ok(( + values, + direct_profile( + total_started.elapsed(), + parameter_setup, + external_momentum_flatten, + reduction, + profile_before, + DirectProfileSnapshot::capture(&self.scheduler, &self._backend_owner), + ), + )) + } + + pub(super) fn run_resolved_f64( + &mut self, + common: &mut ExecutionRuntime, + batch: &[Vec<[f64; 4]>], + selected_helicities: Option<&BTreeSet>, + selected_colors: Option<&BTreeSet>, + ) -> RusticolResult<(ResolvedValues, RuntimeProfile)> { + match &self.selectors { + RecurrenceNativeSelectors::AllFlowUnion { .. } => { + return self.run_union_resolved_f64( + common, + batch, + selected_helicities, + selected_colors, + ); + } + RecurrenceNativeSelectors::ContractedColorUnion { .. } => { + return self.run_contracted_resolved_f64( + common, + batch, + selected_helicities, + selected_colors, + ); + } + RecurrenceNativeSelectors::TopologyReplay { .. } => {} + } + if batch.is_empty() { + return Err(RusticolError::invalid_argument( + "resolved recurrence evaluation requires at least one point", + )); + } + let total_started = Instant::now(); + let physics = common.physics.clone().ok_or_else(|| { + RusticolError::artifact("resolved recurrence execution requires physics metadata") + })?; + let reduction_view = physics.lc_selector_reduction_view(); + let helicity_indices = reduction_view.selected_helicity_indices(selected_helicities)?; + let color_indices = reduction_view.selected_color_indices(selected_colors)?; + self.validate_public_axes(&physics, &helicity_indices, &color_indices)?; + let output_layout = + LcResolvedOutputLayout::new(batch.len(), helicity_indices.len(), color_indices.len())?; + let mut values = vec![0.0; output_layout.output_len()?]; + let mut helicity_position = vec![None; reduction_view.helicities.len()]; + for (position, index) in helicity_indices.iter().copied().enumerate() { + helicity_position[index] = Some(position); + } + + let profile_before = DirectProfileSnapshot::capture(&self.scheduler, &self._backend_owner); + let parameter_started = Instant::now(); + self.prepare_parameters(common)?; + let parameter_setup = parameter_started.elapsed(); + let mut external_momentum_flatten = Duration::ZERO; + let mut reduction = Duration::ZERO; + + for (color_position, color_index) in color_indices.iter().copied().enumerate() { + if !reduction_view.color_is_computed(color_index) { + continue; + } + let color_weight = reduction_view.color(color_index).coefficient(); + let selected_replay = + self.selected_replay_color_plan(reduction_view, selected_helicities, color_index)?; + let mut tile_start = 0usize; + while tile_start < batch.len() { + let tile_stop = (tile_start + self.effective_point_tile_size()).min(batch.len()); + let flatten_started = Instant::now(); + let point_count = + self.flatten_external_tile::(&batch[tile_start..tile_stop])?; + external_momentum_flatten += flatten_started.elapsed(); + let input_len = point_count + .checked_mul(self.external_source_count) + .and_then(|count| count.checked_mul(4)) + .ok_or_else(|| { + RusticolError::invalid_argument( + "recurrence external-momentum tile length overflows", + ) + })?; + let (replay_selector, direct_helicity_to_physics) = match &self.selectors { + RecurrenceNativeSelectors::TopologyReplay { + replay_selectors, + direct_helicity_to_physics, + } => ( + replay_selectors.get(color_index).ok_or_else(|| { + RusticolError::integrity( + "recurrence replay selector is outside the public color axis", + ) + })?, + direct_helicity_to_physics, + ), + RecurrenceNativeSelectors::AllFlowUnion { .. } => unreachable!(), + RecurrenceNativeSelectors::ContractedColorUnion { .. } => unreachable!(), + }; + let replay_selector = selected_replay.as_deref().unwrap_or(replay_selector); + let output = self + .scheduler + .execute_replay_tile_from_external_planes::( + replay_selector, + u32::try_from(point_count).map_err(|_| { + RusticolError::invalid_argument( + "recurrence point tile exceeds the native u32 ABI", + ) + })?, + &self.external_momenta[..input_len], + )?; + + let reduction_started = Instant::now(); + for amplitude in output.selected_amplitudes() { + let amplitude = amplitude?; + let helicity_index = replay_destination_physics_helicity( + replay_selector.helicity_map(), + direct_helicity_to_physics, + amplitude.destination.target_helicity_id_or_sentinel, + )?; + let helicity = reduction_view.helicity(helicity_index); + if !helicity.computed || helicity.structural_zero || helicity.coefficient == 0.0 + { + continue; + } + for physical_helicity in reduction_view.helicity_orbit_members(helicity_index) { + let Some(helicity_position) = helicity_position[*physical_helicity] else { + continue; + }; + let weight = reduction_view.helicity(*physical_helicity).coefficient + * color_weight + * common.normalization_factor; + accumulate_lc_diagonal_amplitude( + point_count, + weight, + |point| (amplitude.real[point], amplitude.imaginary[point]), + |point, value| { + let target = output_layout + .index(tile_start + point, helicity_position, color_position) + .expect("validated resolved LC coordinate"); + values[target] += value; + }, + ); + } + } + reduction += reduction_started.elapsed(); + tile_start = tile_stop; + } + } + + Ok(( + ResolvedValues { + values, + point_count: batch.len(), + helicity_indices, + color_indices, + }, + direct_profile( + total_started.elapsed(), + parameter_setup, + external_momentum_flatten, + reduction, + profile_before, + DirectProfileSnapshot::capture(&self.scheduler, &self._backend_owner), + ), + )) + } + + fn run_contracted_f64_with_global_selectors( + &mut self, + common: &mut ExecutionRuntime, + batch: &[Vec<[f64; 4]>], + selected_helicities: Option<&BTreeSet>, + selected_colors: Option<&BTreeSet>, + ) -> RusticolResult<(Vec, RuntimeProfile)> { + if batch.is_empty() { + return Err(RusticolError::invalid_argument( + "recurrence evaluation requires at least one point", + )); + } + reject_contracted_color_selector(selected_colors)?; + let total_started = Instant::now(); + let physics = common.physics.clone().ok_or_else(|| { + RusticolError::artifact("recurrence execution requires physics metadata") + })?; + let helicity_indices = physics.selected_helicity_indices(selected_helicities)?; + self.validate_public_axes(&physics, &helicity_indices, &[0])?; + let mut values = vec![0.0; batch.len()]; + if selected_helicities.is_some_and(BTreeSet::is_empty) { + return Ok((values, RuntimeProfile::default())); + } + if let Some(selection) = + self.companion_singleton_selection(&physics, selected_helicities, selected_colors)? + { + return match selection { + CompanionSingletonSelection::StructuralZero => { + Ok((values, RuntimeProfile::default())) + } + CompanionSingletonSelection::Runnable { + physics_helicity_id, + resolved_helicity_id, + } => { + let profile = self.run_persisted_companion_profiled( + common, + &physics, + batch, + physics_helicity_id, + resolved_helicity_id, + |point, value| values[point] += value, + )?; + Ok((values, profile)) + } + }; + } + let profile_before = DirectProfileSnapshot::capture(&self.scheduler, &self._backend_owner); + let parameter_started = Instant::now(); + self.prepare_parameters(common)?; + let parameter_setup = parameter_started.elapsed(); + let mut external_momentum_flatten = Duration::ZERO; + let mut reduction = Duration::ZERO; + + let mut tile_start = 0usize; + while tile_start < batch.len() { + let tile_stop = (tile_start + self.effective_point_tile_size()).min(batch.len()); + let flatten_started = Instant::now(); + let point_count = self.flatten_external_tile::(&batch[tile_start..tile_stop])?; + external_momentum_flatten += flatten_started.elapsed(); + let input_len = self.external_tile_input_len(point_count)?; + reduction += self.execute_and_contract_contracted_tile( + point_count, + input_len, + &physics, + selected_helicities, + common.normalization_factor, + true, + |point, _helicity, value| values[tile_start + point] += value, + )?; + tile_start = tile_stop; + } + + Ok(( + values, + direct_profile( + total_started.elapsed(), + parameter_setup, + external_momentum_flatten, + reduction, + profile_before, + DirectProfileSnapshot::capture(&self.scheduler, &self._backend_owner), + ), + )) + } + + fn run_contracted_resolved_f64( + &mut self, + common: &mut ExecutionRuntime, + batch: &[Vec<[f64; 4]>], + selected_helicities: Option<&BTreeSet>, + selected_colors: Option<&BTreeSet>, + ) -> RusticolResult<(ResolvedValues, RuntimeProfile)> { + if batch.is_empty() { + return Err(RusticolError::invalid_argument( + "resolved recurrence evaluation requires at least one point", + )); + } + reject_contracted_color_selector(selected_colors)?; + let total_started = Instant::now(); + let physics = common.physics.clone().ok_or_else(|| { + RusticolError::artifact("resolved recurrence execution requires physics metadata") + })?; + let helicity_indices = physics.selected_helicity_indices(selected_helicities)?; + self.validate_public_axes(&physics, &helicity_indices, &[0])?; + let mut helicity_position = vec![None; physics.manifest.helicities.len()]; + for (position, index) in helicity_indices.iter().copied().enumerate() { + helicity_position[index] = Some(position); + } + let component_count = helicity_indices.len(); + let mut values = vec![ + 0.0; + batch.len().checked_mul(component_count).ok_or_else(|| { + RusticolError::invalid_argument("recurrence resolved output overflows") + })? + ]; + if selected_helicities.is_some_and(BTreeSet::is_empty) { + return Ok(( + ResolvedValues { + values, + point_count: batch.len(), + helicity_indices, + color_indices: vec![0], + }, + RuntimeProfile::default(), + )); + } + if let Some(selection) = + self.companion_singleton_selection(&physics, selected_helicities, selected_colors)? + { + let profile = match selection { + CompanionSingletonSelection::StructuralZero => RuntimeProfile::default(), + CompanionSingletonSelection::Runnable { + physics_helicity_id, + resolved_helicity_id, + } => { + let position = helicity_position[physics_helicity_id] + .expect("singleton persisted helicity has no resolved output position"); + self.run_persisted_companion_profiled( + common, + &physics, + batch, + physics_helicity_id, + resolved_helicity_id, + |point, value| { + values[point * component_count + position] += value; + }, + )? + } + }; + return Ok(( + ResolvedValues { + values, + point_count: batch.len(), + helicity_indices, + color_indices: vec![0], + }, + profile, + )); + } + let profile_before = DirectProfileSnapshot::capture(&self.scheduler, &self._backend_owner); + let parameter_started = Instant::now(); + self.prepare_parameters(common)?; + let parameter_setup = parameter_started.elapsed(); + let mut external_momentum_flatten = Duration::ZERO; + let mut reduction = Duration::ZERO; + + let mut tile_start = 0usize; + while tile_start < batch.len() { + let tile_stop = (tile_start + self.effective_point_tile_size()).min(batch.len()); + let flatten_started = Instant::now(); + let point_count = self.flatten_external_tile::(&batch[tile_start..tile_stop])?; + external_momentum_flatten += flatten_started.elapsed(); + let input_len = self.external_tile_input_len(point_count)?; + reduction += self.execute_and_contract_contracted_tile( + point_count, + input_len, + &physics, + selected_helicities, + common.normalization_factor, + true, + |point, helicity, value| { + let position = helicity_position[helicity] + .expect("selected contracted helicity has a result position"); + values[(tile_start + point) * component_count + position] += value; + }, + )?; + tile_start = tile_stop; + } + + Ok(( + ResolvedValues { + values, + point_count: batch.len(), + helicity_indices, + color_indices: vec![0], + }, + direct_profile( + total_started.elapsed(), + parameter_setup, + external_momentum_flatten, + reduction, + profile_before, + DirectProfileSnapshot::capture(&self.scheduler, &self._backend_owner), + ), + )) + } + + fn run_union_f64_with_global_selectors( + &mut self, + common: &mut ExecutionRuntime, + batch: &[Vec<[f64; 4]>], + selected_helicities: Option<&BTreeSet>, + selected_colors: Option<&BTreeSet>, + ) -> RusticolResult<(Vec, RuntimeProfile)> { + if batch.is_empty() { + return Err(RusticolError::invalid_argument( + "recurrence evaluation requires at least one point", + )); + } + let total_started = Instant::now(); + let physics = common.physics.clone().ok_or_else(|| { + RusticolError::artifact("recurrence execution requires physics metadata") + })?; + let helicity_indices = physics.selected_helicity_indices(selected_helicities)?; + let color_indices = physics.selected_color_indices(selected_colors)?; + self.validate_public_axes(&physics, &helicity_indices, &color_indices)?; + let color_destinations = color_indices + .iter() + .copied() + .map(|color_index| Ok((color_index, self.union_destination_id(color_index)?))) + .collect::>>()?; + + let mut values = vec![0.0; batch.len()]; + let profile_before = DirectProfileSnapshot::capture(&self.scheduler, &self._backend_owner); + let parameter_started = Instant::now(); + self.prepare_parameters(common)?; + let parameter_setup = parameter_started.elapsed(); + let mut external_momentum_flatten = Duration::ZERO; + let mut reduction = Duration::ZERO; + + for helicity_index in helicity_indices { + let helicity = &physics.manifest.helicities[helicity_index]; + if !helicity.computed || helicity.structural_zero || helicity.coefficient == 0.0 { + continue; + } + let selector = self.union_helicity_selector(helicity_index)?; + let mut tile_start = 0usize; + while tile_start < batch.len() { + let tile_stop = (tile_start + self.effective_point_tile_size()).min(batch.len()); + let flatten_started = Instant::now(); + let point_count = + self.flatten_external_tile::(&batch[tile_start..tile_stop])?; + external_momentum_flatten += flatten_started.elapsed(); + let input_len = point_count + .checked_mul(self.external_source_count) + .and_then(|count| count.checked_mul(4)) + .ok_or_else(|| { + RusticolError::invalid_argument( + "recurrence external-momentum tile length overflows", + ) + })?; + let output = self + .scheduler + .execute_union_tile_from_external_planes::( + &selector, + u32::try_from(point_count).map_err(|_| { + RusticolError::invalid_argument( + "recurrence point tile exceeds the native u32 ABI", + ) + })?, + &self.external_momenta[..input_len], + )?; + + let reduction_started = Instant::now(); + for (color_index, destination_id) in color_destinations.iter().copied() { + if !physics.color_is_computed(color_index) { + continue; + } + let values_re = output.destination_re(destination_id).ok_or_else(|| { + RusticolError::integrity("all-flow-union amplitude destination is absent") + })?; + let values_im = output.destination_im(destination_id).ok_or_else(|| { + RusticolError::integrity("all-flow-union amplitude destination is absent") + })?; + let color_weight = physics.manifest.color_components[color_index].coefficient(); + let weight = helicity.coefficient * color_weight * common.normalization_factor; + for point in 0..point_count { + values[tile_start + point] += weight + * values_re[point] + .mul_add(values_re[point], values_im[point] * values_im[point]); + } + } + reduction += reduction_started.elapsed(); + tile_start = tile_stop; + } + } + + Ok(( + values, + direct_profile( + total_started.elapsed(), + parameter_setup, + external_momentum_flatten, + reduction, + profile_before, + DirectProfileSnapshot::capture(&self.scheduler, &self._backend_owner), + ), + )) + } + + fn run_union_resolved_f64( + &mut self, + common: &mut ExecutionRuntime, + batch: &[Vec<[f64; 4]>], + selected_helicities: Option<&BTreeSet>, + selected_colors: Option<&BTreeSet>, + ) -> RusticolResult<(ResolvedValues, RuntimeProfile)> { + if batch.is_empty() { + return Err(RusticolError::invalid_argument( + "resolved recurrence evaluation requires at least one point", + )); + } + let total_started = Instant::now(); + let physics = common.physics.clone().ok_or_else(|| { + RusticolError::artifact("resolved recurrence execution requires physics metadata") + })?; + let helicity_indices = physics.selected_helicity_indices(selected_helicities)?; + let color_indices = physics.selected_color_indices(selected_colors)?; + self.validate_public_axes(&physics, &helicity_indices, &color_indices)?; + let color_destinations = color_indices + .iter() + .copied() + .enumerate() + .map(|(color_position, color_index)| { + Ok(( + color_position, + color_index, + self.union_destination_id(color_index)?, + )) + }) + .collect::>>()?; + let component_count = helicity_indices + .len() + .checked_mul(color_indices.len()) + .ok_or_else(|| { + RusticolError::invalid_argument("recurrence resolved shape overflows") + })?; + let mut values = vec![ + 0.0; + batch.len().checked_mul(component_count).ok_or_else(|| { + RusticolError::invalid_argument("recurrence resolved output overflows") + })? + ]; + + let profile_before = DirectProfileSnapshot::capture(&self.scheduler, &self._backend_owner); + let parameter_started = Instant::now(); + self.prepare_parameters(common)?; + let parameter_setup = parameter_started.elapsed(); + let mut external_momentum_flatten = Duration::ZERO; + let mut reduction = Duration::ZERO; + + for (helicity_position, helicity_index) in helicity_indices.iter().copied().enumerate() { + let helicity = &physics.manifest.helicities[helicity_index]; + if !helicity.computed || helicity.structural_zero || helicity.coefficient == 0.0 { + continue; + } + let selector = self.union_helicity_selector(helicity_index)?; + let mut tile_start = 0usize; + while tile_start < batch.len() { + let tile_stop = (tile_start + self.effective_point_tile_size()).min(batch.len()); + let flatten_started = Instant::now(); + let point_count = + self.flatten_external_tile::(&batch[tile_start..tile_stop])?; + external_momentum_flatten += flatten_started.elapsed(); + let input_len = point_count + .checked_mul(self.external_source_count) + .and_then(|count| count.checked_mul(4)) + .ok_or_else(|| { + RusticolError::invalid_argument( + "recurrence external-momentum tile length overflows", + ) + })?; + let output = self + .scheduler + .execute_union_tile_from_external_planes::( + &selector, + u32::try_from(point_count).map_err(|_| { + RusticolError::invalid_argument( + "recurrence point tile exceeds the native u32 ABI", + ) + })?, + &self.external_momenta[..input_len], + )?; + + let reduction_started = Instant::now(); + for (color_position, color_index, destination_id) in + color_destinations.iter().copied() + { + if !physics.color_is_computed(color_index) { + continue; + } + let values_re = output.destination_re(destination_id).ok_or_else(|| { + RusticolError::integrity("all-flow-union amplitude destination is absent") + })?; + let values_im = output.destination_im(destination_id).ok_or_else(|| { + RusticolError::integrity("all-flow-union amplitude destination is absent") + })?; + let color_weight = physics.manifest.color_components[color_index].coefficient(); + let weight = helicity.coefficient * color_weight * common.normalization_factor; + for point in 0..point_count { + let target = (tile_start + point) * component_count + + helicity_position * color_indices.len() + + color_position; + values[target] += weight + * values_re[point] + .mul_add(values_re[point], values_im[point] * values_im[point]); + } + } + reduction += reduction_started.elapsed(); + tile_start = tile_stop; + } + } + + Ok(( + ResolvedValues { + values, + point_count: batch.len(), + helicity_indices, + color_indices, + }, + direct_profile( + total_started.elapsed(), + parameter_setup, + external_momentum_flatten, + reduction, + profile_before, + DirectProfileSnapshot::capture(&self.scheduler, &self._backend_owner), + ), + )) + } + + fn union_helicity_selector( + &self, + physics_helicity_index: usize, + ) -> RusticolResult { + match &self.selectors { + RecurrenceNativeSelectors::AllFlowUnion { + helicity_selectors_by_physics, + .. + } => helicity_selectors_by_physics + .get(physics_helicity_index) + .copied() + .flatten() + .ok_or_else(|| { + RusticolError::integrity( + "all-flow-union helicity is outside retained public coverage", + ) + }), + RecurrenceNativeSelectors::TopologyReplay { .. } => Err(RusticolError::integrity( + "union selector requested from topology replay", + )), + RecurrenceNativeSelectors::ContractedColorUnion { .. } => { + Err(RusticolError::integrity( + "union selector requested from contracted-color recurrence", + )) + } + } + } + + fn union_destination_id(&self, color_index: usize) -> RusticolResult { + match &self.selectors { + RecurrenceNativeSelectors::AllFlowUnion { + destination_by_public_flow, + .. + } => destination_by_public_flow + .get(color_index) + .copied() + .ok_or_else(|| { + RusticolError::integrity( + "all-flow-union color is outside retained public coverage", + ) + }), + RecurrenceNativeSelectors::TopologyReplay { .. } => Err(RusticolError::integrity( + "union destination requested from topology replay", + )), + RecurrenceNativeSelectors::ContractedColorUnion { .. } => { + Err(RusticolError::integrity( + "union destination requested from contracted-color recurrence", + )) + } + } + } + + fn prepare_parameters(&mut self, common: &ExecutionRuntime) -> RusticolResult<()> { + let (parameters_re, parameters_im) = self.scheduler.parameters_mut(); + initialize_prepared_parameter_planes( + &self.parameter_defaults, + &self.parameter_projection, + &common.model_parameter_values_f64, + parameters_re, + parameters_im, + ) + } + + // The ordinary direct scheduler consumes contiguous source/component + // planes. Persisted-helicity companion entrypoints retain their existing + // point-major contract by requesting SOURCE_MAJOR=false. Both layouts use + // the same preallocated workspace and the same single input-copy pass. + fn flatten_external_tile( + &mut self, + batch: &[Vec<[f64; 4]>], + ) -> RusticolResult { + let point_count = batch.len(); + for point in batch { + if point.len() != self.external_source_count { + return Err(RusticolError::invalid_argument(format!( + "recurrence point has {} external momenta, expected {}", + point.len(), + self.external_source_count + ))); + } + } + ensure_external_momentum_workspace_capacity( + &mut self.external_momenta, + point_count, + self.external_source_count, + self.effective_point_tile_size, + )?; + for (point_index, point) in batch.iter().enumerate() { + for (source_slot, momentum) in point.iter().enumerate() { + write_external_momentum::( + &mut self.external_momenta, + point_count, + self.external_source_count, + point_index, + source_slot, + momentum, + ); + } + } + Ok(point_count) + } + + fn flatten_external_tile_view( + &mut self, + batch: F64MomentumBatchView<'_>, + ) -> RusticolResult { + let point_count = batch.point_count(); + if batch.external_count() != self.external_source_count { + return Err(RusticolError::invalid_argument(format!( + "recurrence input has {} external momenta, expected {}", + batch.external_count(), + self.external_source_count + ))); + } + ensure_external_momentum_workspace_capacity( + &mut self.external_momenta, + point_count, + self.external_source_count, + self.effective_point_tile_size, + )?; + for point_index in 0..point_count { + let point = batch.point(point_index); + for source_slot in 0..self.external_source_count { + let momentum = point.momentum(source_slot).ok_or_else(|| { + RusticolError::integrity( + "validated recurrence momentum view is missing an external leg", + ) + })?; + write_external_momentum::( + &mut self.external_momenta, + point_count, + self.external_source_count, + point_index, + source_slot, + &momentum, + ); + } + } + Ok(point_count) + } + + fn external_tile_input_len(&self, point_count: usize) -> RusticolResult { + external_momentum_scalar_len(point_count, self.external_source_count) + } + + #[allow(clippy::too_many_arguments)] + fn execute_and_contract_contracted_tile( + &mut self, + point_count: usize, + input_len: usize, + physics: &PhysicsRuntime, + selected_helicities: Option<&BTreeSet>, + normalization_factor: f64, + profiled: bool, + accumulate: impl FnMut(usize, usize, f64), + ) -> RusticolResult { + { + let RecurrenceNativeSelectors::ContractedColorUnion { + destination_physics_helicity, + .. + } = &self.selectors + else { + return Err(RusticolError::integrity( + "contracted execution requested from a non-contracted recurrence", + )); + }; + if let Some(selected_helicities) = selected_helicities + && !contracted_selection_has_computed_destination( + destination_physics_helicity, + physics, + selected_helicities, + )? + { + // The selected public orbit is structural-zero or has no computed + // representative. The caller zeroed its output before entering + // this tile, so no recurrence, color work, or lazy FFT workspace + // is required. An absent selector deliberately still means full + // execution. + return Ok(Duration::ZERO); + } + } + self.ensure_primary_symmetric_group_color_workspace_for_capacity(point_count)?; + let Self { + scheduler, + selectors: + RecurrenceNativeSelectors::ContractedColorUnion { + contraction, + destination_physics_helicity, + replay_routes, + }, + external_momenta, + contracted_replay_re, + contracted_replay_im, + color_transform_re, + color_transform_im, + symmetric_group_color_workspace, + .. + } = self + else { + return Err(RusticolError::integrity( + "contracted execution requested from a non-contracted recurrence", + )); + }; + let input = &external_momenta[..input_len]; + let point_count_u32 = direct_point_count(point_count)?; + + if replay_routes.is_empty() { + let direct_output = if profiled { + scheduler + .execute_contracted_tile_from_external_planes::(point_count_u32, input)? + } else { + scheduler + .execute_contracted_tile_from_external_planes::(point_count_u32, input)? + }; + let reduction_started = profiled.then(Instant::now); + contract_color_tile( + &direct_output, + contraction, + destination_physics_helicity, + physics, + selected_helicities, + normalization_factor, + color_transform_re, + color_transform_im, + symmetric_group_color_workspace.as_mut(), + accumulate, + )?; + return Ok(reduction_started.map_or(Duration::ZERO, |started| started.elapsed())); + } + + let point_stride = scheduler.point_tile_size() as usize; + let destination_count = contraction.destination_count() as usize; + let required = destination_count.checked_mul(point_stride).ok_or_else(|| { + RusticolError::integrity("contracted replay amplitude workspace overflows usize") + })?; + if contracted_replay_re.len() < required || contracted_replay_im.len() < required { + return Err(RusticolError::integrity( + "contracted replay amplitude workspace is too small", + )); + } + let mut replay_output_copy = Duration::ZERO; + for route in replay_routes { + let direct_output = if profiled { + scheduler.execute_replay_tile_from_external_planes::( + &route.selector, + point_count_u32, + input, + )? + } else { + scheduler.execute_replay_tile_from_external_planes::( + &route.selector, + point_count_u32, + input, + )? + }; + // The scheduler profiles momentum fill, schedule execution, and + // replay scaling independently. Start this clock only after it + // returns: including the scheduler call here would attribute the + // complete recurrence execution a second time as reduction. + let replay_output_copy_started = profiled.then(Instant::now); + for &(source_destination, physical_destination) in &route.destination_copies { + let source_re = direct_output + .destination_re(source_destination) + .ok_or_else(|| { + RusticolError::integrity( + "contracted replay source amplitude destination is absent", + ) + })?; + let source_im = direct_output + .destination_im(source_destination) + .ok_or_else(|| { + RusticolError::integrity( + "contracted replay source amplitude destination is absent", + ) + })?; + let start = physical_destination as usize * point_stride; + contracted_replay_re[start..start + point_count].copy_from_slice(source_re); + contracted_replay_im[start..start + point_count].copy_from_slice(source_im); + } + if let Some(started) = replay_output_copy_started { + replay_output_copy += started.elapsed(); + } + } + let tile = ContractedReplayTile { + values_re: contracted_replay_re, + values_im: contracted_replay_im, + point_count, + point_stride, + destination_count, + }; + let reduction_started = profiled.then(Instant::now); + contract_color_tile( + &tile, + contraction, + destination_physics_helicity, + physics, + selected_helicities, + normalization_factor, + color_transform_re, + color_transform_im, + symmetric_group_color_workspace.as_mut(), + accumulate, + )?; + Ok(replay_output_copy + + reduction_started.map_or(Duration::ZERO, |started| started.elapsed())) + } + + fn ensure_primary_symmetric_group_color_workspace_for_capacity( + &mut self, + point_capacity: usize, + ) -> RusticolResult<()> { + let Self { + selectors, + symmetric_group_color_workspace, + .. + } = self; + let RecurrenceNativeSelectors::ContractedColorUnion { contraction, .. } = selectors else { + return Ok(()); + }; + let Some(RuntimeColorContractionReducer::SymmetricGroupFourier(reducer)) = + contraction.runtime_reducer() + else { + return Ok(()); + }; + ensure_symmetric_group_color_workspace_capacity( + symmetric_group_color_workspace, + reducer, + point_capacity, + ) + } + + fn validate_public_axis_lengths(&self, physics: &PhysicsRuntime) -> RusticolResult<()> { + let (color_count, helicity_count) = match &self.selectors { + RecurrenceNativeSelectors::TopologyReplay { + replay_selectors, .. + } => (replay_selectors.len(), physics.manifest.helicities.len()), + RecurrenceNativeSelectors::AllFlowUnion { + helicity_selectors_by_physics, + destination_by_public_flow, + } => ( + destination_by_public_flow.len(), + helicity_selectors_by_physics.len(), + ), + RecurrenceNativeSelectors::ContractedColorUnion { + destination_physics_helicity, + .. + } => { + if destination_physics_helicity + .iter() + .any(|index| *index >= physics.manifest.helicities.len()) + { + return Err(RusticolError::integrity( + "contracted recurrence maps a destination outside the public helicity axis", + )); + } + (1, physics.manifest.helicities.len()) + } + }; + if color_count != physics.manifest.color_components.len() + || helicity_count != physics.manifest.helicities.len() + { + return Err(RusticolError::integrity( + "recurrence selectors do not cover the public physics axes", + )); + } + Ok(()) + } + + fn validate_public_axes( + &self, + physics: &PhysicsRuntime, + helicity_indices: &[usize], + color_indices: &[usize], + ) -> RusticolResult<()> { + self.validate_public_axis_lengths(physics)?; + let color_count = physics.manifest.color_components.len(); + let helicity_count = physics.manifest.helicities.len(); + if color_indices.iter().any(|index| *index >= color_count) + || helicity_indices + .iter() + .any(|index| *index >= helicity_count) + { + return Err(RusticolError::integrity( + "recurrence selector mapping is outside the public physics axes", + )); + } + Ok(()) + } +} + +fn lazy_symmetric_group_workspace_for_point_tile( + reducer: &crate::recurrence::RuntimeSymmetricGroupColorContraction, + requested_point_tile_size: usize, +) -> RusticolResult<(usize, Option)> { + Ok(( + reducer.bounded_lane_capacity(requested_point_tile_size)?, + None, + )) +} + +fn ensure_symmetric_group_color_workspace_capacity( + workspace: &mut Option, + reducer: &crate::recurrence::RuntimeSymmetricGroupColorContraction, + point_capacity: usize, +) -> RusticolResult<()> { + if let Some(workspace) = workspace.as_mut() { + return workspace.ensure_lane_capacity(reducer, point_capacity); + } + *workspace = Some(reducer.workspace(point_capacity)?); + Ok(()) +} + +fn external_momentum_scalar_len( + point_count: usize, + external_source_count: usize, +) -> RusticolResult { + point_count + .checked_mul(external_source_count) + .and_then(|count| count.checked_mul(4)) + .ok_or_else(|| { + RusticolError::invalid_argument("recurrence external-momentum tile length overflows") + }) +} + +#[inline] +fn write_external_momentum( + output: &mut [f64], + point_count: usize, + source_count: usize, + point: usize, + source: usize, + momentum: &[f64; 4], +) { + if SOURCE_MAJOR { + for (component, &value) in momentum.iter().enumerate() { + output[(source * 4 + component) * point_count + point] = value; + } + } else { + let start = (point * source_count + source) * 4; + output[start..start + 4].copy_from_slice(momentum); + } +} + +fn ensure_external_momentum_workspace_capacity( + workspace: &mut Vec, + point_count: usize, + external_source_count: usize, + maximum_point_count: usize, +) -> RusticolResult { + if point_count > maximum_point_count { + return Err(RusticolError::invalid_argument( + "recurrence point tile exceeds its persistent workspace", + )); + } + let required = external_momentum_scalar_len(point_count, external_source_count)?; + if workspace.len() < required { + workspace + .try_reserve_exact(required - workspace.len()) + .map_err(|error| { + RusticolError::internal(format!( + "recurrence external-momentum workspace allocation failed: {error}" + )) + })?; + workspace.resize(required, 0.0); + } + Ok(required) +} + +fn validate_recurrence_selector_ids( + physics: &PhysicsRuntime, + selected_helicities: Option<&BTreeSet>, + selected_colors: Option<&BTreeSet>, +) -> RusticolResult<()> { + if let Some(ids) = selected_helicities + && let Some(id) = ids + .iter() + .find(|id| !physics.helicity_index_by_id.contains_key(*id)) + { + return Err(RusticolError::selector(format!( + "unknown resolved helicity id {id:?}" + ))); + } + if let Some(ids) = selected_colors + && let Some(id) = ids + .iter() + .find(|id| !physics.color_index_by_id.contains_key(*id)) + { + return Err(RusticolError::selector(format!( + "unknown resolved color component id {id:?}" + ))); + } + Ok(()) +} + +#[inline(always)] +fn recurrence_helicity_is_selected( + physics: &PhysicsRuntime, + selected: Option<&BTreeSet>, + index: usize, +) -> bool { + selected.is_none_or(|ids| ids.contains(&physics.manifest.helicities[index].id)) +} + +#[inline(always)] +fn recurrence_helicity_orbit_weight( + physics: &PhysicsRuntime, + selected: Option<&BTreeSet>, + representative_index: usize, +) -> f64 { + physics + .helicity_orbit_members(representative_index) + .iter() + .copied() + .filter(|index| recurrence_helicity_is_selected(physics, selected, *index)) + .map(|index| physics.manifest.helicities[index].coefficient) + .sum() +} + +#[inline(always)] +fn recurrence_color_is_selected( + physics: &PhysicsRuntime, + selected: Option<&BTreeSet>, + index: usize, +) -> bool { + selected.is_none_or(|ids| ids.contains(physics.manifest.color_components[index].id())) +} + +fn direct_point_count(point_count: usize) -> RusticolResult { + u32::try_from(point_count).map_err(|_| { + RusticolError::invalid_argument("recurrence point tile exceeds the native u32 ABI") + }) +} + +fn reject_contracted_color_selector( + selected_colors: Option<&BTreeSet>, +) -> RusticolResult<()> { + if selected_colors.is_some() { + return Err(RusticolError::selector( + "contracted NLC/full recurrence does not expose a color-flow selector", + )); + } + Ok(()) +} + +fn contracted_destination_helicity_map( + contraction: &RecurrenceColorContraction, + direct_helicity_to_physics: &[usize], +) -> RusticolResult> { + let destination_count = contraction.destination_count() as usize; + let group_count = contraction.group_count() as usize; + let symmetric_group = matches!( + contraction.runtime_reducer(), + Some(RuntimeColorContractionReducer::SymmetricGroupFourier(_)) + ); + if contraction.component_count() as usize != direct_helicity_to_physics.len() + || contraction.destination_by_group().len() != group_count + || contraction.sector_by_group().len() != group_count + || contraction.component_by_group().len() != group_count + || (!symmetric_group && group_count != destination_count) + { + return Err(RusticolError::integrity( + "contracted color dimensions disagree with the public helicity map", + )); + } + + let mut destination_physics_helicity = vec![usize::MAX; destination_count]; + for (group_id, destination_id) in contraction + .destination_by_group() + .iter() + .copied() + .enumerate() + { + let expected_direct_helicity = contraction.component_by_group()[group_id] as usize; + let physics_helicity = *direct_helicity_to_physics + .get(expected_direct_helicity) + .ok_or_else(|| { + RusticolError::integrity("contracted destination has no public helicity mapping") + })?; + let slot = destination_physics_helicity + .get_mut(destination_id as usize) + .ok_or_else(|| { + RusticolError::integrity( + "contracted destination mapping exceeds its authenticated range", + ) + })?; + if *slot != usize::MAX { + return Err(RusticolError::integrity( + "contracted color map repeats an amplitude destination", + )); + } + *slot = physics_helicity; + } + if !symmetric_group && destination_physics_helicity.contains(&usize::MAX) { + return Err(RusticolError::integrity( + "contracted color map does not cover every amplitude destination", + )); + } + if !symmetric_group { + for entry in contraction.canonical_logical_entries() { + if contraction.component_by_group()[entry.left_group_id as usize] + != contraction.component_by_group()[entry.right_group_id as usize] + { + return Err(RusticolError::integrity( + "contracted color entry mixes different helicity components", + )); + } + } + } + Ok(destination_physics_helicity) +} + +fn contracted_selection_has_computed_destination( + destination_physics_helicity: &[usize], + physics: &PhysicsRuntime, + selected_helicities: &BTreeSet, +) -> RusticolResult { + for &helicity_index in destination_physics_helicity { + // Symmetric-group payloads may reserve authenticated amplitude slots + // that are absent from the active local-group table. + if helicity_index == usize::MAX { + continue; + } + let helicity = physics + .manifest + .helicities + .get(helicity_index) + .ok_or_else(|| { + RusticolError::integrity( + "contracted destination selector references an absent public helicity", + ) + })?; + if !helicity.computed + || helicity.structural_zero + || helicity.coefficient == 0.0 + || recurrence_helicity_orbit_weight(physics, Some(selected_helicities), helicity_index) + == 0.0 + { + continue; + } + return Ok(true); + } + Ok(false) +} + +fn contracted_replay_routes( + scheduler: &DirectRecurrenceExecutionRuntime, + contraction: &RecurrenceColorContraction, +) -> RusticolResult> { + let plan = scheduler.plan(); + if plan.strategy() != RecurrenceStrategy::ContractedColorUnion { + return Err(RusticolError::integrity( + "contracted replay routes require a contracted-color plan", + )); + } + if plan.replay_targets().is_empty() { + if contraction.destination_count() as usize != plan.amplitude_destinations().len() { + return Err(RusticolError::integrity( + "non-replayed contracted color destinations disagree with the Direct plan", + )); + } + return Ok(Vec::new()); + } + if contraction.sector_count() != plan.physical_sector_count() + || contraction.component_count() as usize != plan.resolved_helicities().len() + { + return Err(RusticolError::integrity( + "contracted replay domain disagrees with the Direct plan", + )); + } + + let destination_count = contraction.destination_count() as usize; + let component_count = contraction.component_count() as usize; + let symmetric_group = matches!( + contraction.runtime_reducer(), + Some(RuntimeColorContractionReducer::SymmetricGroupFourier(_)) + ); + let mut covered = vec![false; destination_count]; + let mut routes = Vec::with_capacity(plan.replay_targets().len()); + for target in plan.replay_targets() { + let selector = scheduler.prepare_replay_selector(target.public_flow_id)?; + let target_sector = selector.public_flow_id() as usize; + let mut destination_copies = Vec::with_capacity(component_count); + for destination in plan + .amplitude_destinations() + .iter() + .filter(|destination| destination.target_sector_id == selector.representative_flow_id()) + { + let direct_helicity = destination.target_helicity_id_or_sentinel; + if direct_helicity == DIRECT_NONE_U32 { + return Err(RusticolError::integrity( + "contracted replay destination lacks a resolved helicity", + )); + } + let mapped_helicity = selector + .helicity_map() + .get(direct_helicity as usize) + .copied() + .ok_or_else(|| { + RusticolError::integrity("contracted replay destination helicity is not mapped") + })? as usize; + let physical_destination = target_sector + .checked_mul(component_count) + .and_then(|offset| offset.checked_add(mapped_helicity)) + .ok_or_else(|| { + RusticolError::artifact( + "contracted replay physical destination overflows usize", + ) + })?; + if physical_destination >= destination_count || covered[physical_destination] { + return Err(RusticolError::integrity( + "contracted replay route is not a dense physical color/helicity bijection", + )); + } + if !symmetric_group + && (contraction.sector_by_group()[physical_destination] as usize != target_sector + || contraction.component_by_group()[physical_destination] as usize + != mapped_helicity + || contraction.destination_by_group()[physical_destination] as usize + != physical_destination) + { + return Err(RusticolError::integrity( + "contracted replay route disagrees with the direct color projection", + )); + } + covered[physical_destination] = true; + destination_copies.push(( + destination.id, + u32::try_from(physical_destination).map_err(|_| { + RusticolError::artifact("contracted replay physical destination exceeds u32") + })?, + )); + } + if destination_copies.len() != component_count { + return Err(RusticolError::integrity( + "contracted replay representative lacks complete helicity coverage", + )); + } + routes.push(ContractedReplayRoute { + selector, + destination_copies, + }); + } + if covered.contains(&false) { + return Err(RusticolError::integrity( + "contracted replay routes do not cover every physical color/helicity destination", + )); + } + Ok(routes) +} + +#[allow(clippy::too_many_arguments)] +fn contract_primary_local_color_tile( + output: PersistedHelicityAmplitudeTileV1<'_>, + contraction: &RecurrenceColorContraction, + view: &PrimaryLocalColorViewV1, + physics: &PhysicsRuntime, + physics_helicity_id: usize, + normalization_factor: f64, + color_transform_re: &mut [f64], + color_transform_im: &mut [f64], + symmetric_group_workspace: Option<&mut RuntimeSymmetricGroupColorWorkspace>, + mut accumulate: impl FnMut(usize, f64), +) -> RusticolResult<()> { + let helicity = physics + .manifest + .helicities + .get(physics_helicity_id) + .ok_or_else(|| { + RusticolError::integrity("persisted selector references an absent public helicity") + })?; + if helicity.structural_zero || helicity.coefficient == 0.0 { + return Err(RusticolError::internal( + "structural-zero helicity reached persisted color contraction", + )); + } + let scale = helicity.coefficient * normalization_factor; + match contraction.runtime_reducer() { + Some(RuntimeColorContractionReducer::Walsh(factorization)) => { + contract_primary_local_walsh_tile( + output, + factorization, + &view.auxiliary_destination_by_local_group, + scale, + color_transform_re, + color_transform_im, + &mut accumulate, + ) + } + Some(RuntimeColorContractionReducer::SymmetricGroupFourier(reducer)) => { + let workspace = symmetric_group_workspace.ok_or_else(|| { + RusticolError::integrity("symmetric-group recurrence color workspace is absent") + })?; + contract_primary_local_symmetric_group_tile( + output, + reducer, + workspace, + &view.auxiliary_destination_by_local_group, + scale, + &mut accumulate, + ) + } + None => { + contract_primary_local_direct_tile(output, contraction, view, scale, &mut accumulate) + } + } +} + +fn contract_primary_local_direct_tile( + output: PersistedHelicityAmplitudeTileV1<'_>, + contraction: &RecurrenceColorContraction, + view: &PrimaryLocalColorViewV1, + scale: f64, + accumulate: &mut impl FnMut(usize, f64), +) -> RusticolResult<()> { + for raw in contraction.entries() { + let local_group = |stored_group_id: u32| -> RusticolResult { + match contraction.storage() { + RecurrenceColorStorage::Repeated => { + let local = stored_group_id as usize; + if local >= view.auxiliary_destination_by_local_group.len() { + return Err(RusticolError::integrity( + "repeated primary color entry is outside its local group domain", + )); + } + Ok(local) + } + RecurrenceColorStorage::Expanded => { + let primary_destination = contraction + .destination_by_group() + .get(stored_group_id as usize) + .copied() + .ok_or_else(|| { + RusticolError::integrity( + "expanded primary color entry references an absent group", + ) + })?; + let local = view + .local_group_by_primary_destination + .get(primary_destination as usize) + .copied() + .ok_or_else(|| { + RusticolError::integrity( + "expanded primary color destination is out of bounds", + ) + })?; + if local == DIRECT_NONE_U32 { + return Err(RusticolError::integrity( + "expanded primary color destination has no local owner", + )); + } + Ok(local as usize) + } + RecurrenceColorStorage::ConvolutionKernels => Err(RusticolError::internal( + "factorized primary color reached the direct reducer", + )), + } + }; + let left_local = local_group(raw.left_group_id)?; + let right_local = local_group(raw.right_group_id)?; + let entry = RuntimeColorContractionEntry { + left_destination_id: view.auxiliary_destination_by_local_group[left_local], + right_destination_id: view.auxiliary_destination_by_local_group[right_local], + coefficient_re: raw.weight_re * raw.symmetry_factor, + coefficient_im: raw.weight_im * raw.symmetry_factor, + }; + let left_re = output.destination_re(entry.left_destination_id)?; + let left_im = output.destination_im(entry.left_destination_id)?; + let right_re = output.destination_re(entry.right_destination_id)?; + let right_im = output.destination_im(entry.right_destination_id)?; + for point in 0..output.point_count() as usize { + accumulate( + point, + scale + * entry.contract_real_bilinear( + left_re[point], + left_im[point], + right_re[point], + right_im[point], + ), + ); + } + } + Ok(()) +} + +fn contract_primary_local_symmetric_group_tile( + output: PersistedHelicityAmplitudeTileV1<'_>, + reducer: &crate::recurrence::RuntimeSymmetricGroupColorContraction, + workspace: &mut RuntimeSymmetricGroupColorWorkspace, + auxiliary_destination_by_local_group: &[u32], + scale: f64, + accumulate: &mut impl FnMut(usize, f64), +) -> RusticolResult<()> { + if reducer.local_group_count() != auxiliary_destination_by_local_group.len() { + return Err(RusticolError::integrity( + "symmetric-group primary local-color view has the wrong domain", + )); + } + let point_count = output.point_count() as usize; + reducer.reduce_lanes(workspace, point_count, |local_group, point| { + let destination = auxiliary_destination_by_local_group[local_group]; + let real = output.destination_re(destination)?; + let imaginary = output.destination_im(destination)?; + Ok((real[point], imaginary[point])) + })?; + for (point, contracted) in workspace.reduced(point_count)?.iter().copied().enumerate() { + accumulate(point, scale * contracted); + } + Ok(()) +} + +#[allow(clippy::too_many_arguments)] +fn contract_primary_local_walsh_tile( + output: PersistedHelicityAmplitudeTileV1<'_>, + factorization: &crate::recurrence::RuntimeFactorizedColorContraction, + auxiliary_destination_by_local_group: &[u32], + scale: f64, + color_transform_re: &mut [f64], + color_transform_im: &mut [f64], + accumulate: &mut impl FnMut(usize, f64), +) -> RusticolResult<()> { + let point_count = output.point_count() as usize; + let local_group_count = auxiliary_destination_by_local_group.len(); + let scratch_len = local_group_count.checked_mul(point_count).ok_or_else(|| { + RusticolError::integrity("factorized primary local-color scratch length overflows") + })?; + if color_transform_re.len() < scratch_len || color_transform_im.len() < scratch_len { + return Err(RusticolError::integrity( + "factorized primary local-color workspace has the wrong shape", + )); + } + let transform_re = &mut color_transform_re[..scratch_len]; + let transform_im = &mut color_transform_im[..scratch_len]; + for (local_group, destination) in auxiliary_destination_by_local_group + .iter() + .copied() + .enumerate() + { + let source_re = output.destination_re(destination)?; + let source_im = output.destination_im(destination)?; + let start = local_group * point_count; + transform_re[start..start + point_count].copy_from_slice(source_re); + transform_im[start..start + point_count].copy_from_slice(source_im); + } + + if factorization.subgroup_order() == 4 { + let amplitude_scale = factorization.amplitude_scale(); + for coset in factorization.cosets() { + let [g0, g1, g2, g3] = <[u32; 4]>::try_from(coset.as_slice()).map_err(|_| { + RusticolError::integrity("rank-two color factorization has a malformed coset") + })?; + let starts = [g0, g1, g2, g3].map(|group| group as usize * point_count); + for point in 0..point_count { + let re0 = transform_re[starts[0] + point]; + let re1 = transform_re[starts[1] + point]; + let re2 = transform_re[starts[2] + point]; + let re3 = transform_re[starts[3] + point]; + let im0 = transform_im[starts[0] + point]; + let im1 = transform_im[starts[1] + point]; + let im2 = transform_im[starts[2] + point]; + let im3 = transform_im[starts[3] + point]; + let re01 = re0 + re1; + let re23 = re2 + re3; + let re01_difference = re0 - re1; + let re23_difference = re2 - re3; + let im01 = im0 + im1; + let im23 = im2 + im3; + let im01_difference = im0 - im1; + let im23_difference = im2 - im3; + transform_re[starts[0] + point] = (re01 + re23) * amplitude_scale; + transform_re[starts[1] + point] = + (re01_difference + re23_difference) * amplitude_scale; + transform_re[starts[2] + point] = (re01 - re23) * amplitude_scale; + transform_re[starts[3] + point] = + (re01_difference - re23_difference) * amplitude_scale; + transform_im[starts[0] + point] = (im01 + im23) * amplitude_scale; + transform_im[starts[1] + point] = + (im01_difference + im23_difference) * amplitude_scale; + transform_im[starts[2] + point] = (im01 - im23) * amplitude_scale; + transform_im[starts[3] + point] = + (im01_difference - im23_difference) * amplitude_scale; + } + } + } else { + for coset in factorization.cosets() { + let mut stride = 1usize; + while stride < factorization.subgroup_order() { + for start in (0..factorization.subgroup_order()).step_by(stride * 2) { + for offset in 0..stride { + let left_start = coset[start + offset] as usize * point_count; + let right_start = coset[start + stride + offset] as usize * point_count; + for point in 0..point_count { + let left_re = transform_re[left_start + point]; + let right_re = transform_re[right_start + point]; + let left_im = transform_im[left_start + point]; + let right_im = transform_im[right_start + point]; + transform_re[left_start + point] = left_re + right_re; + transform_re[right_start + point] = left_re - right_re; + transform_im[left_start + point] = left_im + right_im; + transform_im[right_start + point] = left_im - right_im; + } + } + } + stride *= 2; + } + } + } + + for entry in factorization.entries() { + let left_start = entry.left_group_index as usize * point_count; + let right_start = entry.right_group_index as usize * point_count; + for point in 0..point_count { + let product_re = transform_re[left_start + point].mul_add( + transform_re[right_start + point], + transform_im[left_start + point] * transform_im[right_start + point], + ); + let product_im = transform_im[left_start + point].mul_add( + transform_re[right_start + point], + -transform_re[left_start + point] * transform_im[right_start + point], + ); + let contracted = entry + .coefficient_re + .mul_add(product_re, -entry.coefficient_im * product_im); + accumulate(point, scale * contracted); + } + } + Ok(()) +} + +trait ContractedAmplitudeTile { + fn point_count(&self) -> usize; + fn destination_re(&self, destination_id: u32) -> Option<&[f64]>; + fn destination_im(&self, destination_id: u32) -> Option<&[f64]>; +} + +impl ContractedAmplitudeTile for DirectRecurrenceTileOutput<'_> { + fn point_count(&self) -> usize { + DirectRecurrenceTileOutput::point_count(self) as usize + } + + fn destination_re(&self, destination_id: u32) -> Option<&[f64]> { + DirectRecurrenceTileOutput::destination_re(self, destination_id) + } + + fn destination_im(&self, destination_id: u32) -> Option<&[f64]> { + DirectRecurrenceTileOutput::destination_im(self, destination_id) + } +} + +struct ContractedReplayTile<'a> { + values_re: &'a [f64], + values_im: &'a [f64], + point_count: usize, + point_stride: usize, + destination_count: usize, +} + +impl ContractedAmplitudeTile for ContractedReplayTile<'_> { + fn point_count(&self) -> usize { + self.point_count + } + + fn destination_re(&self, destination_id: u32) -> Option<&[f64]> { + self.destination(self.values_re, destination_id) + } + + fn destination_im(&self, destination_id: u32) -> Option<&[f64]> { + self.destination(self.values_im, destination_id) + } +} + +impl ContractedReplayTile<'_> { + fn destination<'a>(&self, values: &'a [f64], destination_id: u32) -> Option<&'a [f64]> { + let destination = destination_id as usize; + if destination >= self.destination_count { + return None; + } + let start = destination.checked_mul(self.point_stride)?; + values.get(start..start + self.point_count) + } +} + +// These arguments are the authenticated tile views and selector/reduction +// contract; grouping them would obscure ownership without reducing call state. +#[allow(clippy::too_many_arguments)] +fn contract_color_tile( + output: &T, + contraction: &RecurrenceColorContraction, + destination_physics_helicity: &[usize], + physics: &PhysicsRuntime, + selected_helicities: Option<&BTreeSet>, + normalization_factor: f64, + color_transform_re: &mut [f64], + color_transform_im: &mut [f64], + symmetric_group_workspace: Option<&mut RuntimeSymmetricGroupColorWorkspace>, + mut accumulate: impl FnMut(usize, usize, f64), +) -> RusticolResult<()> { + let point_count = output.point_count(); + match contraction.runtime_reducer() { + Some(RuntimeColorContractionReducer::Walsh(factorization)) => { + return contract_factorized_color_tile( + output, + contraction, + factorization, + destination_physics_helicity, + physics, + selected_helicities, + normalization_factor, + color_transform_re, + color_transform_im, + &mut accumulate, + ); + } + Some(RuntimeColorContractionReducer::SymmetricGroupFourier(reducer)) => { + let workspace = symmetric_group_workspace.ok_or_else(|| { + RusticolError::integrity("symmetric-group recurrence color workspace is absent") + })?; + return contract_symmetric_group_color_tile( + output, + contraction, + reducer, + workspace, + destination_physics_helicity, + physics, + selected_helicities, + normalization_factor, + &mut accumulate, + ); + } + None => {} + } + for entry in contraction.runtime_entries() { + let left_helicity = *destination_physics_helicity + .get(entry.left_destination_id as usize) + .ok_or_else(|| { + RusticolError::integrity( + "contracted left destination has no public helicity mapping", + ) + })?; + let right_helicity = *destination_physics_helicity + .get(entry.right_destination_id as usize) + .ok_or_else(|| { + RusticolError::integrity( + "contracted right destination has no public helicity mapping", + ) + })?; + if left_helicity != right_helicity { + return Err(RusticolError::integrity( + "contracted color entry mixes public helicities", + )); + } + let helicity = physics + .manifest + .helicities + .get(left_helicity) + .ok_or_else(|| { + RusticolError::integrity( + "contracted color entry references an absent public helicity", + ) + })?; + if !helicity.computed || helicity.structural_zero || helicity.coefficient == 0.0 { + continue; + } + if recurrence_helicity_orbit_weight(physics, selected_helicities, left_helicity) == 0.0 { + continue; + } + let left_re = output + .destination_re(entry.left_destination_id) + .ok_or_else(|| { + RusticolError::integrity("contracted left amplitude destination is absent") + })?; + let left_im = output + .destination_im(entry.left_destination_id) + .ok_or_else(|| { + RusticolError::integrity("contracted left amplitude destination is absent") + })?; + let right_re = output + .destination_re(entry.right_destination_id) + .ok_or_else(|| { + RusticolError::integrity("contracted right amplitude destination is absent") + })?; + let right_im = output + .destination_im(entry.right_destination_id) + .ok_or_else(|| { + RusticolError::integrity("contracted right amplitude destination is absent") + })?; + for point in 0..point_count { + let contracted = entry.contract_real_bilinear( + left_re[point], + left_im[point], + right_re[point], + right_im[point], + ); + for physical_helicity in physics.helicity_orbit_members(left_helicity) { + if !recurrence_helicity_is_selected( + physics, + selected_helicities, + *physical_helicity, + ) { + continue; + } + let scale = physics.manifest.helicities[*physical_helicity].coefficient + * normalization_factor; + accumulate(point, *physical_helicity, scale * contracted); + } + } + } + Ok(()) +} + +#[allow(clippy::too_many_arguments)] +fn contract_symmetric_group_color_tile( + output: &T, + contraction: &RecurrenceColorContraction, + reducer: &crate::recurrence::RuntimeSymmetricGroupColorContraction, + workspace: &mut RuntimeSymmetricGroupColorWorkspace, + destination_physics_helicity: &[usize], + physics: &PhysicsRuntime, + selected_helicities: Option<&BTreeSet>, + normalization_factor: f64, + accumulate: &mut impl FnMut(usize, usize, f64), +) -> RusticolResult<()> { + let point_count = output.point_count(); + let component_count = contraction.component_count() as usize; + if reducer.local_group_count() != contraction.local_group_count() as usize { + return Err(RusticolError::integrity( + "symmetric-group recurrence reducer has the wrong local-group domain", + )); + } + + for component_index in 0..component_count { + let representative_destination = contraction + .ordered_destination_id(0, component_index) + .ok_or_else(|| { + RusticolError::integrity( + "symmetric-group recurrence component has no amplitude destination", + ) + })?; + let helicity_index = *destination_physics_helicity + .get(representative_destination as usize) + .ok_or_else(|| { + RusticolError::integrity( + "symmetric-group recurrence destination has no public helicity mapping", + ) + })?; + let helicity = physics + .manifest + .helicities + .get(helicity_index) + .ok_or_else(|| { + RusticolError::integrity( + "symmetric-group recurrence component references an absent helicity", + ) + })?; + if !helicity.computed || helicity.structural_zero || helicity.coefficient == 0.0 { + continue; + } + if recurrence_helicity_orbit_weight(physics, selected_helicities, helicity_index) == 0.0 { + continue; + } + + for local_group_index in 0..reducer.local_group_count() { + let destination_id = contraction + .ordered_destination_id(local_group_index, component_index) + .ok_or_else(|| { + RusticolError::integrity( + "symmetric-group recurrence local group has no amplitude destination", + ) + })?; + if destination_physics_helicity + .get(destination_id as usize) + .copied() + != Some(helicity_index) + || output.destination_re(destination_id).is_none() + || output.destination_im(destination_id).is_none() + { + return Err(RusticolError::integrity( + "symmetric-group recurrence local groups mix helicities or lack amplitudes", + )); + } + } + + reducer.reduce_lanes(workspace, point_count, |local_group_index, point| { + let destination_id = contraction + .ordered_destination_id(local_group_index, component_index) + .ok_or_else(|| { + RusticolError::integrity( + "symmetric-group recurrence local group has no amplitude destination", + ) + })?; + let real = output.destination_re(destination_id).ok_or_else(|| { + RusticolError::integrity( + "symmetric-group recurrence real amplitude destination is absent", + ) + })?; + let imaginary = output.destination_im(destination_id).ok_or_else(|| { + RusticolError::integrity( + "symmetric-group recurrence imaginary amplitude destination is absent", + ) + })?; + Ok(( + *real.get(point).ok_or_else(|| { + RusticolError::integrity( + "symmetric-group recurrence real amplitude tile is too short", + ) + })?, + *imaginary.get(point).ok_or_else(|| { + RusticolError::integrity( + "symmetric-group recurrence imaginary amplitude tile is too short", + ) + })?, + )) + })?; + + for (point, contracted) in workspace.reduced(point_count)?.iter().copied().enumerate() { + for physical_helicity in physics.helicity_orbit_members(helicity_index) { + if !recurrence_helicity_is_selected( + physics, + selected_helicities, + *physical_helicity, + ) { + continue; + } + let scale = physics.manifest.helicities[*physical_helicity].coefficient + * normalization_factor; + accumulate(point, *physical_helicity, scale * contracted); + } + } + } + Ok(()) +} + +#[allow(clippy::too_many_arguments)] +fn contract_factorized_color_tile( + output: &T, + contraction: &RecurrenceColorContraction, + factorization: &crate::recurrence::RuntimeFactorizedColorContraction, + destination_physics_helicity: &[usize], + physics: &PhysicsRuntime, + selected_helicities: Option<&BTreeSet>, + normalization_factor: f64, + color_transform_re: &mut [f64], + color_transform_im: &mut [f64], + accumulate: &mut impl FnMut(usize, usize, f64), +) -> RusticolResult<()> { + let point_count = output.point_count(); + let local_group_count = contraction.local_group_count() as usize; + let scratch_len = local_group_count.checked_mul(point_count).ok_or_else(|| { + RusticolError::integrity("factorized recurrence color scratch length overflows") + })?; + if color_transform_re.len() < scratch_len || color_transform_im.len() < scratch_len { + return Err(RusticolError::integrity( + "factorized recurrence color workspace is smaller than the point tile", + )); + } + let transform_re = &mut color_transform_re[..scratch_len]; + let transform_im = &mut color_transform_im[..scratch_len]; + let component_count = contraction.component_count() as usize; + + for component_index in 0..component_count { + let representative_destination = contraction + .ordered_destination_id(0, component_index) + .ok_or_else(|| { + RusticolError::integrity( + "factorized recurrence component has no amplitude destination", + ) + })?; + let helicity_index = *destination_physics_helicity + .get(representative_destination as usize) + .ok_or_else(|| { + RusticolError::integrity( + "factorized recurrence destination has no public helicity mapping", + ) + })?; + let helicity = physics + .manifest + .helicities + .get(helicity_index) + .ok_or_else(|| { + RusticolError::integrity( + "factorized recurrence color component references an absent helicity", + ) + })?; + if !helicity.computed || helicity.structural_zero || helicity.coefficient == 0.0 { + continue; + } + if recurrence_helicity_orbit_weight(physics, selected_helicities, helicity_index) == 0.0 { + continue; + } + + for local_group_index in 0..local_group_count { + let destination_id = contraction + .ordered_destination_id(local_group_index, component_index) + .ok_or_else(|| { + RusticolError::integrity( + "factorized recurrence local group has no amplitude destination", + ) + })?; + if destination_physics_helicity + .get(destination_id as usize) + .copied() + != Some(helicity_index) + { + return Err(RusticolError::integrity( + "factorized recurrence local groups mix public helicities", + )); + } + let source_re = output.destination_re(destination_id).ok_or_else(|| { + RusticolError::integrity( + "factorized recurrence real amplitude destination is absent", + ) + })?; + let source_im = output.destination_im(destination_id).ok_or_else(|| { + RusticolError::integrity( + "factorized recurrence imaginary amplitude destination is absent", + ) + })?; + let start = local_group_index * point_count; + transform_re[start..start + point_count].copy_from_slice(source_re); + transform_im[start..start + point_count].copy_from_slice(source_im); + } + + if factorization.subgroup_order() == 4 { + let amplitude_scale = factorization.amplitude_scale(); + for coset in factorization.cosets() { + let [g0, g1, g2, g3] = <[u32; 4]>::try_from(coset.as_slice()).map_err(|_| { + RusticolError::integrity( + "rank-two recurrence color factorization has a malformed coset", + ) + })?; + let starts = [ + g0 as usize * point_count, + g1 as usize * point_count, + g2 as usize * point_count, + g3 as usize * point_count, + ]; + for point in 0..point_count { + let re0 = transform_re[starts[0] + point]; + let re1 = transform_re[starts[1] + point]; + let re2 = transform_re[starts[2] + point]; + let re3 = transform_re[starts[3] + point]; + let im0 = transform_im[starts[0] + point]; + let im1 = transform_im[starts[1] + point]; + let im2 = transform_im[starts[2] + point]; + let im3 = transform_im[starts[3] + point]; + let re01 = re0 + re1; + let re23 = re2 + re3; + let re01_difference = re0 - re1; + let re23_difference = re2 - re3; + let im01 = im0 + im1; + let im23 = im2 + im3; + let im01_difference = im0 - im1; + let im23_difference = im2 - im3; + transform_re[starts[0] + point] = (re01 + re23) * amplitude_scale; + transform_re[starts[1] + point] = + (re01_difference + re23_difference) * amplitude_scale; + transform_re[starts[2] + point] = (re01 - re23) * amplitude_scale; + transform_re[starts[3] + point] = + (re01_difference - re23_difference) * amplitude_scale; + transform_im[starts[0] + point] = (im01 + im23) * amplitude_scale; + transform_im[starts[1] + point] = + (im01_difference + im23_difference) * amplitude_scale; + transform_im[starts[2] + point] = (im01 - im23) * amplitude_scale; + transform_im[starts[3] + point] = + (im01_difference - im23_difference) * amplitude_scale; + } + } + } else { + for coset in factorization.cosets() { + let mut stride = 1usize; + while stride < factorization.subgroup_order() { + for start in (0..factorization.subgroup_order()).step_by(stride * 2) { + for offset in 0..stride { + let left_group = coset[start + offset] as usize; + let right_group = coset[start + stride + offset] as usize; + let left_start = left_group * point_count; + let right_start = right_group * point_count; + for point in 0..point_count { + let left_re = transform_re[left_start + point]; + let right_re = transform_re[right_start + point]; + let left_im = transform_im[left_start + point]; + let right_im = transform_im[right_start + point]; + transform_re[left_start + point] = left_re + right_re; + transform_re[right_start + point] = left_re - right_re; + transform_im[left_start + point] = left_im + right_im; + transform_im[right_start + point] = left_im - right_im; + } + } + } + stride *= 2; + } + } + } + + for entry in factorization.entries() { + let left_start = entry.left_group_index as usize * point_count; + let right_start = entry.right_group_index as usize * point_count; + for point in 0..point_count { + let product_re = transform_re[left_start + point].mul_add( + transform_re[right_start + point], + transform_im[left_start + point] * transform_im[right_start + point], + ); + let product_im = transform_im[left_start + point].mul_add( + transform_re[right_start + point], + -transform_re[left_start + point] * transform_im[right_start + point], + ); + let contracted = entry + .coefficient_re + .mul_add(product_re, -entry.coefficient_im * product_im); + for physical_helicity in physics.helicity_orbit_members(helicity_index) { + if !recurrence_helicity_is_selected( + physics, + selected_helicities, + *physical_helicity, + ) { + continue; + } + let scale = physics.manifest.helicities[*physical_helicity].coefficient + * normalization_factor; + accumulate(point, *physical_helicity, scale * contracted); + } + } + } + } + Ok(()) +} + +fn union_destination_ids( + plan: &DirectRecurrencePlan, + public_flow_ids: &[u32], +) -> RusticolResult> { + if plan.strategy() != RecurrenceStrategy::AllFlowUnion { + return Err(RusticolError::integrity( + "union destination mapping requires an all-flow-union plan", + )); + } + let mut destination_by_sector = BTreeMap::new(); + for destination in plan.amplitude_destinations() { + if destination.target_helicity_id_or_sentinel != DIRECT_NONE_U32 { + return Err(RusticolError::integrity( + "all-flow-union amplitude destination fixes a numerical helicity", + )); + } + if destination_by_sector + .insert(destination.target_sector_id, destination.id) + .is_some() + { + return Err(RusticolError::integrity( + "all-flow-union repeats a physical-flow destination", + )); + } + } + let result = public_flow_ids + .iter() + .map(|sector_id| { + destination_by_sector + .get(sector_id) + .copied() + .ok_or_else(|| { + RusticolError::integrity( + "all-flow-union public flow has no amplitude destination", + ) + }) + }) + .collect::>>()?; + if result.iter().copied().collect::>() + != destination_by_sector + .values() + .copied() + .collect::>() + { + return Err(RusticolError::integrity( + "all-flow-union amplitude destinations do not match the public flow axis", + )); + } + Ok(result) +} + +fn validate_replay_destination_helicity_mappings( + plan: &DirectRecurrencePlan, + replay_selectors: &[DirectReplaySelectorPlan], + direct_helicity_to_physics: &[usize], +) -> RusticolResult<()> { + for destination in plan.amplitude_destinations() { + replay_target_helicity_index( + destination.target_helicity_id_or_sentinel, + plan.resolved_helicities().len(), + )?; + } + + for selector in replay_selectors { + if selector.helicity_map().len() != plan.resolved_helicities().len() { + return Err(RusticolError::integrity(format!( + "recurrence replay flow {} has incomplete helicity coverage", + selector.public_flow_id() + ))); + } + for mapped_direct_id in selector.helicity_map() { + replay_mapped_direct_physics_helicity(direct_helicity_to_physics, *mapped_direct_id)?; + } + } + Ok(()) +} + +/// Resolve selection in the physical helicity domain, after the requested +/// flow's relabelling. A parity/numerical alias requests its computed +/// representative's destination; a structural zero requests no work. +fn selected_replay_destination_ids( + destinations: &[DirectAmplitudeDestinationDescriptor], + representative_flow_id: u32, + replay_helicity_map: &[u32], + direct_helicity_to_physics: &[usize], + reduction: LcSelectorReductionView<'_>, + selected_helicities: &BTreeSet, +) -> RusticolResult> { + let mut selected = Vec::new(); + for destination in destinations { + if destination.target_sector_id != representative_flow_id { + continue; + } + let physics_helicity = replay_destination_physics_helicity( + replay_helicity_map, + direct_helicity_to_physics, + destination.target_helicity_id_or_sentinel, + )?; + let helicity = reduction.helicity(physics_helicity); + if helicity.computed + && !helicity.structural_zero + && helicity.coefficient != 0.0 + && reduction.helicity_orbit_weight(Some(selected_helicities), physics_helicity) != 0.0 + { + selected.push(destination.id); + } + } + Ok(selected) +} + +fn replay_destination_physics_helicity( + replay_helicity_map: &[u32], + direct_helicity_to_physics: &[usize], + target_helicity_id_or_sentinel: u32, +) -> RusticolResult { + let target_helicity_index = + replay_target_helicity_index(target_helicity_id_or_sentinel, replay_helicity_map.len())?; + let mapped_direct_id = replay_helicity_map[target_helicity_index]; + replay_mapped_direct_physics_helicity(direct_helicity_to_physics, mapped_direct_id) +} + +fn replay_target_helicity_index( + target_helicity_id_or_sentinel: u32, + helicity_count: usize, +) -> RusticolResult { + if target_helicity_id_or_sentinel == DIRECT_NONE_U32 { + return Err(RusticolError::integrity( + "topology-replay amplitude destination lacks a resolved helicity", + )); + } + let target_helicity_index = target_helicity_id_or_sentinel as usize; + if target_helicity_index >= helicity_count { + return Err(RusticolError::integrity( + "recurrence amplitude destination helicity is not replay-mapped", + )); + } + Ok(target_helicity_index) +} + +fn replay_mapped_direct_physics_helicity( + direct_helicity_to_physics: &[usize], + mapped_direct_id: u32, +) -> RusticolResult { + direct_helicity_to_physics + .get(mapped_direct_id as usize) + .copied() + .ok_or_else(|| { + RusticolError::integrity("recurrence replay helicity has no public physics mapping") + }) +} + +#[cfg(test)] +mod replay_destination_helicity_tests { + use super::*; + + fn auxiliary_physical_destinations(count: u32) -> Vec { + (0..count) + .map(|id| DirectAmplitudeDestinationDescriptor { + closure_row_start: 0, + id, + target_sector_id: id, + target_helicity_id_or_sentinel: DIRECT_NONE_U32, + closure_row_count: 0, + selector_domain_id: 0, + }) + .collect() + } + + #[test] + fn primary_local_color_view_uses_dense_owner_ranks_and_supports_expanded_one_component() { + let contraction = + RecurrenceColorContraction::symmetric_group_s3_for_runtime_test((0..13).collect(), 13) + .with_sparse_sector_domain_for_runtime_test(); + let mut auxiliary = auxiliary_physical_destinations(13); + for destination in &mut auxiliary { + destination.id = 12 - destination.id; + } + auxiliary.reverse(); + let view = primary_local_color_destination_map(&contraction, &auxiliary).unwrap(); + assert_eq!( + view.auxiliary_destination_by_local_group.as_ref(), + &[12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0] + ); + + let expanded = RecurrenceColorContraction::expanded_identity_for_runtime_test(); + let expanded_view = + primary_local_color_destination_map(&expanded, &auxiliary_physical_destinations(1)) + .unwrap(); + assert_eq!( + expanded_view.auxiliary_destination_by_local_group.as_ref(), + &[0] + ); + assert_eq!( + expanded_view.local_group_by_primary_destination.as_ref(), + &[0] + ); + } + + #[test] + fn primary_local_color_view_rejects_reordered_auxiliary_sector_identity() { + let contraction = + RecurrenceColorContraction::symmetric_group_s3_for_runtime_test((0..13).collect(), 13) + .with_sparse_sector_domain_for_runtime_test(); + let mut auxiliary = auxiliary_physical_destinations(13); + auxiliary[0].target_sector_id = 1; + let error = match primary_local_color_destination_map(&contraction, &auxiliary) { + Ok(_) => panic!("duplicate/missing dense auxiliary owner must fail closed"), + Err(error) => error, + }; + assert!( + error + .message() + .contains("repeats an auxiliary physical sector") + ); + } + + #[test] + fn persisted_companion_census_keeps_legacy_outer_contract() { + let cold = RecurrenceNativeRuntime::persisted_helicity_selector_state_census( + "gg_to_gg", + PersistedHelicityFamilyCacheCensusV1::default(), + None, + ); + assert_eq!(cold["kind"], "rusticol-on-the-fly-runtime-state-census-v1"); + assert_eq!(cold["family_cache_policy"], "last-family-only"); + assert_eq!(cold["family_cache_limit"], 1); + assert_eq!(cold["retained_family_count"], 0); + assert_eq!(cold["retained_executor_handle_count"], 0); + assert!(cold["active_family_union_census"].is_null()); + + let warm = RecurrenceNativeRuntime::persisted_helicity_selector_state_census( + "gg_to_gg", + PersistedHelicityFamilyCacheCensusV1 { + resolved_helicity_count: 4, + retained_family_count: 1, + retained_row_count: 17, + active_resolved_helicity_id: Some(2), + }, + Some(PersistedHelicityFamilyInspectionCensusV1 { + query_count: 3, + union_unique_current_count: 5, + union_unique_current_component_count: 9, + union_source_rows: 2, + union_contribution_rows: 7, + union_finalization_rows: 2, + union_closure_rows: 3, + union_amplitude_destination_count: 3, + union_source_executor_call_groups: 1, + union_contribution_executor_call_groups: 2, + union_finalization_executor_call_groups: 1, + union_closure_executor_call_groups: 1, + semantic_executor_binding_count: 4, + }), + ); + assert_eq!(warm["process_preparation_count"], 1); + assert_eq!(warm["retained_family_count"], 1); + assert_eq!(warm["retained_selection_count"], 1); + assert_eq!(warm["retained_request_count"], 3); + assert_eq!(warm["retained_amplitude_destination_count"], 3); + assert_eq!(warm["retained_executor_handle_count"], 1); + assert_eq!(warm["semantic_executor_binding_count"], 4); + assert_eq!( + warm["active_family_union_census"]["union_unique_current_count"], + 5 + ); + assert_eq!(warm["active_family_union_census"]["union_closure_rows"], 3); + let fields = warm + .as_object() + .expect("persisted census must be an object") + .keys() + .map(String::as_str) + .collect::>(); + assert_eq!( + fields, + BTreeSet::from([ + "kind", + "process_id", + "family_cache_policy", + "family_cache_limit", + "process_preparation_count", + "retained_family_count", + "pending_family_count", + "retained_selection_count", + "retained_request_count", + "retained_amplitude_destination_count", + "retained_executor_handle_count", + "retained_query_local_trace_count", + "retained_embedded_lookup_key_count", + "semantic_executor_binding_count", + "active_family_union_census", + ]) + ); + } + + #[test] + fn prepared_parameter_projection_validates_before_modifying_planes() { + let defaults = [ + crate::EagerComplex64::new(1.0, -1.0), + crate::EagerComplex64::new(2.0, -2.0), + ]; + let runtime_values = [11.0, 22.0]; + let mut real = [91.0, 92.0]; + let mut imaginary = [-91.0, -92.0]; + let invalid = [PreparedParameterProjectionEntry { + runtime_slot: 2, + prepared_slot: 0, + component: 0, + }]; + + initialize_prepared_parameter_planes( + &defaults, + &invalid, + &runtime_values, + &mut real, + &mut imaginary, + ) + .expect_err("invalid projection must fail closed"); + assert_eq!(real, [91.0, 92.0]); + assert_eq!(imaginary, [-91.0, -92.0]); + + let projection = [ + PreparedParameterProjectionEntry { + runtime_slot: 1, + prepared_slot: 0, + component: 0, + }, + PreparedParameterProjectionEntry { + runtime_slot: 0, + prepared_slot: 1, + component: 1, + }, + ]; + initialize_prepared_parameter_planes( + &defaults, + &projection, + &runtime_values, + &mut real, + &mut imaginary, + ) + .unwrap(); + assert_eq!(real, [22.0, 2.0]); + assert_eq!(imaginary, [-1.0, 11.0]); + } + + #[test] + fn resolved_lc_output_layout_is_point_helicity_color_major() { + let layout = LcResolvedOutputLayout::new(2, 3, 4).unwrap(); + assert_eq!(layout.output_len().unwrap(), 24); + assert_eq!(layout.index(0, 0, 0).unwrap(), 0); + assert_eq!(layout.index(0, 1, 0).unwrap(), 4); + assert_eq!(layout.index(1, 0, 0).unwrap(), 12); + assert_eq!(layout.index(1, 2, 3).unwrap(), 23); + assert!(layout.index(2, 0, 0).is_err()); + assert!(LcResolvedOutputLayout::new(1, 0, 1).is_err()); + } + + #[test] + fn compact_lc_selector_reduction_view_matches_topology_replay_semantics() { + let helicities = vec![ + crate::Helicity { + id: "h0".into(), + index: 0, + values: vec![1, -1], + computed: true, + structural_zero: false, + representative_id: "h0".into(), + coefficient: 0.25, + }, + crate::Helicity { + id: "h1".into(), + index: 1, + values: vec![-1, 1], + computed: false, + structural_zero: false, + representative_id: "h0".into(), + coefficient: 0.75, + }, + ]; + let colors = vec![ + crate::ColorComponent::LcFlow(crate::LcColorFlow { + id: "c0".into(), + index: 0, + word: vec![0, 1], + computed: true, + representative_id: "c0".into(), + coefficient: 2.0, + }), + crate::ColorComponent::LcFlow(crate::LcColorFlow { + id: "c1".into(), + index: 1, + word: vec![1, 0], + computed: false, + representative_id: "c0".into(), + coefficient: 3.0, + }), + ]; + let helicity_ids = BTreeMap::from([("h0".into(), 0), ("h1".into(), 1)]); + let color_ids = BTreeMap::from([("c0".into(), 0), ("c1".into(), 1)]); + let orbits = vec![vec![0, 1], Vec::new()]; + let view = LcSelectorReductionView::from_parts( + &helicities, + &colors, + &helicity_ids, + &color_ids, + &orbits, + ); + let selected_helicities = BTreeSet::from(["h1".to_string()]); + let selected_colors = BTreeSet::from(["c0".to_string()]); + + view.validate_selector_ids(Some(&selected_helicities), Some(&selected_colors)) + .unwrap(); + assert_eq!( + view.selected_helicity_indices(Some(&selected_helicities)) + .unwrap(), + [1] + ); + assert_eq!( + view.selected_color_indices(Some(&selected_colors)).unwrap(), + [0] + ); + assert_eq!(view.helicity_orbit_weight(None, 0), 1.0); + assert_eq!( + view.helicity_orbit_weight(Some(&selected_helicities), 0), + 0.75 + ); + assert!(view.color_is_computed(0)); + assert!(!view.color_is_computed(1)); + assert!(view.color_is_selected(Some(&selected_colors), 0)); + assert!( + view.validate_selector_ids(Some(&BTreeSet::from(["missing".into()])), None) + .is_err() + ); + } + + #[test] + fn selected_replay_destinations_follow_flow_permutation_and_physical_orbits() { + let helicities = (0..5) + .map(|index| crate::Helicity { + id: format!("h{index}"), + index, + values: Vec::new(), + computed: !matches!(index, 1 | 3), + structural_zero: index == 3, + representative_id: format!("h{}", if index == 1 { 0 } else { index }), + coefficient: if index == 4 { 0.0 } else { 1.0 }, + }) + .collect::>(); + let helicity_ids = helicities + .iter() + .enumerate() + .map(|(index, helicity)| (helicity.id.clone(), index)) + .collect::>(); + let colors = BTreeMap::new(); + let orbits = vec![vec![0, 1], vec![], vec![2], vec![], vec![4]]; + let reduction = + LcSelectorReductionView::from_parts(&helicities, &[], &helicity_ids, &colors, &orbits); + let mut destinations = (0..4) + .map(|id| DirectAmplitudeDestinationDescriptor { + id, + target_sector_id: 7, + target_helicity_id_or_sentinel: id, + closure_row_start: 0, + closure_row_count: 0, + selector_domain_id: 0, + }) + .collect::>(); + destinations.push(DirectAmplitudeDestinationDescriptor { + id: 4, + target_sector_id: 8, + target_helicity_id_or_sentinel: 0, + closure_row_start: 0, + closure_row_count: 0, + selector_domain_id: 0, + }); + let selected = |ids: &[&str]| { + selected_replay_destination_ids( + &destinations, + 7, + &[1, 0, 2, 3], + &[0, 2, 3, 4], + reduction, + &ids.iter().map(|id| (*id).to_string()).collect(), + ) + .unwrap() + }; + + // Selecting an uncomputed physical alias must retain the current of + // its computed representative, after the non-identity flow map. + assert_eq!(selected(&["h1"]), [1]); + assert_eq!(selected(&["h0"]), [1]); + assert_eq!(selected(&["h2"]), [0]); + assert_eq!(selected(&["h1", "h2"]), [0, 1]); + assert_eq!(selected(&["h0", "h1"]), [1]); + assert!(selected(&["h3"]).is_empty()); + assert!(selected(&["h4"]).is_empty()); + assert!(selected(&[]).is_empty()); + let invalid = BTreeSet::from(["not-a-helicity".to_string()]); + assert!( + reduction + .validate_selector_ids(Some(&invalid), None) + .is_err() + ); + } + + #[test] + fn unprofiled_replay_selected_colors_are_sorted_by_public_index() { + let helicity_ids = BTreeMap::new(); + let color_ids = BTreeMap::from([ + ("alpha".to_string(), 4), + ("middle".to_string(), 0), + ("zeta".to_string(), 2), + ]); + let view = LcSelectorReductionView::from_parts(&[], &[], &helicity_ids, &color_ids, &[]); + let selected = BTreeSet::from([ + "alpha".to_string(), + "middle".to_string(), + "zeta".to_string(), + ]); + let mut scratch = vec![usize::MAX]; + + view.retain_selected_color_indices(&selected, &mut scratch) + .unwrap(); + + assert_eq!(scratch, [0, 2, 4]); + let mut visited = Vec::new(); + visit_replay_color_indices(5, Some(&scratch), |color_index| { + visited.push(color_index); + Ok(()) + }) + .unwrap(); + assert_eq!(visited, scratch); + } + + #[test] + fn unprofiled_replay_all_colors_keep_public_axis_order() { + let mut visited = Vec::new(); + visit_replay_color_indices(4, None, |color_index| { + visited.push(color_index); + Ok(()) + }) + .unwrap(); + assert_eq!(visited, [0, 1, 2, 3]); + } + + #[test] + fn unprofiled_replay_color_scratch_retains_allocation() { + let helicity_ids = BTreeMap::new(); + let color_ids = BTreeMap::from([ + ("c0".to_string(), 0), + ("c1".to_string(), 1), + ("c2".to_string(), 2), + ("c3".to_string(), 3), + ]); + let view = LcSelectorReductionView::from_parts(&[], &[], &helicity_ids, &color_ids, &[]); + let all = BTreeSet::from([ + "c0".to_string(), + "c1".to_string(), + "c2".to_string(), + "c3".to_string(), + ]); + let subset = BTreeSet::from(["c1".to_string(), "c3".to_string()]); + let mut scratch = Vec::new(); + + view.retain_selected_color_indices(&all, &mut scratch) + .unwrap(); + let capacity = scratch.capacity(); + let allocation = scratch.as_ptr(); + view.retain_selected_color_indices(&subset, &mut scratch) + .unwrap(); + + assert_eq!(scratch, [1, 3]); + assert_eq!(scratch.capacity(), capacity); + assert_eq!(scratch.as_ptr(), allocation); + } + + #[test] + fn composes_distinct_replay_flow_permutations_without_destination_tables() { + let direct_helicity_to_physics = [2, 0, 1]; + let flow_zero = [0, 1, 2]; + let flow_one = [2, 0, 1]; + + for (replay_helicity_map, expected) in + [(&flow_zero[..], [2, 0, 1]), (&flow_one[..], [1, 2, 0])] + { + let actual = (0..3) + .map(|target_helicity_id| { + replay_destination_physics_helicity( + replay_helicity_map, + &direct_helicity_to_physics, + target_helicity_id, + ) + .expect("compose authenticated replay and physics mappings") + }) + .collect::>(); + assert_eq!(actual, expected); + } + } + + #[test] + fn rejects_absent_or_out_of_range_replay_helicity_mappings() { + let missing = replay_destination_physics_helicity(&[0], &[0], DIRECT_NONE_U32) + .expect_err("sentinel destination helicity must fail closed"); + assert_eq!( + missing.message(), + "topology-replay amplitude destination lacks a resolved helicity" + ); + + let incomplete = replay_destination_physics_helicity(&[0], &[0], 1) + .expect_err("incomplete replay permutation must fail closed"); + assert_eq!( + incomplete.message(), + "recurrence amplitude destination helicity is not replay-mapped" + ); + + let out_of_range = replay_destination_physics_helicity(&[1], &[0], 0) + .expect_err("out-of-range direct helicity must fail closed"); + assert_eq!( + out_of_range.message(), + "recurrence replay helicity has no public physics mapping" + ); + } + + #[test] + fn symmetric_group_recurrence_workspace_is_lazy_and_first_allocation_is_bounded() { + let contraction = + RecurrenceColorContraction::symmetric_group_s3_for_runtime_test((0..13).collect(), 13); + let RuntimeColorContractionReducer::SymmetricGroupFourier(reducer) = + contraction.runtime_reducer().unwrap() + else { + panic!("expected symmetric-group runtime reducer") + }; + let (effective_point_tile_size, workspace) = + lazy_symmetric_group_workspace_for_point_tile(reducer, 5).unwrap(); + assert_eq!(effective_point_tile_size, 5); + assert!(workspace.is_none()); + + let mut workspace = None; + ensure_symmetric_group_color_workspace_capacity(&mut workspace, reducer, 1).unwrap(); + assert_eq!(workspace.as_ref().unwrap().lane_capacity(), 1); + + ensure_symmetric_group_color_workspace_capacity(&mut workspace, reducer, 1).unwrap(); + assert_eq!(workspace.as_ref().unwrap().lane_capacity(), 1); + + ensure_symmetric_group_color_workspace_capacity(&mut workspace, reducer, 3).unwrap(); + assert_eq!(workspace.as_ref().unwrap().lane_capacity(), 3); + + ensure_symmetric_group_color_workspace_capacity(&mut workspace, reducer, 2).unwrap(); + assert_eq!(workspace.as_ref().unwrap().lane_capacity(), 3); + } + + #[test] + fn recurrence_external_planes_match_nested_contiguous_and_crossed_views() { + let source_count = 3; + let values = (0..7 * source_count * 4) + .map(|index| { + if index % 9 == 0 { + -0.0 + } else { + index as f64 + 0.5 + } + }) + .collect::>(); + let crossing = [ + InputCrossingMapEntry { + target_index: 0, + source_index: 2, + sign: -1.0, + }, + InputCrossingMapEntry { + target_index: 1, + source_index: 0, + sign: 1.0, + }, + InputCrossingMapEntry { + target_index: 2, + source_index: 1, + sign: -1.0, + }, + ]; + for lookup in [None, Some(crossing.as_slice())] { + let contiguous = F64MomentumBatchView::from_contiguous_prevalidated( + &values, + 7, + source_count, + lookup, + ) + .unwrap(); + let nested = contiguous.materialize_nested(); + let nested_view = F64MomentumBatchView::from_nested(&nested, source_count).unwrap(); + for (start, stop) in [(0, 7), (2, 6), (6, 7)] { + let count = stop - start; + let mut expected = vec![0.0_f64; source_count * 4 * count]; + for source in 0..source_count { + for component in 0..4 { + for point in 0..count { + expected[(source * 4 + component) * count + point] = + nested[start + point][source][component]; + } + } + } + for view in [contiguous, nested_view] { + let view = view.subview(start, stop).unwrap(); + let mut source_major = vec![17.0; expected.len() + 8]; + let mut point_major = vec![17.0; expected.len() + 8]; + for point in 0..count { + for source in 0..source_count { + let momentum = view.point(point).momentum(source).unwrap(); + write_external_momentum::( + &mut source_major, + count, + source_count, + point, + source, + &momentum, + ); + write_external_momentum::( + &mut point_major, + count, + source_count, + point, + source, + &momentum, + ); + assert_eq!( + point_major[(point * source_count + source) * 4 + ..(point * source_count + source + 1) * 4], + momentum + ); + } + } + assert_eq!( + source_major[..expected.len()] + .iter() + .map(|value| value.to_bits()) + .collect::>(), + expected + .iter() + .map(|value| value.to_bits()) + .collect::>() + ); + assert!( + source_major[expected.len()..] + .iter() + .all(|&value| value == 17.0) + ); + assert!( + point_major[expected.len()..] + .iter() + .all(|&value| value == 17.0) + ); + } + } + } + } + + #[test] + fn recurrence_external_momentum_workspace_starts_singleton_and_grows_without_shrinking() { + let external_source_count = 4; + let maximum_point_count = 5; + let mut workspace = Vec::new(); + + let singleton_len = ensure_external_momentum_workspace_capacity( + &mut workspace, + 1, + external_source_count, + maximum_point_count, + ) + .unwrap(); + assert_eq!(singleton_len, external_source_count * 4); + assert_eq!(workspace.len(), singleton_len); + workspace[0] = 17.0; + + let singleton_ptr = workspace.as_ptr(); + let singleton_capacity = workspace.capacity(); + ensure_external_momentum_workspace_capacity( + &mut workspace, + 1, + external_source_count, + maximum_point_count, + ) + .unwrap(); + assert_eq!(workspace.as_ptr(), singleton_ptr); + assert_eq!(workspace.capacity(), singleton_capacity); + + let grown_len = ensure_external_momentum_workspace_capacity( + &mut workspace, + 3, + external_source_count, + maximum_point_count, + ) + .unwrap(); + assert_eq!(grown_len, 3 * external_source_count * 4); + assert_eq!(workspace.len(), grown_len); + assert_eq!(workspace[0], 17.0); + assert!(workspace[singleton_len..].iter().all(|value| *value == 0.0)); + + let grown_ptr = workspace.as_ptr(); + let grown_capacity = workspace.capacity(); + ensure_external_momentum_workspace_capacity( + &mut workspace, + 2, + external_source_count, + maximum_point_count, + ) + .unwrap(); + assert_eq!(workspace.len(), grown_len); + assert_eq!(workspace.as_ptr(), grown_ptr); + assert_eq!(workspace.capacity(), grown_capacity); + + let error = ensure_external_momentum_workspace_capacity( + &mut workspace, + maximum_point_count + 1, + external_source_count, + maximum_point_count, + ) + .expect_err("workspace growth past the persistent tile limit must fail"); + assert_eq!( + error.message(), + "recurrence point tile exceeds its persistent workspace" + ); + assert_eq!(workspace.len(), grown_len); + } + + #[test] + fn symmetric_group_recurrence_dispatch_handles_sparse_owner_destinations_and_partial_tiles() { + let contraction = RecurrenceColorContraction::symmetric_group_s3_for_runtime_test( + (0..13).map(|group| group + 2).collect(), + 20, + ); + let destination_physics_helicity = + contracted_destination_helicity_map(&contraction, &[0]).unwrap(); + assert_eq!(destination_physics_helicity.len(), 20); + assert_eq!(destination_physics_helicity[..2], [usize::MAX; 2]); + assert!( + destination_physics_helicity[2..15] + .iter() + .all(|helicity| *helicity == 0) + ); + assert!( + destination_physics_helicity[15..] + .iter() + .all(|helicity| *helicity == usize::MAX) + ); + + let mut physics = PhysicsRuntime { + binding_id: 0, + manifest: ProcessPhysicsV1 { + schema_version: crate::RUNTIME_PHYSICS_SCHEMA_VERSION, + kind: "pyamplicol-resolved-physics".to_string(), + process_id: "fft-runtime-test".to_string(), + process: "g g > g g".to_string(), + color_accuracy: crate::ColorAccuracy::Full, + coverage: crate::Coverage { + helicities: "complete".to_string(), + color: "contracted".to_string(), + color_kind: "contracted-color".to_string(), + structural_zero_helicity_count: 0, + }, + external_particles: Vec::new(), + helicities: vec![crate::Helicity { + id: "h0".to_string(), + index: 0, + values: Vec::new(), + representative_id: "h0".to_string(), + computed: true, + structural_zero: false, + coefficient: 1.0, + }], + color_components: vec![crate::ColorComponent::ContractedColor( + crate::ContractedColor { + id: "contracted".to_string(), + index: 0, + description: "contracted color".to_string(), + }, + )], + reduction: crate::Reduction { + kind: crate::ReductionKind::ContractedColor, + groups: Vec::new(), + }, + model_parameters: Vec::new(), + selectors: crate::SelectorCapabilities { + helicity: true, + color_flow: false, + contracted_color: false, + }, + extensions: BTreeMap::new(), + }, + helicity_index_by_id: BTreeMap::from([("h0".to_string(), 0)]), + helicity_members_by_representative: vec![vec![0]], + color_index_by_id: BTreeMap::from([("contracted".to_string(), 0)]), + reduction_by_group_id: BTreeMap::new(), + numeric_reduction_by_group_id: BTreeMap::new(), + }; + let point_count = 3; + let point_stride = point_count; + let mut values_re = vec![0.0; 20 * point_stride]; + let mut values_im = vec![0.0; 20 * point_stride]; + let mut local_values = vec![(0.0, 0.0); 13 * point_count]; + for group in 0..13 { + for point in 0..point_count { + let value = ( + (group * 7 + point * 3) as f64 / 11.0 - 1.0, + (group * 5 + point) as f64 / 13.0 - 0.4, + ); + local_values[group * point_count + point] = value; + values_re[(group + 2) * point_stride + point] = value.0; + values_im[(group + 2) * point_stride + point] = value.1; + } + } + let tile = ContractedReplayTile { + values_re: &values_re, + values_im: &values_im, + point_count, + point_stride, + destination_count: 20, + }; + let RuntimeColorContractionReducer::SymmetricGroupFourier(reducer) = + contraction.runtime_reducer().unwrap() + else { + panic!("expected symmetric-group runtime reducer") + }; + let covered_group_count = reducer.channel_count() * reducer.group_order(); + assert!(reducer.channel_count() > 0); + assert!(reducer.local_group_count() > covered_group_count); + assert!(reducer.residual_entries().iter().any(|entry| { + entry.left_group_index < covered_group_count as u32 + && entry.right_group_index >= covered_group_count as u32 + })); + let expected = RecurrenceColorContraction::symmetric_group_s3_dense_for_runtime_test( + &local_values, + point_count, + ); + + let mut workspace = reducer.workspace(1).unwrap(); + let mut actual = vec![0.0; point_count]; + contract_color_tile( + &tile, + &contraction, + &destination_physics_helicity, + &physics, + None, + 1.0, + &mut [], + &mut [], + Some(&mut workspace), + |point, helicity, value| { + assert_eq!(helicity, 0); + actual[point] += value; + }, + ) + .unwrap(); + assert_eq!(workspace.lane_capacity(), point_count); + for (actual, expected) in actual.into_iter().zip(expected) { + let scale = expected.abs().max(1.0); + assert!((actual - expected).abs() <= 2.0e-11 * scale); + } + + physics.manifest.helicities[0].computed = false; + physics.manifest.helicities[0].structural_zero = true; + let selected = BTreeSet::from(["h0".to_string()]); + assert!( + !contracted_selection_has_computed_destination( + &destination_physics_helicity, + &physics, + &selected, + ) + .unwrap() + ); + } +} + +#[derive(Clone, Copy)] +struct DirectProfileSnapshot { + phases: DirectRuntimePhaseTimings, + roles: DirectExecutionRoleTimings, + execution: DirectExecutionCounters, + traffic: DirectArenaTrafficCounters, + activity: DirectRuntimeActivityCounters, + internal_scratch_bytes: u64, + internal_broadcast_bytes: u64, +} + +impl DirectProfileSnapshot { + fn capture( + runtime: &DirectRecurrenceExecutionRuntime, + owners: &NativeRecurrenceDirectExecutorOwners, + ) -> Self { + let (internal_scratch_bytes, internal_broadcast_bytes) = owners.internal_traffic_bytes(); + Self { + phases: runtime.phase_timings(), + roles: runtime.role_timings(), + execution: runtime.counters(), + traffic: runtime.traffic_counters(), + activity: runtime.activity_counters(), + internal_scratch_bytes, + internal_broadcast_bytes, + } + } +} + +#[derive(Clone, Copy, Default)] +pub(super) struct DirectProfileDelta { + pub(super) momentum_fill: Duration, + pub(super) union_source_fill: Duration, + pub(super) direct_execution: Duration, + pub(super) replay_output_mapping: Duration, + pub(super) source_kernel: Duration, + pub(super) contribution_kernel: Duration, + pub(super) finalization: Duration, + pub(super) closure: Duration, + pub(super) execution: DirectExecutionCounters, + pub(super) traffic: DirectArenaTrafficCounters, + pub(super) activity: DirectRuntimeActivityCounters, + pub(super) internal_scratch_bytes: u64, + pub(super) internal_broadcast_bytes: u64, +} + +fn add_persisted_execution_report( + target: &mut PersistedHelicityFamilyExecutionReportV1, + source: PersistedHelicityFamilyExecutionReportV1, +) { + target.cache_hit &= source.cache_hit; + target.source_calls = target.source_calls.saturating_add(source.source_calls); + target.source_rows = target.source_rows.saturating_add(source.source_rows); + target.contribution_calls = target + .contribution_calls + .saturating_add(source.contribution_calls); + target.contribution_rows = target + .contribution_rows + .saturating_add(source.contribution_rows); + target.finalization_calls = target + .finalization_calls + .saturating_add(source.finalization_calls); + target.finalization_rows = target + .finalization_rows + .saturating_add(source.finalization_rows); + target.closure_calls = target.closure_calls.saturating_add(source.closure_calls); + target.closure_rows = target.closure_rows.saturating_add(source.closure_rows); +} + +fn persisted_execution_report_aggregate() -> PersistedHelicityFamilyExecutionReportV1 { + PersistedHelicityFamilyExecutionReportV1 { + // Conjunction uses true as its identity, so an all-hit tile sequence + // remains a hit while any cold tile turns the aggregate into a miss. + cache_hit: true, + ..PersistedHelicityFamilyExecutionReportV1::default() + } +} + +#[allow(clippy::too_many_arguments)] +fn persisted_companion_profile( + total: Duration, + parameter_setup: Duration, + input_setup: Duration, + execution: Duration, + reduction: Duration, + momentum_scalar_values: u64, + report: PersistedHelicityFamilyExecutionReportV1, +) -> RuntimeProfile { + let calls = u64::from(report.source_calls) + + u64::from(report.contribution_calls) + + u64::from(report.finalization_calls) + + u64::from(report.closure_calls); + let rows = u64::from(report.source_rows) + + u64::from(report.contribution_rows) + + u64::from(report.finalization_rows) + + u64::from(report.closure_rows); + let schedule_executions = u64::from(report.source_calls); + direct_profile_from_delta( + total, + parameter_setup, + input_setup, + reduction, + DirectProfileDelta { + direct_execution: execution, + execution: DirectExecutionCounters { + source_calls: u64::from(report.source_calls), + source_rows: u64::from(report.source_rows), + contribution_calls: u64::from(report.contribution_calls), + contribution_rows: u64::from(report.contribution_rows), + finalization_calls: u64::from(report.finalization_calls), + finalization_rows: u64::from(report.finalization_rows), + closure_calls: u64::from(report.closure_calls), + closure_rows: u64::from(report.closure_rows), + ..DirectExecutionCounters::default() + }, + traffic: DirectArenaTrafficCounters { + calls, + rows, + ..DirectArenaTrafficCounters::default() + }, + activity: DirectRuntimeActivityCounters { + momentum_fill_calls: schedule_executions, + momentum_scalar_values_filled: momentum_scalar_values, + schedule_executions, + union_source_dispatch_calls: u64::from(report.source_calls), + union_source_rows: u64::from(report.source_rows), + union_schedule_executions: schedule_executions, + ..DirectRuntimeActivityCounters::default() + }, + ..DirectProfileDelta::default() + }, + ) +} + +fn direct_profile( + total: Duration, + parameter_setup: Duration, + external_momentum_flatten: Duration, + reduction: Duration, + before: DirectProfileSnapshot, + after: DirectProfileSnapshot, +) -> RuntimeProfile { + let momentum_fill = after + .phases + .momentum_fill + .saturating_sub(before.phases.momentum_fill); + let union_source_fill = after + .phases + .union_source_fill + .saturating_sub(before.phases.union_source_fill); + let direct_execution = after + .phases + .direct_execution + .saturating_sub(before.phases.direct_execution); + let replay_output_mapping = after + .phases + .replay_output_mapping + .saturating_sub(before.phases.replay_output_mapping); + let source_kernel = after.roles.source.saturating_sub(before.roles.source); + let contribution_kernel = after + .roles + .contribution + .saturating_sub(before.roles.contribution); + let finalization = after + .roles + .finalization + .saturating_sub(before.roles.finalization); + let closure = after.roles.closure.saturating_sub(before.roles.closure); + let execution = DirectExecutionCounters { + source_calls: after + .execution + .source_calls + .saturating_sub(before.execution.source_calls), + source_rows: after + .execution + .source_rows + .saturating_sub(before.execution.source_rows), + contribution_calls: after + .execution + .contribution_calls + .saturating_sub(before.execution.contribution_calls), + contribution_rows: after + .execution + .contribution_rows + .saturating_sub(before.execution.contribution_rows), + finalization_calls: after + .execution + .finalization_calls + .saturating_sub(before.execution.finalization_calls), + finalization_rows: after + .execution + .finalization_rows + .saturating_sub(before.execution.finalization_rows), + closure_calls: after + .execution + .closure_calls + .saturating_sub(before.execution.closure_calls), + closure_rows: after + .execution + .closure_rows + .saturating_sub(before.execution.closure_rows), + packed_input_bytes: after + .execution + .packed_input_bytes + .saturating_sub(before.execution.packed_input_bytes), + packed_output_bytes: after + .execution + .packed_output_bytes + .saturating_sub(before.execution.packed_output_bytes), + scatter_bytes: after + .execution + .scatter_bytes + .saturating_sub(before.execution.scatter_bytes), + }; + let traffic = DirectArenaTrafficCounters { + calls: after.traffic.calls.saturating_sub(before.traffic.calls), + rows: after.traffic.rows.saturating_sub(before.traffic.rows), + points: after.traffic.points.saturating_sub(before.traffic.points), + packet_input_bytes: after + .traffic + .packet_input_bytes + .saturating_sub(before.traffic.packet_input_bytes), + packet_output_bytes: after + .traffic + .packet_output_bytes + .saturating_sub(before.traffic.packet_output_bytes), + gather_bytes: after + .traffic + .gather_bytes + .saturating_sub(before.traffic.gather_bytes), + scatter_bytes: after + .traffic + .scatter_bytes + .saturating_sub(before.traffic.scatter_bytes), + remap_bytes: after + .traffic + .remap_bytes + .saturating_sub(before.traffic.remap_bytes), + }; + let internal_scratch_bytes = after + .internal_scratch_bytes + .saturating_sub(before.internal_scratch_bytes); + let internal_broadcast_bytes = after + .internal_broadcast_bytes + .saturating_sub(before.internal_broadcast_bytes); + let activity = DirectRuntimeActivityCounters { + momentum_fill_calls: after + .activity + .momentum_fill_calls + .saturating_sub(before.activity.momentum_fill_calls), + momentum_forms_filled: after + .activity + .momentum_forms_filled + .saturating_sub(before.activity.momentum_forms_filled), + momentum_terms_filled: after + .activity + .momentum_terms_filled + .saturating_sub(before.activity.momentum_terms_filled), + momentum_scalar_values_filled: after + .activity + .momentum_scalar_values_filled + .saturating_sub(before.activity.momentum_scalar_values_filled), + schedule_executions: after + .activity + .schedule_executions + .saturating_sub(before.activity.schedule_executions), + replay_schedule_executions: after + .activity + .replay_schedule_executions + .saturating_sub(before.activity.replay_schedule_executions), + replay_output_values_scaled: after + .activity + .replay_output_values_scaled + .saturating_sub(before.activity.replay_output_values_scaled), + union_source_dispatch_calls: after + .activity + .union_source_dispatch_calls + .saturating_sub(before.activity.union_source_dispatch_calls), + union_source_rows: after + .activity + .union_source_rows + .saturating_sub(before.activity.union_source_rows), + union_schedule_executions: after + .activity + .union_schedule_executions + .saturating_sub(before.activity.union_schedule_executions), + }; + direct_profile_from_delta( + total, + parameter_setup, + external_momentum_flatten, + reduction, + DirectProfileDelta { + momentum_fill, + union_source_fill, + direct_execution, + replay_output_mapping, + source_kernel, + contribution_kernel, + finalization, + closure, + execution, + traffic, + activity, + internal_scratch_bytes, + internal_broadcast_bytes, + }, + ) +} + +pub(super) fn direct_profile_from_delta( + total: Duration, + parameter_setup: Duration, + external_momentum_flatten: Duration, + reduction: Duration, + delta: DirectProfileDelta, +) -> RuntimeProfile { + RuntimeProfile { + momentum_setup_s: profile_duration_seconds(external_momentum_flatten + delta.momentum_fill), + momentum_input_setup_s: profile_duration_seconds(external_momentum_flatten), + model_parameter_setup_s: profile_duration_seconds(parameter_setup), + stage_evaluator_call_s: profile_duration_seconds(delta.direct_execution), + stage_evaluator_s: profile_duration_seconds(delta.direct_execution), + recurrence_momentum_fill_s: profile_duration_seconds(delta.momentum_fill), + recurrence_union_source_fill_s: profile_duration_seconds(delta.union_source_fill), + recurrence_schedule_s: profile_duration_seconds(delta.direct_execution), + recurrence_source_kernel_s: profile_duration_seconds(delta.source_kernel), + recurrence_contribution_kernel_s: profile_duration_seconds(delta.contribution_kernel), + recurrence_finalization_s: profile_duration_seconds(delta.finalization), + recurrence_closure_s: profile_duration_seconds(delta.closure), + recurrence_replay_output_mapping_s: profile_duration_seconds(delta.replay_output_mapping), + recurrence_momentum_scalar_value_count: delta.activity.momentum_scalar_values_filled, + recurrence_schedule_execution_count: delta.activity.schedule_executions, + recurrence_replay_schedule_execution_count: delta.activity.replay_schedule_executions, + recurrence_union_schedule_execution_count: delta.activity.union_schedule_executions, + recurrence_union_source_row_count: delta.activity.union_source_rows, + recurrence_replay_output_value_count: delta.activity.replay_output_values_scaled, + recurrence_source_call_count: delta.execution.source_calls, + recurrence_source_row_count: delta.execution.source_rows, + recurrence_contribution_call_count: delta.execution.contribution_calls, + recurrence_contribution_row_count: delta.execution.contribution_rows, + recurrence_finalization_call_count: delta.execution.finalization_calls, + recurrence_finalization_row_count: delta.execution.finalization_rows, + recurrence_closure_call_count: delta.execution.closure_calls, + recurrence_closure_row_count: delta.execution.closure_rows, + recurrence_direct_packed_input_bytes: delta.execution.packed_input_bytes, + recurrence_direct_packed_output_bytes: delta.execution.packed_output_bytes, + // Fanout broadcasts stay within the persistent Direct-Arena current + // workspace. They are not the retired evaluator-boundary scatter. + recurrence_direct_scatter_bytes: 0, + recurrence_direct_packet_input_bytes: delta.traffic.packet_input_bytes, + recurrence_direct_packet_output_bytes: delta.traffic.packet_output_bytes, + recurrence_direct_gather_bytes: delta.traffic.gather_bytes, + recurrence_direct_traffic_scatter_bytes: delta.traffic.scatter_bytes, + recurrence_direct_remap_bytes: delta.traffic.remap_bytes, + recurrence_internal_scratch_bytes: delta.internal_scratch_bytes, + recurrence_internal_broadcast_bytes: delta + .internal_broadcast_bytes + .saturating_add(delta.execution.scatter_bytes), + reduction_s: profile_duration_seconds(reduction), + total_s: profile_duration_seconds(total), + ..RuntimeProfile::default() + } +} + +#[cfg(test)] +mod direct_profile_tests { + use super::*; + + #[test] + fn persisted_cache_hit_aggregation_is_true_only_when_every_tile_hits() { + let mut all_hits = persisted_execution_report_aggregate(); + add_persisted_execution_report( + &mut all_hits, + PersistedHelicityFamilyExecutionReportV1 { + cache_hit: true, + ..PersistedHelicityFamilyExecutionReportV1::default() + }, + ); + add_persisted_execution_report( + &mut all_hits, + PersistedHelicityFamilyExecutionReportV1 { + cache_hit: true, + ..PersistedHelicityFamilyExecutionReportV1::default() + }, + ); + assert!(all_hits.cache_hit); + + add_persisted_execution_report( + &mut all_hits, + PersistedHelicityFamilyExecutionReportV1::default(), + ); + assert!(!all_hits.cache_hit); + } + + #[test] + fn fanout_scatter_is_reported_as_internal_broadcast_traffic() { + let profile = direct_profile_from_delta( + Duration::ZERO, + Duration::ZERO, + Duration::ZERO, + Duration::ZERO, + DirectProfileDelta { + execution: DirectExecutionCounters { + scatter_bytes: 96, + ..DirectExecutionCounters::default() + }, + internal_broadcast_bytes: 32, + ..DirectProfileDelta::default() + }, + ); + + assert_eq!(profile.recurrence_direct_scatter_bytes, 0); + assert_eq!(profile.recurrence_internal_broadcast_bytes, 128); + profile + .validate_recurrence_direct_boundary_traffic() + .unwrap(); + } +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/engine/recurrence_load.rs b/pyamplicol/rust/crates/rusticol-core/src/engine/recurrence_load.rs new file mode 100644 index 0000000000..943c4722c7 --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/engine/recurrence_load.rs @@ -0,0 +1,5434 @@ +// SPDX-License-Identifier: 0BSD + +//! Native loading of compact recurrence artifacts. + +use super::eager_manifest::PreparedKernelPackManifest; +use super::evaluator::recurrence_source_direct::{ + DirectSourceDispatchDomainSpec, DirectSourceDispatchKey, DirectSourceDispatchVariantSpec, + DirectSourceOrientation, DirectSourceTemplateSpec, DirectSourceWavefunctionFamily, +}; +use super::recurrence_backend::NativeRecurrenceDirectExecutorBackend; +#[cfg(feature = "on-the-fly-test-support")] +use super::recurrence_backend::NativeRecurrencePreparedExecutorPool; +#[cfg(feature = "python-generation-bridge")] +use super::recurrence_bootstrap::{ + RecurrenceReadyCompanionV1, RecurrenceReadyRuntimeV1, RecurrenceReadySourceTemplateV1, + RecurrenceReadySourceVariantV1, +}; +use super::recurrence_bootstrap::{ + RecurrenceReadyExecutionV1, RecurrenceReadyPlanV1, RecurrenceReadySourceDomainV1, + RecurrenceReadySourceFamilyV1, RecurrenceReadySourceKeyV1, RecurrenceReadySourceOrientationV1, +}; +use super::recurrence_manifest::*; +use super::recurrence_process_pack::{ + ProcessDirectExecutorBinding, ProcessDirectExecutorPack, ProcessExecutorBackend, + ProcessExecutorIdentities, ProcessExecutorTarget, RECURRENCE_PROCESS_BINDING_HEADER_SIZE_V4, + RECURRENCE_PROCESS_BINDING_MAGIC_V4, RECURRENCE_PROCESS_BINDING_VERSION_V4, + decode_process_executor_descriptors, read_string, semantic_digest_from_bytes, +}; +use super::*; +use crate::pacbin::{PacbinMemberKind, PacbinReader}; +#[cfg(feature = "on-the-fly-test-support")] +use crate::recurrence::direct_backend::{ + DirectCurrentObservation, begin_direct_current_observation, take_direct_current_observation, +}; +#[cfg(feature = "on-the-fly-test-support")] +use crate::recurrence::direct_runtime::{ + DirectRecurrenceExecutionRuntime, DirectReplaySelectorPlan, +}; +#[cfg(test)] +use crate::recurrence::on_the_fly::OnTheFlySourceExecutionSpecV1; +#[cfg(feature = "on-the-fly-test-support")] +use crate::recurrence::on_the_fly::hash_current_key; +#[cfg(feature = "on-the-fly-test-support")] +use crate::recurrence::on_the_fly::{ + ON_THE_FLY_WORK_CENSUS_BASIS_V1, OnTheFlyForbiddenWorkGuardV1, OnTheFlyQueryFamilyExecutorV1, + OnTheFlySelectedQueryTraceV1, OnTheFlyStructuralInterpreter, OnTheFlyWorkspaceV1, + QueryFamilyTraceInput, build_on_the_fly_selected_trace_v1, +}; +use crate::recurrence::on_the_fly::{ + OnTheFlyProcessSeedV1, OnTheFlySourceOrientationV1, OnTheFlySourceWavefunctionFamilyV1, +}; +#[cfg(feature = "on-the-fly-test-support")] +use crate::recurrence::{AuthenticatedRecurrenceBuilderInput, PreparedDirectExecutorCatalog}; +use crate::recurrence::{ + DirectRecurrencePlan, FactorizedColorContractionKind, + RECURRENCE_COLOR_PROJECTION_CERTIFICATE_MEMBER, RECURRENCE_DIRECT_SCHEDULE_MEMBER, + RecurrenceColorContraction, SemanticDigest, decode_recurrence_color_contraction_v3, + decode_recurrence_direct_plan_v2, recurrence_color_contraction_digest, + validate_recurrence_color_projection_certificate, +}; + +#[cfg(feature = "on-the-fly-test-support")] +const ON_THE_FLY_FAMILY_WORK_CENSUS_BASIS_V1: &str = "shared-query-family-union-v1"; + +pub(super) struct LoadedRecurrenceRuntime { + pub(super) common: ExecutionRuntime, + pub(super) lane: RecurrenceNativeRuntime, +} + +fn companion_helicity_coefficient_is_canonical(structural_zero: bool, coefficient: f64) -> bool { + coefficient == if structural_zero { 0.0 } else { 1.0 } +} + +pub(super) struct LoadedRecurrencePlan { + pub(super) plan: DirectRecurrencePlan, + pub(super) process_executors: ProcessDirectExecutorPack, +} + +struct LoadedRecurrenceProcessBinding { + remap: RecurrenceProcessRemap, + process_executors: ProcessDirectExecutorPack, +} + +pub(super) fn load_recurrence_native_runtime( + artifact: &VerifiedArtifact, + evaluator_root: &Path, + manifest: &RecurrenceExecutionManifest, + physics: &ProcessPhysicsV1, + _selection: &crate::ArtifactSelection, +) -> RusticolResult { + let loaded_plan = load_plan(artifact, evaluator_root, manifest)?; + let plan = loaded_plan.plan; + let (mut common, parameter_defaults, parameter_projection, source_domains) = + build_common_runtime(&plan, manifest, physics)?; + let payload_root = recurrence_payload_root(artifact, manifest)?; + let kernel_payloads = artifact.evaluator_payload_store(&payload_root)?; + if manifest.helicity_selector_companion.is_some() { + validate_recurrence_companion_physics(physics)?; + } + common.model_parameter_evaluator = if common + .model_parameters + .iter() + .any(|parameter| parameter.kind == "derived_parameter_component") + { + let (pack_bytes, mut pack, fallback_payload_root) = load_prepared_pack(artifact, manifest)?; + drop(pack_bytes); + drop(pack.recurrence_template.take()); + drop(pack.recurrence_direct_template.take()); + if fallback_payload_root != payload_root { + return Err(RusticolError::integrity( + "recurrence parameter fallback resolved a different prepared payload root", + )); + } + super::eager_load::load_prepared_model_parameter_evaluator_for_runtime( + &pack, + &common.model_parameters, + &kernel_payloads, + )? + } else { + None + }; + common.refresh_derived_model_parameters()?; + let helicity_selector_companion = if manifest.helicity_selector_companion.is_some() { + Some( + super::on_the_fly_load::load_recurrence_helicity_selector_companion( + artifact, + evaluator_root, + manifest, + physics, + &kernel_payloads, + &common, + )? + .runtime, + ) + } else { + None + }; + let loaded_backend = NativeRecurrenceDirectExecutorBackend::load_from_process_pack( + &loaded_plan.process_executors, + &kernel_payloads, + &plan, + source_domains, + )?; + let (executors, backend_owners) = loaded_backend.into_parts(); + let public_flow_ids = public_flow_ids(&plan, &manifest.runtime_metadata, physics)?; + let direct_helicity_to_physics = direct_helicity_to_physics(&plan, physics)?; + let color_contraction = load_color_contraction(artifact, evaluator_root, manifest)?; + let lane = RecurrenceNativeRuntime::new( + plan, + executors, + backend_owners, + parameter_defaults, + parameter_projection, + public_flow_ids, + direct_helicity_to_physics, + color_contraction, + helicity_selector_companion, + )?; + Ok(LoadedRecurrenceRuntime { common, lane }) +} + +#[cfg_attr(target_vendor = "apple", unsafe(link_section = "__TEXT,__rcl_load"))] +#[cfg_attr(target_vendor = "apple", inline(never))] +pub(super) fn load_recurrence_ready_runtime( + artifact: &mut VerifiedArtifact, + evaluator_root: &Path, + ready: &RecurrenceReadyExecutionV1, + physics: &ProcessPhysicsV1, + evaluator_payload_container: &crate::artifact::EvaluatorPayloadContainerExtension, +) -> RusticolResult { + validate_ready_runtime_capabilities(ready)?; + if ready.runtime.external_is_initial.len() != physics.external_particles.len() + || ready + .runtime + .external_is_initial + .iter() + .zip(&physics.external_particles) + .any(|(is_initial, particle)| { + *is_initial != (particle.role == crate::ParticleRole::Initial) + }) + { + return Err(RusticolError::integrity( + "process-ready recurrence external roles disagree with authenticated physics", + )); + } + let loaded_primary = load_ready_plan( + artifact, + evaluator_root, + ready, + &ready.primary_plan, + RecurrenceReadyPlanRoleV1::Primary, + )?; + let mut loaded_companion = ready + .companion + .as_ref() + .map(|companion| { + validate_recurrence_companion_physics(physics)?; + if companion.process_digest != ready.primary_plan.process_binding.process_digest + || companion.process_digest != companion.plan.process_binding.process_digest + { + return Err(RusticolError::integrity( + "process-ready recurrence companion belongs to a different process", + )); + } + load_ready_plan( + artifact, + evaluator_root, + ready, + &companion.plan, + RecurrenceReadyPlanRoleV1::HelicitySelector, + ) + }) + .transpose()?; + let has_derived_parameters = ready + .runtime + .runtime_parameters + .iter() + .any(|parameter| parameter.kind == "derived_parameter_component"); + let needs_evaluator_payloads = recurrence_ready_needs_evaluator_payloads( + process_pack_requires_evaluator_payloads(&loaded_primary.process_executors), + has_derived_parameters, + ready.companion.is_some(), + ); + if needs_evaluator_payloads { + artifact.install_evaluator_payload_container(evaluator_payload_container)?; + } + + let plan = loaded_primary.plan; + let (mut common, parameter_defaults, parameter_projection, source_domains) = + build_ready_common_runtime(&plan, ready)?; + let payload_root = ready_recurrence_payload_root(artifact, ready)?; + let kernel_payloads = artifact.evaluator_payload_store(&payload_root)?; + common.model_parameter_evaluator = if has_derived_parameters { + let (pack_bytes, mut pack, fallback_payload_root) = + load_ready_prepared_pack(artifact, ready)?; + drop(pack_bytes); + drop(pack.recurrence_template.take()); + let direct = pack.recurrence_direct_template_catalog( + &ready.prepared_kernel_pack_digest, + &ready.direct_template_catalog_digest, + )?; + if direct.compiled_model_digest != ready.compiled_model_digest + || direct.recurrence_template_catalog_digest != ready.recurrence_template_catalog_digest + { + return Err(RusticolError::integrity( + "prepared parameter fallback belongs to a different recurrence model", + )); + } + drop(direct); + drop(pack.recurrence_direct_template.take()); + if fallback_payload_root != payload_root { + return Err(RusticolError::integrity( + "process-ready recurrence parameter fallback resolved a different payload root", + )); + } + super::eager_load::load_prepared_model_parameter_evaluator_for_runtime( + &pack, + &common.model_parameters, + &kernel_payloads, + )? + } else { + None + }; + common.refresh_derived_model_parameters()?; + + let helicity_selector_companion = match (ready.companion.as_ref(), loaded_companion.take()) { + (Some(companion), Some(loaded)) => Some( + super::on_the_fly_load::load_recurrence_helicity_selector_companion_from_ready_parts( + artifact, + loaded, + &companion.plan, + companion, + ready, + physics, + &kernel_payloads, + )? + .runtime, + ), + (None, None) => None, + _ => { + return Err(RusticolError::internal( + "process-ready recurrence companion load state drifted", + )); + } + }; + let loaded_backend = NativeRecurrenceDirectExecutorBackend::load_from_process_pack( + &loaded_primary.process_executors, + &kernel_payloads, + &plan, + source_domains, + )?; + let (executors, backend_owners) = loaded_backend.into_parts(); + let public_flow_ids = validate_ready_public_flow_ids( + &plan, + &ready.runtime.public_flow_ids, + &ready.runtime.public_flow_public_ids, + physics, + )?; + let direct_helicity_to_physics = validate_ready_direct_helicity_to_physics( + &plan, + &ready.runtime.direct_helicity_to_physics, + physics, + )?; + let color_contraction = ready + .runtime + .color_contraction + .as_ref() + .map(|reference| { + load_color_contraction_reference(artifact, evaluator_root, &ready.key, reference) + }) + .transpose()?; + let lane = RecurrenceNativeRuntime::new( + plan, + executors, + backend_owners, + parameter_defaults, + parameter_projection, + public_flow_ids, + direct_helicity_to_physics, + color_contraction, + helicity_selector_companion, + )?; + Ok(LoadedRecurrenceRuntime { common, lane }) +} + +fn process_pack_requires_evaluator_payloads(pack: &ProcessDirectExecutorPack) -> bool { + pack.descriptors + .iter() + .any(|descriptor| process_binding_requires_evaluator_payloads(&descriptor.binding)) +} + +fn process_binding_requires_evaluator_payloads(binding: &ProcessDirectExecutorBinding) -> bool { + match binding { + ProcessDirectExecutorBinding::Source | ProcessDirectExecutorBinding::Intrinsic(_) => false, + ProcessDirectExecutorBinding::Jit(_) | ProcessDirectExecutorBinding::Native(_) => true, + } +} + +const fn recurrence_ready_needs_evaluator_payloads( + process_pack_requires_payloads: bool, + has_derived_parameters: bool, + has_companion: bool, +) -> bool { + process_pack_requires_payloads + || has_derived_parameters + // Companion executors are intentionally conservative here. They may + // warm a different sparse descriptor family than the primary plan. + || has_companion +} + +fn validate_ready_runtime_capabilities(ready: &RecurrenceReadyExecutionV1) -> RusticolResult<()> { + let color_capability = match ready.color_accuracy.as_str() { + "lc" => crate::recurrence::RECURRENCE_LC_COLOR_CAPABILITY, + "nlc" | "full" => crate::recurrence::RECURRENCE_CONTRACTED_COLOR_CAPABILITY, + other => { + return Err(RusticolError::compatibility(format!( + "process-ready recurrence has unsupported color accuracy {other:?}" + ))); + } + }; + let mut expected = vec![ + color_capability, + crate::recurrence::RECURRENCE_RUNTIME_CAPABILITY, + ]; + if ready + .runtime + .color_contraction + .as_ref() + .and_then(|reference| reference.factorization.as_ref()) + .is_some_and(|factorization| factorization.kind == "symmetric-group-fourier") + { + expected.push(SYMMETRIC_GROUP_FFT_COLOR_RUNTIME_CAPABILITY); + } + if ready.companion.is_some() { + expected.push(RECURRENCE_HELICITY_SELECTOR_COMPANION_RUNTIME_CAPABILITY); + } + expected.sort_unstable(); + if ready + .required_runtime_capabilities + .iter() + .map(String::as_str) + .ne(expected.iter().copied()) + { + return Err(RusticolError::compatibility(format!( + "process-ready recurrence must require exactly {expected:?}" + ))); + } + Ok(()) +} + +fn load_color_contraction( + artifact: &VerifiedArtifact, + evaluator_root: &Path, + manifest: &RecurrenceExecutionManifest, +) -> RusticolResult> { + let Some(reference) = manifest.runtime_metadata.color_contraction.as_ref() else { + return Ok(None); + }; + load_color_contraction_reference(artifact, evaluator_root, &manifest.key, reference).map(Some) +} + +pub(super) fn load_color_contraction_reference( + artifact: &VerifiedArtifact, + evaluator_root: &Path, + process_key: &str, + reference: &RecurrenceColorContractionReference, +) -> RusticolResult { + let relative_root = evaluator_root + .strip_prefix(artifact.root()) + .map_err(|_| RusticolError::security("recurrence process root escapes the artifact"))?; + let logical = relative_root.join(&reference.path); + let logical = logical + .to_str() + .ok_or_else(|| RusticolError::security("recurrence color payload path is not UTF-8"))?; + let record = artifact.payload(logical)?; + if record.role != PayloadRole::EvaluatorState + || record.media_type != "application/octet-stream" + || record.process_id.as_deref() != Some(process_key) + || record.size_bytes != reference.size_bytes + || record.sha256 != reference.sha256 + { + return Err(RusticolError::integrity( + "recurrence color-contraction payload disagrees with execution.json", + )); + } + let bytes = artifact.read_payload(logical)?; + let semantic_digest = SemanticDigest::new(recurrence_color_contraction_digest(&bytes)) + .map_err(|_| { + RusticolError::integrity( + "recurrence color-contraction payload has an invalid semantic digest", + ) + })?; + if semantic_digest.to_string() != reference.semantic_digest { + return Err(RusticolError::integrity( + "recurrence color-contraction payload digest is inconsistent", + )); + } + let contraction = decode_recurrence_color_contraction_v3(&bytes)?; + let accuracy = match contraction.accuracy() { + crate::recurrence::RecurrenceColorAccuracy::Nlc => "nlc", + crate::recurrence::RecurrenceColorAccuracy::Full => "full", + }; + let storage = match contraction.storage() { + crate::recurrence::RecurrenceColorStorage::Expanded => "expanded", + crate::recurrence::RecurrenceColorStorage::Repeated => "repeated", + crate::recurrence::RecurrenceColorStorage::ConvolutionKernels => "convolution-kernels", + }; + if accuracy != reference.color_accuracy + || storage != reference.storage + || contraction.active_sector_count() as u64 != reference.active_sector_count + || contraction.group_count() as u64 != reference.group_count + || contraction.sector_count() as u64 != reference.sector_count + || contraction.component_count() as u64 != reference.component_count + || contraction.destination_count() as u64 != reference.destination_count + || contraction.stored_entry_count() as u64 != reference.entry_count + || contraction.logical_entry_count() as u64 != reference.logical_entry_count + || contraction.includes_color_factor() != reference.includes_color_factor + { + return Err(RusticolError::integrity( + "recurrence color-contraction payload disagrees with its bounded summary", + )); + } + let factorization_matches = match ( + reference.factorization.as_ref(), + contraction.factorization(), + ) { + (None, None) => true, + (Some(reference), Some(factorization)) => { + let kind = match factorization.kind() { + FactorizedColorContractionKind::KleinFourWalsh => "klein-four-walsh", + FactorizedColorContractionKind::ElementaryAbelianWalsh => { + "elementary-abelian-walsh" + } + FactorizedColorContractionKind::SymmetricGroupFourier => "symmetric-group-fourier", + }; + reference.kind == kind + && reference.rank == factorization.rank() + && reference.coset_count == factorization.coset_count() as u64 + } + _ => false, + }; + if !factorization_matches { + return Err(RusticolError::integrity( + "recurrence color-contraction factorization disagrees with its bounded summary", + )); + } + Ok(contraction) +} + +pub(super) fn load_recurrence_exact_sections( + artifact: &VerifiedArtifact, + evaluator_root: &Path, + manifest: &RecurrenceExecutionManifest, + physics: &ProcessPhysicsV1, +) -> RusticolResult { + let (pack_bytes, pack, _payload_root) = load_prepared_pack(artifact, manifest)?; + drop(pack_bytes); + let plan = load_plan(artifact, evaluator_root, manifest)?.plan; + let direct = pack.recurrence_direct_template_catalog( + &manifest.prepared_kernel_pack_digest, + &manifest.direct_template_catalog_digest, + )?; + if direct.catalog_digest != plan.direct_template_catalog_digest().to_string() { + return Err(RusticolError::integrity( + "prepared Direct-Arena catalog digest does not match the recurrence plan", + )); + } + let public_flow_ids = public_flow_ids(&plan, &manifest.runtime_metadata, physics)?; + let exact_factors = plan + .exact_factors() + .iter() + .map(|factor| NativeRecurrenceExactFactor { + real_numerator: factor.real().numerator().to_string(), + real_denominator: factor.real().denominator().to_string(), + imaginary_numerator: factor.imag().numerator().to_string(), + imaginary_denominator: factor.imag().denominator().to_string(), + }) + .collect(); + let executors = direct + .templates + .into_iter() + .map(|template| NativeRecurrenceExactExecutor { + direct_executor_id: template.direct_executor_id, + role: template.role, + destination_operation: template.destination_operation, + parent_component_counts: template.parent_component_counts, + destination_component_count: template.destination_component_count, + momentum_operand_count: template.momentum_operand_count, + prepared_kernel_id: template.payload_binding.prepared_kernel_id, + runtime_template: template.payload_binding.runtime_template, + }) + .collect(); + Ok(NativeRecurrenceExactSections { + process_id: manifest.key.clone(), + strategy: plan.strategy().as_str().to_string(), + semantic_digest: manifest + .plan + .process_binding + .process_semantic_digest + .clone(), + runtime_layout_digest: plan.runtime_layout_digest().to_string(), + current_arena_components: plan.current_arena_components(), + amplitude_destination_count: plan.amplitude_destination_count(), + parameter_value_count: plan.parameter_value_count(), + external_source_count: plan.external_source_count(), + currents: plan.currents().to_vec(), + sources: plan.sources().to_vec(), + contributions: plan.contributions().to_vec(), + finalizations: plan.finalizations().to_vec(), + closures: plan.closures().to_vec(), + row_groups: plan.row_groups().to_vec(), + momentum_forms: plan.momentum_forms().to_vec(), + momentum_terms: plan.momentum_terms().to_vec(), + replay_targets: plan.replay_targets().to_vec(), + source_permutations: plan.source_permutations().to_vec(), + replay_momentum_signs: plan.replay_momentum_signs().to_vec(), + replay_helicity_map: plan.replay_helicity_map().to_vec(), + amplitude_destinations: plan.amplitude_destinations().to_vec(), + resolved_helicities: plan.resolved_helicities().to_vec(), + public_helicities: plan.public_helicities().to_vec(), + source_state_assignments: plan.source_state_assignments().to_vec(), + source_dispatch_variants: plan.source_dispatch_variants().to_vec(), + source_embeddings: plan.source_embeddings().to_vec(), + source_projections: plan.source_projections().to_vec(), + resolved_source_selections: plan.resolved_source_selections().to_vec(), + exact_factors, + public_flow_ids, + executors, + }) +} + +fn public_flow_ids( + plan: &DirectRecurrencePlan, + metadata: &RecurrenceRuntimeMetadata, + physics: &ProcessPhysicsV1, +) -> RusticolResult> { + let available = match plan.strategy() { + crate::recurrence::RecurrenceStrategy::TopologyReplay => { + let available = plan + .replay_targets() + .iter() + .map(|target| target.public_flow_id) + .collect::>(); + if available.len() != plan.replay_targets().len() { + return Err(RusticolError::integrity( + "recurrence direct plan repeats a public replay target", + )); + } + available + } + crate::recurrence::RecurrenceStrategy::AllFlowUnion => { + let available = plan + .amplitude_destinations() + .iter() + .map(|destination| destination.target_sector_id) + .collect::>(); + if available.len() != plan.amplitude_destinations().len() { + return Err(RusticolError::integrity( + "all-flow-union direct plan repeats a physical-flow destination", + )); + } + available + } + crate::recurrence::RecurrenceStrategy::ContractedColorUnion => { + if !metadata.public_color_flows.is_empty() + || physics.color_components.len() != 1 + || !matches!( + physics.color_components.first(), + Some(PhysicsColorComponentV1::ContractedColor(_)) + ) + { + return Err(RusticolError::integrity( + "contracted recurrence must expose exactly one contracted color component and no public flow bindings", + )); + } + return Ok(Vec::new()); + } + }; + if metadata.public_color_flows.len() != physics.color_components.len() { + return Err(RusticolError::integrity( + "recurrence public color-flow bindings do not cover the physics axis", + )); + } + let mut seen = BTreeSet::new(); + let strategy = plan.strategy(); + let result = metadata + .public_color_flows + .iter() + .zip(&physics.color_components) + .map(|(binding, component)| { + if binding.public_id != component.id() { + return Err(RusticolError::integrity( + "recurrence public color-flow binding order disagrees with physics.json", + )); + } + if !matches!(component, PhysicsColorComponentV1::LcFlow(_)) { + return Err(RusticolError::integrity( + "recurrence public color-flow binding references a non-LC component", + )); + } + let plan_sector_id = match strategy { + crate::recurrence::RecurrenceStrategy::TopologyReplay => binding.target_sector_id, + crate::recurrence::RecurrenceStrategy::AllFlowUnion => { + binding.construction_sector_id + } + crate::recurrence::RecurrenceStrategy::ContractedColorUnion => { + unreachable!("contracted recurrence has no public color-flow bindings") + } + }; + if plan_sector_id >= plan.physical_sector_count() + || !available.contains(&plan_sector_id) + || (strategy == crate::recurrence::RecurrenceStrategy::TopologyReplay + && !seen.insert(plan_sector_id)) + { + return Err(RusticolError::integrity( + "recurrence public color-flow target is absent or repeated in the direct plan", + )); + } + seen.insert(plan_sector_id); + Ok(plan_sector_id) + }) + .collect::>>()?; + if seen != available { + return Err(RusticolError::integrity( + "recurrence direct-plan flow destinations do not match the public color-flow axis", + )); + } + Ok(result) +} + +fn validate_ready_public_flow_ids( + plan: &DirectRecurrencePlan, + stored: &[u32], + stored_public_ids: &[String], + physics: &ProcessPhysicsV1, +) -> RusticolResult> { + let available = match plan.strategy() { + crate::recurrence::RecurrenceStrategy::TopologyReplay => plan + .replay_targets() + .iter() + .map(|target| target.public_flow_id) + .collect::>(), + crate::recurrence::RecurrenceStrategy::AllFlowUnion => plan + .amplitude_destinations() + .iter() + .map(|destination| destination.target_sector_id) + .collect::>(), + crate::recurrence::RecurrenceStrategy::ContractedColorUnion => { + if !stored.is_empty() + || !stored_public_ids.is_empty() + || physics.color_components.len() != 1 + || !matches!( + physics.color_components.first(), + Some(PhysicsColorComponentV1::ContractedColor(_)) + ) + { + return Err(RusticolError::integrity( + "process-ready contracted recurrence has an invalid public color axis", + )); + } + return Ok(Vec::new()); + } + }; + if stored.len() != physics.color_components.len() + || stored_public_ids.len() != physics.color_components.len() + || physics + .color_components + .iter() + .any(|component| !matches!(component, PhysicsColorComponentV1::LcFlow(_))) + { + return Err(RusticolError::integrity( + "process-ready recurrence flow mapping does not cover the LC physics axis", + )); + } + if stored_public_ids + .iter() + .zip(&physics.color_components) + .any(|(stored_id, component)| stored_id != component.id()) + { + return Err(RusticolError::integrity( + "process-ready recurrence flow IDs disagree with the physics axis", + )); + } + let mut seen = BTreeSet::new(); + for sector in stored { + if *sector >= plan.physical_sector_count() + || !available.contains(sector) + || (plan.strategy() == crate::recurrence::RecurrenceStrategy::TopologyReplay + && !seen.insert(*sector)) + { + return Err(RusticolError::integrity( + "process-ready recurrence flow mapping references an absent plan sector", + )); + } + seen.insert(*sector); + } + if seen != available { + return Err(RusticolError::integrity( + "process-ready recurrence flow mapping does not cover the plan destinations", + )); + } + Ok(stored.to_vec()) +} + +pub(super) fn direct_helicity_to_physics( + plan: &DirectRecurrencePlan, + physics: &ProcessPhysicsV1, +) -> RusticolResult> { + let mut result = Vec::with_capacity(plan.resolved_helicities().len()); + let mut seen_physics = BTreeSet::new(); + for (expected_id, descriptor) in plan.resolved_helicities().iter().enumerate() { + if descriptor.id as usize != expected_id { + return Err(RusticolError::integrity( + "recurrence resolved-helicity IDs are not dense and ordered", + )); + } + let start = usize::try_from(descriptor.public_helicity_start).map_err(|_| { + RusticolError::artifact("recurrence public-helicity offset exceeds usize") + })?; + let count = usize::try_from(descriptor.public_helicity_count).map_err(|_| { + RusticolError::artifact("recurrence public-helicity count exceeds usize") + })?; + let stop = start + .checked_add(count) + .ok_or_else(|| RusticolError::artifact("recurrence public-helicity range overflows"))?; + let values = plan.public_helicities().get(start..stop).ok_or_else(|| { + RusticolError::integrity( + "recurrence resolved-helicity public vector is outside the direct plan", + ) + })?; + if values.len() != physics.external_particles.len() { + return Err(RusticolError::integrity( + "recurrence resolved-helicity width disagrees with the physics axis", + )); + } + let matches = physics + .helicities + .iter() + .enumerate() + .filter_map(|(index, helicity)| (helicity.values == values).then_some(index)) + .collect::>(); + if matches.len() != 1 { + return Err(RusticolError::integrity(format!( + "recurrence resolved helicity {expected_id} maps to {} physics helicities", + matches.len() + ))); + } + let physics_index = matches[0]; + if !seen_physics.insert(physics_index) { + return Err(RusticolError::integrity( + "recurrence resolved helicities repeat a physics helicity", + )); + } + result.push(physics_index); + } + Ok(result) +} + +pub(super) fn validate_ready_direct_helicity_to_physics( + plan: &DirectRecurrencePlan, + stored: &[u64], + physics: &ProcessPhysicsV1, +) -> RusticolResult> { + if stored.len() != plan.resolved_helicities().len() { + return Err(RusticolError::integrity( + "process-ready helicity mapping has the wrong size", + )); + } + let mut result = Vec::with_capacity(stored.len()); + let mut seen = BTreeSet::new(); + for (expected_id, (descriptor, physics_index)) in + plan.resolved_helicities().iter().zip(stored).enumerate() + { + if descriptor.id as usize != expected_id { + return Err(RusticolError::integrity( + "recurrence resolved-helicity IDs are not dense and ordered", + )); + } + let physics_index = usize::try_from(*physics_index) + .map_err(|_| RusticolError::artifact("physics helicity index exceeds usize"))?; + if !seen.insert(physics_index) { + return Err(RusticolError::integrity( + "process-ready recurrence helicity mapping repeats a physics row", + )); + } + let physics_helicity = physics.helicities.get(physics_index).ok_or_else(|| { + RusticolError::integrity( + "process-ready recurrence helicity mapping is outside the physics axis", + ) + })?; + let start = usize::try_from(descriptor.public_helicity_start) + .map_err(|_| RusticolError::artifact("public-helicity offset exceeds usize"))?; + let count = usize::try_from(descriptor.public_helicity_count) + .map_err(|_| RusticolError::artifact("public-helicity count exceeds usize"))?; + let stop = start + .checked_add(count) + .ok_or_else(|| RusticolError::artifact("public-helicity range overflows"))?; + if plan.public_helicities().get(start..stop) != Some(physics_helicity.values.as_slice()) { + return Err(RusticolError::integrity( + "process-ready recurrence helicity mapping disagrees with the direct plan", + )); + } + result.push(physics_index); + } + Ok(result) +} + +fn load_prepared_pack( + artifact: &VerifiedArtifact, + manifest: &RecurrenceExecutionManifest, +) -> RusticolResult<(Vec, PreparedKernelPackManifest, PathBuf)> { + load_prepared_pack_reference( + artifact, + &manifest.kernel_pack.manifest_path, + &manifest.kernel_pack.payload_root, + ) +} + +fn load_ready_prepared_pack( + artifact: &VerifiedArtifact, + ready: &RecurrenceReadyExecutionV1, +) -> RusticolResult<(Vec, PreparedKernelPackManifest, PathBuf)> { + load_prepared_pack_reference( + artifact, + &ready.kernel_pack.manifest_path, + &ready.kernel_pack.payload_root, + ) +} + +fn load_prepared_pack_reference( + artifact: &VerifiedArtifact, + manifest_path: &str, + payload_root: &str, +) -> RusticolResult<(Vec, PreparedKernelPackManifest, PathBuf)> { + let manifest_path = confined_internal_path( + manifest_path, + "recurrence prepared kernel-pack manifest path", + )?; + let manifest_path = manifest_path.to_str().ok_or_else(|| { + RusticolError::security("recurrence kernel-pack manifest path is not valid UTF-8") + })?; + if artifact.payload(manifest_path)?.role != PayloadRole::EvaluatorManifest { + return Err(RusticolError::security( + "recurrence kernel-pack manifest is not an evaluator-manifest payload", + )); + } + let bytes = artifact.read_payload(manifest_path)?; + let pack: PreparedKernelPackManifest = serde_json::from_slice(&bytes).map_err(|error| { + RusticolError::serialization(format!( + "could not parse recurrence prepared kernel pack: {error}" + )) + })?; + pack.validate()?; + validate_recurrence_prepared_pack_outer_target(&artifact.manifest().producer.target, &pack)?; + let payload_root = artifact.root().join(confined_internal_path( + payload_root, + "recurrence prepared kernel payload root", + )?); + Ok((bytes, pack, payload_root)) +} + +fn recurrence_payload_root( + artifact: &VerifiedArtifact, + manifest: &RecurrenceExecutionManifest, +) -> RusticolResult { + Ok(artifact.root().join(confined_internal_path( + &manifest.kernel_pack.payload_root, + "recurrence prepared kernel payload root", + )?)) +} + +fn ready_recurrence_payload_root( + artifact: &VerifiedArtifact, + ready: &RecurrenceReadyExecutionV1, +) -> RusticolResult { + Ok(artifact.root().join(confined_internal_path( + &ready.kernel_pack.payload_root, + "process-ready recurrence kernel payload root", + )?)) +} + +pub(super) fn validate_recurrence_prepared_pack_outer_target( + outer_target: &crate::Target, + pack: &PreparedKernelPackManifest, +) -> RusticolResult<()> { + pack.validate_portable_process_artifact_target(outer_target, "recurrence") +} + +fn load_plan( + artifact: &VerifiedArtifact, + evaluator_root: &Path, + manifest: &RecurrenceExecutionManifest, +) -> RusticolResult { + load_plan_summary(artifact, evaluator_root, manifest, &manifest.plan) +} + +pub(super) fn load_plan_summary( + artifact: &VerifiedArtifact, + evaluator_root: &Path, + manifest: &RecurrenceExecutionManifest, + summary: &RecurrencePlanSummary, +) -> RusticolResult { + load_plan_reference( + artifact, + evaluator_root, + RecurrencePlanLoadSpec { + process_key: &manifest.key, + compiled_model_digest: &manifest.compiled_model_digest, + recurrence_template_catalog_digest: &manifest.recurrence_template_catalog_digest, + prepared_kernel_pack_digest: &manifest.prepared_kernel_pack_digest, + direct_template_catalog_digest: &manifest.direct_template_catalog_digest, + plan: RecurrenceReadyPlanRef { + runtime_schedule: &summary.runtime_schedule, + process_binding: &summary.process_binding, + runtime_container_member: &summary.inspection_summary.runtime_container_member, + color_projection_certificate: summary + .inspection_summary + .color_projection_certificate + .as_ref(), + helicity_dispatch: summary.helicity_dispatch.as_ref(), + }, + role: if summary.helicity_dispatch.is_some() { + RecurrenceReadyPlanRoleV1::HelicitySelector + } else { + RecurrenceReadyPlanRoleV1::Primary + }, + }, + ) +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(super) enum RecurrenceReadyPlanRoleV1 { + Primary, + HelicitySelector, +} + +struct RecurrenceReadyPlanRef<'a> { + runtime_schedule: &'a RecurrenceRuntimeContainer, + process_binding: &'a RecurrenceProcessBinding, + runtime_container_member: &'a RecurrenceRuntimeContainerMember, + color_projection_certificate: Option<&'a RecurrenceColorProjectionCertificate>, + helicity_dispatch: Option<&'a RecurrenceHelicityDispatchReference>, +} + +struct RecurrencePlanLoadSpec<'a> { + process_key: &'a str, + compiled_model_digest: &'a str, + recurrence_template_catalog_digest: &'a str, + prepared_kernel_pack_digest: &'a str, + direct_template_catalog_digest: &'a str, + plan: RecurrenceReadyPlanRef<'a>, + role: RecurrenceReadyPlanRoleV1, +} + +fn validate_ready_plan_reference(spec: &RecurrencePlanLoadSpec<'_>) -> RusticolResult<()> { + let container = spec.plan.runtime_schedule; + let binding = spec.plan.process_binding; + if container.kind != RECURRENCE_RUNTIME_CONTAINER_KIND + || container.schema_version != RECURRENCE_RUNTIME_CONTAINER_SCHEMA + || container.storage_abi != RECURRENCE_RUNTIME_STORAGE_ABI + || container.plan_member_path != RECURRENCE_DIRECT_SCHEDULE_MEMBER + { + return Err(RusticolError::compatibility( + "unsupported process-ready recurrence schedule contract", + )); + } + let expected_schedule_path = format!( + "recurrence/schedules/{}/recurrence-runtime.pacbin", + binding.schedule_digest + ); + if container.path != expected_schedule_path + || !(1..=2).contains(&container.member_count) + || container.size_bytes == 0 + || container.unpacked_size_bytes == 0 + || container.unpacked_size_bytes > container.size_bytes + { + return Err(RusticolError::integrity( + "process-ready recurrence schedule metadata is inconsistent", + )); + } + for digest in [ + &container.sha256, + &container.index_sha256, + &binding.schedule_digest, + &binding.native_schedule_semantic_digest, + &binding.process_digest, + &binding.process_semantic_digest, + &binding.sha256, + ] { + decode_sha256(digest)?; + } + let expected_binding_path = match spec.role { + RecurrenceReadyPlanRoleV1::Primary => RECURRENCE_PROCESS_BINDING_PATH, + RecurrenceReadyPlanRoleV1::HelicitySelector => RECURRENCE_HELICITY_SELECTOR_BINDING_PATH, + }; + if binding.abi != RECURRENCE_PROCESS_BINDING_ABI + || binding.path != expected_binding_path + || binding.process_id != spec.process_key + || binding.size_bytes == 0 + || binding.process_support_words.is_empty() + || binding.process_support_words.last() == Some(&0) + || binding + .process_support_words + .iter() + .map(|word| word.count_ones()) + .sum::() + != 1 + { + return Err(RusticolError::integrity( + "process-ready recurrence binding metadata is inconsistent", + )); + } + validate_ready_process_remap(&binding.remap)?; + + let member = spec.plan.runtime_container_member; + if member.path != RECURRENCE_DIRECT_SCHEDULE_MEMBER + || member.size_bytes == 0 + || member.container_size_bytes != container.size_bytes + || member.size_bytes > member.container_size_bytes + { + return Err(RusticolError::integrity( + "process-ready recurrence plan-member metadata is inconsistent", + )); + } + decode_sha256(&member.sha256)?; + let certificate_size = if let Some(certificate) = spec.plan.color_projection_certificate { + if certificate.path != RECURRENCE_COLOR_PROJECTION_CERTIFICATE_MEMBER + || certificate.schema_version != 1 + || certificate.proof_kind != "exact-rectangular-sum-projection" + || !certificate.publishable + || certificate.size_bytes == 0 + { + return Err(RusticolError::compatibility( + "unsupported process-ready recurrence projection certificate", + )); + } + decode_sha256(&certificate.sha256)?; + certificate.size_bytes + } else { + 0 + }; + let expected_member_count = 1 + u64::from(certificate_size != 0); + let expected_unpacked = member + .size_bytes + .checked_add(certificate_size) + .ok_or_else(|| RusticolError::artifact("recurrence member sizes overflow u64"))?; + if container.member_count != expected_member_count + || container.unpacked_size_bytes != expected_unpacked + { + return Err(RusticolError::integrity( + "process-ready recurrence PACBIN member inventory is inconsistent", + )); + } + match (spec.role, spec.plan.helicity_dispatch) { + (RecurrenceReadyPlanRoleV1::Primary, None) => {} + (RecurrenceReadyPlanRoleV1::HelicitySelector, Some(dispatch)) => { + let expected_path = format!( + "recurrence/schedules/{}/{}", + binding.schedule_digest, RECURRENCE_HELICITY_DISPATCH_PATH + ); + if dispatch.abi != crate::recurrence::RECURRENCE_HELICITY_DISPATCH_ABI + || dispatch.path != expected_path + || dispatch.size_bytes == 0 + || dispatch.resolved_helicity_count > u64::from(u32::MAX) + { + return Err(RusticolError::compatibility( + "unsupported process-ready recurrence helicity dispatch", + )); + } + decode_sha256(&dispatch.sha256)?; + decode_sha256(&dispatch.base_runtime_layout_digest)?; + } + _ => { + return Err(RusticolError::integrity( + "process-ready recurrence plan has the wrong helicity-dispatch role", + )); + } + } + Ok(()) +} + +fn validate_ready_process_remap(remap: &RecurrenceProcessRemap) -> RusticolResult<()> { + decode_sha256(&remap.bijection_digest)?; + validate_ready_permutation(&remap.source_slots, "source slots")?; + validate_ready_permutation(&remap.public_flow_ids, "public flows")?; + validate_ready_permutation(&remap.physical_sector_ids, "physical sectors")?; + if remap.source_momentum_signs.len() != remap.source_slots.len() + || remap.source_helicity_signs.len() != remap.source_slots.len() + || remap + .source_momentum_signs + .iter() + .chain(&remap.source_helicity_signs) + .any(|sign| !matches!(sign, -1 | 1)) + || remap.source_state_offsets.len() != remap.source_slots.len() + 1 + || remap.source_state_offsets.first() != Some(&0) + || remap.source_state_offsets.last().copied().map(u64::from) + != Some(remap.source_state_indices.len() as u64) + || remap + .source_state_offsets + .windows(2) + .any(|window| window[0] > window[1]) + { + return Err(RusticolError::integrity( + "process-ready recurrence source-state remap is malformed", + )); + } + for window in remap.source_state_offsets.windows(2) { + let start = usize::try_from(window[0]) + .map_err(|_| RusticolError::artifact("source-state offset exceeds usize"))?; + let stop = usize::try_from(window[1]) + .map_err(|_| RusticolError::artifact("source-state offset exceeds usize"))?; + validate_ready_permutation( + remap.source_state_indices.get(start..stop).ok_or_else(|| { + RusticolError::integrity("source-state remap range is out of bounds") + })?, + "source states", + )?; + } + for mapping in [ + &remap.state_templates, + &remap.source_templates, + &remap.direct_executors, + &remap.parameter_slots, + ] { + let mut previous = None; + let mut sources = BTreeSet::new(); + let mut targets = BTreeSet::new(); + for [source, target] in &mapping.changes { + if *source >= mapping.count + || *target >= mapping.count + || source == target + || previous.is_some_and(|value| value >= *source) + || !targets.insert(*target) + { + return Err(RusticolError::integrity( + "process-ready sparse recurrence bijection is malformed", + )); + } + previous = Some(*source); + sources.insert(*source); + } + if sources != targets { + return Err(RusticolError::integrity( + "process-ready sparse recurrence changes do not form a bijection", + )); + } + } + Ok(()) +} + +fn validate_ready_permutation(values: &[u32], context: &str) -> RusticolResult<()> { + let mut seen = vec![false; values.len()]; + for value in values { + let index = usize::try_from(*value) + .map_err(|_| RusticolError::artifact(format!("{context} index exceeds usize")))?; + let slot = seen.get_mut(index).ok_or_else(|| { + RusticolError::integrity(format!( + "process-ready recurrence {context} is not a permutation" + )) + })?; + if std::mem::replace(slot, true) { + return Err(RusticolError::integrity(format!( + "process-ready recurrence {context} repeats an index" + ))); + } + } + Ok(()) +} + +pub(super) fn load_ready_plan( + artifact: &VerifiedArtifact, + evaluator_root: &Path, + ready: &RecurrenceReadyExecutionV1, + plan: &RecurrenceReadyPlanV1, + role: RecurrenceReadyPlanRoleV1, +) -> RusticolResult { + load_plan_reference( + artifact, + evaluator_root, + RecurrencePlanLoadSpec { + process_key: &ready.key, + compiled_model_digest: &ready.compiled_model_digest, + recurrence_template_catalog_digest: &ready.recurrence_template_catalog_digest, + prepared_kernel_pack_digest: &ready.prepared_kernel_pack_digest, + direct_template_catalog_digest: &ready.direct_template_catalog_digest, + plan: RecurrenceReadyPlanRef { + runtime_schedule: &plan.runtime_schedule, + process_binding: &plan.process_binding, + runtime_container_member: &plan.runtime_container_member, + color_projection_certificate: plan.color_projection_certificate.as_ref(), + helicity_dispatch: plan.helicity_dispatch.as_ref(), + }, + role, + }, + ) +} + +#[cfg_attr(target_vendor = "apple", unsafe(link_section = "__TEXT,__rcl_load"))] +#[cfg_attr(target_vendor = "apple", inline(never))] +fn load_plan_reference( + artifact: &VerifiedArtifact, + evaluator_root: &Path, + spec: RecurrencePlanLoadSpec<'_>, +) -> RusticolResult { + #[cfg(feature = "on-the-fly-test-support")] + crate::recurrence::on_the_fly::reject_forbidden_work_if_probed( + crate::recurrence::on_the_fly::OnTheFlyForbiddenWorkV1::DirectPlanLoad, + )?; + validate_ready_plan_reference(&spec)?; + let container = spec.plan.runtime_schedule; + let path = artifact.root().join(&container.path); + let payload = artifact.payload(&container.path)?; + if payload.role != PayloadRole::EvaluatorState + || payload.media_type != "application/octet-stream" + || payload.process_id.is_some() + || payload.executable + || payload.size_bytes != container.size_bytes + || payload.sha256 != container.sha256 + { + return Err(RusticolError::integrity( + "recurrence root schedule disagrees with its authenticated payload", + )); + } + + let process_binding = validate_process_binding(artifact, evaluator_root, &spec)?; + + let expected_file_sha = decode_sha256(&container.sha256)?; + let container_file = artifact.open_payload_file(&container.path)?; + let reader = PacbinReader::open_file_with_sha256(container_file, &path, &expected_file_sha)?; + let index = reader.index(); + if index.file_size() != container.size_bytes + || reader.container_size() as u64 != container.size_bytes + || index.index_sha256().as_slice() != decode_sha256(&container.index_sha256)?.as_slice() + || index.members().len() as u64 != container.member_count + || index + .members() + .iter() + .map(|member| member.length()) + .sum::() + != container.unpacked_size_bytes + { + return Err(RusticolError::integrity( + "recurrence runtime PACBIN metadata disagrees with execution.json", + )); + } + let member = reader.member(RECURRENCE_DIRECT_SCHEDULE_MEMBER)?; + let plan_metadata = spec.plan.runtime_container_member; + if member.kind() != PacbinMemberKind::RecurrenceDirectPlan { + return Err(RusticolError::compatibility( + "recurrence runtime PACBIN contains an incompatible plan member", + )); + } + if member.length() != plan_metadata.size_bytes + || member.sha256().as_slice() != decode_sha256(&plan_metadata.sha256)?.as_slice() + { + return Err(RusticolError::integrity( + "recurrence DirectPlan member disagrees with execution.json", + )); + } + match ( + spec.plan.color_projection_certificate, + index.members().len(), + ) { + (None, 1) => {} + (Some(metadata), 2) => { + let certificate = reader.member(RECURRENCE_COLOR_PROJECTION_CERTIFICATE_MEMBER)?; + if certificate.kind() != PacbinMemberKind::RecurrenceColorProjectionCertificate { + return Err(RusticolError::compatibility( + "recurrence runtime PACBIN contains an incompatible projection certificate", + )); + } + if certificate.length() != metadata.size_bytes + || certificate.sha256().as_slice() != decode_sha256(&metadata.sha256)?.as_slice() + { + return Err(RusticolError::integrity( + "recurrence color-projection certificate disagrees with execution.json", + )); + } + validate_recurrence_color_projection_certificate( + reader.member_bytes(RECURRENCE_COLOR_PROJECTION_CERTIFICATE_MEMBER)?, + )?; + } + (None, 2) | (Some(_), 1) => { + return Err(RusticolError::integrity( + "recurrence runtime PACBIN projection certificate disagrees with execution.json", + )); + } + (_, count) => { + return Err(RusticolError::compatibility(format!( + "recurrence runtime PACBIN contains {count} members; expected one plan and at most one projection certificate" + ))); + } + } + let bytes = reader.member_bytes(RECURRENCE_DIRECT_SCHEDULE_MEMBER)?; + let plan = decode_recurrence_direct_plan_v2(bytes)?; + if plan.semantic_digest().to_string() + != spec.plan.process_binding.native_schedule_semantic_digest() + { + return Err(RusticolError::integrity( + "recurrence native schedule semantic digest disagrees with its process binding", + )); + } + if plan.prepared_pack_digest().to_string() != spec.prepared_kernel_pack_digest + || plan.direct_template_catalog_digest().to_string() != spec.direct_template_catalog_digest + { + return Err(RusticolError::integrity( + "direct recurrence plan authentication digests disagree with execution.json", + )); + } + let root_runtime_layout_digest = plan.runtime_layout_digest(); + let plan = apply_process_remap(plan, &process_binding.remap)?.into_runtime_compacted(); + let expected_prepared_pack_digest = semantic_digest_from_bytes( + decode_sha256(spec.prepared_kernel_pack_digest)?, + "prepared-kernel pack", + )?; + let expected_catalog_digest = semantic_digest_from_bytes( + decode_sha256(spec.direct_template_catalog_digest)?, + "direct-template catalog", + )?; + process_binding.process_executors.validate_for_plan( + &plan, + expected_prepared_pack_digest, + expected_catalog_digest, + root_runtime_layout_digest, + )?; + Ok(LoadedRecurrencePlan { + plan, + process_executors: process_binding.process_executors, + }) +} + +#[cfg_attr(target_vendor = "apple", unsafe(link_section = "__TEXT,__rcl_load"))] +#[cfg_attr(target_vendor = "apple", inline(never))] +#[cfg_attr(target_vendor = "apple", cold)] +fn validate_process_binding( + artifact: &VerifiedArtifact, + evaluator_root: &Path, + spec: &RecurrencePlanLoadSpec<'_>, +) -> RusticolResult { + let binding = spec.plan.process_binding; + let relative_root = evaluator_root + .strip_prefix(artifact.root()) + .map_err(|_| RusticolError::security("recurrence process root escapes the artifact"))?; + let logical = relative_root.join(&binding.path); + let logical = logical + .to_str() + .ok_or_else(|| RusticolError::security("recurrence process-binding path is not UTF-8"))?; + let record = artifact.payload(logical)?; + if record.role != PayloadRole::EvaluatorState + || record.process_id.as_deref() != Some(spec.process_key) + || record.size_bytes != binding.size_bytes + || record.sha256 != binding.sha256 + { + return Err(RusticolError::integrity( + "recurrence process-binding payload disagrees with execution.json", + )); + } + let bytes = artifact.read_payload(logical)?; + if bytes.len() < RECURRENCE_PROCESS_BINDING_HEADER_SIZE_V4 + || bytes.get(..8) != Some(RECURRENCE_PROCESS_BINDING_MAGIC_V4.as_slice()) + { + return Err(RusticolError::compatibility( + "unsupported recurrence process-binding payload", + )); + } + let u32_at = |offset: usize| { + bytes + .get(offset..offset + 4) + .and_then(|raw| raw.try_into().ok()) + .map(u32::from_le_bytes) + .ok_or_else(|| RusticolError::artifact("truncated recurrence process binding")) + }; + if u32_at(8)? != RECURRENCE_PROCESS_BINDING_VERSION_V4 + || usize::try_from(u32_at(12)?).ok() != Some(RECURRENCE_PROCESS_BINDING_HEADER_SIZE_V4) + { + return Err(RusticolError::compatibility( + "unsupported recurrence process-binding version", + )); + } + let process_len = usize::try_from(u32_at(16)?) + .map_err(|_| RusticolError::artifact("recurrence process ID is too large"))?; + let word_count = usize::try_from(u32_at(20)?) + .map_err(|_| RusticolError::artifact("recurrence support mask is too large"))?; + let target_len = usize::try_from(u32_at(24)?) + .map_err(|_| RusticolError::artifact("recurrence executor target is too large"))?; + let cpu_feature_count = u32_at(28)?; + let descriptor_count = u32_at(32)?; + let catalog_executor_count = u32_at(36)?; + let backend = ProcessExecutorBackend::decode( + *bytes + .get(40) + .ok_or_else(|| RusticolError::artifact("truncated executor backend"))?, + )?; + let portable = match bytes.get(41).copied() { + Some(0) => false, + Some(1) => true, + _ => { + return Err(RusticolError::artifact( + "recurrence executor portability is not a canonical boolean", + )); + } + }; + if bytes.get(42) != Some(&64) || bytes.get(43) != Some(&0) { + return Err(RusticolError::compatibility( + "recurrence process executors require a 64-bit little-endian target", + )); + } + let counts = (0..11) + .map(|index| u32_at(44 + index * 4)) + .collect::>>()?; + if counts[5] != catalog_executor_count { + return Err(RusticolError::integrity( + "recurrence executor catalog domain disagrees with its process remap", + )); + } + let schedule_digest = read_header_digest(&bytes, 88, "root schedule")?; + let process_semantic_digest = read_header_digest(&bytes, 120, "process semantic")?; + let compiled_model_digest = semantic_digest_from_bytes( + read_header_digest(&bytes, 152, "compiled model")?, + "compiled model", + )?; + let recurrence_template_catalog_digest = semantic_digest_from_bytes( + read_header_digest(&bytes, 184, "recurrence-template catalog")?, + "recurrence-template catalog", + )?; + let prepared_kernel_pack_digest = semantic_digest_from_bytes( + read_header_digest(&bytes, 216, "prepared-kernel pack")?, + "prepared-kernel pack", + )?; + let direct_template_catalog_digest = semantic_digest_from_bytes( + read_header_digest(&bytes, 248, "direct-template catalog")?, + "direct-template catalog", + )?; + let runtime_layout_digest = semantic_digest_from_bytes( + read_header_digest(&bytes, 280, "runtime layout")?, + "runtime layout", + )?; + let process_digest = read_header_digest(&bytes, 312, "process")?; + if schedule_digest != decode_sha256(&binding.schedule_digest)? + || process_digest != decode_sha256(&binding.process_digest)? + || process_semantic_digest != decode_sha256(&binding.process_semantic_digest)? + || compiled_model_digest.to_string() != spec.compiled_model_digest + || recurrence_template_catalog_digest.to_string() != spec.recurrence_template_catalog_digest + || prepared_kernel_pack_digest.to_string() != spec.prepared_kernel_pack_digest + || direct_template_catalog_digest.to_string() != spec.direct_template_catalog_digest + { + return Err(RusticolError::integrity( + "recurrence process-executor identities disagree with execution.json", + )); + } + let process_start = RECURRENCE_PROCESS_BINDING_HEADER_SIZE_V4; + let process_end = process_start + .checked_add(process_len) + .ok_or_else(|| RusticolError::artifact("recurrence process binding overflows"))?; + let target_end = process_end + .checked_add(target_len) + .ok_or_else(|| RusticolError::artifact("recurrence executor target range overflows"))?; + if bytes.get(process_start..process_end) != Some(spec.process_key.as_bytes()) || word_count == 0 + { + return Err(RusticolError::integrity( + "recurrence process-binding payload is inconsistent", + )); + } + let target_triple = + std::str::from_utf8(bytes.get(process_end..target_end).ok_or_else(|| { + RusticolError::artifact("truncated recurrence process-executor target") + })?) + .map_err(|_| RusticolError::artifact("recurrence executor target is not UTF-8"))? + .to_owned(); + if target_triple.is_empty() { + return Err(RusticolError::artifact( + "recurrence process-executor target is empty", + )); + } + let mut cursor = target_end; + let cpu_features = (0..cpu_feature_count) + .map(|_| read_string(&bytes, &mut cursor, "executor CPU feature")) + .collect::>>()?; + let target = ProcessExecutorTarget { + backend, + target_triple, + portable, + cpu_features, + }; + target.validate(&artifact.manifest().producer.target)?; + let words = read_u64_values( + &bytes, + &mut cursor, + u32::try_from(word_count) + .map_err(|_| RusticolError::artifact("recurrence support mask is too large"))?, + "recurrence support mask", + )?; + if words != binding.process_support_words { + return Err(RusticolError::integrity( + "recurrence process support mask is inconsistent", + )); + } + let source_slots = read_u32_rows(&bytes, &mut cursor, counts[0], 1, "source-slot remap")? + .into_iter() + .map(|row| row[0]) + .collect(); + let source_momentum_signs = + read_i32_values(&bytes, &mut cursor, counts[0], "source momentum signs")?; + let source_helicity_signs = + read_i32_values(&bytes, &mut cursor, counts[0], "source helicity signs")?; + let source_state_offsets = read_u32_rows( + &bytes, + &mut cursor, + counts[0].checked_add(1).ok_or_else(|| { + RusticolError::artifact("recurrence source-state offset count overflows") + })?, + 1, + "source-state remap offsets", + )? + .into_iter() + .map(|row| row[0]) + .collect::>(); + let source_state_count = source_state_offsets + .last() + .copied() + .ok_or_else(|| RusticolError::artifact("recurrence source-state remap has no offsets"))?; + let source_state_indices = read_u32_rows( + &bytes, + &mut cursor, + source_state_count, + 1, + "source-state remap indices", + )? + .into_iter() + .map(|row| row[0]) + .collect(); + let public_flow_ids = read_u32_rows(&bytes, &mut cursor, counts[1], 1, "public-flow remap")? + .into_iter() + .map(|row| row[0]) + .collect(); + let physical_sector_ids = + read_u32_rows(&bytes, &mut cursor, counts[2], 1, "physical-sector remap")? + .into_iter() + .map(|row| row[0]) + .collect(); + let state_templates = RecurrenceSparseBijection { + count: counts[3], + changes: read_u32_pairs(&bytes, &mut cursor, counts[7], "state-template remap")?, + }; + let source_templates = RecurrenceSparseBijection { + count: counts[4], + changes: read_u32_pairs(&bytes, &mut cursor, counts[8], "source-template remap")?, + }; + let direct_executors = RecurrenceSparseBijection { + count: counts[5], + changes: read_u32_pairs(&bytes, &mut cursor, counts[9], "direct-executor remap")?, + }; + let parameter_slots = RecurrenceSparseBijection { + count: counts[6], + changes: read_u32_pairs(&bytes, &mut cursor, counts[10], "parameter-slot remap")?, + }; + let decoded = RecurrenceProcessRemap { + bijection_digest: binding.remap.bijection_digest.clone(), + source_slots, + source_momentum_signs, + source_helicity_signs, + source_state_offsets, + source_state_indices, + public_flow_ids, + physical_sector_ids, + state_templates, + source_templates, + direct_executors, + parameter_slots, + }; + if decoded != binding.remap { + return Err(RusticolError::integrity( + "recurrence process-binding binary remap disagrees with execution.json", + )); + } + let process_executors = decode_process_executor_descriptors( + &bytes, + &mut cursor, + descriptor_count, + catalog_executor_count, + target, + ProcessExecutorIdentities { + compiled_model_digest, + recurrence_template_catalog_digest, + prepared_kernel_pack_digest, + direct_template_catalog_digest, + runtime_layout_digest, + }, + )?; + if cursor != bytes.len() { + return Err(RusticolError::integrity( + "recurrence process-binding payload has trailing bytes", + )); + } + Ok(LoadedRecurrenceProcessBinding { + remap: decoded, + process_executors, + }) +} + +fn read_header_digest(bytes: &[u8], offset: usize, context: &str) -> RusticolResult<[u8; 32]> { + bytes + .get(offset..offset + 32) + .ok_or_else(|| RusticolError::artifact(format!("truncated {context} digest")))? + .try_into() + .map_err(|_| RusticolError::internal("process-binding digest width drifted")) +} + +fn read_u32_pairs( + bytes: &[u8], + cursor: &mut usize, + count: u32, + context: &str, +) -> RusticolResult> { + read_u32_rows(bytes, cursor, count, 2, context)? + .into_iter() + .map(|row| { + row.try_into() + .map_err(|_| RusticolError::internal("u32 pair width drifted")) + }) + .collect() +} + +fn read_u32_rows( + bytes: &[u8], + cursor: &mut usize, + row_count: u32, + row_width: usize, + context: &str, +) -> RusticolResult>> { + let value_count = usize::try_from(row_count) + .ok() + .and_then(|count| count.checked_mul(row_width)) + .ok_or_else(|| RusticolError::artifact(format!("{context} count overflows")))?; + let byte_count = value_count + .checked_mul(4) + .ok_or_else(|| RusticolError::artifact(format!("{context} byte size overflows")))?; + let end = cursor + .checked_add(byte_count) + .ok_or_else(|| RusticolError::artifact(format!("{context} range overflows")))?; + let payload = bytes + .get(*cursor..end) + .ok_or_else(|| RusticolError::artifact(format!("truncated {context}")))?; + *cursor = end; + payload + .chunks_exact(row_width * 4) + .map(|row| { + row.chunks_exact(4) + .map(|raw| { + raw.try_into() + .map(u32::from_le_bytes) + .map_err(|_| RusticolError::artifact(format!("truncated {context}"))) + }) + .collect() + }) + .collect() +} + +fn read_i32_values( + bytes: &[u8], + cursor: &mut usize, + count: u32, + context: &str, +) -> RusticolResult> { + let count = usize::try_from(count) + .map_err(|_| RusticolError::artifact(format!("{context} count exceeds usize")))?; + let byte_count = count + .checked_mul(4) + .ok_or_else(|| RusticolError::artifact(format!("{context} byte size overflows")))?; + let end = cursor + .checked_add(byte_count) + .ok_or_else(|| RusticolError::artifact(format!("{context} range overflows")))?; + let payload = bytes + .get(*cursor..end) + .ok_or_else(|| RusticolError::artifact(format!("truncated {context}")))?; + *cursor = end; + payload + .chunks_exact(4) + .map(|raw| { + raw.try_into() + .map(i32::from_le_bytes) + .map_err(|_| RusticolError::artifact(format!("truncated {context}"))) + }) + .collect() +} + +fn read_u64_values( + bytes: &[u8], + cursor: &mut usize, + count: u32, + context: &str, +) -> RusticolResult> { + let count = usize::try_from(count) + .map_err(|_| RusticolError::artifact(format!("{context} count exceeds usize")))?; + let byte_count = count + .checked_mul(8) + .ok_or_else(|| RusticolError::artifact(format!("{context} byte size overflows")))?; + let end = cursor + .checked_add(byte_count) + .ok_or_else(|| RusticolError::artifact(format!("{context} range overflows")))?; + let payload = bytes + .get(*cursor..end) + .ok_or_else(|| RusticolError::artifact(format!("truncated {context}")))?; + *cursor = end; + payload + .chunks_exact(8) + .map(|raw| { + raw.try_into() + .map(u64::from_le_bytes) + .map_err(|_| RusticolError::artifact(format!("truncated {context}"))) + }) + .collect() +} + +fn apply_process_remap( + plan: DirectRecurrencePlan, + remap: &RecurrenceProcessRemap, +) -> RusticolResult { + let source_count = usize::try_from(plan.external_source_count()) + .map_err(|_| RusticolError::artifact("recurrence source count exceeds usize"))?; + let topology_flow_count = match plan.strategy() { + crate::recurrence::RecurrenceStrategy::TopologyReplay => Some(plan.replay_targets().len()), + crate::recurrence::RecurrenceStrategy::AllFlowUnion + | crate::recurrence::RecurrenceStrategy::ContractedColorUnion => None, + }; + if remap.source_slots.len() != source_count + || remap.source_momentum_signs.len() != source_count + || remap.source_helicity_signs.len() != source_count + || topology_flow_count.is_some_and(|flow_count| remap.public_flow_ids.len() != flow_count) + || remap.physical_sector_ids.len() != plan.physical_sector_count() as usize + || remap.state_templates.count != plan.state_template_count() + || remap.source_templates.count != plan.source_template_count() + || remap.direct_executors.count != plan.direct_executor_count() + || remap.parameter_slots.count != plan.parameter_value_count() + { + return Err(RusticolError::integrity( + "recurrence process remap domain counts disagree with the root schedule", + )); + } + let identity = remap + .source_slots + .iter() + .enumerate() + .all(|(index, value)| *value as usize == index) + && remap.source_momentum_signs.iter().all(|value| *value == 1) + && remap.source_helicity_signs.iter().all(|value| *value == 1) + && remap + .source_state_offsets + .windows(2) + .enumerate() + .all(|(source_slot, window)| { + let start = window[0] as usize; + let stop = window[1] as usize; + remap.source_state_indices[start..stop] + .iter() + .enumerate() + .all(|(state_index, value)| *value as usize == state_index) + && source_slot < remap.source_slots.len() + }) + && remap + .public_flow_ids + .iter() + .enumerate() + .all(|(index, value)| *value as usize == index) + && remap + .physical_sector_ids + .iter() + .enumerate() + .all(|(index, value)| *value as usize == index) + && remap.state_templates.changes.is_empty() + && remap.source_templates.changes.is_empty() + && remap.direct_executors.changes.is_empty() + && remap.parameter_slots.changes.is_empty(); + if identity { + return Ok(plan); + } + if plan.strategy() != crate::recurrence::RecurrenceStrategy::TopologyReplay { + return Err(RusticolError::compatibility( + "cross-process recurrence sharing currently supports topology-replay schedules only", + )); + } + if !remap.parameter_slots.changes.is_empty() { + return Err(RusticolError::compatibility( + "cross-process recurrence sharing does not support prepared-parameter reordering", + )); + } + + let mut parts = plan.into_parts(); + for current in &mut parts.currents { + current.state_template_id = + sparse_bijection_value(&remap.state_templates, current.state_template_id)?; + } + for source in &mut parts.sources { + source.source_slot = *remap + .source_slots + .get(source.source_slot as usize) + .ok_or_else(|| { + RusticolError::integrity( + "recurrence source row is outside the process source remap", + ) + })?; + source.source_template_or_dispatch_domain = sparse_bijection_value( + &remap.source_templates, + source.source_template_or_dispatch_domain, + )?; + } + for group in &mut parts.row_groups { + group.direct_executor_id = + sparse_bijection_value(&remap.direct_executors, group.direct_executor_id)?; + } + for target in &mut parts.replay_targets { + target.public_flow_id = *remap + .public_flow_ids + .get(target.public_flow_id as usize) + .ok_or_else(|| { + RusticolError::integrity( + "recurrence replay target is outside the public-flow remap", + ) + })?; + let start = usize::try_from(target.source_permutation_start).map_err(|_| { + RusticolError::artifact("recurrence replay permutation offset exceeds usize") + })?; + let stop = start + .checked_add(target.source_permutation_count as usize) + .ok_or_else(|| { + RusticolError::artifact("recurrence replay permutation range overflows") + })?; + let permutations = parts + .source_permutations + .get_mut(start..stop) + .ok_or_else(|| { + RusticolError::integrity( + "recurrence replay permutation is outside the root schedule", + ) + })?; + let momentum_signs = parts + .replay_momentum_signs + .get_mut(start..stop) + .ok_or_else(|| { + RusticolError::integrity( + "recurrence replay momentum signs are outside the root schedule", + ) + })?; + for (source_slot, momentum_sign) in permutations.iter_mut().zip(momentum_signs.iter_mut()) { + let root_slot = *source_slot as usize; + *source_slot = *remap.source_slots.get(root_slot).ok_or_else(|| { + RusticolError::integrity("recurrence replay source is outside the process remap") + })?; + *momentum_sign = momentum_sign + .checked_mul(*remap.source_momentum_signs.get(root_slot).ok_or_else(|| { + RusticolError::integrity( + "recurrence replay source has no process momentum sign", + ) + })?) + .ok_or_else(|| { + RusticolError::integrity("recurrence replay momentum sign overflows") + })?; + } + } + for descriptor in &parts.resolved_helicities { + let start = usize::try_from(descriptor.public_helicity_start).map_err(|_| { + RusticolError::artifact("recurrence public-helicity offset exceeds usize") + })?; + let stop = start + .checked_add(descriptor.public_helicity_count as usize) + .ok_or_else(|| RusticolError::artifact("recurrence public-helicity range overflows"))?; + let source = parts + .public_helicities + .get(start..stop) + .ok_or_else(|| { + RusticolError::integrity( + "recurrence public-helicity vector is outside the root schedule", + ) + })? + .to_vec(); + if source.len() != source_count { + return Err(RusticolError::integrity( + "recurrence public-helicity width disagrees with the process remap", + )); + } + let target = parts + .public_helicities + .get_mut(start..stop) + .ok_or_else(|| { + RusticolError::integrity( + "recurrence public-helicity vector is outside the root schedule", + ) + })?; + for (root_slot, value) in source.into_iter().enumerate() { + let target_slot = remap.source_slots[root_slot] as usize; + target[target_slot] = value + .checked_mul(remap.source_helicity_signs[root_slot]) + .ok_or_else(|| RusticolError::integrity("recurrence public helicity overflows"))?; + } + } + for descriptor in &parts.resolved_helicities { + let start = usize::try_from(descriptor.source_state_start).map_err(|_| { + RusticolError::artifact("recurrence source-state assignment offset exceeds usize") + })?; + let stop = start + .checked_add(descriptor.source_state_count as usize) + .ok_or_else(|| { + RusticolError::artifact("recurrence source-state assignment range overflows") + })?; + let source = parts + .source_state_assignments + .get(start..stop) + .ok_or_else(|| { + RusticolError::integrity( + "recurrence source-state assignment range is outside the root schedule", + ) + })? + .to_vec(); + if source.len() != source_count { + return Err(RusticolError::integrity( + "recurrence source-state assignment width disagrees with the process remap", + )); + } + let target = parts + .source_state_assignments + .get_mut(start..stop) + .ok_or_else(|| { + RusticolError::integrity( + "recurrence source-state assignment range is outside the root schedule", + ) + })?; + for assignment in source { + let target_slot = *remap + .source_slots + .get(assignment.source_slot as usize) + .ok_or_else(|| { + RusticolError::integrity( + "recurrence source-state assignment is outside the process remap", + ) + })?; + let state_start = *remap + .source_state_offsets + .get(assignment.source_slot as usize) + .ok_or_else(|| { + RusticolError::integrity( + "recurrence source-state assignment has no remap offset", + ) + })? as usize; + let state_stop = *remap + .source_state_offsets + .get(assignment.source_slot as usize + 1) + .ok_or_else(|| { + RusticolError::integrity( + "recurrence source-state assignment has no remap limit", + ) + })? as usize; + let target_state_index = *remap + .source_state_indices + .get(state_start..state_stop) + .and_then(|values| values.get(assignment.state_index as usize)) + .ok_or_else(|| { + RusticolError::integrity( + "recurrence source-state assignment is outside its state remap", + ) + })?; + target[target_slot as usize] = crate::recurrence::DirectSourceStateAssignment { + source_slot: target_slot, + state_index: target_state_index, + }; + } + } + DirectRecurrencePlan::new(parts) +} + +fn sparse_bijection_value(mapping: &RecurrenceSparseBijection, value: u32) -> RusticolResult { + if value >= mapping.count { + return Err(RusticolError::integrity( + "recurrence process remap input is outside its domain", + )); + } + Ok(mapping + .changes + .binary_search_by_key(&value, |change| change[0]) + .map(|index| mapping.changes[index][1]) + .unwrap_or(value)) +} + +type RecurrenceCommonRuntimeParts = ( + ExecutionRuntime, + Vec, + Vec, + Vec, +); + +fn recurrence_runtime_parameters( + metadata: &RecurrenceRuntimeMetadata, +) -> Vec { + recurrence_runtime_parameters_from(&metadata.runtime_parameters) +} + +fn recurrence_runtime_parameters_from( + parameters: &[RecurrenceRuntimeParameter], +) -> Vec { + parameters + .iter() + .map(|parameter| GenericRuntimeModelParameterManifest { + name: parameter.name.clone(), + kind: parameter.kind.clone(), + parameter_index: parameter.parameter_index as usize, + default: parameter.default, + pdg: None, + runtime_name: parameter.runtime_name.clone(), + complex_component: parameter.complex_component.clone(), + }) + .collect() +} + +#[derive(Clone, Copy)] +struct RecurrenceNormalizationValues { + factor: f64, + color_factor: f64, +} + +fn recurrence_normalization_values( + metadata: &RecurrenceRuntimeMetadata, +) -> RusticolResult { + recurrence_normalization_values_from( + &metadata.normalization, + metadata.color_contraction.as_ref(), + ) +} + +fn recurrence_normalization_values_from( + normalization: &RecurrenceNormalization, + color_contraction: Option<&RecurrenceColorContractionReference>, +) -> RusticolResult { + if !normalization.couplings_in_stage_evaluators { + return Err(RusticolError::compatibility( + "recurrence execution requires local vertex couplings in prepared kernel calls", + )); + } + let color_factor = + if color_contraction.is_some_and(|contraction| contraction.includes_color_factor) { + 1.0 + } else { + normalization.color_factor + }; + let factor = color_factor * normalization.global_coupling_factor + / (normalization.average_factor * normalization.identical_factor); + if !factor.is_finite() { + return Err(RusticolError::integrity( + "recurrence runtime normalization is not finite", + )); + } + Ok(RecurrenceNormalizationValues { + factor, + color_factor, + }) +} + +#[cfg(feature = "on-the-fly-test-support")] +fn on_the_fly_prepared_parameters( + metadata: &RecurrenceRuntimeMetadata, + runtime_parameters: &[GenericRuntimeModelParameterManifest], + mut model_parameter_evaluator: Option, + overrides: &BTreeMap, +) -> RusticolResult> { + let runtime_slots = runtime_parameter_slots(runtime_parameters)?; + let mut runtime_values = runtime_parameters + .iter() + .map(|parameter| parameter.default) + .collect::>(); + for value in &runtime_values { + if !value.is_finite() { + return Err(RusticolError::integrity( + "recurrence runtime parameter default is not finite", + )); + } + } + for value in &metadata.prepared_parameter_defaults { + if !value[0].is_finite() || !value[1].is_finite() { + return Err(RusticolError::integrity( + "recurrence prepared parameter default is not finite", + )); + } + } + for (name, override_value) in overrides { + if !override_value[0].is_finite() || !override_value[1].is_finite() { + return Err(RusticolError::invalid_argument(format!( + "on-the-fly parameter override {name:?} is not finite" + ))); + } + if runtime_parameters.iter().any(|parameter| { + parameter.kind == "derived_parameter_component" + && parameter.runtime_name.as_deref().unwrap_or(¶meter.name) == name + }) { + return Err(RusticolError::invalid_argument(format!( + "derived on-the-fly parameter {name:?} is immutable" + ))); + } + let Some(slots) = runtime_slots.get(name).copied() else { + return Err(RusticolError::invalid_argument(format!( + "on-the-fly parameter override {name:?} is not used by the process" + ))); + }; + let real = runtime_values.get_mut(slots.real).ok_or_else(|| { + RusticolError::integrity("on-the-fly runtime parameter real slot is absent") + })?; + *real = override_value[0]; + if let Some(imaginary_slot) = slots.imaginary { + let imaginary = runtime_values.get_mut(imaginary_slot).ok_or_else(|| { + RusticolError::integrity("on-the-fly runtime parameter imaginary slot is absent") + })?; + *imaginary = override_value[1]; + } else if override_value[1] != 0.0 { + return Err(RusticolError::invalid_argument(format!( + "real on-the-fly parameter {name:?} cannot receive an imaginary value" + ))); + } + } + super::model_parameters::refresh_derived_model_parameter_values( + model_parameter_evaluator.as_mut(), + &mut runtime_values, + )?; + + let defaults = metadata + .prepared_parameter_defaults + .iter() + .map(|[real, imaginary]| crate::EagerComplex64::new(*real, *imaginary)) + .collect::>(); + let projection = metadata + .parameter_projection + .iter() + .filter_map(|row| { + row.prepared_parameter_id + .map(|prepared_slot| (row, prepared_slot)) + }) + .map(|(row, prepared_slot)| { + Ok(PreparedParameterProjectionEntry { + runtime_slot: usize::try_from(row.runtime_slot).map_err(|_| { + RusticolError::integrity("on-the-fly runtime parameter slot exceeds usize") + })?, + prepared_slot: usize::try_from(prepared_slot).map_err(|_| { + RusticolError::integrity("on-the-fly prepared parameter slot exceeds usize") + })?, + component: u8::try_from(row.component).map_err(|_| { + RusticolError::integrity("on-the-fly parameter component exceeds u8") + })?, + }) + }) + .collect::>>()?; + projected_prepared_parameter_values(&defaults, &projection, &runtime_values).map(|values| { + values + .into_iter() + .map(|(real, imaginary)| [real, imaginary]) + .collect() + }) +} + +#[cfg(feature = "on-the-fly-test-support")] +pub(super) fn on_the_fly_source_major_momenta( + seed: &OnTheFlyProcessSeedV1, + point_major: &[f64], + point_count: u32, + lorentz_component_count: u16, +) -> RusticolResult> { + let mut source_major = Vec::new(); + let output_len = on_the_fly_source_major_momenta_into( + seed, + point_major, + point_count, + lorentz_component_count, + &mut source_major, + )?; + source_major.truncate(output_len); + Ok(source_major) +} + +/// Transpose public point-major momenta into the source-major plane layout +/// without reallocating once `source_major` has reached the warmed capacity. +/// The returned length identifies the initialized prefix; callers may retain +/// a larger backing allocation for later batches. +pub(super) fn on_the_fly_source_major_momenta_into( + seed: &OnTheFlyProcessSeedV1, + point_major: &[f64], + point_count: u32, + lorentz_component_count: u16, + source_major: &mut Vec, +) -> RusticolResult { + let permutation = seed.external_permutation(); + let point_count_usize = point_count as usize; + let lorentz_count = usize::from(lorentz_component_count); + let point_width = permutation + .len() + .checked_mul(lorentz_count) + .ok_or_else(|| { + RusticolError::invalid_argument("on-the-fly momentum shape exceeds usize") + })?; + let expected = point_count_usize.checked_mul(point_width).ok_or_else(|| { + RusticolError::invalid_argument("on-the-fly momentum shape exceeds usize") + })?; + if point_count == 0 || point_major.len() != expected { + return Err(RusticolError::invalid_argument(format!( + "on-the-fly point-major momenta contain {} scalars, expected {expected}", + point_major.len() + ))); + } + if source_major.len() < expected { + source_major + .try_reserve_exact(expected - source_major.len()) + .map_err(|error| { + RusticolError::invalid_argument(format!( + "on-the-fly momentum workspace allocation failed: {error}" + )) + })?; + source_major.resize(expected, 0.0); + } + for (source_slot, public_slot) in permutation.iter().copied().enumerate() { + let public_slot = usize::try_from(public_slot).map_err(|_| { + RusticolError::integrity("on-the-fly public momentum slot exceeds usize") + })?; + if public_slot >= permutation.len() { + return Err(RusticolError::integrity( + "on-the-fly public momentum slot is out of bounds", + )); + } + for lorentz in 0..lorentz_count { + // The checked total shape bounds every plane offset. Walk the + // strided input and contiguous destination directly instead of + // repeating checked index arithmetic for every scalar. + let input_offset = public_slot * lorentz_count + lorentz; + let output_offset = (source_slot * lorentz_count + lorentz) * point_count_usize; + let output = &mut source_major[output_offset..output_offset + point_count_usize]; + for (value, source) in output + .iter_mut() + .zip(point_major[input_offset..].iter().step_by(point_width)) + { + *value = *source; + } + } + } + Ok(expected) +} + +#[cfg(feature = "on-the-fly-test-support")] +struct OnTheFlyQueryTraceCacheV1 { + entries: BTreeMap, + trace_build_count: u32, + trace_cache_hit_count: std::cell::Cell, +} + +#[cfg(feature = "on-the-fly-test-support")] +impl Default for OnTheFlyQueryTraceCacheV1 { + fn default() -> Self { + Self { + entries: BTreeMap::new(), + trace_build_count: 0, + trace_cache_hit_count: std::cell::Cell::new(0), + } + } +} + +#[cfg(feature = "on-the-fly-test-support")] +impl OnTheFlyQueryTraceCacheV1 { + fn insert_built(&mut self, query_digest: SemanticDigest, trace: T) -> RusticolResult<()> { + match self.entries.entry(query_digest) { + std::collections::btree_map::Entry::Vacant(entry) => { + entry.insert(trace); + } + std::collections::btree_map::Entry::Occupied(_) => { + return Err(RusticolError::integrity( + "on-the-fly trace cache repeated a fresh query digest", + )); + } + } + self.trace_build_count = self + .trace_build_count + .checked_add(1) + .ok_or_else(|| RusticolError::integrity("on-the-fly trace-build counter overflowed"))?; + Ok(()) + } + + fn prepared(&self, query_digest: SemanticDigest) -> RusticolResult<&T> { + self.entries.get(&query_digest).ok_or_else(|| { + RusticolError::integrity("fresh on-the-fly trace disappeared from its cache") + }) + } + + fn prepared_mut(&mut self, query_digest: SemanticDigest) -> RusticolResult<&mut T> { + self.entries.get_mut(&query_digest).ok_or_else(|| { + RusticolError::integrity("fresh on-the-fly trace disappeared from its cache") + }) + } + + fn lookup(&self, query_digest: SemanticDigest) -> RusticolResult<&T> { + let trace = self.entries.get(&query_digest).ok_or_else(|| { + RusticolError::integrity("on-the-fly benchmark query disappeared from its trace cache") + })?; + self.trace_cache_hit_count.set( + self.trace_cache_hit_count + .get() + .checked_add(1) + .ok_or_else(|| { + RusticolError::integrity("on-the-fly trace-cache-hit counter overflowed") + })?, + ); + Ok(trace) + } + + fn counts(&self) -> (u32, u32) { + (self.trace_build_count, self.trace_cache_hit_count.get()) + } +} + +#[cfg(feature = "on-the-fly-test-support")] +fn validate_on_the_fly_probe_outputs( + raw_amplitudes: &[[f64; 2]], + normalized_values: &[f64], +) -> RusticolResult<()> { + if raw_amplitudes.len() != normalized_values.len() { + return Err(RusticolError::integrity( + "on-the-fly probe output lengths disagree", + )); + } + for (point, ([real, imaginary], normalized)) in + raw_amplitudes.iter().zip(normalized_values).enumerate() + { + if !real.is_finite() || !imaginary.is_finite() { + return Err(RusticolError::evaluation(format!( + "on-the-fly probe produced a non-finite raw amplitude at point {point}" + ))); + } + if !normalized.is_finite() { + return Err(RusticolError::evaluation(format!( + "on-the-fly probe produced a non-finite normalized value at point {point}" + ))); + } + } + Ok(()) +} + +#[cfg(feature = "on-the-fly-test-support")] +fn validate_on_the_fly_family_execution( + census: crate::recurrence::OnTheFlyQueryFamilyCensusV1, + report: crate::recurrence::on_the_fly::OnTheFlyQueryFamilyExecutionReportV1, + require_cache_hit: bool, +) -> RusticolResult<()> { + if report.cache_hit != require_cache_hit + || report.source_calls != census.union_source_executor_call_groups + || report.source_rows != census.union_source_rows + || report.contribution_calls != census.union_contribution_executor_call_groups + || report.contribution_rows != census.union_contribution_rows + || report.finalization_calls != census.union_finalization_executor_call_groups + || report.finalization_rows != census.union_finalization_rows + || report.closure_calls != census.union_closure_executor_call_groups + || report.closure_rows != census.union_closure_rows + { + return Err(RusticolError::integrity( + "on-the-fly family execution differs from its prepared census/cache state", + )); + } + Ok(()) +} + +#[cfg(feature = "on-the-fly-test-support")] +#[allow(clippy::too_many_arguments)] +fn execute_on_the_fly_query_family_v1( + artifact: &VerifiedArtifact, + manifest: &RecurrenceExecutionManifest, + authenticated: &AuthenticatedRecurrenceBuilderInput, + direct: &PreparedDirectExecutorCatalog, + selected_public_flow_id: u32, + public_helicities: &[i32], + query_family: &[(u32, Vec)], + point_major_external_momenta: &[f64], + point_count: u32, + parameter_overrides: &BTreeMap, + benchmark: bool, + benchmark_warmup_repetitions: u32, + benchmark_repetitions: u32, + enable_color_projection: bool, +) -> RusticolResult { + if query_family.is_empty() { + return Err(RusticolError::invalid_argument( + "on-the-fly query family must not be empty", + )); + } + if query_family[0].0 != selected_public_flow_id + || query_family[0].1.as_slice() != public_helicities + { + return Err(RusticolError::invalid_argument( + "on-the-fly query family must begin with the required single selector", + )); + } + if benchmark && benchmark_warmup_repetitions < 2 { + return Err(RusticolError::invalid_argument( + "on-the-fly family benchmark requires at least two warmup repetitions", + )); + } + + let selected_with_seed = query_family + .iter() + .map(|(flow, helicities)| { + build_on_the_fly_selected_trace_v1( + authenticated, + direct, + *flow, + helicities, + enable_color_projection, + ) + }) + .collect::>>()?; + let seed = selected_with_seed + .first() + .ok_or_else(|| RusticolError::integrity("on-the-fly family compact seed is absent"))? + .seed + .clone(); + let seed_digest = seed.semantic_digest(); + let mut selected = selected_with_seed + .into_iter() + .map(|selected| { + if selected.seed.semantic_digest() != seed_digest + || selected.query.process_seed_digest() != seed_digest + || selected.trace.seed_digest() != seed_digest + { + return Err(RusticolError::integrity( + "on-the-fly query family does not share one compact process identity", + )); + } + Ok(OnTheFlySelectedQueryTraceV1 { + query: selected.query, + trace: selected.trace, + projection: selected.projection, + }) + }) + .collect::>>()?; + let (pack_json, pack, payload_root) = load_prepared_pack(artifact, manifest)?; + let payloads = artifact.evaluator_payload_store(&payload_root)?; + let runtime_parameters = recurrence_runtime_parameters(&manifest.runtime_metadata); + let model_parameter_evaluator = + super::eager_load::load_prepared_model_parameter_evaluator_for_runtime( + &pack, + &runtime_parameters, + &payloads, + )?; + let pool = NativeRecurrencePreparedExecutorPool::load_from_store( + &pack_json, + &payloads, + &manifest.prepared_kernel_pack_digest, + &manifest.direct_template_catalog_digest, + )?; + let source_domains = build_on_the_fly_source_domains(&seed, authenticated.template())?; + let sources = pool.bind_source_domains(source_domains)?; + let mut resolver = pool.into_on_the_fly_resolver(sources); + resolver.bind_on_the_fly_family(authenticated.template(), direct, &seed, &mut selected)?; + let semantic_executor_binding_count = resolver.semantic_executor_binding_count()?; + let distinct_prepared_executor_count = resolver.distinct_prepared_executor_count()?; + let trace_inputs = selected + .iter() + .map(|selected| QueryFamilyTraceInput { + trace: &selected.trace, + projection: selected.projection, + }) + .collect::>(); + let mut executor = OnTheFlyQueryFamilyExecutorV1::new(resolver); + let prepare_started = Instant::now(); + if executor.prepare(direct, &trace_inputs, point_count)? { + return Err(RusticolError::integrity( + "fresh on-the-fly family executor reported a cold cache hit", + )); + } + let cold_prepare_seconds = profile_duration_seconds(prepare_started.elapsed()); + let census = executor + .prepared_census() + .ok_or_else(|| RusticolError::integrity("prepared on-the-fly family census is absent"))?; + let prepared_parameters = on_the_fly_prepared_parameters( + &manifest.runtime_metadata, + &runtime_parameters, + model_parameter_evaluator, + parameter_overrides, + )? + .into_iter() + .map(|value| (value[0], value[1])) + .collect::>(); + executor.set_parameters(&prepared_parameters)?; + let source_major = on_the_fly_source_major_momenta( + &seed, + point_major_external_momenta, + point_count, + selected[0].trace.layout().lorentz_component_count(), + )?; + let output_len = query_family + .len() + .checked_mul(point_count as usize) + .ok_or_else(|| RusticolError::invalid_argument("on-the-fly family output exceeds usize"))?; + let mut outputs = vec![(0.0, 0.0); output_len]; + let mut execution_count = 0_u32; + let mut last_report = None; + let benchmark_elapsed_seconds = if benchmark { + for warmup in 0..benchmark_warmup_repetitions { + let report = executor.execute_into(&source_major, point_count, &mut outputs)?; + validate_on_the_fly_family_execution(census, report, warmup != 0)?; + execution_count = execution_count.checked_add(1).ok_or_else(|| { + RusticolError::integrity("on-the-fly family execution count exceeds u32") + })?; + last_report = Some(report); + std::hint::black_box(&outputs); + } + let started = Instant::now(); + for _ in 0..benchmark_repetitions { + let report = executor.execute_into(&source_major, point_count, &mut outputs)?; + validate_on_the_fly_family_execution(census, report, true)?; + execution_count = execution_count.checked_add(1).ok_or_else(|| { + RusticolError::integrity("on-the-fly family execution count exceeds u32") + })?; + last_report = Some(report); + std::hint::black_box(&outputs); + } + Some(profile_duration_seconds(started.elapsed())) + } else { + let report = executor.execute_into(&source_major, point_count, &mut outputs)?; + validate_on_the_fly_family_execution(census, report, false)?; + execution_count = 1; + last_report = Some(report); + None + }; + let report = last_report.ok_or_else(|| { + RusticolError::integrity("on-the-fly family produced no execution report") + })?; + let normalization_factor = recurrence_normalization_values(&manifest.runtime_metadata)?.factor; + let mut query_reports = Vec::new(); + for ((flow, helicities), (selected_query, amplitudes)) in query_family.iter().zip( + selected + .iter() + .zip(outputs.chunks_exact(point_count as usize)), + ) { + let raw_amplitudes = amplitudes + .iter() + .map(|&(real, imaginary)| [real, imaginary]) + .collect::>(); + let normalized_values = raw_amplitudes + .iter() + .map(|[real, imaginary]| { + real.mul_add(*real, imaginary * imaginary) * normalization_factor + }) + .collect::>(); + validate_on_the_fly_probe_outputs(&raw_amplitudes, &normalized_values)?; + query_reports.push(NativeOnTheFlyFamilyQueryProbeV1 { + selected_public_flow_id: *flow, + public_helicities: helicities.clone(), + query_digest: selected_query.query.semantic_digest().to_string(), + raw_amplitudes, + normalized_values, + }); + } + let first = query_reports + .first() + .ok_or_else(|| RusticolError::integrity("on-the-fly family lost its first query output"))?; + let first_raw_amplitudes = first.raw_amplitudes.clone(); + let first_normalized_values = first.normalized_values.clone(); + let benchmark_seconds_per_point = benchmark_elapsed_seconds + .map(|elapsed| elapsed / (f64::from(benchmark_repetitions) * f64::from(point_count))); + let first_selected = &selected[0]; + Ok(NativeOnTheFlyArtifactProbeV1 { + artifact_id: artifact.manifest().artifact_id.clone(), + process_id: manifest.key.clone(), + seed_digest: seed.semantic_digest().to_string(), + query_digest: first_selected.query.semantic_digest().to_string(), + trace_digest: first_selected.trace.semantic_digest().to_string(), + point_count, + raw_amplitudes: first_raw_amplitudes, + normalized_values: first_normalized_values, + normalization_factor, + query_family: Some(NativeOnTheFlyFamilyProbeV1 { + queries: query_reports, + census, + execution_cache_hit: report.cache_hit, + execution_source_calls: report.source_calls, + execution_source_rows: report.source_rows, + execution_contribution_calls: report.contribution_calls, + execution_contribution_rows: report.contribution_rows, + execution_finalization_calls: report.finalization_calls, + execution_finalization_rows: report.finalization_rows, + execution_closure_calls: report.closure_calls, + execution_closure_rows: report.closure_rows, + cold_prepare_seconds, + benchmark_warmup_repetitions, + benchmark_repetitions, + benchmark_elapsed_seconds, + benchmark_seconds_per_point, + }), + currents: Vec::new(), + projection_enabled: first_selected.projection.enabled, + projection_applied: first_selected.projection.applied, + projection_counts: [ + first_selected.projection.pre, + first_selected.projection.post, + ], + work_census_basis: ON_THE_FLY_FAMILY_WORK_CENSUS_BASIS_V1.to_owned(), + logical_current_count: census.union_unique_current_count, + resident_current_count: census.union_unique_current_count, + resident_current_component_count: census.union_unique_current_component_count, + source_operation_count: census.union_source_rows, + contribution_operation_count: census.union_contribution_rows, + finalization_operation_count: census.union_finalization_rows, + closure_operation_count: census.union_closure_rows, + total_kernel_application_count: census.union_kernel_application_count()?, + semantic_executor_binding_count, + distinct_prepared_executor_count, + trace_build_count: census.query_count, + // The prepared family cache is reported by `query_family`; this path + // performs no query-trace cache lookup after construction. + trace_cache_hit_count: 0, + momentum_fill_count: execution_count, + benchmark_warmup_repetitions, + benchmark_repetitions, + benchmark_elapsed_seconds, + benchmark_seconds_per_point, + direct_plan_load_attempts: 0, + direct_plan_decode_attempts: 0, + direct_plan_materialization_attempts: 0, + established_builder_attempts: 0, + }) +} + +#[cfg(feature = "on-the-fly-test-support")] +impl NativeRuntime { + /// Execute one authenticated selector-local recurrence trace directly from + /// a genuine artifact without loading or decoding its DirectPlan. + #[doc(hidden)] + #[allow(clippy::too_many_arguments)] + pub fn on_the_fly_artifact_probe_v1( + artifact_path: impl AsRef, + process_id: &str, + authenticated: &AuthenticatedRecurrenceBuilderInput, + direct: &PreparedDirectExecutorCatalog, + selected_public_flow_id: u32, + public_helicities: &[i32], + point_major_external_momenta: &[f64], + point_count: u32, + parameter_overrides: &BTreeMap, + tamper_executor_key: bool, + benchmark: bool, + benchmark_warmup_repetitions: u32, + benchmark_repetitions: u32, + collect_current_diagnostics: bool, + enable_color_projection: bool, + query_family: Option<&[(u32, Vec)]>, + ) -> RusticolResult { + if benchmark && benchmark_repetitions == 0 { + return Err(RusticolError::invalid_argument( + "on-the-fly benchmark requires at least one timed repetition", + )); + } + if !benchmark && (benchmark_warmup_repetitions != 0 || benchmark_repetitions != 0) { + return Err(RusticolError::invalid_argument( + "on-the-fly benchmark repetitions require benchmark=true", + )); + } + if query_family.is_some() && collect_current_diagnostics { + return Err(RusticolError::invalid_argument( + "on-the-fly family probing does not collect per-current diagnostics", + )); + } + if query_family.is_some() && tamper_executor_key { + return Err(RusticolError::invalid_argument( + "on-the-fly family probing does not support executor-key tampering", + )); + } + let guard = OnTheFlyForbiddenWorkGuardV1::begin()?; + let result = (|| { + let artifact = VerifiedArtifact::open(artifact_path)?; + let selection = artifact.select_process(Some(process_id))?; + if selection.remaps_public_process() { + return Err(RusticolError::invalid_argument( + "on-the-fly artifact probing requires a representative process ID", + )); + } + let (loaded, _evaluator_root) = load_verified_evaluator(&artifact, &selection)?; + let physics_bytes = artifact.read_payload(&selection.process.physics_path)?; + let physics = + ProcessPhysicsV1::from_json(&physics_bytes, &selection.process.physics_path)?; + if physics.process_id != selection.process.id + || physics.process != selection.process.expression + || physics.color_accuracy.as_str() != selection.process.color_accuracy + || physics + .external_particles + .iter() + .map(|particle| particle.pdg) + .ne(selection.process.external_pdgs.iter().copied()) + { + return Err(RusticolError::integrity(format!( + "runtime physics payload {:?} does not match process {:?}", + selection.process.physics_path, selection.process.id + ))); + } + let manifest = match loaded { + LoadedExecutionManifest::Recurrence(manifest) => manifest, + LoadedExecutionManifest::Compiled(_) + | LoadedExecutionManifest::EagerV3(_) + | LoadedExecutionManifest::OnTheFly(_) => { + return Err(RusticolError::compatibility( + "on-the-fly artifact probing requires a recurrence artifact", + )); + } + }; + if authenticated + .process() + .semantic_identity() + .input_digest() + .to_string() + != manifest.plan.builder_input_sha256 + || direct.direct_template_catalog_digest().to_string() + != manifest.direct_template_catalog_digest + || authenticated + .template() + .summary() + .prepared_kernel_pack_digest + .to_string() + != manifest.prepared_kernel_pack_digest + { + return Err(RusticolError::integrity( + "retained on-the-fly canonical inputs do not belong to the selected artifact", + )); + } + + if let Some(query_family) = query_family { + return execute_on_the_fly_query_family_v1( + &artifact, + &manifest, + authenticated, + direct, + selected_public_flow_id, + public_helicities, + query_family, + point_major_external_momenta, + point_count, + parameter_overrides, + benchmark, + benchmark_warmup_repetitions, + benchmark_repetitions, + enable_color_projection, + ); + } + + let mut trace_cache = OnTheFlyQueryTraceCacheV1::default(); + let selected = build_on_the_fly_selected_trace_v1( + authenticated, + direct, + selected_public_flow_id, + public_helicities, + enable_color_projection, + )?; + let query_digest = selected.query.semantic_digest(); + trace_cache.insert_built(query_digest, selected)?; + if tamper_executor_key { + trace_cache + .prepared_mut(query_digest)? + .trace + .tamper_first_prepared_executor_key_for_probe()?; + } + let (pack_json, pack, payload_root) = load_prepared_pack(&artifact, &manifest)?; + let payloads = artifact.evaluator_payload_store(&payload_root)?; + let runtime_parameters = recurrence_runtime_parameters(&manifest.runtime_metadata); + let model_parameter_evaluator = + super::eager_load::load_prepared_model_parameter_evaluator_for_runtime( + &pack, + &runtime_parameters, + &payloads, + )?; + let pool = NativeRecurrencePreparedExecutorPool::load_from_store( + &pack_json, + &payloads, + &manifest.prepared_kernel_pack_digest, + &manifest.direct_template_catalog_digest, + )?; + let selected = trace_cache.prepared(query_digest)?; + let source_domains = + build_on_the_fly_source_domains(&selected.seed, authenticated.template())?; + let sources = pool.bind_source_domains(source_domains)?; + let mut resolver = pool.into_on_the_fly_resolver(sources); + { + let selected = trace_cache.prepared_mut(query_digest)?; + resolver.bind_on_the_fly_trace( + authenticated.template(), + direct, + &selected.seed, + &mut selected.trace, + )?; + } + let selected = trace_cache.prepared(query_digest)?; + let work_census = selected.trace.execution_work_census()?; + let semantic_executor_binding_count = resolver.semantic_executor_binding_count()?; + let distinct_prepared_executor_count = resolver.distinct_prepared_executor_count()?; + let mut workspace = OnTheFlyWorkspaceV1::new(&selected.trace, point_count)?; + for (parameter_id, value) in on_the_fly_prepared_parameters( + &manifest.runtime_metadata, + &runtime_parameters, + model_parameter_evaluator, + parameter_overrides, + )? + .into_iter() + .enumerate() + { + workspace.set_parameter( + u32::try_from(parameter_id).map_err(|_| { + RusticolError::artifact("on-the-fly prepared parameter count exceeds u32") + })?, + value[0], + value[1], + )?; + } + let source_major = on_the_fly_source_major_momenta( + &selected.seed, + point_major_external_momenta, + point_count, + selected.trace.layout().lorentz_component_count(), + )?; + let mut momentum_fill_count = 0_u32; + let benchmark_elapsed_seconds = if !benchmark { + workspace.fill_momenta_from_external( + &selected.trace, + &source_major, + point_count, + )?; + momentum_fill_count = 1; + OnTheFlyStructuralInterpreter::execute( + &selected.trace, + &resolver, + &mut workspace, + point_count, + )?; + None + } else { + for _ in 0..benchmark_warmup_repetitions { + let cached = trace_cache.lookup(std::hint::black_box(query_digest))?; + workspace.fill_momenta_from_external( + &cached.trace, + &source_major, + point_count, + )?; + momentum_fill_count = momentum_fill_count.checked_add(1).ok_or_else(|| { + RusticolError::integrity("on-the-fly momentum-fill counter overflowed") + })?; + OnTheFlyStructuralInterpreter::execute( + &cached.trace, + &resolver, + &mut workspace, + point_count, + )?; + std::hint::black_box(workspace.amplitude(0)?); + } + let started = Instant::now(); + for _ in 0..benchmark_repetitions { + let cached = trace_cache.lookup(std::hint::black_box(query_digest))?; + workspace.fill_momenta_from_external( + &cached.trace, + &source_major, + point_count, + )?; + momentum_fill_count = momentum_fill_count.checked_add(1).ok_or_else(|| { + RusticolError::integrity("on-the-fly momentum-fill counter overflowed") + })?; + OnTheFlyStructuralInterpreter::execute( + &cached.trace, + &resolver, + &mut workspace, + point_count, + )?; + std::hint::black_box(workspace.amplitude(0)?); + } + Some(profile_duration_seconds(started.elapsed())) + }; + let benchmark_seconds_per_point = benchmark_elapsed_seconds.map(|elapsed| { + elapsed / (f64::from(benchmark_repetitions) * f64::from(point_count)) + }); + let (trace_build_count, trace_cache_hit_count) = trace_cache.counts(); + + let raw_amplitudes = (0..point_count) + .map(|point| workspace.amplitude(point).map(|(real, imag)| [real, imag])) + .collect::>>()?; + let normalization_factor = + recurrence_normalization_values(&manifest.runtime_metadata)?.factor; + let normalized_values: Vec = raw_amplitudes + .iter() + .map(|[real, imaginary]| { + (real.mul_add(*real, imaginary * imaginary)) * normalization_factor + }) + .collect(); + validate_on_the_fly_probe_outputs(&raw_amplitudes, &normalized_values)?; + let current_count = selected.trace.proof().current_count(); + let currents = if collect_current_diagnostics { + (0..current_count) + .map(|current_id| { + let [_, component_count] = + selected.trace.current_component_range(current_id)?; + let mut values = + Vec::with_capacity(point_count as usize * component_count as usize); + for point in 0..point_count { + values.extend( + workspace + .observed_current_components( + &selected.trace, + current_id, + point, + )? + .into_iter() + .map(|(real, imaginary)| [real, imaginary]), + ); + } + Ok(NativeOnTheFlyCurrentProbeV1 { + semantic_digest: selected + .trace + .current_semantic_digest(current_id)? + .to_string(), + component_count, + values, + }) + }) + .collect::>>()? + } else { + Vec::new() + }; + + Ok(NativeOnTheFlyArtifactProbeV1 { + artifact_id: artifact.manifest().artifact_id.clone(), + process_id: manifest.key.clone(), + seed_digest: selected.seed.semantic_digest().to_string(), + query_digest: selected.query.semantic_digest().to_string(), + trace_digest: selected.trace.semantic_digest().to_string(), + point_count, + raw_amplitudes, + normalized_values, + query_family: None, + normalization_factor, + currents, + projection_enabled: selected.projection.enabled, + projection_applied: selected.projection.applied, + projection_counts: [selected.projection.pre, selected.projection.post], + work_census_basis: ON_THE_FLY_WORK_CENSUS_BASIS_V1.to_owned(), + logical_current_count: work_census.logical_current_count, + resident_current_count: work_census.resident_current_count, + resident_current_component_count: work_census.resident_current_component_count, + source_operation_count: work_census.source_operation_count, + contribution_operation_count: work_census.contribution_operation_count, + finalization_operation_count: work_census.finalization_operation_count, + closure_operation_count: work_census.closure_operation_count, + total_kernel_application_count: work_census.total_kernel_application_count, + semantic_executor_binding_count, + distinct_prepared_executor_count, + trace_build_count, + trace_cache_hit_count, + momentum_fill_count, + benchmark_warmup_repetitions, + benchmark_repetitions, + benchmark_elapsed_seconds, + benchmark_seconds_per_point, + direct_plan_load_attempts: 0, + direct_plan_decode_attempts: 0, + direct_plan_materialization_attempts: 0, + established_builder_attempts: 0, + }) + })(); + let counts = guard.finish()?; + let mut report = result?; + report.direct_plan_load_attempts = counts.direct_plan_load_attempts; + report.direct_plan_decode_attempts = counts.direct_plan_decode_attempts; + report.direct_plan_materialization_attempts = counts.direct_plan_materialization_attempts; + report.established_builder_attempts = counts.established_builder_attempts; + if report.direct_plan_load_attempts != 0 + || report.direct_plan_decode_attempts != 0 + || report.direct_plan_materialization_attempts != 0 + || report.established_builder_attempts != 0 + { + return Err(RusticolError::integrity( + "on-the-fly artifact probe observed forbidden global work", + )); + } + Ok(report) + } +} + +#[cfg(feature = "on-the-fly-test-support")] +fn direct_public_helicity_id( + plan: &DirectRecurrencePlan, + public_helicities: &[i32], +) -> RusticolResult { + let mut matched = None; + for descriptor in plan.resolved_helicities() { + let start = usize::try_from(descriptor.public_helicity_start) + .map_err(|_| RusticolError::integrity("direct public-helicity start exceeds usize"))?; + let end = start + .checked_add(descriptor.public_helicity_count as usize) + .ok_or_else(|| RusticolError::integrity("direct public-helicity range overflows"))?; + if plan.public_helicities().get(start..end) == Some(public_helicities) { + if matched.replace(descriptor.id).is_some() { + return Err(RusticolError::integrity( + "direct plan repeats the requested public helicity", + )); + } + } + } + matched.ok_or_else(|| { + RusticolError::invalid_argument( + "requested public helicities are absent from the recurrence comparator", + ) + }) +} + +#[cfg(any(test, feature = "on-the-fly-test-support"))] +fn invert_replay_helicity_map( + public_flow_id: u32, + helicity_map: &[u32], + public_direct_helicity_id: u32, +) -> RusticolResult { + let matches = helicity_map + .iter() + .copied() + .enumerate() + .filter(|(_, mapped)| *mapped == public_direct_helicity_id) + .map(|(representative, _)| representative) + .collect::>(); + let [representative] = matches.as_slice() else { + return Err(RusticolError::integrity(format!( + "replay flow {} maps {} representative helicities to public helicity {}", + public_flow_id, + matches.len(), + public_direct_helicity_id, + ))); + }; + u32::try_from(*representative) + .map_err(|_| RusticolError::integrity("representative helicity ID exceeds u32")) +} + +#[cfg(feature = "on-the-fly-test-support")] +fn representative_direct_helicity_id( + selector: &DirectReplaySelectorPlan, + public_direct_helicity_id: u32, +) -> RusticolResult { + invert_replay_helicity_map( + selector.public_flow_id(), + selector.helicity_map(), + public_direct_helicity_id, + ) +} + +#[cfg(feature = "on-the-fly-test-support")] +fn direct_representative_source_states( + plan: &DirectRecurrencePlan, + representative_helicity_id: u32, +) -> RusticolResult> { + let descriptor = plan + .resolved_helicities() + .get(representative_helicity_id as usize) + .filter(|descriptor| descriptor.id == representative_helicity_id) + .ok_or_else(|| RusticolError::integrity("representative helicity is absent"))?; + let start = usize::try_from(descriptor.source_state_start) + .map_err(|_| RusticolError::integrity("source-state start exceeds usize"))?; + let end = start + .checked_add(descriptor.source_state_count as usize) + .ok_or_else(|| RusticolError::integrity("source-state range overflows usize"))?; + plan.source_state_assignments() + .get(start..end) + .ok_or_else(|| RusticolError::integrity("source-state range is absent"))? + .iter() + .map(|assignment| Ok((assignment.source_slot, assignment.state_index))) + .collect() +} + +#[cfg(feature = "on-the-fly-test-support")] +fn current_belongs_to_representative_helicity( + current: &crate::recurrence::RecurrenceCurrent, + source_states: &BTreeMap, +) -> bool { + current + .key() + .helicity_identity() + .local_source_states() + .iter() + .all(|assignment| { + source_states.get(&assignment.source_slot()) == Some(&assignment.state_index()) + }) +} + +#[cfg(feature = "on-the-fly-test-support")] +fn diagnostic_complex_scale(value: (f64, f64), scale: (f64, f64)) -> (f64, f64) { + ( + value.0 * scale.0 - value.1 * scale.1, + value.0 * scale.1 + value.1 * scale.0, + ) +} + +#[cfg(feature = "on-the-fly-test-support")] +impl NativeRuntime { + /// Compare point zero from a genuine compact artifact with a separately + /// authenticated recurrence artifact. Neither artifact is treated as a + /// carrier for the other's execution payload. + #[doc(hidden)] + #[allow(clippy::too_many_arguments)] + pub fn on_the_fly_execution_diagnostic_v1( + on_the_fly_artifact_path: impl AsRef, + on_the_fly_process_id: &str, + recurrence_artifact_path: impl AsRef, + recurrence_process_id: &str, + authenticated: &AuthenticatedRecurrenceBuilderInput, + retained_direct: &PreparedDirectExecutorCatalog, + selected_public_flow_id: u32, + public_helicities: &[i32], + point_major_external_momenta: &[f64], + parameter_overrides: &BTreeMap, + ) -> RusticolResult { + let on_the_fly_artifact = VerifiedArtifact::open(on_the_fly_artifact_path)?; + let on_the_fly_selection = + on_the_fly_artifact.select_process(Some(on_the_fly_process_id))?; + if on_the_fly_selection.remaps_public_process() { + return Err(RusticolError::invalid_argument( + "execution diagnostics require a representative on-the-fly process ID", + )); + } + let (loaded_on_the_fly, on_the_fly_evaluator_root) = + load_verified_evaluator(&on_the_fly_artifact, &on_the_fly_selection)?; + let on_the_fly_manifest = match loaded_on_the_fly { + LoadedExecutionManifest::OnTheFly(manifest) => manifest, + _ => { + return Err(RusticolError::compatibility( + "execution diagnostic artifact A must be a genuine on-the-fly artifact", + )); + } + }; + let loaded_on_the_fly = super::on_the_fly_load::load_on_the_fly_native_runtime( + &on_the_fly_artifact, + &on_the_fly_evaluator_root, + &on_the_fly_manifest, + &on_the_fly_selection, + )?; + let super::on_the_fly_load::LoadedOnTheFlyRuntime { + common: mut on_the_fly_common, + lane: mut on_the_fly_lane, + selectors: _, + metadata_selectors: _, + public_metadata: _, + color_contraction: _, + } = loaded_on_the_fly; + + let seed = on_the_fly_lane.seed(); + let retained_summary = authenticated.template().summary(); + if seed.process_digest() != authenticated.process().semantic_identity().process_digest() + || seed.model_digest() != retained_summary.compiled_model_digest + || seed.template_catalog_digest() != retained_summary.catalog_digest + || seed.prepared_pack_digest() != retained_summary.prepared_kernel_pack_digest + || seed.direct_catalog_digest() != retained_direct.direct_template_catalog_digest() + { + return Err(RusticolError::integrity( + "retained diagnostic inputs do not belong to on-the-fly artifact A", + )); + } + let on_the_fly_seed_model_digest = seed.model_digest(); + let on_the_fly_seed_template_digest = seed.template_catalog_digest(); + let on_the_fly_seed_pack_digest = seed.prepared_pack_digest(); + let on_the_fly_seed_direct_digest = seed.direct_catalog_digest(); + + let overrides = parameter_overrides + .iter() + .map(|(name, value)| (name.clone(), (value[0], value[1]))) + .collect::>(); + on_the_fly_common.apply_model_parameter_overrides(&overrides)?; + let on_the_fly_snapshot = on_the_fly_lane.execution_diagnostic_v1( + authenticated, + selected_public_flow_id, + public_helicities, + point_major_external_momenta, + &on_the_fly_common.model_parameter_values_f64, + )?; + + // The compact probe guard is deliberately not active here: artifact B + // is an independent recurrence comparator whose DirectPlan must load. + let recurrence_artifact = VerifiedArtifact::open(recurrence_artifact_path)?; + let recurrence_selection = + recurrence_artifact.select_process(Some(recurrence_process_id))?; + if recurrence_selection.remaps_public_process() { + return Err(RusticolError::invalid_argument( + "execution diagnostics require a representative recurrence process ID", + )); + } + let (loaded_recurrence, recurrence_evaluator_root) = + load_verified_evaluator(&recurrence_artifact, &recurrence_selection)?; + let recurrence_manifest = match loaded_recurrence { + LoadedExecutionManifest::Recurrence(manifest) => manifest, + _ => { + return Err(RusticolError::compatibility( + "execution diagnostic artifact B must be a recurrence artifact", + )); + } + }; + if on_the_fly_selection.process.expression != recurrence_selection.process.expression + || on_the_fly_selection.process.external_pdgs + != recurrence_selection.process.external_pdgs + || on_the_fly_selection.process.color_accuracy + != recurrence_selection.process.color_accuracy + || recurrence_manifest.plan.builder_input_sha256 + != authenticated + .process() + .semantic_identity() + .input_digest() + .to_string() + || recurrence_manifest.prepared_kernel_pack_digest + != on_the_fly_seed_pack_digest.to_string() + || recurrence_manifest.direct_template_catalog_digest + != on_the_fly_seed_direct_digest.to_string() + { + return Err(RusticolError::integrity( + "on-the-fly artifact A, recurrence artifact B, and retained inputs have different identities", + )); + } + + let plan = load_plan( + &recurrence_artifact, + &recurrence_evaluator_root, + &recurrence_manifest, + )? + .plan; + let (recurrence_pack_bytes, mut recurrence_pack, recurrence_payload_root) = + load_prepared_pack(&recurrence_artifact, &recurrence_manifest)?; + drop(recurrence_pack_bytes); + let recurrence_direct_manifest = recurrence_pack.recurrence_direct_template_catalog( + &recurrence_manifest.prepared_kernel_pack_digest, + &recurrence_manifest.direct_template_catalog_digest, + )?; + if recurrence_direct_manifest.compiled_model_digest + != on_the_fly_seed_model_digest.to_string() + || recurrence_direct_manifest.recurrence_template_catalog_digest + != on_the_fly_seed_template_digest.to_string() + { + return Err(RusticolError::integrity( + "artifact A and comparator B use different model/template catalogs", + )); + } + let recurrence_payloads = + recurrence_artifact.evaluator_payload_store(&recurrence_payload_root)?; + let recurrence_runtime_parameters = + recurrence_runtime_parameters(&recurrence_manifest.runtime_metadata); + let recurrence_parameter_evaluator = + super::eager_load::load_prepared_model_parameter_evaluator_for_runtime( + &recurrence_pack, + &recurrence_runtime_parameters, + &recurrence_payloads, + )?; + let recurrence_prepared_parameters = on_the_fly_prepared_parameters( + &recurrence_manifest.runtime_metadata, + &recurrence_runtime_parameters, + recurrence_parameter_evaluator, + parameter_overrides, + )?; + if recurrence_prepared_parameters.len() != on_the_fly_snapshot.prepared_parameters.len() + || recurrence_prepared_parameters + .iter() + .zip(&on_the_fly_snapshot.prepared_parameters) + .any(|(recurrence, on_the_fly)| { + recurrence[0].to_bits() != on_the_fly.0.to_bits() + || recurrence[1].to_bits() != on_the_fly.1.to_bits() + }) + { + return Err(RusticolError::integrity( + "artifacts A and B prepared different parameter vectors", + )); + } + + let recurrence_runtime_slots = runtime_parameter_slots(&recurrence_runtime_parameters)?; + let source_domains = build_direct_source_domains( + &plan, + &recurrence_manifest.runtime_metadata, + &recurrence_runtime_slots, + )?; + let backend = NativeRecurrenceDirectExecutorBackend::load_from_verified_artifact( + &mut recurrence_pack, + &recurrence_artifact, + &recurrence_payload_root, + &plan, + &recurrence_manifest.prepared_kernel_pack_digest, + &recurrence_manifest.direct_template_catalog_digest, + source_domains, + )?; + let (executors, _backend_owners) = backend.into_parts(); + let mut direct_runtime = DirectRecurrenceExecutionRuntime::new(plan.clone(), executors, 4)?; + let parameter_re = on_the_fly_snapshot + .prepared_parameters + .iter() + .map(|value| value.0) + .collect::>(); + let parameter_im = on_the_fly_snapshot + .prepared_parameters + .iter() + .map(|value| value.1) + .collect::>(); + direct_runtime.set_parameters(¶meter_re, ¶meter_im)?; + let selector = direct_runtime.prepare_replay_selector(selected_public_flow_id)?; + let public_direct_helicity_id = direct_public_helicity_id(&plan, public_helicities)?; + let representative_direct_helicity_id = + representative_direct_helicity_id(&selector, public_direct_helicity_id)?; + let representative_source_states = + direct_representative_source_states(&plan, representative_direct_helicity_id)?; + let destination = plan + .amplitude_destinations() + .iter() + .filter(|destination| { + destination.target_sector_id == selector.representative_flow_id() + && destination.target_helicity_id_or_sentinel + == representative_direct_helicity_id + }) + .copied() + .collect::>(); + let [destination] = destination.as_slice() else { + return Err(RusticolError::integrity(format!( + "selected recurrence lane has {} amplitude destinations, expected one", + destination.len(), + ))); + }; + + begin_direct_current_observation( + direct_runtime.plan(), + Some(selector.representative_flow_id()), + 1, + )?; + let execution = direct_runtime + .execute_replay_tile_from_external_unprofiled( + &selector, + 1, + point_major_external_momenta, + ) + .and_then(|output| { + Ok(( + *output + .destination_re(destination.id) + .and_then(|values| values.first()) + .ok_or_else(|| { + RusticolError::integrity( + "selected recurrence public amplitude real value is absent", + ) + })?, + *output + .destination_im(destination.id) + .and_then(|values| values.first()) + .ok_or_else(|| { + RusticolError::integrity( + "selected recurrence public amplitude imaginary value is absent", + ) + })?, + )) + }); + let observation = take_direct_current_observation(); + let recurrence_public_amplitude = execution?; + let observation: DirectCurrentObservation = observation?; + let recurrence_representative_amplitude = observation + .amplitudes_before_replay + .get(&destination.id) + .and_then(|values| values.first()) + .copied() + .ok_or_else(|| { + RusticolError::integrity( + "selected recurrence representative amplitude was not observed", + ) + })?; + let phase = selector.phase(); + let scale = ( + phase.0 * f64::from(selector.multiplicity()), + phase.1 * f64::from(selector.multiplicity()), + ); + let expected_public = diagnostic_complex_scale(recurrence_representative_amplitude, scale); + if expected_public.0.to_bits() != recurrence_public_amplitude.0.to_bits() + || expected_public.1.to_bits() != recurrence_public_amplitude.1.to_bits() + { + return Err(RusticolError::integrity( + "recurrence replay scale was not applied exactly once", + )); + } + + let established = authenticated.build()?; + if established.currents().len() != plan.currents().len() { + return Err(RusticolError::integrity( + "retained established current catalog differs from comparator B", + )); + } + let mut on_the_fly_currents = BTreeMap::new(); + for current in &on_the_fly_snapshot.currents { + if on_the_fly_currents + .insert(current.semantic_digest, current) + .is_some() + { + return Err(RusticolError::integrity( + "on-the-fly selected trace repeats a semantic current", + )); + } + } + let mut direct_currents = BTreeMap::new(); + let mut excluded_direct_current_count = 0_u32; + for (semantic_current_id, observed) in &observation.currents { + let current = established + .currents() + .get(*semantic_current_id as usize) + .filter(|current| current.id() == *semantic_current_id) + .ok_or_else(|| { + RusticolError::integrity( + "observed Direct current is absent from retained construction", + ) + })?; + if !current_belongs_to_representative_helicity(current, &representative_source_states) { + excluded_direct_current_count = excluded_direct_current_count.saturating_add(1); + continue; + } + let color = established + .dynamic_color_states() + .get(current.key().dynamic_lc_color_state_id().get() as usize) + .ok_or_else(|| { + RusticolError::integrity("observed Direct current color state is absent") + })?; + let digest = hash_current_key(current.key(), color)?; + if !on_the_fly_currents.contains_key(&digest) { + excluded_direct_current_count = excluded_direct_current_count.saturating_add(1); + continue; + } + if direct_currents.insert(digest, observed).is_some() { + return Err(RusticolError::integrity( + "selected Direct live slice repeats a semantic current", + )); + } + } + if direct_currents.len() != on_the_fly_currents.len() { + return Err(RusticolError::integrity(format!( + "selected current bijection has {} on-the-fly and {} Direct currents", + on_the_fly_currents.len(), + direct_currents.len(), + ))); + } + + let mut ordered = on_the_fly_currents + .iter() + .map(|(digest, on_the_fly)| { + let direct = direct_currents.get(digest).copied().ok_or_else(|| { + RusticolError::integrity( + "selected Direct live slice omits an on-the-fly current", + ) + })?; + if on_the_fly.stage != u32::from(direct.descriptor.stage) + || on_the_fly.values.len() != usize::from(direct.descriptor.component_count) + || direct.values.len() != on_the_fly.values.len() + { + return Err(RusticolError::integrity( + "matched semantic current has a different depth or component layout", + )); + } + Ok((on_the_fly.stage, *digest, *on_the_fly, direct)) + }) + .collect::>>()?; + ordered.sort_by_key(|(stage, digest, _, _)| (*stage, digest.to_string())); + let mut current_components = Vec::new(); + for (stage, digest, on_the_fly, direct) in ordered { + for (component, (&on_the_fly_value, &recurrence_value)) in + on_the_fly.values.iter().zip(&direct.values).enumerate() + { + for value in [on_the_fly_value, recurrence_value] { + if !value.0.is_finite() || !value.1.is_finite() { + return Err(RusticolError::evaluation( + "execution diagnostic observed a non-finite current value", + )); + } + } + current_components.push(NativeOnTheFlyExecutionComponentV1 { + dependency_depth: stage, + semantic_digest: digest.to_string(), + component: u32::try_from(component).map_err(|_| { + RusticolError::integrity("current component index exceeds u32") + })?, + on_the_fly: [on_the_fly_value.0, on_the_fly_value.1], + recurrence: [recurrence_value.0, recurrence_value.1], + on_the_fly_bits: [on_the_fly_value.0.to_bits(), on_the_fly_value.1.to_bits()], + recurrence_bits: [recurrence_value.0.to_bits(), recurrence_value.1.to_bits()], + absolute_delta: [ + (on_the_fly_value.0 - recurrence_value.0).abs(), + (on_the_fly_value.1 - recurrence_value.1).abs(), + ], + }); + } + } + let first_raw_bit_difference = current_components + .iter() + .position(|component| component.on_the_fly_bits != component.recurrence_bits); + let raw_bit_difference_count = u32::try_from( + current_components + .iter() + .filter(|component| component.on_the_fly_bits != component.recurrence_bits) + .count(), + ) + .map_err(|_| RusticolError::integrity("raw difference count exceeds u32"))?; + let on_the_fly_public_amplitude = on_the_fly_snapshot.raw_amplitude; + for value in [ + on_the_fly_public_amplitude, + recurrence_representative_amplitude, + recurrence_public_amplitude, + ] { + if !value.0.is_finite() || !value.1.is_finite() { + return Err(RusticolError::evaluation( + "execution diagnostic observed a non-finite amplitude", + )); + } + } + + Ok(NativeOnTheFlyExecutionDiagnosticV1 { + on_the_fly_artifact_id: on_the_fly_artifact.manifest().artifact_id.clone(), + on_the_fly_process_id: on_the_fly_manifest.key.clone(), + recurrence_artifact_id: recurrence_artifact.manifest().artifact_id.clone(), + recurrence_process_id: recurrence_manifest.key.clone(), + seed_digest: on_the_fly_snapshot.seed_digest.to_string(), + query_digest: on_the_fly_snapshot.query_digest.to_string(), + trace_digest: on_the_fly_snapshot.trace_digest.to_string(), + direct_plan_semantic_digest: plan.semantic_digest().to_string(), + direct_runtime_layout_digest: plan.runtime_layout_digest().to_string(), + public_flow_id: selector.public_flow_id(), + representative_flow_id: selector.representative_flow_id(), + public_helicities: public_helicities.to_vec(), + public_direct_helicity_id, + representative_direct_helicity_id, + replay_phase: [phase.0, phase.1], + replay_multiplicity: selector.multiplicity(), + replay_scale: [scale.0, scale.1], + on_the_fly_public_amplitude: [ + on_the_fly_public_amplitude.0, + on_the_fly_public_amplitude.1, + ], + recurrence_representative_amplitude: [ + recurrence_representative_amplitude.0, + recurrence_representative_amplitude.1, + ], + recurrence_public_amplitude: [ + recurrence_public_amplitude.0, + recurrence_public_amplitude.1, + ], + amplitude_absolute_delta: [ + (on_the_fly_public_amplitude.0 - recurrence_public_amplitude.0).abs(), + (on_the_fly_public_amplitude.1 - recurrence_public_amplitude.1).abs(), + ], + current_components, + first_raw_bit_difference, + raw_bit_difference_count, + compared_current_count: u32::try_from(on_the_fly_currents.len()) + .map_err(|_| RusticolError::integrity("compared current count exceeds u32"))?, + excluded_direct_current_count, + }) + } +} + +#[cfg(feature = "python-generation-bridge")] +pub(super) fn build_recurrence_ready_execution_v1( + artifact: &VerifiedArtifact, + evaluator_root: &Path, + manifest: &RecurrenceExecutionManifest, + physics: &ProcessPhysicsV1, +) -> RusticolResult { + let loaded_primary = load_plan(artifact, evaluator_root, manifest)?; + let primary_plan = &loaded_primary.plan; + let (common, _defaults, _projection, source_domains) = + build_common_runtime(primary_plan, manifest, physics)?; + let public_flow_ids = public_flow_ids(primary_plan, &manifest.runtime_metadata, physics)?; + let primary_direct_helicity_to_physics = direct_helicity_to_physics(primary_plan, physics)? + .into_iter() + .map(|value| { + u64::try_from(value) + .map_err(|_| RusticolError::artifact("physics helicity index exceeds u64")) + }) + .collect::>>()?; + let runtime = RecurrenceReadyRuntimeV1 { + runtime_parameters: manifest.runtime_metadata.runtime_parameters.clone(), + prepared_parameter_defaults: manifest + .runtime_metadata + .prepared_parameter_defaults + .clone(), + parameter_projection: manifest.runtime_metadata.parameter_projection.clone(), + source_domains: encode_ready_source_domains(&source_domains), + external_is_initial: common.external_is_initial.clone(), + particle_masses: common + .particle_masses + .iter() + .map(|(pdg, mass)| (*pdg, *mass)) + .collect(), + particle_mass_parameter_names: common + .particle_mass_parameter_names + .iter() + .map(|(pdg, name)| (*pdg, name.clone())) + .collect(), + normalization: manifest.runtime_metadata.normalization.clone(), + public_flow_ids, + public_flow_public_ids: manifest + .runtime_metadata + .public_color_flows + .iter() + .map(|binding| binding.public_id.clone()) + .collect(), + direct_helicity_to_physics: primary_direct_helicity_to_physics, + color_contraction: manifest.runtime_metadata.color_contraction.clone(), + }; + let companion = manifest + .helicity_selector_companion + .as_ref() + .map(|companion| { + validate_recurrence_companion_physics(physics)?; + let loaded = load_plan_summary(artifact, evaluator_root, manifest, &companion.plan)?; + let source_domains = build_direct_source_domains( + &loaded.plan, + &manifest.runtime_metadata, + &common.model_parameter_runtime_slots, + )?; + let direct_helicity_to_physics = direct_helicity_to_physics(&loaded.plan, physics)? + .into_iter() + .map(|value| { + u64::try_from(value) + .map_err(|_| RusticolError::artifact("companion physics index exceeds u64")) + }) + .collect::>>()?; + Ok(RecurrenceReadyCompanionV1 { + process_digest: companion.process_digest.clone(), + plan: RecurrenceReadyPlanV1::from_summary(&companion.plan), + source_domains: encode_ready_source_domains(&source_domains), + direct_helicity_to_physics, + }) + }) + .transpose()?; + Ok(RecurrenceReadyExecutionV1 { + schema_version: 1, + required_runtime_capabilities: manifest.required_runtime_capabilities.clone(), + process: manifest.process.clone(), + key: manifest.key.clone(), + color_accuracy: manifest.color_accuracy.clone(), + external_pdg_order: manifest.external_pdg_order.clone(), + compiled_model_digest: manifest.compiled_model_digest.clone(), + recurrence_template_catalog_digest: manifest.recurrence_template_catalog_digest.clone(), + prepared_kernel_pack_digest: manifest.prepared_kernel_pack_digest.clone(), + direct_template_catalog_digest: manifest.direct_template_catalog_digest.clone(), + kernel_pack: manifest.kernel_pack.clone(), + primary_plan: RecurrenceReadyPlanV1::from_summary(&manifest.plan), + runtime, + companion, + }) +} + +#[cfg(feature = "python-generation-bridge")] +fn encode_ready_source_domains( + domains: &[DirectSourceDispatchDomainSpec], +) -> Vec { + domains + .iter() + .map(|domain| RecurrenceReadySourceDomainV1 { + variants: domain + .variants + .iter() + .map(|variant| RecurrenceReadySourceVariantV1 { + key: match variant.key { + DirectSourceDispatchKey::SpinStateClass(value) => { + RecurrenceReadySourceKeyV1::SpinStateClass(value) + } + DirectSourceDispatchKey::RuntimeVariant { + source_row_id, + runtime_variant_id, + } => RecurrenceReadySourceKeyV1::RuntimeVariant { + source_row_id, + runtime_variant_id, + }, + }, + template: RecurrenceReadySourceTemplateV1 { + spin_state_class: variant.template.spin_state_class, + family: match variant.template.family { + DirectSourceWavefunctionFamily::Scalar => { + RecurrenceReadySourceFamilyV1::Scalar + } + DirectSourceWavefunctionFamily::WeylFermion => { + RecurrenceReadySourceFamilyV1::WeylFermion + } + DirectSourceWavefunctionFamily::DiracFermion => { + RecurrenceReadySourceFamilyV1::DiracFermion + } + DirectSourceWavefunctionFamily::Vector => { + RecurrenceReadySourceFamilyV1::Vector + } + DirectSourceWavefunctionFamily::Spin2 => { + RecurrenceReadySourceFamilyV1::Spin2 + } + }, + orientation: match variant.template.orientation { + DirectSourceOrientation::Particle => { + RecurrenceReadySourceOrientationV1::Particle + } + DirectSourceOrientation::Antiparticle => { + RecurrenceReadySourceOrientationV1::Antiparticle + } + DirectSourceOrientation::SelfConjugate => { + RecurrenceReadySourceOrientationV1::SelfConjugate + } + }, + helicity: variant.template.helicity, + chirality: variant.template.chirality, + mass_parameter_index: variant.template.mass_parameter_index, + }, + }) + .collect(), + }) + .collect() +} + +#[cfg_attr(target_vendor = "apple", unsafe(link_section = "__TEXT,__rcl_load"))] +#[cfg_attr(target_vendor = "apple", inline(never))] +pub(super) fn decode_ready_source_domains( + domains: &[RecurrenceReadySourceDomainV1], + plan: &DirectRecurrencePlan, +) -> RusticolResult> { + if domains.len() + != usize::try_from(plan.source_template_or_dispatch_count()) + .map_err(|_| RusticolError::artifact("source-domain count exceeds usize"))? + { + return Err(RusticolError::integrity( + "process-ready source-domain count disagrees with the recurrence plan", + )); + } + let domains = domains + .iter() + .map(|domain| DirectSourceDispatchDomainSpec { + variants: domain + .variants + .iter() + .map(|variant| DirectSourceDispatchVariantSpec { + key: match variant.key { + RecurrenceReadySourceKeyV1::SpinStateClass(value) => { + DirectSourceDispatchKey::SpinStateClass(value) + } + RecurrenceReadySourceKeyV1::RuntimeVariant { + source_row_id, + runtime_variant_id, + } => DirectSourceDispatchKey::RuntimeVariant { + source_row_id, + runtime_variant_id, + }, + }, + template: DirectSourceTemplateSpec { + spin_state_class: variant.template.spin_state_class, + family: match variant.template.family { + RecurrenceReadySourceFamilyV1::Scalar => { + DirectSourceWavefunctionFamily::Scalar + } + RecurrenceReadySourceFamilyV1::WeylFermion => { + DirectSourceWavefunctionFamily::WeylFermion + } + RecurrenceReadySourceFamilyV1::DiracFermion => { + DirectSourceWavefunctionFamily::DiracFermion + } + RecurrenceReadySourceFamilyV1::Vector => { + DirectSourceWavefunctionFamily::Vector + } + RecurrenceReadySourceFamilyV1::Spin2 => { + DirectSourceWavefunctionFamily::Spin2 + } + }, + orientation: match variant.template.orientation { + RecurrenceReadySourceOrientationV1::Particle => { + DirectSourceOrientation::Particle + } + RecurrenceReadySourceOrientationV1::Antiparticle => { + DirectSourceOrientation::Antiparticle + } + RecurrenceReadySourceOrientationV1::SelfConjugate => { + DirectSourceOrientation::SelfConjugate + } + }, + helicity: variant.template.helicity, + chirality: variant.template.chirality, + mass_parameter_index: variant.template.mass_parameter_index, + }, + }) + .collect(), + }) + .collect::>(); + validate_ready_source_domains_against_plan(&domains, plan)?; + Ok(domains) +} + +fn validate_ready_source_domains_against_plan( + domains: &[DirectSourceDispatchDomainSpec], + plan: &DirectRecurrencePlan, +) -> RusticolResult<()> { + let mut expected = vec![BTreeMap::new(); domains.len()]; + match plan.strategy() { + crate::recurrence::RecurrenceStrategy::TopologyReplay + | crate::recurrence::RecurrenceStrategy::ContractedColorUnion => { + for row in plan.sources() { + let domain = expected + .get_mut(row.source_template_or_dispatch_domain as usize) + .ok_or_else(|| { + RusticolError::integrity( + "process-ready fixed source references an absent domain", + ) + })?; + let key = DirectSourceDispatchKey::SpinStateClass(row.spin_state_class); + if domain + .insert(key, row.spin_state_class) + .is_some_and(|previous| previous != row.spin_state_class) + { + return Err(RusticolError::integrity( + "process-ready fixed source key has inconsistent spin semantics", + )); + } + } + } + crate::recurrence::RecurrenceStrategy::AllFlowUnion => { + for variant in plan.source_dispatch_variants() { + let domain = expected + .get_mut(variant.dispatch_domain_id as usize) + .ok_or_else(|| { + RusticolError::integrity( + "process-ready union source references an absent domain", + ) + })?; + let key = DirectSourceDispatchKey::RuntimeVariant { + source_row_id: variant.source_row_id, + runtime_variant_id: variant.runtime_variant_id, + }; + if domain + .insert(key, variant.crossed_spin_state_class) + .is_some_and(|previous| previous != variant.crossed_spin_state_class) + { + return Err(RusticolError::integrity( + "process-ready union source key has inconsistent spin semantics", + )); + } + } + } + } + validate_ready_source_domain_shape(domains, &expected, plan.parameter_value_count()) +} + +fn validate_ready_source_domain_shape( + domains: &[DirectSourceDispatchDomainSpec], + expected: &[BTreeMap], + parameter_count: u32, +) -> RusticolResult<()> { + if domains.len() != expected.len() { + return Err(RusticolError::integrity( + "process-ready source-domain expectations have the wrong width", + )); + } + let globally_referenced = expected + .iter() + .flat_map(BTreeMap::iter) + .map(|(key, spin)| (*key, *spin)) + .collect::>(); + for (domain_id, (domain, expected)) in domains.iter().zip(expected).enumerate() { + if domain.variants.is_empty() + || domain + .variants + .windows(2) + .any(|pair| pair[0].key >= pair[1].key) + || domain.variants.iter().any(|variant| { + variant + .template + .mass_parameter_index + .is_some_and(|slot| slot >= parameter_count) + }) + { + return Err(RusticolError::integrity(format!( + "process-ready source domain {domain_id} is non-canonical or out of bounds" + ))); + } + + if expected.is_empty() { + // Model-global source-template IDs can be sparse for one process. + // The established builder fills each unused slot with exactly one + // inert copy of a referenced source so the row-addressable ABI + // remains dense; it can never be selected by this plan. + let [inert] = domain.variants.as_slice() else { + return Err(RusticolError::integrity(format!( + "process-ready unused source domain {domain_id} is not a singleton" + ))); + }; + if globally_referenced.get(&inert.key) != Some(&inert.template.spin_state_class) { + return Err(RusticolError::integrity(format!( + "process-ready unused source domain {domain_id} is not an inert referenced source" + ))); + } + continue; + } + + if domain.variants.len() != expected.len() + || domain.variants.iter().any(|variant| { + expected.get(&variant.key) != Some(&variant.template.spin_state_class) + || matches!( + variant.key, + DirectSourceDispatchKey::SpinStateClass(spin) + if spin != variant.template.spin_state_class + ) + }) + { + return Err(RusticolError::integrity(format!( + "process-ready source domain {domain_id} does not exactly cover its plan keys" + ))); + } + } + Ok(()) +} + +fn validate_recurrence_companion_physics(physics: &ProcessPhysicsV1) -> RusticolResult<()> { + if physics.coverage.helicities != "complete" || physics.coverage.color != "contracted" { + return Err(RusticolError::integrity( + "recurrence helicity-selector companion requires complete helicity and contracted-color coverage", + )); + } + if physics.helicities.iter().any(|helicity| { + !companion_helicity_coefficient_is_canonical(helicity.structural_zero, helicity.coefficient) + }) { + return Err(RusticolError::integrity( + "recurrence helicity-selector companion requires unit physical-helicity coefficients and zero structural-zero coefficients", + )); + } + Ok(()) +} + +fn recurrence_parameter_parts( + plan: &DirectRecurrencePlan, + defaults: &[[f64; 2]], + projection: &[RecurrenceParameterProjection], +) -> RusticolResult<( + Vec, + Vec, +)> { + if defaults + .iter() + .flatten() + .any(|component| !component.is_finite()) + { + return Err(RusticolError::integrity( + "recurrence prepared parameter defaults are not finite", + )); + } + let defaults = defaults + .iter() + .map(|[real, imaginary]| crate::EagerComplex64::new(*real, *imaginary)) + .collect::>(); + if defaults.len() + != usize::try_from(plan.parameter_value_count()) + .map_err(|_| RusticolError::artifact("recurrence parameter count exceeds usize"))? + { + return Err(RusticolError::integrity( + "recurrence prepared defaults do not match the direct plan", + )); + } + let projection = projection + .iter() + .filter_map(|row| { + row.prepared_parameter_id + .map(|prepared_slot| (row, prepared_slot)) + }) + .map(|(row, prepared_slot)| { + Ok(PreparedParameterProjectionEntry { + runtime_slot: usize::try_from(row.runtime_slot).map_err(|_| { + RusticolError::artifact("recurrence runtime parameter slot exceeds usize") + })?, + prepared_slot: usize::try_from(prepared_slot).map_err(|_| { + RusticolError::artifact("recurrence prepared parameter slot exceeds usize") + })?, + component: u8::try_from(row.component).map_err(|_| { + RusticolError::artifact("recurrence parameter component exceeds u8") + })?, + }) + }) + .collect::>>()?; + Ok((defaults, projection)) +} + +fn build_common_runtime( + plan: &DirectRecurrencePlan, + manifest: &RecurrenceExecutionManifest, + physics: &ProcessPhysicsV1, +) -> RusticolResult { + let metadata = &manifest.runtime_metadata; + let runtime_parameters = recurrence_runtime_parameters(metadata); + let model_parameter_runtime_slots = runtime_parameter_slots(&runtime_parameters)?; + let model_parameter_values_f64 = runtime_parameters + .iter() + .map(|parameter| parameter.default) + .collect::>(); + let (parameter_defaults, parameter_projection) = recurrence_parameter_parts( + plan, + &metadata.prepared_parameter_defaults, + &metadata.parameter_projection, + )?; + + let mut particle_masses = metadata + .particle_masses + .iter() + .map(|row| (row.outgoing_pdg, row.mass)) + .collect::>(); + let mut particle_mass_parameter_names = BTreeMap::new(); + for source in &metadata.source_templates { + let Some(name) = source.source_ir.mass_parameter.as_ref() else { + continue; + }; + for pdg in [ + source.source_ir.identity.pdg_label, + source.source_ir.identity.anti_pdg_label, + ] { + particle_mass_parameter_names.insert(pdg, name.clone()); + if let Some(slots) = model_parameter_runtime_slots.get(name) + && let Some(mass) = model_parameter_values_f64.get(slots.real) + { + particle_masses.insert(pdg, *mass); + } + } + } + + let source_domains = + build_direct_source_domains(plan, metadata, &model_parameter_runtime_slots)?; + let normalization = &metadata.normalization; + let normalization_values = recurrence_normalization_values(metadata)?; + let color_factor = normalization_values.color_factor; + let normalization_factor = normalization_values.factor; + let external_count = manifest.external_pdg_order.len(); + if plan.external_source_count() as usize != external_count + || metadata.external_legs.len() != external_count + { + return Err(RusticolError::integrity( + "recurrence direct plan external-source count disagrees with process metadata", + )); + } + let external_is_initial = physics + .external_particles + .iter() + .map(|particle| particle.role == crate::ParticleRole::Initial) + .collect(); + let common = assemble_common_runtime( + plan, + RecurrenceCommonRuntimeDefinition { + process: manifest.process.clone(), + key: manifest.key.clone(), + color_accuracy: manifest.color_accuracy.clone(), + external_pdg_order: manifest.external_pdg_order.clone(), + runtime_parameters, + model_parameter_runtime_slots, + model_parameter_values_f64, + external_is_initial, + particle_masses, + particle_mass_parameter_names, + normalization_factor, + normalization_color_factor: color_factor, + normalization_average_factor: normalization.average_factor, + normalization_identical_factor: normalization.identical_factor, + normalization_qcd_coupling_power: normalization.qcd_coupling_power.unwrap_or(0), + normalization_electroweak_coupling_power: normalization + .electroweak_coupling_power + .unwrap_or(0), + }, + )?; + Ok(( + common, + parameter_defaults, + parameter_projection, + source_domains, + )) +} + +fn build_ready_common_runtime( + plan: &DirectRecurrencePlan, + ready: &RecurrenceReadyExecutionV1, +) -> RusticolResult { + let runtime = &ready.runtime; + validate_recurrence_parameter_runtime_parts( + &runtime.runtime_parameters, + &runtime.prepared_parameter_defaults, + &runtime.parameter_projection, + u64::from(plan.parameter_value_count()), + )?; + let runtime_parameters = recurrence_runtime_parameters_from(&runtime.runtime_parameters); + let model_parameter_runtime_slots = runtime_parameter_slots(&runtime_parameters)?; + let model_parameter_values_f64 = runtime_parameters + .iter() + .map(|parameter| parameter.default) + .collect::>(); + let (parameter_defaults, parameter_projection) = recurrence_parameter_parts( + plan, + &runtime.prepared_parameter_defaults, + &runtime.parameter_projection, + )?; + let particle_masses = ready_pairs_to_map(&runtime.particle_masses, "particle mass", |mass| { + mass.is_finite() && mass >= 0.0 + })?; + let particle_mass_parameter_names = + ready_string_pairs_to_map(&runtime.particle_mass_parameter_names, "particle mass name")?; + if particle_mass_parameter_names + .values() + .any(|name| !model_parameter_runtime_slots.contains_key(name)) + { + return Err(RusticolError::integrity( + "process-ready particle mass references an absent runtime parameter", + )); + } + if runtime.normalization.color_accuracy != ready.color_accuracy + || !runtime.normalization.average_factor.is_finite() + || runtime.normalization.average_factor <= 0.0 + || !runtime.normalization.identical_factor.is_finite() + || runtime.normalization.identical_factor <= 0.0 + { + return Err(RusticolError::integrity( + "process-ready recurrence normalization is inconsistent", + )); + } + let normalization = recurrence_normalization_values_from( + &runtime.normalization, + runtime.color_contraction.as_ref(), + )?; + let source_domains = decode_ready_source_domains(&runtime.source_domains, plan)?; + let common = assemble_common_runtime( + plan, + RecurrenceCommonRuntimeDefinition { + process: ready.process.clone(), + key: ready.key.clone(), + color_accuracy: ready.color_accuracy.clone(), + external_pdg_order: ready.external_pdg_order.clone(), + runtime_parameters, + model_parameter_runtime_slots, + model_parameter_values_f64, + external_is_initial: runtime.external_is_initial.clone(), + particle_masses, + particle_mass_parameter_names, + normalization_factor: normalization.factor, + normalization_color_factor: normalization.color_factor, + normalization_average_factor: runtime.normalization.average_factor, + normalization_identical_factor: runtime.normalization.identical_factor, + normalization_qcd_coupling_power: runtime.normalization.qcd_coupling_power.unwrap_or(0), + normalization_electroweak_coupling_power: runtime + .normalization + .electroweak_coupling_power + .unwrap_or(0), + }, + )?; + Ok(( + common, + parameter_defaults, + parameter_projection, + source_domains, + )) +} + +fn ready_pairs_to_map( + values: &[(i32, f64)], + context: &str, + valid: impl Fn(f64) -> bool, +) -> RusticolResult> { + let mut result = BTreeMap::new(); + let mut previous = None; + for (key, value) in values { + if previous.is_some_and(|previous| previous >= *key) + || result.insert(*key, *value).is_some() + || !valid(*value) + { + return Err(RusticolError::integrity(format!( + "process-ready recurrence {context} entries are not canonical" + ))); + } + previous = Some(*key); + } + Ok(result) +} + +fn ready_string_pairs_to_map( + values: &[(i32, String)], + context: &str, +) -> RusticolResult> { + let mut result = BTreeMap::new(); + let mut previous = None; + for (key, value) in values { + if previous.is_some_and(|previous| previous >= *key) + || value.is_empty() + || result.insert(*key, value.clone()).is_some() + { + return Err(RusticolError::integrity(format!( + "process-ready recurrence {context} entries are not canonical" + ))); + } + previous = Some(*key); + } + Ok(result) +} + +struct RecurrenceCommonRuntimeDefinition { + process: String, + key: String, + color_accuracy: String, + external_pdg_order: Vec, + runtime_parameters: Vec, + model_parameter_runtime_slots: BTreeMap, + model_parameter_values_f64: Vec, + external_is_initial: Vec, + particle_masses: BTreeMap, + particle_mass_parameter_names: BTreeMap, + normalization_factor: f64, + normalization_color_factor: f64, + normalization_average_factor: f64, + normalization_identical_factor: f64, + normalization_qcd_coupling_power: u64, + normalization_electroweak_coupling_power: u64, +} + +#[cfg_attr(target_vendor = "apple", unsafe(link_section = "__TEXT,__rcl_load"))] +#[cfg_attr(target_vendor = "apple", inline(never))] +fn assemble_common_runtime( + plan: &DirectRecurrencePlan, + definition: RecurrenceCommonRuntimeDefinition, +) -> RusticolResult { + let RecurrenceCommonRuntimeDefinition { + process, + key, + color_accuracy, + external_pdg_order, + runtime_parameters, + model_parameter_runtime_slots, + model_parameter_values_f64, + external_is_initial, + particle_masses, + particle_mass_parameter_names, + normalization_factor, + normalization_color_factor, + normalization_average_factor, + normalization_identical_factor, + normalization_qcd_coupling_power, + normalization_electroweak_coupling_power, + } = definition; + let external_count = external_pdg_order.len(); + if plan.external_source_count() as usize != external_count + || external_is_initial.len() != external_count + { + return Err(RusticolError::integrity( + "recurrence common runtime external domain disagrees with its plan", + )); + } + let model_parameter_name_to_index = runtime_parameters + .iter() + .map(|parameter| (parameter.name.clone(), parameter.parameter_index)) + .collect::>(); + let source_count = plan.sources().len(); + let current_count = plan.currents().len(); + let interaction_count = plan.contributions().len(); + let stage_count = plan + .row_groups() + .iter() + .map(|group| usize::from(group.stage)) + .max() + .and_then(|stage| stage.checked_add(1)) + .ok_or_else(|| RusticolError::integrity("recurrence direct plan has no row groups"))?; + let amplitude_output_count = plan.amplitude_destinations().len(); + Ok(ExecutionRuntime { + process, + key, + color_accuracy, + external_pdg_order, + external_count, + parameter_count: runtime_parameters.len(), + value_parameter_count: 0, + momentum_parameter_count: 0, + current_count, + source_count, + interaction_count, + stage_count, + amplitude_output_count, + lc_topology_replay_enabled: false, + lc_topology_replay_mappings: Arc::new(Vec::new()), + lc_topology_replay_public_mappings: Vec::new(), + lc_topology_replay_routes: Vec::new(), + lc_topology_replay_materialized_sector_ids: BTreeSet::new(), + lc_resolved_replay_plan: None, + lc_resolved_replay_selection_cache: None, + lc_replay_flat_momenta_scratch: Vec::new(), + lc_replay_target_components_scratch: Vec::new(), + color_topology_replay_enabled: false, + color_topology_replay_mappings: Arc::new(Vec::new()), + color_replay_flat_momenta_scratch: Vec::new(), + helicity_recurrence: None, + compiled_helicity_execution_plan: None, + compiled_color_execution_plan: None, + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + compiled_direct_runtime: None, + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + compiled_direct_color_schedules: BTreeMap::new(), + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + compiled_direct_helicity_schedules: BTreeMap::new(), + helicity_sum_runtime: None, + helicity_selector_runtimes: Vec::new(), + helicity_selector_runtime_schedule_modes: Vec::new(), + helicity_selector_lane_by_domain: BTreeMap::new(), + color_selector_runtimes: BTreeMap::new(), + runtime_unavailable_message: None, + // Direct-Arena source execution owns typed SourceIR dispatch domains. + // The legacy source records are intentionally not reconstructed. + sources: Vec::new(), + momentum_slots: Vec::new(), + external_is_initial, + particle_masses, + particle_mass_parameter_names, + normalization_factor, + normalization_color_factor, + normalization_average_factor, + normalization_identical_factor, + normalization_qcd_coupling_power: usize::try_from(normalization_qcd_coupling_power) + .map_err(|_| RusticolError::artifact("QCD coupling power exceeds usize"))?, + normalization_electroweak_coupling_power: usize::try_from( + normalization_electroweak_coupling_power, + ) + .map_err(|_| RusticolError::artifact("electroweak coupling power exceeds usize"))?, + model_parameters: runtime_parameters, + model_parameter_name_to_index, + model_parameter_runtime_slots, + model_parameter_values_f64, + model_parameter_evaluator: None, + physics_reduction_override: None, + physics: None, + stages: None, + amplitude_stage: None, + state_scratch_f64: Vec::new(), + state_scratch_f64_requires_clear: false, + values_scratch_f64: Vec::new(), + }) +} + +pub(super) fn build_direct_source_domains( + plan: &DirectRecurrencePlan, + metadata: &RecurrenceRuntimeMetadata, + runtime_parameter_slots: &BTreeMap, +) -> RusticolResult> { + let templates = metadata + .source_templates + .iter() + .map(|source| (source.source_template_id, source)) + .collect::>(); + let legs = metadata + .external_legs + .iter() + .map(|leg| (leg.source_slot, leg)) + .collect::>(); + let domain_count = usize::try_from(plan.source_template_or_dispatch_count()) + .map_err(|_| RusticolError::artifact("recurrence source-domain count exceeds usize"))?; + let mut variants = + vec![BTreeMap::::new(); domain_count]; + + match plan.strategy() { + crate::recurrence::RecurrenceStrategy::TopologyReplay + | crate::recurrence::RecurrenceStrategy::ContractedColorUnion => { + for row in plan.sources() { + let source = templates + .get(&row.source_template_or_dispatch_domain) + .ok_or_else(|| { + RusticolError::integrity( + "recurrence direct plan references an absent SourceIR template", + ) + })?; + let leg = legs.get(&row.source_slot).ok_or_else(|| { + RusticolError::integrity( + "recurrence direct source references an absent external leg", + ) + })?; + let spec = direct_source_template_spec( + source, + leg, + metadata, + runtime_parameter_slots, + plan.parameter_value_count(), + )?; + if row.spin_state_class != spec.spin_state_class { + return Err(RusticolError::integrity( + "recurrence direct source spin class disagrees with crossed SourceIR metadata", + )); + } + insert_source_domain_variant( + &mut variants, + row.source_template_or_dispatch_domain, + DirectSourceDispatchKey::SpinStateClass(row.spin_state_class), + spec, + )?; + } + } + crate::recurrence::RecurrenceStrategy::AllFlowUnion => { + for variant in plan.source_dispatch_variants() { + let row = plan + .sources() + .get(variant.source_row_id as usize) + .ok_or_else(|| { + RusticolError::integrity( + "recurrence source variant references an absent source row", + ) + })?; + let source = templates.get(&variant.source_template_id).ok_or_else(|| { + RusticolError::integrity( + "recurrence source variant references an absent SourceIR template", + ) + })?; + let leg = legs.get(&row.source_slot).ok_or_else(|| { + RusticolError::integrity( + "recurrence source variant references an absent external leg", + ) + })?; + let spec = direct_source_template_spec( + source, + leg, + metadata, + runtime_parameter_slots, + plan.parameter_value_count(), + )?; + if row.source_template_or_dispatch_domain != variant.dispatch_domain_id + || spec.spin_state_class != variant.crossed_spin_state_class + { + return Err(RusticolError::integrity( + "recurrence union source variant disagrees with its dispatch domain or crossed SourceIR state", + )); + } + insert_source_domain_variant( + &mut variants, + variant.dispatch_domain_id, + DirectSourceDispatchKey::RuntimeVariant { + source_row_id: variant.source_row_id, + runtime_variant_id: variant.runtime_variant_id, + }, + spec, + )?; + } + } + } + + finish_direct_source_domains(variants) +} + +fn finish_direct_source_domains( + variants: Vec>, +) -> RusticolResult> { + let inert = variants + .iter() + .flat_map(BTreeMap::iter) + .next() + .map(|(key, spec)| (*key, *spec)) + .ok_or_else(|| RusticolError::integrity("recurrence direct plan has no source rows"))?; + Ok(variants + .into_iter() + .map(|domain| DirectSourceDispatchDomainSpec { + // Prepared source-template IDs are model-global and may be sparse + // for one process. Unreferenced slots remain inert but preserve the + // stable IDs addressed by DirectSourceRow. + variants: if domain.is_empty() { + vec![DirectSourceDispatchVariantSpec { + key: inert.0, + template: inert.1, + }] + } else { + domain + .into_iter() + .map(|(key, template)| DirectSourceDispatchVariantSpec { key, template }) + .collect() + }, + }) + .collect()) +} + +/// Bind the compact process seed to the same crossed SourceIR semantics used +/// by the established direct-plan loader. This boundary is deliberately +/// plan-independent: source-template IDs remain the authenticated model-level +/// IDs, including sparse slots that this process does not address. +pub(super) fn build_on_the_fly_source_domains( + seed: &OnTheFlyProcessSeedV1, + templates: &crate::recurrence::template::ValidatedRecurrenceTemplateInput, +) -> RusticolResult> { + let summary = templates.summary(); + if seed.template_catalog_digest() != summary.catalog_digest + || seed.model_digest() != summary.compiled_model_digest + || seed.prepared_pack_digest() != summary.prepared_kernel_pack_digest + { + return Err(RusticolError::integrity( + "on-the-fly source seed does not belong to the authenticated recurrence templates", + )); + } + let domain_count = usize::try_from(summary.source_count) + .map_err(|_| RusticolError::artifact("recurrence source-domain count exceeds usize"))?; + let mut variants = + vec![BTreeMap::::new(); domain_count]; + for compact in seed.source_execution_specs() { + if compact.source_template_id as usize >= domain_count + || compact + .prepared_mass_parameter_slot + .is_some_and(|slot| slot >= summary.parameter_count) + { + return Err(RusticolError::integrity( + "on-the-fly source or mass-parameter slot is outside its authenticated template domain", + )); + } + let source_template_id = compact.source_template_id; + let spec = DirectSourceTemplateSpec { + spin_state_class: compact.spin_state_class, + family: direct_on_the_fly_source_family(compact.family), + orientation: direct_on_the_fly_source_orientation(compact.orientation), + helicity: compact.helicity, + chirality: compact.chirality, + mass_parameter_index: compact.prepared_mass_parameter_slot, + }; + insert_source_domain_variant( + &mut variants, + source_template_id, + DirectSourceDispatchKey::SpinStateClass(spec.spin_state_class), + spec, + )?; + } + finish_direct_source_domains(variants) +} + +#[cfg(test)] +fn build_on_the_fly_source_domains_from_specs( + specs: impl IntoIterator, + domain_count: usize, + parameter_count: u32, + metadata: &RecurrenceRuntimeMetadata, + runtime_parameter_slots: &BTreeMap, +) -> RusticolResult> { + let templates = metadata + .source_templates + .iter() + .map(|source| (source.source_template_id, source)) + .collect::>(); + let legs = metadata + .external_legs + .iter() + .map(|leg| (leg.source_slot, leg)) + .collect::>(); + let mut variants = + vec![BTreeMap::::new(); domain_count]; + for compact in specs { + let source = templates.get(&compact.source_template_id).ok_or_else(|| { + RusticolError::integrity("on-the-fly source references an absent SourceIR template") + })?; + let leg = legs.get(&compact.source_slot).ok_or_else(|| { + RusticolError::integrity("on-the-fly source references an absent external leg") + })?; + let authoritative = direct_source_template_spec( + source, + leg, + metadata, + runtime_parameter_slots, + parameter_count, + )?; + let compact = DirectSourceTemplateSpec { + spin_state_class: compact.spin_state_class, + family: direct_on_the_fly_source_family(compact.family), + orientation: direct_on_the_fly_source_orientation(compact.orientation), + helicity: compact.helicity, + chirality: compact.chirality, + mass_parameter_index: compact.prepared_mass_parameter_slot, + }; + let authoritative = bind_on_the_fly_source_spec(compact, authoritative)?; + insert_source_domain_variant( + &mut variants, + source.source_template_id, + DirectSourceDispatchKey::SpinStateClass(authoritative.spin_state_class), + authoritative, + )?; + } + finish_direct_source_domains(variants) +} + +#[cfg(test)] +fn bind_on_the_fly_source_spec( + compact: DirectSourceTemplateSpec, + authoritative: DirectSourceTemplateSpec, +) -> RusticolResult { + if compact.spin_state_class != authoritative.spin_state_class { + return Err(RusticolError::integrity( + "on-the-fly source spin class disagrees with crossed SourceIR metadata", + )); + } + if compact.family != authoritative.family { + return Err(RusticolError::integrity( + "on-the-fly source family disagrees with crossed SourceIR metadata", + )); + } + if compact.orientation != authoritative.orientation { + return Err(RusticolError::integrity( + "on-the-fly source orientation disagrees with crossed SourceIR metadata", + )); + } + if compact.helicity != authoritative.helicity { + return Err(RusticolError::integrity( + "on-the-fly source helicity disagrees with crossed SourceIR metadata", + )); + } + if compact.chirality != authoritative.chirality { + return Err(RusticolError::integrity( + "on-the-fly source chirality disagrees with crossed SourceIR metadata", + )); + } + if compact.mass_parameter_index.is_some() + && compact.mass_parameter_index != authoritative.mass_parameter_index + { + return Err(RusticolError::integrity( + "on-the-fly source prepared mass parameter slot disagrees with SourceIR metadata", + )); + } + Ok(authoritative) +} + +const fn direct_on_the_fly_source_family( + value: OnTheFlySourceWavefunctionFamilyV1, +) -> DirectSourceWavefunctionFamily { + match value { + OnTheFlySourceWavefunctionFamilyV1::Scalar => DirectSourceWavefunctionFamily::Scalar, + OnTheFlySourceWavefunctionFamilyV1::WeylFermion => { + DirectSourceWavefunctionFamily::WeylFermion + } + OnTheFlySourceWavefunctionFamilyV1::DiracFermion => { + DirectSourceWavefunctionFamily::DiracFermion + } + OnTheFlySourceWavefunctionFamilyV1::Vector => DirectSourceWavefunctionFamily::Vector, + OnTheFlySourceWavefunctionFamilyV1::Spin2 => DirectSourceWavefunctionFamily::Spin2, + } +} + +const fn direct_on_the_fly_source_orientation( + value: OnTheFlySourceOrientationV1, +) -> DirectSourceOrientation { + match value { + OnTheFlySourceOrientationV1::Particle => DirectSourceOrientation::Particle, + OnTheFlySourceOrientationV1::Antiparticle => DirectSourceOrientation::Antiparticle, + OnTheFlySourceOrientationV1::SelfConjugate => DirectSourceOrientation::SelfConjugate, + } +} + +fn insert_source_domain_variant( + domains: &mut [BTreeMap], + domain_id: u32, + key: DirectSourceDispatchKey, + spec: DirectSourceTemplateSpec, +) -> RusticolResult<()> { + let domain = domains.get_mut(domain_id as usize).ok_or_else(|| { + RusticolError::integrity("recurrence direct source-domain ID is out of bounds") + })?; + if let Some(previous) = domain.insert(key, spec) + && previous != spec + { + return Err(RusticolError::integrity( + "recurrence source domain maps one dispatch key to different SourceIR semantics", + )); + } + Ok(()) +} + +fn direct_source_template_spec( + source: &RecurrenceSourceTemplate, + leg: &RecurrenceExternalLeg, + metadata: &RecurrenceRuntimeMetadata, + runtime_parameter_slots: &BTreeMap, + parameter_count: u32, +) -> RusticolResult { + if source.source_ir.identity.pdg_label != leg.outgoing_pdg { + return Err(RusticolError::integrity( + "recurrence SourceIR particle disagrees with its external leg", + )); + } + let crossing = if leg.is_initial { + &source.crossing + } else { + &IDENTITY_RECURRENCE_CROSSING + }; + let helicity = source + .helicity + .checked_mul(crossing.helicity_factor) + .ok_or_else(|| RusticolError::integrity("recurrence source helicity overflows"))?; + let chirality = source + .chirality + .checked_mul(crossing.chirality_factor) + .ok_or_else(|| RusticolError::integrity("recurrence source chirality overflows"))?; + let spin_state_class = source + .spin_state + .checked_mul(crossing.spin_state_factor) + .ok_or_else(|| RusticolError::integrity("recurrence source spin state overflows"))?; + let family = direct_source_family(&source.source_ir)?; + let orientation = match source.source_ir.identity.orientation { + RecurrenceSourceOrientation::Particle => DirectSourceOrientation::Particle, + RecurrenceSourceOrientation::Antiparticle => DirectSourceOrientation::Antiparticle, + RecurrenceSourceOrientation::SelfConjugate => DirectSourceOrientation::SelfConjugate, + }; + let mass_parameter_index = source + .source_ir + .mass_parameter + .as_deref() + .map(|name| { + prepared_parameter_index( + name, + runtime_parameter_slots, + &metadata.parameter_projection, + parameter_count, + ) + }) + .transpose()?; + if mass_parameter_index.is_none() + && metadata + .particle_masses + .iter() + .find(|mass| mass.outgoing_pdg == leg.outgoing_pdg) + .is_some_and(|mass| mass.mass != 0.0) + { + return Err(RusticolError::compatibility( + "massive recurrence SourceIR has no prepared mass-parameter slot", + )); + } + Ok(DirectSourceTemplateSpec { + spin_state_class, + family, + orientation, + helicity, + chirality, + mass_parameter_index, + }) +} + +const IDENTITY_RECURRENCE_CROSSING: RecurrenceGenericCrossingIr = RecurrenceGenericCrossingIr { + momentum_transform: RecurrenceMomentumTransform::Identity, + helicity_factor: 1, + chirality_factor: 1, + spin_state_factor: 1, + phase: [1.0, 0.0], +}; + +fn direct_source_family( + source: &RecurrenceGenericSourceIr, +) -> RusticolResult { + match ( + source.statistics, + source.wavefunction_family, + source.component_dimension, + ) { + (RecurrenceParticleStatistics::Boson, RecurrenceWavefunctionFamily::Scalar, 1) => { + Ok(DirectSourceWavefunctionFamily::Scalar) + } + (RecurrenceParticleStatistics::Fermion, RecurrenceWavefunctionFamily::Fermion, 2) => { + Ok(DirectSourceWavefunctionFamily::WeylFermion) + } + (RecurrenceParticleStatistics::Fermion, RecurrenceWavefunctionFamily::Fermion, 4) => { + Ok(DirectSourceWavefunctionFamily::DiracFermion) + } + (RecurrenceParticleStatistics::Boson, RecurrenceWavefunctionFamily::Vector, 4) => { + Ok(DirectSourceWavefunctionFamily::Vector) + } + (RecurrenceParticleStatistics::Boson, RecurrenceWavefunctionFamily::Spin2, 16) => { + Ok(DirectSourceWavefunctionFamily::Spin2) + } + (_, RecurrenceWavefunctionFamily::Ghost, _) + | (_, RecurrenceWavefunctionFamily::Auxiliary, _) => Err(RusticolError::compatibility( + "Direct-Arena recurrence does not yet support ghost or auxiliary external sources", + )), + _ => Err(RusticolError::integrity( + "recurrence SourceIR statistics, family, and component dimension are incompatible", + )), + } +} + +fn prepared_parameter_index( + name: &str, + runtime_parameter_slots: &BTreeMap, + projection: &[RecurrenceParameterProjection], + parameter_count: u32, +) -> RusticolResult { + let runtime_slot = runtime_parameter_slots + .get(name) + .ok_or_else(|| { + RusticolError::integrity(format!( + "recurrence source mass parameter {name:?} has no runtime projection" + )) + })? + .real; + let row = projection + .iter() + .find(|row| row.runtime_slot as usize == runtime_slot && row.component == 0) + .ok_or_else(|| { + RusticolError::integrity( + "recurrence source mass parameter has no real prepared projection", + ) + })?; + if row.runtime_name != name { + return Err(RusticolError::integrity( + "recurrence source mass projection is not its real component", + )); + } + let prepared = row.prepared_parameter_id.ok_or_else(|| { + RusticolError::compatibility(format!( + "recurrence source mass parameter {name:?} has no prepared parameter slot" + )) + })?; + if prepared >= parameter_count { + return Err(RusticolError::integrity( + "recurrence source mass prepared slot exceeds the direct plan", + )); + } + Ok(prepared) +} + +pub(super) fn runtime_parameter_slots( + parameters: &[GenericRuntimeModelParameterManifest], +) -> RusticolResult> { + let mut result = BTreeMap::new(); + let mut complex = BTreeMap::, Option)>::new(); + for parameter in parameters { + if let Some(name) = ¶meter.runtime_name { + let slots = complex.entry(name.clone()).or_default(); + let target = match parameter.complex_component.as_deref() { + Some("real") => &mut slots.0, + Some("imag") => &mut slots.1, + _ => { + return Err(RusticolError::integrity( + "recurrence complex parameter lacks component metadata", + )); + } + }; + if target.replace(parameter.parameter_index).is_some() { + return Err(RusticolError::integrity( + "recurrence complex parameter repeats a component", + )); + } + } else if result + .insert( + parameter.name.clone(), + RuntimeParameterSlots { + real: parameter.parameter_index, + imaginary: None, + }, + ) + .is_some() + { + return Err(RusticolError::integrity( + "recurrence runtime parameter names are not unique", + )); + } + } + for (name, (real, imaginary)) in complex { + let real = real.ok_or_else(|| { + RusticolError::integrity("recurrence complex parameter lacks a real component") + })?; + if result + .insert(name, RuntimeParameterSlots { real, imaginary }) + .is_some() + { + return Err(RusticolError::integrity( + "recurrence runtime parameter names conflict", + )); + } + } + Ok(result) +} + +pub(super) fn decode_sha256(value: &str) -> RusticolResult<[u8; 32]> { + if value.len() != 64 { + return Err(RusticolError::integrity( + "recurrence SHA-256 has an invalid encoded length", + )); + } + let mut output = [0u8; 32]; + for (index, byte) in output.iter_mut().enumerate() { + *byte = u8::from_str_radix(&value[index * 2..index * 2 + 2], 16).map_err(|_| { + RusticolError::integrity("recurrence SHA-256 is not lowercase hexadecimal") + })?; + } + Ok(output) +} + +#[cfg(test)] +mod binding_decode_tests { + use super::super::recurrence_process_pack::{ + ProcessDirectExecutorBinding, ProcessIntrinsicExecutor, ProcessJitExecutor, + ProcessNativeExecutor, + }; + use super::{ + DirectSourceDispatchDomainSpec, DirectSourceDispatchKey, DirectSourceDispatchVariantSpec, + DirectSourceOrientation, DirectSourceTemplateSpec, DirectSourceWavefunctionFamily, + OnTheFlySourceExecutionSpecV1, OnTheFlySourceOrientationV1, + OnTheFlySourceWavefunctionFamilyV1, RecurrenceExternalLeg, RecurrenceGenericCrossingIr, + RecurrenceGenericParticleIdentityIr, RecurrenceGenericSourceIr, + RecurrenceMomentumTransform, RecurrenceNormalization, RecurrenceParameterProjection, + RecurrenceParticleMass, RecurrenceParticleStatistics, RecurrenceRuntimeMetadata, + RecurrenceRuntimeParameter, RecurrenceSourceOrientation, RecurrenceSourceTemplate, + RecurrenceWavefunctionFamily, RuntimeParameterSlots, + build_on_the_fly_source_domains_from_specs, companion_helicity_coefficient_is_canonical, + invert_replay_helicity_map, process_binding_requires_evaluator_payloads, read_u64_values, + recurrence_ready_needs_evaluator_payloads, validate_ready_source_domain_shape, + validate_recurrence_parameter_runtime_parts, + }; + #[cfg(feature = "on-the-fly-test-support")] + use super::{OnTheFlyQueryTraceCacheV1, validate_on_the_fly_probe_outputs}; + use std::collections::BTreeMap; + + #[test] + fn execution_diagnostic_inverts_nonidentity_replay_helicity_map() { + // The replay map is representative ID -> public ID. Public helicity + // zero is therefore represented by ID one, not by the same index. + assert_eq!(invert_replay_helicity_map(17, &[2, 0, 1], 0).unwrap(), 1); + } + + #[test] + fn process_ready_evaluator_payload_predicate_is_exhaustive() { + assert!(!process_binding_requires_evaluator_payloads( + &ProcessDirectExecutorBinding::Source + )); + assert!(!process_binding_requires_evaluator_payloads( + &ProcessDirectExecutorBinding::Intrinsic(ProcessIntrinsicExecutor { + runtime_template: "identity-finalizer-v1".into(), + scale: None, + }) + )); + assert!(process_binding_requires_evaluator_payloads( + &ProcessDirectExecutorBinding::Jit(ProcessJitExecutor { + optimization_level: 2, + plane_compression: true, + source_application_sha256: [0; 32], + source_application_path: "application.bin".into(), + source_application_abi: "test".into(), + parameter_bindings: Vec::new(), + input_plane_projections: Vec::new(), + scalar_projections: Vec::new(), + output_alias_inputs: Vec::new(), + }) + )); + assert!(process_binding_requires_evaluator_payloads( + &ProcessDirectExecutorBinding::Native(ProcessNativeExecutor { + prepared_kernel_id: 0, + library_path: "kernel.so".into(), + native_entry_point: "kernel".into(), + coupling: None, + }) + )); + } + + #[test] + fn process_ready_container_need_skips_plain_intrinsic_runtime() { + assert!(!recurrence_ready_needs_evaluator_payloads( + false, false, false + )); + for flags in [ + (true, false, false), + (false, true, false), + (false, false, true), + ] { + assert!(recurrence_ready_needs_evaluator_payloads( + flags.0, flags.1, flags.2 + )); + } + } + + #[test] + fn process_ready_parameter_schema_rejects_manifest_parity_drift() { + let defaults = vec![[1.0, 2.0]]; + let projection = vec![ + RecurrenceParameterProjection { + runtime_slot: 0, + runtime_name: "Z".into(), + parameter_template_id: 0, + prepared_parameter_id: Some(0), + component: 0, + }, + RecurrenceParameterProjection { + runtime_slot: 1, + runtime_name: "Z".into(), + parameter_template_id: 0, + prepared_parameter_id: Some(0), + component: 1, + }, + ]; + let parameters = vec![ + RecurrenceRuntimeParameter { + name: "Z.real".into(), + kind: "external_parameter_component".into(), + parameter_index: 0, + default: 1.0, + runtime_name: Some("Z".into()), + complex_component: Some("real".into()), + }, + RecurrenceRuntimeParameter { + name: "Z.imag".into(), + kind: "external_parameter_component".into(), + parameter_index: 1, + default: 2.0, + runtime_name: Some("Z".into()), + complex_component: Some("imag".into()), + }, + ]; + validate_recurrence_parameter_runtime_parts(¶meters, &defaults, &projection, 1) + .unwrap(); + + let mut malformed = parameters.clone(); + malformed[1].default = 3.0; + let error = + validate_recurrence_parameter_runtime_parts(&malformed, &defaults, &projection, 1) + .expect_err("process-ready defaults must remain bound to prepared defaults"); + assert!(error.to_string().contains("prepared default")); + + let mut malformed = projection.clone(); + malformed[1].runtime_slot = 7; + let error = + validate_recurrence_parameter_runtime_parts(¶meters, &defaults, &malformed, 1) + .expect_err("process-ready projection slots must remain dense"); + assert!(error.to_string().contains("dense")); + } + + #[test] + fn process_ready_sources_reject_key_or_mass_drift_from_plan() { + let template = DirectSourceTemplateSpec { + spin_state_class: 7, + family: DirectSourceWavefunctionFamily::WeylFermion, + orientation: DirectSourceOrientation::Particle, + helicity: 1, + chirality: -1, + mass_parameter_index: Some(0), + }; + let expected = vec![BTreeMap::from([( + DirectSourceDispatchKey::SpinStateClass(7), + 7, + )])]; + let valid = vec![DirectSourceDispatchDomainSpec { + variants: vec![DirectSourceDispatchVariantSpec { + key: DirectSourceDispatchKey::SpinStateClass(7), + template, + }], + }]; + validate_ready_source_domain_shape(&valid, &expected, 1).unwrap(); + + let mut malformed_key = valid.clone(); + malformed_key[0].variants[0].key = DirectSourceDispatchKey::SpinStateClass(8); + let error = validate_ready_source_domain_shape(&malformed_key, &expected, 1) + .expect_err("a process-ready source key must be plan-referenced"); + assert!(error.to_string().contains("plan keys")); + + let mut malformed_mass = valid; + malformed_mass[0].variants[0].template.mass_parameter_index = Some(1); + let error = validate_ready_source_domain_shape(&malformed_mass, &expected, 1) + .expect_err("a process-ready source mass slot must be plan-bounded"); + assert!(error.to_string().contains("out of bounds")); + } + + #[test] + fn companion_helicity_coefficients_fail_closed_outside_physical_unit_weights() { + assert!(companion_helicity_coefficient_is_canonical(false, 1.0)); + assert!(companion_helicity_coefficient_is_canonical(true, 0.0)); + for (structural_zero, coefficient) in + [(false, 0.0), (false, 2.0), (true, 1.0), (true, f64::NAN)] + { + assert!(!companion_helicity_coefficient_is_canonical( + structural_zero, + coefficient, + )); + } + } + + #[cfg(feature = "on-the-fly-test-support")] + #[test] + fn on_the_fly_query_cache_counts_builds_and_real_digest_lookups() { + let digest = SemanticDigest::new([7; 32]).unwrap(); + let mut cache = OnTheFlyQueryTraceCacheV1::default(); + + cache.insert_built(digest, 41_u32).unwrap(); + assert_eq!(cache.counts(), (1, 0)); + assert_eq!(*cache.prepared(digest).unwrap(), 41); + assert_eq!(cache.counts(), (1, 0)); + assert_eq!(*cache.lookup(digest).unwrap(), 41); + assert_eq!(*cache.lookup(digest).unwrap(), 41); + assert_eq!(cache.counts(), (1, 2)); + + let error = cache + .insert_built(digest, 99) + .expect_err("one query digest must not be rebuilt"); + assert!(error.to_string().contains("repeated a fresh query digest")); + assert_eq!(*cache.prepared(digest).unwrap(), 41); + assert_eq!(cache.counts(), (1, 2)); + } + + #[cfg(feature = "on-the-fly-test-support")] + #[test] + fn on_the_fly_probe_rejects_non_finite_outputs() { + validate_on_the_fly_probe_outputs(&[[0.0, 1.0]], &[1.0]).unwrap(); + + let raw_error = validate_on_the_fly_probe_outputs(&[[f64::NAN, 1.0]], &[1.0]) + .expect_err("non-finite raw amplitude must fail closed"); + assert!(raw_error.to_string().contains("non-finite raw amplitude")); + + let normalized_error = validate_on_the_fly_probe_outputs(&[[0.0, 1.0]], &[f64::INFINITY]) + .expect_err("non-finite normalized value must fail closed"); + assert!( + normalized_error + .to_string() + .contains("non-finite normalized value") + ); + } + + fn source_ir( + name: &str, + pdg: i32, + family: RecurrenceWavefunctionFamily, + statistics: RecurrenceParticleStatistics, + dimension: u64, + mass_parameter: Option<&str>, + ) -> RecurrenceGenericSourceIr { + RecurrenceGenericSourceIr { + identity: RecurrenceGenericParticleIdentityIr { + canonical_id: name.into(), + species_id: name.into(), + anti_canonical_id: format!("anti-{name}"), + display_name: name.into(), + anti_display_name: format!("anti-{name}"), + pdg_label: pdg, + anti_pdg_label: -pdg, + orientation: if statistics == RecurrenceParticleStatistics::Fermion { + RecurrenceSourceOrientation::Particle + } else { + RecurrenceSourceOrientation::SelfConjugate + }, + self_conjugate: statistics != RecurrenceParticleStatistics::Fermion, + }, + statistics, + wavefunction_family: family, + component_dimension: dimension, + states: Vec::new(), + crossing: RecurrenceGenericCrossingIr { + momentum_transform: RecurrenceMomentumTransform::Identity, + helicity_factor: 1, + chirality_factor: 1, + spin_state_factor: 1, + phase: [1.0, 0.0], + }, + basis: "test".into(), + mass_parameter: mass_parameter.map(str::to_owned), + width_parameter: None, + } + } + + fn source_domain_metadata() -> RecurrenceRuntimeMetadata { + RecurrenceRuntimeMetadata { + public_color_flows: Vec::new(), + runtime_parameters: Vec::new(), + prepared_parameter_defaults: vec![[0.0, 0.0]; 4], + parameter_projection: vec![RecurrenceParameterProjection { + runtime_slot: 0, + runtime_name: "MF".into(), + parameter_template_id: 0, + prepared_parameter_id: Some(3), + component: 0, + }], + source_templates: vec![ + RecurrenceSourceTemplate { + source_template_id: 0, + _current_state_template_id: 0, + dimension: 2, + helicity: -1, + chirality: 1, + spin_state: 7, + source_ir: source_ir( + "f", + 1, + RecurrenceWavefunctionFamily::Fermion, + RecurrenceParticleStatistics::Fermion, + 2, + Some("MF"), + ), + crossing: RecurrenceGenericCrossingIr { + momentum_transform: RecurrenceMomentumTransform::NegateFourMomentum, + helicity_factor: -1, + chirality_factor: -1, + spin_state_factor: 1, + phase: [1.0, 0.0], + }, + }, + RecurrenceSourceTemplate { + source_template_id: 2, + _current_state_template_id: 2, + dimension: 1, + helicity: 0, + chirality: 0, + spin_state: 0, + source_ir: source_ir( + "s", + 25, + RecurrenceWavefunctionFamily::Scalar, + RecurrenceParticleStatistics::Boson, + 1, + None, + ), + crossing: RecurrenceGenericCrossingIr { + momentum_transform: RecurrenceMomentumTransform::Identity, + helicity_factor: 1, + chirality_factor: 1, + spin_state_factor: 1, + phase: [1.0, 0.0], + }, + }, + ], + external_legs: vec![ + RecurrenceExternalLeg { + source_slot: 0, + public_label: 0, + physical_pdg: 1, + outgoing_pdg: 1, + is_initial: true, + }, + RecurrenceExternalLeg { + source_slot: 1, + public_label: 1, + physical_pdg: 25, + outgoing_pdg: 25, + is_initial: false, + }, + RecurrenceExternalLeg { + source_slot: 2, + public_label: 2, + physical_pdg: 1, + outgoing_pdg: 1, + is_initial: false, + }, + ], + particle_masses: vec![RecurrenceParticleMass { + outgoing_pdg: 1, + mass: 2.0, + }], + normalization: RecurrenceNormalization { + color_accuracy: "lc".into(), + color_factor: 1.0, + average_factor: 1.0, + identical_factor: 1.0, + global_coupling_factor: 1.0, + qcd_coupling_power: None, + electroweak_coupling_power: None, + couplings_in_stage_evaluators: true, + coupling_policy: "test".into(), + }, + color_contraction: None, + } + } + + fn crossed_fermion( + source_slot: u32, + helicity: i32, + chirality: i32, + ) -> OnTheFlySourceExecutionSpecV1 { + OnTheFlySourceExecutionSpecV1 { + source_slot, + source_template_id: 0, + spin_state_class: 7, + family: OnTheFlySourceWavefunctionFamilyV1::WeylFermion, + orientation: OnTheFlySourceOrientationV1::Particle, + helicity, + chirality, + prepared_mass_parameter_slot: Some(3), + } + } + + fn scalar_source() -> OnTheFlySourceExecutionSpecV1 { + OnTheFlySourceExecutionSpecV1 { + source_slot: 1, + source_template_id: 2, + spin_state_class: 0, + family: OnTheFlySourceWavefunctionFamilyV1::Scalar, + orientation: OnTheFlySourceOrientationV1::SelfConjugate, + helicity: 0, + chirality: 0, + prepared_mass_parameter_slot: None, + } + } + + #[test] + fn truncated_support_words_return_an_artifact_error() { + let mut cursor = 0; + let error = read_u64_values(&[0; 7], &mut cursor, 1, "recurrence support mask") + .expect_err("truncated support words must be rejected"); + assert!( + error + .to_string() + .contains("truncated recurrence support mask") + ); + assert_eq!(cursor, 0); + } + + #[test] + fn compact_sources_match_crossing_mass_and_sparse_inert_domains() { + let metadata = source_domain_metadata(); + let slots = BTreeMap::from([( + "MF".into(), + RuntimeParameterSlots { + real: 0, + imaginary: None, + }, + )]); + let fermion = crossed_fermion(0, 1, -1); + let domains = build_on_the_fly_source_domains_from_specs( + [fermion, fermion, scalar_source()], + 4, + 4, + &metadata, + &slots, + ) + .unwrap(); + assert_eq!(domains.len(), 4); + assert_eq!(domains[0].variants.len(), 1); + assert_eq!( + domains[0].variants[0].key, + DirectSourceDispatchKey::SpinStateClass(7) + ); + let template = domains[0].variants[0].template; + assert_eq!(template.family, DirectSourceWavefunctionFamily::WeylFermion); + assert_eq!(template.orientation, DirectSourceOrientation::Particle); + assert_eq!((template.helicity, template.chirality), (1, -1)); + assert_eq!(template.mass_parameter_index, Some(3)); + assert_eq!(domains[1], domains[0]); + assert_eq!(domains[3], domains[0]); + assert_eq!(domains[2].variants.len(), 1); + assert_eq!( + domains[2].variants[0].template.family, + DirectSourceWavefunctionFamily::Scalar + ); + } + + #[test] + fn compact_source_without_prebound_mass_uses_authoritative_source_ir_slot() { + let metadata = source_domain_metadata(); + let slots = BTreeMap::from([( + "MF".into(), + RuntimeParameterSlots { + real: 0, + imaginary: None, + }, + )]); + let mut fermion = crossed_fermion(0, 1, -1); + fermion.prepared_mass_parameter_slot = None; + + let domains = + build_on_the_fly_source_domains_from_specs([fermion], 4, 4, &metadata, &slots).unwrap(); + + assert_eq!( + domains[0].variants[0].template.mass_parameter_index, + Some(3) + ); + } + + #[test] + fn compact_source_rejects_conflicting_prebound_mass_slot() { + let metadata = source_domain_metadata(); + let slots = BTreeMap::from([( + "MF".into(), + RuntimeParameterSlots { + real: 0, + imaginary: None, + }, + )]); + let mut fermion = crossed_fermion(0, 1, -1); + fermion.prepared_mass_parameter_slot = Some(2); + + let error = build_on_the_fly_source_domains_from_specs([fermion], 4, 4, &metadata, &slots) + .expect_err("an explicit compact mass slot must match SourceIR"); + + assert!(error.to_string().contains("prepared mass parameter slot")); + } + + #[test] + fn compact_source_still_rejects_non_mass_semantic_mismatch() { + let metadata = source_domain_metadata(); + let slots = BTreeMap::from([( + "MF".into(), + RuntimeParameterSlots { + real: 0, + imaginary: None, + }, + )]); + let mut fermion = crossed_fermion(0, 1, -1); + fermion.helicity = -1; + + let error = build_on_the_fly_source_domains_from_specs([fermion], 4, 4, &metadata, &slots) + .expect_err("compact helicity must remain exact"); + + assert!(error.to_string().contains("source helicity")); + } + + #[test] + fn compact_sources_reject_one_key_with_conflicting_crossed_semantics() { + let metadata = source_domain_metadata(); + let slots = BTreeMap::from([( + "MF".into(), + RuntimeParameterSlots { + real: 0, + imaginary: None, + }, + )]); + let error = build_on_the_fly_source_domains_from_specs( + [crossed_fermion(0, 1, -1), crossed_fermion(2, -1, 1)], + 4, + 4, + &metadata, + &slots, + ) + .expect_err("one dispatch key cannot have two crossed SourceIR meanings"); + assert!(error.to_string().contains("maps one dispatch key")); + } +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/engine/recurrence_manifest.rs b/pyamplicol/rust/crates/rusticol-core/src/engine/recurrence_manifest.rs new file mode 100644 index 0000000000..ca8eae7ae4 --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/engine/recurrence_manifest.rs @@ -0,0 +1,5054 @@ +// SPDX-License-Identifier: 0BSD + +//! Bounded parsing and semantic validation for recurrence execution manifests. + +use crate::engine::{ + RECURRENCE_HELICITY_SELECTOR_COMPANION_RUNTIME_CAPABILITY, + SYMMETRIC_GROUP_FFT_COLOR_RUNTIME_CAPABILITY, +}; +use crate::recurrence::direct_backend::RECURRENCE_DIRECT_BACKEND_ABI; +use crate::recurrence::template::RECURRENCE_TEMPLATE_INPUT_ABI; +use crate::recurrence::{ + CLOSURE_TARGET_DOMAIN_PROOF_KIND, RECURRENCE_BUILDER_INPUT_ABI, + RECURRENCE_COLOR_CONTRACTION_CODEC_ABI, RECURRENCE_COLOR_PROJECTION_CERTIFICATE_MEMBER, + RECURRENCE_CONTRACTED_COLOR_CAPABILITY, RECURRENCE_DIRECT_SCHEDULE_MEMBER, + RECURRENCE_DIRECT_TEMPLATE_ABI, RECURRENCE_HELICITY_DISPATCH_ABI, + RECURRENCE_LC_COLOR_CAPABILITY, RECURRENCE_PLAN_ABI, RECURRENCE_RUNTIME_CAPABILITY, + RECURRENCE_RUNTIME_KIND, RECURRENCE_RUNTIME_LAYOUT_ABI, relation_certificate_algorithm, +}; +use crate::{ArtifactProcess, PROCESS_ARTIFACT_SCHEMA_VERSION, RusticolError, RusticolResult}; +use bincode::Decode; +#[cfg(any(feature = "python-generation-bridge", test))] +use bincode::Encode; +use serde::Deserialize; +use serde_json::{Value, json}; +use sha2::{Digest, Sha256}; +use std::collections::BTreeSet; + +pub(super) const RECURRENCE_RUNTIME_STORAGE_ABI: &str = "pacbin-v1"; +pub(super) const RECURRENCE_RUNTIME_CONTAINER_KIND: &str = + "pyamplicol-recurrence-runtime-container"; +pub(super) const RECURRENCE_RUNTIME_CONTAINER_SCHEMA: u16 = 1; +pub(super) const RECURRENCE_PROCESS_BINDING_ABI: &str = "pyamplicol-recurrence-process-binding-v4"; +pub(super) const RECURRENCE_PROCESS_BINDING_PATH: &str = "recurrence-binding.bin"; +pub(super) const RECURRENCE_HELICITY_SELECTOR_BINDING_PATH: &str = "helicity-selector-binding.bin"; +pub(super) const RECURRENCE_HELICITY_DISPATCH_PATH: &str = "recurrence-helicity-dispatch-v1.bin"; +pub(super) const RECURRENCE_HELICITY_SELECTOR_COMPANION_KIND: &str = + "pyamplicol-recurrence-helicity-selector-companion-v2"; +pub(super) const RECURRENCE_PRIMARY_LOCAL_COLOR_VIEW: &str = "primary-local-color-view-v1"; +pub(super) const RECURRENCE_COLOR_CONTRACTION_PATH: &str = "recurrence-color.bin"; +pub(super) const RECURRENCE_KERNEL_PACK_MANIFEST_PATH: &str = "model/eager-kernel-pack.json"; +pub(super) const RECURRENCE_KERNEL_PAYLOAD_ROOT: &str = "model/eager-kernels"; + +const MAX_RELATION_DISCOVERY_SAMPLES: usize = 16; + +#[derive(Clone, Debug, Deserialize, Decode)] +#[cfg_attr(any(feature = "python-generation-bridge", test), derive(Encode))] +#[serde(deny_unknown_fields)] +pub(crate) struct RecurrenceExecutionManifest { + pub(super) schema_version: u32, + pub(super) kind: String, + pub(super) required_runtime_capabilities: Vec, + pub(super) process: String, + pub(super) key: String, + pub(super) color_accuracy: String, + pub(super) external_pdg_order: Vec, + pub(super) process_digest: Option, + pub(super) builder_input_abi: String, + pub(super) recurrence_plan_abi: String, + pub(super) runtime_layout_abi: String, + pub(super) direct_template_abi: String, + pub(super) direct_backend_abi: String, + pub(super) compiled_model_digest: String, + pub(super) recurrence_template_catalog_digest: String, + pub(super) prepared_kernel_pack_digest: String, + pub(super) direct_template_catalog_digest: String, + pub(super) kernel_pack: RecurrenceKernelPackReference, + pub(super) runtime_options: RecurrenceRuntimeOptions, + pub(super) runtime_metadata: RecurrenceRuntimeMetadata, + pub(super) helicity_selector_companion: Option, + pub(super) plan: RecurrencePlanSummary, + pub(super) recurrence_summary: RecurrenceSummary, +} + +#[derive(Clone, Debug, Deserialize, Decode)] +#[cfg_attr(any(feature = "python-generation-bridge", test), derive(Encode))] +#[serde(deny_unknown_fields)] +pub(super) struct RecurrenceKernelPackReference { + pub(super) manifest_path: String, + pub(super) payload_root: String, +} + +#[derive(Clone, Copy, Debug, Deserialize, Decode)] +#[cfg_attr(any(feature = "python-generation-bridge", test), derive(Encode))] +#[serde(deny_unknown_fields)] +pub(super) struct RecurrenceRuntimeOptions { + pub(super) point_tile_size: u32, + pub(super) workspace_mib: u32, +} + +#[derive(Clone, Debug, Deserialize, Decode)] +#[cfg_attr(any(feature = "python-generation-bridge", test), derive(Encode))] +#[serde(deny_unknown_fields)] +pub(super) struct RecurrencePlanSummary { + pub(super) kind: String, + pub(super) builder_input_abi: String, + pub(super) recurrence_plan_abi: String, + pub(super) runtime_layout_abi: String, + pub(super) direct_template_abi: String, + pub(super) direct_backend_abi: String, + pub(super) builder_input_sha256: String, + pub(super) prepared_kernel_pack_digest: String, + pub(super) direct_template_catalog_digest: String, + pub(super) required_runtime_capabilities: Vec, + pub(super) runtime_schedule: RecurrenceRuntimeContainer, + #[serde(default)] + pub(super) helicity_dispatch: Option, + pub(super) process_binding: RecurrenceProcessBinding, + pub(super) inspection_summary: RecurrenceInspectionSummary, +} + +/// One pre-lowered physical-colour schedule plus an exact runtime-helicity +/// dispatch. Physics and color storage remain owned by the enclosing +/// recurrence process. +#[derive(Clone, Debug, Deserialize, Decode)] +#[cfg_attr(any(feature = "python-generation-bridge", test), derive(Encode))] +#[serde(deny_unknown_fields)] +pub(super) struct RecurrenceHelicitySelectorCompanionManifest { + pub(super) schema_version: u32, + pub(super) kind: String, + pub(super) process_digest: String, + pub(super) plan: RecurrencePlanSummary, + pub(super) color_contraction: RecurrenceHelicitySelectorColorView, +} + +#[derive(Clone, Debug, Deserialize, Decode)] +#[cfg_attr(any(feature = "python-generation-bridge", test), derive(Encode))] +#[serde(deny_unknown_fields)] +pub(super) struct RecurrenceHelicityDispatchReference { + pub(super) abi: String, + pub(super) path: String, + pub(super) size_bytes: u64, + pub(super) sha256: String, + pub(super) base_runtime_layout_digest: String, + pub(super) resolved_helicity_count: u64, +} + +#[derive(Clone, Debug, Deserialize, Decode)] +#[cfg_attr(any(feature = "python-generation-bridge", test), derive(Encode))] +#[serde(deny_unknown_fields)] +pub(super) struct RecurrenceHelicitySelectorColorView { + pub(super) source: String, + pub(super) view: String, +} + +#[derive(Clone, Debug, Deserialize, Decode)] +#[cfg_attr(any(feature = "python-generation-bridge", test), derive(Encode))] +#[serde(deny_unknown_fields)] +pub(super) struct RecurrenceProcessBinding { + pub(super) abi: String, + pub(super) process_id: String, + pub(super) schedule_digest: String, + pub(super) native_schedule_semantic_digest: String, + pub(super) process_digest: String, + pub(super) process_semantic_digest: String, + pub(super) process_support_words: Vec, + pub(super) remap: RecurrenceProcessRemap, + pub(super) path: String, + pub(super) size_bytes: u64, + pub(super) sha256: String, +} + +#[derive(Clone, Debug, Deserialize, Decode, Eq, PartialEq)] +#[cfg_attr(any(feature = "python-generation-bridge", test), derive(Encode))] +#[serde(deny_unknown_fields)] +pub(super) struct RecurrenceProcessRemap { + pub(super) bijection_digest: String, + pub(super) source_slots: Vec, + pub(super) source_momentum_signs: Vec, + pub(super) source_helicity_signs: Vec, + pub(super) source_state_offsets: Vec, + pub(super) source_state_indices: Vec, + pub(super) public_flow_ids: Vec, + pub(super) physical_sector_ids: Vec, + pub(super) state_templates: RecurrenceSparseBijection, + pub(super) source_templates: RecurrenceSparseBijection, + pub(super) direct_executors: RecurrenceSparseBijection, + pub(super) parameter_slots: RecurrenceSparseBijection, +} + +#[derive(Clone, Debug, Deserialize, Decode, Eq, PartialEq)] +#[cfg_attr(any(feature = "python-generation-bridge", test), derive(Encode))] +#[serde(deny_unknown_fields)] +pub(super) struct RecurrenceSparseBijection { + pub(super) count: u32, + pub(super) changes: Vec<[u32; 2]>, +} + +#[derive(Clone, Debug, Deserialize, Decode)] +#[cfg_attr(any(feature = "python-generation-bridge", test), derive(Encode))] +#[serde(deny_unknown_fields)] +pub(super) struct RecurrenceRuntimeContainer { + pub(super) kind: String, + pub(super) schema_version: u16, + pub(super) storage_abi: String, + pub(super) path: String, + pub(super) plan_member_path: String, + pub(super) size_bytes: u64, + pub(super) sha256: String, + pub(super) member_count: u64, + pub(super) unpacked_size_bytes: u64, + pub(super) index_sha256: String, +} + +#[derive(Clone, Copy, Debug, Deserialize, Decode, Eq, PartialEq)] +#[cfg_attr(any(feature = "python-generation-bridge", test), derive(Encode))] +#[serde(rename_all = "kebab-case")] +pub(super) enum RecurrenceLcFlowLayout { + TopologyReplay, + AllFlowUnion, + ContractedColorUnion, +} + +#[derive(Clone, Copy, Debug, Deserialize, Decode, Eq, PartialEq)] +#[cfg_attr(any(feature = "python-generation-bridge", test), derive(Encode))] +#[serde(rename_all = "kebab-case")] +pub(super) enum RecurrenceCacheFootprintPolicy { + SelectorActiveMaxV1, + PersistedArenaV1, +} + +#[derive(Clone, Copy, Debug, Deserialize, Decode, Eq, PartialEq)] +#[cfg_attr(any(feature = "python-generation-bridge", test), derive(Encode))] +#[serde(rename_all = "kebab-case")] +pub(super) enum RecurrencePeakContributionCountSemantics { + ResidentPendingContributionsV1, +} + +#[derive(Clone, Debug, Deserialize, Decode)] +#[cfg_attr(any(feature = "python-generation-bridge", test), derive(Encode))] +#[serde(deny_unknown_fields)] +pub(super) struct RecurrenceInspectionSummary { + pub(super) execution_mode: String, + pub(super) recurrence_plan_abi: String, + pub(super) runtime_layout_abi: String, + pub(super) direct_template_abi: String, + pub(super) process_id: String, + pub(super) lc_flow_layout: RecurrenceLcFlowLayout, + pub(super) parameter_count: u64, + pub(super) sector_count: u64, + pub(super) prepared_kernel_count: u64, + pub(super) direct_executor_count: u64, + pub(super) semantic_digest: String, + pub(super) schedule_digest: String, + pub(super) runtime_layout_digest: String, + pub(super) schedule: RecurrenceScheduleSummary, + #[serde(default)] + pub(super) selector_work_certificate: Option, + pub(super) construction: RecurrenceConstructionSummary, + pub(super) direct_arena: RecurrenceDirectArenaSummary, + pub(super) runtime_container_member: RecurrenceRuntimeContainerMember, + #[serde(default)] + pub(super) color_projection_certificate: Option, + #[serde(default)] + pub(super) relation_discovery: Option, + #[serde(default)] + pub(super) structural_zero_physical_sector_certificate: + Option, + pub(super) generation_timings_seconds: RecurrenceGenerationTimings, +} + +#[derive(Clone, Debug, Deserialize, Decode)] +#[cfg_attr(any(feature = "python-generation-bridge", test), derive(Encode))] +#[serde(deny_unknown_fields)] +pub(super) struct RecurrenceStructuralZeroPhysicalSectorCertificate { + pub(super) proof_kind: String, + pub(super) semantic_digest: String, + pub(super) sector_count: u64, + pub(super) resolved_helicity_count: u64, + pub(super) certified_structural_zero_sector_count: u64, +} + +#[derive(Clone, Copy, Debug, Deserialize, Decode)] +#[cfg_attr(any(feature = "python-generation-bridge", test), derive(Encode))] +#[serde(deny_unknown_fields)] +pub(super) struct RecurrenceScheduleSummary { + pub(super) current_count: u64, + pub(super) source_row_count: u64, + pub(super) contribution_count: u64, + pub(super) finalization_count: u64, + pub(super) amplitude_destination_count: u64, + pub(super) closure_term_count: u64, + pub(super) replay_target_count: u64, + pub(super) retained_helicity_count: u64, + pub(super) resolved_helicity_count: u64, + pub(super) exact_factor_count: u64, +} + +#[derive(Clone, Copy, Debug, Deserialize, Decode)] +#[cfg_attr(any(feature = "python-generation-bridge", test), derive(Encode))] +#[serde(deny_unknown_fields)] +pub(super) struct RecurrenceSelectorPersistedUnion { + pub(super) current_count: u64, + pub(super) semantic_component_count: u64, + pub(super) source_row_count: u64, + pub(super) contribution_count: u64, + pub(super) finalization_count: u64, + pub(super) closure_count: u64, + pub(super) row_count: u64, +} + +#[derive(Clone, Copy, Debug, Deserialize, Decode)] +#[cfg_attr(any(feature = "python-generation-bridge", test), derive(Encode))] +#[serde(deny_unknown_fields)] +pub(super) struct RecurrenceSelectorRepresentative { + pub(super) representative_sector_id: u64, + pub(super) public_flow_count: u64, + pub(super) current_count: u64, + pub(super) semantic_component_count: u64, + pub(super) source_row_count: u64, + pub(super) contribution_count: u64, + pub(super) finalization_count: u64, + pub(super) closure_count: u64, + pub(super) amplitude_destination_count: u64, + pub(super) row_count: u64, +} + +#[derive(Clone, Debug, Deserialize, Decode)] +#[cfg_attr(any(feature = "python-generation-bridge", test), derive(Encode))] +#[serde(deny_unknown_fields)] +pub(super) struct RecurrenceSelectorWorkCertificate { + pub(super) schema_version: u16, + pub(super) binding: String, + pub(super) persisted_union: RecurrenceSelectorPersistedUnion, + pub(super) representatives: Vec, +} + +#[derive(Clone, Copy, Debug, Deserialize, Decode)] +#[cfg_attr(any(feature = "python-generation-bridge", test), derive(Encode))] +#[serde(deny_unknown_fields)] +pub(super) struct RecurrenceConstructionSummary { + pub(super) peak_current_count: u64, + pub(super) peak_contribution_count: u64, + // Typed deserialization authenticates the marker; publication audits + // decide whether an absent legacy marker is admissible evidence. + #[allow(dead_code)] + #[serde(default)] + pub(super) peak_contribution_count_semantics: Option, + pub(super) peak_dynamic_color_state_count: u64, + pub(super) color_target_prune_count: u64, + pub(super) candidate_parent_pair_count: u64, + pub(super) peak_to_final_current_ratio: f64, + pub(super) peak_to_final_contribution_ratio: f64, +} + +#[derive(Clone, Copy, Debug, Deserialize, Decode)] +#[cfg_attr(any(feature = "python-generation-bridge", test), derive(Encode))] +#[serde(deny_unknown_fields)] +pub(super) struct RecurrenceDirectArenaSummary { + pub(super) semantic_component_count: u64, + pub(super) current_arena_components: u64, + pub(super) arena_component_reuse_count: u64, + pub(super) momentum_form_count: u64, + pub(super) selector_domain_count: u64, + #[serde(default)] + pub(super) cache_footprint_policy: Option, + pub(super) row_group_count: u64, + pub(super) packed_input_bytes: u64, + pub(super) packed_output_bytes: u64, + pub(super) scatter_bytes: u64, +} + +#[derive(Clone, Debug, Deserialize, Decode)] +#[cfg_attr(any(feature = "python-generation-bridge", test), derive(Encode))] +#[serde(deny_unknown_fields)] +pub(super) struct RecurrenceRuntimeContainerMember { + pub(super) path: String, + pub(super) size_bytes: u64, + pub(super) sha256: String, + pub(super) container_size_bytes: u64, +} + +#[derive(Clone, Debug, Deserialize, Decode)] +#[cfg_attr(any(feature = "python-generation-bridge", test), derive(Encode))] +#[serde(deny_unknown_fields)] +pub(super) struct RecurrenceColorProjectionCertificate { + pub(super) path: String, + pub(super) schema_version: u16, + pub(super) proof_kind: String, + pub(super) publishable: bool, + pub(super) size_bytes: u64, + pub(super) sha256: String, +} + +#[derive(Clone, Copy, Debug, Deserialize, Decode, Eq, PartialEq)] +#[cfg_attr(any(feature = "python-generation-bridge", test), derive(Encode))] +#[serde(rename_all = "kebab-case")] +pub(super) enum RecurrenceRelationDiscoveryMode { + Diagnostic, + CertifiedReuse, +} + +#[derive(Clone, Debug, Deserialize, Decode)] +#[cfg_attr(any(feature = "python-generation-bridge", test), derive(Encode))] +#[serde(deny_unknown_fields)] +pub(super) struct RecurrenceRelationDiscoverySummary { + pub(super) schema_version: u16, + pub(super) requested_mode: RecurrenceRelationDiscoveryMode, + pub(super) state: String, + pub(super) scope: RecurrenceRelationDiscoveryScope, + pub(super) probe: RecurrenceRelationDiscoveryProbe, + pub(super) certificate_replay: RecurrenceRelationCertificateReplay, + pub(super) numerical_candidate_count: u64, + pub(super) uncertified_candidate_count: u64, + pub(super) rejected_hypothesis_count: u64, + pub(super) exact_certified_relation_count: u64, + pub(super) applied_relation_count: u64, + pub(super) interaction_evaluation_count_before: u64, + pub(super) interaction_evaluation_count_after: u64, + pub(super) interaction_evaluation_savings: u64, + pub(super) current_count_before: u64, + pub(super) current_count_after: u64, + pub(super) contribution_count_before: u64, + pub(super) contribution_count_after: u64, + pub(super) scale_copy_row_count: u64, + pub(super) certificates: Vec, + pub(super) certificate_count: u64, + pub(super) certificates_truncated: bool, + pub(super) rejected_candidates: Vec, + #[serde(default)] + pub(super) rejected_candidate_diagnostics: Option, + pub(super) follow_up_boundary: String, +} + +#[derive(Clone, Debug, Deserialize, Decode)] +#[cfg_attr(any(feature = "python-generation-bridge", test), derive(Encode))] +#[serde(deny_unknown_fields)] +pub(super) struct RecurrenceRelationRejectedDiagnostics { + pub(super) total_rejected_hypothesis_count: u64, + pub(super) retained_count: u64, + pub(super) truncated: bool, + pub(super) truncation_policy: String, + pub(super) retained_sha256: String, + pub(super) full_census_sha256: String, + pub(super) full_rejection_sha256: String, +} + +#[derive(Clone, Debug, Deserialize, Decode)] +#[cfg_attr(any(feature = "python-generation-bridge", test), derive(Encode))] +#[serde(deny_unknown_fields)] +pub(super) struct RecurrenceRelationDiscoveryScope { + pub(super) execution_mode: String, + pub(super) color_accuracy: String, + pub(super) representation: String, + pub(super) lc_flow_layout: String, +} + +#[derive(Clone, Debug, Deserialize, Decode)] +#[cfg_attr(any(feature = "python-generation-bridge", test), derive(Encode))] +#[serde(deny_unknown_fields)] +pub(super) struct RecurrenceRelationDiscoveryProbe { + pub(super) status: String, + pub(super) precision_digits: u32, + pub(super) probe_count: u32, + pub(super) verification_probe_count: u32, + pub(super) effective_projection_count: u32, + pub(super) seed: u64, + pub(super) deterministic: bool, + pub(super) candidate_only: bool, + pub(super) tested_hypothesis_count: u64, + pub(super) verification_rejected_count: u64, + pub(super) runtime_parameter_schema_sha256: Option, + pub(super) candidate_observation_batch_sha256: Option, + pub(super) verification_observation_batch_sha256: Option, + pub(super) decision_sha256: Option, + pub(super) rejection_decision_sha256: String, +} + +#[derive(Clone, Debug, Deserialize, Decode)] +#[cfg_attr(any(feature = "python-generation-bridge", test), derive(Encode))] +#[serde(deny_unknown_fields)] +pub(super) struct RecurrenceRelationCertificateReplay { + pub(super) algorithm: String, + pub(super) status: String, + pub(super) certificate_set_sha256: String, +} + +#[derive(Clone, Debug, Deserialize, Decode)] +#[cfg_attr(any(feature = "python-generation-bridge", test), derive(Encode))] +#[serde(deny_unknown_fields)] +pub(super) struct RecurrenceRelationCertificateSample { + pub(super) algorithm: String, + pub(super) current_id: u64, + pub(super) representative_id: u64, + pub(super) factor_exact_rational: RecurrenceRelationExactFactor, + pub(super) current_term_vector_sha256: String, + pub(super) representative_term_vector_sha256: String, + pub(super) proof_sha256: String, +} + +#[derive(Clone, Debug, Deserialize, Decode)] +#[cfg_attr(any(feature = "python-generation-bridge", test), derive(Encode))] +#[serde(deny_unknown_fields)] +pub(super) struct RecurrenceRelationExactFactor { + pub(super) real_numerator: String, + pub(super) real_denominator: String, + pub(super) imag_numerator: String, + pub(super) imag_denominator: String, +} + +#[derive(Clone, Debug, Deserialize, Decode)] +#[cfg_attr(any(feature = "python-generation-bridge", test), derive(Encode))] +#[serde(deny_unknown_fields)] +pub(super) struct RecurrenceRelationRejectedCandidate { + pub(super) reason: String, +} + +#[derive(Clone, Copy, Debug, Deserialize, Decode)] +#[cfg_attr(any(feature = "python-generation-bridge", test), derive(Encode))] +#[serde(deny_unknown_fields)] +pub(super) struct RecurrenceGenerationTimings { + pub(super) python_extraction: f64, + pub(super) catalog_authentication: f64, + pub(super) semantic_construction: f64, + pub(super) direct_lowering: f64, + pub(super) serialization: f64, + pub(super) native_total: f64, +} + +#[derive(Clone, Debug, Deserialize, Decode)] +#[cfg_attr(any(feature = "python-generation-bridge", test), derive(Encode))] +#[serde(deny_unknown_fields)] +pub(super) struct RecurrenceSummary { + pub(super) lc_flow_layout: RecurrenceLcFlowLayout, + pub(super) builder_input_abi: String, + pub(super) template_input_abi: String, + pub(super) current_count: u64, + pub(super) contribution_count: u64, + pub(super) closure_term_count: u64, +} + +#[derive(Clone, Debug, Deserialize, Decode)] +#[cfg_attr(any(feature = "python-generation-bridge", test), derive(Encode))] +#[serde(deny_unknown_fields)] +pub(super) struct RecurrenceRuntimeMetadata { + pub(super) public_color_flows: Vec, + pub(super) runtime_parameters: Vec, + pub(super) prepared_parameter_defaults: Vec<[f64; 2]>, + pub(super) parameter_projection: Vec, + pub(super) source_templates: Vec, + pub(super) external_legs: Vec, + pub(super) particle_masses: Vec, + pub(super) normalization: RecurrenceNormalization, + pub(super) color_contraction: Option, +} + +#[derive(Clone, Debug, Deserialize, Decode)] +#[cfg_attr(any(feature = "python-generation-bridge", test), derive(Encode))] +#[serde(deny_unknown_fields)] +pub(super) struct RecurrenceColorContractionReference { + pub(super) abi: String, + pub(super) path: String, + pub(super) size_bytes: u64, + pub(super) sha256: String, + pub(super) color_accuracy: String, + pub(super) storage: String, + pub(super) includes_color_factor: bool, + pub(super) group_count: u64, + pub(super) sector_count: u64, + pub(super) active_sector_count: u64, + pub(super) component_count: u64, + pub(super) destination_count: u64, + #[serde(default, deserialize_with = "deserialize_optional_u64_if_present")] + pub(super) materialized_destination_count: Option, + pub(super) entry_count: u64, + pub(super) logical_entry_count: u64, + pub(super) semantic_digest: String, + #[serde(default)] + pub(super) factorization: Option, + #[serde(default)] + pub(super) fft_provenance: Option, +} + +#[derive(Clone, Debug, Deserialize, Decode)] +#[cfg_attr(any(feature = "python-generation-bridge", test), derive(Encode))] +#[serde(deny_unknown_fields)] +pub(super) struct RecurrenceColorFactorizationReference { + pub(super) kind: String, + pub(super) rank: u32, + pub(super) coset_count: u64, +} + +#[derive(Clone, Debug, Deserialize, Decode)] +#[cfg_attr(any(feature = "python-generation-bridge", test), derive(Encode))] +#[serde(deny_unknown_fields)] +pub(super) struct RecurrenceColorFftProvenance { + pub(super) method: String, + pub(super) degree: u32, + pub(super) channel_count: u64, + pub(super) covered_local_group_count: u64, + pub(super) residual_group_count: u64, + pub(super) residual_entry_count: u64, + pub(super) raw_kernel_bytes: u64, + pub(super) transformed_kernel_bytes: u64, + pub(super) capability: String, +} + +#[derive(Clone, Debug, Deserialize, Decode)] +#[cfg_attr(any(feature = "python-generation-bridge", test), derive(Encode))] +#[serde(deny_unknown_fields)] +pub(super) struct RecurrencePublicColorFlow { + pub(super) public_id: String, + pub(super) construction_sector_id: u32, + pub(super) target_sector_id: u32, +} + +#[derive(Clone, Debug, Deserialize, Decode)] +#[cfg_attr(any(feature = "python-generation-bridge", test), derive(Encode))] +#[serde(deny_unknown_fields)] +pub(super) struct RecurrenceRuntimeParameter { + pub(super) name: String, + pub(super) kind: String, + pub(super) parameter_index: u32, + pub(super) default: f64, + #[serde(default)] + pub(super) runtime_name: Option, + #[serde(default)] + pub(super) complex_component: Option, +} + +#[derive(Clone, Debug, Deserialize, Decode)] +#[cfg_attr(any(feature = "python-generation-bridge", test), derive(Encode))] +#[serde(deny_unknown_fields)] +pub(super) struct RecurrenceParameterProjection { + pub(super) runtime_slot: u32, + pub(super) runtime_name: String, + pub(super) parameter_template_id: u32, + #[serde(deserialize_with = "deserialize_required_nullable_u32")] + pub(super) prepared_parameter_id: Option, + pub(super) component: u32, +} + +#[derive(Clone, Debug, Deserialize, Decode)] +#[cfg_attr(any(feature = "python-generation-bridge", test), derive(Encode))] +#[serde(deny_unknown_fields)] +pub(super) struct RecurrenceSourceTemplate { + pub(super) source_template_id: u32, + #[serde(rename = "current_state_template_id")] + pub(super) _current_state_template_id: u32, + pub(super) dimension: u64, + pub(super) helicity: i32, + pub(super) chirality: i32, + pub(super) spin_state: i32, + pub(super) source_ir: RecurrenceGenericSourceIr, + pub(super) crossing: RecurrenceGenericCrossingIr, +} + +#[derive(Clone, Debug, Deserialize, Decode)] +#[cfg_attr(any(feature = "python-generation-bridge", test), derive(Encode))] +#[serde(deny_unknown_fields)] +pub(super) struct RecurrenceExternalLeg { + pub(super) source_slot: u32, + pub(super) public_label: u32, + pub(super) physical_pdg: i32, + pub(super) outgoing_pdg: i32, + pub(super) is_initial: bool, +} + +#[derive(Clone, Copy, Debug, Deserialize, Decode)] +#[cfg_attr(any(feature = "python-generation-bridge", test), derive(Encode))] +#[serde(deny_unknown_fields)] +pub(super) struct RecurrenceParticleMass { + pub(super) outgoing_pdg: i32, + pub(super) mass: f64, +} + +#[derive(Clone, Debug, Deserialize, Decode)] +#[cfg_attr(any(feature = "python-generation-bridge", test), derive(Encode))] +#[serde(deny_unknown_fields)] +pub(super) struct RecurrenceNormalization { + pub(super) color_accuracy: String, + pub(super) color_factor: f64, + pub(super) average_factor: f64, + pub(super) identical_factor: f64, + pub(super) global_coupling_factor: f64, + #[serde(default, deserialize_with = "deserialize_optional_u64_if_present")] + pub(super) qcd_coupling_power: Option, + #[serde(default, deserialize_with = "deserialize_optional_u64_if_present")] + pub(super) electroweak_coupling_power: Option, + pub(super) couplings_in_stage_evaluators: bool, + pub(super) coupling_policy: String, +} + +#[derive(Clone, Copy, Debug, Deserialize, Decode, Eq, PartialEq)] +#[cfg_attr(any(feature = "python-generation-bridge", test), derive(Encode))] +#[serde(rename_all = "kebab-case")] +pub(super) enum RecurrenceSourceOrientation { + Particle, + Antiparticle, + SelfConjugate, +} + +#[derive(Clone, Copy, Debug, Deserialize, Decode, Eq, PartialEq)] +#[cfg_attr(any(feature = "python-generation-bridge", test), derive(Encode))] +#[serde(rename_all = "lowercase")] +pub(super) enum RecurrenceParticleStatistics { + Boson, + Fermion, + Ghost, + Auxiliary, +} + +#[derive(Clone, Copy, Debug, Deserialize, Decode, Eq, PartialEq)] +#[cfg_attr(any(feature = "python-generation-bridge", test), derive(Encode))] +#[serde(rename_all = "lowercase")] +pub(super) enum RecurrenceWavefunctionFamily { + Scalar, + Fermion, + Vector, + Spin2, + Ghost, + Auxiliary, +} + +#[derive(Clone, Copy, Debug, Deserialize, Decode, Eq, PartialEq)] +#[cfg_attr(any(feature = "python-generation-bridge", test), derive(Encode))] +#[serde(rename_all = "kebab-case")] +pub(super) enum RecurrenceMomentumTransform { + Identity, + NegateFourMomentum, +} + +#[derive(Clone, Debug, Deserialize, Decode, Eq, Ord, PartialEq, PartialOrd)] +#[cfg_attr(any(feature = "python-generation-bridge", test), derive(Encode))] +#[serde(untagged)] +pub(super) enum RecurrenceSourceSpinState { + Scalar(i32), + Components(Vec), +} + +#[derive(Clone, Debug, Deserialize, Decode, Eq, Ord, PartialEq, PartialOrd)] +#[cfg_attr(any(feature = "python-generation-bridge", test), derive(Encode))] +#[serde(deny_unknown_fields)] +pub(super) struct RecurrenceGenericSourceStateIr { + pub(super) helicity: i32, + pub(super) chirality: i32, + pub(super) spin_state: RecurrenceSourceSpinState, +} + +#[derive(Clone, Debug, Deserialize, Decode, PartialEq)] +#[cfg_attr(any(feature = "python-generation-bridge", test), derive(Encode))] +#[serde(deny_unknown_fields)] +pub(super) struct RecurrenceGenericCrossingIr { + pub(super) momentum_transform: RecurrenceMomentumTransform, + pub(super) helicity_factor: i32, + pub(super) chirality_factor: i32, + pub(super) spin_state_factor: i32, + #[serde(deserialize_with = "deserialize_recurrence_phase")] + pub(super) phase: [f64; 2], +} + +#[derive(Clone, Debug, Deserialize, Decode, Eq, PartialEq)] +#[cfg_attr(any(feature = "python-generation-bridge", test), derive(Encode))] +#[serde(deny_unknown_fields)] +pub(super) struct RecurrenceGenericParticleIdentityIr { + pub(super) canonical_id: String, + pub(super) species_id: String, + pub(super) anti_canonical_id: String, + pub(super) display_name: String, + pub(super) anti_display_name: String, + pub(super) pdg_label: i32, + pub(super) anti_pdg_label: i32, + pub(super) orientation: RecurrenceSourceOrientation, + pub(super) self_conjugate: bool, +} + +#[derive(Clone, Debug, Deserialize, Decode, PartialEq)] +#[cfg_attr(any(feature = "python-generation-bridge", test), derive(Encode))] +#[serde(deny_unknown_fields)] +pub(super) struct RecurrenceGenericSourceIr { + pub(super) identity: RecurrenceGenericParticleIdentityIr, + pub(super) statistics: RecurrenceParticleStatistics, + pub(super) wavefunction_family: RecurrenceWavefunctionFamily, + pub(super) component_dimension: u64, + pub(super) states: Vec, + pub(super) crossing: RecurrenceGenericCrossingIr, + pub(super) basis: String, + #[serde(deserialize_with = "deserialize_required_nullable_string")] + pub(super) mass_parameter: Option, + #[serde(deserialize_with = "deserialize_required_nullable_string")] + pub(super) width_parameter: Option, +} + +impl RecurrenceExecutionManifest { + fn validate(&self, outer: &ArtifactProcess) -> RusticolResult<()> { + if self.schema_version != PROCESS_ARTIFACT_SCHEMA_VERSION + || self.kind != RECURRENCE_RUNTIME_KIND + { + return Err(RusticolError::compatibility(format!( + "unsupported recurrence execution kind {:?} schema {}; regenerate the artifact", + self.kind, self.schema_version + ))); + } + if self.process != outer.expression + || self.key != outer.id + || self.color_accuracy != outer.color_accuracy + || self.external_pdg_order != outer.external_pdgs + { + return Err(RusticolError::integrity(format!( + "recurrence execution manifest does not match outer process {:?}", + outer.id + ))); + } + let uses_symmetric_group_fft = self + .runtime_metadata + .color_contraction + .as_ref() + .and_then(|reference| reference.factorization.as_ref()) + .is_some_and(|factorization| factorization.kind == "symmetric-group-fourier"); + let has_helicity_selector_companion = self.helicity_selector_companion.is_some(); + validate_capabilities( + &self.required_runtime_capabilities, + &self.color_accuracy, + uses_symmetric_group_fft, + has_helicity_selector_companion, + "recurrence execution manifest", + )?; + validate_capabilities( + &outer.required_runtime_capabilities, + &self.color_accuracy, + uses_symmetric_group_fft, + has_helicity_selector_companion, + "outer recurrence process", + )?; + validate_direct_contract( + &self.builder_input_abi, + &self.recurrence_plan_abi, + &self.runtime_layout_abi, + &self.direct_template_abi, + &self.direct_backend_abi, + "recurrence execution manifest", + )?; + parse_sha256(&self.compiled_model_digest, "recurrence compiled model")?; + parse_sha256( + &self.recurrence_template_catalog_digest, + "recurrence template catalog", + )?; + parse_sha256( + &self.prepared_kernel_pack_digest, + "recurrence prepared-kernel pack", + )?; + parse_sha256( + &self.direct_template_catalog_digest, + "recurrence direct-template catalog", + )?; + if self.kernel_pack.manifest_path != RECURRENCE_KERNEL_PACK_MANIFEST_PATH + || self.kernel_pack.payload_root != RECURRENCE_KERNEL_PAYLOAD_ROOT + { + return Err(RusticolError::security( + "recurrence kernel-pack paths are not canonical", + )); + } + bounded_len("external PDG order", self.external_pdg_order.len())?; + if self.external_pdg_order.is_empty() { + return Err(RusticolError::artifact( + "recurrence execution manifest has no external particles", + )); + } + self.runtime_options.validate()?; + self.plan.validate(RecurrencePlanValidation { + process_key: &self.key, + prepared_kernel_pack_digest: &self.prepared_kernel_pack_digest, + direct_template_catalog_digest: &self.direct_template_catalog_digest, + color_accuracy: &self.color_accuracy, + uses_symmetric_group_fft, + has_helicity_selector_companion, + role: RecurrencePlanRole::Primary, + })?; + self.recurrence_summary.validate()?; + self.runtime_metadata.validate( + &self.external_pdg_order, + &self.color_accuracy, + &self.plan.inspection_summary, + )?; + if has_helicity_selector_companion != self.process_digest.is_some() { + return Err(RusticolError::integrity( + "recurrence process digest must appear exactly with its helicity-selector companion", + )); + } + if let Some(process_digest) = &self.process_digest { + parse_sha256(process_digest, "recurrence process")?; + if process_digest != &self.plan.process_binding.process_digest { + return Err(RusticolError::integrity( + "recurrence companion process digest disagrees with the primary process binding", + )); + } + } + if let Some(companion) = &self.helicity_selector_companion { + companion.validate(self, uses_symmetric_group_fft)?; + } + + let inspection = &self.plan.inspection_summary; + let remap = &self.plan.process_binding.remap; + if (inspection.lc_flow_layout != RecurrenceLcFlowLayout::ContractedColorUnion + && remap.public_flow_ids.len() != self.runtime_metadata.public_color_flows.len()) + || remap.physical_sector_ids.len() as u64 != inspection.sector_count + { + return Err(RusticolError::integrity( + "recurrence process binding domains disagree with runtime metadata", + )); + } + let schedule = inspection.schedule; + if inspection.lc_flow_layout != self.recurrence_summary.lc_flow_layout + || schedule.current_count != self.recurrence_summary.current_count + || schedule.contribution_count != self.recurrence_summary.contribution_count + || schedule.closure_term_count != self.recurrence_summary.closure_term_count + { + return Err(RusticolError::integrity( + "recurrence inspection counts disagree with the recurrence summary", + )); + } + Ok(()) + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum RecurrencePlanRole { + Primary, + HelicitySelector, +} + +struct RecurrencePlanValidation<'a> { + process_key: &'a str, + prepared_kernel_pack_digest: &'a str, + direct_template_catalog_digest: &'a str, + color_accuracy: &'a str, + uses_symmetric_group_fft: bool, + has_helicity_selector_companion: bool, + role: RecurrencePlanRole, +} + +impl RecurrenceRuntimeOptions { + fn validate(self) -> RusticolResult<()> { + if self.point_tile_size == 0 { + return Err(RusticolError::artifact( + "recurrence point_tile_size must be positive", + )); + } + if self.workspace_mib == 0 { + return Err(RusticolError::artifact( + "recurrence workspace_mib must be positive", + )); + } + Ok(()) + } +} + +impl RecurrencePlanSummary { + fn validate(&self, validation: RecurrencePlanValidation<'_>) -> RusticolResult<()> { + let RecurrencePlanValidation { + process_key, + prepared_kernel_pack_digest, + direct_template_catalog_digest, + color_accuracy, + uses_symmetric_group_fft, + has_helicity_selector_companion, + role, + } = validation; + if self.kind != RECURRENCE_RUNTIME_KIND { + return Err(RusticolError::compatibility(format!( + "unsupported recurrence plan kind {:?}", + self.kind + ))); + } + validate_direct_contract( + &self.builder_input_abi, + &self.recurrence_plan_abi, + &self.runtime_layout_abi, + &self.direct_template_abi, + &self.direct_backend_abi, + "nested recurrence plan", + )?; + parse_sha256(&self.builder_input_sha256, "recurrence builder input")?; + parse_sha256( + &self.prepared_kernel_pack_digest, + "nested recurrence prepared-kernel pack", + )?; + parse_sha256( + &self.direct_template_catalog_digest, + "nested recurrence direct-template catalog", + )?; + if self.prepared_kernel_pack_digest != prepared_kernel_pack_digest { + return Err(RusticolError::integrity( + "recurrence prepared-kernel pack digest differs between execution and plan metadata", + )); + } + if self.direct_template_catalog_digest != direct_template_catalog_digest { + return Err(RusticolError::integrity( + "recurrence direct-template catalog digest differs between execution and plan metadata", + )); + } + validate_capabilities( + &self.required_runtime_capabilities, + color_accuracy, + uses_symmetric_group_fft, + has_helicity_selector_companion, + "nested recurrence plan", + )?; + if self.helicity_dispatch.is_some() != (role == RecurrencePlanRole::HelicitySelector) { + return Err(RusticolError::integrity(match role { + RecurrencePlanRole::Primary => { + "primary recurrence plan must not carry a helicity dispatch" + } + RecurrencePlanRole::HelicitySelector => { + "recurrence helicity-selector plan requires a helicity dispatch" + } + })); + } + self.runtime_schedule.validate()?; + let binding_path = match role { + RecurrencePlanRole::Primary => RECURRENCE_PROCESS_BINDING_PATH, + RecurrencePlanRole::HelicitySelector => RECURRENCE_HELICITY_SELECTOR_BINDING_PATH, + }; + self.process_binding.validate(process_key, binding_path)?; + self.inspection_summary + .validate(process_key, color_accuracy)?; + let schedule_identity_matches = match role { + RecurrencePlanRole::Primary => { + self.process_binding.schedule_digest == self.inspection_summary.schedule_digest + } + RecurrencePlanRole::HelicitySelector => { + self.process_binding.native_schedule_semantic_digest + == self.inspection_summary.schedule_digest + } + }; + if !schedule_identity_matches + || self.process_binding.process_semantic_digest + != self.inspection_summary.semantic_digest + || self.process_binding.process_semantic_digest != self.builder_input_sha256 + { + return Err(RusticolError::integrity( + "recurrence schedule/process binding disagrees with inspection metadata", + )); + } + let expected_schedule_path = format!( + "recurrence/schedules/{}/recurrence-runtime.pacbin", + self.process_binding.schedule_digest + ); + if self.runtime_schedule.path != expected_schedule_path { + return Err(RusticolError::integrity( + "recurrence root schedule path disagrees with its binding digest", + )); + } + if let Some(dispatch) = self.helicity_dispatch.as_ref() { + dispatch.validate(&self.process_binding, &self.inspection_summary)?; + } + let member = &self.inspection_summary.runtime_container_member; + let certificate = self + .inspection_summary + .color_projection_certificate + .as_ref(); + let expected_member_count = if certificate.is_some() { 2 } else { 1 }; + let expected_unpacked_size_bytes = + certificate.map_or(Ok(member.size_bytes), |certificate| { + member + .size_bytes + .checked_add(certificate.size_bytes) + .ok_or_else(|| { + RusticolError::artifact("recurrence runtime member sizes overflow u64") + }) + })?; + if member.path != self.runtime_schedule.plan_member_path + || member.container_size_bytes != self.runtime_schedule.size_bytes + || self.runtime_schedule.member_count != expected_member_count + || self.runtime_schedule.unpacked_size_bytes != expected_unpacked_size_bytes + { + return Err(RusticolError::integrity( + "Direct-Arena v2 plan member metadata disagrees with its recurrence runtime container", + )); + } + Ok(()) + } +} + +impl RecurrenceHelicitySelectorCompanionManifest { + fn validate( + &self, + primary: &RecurrenceExecutionManifest, + uses_symmetric_group_fft: bool, + ) -> RusticolResult<()> { + if self.schema_version != 2 || self.kind != RECURRENCE_HELICITY_SELECTOR_COMPANION_KIND { + return Err(RusticolError::compatibility( + "unsupported persisted recurrence helicity-selector companion; regenerate the artifact", + )); + } + if !matches!(primary.color_accuracy.as_str(), "nlc" | "full") { + return Err(RusticolError::integrity( + "only contracted recurrence may carry a persisted helicity-selector companion", + )); + } + parse_sha256(&self.process_digest, "recurrence helicity-selector process")?; + if primary.process_digest.as_deref() != Some(self.process_digest.as_str()) + || primary.plan.process_binding.process_digest != self.process_digest + || self.plan.process_binding.process_digest != self.process_digest + { + return Err(RusticolError::integrity( + "recurrence helicity-selector companion disagrees with the primary process digest", + )); + } + + // The v4 binding payload authenticates the compiled-model and + // recurrence-template identities. At the bounded manifest boundary, + // the shared process digest plus the two explicitly repeated pack + // identities prevent a selector plan from crossing model owners. + parse_sha256( + &primary.compiled_model_digest, + "primary recurrence compiled model", + )?; + parse_sha256( + &primary.recurrence_template_catalog_digest, + "primary recurrence template catalog", + )?; + self.plan.validate(RecurrencePlanValidation { + process_key: &primary.key, + prepared_kernel_pack_digest: &primary.prepared_kernel_pack_digest, + direct_template_catalog_digest: &primary.direct_template_catalog_digest, + color_accuracy: &primary.color_accuracy, + uses_symmetric_group_fft, + has_helicity_selector_companion: true, + role: RecurrencePlanRole::HelicitySelector, + })?; + + let primary_inspection = &primary.plan.inspection_summary; + let inspection = &self.plan.inspection_summary; + let schedule = inspection.schedule; + if inspection.lc_flow_layout != RecurrenceLcFlowLayout::AllFlowUnion + || schedule.replay_target_count != 0 + || schedule.amplitude_destination_count == 0 + || schedule.amplitude_destination_count != inspection.sector_count + || self.plan.process_binding.remap.physical_sector_ids.len() as u64 + != inspection.sector_count + { + return Err(RusticolError::integrity( + "recurrence helicity-selector plan is not a dense physical all-flow union", + )); + } + if schedule.resolved_helicity_count == 0 + || schedule.resolved_helicity_count != schedule.retained_helicity_count + || schedule.resolved_helicity_count + != primary_inspection.schedule.retained_helicity_count + { + return Err(RusticolError::integrity( + "recurrence helicity-selector plan does not retain its full resolved-helicity domain", + )); + } + if inspection.prepared_kernel_count != primary_inspection.prepared_kernel_count + || inspection.direct_executor_count != primary_inspection.direct_executor_count + || self.plan.process_binding.process_support_words + != primary.plan.process_binding.process_support_words + { + return Err(RusticolError::integrity( + "recurrence helicity-selector model identities disagree with the primary recurrence plan", + )); + } + + let primary_color = primary + .runtime_metadata + .color_contraction + .as_ref() + .ok_or_else(|| { + RusticolError::integrity( + "persisted recurrence helicity selection requires a primary color reducer", + ) + })?; + self.color_contraction + .validate(primary_color, &primary.color_accuracy, inspection)?; + Ok(()) + } +} + +impl RecurrenceHelicityDispatchReference { + fn validate( + &self, + binding: &RecurrenceProcessBinding, + inspection: &RecurrenceInspectionSummary, + ) -> RusticolResult<()> { + if self.abi != RECURRENCE_HELICITY_DISPATCH_ABI { + return Err(unsupported_abi("recurrence helicity-dispatch", &self.abi)); + } + parse_sha256(&self.sha256, "recurrence helicity-dispatch payload")?; + parse_sha256( + &self.base_runtime_layout_digest, + "recurrence helicity-dispatch base runtime layout", + )?; + if self.size_bytes == 0 || u32::try_from(self.resolved_helicity_count).is_err() { + return Err(RusticolError::artifact( + "recurrence helicity-dispatch size or resolved-helicity count is outside its wire domain", + )); + } + if self.base_runtime_layout_digest != inspection.runtime_layout_digest + || self.resolved_helicity_count != inspection.schedule.resolved_helicity_count + || self.resolved_helicity_count != inspection.schedule.retained_helicity_count + { + return Err(RusticolError::integrity( + "recurrence helicity dispatch disagrees with its base runtime layout or helicity domain", + )); + } + let expected_schedule_digest = recurrence_helicity_selector_schedule_digest( + &inspection.schedule_digest, + &self.sha256, + )?; + if binding.schedule_digest != expected_schedule_digest { + return Err(RusticolError::integrity( + "recurrence helicity-selector root digest does not bind its Direct plan and dispatch", + )); + } + let expected_path = format!( + "recurrence/schedules/{}/{}", + binding.schedule_digest, RECURRENCE_HELICITY_DISPATCH_PATH + ); + if self.path != expected_path { + return Err(RusticolError::security( + "recurrence helicity-dispatch path is not canonical", + )); + } + Ok(()) + } +} + +impl RecurrenceHelicitySelectorColorView { + fn validate( + &self, + primary: &RecurrenceColorContractionReference, + color_accuracy: &str, + selector_inspection: &RecurrenceInspectionSummary, + ) -> RusticolResult<()> { + if self.source != "primary" || self.view != RECURRENCE_PRIMARY_LOCAL_COLOR_VIEW { + return Err(RusticolError::compatibility( + "unsupported recurrence helicity-selector color view", + )); + } + if primary.color_accuracy != color_accuracy + || !matches!(color_accuracy, "nlc" | "full") + || primary.abi != RECURRENCE_COLOR_CONTRACTION_CODEC_ABI + { + return Err(RusticolError::integrity( + "recurrence helicity-selector primary color view has the wrong owner", + )); + } + let factorization_kind = primary + .factorization + .as_ref() + .map(|factorization| factorization.kind.as_str()); + let supported_reducer = matches!( + (primary.storage.as_str(), factorization_kind), + ("expanded", None) + | ( + "repeated", + None | Some("klein-four-walsh") | Some("elementary-abelian-walsh") + ) + | ("convolution-kernels", Some("symmetric-group-fourier")) + ); + if !supported_reducer + || (primary.storage == "expanded" && primary.component_count != 1) + || primary.component_count == 0 + || !primary.group_count.is_multiple_of(primary.component_count) + || primary.group_count / primary.component_count != selector_inspection.sector_count + { + return Err(RusticolError::integrity( + "recurrence helicity-selector primary local-color group shape or reducer mode is incompatible", + )); + } + Ok(()) + } +} + +fn recurrence_helicity_selector_schedule_digest( + base_schedule_digest: &str, + dispatch_sha256: &str, +) -> RusticolResult { + parse_sha256( + base_schedule_digest, + "recurrence helicity-selector base schedule", + )?; + parse_sha256(dispatch_sha256, "recurrence helicity-selector dispatch")?; + let payload = json!({ + "base_schedule_digest": base_schedule_digest, + "contract": "pyamplicol-recurrence-helicity-selector-schedule-v1", + "helicity_dispatch": { + "abi": RECURRENCE_HELICITY_DISPATCH_ABI, + "sha256": dispatch_sha256, + }, + }); + let bytes = serde_json::to_vec(&payload).map_err(|error| { + RusticolError::serialization(format!( + "could not encode recurrence helicity-selector schedule identity: {error}" + )) + })?; + Ok(format!("{:x}", Sha256::digest(bytes))) +} + +impl RecurrenceRuntimeContainer { + fn validate(&self) -> RusticolResult<()> { + if self.kind != RECURRENCE_RUNTIME_CONTAINER_KIND { + return Err(RusticolError::compatibility(format!( + "unsupported recurrence runtime container kind {:?}", + self.kind + ))); + } + if self.schema_version != RECURRENCE_RUNTIME_CONTAINER_SCHEMA { + return Err(RusticolError::compatibility(format!( + "unsupported recurrence runtime container schema {}", + self.schema_version + ))); + } + if self.storage_abi != RECURRENCE_RUNTIME_STORAGE_ABI { + return Err(unsupported_abi( + "recurrence runtime storage", + &self.storage_abi, + )); + } + let expected_prefix = "recurrence/schedules/"; + let expected_suffix = "/recurrence-runtime.pacbin"; + if !self.path.starts_with(expected_prefix) + || !self.path.ends_with(expected_suffix) + || self.path[expected_prefix.len()..self.path.len() - expected_suffix.len()].len() != 64 + || self.path.contains("..") + { + return Err(RusticolError::security( + "recurrence root schedule path is not canonical", + )); + } + if self.plan_member_path != RECURRENCE_DIRECT_SCHEDULE_MEMBER { + return Err(RusticolError::security(format!( + "Direct-Arena v2 schedule member path must be exactly {RECURRENCE_DIRECT_SCHEDULE_MEMBER:?}" + ))); + } + if !(1..=2).contains(&self.member_count) { + return Err(RusticolError::integrity( + "Direct-Arena v2 recurrence runtime container must declare one or two members", + )); + } + if self.size_bytes == 0 { + return Err(RusticolError::artifact( + "recurrence runtime container size must be positive", + )); + } + if self.unpacked_size_bytes == 0 || self.unpacked_size_bytes > self.size_bytes { + return Err(RusticolError::artifact( + "recurrence runtime unpacked size is outside canonical bounds", + )); + } + parse_sha256(&self.sha256, "recurrence runtime container")?; + parse_sha256(&self.index_sha256, "recurrence runtime index")?; + Ok(()) + } +} + +impl RecurrenceProcessBinding { + pub(super) fn native_schedule_semantic_digest(&self) -> &str { + &self.native_schedule_semantic_digest + } + + fn validate(&self, process_key: &str, expected_path: &str) -> RusticolResult<()> { + if self.abi != RECURRENCE_PROCESS_BINDING_ABI || self.path != expected_path { + return Err(RusticolError::compatibility( + "unsupported recurrence process-binding contract", + )); + } + if self.process_id != process_key { + return Err(RusticolError::integrity( + "recurrence process binding has the wrong process ID", + )); + } + parse_sha256(&self.schedule_digest, "recurrence root schedule")?; + parse_sha256( + &self.native_schedule_semantic_digest, + "recurrence native schedule semantic digest", + )?; + parse_sha256(&self.process_digest, "recurrence process")?; + parse_sha256( + &self.process_semantic_digest, + "recurrence process semantic binding", + )?; + parse_sha256(&self.sha256, "recurrence process-binding payload")?; + self.remap.validate()?; + if self.process_support_words.is_empty() + || self.process_support_words.last() == Some(&0) + || self + .process_support_words + .iter() + .map(|word| word.count_ones()) + .sum::() + != 1 + || self.size_bytes == 0 + { + return Err(RusticolError::integrity( + "recurrence process binding has invalid support words or size metadata", + )); + } + Ok(()) + } +} + +impl RecurrenceProcessRemap { + fn validate(&self) -> RusticolResult<()> { + parse_sha256(&self.bijection_digest, "recurrence process bijection")?; + validate_permutation(&self.source_slots, "recurrence source-slot remap")?; + if self.source_momentum_signs.len() != self.source_slots.len() + || self.source_helicity_signs.len() != self.source_slots.len() + || self + .source_momentum_signs + .iter() + .chain(&self.source_helicity_signs) + .any(|sign| !matches!(sign, -1 | 1)) + { + return Err(RusticolError::integrity( + "recurrence process binding has invalid source signs", + )); + } + validate_ragged_permutations( + &self.source_state_offsets, + &self.source_state_indices, + self.source_slots.len(), + "recurrence source-state remap", + )?; + validate_permutation(&self.public_flow_ids, "recurrence public-flow remap")?; + validate_permutation( + &self.physical_sector_ids, + "recurrence physical-sector remap", + )?; + self.state_templates + .validate("recurrence state-template remap")?; + self.source_templates + .validate("recurrence source-template remap")?; + self.direct_executors + .validate("recurrence direct-executor remap")?; + self.parameter_slots + .validate("recurrence parameter-slot remap")?; + Ok(()) + } +} + +impl RecurrenceSparseBijection { + fn validate(&self, context: &str) -> RusticolResult<()> { + let count = usize::try_from(self.count) + .map_err(|_| RusticolError::artifact(format!("{context} count exceeds usize")))?; + bounded_len(context, count)?; + let mut mapping = (0..self.count).collect::>(); + let mut previous = None; + for [source, target] in &self.changes { + if *source >= self.count + || *target >= self.count + || source == target + || previous.is_some_and(|value| value >= *source) + { + return Err(RusticolError::integrity(format!( + "{context} contains a noncanonical change" + ))); + } + previous = Some(*source); + mapping[*source as usize] = *target; + } + validate_permutation(&mapping, context) + } +} + +fn validate_permutation(values: &[u32], context: &str) -> RusticolResult<()> { + bounded_len(context, values.len())?; + if values + .iter() + .copied() + .any(|value| value as usize >= values.len()) + || values.iter().copied().collect::>().len() != values.len() + { + return Err(RusticolError::integrity(format!( + "{context} is not a dense zero-based permutation" + ))); + } + Ok(()) +} + +fn validate_ragged_permutations( + offsets: &[u32], + values: &[u32], + row_count: usize, + context: &str, +) -> RusticolResult<()> { + if offsets.len() != row_count.saturating_add(1) + || offsets.first().copied() != Some(0) + || offsets.last().copied().map(|value| value as usize) != Some(values.len()) + || offsets.windows(2).any(|window| window[0] > window[1]) + { + return Err(RusticolError::integrity(format!( + "{context} offsets are invalid" + ))); + } + for window in offsets.windows(2) { + let start = window[0] as usize; + let stop = window[1] as usize; + validate_permutation(&values[start..stop], context)?; + } + Ok(()) +} + +impl RecurrenceInspectionSummary { + fn validate(&self, process_key: &str, color_accuracy: &str) -> RusticolResult<()> { + if self.execution_mode != "recurrence" + || self.recurrence_plan_abi != RECURRENCE_PLAN_ABI + || self.runtime_layout_abi != RECURRENCE_RUNTIME_LAYOUT_ABI + || self.direct_template_abi != RECURRENCE_DIRECT_TEMPLATE_ABI + { + return Err(RusticolError::compatibility( + "recurrence inspection summary does not match Direct-Arena v2", + )); + } + if self.process_id != process_key { + return Err(RusticolError::integrity( + "recurrence inspection process id does not match its execution manifest", + )); + } + validate_counts( + "recurrence inspection summary", + &[ + self.parameter_count, + self.sector_count, + self.prepared_kernel_count, + self.direct_executor_count, + ], + )?; + parse_sha256(&self.semantic_digest, "recurrence semantic plan")?; + parse_sha256(&self.schedule_digest, "recurrence root schedule")?; + parse_sha256( + &self.runtime_layout_digest, + "recurrence Direct-Arena runtime layout", + )?; + self.schedule.validate()?; + self.construction.validate(self.schedule)?; + self.direct_arena.validate(self.lc_flow_layout)?; + if self.selector_work_certificate.is_some() + != self.direct_arena.cache_footprint_policy.is_some() + { + return Err(RusticolError::integrity( + "recurrence selector-work certificate and cache-footprint policy must be declared together", + )); + } + if let Some(certificate) = self.selector_work_certificate.as_ref() { + certificate.validate( + self.lc_flow_layout, + self.schedule, + self.direct_arena, + self.sector_count, + )?; + } + self.runtime_container_member.validate()?; + if let Some(certificate) = self.color_projection_certificate.as_ref() { + certificate.validate(self.lc_flow_layout)?; + } + if let Some(discovery) = self.relation_discovery.as_ref() { + discovery.validate(color_accuracy, self.lc_flow_layout, self.schedule)?; + } + match ( + self.structural_zero_physical_sector_certificate.as_ref(), + self.lc_flow_layout, + ) { + (Some(certificate), RecurrenceLcFlowLayout::ContractedColorUnion) => { + certificate.validate(self.sector_count, self.schedule.resolved_helicity_count)?; + } + (None, RecurrenceLcFlowLayout::ContractedColorUnion) => { + return Err(RusticolError::integrity( + "contracted recurrence inspection omitted its structural-zero physical-sector certificate", + )); + } + (Some(_), _) => { + return Err(RusticolError::integrity( + "structural-zero physical-sector certificate is restricted to contracted recurrence", + )); + } + (None, _) => {} + } + self.generation_timings_seconds.validate()?; + if self.schedule.source_row_count > self.schedule.current_count + || self.schedule.finalization_count > self.schedule.current_count + { + return Err(RusticolError::integrity( + "recurrence inspection schedule disagrees with its top-level counts", + )); + } + Ok(()) + } +} + +impl RecurrenceStructuralZeroPhysicalSectorCertificate { + fn validate( + &self, + physical_sector_count: u64, + resolved_helicity_count: u64, + ) -> RusticolResult<()> { + if self.proof_kind != CLOSURE_TARGET_DOMAIN_PROOF_KIND { + return Err(RusticolError::compatibility( + "unsupported recurrence structural-zero physical-sector proof", + )); + } + parse_sha256( + &self.semantic_digest, + "recurrence structural-zero physical-sector proof", + )?; + if self.sector_count != physical_sector_count + || self.resolved_helicity_count != resolved_helicity_count + || self.certified_structural_zero_sector_count > self.sector_count + { + return Err(RusticolError::integrity( + "recurrence structural-zero physical-sector proof disagrees with its inspection domain", + )); + } + Ok(()) + } +} + +fn selector_row_count( + source_rows: u64, + contributions: u64, + finalizations: u64, + closures: u64, +) -> RusticolResult { + source_rows + .checked_add(contributions) + .and_then(|total| total.checked_add(finalizations)) + .and_then(|total| total.checked_add(closures)) + .ok_or_else(|| RusticolError::artifact("recurrence selector row count overflows u64")) +} + +impl RecurrenceSelectorWorkCertificate { + fn validate( + &self, + layout: RecurrenceLcFlowLayout, + schedule: RecurrenceScheduleSummary, + direct_arena: RecurrenceDirectArenaSummary, + sector_count: u64, + ) -> RusticolResult<()> { + if self.schema_version != 1 || self.binding != "runtime_layout_digest" { + return Err(RusticolError::compatibility( + "recurrence selector-work certificate identity is unsupported", + )); + } + self.persisted_union.validate(schedule, direct_arena)?; + bounded_len( + "recurrence selector-work representatives", + self.representatives.len(), + )?; + let has_representatives = !self.representatives.is_empty(); + let has_replay_targets = schedule.replay_target_count != 0; + match layout { + RecurrenceLcFlowLayout::TopologyReplay if !has_representatives => { + return Err(RusticolError::integrity( + "recurrence selector-work representatives disagree with the flow layout", + )); + } + RecurrenceLcFlowLayout::AllFlowUnion if has_representatives => { + return Err(RusticolError::integrity( + "recurrence selector-work representatives disagree with the flow layout", + )); + } + // Schema-v1 contracted artifacts predating representative-level + // accounting legitimately carry replay targets with an empty + // representative list. New writers emit representatives, but + // their absence must remain load-compatible until the certificate + // schema itself is versioned. + RecurrenceLcFlowLayout::ContractedColorUnion + if has_representatives && !has_replay_targets => + { + return Err(RusticolError::integrity( + "recurrence selector-work representatives disagree with the flow layout", + )); + } + RecurrenceLcFlowLayout::TopologyReplay + | RecurrenceLcFlowLayout::AllFlowUnion + | RecurrenceLcFlowLayout::ContractedColorUnion => {} + } + let mut sector_ids = BTreeSet::new(); + let mut public_flow_count = 0_u64; + for representative in &self.representatives { + representative.validate( + self.persisted_union, + schedule.amplitude_destination_count, + sector_count, + )?; + if !sector_ids.insert(representative.representative_sector_id) { + return Err(RusticolError::integrity( + "recurrence selector-work representative sector is duplicated", + )); + } + public_flow_count = public_flow_count + .checked_add(representative.public_flow_count) + .ok_or_else(|| { + RusticolError::artifact( + "recurrence selector-work public-flow count overflows u64", + ) + })?; + } + if has_representatives && public_flow_count != schedule.replay_target_count { + return Err(RusticolError::integrity( + "recurrence selector-work public-flow count disagrees with replay targets", + )); + } + Ok(()) + } +} + +impl RecurrenceSelectorPersistedUnion { + fn validate( + self, + schedule: RecurrenceScheduleSummary, + direct_arena: RecurrenceDirectArenaSummary, + ) -> RusticolResult<()> { + validate_counts( + "recurrence persisted selector-work summary", + &[ + self.current_count, + self.semantic_component_count, + self.source_row_count, + self.contribution_count, + self.finalization_count, + self.closure_count, + self.row_count, + ], + )?; + if self.current_count != schedule.current_count + || self.semantic_component_count != direct_arena.semantic_component_count + || self.source_row_count != schedule.source_row_count + || self.contribution_count != schedule.contribution_count + || self.finalization_count != schedule.finalization_count + || self.closure_count != schedule.closure_term_count + || self.row_count + != selector_row_count( + self.source_row_count, + self.contribution_count, + self.finalization_count, + self.closure_count, + )? + { + return Err(RusticolError::integrity( + "recurrence persisted selector-work summary disagrees with the inspection schedule", + )); + } + Ok(()) + } +} + +impl RecurrenceSelectorRepresentative { + fn validate( + self, + persisted: RecurrenceSelectorPersistedUnion, + amplitude_destination_count: u64, + sector_count: u64, + ) -> RusticolResult<()> { + validate_counts( + "recurrence selector-work representative", + &[ + self.representative_sector_id, + self.public_flow_count, + self.current_count, + self.semantic_component_count, + self.source_row_count, + self.contribution_count, + self.finalization_count, + self.closure_count, + self.amplitude_destination_count, + self.row_count, + ], + )?; + if self.representative_sector_id >= sector_count + || self.public_flow_count == 0 + || self.current_count > persisted.current_count + || self.semantic_component_count > persisted.semantic_component_count + || self.source_row_count > persisted.source_row_count + || self.contribution_count > persisted.contribution_count + || self.finalization_count > persisted.finalization_count + || self.closure_count > persisted.closure_count + || self.amplitude_destination_count > amplitude_destination_count + || self.row_count + != selector_row_count( + self.source_row_count, + self.contribution_count, + self.finalization_count, + self.closure_count, + )? + { + return Err(RusticolError::integrity( + "recurrence selector-work representative is inconsistent", + )); + } + Ok(()) + } +} + +impl RecurrenceConstructionSummary { + fn validate(self, schedule: RecurrenceScheduleSummary) -> RusticolResult<()> { + validate_counts( + "recurrence construction summary", + &[ + self.peak_current_count, + self.peak_contribution_count, + self.peak_dynamic_color_state_count, + self.color_target_prune_count, + self.candidate_parent_pair_count, + ], + )?; + if self.peak_current_count < schedule.current_count + || self.peak_contribution_count < schedule.contribution_count + || !self.peak_to_final_current_ratio.is_finite() + || !self.peak_to_final_contribution_ratio.is_finite() + || self.peak_to_final_current_ratio < 0.0 + || self.peak_to_final_contribution_ratio < 0.0 + || (schedule.current_count != 0 && self.peak_to_final_current_ratio < 1.0) + || (schedule.contribution_count != 0 && self.peak_to_final_contribution_ratio < 1.0) + { + return Err(RusticolError::integrity( + "recurrence construction summary is inconsistent with the final schedule", + )); + } + Ok(()) + } +} + +impl RecurrenceScheduleSummary { + fn validate(self) -> RusticolResult<()> { + validate_counts( + "recurrence schedule summary", + &[ + self.current_count, + self.source_row_count, + self.contribution_count, + self.finalization_count, + self.amplitude_destination_count, + self.closure_term_count, + self.replay_target_count, + self.retained_helicity_count, + self.resolved_helicity_count, + self.exact_factor_count, + ], + )?; + if self.resolved_helicity_count > self.retained_helicity_count { + return Err(RusticolError::integrity( + "recurrence resolved helicities exceed retained helicities", + )); + } + Ok(()) + } +} + +impl RecurrenceDirectArenaSummary { + fn validate(self, layout: RecurrenceLcFlowLayout) -> RusticolResult<()> { + validate_counts( + "recurrence Direct-Arena summary", + &[ + self.semantic_component_count, + self.current_arena_components, + self.arena_component_reuse_count, + self.momentum_form_count, + self.selector_domain_count, + self.row_group_count, + self.packed_input_bytes, + self.packed_output_bytes, + self.scatter_bytes, + ], + )?; + let expected_semantic = self + .current_arena_components + .checked_add(self.arena_component_reuse_count) + .ok_or_else(|| { + RusticolError::artifact("recurrence Direct-Arena component count overflows u64") + })?; + if self.semantic_component_count != expected_semantic { + return Err(RusticolError::integrity( + "recurrence Direct-Arena component reuse count is inconsistent", + )); + } + if self.packed_input_bytes != 0 || self.packed_output_bytes != 0 || self.scatter_bytes != 0 + { + return Err(RusticolError::compatibility( + "Direct-Arena v2 must not declare packed input, packed output, or scatter bytes", + )); + } + let expected_policy = match layout { + RecurrenceLcFlowLayout::TopologyReplay => { + RecurrenceCacheFootprintPolicy::SelectorActiveMaxV1 + } + RecurrenceLcFlowLayout::AllFlowUnion | RecurrenceLcFlowLayout::ContractedColorUnion => { + RecurrenceCacheFootprintPolicy::PersistedArenaV1 + } + }; + if self + .cache_footprint_policy + .is_some_and(|policy| policy != expected_policy) + { + return Err(RusticolError::integrity( + "recurrence Direct-Arena cache-footprint policy disagrees with its flow layout", + )); + } + Ok(()) + } +} + +impl RecurrenceRuntimeContainerMember { + fn validate(&self) -> RusticolResult<()> { + if self.path != RECURRENCE_DIRECT_SCHEDULE_MEMBER { + return Err(RusticolError::security(format!( + "Direct-Arena v2 inspection member path must be exactly {RECURRENCE_DIRECT_SCHEDULE_MEMBER:?}" + ))); + } + if self.size_bytes == 0 { + return Err(RusticolError::artifact( + "Direct-Arena v2 plan member size is outside canonical bounds", + )); + } + if self.container_size_bytes == 0 || self.size_bytes > self.container_size_bytes { + return Err(RusticolError::artifact( + "Direct-Arena v2 plan member/container sizes are inconsistent", + )); + } + parse_sha256(&self.sha256, "recurrence Direct-Arena plan member")?; + Ok(()) + } +} + +impl RecurrenceColorProjectionCertificate { + fn validate(&self, layout: RecurrenceLcFlowLayout) -> RusticolResult<()> { + if layout != RecurrenceLcFlowLayout::TopologyReplay + || self.path != RECURRENCE_COLOR_PROJECTION_CERTIFICATE_MEMBER + || self.schema_version != 1 + || self.proof_kind != "exact-rectangular-sum-projection" + { + return Err(RusticolError::compatibility( + "unsupported recurrence color-projection certificate contract", + )); + } + if !self.publishable || self.size_bytes == 0 { + return Err(RusticolError::integrity( + "recurrence color-projection certificate metadata is not publishable", + )); + } + parse_sha256(&self.sha256, "recurrence color-projection certificate")?; + Ok(()) + } +} + +impl RecurrenceRelationDiscoverySummary { + fn validate( + &self, + color_accuracy: &str, + layout: RecurrenceLcFlowLayout, + schedule: RecurrenceScheduleSummary, + ) -> RusticolResult<()> { + if self.schema_version != 1 { + return Err(RusticolError::compatibility( + "unsupported recurrence relation-discovery summary schema", + )); + } + let expected_layout = match layout { + RecurrenceLcFlowLayout::TopologyReplay => "topology-replay", + RecurrenceLcFlowLayout::AllFlowUnion => "all-flow-union", + RecurrenceLcFlowLayout::ContractedColorUnion => "contracted-color-union", + }; + if self.scope.execution_mode != "recurrence" + || self.scope.representation != "recurrence-direct-plan-v2" + || self.scope.lc_flow_layout != expected_layout + || self.scope.color_accuracy != color_accuracy + { + return Err(RusticolError::compatibility( + "unsupported recurrence relation-discovery scope", + )); + } + for (value, context) in [ + (&self.state, "recurrence relation-discovery state"), + ( + &self.scope.color_accuracy, + "recurrence relation-discovery color accuracy", + ), + ( + &self.follow_up_boundary, + "recurrence relation-discovery follow-up boundary", + ), + ] { + validate_text(value, context)?; + } + validate_counts( + "recurrence relation-discovery summary", + &[ + self.numerical_candidate_count, + self.uncertified_candidate_count, + self.rejected_hypothesis_count, + self.exact_certified_relation_count, + self.applied_relation_count, + self.interaction_evaluation_count_before, + self.interaction_evaluation_count_after, + self.interaction_evaluation_savings, + self.current_count_before, + self.current_count_after, + self.contribution_count_before, + self.contribution_count_after, + self.scale_copy_row_count, + self.certificate_count, + ], + )?; + // Every u64 seed is valid. It remains part of the strict manifest + // identity even though no runtime arithmetic depends on it. + let _probe_seed = self.probe.seed; + if self.probe.status != "completed" + || self.probe.precision_digits < 80 + || self.probe.probe_count < 2 + || self.probe.verification_probe_count < 2 + || self.probe.effective_projection_count < self.probe.probe_count + || !self.probe.deterministic + { + return Err(RusticolError::integrity( + "recurrence relation-discovery probe contract is invalid", + )); + } + parse_sha256( + &self.probe.rejection_decision_sha256, + "recurrence rejected-hypothesis decisions", + )?; + for (digest, context) in [ + ( + self.probe.runtime_parameter_schema_sha256.as_ref(), + "recurrence runtime-parameter schema", + ), + ( + self.probe.candidate_observation_batch_sha256.as_ref(), + "recurrence candidate observation batch", + ), + ( + self.probe.verification_observation_batch_sha256.as_ref(), + "recurrence verification observation batch", + ), + ( + self.probe.decision_sha256.as_ref(), + "recurrence full hypothesis decisions", + ), + ] { + if let Some(digest) = digest { + parse_sha256(digest, context)?; + } + } + if !self.probe.candidate_only { + let authenticated_raw_commitments = [ + self.probe.runtime_parameter_schema_sha256.as_ref(), + self.probe.candidate_observation_batch_sha256.as_ref(), + self.probe.verification_observation_batch_sha256.as_ref(), + self.probe.decision_sha256.as_ref(), + ]; + let certified = self.exact_certified_relation_count; + if authenticated_raw_commitments + .iter() + .any(|digest| digest.is_none()) + || self.numerical_candidate_count + != certified + .checked_add(self.probe.verification_rejected_count) + .ok_or_else(|| { + RusticolError::integrity("recurrence raw candidate count overflows") + })? + || self.uncertified_candidate_count != self.probe.verification_rejected_count + || self.uncertified_candidate_count + != self + .numerical_candidate_count + .checked_sub(certified) + .ok_or_else(|| { + RusticolError::integrity("recurrence raw uncertified count underflows") + })? + || self.rejected_hypothesis_count + != self + .probe + .tested_hypothesis_count + .checked_sub(certified) + .ok_or_else(|| { + RusticolError::integrity("recurrence raw rejection count underflows") + })? + { + return Err(RusticolError::integrity( + "recurrence authenticated raw rejection census is inconsistent", + )); + } + } + let certified = self.exact_certified_relation_count; + let applied = self.applied_relation_count; + let expected_state = match self.requested_mode { + RecurrenceRelationDiscoveryMode::Diagnostic => { + if applied != 0 || self.scale_copy_row_count != 0 { + return Err(RusticolError::integrity( + "diagnostic recurrence relation discovery altered the schedule", + )); + } + "diagnostic-only" + } + RecurrenceRelationDiscoveryMode::CertifiedReuse => { + if applied != certified || self.scale_copy_row_count != applied { + return Err(RusticolError::integrity( + "certified recurrence relation-discovery counts disagree", + )); + } + if applied == 0 { + "diagnostic-only" + } else { + "exact-certified-applied" + } + } + }; + if self.state != expected_state + || self.current_count_before != schedule.current_count + || self.current_count_after != schedule.current_count + || self.interaction_evaluation_count_before != self.contribution_count_before + || self.interaction_evaluation_count_after > self.interaction_evaluation_count_before + || self.interaction_evaluation_savings + != self + .interaction_evaluation_count_before + .checked_sub(self.interaction_evaluation_count_after) + .ok_or_else(|| { + RusticolError::integrity( + "recurrence relation-discovery interaction counts underflow", + ) + })? + || self.contribution_count_after + != self + .interaction_evaluation_count_after + .checked_add(certified) + .ok_or_else(|| { + RusticolError::artifact( + "recurrence relation-discovery contribution count overflows", + ) + })? + || match self.requested_mode { + RecurrenceRelationDiscoveryMode::Diagnostic => { + self.contribution_count_before != schedule.contribution_count + } + RecurrenceRelationDiscoveryMode::CertifiedReuse => { + self.contribution_count_after != schedule.contribution_count + } + } + { + return Err(RusticolError::integrity( + "recurrence relation-discovery summary disagrees with the schedule", + )); + } + if self.certificate_count != certified + || self.certificates.len() + != usize::try_from(certified.min(MAX_RELATION_DISCOVERY_SAMPLES as u64)).map_err( + |_| { + RusticolError::artifact( + "recurrence relation-discovery sample count exceeds usize", + ) + }, + )? + || self.certificates_truncated != (certified > MAX_RELATION_DISCOVERY_SAMPLES as u64) + || self.certificates.len() > MAX_RELATION_DISCOVERY_SAMPLES + || self.rejected_candidates.len() > MAX_RELATION_DISCOVERY_SAMPLES + { + return Err(RusticolError::integrity( + "recurrence relation-discovery bounded samples are inconsistent", + )); + } + let replay = &self.certificate_replay; + if replay.algorithm != relation_certificate_algorithm() + || replay.status + != if certified == 0 { + "no-certified-relations" + } else { + "verified" + } + { + return Err(RusticolError::compatibility( + "unsupported recurrence relation-certificate replay contract", + )); + } + parse_sha256( + &replay.certificate_set_sha256, + "recurrence relation certificate set", + )?; + for certificate in &self.certificates { + certificate.validate(self.current_count_before)?; + } + for rejected in &self.rejected_candidates { + validate_text( + &rejected.reason, + "recurrence relation-discovery rejection reason", + )?; + } + if !self.probe.candidate_only && self.rejected_candidate_diagnostics.is_none() { + return Err(RusticolError::integrity( + "full recurrence numerical probes require rejected-candidate diagnostics", + )); + } + if let Some(diagnostics) = self.rejected_candidate_diagnostics.as_ref() { + let retained = u64::try_from(self.rejected_candidates.len()).map_err(|_| { + RusticolError::artifact( + "recurrence relation-discovery rejection sample count exceeds u64", + ) + })?; + let retention_is_consistent = match diagnostics.truncation_policy.as_str() { + "first-16-in-canonical-discovery-order-v1" => { + retained + == diagnostics + .total_rejected_hypothesis_count + .min(MAX_RELATION_DISCOVERY_SAMPLES as u64) + } + "none-authenticated-full-rejection-digest-v1" => retained == 0, + _ => false, + }; + if diagnostics.total_rejected_hypothesis_count != self.rejected_hypothesis_count + || diagnostics.retained_count != retained + || diagnostics.truncated != (diagnostics.total_rejected_hypothesis_count > retained) + || !retention_is_consistent + { + return Err(RusticolError::integrity( + "recurrence relation-discovery rejection diagnostics are inconsistent", + )); + } + let retained_rows = self + .rejected_candidates + .iter() + .map(|rejected| json!({"reason": &rejected.reason})) + .collect::>(); + let retained_payload = json!({ + "abi": "pyamplicol-rejected-relation-diagnostics-v1", + "rows": retained_rows, + }); + let retained_bytes = serde_json::to_vec(&retained_payload).map_err(|error| { + RusticolError::artifact(format!( + "could not canonicalize recurrence retained rejection diagnostics: {error}" + )) + })?; + let expected_retained_sha256 = format!("{:x}", Sha256::digest(retained_bytes)); + if diagnostics.retained_sha256 != expected_retained_sha256 { + return Err(RusticolError::integrity( + "recurrence retained rejection diagnostics commitment is inconsistent", + )); + } + let full_census_payload = json!({ + "abi": "pyamplicol-relation-discovery-full-census-v1", + "tested_hypothesis_count": + self.probe.tested_hypothesis_count, + "numerical_candidate_count": self.numerical_candidate_count, + "verification_rejected_count": + self.probe.verification_rejected_count, + "uncertified_candidate_count": + self.uncertified_candidate_count, + "certified_relation_count": + self.exact_certified_relation_count, + "rejected_hypothesis_count": + self.rejected_hypothesis_count, + "applied_relation_count": self.applied_relation_count, + "runtime_parameter_schema_sha256": + self.probe.runtime_parameter_schema_sha256, + "candidate_observation_batch_sha256": + self.probe.candidate_observation_batch_sha256, + "verification_observation_batch_sha256": + self.probe.verification_observation_batch_sha256, + "decision_sha256": self.probe.decision_sha256, + "rejection_decision_sha256": + self.probe.rejection_decision_sha256, + }); + let full_census_bytes = serde_json::to_vec(&full_census_payload).map_err(|error| { + RusticolError::artifact(format!( + "could not canonicalize recurrence rejection census: {error}" + )) + })?; + let expected_full_census_sha256 = format!("{:x}", Sha256::digest(full_census_bytes)); + if diagnostics.full_census_sha256 != expected_full_census_sha256 + || diagnostics.full_rejection_sha256 != self.probe.rejection_decision_sha256 + { + return Err(RusticolError::integrity( + "recurrence full rejection census or decision commitment is inconsistent", + )); + } + for (digest, context) in [ + ( + &diagnostics.retained_sha256, + "recurrence retained rejection diagnostics", + ), + ( + &diagnostics.full_census_sha256, + "recurrence rejection full census", + ), + ( + &diagnostics.full_rejection_sha256, + "recurrence full rejection decisions", + ), + ] { + parse_sha256(digest, context)?; + } + } + Ok(()) + } +} + +impl RecurrenceRelationCertificateSample { + fn validate(&self, current_count: u64) -> RusticolResult<()> { + if self.algorithm != relation_certificate_algorithm() + || self.current_id >= current_count + || self.representative_id >= current_count + || self.current_id == self.representative_id + { + return Err(RusticolError::integrity( + "recurrence relation-certificate sample is inconsistent", + )); + } + self.factor_exact_rational.validate()?; + for (digest, context) in [ + ( + &self.current_term_vector_sha256, + "recurrence current term vector", + ), + ( + &self.representative_term_vector_sha256, + "recurrence representative term vector", + ), + (&self.proof_sha256, "recurrence relation proof"), + ] { + parse_sha256(digest, context)?; + } + Ok(()) + } +} + +impl RecurrenceRelationExactFactor { + fn validate(&self) -> RusticolResult<()> { + for (value, context) in [ + (&self.real_numerator, "recurrence relation real numerator"), + ( + &self.real_denominator, + "recurrence relation real denominator", + ), + ( + &self.imag_numerator, + "recurrence relation imaginary numerator", + ), + ( + &self.imag_denominator, + "recurrence relation imaginary denominator", + ), + ] { + validate_text(value, context)?; + value.parse::().map_err(|_| { + RusticolError::artifact(format!("{context} is not a bounded integer")) + })?; + } + if self.real_denominator == "0" || self.imag_denominator == "0" { + return Err(RusticolError::artifact( + "recurrence relation factor denominator is zero", + )); + } + Ok(()) + } +} + +impl RecurrenceGenerationTimings { + fn validate(self) -> RusticolResult<()> { + let values = [ + self.python_extraction, + self.catalog_authentication, + self.semantic_construction, + self.direct_lowering, + self.serialization, + self.native_total, + ]; + if values + .iter() + .any(|value| !value.is_finite() || *value < 0.0) + { + return Err(RusticolError::artifact( + "recurrence Direct-Arena generation timings must be finite and nonnegative", + )); + } + Ok(()) + } +} + +impl RecurrenceSummary { + fn validate(&self) -> RusticolResult<()> { + if self.builder_input_abi != RECURRENCE_BUILDER_INPUT_ABI { + return Err(unsupported_abi( + "recurrence builder input", + &self.builder_input_abi, + )); + } + if self.template_input_abi != RECURRENCE_TEMPLATE_INPUT_ABI { + return Err(unsupported_abi( + "recurrence template input", + &self.template_input_abi, + )); + } + validate_counts( + "recurrence summary", + &[ + self.current_count, + self.contribution_count, + self.closure_term_count, + ], + ) + } +} + +impl RecurrenceRuntimeMetadata { + fn validate( + &self, + external_pdgs: &[i32], + color_accuracy: &str, + inspection: &RecurrenceInspectionSummary, + ) -> RusticolResult<()> { + for (context, len) in [ + ("public color flows", self.public_color_flows.len()), + ("runtime parameters", self.runtime_parameters.len()), + ( + "prepared parameter defaults", + self.prepared_parameter_defaults.len(), + ), + ("parameter projection", self.parameter_projection.len()), + ("source templates", self.source_templates.len()), + ("external legs", self.external_legs.len()), + ("particle masses", self.particle_masses.len()), + ] { + bounded_len(context, len)?; + } + let expected_public_flow_count = match inspection.lc_flow_layout { + RecurrenceLcFlowLayout::TopologyReplay => Some(inspection.schedule.replay_target_count), + // Folded trace reflections remain distinct public flows while + // sharing one authenticated construction sector. + RecurrenceLcFlowLayout::AllFlowUnion => None, + RecurrenceLcFlowLayout::ContractedColorUnion => Some(0), + }; + if expected_public_flow_count + .is_some_and(|count| self.public_color_flows.len() as u64 != count) + { + return Err(RusticolError::integrity( + "recurrence public color-flow count does not match the Direct-Arena replay count", + )); + } + let mut public_ids = BTreeSet::new(); + let mut construction_sector_ids = BTreeSet::new(); + for (target_sector_id, flow) in self.public_color_flows.iter().enumerate() { + validate_text(&flow.public_id, "recurrence public color-flow ID")?; + if !public_ids.insert(flow.public_id.as_str()) { + return Err(RusticolError::artifact( + "recurrence public color-flow IDs are not unique", + )); + } + if u64::from(flow.construction_sector_id) >= inspection.sector_count { + return Err(RusticolError::integrity( + "recurrence public color flow references an absent construction sector", + )); + } + construction_sector_ids.insert(flow.construction_sector_id); + if flow.target_sector_id as usize != target_sector_id + || flow.target_sector_id as usize >= self.public_color_flows.len() + { + return Err(RusticolError::integrity( + "recurrence public color-flow target sectors are not dense or in range", + )); + } + } + if inspection.lc_flow_layout == RecurrenceLcFlowLayout::AllFlowUnion + && (construction_sector_ids.len() as u64 != inspection.sector_count + || construction_sector_ids.iter().copied().ne(0..u32::try_from( + inspection.sector_count, + ) + .map_err(|_| { + RusticolError::artifact("recurrence Direct-Arena sector count exceeds u32") + })?)) + { + return Err(RusticolError::integrity( + "recurrence public color flows do not cover every Direct-Arena construction sector", + )); + } + match inspection.lc_flow_layout { + RecurrenceLcFlowLayout::TopologyReplay | RecurrenceLcFlowLayout::AllFlowUnion => { + if color_accuracy != "lc" || self.public_color_flows.is_empty() { + return Err(RusticolError::artifact( + "LC recurrence runtime metadata requires public physical color flows", + )); + } + if self.color_contraction.is_some() { + return Err(RusticolError::artifact( + "LC recurrence must not carry a contracted-color payload", + )); + } + } + RecurrenceLcFlowLayout::ContractedColorUnion => { + if !matches!(color_accuracy, "nlc" | "full") || !self.public_color_flows.is_empty() + { + return Err(RusticolError::artifact( + "contracted recurrence metadata requires NLC/full color and no public color-flow axis", + )); + } + let color_contraction = self.color_contraction.as_ref().ok_or_else(|| { + RusticolError::artifact( + "NLC/full recurrence has no contracted-color payload reference", + ) + })?; + color_contraction.validate(color_accuracy, inspection)?; + } + } + validate_recurrence_parameter_runtime_parts( + &self.runtime_parameters, + &self.prepared_parameter_defaults, + &self.parameter_projection, + inspection.parameter_count, + )?; + + if self.source_templates.is_empty() { + return Err(RusticolError::artifact( + "recurrence runtime metadata has no source templates", + )); + } + let mut previous_source_id = None; + let mut source_pdgs = BTreeSet::new(); + for (index, source) in self.source_templates.iter().enumerate() { + if previous_source_id.is_some_and(|previous| previous >= source.source_template_id) { + return Err(RusticolError::artifact( + "recurrence source templates are not in strict source-template ID order", + )); + } + previous_source_id = Some(source.source_template_id); + source.validate(index)?; + source_pdgs.insert(source.source_ir.identity.pdg_label); + } + + if self.external_legs.len() != external_pdgs.len() { + return Err(RusticolError::integrity( + "recurrence runtime external legs do not match the execution PDG order", + )); + } + let mut public_labels = BTreeSet::new(); + let mut saw_final = false; + for (source_slot, (leg, expected_pdg)) in + self.external_legs.iter().zip(external_pdgs).enumerate() + { + if leg.source_slot as usize != source_slot + || leg.public_label == 0 + || !public_labels.insert(leg.public_label) + { + return Err(RusticolError::artifact( + "recurrence external source slots or public labels are not canonical", + )); + } + if leg.physical_pdg != *expected_pdg { + return Err(RusticolError::integrity( + "recurrence external physical PDGs disagree with the execution manifest", + )); + } + if saw_final && leg.is_initial { + return Err(RusticolError::artifact( + "recurrence initial-state legs must precede final-state legs", + )); + } + saw_final |= !leg.is_initial; + if !source_pdgs.contains(&leg.outgoing_pdg) { + return Err(RusticolError::integrity(format!( + "recurrence external outgoing PDG {} has no source template", + leg.outgoing_pdg + ))); + } + } + + let mut mass_pdgs = BTreeSet::new(); + let mut previous_mass_pdg = None; + for row in &self.particle_masses { + if previous_mass_pdg.is_some_and(|previous| previous >= row.outgoing_pdg) + || !mass_pdgs.insert(row.outgoing_pdg) + { + return Err(RusticolError::artifact( + "recurrence particle masses are not in strict outgoing-PDG order", + )); + } + previous_mass_pdg = Some(row.outgoing_pdg); + if !row.mass.is_finite() || row.mass < 0.0 { + return Err(RusticolError::artifact( + "recurrence particle masses must be finite and nonnegative", + )); + } + } + if mass_pdgs != source_pdgs { + return Err(RusticolError::integrity( + "recurrence particle masses do not cover exactly the source-template particles", + )); + } + self.normalization.validate(color_accuracy) + } +} + +/// Validate the operational parameter schema shared by the broad manifest +/// and the compact process-ready recipe. Keeping this boundary shared avoids +/// accepting a recipe that would bind runtime slots differently from the +/// generation manifest it was derived from. +pub(super) fn validate_recurrence_parameter_runtime_parts( + parameters: &[RecurrenceRuntimeParameter], + prepared_parameter_defaults: &[[f64; 2]], + parameter_projection: &[RecurrenceParameterProjection], + expected_parameter_count: u64, +) -> RusticolResult<()> { + if prepared_parameter_defaults.len() as u64 != expected_parameter_count { + return Err(RusticolError::integrity( + "recurrence prepared-parameter defaults do not match the plan parameter count", + )); + } + if prepared_parameter_defaults + .iter() + .flatten() + .any(|component| !component.is_finite()) + { + return Err(RusticolError::artifact( + "recurrence prepared-parameter defaults must be finite complex-f64 values", + )); + } + validate_parameter_projection(prepared_parameter_defaults.len(), parameter_projection)?; + validate_runtime_parameters( + parameters, + prepared_parameter_defaults, + parameter_projection, + ) +} + +fn validate_runtime_parameters( + parameters: &[RecurrenceRuntimeParameter], + prepared_parameter_defaults: &[[f64; 2]], + parameter_projection: &[RecurrenceParameterProjection], +) -> RusticolResult<()> { + if parameters.len() != parameter_projection.len() { + return Err(RusticolError::integrity( + "recurrence runtime parameters do not cover the parameter projection", + )); + } + let mut previous_runtime_name: Option<&str> = None; + let mut previous_kind: Option<&str> = None; + for (parameter_index, (parameter, projection)) in + parameters.iter().zip(parameter_projection).enumerate() + { + if parameter.parameter_index as usize != parameter_index + || parameter.parameter_index != projection.runtime_slot + { + return Err(RusticolError::artifact( + "recurrence runtime parameter indices must be dense and match projection slots", + )); + } + validate_text(¶meter.name, "recurrence runtime parameter name")?; + validate_text(¶meter.kind, "recurrence runtime parameter kind")?; + if !parameter.default.is_finite() { + return Err(RusticolError::artifact( + "recurrence runtime parameter defaults must be finite", + )); + } + + match (¶meter.runtime_name, ¶meter.complex_component) { + (Some(runtime_name), Some(component)) => { + validate_text(runtime_name, "recurrence runtime parameter public name")?; + let expected_component = match projection.component { + 0 => "real", + 1 => "imag", + _ => { + return Err(RusticolError::integrity( + "recurrence runtime parameter has an invalid projected component", + )); + } + }; + if component != expected_component + || projection.runtime_name != *runtime_name + || parameter.name != format!("{runtime_name}.{component}") + { + return Err(RusticolError::integrity( + "recurrence complex runtime parameter disagrees with its projection", + )); + } + if previous_runtime_name == Some(runtime_name.as_str()) + && previous_kind != Some(parameter.kind.as_str()) + { + return Err(RusticolError::integrity( + "recurrence complex runtime parameter components have different kinds", + )); + } + previous_runtime_name = Some(runtime_name); + previous_kind = Some(¶meter.kind); + } + (None, None) => { + if projection.component != 0 || parameter.name != projection.runtime_name { + return Err(RusticolError::integrity( + "recurrence real runtime parameter disagrees with its projection", + )); + } + previous_runtime_name = None; + previous_kind = None; + } + _ => { + return Err(RusticolError::artifact( + "recurrence runtime_name and complex_component must be both present or both absent", + )); + } + } + + if let Some(prepared_id) = projection.prepared_parameter_id { + let expected_default = + prepared_parameter_defaults[prepared_id as usize][projection.component as usize]; + if parameter.default != expected_default { + return Err(RusticolError::integrity( + "recurrence runtime parameter default disagrees with its prepared default", + )); + } + } + } + Ok(()) +} + +fn validate_parameter_projection( + parameter_count: usize, + parameter_projection: &[RecurrenceParameterProjection], +) -> RusticolResult<()> { + let mut previous_key: Option<(&str, u32)> = None; + let mut current_name: Option<&str> = None; + let mut current_template = None; + let mut current_prepared = None; + for (runtime_slot, row) in parameter_projection.iter().enumerate() { + if row.runtime_slot as usize != runtime_slot { + return Err(RusticolError::artifact( + "recurrence runtime parameter slots must be dense and ordered from zero", + )); + } + validate_text(&row.runtime_name, "recurrence runtime parameter name")?; + if row.component > 1 { + return Err(RusticolError::artifact( + "recurrence runtime parameter component must be zero or one", + )); + } + if row.parameter_template_id as usize >= parameter_count + || row + .prepared_parameter_id + .is_some_and(|id| id as usize >= parameter_count) + { + return Err(RusticolError::integrity( + "recurrence parameter projection references an absent parameter", + )); + } + let key = (row.runtime_name.as_str(), row.component); + if previous_key.is_some_and(|previous| previous >= key) { + return Err(RusticolError::artifact( + "recurrence parameter projection is not in strict name/component order", + )); + } + previous_key = Some(key); + if current_name == Some(row.runtime_name.as_str()) { + if row.component != 1 + || current_template != Some(row.parameter_template_id) + || current_prepared != Some(row.prepared_parameter_id) + { + return Err(RusticolError::integrity( + "recurrence complex parameter projection rows are inconsistent", + )); + } + } else { + if row.component != 0 { + return Err(RusticolError::artifact( + "recurrence parameter projection must begin each parameter at component zero", + )); + } + current_name = Some(row.runtime_name.as_str()); + current_template = Some(row.parameter_template_id); + current_prepared = Some(row.prepared_parameter_id); + } + } + Ok(()) +} + +impl RecurrenceColorContractionReference { + fn validate( + &self, + color_accuracy: &str, + inspection: &RecurrenceInspectionSummary, + ) -> RusticolResult<()> { + if self.abi != RECURRENCE_COLOR_CONTRACTION_CODEC_ABI + || self.path != RECURRENCE_COLOR_CONTRACTION_PATH + { + return Err(RusticolError::compatibility( + "unsupported recurrence color-contraction payload contract", + )); + } + if self.color_accuracy != color_accuracy + || !matches!(self.color_accuracy.as_str(), "nlc" | "full") + || !matches!( + self.storage.as_str(), + "expanded" | "repeated" | "convolution-kernels" + ) + { + return Err(RusticolError::integrity( + "recurrence color-contraction summary has incompatible accuracy or storage", + )); + } + if !self.includes_color_factor { + return Err(RusticolError::integrity( + "NLC/full recurrence color contraction must include the color factor", + )); + } + if let Some(factorization) = &self.factorization { + let valid_kind_and_rank = match factorization.kind.as_str() { + "klein-four-walsh" => factorization.rank == 2, + "elementary-abelian-walsh" => (3..u32::BITS).contains(&factorization.rank), + "symmetric-group-fourier" => (2..=10).contains(&factorization.rank), + _ => false, + }; + let valid_storage = match factorization.kind.as_str() { + "symmetric-group-fourier" => self.storage == "convolution-kernels", + _ => self.storage == "repeated", + }; + if !valid_storage || !valid_kind_and_rank || factorization.coset_count == 0 { + return Err(RusticolError::integrity( + "recurrence color factorization summary is inconsistent", + )); + } + } + if (self.storage == "convolution-kernels") + != self + .factorization + .as_ref() + .is_some_and(|value| value.kind == "symmetric-group-fourier") + { + return Err(RusticolError::integrity( + "recurrence convolution-kernel storage requires symmetric-group Fourier factorization", + )); + } + match (&self.factorization, &self.fft_provenance) { + (Some(factorization), Some(provenance)) + if factorization.kind == "symmetric-group-fourier" => + { + self.validate_fft_provenance(factorization, provenance)?; + } + (Some(factorization), None) if factorization.kind == "symmetric-group-fourier" => { + return Err(RusticolError::integrity( + "recurrence symmetric-group Fourier summary is missing FFT provenance", + )); + } + (_, Some(_)) => { + return Err(RusticolError::integrity( + "recurrence non-FFT color summary carries FFT provenance", + )); + } + _ => {} + } + parse_sha256(&self.sha256, "recurrence color-contraction payload")?; + parse_sha256( + &self.semantic_digest, + "recurrence color-contraction semantic payload", + )?; + let expected_destination_count = if inspection.schedule.replay_target_count != 0 { + inspection + .sector_count + .checked_mul(inspection.schedule.resolved_helicity_count) + .ok_or_else(|| { + RusticolError::artifact( + "recurrence replay color-contraction destination count overflows", + ) + })? + } else { + inspection.schedule.amplitude_destination_count + }; + if self.sha256 != self.semantic_digest + || self.size_bytes == 0 + || self.sector_count != inspection.sector_count + || self.component_count != inspection.schedule.resolved_helicity_count + || self.destination_count != expected_destination_count + || self.materialized_destination_count.is_some_and(|count| { + count != inspection.schedule.amplitude_destination_count + || count > self.destination_count + }) + || self.active_sector_count == 0 + || self.active_sector_count > self.sector_count + || self.group_count < self.active_sector_count + || self.group_count + > self + .active_sector_count + .checked_mul(self.component_count) + .ok_or_else(|| { + RusticolError::artifact( + "recurrence color-contraction active group bound overflows", + ) + })? + || self.entry_count == 0 + || self.logical_entry_count < self.entry_count + { + return Err(RusticolError::integrity( + "recurrence color-contraction summary is inconsistent with the Direct-Arena schedule", + )); + } + Ok(()) + } + + pub(super) fn validate_fft_provenance( + &self, + factorization: &RecurrenceColorFactorizationReference, + provenance: &RecurrenceColorFftProvenance, + ) -> RusticolResult<()> { + let group_order = (2..=factorization.rank) + .try_fold(1_u64, |value, factor| value.checked_mul(u64::from(factor))); + let Some(group_order) = group_order else { + return Err(RusticolError::artifact( + "recurrence symmetric-group order overflows u64", + )); + }; + let Some(covered_local_group_count) = factorization.coset_count.checked_mul(group_order) + else { + return Err(RusticolError::artifact( + "recurrence FFT-covered local group count overflows u64", + )); + }; + if self.component_count == 0 || !self.group_count.is_multiple_of(self.component_count) { + return Err(RusticolError::integrity( + "recurrence symmetric-group component map is not rectangular", + )); + } + let local_group_count = self.group_count / self.component_count; + let Some(residual_group_count) = local_group_count.checked_sub(covered_local_group_count) + else { + return Err(RusticolError::integrity( + "recurrence FFT-covered groups exceed the local color domain", + )); + }; + let upper_triangle_count = |count: u64| { + count + .checked_add(1) + .and_then(|successor| count.checked_mul(successor)) + .and_then(|value| value.checked_div(2)) + }; + let Some(total_pair_count) = upper_triangle_count(local_group_count) else { + return Err(RusticolError::artifact( + "recurrence local color pair count overflows u64", + )); + }; + let Some(covered_pair_count) = upper_triangle_count(covered_local_group_count) else { + return Err(RusticolError::artifact( + "recurrence FFT-covered color pair count overflows u64", + )); + }; + let residual_entry_count = total_pair_count - covered_pair_count; + let Some(channel_pair_count) = upper_triangle_count(factorization.coset_count) else { + return Err(RusticolError::artifact( + "recurrence FFT channel-pair count overflows u64", + )); + }; + let Some(kernel_entry_count) = channel_pair_count.checked_mul(group_order) else { + return Err(RusticolError::artifact( + "recurrence FFT kernel entry count overflows u64", + )); + }; + let Some(raw_kernel_bytes) = kernel_entry_count.checked_mul(16) else { + return Err(RusticolError::artifact( + "recurrence FFT kernel storage exceeds u64", + )); + }; + let Some(transformed_kernel_bytes) = kernel_entry_count.checked_mul(8) else { + return Err(RusticolError::artifact( + "recurrence transformed FFT kernel storage exceeds u64", + )); + }; + let expected_entry_count = kernel_entry_count + .checked_add(residual_entry_count) + .ok_or_else(|| RusticolError::artifact("recurrence FFT entry count overflows u64"))?; + let expected_logical_entry_count = expected_entry_count + .checked_mul(self.component_count) + .ok_or_else(|| { + RusticolError::artifact("recurrence FFT logical entry count overflows u64") + })?; + if provenance.method != "symmetric-group-fourier" + || provenance.degree != factorization.rank + || provenance.channel_count != factorization.coset_count + || provenance.covered_local_group_count != covered_local_group_count + || provenance.residual_group_count != residual_group_count + || provenance.residual_entry_count != residual_entry_count + || provenance.raw_kernel_bytes != raw_kernel_bytes + || provenance.transformed_kernel_bytes != transformed_kernel_bytes + || provenance.capability != SYMMETRIC_GROUP_FFT_COLOR_RUNTIME_CAPABILITY + || self.entry_count != expected_entry_count + || self.logical_entry_count != expected_logical_entry_count + { + return Err(RusticolError::integrity( + "recurrence symmetric-group FFT provenance is inconsistent", + )); + } + Ok(()) + } +} + +impl RecurrenceSourceTemplate { + fn validate(&self, index: usize) -> RusticolResult<()> { + if self.dimension == 0 || u32::try_from(self.dimension).is_err() { + return Err(RusticolError::artifact(format!( + "recurrence source template {index} has an unsupported dimension" + ))); + } + self.source_ir.validate(index)?; + self.crossing.validate(index, "source-template")?; + if self.dimension != self.source_ir.component_dimension + || self.crossing != self.source_ir.crossing + { + return Err(RusticolError::integrity(format!( + "recurrence source template {index} disagrees with its typed SourceIR" + ))); + } + let declared = self.source_ir.states.iter().any(|state| { + state.helicity == self.helicity + && state.chirality == self.chirality + && state.spin_state == RecurrenceSourceSpinState::Scalar(self.spin_state) + }); + if !declared { + return Err(RusticolError::artifact(format!( + "recurrence source template {index} state is not declared by its typed SourceIR" + ))); + } + Ok(()) + } +} + +impl RecurrenceGenericSourceIr { + fn validate(&self, index: usize) -> RusticolResult<()> { + let identity = &self.identity; + for (label, value) in [ + ("canonical id", identity.canonical_id.as_str()), + ("species id", identity.species_id.as_str()), + ("anti-canonical id", identity.anti_canonical_id.as_str()), + ("display name", identity.display_name.as_str()), + ("anti-display name", identity.anti_display_name.as_str()), + ] { + validate_text(value, &format!("recurrence source {index} {label}"))?; + } + if identity.anti_pdg_label == 0 { + return Err(RusticolError::artifact(format!( + "recurrence source {index} has an invalid antiparticle relation" + ))); + } + let canonical_self_conjugate = identity.canonical_id == identity.anti_canonical_id; + let pdg_self_conjugate = identity.pdg_label == identity.anti_pdg_label; + if identity.self_conjugate != canonical_self_conjugate + || identity.self_conjugate != pdg_self_conjugate + || (identity.orientation == RecurrenceSourceOrientation::SelfConjugate) + != identity.self_conjugate + { + return Err(RusticolError::artifact(format!( + "recurrence source {index} orientation is inconsistent with its antiparticle relation" + ))); + } + validate_text(&self.basis, &format!("recurrence source {index} basis"))?; + for (label, value) in [ + ("mass parameter", self.mass_parameter.as_deref()), + ("width parameter", self.width_parameter.as_deref()), + ] { + if let Some(value) = value { + validate_text(value, &format!("recurrence source {index} {label}"))?; + } + } + if self.component_dimension == 0 || u32::try_from(self.component_dimension).is_err() { + return Err(RusticolError::artifact(format!( + "recurrence source {index} has an unsupported component dimension" + ))); + } + bounded_len("recurrence SourceIR states", self.states.len())?; + if self.states.is_empty() { + return Err(RusticolError::artifact(format!( + "recurrence source {index} has no declared SourceIR states" + ))); + } + self.crossing.validate(index, "declared")?; + + let expected_statistics = match self.wavefunction_family { + RecurrenceWavefunctionFamily::Fermion => RecurrenceParticleStatistics::Fermion, + RecurrenceWavefunctionFamily::Ghost => RecurrenceParticleStatistics::Ghost, + RecurrenceWavefunctionFamily::Auxiliary => RecurrenceParticleStatistics::Auxiliary, + RecurrenceWavefunctionFamily::Scalar + | RecurrenceWavefunctionFamily::Vector + | RecurrenceWavefunctionFamily::Spin2 => RecurrenceParticleStatistics::Boson, + }; + if self.statistics != expected_statistics { + return Err(RusticolError::artifact(format!( + "recurrence source {index} statistics disagree with its wavefunction family" + ))); + } + match (self.wavefunction_family, self.component_dimension) { + (RecurrenceWavefunctionFamily::Fermion, 2 | 4) => { + if identity.self_conjugate { + return Err(RusticolError::artifact(format!( + "recurrence source {index} is an unsupported self-conjugate fermion source" + ))); + } + } + (RecurrenceWavefunctionFamily::Scalar, 1) + | (RecurrenceWavefunctionFamily::Vector, 4) + | (RecurrenceWavefunctionFamily::Spin2, 16) => {} + _ => { + return Err(RusticolError::artifact(format!( + "recurrence source {index} has an unsupported wavefunction family/dimension pair" + ))); + } + } + + let max_helicity = if self.wavefunction_family == RecurrenceWavefunctionFamily::Spin2 { + 2 + } else { + 1 + }; + let mut states = BTreeSet::new(); + for (state_index, state) in self.states.iter().enumerate() { + if state.chirality.unsigned_abs() > 1 || state.helicity.unsigned_abs() > max_helicity { + return Err(RusticolError::artifact(format!( + "recurrence source {index} SourceIR state {state_index} is outside the supported helicity/chirality range" + ))); + } + if let RecurrenceSourceSpinState::Components(components) = &state.spin_state { + bounded_len("structured source spin state", components.len())?; + if components.is_empty() || components.len() as u64 > self.component_dimension { + return Err(RusticolError::artifact(format!( + "recurrence source {index} SourceIR state {state_index} has invalid structured spin metadata" + ))); + } + } + if !states.insert(state) { + return Err(RusticolError::artifact(format!( + "recurrence source {index} contains duplicate SourceIR states" + ))); + } + } + Ok(()) + } +} + +impl RecurrenceGenericCrossingIr { + fn validate(&self, index: usize, label: &str) -> RusticolResult<()> { + if ![-1, 1].contains(&self.helicity_factor) + || ![-1, 1].contains(&self.chirality_factor) + || ![-1, 1].contains(&self.spin_state_factor) + || !self.phase.iter().all(|component| component.is_finite()) + || self.phase == [0.0, 0.0] + { + return Err(RusticolError::artifact(format!( + "recurrence source {index} has invalid {label} CrossingIR metadata" + ))); + } + Ok(()) + } +} + +impl RecurrenceNormalization { + fn validate(&self, color_accuracy: &str) -> RusticolResult<()> { + if self.color_accuracy != color_accuracy { + return Err(RusticolError::integrity( + "recurrence normalization color accuracy disagrees with the process", + )); + } + if [ + self.color_factor, + self.average_factor, + self.identical_factor, + self.global_coupling_factor, + ] + .iter() + .any(|value| !value.is_finite()) + { + return Err(RusticolError::artifact( + "recurrence normalization factors must be finite", + )); + } + if self.average_factor <= 0.0 || self.identical_factor <= 0.0 { + return Err(RusticolError::artifact( + "recurrence averaging and identical-particle factors must be positive", + )); + } + if self.qcd_coupling_power.is_some() != self.electroweak_coupling_power.is_some() { + return Err(RusticolError::artifact( + "recurrence normalization coupling powers must be both present or both absent", + )); + } + validate_counts( + "recurrence normalization", + &[ + self.qcd_coupling_power.unwrap_or(0), + self.electroweak_coupling_power.unwrap_or(0), + ], + )?; + validate_text( + &self.coupling_policy, + "recurrence normalization coupling policy", + )?; + Ok(()) + } +} + +/// Parse and validate one bounded recurrence `execution.json`. +pub(super) fn parse_recurrence_execution_manifest( + bytes: &[u8], + path: &str, + outer: &ArtifactProcess, +) -> RusticolResult { + let manifest: RecurrenceExecutionManifest = serde_json::from_slice(bytes).map_err(|error| { + RusticolError::serialization(format!( + "could not parse bounded recurrence execution manifest {path:?}: {error}" + )) + })?; + manifest.validate(outer)?; + Ok(manifest) +} + +fn validate_capabilities( + capabilities: &[String], + color_accuracy: &str, + uses_symmetric_group_fft: bool, + has_helicity_selector_companion: bool, + context: &str, +) -> RusticolResult<()> { + let color_capability = match color_accuracy { + "lc" => RECURRENCE_LC_COLOR_CAPABILITY, + "nlc" | "full" => RECURRENCE_CONTRACTED_COLOR_CAPABILITY, + other => { + return Err(RusticolError::compatibility(format!( + "{context} has unsupported recurrence color accuracy {other:?}" + ))); + } + }; + let mut expected = Vec::from([color_capability, RECURRENCE_RUNTIME_CAPABILITY]); + if uses_symmetric_group_fft { + expected.push(SYMMETRIC_GROUP_FFT_COLOR_RUNTIME_CAPABILITY); + } + if has_helicity_selector_companion { + expected.push(RECURRENCE_HELICITY_SELECTOR_COMPANION_RUNTIME_CAPABILITY); + } + expected.sort_unstable(); + if capabilities.iter().map(String::as_str).collect::>() != expected { + return Err(RusticolError::compatibility(format!( + "{context} must require exactly {expected:?}" + ))); + } + Ok(()) +} + +fn validate_direct_contract( + builder_input_abi: &str, + recurrence_plan_abi: &str, + runtime_layout_abi: &str, + direct_template_abi: &str, + direct_backend_abi: &str, + context: &str, +) -> RusticolResult<()> { + for (label, actual, expected) in [ + ( + "builder input", + builder_input_abi, + RECURRENCE_BUILDER_INPUT_ABI, + ), + ("plan", recurrence_plan_abi, RECURRENCE_PLAN_ABI), + ( + "runtime layout", + runtime_layout_abi, + RECURRENCE_RUNTIME_LAYOUT_ABI, + ), + ( + "direct template", + direct_template_abi, + RECURRENCE_DIRECT_TEMPLATE_ABI, + ), + ( + "direct backend", + direct_backend_abi, + RECURRENCE_DIRECT_BACKEND_ABI, + ), + ] { + if actual != expected { + return Err(RusticolError::compatibility(format!( + "{context} has unsupported Direct-Arena v2 {label} ABI {actual:?}; regenerate the recurrence artifact" + ))); + } + } + Ok(()) +} + +fn bounded_len(context: &str, len: usize) -> RusticolResult<()> { + if u32::try_from(len).is_err() { + return Err(RusticolError::artifact(format!( + "{context} count exceeds the u32 identifier domain" + ))); + } + Ok(()) +} + +fn validate_counts(context: &str, counts: &[u64]) -> RusticolResult<()> { + if counts.iter().any(|count| usize::try_from(*count).is_err()) { + return Err(RusticolError::artifact(format!( + "{context} count exceeds the platform index domain" + ))); + } + Ok(()) +} + +fn validate_text(value: &str, context: &str) -> RusticolResult<()> { + if value.is_empty() { + return Err(RusticolError::artifact(format!("{context} is empty"))); + } + Ok(()) +} + +fn unsupported_abi(context: &str, value: &str) -> RusticolError { + RusticolError::compatibility(format!("unsupported {context} ABI {value:?}")) +} + +fn parse_sha256(value: &str, context: &str) -> RusticolResult<[u8; 32]> { + if value.len() != 64 + || value + .as_bytes() + .iter() + .any(|byte| !matches!(byte, b'0'..=b'9' | b'a'..=b'f')) + { + return Err(RusticolError::artifact(format!( + "{context} SHA-256 must be 64 lowercase hexadecimal characters" + ))); + } + let mut digest = [0_u8; 32]; + for (index, pair) in value.as_bytes().chunks_exact(2).enumerate() { + digest[index] = (hex_nibble(pair[0]) << 4) | hex_nibble(pair[1]); + } + Ok(digest) +} + +fn hex_nibble(value: u8) -> u8 { + match value { + b'0'..=b'9' => value - b'0', + b'a'..=b'f' => value - b'a' + 10, + _ => unreachable!("digest validation precedes hexadecimal decoding"), + } +} + +fn deserialize_required_nullable_string<'de, D>(deserializer: D) -> Result, D::Error> +where + D: serde::Deserializer<'de>, +{ + match Value::deserialize(deserializer)? { + Value::Null => Ok(None), + Value::String(value) => Ok(Some(value)), + _ => Err(::custom( + "expected a string or null", + )), + } +} + +fn deserialize_required_nullable_u32<'de, D>(deserializer: D) -> Result, D::Error> +where + D: serde::Deserializer<'de>, +{ + Option::::deserialize(deserializer) +} + +fn deserialize_recurrence_phase<'de, D>(deserializer: D) -> Result<[f64; 2], D::Error> +where + D: serde::Deserializer<'de>, +{ + let value = Value::deserialize(deserializer)?; + if let Ok(pair) = serde_json::from_value::<[f64; 2]>(value.clone()) { + return Ok(pair); + } + let object = value.as_object().ok_or_else(|| { + serde::de::Error::custom("recurrence crossing phase must be a complex-f64 pair") + })?; + let component = |prefix: &str| -> Result { + let numerator = object + .get(&format!("{prefix}_numerator")) + .and_then(Value::as_str) + .ok_or_else(|| serde::de::Error::custom("invalid exact recurrence phase"))? + .parse::() + .map_err(|_| serde::de::Error::custom("invalid exact recurrence phase numerator"))?; + let denominator = object + .get(&format!("{prefix}_denominator")) + .and_then(Value::as_str) + .ok_or_else(|| serde::de::Error::custom("invalid exact recurrence phase"))? + .parse::() + .map_err(|_| serde::de::Error::custom("invalid exact recurrence phase denominator"))?; + let value = numerator / denominator; + if denominator == 0.0 || !value.is_finite() { + return Err(serde::de::Error::custom( + "exact recurrence phase is not finite", + )); + } + Ok(value) + }; + Ok([component("real")?, component("imag")?]) +} + +fn deserialize_optional_u64_if_present<'de, D>(deserializer: D) -> Result, D::Error> +where + D: serde::Deserializer<'de>, +{ + u64::deserialize(deserializer).map(Some) +} + +#[cfg(test)] +pub(super) mod tests { + use super::*; + use serde_json::{Value, json}; + + fn capabilities() -> Value { + json!([ + RECURRENCE_LC_COLOR_CAPABILITY, + RECURRENCE_RUNTIME_CAPABILITY + ]) + } + + fn crossing() -> Value { + json!({ + "momentum_transform": "identity", + "helicity_factor": 1, + "chirality_factor": 1, + "spin_state_factor": 1, + "phase": [1.0, 0.0] + }) + } + + fn source_ir() -> Value { + json!({ + "identity": { + "canonical_id": "model:test:state:x", + "species_id": "model:test:species:x", + "anti_canonical_id": "model:test:state:x", + "display_name": "x", + "anti_display_name": "x", + "pdg_label": 25, + "anti_pdg_label": 25, + "orientation": "self-conjugate", + "self_conjugate": true + }, + "statistics": "boson", + "wavefunction_family": "scalar", + "component_dimension": 1, + "states": [{"helicity": 0, "chirality": 0, "spin_state": 0}], + "crossing": crossing(), + "basis": "scalar", + "mass_parameter": null, + "width_parameter": null + }) + } + + fn runtime_metadata() -> Value { + json!({ + "public_color_flows": [{ + "public_id": "flow:1", + "construction_sector_id": 0, + "target_sector_id": 0 + }], + "runtime_parameters": [{ + "name": "mass.x", + "kind": "external_parameter", + "parameter_index": 0, + "default": 1.0, + "runtime_name": null, + "complex_component": null + }], + "prepared_parameter_defaults": [[1.0, 0.0]], + "parameter_projection": [{ + "runtime_slot": 0, + "runtime_name": "mass.x", + "parameter_template_id": 0, + "prepared_parameter_id": 0, + "component": 0 + }], + "source_templates": [{ + "source_template_id": 0, + "current_state_template_id": 0, + "dimension": 1, + "helicity": 0, + "chirality": 0, + "spin_state": 0, + "source_ir": source_ir(), + "crossing": crossing() + }], + "external_legs": [{ + "source_slot": 0, + "public_label": 1, + "physical_pdg": 25, + "outgoing_pdg": 25, + "is_initial": false + }], + "particle_masses": [{"outgoing_pdg": 25, "mass": 0.0}], + "color_contraction": null, + "normalization": { + "color_accuracy": "lc", + "color_factor": 1.0, + "average_factor": 1.0, + "identical_factor": 1.0, + "global_coupling_factor": 1.0, + "qcd_coupling_power": 0, + "electroweak_coupling_power": 0, + "couplings_in_stage_evaluators": true, + "coupling_policy": "test" + } + }) + } + + fn inspection_summary() -> Value { + json!({ + "execution_mode": "recurrence", + "recurrence_plan_abi": RECURRENCE_PLAN_ABI, + "runtime_layout_abi": RECURRENCE_RUNTIME_LAYOUT_ABI, + "direct_template_abi": RECURRENCE_DIRECT_TEMPLATE_ABI, + "process_id": "x_to_x", + "lc_flow_layout": "topology-replay", + "parameter_count": 1, + "sector_count": 1, + "prepared_kernel_count": 1, + "direct_executor_count": 4, + "semantic_digest": "11".repeat(32), + "schedule_digest": "aa".repeat(32), + "runtime_layout_digest": "77".repeat(32), + "schedule": { + "current_count": 1, + "source_row_count": 1, + "contribution_count": 0, + "finalization_count": 1, + "amplitude_destination_count": 1, + "closure_term_count": 0, + "replay_target_count": 1, + "retained_helicity_count": 1, + "resolved_helicity_count": 1, + "exact_factor_count": 1 + }, + "construction": { + "peak_current_count": 1, + "peak_contribution_count": 0, + "peak_dynamic_color_state_count": 1, + "color_target_prune_count": 0, + "candidate_parent_pair_count": 0, + "peak_to_final_current_ratio": 1.0, + "peak_to_final_contribution_ratio": 0.0 + }, + "direct_arena": { + "semantic_component_count": 1, + "current_arena_components": 1, + "arena_component_reuse_count": 0, + "momentum_form_count": 1, + "selector_domain_count": 1, + "row_group_count": 4, + "packed_input_bytes": 0, + "packed_output_bytes": 0, + "scatter_bytes": 0 + }, + "runtime_container_member": { + "path": RECURRENCE_DIRECT_SCHEDULE_MEMBER, + "size_bytes": 512, + "sha256": "88".repeat(32), + "container_size_bytes": 4096 + }, + "generation_timings_seconds": { + "python_extraction": 0.1, + "catalog_authentication": 0.1, + "semantic_construction": 0.1, + "direct_lowering": 0.1, + "serialization": 0.1, + "native_total": 0.5 + } + }) + } + + pub(crate) fn manifest() -> Value { + let plan = json!({ + "kind": RECURRENCE_RUNTIME_KIND, + "builder_input_abi": RECURRENCE_BUILDER_INPUT_ABI, + "recurrence_plan_abi": RECURRENCE_PLAN_ABI, + "runtime_layout_abi": RECURRENCE_RUNTIME_LAYOUT_ABI, + "direct_template_abi": RECURRENCE_DIRECT_TEMPLATE_ABI, + "direct_backend_abi": RECURRENCE_DIRECT_BACKEND_ABI, + "builder_input_sha256": "11".repeat(32), + "prepared_kernel_pack_digest": "44".repeat(32), + "direct_template_catalog_digest": "55".repeat(32), + "required_runtime_capabilities": capabilities(), + "runtime_schedule": { + "kind": RECURRENCE_RUNTIME_CONTAINER_KIND, + "schema_version": RECURRENCE_RUNTIME_CONTAINER_SCHEMA, + "storage_abi": RECURRENCE_RUNTIME_STORAGE_ABI, + "path": format!( + "recurrence/schedules/{}/recurrence-runtime.pacbin", + "aa".repeat(32) + ), + "plan_member_path": RECURRENCE_DIRECT_SCHEDULE_MEMBER, + "size_bytes": 4096, + "sha256": "22".repeat(32), + "member_count": 1, + "unpacked_size_bytes": 512, + "index_sha256": "33".repeat(32) + }, + "process_binding": { + "abi": RECURRENCE_PROCESS_BINDING_ABI, + "process_id": "x_to_x", + "schedule_digest": "aa".repeat(32), + "native_schedule_semantic_digest": "ab".repeat(32), + "process_digest": "12".repeat(32), + "process_semantic_digest": "11".repeat(32), + "process_support_words": [1], + "remap": { + "bijection_digest": "66".repeat(32), + "source_slots": [0], + "source_momentum_signs": [1], + "source_helicity_signs": [1], + "source_state_offsets": [0, 1], + "source_state_indices": [0], + "public_flow_ids": [0], + "physical_sector_ids": [0], + "state_templates": {"count": 1, "changes": []}, + "source_templates": {"count": 1, "changes": []}, + "direct_executors": {"count": 4, "changes": []}, + "parameter_slots": {"count": 1, "changes": []} + }, + "path": RECURRENCE_PROCESS_BINDING_PATH, + "size_bytes": 256, + "sha256": "99".repeat(32) + }, + "inspection_summary": inspection_summary() + }); + json!({ + "schema_version": PROCESS_ARTIFACT_SCHEMA_VERSION, + "kind": RECURRENCE_RUNTIME_KIND, + "required_runtime_capabilities": capabilities(), + "process": "x > x", + "key": "x_to_x", + "color_accuracy": "lc", + "external_pdg_order": [25], + "process_digest": null, + "builder_input_abi": RECURRENCE_BUILDER_INPUT_ABI, + "recurrence_plan_abi": RECURRENCE_PLAN_ABI, + "runtime_layout_abi": RECURRENCE_RUNTIME_LAYOUT_ABI, + "direct_template_abi": RECURRENCE_DIRECT_TEMPLATE_ABI, + "direct_backend_abi": RECURRENCE_DIRECT_BACKEND_ABI, + "compiled_model_digest": "bb".repeat(32), + "recurrence_template_catalog_digest": "cc".repeat(32), + "prepared_kernel_pack_digest": "44".repeat(32), + "direct_template_catalog_digest": "55".repeat(32), + "kernel_pack": { + "manifest_path": RECURRENCE_KERNEL_PACK_MANIFEST_PATH, + "payload_root": RECURRENCE_KERNEL_PAYLOAD_ROOT + }, + "runtime_options": {"point_tile_size": 8, "workspace_mib": 64}, + "runtime_metadata": runtime_metadata(), + "helicity_selector_companion": null, + "plan": plan, + "recurrence_summary": { + "lc_flow_layout": "topology-replay", + "builder_input_abi": RECURRENCE_BUILDER_INPUT_ABI, + "template_input_abi": RECURRENCE_TEMPLATE_INPUT_ABI, + "current_count": 1, + "contribution_count": 0, + "closure_term_count": 0 + } + }) + } + + pub(crate) fn outer() -> ArtifactProcess { + ArtifactProcess { + id: "x_to_x".to_owned(), + expression: "x > x".to_owned(), + color_accuracy: "lc".to_owned(), + external_pdgs: vec![25], + physics_path: "processes/x_to_x/physics.json".to_owned(), + required_runtime_capabilities: vec![ + RECURRENCE_LC_COLOR_CAPABILITY.to_owned(), + RECURRENCE_RUNTIME_CAPABILITY.to_owned(), + ], + aliases: Vec::new(), + } + } + + fn parse(value: &Value) -> RusticolResult { + parse_recurrence_execution_manifest( + &serde_json::to_vec(value).unwrap(), + "processes/x_to_x/execution.json", + &outer(), + ) + } + + fn add_python_selector_work_certificate(value: &mut Value) { + value["plan"]["inspection_summary"]["selector_work_certificate"] = json!({ + "schema_version": 1, + "binding": "runtime_layout_digest", + "persisted_union": { + "current_count": 1, + "semantic_component_count": 1, + "source_row_count": 1, + "contribution_count": 0, + "finalization_count": 1, + "closure_count": 0, + "row_count": 2 + }, + "representatives": [{ + "representative_sector_id": 0, + "public_flow_count": 1, + "current_count": 1, + "semantic_component_count": 1, + "source_row_count": 1, + "contribution_count": 0, + "finalization_count": 1, + "closure_count": 0, + "amplitude_destination_count": 1, + "row_count": 2 + }] + }); + value["plan"]["inspection_summary"]["direct_arena"]["cache_footprint_policy"] = + json!("selector-active-max-v1"); + } + + fn add_python_color_projection_certificate(value: &mut Value) { + value["plan"]["inspection_summary"]["color_projection_certificate"] = json!({ + "path": RECURRENCE_COLOR_PROJECTION_CERTIFICATE_MEMBER, + "schema_version": 1, + "proof_kind": "exact-rectangular-sum-projection", + "publishable": true, + "size_bytes": 128, + "sha256": "cc".repeat(32) + }); + value["plan"]["runtime_schedule"]["member_count"] = json!(2); + value["plan"]["runtime_schedule"]["unpacked_size_bytes"] = json!(640); + } + + fn add_python_relation_discovery(value: &mut Value, requested_mode: &str) { + let retained_payload = json!({ + "abi": "pyamplicol-rejected-relation-diagnostics-v1", + "rows": [], + }); + let retained_sha256 = format!( + "{:x}", + Sha256::digest(serde_json::to_vec(&retained_payload).unwrap()) + ); + let rejection_decision_sha256 = "aa".repeat(32); + let full_census = json!({ + "abi": "pyamplicol-relation-discovery-full-census-v1", + "tested_hypothesis_count": 0, + "numerical_candidate_count": 0, + "verification_rejected_count": 0, + "uncertified_candidate_count": 0, + "certified_relation_count": 0, + "rejected_hypothesis_count": 0, + "applied_relation_count": 0, + "runtime_parameter_schema_sha256": null, + "candidate_observation_batch_sha256": null, + "verification_observation_batch_sha256": null, + "decision_sha256": null, + "rejection_decision_sha256": rejection_decision_sha256, + }); + let full_census_sha256 = format!( + "{:x}", + Sha256::digest(serde_json::to_vec(&full_census).unwrap()) + ); + value["plan"]["inspection_summary"]["relation_discovery"] = json!({ + "schema_version": 1, + "requested_mode": requested_mode, + "state": "diagnostic-only", + "scope": { + "execution_mode": "recurrence", + "color_accuracy": "lc", + "representation": "recurrence-direct-plan-v2", + "lc_flow_layout": "topology-replay" + }, + "probe": { + "status": "completed", + "precision_digits": 96, + "probe_count": 4, + "verification_probe_count": 4, + "effective_projection_count": 7, + "seed": 17, + "deterministic": true, + "candidate_only": true, + "tested_hypothesis_count": 0, + "verification_rejected_count": 0, + "runtime_parameter_schema_sha256": null, + "candidate_observation_batch_sha256": null, + "verification_observation_batch_sha256": null, + "decision_sha256": null, + "rejection_decision_sha256": rejection_decision_sha256 + }, + "certificate_replay": { + "algorithm": relation_certificate_algorithm(), + "status": "no-certified-relations", + "certificate_set_sha256": "dd".repeat(32) + }, + "numerical_candidate_count": 0, + "uncertified_candidate_count": 0, + "rejected_hypothesis_count": 0, + "exact_certified_relation_count": 0, + "applied_relation_count": 0, + "interaction_evaluation_count_before": 0, + "interaction_evaluation_count_after": 0, + "interaction_evaluation_savings": 0, + "current_count_before": 1, + "current_count_after": 1, + "contribution_count_before": 0, + "contribution_count_after": 0, + "scale_copy_row_count": 0, + "certificates": [], + "certificate_count": 0, + "certificates_truncated": false, + "rejected_candidates": [], + "rejected_candidate_diagnostics": { + "total_rejected_hypothesis_count": 0, + "retained_count": 0, + "truncated": false, + "truncation_policy": "first-16-in-canonical-discovery-order-v1", + "retained_sha256": retained_sha256, + "full_census_sha256": full_census_sha256, + "full_rejection_sha256": "aa".repeat(32) + }, + "follow_up_boundary": "exact relation discovery is opt-in" + }); + } + + fn hzz_outer(color_accuracy: &str) -> ArtifactProcess { + ArtifactProcess { + id: "d_dbar_to_z_z_z".to_owned(), + expression: "d d~ > z z z".to_owned(), + color_accuracy: color_accuracy.to_owned(), + external_pdgs: vec![1, -1, 23, 23, 23], + physics_path: "processes/d_dbar_to_z_z_z/physics.json".to_owned(), + required_runtime_capabilities: vec![ + if color_accuracy == "lc" { + RECURRENCE_LC_COLOR_CAPABILITY + } else { + RECURRENCE_CONTRACTED_COLOR_CAPABILITY + } + .to_owned(), + RECURRENCE_RUNTIME_CAPABILITY.to_owned(), + ], + aliases: Vec::new(), + } + } + + fn hzz_companion_outer(color_accuracy: &str) -> ArtifactProcess { + let mut outer = hzz_outer(color_accuracy); + outer + .required_runtime_capabilities + .push(RECURRENCE_HELICITY_SELECTOR_COMPANION_RUNTIME_CAPABILITY.to_owned()); + outer.required_runtime_capabilities.sort_unstable(); + outer + } + + fn parse_hzz_fixture( + bytes: &[u8], + color_accuracy: &str, + ) -> RusticolResult { + parse_recurrence_execution_manifest( + bytes, + "processes/d_dbar_to_z_z_z/execution.json", + &hzz_outer(color_accuracy), + ) + } + + fn parse_hzz_companion_fixture( + bytes: &[u8], + color_accuracy: &str, + ) -> RusticolResult { + parse_recurrence_execution_manifest( + bytes, + "processes/d_dbar_to_z_z_z/execution.json", + &hzz_companion_outer(color_accuracy), + ) + } + + fn legacy_full_hzz_fixture_value() -> Value { + let mut value: Value = serde_json::from_slice(include_bytes!( + "../../tests/fixtures/recurrence_execution_hzz_full_v2.json" + )) + .unwrap(); + upgrade_hzz_fixture_manifest(&mut value); + value + } + + fn full_hzz_fixture_value() -> Value { + let mut value = legacy_full_hzz_fixture_value(); + add_hzz_contracted_selector_representative(&mut value); + value + } + + fn add_hzz_contracted_selector_representative(value: &mut Value) { + value["plan"]["inspection_summary"]["selector_work_certificate"]["representatives"] = json!([{ + "representative_sector_id": 0, + "public_flow_count": 1, + "current_count": 193, + "semantic_component_count": 431, + "source_row_count": 13, + "contribution_count": 450, + "finalization_count": 126, + "closure_count": 54, + "amplitude_destination_count": 54, + "row_count": 643 + }]); + } + + fn add_hzz_helicity_selector_companion(value: &mut Value) { + let process_digest = "10".repeat(32); + let builder_input_digest = "20".repeat(32); + let base_schedule_digest = "21".repeat(32); + let runtime_layout_digest = "22".repeat(32); + let dispatch_sha256 = "23".repeat(32); + let composite_schedule_digest = + recurrence_helicity_selector_schedule_digest(&base_schedule_digest, &dispatch_sha256) + .unwrap(); + let resolved_helicity_count = 108_u64; + + for pointer in [ + "/required_runtime_capabilities", + "/plan/required_runtime_capabilities", + ] { + let capabilities = value.pointer_mut(pointer).unwrap(); + capabilities.as_array_mut().unwrap().push(json!( + RECURRENCE_HELICITY_SELECTOR_COMPANION_RUNTIME_CAPABILITY + )); + capabilities + .as_array_mut() + .unwrap() + .sort_by(|left, right| left.as_str().unwrap().cmp(right.as_str().unwrap())); + } + + value["process_digest"] = json!(process_digest.clone()); + value["plan"]["process_binding"]["process_digest"] = json!(process_digest.clone()); + let mut plan = value["plan"].clone(); + plan["builder_input_sha256"] = json!(builder_input_digest.clone()); + plan["process_binding"]["schedule_digest"] = json!(composite_schedule_digest.clone()); + plan["process_binding"]["native_schedule_semantic_digest"] = + json!(base_schedule_digest.clone()); + plan["process_binding"]["process_digest"] = json!(process_digest.clone()); + plan["process_binding"]["process_semantic_digest"] = json!(builder_input_digest.clone()); + plan["process_binding"]["path"] = json!(RECURRENCE_HELICITY_SELECTOR_BINDING_PATH); + plan["process_binding"]["sha256"] = json!("24".repeat(32)); + plan["runtime_schedule"]["path"] = json!(format!( + "recurrence/schedules/{composite_schedule_digest}/recurrence-runtime.pacbin" + )); + plan["runtime_schedule"]["sha256"] = json!("25".repeat(32)); + plan["inspection_summary"]["lc_flow_layout"] = json!("all-flow-union"); + plan["inspection_summary"]["semantic_digest"] = json!(builder_input_digest); + plan["inspection_summary"]["schedule_digest"] = json!(base_schedule_digest); + plan["inspection_summary"]["runtime_layout_digest"] = json!(runtime_layout_digest.clone()); + plan["inspection_summary"]["schedule"]["amplitude_destination_count"] = json!(1); + plan["inspection_summary"]["schedule"]["replay_target_count"] = json!(0); + plan["inspection_summary"]["schedule"]["retained_helicity_count"] = + json!(resolved_helicity_count); + plan["inspection_summary"]["schedule"]["resolved_helicity_count"] = + json!(resolved_helicity_count); + plan["inspection_summary"] + .as_object_mut() + .unwrap() + .remove("structural_zero_physical_sector_certificate"); + plan["inspection_summary"]["selector_work_certificate"]["representatives"] = json!([]); + plan["helicity_dispatch"] = json!({ + "abi": RECURRENCE_HELICITY_DISPATCH_ABI, + "path": format!( + "recurrence/schedules/{composite_schedule_digest}/{RECURRENCE_HELICITY_DISPATCH_PATH}" + ), + "size_bytes": 96, + "sha256": dispatch_sha256, + "base_runtime_layout_digest": runtime_layout_digest, + "resolved_helicity_count": resolved_helicity_count, + }); + value["helicity_selector_companion"] = json!({ + "schema_version": 2, + "kind": RECURRENCE_HELICITY_SELECTOR_COMPANION_KIND, + "process_digest": process_digest, + "plan": plan, + "color_contraction": { + "source": "primary", + "view": RECURRENCE_PRIMARY_LOCAL_COLOR_VIEW, + }, + }); + } + + fn legacy_nlc_hzz_fixture_value() -> Value { + let mut value: Value = serde_json::from_slice(include_bytes!( + "../../tests/fixtures/recurrence_execution_hzz_nlc.json" + )) + .unwrap(); + upgrade_hzz_fixture_manifest(&mut value); + value + } + + fn nlc_hzz_fixture_value() -> Value { + let mut value = legacy_nlc_hzz_fixture_value(); + add_hzz_contracted_selector_representative(&mut value); + value + } + + fn lc_hzz_fixture_value() -> Value { + let mut value: Value = serde_json::from_slice(include_bytes!( + "../../tests/fixtures/recurrence_execution_hzz_lc.json" + )) + .unwrap(); + upgrade_hzz_fixture_manifest(&mut value); + value + } + + fn upgrade_hzz_fixture_manifest(value: &mut Value) { + value["compiled_model_digest"] = json!("bb".repeat(32)); + value["recurrence_template_catalog_digest"] = json!("cc".repeat(32)); + value["plan"]["process_binding"]["abi"] = json!(RECURRENCE_PROCESS_BINDING_ABI); + value["plan"]["process_binding"]["native_schedule_semantic_digest"] = + value["plan"]["process_binding"]["schedule_digest"].clone(); + value["plan"]["process_binding"]["process_digest"] = json!("12".repeat(32)); + if value["plan"]["inspection_summary"]["lc_flow_layout"] == json!("contracted-color-union") + { + let sector_count = value["plan"]["inspection_summary"]["sector_count"].clone(); + let resolved_helicity_count = + value["plan"]["inspection_summary"]["schedule"]["resolved_helicity_count"].clone(); + value["plan"]["inspection_summary"]["structural_zero_physical_sector_certificate"] = json!({ + "proof_kind": CLOSURE_TARGET_DOMAIN_PROOF_KIND, + "semantic_digest": "de".repeat(32), + "sector_count": sector_count, + "resolved_helicity_count": resolved_helicity_count, + "certified_structural_zero_sector_count": 0 + }); + } + } + + fn manifest_bytes_with_size(size: usize) -> Vec { + let mut bytes = serde_json::to_vec(&manifest()).unwrap(); + assert!(bytes.len() <= size); + bytes.resize(size, b' '); + bytes + } + + #[test] + fn accepts_strict_typed_recurrence_manifest() { + let parsed = parse(&manifest()).unwrap(); + assert_eq!(parsed.runtime_metadata.public_color_flows.len(), 1); + assert_eq!(parsed.runtime_metadata.source_templates.len(), 1); + assert_eq!(parsed.plan.runtime_schedule.member_count, 1); + assert_eq!( + parsed.plan.inspection_summary.direct_arena.row_group_count, + 4 + ); + } + + #[test] + fn accepts_and_validates_generated_structural_zero_sector_provenance() { + let value = full_hzz_fixture_value(); + let parsed = parse_hzz_fixture(&serde_json::to_vec(&value).unwrap(), "full").unwrap(); + let certificate = parsed + .plan + .inspection_summary + .structural_zero_physical_sector_certificate + .unwrap(); + assert_eq!(certificate.proof_kind, CLOSURE_TARGET_DOMAIN_PROOF_KIND); + assert_eq!(certificate.sector_count, 1); + assert_eq!(certificate.resolved_helicity_count, 54); + + let mut mismatched = value; + mismatched["plan"]["inspection_summary"]["structural_zero_physical_sector_certificate"]["sector_count"] = + json!(2); + let error = + parse_hzz_fixture(&serde_json::to_vec(&mismatched).unwrap(), "full").unwrap_err(); + assert!( + error + .to_string() + .contains("disagrees with its inspection domain") + ); + } + + #[test] + fn accepts_resident_peak_contribution_semantics() { + let mut value = manifest(); + value["plan"]["inspection_summary"]["construction"]["peak_contribution_count_semantics"] = + json!("resident-pending-contributions-v1"); + + let parsed = parse(&value).unwrap(); + + assert_eq!( + parsed + .plan + .inspection_summary + .construction + .peak_contribution_count_semantics, + Some(RecurrencePeakContributionCountSemantics::ResidentPendingContributionsV1) + ); + } + + #[test] + fn rejects_unknown_peak_contribution_semantics() { + let mut value = manifest(); + value["plan"]["inspection_summary"]["construction"]["peak_contribution_count_semantics"] = + json!("unknown"); + + assert!(parse(&value).is_err()); + } + + #[test] + fn accepts_python_emitted_selector_work_schema() { + let mut value = manifest(); + add_python_selector_work_certificate(&mut value); + + let parsed = parse(&value).unwrap(); + + assert_eq!( + parsed + .plan + .inspection_summary + .direct_arena + .cache_footprint_policy, + Some(RecurrenceCacheFootprintPolicy::SelectorActiveMaxV1) + ); + } + + #[test] + fn accepts_contracted_selector_work_representatives() { + let value = full_hzz_fixture_value(); + + let parsed = parse_hzz_fixture(&serde_json::to_vec(&value).unwrap(), "full").unwrap(); + + assert_eq!( + parsed + .plan + .inspection_summary + .selector_work_certificate + .as_ref() + .unwrap() + .representatives + .len(), + 1 + ); + } + + #[test] + fn accepts_persisted_helicity_selector_companion_v2() { + let mut value = full_hzz_fixture_value(); + add_hzz_helicity_selector_companion(&mut value); + + let parsed = + parse_hzz_companion_fixture(&serde_json::to_vec(&value).unwrap(), "full").unwrap(); + + assert_eq!(parsed.process_digest, Some("10".repeat(32))); + let companion = parsed.helicity_selector_companion.unwrap(); + assert_eq!(companion.schema_version, 2); + assert_eq!(companion.kind, RECURRENCE_HELICITY_SELECTOR_COMPANION_KIND); + assert_eq!( + companion.plan.inspection_summary.lc_flow_layout, + RecurrenceLcFlowLayout::AllFlowUnion + ); + assert_eq!( + companion + .plan + .helicity_dispatch + .as_ref() + .unwrap() + .resolved_helicity_count, + 108 + ); + assert_eq!(companion.color_contraction.source, "primary"); + assert_eq!( + companion.color_contraction.view, + RECURRENCE_PRIMARY_LOCAL_COLOR_VIEW + ); + } + + #[test] + fn helicity_selector_composite_digest_matches_the_python_writer() { + assert_eq!( + recurrence_helicity_selector_schedule_digest(&"21".repeat(32), &"23".repeat(32),) + .unwrap(), + "5913a2ef2448054b070d57414e621e2a9892676d7ecac59ec161fbf82b3580e7" + ); + } + + #[test] + fn rejects_incomplete_or_tampered_helicity_selector_companion_contract() { + let mut baseline = full_hzz_fixture_value(); + add_hzz_helicity_selector_companion(&mut baseline); + + let mut missing_process_digest = baseline.clone(); + missing_process_digest["process_digest"] = Value::Null; + assert!( + parse_hzz_companion_fixture( + &serde_json::to_vec(&missing_process_digest).unwrap(), + "full", + ) + .is_err() + ); + + let mut swapped_primary_process = baseline.clone(); + swapped_primary_process["plan"]["process_binding"]["process_digest"] = + json!("30".repeat(32)); + assert!( + parse_hzz_companion_fixture( + &serde_json::to_vec(&swapped_primary_process).unwrap(), + "full", + ) + .is_err() + ); + + let mut missing_capability = baseline.clone(); + missing_capability["required_runtime_capabilities"] + .as_array_mut() + .unwrap() + .retain(|value| { + value.as_str() != Some(RECURRENCE_HELICITY_SELECTOR_COMPANION_RUNTIME_CAPABILITY) + }); + assert!( + parse_hzz_companion_fixture(&serde_json::to_vec(&missing_capability).unwrap(), "full",) + .is_err() + ); + + for (pointer, replacement) in [ + ("/helicity_selector_companion/schema_version", json!(1)), + ( + "/helicity_selector_companion/kind", + json!("pyamplicol-recurrence-helicity-selector-companion-v1"), + ), + ( + "/helicity_selector_companion/process_digest", + json!("40".repeat(32)), + ), + ( + "/helicity_selector_companion/plan/prepared_kernel_pack_digest", + json!("41".repeat(32)), + ), + ( + "/helicity_selector_companion/plan/direct_template_catalog_digest", + json!("42".repeat(32)), + ), + ( + "/helicity_selector_companion/plan/inspection_summary/lc_flow_layout", + json!("contracted-color-union"), + ), + ( + "/helicity_selector_companion/plan/inspection_summary/schedule/amplitude_destination_count", + json!(2), + ), + ( + "/helicity_selector_companion/plan/inspection_summary/schedule/retained_helicity_count", + json!(107), + ), + ( + "/plan/inspection_summary/schedule/retained_helicity_count", + json!(107), + ), + ( + "/helicity_selector_companion/plan/process_binding/native_schedule_semantic_digest", + json!("43".repeat(32)), + ), + ( + "/helicity_selector_companion/plan/process_binding/schedule_digest", + json!("44".repeat(32)), + ), + ( + "/helicity_selector_companion/plan/process_binding/path", + json!(RECURRENCE_PROCESS_BINDING_PATH), + ), + ( + "/helicity_selector_companion/plan/runtime_schedule/path", + json!("recurrence-runtime.pacbin"), + ), + ( + "/helicity_selector_companion/plan/helicity_dispatch/abi", + json!("pyamplicol-recurrence-helicity-dispatch-v0"), + ), + ( + "/helicity_selector_companion/plan/helicity_dispatch/path", + json!(RECURRENCE_HELICITY_DISPATCH_PATH), + ), + ( + "/helicity_selector_companion/plan/helicity_dispatch/size_bytes", + json!(0), + ), + ( + "/helicity_selector_companion/plan/helicity_dispatch/sha256", + json!("not-a-sha"), + ), + ( + "/helicity_selector_companion/plan/helicity_dispatch/base_runtime_layout_digest", + json!("45".repeat(32)), + ), + ( + "/helicity_selector_companion/plan/helicity_dispatch/resolved_helicity_count", + json!(107), + ), + ( + "/helicity_selector_companion/color_contraction/source", + json!("companion"), + ), + ( + "/helicity_selector_companion/color_contraction/view", + json!("primary-local-color-view-v2"), + ), + ( + "/runtime_metadata/color_contraction/storage", + json!("expanded"), + ), + ] { + let mut invalid = baseline.clone(); + *invalid.pointer_mut(pointer).unwrap() = replacement; + assert!( + parse_hzz_companion_fixture(&serde_json::to_vec(&invalid).unwrap(), "full") + .is_err(), + "accepted invalid companion field at {pointer}", + ); + } + } + + #[test] + fn accepts_legacy_contracted_replay_without_selector_work_representatives() { + for (value, accuracy) in [ + (legacy_full_hzz_fixture_value(), "full"), + (legacy_nlc_hzz_fixture_value(), "nlc"), + ] { + let parsed = parse_hzz_fixture(&serde_json::to_vec(&value).unwrap(), accuracy).unwrap(); + assert!( + parsed + .plan + .inspection_summary + .selector_work_certificate + .as_ref() + .unwrap() + .representatives + .is_empty() + ); + } + } + + #[test] + fn rejects_contracted_selector_work_representatives_without_replay_targets() { + let mut value = full_hzz_fixture_value(); + value["plan"]["inspection_summary"]["schedule"]["replay_target_count"] = json!(0); + + assert!( + parse_hzz_fixture(&serde_json::to_vec(&value).unwrap(), "full") + .unwrap_err() + .to_string() + .contains("representatives disagree with the flow layout") + ); + } + + #[test] + fn accepts_python_emitted_color_projection_certificate() { + let mut value = manifest(); + add_python_selector_work_certificate(&mut value); + add_python_color_projection_certificate(&mut value); + + let parsed = parse(&value).unwrap(); + + assert_eq!(parsed.plan.runtime_schedule.member_count, 2); + assert_eq!(parsed.plan.runtime_schedule.unpacked_size_bytes, 640); + assert_eq!( + parsed + .plan + .inspection_summary + .color_projection_certificate + .as_ref() + .unwrap() + .size_bytes, + 128 + ); + } + + #[test] + fn accepts_python_emitted_relation_discovery_summaries() { + for mode in ["diagnostic", "certified-reuse"] { + let mut value = manifest(); + add_python_relation_discovery(&mut value, mode); + + let parsed = parse(&value).unwrap(); + + assert_eq!( + parsed + .plan + .inspection_summary + .relation_discovery + .as_ref() + .unwrap() + .certificate_count, + 0 + ); + } + } + + #[test] + fn rejects_relation_discovery_that_mutates_diagnostic_mode() { + let mut value = manifest(); + add_python_relation_discovery(&mut value, "diagnostic"); + value["plan"]["inspection_summary"]["relation_discovery"]["applied_relation_count"] = + json!(1); + + assert!( + parse(&value) + .unwrap_err() + .to_string() + .contains("diagnostic recurrence relation discovery altered") + ); + } + + #[test] + fn rejects_tampered_rejected_diagnostics_commitment() { + let mut value = manifest(); + add_python_relation_discovery(&mut value, "diagnostic"); + value["plan"]["inspection_summary"]["relation_discovery"]["rejected_candidate_diagnostics"] + ["retained_sha256"] = json!("00".repeat(32)); + + assert!( + parse(&value) + .unwrap_err() + .to_string() + .contains("retained rejection diagnostics commitment") + ); + } + + #[test] + fn rejects_tampered_full_rejection_census_and_decision_commitments() { + for field in ["full_census_sha256", "full_rejection_sha256"] { + let mut value = manifest(); + add_python_relation_discovery(&mut value, "diagnostic"); + value["plan"]["inspection_summary"]["relation_discovery"]["rejected_candidate_diagnostics"] + [field] = json!("00".repeat(32)); + + assert!( + parse(&value) + .unwrap_err() + .to_string() + .contains("full rejection census or decision commitment"), + "tampered {field} was not rejected" + ); + } + } + + fn mark_relation_discovery_as_full_probe(value: &mut Value) { + let discovery = &mut value["plan"]["inspection_summary"]["relation_discovery"]; + discovery["probe"]["candidate_only"] = json!(false); + for field in [ + "runtime_parameter_schema_sha256", + "candidate_observation_batch_sha256", + "verification_observation_batch_sha256", + "decision_sha256", + ] { + discovery["probe"][field] = json!("bb".repeat(32)); + } + } + + #[test] + fn rejects_full_probe_with_removed_rejected_diagnostics() { + let mut value = manifest(); + add_python_relation_discovery(&mut value, "diagnostic"); + mark_relation_discovery_as_full_probe(&mut value); + value["plan"]["inspection_summary"]["relation_discovery"] + .as_object_mut() + .unwrap() + .remove("rejected_candidate_diagnostics"); + + assert!( + parse(&value) + .unwrap_err() + .to_string() + .contains("require rejected-candidate diagnostics") + ); + } + + #[test] + fn rejects_full_probe_with_null_rejected_diagnostics() { + let mut value = manifest(); + add_python_relation_discovery(&mut value, "diagnostic"); + mark_relation_discovery_as_full_probe(&mut value); + value["plan"]["inspection_summary"]["relation_discovery"]["rejected_candidate_diagnostics"] = + Value::Null; + + assert!( + parse(&value) + .unwrap_err() + .to_string() + .contains("require rejected-candidate diagnostics") + ); + } + + #[test] + fn rejects_raw_uncertified_count_that_disagrees_with_verification_failures() { + let mut value = manifest(); + add_python_relation_discovery(&mut value, "diagnostic"); + mark_relation_discovery_as_full_probe(&mut value); + let discovery = &mut value["plan"]["inspection_summary"]["relation_discovery"]; + discovery["uncertified_candidate_count"] = json!(1); + + assert!( + parse(&value) + .unwrap_err() + .to_string() + .contains("authenticated raw rejection census is inconsistent") + ); + } + + #[test] + fn accepts_actual_python_emitted_full_lc_and_nlc_manifests() { + let fixtures = [ + (full_hzz_fixture_value(), "full", Some(54)), + (lc_hzz_fixture_value(), "lc", None), + (nlc_hzz_fixture_value(), "nlc", Some(54)), + ]; + + for (value, color_accuracy, expected_materialized_destinations) in fixtures { + let parsed = + parse_hzz_fixture(&serde_json::to_vec(&value).unwrap(), color_accuracy).unwrap(); + assert_eq!( + parsed + .runtime_metadata + .color_contraction + .as_ref() + .and_then(|metadata| metadata.materialized_destination_count), + expected_materialized_destinations + ); + } + } + + #[test] + fn materialized_destination_count_is_backward_compatible_and_validated() { + let mut value = full_hzz_fixture_value(); + value["runtime_metadata"]["color_contraction"] + .as_object_mut() + .unwrap() + .remove("materialized_destination_count"); + parse_hzz_fixture(&serde_json::to_vec(&value).unwrap(), "full").unwrap(); + + value["runtime_metadata"]["color_contraction"]["materialized_destination_count"] = + json!(53); + assert!( + parse_hzz_fixture(&serde_json::to_vec(&value).unwrap(), "full") + .unwrap_err() + .to_string() + .contains("color-contraction summary is inconsistent") + ); + } + + #[test] + fn rejects_inconsistent_color_projection_certificate_metadata() { + let mut bad_members = manifest(); + add_python_selector_work_certificate(&mut bad_members); + add_python_color_projection_certificate(&mut bad_members); + bad_members["plan"]["runtime_schedule"]["member_count"] = json!(1); + assert!( + parse(&bad_members) + .unwrap_err() + .to_string() + .contains("plan member metadata disagrees") + ); + + let mut bad_unpacked_size = manifest(); + add_python_selector_work_certificate(&mut bad_unpacked_size); + add_python_color_projection_certificate(&mut bad_unpacked_size); + bad_unpacked_size["plan"]["runtime_schedule"]["unpacked_size_bytes"] = json!(639); + assert!( + parse(&bad_unpacked_size) + .unwrap_err() + .to_string() + .contains("plan member metadata disagrees") + ); + + let mut not_publishable = manifest(); + add_python_selector_work_certificate(&mut not_publishable); + add_python_color_projection_certificate(&mut not_publishable); + not_publishable["plan"]["inspection_summary"]["color_projection_certificate"]["publishable"] = + json!(false); + assert!( + parse(¬_publishable) + .unwrap_err() + .to_string() + .contains("metadata is not publishable") + ); + + let mut wrong_layout = manifest(); + add_python_color_projection_certificate(&mut wrong_layout); + wrong_layout["plan"]["inspection_summary"]["lc_flow_layout"] = json!("all-flow-union"); + wrong_layout["recurrence_summary"]["lc_flow_layout"] = json!("all-flow-union"); + assert!( + parse(&wrong_layout) + .unwrap_err() + .to_string() + .contains("unsupported recurrence color-projection certificate contract") + ); + } + + #[test] + fn accepts_persisted_selector_work_schema() { + for layout in ["all-flow-union", "contracted-color-union"] { + let mut value = inspection_summary(); + value["lc_flow_layout"] = json!(layout); + value["schedule"]["replay_target_count"] = json!(0); + if layout == "contracted-color-union" { + value["structural_zero_physical_sector_certificate"] = json!({ + "proof_kind": CLOSURE_TARGET_DOMAIN_PROOF_KIND, + "semantic_digest": "de".repeat(32), + "sector_count": 1, + "resolved_helicity_count": 1, + "certified_structural_zero_sector_count": 0 + }); + } + value["selector_work_certificate"] = json!({ + "schema_version": 1, + "binding": "runtime_layout_digest", + "persisted_union": { + "current_count": 1, + "semantic_component_count": 1, + "source_row_count": 1, + "contribution_count": 0, + "finalization_count": 1, + "closure_count": 0, + "row_count": 2 + }, + "representatives": [] + }); + value["direct_arena"]["cache_footprint_policy"] = json!("persisted-arena-v1"); + let parsed = serde_json::from_value::(value).unwrap(); + + parsed.validate("x_to_x", "lc").unwrap(); + } + } + + #[test] + fn rejects_cache_footprint_policy_for_another_flow_layout() { + let mut value = manifest(); + add_python_selector_work_certificate(&mut value); + value["plan"]["inspection_summary"]["direct_arena"]["cache_footprint_policy"] = + json!("persisted-arena-v1"); + + let error = parse(&value).unwrap_err(); + + assert!( + error + .to_string() + .contains("cache-footprint policy disagrees with its flow layout") + ); + } + + #[test] + fn rejects_standalone_selector_work_schema_member() { + let mut policy_only = manifest(); + policy_only["plan"]["inspection_summary"]["direct_arena"]["cache_footprint_policy"] = + json!("selector-active-max-v1"); + assert!( + parse(&policy_only) + .unwrap_err() + .to_string() + .contains("must be declared together") + ); + + let mut certificate_only = manifest(); + add_python_selector_work_certificate(&mut certificate_only); + certificate_only["plan"]["inspection_summary"]["direct_arena"] + .as_object_mut() + .unwrap() + .remove("cache_footprint_policy"); + assert!( + parse(&certificate_only) + .unwrap_err() + .to_string() + .contains("must be declared together") + ); + } + + #[test] + fn rejects_inconsistent_selector_work_certificates() { + let mut wrong_schema = manifest(); + add_python_selector_work_certificate(&mut wrong_schema); + wrong_schema["plan"]["inspection_summary"]["selector_work_certificate"]["schema_version"] = + json!(2); + assert!( + parse(&wrong_schema) + .unwrap_err() + .to_string() + .contains("certificate identity is unsupported") + ); + + let mut wrong_binding = manifest(); + add_python_selector_work_certificate(&mut wrong_binding); + wrong_binding["plan"]["inspection_summary"]["selector_work_certificate"]["binding"] = + json!("semantic_digest"); + assert!( + parse(&wrong_binding) + .unwrap_err() + .to_string() + .contains("certificate identity is unsupported") + ); + + let mut wrong_union = manifest(); + add_python_selector_work_certificate(&mut wrong_union); + wrong_union["plan"]["inspection_summary"]["selector_work_certificate"]["persisted_union"] + ["row_count"] = json!(3); + assert!( + parse(&wrong_union) + .unwrap_err() + .to_string() + .contains("persisted selector-work summary disagrees") + ); + + let mut duplicate = manifest(); + add_python_selector_work_certificate(&mut duplicate); + let representative = + duplicate["plan"]["inspection_summary"]["selector_work_certificate"]["representatives"] + [0] + .clone(); + duplicate["plan"]["inspection_summary"]["selector_work_certificate"]["representatives"] + .as_array_mut() + .unwrap() + .push(representative); + duplicate["plan"]["inspection_summary"]["schedule"]["replay_target_count"] = json!(2); + assert!( + parse(&duplicate) + .unwrap_err() + .to_string() + .contains("representative sector is duplicated") + ); + + let mut out_of_range = manifest(); + add_python_selector_work_certificate(&mut out_of_range); + out_of_range["plan"]["inspection_summary"]["selector_work_certificate"]["representatives"] + [0]["representative_sector_id"] = json!(1); + assert!( + parse(&out_of_range) + .unwrap_err() + .to_string() + .contains("representative is inconsistent") + ); + + let mut wrong_public_flow_count = manifest(); + add_python_selector_work_certificate(&mut wrong_public_flow_count); + wrong_public_flow_count["plan"]["inspection_summary"]["selector_work_certificate"]["representatives"] + [0]["public_flow_count"] = json!(2); + assert!( + parse(&wrong_public_flow_count) + .unwrap_err() + .to_string() + .contains("public-flow count disagrees with replay targets") + ); + } + + #[test] + fn accepts_observed_large_recurrence_manifest() { + let bytes = manifest_bytes_with_size(40_826_959); + let parsed = parse_recurrence_execution_manifest( + &bytes, + "processes/x_to_x/execution.json", + &outer(), + ) + .unwrap(); + + assert_eq!(parsed.key, "x_to_x"); + } + + #[test] + fn accepts_runtime_options_above_old_reader_ceilings() { + let mut value = manifest(); + value["runtime_options"]["point_tile_size"] = json!(1_048_577); + value["runtime_options"]["workspace_mib"] = json!(4097); + + parse(&value).unwrap(); + + value["runtime_options"]["workspace_mib"] = json!(0); + assert!(parse(&value).is_err()); + + value["runtime_options"]["workspace_mib"] = json!(u64::from(u32::MAX) + 1); + assert!(parse(&value).is_err()); + + value["runtime_options"]["workspace_mib"] = json!(4097); + value["runtime_options"]["point_tile_size"] = json!(u64::from(u32::MAX) + 1); + assert!(parse(&value).is_err()); + } + + #[test] + fn accepts_large_authenticated_recurrence_payload_metadata() { + let mut value = manifest(); + add_python_selector_work_certificate(&mut value); + add_python_color_projection_certificate(&mut value); + + let member_size = 1_u64 << 40; + let certificate_size = member_size + 1; + let unpacked_size = member_size + certificate_size; + let container_size = unpacked_size + 4096; + value["plan"]["runtime_schedule"]["size_bytes"] = json!(container_size); + value["plan"]["runtime_schedule"]["unpacked_size_bytes"] = json!(unpacked_size); + value["plan"]["process_binding"]["size_bytes"] = json!(1_u64 << 30); + value["plan"]["inspection_summary"]["runtime_container_member"]["size_bytes"] = + json!(member_size); + value["plan"]["inspection_summary"]["runtime_container_member"]["container_size_bytes"] = + json!(container_size); + value["plan"]["inspection_summary"]["color_projection_certificate"]["size_bytes"] = + json!(certificate_size); + + let parsed = parse(&value).unwrap(); + + assert_eq!(parsed.plan.runtime_schedule.size_bytes, container_size); + assert_eq!( + parsed.plan.runtime_schedule.unpacked_size_bytes, + unpacked_size + ); + assert_eq!(parsed.plan.process_binding.size_bytes, 1_u64 << 30); + assert_eq!( + parsed + .plan + .inspection_summary + .runtime_container_member + .size_bytes, + member_size + ); + assert_eq!( + parsed + .plan + .inspection_summary + .color_projection_certificate + .unwrap() + .size_bytes, + certificate_size + ); + } + + #[test] + fn rejects_zero_recurrence_payload_metadata() { + let mut baseline = manifest(); + add_python_selector_work_certificate(&mut baseline); + add_python_color_projection_certificate(&mut baseline); + + for pointer in [ + "/plan/runtime_schedule/size_bytes", + "/plan/runtime_schedule/unpacked_size_bytes", + "/plan/process_binding/size_bytes", + "/plan/inspection_summary/runtime_container_member/size_bytes", + "/plan/inspection_summary/runtime_container_member/container_size_bytes", + "/plan/inspection_summary/color_projection_certificate/size_bytes", + ] { + let mut value = baseline.clone(); + *value.pointer_mut(pointer).unwrap() = json!(0); + assert!( + parse(&value).is_err(), + "accepted zero metadata at {pointer}" + ); + } + + let mut color = full_hzz_fixture_value(); + color["runtime_metadata"]["color_contraction"]["size_bytes"] = json!(0); + assert!( + parse_hzz_fixture(&serde_json::to_vec(&color).unwrap(), "full").is_err(), + "accepted zero recurrence color payload size" + ); + } + + #[test] + fn accepts_large_recurrence_color_payload_metadata() { + let mut value = full_hzz_fixture_value(); + value["runtime_metadata"]["color_contraction"]["size_bytes"] = json!(1_u64 << 40); + + let parsed = parse_hzz_fixture(&serde_json::to_vec(&value).unwrap(), "full").unwrap(); + + assert_eq!( + parsed + .runtime_metadata + .color_contraction + .unwrap() + .size_bytes, + 1_u64 << 40 + ); + } + + #[test] + fn accepts_large_generated_text_but_still_rejects_empty_text() { + let mut value = manifest(); + let coupling_policy = "x".repeat(4097); + value["runtime_metadata"]["normalization"]["coupling_policy"] = json!(coupling_policy); + + let parsed = parse(&value).unwrap(); + assert_eq!( + parsed.runtime_metadata.normalization.coupling_policy.len(), + 4097 + ); + + value["runtime_metadata"]["normalization"]["coupling_policy"] = json!(""); + assert!( + parse(&value) + .unwrap_err() + .to_string() + .contains("recurrence normalization coupling policy is empty") + ); + } + + #[test] + fn rejects_unknown_runtime_metadata_fields() { + let mut value = manifest(); + value["runtime_metadata"]["opaque"] = json!({}); + let error = parse(&value).unwrap_err(); + assert!(error.to_string().contains("unknown field `opaque`")); + } + + #[test] + fn rejects_public_color_flow_outside_construction_sectors() { + let mut value = manifest(); + value["runtime_metadata"]["public_color_flows"][0]["construction_sector_id"] = json!(1); + let error = parse(&value).unwrap_err(); + assert!(error.to_string().contains("absent construction sector")); + } + + #[test] + fn rejects_noncanonical_public_color_flow_target_sector() { + let mut value = manifest(); + value["runtime_metadata"]["public_color_flows"][0]["target_sector_id"] = json!(1); + let error = parse(&value).unwrap_err(); + assert!(error.to_string().contains("not dense or in range")); + } + + #[test] + fn rejects_source_ir_and_crossing_disagreement() { + let mut value = manifest(); + value["runtime_metadata"]["source_templates"][0]["crossing"]["phase"] = json!([0.0, 1.0]); + let error = parse(&value).unwrap_err(); + assert!( + error + .to_string() + .contains("disagrees with its typed SourceIR") + ); + } + + #[test] + fn rejects_noncanonical_container_and_capability_metadata() { + let mut bad_path = manifest(); + bad_path["plan"]["runtime_schedule"]["path"] = json!("../recurrence-runtime.pacbin"); + assert!( + parse(&bad_path) + .unwrap_err() + .to_string() + .contains("canonical") + ); + + let mut bad_capabilities = manifest(); + bad_capabilities["required_runtime_capabilities"] = json!([ + RECURRENCE_RUNTIME_CAPABILITY, + RECURRENCE_LC_COLOR_CAPABILITY + ]); + assert!( + parse(&bad_capabilities) + .unwrap_err() + .to_string() + .contains("must require exactly") + ); + + let mut bad_member_path = manifest(); + bad_member_path["plan"]["runtime_schedule"]["plan_member_path"] = + json!("plan/recurrence-plan-v1.bin"); + assert!( + parse(&bad_member_path) + .unwrap_err() + .to_string() + .contains("Direct-Arena v2 schedule member path") + ); + } + + #[test] + fn rejects_pre_direct_arena_abis() { + let mut value = manifest(); + value["recurrence_plan_abi"] = json!("pyamplicol-recurrence-plan-v1"); + let error = parse(&value).unwrap_err(); + assert!(error.to_string().contains("Direct-Arena v2 plan ABI")); + assert!(error.to_string().contains("regenerate")); + } + + #[test] + fn rejects_cross_record_direct_digest_mismatches() { + let mut bad_pack = manifest(); + bad_pack["plan"]["prepared_kernel_pack_digest"] = json!("99".repeat(32)); + assert!( + parse(&bad_pack) + .unwrap_err() + .to_string() + .contains("prepared-kernel pack digest differs") + ); + + let mut bad_catalog = manifest(); + bad_catalog["plan"]["direct_template_catalog_digest"] = json!("aa".repeat(32)); + assert!( + parse(&bad_catalog) + .unwrap_err() + .to_string() + .contains("direct-template catalog digest differs") + ); + } + + #[test] + fn rejects_malformed_direct_authentication_digests() { + let mut value = manifest(); + value["direct_template_catalog_digest"] = json!("A5".repeat(32)); + let error = parse(&value).unwrap_err(); + assert!( + error + .to_string() + .contains("64 lowercase hexadecimal characters") + ); + } + + #[test] + fn rejects_malformed_native_schedule_semantic_digest() { + let mut value = manifest(); + value["plan"]["process_binding"]["native_schedule_semantic_digest"] = + json!("AB".repeat(32)); + let error = parse(&value).unwrap_err(); + assert!( + error + .to_string() + .contains("64 lowercase hexadecimal characters") + ); + } + + #[test] + fn rejects_missing_native_schedule_semantic_digest() { + let mut value = manifest(); + value["plan"]["process_binding"] + .as_object_mut() + .unwrap() + .remove("native_schedule_semantic_digest"); + let error = parse(&value).unwrap_err(); + assert!( + error + .to_string() + .contains("native_schedule_semantic_digest"), + "{error}" + ); + } + + #[test] + fn rejects_inconsistent_direct_container_member_metadata() { + let mut value = manifest(); + value["plan"]["inspection_summary"]["runtime_container_member"]["container_size_bytes"] = + json!(4095); + let error = parse(&value).unwrap_err(); + assert!(error.to_string().contains("plan member metadata disagrees")); + } + + #[test] + fn rejects_runtime_metadata_cross_record_mismatches() { + let mut bad_parameter_count = manifest(); + bad_parameter_count["plan"]["inspection_summary"]["parameter_count"] = json!(2); + assert!( + parse(&bad_parameter_count) + .unwrap_err() + .to_string() + .contains("plan parameter count") + ); + + let mut bad_external = manifest(); + bad_external["runtime_metadata"]["external_legs"][0]["physical_pdg"] = json!(24); + assert!( + parse(&bad_external) + .unwrap_err() + .to_string() + .contains("physical PDGs") + ); + } + + #[test] + fn rejects_nonbijective_source_state_remaps() { + let mut duplicate = manifest(); + duplicate["plan"]["process_binding"]["remap"]["source_state_offsets"] = json!([0, 2]); + duplicate["plan"]["process_binding"]["remap"]["source_state_indices"] = json!([0, 0]); + assert!( + parse(&duplicate) + .unwrap_err() + .to_string() + .contains("dense zero-based permutation") + ); + + let mut invalid_offsets = manifest(); + invalid_offsets["plan"]["process_binding"]["remap"]["source_state_offsets"] = json!([1, 2]); + invalid_offsets["plan"]["process_binding"]["remap"]["source_state_indices"] = json!([0, 1]); + assert!( + parse(&invalid_offsets) + .unwrap_err() + .to_string() + .contains("offsets are invalid") + ); + } +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/engine/recurrence_process_pack.rs b/pyamplicol/rust/crates/rusticol-core/src/engine/recurrence_process_pack.rs new file mode 100644 index 0000000000..6e7a8c72e2 --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/engine/recurrence_process_pack.rs @@ -0,0 +1,866 @@ +// SPDX-License-Identifier: 0BSD + +//! Process-owned operational Direct-executor records embedded in PACRDBN4. + +use super::eager_manifest::{ + RecurrenceDirectParameterBindingManifest, RecurrenceDirectPlaneProjectionManifest, + RecurrenceDirectScalarProjectionManifest, +}; +use crate::recurrence::{ + DIRECT_NONE_U32, DirectExecutorRole, DirectRecurrencePlan, SemanticDigest, +}; +use crate::{RusticolError, RusticolResult, Target}; +use std::collections::{BTreeMap, BTreeSet}; + +pub(super) const RECURRENCE_PROCESS_BINDING_MAGIC_V4: &[u8; 8] = b"PACRDBN4"; +pub(super) const RECURRENCE_PROCESS_BINDING_VERSION_V4: u32 = 4; +pub(super) const RECURRENCE_PROCESS_BINDING_HEADER_SIZE_V4: usize = 344; + +const PROCESS_EXECUTOR_FLAG_USES_EXACT_FACTOR: u16 = 1 << 0; +const PROCESS_EXECUTOR_FLAGS_KNOWN: u16 = PROCESS_EXECUTOR_FLAG_USES_EXACT_FACTOR; +const PREPARED_JIT_PORTABLE_TARGET: &str = "symjit-storage-v3-portable"; +const PREPARED_JIT_OPTIMIZATION_LEVEL: u32 = 2; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(super) enum ProcessExecutorBackend { + Jit, + Cpp, + Asm, +} + +impl ProcessExecutorBackend { + pub(super) fn decode(value: u8) -> RusticolResult { + match value { + 0 => Ok(Self::Jit), + 1 => Ok(Self::Cpp), + 2 => Ok(Self::Asm), + _ => Err(RusticolError::compatibility(format!( + "unsupported recurrence process-executor backend tag {value}" + ))), + } + } + + pub(super) const fn as_str(self) -> &'static str { + match self { + Self::Jit => "jit", + Self::Cpp => "cpp", + Self::Asm => "asm", + } + } +} + +#[derive(Clone, Debug)] +pub(super) struct ProcessExecutorTarget { + pub(super) backend: ProcessExecutorBackend, + pub(super) target_triple: String, + pub(super) portable: bool, + pub(super) cpu_features: Vec, +} + +impl ProcessExecutorTarget { + pub(super) fn validate(&self, outer: &Target) -> RusticolResult<()> { + if self.cpu_features.windows(2).any(|pair| pair[0] >= pair[1]) { + return Err(RusticolError::integrity( + "recurrence process-executor CPU features are not sorted and unique", + )); + } + match self.backend { + ProcessExecutorBackend::Jit => { + if !matches!(std::env::consts::ARCH, "aarch64" | "x86_64") + || !self.portable + || self.target_triple != PREPARED_JIT_PORTABLE_TARGET + || !self.cpu_features.is_empty() + || outer.triple != crate::artifact::PORTABLE_64LE_ARTIFACT_TARGET + || !outer.cpu_features.is_empty() + { + return Err(RusticolError::compatibility( + "portable recurrence JIT executors have an incompatible target", + )); + } + } + ProcessExecutorBackend::Cpp | ProcessExecutorBackend::Asm => { + let host = crate::runtime_target_info(); + let host_features = host.cpu_features.into_iter().collect::>(); + if self.portable + || self.target_triple != host.triple + || outer.triple != host.triple + || self + .cpu_features + .iter() + .any(|feature| !host_features.contains(feature)) + || self.cpu_features != outer.cpu_features + { + return Err(RusticolError::compatibility( + "target-native recurrence executors are incompatible with this host", + )); + } + } + } + Ok(()) + } +} + +#[derive(Clone, Debug)] +pub(super) struct ProcessExecutorIdentities { + pub(super) compiled_model_digest: SemanticDigest, + pub(super) recurrence_template_catalog_digest: SemanticDigest, + pub(super) prepared_kernel_pack_digest: SemanticDigest, + pub(super) direct_template_catalog_digest: SemanticDigest, + pub(super) runtime_layout_digest: SemanticDigest, +} + +#[derive(Clone, Debug)] +pub(super) struct ProcessDirectExecutorPack { + pub(super) identities: ProcessExecutorIdentities, + pub(super) target: ProcessExecutorTarget, + pub(super) catalog_executor_count: u32, + pub(super) descriptors: Vec, +} + +impl ProcessDirectExecutorPack { + pub(super) fn validate_for_plan( + &self, + plan: &DirectRecurrencePlan, + expected_prepared_pack_digest: SemanticDigest, + expected_catalog_digest: SemanticDigest, + expected_root_runtime_layout_digest: SemanticDigest, + ) -> RusticolResult<()> { + if self.catalog_executor_count != plan.direct_executor_count() + || self.identities.prepared_kernel_pack_digest != expected_prepared_pack_digest + || self.identities.prepared_kernel_pack_digest != plan.prepared_pack_digest() + || self.identities.direct_template_catalog_digest != expected_catalog_digest + || self.identities.direct_template_catalog_digest + != plan.direct_template_catalog_digest() + || self.identities.runtime_layout_digest != expected_root_runtime_layout_digest + { + return Err(RusticolError::integrity( + "recurrence process-executor identities disagree with the remapped plan", + )); + } + let descriptors = self + .descriptors + .iter() + .map(|descriptor| (descriptor.direct_executor_id, descriptor)) + .collect::>(); + if descriptors.len() != self.descriptors.len() { + return Err(RusticolError::integrity( + "recurrence process-executor pack repeats an executor ID", + )); + } + if descriptors + .keys() + .any(|executor_id| *executor_id >= self.catalog_executor_count) + { + return Err(RusticolError::integrity( + "recurrence process-executor pack contains an out-of-range executor ID", + )); + } + let mut required = BTreeMap::::new(); + for group in plan.row_groups() { + if group.direct_executor_id == DIRECT_NONE_U32 { + continue; + } + if group.direct_executor_id >= self.catalog_executor_count { + return Err(RusticolError::integrity( + "recurrence plan references an executor outside its complete catalog", + )); + } + if required + .insert(group.direct_executor_id, group.role) + .is_some_and(|previous| previous != group.role) + { + return Err(RusticolError::integrity(format!( + "recurrence executor {} is used with multiple roles", + group.direct_executor_id + ))); + } + } + if required.len() != descriptors.len() + || required.keys().copied().ne(descriptors.keys().copied()) + { + return Err(RusticolError::integrity( + "recurrence process-executor pack has a missing or extra required executor", + )); + } + for (executor_id, role) in required { + let descriptor = descriptors.get(&executor_id).ok_or_else(|| { + RusticolError::integrity("required recurrence executor descriptor is absent") + })?; + if descriptor.role != role { + return Err(RusticolError::integrity(format!( + "recurrence executor {executor_id} has the wrong role" + ))); + } + } + Ok(()) + } +} + +#[derive(Clone, Debug)] +pub(super) struct ProcessDirectExecutorDescriptor { + pub(super) direct_executor_id: u32, + pub(super) role: DirectExecutorRole, + pub(super) destination_component_count: u32, + pub(super) uses_exact_factor: bool, + pub(super) binding: ProcessDirectExecutorBinding, +} + +#[derive(Clone, Debug)] +pub(super) enum ProcessDirectExecutorBinding { + Source, + Intrinsic(ProcessIntrinsicExecutor), + Jit(ProcessJitExecutor), + Native(ProcessNativeExecutor), +} + +#[derive(Clone, Debug)] +pub(super) struct ProcessIntrinsicExecutor { + pub(super) runtime_template: String, + pub(super) scale: Option, +} + +#[derive(Clone, Copy, Debug)] +pub(super) struct ProcessIntrinsicScale { + pub(super) constant_real_bits: u64, + pub(super) constant_imag_bits: u64, + pub(super) parameter_index: Option, +} + +#[derive(Clone, Debug)] +pub(super) struct ProcessJitExecutor { + pub(super) optimization_level: u32, + pub(super) plane_compression: bool, + pub(super) source_application_sha256: [u8; 32], + pub(super) source_application_path: String, + pub(super) source_application_abi: String, + pub(super) parameter_bindings: Vec, + pub(super) input_plane_projections: Vec, + pub(super) scalar_projections: Vec, + pub(super) output_alias_inputs: Vec, +} + +#[derive(Clone, Debug)] +pub(super) struct ProcessNativeExecutor { + pub(super) prepared_kernel_id: u32, + pub(super) library_path: String, + pub(super) native_entry_point: String, + pub(super) coupling: Option<(f64, f64)>, +} + +#[cfg_attr(target_vendor = "apple", unsafe(link_section = "__TEXT,__rcl_load"))] +#[cfg_attr(target_vendor = "apple", inline(never))] +#[cfg_attr(target_vendor = "apple", cold)] +pub(super) fn decode_process_executor_descriptors( + bytes: &[u8], + cursor: &mut usize, + descriptor_count: u32, + catalog_executor_count: u32, + target: ProcessExecutorTarget, + identities: ProcessExecutorIdentities, +) -> RusticolResult { + if descriptor_count > catalog_executor_count { + return Err(RusticolError::integrity( + "recurrence process-executor descriptor count is outside its catalog domain", + )); + } + let capacity = usize::try_from(descriptor_count) + .map_err(|_| RusticolError::artifact("recurrence process-executor count exceeds usize"))?; + let mut descriptors = Vec::with_capacity(capacity); + let mut previous_id = None; + for _ in 0..descriptor_count { + let record_start = *cursor; + let record_size = usize::try_from(read_u32(bytes, cursor, "executor record size")?) + .map_err(|_| RusticolError::artifact("executor record size exceeds usize"))?; + if record_size < 16 { + return Err(RusticolError::artifact( + "recurrence process-executor record is shorter than its fixed header", + )); + } + let record_end = record_start + .checked_add(record_size) + .filter(|end| *end <= bytes.len()) + .ok_or_else(|| RusticolError::artifact("truncated process-executor record"))?; + let executor_id = read_u32(bytes, cursor, "executor ID")?; + if executor_id >= catalog_executor_count + || previous_id.is_some_and(|previous| executor_id <= previous) + { + return Err(RusticolError::integrity( + "recurrence process-executor IDs are not sorted, unique, and in range", + )); + } + previous_id = Some(executor_id); + let role = + DirectExecutorRole::try_from(u16::from(read_u8(bytes, cursor, "executor role")?)) + .map_err(|_| RusticolError::compatibility("unsupported process-executor role"))?; + let binding_tag = read_u8(bytes, cursor, "executor binding tag")?; + let flags = read_u16(bytes, cursor, "executor flags")?; + if flags & !PROCESS_EXECUTOR_FLAGS_KNOWN != 0 { + return Err(RusticolError::compatibility( + "recurrence process executor has unknown flags", + )); + } + let destination_component_count = + read_u32(bytes, cursor, "executor destination component count")?; + if destination_component_count == 0 { + return Err(RusticolError::artifact( + "recurrence process executor has an empty destination shape", + )); + } + let binding = match binding_tag { + 0 => ProcessDirectExecutorBinding::Source, + 1 => ProcessDirectExecutorBinding::Intrinsic(decode_intrinsic(bytes, cursor)?), + 2 => ProcessDirectExecutorBinding::Jit(decode_jit(bytes, cursor)?), + 3 => ProcessDirectExecutorBinding::Native(decode_native(bytes, cursor)?), + _ => { + return Err(RusticolError::compatibility(format!( + "unsupported process-executor binding tag {binding_tag}" + ))); + } + }; + if *cursor != record_end { + return Err(RusticolError::integrity( + "recurrence process-executor record has trailing bytes", + )); + } + validate_binding_policy(role, &binding, target.backend)?; + descriptors.push(ProcessDirectExecutorDescriptor { + direct_executor_id: executor_id, + role, + destination_component_count, + uses_exact_factor: flags & PROCESS_EXECUTOR_FLAG_USES_EXACT_FACTOR != 0, + binding, + }); + } + Ok(ProcessDirectExecutorPack { + identities, + target, + catalog_executor_count, + descriptors, + }) +} + +fn validate_binding_policy( + role: DirectExecutorRole, + binding: &ProcessDirectExecutorBinding, + backend: ProcessExecutorBackend, +) -> RusticolResult<()> { + let valid = match binding { + ProcessDirectExecutorBinding::Source => role == DirectExecutorRole::Source, + ProcessDirectExecutorBinding::Intrinsic(intrinsic) => { + role != DirectExecutorRole::Source + && (role == DirectExecutorRole::Contribution) == intrinsic.scale.is_some() + } + ProcessDirectExecutorBinding::Jit(jit) => { + role != DirectExecutorRole::Source + && backend == ProcessExecutorBackend::Jit + && jit.optimization_level == PREPARED_JIT_OPTIMIZATION_LEVEL + } + ProcessDirectExecutorBinding::Native(_) => { + role != DirectExecutorRole::Source + && matches!( + backend, + ProcessExecutorBackend::Cpp | ProcessExecutorBackend::Asm + ) + } + }; + if !valid { + return Err(RusticolError::integrity( + "recurrence process-executor binding does not match its role/backend", + )); + } + Ok(()) +} + +fn decode_intrinsic(bytes: &[u8], cursor: &mut usize) -> RusticolResult { + let has_scale = read_bool(bytes, cursor, "intrinsic scale flag")?; + require_zero(bytes, cursor, 3, "intrinsic reserved bytes")?; + let constant_real_bits = read_u64(bytes, cursor, "intrinsic real bits")?; + let constant_imag_bits = read_u64(bytes, cursor, "intrinsic imaginary bits")?; + let parameter = read_u32(bytes, cursor, "intrinsic parameter index")?; + let runtime_template = read_string(bytes, cursor, "intrinsic runtime template")?; + let scale = if has_scale { + Some(ProcessIntrinsicScale { + constant_real_bits, + constant_imag_bits, + parameter_index: (parameter != DIRECT_NONE_U32).then_some(parameter), + }) + } else { + if constant_real_bits != 0 || constant_imag_bits != 0 || parameter != DIRECT_NONE_U32 { + return Err(RusticolError::integrity( + "non-scale intrinsic has noncanonical scale fields", + )); + } + None + }; + Ok(ProcessIntrinsicExecutor { + runtime_template, + scale, + }) +} + +fn decode_jit(bytes: &[u8], cursor: &mut usize) -> RusticolResult { + let prepared_kernel_id = read_u32(bytes, cursor, "JIT prepared-kernel ID")?; + if prepared_kernel_id == DIRECT_NONE_U32 { + return Err(RusticolError::artifact( + "JIT process executor uses the missing kernel sentinel", + )); + } + let optimization_level = read_u32(bytes, cursor, "JIT optimization level")?; + let plane_compression = read_bool(bytes, cursor, "JIT compression flag")?; + require_zero(bytes, cursor, 3, "JIT reserved bytes")?; + let source_application_sha256 = read_array::<32>(bytes, cursor, "JIT source digest")?; + let source_application_path = read_string(bytes, cursor, "JIT source path")?; + let source_application_abi = read_string(bytes, cursor, "JIT source ABI")?; + let parameter_count = read_u32(bytes, cursor, "JIT parameter-binding count")?; + let plane_count = read_u32(bytes, cursor, "JIT plane-projection count")?; + let scalar_count = read_u32(bytes, cursor, "JIT scalar-projection count")?; + let alias_count = read_u32(bytes, cursor, "JIT output-alias count")?; + let parameter_bindings = (0..parameter_count) + .map(|_| decode_parameter_binding(bytes, cursor)) + .collect::>>()?; + let input_plane_projections = (0..plane_count) + .map(|_| decode_plane_projection(bytes, cursor)) + .collect::>>()?; + let scalar_projections = (0..scalar_count) + .map(|_| decode_scalar_projection(bytes, cursor)) + .collect::>>()?; + let output_alias_inputs = (0..alias_count) + .map(|_| read_u32(bytes, cursor, "JIT output-alias input")) + .collect::>>()?; + for binding in ¶meter_bindings { + let (index, limit) = match *binding { + RecurrenceDirectParameterBindingManifest::Plane { index } => (index, plane_count), + RecurrenceDirectParameterBindingManifest::Scalar { index } => (index, scalar_count), + }; + if index >= limit { + return Err(RusticolError::integrity( + "JIT process executor parameter binding is out of bounds", + )); + } + } + if output_alias_inputs + .iter() + .any(|index| *index >= plane_count) + { + return Err(RusticolError::integrity( + "JIT process executor output alias is out of bounds", + )); + } + Ok(ProcessJitExecutor { + optimization_level, + plane_compression, + source_application_sha256, + source_application_path, + source_application_abi, + parameter_bindings, + input_plane_projections, + scalar_projections, + output_alias_inputs, + }) +} + +fn decode_native(bytes: &[u8], cursor: &mut usize) -> RusticolResult { + let prepared_kernel_id = read_u32(bytes, cursor, "native prepared-kernel ID")?; + if prepared_kernel_id == DIRECT_NONE_U32 { + return Err(RusticolError::artifact( + "native process executor uses the missing kernel sentinel", + )); + } + let has_coupling = read_bool(bytes, cursor, "native coupling flag")?; + require_zero(bytes, cursor, 3, "native reserved bytes")?; + let coupling_real = f64::from_bits(read_u64(bytes, cursor, "native coupling real bits")?); + let coupling_imag = f64::from_bits(read_u64(bytes, cursor, "native coupling imaginary bits")?); + let coupling = if has_coupling { + if !coupling_real.is_finite() || !coupling_imag.is_finite() { + return Err(RusticolError::artifact( + "native process executor coupling is not finite", + )); + } + Some((coupling_real, coupling_imag)) + } else { + if coupling_real.to_bits() != 0 || coupling_imag.to_bits() != 0 { + return Err(RusticolError::integrity( + "native process executor has noncanonical absent coupling bits", + )); + } + None + }; + Ok(ProcessNativeExecutor { + prepared_kernel_id, + library_path: read_string(bytes, cursor, "native library path")?, + native_entry_point: read_string(bytes, cursor, "native entry point")?, + coupling, + }) +} + +fn decode_parameter_binding( + bytes: &[u8], + cursor: &mut usize, +) -> RusticolResult { + let tag = read_u8(bytes, cursor, "JIT parameter-binding tag")?; + require_zero(bytes, cursor, 3, "JIT parameter-binding reserved bytes")?; + let index = read_u32(bytes, cursor, "JIT parameter-binding index")?; + match tag { + 0 => Ok(RecurrenceDirectParameterBindingManifest::Plane { index }), + 1 => Ok(RecurrenceDirectParameterBindingManifest::Scalar { index }), + _ => Err(RusticolError::compatibility( + "unsupported JIT parameter-binding tag", + )), + } +} + +fn decode_plane_projection( + bytes: &[u8], + cursor: &mut usize, +) -> RusticolResult { + let tag = read_u8(bytes, cursor, "JIT plane-projection tag")?; + let operand = read_u8(bytes, cursor, "JIT plane-projection operand")?; + let component = read_u16(bytes, cursor, "JIT plane-projection component")?; + let imaginary = read_bool(bytes, cursor, "JIT plane-projection imaginary flag")?; + require_zero(bytes, cursor, 3, "JIT plane-projection reserved bytes")?; + match tag { + 0 => Ok(RecurrenceDirectPlaneProjectionManifest::ParentCurrent { + parent: operand, + component, + imaginary, + }), + 1 if !imaginary => Ok(RecurrenceDirectPlaneProjectionManifest::Momentum { + operand, + lorentz_component: component, + }), + 2 if operand == 0 => Ok( + RecurrenceDirectPlaneProjectionManifest::DestinationCurrent { + component, + imaginary, + }, + ), + 3 if operand == 0 => Ok( + RecurrenceDirectPlaneProjectionManifest::DestinationAmplitude { + component, + imaginary, + }, + ), + _ => Err(RusticolError::integrity( + "JIT plane-projection record is noncanonical", + )), + } +} + +fn decode_scalar_projection( + bytes: &[u8], + cursor: &mut usize, +) -> RusticolResult { + let tag = read_u8(bytes, cursor, "JIT scalar-projection tag")?; + let imaginary = read_bool(bytes, cursor, "JIT scalar-projection imaginary flag")?; + if read_u16(bytes, cursor, "JIT scalar-projection reserved bytes")? != 0 { + return Err(RusticolError::integrity( + "JIT scalar-projection reserved bytes are nonzero", + )); + } + let index = read_u32(bytes, cursor, "JIT scalar-projection index")?; + let bits = read_u64(bytes, cursor, "JIT scalar-projection bits")?; + match tag { + 0 if index == 0 && bits == 0 => { + Ok(RecurrenceDirectScalarProjectionManifest::ExactFactor { imaginary }) + } + 1 if bits == 0 => { + Ok(RecurrenceDirectScalarProjectionManifest::Parameter { index, imaginary }) + } + 2 if !imaginary && index == 0 => { + let value = f64::from_bits(bits); + if !value.is_finite() { + return Err(RusticolError::artifact( + "JIT literal scalar projection is not finite", + )); + } + Ok(RecurrenceDirectScalarProjectionManifest::Literal { value }) + } + _ => Err(RusticolError::integrity( + "JIT scalar-projection record is noncanonical", + )), + } +} + +pub(super) fn semantic_digest_from_bytes( + bytes: [u8; 32], + context: &str, +) -> RusticolResult { + SemanticDigest::new(bytes) + .map_err(|_| RusticolError::artifact(format!("{context} digest must not be all zero"))) +} + +pub(super) fn read_u8(bytes: &[u8], cursor: &mut usize, context: &str) -> RusticolResult { + let value = *bytes + .get(*cursor) + .ok_or_else(|| RusticolError::artifact(format!("truncated {context}")))?; + *cursor += 1; + Ok(value) +} + +pub(super) fn read_u16(bytes: &[u8], cursor: &mut usize, context: &str) -> RusticolResult { + Ok(u16::from_le_bytes(read_array(bytes, cursor, context)?)) +} + +pub(super) fn read_u32(bytes: &[u8], cursor: &mut usize, context: &str) -> RusticolResult { + Ok(u32::from_le_bytes(read_array(bytes, cursor, context)?)) +} + +pub(super) fn read_u64(bytes: &[u8], cursor: &mut usize, context: &str) -> RusticolResult { + Ok(u64::from_le_bytes(read_array(bytes, cursor, context)?)) +} + +pub(super) fn read_array( + bytes: &[u8], + cursor: &mut usize, + context: &str, +) -> RusticolResult<[u8; N]> { + let end = cursor + .checked_add(N) + .ok_or_else(|| RusticolError::artifact(format!("{context} range overflows")))?; + let value = bytes + .get(*cursor..end) + .ok_or_else(|| RusticolError::artifact(format!("truncated {context}")))? + .try_into() + .map_err(|_| RusticolError::internal("fixed-width process-pack read drifted"))?; + *cursor = end; + Ok(value) +} + +pub(super) fn read_string( + bytes: &[u8], + cursor: &mut usize, + context: &str, +) -> RusticolResult { + let length = usize::try_from(read_u32(bytes, cursor, context)?) + .map_err(|_| RusticolError::artifact(format!("{context} length exceeds usize")))?; + if length == 0 { + return Err(RusticolError::artifact(format!("{context} is empty"))); + } + let end = cursor + .checked_add(length) + .ok_or_else(|| RusticolError::artifact(format!("{context} range overflows")))?; + let raw = bytes + .get(*cursor..end) + .ok_or_else(|| RusticolError::artifact(format!("truncated {context}")))?; + let value = std::str::from_utf8(raw) + .map_err(|_| RusticolError::artifact(format!("{context} is not UTF-8")))? + .to_owned(); + *cursor = end; + Ok(value) +} + +fn read_bool(bytes: &[u8], cursor: &mut usize, context: &str) -> RusticolResult { + match read_u8(bytes, cursor, context)? { + 0 => Ok(false), + 1 => Ok(true), + _ => Err(RusticolError::artifact(format!( + "{context} is not a canonical boolean" + ))), + } +} + +fn require_zero( + bytes: &[u8], + cursor: &mut usize, + count: usize, + context: &str, +) -> RusticolResult<()> { + let end = cursor + .checked_add(count) + .ok_or_else(|| RusticolError::artifact(format!("{context} range overflows")))?; + if bytes + .get(*cursor..end) + .is_none_or(|reserved| reserved.iter().any(|byte| *byte != 0)) + { + return Err(RusticolError::integrity(format!( + "{context} are nonzero or truncated" + ))); + } + *cursor = end; + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::recurrence::{valid_direct_plan_fixture, valid_direct_plan_parts_fixture}; + + fn digest(byte: u8) -> SemanticDigest { + SemanticDigest::new([byte; 32]).unwrap() + } + + fn descriptor( + direct_executor_id: u32, + role: DirectExecutorRole, + ) -> ProcessDirectExecutorDescriptor { + let binding = match role { + DirectExecutorRole::Source => ProcessDirectExecutorBinding::Source, + DirectExecutorRole::Contribution => { + ProcessDirectExecutorBinding::Intrinsic(ProcessIntrinsicExecutor { + runtime_template: "rusticol.test-contribution.v1".to_owned(), + scale: Some(ProcessIntrinsicScale { + constant_real_bits: 1.0_f64.to_bits(), + constant_imag_bits: 0, + parameter_index: None, + }), + }) + } + DirectExecutorRole::Finalization | DirectExecutorRole::Closure => { + ProcessDirectExecutorBinding::Intrinsic(ProcessIntrinsicExecutor { + runtime_template: "rusticol.test-noncontribution.v1".to_owned(), + scale: None, + }) + } + }; + ProcessDirectExecutorDescriptor { + direct_executor_id, + role, + destination_component_count: 1, + uses_exact_factor: role == DirectExecutorRole::Contribution, + binding, + } + } + + fn pack_for(plan: &DirectRecurrencePlan) -> ProcessDirectExecutorPack { + let descriptors = plan + .row_groups() + .iter() + .filter(|group| group.direct_executor_id != DIRECT_NONE_U32) + .map(|group| { + ( + group.direct_executor_id, + descriptor(group.direct_executor_id, group.role), + ) + }) + .collect::>() + .into_values() + .collect(); + ProcessDirectExecutorPack { + identities: ProcessExecutorIdentities { + compiled_model_digest: digest(0x44), + recurrence_template_catalog_digest: digest(0x55), + prepared_kernel_pack_digest: plan.prepared_pack_digest(), + direct_template_catalog_digest: plan.direct_template_catalog_digest(), + runtime_layout_digest: plan.runtime_layout_digest(), + }, + target: ProcessExecutorTarget { + backend: ProcessExecutorBackend::Jit, + target_triple: PREPARED_JIT_PORTABLE_TARGET.to_owned(), + portable: true, + cpu_features: Vec::new(), + }, + catalog_executor_count: plan.direct_executor_count(), + descriptors, + } + } + + fn validate( + pack: &ProcessDirectExecutorPack, + plan: &DirectRecurrencePlan, + ) -> RusticolResult<()> { + pack.validate_for_plan( + plan, + plan.prepared_pack_digest(), + plan.direct_template_catalog_digest(), + plan.runtime_layout_digest(), + ) + } + + #[test] + fn required_executor_set_is_exact_and_role_typed() { + let plan = valid_direct_plan_fixture(); + let pack = pack_for(&plan); + validate(&pack, &plan).unwrap(); + + let mut missing = pack.clone(); + missing.descriptors.pop(); + assert!( + validate(&missing, &plan) + .unwrap_err() + .to_string() + .contains("missing or extra") + ); + + let mut duplicate = pack.clone(); + duplicate.descriptors.push(duplicate.descriptors[0].clone()); + assert!( + validate(&duplicate, &plan) + .unwrap_err() + .to_string() + .contains("repeats an executor ID") + ); + + let mut wrong_role = pack.clone(); + wrong_role.descriptors[0].role = DirectExecutorRole::Closure; + assert!( + validate(&wrong_role, &plan) + .unwrap_err() + .to_string() + .contains("wrong role") + ); + + let mut out_of_range = pack; + out_of_range.descriptors[0].direct_executor_id = plan.direct_executor_count(); + assert!( + validate(&out_of_range, &plan) + .unwrap_err() + .to_string() + .contains("out-of-range") + ); + } + + #[test] + fn required_executor_set_rejects_an_extra_in_range_record() { + let mut parts = valid_direct_plan_parts_fixture(); + parts.direct_executor_count = 5; + let plan = DirectRecurrencePlan::new(parts).unwrap(); + let mut pack = pack_for(&plan); + pack.descriptors + .push(descriptor(4, DirectExecutorRole::Closure)); + + assert!( + validate(&pack, &plan) + .unwrap_err() + .to_string() + .contains("missing or extra") + ); + } + + #[test] + fn decodes_python_source_descriptor_golden() { + // Python struct.pack(", + payloads: &EvaluatorPayloadStore, + physics: &PhysicsRuntime, +) -> RusticolResult> { + let Some(payload) = payload else { + return Ok(None); + }; + if !physics.has_contracted_color_axis() || physics.manifest.color_components.len() != 1 { + return Err(RusticolError::compatibility( + "compiled symmetric-group FFT diagnostic requires one contracted FullColour axis", + )); + } + if physics.manifest.coverage.helicities != "selected" { + return Err(RusticolError::compatibility( + "compiled symmetric-group FFT diagnostic requires generation-time selected helicity coverage", + )); + } + let live_helicity_count = physics + .manifest + .helicities + .iter() + .filter(|helicity| { + helicity.computed && !helicity.structural_zero && helicity.coefficient != 0.0 + }) + .count(); + if live_helicity_count != 1 { + return Err(RusticolError::compatibility( + "compiled symmetric-group FFT diagnostic requires exactly one generation-time selected helicity", + )); + } + let bytes = payloads.packed_member_bytes( + &payload.path, + crate::pacbin::PacbinMemberKind::ColorContraction, + )?; + crate::recurrence::decode_recurrence_color_contraction_v3(bytes).map(Some) +} + +#[cfg(test)] +mod compiled_symmetric_group_load_tests { + use super::*; + + #[test] + fn complete_helicity_coverage_is_rejected_even_with_one_live_helicity() { + let physics = crate::engine::tests::test_physics_runtime("full"); + assert_eq!(physics.manifest.coverage.helicities, "complete"); + assert_eq!( + physics + .manifest + .helicities + .iter() + .filter(|helicity| { + helicity.computed && !helicity.structural_zero && helicity.coefficient != 0.0 + }) + .count(), + 1, + ); + let payload = CompiledColorContractionPayloadManifest { + path: "not-read.pacrclr3".to_string(), + }; + let payloads = EvaluatorPayloadStore::directory(std::path::Path::new(".")); + + let error = + load_compiled_symmetric_group_color_contraction(Some(&payload), &payloads, &physics) + .expect_err("complete helicity coverage must not enter the selected diagnostic"); + + assert_eq!(error.kind(), crate::RusticolErrorKind::Compatibility); + assert!(error.message().contains("selected helicity coverage")); + } +} + +fn validate_compiled_plane_arena_contract(manifest: &ExecutionManifest) -> RusticolResult { + let declared_execution = manifest + .required_runtime_capabilities + .iter() + .any(|value| value == COMPILED_PLANE_ARENA_RUNTIME_CAPABILITY); + let Some(stages) = manifest.compiled.stage_evaluators.as_ref() else { + if declared_execution { + return Err(RusticolError::integrity( + "compiled-plane-arena-v1 is declared without fused stage evaluators", + )); + } + return Ok(false); + }; + let declared_stage = stages + .required_runtime_capabilities + .iter() + .any(|value| value == COMPILED_PLANE_ARENA_RUNTIME_CAPABILITY); + let all_stages = stages + .stages + .iter() + .chain(std::iter::once(&stages.amplitude_stage)) + .collect::>(); + let all_direct_capable = all_stages.iter().all(|stage| { + stage.evaluator.leaf_layout().is_ok_and(|leaves| { + !leaves.is_empty() + && leaves.iter().all(|leaf| { + matches!( + leaf.evaluator, + EvaluatorManifest::SymjitApplication { .. } + | EvaluatorManifest::CompiledComplex { + native_direct_application: Some(_), + .. + } + ) + }) + }) + }); + let direct_count = all_stages + .iter() + .filter(|stage| stage.compiled_plane_arena.is_some()) + .count(); + + if !declared_execution && !declared_stage && direct_count == 0 { + return Err(RusticolError::compatibility( + "this compiled f64 artifact predates compiled-plane-arena-v1; regenerate it with \ + `pyamplicol generate` using the current pyAmpliCol build", + )); + } + if !declared_execution + || !declared_stage + || direct_count != all_stages.len() + || !all_direct_capable + { + return Err(RusticolError::integrity( + "compiled-plane-arena-v1 capability, fused direct-capable leaves, and stage bindings \ + do not form one complete execution contract", + )); + } + let momentum_slot_ids = manifest.compiled.momentum_slot_ids.as_deref().ok_or_else(|| { + RusticolError::compatibility( + "compiled execution omits generation-time momentum-slot selection; regenerate the artifact", + ) + })?; + validate_sorted_unique_ids( + momentum_slot_ids, + manifest.runtime_schema.momentum_slots.len(), + "compiled momentum-slot ids", + )?; + for (position, stage) in all_stages.into_iter().enumerate() { + validate_compiled_plane_arena_stage( + stage, + position == stages.stages.len(), + manifest.runtime_schema.value_storage.component_count, + manifest.runtime_schema.amplitude_stage.output_count, + manifest + .runtime_schema + .parameter_layout + .momentum_parameter_count, + momentum_slot_ids, + )?; + } + Ok(true) +} + +fn validate_compiled_plane_arena_stage( + stage: &GenericSerializedStageEvaluatorManifest, + is_amplitude: bool, + value_component_count: usize, + amplitude_component_count: usize, + momentum_component_count: usize, + momentum_slot_ids: &[usize], +) -> RusticolResult<()> { + let direct = stage.compiled_plane_arena.as_ref().ok_or_else(|| { + RusticolError::integrity("compiled plane-arena fused stage has no direct bindings") + })?; + let symjit_contract = direct.application_abi == COMPILED_PLANE_DIRECT_APPLICATION_ABI + && direct.source_application_abi == COMPILED_PLANE_SOURCE_APPLICATION_ABI; + #[cfg(feature = "f64-compiled")] + let native_contract = direct.application_abi + == super::evaluator::native_compiled_direct::NATIVE_COMPILED_DIRECT_APPLICATION_ABI + && direct.source_application_abi + == super::evaluator::native_compiled_direct::NATIVE_COMPILED_DIRECT_APPLICATION_ABI; + #[cfg(not(feature = "f64-compiled"))] + let native_contract = false; + if direct.schema_version != 1 + || direct.kind != "compiled-plane-arena-stage" + || !(symjit_contract || native_contract) + || direct.element_layout != "split-complex-component-major" + || direct.output_operation != "overwrite" + || direct.output_factor != "identity" + || direct.input_output_aliasing != "forbidden" + || direct.output_output_aliasing != "forbidden" + { + return Err(RusticolError::compatibility(format!( + "compiled plane-arena stage {:?} has an incompatible direct ABI; regenerate the artifact", + stage.evaluator_label + ))); + } + if stage.parameter_layout != "stage-local-value-momentum" + || direct.input_bindings.len() != stage.parameter_count + || direct.output_bindings.len() != stage.output_length + || direct.leaves.is_empty() + { + return Err(RusticolError::integrity(format!( + "compiled plane-arena stage {:?} has incomplete direct bindings", + stage.evaluator_label + ))); + } + u32::try_from(value_component_count) + .and_then(|_| u32::try_from(amplitude_component_count)) + .and_then(|_| u32::try_from(stage.parameter_count)) + .and_then(|_| u32::try_from(stage.output_length)) + .map_err(|_| { + RusticolError::integrity( + "compiled plane-arena plane or scalar address space exceeds u32", + ) + })?; + + let mut canonical_inputs = vec![None; stage.parameter_count]; + for component in &stage.input_components { + let slot = canonical_inputs + .get_mut(component.parameter_index) + .ok_or_else(|| { + RusticolError::integrity( + "compiled plane-arena input binding index is out of bounds", + ) + })?; + if slot + .replace(CompiledPlaneInputBindingManifest { + parameter_index: component.parameter_index, + kind: component.kind.clone(), + source_id: component.source_id, + component: component.component, + global_component: component.global_component, + real_valued: component.real_valued, + }) + .is_some() + { + return Err(RusticolError::integrity( + "compiled plane-arena input bindings contain duplicates", + )); + } + } + if canonical_inputs.iter().any(Option::is_none) + || canonical_inputs + .into_iter() + .map(Option::unwrap) + .ne(direct.input_bindings.iter().cloned()) + { + return Err(RusticolError::integrity(format!( + "compiled plane-arena stage {:?} input bindings disagree with the DAG", + stage.evaluator_label + ))); + } + + let expected_arena = if is_amplitude { "amplitude" } else { "current" }; + let output_limit = if is_amplitude { + amplitude_component_count + } else { + value_component_count + }; + let mut canonical_outputs = vec![None; stage.output_length]; + let mut output_components = BTreeSet::new(); + for slot in &stage.output_slots { + let output_len = slot + .output_stop + .checked_sub(slot.output_start) + .filter(|length| slot.component_stop.checked_sub(slot.component_start) == Some(*length)) + .ok_or_else(|| { + RusticolError::integrity("compiled plane-arena output binding range is invalid") + })?; + if slot.output_stop > stage.output_length || slot.component_stop > output_limit { + return Err(RusticolError::integrity( + "compiled plane-arena output binding is out of bounds", + )); + } + for offset in 0..output_len { + let output_index = slot.output_start + offset; + let component = slot.component_start + offset; + if !output_components.insert(component) + || canonical_outputs[output_index] + .replace(CompiledPlaneOutputBindingManifest { + output_index, + arena: expected_arena.to_string(), + component, + }) + .is_some() + { + return Err(RusticolError::integrity( + "compiled plane-arena output bindings alias", + )); + } + } + } + if canonical_outputs.iter().any(Option::is_none) + || canonical_outputs + .into_iter() + .map(Option::unwrap) + .ne(direct.output_bindings.iter().cloned()) + { + return Err(RusticolError::integrity(format!( + "compiled plane-arena stage {:?} output bindings disagree with the DAG", + stage.evaluator_label + ))); + } + if !is_amplitude { + let input_currents = direct + .input_bindings + .iter() + .filter(|binding| binding.kind == "value") + .map(|binding| binding.global_component) + .collect::>(); + if output_components + .iter() + .any(|component| input_currents.contains(component)) + { + return Err(RusticolError::integrity( + "compiled plane-arena fused stage aliases an input and output plane", + )); + } + } + + let canonical_leaves = stage.evaluator.leaf_layout()?; + if canonical_leaves.len() != direct.leaves.len() { + return Err(RusticolError::integrity( + "compiled plane-arena leaf bindings do not cover the evaluator", + )); + } + for (canonical, binding) in canonical_leaves.iter().zip(&direct.leaves) { + let ( + application_path, + application_abi, + input_len, + output_len, + optimization_level, + supported_optimization_level, + ) = match canonical.evaluator { + EvaluatorManifest::SymjitApplication { + input_len, + output_len, + optimization_level, + plane_application, + .. + } => { + let plane_application = plane_application.as_ref().ok_or_else(|| { + RusticolError::compatibility( + "compiled SymJIT leaf predates the standard plane application ABI; \ + regenerate the artifact", + ) + })?; + plane_application.validate(*input_len, *output_len, *optimization_level)?; + ( + plane_application.application_path.as_str(), + plane_application.application_abi.as_str(), + *input_len, + *output_len, + *optimization_level, + *optimization_level <= 3, + ) + } + EvaluatorManifest::CompiledComplex { + function_name, + input_len, + output_len, + native_direct_application: Some(application), + .. + } => { + application.validate(function_name, *input_len, *output_len)?; + ( + application.library_path.as_str(), + application.application_abi.as_str(), + *input_len, + *output_len, + 3, + true, + ) + } + _ => { + return Err(RusticolError::integrity( + "compiled plane-arena stage contains a leaf without a direct application", + )); + } + }; + if !supported_optimization_level { + return Err(RusticolError::compatibility( + "compiled-plane-arena-v1 supports compiled JIT optimization levels 0 through 3", + )); + } + if binding.optimization_level != optimization_level { + return Err(RusticolError::integrity( + "compiled plane-arena leaf optimization level disagrees with its source payload", + )); + } + if binding.direct_codegen_optimization_level != optimization_level { + return Err(RusticolError::compatibility( + "compiled plane-arena leaf direct code generation must match its standard \ + P-kernel optimization level", + )); + } + if binding.application_path != application_path + || binding.source_application_abi != application_abi + || binding.source_application_abi != direct.source_application_abi + || binding.input_len != input_len + || binding.output_len != output_len + || binding.input_indices != canonical.input_indices + || binding.output_start != canonical.output_range.start + || binding.output_stop != canonical.output_range.end + { + return Err(RusticolError::integrity(format!( + "compiled plane-arena stage {:?} leaf bindings disagree with its source payload", + stage.evaluator_label + ))); + } + for &input_index in &binding.input_indices { + let input = direct.input_bindings.get(input_index).ok_or_else(|| { + RusticolError::integrity("compiled leaf input index is outside its bindings") + })?; + if input.kind == "momentum" { + let component = input + .global_component + .checked_sub(value_component_count) + .filter(|component| *component < momentum_component_count) + .ok_or_else(|| { + RusticolError::integrity( + "compiled leaf momentum input is outside momentum planes", + ) + })?; + if momentum_slot_ids.binary_search(&(component / 4)).is_err() { + return Err(RusticolError::integrity( + "compiled momentum-slot selection omits a leaf input", + )); + } + } + } + } + Ok(()) +} + +pub(super) fn build_lc_topology_replay_mappings( + replay: Option<&LcTopologyReplayManifest>, +) -> RusticolResult<(LcTopologyReplayMappings, Vec)> { + let data = build_lc_topology_replay_data(replay)?; + let mapping_count = data.mappings.len(); + Ok((data.mappings, vec![1.0; mapping_count])) +} + +pub(super) fn build_lc_topology_replay_data( + replay: Option<&LcTopologyReplayManifest>, +) -> RusticolResult { + let Some(replay) = replay else { + return Ok(LcTopologyReplayData::default()); + }; + let contract_version = replay.contract_version.unwrap_or(1); + if !matches!(contract_version, 1 | 2) { + return Err(RusticolError::compatibility(format!( + "unsupported LC topology replay contract version {contract_version}" + ))); + } + if !replay.enabled { + if contract_version == 2 + && (!replay.groups.is_empty() + || replay.replayed_sector_count != 0 + || !replay.materialized_sector_ids.is_empty() + || !replay.residual_sector_ids.is_empty()) + { + return Err(RusticolError::invalid_argument( + "disabled LC topology replay contract v2 contains active coverage", + )); + } + return Ok(LcTopologyReplayData::default()); + } + if replay.mode != "external-label-permutation" { + return Err(RusticolError::invalid_argument(format!( + "unsupported LC topology replay mode {:?}", + replay.mode + ))); + } + let mut routes_by_mapping = + BTreeMap::, Vec>::new(); + let mut replayed_sector_ids = BTreeSet::new(); + let mut group_materialized_sector_ids = BTreeSet::new(); + for group in &replay.groups { + if group.materialized_sector_id != group.representative_sector_id { + return Err(RusticolError::invalid_argument( + "LC topology replay currently requires the materialized sector to be the representative sector", + )); + } + if group.sector_permutations.is_empty() { + return Err(RusticolError::invalid_argument( + "enabled LC topology replay group contains no sector permutations", + )); + } + if contract_version == 2 { + validate_lc_topology_replay_proof(group.proof.as_ref())?; + } + let active_sector_ids = if group.active_sector_ids.is_empty() && contract_version == 1 { + group + .sector_permutations + .iter() + .map(|permutation| permutation.sector_id) + .collect::>() + } else { + group.active_sector_ids.clone() + }; + let active_sector_ids = + unique_lc_sector_ids(&active_sector_ids, "LC topology replay active sector ids")?; + if !active_sector_ids.contains(&group.representative_sector_id) { + return Err(RusticolError::invalid_argument( + "LC topology replay active sector ids do not include the representative sector", + )); + } + if !group_materialized_sector_ids.insert(group.materialized_sector_id) { + return Err(RusticolError::invalid_argument( + "LC topology replay groups reuse a materialized representative sector", + )); + } + for sector_id in &active_sector_ids { + if !replayed_sector_ids.insert(*sector_id) { + return Err(RusticolError::invalid_argument( + "LC topology replay groups overlap in physical sector coverage", + )); + } + } + let mut seen_permutations = BTreeSet::new(); + for permutation in &group.sector_permutations { + if !active_sector_ids.contains(&permutation.sector_id) { + return Err(RusticolError::invalid_argument( + "LC topology replay sector permutation is not listed in active sector ids", + )); + } + if !seen_permutations.insert(permutation.sector_id) { + return Err(RusticolError::invalid_argument( + "LC topology replay sector permutations contain duplicate sector ids", + )); + } + if !permutation.weight.is_finite() || permutation.weight <= 0.0 { + return Err(RusticolError::invalid_argument( + "LC topology replay sector permutation weights must be positive finite numbers", + )); + } + if !matches!(permutation.sign, -1 | 1) { + return Err(RusticolError::invalid_argument( + "LC topology replay permutation signs must be -1 or 1", + )); + } + let expected_factor = permutation.weight * f64::from(permutation.sign); + let amplitude_factor = match permutation.factor.as_deref() { + Some([real, imaginary]) => { + if !real.is_finite() + || !imaginary.is_finite() + || *imaginary != 0.0 + || real.to_bits() != expected_factor.to_bits() + { + return Err(RusticolError::invalid_argument( + "LC topology replay signed factor does not match its weight and sign", + )); + } + [*real, *imaginary] + } + Some(_) => { + return Err(RusticolError::invalid_argument( + "LC topology replay signed factor must contain real and imaginary components", + )); + } + None if contract_version == 1 => [expected_factor, 0.0], + None => { + return Err(RusticolError::invalid_argument( + "LC topology replay contract v2 permutation is missing its signed factor", + )); + } + }; + let mapping = normalized_lc_topology_label_permutation(&permutation.label_permutation)?; + let route = LcTopologyReplaySectorRoute { + physical_sector_id: permutation.sector_id, + materialized_sector_id: group.materialized_sector_id, + weight: permutation.weight, + sign: permutation.sign, + amplitude_factor, + residual: false, + }; + if route.squared_reduction_weight().to_bits() != route.weight.to_bits() { + return Err(RusticolError::invalid_argument( + "LC topology replay signed factor has inconsistent squared reduction semantics", + )); + } + routes_by_mapping.entry(mapping).or_default().push(route); + } + if seen_permutations != active_sector_ids { + return Err(RusticolError::invalid_argument( + "LC topology replay sector permutations do not exactly cover the active sectors", + )); + } + } + if replayed_sector_ids.len() != replay.replayed_sector_count { + return Err(RusticolError::invalid_argument(format!( + "LC topology replay declares {} sectors but contains {} permutations", + replay.replayed_sector_count, + replayed_sector_ids.len() + ))); + } + let residual_sector_ids = unique_lc_sector_ids( + &replay.residual_sector_ids, + "LC topology replay residual sector ids", + )?; + if replayed_sector_ids + .intersection(&residual_sector_ids) + .next() + .is_some() + { + return Err(RusticolError::invalid_argument( + "LC topology replay residual sectors overlap replayed sectors", + )); + } + for sector_id in &residual_sector_ids { + routes_by_mapping + .entry(Vec::new()) + .or_default() + .push(LcTopologyReplaySectorRoute { + physical_sector_id: *sector_id, + materialized_sector_id: *sector_id, + weight: 1.0, + sign: 1, + amplitude_factor: [1.0, 0.0], + residual: true, + }); + } + + let mut materialized_sector_ids = group_materialized_sector_ids; + materialized_sector_ids.extend(residual_sector_ids.iter().copied()); + if contract_version == 2 { + let declared_materialized = unique_lc_sector_ids( + &replay.materialized_sector_ids, + "LC topology replay materialized sector ids", + )?; + if declared_materialized != materialized_sector_ids { + return Err(RusticolError::invalid_argument( + "LC topology replay materialized coverage does not match groups and residuals", + )); + } + let physical_sector_count = replay.physical_sector_count.ok_or_else(|| { + RusticolError::invalid_argument( + "LC topology replay contract v2 is missing physical_sector_count", + ) + })?; + let mut physical_sector_ids = replayed_sector_ids.clone(); + physical_sector_ids.extend(residual_sector_ids.iter().copied()); + let expected = (0..physical_sector_count) + .map(|sector_id| sector_id as i64) + .collect::>(); + if physical_sector_ids != expected { + return Err(RusticolError::invalid_argument( + "LC topology replay contract v2 does not cover every physical sector exactly once", + )); + } + } else if !replay.materialized_sector_ids.is_empty() + || !replay.residual_sector_ids.is_empty() + || replay.physical_sector_count.is_some() + { + return Err(RusticolError::invalid_argument( + "legacy LC topology replay contract contains contract-v2 coverage fields", + )); + } + + let mut mappings = Vec::with_capacity(routes_by_mapping.len()); + let mut routes = Vec::with_capacity(routes_by_mapping.len()); + for (mapping, mut mapping_routes) in routes_by_mapping { + mapping_routes.sort_by_key(|route| route.physical_sector_id); + mappings.push(mapping); + routes.push(mapping_routes); + } + Ok(LcTopologyReplayData { + mappings, + routes, + materialized_sector_ids, + }) +} + +pub(super) fn build_color_topology_replay_data( + replay: Option<&ColorTopologyReplayManifest>, + color_accuracy: &str, +) -> RusticolResult { + let Some(replay) = replay else { + return Ok(LcTopologyReplayData::default()); + }; + if !replay.enabled { + if !replay.groups.is_empty() + || replay.replayed_sector_count != 0 + || !replay.materialized_sector_ids.is_empty() + || !replay.residual_sector_ids.is_empty() + { + return Err(RusticolError::artifact( + "disabled color topology replay contains active coverage", + )); + } + return Ok(LcTopologyReplayData::default()); + } + if !matches!(color_accuracy, "nlc" | "full") + || replay.color_accuracy != color_accuracy + || replay.mode != "external-label-permutation" + || replay.contract_version != Some(3) + { + return Err(RusticolError::artifact( + "color topology replay is not bound to the NLC/full artifact accuracy", + )); + } + let compatibility = LcTopologyReplayManifest { + enabled: replay.enabled, + mode: replay.mode.clone(), + contract_version: Some(2), + physical_sector_count: replay.physical_sector_count, + replayed_sector_count: replay.replayed_sector_count, + materialized_sector_ids: replay.materialized_sector_ids.clone(), + residual_sector_ids: replay.residual_sector_ids.clone(), + groups: replay.groups.clone(), + }; + build_lc_topology_replay_data(Some(&compatibility)) +} + +fn validate_lc_topology_replay_proof( + proof: Option<&LcTopologyReplayProofManifest>, +) -> RusticolResult<()> { + let proof = proof.ok_or_else(|| { + RusticolError::invalid_argument( + "LC topology replay contract v2 group is missing proof metadata", + ) + })?; + let algorithm = proof.algorithm.as_deref().unwrap_or_default(); + let digest = proof.digest.as_deref().unwrap_or_default(); + if proof.status != "proven" + || algorithm.is_empty() + || digest.len() != 64 + || !digest + .bytes() + .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte)) + { + return Err(RusticolError::invalid_argument( + "LC topology replay contract v2 requires a proven algorithm and lowercase SHA-256 digest", + )); + } + Ok(()) +} + +fn unique_lc_sector_ids(values: &[i64], description: &str) -> RusticolResult> { + let result = values.iter().copied().collect::>(); + if result.len() != values.len() || result.iter().any(|sector_id| *sector_id < 0) { + return Err(RusticolError::invalid_argument(format!( + "{description} contain duplicates or negative ids" + ))); + } + Ok(result) +} + +fn normalized_lc_topology_label_permutation( + values: &[LcTopologyReplayLabelPermutationManifest], +) -> RusticolResult> { + let mut representatives = BTreeSet::new(); + let mut sectors = BTreeSet::new(); + let mut mapping = Vec::new(); + for item in values { + if item.representative_label == 0 || item.sector_label == 0 { + return Err(RusticolError::invalid_argument( + "LC topology replay label permutations must use one-based labels", + )); + } + let representative = item.representative_label - 1; + let sector = item.sector_label - 1; + if !representatives.insert(representative) || !sectors.insert(sector) { + return Err(RusticolError::invalid_argument( + "LC topology replay label permutation is not one-to-one", + )); + } + if representative != sector { + mapping.push((representative, sector)); + } + } + if representatives != sectors { + return Err(RusticolError::invalid_argument( + "LC topology replay label permutation is not a permutation of its support", + )); + } + mapping.sort_unstable(); + Ok(mapping) +} + +#[derive(Clone, Debug)] +struct HelicityRecurrenceSchemaCurrent { + is_source: bool, + external_labels: Vec, +} + +#[derive(Clone, Debug)] +struct HelicityRecurrenceSchemaSource { + external_label: usize, + helicity: i32, + chirality: i32, + spin_state: GenericSourceSpinStateManifest, + declared_states: Vec, +} + +#[derive(Clone, Copy, Debug)] +struct HelicityRecurrenceSchemaRoot { + output_index: usize, + left_current_id: usize, + right_current_id: usize, + helicity_weight: f64, +} + +#[derive(Clone, Debug)] +struct HelicityCompiledStageChunkCoverage { + current_ids_by_chunk: Vec>, + input_current_ids_by_chunk: Vec>, +} + +#[derive(Clone, Debug)] +struct HelicityCompiledChunkCoverage { + stages: Vec, + amplitude_root_ids_by_chunk: Vec>, + amplitude_input_current_ids_by_chunk: Vec>, +} + +#[derive(Clone, Debug)] +struct HelicityRecurrenceSchemaView { + external_count: usize, + currents: Vec, + sources: BTreeMap, + roots: Vec, + compiled_chunks: Option, +} + +impl HelicityRecurrenceSchemaView { + fn from_execution_manifest(manifest: &ExecutionManifest) -> RusticolResult { + let currents = manifest + .runtime_schema + .current_storage + .current_slots + .iter() + .map(|slot| HelicityRecurrenceSchemaCurrent { + is_source: slot.is_source, + external_labels: slot.external_labels.clone(), + }) + .collect::>(); + let mut sources = BTreeMap::new(); + for source in &manifest.runtime_schema.source_fill.sources { + let declared_states = source + .source_ir + .states + .iter() + .map(|state| { + state.transformed(&source.applied_crossing).map_err(|message| { + RusticolError::artifact(format!( + "helicity recurrence source current {} has an invalid declared state: {message}", + source.current_id + )) + }) + }) + .collect::>>()?; + let spin_state = serde_json::from_value(source.spin_state.clone()).map_err(|_| { + RusticolError::artifact(format!( + "helicity recurrence source current {} has an invalid spin state", + source.current_id + )) + })?; + let record = HelicityRecurrenceSchemaSource { + external_label: source.leg_label, + helicity: source.source_helicity, + chirality: source.chirality, + spin_state, + declared_states, + }; + if sources.insert(source.current_id, record).is_some() { + return Err(RusticolError::artifact(format!( + "helicity recurrence runtime schema repeats source current {}", + source.current_id + ))); + } + } + let roots = manifest + .runtime_schema + .amplitude_stage + .roots + .iter() + .map(|root| HelicityRecurrenceSchemaRoot { + output_index: root.output_index, + left_current_id: root.left_current_id, + right_current_id: root.right_current_id, + helicity_weight: root.helicity_weight, + }) + .collect::>(); + let compiled_chunks = if manifest + .runtime_schema + .helicity_recurrence + .as_ref() + .is_some_and(|recurrence| recurrence.materialization.is_some()) + { + manifest + .compiled + .stage_evaluators + .as_ref() + .map(|evaluators| { + build_helicity_compiled_chunk_coverage(evaluators, &manifest.runtime_schema) + }) + .transpose()? + } else { + None + }; + Ok(Self { + external_count: manifest.runtime_schema.external_particles.len(), + currents, + sources, + roots, + compiled_chunks, + }) + } +} + +#[derive(Clone, Debug)] +struct CompiledValueInputLayout { + current_ids_by_slot: BTreeMap, + current_ids_by_global_component: Vec>, +} + +fn compiled_value_input_layout(schema: &ExecutionPlan) -> RusticolResult { + let current_count = schema.current_storage.current_slots.len(); + let mut current_ids_by_slot = BTreeMap::new(); + let mut current_ids_by_global_component = vec![None; schema.value_storage.component_count]; + for slot in &schema.value_storage.value_slots { + if slot.current_id >= current_count + || slot.component_start >= slot.component_stop + || slot.component_stop > current_ids_by_global_component.len() + { + return Err(RusticolError::artifact(format!( + "value slot {} has stale current {} or component coverage", + slot.value_slot_id, slot.current_id + ))); + } + if current_ids_by_slot + .insert(slot.value_slot_id, slot.current_id) + .is_some() + { + return Err(RusticolError::artifact(format!( + "runtime schema repeats value slot {}", + slot.value_slot_id + ))); + } + for owner in &mut current_ids_by_global_component[slot.component_start..slot.component_stop] + { + if owner.replace(slot.current_id).is_some() { + return Err(RusticolError::artifact(format!( + "value slot {} overlaps another global value component range", + slot.value_slot_id + ))); + } + } + } + Ok(CompiledValueInputLayout { + current_ids_by_slot, + current_ids_by_global_component, + }) +} + +fn compiled_input_current_ids_by_chunk( + stage: &GenericSerializedStageEvaluatorManifest, + value_inputs: &CompiledValueInputLayout, + current_count: usize, +) -> RusticolResult>> { + if stage.parameter_layout != "stage-local-value-momentum" { + return Err(RusticolError::artifact(format!( + "compiled selector stage {:?} requires stage-local input metadata", + stage.evaluator_label + ))); + } + let evaluator_input_len = stage.evaluator.io_len()?.0; + if evaluator_input_len != stage.parameter_count { + return Err(RusticolError::artifact(format!( + "compiled selector stage {:?} evaluator input count does not match its parameter layout", + stage.evaluator_label + ))); + } + let mut components_by_parameter = vec![None; stage.parameter_count]; + for component in &stage.input_components { + if component.parameter_index >= stage.parameter_count + || components_by_parameter[component.parameter_index] + .replace(component) + .is_some() + { + return Err(RusticolError::artifact(format!( + "compiled selector stage {:?} has duplicate or stale input-component metadata", + stage.evaluator_label + ))); + } + } + if components_by_parameter.iter().any(Option::is_none) { + return Err(RusticolError::artifact(format!( + "compiled selector stage {:?} has incomplete input-component metadata", + stage.evaluator_label + ))); + } + + stage + .evaluator + .leaf_layout()? + .into_iter() + .map(|leaf| { + let mut current_ids = BTreeSet::new(); + for parameter_index in leaf.input_indices { + let component = components_by_parameter + .get(parameter_index) + .and_then(|component| *component) + .ok_or_else(|| { + RusticolError::artifact(format!( + "compiled selector stage {:?} chunk references an absent input component", + stage.evaluator_label + )) + })?; + match component.kind.as_str() { + "value" => { + let declared_current_id = value_inputs + .current_ids_by_slot + .get(&component.source_id) + .copied() + .ok_or_else(|| { + RusticolError::artifact(format!( + "compiled selector stage {:?} references absent value slot {}", + stage.evaluator_label, component.source_id + )) + })?; + let current_id = value_inputs + .current_ids_by_global_component + .get(component.global_component) + .copied() + .flatten() + .ok_or_else(|| { + RusticolError::artifact(format!( + "compiled selector stage {:?} references unowned global value component {}", + stage.evaluator_label, component.global_component + )) + })?; + if current_id != declared_current_id { + return Err(RusticolError::artifact(format!( + "compiled selector stage {:?} value-slot and global-component dependencies disagree", + stage.evaluator_label + ))); + } + if current_id >= current_count { + return Err(RusticolError::artifact(format!( + "compiled selector stage {:?} references stale current {current_id}", + stage.evaluator_label + ))); + } + current_ids.insert(current_id); + } + "momentum" | "model_parameter" => {} + kind => { + return Err(RusticolError::artifact(format!( + "compiled selector stage {:?} has unsupported input kind {kind:?}", + stage.evaluator_label + ))); + } + } + } + Ok(current_ids) + }) + .collect() +} + +fn build_helicity_compiled_chunk_coverage( + evaluators: &GenericStageEvaluatorArtifactsManifest, + schema: &ExecutionPlan, +) -> RusticolResult { + let current_count = schema.current_storage.current_slots.len(); + let roots = schema + .amplitude_stage + .roots + .iter() + .map(|root| HelicityRecurrenceSchemaRoot { + output_index: root.output_index, + left_current_id: root.left_current_id, + right_current_id: root.right_current_id, + helicity_weight: root.helicity_weight, + }) + .collect::>(); + let value_inputs = compiled_value_input_layout(schema)?; + build_helicity_compiled_chunk_coverage_from_parts( + evaluators, + current_count, + &roots, + &value_inputs, + ) +} + +fn build_helicity_compiled_chunk_coverage_from_parts( + evaluators: &GenericStageEvaluatorArtifactsManifest, + current_count: usize, + roots: &[HelicityRecurrenceSchemaRoot], + value_inputs: &CompiledValueInputLayout, +) -> RusticolResult { + let mut stages = Vec::with_capacity(evaluators.stages.len()); + for stage in &evaluators.stages { + let ranges = evaluator_output_chunk_ranges(&stage.evaluator)?; + validate_evaluator_output_ranges(&ranges, stage.output_length, &stage.evaluator_label)?; + let input_current_ids_by_chunk = + compiled_input_current_ids_by_chunk(stage, value_inputs, current_count)?; + if input_current_ids_by_chunk.len() != ranges.len() { + return Err(RusticolError::artifact(format!( + "compiled selector stage {:?} input and output chunk counts disagree", + stage.evaluator_label + ))); + } + let mut current_ids_by_chunk = vec![BTreeSet::new(); ranges.len()]; + for slot in &stage.output_slots { + let current_id = usize::try_from(slot.current_id).map_err(|_| { + RusticolError::artifact(format!( + "helicity recurrence stage {} has a non-current output slot", + stage.stage_index + )) + })?; + if current_id >= current_count + || slot.output_start >= slot.output_stop + || slot.output_stop > stage.output_length + { + return Err(RusticolError::artifact(format!( + "helicity recurrence stage {} has a stale current or output range", + stage.stage_index + ))); + } + let mut covered = false; + for (chunk_index, (start, stop)) in ranges.iter().copied().enumerate() { + if slot.output_start < stop && start < slot.output_stop { + current_ids_by_chunk[chunk_index].insert(current_id); + covered = true; + } + } + if !covered { + return Err(RusticolError::artifact(format!( + "helicity recurrence stage {} output for current {current_id} is not covered by an evaluator chunk", + stage.stage_index + ))); + } + } + stages.push(HelicityCompiledStageChunkCoverage { + current_ids_by_chunk, + input_current_ids_by_chunk, + }); + } + + let amplitude = &evaluators.amplitude_stage; + let ranges = evaluator_output_chunk_ranges(&litude.evaluator)?; + validate_evaluator_output_ranges(&ranges, amplitude.output_length, &litude.evaluator_label)?; + let amplitude_input_current_ids_by_chunk = + compiled_input_current_ids_by_chunk(amplitude, value_inputs, current_count)?; + if amplitude_input_current_ids_by_chunk.len() != ranges.len() { + return Err(RusticolError::artifact( + "compiled selector amplitude input and output chunk counts disagree", + )); + } + let mut amplitude_root_ids_by_chunk = vec![BTreeSet::new(); ranges.len()]; + let mut output_owner = vec![None; amplitude.output_length]; + let mut evaluator_column_by_root_output = vec![None; amplitude.output_length]; + for slot in &litude.output_slots { + if slot.current_id != -1 + || slot.output_start >= slot.output_stop + || slot.output_stop > amplitude.output_length + { + return Err(RusticolError::artifact( + "helicity recurrence amplitude evaluator has an invalid output slot", + )); + } + let output_len = slot.output_stop - slot.output_start; + if slot.component_stop.checked_sub(slot.component_start) != Some(output_len) { + return Err(RusticolError::artifact( + "helicity recurrence amplitude evaluator has inconsistent output mapping", + )); + } + for offset in 0..output_len { + let evaluator_output = slot.output_start + offset; + let root_output = slot.component_start + offset; + if root_output >= amplitude.output_length + || output_owner[evaluator_output].replace(()).is_some() + || evaluator_column_by_root_output[root_output] + .replace(evaluator_output) + .is_some() + { + return Err(RusticolError::artifact( + "helicity recurrence amplitude evaluator has overlapping output slots", + )); + } + } + } + for (root_id, root) in roots.iter().enumerate() { + let evaluator_output = evaluator_column_by_root_output + .get(root.output_index) + .copied() + .flatten() + .ok_or_else(|| { + RusticolError::artifact(format!( + "helicity recurrence amplitude root {root_id} is not covered by an evaluator output slot" + )) + })?; + if output_owner[evaluator_output].is_none() { + return Err(RusticolError::artifact(format!( + "helicity recurrence amplitude root {root_id} is not covered by an evaluator output slot" + ))); + } + let chunk_index = ranges + .iter() + .position(|(start, stop)| *start <= evaluator_output && evaluator_output < *stop) + .ok_or_else(|| { + RusticolError::artifact(format!( + "helicity recurrence amplitude root {root_id} is not covered by an evaluator chunk" + )) + })?; + amplitude_root_ids_by_chunk[chunk_index].insert(root_id); + } + Ok(HelicityCompiledChunkCoverage { + stages, + amplitude_root_ids_by_chunk, + amplitude_input_current_ids_by_chunk, + }) +} + +fn validate_compiled_selector_schedule_closure( + context: &str, + current_is_source: &[bool], + chunks: &HelicityCompiledChunkCoverage, + active_stage_chunk_indices: &[Vec], + active_amplitude_chunk_indices: &[usize], + allowed_current_ids: Option<&BTreeSet>, + allowed_root_ids: Option<&BTreeSet>, +) -> RusticolResult<()> { + if active_stage_chunk_indices.len() != chunks.stages.len() { + return Err(RusticolError::artifact(format!( + "{context} has {} stage schedules, expected {}", + active_stage_chunk_indices.len(), + chunks.stages.len() + ))); + } + let mut available_current_ids = current_is_source + .iter() + .enumerate() + .filter_map(|(current_id, is_source)| { + (*is_source && allowed_current_ids.is_none_or(|allowed| allowed.contains(¤t_id))) + .then_some(current_id) + }) + .collect::>(); + + for (stage_index, (stage, active_chunks)) in chunks + .stages + .iter() + .zip(active_stage_chunk_indices) + .enumerate() + { + validate_sorted_unique_ids( + active_chunks, + stage.current_ids_by_chunk.len(), + &format!("{context} stage {stage_index} active chunks"), + )?; + let mut stage_outputs = BTreeSet::new(); + for chunk_index in active_chunks { + let output_current_ids = &stage.current_ids_by_chunk[*chunk_index]; + if let Some(allowed) = allowed_current_ids + && !output_current_ids.is_subset(allowed) + { + let foreign = output_current_ids + .difference(allowed) + .copied() + .collect::>(); + return Err(RusticolError::artifact(format!( + "{context} stage {stage_index} evaluator chunk {chunk_index} crosses selector domains through currents {foreign:?}", + ))); + } + let missing = stage.input_current_ids_by_chunk[*chunk_index] + .difference(&available_current_ids) + .copied() + .collect::>(); + if !missing.is_empty() { + return Err(RusticolError::artifact(format!( + "{context} stage {stage_index} evaluator chunk {chunk_index} is missing compiled ancestors {missing:?}", + ))); + } + stage_outputs.extend(output_current_ids.iter().copied()); + } + available_current_ids.extend(stage_outputs); + } + + validate_sorted_unique_ids( + active_amplitude_chunk_indices, + chunks.amplitude_root_ids_by_chunk.len(), + &format!("{context} active amplitude chunks"), + )?; + for chunk_index in active_amplitude_chunk_indices { + let root_ids = &chunks.amplitude_root_ids_by_chunk[*chunk_index]; + if let Some(allowed) = allowed_root_ids + && !root_ids.is_subset(allowed) + { + let foreign = root_ids.difference(allowed).copied().collect::>(); + return Err(RusticolError::artifact(format!( + "{context} amplitude evaluator chunk {chunk_index} crosses selector domains through roots {foreign:?}", + ))); + } + let missing = chunks.amplitude_input_current_ids_by_chunk[*chunk_index] + .difference(&available_current_ids) + .copied() + .collect::>(); + if !missing.is_empty() { + return Err(RusticolError::artifact(format!( + "{context} amplitude evaluator chunk {chunk_index} is missing compiled ancestors {missing:?}", + ))); + } + } + Ok(()) +} + +pub(super) fn build_compiled_color_execution_plan( + evaluators: &GenericStageEvaluatorArtifactsManifest, + schema: &ExecutionPlan, + replay_materialized_sector_ids: &BTreeSet, +) -> RusticolResult> { + let declared_materialized_sector_ids = evaluators + .stages + .iter() + .chain(std::iter::once(&evaluators.amplitude_stage)) + .flat_map(|stage| &stage.output_slots) + .flat_map(|slot| slot.color_selector_domain_ids.iter().copied()) + .collect::>(); + if declared_materialized_sector_ids.is_empty() { + return Ok(None); + } + if !replay_materialized_sector_ids.is_empty() + && &declared_materialized_sector_ids != replay_materialized_sector_ids + { + return Err(RusticolError::artifact( + "compiled color-selector metadata does not match LC topology-replay sectors", + )); + } + let materialized_sector_ids = &declared_materialized_sector_ids; + let chunks = build_helicity_compiled_chunk_coverage(evaluators, schema)?; + let current_is_source = schema + .current_storage + .current_slots + .iter() + .map(|current| current.is_source) + .collect::>(); + + let mut active_stages = materialized_sector_ids + .iter() + .map(|sector_id| (*sector_id, Vec::with_capacity(evaluators.stages.len()))) + .collect::>(); + for stage in &evaluators.stages { + let domains_by_chunk = compiled_color_domains_by_chunk(stage, materialized_sector_ids)?; + for sector_id in materialized_sector_ids { + active_stages + .get_mut(sector_id) + .expect("materialized color schedule initialized") + .push( + domains_by_chunk + .iter() + .enumerate() + .filter_map(|(chunk_index, domains)| { + domains.contains(sector_id).then_some(chunk_index) + }) + .collect(), + ); + } + } + + let amplitude_domains = + compiled_color_domains_by_chunk(&evaluators.amplitude_stage, materialized_sector_ids)?; + let mut schedules_by_materialized_sector = BTreeMap::new(); + for sector_id in materialized_sector_ids { + let active_amplitude_chunk_indices = amplitude_domains + .iter() + .enumerate() + .filter_map(|(chunk_index, domains)| domains.contains(sector_id).then_some(chunk_index)) + .collect::>(); + if active_amplitude_chunk_indices.is_empty() { + return Err(RusticolError::artifact(format!( + "compiled color selector sector {sector_id} has no amplitude chunk coverage" + ))); + } + let active_stage_chunk_indices = active_stages + .remove(sector_id) + .expect("materialized color schedule initialized"); + validate_compiled_selector_schedule_closure( + &format!("compiled color selector sector {sector_id}"), + ¤t_is_source, + &chunks, + &active_stage_chunk_indices, + &active_amplitude_chunk_indices, + None, + None, + )?; + schedules_by_materialized_sector.insert( + *sector_id, + Arc::new(CompiledColorSelectorSchedule { + active_stage_chunk_indices, + active_amplitude_chunk_indices, + }), + ); + } + Ok(Some(CompiledColorExecutionPlan { + schedules_by_materialized_sector, + })) +} + +fn compiled_color_domains_by_chunk( + stage: &GenericSerializedStageEvaluatorManifest, + materialized_sector_ids: &BTreeSet, +) -> RusticolResult>> { + let ranges = evaluator_output_chunk_ranges(&stage.evaluator)?; + validate_evaluator_output_ranges(&ranges, stage.output_length, &stage.evaluator_label)?; + let mut domains_by_chunk = vec![None; ranges.len()]; + for slot in &stage.output_slots { + if slot.output_start >= slot.output_stop || slot.output_stop > stage.output_length { + return Err(RusticolError::artifact(format!( + "compiled color selector stage {:?} has an invalid output slot", + stage.evaluator_label + ))); + } + let domains = slot + .color_selector_domain_ids + .iter() + .copied() + .collect::>(); + if domains.len() != slot.color_selector_domain_ids.len() + || !domains.is_subset(materialized_sector_ids) + { + return Err(RusticolError::artifact(format!( + "compiled color selector stage {:?} has invalid materialized-sector coverage", + stage.evaluator_label + ))); + } + for (chunk_index, (start, stop)) in ranges.iter().copied().enumerate() { + if slot.output_start < stop && start < slot.output_stop { + if let Some(previous) = &domains_by_chunk[chunk_index] { + if previous != &domains { + return Err(RusticolError::artifact(format!( + "compiled color selector stage {:?} evaluator chunk {chunk_index} crosses selector domains", + stage.evaluator_label + ))); + } + } else { + domains_by_chunk[chunk_index] = Some(domains.clone()); + } + } + } + } + domains_by_chunk + .into_iter() + .enumerate() + .map(|(chunk_index, domains)| { + domains.ok_or_else(|| { + RusticolError::artifact(format!( + "compiled color selector stage {:?} evaluator chunk {chunk_index} has no output ownership", + stage.evaluator_label + )) + }) + }) + .collect() +} + +fn evaluator_output_chunk_ranges( + evaluator: &EvaluatorManifest, +) -> RusticolResult> { + evaluator.leaf_layout().map(|layout| { + layout + .into_iter() + .map(|leaf| (leaf.output_range.start, leaf.output_range.end)) + .collect() + }) +} + +fn validate_evaluator_output_ranges( + ranges: &[(usize, usize)], + expected_output_length: usize, + label: &str, +) -> RusticolResult<()> { + if ranges.is_empty() + || ranges.first().map(|range| range.0) != Some(0) + || ranges.last().map(|range| range.1) != Some(expected_output_length) + || ranges + .windows(2) + .any(|pair| pair[0].1 != pair[1].0 || pair[0].0 >= pair[0].1) + { + return Err(RusticolError::artifact(format!( + "helicity recurrence evaluator {label:?} has inconsistent chunk output ranges" + ))); + } + Ok(()) +} + +#[derive(Clone, Copy, Debug)] +struct HelicityCurrentOwner { + class_index: usize, + selector_domain_id: usize, + factor: [f64; 2], +} + +fn build_helicity_recurrence_runtime( + manifest: &ExecutionManifest, +) -> RusticolResult> { + let recurrence = manifest.runtime_schema.helicity_recurrence.as_ref(); + let view = HelicityRecurrenceSchemaView::from_execution_manifest(manifest)?; + build_helicity_recurrence_runtime_from_view(recurrence, &view) +} + +fn detach_compiled_helicity_load_inputs( + manifest: &mut ExecutionManifest, +) -> RusticolResult<( + Option, + Option, +)> { + let helicity_recurrence = build_helicity_recurrence_runtime(manifest)?; + let stage_evaluators = manifest.compiled.stage_evaluators.take(); + // The authenticated runtime owns every derived selector schedule. Hide the + // source contract from the metadata-only constructor after its serialized + // evaluators have moved into the payload loader so it cannot rederive the + // schedules against an artificially empty evaluator set. + manifest.runtime_schema.helicity_recurrence = None; + Ok((stage_evaluators, helicity_recurrence)) +} + +fn build_helicity_recurrence_runtime_from_view( + recurrence: Option<&HelicityRecurrenceManifest>, + schema: &HelicityRecurrenceSchemaView, +) -> RusticolResult> { + let Some(recurrence) = recurrence else { + return Ok(None); + }; + if recurrence.kind != HELICITY_RECURRENCE_KIND { + return Err(RusticolError::artifact(format!( + "unsupported helicity recurrence kind {:?}", + recurrence.kind + ))); + } + if recurrence.contract_version != HELICITY_RECURRENCE_CONTRACT_VERSION { + return Err(RusticolError::compatibility(format!( + "unsupported helicity recurrence contract version {}; regenerate the artifact", + recurrence.contract_version + ))); + } + if recurrence.proof_algorithm != HELICITY_RECURRENCE_PROOF_ALGORITHM { + return Err(RusticolError::artifact(format!( + "unsupported helicity recurrence proof algorithm {:?}", + recurrence.proof_algorithm + ))); + } + if recurrence.materialization.is_none() + && (recurrence.current_count != schema.currents.len() + || recurrence.amplitude_root_count != schema.roots.len()) + { + return Err(RusticolError::artifact( + "helicity recurrence counts do not match the final runtime schema", + )); + } + + let mut domain_keys = BTreeSet::new(); + let mut selector_domains = Vec::with_capacity(recurrence.selector_domains.len()); + for (expected_id, domain) in recurrence.selector_domains.iter().enumerate() { + if domain.id != expected_id { + return Err(RusticolError::artifact(format!( + "helicity selector domain ids must be contiguous; expected {expected_id}, got {}", + domain.id + ))); + } + let labels = validate_helicity_external_labels( + domain + .source_states + .iter() + .map(|state| state.external_label), + schema.external_count, + &format!("helicity selector domain {expected_id}"), + )?; + let is_complete = labels.len() == schema.external_count + && labels + .iter() + .enumerate() + .all(|(index, label)| *label == index + 1); + if domain.complete != is_complete { + return Err(RusticolError::artifact(format!( + "helicity selector domain {expected_id} has inconsistent complete coverage" + ))); + } + let key = domain + .source_states + .iter() + .map(|state| (state.external_label, state.helicity)) + .collect::>(); + if !domain_keys.insert(key.clone()) { + return Err(RusticolError::artifact( + "helicity selector domains repeat a source-state assignment", + )); + } + selector_domains.push(HelicitySelectorDomainRuntime { + complete: domain.complete, + source_states: key + .into_iter() + .map(|(label, helicity)| (label - 1, helicity)) + .collect(), + }); + } + + let mut class_ids = BTreeMap::new(); + let mut current_owners = vec![None; recurrence.current_count]; + let mut recurrence_classes = Vec::with_capacity(recurrence.recurrence_classes.len()); + for (class_index, class) in recurrence.recurrence_classes.iter().enumerate() { + validate_helicity_recurrence_proof( + &class.class_id, + "helicity-current-sha256:", + &class.proof, + )?; + if class_ids + .insert(class.class_id.as_str(), class_index) + .is_some() + { + return Err(RusticolError::artifact( + "helicity recurrence class ids must be unique", + )); + } + if class.members.is_empty() { + return Err(RusticolError::artifact(format!( + "helicity recurrence class {:?} has no members", + class.class_id + ))); + } + let external_labels = validate_helicity_external_labels( + class.external_labels.iter().copied(), + schema.external_count, + &format!("helicity recurrence class {:?}", class.class_id), + )?; + if class.source_class && external_labels.len() != 1 { + return Err(RusticolError::artifact(format!( + "helicity source recurrence class {:?} must cover exactly one external label", + class.class_id + ))); + } + let mut representative_seen = false; + let mut members = Vec::with_capacity(class.members.len()); + for member in &class.members { + validate_helicity_factor( + member.factor, + &format!("helicity recurrence current {}", member.current_id), + )?; + if member.current_id >= recurrence.current_count { + return Err(RusticolError::artifact(format!( + "helicity recurrence class {:?} references stale current id {}", + class.class_id, member.current_id + ))); + } + let domain = recurrence + .selector_domains + .get(member.selector_domain_id) + .ok_or_else(|| { + RusticolError::artifact(format!( + "helicity recurrence current {} references unknown selector domain {}", + member.current_id, member.selector_domain_id + )) + })?; + let domain_labels = domain + .source_states + .iter() + .map(|state| state.external_label) + .collect::>(); + if domain_labels != external_labels { + return Err(RusticolError::artifact(format!( + "helicity recurrence current {} does not match class {:?}", + member.current_id, class.class_id + ))); + } + if recurrence.materialization.is_none() { + let current = &schema.currents[member.current_id]; + if current.is_source != class.source_class + || (!current.external_labels.is_empty() + && current.external_labels != external_labels) + { + return Err(RusticolError::artifact(format!( + "helicity recurrence current {} does not match class {:?}", + member.current_id, class.class_id + ))); + } + } + if current_owners[member.current_id] + .replace(HelicityCurrentOwner { + class_index, + selector_domain_id: member.selector_domain_id, + factor: member.factor, + }) + .is_some() + { + return Err(RusticolError::artifact(format!( + "helicity recurrence current {} belongs to multiple classes", + member.current_id + ))); + } + if member.current_id == class.representative_current_id { + representative_seen = true; + if !helicity_factors_equal(member.factor, [1.0, 0.0]) { + return Err(RusticolError::artifact(format!( + "helicity recurrence representative current {} must carry the identity factor", + member.current_id + ))); + } + } + members.push(HelicityCurrentReplayMemberRuntime { + current_id: member.current_id, + selector_domain_id: member.selector_domain_id, + factor: member.factor, + }); + } + if !representative_seen { + return Err(RusticolError::artifact(format!( + "helicity recurrence representative current {} is not a member of class {:?}", + class.representative_current_id, class.class_id + ))); + } + recurrence_classes.push(HelicityRecurrenceClassRuntime { + representative_current_id: class.representative_current_id, + external_labels: external_labels.into_iter().map(|label| label - 1).collect(), + source_class: class.source_class, + members, + }); + } + + validate_sorted_unique_ids( + &recurrence.residual_current_ids, + recurrence.current_count, + "helicity recurrence residual current ids", + )?; + for current_id in &recurrence.residual_current_ids { + if current_owners[*current_id].is_some() { + return Err(RusticolError::artifact(format!( + "helicity residual current {current_id} overlaps a proven recurrence class" + ))); + } + } + if current_owners + .iter() + .filter(|owner| owner.is_some()) + .count() + + recurrence.residual_current_ids.len() + != recurrence.current_count + { + return Err(RusticolError::artifact( + "helicity recurrence classes and residual currents do not cover the proof DAG", + )); + } + if recurrence.materialization.is_none() + && recurrence + .residual_current_ids + .iter() + .any(|current_id| schema.currents[*current_id].is_source) + { + return Err(RusticolError::artifact( + "helicity recurrence leaves a final runtime source current residual", + )); + } + + let source_class_current_ids = recurrence + .recurrence_classes + .iter() + .filter(|class| class.source_class) + .flat_map(|class| class.members.iter().map(|member| member.current_id)) + .collect::>(); + let mut mapped_source_ids = BTreeSet::new(); + let mut source_state_mappings = Vec::with_capacity(recurrence.source_state_mappings.len()); + for mapping in &recurrence.source_state_mappings { + validate_helicity_factor( + mapping.factor, + &format!("helicity source mapping for current {}", mapping.current_id), + )?; + validate_sha256_digest( + &mapping.source_contract_digest, + &format!("helicity source mapping for current {}", mapping.current_id), + )?; + if !mapped_source_ids.insert(mapping.current_id) { + return Err(RusticolError::artifact(format!( + "helicity source current {} has multiple state mappings", + mapping.current_id + ))); + } + let owner = current_owners + .get(mapping.current_id) + .and_then(|owner| *owner) + .ok_or_else(|| { + RusticolError::artifact(format!( + "helicity source mapping references residual or stale current {}", + mapping.current_id + )) + })?; + let class = &recurrence.recurrence_classes[owner.class_index]; + let domain = recurrence + .selector_domains + .get(mapping.selector_domain_id) + .ok_or_else(|| { + RusticolError::artifact(format!( + "helicity source mapping current {} references unknown selector domain {}", + mapping.current_id, mapping.selector_domain_id + )) + })?; + if !class.source_class + || mapping.recurrence_class_id != class.class_id + || mapping.representative_current_id != class.representative_current_id + || mapping.selector_domain_id != owner.selector_domain_id + || !helicity_factors_equal(mapping.factor, owner.factor) + || !domain.source_states.iter().any(|state| { + state.external_label == mapping.external_label && state.helicity == mapping.helicity + }) + { + return Err(RusticolError::artifact(format!( + "helicity source mapping current {} is inconsistent with its recurrence class, selector domain, or SourceIR", + mapping.current_id + ))); + } + if recurrence.materialization.is_none() { + validate_helicity_source_against_runtime( + mapping.current_id, + mapping.external_label, + mapping.helicity, + mapping.chirality, + &mapping.spin_state, + mapping.declared_state_index, + schema, + )?; + } + source_state_mappings.push(HelicitySourceStateMappingRuntime { + current_id: mapping.current_id, + external_index: mapping.external_label - 1, + helicity: mapping.helicity, + chirality: mapping.chirality, + spin_state: mapping.spin_state.clone(), + declared_state_index: mapping.declared_state_index, + selector_domain_id: mapping.selector_domain_id, + recurrence_class_index: owner.class_index, + representative_current_id: mapping.representative_current_id, + factor: mapping.factor, + }); + } + if mapped_source_ids != source_class_current_ids { + return Err(RusticolError::artifact( + "helicity source-state mappings do not exactly cover proven source-class currents", + )); + } + let residual_current_ids = recurrence + .residual_current_ids + .iter() + .copied() + .collect::>(); + let mut root_owners = vec![false; recurrence.amplitude_root_count]; + let mut amplitude_class_ids = BTreeSet::new(); + let mut represented_complete_domains = BTreeSet::new(); + let mut amplitude_classes = Vec::with_capacity(recurrence.amplitude_classes.len()); + for class in &recurrence.amplitude_classes { + validate_helicity_recurrence_proof( + &class.class_id, + "helicity-amplitude-sha256:", + &class.proof, + )?; + if !amplitude_class_ids.insert(class.class_id.as_str()) { + return Err(RusticolError::artifact( + "helicity amplitude recurrence class ids must be unique", + )); + } + if class.members.is_empty() { + return Err(RusticolError::artifact(format!( + "helicity amplitude class {:?} has no members", + class.class_id + ))); + } + let mut representative_seen = false; + let mut members = Vec::with_capacity(class.members.len()); + for member in &class.members { + validate_helicity_factor( + member.factor, + &format!("helicity amplitude root {}", member.root_id), + )?; + if member.root_id >= recurrence.amplitude_root_count { + return Err(RusticolError::artifact(format!( + "helicity amplitude class {:?} references stale root id {}", + class.class_id, member.root_id + ))); + } + if root_owners[member.root_id] { + return Err(RusticolError::artifact(format!( + "helicity amplitude root {} belongs to multiple classes", + member.root_id + ))); + } + root_owners[member.root_id] = true; + validate_complete_selector_ids( + &member.selector_domain_ids, + recurrence, + &format!("helicity amplitude root {}", member.root_id), + )?; + represented_complete_domains.extend(member.selector_domain_ids.iter().copied()); + if recurrence.materialization.is_none() { + let root = &schema.roots[member.root_id]; + if !root.helicity_weight.is_finite() + || (root.helicity_weight - member.selector_domain_ids.len() as f64).abs() + > 1.0e-12 + { + return Err(RusticolError::artifact(format!( + "helicity amplitude root {} selector coverage disagrees with its final runtime helicity weight", + member.root_id + ))); + } + if residual_current_ids.contains(&root.left_current_id) + || residual_current_ids.contains(&root.right_current_id) + { + return Err(RusticolError::artifact(format!( + "proven helicity amplitude root {} depends on a residual current", + member.root_id + ))); + } + } + representative_seen |= member.root_id == class.representative_root_id; + members.push(HelicityAmplitudeReplayMemberRuntime { + root_id: member.root_id, + selector_domain_ids: member.selector_domain_ids.clone(), + factor: member.factor, + }); + } + if !representative_seen { + return Err(RusticolError::artifact(format!( + "helicity amplitude representative root {} is not a member of class {:?}", + class.representative_root_id, class.class_id + ))); + } + amplitude_classes.push(HelicityAmplitudeReplayClassRuntime { + representative_root_id: class.representative_root_id, + members, + }); + } + + validate_sorted_unique_ids( + &recurrence.residual_root_ids, + recurrence.amplitude_root_count, + "helicity recurrence residual root ids", + )?; + for root_id in &recurrence.residual_root_ids { + if root_owners[*root_id] { + return Err(RusticolError::artifact(format!( + "helicity residual root {root_id} overlaps a proven amplitude class" + ))); + } + } + if root_owners.iter().filter(|owned| **owned).count() + recurrence.residual_root_ids.len() + != recurrence.amplitude_root_count + { + return Err(RusticolError::artifact( + "helicity amplitude classes and residual roots do not cover the proof DAG", + )); + } + + validate_sorted_unique_ids( + &recurrence.structural_zero_selector_domain_ids, + recurrence.selector_domains.len(), + "structural-zero helicity selector domain ids", + )?; + let structural_zero_domains = recurrence + .structural_zero_selector_domain_ids + .iter() + .copied() + .collect::>(); + if structural_zero_domains + .iter() + .any(|domain_id| !recurrence.selector_domains[*domain_id].complete) + || !structural_zero_domains.is_disjoint(&represented_complete_domains) + { + return Err(RusticolError::artifact( + "structural-zero helicity domains must be complete and disjoint from amplitude coverage", + )); + } + if recurrence.materialization.is_none() { + let available_source_states = recurrence + .source_state_mappings + .iter() + .map(|mapping| (mapping.external_label, mapping.helicity)) + .collect::>(); + let directly_executable_domains = recurrence + .selector_domains + .iter() + .filter(|domain| { + domain.source_states.iter().all(|state| { + available_source_states.contains(&(state.external_label, state.helicity)) + }) + }) + .map(|domain| domain.id) + .collect::>(); + let mut replay_anchored_domains = BTreeSet::new(); + for class in &recurrence.amplitude_classes { + for member in &class.members { + if !member + .selector_domain_ids + .iter() + .any(|domain_id| directly_executable_domains.contains(domain_id)) + { + let domain_id = + member.selector_domain_ids.first().copied().ok_or_else(|| { + RusticolError::artifact( + "helicity amplitude replay member has no selector domains", + ) + })?; + return Err(RusticolError::artifact(format!( + "helicity selector domain {domain_id} has no directly executable source-state anchor in its certified amplitude replay group" + ))); + } + replay_anchored_domains.extend(member.selector_domain_ids.iter().copied()); + } + } + for domain in &recurrence.selector_domains { + if structural_zero_domains.contains(&domain.id) + || directly_executable_domains.contains(&domain.id) + || replay_anchored_domains.contains(&domain.id) + { + continue; + } + if domain.complete { + return Err(RusticolError::artifact(format!( + "helicity selector domain {} is neither directly executable nor covered by a certified amplitude replay group", + domain.id + ))); + } + } + } + let complete_domains = recurrence + .selector_domains + .iter() + .filter(|domain| domain.complete) + .map(|domain| domain.id) + .collect::>(); + if complete_domains + != represented_complete_domains + .union(&structural_zero_domains) + .copied() + .collect() + { + return Err(RusticolError::artifact( + "helicity amplitude and structural-zero domains do not cover every complete selector domain", + )); + } + + let actual_counts = HelicityRecurrenceProofCountsManifest { + recurrence_class_count: recurrence.recurrence_classes.len(), + optimized_recurrence_class_count: recurrence + .recurrence_classes + .iter() + .filter(|class| class.members.len() > 1) + .count(), + optimized_current_count: recurrence + .recurrence_classes + .iter() + .filter(|class| class.members.len() > 1) + .map(|class| class.members.len()) + .sum(), + residual_current_count: recurrence.residual_current_ids.len(), + amplitude_class_count: recurrence.amplitude_classes.len(), + optimized_amplitude_class_count: recurrence + .amplitude_classes + .iter() + .filter(|class| class.members.len() > 1) + .count(), + residual_amplitude_count: recurrence.residual_root_ids.len(), + source_state_mapping_count: recurrence.source_state_mappings.len(), + physical_helicity_count: complete_domains.len(), + structural_zero_helicity_count: structural_zero_domains.len(), + }; + if recurrence.proof_counts != actual_counts { + return Err(RusticolError::artifact( + "helicity recurrence proof counts do not match validated manifest contents", + )); + } + + let materialization = recurrence + .materialization + .as_ref() + .map(|materialization| { + build_helicity_materialization_runtime( + recurrence, + materialization, + schema, + ¤t_owners, + &complete_domains, + &structural_zero_domains, + ) + }) + .transpose()?; + + Ok(Some(HelicityRecurrenceRuntime { + selector_domains, + source_state_mappings, + recurrence_classes, + amplitude_classes, + residual_current_ids: recurrence.residual_current_ids.clone(), + residual_root_ids: recurrence.residual_root_ids.clone(), + structural_zero_selector_domain_ids: recurrence.structural_zero_selector_domain_ids.clone(), + materialization, + })) +} + +fn validate_helicity_source_against_runtime( + current_id: usize, + external_label: usize, + helicity: i32, + chirality: i32, + spin_state: &GenericSourceSpinStateManifest, + declared_state_index: usize, + schema: &HelicityRecurrenceSchemaView, +) -> RusticolResult<()> { + let source = schema.sources.get(¤t_id).ok_or_else(|| { + RusticolError::artifact(format!( + "helicity source mapping current {current_id} is not a runtime source" + )) + })?; + let declared_state = source + .declared_states + .get(declared_state_index) + .ok_or_else(|| { + RusticolError::artifact(format!( + "helicity source mapping current {current_id} has stale declared state index {declared_state_index}" + )) + })?; + if external_label != source.external_label + || helicity != source.helicity + || chirality != source.chirality + || spin_state != &source.spin_state + || declared_state.helicity != helicity + || declared_state.chirality != chirality + || &declared_state.spin_state != spin_state + { + return Err(RusticolError::artifact(format!( + "helicity source mapping current {current_id} is inconsistent with its SourceIR" + ))); + } + Ok(()) +} + +fn build_helicity_materialization_runtime( + recurrence: &HelicityRecurrenceManifest, + materialization: &HelicityRecurrenceMaterializationManifest, + schema: &HelicityRecurrenceSchemaView, + current_owners: &[Option], + complete_domains: &BTreeSet, + structural_zero_domains: &BTreeSet, +) -> RusticolResult { + if materialization.kind != HELICITY_MATERIALIZATION_KIND { + return Err(RusticolError::artifact(format!( + "unsupported helicity recurrence materialization kind {:?}", + materialization.kind + ))); + } + if materialization.contract_version != HELICITY_MATERIALIZATION_CONTRACT_VERSION { + return Err(RusticolError::compatibility(format!( + "unsupported helicity recurrence materialization contract version {}; regenerate the artifact", + materialization.contract_version + ))); + } + if materialization.proof_current_count != recurrence.current_count + || materialization.proof_root_count != recurrence.amplitude_root_count + { + return Err(RusticolError::artifact( + "helicity materialization proof counts do not match the recurrence proof DAG", + )); + } + if materialization.materialized_current_count != schema.currents.len() + || materialization.materialized_root_count != schema.roots.len() + { + return Err(RusticolError::artifact( + "helicity materialization counts do not match the final runtime schema", + )); + } + if materialization.proof_to_materialized_current.len() != recurrence.current_count + || materialization + .proof_to_materialized_current + .iter() + .any(|current_id| *current_id >= materialization.materialized_current_count) + { + return Err(RusticolError::artifact( + "helicity proof-to-materialized current map has a stale id or incorrect length", + )); + } + + let residual_targets = match materialization.strategy { + HelicityMaterializationStrategy::Quotient => { + validate_quotient_helicity_current_map(recurrence, materialization, schema)? + } + HelicityMaterializationStrategy::RetainedProofGraph => { + validate_retained_helicity_current_map(recurrence, materialization, schema)? + } + }; + + let source_routes = validate_helicity_materialized_source_routes( + recurrence, + materialization, + schema, + current_owners, + )?; + let amplitude_routes = validate_helicity_materialized_amplitude_routes( + recurrence, + materialization, + schema, + complete_domains, + structural_zero_domains, + &residual_targets, + )?; + let selector_schedules = validate_helicity_materialized_selector_schedules( + recurrence, + materialization, + schema, + complete_domains, + structural_zero_domains, + )?; + + Ok(HelicityRecurrenceMaterializationRuntime { + strategy: materialization.strategy, + proof_to_materialized_current: materialization.proof_to_materialized_current.clone(), + source_routes, + amplitude_routes, + selector_schedules, + }) +} + +fn validate_quotient_helicity_current_map( + recurrence: &HelicityRecurrenceManifest, + materialization: &HelicityRecurrenceMaterializationManifest, + schema: &HelicityRecurrenceSchemaView, +) -> RusticolResult> { + let mut target_seen = vec![false; materialization.materialized_current_count]; + for class in &recurrence.recurrence_classes { + let target = materialization.proof_to_materialized_current[class.representative_current_id]; + if class.members.iter().any(|member| { + materialization.proof_to_materialized_current[member.current_id] != target + }) { + return Err(RusticolError::artifact(format!( + "helicity recurrence class {:?} is split across materialized currents", + class.class_id + ))); + } + if target_seen[target] { + return Err(RusticolError::artifact(format!( + "helicity materialized current {target} is shared by distinct proof classes" + ))); + } + target_seen[target] = true; + let current = &schema.currents[target]; + if current.is_source != class.source_class + || (!current.external_labels.is_empty() + && current.external_labels != class.external_labels) + { + return Err(RusticolError::artifact(format!( + "helicity materialized current {target} does not match proof class {:?}", + class.class_id + ))); + } + } + let mut residual_targets = BTreeSet::new(); + for proof_current_id in &recurrence.residual_current_ids { + let target = materialization.proof_to_materialized_current[*proof_current_id]; + if target_seen[target] || !residual_targets.insert(target) { + return Err(RusticolError::artifact(format!( + "helicity residual proof current {proof_current_id} overlaps another materialized current" + ))); + } + if schema.currents[target].is_source { + return Err(RusticolError::artifact(format!( + "helicity residual proof current {proof_current_id} materializes as an unrouted source" + ))); + } + target_seen[target] = true; + } + if target_seen.iter().any(|seen| !seen) { + return Err(RusticolError::artifact( + "helicity proof-to-materialized current map does not cover every final current", + )); + } + Ok(residual_targets) +} + +fn validate_retained_helicity_current_map( + recurrence: &HelicityRecurrenceManifest, + materialization: &HelicityRecurrenceMaterializationManifest, + schema: &HelicityRecurrenceSchemaView, +) -> RusticolResult> { + if materialization.materialized_current_count != recurrence.current_count + || materialization.materialized_root_count != recurrence.amplitude_root_count + { + return Err(RusticolError::artifact( + "retained helicity materialization must preserve every proof current and root", + )); + } + if materialization + .proof_to_materialized_current + .iter() + .enumerate() + .any(|(proof_id, materialized_id)| proof_id != *materialized_id) + { + return Err(RusticolError::artifact( + "retained helicity materialization requires an identity current map", + )); + } + + let mut current_seen = vec![false; recurrence.current_count]; + for class in &recurrence.recurrence_classes { + for member in &class.members { + let current = &schema.currents[member.current_id]; + if current_seen[member.current_id] + || current.is_source != class.source_class + || (!current.external_labels.is_empty() + && current.external_labels != class.external_labels) + { + return Err(RusticolError::artifact(format!( + "retained helicity current {} does not match proof class {:?}", + member.current_id, class.class_id + ))); + } + current_seen[member.current_id] = true; + } + } + let mut residual_targets = BTreeSet::new(); + for proof_current_id in &recurrence.residual_current_ids { + if current_seen[*proof_current_id] || !residual_targets.insert(*proof_current_id) { + return Err(RusticolError::artifact(format!( + "retained helicity residual current {proof_current_id} overlaps another proof current" + ))); + } + if schema.currents[*proof_current_id].is_source { + return Err(RusticolError::artifact(format!( + "retained helicity residual current {proof_current_id} is an unrouted source" + ))); + } + current_seen[*proof_current_id] = true; + } + if current_seen.iter().any(|seen| !seen) { + return Err(RusticolError::artifact( + "retained helicity materialization does not cover every proof current", + )); + } + Ok(residual_targets) +} + +fn validate_helicity_materialized_source_routes( + recurrence: &HelicityRecurrenceManifest, + materialization: &HelicityRecurrenceMaterializationManifest, + schema: &HelicityRecurrenceSchemaView, + current_owners: &[Option], +) -> RusticolResult> { + if materialization.source_routes.len() != recurrence.source_state_mappings.len() { + return Err(RusticolError::artifact( + "helicity materialized source routes do not cover every proof source mapping", + )); + } + let mut route_keys = BTreeSet::new(); + let mut routed_source_ids = BTreeSet::new(); + let mut runtime_routes = Vec::with_capacity(materialization.source_routes.len()); + for (mapping, route) in recurrence + .source_state_mappings + .iter() + .zip(&materialization.source_routes) + { + validate_helicity_factor( + route.factor, + &format!( + "helicity materialized source route for proof current {}", + mapping.current_id + ), + )?; + let owner = current_owners[mapping.current_id].ok_or_else(|| { + RusticolError::artifact(format!( + "helicity materialized source route references unproven current {}", + mapping.current_id + )) + })?; + let class = &recurrence.recurrence_classes[owner.class_index]; + let representative_factor = class + .members + .iter() + .find(|member| member.current_id == class.representative_current_id) + .map(|member| member.factor) + .ok_or_else(|| { + RusticolError::artifact(format!( + "helicity source class {:?} has no representative factor", + class.class_id + )) + })?; + let expected_current_id = materialization.proof_to_materialized_current[mapping.current_id]; + let factor_matches = match materialization.strategy { + HelicityMaterializationStrategy::Quotient => { + helicity_factor_ratio_matches(route.factor, mapping.factor, representative_factor) + } + HelicityMaterializationStrategy::RetainedProofGraph => { + helicity_factors_equal(route.factor, [1.0, 0.0]) + } + }; + if route.materialized_current_id != expected_current_id + || route.external_label != mapping.external_label + || route.helicity != mapping.helicity + || route.chirality != mapping.chirality + || route.spin_state != mapping.spin_state + || route.declared_state_index != mapping.declared_state_index + || route.selector_domain_id != mapping.selector_domain_id + || !factor_matches + { + return Err(RusticolError::artifact(format!( + "helicity materialized source route for proof current {} is inconsistent", + mapping.current_id + ))); + } + let source = schema + .sources + .get(&route.materialized_current_id) + .ok_or_else(|| { + RusticolError::artifact(format!( + "helicity materialized source route targets non-source current {}", + route.materialized_current_id + )) + })?; + let declared_state = source + .declared_states + .get(route.declared_state_index) + .ok_or_else(|| { + RusticolError::artifact(format!( + "helicity materialized source route for current {} has stale declared state index {}", + route.materialized_current_id, route.declared_state_index + )) + })?; + if source.external_label != route.external_label + || declared_state.helicity != route.helicity + || declared_state.chirality != route.chirality + || declared_state.spin_state != route.spin_state + { + return Err(RusticolError::artifact(format!( + "helicity materialized source route for current {} disagrees with SourceIR", + route.materialized_current_id + ))); + } + if !route_keys.insert(( + route.materialized_current_id, + route.selector_domain_id, + route.external_label, + route.helicity, + route.chirality, + route.declared_state_index, + )) { + return Err(RusticolError::artifact( + "helicity materialized source routes contain a duplicate physical state", + )); + } + routed_source_ids.insert(route.materialized_current_id); + runtime_routes.push(HelicityMaterializedSourceRouteRuntime { + materialized_current_id: route.materialized_current_id, + external_index: route.external_label - 1, + helicity: route.helicity, + chirality: route.chirality, + spin_state: route.spin_state.clone(), + declared_state_index: route.declared_state_index, + selector_domain_id: route.selector_domain_id, + factor: route.factor, + }); + } + if routed_source_ids != schema.sources.keys().copied().collect() { + return Err(RusticolError::artifact( + "helicity materialized source routes do not cover every final runtime source", + )); + } + Ok(runtime_routes) +} + +fn validate_helicity_materialized_amplitude_routes( + recurrence: &HelicityRecurrenceManifest, + materialization: &HelicityRecurrenceMaterializationManifest, + schema: &HelicityRecurrenceSchemaView, + complete_domains: &BTreeSet, + structural_zero_domains: &BTreeSet, + residual_current_targets: &BTreeSet, +) -> RusticolResult> { + match materialization.strategy { + HelicityMaterializationStrategy::Quotient => validate_quotient_helicity_amplitude_routes( + recurrence, + materialization, + schema, + complete_domains, + structural_zero_domains, + residual_current_targets, + ), + HelicityMaterializationStrategy::RetainedProofGraph => { + validate_retained_helicity_amplitude_routes( + recurrence, + materialization, + schema, + complete_domains, + structural_zero_domains, + residual_current_targets, + ) + } + } +} + +fn validate_quotient_helicity_amplitude_routes( + recurrence: &HelicityRecurrenceManifest, + materialization: &HelicityRecurrenceMaterializationManifest, + schema: &HelicityRecurrenceSchemaView, + complete_domains: &BTreeSet, + structural_zero_domains: &BTreeSet, + residual_current_targets: &BTreeSet, +) -> RusticolResult> { + let expected_root_count = recurrence + .amplitude_classes + .len() + .checked_add(recurrence.residual_root_ids.len()) + .ok_or_else(|| { + RusticolError::artifact("helicity materialized root count overflows usize") + })?; + let expected_route_count = recurrence + .amplitude_classes + .iter() + .map(|class| class.members.len()) + .sum::() + .checked_add(recurrence.residual_root_ids.len()) + .ok_or_else(|| { + RusticolError::artifact("helicity materialized route count overflows usize") + })?; + if materialization.materialized_root_count != expected_root_count + || materialization.amplitude_routes.len() != expected_route_count + { + return Err(RusticolError::artifact( + "helicity materialized amplitude routes do not match proof class and residual coverage", + )); + } + let mut output_indices = BTreeSet::new(); + for (root_id, root) in schema.roots.iter().enumerate() { + if root.left_current_id >= schema.currents.len() + || root.right_current_id >= schema.currents.len() + || root.output_index >= schema.roots.len() + || !output_indices.insert(root.output_index) + || !root.helicity_weight.is_finite() + || (root.helicity_weight - 1.0).abs() > 1.0e-12 + { + return Err(RusticolError::artifact(format!( + "helicity materialized amplitude root {root_id} is inconsistent with the final runtime schema" + ))); + } + } + + let mut runtime_routes = Vec::with_capacity(expected_route_count); + let mut route_index = 0; + let mut represented_domains = BTreeSet::new(); + for (materialized_root_id, class) in recurrence.amplitude_classes.iter().enumerate() { + let representative_factor = class + .members + .iter() + .find(|member| member.root_id == class.representative_root_id) + .map(|member| member.factor) + .ok_or_else(|| { + RusticolError::artifact(format!( + "helicity amplitude class {:?} has no representative factor", + class.class_id + )) + })?; + let root = &schema.roots[materialized_root_id]; + if residual_current_targets.contains(&root.left_current_id) + || residual_current_targets.contains(&root.right_current_id) + { + return Err(RusticolError::artifact(format!( + "proven helicity materialized amplitude root {materialized_root_id} depends on a residual current" + ))); + } + for member in &class.members { + let route = &materialization.amplitude_routes[route_index]; + validate_helicity_factor( + route.factor, + &format!("helicity materialized amplitude route {route_index}"), + )?; + validate_complete_selector_ids( + &route.selector_domain_ids, + recurrence, + &format!("helicity materialized amplitude route {route_index}"), + )?; + if route.materialized_root_id != materialized_root_id + || route.selector_domain_ids != member.selector_domain_ids + || route.residual + || !helicity_factor_ratio_matches( + route.factor, + member.factor, + representative_factor, + ) + { + return Err(RusticolError::artifact(format!( + "helicity materialized amplitude route {route_index} disagrees with its proof member" + ))); + } + represented_domains.extend(route.selector_domain_ids.iter().copied()); + runtime_routes.push(HelicityMaterializedAmplitudeRouteRuntime { + materialized_root_id: route.materialized_root_id, + selector_domain_ids: route.selector_domain_ids.clone(), + factor: route.factor, + residual: false, + }); + route_index += 1; + } + } + for residual_index in 0..recurrence.residual_root_ids.len() { + let route = &materialization.amplitude_routes[route_index]; + let materialized_root_id = recurrence.amplitude_classes.len() + residual_index; + validate_complete_selector_ids( + &route.selector_domain_ids, + recurrence, + &format!("helicity residual materialized amplitude route {route_index}"), + )?; + if route.materialized_root_id != materialized_root_id + || route.selector_domain_ids.len() != 1 + || !route.residual + || !helicity_factors_equal(route.factor, [1.0, 0.0]) + { + return Err(RusticolError::artifact(format!( + "helicity residual materialized amplitude route {route_index} is inconsistent" + ))); + } + represented_domains.extend(route.selector_domain_ids.iter().copied()); + runtime_routes.push(HelicityMaterializedAmplitudeRouteRuntime { + materialized_root_id: route.materialized_root_id, + selector_domain_ids: route.selector_domain_ids.clone(), + factor: route.factor, + residual: true, + }); + route_index += 1; + } + let expected_nonzero_domains = complete_domains + .difference(structural_zero_domains) + .copied() + .collect::>(); + if represented_domains != expected_nonzero_domains { + return Err(RusticolError::artifact( + "helicity materialized amplitude routes do not cover exactly the nonzero complete selector domains", + )); + } + Ok(runtime_routes) +} + +fn validate_retained_helicity_amplitude_routes( + recurrence: &HelicityRecurrenceManifest, + materialization: &HelicityRecurrenceMaterializationManifest, + schema: &HelicityRecurrenceSchemaView, + complete_domains: &BTreeSet, + structural_zero_domains: &BTreeSet, + residual_current_targets: &BTreeSet, +) -> RusticolResult> { + if materialization.materialized_root_count != recurrence.amplitude_root_count + || materialization.amplitude_routes.len() != recurrence.amplitude_root_count + { + return Err(RusticolError::artifact( + "retained helicity amplitude routes must cover every proof root", + )); + } + + let mut expected_routes = vec![None; recurrence.amplitude_root_count]; + for class in &recurrence.amplitude_classes { + for member in &class.members { + if member.root_id >= expected_routes.len() + || expected_routes[member.root_id] + .replace((member.selector_domain_ids.clone(), false)) + .is_some() + { + return Err(RusticolError::artifact(format!( + "retained helicity amplitude root {} is covered more than once", + member.root_id + ))); + } + } + } + for root_id in &recurrence.residual_root_ids { + if *root_id >= expected_routes.len() + || expected_routes[*root_id] + .replace((Vec::new(), true)) + .is_some() + { + return Err(RusticolError::artifact(format!( + "retained helicity residual root {root_id} overlaps another proof root" + ))); + } + } + if expected_routes.iter().any(Option::is_none) { + return Err(RusticolError::artifact( + "retained helicity amplitude routes do not cover the proof root space", + )); + } + + let mut output_indices = BTreeSet::new(); + let mut represented_domains = BTreeSet::new(); + let mut runtime_routes = Vec::with_capacity(materialization.amplitude_routes.len()); + for (root_id, (root, route)) in schema + .roots + .iter() + .zip(&materialization.amplitude_routes) + .enumerate() + { + if root.left_current_id >= schema.currents.len() + || root.right_current_id >= schema.currents.len() + || root.output_index >= schema.roots.len() + || !output_indices.insert(root.output_index) + || !root.helicity_weight.is_finite() + || root.helicity_weight <= 0.0 + { + return Err(RusticolError::artifact(format!( + "retained helicity amplitude root {root_id} is inconsistent with the final runtime schema" + ))); + } + let (expected_domain_ids, residual) = expected_routes[root_id] + .as_ref() + .expect("retained root coverage checked above"); + validate_complete_selector_ids( + &route.selector_domain_ids, + recurrence, + &format!("retained helicity amplitude route {root_id}"), + )?; + if route.materialized_root_id != root_id + || route.residual != *residual + || (!*residual + && route.selector_domain_ids.as_slice() != expected_domain_ids.as_slice()) + || (*residual && route.selector_domain_ids.is_empty()) + || !helicity_factors_equal(route.factor, [root.helicity_weight.sqrt().recip(), 0.0]) + { + return Err(RusticolError::artifact(format!( + "retained helicity amplitude route {root_id} disagrees with its proof root" + ))); + } + if !*residual + && (residual_current_targets.contains(&root.left_current_id) + || residual_current_targets.contains(&root.right_current_id)) + { + return Err(RusticolError::artifact(format!( + "proven retained helicity amplitude root {root_id} depends on a residual current" + ))); + } + represented_domains.extend(route.selector_domain_ids.iter().copied()); + runtime_routes.push(HelicityMaterializedAmplitudeRouteRuntime { + materialized_root_id: root_id, + selector_domain_ids: route.selector_domain_ids.clone(), + factor: route.factor, + residual: *residual, + }); + } + let expected_nonzero_domains = complete_domains + .difference(structural_zero_domains) + .copied() + .collect::>(); + if represented_domains != expected_nonzero_domains { + return Err(RusticolError::artifact( + "retained helicity amplitude routes do not cover exactly the nonzero complete selector domains", + )); + } + Ok(runtime_routes) +} + +fn validate_helicity_materialized_selector_schedules( + recurrence: &HelicityRecurrenceManifest, + materialization: &HelicityRecurrenceMaterializationManifest, + schema: &HelicityRecurrenceSchemaView, + complete_domains: &BTreeSet, + structural_zero_domains: &BTreeSet, +) -> RusticolResult> { + let expected_domain_ids = complete_domains.iter().copied().collect::>(); + if materialization.selector_schedules.len() != expected_domain_ids.len() + || materialization + .selector_schedules + .iter() + .map(|schedule| schedule.selector_domain_id) + .ne(expected_domain_ids.iter().copied()) + { + return Err(RusticolError::artifact( + "helicity materialized selector schedules must cover complete domains in order", + )); + } + let chunks = schema.compiled_chunks.as_ref().ok_or_else(|| { + RusticolError::artifact( + "helicity materialized selector schedules require serialized stage evaluators", + ) + })?; + let mut runtime_schedules = Vec::with_capacity(materialization.selector_schedules.len()); + for schedule in &materialization.selector_schedules { + validate_sorted_unique_ids( + &schedule.active_current_ids, + schema.currents.len(), + &format!( + "helicity selector schedule {} active current ids", + schedule.selector_domain_id + ), + )?; + validate_sorted_unique_ids( + &schedule.active_root_ids, + schema.roots.len(), + &format!( + "helicity selector schedule {} active root ids", + schedule.selector_domain_id + ), + )?; + let structural_zero = structural_zero_domains.contains(&schedule.selector_domain_id); + if schedule.structural_zero != structural_zero + || (structural_zero + && (!schedule.active_current_ids.is_empty() + || !schedule.active_root_ids.is_empty())) + { + return Err(RusticolError::artifact(format!( + "helicity selector schedule {} disagrees with structural-zero proof coverage", + schedule.selector_domain_id + ))); + } + let expected_root_ids = materialization + .amplitude_routes + .iter() + .filter(|route| { + route + .selector_domain_ids + .contains(&schedule.selector_domain_id) + }) + .map(|route| route.materialized_root_id) + .collect::>(); + if expected_root_ids + != schedule + .active_root_ids + .iter() + .copied() + .collect::>() + { + return Err(RusticolError::artifact(format!( + "helicity selector schedule {} active roots disagree with amplitude routes", + schedule.selector_domain_id + ))); + } + if !structural_zero && schedule.active_root_ids.is_empty() { + return Err(RusticolError::artifact(format!( + "helicity selector schedule {} has no active amplitude roots", + schedule.selector_domain_id + ))); + } + + let active_current_ids = schedule + .active_current_ids + .iter() + .copied() + .collect::>(); + for root_id in &schedule.active_root_ids { + let root = &schema.roots[*root_id]; + if !active_current_ids.contains(&root.left_current_id) + || !active_current_ids.contains(&root.right_current_id) + { + return Err(RusticolError::artifact(format!( + "helicity selector schedule {} omits a parent of active root {root_id}", + schedule.selector_domain_id + ))); + } + } + if !structural_zero { + let domain = &recurrence.selector_domains[schedule.selector_domain_id]; + let active_source_ids = active_current_ids + .iter() + .filter(|current_id| schema.currents[**current_id].is_source) + .copied() + .collect::>(); + let active_source_labels = active_source_ids + .iter() + .map(|current_id| { + schema + .sources + .get(current_id) + .map(|source| source.external_label) + .ok_or_else(|| { + RusticolError::artifact(format!( + "helicity selector schedule {} references source current {} without source metadata", + schedule.selector_domain_id, current_id + )) + }) + }) + .collect::>>()?; + let expected_source_labels = domain + .source_states + .iter() + .map(|state| state.external_label) + .collect::>(); + if active_source_labels != expected_source_labels { + return Err(RusticolError::artifact(format!( + "helicity selector schedule {} source-label coverage disagrees with its complete domain", + schedule.selector_domain_id + ))); + } + } + + let mut covered_non_source_ids = BTreeSet::new(); + let mut active_stage_chunk_indices = Vec::with_capacity(chunks.stages.len()); + for stage in &chunks.stages { + let indices = stage + .current_ids_by_chunk + .iter() + .enumerate() + .filter_map(|(chunk_index, current_ids)| { + current_ids + .iter() + .any(|current_id| active_current_ids.contains(current_id)) + .then_some(chunk_index) + }) + .collect::>(); + for chunk_index in &indices { + covered_non_source_ids.extend( + stage.current_ids_by_chunk[*chunk_index] + .intersection(&active_current_ids) + .copied() + .filter(|current_id| !schema.currents[*current_id].is_source), + ); + } + active_stage_chunk_indices.push(indices); + } + let required_non_source_ids = active_current_ids + .iter() + .copied() + .filter(|current_id| !schema.currents[*current_id].is_source) + .collect::>(); + if covered_non_source_ids != required_non_source_ids { + let missing = required_non_source_ids + .difference(&covered_non_source_ids) + .copied() + .collect::>(); + return Err(RusticolError::artifact(format!( + "helicity selector schedule {} has active non-source currents without compiled stage output coverage: {missing:?}", + schedule.selector_domain_id + ))); + } + + let active_root_ids = schedule + .active_root_ids + .iter() + .copied() + .collect::>(); + let active_amplitude_chunk_indices = chunks + .amplitude_root_ids_by_chunk + .iter() + .enumerate() + .filter_map(|(chunk_index, root_ids)| { + root_ids + .iter() + .any(|root_id| active_root_ids.contains(root_id)) + .then_some(chunk_index) + }) + .collect::>(); + let covered_root_ids = active_amplitude_chunk_indices + .iter() + .flat_map(|chunk_index| { + chunks.amplitude_root_ids_by_chunk[*chunk_index] + .intersection(&active_root_ids) + .copied() + }) + .collect::>(); + if covered_root_ids != active_root_ids { + return Err(RusticolError::artifact(format!( + "helicity selector schedule {} has active roots without compiled amplitude output coverage", + schedule.selector_domain_id + ))); + } + let current_is_source = schema + .currents + .iter() + .map(|current| current.is_source) + .collect::>(); + validate_compiled_selector_schedule_closure( + &format!("helicity selector schedule {}", schedule.selector_domain_id), + ¤t_is_source, + chunks, + &active_stage_chunk_indices, + &active_amplitude_chunk_indices, + Some(&active_current_ids), + Some(&active_root_ids), + )?; + runtime_schedules.push(HelicityMaterializedSelectorScheduleRuntime { + selector_domain_id: schedule.selector_domain_id, + active_current_ids: schedule.active_current_ids.clone(), + active_root_ids: schedule.active_root_ids.clone(), + active_stage_chunk_indices, + active_amplitude_chunk_indices, + structural_zero, + }); + } + Ok(runtime_schedules) +} + +fn validate_complete_selector_ids( + selector_domain_ids: &[usize], + recurrence: &HelicityRecurrenceManifest, + context: &str, +) -> RusticolResult<()> { + if selector_domain_ids.is_empty() + || selector_domain_ids.iter().any(|domain_id| { + recurrence + .selector_domains + .get(*domain_id) + .is_none_or(|domain| !domain.complete) + }) + || selector_domain_ids + .windows(2) + .any(|pair| pair[0] >= pair[1]) + { + return Err(RusticolError::artifact(format!( + "{context} requires distinct ordered complete selector domains" + ))); + } + Ok(()) +} + +fn helicity_factor_ratio_matches( + actual: [f64; 2], + numerator: [f64; 2], + denominator: [f64; 2], +) -> bool { + let product = [ + actual[0] * denominator[0] - actual[1] * denominator[1], + actual[0] * denominator[1] + actual[1] * denominator[0], + ]; + product + .into_iter() + .zip(numerator) + .all(|(actual, expected)| { + let tolerance = 64.0 * f64::EPSILON * expected.abs().max(1.0); + (actual - expected).abs() <= tolerance + }) +} + +fn validate_helicity_recurrence_proof( + class_id: &str, + class_prefix: &str, + proof: &HelicityRecurrenceProofManifest, +) -> RusticolResult<()> { + validate_sha256_digest(&proof.digest, class_id)?; + if proof.status != "proven" + || proof.algorithm != HELICITY_RECURRENCE_PROOF_ALGORITHM + || class_id != format!("{class_prefix}{}", proof.digest) + || proof + .transition_contract_ids + .iter() + .any(|contract| contract.is_empty()) + || proof + .transition_contract_ids + .windows(2) + .any(|pair| pair[0] >= pair[1]) + { + return Err(RusticolError::artifact(format!( + "helicity recurrence class {class_id:?} has invalid proof metadata" + ))); + } + Ok(()) +} + +fn validate_sha256_digest(digest: &str, context: &str) -> RusticolResult<()> { + if digest.len() != 64 + || !digest + .bytes() + .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte)) + { + return Err(RusticolError::artifact(format!( + "{context} requires a lowercase SHA-256 digest" + ))); + } + Ok(()) +} + +fn validate_helicity_external_labels( + labels: impl IntoIterator, + external_count: usize, + context: &str, +) -> RusticolResult> { + let labels = labels.into_iter().collect::>(); + if labels.is_empty() + || labels + .iter() + .any(|label| *label == 0 || *label > external_count) + || labels.windows(2).any(|pair| pair[0] >= pair[1]) + { + return Err(RusticolError::artifact(format!( + "{context} contains invalid, duplicate, or unordered external labels" + ))); + } + Ok(labels) +} + +fn validate_sorted_unique_ids( + ids: &[usize], + upper_bound: usize, + context: &str, +) -> RusticolResult<()> { + if ids.iter().any(|id| *id >= upper_bound) || ids.windows(2).any(|pair| pair[0] >= pair[1]) { + return Err(RusticolError::artifact(format!( + "{context} contain stale, duplicate, or unordered ids" + ))); + } + Ok(()) +} + +fn validate_helicity_factor(factor: [f64; 2], context: &str) -> RusticolResult<()> { + if !factor.iter().all(|component| component.is_finite()) || factor == [0.0, 0.0] { + return Err(RusticolError::artifact(format!( + "{context} has an invalid recurrence factor" + ))); + } + Ok(()) +} + +fn helicity_factors_equal(left: [f64; 2], right: [f64; 2]) -> bool { + left.into_iter() + .zip(right) + .all(|(left, right)| left.to_bits() == right.to_bits()) +} + +fn build_runtime_parameter_slots( + parameters: &[GenericRuntimeModelParameterManifest], +) -> RusticolResult> { + let mut direct = BTreeMap::new(); + let mut complex_components: BTreeMap, Option)> = BTreeMap::new(); + for parameter in parameters { + if parameter.kind == "derived_parameter_component" { + continue; + } + if let Some(runtime_name) = ¶meter.runtime_name { + let slots = complex_components + .entry(runtime_name.clone()) + .or_insert((None, None)); + match parameter.complex_component.as_deref() { + Some("real") if slots.0.replace(parameter.parameter_index).is_none() => {} + Some("imag") if slots.1.replace(parameter.parameter_index).is_none() => {} + Some(component) => { + return Err(RusticolError::invalid_argument(format!( + "runtime model parameter {runtime_name:?} has duplicate or invalid component {component:?}" + ))); + } + None => { + return Err(RusticolError::invalid_argument(format!( + "runtime model parameter {runtime_name:?} is missing component metadata" + ))); + } + } + } else if direct + .insert( + parameter.name.clone(), + RuntimeParameterSlots { + real: parameter.parameter_index, + imaginary: None, + }, + ) + .is_some() + { + return Err(RusticolError::invalid_argument(format!( + "duplicate runtime model parameter name {:?}", + parameter.name + ))); + } + } + for (name, (real, imaginary)) in complex_components { + let (Some(real), Some(imaginary)) = (real, imaginary) else { + return Err(RusticolError::invalid_argument(format!( + "runtime model parameter {name:?} requires real and imaginary slots" + ))); + }; + if direct + .insert( + name.clone(), + RuntimeParameterSlots { + real, + imaginary: Some(imaginary), + }, + ) + .is_some() + { + return Err(RusticolError::invalid_argument(format!( + "duplicate runtime model parameter name {name:?}" + ))); + } + } + Ok(direct) +} + +impl ExecutionRuntime { + pub(super) fn from_manifest(mut manifest: ExecutionManifest) -> RusticolResult { + validate_helicity_sum_execution(&manifest)?; + validate_helicity_selector_executions(&manifest)?; + validate_color_selector_executions(&manifest)?; + let helicity_sum_runtime = manifest + .helicity_sum_execution + .take() + .map(|sum_manifest| Self::from_manifest(*sum_manifest).map(Box::new)) + .transpose()?; + let helicity_selector_manifests = + std::mem::take(&mut manifest.helicity_selector_executions); + let mut helicity_selector_runtimes = Vec::with_capacity(helicity_selector_manifests.len()); + let mut helicity_selector_runtime_schedule_modes = + Vec::with_capacity(helicity_selector_manifests.len()); + let mut helicity_selector_lane_by_domain = BTreeMap::new(); + for record in helicity_selector_manifests { + let lane_index = helicity_selector_runtimes.len(); + for selector_domain_id in record.selector_domain_ids { + helicity_selector_lane_by_domain.insert(selector_domain_id, lane_index); + } + helicity_selector_runtime_schedule_modes.push(record.schedule_mode); + helicity_selector_runtimes.push(Box::new(Self::from_manifest(*record.execution)?)); + } + let color_selector_runtimes = manifest + .color_selector_executions + .drain(..) + .map(|record| { + Ok(( + record.materialized_sector_id, + Box::new(Self::from_manifest(*record.execution)?), + )) + }) + .collect::>>()?; + let topology_replay = manifest.compiled.lc_topology_replay.as_ref(); + let topology_replay_data = build_lc_topology_replay_data(topology_replay)?; + let color_topology_replay = manifest.compiled.color_topology_replay.as_ref(); + let color_topology_replay_data = + build_color_topology_replay_data(color_topology_replay, &manifest.color_accuracy)?; + if !topology_replay_data.mappings.is_empty() + && !color_topology_replay_data.mappings.is_empty() + { + return Err(RusticolError::artifact( + "an artifact cannot enable LC and contracted-color topology replay together", + )); + } + let compiled_color_execution_plan = manifest + .compiled + .stage_evaluators + .as_ref() + .map(|evaluators| { + build_compiled_color_execution_plan( + evaluators, + &manifest.runtime_schema, + &topology_replay_data.materialized_sector_ids, + ) + }) + .transpose()? + .flatten(); + let topology_replay_mappings = topology_replay_data.mappings; + let topology_replay_public_mappings = topology_replay_mappings.clone(); + let helicity_recurrence = build_helicity_recurrence_runtime(&manifest)?; + let external_is_initial = manifest + .runtime_schema + .external_particles + .iter() + .map(|particle| particle.role == "initial") + .collect::>(); + let particle_masses = manifest + .runtime_schema + .model + .as_ref() + .map(|model| { + model + .particles + .iter() + .map(|particle| (particle.pdg, particle.mass)) + .collect::>() + }) + .unwrap_or_default(); + let particle_mass_parameter_names = manifest + .runtime_schema + .model + .as_ref() + .map(|model| { + model + .particles + .iter() + .filter_map(|particle| { + particle + .mass_parameter + .as_ref() + .map(|name| (particle.pdg, name.clone())) + }) + .collect::>() + }) + .unwrap_or_default(); + let mut model_parameters = manifest.runtime_schema.model_parameters.clone(); + model_parameters.sort_by_key(|parameter| parameter.parameter_index); + let model_parameter_values_f64 = model_parameters + .iter() + .map(|parameter| parameter.default) + .collect::>(); + let model_parameter_name_to_index = model_parameters + .iter() + .map(|parameter| (parameter.name.clone(), parameter.parameter_index)) + .collect::>(); + let model_parameter_runtime_slots = build_runtime_parameter_slots(&model_parameters)?; + let color_factor_in_contraction = manifest + .runtime_schema + .amplitude_stage + .color_contraction + .as_ref() + .map(|contraction| contraction.supported && contraction.includes_color_factor) + .unwrap_or(false); + let ( + normalization_factor, + normalization_color_factor, + normalization_average_factor, + normalization_identical_factor, + normalization_qcd_coupling_power, + normalization_electroweak_coupling_power, + ) = manifest + .runtime_schema + .normalization + .as_ref() + .map(|normalization| { + let color_factor = if color_factor_in_contraction { + 1.0 + } else { + normalization.color_factor + }; + ( + color_factor * normalization.global_coupling_factor + / (normalization.average_factor * normalization.identical_factor), + color_factor, + normalization.average_factor, + normalization.identical_factor, + normalization.qcd_coupling_power, + normalization.electroweak_coupling_power, + ) + }) + .unwrap_or((1.0, 1.0, 1.0, 1.0, 0, 0)); + Ok(Self { + process: manifest.process, + key: manifest.key, + color_accuracy: manifest.color_accuracy, + external_count: manifest.external_pdg_order.len(), + external_pdg_order: manifest.external_pdg_order, + parameter_count: manifest + .runtime_schema + .parameter_layout + .value_component_count + + manifest + .runtime_schema + .parameter_layout + .momentum_parameter_count + + manifest + .runtime_schema + .parameter_layout + .model_parameter_count, + value_parameter_count: manifest + .runtime_schema + .parameter_layout + .value_component_count, + momentum_parameter_count: manifest + .runtime_schema + .parameter_layout + .momentum_parameter_count, + current_count: manifest.dag_summary.current_count, + source_count: manifest.dag_summary.source_count, + interaction_count: manifest.dag_summary.interaction_count, + stage_count: manifest.runtime_schema.stages.len(), + amplitude_output_count: manifest.runtime_schema.amplitude_stage.output_count, + lc_topology_replay_enabled: !topology_replay_mappings.is_empty(), + lc_topology_replay_mappings: Arc::new(topology_replay_mappings), + lc_topology_replay_public_mappings: topology_replay_public_mappings, + lc_topology_replay_routes: topology_replay_data.routes, + lc_topology_replay_materialized_sector_ids: topology_replay_data + .materialized_sector_ids, + lc_resolved_replay_plan: None, + lc_resolved_replay_selection_cache: None, + lc_replay_flat_momenta_scratch: Vec::new(), + lc_replay_target_components_scratch: Vec::new(), + color_topology_replay_enabled: !color_topology_replay_data.mappings.is_empty(), + color_topology_replay_mappings: Arc::new(color_topology_replay_data.mappings), + color_replay_flat_momenta_scratch: Vec::new(), + helicity_recurrence, + compiled_helicity_execution_plan: None, + compiled_color_execution_plan, + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + compiled_direct_runtime: None, + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + compiled_direct_color_schedules: BTreeMap::new(), + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + compiled_direct_helicity_schedules: BTreeMap::new(), + helicity_sum_runtime, + helicity_selector_runtimes, + helicity_selector_runtime_schedule_modes, + helicity_selector_lane_by_domain, + color_selector_runtimes, + runtime_unavailable_message: manifest.compiled.runtime_unavailable_message, + sources: manifest.runtime_schema.source_fill.sources, + momentum_slots: manifest.runtime_schema.momentum_slots, + external_is_initial, + particle_masses, + particle_mass_parameter_names, + normalization_factor, + normalization_color_factor, + normalization_average_factor, + normalization_identical_factor, + normalization_qcd_coupling_power, + normalization_electroweak_coupling_power, + model_parameters, + model_parameter_name_to_index, + model_parameter_runtime_slots, + model_parameter_values_f64, + model_parameter_evaluator: None, + physics_reduction_override: manifest.physics_reduction, + physics: None, + stages: None, + amplitude_stage: None, + state_scratch_f64: Vec::new(), + state_scratch_f64_requires_clear: false, + values_scratch_f64: Vec::new(), + }) + } + + fn compiled_direct_routed_reduction_footprint( + &self, + amplitude: &AmplitudeRuntime, + physics: &PhysicsRuntime, + ) -> RusticolResult { + if !self.lc_topology_replay_enabled { + return Ok(CompiledDirectRoutedReductionFootprint::default()); + } + let materialized_sector_by_id = + self.lc_materialized_sectors_by_id_from_groups(physics, &litude.raw_sum_groups)?; + let replay_plan = physics.lc_resolved_replay_plan( + &self.lc_topology_replay_public_mappings, + &self.lc_topology_replay_routes, + &materialized_sector_by_id, + )?; + let selection_key = physics.lc_resolved_replay_selection_key(None, None)?; + let selection = + physics.select_lc_resolved_replay_plan_for_key(&replay_plan, &selection_key)?; + let maximum_source_component_count = selection + .source_groups + .iter() + .map(|group| group.source_component_count) + .max() + .ok_or_else(|| { + RusticolError::artifact( + "enabled LC topology replay has no authenticated source group", + ) + })?; + let maximum_target_component_count = selection + .helicity_indices + .len() + .checked_mul(selection.color_indices.len()) + .filter(|count| *count != 0) + .ok_or_else(|| { + RusticolError::artifact( + "enabled LC topology replay has no authenticated target components", + ) + })?; + Ok(CompiledDirectRoutedReductionFootprint { + maximum_source_component_count, + maximum_target_component_count, + }) + } + + pub(super) fn load_from_manifest_with_store( + manifest: ExecutionManifest, + payloads: &EvaluatorPayloadStore, + sizing_physics: &PhysicsRuntime, + ) -> RusticolResult { + Self::load_from_manifest_with_store_and_replay_footprint( + manifest, + payloads, + Some(CompiledDirectRoutedReductionFootprint::default()), + sizing_physics, + ) + } + + fn load_from_manifest_with_store_and_replay_footprint( + mut manifest: ExecutionManifest, + payloads: &EvaluatorPayloadStore, + inherited_routed_footprint: Option, + inherited_sizing_physics: &PhysicsRuntime, + ) -> RusticolResult { + validate_helicity_sum_execution(&manifest)?; + validate_helicity_selector_executions(&manifest)?; + validate_color_selector_executions(&manifest)?; + let compiled_plane_arena = validate_compiled_plane_arena_contract(&manifest)?; + let compiled_momentum_slot_ids = manifest.compiled.momentum_slot_ids.take(); + ensure_execution_capabilities_supported(&manifest)?; + let helicity_sum_manifest = manifest.helicity_sum_execution.take(); + let helicity_selector_manifests = + std::mem::take(&mut manifest.helicity_selector_executions); + let color_selector_manifests = std::mem::take(&mut manifest.color_selector_executions); + let compiled_color_payload = manifest.color_contraction_payload.take(); + let (stage_evaluators, helicity_recurrence) = + detach_compiled_helicity_load_inputs(&mut manifest)?; + let model_parameter_evaluator = manifest.compiled.model_parameter_evaluator.take(); + let replay_materialized_sector_ids = + build_lc_topology_replay_data(manifest.compiled.lc_topology_replay.as_ref())? + .materialized_sector_ids; + let compiled_color_execution_plan = stage_evaluators + .as_ref() + .map(|evaluators| { + build_compiled_color_execution_plan( + evaluators, + &manifest.runtime_schema, + &replay_materialized_sector_ids, + ) + }) + .transpose()? + .flatten(); + drop(replay_materialized_sector_ids); + let amplitude_stage_manifest = manifest.runtime_schema.amplitude_stage.clone(); + let mut runtime = Self::from_manifest(manifest)?; + if compiled_plane_arena { + // These ids were selected at generation and checked alongside + // the final leaf bindings above. Loading only binds that list; + // it does not rediscover which momentum sums are live. + runtime.momentum_slots = compiled_momentum_slot_ids + .expect("validated compiled momentum-slot selection") + .into_iter() + .map(|id| runtime.momentum_slots[id].clone()) + .collect(); + } + runtime.helicity_recurrence = helicity_recurrence; + runtime.compiled_color_execution_plan = compiled_color_execution_plan; + let sizing_physics = if let Some(reduction) = runtime.physics_reduction_override.as_ref() { + let mut manifest = inherited_sizing_physics.manifest.clone(); + manifest.reduction = reduction.clone(); + PhysicsRuntime::new(manifest)? + } else { + inherited_sizing_physics.clone() + }; + let mut compiled_color_contraction = load_compiled_symmetric_group_color_contraction( + compiled_color_payload.as_ref(), + payloads, + &sizing_physics, + )?; + let routed_footprint = if runtime.lc_topology_replay_enabled { + if let Some(stage_evaluators) = stage_evaluators.as_ref() { + let sizing_amplitude = AmplitudeRuntime::load_reducer_only( + &litude_stage_manifest, + &stage_evaluators.amplitude_stage, + )?; + let own = runtime.compiled_direct_routed_reduction_footprint( + &sizing_amplitude, + &sizing_physics, + )?; + inherited_routed_footprint.map(|inherited| inherited.merge(own)) + } else { + // The replay may route into a nested Direct engine, but this + // legacy parent exposes no amplitude metadata from which to + // authenticate its source/target shape. + None + } + } else { + inherited_routed_footprint + }; + runtime.helicity_sum_runtime = helicity_sum_manifest + .map(|sum_manifest| { + Self::load_from_manifest_with_store_and_replay_footprint( + *sum_manifest, + payloads, + routed_footprint, + &sizing_physics, + ) + .map(Box::new) + }) + .transpose()?; + runtime.helicity_selector_runtimes = Vec::with_capacity(helicity_selector_manifests.len()); + runtime.helicity_selector_runtime_schedule_modes = + Vec::with_capacity(helicity_selector_manifests.len()); + runtime.helicity_selector_lane_by_domain.clear(); + for record in helicity_selector_manifests { + let lane_index = runtime.helicity_selector_runtimes.len(); + for selector_domain_id in record.selector_domain_ids { + runtime + .helicity_selector_lane_by_domain + .insert(selector_domain_id, lane_index); + } + runtime + .helicity_selector_runtime_schedule_modes + .push(record.schedule_mode); + runtime.helicity_selector_runtimes.push(Box::new( + Self::load_from_manifest_with_store_and_replay_footprint( + *record.execution, + payloads, + routed_footprint, + &sizing_physics, + )?, + )); + } + runtime.color_selector_runtimes = color_selector_manifests + .into_iter() + .map(|record| { + Ok(( + record.materialized_sector_id, + Box::new(Self::load_from_manifest_with_store_and_replay_footprint( + *record.execution, + payloads, + routed_footprint, + &sizing_physics, + )?), + )) + }) + .collect::>>()?; + if let Some(manifest) = model_parameter_evaluator { + if manifest.kind != "generic-model-parameter-evaluator" { + return Err(RusticolError::invalid_argument(format!( + "unsupported model-parameter evaluator kind {:?}", + manifest.kind + ))); + } + runtime.model_parameter_evaluator = Some(ModelParameterEvaluatorRuntime { + input_parameter_indices: manifest.input_parameter_indices, + outputs: manifest.outputs, + evaluator: EvaluatorGroup::load_from_store(&manifest.evaluator, payloads)?, + }); + runtime.refresh_derived_model_parameters()?; + } + if let Some(stage_evaluators) = stage_evaluators { + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + if compiled_plane_arena { + #[cfg(feature = "symbolica-runtime")] + let (stages, mut amplitude) = match compiled_color_contraction.take() { + Some(color) => ( + Vec::new(), + AmplitudeRuntime::load_reducer_only_with_compiled_color( + &litude_stage_manifest, + &stage_evaluators.amplitude_stage, + Some(color), + )?, + ), + None => { + // Preserve the public exact-precision API for ordinary + // compiled artifacts without instantiating a second f64 + // evaluator representation. The selected-helicity SG + // diagnostic rejects exact execution and therefore uses + // only the reducer construction above. + ( + stage_evaluators + .stages + .iter() + .map(|stage| StageRuntime::load_exact_from_plane(stage, payloads)) + .collect::>>()?, + AmplitudeRuntime::load_exact_from_plane( + &litude_stage_manifest, + &stage_evaluators.amplitude_stage, + payloads, + )?, + ) + } + }; + #[cfg(not(feature = "symbolica-runtime"))] + let (stages, mut amplitude) = ( + Vec::new(), + match compiled_color_contraction.take() { + Some(color) => AmplitudeRuntime::load_reducer_only_with_compiled_color( + &litude_stage_manifest, + &stage_evaluators.amplitude_stage, + Some(color), + )?, + None => AmplitudeRuntime::load_reducer_only( + &litude_stage_manifest, + &stage_evaluators.amplitude_stage, + )?, + }, + ); + let maximum_resolved_component_count = sizing_physics + .manifest + .helicities + .len() + .checked_mul(sizing_physics.manifest.color_components.len()) + .ok_or_else(|| { + RusticolError::integrity( + "compiled Direct-Arena resolved footprint overflows", + ) + })?; + let reduction_footprint = if let Some(routed_footprint) = routed_footprint { + amplitude.compiled_direct_reduction_footprint( + maximum_resolved_component_count, + routed_footprint, + )? + } else { + CompiledDirectReductionFootprint::Unauthenticated + }; + let direct = + compiled_direct_prototype::CompiledDirectEnginePrototype::load_production( + &stage_evaluators.stages, + &stage_evaluators.amplitude_stage, + payloads, + &runtime.sources, + runtime.value_parameter_count, + runtime.momentum_parameter_count, + runtime.model_parameter_values_f64.len(), + stage_evaluators.amplitude_stage.output_length, + reduction_footprint, + )?; + amplitude + .prepare_compiled_symmetric_group_workspace(direct.reduction_tile_capacity())?; + runtime.stages = Some(stages); + runtime.amplitude_stage = Some(amplitude); + let mut color_schedules = BTreeMap::new(); + if let Some(color_plan) = runtime.compiled_color_execution_plan.as_ref() { + for (sector_id, schedule) in &color_plan.schedules_by_materialized_sector { + color_schedules + .insert(*sector_id, direct.bind_color_schedule(schedule.as_ref())?); + } + } + runtime.compiled_direct_color_schedules = color_schedules; + runtime.compiled_direct_runtime = Some(direct); + } else { + if compiled_color_contraction.is_some() { + return Err(RusticolError::compatibility( + "compiled symmetric-group FFT diagnostic requires compiled-plane-arena-v1", + )); + } + let stages = stage_evaluators + .stages + .iter() + .map(|stage| StageRuntime::load(stage, payloads)) + .collect::>>()?; + runtime.stages = Some(stages); + runtime.amplitude_stage = Some(AmplitudeRuntime::load( + &litude_stage_manifest, + &stage_evaluators.amplitude_stage, + payloads, + )?); + } + #[cfg(not(any(feature = "f64-compiled", feature = "f64-symjit")))] + { + if compiled_plane_arena { + return Err(RusticolError::unsupported_runtime_capability( + COMPILED_PLANE_ARENA_RUNTIME_CAPABILITY, + "this Rusticol build has no compiled DirectApplication backend", + )); + } + let stages = stage_evaluators + .stages + .iter() + .map(|stage| StageRuntime::load(stage, payloads)) + .collect::>>()?; + runtime.stages = Some(stages); + runtime.amplitude_stage = Some(AmplitudeRuntime::load( + &litude_stage_manifest, + &stage_evaluators.amplitude_stage, + payloads, + )?); + } + } + runtime.validate_color_topology_replay_amplitude()?; + Ok(runtime) + } + + fn validate_color_topology_replay_amplitude(&self) -> RusticolResult<()> { + if !self.color_topology_replay_enabled { + if self + .amplitude_stage + .as_ref() + .and_then(|amplitude| amplitude.color_topology_replay.as_ref()) + .is_some() + { + return Err(RusticolError::artifact( + "amplitude color replay is present without its proof certificate", + )); + } + return Ok(()); + } + let amplitude = self.amplitude_stage.as_ref().ok_or_else(|| { + RusticolError::artifact("color topology replay proof has no executable amplitude stage") + })?; + let amplitude_mappings = amplitude.color_topology_replay_mappings().ok_or_else(|| { + RusticolError::artifact("color topology replay proof has no amplitude-level gather") + })?; + if amplitude_mappings.as_slice() != self.color_topology_replay_mappings.as_slice() { + return Err(RusticolError::artifact( + "color topology replay proof and amplitude gather mappings disagree", + )); + } + Ok(()) + } + + pub(super) fn execution_unavailable_error(&self) -> RusticolError { + if self.stages.is_none() || self.amplitude_stage.is_none() { + return RusticolError::invalid_argument(format!( + "generic DAG schema-v3 artifact for {} loaded successfully, but it has no \ + serialized generic stage evaluators", + self.process + )); + } + let detail = self.runtime_unavailable_message.as_deref().unwrap_or( + "generic source filling and staged evaluator execution from schema-v3 metadata remain pending", + ); + RusticolError::invalid_argument(format!( + "generic DAG schema-v3 artifact for {} loaded successfully, but generic Rusticol \ + execution is not wired yet; {detail}", + self.process, + )) + } +} + +fn validate_helicity_sum_execution(manifest: &ExecutionManifest) -> RusticolResult<()> { + let Some(sum_manifest) = manifest.helicity_sum_execution.as_deref() else { + return Ok(()); + }; + if sum_manifest.helicity_sum_execution.is_some() + || !sum_manifest.helicity_selector_executions.is_empty() + { + return Err(RusticolError::integrity( + "compiled helicity-sum execution cannot contain another auxiliary execution", + )); + } + if sum_manifest.physics_reduction.is_none() { + return Err(RusticolError::integrity( + "compiled helicity-sum execution has no reduction override", + )); + } + if sum_manifest + .runtime_schema + .helicity_recurrence + .as_ref() + .is_some_and(|recurrence| recurrence.materialization.is_some()) + { + return Err(RusticolError::integrity( + "compiled helicity-sum execution cannot contain helicity materialization", + )); + } + if sum_manifest.process != manifest.process + || sum_manifest.key != manifest.key + || sum_manifest.color_accuracy != manifest.color_accuracy + || sum_manifest.external_pdg_order != manifest.external_pdg_order + { + return Err(RusticolError::integrity( + "compiled helicity-sum execution does not match its primary process contract", + )); + } + Ok(()) +} + +fn validate_helicity_selector_executions(manifest: &ExecutionManifest) -> RusticolResult<()> { + if manifest.helicity_selector_executions.is_empty() { + return Ok(()); + } + let recurrence = manifest + .runtime_schema + .helicity_recurrence + .as_ref() + .filter(|recurrence| recurrence.materialization.is_some()) + .ok_or_else(|| { + RusticolError::integrity( + "compiled helicity-selector executions require primary helicity materialization", + ) + })?; + let mut assigned_domains = BTreeSet::new(); + for (lane_index, record) in manifest.helicity_selector_executions.iter().enumerate() { + if record.selector_domain_ids.is_empty() { + return Err(RusticolError::integrity(format!( + "compiled helicity-selector execution {lane_index} has no selector domains" + ))); + } + for selector_domain_id in &record.selector_domain_ids { + let domain = recurrence + .selector_domains + .get(*selector_domain_id) + .ok_or_else(|| { + RusticolError::integrity(format!( + "compiled helicity-selector execution {lane_index} references unknown selector domain {selector_domain_id}" + )) + })?; + if domain.id != *selector_domain_id || !domain.complete { + return Err(RusticolError::integrity(format!( + "compiled helicity-selector execution {lane_index} references incomplete selector domain {selector_domain_id}" + ))); + } + if !assigned_domains.insert(*selector_domain_id) { + return Err(RusticolError::integrity(format!( + "compiled helicity-selector executions contain duplicate selector domain {selector_domain_id}" + ))); + } + } + + let selector_manifest = record.execution.as_ref(); + if selector_manifest.helicity_sum_execution.is_some() + || !selector_manifest.color_selector_executions.is_empty() + { + return Err(RusticolError::integrity( + "compiled helicity-selector execution cannot contain a helicity-sum or color-selector auxiliary execution", + )); + } + if !selector_manifest.helicity_selector_executions.is_empty() + && record.schedule_mode != HelicitySelectorScheduleMode::NestedRuntime + { + return Err(RusticolError::integrity( + "only a nested-runtime helicity-selector execution may contain closure lanes", + )); + } + if selector_manifest.physics_reduction.is_none() { + return Err(RusticolError::integrity( + "compiled helicity-selector execution has no reduction override", + )); + } + if selector_manifest.process != manifest.process + || selector_manifest.key != manifest.key + || selector_manifest.color_accuracy != manifest.color_accuracy + || selector_manifest.external_pdg_order != manifest.external_pdg_order + { + return Err(RusticolError::integrity( + "compiled helicity-selector execution does not match its primary process contract", + )); + } + match record.schedule_mode { + HelicitySelectorScheduleMode::ParentClosure => { + if selector_manifest.compiled.lc_topology_replay.is_some() + || selector_manifest.compiled.color_topology_replay.is_some() + || selector_manifest + .runtime_schema + .amplitude_stage + .color_topology_replay + .is_some() + { + return Err(RusticolError::integrity( + "parent-closure helicity-selector execution cannot own colour-topology replay", + )); + } + if selector_manifest + .runtime_schema + .helicity_recurrence + .as_ref() + .is_some_and(|lane_recurrence| lane_recurrence.materialization.is_some()) + { + return Err(RusticolError::integrity( + "parent-closure helicity-selector execution cannot contain helicity materialization", + )); + } + let primary_layout = &manifest.runtime_schema.parameter_layout; + let selector_layout = &selector_manifest.runtime_schema.parameter_layout; + if primary_layout.source_component_parameter_count + != selector_layout.source_component_parameter_count + || primary_layout.value_component_count != selector_layout.value_component_count + || primary_layout.momentum_parameter_count + != selector_layout.momentum_parameter_count + || primary_layout.model_parameter_count != selector_layout.model_parameter_count + || primary_layout.parameter_count_if_flattened + != selector_layout.parameter_count_if_flattened + || primary_layout.source_components_complex + != selector_layout.source_components_complex + || primary_layout.momentum_components_real + != selector_layout.momentum_components_real + || primary_layout.real_valued_inputs != selector_layout.real_valued_inputs + || manifest.runtime_schema.current_storage.component_count + != selector_manifest + .runtime_schema + .current_storage + .component_count + || manifest.runtime_schema.value_storage.component_count + != selector_manifest + .runtime_schema + .value_storage + .component_count + || manifest.runtime_schema.stages.len() + != selector_manifest.runtime_schema.stages.len() + || manifest.runtime_schema.amplitude_stage.output_count + != selector_manifest + .runtime_schema + .amplitude_stage + .output_count + { + return Err(RusticolError::integrity( + "parent-closure helicity-selector execution layout does not match its primary execution", + )); + } + } + HelicitySelectorScheduleMode::NestedRuntime => { + let selector_recurrence = selector_manifest + .runtime_schema + .helicity_recurrence + .as_ref() + .filter(|lane_recurrence| lane_recurrence.materialization.is_some()) + .ok_or_else(|| { + RusticolError::integrity( + "nested-runtime helicity-selector execution requires helicity materialization", + ) + })?; + let selector_materialization = selector_recurrence + .materialization + .as_ref() + .expect("nested-runtime materialization checked"); + let selector_schedule_domains = selector_materialization + .selector_schedules + .iter() + .map(|schedule| schedule.selector_domain_id) + .collect::>(); + let primary_complete_domains = recurrence + .selector_domains + .iter() + .filter(|domain| domain.complete) + .map(|domain| domain.source_states.clone()) + .collect::>(); + let nested_complete_domains = selector_recurrence + .selector_domains + .iter() + .filter(|domain| domain.complete) + .map(|domain| domain.source_states.clone()) + .collect::>(); + if nested_complete_domains != primary_complete_domains { + return Err(RusticolError::integrity(format!( + "nested-runtime helicity-selector execution {lane_index} disagrees with the primary complete-helicity domain contract" + ))); + } + let primary_structural_zeros = recurrence + .structural_zero_selector_domain_ids + .iter() + .map(|domain_id| { + recurrence.selector_domains[*domain_id] + .source_states + .clone() + }) + .collect::>(); + let nested_structural_zeros = selector_recurrence + .structural_zero_selector_domain_ids + .iter() + .map(|domain_id| { + selector_recurrence.selector_domains[*domain_id] + .source_states + .clone() + }) + .collect::>(); + if nested_structural_zeros != primary_structural_zeros { + return Err(RusticolError::integrity(format!( + "nested-runtime helicity-selector execution {lane_index} disagrees with the primary structural-zero domain contract" + ))); + } + for selector_domain_id in &record.selector_domain_ids { + let primary_domain = &recurrence.selector_domains[*selector_domain_id]; + let nested_domain = selector_recurrence + .selector_domains + .iter() + .find(|domain| { + domain.complete && domain.source_states == primary_domain.source_states + }) + .ok_or_else(|| { + RusticolError::integrity(format!( + "nested-runtime helicity-selector execution {lane_index} omits selector domain {selector_domain_id}" + )) + })?; + if !selector_schedule_domains.contains(&nested_domain.id) { + return Err(RusticolError::integrity(format!( + "nested-runtime helicity-selector execution {lane_index} has no materialized schedule for selector domain {selector_domain_id}" + ))); + } + } + } + } + } + Ok(()) +} + +fn validate_color_selector_executions(manifest: &ExecutionManifest) -> RusticolResult<()> { + let mut sector_ids = BTreeSet::new(); + for record in &manifest.color_selector_executions { + if !sector_ids.insert(record.materialized_sector_id) { + return Err(RusticolError::integrity(format!( + "compiled color-selector executions contain duplicate materialized sector {}", + record.materialized_sector_id + ))); + } + let lane = record.execution.as_ref(); + if lane.helicity_sum_execution.is_some() + || !lane.helicity_selector_executions.is_empty() + || !lane.color_selector_executions.is_empty() + { + return Err(RusticolError::integrity( + "compiled color-selector execution cannot contain auxiliary executions", + )); + } + if lane.physics_reduction.is_none() { + return Err(RusticolError::integrity( + "compiled color-selector execution has no reduction override", + )); + } + if lane.process != manifest.process + || lane.key != manifest.key + || lane.color_accuracy != manifest.color_accuracy + || lane.external_pdg_order != manifest.external_pdg_order + { + return Err(RusticolError::integrity( + "compiled color-selector execution does not match its primary process contract", + )); + } + } + Ok(()) +} + +fn ensure_execution_capabilities_supported(manifest: &ExecutionManifest) -> RusticolResult<()> { + let has_helicity_sum_execution = manifest.helicity_sum_execution.is_some(); + let has_helicity_selector_executions = !manifest.helicity_selector_executions.is_empty(); + let has_color_selector_executions = !manifest.color_selector_executions.is_empty(); + if let Some(sum_manifest) = manifest.helicity_sum_execution.as_deref() { + ensure_execution_capabilities_supported(sum_manifest)?; + } + for record in &manifest.helicity_selector_executions { + ensure_execution_capabilities_supported(record.execution.as_ref())?; + } + for record in &manifest.color_selector_executions { + ensure_execution_capabilities_supported(record.execution.as_ref())?; + } + let mut execution_capabilities = BTreeSet::new(); + if let Some(parameters) = &manifest.compiled.model_parameter_evaluator { + ensure_evaluator_capabilities_supported(¶meters.evaluator)?; + let actual = evaluator_runtime_capabilities(¶meters.evaluator)?; + validate_declared_capabilities( + ¶meters.required_runtime_capabilities, + &actual, + "model-parameter evaluator", + )?; + execution_capabilities.extend(actual); + } + if let Some(stages) = &manifest.compiled.stage_evaluators { + let mut stage_capabilities = BTreeSet::new(); + for stage in &stages.stages { + ensure_evaluator_capabilities_supported(&stage.evaluator)?; + stage_capabilities.extend(evaluator_runtime_capabilities(&stage.evaluator)?); + } + ensure_evaluator_capabilities_supported(&stages.amplitude_stage.evaluator)?; + stage_capabilities.extend(evaluator_runtime_capabilities( + &stages.amplitude_stage.evaluator, + )?); + let has_compiled_selector_metadata = manifest + .runtime_schema + .helicity_recurrence + .as_ref() + .is_some_and(|recurrence| recurrence.materialization.is_some()) + || stages + .stages + .iter() + .chain(std::iter::once(&stages.amplitude_stage)) + .flat_map(|stage| &stage.output_slots) + .any(|slot| !slot.color_selector_domain_ids.is_empty()); + if stages + .required_runtime_capabilities + .iter() + .any(|capability| capability == COMPILED_RUNTIME_SELECTORS_CAPABILITY) + { + if !has_compiled_selector_metadata { + return Err(RusticolError::integrity( + "compiled runtime-selector capability has no selector metadata", + )); + } + stage_capabilities.insert(COMPILED_RUNTIME_SELECTORS_CAPABILITY.to_string()); + } + if stages + .required_runtime_capabilities + .iter() + .any(|capability| capability == COMPILED_PLANE_ARENA_RUNTIME_CAPABILITY) + { + let all_direct = stages + .stages + .iter() + .chain(std::iter::once(&stages.amplitude_stage)) + .all(|stage| stage.compiled_plane_arena.is_some()); + if !all_direct { + return Err(RusticolError::integrity( + "compiled-plane-arena-v1 capability has incomplete stage metadata", + )); + } + stage_capabilities.insert(COMPILED_PLANE_ARENA_RUNTIME_CAPABILITY.to_string()); + } + validate_declared_capabilities( + &stages.required_runtime_capabilities, + &stage_capabilities, + "stage evaluator set", + )?; + execution_capabilities.extend(stage_capabilities); + } + if has_helicity_sum_execution { + execution_capabilities.extend( + manifest + .helicity_sum_execution + .as_deref() + .into_iter() + .flat_map(|sum_manifest| { + sum_manifest.required_runtime_capabilities.iter().cloned() + }), + ); + execution_capabilities.insert(COMPILED_HELICITY_DUAL_LANE_CAPABILITY.to_string()); + } + if !has_helicity_sum_execution + && manifest + .runtime_schema + .helicity_recurrence + .as_ref() + .is_some_and(|recurrence| recurrence.materialization.is_some()) + { + execution_capabilities.insert(COMPILED_HELICITY_PRIMARY_RECURRENCE_CAPABILITY.to_string()); + } + if has_helicity_selector_executions { + execution_capabilities.extend(manifest.helicity_selector_executions.iter().flat_map( + |record| { + record + .execution + .required_runtime_capabilities + .iter() + .cloned() + }, + )); + execution_capabilities.insert(COMPILED_HELICITY_SELECTOR_UNION_CAPABILITY.to_string()); + execution_capabilities.insert(COMPILED_RUNTIME_SELECTORS_CAPABILITY.to_string()); + } + if has_color_selector_executions { + execution_capabilities.extend(manifest.color_selector_executions.iter().flat_map( + |record| { + record + .execution + .required_runtime_capabilities + .iter() + .cloned() + }, + )); + execution_capabilities.insert(COMPILED_COLOR_TOPOLOGY_LANES_CAPABILITY.to_string()); + execution_capabilities.insert(COMPILED_RUNTIME_SELECTORS_CAPABILITY.to_string()); + } + if let Some(factorized_block) = manifest + .runtime_schema + .amplitude_stage + .color_contraction + .as_ref() + .and_then(|contraction| contraction.repeated_block.as_ref()) + .and_then(|repeated| repeated.factorized_block.as_ref()) + { + execution_capabilities.insert( + match factorized_block { + GenericFactorizedColorContractionBlockManifest::KleinFourWalsh { .. } => { + COMPILED_COLOR_CONTRACTION_WALSH_CAPABILITY + } + GenericFactorizedColorContractionBlockManifest::ElementaryAbelianWalsh { + .. + } => COMPILED_COLOR_CONTRACTION_WALSH_C2K_CAPABILITY, + } + .to_string(), + ); + } + if manifest.color_contraction_payload.is_some() { + execution_capabilities.insert(SYMMETRIC_GROUP_FFT_COLOR_RUNTIME_CAPABILITY.to_string()); + } + validate_declared_execution_capabilities( + &manifest.required_runtime_capabilities, + &execution_capabilities, + )?; + Ok(()) +} + +fn validate_declared_execution_capabilities( + declared: &[String], + actual: &BTreeSet, +) -> RusticolResult<()> { + let declared_set = declared.iter().cloned().collect::>(); + if declared_set.len() == declared.len() && &declared_set == actual { + return Ok(()); + } + + // Artifacts written before this capability was made explicit already carry + // the complete primary-recurrence materialization. Accept only that exact + // legacy omission; every other declaration mismatch remains an integrity + // error. + let mut legacy_actual = actual.clone(); + if declared_set.len() == declared.len() + && legacy_actual.remove(COMPILED_HELICITY_PRIMARY_RECURRENCE_CAPABILITY) + && declared_set == legacy_actual + { + return Ok(()); + } + + Err(RusticolError::integrity(format!( + "execution manifest capabilities {declared:?} do not match evaluator capabilities {actual:?}" + ))) +} + +#[cfg(all(test, feature = "f64-symjit"))] +mod compiled_plane_arena_contract_tests { + use super::*; + use serde_json::{Value, json}; + + fn source_binding(parameter_index: usize) -> Value { + let (kind, source_id, component, global_component, real_valued) = if parameter_index < 2 { + ("value", parameter_index, 0, parameter_index, false) + } else { + ( + "momentum", + (parameter_index - 2) / 4, + (parameter_index - 2) % 4, + parameter_index, + true, + ) + }; + json!({ + "parameter_index": parameter_index, + "kind": kind, + "source_id": source_id, + "component": component, + "global_component": global_component, + "real_valued": real_valued, + }) + } + + fn arena_manifest_at(optimization_level: u8) -> ExecutionManifest { + let mut value = crate::artifact::tests::minimal_execution_manifest( + "p0", + "a b > c", + SYMJIT_APPLICATION_RUNTIME_CAPABILITY, + crate::artifact::tests::direct_evaluator_manifest("evaluators/direct.symjit"), + ); + value["compiled"]["momentum_slot_ids"] = json!([0, 1, 2]); + let stage = &mut value["compiled"]["stage_evaluators"]["amplitude_stage"]; + stage["parameter_layout"] = json!("stage-local-value-momentum"); + stage["input_components"] = Value::Array((0..14).map(source_binding).collect()); + stage["evaluator"]["optimization_level"] = json!(optimization_level); + stage["evaluator"]["plane_application"] = json!({ + "application_path": "evaluators/direct.plane.symjit", + "application_abi": SYMJIT_PLANE_APPLICATION_ABI, + "storage_abi": SYMJIT_APPLICATION_STORAGE_ABI, + "element_layout": "split-complex-plane-major", + "descriptor_order": "inputs-re-im-then-outputs-re-im", + "input_complex_count": 14, + "output_complex_count": 1, + "input_plane_count": 28, + "output_plane_count": 2, + "compiler_type": "native", + "translation_mode": "symbolica-structured-instructions", + "optimization_level": optimization_level, + "simd": true, + "complex": true, + "fast_math": true, + "fast_complex": false, + "compression": true, + "threading": false, + "direct_arena": true, + "source_digest": "00".repeat(32), + "target": { + "word_bits": 64, + "endianness": "little", + "triple": "test-native", + "cpu_features": [], + }, + }); + stage["compiled_plane_arena"] = json!({ + "schema_version": 1, + "kind": "compiled-plane-arena-stage", + "application_abi": COMPILED_PLANE_DIRECT_APPLICATION_ABI, + "source_application_abi": COMPILED_PLANE_SOURCE_APPLICATION_ABI, + "element_layout": "split-complex-component-major", + "output_operation": "overwrite", + "output_factor": "identity", + "input_output_aliasing": "forbidden", + "output_output_aliasing": "forbidden", + "input_bindings": (0..14).map(source_binding).collect::>(), + "output_bindings": [{ + "output_index": 0, + "arena": "amplitude", + "component": 0, + }], + "leaves": [{ + "application_path": "evaluators/direct.plane.symjit", + "source_application_abi": COMPILED_PLANE_SOURCE_APPLICATION_ABI, + "optimization_level": optimization_level, + "direct_codegen_optimization_level": optimization_level, + "input_len": 14, + "output_len": 1, + "input_indices": (0..14).collect::>(), + "output_start": 0, + "output_stop": 1, + }], + }); + value["compiled"]["stage_evaluators"]["parameter_layout"] = + json!("stage-local-value-momentum"); + value["compiled"]["stage_evaluators"]["required_runtime_capabilities"] = json!([ + COMPILED_PLANE_ARENA_RUNTIME_CAPABILITY, + SYMJIT_APPLICATION_RUNTIME_CAPABILITY, + ]); + value["required_runtime_capabilities"] = json!([ + COMPILED_PLANE_ARENA_RUNTIME_CAPABILITY, + SYMJIT_APPLICATION_RUNTIME_CAPABILITY, + ]); + serde_json::from_value(value).expect("deserialize compiled plane-arena fixture") + } + + fn arena_manifest() -> ExecutionManifest { + arena_manifest_at(3) + } + + #[test] + fn compiled_momentum_slot_selection_is_required_and_covers_final_inputs() { + let mut manifest = arena_manifest(); + assert!(validate_compiled_plane_arena_contract(&manifest).unwrap()); + manifest.compiled.momentum_slot_ids = None; + assert!( + validate_compiled_plane_arena_contract(&manifest) + .unwrap_err() + .message() + .contains("regenerate") + ); + for ids in [ + vec![0, 1], + vec![0, 0, 1, 2], + vec![2, 1, 0], + vec![0, 1, 2, 3], + ] { + manifest.compiled.momentum_slot_ids = Some(ids); + assert!(validate_compiled_plane_arena_contract(&manifest).is_err()); + } + } + + #[test] + fn pre_arena_compiled_f64_requires_actionable_regeneration() { + let value = crate::artifact::tests::minimal_execution_manifest( + "p0", + "a b > c", + SYMJIT_APPLICATION_RUNTIME_CAPABILITY, + crate::artifact::tests::direct_evaluator_manifest("evaluators/direct.symjit"), + ); + let manifest = + serde_json::from_value(value).expect("deserialize pre-arena compiled fixture"); + + let error = validate_compiled_plane_arena_contract(&manifest) + .expect_err("pre-arena compiled f64 must fail closed"); + + let message = error.to_string(); + assert_eq!(error.kind(), crate::RusticolErrorKind::Compatibility); + assert!(message.contains("compiled-plane-arena-v1")); + assert!(message.contains("`pyamplicol generate`")); + } + + #[test] + fn pre_arena_non_o3_compiled_f64_fails_closed() { + let mut value = crate::artifact::tests::minimal_execution_manifest( + "p0", + "a b > c", + SYMJIT_APPLICATION_RUNTIME_CAPABILITY, + crate::artifact::tests::direct_evaluator_manifest("evaluators/direct.symjit"), + ); + value["compiled"]["stage_evaluators"]["amplitude_stage"]["evaluator"]["optimization_level"] = + json!(1); + let manifest = + serde_json::from_value(value).expect("deserialize row-major compiled fixture"); + + let error = validate_compiled_plane_arena_contract(&manifest) + .expect_err("pre-arena non-O3 compiled f64 must fail closed"); + assert_eq!(error.kind(), crate::RusticolErrorKind::Compatibility); + assert!(error.to_string().contains("compiled-plane-arena-v1")); + } + + #[test] + fn compiled_plane_contract_rejects_predecessor_binding_abi() { + let mut manifest = arena_manifest(); + manifest + .compiled + .stage_evaluators + .as_mut() + .unwrap() + .amplitude_stage + .compiled_plane_arena + .as_mut() + .unwrap() + .application_abi = concat!("symjit-direct-", "application-storage-v1").to_string(); + + let error = validate_compiled_plane_arena_contract(&manifest) + .expect_err("the predecessor compiled-plane ABI must fail closed"); + + assert_eq!(error.kind(), crate::RusticolErrorKind::Compatibility); + assert!(error.to_string().contains("incompatible direct ABI")); + assert!(error.to_string().contains("regenerate")); + } + + #[test] + fn complete_plane_contract_supports_jit_o0_through_o3() { + for optimization_level in 0..=3 { + let manifest = arena_manifest_at(optimization_level); + + assert!(validate_compiled_plane_arena_contract(&manifest).unwrap()); + ensure_execution_capabilities_supported(&manifest).unwrap(); + } + assert!( + supported_runtime_capabilities().contains(&COMPILED_PLANE_ARENA_RUNTIME_CAPABILITY) + ); + } + + #[test] + fn plane_contract_rejects_binding_and_capability_drift() { + let mut manifest = arena_manifest(); + manifest + .compiled + .stage_evaluators + .as_mut() + .unwrap() + .amplitude_stage + .compiled_plane_arena + .as_mut() + .unwrap() + .input_bindings[2] + .global_component = 99; + let error = validate_compiled_plane_arena_contract(&manifest).unwrap_err(); + assert_eq!(error.kind(), crate::RusticolErrorKind::Integrity); + assert!(error.to_string().contains("input bindings disagree")); + + let mut manifest = arena_manifest(); + manifest + .compiled + .stage_evaluators + .as_mut() + .unwrap() + .required_runtime_capabilities + .retain(|value| value != COMPILED_PLANE_ARENA_RUNTIME_CAPABILITY); + let error = validate_compiled_plane_arena_contract(&manifest).unwrap_err(); + assert_eq!(error.kind(), crate::RusticolErrorKind::Integrity); + } + + #[test] + fn plane_contract_authenticates_leaf_optimization_level() { + let mut manifest = arena_manifest_at(1); + let stage = &mut manifest + .compiled + .stage_evaluators + .as_mut() + .unwrap() + .amplitude_stage; + stage.compiled_plane_arena.as_mut().unwrap().leaves[0].optimization_level = 2; + + let error = validate_compiled_plane_arena_contract(&manifest).unwrap_err(); + assert_eq!(error.kind(), crate::RusticolErrorKind::Integrity); + assert!(error.to_string().contains("optimization level disagrees")); + } + + #[test] + fn plane_contract_rejects_unknown_jit_optimization_level() { + let mut manifest = arena_manifest(); + let stage = &mut manifest + .compiled + .stage_evaluators + .as_mut() + .unwrap() + .amplitude_stage; + let EvaluatorManifest::SymjitApplication { + optimization_level, + plane_application, + .. + } = &mut stage.evaluator + else { + panic!("compiled plane fixture must use one SymJIT evaluator"); + }; + *optimization_level = 4; + plane_application.as_mut().unwrap().optimization_level = 4; + let leaf = &mut stage.compiled_plane_arena.as_mut().unwrap().leaves[0]; + leaf.optimization_level = 4; + leaf.direct_codegen_optimization_level = 4; + + let error = validate_compiled_plane_arena_contract(&manifest).unwrap_err(); + assert_eq!(error.kind(), crate::RusticolErrorKind::Compatibility); + assert!(error.to_string().contains("levels 0 through 3")); + } +} + +#[cfg(test)] +mod legacy_primary_recurrence_capability_tests { + use super::*; + + fn capabilities(values: &[&str]) -> BTreeSet { + values.iter().map(|value| (*value).to_string()).collect() + } + + #[test] + fn accepts_only_the_legacy_primary_recurrence_omission() { + let direct = "symjit.application.complex-f64.v1"; + let declared = vec![direct.to_string()]; + let actual = capabilities(&[COMPILED_HELICITY_PRIMARY_RECURRENCE_CAPABILITY, direct]); + + validate_declared_execution_capabilities(&declared, &actual) + .expect("the legacy omission remains compatible"); + } + + #[test] + fn rejects_every_other_capability_mismatch() { + let direct = "symjit.application.complex-f64.v1"; + let declared = vec![direct.to_string()]; + let actual = capabilities(&[COMPILED_RUNTIME_SELECTORS_CAPABILITY, direct]); + + let error = validate_declared_execution_capabilities(&declared, &actual) + .expect_err("unrelated missing capabilities remain invalid"); + assert_eq!(error.kind(), crate::RusticolErrorKind::Integrity); + } +} + +#[cfg(all(test, feature = "f64-symjit"))] +mod walsh_color_contraction_capability_tests { + use super::*; + use serde_json::json; + + const DIRECT: &str = "symjit.application.complex-f64.v1"; + + fn manifest(has_factorized_block: bool, declares_capability: bool) -> ExecutionManifest { + let mut value = crate::artifact::tests::minimal_execution_manifest( + "p0", + "a b > c", + DIRECT, + crate::artifact::tests::direct_evaluator_manifest("evaluators/direct.symjit"), + ); + if has_factorized_block { + value["runtime_schema"]["amplitude_stage"]["color_contraction"] = json!({ + "supported": true, + "reason": null, + "group_count": 8, + "includes_color_factor": true, + "entries": [], + "repeated_block": { + "component_count": 2, + "component_group_ids": [0, 1, 2, 3, 4, 5, 6, 7], + "entries": [], + "factorized_block": { + "kind": "klein-four-walsh", + "cosets": [[0, 1, 2, 3]], + }, + }, + }); + } + if declares_capability { + value["required_runtime_capabilities"] = + json!([COMPILED_COLOR_CONTRACTION_WALSH_CAPABILITY, DIRECT]); + } + serde_json::from_value(value).expect("deserialize Walsh capability fixture") + } + + fn c2k_manifest(has_factorized_block: bool, declares_capability: bool) -> ExecutionManifest { + let mut value = crate::artifact::tests::minimal_execution_manifest( + "p0", + "a b > c", + DIRECT, + crate::artifact::tests::direct_evaluator_manifest("evaluators/direct.symjit"), + ); + if has_factorized_block { + value["runtime_schema"]["amplitude_stage"]["color_contraction"] = json!({ + "supported": true, + "reason": null, + "group_count": 16, + "includes_color_factor": true, + "entries": [], + "repeated_block": { + "component_count": 2, + "component_group_ids": [ + 0, 1, 2, 3, 4, 5, 6, 7, + 8, 9, 10, 11, 12, 13, 14, 15, + ], + "entries": [], + "factorized_block": { + "kind": "elementary-abelian-walsh", + "rank": 3, + "cosets": [[0, 1, 2, 3, 4, 5, 6, 7]], + }, + }, + }); + } + if declares_capability { + value["required_runtime_capabilities"] = + json!([COMPILED_COLOR_CONTRACTION_WALSH_C2K_CAPABILITY, DIRECT]); + } + serde_json::from_value(value).expect("deserialize C2^k Walsh capability fixture") + } + + #[test] + fn supported_capabilities_include_walsh_color_contraction() { + assert!( + supported_runtime_capabilities().contains(&COMPILED_COLOR_CONTRACTION_WALSH_CAPABILITY) + ); + assert!( + supported_runtime_capabilities() + .contains(&COMPILED_COLOR_CONTRACTION_WALSH_C2K_CAPABILITY) + ); + } + + #[test] + fn walsh_color_contraction_requires_an_exact_capability_declaration() { + ensure_execution_capabilities_supported(&manifest(false, false)) + .expect("legacy execution without Walsh metadata"); + ensure_execution_capabilities_supported(&manifest(true, true)) + .expect("Walsh execution with matching capability"); + + for manifest in [manifest(true, false), manifest(false, true)] { + let error = ensure_execution_capabilities_supported(&manifest) + .expect_err("Walsh capability drift must fail closed"); + assert_eq!(error.kind(), crate::RusticolErrorKind::Integrity, "{error}"); + assert!( + error + .to_string() + .contains(COMPILED_COLOR_CONTRACTION_WALSH_CAPABILITY), + "{error}" + ); + } + } + + #[test] + fn c2k_walsh_color_contraction_requires_its_distinct_exact_capability() { + ensure_execution_capabilities_supported(&c2k_manifest(true, true)) + .expect("C2^k Walsh execution with matching capability"); + + for manifest in [c2k_manifest(true, false), c2k_manifest(false, true), { + let mut manifest = c2k_manifest(true, true); + manifest.required_runtime_capabilities[0] = + COMPILED_COLOR_CONTRACTION_WALSH_CAPABILITY.to_string(); + manifest + }] { + let error = ensure_execution_capabilities_supported(&manifest) + .expect_err("C2^k Walsh capability drift must fail closed"); + assert_eq!(error.kind(), crate::RusticolErrorKind::Integrity, "{error}"); + } + } +} + +fn validate_declared_capabilities( + declared: &[String], + actual: &BTreeSet, + context: &str, +) -> RusticolResult<()> { + let declared_set = declared.iter().cloned().collect::>(); + if declared_set.len() != declared.len() || &declared_set != actual { + return Err(RusticolError::integrity(format!( + "{context} capabilities {declared:?} do not match evaluator capabilities {actual:?}" + ))); + } + Ok(()) +} + +#[cfg(test)] +mod lc_topology_replay_contract_tests { + use super::*; + + fn proof() -> LcTopologyReplayProofManifest { + LcTopologyReplayProofManifest { + status: "proven".to_string(), + algorithm: Some("canonical-contract-test-v1".to_string()), + digest: Some("a".repeat(64)), + } + } + + fn labels(values: &[(usize, usize)]) -> Vec { + values + .iter() + .map( + |(representative_label, sector_label)| LcTopologyReplayLabelPermutationManifest { + representative_label: *representative_label, + sector_label: *sector_label, + }, + ) + .collect() + } + + fn permutation( + sector_id: i64, + weight: f64, + sign: i8, + mapping: &[(usize, usize)], + ) -> LcTopologyReplaySectorPermutationManifest { + LcTopologyReplaySectorPermutationManifest { + sector_id, + weight, + sign, + factor: Some(vec![weight * f64::from(sign), 0.0]), + label_permutation: labels(mapping), + } + } + + fn group( + representative_sector_id: i64, + active_sector_ids: &[i64], + target_sector_id: i64, + sign: i8, + ) -> LcTopologyReplayGroupManifest { + LcTopologyReplayGroupManifest { + representative_sector_id, + materialized_sector_id: representative_sector_id, + active_sector_ids: active_sector_ids.to_vec(), + proof: Some(proof()), + sector_permutations: vec![ + permutation(representative_sector_id, 1.0, 1, &[(1, 1), (2, 2)]), + permutation(target_sector_id, 1.0, sign, &[(1, 2), (2, 1)]), + ], + } + } + + fn v2_replay() -> LcTopologyReplayManifest { + LcTopologyReplayManifest { + enabled: true, + mode: "external-label-permutation".to_string(), + contract_version: Some(2), + physical_sector_count: Some(5), + replayed_sector_count: 4, + materialized_sector_ids: vec![0, 2, 4], + residual_sector_ids: vec![4], + groups: vec![group(0, &[0, 1], 1, -1), group(2, &[2, 3], 3, 1)], + } + } + + #[test] + fn v2_deduplicates_mappings_and_keeps_groups_and_residuals_additive() { + let data = build_lc_topology_replay_data(Some(&v2_replay())).unwrap(); + + assert_eq!(data.mappings, vec![vec![], vec![(0, 1), (1, 0)]]); + assert_eq!(data.materialized_sector_ids, BTreeSet::from([0, 2, 4])); + assert_eq!( + data.routes[0] + .iter() + .map(|route| (route.physical_sector_id, route.materialized_sector_id)) + .collect::>(), + vec![(0, 0), (2, 2), (4, 4)] + ); + assert_eq!( + data.routes[1] + .iter() + .map(|route| (route.physical_sector_id, route.materialized_sector_id)) + .collect::>(), + vec![(1, 0), (3, 2)] + ); + assert_eq!(data.routes[1][0].sign, -1); + assert_eq!(data.routes[1][0].amplitude_factor, [-1.0, 0.0]); + assert_eq!(data.routes[1][0].squared_reduction_weight(), 1.0); + } + + #[test] + fn v2_rejects_malformed_physical_and_materialized_coverage() { + let mut replay = v2_replay(); + replay.materialized_sector_ids = vec![0, 2]; + let error = build_lc_topology_replay_data(Some(&replay)).unwrap_err(); + assert!(error.to_string().contains("materialized coverage")); + + let mut replay = v2_replay(); + replay.physical_sector_count = Some(6); + let error = build_lc_topology_replay_data(Some(&replay)).unwrap_err(); + assert!(error.to_string().contains("every physical sector")); + } + + #[test] + fn v2_rejects_unproven_groups_and_inconsistent_signed_factors() { + let mut replay = v2_replay(); + replay.groups[0].proof.as_mut().unwrap().status = "assumed".to_string(); + let error = build_lc_topology_replay_data(Some(&replay)).unwrap_err(); + assert!(error.to_string().contains("requires a proven")); + + let mut replay = v2_replay(); + replay.groups[0].sector_permutations[1].factor = Some(vec![1.0, 0.0]); + let error = build_lc_topology_replay_data(Some(&replay)).unwrap_err(); + assert!( + error + .to_string() + .contains("does not match its weight and sign") + ); + } + + #[test] + fn legacy_contract_remains_loadable() { + let mut replay = v2_replay(); + replay.contract_version = None; + replay.physical_sector_count = None; + replay.materialized_sector_ids.clear(); + replay.residual_sector_ids.clear(); + replay.replayed_sector_count = 4; + for group in &mut replay.groups { + group.proof = None; + for permutation in &mut group.sector_permutations { + permutation.factor = None; + } + } + + let data = build_lc_topology_replay_data(Some(&replay)).unwrap(); + assert_eq!(data.materialized_sector_ids, BTreeSet::from([0, 2])); + assert_eq!(data.routes.iter().map(Vec::len).sum::(), 4); + } +} + +#[cfg(test)] +mod helicity_recurrence_contract_tests { + use super::*; + use serde_json::json; + + #[cfg(feature = "f64-symjit")] + #[test] + fn compiled_parent_detach_preserves_selected_materialized_stage_schedule() { + const DIRECT: &str = "symjit.application.complex-f64.v1"; + let mut value = crate::artifact::tests::minimal_execution_manifest( + "p0", + "a > a", + DIRECT, + crate::artifact::tests::direct_evaluator_manifest("evaluators/direct.symjit"), + ); + value["external_pdg_order"] = json!([1]); + value["runtime_schema"]["external_particles"] + .as_array_mut() + .expect("external-particle fixture") + .truncate(1); + value["runtime_schema"]["current_storage"]["current_slots"][1]["is_source"] = json!(false); + value["runtime_schema"]["source_fill"]["source_count"] = json!(1); + value["runtime_schema"]["source_fill"]["sources"] + .as_array_mut() + .expect("source fixture") + .truncate(1); + let source = &mut value["runtime_schema"]["source_fill"]["sources"][0]; + source["source_helicity"] = json!(-1); + source["chirality"] = json!(-1); + source["spin_state"] = json!(-1); + source["source_ir"]["states"] = json!([ + {"helicity": -1, "chirality": -1, "spin_state": -1}, + {"helicity": 1, "chirality": 1, "spin_state": 1}, + ]); + let root = &mut value["runtime_schema"]["amplitude_stage"]["roots"][0]; + root["left_current_id"] = json!(1); + root["right_current_id"] = json!(0); + + let evaluators = &mut value["compiled"]["stage_evaluators"]; + evaluators["parameter_layout"] = json!("stage-local-value-momentum"); + evaluators["parameter_count"] = json!(0); + evaluators["value_parameter_count"] = json!(0); + evaluators["momentum_parameter_count"] = json!(0); + evaluators["real_valued_inputs"] = json!([]); + evaluators["stage_count"] = json!(2); + let mut current_stage = evaluators["amplitude_stage"].clone(); + current_stage["stage_index"] = json!(1); + current_stage["stage_kind"] = json!("current-combine"); + current_stage["subset_size"] = json!(1); + current_stage["evaluator_label"] = json!("materialized_current"); + current_stage["parameter_layout"] = json!("stage-local-value-momentum"); + current_stage["output_slots"] = json!([{ + "value_slot_id": 1, + "current_id": 1, + "variant": "unpropagated", + "component_start": 0, + "component_stop": 1, + "output_start": 0, + "output_stop": 1, + }]); + current_stage["input_value_slot_ids"] = json!([0]); + current_stage["output_value_slot_ids"] = json!([1]); + current_stage["input_components"] = json!([{ + "kind": "value", + "source_id": 0, + "component": 0, + "global_component": 0, + "parameter_index": 0, + "real_valued": false, + }]); + current_stage["parameter_count"] = json!(1); + current_stage["value_parameter_count"] = json!(1); + current_stage["momentum_parameter_count"] = json!(0); + current_stage["real_valued_inputs"] = json!([]); + current_stage["evaluator"]["input_len"] = json!(1); + evaluators["stages"] = json!([current_stage]); + + let amplitude = &mut evaluators["amplitude_stage"]; + amplitude["parameter_layout"] = json!("stage-local-value-momentum"); + amplitude["input_components"] = json!([ + { + "kind": "value", "source_id": 0, "component": 0, + "global_component": 0, "parameter_index": 0, + "real_valued": false, + }, + { + "kind": "value", "source_id": 1, "component": 0, + "global_component": 1, "parameter_index": 1, + "real_valued": false, + }, + ]); + amplitude["parameter_count"] = json!(2); + amplitude["value_parameter_count"] = json!(2); + amplitude["momentum_parameter_count"] = json!(0); + amplitude["real_valued_inputs"] = json!([]); + amplitude["evaluator"]["input_len"] = json!(2); + + let mut manifest = serde_json::from_value::(value) + .expect("deserialize compiled-parent fixture"); + let (recurrence, _) = valid_materialized_contract(); + manifest.runtime_schema.helicity_recurrence = Some(recurrence); + + let (stage_evaluators, helicity_recurrence) = + detach_compiled_helicity_load_inputs(&mut manifest).unwrap(); + + assert!(stage_evaluators.is_some()); + assert!(manifest.compiled.stage_evaluators.is_none()); + assert!(manifest.runtime_schema.helicity_recurrence.is_none()); + let materialization = helicity_recurrence + .as_ref() + .and_then(|recurrence| recurrence.materialization.as_ref()) + .expect("compiled parent retained its selected-helicity schedule"); + assert_eq!( + materialization.selector_schedules[0].active_stage_chunk_indices, + vec![vec![0]] + ); + assert_eq!( + materialization.selector_schedules[0].active_amplitude_chunk_indices, + vec![0] + ); + + let mut runtime = ExecutionRuntime::from_manifest(manifest).unwrap(); + runtime.helicity_recurrence = helicity_recurrence; + assert!( + runtime + .helicity_recurrence + .as_ref() + .is_some_and(|recurrence| recurrence.materialization.is_some()) + ); + } + + #[cfg(feature = "f64-symjit")] + #[test] + fn helicity_selector_lane_rejects_parent_layout_mismatch() { + let value = crate::artifact::tests::minimal_helicity_selector_lane_execution(); + let mut manifest = serde_json::from_value::(value) + .expect("deserialize selector-lane fixture"); + manifest.helicity_selector_executions[0] + .execution + .runtime_schema + .parameter_layout + .value_component_count += 1; + + let error = validate_helicity_selector_executions(&manifest).unwrap_err(); + + assert_eq!(error.kind(), crate::RusticolErrorKind::Integrity, "{error}"); + assert!( + error + .to_string() + .contains("layout does not match its primary execution"), + "{error}" + ); + } + + #[cfg(feature = "f64-symjit")] + #[test] + fn parent_closure_helicity_selector_lane_rejects_topology_replay() { + let mut value = crate::artifact::tests::minimal_helicity_selector_lane_execution(); + value["helicity_selector_executions"][0]["execution"]["compiled"]["color_topology_replay"] = + json!({"enabled": true}); + let manifest = serde_json::from_value::(value) + .expect("deserialize replay-owning selector-lane fixture"); + + let error = validate_helicity_selector_executions(&manifest).unwrap_err(); + + assert_eq!(error.kind(), crate::RusticolErrorKind::Integrity, "{error}"); + assert!( + error + .to_string() + .contains("cannot own colour-topology replay"), + "{error}" + ); + } + + #[cfg(feature = "f64-symjit")] + #[test] + fn helicity_selector_lane_capabilities_are_aggregated_from_every_record() { + let value = crate::artifact::tests::minimal_helicity_selector_lane_execution(); + let manifest = serde_json::from_value::(value) + .expect("deserialize selector-lane fixture"); + + ensure_execution_capabilities_supported(&manifest).unwrap(); + } + + #[cfg(feature = "f64-symjit")] + #[test] + fn primary_recurrence_legacy_omission_does_not_mask_other_capability_drift() { + let mut value = crate::artifact::tests::minimal_helicity_selector_lane_execution(); + value["required_runtime_capabilities"] + .as_array_mut() + .expect("capability array") + .retain(|capability| { + !matches!( + capability.as_str(), + Some(COMPILED_HELICITY_PRIMARY_RECURRENCE_CAPABILITY) + | Some(COMPILED_RUNTIME_SELECTORS_CAPABILITY) + ) + }); + let manifest = serde_json::from_value::(value) + .expect("deserialize selector-lane fixture"); + + let error = ensure_execution_capabilities_supported(&manifest).unwrap_err(); + + assert_eq!(error.kind(), crate::RusticolErrorKind::Integrity, "{error}"); + assert!( + error + .to_string() + .contains(COMPILED_RUNTIME_SELECTORS_CAPABILITY), + "{error}" + ); + } + + #[cfg(feature = "f64-symjit")] + #[test] + fn nested_runtime_helicity_selector_lane_accepts_its_own_domain_schedule() { + let mut value = crate::artifact::tests::minimal_helicity_selector_lane_execution(); + value["helicity_selector_executions"][0]["schedule_mode"] = json!("nested-runtime"); + let child = value["helicity_selector_executions"][0]["execution"].clone(); + let mut nested_recurrence = crate::artifact::tests::mock_helicity_materialization(); + nested_recurrence["materialization"]["selector_schedules"] = json!([ + { + "selector_domain_id": 0, + "active_current_ids": [0, 1], + "active_root_ids": [0], + "structural_zero": false, + }, + { + "selector_domain_id": 1, + "active_current_ids": [0, 1], + "active_root_ids": [0], + "structural_zero": false, + }, + ]); + value["helicity_selector_executions"][0]["execution"]["runtime_schema"]["helicity_recurrence"] = + nested_recurrence; + value["helicity_selector_executions"][0]["execution"]["helicity_selector_executions"] = json!([ + { + "selector_domain_ids": [0], + "schedule_mode": "parent-closure", + "execution": child.clone(), + }, + { + "selector_domain_ids": [1], + "schedule_mode": "parent-closure", + "execution": child, + }, + ]); + let manifest = serde_json::from_value::(value) + .expect("deserialize nested-runtime selector-lane fixture"); + + validate_helicity_selector_executions(&manifest).unwrap(); + validate_helicity_selector_executions( + manifest.helicity_selector_executions[0].execution.as_ref(), + ) + .unwrap(); + } + + fn digest(character: char) -> String { + std::iter::repeat_n(character, 64).collect() + } + + fn proof(digest: &str) -> HelicityRecurrenceProofManifest { + HelicityRecurrenceProofManifest { + status: "proven".to_string(), + algorithm: HELICITY_RECURRENCE_PROOF_ALGORITHM.to_string(), + digest: digest.to_string(), + transition_contract_ids: Vec::new(), + } + } + + fn domain(id: usize, helicity: i32) -> HelicitySelectorDomainManifest { + HelicitySelectorDomainManifest { + id, + complete: true, + source_states: vec![HelicitySelectorSourceStateManifest { + external_label: 1, + helicity, + }], + } + } + + fn source_mapping( + current_id: usize, + helicity: i32, + chirality: i32, + selector_domain_id: usize, + source_digest: char, + ) -> HelicitySourceStateMappingManifest { + HelicitySourceStateMappingManifest { + current_id, + external_label: 1, + helicity, + chirality, + spin_state: GenericSourceSpinStateManifest::Scalar(helicity), + declared_state_index: 0, + selector_domain_id, + recurrence_class_id: format!("helicity-current-sha256:{}", digest('a')), + representative_current_id: 0, + source_contract_digest: digest(source_digest), + factor: [1.0, 0.0], + } + } + + fn valid_recurrence() -> HelicityRecurrenceManifest { + let current_digest = digest('a'); + let amplitude_digest = digest('b'); + HelicityRecurrenceManifest { + kind: HELICITY_RECURRENCE_KIND.to_string(), + contract_version: HELICITY_RECURRENCE_CONTRACT_VERSION, + proof_algorithm: HELICITY_RECURRENCE_PROOF_ALGORITHM.to_string(), + current_count: 2, + amplitude_root_count: 1, + proof_counts: HelicityRecurrenceProofCountsManifest { + recurrence_class_count: 1, + optimized_recurrence_class_count: 1, + optimized_current_count: 2, + residual_current_count: 0, + amplitude_class_count: 1, + optimized_amplitude_class_count: 0, + residual_amplitude_count: 0, + source_state_mapping_count: 2, + physical_helicity_count: 2, + structural_zero_helicity_count: 0, + }, + selector_domains: vec![domain(0, -1), domain(1, 1)], + source_state_mappings: vec![ + source_mapping(0, -1, -1, 0, 'c'), + source_mapping(1, 1, 1, 1, 'd'), + ], + recurrence_classes: vec![HelicityRecurrenceClassManifest { + class_id: format!("helicity-current-sha256:{current_digest}"), + representative_current_id: 0, + external_labels: vec![1], + source_class: true, + members: vec![ + HelicityCurrentReplayMemberManifest { + current_id: 0, + selector_domain_id: 0, + factor: [1.0, 0.0], + }, + HelicityCurrentReplayMemberManifest { + current_id: 1, + selector_domain_id: 1, + factor: [1.0, 0.0], + }, + ], + proof: proof(¤t_digest), + }], + amplitude_classes: vec![HelicityAmplitudeReplayClassManifest { + class_id: format!("helicity-amplitude-sha256:{amplitude_digest}"), + representative_root_id: 0, + members: vec![HelicityAmplitudeReplayMemberManifest { + root_id: 0, + selector_domain_ids: vec![0, 1], + factor: [1.0, 0.0], + }], + proof: proof(&litude_digest), + }], + residual_current_ids: Vec::new(), + residual_root_ids: Vec::new(), + structural_zero_selector_domain_ids: Vec::new(), + diagnostics: Vec::new(), + materialization: None, + } + } + + fn source_schema(helicity: i32, chirality: i32) -> HelicityRecurrenceSchemaSource { + HelicityRecurrenceSchemaSource { + external_label: 1, + helicity, + chirality, + spin_state: GenericSourceSpinStateManifest::Scalar(helicity), + declared_states: vec![GenericSourceStateIrManifest { + helicity, + chirality, + spin_state: GenericSourceSpinStateManifest::Scalar(helicity), + }], + } + } + + fn valid_schema() -> HelicityRecurrenceSchemaView { + HelicityRecurrenceSchemaView { + external_count: 1, + currents: vec![ + HelicityRecurrenceSchemaCurrent { + is_source: true, + external_labels: vec![1], + }, + HelicityRecurrenceSchemaCurrent { + is_source: true, + external_labels: vec![1], + }, + ], + sources: BTreeMap::from([(0, source_schema(-1, -1)), (1, source_schema(1, 1))]), + roots: vec![HelicityRecurrenceSchemaRoot { + output_index: 0, + left_current_id: 0, + right_current_id: 1, + helicity_weight: 2.0, + }], + compiled_chunks: None, + } + } + + fn valid_materialized_contract() -> (HelicityRecurrenceManifest, HelicityRecurrenceSchemaView) { + let mut recurrence = valid_recurrence(); + recurrence.current_count = 4; + recurrence.proof_counts.recurrence_class_count = 2; + recurrence.proof_counts.optimized_recurrence_class_count = 2; + recurrence.proof_counts.optimized_current_count = 4; + recurrence.source_state_mappings[1].declared_state_index = 1; + let generated_digest = digest('e'); + recurrence + .recurrence_classes + .push(HelicityRecurrenceClassManifest { + class_id: format!("helicity-current-sha256:{generated_digest}"), + representative_current_id: 2, + external_labels: vec![1], + source_class: false, + members: vec![ + HelicityCurrentReplayMemberManifest { + current_id: 2, + selector_domain_id: 0, + factor: [1.0, 0.0], + }, + HelicityCurrentReplayMemberManifest { + current_id: 3, + selector_domain_id: 1, + factor: [1.0, 0.0], + }, + ], + proof: proof(&generated_digest), + }); + recurrence.materialization = Some(HelicityRecurrenceMaterializationManifest { + kind: HELICITY_MATERIALIZATION_KIND.to_string(), + contract_version: HELICITY_MATERIALIZATION_CONTRACT_VERSION, + strategy: HelicityMaterializationStrategy::Quotient, + proof_current_count: 4, + proof_root_count: 1, + materialized_current_count: 2, + materialized_root_count: 1, + proof_to_materialized_current: vec![0, 0, 1, 1], + source_routes: vec![ + HelicityMaterializedSourceRouteManifest { + materialized_current_id: 0, + external_label: 1, + helicity: -1, + chirality: -1, + spin_state: GenericSourceSpinStateManifest::Scalar(-1), + declared_state_index: 0, + selector_domain_id: 0, + factor: [1.0, 0.0], + }, + HelicityMaterializedSourceRouteManifest { + materialized_current_id: 0, + external_label: 1, + helicity: 1, + chirality: 1, + spin_state: GenericSourceSpinStateManifest::Scalar(1), + declared_state_index: 1, + selector_domain_id: 1, + factor: [1.0, 0.0], + }, + ], + amplitude_routes: vec![HelicityMaterializedAmplitudeRouteManifest { + materialized_root_id: 0, + selector_domain_ids: vec![0, 1], + factor: [1.0, 0.0], + residual: false, + }], + selector_schedules: vec![ + HelicityMaterializedSelectorScheduleManifest { + selector_domain_id: 0, + active_current_ids: vec![0, 1], + active_root_ids: vec![0], + structural_zero: false, + }, + HelicityMaterializedSelectorScheduleManifest { + selector_domain_id: 1, + active_current_ids: vec![0, 1], + active_root_ids: vec![0], + structural_zero: false, + }, + ], + }); + let source = HelicityRecurrenceSchemaSource { + external_label: 1, + helicity: -1, + chirality: -1, + spin_state: GenericSourceSpinStateManifest::Scalar(-1), + declared_states: vec![ + GenericSourceStateIrManifest { + helicity: -1, + chirality: -1, + spin_state: GenericSourceSpinStateManifest::Scalar(-1), + }, + GenericSourceStateIrManifest { + helicity: 1, + chirality: 1, + spin_state: GenericSourceSpinStateManifest::Scalar(1), + }, + ], + }; + let schema = HelicityRecurrenceSchemaView { + external_count: 1, + currents: vec![ + HelicityRecurrenceSchemaCurrent { + is_source: true, + external_labels: vec![1], + }, + HelicityRecurrenceSchemaCurrent { + is_source: false, + external_labels: vec![1], + }, + ], + sources: BTreeMap::from([(0, source)]), + roots: vec![HelicityRecurrenceSchemaRoot { + output_index: 0, + left_current_id: 1, + right_current_id: 0, + helicity_weight: 1.0, + }], + compiled_chunks: Some(HelicityCompiledChunkCoverage { + stages: vec![HelicityCompiledStageChunkCoverage { + current_ids_by_chunk: vec![BTreeSet::from([1])], + input_current_ids_by_chunk: vec![BTreeSet::from([0])], + }], + amplitude_root_ids_by_chunk: vec![BTreeSet::from([0])], + amplitude_input_current_ids_by_chunk: vec![BTreeSet::from([0, 1])], + }), + }; + (recurrence, schema) + } + + fn valid_retained_materialized_contract() + -> (HelicityRecurrenceManifest, HelicityRecurrenceSchemaView) { + let (mut recurrence, compact_schema) = valid_materialized_contract(); + let materialization = recurrence + .materialization + .as_mut() + .expect("materialization fixture"); + materialization.strategy = HelicityMaterializationStrategy::RetainedProofGraph; + materialization.materialized_current_count = 4; + materialization.materialized_root_count = 1; + materialization.proof_to_materialized_current = vec![0, 1, 2, 3]; + materialization.source_routes[1].materialized_current_id = 1; + materialization.amplitude_routes[0].factor = [2.0_f64.sqrt().recip(), 0.0]; + for schedule in &mut materialization.selector_schedules { + schedule.active_current_ids = vec![0, 2]; + } + + let source = compact_schema.sources[&0].clone(); + let mut flipped_source = source.clone(); + flipped_source.helicity = 1; + flipped_source.chirality = 1; + flipped_source.spin_state = GenericSourceSpinStateManifest::Scalar(1); + let schema = HelicityRecurrenceSchemaView { + external_count: 1, + currents: vec![ + HelicityRecurrenceSchemaCurrent { + is_source: true, + external_labels: vec![1], + }, + HelicityRecurrenceSchemaCurrent { + is_source: true, + external_labels: vec![1], + }, + HelicityRecurrenceSchemaCurrent { + is_source: false, + external_labels: vec![1], + }, + HelicityRecurrenceSchemaCurrent { + is_source: false, + external_labels: vec![1], + }, + ], + sources: BTreeMap::from([(0, source), (1, flipped_source)]), + roots: vec![HelicityRecurrenceSchemaRoot { + output_index: 0, + left_current_id: 2, + right_current_id: 0, + helicity_weight: 2.0, + }], + compiled_chunks: Some(HelicityCompiledChunkCoverage { + stages: vec![HelicityCompiledStageChunkCoverage { + current_ids_by_chunk: vec![BTreeSet::from([2]), BTreeSet::from([3])], + input_current_ids_by_chunk: vec![BTreeSet::from([0]), BTreeSet::from([1])], + }], + amplitude_root_ids_by_chunk: vec![BTreeSet::from([0])], + amplitude_input_current_ids_by_chunk: vec![BTreeSet::from([0, 2])], + }), + }; + (recurrence, schema) + } + + fn materialized_load_error( + recurrence: &HelicityRecurrenceManifest, + schema: &HelicityRecurrenceSchemaView, + ) -> String { + build_helicity_recurrence_runtime_from_view(Some(recurrence), schema) + .unwrap_err() + .to_string() + } + + fn load_error(recurrence: &HelicityRecurrenceManifest) -> String { + build_helicity_recurrence_runtime_from_view(Some(recurrence), &valid_schema()) + .unwrap_err() + .to_string() + } + + #[test] + fn valid_contract_builds_compact_runtime_and_absence_remains_compatible() { + let schema = valid_schema(); + assert!( + build_helicity_recurrence_runtime_from_view(None, &schema) + .unwrap() + .is_none() + ); + + let runtime = + build_helicity_recurrence_runtime_from_view(Some(&valid_recurrence()), &schema) + .unwrap() + .unwrap(); + + assert_eq!(runtime.selector_domains.len(), 2); + assert_eq!(runtime.recurrence_classes.len(), 1); + assert_eq!(runtime.recurrence_classes[0].members.len(), 2); + assert_eq!(runtime.source_state_mappings.len(), 2); + assert_eq!(runtime.source_state_mappings[1].recurrence_class_index, 0); + assert_eq!(runtime.amplitude_classes.len(), 1); + assert!(runtime.residual_current_ids.is_empty()); + assert!(runtime.residual_root_ids.is_empty()); + } + + #[test] + fn strict_wire_types_reject_unknown_fields_and_bad_factor_shapes() { + let error = serde_json::from_value::(json!({ + "status": "proven", + "algorithm": HELICITY_RECURRENCE_PROOF_ALGORITHM, + "digest": digest('a'), + "transition_contract_ids": [], + "unexpected": true, + })) + .unwrap_err(); + assert!(error.to_string().contains("unknown field")); + + let error = serde_json::from_value::(json!({ + "current_id": 0, + "selector_domain_id": 0, + "factor": [1.0], + })) + .unwrap_err(); + assert!(error.to_string().contains("length 2")); + + let error = serde_json::from_value::(json!({ + "selector_domain_id": 0, + "active_current_ids": [], + "active_root_ids": [], + "structural_zero": true, + "unexpected": true, + })) + .unwrap_err(); + assert!(error.to_string().contains("unknown field")); + } + + #[test] + fn contract_and_proof_drift_are_rejected() { + let mut recurrence = valid_recurrence(); + recurrence.contract_version += 1; + assert!(load_error(&recurrence).contains("contract version")); + + let mut recurrence = valid_recurrence(); + recurrence.recurrence_classes[0].proof.status = "assumed".to_string(); + assert!(load_error(&recurrence).contains("invalid proof metadata")); + + let mut recurrence = valid_recurrence(); + recurrence.amplitude_classes[0].proof.digest = "A".repeat(64); + assert!(load_error(&recurrence).contains("lowercase SHA-256")); + } + + #[test] + fn selector_domains_must_be_contiguous_unique_and_complete() { + let mut recurrence = valid_recurrence(); + recurrence.selector_domains[1].id = 4; + assert!(load_error(&recurrence).contains("must be contiguous")); + + let mut recurrence = valid_recurrence(); + recurrence.selector_domains[1].source_states[0].helicity = -1; + assert!(load_error(&recurrence).contains("repeat a source-state assignment")); + + let mut recurrence = valid_recurrence(); + recurrence.selector_domains[0].complete = false; + assert!(load_error(&recurrence).contains("inconsistent complete coverage")); + } + + #[test] + fn stale_current_ids_and_residual_overlap_are_rejected() { + let mut recurrence = valid_recurrence(); + recurrence.recurrence_classes[0].members[1].current_id = 2; + assert!(load_error(&recurrence).contains("stale current id")); + + let mut recurrence = valid_recurrence(); + recurrence.residual_current_ids = vec![0]; + assert!(load_error(&recurrence).contains("overlaps a proven recurrence class")); + + let mut recurrence = valid_recurrence(); + recurrence.recurrence_classes[0].members.pop(); + assert!(load_error(&recurrence).contains("do not cover the proof DAG")); + } + + #[test] + fn source_state_mappings_are_checked_against_domains_classes_and_source_ir() { + let mut recurrence = valid_recurrence(); + recurrence.source_state_mappings[0].declared_state_index = 1; + assert!(load_error(&recurrence).contains("stale declared state index")); + + let mut recurrence = valid_recurrence(); + recurrence.source_state_mappings[0].recurrence_class_id = "wrong".to_string(); + assert!(load_error(&recurrence).contains("inconsistent with its recurrence class")); + + let mut recurrence = valid_recurrence(); + recurrence.source_state_mappings.pop(); + assert!(load_error(&recurrence).contains("do not exactly cover")); + } + + #[test] + fn factors_root_overlap_and_structural_zero_overlap_are_rejected() { + let mut recurrence = valid_recurrence(); + recurrence.recurrence_classes[0].members[0].factor = [0.0, 0.0]; + assert!(load_error(&recurrence).contains("invalid recurrence factor")); + + let mut recurrence = valid_recurrence(); + recurrence.recurrence_classes[0].members[0].factor = [-1.0, 0.0]; + assert!(load_error(&recurrence).contains("identity factor")); + + let mut recurrence = valid_recurrence(); + recurrence.residual_root_ids = vec![0]; + assert!(load_error(&recurrence).contains("overlaps a proven amplitude class")); + + let mut recurrence = valid_recurrence(); + recurrence.structural_zero_selector_domain_ids = vec![0]; + assert!(load_error(&recurrence).contains("disjoint from amplitude coverage")); + } + + #[test] + fn structural_zero_domains_may_reference_unmaterialized_source_states() { + let mut recurrence = valid_recurrence(); + recurrence.selector_domains.push(domain(2, 0)); + recurrence.structural_zero_selector_domain_ids.push(2); + recurrence.proof_counts.physical_helicity_count = 3; + recurrence.proof_counts.structural_zero_helicity_count = 1; + + build_helicity_recurrence_runtime_from_view(Some(&recurrence), &valid_schema()) + .expect("structural-zero domains do not require executable source states"); + } + + #[test] + fn nonzero_selector_domains_may_replay_from_an_executable_anchor() { + let mut recurrence = valid_recurrence(); + recurrence.selector_domains.push(domain(2, 0)); + recurrence.amplitude_classes[0].members[0] + .selector_domain_ids + .push(2); + recurrence.proof_counts.physical_helicity_count = 3; + let mut schema = valid_schema(); + schema.roots[0].helicity_weight = 3.0; + + build_helicity_recurrence_runtime_from_view(Some(&recurrence), &schema) + .expect("certified replay group contains directly executable domains"); + } + + #[test] + fn nonzero_replay_groups_require_a_directly_executable_anchor() { + let mut recurrence = valid_recurrence(); + recurrence.selector_domains.push(domain(2, 0)); + recurrence.amplitude_classes[0].members[0].selector_domain_ids = vec![2]; + recurrence.proof_counts.physical_helicity_count = 3; + let mut schema = valid_schema(); + schema.roots[0].helicity_weight = 1.0; + + let error = materialized_load_error(&recurrence, &schema); + assert!( + error.contains("no directly executable source-state anchor"), + "{error}" + ); + } + + #[test] + fn proof_counts_and_final_runtime_counts_must_match() { + let mut recurrence = valid_recurrence(); + recurrence.proof_counts.optimized_current_count = 99; + assert!(load_error(&recurrence).contains("proof counts")); + + let mut recurrence = valid_recurrence(); + recurrence.current_count = 3; + assert!(load_error(&recurrence).contains("final runtime schema")); + } + + #[test] + fn materialized_contract_accepts_larger_proof_space_and_derives_active_chunks() { + let (recurrence, schema) = valid_materialized_contract(); + let runtime = build_helicity_recurrence_runtime_from_view(Some(&recurrence), &schema) + .unwrap() + .unwrap(); + let materialization = runtime.materialization.unwrap(); + + assert_eq!( + materialization.proof_to_materialized_current, + vec![0, 0, 1, 1] + ); + assert_eq!(materialization.source_routes.len(), 2); + assert_eq!(materialization.amplitude_routes.len(), 1); + assert_eq!(materialization.selector_schedules.len(), 2); + assert_eq!( + materialization.selector_schedules[0].active_stage_chunk_indices, + vec![vec![0]] + ); + assert_eq!( + materialization.selector_schedules[0].active_amplitude_chunk_indices, + vec![0] + ); + } + + #[test] + fn retained_materialization_accepts_identity_proof_graph() { + let (recurrence, schema) = valid_retained_materialized_contract(); + let runtime = build_helicity_recurrence_runtime_from_view(Some(&recurrence), &schema) + .unwrap() + .unwrap(); + let materialization = runtime.materialization.unwrap(); + + assert_eq!( + materialization.strategy, + HelicityMaterializationStrategy::RetainedProofGraph + ); + assert_eq!( + materialization.proof_to_materialized_current, + vec![0, 1, 2, 3] + ); + assert_eq!( + materialization.selector_schedules[0].active_stage_chunk_indices, + vec![vec![0]] + ); + } + + #[test] + fn retained_materialization_rejects_nonidentity_current_map() { + let (mut recurrence, schema) = valid_retained_materialized_contract(); + recurrence + .materialization + .as_mut() + .unwrap() + .proof_to_materialized_current + .swap(0, 1); + + assert!(materialized_load_error(&recurrence, &schema).contains("identity current map")); + } + + #[test] + fn materialized_contract_rejects_stale_maps_and_overlapping_targets() { + let (mut recurrence, schema) = valid_materialized_contract(); + recurrence + .materialization + .as_mut() + .unwrap() + .proof_to_materialized_current + .pop(); + assert!(materialized_load_error(&recurrence, &schema).contains("incorrect length")); + + let (mut recurrence, schema) = valid_materialized_contract(); + recurrence + .materialization + .as_mut() + .unwrap() + .proof_to_materialized_current = vec![0, 0, 0, 0]; + assert!(materialized_load_error(&recurrence, &schema).contains("distinct proof classes")); + } + + #[test] + fn materialized_routes_and_selector_domains_are_strict() { + let (mut recurrence, schema) = valid_materialized_contract(); + recurrence.materialization.as_mut().unwrap().source_routes[1].declared_state_index = 0; + assert!(materialized_load_error(&recurrence, &schema).contains("inconsistent")); + + let (mut recurrence, schema) = valid_materialized_contract(); + recurrence + .materialization + .as_mut() + .unwrap() + .amplitude_routes[0] + .selector_domain_ids = vec![1, 0]; + assert!( + materialized_load_error(&recurrence, &schema) + .contains("ordered complete selector domains") + ); + } + + #[test] + fn materialized_schedules_require_compiled_coverage_for_active_currents() { + let (recurrence, mut schema) = valid_materialized_contract(); + schema.compiled_chunks.as_mut().unwrap().stages[0].current_ids_by_chunk[0].clear(); + assert!( + materialized_load_error(&recurrence, &schema) + .contains("without compiled stage output coverage") + ); + + let (mut recurrence, schema) = valid_materialized_contract(); + recurrence + .materialization + .as_mut() + .unwrap() + .selector_schedules[0] + .active_root_ids + .clear(); + assert!( + materialized_load_error(&recurrence, &schema) + .contains("active roots disagree with amplitude routes") + ); + } + + #[test] + fn materialized_schedules_reject_cross_domain_chunks_and_missing_ancestors() { + let (recurrence, mut schema) = valid_retained_materialized_contract(); + schema.compiled_chunks.as_mut().unwrap().stages[0].current_ids_by_chunk[0].insert(3); + assert!( + materialized_load_error(&recurrence, &schema) + .contains("crosses selector domains through currents [3]") + ); + + let (recurrence, mut schema) = valid_materialized_contract(); + schema.compiled_chunks.as_mut().unwrap().stages[0].input_current_ids_by_chunk[0] = + BTreeSet::from([1]); + assert!( + materialized_load_error(&recurrence, &schema) + .contains("stage 0 evaluator chunk 0 is missing compiled ancestors [1]") + ); + + let (recurrence, mut schema) = valid_retained_materialized_contract(); + schema + .compiled_chunks + .as_mut() + .unwrap() + .amplitude_input_current_ids_by_chunk[0] = BTreeSet::from([3]); + assert!( + materialized_load_error(&recurrence, &schema) + .contains("amplitude evaluator chunk 0 is missing compiled ancestors [3]") + ); + } + + #[test] + fn compiled_color_schedule_rejects_a_missing_transitive_ancestor() { + let chunks = HelicityCompiledChunkCoverage { + stages: vec![ + HelicityCompiledStageChunkCoverage { + current_ids_by_chunk: vec![BTreeSet::from([1])], + input_current_ids_by_chunk: vec![BTreeSet::from([0])], + }, + HelicityCompiledStageChunkCoverage { + current_ids_by_chunk: vec![BTreeSet::from([2])], + input_current_ids_by_chunk: vec![BTreeSet::from([1])], + }, + ], + amplitude_root_ids_by_chunk: vec![BTreeSet::from([0])], + amplitude_input_current_ids_by_chunk: vec![BTreeSet::from([2])], + }; + + validate_compiled_selector_schedule_closure( + "compiled color selector sector 0", + &[true, false, false], + &chunks, + &[vec![0], vec![0]], + &[0], + None, + None, + ) + .unwrap(); + + let error = validate_compiled_selector_schedule_closure( + "compiled color selector sector 0", + &[true, false, false], + &chunks, + &[Vec::new(), vec![0]], + &[0], + None, + None, + ) + .unwrap_err(); + assert!( + error + .to_string() + .contains("stage 1 evaluator chunk 0 is missing compiled ancestors [1]") + ); + } + + #[test] + fn serialized_chunk_input_maps_resolve_actual_current_dependencies() { + let leaf = |name: &str| EvaluatorManifest::CompiledComplex { + runtime_capability: "test.runtime".to_string(), + function_name: name.to_string(), + input_len: 1, + output_len: 1, + library_path: "test.so".to_string(), + evaluator_state_path: None, + number_type: "complex-f64".to_string(), + native_direct_application: None, + }; + let stage = GenericSerializedStageEvaluatorManifest { + stage_index: 1, + stage_kind: "current-combine".to_string(), + subset_size: Some(2), + evaluator_label: "mapped-input-stage".to_string(), + parameter_layout: "stage-local-value-momentum".to_string(), + output_length: 2, + output_slots: Vec::new(), + input_value_slot_ids: vec![10, 11], + output_value_slot_ids: Vec::new(), + interaction_ids: Vec::new(), + input_components: vec![ + GenericStageInputComponentManifest { + kind: "value".to_string(), + source_id: 10, + component: 0, + global_component: 0, + parameter_index: 0, + real_valued: false, + }, + GenericStageInputComponentManifest { + kind: "value".to_string(), + source_id: 11, + component: 0, + global_component: 1, + parameter_index: 1, + real_valued: false, + }, + ], + parameter_count: 2, + value_parameter_count: 2, + momentum_parameter_count: 0, + model_parameter_count: 0, + real_valued_inputs: Vec::new(), + expression_ready: true, + blockers: Vec::new(), + evaluator: EvaluatorManifest::Chunked { + required_runtime_capabilities: Vec::new(), + input_len: Some(2), + chunk_input_indices: Some(vec![vec![1], vec![0]]), + chunks: vec![leaf("right"), leaf("left")], + }, + compiled_plane_arena: None, + }; + + let dependencies = compiled_input_current_ids_by_chunk( + &stage, + &CompiledValueInputLayout { + current_ids_by_slot: BTreeMap::from([(10, 0), (11, 1)]), + current_ids_by_global_component: vec![Some(0), Some(1)], + }, + 2, + ) + .unwrap(); + + assert_eq!(dependencies, vec![BTreeSet::from([1]), BTreeSet::from([0])]); + + let mut mismatched = stage; + mismatched.input_components[0].global_component = 1; + let error = compiled_input_current_ids_by_chunk( + &mismatched, + &CompiledValueInputLayout { + current_ids_by_slot: BTreeMap::from([(10, 0), (11, 1)]), + current_ids_by_global_component: vec![Some(0), Some(1)], + }, + 2, + ) + .unwrap_err(); + assert!( + error + .to_string() + .contains("value-slot and global-component dependencies disagree") + ); + } + + #[test] + fn serialized_output_ranges_derive_stage_and_amplitude_chunk_coverage() { + fn leaf(output_len: usize) -> EvaluatorManifest { + EvaluatorManifest::CompiledComplex { + runtime_capability: "test.runtime".to_string(), + function_name: "test".to_string(), + input_len: 0, + output_len, + library_path: "test.so".to_string(), + evaluator_state_path: None, + number_type: "complex-f64".to_string(), + native_direct_application: None, + } + } + fn chunked(output_lengths: &[usize]) -> EvaluatorManifest { + EvaluatorManifest::Chunked { + required_runtime_capabilities: Vec::new(), + input_len: Some(0), + chunk_input_indices: Some(vec![Vec::new(); output_lengths.len()]), + chunks: output_lengths.iter().copied().map(leaf).collect(), + } + } + fn stage( + stage_index: usize, + label: &str, + output_slots: Vec, + output_lengths: &[usize], + ) -> GenericSerializedStageEvaluatorManifest { + GenericSerializedStageEvaluatorManifest { + stage_index, + stage_kind: label.to_string(), + subset_size: Some(stage_index + 1), + evaluator_label: label.to_string(), + parameter_layout: "stage-local-value-momentum".to_string(), + output_length: output_lengths.iter().sum(), + output_slots, + input_value_slot_ids: Vec::new(), + output_value_slot_ids: Vec::new(), + interaction_ids: Vec::new(), + input_components: Vec::new(), + parameter_count: 0, + value_parameter_count: 0, + momentum_parameter_count: 0, + model_parameter_count: 0, + real_valued_inputs: Vec::new(), + expression_ready: true, + blockers: Vec::new(), + evaluator: chunked(output_lengths), + compiled_plane_arena: None, + } + } + fn slot( + current_id: isize, + output_start: usize, + output_stop: usize, + ) -> GenericStageOutputSlotManifest { + GenericStageOutputSlotManifest { + value_slot_id: current_id, + current_id, + variant: if current_id < 0 { + "amplitude-root".to_string() + } else { + "unpropagated".to_string() + }, + component_start: output_start, + component_stop: output_stop, + output_start, + output_stop, + color_selector_domain_ids: Vec::new(), + } + } + + let stages = GenericStageEvaluatorArtifactsManifest { + kind: "generic-dag-stage-evaluator-artifacts".to_string(), + required_runtime_capabilities: Vec::new(), + runtime_available: true, + runtime_unavailable_message: None, + parameter_count: 0, + value_parameter_count: 0, + momentum_parameter_count: 0, + model_parameter_count: 0, + real_valued_inputs: Vec::new(), + parameter_layout: "stage-local-value-momentum".to_string(), + stage_count: 1, + stages: vec![stage( + 1, + "current-combine", + vec![slot(0, 0, 2), slot(1, 2, 4)], + &[2, 2], + )], + amplitude_stage: stage( + 0, + "amplitude-roots", + vec![slot(-1, 0, 1), slot(-1, 1, 2)], + &[1, 1], + ), + }; + let roots = vec![ + HelicityRecurrenceSchemaRoot { + output_index: 0, + left_current_id: 0, + right_current_id: 0, + helicity_weight: 1.0, + }, + HelicityRecurrenceSchemaRoot { + output_index: 1, + left_current_id: 1, + right_current_id: 1, + helicity_weight: 1.0, + }, + ]; + let coverage = build_helicity_compiled_chunk_coverage_from_parts( + &stages, + 2, + &roots, + &CompiledValueInputLayout { + current_ids_by_slot: BTreeMap::new(), + current_ids_by_global_component: Vec::new(), + }, + ) + .unwrap(); + + assert_eq!( + coverage.stages[0].current_ids_by_chunk, + vec![BTreeSet::from([0]), BTreeSet::from([1])] + ); + assert_eq!( + coverage.amplitude_root_ids_by_chunk, + vec![BTreeSet::from([0]), BTreeSet::from([1])] + ); + + let mut inactive_chunk = stage( + 1, + "inactive-chunk", + vec![slot(0, 0, 2), slot(1, 2, 4)], + &[2, 2], + ); + inactive_chunk.output_slots[0].color_selector_domain_ids = vec![0]; + assert_eq!( + compiled_color_domains_by_chunk(&inactive_chunk, &BTreeSet::from([0_i64])).unwrap(), + vec![BTreeSet::from([0]), BTreeSet::new()] + ); + + let mut cross_domain = stage(1, "cross-domain", vec![slot(0, 0, 2), slot(1, 2, 4)], &[4]); + cross_domain.output_slots[0].color_selector_domain_ids = vec![0]; + cross_domain.output_slots[1].color_selector_domain_ids = vec![1]; + let error = compiled_color_domains_by_chunk(&cross_domain, &BTreeSet::from([0_i64, 1_i64])) + .unwrap_err(); + assert!(error.to_string().contains("crosses selector domains")); + } +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/engine/source_metadata_tests.rs b/pyamplicol/rust/crates/rusticol-core/src/engine/source_metadata_tests.rs new file mode 100644 index 0000000000..39998a3b0e --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/engine/source_metadata_tests.rs @@ -0,0 +1,777 @@ +// SPDX-License-Identifier: 0BSD + +use super::*; +use serde_json::{Value, json}; + +#[test] +fn resolved_source_inputs_match_formula_bits_for_all_families_and_crossings() { + let external_count = 3; + let flat = (0..9) + .flat_map(|index| [10.0 + index as f64, 1.0, -0.0, 3.0]) + .collect::>(); + let crossing = [ + InputCrossingMapEntry { + target_index: 0, + source_index: 2, + sign: -1.0, + }, + InputCrossingMapEntry { + target_index: 1, + source_index: 0, + sign: -1.0, + }, + InputCrossingMapEntry { + target_index: 2, + source_index: 1, + sign: 1.0, + }, + ]; + for (family, dimension, orientation, mass) in [ + ("scalar", 1, "self-conjugate", 0.0), + ("fermion", 2, "particle", 0.0), + ("fermion", 2, "antiparticle", 0.0), + ("fermion", 4, "particle", 2.0), + ("fermion", 4, "antiparticle", 2.0), + ("vector", 4, "self-conjugate", 0.0), + ("vector", 4, "self-conjugate", 2.0), + ("spin2", 16, "self-conjugate", 2.0), + ] { + let mut source = source_record(900, -900, orientation, family, dimension); + source.leg_label = 2; + for helicity in [-1, 1] { + for chirality in [-1, 1] { + let state = GenericSourceStateIrManifest { + helicity, + chirality, + spin_state: GenericSourceSpinStateManifest::Scalar(helicity), + }; + for phase in [[1.0, 0.0], [0.0, 1.0], [0.6, -0.8]] { + source.applied_crossing.phase = phase; + for transform in [ + GenericMomentumTransformManifest::Identity, + GenericMomentumTransformManifest::NegateFourMomentum, + ] { + source.applied_crossing.momentum_transform = transform; + // Re-resolve changed masses, just as each compiled tile does. + for mass in [mass, mass * 1.5] { + let masses = BTreeMap::from([(-900, mass)]); + let resolved = sources::ResolvedSourceWavefunction::new( + &source, + &state, + external_count, + &masses, + dimension, + ) + .unwrap(); + for lookup in [None, Some(crossing.as_slice())] { + let contiguous = + F64MomentumBatchView::from_contiguous_prevalidated( + &flat, + 3, + external_count, + lookup, + ) + .unwrap(); + let nested = contiguous.materialize_nested(); + let nested_view = + F64MomentumBatchView::from_nested(&nested, external_count) + .unwrap(); + for batch in [contiguous, nested_view] { + for (start, stop) in [(0, 3), (1, 2)] { + let batch = batch.subview(start, stop).unwrap(); + for point_index in 0..batch.point_count() { + let point = batch.point(point_index); + let mut momentum = point.momentum(1).unwrap(); + if transform == GenericMomentumTransformManifest::NegateFourMomentum { + momentum = negate(momentum); + } + let mut expected = + match (family, dimension, orientation) { + ("scalar", _, _) => vec![c64(1.0, 0.0)], + ("fermion", 2, "particle") => { + ext_quark_weyl_array( + momentum, helicity, chirality, + ) + .to_vec() + } + ("fermion", 2, _) => ext_antiquark_weyl_array( + momentum, helicity, chirality, + ) + .to_vec(), + ("fermion", 4, "particle") => { + ext_quark_dirac_massive( + momentum, helicity, mass, + ) + .to_vec() + } + ("fermion", 4, _) => { + ext_antiquark_dirac_massive( + momentum, helicity, mass, + ) + .to_vec() + } + ("vector", _, _) if mass == 0.0 => { + ext_gluon(momentum, helicity).to_vec() + } + ("vector", _, _) => { + ext_massive_vector(momentum, helicity, mass) + .to_vec() + } + ("spin2", _, _) => { + ext_spin2(momentum, helicity, mass) + .unwrap() + .to_vec() + } + _ => unreachable!(), + }; + if phase != [1.0, 0.0] { + for value in &mut expected { + *value *= c64(phase[0], phase[1]); + } + } + let mut observed = + vec![c64(17.0, -19.0); dimension + 2]; + resolved + .write(&point, &mut observed[..dimension]) + .unwrap(); + for (observed, expected) in + observed.iter().zip(expected) + { + assert_eq!( + observed.re.to_bits(), + expected.re.to_bits() + ); + assert_eq!( + observed.im.to_bits(), + expected.im.to_bits() + ); + } + assert!( + observed[dimension..] + .iter() + .all(|&v| v == c64(17.0, -19.0)) + ); + } + } + } + } + } + } + } + } + } + } +} + +#[test] +fn resolved_source_inputs_reject_invalid_metadata_and_absent_points() { + let mut source = source_record(25, 25, "self-conjugate", "scalar", 1); + let state = ExecutionRuntime::default_runtime_source_state(&source).unwrap(); + let masses = BTreeMap::new(); + assert!(sources::ResolvedSourceWavefunction::new(&source, &state, 1, &masses, 2).is_err()); + source.leg_label = 0; + assert!(sources::ResolvedSourceWavefunction::new(&source, &state, 1, &masses, 1).is_err()); + source.leg_label = 2; + assert!(sources::ResolvedSourceWavefunction::new(&source, &state, 1, &masses, 1).is_err()); + source.leg_label = 1; + source.source_kind = "unsupported".to_owned(); + assert!(sources::ResolvedSourceWavefunction::new(&source, &state, 1, &masses, 1).is_err()); + source.source_kind = "external-wavefunction".to_owned(); + source.source_ir.component_dimension = 3; + assert!(sources::ResolvedSourceWavefunction::new(&source, &state, 1, &masses, 3).is_err()); + source.source_ir.component_dimension = 1; + let resolved = + sources::ResolvedSourceWavefunction::new(&source, &state, 1, &masses, 1).unwrap(); + let mut output = [c64(17.0, -19.0)]; + assert!( + resolved + .write(&Vec::<[f64; 4]>::new(), &mut output) + .is_err() + ); + assert_eq!(output, [c64(17.0, -19.0)]); +} + +fn crossing_value( + momentum_transform: &str, + helicity_factor: i32, + chirality_factor: i32, + spin_state_factor: i32, +) -> Value { + json!({ + "momentum_transform": momentum_transform, + "helicity_factor": helicity_factor, + "chirality_factor": chirality_factor, + "spin_state_factor": spin_state_factor, + "phase": [1.0, 0.0], + }) +} + +fn source_value( + particle_id: i32, + anti_particle_id: i32, + source_orientation: &str, + wavefunction_kind: &str, + dimension: usize, +) -> Value { + let basis = match (wavefunction_kind, dimension) { + ("fermion", 2) => "weyl-chiral", + ("fermion", 4) => "dirac", + ("scalar", _) => "scalar", + ("vector", _) => "lorentz-vector", + ("spin2", _) => "lorentz-tensor-rank2", + _ => "test-basis", + }; + let statistics = if wavefunction_kind == "fermion" { + "fermion" + } else { + "boson" + }; + let chirality = if wavefunction_kind == "fermion" { 1 } else { 0 }; + let identity_crossing = crossing_value("identity", 1, 1, 1); + json!({ + "source_id": 0, + "current_id": 0, + "current_component_start": 0, + "current_component_stop": dimension, + "value_slot": { + "value_slot_id": 0, + "current_id": 0, + "variant": "source", + "component_start": 0, + "component_stop": dimension, + "dimension": dimension, + }, + "source_parameter_start": 0, + "source_parameter_stop": dimension, + "leg_label": 1, + "input_momentum_slot": 0, + "side": "final", + "crossing": "identity", + "physical_pdg": particle_id, + "outgoing_pdg": particle_id, + "particle_id": particle_id, + "anti_particle_id": anti_particle_id, + "source_kind": "external-wavefunction", + "wavefunction_kind": wavefunction_kind, + "source_orientation": source_orientation, + "source_basis": basis, + "source_ir": { + "identity": { + "canonical_id": format!("model:test:state:{particle_id}"), + "species_id": format!("model:test:species:{}", particle_id.abs()), + "anti_canonical_id": format!("model:test:state:{anti_particle_id}"), + "display_name": format!("state_{particle_id}"), + "anti_display_name": format!("state_{anti_particle_id}"), + "pdg_label": particle_id, + "anti_pdg_label": anti_particle_id, + "orientation": source_orientation, + "self_conjugate": particle_id == anti_particle_id, + }, + "statistics": statistics, + "wavefunction_family": wavefunction_kind, + "component_dimension": dimension, + "states": [{"helicity": 1, "chirality": chirality, "spin_state": 1}], + "crossing": identity_crossing, + "basis": basis, + "mass_parameter": null, + "width_parameter": null, + }, + "applied_crossing": crossing_value("identity", 1, 1, 1), + "source_helicity": 1, + "chirality": chirality, + "spin_state": 1, + "dimension": dimension, + "helicity_ancestry": 1, + "color_state": {}, + }) +} + +fn initial_crossed_source_value( + particle_id: i32, + anti_particle_id: i32, + source_orientation: &str, +) -> Value { + let mut source = source_value( + particle_id, + anti_particle_id, + source_orientation, + "fermion", + 2, + ); + let crossing = crossing_value("negate-four-momentum", 1, -1, -1); + source["side"] = json!("initial"); + source["crossing"] = json!("negate-incoming-momentum"); + source["source_ir"]["crossing"] = crossing.clone(); + source["applied_crossing"] = crossing; + source["chirality"] = json!(-1); + source["spin_state"] = json!(-1); + source +} + +fn source_record( + particle_id: i32, + anti_particle_id: i32, + source_orientation: &str, + wavefunction_kind: &str, + dimension: usize, +) -> GenericSourceRecordManifest { + serde_json::from_value(source_value( + particle_id, + anti_particle_id, + source_orientation, + wavefunction_kind, + dimension, + )) + .expect("source manifest record") +} + +#[test] +fn runtime_source_state_selects_declared_helicity_and_applies_factor() { + let mut value = source_value(21, 21, "self-conjugate", "vector", 4); + value["source_ir"]["states"] = json!([ + {"helicity": 1, "chirality": 0, "spin_state": 1}, + {"helicity": -1, "chirality": 0, "spin_state": -1}, + ]); + let source: GenericSourceRecordManifest = + serde_json::from_value(value).expect("runtime-selectable vector source"); + let runtime_state = ExecutionRuntime::runtime_source_state(&source, 1, c64(0.0, 1.0)) + .expect("declared runtime source state"); + assert_eq!(runtime_state.state.helicity, -1); + + let point = [[10.0, 2.0, 1.0, 9.746_794_344_808_963]]; + let masses = BTreeMap::new(); + let mut raw = vec![c64(0.0, 0.0); 4]; + ExecutionRuntime::write_source_wavefunction_with_state( + &source, + &runtime_state.state, + 1, + &masses, + &point, + &mut raw, + ) + .expect("raw selected source"); + let mut factored = vec![c64(0.0, 0.0); 4]; + ExecutionRuntime::fill_sources_row_with_states( + std::slice::from_ref(&source), + std::slice::from_ref(&runtime_state), + 1, + &masses, + &mut factored, + &point, + ) + .expect("factored selected source"); + for (raw_component, factored_component) in raw.iter().zip(&factored) { + assert_eq!(*factored_component, *raw_component * c64(0.0, 1.0)); + } +} + +#[test] +fn inactive_runtime_source_state_zero_fills_its_slot() { + let source = source_record(21, 21, "self-conjugate", "vector", 4); + let runtime_state = ExecutionRuntime::inactive_runtime_source_state(&source) + .expect("inactive runtime source state"); + let point = [[10.0, 2.0, 1.0, 9.746_794_344_808_963]]; + let masses = BTreeMap::new(); + let mut row = vec![c64(7.0, -3.0); 4]; + ExecutionRuntime::fill_sources_row_with_states( + std::slice::from_ref(&source), + std::slice::from_ref(&runtime_state), + 1, + &masses, + &mut row, + &point, + ) + .expect("zero-filled inactive source"); + assert!(row.iter().all(|value| *value == c64(0.0, 0.0))); +} + +#[test] +fn runtime_source_helicity_lookup_is_exact_and_rejects_ambiguity() { + let mut value = source_value(21, 21, "self-conjugate", "vector", 4); + value["source_ir"]["states"] = json!([ + {"helicity": 1, "chirality": 0, "spin_state": 1}, + {"helicity": -1, "chirality": 0, "spin_state": -1}, + ]); + let source: GenericSourceRecordManifest = + serde_json::from_value(value.clone()).expect("runtime-selectable vector source"); + assert_eq!( + ExecutionRuntime::runtime_source_state_index_for_helicity(&source, 1).unwrap(), + Some(0) + ); + assert_eq!( + ExecutionRuntime::runtime_source_state_index_for_helicity(&source, -1).unwrap(), + Some(1) + ); + assert_eq!( + ExecutionRuntime::runtime_source_state_index_for_helicity(&source, 0).unwrap(), + None + ); + + value["source_ir"]["states"] = json!([ + {"helicity": 1, "chirality": -1, "spin_state": -1}, + {"helicity": 1, "chirality": 1, "spin_state": 1}, + ]); + let ambiguous: GenericSourceRecordManifest = + serde_json::from_value(value).expect("same-helicity source states"); + assert!( + ExecutionRuntime::runtime_source_state_index_for_helicity(&ambiguous, 1) + .unwrap_err() + .to_string() + .contains("multiple runtime states") + ); +} + +#[test] +fn source_manifest_requires_typed_source_and_crossing_records() { + for field in ["source_ir", "applied_crossing"] { + let mut missing = source_value(810_001, -810_001, "particle", "fermion", 2); + missing + .as_object_mut() + .expect("source object") + .remove(field); + + let error = serde_json::from_value::(missing).unwrap_err(); + + assert!(error.to_string().contains(field)); + } +} + +#[test] +fn source_manifest_requires_complete_strict_source_ir() { + let mut missing_orientation = source_value(810_001, -810_001, "particle", "fermion", 2); + missing_orientation["source_ir"]["identity"] + .as_object_mut() + .expect("identity object") + .remove("orientation"); + let error = + serde_json::from_value::(missing_orientation).unwrap_err(); + assert!(error.to_string().contains("orientation")); + + let mut missing_nullable = source_value(810_001, -810_001, "particle", "fermion", 2); + missing_nullable["source_ir"] + .as_object_mut() + .expect("SourceIR object") + .remove("mass_parameter"); + let error = + serde_json::from_value::(missing_nullable).unwrap_err(); + assert!(error.to_string().contains("mass_parameter")); + + let mut unknown = source_value(810_001, -810_001, "particle", "fermion", 2); + unknown["source_ir"]["identity"]["taxonomy"] = json!("sm-fermion"); + let error = serde_json::from_value::(unknown).unwrap_err(); + assert!(error.to_string().contains("unknown field")); +} + +#[test] +fn pre_source_ir_schema_v3_reports_actionable_regeneration_error() { + let mut missing = source_value(810_001, -810_001, "particle", "fermion", 2); + missing.as_object_mut().unwrap().remove("source_ir"); + let parse_error = serde_json::from_value::(missing).unwrap_err(); + + let error = execution_manifest_parse_error("processes/example/execution.json", parse_error); + + assert_eq!(error.kind(), crate::RusticolErrorKind::Compatibility); + assert!(error.to_string().contains("typed source metadata")); + assert!(error.to_string().contains("regenerate")); +} + +#[test] +fn repeated_source_records_must_share_one_canonical_source_ir() { + let first = source_record(810_001, -810_001, "particle", "fermion", 2); + let mut second = source_record(810_001, -810_001, "particle", "fermion", 2); + second.source_id = 1; + second.current_id = 1; + second.source_ir.crossing.phase = [0.0, 1.0]; + let mut canonical_by_slot = BTreeMap::new(); + let mut canonical_by_identity = BTreeMap::new(); + + validate_consistent_source_ir( + &mut canonical_by_slot, + &mut canonical_by_identity, + 0, + &first, + ) + .expect("first source is canonical"); + let error = validate_consistent_source_ir( + &mut canonical_by_slot, + &mut canonical_by_identity, + 1, + &second, + ) + .unwrap_err(); + + assert!(error.to_string().contains("canonical SourceIR")); + assert!(error.to_string().contains("momentum slot")); +} + +#[test] +fn separate_legs_share_one_source_ir_per_oriented_particle() { + let first = source_record(810_001, -810_001, "particle", "fermion", 2); + let mut second = source_record(810_001, -810_001, "particle", "fermion", 2); + second.source_id = 1; + second.current_id = 1; + second.input_momentum_slot = 1; + second.source_ir.crossing.phase = [0.0, 1.0]; + let mut canonical_by_slot = BTreeMap::new(); + let mut canonical_by_identity = BTreeMap::new(); + + validate_consistent_source_ir( + &mut canonical_by_slot, + &mut canonical_by_identity, + 0, + &first, + ) + .expect("first source is canonical"); + let error = validate_consistent_source_ir( + &mut canonical_by_slot, + &mut canonical_by_identity, + 1, + &second, + ) + .unwrap_err(); + + assert!(error.to_string().contains("canonical SourceIR")); + assert!(error.to_string().contains("oriented particle")); +} + +#[test] +fn represented_antiparticle_identities_must_form_an_involution() { + let particle = source_record(810_001, -810_001, "particle", "fermion", 2); + let mut antiparticle = source_record(-810_001, 810_001, "antiparticle", "fermion", 2); + antiparticle.source_id = 1; + antiparticle.current_id = 1; + antiparticle.input_momentum_slot = 1; + let mut canonical_by_slot = BTreeMap::new(); + let mut canonical_by_identity = BTreeMap::new(); + + validate_consistent_source_ir( + &mut canonical_by_slot, + &mut canonical_by_identity, + 0, + &particle, + ) + .expect("particle source is valid"); + validate_consistent_source_ir( + &mut canonical_by_slot, + &mut canonical_by_identity, + 1, + &antiparticle, + ) + .expect("valid antiparticle relation is accepted"); + + let mut inconsistent = antiparticle; + inconsistent.source_ir.identity.species_id = "model:test:species:other".to_string(); + let mut canonical_by_slot = BTreeMap::new(); + let mut canonical_by_identity = BTreeMap::new(); + validate_consistent_source_ir( + &mut canonical_by_slot, + &mut canonical_by_identity, + 0, + &particle, + ) + .expect("particle source is valid"); + let error = validate_consistent_source_ir( + &mut canonical_by_slot, + &mut canonical_by_identity, + 1, + &inconsistent, + ) + .unwrap_err(); + + assert!(error.to_string().contains("non-involutive")); +} + +#[test] +fn source_manifest_validation_rejects_self_conjugate_fermions() { + let source = source_record(810_001, 810_001, "self-conjugate", "fermion", 2); + + let error = validate_source_wavefunction_metadata(0, &source).unwrap_err(); + + assert!( + error + .to_string() + .contains("unsupported self-conjugate fermion source") + ); +} + +#[test] +fn source_manifest_validation_rejects_family_dimension_mismatches() { + let source = source_record(910_101, 910_101, "self-conjugate", "vector", 2); + + let error = validate_source_wavefunction_metadata(0, &source).unwrap_err(); + + assert!(error.to_string().contains("wavefunction kind \"vector\"")); +} + +#[test] +fn source_manifest_validation_checks_flattened_projections() { + let mut value = source_value(810_001, -810_001, "particle", "fermion", 2); + value["wavefunction_kind"] = json!("scalar"); + let source = serde_json::from_value(value).expect("source manifest record"); + + let error = validate_source_wavefunction_metadata(0, &source).unwrap_err(); + + assert!(error.to_string().contains("\"wavefunction_kind\"")); +} + +#[test] +fn source_manifest_validation_checks_statistics_against_family() { + let mut value = source_value(810_001, -810_001, "particle", "fermion", 2); + value["source_ir"]["statistics"] = json!("boson"); + let source = serde_json::from_value(value).expect("source manifest record"); + + let error = validate_source_wavefunction_metadata(0, &source).unwrap_err(); + + assert!(error.to_string().contains("statistics disagree")); +} + +#[test] +fn source_manifest_validation_enforces_side_dependent_crossing() { + let mut final_value = source_value(810_001, -810_001, "particle", "fermion", 2); + final_value["crossing"] = json!("negate-incoming-momentum"); + final_value["applied_crossing"] = crossing_value("negate-four-momentum", 1, 1, 1); + let final_source = serde_json::from_value(final_value).expect("final source record"); + let error = validate_source_wavefunction_metadata(0, &final_source).unwrap_err(); + assert!(error.to_string().contains("final-state applied crossing")); + + let mut initial_value = initial_crossed_source_value(810_001, -810_001, "particle"); + initial_value["crossing"] = json!("identity"); + initial_value["applied_crossing"] = crossing_value("identity", 1, 1, 1); + let initial_source = serde_json::from_value(initial_value).expect("initial source record"); + let error = validate_source_wavefunction_metadata(0, &initial_source).unwrap_err(); + assert!(error.to_string().contains("declared SourceIR crossing")); +} + +#[test] +fn source_manifest_validation_checks_crossed_current_state() { + let mut value = initial_crossed_source_value(810_001, -810_001, "particle"); + value["chirality"] = json!(1); + let source = serde_json::from_value(value).expect("initial source record"); + + let error = validate_source_wavefunction_metadata(0, &source).unwrap_err(); + + assert!(error.to_string().contains("current state is not declared")); +} + +#[test] +fn source_manifest_validation_rejects_zero_and_nonfinite_crossing_phases() { + for phase in [[0.0, 0.0], [f64::INFINITY, 0.0]] { + let mut source: GenericSourceRecordManifest = + serde_json::from_value(initial_crossed_source_value(810_001, -810_001, "particle")) + .expect("initial source record"); + source.source_ir.crossing.phase = phase; + source.applied_crossing.phase = phase; + + let error = validate_source_wavefunction_metadata(0, &source).unwrap_err(); + + assert!(error.to_string().contains("invalid declared CrossingIR")); + } +} + +#[test] +fn f64_sources_use_typed_orientation_instead_of_flattened_metadata() { + let mut source = source_record(810_001, -810_001, "antiparticle", "fermion", 2); + source.source_orientation = GenericSourceOrientationManifest::Particle; + let point = [[5.0, 3.0, 4.0, 0.0]]; + let mut output = [c64(0.0, 0.0); 2]; + + ExecutionRuntime::write_source_wavefunction(&source, 1, &BTreeMap::new(), &point, &mut output) + .expect("evaluate relabeled antiparticle source"); + + assert_eq!(output, ext_antiquark_weyl_array(point[0], 1, 1)); + assert_ne!(output, ext_quark_weyl_array(point[0], 1, 1)); +} + +#[test] +fn f64_sources_use_typed_family_dimension_and_pdg_relation() { + let mut source = source_record(810_001, 910_002, "particle", "vector", 4); + source.wavefunction_kind = "scalar".to_owned(); + source.dimension = 1; + source.particle_id = -810_001; + source.anti_particle_id = -810_001; + let point = [[13.0, 0.0, 0.0, 12.0]]; + let masses = BTreeMap::from([(-810_001, 99.0), (910_002, 5.0)]); + let mut output = [c64(0.0, 0.0); 4]; + + ExecutionRuntime::write_source_wavefunction(&source, 1, &masses, &point, &mut output) + .expect("evaluate source with typed SourceIR metadata"); + + assert_eq!(output, ext_massive_vector(point[0], 1, 5.0)); +} + +#[test] +fn f64_sources_use_applied_crossing_momentum_transform() { + let mut source: GenericSourceRecordManifest = + serde_json::from_value(initial_crossed_source_value(810_001, -810_001, "particle")) + .expect("initial source record"); + source.crossing = "identity".to_owned(); + let point = [[5.0, 3.0, 4.0, 0.0]]; + let mut output = [c64(0.0, 0.0); 2]; + + ExecutionRuntime::write_source_wavefunction(&source, 1, &BTreeMap::new(), &point, &mut output) + .expect("evaluate crossed source"); + + assert_eq!(output, ext_quark_weyl_array(negate(point[0]), 1, -1)); +} + +#[test] +fn f64_sources_apply_nontrivial_crossing_phase_to_every_component() { + let mut value = initial_crossed_source_value(810_001, -810_001, "particle"); + value["source_ir"]["crossing"]["phase"] = json!([0.5, -0.25]); + value["applied_crossing"]["phase"] = json!([0.5, -0.25]); + let source: GenericSourceRecordManifest = + serde_json::from_value(value).expect("phased initial source record"); + validate_source_wavefunction_metadata(0, &source).expect("valid nontrivial phase"); + let point = [[5.0, 3.0, 4.0, 0.0]]; + let mut output = [c64(0.0, 0.0); 2]; + + ExecutionRuntime::write_source_wavefunction(&source, 1, &BTreeMap::new(), &point, &mut output) + .expect("evaluate phased source"); + + let phase = c64(0.5, -0.25); + let expected = ext_quark_weyl_array(negate(point[0]), 1, -1).map(|component| component * phase); + assert_eq!(output, expected); +} + +#[cfg(feature = "symbolica-runtime")] +#[test] +fn high_precision_sources_use_typed_orientation_crossing_and_phase() { + let mut source: GenericSourceRecordManifest = serde_json::from_value( + initial_crossed_source_value(810_001, -810_001, "antiparticle"), + ) + .expect("initial source record"); + source.source_orientation = GenericSourceOrientationManifest::Particle; + source.crossing = "identity".to_owned(); + source.source_ir.crossing.phase = [0.5, -0.25]; + source.applied_crossing.phase = [0.5, -0.25]; + let point = [[ + DoubleFloat::from(5.0), + DoubleFloat::from(3.0), + DoubleFloat::from(4.0), + DoubleFloat::from(0.0), + ]]; + let zero = || Complex::new(DoubleFloat::from(0.0), DoubleFloat::from(0.0)); + let mut output = vec![zero(), zero()]; + + ExecutionRuntime::write_source_wavefunction_generic( + &source, + 1, + &BTreeMap::new(), + &point, + &mut output, + ) + .expect("evaluate high-precision crossed antiparticle source"); + + let momentum = negate_generic(&point[0]); + let phase = c_generic(DoubleFloat::from(0.5), DoubleFloat::from(-0.25)); + let expected = ext_antiquark_weyl_generic(&momentum, 1, -1) + .into_iter() + .map(|component| component * phase) + .collect::>(); + let unexpected = ext_quark_weyl_generic(&momentum, 1, -1) + .into_iter() + .map(|component| component * phase) + .collect::>(); + assert_eq!(output, expected); + assert_ne!(output, unexpected); +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/engine/sources.rs b/pyamplicol/rust/crates/rusticol-core/src/engine/sources.rs new file mode 100644 index 0000000000..0a9e1f342e --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/engine/sources.rs @@ -0,0 +1,814 @@ +// SPDX-License-Identifier: 0BSD + +use super::*; + +#[derive(Clone, Debug)] +pub(super) struct RuntimeSourceState { + pub(super) state: GenericSourceStateIrManifest, + pub(super) factor: Complex, +} + +#[derive(Clone, Copy)] +enum ResolvedSourceKind { + Scalar, + Weyl { antiparticle: bool }, + Dirac { antiparticle: bool, mass: f64 }, + MasslessVector, + MassiveVector { mass: f64 }, + Spin2 { mass: f64 }, +} + +/// Resolve the already-generated SourceIR once for a source's point loop. +/// Masses are read at this boundary, so parameter changes apply to the next +/// tile without a new process plan or a second set of wavefunction formulas. +pub(super) struct ResolvedSourceWavefunction<'a> { + source: &'a GenericSourceRecordManifest, + state: &'a GenericSourceStateIrManifest, + external_index: usize, + kind: ResolvedSourceKind, +} + +impl<'a> ResolvedSourceWavefunction<'a> { + #[inline] + pub(super) fn new( + source: &'a GenericSourceRecordManifest, + state: &'a GenericSourceStateIrManifest, + external_count: usize, + particle_masses: &BTreeMap, + output_len: usize, + ) -> RusticolResult { + if source.source_kind != "external-wavefunction" { + return Err(RusticolError::invalid_argument(format!( + "generic source kind {:?} is not implemented", + source.source_kind + ))); + } + let external_index = source.leg_label.checked_sub(1).ok_or_else(|| { + RusticolError::invalid_argument("generic source leg labels are one-based") + })?; + if external_index >= external_count { + return Err(RusticolError::invalid_argument(format!( + "generic source {} refers to unknown external label {}", + source.source_id, source.leg_label + ))); + } + let family = source.source_ir.wavefunction_family; + let dimension = source.source_ir.component_dimension; + if output_len != dimension { + return Err(RusticolError::invalid_argument(format!( + "generic source {} expected dimension {} but slot has length {}", + source.source_id, dimension, output_len + ))); + } + let mass = || { + particle_mass_from_map( + particle_masses, + source.source_ir.identity.pdg_label, + source.source_ir.identity.anti_pdg_label, + ) + }; + let kind = match (family, dimension) { + (GenericWavefunctionFamilyManifest::Scalar, 1) => ResolvedSourceKind::Scalar, + (GenericWavefunctionFamilyManifest::Fermion, 2) => ResolvedSourceKind::Weyl { + antiparticle: source_is_antiparticle(source)?, + }, + (GenericWavefunctionFamilyManifest::Fermion, 4) => ResolvedSourceKind::Dirac { + antiparticle: source_is_antiparticle(source)?, + mass: mass(), + }, + (GenericWavefunctionFamilyManifest::Vector, 4) => match mass() { + 0.0 => ResolvedSourceKind::MasslessVector, + mass => ResolvedSourceKind::MassiveVector { mass }, + }, + (GenericWavefunctionFamilyManifest::Spin2, 16) => { + ResolvedSourceKind::Spin2 { mass: mass() } + } + _ => return Err(unsupported_source_wavefunction(source)), + }; + Ok(Self { + source, + state, + external_index, + kind, + }) + } + + #[inline] + pub(super) fn write

(&self, point: &P, out: &mut [Complex]) -> RusticolResult<()> + where + P: F64MomentumPoint + ?Sized, + { + debug_assert_eq!(out.len(), self.source.source_ir.component_dimension); + let input = point.momentum(self.external_index).ok_or_else(|| { + RusticolError::invalid_argument(format!( + "generic source {} refers to absent external label {}", + self.source.source_id, self.source.leg_label + )) + })?; + let momentum = match self.source.applied_crossing.momentum_transform { + GenericMomentumTransformManifest::Identity => input, + GenericMomentumTransformManifest::NegateFourMomentum => negate(input), + }; + match self.kind { + ResolvedSourceKind::Scalar => out[0] = c64(1.0, 0.0), + ResolvedSourceKind::Weyl { antiparticle } => { + let wave = if antiparticle { + ext_antiquark_weyl_array(momentum, self.state.helicity, self.state.chirality) + } else { + ext_quark_weyl_array(momentum, self.state.helicity, self.state.chirality) + }; + out.copy_from_slice(&wave); + } + ResolvedSourceKind::Dirac { antiparticle, mass } => { + let wave = if antiparticle { + ext_antiquark_dirac_massive(momentum, self.state.helicity, mass) + } else { + ext_quark_dirac_massive(momentum, self.state.helicity, mass) + }; + out.copy_from_slice(&wave); + } + ResolvedSourceKind::MasslessVector => { + out.copy_from_slice(&ext_gluon(momentum, self.state.helicity)); + } + ResolvedSourceKind::MassiveVector { mass } => { + out.copy_from_slice(&ext_massive_vector(momentum, self.state.helicity, mass)); + } + ResolvedSourceKind::Spin2 { mass } => { + out.copy_from_slice(&ext_spin2(momentum, self.state.helicity, mass)?); + } + } + // Do not fold this phase into a runtime-state factor: their existing + // two complex multiplications have a defined floating-point order. + apply_source_phase_f64(&self.source.applied_crossing, out); + Ok(()) + } +} + +impl ExecutionRuntime { + pub(super) fn default_runtime_source_state( + source: &GenericSourceRecordManifest, + ) -> RusticolResult { + default_source_state(source) + } + + pub(super) fn fill_sources_row_with_states

( + sources: &[GenericSourceRecordManifest], + source_states: &[RuntimeSourceState], + external_count: usize, + particle_masses: &BTreeMap, + row: &mut [Complex], + point: &P, + ) -> RusticolResult<()> + where + P: F64MomentumPoint + ?Sized, + { + if source_states.len() != sources.len() { + return Err(RusticolError::invalid_argument(format!( + "runtime source-state count {} does not match source count {}", + source_states.len(), + sources.len() + ))); + } + for (source, runtime_state) in sources.iter().zip(source_states) { + let start = source.value_slot.component_start; + let stop = source.value_slot.component_stop; + if stop > row.len() || stop < start { + return Err(RusticolError::invalid_argument(format!( + "generic source {} has an invalid value-slot range", + source.source_id + ))); + } + if runtime_state.factor == c64(0.0, 0.0) { + row[start..stop].fill(c64(0.0, 0.0)); + continue; + } + Self::write_source_wavefunction_with_state( + source, + &runtime_state.state, + external_count, + particle_masses, + point, + &mut row[start..stop], + )?; + if runtime_state.factor != c64(1.0, 0.0) { + for component in &mut row[start..stop] { + *component *= runtime_state.factor; + } + } + } + Ok(()) + } + + #[cfg(feature = "symbolica-runtime")] + pub(super) fn fill_sources_row_generic_with_states( + sources: &[GenericSourceRecordManifest], + source_states: &[RuntimeSourceState], + external_count: usize, + particle_masses: &BTreeMap, + row: &mut [Complex], + point: &[[T; 4]], + ) -> RusticolResult<()> + where + T: RusticolHighPrecisionNumber, + Complex: Real + EvaluationDomain, + { + if source_states.len() != sources.len() { + return Err(RusticolError::invalid_argument(format!( + "runtime source-state count {} does not match source count {}", + source_states.len(), + sources.len() + ))); + } + for (source, runtime_state) in sources.iter().zip(source_states) { + let start = source.value_slot.component_start; + let stop = source.value_slot.component_stop; + if stop > row.len() || stop < start { + return Err(RusticolError::invalid_argument(format!( + "generic source {} has an invalid value-slot range", + source.source_id + ))); + } + if runtime_state.factor == c64(0.0, 0.0) { + row[start..stop].fill(complex_zero::()); + continue; + } + Self::write_source_wavefunction_generic_with_state( + source, + &runtime_state.state, + external_count, + particle_masses, + point, + &mut row[start..stop], + )?; + if runtime_state.factor != c64(1.0, 0.0) { + let factor = c_generic( + T::from(runtime_state.factor.re), + T::from(runtime_state.factor.im), + ); + for component in &mut row[start..stop] { + *component *= factor.clone(); + } + } + } + Ok(()) + } + + pub(super) fn runtime_source_state( + source: &GenericSourceRecordManifest, + declared_state_index: usize, + factor: Complex, + ) -> RusticolResult { + let declared = source + .source_ir + .states + .get(declared_state_index) + .ok_or_else(|| { + RusticolError::invalid_argument(format!( + "generic source {} has no declared state {}", + source.source_id, declared_state_index + )) + })?; + let state = declared + .transformed(&source.applied_crossing) + .map_err(|error| { + RusticolError::invalid_argument(format!( + "generic source {} runtime state is invalid: {error}", + source.source_id + )) + })?; + Ok(RuntimeSourceState { state, factor }) + } + + pub(super) fn runtime_source_state_index_for_helicity( + source: &GenericSourceRecordManifest, + helicity: i32, + ) -> RusticolResult> { + let mut matching = + source + .source_ir + .states + .iter() + .enumerate() + .filter_map(|(index, state)| { + state + .transformed(&source.applied_crossing) + .ok() + .filter(|state| state.helicity == helicity) + .map(|_| index) + }); + let result = matching.next(); + if matching.next().is_some() { + return Err(RusticolError::artifact(format!( + "generic source {} declares multiple runtime states for helicity {}", + source.source_id, helicity + ))); + } + Ok(result) + } + + pub(super) fn inactive_runtime_source_state( + source: &GenericSourceRecordManifest, + ) -> RusticolResult { + Ok(RuntimeSourceState { + state: default_source_state(source)?, + factor: c64(0.0, 0.0), + }) + } + + pub(super) fn fill_sources_row

( + sources: &[GenericSourceRecordManifest], + external_count: usize, + particle_masses: &BTreeMap, + row: &mut [Complex], + point: &P, + ) -> RusticolResult<()> + where + P: F64MomentumPoint + ?Sized, + { + for source in sources { + let start = source.value_slot.component_start; + let stop = source.value_slot.component_stop; + if stop > row.len() || stop < start { + return Err(RusticolError::invalid_argument(format!( + "generic source {} has an invalid value-slot range", + source.source_id + ))); + } + Self::write_source_wavefunction( + source, + external_count, + particle_masses, + point, + &mut row[start..stop], + )?; + } + Ok(()) + } + + #[cfg(feature = "symbolica-runtime")] + pub(super) fn fill_sources_row_generic( + sources: &[GenericSourceRecordManifest], + external_count: usize, + particle_masses: &BTreeMap, + row: &mut [Complex], + point: &[[T; 4]], + ) -> RusticolResult<()> + where + T: RusticolHighPrecisionNumber, + Complex: Real + EvaluationDomain, + { + for source in sources { + let start = source.value_slot.component_start; + let stop = source.value_slot.component_stop; + if stop > row.len() || stop < start { + return Err(RusticolError::invalid_argument(format!( + "generic source {} has an invalid value-slot range", + source.source_id + ))); + } + Self::write_source_wavefunction_generic( + source, + external_count, + particle_masses, + point, + &mut row[start..stop], + )?; + } + Ok(()) + } + + pub(super) fn fill_momenta_row

( + momentum_slots: &[GenericMomentumSlotManifest], + value_parameter_count: usize, + external_count: usize, + external_is_initial: &[bool], + row: &mut [Complex], + point: &P, + ) -> RusticolResult<()> + where + P: F64MomentumPoint + ?Sized, + { + for slot in momentum_slots { + let start = value_parameter_count + slot.component_start; + let stop = value_parameter_count + slot.component_stop; + if stop > row.len() || stop < start || stop - start != 4 { + return Err(RusticolError::invalid_argument(format!( + "generic momentum slot {} has an invalid component range", + slot.momentum_slot_id + ))); + } + let mut momentum = [0.0; 4]; + for label in &slot.external_labels { + let index = label.checked_sub(1).ok_or_else(|| { + RusticolError::invalid_argument("generic momentum labels are one-based") + })?; + if index >= external_count || index >= external_is_initial.len() { + return Err(RusticolError::invalid_argument(format!( + "generic momentum slot {} refers to unknown external label {}", + slot.momentum_slot_id, label + ))); + } + let sign = if external_is_initial[index] { + -1.0 + } else { + 1.0 + }; + let point_momentum = point.momentum(index).ok_or_else(|| { + RusticolError::invalid_argument(format!( + "generic momentum slot {} refers to absent external label {}", + slot.momentum_slot_id, label + )) + })?; + for (momentum_component, point_component) in momentum.iter_mut().zip(point_momentum) + { + *momentum_component += sign * point_component; + } + } + for (output, component) in row[start..stop].iter_mut().zip(momentum) { + *output = c64(component, 0.0); + } + } + Ok(()) + } + + #[cfg(feature = "symbolica-runtime")] + pub(super) fn fill_momenta_row_generic( + momentum_slots: &[GenericMomentumSlotManifest], + value_parameter_count: usize, + external_count: usize, + external_is_initial: &[bool], + row: &mut [Complex], + point: &[[T; 4]], + ) -> RusticolResult<()> + where + T: RusticolHighPrecisionNumber, + Complex: Real + EvaluationDomain, + { + for slot in momentum_slots { + let start = value_parameter_count + slot.component_start; + let stop = value_parameter_count + slot.component_stop; + if stop > row.len() || stop < start || stop - start != 4 { + return Err(RusticolError::invalid_argument(format!( + "generic momentum slot {} has an invalid component range", + slot.momentum_slot_id + ))); + } + let mut momentum: [T; 4] = std::array::from_fn(|_| T::new_zero()); + for label in &slot.external_labels { + let index = label.checked_sub(1).ok_or_else(|| { + RusticolError::invalid_argument("generic momentum labels are one-based") + })?; + if index >= external_count || index >= external_is_initial.len() { + return Err(RusticolError::invalid_argument(format!( + "generic momentum slot {} refers to unknown external label {}", + slot.momentum_slot_id, label + ))); + } + for (momentum_component, point_component) in momentum.iter_mut().zip(&point[index]) + { + if external_is_initial[index] { + *momentum_component -= point_component.clone(); + } else { + *momentum_component += point_component.clone(); + } + } + } + for (output, component) in row[start..stop].iter_mut().zip(momentum) { + *output = c_generic(component, T::new_zero()); + } + } + Ok(()) + } + + pub(super) fn fill_model_parameters_row( + model_parameter_start: usize, + model_parameter_values: &[f64], + row: &mut [Complex], + ) -> RusticolResult<()> { + let stop = model_parameter_start + model_parameter_values.len(); + if stop > row.len() { + return Err(RusticolError::invalid_argument( + "generic model-parameter block exceeds runtime row length", + )); + } + for (index, value) in model_parameter_values.iter().enumerate() { + row[model_parameter_start + index] = c64(*value, 0.0); + } + Ok(()) + } + + #[cfg(feature = "symbolica-runtime")] + pub(super) fn fill_model_parameters_row_generic( + model_parameter_start: usize, + model_parameter_values: &[f64], + row: &mut [Complex], + ) -> RusticolResult<()> + where + T: RusticolHighPrecisionNumber, + Complex: Real + EvaluationDomain, + { + let stop = model_parameter_start + model_parameter_values.len(); + if stop > row.len() { + return Err(RusticolError::invalid_argument( + "generic model-parameter block exceeds runtime row length", + )); + } + for (index, value) in model_parameter_values.iter().enumerate() { + row[model_parameter_start + index] = c_generic(T::from(*value), T::new_zero()); + } + Ok(()) + } + + pub(super) fn write_source_wavefunction

( + source: &GenericSourceRecordManifest, + external_count: usize, + particle_masses: &BTreeMap, + point: &P, + out: &mut [Complex], + ) -> RusticolResult<()> + where + P: F64MomentumPoint + ?Sized, + { + let state = default_source_state(source)?; + Self::write_source_wavefunction_with_state( + source, + &state, + external_count, + particle_masses, + point, + out, + ) + } + + pub(super) fn write_source_wavefunction_with_state

( + source: &GenericSourceRecordManifest, + state: &GenericSourceStateIrManifest, + external_count: usize, + particle_masses: &BTreeMap, + point: &P, + out: &mut [Complex], + ) -> RusticolResult<()> + where + P: F64MomentumPoint + ?Sized, + { + ResolvedSourceWavefunction::new(source, state, external_count, particle_masses, out.len())? + .write(point, out) + } + + #[cfg(feature = "symbolica-runtime")] + pub(super) fn write_source_wavefunction_generic( + source: &GenericSourceRecordManifest, + external_count: usize, + particle_masses: &BTreeMap, + point: &[[T; 4]], + out: &mut [Complex], + ) -> RusticolResult<()> + where + T: RusticolHighPrecisionNumber, + Complex: Real + EvaluationDomain, + { + let state = default_source_state(source)?; + Self::write_source_wavefunction_generic_with_state( + source, + &state, + external_count, + particle_masses, + point, + out, + ) + } + + #[cfg(feature = "symbolica-runtime")] + pub(super) fn write_source_wavefunction_generic_with_state( + source: &GenericSourceRecordManifest, + state: &GenericSourceStateIrManifest, + external_count: usize, + particle_masses: &BTreeMap, + point: &[[T; 4]], + out: &mut [Complex], + ) -> RusticolResult<()> + where + T: RusticolHighPrecisionNumber, + Complex: Real + EvaluationDomain, + { + Self::write_source_wavefunction_generic_unphased( + source, + state, + external_count, + particle_masses, + point, + out, + )?; + apply_source_phase_generic(&source.applied_crossing, out); + Ok(()) + } + + #[cfg(feature = "symbolica-runtime")] + fn write_source_wavefunction_generic_unphased( + source: &GenericSourceRecordManifest, + state: &GenericSourceStateIrManifest, + external_count: usize, + particle_masses: &BTreeMap, + point: &[[T; 4]], + out: &mut [Complex], + ) -> RusticolResult<()> + where + T: RusticolHighPrecisionNumber, + Complex: Real + EvaluationDomain, + { + if source.source_kind != "external-wavefunction" { + return Err(RusticolError::invalid_argument(format!( + "generic source kind {:?} is not implemented", + source.source_kind + ))); + } + let index = source.leg_label.checked_sub(1).ok_or_else(|| { + RusticolError::invalid_argument("generic source leg labels are one-based") + })?; + if index >= external_count { + return Err(RusticolError::invalid_argument(format!( + "generic source {} refers to unknown external label {}", + source.source_id, source.leg_label + ))); + } + let source_ir = &source.source_ir; + let identity = &source_ir.identity; + let family = source_ir.wavefunction_family; + let dimension = source_ir.component_dimension; + let momentum = match source.applied_crossing.momentum_transform { + GenericMomentumTransformManifest::Identity => point[index].clone(), + GenericMomentumTransformManifest::NegateFourMomentum => negate_generic(&point[index]), + }; + if dimension == 1 && family == GenericWavefunctionFamilyManifest::Scalar { + if out.len() != 1 { + return Err(RusticolError::invalid_argument(format!( + "generic source {} expected dimension 1 but slot has length {}", + source.source_id, + out.len() + ))); + } + out[0] = c_generic(T::from(1.0), T::new_zero()); + return Ok(()); + } + if dimension == 2 && family == GenericWavefunctionFamilyManifest::Fermion { + if out.len() != 2 { + return Err(RusticolError::invalid_argument(format!( + "generic source {} expected dimension 2 but slot has length {}", + source.source_id, + out.len() + ))); + } + let chirality = state.chirality; + let wave = if source_is_antiparticle(source)? { + ext_antiquark_weyl_generic(&momentum, state.helicity, chirality) + } else { + ext_quark_weyl_generic(&momentum, state.helicity, chirality) + }; + out.clone_from_slice(&wave); + return Ok(()); + } + if dimension == 4 { + if out.len() != 4 { + return Err(RusticolError::invalid_argument(format!( + "generic source {} expected dimension 4 but slot has length {}", + source.source_id, + out.len() + ))); + } + let wave = if family == GenericWavefunctionFamilyManifest::Fermion { + let mass = particle_mass_from_map( + particle_masses, + identity.pdg_label, + identity.anti_pdg_label, + ); + if mass != 0.0 { + return Err(RusticolError::invalid_argument( + "high-precision generic massive fermion sources are not implemented", + )); + } + if source_is_antiparticle(source)? { + ext_antiquark_dirac_generic(&momentum, state.helicity) + } else { + ext_quark_dirac_generic(&momentum, state.helicity) + } + } else if family == GenericWavefunctionFamilyManifest::Vector { + let mass = particle_mass_from_map( + particle_masses, + identity.pdg_label, + identity.anti_pdg_label, + ); + if mass == 0.0 { + ext_gluon_generic(&momentum, state.helicity) + } else { + ext_massive_vector_generic(&momentum, state.helicity, T::from(mass)) + } + } else { + return Err(unsupported_source_wavefunction(source)); + }; + out.clone_from_slice(&wave); + return Ok(()); + } + if dimension == 16 && family == GenericWavefunctionFamilyManifest::Spin2 { + if out.len() != 16 { + return Err(RusticolError::invalid_argument(format!( + "generic source {} expected dimension 16 but slot has length {}", + source.source_id, + out.len() + ))); + } + let wave = ext_spin2_generic( + &momentum, + state.helicity, + T::from(particle_mass_from_map( + particle_masses, + identity.pdg_label, + identity.anti_pdg_label, + )), + )?; + out.clone_from_slice(&wave); + return Ok(()); + } + Err(RusticolError::invalid_argument(format!( + "generic source kind {:?} with dimension {} is not implemented", + family.as_str(), + dimension + ))) + } +} + +fn default_source_state( + source: &GenericSourceRecordManifest, +) -> RusticolResult { + source + .source_ir + .states + .iter() + .filter_map(|state| state.transformed(&source.applied_crossing).ok()) + .find(|state| { + state.helicity == source.source_helicity && state.chirality == source.chirality + }) + .ok_or_else(|| { + RusticolError::invalid_argument(format!( + "generic source {} default state is absent from its SourceIR", + source.source_id + )) + }) +} + +fn apply_source_phase_f64(crossing: &GenericCrossingIrManifest, out: &mut [Complex]) { + if crossing.phase == [1.0, 0.0] { + return; + } + let phase = c64(crossing.phase[0], crossing.phase[1]); + for component in out { + *component *= phase; + } +} + +#[cfg(feature = "symbolica-runtime")] +fn apply_source_phase_generic(crossing: &GenericCrossingIrManifest, out: &mut [Complex]) +where + T: RusticolHighPrecisionNumber, + Complex: Real + EvaluationDomain, +{ + if crossing.phase == [1.0, 0.0] { + return; + } + let phase = c_generic(T::from(crossing.phase[0]), T::from(crossing.phase[1])); + for component in out { + *component *= &phase; + } +} + +fn source_is_antiparticle(source: &GenericSourceRecordManifest) -> RusticolResult { + match source.source_ir.identity.orientation { + GenericSourceOrientationManifest::Particle => Ok(false), + GenericSourceOrientationManifest::Antiparticle => Ok(true), + GenericSourceOrientationManifest::SelfConjugate => { + Err(RusticolError::invalid_argument(format!( + "generic source {} uses an unsupported self-conjugate fermion wavefunction", + source.source_id + ))) + } + } +} + +fn unsupported_source_wavefunction(source: &GenericSourceRecordManifest) -> RusticolError { + RusticolError::invalid_argument(format!( + "generic source kind {:?} with dimension {} is not implemented", + source.source_ir.wavefunction_family.as_str(), + source.source_ir.component_dimension + )) +} + +fn particle_mass_from_map( + particle_masses: &BTreeMap, + particle_id: i32, + anti_particle_id: i32, +) -> f64 { + particle_masses + .get(&particle_id) + .copied() + .or_else(|| particle_masses.get(&anti_particle_id).copied()) + .unwrap_or(0.0) +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/engine/validation.rs b/pyamplicol/rust/crates/rusticol-core/src/engine/validation.rs new file mode 100644 index 0000000000..fd6f465843 --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/engine/validation.rs @@ -0,0 +1,2104 @@ +// SPDX-License-Identifier: 0BSD + +use super::*; + +pub(super) fn load_execution_manifest_with_store( + manifest: ExecutionManifest, + payloads: &EvaluatorPayloadStore, + sizing_physics: &PhysicsRuntime, +) -> RusticolResult { + validate_execution_manifest(&manifest)?; + ExecutionRuntime::load_from_manifest_with_store(manifest, payloads, sizing_physics) +} + +fn validate_execution_manifest(manifest: &ExecutionManifest) -> RusticolResult<()> { + if manifest.schema_version != PROCESS_ARTIFACT_SCHEMA_VERSION + || manifest.kind != "pyamplicol-runtime-execution" + { + return Err(RusticolError::compatibility(format!( + "unsupported execution manifest kind {} schema {}; regenerate the artifact with pyAmpliCol 0.1.0 or newer", + manifest.kind, manifest.schema_version + ))); + } + if manifest.runtime_schema.schema_version != PROCESS_ARTIFACT_SCHEMA_VERSION + || manifest.runtime_schema.kind != "pyamplicol-runtime-execution-plan" + { + return Err(RusticolError::compatibility(format!( + "unsupported execution-plan kind {} schema {}; regenerate the artifact with pyAmpliCol 0.1.0 or newer", + manifest.runtime_schema.kind, manifest.runtime_schema.schema_version + ))); + } + if manifest.runtime_schema.process != manifest.process + || manifest.runtime_schema.process_key != manifest.key + { + return Err(RusticolError::artifact( + "generic runtime schema process identity does not match manifest", + )); + } + if manifest.compiled.kind != "generic-dag-stage-blueprint" { + return Err(RusticolError::artifact( + "schema-v3 execution manifests require a generic-dag-stage-blueprint", + )); + } + if !manifest.compiled.runtime_available || manifest.compiled.stage_evaluators.is_none() { + return Err(RusticolError::artifact( + "schema-v3 execution manifest has no serialized stage evaluators; regenerate the artifact", + )); + } + if manifest.dag_summary.truncated { + return Err(RusticolError::artifact( + "generic DAG schema-v3 artifact was truncated during current construction", + )); + } + if manifest.dag_summary.interaction_evaluation_count > manifest.dag_summary.interaction_count { + return Err(RusticolError::integrity( + "generic DAG interaction-evaluation count exceeds its interaction count", + )); + } + let expected_materialization = BTreeMap::from([ + ( + "amplitude_root_count".to_string(), + manifest.dag_summary.amplitude_root_count, + ), + ( + "current_count".to_string(), + manifest.dag_summary.current_count, + ), + ( + "interaction_count".to_string(), + manifest.dag_summary.interaction_count, + ), + ( + "interaction_evaluation_count".to_string(), + manifest.dag_summary.interaction_evaluation_count, + ), + ( + "source_count".to_string(), + manifest.dag_summary.source_count, + ), + ]); + if manifest.materialization_census.abi != "pyamplicol-fully-resident-materialization-census-v1" + || manifest.materialization_census.basis != "immutable-fully-resident-compiled-dag" + { + return Err(RusticolError::compatibility( + "compiled materialization census ABI or basis is unsupported", + )); + } + if !manifest.materialization_census.final_equals_peak + || manifest.materialization_census.r#final != expected_materialization + || manifest.materialization_census.peak != expected_materialization + { + return Err(RusticolError::integrity( + "compiled materialization census disagrees with the persisted DAG", + )); + } + if manifest.external_pdg_order.len() != manifest.runtime_schema.external_particles.len() { + return Err(RusticolError::artifact( + "generic runtime schema external particle count does not match external_pdg_order", + )); + } + for (index, (pdg, particle)) in manifest + .external_pdg_order + .iter() + .zip(&manifest.runtime_schema.external_particles) + .enumerate() + { + if particle.index != index || particle.momentum_slot != index || particle.pdg != *pdg { + return Err(RusticolError::artifact(format!( + "generic external particle metadata mismatch at index {index}" + ))); + } + if particle.label != index + 1 { + return Err(RusticolError::artifact(format!( + "generic external particle labels must be contiguous; got {} at index {index}", + particle.label + ))); + } + if particle.outgoing_pdg == 0 || (particle.role != "initial" && particle.role != "final") { + return Err(RusticolError::artifact(format!( + "generic external particle {index} has invalid role or outgoing PDG" + ))); + } + } + validate_generic_parameter_layout(&manifest.runtime_schema)?; + validate_generic_current_storage(manifest)?; + validate_generic_value_storage(manifest)?; + validate_generic_sources(manifest)?; + validate_generic_momentum_slots(manifest)?; + validate_generic_stages(manifest)?; + validate_generic_amplitudes(manifest)?; + validate_compiled_symmetric_group_diagnostic(manifest)?; + validate_generic_stage_evaluators(manifest)?; + validate_lc_topology_replay(manifest)?; + if let Some(sum_manifest) = manifest.helicity_sum_execution.as_deref() { + if sum_manifest.helicity_sum_execution.is_some() + || !sum_manifest.helicity_selector_executions.is_empty() + { + return Err(RusticolError::integrity( + "compiled helicity-sum execution contains a recursively nested auxiliary execution", + )); + } + validate_execution_manifest(sum_manifest)?; + } + let mut helicity_selector_domain_ids = BTreeSet::new(); + for record in &manifest.helicity_selector_executions { + for selector_domain_id in &record.selector_domain_ids { + if !helicity_selector_domain_ids.insert(*selector_domain_id) { + return Err(RusticolError::integrity(format!( + "compiled helicity-selector executions contain duplicate selector domain {selector_domain_id}" + ))); + } + } + let selector_manifest = record.execution.as_ref(); + if selector_manifest.helicity_sum_execution.is_some() + || !selector_manifest.color_selector_executions.is_empty() + { + return Err(RusticolError::integrity( + "compiled helicity-selector execution contains an unsupported auxiliary execution", + )); + } + if !selector_manifest.helicity_selector_executions.is_empty() + && record.schedule_mode != HelicitySelectorScheduleMode::NestedRuntime + { + return Err(RusticolError::integrity( + "only a nested-runtime helicity-selector execution may contain closure lanes", + )); + } + let has_materialization = selector_manifest + .runtime_schema + .helicity_recurrence + .as_ref() + .is_some_and(|recurrence| recurrence.materialization.is_some()); + match record.schedule_mode { + HelicitySelectorScheduleMode::ParentClosure if has_materialization => { + return Err(RusticolError::integrity( + "parent-closure helicity-selector execution cannot contain helicity materialization", + )); + } + HelicitySelectorScheduleMode::NestedRuntime if !has_materialization => { + return Err(RusticolError::integrity( + "nested-runtime helicity-selector execution requires helicity materialization", + )); + } + _ => {} + } + validate_execution_manifest(selector_manifest)?; + } + let mut color_sector_ids = BTreeSet::new(); + for lane in &manifest.color_selector_executions { + if !color_sector_ids.insert(lane.materialized_sector_id) { + return Err(RusticolError::integrity(format!( + "compiled color-selector executions contain duplicate materialized sector {}", + lane.materialized_sector_id + ))); + } + if lane.execution.helicity_sum_execution.is_some() + || !lane.execution.helicity_selector_executions.is_empty() + || !lane.execution.color_selector_executions.is_empty() + { + return Err(RusticolError::integrity( + "compiled color-selector execution cannot contain auxiliary executions", + )); + } + validate_execution_manifest(lane.execution.as_ref())?; + } + Ok(()) +} + +fn validate_compiled_symmetric_group_diagnostic( + manifest: &ExecutionManifest, +) -> RusticolResult<()> { + let Some(payload) = manifest.color_contraction_payload.as_ref() else { + return Ok(()); + }; + if payload.path.is_empty() { + return Err(RusticolError::compatibility( + "compiled symmetric-group FFT payload has an empty path", + )); + } + if manifest.color_accuracy != "full" + || manifest.helicity_sum_execution.is_some() + || !manifest.helicity_selector_executions.is_empty() + || !manifest.color_selector_executions.is_empty() + || manifest.runtime_schema.helicity_recurrence.is_some() + || manifest.compiled.lc_topology_replay.is_some() + || manifest.compiled.color_topology_replay.is_some() + || manifest + .runtime_schema + .amplitude_stage + .color_topology_replay + .is_some() + { + return Err(RusticolError::compatibility( + "compiled symmetric-group FFT diagnostic supports only selected-helicity FullColour total execution without selector or topology-replay lanes", + )); + } + let contraction = manifest + .runtime_schema + .amplitude_stage + .color_contraction + .as_ref() + .ok_or_else(|| { + RusticolError::integrity( + "compiled symmetric-group FFT payload has no amplitude contraction metadata", + ) + })?; + if !contraction.supported + || contraction.group_count == 0 + || !contraction.entries.is_empty() + || contraction.repeated_block.is_some() + { + return Err(RusticolError::integrity( + "compiled symmetric-group FFT diagnostic requires a compact payload-only amplitude contraction", + )); + } + Ok(()) +} + +#[cfg(all(test, feature = "f64-symjit"))] +mod execution_summary_tests { + use super::*; + + fn manifest() -> ExecutionManifest { + serde_json::from_value(crate::artifact::tests::minimal_execution_manifest( + "p0", + "a b > c", + SYMJIT_APPLICATION_RUNTIME_CAPABILITY, + crate::artifact::tests::direct_evaluator_manifest("evaluators/direct.symjit"), + )) + .expect("deserialize compiled execution summary fixture") + } + + #[test] + fn compiled_summary_and_materialization_census_round_trip() { + validate_execution_manifest(&manifest()).expect("validate exact compiled census"); + + let mut excessive = manifest(); + excessive.dag_summary.interaction_evaluation_count = 1; + let error = validate_execution_manifest(&excessive).unwrap_err(); + assert_eq!(error.kind(), crate::RusticolErrorKind::Integrity); + assert!(error.to_string().contains("exceeds")); + + let mut inconsistent = manifest(); + inconsistent + .materialization_census + .r#final + .insert("interaction_evaluation_count".to_string(), 1); + let error = validate_execution_manifest(&inconsistent).unwrap_err(); + assert_eq!(error.kind(), crate::RusticolErrorKind::Integrity); + assert!(error.to_string().contains("disagrees")); + } +} + +fn validate_lc_topology_replay(manifest: &ExecutionManifest) -> RusticolResult<()> { + let Some(replay) = manifest.compiled.lc_topology_replay.as_ref() else { + return Ok(()); + }; + if !replay.enabled { + return Ok(()); + } + let (mappings, _weights) = build_lc_topology_replay_mappings(Some(replay))?; + if mappings.is_empty() { + return Err(RusticolError::artifact( + "enabled LC topology replay contains no sector mappings", + )); + } + let expected_legs = manifest.external_pdg_order.len(); + for mapping in &mappings { + let mut seen = vec![false; expected_legs]; + for (representative_index, sector_index) in mapping { + if *representative_index >= expected_legs || *sector_index >= expected_legs { + return Err(RusticolError::artifact( + "LC topology replay mapping references an out-of-range external leg", + )); + } + if seen[*representative_index] { + return Err(RusticolError::artifact( + "LC topology replay mapping contains a duplicate representative label", + )); + } + seen[*representative_index] = true; + } + } + Ok(()) +} + +fn validate_generic_parameter_layout(schema: &ExecutionPlan) -> RusticolResult<()> { + let layout = &schema.parameter_layout; + if !layout.source_components_complex || !layout.momentum_components_real { + return Err(RusticolError::artifact( + "generic runtime schema requires complex source components and real momenta", + )); + } + if layout.parameter_count_if_flattened + != layout.source_component_parameter_count + + layout.momentum_parameter_count + + layout.model_parameter_count + { + return Err(RusticolError::artifact( + "generic runtime schema flattened parameter count is inconsistent", + )); + } + if layout.momentum_parameter_count != 4 * schema.momentum_slots.len() { + return Err(RusticolError::artifact( + "generic runtime schema momentum parameter count does not match momentum slots", + )); + } + let expected_real_inputs = (layout.source_component_parameter_count + ..layout.parameter_count_if_flattened) + .collect::>(); + if layout.real_valued_inputs != expected_real_inputs { + return Err(RusticolError::artifact( + "generic runtime schema real-valued input indices are inconsistent", + )); + } + if schema.model_parameters.len() != layout.model_parameter_count { + return Err(RusticolError::artifact( + "generic runtime schema model-parameter count is inconsistent", + )); + } + let mut seen_model_parameters = BTreeSet::new(); + let mut seen_model_parameter_names = BTreeSet::new(); + for parameter in &schema.model_parameters { + if parameter.name.is_empty() + || parameter.parameter_index >= layout.model_parameter_count + || !parameter.default.is_finite() + || !seen_model_parameters.insert(parameter.parameter_index) + || !seen_model_parameter_names.insert(parameter.name.clone()) + || parameter + .runtime_name + .as_ref() + .is_some_and(|name| name.is_empty()) + || !matches!( + ( + ¶meter.runtime_name, + parameter.complex_component.as_deref(), + ), + (Some(_), Some("real" | "imag")) | (None, None) + ) + { + return Err(RusticolError::artifact( + "generic runtime schema contains invalid model-parameter metadata", + )); + } + } + Ok(()) +} + +fn positive_json_integer(value: &Value) -> bool { + if let Some(unsigned) = value.as_u64() { + return unsigned > 0; + } + if let Some(signed) = value.as_i64() { + return signed > 0; + } + if let Some(float_value) = value.as_f64() { + return float_value.is_finite() && float_value > 0.0; + } + if let Some(text) = value.as_str() { + if let Some(hex) = text.strip_prefix("0x").or_else(|| text.strip_prefix("0X")) { + return !hex.is_empty() + && hex.bytes().all(|byte| byte.is_ascii_hexdigit()) + && hex.bytes().any(|byte| byte != b'0'); + } + return !text.is_empty() + && text.bytes().all(|byte| byte.is_ascii_digit()) + && text.bytes().any(|byte| byte != b'0'); + } + false +} + +fn validate_generic_current_storage(manifest: &ExecutionManifest) -> RusticolResult<()> { + let storage = &manifest.runtime_schema.current_storage; + if storage.number_type != "complex" { + return Err(RusticolError::artifact( + "generic current storage must use complex number type", + )); + } + if storage.current_slots.len() != manifest.dag_summary.current_count { + return Err(RusticolError::artifact( + "generic current slot count does not match DAG summary", + )); + } + let mut offset = 0usize; + for (index, slot) in storage.current_slots.iter().enumerate() { + if slot.current_id != index { + return Err(RusticolError::artifact(format!( + "generic current slot id mismatch at index {index}" + ))); + } + if slot.component_start != offset + || slot.component_stop != slot.component_start + slot.dimension + || slot.dimension == 0 + { + return Err(RusticolError::artifact(format!( + "generic current slot {index} has inconsistent component range" + ))); + } + if slot.particle_id == 0 || slot.external_mask == 0 || slot.momentum_mask == 0 { + return Err(RusticolError::artifact(format!( + "generic current slot {index} has invalid physics identity" + ))); + } + if !valid_current_chirality(slot.chirality) { + return Err(RusticolError::artifact(format!( + "generic current slot {index} has invalid quantum-flow metadata" + ))); + } + if !storage.metadata_compacted { + if slot.external_labels.is_empty() + || !positive_json_integer(&slot.helicity_ancestry) + || slot.color_state.is_null() + { + return Err(RusticolError::artifact(format!( + "generic current slot {index} is missing current-index metadata" + ))); + } + if slot.flavour_flow.is_empty() { + return Err(RusticolError::artifact(format!( + "generic current slot {index} has invalid quantum-flow metadata" + ))); + } + if slot.spin_state.is_null() + || slot + .auxiliary_kind + .as_ref() + .is_some_and(|kind| kind.is_empty()) + { + return Err(RusticolError::artifact(format!( + "generic current slot {index} has invalid spin/auxiliary metadata" + ))); + } + } + offset = slot.component_stop; + } + if storage.component_count != offset { + return Err(RusticolError::artifact( + "generic current storage component_count is inconsistent", + )); + } + Ok(()) +} + +fn validate_generic_value_storage(manifest: &ExecutionManifest) -> RusticolResult<()> { + let schema = &manifest.runtime_schema; + let storage = &schema.value_storage; + if storage.number_type != "complex" { + return Err(RusticolError::artifact( + "generic value storage must use complex number type", + )); + } + if schema.parameter_layout.value_component_count != storage.component_count { + return Err(RusticolError::artifact( + "generic value component count does not match parameter layout", + )); + } + if storage.value_slots.is_empty() && manifest.dag_summary.current_count != 0 { + return Err(RusticolError::artifact( + "generic value storage has no value slots", + )); + } + let current_slots = &schema.current_storage.current_slots; + let mut offset = 0usize; + for (index, slot) in storage.value_slots.iter().enumerate() { + if slot.value_slot_id != index + || slot.component_start != offset + || slot.component_stop != slot.component_start + slot.dimension + || slot.dimension == 0 + { + return Err(RusticolError::artifact(format!( + "generic value slot {index} has inconsistent component range" + ))); + } + let current = current_slots.get(slot.current_id).ok_or_else(|| { + RusticolError::artifact(format!( + "generic value slot {index} references missing current {}", + slot.current_id + )) + })?; + if current.dimension != slot.dimension || current.is_source != slot.is_source { + return Err(RusticolError::artifact(format!( + "generic value slot {index} does not match its current slot" + ))); + } + if slot.current_component_start != current.component_start + || slot.current_component_stop != current.component_stop + || slot.particle_id != current.particle_id + || slot.external_mask != current.external_mask + || (!storage.metadata_compacted && slot.external_labels != current.external_labels) + || slot.momentum_mask != current.momentum_mask + || slot.chirality != current.chirality + { + return Err(RusticolError::artifact(format!( + "generic value slot {index} does not preserve its current identity" + ))); + } + validate_generic_propagator(index, slot, current)?; + match slot.variant.as_str() { + "source" => { + if !slot.is_source || slot.applies_propagator { + return Err(RusticolError::artifact(format!( + "generic source value slot {index} is inconsistent" + ))); + } + } + "propagated" => { + if slot.is_source || !slot.applies_propagator { + return Err(RusticolError::artifact(format!( + "generic propagated value slot {index} is inconsistent" + ))); + } + } + "unpropagated" => { + if slot.is_source || slot.applies_propagator { + return Err(RusticolError::artifact(format!( + "generic unpropagated value slot {index} is inconsistent" + ))); + } + } + other => { + return Err(RusticolError::artifact(format!( + "generic value slot {index} has unsupported variant {other:?}" + ))); + } + } + offset = slot.component_stop; + } + if storage.component_count != offset { + return Err(RusticolError::artifact( + "generic value storage component_count is inconsistent", + )); + } + Ok(()) +} + +fn validate_generic_propagator( + index: usize, + slot: &GenericValueSlotManifest, + current: &GenericCurrentSlotManifest, +) -> RusticolResult<()> { + let propagator = &slot.propagator; + let identity = &propagator.identity; + if propagator.particle_id != slot.particle_id + || propagator.chirality != slot.chirality + || identity.pdg_label != slot.particle_id + || identity.canonical_id.is_empty() + || identity.species_id.is_empty() + || identity.anti_canonical_id.is_empty() + || identity.display_name.is_empty() + || identity.anti_display_name.is_empty() + || identity.self_conjugate != (identity.pdg_label == identity.anti_pdg_label) + || identity.self_conjugate != (identity.canonical_id == identity.anti_canonical_id) + || propagator.backend.is_empty() + || propagator.basis.is_empty() + || propagator.kernel.is_empty() + || propagator.description.is_empty() + { + return Err(RusticolError::artifact(format!( + "generic value slot {index} has inconsistent propagator identity" + ))); + } + if identity.self_conjugate + != matches!( + identity.orientation, + GenericSourceOrientationManifest::SelfConjugate + ) + { + return Err(RusticolError::artifact(format!( + "generic value slot {index} has inconsistent propagator orientation" + ))); + } + for value in [ + propagator.numerator.as_ref(), + propagator.denominator.as_ref(), + propagator.mass_parameter.as_ref(), + propagator.width_parameter.as_ref(), + propagator.custom_source.as_ref(), + propagator.auxiliary_policy.as_ref(), + ] { + if value.is_some_and(String::is_empty) { + return Err(RusticolError::artifact(format!( + "generic value slot {index} has empty propagator metadata" + ))); + } + } + let expected_dimension = match propagator.kind { + GenericPropagatorKindManifest::Scalar => Some(1), + GenericPropagatorKindManifest::WeylFermion => Some(2), + GenericPropagatorKindManifest::DiracFermion | GenericPropagatorKindManifest::Vector => { + Some(4) + } + GenericPropagatorKindManifest::Spin2 => Some(16), + GenericPropagatorKindManifest::Identity + | GenericPropagatorKindManifest::Custom + | GenericPropagatorKindManifest::Unsupported => None, + }; + if expected_dimension.is_some_and(|dimension| dimension != current.dimension) { + return Err(RusticolError::artifact(format!( + "generic value slot {index} propagator dimension does not match its current" + ))); + } + if propagator.kind != GenericPropagatorKindManifest::Unsupported + && !propagator.full_tensor_network_ready + { + return Err(RusticolError::artifact(format!( + "generic value slot {index} has an incomplete propagator contract" + ))); + } + match propagator.kind { + GenericPropagatorKindManifest::Identity => { + if propagator.applies_propagator + || propagator.mass_class != GenericPropagatorMassClassManifest::NotApplicable + || propagator.gauge.is_some() + || propagator.auxiliary_policy.is_none() + { + return Err(RusticolError::artifact(format!( + "generic value slot {index} has an invalid identity propagator" + ))); + } + } + GenericPropagatorKindManifest::Vector => { + if !propagator.applies_propagator + || !matches!( + propagator.gauge, + Some( + GenericPropagatorGaugeManifest::Feynman + | GenericPropagatorGaugeManifest::Unitary + ) + ) + { + return Err(RusticolError::artifact(format!( + "generic value slot {index} has an invalid vector propagator" + ))); + } + } + GenericPropagatorKindManifest::Spin2 => { + if !propagator.applies_propagator + || !matches!( + propagator.gauge, + Some( + GenericPropagatorGaugeManifest::DeDonder + | GenericPropagatorGaugeManifest::FierzPauli + ) + ) + { + return Err(RusticolError::artifact(format!( + "generic value slot {index} has an invalid spin-2 propagator" + ))); + } + } + GenericPropagatorKindManifest::Custom => { + if !propagator.applies_propagator + || propagator.gauge != Some(GenericPropagatorGaugeManifest::ModelSupplied) + || propagator.custom_source.is_none() + { + return Err(RusticolError::artifact(format!( + "generic value slot {index} has an invalid custom propagator" + ))); + } + } + GenericPropagatorKindManifest::Scalar + | GenericPropagatorKindManifest::WeylFermion + | GenericPropagatorKindManifest::DiracFermion + | GenericPropagatorKindManifest::Unsupported => { + if !propagator.applies_propagator || propagator.gauge.is_some() { + return Err(RusticolError::artifact(format!( + "generic value slot {index} has an invalid standard propagator" + ))); + } + } + } + if propagator.kind != GenericPropagatorKindManifest::Identity + && propagator.mass_class == GenericPropagatorMassClassManifest::NotApplicable + { + return Err(RusticolError::artifact(format!( + "generic value slot {index} has an invalid propagator mass class" + ))); + } + if propagator.goldstone_policy == GenericGoldstonePolicyManifest::Absorbed + && !(propagator.kind == GenericPropagatorKindManifest::Vector + && propagator.gauge == Some(GenericPropagatorGaugeManifest::Unitary)) + { + return Err(RusticolError::artifact(format!( + "generic value slot {index} has an invalid Goldstone policy" + ))); + } + if propagator.goldstone_policy == GenericGoldstonePolicyManifest::ModelSupplied + && propagator.kind != GenericPropagatorKindManifest::Custom + { + return Err(RusticolError::artifact(format!( + "generic value slot {index} has an invalid model-supplied Goldstone policy" + ))); + } + Ok(()) +} + +fn validate_generic_sources(manifest: &ExecutionManifest) -> RusticolResult<()> { + let schema = &manifest.runtime_schema; + if schema.source_fill.source_count != manifest.dag_summary.source_count + || schema.source_fill.sources.len() != manifest.dag_summary.source_count + { + return Err(RusticolError::artifact( + "generic source count does not match DAG summary", + )); + } + let current_slots = &schema.current_storage.current_slots; + let mut source_offset = 0usize; + let mut source_ir_by_momentum_slot = BTreeMap::new(); + let mut source_ir_by_canonical_id = BTreeMap::new(); + for (index, source) in schema.source_fill.sources.iter().enumerate() { + if source.source_id != index { + return Err(RusticolError::artifact(format!( + "generic source id mismatch at index {index}" + ))); + } + let slot = current_slots.get(source.current_id).ok_or_else(|| { + RusticolError::artifact(format!( + "generic source {index} references missing current {}", + source.current_id + )) + })?; + if !slot.is_source + || slot.dimension != source.dimension + || slot.particle_id != source.particle_id + { + return Err(RusticolError::artifact(format!( + "generic source {index} does not match its current slot" + ))); + } + if source.current_component_start != slot.component_start + || source.current_component_stop != slot.component_stop + || source.source_parameter_start != source_offset + || source.source_parameter_stop != source_offset + source.dimension + { + return Err(RusticolError::artifact(format!( + "generic source {index} has inconsistent component offsets" + ))); + } + validate_value_slot_ref( + &source.value_slot, + source.current_id, + Some("source"), + &schema.value_storage, + )?; + if source.source_kind != "external-wavefunction" { + return Err(RusticolError::artifact(format!( + "unsupported generic source kind {:?}", + source.source_kind + ))); + } + if source.side != "initial" && source.side != "final" { + return Err(RusticolError::artifact(format!( + "generic source {index} has invalid side {:?}", + source.side + ))); + } + validate_source_wavefunction_metadata(index, source)?; + validate_consistent_source_ir( + &mut source_ir_by_momentum_slot, + &mut source_ir_by_canonical_id, + index, + source, + )?; + let max_helicity = + if source.source_ir.wavefunction_family == GenericWavefunctionFamilyManifest::Spin2 { + 2 + } else { + 1 + }; + if source.physical_pdg == 0 + || source.outgoing_pdg != source.particle_id + || source.chirality.unsigned_abs() > 1 + || source.source_helicity.unsigned_abs() > max_helicity + || source.spin_state.is_null() + || !positive_json_integer(&source.helicity_ancestry) + || source.color_state.is_null() + { + return Err(RusticolError::artifact(format!( + "generic source {index} has invalid physics metadata" + ))); + } + let particle = schema + .external_particles + .get(source.input_momentum_slot) + .ok_or_else(|| { + RusticolError::artifact(format!( + "generic source {index} references missing momentum slot {}", + source.input_momentum_slot + )) + })?; + if particle.label != source.leg_label + || particle.role != source.side + || particle.pdg != source.physical_pdg + || particle.outgoing_pdg != source.outgoing_pdg + { + return Err(RusticolError::artifact(format!( + "generic source {index} does not match its external particle" + ))); + } + source_offset = source.source_parameter_stop; + } + if source_offset != schema.parameter_layout.source_component_parameter_count { + return Err(RusticolError::artifact( + "generic source parameter count does not match source records", + )); + } + Ok(()) +} + +pub(super) fn validate_consistent_source_ir( + source_ir_by_momentum_slot: &mut BTreeMap, + source_ir_by_canonical_id: &mut BTreeMap, + index: usize, + source: &GenericSourceRecordManifest, +) -> RusticolResult<()> { + if let Some(canonical) = source_ir_by_momentum_slot.get(&source.input_momentum_slot) { + if canonical != &source.source_ir { + return Err(RusticolError::artifact(format!( + "generic source {index} disagrees with canonical SourceIR metadata for momentum slot {}", + source.input_momentum_slot + ))); + } + } else { + source_ir_by_momentum_slot.insert(source.input_momentum_slot, source.source_ir.clone()); + } + + let identity = &source.source_ir.identity; + if let Some(canonical) = source_ir_by_canonical_id.get(&identity.canonical_id) + && canonical != &source.source_ir + { + return Err(RusticolError::artifact(format!( + "generic source {index} disagrees with canonical SourceIR metadata for oriented particle {:?}", + identity.canonical_id + ))); + } + if let Some(antiparticle) = source_ir_by_canonical_id.get(&identity.anti_canonical_id) { + validate_antiparticle_identity_pair(index, identity, &antiparticle.identity)?; + } + source_ir_by_canonical_id + .entry(identity.canonical_id.clone()) + .or_insert_with(|| source.source_ir.clone()); + Ok(()) +} + +fn validate_antiparticle_identity_pair( + index: usize, + identity: &GenericParticleIdentityIrManifest, + antiparticle: &GenericParticleIdentityIrManifest, +) -> RusticolResult<()> { + let orientations_match = matches!( + (identity.orientation, antiparticle.orientation), + ( + GenericSourceOrientationManifest::Particle, + GenericSourceOrientationManifest::Antiparticle, + ) | ( + GenericSourceOrientationManifest::Antiparticle, + GenericSourceOrientationManifest::Particle, + ) | ( + GenericSourceOrientationManifest::SelfConjugate, + GenericSourceOrientationManifest::SelfConjugate, + ) + ); + if identity.anti_canonical_id != antiparticle.canonical_id + || antiparticle.anti_canonical_id != identity.canonical_id + || identity.species_id != antiparticle.species_id + || identity.anti_pdg_label != antiparticle.pdg_label + || antiparticle.anti_pdg_label != identity.pdg_label + || identity.anti_display_name != antiparticle.display_name + || antiparticle.anti_display_name != identity.display_name + || identity.self_conjugate != antiparticle.self_conjugate + || !orientations_match + { + return Err(RusticolError::artifact(format!( + "generic source {index} has a non-involutive particle/antiparticle identity relation for {:?}", + identity.canonical_id + ))); + } + Ok(()) +} + +pub(super) fn validate_source_wavefunction_metadata( + index: usize, + source: &GenericSourceRecordManifest, +) -> RusticolResult<()> { + let source_ir = &source.source_ir; + let identity = &source_ir.identity; + if identity.canonical_id.is_empty() + || identity.species_id.is_empty() + || identity.anti_canonical_id.is_empty() + || identity.display_name.is_empty() + || identity.anti_display_name.is_empty() + { + return Err(RusticolError::artifact(format!( + "generic source {index} has incomplete typed particle identity" + ))); + } + if identity.anti_pdg_label == 0 { + return Err(RusticolError::artifact(format!( + "generic source {index} has an invalid antiparticle relation" + ))); + } + let canonical_self_conjugate = identity.canonical_id == identity.anti_canonical_id; + let pdg_self_conjugate = identity.pdg_label == identity.anti_pdg_label; + if identity.self_conjugate != canonical_self_conjugate + || identity.self_conjugate != pdg_self_conjugate + || (identity.orientation == GenericSourceOrientationManifest::SelfConjugate) + != identity.self_conjugate + { + return Err(RusticolError::artifact(format!( + "generic source {index} orientation is inconsistent with its antiparticle relation" + ))); + } + + if source_ir.component_dimension == 0 + || source_ir.states.is_empty() + || source_ir.basis.is_empty() + || source_ir + .mass_parameter + .as_ref() + .is_some_and(String::is_empty) + || source_ir + .width_parameter + .as_ref() + .is_some_and(String::is_empty) + { + return Err(RusticolError::artifact(format!( + "generic source {index} has invalid typed SourceIR metadata" + ))); + } + validate_crossing_ir(index, "declared", &source_ir.crossing)?; + validate_crossing_ir(index, "applied", &source.applied_crossing)?; + + let expected_statistics = match source_ir.wavefunction_family { + GenericWavefunctionFamilyManifest::Fermion => GenericParticleStatisticsManifest::Fermion, + GenericWavefunctionFamilyManifest::Ghost => GenericParticleStatisticsManifest::Ghost, + GenericWavefunctionFamilyManifest::Auxiliary => { + GenericParticleStatisticsManifest::Auxiliary + } + GenericWavefunctionFamilyManifest::Scalar + | GenericWavefunctionFamilyManifest::Vector + | GenericWavefunctionFamilyManifest::Spin2 => GenericParticleStatisticsManifest::Boson, + }; + if source_ir.statistics != expected_statistics { + return Err(RusticolError::artifact(format!( + "generic source {index} statistics disagree with its wavefunction family" + ))); + } + + match (source_ir.wavefunction_family, source_ir.component_dimension) { + (GenericWavefunctionFamilyManifest::Fermion, 2 | 4) => { + if identity.self_conjugate { + return Err(RusticolError::artifact(format!( + "generic source {index} is an unsupported self-conjugate fermion source" + ))); + } + } + (GenericWavefunctionFamilyManifest::Scalar, 1) + | (GenericWavefunctionFamilyManifest::Vector, 4) + | (GenericWavefunctionFamilyManifest::Spin2, 16) => {} + (kind, dimension) => { + return Err(RusticolError::artifact(format!( + "generic source {index} has unsupported wavefunction kind {:?} with dimension {dimension}", + kind.as_str() + ))); + } + } + + for (field, matches) in [ + ("particle_id", source.particle_id == identity.pdg_label), + ( + "anti_particle_id", + source.anti_particle_id == identity.anti_pdg_label, + ), + ( + "source_orientation", + source.source_orientation == identity.orientation, + ), + ( + "wavefunction_kind", + source.wavefunction_kind == source_ir.wavefunction_family.as_str(), + ), + ( + "dimension", + source.dimension == source_ir.component_dimension, + ), + ("source_basis", source.source_basis == source_ir.basis), + ( + "crossing", + source.crossing + == source + .applied_crossing + .momentum_transform + .legacy_projection(), + ), + ] { + if !matches { + return Err(RusticolError::artifact(format!( + "generic source {index} flattened field {field:?} disagrees with typed metadata" + ))); + } + } + + match source.side.as_str() { + "initial" if source.applied_crossing != source_ir.crossing => { + return Err(RusticolError::artifact(format!( + "generic source {index} applied crossing does not match its declared SourceIR crossing" + ))); + } + "final" if !source.applied_crossing.is_identity() => { + return Err(RusticolError::artifact(format!( + "generic source {index} final-state applied crossing is not identity" + ))); + } + "initial" | "final" => {} + _ => { + return Err(RusticolError::artifact(format!( + "generic source {index} has invalid side {:?}", + source.side + ))); + } + } + let current_spin_state: GenericSourceSpinStateManifest = + serde_json::from_value(source.spin_state.clone()).map_err(|_| { + RusticolError::artifact(format!( + "generic source {index} has an invalid flattened spin state" + )) + })?; + let current_state = GenericSourceStateIrManifest { + helicity: source.source_helicity, + chirality: source.chirality, + spin_state: current_spin_state, + }; + let max_helicity = if source_ir.wavefunction_family == GenericWavefunctionFamilyManifest::Spin2 + { + 2 + } else { + 1 + }; + let mut state_is_declared = false; + for (state_index, declared_state) in source_ir.states.iter().enumerate() { + if declared_state.chirality.unsigned_abs() > 1 + || declared_state.helicity.unsigned_abs() > max_helicity + { + return Err(RusticolError::artifact(format!( + "generic source {index} SourceIR state {state_index} is outside the supported helicity/chirality range" + ))); + } + let transformed = declared_state + .transformed(&source.applied_crossing) + .map_err(|message| { + RusticolError::artifact(format!( + "generic source {index} SourceIR state {state_index} is invalid: {message}" + )) + })?; + state_is_declared |= transformed == current_state; + } + if !state_is_declared { + return Err(RusticolError::artifact(format!( + "generic source {index} current state is not declared by its typed SourceIR" + ))); + } + Ok(()) +} + +fn validate_crossing_ir( + index: usize, + label: &str, + crossing: &GenericCrossingIrManifest, +) -> RusticolResult<()> { + if ![-1, 1].contains(&crossing.helicity_factor) + || ![-1, 1].contains(&crossing.chirality_factor) + || ![-1, 1].contains(&crossing.spin_state_factor) + || !crossing.phase.iter().all(|component| component.is_finite()) + || crossing.phase == [0.0, 0.0] + { + return Err(RusticolError::artifact(format!( + "generic source {index} has invalid {label} CrossingIR metadata" + ))); + } + Ok(()) +} + +fn validate_generic_momentum_slots(manifest: &ExecutionManifest) -> RusticolResult<()> { + let schema = &manifest.runtime_schema; + for (index, slot) in schema.momentum_slots.iter().enumerate() { + if slot.momentum_slot_id != index + || slot.component_start != 4 * index + || slot.component_stop != slot.component_start + 4 + || !slot.real_valued + || slot.momentum_mask == 0 + || slot.external_labels.is_empty() + { + return Err(RusticolError::artifact(format!( + "generic momentum slot {index} is inconsistent" + ))); + } + for label in &slot.external_labels { + if *label == 0 || *label > schema.external_particles.len() { + return Err(RusticolError::artifact(format!( + "generic momentum slot {index} has invalid external label {label}" + ))); + } + } + } + Ok(()) +} + +fn validate_generic_stages(manifest: &ExecutionManifest) -> RusticolResult<()> { + let schema = &manifest.runtime_schema; + let current_count = schema.current_storage.current_slots.len(); + let value_count = schema.value_storage.value_slots.len(); + let momentum_count = schema.momentum_slots.len(); + let input_value_variants = + build_input_value_variants(&schema.current_storage, &schema.value_storage)?; + let mut seen_interactions = 0usize; + let mut seen_interaction_ids = BTreeSet::new(); + for (stage_offset, stage) in schema.stages.iter().enumerate() { + if stage.stage_index != stage_offset + 1 + || stage.stage_kind != "current-combine" + || stage.subset_size < 2 + { + return Err(RusticolError::artifact(format!( + "generic stage {} is inconsistent", + stage.stage_index + ))); + } + if stage.interactions_compacted { + if !stage.interactions.is_empty() + || stage.interaction_ids.len() != stage.interaction_count + { + return Err(RusticolError::artifact(format!( + "generic compact stage {} has inconsistent interaction metadata", + stage.stage_index + ))); + } + for interaction_id in &stage.interaction_ids { + if !seen_interaction_ids.insert(*interaction_id) { + return Err(RusticolError::artifact(format!( + "generic compact stage {} repeats interaction {interaction_id}", + stage.stage_index + ))); + } + } + } else if stage.interaction_count != stage.interactions.len() + || !stage.interaction_ids.is_empty() + { + return Err(RusticolError::artifact(format!( + "generic stage {} is inconsistent", + stage.stage_index + ))); + } + let mut input_current_membership = vec![false; current_count]; + let mut output_current_membership = vec![false; current_count]; + for (ids, membership) in [ + (&stage.input_current_ids, &mut input_current_membership), + (&stage.output_current_ids, &mut output_current_membership), + ] { + for id in ids { + if *id >= current_count { + return Err(RusticolError::artifact(format!( + "generic stage {} references invalid current {id}", + stage.stage_index + ))); + } + membership[*id] = true; + } + } + let mut input_value_membership = vec![false; value_count]; + let mut output_value_membership = vec![false; value_count]; + for (ids, membership) in [ + (&stage.input_value_slot_ids, &mut input_value_membership), + (&stage.output_value_slot_ids, &mut output_value_membership), + ] { + for value_id in ids { + if *value_id >= value_count { + return Err(RusticolError::artifact(format!( + "generic stage {} references invalid value slot {value_id}", + stage.stage_index + ))); + } + membership[*value_id] = true; + } + } + for interaction in &stage.interactions { + if !seen_interaction_ids.insert(interaction.interaction_id) { + return Err(RusticolError::artifact(format!( + "generic stage {} repeats interaction {}", + stage.stage_index, interaction.interaction_id + ))); + } + validate_generic_interaction( + interaction, + &schema.current_storage, + &schema.value_storage, + &input_value_variants, + momentum_count, + )?; + if !input_current_membership[interaction.left_current_id] + || !input_current_membership[interaction.right_current_id] + || !output_current_membership[interaction.result_current_id] + { + return Err(RusticolError::artifact(format!( + "generic interaction {} is not listed in its stage inputs/outputs", + interaction.interaction_id + ))); + } + if !input_value_membership[interaction.left_value_slot.value_slot_id] + || !input_value_membership[interaction.right_value_slot.value_slot_id] + || interaction + .result_value_slots + .iter() + .any(|slot| !output_value_membership[slot.value_slot_id]) + { + return Err(RusticolError::artifact(format!( + "generic interaction {} value slots are not listed in its stage inputs/outputs", + interaction.interaction_id + ))); + } + } + seen_interactions += stage.interaction_count; + } + if seen_interactions != manifest.dag_summary.interaction_count { + return Err(RusticolError::artifact( + "generic stage interaction count does not match DAG summary", + )); + } + Ok(()) +} + +fn validate_generic_interaction( + interaction: &GenericInteractionManifest, + current_storage: &GenericCurrentStorageManifest, + value_storage: &GenericValueStorageManifest, + input_value_variants: &[Option], + momentum_count: usize, +) -> RusticolResult<()> { + let current_count = current_storage.current_slots.len(); + for current_id in [ + interaction.left_current_id, + interaction.right_current_id, + interaction.result_current_id, + ] { + if current_id >= current_count { + return Err(RusticolError::artifact(format!( + "generic interaction {} references invalid current {current_id}", + interaction.interaction_id + ))); + } + } + for slot_id in [ + interaction.momentum_slots.left, + interaction.momentum_slots.right, + interaction.momentum_slots.result, + ] { + if slot_id >= momentum_count { + return Err(RusticolError::artifact(format!( + "generic interaction {} references invalid momentum slot {slot_id}", + interaction.interaction_id + ))); + } + } + validate_slot_ref( + &interaction.left_slot, + interaction.left_current_id, + current_storage, + )?; + validate_slot_ref( + &interaction.right_slot, + interaction.right_current_id, + current_storage, + )?; + validate_slot_ref( + &interaction.result_slot, + interaction.result_current_id, + current_storage, + )?; + validate_value_slot_ref( + &interaction.left_value_slot, + interaction.left_current_id, + Some(input_value_variant( + input_value_variants, + interaction.left_current_id, + )?), + value_storage, + )?; + validate_value_slot_ref( + &interaction.right_value_slot, + interaction.right_current_id, + Some(input_value_variant( + input_value_variants, + interaction.right_current_id, + )?), + value_storage, + )?; + if interaction.result_value_slots.is_empty() { + return Err(RusticolError::artifact(format!( + "generic interaction {} has no result value slots", + interaction.interaction_id + ))); + } + for result_slot in &interaction.result_value_slots { + validate_value_slot_ref( + result_slot, + interaction.result_current_id, + None, + value_storage, + )?; + if result_slot.variant != "propagated" && result_slot.variant != "unpropagated" { + return Err(RusticolError::artifact(format!( + "generic interaction {} has invalid result value variant {:?}", + interaction.interaction_id, result_slot.variant + ))); + } + } + if interaction.vertex_kind < 0 { + return Err(RusticolError::artifact(format!( + "generic interaction {} has invalid vertex kind {}", + interaction.interaction_id, interaction.vertex_kind + ))); + } + if interaction.vertex_particles.len() != 3 + || interaction.coupling.len() != 2 + || interaction.color_weight.len() != 2 + || interaction.accumulation != "sum-into-result-current" + || interaction.lowering.kind != interaction.vertex_kind + || interaction.lowering.full_tensor_network_ready != interaction.full_tensor_network_ready + || interaction.lowering.backend.is_empty() + || interaction.lowering.expression_head.is_empty() + || interaction.lowering.kernel.is_empty() + || interaction.lowering.input_roles.len() != 2 + || interaction.lowering.output_role.is_empty() + || interaction.lowering.coupling_mode.is_empty() + || interaction + .lowering + .tensor_names + .iter() + .any(|name| name.is_empty()) + || interaction.lowering.description.is_empty() + { + return Err(RusticolError::artifact(format!( + "generic interaction {} has inconsistent model/lowering metadata", + interaction.interaction_id + ))); + } + if interaction.result_requires_propagated_value + && !interaction + .result_value_slots + .iter() + .any(|slot| slot.variant == "propagated") + { + return Err(RusticolError::artifact(format!( + "generic interaction {} declares a propagated result without a propagated slot", + interaction.interaction_id + ))); + } + if interaction.result_requires_unpropagated_value + && !interaction + .result_value_slots + .iter() + .any(|slot| slot.variant == "unpropagated") + { + return Err(RusticolError::artifact(format!( + "generic interaction {} declares an unpropagated result without an unpropagated slot", + interaction.interaction_id + ))); + } + if !interaction.full_tensor_network_ready { + return Err(RusticolError::artifact(format!( + "generic interaction {} is not ready for tensor-network lowering", + interaction.interaction_id + ))); + } + Ok(()) +} + +fn validate_generic_amplitudes(manifest: &ExecutionManifest) -> RusticolResult<()> { + let schema = &manifest.runtime_schema; + let stage = &schema.amplitude_stage; + if stage.stage_kind != "amplitude-roots" + || stage.output_count != stage.roots.len() + || stage.output_count != manifest.dag_summary.amplitude_root_count + { + return Err(RusticolError::artifact( + "generic amplitude stage output count is inconsistent", + )); + } + let current_count = schema.current_storage.current_slots.len(); + for (index, root) in stage.roots.iter().enumerate() { + if root.output_index != index || root.root_id != index { + return Err(RusticolError::artifact(format!( + "generic amplitude root index mismatch at output {index}" + ))); + } + if root.left_current_id >= current_count || root.right_current_id >= current_count { + return Err(RusticolError::artifact(format!( + "generic amplitude root {index} references invalid current" + ))); + } + if root.kind != "direct-contraction" && root.kind != "vertex-closure" { + return Err(RusticolError::artifact(format!( + "generic amplitude root {index} has unsupported kind {:?}", + root.kind + ))); + } + if root.coupling.len() != 2 + || root.color_weight.len() != 2 + || root.contraction.is_empty() + || !root.helicity_weight.is_finite() + || root.coherent_group_id.as_ref().is_some_and(Value::is_null) + { + return Err(RusticolError::artifact(format!( + "generic amplitude root {index} has inconsistent physics metadata" + ))); + } + validate_amplitude_root_kind(index, root, &schema.current_storage)?; + validate_slot_ref( + &root.left_slot, + root.left_current_id, + &schema.current_storage, + )?; + validate_slot_ref( + &root.right_slot, + root.right_current_id, + &schema.current_storage, + )?; + validate_value_slot_ref( + &root.left_value_slot, + root.left_current_id, + Some(amplitude_value_variant( + &schema.current_storage, + root.left_current_id, + )?), + &schema.value_storage, + )?; + validate_value_slot_ref( + &root.right_value_slot, + root.right_current_id, + Some(amplitude_value_variant( + &schema.current_storage, + root.right_current_id, + )?), + &schema.value_storage, + )?; + validate_amplitude_contraction( + index, + root, + &schema.current_storage, + &schema.value_storage, + )?; + } + Ok(()) +} + +pub(super) fn validate_amplitude_contraction( + index: usize, + root: &GenericAmplitudeRootManifest, + current_storage: &GenericCurrentStorageManifest, + value_storage: &GenericValueStorageManifest, +) -> RusticolResult<()> { + let contraction = &root.contraction_ir; + if contraction.name.is_empty() + || contraction.left_basis.is_empty() + || contraction.right_basis.is_empty() + || contraction.coefficients.is_empty() + || contraction + .metric_signature + .as_ref() + .is_some_and(String::is_empty) + || contraction + .coefficients + .iter() + .flatten() + .any(|component| !component.is_finite()) + || !contraction + .coefficients + .iter() + .any(|coefficient| *coefficient != [0.0, 0.0]) + { + return Err(RusticolError::artifact(format!( + "generic amplitude root {index} has invalid typed contraction metadata" + ))); + } + if root.contraction != contraction.name { + return Err(RusticolError::artifact(format!( + "generic amplitude root {index} contraction projection does not match ContractionIR" + ))); + } + + if root.kind == "vertex-closure" { + if contraction.coefficients.len() != 1 + || contraction.left_basis != "scalar" + || contraction.right_basis != "scalar" + || contraction.chirality_relation != GenericContractionChiralityRelationManifest::Any + { + return Err(RusticolError::artifact(format!( + "generic vertex-closure root {index} does not declare a scalar projection" + ))); + } + return Ok(()); + } + + let left_current = current_storage + .current_slots + .get(root.left_current_id) + .ok_or_else(|| { + RusticolError::artifact(format!( + "generic amplitude root {index} references a missing left current" + )) + })?; + let right_current = current_storage + .current_slots + .get(root.right_current_id) + .ok_or_else(|| { + RusticolError::artifact(format!( + "generic amplitude root {index} references a missing right current" + )) + })?; + let left_value = value_storage + .value_slots + .get(root.left_value_slot.value_slot_id) + .ok_or_else(|| { + RusticolError::artifact(format!( + "generic amplitude root {index} references a missing left value slot" + )) + })?; + let right_value = value_storage + .value_slots + .get(root.right_value_slot.value_slot_id) + .ok_or_else(|| { + RusticolError::artifact(format!( + "generic amplitude root {index} references a missing right value slot" + )) + })?; + if contraction.coefficients.len() != root.left_value_slot.dimension + || contraction.coefficients.len() != root.right_value_slot.dimension + || contraction.left_basis != left_value.propagator.basis + || contraction.right_basis != right_value.propagator.basis + { + return Err(RusticolError::artifact(format!( + "generic amplitude root {index} ContractionIR does not match its value slots" + ))); + } + let chirality_matches = contraction_chiralities_match( + contraction.chirality_relation, + left_current.chirality, + right_current.chirality, + ); + if !chirality_matches { + return Err(RusticolError::artifact(format!( + "generic amplitude root {index} violates its ContractionIR chirality relation" + ))); + } + Ok(()) +} + +pub(super) fn validate_amplitude_root_kind( + index: usize, + root: &GenericAmplitudeRootManifest, + current_storage: &GenericCurrentStorageManifest, +) -> RusticolResult<()> { + match root.kind.as_str() { + "direct-contraction" => { + if root.vertex_kind.is_some() || root.vertex_particles.is_some() { + return Err(RusticolError::artifact(format!( + "generic direct-contraction root {index} carries vertex metadata" + ))); + } + } + "vertex-closure" => { + let vertex_kind = root.vertex_kind.filter(|kind| *kind > 0).ok_or_else(|| { + RusticolError::artifact(format!( + "generic vertex-closure root {index} has an invalid vertex kind" + )) + })?; + let particles = root + .vertex_particles + .as_ref() + .filter(|values| values.len() == 3) + .ok_or_else(|| { + RusticolError::artifact(format!( + "generic vertex-closure root {index} is missing vertex particles" + )) + })?; + let left = current_storage + .current_slots + .get(root.left_current_id) + .ok_or_else(|| { + RusticolError::artifact(format!( + "generic vertex-closure root {index} references a missing left current" + )) + })?; + let right = current_storage + .current_slots + .get(root.right_current_id) + .ok_or_else(|| { + RusticolError::artifact(format!( + "generic vertex-closure root {index} references a missing right current" + )) + })?; + if particles[0] != left.particle_id || particles[1] != right.particle_id { + return Err(RusticolError::artifact(format!( + "generic vertex-closure root {index} vertex {vertex_kind} does not match its input currents" + ))); + } + } + _ => { + return Err(RusticolError::artifact(format!( + "generic amplitude root {index} has unsupported kind {:?}", + root.kind + ))); + } + } + Ok(()) +} + +pub(super) fn valid_current_chirality(chirality: i32) -> bool { + (-1..=1).contains(&chirality) +} + +pub(super) fn contraction_chiralities_match( + relation: GenericContractionChiralityRelationManifest, + left: i32, + right: i32, +) -> bool { + match relation { + GenericContractionChiralityRelationManifest::Any => true, + GenericContractionChiralityRelationManifest::Equal => left == right, + GenericContractionChiralityRelationManifest::Opposite => { + right.checked_neg().is_some_and(|opposite| left == opposite) + } + } +} + +fn validate_generic_stage_evaluators(manifest: &ExecutionManifest) -> RusticolResult<()> { + let Some(stage_evaluators) = &manifest.compiled.stage_evaluators else { + return Ok(()); + }; + let schema = &manifest.runtime_schema; + let expected_parameter_count = schema.parameter_layout.value_component_count + + schema.parameter_layout.momentum_parameter_count + + schema.parameter_layout.model_parameter_count; + let expected_real_inputs = (schema.parameter_layout.value_component_count + ..expected_parameter_count) + .collect::>(); + let header_is_global = stage_evaluators.parameter_layout == "global-value-momentum" + && stage_evaluators.parameter_count == expected_parameter_count + && stage_evaluators.value_parameter_count == schema.parameter_layout.value_component_count + && stage_evaluators.momentum_parameter_count + == schema.parameter_layout.momentum_parameter_count + && stage_evaluators.model_parameter_count == schema.parameter_layout.model_parameter_count + && stage_evaluators.real_valued_inputs == expected_real_inputs; + let header_is_stage_local = stage_evaluators.parameter_layout == "stage-local-value-momentum" + && stage_evaluators.parameter_count == 0 + && stage_evaluators.value_parameter_count == 0 + && stage_evaluators.momentum_parameter_count == 0 + && stage_evaluators.model_parameter_count == 0 + && stage_evaluators.real_valued_inputs.is_empty(); + if stage_evaluators.kind != "generic-dag-stage-evaluator-artifacts" + || (!header_is_global && !header_is_stage_local) + || stage_evaluators.stage_count != schema.stages.len() + 1 + || stage_evaluators.stages.len() != schema.stages.len() + || !stage_evaluators.runtime_available + || stage_evaluators.runtime_unavailable_message.is_some() + { + return Err(RusticolError::artifact( + "generic stage evaluator artifact header is inconsistent with runtime schema", + )); + } + for (stage, runtime_stage) in stage_evaluators.stages.iter().zip(&schema.stages) { + validate_generic_serialized_stage_evaluator( + stage, + runtime_stage.stage_index, + "current-combine", + Some(runtime_stage.subset_size), + expected_parameter_count, + Some(runtime_stage), + None, + )?; + } + validate_generic_serialized_stage_evaluator( + &stage_evaluators.amplitude_stage, + 0, + "amplitude-roots", + None, + expected_parameter_count, + None, + Some(&schema.amplitude_stage), + )?; + Ok(()) +} + +fn validate_generic_serialized_stage_evaluator( + stage: &GenericSerializedStageEvaluatorManifest, + expected_stage_index: usize, + expected_stage_kind: &str, + expected_subset_size: Option, + expected_parameter_count: usize, + runtime_stage: Option<&GenericStageManifest>, + amplitude_stage: Option<&GenericAmplitudeStageManifest>, +) -> RusticolResult<()> { + let stage_is_global = stage.parameter_layout == "global-value-momentum"; + let stage_is_local = stage.parameter_layout == "stage-local-value-momentum"; + if stage.stage_index != expected_stage_index + || stage.stage_kind != expected_stage_kind + || stage.subset_size != expected_subset_size + || (!stage_is_global && !stage_is_local) + || !stage.expression_ready + || !stage.blockers.is_empty() + || stage.evaluator_label.is_empty() + || stage.output_length == 0 + { + return Err(RusticolError::artifact(format!( + "generic serialized stage evaluator {} is inconsistent", + stage.evaluator_label + ))); + } + validate_generic_stage_output_slots(stage)?; + let (input_len, output_len) = stage.evaluator.io_len()?; + if stage_is_global && input_len != expected_parameter_count { + return Err(RusticolError::artifact(format!( + "generic serialized stage evaluator {} has inconsistent global evaluator input length", + stage.evaluator_label + ))); + } + if stage_is_local { + validate_generic_stage_input_components(stage, expected_parameter_count)?; + if input_len != stage.parameter_count + || stage.parameter_count != stage.input_components.len() + || stage.value_parameter_count + + stage.momentum_parameter_count + + stage.model_parameter_count + != stage.parameter_count + || stage + .real_valued_inputs + .iter() + .any(|index| *index >= stage.parameter_count) + { + return Err(RusticolError::artifact(format!( + "generic serialized stage evaluator {} has inconsistent local evaluator input metadata", + stage.evaluator_label + ))); + } + } + if output_len != stage.output_length { + return Err(RusticolError::artifact(format!( + "generic serialized stage evaluator {} has inconsistent evaluator IO", + stage.evaluator_label + ))); + } + if let Some(runtime_stage) = runtime_stage { + let expected_interactions = if runtime_stage.interactions_compacted { + runtime_stage.interaction_ids.clone() + } else { + runtime_stage + .interactions + .iter() + .map(|interaction| interaction.interaction_id) + .collect::>() + }; + if stage.input_value_slot_ids != runtime_stage.input_value_slot_ids + || stage.output_value_slot_ids != runtime_stage.output_value_slot_ids + || stage.interaction_ids != expected_interactions + { + return Err(RusticolError::artifact(format!( + "generic serialized stage evaluator {} does not match runtime stage slots", + stage.evaluator_label + ))); + } + } + if let Some(amplitude_stage) = amplitude_stage + && (!stage.output_value_slot_ids.is_empty() + || !stage.interaction_ids.is_empty() + || stage.output_length != amplitude_stage.output_count) + { + return Err(RusticolError::artifact( + "generic serialized amplitude evaluator does not match amplitude stage", + )); + } + Ok(()) +} + +fn validate_generic_stage_output_slots( + stage: &GenericSerializedStageEvaluatorManifest, +) -> RusticolResult<()> { + if stage.output_slots.is_empty() { + return Err(RusticolError::artifact(format!( + "generic serialized stage evaluator {} has no output slots", + stage.evaluator_label + ))); + } + let mut max_output_stop = 0usize; + for slot in &stage.output_slots { + if slot.variant.is_empty() + || slot.component_stop < slot.component_start + || slot.output_stop <= slot.output_start + || slot.output_stop > stage.output_length + { + return Err(RusticolError::artifact(format!( + "generic serialized stage evaluator {} has invalid output slot", + stage.evaluator_label + ))); + } + if stage.stage_kind == "amplitude-roots" { + if slot.value_slot_id != -1 || slot.current_id != -1 || slot.variant != "amplitude-root" + { + return Err(RusticolError::artifact( + "generic serialized amplitude evaluator has invalid output slot metadata", + )); + } + } else if slot.value_slot_id < 0 || slot.current_id < 0 || slot.variant == "amplitude-root" + { + return Err(RusticolError::artifact(format!( + "generic serialized stage evaluator {} has invalid current output slot metadata", + stage.evaluator_label + ))); + } + max_output_stop = max_output_stop.max(slot.output_stop); + } + if max_output_stop != stage.output_length { + return Err(RusticolError::artifact(format!( + "generic serialized stage evaluator {} output slots do not cover evaluator outputs", + stage.evaluator_label + ))); + } + Ok(()) +} + +fn validate_generic_stage_input_components( + stage: &GenericSerializedStageEvaluatorManifest, + global_parameter_count: usize, +) -> RusticolResult<()> { + if stage.input_components.is_empty() { + return Err(RusticolError::artifact(format!( + "generic serialized stage evaluator {} has no local input components", + stage.evaluator_label + ))); + } + let mut seen_parameters = BTreeSet::new(); + for component in &stage.input_components { + if component.kind != "value" + && component.kind != "momentum" + && component.kind != "model_parameter" + { + return Err(RusticolError::artifact(format!( + "generic serialized stage evaluator {} has invalid local input kind {:?}", + stage.evaluator_label, component.kind + ))); + } + if component.global_component >= global_parameter_count + || component.parameter_index >= stage.parameter_count + || component.source_id >= global_parameter_count + || component.component >= global_parameter_count + || !seen_parameters.insert(component.parameter_index) + { + return Err(RusticolError::artifact(format!( + "generic serialized stage evaluator {} has invalid local input component map", + stage.evaluator_label + ))); + } + if component.real_valued + && component.kind != "momentum" + && component.kind != "model_parameter" + { + return Err(RusticolError::artifact(format!( + "generic serialized stage evaluator {} marks a complex local input as real", + stage.evaluator_label + ))); + } + } + if seen_parameters.len() != stage.parameter_count { + return Err(RusticolError::artifact(format!( + "generic serialized stage evaluator {} local input map is not dense", + stage.evaluator_label + ))); + } + let real_inputs = stage + .input_components + .iter() + .filter_map(|component| { + if component.real_valued { + Some(component.parameter_index) + } else { + None + } + }) + .collect::>(); + if real_inputs != stage.real_valued_inputs { + return Err(RusticolError::artifact(format!( + "generic serialized stage evaluator {} local real-input metadata is inconsistent", + stage.evaluator_label + ))); + } + Ok(()) +} + +pub(super) fn validate_slot_ref( + slot: &GenericSlotRefManifest, + current_id: usize, + current_storage: &GenericCurrentStorageManifest, +) -> RusticolResult<()> { + if slot.current_id != current_id + || slot.component_stop != slot.component_start + slot.dimension + || slot.dimension == 0 + { + return Err(RusticolError::artifact(format!( + "generic slot reference for current {current_id} is inconsistent" + ))); + } + let storage_slot = current_storage + .current_slots + .get(current_id) + .ok_or_else(|| { + RusticolError::artifact(format!( + "generic slot reference for current {current_id} points outside current storage" + )) + })?; + if storage_slot.current_id != slot.current_id + || storage_slot.component_start != slot.component_start + || storage_slot.component_stop != slot.component_stop + || storage_slot.dimension != slot.dimension + { + return Err(RusticolError::artifact(format!( + "generic slot reference for current {current_id} does not match current storage" + ))); + } + Ok(()) +} + +fn validate_value_slot_ref( + slot: &GenericValueSlotRefManifest, + current_id: usize, + expected_variant: Option<&str>, + value_storage: &GenericValueStorageManifest, +) -> RusticolResult<()> { + if slot.current_id != current_id + || slot.component_stop != slot.component_start + slot.dimension + || slot.dimension == 0 + { + return Err(RusticolError::artifact(format!( + "generic value slot reference for current {current_id} is inconsistent" + ))); + } + if let Some(expected) = expected_variant + && slot.variant != expected + { + return Err(RusticolError::artifact(format!( + "generic value slot reference for current {current_id} uses variant {:?}, expected {expected:?}", + slot.variant + ))); + } + let storage_slot = value_storage + .value_slots + .get(slot.value_slot_id) + .ok_or_else(|| { + RusticolError::artifact(format!( + "generic value slot reference for current {current_id} points outside value storage" + )) + })?; + if storage_slot.current_id != slot.current_id + || storage_slot.variant != slot.variant + || storage_slot.component_start != slot.component_start + || storage_slot.component_stop != slot.component_stop + || storage_slot.dimension != slot.dimension + { + return Err(RusticolError::artifact(format!( + "generic value slot reference for current {current_id} does not match value storage" + ))); + } + Ok(()) +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum GenericInputValueVariant { + Source, + Propagated, + Unpropagated, +} + +impl GenericInputValueVariant { + fn as_str(self) -> &'static str { + match self { + Self::Source => "source", + Self::Propagated => "propagated", + Self::Unpropagated => "unpropagated", + } + } +} + +fn build_input_value_variants( + current_storage: &GenericCurrentStorageManifest, + value_storage: &GenericValueStorageManifest, +) -> RusticolResult>> { + let mut variants = current_storage + .current_slots + .iter() + .map(|current| { + current + .is_source + .then_some(GenericInputValueVariant::Source) + }) + .collect::>(); + for slot in &value_storage.value_slots { + let current = current_storage + .current_slots + .get(slot.current_id) + .ok_or_else(|| { + RusticolError::artifact(format!( + "generic value slot references missing current {}", + slot.current_id + )) + })?; + if current.is_source { + continue; + } + match slot.variant.as_str() { + "propagated" => { + variants[slot.current_id] = Some(GenericInputValueVariant::Propagated); + } + "unpropagated" + if variants[slot.current_id] != Some(GenericInputValueVariant::Propagated) => + { + variants[slot.current_id] = Some(GenericInputValueVariant::Unpropagated); + } + _ => {} + } + } + Ok(variants) +} + +fn input_value_variant( + input_value_variants: &[Option], + current_id: usize, +) -> RusticolResult<&'static str> { + let variant = input_value_variants.get(current_id).ok_or_else(|| { + RusticolError::artifact(format!( + "generic input value references missing current {current_id}" + )) + })?; + variant + .map(GenericInputValueVariant::as_str) + .ok_or_else(|| { + RusticolError::artifact(format!( + "generic input value references current {current_id} without an input value slot" + )) + }) +} + +fn amplitude_value_variant( + current_storage: &GenericCurrentStorageManifest, + current_id: usize, +) -> RusticolResult<&'static str> { + let current = current_storage + .current_slots + .get(current_id) + .ok_or_else(|| { + RusticolError::artifact(format!( + "generic amplitude value references missing current {current_id}" + )) + })?; + Ok(if current.is_source { + "source" + } else { + "unpropagated" + }) +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/engine_tests.rs b/pyamplicol/rust/crates/rusticol-core/src/engine_tests.rs new file mode 100644 index 0000000000..196df30c3f --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/engine_tests.rs @@ -0,0 +1,6441 @@ +// SPDX-License-Identifier: 0BSD + +use super::evaluation::accumulate_selected_lc_replay_resolved_f64; +use super::physics::certifies_lc_direct_total_source; +use super::*; +use serde_json::json; + +fn symjit_plane_application_manifest(target: Value) -> SymjitPlaneApplicationManifest { + serde_json::from_value(json!({ + "application_path": "evaluators/kernel.plane.symjit", + "application_abi": SYMJIT_PLANE_APPLICATION_ABI, + "storage_abi": SYMJIT_APPLICATION_STORAGE_ABI, + "element_layout": "split-complex-plane-major", + "descriptor_order": "inputs-re-im-then-outputs-re-im", + "input_complex_count": 2, + "output_complex_count": 1, + "input_plane_count": 4, + "output_plane_count": 2, + "compiler_type": "native", + "translation_mode": "symbolica-structured-instructions", + "optimization_level": 2, + "simd": true, + "complex": true, + "fast_math": true, + "fast_complex": false, + "compression": false, + "threading": false, + "direct_arena": true, + "source_digest": "a".repeat(64), + "target": target, + })) + .unwrap() +} + +#[test] +fn symjit_plane_application_manifest_authenticates_target_shape() { + for target in [ + json!({"word_bits": 64, "endianness": "little"}), + json!({ + "word_bits": 64, + "endianness": "little", + "triple": "aarch64-apple-darwin", + "cpu_features": ["aes", "neon"], + }), + ] { + symjit_plane_application_manifest(target) + .validate(2, 1, 2) + .unwrap(); + } + + for target in [ + json!({"word_bits": 32, "endianness": "little"}), + json!({"word_bits": 64, "endianness": "big"}), + json!({"word_bits": 64, "endianness": "little", "triple": ""}), + json!({"word_bits": 64, "endianness": "little", "cpu_features": "neon"}), + json!({ + "word_bits": 64, + "endianness": "little", + "cpu_features": ["neon", "neon"], + }), + json!({"word_bits": 64, "endianness": "little", "unexpected": true}), + ] { + let error = symjit_plane_application_manifest(target) + .validate(2, 1, 2) + .unwrap_err(); + assert_eq!(error.kind(), crate::RusticolErrorKind::Compatibility); + assert!(error.message().contains("regenerate")); + } +} + +fn native_compiled_direct_application() -> NativeCompiledDirectApplicationManifest { + NativeCompiledDirectApplicationManifest { + application_abi: "pyamplicol-native-compiled-direct-application-v1".to_string(), + function_name: "direct_leaf".to_string(), + source_path: "compiled/direct_leaf.cpp".to_string(), + library_path: "compiled/libdirect_leaf".to_string(), + target: NativeCompiledDirectTargetManifest { + triple: "aarch64-apple-darwin".to_string(), + cpu_features: vec!["neon".to_string()], + }, + evaluator_state_sha256: "a".repeat(64), + instruction_count: 17, + temporary_count: 3, + input_plane_count: 4, + scalar_input_count: 1, + output_plane_count: 4, + simd_lane_width: 2, + logical_stack_bytes: 160, + output_semantics: "factor-free-overwrite".to_string(), + } +} + +#[test] +fn native_compiled_direct_application_manifest_validates_shape() { + let manifest = EvaluatorManifest::CompiledComplex { + runtime_capability: "symbolica.compiled-cpp.complex-f64.v1".to_string(), + function_name: "direct_leaf".to_string(), + input_len: 3, + output_len: 2, + library_path: "compiled/liblegacy_leaf".to_string(), + evaluator_state_path: Some("compiled/direct_leaf.evaluator.bin".to_string()), + number_type: "complex".to_string(), + native_direct_application: Some(native_compiled_direct_application()), + }; + + assert_eq!(manifest.io_len().unwrap(), (3, 2)); +} + +#[test] +fn native_compiled_direct_application_manifest_rejects_inconsistent_stack() { + let mut application = native_compiled_direct_application(); + application.logical_stack_bytes += 32; + let manifest = EvaluatorManifest::CompiledComplex { + runtime_capability: "symbolica.compiled-cpp.complex-f64.v1".to_string(), + function_name: "direct_leaf".to_string(), + input_len: 3, + output_len: 2, + library_path: "compiled/liblegacy_leaf".to_string(), + evaluator_state_path: Some("compiled/direct_leaf.evaluator.bin".to_string()), + number_type: "complex".to_string(), + native_direct_application: Some(application), + }; + + let error = manifest.io_len().unwrap_err(); + assert_eq!(error.kind(), crate::RusticolErrorKind::Integrity); + assert!(error.to_string().contains("logical stack metadata")); +} + +pub(super) fn test_physics_runtime(color_accuracy: &str) -> PhysicsRuntime { + let contracted = color_accuracy != "lc"; + let color_components = if contracted { + vec![crate::ColorComponent::ContractedColor( + crate::ContractedColor { + id: "contracted".to_string(), + index: 0, + description: "contracted color sum".to_string(), + }, + )] + } else { + vec![ + crate::ColorComponent::LcFlow(crate::LcColorFlow { + id: "flow:0".to_string(), + index: 0, + word: vec![1, 2], + representative_id: "flow:0".to_string(), + computed: true, + coefficient: 1.0, + }), + crate::ColorComponent::LcFlow(crate::LcColorFlow { + id: "flow:1".to_string(), + index: 1, + word: vec![2, 1], + representative_id: "flow:0".to_string(), + computed: false, + coefficient: 1.0, + }), + ] + }; + let physical_color_ids = color_components + .iter() + .map(|item| item.id().to_string()) + .collect(); + let mut helicities = vec![ + crate::Helicity { + id: "hel:+-".to_string(), + index: 0, + values: vec![1, -1, 1], + representative_id: "hel:+-".to_string(), + computed: true, + structural_zero: false, + coefficient: 1.0, + }, + crate::Helicity { + id: "hel:-+".to_string(), + index: 1, + values: vec![-1, 1, 1], + representative_id: "hel:+-".to_string(), + computed: false, + structural_zero: false, + coefficient: 1.0, + }, + ]; + if !contracted { + helicities.push(crate::Helicity { + id: "hel:zero".to_string(), + index: 2, + values: vec![1, 1, 1], + representative_id: "hel:zero".to_string(), + computed: false, + structural_zero: true, + coefficient: 0.0, + }); + } + PhysicsRuntime::new(ProcessPhysicsV1 { + schema_version: crate::RUNTIME_PHYSICS_SCHEMA_VERSION, + kind: "pyamplicol-resolved-physics".to_string(), + process_id: "x_x_to_y".to_string(), + process: "x x > y".to_string(), + color_accuracy: if contracted { + if color_accuracy == "nlc" { + crate::ColorAccuracy::Nlc + } else { + crate::ColorAccuracy::Full + } + } else { + crate::ColorAccuracy::Lc + }, + coverage: crate::Coverage { + helicities: "complete".to_string(), + color: if contracted { "contracted" } else { "complete" }.to_string(), + color_kind: if contracted { + "contracted-color" + } else { + "physical-lc-flows" + } + .to_string(), + structural_zero_helicity_count: usize::from(!contracted), + }, + external_particles: vec![ + test_external_particle(0, "x", 1, crate::ParticleRole::Initial), + test_external_particle(1, "x~", -1, crate::ParticleRole::Initial), + test_external_particle(2, "y", 23, crate::ParticleRole::Final), + ], + helicities, + color_components, + reduction: crate::Reduction { + kind: if contracted { + crate::ReductionKind::ContractedColor + } else { + crate::ReductionKind::LcDiagonal + }, + groups: vec![crate::ReductionGroup { + id: "group:7".to_string(), + representative_helicity_id: "hel:+-".to_string(), + physical_helicity_ids: vec!["hel:+-".to_string(), "hel:-+".to_string()], + representative_color_id: if contracted { + "contracted".to_string() + } else { + "flow:0".to_string() + }, + physical_color_ids, + }], + }, + model_parameters: Vec::new(), + selectors: crate::SelectorCapabilities { + helicity: true, + color_flow: !contracted, + contracted_color: false, + }, + extensions: BTreeMap::new(), + }) + .unwrap() +} + +#[test] +fn total_selector_canonicalization_elides_only_exact_authenticated_axes() { + let physics = test_physics_runtime("lc"); + let all_helicities = physics + .manifest + .helicities + .iter() + .map(|helicity| helicity.id.clone()) + .collect::>(); + let all_colors = physics + .manifest + .color_components + .iter() + .map(|color| color.id().to_string()) + .collect::>(); + assert!( + physics + .canonical_total_helicity_selector(Some(&all_helicities)) + .is_none() + ); + assert!( + physics + .canonical_total_color_selector(Some(&all_colors)) + .is_none() + ); + + let helicity_subset = BTreeSet::from(["hel:+-".to_string()]); + let color_subset = BTreeSet::from(["flow:0".to_string()]); + assert_eq!( + physics.canonical_total_helicity_selector(Some(&helicity_subset)), + Some(&helicity_subset) + ); + assert_eq!( + physics.canonical_total_color_selector(Some(&color_subset)), + Some(&color_subset) + ); + + let same_size_unknown_helicities = BTreeSet::from([ + "hel:+-".to_string(), + "hel:-+".to_string(), + "hel:unknown".to_string(), + ]); + let same_size_unknown_colors = + BTreeSet::from(["flow:0".to_string(), "flow:unknown".to_string()]); + let retained_helicities = + physics.canonical_total_helicity_selector(Some(&same_size_unknown_helicities)); + let retained_colors = physics.canonical_total_color_selector(Some(&same_size_unknown_colors)); + assert_eq!(retained_helicities, Some(&same_size_unknown_helicities)); + assert_eq!(retained_colors, Some(&same_size_unknown_colors)); + assert_eq!( + physics + .selected_helicity_indices(retained_helicities) + .unwrap_err() + .kind(), + crate::RusticolErrorKind::Selector + ); + assert_eq!( + physics + .selected_color_indices(retained_colors) + .unwrap_err() + .kind(), + crate::RusticolErrorKind::Selector + ); + + let mut selected_manifest = test_physics_runtime("full").manifest; + selected_manifest.helicities.truncate(1); + selected_manifest.reduction.groups[0] + .physical_helicity_ids + .truncate(1); + selected_manifest.coverage.helicities = "selected".to_string(); + let selected_physics = PhysicsRuntime::new(selected_manifest).unwrap(); + let sole_helicity = BTreeSet::from([selected_physics.manifest.helicities[0].id.clone()]); + assert!( + selected_physics + .canonical_total_helicity_selector(Some(&sole_helicity)) + .is_none() + ); +} + +#[test] +fn compiled_symmetric_group_rejects_exhaustive_runtime_selector_before_canonicalization() { + let physics = test_physics_runtime("full"); + let all_helicities = physics + .manifest + .helicities + .iter() + .map(|helicity| helicity.id.clone()) + .collect::>(); + let plan = crate::recurrence::RecurrenceColorContraction::symmetric_group_s3_for_runtime_test( + vec![0; 13], + 13, + ); + let contraction = ColorContractionRuntime { + group_count: 0, + entries: Vec::new(), + repeated_block: None, + symmetric_group: Some(CompiledSymmetricGroupColorContraction { + plan, + ordered_group_indices: Vec::new(), + workspace: None, + }), + group_scratch_f64: Vec::new(), + }; + let mut runtime = empty_generic_runtime(); + runtime.physics = Some(Arc::new(physics)); + runtime.amplitude_stage = Some(test_amplitude_runtime(Vec::new(), Some(contraction))); + let batch = F64MomentumBatchView::from_contiguous_prevalidated(&[], 1, 0, None).unwrap(); + let error = runtime + .run_f64_selected_into_unprofiled(batch, Some(&all_helicities), None, &mut [0.0]) + .unwrap_err(); + assert_eq!(error.kind(), crate::RusticolErrorKind::Compatibility); + assert!(error.message().contains("runtime selectors")); +} + +fn test_external_particle( + index: usize, + particle: &str, + pdg: i32, + role: crate::ParticleRole, +) -> crate::ExternalParticle { + crate::ExternalParticle { + index, + label: index + 1, + particle: particle.to_string(), + pdg, + role, + momentum_slot: index, + momentum_components: [ + "E".to_string(), + "px".to_string(), + "py".to_string(), + "pz".to_string(), + ], + } +} + +fn replay_test_physics() -> PhysicsRuntime { + PhysicsRuntime::new(ProcessPhysicsV1 { + schema_version: crate::RUNTIME_PHYSICS_SCHEMA_VERSION, + kind: "pyamplicol-resolved-physics".to_string(), + process_id: "d_dbar_to_g_g".to_string(), + process: "d d~ > g g".to_string(), + color_accuracy: crate::ColorAccuracy::Lc, + coverage: crate::Coverage { + helicities: "complete".to_string(), + color: "complete".to_string(), + color_kind: "physical-lc-flows".to_string(), + structural_zero_helicity_count: 0, + }, + external_particles: vec![ + test_external_particle(0, "d", 1, crate::ParticleRole::Initial), + test_external_particle(1, "d~", -1, crate::ParticleRole::Initial), + test_external_particle(2, "g", 21, crate::ParticleRole::Final), + test_external_particle(3, "g", 21, crate::ParticleRole::Final), + ], + helicities: vec![ + crate::Helicity { + id: "h:+1,-1,+1,-1".to_string(), + index: 0, + values: vec![1, -1, 1, -1], + computed: true, + structural_zero: false, + representative_id: "h:+1,-1,+1,-1".to_string(), + coefficient: 1.0, + }, + crate::Helicity { + id: "h:+1,-1,-1,+1".to_string(), + index: 1, + values: vec![1, -1, -1, 1], + computed: false, + structural_zero: false, + representative_id: "h:+1,-1,+1,-1".to_string(), + coefficient: 1.0, + }, + ], + color_components: vec![ + crate::ColorComponent::LcFlow(crate::LcColorFlow { + id: "flow:1,2,3,4".to_string(), + index: 0, + word: vec![1, 2, 3, 4], + computed: true, + representative_id: "flow:1,2,3,4".to_string(), + coefficient: 1.0, + }), + crate::ColorComponent::LcFlow(crate::LcColorFlow { + id: "flow:1,4,3,2".to_string(), + index: 1, + word: vec![1, 4, 3, 2], + computed: false, + representative_id: "flow:1,2,3,4".to_string(), + coefficient: 1.0, + }), + crate::ColorComponent::LcFlow(crate::LcColorFlow { + id: "flow:1,2,4,3".to_string(), + index: 2, + word: vec![1, 2, 4, 3], + computed: false, + representative_id: "flow:1,2,3,4".to_string(), + coefficient: 1.0, + }), + ], + reduction: crate::Reduction { + kind: crate::ReductionKind::LcDiagonal, + groups: vec![crate::ReductionGroup { + id: "reduction:7".to_string(), + representative_helicity_id: "h:+1,-1,+1,-1".to_string(), + representative_color_id: "flow:1,2,3,4".to_string(), + physical_helicity_ids: vec!["h:+1,-1,+1,-1".to_string()], + physical_color_ids: vec!["flow:1,2,3,4".to_string()], + }], + }, + model_parameters: Vec::new(), + selectors: crate::SelectorCapabilities { + helicity: true, + color_flow: true, + contracted_color: false, + }, + extensions: BTreeMap::new(), + }) + .unwrap() +} + +#[test] +fn lc_replay_routes_materialized_cells_to_public_axes_and_selectors() { + let physics = replay_test_physics(); + let mappings = vec![Vec::new(), vec![(2, 3), (3, 2)]]; + let routes = vec![ + vec![LcTopologyReplaySectorRoute { + physical_sector_id: 0, + materialized_sector_id: 0, + weight: 2.0, + sign: 1, + amplitude_factor: [2.0, 0.0], + residual: false, + }], + vec![LcTopologyReplaySectorRoute { + physical_sector_id: 1, + materialized_sector_id: 0, + weight: 1.0, + sign: -1, + amplitude_factor: [-1.0, 0.0], + residual: false, + }], + ]; + let plan = physics + .lc_resolved_replay_plan( + &mappings, + &routes, + &BTreeMap::from([( + 0, + LcMaterializedSector { + color_index: 0, + reduction_weight: 2.0, + }, + )]), + ) + .unwrap(); + assert_eq!(plan.color_count, 3); + let replay_selection = physics + .select_lc_resolved_replay_plan( + &plan, + Some(&BTreeSet::from(["h:+1,-1,-1,+1".to_string()])), + Some(&BTreeSet::from(["flow:1,2,4,3".to_string()])), + ) + .unwrap(); + assert_eq!(replay_selection.mapping_indices, vec![1]); + assert_eq!(replay_selection.entries.len(), 1); + assert_eq!(replay_selection.entries[0].routes.len(), 1); + assert_eq!(replay_selection.entries[0].routes[0].source_index, 0); + assert_eq!(replay_selection.entries[0].routes[0].target_index, 0); + assert_eq!(replay_selection.source_helicity_indices, vec![vec![0]]); + assert_eq!(replay_selection.source_color_indices, vec![vec![0]]); + + let materialized = ResolvedValues { + values: vec![ + 3.0, 0.0, 0.0, 0.0, 0.0, 0.0, // identity, point 0 + 5.0, 0.0, 0.0, 0.0, 0.0, 0.0, // identity, point 1 + 7.0, 0.0, 0.0, 0.0, 0.0, 0.0, // swap, point 0 + 11.0, 0.0, 0.0, 0.0, 0.0, 0.0, // swap, point 1 + ], + point_count: 4, + helicity_indices: vec![0, 1], + color_indices: vec![0, 1, 2], + }; + let mut full = vec![0.0; 12]; + super::evaluation::accumulate_lc_replay_resolved_f64( + &mut full, + 2, + &materialized, + &plan.entries, + 6, + ) + .unwrap(); + + assert_eq!( + full, + vec![3.0, 3.0, 0.0, 0.0, 0.0, 7.0, 5.0, 5.0, 0.0, 0.0, 0.0, 11.0] + ); + assert_eq!(full[..6].iter().sum::(), 2.0 * 3.0 + 7.0); + assert_eq!(full[6..].iter().sum::(), 2.0 * 5.0 + 11.0); + + let selected = super::evaluation::select_resolved_values( + full, + 2, + &physics, + Some(&BTreeSet::from(["h:+1,-1,-1,+1".to_string()])), + Some(&BTreeSet::from(["flow:1,2,4,3".to_string()])), + ) + .unwrap(); + assert_eq!(selected.helicity_indices, vec![1]); + assert_eq!(selected.color_indices, vec![2]); + assert_eq!(selected.values, vec![7.0, 11.0]); +} + +#[test] +fn lc_replay_requires_every_public_flow_reduction_member() { + let mut manifest = replay_test_physics().manifest; + manifest.color_components.remove(1); + manifest.color_components[1] = match manifest.color_components[1].clone() { + crate::ColorComponent::LcFlow(mut flow) => { + flow.index = 1; + crate::ColorComponent::LcFlow(flow) + } + value => value, + }; + manifest.coverage.color = "selected".to_string(); + let physics = PhysicsRuntime::new(manifest).unwrap(); + + let error = physics + .lc_resolved_replay_plan( + &vec![Vec::new()], + &[vec![LcTopologyReplaySectorRoute { + physical_sector_id: 0, + materialized_sector_id: 0, + weight: 2.0, + sign: 1, + amplitude_factor: [2.0, 0.0], + residual: false, + }]], + &BTreeMap::from([( + 0, + LcMaterializedSector { + color_index: 0, + reduction_weight: 2.0, + }, + )]), + ) + .unwrap_err(); + + assert!(error.to_string().contains("missing replayed LC flow word")); +} + +#[test] +fn lc_replay_expands_trace_reflection_for_residual_sectors() { + fn flow( + index: usize, + word: &[usize], + computed: bool, + representative: &[usize], + ) -> crate::ColorComponent { + let id = format!( + "flow:{}", + word.iter() + .map(usize::to_string) + .collect::>() + .join(",") + ); + let representative_id = format!( + "flow:{}", + representative + .iter() + .map(usize::to_string) + .collect::>() + .join(",") + ); + crate::ColorComponent::LcFlow(crate::LcColorFlow { + id, + index, + word: word.to_vec(), + computed, + representative_id, + coefficient: 1.0, + }) + } + + let mut manifest = replay_test_physics().manifest; + manifest.color_components = vec![ + flow(0, &[1, 2, 3, 4], true, &[1, 2, 3, 4]), + flow(1, &[1, 2, 4, 3], false, &[1, 2, 3, 4]), + flow(2, &[1, 3, 2, 4], true, &[1, 3, 2, 4]), + flow(3, &[1, 3, 4, 2], false, &[1, 2, 3, 4]), + flow(4, &[1, 4, 2, 3], false, &[1, 3, 2, 4]), + flow(5, &[1, 4, 3, 2], false, &[1, 2, 3, 4]), + ]; + manifest.reduction.groups = vec![ + crate::ReductionGroup { + id: "reduction:0".to_string(), + representative_helicity_id: "h:+1,-1,+1,-1".to_string(), + representative_color_id: "flow:1,2,3,4".to_string(), + physical_helicity_ids: vec!["h:+1,-1,+1,-1".to_string()], + physical_color_ids: vec!["flow:1,2,3,4".to_string(), "flow:1,4,3,2".to_string()], + }, + crate::ReductionGroup { + id: "reduction:1".to_string(), + representative_helicity_id: "h:+1,-1,+1,-1".to_string(), + representative_color_id: "flow:1,3,2,4".to_string(), + physical_helicity_ids: vec!["h:+1,-1,+1,-1".to_string()], + physical_color_ids: vec!["flow:1,3,2,4".to_string(), "flow:1,4,2,3".to_string()], + }, + ]; + let physics = PhysicsRuntime::new(manifest).unwrap(); + let mappings = vec![Vec::new(), vec![(2, 3), (3, 2)]]; + let routes = vec![ + vec![ + LcTopologyReplaySectorRoute { + physical_sector_id: 0, + materialized_sector_id: 0, + weight: 2.0, + sign: 1, + amplitude_factor: [2.0, 0.0], + residual: false, + }, + LcTopologyReplaySectorRoute { + physical_sector_id: 2, + materialized_sector_id: 2, + weight: 1.0, + sign: 1, + amplitude_factor: [1.0, 0.0], + residual: true, + }, + ], + vec![LcTopologyReplaySectorRoute { + physical_sector_id: 1, + materialized_sector_id: 0, + weight: 2.0, + sign: 1, + amplitude_factor: [2.0, 0.0], + residual: false, + }], + ]; + let materialized = BTreeMap::from([ + ( + 0, + LcMaterializedSector { + color_index: 0, + reduction_weight: 2.0, + }, + ), + ( + 2, + LcMaterializedSector { + color_index: 2, + reduction_weight: 2.0, + }, + ), + ]); + + let plan = physics + .lc_resolved_replay_plan(&mappings, &routes, &materialized) + .unwrap(); + let target_colors = plan + .entries + .iter() + .flat_map(|entry| entry.routes.iter()) + .map(|route| route.target_index % plan.color_count) + .collect::>(); + + assert_eq!(target_colors, (0..6).collect()); + assert!( + plan.entries + .iter() + .flat_map(|entry| entry.routes.iter()) + .all(|route| route.weight.to_bits() == 1.0f64.to_bits()) + ); +} + +fn alias_test_physics(color_accuracy: &str) -> ProcessPhysicsV1 { + let contracted = color_accuracy != "lc"; + let representative_helicity_id = "h:+1,-1,+0,+1,-1".to_string(); + let helicities = vec![ + crate::Helicity { + id: representative_helicity_id.clone(), + index: 0, + values: vec![1, -1, 0, 1, -1], + representative_id: representative_helicity_id.clone(), + computed: true, + structural_zero: false, + coefficient: 1.0, + }, + crate::Helicity { + id: "h:-1,+1,+0,-1,+1".to_string(), + index: 1, + values: vec![-1, 1, 0, -1, 1], + representative_id: representative_helicity_id.clone(), + computed: false, + structural_zero: false, + coefficient: 1.0, + }, + ]; + let (color_components, reduction_kind, representative_color_id, physical_color_ids) = + if contracted { + ( + vec![crate::ColorComponent::ContractedColor( + crate::ContractedColor { + id: "color:contracted".to_string(), + index: 0, + description: "contracted color sum".to_string(), + }, + )], + crate::ReductionKind::ContractedColor, + "color:contracted".to_string(), + vec!["color:contracted".to_string()], + ) + } else { + ( + vec![ + crate::ColorComponent::LcFlow(crate::LcColorFlow { + id: "flow:1,3,4,5,2".to_string(), + index: 0, + word: vec![1, 3, 4, 5, 2], + representative_id: "flow:1,3,4,5,2".to_string(), + computed: true, + coefficient: 1.0, + }), + crate::ColorComponent::LcFlow(crate::LcColorFlow { + id: "flow:1,5,4,3,2".to_string(), + index: 1, + word: vec![1, 5, 4, 3, 2], + representative_id: "flow:1,3,4,5,2".to_string(), + computed: false, + coefficient: 1.0, + }), + ], + crate::ReductionKind::LcDiagonal, + "flow:1,3,4,5,2".to_string(), + vec!["flow:1,3,4,5,2".to_string(), "flow:1,5,4,3,2".to_string()], + ) + }; + ProcessPhysicsV1 { + schema_version: crate::RUNTIME_PHYSICS_SCHEMA_VERSION, + kind: "pyamplicol-resolved-physics".to_string(), + process_id: "representative".to_string(), + process: "d d~ > z g a".to_string(), + color_accuracy: match color_accuracy { + "lc" => crate::ColorAccuracy::Lc, + "nlc" => crate::ColorAccuracy::Nlc, + _ => crate::ColorAccuracy::Full, + }, + coverage: crate::Coverage { + helicities: "complete".to_string(), + color: if contracted { "contracted" } else { "complete" }.to_string(), + color_kind: if contracted { + "contracted-color" + } else { + "physical-lc-flows" + } + .to_string(), + structural_zero_helicity_count: 0, + }, + external_particles: vec![ + test_external_particle(0, "d", 1, crate::ParticleRole::Initial), + test_external_particle(1, "d~", -1, crate::ParticleRole::Initial), + test_external_particle(2, "z", 23, crate::ParticleRole::Final), + test_external_particle(3, "g", 21, crate::ParticleRole::Final), + test_external_particle(4, "a", 22, crate::ParticleRole::Final), + ], + helicities, + color_components, + reduction: crate::Reduction { + kind: reduction_kind, + groups: vec![crate::ReductionGroup { + id: "group:7".to_string(), + representative_helicity_id, + representative_color_id, + physical_helicity_ids: vec![ + "h:+1,-1,+0,+1,-1".to_string(), + "h:-1,+1,+0,-1,+1".to_string(), + ], + physical_color_ids, + }], + }, + model_parameters: Vec::new(), + selectors: crate::SelectorCapabilities { + helicity: true, + color_flow: !contracted, + contracted_color: false, + }, + extensions: BTreeMap::new(), + } +} + +fn three_cycle_alias() -> crate::ProcessAlias { + crate::ProcessAlias { + id: "cycled".to_string(), + expression: "d d~ > a z g".to_string(), + external_pdgs: vec![1, -1, 22, 23, 21], + external_permutation: vec![0, 1, 3, 4, 2], + } +} + +fn stored_alias_selection( + physics: &ProcessPhysicsV1, + alias: &crate::ProcessAlias, +) -> crate::ArtifactSelection { + crate::ArtifactSelection { + process: crate::ArtifactProcess { + id: physics.process_id.clone(), + expression: physics.process.clone(), + color_accuracy: physics.color_accuracy.as_str().to_string(), + external_pdgs: physics + .external_particles + .iter() + .map(|particle| particle.pdg) + .collect(), + physics_path: "processes/test/physics.json".to_string(), + required_runtime_capabilities: vec!["symjit.application.complex-f64.v1".to_string()], + aliases: vec![alias.clone()], + }, + requested_id: alias.id.clone(), + alias: Some(alias.clone()), + public_expression: alias.expression.clone(), + external_pdgs: alias.external_pdgs.clone(), + external_permutation: alias.external_permutation.clone(), + inferred_permutation: false, + process_group: None, + } +} + +#[test] +fn final_state_alias_three_cycle_remaps_lc_metadata_and_selectors() { + let representative_manifest = alias_test_physics("lc"); + representative_manifest.validate().unwrap(); + let representative_physics = PhysicsRuntime::new(representative_manifest.clone()).unwrap(); + let alias = three_cycle_alias(); + let alias_manifest = apply_process_permutation_metadata( + representative_manifest.clone(), + &stored_alias_selection(&representative_manifest, &alias), + ) + .unwrap(); + + let helicity_id_map = representative_manifest + .helicities + .iter() + .zip(&alias_manifest.helicities) + .map(|(representative, public)| (representative.id.clone(), public.id.clone())) + .collect::>(); + let color_id_map = representative_manifest + .color_components + .iter() + .zip(&alias_manifest.color_components) + .map(|(representative, public)| (representative.id().to_string(), public.id().to_string())) + .collect::>(); + let mut override_runtime = empty_generic_runtime(); + override_runtime.physics_reduction_override = Some(representative_manifest.reduction.clone()); + override_runtime + .remap_physics_reduction_overrides(&helicity_id_map, &color_id_map) + .unwrap(); + assert_eq!( + override_runtime.physics_reduction_override, + Some(alias_manifest.reduction.clone()) + ); + + assert_eq!(alias_manifest.process_id, "cycled"); + assert_eq!(alias_manifest.process, "d d~ > a z g"); + assert_eq!( + alias_manifest + .external_particles + .iter() + .map(|particle| ( + particle.index, + particle.label, + particle.momentum_slot, + particle.pdg + )) + .collect::>(), + vec![ + (0, 1, 0, 1), + (1, 2, 1, -1), + (2, 3, 2, 22), + (3, 4, 3, 23), + (4, 5, 4, 21), + ] + ); + assert_eq!( + alias_manifest + .external_particles + .iter() + .map(|particle| particle.particle.as_str()) + .collect::>(), + vec!["d", "d~", "a", "z", "g"] + ); + assert_eq!(alias_manifest.helicities[0].values, vec![1, -1, -1, 0, 1]); + assert_eq!(alias_manifest.helicities[0].id, "h:+1,-1,-1,+0,+1"); + assert_eq!( + alias_manifest.helicities[1].representative_id, + "h:+1,-1,-1,+0,+1" + ); + let PhysicsColorComponentV1::LcFlow(first_flow) = &alias_manifest.color_components[0] else { + panic!("expected LC flow"); + }; + let PhysicsColorComponentV1::LcFlow(second_flow) = &alias_manifest.color_components[1] else { + panic!("expected LC flow"); + }; + assert_eq!(first_flow.word, vec![1, 4, 5, 3, 2]); + assert_eq!(first_flow.id, "flow:1,4,5,3,2"); + assert_eq!(second_flow.id, "flow:1,3,5,4,2"); + assert_eq!(second_flow.representative_id, "flow:1,4,5,3,2"); + assert_eq!( + alias_manifest.reduction.groups[0].physical_helicity_ids, + vec![ + "h:+1,-1,-1,+0,+1".to_string(), + "h:-1,+1,+1,+0,-1".to_string(), + ] + ); + assert_eq!( + alias_manifest.reduction.groups[0].physical_color_ids, + vec!["flow:1,4,5,3,2".to_string(), "flow:1,3,5,4,2".to_string(),] + ); + + let alias_physics = PhysicsRuntime::new(alias_manifest.clone()).unwrap(); + assert_eq!( + alias_physics + .selected_helicity_indices(Some(&BTreeSet::from(["h:-1,+1,+1,+0,-1".to_string(),]))) + .unwrap(), + vec![1] + ); + assert_eq!( + alias_physics + .selected_color_indices(Some(&BTreeSet::from(["flow:1,3,5,4,2".to_string(),]))) + .unwrap(), + vec![1] + ); + + let representative_total = test_amplitude_runtime(vec![c64(2.0, 0.0)], None) + .reduce_scratch_f64_resolved(1, &representative_physics, 4.0, None, None) + .unwrap() + .values + .iter() + .sum::(); + let alias_total = test_amplitude_runtime(vec![c64(2.0, 0.0)], None) + .reduce_scratch_f64_resolved(1, &alias_physics, 4.0, None, None) + .unwrap() + .values + .iter() + .sum::(); + assert_eq!(alias_total, representative_total); + + let representative_point = (0..5) + .map(|index| [index as f64, index as f64 + 0.1, 0.0, 0.0]) + .collect::>(); + let mut alias_point = vec![[0.0; 4]; 5]; + for (representative_index, alias_index) in + alias.external_permutation.iter().copied().enumerate() + { + alias_point[alias_index] = representative_point[representative_index]; + } + let crossing_map = alias + .external_permutation + .iter() + .copied() + .enumerate() + .map(|(target_index, source_index)| InputCrossingMapEntry { + target_index, + source_index, + sign: 1.0, + }) + .collect::>(); + assert_eq!( + apply_input_crossing_map(vec![alias_point], 5, Some(&crossing_map)).unwrap(), + vec![representative_point] + ); + + let mut execution = empty_generic_runtime(); + execution.external_pdg_order = alias.external_pdgs.clone(); + execution.external_count = 5; + execution.physics = Some(Arc::new(alias_physics)); + let runtime = NativeRuntime { + root: PathBuf::new(), + artifact_id: "0".repeat(64), + runtime: execution, + execution_lane: NativeExecutionLane::Compiled, + process: alias.expression, + process_key: alias.id, + representative_process_id: "representative".to_string(), + external_permutation: alias.external_permutation, + input_crossing_map: Some(crossing_map), + permutation_alias_of: Some("representative".to_string()), + final_state_permutation_alias_of: Some("representative".to_string()), + physics_v1: native_runtime::LazyProcessPhysicsV1::loaded(alias_manifest), + warnings_muted: false, + warned_kinds: BTreeSet::new(), + pending_warnings: Vec::new(), + point_selector_scratch: PointSelectorExecutionScratch::default(), + selector_simd_lane_width: 1, + correlated: None, + process_group: None, + }; + let metadata = runtime.metadata(); + assert_eq!(metadata.external_pdg_order, vec![1, -1, 22, 23, 21]); + assert_eq!(metadata.external_permutation, vec![0, 1, 3, 4, 2]); + assert_eq!( + metadata.permutation_alias_of.as_deref(), + Some("representative") + ); + assert_eq!( + metadata.final_state_permutation_alias_of.as_deref(), + Some("representative") + ); + let exact_state: serde_json::Value = + serde_json::from_str(&runtime.exact_runtime_state_json().unwrap()).unwrap(); + assert_eq!(exact_state["representative_process_id"], "representative"); + assert_eq!(exact_state["representative_process_key"], "p0"); + assert_eq!( + exact_state["external_permutation"], + serde_json::json!([0, 1, 3, 4, 2]) + ); +} + +fn conjugated_member_selection(physics: &ProcessPhysicsV1) -> crate::ArtifactSelection { + let mut selection = stored_alias_selection(physics, &three_cycle_alias()); + selection.process.aliases.clear(); + selection.alias = None; + selection.requested_id = "s_sbar_member".to_string(); + selection.public_expression = "s~ s > a z g".to_string(); + selection.external_pdgs = vec![-3, 3, 22, 23, 21]; + selection.external_permutation = vec![1, 0, 3, 4, 2]; + selection.process_group = Some(crate::ProcessGroupRelation { + factor_numerator: 3, + factor_denominator: 2, + conjugated: true, + external_particles: ["s~", "s", "a", "z", "g"].map(str::to_string).to_vec(), + }); + selection +} + +#[test] +fn process_group_member_relabels_flavours_and_reverses_lc_words() { + let representative = alias_test_physics("lc"); + let selection = conjugated_member_selection(&representative); + let execution = apply_process_permutation_metadata(representative, &selection).unwrap(); + let public = apply_process_group_metadata(&execution, &selection).unwrap(); + + assert_eq!(public.process_id, "s_sbar_member"); + assert_eq!(public.process, "s~ s > a z g"); + assert_eq!( + public + .external_particles + .iter() + .map(|particle| (particle.particle.as_str(), particle.pdg)) + .collect::>(), + [("s~", -3), ("s", 3), ("a", 22), ("z", 23), ("g", 21)] + ); + // Execution keeps the representative flavours in the public order. + assert_eq!( + execution + .external_particles + .iter() + .map(|particle| particle.pdg) + .collect::>(), + [-1, 1, 22, 23, 21] + ); + assert_eq!(execution.helicities, public.helicities); + for (execution_color, public_color) in execution + .color_components + .iter() + .zip(&public.color_components) + { + let ( + PhysicsColorComponentV1::LcFlow(execution_flow), + PhysicsColorComponentV1::LcFlow(public_flow), + ) = (execution_color, public_color) + else { + panic!("expected LC flows"); + }; + let mut reversed = execution_flow.word.clone(); + reversed.reverse(); + assert_eq!(public_flow.word, reversed); + assert_eq!( + public_flow.id, + format!( + "flow:{}", + reversed + .iter() + .map(usize::to_string) + .collect::>() + .join(",") + ) + ); + assert_eq!( + charge_conjugate_color_id(&public_flow.id), + execution_flow.id + ); + assert_eq!( + charge_conjugate_color_id(&execution_flow.representative_id), + public_flow.representative_id + ); + } + assert_eq!( + public.reduction.groups[0].physical_color_ids, + execution.reduction.groups[0] + .physical_color_ids + .iter() + .map(|id| charge_conjugate_color_id(id).into_owned()) + .collect::>() + ); + assert_eq!(charge_conjugate_color_id("flow:singlet"), "flow:singlet"); + assert_eq!(charge_conjugate_color_id("flow:7"), "flow:7"); + assert_eq!( + charge_conjugate_color_id("color:contracted"), + "color:contracted" + ); + + let contracted = alias_test_physics("full"); + let selection = conjugated_member_selection(&contracted); + let execution = apply_process_permutation_metadata(contracted, &selection).unwrap(); + let public = apply_process_group_metadata(&execution, &selection).unwrap(); + assert_eq!(public.color_components, execution.color_components); + assert_eq!(public.reduction, execution.reduction); +} + +#[test] +fn process_group_runtime_scales_outputs_and_translates_color_ids() { + let representative = alias_test_physics("lc"); + let selection = conjugated_member_selection(&representative); + let execution_physics = apply_process_permutation_metadata(representative, &selection).unwrap(); + let public_physics = apply_process_group_metadata(&execution_physics, &selection).unwrap(); + let mut execution = empty_generic_runtime(); + execution.external_count = 5; + execution.physics = Some(Arc::new( + PhysicsRuntime::new(execution_physics.clone()).unwrap(), + )); + let mut runtime = NativeRuntime { + root: PathBuf::new(), + artifact_id: "0".repeat(64), + runtime: execution, + execution_lane: NativeExecutionLane::Compiled, + process: selection.public_expression.clone(), + process_key: selection.requested_id.clone(), + representative_process_id: "representative".to_string(), + external_permutation: selection.external_permutation.clone(), + input_crossing_map: None, + permutation_alias_of: Some("p0".to_string()), + final_state_permutation_alias_of: None, + process_group: None, + physics_v1: native_runtime::LazyProcessPhysicsV1::loaded(public_physics.clone()), + warnings_muted: false, + warned_kinds: BTreeSet::new(), + pending_warnings: Vec::new(), + point_selector_scratch: PointSelectorExecutionScratch::default(), + selector_simd_lane_width: 1, + correlated: None, + }; + + // Without a relation, values and IDs pass through unchanged. + let mut values = [2.0, 6.0]; + runtime.apply_process_group_factor(&mut values); + assert_eq!(values, [2.0, 6.0]); + let caller = ["flow:2,3,5,4,1".to_string()]; + assert_eq!( + runtime.execution_color_ids(Some(&caller)).as_deref(), + Some(&caller[..]) + ); + + runtime.process_group = Some(Box::new(ProcessGroupRouting { + representative_process_key: "p0".to_string(), + factor: NativeRationalFactor { + numerator: 3, + denominator: 2, + }, + conjugated: true, + })); + runtime.apply_process_group_factor(&mut values); + assert_eq!(values, [3.0, 9.0]); + let public_ids = runtime.color_ids().unwrap(); + let execution_ids = runtime + .execution_color_ids(Some(&public_ids)) + .expect("translated IDs") + .into_owned(); + assert_eq!( + execution_ids, + execution_physics + .color_components + .iter() + .map(|color| color.id().to_string()) + .collect::>() + ); + let mut round_trip = execution_ids.clone(); + runtime.publish_color_ids(&mut round_trip); + assert_eq!(round_trip, public_ids); + + let metadata = runtime.metadata(); + assert_eq!(metadata.process_group_member_of.as_deref(), Some("p0")); + assert_eq!( + metadata.process_group_factor, + NativeRationalFactor { + numerator: 3, + denominator: 2 + } + ); + assert!(metadata.charge_conjugated); + let exact_state: serde_json::Value = + serde_json::from_str(&runtime.exact_runtime_state_json().unwrap()).unwrap(); + assert_eq!( + exact_state["process_group_factor"], + serde_json::json!({"numerator": 3, "denominator": 2}) + ); + assert_eq!(exact_state["charge_conjugated"], true); + assert_eq!(exact_state["process_group_member_of"], "p0"); +} + +#[test] +fn incoming_and_outgoing_process_permutation_remaps_all_resolved_metadata() { + let representative = alias_test_physics("lc"); + let alias = crate::ProcessAlias { + id: "both-sides".to_string(), + expression: "d~ d > a z g".to_string(), + external_pdgs: vec![-1, 1, 22, 23, 21], + external_permutation: vec![1, 0, 3, 4, 2], + }; + let selection = stored_alias_selection(&representative, &alias); + let public = apply_process_permutation_metadata(representative, &selection).unwrap(); + + assert_eq!(public.process_id, "both-sides"); + assert_eq!(public.process, "d~ d > a z g"); + assert_eq!( + public + .external_particles + .iter() + .map(|particle| (particle.index, particle.pdg, particle.role)) + .collect::>(), + vec![ + (0, -1, crate::ParticleRole::Initial), + (1, 1, crate::ParticleRole::Initial), + (2, 22, crate::ParticleRole::Final), + (3, 23, crate::ParticleRole::Final), + (4, 21, crate::ParticleRole::Final), + ] + ); + assert_eq!(public.helicities[0].values, [-1, 1, -1, 0, 1]); + assert_eq!(public.helicities[0].id, "h:-1,+1,-1,+0,+1"); + let PhysicsColorComponentV1::LcFlow(flow) = &public.color_components[0] else { + panic!("expected LC flow"); + }; + assert_eq!(flow.word, [2, 4, 5, 3, 1]); + assert_eq!(flow.id, "flow:2,4,5,3,1"); + assert_eq!( + public.reduction.groups[0].representative_helicity_id, + "h:-1,+1,-1,+0,+1" + ); + assert_eq!( + public.reduction.groups[0].representative_color_id, + "flow:2,4,5,3,1" + ); +} + +#[test] +fn final_state_alias_three_cycle_preserves_contracted_color_reduction() { + let representative_manifest = alias_test_physics("full"); + representative_manifest.validate().unwrap(); + let representative_physics = PhysicsRuntime::new(representative_manifest.clone()).unwrap(); + let alias = three_cycle_alias(); + let alias_manifest = apply_process_permutation_metadata( + representative_manifest.clone(), + &stored_alias_selection(&representative_manifest, &alias), + ) + .unwrap(); + let alias_physics = PhysicsRuntime::new(alias_manifest.clone()).unwrap(); + + assert_eq!(alias_manifest.color_components[0].id(), "color:contracted"); + assert_eq!( + alias_manifest.reduction.groups[0].representative_color_id, + "color:contracted" + ); + assert_eq!( + alias_physics + .selected_helicity_indices(Some(&BTreeSet::from(["h:-1,+1,+1,+0,-1".to_string(),]))) + .unwrap(), + vec![1] + ); + assert_eq!( + alias_physics + .selected_color_indices(Some(&BTreeSet::from(["color:contracted".to_string()]))) + .unwrap(), + vec![0] + ); + + let contraction = || { + let groups = [RawSumGroup { + id: 7, + indices: vec![0], + weight: 1.0, + all_sector_weight: 1.0, + sector_ids: vec![0], + }]; + ColorContractionRuntime::new( + &groups, + vec![ColorContractionEntry { + left_group_index: 0, + right_group_index: 0, + weight_re: 2.0, + weight_im: 0.0, + symmetry_factor: 1.0, + }], + ) + }; + let representative_total = test_amplitude_runtime(vec![c64(3.0, 0.0)], Some(contraction())) + .reduce_scratch_f64_resolved(1, &representative_physics, 2.0, None, None) + .unwrap() + .values + .iter() + .sum::(); + let alias_total = test_amplitude_runtime(vec![c64(3.0, 0.0)], Some(contraction())) + .reduce_scratch_f64_resolved(1, &alias_physics, 2.0, None, None) + .unwrap() + .values + .iter() + .sum::(); + assert_eq!(alias_total, representative_total); +} + +fn empty_evaluator_group() -> EvaluatorGroup { + EvaluatorGroup { + evaluators: Vec::new(), + input_len: 0, + input_mappings: Vec::new(), + input_mapping_spans: Vec::new(), + output_len: 0, + chunk_parameter_scratch_f64: Vec::new(), + chunk_scratch_f64: Vec::new(), + chunk_parameter_scratch_aosoa_f64: Vec::new(), + chunk_scratch_aosoa_f64: Vec::new(), + chunk_input_mapping_scratch: Vec::new(), + } +} + +fn test_amplitude_runtime( + outputs: Vec>, + color_contraction: Option, +) -> AmplitudeRuntime { + let output_length = outputs.len(); + AmplitudeRuntime { + output_length, + raw_sum_weights: vec![1.0; output_length], + raw_sum_all_sector_weights: vec![1.0; output_length], + raw_sum_color_sector_ids: vec![None; output_length], + raw_sum_groups: vec![RawSumGroup { + id: 7, + indices: (0..output_length).collect(), + weight: 1.0, + all_sector_weight: 1.0, + sector_ids: vec![0], + }], + has_coherent_groups: true, + color_contraction, + color_topology_replay: None, + input_components: None, + input_spans: Vec::new(), + parameter_scratch_f64: Vec::new(), + evaluator_output_scratch_f64: Vec::new(), + output_scratch_f64: outputs, + resolved_source_row_scratch_f64: Vec::new(), + resolved_target_row_scratch_f64: Vec::new(), + routed_reduction_scratch: RoutedReductionScratch::default(), + materialized_helicity_direct_total_plans: Vec::new(), + materialized_helicity_direct_total_plan_capacity: 0, + materialized_helicity_direct_total_next_replacement: 0, + materialized_helicity_direct_default_plan_by_selector_domain: Vec::new(), + evaluator_output_order: None, + evaluator: Some(empty_evaluator_group()), + } +} + +fn plane_native_totals( + amplitude: &mut AmplitudeRuntime, + outputs: &[Complex], + batch_size: usize, + selected_color_sector_ids: Option<&BTreeSet>, +) -> Vec { + assert_eq!(outputs.len(), batch_size * amplitude.output_length); + let mut workspace = crate::direct_arena::DirectArenaWorkspace::new( + 0, + amplitude.output_length as u32, + batch_size as u32, + ) + .unwrap(); + workspace.begin_tile(batch_size as u32).unwrap(); + let stride = workspace.point_stride() as usize; + { + let (_, _, values_re, values_im) = workspace.split_slices_mut(); + values_re.fill(73.0); + values_im.fill(-91.0); + for point in 0..batch_size { + for component in 0..amplitude.output_length { + let value = outputs[point * amplitude.output_length + component]; + values_re[component * stride + point] = value.re; + values_im[component * stride + point] = value.im; + } + } + } + let mut totals = vec![f64::from_bits(0x7ff8_0000_0000_0042); batch_size]; + { + let (values_re, values_im) = workspace.amplitude_slices(); + let planes = crate::direct_arena::DirectAmplitudePlanes::new( + values_re, + values_im, + stride as u32, + batch_size as u32, + ) + .unwrap(); + amplitude + .reduce_planes_f64_into_selected_slice(planes, &mut totals, selected_color_sector_ids) + .unwrap(); + } + let (values_re, values_im) = workspace.amplitude_slices(); + for component in 0..amplitude.output_length { + assert!( + values_re[component * stride + batch_size..(component + 1) * stride] + .iter() + .all(|value| *value == 73.0) + ); + assert!( + values_im[component * stride + batch_size..(component + 1) * stride] + .iter() + .all(|value| *value == -91.0) + ); + } + totals +} + +fn with_plane_native_amplitudes( + outputs: &[Complex], + batch_size: usize, + output_length: usize, + reduce: impl FnOnce(crate::direct_arena::DirectAmplitudePlanes<'_>) -> T, +) -> T { + assert_eq!(outputs.len(), batch_size * output_length); + let mut workspace = + crate::direct_arena::DirectArenaWorkspace::new(0, output_length as u32, batch_size as u32) + .unwrap(); + workspace.begin_tile(batch_size as u32).unwrap(); + let stride = workspace.point_stride() as usize; + const PAD_RE: f64 = 73.0; + const PAD_IM: f64 = -91.0; + { + let (_, _, values_re, values_im) = workspace.split_slices_mut(); + values_re.fill(PAD_RE); + values_im.fill(PAD_IM); + for point in 0..batch_size { + for component in 0..output_length { + let value = outputs[point * output_length + component]; + values_re[component * stride + point] = value.re; + values_im[component * stride + point] = value.im; + } + } + } + let reduced = { + let (values_re, values_im) = workspace.amplitude_slices(); + let planes = crate::direct_arena::DirectAmplitudePlanes::new( + values_re, + values_im, + stride as u32, + batch_size as u32, + ) + .unwrap(); + reduce(planes) + }; + let (values_re, values_im) = workspace.amplitude_slices(); + for component in 0..output_length { + assert!( + values_re[component * stride + batch_size..(component + 1) * stride] + .iter() + .all(|value| *value == PAD_RE) + ); + assert!( + values_im[component * stride + batch_size..(component + 1) * stride] + .iter() + .all(|value| *value == PAD_IM) + ); + } + reduced +} + +fn assert_resolved_values_equal(left: &ResolvedValues, right: &ResolvedValues) { + assert_eq!(left.point_count, right.point_count); + assert_eq!(left.helicity_indices, right.helicity_indices); + assert_eq!(left.color_indices, right.color_indices); + assert_eq!(left.values, right.values); +} + +#[test] +fn plane_native_lc_totals_match_selected_row_major_odd_tail_and_structural_zero() { + const POINT_COUNT: usize = 129; + const OUTPUT_COUNT: usize = 5; + let outputs = (0..POINT_COUNT) + .flat_map(|point| { + (0..OUTPUT_COUNT).map(move |output| { + if output == 3 { + c64(0.0, 0.0) + } else { + c64( + (point * 7 + output * 3) as f64 * 0.03125 - 2.0, + (point * 5 + output * 11) as f64 * -0.015625 + 1.0, + ) + } + }) + }) + .collect::>(); + let mut amplitude = test_amplitude_runtime(outputs.clone(), None); + amplitude.output_length = OUTPUT_COUNT; + amplitude.has_coherent_groups = false; + amplitude.raw_sum_groups.clear(); + amplitude.raw_sum_weights = vec![1.0, 0.5, 2.0, 17.0, -0.25]; + amplitude.raw_sum_all_sector_weights = vec![1.25, 0.75, 3.0, 19.0, -0.5]; + amplitude.raw_sum_color_sector_ids = vec![Some(10), Some(20), Some(10), Some(30), Some(20)]; + + for selected in [None, Some(BTreeSet::from([20_i64]))] { + let mut row_major = vec![f64::NAN; POINT_COUNT]; + amplitude + .reduce_scratch_f64_into_selected_slice(POINT_COUNT, &mut row_major, selected.as_ref()) + .unwrap(); + let plane_native = + plane_native_totals(&mut amplitude, &outputs, POINT_COUNT, selected.as_ref()); + assert_eq!(plane_native, row_major); + } +} + +#[test] +fn row_major_reduction_dimensions_fail_closed_before_indexing() { + let mut amplitude = test_amplitude_runtime(vec![c64(1.0, 0.0), c64(2.0, 0.0)], None); + let error = amplitude + .reduce_scratch_f64_into_selected_slice(usize::MAX, &mut [], None) + .unwrap_err(); + assert_eq!(error.kind(), crate::RusticolErrorKind::InvalidArgument); + assert!(error.to_string().contains("dimensions overflow")); +} + +#[cfg(not(feature = "f64-symjit"))] +#[test] +fn warmed_plane_native_reduction_allocates_zero() { + const POINT_COUNT: usize = 129; + const OUTPUT_COUNT: usize = 5; + let outputs = (0..POINT_COUNT * OUTPUT_COUNT) + .map(|index| c64(index as f64 * 0.03125 - 3.0, index as f64 * -0.015625 + 2.0)) + .collect::>(); + let mut amplitude = test_amplitude_runtime(outputs.clone(), None); + amplitude.output_length = OUTPUT_COUNT; + amplitude.has_coherent_groups = false; + amplitude.raw_sum_groups.clear(); + amplitude.raw_sum_weights = vec![1.0; OUTPUT_COUNT]; + amplitude.raw_sum_all_sector_weights = vec![1.0; OUTPUT_COUNT]; + amplitude.raw_sum_color_sector_ids = vec![None; OUTPUT_COUNT]; + + let mut workspace = + crate::direct_arena::DirectArenaWorkspace::new(0, OUTPUT_COUNT as u32, POINT_COUNT as u32) + .unwrap(); + workspace.begin_tile(POINT_COUNT as u32).unwrap(); + let stride = workspace.point_stride() as usize; + { + let (_, _, values_re, values_im) = workspace.split_slices_mut(); + for point in 0..POINT_COUNT { + for component in 0..OUTPUT_COUNT { + let value = outputs[point * OUTPUT_COUNT + component]; + values_re[component * stride + point] = value.re; + values_im[component * stride + point] = value.im; + } + } + } + let mut totals = vec![0.0; POINT_COUNT]; + let (values_re, values_im) = workspace.amplitude_slices(); + let planes = crate::direct_arena::DirectAmplitudePlanes::new( + values_re, + values_im, + stride as u32, + POINT_COUNT as u32, + ) + .unwrap(); + + amplitude + .reduce_planes_f64_into_selected_slice(planes, &mut totals, None) + .unwrap(); + let (result, allocation_count, allocated_bytes) = + super::evaluator::native_direct::tests::count_allocations(|| { + amplitude.reduce_planes_f64_into_selected_slice(planes, &mut totals, None) + }); + result.unwrap(); + assert_eq!(allocation_count, 0, "warmed plane reduction allocated"); + assert_eq!(allocated_bytes, 0, "warmed plane reduction allocated bytes"); +} + +#[cfg(not(feature = "f64-symjit"))] +#[test] +fn warmed_plane_native_repeated_contracted_reduction_allocates_zero() { + const POINT_COUNT: usize = 129; + const COMPONENT_COUNT: usize = 8; + const OUTPUT_COUNT: usize = COMPONENT_COUNT * 2; + let groups = (0..OUTPUT_COUNT) + .map(|output_index| { + repeated_test_group( + 10 + output_index as i64, + output_index, + if output_index < COMPONENT_COUNT { + 100 + } else { + 200 + }, + ) + }) + .collect::>(); + let entries = (0..COMPONENT_COUNT) + .map(|component| ColorContractionEntry { + left_group_index: component, + right_group_index: COMPONENT_COUNT + component, + weight_re: 0.75, + weight_im: 0.0, + symmetry_factor: 2.0, + }) + .collect::>(); + let outputs = (0..POINT_COUNT * OUTPUT_COUNT) + .map(|index| { + c64( + (index * 13 % 101) as f64 * 0.03125 - 1.0, + (index * 19 % 103) as f64 * -0.015625 + 0.5, + ) + }) + .collect::>(); + let mut amplitude = reduction_test_amplitude(OUTPUT_COUNT, outputs.clone(), groups, entries); + let repeated = amplitude + .color_contraction + .as_ref() + .and_then(|contraction| contraction.repeated_block.as_ref()) + .unwrap(); + assert_eq!(repeated.component_count, COMPONENT_COUNT); + assert!(repeated.all_weights_real); + + let mut workspace = + crate::direct_arena::DirectArenaWorkspace::new(0, OUTPUT_COUNT as u32, POINT_COUNT as u32) + .unwrap(); + workspace.begin_tile(POINT_COUNT as u32).unwrap(); + let stride = workspace.point_stride() as usize; + { + let (_, _, values_re, values_im) = workspace.split_slices_mut(); + for point in 0..POINT_COUNT { + for component in 0..OUTPUT_COUNT { + let value = outputs[point * OUTPUT_COUNT + component]; + values_re[component * stride + point] = value.re; + values_im[component * stride + point] = value.im; + } + } + } + let (values_re, values_im) = workspace.amplitude_slices(); + let planes = crate::direct_arena::DirectAmplitudePlanes::new( + values_re, + values_im, + stride as u32, + POINT_COUNT as u32, + ) + .unwrap(); + let mut totals = vec![0.0; POINT_COUNT]; + amplitude + .reduce_planes_f64_into_selected_slice(planes, &mut totals, None) + .unwrap(); + let (result, allocation_count, allocated_bytes) = + super::evaluator::native_direct::tests::count_allocations(|| { + amplitude.reduce_planes_f64_into_selected_slice(planes, &mut totals, None) + }); + result.unwrap(); + assert_eq!( + allocation_count, 0, + "warmed repeated NLC/full contraction allocated" + ); + assert_eq!( + allocated_bytes, 0, + "warmed repeated NLC/full contraction allocated bytes" + ); +} + +#[test] +fn resolved_lc_reduction_expands_symmetries_and_structural_zeros() { + let physics = test_physics_runtime("lc"); + let mut amplitude = test_amplitude_runtime(vec![c64(2.0, 0.0)], None); + + let resolved = amplitude + .reduce_scratch_f64_resolved(1, &physics, 4.0, None, None) + .unwrap(); + + assert_eq!(resolved.helicity_indices, vec![0, 1, 2]); + assert_eq!(resolved.color_indices, vec![0, 1]); + assert_eq!(resolved.values, vec![4.0, 4.0, 4.0, 4.0, 0.0, 0.0]); + assert_eq!(resolved.values.iter().sum::(), 16.0); + + let helicities = BTreeSet::from(["hel:-+".to_string()]); + let colors = BTreeSet::from(["flow:1".to_string()]); + let selected = amplitude + .reduce_scratch_f64_resolved(1, &physics, 4.0, Some(&helicities), Some(&colors)) + .unwrap(); + assert_eq!(selected.values, vec![4.0]); +} + +#[test] +fn plane_native_lc_resolved_matches_full_and_selected_row_major_odd_tail() { + const POINT_COUNT: usize = 129; + let physics = test_physics_runtime("lc"); + let outputs = (0..POINT_COUNT) + .map(|point| c64(point as f64 * 0.03125 - 2.0, point as f64 * -0.015625 + 1.0)) + .collect::>(); + let mut amplitude = test_amplitude_runtime(outputs.clone(), None); + amplitude.output_length = 1; + amplitude.raw_sum_weights = vec![1.0]; + amplitude.raw_sum_all_sector_weights = vec![1.0]; + amplitude.raw_sum_color_sector_ids = vec![None]; + amplitude.raw_sum_groups[0].indices = vec![0]; + let selected_helicities = BTreeSet::from(["hel:-+".to_string()]); + let selected_colors = BTreeSet::from(["flow:1".to_string()]); + + for (helicities, colors) in [ + (None, None), + (Some(&selected_helicities), Some(&selected_colors)), + ] { + let row_major = amplitude + .reduce_scratch_f64_resolved(POINT_COUNT, &physics, 1.75, helicities, colors) + .unwrap(); + let plane_native = with_plane_native_amplitudes(&outputs, POINT_COUNT, 1, |planes| { + amplitude + .reduce_planes_f64_resolved(planes, &physics, 1.75, helicities, colors) + .unwrap() + }); + assert_resolved_values_equal(&plane_native, &row_major); + } +} + +#[test] +fn plane_native_lc_resolved_routes_preserve_row_major_order() { + let physics = test_physics_runtime("lc"); + let outputs = vec![c64(2.0, -0.5)]; + let mut amplitude = test_amplitude_runtime(outputs.clone(), None); + let replay = LcResolvedReplayEntry { + routes: vec![ + LcResolvedReplayRoute { + source_index: 1, + target_index: 0, + weight: 0.5, + }, + LcResolvedReplayRoute { + source_index: 0, + target_index: 2, + weight: 2.0, + }, + LcResolvedReplayRoute { + source_index: 2, + target_index: 1, + weight: -0.25, + }, + LcResolvedReplayRoute { + source_index: 3, + target_index: 2, + weight: 1.5, + }, + ], + }; + let mut row_major = [f64::NAN]; + amplitude + .reduce_scratch_f64_routed_totals_into( + 1, + &physics, + 4.0, + None, + None, + &replay, + 6, + 3, + &mut row_major, + ) + .unwrap(); + + let mut workspace = crate::direct_arena::DirectArenaWorkspace::new(0, 1, 1).unwrap(); + workspace.begin_tile(1).unwrap(); + let stride = workspace.point_stride(); + { + let (_, _, values_re, values_im) = workspace.split_slices_mut(); + values_re[0] = outputs[0].re; + values_im[0] = outputs[0].im; + } + let (values_re, values_im) = workspace.amplitude_slices(); + let planes = + crate::direct_arena::DirectAmplitudePlanes::new(values_re, values_im, stride, 1).unwrap(); + let mut plane_native = [f64::NAN]; + amplitude + .reduce_planes_f64_routed_totals_into( + planes, + &physics, + 4.0, + None, + None, + &replay, + 6, + 3, + &mut plane_native, + ) + .unwrap(); + assert_eq!(plane_native, row_major); +} + +#[test] +fn plane_native_lc_routed_components_match_resolved_multi_mapping_accumulation() { + const POINT_COUNT: usize = 65; + const TARGET_COMPONENT_COUNT: usize = 3; + let physics = test_physics_runtime("lc"); + let mapping_outputs = [ + (0..POINT_COUNT) + .map(|point| c64(point as f64 * 0.03125 - 1.0, point as f64 * -0.015625 + 0.5)) + .collect::>(), + (0..POINT_COUNT) + .map(|point| { + c64( + point as f64 * -0.0625 + 2.0, + point as f64 * 0.0078125 - 0.25, + ) + }) + .collect::>(), + ]; + let replay_entries = [ + LcResolvedReplayEntry { + routes: vec![ + LcResolvedReplayRoute { + source_index: 0, + target_index: 2, + weight: 1.25, + }, + LcResolvedReplayRoute { + source_index: 3, + target_index: 0, + weight: -0.5, + }, + ], + }, + LcResolvedReplayEntry { + routes: vec![ + LcResolvedReplayRoute { + source_index: 1, + target_index: 1, + weight: 0.75, + }, + LcResolvedReplayRoute { + source_index: 4, + target_index: 2, + weight: 2.0, + }, + ], + }, + ]; + let mut amplitude = test_amplitude_runtime(mapping_outputs[0].clone(), None); + amplitude.output_length = 1; + amplitude.raw_sum_weights = vec![1.0]; + amplitude.raw_sum_all_sector_weights = vec![1.0]; + amplitude.raw_sum_color_sector_ids = vec![None]; + amplitude.raw_sum_groups[0].indices = vec![0]; + let mut expected = vec![0.0; POINT_COUNT * TARGET_COMPONENT_COUNT]; + let mut candidate = vec![0.0; POINT_COUNT * TARGET_COMPONENT_COUNT]; + + for (outputs, replay_entry) in mapping_outputs.iter().zip(&replay_entries) { + let materialized = with_plane_native_amplitudes(outputs, POINT_COUNT, 1, |planes| { + amplitude + .reduce_planes_f64_resolved(planes, &physics, 1.75, None, None) + .unwrap() + }); + accumulate_selected_lc_replay_resolved_f64( + &mut expected, + POINT_COUNT, + &materialized, + std::slice::from_ref(replay_entry), + 6, + TARGET_COMPONENT_COUNT, + ) + .unwrap(); + for (start, stop) in [(0usize, 32usize), (32, POINT_COUNT)] { + with_plane_native_amplitudes(&outputs[start..stop], stop - start, 1, |planes| { + amplitude + .reduce_planes_f64_routed_components_add_into( + planes, + &physics, + 1.75, + None, + None, + replay_entry, + 6, + TARGET_COMPONENT_COUNT, + POINT_COUNT, + start, + &mut candidate, + ) + .unwrap() + }); + } + } + assert_eq!(candidate, expected); + let candidate_totals = candidate + .chunks_exact(TARGET_COMPONENT_COUNT) + .map(|components| components.iter().sum::()) + .collect::>(); + let expected_totals = expected + .chunks_exact(TARGET_COMPONENT_COUNT) + .map(|components| components.iter().sum::()) + .collect::>(); + assert_eq!(candidate_totals, expected_totals); +} + +#[cfg(not(feature = "f64-symjit"))] +#[test] +fn warmed_plane_native_selected_lc_routes_allocate_zero() { + let physics = test_physics_runtime("lc"); + let outputs = vec![c64(2.0, -0.5)]; + let mut amplitude = test_amplitude_runtime(outputs.clone(), None); + let replay = LcResolvedReplayEntry { + routes: vec![LcResolvedReplayRoute { + source_index: 0, + target_index: 0, + weight: 1.25, + }], + }; + let helicities = BTreeSet::from(["hel:-+".to_string()]); + let colors = BTreeSet::from(["flow:1".to_string()]); + let mut workspace = crate::direct_arena::DirectArenaWorkspace::new(0, 1, 1).unwrap(); + workspace.begin_tile(1).unwrap(); + let stride = workspace.point_stride(); + { + let (_, _, values_re, values_im) = workspace.split_slices_mut(); + values_re[0] = outputs[0].re; + values_im[0] = outputs[0].im; + } + let (values_re, values_im) = workspace.amplitude_slices(); + let planes = + crate::direct_arena::DirectAmplitudePlanes::new(values_re, values_im, stride, 1).unwrap(); + let mut totals = [0.0]; + amplitude + .reduce_planes_f64_routed_totals_into( + planes, + &physics, + 4.0, + Some(&helicities), + Some(&colors), + &replay, + 1, + 1, + &mut totals, + ) + .unwrap(); + let (result, allocation_count, allocated_bytes) = + super::evaluator::native_direct::tests::count_allocations(|| { + amplitude.reduce_planes_f64_routed_totals_into( + planes, + &physics, + 4.0, + Some(&helicities), + Some(&colors), + &replay, + 1, + 1, + &mut totals, + ) + }); + result.unwrap(); + assert_eq!( + allocation_count, 0, + "warmed selected LC routed reduction allocated" + ); + assert_eq!( + allocated_bytes, 0, + "warmed selected LC routed reduction allocated bytes" + ); + + let mut target_components = [0.0]; + amplitude + .reduce_planes_f64_routed_components_add_into( + planes, + &physics, + 4.0, + Some(&helicities), + Some(&colors), + &replay, + 1, + 1, + 1, + 0, + &mut target_components, + ) + .unwrap(); + target_components.fill(0.0); + let (result, allocation_count, allocated_bytes) = + super::evaluator::native_direct::tests::count_allocations(|| { + amplitude.reduce_planes_f64_routed_components_add_into( + planes, + &physics, + 4.0, + Some(&helicities), + Some(&colors), + &replay, + 1, + 1, + 1, + 0, + &mut target_components, + ) + }); + result.unwrap(); + assert_eq!( + allocation_count, 0, + "warmed persistent selected LC routed reduction allocated" + ); + assert_eq!( + allocated_bytes, 0, + "warmed persistent selected LC routed reduction allocated bytes" + ); +} + +#[test] +fn contracted_reductions_have_one_color_component_and_sum_to_total() { + for color_accuracy in ["nlc", "full"] { + let physics = test_physics_runtime(color_accuracy); + let groups = [RawSumGroup { + id: 7, + indices: vec![0], + weight: 1.0, + all_sector_weight: 1.0, + sector_ids: vec![0], + }]; + let contraction = ColorContractionRuntime::new( + &groups, + vec![ColorContractionEntry { + left_group_index: 0, + right_group_index: 0, + weight_re: 2.0, + weight_im: 0.0, + symmetry_factor: 1.0, + }], + ); + let mut amplitude = test_amplitude_runtime(vec![c64(3.0, 0.0)], Some(contraction)); + + let resolved = amplitude + .reduce_scratch_f64_resolved(1, &physics, 2.0, None, None) + .unwrap(); + + assert_eq!(resolved.helicity_indices, vec![0, 1]); + assert_eq!(resolved.color_indices, vec![0]); + assert_eq!(resolved.values, vec![18.0, 18.0]); + assert_eq!(resolved.values.iter().sum::(), 36.0); + } +} + +#[test] +fn plane_native_nlc_and_full_resolved_match_row_major_odd_tail() { + const POINT_COUNT: usize = 129; + let outputs = (0..POINT_COUNT) + .map(|point| c64(point as f64 * 0.0625 - 3.0, point as f64 * -0.03125 + 0.75)) + .collect::>(); + let selected_helicities = BTreeSet::from(["hel:-+".to_string()]); + let root_factors = [Some(c64(0.75, -0.25))]; + + for color_accuracy in ["nlc", "full"] { + let physics = test_physics_runtime(color_accuracy); + let groups = [RawSumGroup { + id: 7, + indices: vec![0], + weight: 1.0, + all_sector_weight: 1.0, + sector_ids: vec![0], + }]; + let contraction = || { + ColorContractionRuntime::new( + &groups, + vec![ColorContractionEntry { + left_group_index: 0, + right_group_index: 0, + weight_re: 2.0, + weight_im: 0.0, + symmetry_factor: 1.0, + }], + ) + }; + let mut amplitude = test_amplitude_runtime(outputs.clone(), Some(contraction())); + amplitude.output_length = 1; + amplitude.raw_sum_weights = vec![1.0]; + amplitude.raw_sum_all_sector_weights = vec![1.0]; + amplitude.raw_sum_color_sector_ids = vec![None]; + amplitude.raw_sum_groups[0].indices = vec![0]; + let row_major = amplitude + .reduce_scratch_f64_resolved( + POINT_COUNT, + &physics, + 2.0, + Some(&selected_helicities), + None, + ) + .unwrap(); + let plane_native = with_plane_native_amplitudes(&outputs, POINT_COUNT, 1, |planes| { + amplitude + .reduce_planes_f64_resolved(planes, &physics, 2.0, Some(&selected_helicities), None) + .unwrap() + }); + assert_resolved_values_equal(&plane_native, &row_major); + + let row_major_materialized = amplitude + .reduce_scratch_f64_for_materialized_helicity( + POINT_COUNT, + &physics, + 2.0, + 0, + &root_factors, + None, + ) + .unwrap(); + let plane_native_materialized = + with_plane_native_amplitudes(&outputs, POINT_COUNT, 1, |planes| { + amplitude + .reduce_planes_f64_for_materialized_helicity( + planes, + &physics, + 2.0, + 0, + &root_factors, + None, + ) + .unwrap() + }); + assert_resolved_values_equal(&plane_native_materialized, &row_major_materialized); + + let initial = (0..POINT_COUNT) + .map(|point| point as f64 * 0.125 - 4.0) + .collect::>(); + let mut row_major_totals = initial.clone(); + amplitude + .reduce_scratch_f64_for_materialized_helicity_add_into( + POINT_COUNT, + &physics, + 2.0, + 0, + &root_factors, + None, + &mut row_major_totals, + ) + .unwrap(); + let mut plane_native_totals = initial; + with_plane_native_amplitudes(&outputs, POINT_COUNT, 1, |planes| { + amplitude + .reduce_planes_f64_for_materialized_helicity_add_into( + planes, + &physics, + 2.0, + 0, + &root_factors, + None, + &mut plane_native_totals, + ) + .unwrap() + }); + assert_eq!(plane_native_totals, row_major_totals); + assert_eq!(amplitude.materialized_helicity_direct_total_plans.len(), 1); + + let identity_root_factors = [Some(c64(1.0, 0.0))]; + let mut identity_row_major = vec![0.25; POINT_COUNT]; + amplitude + .reduce_scratch_f64_for_materialized_helicity_add_into( + POINT_COUNT, + &physics, + 2.0, + 0, + &identity_root_factors, + None, + &mut identity_row_major, + ) + .unwrap(); + let mut identity_plane_native = vec![0.25; POINT_COUNT]; + with_plane_native_amplitudes(&outputs, POINT_COUNT, 1, |planes| { + amplitude + .reduce_planes_f64_for_materialized_helicity_add_into( + planes, + &physics, + 2.0, + 0, + &identity_root_factors, + None, + &mut identity_plane_native, + ) + .unwrap() + }); + assert_eq!( + identity_plane_native + .iter() + .map(|value| value.to_bits()) + .collect::>(), + identity_row_major + .iter() + .map(|value| value.to_bits()) + .collect::>() + ); + assert_eq!( + amplitude.materialized_helicity_direct_total_plans[1].groups[0].identity_output_index, + Some(0) + ); + } +} + +#[test] +fn materialized_contracted_plan_keeps_only_selected_helicity_entries_and_caches_domain() { + let mut physics_manifest = test_physics_runtime("full").manifest.clone(); + physics_manifest.reduction.groups = vec![ + crate::ReductionGroup { + id: "group:7".to_string(), + representative_helicity_id: "hel:+-".to_string(), + physical_helicity_ids: vec!["hel:+-".to_string()], + representative_color_id: "contracted".to_string(), + physical_color_ids: vec!["contracted".to_string()], + }, + crate::ReductionGroup { + id: "group:8".to_string(), + representative_helicity_id: "hel:-+".to_string(), + physical_helicity_ids: vec!["hel:-+".to_string()], + representative_color_id: "contracted".to_string(), + physical_color_ids: vec!["contracted".to_string()], + }, + ]; + let physics = PhysicsRuntime::new(physics_manifest).unwrap(); + let groups = vec![ + RawSumGroup { + id: 7, + indices: vec![0], + weight: 1.0, + all_sector_weight: 1.0, + sector_ids: vec![0], + }, + RawSumGroup { + id: 8, + indices: vec![1], + weight: 1.0, + all_sector_weight: 1.0, + sector_ids: vec![0], + }, + ]; + let contraction = ColorContractionRuntime::new( + &groups, + vec![ + ColorContractionEntry { + left_group_index: 0, + right_group_index: 0, + weight_re: 2.0, + weight_im: 0.0, + symmetry_factor: 1.0, + }, + ColorContractionEntry { + left_group_index: 1, + right_group_index: 1, + weight_re: 2.0, + weight_im: 0.0, + symmetry_factor: 1.0, + }, + ], + ); + let mut amplitude = test_amplitude_runtime( + vec![c64(3.0, 0.0), c64(f64::NAN, f64::NAN)], + Some(contraction), + ); + amplitude.output_length = 2; + amplitude.raw_sum_weights = vec![1.0; 2]; + amplitude.raw_sum_all_sector_weights = vec![1.0; 2]; + amplitude.raw_sum_color_sector_ids = vec![None; 2]; + amplitude.raw_sum_groups = groups; + let root_factors = [Some(c64(1.0, 0.0)), None]; + + let plan_index = amplitude + .bind_default_materialized_helicity_direct_total_plan(4, &physics, 0, &root_factors, None) + .unwrap(); + let MaterializedHelicityDirectTotalReduction::Contracted { entries } = + &litude.materialized_helicity_direct_total_plans[plan_index].reduction + else { + panic!("FullColour materialized plan must use contracted reduction"); + }; + assert_eq!(entries.len(), 1); + assert_eq!(entries[0].left_group_index, 0); + assert_eq!(entries[0].right_group_index, 0); + + assert_eq!( + amplitude + .bind_default_materialized_helicity_direct_total_plan( + 4, + &physics, + 0, + &root_factors, + None, + ) + .unwrap(), + plan_index + ); + assert_eq!(amplitude.materialized_helicity_direct_total_plans.len(), 1); + assert_eq!( + amplitude.materialized_helicity_direct_default_plan_by_selector_domain[4] + .expect("selector-domain plan cache") + .plan_index, + plan_index + ); +} + +#[test] +fn plane_native_materialized_helicity_resolved_and_add_into_match_lc_odd_tail() { + const POINT_COUNT: usize = 129; + const OUTPUT_COUNT: usize = 2; + let physics = test_physics_runtime("lc"); + let outputs = (0..POINT_COUNT) + .flat_map(|point| { + (0..OUTPUT_COUNT).map(move |output| { + c64( + (point * 7 + output * 3) as f64 * 0.03125 - 1.5, + (point * 5 + output * 11) as f64 * -0.015625 + 0.25, + ) + }) + }) + .collect::>(); + let root_factors = [Some(c64(0.75, -0.25)), Some(c64(-0.5, 0.125))]; + let mut amplitude = test_amplitude_runtime(outputs.clone(), None); + amplitude.output_length = OUTPUT_COUNT; + amplitude.raw_sum_weights = vec![1.0; OUTPUT_COUNT]; + amplitude.raw_sum_all_sector_weights = vec![1.0; OUTPUT_COUNT]; + amplitude.raw_sum_color_sector_ids = vec![None; OUTPUT_COUNT]; + amplitude.raw_sum_groups[0].indices = vec![0, 1]; + let selected_colors = BTreeSet::from(["flow:1".to_string()]); + + for colors in [None, Some(&selected_colors)] { + let row_major = amplitude + .reduce_scratch_f64_for_materialized_helicity( + POINT_COUNT, + &physics, + 1.25, + 0, + &root_factors, + colors, + ) + .unwrap(); + let plane_native = + with_plane_native_amplitudes(&outputs, POINT_COUNT, OUTPUT_COUNT, |planes| { + amplitude + .reduce_planes_f64_for_materialized_helicity( + planes, + &physics, + 1.25, + 0, + &root_factors, + colors, + ) + .unwrap() + }); + assert_resolved_values_equal(&plane_native, &row_major); + + let initial = (0..POINT_COUNT) + .map(|point| point as f64 * 0.125 - 4.0) + .collect::>(); + let mut row_major_totals = initial.clone(); + amplitude + .reduce_scratch_f64_for_materialized_helicity_add_into( + POINT_COUNT, + &physics, + 1.25, + 0, + &root_factors, + colors, + &mut row_major_totals, + ) + .unwrap(); + let mut plane_native_totals = initial; + with_plane_native_amplitudes(&outputs, POINT_COUNT, OUTPUT_COUNT, |planes| { + amplitude + .reduce_planes_f64_for_materialized_helicity_add_into( + planes, + &physics, + 1.25, + 0, + &root_factors, + colors, + &mut plane_native_totals, + ) + .unwrap() + }); + assert_eq!(plane_native_totals, row_major_totals); + let expected_plan_index = usize::from(colors.is_some()); + assert_eq!( + amplitude + .bind_materialized_helicity_direct_total_plan(&physics, 0, &root_factors, colors) + .unwrap(), + expected_plan_index + ); + assert_eq!( + amplitude.materialized_helicity_direct_total_plans.len(), + expected_plan_index + 1, + "equivalent materialized-helicity reductions must reuse their cold binding" + ); + } +} + +#[test] +fn materialized_helicity_direct_total_plan_uses_actual_physics_recipient() { + const POINT_COUNT: usize = 17; + const OUTPUT_COUNT: usize = 2; + let representative_physics = test_physics_runtime("lc"); + let mut weighted_manifest = representative_physics.manifest.clone(); + let crate::ColorComponent::LcFlow(weighted_flow) = &mut weighted_manifest.color_components[1] + else { + panic!("LC test physics must contain physical flows"); + }; + weighted_flow.coefficient = 3.0; + let parent_physics = + PhysicsRuntime::new(weighted_manifest).expect("valid parent-closure test physics"); + let outputs = (0..POINT_COUNT) + .flat_map(|point| { + (0..OUTPUT_COUNT).map(move |output| { + c64( + (point * 5 + output * 7) as f64 * 0.03125 - 0.75, + (point * 11 + output * 3) as f64 * -0.015625 + 0.5, + ) + }) + }) + .collect::>(); + let root_factors = [Some(c64(0.75, -0.25)), Some(c64(-0.5, 0.125))]; + let selected_colors = BTreeSet::from(["flow:0".to_string()]); + let mut amplitude = test_amplitude_runtime(outputs.clone(), None); + amplitude.output_length = OUTPUT_COUNT; + amplitude.raw_sum_weights = vec![1.0; OUTPUT_COUNT]; + amplitude.raw_sum_all_sector_weights = vec![1.0; OUTPUT_COUNT]; + amplitude.raw_sum_color_sector_ids = vec![None; OUTPUT_COUNT]; + amplitude.raw_sum_groups[0].indices = vec![0, 1]; + + for (expected_plan_index, physics) in [&representative_physics, &parent_physics] + .into_iter() + .enumerate() + { + let mut expected = vec![0.0; POINT_COUNT]; + amplitude + .reduce_scratch_f64_for_materialized_helicity_add_into( + POINT_COUNT, + physics, + 1.25, + 0, + &root_factors, + Some(&selected_colors), + &mut expected, + ) + .unwrap(); + let mut candidate = vec![0.0; POINT_COUNT]; + with_plane_native_amplitudes(&outputs, POINT_COUNT, OUTPUT_COUNT, |planes| { + amplitude + .reduce_planes_f64_for_materialized_helicity_add_into( + planes, + physics, + 1.25, + 0, + &root_factors, + Some(&selected_colors), + &mut candidate, + ) + .unwrap() + }); + assert_eq!(candidate, expected); + assert_eq!( + amplitude + .bind_materialized_helicity_direct_total_plan( + physics, + 0, + &root_factors, + Some(&selected_colors) + ) + .unwrap(), + expected_plan_index + ); + } + assert_eq!(amplitude.materialized_helicity_direct_total_plans.len(), 2); +} + +#[test] +fn materialized_helicity_direct_total_identity_singleton_is_bitwise_exact() { + let physics = test_physics_runtime("lc"); + let outputs = vec![ + c64(2.0, -3.0), + c64(-0.0, 0.0), + c64(0.0, -0.0), + c64(-1.25, 4.5), + ]; + let root_factors = [Some(c64(1.0, 0.0))]; + let mut amplitude = test_amplitude_runtime(outputs.clone(), None); + amplitude.output_length = 1; + amplitude.raw_sum_weights = vec![1.0]; + amplitude.raw_sum_all_sector_weights = vec![1.0]; + amplitude.raw_sum_color_sector_ids = vec![None]; + amplitude.raw_sum_groups[0].indices = vec![0]; + + let mut expected = vec![-0.0, 0.25, -2.0, 7.0]; + amplitude + .reduce_scratch_f64_for_materialized_helicity_add_into( + outputs.len(), + &physics, + 1.25, + 0, + &root_factors, + None, + &mut expected, + ) + .unwrap(); + let mut candidate = vec![-0.0, 0.25, -2.0, 7.0]; + with_plane_native_amplitudes(&outputs, outputs.len(), 1, |planes| { + amplitude + .reduce_planes_f64_for_materialized_helicity_add_into( + planes, + &physics, + 1.25, + 0, + &root_factors, + None, + &mut candidate, + ) + .unwrap() + }); + assert_eq!( + candidate + .iter() + .map(|value| value.to_bits()) + .collect::>(), + expected + .iter() + .map(|value| value.to_bits()) + .collect::>() + ); + assert_eq!( + amplitude.materialized_helicity_direct_total_plans[0].groups[0].identity_output_index, + Some(0) + ); +} + +#[test] +fn materialized_helicity_direct_total_plan_cache_is_artifact_bounded() { + let physics = test_physics_runtime("lc"); + let mut amplitude = test_amplitude_runtime(vec![c64(1.0, 0.0)], None); + for index in 0..24 { + amplitude + .bind_materialized_helicity_direct_total_plan( + &physics, + 0, + &[Some(c64(index as f64 + 1.0, 0.0))], + None, + ) + .unwrap(); + } + let expected_capacity = physics.manifest.helicities.len().clamp(16, 512); + assert_eq!( + amplitude.materialized_helicity_direct_total_plan_capacity, + expected_capacity + ); + assert_eq!( + amplitude.materialized_helicity_direct_total_plans.len(), + expected_capacity + ); +} + +#[test] +fn plane_native_materialized_helicity_routed_components_match_resolved_accumulation() { + const POINT_COUNT: usize = 65; + const OUTPUT_COUNT: usize = 2; + const TARGET_COMPONENT_COUNT: usize = 3; + let physics = test_physics_runtime("lc"); + let outputs = (0..POINT_COUNT) + .flat_map(|point| { + (0..OUTPUT_COUNT).map(move |output| { + c64( + (point * 7 + output * 3) as f64 * 0.03125 - 1.5, + (point * 5 + output * 11) as f64 * -0.015625 + 0.25, + ) + }) + }) + .collect::>(); + let root_factors = [Some(c64(0.75, -0.25)), Some(c64(-0.5, 0.125))]; + let replay = LcResolvedReplayEntry { + routes: vec![ + LcResolvedReplayRoute { + source_index: 0, + target_index: 2, + weight: 1.5, + }, + LcResolvedReplayRoute { + source_index: 1, + target_index: 0, + weight: -0.25, + }, + ], + }; + let mut amplitude = test_amplitude_runtime(outputs.clone(), None); + amplitude.output_length = OUTPUT_COUNT; + amplitude.raw_sum_weights = vec![1.0; OUTPUT_COUNT]; + amplitude.raw_sum_all_sector_weights = vec![1.0; OUTPUT_COUNT]; + amplitude.raw_sum_color_sector_ids = vec![None; OUTPUT_COUNT]; + amplitude.raw_sum_groups[0].indices = vec![0, 1]; + let materialized = + with_plane_native_amplitudes(&outputs, POINT_COUNT, OUTPUT_COUNT, |planes| { + amplitude + .reduce_planes_f64_for_materialized_helicity( + planes, + &physics, + 1.25, + 0, + &root_factors, + None, + ) + .unwrap() + }); + let mut expected = vec![0.0; POINT_COUNT * TARGET_COMPONENT_COUNT]; + accumulate_selected_lc_replay_resolved_f64( + &mut expected, + POINT_COUNT, + &materialized, + std::slice::from_ref(&replay), + 2, + TARGET_COMPONENT_COUNT, + ) + .unwrap(); + let mut candidate = vec![0.0; POINT_COUNT * TARGET_COMPONENT_COUNT]; + for (start, stop) in [(0usize, 32usize), (32, POINT_COUNT)] { + with_plane_native_amplitudes( + &outputs[start * OUTPUT_COUNT..stop * OUTPUT_COUNT], + stop - start, + OUTPUT_COUNT, + |planes| { + amplitude + .reduce_planes_f64_for_materialized_helicity_routed_components_add_into( + planes, + &physics, + 1.25, + 0, + &root_factors, + None, + &replay, + 2, + TARGET_COMPONENT_COUNT, + POINT_COUNT, + start, + &mut candidate, + ) + .unwrap() + }, + ); + } + assert_eq!(candidate, expected); + + // Alternate proper subsets with implicit and explicit complete axes. + // Sparse routing must neither read nor reset a previous dense position + // table, and it must preserve the resolved reducer's accumulation order. + for selected_colors in [ + Some(BTreeSet::from(["flow:1".to_string()])), + None, + Some(BTreeSet::from(["flow:0".to_string()])), + Some(BTreeSet::from(["flow:0".to_string(), "flow:1".to_string()])), + Some(BTreeSet::from(["flow:1".to_string()])), + ] { + let selected_colors = selected_colors.as_ref(); + let color_indices = physics.selected_color_indices(selected_colors).unwrap(); + let selected_replay = LcResolvedReplayEntry { + routes: color_indices + .iter() + .enumerate() + .map(|(position, &color)| LcResolvedReplayRoute { + source_index: position, + target_index: (color + 1) % TARGET_COMPONENT_COUNT, + weight: if position == 0 { 1.5 } else { -0.25 }, + }) + .collect(), + }; + let materialized = + with_plane_native_amplitudes(&outputs, POINT_COUNT, OUTPUT_COUNT, |planes| { + amplitude + .reduce_planes_f64_for_materialized_helicity( + planes, + &physics, + 1.25, + 0, + &root_factors, + selected_colors, + ) + .unwrap() + }); + let mut expected = vec![0.0; POINT_COUNT * TARGET_COMPONENT_COUNT]; + accumulate_selected_lc_replay_resolved_f64( + &mut expected, + POINT_COUNT, + &materialized, + std::slice::from_ref(&selected_replay), + color_indices.len(), + TARGET_COMPONENT_COUNT, + ) + .unwrap(); + amplitude.routed_reduction_scratch.color_positions = vec![Some(usize::MAX); 7]; + let mut candidate = vec![0.0; POINT_COUNT * TARGET_COMPONENT_COUNT]; + for (start, stop) in [(0usize, 32usize), (32, POINT_COUNT)] { + with_plane_native_amplitudes( + &outputs[start * OUTPUT_COUNT..stop * OUTPUT_COUNT], + stop - start, + OUTPUT_COUNT, + |planes| { + amplitude + .reduce_planes_f64_for_materialized_helicity_routed_components_add_into( + planes, + &physics, + 1.25, + 0, + &root_factors, + selected_colors, + &selected_replay, + color_indices.len(), + TARGET_COMPONENT_COUNT, + POINT_COUNT, + start, + &mut candidate, + ) + .unwrap() + }, + ); + } + assert_eq!(candidate, expected); + if color_indices.len() < physics.manifest.color_components.len() { + assert_eq!( + amplitude.routed_reduction_scratch.color_positions, + vec![Some(usize::MAX); 7], + "restricted colours must not touch the complete-axis position table" + ); + } else { + assert_eq!( + amplitude.routed_reduction_scratch.color_positions, + vec![Some(0), Some(1)] + ); + } + } +} + +#[cfg(not(feature = "f64-symjit"))] +#[test] +fn warmed_plane_native_materialized_helicity_add_into_allocates_zero() { + const POINT_COUNT: usize = 129; + const OUTPUT_COUNT: usize = 2; + let physics = test_physics_runtime("lc"); + let outputs = (0..POINT_COUNT) + .flat_map(|point| { + (0..OUTPUT_COUNT).map(move |output| { + c64( + (point * 7 + output * 3) as f64 * 0.03125 - 1.5, + (point * 5 + output * 11) as f64 * -0.015625 + 0.25, + ) + }) + }) + .collect::>(); + let root_factors = [Some(c64(0.75, -0.25)), Some(c64(-0.5, 0.125))]; + let selected_colors = BTreeSet::from(["flow:1".to_string()]); + let mut amplitude = test_amplitude_runtime(outputs.clone(), None); + amplitude.output_length = OUTPUT_COUNT; + amplitude.raw_sum_weights = vec![1.0; OUTPUT_COUNT]; + amplitude.raw_sum_all_sector_weights = vec![1.0; OUTPUT_COUNT]; + amplitude.raw_sum_color_sector_ids = vec![None; OUTPUT_COUNT]; + amplitude.raw_sum_groups[0].indices = vec![0, 1]; + + let mut workspace = + crate::direct_arena::DirectArenaWorkspace::new(0, OUTPUT_COUNT as u32, POINT_COUNT as u32) + .unwrap(); + workspace.begin_tile(POINT_COUNT as u32).unwrap(); + let stride = workspace.point_stride() as usize; + { + let (_, _, values_re, values_im) = workspace.split_slices_mut(); + for point in 0..POINT_COUNT { + for component in 0..OUTPUT_COUNT { + let value = outputs[point * OUTPUT_COUNT + component]; + values_re[component * stride + point] = value.re; + values_im[component * stride + point] = value.im; + } + } + } + let (values_re, values_im) = workspace.amplitude_slices(); + let planes = crate::direct_arena::DirectAmplitudePlanes::new( + values_re, + values_im, + stride as u32, + POINT_COUNT as u32, + ) + .unwrap(); + let mut totals = vec![0.0; POINT_COUNT]; + amplitude + .reduce_planes_f64_for_materialized_helicity_add_into( + planes, + &physics, + 1.25, + 0, + &root_factors, + Some(&selected_colors), + &mut totals, + ) + .unwrap(); + totals.fill(0.0); + let (result, allocation_count, allocated_bytes) = + super::evaluator::native_direct::tests::count_allocations(|| { + amplitude.reduce_planes_f64_for_materialized_helicity_add_into( + planes, + &physics, + 1.25, + 0, + &root_factors, + Some(&selected_colors), + &mut totals, + ) + }); + result.unwrap(); + assert_eq!( + allocation_count, 0, + "warmed materialized-helicity plane accumulator allocated" + ); + assert_eq!( + allocated_bytes, 0, + "warmed materialized-helicity plane accumulator allocated bytes" + ); + + let replay = LcResolvedReplayEntry { + routes: vec![LcResolvedReplayRoute { + source_index: 0, + target_index: 0, + weight: 1.0, + }], + }; + let mut target_components = vec![0.0; POINT_COUNT]; + amplitude + .reduce_planes_f64_for_materialized_helicity_routed_components_add_into( + planes, + &physics, + 1.25, + 0, + &root_factors, + Some(&selected_colors), + &replay, + 1, + 1, + POINT_COUNT, + 0, + &mut target_components, + ) + .unwrap(); + target_components.fill(0.0); + let (result, allocation_count, allocated_bytes) = + super::evaluator::native_direct::tests::count_allocations(|| { + amplitude.reduce_planes_f64_for_materialized_helicity_routed_components_add_into( + planes, + &physics, + 1.25, + 0, + &root_factors, + Some(&selected_colors), + &replay, + 1, + 1, + POINT_COUNT, + 0, + &mut target_components, + ) + }); + result.unwrap(); + assert_eq!( + allocation_count, 0, + "warmed persistent materialized-helicity route allocated" + ); + assert_eq!( + allocated_bytes, 0, + "warmed persistent materialized-helicity route allocated bytes" + ); +} + +fn repeated_test_group(id: i64, output_index: usize, sector_id: i64) -> RawSumGroup { + RawSumGroup { + id, + indices: vec![output_index], + weight: 1.0, + all_sector_weight: 1.0, + sector_ids: vec![sector_id], + } +} + +fn legacy_color_contraction_totals( + amplitudes: &[Complex], + output_length: usize, + groups: &[RawSumGroup], + entries: &[ColorContractionEntry], +) -> Vec { + amplitudes + .chunks_exact(output_length) + .map(|row| { + let group_values = groups + .iter() + .map(|group| { + group + .indices + .iter() + .fold(Complex::new(0.0, 0.0), |total, index| total + row[*index]) + }) + .collect::>(); + entries.iter().fold(0.0, |total, entry| { + let product = group_values[entry.left_group_index] + * group_values[entry.right_group_index].conj(); + total + + entry.symmetry_factor + * (entry.weight_re * product.re - entry.weight_im * product.im) + }) + }) + .collect() +} + +fn reduction_test_amplitude( + output_length: usize, + outputs: Vec>, + groups: Vec, + entries: Vec, +) -> AmplitudeRuntime { + let contraction = ColorContractionRuntime::new(&groups, entries); + AmplitudeRuntime { + output_length, + raw_sum_weights: vec![1.0; output_length], + raw_sum_all_sector_weights: vec![1.0; output_length], + raw_sum_color_sector_ids: vec![None; output_length], + raw_sum_groups: groups, + has_coherent_groups: true, + color_contraction: Some(contraction), + color_topology_replay: None, + input_components: None, + input_spans: Vec::new(), + parameter_scratch_f64: Vec::new(), + evaluator_output_scratch_f64: Vec::new(), + output_scratch_f64: outputs, + resolved_source_row_scratch_f64: Vec::new(), + resolved_target_row_scratch_f64: Vec::new(), + routed_reduction_scratch: RoutedReductionScratch::default(), + materialized_helicity_direct_total_plans: Vec::new(), + materialized_helicity_direct_total_plan_capacity: 0, + materialized_helicity_direct_total_next_replacement: 0, + materialized_helicity_direct_default_plan_by_selector_domain: Vec::new(), + evaluator_output_order: None, + evaluator: Some(empty_evaluator_group()), + } +} + +#[test] +fn compiled_symmetric_group_reducer_binds_group_ids_and_multi_root_odd_tail() { + const POINT_COUNT: usize = 127; + const PREPARED_CAPACITY: usize = 136; + const GROUP_COUNT: usize = 13; + const OUTPUT_COUNT: usize = 15; + let destination_ids = vec![7, 0, 12, 3, 9, 1, 11, 4, 10, 2, 8, 5, 6]; + let vector_order = [12, 4, 1, 9, 0, 7, 3, 11, 2, 10, 5, 8, 6]; + let mut sector_by_id = [0usize; GROUP_COUNT]; + for (sector, destination) in destination_ids.iter().copied().enumerate() { + sector_by_id[destination as usize] = sector; + } + let mut next_output = 0usize; + let groups = vector_order + .into_iter() + .map(|id| { + let root_count = if id == 7 { 3 } else { 1 }; + let indices = (next_output..next_output + root_count).collect::>(); + next_output += root_count; + RawSumGroup { + id, + indices, + weight: 1.0, + all_sector_weight: 1.0, + sector_ids: vec![sector_by_id[id as usize] as i64], + } + }) + .collect::>(); + assert_eq!(next_output, OUTPUT_COUNT); + let outputs = (0..POINT_COUNT * OUTPUT_COUNT) + .map(|index| { + c64( + (index * 29 % 211) as f64 * 0.03125 - 2.5, + (index * 43 % 197) as f64 * -0.015625 + 1.25, + ) + }) + .collect::>(); + let manifest = GenericColorContractionManifest { + supported: true, + reason: None, + group_count: GROUP_COUNT, + includes_color_factor: true, + entries: Vec::new(), + repeated_block: None, + }; + + let duplicated_destination = + crate::recurrence::RecurrenceColorContraction::symmetric_group_s3_for_runtime_test( + vec![0; GROUP_COUNT], + GROUP_COUNT as u32, + ); + let duplicate_error = match build_compiled_symmetric_group_color_contraction_runtime( + Some(&manifest), + &groups, + duplicated_destination, + ) { + Ok(_) => panic!("duplicate coherent destination unexpectedly bound"), + Err(error) => error, + }; + assert_eq!(duplicate_error.kind(), crate::RusticolErrorKind::Integrity); + assert!(duplicate_error.message().contains("symmetric-group FFT")); + + let sparse_groups = groups + .iter() + .map(|group| RawSumGroup { + id: group.id, + indices: group.indices.clone(), + weight: group.weight, + all_sector_weight: group.all_sector_weight, + sector_ids: group + .sector_ids + .iter() + .map(|sector| 2 * sector + 1) + .collect(), + }) + .collect::>(); + let sparse_plan = + crate::recurrence::RecurrenceColorContraction::symmetric_group_s3_for_runtime_test( + destination_ids.clone(), + GROUP_COUNT as u32, + ) + .with_sparse_sector_domain_for_runtime_test(); + let sparse_contraction = build_compiled_symmetric_group_color_contraction_runtime( + Some(&manifest), + &sparse_groups, + sparse_plan, + ) + .expect("inactive physical sectors must not block a compiled FFT reducer"); + assert_eq!( + sparse_contraction + .symmetric_group + .as_ref() + .unwrap() + .plan + .active_sector_count(), + GROUP_COUNT, + ); + + let plan = crate::recurrence::RecurrenceColorContraction::symmetric_group_s3_for_runtime_test( + destination_ids.clone(), + GROUP_COUNT as u32, + ); + let reducer = match plan.runtime_reducer().unwrap() { + crate::recurrence::RuntimeColorContractionReducer::SymmetricGroupFourier(reducer) => { + reducer + } + _ => panic!("expected symmetric-group reducer"), + }; + let mut expected_workspace = reducer.workspace(PREPARED_CAPACITY).unwrap(); + reducer + .reduce_lanes( + &mut expected_workspace, + POINT_COUNT, + |local_group, point| { + let group_id = i64::from(destination_ids[local_group]); + let group = groups.iter().find(|group| group.id == group_id).unwrap(); + let value = group.indices.iter().fold(c64(0.0, 0.0), |total, index| { + total + outputs[point * OUTPUT_COUNT + *index] + }); + Ok((value.re, value.im)) + }, + ) + .unwrap(); + let expected = expected_workspace.reduced(POINT_COUNT).unwrap().to_vec(); + + let contraction = + build_compiled_symmetric_group_color_contraction_runtime(Some(&manifest), &groups, plan) + .unwrap(); + assert_eq!( + contraction + .symmetric_group + .as_ref() + .unwrap() + .ordered_group_indices, + destination_ids + .iter() + .map(|destination| { + groups + .iter() + .position(|group| group.id == i64::from(*destination)) + .unwrap() + }) + .collect::>(), + ); + let mut amplitude = reduction_test_amplitude(OUTPUT_COUNT, outputs.clone(), groups, Vec::new()); + amplitude.color_contraction = Some(contraction); + amplitude + .prepare_compiled_symmetric_group_workspace(PREPARED_CAPACITY) + .unwrap(); + assert_eq!( + amplitude + .color_contraction + .as_ref() + .and_then(|contraction| contraction.symmetric_group.as_ref()) + .and_then(|symmetric_group| symmetric_group.workspace.as_ref()) + .unwrap() + .lane_capacity(), + PREPARED_CAPACITY, + ); + + let actual = plane_native_totals(&mut amplitude, &outputs, POINT_COUNT, None); + for (actual, expected) in actual.iter().zip(expected.iter()) { + assert!( + (actual - expected).abs() <= 1.0e-11 * expected.abs().max(1.0), + "compiled symmetric-group reduction {actual} differs from reference {expected}", + ); + } + + #[cfg(not(feature = "f64-symjit"))] + { + let mut workspace = crate::direct_arena::DirectArenaWorkspace::new( + 0, + OUTPUT_COUNT as u32, + POINT_COUNT as u32, + ) + .unwrap(); + workspace.begin_tile(POINT_COUNT as u32).unwrap(); + let stride = workspace.point_stride() as usize; + { + let (_, _, values_re, values_im) = workspace.split_slices_mut(); + for point in 0..POINT_COUNT { + for component in 0..OUTPUT_COUNT { + let value = outputs[point * OUTPUT_COUNT + component]; + values_re[component * stride + point] = value.re; + values_im[component * stride + point] = value.im; + } + } + } + let (values_re, values_im) = workspace.amplitude_slices(); + let planes = crate::direct_arena::DirectAmplitudePlanes::new( + values_re, + values_im, + stride as u32, + POINT_COUNT as u32, + ) + .unwrap(); + let mut totals = vec![0.0; POINT_COUNT]; + amplitude + .reduce_planes_f64_into_selected_slice(planes, &mut totals, None) + .unwrap(); + let (result, allocation_count, allocated_bytes) = + super::evaluator::native_direct::tests::count_allocations(|| { + amplitude.reduce_planes_f64_into_selected_slice(planes, &mut totals, None) + }); + result.unwrap(); + assert_eq!( + allocation_count, 0, + "warmed symmetric-group reduction allocated" + ); + assert_eq!( + allocated_bytes, 0, + "warmed symmetric-group reduction allocated bytes", + ); + } +} + +#[test] +fn repeated_real_color_blocks_match_legacy_reduction_for_permuted_outputs() { + let groups = vec![ + repeated_test_group(10, 2, 100), + repeated_test_group(11, 5, 100), + repeated_test_group(12, 0, 200), + repeated_test_group(13, 3, 200), + repeated_test_group(14, 1, 300), + repeated_test_group(15, 4, 300), + ]; + // Deliberately interleave the two disconnected components and do not + // present their left indices monotonically. Runtime canonicalization may + // change floating-point association, but it must preserve the contraction. + let entries = vec![ + ColorContractionEntry { + left_group_index: 3, + right_group_index: 5, + weight_re: 0.5, + weight_im: 0.0, + symmetry_factor: 2.0, + }, + ColorContractionEntry { + left_group_index: 0, + right_group_index: 0, + weight_re: 1.25, + weight_im: 0.0, + symmetry_factor: 1.0, + }, + ColorContractionEntry { + left_group_index: 1, + right_group_index: 3, + weight_re: -0.75, + weight_im: 0.0, + symmetry_factor: 2.0, + }, + ColorContractionEntry { + left_group_index: 4, + right_group_index: 4, + weight_re: 2.0, + weight_im: 0.0, + symmetry_factor: 1.0, + }, + ColorContractionEntry { + left_group_index: 2, + right_group_index: 4, + weight_re: 0.5, + weight_im: 0.0, + symmetry_factor: 2.0, + }, + ColorContractionEntry { + left_group_index: 1, + right_group_index: 1, + weight_re: 1.25, + weight_im: 0.0, + symmetry_factor: 1.0, + }, + ColorContractionEntry { + left_group_index: 0, + right_group_index: 2, + weight_re: -0.75, + weight_im: 0.0, + symmetry_factor: 2.0, + }, + ColorContractionEntry { + left_group_index: 5, + right_group_index: 5, + weight_re: 2.0, + weight_im: 0.0, + symmetry_factor: 1.0, + }, + ]; + let outputs = vec![ + c64(0.5, -1.0), + c64(1.5, 0.25), + c64(-0.75, 2.0), + c64(0.125, -0.5), + c64(2.25, 1.0), + c64(-1.25, -0.75), + c64(1.0, 0.5), + c64(-0.25, 1.25), + c64(0.75, -1.5), + c64(2.0, 0.125), + c64(-1.0, 0.75), + c64(0.25, -2.0), + ]; + let expected = legacy_color_contraction_totals(&outputs, 6, &groups, &entries); + let mut amplitude = reduction_test_amplitude(6, outputs.clone(), groups, entries); + let repeated = amplitude + .color_contraction + .as_ref() + .and_then(|contraction| contraction.repeated_block.as_ref()) + .expect("two identical color components should be canonicalized"); + assert_eq!(repeated.component_count, 2); + assert_eq!(repeated.entries.len(), 4); + assert_eq!( + repeated.singleton_output_indices.as_deref(), + Some([2, 5, 0, 3, 1, 4].as_slice()) + ); + assert!(repeated.all_weights_real); + + let mut actual = vec![0.0; 2]; + amplitude + .reduce_scratch_f64_into_selected_slice(2, &mut actual, None) + .unwrap(); + let plane_native = plane_native_totals(&mut amplitude, &outputs, 2, None); + assert_eq!(plane_native, actual); + for (actual, expected) in actual.iter().zip(expected) { + assert!( + (actual - expected).abs() <= 1.0e-12 * expected.abs().max(1.0), + "repeated-block reduction {actual} differs from legacy {expected}" + ); + } +} + +#[test] +fn repeated_color_block_requires_identical_component_coefficients() { + let groups = vec![ + repeated_test_group(10, 0, 100), + repeated_test_group(11, 1, 100), + repeated_test_group(12, 2, 200), + repeated_test_group(13, 3, 200), + ]; + let entries = vec![ + ColorContractionEntry { + left_group_index: 0, + right_group_index: 2, + weight_re: 1.0, + weight_im: 0.0, + symmetry_factor: 2.0, + }, + ColorContractionEntry { + left_group_index: 1, + right_group_index: 3, + weight_re: 1.0 + f64::EPSILON, + weight_im: 0.0, + symmetry_factor: 2.0, + }, + ]; + let contraction = ColorContractionRuntime::new(&groups, entries); + assert!(contraction.repeated_block.is_none()); +} + +#[test] +fn plane_native_expanded_color_contraction_matches_row_major_odd_tail() { + const POINT_COUNT: usize = 127; + let groups = vec![ + RawSumGroup { + id: 10, + indices: vec![0, 3], + weight: 1.0, + all_sector_weight: 1.0, + sector_ids: vec![100], + }, + repeated_test_group(11, 1, 200), + repeated_test_group(12, 2, 300), + ]; + let entries = vec![ + ColorContractionEntry { + left_group_index: 0, + right_group_index: 0, + weight_re: 1.25, + weight_im: 0.0, + symmetry_factor: 1.0, + }, + ColorContractionEntry { + left_group_index: 0, + right_group_index: 1, + weight_re: -0.75, + weight_im: 0.125, + symmetry_factor: 2.0, + }, + ColorContractionEntry { + left_group_index: 1, + right_group_index: 2, + weight_re: 0.5, + weight_im: -0.25, + symmetry_factor: 2.0, + }, + ]; + let outputs = (0..POINT_COUNT * 4) + .map(|index| { + c64( + (index * 13 % 97) as f64 * 0.0625 - 2.0, + (index * 17 % 89) as f64 * -0.03125 + 1.5, + ) + }) + .collect::>(); + let mut amplitude = reduction_test_amplitude(4, outputs.clone(), groups, entries); + assert!( + amplitude + .color_contraction + .as_ref() + .unwrap() + .repeated_block + .is_none() + ); + let mut row_major = vec![0.0; POINT_COUNT]; + amplitude + .reduce_scratch_f64_into_selected_slice(POINT_COUNT, &mut row_major, None) + .unwrap(); + let plane_native = plane_native_totals(&mut amplitude, &outputs, POINT_COUNT, None); + assert_eq!(plane_native, row_major); +} + +#[test] +fn compact_repeated_color_manifest_builds_without_expanded_entries() { + let groups = vec![ + repeated_test_group(10, 2, 100), + repeated_test_group(11, 3, 100), + repeated_test_group(12, 0, 200), + repeated_test_group(13, 1, 200), + ]; + let manifest = GenericColorContractionManifest { + supported: true, + reason: None, + group_count: groups.len(), + includes_color_factor: true, + entries: Vec::new(), + repeated_block: Some(GenericRepeatedColorContractionBlockManifest { + component_count: 2, + component_group_ids: vec![10, 11, 12, 13], + entries: vec![ + GenericRepeatedColorContractionEntryManifest { + _exact_weight: None, + left_group_index: 0, + right_group_index: 0, + weight: vec![1.25, 0.0], + symmetry_factor: 1.0, + }, + GenericRepeatedColorContractionEntryManifest { + _exact_weight: None, + left_group_index: 0, + right_group_index: 1, + weight: vec![-0.75, 0.0], + symmetry_factor: 2.0, + }, + GenericRepeatedColorContractionEntryManifest { + _exact_weight: None, + left_group_index: 1, + right_group_index: 1, + weight: vec![2.0, 0.0], + symmetry_factor: 1.0, + }, + ], + factorized_block: None, + }), + }; + let contraction = build_color_contraction_runtime(Some(&manifest), &groups) + .unwrap() + .expect("compact repeated contraction"); + assert!(contraction.entries.is_empty()); + assert_eq!(contraction.logical_entry_count().unwrap(), 6); + let logical_entries = contraction.logical_entries().collect::>(); + assert_eq!( + logical_entries + .iter() + .map(|entry| (entry.left_group_index, entry.right_group_index)) + .collect::>(), + vec![(0, 0), (0, 2), (2, 2), (1, 1), (1, 3), (3, 3)] + ); + + let outputs = vec![ + c64(0.5, -1.0), + c64(1.5, 0.25), + c64(-0.75, 2.0), + c64(0.125, -0.5), + ]; + let expected = legacy_color_contraction_totals(&outputs, 4, &groups, &logical_entries)[0]; + let mut amplitude = test_amplitude_runtime(outputs.clone(), Some(contraction)); + amplitude.raw_sum_groups = groups; + let mut actual = vec![0.0]; + amplitude + .reduce_scratch_f64_into_selected_slice(1, &mut actual, None) + .unwrap(); + assert!((actual[0] - expected).abs() <= 1.0e-12 * expected.abs().max(1.0)); +} + +#[test] +fn compact_walsh_color_manifest_matches_expanded_repeated_reduction() { + let component_count = 3; + let output_indices = [7, 0, 10, 3, 5, 11, 1, 8, 4, 9, 2, 6]; + let groups = output_indices + .iter() + .copied() + .enumerate() + .map(|(group_index, output_index)| { + repeated_test_group( + 10 + group_index as i64, + output_index, + 100 + (group_index / component_count) as i64, + ) + }) + .collect::>(); + let kernel = [4.0, 1.0, 2.0, 0.5]; + let entries = (0..4) + .flat_map(|left_group_index| { + (left_group_index..4).map(move |right_group_index| { + GenericRepeatedColorContractionEntryManifest { + _exact_weight: None, + left_group_index, + right_group_index, + weight: vec![kernel[left_group_index ^ right_group_index], 0.0], + symmetry_factor: if left_group_index == right_group_index { + 1.0 + } else { + 2.0 + }, + } + }) + }) + .collect::>(); + let manifest = GenericColorContractionManifest { + supported: true, + reason: None, + group_count: groups.len(), + includes_color_factor: true, + entries: Vec::new(), + repeated_block: Some(GenericRepeatedColorContractionBlockManifest { + component_count, + component_group_ids: (10..10 + groups.len() as i64).collect(), + entries, + factorized_block: Some( + GenericFactorizedColorContractionBlockManifest::KleinFourWalsh { + cosets: vec![[0, 1, 2, 3]], + }, + ), + }), + }; + let contraction = build_color_contraction_runtime(Some(&manifest), &groups) + .unwrap() + .expect("compact Walsh contraction"); + let repeated = contraction + .repeated_block + .as_ref() + .expect("repeated contraction"); + let walsh = repeated.walsh_block.as_ref().expect("Walsh block"); + assert_eq!(walsh.cosets, vec![[0, 1, 2, 3]]); + assert_eq!(walsh.entries.len(), 4); + let logical_entries = contraction.logical_entries().collect::>(); + let outputs = (0..12) + .map(|index| { + let value = index as f64 + 1.0; + c64(0.25 * value, 0.125 * (5.0 - value)) + }) + .collect::>(); + let expected = legacy_color_contraction_totals(&outputs, 12, &groups, &logical_entries)[0]; + let mut amplitude = test_amplitude_runtime(outputs.clone(), Some(contraction)); + amplitude.raw_sum_groups = groups; + let mut actual = vec![0.0]; + + amplitude + .reduce_scratch_f64_into_selected_slice(1, &mut actual, None) + .unwrap(); + let plane_native = plane_native_totals(&mut amplitude, &outputs, 1, None); + + assert!( + (actual[0] - expected).abs() <= 1.0e-12 * expected.abs().max(1.0), + "Walsh reduction {} differs from expanded repeated reduction {expected}", + actual[0] + ); + assert_eq!(plane_native, actual); +} + +#[test] +fn compact_c2k_walsh_h8_manifest_matches_expanded_repeated_reduction() { + let component_count = 3; + let local_group_count = 8; + let output_count = component_count * local_group_count; + let groups = (0..output_count) + .map(|group_index| { + repeated_test_group( + 10 + group_index as i64, + (group_index * 7) % output_count, + 100 + (group_index / component_count) as i64, + ) + }) + .collect::>(); + let kernel = [8.0, 1.0, 2.0, 0.5, 3.0, 0.25, 0.75, 0.125]; + let entries = (0..local_group_count) + .flat_map(|left_group_index| { + (left_group_index..local_group_count).map(move |right_group_index| { + GenericRepeatedColorContractionEntryManifest { + _exact_weight: None, + left_group_index, + right_group_index, + weight: vec![kernel[left_group_index ^ right_group_index], 0.0], + symmetry_factor: if left_group_index == right_group_index { + 1.0 + } else { + 2.0 + }, + } + }) + }) + .collect::>(); + let manifest = GenericColorContractionManifest { + supported: true, + reason: None, + group_count: groups.len(), + includes_color_factor: true, + entries: Vec::new(), + repeated_block: Some(GenericRepeatedColorContractionBlockManifest { + component_count, + component_group_ids: (10..10 + groups.len() as i64).collect(), + entries, + factorized_block: Some( + GenericFactorizedColorContractionBlockManifest::ElementaryAbelianWalsh { + rank: 3, + cosets: vec![(0..local_group_count).collect()], + }, + ), + }), + }; + let contraction = build_color_contraction_runtime(Some(&manifest), &groups) + .unwrap() + .expect("compact C2^3 Walsh contraction"); + let repeated = contraction + .repeated_block + .as_ref() + .expect("repeated contraction"); + let walsh = repeated.c2k_walsh_block.as_ref().expect("C2^3 Walsh block"); + assert_eq!(walsh.subgroup_order, 8); + assert_eq!(walsh.cosets, vec![(0..8).collect::>()]); + assert_eq!(walsh.entries.len(), 8); + let logical_entries = contraction.logical_entries().collect::>(); + let outputs = (0..output_count) + .map(|index| { + let value = index as f64 + 1.0; + c64(0.125 * value, 0.0625 * (7.0 - value)) + }) + .collect::>(); + let expected = + legacy_color_contraction_totals(&outputs, output_count, &groups, &logical_entries)[0]; + let mut amplitude = test_amplitude_runtime(outputs.clone(), Some(contraction)); + amplitude.raw_sum_groups = groups; + let mut actual = vec![0.0]; + + amplitude + .reduce_scratch_f64_into_selected_slice(1, &mut actual, None) + .unwrap(); + let plane_native = plane_native_totals(&mut amplitude, &outputs, 1, None); + + assert!( + (actual[0] - expected).abs() <= 1.0e-12 * expected.abs().max(1.0), + "C2^3 Walsh reduction {} differs from expanded repeated reduction {expected}", + actual[0] + ); + assert_eq!(plane_native, actual); +} + +#[test] +fn compact_c2k_walsh_h8_multiple_cosets_matches_expanded_reference() { + const COMPONENT_COUNT: usize = 4; + const COSET_COUNT: usize = 3; + const SUBGROUP_ORDER: usize = 8; + const LOCAL_GROUP_COUNT: usize = COSET_COUNT * SUBGROUP_ORDER; + const OUTPUT_COUNT: usize = COMPONENT_COUNT * LOCAL_GROUP_COUNT; + + // Each row is ordered by the C2^3 generator bitmask, but the local group + // indices deliberately form a shuffled partition. + let cosets = vec![ + vec![17, 2, 21, 6, 13, 10, 1, 22], + vec![5, 18, 8, 15, 3, 20, 12, 7], + vec![23, 0, 14, 9, 19, 4, 16, 11], + ]; + let mut local_coordinates = vec![(usize::MAX, usize::MAX); LOCAL_GROUP_COUNT]; + for (coset_index, coset) in cosets.iter().enumerate() { + for (subgroup_index, local_group_index) in coset.iter().copied().enumerate() { + local_coordinates[local_group_index] = (coset_index, subgroup_index); + } + } + assert!( + local_coordinates + .iter() + .all(|coordinates| coordinates.0 != usize::MAX) + ); + + // The six independent coset-pair kernels include negative, small, and + // off-diagonal weights. XOR indexing makes every block exactly C2^3 + // circulant while retaining nontrivial couplings between all cosets. + let kernels = [ + [3.25, -0.5, 0.125, 1.75, -2.0, 0.0625, 0.875, -0.25], + [-1.5, 0.375, 2.25, -0.125, 0.75, -3.0, 0.03125, 1.0], + [0.625, -2.5, 0.1875, 1.125, -0.75, 0.5, 2.0, -0.0625], + [4.0, 0.25, -1.25, 0.5, 0.015625, -0.875, 1.5, -2.25], + [-0.4375, 1.875, -0.03125, 2.5, 0.75, -1.0, 0.3125, 3.0], + [2.75, -0.1875, 0.5625, -1.75, 1.25, 0.046875, -2.0, 0.375], + ]; + let kernel_index = |left_coset: usize, right_coset: usize| match ( + left_coset.min(right_coset), + left_coset.max(right_coset), + ) { + (0, 0) => 0, + (0, 1) => 1, + (0, 2) => 2, + (1, 1) => 3, + (1, 2) => 4, + (2, 2) => 5, + pair => panic!("unexpected coset pair {pair:?}"), + }; + let mut entries = Vec::new(); + for left_group_index in 0..LOCAL_GROUP_COUNT { + let (left_coset, left_subgroup) = local_coordinates[left_group_index]; + for (right_group_index, &(right_coset, right_subgroup)) in + local_coordinates.iter().enumerate().skip(left_group_index) + { + entries.push(GenericRepeatedColorContractionEntryManifest { + _exact_weight: None, + left_group_index, + right_group_index, + weight: vec![ + kernels[kernel_index(left_coset, right_coset)][left_subgroup ^ right_subgroup], + 0.0, + ], + symmetry_factor: if left_group_index == right_group_index { + 1.0 + } else { + 2.0 + }, + }); + } + } + + let group_id = |mapping_slot: usize| 10_000 + 13 * mapping_slot as i64; + let component_group_ids = (0..OUTPUT_COUNT).map(group_id).collect::>(); + let mut group_index_by_mapping_slot = vec![usize::MAX; OUTPUT_COUNT]; + let groups = (0..OUTPUT_COUNT) + .map(|group_index| { + let mapping_slot = (group_index * 37 + 11) % OUTPUT_COUNT; + group_index_by_mapping_slot[mapping_slot] = group_index; + repeated_test_group( + group_id(mapping_slot), + (mapping_slot * 29 + 7) % OUTPUT_COUNT, + 500 + (mapping_slot / COMPONENT_COUNT) as i64, + ) + }) + .collect::>(); + assert!( + group_index_by_mapping_slot + .iter() + .all(|group_index| *group_index != usize::MAX) + ); + + // Build the independent expanded reference before moving the compact + // entries into the manifest. This reproduces the logical matrix for every + // repeated component without consulting the optimized runtime plan. + let group_index_by_mapping_slot = &group_index_by_mapping_slot; + let explicit_entries = (0..COMPONENT_COUNT) + .flat_map(|component_index| { + entries.iter().map(move |entry| ColorContractionEntry { + left_group_index: group_index_by_mapping_slot + [entry.left_group_index * COMPONENT_COUNT + component_index], + right_group_index: group_index_by_mapping_slot + [entry.right_group_index * COMPONENT_COUNT + component_index], + weight_re: entry.weight[0], + weight_im: entry.weight[1], + symmetry_factor: entry.symmetry_factor, + }) + }) + .collect::>(); + let manifest = GenericColorContractionManifest { + supported: true, + reason: None, + group_count: groups.len(), + includes_color_factor: true, + entries: Vec::new(), + repeated_block: Some(GenericRepeatedColorContractionBlockManifest { + component_count: COMPONENT_COUNT, + component_group_ids, + entries, + factorized_block: Some( + GenericFactorizedColorContractionBlockManifest::ElementaryAbelianWalsh { + rank: 3, + cosets, + }, + ), + }), + }; + let contraction = build_color_contraction_runtime(Some(&manifest), &groups) + .unwrap() + .expect("multi-coset C2^3 Walsh contraction"); + let repeated = contraction + .repeated_block + .as_ref() + .expect("repeated contraction"); + let walsh = repeated.c2k_walsh_block.as_ref().expect("C2^3 Walsh block"); + assert_eq!(walsh.subgroup_order, SUBGROUP_ORDER); + assert_eq!(walsh.cosets.len(), COSET_COUNT); + assert!( + walsh + .entries + .iter() + .any(|entry| entry.left_group_index != entry.right_group_index), + "transformed plan must retain off-diagonal coset couplings" + ); + + let point_count = 3; + let mut outputs = Vec::with_capacity(point_count * OUTPUT_COUNT); + for point_index in 0..point_count { + let point_scale = [1.0, 0.03125, 5.5][point_index]; + for output_index in 0..OUTPUT_COUNT { + let centered = output_index as f64 - (OUTPUT_COUNT as f64 - 1.0) * 0.5; + let modulation = ((output_index * 17 + point_index * 5) % 19) as f64 - 9.0; + outputs.push(c64( + point_scale * (0.0234375 * centered + 0.0078125 * modulation), + point_scale + * (-0.015625 * centered + 0.00390625 * (modulation * modulation - 13.0)), + )); + } + } + let expected = + legacy_color_contraction_totals(&outputs, OUTPUT_COUNT, &groups, &explicit_entries); + let mut amplitude = test_amplitude_runtime(outputs, Some(contraction)); + amplitude.output_length = OUTPUT_COUNT; + amplitude.raw_sum_weights = vec![1.0; OUTPUT_COUNT]; + amplitude.raw_sum_all_sector_weights = vec![1.0; OUTPUT_COUNT]; + amplitude.raw_sum_color_sector_ids = vec![None; OUTPUT_COUNT]; + amplitude.raw_sum_groups = groups; + let mut actual = vec![0.0; point_count]; + + amplitude + .reduce_scratch_f64_into_selected_slice(point_count, &mut actual, None) + .unwrap(); + + for (point_index, (actual, expected)) in actual.iter().zip(expected).enumerate() { + let absolute_error = (actual - expected).abs(); + let tolerance = 1.0e-15 + 1.0e-12 * expected.abs(); + assert!( + absolute_error <= tolerance, + "multi-coset C2^3 Walsh point {point_index} differs from expanded reference: \ + actual={actual}, expected={expected}, absolute_error={absolute_error}, \ + tolerance={tolerance}" + ); + } +} + +#[test] +fn compact_c2k_walsh_generic_rank_four_matches_expanded_reduction() { + let component_count = 2; + let local_group_count = 16; + let output_count = component_count * local_group_count; + let groups = (0..output_count) + .map(|group_index| { + repeated_test_group( + 20 + group_index as i64, + group_index, + 200 + (group_index / component_count) as i64, + ) + }) + .collect::>(); + let kernel = (0..local_group_count) + .map(|index| 1.0 / (index + 1) as f64) + .collect::>(); + let entries = (0..local_group_count) + .flat_map(|left_group_index| { + let kernel = &kernel; + (left_group_index..local_group_count).map(move |right_group_index| { + GenericRepeatedColorContractionEntryManifest { + _exact_weight: None, + left_group_index, + right_group_index, + weight: vec![kernel[left_group_index ^ right_group_index], 0.0], + symmetry_factor: if left_group_index == right_group_index { + 1.0 + } else { + 2.0 + }, + } + }) + }) + .collect::>(); + let manifest = GenericColorContractionManifest { + supported: true, + reason: None, + group_count: groups.len(), + includes_color_factor: true, + entries: Vec::new(), + repeated_block: Some(GenericRepeatedColorContractionBlockManifest { + component_count, + component_group_ids: (20..20 + groups.len() as i64).collect(), + entries, + factorized_block: Some( + GenericFactorizedColorContractionBlockManifest::ElementaryAbelianWalsh { + rank: 4, + cosets: vec![(0..local_group_count).collect()], + }, + ), + }), + }; + let contraction = build_color_contraction_runtime(Some(&manifest), &groups) + .unwrap() + .expect("compact C2^4 Walsh contraction"); + let logical_entries = contraction.logical_entries().collect::>(); + let outputs = (0..output_count) + .map(|index| c64(index as f64 * 0.03125, 1.0 - index as f64 * 0.015625)) + .collect::>(); + let expected = + legacy_color_contraction_totals(&outputs, output_count, &groups, &logical_entries)[0]; + let mut amplitude = test_amplitude_runtime(outputs, Some(contraction)); + amplitude.raw_sum_groups = groups; + let mut actual = vec![0.0]; + + amplitude + .reduce_scratch_f64_into_selected_slice(1, &mut actual, None) + .unwrap(); + + assert!( + (actual[0] - expected).abs() <= 1.0e-12 * expected.abs().max(1.0), + "generic C2^4 Walsh reduction {} differs from expanded repeated reduction {expected}", + actual[0] + ); +} + +#[test] +fn compact_walsh_color_manifest_rejects_malformed_or_noninvariant_plans() { + let groups = (0..8) + .map(|group_index| { + repeated_test_group( + 10 + group_index as i64, + group_index, + 100 + (group_index / 2) as i64, + ) + }) + .collect::>(); + let manifest = |cosets: Vec<[usize; 4]>, + entries: Vec| { + GenericColorContractionManifest { + supported: true, + reason: None, + group_count: groups.len(), + includes_color_factor: true, + entries: Vec::new(), + repeated_block: Some(GenericRepeatedColorContractionBlockManifest { + component_count: 2, + component_group_ids: (10..18).collect(), + entries, + factorized_block: Some( + GenericFactorizedColorContractionBlockManifest::KleinFourWalsh { cosets }, + ), + }), + } + }; + let cases = [ + ("duplicate index", manifest(vec![[0, 1, 2, 2]], Vec::new())), + ( + "not invariant", + manifest( + vec![[0, 1, 2, 3]], + vec![GenericRepeatedColorContractionEntryManifest { + _exact_weight: None, + left_group_index: 0, + right_group_index: 0, + weight: vec![1.0, 0.0], + symmetry_factor: 1.0, + }], + ), + ), + ]; + for (expected, manifest) in cases { + let error = match build_color_contraction_runtime(Some(&manifest), &groups) { + Ok(_) => panic!("malformed Walsh color contraction must fail"), + Err(error) => error, + }; + assert!( + error.to_string().contains(expected), + "unexpected error for {expected}: {error}" + ); + } + + let unknown = serde_json::from_value::( + json!({"kind": "future-transform", "cosets": [[0, 1, 2, 3]]}), + ); + assert!( + unknown.is_err(), + "unknown factorization kind must fail closed" + ); +} + +#[test] +fn compact_c2k_walsh_manifest_fails_closed_on_invalid_metadata() { + let groups = (0..16) + .map(|group_index| { + repeated_test_group( + 10 + group_index as i64, + group_index, + 100 + (group_index / 2) as i64, + ) + }) + .collect::>(); + let entry = |left_group_index: usize, + right_group_index: usize, + weight_re: f64| + -> GenericRepeatedColorContractionEntryManifest { + GenericRepeatedColorContractionEntryManifest { + _exact_weight: None, + left_group_index, + right_group_index, + weight: vec![weight_re, 0.0], + symmetry_factor: if left_group_index == right_group_index { + 1.0 + } else { + 2.0 + }, + } + }; + let manifest = |rank: usize, + cosets: Vec>, + entries: Vec| { + GenericColorContractionManifest { + supported: true, + reason: None, + group_count: groups.len(), + includes_color_factor: true, + entries: Vec::new(), + repeated_block: Some(GenericRepeatedColorContractionBlockManifest { + component_count: 2, + component_group_ids: (10..26).collect(), + entries, + factorized_block: Some( + GenericFactorizedColorContractionBlockManifest::ElementaryAbelianWalsh { + rank, + cosets, + }, + ), + }), + } + }; + let canonical_coset = vec![(0..8).collect::>()]; + let cases = [ + ( + "rank must be at least three", + manifest(2, vec![(0..4).collect()], Vec::new()), + ), + ( + "do not match rank or local groups", + manifest(3, vec![(0..7).collect()], Vec::new()), + ), + ( + "duplicate index", + manifest(3, vec![vec![0, 1, 2, 3, 4, 5, 6, 6]], Vec::new()), + ), + ( + "weights must be finite", + manifest(3, canonical_coset.clone(), vec![entry(0, 0, f64::NAN)]), + ), + ( + "duplicate matrix entry", + manifest( + 3, + canonical_coset.clone(), + vec![entry(0, 1, 1.0), entry(1, 0, 1.0)], + ), + ), + ( + "not invariant", + manifest(3, canonical_coset.clone(), vec![entry(0, 0, 1.0)]), + ), + ( + "subgroup order overflows", + manifest(usize::BITS as usize, canonical_coset, Vec::new()), + ), + ]; + for (expected, manifest) in cases { + let error = match build_color_contraction_runtime(Some(&manifest), &groups) { + Ok(_) => panic!("malformed C2^k Walsh color contraction must fail"), + Err(error) => error, + }; + assert!( + error.to_string().contains(expected), + "unexpected error for {expected}: {error}" + ); + } + + for malformed in [ + json!({ + "kind": "elementary-abelian-walsh", + "cosets": [[0, 1, 2, 3, 4, 5, 6, 7]], + }), + json!({ + "kind": "elementary-abelian-walsh", + "rank": 3, + "cosets": [[0, 1, 2, 3, 4, 5, 6, 7]], + "unexpected": true, + }), + ] { + assert!( + serde_json::from_value::(malformed) + .is_err(), + "C2^k Walsh wire metadata must be exact" + ); + } +} + +#[test] +fn compact_repeated_color_manifest_rejects_duplicate_group_mapping() { + let groups = vec![ + repeated_test_group(10, 0, 100), + repeated_test_group(11, 1, 100), + ]; + let manifest = GenericColorContractionManifest { + supported: true, + reason: None, + group_count: groups.len(), + includes_color_factor: true, + entries: Vec::new(), + repeated_block: Some(GenericRepeatedColorContractionBlockManifest { + component_count: 2, + component_group_ids: vec![10, 10], + entries: Vec::new(), + factorized_block: None, + }), + }; + let error = match build_color_contraction_runtime(Some(&manifest), &groups) { + Ok(_) => panic!("duplicate repeated color group mapping must fail"), + Err(error) => error, + }; + assert!( + error + .to_string() + .contains("maps a coherent group more than once") + ); +} + +#[test] +fn compact_repeated_color_manifest_rejects_malformed_storage() { + let missing_entries = serde_json::from_value::(json!({ + "supported": true, + "group_count": 2, + "includes_color_factor": true, + })); + assert!(missing_entries.is_err()); + + let groups = vec![ + repeated_test_group(10, 0, 100), + repeated_test_group(11, 1, 100), + ]; + let repeated = |weight: Vec, left_group_index: usize| { + Some(GenericRepeatedColorContractionBlockManifest { + component_count: 2, + component_group_ids: vec![10, 11], + entries: vec![GenericRepeatedColorContractionEntryManifest { + _exact_weight: None, + left_group_index, + right_group_index: 0, + weight, + symmetry_factor: 1.0, + }], + factorized_block: None, + }) + }; + let malformed = [ + ( + "two components", + GenericColorContractionManifest { + supported: true, + reason: None, + group_count: 2, + includes_color_factor: true, + entries: Vec::new(), + repeated_block: repeated(vec![1.0], 0), + }, + ), + ( + "out of bounds", + GenericColorContractionManifest { + supported: true, + reason: None, + group_count: 2, + includes_color_factor: true, + entries: Vec::new(), + repeated_block: repeated(vec![1.0, 0.0], 1), + }, + ), + ( + "cannot mix", + GenericColorContractionManifest { + supported: true, + reason: None, + group_count: 2, + includes_color_factor: true, + entries: vec![GenericColorContractionEntryManifest { + _exact_weight: None, + left_group_id: 10, + right_group_id: 10, + weight: vec![1.0, 0.0], + symmetry_factor: 1.0, + }], + repeated_block: repeated(vec![1.0, 0.0], 0), + }, + ), + ]; + for (expected, manifest) in malformed { + let error = match build_color_contraction_runtime(Some(&manifest), &groups) { + Ok(_) => panic!("malformed repeated color storage must fail"), + Err(error) => error, + }; + assert!( + error.to_string().contains(expected), + "unexpected error for {expected}: {error}" + ); + } +} + +#[test] +fn repeated_complex_color_blocks_match_legacy_reduction() { + let groups = vec![ + repeated_test_group(10, 0, 100), + repeated_test_group(11, 1, 100), + repeated_test_group(12, 2, 200), + repeated_test_group(13, 3, 200), + ]; + let entries = vec![ + ColorContractionEntry { + left_group_index: 0, + right_group_index: 2, + weight_re: 0.75, + weight_im: -0.25, + symmetry_factor: 2.0, + }, + ColorContractionEntry { + left_group_index: 1, + right_group_index: 3, + weight_re: 0.75, + weight_im: -0.25, + symmetry_factor: 2.0, + }, + ]; + let outputs = vec![ + c64(1.0, 2.0), + c64(-0.5, 0.25), + c64(0.75, -1.0), + c64(2.0, 0.5), + ]; + let expected = legacy_color_contraction_totals(&outputs, 4, &groups, &entries); + let mut amplitude = reduction_test_amplitude(4, outputs.clone(), groups, entries); + assert!( + !amplitude + .color_contraction + .as_ref() + .unwrap() + .repeated_block + .as_ref() + .unwrap() + .all_weights_real + ); + let mut actual = vec![0.0]; + amplitude + .reduce_scratch_f64_into_selected_slice(1, &mut actual, None) + .unwrap(); + let plane_native = plane_native_totals(&mut amplitude, &outputs, 1, None); + assert_eq!(plane_native, actual); + assert!((actual[0] - expected[0]).abs() <= 1.0e-12 * expected[0].abs().max(1.0)); +} + +#[test] +fn contracted_color_coverage_does_not_warn_as_incomplete() { + for color_accuracy in ["nlc", "full"] { + let physics = test_physics_runtime(color_accuracy); + let physics_v1 = physics.manifest.clone(); + let mut execution = empty_generic_runtime(); + execution.color_accuracy = color_accuracy.to_string(); + execution.physics = Some(Arc::new(physics)); + let mut runtime = NativeRuntime { + root: PathBuf::new(), + artifact_id: "0".repeat(64), + runtime: execution, + execution_lane: NativeExecutionLane::Compiled, + process: "x x > y".to_string(), + process_key: "x_x_to_y".to_string(), + representative_process_id: "x_x_to_y".to_string(), + external_permutation: vec![0, 1, 2], + input_crossing_map: None, + permutation_alias_of: None, + final_state_permutation_alias_of: None, + physics_v1: native_runtime::LazyProcessPhysicsV1::loaded(physics_v1), + warnings_muted: false, + warned_kinds: BTreeSet::new(), + pending_warnings: Vec::new(), + point_selector_scratch: PointSelectorExecutionScratch::default(), + selector_simd_lane_width: 1, + correlated: None, + process_group: None, + }; + + runtime.record_resolved_warnings(None, None).unwrap(); + + assert!(runtime.pending_warnings.is_empty()); + } +} + +#[test] +fn inconsistent_helicity_weights_are_rejected() { + let mut physics = test_physics_runtime("nlc").manifest; + physics.helicities[1].coefficient = 2.0; + + let error = PhysicsRuntime::new(physics).err().unwrap(); + + assert!(error.to_string().contains("inconsistent helicity weights")); +} + +#[test] +fn numeric_reduction_binding_preserves_manifest_order_and_weights() { + for color_accuracy in ["lc", "nlc", "full"] { + let physics = test_physics_runtime(color_accuracy); + let reduction = physics.reduction_by_group_id.get(&7).unwrap(); + let numeric = physics.numeric_reduction_by_group_id.get(&7).unwrap(); + + let expected_helicity_indices = reduction + .physical_helicity_ids + .iter() + .map(|id| physics.helicity_index_by_id[id]) + .collect::>(); + assert_eq!(numeric.physical_helicity_indices, expected_helicity_indices); + for (index, helicity) in physics.manifest.helicities.iter().enumerate() { + assert_eq!( + numeric.contains_helicity(index), + reduction.physical_helicity_ids.contains(&helicity.id) + ); + } + assert_eq!( + numeric.normalized_helicity_weights, + physics.normalized_helicity_weights(reduction).unwrap() + ); + assert_eq!( + numeric.normalized_member_weights, + physics.normalized_member_weights(reduction).unwrap() + ); + + let mut expected_color_weights = reduction + .physical_color_ids + .iter() + .map(|id| { + let index = physics.color_index_by_id[id]; + ( + index, + physics.manifest.color_components[index].coefficient(), + ) + }) + .collect::>(); + let total_color_weight = expected_color_weights + .iter() + .map(|(_, weight)| *weight) + .sum::(); + for (_, weight) in &mut expected_color_weights { + *weight /= total_color_weight; + } + assert_eq!(numeric.normalized_color_weights, expected_color_weights); + } +} + +pub(super) fn empty_generic_runtime() -> ExecutionRuntime { + ExecutionRuntime { + process: "a b > c".to_string(), + key: "p0".to_string(), + color_accuracy: "lc".to_string(), + external_pdg_order: Vec::new(), + external_count: 0, + parameter_count: 1, + value_parameter_count: 0, + momentum_parameter_count: 0, + current_count: 0, + source_count: 0, + interaction_count: 0, + stage_count: 0, + amplitude_output_count: 0, + lc_topology_replay_enabled: false, + lc_topology_replay_mappings: Arc::new(Vec::new()), + lc_topology_replay_public_mappings: Vec::new(), + lc_topology_replay_routes: Vec::new(), + lc_topology_replay_materialized_sector_ids: BTreeSet::new(), + lc_resolved_replay_plan: None, + lc_resolved_replay_selection_cache: None, + lc_replay_flat_momenta_scratch: Vec::new(), + lc_replay_target_components_scratch: Vec::new(), + color_topology_replay_enabled: false, + color_topology_replay_mappings: Arc::new(Vec::new()), + color_replay_flat_momenta_scratch: Vec::new(), + helicity_recurrence: None, + compiled_helicity_execution_plan: None, + compiled_color_execution_plan: None, + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + compiled_direct_runtime: None, + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + compiled_direct_color_schedules: BTreeMap::new(), + #[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] + compiled_direct_helicity_schedules: BTreeMap::new(), + helicity_sum_runtime: None, + helicity_selector_runtimes: Vec::new(), + helicity_selector_runtime_schedule_modes: Vec::new(), + helicity_selector_lane_by_domain: BTreeMap::new(), + color_selector_runtimes: BTreeMap::new(), + runtime_unavailable_message: None, + sources: Vec::new(), + momentum_slots: Vec::new(), + external_is_initial: Vec::new(), + particle_masses: BTreeMap::new(), + particle_mass_parameter_names: BTreeMap::new(), + normalization_factor: 1.0, + normalization_color_factor: 1.0, + normalization_average_factor: 1.0, + normalization_identical_factor: 1.0, + normalization_qcd_coupling_power: 0, + normalization_electroweak_coupling_power: 0, + model_parameters: Vec::new(), + model_parameter_name_to_index: BTreeMap::new(), + model_parameter_runtime_slots: BTreeMap::new(), + model_parameter_values_f64: vec![0.118], + model_parameter_evaluator: None, + physics_reduction_override: None, + physics: None, + stages: None, + amplitude_stage: None, + state_scratch_f64: Vec::new(), + state_scratch_f64_requires_clear: false, + values_scratch_f64: Vec::new(), + } +} + +fn lc_direct_total_test_group(id: i64, helicity_id: &str, color_id: &str) -> crate::ReductionGroup { + crate::ReductionGroup { + id: format!("group:{id}"), + representative_helicity_id: helicity_id.to_string(), + physical_helicity_ids: vec![helicity_id.to_string()], + representative_color_id: color_id.to_string(), + physical_color_ids: vec![color_id.to_string()], + } +} + +fn strict_lc_direct_total_test_groups() -> Vec { + [ + ("hel:+-", "flow:0"), + ("hel:+-", "flow:1"), + ("hel:-+", "flow:0"), + ("hel:-+", "flow:1"), + ] + .into_iter() + .enumerate() + .map(|(index, (helicity_id, color_id))| { + lc_direct_total_test_group(7 + index as i64, helicity_id, color_id) + }) + .collect() +} + +fn lc_direct_total_test_runtime(groups: Vec) -> ExecutionRuntime { + let mut manifest = test_physics_runtime("lc").manifest; + manifest.helicities[1].computed = true; + manifest.helicities[1].representative_id = manifest.helicities[1].id.clone(); + let crate::ColorComponent::LcFlow(second_flow) = &mut manifest.color_components[1] else { + panic!("LC test physics must contain LC flows"); + }; + second_flow.computed = true; + second_flow.representative_id = second_flow.id.clone(); + manifest.reduction.groups = groups.clone(); + let physics = PhysicsRuntime::new(manifest).expect("valid LC direct-total test physics"); + + let mut amplitude = test_amplitude_runtime(vec![c64(1.0, 0.0); groups.len()], None); + amplitude.raw_sum_groups = groups + .iter() + .enumerate() + .map(|(index, group)| RawSumGroup { + id: parse_reduction_group_id(&group.id).expect("numeric test reduction group id"), + indices: vec![index], + weight: 1.0, + all_sector_weight: 1.0, + sector_ids: vec![index as i64], + }) + .collect(); + + let mut runtime = empty_generic_runtime(); + runtime.amplitude_output_count = groups.len(); + runtime.physics = Some(Arc::new(physics)); + runtime.amplitude_stage = Some(amplitude); + runtime +} + +fn lc_direct_total_test_selectors() -> (BTreeSet, BTreeSet) { + ( + BTreeSet::from(["hel:+-".to_string(), "hel:-+".to_string()]), + BTreeSet::from(["flow:0".to_string(), "flow:1".to_string()]), + ) +} + +#[test] +fn lc_direct_total_source_certification_accepts_exact_cartesian_partitions() { + let runtime = lc_direct_total_test_runtime(strict_lc_direct_total_test_groups()); + let (helicity_ids, color_ids) = lc_direct_total_test_selectors(); + + assert!(certifies_lc_direct_total_source( + &runtime, + &helicity_ids, + &color_ids + )); + + let grouped_runtime = lc_direct_total_test_runtime(vec![ + crate::ReductionGroup { + id: "group:7".to_string(), + representative_helicity_id: "hel:+-".to_string(), + physical_helicity_ids: vec!["hel:+-".to_string(), "hel:-+".to_string()], + representative_color_id: "flow:0".to_string(), + physical_color_ids: vec!["flow:0".to_string()], + }, + crate::ReductionGroup { + id: "group:8".to_string(), + representative_helicity_id: "hel:+-".to_string(), + physical_helicity_ids: vec!["hel:+-".to_string(), "hel:-+".to_string()], + representative_color_id: "flow:1".to_string(), + physical_color_ids: vec!["flow:1".to_string()], + }, + ]); + assert!(certifies_lc_direct_total_source( + &grouped_runtime, + &helicity_ids, + &color_ids + )); +} + +#[test] +fn lc_direct_total_source_certification_rejects_nonexact_auxiliary_reductions() { + let (helicity_ids, color_ids) = lc_direct_total_test_selectors(); + + let mut overbroad_groups = strict_lc_direct_total_test_groups(); + overbroad_groups[0].physical_helicity_ids = vec!["hel:+-".to_string(), "hel:-+".to_string()]; + let overbroad = lc_direct_total_test_runtime(overbroad_groups); + assert!( + !certifies_lc_direct_total_source(&overbroad, &helicity_ids, &color_ids), + "an auxiliary reduction group may not cover extra source helicities" + ); + + let mut partial_groups = strict_lc_direct_total_test_groups(); + partial_groups.pop(); + let partial = lc_direct_total_test_runtime(partial_groups); + assert!( + !certifies_lc_direct_total_source(&partial, &helicity_ids, &color_ids), + "an auxiliary reduction must cover every requested source component" + ); + + let mut duplicate_groups = strict_lc_direct_total_test_groups(); + duplicate_groups[3].representative_helicity_id = "hel:+-".to_string(); + duplicate_groups[3].physical_helicity_ids = vec!["hel:+-".to_string()]; + duplicate_groups[3].representative_color_id = "flow:0".to_string(); + duplicate_groups[3].physical_color_ids = vec!["flow:0".to_string()]; + let duplicate = lc_direct_total_test_runtime(duplicate_groups); + assert!( + !certifies_lc_direct_total_source(&duplicate, &helicity_ids, &color_ids), + "duplicate source members may not stand in for a missing Cartesian member" + ); + + let mut mismatched_ids = lc_direct_total_test_runtime(strict_lc_direct_total_test_groups()); + mismatched_ids + .amplitude_stage + .as_mut() + .expect("test amplitude") + .raw_sum_groups[0] + .id = 999; + assert!( + !certifies_lc_direct_total_source(&mismatched_ids, &helicity_ids, &color_ids), + "the auxiliary evaluator groups must match the certified physics reduction" + ); +} + +fn zero_native_runtime() -> NativeRuntime { + let physics = test_physics_runtime("lc"); + let physics_v1 = physics.manifest.clone(); + let mut execution = empty_generic_runtime(); + execution.external_count = 3; + execution.external_pdg_order = vec![1, -1, 23]; + execution.external_is_initial = vec![true, true, false]; + execution.physics = Some(Arc::new(physics)); + execution.stages = Some(Vec::new()); + let mut amplitude = test_amplitude_runtime(Vec::new(), None); + amplitude.input_components = Some(Vec::new()); + execution.amplitude_stage = Some(amplitude); + NativeRuntime { + root: PathBuf::new(), + artifact_id: "0".repeat(64), + runtime: execution, + execution_lane: NativeExecutionLane::Compiled, + process: "x x > y".to_string(), + process_key: "x_x_to_y".to_string(), + representative_process_id: "x_x_to_y".to_string(), + external_permutation: vec![0, 1, 2], + input_crossing_map: None, + permutation_alias_of: None, + final_state_permutation_alias_of: None, + physics_v1: native_runtime::LazyProcessPhysicsV1::loaded(physics_v1), + warnings_muted: false, + warned_kinds: BTreeSet::new(), + pending_warnings: Vec::new(), + point_selector_scratch: PointSelectorExecutionScratch::default(), + selector_simd_lane_width: 1, + correlated: None, + process_group: None, + } +} + +#[test] +fn native_runtime_clear_is_a_noop_outside_the_on_the_fly_lane() { + let mut runtime = zero_native_runtime(); + let artifact_id = runtime.artifact_id().to_string(); + let parameters = runtime.runtime.model_parameter_values_f64.clone(); + let metadata = runtime.metadata_json().unwrap(); + + runtime.clear().unwrap(); + + assert_eq!(runtime.artifact_id(), artifact_id); + assert_eq!(runtime.runtime.model_parameter_values_f64, parameters); + assert_eq!(runtime.metadata_json().unwrap(), metadata); + assert!( + runtime + .on_the_fly_runtime_state_census_json() + .unwrap() + .is_none() + ); +} + +#[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] +fn scalar_on_the_fly_native_runtime() -> NativeRuntime { + scalar_on_the_fly_native_runtime_with_color_contraction(None) +} + +#[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] +fn scalar_on_the_fly_native_runtime_with_color_contraction( + color_contraction: Option, +) -> NativeRuntime { + use super::on_the_fly_lane::OnTheFlyNativeRuntime; + use super::on_the_fly_selectors::{ + OnTheFlyCompactSelectorAdapterV1, OnTheFlyLcColorCoverageV1, OnTheFlyLcSelectorPolicyV1, + }; + use super::recurrence_backend::on_the_fly_adapter_tests::{ + complete_scalar_direct_catalog, complete_scalar_prepared_pool, digest, source_domains, + }; + use crate::recurrence::CheckedTableRange; + use crate::recurrence::on_the_fly::scalar_adapter_test_seed; + use crate::recurrence::template::{CouplingOrderTermRow, IndexedRangeRow}; + use crate::recurrence::validated_template_fixture; + + let mut template_input = validated_template_fixture().into_input(); + let spin_start = u64::try_from(template_input.i32_sequence_values.len()).unwrap(); + let spin_sequence_id = u32::try_from(template_input.i32_sequence_ranges.len()).unwrap(); + template_input.i32_sequence_ranges.push(IndexedRangeRow { + id: spin_sequence_id, + range: CheckedTableRange::new(spin_start, 2), + }); + template_input.i32_sequence_values.extend([50_000, 50_000]); + template_input.quantum_flows[0].input_spin_sequence_id = spin_sequence_id; + template_input.coupling_order_ranges.push(IndexedRangeRow { + id: 1, + range: CheckedTableRange::new(0, 1), + }); + template_input + .coupling_order_terms + .push(CouplingOrderTermRow { + set_id: 1, + name_string_id: 0, + power: 1, + }); + let templates = template_input.validate().unwrap(); + let summary = templates.summary(); + let direct_digest = digest(40); + let direct = complete_scalar_direct_catalog(direct_digest); + let seed = scalar_adapter_test_seed( + summary.compiled_model_digest, + summary.catalog_digest, + summary.prepared_kernel_pack_digest, + direct_digest, + ) + .and_then(|seed| seed.with_selector_local_zero()) + .unwrap(); + let pool = complete_scalar_prepared_pool(&templates, direct_digest); + let sources = pool.bind_source_domains(source_domains()).unwrap(); + let resolver = pool.into_on_the_fly_resolver(sources); + let defaults = vec![ + crate::EagerComplex64::new(0.0, 0.0); + usize::try_from(summary.parameter_count).unwrap() + ]; + let selectors = OnTheFlyCompactSelectorAdapterV1::from_seed( + &seed, + OnTheFlyLcSelectorPolicyV1 { + color_coverage: OnTheFlyLcColorCoverageV1::Complete, + color_basis: Default::default(), + reference_color_word: None, + trace_reflections_folded: false, + }, + ) + .unwrap(); + let lane = OnTheFlyNativeRuntime::new( + templates, + direct, + seed, + resolver, + 1, + 1, + defaults, + Vec::new(), + &[], + ) + .unwrap(); + let mut execution = empty_generic_runtime(); + execution.external_count = 2; + execution.external_pdg_order = vec![900_000, 900_000]; + execution.external_is_initial = vec![true, false]; + execution.physics = None; + execution.normalization_factor = 1.0; + NativeRuntime { + root: PathBuf::new(), + artifact_id: "0".repeat(64), + runtime: execution, + execution_lane: NativeExecutionLane::OnTheFly(Box::new( + native_runtime::OnTheFlyExecutionRuntime::new(lane, selectors, color_contraction), + )), + process: "s > s".to_string(), + process_key: "s_to_s".to_string(), + representative_process_id: "s_to_s".to_string(), + external_permutation: vec![0, 1], + input_crossing_map: None, + permutation_alias_of: None, + final_state_permutation_alias_of: None, + physics_v1: native_runtime::LazyProcessPhysicsV1::unavailable(), + warnings_muted: false, + warned_kinds: BTreeSet::new(), + pending_warnings: Vec::new(), + point_selector_scratch: PointSelectorExecutionScratch::default(), + selector_simd_lane_width: 1, + correlated: None, + process_group: None, + } +} + +#[cfg(all( + any(feature = "f64-compiled", feature = "f64-symjit"), + any(target_os = "linux", target_os = "macos") +))] +#[test] +fn on_the_fly_warmed_fixed_selection_is_allocation_free_and_last_family_only() { + let mut runtime = scalar_on_the_fly_native_runtime(); + let point_count = 2; + let momenta = vec![0.0; point_count * 2 * 4]; + let color = ["flow:singlet".to_string()]; + let selection_a = ["h:+0,+0".to_string()]; + let selection_b = ["h:+1,+1".to_string()]; + let mut output = vec![0.0; point_count]; + + runtime + .evaluate_f64_into_with_selectors( + &momenta, + point_count, + Some(&selection_a), + Some(&color), + None, + None, + &mut output, + ) + .unwrap(); + assert!(output.iter().all(|value| value.abs() > f64::EPSILON)); + let expected_a = output.clone(); + + let (warm_a, allocations, bytes) = super::count_allocations(|| { + runtime.evaluate_f64_into_with_selectors( + &momenta, + point_count, + Some(&selection_a), + Some(&color), + None, + None, + &mut output, + ) + }); + warm_a.unwrap(); + assert_eq!(output, expected_a); + assert_eq!(allocations, 0, "warmed fixed OTF selection allocated"); + assert_eq!(bytes, 0, "warmed fixed OTF selection allocated bytes"); + + let (cold_b, allocations, _) = super::count_allocations(|| { + runtime.evaluate_f64_into_with_selectors( + &momenta, + point_count, + Some(&selection_b), + Some(&color), + None, + None, + &mut output, + ) + }); + cold_b.unwrap(); + assert!(allocations > 0, "replacement family was not prepared cold"); + assert_eq!(output, [0.0; 2]); + + let (warm_b, allocations, bytes) = super::count_allocations(|| { + runtime.evaluate_f64_into_with_selectors( + &momenta, + point_count, + Some(&selection_b), + Some(&color), + None, + None, + &mut output, + ) + }); + warm_b.unwrap(); + assert_eq!(allocations, 0, "warmed replacement OTF selection allocated"); + assert_eq!(bytes, 0, "warmed replacement OTF selection allocated bytes"); + + // Only the current family is retained. Returning to A must rebuild it, + // after which the same A selection is allocation-free again. + let (revisited_a, allocations, _) = super::count_allocations(|| { + runtime.evaluate_f64_into_with_selectors( + &momenta, + point_count, + Some(&selection_a), + Some(&color), + None, + None, + &mut output, + ) + }); + revisited_a.unwrap(); + assert!( + allocations > 0, + "evicted fixed OTF selection was not rebuilt" + ); + assert_eq!(output, expected_a); + + let (warm_revisited_a, allocations, bytes) = super::count_allocations(|| { + runtime.evaluate_f64_into_with_selectors( + &momenta, + point_count, + Some(&selection_a), + Some(&color), + None, + None, + &mut output, + ) + }); + warm_revisited_a.unwrap(); + assert_eq!(allocations, 0, "rewarmed fixed OTF selection allocated"); + assert_eq!(bytes, 0, "rewarmed fixed OTF selection allocated bytes"); + let NativeExecutionLane::OnTheFly(execution) = &runtime.execution_lane else { + panic!("test runtime changed execution lane"); + }; + assert_eq!(execution.retained_family_count(), 1); + assert_eq!(execution.retained_selection_count(), 1); +} + +#[cfg(all( + any(feature = "f64-compiled", feature = "f64-symjit"), + any(target_os = "linux", target_os = "macos") +))] +#[test] +fn on_the_fly_unprofiled_lc_and_contracted_match_profiles_and_allocate_nothing() { + for contracted in [false, true] { + let contraction = contracted.then(|| { + super::on_the_fly_load::LoadedOnTheFlyColorContractionV1 { + plan: crate::recurrence::RecurrenceColorContraction::expanded_identity_for_runtime_test(), + destination_by_owner_ordinal: vec![0].into_boxed_slice(), + point_tile_size: 1, + } + }); + let mut runtime = scalar_on_the_fly_native_runtime_with_color_contraction(contraction); + if contracted { + runtime.runtime.color_accuracy = "full".to_string(); + } + let label = if contracted { "contracted" } else { "LC" }; + let point_count = 2; + let momenta = vec![0.0; point_count * 2 * 4]; + let helicities = ["h:+0,+0".to_string()]; + let color = ["flow:singlet".to_string()]; + let colors = (!contracted).then_some(color.as_slice()); + let mut output = vec![f64::NAN; point_count]; + + runtime + .evaluate_f64_into_with_selectors( + &momenta, + point_count, + Some(&helicities), + colors, + None, + None, + &mut output, + ) + .unwrap(); + let expected = output.clone(); + let profiled = runtime + .evaluate_f64_profile(&momenta, point_count, Some(&helicities), colors) + .unwrap(); + assert!( + profiled + .values + .iter() + .zip(&expected) + .all(|(candidate, expected)| candidate.to_bits() == expected.to_bits()), + "unprofiled {label} values differ bitwise from the diagnostic profile" + ); + assert!( + profiled.profile.total_s.is_finite() && profiled.profile.total_s >= 0.0, + "{label} profile produced invalid timing" + ); + assert!( + profiled.profile.recurrence_schedule_execution_count > 0, + "{label} profile lost schedule counts" + ); + assert!( + profiled.profile.recurrence_source_call_count > 0, + "{label} profile lost source-call counts" + ); + assert!( + profiled.profile.recurrence_closure_call_count > 0, + "{label} profile lost closure-call counts" + ); + let NativeExecutionLane::OnTheFly(execution) = &runtime.execution_lane else { + panic!("test runtime changed execution lane"); + }; + let selected_census = execution.active_family_prepared_census().unwrap(); + assert_eq!( + selected_census.query_count, 1, + "{label} evaluated unselected helicities" + ); + assert_eq!(selected_census.union_amplitude_destination_count, 1); + + runtime + .evaluate_f64_into_with_selectors( + &momenta, + point_count, + Some(&helicities), + colors, + None, + None, + &mut output, + ) + .unwrap(); + let (result, allocations, bytes) = super::count_allocations(|| { + runtime.evaluate_f64_into_with_selectors( + &momenta, + point_count, + Some(&helicities), + colors, + None, + None, + &mut output, + ) + }); + result.unwrap(); + assert_eq!(output, expected, "warmed {label} values changed"); + assert_eq!(allocations, 0, "warmed {label} OTF evaluation allocated"); + assert_eq!(bytes, 0, "warmed {label} OTF evaluation allocated bytes"); + } +} + +#[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] +#[test] +fn on_the_fly_runtime_state_census_is_observational() { + let retained_state = |runtime: &NativeRuntime| { + let NativeExecutionLane::OnTheFly(lane) = &runtime.execution_lane else { + panic!("test runtime changed execution lane"); + }; + ( + lane.retained_family_count(), + lane.retained_selection_count(), + lane.semantic_executor_binding_count().unwrap(), + ) + }; + let census = |runtime: &NativeRuntime| { + runtime + .on_the_fly_runtime_state_census_json() + .unwrap() + .expect("on-the-fly runtime census is absent") + }; + + let mut runtime = scalar_on_the_fly_native_runtime(); + let metadata = runtime.metadata(); + assert_eq!( + metadata.on_the_fly_requested_query_construction_threads, + Some(1) + ); + assert_eq!( + metadata.on_the_fly_effective_query_construction_threads, + Some(1) + ); + let cold_state = retained_state(&runtime); + let cold_census = census(&runtime); + assert_eq!(census(&runtime), cold_census); + assert_eq!(retained_state(&runtime), cold_state); + + let point_count = 2; + let momenta = vec![0.0; point_count * 2 * 4]; + let first = runtime + .evaluate_f64_with_selectors(&momenta, point_count, None, None, None, None) + .unwrap(); + let warm_state = retained_state(&runtime); + let warm_census = census(&runtime); + assert_eq!(census(&runtime), warm_census); + assert_eq!(retained_state(&runtime), warm_state); + + let repeated = runtime + .evaluate_f64_with_selectors(&momenta, point_count, None, None, None, None) + .unwrap(); + assert_eq!(repeated, first); + assert_eq!(census(&runtime), warm_census); + assert_eq!(retained_state(&runtime), warm_state); +} + +#[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] +#[test] +fn on_the_fly_contracted_color_selectors_fail_at_the_common_public_boundary() { + let mut runtime = scalar_on_the_fly_native_runtime(); + // Selector capability validation is intentionally lane-neutral and keyed + // by authenticated public accuracy. No contracted request may reach the + // compact structural planner. + runtime.runtime.color_accuracy = "full".to_string(); + let momenta = vec![0.0; 2 * 4]; + let color_by_point = [0_u32]; + let error = runtime + .evaluate_f64_with_selectors(&momenta, 1, None, None, None, Some(&color_by_point)) + .unwrap_err(); + assert!(error.to_string().contains("only for LC")); + + let color_ids = ["color:contracted".to_string()]; + let error = runtime.resolved_shape(None, Some(&color_ids)).unwrap_err(); + assert!(error.to_string().contains("only for LC")); +} + +#[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] +#[test] +fn on_the_fly_contracted_later_tile_failure_clears_both_selection_caches() { + let contraction = super::on_the_fly_load::LoadedOnTheFlyColorContractionV1 { + plan: crate::recurrence::RecurrenceColorContraction::expanded_identity_for_runtime_test(), + destination_by_owner_ordinal: vec![0].into_boxed_slice(), + point_tile_size: 1, + }; + let mut runtime = scalar_on_the_fly_native_runtime_with_color_contraction(Some(contraction)); + runtime.runtime.color_accuracy = "full".to_string(); + let point_count = 2; + let momenta = vec![0.0; point_count * 2 * 4]; + let selection_a = ["h:+0,+0".to_string()]; + let selection_b = ["h:+1,+1".to_string()]; + + let a_before = runtime + .evaluate_f64_with_selectors(&momenta, point_count, Some(&selection_a), None, None, None) + .unwrap(); + let NativeExecutionLane::OnTheFly(execution) = &mut runtime.execution_lane else { + panic!("test runtime changed execution lane"); + }; + assert_eq!(execution.retained_family_count(), 1); + assert_eq!(execution.retained_selection_count(), 1); + // Selection A is retained on both sides. For replacement B, attempt zero + // commits B's cold family on tile one; attempt one fails before tile two, + // while B's public wrapper selection is still pending. + execution.fail_contracted_execution_at_for_test(Some(1)); + + let error = runtime + .evaluate_f64_with_selectors(&momenta, point_count, Some(&selection_b), None, None, None) + .unwrap_err(); + assert!(error.to_string().contains("injected")); + + let NativeExecutionLane::OnTheFly(execution) = &mut runtime.execution_lane else { + panic!("test runtime changed execution lane"); + }; + assert_eq!(execution.retained_family_count(), 0); + assert_eq!(execution.retained_selection_count(), 0); + execution.fail_contracted_execution_at_for_test(None); + + let a_after = runtime + .evaluate_f64_with_selectors(&momenta, point_count, Some(&selection_a), None, None, None) + .unwrap(); + assert_eq!(a_after, a_before); + let NativeExecutionLane::OnTheFly(execution) = &runtime.execution_lane else { + panic!("test runtime changed execution lane"); + }; + assert_eq!(execution.retained_family_count(), 1); + assert_eq!(execution.retained_selection_count(), 1); +} + +#[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] +#[test] +fn on_the_fly_contracted_streaming_bounds_live_outcomes_and_retains_one_family() { + let contraction = super::on_the_fly_load::LoadedOnTheFlyColorContractionV1 { + plan: crate::recurrence::RecurrenceColorContraction::expanded_identity_for_runtime_test(), + destination_by_owner_ordinal: vec![0].into_boxed_slice(), + point_tile_size: 1, + }; + let mut runtime = scalar_on_the_fly_native_runtime_with_color_contraction(Some(contraction)); + runtime.runtime.color_accuracy = "full".to_string(); + let momenta = vec![0.0; 2 * 4]; + + let values = runtime + .evaluate_f64_with_selectors(&momenta, 1, None, None, None, None) + .unwrap(); + assert_eq!(values.len(), 1); + let NativeExecutionLane::OnTheFly(execution) = &runtime.execution_lane else { + panic!("test runtime changed execution lane"); + }; + // The complete scalar selector domain contains four helicities. With a + // one-thread construction pool, only one query outcome may be alive at a + // time even though the complete H x S projection remains reusable. + assert_eq!(execution.contracted_max_live_query_outcomes_for_test(), 1); + assert_eq!(execution.retained_family_count(), 1); + assert_eq!(execution.retained_selection_count(), 1); + assert!(execution.active_family_prepared_census().is_some()); +} + +#[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] +#[test] +fn on_the_fly_warm_up_reports_ordered_phases_and_reuses_the_committed_family() { + let contraction = super::on_the_fly_load::LoadedOnTheFlyColorContractionV1 { + plan: crate::recurrence::RecurrenceColorContraction::expanded_identity_for_runtime_test(), + destination_by_owner_ordinal: vec![0].into_boxed_slice(), + point_tile_size: 1, + }; + let mut runtime = scalar_on_the_fly_native_runtime_with_color_contraction(Some(contraction)); + runtime.runtime.color_accuracy = "full".to_string(); + let momenta = vec![0.0; 2 * 4]; + let mut events = Vec::new(); + let mut observer = |event: &NativeOnTheFlyWarmUpEvent| { + assert_eq!(event.schema_version, 1); + assert!(event.completed <= event.total); + assert!(event.elapsed_seconds.is_finite() && event.elapsed_seconds >= 0.0); + events.push((event.kind, event.stage, event.completed, event.total)); + Ok(true) + }; + let observer: &mut NativeOnTheFlyWarmUpObserver<'_> = &mut observer; + let first = runtime + .warm_up_on_the_fly_f64_with_selectors(&momenta, None, None, None, Some(observer)) + .unwrap(); + assert!(!first.already_warm); + assert!(first.first_evaluation_completed); + assert_eq!(first.query_count, 4); + assert_eq!(first.warmed_query_count, 4); + assert_eq!( + events + .iter() + .map(|(_, stage, _, _)| *stage) + .collect::>(), + BTreeSet::from([ + NativeOnTheFlyWarmUpStage::ProcessPreparation, + NativeOnTheFlyWarmUpStage::QueryFamily, + NativeOnTheFlyWarmUpStage::FamilyFinalization, + NativeOnTheFlyWarmUpStage::FirstEvaluation, + ]) + ); + let stage_ordinals = events + .iter() + .map(|(_, stage, _, _)| match stage { + NativeOnTheFlyWarmUpStage::ProcessPreparation => 0, + NativeOnTheFlyWarmUpStage::QueryFamily => 1, + NativeOnTheFlyWarmUpStage::FamilyFinalization => 2, + NativeOnTheFlyWarmUpStage::FirstEvaluation => 3, + }) + .collect::>(); + assert!(stage_ordinals.windows(2).all(|pair| pair[0] <= pair[1])); + + let repeated = runtime + .warm_up_on_the_fly_f64_with_selectors(&momenta, None, None, None, None) + .unwrap(); + assert!(repeated.already_warm); + assert_eq!(repeated.query_count, first.query_count); + assert_eq!(repeated.warmed_query_count, 0); + let values = runtime + .evaluate_f64_with_selectors(&momenta, 1, None, None, None, None) + .unwrap(); + assert_eq!(values.len(), 1); + assert!(values[0].is_finite()); +} + +#[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] +#[test] +fn on_the_fly_lc_warm_up_streams_intermediate_progress_and_matches_ordinary_evaluation() { + let momenta = vec![0.0; 2 * 4]; + let mut warmed = scalar_on_the_fly_native_runtime(); + let mut events = Vec::new(); + let mut observer = |event: &NativeOnTheFlyWarmUpEvent| { + events.push(event.clone()); + Ok(true) + }; + let observer: &mut NativeOnTheFlyWarmUpObserver<'_> = &mut observer; + let result = warmed + .warm_up_on_the_fly_f64_with_selectors(&momenta, None, None, None, Some(observer)) + .unwrap(); + assert!(result.query_count > 1); + assert!(events.iter().any(|event| { + event.kind == NativeOnTheFlyWarmUpEventKind::Update + && event.stage == NativeOnTheFlyWarmUpStage::QueryFamily + && event.completed > 0 + && event.completed < event.total + })); + + let mut per_stage = BTreeMap::::new(); + for event in &events { + assert!(event.completed <= event.total); + let (total, last_completed) = per_stage + .entry(event.stage) + .or_insert((event.total, event.completed)); + assert_eq!(*total, event.total, "warm-up stage total changed"); + assert!( + event.completed >= *last_completed, + "warm-up progress regressed" + ); + *last_completed = event.completed; + } + + let warmed_value = warmed + .evaluate_f64_with_selectors(&momenta, 1, None, None, None, None) + .unwrap(); + let mut ordinary = scalar_on_the_fly_native_runtime(); + let ordinary_value = ordinary + .evaluate_f64_with_selectors(&momenta, 1, None, None, None, None) + .unwrap(); + assert_eq!(warmed_value.len(), ordinary_value.len()); + assert!( + warmed_value + .iter() + .zip(&ordinary_value) + .all(|(warm, ordinary)| warm.to_bits() == ordinary.to_bits()) + ); +} + +#[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] +#[test] +fn on_the_fly_warm_up_core_override_preserves_nonzero_results_and_cache() { + for color_accuracy in ["lc", "nlc", "full"] { + let make_runtime = || { + let contraction = (color_accuracy != "lc").then(|| { + super::on_the_fly_load::LoadedOnTheFlyColorContractionV1 { + plan: crate::recurrence::RecurrenceColorContraction::expanded_identity_for_runtime_test(), + destination_by_owner_ordinal: vec![0].into_boxed_slice(), + point_tile_size: 1, + } + }); + let mut runtime = scalar_on_the_fly_native_runtime_with_color_contraction(contraction); + runtime.runtime.color_accuracy = color_accuracy.to_string(); + runtime + }; + let momenta = [0.0; 2 * 4]; + let mut serial = make_runtime(); + serial + .warm_up_f64(&momenta, None, None, Some(1), None) + .unwrap(); + let expected = serial + .evaluate_f64_with_selectors(&momenta, 1, None, None, None, None) + .unwrap(); + assert!( + expected + .iter() + .all(|value| value.is_finite() && *value > 0.0) + ); + + let mut parallel = make_runtime(); + let effective = super::on_the_fly_load::effective_query_construction_threads(4); + let caller = std::thread::current().id(); + let mut events = Vec::new(); + let mut observer = |event: &NativeOnTheFlyWarmUpEvent| { + assert_eq!(std::thread::current().id(), caller); + assert_eq!(event.workers, effective as u64); + events.push(event.clone()); + Ok(true) + }; + let first = parallel + .warm_up_f64(&momenta, None, None, Some(4), Some(&mut observer)) + .unwrap(); + assert!(!first.already_warm); + assert_eq!(first.query_count, 4); + assert!(!events.is_empty()); + let actual = parallel + .evaluate_f64_with_selectors(&momenta, 1, None, None, None, None) + .unwrap(); + assert_eq!(actual, expected); + let NativeExecutionLane::OnTheFly(serial_lane) = &serial.execution_lane else { + panic!("test runtime changed execution lane"); + }; + let NativeExecutionLane::OnTheFly(parallel_lane) = ¶llel.execution_lane else { + panic!("test runtime changed execution lane"); + }; + assert_eq!( + parallel_lane.active_family_prepared_census(), + serial_lane.active_family_prepared_census(), + ); + if color_accuracy != "lc" { + assert_eq!( + parallel_lane.contracted_max_live_query_outcomes_for_test(), + effective.min(4), + ); + } + let metadata = parallel.metadata(); + assert_eq!( + metadata.on_the_fly_requested_query_construction_threads, + Some(1) + ); + assert_eq!( + metadata.on_the_fly_effective_query_construction_threads, + Some(1) + ); + + // A different override and then the process default must both reuse + // the exact committed family, rather than reconstructing it. + for n_cores in [Some(1), None] { + let mut observer = |event: &NativeOnTheFlyWarmUpEvent| { + assert_eq!(event.workers, 1); + Ok(true) + }; + let repeated = parallel + .warm_up_f64(&momenta, None, None, n_cores, Some(&mut observer)) + .unwrap(); + assert!(repeated.already_warm); + assert_eq!(repeated.warmed_query_count, 0); + } + } +} + +#[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] +#[test] +fn on_the_fly_warm_up_rejects_zero_cores_without_changing_cache() { + let mut runtime = scalar_on_the_fly_native_runtime(); + let momenta = [0.0; 2 * 4]; + let cold = runtime.on_the_fly_runtime_state_census_json().unwrap(); + let error = runtime + .warm_up_f64(&momenta, None, None, Some(0), None) + .unwrap_err(); + assert_eq!(error.kind(), crate::RusticolErrorKind::InvalidArgument); + assert!(error.to_string().contains("n_cores must be positive")); + assert_eq!( + runtime.on_the_fly_runtime_state_census_json().unwrap(), + cold + ); + + runtime + .warm_up_f64(&momenta, None, None, Some(2), None) + .unwrap(); + let warm = runtime.on_the_fly_runtime_state_census_json().unwrap(); + let error = runtime + .warm_up_f64(&momenta, None, None, Some(0), None) + .unwrap_err(); + assert_eq!(error.kind(), crate::RusticolErrorKind::InvalidArgument); + assert_eq!( + runtime.on_the_fly_runtime_state_census_json().unwrap(), + warm + ); + let repeated = runtime + .warm_up_f64(&momenta, None, None, None, None) + .unwrap(); + assert!(repeated.already_warm); + assert_eq!(repeated.warmed_query_count, 0); +} + +#[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] +#[test] +fn on_the_fly_contracted_midstream_warm_up_cancellation_rolls_back_to_retained_selection() { + let contraction = super::on_the_fly_load::LoadedOnTheFlyColorContractionV1 { + plan: crate::recurrence::RecurrenceColorContraction::expanded_identity_for_runtime_test(), + destination_by_owner_ordinal: vec![0].into_boxed_slice(), + point_tile_size: 1, + }; + let mut runtime = scalar_on_the_fly_native_runtime_with_color_contraction(Some(contraction)); + runtime.runtime.color_accuracy = "full".to_string(); + let momenta = vec![0.0; 2 * 4]; + let retained_helicity = ["h:+0,+0".to_string()]; + runtime + .warm_up_on_the_fly_f64_with_selectors(&momenta, Some(&retained_helicity), None, None, None) + .unwrap(); + let expected = runtime + .evaluate_f64_with_selectors(&momenta, 1, Some(&retained_helicity), None, None, None) + .unwrap(); + + let saw_midstream = std::cell::Cell::new(false); + let mut observer = |event: &NativeOnTheFlyWarmUpEvent| { + if event.kind == NativeOnTheFlyWarmUpEventKind::Update + && event.stage == NativeOnTheFlyWarmUpStage::QueryFamily + && event.completed < event.total + { + saw_midstream.set(true); + return Ok(false); + } + Ok(true) + }; + let observer: &mut NativeOnTheFlyWarmUpObserver<'_> = &mut observer; + let error = runtime + .warm_up_on_the_fly_f64_with_selectors(&momenta, None, None, Some(2), Some(observer)) + .unwrap_err(); + assert!(error.to_string().contains("cancelled")); + assert!(saw_midstream.get()); + let retained = runtime + .evaluate_f64_with_selectors(&momenta, 1, Some(&retained_helicity), None, None, None) + .unwrap(); + assert_eq!(retained, expected); + let NativeExecutionLane::OnTheFly(execution) = &runtime.execution_lane else { + panic!("test runtime changed execution lane"); + }; + assert_eq!(execution.retained_family_count(), 1); + assert_eq!(execution.retained_selection_count(), 1); + assert_eq!(execution.pending_family_count(), 0); +} + +#[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] +#[test] +fn on_the_fly_warm_up_terminal_notification_is_non_cancellable_after_commit() { + let momenta = vec![0.0; 2 * 4]; + let mut runtime = scalar_on_the_fly_native_runtime(); + let terminal_delivery_count = std::cell::Cell::new(0); + let mut failing_terminal = |event: &NativeOnTheFlyWarmUpEvent| { + if event.kind == NativeOnTheFlyWarmUpEventKind::End + && event.stage == NativeOnTheFlyWarmUpStage::FirstEvaluation + { + terminal_delivery_count.set(terminal_delivery_count.get() + 1); + return Err(RusticolError::evaluation( + "injected terminal notification failure", + )); + } + Ok(true) + }; + let observer: &mut NativeOnTheFlyWarmUpObserver<'_> = &mut failing_terminal; + let first = runtime + .warm_up_on_the_fly_f64_with_selectors(&momenta, None, None, None, Some(observer)) + .unwrap(); + assert!(first.first_evaluation_completed); + assert_eq!(terminal_delivery_count.get(), 1); + + let mut rejecting_terminal = |event: &NativeOnTheFlyWarmUpEvent| { + Ok(!(event.kind == NativeOnTheFlyWarmUpEventKind::End + && event.stage == NativeOnTheFlyWarmUpStage::FirstEvaluation)) + }; + let observer: &mut NativeOnTheFlyWarmUpObserver<'_> = &mut rejecting_terminal; + let repeated = runtime + .warm_up_on_the_fly_f64_with_selectors(&momenta, None, None, None, Some(observer)) + .unwrap(); + assert!(repeated.already_warm); + assert_eq!(repeated.warmed_query_count, 0); +} + +#[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] +#[test] +fn on_the_fly_warm_up_native_boundary_rejects_invalid_lane_and_selectors() { + let mut compiled = zero_native_runtime(); + let compiled_momenta = vec![0.0; compiled.external_count() * 4]; + let error = compiled + .warm_up_on_the_fly_f64_with_selectors(&compiled_momenta, None, None, None, None) + .unwrap_err(); + assert_eq!( + error.kind(), + crate::RusticolErrorKind::UnsupportedRuntimeCapability + ); + + let contraction = super::on_the_fly_load::LoadedOnTheFlyColorContractionV1 { + plan: crate::recurrence::RecurrenceColorContraction::expanded_identity_for_runtime_test(), + destination_by_owner_ordinal: vec![0].into_boxed_slice(), + point_tile_size: 1, + }; + let mut contracted = scalar_on_the_fly_native_runtime_with_color_contraction(Some(contraction)); + contracted.runtime.color_accuracy = "full".to_string(); + let momenta = vec![0.0; contracted.external_count() * 4]; + let color = ["color:contracted".to_string()]; + let error = contracted + .warm_up_on_the_fly_f64_with_selectors(&momenta, None, Some(&color), None, None) + .unwrap_err(); + assert_eq!(error.kind(), crate::RusticolErrorKind::Selector); + + let mut lc = scalar_on_the_fly_native_runtime(); + let unknown_helicity = ["h:not-a-helicity".to_string()]; + let error = lc + .warm_up_on_the_fly_f64_with_selectors(&momenta, Some(&unknown_helicity), None, None, None) + .unwrap_err(); + assert_eq!(error.kind(), crate::RusticolErrorKind::Selector); +} + +#[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] +#[test] +fn on_the_fly_warm_up_cancellation_preserves_the_last_committed_selection() { + let mut runtime = scalar_on_the_fly_native_runtime(); + let momenta = vec![0.0; 2 * 4]; + let color = ["flow:singlet".to_string()]; + let selection_a = ["h:+0,+0".to_string()]; + let selection_b = ["h:+1,+1".to_string()]; + runtime + .warm_up_on_the_fly_f64_with_selectors( + &momenta, + Some(&selection_a), + Some(&color), + None, + None, + ) + .unwrap(); + let expected = runtime + .evaluate_f64_with_selectors(&momenta, 1, Some(&selection_a), Some(&color), None, None) + .unwrap(); + + let mut observer = |event: &NativeOnTheFlyWarmUpEvent| { + Ok(!(event.kind == NativeOnTheFlyWarmUpEventKind::Start + && event.stage == NativeOnTheFlyWarmUpStage::QueryFamily)) + }; + let observer: &mut NativeOnTheFlyWarmUpObserver<'_> = &mut observer; + let error = runtime + .warm_up_on_the_fly_f64_with_selectors( + &momenta, + Some(&selection_b), + Some(&color), + None, + Some(observer), + ) + .unwrap_err(); + assert!(error.to_string().contains("cancelled")); + let retained = runtime + .evaluate_f64_with_selectors(&momenta, 1, Some(&selection_a), Some(&color), None, None) + .unwrap(); + assert_eq!(retained, expected); + let NativeExecutionLane::OnTheFly(execution) = &runtime.execution_lane else { + panic!("test runtime changed execution lane"); + }; + assert_eq!(execution.retained_family_count(), 1); + assert_eq!(execution.retained_selection_count(), 1); +} + +#[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] +#[test] +fn on_the_fly_warm_up_rejects_more_than_one_point() { + let mut runtime = scalar_on_the_fly_native_runtime(); + let error = runtime + .warm_up_on_the_fly_f64_with_selectors(&[0.0; 2 * 2 * 4], None, None, None, None) + .unwrap_err(); + assert!(error.to_string().contains("exactly one point")); +} + +#[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] +#[test] +fn on_the_fly_public_paths_vectorize_with_a_last_family_only_cache() { + let retained = |runtime: &NativeRuntime| { + let NativeExecutionLane::OnTheFly(lane) = &runtime.execution_lane else { + panic!("test runtime changed execution lane"); + }; + ( + lane.retained_family_count(), + lane.retained_selection_count(), + lane.semantic_executor_binding_count().unwrap(), + ) + }; + let census = |runtime: &NativeRuntime| -> Value { + serde_json::from_str( + runtime + .on_the_fly_runtime_state_census_json() + .unwrap() + .as_deref() + .expect("on-the-fly runtime census is absent"), + ) + .unwrap() + }; + let prepared_census = |runtime: &NativeRuntime| { + let NativeExecutionLane::OnTheFly(lane) = &runtime.execution_lane else { + panic!("test runtime changed execution lane"); + }; + lane.active_family_prepared_census() + .expect("active on-the-fly family has no prepared census") + }; + + let mut runtime = scalar_on_the_fly_native_runtime(); + let point_count = 6; + let momenta = vec![0.0; point_count * 2 * 4]; + let cold_census = census(&runtime); + assert_eq!( + cold_census["kind"], + "rusticol-on-the-fly-runtime-state-census-v1" + ); + assert_eq!(cold_census["process_id"], "s_to_s"); + assert_eq!(cold_census["family_cache_policy"], "last-family-only"); + assert_eq!(cold_census["family_cache_limit"], 1); + assert_eq!(cold_census["process_preparation_count"], 0); + assert_eq!(cold_census["retained_family_count"], 0); + assert_eq!(cold_census["pending_family_count"], 0); + assert_eq!(cold_census["retained_selection_count"], 0); + assert_eq!(cold_census["semantic_executor_binding_count"], 0); + assert!(cold_census["active_family_union_census"].is_null()); + + // A: the complete compact selector domain. Only its first helicity is + // executable; the other three are authenticated selector-local zeros. + let global = runtime + .evaluate_f64_with_selectors(&momenta, point_count, None, None, None, None) + .unwrap(); + assert_eq!(global.len(), point_count); + assert!( + global + .iter() + .all(|value| value.is_finite() && value.abs() > f64::EPSILON) + ); + let (family_count, selection_count, binding_count) = retained(&runtime); + assert_eq!((family_count, selection_count), (1, 1)); + assert!(binding_count > 0); + let global_census = census(&runtime); + assert_eq!(global_census["process_preparation_count"], 1); + assert_eq!(global_census["retained_family_count"], 1); + assert_eq!(global_census["pending_family_count"], 0); + assert_eq!(global_census["retained_selection_count"], 1); + assert_eq!(global_census["retained_request_count"], 4); + assert_eq!(global_census["retained_amplitude_destination_count"], 1); + assert_eq!( + global_census["retained_request_count"].as_u64().unwrap() + - global_census["retained_amplitude_destination_count"] + .as_u64() + .unwrap(), + 3 + ); + let active = &global_census["active_family_union_census"]; + assert_eq!(active["basis"], "shared-query-family-union-v1"); + assert_eq!(active["scope"], "active-family-union"); + let prepared = prepared_census(&runtime); + macro_rules! assert_mapped { + ($($field:ident),+ $(,)?) => { + $(assert_eq!( + active[stringify!($field)].as_u64(), + Some(u64::from(prepared.$field)), + stringify!($field), + );)+ + }; + } + assert_mapped!( + query_count, + union_unique_current_count, + union_unique_current_component_count, + union_source_rows, + union_contribution_rows, + union_finalization_rows, + union_closure_rows, + union_amplitude_destination_count, + union_source_executor_call_groups, + union_contribution_executor_call_groups, + union_finalization_executor_call_groups, + union_closure_executor_call_groups, + ); + assert!(prepared.union_unique_current_count <= prepared.union_unique_current_component_count); + for role in ["source", "contribution", "finalization", "closure"] { + let groups = active[format!("union_{role}_executor_call_groups")] + .as_u64() + .unwrap(); + let rows = active[format!("union_{role}_rows")].as_u64().unwrap(); + assert!(groups <= rows, "{role}"); + } + + // B: one explicit nonzero selector. Switching A -> B -> A retains only + // the current public mapping and lower-lane family; evicted A is cold. + let helicity = vec!["h:+0,+0".to_string()]; + let color = vec!["flow:singlet".to_string()]; + let selected = runtime + .evaluate_f64_with_selectors( + &momenta, + point_count, + Some(&helicity), + Some(&color), + None, + None, + ) + .unwrap(); + assert_eq!(selected, global); + assert_eq!(retained(&runtime).0, 1); + assert_eq!(retained(&runtime).1, 1); + // The output retains all four physical helicities, but this selected + // call prepares only one query, rather than evaluating all and filtering. + let selected_census = census(&runtime); + assert_eq!(selected_census["retained_request_count"], 1); + assert_eq!(selected_census["retained_amplitude_destination_count"], 1); + assert_eq!( + selected_census["active_family_union_census"]["query_count"], + 1 + ); + + let global_again = runtime + .evaluate_f64_with_selectors(&momenta, point_count, None, None, None, None) + .unwrap(); + assert_eq!(global_again, global); + assert_eq!(retained(&runtime).0, 1); + assert_eq!(retained(&runtime).1, 1); + let revisited_census = census(&runtime); + assert_eq!(revisited_census["process_preparation_count"], 1); + assert_eq!(revisited_census["retained_family_count"], 1); + assert_eq!(revisited_census["pending_family_count"], 0); + assert_eq!(revisited_census["retained_selection_count"], 1); + assert_eq!( + revisited_census["active_family_union_census"], + global_census["active_family_union_census"] + ); + + let resolved = runtime + .evaluate_resolved_f64(&momenta, point_count, None, None) + .unwrap(); + assert_eq!(resolved.shape(), (point_count, 4, 1)); + assert_eq!(resolved.totals(), global); + assert_eq!( + resolved.helicity_ids, + ["h:+0,+0", "h:+0,+1", "h:+1,+0", "h:+1,+1"] + ); + assert_eq!(resolved.color_ids, color); + assert_eq!(retained(&runtime).0, 1); + assert_eq!(retained(&runtime).1, 1); + + // Alternating selectors form two stable three-point partitions. The + // nonzero partition reaches each prepared row group once, while the + // selector-local-zero partition remains exactly zero. + let helicity_by_point = [0, 3, 0, 3, 0, 3]; + let color_by_point = [0; 6]; + let expected_partitioned = global + .iter() + .enumerate() + .map(|(index, value)| if index % 2 == 0 { *value } else { 0.0 }) + .collect::>(); + let per_point = runtime + .evaluate_f64_with_selectors( + &momenta, + point_count, + None, + None, + Some(&helicity_by_point), + Some(&color_by_point), + ) + .unwrap(); + assert_eq!(per_point, expected_partitioned); + assert_eq!(retained(&runtime).0, 1); + assert_eq!(retained(&runtime).1, 1); + let partitioned_census = census(&runtime); + + let per_point_again = runtime + .evaluate_f64_with_selectors( + &momenta, + point_count, + None, + None, + Some(&helicity_by_point), + Some(&color_by_point), + ) + .unwrap(); + assert_eq!(per_point_again, expected_partitioned); + assert_eq!(retained(&runtime).0, 1); + assert_eq!(retained(&runtime).1, 1); + assert_eq!(census(&runtime), partitioned_census); + + let profiled = runtime + .evaluate_f64_profile_with_selectors( + &momenta, + point_count, + None, + None, + Some(&helicity_by_point), + Some(&color_by_point), + ) + .unwrap(); + assert_eq!(profiled.values, expected_partitioned); + assert_eq!(profiled.profile.selector_plan_kind, "stable-grouped"); + assert_eq!(profiled.profile.selector_group_sizes, [3, 3]); + assert_eq!(profiled.profile.recurrence_schedule_execution_count, 2); + assert_eq!( + profiled.profile.recurrence_momentum_scalar_value_count, + u64::try_from(point_count * 2 * 4).unwrap() + ); + assert!(profiled.profile.recurrence_source_call_count > 0); + assert!(profiled.profile.recurrence_closure_call_count > 0); + assert!(profiled.profile.recurrence_source_call_count < point_count as u64); + assert!(profiled.profile.recurrence_closure_call_count < point_count as u64); + assert!( + runtime + .benchmark_f64_wall_time_with_selectors( + &momenta, + point_count, + 2, + None, + None, + Some(&helicity_by_point), + Some(&color_by_point), + ) + .unwrap() + >= 0.0 + ); + assert_eq!(retained(&runtime).0, 1); + assert_eq!(retained(&runtime).1, 1); + assert_eq!(census(&runtime), partitioned_census); + + assert!(runtime.physics_v1.get().is_err()); + + runtime.clear().unwrap(); + assert_eq!(retained(&runtime), (0, 0, 0)); + assert_eq!(census(&runtime), cold_census); + let NativeExecutionLane::OnTheFly(lane) = &runtime.execution_lane else { + panic!("test runtime changed execution lane"); + }; + assert_eq!(lane.point_major_scratch_state(), (0, 0)); + assert!(runtime.physics_v1.get().is_err()); + let rebuilt = runtime + .evaluate_f64_with_selectors(&momenta, point_count, None, None, None, None) + .unwrap(); + assert_eq!(rebuilt, global); + let (family_count, selection_count, binding_count) = retained(&runtime); + assert_eq!((family_count, selection_count), (1, 1)); + assert!(binding_count > 0); + assert_eq!(census(&runtime), global_census); +} + +#[test] +fn recurrence_selector_plan_is_bound_to_the_public_external_ordering() { + let runtime = zero_native_runtime(); + let plan = NativeRecurrenceSelectorPlan { + artifact_root: runtime.root.clone(), + process_key: runtime.process_key.clone(), + external_permutation: runtime.external_permutation.clone(), + selected_helicities: None, + selected_colors: None, + }; + plan.ensure_matches(&runtime).unwrap(); + + let mut reordered_runtime = zero_native_runtime(); + reordered_runtime.external_permutation = vec![1, 0, 2]; + let error = plan.ensure_matches(&reordered_runtime).unwrap_err(); + assert_eq!(error.kind(), crate::RusticolErrorKind::Selector); + assert!(error.to_string().contains("external ordering")); +} + +#[test] +fn native_kinematics_json_accepts_one_numeric_or_decimal_string_point() { + let direct = serde_json::json!([ + ["10.0000000000000001", 0, 0.0, "10"], + [10, 0, 0, -10], + [20, "0", 0, 0] + ]); + let flat = native_runtime::parse_public_kinematics_point(&direct, 3).unwrap(); + assert_eq!(flat.len(), 12); + assert_eq!(flat[0], 10.0); + assert_eq!(flat[3], 10.0); + assert_eq!(flat[7], -10.0); + assert_eq!(flat[8], 20.0); + + let singleton = serde_json::json!([direct]); + assert_eq!( + native_runtime::parse_public_kinematics_point(&singleton, 3).unwrap(), + flat + ); +} + +#[test] +fn native_kinematics_json_rejects_multiple_points_and_invalid_components() { + let point = serde_json::json!([[1, 0, 0, 1], [1, 0, 0, -1], [2, 0, 0, 0]]); + for invalid in [ + serde_json::json!([point.clone(), point]), + serde_json::json!([[1, 0, 0, 1], [1, 0, 0, -1]]), + serde_json::json!([[1, 0, 0, true], [1, 0, 0, -1], [2, 0, 0, 0]]), + serde_json::json!([[1, 0, 0, "1e400"], [1, 0, 0, -1], [2, 0, 0, 0]]), + serde_json::json!([[1, 0, 1], [1, 0, 0, -1], [2, 0, 0, 0]]), + ] { + let error = native_runtime::parse_public_kinematics_point(&invalid, 3).unwrap_err(); + assert_eq!(error.kind(), crate::RusticolErrorKind::InvalidArgument); + } +} + +#[test] +fn native_f64_into_matches_allocating_wrapper_and_validates_output() { + let point = [ + 10.0, 0.0, 0.0, 10.0, 10.0, 0.0, 0.0, -10.0, 20.0, 0.0, 0.0, 0.0, + ]; + let momenta = point.repeat(4); + let mut runtime = zero_native_runtime(); + let allocated = runtime.evaluate_f64(&momenta, 4).unwrap(); + let mut output = vec![f64::NAN; 4]; + runtime.evaluate_f64_into(&momenta, 4, &mut output).unwrap(); + assert_eq!(output, allocated); + + let helicity_by_point = [0_u32, 1, 2, 0]; + runtime + .evaluate_f64_into_with_selectors( + &momenta, + 4, + None, + None, + Some(&helicity_by_point), + None, + &mut output, + ) + .unwrap(); + assert_eq!(output, vec![0.0; 4]); + + let error = runtime + .evaluate_f64_into(&momenta, 4, &mut [0.0; 3]) + .unwrap_err(); + assert!( + error + .to_string() + .contains("output has length 3, expected 4") + ); + let error = runtime + .evaluate_f64_into(&momenta, 4, &mut [0.0; 5]) + .unwrap_err(); + assert!( + error + .to_string() + .contains("output has length 5, expected 4") + ); +} + +#[test] +fn native_f64_totals_bypass_point_selector_scratch() { + let point = [ + 10.0, 0.0, 0.0, 10.0, 10.0, 0.0, 0.0, -10.0, 20.0, 0.0, 0.0, 0.0, + ]; + let momenta = point.repeat(4); + let mut runtime = zero_native_runtime(); + runtime + .point_selector_scratch + .planner + .build(4, Some(&[0, 1, 0, 1]), None, 3, 1) + .expect("seed point-selector planner"); + let retained_partitions = runtime.point_selector_scratch.planner.partitions().to_vec(); + assert!(!retained_partitions.is_empty()); + + let allocated = runtime + .evaluate_f64(&momenta, 4) + .expect("evaluate owned totals without selectors"); + let mut output = [f64::NAN; 4]; + runtime + .evaluate_f64_into(&momenta, 4, &mut output) + .expect("evaluate borrowed totals without selectors"); + + assert_eq!(output.as_slice(), allocated); + assert_eq!( + runtime.point_selector_scratch.planner.partitions(), + retained_partitions, + "no-selector totals must not clear or rebuild selector planner state" + ); +} + +#[test] +fn current_parameter_values_include_complex_and_derived_slots() { + let mut runtime = empty_generic_runtime(); + runtime.model_parameters = vec![ + GenericRuntimeModelParameterManifest { + name: "aS".into(), + kind: "external_parameter".into(), + parameter_index: 0, + default: 0.118, + pdg: None, + runtime_name: None, + complex_component: None, + }, + GenericRuntimeModelParameterManifest { + name: "derived.real".into(), + kind: "derived_parameter_component".into(), + parameter_index: 1, + default: 1.0, + pdg: None, + runtime_name: Some("derived".into()), + complex_component: Some("real".into()), + }, + GenericRuntimeModelParameterManifest { + name: "derived.imag".into(), + kind: "derived_parameter_component".into(), + parameter_index: 2, + default: 0.0, + pdg: None, + runtime_name: Some("derived".into()), + complex_component: Some("imag".into()), + }, + ]; + runtime.model_parameter_values_f64 = vec![0.101, 2.5, -0.75]; + assert_eq!( + runtime.current_model_parameter_value("aS").unwrap(), + (0.101, 0.0) + ); + assert_eq!( + runtime.current_model_parameter_value("derived").unwrap(), + (2.5, -0.75) + ); + assert!(runtime.current_model_parameter_value("unknown").is_err()); + runtime.model_parameter_values_f64[1] = 7.0; + assert_eq!( + runtime.current_model_parameter_value("derived").unwrap(), + (7.0, -0.75) + ); + runtime.model_parameter_values_f64.truncate(1); + assert!(runtime.current_model_parameter_value("derived").is_err()); +} + +#[test] +fn model_parameter_override_batch_is_atomic() { + let mut runtime = empty_generic_runtime(); + runtime.model_parameter_runtime_slots.insert( + "alpha_s".to_string(), + RuntimeParameterSlots { + real: 0, + imaginary: None, + }, + ); + let invalid_batch = BTreeMap::from([ + ("alpha_s".to_string(), (0.101, 0.0)), + ("unknown.parameter".to_string(), (1.0, 0.0)), + ]); + + let error = runtime + .apply_model_parameter_overrides(&invalid_batch) + .unwrap_err(); + + assert!(error.to_string().contains("unknown.parameter")); + assert_eq!(runtime.model_parameter_values_f64, vec![0.118]); + runtime + .apply_model_parameter_overrides(&BTreeMap::from([("alpha_s".to_string(), (0.101, 0.0))])) + .unwrap(); + assert_eq!(runtime.model_parameter_values_f64, vec![0.101]); +} + +#[test] +fn model_parameter_overrides_are_atomic_across_helicity_lanes() { + let mut runtime = empty_generic_runtime(); + runtime.model_parameter_runtime_slots.insert( + "alpha_s".to_string(), + RuntimeParameterSlots { + real: 0, + imaginary: None, + }, + ); + let mut sum_runtime = empty_generic_runtime(); + sum_runtime.model_parameter_runtime_slots.insert( + "alpha_s".to_string(), + RuntimeParameterSlots { + real: 0, + imaginary: None, + }, + ); + runtime.helicity_sum_runtime = Some(Box::new(sum_runtime)); + + runtime + .apply_model_parameter_overrides(&BTreeMap::from([("alpha_s".to_string(), (0.101, 0.0))])) + .unwrap(); + + assert_eq!(runtime.model_parameter_values_f64, vec![0.101]); + assert_eq!( + runtime + .helicity_sum_runtime + .as_ref() + .unwrap() + .model_parameter_values_f64, + vec![0.101] + ); + + runtime + .helicity_sum_runtime + .as_mut() + .unwrap() + .model_parameter_runtime_slots + .clear(); + let error = runtime + .apply_model_parameter_overrides(&BTreeMap::from([("alpha_s".to_string(), (0.089, 0.0))])) + .unwrap_err(); + + assert!(error.to_string().contains("alpha_s")); + assert_eq!(runtime.model_parameter_values_f64, vec![0.101]); + assert_eq!( + runtime + .helicity_sum_runtime + .as_ref() + .unwrap() + .model_parameter_values_f64, + vec![0.101] + ); +} + +#[test] +fn model_parameter_overrides_are_atomic_across_color_selector_lanes() { + fn runtime_with_alpha_s() -> ExecutionRuntime { + let mut runtime = empty_generic_runtime(); + runtime.model_parameter_runtime_slots.insert( + "alpha_s".to_string(), + RuntimeParameterSlots { + real: 0, + imaginary: None, + }, + ); + runtime + } + + let mut runtime = runtime_with_alpha_s(); + runtime + .color_selector_runtimes + .insert(0, Box::new(runtime_with_alpha_s())); + runtime + .color_selector_runtimes + .insert(1, Box::new(runtime_with_alpha_s())); + + runtime + .apply_model_parameter_overrides(&BTreeMap::from([("alpha_s".to_string(), (0.101, 0.0))])) + .unwrap(); + assert_eq!(runtime.model_parameter_values_f64, vec![0.101]); + assert!( + runtime + .color_selector_runtimes + .values() + .all(|lane| lane.model_parameter_values_f64 == vec![0.101]) + ); + + runtime + .color_selector_runtimes + .get_mut(&1) + .unwrap() + .model_parameter_runtime_slots + .clear(); + let error = runtime + .apply_model_parameter_overrides(&BTreeMap::from([("alpha_s".to_string(), (0.089, 0.0))])) + .unwrap_err(); + + assert!(error.to_string().contains("alpha_s")); + assert_eq!(runtime.model_parameter_values_f64, vec![0.101]); + assert!( + runtime + .color_selector_runtimes + .values() + .all(|lane| lane.model_parameter_values_f64 == vec![0.101]) + ); +} + +#[test] +fn model_parameter_overrides_are_atomic_across_shared_helicity_selector_lanes() { + fn runtime_with_alpha_s() -> ExecutionRuntime { + let mut runtime = empty_generic_runtime(); + runtime.model_parameter_runtime_slots.insert( + "alpha_s".to_string(), + RuntimeParameterSlots { + real: 0, + imaginary: None, + }, + ); + runtime + } + + let mut runtime = runtime_with_alpha_s(); + runtime + .helicity_selector_runtimes + .push(Box::new(runtime_with_alpha_s())); + runtime + .helicity_selector_runtime_schedule_modes + .push(HelicitySelectorScheduleMode::ParentClosure); + runtime + .helicity_selector_runtimes + .push(Box::new(runtime_with_alpha_s())); + runtime + .helicity_selector_runtime_schedule_modes + .push(HelicitySelectorScheduleMode::ParentClosure); + runtime.helicity_selector_lane_by_domain = BTreeMap::from([(0, 0), (1, 0), (2, 1)]); + + runtime + .apply_model_parameter_overrides(&BTreeMap::from([("alpha_s".to_string(), (0.101, 0.0))])) + .unwrap(); + assert_eq!(runtime.model_parameter_values_f64, vec![0.101]); + assert_eq!(runtime.helicity_selector_runtimes.len(), 2); + assert_eq!(runtime.helicity_selector_lane_by_domain.len(), 3); + assert!( + runtime + .helicity_selector_runtimes + .iter() + .all(|lane| lane.model_parameter_values_f64 == vec![0.101]) + ); + + runtime.helicity_selector_runtimes[1] + .model_parameter_runtime_slots + .clear(); + let error = runtime + .apply_model_parameter_overrides(&BTreeMap::from([("alpha_s".to_string(), (0.089, 0.0))])) + .unwrap_err(); + + assert!(error.to_string().contains("alpha_s")); + assert_eq!(runtime.model_parameter_values_f64, vec![0.101]); + assert!( + runtime + .helicity_selector_runtimes + .iter() + .all(|lane| lane.model_parameter_values_f64 == vec![0.101]) + ); +} + +#[test] +fn alias_external_order_initialization_reaches_shared_helicity_selector_lanes() { + let mut runtime = empty_generic_runtime(); + runtime + .helicity_selector_runtimes + .push(Box::new(empty_generic_runtime())); + runtime + .helicity_selector_runtime_schedule_modes + .push(HelicitySelectorScheduleMode::ParentClosure); + runtime.helicity_selector_lane_by_domain = BTreeMap::from([(0, 0), (1, 0)]); + + runtime.set_external_pdg_order_recursive(&[1, -1, 23]); + + assert_eq!(runtime.external_pdg_order, vec![1, -1, 23]); + assert_eq!(runtime.helicity_selector_runtimes.len(), 1); + assert_eq!( + runtime.helicity_selector_runtimes[0].external_pdg_order, + vec![1, -1, 23] + ); +} + +#[test] +fn model_parameter_override_rejects_mass_class_changes_atomically() { + let mut runtime = empty_generic_runtime(); + runtime.model_parameter_values_f64 = vec![91.188]; + runtime.model_parameter_runtime_slots.insert( + "MZ".to_string(), + RuntimeParameterSlots { + real: 0, + imaginary: None, + }, + ); + runtime + .particle_mass_parameter_names + .insert(23, "MZ".to_string()); + runtime.particle_masses.insert(23, 91.188); + + let error = runtime + .apply_model_parameter_overrides(&BTreeMap::from([("MZ".to_string(), (0.0, 0.0))])) + .unwrap_err(); + + assert!(error.to_string().contains("mass class")); + assert!(error.to_string().contains("regenerate")); + assert_eq!(runtime.model_parameter_values_f64, vec![91.188]); + assert_eq!(runtime.particle_masses.get(&23), Some(&91.188)); + + runtime + .apply_model_parameter_overrides(&BTreeMap::from([("MZ".to_string(), (100.0, 0.0))])) + .expect("massive-to-massive update remains valid"); + assert_eq!(runtime.model_parameter_values_f64, vec![100.0]); + assert_eq!(runtime.particle_masses.get(&23), Some(&100.0)); +} + +#[test] +fn compiled_color_topology_lane_requires_physics_reduction() { + let mut manifest: ExecutionManifest = serde_json::from_slice(include_bytes!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../../../src/pyamplicol/assets/selftest/portable-64le/artifact/processes/", + "d_dbar_to_z/execution.json" + ))) + .expect("parse packaged execution fixture"); + let mut lane = manifest.clone(); + lane.physics_reduction = None; + lane.helicity_sum_execution = None; + lane.helicity_selector_executions.clear(); + lane.color_selector_executions.clear(); + manifest + .color_selector_executions + .push(ColorSelectorExecutionManifest { + materialized_sector_id: 0, + execution: Box::new(lane), + }); + + let error = ExecutionRuntime::from_manifest(manifest) + .err() + .expect("color topology lane without reduction must fail"); + + assert_eq!(error.kind(), crate::RusticolErrorKind::Integrity); + assert!( + error.to_string().contains("has no reduction override"), + "unexpected error: {error}" + ); +} + +#[test] +fn eager_native_profile_accepts_non_overlapping_top_level_phases() { + let mut profile: NativeRuntimeProfile = RuntimeProfile::default().into(); + profile.total_s = 10.0e-3; + profile.source_fill_s = 1.0e-3; + profile.momentum_input_setup_s = 0.5e-3; + profile.momentum_setup_s = 0.5e-3; + profile.stage_evaluator_call_s = 7.0e-3; + profile.eager_initialize_s = 0.5e-3; + profile.eager_kernel_call_s = 6.0e-3; + profile.eager_copy_out_s = 0.5e-3; + + profile.validate_eager_top_level_accounting().unwrap(); +} + +#[test] +fn native_profile_preserves_legacy_momentum_setup_aggregate() { + let profile: NativeRuntimeProfile = RuntimeProfile { + momentum_input_setup_s: 0.25, + momentum_setup_s: 0.75, + model_parameter_setup_s: 0.5, + ..RuntimeProfile::default() + } + .into(); + + assert_eq!(profile.momentum_input_setup_s, 0.25); + assert_eq!(profile.momentum_setup_s, 0.75); + assert_eq!(profile.model_parameter_setup_s, 0.5); +} + +#[test] +fn eager_native_profile_rejects_top_level_overlap() { + let mut profile: NativeRuntimeProfile = RuntimeProfile::default().into(); + profile.total_s = 10.0e-3; + profile.source_fill_s = 4.0e-3; + profile.stage_evaluator_call_s = 7.0e-3; + + let error = profile.validate_eager_top_level_accounting().unwrap_err(); + + assert_eq!(error.kind(), crate::RusticolErrorKind::Internal); + assert!(error.to_string().contains("exclusive top-level phases")); + assert!(error.to_string().contains("exceeding wall time")); +} + +#[test] +fn recurrence_native_profile_accepts_nested_schedule_attribution() { + let mut profile: NativeRuntimeProfile = RuntimeProfile::default().into(); + profile.total_s = 12.0e-3; + profile.source_fill_s = 0.5e-3; + profile.momentum_input_setup_s = 0.5e-3; + profile.recurrence_momentum_fill_s = 1.0e-3; + profile.recurrence_union_source_fill_s = 0.5e-3; + profile.recurrence_schedule_s = 8.0e-3; + profile.recurrence_source_kernel_s = 0.5e-3; + profile.recurrence_contribution_kernel_s = 5.0e-3; + profile.recurrence_finalization_s = 1.0e-3; + profile.recurrence_closure_s = 0.5e-3; + profile.recurrence_replay_output_mapping_s = 0.5e-3; + profile.reduction_s = 0.5e-3; + + profile.validate_recurrence_top_level_accounting().unwrap(); +} + +#[test] +fn recurrence_native_profile_keeps_contracted_replay_schedule_out_of_reduction() { + // A contracted replay owns three distinct top-level intervals: momentum + // fill, schedule execution, and the post-schedule copy/contraction. The + // latter must not start before the scheduler call or it counts the + // schedule twice and can exceed the enclosing wall clock. + let mut profile: NativeRuntimeProfile = RuntimeProfile::default().into(); + profile.total_s = 26.959e-6; + profile.recurrence_momentum_fill_s = 2.0e-6; + profile.recurrence_schedule_s = 20.459e-6; + profile.recurrence_replay_output_mapping_s = 0.5e-6; + profile.reduction_s = 2.0e-6; + + profile.validate_recurrence_top_level_accounting().unwrap(); + + let mut double_counted = profile; + double_counted.reduction_s += double_counted.recurrence_schedule_s; + let error = double_counted + .validate_recurrence_top_level_accounting() + .unwrap_err(); + assert!(error.to_string().contains("exclusive top-level phases")); +} + +#[test] +fn recurrence_native_profile_rejects_top_level_overlap() { + let mut profile: NativeRuntimeProfile = RuntimeProfile::default().into(); + profile.total_s = 10.0e-3; + profile.recurrence_momentum_fill_s = 4.0e-3; + profile.recurrence_schedule_s = 7.0e-3; + + let error = profile + .validate_recurrence_top_level_accounting() + .unwrap_err(); + + assert_eq!(error.kind(), crate::RusticolErrorKind::Internal); + assert!(error.to_string().contains("exclusive top-level phases")); + assert!(error.to_string().contains("exceeding wall time")); +} + +#[test] +fn recurrence_native_profile_rejects_attribution_larger_than_schedule() { + let mut profile: NativeRuntimeProfile = RuntimeProfile::default().into(); + profile.total_s = 10.0e-3; + profile.recurrence_schedule_s = 7.0e-3; + profile.recurrence_source_kernel_s = 2.0e-3; + profile.recurrence_contribution_kernel_s = 6.0e-3; + + let error = profile + .validate_recurrence_top_level_accounting() + .unwrap_err(); + + assert_eq!(error.kind(), crate::RusticolErrorKind::Internal); + assert!(error.to_string().contains("schedule sub-attribution")); + assert!( + error + .to_string() + .contains("exceeding inclusive recurrence schedule") + ); +} + +#[test] +fn compiled_native_profile_rejects_top_level_overlap() { + let mut profile: NativeRuntimeProfile = RuntimeProfile::default().into(); + profile.total_s = 10.0e-3; + profile.stage_input_pack_s = 4.0e-3; + profile.stage_evaluator_call_s = 7.0e-3; + + let error = profile + .validate_compiled_top_level_accounting() + .unwrap_err(); + + assert_eq!(error.kind(), crate::RusticolErrorKind::Internal); + assert!(error.to_string().contains("exclusive top-level phases")); + assert!(error.to_string().contains("exceeding wall time")); +} + +#[test] +fn native_profile_accumulates_compiled_accounting() { + let mut profile: NativeRuntimeProfile = RuntimeProfile { + orchestration_s: 1.0, + stage_leaf_input_pack_s: 2.0, + recurrence_schedule_s: 6.0, + recurrence_contribution_kernel_s: 4.0, + recurrence_schedule_execution_count: 2, + recurrence_contribution_row_count: 7, + stage_leaf_input_pack_by_stage_s: vec![2.0], + stage_leaf_input_copy_component_count: 3, + evaluator_backend_call_count: 4, + scratch_reallocation_count: 5, + ..RuntimeProfile::default() + } + .into(); + let repeated: NativeRuntimeProfile = RuntimeProfile { + orchestration_s: 10.0, + stage_leaf_input_pack_s: 20.0, + recurrence_schedule_s: 60.0, + recurrence_contribution_kernel_s: 40.0, + recurrence_schedule_execution_count: 20, + recurrence_contribution_row_count: 70, + stage_leaf_input_pack_by_stage_s: vec![20.0, 30.0], + stage_leaf_input_copy_component_count: 30, + evaluator_backend_call_count: 40, + scratch_reallocation_count: 50, + ..RuntimeProfile::default() + } + .into(); + + profile.accumulate(&repeated); + + assert_eq!(profile.orchestration_s, 11.0); + assert_eq!(profile.stage_leaf_input_pack_s, 22.0); + assert_eq!(profile.recurrence_schedule_s, 66.0); + assert_eq!(profile.recurrence_contribution_kernel_s, 44.0); + assert_eq!(profile.recurrence_schedule_execution_count, 22); + assert_eq!(profile.recurrence_contribution_row_count, 77); + assert_eq!(profile.stage_leaf_input_pack_by_stage_s, [22.0, 30.0]); + assert_eq!(profile.stage_leaf_input_copy_component_count, 33); + assert_eq!(profile.evaluator_backend_call_count, 44); + assert_eq!(profile.observed_scratch_reallocation_count, 55); +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/error.rs b/pyamplicol/rust/crates/rusticol-core/src/error.rs new file mode 100644 index 0000000000..fe6e751d20 --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/error.rs @@ -0,0 +1,117 @@ +// SPDX-License-Identifier: 0BSD + +use std::fmt; + +/// Stable error categories shared by the Python and C ABI boundaries. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[non_exhaustive] +pub enum RusticolErrorKind { + InvalidArgument, + Artifact, + Security, + Integrity, + Compatibility, + Serialization, + Evaluation, + Selector, + ModelParameter, + UnsupportedPrecision, + UnsupportedRuntimeCapability, + Internal, +} + +/// A Python-independent runtime error with a machine-readable category. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct RusticolError { + kind: RusticolErrorKind, + message: String, +} + +pub type RusticolResult = Result; + +impl RusticolError { + pub fn new(message: impl Into) -> Self { + Self::internal(message) + } + + pub fn with_kind(kind: RusticolErrorKind, message: impl Into) -> Self { + Self { + kind, + message: message.into(), + } + } + + pub fn invalid_argument(message: impl Into) -> Self { + Self::with_kind(RusticolErrorKind::InvalidArgument, message) + } + + pub fn artifact(message: impl Into) -> Self { + Self::with_kind(RusticolErrorKind::Artifact, message) + } + + pub fn security(message: impl Into) -> Self { + Self::with_kind(RusticolErrorKind::Security, message) + } + + pub fn integrity(message: impl Into) -> Self { + Self::with_kind(RusticolErrorKind::Integrity, message) + } + + pub fn compatibility(message: impl Into) -> Self { + Self::with_kind(RusticolErrorKind::Compatibility, message) + } + + pub fn serialization(message: impl Into) -> Self { + Self::with_kind(RusticolErrorKind::Serialization, message) + } + + pub fn evaluation(message: impl Into) -> Self { + Self::with_kind(RusticolErrorKind::Evaluation, message) + } + + pub fn selector(message: impl Into) -> Self { + Self::with_kind(RusticolErrorKind::Selector, message) + } + + pub fn model_parameter(message: impl Into) -> Self { + Self::with_kind(RusticolErrorKind::ModelParameter, message) + } + + pub fn unsupported_precision(message: impl Into) -> Self { + Self::with_kind(RusticolErrorKind::UnsupportedPrecision, message) + } + + pub fn unsupported_runtime_capability( + capability: impl AsRef, + detail: impl AsRef, + ) -> Self { + Self::with_kind( + RusticolErrorKind::UnsupportedRuntimeCapability, + format!( + "unsupported runtime capability {:?}: {}", + capability.as_ref(), + detail.as_ref() + ), + ) + } + + pub fn internal(message: impl Into) -> Self { + Self::with_kind(RusticolErrorKind::Internal, message) + } + + pub fn kind(&self) -> RusticolErrorKind { + self.kind + } + + pub fn message(&self) -> &str { + &self.message + } +} + +impl fmt::Display for RusticolError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str(&self.message) + } +} + +impl std::error::Error for RusticolError {} diff --git a/pyamplicol/rust/crates/rusticol-core/src/evaluator.rs b/pyamplicol/rust/crates/rusticol-core/src/evaluator.rs new file mode 100644 index 0000000000..ff698d575d --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/evaluator.rs @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: 0BSD + +#[path = "evaluator/crossing.rs"] +mod crossing; +pub(crate) use crossing::*; + +#[path = "evaluator/backend.rs"] +mod backend; +pub(crate) use backend::{ensure_evaluator_capabilities_supported, evaluator_runtime_capabilities}; + +#[cfg(feature = "f64-compiled")] +#[path = "evaluator/compiled.rs"] +mod compiled; +#[cfg(feature = "f64-compiled")] +pub(crate) use compiled::*; + +#[cfg(feature = "f64-compiled")] +#[path = "evaluator/native_direct.rs"] +pub(crate) mod native_direct; + +#[cfg(feature = "f64-compiled")] +#[path = "evaluator/native_compiled_direct.rs"] +pub(crate) mod native_compiled_direct; + +#[cfg(feature = "f64-symjit")] +#[path = "evaluator/symjit.rs"] +mod symjit; +#[cfg(feature = "f64-symjit")] +pub(crate) use symjit::*; + +#[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] +#[path = "evaluator/symjit_plane.rs"] +pub(crate) mod symjit_plane; + +#[cfg(feature = "f64-symjit")] +#[path = "evaluator/symjit_direct.rs"] +pub(crate) mod symjit_direct; +#[cfg(all(test, feature = "f64-symjit"))] +pub(crate) use symjit_direct::tests::count_allocations as count_test_allocations; + +#[cfg(feature = "f64-symjit")] +#[path = "evaluator/symjit_compiled_direct.rs"] +pub(crate) mod symjit_compiled_direct; + +#[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] +#[path = "evaluator/symjit_eager_direct.rs"] +pub(crate) mod symjit_eager_direct; + +#[path = "evaluator/recurrence_source_direct.rs"] +pub(crate) mod recurrence_source_direct; + +#[path = "evaluator/recurrence_closure_direct.rs"] +pub(crate) mod recurrence_closure_direct; + +#[path = "evaluator/recurrence_intrinsic_direct.rs"] +pub(crate) mod recurrence_intrinsic_direct; + +#[path = "evaluator/stage.rs"] +mod stage; +pub(crate) use stage::*; + +#[path = "evaluator/amplitude.rs"] +mod amplitude; +#[cfg(test)] +pub(crate) use amplitude::build_compiled_symmetric_group_color_contraction_runtime; +pub(crate) use amplitude::{ + build_color_contraction_runtime, build_raw_sum_groups, generic_root_group_id, +}; + +pub(crate) fn native_f64_simd_lane_width() -> usize { + #[cfg(target_arch = "x86_64")] + { + if std::is_x86_feature_detected!("avx512f") { + return 8; + } + if std::is_x86_feature_detected!("avx2") { + return 4; + } + if std::is_x86_feature_detected!("sse2") { + return 2; + } + } + #[cfg(target_arch = "aarch64")] + { + if std::arch::is_aarch64_feature_detected!("neon") { + return 2; + } + } + 1 +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/evaluator/amplitude.rs b/pyamplicol/rust/crates/rusticol-core/src/evaluator/amplitude.rs new file mode 100644 index 0000000000..7ddf688727 --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/evaluator/amplitude.rs @@ -0,0 +1,4946 @@ +// SPDX-License-Identifier: 0BSD + +use super::super::*; +use super::*; + +/// A single-state selector needs only an equality check, not a position table +/// covering every physical helicity or colour per tile. Larger selections +/// retain the existing constant-time dense lookup, including nearly full axes. +enum SelectedAxisPositions<'a> { + Sparse(&'a [usize]), + Dense(&'a [Option]), +} + +impl<'a> SelectedAxisPositions<'a> { + fn new(indices: &'a [usize], complete_count: usize, dense: &'a mut Vec>) -> Self { + if indices.len() <= 1 { + return Self::Sparse(indices); + } + dense.clear(); + dense.resize(complete_count, None); + for (position, index) in indices.iter().copied().enumerate() { + dense[index] = Some(position); + } + Self::Dense(dense) + } + + #[inline(always)] + fn position(&self, index: usize) -> Option { + match self { + Self::Sparse(indices) => indices + .first() + .filter(|&&selected| selected == index) + .map(|_| 0), + Self::Dense(positions) => positions[index], + } + } +} + +trait AmplitudeSamples: Copy { + fn value(self, row: usize, output: usize, output_length: usize) -> Complex; +} + +#[derive(Clone, Copy)] +struct RowMajorAmplitudeSamples<'a> { + values: &'a [Complex], + point_count: usize, + output_length: usize, +} + +impl<'a> RowMajorAmplitudeSamples<'a> { + fn new( + values: &'a [Complex], + point_count: usize, + output_length: usize, + ) -> RusticolResult { + let expected = point_count.checked_mul(output_length).ok_or_else(|| { + RusticolError::invalid_argument("generic amplitude output dimensions overflow") + })?; + if values.len() != expected { + return Err(RusticolError::invalid_argument(format!( + "generic amplitude output buffer has length {}, expected {expected}", + values.len() + ))); + } + Ok(Self { + values, + point_count, + output_length, + }) + } +} + +impl AmplitudeSamples for RowMajorAmplitudeSamples<'_> { + #[inline(always)] + fn value(self, row: usize, output: usize, output_length: usize) -> Complex { + debug_assert_eq!(output_length, self.output_length); + debug_assert!(row < self.point_count); + debug_assert!(output < self.output_length); + self.values[row * self.output_length + output] + } +} + +#[derive(Clone, Copy)] +struct EagerAmplitudeMajorSamples<'a> { + values: &'a [crate::EagerComplex64], + point_count: usize, + output_length: usize, +} + +impl<'a> EagerAmplitudeMajorSamples<'a> { + fn new( + values: &'a [crate::EagerComplex64], + point_count: usize, + output_length: usize, + ) -> RusticolResult { + let expected = point_count.checked_mul(output_length).ok_or_else(|| { + RusticolError::invalid_argument("eager amplitude output dimensions overflow") + })?; + if values.len() != expected { + return Err(RusticolError::invalid_argument(format!( + "eager amplitude output buffer has length {}, expected {expected}", + values.len() + ))); + } + Ok(Self { + values, + point_count, + output_length, + }) + } +} + +impl AmplitudeSamples for EagerAmplitudeMajorSamples<'_> { + #[inline(always)] + fn value(self, row: usize, output: usize, output_length: usize) -> Complex { + debug_assert_eq!(output_length, self.output_length); + debug_assert!(row < self.point_count); + debug_assert!(output < self.output_length); + // The eager scheduler copies each amplitude's complete point plane, + // unlike the point-major generic evaluator output. + let value = self.values[output * self.point_count + row]; + c64(value.re, value.im) + } +} + +impl AmplitudeSamples for crate::direct_arena::DirectAmplitudePlanes<'_> { + #[inline(always)] + fn value(self, row: usize, output: usize, _output_length: usize) -> Complex { + let (re, im) = self.value_unchecked(output, row); + c64(re, im) + } +} + +#[inline(always)] +fn sum_real_hermitian_products(left: &[Complex], right: &[Complex]) -> f64 { + debug_assert_eq!(left.len(), right.len()); + // Eight independent chains cover the wide repeated-helicity blocks while + // the four-lane remainder preserves the smaller-block execution shape. + let mut sums = [0.0; 8]; + let mut index = 0; + while index + 8 <= left.len() { + for lane in 0..8 { + let left_value = left[index + lane]; + let right_value = right[index + lane]; + sums[lane] += left_value.re * right_value.re + left_value.im * right_value.im; + } + index += 8; + } + while index + 4 <= left.len() { + for lane in 0..4 { + let left_value = left[index + lane]; + let right_value = right[index + lane]; + sums[lane] += left_value.re * right_value.re + left_value.im * right_value.im; + } + index += 4; + } + while index < left.len() { + let left_value = left[index]; + let right_value = right[index]; + sums[0] += left_value.re * right_value.re + left_value.im * right_value.im; + index += 1; + } + ((sums[0] + sums[1]) + (sums[2] + sums[3])) + ((sums[4] + sums[5]) + (sums[6] + sums[7])) +} + +#[inline(always)] +fn sum_hermitian_products(left: &[Complex], right: &[Complex]) -> Complex { + debug_assert_eq!(left.len(), right.len()); + let mut sum = c64(0.0, 0.0); + for (left_value, right_value) in left.iter().zip(right) { + sum.re += left_value.re * right_value.re + left_value.im * right_value.im; + sum.im += left_value.im * right_value.re - left_value.re * right_value.im; + } + sum +} + +fn remap_amplitude_outputs( + batch_size: usize, + evaluator_output_order: &[usize], + evaluator_outputs: &[Complex], + canonical_outputs: &mut Vec>, +) -> RusticolResult<()> { + let output_length = evaluator_output_order.len(); + let expected = batch_size + .checked_mul(output_length) + .ok_or_else(|| RusticolError::invalid_argument("amplitude output length overflows"))?; + if evaluator_outputs.len() != expected { + return Err(RusticolError::integrity( + "amplitude evaluator output mapping has an inconsistent buffer length", + )); + } + canonical_outputs.clear(); + canonical_outputs.resize(expected, c64(0.0, 0.0)); + for row in 0..batch_size { + let row_offset = row * output_length; + for (evaluator_output, canonical_output) in + evaluator_output_order.iter().copied().enumerate() + { + canonical_outputs[row_offset + canonical_output] = + evaluator_outputs[row_offset + evaluator_output]; + } + } + Ok(()) +} + +impl AmplitudeRuntime { + pub(crate) fn compiled_direct_reduction_footprint( + &self, + maximum_resolved_component_count: usize, + routed: CompiledDirectRoutedReductionFootprint, + ) -> RusticolResult { + let maximum_group_component_count = self + .raw_sum_groups + .iter() + .map(|group| group.indices.len()) + .max() + .unwrap_or(1); + let kind = if let Some(replay) = self.color_topology_replay.as_ref() { + CompiledDirectReducerKind::ColorTopologyReplay { + physical_group_count: replay.physical_groups.len(), + } + } else if let Some(workspace_scalars_per_lane) = + self.compiled_symmetric_group_workspace_scalars_per_lane()? + { + CompiledDirectReducerKind::SymmetricGroup { + workspace_scalars_per_lane, + } + } else if let Some(contraction) = self.color_contraction.as_ref() { + CompiledDirectReducerKind::Contracted { + group_count: contraction.group_count, + } + } else if self.has_coherent_groups { + CompiledDirectReducerKind::Coherent + } else { + CompiledDirectReducerKind::Plain + }; + CompiledDirectReductionFootprint::authenticated( + kind, + maximum_group_component_count, + maximum_resolved_component_count, + routed, + ) + } + + pub(in crate::engine) fn color_topology_replay_reducer( + output_length: usize, + materialized_groups: &[RawSumGroup], + replay_manifest: &GenericColorTopologyReplayAmplitudeManifest, + contraction_manifest: &GenericColorContractionManifest, + ) -> RusticolResult { + let raw_sum_groups = materialized_groups + .iter() + .map(|group| RawSumGroup { + id: group.id, + indices: group.indices.clone(), + weight: group.weight, + all_sector_weight: group.all_sector_weight, + sector_ids: group.sector_ids.clone(), + }) + .collect::>(); + let color_topology_replay = build_color_topology_replay_amplitude_runtime( + Some(replay_manifest), + Some(contraction_manifest), + &raw_sum_groups, + )? + .ok_or_else(|| { + RusticolError::integrity("color topology replay reducer lost its amplitude manifest") + })?; + Ok(Self { + output_length, + raw_sum_weights: vec![1.0; output_length], + raw_sum_all_sector_weights: vec![1.0; output_length], + raw_sum_color_sector_ids: vec![None; output_length], + raw_sum_groups, + has_coherent_groups: true, + color_contraction: None, + color_topology_replay: Some(color_topology_replay), + input_components: None, + input_spans: Vec::new(), + parameter_scratch_f64: Vec::new(), + evaluator_output_scratch_f64: Vec::new(), + output_scratch_f64: Vec::new(), + resolved_source_row_scratch_f64: Vec::new(), + resolved_target_row_scratch_f64: Vec::new(), + routed_reduction_scratch: RoutedReductionScratch::default(), + materialized_helicity_direct_total_plans: Vec::new(), + materialized_helicity_direct_total_plan_capacity: 0, + materialized_helicity_direct_total_next_replacement: 0, + materialized_helicity_direct_default_plan_by_selector_domain: Vec::new(), + evaluator_output_order: None, + evaluator: None, + }) + } + + pub(crate) fn load( + amplitude_stage: &GenericAmplitudeStageManifest, + stage: &GenericSerializedStageEvaluatorManifest, + payloads: &EvaluatorPayloadStore, + ) -> RusticolResult { + let mut runtime = Self::load_reducer_only(amplitude_stage, stage)?; + runtime.evaluator = Some(EvaluatorGroup::load_from_store(&stage.evaluator, payloads)?); + Ok(runtime) + } + + #[cfg(feature = "symbolica-runtime")] + pub(crate) fn load_exact_from_plane( + amplitude_stage: &GenericAmplitudeStageManifest, + stage: &GenericSerializedStageEvaluatorManifest, + payloads: &EvaluatorPayloadStore, + ) -> RusticolResult { + let mut runtime = Self::load_reducer_only(amplitude_stage, stage)?; + runtime.evaluator = Some(EvaluatorGroup::load_exact_from_plane(stage, payloads)?); + Ok(runtime) + } + + pub(crate) fn load_reducer_only( + amplitude_stage: &GenericAmplitudeStageManifest, + stage: &GenericSerializedStageEvaluatorManifest, + ) -> RusticolResult { + Self::load_reducer_only_with_compiled_color(amplitude_stage, stage, None) + } + + pub(crate) fn load_reducer_only_with_compiled_color( + amplitude_stage: &GenericAmplitudeStageManifest, + stage: &GenericSerializedStageEvaluatorManifest, + compiled_color: Option, + ) -> RusticolResult { + if stage.stage_kind != "amplitude-roots" { + return Err(RusticolError::invalid_argument( + "generic amplitude runtime expected an amplitude-roots stage", + )); + } + let raw_sum_weights = amplitude_stage + .roots + .iter() + .map(|root| root.helicity_weight) + .collect::>(); + let raw_sum_all_sector_weights = amplitude_stage + .roots + .iter() + .map(|root| root.all_sector_weight.unwrap_or(root.helicity_weight)) + .collect::>(); + let raw_sum_color_sector_ids = amplitude_stage + .roots + .iter() + .map(|root| root.color_sector_id) + .collect::>(); + let raw_sum_group_ids = amplitude_stage + .roots + .iter() + .map(generic_root_group_id) + .collect::>>()?; + let has_coherent_groups = raw_sum_group_ids.iter().any(Option::is_some); + let raw_sum_groups = if has_coherent_groups { + build_raw_sum_groups( + amplitude_stage.output_count, + &raw_sum_weights, + &raw_sum_all_sector_weights, + &raw_sum_group_ids, + &raw_sum_color_sector_ids, + )? + } else { + Vec::new() + }; + let color_topology_replay = build_color_topology_replay_amplitude_runtime( + amplitude_stage.color_topology_replay.as_ref(), + amplitude_stage.color_contraction.as_ref(), + &raw_sum_groups, + )?; + let color_contraction = if color_topology_replay.is_some() { + if compiled_color.is_some() { + return Err(RusticolError::compatibility( + "compiled symmetric-group FFT diagnostic does not support colour-topology replay", + )); + } + None + } else if let Some(compiled_color) = compiled_color { + Some(build_compiled_symmetric_group_color_contraction_runtime( + amplitude_stage.color_contraction.as_ref(), + &raw_sum_groups, + compiled_color, + )?) + } else { + build_color_contraction_runtime( + amplitude_stage.color_contraction.as_ref(), + &raw_sum_groups, + )? + }; + let (input_components, input_spans) = + if stage.parameter_layout == "stage-local-value-momentum" { + let mut map = vec![0usize; stage.parameter_count]; + if let Some(direct) = stage.compiled_plane_arena.as_ref() { + for component in &direct.input_bindings { + map[component.parameter_index] = component.global_component; + } + } else { + for component in &stage.input_components { + map[component.parameter_index] = component.global_component; + } + } + let spans = contiguous_input_spans(&map); + (Some(map), spans) + } else { + (None, Vec::new()) + }; + let mut evaluator_output_order = vec![usize::MAX; stage.output_length]; + let mut canonical_outputs = vec![false; stage.output_length]; + if let Some(direct) = stage.compiled_plane_arena.as_ref() { + for binding in &direct.output_bindings { + if binding.arena != "amplitude" { + return Err(RusticolError::integrity( + "compiled plane-arena amplitude binding names the wrong arena", + )); + } + let evaluator_output = binding.output_index; + let canonical_output = binding.component; + if evaluator_output >= stage.output_length + || canonical_output >= stage.output_length + || evaluator_output_order[evaluator_output] != usize::MAX + || canonical_outputs[canonical_output] + { + return Err(RusticolError::artifact( + "amplitude evaluator output mapping is not a permutation", + )); + } + evaluator_output_order[evaluator_output] = canonical_output; + canonical_outputs[canonical_output] = true; + } + } else { + for slot in &stage.output_slots { + let output_len = + slot.output_stop + .checked_sub(slot.output_start) + .ok_or_else(|| { + RusticolError::artifact( + "amplitude evaluator has an invalid output range", + ) + })?; + let component_len = slot + .component_stop + .checked_sub(slot.component_start) + .ok_or_else(|| { + RusticolError::artifact( + "amplitude evaluator has an invalid component range", + ) + })?; + if output_len != component_len { + return Err(RusticolError::artifact( + "amplitude evaluator output and component ranges disagree", + )); + } + for offset in 0..output_len { + let evaluator_output = slot.output_start + offset; + let canonical_output = slot.component_start + offset; + if evaluator_output >= stage.output_length + || canonical_output >= stage.output_length + || evaluator_output_order[evaluator_output] != usize::MAX + || canonical_outputs[canonical_output] + { + return Err(RusticolError::artifact( + "amplitude evaluator output mapping is not a permutation", + )); + } + evaluator_output_order[evaluator_output] = canonical_output; + canonical_outputs[canonical_output] = true; + } + } + } + if evaluator_output_order.contains(&usize::MAX) + || canonical_outputs.iter().any(|covered| !covered) + { + return Err(RusticolError::artifact( + "amplitude evaluator output mapping is incomplete", + )); + } + let evaluator_output_order = evaluator_output_order + .iter() + .enumerate() + .any(|(index, output)| index != *output) + .then_some(evaluator_output_order); + Ok(Self { + output_length: amplitude_stage.output_count, + raw_sum_weights, + raw_sum_all_sector_weights, + raw_sum_color_sector_ids, + raw_sum_groups, + has_coherent_groups, + color_contraction, + color_topology_replay, + input_components, + input_spans, + parameter_scratch_f64: Vec::new(), + evaluator_output_scratch_f64: Vec::new(), + output_scratch_f64: Vec::new(), + resolved_source_row_scratch_f64: Vec::new(), + resolved_target_row_scratch_f64: Vec::new(), + routed_reduction_scratch: RoutedReductionScratch::default(), + materialized_helicity_direct_total_plans: Vec::new(), + materialized_helicity_direct_total_plan_capacity: 0, + materialized_helicity_direct_total_next_replacement: 0, + materialized_helicity_direct_default_plan_by_selector_domain: Vec::new(), + evaluator_output_order, + evaluator: None, + }) + } + + pub(crate) fn prepare_compiled_symmetric_group_workspace( + &mut self, + lane_capacity: usize, + ) -> RusticolResult<()> { + let Some(contraction) = self.color_contraction.as_mut() else { + return Ok(()); + }; + let Some(symmetric_group) = contraction.symmetric_group.as_mut() else { + return Ok(()); + }; + let reducer = match symmetric_group.plan.runtime_reducer() { + Some(crate::recurrence::RuntimeColorContractionReducer::SymmetricGroupFourier( + reducer, + )) => reducer, + _ => { + return Err(RusticolError::integrity( + "compiled symmetric-group FFT payload lost its runtime reducer", + )); + } + }; + symmetric_group.workspace = Some(reducer.workspace(lane_capacity)?); + Ok(()) + } + + fn compiled_symmetric_group_workspace_scalars_per_lane(&self) -> RusticolResult> { + let Some(symmetric_group) = self + .color_contraction + .as_ref() + .and_then(|contraction| contraction.symmetric_group.as_ref()) + else { + return Ok(None); + }; + let reducer = match symmetric_group.plan.runtime_reducer() { + Some(crate::recurrence::RuntimeColorContractionReducer::SymmetricGroupFourier( + reducer, + )) => reducer, + _ => { + return Err(RusticolError::integrity( + "compiled symmetric-group FFT payload lost its runtime reducer", + )); + } + }; + // The largest irrep block has d^2 <= |S_m|. Count split-complex + // gathered, transformed, permutation, and block workspaces plus the + // real reduced lane. This conservative authenticated bound controls + // the reduction tile; the workspace itself is allocated exactly once. + let complex_values = reducer + .local_group_count() + .checked_add( + reducer + .channel_count() + .checked_mul(reducer.group_order()) + .ok_or_else(|| { + RusticolError::integrity("compiled symmetric-group workspace overflows") + })?, + ) + .and_then(|value| { + reducer + .group_order() + .checked_mul(2) + .and_then(|scratch| value.checked_add(scratch)) + }) + .ok_or_else(|| { + RusticolError::integrity("compiled symmetric-group workspace overflows") + })?; + Ok(Some( + complex_values + .checked_mul(2) + .and_then(|value| value.checked_add(1)) + .ok_or_else(|| { + RusticolError::integrity("compiled symmetric-group scalar footprint overflows") + })?, + )) + } + + pub(crate) fn color_topology_replay_mappings(&self) -> Option>> { + self.color_topology_replay.as_ref().map(|replay| { + replay + .mappings + .iter() + .map(|mapping| mapping.label_permutation.clone()) + .collect() + }) + } + + pub(crate) fn begin_color_topology_replay(&mut self, batch_size: usize) -> RusticolResult<()> { + let replay = self.color_topology_replay.as_mut().ok_or_else(|| { + RusticolError::integrity( + "color topology replay was requested without its amplitude gather", + ) + })?; + let scalar_count = batch_size + .checked_mul(replay.physical_groups.len()) + .ok_or_else(|| { + RusticolError::invalid_argument( + "color topology replay physical amplitude shape overflows usize", + ) + })?; + replay + .physical_group_scratch_f64 + .resize(scalar_count, c64(0.0, 0.0)); + replay.physical_group_scratch_f64.fill(c64(0.0, 0.0)); + replay.covered_groups.fill(false); + Ok(()) + } + + pub(crate) fn gather_color_topology_replay_mapping( + &mut self, + batch_size: usize, + mapping_index: usize, + ) -> RusticolResult<()> { + let output_scratch = std::mem::take(&mut self.output_scratch_f64); + let result = (|| { + let samples = + RowMajorAmplitudeSamples::new(&output_scratch, batch_size, self.output_length)?; + self.gather_color_topology_replay_samples(samples, batch_size, 0, mapping_index) + })(); + self.output_scratch_f64 = output_scratch; + result + } + + pub(in crate::engine) fn gather_color_topology_replay_eager_amplitudes( + &mut self, + amplitudes: &[crate::EagerComplex64], + batch_size: usize, + mapping_index: usize, + ) -> RusticolResult<()> { + let samples = EagerAmplitudeMajorSamples::new(amplitudes, batch_size, self.output_length)?; + self.gather_color_topology_replay_samples(samples, batch_size, 0, mapping_index) + } + + pub(crate) fn gather_color_topology_replay_planes( + &mut self, + amplitudes: crate::direct_arena::DirectAmplitudePlanes<'_>, + target_row_start: usize, + mapping_index: usize, + ) -> RusticolResult<()> { + let plane_count = usize::try_from(amplitudes.component_count()?) + .map_err(|_| RusticolError::invalid_argument("amplitude plane count exceeds usize"))?; + if plane_count != self.output_length { + return Err(RusticolError::integrity( + "color topology replay direct amplitude width is inconsistent", + )); + } + self.gather_color_topology_replay_samples( + amplitudes, + amplitudes.point_count() as usize, + target_row_start, + mapping_index, + ) + } + + fn gather_color_topology_replay_samples( + &mut self, + amplitudes: S, + batch_size: usize, + target_row_start: usize, + mapping_index: usize, + ) -> RusticolResult<()> { + let replay = self.color_topology_replay.as_mut().ok_or_else(|| { + RusticolError::integrity( + "color topology replay was requested without its amplitude gather", + ) + })?; + let mapping = replay.mappings.get(mapping_index).ok_or_else(|| { + RusticolError::integrity( + "color topology replay requested an unknown external-label mapping", + ) + })?; + let physical_group_count = replay.physical_groups.len(); + let physical_row_count = replay + .physical_group_scratch_f64 + .len() + .checked_div(physical_group_count) + .ok_or_else(|| { + RusticolError::integrity( + "color topology replay physical amplitude scratch is invalid", + ) + })?; + if target_row_start + .checked_add(batch_size) + .is_none_or(|stop| stop > physical_row_count) + { + return Err(RusticolError::integrity( + "color topology replay target row range exceeds its physical scratch", + )); + } + for route in &mapping.group_routes { + if target_row_start == 0 { + if std::mem::replace(&mut replay.covered_groups[route.target_group_index], true) { + return Err(RusticolError::integrity( + "color topology replay attempted to materialize a physical group twice", + )); + } + } else if !replay.covered_groups[route.target_group_index] { + return Err(RusticolError::integrity( + "color topology replay direct tiles arrived out of order", + )); + } + let source_group = &self.raw_sum_groups[route.source_group_index]; + for row in 0..batch_size { + let mut value = c64(0.0, 0.0); + for output_index in &source_group.indices { + value += amplitudes.value(row, *output_index, self.output_length); + } + replay.physical_group_scratch_f64 + [(target_row_start + row) * physical_group_count + route.target_group_index] = + value * route.factor; + } + } + Ok(()) + } + + pub(crate) fn reduce_color_topology_replay_f64_into( + &mut self, + batch_size: usize, + output: &mut [f64], + ) -> RusticolResult<()> { + let replay = self.color_topology_replay.as_mut().ok_or_else(|| { + RusticolError::integrity( + "color topology replay was requested without its amplitude gather", + ) + })?; + if replay.covered_groups.iter().any(|covered| !covered) { + return Err(RusticolError::integrity( + "color topology replay did not materialize every physical amplitude group", + )); + } + let samples = RowMajorAmplitudeSamples::new( + &replay.physical_group_scratch_f64, + batch_size, + replay.physical_groups.len(), + )?; + Self::reduce_amplitude_samples_f64_into_selected_slice( + samples, + batch_size, + output, + None, + replay.physical_groups.len(), + &replay.unit_weights, + &replay.unit_weights, + &replay.no_sector_ids, + &replay.physical_groups, + true, + &mut replay.color_contraction, + ) + } + + pub(crate) fn reduce_color_topology_replay_f64_resolved( + &mut self, + batch_size: usize, + physics: &PhysicsRuntime, + normalization_factor: f64, + selected_helicity_ids: Option<&BTreeSet>, + selected_color_ids: Option<&BTreeSet>, + ) -> RusticolResult> { + let replay = self.color_topology_replay.as_mut().ok_or_else(|| { + RusticolError::integrity( + "color topology replay was requested without its amplitude gather", + ) + })?; + if replay.covered_groups.iter().any(|covered| !covered) { + return Err(RusticolError::integrity( + "color topology replay did not materialize every physical amplitude group", + )); + } + if !physics.has_contracted_color_axis() { + return Err(RusticolError::artifact( + "color topology replay resolved reduction requires contracted color", + )); + } + let helicity_indices = physics.selected_helicity_indices(selected_helicity_ids)?; + let color_indices = physics.selected_color_indices(selected_color_ids)?; + if color_indices.iter().any(|index| *index != 0) { + return Err(RusticolError::selector( + "color topology replay exposes one contracted color component", + )); + } + let mut selected_positions = vec![None; physics.manifest.helicities.len()]; + for (position, index) in helicity_indices.iter().copied().enumerate() { + selected_positions[index] = Some(position); + } + let index_by_values = physics + .manifest + .helicities + .iter() + .map(|helicity| (helicity.values.clone(), helicity.index)) + .collect::>(); + let mut members_by_group = Vec::with_capacity(replay.physical_groups.len()); + for (group, representative) in replay + .physical_groups + .iter() + .zip(&replay.physical_group_helicities) + { + let mut members = vec![representative.clone()]; + if group.weight > 1.0 { + if group.weight.to_bits() != 2.0f64.to_bits() { + return Err(RusticolError::artifact( + "color topology replay has an unsupported helicity reuse weight", + )); + } + let flipped = representative + .iter() + .map(|value| -*value) + .collect::>(); + if flipped != *representative { + members.push(flipped); + } + } + let mut indexed = Vec::with_capacity(members.len()); + let mut total_weight = 0.0; + for member in members { + let index = index_by_values.get(&member).copied().ok_or_else(|| { + RusticolError::artifact( + "color topology replay physical helicity is absent from public metadata", + ) + })?; + let weight = physics.manifest.helicities[index].coefficient; + total_weight += weight; + indexed.push((index, weight)); + } + if !total_weight.is_finite() || total_weight <= 0.0 { + return Err(RusticolError::artifact( + "color topology replay physical helicity has no positive weight", + )); + } + for (_, weight) in &mut indexed { + *weight /= total_weight; + } + members_by_group.push(indexed); + } + let contraction = replay.color_contraction.as_mut().ok_or_else(|| { + RusticolError::integrity( + "color topology replay lost its authenticated color contraction", + ) + })?; + let entries = contraction.logical_entries().collect::>(); + for entry in &entries { + if members_by_group[entry.left_group_index] != members_by_group[entry.right_group_index] + { + return Err(RusticolError::artifact( + "color contraction mixes distinct replayed physical helicities", + )); + } + } + let component_count = helicity_indices + .len() + .checked_mul(color_indices.len()) + .ok_or_else(|| RusticolError::invalid_argument("resolved shape overflows usize"))?; + let mut values = vec![0.0; batch_size * component_count]; + let group_count = replay.physical_groups.len(); + for row in 0..batch_size { + let group_row = row * group_count; + for entry in &entries { + let left = replay.physical_group_scratch_f64[group_row + entry.left_group_index]; + let right = replay.physical_group_scratch_f64[group_row + entry.right_group_index]; + let product = left * right.conj(); + let contribution = normalization_factor + * entry.symmetry_factor + * (entry.weight_re * product.re - entry.weight_im * product.im); + for (helicity_index, weight) in &members_by_group[entry.left_group_index] { + let Some(position) = selected_positions[*helicity_index] else { + continue; + }; + values[row * component_count + position * color_indices.len()] += + contribution * weight; + } + } + } + Ok(ResolvedValues { + values, + point_count: batch_size, + helicity_indices, + color_indices, + }) + } + + pub(crate) fn evaluate_f64_into_scratch( + &mut self, + batch_size: usize, + state: &[Complex], + ) -> RusticolResult<()> { + let evaluator = self.evaluator.as_mut().ok_or_else(|| { + RusticolError::integrity( + "compiled plane-arena reducer cannot execute a dense amplitude evaluator", + ) + })?; + let evaluator_params = if let Some(input_components) = self.input_components.as_ref() { + let local_parameter_count = input_components.len(); + let global_parameter_count = state.len().checked_div(batch_size).ok_or_else(|| { + RusticolError::invalid_argument("generic amplitude batch size is zero") + })?; + self.parameter_scratch_f64 + .resize(batch_size * local_parameter_count, c64(0.0, 0.0)); + for row in 0..batch_size { + let row_state = row * global_parameter_count; + let row_params = row * local_parameter_count; + if self.input_spans.is_empty() { + for (local_index, global_index) in input_components.iter().enumerate() { + self.parameter_scratch_f64[row_params + local_index] = + state[row_state + *global_index]; + } + } else { + for (local_start, global_start, len) in &self.input_spans { + let target_start = row_params + *local_start; + let source_start = row_state + *global_start; + self.parameter_scratch_f64[target_start..target_start + *len] + .copy_from_slice(&state[source_start..source_start + *len]); + } + } + } + self.parameter_scratch_f64.as_slice() + } else { + state + }; + if let Some(order) = self.evaluator_output_order.as_deref() { + evaluator.evaluate_batch_into( + batch_size, + evaluator_params, + &mut self.evaluator_output_scratch_f64, + )?; + remap_amplitude_outputs( + batch_size, + order, + &self.evaluator_output_scratch_f64, + &mut self.output_scratch_f64, + ) + } else { + evaluator.evaluate_batch_into( + batch_size, + evaluator_params, + &mut self.output_scratch_f64, + ) + } + } + + pub(crate) fn evaluate_f64_into_scratch_profile( + &mut self, + batch_size: usize, + state: &[Complex], + ) -> RusticolResult { + let evaluator_runtime = self.evaluator.as_mut().ok_or_else(|| { + RusticolError::integrity( + "compiled plane-arena reducer cannot profile a dense amplitude evaluator", + ) + })?; + let mut input_pack_elapsed = Duration::ZERO; + let input_copy_component_count; + let mut scratch_reallocation_count = 0; + let evaluator; + let evaluator_params; + if let Some(input_components) = self.input_components.as_ref() { + let local_parameter_count = input_components.len(); + let global_parameter_count = state.len().checked_div(batch_size).ok_or_else(|| { + RusticolError::invalid_argument("generic amplitude batch size is zero") + })?; + let pack_start = Instant::now(); + let capacity = self.parameter_scratch_f64.capacity(); + self.parameter_scratch_f64 + .resize(batch_size * local_parameter_count, c64(0.0, 0.0)); + scratch_reallocation_count += + u64::from(self.parameter_scratch_f64.capacity() != capacity); + for row in 0..batch_size { + let row_state = row * global_parameter_count; + let row_params = row * local_parameter_count; + if self.input_spans.is_empty() { + for (local_index, global_index) in input_components.iter().enumerate() { + self.parameter_scratch_f64[row_params + local_index] = + state[row_state + *global_index]; + } + } else { + for (local_start, global_start, len) in &self.input_spans { + let target_start = row_params + *local_start; + let source_start = row_state + *global_start; + self.parameter_scratch_f64[target_start..target_start + *len] + .copy_from_slice(&state[source_start..source_start + *len]); + } + } + } + // Generated evaluators may use the platform floating-point ABI + // aggressively. Keep timing state in integer-backed Duration + // values until the generated call has returned. + input_pack_elapsed = pack_start.elapsed(); + input_copy_component_count = batch_size * local_parameter_count; + evaluator_params = self.parameter_scratch_f64.as_slice(); + } else { + input_copy_component_count = 0; + evaluator_params = state; + } + let mut output_remap_elapsed = Duration::ZERO; + if let Some(order) = self.evaluator_output_order.as_deref() { + evaluator = evaluator_runtime.evaluate_batch_into_profile( + batch_size, + evaluator_params, + &mut self.evaluator_output_scratch_f64, + false, + )?; + let remap_start = Instant::now(); + remap_amplitude_outputs( + batch_size, + order, + &self.evaluator_output_scratch_f64, + &mut self.output_scratch_f64, + )?; + output_remap_elapsed = remap_start.elapsed(); + } else { + evaluator = evaluator_runtime.evaluate_batch_into_profile( + batch_size, + evaluator_params, + &mut self.output_scratch_f64, + false, + )?; + } + let input_pack_s = profile_duration_seconds(input_pack_elapsed); + Ok(AmplitudeEvaluationProfile { + input_pack_s, + evaluator, + output_remap_s: profile_duration_seconds(output_remap_elapsed), + input_copy_component_count: input_copy_component_count as u64, + output_remap_component_count: if self.evaluator_output_order.is_some() { + (batch_size * self.output_length) as u64 + } else { + 0 + }, + scratch_reallocation_count, + }) + } + + pub(crate) fn evaluate_active_chunks_f64_into_scratch( + &mut self, + batch_size: usize, + state: &[Complex], + active_chunk_indices: &[usize], + ) -> RusticolResult<()> { + let evaluator = self.evaluator.as_mut().ok_or_else(|| { + RusticolError::integrity( + "compiled plane-arena reducer cannot execute selected dense amplitude chunks", + ) + })?; + if let Some(order) = self.evaluator_output_order.as_deref() { + evaluator.evaluate_selected_chunks_f64_into_output( + batch_size, + state, + self.input_components.as_deref(), + &self.input_spans, + &mut self.evaluator_output_scratch_f64, + active_chunk_indices, + )?; + remap_amplitude_outputs( + batch_size, + order, + &self.evaluator_output_scratch_f64, + &mut self.output_scratch_f64, + ) + } else { + evaluator.evaluate_selected_chunks_f64_into_output( + batch_size, + state, + self.input_components.as_deref(), + &self.input_spans, + &mut self.output_scratch_f64, + active_chunk_indices, + ) + } + } + + pub(crate) fn evaluate_active_chunks_f64_into_scratch_profile( + &mut self, + batch_size: usize, + state: &[Complex], + active_chunk_indices: &[usize], + ) -> RusticolResult { + let evaluator_runtime = self.evaluator.as_mut().ok_or_else(|| { + RusticolError::integrity( + "compiled plane-arena reducer cannot profile selected dense amplitude chunks", + ) + })?; + let evaluator; + let mut output_remap_elapsed = Duration::ZERO; + if let Some(order) = self.evaluator_output_order.as_deref() { + evaluator = evaluator_runtime.evaluate_selected_chunks_f64_into_output_profile( + batch_size, + state, + self.input_components.as_deref(), + &self.input_spans, + &mut self.evaluator_output_scratch_f64, + active_chunk_indices, + )?; + let remap_start = Instant::now(); + remap_amplitude_outputs( + batch_size, + order, + &self.evaluator_output_scratch_f64, + &mut self.output_scratch_f64, + )?; + output_remap_elapsed = remap_start.elapsed(); + } else { + evaluator = evaluator_runtime.evaluate_selected_chunks_f64_into_output_profile( + batch_size, + state, + self.input_components.as_deref(), + &self.input_spans, + &mut self.output_scratch_f64, + active_chunk_indices, + )?; + } + Ok(AmplitudeEvaluationProfile { + input_pack_s: evaluator.leaf_input_pack_s, + output_remap_s: profile_duration_seconds(output_remap_elapsed), + // Active chunks compose the amplitude input and leaf maps, so + // the evaluator leaf gather is the only actual input copy. + input_copy_component_count: 0, + output_remap_component_count: if self.evaluator_output_order.is_some() { + (batch_size * self.output_length) as u64 + } else { + 0 + }, + scratch_reallocation_count: 0, + evaluator, + }) + } + + pub(crate) fn reduce_scratch_f64_into_selected( + &mut self, + batch_size: usize, + raw_sums: &mut Vec, + selected_color_sector_ids: Option<&BTreeSet>, + ) -> RusticolResult<()> { + raw_sums.clear(); + raw_sums.resize(batch_size, 0.0); + self.reduce_scratch_f64_into_selected_slice(batch_size, raw_sums, selected_color_sector_ids) + } + + pub(crate) fn reduce_scratch_f64_into_selected_slice( + &mut self, + batch_size: usize, + raw_sums: &mut [f64], + selected_color_sector_ids: Option<&BTreeSet>, + ) -> RusticolResult<()> { + let samples = RowMajorAmplitudeSamples::new( + &self.output_scratch_f64, + batch_size, + self.output_length, + )?; + Self::reduce_amplitude_samples_f64_into_selected_slice( + samples, + batch_size, + raw_sums, + selected_color_sector_ids, + self.output_length, + &self.raw_sum_weights, + &self.raw_sum_all_sector_weights, + &self.raw_sum_color_sector_ids, + &self.raw_sum_groups, + self.has_coherent_groups, + &mut self.color_contraction, + ) + } + + /// Reduce canonical split-complex amplitude planes without packing them + /// into the legacy point-major complex buffer. + #[allow(dead_code)] + pub(crate) fn reduce_planes_f64_into_selected_slice( + &mut self, + amplitudes: crate::direct_arena::DirectAmplitudePlanes<'_>, + raw_sums: &mut [f64], + selected_color_sector_ids: Option<&BTreeSet>, + ) -> RusticolResult<()> { + let plane_count = usize::try_from(amplitudes.component_count()?) + .map_err(|_| RusticolError::invalid_argument("amplitude plane count exceeds usize"))?; + if plane_count != self.output_length { + return Err(RusticolError::invalid_argument(format!( + "direct amplitude plane count is {plane_count}, expected {}", + self.output_length + ))); + } + let batch_size = amplitudes.point_count() as usize; + if raw_sums.len() != batch_size { + return Err(RusticolError::invalid_argument(format!( + "generic reduction output has length {}, expected {batch_size}", + raw_sums.len() + ))); + } + raw_sums.fill(0.0); + let scratch = &mut self.routed_reduction_scratch; + if let Some(contraction) = self.color_contraction.as_mut() { + if selected_color_sector_ids.is_some() { + return Err(RusticolError::invalid_argument( + "LC color-sector runtime selection is only supported for leading-colour diagonal artifacts", + )); + } + if self.raw_sum_groups.len() != contraction.group_count { + return Err(RusticolError::invalid_argument( + "colour contraction group count does not match coherent groups", + )); + } + if let Some(symmetric_group) = contraction.symmetric_group.as_mut() { + return reduce_compiled_symmetric_group_planes( + amplitudes, + raw_sums, + &self.raw_sum_groups, + symmetric_group, + ); + } + let group_scalar_count = + contraction + .group_count + .checked_mul(batch_size) + .ok_or_else(|| { + RusticolError::invalid_argument( + "plane-native color-contraction scratch shape overflows usize", + ) + })?; + scratch.direct_group_re.resize(group_scalar_count, 0.0); + scratch.direct_group_re.fill(0.0); + scratch.direct_group_im.resize(group_scalar_count, 0.0); + scratch.direct_group_im.fill(0.0); + for (group_index, group) in self.raw_sum_groups.iter().enumerate() { + let group_start = group_index * batch_size; + for output_index in &group.indices { + let (plane_re, plane_im) = amplitudes.plane_unchecked(*output_index); + for point in 0..batch_size { + scratch.direct_group_re[group_start + point] += plane_re[point]; + scratch.direct_group_im[group_start + point] += plane_im[point]; + } + } + } + for entry in contraction.logical_entries() { + let left_start = entry.left_group_index * batch_size; + let right_start = entry.right_group_index * batch_size; + for (point, raw_sum) in raw_sums.iter_mut().enumerate() { + let left_re = scratch.direct_group_re[left_start + point]; + let left_im = scratch.direct_group_im[left_start + point]; + let right_re = scratch.direct_group_re[right_start + point]; + let right_im = scratch.direct_group_im[right_start + point]; + let product_re = left_re * right_re + left_im * right_im; + let product_im = left_im * right_re - left_re * right_im; + *raw_sum += entry.symmetry_factor + * (entry.weight_re * product_re - entry.weight_im * product_im); + } + } + return Ok(()); + } + + scratch.direct_group_re.resize(batch_size, 0.0); + scratch.direct_group_im.resize(batch_size, 0.0); + if self.has_coherent_groups { + for group in &self.raw_sum_groups { + if !raw_sum_group_is_selected(group, selected_color_sector_ids) { + continue; + } + scratch.direct_group_re.fill(0.0); + scratch.direct_group_im.fill(0.0); + for index in &group.indices { + let (plane_re, plane_im) = amplitudes.plane_unchecked(*index); + for point in 0..batch_size { + scratch.direct_group_re[point] += plane_re[point]; + scratch.direct_group_im[point] += plane_im[point]; + } + } + let weight = if selected_color_sector_ids.is_none() { + group.all_sector_weight + } else { + group.weight + }; + for (point, raw_sum) in raw_sums.iter_mut().enumerate() { + let re = scratch.direct_group_re[point]; + let im = scratch.direct_group_im[point]; + *raw_sum += weight * (re * re + im * im); + } + } + return Ok(()); + } + for index in 0..self.output_length { + if !raw_sum_index_is_selected( + self.raw_sum_color_sector_ids.get(index).copied().flatten(), + selected_color_sector_ids, + ) { + continue; + } + let (plane_re, plane_im) = amplitudes.plane_unchecked(index); + let weight = if selected_color_sector_ids.is_none() { + self.raw_sum_all_sector_weights[index] + } else { + self.raw_sum_weights[index] + }; + for point in 0..batch_size { + raw_sums[point] += weight + * (plane_re[point] * plane_re[point] + plane_im[point] * plane_im[point]); + } + } + Ok(()) + } + + #[allow(clippy::too_many_arguments)] + fn reduce_amplitude_samples_f64_into_selected_slice( + amplitudes: S, + batch_size: usize, + raw_sums: &mut [f64], + selected_color_sector_ids: Option<&BTreeSet>, + output_length: usize, + raw_sum_weights: &[f64], + raw_sum_all_sector_weights: &[f64], + raw_sum_color_sector_ids: &[Option], + raw_sum_groups: &[RawSumGroup], + has_coherent_groups: bool, + color_contraction: &mut Option, + ) -> RusticolResult<()> { + if raw_sums.len() != batch_size { + return Err(RusticolError::invalid_argument(format!( + "generic reduction output has length {}, expected {batch_size}", + raw_sums.len() + ))); + } + raw_sums.fill(0.0); + if let Some(contraction) = color_contraction.as_mut() { + if selected_color_sector_ids.is_some() { + return Err(RusticolError::invalid_argument( + "LC color-sector runtime selection is only supported for leading-colour diagonal artifacts", + )); + } + if raw_sum_groups.len() != contraction.group_count { + return Err(RusticolError::invalid_argument( + "colour contraction group count does not match coherent groups", + )); + } + if let Some(repeated_block) = contraction.repeated_block.as_ref() { + contraction + .group_scratch_f64 + .resize(contraction.group_count, c64(0.0, 0.0)); + for (row, raw_sum) in raw_sums.iter_mut().enumerate() { + if let (Some(walsh_block), Some(output_indices)) = ( + repeated_block.walsh_block.as_ref(), + repeated_block.singleton_output_indices.as_deref(), + ) { + let component_count = repeated_block.component_count; + let coset_count = walsh_block.cosets.len(); + for (coset_index, coset) in walsh_block.cosets.iter().enumerate() { + for component_index in 0..component_count { + let source = |local_group_index: usize| { + amplitudes.value( + row, + output_indices + [local_group_index * component_count + component_index], + output_length, + ) + }; + let x0 = source(coset[0]); + let x1 = source(coset[1]); + let x2 = source(coset[2]); + let x3 = source(coset[3]); + let sum01 = x0 + x1; + let difference01 = x0 - x1; + let sum23 = x2 + x3; + let difference23 = x2 - x3; + let targets = [ + (sum01 + sum23) * 0.5, + (difference01 + difference23) * 0.5, + (sum01 - sum23) * 0.5, + (difference01 - difference23) * 0.5, + ]; + for (character_index, value) in targets.into_iter().enumerate() { + contraction.group_scratch_f64[((character_index + * coset_count + + coset_index) + * component_count) + + component_index] = value; + } + } + } + for entry in &walsh_block.entries { + let left_start = entry.left_group_index * component_count; + let right_start = entry.right_group_index * component_count; + let product_re = sum_real_hermitian_products( + &contraction.group_scratch_f64 + [left_start..left_start + component_count], + &contraction.group_scratch_f64 + [right_start..right_start + component_count], + ); + *raw_sum += entry.symmetry_factor * entry.weight_re * product_re; + } + continue; + } + if let (Some(walsh_block), Some(output_indices)) = ( + repeated_block.c2k_walsh_block.as_ref(), + repeated_block.singleton_output_indices.as_deref(), + ) { + let component_count = repeated_block.component_count; + let coset_count = walsh_block.cosets.len(); + if walsh_block.subgroup_order == 8 { + for (coset_index, coset) in walsh_block.cosets.iter().enumerate() { + for component_index in 0..component_count { + let source = |local_group_index: usize| { + amplitudes.value( + row, + output_indices[local_group_index * component_count + + component_index], + output_length, + ) + }; + let x0 = source(coset[0]); + let x1 = source(coset[1]); + let x2 = source(coset[2]); + let x3 = source(coset[3]); + let x4 = source(coset[4]); + let x5 = source(coset[5]); + let x6 = source(coset[6]); + let x7 = source(coset[7]); + let sum01 = x0 + x1; + let difference01 = x0 - x1; + let sum23 = x2 + x3; + let difference23 = x2 - x3; + let sum45 = x4 + x5; + let difference45 = x4 - x5; + let sum67 = x6 + x7; + let difference67 = x6 - x7; + let lower = [ + sum01 + sum23, + difference01 + difference23, + sum01 - sum23, + difference01 - difference23, + ]; + let upper = [ + sum45 + sum67, + difference45 + difference67, + sum45 - sum67, + difference45 - difference67, + ]; + let targets = [ + lower[0] + upper[0], + lower[1] + upper[1], + lower[2] + upper[2], + lower[3] + upper[3], + lower[0] - upper[0], + lower[1] - upper[1], + lower[2] - upper[2], + lower[3] - upper[3], + ]; + for (character_index, value) in targets.into_iter().enumerate() + { + contraction.group_scratch_f64[((character_index + * coset_count + + coset_index) + * component_count) + + component_index] = value; + } + } + } + } else { + for (coset_index, coset) in walsh_block.cosets.iter().enumerate() { + for component_index in 0..component_count { + for (subgroup_index, local_group_index) in + coset.iter().copied().enumerate() + { + let target = ((subgroup_index * coset_count + coset_index) + * component_count) + + component_index; + contraction.group_scratch_f64[target] = amplitudes.value( + row, + output_indices[local_group_index * component_count + + component_index], + output_length, + ); + } + let mut stride = 1; + while stride < walsh_block.subgroup_order { + for start in + (0..walsh_block.subgroup_order).step_by(stride * 2) + { + for offset in 0..stride { + let left_character = start + offset; + let right_character = left_character + stride; + let left_target = ((left_character * coset_count + + coset_index) + * component_count) + + component_index; + let right_target = + ((right_character * coset_count + coset_index) + * component_count) + + component_index; + let left = + contraction.group_scratch_f64[left_target]; + let right = + contraction.group_scratch_f64[right_target]; + contraction.group_scratch_f64[left_target] = + left + right; + contraction.group_scratch_f64[right_target] = + left - right; + } + } + stride *= 2; + } + } + } + } + for entry in &walsh_block.entries { + let left_start = entry.left_group_index * component_count; + let right_start = entry.right_group_index * component_count; + let product_re = sum_real_hermitian_products( + &contraction.group_scratch_f64 + [left_start..left_start + component_count], + &contraction.group_scratch_f64 + [right_start..right_start + component_count], + ); + *raw_sum += entry.symmetry_factor * entry.weight_re * product_re; + } + continue; + } + if let Some(output_indices) = repeated_block.singleton_output_indices.as_deref() + { + for (target, output_index) in + contraction.group_scratch_f64.iter_mut().zip(output_indices) + { + *target = amplitudes.value(row, *output_index, output_length); + } + } else { + for (target, group_index) in contraction + .group_scratch_f64 + .iter_mut() + .zip(&repeated_block.component_group_indices) + { + let mut sum = c64(0.0, 0.0); + for output_index in &raw_sum_groups[*group_index].indices { + sum += amplitudes.value(row, *output_index, output_length); + } + *target = sum; + } + } + + let component_count = repeated_block.component_count; + if repeated_block.all_weights_real { + for entry in &repeated_block.entries { + let left_start = entry.left_group_index * component_count; + let right_start = entry.right_group_index * component_count; + let product_re = sum_real_hermitian_products( + &contraction.group_scratch_f64 + [left_start..left_start + component_count], + &contraction.group_scratch_f64 + [right_start..right_start + component_count], + ); + *raw_sum += entry.symmetry_factor * (entry.weight_re * product_re); + } + } else { + for entry in &repeated_block.entries { + let left_start = entry.left_group_index * component_count; + let right_start = entry.right_group_index * component_count; + let product = sum_hermitian_products( + &contraction.group_scratch_f64 + [left_start..left_start + component_count], + &contraction.group_scratch_f64 + [right_start..right_start + component_count], + ); + *raw_sum += entry.symmetry_factor + * (entry.weight_re * product.re - entry.weight_im * product.im); + } + } + } + return Ok(()); + } + contraction + .group_scratch_f64 + .resize(batch_size * contraction.group_count, c64(0.0, 0.0)); + for (row, raw_sum) in raw_sums.iter_mut().enumerate() { + let group_row = row * contraction.group_count; + for (group_index, group) in raw_sum_groups.iter().enumerate() { + let mut sum = c64(0.0, 0.0); + for index in &group.indices { + sum += amplitudes.value(row, *index, output_length); + } + contraction.group_scratch_f64[group_row + group_index] = sum; + } + for entry in contraction.logical_entries() { + let left = contraction.group_scratch_f64[group_row + entry.left_group_index]; + let right = contraction.group_scratch_f64[group_row + entry.right_group_index]; + let product = left * right.conj(); + *raw_sum += entry.symmetry_factor + * (entry.weight_re * product.re - entry.weight_im * product.im); + } + } + return Ok(()); + } + for (row, raw_sum) in raw_sums.iter_mut().enumerate() { + if has_coherent_groups { + for group in raw_sum_groups { + if !raw_sum_group_is_selected(group, selected_color_sector_ids) { + continue; + } + let mut sum = c64(0.0, 0.0); + for index in &group.indices { + sum += amplitudes.value(row, *index, output_length); + } + let weight = if selected_color_sector_ids.is_none() { + group.all_sector_weight + } else { + group.weight + }; + *raw_sum += weight * (sum.re * sum.re + sum.im * sum.im); + } + continue; + } + for index in 0..output_length { + if !raw_sum_index_is_selected( + raw_sum_color_sector_ids.get(index).copied().flatten(), + selected_color_sector_ids, + ) { + continue; + } + let value = amplitudes.value(row, index, output_length); + let weight = if selected_color_sector_ids.is_none() { + raw_sum_all_sector_weights[index] + } else { + raw_sum_weights[index] + }; + *raw_sum += weight * (value.re * value.re + value.im * value.im); + } + } + Ok(()) + } + + pub(crate) fn reduce_scratch_f64_resolved( + &mut self, + batch_size: usize, + physics: &PhysicsRuntime, + normalization_factor: f64, + selected_helicity_ids: Option<&BTreeSet>, + selected_color_ids: Option<&BTreeSet>, + ) -> RusticolResult> { + let amplitudes = RowMajorAmplitudeSamples::new( + &self.output_scratch_f64, + batch_size, + self.output_length, + )?; + Self::reduce_amplitude_samples_f64_resolved( + amplitudes, + batch_size, + physics, + normalization_factor, + selected_helicity_ids, + selected_color_ids, + self.output_length, + &self.raw_sum_groups, + self.has_coherent_groups, + &mut self.color_contraction, + ) + } + + /// Plane-native counterpart of the ordinary resolved reducer. + #[allow(dead_code)] + pub(crate) fn reduce_planes_f64_resolved( + &mut self, + amplitudes: crate::direct_arena::DirectAmplitudePlanes<'_>, + physics: &PhysicsRuntime, + normalization_factor: f64, + selected_helicity_ids: Option<&BTreeSet>, + selected_color_ids: Option<&BTreeSet>, + ) -> RusticolResult> { + let plane_count = usize::try_from(amplitudes.component_count()?) + .map_err(|_| RusticolError::invalid_argument("amplitude plane count exceeds usize"))?; + if plane_count != self.output_length { + return Err(RusticolError::invalid_argument(format!( + "direct amplitude plane count is {plane_count}, expected {}", + self.output_length + ))); + } + Self::reduce_amplitude_samples_f64_resolved( + amplitudes, + amplitudes.point_count() as usize, + physics, + normalization_factor, + selected_helicity_ids, + selected_color_ids, + self.output_length, + &self.raw_sum_groups, + self.has_coherent_groups, + &mut self.color_contraction, + ) + } + + #[allow(clippy::too_many_arguments)] + fn reduce_amplitude_samples_f64_resolved( + amplitudes: S, + batch_size: usize, + physics: &PhysicsRuntime, + normalization_factor: f64, + selected_helicity_ids: Option<&BTreeSet>, + selected_color_ids: Option<&BTreeSet>, + output_length: usize, + raw_sum_groups: &[RawSumGroup], + has_coherent_groups: bool, + color_contraction: &mut Option, + ) -> RusticolResult> { + let helicity_count = physics.manifest.helicities.len(); + let color_count = physics.manifest.color_components.len(); + let helicity_indices = physics.selected_helicity_indices(selected_helicity_ids)?; + let color_indices = physics.selected_color_indices(selected_color_ids)?; + let mut selected_helicity_positions = vec![None; helicity_count]; + for (position, index) in helicity_indices.iter().copied().enumerate() { + selected_helicity_positions[index] = Some(position); + } + let mut selected_color_positions = vec![None; color_count]; + for (position, index) in color_indices.iter().copied().enumerate() { + selected_color_positions[index] = Some(position); + } + let selected_component_count = helicity_indices + .len() + .checked_mul(color_indices.len()) + .ok_or_else(|| RusticolError::invalid_argument("resolved shape overflows usize"))?; + let mut values = vec![0.0; batch_size * selected_component_count]; + + if let Some(contraction) = color_contraction.as_mut() { + if color_count != 1 || !physics.has_contracted_color_axis() { + return Err(RusticolError::invalid_argument( + "resolved NLC/full evaluation requires one contracted color component", + )); + } + if raw_sum_groups.len() != contraction.group_count { + return Err(RusticolError::invalid_argument( + "colour contraction group count does not match coherent groups", + )); + } + let selected_contracted_color_position = selected_color_positions[0]; + let mut selected_helicity_weights_by_entry = + Vec::with_capacity(contraction.logical_entry_count()?); + for entry in contraction.logical_entries() { + let left_group = &raw_sum_groups[entry.left_group_index]; + let right_group = &raw_sum_groups[entry.right_group_index]; + let left_reduction = physics + .numeric_reduction_by_group_id + .get(&left_group.id) + .ok_or_else(|| { + RusticolError::invalid_argument(format!( + "resolved metadata is missing coherent group {}", + left_group.id + )) + })?; + let right_reduction = physics + .numeric_reduction_by_group_id + .get(&right_group.id) + .ok_or_else(|| { + RusticolError::invalid_argument(format!( + "resolved metadata is missing coherent group {}", + right_group.id + )) + })?; + if left_reduction.physical_helicity_indices + != right_reduction.physical_helicity_indices + { + return Err(RusticolError::invalid_argument( + "colour contraction mixed distinct physical helicities", + )); + } + let left_weights = &left_reduction.normalized_helicity_weights; + let right_weights = &right_reduction.normalized_helicity_weights; + if left_weights.len() != right_weights.len() + || left_weights.iter().zip(right_weights).any( + |((left_index, left_weight), (right_index, right_weight))| { + left_index != right_index + || left_weight.to_bits() != right_weight.to_bits() + }, + ) + { + return Err(RusticolError::invalid_argument( + "colour contraction groups have inconsistent helicity weights", + )); + } + selected_helicity_weights_by_entry.push( + left_weights + .iter() + .copied() + .filter_map(|(helicity_index, weight)| { + selected_helicity_positions[helicity_index] + .map(|position| (position, weight)) + }) + .collect::>(), + ); + } + contraction + .group_scratch_f64 + .resize(batch_size * contraction.group_count, c64(0.0, 0.0)); + for row in 0..batch_size { + let group_row = row * contraction.group_count; + for (group_index, group) in raw_sum_groups.iter().enumerate() { + let mut sum = c64(0.0, 0.0); + for index in &group.indices { + sum += amplitudes.value(row, *index, output_length); + } + contraction.group_scratch_f64[group_row + group_index] = sum; + } + for (entry, left_weights) in contraction + .logical_entries() + .zip(&selected_helicity_weights_by_entry) + { + let left = contraction.group_scratch_f64[group_row + entry.left_group_index]; + let right = contraction.group_scratch_f64[group_row + entry.right_group_index]; + let product = left * right.conj(); + let contribution = normalization_factor + * entry.symmetry_factor + * (entry.weight_re * product.re - entry.weight_im * product.im); + let Some(color_position) = selected_contracted_color_position else { + continue; + }; + for (helicity_position, weight) in left_weights { + values[row * selected_component_count + + *helicity_position * color_indices.len() + + color_position] += contribution * weight; + } + } + } + } else { + if !has_coherent_groups { + return Err(RusticolError::invalid_argument( + "resolved evaluation requires coherent amplitude-group metadata", + )); + } + let mut selected_member_weights_by_group = Vec::with_capacity(raw_sum_groups.len()); + for group in raw_sum_groups { + let reduction = physics + .numeric_reduction_by_group_id + .get(&group.id) + .ok_or_else(|| { + RusticolError::invalid_argument(format!( + "resolved metadata is missing coherent group {}", + group.id + )) + })?; + selected_member_weights_by_group.push( + reduction + .normalized_member_weights + .iter() + .copied() + .filter_map(|(helicity_index, color_index, weight)| { + Some(( + selected_helicity_positions[helicity_index]?, + selected_color_positions[color_index]?, + weight, + )) + }) + .collect::>(), + ); + } + for row in 0..batch_size { + for (group, member_weights) in + raw_sum_groups.iter().zip(&selected_member_weights_by_group) + { + let mut sum = c64(0.0, 0.0); + for index in &group.indices { + sum += amplitudes.value(row, *index, output_length); + } + let contribution = normalization_factor + * group.all_sector_weight + * (sum.re * sum.re + sum.im * sum.im); + for (helicity_position, color_position, weight) in member_weights { + values[row * selected_component_count + + *helicity_position * color_indices.len() + + *color_position] += contribution * weight; + } + } + } + } + Ok(ResolvedValues { + values, + point_count: batch_size, + helicity_indices, + color_indices, + }) + } + + /// Reduce one topology-replay source mapping directly into public totals. + /// + /// The source and target component buffers are row-sized persistent + /// scratch. Replaying the routes before summing the target row preserves + /// the resolved contract's component accumulation and H-major/C-minor + /// summation order without materializing either batch-wide tensor. + #[allow(clippy::too_many_arguments)] + pub(crate) fn reduce_scratch_f64_routed_totals_into( + &mut self, + batch_size: usize, + physics: &PhysicsRuntime, + normalization_factor: f64, + selected_helicity_ids: Option<&BTreeSet>, + selected_color_ids: Option<&BTreeSet>, + replay_entry: &LcResolvedReplayEntry, + source_component_count: usize, + target_component_count: usize, + output: &mut [f64], + ) -> RusticolResult<()> { + let amplitudes = RowMajorAmplitudeSamples::new( + &self.output_scratch_f64, + batch_size, + self.output_length, + )?; + Self::reduce_amplitude_samples_f64_routed_totals_into( + amplitudes, + batch_size, + physics, + normalization_factor, + selected_helicity_ids, + selected_color_ids, + replay_entry, + source_component_count, + target_component_count, + output, + self.output_length, + &self.raw_sum_groups, + self.has_coherent_groups, + self.color_contraction.is_some(), + &mut self.resolved_source_row_scratch_f64, + &mut self.resolved_target_row_scratch_f64, + &mut self.routed_reduction_scratch, + ) + } + + /// Plane-native counterpart of the LC topology-replay totals reducer. + #[allow(clippy::too_many_arguments)] + #[allow(dead_code)] + pub(crate) fn reduce_planes_f64_routed_totals_into( + &mut self, + amplitudes: crate::direct_arena::DirectAmplitudePlanes<'_>, + physics: &PhysicsRuntime, + normalization_factor: f64, + selected_helicity_ids: Option<&BTreeSet>, + selected_color_ids: Option<&BTreeSet>, + replay_entry: &LcResolvedReplayEntry, + source_component_count: usize, + target_component_count: usize, + output: &mut [f64], + ) -> RusticolResult<()> { + let plane_count = usize::try_from(amplitudes.component_count()?) + .map_err(|_| RusticolError::invalid_argument("amplitude plane count exceeds usize"))?; + if plane_count != self.output_length { + return Err(RusticolError::invalid_argument(format!( + "direct amplitude plane count is {plane_count}, expected {}", + self.output_length + ))); + } + let batch_size = amplitudes.point_count() as usize; + if output.len() != batch_size { + return Err(RusticolError::invalid_argument(format!( + "routed reduction output has length {}, expected {batch_size}", + output.len() + ))); + } + if self.color_contraction.is_some() { + return Err(RusticolError::invalid_argument( + "LC topology replay does not support contracted color reduction", + )); + } + if !self.has_coherent_groups { + return Err(RusticolError::invalid_argument( + "resolved evaluation requires coherent amplitude-group metadata", + )); + } + + let scratch = &mut self.routed_reduction_scratch; + let helicity_count = physics.manifest.helicities.len(); + let color_count = physics.manifest.color_components.len(); + physics + .selected_helicity_indices_into(selected_helicity_ids, &mut scratch.helicity_indices)?; + physics.selected_color_indices_into(selected_color_ids, &mut scratch.color_indices)?; + let selected_color_count = scratch.color_indices.len(); + let expected_source_component_count = scratch + .helicity_indices + .len() + .checked_mul(selected_color_count) + .ok_or_else(|| RusticolError::invalid_argument("resolved shape overflows usize"))?; + if source_component_count != expected_source_component_count { + return Err(RusticolError::integrity(format!( + "topology replay source has {source_component_count} components, expected {expected_source_component_count}" + ))); + } + for route in &replay_entry.routes { + if route.source_index >= source_component_count + || route.target_index >= target_component_count + { + return Err(RusticolError::integrity( + "LC topology replay selected route is out of bounds", + )); + } + } + + let helicity_positions = SelectedAxisPositions::new( + &scratch.helicity_indices, + helicity_count, + &mut scratch.helicity_positions, + ); + let color_positions = SelectedAxisPositions::new( + &scratch.color_indices, + color_count, + &mut scratch.color_positions, + ); + scratch.selected_member_weights.clear(); + scratch.selected_member_weight_ranges.clear(); + for group in &self.raw_sum_groups { + let reduction = physics + .numeric_reduction_by_group_id + .get(&group.id) + .ok_or_else(|| { + RusticolError::invalid_argument(format!( + "resolved metadata is missing coherent group {}", + group.id + )) + })?; + let start = scratch.selected_member_weights.len(); + for (helicity_index, color_index, weight) in + reduction.normalized_member_weights.iter().copied() + { + let (Some(helicity_position), Some(color_position)) = ( + helicity_positions.position(helicity_index), + color_positions.position(color_index), + ) else { + continue; + }; + scratch + .selected_member_weights + .push((helicity_position, color_position, weight)); + } + scratch + .selected_member_weight_ranges + .push(start..scratch.selected_member_weights.len()); + } + + let source_scalar_count = + source_component_count + .checked_mul(batch_size) + .ok_or_else(|| { + RusticolError::invalid_argument("routed source scratch shape overflows usize") + })?; + let target_scalar_count = + target_component_count + .checked_mul(batch_size) + .ok_or_else(|| { + RusticolError::invalid_argument("routed target scratch shape overflows usize") + })?; + scratch.direct_group_re.resize(batch_size, 0.0); + scratch.direct_group_im.resize(batch_size, 0.0); + scratch.direct_totals.resize(batch_size, 0.0); + scratch + .direct_source_components + .resize(source_scalar_count, 0.0); + scratch.direct_source_components.fill(0.0); + scratch + .direct_target_components + .resize(target_scalar_count, 0.0); + scratch.direct_target_components.fill(0.0); + + for (group, member_weight_range) in self + .raw_sum_groups + .iter() + .zip(&scratch.selected_member_weight_ranges) + { + if member_weight_range.is_empty() { + continue; + } + scratch.direct_group_re.fill(0.0); + scratch.direct_group_im.fill(0.0); + for index in &group.indices { + let (plane_re, plane_im) = amplitudes.plane_unchecked(*index); + for point in 0..batch_size { + scratch.direct_group_re[point] += plane_re[point]; + scratch.direct_group_im[point] += plane_im[point]; + } + } + for point in 0..batch_size { + let re = scratch.direct_group_re[point]; + let im = scratch.direct_group_im[point]; + scratch.direct_totals[point] = + normalization_factor * group.all_sector_weight * (re * re + im * im); + } + for (helicity_position, color_position, weight) in + &scratch.selected_member_weights[member_weight_range.clone()] + { + let component = *helicity_position * selected_color_count + *color_position; + let source_start = component * batch_size; + for point in 0..batch_size { + scratch.direct_source_components[source_start + point] += + scratch.direct_totals[point] * *weight; + } + } + } + + for route in &replay_entry.routes { + let source_start = route.source_index * batch_size; + let target_start = route.target_index * batch_size; + for point in 0..batch_size { + scratch.direct_target_components[target_start + point] += + route.weight * scratch.direct_source_components[source_start + point]; + } + } + output.fill(0.0); + for target_index in 0..target_component_count { + let target_start = target_index * batch_size; + for (point, target) in output.iter_mut().enumerate() { + *target += scratch.direct_target_components[target_start + point]; + } + } + Ok(()) + } + + /// Add one plane-native topology-replay mapping to persistent point-major + /// target components. + /// + /// Mappings are accumulated in caller order, while the final public fold + /// remains H-major/C-minor. This is the allocation-free multi-mapping + /// counterpart of `reduce_planes_f64_routed_totals_into`. + #[allow(clippy::too_many_arguments)] + pub(crate) fn reduce_planes_f64_routed_components_add_into( + &mut self, + amplitudes: crate::direct_arena::DirectAmplitudePlanes<'_>, + physics: &PhysicsRuntime, + normalization_factor: f64, + selected_helicity_ids: Option<&BTreeSet>, + selected_color_ids: Option<&BTreeSet>, + replay_entry: &LcResolvedReplayEntry, + source_component_count: usize, + target_component_count: usize, + target_point_count: usize, + target_point_start: usize, + target_components: &mut [f64], + ) -> RusticolResult<()> { + let plane_count = usize::try_from(amplitudes.component_count()?) + .map_err(|_| RusticolError::invalid_argument("amplitude plane count exceeds usize"))?; + if plane_count != self.output_length { + return Err(RusticolError::invalid_argument(format!( + "direct amplitude plane count is {plane_count}, expected {}", + self.output_length + ))); + } + let batch_size = amplitudes.point_count() as usize; + let expected_target_len = target_point_count + .checked_mul(target_component_count) + .ok_or_else(|| RusticolError::invalid_argument("routed target shape overflows"))?; + if target_components.len() != expected_target_len + || target_point_start + .checked_add(batch_size) + .is_none_or(|stop| stop > target_point_count) + { + return Err(RusticolError::invalid_argument( + "routed target components have an inconsistent shape", + )); + } + if self.color_contraction.is_some() { + return Err(RusticolError::invalid_argument( + "LC topology replay does not support contracted color reduction", + )); + } + if !self.has_coherent_groups { + return Err(RusticolError::invalid_argument( + "resolved evaluation requires coherent amplitude-group metadata", + )); + } + + let scratch = &mut self.routed_reduction_scratch; + let helicity_count = physics.manifest.helicities.len(); + let color_count = physics.manifest.color_components.len(); + physics + .selected_helicity_indices_into(selected_helicity_ids, &mut scratch.helicity_indices)?; + physics.selected_color_indices_into(selected_color_ids, &mut scratch.color_indices)?; + let selected_color_count = scratch.color_indices.len(); + let expected_source_component_count = scratch + .helicity_indices + .len() + .checked_mul(selected_color_count) + .ok_or_else(|| RusticolError::invalid_argument("resolved shape overflows usize"))?; + if source_component_count != expected_source_component_count { + return Err(RusticolError::integrity(format!( + "topology replay source has {source_component_count} components, expected {expected_source_component_count}" + ))); + } + for route in &replay_entry.routes { + if route.source_index >= source_component_count + || route.target_index >= target_component_count + { + return Err(RusticolError::integrity( + "LC topology replay selected route is out of bounds", + )); + } + } + + let helicity_positions = SelectedAxisPositions::new( + &scratch.helicity_indices, + helicity_count, + &mut scratch.helicity_positions, + ); + let color_positions = SelectedAxisPositions::new( + &scratch.color_indices, + color_count, + &mut scratch.color_positions, + ); + scratch.selected_member_weights.clear(); + scratch.selected_member_weight_ranges.clear(); + for group in &self.raw_sum_groups { + let reduction = physics + .numeric_reduction_by_group_id + .get(&group.id) + .ok_or_else(|| { + RusticolError::invalid_argument(format!( + "resolved metadata is missing coherent group {}", + group.id + )) + })?; + let start = scratch.selected_member_weights.len(); + for (helicity_index, color_index, weight) in + reduction.normalized_member_weights.iter().copied() + { + let (Some(helicity_position), Some(color_position)) = ( + helicity_positions.position(helicity_index), + color_positions.position(color_index), + ) else { + continue; + }; + scratch + .selected_member_weights + .push((helicity_position, color_position, weight)); + } + scratch + .selected_member_weight_ranges + .push(start..scratch.selected_member_weights.len()); + } + + let source_scalar_count = + source_component_count + .checked_mul(batch_size) + .ok_or_else(|| { + RusticolError::invalid_argument("routed source scratch shape overflows usize") + })?; + scratch.direct_group_re.resize(batch_size, 0.0); + scratch.direct_group_im.resize(batch_size, 0.0); + scratch.direct_totals.resize(batch_size, 0.0); + scratch + .direct_source_components + .resize(source_scalar_count, 0.0); + scratch.direct_source_components.fill(0.0); + + for (group, member_weight_range) in self + .raw_sum_groups + .iter() + .zip(&scratch.selected_member_weight_ranges) + { + if member_weight_range.is_empty() { + continue; + } + scratch.direct_group_re.fill(0.0); + scratch.direct_group_im.fill(0.0); + for index in &group.indices { + let (plane_re, plane_im) = amplitudes.plane_unchecked(*index); + for point in 0..batch_size { + scratch.direct_group_re[point] += plane_re[point]; + scratch.direct_group_im[point] += plane_im[point]; + } + } + for point in 0..batch_size { + let re = scratch.direct_group_re[point]; + let im = scratch.direct_group_im[point]; + scratch.direct_totals[point] = + normalization_factor * group.all_sector_weight * (re * re + im * im); + } + for (helicity_position, color_position, weight) in + &scratch.selected_member_weights[member_weight_range.clone()] + { + let component = *helicity_position * selected_color_count + *color_position; + let source_start = component * batch_size; + for point in 0..batch_size { + scratch.direct_source_components[source_start + point] += + scratch.direct_totals[point] * *weight; + } + } + } + + for route in &replay_entry.routes { + let source_start = route.source_index * batch_size; + for point in 0..batch_size { + let target = + (target_point_start + point) * target_component_count + route.target_index; + target_components[target] += + route.weight * scratch.direct_source_components[source_start + point]; + } + } + Ok(()) + } + + #[allow(clippy::too_many_arguments)] + fn reduce_amplitude_samples_f64_routed_totals_into( + amplitudes: S, + batch_size: usize, + physics: &PhysicsRuntime, + normalization_factor: f64, + selected_helicity_ids: Option<&BTreeSet>, + selected_color_ids: Option<&BTreeSet>, + replay_entry: &LcResolvedReplayEntry, + source_component_count: usize, + target_component_count: usize, + output: &mut [f64], + output_length: usize, + raw_sum_groups: &[RawSumGroup], + has_coherent_groups: bool, + has_color_contraction: bool, + resolved_source_row_scratch_f64: &mut Vec, + resolved_target_row_scratch_f64: &mut Vec, + scratch: &mut RoutedReductionScratch, + ) -> RusticolResult<()> { + if output.len() != batch_size { + return Err(RusticolError::invalid_argument(format!( + "routed reduction output has length {}, expected {batch_size}", + output.len() + ))); + } + if has_color_contraction { + return Err(RusticolError::invalid_argument( + "LC topology replay does not support contracted color reduction", + )); + } + if !has_coherent_groups { + return Err(RusticolError::invalid_argument( + "resolved evaluation requires coherent amplitude-group metadata", + )); + } + + let helicity_count = physics.manifest.helicities.len(); + let color_count = physics.manifest.color_components.len(); + physics + .selected_helicity_indices_into(selected_helicity_ids, &mut scratch.helicity_indices)?; + physics.selected_color_indices_into(selected_color_ids, &mut scratch.color_indices)?; + let expected_source_component_count = scratch + .helicity_indices + .len() + .checked_mul(scratch.color_indices.len()) + .ok_or_else(|| RusticolError::invalid_argument("resolved shape overflows usize"))?; + if source_component_count != expected_source_component_count { + return Err(RusticolError::integrity(format!( + "topology replay source has {source_component_count} components, expected {expected_source_component_count}" + ))); + } + + let helicity_positions = SelectedAxisPositions::new( + &scratch.helicity_indices, + helicity_count, + &mut scratch.helicity_positions, + ); + let color_positions = SelectedAxisPositions::new( + &scratch.color_indices, + color_count, + &mut scratch.color_positions, + ); + scratch.selected_member_weights.clear(); + scratch.selected_member_weight_ranges.clear(); + for group in raw_sum_groups { + let reduction = physics + .numeric_reduction_by_group_id + .get(&group.id) + .ok_or_else(|| { + RusticolError::invalid_argument(format!( + "resolved metadata is missing coherent group {}", + group.id + )) + })?; + let start = scratch.selected_member_weights.len(); + for (helicity_index, color_index, weight) in + reduction.normalized_member_weights.iter().copied() + { + let (Some(helicity_position), Some(color_position)) = ( + helicity_positions.position(helicity_index), + color_positions.position(color_index), + ) else { + continue; + }; + scratch + .selected_member_weights + .push((helicity_position, color_position, weight)); + } + scratch + .selected_member_weight_ranges + .push(start..scratch.selected_member_weights.len()); + } + + resolved_source_row_scratch_f64.resize(source_component_count, 0.0); + resolved_target_row_scratch_f64.resize(target_component_count, 0.0); + for (row, target_total) in output.iter_mut().enumerate() { + let source_row = &mut *resolved_source_row_scratch_f64; + source_row.fill(0.0); + for (group, member_weight_range) in raw_sum_groups + .iter() + .zip(&scratch.selected_member_weight_ranges) + { + let mut sum = c64(0.0, 0.0); + for index in &group.indices { + sum += amplitudes.value(row, *index, output_length); + } + let contribution = normalization_factor + * group.all_sector_weight + * (sum.re * sum.re + sum.im * sum.im); + for (helicity_position, color_position, weight) in + &scratch.selected_member_weights[member_weight_range.clone()] + { + source_row + [*helicity_position * scratch.color_indices.len() + *color_position] += + contribution * weight; + } + } + + let target_row = &mut *resolved_target_row_scratch_f64; + target_row.fill(0.0); + for route in &replay_entry.routes { + if route.source_index >= source_component_count + || route.target_index >= target_component_count + { + return Err(RusticolError::integrity( + "LC topology replay selected route is out of bounds", + )); + } + target_row[route.target_index] += route.weight * source_row[route.source_index]; + } + *target_total = target_row.iter().sum(); + } + Ok(()) + } + + /// Reduce amplitudes evaluated for one physical runtime-helicity domain. + /// + /// The ordinary resolved reducer distributes one representative amplitude + /// over every proof-equivalent physical helicity. A materialized + /// recurrence artifact instead reevaluates that representative with the + /// requested source states, so the selected helicity receives the full + /// contribution. Colour symmetry weights remain distributed over their + /// physical components exactly as in the ordinary resolved contract. + pub(crate) fn reduce_scratch_f64_for_materialized_helicity( + &mut self, + batch_size: usize, + physics: &PhysicsRuntime, + normalization_factor: f64, + helicity_index: usize, + root_factors: &[Option>], + selected_color_ids: Option<&BTreeSet>, + ) -> RusticolResult> { + let amplitudes = RowMajorAmplitudeSamples::new( + &self.output_scratch_f64, + batch_size, + self.output_length, + )?; + Self::reduce_amplitude_samples_f64_for_materialized_helicity( + amplitudes, + batch_size, + physics, + normalization_factor, + helicity_index, + root_factors, + selected_color_ids, + self.output_length, + &self.raw_sum_groups, + self.has_coherent_groups, + &mut self.color_contraction, + ) + } + + /// Plane-native counterpart of the materialized physical-helicity reducer. + #[allow(dead_code)] + pub(crate) fn reduce_planes_f64_for_materialized_helicity( + &mut self, + amplitudes: crate::direct_arena::DirectAmplitudePlanes<'_>, + physics: &PhysicsRuntime, + normalization_factor: f64, + helicity_index: usize, + root_factors: &[Option>], + selected_color_ids: Option<&BTreeSet>, + ) -> RusticolResult> { + let plane_count = usize::try_from(amplitudes.component_count()?) + .map_err(|_| RusticolError::invalid_argument("amplitude plane count exceeds usize"))?; + if plane_count != self.output_length { + return Err(RusticolError::invalid_argument(format!( + "direct amplitude plane count is {plane_count}, expected {}", + self.output_length + ))); + } + Self::reduce_amplitude_samples_f64_for_materialized_helicity( + amplitudes, + amplitudes.point_count() as usize, + physics, + normalization_factor, + helicity_index, + root_factors, + selected_color_ids, + self.output_length, + &self.raw_sum_groups, + self.has_coherent_groups, + &mut self.color_contraction, + ) + } + + #[allow(clippy::too_many_arguments)] + fn reduce_amplitude_samples_f64_for_materialized_helicity( + amplitudes: S, + batch_size: usize, + physics: &PhysicsRuntime, + normalization_factor: f64, + helicity_index: usize, + root_factors: &[Option>], + selected_color_ids: Option<&BTreeSet>, + output_length: usize, + raw_sum_groups: &[RawSumGroup], + has_coherent_groups: bool, + color_contraction: &mut Option, + ) -> RusticolResult> { + if root_factors.len() != output_length { + return Err(RusticolError::integrity(format!( + "helicity recurrence route table has {} roots, expected {}", + root_factors.len(), + output_length + ))); + } + if helicity_index >= physics.manifest.helicities.len() { + return Err(RusticolError::selector(format!( + "runtime helicity index {helicity_index} is out of range" + ))); + } + let color_count = physics.manifest.color_components.len(); + let color_indices = physics.selected_color_indices(selected_color_ids)?; + let mut full_values = vec![0.0; batch_size * color_count]; + + if let Some(contraction) = color_contraction.as_mut() { + if color_count != 1 || !physics.has_contracted_color_axis() { + return Err(RusticolError::invalid_argument( + "resolved NLC/full evaluation requires one contracted color component", + )); + } + if raw_sum_groups.len() != contraction.group_count { + return Err(RusticolError::invalid_argument( + "colour contraction group count does not match coherent groups", + )); + } + contraction + .group_scratch_f64 + .resize(batch_size * contraction.group_count, c64(0.0, 0.0)); + let mut group_active = vec![false; contraction.group_count]; + for (group_index, group) in raw_sum_groups.iter().enumerate() { + let reduction = physics + .numeric_reduction_by_group_id + .get(&group.id) + .ok_or_else(|| { + RusticolError::invalid_argument(format!( + "resolved metadata is missing coherent group {}", + group.id + )) + })?; + group_active[group_index] = reduction.contains_helicity(helicity_index) + && group + .indices + .iter() + .any(|index| root_factors[*index].is_some()); + } + for (row, full_value) in full_values.iter_mut().enumerate().take(batch_size) { + let group_row = row * contraction.group_count; + for (group_index, group) in raw_sum_groups.iter().enumerate() { + let mut sum = c64(0.0, 0.0); + if group_active[group_index] { + for index in &group.indices { + if let Some(factor) = root_factors[*index] { + sum += amplitudes.value(row, *index, output_length) * factor; + } + } + } + contraction.group_scratch_f64[group_row + group_index] = sum; + } + for entry in contraction.logical_entries() { + if !group_active[entry.left_group_index] + || !group_active[entry.right_group_index] + { + continue; + } + let left = contraction.group_scratch_f64[group_row + entry.left_group_index]; + let right = contraction.group_scratch_f64[group_row + entry.right_group_index]; + let product = left * right.conj(); + *full_value += normalization_factor + * entry.symmetry_factor + * (entry.weight_re * product.re - entry.weight_im * product.im); + } + } + } else { + if !has_coherent_groups { + return Err(RusticolError::invalid_argument( + "materialized helicity reduction requires coherent amplitude-group metadata", + )); + } + let mut active_groups = Vec::new(); + for (group_index, group) in raw_sum_groups.iter().enumerate() { + let reduction = physics + .numeric_reduction_by_group_id + .get(&group.id) + .ok_or_else(|| { + RusticolError::invalid_argument(format!( + "resolved metadata is missing coherent group {}", + group.id + )) + })?; + if !reduction.contains_helicity(helicity_index) + || !group + .indices + .iter() + .any(|index| root_factors[*index].is_some()) + { + continue; + } + active_groups.push((group_index, reduction.normalized_color_weights.clone())); + } + for row in 0..batch_size { + let color_row = row * color_count; + for (group_index, color_weights) in &active_groups { + let group = &raw_sum_groups[*group_index]; + let mut sum = c64(0.0, 0.0); + for index in &group.indices { + if let Some(factor) = root_factors[*index] { + sum += amplitudes.value(row, *index, output_length) * factor; + } + } + let contribution = normalization_factor + * group.all_sector_weight + * (sum.re * sum.re + sum.im * sum.im); + for (color_index, weight) in color_weights { + full_values[color_row + *color_index] += contribution * *weight; + } + } + } + } + + let mut values = Vec::with_capacity(batch_size * color_indices.len()); + for row in 0..batch_size { + for color_index in &color_indices { + values.push(full_values[row * color_count + *color_index]); + } + } + Ok(ResolvedValues { + values, + point_count: batch_size, + helicity_indices: vec![helicity_index], + color_indices, + }) + } + + /// Add one materialized physical-helicity row to caller-owned totals. + /// + /// Colour cells are accumulated in the same group order as the resolved + /// reducer, then folded into each point total in manifest colour order. + /// Keeping only one colour row preserves exact summation semantics while + /// avoiding a batch-by-colour resolved allocation. + #[allow(clippy::too_many_arguments)] + pub(crate) fn reduce_scratch_f64_for_materialized_helicity_add_into( + &mut self, + batch_size: usize, + physics: &PhysicsRuntime, + normalization_factor: f64, + helicity_index: usize, + root_factors: &[Option>], + selected_color_ids: Option<&BTreeSet>, + output: &mut [f64], + ) -> RusticolResult<()> { + let amplitudes = RowMajorAmplitudeSamples::new( + &self.output_scratch_f64, + batch_size, + self.output_length, + )?; + Self::reduce_amplitude_samples_f64_for_materialized_helicity_add_into( + amplitudes, + batch_size, + physics, + normalization_factor, + helicity_index, + root_factors, + selected_color_ids, + output, + self.output_length, + &self.raw_sum_groups, + self.has_coherent_groups, + &mut self.color_contraction, + &mut self.resolved_source_row_scratch_f64, + &mut self.routed_reduction_scratch, + ) + } + + /// Cold-bind the numeric materialized-helicity totals reducer for one + /// concrete physics/amplitude recipient. + /// + /// Parent-closure lanes deliberately pass the parent's physics metadata to + /// a child amplitude runtime. The binding key therefore includes the + /// actual `PhysicsRuntime`, rather than assuming the amplitude's owning + /// execution runtime supplies the reduction metadata. + #[allow(clippy::too_many_arguments)] + pub(crate) fn bind_materialized_helicity_direct_total_plan( + &mut self, + physics: &PhysicsRuntime, + helicity_index: usize, + root_factors: &[Option>], + selected_color_ids: Option<&BTreeSet>, + ) -> RusticolResult { + if root_factors.len() != self.output_length { + return Err(RusticolError::integrity(format!( + "helicity recurrence route table has {} roots, expected {}", + root_factors.len(), + self.output_length + ))); + } + if helicity_index >= physics.manifest.helicities.len() { + return Err(RusticolError::selector(format!( + "runtime helicity index {helicity_index} is out of range" + ))); + } + let color_indices = physics.selected_color_indices(selected_color_ids)?; + let root_factor_bits = root_factors + .iter() + .enumerate() + .filter_map(|(index, factor)| { + factor.map(|factor| (index, [factor.re.to_bits(), factor.im.to_bits()])) + }) + .collect::>(); + let key = MaterializedHelicityDirectTotalPlanKey { + physics_binding_id: physics.binding_id, + helicity_index, + root_factor_bits, + color_indices, + }; + // Common 384-helicity artifacts fit every schedule; 768-helicity + // artifacts have at most 384 non-structural/global-flip primaries and + // therefore fit as well. Parent-closure lanes use 192 schedules for the + // qq_Z6g artifact. Arbitrary dynamic selector/root-factor combinations + // remain capped instead of growing the runtime cache indefinitely. + let artifact_plan_capacity = physics.manifest.helicities.len().clamp(16, 512); + self.materialized_helicity_direct_total_plan_capacity = self + .materialized_helicity_direct_total_plan_capacity + .max(artifact_plan_capacity); + if let Some(index) = self + .materialized_helicity_direct_total_plans + .iter() + .position(|plan| plan.key == key) + { + return Ok(index); + } + + let mut roots = Vec::new(); + let mut groups = Vec::new(); + let mut bound_group_by_raw_group = vec![None; self.raw_sum_groups.len()]; + let mut color_weights_by_group = Vec::new(); + for (raw_group_index, group) in self.raw_sum_groups.iter().enumerate() { + let numeric = physics + .numeric_reduction_by_group_id + .get(&group.id) + .ok_or_else(|| { + RusticolError::invalid_argument(format!( + "resolved metadata is missing coherent group {}", + group.id + )) + })?; + if !numeric.contains_helicity(helicity_index) { + continue; + } + let root_start = roots.len(); + for output_index in &group.indices { + let factor = root_factors.get(*output_index).ok_or_else(|| { + RusticolError::integrity(format!( + "coherent group {} references missing amplitude root {output_index}", + group.id + )) + })?; + if let Some(factor) = factor { + roots.push(MaterializedHelicityDirectTotalRoot { + output_index: *output_index, + factor: *factor, + }); + } + } + if roots.len() == root_start { + continue; + } + let bound_group_index = groups.len(); + bound_group_by_raw_group[raw_group_index] = Some(bound_group_index); + let root_range = root_start..roots.len(); + let identity_output_index = match &roots[root_range.clone()] { + [root] + if root.factor.re.to_bits() == 1.0f64.to_bits() + && root.factor.im.to_bits() == 0.0f64.to_bits() => + { + Some(root.output_index) + } + _ => None, + }; + groups.push(MaterializedHelicityDirectTotalGroup { + root_range, + all_sector_weight: group.all_sector_weight, + identity_output_index, + }); + color_weights_by_group.push(numeric.normalized_color_weights.clone()); + } + + let reduction = if let Some(contraction) = self.color_contraction.as_ref() { + if !physics.has_contracted_color_axis() || physics.manifest.color_components.len() != 1 + { + return Err(RusticolError::invalid_argument( + "materialized-helicity color contraction requires one contracted color component", + )); + } + if self.raw_sum_groups.len() != contraction.group_count { + return Err(RusticolError::invalid_argument( + "colour contraction group count does not match coherent groups", + )); + } + if selected_color_ids.is_some() && key.color_indices.as_slice() != [0] { + return Err(RusticolError::selector( + "materialized-helicity color selection does not match the contracted axis", + )); + } + let entries = contraction + .logical_entries() + .filter_map(|entry| { + Some(MaterializedHelicityDirectTotalContractionEntry { + left_group_index: bound_group_by_raw_group[entry.left_group_index]?, + right_group_index: bound_group_by_raw_group[entry.right_group_index]?, + weight_re: entry.weight_re, + weight_im: entry.weight_im, + symmetry_factor: entry.symmetry_factor, + }) + }) + .collect(); + MaterializedHelicityDirectTotalReduction::Contracted { entries } + } else { + if !self.has_coherent_groups { + return Err(RusticolError::invalid_argument( + "materialized helicity reduction requires coherent amplitude-group metadata", + )); + } + let mut color_group_ranges = Vec::with_capacity(key.color_indices.len()); + let mut color_groups = Vec::new(); + for color_index in &key.color_indices { + let start = color_groups.len(); + for (group_index, color_weights) in color_weights_by_group.iter().enumerate() { + for (_, weight) in color_weights + .iter() + .filter(|(candidate, _)| candidate == color_index) + { + color_groups.push(MaterializedHelicityDirectTotalColorGroup { + group_index, + weight: *weight, + }); + } + } + color_group_ranges.push(start..color_groups.len()); + } + MaterializedHelicityDirectTotalReduction::Lc { + color_group_ranges, + color_groups, + } + }; + let plan = MaterializedHelicityDirectTotalPlan { + key, + roots, + groups, + reduction, + }; + let index = if self.materialized_helicity_direct_total_plans.len() + < self.materialized_helicity_direct_total_plan_capacity + { + let index = self.materialized_helicity_direct_total_plans.len(); + self.materialized_helicity_direct_total_plans.push(plan); + index + } else { + let index = self.materialized_helicity_direct_total_next_replacement + % self.materialized_helicity_direct_total_plans.len(); + self.materialized_helicity_direct_total_plans[index] = plan; + for cached in &mut self.materialized_helicity_direct_default_plan_by_selector_domain { + if cached.is_some_and(|cached| cached.plan_index == index) { + *cached = None; + } + } + self.materialized_helicity_direct_total_next_replacement = + (index + 1) % self.materialized_helicity_direct_total_plans.len(); + index + }; + Ok(index) + } + + /// Bind the immutable default contracted-colour plan for one authenticated + /// selector domain, reusing its plan index on subsequent warm calls. + #[allow(clippy::too_many_arguments)] + pub(crate) fn bind_default_materialized_helicity_direct_total_plan( + &mut self, + selector_domain_id: usize, + physics: &PhysicsRuntime, + helicity_index: usize, + root_factors: &[Option>], + selected_color_ids: Option<&BTreeSet>, + ) -> RusticolResult { + if selected_color_ids.is_some() || !physics.has_contracted_color_axis() { + return self.bind_materialized_helicity_direct_total_plan( + physics, + helicity_index, + root_factors, + selected_color_ids, + ); + } + let cached = self + .materialized_helicity_direct_default_plan_by_selector_domain + .get(selector_domain_id) + .copied() + .flatten(); + if let Some(cached) = cached + && cached.physics_binding_id == physics.binding_id + && cached.helicity_index == helicity_index + && self + .materialized_helicity_direct_total_plans + .get(cached.plan_index) + .is_some_and(|plan| { + plan.key.physics_binding_id == physics.binding_id + && plan.key.helicity_index == helicity_index + && plan.key.color_indices.as_slice() == [0] + }) + { + return Ok(cached.plan_index); + } + + let plan_index = self.bind_materialized_helicity_direct_total_plan( + physics, + helicity_index, + root_factors, + selected_color_ids, + )?; + if self + .materialized_helicity_direct_default_plan_by_selector_domain + .len() + <= selector_domain_id + { + self.materialized_helicity_direct_default_plan_by_selector_domain + .resize(selector_domain_id + 1, None); + } + self.materialized_helicity_direct_default_plan_by_selector_domain[selector_domain_id] = + Some(MaterializedHelicityDirectDefaultPlanCacheEntry { + physics_binding_id: physics.binding_id, + helicity_index, + plan_index, + }); + Ok(plan_index) + } + + /// Plane-native materialized-helicity totals using a cold-bound numeric + /// reduction plan. + pub(crate) fn reduce_planes_f64_for_materialized_helicity_plan_add_into( + &mut self, + amplitudes: crate::direct_arena::DirectAmplitudePlanes<'_>, + normalization_factor: f64, + plan_index: usize, + output: &mut [f64], + ) -> RusticolResult<()> { + let plane_count = usize::try_from(amplitudes.component_count()?) + .map_err(|_| RusticolError::invalid_argument("amplitude plane count exceeds usize"))?; + if plane_count != self.output_length { + return Err(RusticolError::invalid_argument(format!( + "direct amplitude plane count is {plane_count}, expected {}", + self.output_length + ))); + } + let batch_size = amplitudes.point_count() as usize; + if output.len() != batch_size { + return Err(RusticolError::invalid_argument(format!( + "materialized helicity output has length {}, expected {batch_size}", + output.len() + ))); + } + let plan = self + .materialized_helicity_direct_total_plans + .get(plan_index) + .ok_or_else(|| { + RusticolError::integrity(format!( + "materialized-helicity direct-total plan {plan_index} is not bound" + )) + })?; + let scratch = &mut self.routed_reduction_scratch; + scratch.direct_group_re.resize(batch_size, 0.0); + scratch.direct_group_im.resize(batch_size, 0.0); + scratch.direct_totals.resize(batch_size, 0.0); + + match &plan.reduction { + MaterializedHelicityDirectTotalReduction::Lc { + color_group_ranges, + color_groups, + } => { + for color_group_range in color_group_ranges { + scratch.direct_totals.fill(0.0); + for color_group in &color_groups[color_group_range.clone()] { + let group = &plan.groups[color_group.group_index]; + if let Some(output_index) = group.identity_output_index { + let (plane_re, plane_im) = amplitudes.plane_unchecked(output_index); + for point in 0..batch_size { + let re = plane_re[point]; + let im = plane_im[point]; + let norm = if re.is_finite() && im.is_finite() { + re * re + im * im + } else { + let value = c64(re, im) * c64(1.0, 0.0); + value.re * value.re + value.im * value.im + }; + let contribution = + normalization_factor * group.all_sector_weight * norm; + scratch.direct_totals[point] += contribution * color_group.weight; + } + continue; + } + scratch.direct_group_re.fill(0.0); + scratch.direct_group_im.fill(0.0); + for root in &plan.roots[group.root_range.clone()] { + let (plane_re, plane_im) = + amplitudes.plane_unchecked(root.output_index); + for point in 0..batch_size { + let value = c64(plane_re[point], plane_im[point]) * root.factor; + scratch.direct_group_re[point] += value.re; + scratch.direct_group_im[point] += value.im; + } + } + for point in 0..batch_size { + let re = scratch.direct_group_re[point]; + let im = scratch.direct_group_im[point]; + let contribution = normalization_factor + * group.all_sector_weight + * (re * re + im * im); + scratch.direct_totals[point] += contribution * color_group.weight; + } + } + for (target, total) in output.iter_mut().zip(&scratch.direct_totals) { + *target += *total; + } + } + } + MaterializedHelicityDirectTotalReduction::Contracted { entries } => { + let group_scalar_count = + plan.groups.len().checked_mul(batch_size).ok_or_else(|| { + RusticolError::invalid_argument( + "materialized-helicity group scratch shape overflows usize", + ) + })?; + scratch.direct_group_re.resize(group_scalar_count, 0.0); + scratch.direct_group_re.fill(0.0); + scratch.direct_group_im.resize(group_scalar_count, 0.0); + scratch.direct_group_im.fill(0.0); + for (group_index, group) in plan.groups.iter().enumerate() { + let group_start = group_index * batch_size; + if let Some(output_index) = group.identity_output_index { + let (plane_re, plane_im) = amplitudes.plane_unchecked(output_index); + for point in 0..batch_size { + let re = plane_re[point]; + let im = plane_im[point]; + if re.is_finite() && im.is_finite() { + scratch.direct_group_re[group_start + point] = re; + scratch.direct_group_im[group_start + point] = im; + } else { + let value = c64(re, im) * c64(1.0, 0.0); + scratch.direct_group_re[group_start + point] = value.re; + scratch.direct_group_im[group_start + point] = value.im; + } + } + continue; + } + for root in &plan.roots[group.root_range.clone()] { + let (plane_re, plane_im) = amplitudes.plane_unchecked(root.output_index); + for point in 0..batch_size { + let value = c64(plane_re[point], plane_im[point]) * root.factor; + scratch.direct_group_re[group_start + point] += value.re; + scratch.direct_group_im[group_start + point] += value.im; + } + } + } + scratch.direct_totals.fill(0.0); + for entry in entries { + for point in 0..batch_size { + let left_start = entry.left_group_index * batch_size; + let left = c64( + scratch.direct_group_re[left_start + point], + scratch.direct_group_im[left_start + point], + ); + let right_start = entry.right_group_index * batch_size; + let right = c64( + scratch.direct_group_re[right_start + point], + scratch.direct_group_im[right_start + point], + ); + let product = left * right.conj(); + scratch.direct_totals[point] += normalization_factor + * entry.symmetry_factor + * (entry.weight_re * product.re - entry.weight_im * product.im); + } + } + for (target, total) in output.iter_mut().zip(&scratch.direct_totals) { + *target += *total; + } + } + } + Ok(()) + } + + /// Plane-native counterpart of the materialized-helicity totals accumulator. + #[allow(dead_code)] + #[allow(clippy::too_many_arguments)] + pub(crate) fn reduce_planes_f64_for_materialized_helicity_add_into( + &mut self, + amplitudes: crate::direct_arena::DirectAmplitudePlanes<'_>, + physics: &PhysicsRuntime, + normalization_factor: f64, + helicity_index: usize, + root_factors: &[Option>], + selected_color_ids: Option<&BTreeSet>, + output: &mut [f64], + ) -> RusticolResult<()> { + let plan_index = self.bind_materialized_helicity_direct_total_plan( + physics, + helicity_index, + root_factors, + selected_color_ids, + )?; + self.reduce_planes_f64_for_materialized_helicity_plan_add_into( + amplitudes, + normalization_factor, + plan_index, + output, + ) + } + + /// Route one materialized-helicity tile into persistent public replay + /// components without constructing a resolved colour tensor. + #[allow(clippy::too_many_arguments)] + pub(crate) fn reduce_planes_f64_for_materialized_helicity_routed_components_add_into( + &mut self, + amplitudes: crate::direct_arena::DirectAmplitudePlanes<'_>, + physics: &PhysicsRuntime, + normalization_factor: f64, + helicity_index: usize, + root_factors: &[Option>], + selected_color_ids: Option<&BTreeSet>, + replay_entry: &LcResolvedReplayEntry, + source_component_count: usize, + target_component_count: usize, + target_point_count: usize, + target_point_start: usize, + target_components: &mut [f64], + ) -> RusticolResult<()> { + let plane_count = usize::try_from(amplitudes.component_count()?) + .map_err(|_| RusticolError::invalid_argument("amplitude plane count exceeds usize"))?; + if plane_count != self.output_length { + return Err(RusticolError::invalid_argument(format!( + "direct amplitude plane count is {plane_count}, expected {}", + self.output_length + ))); + } + if root_factors.len() != self.output_length { + return Err(RusticolError::integrity(format!( + "helicity recurrence route table has {} roots, expected {}", + root_factors.len(), + self.output_length + ))); + } + if self.color_contraction.is_some() { + return Err(RusticolError::invalid_argument( + "LC topology replay does not support contracted color reduction", + )); + } + if !self.has_coherent_groups { + return Err(RusticolError::invalid_argument( + "materialized helicity reduction requires coherent amplitude-group metadata", + )); + } + if helicity_index >= physics.manifest.helicities.len() { + return Err(RusticolError::selector(format!( + "runtime helicity index {helicity_index} is out of range" + ))); + } + let batch_size = amplitudes.point_count() as usize; + let expected_target_len = target_point_count + .checked_mul(target_component_count) + .ok_or_else(|| RusticolError::invalid_argument("routed target shape overflows"))?; + if target_components.len() != expected_target_len + || target_point_start + .checked_add(batch_size) + .is_none_or(|stop| stop > target_point_count) + { + return Err(RusticolError::invalid_argument( + "routed target components have an inconsistent shape", + )); + } + + let scratch = &mut self.routed_reduction_scratch; + let color_count = physics.manifest.color_components.len(); + physics.selected_color_indices_into(selected_color_ids, &mut scratch.color_indices)?; + if source_component_count != scratch.color_indices.len() { + return Err(RusticolError::integrity(format!( + "topology replay materialized-helicity source has {source_component_count} components, expected {}", + scratch.color_indices.len() + ))); + } + for route in &replay_entry.routes { + if route.source_index >= source_component_count + || route.target_index >= target_component_count + { + return Err(RusticolError::integrity( + "LC topology replay selected route is out of bounds", + )); + } + } + let color_positions = SelectedAxisPositions::new( + &scratch.color_indices, + color_count, + &mut scratch.color_positions, + ); + + scratch.selected_member_weights.clear(); + scratch.selected_member_weight_ranges.clear(); + for group in &self.raw_sum_groups { + let start = scratch.selected_member_weights.len(); + let reduction = physics + .numeric_reduction_by_group_id + .get(&group.id) + .ok_or_else(|| { + RusticolError::invalid_argument(format!( + "resolved metadata is missing coherent group {}", + group.id + )) + })?; + if !reduction.contains_helicity(helicity_index) + || !group + .indices + .iter() + .any(|index| root_factors[*index].is_some()) + { + scratch.selected_member_weight_ranges.push(start..start); + continue; + } + for (color_index, weight) in reduction.normalized_color_weights.iter().copied() { + if let Some(color_position) = color_positions.position(color_index) { + scratch + .selected_member_weights + .push((0, color_position, weight)); + } + } + scratch + .selected_member_weight_ranges + .push(start..scratch.selected_member_weights.len()); + } + + let source_scalar_count = + source_component_count + .checked_mul(batch_size) + .ok_or_else(|| { + RusticolError::invalid_argument("routed source scratch shape overflows usize") + })?; + scratch.direct_group_re.resize(batch_size, 0.0); + scratch.direct_group_im.resize(batch_size, 0.0); + scratch.direct_totals.resize(batch_size, 0.0); + scratch + .direct_source_components + .resize(source_scalar_count, 0.0); + scratch.direct_source_components.fill(0.0); + for (group, color_weight_range) in self + .raw_sum_groups + .iter() + .zip(&scratch.selected_member_weight_ranges) + { + if color_weight_range.is_empty() { + continue; + } + scratch.direct_group_re.fill(0.0); + scratch.direct_group_im.fill(0.0); + for index in &group.indices { + let Some(factor) = root_factors[*index] else { + continue; + }; + let (plane_re, plane_im) = amplitudes.plane_unchecked(*index); + for point in 0..batch_size { + let value = c64(plane_re[point], plane_im[point]) * factor; + scratch.direct_group_re[point] += value.re; + scratch.direct_group_im[point] += value.im; + } + } + for point in 0..batch_size { + let re = scratch.direct_group_re[point]; + let im = scratch.direct_group_im[point]; + scratch.direct_totals[point] = + normalization_factor * group.all_sector_weight * (re * re + im * im); + } + for (_, color_position, weight) in + &scratch.selected_member_weights[color_weight_range.clone()] + { + let source_start = *color_position * batch_size; + for point in 0..batch_size { + scratch.direct_source_components[source_start + point] += + scratch.direct_totals[point] * *weight; + } + } + } + + for route in &replay_entry.routes { + let source_start = route.source_index * batch_size; + for point in 0..batch_size { + let target = + (target_point_start + point) * target_component_count + route.target_index; + target_components[target] += + route.weight * scratch.direct_source_components[source_start + point]; + } + } + Ok(()) + } + + #[allow(clippy::too_many_arguments)] + fn reduce_amplitude_samples_f64_for_materialized_helicity_add_into( + amplitudes: S, + batch_size: usize, + physics: &PhysicsRuntime, + normalization_factor: f64, + helicity_index: usize, + root_factors: &[Option>], + selected_color_ids: Option<&BTreeSet>, + output: &mut [f64], + output_length: usize, + raw_sum_groups: &[RawSumGroup], + has_coherent_groups: bool, + color_contraction: &mut Option, + resolved_source_row_scratch_f64: &mut Vec, + scratch: &mut RoutedReductionScratch, + ) -> RusticolResult<()> { + if output.len() != batch_size { + return Err(RusticolError::invalid_argument(format!( + "materialized helicity output has length {}, expected {batch_size}", + output.len() + ))); + } + if root_factors.len() != output_length { + return Err(RusticolError::integrity(format!( + "helicity recurrence route table has {} roots, expected {}", + root_factors.len(), + output_length + ))); + } + if helicity_index >= physics.manifest.helicities.len() { + return Err(RusticolError::selector(format!( + "runtime helicity index {helicity_index} is out of range" + ))); + } + if let Some(contraction) = color_contraction.as_mut() { + if !physics.has_contracted_color_axis() || physics.manifest.color_components.len() != 1 + { + return Err(RusticolError::invalid_argument( + "materialized-helicity color contraction requires one contracted color component", + )); + } + if raw_sum_groups.len() != contraction.group_count { + return Err(RusticolError::invalid_argument( + "colour contraction group count does not match coherent groups", + )); + } + if let Some(ids) = selected_color_ids + && (ids.len() != 1 + || !ids + .iter() + .all(|id| physics.color_index_by_id.contains_key(id))) + { + return Err(RusticolError::selector( + "materialized-helicity color selection does not match the contracted axis", + )); + } + contraction + .group_scratch_f64 + .resize(contraction.group_count, c64(0.0, 0.0)); + for (row, target) in output.iter_mut().enumerate() { + for (group_index, group) in raw_sum_groups.iter().enumerate() { + let reduction = physics + .numeric_reduction_by_group_id + .get(&group.id) + .ok_or_else(|| { + RusticolError::invalid_argument(format!( + "resolved metadata is missing coherent group {}", + group.id + )) + })?; + let active = reduction.contains_helicity(helicity_index) + && group + .indices + .iter() + .any(|index| root_factors[*index].is_some()); + let mut sum = c64(0.0, 0.0); + if active { + for index in &group.indices { + if let Some(factor) = root_factors[*index] { + sum += amplitudes.value(row, *index, output_length) * factor; + } + } + } + contraction.group_scratch_f64[group_index] = sum; + } + let mut contribution = 0.0; + for entry in contraction.logical_entries() { + let left = contraction.group_scratch_f64[entry.left_group_index]; + let right = contraction.group_scratch_f64[entry.right_group_index]; + let product = left * right.conj(); + contribution += normalization_factor + * entry.symmetry_factor + * (entry.weight_re * product.re - entry.weight_im * product.im); + } + *target += contribution; + } + return Ok(()); + } + if !has_coherent_groups { + return Err(RusticolError::invalid_argument( + "materialized helicity reduction requires coherent amplitude-group metadata", + )); + } + let color_count = physics.manifest.color_components.len(); + physics.selected_color_indices_into(selected_color_ids, &mut scratch.color_indices)?; + + scratch.selected_member_weights.clear(); + scratch.selected_member_weight_ranges.clear(); + for group in raw_sum_groups { + let start = scratch.selected_member_weights.len(); + let reduction = physics + .numeric_reduction_by_group_id + .get(&group.id) + .ok_or_else(|| { + RusticolError::invalid_argument(format!( + "resolved metadata is missing coherent group {}", + group.id + )) + })?; + if !reduction.contains_helicity(helicity_index) + || !group + .indices + .iter() + .any(|index| root_factors[*index].is_some()) + { + scratch.selected_member_weight_ranges.push(start..start); + continue; + } + for (color_index, weight) in reduction.normalized_color_weights.iter().copied() { + scratch + .selected_member_weights + .push((0, color_index, weight)); + } + let stop = scratch.selected_member_weights.len(); + scratch.selected_member_weight_ranges.push(start..stop); + } + + resolved_source_row_scratch_f64.resize(color_count, 0.0); + for (row, target_total) in output.iter_mut().enumerate() { + let color_row = &mut *resolved_source_row_scratch_f64; + color_row.fill(0.0); + for (group, color_weight_range) in raw_sum_groups + .iter() + .zip(&scratch.selected_member_weight_ranges) + { + if color_weight_range.is_empty() { + continue; + } + let mut sum = c64(0.0, 0.0); + for index in &group.indices { + if let Some(factor) = root_factors[*index] { + sum += amplitudes.value(row, *index, output_length) * factor; + } + } + let contribution = normalization_factor + * group.all_sector_weight + * (sum.re * sum.re + sum.im * sum.im); + for (_, color_index, weight) in + &scratch.selected_member_weights[color_weight_range.clone()] + { + color_row[*color_index] += contribution * *weight; + } + } + for color_index in &scratch.color_indices { + *target_total += color_row[*color_index]; + } + } + Ok(()) + } + + #[cfg(feature = "symbolica-runtime")] + #[allow(clippy::too_many_arguments)] + pub(crate) fn evaluate_materialized_helicity_generic( + &mut self, + batch_size: usize, + state: &[Complex], + binary_precision: Option, + physics: &PhysicsRuntime, + normalization_factor: f64, + helicity_index: usize, + root_factors: &[Option>], + selected_color_ids: Option<&BTreeSet>, + active_chunk_indices: &[usize], + ) -> RusticolResult<(ResolvedValues, f64, f64, f64)> + where + T: RusticolHighPrecisionNumber, + Complex: Real + EvaluationDomain, + { + let (amplitudes, input_pack_s, evaluator_call_s) = self.evaluate_selected_outputs_generic( + batch_size, + state, + binary_precision, + Some(active_chunk_indices), + )?; + let reduction_start = Instant::now(); + if root_factors.len() != self.output_length { + return Err(RusticolError::integrity(format!( + "helicity recurrence route table has {} roots, expected {}", + root_factors.len(), + self.output_length + ))); + } + let helicity = physics + .manifest + .helicities + .get(helicity_index) + .ok_or_else(|| { + RusticolError::selector(format!( + "runtime helicity index {helicity_index} is out of range" + )) + })?; + let color_count = physics.manifest.color_components.len(); + let color_indices = physics.selected_color_indices(selected_color_ids)?; + let mut full_values = vec![T::new_zero(); batch_size * color_count]; + + if let Some(contraction) = self.color_contraction.as_ref() { + if color_count != 1 || !physics.has_contracted_color_axis() { + return Err(RusticolError::invalid_argument( + "resolved NLC/full evaluation requires one contracted color component", + )); + } + if self.raw_sum_groups.len() != contraction.group_count { + return Err(RusticolError::invalid_argument( + "colour contraction group count does not match coherent groups", + )); + } + let mut group_active = vec![false; contraction.group_count]; + for (group_index, group) in self.raw_sum_groups.iter().enumerate() { + let reduction = physics + .reduction_by_group_id + .get(&group.id) + .ok_or_else(|| { + RusticolError::invalid_argument(format!( + "resolved metadata is missing coherent group {}", + group.id + )) + })?; + group_active[group_index] = reduction + .physical_helicity_ids + .iter() + .any(|id| id == &helicity.id) + && group + .indices + .iter() + .any(|index| root_factors[*index].is_some()); + } + let mut group_values = vec![complex_zero::(); batch_size * contraction.group_count]; + for (row, full_value) in full_values.iter_mut().enumerate() { + let row_offset = row * self.output_length; + let group_row = row * contraction.group_count; + for (group_index, group) in self.raw_sum_groups.iter().enumerate() { + if !group_active[group_index] { + continue; + } + let mut sum = complex_zero::(); + for index in &group.indices { + let Some(factor) = root_factors[*index] else { + continue; + }; + sum += amplitudes[row_offset + *index].clone() + * c_generic(T::from(factor.re), T::from(factor.im)); + } + group_values[group_row + group_index] = sum; + } + for entry in contraction.logical_entries() { + if !group_active[entry.left_group_index] + || !group_active[entry.right_group_index] + { + continue; + } + let left = &group_values[group_row + entry.left_group_index]; + let right = &group_values[group_row + entry.right_group_index]; + let product_re = + left.re.clone() * right.re.clone() + left.im.clone() * right.im.clone(); + let product_im = + left.im.clone() * right.re.clone() - left.re.clone() * right.im.clone(); + *full_value += T::from(normalization_factor * entry.symmetry_factor) + * (T::from(entry.weight_re) * product_re + - T::from(entry.weight_im) * product_im); + } + } + } else { + if !self.has_coherent_groups { + return Err(RusticolError::invalid_argument( + "materialized helicity reduction requires coherent amplitude-group metadata", + )); + } + for row in 0..batch_size { + let row_offset = row * self.output_length; + let color_row = row * color_count; + for group in &self.raw_sum_groups { + let reduction = + physics + .reduction_by_group_id + .get(&group.id) + .ok_or_else(|| { + RusticolError::invalid_argument(format!( + "resolved metadata is missing coherent group {}", + group.id + )) + })?; + if !reduction + .physical_helicity_ids + .iter() + .any(|id| id == &helicity.id) + { + continue; + } + let mut sum = complex_zero::(); + for index in &group.indices { + let Some(factor) = root_factors[*index] else { + continue; + }; + sum += amplitudes[row_offset + *index].clone() + * c_generic(T::from(factor.re), T::from(factor.im)); + } + let color_weights = reduction + .physical_color_ids + .iter() + .map(|id| { + let index = *physics.color_index_by_id.get(id).ok_or_else(|| { + RusticolError::artifact(format!( + "resolved reduction group {} references unknown color {id:?}", + group.id + )) + })?; + Ok(( + index, + physics.manifest.color_components[index].coefficient(), + )) + }) + .collect::>>()?; + let total_color_weight = + color_weights.iter().map(|(_, weight)| *weight).sum::(); + if !total_color_weight.is_finite() || total_color_weight <= 0.0 { + return Err(RusticolError::artifact(format!( + "resolved reduction group {} has no positive color weight", + group.id + ))); + } + let contribution = T::from(normalization_factor * group.all_sector_weight) + * (sum.re.clone() * sum.re + sum.im.clone() * sum.im); + for (color_index, weight) in color_weights { + full_values[color_row + color_index] += + contribution.clone() * T::from(weight / total_color_weight); + } + } + } + } + + let mut values = Vec::with_capacity(batch_size * color_indices.len()); + for row in 0..batch_size { + for color_index in &color_indices { + values.push(full_values[row * color_count + *color_index].clone()); + } + } + Ok(( + ResolvedValues { + values, + point_count: batch_size, + helicity_indices: vec![helicity_index], + color_indices, + }, + input_pack_s, + evaluator_call_s, + reduction_start.elapsed().as_secs_f64(), + )) + } + + #[cfg(feature = "symbolica-runtime")] + pub(crate) fn evaluate_outputs_generic( + &mut self, + batch_size: usize, + state: &[Complex], + binary_precision: Option, + ) -> RusticolResult<(Vec>, f64, f64)> + where + T: RusticolHighPrecisionNumber, + Complex: Real + EvaluationDomain, + { + self.evaluate_selected_outputs_generic(batch_size, state, binary_precision, None) + } + + #[cfg(feature = "symbolica-runtime")] + fn evaluate_selected_outputs_generic( + &mut self, + batch_size: usize, + state: &[Complex], + binary_precision: Option, + active_chunk_indices: Option<&[usize]>, + ) -> RusticolResult<(Vec>, f64, f64)> + where + T: RusticolHighPrecisionNumber, + Complex: Real + EvaluationDomain, + { + let evaluator = self.evaluator.as_mut().ok_or_else(|| { + RusticolError::integrity( + "compiled plane-arena reducer cannot execute an exact dense amplitude evaluator", + ) + })?; + let mut input_pack_elapsed = Duration::ZERO; + let (evaluated, evaluator_call_s) = if let Some(input_components) = + self.input_components.as_ref() + { + let local_parameter_count = input_components.len(); + let global_parameter_count = state.len().checked_div(batch_size).ok_or_else(|| { + RusticolError::invalid_argument("generic amplitude batch size is zero") + })?; + let pack_start = Instant::now(); + let mut parameter_scratch = + vec![complex_zero::(); batch_size * local_parameter_count]; + for row in 0..batch_size { + let row_state = row * global_parameter_count; + let row_params = row * local_parameter_count; + if self.input_spans.is_empty() { + for (local_index, global_index) in input_components.iter().enumerate() { + parameter_scratch[row_params + local_index] = + state[row_state + *global_index].clone(); + } + } else { + for (local_start, global_start, len) in &self.input_spans { + let target_start = row_params + *local_start; + let source_start = row_state + *global_start; + parameter_scratch[target_start..target_start + *len] + .clone_from_slice(&state[source_start..source_start + *len]); + } + } + } + input_pack_elapsed = pack_start.elapsed(); + let eval_start = Instant::now(); + let evaluated = evaluator.evaluate_selected_chunks_generic( + batch_size, + ¶meter_scratch, + binary_precision, + active_chunk_indices, + )?; + (evaluated, eval_start.elapsed().as_secs_f64()) + } else { + let eval_start = Instant::now(); + let evaluated = evaluator.evaluate_selected_chunks_generic( + batch_size, + state, + binary_precision, + active_chunk_indices, + )?; + (evaluated, eval_start.elapsed().as_secs_f64()) + }; + if evaluated.len() != batch_size * self.output_length { + return Err(RusticolError::invalid_argument(format!( + "generic amplitude output buffer has length {}, expected {}", + evaluated.len(), + batch_size * self.output_length + ))); + } + Ok(( + evaluated, + profile_duration_seconds(input_pack_elapsed), + evaluator_call_s, + )) + } + + #[cfg(feature = "symbolica-runtime")] + pub(crate) fn evaluate_raw_sums_generic( + &mut self, + batch_size: usize, + state: &[Complex], + binary_precision: Option, + ) -> RusticolResult<(Vec, f64, f64)> + where + T: RusticolHighPrecisionNumber, + Complex: Real + EvaluationDomain, + { + let (evaluated, input_pack_s, evaluator_call_s) = + self.evaluate_outputs_generic(batch_size, state, binary_precision)?; + let mut raw_sums = vec![T::new_zero(); batch_size]; + if let Some(contraction) = self.color_contraction.as_ref() { + if self.raw_sum_groups.len() != contraction.group_count { + return Err(RusticolError::invalid_argument( + "colour contraction group count does not match coherent groups", + )); + } + let mut group_values = vec![complex_zero::(); batch_size * contraction.group_count]; + for (row, raw_sum) in raw_sums.iter_mut().enumerate() { + let row_offset = row * self.output_length; + let group_row = row * contraction.group_count; + for (group_index, group) in self.raw_sum_groups.iter().enumerate() { + let mut sum = complex_zero::(); + for index in &group.indices { + sum.re += evaluated[row_offset + *index].re.clone(); + sum.im += evaluated[row_offset + *index].im.clone(); + } + group_values[group_row + group_index] = sum; + } + for entry in contraction.logical_entries() { + let left = &group_values[group_row + entry.left_group_index]; + let right = &group_values[group_row + entry.right_group_index]; + let product_re = + left.re.clone() * right.re.clone() + left.im.clone() * right.im.clone(); + let product_im = + left.im.clone() * right.re.clone() - left.re.clone() * right.im.clone(); + *raw_sum += T::from(entry.symmetry_factor) + * (T::from(entry.weight_re) * product_re + - T::from(entry.weight_im) * product_im); + } + } + return Ok((raw_sums, input_pack_s, evaluator_call_s)); + } + for (row, raw_sum) in raw_sums.iter_mut().enumerate() { + let row_offset = row * self.output_length; + if self.has_coherent_groups { + for group in &self.raw_sum_groups { + let mut sum_re = T::new_zero(); + let mut sum_im = T::new_zero(); + for index in &group.indices { + let value = &evaluated[row_offset + *index]; + sum_re += value.re.clone(); + sum_im += value.im.clone(); + } + *raw_sum += T::from(group.all_sector_weight) + * (sum_re.clone() * sum_re + sum_im.clone() * sum_im); + } + continue; + } + for index in 0..self.output_length { + let value = &evaluated[row_offset + index]; + *raw_sum += T::from(self.raw_sum_all_sector_weights[index]) + * (value.re.clone() * value.re.clone() + value.im.clone() * value.im.clone()); + } + } + Ok((raw_sums, input_pack_s, evaluator_call_s)) + } + + #[cfg(feature = "symbolica-runtime")] + #[allow(clippy::too_many_arguments)] + pub(crate) fn evaluate_resolved_generic( + &mut self, + batch_size: usize, + state: &[Complex], + binary_precision: Option, + physics: &PhysicsRuntime, + normalization_factor: f64, + selected_helicity_ids: Option<&BTreeSet>, + selected_color_ids: Option<&BTreeSet>, + ) -> RusticolResult<(ResolvedValues, f64, f64)> + where + T: RusticolHighPrecisionNumber, + Complex: Real + EvaluationDomain, + { + let (evaluated, input_pack_s, evaluator_call_s) = + self.evaluate_outputs_generic(batch_size, state, binary_precision)?; + let helicity_count = physics.manifest.helicities.len(); + let color_count = physics.manifest.color_components.len(); + let mut full_values = vec![T::new_zero(); batch_size * helicity_count * color_count]; + if let Some(contraction) = self.color_contraction.as_ref() { + if color_count != 1 || !physics.has_contracted_color_axis() { + return Err(RusticolError::invalid_argument( + "resolved NLC/full evaluation requires one contracted color component", + )); + } + let mut group_values = vec![complex_zero::(); batch_size * contraction.group_count]; + for row in 0..batch_size { + let row_offset = row * self.output_length; + let group_row = row * contraction.group_count; + for (group_index, group) in self.raw_sum_groups.iter().enumerate() { + let mut sum = complex_zero::(); + for index in &group.indices { + sum.re += evaluated[row_offset + *index].re.clone(); + sum.im += evaluated[row_offset + *index].im.clone(); + } + group_values[group_row + group_index] = sum; + } + for entry in contraction.logical_entries() { + let left_group = &self.raw_sum_groups[entry.left_group_index]; + let right_group = &self.raw_sum_groups[entry.right_group_index]; + let left_reduction = physics + .reduction_by_group_id + .get(&left_group.id) + .ok_or_else(|| { + RusticolError::invalid_argument(format!( + "resolved metadata is missing coherent group {}", + left_group.id + )) + })?; + let right_reduction = physics + .reduction_by_group_id + .get(&right_group.id) + .ok_or_else(|| { + RusticolError::invalid_argument(format!( + "resolved metadata is missing coherent group {}", + right_group.id + )) + })?; + if left_reduction.physical_helicity_ids != right_reduction.physical_helicity_ids + { + return Err(RusticolError::invalid_argument( + "colour contraction mixed distinct physical helicities", + )); + } + let left_weights = physics.normalized_helicity_weights(left_reduction)?; + let right_weights = physics.normalized_helicity_weights(right_reduction)?; + if left_weights.len() != right_weights.len() + || left_weights.iter().zip(&right_weights).any( + |((left_index, left_weight), (right_index, right_weight))| { + left_index != right_index + || left_weight.to_bits() != right_weight.to_bits() + }, + ) + { + return Err(RusticolError::invalid_argument( + "colour contraction groups have inconsistent helicity weights", + )); + } + let left = &group_values[group_row + entry.left_group_index]; + let right = &group_values[group_row + entry.right_group_index]; + let product_re = + left.re.clone() * right.re.clone() + left.im.clone() * right.im.clone(); + let product_im = + left.im.clone() * right.re.clone() - left.re.clone() * right.im.clone(); + let coefficient = normalization_factor * entry.symmetry_factor; + let contribution = T::from(coefficient) + * (T::from(entry.weight_re) * product_re + - T::from(entry.weight_im) * product_im); + for (helicity_index, weight) in left_weights { + full_values[(row * helicity_count + helicity_index) * color_count] += + contribution.clone() * T::from(weight); + } + } + } + } else { + for row in 0..batch_size { + let row_offset = row * self.output_length; + for group in &self.raw_sum_groups { + let reduction = + physics + .reduction_by_group_id + .get(&group.id) + .ok_or_else(|| { + RusticolError::invalid_argument(format!( + "resolved metadata is missing coherent group {}", + group.id + )) + })?; + let mut sum_re = T::new_zero(); + let mut sum_im = T::new_zero(); + for index in &group.indices { + sum_re += evaluated[row_offset + *index].re.clone(); + sum_im += evaluated[row_offset + *index].im.clone(); + } + let contribution = T::from(normalization_factor * group.all_sector_weight) + * (sum_re.clone() * sum_re + sum_im.clone() * sum_im); + for (helicity_index, color_index, weight) in + physics.normalized_member_weights(reduction)? + { + full_values[(row * helicity_count + helicity_index) * color_count + + color_index] += contribution.clone() * T::from(weight); + } + } + } + } + let helicity_indices = physics.selected_helicity_indices(selected_helicity_ids)?; + let color_indices = physics.selected_color_indices(selected_color_ids)?; + let mut values = + Vec::with_capacity(batch_size * helicity_indices.len() * color_indices.len()); + for row in 0..batch_size { + for helicity_index in &helicity_indices { + for color_index in &color_indices { + values.push( + full_values + [(row * helicity_count + *helicity_index) * color_count + *color_index] + .clone(), + ); + } + } + } + Ok(( + ResolvedValues { + values, + point_count: batch_size, + helicity_indices, + color_indices, + }, + input_pack_s, + evaluator_call_s, + )) + } +} + +fn reduce_compiled_symmetric_group_planes( + amplitudes: crate::direct_arena::DirectAmplitudePlanes<'_>, + output: &mut [f64], + groups: &[RawSumGroup], + symmetric_group: &mut CompiledSymmetricGroupColorContraction, +) -> RusticolResult<()> { + let point_count = amplitudes.point_count() as usize; + if output.len() != point_count { + return Err(RusticolError::invalid_argument( + "compiled symmetric-group FFT output has the wrong point count", + )); + } + let reducer = match symmetric_group.plan.runtime_reducer() { + Some(crate::recurrence::RuntimeColorContractionReducer::SymmetricGroupFourier(reducer)) => { + reducer + } + _ => { + return Err(RusticolError::integrity( + "compiled symmetric-group FFT payload lost its runtime reducer", + )); + } + }; + let ordered_group_indices = &symmetric_group.ordered_group_indices; + let workspace = symmetric_group.workspace.as_mut().ok_or_else(|| { + RusticolError::integrity("compiled symmetric-group FFT workspace was not cold-prepared") + })?; + reducer.reduce_lanes(workspace, point_count, |local_group, point| { + let group_index = *ordered_group_indices.get(local_group).ok_or_else(|| { + RusticolError::integrity( + "compiled symmetric-group FFT local group binding is incomplete", + ) + })?; + let group = groups.get(group_index).ok_or_else(|| { + RusticolError::integrity( + "compiled symmetric-group FFT coherent group binding is out of bounds", + ) + })?; + let mut real = 0.0; + let mut imaginary = 0.0; + for output_index in &group.indices { + let (plane_re, plane_im) = amplitudes.plane_unchecked(*output_index); + real += plane_re[point]; + imaginary += plane_im[point]; + } + Ok((real, imaginary)) + })?; + output.copy_from_slice(workspace.reduced(point_count)?); + Ok(()) +} + +pub(crate) fn build_raw_sum_groups( + output_length: usize, + weights: &[f64], + all_sector_weights: &[f64], + group_ids: &[Option], + color_sector_ids: &[Option], +) -> RusticolResult> { + if weights.len() != output_length + || all_sector_weights.len() != output_length + || group_ids.len() != output_length + || color_sector_ids.len() != output_length + { + return Err(RusticolError::invalid_argument( + "raw-sum group metadata length does not match amplitude outputs", + )); + } + let mut grouped: BTreeMap> = BTreeMap::new(); + let mut groups = Vec::new(); + for index in 0..output_length { + if let Some(group_id) = group_ids[index] { + grouped.entry(group_id).or_default().push(index); + } else { + groups.push(RawSumGroup { + id: index as i64, + indices: vec![index], + weight: weights[index], + all_sector_weight: all_sector_weights[index], + sector_ids: color_sector_ids[index].into_iter().collect(), + }); + } + } + for (group_id, indices) in grouped { + let weight = weights[indices[0]]; + let all_sector_weight = all_sector_weights[indices[0]]; + if indices + .iter() + .any(|index| (weights[*index] - weight).abs() > 0.0) + { + return Err(RusticolError::invalid_argument(format!( + "coherent amplitude group {group_id} has inconsistent raw-sum weights" + ))); + } + if indices + .iter() + .any(|index| (all_sector_weights[*index] - all_sector_weight).abs() > 0.0) + { + return Err(RusticolError::invalid_argument(format!( + "coherent amplitude group {group_id} has inconsistent all-sector raw-sum weights" + ))); + } + groups.push(RawSumGroup { + id: group_id, + sector_ids: unique_color_sector_ids(&indices, color_sector_ids), + indices, + weight, + all_sector_weight, + }); + } + Ok(groups) +} + +fn build_color_topology_replay_amplitude_runtime( + manifest: Option<&GenericColorTopologyReplayAmplitudeManifest>, + contraction_manifest: Option<&GenericColorContractionManifest>, + materialized_groups: &[RawSumGroup], +) -> RusticolResult> { + let Some(manifest) = manifest else { + return Ok(None); + }; + if manifest.contract_version != 1 + || manifest.physical_group_count == 0 + || manifest.physical_groups.len() != manifest.physical_group_count + || manifest.mappings.is_empty() + { + return Err(RusticolError::artifact( + "color topology replay amplitude gather has an invalid header", + )); + } + let source_index_by_id = materialized_groups + .iter() + .enumerate() + .map(|(index, group)| (group.id, index)) + .collect::>(); + if source_index_by_id.len() != materialized_groups.len() { + return Err(RusticolError::artifact( + "color topology replay materialized coherent-group ids are not unique", + )); + } + let mut physical_groups = Vec::with_capacity(manifest.physical_group_count); + let mut physical_group_helicities = Vec::with_capacity(manifest.physical_group_count); + for (index, group) in manifest.physical_groups.iter().enumerate() { + if group.group_id != index as i64 + || group.color_sector_id < 0 + || group.color_word.is_empty() + || group.helicities.is_empty() + || !group.helicity_weight.is_finite() + || group.helicity_weight <= 0.0 + { + return Err(RusticolError::artifact( + "color topology replay physical coherent-group metadata is invalid", + )); + } + physical_groups.push(RawSumGroup { + id: group.group_id, + indices: vec![index], + weight: group.helicity_weight, + all_sector_weight: group.helicity_weight, + sector_ids: vec![group.color_sector_id], + }); + physical_group_helicities.push(group.helicities.clone()); + } + let color_contraction = + build_color_contraction_runtime(contraction_manifest, &physical_groups)?.ok_or_else( + || { + RusticolError::artifact( + "color topology replay requires a complete NLC/full color contraction", + ) + }, + )?; + let mut target_coverage = vec![false; manifest.physical_group_count]; + let mut mappings = Vec::with_capacity(manifest.mappings.len()); + for mapping in &manifest.mappings { + let label_permutation = + normalized_color_topology_label_permutation(&mapping.label_permutation)?; + if mapping.group_routes.is_empty() { + return Err(RusticolError::artifact( + "color topology replay mapping contains no amplitude routes", + )); + } + let mut group_routes = Vec::with_capacity(mapping.group_routes.len()); + let mut mapping_sources = BTreeSet::new(); + for route in &mapping.group_routes { + let source_group_index = source_index_by_id + .get(&route.source_group_id) + .copied() + .ok_or_else(|| { + RusticolError::artifact( + "color topology replay route names an unknown materialized group", + ) + })?; + let target_group_index = usize::try_from(route.target_group_id) + .ok() + .filter(|index| *index < manifest.physical_group_count) + .ok_or_else(|| { + RusticolError::artifact( + "color topology replay route names an unknown physical group", + ) + })?; + let factor = match route.factor.as_slice() { + [real, imaginary] if real.is_finite() && imaginary.is_finite() => { + c64(*real, *imaginary) + } + _ => { + return Err(RusticolError::artifact( + "color topology replay amplitude factor must be finite complex data", + )); + } + }; + if factor == c64(0.0, 0.0) + || !mapping_sources.insert(source_group_index) + || std::mem::replace(&mut target_coverage[target_group_index], true) + { + return Err(RusticolError::artifact( + "color topology replay amplitude routes are not a bijection", + )); + } + group_routes.push(ColorTopologyReplayAmplitudeGroupRoute { + source_group_index, + target_group_index, + factor, + }); + } + group_routes.sort_by_key(|route| route.target_group_index); + mappings.push(ColorTopologyReplayAmplitudeMapping { + label_permutation, + group_routes, + }); + } + if target_coverage.iter().any(|covered| !covered) { + return Err(RusticolError::artifact( + "color topology replay amplitude routes do not cover every physical group", + )); + } + Ok(Some(ColorTopologyReplayAmplitudeRuntime { + mappings, + physical_groups, + physical_group_helicities, + color_contraction: Some(color_contraction), + unit_weights: vec![1.0; manifest.physical_group_count], + no_sector_ids: vec![None; manifest.physical_group_count], + physical_group_scratch_f64: Vec::new(), + covered_groups: vec![false; manifest.physical_group_count], + })) +} + +fn normalized_color_topology_label_permutation( + values: &[LcTopologyReplayLabelPermutationManifest], +) -> RusticolResult> { + let mut representatives = BTreeSet::new(); + let mut sectors = BTreeSet::new(); + let mut mapping = Vec::new(); + for item in values { + if item.representative_label == 0 || item.sector_label == 0 { + return Err(RusticolError::artifact( + "color topology replay label permutations use one-based labels", + )); + } + let representative = item.representative_label - 1; + let sector = item.sector_label - 1; + if !representatives.insert(representative) || !sectors.insert(sector) { + return Err(RusticolError::artifact( + "color topology replay label permutation is not one-to-one", + )); + } + if representative != sector { + mapping.push((representative, sector)); + } + } + if representatives != sectors { + return Err(RusticolError::artifact( + "color topology replay label permutation is not a permutation", + )); + } + mapping.sort_unstable(); + Ok(mapping) +} + +pub(crate) fn unique_color_sector_ids( + indices: &[usize], + color_sector_ids: &[Option], +) -> Vec { + indices + .iter() + .filter_map(|index| color_sector_ids.get(*index).copied().flatten()) + .collect::>() + .into_iter() + .collect() +} + +pub(crate) fn raw_sum_group_is_selected( + group: &RawSumGroup, + selected_color_sector_ids: Option<&BTreeSet>, +) -> bool { + let Some(selected) = selected_color_sector_ids else { + return true; + }; + group + .sector_ids + .iter() + .any(|sector_id| selected.contains(sector_id)) +} + +pub(crate) fn raw_sum_index_is_selected( + sector_id: Option, + selected_color_sector_ids: Option<&BTreeSet>, +) -> bool { + let Some(selected) = selected_color_sector_ids else { + return true; + }; + sector_id + .map(|value| selected.contains(&value)) + .unwrap_or(false) +} + +pub(crate) fn build_color_contraction_runtime( + manifest: Option<&GenericColorContractionManifest>, + groups: &[RawSumGroup], +) -> RusticolResult> { + let Some(manifest) = manifest else { + return Ok(None); + }; + if !manifest.supported { + return Err(RusticolError::invalid_argument(format!( + "generic colour contraction is unsupported: {}", + manifest + .reason + .as_deref() + .unwrap_or("no diagnostic was provided") + ))); + } + let group_index_by_id = groups + .iter() + .enumerate() + .map(|(index, group)| (group.id, index)) + .collect::>(); + if group_index_by_id.len() != manifest.group_count { + return Err(RusticolError::invalid_argument(format!( + "colour contraction declares {} groups but runtime has {} coherent groups", + manifest.group_count, + group_index_by_id.len() + ))); + } + if let Some(repeated) = manifest.repeated_block.as_ref() { + if !manifest.entries.is_empty() { + return Err(RusticolError::invalid_argument( + "colour contraction cannot mix expanded entries with a repeated block", + )); + } + if repeated.component_count < 2 { + return Err(RusticolError::invalid_argument( + "repeated colour contraction must contain at least two components", + )); + } + if repeated.component_group_ids.len() != manifest.group_count + || repeated.component_group_ids.len() % repeated.component_count != 0 + { + return Err(RusticolError::invalid_argument( + "repeated colour contraction group mapping has an inconsistent shape", + )); + } + let groups_per_component = repeated.component_group_ids.len() / repeated.component_count; + if groups_per_component == 0 { + return Err(RusticolError::invalid_argument( + "repeated colour contraction components cannot be empty", + )); + } + let mut mapped_group_ids = BTreeSet::new(); + let mut component_group_indices = Vec::with_capacity(manifest.group_count); + for group_id in &repeated.component_group_ids { + if !mapped_group_ids.insert(*group_id) { + return Err(RusticolError::invalid_argument( + "repeated colour contraction maps a coherent group more than once", + )); + } + component_group_indices.push(*group_index_by_id.get(group_id).ok_or_else(|| { + RusticolError::invalid_argument(format!( + "repeated colour contraction references unknown group {group_id}" + )) + })?); + } + let mut entries = Vec::with_capacity(repeated.entries.len()); + for entry in &repeated.entries { + if entry.weight.len() != 2 { + return Err(RusticolError::invalid_argument( + "repeated colour contraction weight must have two components", + )); + } + if entry.left_group_index >= groups_per_component + || entry.right_group_index >= groups_per_component + { + return Err(RusticolError::invalid_argument( + "repeated colour contraction entry index is out of bounds", + )); + } + entries.push(ColorContractionEntry { + left_group_index: entry.left_group_index, + right_group_index: entry.right_group_index, + weight_re: entry.weight.first().copied().unwrap_or(0.0), + weight_im: entry.weight.get(1).copied().unwrap_or(0.0), + symmetry_factor: entry.symmetry_factor, + }); + } + let (walsh_block, c2k_walsh_block) = build_walsh_color_contraction_blocks( + repeated.factorized_block.as_ref(), + groups_per_component, + &entries, + )?; + return Ok(Some(ColorContractionRuntime::from_repeated_block( + groups, + repeated.component_count, + component_group_indices, + entries, + walsh_block, + c2k_walsh_block, + ))); + } + let mut entries = Vec::with_capacity(manifest.entries.len()); + for entry in &manifest.entries { + let left_group_index = *group_index_by_id.get(&entry.left_group_id).ok_or_else(|| { + RusticolError::invalid_argument(format!( + "colour contraction references unknown left group {}", + entry.left_group_id + )) + })?; + let right_group_index = *group_index_by_id + .get(&entry.right_group_id) + .ok_or_else(|| { + RusticolError::invalid_argument(format!( + "colour contraction references unknown right group {}", + entry.right_group_id + )) + })?; + let weight_re = entry.weight.first().copied().unwrap_or(0.0); + let weight_im = entry.weight.get(1).copied().unwrap_or(0.0); + entries.push(ColorContractionEntry { + left_group_index, + right_group_index, + weight_re, + weight_im, + symmetry_factor: entry.symmetry_factor, + }); + } + Ok(Some(ColorContractionRuntime::new(groups, entries))) +} + +pub(crate) fn build_compiled_symmetric_group_color_contraction_runtime( + manifest: Option<&GenericColorContractionManifest>, + groups: &[RawSumGroup], + plan: crate::recurrence::RecurrenceColorContraction, +) -> RusticolResult { + let manifest = manifest.ok_or_else(|| { + RusticolError::integrity("compiled symmetric-group FFT payload has no contraction metadata") + })?; + if !manifest.supported + || manifest.group_count != groups.len() + || !manifest.entries.is_empty() + || manifest.repeated_block.is_some() + { + return Err(RusticolError::integrity( + "compiled symmetric-group FFT metadata disagrees with coherent groups", + )); + } + if plan.accuracy() != crate::recurrence::RecurrenceColorAccuracy::Full + || plan.storage() != crate::recurrence::RecurrenceColorStorage::ConvolutionKernels + || plan.component_count() != 1 + || plan.group_count() as usize != groups.len() + || plan.local_group_count() as usize != groups.len() + || plan.destination_count() as usize != groups.len() + || plan.active_sector_count() != groups.len() + || !matches!( + plan.runtime_reducer(), + Some(crate::recurrence::RuntimeColorContractionReducer::SymmetricGroupFourier(_)) + ) + { + return Err(RusticolError::compatibility( + "compiled symmetric-group FFT diagnostic payload is not a one-component FullColour active-sector convolution", + )); + } + let group_index_by_id = groups + .iter() + .enumerate() + .map(|(index, group)| (group.id, index)) + .collect::>(); + if group_index_by_id.len() != groups.len() { + return Err(RusticolError::integrity( + "compiled symmetric-group FFT coherent group IDs are not unique", + )); + } + let mut ordered_group_indices = Vec::with_capacity(groups.len()); + let mut covered = BTreeSet::new(); + for local_group in 0..groups.len() { + let destination_id = plan.ordered_destination_id(local_group, 0).ok_or_else(|| { + RusticolError::integrity( + "compiled symmetric-group FFT local group has no coherent destination", + ) + })?; + let group_index = *group_index_by_id + .get(&i64::from(destination_id)) + .ok_or_else(|| { + RusticolError::integrity(format!( + "compiled symmetric-group FFT references unknown coherent group {destination_id}" + )) + })?; + if !covered.insert(group_index) { + return Err(RusticolError::integrity( + "compiled symmetric-group FFT maps a coherent group more than once", + )); + } + let ordered_group_id = *plan.ordered_group_ids().get(local_group).ok_or_else(|| { + RusticolError::integrity("compiled symmetric-group FFT ordered group map is incomplete") + })? as usize; + let sector_id = *plan + .sector_by_group() + .get(ordered_group_id) + .ok_or_else(|| { + RusticolError::integrity( + "compiled symmetric-group FFT ordered sector map is incomplete", + ) + })?; + if groups[group_index].sector_ids.as_slice() != [i64::from(sector_id)] { + return Err(RusticolError::integrity( + "compiled symmetric-group FFT coherent group sector identity disagrees with its payload", + )); + } + ordered_group_indices.push(group_index); + } + if covered.len() != groups.len() { + return Err(RusticolError::integrity( + "compiled symmetric-group FFT does not exhaust coherent groups", + )); + } + Ok(ColorContractionRuntime::from_symmetric_group( + groups, + plan, + ordered_group_indices, + )) +} + +fn build_walsh_color_contraction_blocks( + manifest: Option<&GenericFactorizedColorContractionBlockManifest>, + local_group_count: usize, + entries: &[ColorContractionEntry], +) -> RusticolResult<( + Option, + Option, +)> { + let Some(manifest) = manifest else { + return Ok((None, None)); + }; + match manifest { + GenericFactorizedColorContractionBlockManifest::KleinFourWalsh { cosets } => Ok(( + Some(build_klein_four_walsh_color_contraction_block( + cosets, + local_group_count, + entries, + )?), + None, + )), + GenericFactorizedColorContractionBlockManifest::ElementaryAbelianWalsh { rank, cosets } => { + Ok(( + None, + Some(build_c2k_walsh_color_contraction_block( + *rank, + cosets, + local_group_count, + entries, + )?), + )) + } + } +} + +fn build_klein_four_walsh_color_contraction_block( + cosets: &[[usize; 4]], + local_group_count: usize, + entries: &[ColorContractionEntry], +) -> RusticolResult { + let mapped_group_count = cosets.len().checked_mul(4).ok_or_else(|| { + RusticolError::invalid_argument("factorized color contraction coset count overflows") + })?; + if cosets.is_empty() || mapped_group_count != local_group_count { + return Err(RusticolError::invalid_argument( + "factorized color contraction cosets do not match local groups", + )); + } + let mut mapped_groups = vec![false; local_group_count]; + for coset in cosets { + for local_group_index in coset { + let Some(mapped) = mapped_groups.get_mut(*local_group_index) else { + return Err(RusticolError::invalid_argument( + "factorized color contraction coset index is out of bounds", + )); + }; + if *mapped { + return Err(RusticolError::invalid_argument( + "factorized color contraction cosets contain a duplicate index", + )); + } + *mapped = true; + } + } + + let matrix_size = local_group_count + .checked_mul(local_group_count) + .ok_or_else(|| { + RusticolError::invalid_argument("factorized color contraction matrix size overflows") + })?; + let mut matrix = vec![0.0; matrix_size]; + let mut seen_pairs = BTreeSet::new(); + for entry in entries { + if entry.weight_im != 0.0 { + return Err(RusticolError::invalid_argument( + "factorized color contraction requires real weights", + )); + } + if !entry.weight_re.is_finite() || !entry.symmetry_factor.is_finite() { + return Err(RusticolError::invalid_argument( + "factorized color contraction weights must be finite", + )); + } + let (left, right) = if entry.left_group_index <= entry.right_group_index { + (entry.left_group_index, entry.right_group_index) + } else { + (entry.right_group_index, entry.left_group_index) + }; + if !seen_pairs.insert((left, right)) { + return Err(RusticolError::invalid_argument( + "factorized color contraction has a duplicate matrix entry", + )); + } + let weight = if left == right { + entry.symmetry_factor * entry.weight_re + } else { + 0.5 * entry.symmetry_factor * entry.weight_re + }; + if !weight.is_finite() { + return Err(RusticolError::invalid_argument( + "factorized color contraction matrix weight is not finite", + )); + } + matrix[left * local_group_count + right] = weight; + matrix[right * local_group_count + left] = weight; + } + + for left_coset in cosets { + for right_coset in cosets { + for left_subgroup_index in 0..4 { + for right_subgroup_index in 0..4 { + let actual = matrix[left_coset[left_subgroup_index] * local_group_count + + right_coset[right_subgroup_index]]; + let expected = matrix[left_coset[0] * local_group_count + + right_coset[left_subgroup_index ^ right_subgroup_index]]; + if actual != expected { + return Err(RusticolError::invalid_argument( + "factorized color contraction matrix is not invariant under its Klein-four action", + )); + } + } + } + } + } + + let coset_count = cosets.len(); + let mut transformed_entries = Vec::new(); + let characters = [ + [1.0, 1.0, 1.0, 1.0], + [1.0, -1.0, 1.0, -1.0], + [1.0, 1.0, -1.0, -1.0], + [1.0, -1.0, -1.0, 1.0], + ]; + for (character_index, character) in characters.iter().enumerate() { + for left_coset_index in 0..coset_count { + for right_coset_index in left_coset_index..coset_count { + let left_coset = cosets[left_coset_index]; + let right_coset = cosets[right_coset_index]; + let values = [ + matrix[left_coset[0] * local_group_count + right_coset[0]], + matrix[left_coset[0] * local_group_count + right_coset[1]], + matrix[left_coset[0] * local_group_count + right_coset[2]], + matrix[left_coset[0] * local_group_count + right_coset[3]], + ]; + let mut weight = 0.0; + for (sign, value) in character.iter().zip(values) { + weight += sign * value; + } + if !weight.is_finite() { + return Err(RusticolError::invalid_argument( + "factorized color contraction Walsh weight is not finite", + )); + } + if weight == 0.0 { + continue; + } + transformed_entries.push(ColorContractionEntry { + left_group_index: character_index * coset_count + left_coset_index, + right_group_index: character_index * coset_count + right_coset_index, + weight_re: weight, + weight_im: 0.0, + symmetry_factor: if left_coset_index == right_coset_index { + 1.0 + } else { + 2.0 + }, + }); + } + } + } + Ok(WalshColorContractionBlock { + cosets: cosets.to_vec(), + entries: transformed_entries, + }) +} + +fn build_c2k_walsh_color_contraction_block( + rank: usize, + cosets: &[Vec], + local_group_count: usize, + entries: &[ColorContractionEntry], +) -> RusticolResult { + if rank < 3 { + return Err(RusticolError::invalid_argument( + "elementary-Abelian Walsh color contraction rank must be at least three", + )); + } + let shift = u32::try_from(rank).map_err(|_| { + RusticolError::invalid_argument( + "elementary-Abelian Walsh color contraction rank is too large", + ) + })?; + let subgroup_order = 1usize.checked_shl(shift).ok_or_else(|| { + RusticolError::invalid_argument( + "elementary-Abelian Walsh color contraction subgroup order overflows", + ) + })?; + let mapped_group_count = cosets.len().checked_mul(subgroup_order).ok_or_else(|| { + RusticolError::invalid_argument("factorized color contraction coset count overflows") + })?; + if cosets.is_empty() + || cosets.iter().any(|coset| coset.len() != subgroup_order) + || mapped_group_count != local_group_count + { + return Err(RusticolError::invalid_argument( + "elementary-Abelian Walsh color contraction cosets do not match rank or local groups", + )); + } + + let mut mapped_groups = vec![false; local_group_count]; + for coset in cosets { + for local_group_index in coset { + let Some(mapped) = mapped_groups.get_mut(*local_group_index) else { + return Err(RusticolError::invalid_argument( + "factorized color contraction coset index is out of bounds", + )); + }; + if *mapped { + return Err(RusticolError::invalid_argument( + "factorized color contraction cosets contain a duplicate index", + )); + } + *mapped = true; + } + } + if mapped_groups.iter().any(|mapped| !mapped) { + return Err(RusticolError::invalid_argument( + "factorized color contraction cosets do not partition local groups", + )); + } + + let matrix_size = local_group_count + .checked_mul(local_group_count) + .ok_or_else(|| { + RusticolError::invalid_argument("factorized color contraction matrix size overflows") + })?; + let mut matrix = vec![0.0; matrix_size]; + let mut seen_pairs = BTreeSet::new(); + for entry in entries { + if entry.weight_im != 0.0 { + return Err(RusticolError::invalid_argument( + "factorized color contraction requires real weights", + )); + } + if !entry.weight_re.is_finite() || !entry.symmetry_factor.is_finite() { + return Err(RusticolError::invalid_argument( + "factorized color contraction weights must be finite", + )); + } + let (left, right) = if entry.left_group_index <= entry.right_group_index { + (entry.left_group_index, entry.right_group_index) + } else { + (entry.right_group_index, entry.left_group_index) + }; + if !seen_pairs.insert((left, right)) { + return Err(RusticolError::invalid_argument( + "factorized color contraction has a duplicate matrix entry", + )); + } + let weight = if left == right { + entry.symmetry_factor * entry.weight_re + } else { + 0.5 * entry.symmetry_factor * entry.weight_re + }; + if !weight.is_finite() { + return Err(RusticolError::invalid_argument( + "factorized color contraction matrix weight is not finite", + )); + } + matrix[left * local_group_count + right] = weight; + matrix[right * local_group_count + left] = weight; + } + + for left_coset in cosets { + for right_coset in cosets { + for left_subgroup_index in 0..subgroup_order { + for right_subgroup_index in 0..subgroup_order { + let actual = matrix[left_coset[left_subgroup_index] * local_group_count + + right_coset[right_subgroup_index]]; + let expected = matrix[left_coset[0] * local_group_count + + right_coset[left_subgroup_index ^ right_subgroup_index]]; + if actual != expected { + return Err(RusticolError::invalid_argument( + "factorized color contraction matrix is not invariant under its elementary-Abelian XOR action", + )); + } + } + } + } + } + + let coset_count = cosets.len(); + let mut transformed_entries = Vec::new(); + for left_coset_index in 0..coset_count { + for right_coset_index in left_coset_index..coset_count { + let left_coset = &cosets[left_coset_index]; + let right_coset = &cosets[right_coset_index]; + let mut weights = (0..subgroup_order) + .map(|subgroup_index| { + matrix[left_coset[0] * local_group_count + right_coset[subgroup_index]] + }) + .collect::>(); + walsh_butterfly_f64(&mut weights); + for (character_index, weight) in weights.into_iter().enumerate() { + if !weight.is_finite() { + return Err(RusticolError::invalid_argument( + "factorized color contraction Walsh weight is not finite", + )); + } + if weight == 0.0 { + continue; + } + transformed_entries.push(ColorContractionEntry { + left_group_index: character_index * coset_count + left_coset_index, + right_group_index: character_index * coset_count + right_coset_index, + // The C2^k runtime deliberately applies the unnormalized + // Walsh transform. H H^T = subgroup_order * I, so carry + // its inverse once in the transformed matrix instead of + // scaling every complex amplitude at every point. + weight_re: weight / subgroup_order as f64, + weight_im: 0.0, + symmetry_factor: if left_coset_index == right_coset_index { + 1.0 + } else { + 2.0 + }, + }); + } + } + } + + Ok(C2kWalshColorContractionBlock { + subgroup_order, + cosets: cosets.to_vec(), + entries: transformed_entries, + }) +} + +fn walsh_butterfly_f64(values: &mut [f64]) { + debug_assert!(values.len().is_power_of_two()); + let mut stride = 1; + while stride < values.len() { + for start in (0..values.len()).step_by(stride * 2) { + for offset in 0..stride { + let left = values[start + offset]; + let right = values[start + stride + offset]; + values[start + offset] = left + right; + values[start + stride + offset] = left - right; + } + } + stride *= 2; + } +} + +pub(crate) fn generic_root_group_id( + root: &GenericAmplitudeRootManifest, +) -> RusticolResult> { + let Some(value) = root.coherent_group_id.as_ref() else { + return Ok(None); + }; + if value.is_null() { + return Ok(None); + } + if let Some(group_id) = value.as_i64() { + return Ok(Some(group_id)); + } + if let Some(group_id) = value.as_u64() { + return i64::try_from(group_id) + .map(Some) + .map_err(|_| RusticolError::invalid_argument("generic coherent group id exceeds i64")); + } + if let Some(text) = value.as_str() { + return text.parse::().map(Some).map_err(|err| { + RusticolError::invalid_argument(format!( + "could not parse generic coherent group id {text:?}: {err}" + )) + }); + } + Err(RusticolError::invalid_argument(format!( + "generic coherent group id for root {} must be an integer or string", + root.root_id + ))) +} + +#[cfg(test)] +mod selected_axis_position_tests { + use super::*; + + #[test] + fn sparse_and_complete_axis_positions_match_without_resizing_sparse_scratch() { + let mut dense = vec![Some(usize::MAX); 17]; + for selected in [&[8][..], &[][..]] { + let positions = SelectedAxisPositions::new(selected, 10, &mut dense); + assert!(matches!(positions, SelectedAxisPositions::Sparse(_))); + for index in 0..10 { + assert_eq!( + positions.position(index), + selected.iter().position(|&i| i == index) + ); + } + assert_eq!(dense, vec![Some(usize::MAX); 17]); + } + for selected in [vec![1, 4, 8], (1..10).collect(), (0..10).collect()] { + let positions = SelectedAxisPositions::new(&selected, 10, &mut dense); + assert!(matches!(positions, SelectedAxisPositions::Dense(_))); + for index in 0..10 { + assert_eq!( + positions.position(index), + selected.iter().position(|&i| i == index) + ); + } + assert_eq!(dense.len(), 10); + } + } +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/evaluator/backend.rs b/pyamplicol/rust/crates/rusticol-core/src/evaluator/backend.rs new file mode 100644 index 0000000000..d94078962c --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/evaluator/backend.rs @@ -0,0 +1,2803 @@ +// SPDX-License-Identifier: 0BSD + +use super::super::*; +use super::*; + +impl EvaluatorGroup { + pub(crate) fn load(manifest: &EvaluatorManifest, root: &Path) -> RusticolResult { + let payloads = EvaluatorPayloadStore::directory(root); + Self::load_from_store(manifest, &payloads) + } + + pub(crate) fn load_from_store( + manifest: &EvaluatorManifest, + payloads: &EvaluatorPayloadStore, + ) -> RusticolResult { + ensure_evaluator_capabilities_supported(manifest)?; + let (input_len, _) = manifest.io_len()?; + let leaf_layout = manifest.leaf_layout()?; + let mut evaluators = Vec::new(); + let mut input_mappings = Vec::new(); + for leaf in &leaf_layout { + let before = evaluators.len(); + flatten_evaluators_from_store(leaf.evaluator, payloads, &mut evaluators)?; + if evaluators.len() != before + 1 { + return Err(RusticolError::integrity( + "canonical evaluator leaf layout did not load exactly one evaluator", + )); + } + let mapping = if leaf.input_indices.len() == input_len + && leaf + .input_indices + .iter() + .enumerate() + .all(|(expected, index)| expected == *index) + { + None + } else { + Some(leaf.input_indices.clone()) + }; + input_mappings.push(mapping); + } + let input_mapping_spans = input_mappings + .iter() + .map(|mapping| { + mapping + .as_deref() + .map(contiguous_input_spans) + .unwrap_or_default() + }) + .collect(); + let output_len = leaf_layout.last().map_or(0, |leaf| leaf.output_range.end); + Ok(Self { + evaluators, + input_len, + input_mappings, + input_mapping_spans, + output_len, + chunk_parameter_scratch_f64: Vec::new(), + chunk_scratch_f64: Vec::new(), + chunk_parameter_scratch_aosoa_f64: Vec::new(), + chunk_scratch_aosoa_f64: Vec::new(), + chunk_input_mapping_scratch: Vec::new(), + }) + } + + #[cfg(feature = "symbolica-runtime")] + pub(crate) fn load_exact_from_plane( + stage: &GenericSerializedStageEvaluatorManifest, + payloads: &EvaluatorPayloadStore, + ) -> RusticolResult { + let manifest = &stage.evaluator; + let direct = stage.compiled_plane_arena.as_ref().ok_or_else(|| { + RusticolError::integrity( + "compiled plane-arena exact evaluator has no serialized bindings", + ) + })?; + let (input_len, _) = manifest.io_len()?; + let leaf_layout = manifest.leaf_layout()?; + if leaf_layout.len() != direct.leaves.len() { + return Err(RusticolError::integrity( + "compiled plane-arena exact leaf bindings are incomplete", + )); + } + let mut evaluators = Vec::new(); + let mut input_mappings = Vec::new(); + for (leaf, binding) in leaf_layout.iter().zip(&direct.leaves) { + flatten_exact_evaluators_from_store(leaf.evaluator, payloads, &mut evaluators)?; + let mapping = if binding.input_indices.len() == input_len + && binding + .input_indices + .iter() + .enumerate() + .all(|(expected, index)| expected == *index) + { + None + } else { + Some(binding.input_indices.clone()) + }; + input_mappings.push(mapping); + } + let input_mapping_spans = input_mappings + .iter() + .map(|mapping| { + mapping + .as_deref() + .map(contiguous_input_spans) + .unwrap_or_default() + }) + .collect(); + let output_len = direct.leaves.last().map_or(0, |leaf| leaf.output_stop); + Ok(Self { + evaluators, + input_len, + input_mappings, + input_mapping_spans, + output_len, + chunk_parameter_scratch_f64: Vec::new(), + chunk_scratch_f64: Vec::new(), + chunk_parameter_scratch_aosoa_f64: Vec::new(), + chunk_scratch_aosoa_f64: Vec::new(), + chunk_input_mapping_scratch: Vec::new(), + }) + } + + pub(crate) fn uses_simd_jit(&self) -> bool { + self.evaluators + .iter() + .any(|evaluator| match &evaluator.eval { + #[cfg(feature = "symbolica-runtime")] + F64Evaluator::ExactOnly => false, + #[cfg(feature = "f64-symjit")] + F64Evaluator::SymjitApplication(_) => true, + #[cfg(feature = "symbolica-runtime")] + F64Evaluator::Jit(_) => true, + #[cfg(feature = "f64-compiled")] + F64Evaluator::Compiled(_) => false, + }) + } + + pub(crate) fn evaluate_batch( + &mut self, + batch_size: usize, + params: &[Complex], + ) -> RusticolResult>> { + let mut out = Vec::new(); + self.evaluate_batch_into(batch_size, params, &mut out)?; + Ok(out) + } + + pub(crate) fn evaluate_single_row( + &mut self, + params: &[Complex], + ) -> RusticolResult>> { + self.evaluate_batch(1, params) + } + + pub(crate) fn output_chunk_lengths(&self) -> Vec { + self.evaluators + .iter() + .map(|evaluator| evaluator.output_len) + .collect() + } + + pub(crate) fn is_chunked(&self) -> bool { + self.evaluators.len() > 1 + } + + pub(crate) fn evaluate_chunks_f64_into_state( + &mut self, + batch_size: usize, + params: &[Complex], + state_parameter_count: usize, + state: &mut [Complex], + chunk_outputs: &[Vec<(usize, usize)>], + chunk_output_spans: &[Vec<(usize, usize, usize)>], + ) -> RusticolResult<()> { + if chunk_outputs.len() != self.evaluators.len() + || chunk_output_spans.len() != self.evaluators.len() + { + return Err(RusticolError::invalid_argument( + "stage chunk-output layout does not match evaluator chunks", + )); + } + let expected_state_len = batch_size + .checked_mul(state_parameter_count) + .ok_or_else(|| RusticolError::invalid_argument("stage state length overflows usize"))?; + if state.len() != expected_state_len { + return Err(RusticolError::invalid_argument(format!( + "stage state has length {}, expected {expected_state_len}", + state.len() + ))); + } + if params.len() != batch_size * self.input_len { + return Err(RusticolError::invalid_argument(format!( + "parameter buffer has length {}, expected {}", + params.len(), + batch_size * self.input_len + ))); + } + for ((((evaluator, input_mapping), input_spans), outputs), spans) in self + .evaluators + .iter_mut() + .zip(&self.input_mappings) + .zip(&self.input_mapping_spans) + .zip(chunk_outputs) + .zip(chunk_output_spans) + { + if let Some(lane_width) = evaluator + .simd_lane_width() + .filter(|lane_width| *lane_width > 1 && batch_size >= *lane_width) + { + let mapping = input_mapping.as_deref(); + let block_count = pack_f64_parameters_aosoa( + params, + batch_size, + self.input_len, + evaluator.input_len, + lane_width, + mapping, + &mut self.chunk_parameter_scratch_aosoa_f64, + )?; + self.chunk_scratch_aosoa_f64.resize( + aosoa_scalar_len(block_count, evaluator.output_len, lane_width, "output")?, + 0.0, + ); + if evaluator.evaluate_f64_aosoa_blocks( + block_count, + &self.chunk_parameter_scratch_aosoa_f64, + &mut self.chunk_scratch_aosoa_f64, + )? { + scatter_f64_aosoa_outputs_to_state( + &self.chunk_scratch_aosoa_f64, + batch_size, + evaluator.output_len, + lane_width, + state_parameter_count, + state, + outputs, + spans, + )?; + continue; + } + } + let evaluator_params = mapped_f64_parameters( + params, + batch_size, + self.input_len, + input_mapping.as_deref(), + input_spans, + &mut self.chunk_parameter_scratch_f64, + ); + validate_leaf_parameter_length(evaluator, batch_size, evaluator_params)?; + self.chunk_scratch_f64 + .resize(batch_size * evaluator.output_len, c64(0.0, 0.0)); + evaluator.evaluate_f64_batch_unpadded( + batch_size, + evaluator_params, + &mut self.chunk_scratch_f64, + )?; + for row in 0..batch_size { + let row_state = row * state_parameter_count; + let row_eval = row * evaluator.output_len; + if spans.is_empty() { + for (column, state_offset) in outputs { + state[row_state + *state_offset] = + self.chunk_scratch_f64[row_eval + *column]; + } + } else { + for (column_start, state_offset_start, len) in spans { + let source_start = row_eval + *column_start; + let target_start = row_state + *state_offset_start; + state[target_start..target_start + *len].copy_from_slice( + &self.chunk_scratch_f64[source_start..source_start + *len], + ); + } + } + } + } + Ok(()) + } + + pub(crate) fn evaluate_chunks_f64_into_state_profile( + &mut self, + batch_size: usize, + params: &[Complex], + state_parameter_count: usize, + state: &mut [Complex], + chunk_outputs: &[Vec<(usize, usize)>], + chunk_output_spans: &[Vec<(usize, usize, usize)>], + ) -> RusticolResult<(EvaluatorBatchProfile, f64)> { + if chunk_outputs.len() != self.evaluators.len() + || chunk_output_spans.len() != self.evaluators.len() + { + return Err(RusticolError::invalid_argument( + "stage chunk-output layout does not match evaluator chunks", + )); + } + let expected_state_len = batch_size + .checked_mul(state_parameter_count) + .ok_or_else(|| RusticolError::invalid_argument("stage state length overflows usize"))?; + if state.len() != expected_state_len { + return Err(RusticolError::invalid_argument(format!( + "stage state has length {}, expected {expected_state_len}", + state.len() + ))); + } + + // Keep timing state out of floating-point registers while generated + // evaluators execute. Some native evaluator ABIs use the full SIMD + // register file, whereas Duration remains integer-backed. + let mut leaf_input_pack_elapsed = Duration::ZERO; + let mut evaluator_elapsed = Duration::ZERO; + let mut leaf_input_copy_component_count = 0u64; + let mut backend_call_count = 0u64; + let mut scratch_reallocation_count = 0u64; + let mut assign_elapsed = Duration::ZERO; + if params.len() != batch_size * self.input_len { + return Err(RusticolError::invalid_argument(format!( + "parameter buffer has length {}, expected {}", + params.len(), + batch_size * self.input_len + ))); + } + for ((((evaluator, input_mapping), input_spans), outputs), spans) in self + .evaluators + .iter_mut() + .zip(&self.input_mappings) + .zip(&self.input_mapping_spans) + .zip(chunk_outputs) + .zip(chunk_output_spans) + { + if let Some(lane_width) = evaluator + .simd_lane_width() + .filter(|lane_width| *lane_width > 1 && batch_size >= *lane_width) + { + let parameter_capacity = self.chunk_parameter_scratch_aosoa_f64.capacity(); + let leaf_start = Instant::now(); + let mapping = input_mapping.as_deref(); + let block_count = pack_f64_parameters_aosoa( + params, + batch_size, + self.input_len, + evaluator.input_len, + lane_width, + mapping, + &mut self.chunk_parameter_scratch_aosoa_f64, + )?; + leaf_input_pack_elapsed += leaf_start.elapsed(); + leaf_input_copy_component_count += + (self.chunk_parameter_scratch_aosoa_f64.len() / 2) as u64; + scratch_reallocation_count += u64::from( + self.chunk_parameter_scratch_aosoa_f64.capacity() != parameter_capacity, + ); + + let output_capacity = self.chunk_scratch_aosoa_f64.capacity(); + self.chunk_scratch_aosoa_f64.resize( + aosoa_scalar_len(block_count, evaluator.output_len, lane_width, "output")?, + 0.0, + ); + scratch_reallocation_count += + u64::from(self.chunk_scratch_aosoa_f64.capacity() != output_capacity); + let eval_start = Instant::now(); + let evaluated = evaluator.evaluate_f64_aosoa_blocks( + block_count, + &self.chunk_parameter_scratch_aosoa_f64, + &mut self.chunk_scratch_aosoa_f64, + )?; + evaluator_elapsed += eval_start.elapsed(); + backend_call_count += 1; + if evaluated { + let assign_start = Instant::now(); + scatter_f64_aosoa_outputs_to_state( + &self.chunk_scratch_aosoa_f64, + batch_size, + evaluator.output_len, + lane_width, + state_parameter_count, + state, + outputs, + spans, + )?; + assign_elapsed += assign_start.elapsed(); + continue; + } + } + let leaf_capacity = self.chunk_parameter_scratch_f64.capacity(); + let leaf_start = Instant::now(); + let evaluator_params = mapped_f64_parameters( + params, + batch_size, + self.input_len, + input_mapping.as_deref(), + input_spans, + &mut self.chunk_parameter_scratch_f64, + ); + leaf_input_pack_elapsed += leaf_start.elapsed(); + if input_mapping.is_some() { + leaf_input_copy_component_count += evaluator_params.len() as u64; + } + validate_leaf_parameter_length(evaluator, batch_size, evaluator_params)?; + let chunk_capacity = self.chunk_scratch_f64.capacity(); + self.chunk_scratch_f64 + .resize(batch_size * evaluator.output_len, c64(0.0, 0.0)); + let eval_start = Instant::now(); + evaluator.evaluate_f64_batch_unpadded( + batch_size, + evaluator_params, + &mut self.chunk_scratch_f64, + )?; + evaluator_elapsed += eval_start.elapsed(); + backend_call_count += 1; + scratch_reallocation_count += + u64::from(self.chunk_scratch_f64.capacity() != chunk_capacity); + // The mapped slice is no longer live after the backend call. + scratch_reallocation_count += + u64::from(self.chunk_parameter_scratch_f64.capacity() != leaf_capacity); + + let assign_start = Instant::now(); + for row in 0..batch_size { + let row_state = row * state_parameter_count; + let row_eval = row * evaluator.output_len; + if spans.is_empty() { + for (column, state_offset) in outputs { + state[row_state + *state_offset] = + self.chunk_scratch_f64[row_eval + *column]; + } + } else { + for (column_start, state_offset_start, len) in spans { + let source_start = row_eval + *column_start; + let target_start = row_state + *state_offset_start; + state[target_start..target_start + *len].copy_from_slice( + &self.chunk_scratch_f64[source_start..source_start + *len], + ); + } + } + } + assign_elapsed += assign_start.elapsed(); + } + let leaf_input_pack_s = profile_duration_seconds(leaf_input_pack_elapsed); + let evaluator_call_s = profile_duration_seconds(evaluator_elapsed); + let profile = EvaluatorBatchProfile { + leaf_input_pack_s, + legacy_evaluator_call_s: leaf_input_pack_s + evaluator_call_s, + evaluator_call_s, + leaf_input_copy_component_count, + backend_call_count, + scratch_reallocation_count, + ..EvaluatorBatchProfile::default() + }; + Ok((profile, assign_elapsed.as_secs_f64())) + } + + // The explicit slices avoid constructing a descriptor in this selector hot path. + #[allow(clippy::too_many_arguments)] + pub(crate) fn evaluate_selected_chunks_f64_into_state( + &mut self, + batch_size: usize, + state_parameter_count: usize, + state: &mut [Complex], + parent_input_components: Option<&[usize]>, + parent_input_spans: &[(usize, usize, usize)], + chunk_outputs: &[Vec<(usize, usize)>], + chunk_output_spans: &[Vec<(usize, usize, usize)>], + active_chunk_indices: &[usize], + ) -> RusticolResult<()> { + if chunk_outputs.len() != self.evaluators.len() + || chunk_output_spans.len() != self.evaluators.len() + { + return Err(RusticolError::invalid_argument( + "stage chunk-output layout does not match evaluator chunks", + )); + } + validate_active_chunk_indices(active_chunk_indices, self.evaluators.len())?; + let expected_state_len = batch_size + .checked_mul(state_parameter_count) + .ok_or_else(|| RusticolError::invalid_argument("stage state length overflows usize"))?; + if state.len() != expected_state_len { + return Err(RusticolError::invalid_argument(format!( + "stage state has length {}, expected {expected_state_len}", + state.len() + ))); + } + match parent_input_components { + Some(components) => { + if components.len() != self.input_len { + return Err(RusticolError::invalid_argument(format!( + "stage input-component map has length {}, expected {}", + components.len(), + self.input_len + ))); + } + if components + .iter() + .any(|component| *component >= state_parameter_count) + { + return Err(RusticolError::invalid_argument( + "stage input-component map references state outside the parameter layout", + )); + } + } + None if self.input_len != state_parameter_count => { + return Err(RusticolError::invalid_argument(format!( + "evaluator input length {} does not match stage state parameter count {state_parameter_count}", + self.input_len + ))); + } + None => {} + } + for &chunk_index in active_chunk_indices { + let evaluator = &mut self.evaluators[chunk_index]; + if let Some(lane_width) = evaluator + .simd_lane_width() + .filter(|lane_width| *lane_width > 1 && batch_size >= *lane_width) + { + let input_mapping = self.input_mappings[chunk_index].as_deref(); + self.chunk_input_mapping_scratch.clear(); + self.chunk_input_mapping_scratch + .reserve(evaluator.input_len); + for local_component in 0..evaluator.input_len { + let parent_component = + input_mapping.map_or(local_component, |indices| indices[local_component]); + self.chunk_input_mapping_scratch.push( + parent_input_components + .map_or(parent_component, |components| components[parent_component]), + ); + } + let block_count = pack_f64_parameters_aosoa( + state, + batch_size, + state_parameter_count, + evaluator.input_len, + lane_width, + Some(&self.chunk_input_mapping_scratch), + &mut self.chunk_parameter_scratch_aosoa_f64, + )?; + self.chunk_scratch_aosoa_f64.resize( + aosoa_scalar_len(block_count, evaluator.output_len, lane_width, "output")?, + 0.0, + ); + if evaluator.evaluate_f64_aosoa_blocks( + block_count, + &self.chunk_parameter_scratch_aosoa_f64, + &mut self.chunk_scratch_aosoa_f64, + )? { + scatter_f64_aosoa_outputs_to_state( + &self.chunk_scratch_aosoa_f64, + batch_size, + evaluator.output_len, + lane_width, + state_parameter_count, + state, + &chunk_outputs[chunk_index], + &chunk_output_spans[chunk_index], + )?; + continue; + } + } + let evaluator_params = selected_chunk_f64_parameters( + state, + batch_size, + state_parameter_count, + self.input_len, + parent_input_components, + parent_input_spans, + self.input_mappings[chunk_index].as_deref(), + &self.input_mapping_spans[chunk_index], + &mut self.chunk_parameter_scratch_f64, + )?; + validate_leaf_parameter_length(evaluator, batch_size, evaluator_params)?; + let scratch_len = batch_size + .checked_mul(evaluator.output_len) + .ok_or_else(|| { + RusticolError::invalid_argument("stage chunk output length overflows usize") + })?; + self.chunk_scratch_f64.resize(scratch_len, c64(0.0, 0.0)); + evaluator.evaluate_f64_batch_unpadded( + batch_size, + evaluator_params, + &mut self.chunk_scratch_f64, + )?; + let outputs = &chunk_outputs[chunk_index]; + let spans = &chunk_output_spans[chunk_index]; + for row in 0..batch_size { + let row_state = row * state_parameter_count; + let row_eval = row * evaluator.output_len; + if spans.is_empty() { + for (column, state_offset) in outputs { + state[row_state + *state_offset] = + self.chunk_scratch_f64[row_eval + *column]; + } + } else { + for (column_start, state_offset_start, len) in spans { + let source_start = row_eval + *column_start; + let target_start = row_state + *state_offset_start; + state[target_start..target_start + *len].copy_from_slice( + &self.chunk_scratch_f64[source_start..source_start + *len], + ); + } + } + } + } + Ok(()) + } + + #[allow(clippy::too_many_arguments)] + pub(crate) fn evaluate_selected_chunks_f64_into_state_profile( + &mut self, + batch_size: usize, + state_parameter_count: usize, + state: &mut [Complex], + parent_input_components: Option<&[usize]>, + parent_input_spans: &[(usize, usize, usize)], + chunk_outputs: &[Vec<(usize, usize)>], + chunk_output_spans: &[Vec<(usize, usize, usize)>], + active_chunk_indices: &[usize], + ) -> RusticolResult<(EvaluatorBatchProfile, f64)> { + if chunk_outputs.len() != self.evaluators.len() + || chunk_output_spans.len() != self.evaluators.len() + { + return Err(RusticolError::invalid_argument( + "stage chunk-output layout does not match evaluator chunks", + )); + } + validate_active_chunk_indices(active_chunk_indices, self.evaluators.len())?; + let expected_state_len = batch_size + .checked_mul(state_parameter_count) + .ok_or_else(|| RusticolError::invalid_argument("stage state length overflows usize"))?; + if state.len() != expected_state_len { + return Err(RusticolError::invalid_argument(format!( + "stage state has length {}, expected {expected_state_len}", + state.len() + ))); + } + match parent_input_components { + Some(components) => { + if components.len() != self.input_len { + return Err(RusticolError::invalid_argument(format!( + "stage input-component map has length {}, expected {}", + components.len(), + self.input_len + ))); + } + if components + .iter() + .any(|component| *component >= state_parameter_count) + { + return Err(RusticolError::invalid_argument( + "stage input-component map references state outside the parameter layout", + )); + } + } + None if self.input_len != state_parameter_count => { + return Err(RusticolError::invalid_argument(format!( + "evaluator input length {} does not match stage state parameter count {state_parameter_count}", + self.input_len + ))); + } + None => {} + } + + let mut input_pack_elapsed = Duration::ZERO; + let mut evaluator_elapsed = Duration::ZERO; + let mut assign_elapsed = Duration::ZERO; + let mut leaf_input_copy_component_count = 0u64; + let mut backend_call_count = 0u64; + let mut scratch_reallocation_count = 0u64; + for &chunk_index in active_chunk_indices { + let evaluator = &mut self.evaluators[chunk_index]; + if let Some(lane_width) = evaluator + .simd_lane_width() + .filter(|lane_width| *lane_width > 1 && batch_size >= *lane_width) + { + let parameter_capacity = self.chunk_parameter_scratch_aosoa_f64.capacity(); + let input_pack_start = Instant::now(); + let input_mapping = self.input_mappings[chunk_index].as_deref(); + let mapping_capacity = self.chunk_input_mapping_scratch.capacity(); + self.chunk_input_mapping_scratch.clear(); + self.chunk_input_mapping_scratch + .reserve(evaluator.input_len); + for local_component in 0..evaluator.input_len { + let parent_component = + input_mapping.map_or(local_component, |indices| indices[local_component]); + self.chunk_input_mapping_scratch.push( + parent_input_components + .map_or(parent_component, |components| components[parent_component]), + ); + } + scratch_reallocation_count += + u64::from(self.chunk_input_mapping_scratch.capacity() != mapping_capacity); + let block_count = pack_f64_parameters_aosoa( + state, + batch_size, + state_parameter_count, + evaluator.input_len, + lane_width, + Some(&self.chunk_input_mapping_scratch), + &mut self.chunk_parameter_scratch_aosoa_f64, + )?; + input_pack_elapsed += input_pack_start.elapsed(); + leaf_input_copy_component_count += + (self.chunk_parameter_scratch_aosoa_f64.len() / 2) as u64; + scratch_reallocation_count += u64::from( + self.chunk_parameter_scratch_aosoa_f64.capacity() != parameter_capacity, + ); + + let output_capacity = self.chunk_scratch_aosoa_f64.capacity(); + self.chunk_scratch_aosoa_f64.resize( + aosoa_scalar_len(block_count, evaluator.output_len, lane_width, "output")?, + 0.0, + ); + scratch_reallocation_count += + u64::from(self.chunk_scratch_aosoa_f64.capacity() != output_capacity); + let eval_start = Instant::now(); + let evaluated = evaluator.evaluate_f64_aosoa_blocks( + block_count, + &self.chunk_parameter_scratch_aosoa_f64, + &mut self.chunk_scratch_aosoa_f64, + )?; + evaluator_elapsed += eval_start.elapsed(); + backend_call_count += 1; + if evaluated { + let assign_start = Instant::now(); + scatter_f64_aosoa_outputs_to_state( + &self.chunk_scratch_aosoa_f64, + batch_size, + evaluator.output_len, + lane_width, + state_parameter_count, + state, + &chunk_outputs[chunk_index], + &chunk_output_spans[chunk_index], + )?; + assign_elapsed += assign_start.elapsed(); + continue; + } + } + let parameter_capacity = self.chunk_parameter_scratch_f64.capacity(); + let input_pack_start = Instant::now(); + let evaluator_params = selected_chunk_f64_parameters( + state, + batch_size, + state_parameter_count, + self.input_len, + parent_input_components, + parent_input_spans, + self.input_mappings[chunk_index].as_deref(), + &self.input_mapping_spans[chunk_index], + &mut self.chunk_parameter_scratch_f64, + )?; + input_pack_elapsed += input_pack_start.elapsed(); + if parent_input_components.is_some() || self.input_mappings[chunk_index].is_some() { + leaf_input_copy_component_count += evaluator_params.len() as u64; + } + + validate_leaf_parameter_length(evaluator, batch_size, evaluator_params)?; + let scratch_len = batch_size + .checked_mul(evaluator.output_len) + .ok_or_else(|| { + RusticolError::invalid_argument("stage chunk output length overflows usize") + })?; + let output_capacity = self.chunk_scratch_f64.capacity(); + self.chunk_scratch_f64.resize(scratch_len, c64(0.0, 0.0)); + let eval_start = Instant::now(); + evaluator.evaluate_f64_batch_unpadded( + batch_size, + evaluator_params, + &mut self.chunk_scratch_f64, + )?; + evaluator_elapsed += eval_start.elapsed(); + backend_call_count += 1; + scratch_reallocation_count += + u64::from(self.chunk_parameter_scratch_f64.capacity() != parameter_capacity) + + u64::from(self.chunk_scratch_f64.capacity() != output_capacity); + + let assign_start = Instant::now(); + let outputs = &chunk_outputs[chunk_index]; + let spans = &chunk_output_spans[chunk_index]; + for row in 0..batch_size { + let row_state = row * state_parameter_count; + let row_eval = row * evaluator.output_len; + if spans.is_empty() { + for (column, state_offset) in outputs { + state[row_state + *state_offset] = + self.chunk_scratch_f64[row_eval + *column]; + } + } else { + for (column_start, state_offset_start, len) in spans { + let source_start = row_eval + *column_start; + let target_start = row_state + *state_offset_start; + state[target_start..target_start + *len].copy_from_slice( + &self.chunk_scratch_f64[source_start..source_start + *len], + ); + } + } + } + assign_elapsed += assign_start.elapsed(); + } + let leaf_input_pack_s = profile_duration_seconds(input_pack_elapsed); + let evaluator_call_s = profile_duration_seconds(evaluator_elapsed); + Ok(( + EvaluatorBatchProfile { + leaf_input_pack_s, + legacy_evaluator_call_s: evaluator_call_s, + evaluator_call_s, + leaf_input_copy_component_count, + backend_call_count, + scratch_reallocation_count, + ..EvaluatorBatchProfile::default() + }, + profile_duration_seconds(assign_elapsed), + )) + } + + pub(crate) fn evaluate_selected_chunks_f64_into_output( + &mut self, + batch_size: usize, + params: &[Complex], + parent_input_components: Option<&[usize]>, + parent_input_spans: &[(usize, usize, usize)], + out: &mut Vec>, + active_chunk_indices: &[usize], + ) -> RusticolResult<()> { + validate_active_chunk_indices(active_chunk_indices, self.evaluators.len())?; + if batch_size == 0 || !params.len().is_multiple_of(batch_size) { + return Err(RusticolError::invalid_argument( + "amplitude evaluator parameter buffer has an inconsistent batch size", + )); + } + let state_parameter_count = params.len() / batch_size; + match parent_input_components { + Some(components) => { + if components.len() != self.input_len + || components + .iter() + .any(|component| *component >= state_parameter_count) + { + return Err(RusticolError::invalid_argument( + "amplitude input-component map is inconsistent with runtime state", + )); + } + } + None if self.input_len != state_parameter_count => { + return Err(RusticolError::invalid_argument(format!( + "evaluator input length {} does not match amplitude state parameter count {state_parameter_count}", + self.input_len + ))); + } + None => {} + } + let output_len = batch_size + .checked_mul(self.output_len) + .ok_or_else(|| RusticolError::invalid_argument("amplitude output length overflows"))?; + out.resize(output_len, c64(0.0, 0.0)); + out.fill(c64(0.0, 0.0)); + + let mut output_start = 0usize; + for chunk_index in 0..self.evaluators.len() { + let evaluator_output_len = self.evaluators[chunk_index].output_len; + if active_chunk_indices.binary_search(&chunk_index).is_ok() { + let evaluator = &mut self.evaluators[chunk_index]; + let mut evaluated_aosoa = false; + if let Some(lane_width) = evaluator + .amplitude_aosoa_lane_width() + .filter(|lane_width| *lane_width > 1 && batch_size >= *lane_width) + { + let input_mapping = self.input_mappings[chunk_index].as_deref(); + self.chunk_input_mapping_scratch.clear(); + self.chunk_input_mapping_scratch + .reserve(evaluator.input_len); + for local_component in 0..evaluator.input_len { + let parent_component = input_mapping + .map_or(local_component, |indices| indices[local_component]); + self.chunk_input_mapping_scratch.push( + parent_input_components.map_or(parent_component, |components| { + components[parent_component] + }), + ); + } + let block_count = pack_f64_parameters_aosoa( + params, + batch_size, + state_parameter_count, + evaluator.input_len, + lane_width, + Some(&self.chunk_input_mapping_scratch), + &mut self.chunk_parameter_scratch_aosoa_f64, + )?; + self.chunk_scratch_aosoa_f64.resize( + aosoa_scalar_len(block_count, evaluator.output_len, lane_width, "output")?, + 0.0, + ); + if evaluator.evaluate_f64_aosoa_blocks( + block_count, + &self.chunk_parameter_scratch_aosoa_f64, + &mut self.chunk_scratch_aosoa_f64, + )? { + scatter_f64_aosoa_outputs_to_rows( + &self.chunk_scratch_aosoa_f64, + batch_size, + evaluator.output_len, + lane_width, + self.output_len, + output_start, + out, + )?; + evaluated_aosoa = true; + } + } + if !evaluated_aosoa { + let evaluator_params = selected_chunk_f64_parameters( + params, + batch_size, + state_parameter_count, + self.input_len, + parent_input_components, + parent_input_spans, + self.input_mappings[chunk_index].as_deref(), + &self.input_mapping_spans[chunk_index], + &mut self.chunk_parameter_scratch_f64, + )?; + validate_leaf_parameter_length(evaluator, batch_size, evaluator_params)?; + self.chunk_scratch_f64.resize( + batch_size + .checked_mul(evaluator_output_len) + .ok_or_else(|| { + RusticolError::invalid_argument( + "amplitude chunk output length overflows usize", + ) + })?, + c64(0.0, 0.0), + ); + evaluator.evaluate_f64_batch_unpadded( + batch_size, + evaluator_params, + &mut self.chunk_scratch_f64, + )?; + for row in 0..batch_size { + let source_start = row * evaluator_output_len; + let target_start = row * self.output_len + output_start; + out[target_start..target_start + evaluator_output_len].copy_from_slice( + &self.chunk_scratch_f64 + [source_start..source_start + evaluator_output_len], + ); + } + } + } + output_start = output_start + .checked_add(evaluator_output_len) + .ok_or_else(|| { + RusticolError::invalid_argument("amplitude chunk output range overflows") + })?; + } + if output_start != self.output_len { + return Err(RusticolError::integrity( + "amplitude evaluator chunk outputs do not cover the declared output length", + )); + } + Ok(()) + } + + pub(crate) fn evaluate_selected_chunks_f64_into_output_profile( + &mut self, + batch_size: usize, + params: &[Complex], + parent_input_components: Option<&[usize]>, + parent_input_spans: &[(usize, usize, usize)], + out: &mut Vec>, + active_chunk_indices: &[usize], + ) -> RusticolResult { + validate_active_chunk_indices(active_chunk_indices, self.evaluators.len())?; + if batch_size == 0 || !params.len().is_multiple_of(batch_size) { + return Err(RusticolError::invalid_argument( + "amplitude evaluator parameter buffer has an inconsistent batch size", + )); + } + let state_parameter_count = params.len() / batch_size; + match parent_input_components { + Some(components) => { + if components.len() != self.input_len + || components + .iter() + .any(|component| *component >= state_parameter_count) + { + return Err(RusticolError::invalid_argument( + "amplitude input-component map is inconsistent with runtime state", + )); + } + } + None if self.input_len != state_parameter_count => { + return Err(RusticolError::invalid_argument(format!( + "evaluator input length {} does not match amplitude state parameter count {state_parameter_count}", + self.input_len + ))); + } + None => {} + } + let output_len = batch_size + .checked_mul(self.output_len) + .ok_or_else(|| RusticolError::invalid_argument("amplitude output length overflows"))?; + let mut output_gather_elapsed = Duration::ZERO; + let output_prepare_start = Instant::now(); + let output_capacity = out.capacity(); + out.resize(output_len, c64(0.0, 0.0)); + out.fill(c64(0.0, 0.0)); + output_gather_elapsed += output_prepare_start.elapsed(); + let mut output_gather_component_count = output_len as u64; + let mut scratch_reallocation_count = u64::from(out.capacity() != output_capacity); + + let mut chunk_output_starts = Vec::with_capacity(self.evaluators.len()); + let mut output_start = 0usize; + for evaluator in &self.evaluators { + chunk_output_starts.push(output_start); + output_start = output_start + .checked_add(evaluator.output_len) + .ok_or_else(|| { + RusticolError::invalid_argument("amplitude chunk output range overflows") + })?; + } + if output_start != self.output_len { + return Err(RusticolError::integrity( + "amplitude evaluator chunk outputs do not cover the declared output length", + )); + } + + let mut input_pack_elapsed = Duration::ZERO; + let mut evaluator_elapsed = Duration::ZERO; + let mut leaf_input_copy_component_count = 0u64; + let mut backend_call_count = 0u64; + for &chunk_index in active_chunk_indices { + let evaluator = &mut self.evaluators[chunk_index]; + if let Some(lane_width) = evaluator + .amplitude_aosoa_lane_width() + .filter(|lane_width| *lane_width > 1 && batch_size >= *lane_width) + { + let parameter_capacity = self.chunk_parameter_scratch_aosoa_f64.capacity(); + let mapping_capacity = self.chunk_input_mapping_scratch.capacity(); + let input_pack_start = Instant::now(); + let input_mapping = self.input_mappings[chunk_index].as_deref(); + self.chunk_input_mapping_scratch.clear(); + self.chunk_input_mapping_scratch + .reserve(evaluator.input_len); + for local_component in 0..evaluator.input_len { + let parent_component = + input_mapping.map_or(local_component, |indices| indices[local_component]); + self.chunk_input_mapping_scratch.push( + parent_input_components + .map_or(parent_component, |components| components[parent_component]), + ); + } + let block_count = pack_f64_parameters_aosoa( + params, + batch_size, + state_parameter_count, + evaluator.input_len, + lane_width, + Some(&self.chunk_input_mapping_scratch), + &mut self.chunk_parameter_scratch_aosoa_f64, + )?; + input_pack_elapsed += input_pack_start.elapsed(); + leaf_input_copy_component_count += + (self.chunk_parameter_scratch_aosoa_f64.len() / 2) as u64; + scratch_reallocation_count += + u64::from( + self.chunk_parameter_scratch_aosoa_f64.capacity() != parameter_capacity, + ) + u64::from(self.chunk_input_mapping_scratch.capacity() != mapping_capacity); + + let chunk_capacity = self.chunk_scratch_aosoa_f64.capacity(); + self.chunk_scratch_aosoa_f64.resize( + aosoa_scalar_len(block_count, evaluator.output_len, lane_width, "output")?, + 0.0, + ); + scratch_reallocation_count += + u64::from(self.chunk_scratch_aosoa_f64.capacity() != chunk_capacity); + let eval_start = Instant::now(); + let evaluated = evaluator.evaluate_f64_aosoa_blocks( + block_count, + &self.chunk_parameter_scratch_aosoa_f64, + &mut self.chunk_scratch_aosoa_f64, + )?; + evaluator_elapsed += eval_start.elapsed(); + backend_call_count += 1; + if evaluated { + let output_gather_start = Instant::now(); + scatter_f64_aosoa_outputs_to_rows( + &self.chunk_scratch_aosoa_f64, + batch_size, + evaluator.output_len, + lane_width, + self.output_len, + chunk_output_starts[chunk_index], + out, + )?; + output_gather_elapsed += output_gather_start.elapsed(); + output_gather_component_count += (batch_size * evaluator.output_len) as u64; + continue; + } + } + let parameter_capacity = self.chunk_parameter_scratch_f64.capacity(); + let input_pack_start = Instant::now(); + let evaluator_params = selected_chunk_f64_parameters( + params, + batch_size, + state_parameter_count, + self.input_len, + parent_input_components, + parent_input_spans, + self.input_mappings[chunk_index].as_deref(), + &self.input_mapping_spans[chunk_index], + &mut self.chunk_parameter_scratch_f64, + )?; + input_pack_elapsed += input_pack_start.elapsed(); + if parent_input_components.is_some() || self.input_mappings[chunk_index].is_some() { + leaf_input_copy_component_count += evaluator_params.len() as u64; + } + + validate_leaf_parameter_length(evaluator, batch_size, evaluator_params)?; + let chunk_capacity = self.chunk_scratch_f64.capacity(); + self.chunk_scratch_f64.resize( + batch_size + .checked_mul(evaluator.output_len) + .ok_or_else(|| { + RusticolError::invalid_argument( + "amplitude chunk output length overflows usize", + ) + })?, + c64(0.0, 0.0), + ); + let eval_start = Instant::now(); + evaluator.evaluate_f64_batch_unpadded( + batch_size, + evaluator_params, + &mut self.chunk_scratch_f64, + )?; + evaluator_elapsed += eval_start.elapsed(); + backend_call_count += 1; + scratch_reallocation_count += + u64::from(self.chunk_parameter_scratch_f64.capacity() != parameter_capacity) + + u64::from(self.chunk_scratch_f64.capacity() != chunk_capacity); + let output_gather_start = Instant::now(); + let chunk_output_start = chunk_output_starts[chunk_index]; + for row in 0..batch_size { + let source_start = row * evaluator.output_len; + let target_start = row * self.output_len + chunk_output_start; + out[target_start..target_start + evaluator.output_len].copy_from_slice( + &self.chunk_scratch_f64[source_start..source_start + evaluator.output_len], + ); + } + output_gather_elapsed += output_gather_start.elapsed(); + output_gather_component_count += (batch_size * evaluator.output_len) as u64; + } + let leaf_input_pack_s = profile_duration_seconds(input_pack_elapsed); + let evaluator_call_s = profile_duration_seconds(evaluator_elapsed); + let output_gather_s = profile_duration_seconds(output_gather_elapsed); + Ok(EvaluatorBatchProfile { + leaf_input_pack_s, + legacy_evaluator_call_s: evaluator_call_s + output_gather_s, + evaluator_call_s, + output_gather_s, + leaf_input_copy_component_count, + output_gather_component_count, + backend_call_count, + scratch_reallocation_count, + }) + } + + pub(crate) fn evaluate_batch_into( + &mut self, + batch_size: usize, + params: &[Complex], + out: &mut Vec>, + ) -> RusticolResult<()> { + self.evaluate_batch_into_unprofiled(batch_size, params, out, false) + } + + pub(crate) fn evaluate_batch_into_padded_simd_tail( + &mut self, + batch_size: usize, + params: &[Complex], + out: &mut Vec>, + ) -> RusticolResult<()> { + self.evaluate_batch_into_unprofiled(batch_size, params, out, true) + } + + fn evaluate_batch_into_unprofiled( + &mut self, + batch_size: usize, + params: &[Complex], + out: &mut Vec>, + pad_incomplete_simd_tail: bool, + ) -> RusticolResult<()> { + let expected_output_len = batch_size * self.output_len; + if out.len() != expected_output_len { + out.resize(expected_output_len, c64(0.0, 0.0)); + } + if params.len() != batch_size * self.input_len { + return Err(RusticolError::invalid_argument(format!( + "parameter buffer has length {}, expected {}", + params.len(), + batch_size * self.input_len + ))); + } + if self.evaluators.len() == 1 { + let evaluator = &mut self.evaluators[0]; + if let Some(lane_width) = evaluator + .amplitude_aosoa_lane_width() + .filter(|lane_width| *lane_width > 1 && batch_size >= *lane_width) + { + let block_count = pack_f64_parameters_aosoa( + params, + batch_size, + self.input_len, + evaluator.input_len, + lane_width, + self.input_mappings[0].as_deref(), + &mut self.chunk_parameter_scratch_aosoa_f64, + )?; + self.chunk_scratch_aosoa_f64.resize( + aosoa_scalar_len(block_count, evaluator.output_len, lane_width, "output")?, + 0.0, + ); + if evaluator.evaluate_f64_aosoa_blocks( + block_count, + &self.chunk_parameter_scratch_aosoa_f64, + &mut self.chunk_scratch_aosoa_f64, + )? { + scatter_f64_aosoa_outputs_to_rows( + &self.chunk_scratch_aosoa_f64, + batch_size, + evaluator.output_len, + lane_width, + self.output_len, + 0, + out, + )?; + return Ok(()); + } + } + let evaluator_params = mapped_f64_parameters( + params, + batch_size, + self.input_len, + self.input_mappings[0].as_deref(), + &self.input_mapping_spans[0], + &mut self.chunk_parameter_scratch_f64, + ); + validate_leaf_parameter_length(evaluator, batch_size, evaluator_params)?; + if pad_incomplete_simd_tail { + evaluator.evaluate_f64_batch(batch_size, evaluator_params, out)?; + } else { + evaluator.evaluate_f64_batch_unpadded(batch_size, evaluator_params, out)?; + } + return Ok(()); + } + let mut output_offset = 0; + for ((evaluator, input_mapping), input_spans) in self + .evaluators + .iter_mut() + .zip(&self.input_mappings) + .zip(&self.input_mapping_spans) + { + if let Some(lane_width) = evaluator + .amplitude_aosoa_lane_width() + .filter(|lane_width| *lane_width > 1 && batch_size >= *lane_width) + { + let block_count = pack_f64_parameters_aosoa( + params, + batch_size, + self.input_len, + evaluator.input_len, + lane_width, + input_mapping.as_deref(), + &mut self.chunk_parameter_scratch_aosoa_f64, + )?; + self.chunk_scratch_aosoa_f64.resize( + aosoa_scalar_len(block_count, evaluator.output_len, lane_width, "output")?, + 0.0, + ); + if evaluator.evaluate_f64_aosoa_blocks( + block_count, + &self.chunk_parameter_scratch_aosoa_f64, + &mut self.chunk_scratch_aosoa_f64, + )? { + scatter_f64_aosoa_outputs_to_rows( + &self.chunk_scratch_aosoa_f64, + batch_size, + evaluator.output_len, + lane_width, + self.output_len, + output_offset, + out, + )?; + output_offset += evaluator.output_len; + continue; + } + } + let evaluator_params = mapped_f64_parameters( + params, + batch_size, + self.input_len, + input_mapping.as_deref(), + input_spans, + &mut self.chunk_parameter_scratch_f64, + ); + validate_leaf_parameter_length(evaluator, batch_size, evaluator_params)?; + self.chunk_scratch_f64 + .resize(batch_size * evaluator.output_len, c64(0.0, 0.0)); + if pad_incomplete_simd_tail { + evaluator.evaluate_f64_batch( + batch_size, + evaluator_params, + &mut self.chunk_scratch_f64, + )?; + } else { + evaluator.evaluate_f64_batch_unpadded( + batch_size, + evaluator_params, + &mut self.chunk_scratch_f64, + )?; + } + for row in 0..batch_size { + let src = row * evaluator.output_len; + let dst = row * self.output_len + output_offset; + out[dst..dst + evaluator.output_len] + .copy_from_slice(&self.chunk_scratch_f64[src..src + evaluator.output_len]); + } + output_offset += evaluator.output_len; + } + Ok(()) + } + + pub(crate) fn evaluate_batch_into_profile( + &mut self, + batch_size: usize, + params: &[Complex], + out: &mut Vec>, + pad_incomplete_simd_tail: bool, + ) -> RusticolResult { + let mut leaf_input_pack_elapsed = Duration::ZERO; + let mut evaluator_elapsed = Duration::ZERO; + let mut output_gather_elapsed = Duration::ZERO; + let mut leaf_input_copy_component_count = 0u64; + let mut output_gather_component_count = 0u64; + let mut backend_call_count = 0u64; + let mut scratch_reallocation_count = 0u64; + let expected_output_len = batch_size * self.output_len; + let output_capacity = out.capacity(); + if out.len() != expected_output_len { + out.resize(expected_output_len, c64(0.0, 0.0)); + } + scratch_reallocation_count += u64::from(out.capacity() != output_capacity); + if params.len() != batch_size * self.input_len { + return Err(RusticolError::invalid_argument(format!( + "parameter buffer has length {}, expected {}", + params.len(), + batch_size * self.input_len + ))); + } + if self.evaluators.len() == 1 { + let evaluator = &mut self.evaluators[0]; + if let Some(lane_width) = evaluator + .amplitude_aosoa_lane_width() + .filter(|lane_width| *lane_width > 1 && batch_size >= *lane_width) + { + let leaf_capacity = self.chunk_parameter_scratch_aosoa_f64.capacity(); + let leaf_start = Instant::now(); + let block_count = pack_f64_parameters_aosoa( + params, + batch_size, + self.input_len, + evaluator.input_len, + lane_width, + self.input_mappings[0].as_deref(), + &mut self.chunk_parameter_scratch_aosoa_f64, + )?; + leaf_input_pack_elapsed += leaf_start.elapsed(); + leaf_input_copy_component_count += + (self.chunk_parameter_scratch_aosoa_f64.len() / 2) as u64; + scratch_reallocation_count += + u64::from(self.chunk_parameter_scratch_aosoa_f64.capacity() != leaf_capacity); + let chunk_capacity = self.chunk_scratch_aosoa_f64.capacity(); + self.chunk_scratch_aosoa_f64.resize( + aosoa_scalar_len(block_count, evaluator.output_len, lane_width, "output")?, + 0.0, + ); + scratch_reallocation_count += + u64::from(self.chunk_scratch_aosoa_f64.capacity() != chunk_capacity); + let backend_start = Instant::now(); + let evaluated = evaluator.evaluate_f64_aosoa_blocks( + block_count, + &self.chunk_parameter_scratch_aosoa_f64, + &mut self.chunk_scratch_aosoa_f64, + )?; + evaluator_elapsed += backend_start.elapsed(); + backend_call_count += 1; + if evaluated { + let gather_start = Instant::now(); + scatter_f64_aosoa_outputs_to_rows( + &self.chunk_scratch_aosoa_f64, + batch_size, + evaluator.output_len, + lane_width, + self.output_len, + 0, + out, + )?; + output_gather_elapsed += gather_start.elapsed(); + output_gather_component_count += (batch_size * evaluator.output_len) as u64; + let leaf_input_pack_s = profile_duration_seconds(leaf_input_pack_elapsed); + let evaluator_call_s = profile_duration_seconds(evaluator_elapsed); + let output_gather_s = profile_duration_seconds(output_gather_elapsed); + return Ok(EvaluatorBatchProfile { + leaf_input_pack_s, + legacy_evaluator_call_s: leaf_input_pack_s + + evaluator_call_s + + output_gather_s, + evaluator_call_s, + output_gather_s, + leaf_input_copy_component_count, + output_gather_component_count, + backend_call_count, + scratch_reallocation_count, + }); + } + } + let leaf_capacity = self.chunk_parameter_scratch_f64.capacity(); + let leaf_start = Instant::now(); + let evaluator_params = mapped_f64_parameters( + params, + batch_size, + self.input_len, + self.input_mappings[0].as_deref(), + &self.input_mapping_spans[0], + &mut self.chunk_parameter_scratch_f64, + ); + leaf_input_pack_elapsed += leaf_start.elapsed(); + if self.input_mappings[0].is_some() { + leaf_input_copy_component_count += evaluator_params.len() as u64; + } + validate_leaf_parameter_length(evaluator, batch_size, evaluator_params)?; + let backend_start = Instant::now(); + if pad_incomplete_simd_tail { + evaluator.evaluate_f64_batch(batch_size, evaluator_params, out)?; + } else { + evaluator.evaluate_f64_batch_unpadded(batch_size, evaluator_params, out)?; + } + evaluator_elapsed += backend_start.elapsed(); + backend_call_count += 1; + scratch_reallocation_count += + u64::from(self.chunk_parameter_scratch_f64.capacity() != leaf_capacity); + let leaf_input_pack_s = profile_duration_seconds(leaf_input_pack_elapsed); + let evaluator_call_s = profile_duration_seconds(evaluator_elapsed); + return Ok(EvaluatorBatchProfile { + leaf_input_pack_s, + legacy_evaluator_call_s: leaf_input_pack_s + evaluator_call_s, + evaluator_call_s, + leaf_input_copy_component_count, + backend_call_count, + scratch_reallocation_count, + ..EvaluatorBatchProfile::default() + }); + } + let mut output_offset = 0; + for ((evaluator, input_mapping), input_spans) in self + .evaluators + .iter_mut() + .zip(&self.input_mappings) + .zip(&self.input_mapping_spans) + { + if let Some(lane_width) = evaluator + .amplitude_aosoa_lane_width() + .filter(|lane_width| *lane_width > 1 && batch_size >= *lane_width) + { + let leaf_capacity = self.chunk_parameter_scratch_aosoa_f64.capacity(); + let leaf_start = Instant::now(); + let block_count = pack_f64_parameters_aosoa( + params, + batch_size, + self.input_len, + evaluator.input_len, + lane_width, + input_mapping.as_deref(), + &mut self.chunk_parameter_scratch_aosoa_f64, + )?; + leaf_input_pack_elapsed += leaf_start.elapsed(); + leaf_input_copy_component_count += + (self.chunk_parameter_scratch_aosoa_f64.len() / 2) as u64; + scratch_reallocation_count += + u64::from(self.chunk_parameter_scratch_aosoa_f64.capacity() != leaf_capacity); + let chunk_capacity = self.chunk_scratch_aosoa_f64.capacity(); + self.chunk_scratch_aosoa_f64.resize( + aosoa_scalar_len(block_count, evaluator.output_len, lane_width, "output")?, + 0.0, + ); + scratch_reallocation_count += + u64::from(self.chunk_scratch_aosoa_f64.capacity() != chunk_capacity); + let backend_start = Instant::now(); + let evaluated = evaluator.evaluate_f64_aosoa_blocks( + block_count, + &self.chunk_parameter_scratch_aosoa_f64, + &mut self.chunk_scratch_aosoa_f64, + )?; + evaluator_elapsed += backend_start.elapsed(); + backend_call_count += 1; + if evaluated { + let gather_start = Instant::now(); + scatter_f64_aosoa_outputs_to_rows( + &self.chunk_scratch_aosoa_f64, + batch_size, + evaluator.output_len, + lane_width, + self.output_len, + output_offset, + out, + )?; + output_gather_elapsed += gather_start.elapsed(); + output_gather_component_count += (batch_size * evaluator.output_len) as u64; + output_offset += evaluator.output_len; + continue; + } + } + let leaf_capacity = self.chunk_parameter_scratch_f64.capacity(); + let leaf_start = Instant::now(); + let evaluator_params = mapped_f64_parameters( + params, + batch_size, + self.input_len, + input_mapping.as_deref(), + input_spans, + &mut self.chunk_parameter_scratch_f64, + ); + leaf_input_pack_elapsed += leaf_start.elapsed(); + if input_mapping.is_some() { + leaf_input_copy_component_count += evaluator_params.len() as u64; + } + validate_leaf_parameter_length(evaluator, batch_size, evaluator_params)?; + let chunk_capacity = self.chunk_scratch_f64.capacity(); + self.chunk_scratch_f64 + .resize(batch_size * evaluator.output_len, c64(0.0, 0.0)); + scratch_reallocation_count += + u64::from(self.chunk_scratch_f64.capacity() != chunk_capacity); + let backend_start = Instant::now(); + if pad_incomplete_simd_tail { + evaluator.evaluate_f64_batch( + batch_size, + evaluator_params, + &mut self.chunk_scratch_f64, + )?; + } else { + evaluator.evaluate_f64_batch_unpadded( + batch_size, + evaluator_params, + &mut self.chunk_scratch_f64, + )?; + } + evaluator_elapsed += backend_start.elapsed(); + backend_call_count += 1; + scratch_reallocation_count += + u64::from(self.chunk_parameter_scratch_f64.capacity() != leaf_capacity); + let gather_start = Instant::now(); + for row in 0..batch_size { + let src = row * evaluator.output_len; + let dst = row * self.output_len + output_offset; + out[dst..dst + evaluator.output_len] + .copy_from_slice(&self.chunk_scratch_f64[src..src + evaluator.output_len]); + } + output_gather_elapsed += gather_start.elapsed(); + output_gather_component_count += (batch_size * evaluator.output_len) as u64; + output_offset += evaluator.output_len; + } + let leaf_input_pack_s = profile_duration_seconds(leaf_input_pack_elapsed); + let evaluator_call_s = profile_duration_seconds(evaluator_elapsed); + let output_gather_s = profile_duration_seconds(output_gather_elapsed); + Ok(EvaluatorBatchProfile { + leaf_input_pack_s, + legacy_evaluator_call_s: leaf_input_pack_s + evaluator_call_s + output_gather_s, + evaluator_call_s, + output_gather_s, + leaf_input_copy_component_count, + output_gather_component_count, + backend_call_count, + scratch_reallocation_count, + }) + } + + #[cfg(feature = "symbolica-runtime")] + pub(crate) fn evaluate_selected_chunks_generic( + &mut self, + batch_size: usize, + params: &[Complex], + binary_precision: Option, + active_chunk_indices: Option<&[usize]>, + ) -> RusticolResult>> + where + T: RusticolHighPrecisionNumber, + Complex: Real + EvaluationDomain, + { + if let Some(indices) = active_chunk_indices { + validate_active_chunk_indices(indices, self.evaluators.len())?; + } + let mut out = vec![complex_zero::(); batch_size * self.output_len]; + if params.len() != batch_size * self.input_len { + return Err(RusticolError::invalid_argument(format!( + "parameter buffer has length {}, expected {}", + params.len(), + batch_size * self.input_len + ))); + } + let mut output_offset = 0; + let mut active_position = 0; + for (chunk_index, (evaluator, input_mapping)) in self + .evaluators + .iter_mut() + .zip(&self.input_mappings) + .enumerate() + { + if let Some(indices) = active_chunk_indices { + if indices.get(active_position) != Some(&chunk_index) { + output_offset += evaluator.output_len; + continue; + } + active_position += 1; + } + let mapped_params = input_mapping.as_ref().map(|indices| { + let mut mapped = Vec::with_capacity(batch_size * indices.len()); + for row in 0..batch_size { + let row_start = row * self.input_len; + mapped.extend( + indices + .iter() + .map(|index| params[row_start + *index].clone()), + ); + } + mapped + }); + let evaluator_params = mapped_params.as_deref().unwrap_or(params); + let mut chunk_out = vec![complex_zero::(); batch_size * evaluator.output_len]; + for row in 0..batch_size { + let in_start = row * evaluator.input_len; + let out_start = row * evaluator.output_len; + T::evaluate_loaded( + evaluator, + &evaluator_params[in_start..in_start + evaluator.input_len], + &mut chunk_out[out_start..out_start + evaluator.output_len], + binary_precision, + )?; + } + for row in 0..batch_size { + let src = row * evaluator.output_len; + let dst = row * self.output_len + output_offset; + out[dst..dst + evaluator.output_len] + .clone_from_slice(&chunk_out[src..src + evaluator.output_len]); + } + output_offset += evaluator.output_len; + } + Ok(out) + } +} + +fn aosoa_scalar_len( + block_count: usize, + component_count: usize, + lane_width: usize, + label: &str, +) -> RusticolResult { + block_count + .checked_mul(component_count) + .and_then(|len| len.checked_mul(2)) + .and_then(|len| len.checked_mul(lane_width)) + .ok_or_else(|| { + RusticolError::invalid_argument(format!("{label} AoSoA buffer length overflows usize")) + }) +} + +fn pack_f64_parameters_aosoa( + source: &[Complex], + batch_size: usize, + source_parameter_count: usize, + leaf_input_len: usize, + lane_width: usize, + input_components: Option<&[usize]>, + scratch: &mut Vec, +) -> RusticolResult { + if batch_size == 0 || lane_width <= 1 { + return Err(RusticolError::invalid_argument( + "AoSoA parameter packing requires a non-empty SIMD batch", + )); + } + let expected_source_len = batch_size + .checked_mul(source_parameter_count) + .ok_or_else(|| { + RusticolError::invalid_argument("source parameter length overflows usize") + })?; + if source.len() != expected_source_len { + return Err(RusticolError::invalid_argument(format!( + "source parameter buffer has length {}, expected {expected_source_len}", + source.len() + ))); + } + match input_components { + Some(components) + if components.len() != leaf_input_len + || components + .iter() + .any(|component| *component >= source_parameter_count) => + { + return Err(RusticolError::integrity( + "AoSoA leaf input mapping is inconsistent with source parameters", + )); + } + None if leaf_input_len != source_parameter_count => { + return Err(RusticolError::integrity( + "AoSoA identity leaf input length does not match source parameters", + )); + } + Some(_) | None => {} + } + let block_count = batch_size.div_ceil(lane_width); + scratch.resize( + aosoa_scalar_len(block_count, leaf_input_len, lane_width, "parameter")?, + 0.0, + ); + match lane_width { + // SAFETY: the checks above establish every source component and the + // complete destination length required by the fixed-lane packers. + 2 => unsafe { + pack_f64_parameters_aosoa_lanes::<2>( + source, + batch_size, + source_parameter_count, + leaf_input_len, + input_components, + scratch, + ) + }, + // SAFETY: see the lane-2 branch. + 4 => unsafe { + pack_f64_parameters_aosoa_lanes::<4>( + source, + batch_size, + source_parameter_count, + leaf_input_len, + input_components, + scratch, + ) + }, + // SAFETY: see the lane-2 branch. + 8 => unsafe { + pack_f64_parameters_aosoa_lanes::<8>( + source, + batch_size, + source_parameter_count, + leaf_input_len, + input_components, + scratch, + ) + }, + _ => pack_f64_parameters_aosoa_dynamic( + source, + batch_size, + source_parameter_count, + leaf_input_len, + lane_width, + input_components, + scratch, + ), + } + Ok(block_count) +} + +/// # Safety +/// +/// `source` must contain `batch_size * source_parameter_count` elements, +/// `input_components` must contain `leaf_input_len` in-range indices when +/// present, and `scratch` must contain the complete padded AoSoA destination. +unsafe fn pack_f64_parameters_aosoa_lanes( + source: &[Complex], + batch_size: usize, + source_parameter_count: usize, + leaf_input_len: usize, + input_components: Option<&[usize]>, + scratch: &mut [f64], +) { + let block_count = batch_size.div_ceil(LANES); + let source_ptr = source.as_ptr(); + let target_ptr = scratch.as_mut_ptr(); + for block in 0..block_count { + let first_row = block * LANES; + for local_component in 0..leaf_input_len { + let component = + input_components.map_or(local_component, |components| components[local_component]); + let target = (block * leaf_input_len + local_component) * 2 * LANES; + for lane in 0..LANES { + let row = (first_row + lane).min(batch_size - 1); + // SAFETY: the caller validated the complete source and target + // lengths plus every mapped component before entering this + // fixed-lane hot loop. + unsafe { + let value = *source_ptr.add(row * source_parameter_count + component); + *target_ptr.add(target + lane) = value.re; + *target_ptr.add(target + LANES + lane) = value.im; + } + } + } + } +} + +fn pack_f64_parameters_aosoa_dynamic( + source: &[Complex], + batch_size: usize, + source_parameter_count: usize, + leaf_input_len: usize, + lane_width: usize, + input_components: Option<&[usize]>, + scratch: &mut [f64], +) { + let block_count = batch_size.div_ceil(lane_width); + let scalar_stride = lane_width * 2; + for block in 0..block_count { + let first_row = block * lane_width; + for local_component in 0..leaf_input_len { + let component = + input_components.map_or(local_component, |components| components[local_component]); + let target = (block * leaf_input_len + local_component) * scalar_stride; + for lane in 0..lane_width { + let row = (first_row + lane).min(batch_size - 1); + let value = source[row * source_parameter_count + component]; + scratch[target + lane] = value.re; + scratch[target + lane_width + lane] = value.im; + } + } + } +} + +#[allow(clippy::too_many_arguments)] +fn scatter_f64_aosoa_outputs_to_state( + outputs_aosoa: &[f64], + batch_size: usize, + evaluator_output_len: usize, + lane_width: usize, + state_parameter_count: usize, + state: &mut [Complex], + outputs: &[(usize, usize)], + spans: &[(usize, usize, usize)], +) -> RusticolResult<()> { + let expected_state_len = batch_size + .checked_mul(state_parameter_count) + .ok_or_else(|| RusticolError::invalid_argument("stage state length overflows usize"))?; + if state.len() != expected_state_len { + return Err(RusticolError::invalid_argument(format!( + "stage state has length {}, expected {expected_state_len}", + state.len() + ))); + } + let block_count = batch_size.div_ceil(lane_width); + let expected_output_len = + aosoa_scalar_len(block_count, evaluator_output_len, lane_width, "output")?; + if outputs_aosoa.len() != expected_output_len { + return Err(RusticolError::integrity(format!( + "AoSoA evaluator output has length {}, expected {expected_output_len}", + outputs_aosoa.len() + ))); + } + let scalar_stride = lane_width * 2; + for block in 0..block_count { + let first_row = block * lane_width; + let valid_lanes = (batch_size - first_row).min(lane_width); + for lane in 0..valid_lanes { + let row_state = (first_row + lane) * state_parameter_count; + if spans.is_empty() { + for (column, state_offset) in outputs { + let source = (block * evaluator_output_len + *column) * scalar_stride; + state[row_state + *state_offset] = Complex::new( + outputs_aosoa[source + lane], + outputs_aosoa[source + lane_width + lane], + ); + } + } else { + for (column_start, state_offset_start, len) in spans { + for offset in 0..*len { + let source = + (block * evaluator_output_len + column_start + offset) * scalar_stride; + state[row_state + state_offset_start + offset] = Complex::new( + outputs_aosoa[source + lane], + outputs_aosoa[source + lane_width + lane], + ); + } + } + } + } + } + Ok(()) +} + +#[allow(clippy::too_many_arguments)] +fn scatter_f64_aosoa_outputs_to_rows( + outputs_aosoa: &[f64], + batch_size: usize, + evaluator_output_len: usize, + lane_width: usize, + row_output_len: usize, + output_offset: usize, + out: &mut [Complex], +) -> RusticolResult<()> { + let expected_output_rows = batch_size.checked_mul(row_output_len).ok_or_else(|| { + RusticolError::invalid_argument("amplitude output length overflows usize") + })?; + if out.len() != expected_output_rows { + return Err(RusticolError::invalid_argument(format!( + "amplitude output has length {}, expected {expected_output_rows}", + out.len() + ))); + } + let output_end = output_offset + .checked_add(evaluator_output_len) + .ok_or_else(|| RusticolError::invalid_argument("amplitude output range overflows usize"))?; + if output_end > row_output_len { + return Err(RusticolError::integrity( + "AoSoA evaluator output range exceeds the amplitude row", + )); + } + let block_count = batch_size.div_ceil(lane_width); + let expected_aosoa_len = + aosoa_scalar_len(block_count, evaluator_output_len, lane_width, "output")?; + if outputs_aosoa.len() != expected_aosoa_len { + return Err(RusticolError::integrity(format!( + "AoSoA evaluator output has length {}, expected {expected_aosoa_len}", + outputs_aosoa.len() + ))); + } + let scalar_stride = lane_width * 2; + for block in 0..block_count { + let first_row = block * lane_width; + let valid_lanes = (batch_size - first_row).min(lane_width); + for lane in 0..valid_lanes { + let row_output = (first_row + lane) * row_output_len + output_offset; + for column in 0..evaluator_output_len { + let source = (block * evaluator_output_len + column) * scalar_stride; + out[row_output + column] = Complex::new( + outputs_aosoa[source + lane], + outputs_aosoa[source + lane_width + lane], + ); + } + } + } + Ok(()) +} + +fn mapped_f64_parameters<'a>( + params: &'a [Complex], + batch_size: usize, + parent_input_len: usize, + input_mapping: Option<&[usize]>, + input_spans: &[(usize, usize, usize)], + scratch: &'a mut Vec>, +) -> &'a [Complex] { + let Some(indices) = input_mapping else { + return params; + }; + scratch.resize(batch_size * indices.len(), c64(0.0, 0.0)); + for row in 0..batch_size { + let source_start = row * parent_input_len; + let target_start = row * indices.len(); + if input_spans.is_empty() { + for (local_index, parent_index) in indices.iter().enumerate() { + scratch[target_start + local_index] = params[source_start + *parent_index]; + } + } else { + for (local_start, parent_start, len) in input_spans { + let target = target_start + *local_start; + let source = source_start + *parent_start; + scratch[target..target + *len].copy_from_slice(¶ms[source..source + *len]); + } + } + } + scratch +} + +#[allow(clippy::too_many_arguments, dead_code)] +fn selected_chunk_f64_parameters<'a>( + state: &'a [Complex], + batch_size: usize, + state_parameter_count: usize, + parent_input_len: usize, + parent_input_components: Option<&[usize]>, + parent_input_spans: &[(usize, usize, usize)], + input_mapping: Option<&[usize]>, + input_mapping_spans: &[(usize, usize, usize)], + scratch: &'a mut Vec>, +) -> RusticolResult<&'a [Complex]> { + let Some(parent_components) = parent_input_components else { + return Ok(mapped_f64_parameters( + state, + batch_size, + state_parameter_count, + input_mapping, + input_mapping_spans, + scratch, + )); + }; + let mapped_input_len = input_mapping.map_or(parent_input_len, <[usize]>::len); + let scratch_len = batch_size.checked_mul(mapped_input_len).ok_or_else(|| { + RusticolError::invalid_argument("selected stage input length overflows usize") + })?; + scratch.resize(scratch_len, c64(0.0, 0.0)); + for row in 0..batch_size { + let source_start = row * state_parameter_count; + let target_start = row * mapped_input_len; + if let Some(indices) = input_mapping { + for (local_index, parent_index) in indices.iter().copied().enumerate() { + scratch[target_start + local_index] = + state[source_start + parent_components[parent_index]]; + } + } else if parent_input_spans.is_empty() { + for (local_index, global_index) in parent_components.iter().copied().enumerate() { + scratch[target_start + local_index] = state[source_start + global_index]; + } + } else { + for (local_start, global_start, len) in parent_input_spans { + let target = target_start + *local_start; + let source = source_start + *global_start; + scratch[target..target + *len].copy_from_slice(&state[source..source + *len]); + } + } + } + Ok(scratch) +} + +#[allow(dead_code)] +fn validate_active_chunk_indices( + active_chunk_indices: &[usize], + chunk_count: usize, +) -> RusticolResult<()> { + let mut previous = None; + for &chunk_index in active_chunk_indices { + if chunk_index >= chunk_count { + return Err(RusticolError::invalid_argument(format!( + "active evaluator chunk index {chunk_index} is outside chunk count {chunk_count}", + ))); + } + if previous.is_some_and(|previous| previous >= chunk_index) { + return Err(RusticolError::invalid_argument( + "active evaluator chunk indices must be strictly increasing", + )); + } + previous = Some(chunk_index); + } + Ok(()) +} + +fn validate_leaf_parameter_length( + evaluator: &LoadedEvaluator, + batch_size: usize, + params: &[Complex], +) -> RusticolResult<()> { + if params.len() != batch_size * evaluator.input_len { + return Err(RusticolError::artifact(format!( + "mapped evaluator parameter buffer has length {}, expected {}", + params.len(), + batch_size * evaluator.input_len + ))); + } + Ok(()) +} + +impl LoadedEvaluator { + #[cfg(feature = "symbolica-runtime")] + pub(crate) fn exact_evaluator( + &mut self, + ) -> RusticolResult<&ExpressionEvaluator>> { + if self.exact_eval.is_none() { + let source = self.exact_eval_source.as_ref().ok_or_else(|| { + RusticolError::invalid_argument( + "high-precision evaluation requires an evaluator-state artifact, but this process artifact has no evaluator_state_path for one or more chunks", + ) + })?; + self.exact_eval = Some(load_evaluator_state_source(source)?.1); + } + Ok(self + .exact_eval + .as_ref() + .expect("exact evaluator initialized from its state path")) + } + + fn simd_lane_width(&self) -> Option { + match &self.eval { + #[cfg(feature = "symbolica-runtime")] + F64Evaluator::ExactOnly => None, + #[cfg(feature = "f64-symjit")] + F64Evaluator::SymjitApplication(eval) => eval.simd_lane_width(), + #[cfg(feature = "f64-compiled")] + F64Evaluator::Compiled(_) => None, + #[cfg(feature = "symbolica-runtime")] + F64Evaluator::Jit(_) => None, + } + } + + fn amplitude_aosoa_lane_width(&self) -> Option { + // Tiny amplitude leaves (notably LC's common 4 -> 1 kernels) do not + // amortize direct packing and scattering. This static shape gate keeps + // the portable path for substantive kernels without runtime tuning or + // architecture-specific thresholds. + const MINIMUM_COMPONENT_FOOTPRINT: usize = 32; + if self.input_len.saturating_add(self.output_len) < MINIMUM_COMPONENT_FOOTPRINT { + None + } else { + self.simd_lane_width() + } + } + + fn evaluate_f64_aosoa_blocks( + &self, + block_count: usize, + params: &[f64], + out: &mut [f64], + ) -> RusticolResult { + match &self.eval { + #[cfg(feature = "symbolica-runtime")] + F64Evaluator::ExactOnly => Ok(false), + #[cfg(feature = "f64-symjit")] + F64Evaluator::SymjitApplication(eval) => { + eval.evaluate_aosoa_blocks(block_count, params, out) + } + #[cfg(feature = "f64-compiled")] + F64Evaluator::Compiled(_) => Ok(false), + #[cfg(feature = "symbolica-runtime")] + F64Evaluator::Jit(_) => Ok(false), + } + } + + pub(crate) fn evaluate_f64_batch( + &mut self, + batch_size: usize, + params: &[Complex], + out: &mut [Complex], + ) -> RusticolResult<()> { + match &mut self.eval { + #[cfg(feature = "symbolica-runtime")] + F64Evaluator::ExactOnly => Err(RusticolError::integrity( + "compiled plane-arena exact evaluator cannot execute f64", + )), + #[cfg(feature = "f64-symjit")] + F64Evaluator::SymjitApplication(eval) => eval.evaluate_batch(batch_size, params, out), + #[cfg(feature = "f64-compiled")] + F64Evaluator::Compiled(eval) => eval.evaluate_batch(batch_size, params, out), + #[cfg(feature = "symbolica-runtime")] + F64Evaluator::Jit(eval) => { + if params.len() != batch_size * self.input_len { + return Err(RusticolError::invalid_argument(format!( + "parameter buffer has length {}, expected {}", + params.len(), + batch_size * self.input_len + ))); + } + if out.len() != batch_size * self.output_len { + return Err(RusticolError::invalid_argument(format!( + "output buffer has length {}, expected {}", + out.len(), + batch_size * self.output_len + ))); + } + eval.evaluate_batch(batch_size, params, out) + .map_err(RusticolError::evaluation) + } + } + } + + fn evaluate_f64_batch_unpadded( + &mut self, + batch_size: usize, + params: &[Complex], + out: &mut [Complex], + ) -> RusticolResult<()> { + match &mut self.eval { + #[cfg(feature = "symbolica-runtime")] + F64Evaluator::ExactOnly => Err(RusticolError::integrity( + "compiled plane-arena exact evaluator cannot execute f64", + )), + #[cfg(feature = "f64-symjit")] + F64Evaluator::SymjitApplication(eval) => { + eval.evaluate_batch_unpadded(batch_size, params, out) + } + #[cfg(feature = "f64-compiled")] + F64Evaluator::Compiled(eval) => eval.evaluate_batch(batch_size, params, out), + #[cfg(feature = "symbolica-runtime")] + F64Evaluator::Jit(eval) => { + if params.len() != batch_size * self.input_len { + return Err(RusticolError::invalid_argument(format!( + "parameter buffer has length {}, expected {}", + params.len(), + batch_size * self.input_len + ))); + } + if out.len() != batch_size * self.output_len { + return Err(RusticolError::invalid_argument(format!( + "output buffer has length {}, expected {}", + out.len(), + batch_size * self.output_len + ))); + } + eval.evaluate_batch(batch_size, params, out) + .map_err(RusticolError::evaluation) + } + } + } +} + +#[cfg(feature = "symbolica-runtime")] +fn flatten_exact_evaluators_from_store( + manifest: &EvaluatorManifest, + payloads: &EvaluatorPayloadStore, + output: &mut Vec, +) -> RusticolResult<()> { + match manifest { + EvaluatorManifest::SymjitApplication { + input_len, + output_len, + evaluator_state_path, + evaluator_state_runtime_capability, + .. + } => { + let state_path = evaluator_state_path.as_ref().ok_or_else(|| { + RusticolError::compatibility( + "compiled plane-arena exact execution requires evaluator-state payloads", + ) + })?; + if evaluator_state_runtime_capability.as_deref() + != Some(SYMBOLICA_LEGACY_JIT_RUNTIME_CAPABILITY) + { + return Err(RusticolError::compatibility(format!( + "SymJIT evaluator state {state_path:?} does not declare capability {:?}", + SYMBOLICA_LEGACY_JIT_RUNTIME_CAPABILITY + ))); + } + output.push(LoadedEvaluator { + eval: F64Evaluator::ExactOnly, + exact_eval: None, + exact_eval_source: Some(payloads.source(state_path)?), + double_eval: None, + arb_eval: None, + input_len: *input_len, + output_len: *output_len, + }); + Ok(()) + } + EvaluatorManifest::CompiledComplex { + input_len, + output_len, + evaluator_state_path, + native_direct_application: Some(_), + .. + } => { + let state_path = evaluator_state_path.as_ref().ok_or_else(|| { + RusticolError::compatibility( + "compiled plane-arena exact execution requires evaluator-state payloads", + ) + })?; + output.push(LoadedEvaluator { + eval: F64Evaluator::ExactOnly, + exact_eval: None, + exact_eval_source: Some(payloads.source(state_path)?), + double_eval: None, + arb_eval: None, + input_len: *input_len, + output_len: *output_len, + }); + Ok(()) + } + EvaluatorManifest::Chunked { chunks, .. } => { + for chunk in chunks { + flatten_exact_evaluators_from_store(chunk, payloads, output)?; + } + Ok(()) + } + _ => Err(RusticolError::compatibility( + "compiled plane-arena exact execution requires direct leaves with evaluator state", + )), + } +} + +pub(crate) fn flatten_evaluators_from_store( + manifest: &EvaluatorManifest, + payloads: &EvaluatorPayloadStore, + output: &mut Vec, +) -> RusticolResult<()> { + match manifest { + EvaluatorManifest::SymjitApplication { + runtime_capability, + application_path, + application_abi, + input_len, + output_len, + element_layout, + batch_layout, + compiler_type, + translation_mode, + optimization_level, + word_bits, + endianness, + required_defuns, + evaluator_state_path, + evaluator_state_runtime_capability, + .. + } => { + #[cfg(feature = "f64-symjit")] + { + let application_source = payloads.source(application_path)?; + let application_bytes = application_source.read()?; + let eval = SymjitApplicationEvaluator::load_bytes( + application_bytes.as_ref(), + PathBuf::from(application_source.display_name()), + SymjitApplicationMetadata { + runtime_capability, + application_abi, + input_len: *input_len, + output_len: *output_len, + element_layout, + batch_layout, + compiler_type, + translation_mode, + optimization_level: *optimization_level, + word_bits: *word_bits, + endianness, + required_defuns, + }, + )?; + #[cfg(feature = "symbolica-runtime")] + let exact_eval_source = match evaluator_state_path { + Some(state_path) => { + if evaluator_state_runtime_capability.as_deref() + != Some(SYMBOLICA_LEGACY_JIT_RUNTIME_CAPABILITY) + { + return Err(RusticolError::compatibility(format!( + "SymJIT evaluator state {:?} does not declare capability {:?}", + state_path, SYMBOLICA_LEGACY_JIT_RUNTIME_CAPABILITY + ))); + } + Some(payloads.source(state_path)?) + } + None => None, + }; + #[cfg(not(feature = "symbolica-runtime"))] + let _ = (evaluator_state_path, evaluator_state_runtime_capability); + output.push(LoadedEvaluator { + eval: F64Evaluator::SymjitApplication(eval), + #[cfg(feature = "symbolica-runtime")] + exact_eval: None, + #[cfg(feature = "symbolica-runtime")] + exact_eval_source, + #[cfg(feature = "symbolica-runtime")] + double_eval: None, + #[cfg(feature = "symbolica-runtime")] + arb_eval: None, + input_len: *input_len, + output_len: *output_len, + }); + Ok(()) + } + #[cfg(not(feature = "f64-symjit"))] + { + let _ = ( + application_path, + application_abi, + input_len, + output_len, + element_layout, + batch_layout, + compiler_type, + translation_mode, + optimization_level, + word_bits, + endianness, + required_defuns, + evaluator_state_path, + evaluator_state_runtime_capability, + ); + Err(unsupported_capability(runtime_capability)) + } + } + EvaluatorManifest::Jit { + runtime_capability, + input_len, + output_len, + evaluator_state_path, + } => { + #[cfg(feature = "symbolica-runtime")] + { + let _ = runtime_capability; + let (jit_settings, exact_eval, jit_eval) = + load_evaluator_state_source(&payloads.source(evaluator_state_path)?)?; + let eval = F64Evaluator::Jit(match jit_eval { + Some(eval) => eval, + None => exact_eval + .jit_compile::>(jit_settings) + .map_err(|err| { + RusticolError::evaluation(format!( + "could not compile scalar JIT evaluator from {}: {err}", + evaluator_state_path + )) + })?, + }); + output.push(LoadedEvaluator { + eval, + exact_eval: Some(exact_eval), + exact_eval_source: None, + double_eval: None, + arb_eval: None, + input_len: *input_len, + output_len: *output_len, + }); + Ok(()) + } + #[cfg(not(feature = "symbolica-runtime"))] + { + let _ = (input_len, output_len, evaluator_state_path); + Err(unsupported_capability(runtime_capability)) + } + } + EvaluatorManifest::CompiledComplex { + runtime_capability, + function_name, + input_len, + output_len, + library_path, + evaluator_state_path, + number_type, + native_direct_application: _, + } => { + #[cfg(feature = "f64-compiled")] + { + let _ = runtime_capability; + if number_type != "complex" { + return Err(RusticolError::invalid_argument(format!( + "rusticol currently supports compiled complex evaluators, got {number_type}" + ))); + } + let library = payloads.load_native_library(library_path)?; + let eval = CompiledComplexF64Evaluator::load( + library, + function_name, + *input_len, + *output_len, + )?; + #[cfg(feature = "symbolica-runtime")] + let exact_eval_source = evaluator_state_path + .as_deref() + .map(|state_path| payloads.source(state_path)) + .transpose()?; + #[cfg(not(feature = "symbolica-runtime"))] + let _ = evaluator_state_path; + output.push(LoadedEvaluator { + eval: F64Evaluator::Compiled(eval), + #[cfg(feature = "symbolica-runtime")] + exact_eval: None, + #[cfg(feature = "symbolica-runtime")] + exact_eval_source, + #[cfg(feature = "symbolica-runtime")] + double_eval: None, + #[cfg(feature = "symbolica-runtime")] + arb_eval: None, + input_len: *input_len, + output_len: *output_len, + }); + Ok(()) + } + #[cfg(not(feature = "f64-compiled"))] + { + let _ = ( + function_name, + input_len, + output_len, + library_path, + evaluator_state_path, + number_type, + ); + Err(unsupported_capability(runtime_capability)) + } + } + EvaluatorManifest::Chunked { chunks, .. } => { + for chunk in chunks { + flatten_evaluators_from_store(chunk, payloads, output)?; + } + Ok(()) + } + } +} + +pub(crate) fn ensure_evaluator_capabilities_supported( + manifest: &EvaluatorManifest, +) -> RusticolResult<()> { + let declared = evaluator_runtime_capabilities(manifest)?; + ensure_runtime_capabilities_supported(declared.iter().map(String::as_str)) +} + +pub(crate) fn evaluator_runtime_capabilities( + manifest: &EvaluatorManifest, +) -> RusticolResult> { + let mut capabilities = BTreeSet::new(); + collect_evaluator_capabilities(manifest, &mut capabilities)?; + Ok(capabilities) +} + +fn collect_evaluator_capabilities( + manifest: &EvaluatorManifest, + output: &mut BTreeSet, +) -> RusticolResult<()> { + match manifest { + EvaluatorManifest::SymjitApplication { + runtime_capability, .. + } => validate_and_insert_capability( + runtime_capability, + &[SYMJIT_APPLICATION_RUNTIME_CAPABILITY], + output, + ), + EvaluatorManifest::Jit { + runtime_capability, .. + } => validate_and_insert_capability( + runtime_capability, + &[SYMBOLICA_LEGACY_JIT_RUNTIME_CAPABILITY], + output, + ), + EvaluatorManifest::CompiledComplex { + runtime_capability, .. + } => validate_and_insert_capability( + runtime_capability, + &[ + SYMBOLICA_COMPILED_CPP_RUNTIME_CAPABILITY, + SYMBOLICA_COMPILED_ASM_RUNTIME_CAPABILITY, + ], + output, + ), + EvaluatorManifest::Chunked { + required_runtime_capabilities, + chunks, + .. + } => { + let mut actual = BTreeSet::new(); + for chunk in chunks { + collect_evaluator_capabilities(chunk, &mut actual)?; + } + let declared = required_runtime_capabilities + .iter() + .cloned() + .collect::>(); + if declared.len() != required_runtime_capabilities.len() || declared != actual { + return Err(RusticolError::integrity(format!( + "chunked evaluator capabilities {:?} do not match child capabilities {:?}", + required_runtime_capabilities, actual + ))); + } + output.extend(actual); + Ok(()) + } + } +} + +fn validate_and_insert_capability( + capability: &str, + expected: &[&str], + output: &mut BTreeSet, +) -> RusticolResult<()> { + if !expected.contains(&capability) { + return Err(RusticolError::compatibility(format!( + "evaluator declares runtime capability {capability:?}, expected one of {expected:?}" + ))); + } + output.insert(capability.to_string()); + Ok(()) +} + +#[cfg(any( + not(feature = "f64-compiled"), + not(feature = "f64-symjit"), + not(feature = "symbolica-runtime") +))] +fn unsupported_capability(capability: &str) -> RusticolError { + RusticolError::unsupported_runtime_capability( + capability, + format!( + "this Rusticol build supports {:?}", + supported_runtime_capabilities() + ), + ) +} + +#[cfg(feature = "symbolica-runtime")] +type LoadedEvaluatorState = ( + JITCompilationSettings, + ExpressionEvaluator>, + Option>>, +); + +#[cfg(feature = "symbolica-runtime")] +fn load_evaluator_state_source( + source: &EvaluatorPayloadSource, +) -> RusticolResult { + let bytes = source.read()?; + load_evaluator_state_bytes(bytes.as_ref(), &source.display_name()) +} + +#[cfg(feature = "symbolica-runtime")] +fn load_evaluator_state_bytes( + bytes: &[u8], + display_name: &str, +) -> RusticolResult { + type SavedEvaluator = ( + bool, + JITCompilationSettings, + ExpressionEvaluator>, + Option>, + Option>>, + ); + + let ((_, settings, evaluator, _, jit_complex), consumed) = + bincode::decode_from_slice::(bytes, bincode::config::standard()) + .map_err(|error| { + RusticolError::compatibility(format!( + "evaluator state {} does not match Symbolica serialization ABI {}: {error}; regenerate the schema-v3 artifact", + display_name, + crate::SYMBOLICA_SERIALIZATION_ABI + )) + })?; + ensure_evaluator_state_consumed(display_name, bytes.len(), consumed)?; + Ok((settings, evaluator, jit_complex)) +} + +#[cfg(feature = "symbolica-runtime")] +fn ensure_evaluator_state_consumed( + display_name: &str, + encoded_len: usize, + consumed: usize, +) -> RusticolResult<()> { + if consumed == encoded_len { + return Ok(()); + } + Err(RusticolError::integrity(format!( + "evaluator state {} contains {} trailing bytes", + display_name, + encoded_len - consumed + ))) +} + +#[cfg(test)] +mod aosoa_tests { + use super::*; + + fn source_value(row: usize, component: usize) -> Complex { + Complex::new( + (100 * row + component) as f64, + -((100 * row + component) as f64) - 0.5, + ) + } + + fn check_pack_and_scatter(lane_width: usize) { + let batch_size = lane_width + 1; + let source_parameter_count = 4; + let input_components = [2, 0, 3]; + let source = (0..batch_size) + .flat_map(|row| { + (0..source_parameter_count).map(move |component| source_value(row, component)) + }) + .collect::>(); + let mut packed = Vec::new(); + let block_count = pack_f64_parameters_aosoa( + &source, + batch_size, + source_parameter_count, + input_components.len(), + lane_width, + Some(&input_components), + &mut packed, + ) + .unwrap(); + assert_eq!(block_count, 2); + for block in 0..block_count { + for (local_component, source_component) in input_components.iter().copied().enumerate() + { + let scalar_start = + (block * input_components.len() + local_component) * 2 * lane_width; + for lane in 0..lane_width { + let row = (block * lane_width + lane).min(batch_size - 1); + let expected = source_value(row, source_component); + assert_eq!(packed[scalar_start + lane], expected.re); + assert_eq!(packed[scalar_start + lane_width + lane], expected.im); + } + } + } + + let evaluator_output_len = 4; + let mut evaluator_outputs = vec![0.0; block_count * evaluator_output_len * 2 * lane_width]; + for block in 0..block_count { + for column in 0..evaluator_output_len { + let scalar_start = (block * evaluator_output_len + column) * 2 * lane_width; + for lane in 0..lane_width { + let row = block * lane_width + lane; + evaluator_outputs[scalar_start + lane] = (1000 * row + column) as f64; + evaluator_outputs[scalar_start + lane_width + lane] = + -((1000 * row + column) as f64); + } + } + } + + let state_parameter_count = 7; + let sentinel = Complex::new(12345.0, -12345.0); + let mut mapped_state = vec![sentinel; batch_size * state_parameter_count]; + scatter_f64_aosoa_outputs_to_state( + &evaluator_outputs, + batch_size, + evaluator_output_len, + lane_width, + state_parameter_count, + &mut mapped_state, + &[(0, 1), (2, 5)], + &[], + ) + .unwrap(); + for row in 0..batch_size { + assert_eq!( + mapped_state[row * state_parameter_count + 1], + Complex::new((1000 * row) as f64, -((1000 * row) as f64)) + ); + assert_eq!( + mapped_state[row * state_parameter_count + 5], + Complex::new((1000 * row + 2) as f64, -((1000 * row + 2) as f64)) + ); + assert_eq!(mapped_state[row * state_parameter_count], sentinel); + } + + let mut spanned_state = vec![sentinel; batch_size * state_parameter_count]; + scatter_f64_aosoa_outputs_to_state( + &evaluator_outputs, + batch_size, + evaluator_output_len, + lane_width, + state_parameter_count, + &mut spanned_state, + &[], + &[(1, 3, 2)], + ) + .unwrap(); + for row in 0..batch_size { + for offset in 0..2 { + assert_eq!( + spanned_state[row * state_parameter_count + 3 + offset], + Complex::new( + (1000 * row + 1 + offset) as f64, + -((1000 * row + 1 + offset) as f64) + ) + ); + } + assert_eq!(spanned_state[row * state_parameter_count], sentinel); + } + + let row_output_len = evaluator_output_len + 2; + let mut row_outputs = vec![sentinel; batch_size * row_output_len]; + scatter_f64_aosoa_outputs_to_rows( + &evaluator_outputs, + batch_size, + evaluator_output_len, + lane_width, + row_output_len, + 1, + &mut row_outputs, + ) + .unwrap(); + for row in 0..batch_size { + assert_eq!(row_outputs[row * row_output_len], sentinel); + assert_eq!( + row_outputs[row * row_output_len + row_output_len - 1], + sentinel + ); + for column in 0..evaluator_output_len { + assert_eq!( + row_outputs[row * row_output_len + 1 + column], + Complex::new( + (1000 * row + column) as f64, + -((1000 * row + column) as f64) + ) + ); + } + } + } + + #[test] + fn portable_aosoa_pack_and_scatter_cover_fixed_and_dynamic_widths() { + for lane_width in [2, 4, 8, 3] { + check_pack_and_scatter(lane_width); + } + } + + #[test] + fn aosoa_pack_rejects_inconsistent_mappings_and_lengths() { + let mut scratch = Vec::new(); + let source = vec![c64(0.0, 0.0); 8]; + assert!(pack_f64_parameters_aosoa(&source, 2, 4, 2, 4, Some(&[0]), &mut scratch).is_err()); + assert!( + pack_f64_parameters_aosoa(&source, 2, 4, 2, 4, Some(&[0, 4]), &mut scratch).is_err() + ); + assert!( + pack_f64_parameters_aosoa(&source[..7], 2, 4, 2, 4, Some(&[0, 1]), &mut scratch) + .is_err() + ); + } +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/evaluator/compiled.rs b/pyamplicol/rust/crates/rusticol-core/src/evaluator/compiled.rs new file mode 100644 index 0000000000..92aa4f5b60 --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/evaluator/compiled.rs @@ -0,0 +1,263 @@ +// SPDX-License-Identifier: 0BSD + +use super::super::*; +use crate::artifact::PinnedNativeLibrary; +use std::sync::Arc; + +type EvaluateFunction = + unsafe extern "C" fn(*const Complex, *mut Complex, *mut Complex); +type BufferLengthFunction = unsafe extern "C" fn() -> std::ffi::c_ulong; + +pub(crate) struct CompiledComplexF64Evaluator { + _library: Arc, + evaluate: EvaluateFunction, + scratch: Vec>, + input_len: usize, + output_len: usize, +} + +impl CompiledComplexF64Evaluator { + pub(crate) fn load( + library: Arc, + function_name: &str, + input_len: usize, + output_len: usize, + ) -> RusticolResult { + if function_name.is_empty() || input_len == 0 || output_len == 0 { + return Err(RusticolError::invalid_argument( + "compiled evaluator function_name, input_len, and output_len must be non-empty", + )); + } + if std::mem::size_of::>() != 2 * std::mem::size_of::() + || std::mem::align_of::>() != std::mem::align_of::() + { + return Err(RusticolError::compatibility( + "Rusticol complex-f64 storage is incompatible with the compiled evaluator ABI", + )); + } + + let path = library.display_path(); + let exported_name = format!("{function_name}_complexf64"); + let evaluate = unsafe { + *library + .get::(exported_name.as_bytes()) + .map_err(|error| { + RusticolError::evaluation(format!( + "could not load compiled evaluator symbol {exported_name:?} from {}: {error}", + path.display() + )) + })? + }; + let buffer_name = format!("{exported_name}_get_buffer_len"); + let buffer_length = unsafe { + *library + .get::(buffer_name.as_bytes()) + .map_err(|error| { + RusticolError::evaluation(format!( + "could not load compiled evaluator symbol {buffer_name:?} from {}: {error}", + path.display() + )) + })? + }; + let scratch_len = usize::try_from(unsafe { buffer_length() }).map_err(|_| { + RusticolError::evaluation("compiled evaluator scratch-buffer length exceeds usize") + })?; + let mut scratch = Vec::new(); + scratch.try_reserve_exact(scratch_len).map_err(|error| { + RusticolError::evaluation(format!( + "could not allocate compiled evaluator scratch buffer of length {scratch_len}: {error}" + )) + })?; + scratch.resize(scratch_len, Complex { re: 0.0, im: 0.0 }); + + Ok(Self { + _library: library, + evaluate, + scratch, + input_len, + output_len, + }) + } + + pub(crate) fn evaluate_batch( + &mut self, + batch_size: usize, + params: &[Complex], + out: &mut [Complex], + ) -> RusticolResult<()> { + if batch_size == 0 { + return Err(RusticolError::invalid_argument( + "compiled evaluator batch_size must be positive", + )); + } + let expected_params = batch_size.checked_mul(self.input_len).ok_or_else(|| { + RusticolError::invalid_argument("compiled evaluator input length overflows usize") + })?; + let expected_outputs = batch_size.checked_mul(self.output_len).ok_or_else(|| { + RusticolError::invalid_argument("compiled evaluator output length overflows usize") + })?; + if params.len() != expected_params { + return Err(RusticolError::invalid_argument(format!( + "parameter buffer has length {}, expected {expected_params}", + params.len() + ))); + } + if out.len() != expected_outputs { + return Err(RusticolError::invalid_argument(format!( + "output buffer has length {}, expected {expected_outputs}", + out.len() + ))); + } + + for (input_row, output_row) in params + .chunks_exact(self.input_len) + .zip(out.chunks_exact_mut(self.output_len)) + { + // SAFETY: Length checks above guarantee complete input/output rows. The + // scratch buffer has the size reported by the loaded evaluator itself. + unsafe { + (self.evaluate)( + input_row.as_ptr(), + self.scratch.as_mut_ptr(), + output_row.as_mut_ptr(), + ); + } + } + Ok(()) + } +} + +#[cfg(all(test, any(target_os = "linux", target_os = "macos")))] +mod tests { + use super::*; + use std::process::Command; + use std::time::{SystemTime, UNIX_EPOCH}; + + fn compiled_fixture() -> (PathBuf, PathBuf) { + let unique = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + let directory = std::env::temp_dir().join(format!( + "rusticol-compiled-test-{}-{unique}", + std::process::id() + )); + fs::create_dir_all(&directory).unwrap(); + let source = directory.join("fixture.cpp"); + let library = directory.join(if cfg!(target_os = "macos") { + "libfixture.dylib" + } else { + "libfixture.so" + }); + fs::write( + &source, + r#"#include +extern "C" unsigned long rusticol_test_complexf64_get_buffer_len() { return 1; } +extern "C" void rusticol_test_complexf64( + std::complex* params, + std::complex* buffer, + std::complex* out) { + buffer[0] = params[0] + params[1]; + out[0] = buffer[0]; + out[1] = params[0] * params[1]; +} +"#, + ) + .unwrap(); + let compiler = std::env::var("CXX").unwrap_or_else(|_| "c++".to_string()); + let mut command = Command::new(compiler); + command.arg("-std=c++17"); + if cfg!(target_os = "macos") { + command.arg("-dynamiclib"); + } else { + command.args(["-shared", "-fPIC"]); + } + let output = command + .arg(&source) + .arg("-o") + .arg(&library) + .output() + .unwrap(); + assert!( + output.status.success(), + "could not compile test evaluator: {}", + String::from_utf8_lossy(&output.stderr) + ); + (directory, library) + } + + #[test] + fn symbolica_compiled_complex_abi_loads_and_evaluates_batches() { + let (directory, library) = compiled_fixture(); + let mut evaluator = CompiledComplexF64Evaluator::load( + PinnedNativeLibrary::from_test_path(&library).unwrap(), + "rusticol_test", + 2, + 2, + ) + .unwrap(); + let params = [ + Complex::new(1.0, 2.0), + Complex::new(3.0, -4.0), + Complex::new(-2.0, 0.5), + Complex::new(4.0, 1.5), + ]; + let mut output = [Complex::new(0.0, 0.0); 4]; + + evaluator.evaluate_batch(2, ¶ms, &mut output).unwrap(); + + assert_eq!(output[0], Complex::new(4.0, -2.0)); + assert_eq!(output[1], Complex::new(11.0, 2.0)); + assert_eq!(output[2], Complex::new(2.0, 2.0)); + assert_eq!(output[3], Complex::new(-8.75, -1.0)); + fs::remove_dir_all(directory).unwrap(); + } + + #[test] + fn cpp_and_asm_manifests_load_through_the_runtime_group() { + let (directory, library) = compiled_fixture(); + let library_name = library.file_name().unwrap().to_str().unwrap().to_string(); + for capability in [ + SYMBOLICA_COMPILED_CPP_RUNTIME_CAPABILITY, + SYMBOLICA_COMPILED_ASM_RUNTIME_CAPABILITY, + ] { + let manifest = EvaluatorManifest::CompiledComplex { + runtime_capability: capability.to_string(), + function_name: "rusticol_test".to_string(), + input_len: 2, + output_len: 2, + library_path: library_name.clone(), + evaluator_state_path: Some("unused-symbolica-state.bin".to_string()), + number_type: "complex".to_string(), + native_direct_application: None, + }; + let mut group = EvaluatorGroup::load(&manifest, &directory).unwrap(); + let output = group + .evaluate_batch(1, &[Complex::new(1.0, 2.0), Complex::new(3.0, -4.0)]) + .unwrap(); + + assert_eq!(output, [Complex::new(4.0, -2.0), Complex::new(11.0, 2.0)]); + } + fs::remove_dir_all(directory).unwrap(); + } + + #[test] + fn compiled_evaluator_validates_batch_dimensions() { + let (directory, library) = compiled_fixture(); + let mut evaluator = CompiledComplexF64Evaluator::load( + PinnedNativeLibrary::from_test_path(&library).unwrap(), + "rusticol_test", + 2, + 2, + ) + .unwrap(); + let mut output = [Complex::new(0.0, 0.0); 2]; + + let error = evaluator + .evaluate_batch(1, &[Complex::new(1.0, 0.0)], &mut output) + .unwrap_err(); + + assert_eq!(error.kind(), crate::RusticolErrorKind::InvalidArgument); + fs::remove_dir_all(directory).unwrap(); + } +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/evaluator/crossing.rs b/pyamplicol/rust/crates/rusticol-core/src/evaluator/crossing.rs new file mode 100644 index 0000000000..b4acab86e3 --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/evaluator/crossing.rs @@ -0,0 +1,363 @@ +// SPDX-License-Identifier: 0BSD + +use super::super::*; + +pub(crate) fn apply_input_crossing_map( + batch: Vec>, + expected_legs: usize, + input_crossing_map: Option<&[InputCrossingMapEntry]>, +) -> RusticolResult>> { + let Some(map) = input_crossing_map else { + return Ok(batch); + }; + if map.len() != expected_legs { + return Err(RusticolError::invalid_argument(format!( + "input crossing map has {} entries, expected {expected_legs}", + map.len() + ))); + } + let mut seen = vec![false; expected_legs]; + for entry in map { + if entry.target_index >= expected_legs || entry.source_index >= expected_legs { + return Err(RusticolError::invalid_argument( + "input crossing map references an out-of-range external leg", + )); + } + if seen[entry.target_index] { + return Err(RusticolError::invalid_argument( + "input crossing map contains a duplicate target index", + )); + } + seen[entry.target_index] = true; + } + if seen.iter().any(|value| !*value) { + return Err(RusticolError::invalid_argument( + "input crossing map does not cover every target index", + )); + } + let mut mapped_batch = Vec::with_capacity(batch.len()); + for point in batch { + let mut mapped = vec![[0.0; 4]; expected_legs]; + for entry in map { + let source = point[entry.source_index]; + mapped[entry.target_index] = [ + entry.sign * source[0], + entry.sign * source[1], + entry.sign * source[2], + entry.sign * source[3], + ]; + } + mapped_batch.push(mapped); + } + Ok(mapped_batch) +} + +#[cfg(feature = "symbolica-runtime")] +pub(crate) fn validate_input_crossing_map( + expected_legs: usize, + input_crossing_map: Option<&[InputCrossingMapEntry]>, +) -> RusticolResult> { + let Some(map) = input_crossing_map else { + return Ok(None); + }; + if map.len() != expected_legs { + return Err(RusticolError::invalid_argument(format!( + "input crossing map has {} entries, expected {expected_legs}", + map.len() + ))); + } + let mut seen = vec![false; expected_legs]; + for entry in map { + if entry.target_index >= expected_legs || entry.source_index >= expected_legs { + return Err(RusticolError::invalid_argument( + "input crossing map references an out-of-range external leg", + )); + } + if seen[entry.target_index] { + return Err(RusticolError::invalid_argument( + "input crossing map contains a duplicate target index", + )); + } + seen[entry.target_index] = true; + } + if seen.iter().any(|value| !*value) { + return Err(RusticolError::invalid_argument( + "input crossing map does not cover every target index", + )); + } + Ok(Some(map)) +} + +#[cfg(feature = "symbolica-runtime")] +pub(crate) fn apply_input_crossing_map_generic( + batch: &[Vec<[T; 4]>], + expected_legs: usize, + input_crossing_map: Option<&[InputCrossingMapEntry]>, +) -> RusticolResult>> +where + T: RusticolHighPrecisionNumber, + Complex: Real + EvaluationDomain, +{ + let Some(map) = validate_input_crossing_map(expected_legs, input_crossing_map)? else { + return Ok(batch.to_vec()); + }; + let mut mapped_batch = Vec::with_capacity(batch.len()); + for point in batch { + let mut mapped = vec![std::array::from_fn(|_| T::new_zero()); expected_legs]; + for entry in map { + let source = &point[entry.source_index]; + for (target, source_component) in mapped[entry.target_index].iter_mut().zip(source) { + *target = T::from(entry.sign) * source_component.clone(); + } + } + mapped_batch.push(mapped); + } + Ok(mapped_batch) +} + +pub(crate) fn apply_lc_topology_label_permutation( + batch: &[Vec<[f64; 4]>], + expected_legs: usize, + mapping: &[(usize, usize)], +) -> RusticolResult>> { + let mut seen = vec![false; expected_legs]; + for (representative_index, sector_index) in mapping { + if *representative_index >= expected_legs || *sector_index >= expected_legs { + return Err(RusticolError::invalid_argument( + "LC topology replay label permutation references an out-of-range external leg", + )); + } + if seen[*representative_index] { + return Err(RusticolError::invalid_argument( + "LC topology replay label permutation contains a duplicate representative label", + )); + } + seen[*representative_index] = true; + } + let mut mapped_batch = Vec::with_capacity(batch.len()); + for point in batch { + if point.len() != expected_legs { + return Err(RusticolError::invalid_argument(format!( + "LC topology replay point has {} external legs, expected {expected_legs}", + point.len(), + ))); + } + let mut mapped = point.clone(); + for (representative_index, sector_index) in mapping { + mapped[*representative_index] = point[*sector_index]; + } + mapped_batch.push(mapped); + } + Ok(mapped_batch) +} + +pub(crate) fn apply_lc_topology_label_permutations( + batch: &[Vec<[f64; 4]>], + expected_legs: usize, + mappings: &[Vec<(usize, usize)>], +) -> RusticolResult>> { + let mut expanded_batch = Vec::with_capacity(batch.len() * mappings.len()); + for mapping in mappings { + expanded_batch.extend(apply_lc_topology_label_permutation( + batch, + expected_legs, + mapping, + )?); + } + Ok(expanded_batch) +} + +pub(crate) fn apply_lc_topology_label_permutations_from_view( + batch: F64MomentumBatchView<'_>, + expected_legs: usize, + mappings: &[Vec<(usize, usize)>], +) -> RusticolResult>> { + if batch.external_count() != expected_legs { + return Err(RusticolError::invalid_argument(format!( + "LC topology replay input has {} external legs, expected {expected_legs}", + batch.external_count() + ))); + } + let capacity = batch + .point_count() + .checked_mul(mappings.len()) + .ok_or_else(|| RusticolError::invalid_argument("LC topology replay batch overflows"))?; + let mut expanded_batch = Vec::with_capacity(capacity); + for mapping in mappings { + let mut seen = vec![false; expected_legs]; + for (representative_index, sector_index) in mapping { + if *representative_index >= expected_legs || *sector_index >= expected_legs { + return Err(RusticolError::invalid_argument( + "LC topology replay label permutation references an out-of-range external leg", + )); + } + if seen[*representative_index] { + return Err(RusticolError::invalid_argument( + "LC topology replay label permutation contains a duplicate representative label", + )); + } + seen[*representative_index] = true; + } + for point_index in 0..batch.point_count() { + let point = batch.point(point_index); + let mut mapped = (0..expected_legs) + .map(|external_index| { + point.momentum(external_index).ok_or_else(|| { + RusticolError::integrity( + "validated momentum view is missing an external leg during topology replay", + ) + }) + }) + .collect::>>()?; + for (representative_index, sector_index) in mapping { + mapped[*representative_index] = point.momentum(*sector_index).ok_or_else(|| { + RusticolError::integrity( + "validated momentum view is missing a permuted external leg", + ) + })?; + } + expanded_batch.push(mapped); + } + } + Ok(expanded_batch) +} + +/// Materialize one validated topology-replay permutation into reusable flat +/// row-major storage. +/// +/// This is the totals-only Direct-Arena crossing lane: it avoids both the +/// nested per-point vectors and the mapping-by-batch expansion used by the +/// resolved compatibility path. The caller owns and reuses `output`. +pub(crate) fn apply_lc_topology_label_permutation_from_view_into_flat( + batch: F64MomentumBatchView<'_>, + expected_legs: usize, + mapping: &[(usize, usize)], + output: &mut Vec, +) -> RusticolResult<()> { + if batch.external_count() != expected_legs { + return Err(RusticolError::invalid_argument(format!( + "LC topology replay input has {} external legs, expected {expected_legs}", + batch.external_count() + ))); + } + for (mapping_index, (representative_index, sector_index)) in mapping.iter().enumerate() { + if *representative_index >= expected_legs || *sector_index >= expected_legs { + return Err(RusticolError::invalid_argument( + "LC topology replay label permutation references an out-of-range external leg", + )); + } + if mapping[..mapping_index] + .iter() + .any(|(previous, _)| previous == representative_index) + { + return Err(RusticolError::invalid_argument( + "LC topology replay label permutation contains a duplicate representative label", + )); + } + } + let values_per_point = expected_legs + .checked_mul(4) + .ok_or_else(|| RusticolError::invalid_argument("LC topology replay batch overflows"))?; + let scalar_count = batch + .point_count() + .checked_mul(values_per_point) + .ok_or_else(|| RusticolError::invalid_argument("LC topology replay batch overflows"))?; + output.resize(scalar_count, 0.0); + for point_index in 0..batch.point_count() { + let point = batch.point(point_index); + let row_start = point_index * values_per_point; + for external_index in 0..expected_legs { + let momentum = point.momentum(external_index).ok_or_else(|| { + RusticolError::integrity( + "validated momentum view is missing an external leg during topology replay", + ) + })?; + output[row_start + external_index * 4..row_start + (external_index + 1) * 4] + .copy_from_slice(&momentum); + } + for (representative_index, sector_index) in mapping { + let momentum = point.momentum(*sector_index).ok_or_else(|| { + RusticolError::integrity( + "validated momentum view is missing a permuted external leg", + ) + })?; + let start = row_start + representative_index * 4; + output[start..start + 4].copy_from_slice(&momentum); + } + } + Ok(()) +} + +#[cfg(feature = "symbolica-runtime")] +pub(crate) fn apply_lc_topology_label_permutation_generic( + batch: &[Vec<[T; 4]>], + expected_legs: usize, + mapping: &[(usize, usize)], +) -> RusticolResult>> +where + T: RusticolHighPrecisionNumber, + Complex: Real + EvaluationDomain, +{ + let mut seen = vec![false; expected_legs]; + for (representative_index, sector_index) in mapping { + if *representative_index >= expected_legs || *sector_index >= expected_legs { + return Err(RusticolError::invalid_argument( + "LC topology replay label permutation references an out-of-range external leg", + )); + } + if seen[*representative_index] { + return Err(RusticolError::invalid_argument( + "LC topology replay label permutation contains a duplicate representative label", + )); + } + seen[*representative_index] = true; + } + let mut mapped_batch = Vec::with_capacity(batch.len()); + for point in batch { + if point.len() != expected_legs { + return Err(RusticolError::invalid_argument(format!( + "LC topology replay point has {} external legs, expected {expected_legs}", + point.len(), + ))); + } + let mut mapped = point.clone(); + for (representative_index, sector_index) in mapping { + mapped[*representative_index] = point[*sector_index].clone(); + } + mapped_batch.push(mapped); + } + Ok(mapped_batch) +} + +#[cfg(feature = "symbolica-runtime")] +pub(crate) fn apply_lc_topology_label_permutations_generic( + batch: &[Vec<[T; 4]>], + expected_legs: usize, + mappings: &[Vec<(usize, usize)>], +) -> RusticolResult>> +where + T: RusticolHighPrecisionNumber, + Complex: Real + EvaluationDomain, +{ + let mut expanded_batch = Vec::with_capacity(batch.len() * mappings.len()); + for mapping in mappings { + expanded_batch.extend(apply_lc_topology_label_permutation_generic( + batch, + expected_legs, + mapping, + )?); + } + Ok(expanded_batch) +} + +pub(crate) fn replay_mappings_per_expanded_batch(n_points: usize) -> usize { + if n_points == 0 { + return 1; + } + (MAX_LC_TOPOLOGY_REPLAY_EXPANDED_POINTS / n_points).max(1) +} + +#[cfg(feature = "symbolica-runtime")] +pub(crate) fn decimal_digits_to_bits(decimal_digits: u32) -> u32 { + (decimal_digits as f64 * std::f64::consts::LOG2_10).ceil() as u32 +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/evaluator/native_compiled_direct.rs b/pyamplicol/rust/crates/rusticol-core/src/evaluator/native_compiled_direct.rs new file mode 100644 index 0000000000..3eaedeaaa4 --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/evaluator/native_compiled_direct.rs @@ -0,0 +1,1363 @@ +// SPDX-License-Identifier: 0BSD + +//! Target-native DirectApplication adapter for one fused compiled leaf. +//! +//! C++ and inline-ASM compiled artifacts are both target-native dynamic +//! libraries, so they share this small C ABI. Unlike the historical +//! `params/buffer/out` entry point, the native function consumes fixed +//! split-complex plane descriptors and writes canonical output planes itself. +//! It therefore does not hide a dense-row gather/call/scatter wrapper. +//! +//! This module is a bounded ABI prototype and is intentionally not wired into +//! the production engine yet. + +#![allow(dead_code)] + +use std::path::Path; +use std::ptr; +use std::sync::Arc; + +use crate::artifact::PinnedNativeLibrary; +use crate::direct_arena::{ + AlignedF64Buffer, DirectArenaTrafficCounters, DirectArenaView, DirectFactorView, + DirectMomentumView, DirectParameterView, validate_direct_views, +}; +use crate::{RusticolError, RusticolResult}; + +pub(crate) const NATIVE_COMPILED_DIRECT_APPLICATION_ABI: &str = + "pyamplicol-native-compiled-direct-application-v1"; + +const NATIVE_COMPILED_DIRECT_ABI_VERSION: u32 = 1; +const NATIVE_COMPILED_DIRECT_FLAG_SPLIT_COMPLEX: u32 = 1 << 0; +const NATIVE_COMPILED_DIRECT_FLAG_COMPONENT_MAJOR: u32 = 1 << 1; +const NATIVE_COMPILED_DIRECT_FLAG_POINT_CONTIGUOUS: u32 = 1 << 2; +const NATIVE_COMPILED_DIRECT_FLAG_FACTOR_FREE_OVERWRITE: u32 = 1 << 3; +const NATIVE_COMPILED_DIRECT_FLAG_NO_OUTPUT_ALIAS: u32 = 1 << 4; +const NATIVE_COMPILED_DIRECT_FLAG_NOEXCEPT: u32 = 1 << 5; +const NATIVE_COMPILED_DIRECT_REQUIRED_FLAGS: u32 = NATIVE_COMPILED_DIRECT_FLAG_SPLIT_COMPLEX + | NATIVE_COMPILED_DIRECT_FLAG_COMPONENT_MAJOR + | NATIVE_COMPILED_DIRECT_FLAG_POINT_CONTIGUOUS + | NATIVE_COMPILED_DIRECT_FLAG_FACTOR_FREE_OVERWRITE + | NATIVE_COMPILED_DIRECT_FLAG_NO_OUTPUT_ALIAS + | NATIVE_COMPILED_DIRECT_FLAG_NOEXCEPT; +const NATIVE_COMPILED_DIRECT_KNOWN_FLAGS: u32 = NATIVE_COMPILED_DIRECT_REQUIRED_FLAGS; +const NATIVE_COMPILED_DIRECT_STATUS_OK: i32 = 0; + +#[repr(C)] +#[derive(Clone, Copy, Debug)] +struct NativeCompiledDirectMetadataV1 { + abi_version: u32, + struct_size: u32, + flags: u32, + input_plane_count: u32, + scalar_input_count: u32, + output_plane_count: u32, + simd_lane_width: u32, + reserved: u32, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +struct NativeCompiledDirectDescriptorCounts { + input_planes: u32, + scalar_inputs: u32, + output_planes: u32, +} + +impl NativeCompiledDirectDescriptorCounts { + fn checked_from_lengths( + input_planes: usize, + scalar_inputs: usize, + output_planes: usize, + ) -> RusticolResult { + Ok(Self { + input_planes: checked_descriptor_count(input_planes, "input-plane")?, + scalar_inputs: checked_descriptor_count(scalar_inputs, "scalar-input")?, + output_planes: checked_descriptor_count(output_planes, "output-plane")?, + }) + } +} + +#[repr(C)] +#[derive(Clone, Copy)] +struct NativeCompiledDirectInputPlaneV1 { + values: *const f64, +} + +#[repr(C)] +#[derive(Clone, Copy)] +struct NativeCompiledDirectOutputPlaneV1 { + values: *mut f64, +} + +#[repr(C)] +#[derive(Clone, Copy)] +struct NativeCompiledDirectScalarV1 { + value: *const f64, +} + +type NativeCompiledDirectMetadataFunction = + unsafe extern "C" fn() -> NativeCompiledDirectMetadataV1; +type NativeCompiledDirectEvaluateFunction = unsafe extern "C" fn( + *const NativeCompiledDirectInputPlaneV1, + u32, + *const NativeCompiledDirectScalarV1, + u32, + *const NativeCompiledDirectOutputPlaneV1, + u32, + u32, + u32, +) -> i32; + +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub(crate) enum NativeCompiledDirectArenaPlane { + Current { component: u32, imaginary: bool }, + Amplitude { component: u32, imaginary: bool }, +} + +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub(crate) enum NativeCompiledDirectPlaneBinding { + Arena(NativeCompiledDirectArenaPlane), + Momentum { form: u32, lorentz_component: u16 }, + Zero, +} + +#[derive(Clone, Copy, Debug, PartialEq)] +pub(crate) enum NativeCompiledDirectScalarBinding { + Parameter { index: u32, imaginary: bool }, + Literal(f64), +} + +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub(crate) struct NativeCompiledDirectOutputBinding(pub NativeCompiledDirectArenaPlane); + +/// Loaded native function plus its authenticated logical bindings. +pub(crate) struct LoadedNativeCompiledDirectStage { + _library: Arc, + evaluate: NativeCompiledDirectEvaluateFunction, + descriptor_counts: NativeCompiledDirectDescriptorCounts, + declared_lane_width: usize, + input_bindings: Box<[NativeCompiledDirectPlaneBinding]>, + scalar_bindings: Box<[NativeCompiledDirectScalarBinding]>, + output_bindings: Box<[NativeCompiledDirectOutputBinding]>, + display_path: Box, +} + +/// Fixed descriptors borrowing persistent Direct-Arena allocations. +pub(crate) struct BoundNativeCompiledDirectStage { + _library: Arc, + evaluate: NativeCompiledDirectEvaluateFunction, + inputs: Box<[NativeCompiledDirectInputPlaneV1]>, + scalars: Box<[NativeCompiledDirectScalarV1]>, + outputs: Box<[NativeCompiledDirectOutputPlaneV1]>, + _literal_values: Box<[f64]>, + _zero_plane: AlignedF64Buffer, + input_count: u32, + scalar_count: u32, + output_count: u32, + point_stride: u32, + declared_lane_width: usize, + display_path: Box, +} + +impl LoadedNativeCompiledDirectStage { + #[allow(clippy::too_many_arguments)] + pub(crate) fn load( + library: Arc, + function_name: &str, + application_abi: &str, + input_bindings: Vec, + scalar_bindings: Vec, + output_bindings: Vec, + ) -> RusticolResult { + if application_abi != NATIVE_COMPILED_DIRECT_APPLICATION_ABI { + return Err(RusticolError::compatibility(format!( + "unsupported native compiled DirectApplication ABI {application_abi:?}; expected \ + {NATIVE_COMPILED_DIRECT_APPLICATION_ABI:?}" + ))); + } + validate_c_symbol(function_name)?; + let descriptor_counts = NativeCompiledDirectDescriptorCounts::checked_from_lengths( + input_bindings.len(), + scalar_bindings.len(), + output_bindings.len(), + )?; + validate_static_bindings(&input_bindings, &output_bindings)?; + + let display_path = library.display_path().to_path_buf().into_boxed_path(); + let path = display_path.as_ref(); + let symbol_prefix = format!("{function_name}_direct_application_v1"); + let metadata_symbol = format!("{symbol_prefix}_metadata"); + let metadata_function = unsafe { + load_export::(&library, path, &metadata_symbol)? + }; + let evaluate = unsafe { + load_export::(&library, path, &symbol_prefix)? + }; + // SAFETY: the authenticated producer contract assigns this symbol the + // fixed metadata signature. Native exports must be `noexcept`; foreign + // unwinding across this C boundary is not recoverable by Rust. + let metadata = unsafe { metadata_function() }; + validate_metadata(metadata, descriptor_counts, path)?; + + Ok(Self { + _library: library, + evaluate, + descriptor_counts, + declared_lane_width: metadata.simd_lane_width as usize, + input_bindings: input_bindings.into_boxed_slice(), + scalar_bindings: scalar_bindings.into_boxed_slice(), + output_bindings: output_bindings.into_boxed_slice(), + display_path, + }) + } + + pub(crate) const fn declared_lane_width(&self) -> usize { + self.declared_lane_width + } + + /// Bind the native function to persistent plane and scalar storage. + /// + /// # Safety + /// + /// Every allocation referenced by `arena`, `momenta`, and `parameters` + /// must retain its address and declared length until the returned object + /// drops. During `evaluate`, the caller must provide exclusive access to + /// arena outputs and must not mutate any bound input or scalar storage. + pub(crate) unsafe fn bind( + self, + arena: DirectArenaView, + momenta: DirectMomentumView, + parameters: DirectParameterView, + ) -> RusticolResult { + // SAFETY: this entry point preserves the historical identity mapping + // and forwards the caller's complete fixed-storage contract. + unsafe { self.bind_with_current_map(arena, momenta, parameters, &[]) } + } + + /// Bind canonical current IDs through a cold, immutable physical map. + pub(crate) unsafe fn bind_with_current_map( + self, + arena: DirectArenaView, + momenta: DirectMomentumView, + parameters: DirectParameterView, + current_component_map: &[u32], + ) -> RusticolResult { + validate_direct_views( + arena, + momenta, + parameters, + DirectFactorView { + values_re: ptr::null(), + values_im: ptr::null(), + value_count: 0, + }, + )?; + validate_mapped_bindings( + &self.input_bindings, + &self.output_bindings, + current_component_map, + )?; + + let point_stride = arena.point_stride; + let zero_plane = + AlignedF64Buffer::zeroed(point_stride as usize, "native compiled zero plane")?; + let mut literal_values = Vec::new(); + literal_values + .try_reserve_exact(self.scalar_bindings.len()) + .map_err(|error| { + RusticolError::internal(format!( + "could not allocate native compiled literal storage: {error}" + )) + })?; + literal_values.resize(self.scalar_bindings.len(), 0.0); + let mut literal_values = literal_values.into_boxed_slice(); + for (index, binding) in self.scalar_bindings.iter().copied().enumerate() { + if let NativeCompiledDirectScalarBinding::Literal(value) = binding { + if !value.is_finite() { + return Err(RusticolError::integrity( + "native compiled DirectApplication literal must be finite", + )); + } + literal_values[index] = value; + } + } + + let inputs = self + .input_bindings + .iter() + .copied() + .map(|binding| { + resolve_input_plane(binding, arena, momenta, &zero_plane, current_component_map) + .map(|values| NativeCompiledDirectInputPlaneV1 { values }) + }) + .collect::>>()? + .into_boxed_slice(); + let outputs = self + .output_bindings + .iter() + .copied() + .map(|binding| { + resolve_arena_plane(binding.0, arena, current_component_map) + .map(|values| NativeCompiledDirectOutputPlaneV1 { values }) + }) + .collect::>>()? + .into_boxed_slice(); + let scalars = self + .scalar_bindings + .iter() + .copied() + .enumerate() + .map(|(literal_index, binding)| { + let value = match binding { + NativeCompiledDirectScalarBinding::Parameter { index, imaginary } => { + if index >= parameters.value_count { + return Err(RusticolError::integrity(format!( + "native compiled parameter binding {index} is out of bounds" + ))); + } + let base = if imaginary { + parameters.values_im + } else { + parameters.values_re + }; + // SAFETY: the parameter view was validated and `index` + // lies within its declared storage. + unsafe { base.add(index as usize) } + } + NativeCompiledDirectScalarBinding::Literal(_) => { + ptr::from_ref(&literal_values[literal_index]) + } + }; + Ok(NativeCompiledDirectScalarV1 { value }) + }) + .collect::>>()? + .into_boxed_slice(); + let descriptor_counts = NativeCompiledDirectDescriptorCounts::checked_from_lengths( + inputs.len(), + scalars.len(), + outputs.len(), + )?; + if descriptor_counts != self.descriptor_counts { + return Err(RusticolError::internal( + "native compiled descriptor counts changed while binding persistent storage", + )); + } + + Ok(BoundNativeCompiledDirectStage { + _library: self._library, + evaluate: self.evaluate, + inputs, + scalars, + outputs, + _literal_values: literal_values, + _zero_plane: zero_plane, + input_count: descriptor_counts.input_planes, + scalar_count: descriptor_counts.scalar_inputs, + output_count: descriptor_counts.output_planes, + point_stride, + declared_lane_width: self.declared_lane_width, + display_path: self.display_path, + }) + } +} + +impl BoundNativeCompiledDirectStage { + pub(crate) const fn declared_lane_width(&self) -> usize { + self.declared_lane_width + } + + /// Invoke the authenticated native function without allocating or packing. + pub(crate) fn evaluate(&self, point_start: u32, point_count: u32) -> RusticolResult<()> { + if point_count == 0 { + return Err(RusticolError::invalid_argument( + "native compiled DirectApplication point count must be positive", + )); + } + let point_stop = point_start.checked_add(point_count).ok_or_else(|| { + RusticolError::invalid_argument( + "native compiled DirectApplication point range overflows u32", + ) + })?; + if point_stop > self.point_stride { + return Err(RusticolError::invalid_argument(format!( + "native compiled DirectApplication point range {point_start}..{point_stop} \ + exceeds physical stride {}", + self.point_stride + ))); + } + + // SAFETY: load and bind authenticated the function signature and fixed + // descriptors. The producer contract requires `noexcept` and status + // returns; Rust cannot contain foreign unwinding across this C ABI. + let status = unsafe { + (self.evaluate)( + self.inputs.as_ptr(), + self.input_count, + self.scalars.as_ptr(), + self.scalar_count, + self.outputs.as_ptr(), + self.output_count, + point_start, + point_count, + ) + }; + if status != NATIVE_COMPILED_DIRECT_STATUS_OK { + return Err(RusticolError::evaluation(format!( + "native compiled DirectApplication {} returned status {status}", + self.display_path.display() + ))); + } + Ok(()) + } + + /// Profile adapter-side traffic without adding work to the unprofiled path. + /// + /// These counters describe Rust-side materialization around the call. They + /// cannot observe allocation or copying hidden inside a native producer. + pub(crate) fn evaluate_profiled( + &self, + point_start: u32, + point_count: u32, + traffic: &mut DirectArenaTrafficCounters, + ) -> RusticolResult<()> { + self.evaluate(point_start, point_count)?; + traffic.record_call(1, point_count); + traffic.validate_direct() + } +} + +fn validate_metadata( + metadata: NativeCompiledDirectMetadataV1, + descriptor_counts: NativeCompiledDirectDescriptorCounts, + path: &Path, +) -> RusticolResult<()> { + if metadata.abi_version != NATIVE_COMPILED_DIRECT_ABI_VERSION + || metadata.struct_size as usize != std::mem::size_of::() + || metadata.reserved != 0 + { + return Err(RusticolError::compatibility(format!( + "native compiled DirectApplication {} has incompatible v1 metadata", + path.display() + ))); + } + if metadata.flags & NATIVE_COMPILED_DIRECT_REQUIRED_FLAGS + != NATIVE_COMPILED_DIRECT_REQUIRED_FLAGS + || metadata.flags & !NATIVE_COMPILED_DIRECT_KNOWN_FLAGS != 0 + { + return Err(RusticolError::compatibility(format!( + "native compiled DirectApplication {} does not certify split-plane, \ + point-contiguous, factor-free overwrite, no-alias and noexcept semantics", + path.display() + ))); + } + for (actual, expected, label) in [ + ( + metadata.input_plane_count, + descriptor_counts.input_planes, + "input plane", + ), + ( + metadata.scalar_input_count, + descriptor_counts.scalar_inputs, + "scalar input", + ), + ( + metadata.output_plane_count, + descriptor_counts.output_planes, + "output plane", + ), + ] { + if actual != expected { + return Err(RusticolError::integrity(format!( + "native compiled DirectApplication {} {label} count {actual} does not match \ + binding count {expected}", + path.display() + ))); + } + } + if metadata.simd_lane_width == 0 || !metadata.simd_lane_width.is_power_of_two() { + return Err(RusticolError::compatibility(format!( + "native compiled DirectApplication {} has invalid declared lane width {}", + path.display(), + metadata.simd_lane_width + ))); + } + Ok(()) +} + +fn checked_descriptor_count(count: usize, label: &str) -> RusticolResult { + u32::try_from(count).map_err(|_| { + RusticolError::integrity(format!( + "native compiled DirectApplication {label} count {count} exceeds u32" + )) + }) +} + +fn validate_static_bindings( + inputs: &[NativeCompiledDirectPlaneBinding], + outputs: &[NativeCompiledDirectOutputBinding], +) -> RusticolResult<()> { + if outputs.is_empty() { + return Err(RusticolError::integrity( + "native compiled DirectApplication must expose an output", + )); + } + for (index, output) in outputs.iter().copied().enumerate() { + if outputs[..index].contains(&output) { + return Err(RusticolError::integrity(format!( + "native compiled output binding {index} aliases an earlier output" + ))); + } + if inputs.contains(&NativeCompiledDirectPlaneBinding::Arena(output.0)) { + return Err(RusticolError::integrity(format!( + "native compiled output binding {index} aliases an input plane" + ))); + } + } + Ok(()) +} + +fn validate_mapped_bindings( + inputs: &[NativeCompiledDirectPlaneBinding], + outputs: &[NativeCompiledDirectOutputBinding], + current_component_map: &[u32], +) -> RusticolResult<()> { + let mapped_inputs = inputs + .iter() + .copied() + .filter_map(|binding| match binding { + NativeCompiledDirectPlaneBinding::Arena(arena) => { + Some(map_arena_plane(arena, current_component_map)) + } + NativeCompiledDirectPlaneBinding::Momentum { .. } + | NativeCompiledDirectPlaneBinding::Zero => None, + }) + .collect::>>()?; + let mut mapped_outputs = Vec::with_capacity(outputs.len()); + for (index, output) in outputs.iter().copied().enumerate() { + let mapped = map_arena_plane(output.0, current_component_map)?; + if mapped_outputs.contains(&mapped) { + return Err(RusticolError::integrity(format!( + "native compiled mapped output binding {index} aliases an earlier output" + ))); + } + if mapped_inputs.contains(&mapped) { + return Err(RusticolError::integrity(format!( + "native compiled mapped output binding {index} aliases an input plane" + ))); + } + mapped_outputs.push(mapped); + } + Ok(()) +} + +fn map_arena_plane( + binding: NativeCompiledDirectArenaPlane, + current_component_map: &[u32], +) -> RusticolResult { + match binding { + NativeCompiledDirectArenaPlane::Current { + component, + imaginary, + } if !current_component_map.is_empty() => { + let physical = current_component_map + .get(component as usize) + .copied() + .filter(|physical| *physical != u32::MAX) + .ok_or_else(|| { + RusticolError::integrity(format!( + "native compiled current binding {component} has no physical mapping" + )) + })?; + Ok(NativeCompiledDirectArenaPlane::Current { + component: physical, + imaginary, + }) + } + _ => Ok(binding), + } +} + +fn resolve_input_plane( + binding: NativeCompiledDirectPlaneBinding, + arena: DirectArenaView, + momenta: DirectMomentumView, + zero_plane: &AlignedF64Buffer, + current_component_map: &[u32], +) -> RusticolResult<*const f64> { + match binding { + NativeCompiledDirectPlaneBinding::Arena(binding) => { + Ok(resolve_arena_plane(binding, arena, current_component_map)?.cast_const()) + } + NativeCompiledDirectPlaneBinding::Momentum { + form, + lorentz_component, + } => { + if form >= momenta.form_count || lorentz_component >= momenta.lorentz_component_count { + return Err(RusticolError::integrity(format!( + "native compiled momentum binding ({form}, {lorentz_component}) is out of \ + bounds" + ))); + } + let component = form + .checked_mul(u32::from(momenta.lorentz_component_count)) + .and_then(|base| base.checked_add(u32::from(lorentz_component))) + .ok_or_else(|| { + RusticolError::integrity("native compiled momentum component overflows u32") + })?; + let offset = usize::try_from(component) + .ok() + .and_then(|component| component.checked_mul(momenta.point_stride as usize)) + .ok_or_else(|| { + RusticolError::integrity("native compiled momentum offset overflows usize") + })?; + // SAFETY: view and component bounds were validated. + Ok(unsafe { momenta.values.add(offset) }) + } + NativeCompiledDirectPlaneBinding::Zero => Ok(zero_plane.as_ptr()), + } +} + +fn resolve_arena_plane( + binding: NativeCompiledDirectArenaPlane, + arena: DirectArenaView, + current_component_map: &[u32], +) -> RusticolResult<*mut f64> { + let binding = map_arena_plane(binding, current_component_map)?; + let (base, shape, label) = match binding { + NativeCompiledDirectArenaPlane::Current { + component, + imaginary, + } => ( + if imaginary { + arena.current_im + } else { + arena.current_re + }, + arena.current_shape()?, + ("current", component), + ), + NativeCompiledDirectArenaPlane::Amplitude { + component, + imaginary, + } => ( + if imaginary { + arena.amplitude_im + } else { + arena.amplitude_re + }, + arena.amplitude_shape()?, + ("amplitude", component), + ), + }; + let offset = shape.checked_scalar_index(label.1, 0, label.0)?; + // SAFETY: the checked component proves a complete pitched plane exists. + Ok(unsafe { base.add(offset) }) +} + +fn validate_c_symbol(symbol: &str) -> RusticolResult<()> { + let mut bytes = symbol.bytes(); + let Some(first) = bytes.next() else { + return Err(RusticolError::invalid_argument( + "native compiled DirectApplication function name must not be empty", + )); + }; + if !(first == b'_' || first.is_ascii_alphabetic()) + || bytes.any(|byte| !(byte == b'_' || byte.is_ascii_alphanumeric())) + { + return Err(RusticolError::invalid_argument(format!( + "native compiled DirectApplication function name {symbol:?} is not a portable C \ + identifier" + ))); + } + Ok(()) +} + +unsafe fn load_export( + library: &libloading::Library, + path: &Path, + exported_symbol: &str, +) -> RusticolResult { + // SAFETY: the producer contract assigns each fixed symbol the exact v1 + // signature selected by the caller. + unsafe { + library + .get::(exported_symbol.as_bytes()) + .map(|symbol| *symbol) + .map_err(|error| { + RusticolError::evaluation(format!( + "could not load native compiled DirectApplication symbol \ + {exported_symbol:?} from {}: {error}", + path.display() + )) + }) + } +} + +#[cfg(all(test, any(target_os = "linux", target_os = "macos")))] +mod tests { + use super::super::CompiledComplexF64Evaluator; + #[cfg(not(feature = "f64-symjit"))] + use super::super::native_direct::tests::count_allocations; + #[cfg(feature = "f64-symjit")] + use super::super::symjit_direct::tests::count_allocations; + use super::*; + use crate::RusticolErrorKind; + use crate::direct_arena::{DirectArenaWorkspace, DirectMomentumView}; + #[cfg(not(feature = "symbolica-runtime"))] + use num_complex::Complex; + use std::fs; + use std::process::Command; + use std::sync::atomic::{AtomicU64, Ordering}; + use std::time::{Instant, SystemTime, UNIX_EPOCH}; + #[cfg(feature = "symbolica-runtime")] + use symbolica::prelude::Complex; + + const POINT_COUNT: usize = 129; + const REQUIRED_FLAGS: u32 = NATIVE_COMPILED_DIRECT_REQUIRED_FLAGS; + static FIXTURE_SEQUENCE: AtomicU64 = AtomicU64::new(0); + + fn native_fixture() -> (std::path::PathBuf, std::path::PathBuf) { + let unique = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + let directory = std::env::temp_dir().join(format!( + "rusticol-native-compiled-direct-test-{}-{unique}-{}", + std::process::id(), + FIXTURE_SEQUENCE.fetch_add(1, Ordering::Relaxed), + )); + fs::create_dir_all(&directory).unwrap(); + let source = directory.join("fixture.cpp"); + let library = directory.join(if cfg!(target_os = "macos") { + "libfixture.dylib" + } else { + "libfixture.so" + }); + fs::write( + &source, + format!( + r#"#include +#include +#include + +struct Metadata {{ + std::uint32_t abi_version; + std::uint32_t struct_size; + std::uint32_t flags; + std::uint32_t input_plane_count; + std::uint32_t scalar_input_count; + std::uint32_t output_plane_count; + std::uint32_t simd_lane_width; + std::uint32_t reserved; +}}; +struct InputPlane {{ const double* values; }}; +struct OutputPlane {{ double* values; }}; +struct Scalar {{ const double* value; }}; + +extern "C" unsigned long rusticol_native_leaf_complexf64_get_buffer_len() {{ + return 1ul; +}} +extern "C" void rusticol_native_leaf_complexf64( + const std::complex* params, + std::complex* buffer, + std::complex* out) {{ + buffer[0] = params[0] * params[1]; + out[0] = buffer[0] + params[2]; + out[1] = params[0] + params[1]; +}} + +extern "C" Metadata +rusticol_native_leaf_direct_application_v1_metadata() noexcept {{ + return Metadata{{1u, sizeof(Metadata), {REQUIRED_FLAGS}u, 4u, 2u, 4u, 2u, 0u}}; +}} + +// This is a synthetic scalar plane-loop fixture. It addresses planes directly, +// never calls the dense function, and never constructs a params/buffer/out row. +// The declared width of 2 exercises metadata plumbing; it is not proof that +// this fixture contains SIMD or production compressed-O3 code. +extern "C" int rusticol_native_leaf_direct_application_v1( + const InputPlane* inputs, + std::uint32_t input_count, + const Scalar* scalars, + std::uint32_t scalar_count, + const OutputPlane* outputs, + std::uint32_t output_count, + std::uint32_t point_start, + std::uint32_t point_count) noexcept {{ + if (inputs == nullptr || outputs == nullptr || scalars == nullptr || + input_count != 4u || scalar_count != 2u || output_count != 4u || + point_count == 0u) return 2; + for (std::uint32_t offset = 0; offset < point_count; ++offset) {{ + const std::uint32_t point = point_start + offset; + const double x_re = inputs[0].values[point]; + const double x_im = inputs[1].values[point]; + const double y_re = inputs[2].values[point]; + const double y_im = inputs[3].values[point]; + const double product_re = x_re * y_re - x_im * y_im; + const double product_im = x_re * y_im + x_im * y_re; + outputs[0].values[point] = product_re + *scalars[0].value; + outputs[1].values[point] = product_im + *scalars[1].value; + outputs[2].values[point] = x_re + y_re; + outputs[3].values[point] = x_im + y_im; + }} + return 0; +}} + +extern "C" Metadata +rusticol_status_leaf_direct_application_v1_metadata() noexcept {{ + return Metadata{{1u, sizeof(Metadata), {REQUIRED_FLAGS}u, 4u, 2u, 4u, 1u, 0u}}; +}} +extern "C" int rusticol_status_leaf_direct_application_v1( + const InputPlane*, std::uint32_t, const Scalar*, std::uint32_t, + const OutputPlane*, std::uint32_t, std::uint32_t, std::uint32_t) noexcept {{ + return 17; +}} +"# + ), + ) + .unwrap(); + let compiler = std::env::var("CXX").unwrap_or_else(|_| "c++".to_string()); + let mut command = Command::new(compiler); + command.args(["-std=c++17", "-O3"]); + if cfg!(target_os = "macos") { + command.arg("-dynamiclib"); + } else { + command.args(["-shared", "-fPIC"]); + } + let output = command + .arg(&source) + .arg("-o") + .arg(&library) + .output() + .unwrap(); + assert!( + output.status.success(), + "could not compile native direct fixture: {}", + String::from_utf8_lossy(&output.stderr) + ); + (directory, library) + } + + fn input_bindings() -> Vec { + vec![ + NativeCompiledDirectPlaneBinding::Arena(NativeCompiledDirectArenaPlane::Current { + component: 0, + imaginary: false, + }), + NativeCompiledDirectPlaneBinding::Arena(NativeCompiledDirectArenaPlane::Current { + component: 0, + imaginary: true, + }), + NativeCompiledDirectPlaneBinding::Arena(NativeCompiledDirectArenaPlane::Current { + component: 1, + imaginary: false, + }), + NativeCompiledDirectPlaneBinding::Arena(NativeCompiledDirectArenaPlane::Current { + component: 1, + imaginary: true, + }), + ] + } + + fn scalar_bindings() -> Vec { + vec![ + NativeCompiledDirectScalarBinding::Parameter { + index: 0, + imaginary: false, + }, + NativeCompiledDirectScalarBinding::Parameter { + index: 0, + imaginary: true, + }, + ] + } + + fn output_bindings() -> Vec { + vec![ + NativeCompiledDirectOutputBinding(NativeCompiledDirectArenaPlane::Current { + component: 2, + imaginary: false, + }), + NativeCompiledDirectOutputBinding(NativeCompiledDirectArenaPlane::Current { + component: 2, + imaginary: true, + }), + NativeCompiledDirectOutputBinding(NativeCompiledDirectArenaPlane::Current { + component: 3, + imaginary: false, + }), + NativeCompiledDirectOutputBinding(NativeCompiledDirectArenaPlane::Current { + component: 3, + imaginary: true, + }), + ] + } + + fn empty_momenta(point_stride: u32) -> AlignedF64Buffer { + AlignedF64Buffer::zeroed(4 * point_stride as usize, "native compiled test momenta").unwrap() + } + + fn momentum_view(values: &AlignedF64Buffer, point_stride: u32) -> DirectMomentumView { + DirectMomentumView { + values: values.as_ptr(), + scalar_len: values.len() as u64, + form_count: 1, + lorentz_component_count: 4, + point_stride, + } + } + + fn fill_inputs(workspace: &mut DirectArenaWorkspace, point_count: usize) { + let stride = workspace.point_stride() as usize; + let (current_re, current_im, _, _) = workspace.split_slices_mut(); + for point in 0..point_count { + current_re[point] = 0.25 + point as f64 * 0.03125; + current_im[point] = if point % 2 == 0 { -0.0 } else { 0.5 }; + current_re[stride + point] = 1.5 - point as f64 * 0.0078125; + current_im[stride + point] = if point % 3 == 0 { 0.0 } else { -0.25 }; + } + } + + fn load_and_bind( + library: &Path, + function_name: &str, + workspace: &mut DirectArenaWorkspace, + momenta: &AlignedF64Buffer, + parameter_re: &[f64; 1], + parameter_im: &[f64; 1], + ) -> RusticolResult { + let loaded = LoadedNativeCompiledDirectStage::load( + PinnedNativeLibrary::from_test_path(library)?, + function_name, + NATIVE_COMPILED_DIRECT_APPLICATION_ABI, + input_bindings(), + scalar_bindings(), + output_bindings(), + )?; + let arena = workspace.view()?; + let parameters = DirectParameterView { + values_re: parameter_re.as_ptr(), + values_im: parameter_im.as_ptr(), + value_count: 1, + }; + unsafe { + loaded.bind( + arena, + momentum_view(momenta, arena.point_stride), + parameters, + ) + } + } + + #[test] + fn synthetic_plane_loop_preserves_odd_tails_and_allocates_zero_when_warm() { + let (directory, library) = native_fixture(); + let mut workspace = DirectArenaWorkspace::new(4, 0, POINT_COUNT as u32).unwrap(); + workspace.begin_tile(POINT_COUNT as u32).unwrap(); + fill_inputs(&mut workspace, POINT_COUNT); + let stride = workspace.point_stride() as usize; + let momenta = empty_momenta(stride as u32); + let parameter_re = [0.375]; + let parameter_im = [-0.625]; + let bound = load_and_bind( + &library, + "rusticol_native_leaf", + &mut workspace, + &momenta, + ¶meter_re, + ¶meter_im, + ) + .unwrap(); + assert_eq!(bound.declared_lane_width(), 2); + + for (point_start, point_count) in [ + (0_u32, 1_u32), + (0, 2), + (0, 3), + (0, 127), + (1, 127), + (0, 128), + (0, 129), + ] { + { + let (current_re, current_im, _, _) = workspace.split_slices_mut(); + current_re[2 * stride..4 * stride].fill(f64::NAN); + current_im[2 * stride..4 * stride].fill(f64::NAN); + } + bound.evaluate(point_start, point_count).unwrap(); + let point_stop = (point_start + point_count) as usize; + let (current_re, current_im) = workspace.current_slices(); + for point in point_start as usize..point_stop { + let x = Complex::new(current_re[point], current_im[point]); + let y = Complex::new(current_re[stride + point], current_im[stride + point]); + let expected_product = x * y + Complex::new(parameter_re[0], parameter_im[0]); + let expected_sum = x + y; + assert_eq!( + current_re[2 * stride + point].to_bits(), + expected_product.re.to_bits() + ); + assert_eq!( + current_im[2 * stride + point].to_bits(), + expected_product.im.to_bits() + ); + assert_eq!( + current_re[3 * stride + point].to_bits(), + expected_sum.re.to_bits() + ); + assert_eq!( + current_im[3 * stride + point].to_bits(), + expected_sum.im.to_bits() + ); + } + for point in (0..point_start as usize).chain(point_stop..stride) { + assert!(current_re[2 * stride + point].is_nan()); + assert!(current_im[2 * stride + point].is_nan()); + assert!(current_re[3 * stride + point].is_nan()); + assert!(current_im[3 * stride + point].is_nan()); + } + } + + bound.evaluate(0, POINT_COUNT as u32).unwrap(); + let (result, allocation_count, allocated_bytes) = + count_allocations(|| bound.evaluate(0, POINT_COUNT as u32)); + result.unwrap(); + assert_eq!( + allocation_count, 0, + "warmed native direct call allocated in Rust" + ); + assert_eq!( + allocated_bytes, 0, + "warmed native direct call allocated bytes in Rust" + ); + // This accounts only for Rust adapter-side traffic. The fixture source + // is inspected separately to establish that its native loop also + // contains no dense-row wrapper. + let mut traffic = DirectArenaTrafficCounters::default(); + bound + .evaluate_profiled(0, POINT_COUNT as u32, &mut traffic) + .unwrap(); + assert_eq!(traffic.calls, 1); + assert_eq!(traffic.rows, 1); + assert_eq!(traffic.points, POINT_COUNT as u64); + assert_eq!(traffic.packet_input_bytes, 0); + assert_eq!(traffic.packet_output_bytes, 0); + assert_eq!(traffic.gather_bytes, 0); + assert_eq!(traffic.scatter_bytes, 0); + assert_eq!(traffic.remap_bytes, 0); + fs::remove_dir_all(directory).unwrap(); + } + + #[test] + fn synthetic_plane_loop_matches_existing_dense_gather_call_scatter() { + let (directory, library) = native_fixture(); + let mut workspace = DirectArenaWorkspace::new(4, 0, POINT_COUNT as u32).unwrap(); + workspace.begin_tile(POINT_COUNT as u32).unwrap(); + fill_inputs(&mut workspace, POINT_COUNT); + let stride = workspace.point_stride() as usize; + let momenta = empty_momenta(stride as u32); + let parameter_re = [0.375]; + let parameter_im = [-0.625]; + + let mut dense = CompiledComplexF64Evaluator::load( + PinnedNativeLibrary::from_test_path(&library).unwrap(), + "rusticol_native_leaf", + 3, + 2, + ) + .unwrap(); + let mut packed_inputs = vec![Complex::new(0.0, 0.0); POINT_COUNT * 3]; + let mut packed_outputs = vec![Complex::new(0.0, 0.0); POINT_COUNT * 2]; + { + let (current_re, current_im) = workspace.current_slices(); + for point in 0..POINT_COUNT { + packed_inputs[3 * point] = Complex::new(current_re[point], current_im[point]); + packed_inputs[3 * point + 1] = + Complex::new(current_re[stride + point], current_im[stride + point]); + packed_inputs[3 * point + 2] = Complex::new(parameter_re[0], parameter_im[0]); + } + } + dense + .evaluate_batch(POINT_COUNT, &packed_inputs, &mut packed_outputs) + .unwrap(); + let bound = load_and_bind( + &library, + "rusticol_native_leaf", + &mut workspace, + &momenta, + ¶meter_re, + ¶meter_im, + ) + .unwrap(); + bound.evaluate(0, POINT_COUNT as u32).unwrap(); + + let (current_re, current_im) = workspace.current_slices(); + for point in 0..POINT_COUNT { + for (component, expected) in [ + (2, packed_outputs[2 * point]), + (3, packed_outputs[2 * point + 1]), + ] { + assert_eq!( + current_re[component * stride + point].to_bits(), + expected.re.to_bits() + ); + assert_eq!( + current_im[component * stride + point].to_bits(), + expected.im.to_bits() + ); + } + } + fs::remove_dir_all(directory).unwrap(); + } + + #[test] + fn mapped_native_bindings_reject_a_physical_input_output_alias() { + let error = validate_mapped_bindings(&input_bindings(), &output_bindings(), &[0, 1, 0, 1]) + .unwrap_err(); + assert_eq!(error.kind(), RusticolErrorKind::Integrity); + assert!(error.message().contains("mapped output")); + assert!(error.message().contains("aliases an input plane")); + } + + #[test] + fn aliases_bounds_and_native_status_fail_closed() { + let (directory, library) = native_fixture(); + let mut aliased_outputs = output_bindings(); + aliased_outputs[0] = + NativeCompiledDirectOutputBinding(NativeCompiledDirectArenaPlane::Current { + component: 0, + imaginary: false, + }); + let alias_error = match LoadedNativeCompiledDirectStage::load( + PinnedNativeLibrary::from_test_path(&library).unwrap(), + "rusticol_native_leaf", + NATIVE_COMPILED_DIRECT_APPLICATION_ABI, + input_bindings(), + scalar_bindings(), + aliased_outputs, + ) { + Ok(_) => panic!("input/output alias unexpectedly loaded"), + Err(error) => error, + }; + assert_eq!(alias_error.kind(), RusticolErrorKind::Integrity); + assert!(alias_error.message().contains("aliases an input plane")); + + let mut workspace = DirectArenaWorkspace::new(4, 0, POINT_COUNT as u32).unwrap(); + workspace.begin_tile(POINT_COUNT as u32).unwrap(); + fill_inputs(&mut workspace, POINT_COUNT); + let stride = workspace.point_stride(); + let momenta = empty_momenta(stride); + let parameter_re = [0.375]; + let parameter_im = [-0.625]; + let bound = load_and_bind( + &library, + "rusticol_native_leaf", + &mut workspace, + &momenta, + ¶meter_re, + ¶meter_im, + ) + .unwrap(); + assert_eq!( + bound.evaluate(stride - 1, 2).unwrap_err().kind(), + RusticolErrorKind::InvalidArgument + ); + assert_eq!( + bound.evaluate(0, 0).unwrap_err().kind(), + RusticolErrorKind::InvalidArgument + ); + + let status_bound = load_and_bind( + &library, + "rusticol_status_leaf", + &mut workspace, + &momenta, + ¶meter_re, + ¶meter_im, + ) + .unwrap(); + let error = status_bound.evaluate(0, 1).unwrap_err(); + assert_eq!(error.kind(), RusticolErrorKind::Evaluation); + assert!(error.message().contains("status 17")); + fs::remove_dir_all(directory).unwrap(); + } + + #[test] + fn descriptor_counts_fail_before_any_truncating_ffi_conversion() { + let maximum = NativeCompiledDirectDescriptorCounts::checked_from_lengths( + u32::MAX as usize, + u32::MAX as usize, + u32::MAX as usize, + ) + .unwrap(); + assert_eq!(maximum.input_planes, u32::MAX); + assert_eq!(maximum.scalar_inputs, u32::MAX); + assert_eq!(maximum.output_planes, u32::MAX); + + if usize::BITS > u32::BITS { + let too_many = usize::try_from(u64::from(u32::MAX) + 1).unwrap(); + for error in [ + NativeCompiledDirectDescriptorCounts::checked_from_lengths(too_many, 0, 1) + .unwrap_err(), + NativeCompiledDirectDescriptorCounts::checked_from_lengths(0, too_many, 1) + .unwrap_err(), + NativeCompiledDirectDescriptorCounts::checked_from_lengths(0, 0, too_many) + .unwrap_err(), + ] { + assert_eq!(error.kind(), RusticolErrorKind::Integrity); + assert!(error.message().contains("exceeds u32")); + } + } + } + + fn gather_call_scatter( + dense: &mut CompiledComplexF64Evaluator, + workspace: &mut DirectArenaWorkspace, + parameter: Complex, + packed_inputs: &mut [Complex], + packed_outputs: &mut [Complex], + ) { + let stride = workspace.point_stride() as usize; + { + let (current_re, current_im) = workspace.current_slices(); + for point in 0..POINT_COUNT { + packed_inputs[3 * point] = Complex::new(current_re[point], current_im[point]); + packed_inputs[3 * point + 1] = + Complex::new(current_re[stride + point], current_im[stride + point]); + packed_inputs[3 * point + 2] = parameter; + } + } + dense + .evaluate_batch(POINT_COUNT, packed_inputs, packed_outputs) + .unwrap(); + let (current_re, current_im, _, _) = workspace.split_slices_mut(); + for point in 0..POINT_COUNT { + let product = packed_outputs[2 * point]; + let sum = packed_outputs[2 * point + 1]; + current_re[2 * stride + point] = product.re; + current_im[2 * stride + point] = product.im; + current_re[3 * stride + point] = sum.re; + current_im[3 * stride + point] = sum.im; + } + } + + fn median(values: &mut [u128]) -> u128 { + values.sort_unstable(); + values[values.len() / 2] + } + + fn median_absolute_deviation(values: &[u128], center: u128) -> u128 { + let mut deviations = values + .iter() + .map(|value| value.abs_diff(center)) + .collect::>(); + median(&mut deviations) + } + + #[test] + #[ignore = "manual interleaved native DirectApplication benchmark"] + fn benchmark_synthetic_native_plane_loop_against_gather_call_scatter() { + const SAMPLES: usize = 9; + const REPEATS: usize = 10_000; + + let (directory, library) = native_fixture(); + let mut workspace = DirectArenaWorkspace::new(4, 0, POINT_COUNT as u32).unwrap(); + workspace.begin_tile(POINT_COUNT as u32).unwrap(); + fill_inputs(&mut workspace, POINT_COUNT); + let stride = workspace.point_stride(); + let momenta = empty_momenta(stride); + let parameter_re = [0.375]; + let parameter_im = [-0.625]; + let parameter = Complex::new(parameter_re[0], parameter_im[0]); + let bound = load_and_bind( + &library, + "rusticol_native_leaf", + &mut workspace, + &momenta, + ¶meter_re, + ¶meter_im, + ) + .unwrap(); + let mut dense = CompiledComplexF64Evaluator::load( + PinnedNativeLibrary::from_test_path(&library).unwrap(), + "rusticol_native_leaf", + 3, + 2, + ) + .unwrap(); + let mut packed_inputs = vec![Complex::new(0.0, 0.0); POINT_COUNT * 3]; + let mut packed_outputs = vec![Complex::new(0.0, 0.0); POINT_COUNT * 2]; + + bound.evaluate(0, POINT_COUNT as u32).unwrap(); + gather_call_scatter( + &mut dense, + &mut workspace, + parameter, + &mut packed_inputs, + &mut packed_outputs, + ); + let (_, direct_allocations, direct_bytes) = + count_allocations(|| bound.evaluate(0, POINT_COUNT as u32)); + let (_, packed_allocations, packed_bytes) = count_allocations(|| { + gather_call_scatter( + &mut dense, + &mut workspace, + parameter, + &mut packed_inputs, + &mut packed_outputs, + ) + }); + + let mut direct_samples = [0_u128; SAMPLES]; + let mut packed_samples = [0_u128; SAMPLES]; + for sample in 0..SAMPLES { + let time_direct = || { + let started = Instant::now(); + for _ in 0..REPEATS { + bound.evaluate(0, POINT_COUNT as u32).unwrap(); + } + started.elapsed().as_nanos() / REPEATS as u128 + }; + let mut time_packed = || { + let started = Instant::now(); + for _ in 0..REPEATS { + gather_call_scatter( + &mut dense, + &mut workspace, + parameter, + &mut packed_inputs, + &mut packed_outputs, + ); + } + started.elapsed().as_nanos() / REPEATS as u128 + }; + if sample.is_multiple_of(2) { + direct_samples[sample] = time_direct(); + packed_samples[sample] = time_packed(); + } else { + packed_samples[sample] = time_packed(); + direct_samples[sample] = time_direct(); + } + std::hint::black_box(workspace.current_slices().0[2 * stride as usize]); + } + let direct_ordered = direct_samples; + let packed_ordered = packed_samples; + let direct_median = median(&mut direct_samples); + let packed_median = median(&mut packed_samples); + let direct_mad = median_absolute_deviation(&direct_ordered, direct_median); + let packed_mad = median_absolute_deviation(&packed_ordered, packed_median); + eprintln!( + "synthetic-native-compiled-plane-loop points={POINT_COUNT} samples={SAMPLES} \ + repeats={REPEATS} \ + direct_ns={direct_ordered:?} packed_ns={packed_ordered:?} \ + direct_median_ns={direct_median} direct_mad_ns={direct_mad} \ + packed_median_ns={packed_median} packed_mad_ns={packed_mad} \ + speedup={:.6} direct_rust_allocations={direct_allocations} \ + direct_rust_allocated_bytes={direct_bytes} \ + packed_rust_allocations={packed_allocations} \ + packed_rust_allocated_bytes={packed_bytes}", + packed_median as f64 / direct_median as f64, + ); + fs::remove_dir_all(directory).unwrap(); + } +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/evaluator/native_direct.rs b/pyamplicol/rust/crates/rusticol-core/src/evaluator/native_direct.rs new file mode 100644 index 0000000000..ff7e78f9bd --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/evaluator/native_direct.rs @@ -0,0 +1,680 @@ +// SPDX-License-Identifier: 0BSD + +//! Native dynamic-library adapter for Direct-Arena recurrence executors. +//! +//! This module only owns and authenticates native prepared callables. Source +//! filling and intrinsic closure reduction remain Rusticol-owned. The returned +//! handle calls directly into the loaded library and therefore performs no +//! packing, scattering, or allocation. Callers must retain this owner for as +//! long as a copied [`DirectExecutorHandle`] can be invoked. + +use crate::artifact::PinnedNativeLibrary; +use crate::recurrence::direct_backend::{ + DirectClosureExecutor, DirectContributionExecutor, DirectExecutorHandle, + DirectFinalizationExecutor, +}; +use crate::recurrence::{DIRECT_NONE_U32, DirectExecutorRole}; +use crate::{RusticolError, RusticolResult}; +use std::path::Path; +use std::ptr; +use std::sync::Arc; + +const NATIVE_DIRECT_SYMBOL_PREFIX: &str = "pyamplicol_recurrence_direct"; + +#[repr(C)] +struct NativeDirectBindingContextV1 { + coupling_re: f64, + coupling_im: f64, +} + +/// Owns one loaded native Direct-Arena executor and its dynamic library. +/// +/// Moving this value is safe because the copied function pointer addresses the +/// loaded library rather than this Rust object. Dropping it invalidates any +/// copied handle, so runtime integration must retain the owner beside the +/// `DirectExecutorCatalog`. +pub(crate) struct LoadedNativeDirectExecutor { + _library: Arc, + _binding_context: Option>, + handle: DirectExecutorHandle, + #[cfg(test)] + role: DirectExecutorRole, + #[cfg(test)] + prepared_kernel_id: u32, +} + +impl LoadedNativeDirectExecutor { + /// Load one authenticated role-specific native Direct-Arena export. + /// + /// `exported_symbol` is carried by the prepared-kernel producer contract. + /// It must exactly equal [`native_direct_symbol_name`] for the requested + /// role and prepared-kernel ID. This makes role confusion fail before the + /// dynamic library is opened. + pub(crate) fn load( + library: Arc, + role: DirectExecutorRole, + prepared_kernel_id: u32, + exported_symbol: &str, + binding_coupling: Option<(f64, f64)>, + ) -> RusticolResult { + validate_c_symbol(exported_symbol)?; + let expected_symbol = native_direct_symbol_name(role, prepared_kernel_id)?; + if exported_symbol != expected_symbol { + return Err(RusticolError::integrity(format!( + "native Direct-Arena symbol {exported_symbol:?} does not authenticate role \ + {role:?} and prepared kernel {prepared_kernel_id}; expected \ + {expected_symbol:?}" + ))); + } + if binding_coupling.is_some_and(|(re, im)| !re.is_finite() || !im.is_finite()) { + return Err(RusticolError::integrity( + "native Direct-Arena binding coupling must be finite", + )); + } + + let path = library.display_path(); + let binding_context = binding_coupling.map(|(coupling_re, coupling_im)| { + Box::new(NativeDirectBindingContextV1 { + coupling_re, + coupling_im, + }) + }); + let context = binding_context + .as_deref() + .map_or(ptr::null(), |value| std::ptr::from_ref(value).cast()); + let handle = match role { + DirectExecutorRole::Contribution => { + let call = unsafe { + load_export::(&library, path, exported_symbol)? + }; + DirectExecutorHandle::Contribution { call, context } + } + DirectExecutorRole::Finalization => { + let call = unsafe { + load_export::(&library, path, exported_symbol)? + }; + DirectExecutorHandle::Finalization { call, context } + } + DirectExecutorRole::Closure => { + let call = unsafe { + load_export::(&library, path, exported_symbol)? + }; + DirectExecutorHandle::Closure { call, context } + } + DirectExecutorRole::Source => { + return Err(RusticolError::invalid_argument( + "native Direct-Arena libraries cannot provide source executors; source filling is Rusticol-owned", + )); + } + }; + + Ok(Self { + _library: library, + _binding_context: binding_context, + handle, + #[cfg(test)] + role, + #[cfg(test)] + prepared_kernel_id, + }) + } + + /// Return the direct callable. The owner must outlive every invocation. + pub(crate) const fn handle(&self) -> DirectExecutorHandle { + self.handle + } + + #[cfg(test)] + pub(crate) const fn role(&self) -> DirectExecutorRole { + self.role + } + + #[cfg(test)] + pub(crate) const fn prepared_kernel_id(&self) -> u32 { + self.prepared_kernel_id + } +} + +/// Deterministic C export for one prepared Direct-Arena kernel. +/// +/// The convention is +/// `pyamplicol_recurrence_direct__k<8-lowercase-hex-id>_v1`. +pub(crate) fn native_direct_symbol_name( + role: DirectExecutorRole, + prepared_kernel_id: u32, +) -> RusticolResult { + if prepared_kernel_id == DIRECT_NONE_U32 { + return Err(RusticolError::invalid_argument( + "native Direct-Arena prepared-kernel ID must not be the missing-ID sentinel", + )); + } + let role_name = match role { + DirectExecutorRole::Contribution => "contribution", + DirectExecutorRole::Finalization => "finalization", + DirectExecutorRole::Closure => "closure", + DirectExecutorRole::Source => { + return Err(RusticolError::invalid_argument( + "native Direct-Arena symbol names are not defined for Rusticol-owned source executors", + )); + } + }; + Ok(format!( + "{NATIVE_DIRECT_SYMBOL_PREFIX}_{role_name}_k{prepared_kernel_id:08x}_v1" + )) +} + +fn validate_c_symbol(symbol: &str) -> RusticolResult<()> { + let mut bytes = symbol.bytes(); + let Some(first) = bytes.next() else { + return Err(RusticolError::invalid_argument( + "native Direct-Arena exported symbol must not be empty", + )); + }; + if !(first == b'_' || first.is_ascii_alphabetic()) + || bytes.any(|byte| !(byte == b'_' || byte.is_ascii_alphanumeric())) + { + return Err(RusticolError::invalid_argument(format!( + "native Direct-Arena exported symbol {symbol:?} is not a portable C identifier" + ))); + } + Ok(()) +} + +unsafe fn load_export( + library: &libloading::Library, + path: &Path, + exported_symbol: &str, +) -> RusticolResult { + // SAFETY: The producer contract assigns this authenticated role-specific + // symbol exactly the `T` function signature selected by the caller. + unsafe { + library + .get::(exported_symbol.as_bytes()) + .map(|symbol| *symbol) + .map_err(|error| { + RusticolError::evaluation(format!( + "could not load native Direct-Arena symbol {exported_symbol:?} from {}: {error}", + path.display() + )) + }) + } +} + +#[cfg(all(test, any(target_os = "linux", target_os = "macos")))] +pub(crate) mod tests { + use super::*; + use crate::RusticolErrorKind; + use crate::artifact::EvaluatorPayloadStore; + use crate::recurrence::direct_backend::{ + DirectArenaView, DirectFactorView, DirectMomentumView, DirectParameterView, + }; + #[cfg(not(feature = "f64-symjit"))] + use std::alloc::{GlobalAlloc, Layout, System}; + #[cfg(not(feature = "f64-symjit"))] + use std::cell::Cell; + use std::fs; + use std::process::Command; + use std::time::{SystemTime, UNIX_EPOCH}; + + #[cfg(not(feature = "f64-symjit"))] + thread_local! { + static TRACK_ALLOCATIONS: Cell = const { Cell::new(false) }; + static ALLOCATION_COUNT: Cell = const { Cell::new(0) }; + static ALLOCATED_BYTES: Cell = const { Cell::new(0) }; + } + + #[cfg(not(feature = "f64-symjit"))] + struct CountingAllocator; + + #[cfg(not(feature = "f64-symjit"))] + #[global_allocator] + static GLOBAL_ALLOCATOR: CountingAllocator = CountingAllocator; + + #[cfg(not(feature = "f64-symjit"))] + unsafe impl GlobalAlloc for CountingAllocator { + unsafe fn alloc(&self, layout: Layout) -> *mut u8 { + count_allocation(layout.size()); + unsafe { System.alloc(layout) } + } + + unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 { + count_allocation(layout.size()); + unsafe { System.alloc_zeroed(layout) } + } + + unsafe fn realloc(&self, pointer: *mut u8, layout: Layout, new_size: usize) -> *mut u8 { + count_allocation(new_size); + unsafe { System.realloc(pointer, layout, new_size) } + } + + unsafe fn dealloc(&self, pointer: *mut u8, layout: Layout) { + unsafe { System.dealloc(pointer, layout) } + } + } + + #[cfg(not(feature = "f64-symjit"))] + fn count_allocation(bytes: usize) { + let tracking = TRACK_ALLOCATIONS.try_with(Cell::get).unwrap_or(false); + if tracking { + let _ = ALLOCATION_COUNT.try_with(|count| count.set(count.get() + 1)); + let _ = ALLOCATED_BYTES.try_with(|total| total.set(total.get().saturating_add(bytes))); + } + } + + #[cfg(not(feature = "f64-symjit"))] + pub(crate) fn count_allocations(operation: impl FnOnce() -> T) -> (T, usize, usize) { + ALLOCATION_COUNT.with(|count| count.set(0)); + ALLOCATED_BYTES.with(|total| total.set(0)); + TRACK_ALLOCATIONS.with(|tracking| tracking.set(true)); + let result = operation(); + TRACK_ALLOCATIONS.with(|tracking| tracking.set(false)); + let count = ALLOCATION_COUNT.with(Cell::get); + let bytes = ALLOCATED_BYTES.with(Cell::get); + (result, count, bytes) + } + + const CONTRIBUTION_ID: u32 = 7; + const FINALIZATION_ID: u32 = 11; + const CLOSURE_ID: u32 = 13; + + fn expect_load_error(result: RusticolResult) -> RusticolError { + match result { + Ok(_) => panic!("native Direct-Arena executor unexpectedly loaded"), + Err(error) => error, + } + } + + fn native_fixture() -> (std::path::PathBuf, std::path::PathBuf) { + let unique = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + let directory = std::env::temp_dir().join(format!( + "rusticol-native-direct-test-{}-{unique}", + std::process::id() + )); + fs::create_dir_all(&directory).unwrap(); + let source = directory.join("fixture.c"); + let library = directory.join(if cfg!(target_os = "macos") { + "libnative_direct_fixture.dylib" + } else { + "libnative_direct_fixture.so" + }); + let contribution_symbol = + native_direct_symbol_name(DirectExecutorRole::Contribution, CONTRIBUTION_ID).unwrap(); + let finalization_symbol = + native_direct_symbol_name(DirectExecutorRole::Finalization, FINALIZATION_ID).unwrap(); + let closure_symbol = + native_direct_symbol_name(DirectExecutorRole::Closure, CLOSURE_ID).unwrap(); + let fixture = r#"#include + +typedef struct { + double *current_re; + double *current_im; + uint64_t current_scalar_len; + double *amplitude_re; + double *amplitude_im; + uint64_t amplitude_scalar_len; + uint32_t point_stride; +} DirectArenaView; + +typedef struct { + const double *values; + uint64_t scalar_len; + uint32_t form_count; + uint16_t lorentz_component_count; + uint32_t point_stride; +} DirectMomentumView; + +typedef struct { + const double *values_re; + const double *values_im; + uint32_t value_count; +} DirectParameterView; + +typedef DirectParameterView DirectFactorView; + +typedef struct { + double coupling_re; + double coupling_im; +} DirectNativeBindingContextV1; + +#define DIRECT_ARGS \ + const void *context, \ + DirectArenaView arena, \ + DirectMomentumView momenta, \ + DirectParameterView parameters, \ + DirectFactorView factors, \ + const void *rows, \ + uint32_t row_count, \ + uint32_t point_count + +int __CONTRIBUTION_SYMBOL__(DIRECT_ARGS) { + (void)arena; (void)momenta; (void)parameters; (void)factors; (void)rows; + int binding_offset = 0; + if (context != 0) { + const DirectNativeBindingContextV1 *binding = + (const DirectNativeBindingContextV1 *)context; + binding_offset = + (int)(100.0 * binding->coupling_re + 10.0 * binding->coupling_im); + } + return 1000 + binding_offset + (int)row_count + (int)point_count; +} + +int __FINALIZATION_SYMBOL__(DIRECT_ARGS) { + (void)context; (void)arena; (void)momenta; (void)parameters; (void)factors; (void)rows; + return 2000 + (int)row_count + (int)point_count; +} + +int __CLOSURE_SYMBOL__(DIRECT_ARGS) { + (void)context; (void)arena; (void)momenta; (void)parameters; (void)factors; (void)rows; + return 3000 + (int)row_count + (int)point_count; +} +"# + .replace("__CONTRIBUTION_SYMBOL__", &contribution_symbol) + .replace("__FINALIZATION_SYMBOL__", &finalization_symbol) + .replace("__CLOSURE_SYMBOL__", &closure_symbol); + fs::write(&source, fixture).unwrap(); + let compiler = std::env::var("CC").unwrap_or_else(|_| "cc".to_string()); + let mut command = Command::new(compiler); + if cfg!(target_os = "macos") { + command.arg("-dynamiclib"); + } else { + command.args(["-shared", "-fPIC"]); + } + let output = command + .arg(&source) + .arg("-o") + .arg(&library) + .output() + .unwrap(); + assert!( + output.status.success(), + "could not compile native Direct-Arena fixture: {}", + String::from_utf8_lossy(&output.stderr) + ); + (directory, library) + } + + fn pinned(path: &Path) -> Arc { + PinnedNativeLibrary::from_test_path(path).unwrap() + } + + fn empty_views() -> ( + DirectArenaView, + DirectMomentumView, + DirectParameterView, + DirectFactorView, + ) { + ( + DirectArenaView { + current_re: ptr::null_mut(), + current_im: ptr::null_mut(), + current_scalar_len: 0, + amplitude_re: ptr::null_mut(), + amplitude_im: ptr::null_mut(), + amplitude_scalar_len: 0, + point_stride: 0, + }, + DirectMomentumView { + values: ptr::null(), + scalar_len: 0, + form_count: 0, + lorentz_component_count: 0, + point_stride: 0, + }, + DirectParameterView { + values_re: ptr::null(), + values_im: ptr::null(), + value_count: 0, + }, + DirectFactorView { + values_re: ptr::null(), + values_im: ptr::null(), + value_count: 0, + }, + ) + } + + #[test] + fn deterministic_symbols_encode_role_and_prepared_kernel_id() { + assert_eq!( + native_direct_symbol_name(DirectExecutorRole::Contribution, CONTRIBUTION_ID,).unwrap(), + "pyamplicol_recurrence_direct_contribution_k00000007_v1" + ); + assert_eq!( + native_direct_symbol_name(DirectExecutorRole::Finalization, FINALIZATION_ID,).unwrap(), + "pyamplicol_recurrence_direct_finalization_k0000000b_v1" + ); + assert_eq!( + native_direct_symbol_name(DirectExecutorRole::Closure, CLOSURE_ID).unwrap(), + "pyamplicol_recurrence_direct_closure_k0000000d_v1" + ); + assert_eq!( + native_direct_symbol_name(DirectExecutorRole::Source, 0) + .unwrap_err() + .kind(), + RusticolErrorKind::InvalidArgument + ); + assert_eq!( + native_direct_symbol_name(DirectExecutorRole::Contribution, DIRECT_NONE_U32,) + .unwrap_err() + .kind(), + RusticolErrorKind::InvalidArgument + ); + } + + #[test] + fn symbol_authentication_rejects_empty_invalid_and_wrong_role_names() { + let (directory, library_path) = native_fixture(); + let library = pinned(&library_path); + for symbol in ["", "not-a-c-symbol", "contains\0nul"] { + let error = expect_load_error(LoadedNativeDirectExecutor::load( + Arc::clone(&library), + DirectExecutorRole::Contribution, + CONTRIBUTION_ID, + symbol, + None, + )); + assert_eq!(error.kind(), RusticolErrorKind::InvalidArgument); + } + + let contribution_symbol = + native_direct_symbol_name(DirectExecutorRole::Contribution, CONTRIBUTION_ID).unwrap(); + let error = expect_load_error(LoadedNativeDirectExecutor::load( + Arc::clone(&library), + DirectExecutorRole::Closure, + CONTRIBUTION_ID, + &contribution_symbol, + None, + )); + assert_eq!(error.kind(), RusticolErrorKind::Integrity); + assert!(error.message().contains("expected")); + + let error = expect_load_error(LoadedNativeDirectExecutor::load( + Arc::clone(&library), + DirectExecutorRole::Contribution, + CONTRIBUTION_ID, + &contribution_symbol, + Some((f64::NAN, 0.0)), + )); + assert_eq!(error.kind(), RusticolErrorKind::Integrity); + + let error = expect_load_error(LoadedNativeDirectExecutor::load( + library, + DirectExecutorRole::Source, + 0, + "pyamplicol_recurrence_direct_source_k00000000_v1", + None, + )); + assert_eq!(error.kind(), RusticolErrorKind::InvalidArgument); + fs::remove_dir_all(directory).unwrap(); + } + + #[test] + fn native_role_exports_load_and_remain_callable_while_owners_move() { + let (directory, library_path) = native_fixture(); + let store = EvaluatorPayloadStore::directory(&directory); + let relative_library = library_path.file_name().unwrap().to_str().unwrap(); + let library = store.load_native_library(relative_library).unwrap(); + let cached_library = store.load_native_library(relative_library).unwrap(); + assert!(Arc::ptr_eq(&library, &cached_library)); + let specifications = [ + (DirectExecutorRole::Contribution, CONTRIBUTION_ID, 1008), + (DirectExecutorRole::Finalization, FINALIZATION_ID, 2008), + (DirectExecutorRole::Closure, CLOSURE_ID, 3008), + ]; + let mut owners = Vec::new(); + let mut handles = Vec::new(); + for (role, prepared_kernel_id, _) in specifications { + let symbol = native_direct_symbol_name(role, prepared_kernel_id).unwrap(); + let owner = LoadedNativeDirectExecutor::load( + Arc::clone(&library), + role, + prepared_kernel_id, + &symbol, + None, + ) + .unwrap(); + assert_eq!(owner.role(), role); + assert_eq!(owner.prepared_kernel_id(), prepared_kernel_id); + handles.push(owner.handle()); + owners.push(owner); + } + + fs::write(&library_path, b"replaced-after-authenticated-load").unwrap(); + + let (arena, momenta, parameters, factors) = empty_views(); + for ((role, _, expected), handle) in specifications.into_iter().zip(handles.iter().copied()) + { + let status = unsafe { + match (role, handle) { + ( + DirectExecutorRole::Contribution, + DirectExecutorHandle::Contribution { call, context }, + ) => call( + context, + arena, + momenta, + parameters, + factors, + ptr::null(), + 3, + 5, + ), + ( + DirectExecutorRole::Finalization, + DirectExecutorHandle::Finalization { call, context }, + ) => call( + context, + arena, + momenta, + parameters, + factors, + ptr::null(), + 3, + 5, + ), + ( + DirectExecutorRole::Closure, + DirectExecutorHandle::Closure { call, context }, + ) => call( + context, + arena, + momenta, + parameters, + factors, + ptr::null(), + 3, + 5, + ), + _ => panic!("native Direct-Arena handle has the wrong role"), + } + }; + assert_eq!(status, expected); + } + + let contribution_symbol = + native_direct_symbol_name(DirectExecutorRole::Contribution, CONTRIBUTION_ID).unwrap(); + let first_context = LoadedNativeDirectExecutor::load( + Arc::clone(&library), + DirectExecutorRole::Contribution, + CONTRIBUTION_ID, + &contribution_symbol, + Some((2.0, 3.0)), + ) + .unwrap(); + let first_handle = first_context.handle(); + let second_context = LoadedNativeDirectExecutor::load( + Arc::clone(&library), + DirectExecutorRole::Contribution, + CONTRIBUTION_ID, + &contribution_symbol, + Some((-1.0, 4.0)), + ) + .unwrap(); + let second_handle = second_context.handle(); + owners.push(first_context); + owners.push(second_context); + for (handle, expected) in [(first_handle, 1_238), (second_handle, 948)] { + let DirectExecutorHandle::Contribution { call, context } = handle else { + panic!("context fixture must load a contribution"); + }; + let status = unsafe { + call( + context, + arena, + momenta, + parameters, + factors, + ptr::null(), + 3, + 5, + ) + }; + assert_eq!(status, expected); + } + + #[cfg(not(feature = "f64-symjit"))] + { + let DirectExecutorHandle::Contribution { call, context } = handles[0] else { + panic!("first native Direct-Arena handle must be a contribution"); + }; + let (status_sum, allocation_count, allocated_bytes) = count_allocations(|| { + let mut status_sum = 0; + for _ in 0..1_024 { + status_sum += unsafe { + call( + context, + arena, + momenta, + parameters, + factors, + ptr::null(), + 3, + 5, + ) + }; + } + status_sum + }); + assert_eq!(status_sum, 1_024 * 1_008); + assert_eq!(allocation_count, 0, "warmed native direct calls allocated"); + assert_eq!(allocated_bytes, 0, "warmed native direct calls allocated"); + } + + let missing_symbol = + native_direct_symbol_name(DirectExecutorRole::Contribution, 23).unwrap(); + let error = expect_load_error(LoadedNativeDirectExecutor::load( + library, + DirectExecutorRole::Contribution, + 23, + &missing_symbol, + None, + )); + assert_eq!(error.kind(), RusticolErrorKind::Evaluation); + + drop(owners); + fs::remove_dir_all(directory).unwrap(); + } +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/evaluator/recurrence_closure_direct.rs b/pyamplicol/rust/crates/rusticol-core/src/evaluator/recurrence_closure_direct.rs new file mode 100644 index 0000000000..7d75feac8f --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/evaluator/recurrence_closure_direct.rs @@ -0,0 +1,125 @@ +// SPDX-License-Identifier: 0BSD + +//! Allocation-free component-metric closures for Direct-Arena recurrence. + +use crate::recurrence::DirectClosureRow; +use crate::recurrence::direct_backend::{ + DIRECT_STATUS_OK, DirectArenaView, DirectFactorView, DirectMomentumView, DirectParameterView, +}; +use std::ffi::{c_int, c_void}; + +const STATUS_INVALID_ARGUMENT: c_int = 2; +const STATUS_BOUNDS: c_int = 4; + +/// Contract two split-complex currents directly into amplitude planes. +/// +/// Coefficient planes are stored in the authenticated plan factor catalog. +/// The point loop is innermost so each current and destination access remains +/// contiguous and auto-vectorizable. +pub(crate) unsafe extern "C" fn execute_closure_reduce_rows( + _context: *const c_void, + arena: DirectArenaView, + _momenta: DirectMomentumView, + _parameters: DirectParameterView, + factors: DirectFactorView, + rows: *const DirectClosureRow, + row_count: u32, + point_count: u32, +) -> c_int { + if point_count == 0 + || point_count > arena.point_stride + || (row_count != 0 && rows.is_null()) + || arena.current_re.is_null() + || arena.current_im.is_null() + || arena.amplitude_re.is_null() + || arena.amplitude_im.is_null() + || factors.values_re.is_null() + || factors.values_im.is_null() + { + return STATUS_INVALID_ARGUMENT; + } + let rows = unsafe { std::slice::from_raw_parts(rows, row_count as usize) }; + let stride = arena.point_stride as usize; + let points = point_count as usize; + + for row in rows { + if row.parent1_component_base_or_sentinel == u32::MAX + || row.component_count == 0 + || row.exact_factor_id >= factors.value_count + { + return STATUS_INVALID_ARGUMENT; + } + let coefficient_end = match row + .component_factor_start + .checked_add(u32::from(row.component_count)) + { + Some(end) if end <= factors.value_count => end, + _ => return STATUS_BOUNDS, + }; + debug_assert!(coefficient_end <= factors.value_count); + + let row_factor_re = unsafe { *factors.values_re.add(row.exact_factor_id as usize) }; + let row_factor_im = unsafe { *factors.values_im.add(row.exact_factor_id as usize) }; + let destination_base = match (row.amplitude_destination_id as usize).checked_mul(stride) { + Some(value) => value, + None => return STATUS_BOUNDS, + }; + if destination_base + .checked_add(points) + .is_none_or(|end| end > arena.amplitude_scalar_len as usize) + { + return STATUS_BOUNDS; + } + + for component in 0..u32::from(row.component_count) { + let coefficient_id = row.component_factor_start + component; + let coefficient_re = unsafe { *factors.values_re.add(coefficient_id as usize) }; + let coefficient_im = unsafe { *factors.values_im.add(coefficient_id as usize) }; + let scale_re = coefficient_re * row_factor_re - coefficient_im * row_factor_im; + let scale_im = coefficient_re * row_factor_im + coefficient_im * row_factor_re; + + let left_plane = match (row.parent0_component_base as usize) + .checked_add(component as usize) + .and_then(|plane| plane.checked_mul(stride)) + { + Some(value) => value, + None => return STATUS_BOUNDS, + }; + let right_plane = match (row.parent1_component_base_or_sentinel as usize) + .checked_add(component as usize) + .and_then(|plane| plane.checked_mul(stride)) + { + Some(value) => value, + None => return STATUS_BOUNDS, + }; + if left_plane + .checked_add(points) + .is_none_or(|end| end > arena.current_scalar_len as usize) + || right_plane + .checked_add(points) + .is_none_or(|end| end > arena.current_scalar_len as usize) + { + return STATUS_BOUNDS; + } + + for point in 0..points { + let left = left_plane + point; + let right = right_plane + point; + let destination = destination_base + point; + let left_re = unsafe { *arena.current_re.add(left) }; + let left_im = unsafe { *arena.current_im.add(left) }; + let right_re = unsafe { *arena.current_re.add(right) }; + let right_im = unsafe { *arena.current_im.add(right) }; + let product_re = left_re * right_re - left_im * right_im; + let product_im = left_re * right_im + left_im * right_re; + unsafe { + *arena.amplitude_re.add(destination) += + product_re * scale_re - product_im * scale_im; + *arena.amplitude_im.add(destination) += + product_re * scale_im + product_im * scale_re; + } + } + } + } + DIRECT_STATUS_OK +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/evaluator/recurrence_intrinsic_direct.rs b/pyamplicol/rust/crates/rusticol-core/src/evaluator/recurrence_intrinsic_direct.rs new file mode 100644 index 0000000000..248dad2453 --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/evaluator/recurrence_intrinsic_direct.rs @@ -0,0 +1,5004 @@ +// SPDX-License-Identifier: 0BSD + +//! Allocation-free model-generic contribution intrinsics for Direct-Arena recurrence. +//! +//! Every callback consumes authenticated fixed-width contribution rows and +//! reads/writes persistent split-complex current planes directly. The loader +//! owns the immutable scale context and retains it for at least as long as the +//! returned executor handle can be called. + +use crate::recurrence::direct_backend::{ + DIRECT_STATUS_OK, DirectArenaView, DirectContributionExecutor, DirectContributionFanoutBatch, + DirectContributionFanoutExecutor, DirectContributionFanoutExecutorHandle, + DirectContributionFanoutScale, DirectExecutorHandle, DirectFactorView, + DirectFinalizationExecutor, DirectInteractionBundleBatch, DirectInteractionExecutorContexts, + DirectInteractionIntrinsicKind, DirectInteractionOperand, DirectInteractionOperands, + DirectInteractionRuntimeRequirements, DirectInteractionTerminalBatch, + DirectInteractionVectorParent, DirectMomentumView, DirectParameterView, +}; +use crate::recurrence::{ + DIRECT_CONTRIBUTION_FLAG_INITIALIZE_DESTINATION, DIRECT_NONE_U32, DirectContributionRow, + DirectFinalizationRow, +}; +use crate::{RusticolError, RusticolResult}; +use std::ffi::{c_int, c_void}; +use std::ptr; +use wide::f64x2; + +const STATUS_INVALID_CONTEXT: c_int = 1; +const STATUS_INVALID_ARGUMENT: c_int = 2; +const STATUS_BOUNDS: c_int = 4; + +pub(crate) const WEYL_VECTOR_TO_WEYL_POSITIVE_TEMPLATE: &str = + "rusticol.recurrence-intrinsic.weyl-vector-to-weyl-a.v1"; +pub(crate) const WEYL_VECTOR_TO_WEYL_NEGATIVE_TEMPLATE: &str = + "rusticol.recurrence-intrinsic.weyl-vector-to-weyl-b.v1"; +pub(crate) const ANTISYMMETRIC_TENSOR_VECTOR_TO_VECTOR_TEMPLATE: &str = + "rusticol.recurrence-intrinsic.antisymmetric-tensor-vector.v1"; +pub(crate) const VECTOR_WEDGE_VECTOR_TO_ANTISYMMETRIC_TENSOR_TEMPLATE: &str = + "rusticol.recurrence-intrinsic.vector-wedge-vector.v1"; +pub(crate) const COLOR_ORDERED_THREE_VECTOR_TEMPLATE: &str = + "rusticol.recurrence-intrinsic.color-ordered-three-vector.v1"; +pub(crate) const WEYL_PROPAGATOR_POSITIVE_TEMPLATE: &str = + "rusticol.recurrence-intrinsic.weyl-propagator-a.v1"; +pub(crate) const WEYL_PROPAGATOR_NEGATIVE_TEMPLATE: &str = + "rusticol.recurrence-intrinsic.weyl-propagator-b.v1"; +pub(crate) const FEYNMAN_VECTOR_PROPAGATOR_TEMPLATE: &str = + "rusticol.recurrence-intrinsic.vector-propagator-feynman.v1"; + +/// Exact algebra class certified by the model compiler. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) enum RecurrenceContributionIntrinsicKind { + WeylVectorToWeylPositive, + WeylVectorToWeylNegative, + AntisymmetricTensorVectorToVector, + VectorWedgeVectorToAntisymmetricTensor, + ColorOrderedThreeVector, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) enum RecurrenceFinalizationIntrinsicKind { + WeylPropagatorPositive, + WeylPropagatorNegative, + FeynmanVectorPropagator, +} + +impl RecurrenceFinalizationIntrinsicKind { + pub(crate) fn from_runtime_template(value: &str) -> RusticolResult { + match value { + WEYL_PROPAGATOR_POSITIVE_TEMPLATE => Ok(Self::WeylPropagatorPositive), + WEYL_PROPAGATOR_NEGATIVE_TEMPLATE => Ok(Self::WeylPropagatorNegative), + FEYNMAN_VECTOR_PROPAGATOR_TEMPLATE => Ok(Self::FeynmanVectorPropagator), + other => Err(RusticolError::compatibility(format!( + "unsupported Direct-Arena recurrence finalization intrinsic {other:?}" + ))), + } + } +} + +impl RecurrenceContributionIntrinsicKind { + #[cfg(test)] + pub(crate) const fn runtime_template(self) -> &'static str { + match self { + Self::WeylVectorToWeylPositive => WEYL_VECTOR_TO_WEYL_POSITIVE_TEMPLATE, + Self::WeylVectorToWeylNegative => WEYL_VECTOR_TO_WEYL_NEGATIVE_TEMPLATE, + Self::AntisymmetricTensorVectorToVector => { + ANTISYMMETRIC_TENSOR_VECTOR_TO_VECTOR_TEMPLATE + } + Self::VectorWedgeVectorToAntisymmetricTensor => { + VECTOR_WEDGE_VECTOR_TO_ANTISYMMETRIC_TENSOR_TEMPLATE + } + Self::ColorOrderedThreeVector => COLOR_ORDERED_THREE_VECTOR_TEMPLATE, + } + } + + pub(crate) fn from_runtime_template(value: &str) -> RusticolResult { + match value { + WEYL_VECTOR_TO_WEYL_POSITIVE_TEMPLATE => Ok(Self::WeylVectorToWeylPositive), + WEYL_VECTOR_TO_WEYL_NEGATIVE_TEMPLATE => Ok(Self::WeylVectorToWeylNegative), + ANTISYMMETRIC_TENSOR_VECTOR_TO_VECTOR_TEMPLATE => { + Ok(Self::AntisymmetricTensorVectorToVector) + } + VECTOR_WEDGE_VECTOR_TO_ANTISYMMETRIC_TENSOR_TEMPLATE => { + Ok(Self::VectorWedgeVectorToAntisymmetricTensor) + } + COLOR_ORDERED_THREE_VECTOR_TEMPLATE => Ok(Self::ColorOrderedThreeVector), + other => Err(RusticolError::compatibility(format!( + "unsupported Direct-Arena recurrence contribution intrinsic {other:?}" + ))), + } + } +} + +/// Immutable scale applied after the certified intrinsic algebra. +/// +/// The effective scale is `literal * parameters[model_parameter_index]` when +/// a parameter index is present, or just `literal` otherwise. Every row then +/// contributes `effective_scale * exact_factor[row.exact_factor_id] * base`. +#[derive(Clone, Copy, Debug, PartialEq)] +pub(crate) struct RecurrenceIntrinsicScale { + literal_re: f64, + literal_im: f64, + model_parameter_index: Option, +} + +impl RecurrenceIntrinsicScale { + pub(crate) fn new( + literal_re: f64, + literal_im: f64, + model_parameter_index: Option, + ) -> RusticolResult { + if !literal_re.is_finite() || !literal_im.is_finite() { + return Err(RusticolError::invalid_argument( + "Direct-Arena recurrence intrinsic scale must be finite", + )); + } + if model_parameter_index == Some(DIRECT_NONE_U32) { + return Err(RusticolError::invalid_argument( + "Direct-Arena recurrence intrinsic parameter index uses the reserved sentinel", + )); + } + Ok(Self { + literal_re, + literal_im, + model_parameter_index, + }) + } + + #[cfg(test)] + pub(crate) fn unit() -> Self { + Self { + literal_re: 1.0, + literal_im: 0.0, + model_parameter_index: None, + } + } +} + +/// A context-aware contribution handle whose owner must outlive every call. +#[derive(Clone, Copy)] +pub(crate) struct ContextDirectContributionExecutorHandle { + pub(crate) call: DirectContributionExecutor, + pub(crate) context: *const c_void, +} + +/// Owns one immutable intrinsic scale context. +pub(crate) struct LoadedRecurrenceIntrinsicDirectExecutor { + kind: RecurrenceContributionIntrinsicKind, + scale: Box, +} + +impl LoadedRecurrenceIntrinsicDirectExecutor { + pub(crate) fn load( + kind: RecurrenceContributionIntrinsicKind, + scale: RecurrenceIntrinsicScale, + ) -> Self { + Self { + kind, + scale: Box::new(scale), + } + } + + pub(crate) fn load_runtime_template( + runtime_template: &str, + scale: RecurrenceIntrinsicScale, + ) -> RusticolResult { + Ok(Self::load( + RecurrenceContributionIntrinsicKind::from_runtime_template(runtime_template)?, + scale, + )) + } + + pub(crate) fn contribution_handle(&self) -> ContextDirectContributionExecutorHandle { + let call = match self.kind { + RecurrenceContributionIntrinsicKind::WeylVectorToWeylPositive => { + execute_weyl_vector_to_weyl_positive_rows + } + RecurrenceContributionIntrinsicKind::WeylVectorToWeylNegative => { + execute_weyl_vector_to_weyl_negative_rows + } + RecurrenceContributionIntrinsicKind::AntisymmetricTensorVectorToVector => { + execute_antisymmetric_tensor_vector_to_vector_rows + } + RecurrenceContributionIntrinsicKind::VectorWedgeVectorToAntisymmetricTensor => { + execute_vector_wedge_vector_to_antisymmetric_tensor_rows + } + RecurrenceContributionIntrinsicKind::ColorOrderedThreeVector => { + execute_color_ordered_three_vector_rows + } + }; + ContextDirectContributionExecutorHandle { + call, + context: ptr::from_ref(self.scale.as_ref()).cast(), + } + } + + pub(crate) fn contribution_fanout_handle( + &self, + ) -> Option { + let ( + call, + destination_component_count, + parent_component_counts, + requires_two_momenta, + interaction_kind, + ): ( + DirectContributionFanoutExecutor, + u32, + [u32; 2], + bool, + DirectInteractionIntrinsicKind, + ) = match self.kind { + RecurrenceContributionIntrinsicKind::AntisymmetricTensorVectorToVector => ( + execute_antisymmetric_tensor_vector_to_vector_fanout, + 4, + [6, 4], + false, + DirectInteractionIntrinsicKind::AntisymmetricTensorVector, + ), + RecurrenceContributionIntrinsicKind::VectorWedgeVectorToAntisymmetricTensor => ( + execute_vector_wedge_vector_to_antisymmetric_tensor_fanout, + 6, + [4, 4], + false, + DirectInteractionIntrinsicKind::VectorWedgeVector, + ), + RecurrenceContributionIntrinsicKind::ColorOrderedThreeVector => ( + execute_color_ordered_three_vector_fanout, + 4, + [4, 4], + true, + DirectInteractionIntrinsicKind::ColorOrderedThreeVector, + ), + _ => return None, + }; + Some(DirectContributionFanoutExecutorHandle { + call, + context: ptr::from_ref(self.scale.as_ref()).cast(), + destination_component_count, + parent_component_counts, + requires_two_momenta, + required_parameter_count: self + .scale + .model_parameter_index + .map_or(0, |parameter| parameter + 1), + interaction_kind, + interaction_call: (interaction_kind + == DirectInteractionIntrinsicKind::ColorOrderedThreeVector) + .then_some(execute_interaction_bundle), + }) + } + + pub(crate) fn handle(&self) -> DirectExecutorHandle { + let handle = self.contribution_handle(); + DirectExecutorHandle::Contribution { + call: handle.call, + context: handle.context, + } + } + + pub(crate) fn finalization_handle( + runtime_template: &str, + ) -> RusticolResult { + let call: DirectFinalizationExecutor = + match RecurrenceFinalizationIntrinsicKind::from_runtime_template(runtime_template)? { + RecurrenceFinalizationIntrinsicKind::WeylPropagatorPositive => { + execute_weyl_propagator_positive_rows + } + RecurrenceFinalizationIntrinsicKind::WeylPropagatorNegative => { + execute_weyl_propagator_negative_rows + } + RecurrenceFinalizationIntrinsicKind::FeynmanVectorPropagator => { + execute_feynman_vector_propagator_rows + } + }; + Ok(DirectExecutorHandle::Finalization { + call, + context: ptr::null::(), + }) + } +} + +/// One generic identity finalizer for every non-propagating state. +/// +/// The prepared catalog resolves exactly this one function. Per-state +/// dimensions are deliberately read from `row.component_count`. +pub(crate) unsafe extern "C" fn execute_identity_finalization_rows( + _context: *const c_void, + arena: DirectArenaView, + _momenta: DirectMomentumView, + _parameters: DirectParameterView, + factors: DirectFactorView, + rows: *const DirectFinalizationRow, + row_count: u32, + point_count: u32, +) -> c_int { + if rows.is_null() + || row_count == 0 + || point_count == 0 + || arena.point_stride == 0 + || point_count > arena.point_stride + { + return STATUS_INVALID_ARGUMENT; + } + if arena.current_re.is_null() + || arena.current_im.is_null() + || factors.values_re.is_null() + || factors.values_im.is_null() + { + return STATUS_INVALID_ARGUMENT; + } + + let rows = unsafe { std::slice::from_raw_parts(rows, row_count as usize) }; + for row in rows { + if row.component_count == 0 || row.exact_factor_id >= factors.value_count { + return STATUS_BOUNDS; + } + let factor_re = unsafe { *factors.values_re.add(row.exact_factor_id as usize) }; + let factor_im = unsafe { *factors.values_im.add(row.exact_factor_id as usize) }; + for component in 0..u64::from(row.component_count) { + let plane = match u64::from(row.component_base).checked_add(component) { + Some(value) => value, + None => return STATUS_BOUNDS, + }; + let offset = match plane.checked_mul(u64::from(arena.point_stride)) { + Some(value) => value, + None => return STATUS_BOUNDS, + }; + let end = match offset.checked_add(u64::from(point_count)) { + Some(value) => value, + None => return STATUS_BOUNDS, + }; + if end > arena.current_scalar_len { + return STATUS_BOUNDS; + } + let Ok(offset) = usize::try_from(offset) else { + return STATUS_BOUNDS; + }; + for point in 0..point_count as usize { + let index = offset + point; + let value_re = unsafe { *arena.current_re.add(index) }; + let value_im = unsafe { *arena.current_im.add(index) }; + unsafe { + *arena.current_re.add(index) = factor_re * value_re - factor_im * value_im; + *arena.current_im.add(index) = factor_re * value_im + factor_im * value_re; + } + } + } + } + DIRECT_STATUS_OK +} + +#[derive(Clone, Copy)] +struct ComplexValue { + re: f64, + im: f64, +} + +impl ComplexValue { + const ZERO: Self = Self { re: 0.0, im: 0.0 }; + + #[inline(always)] + const fn new(re: f64, im: f64) -> Self { + Self { re, im } + } + + #[inline(always)] + fn add(self, other: Self) -> Self { + Self::new(self.re + other.re, self.im + other.im) + } + + #[inline(always)] + fn sub(self, other: Self) -> Self { + Self::new(self.re - other.re, self.im - other.im) + } + + #[inline(always)] + fn neg(self) -> Self { + Self::new(-self.re, -self.im) + } + + #[inline(always)] + fn mul(self, other: Self) -> Self { + Self::new( + self.re * other.re - self.im * other.im, + self.re * other.im + self.im * other.re, + ) + } + + #[inline(always)] + fn mul_i(self) -> Self { + Self::new(-self.im, self.re) + } + + #[inline(always)] + fn mul_real(self, value: f64) -> Self { + Self::new(self.re * value, self.im * value) + } + + #[inline(always)] + fn mul_real_pair(self, value: f64x2) -> SimdComplex2 { + SimdComplex2::new( + f64x2::new([self.re, self.re]) * value, + f64x2::new([self.im, self.im]) * value, + ) + } +} + +trait DirectContributionFormula { + const PARENT0_COMPONENTS: u32; + const PARENT1_COMPONENTS: u32; + const DESTINATION_COMPONENTS: u32; + const BASE_SCALE: ComplexValue; + + /// Evaluate one point after all row and arena bounds have been validated. + unsafe fn evaluate_point( + arena: DirectArenaView, + row: DirectContributionRow, + stride: usize, + point: usize, + scale: ComplexValue, + ); + + #[inline(always)] + unsafe fn evaluate_pair( + arena: DirectArenaView, + row: DirectContributionRow, + stride: usize, + point: usize, + scale: ComplexValue, + ) { + unsafe { + Self::evaluate_point(arena, row, stride, point, scale); + Self::evaluate_point(arena, row, stride, point + 1, scale); + } + } +} + +struct WeylVectorToWeylPositive; +struct WeylVectorToWeylNegative; +struct AntisymmetricTensorVectorToVector; +struct VectorWedgeVectorToAntisymmetricTensor; +struct ColorOrderedThreeVector; + +trait DirectContributionFanoutFormula { + const BASE_SCALE: ComplexValue; + + unsafe fn evaluate( + arena: DirectArenaView, + momenta: Option, + row: DirectContributionRow, + stride: usize, + point: usize, + ) -> [ComplexValue; N]; +} + +#[derive(Clone, Copy)] +struct SimdComplex2 { + re: f64x2, + im: f64x2, +} + +impl SimdComplex2 { + #[inline(always)] + fn new(re: f64x2, im: f64x2) -> Self { + Self { re, im } + } + + #[inline(always)] + fn add(self, other: Self) -> Self { + Self::new(self.re + other.re, self.im + other.im) + } + + #[inline(always)] + fn sub(self, other: Self) -> Self { + Self::new(self.re - other.re, self.im - other.im) + } + + #[inline(always)] + fn neg(self) -> Self { + Self::new(-self.re, -self.im) + } + + #[inline(always)] + fn mul(self, other: Self) -> Self { + Self::new( + self.re * other.re - self.im * other.im, + self.re * other.im + self.im * other.re, + ) + } + + #[inline(always)] + fn mul_i(self) -> Self { + Self::new(-self.im, self.re) + } + + #[inline(always)] + fn mul_real(self, value: f64x2) -> Self { + Self::new(self.re * value, self.im * value) + } +} + +impl ColorOrderedThreeVector { + const PARENT_COMPONENTS: u32 = 4; + const DESTINATION_COMPONENTS: u32 = 4; + const BASE_SCALE: ComplexValue = ComplexValue::new(1.0, 0.0); + + #[inline(always)] + unsafe fn evaluate_pair( + arena: DirectArenaView, + momenta: DirectMomentumView, + row: DirectContributionRow, + stride: usize, + point: usize, + scale: ComplexValue, + ) { + let mut left = [SimdComplex2::new(f64x2::ZERO, f64x2::ZERO); 4]; + let mut right = [SimdComplex2::new(f64x2::ZERO, f64x2::ZERO); 4]; + let mut parent0_momentum = [f64x2::ZERO; 4]; + let mut parent1_momentum = [f64x2::ZERO; 4]; + for component in 0..4 { + left[component] = unsafe { + load_current_pair( + arena, + row.parent0_component_base + component as u32, + stride, + point, + ) + }; + right[component] = unsafe { + load_current_pair( + arena, + row.parent1_component_base_or_sentinel + component as u32, + stride, + point, + ) + }; + parent0_momentum[component] = unsafe { + load_momentum_pair( + momenta, + row.parent0_momentum_form_id, + component as u16, + stride, + point, + ) + }; + parent1_momentum[component] = unsafe { + load_momentum_pair( + momenta, + row.parent1_momentum_form_id_or_sentinel, + component as u16, + stride, + point, + ) + }; + } + + let vector_dot = minkowski_simd_complex_dot(left, right); + let left_dot_parent1 = minkowski_simd_complex_real_dot(left, parent1_momentum); + let right_dot_parent0 = minkowski_simd_complex_real_dot(right, parent0_momentum); + let two = f64x2::new([2.0, 2.0]); + for component in 0..4 { + let momentum_term = + vector_dot.mul_real(parent0_momentum[component] - parent1_momentum[component]); + let current_term = left_dot_parent1 + .mul(right[component]) + .sub(right_dot_parent0.mul(left[component])) + .mul_real(two); + unsafe { + add_scaled_current_pair( + arena, + row.destination_component_base + component as u32, + stride, + point, + momentum_term.add(current_term), + scale, + contribution_accumulates(row), + ); + } + } + } + + #[inline(always)] + unsafe fn evaluate_point( + arena: DirectArenaView, + momenta: DirectMomentumView, + row: DirectContributionRow, + stride: usize, + point: usize, + scale: ComplexValue, + ) { + let output = unsafe { + >::evaluate( + arena, + Some(momenta), + row, + stride, + point, + ) + }; + for (component, value) in output.into_iter().enumerate() { + unsafe { + add_scaled_current( + arena, + row.destination_component_base + component as u32, + stride, + point, + value, + scale, + contribution_accumulates(row), + ); + } + } + } +} + +#[inline(always)] +fn color_ordered_three_vector_loaded( + left: [ComplexValue; 4], + right: [ComplexValue; 4], + parent0_momentum: [f64; 4], + parent1_momentum: [f64; 4], +) -> [ComplexValue; 4] { + let vector_dot = minkowski_complex_dot(left, right); + let left_dot_parent1 = minkowski_complex_real_dot(left, parent1_momentum); + let right_dot_parent0 = minkowski_complex_real_dot(right, parent0_momentum); + core::array::from_fn(|component| { + let momentum_term = + vector_dot.mul_real(parent0_momentum[component] - parent1_momentum[component]); + let current_term = left_dot_parent1 + .mul(right[component]) + .sub(right_dot_parent0.mul(left[component])) + .mul_real(2.0); + momentum_term.add(current_term) + }) +} + +impl DirectContributionFanoutFormula<4> for ColorOrderedThreeVector { + const BASE_SCALE: ComplexValue = Self::BASE_SCALE; + + #[inline(always)] + unsafe fn evaluate( + arena: DirectArenaView, + momenta: Option, + row: DirectContributionRow, + stride: usize, + point: usize, + ) -> [ComplexValue; 4] { + let momenta = momenta.expect("color-ordered fanout inputs were validated"); + let mut left = [ComplexValue::ZERO; 4]; + let mut right = [ComplexValue::ZERO; 4]; + let mut parent0_momentum = [0.0; 4]; + let mut parent1_momentum = [0.0; 4]; + for component in 0..4 { + left[component] = unsafe { + load_current( + arena, + row.parent0_component_base + component as u32, + stride, + point, + ) + }; + right[component] = unsafe { + load_current( + arena, + row.parent1_component_base_or_sentinel + component as u32, + stride, + point, + ) + }; + parent0_momentum[component] = unsafe { + load_momentum( + momenta, + row.parent0_momentum_form_id, + component as u16, + stride, + point, + ) + }; + parent1_momentum[component] = unsafe { + load_momentum( + momenta, + row.parent1_momentum_form_id_or_sentinel, + component as u16, + stride, + point, + ) + }; + } + + color_ordered_three_vector_loaded(left, right, parent0_momentum, parent1_momentum) + } +} + +impl DirectContributionFormula for WeylVectorToWeylPositive { + const PARENT0_COMPONENTS: u32 = 2; + const PARENT1_COMPONENTS: u32 = 4; + const DESTINATION_COMPONENTS: u32 = 2; + const BASE_SCALE: ComplexValue = ComplexValue::new(1.0, 0.0); + + #[inline(always)] + unsafe fn evaluate_point( + arena: DirectArenaView, + row: DirectContributionRow, + stride: usize, + point: usize, + scale: ComplexValue, + ) { + let l0 = unsafe { load_current(arena, row.parent0_component_base, stride, point) }; + let l1 = unsafe { load_current(arena, row.parent0_component_base + 1, stride, point) }; + let r0 = + unsafe { load_current(arena, row.parent1_component_base_or_sentinel, stride, point) }; + let r1 = unsafe { + load_current( + arena, + row.parent1_component_base_or_sentinel + 1, + stride, + point, + ) + }; + let r2 = unsafe { + load_current( + arena, + row.parent1_component_base_or_sentinel + 2, + stride, + point, + ) + }; + let r3 = unsafe { + load_current( + arena, + row.parent1_component_base_or_sentinel + 3, + stride, + point, + ) + }; + + let output0 = l0 + .mul(r3) + .mul_i() + .neg() + .sub(l1.mul(r1).mul_i()) + .add(l1.mul(r2)) + .add(l0.mul(r0).mul_i()); + let output1 = l0 + .mul(r2) + .neg() + .sub(l0.mul(r1).mul_i()) + .add(l1.mul(r0).mul_i()) + .add(l1.mul(r3).mul_i()); + unsafe { + add_scaled_current( + arena, + row.destination_component_base, + stride, + point, + output0, + scale, + contribution_accumulates(row), + ); + add_scaled_current( + arena, + row.destination_component_base + 1, + stride, + point, + output1, + scale, + contribution_accumulates(row), + ); + } + } + + #[inline(always)] + unsafe fn evaluate_pair( + arena: DirectArenaView, + row: DirectContributionRow, + stride: usize, + point: usize, + scale: ComplexValue, + ) { + let l0 = unsafe { load_current_pair(arena, row.parent0_component_base, stride, point) }; + let l1 = unsafe { load_current_pair(arena, row.parent0_component_base + 1, stride, point) }; + let r0 = unsafe { + load_current_pair(arena, row.parent1_component_base_or_sentinel, stride, point) + }; + let r1 = unsafe { + load_current_pair( + arena, + row.parent1_component_base_or_sentinel + 1, + stride, + point, + ) + }; + let r2 = unsafe { + load_current_pair( + arena, + row.parent1_component_base_or_sentinel + 2, + stride, + point, + ) + }; + let r3 = unsafe { + load_current_pair( + arena, + row.parent1_component_base_or_sentinel + 3, + stride, + point, + ) + }; + + let output0 = l0 + .mul(r3) + .mul_i() + .neg() + .sub(l1.mul(r1).mul_i()) + .add(l1.mul(r2)) + .add(l0.mul(r0).mul_i()); + let output1 = l0 + .mul(r2) + .neg() + .sub(l0.mul(r1).mul_i()) + .add(l1.mul(r0).mul_i()) + .add(l1.mul(r3).mul_i()); + unsafe { + add_scaled_current_pair( + arena, + row.destination_component_base, + stride, + point, + output0, + scale, + contribution_accumulates(row), + ); + add_scaled_current_pair( + arena, + row.destination_component_base + 1, + stride, + point, + output1, + scale, + contribution_accumulates(row), + ); + } + } +} + +impl DirectContributionFormula for WeylVectorToWeylNegative { + const PARENT0_COMPONENTS: u32 = 2; + const PARENT1_COMPONENTS: u32 = 4; + const DESTINATION_COMPONENTS: u32 = 2; + const BASE_SCALE: ComplexValue = ComplexValue::new(1.0, 0.0); + + #[inline(always)] + unsafe fn evaluate_point( + arena: DirectArenaView, + row: DirectContributionRow, + stride: usize, + point: usize, + scale: ComplexValue, + ) { + let l0 = unsafe { load_current(arena, row.parent0_component_base, stride, point) }; + let l1 = unsafe { load_current(arena, row.parent0_component_base + 1, stride, point) }; + let r0 = + unsafe { load_current(arena, row.parent1_component_base_or_sentinel, stride, point) }; + let r1 = unsafe { + load_current( + arena, + row.parent1_component_base_or_sentinel + 1, + stride, + point, + ) + }; + let r2 = unsafe { + load_current( + arena, + row.parent1_component_base_or_sentinel + 2, + stride, + point, + ) + }; + let r3 = unsafe { + load_current( + arena, + row.parent1_component_base_or_sentinel + 3, + stride, + point, + ) + }; + + let output0 = l1 + .mul(r2) + .neg() + .add(l0.mul(r0).mul_i()) + .add(l0.mul(r3).mul_i()) + .add(l1.mul(r1).mul_i()); + let output1 = l1 + .mul(r3) + .mul_i() + .neg() + .add(l0.mul(r2)) + .add(l0.mul(r1).mul_i()) + .add(l1.mul(r0).mul_i()); + unsafe { + add_scaled_current( + arena, + row.destination_component_base, + stride, + point, + output0, + scale, + contribution_accumulates(row), + ); + add_scaled_current( + arena, + row.destination_component_base + 1, + stride, + point, + output1, + scale, + contribution_accumulates(row), + ); + } + } + + #[inline(always)] + unsafe fn evaluate_pair( + arena: DirectArenaView, + row: DirectContributionRow, + stride: usize, + point: usize, + scale: ComplexValue, + ) { + let l0 = unsafe { load_current_pair(arena, row.parent0_component_base, stride, point) }; + let l1 = unsafe { load_current_pair(arena, row.parent0_component_base + 1, stride, point) }; + let r0 = unsafe { + load_current_pair(arena, row.parent1_component_base_or_sentinel, stride, point) + }; + let r1 = unsafe { + load_current_pair( + arena, + row.parent1_component_base_or_sentinel + 1, + stride, + point, + ) + }; + let r2 = unsafe { + load_current_pair( + arena, + row.parent1_component_base_or_sentinel + 2, + stride, + point, + ) + }; + let r3 = unsafe { + load_current_pair( + arena, + row.parent1_component_base_or_sentinel + 3, + stride, + point, + ) + }; + + let output0 = l1 + .mul(r2) + .neg() + .add(l0.mul(r0).mul_i()) + .add(l0.mul(r3).mul_i()) + .add(l1.mul(r1).mul_i()); + let output1 = l1 + .mul(r3) + .mul_i() + .neg() + .add(l0.mul(r2)) + .add(l0.mul(r1).mul_i()) + .add(l1.mul(r0).mul_i()); + unsafe { + add_scaled_current_pair( + arena, + row.destination_component_base, + stride, + point, + output0, + scale, + contribution_accumulates(row), + ); + add_scaled_current_pair( + arena, + row.destination_component_base + 1, + stride, + point, + output1, + scale, + contribution_accumulates(row), + ); + } + } +} + +impl DirectContributionFormula for AntisymmetricTensorVectorToVector { + const PARENT0_COMPONENTS: u32 = 6; + const PARENT1_COMPONENTS: u32 = 4; + const DESTINATION_COMPONENTS: u32 = 4; + const BASE_SCALE: ComplexValue = ComplexValue::new(1.0, 0.0); + + #[inline(always)] + unsafe fn evaluate_point( + arena: DirectArenaView, + row: DirectContributionRow, + stride: usize, + point: usize, + scale: ComplexValue, + ) { + let output = unsafe { + >::evaluate(arena, None, row, stride, point) + }; + for (component, value) in output.into_iter().enumerate() { + unsafe { + add_scaled_current( + arena, + row.destination_component_base + component as u32, + stride, + point, + value, + scale, + contribution_accumulates(row), + ); + } + } + } + + #[inline(always)] + unsafe fn evaluate_pair( + arena: DirectArenaView, + row: DirectContributionRow, + stride: usize, + point: usize, + scale: ComplexValue, + ) { + let mut left = [SimdComplex2::new(f64x2::ZERO, f64x2::ZERO); 6]; + let mut right = [SimdComplex2::new(f64x2::ZERO, f64x2::ZERO); 4]; + for (component, value) in left.iter_mut().enumerate() { + *value = unsafe { + load_current_pair( + arena, + row.parent0_component_base + component as u32, + stride, + point, + ) + }; + } + for (component, value) in right.iter_mut().enumerate() { + *value = unsafe { + load_current_pair( + arena, + row.parent1_component_base_or_sentinel + component as u32, + stride, + point, + ) + }; + } + let output = [ + left[0] + .mul(right[1]) + .add(left[1].mul(right[2])) + .add(left[2].mul(right[3])), + left[0] + .mul(right[0]) + .add(left[3].mul(right[2])) + .add(left[4].mul(right[3])), + left[1] + .mul(right[0]) + .sub(left[3].mul(right[1])) + .add(left[5].mul(right[3])), + left[2] + .mul(right[0]) + .sub(left[4].mul(right[1])) + .sub(left[5].mul(right[2])), + ]; + for (component, value) in output.into_iter().enumerate() { + unsafe { + add_scaled_current_pair( + arena, + row.destination_component_base + component as u32, + stride, + point, + value, + scale, + contribution_accumulates(row), + ); + } + } + } +} + +impl DirectContributionFanoutFormula<4> for AntisymmetricTensorVectorToVector { + const BASE_SCALE: ComplexValue = ::BASE_SCALE; + + #[inline(always)] + unsafe fn evaluate( + arena: DirectArenaView, + _momenta: Option, + row: DirectContributionRow, + stride: usize, + point: usize, + ) -> [ComplexValue; 4] { + let mut left = [ComplexValue::ZERO; 6]; + let mut right = [ComplexValue::ZERO; 4]; + for (component, value) in left.iter_mut().enumerate() { + *value = unsafe { + load_current( + arena, + row.parent0_component_base + component as u32, + stride, + point, + ) + }; + } + for (component, value) in right.iter_mut().enumerate() { + *value = unsafe { + load_current( + arena, + row.parent1_component_base_or_sentinel + component as u32, + stride, + point, + ) + }; + } + antisymmetric_tensor_vector_loaded(left, right) + } +} + +#[inline(always)] +fn antisymmetric_tensor_vector_loaded( + left: [ComplexValue; 6], + right: [ComplexValue; 4], +) -> [ComplexValue; 4] { + [ + left[0] + .mul(right[1]) + .add(left[1].mul(right[2])) + .add(left[2].mul(right[3])), + left[0] + .mul(right[0]) + .add(left[3].mul(right[2])) + .add(left[4].mul(right[3])), + left[1] + .mul(right[0]) + .sub(left[3].mul(right[1])) + .add(left[5].mul(right[3])), + left[2] + .mul(right[0]) + .sub(left[4].mul(right[1])) + .sub(left[5].mul(right[2])), + ] +} + +impl DirectContributionFormula for VectorWedgeVectorToAntisymmetricTensor { + const PARENT0_COMPONENTS: u32 = 4; + const PARENT1_COMPONENTS: u32 = 4; + const DESTINATION_COMPONENTS: u32 = 6; + const BASE_SCALE: ComplexValue = ComplexValue::new(1.0, 0.0); + + #[inline(always)] + unsafe fn evaluate_point( + arena: DirectArenaView, + row: DirectContributionRow, + stride: usize, + point: usize, + scale: ComplexValue, + ) { + let output = unsafe { + >::evaluate(arena, None, row, stride, point) + }; + for (component, value) in output.into_iter().enumerate() { + unsafe { + add_scaled_current( + arena, + row.destination_component_base + component as u32, + stride, + point, + value, + scale, + contribution_accumulates(row), + ); + } + } + } + + #[inline(always)] + unsafe fn evaluate_pair( + arena: DirectArenaView, + row: DirectContributionRow, + stride: usize, + point: usize, + scale: ComplexValue, + ) { + let mut left = [SimdComplex2::new(f64x2::ZERO, f64x2::ZERO); 4]; + let mut right = [SimdComplex2::new(f64x2::ZERO, f64x2::ZERO); 4]; + for component in 0..4 { + left[component] = unsafe { + load_current_pair( + arena, + row.parent0_component_base + component as u32, + stride, + point, + ) + }; + right[component] = unsafe { + load_current_pair( + arena, + row.parent1_component_base_or_sentinel + component as u32, + stride, + point, + ) + }; + } + let output = [ + left[0].mul(right[1]).sub(left[1].mul(right[0])), + left[0].mul(right[2]).sub(left[2].mul(right[0])), + left[0].mul(right[3]).sub(left[3].mul(right[0])), + left[1].mul(right[2]).sub(left[2].mul(right[1])), + left[1].mul(right[3]).sub(left[3].mul(right[1])), + left[2].mul(right[3]).sub(left[3].mul(right[2])), + ]; + for (component, value) in output.into_iter().enumerate() { + unsafe { + add_scaled_current_pair( + arena, + row.destination_component_base + component as u32, + stride, + point, + value, + scale, + contribution_accumulates(row), + ); + } + } + } +} + +impl DirectContributionFanoutFormula<6> for VectorWedgeVectorToAntisymmetricTensor { + const BASE_SCALE: ComplexValue = ::BASE_SCALE; + + #[inline(always)] + unsafe fn evaluate( + arena: DirectArenaView, + _momenta: Option, + row: DirectContributionRow, + stride: usize, + point: usize, + ) -> [ComplexValue; 6] { + let mut left = [ComplexValue::ZERO; 4]; + let mut right = [ComplexValue::ZERO; 4]; + for component in 0..4 { + left[component] = unsafe { + load_current( + arena, + row.parent0_component_base + component as u32, + stride, + point, + ) + }; + right[component] = unsafe { + load_current( + arena, + row.parent1_component_base_or_sentinel + component as u32, + stride, + point, + ) + }; + } + vector_wedge_vector_loaded(left, right) + } +} + +#[inline(always)] +fn vector_wedge_vector_loaded( + left: [ComplexValue; 4], + right: [ComplexValue; 4], +) -> [ComplexValue; 6] { + [ + left[0].mul(right[1]).sub(left[1].mul(right[0])), + left[0].mul(right[2]).sub(left[2].mul(right[0])), + left[0].mul(right[3]).sub(left[3].mul(right[0])), + left[1].mul(right[2]).sub(left[2].mul(right[1])), + left[1].mul(right[3]).sub(left[3].mul(right[1])), + left[2].mul(right[3]).sub(left[3].mul(right[2])), + ] +} + +#[allow(clippy::too_many_arguments)] +unsafe fn execute_contribution_fanout( + context: *const c_void, + arena: DirectArenaView, + momenta: DirectMomentumView, + parameters: DirectParameterView, + factors: DirectFactorView, + batch: DirectContributionFanoutBatch<'_>, +) -> RusticolResult<()> +where + F: DirectContributionFanoutFormula, +{ + // The private batch is produced only by a cold-authenticated direct or + // family-local fanout program. Its row spans and all raw views were + // validated before this call. + let context = unsafe { &*context.cast::() }; + let context_scale = effective_context_scale(*context, parameters) + .map_err(|_| RusticolError::evaluation("fused contribution fanout scale is unavailable"))? + .mul(F::BASE_SCALE); + let stride = arena.point_stride as usize; + + for run in batch.runs { + let row_start = usize::try_from(run.row_start) + .map_err(|_| RusticolError::integrity("fused fanout row start exceeds usize"))?; + let row_end = row_start + .checked_add(run.row_count as usize) + .ok_or_else(|| RusticolError::integrity("fused fanout row range overflows usize"))?; + let run_rows = batch + .rows + .get(row_start..row_end) + .ok_or_else(|| RusticolError::integrity("fused fanout row range is out of bounds"))?; + let representative = *run_rows.first().ok_or_else(|| { + RusticolError::integrity("fused fanout run has no representative row") + })?; + let mut output = unsafe { F::evaluate(arena, Some(momenta), representative, stride, 0) }; + for value in &mut output { + *value = value.mul(context_scale); + } + + let bundle_start = usize::try_from(run.bundle_start) + .map_err(|_| RusticolError::integrity("fused fanout bundle start exceeds usize"))?; + let bundle_end = bundle_start + .checked_add(run.bundle_count as usize) + .ok_or_else(|| RusticolError::integrity("fused fanout bundle range overflows usize"))?; + let bundles = batch.bundles.get(bundle_start..bundle_end).ok_or_else(|| { + RusticolError::integrity("fused fanout bundle range is out of bounds") + })?; + for bundle in bundles { + if bundle.effective_factor_id_or_sentinel == DIRECT_NONE_U32 { + return Err(RusticolError::integrity( + "fused contribution fanout requires an explicit destination factor", + )); + } + let factor = bundle.effective_factor_id_or_sentinel as usize; + if factor >= factors.value_count as usize { + return Err(RusticolError::integrity( + "fused contribution fanout factor is out of bounds", + )); + } + let scale = DirectContributionFanoutScale::new( + unsafe { *factors.values_re.add(factor) }, + unsafe { *factors.values_im.add(factor) }, + ); + let scaled_output = output.map(|value| { + let (re, im) = scale.apply(value.re, value.im); + ComplexValue::new(re, im) + }); + let bundle_row_start = usize::try_from(bundle.row_start).map_err(|_| { + RusticolError::integrity("fused fanout bundle row start exceeds usize") + })?; + let bundle_row_end = bundle_row_start + .checked_add(bundle.row_count as usize) + .ok_or_else(|| { + RusticolError::integrity("fused fanout bundle row range overflows usize") + })?; + let rows = batch + .rows + .get(bundle_row_start..bundle_row_end) + .ok_or_else(|| { + RusticolError::integrity("fused fanout bundle row range is out of bounds") + })?; + for row in rows { + for (component, value) in scaled_output.iter().enumerate() { + let destination = + (row.destination_component_base as usize + component) * stride; + unsafe { + if contribution_accumulates(*row) { + *arena.current_re.add(destination) += value.re; + *arena.current_im.add(destination) += value.im; + } else { + *arena.current_re.add(destination) = value.re; + *arena.current_im.add(destination) = value.im; + } + } + } + } + } + } + Ok(()) +} + +unsafe fn execute_antisymmetric_tensor_vector_to_vector_fanout( + context: *const c_void, + arena: DirectArenaView, + momenta: DirectMomentumView, + parameters: DirectParameterView, + factors: DirectFactorView, + batch: DirectContributionFanoutBatch<'_>, +) -> RusticolResult<()> { + unsafe { + execute_contribution_fanout::( + context, arena, momenta, parameters, factors, batch, + ) + } +} + +unsafe fn execute_vector_wedge_vector_to_antisymmetric_tensor_fanout( + context: *const c_void, + arena: DirectArenaView, + momenta: DirectMomentumView, + parameters: DirectParameterView, + factors: DirectFactorView, + batch: DirectContributionFanoutBatch<'_>, +) -> RusticolResult<()> { + unsafe { + execute_contribution_fanout::( + context, arena, momenta, parameters, factors, batch, + ) + } +} + +unsafe fn execute_color_ordered_three_vector_fanout( + context: *const c_void, + arena: DirectArenaView, + momenta: DirectMomentumView, + parameters: DirectParameterView, + factors: DirectFactorView, + batch: DirectContributionFanoutBatch<'_>, +) -> RusticolResult<()> { + if momenta.values.is_null() + || momenta.lorentz_component_count != 4 + || momenta.point_stride != arena.point_stride + { + return Err(RusticolError::evaluation( + "color-ordered fused fanout requires four-component momenta at the arena point stride", + )); + } + unsafe { + execute_contribution_fanout::( + context, arena, momenta, parameters, factors, batch, + ) + } +} + +unsafe fn execute_interaction_bundle( + contexts: DirectInteractionExecutorContexts, + arena: DirectArenaView, + momenta: DirectMomentumView, + parameters: DirectParameterView, + factors: DirectFactorView, + batch: DirectInteractionBundleBatch<'_>, +) -> RusticolResult<()> { + if contexts.color.is_null() + || contexts.antisymmetric_tensor_vector.is_null() + || (!batch.tensor_outputs.is_empty() && contexts.vector_wedge_vector.is_null()) + { + return Err(RusticolError::evaluation( + "direct interaction bundle has a null intrinsic context", + )); + } + let stride = validate_interaction_runtime_views( + arena, + momenta, + parameters, + factors, + batch.requirements, + )?; + let color_context = unsafe { &*contexts.color.cast::() }; + let atv_context = unsafe { + &*contexts + .antisymmetric_tensor_vector + .cast::() + }; + let color_context_scale = effective_context_scale(*color_context, parameters) + .map_err(|_| RusticolError::evaluation("interaction color scale is unavailable"))?; + let atv_context_scale = effective_context_scale(*atv_context, parameters) + .map_err(|_| RusticolError::evaluation("interaction tensor-vector scale is unavailable"))?; + if batch.tensor_outputs.is_empty() && batch.terminal.is_none() { + return unsafe { + execute_vector_interaction_bundle( + arena, + momenta, + factors, + batch, + stride, + color_context_scale, + atv_context_scale, + ) + }; + } + let wedge_context_scale = if batch.tensor_outputs.is_empty() { + None + } else { + let wedge_context = unsafe { + &*contexts + .vector_wedge_vector + .cast::() + }; + Some( + effective_context_scale(*wedge_context, parameters).map_err(|_| { + RusticolError::evaluation("interaction vector-wedge scale is unavailable") + })?, + ) + }; + let terminal_inputs = if let Some(terminal) = batch.terminal { + let mut partner = [ComplexValue::ZERO; 4]; + let mut projection_weights = [ComplexValue::ZERO; 4]; + for component in 0..4 { + partner[component] = unsafe { + load_current( + arena, + terminal.partner_component_base + component as u32, + stride, + 0, + ) + }; + let factor_id = terminal.component_factor_start + component as u32; + let component_factor = ComplexValue::new( + unsafe { *factors.values_re.add(factor_id as usize) }, + unsafe { *factors.values_im.add(factor_id as usize) }, + ); + projection_weights[component] = partner[component].mul(component_factor); + } + Some(projection_weights) + } else { + None + }; + + for anchor in batch.anchors { + let mut left = [ComplexValue::ZERO; 4]; + let mut right = [ComplexValue::ZERO; 4]; + let mut parent0_momentum = [0.0; 4]; + let mut parent1_momentum = [0.0; 4]; + for component in 0..4 { + left[component] = unsafe { + load_current( + arena, + anchor.parent_component_bases[0] + component as u32, + stride, + 0, + ) + }; + right[component] = unsafe { + load_current( + arena, + anchor.parent_component_bases[1] + component as u32, + stride, + 0, + ) + }; + parent0_momentum[component] = unsafe { + load_momentum( + momenta, + anchor.parent_momentum_form_ids[0], + component as u16, + stride, + 0, + ) + }; + parent1_momentum[component] = unsafe { + load_momentum( + momenta, + anchor.parent_momentum_form_ids[1], + component as u16, + stride, + 0, + ) + }; + } + let color_output = + color_ordered_three_vector_loaded(left, right, parent0_momentum, parent1_momentum) + .map(|value| value.mul(color_context_scale)); + let wedge_output = wedge_context_scale + .map(|scale| vector_wedge_vector_loaded(left, right).map(|value| value.mul(scale))); + + let mut atv_outputs = [[ComplexValue::ZERO; 4]; 2]; + let atv_count = match anchor.operands { + DirectInteractionOperands::None => 0, + DirectInteractionOperands::One(operand) => { + atv_outputs[0] = unsafe { + evaluate_interaction_operand( + operand, + left, + right, + arena, + stride, + atv_context_scale, + ) + }; + 1 + } + DirectInteractionOperands::Two(operands) => { + atv_outputs[0] = unsafe { + evaluate_interaction_operand( + operands[0], + left, + right, + arena, + stride, + atv_context_scale, + ) + }; + atv_outputs[1] = unsafe { + evaluate_interaction_operand( + operands[1], + left, + right, + arena, + stride, + atv_context_scale, + ) + }; + 2 + } + }; + let outputs = batch + .outputs + .get(anchor.output_start as usize..anchor.output_end as usize) + .ok_or_else(|| RusticolError::integrity("interaction output range is out of bounds"))?; + + if let Some(projection_weights) = terminal_inputs { + let projected_color = project_terminal_vector(color_output, projection_weights); + let mut projected_atv = [ComplexValue::ZERO; 2]; + for slot in 0..atv_count { + projected_atv[slot] = + project_terminal_vector(atv_outputs[slot], projection_weights); + } + for output in outputs { + let color_scale = + unsafe { live_interaction_factor(factors, output.exact_factor_ids[0]) }; + let scaled_color = apply_fanout_scale(projected_color, color_scale); + let mut combined = if batch.atv_before_color { + ComplexValue::ZERO + } else { + scaled_color + }; + for (slot, projected) in projected_atv.iter().take(atv_count).enumerate() { + let scale = unsafe { + live_interaction_factor(factors, output.exact_factor_ids[slot + 1]) + }; + combined = combined.add(apply_fanout_scale(*projected, scale)); + } + if batch.atv_before_color { + combined = combined.add(scaled_color); + } + let destination = output.destination_component_base as usize * stride; + unsafe { + if output.initialize { + *arena.current_re.add(destination) = combined.re; + *arena.current_im.add(destination) = combined.im; + } else { + *arena.current_re.add(destination) += combined.re; + *arena.current_im.add(destination) += combined.im; + } + } + } + } else { + for output in outputs { + let color_scale = + unsafe { live_interaction_factor(factors, output.exact_factor_ids[0]) }; + let scaled_color = color_output.map(|value| apply_fanout_scale(value, color_scale)); + let mut combined = if batch.atv_before_color { + [ComplexValue::ZERO; 4] + } else { + scaled_color + }; + for (slot, atv_output) in atv_outputs.iter().take(atv_count).enumerate() { + let scale = unsafe { + live_interaction_factor(factors, output.exact_factor_ids[slot + 1]) + }; + for component in 0..4 { + combined[component] = combined[component] + .add(apply_fanout_scale(atv_output[component], scale)); + } + } + if batch.atv_before_color { + for component in 0..4 { + combined[component] = combined[component].add(scaled_color[component]); + } + } + for (component, value) in combined.into_iter().enumerate() { + let destination = + (output.destination_component_base as usize + component) * stride; + unsafe { + if output.initialize { + *arena.current_re.add(destination) = value.re; + *arena.current_im.add(destination) = value.im; + } else { + *arena.current_re.add(destination) += value.re; + *arena.current_im.add(destination) += value.im; + } + } + } + } + } + let tensor_outputs = batch + .tensor_outputs + .get(anchor.tensor_output_start as usize..anchor.tensor_output_end as usize) + .ok_or_else(|| { + RusticolError::integrity("interaction tensor-output range is out of bounds") + })?; + if !tensor_outputs.is_empty() { + let wedge_output = wedge_output.ok_or_else(|| { + RusticolError::integrity("interaction tensor outputs have no wedge evaluation") + })?; + for output in tensor_outputs { + let scale = unsafe { live_interaction_factor(factors, output.exact_factor_id) }; + for (component, value) in wedge_output.into_iter().enumerate() { + let value = apply_fanout_scale(value, scale); + let destination = + (output.destination_component_base as usize + component) * stride; + unsafe { + if output.initialize { + *arena.current_re.add(destination) = value.re; + *arena.current_im.add(destination) = value.im; + } else { + *arena.current_re.add(destination) += value.re; + *arena.current_im.add(destination) += value.im; + } + } + } + } + } + } + Ok(()) +} + +#[allow(clippy::too_many_arguments)] +unsafe fn execute_vector_interaction_bundle( + arena: DirectArenaView, + momenta: DirectMomentumView, + factors: DirectFactorView, + batch: DirectInteractionBundleBatch<'_>, + stride: usize, + color_context_scale: ComplexValue, + atv_context_scale: ComplexValue, +) -> RusticolResult<()> { + for anchor in batch.anchors { + let mut left = [ComplexValue::ZERO; 4]; + let mut right = [ComplexValue::ZERO; 4]; + let mut parent0_momentum = [0.0; 4]; + let mut parent1_momentum = [0.0; 4]; + for component in 0..4 { + left[component] = unsafe { + load_current( + arena, + anchor.parent_component_bases[0] + component as u32, + stride, + 0, + ) + }; + right[component] = unsafe { + load_current( + arena, + anchor.parent_component_bases[1] + component as u32, + stride, + 0, + ) + }; + parent0_momentum[component] = unsafe { + load_momentum( + momenta, + anchor.parent_momentum_form_ids[0], + component as u16, + stride, + 0, + ) + }; + parent1_momentum[component] = unsafe { + load_momentum( + momenta, + anchor.parent_momentum_form_ids[1], + component as u16, + stride, + 0, + ) + }; + } + let color_output = + color_ordered_three_vector_loaded(left, right, parent0_momentum, parent1_momentum) + .map(|value| value.mul(color_context_scale)); + let mut atv_outputs = [[ComplexValue::ZERO; 4]; 2]; + let atv_count = match anchor.operands { + DirectInteractionOperands::None => 0, + DirectInteractionOperands::One(operand) => { + atv_outputs[0] = unsafe { + evaluate_interaction_operand( + operand, + left, + right, + arena, + stride, + atv_context_scale, + ) + }; + 1 + } + DirectInteractionOperands::Two(operands) => { + atv_outputs[0] = unsafe { + evaluate_interaction_operand( + operands[0], + left, + right, + arena, + stride, + atv_context_scale, + ) + }; + atv_outputs[1] = unsafe { + evaluate_interaction_operand( + operands[1], + left, + right, + arena, + stride, + atv_context_scale, + ) + }; + 2 + } + }; + let outputs = batch + .outputs + .get(anchor.output_start as usize..anchor.output_end as usize) + .ok_or_else(|| RusticolError::integrity("interaction output range is out of bounds"))?; + for output in outputs { + let color_scale = + unsafe { live_interaction_factor(factors, output.exact_factor_ids[0]) }; + let scaled_color = color_output.map(|value| apply_fanout_scale(value, color_scale)); + let mut combined = if batch.atv_before_color { + [ComplexValue::ZERO; 4] + } else { + scaled_color + }; + for (slot, atv_output) in atv_outputs.iter().take(atv_count).enumerate() { + let scale = + unsafe { live_interaction_factor(factors, output.exact_factor_ids[slot + 1]) }; + for component in 0..4 { + combined[component] = + combined[component].add(apply_fanout_scale(atv_output[component], scale)); + } + } + if batch.atv_before_color { + for component in 0..4 { + combined[component] = combined[component].add(scaled_color[component]); + } + } + for (component, value) in combined.into_iter().enumerate() { + let destination = (output.destination_component_base as usize + component) * stride; + unsafe { + if output.initialize { + *arena.current_re.add(destination) = value.re; + *arena.current_im.add(destination) = value.im; + } else { + *arena.current_re.add(destination) += value.re; + *arena.current_im.add(destination) += value.im; + } + } + } + } + } + Ok(()) +} + +pub(crate) unsafe fn execute_interaction_terminal_closures( + arena: DirectArenaView, + factors: DirectFactorView, + terminal: DirectInteractionTerminalBatch<'_>, +) -> RusticolResult<()> { + if arena.point_stride == 0 + || arena.current_re.is_null() + || arena.current_im.is_null() + || arena.amplitude_re.is_null() + || arena.amplitude_im.is_null() + || factors.values_re.is_null() + || factors.values_im.is_null() + { + return Err(RusticolError::evaluation( + "terminal interaction closure arenas are unavailable", + )); + } + let stride = arena.point_stride as usize; + let required_amplitude_scalars = u64::from(terminal.amplitude_destination_count) + .checked_mul(u64::from(arena.point_stride)) + .ok_or_else(|| RusticolError::evaluation("terminal amplitude range overflows"))?; + if required_amplitude_scalars > arena.amplitude_scalar_len { + return Err(RusticolError::evaluation( + "terminal interaction amplitude arena is too short", + )); + } + for closure in terminal.closures { + let root = unsafe { load_current(arena, closure.root_component_base, stride, 0) }; + let row_factor = unsafe { live_interaction_factor(factors, closure.exact_factor_id) }; + let value = apply_fanout_scale(root, row_factor); + let destination = closure.amplitude_destination_id as usize * stride; + unsafe { + *arena.amplitude_re.add(destination) += value.re; + *arena.amplitude_im.add(destination) += value.im; + } + } + Ok(()) +} + +#[inline(always)] +fn project_terminal_vector( + vector: [ComplexValue; 4], + projection_weights: [ComplexValue; 4], +) -> ComplexValue { + let mut projected = ComplexValue::ZERO; + for component in 0..4 { + projected = projected.add(vector[component].mul(projection_weights[component])); + } + projected +} + +#[inline(always)] +unsafe fn evaluate_interaction_operand( + operand: DirectInteractionOperand, + left: [ComplexValue; 4], + right: [ComplexValue; 4], + arena: DirectArenaView, + stride: usize, + context_scale: ComplexValue, +) -> [ComplexValue; 4] { + let mut tensor = [ComplexValue::ZERO; 6]; + for (component, value) in tensor.iter_mut().enumerate() { + *value = unsafe { + load_current( + arena, + operand.tensor_component_base + component as u32, + stride, + 0, + ) + }; + } + let vector = match operand.vector_parent { + DirectInteractionVectorParent::Parent0 => left, + DirectInteractionVectorParent::Parent1 => right, + }; + antisymmetric_tensor_vector_loaded(tensor, vector).map(|value| value.mul(context_scale)) +} + +fn validate_interaction_runtime_views( + arena: DirectArenaView, + momenta: DirectMomentumView, + parameters: DirectParameterView, + factors: DirectFactorView, + requirements: DirectInteractionRuntimeRequirements, +) -> RusticolResult { + if arena.point_stride == 0 || arena.current_re.is_null() || arena.current_im.is_null() { + return Err(RusticolError::evaluation( + "direct interaction current arena is unavailable", + )); + } + let required_current_scalars = u64::from(requirements.current_component_count) + .checked_mul(u64::from(arena.point_stride)) + .ok_or_else(|| RusticolError::evaluation("direct interaction current arena overflows"))?; + if arena.current_scalar_len < required_current_scalars + || usize::try_from(required_current_scalars).is_err() + { + return Err(RusticolError::evaluation( + "direct interaction current arena is too short", + )); + } + let required_momentum_scalars = u64::from(requirements.momentum_form_count) + .checked_mul(4) + .and_then(|count| count.checked_mul(u64::from(arena.point_stride))) + .ok_or_else(|| RusticolError::evaluation("direct interaction momentum arena overflows"))?; + if momenta.values.is_null() + || momenta.lorentz_component_count != 4 + || momenta.point_stride != arena.point_stride + || momenta.form_count < requirements.momentum_form_count + || momenta.scalar_len < required_momentum_scalars + || usize::try_from(required_momentum_scalars).is_err() + { + return Err(RusticolError::evaluation( + "direct interaction momentum arena does not cover its cold program", + )); + } + if requirements.parameter_count != 0 + && (parameters.values_re.is_null() + || parameters.values_im.is_null() + || parameters.value_count < requirements.parameter_count) + { + return Err(RusticolError::evaluation( + "direct interaction parameter catalog does not cover its cold program", + )); + } + if requirements.factor_count != 0 + && (factors.values_re.is_null() + || factors.values_im.is_null() + || factors.value_count < requirements.factor_count) + { + return Err(RusticolError::evaluation( + "direct interaction factor catalog does not cover its cold program", + )); + } + usize::try_from(arena.point_stride) + .map_err(|_| RusticolError::evaluation("direct interaction point stride exceeds usize")) +} + +#[inline(always)] +unsafe fn live_interaction_factor( + factors: DirectFactorView, + factor_id: u32, +) -> DirectContributionFanoutScale { + DirectContributionFanoutScale::new( + unsafe { *factors.values_re.add(factor_id as usize) }, + unsafe { *factors.values_im.add(factor_id as usize) }, + ) +} + +#[inline(always)] +fn apply_fanout_scale(value: ComplexValue, scale: DirectContributionFanoutScale) -> ComplexValue { + let (re, im) = scale.apply(value.re, value.im); + ComplexValue::new(re, im) +} + +unsafe extern "C" fn execute_weyl_vector_to_weyl_positive_rows( + context: *const c_void, + arena: DirectArenaView, + _momenta: DirectMomentumView, + parameters: DirectParameterView, + factors: DirectFactorView, + rows: *const DirectContributionRow, + row_count: u32, + point_count: u32, +) -> c_int { + unsafe { + execute_rows::( + context, + arena, + parameters, + factors, + rows, + row_count, + point_count, + ) + } +} + +unsafe extern "C" fn execute_weyl_vector_to_weyl_negative_rows( + context: *const c_void, + arena: DirectArenaView, + _momenta: DirectMomentumView, + parameters: DirectParameterView, + factors: DirectFactorView, + rows: *const DirectContributionRow, + row_count: u32, + point_count: u32, +) -> c_int { + unsafe { + execute_rows::( + context, + arena, + parameters, + factors, + rows, + row_count, + point_count, + ) + } +} + +unsafe extern "C" fn execute_antisymmetric_tensor_vector_to_vector_rows( + context: *const c_void, + arena: DirectArenaView, + _momenta: DirectMomentumView, + parameters: DirectParameterView, + factors: DirectFactorView, + rows: *const DirectContributionRow, + row_count: u32, + point_count: u32, +) -> c_int { + unsafe { + execute_rows::( + context, + arena, + parameters, + factors, + rows, + row_count, + point_count, + ) + } +} + +unsafe extern "C" fn execute_vector_wedge_vector_to_antisymmetric_tensor_rows( + context: *const c_void, + arena: DirectArenaView, + _momenta: DirectMomentumView, + parameters: DirectParameterView, + factors: DirectFactorView, + rows: *const DirectContributionRow, + row_count: u32, + point_count: u32, +) -> c_int { + unsafe { + execute_rows::( + context, + arena, + parameters, + factors, + rows, + row_count, + point_count, + ) + } +} + +unsafe extern "C" fn execute_color_ordered_three_vector_rows( + context: *const c_void, + arena: DirectArenaView, + momenta: DirectMomentumView, + parameters: DirectParameterView, + factors: DirectFactorView, + rows: *const DirectContributionRow, + row_count: u32, + point_count: u32, +) -> c_int { + if context.is_null() { + return STATUS_INVALID_CONTEXT; + } + if row_count == 0 + || rows.is_null() + || point_count == 0 + || arena.point_stride == 0 + || point_count > arena.point_stride + || arena.current_re.is_null() + || arena.current_im.is_null() + || momenta.values.is_null() + || momenta.lorentz_component_count != 4 + || momenta.point_stride != arena.point_stride + || point_count > momenta.point_stride + || factors.values_re.is_null() + || factors.values_im.is_null() + { + return STATUS_INVALID_ARGUMENT; + } + let context = unsafe { &*context.cast::() }; + let context_scale = match effective_context_scale(*context, parameters) { + Ok(scale) => scale.mul(ColorOrderedThreeVector::BASE_SCALE), + Err(status) => return status, + }; + let stride = arena.point_stride as usize; + let points = point_count as usize; + + for row_index in 0..row_count as usize { + let row = unsafe { *rows.add(row_index) }; + if row.parent1_component_base_or_sentinel == DIRECT_NONE_U32 + || row.parent0_momentum_form_id == DIRECT_NONE_U32 + || row.parent1_momentum_form_id_or_sentinel == DIRECT_NONE_U32 + { + return STATUS_INVALID_ARGUMENT; + } + if row.exact_factor_id >= factors.value_count + || !component_range_in_bounds( + row.parent0_component_base, + ColorOrderedThreeVector::PARENT_COMPONENTS, + arena.current_scalar_len, + arena.point_stride, + point_count, + ) + || !component_range_in_bounds( + row.parent1_component_base_or_sentinel, + ColorOrderedThreeVector::PARENT_COMPONENTS, + arena.current_scalar_len, + arena.point_stride, + point_count, + ) + || !component_range_in_bounds( + row.destination_component_base, + ColorOrderedThreeVector::DESTINATION_COMPONENTS, + arena.current_scalar_len, + arena.point_stride, + point_count, + ) + || !momentum_form_in_bounds(momenta, row.parent0_momentum_form_id, point_count) + || !momentum_form_in_bounds( + momenta, + row.parent1_momentum_form_id_or_sentinel, + point_count, + ) + { + return STATUS_BOUNDS; + } + let row_factor = ComplexValue::new( + unsafe { *factors.values_re.add(row.exact_factor_id as usize) }, + unsafe { *factors.values_im.add(row.exact_factor_id as usize) }, + ); + let scale = context_scale.mul(row_factor); + let mut point = 0usize; + while point + 1 < points { + unsafe { + ColorOrderedThreeVector::evaluate_pair(arena, momenta, row, stride, point, scale); + } + point += 2; + } + if point < points { + unsafe { + ColorOrderedThreeVector::evaluate_point(arena, momenta, row, stride, point, scale); + } + } + } + DIRECT_STATUS_OK +} + +trait DirectFinalizationFormula { + const COMPONENTS: u16; + + unsafe fn evaluate_point( + arena: DirectArenaView, + momenta: DirectMomentumView, + row: DirectFinalizationRow, + stride: usize, + point: usize, + row_factor: ComplexValue, + ); + + unsafe fn evaluate_pair( + arena: DirectArenaView, + momenta: DirectMomentumView, + row: DirectFinalizationRow, + stride: usize, + point: usize, + row_factor: ComplexValue, + ) { + unsafe { + Self::evaluate_point(arena, momenta, row, stride, point, row_factor); + Self::evaluate_point(arena, momenta, row, stride, point + 1, row_factor); + } + } +} + +struct WeylPropagatorPositive; +struct WeylPropagatorNegative; +struct FeynmanVectorPropagator; + +impl DirectFinalizationFormula for WeylPropagatorPositive { + const COMPONENTS: u16 = 2; + + #[inline(always)] + unsafe fn evaluate_point( + arena: DirectArenaView, + momenta: DirectMomentumView, + row: DirectFinalizationRow, + stride: usize, + point: usize, + row_factor: ComplexValue, + ) { + let c0 = unsafe { load_current(arena, row.component_base, stride, point) }; + let c1 = unsafe { load_current(arena, row.component_base + 1, stride, point) }; + let p0 = unsafe { load_momentum(momenta, row.momentum_form_id, 0, stride, point) }; + let p1 = unsafe { load_momentum(momenta, row.momentum_form_id, 1, stride, point) }; + let p2 = unsafe { load_momentum(momenta, row.momentum_form_id, 2, stride, point) }; + let p3 = unsafe { load_momentum(momenta, row.momentum_form_id, 3, stride, point) }; + let inv = reciprocal_minkowski_norm(p0, p1, p2, p3); + let scale = row_factor.mul_real(inv); + let output0 = c0 + .mul_real(p3) + .mul_i() + .neg() + .sub(c1.mul_real(p1).mul_i()) + .add(c1.mul_real(p2)) + .add(c0.mul_real(p0).mul_i()); + let output1 = c0 + .mul_real(p2) + .neg() + .sub(c0.mul_real(p1).mul_i()) + .add(c1.mul_real(p0).mul_i()) + .add(c1.mul_real(p3).mul_i()); + unsafe { + set_scaled_current(arena, row.component_base, stride, point, output0, scale); + set_scaled_current(arena, row.component_base + 1, stride, point, output1, scale); + } + } + + #[inline(always)] + unsafe fn evaluate_pair( + arena: DirectArenaView, + momenta: DirectMomentumView, + row: DirectFinalizationRow, + stride: usize, + point: usize, + row_factor: ComplexValue, + ) { + let c0 = unsafe { load_current_pair(arena, row.component_base, stride, point) }; + let c1 = unsafe { load_current_pair(arena, row.component_base + 1, stride, point) }; + let p0 = unsafe { load_momentum_pair(momenta, row.momentum_form_id, 0, stride, point) }; + let p1 = unsafe { load_momentum_pair(momenta, row.momentum_form_id, 1, stride, point) }; + let p2 = unsafe { load_momentum_pair(momenta, row.momentum_form_id, 2, stride, point) }; + let p3 = unsafe { load_momentum_pair(momenta, row.momentum_form_id, 3, stride, point) }; + let inv = reciprocal_minkowski_norm_pair(p0, p1, p2, p3); + let scale = row_factor.mul_real_pair(inv); + let output0 = c0 + .mul_real(p3) + .mul_i() + .neg() + .sub(c1.mul_real(p1).mul_i()) + .add(c1.mul_real(p2)) + .add(c0.mul_real(p0).mul_i()); + let output1 = c0 + .mul_real(p2) + .neg() + .sub(c0.mul_real(p1).mul_i()) + .add(c1.mul_real(p0).mul_i()) + .add(c1.mul_real(p3).mul_i()); + unsafe { + set_pre_scaled_current_pair(arena, row.component_base, stride, point, output0, scale); + set_pre_scaled_current_pair( + arena, + row.component_base + 1, + stride, + point, + output1, + scale, + ); + } + } +} + +impl DirectFinalizationFormula for WeylPropagatorNegative { + const COMPONENTS: u16 = 2; + + #[inline(always)] + unsafe fn evaluate_point( + arena: DirectArenaView, + momenta: DirectMomentumView, + row: DirectFinalizationRow, + stride: usize, + point: usize, + row_factor: ComplexValue, + ) { + let c0 = unsafe { load_current(arena, row.component_base, stride, point) }; + let c1 = unsafe { load_current(arena, row.component_base + 1, stride, point) }; + let p0 = unsafe { load_momentum(momenta, row.momentum_form_id, 0, stride, point) }; + let p1 = unsafe { load_momentum(momenta, row.momentum_form_id, 1, stride, point) }; + let p2 = unsafe { load_momentum(momenta, row.momentum_form_id, 2, stride, point) }; + let p3 = unsafe { load_momentum(momenta, row.momentum_form_id, 3, stride, point) }; + let inv = reciprocal_minkowski_norm(p0, p1, p2, p3); + let scale = row_factor.mul_real(inv); + let output0 = c1 + .mul_real(p2) + .neg() + .add(c0.mul_real(p0).mul_i()) + .add(c0.mul_real(p3).mul_i()) + .add(c1.mul_real(p1).mul_i()); + let output1 = c1 + .mul_real(p3) + .mul_i() + .neg() + .add(c0.mul_real(p2)) + .add(c0.mul_real(p1).mul_i()) + .add(c1.mul_real(p0).mul_i()); + unsafe { + set_scaled_current(arena, row.component_base, stride, point, output0, scale); + set_scaled_current(arena, row.component_base + 1, stride, point, output1, scale); + } + } + + #[inline(always)] + unsafe fn evaluate_pair( + arena: DirectArenaView, + momenta: DirectMomentumView, + row: DirectFinalizationRow, + stride: usize, + point: usize, + row_factor: ComplexValue, + ) { + let c0 = unsafe { load_current_pair(arena, row.component_base, stride, point) }; + let c1 = unsafe { load_current_pair(arena, row.component_base + 1, stride, point) }; + let p0 = unsafe { load_momentum_pair(momenta, row.momentum_form_id, 0, stride, point) }; + let p1 = unsafe { load_momentum_pair(momenta, row.momentum_form_id, 1, stride, point) }; + let p2 = unsafe { load_momentum_pair(momenta, row.momentum_form_id, 2, stride, point) }; + let p3 = unsafe { load_momentum_pair(momenta, row.momentum_form_id, 3, stride, point) }; + let inv = reciprocal_minkowski_norm_pair(p0, p1, p2, p3); + let scale = row_factor.mul_real_pair(inv); + let output0 = c1 + .mul_real(p2) + .neg() + .add(c0.mul_real(p0).mul_i()) + .add(c0.mul_real(p3).mul_i()) + .add(c1.mul_real(p1).mul_i()); + let output1 = c1 + .mul_real(p3) + .mul_i() + .neg() + .add(c0.mul_real(p2)) + .add(c0.mul_real(p1).mul_i()) + .add(c1.mul_real(p0).mul_i()); + unsafe { + set_pre_scaled_current_pair(arena, row.component_base, stride, point, output0, scale); + set_pre_scaled_current_pair( + arena, + row.component_base + 1, + stride, + point, + output1, + scale, + ); + } + } +} + +impl DirectFinalizationFormula for FeynmanVectorPropagator { + const COMPONENTS: u16 = 4; + + #[inline(always)] + unsafe fn evaluate_point( + arena: DirectArenaView, + momenta: DirectMomentumView, + row: DirectFinalizationRow, + stride: usize, + point: usize, + row_factor: ComplexValue, + ) { + let p0 = unsafe { load_momentum(momenta, row.momentum_form_id, 0, stride, point) }; + let p1 = unsafe { load_momentum(momenta, row.momentum_form_id, 1, stride, point) }; + let p2 = unsafe { load_momentum(momenta, row.momentum_form_id, 2, stride, point) }; + let p3 = unsafe { load_momentum(momenta, row.momentum_form_id, 3, stride, point) }; + let scale = row_factor + .mul(ComplexValue::new(0.0, -1.0)) + .mul_real(reciprocal_minkowski_norm(p0, p1, p2, p3)); + for component in 0..4u32 { + let value = + unsafe { load_current(arena, row.component_base + component, stride, point) }; + unsafe { + set_scaled_current( + arena, + row.component_base + component, + stride, + point, + value, + scale, + ); + } + } + } + + #[inline(always)] + unsafe fn evaluate_pair( + arena: DirectArenaView, + momenta: DirectMomentumView, + row: DirectFinalizationRow, + stride: usize, + point: usize, + row_factor: ComplexValue, + ) { + let p0 = unsafe { load_momentum_pair(momenta, row.momentum_form_id, 0, stride, point) }; + let p1 = unsafe { load_momentum_pair(momenta, row.momentum_form_id, 1, stride, point) }; + let p2 = unsafe { load_momentum_pair(momenta, row.momentum_form_id, 2, stride, point) }; + let p3 = unsafe { load_momentum_pair(momenta, row.momentum_form_id, 3, stride, point) }; + let scale = row_factor + .mul(ComplexValue::new(0.0, -1.0)) + .mul_real_pair(reciprocal_minkowski_norm_pair(p0, p1, p2, p3)); + for component in 0..4u32 { + let value = + unsafe { load_current_pair(arena, row.component_base + component, stride, point) }; + unsafe { + set_pre_scaled_current_pair( + arena, + row.component_base + component, + stride, + point, + value, + scale, + ); + } + } + } +} + +unsafe extern "C" fn execute_weyl_propagator_positive_rows( + _context: *const c_void, + arena: DirectArenaView, + momenta: DirectMomentumView, + _parameters: DirectParameterView, + factors: DirectFactorView, + rows: *const DirectFinalizationRow, + row_count: u32, + point_count: u32, +) -> c_int { + unsafe { + execute_finalization_rows::( + arena, + momenta, + factors, + rows, + row_count, + point_count, + ) + } +} + +unsafe extern "C" fn execute_weyl_propagator_negative_rows( + _context: *const c_void, + arena: DirectArenaView, + momenta: DirectMomentumView, + _parameters: DirectParameterView, + factors: DirectFactorView, + rows: *const DirectFinalizationRow, + row_count: u32, + point_count: u32, +) -> c_int { + unsafe { + execute_finalization_rows::( + arena, + momenta, + factors, + rows, + row_count, + point_count, + ) + } +} + +unsafe extern "C" fn execute_feynman_vector_propagator_rows( + _context: *const c_void, + arena: DirectArenaView, + momenta: DirectMomentumView, + _parameters: DirectParameterView, + factors: DirectFactorView, + rows: *const DirectFinalizationRow, + row_count: u32, + point_count: u32, +) -> c_int { + unsafe { + execute_finalization_rows::( + arena, + momenta, + factors, + rows, + row_count, + point_count, + ) + } +} + +#[allow(clippy::too_many_arguments)] +unsafe fn execute_rows( + context: *const c_void, + arena: DirectArenaView, + parameters: DirectParameterView, + factors: DirectFactorView, + rows: *const DirectContributionRow, + row_count: u32, + point_count: u32, +) -> c_int { + if context.is_null() { + return STATUS_INVALID_CONTEXT; + } + if row_count == 0 + || rows.is_null() + || point_count == 0 + || arena.point_stride == 0 + || point_count > arena.point_stride + || arena.current_re.is_null() + || arena.current_im.is_null() + || factors.values_re.is_null() + || factors.values_im.is_null() + { + return STATUS_INVALID_ARGUMENT; + } + let context = unsafe { &*context.cast::() }; + let context_scale = match effective_context_scale(*context, parameters) { + Ok(scale) => scale.mul(F::BASE_SCALE), + Err(status) => return status, + }; + let stride = arena.point_stride as usize; + let points = point_count as usize; + + for row_index in 0..row_count as usize { + let row = unsafe { *rows.add(row_index) }; + if row.parent1_component_base_or_sentinel == DIRECT_NONE_U32 { + return STATUS_INVALID_ARGUMENT; + } + if row.exact_factor_id >= factors.value_count + || !component_range_in_bounds( + row.parent0_component_base, + F::PARENT0_COMPONENTS, + arena.current_scalar_len, + arena.point_stride, + point_count, + ) + || !component_range_in_bounds( + row.parent1_component_base_or_sentinel, + F::PARENT1_COMPONENTS, + arena.current_scalar_len, + arena.point_stride, + point_count, + ) + || !component_range_in_bounds( + row.destination_component_base, + F::DESTINATION_COMPONENTS, + arena.current_scalar_len, + arena.point_stride, + point_count, + ) + { + return STATUS_BOUNDS; + } + let row_factor = ComplexValue::new( + unsafe { *factors.values_re.add(row.exact_factor_id as usize) }, + unsafe { *factors.values_im.add(row.exact_factor_id as usize) }, + ); + let scale = context_scale.mul(row_factor); + let mut point = 0usize; + while point + 1 < points { + unsafe { F::evaluate_pair(arena, row, stride, point, scale) }; + point += 2; + } + if point < points { + unsafe { F::evaluate_point(arena, row, stride, point, scale) }; + } + } + DIRECT_STATUS_OK +} + +unsafe fn execute_finalization_rows( + arena: DirectArenaView, + momenta: DirectMomentumView, + factors: DirectFactorView, + rows: *const DirectFinalizationRow, + row_count: u32, + point_count: u32, +) -> c_int { + if row_count == 0 + || rows.is_null() + || point_count == 0 + || arena.point_stride == 0 + || point_count > arena.point_stride + || arena.current_re.is_null() + || arena.current_im.is_null() + || momenta.values.is_null() + || momenta.lorentz_component_count != 4 + || momenta.point_stride != arena.point_stride + || point_count > momenta.point_stride + || factors.values_re.is_null() + || factors.values_im.is_null() + { + return STATUS_INVALID_ARGUMENT; + } + let stride = arena.point_stride as usize; + let points = point_count as usize; + for row_index in 0..row_count as usize { + let row = unsafe { *rows.add(row_index) }; + if row.component_count != F::COMPONENTS + || row.exact_factor_id >= factors.value_count + || !component_range_in_bounds( + row.component_base, + u32::from(F::COMPONENTS), + arena.current_scalar_len, + arena.point_stride, + point_count, + ) + || !momentum_form_in_bounds(momenta, row.momentum_form_id, point_count) + { + return STATUS_BOUNDS; + } + let row_factor = ComplexValue::new( + unsafe { *factors.values_re.add(row.exact_factor_id as usize) }, + unsafe { *factors.values_im.add(row.exact_factor_id as usize) }, + ); + let mut point = 0usize; + while point + 1 < points { + unsafe { F::evaluate_pair(arena, momenta, row, stride, point, row_factor) }; + point += 2; + } + if point < points { + unsafe { F::evaluate_point(arena, momenta, row, stride, point, row_factor) }; + } + } + DIRECT_STATUS_OK +} + +fn effective_context_scale( + context: RecurrenceIntrinsicScale, + parameters: DirectParameterView, +) -> Result { + let literal = ComplexValue::new(context.literal_re, context.literal_im); + let Some(parameter_index) = context.model_parameter_index else { + return Ok(literal); + }; + if parameters.values_re.is_null() + || parameters.values_im.is_null() + || parameter_index >= parameters.value_count + { + return Err(STATUS_BOUNDS); + } + let parameter = ComplexValue::new( + unsafe { *parameters.values_re.add(parameter_index as usize) }, + unsafe { *parameters.values_im.add(parameter_index as usize) }, + ); + Ok(literal.mul(parameter)) +} + +#[inline(always)] +fn reciprocal_minkowski_norm(p0: f64, p1: f64, p2: f64, p3: f64) -> f64 { + 1.0 / (p0 * p0 - p1 * p1 - p2 * p2 - p3 * p3) +} + +#[inline(always)] +fn reciprocal_minkowski_norm_pair(p0: f64x2, p1: f64x2, p2: f64x2, p3: f64x2) -> f64x2 { + let one = f64x2::new([1.0, 1.0]); + one / (p0 * p0 - p1 * p1 - p2 * p2 - p3 * p3) +} + +fn component_range_in_bounds( + component_base: u32, + component_count: u32, + scalar_len: u64, + point_stride: u32, + point_count: u32, +) -> bool { + let Some(last_component) = component_base.checked_add(component_count.saturating_sub(1)) else { + return false; + }; + u64::from(last_component) + .checked_mul(u64::from(point_stride)) + .and_then(|base| base.checked_add(u64::from(point_count))) + .is_some_and(|end| end <= scalar_len) +} + +fn momentum_form_in_bounds( + momenta: DirectMomentumView, + momentum_form_id: u32, + point_count: u32, +) -> bool { + if momentum_form_id >= momenta.form_count || momenta.lorentz_component_count != 4 { + return false; + } + u64::from(momentum_form_id) + .checked_mul(u64::from(momenta.lorentz_component_count)) + .and_then(|plane| plane.checked_add(3)) + .and_then(|plane| plane.checked_mul(u64::from(momenta.point_stride))) + .and_then(|base| base.checked_add(u64::from(point_count))) + .is_some_and(|end| end <= momenta.scalar_len) +} + +#[inline(always)] +fn minkowski_complex_dot(left: [ComplexValue; 4], right: [ComplexValue; 4]) -> ComplexValue { + left[0] + .mul(right[0]) + .sub(left[1].mul(right[1])) + .sub(left[2].mul(right[2])) + .sub(left[3].mul(right[3])) +} + +#[inline(always)] +fn minkowski_complex_real_dot(left: [ComplexValue; 4], right: [f64; 4]) -> ComplexValue { + left[0] + .mul_real(right[0]) + .sub(left[1].mul_real(right[1])) + .sub(left[2].mul_real(right[2])) + .sub(left[3].mul_real(right[3])) +} + +#[inline(always)] +fn minkowski_simd_complex_dot(left: [SimdComplex2; 4], right: [SimdComplex2; 4]) -> SimdComplex2 { + left[0] + .mul(right[0]) + .sub(left[1].mul(right[1])) + .sub(left[2].mul(right[2])) + .sub(left[3].mul(right[3])) +} + +#[inline(always)] +fn minkowski_simd_complex_real_dot(left: [SimdComplex2; 4], right: [f64x2; 4]) -> SimdComplex2 { + left[0] + .mul_real(right[0]) + .sub(left[1].mul_real(right[1])) + .sub(left[2].mul_real(right[2])) + .sub(left[3].mul_real(right[3])) +} + +#[inline(always)] +unsafe fn load_current( + arena: DirectArenaView, + component: u32, + stride: usize, + point: usize, +) -> ComplexValue { + let offset = component as usize * stride + point; + ComplexValue::new(unsafe { *arena.current_re.add(offset) }, unsafe { + *arena.current_im.add(offset) + }) +} + +#[inline(always)] +unsafe fn load_current_pair( + arena: DirectArenaView, + component: u32, + stride: usize, + point: usize, +) -> SimdComplex2 { + let offset = component as usize * stride + point; + SimdComplex2::new( + f64x2::new([unsafe { *arena.current_re.add(offset) }, unsafe { + *arena.current_re.add(offset + 1) + }]), + f64x2::new([unsafe { *arena.current_im.add(offset) }, unsafe { + *arena.current_im.add(offset + 1) + }]), + ) +} + +#[inline(always)] +unsafe fn load_momentum( + momenta: DirectMomentumView, + momentum_form_id: u32, + lorentz_component: u16, + stride: usize, + point: usize, +) -> f64 { + let plane = momentum_form_id as usize * usize::from(momenta.lorentz_component_count) + + usize::from(lorentz_component); + unsafe { *momenta.values.add(plane * stride + point) } +} + +#[inline(always)] +unsafe fn load_momentum_pair( + momenta: DirectMomentumView, + momentum_form_id: u32, + lorentz_component: u16, + stride: usize, + point: usize, +) -> f64x2 { + let plane = momentum_form_id as usize * usize::from(momenta.lorentz_component_count) + + usize::from(lorentz_component); + let offset = plane * stride + point; + f64x2::new([unsafe { *momenta.values.add(offset) }, unsafe { + *momenta.values.add(offset + 1) + }]) +} + +#[inline(always)] +fn contribution_accumulates(row: DirectContributionRow) -> bool { + row.flags & DIRECT_CONTRIBUTION_FLAG_INITIALIZE_DESTINATION == 0 +} + +#[inline(always)] +unsafe fn add_scaled_current( + arena: DirectArenaView, + component: u32, + stride: usize, + point: usize, + value: ComplexValue, + scale: ComplexValue, + accumulate: bool, +) { + let offset = component as usize * stride + point; + let contribution = value.mul(scale); + unsafe { + if accumulate { + *arena.current_re.add(offset) += contribution.re; + *arena.current_im.add(offset) += contribution.im; + } else { + *arena.current_re.add(offset) = contribution.re; + *arena.current_im.add(offset) = contribution.im; + } + } +} + +#[inline(always)] +unsafe fn add_scaled_current_pair( + arena: DirectArenaView, + component: u32, + stride: usize, + point: usize, + value: SimdComplex2, + scale: ComplexValue, + accumulate: bool, +) { + let scale_re = f64x2::new([scale.re, scale.re]); + let scale_im = f64x2::new([scale.im, scale.im]); + let contribution_re = value.re * scale_re - value.im * scale_im; + let contribution_im = value.re * scale_im + value.im * scale_re; + let re = unsafe { core::mem::transmute::(contribution_re) }; + let im = unsafe { core::mem::transmute::(contribution_im) }; + let offset = component as usize * stride + point; + unsafe { + if accumulate { + *arena.current_re.add(offset) += re[0]; + *arena.current_re.add(offset + 1) += re[1]; + *arena.current_im.add(offset) += im[0]; + *arena.current_im.add(offset + 1) += im[1]; + } else { + *arena.current_re.add(offset) = re[0]; + *arena.current_re.add(offset + 1) = re[1]; + *arena.current_im.add(offset) = im[0]; + *arena.current_im.add(offset + 1) = im[1]; + } + } +} + +#[inline(always)] +unsafe fn set_scaled_current( + arena: DirectArenaView, + component: u32, + stride: usize, + point: usize, + value: ComplexValue, + scale: ComplexValue, +) { + let offset = component as usize * stride + point; + let contribution = value.mul(scale); + unsafe { + *arena.current_re.add(offset) = contribution.re; + *arena.current_im.add(offset) = contribution.im; + } +} + +#[inline(always)] +unsafe fn set_pre_scaled_current_pair( + arena: DirectArenaView, + component: u32, + stride: usize, + point: usize, + value: SimdComplex2, + scale: SimdComplex2, +) { + let contribution = value.mul(scale); + let re = unsafe { core::mem::transmute::(contribution.re) }; + let im = unsafe { core::mem::transmute::(contribution.im) }; + let offset = component as usize * stride + point; + unsafe { + *arena.current_re.add(offset) = re[0]; + *arena.current_re.add(offset + 1) = re[1]; + *arena.current_im.add(offset) = im[0]; + *arena.current_im.add(offset + 1) = im[1]; + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::recurrence::DirectClosureRow; + use crate::recurrence::direct_backend::{ + DirectContributionFanoutBundle, DirectContributionFanoutRun, DirectInteractionAnchor, + DirectInteractionOutput, DirectInteractionTensorOutput, DirectInteractionTerminalClosure, + }; + + #[derive(Clone, Copy, Debug)] + struct ReferenceComplex { + re: f64, + im: f64, + } + + impl ReferenceComplex { + fn new(re: f64, im: f64) -> Self { + Self { re, im } + } + + fn add(self, other: Self) -> Self { + Self::new(self.re + other.re, self.im + other.im) + } + + fn sub(self, other: Self) -> Self { + Self::new(self.re - other.re, self.im - other.im) + } + + fn neg(self) -> Self { + Self::new(-self.re, -self.im) + } + + fn mul(self, other: Self) -> Self { + Self::new( + self.re * other.re - self.im * other.im, + self.re * other.im + self.im * other.re, + ) + } + + fn mul_i(self) -> Self { + Self::new(-self.im, self.re) + } + + fn mul_real(self, value: f64) -> Self { + Self::new(self.re * value, self.im * value) + } + } + + fn fill_component(re: &mut [f64], im: &mut [f64], stride: usize, component: usize, seed: f64) { + for point in 0..stride { + re[component * stride + point] = seed + point as f64 * 0.17; + im[component * stride + point] = -0.3 * seed + point as f64 * 0.11; + } + } + + fn read_component( + re: &[f64], + im: &[f64], + stride: usize, + component: usize, + point: usize, + ) -> ReferenceComplex { + ReferenceComplex::new( + re[component * stride + point], + im[component * stride + point], + ) + } + + fn views( + current_re: &mut [f64], + current_im: &mut [f64], + stride: u32, + parameters_re: &[f64], + parameters_im: &[f64], + factors_re: &[f64], + factors_im: &[f64], + ) -> ( + DirectArenaView, + DirectMomentumView, + DirectParameterView, + DirectFactorView, + ) { + ( + DirectArenaView { + current_re: current_re.as_mut_ptr(), + current_im: current_im.as_mut_ptr(), + current_scalar_len: current_re.len() as u64, + amplitude_re: ptr::null_mut(), + amplitude_im: ptr::null_mut(), + amplitude_scalar_len: 0, + point_stride: stride, + }, + DirectMomentumView { + values: ptr::null(), + scalar_len: 0, + form_count: 0, + lorentz_component_count: 4, + point_stride: stride, + }, + DirectParameterView { + values_re: parameters_re.as_ptr(), + values_im: parameters_im.as_ptr(), + value_count: parameters_re.len() as u32, + }, + DirectFactorView { + values_re: factors_re.as_ptr(), + values_im: factors_im.as_ptr(), + value_count: factors_re.len() as u32, + }, + ) + } + + fn effective_test_scale( + literal: ReferenceComplex, + parameter: ReferenceComplex, + factor: ReferenceComplex, + ) -> ReferenceComplex { + literal.mul(parameter).mul(factor) + } + + fn assert_close(actual: ReferenceComplex, expected: ReferenceComplex) { + assert!( + (actual.re - expected.re).abs() < 2.0e-13, + "real mismatch: actual {}, expected {}", + actual.re, + expected.re + ); + assert!( + (actual.im - expected.im).abs() < 2.0e-13, + "imaginary mismatch: actual {}, expected {}", + actual.im, + expected.im + ); + } + + #[test] + fn runtime_template_names_round_trip_and_unknown_names_fail_closed() { + let kinds = [ + RecurrenceContributionIntrinsicKind::WeylVectorToWeylPositive, + RecurrenceContributionIntrinsicKind::WeylVectorToWeylNegative, + RecurrenceContributionIntrinsicKind::AntisymmetricTensorVectorToVector, + RecurrenceContributionIntrinsicKind::VectorWedgeVectorToAntisymmetricTensor, + RecurrenceContributionIntrinsicKind::ColorOrderedThreeVector, + ]; + for kind in kinds { + assert_eq!( + RecurrenceContributionIntrinsicKind::from_runtime_template(kind.runtime_template()) + .unwrap(), + kind + ); + } + assert!( + RecurrenceContributionIntrinsicKind::from_runtime_template( + "rusticol.recurrence.contribution.unknown.v1" + ) + .is_err() + ); + } + + #[test] + fn fused_fanout_matches_supported_intrinsic_rows() { + for kind in [ + RecurrenceContributionIntrinsicKind::AntisymmetricTensorVectorToVector, + RecurrenceContributionIntrinsicKind::VectorWedgeVectorToAntisymmetricTensor, + RecurrenceContributionIntrinsicKind::ColorOrderedThreeVector, + ] { + let initial_re = (0..22) + .map(|component| component as f64 * 0.37 - 1.2) + .collect::>(); + let initial_im = initial_re + .iter() + .map(|value| -0.3 * value) + .collect::>(); + let mut direct_re = initial_re.clone(); + let mut direct_im = initial_im.clone(); + let mut fused_re = initial_re; + let mut fused_im = initial_im; + let factors_re = [0.7, -0.3]; + let factors_im = [0.2, 0.8]; + let parameters_re = [-0.4]; + let parameters_im = [0.6]; + let momenta_values = [1.1, 0.2, -0.4, 0.7, 0.6, -0.8, 0.3, -0.5]; + let first = DirectContributionRow { + parent0_component_base: 0, + parent1_component_base_or_sentinel: 6, + parent0_momentum_form_id: 0, + parent1_momentum_form_id_or_sentinel: 1, + destination_component_base: 10, + exact_factor_id: 0, + selector_domain_id: 0, + flags: DIRECT_CONTRIBUTION_FLAG_INITIALIZE_DESTINATION, + }; + let rows = [ + first, + DirectContributionRow { + destination_component_base: 16, + exact_factor_id: 1, + flags: 0, + ..first + }, + ]; + let executor = LoadedRecurrenceIntrinsicDirectExecutor::load( + kind, + RecurrenceIntrinsicScale::new(0.75, -0.2, Some(0)).unwrap(), + ); + let execute_views = |re: &mut [f64], im: &mut [f64]| { + let (arena, _, parameters, factors) = views( + re, + im, + 1, + ¶meters_re, + ¶meters_im, + &factors_re, + &factors_im, + ); + let momenta = DirectMomentumView { + values: momenta_values.as_ptr(), + scalar_len: 8, + form_count: 2, + lorentz_component_count: 4, + point_stride: 1, + }; + (arena, momenta, parameters, factors) + }; + let (arena, momenta, parameters, factors) = + execute_views(&mut direct_re, &mut direct_im); + let handle = executor.contribution_handle(); + assert_eq!( + unsafe { + (handle.call)( + handle.context, + arena, + momenta, + parameters, + factors, + rows.as_ptr(), + 2, + 1, + ) + }, + DIRECT_STATUS_OK + ); + + let (arena, momenta, parameters, factors) = execute_views(&mut fused_re, &mut fused_im); + let fused = executor.contribution_fanout_handle().unwrap(); + let run = DirectContributionFanoutRun { + row_start: 0, + row_count: 2, + scratch_component_base: 0, + bundle_start: 0, + bundle_count: 2, + }; + let bundles = [ + DirectContributionFanoutBundle { + row_start: 0, + row_count: 1, + effective_factor_id_or_sentinel: 0, + }, + DirectContributionFanoutBundle { + row_start: 1, + row_count: 1, + effective_factor_id_or_sentinel: 1, + }, + ]; + unsafe { + (fused.call)( + fused.context, + arena, + momenta, + parameters, + factors, + DirectContributionFanoutBatch { + rows: &rows, + runs: &[run], + bundles: &bundles, + }, + ) + } + .unwrap(); + if kind == RecurrenceContributionIntrinsicKind::ColorOrderedThreeVector { + assert!( + unsafe { + (fused.call)( + fused.context, + arena, + DirectMomentumView { + lorentz_component_count: 3, + ..momenta + }, + parameters, + factors, + DirectContributionFanoutBatch { + rows: &rows, + runs: &[run], + bundles: &bundles, + }, + ) + } + .is_err() + ); + } + for (actual, expected) in fused_re + .iter() + .zip(&direct_re) + .chain(fused_im.iter().zip(&direct_im)) + { + assert!((actual - expected).abs() < 2.0e-13); + } + } + } + + #[test] + fn identity_finalization_is_backend_neutral_and_bounds_checked() { + let stride = 3usize; + let point_count = 2usize; + let mut current_re = vec![1.0, 2.0, 99.0, 3.0, 4.0, 88.0]; + let mut current_im = vec![5.0, 6.0, 77.0, 7.0, 8.0, 66.0]; + let factors_re = [2.0]; + let factors_im = [-1.0]; + let (arena, momenta, parameters, factors) = views( + &mut current_re, + &mut current_im, + stride as u32, + &[], + &[], + &factors_re, + &factors_im, + ); + let row = DirectFinalizationRow { + component_base: 0, + component_count: 2, + momentum_form_id: 0, + exact_factor_id: 0, + selector_domain_id: 0, + flags: 0, + }; + + assert_eq!( + unsafe { + execute_identity_finalization_rows( + ptr::null(), + arena, + momenta, + parameters, + factors, + ptr::from_ref(&row), + 1, + point_count as u32, + ) + }, + DIRECT_STATUS_OK + ); + assert_eq!(current_re, vec![7.0, 10.0, 99.0, 13.0, 16.0, 88.0]); + assert_eq!(current_im, vec![9.0, 10.0, 77.0, 11.0, 12.0, 66.0]); + + let out_of_bounds = DirectFinalizationRow { + component_base: 1, + component_count: 2, + ..row + }; + assert_eq!( + unsafe { + execute_identity_finalization_rows( + ptr::null(), + arena, + momenta, + parameters, + factors, + ptr::from_ref(&out_of_bounds), + 1, + point_count as u32, + ) + }, + STATUS_BOUNDS + ); + } + + #[test] + fn weyl_vector_chirality_variants_match_reference_math_and_add() { + for kind in [ + RecurrenceContributionIntrinsicKind::WeylVectorToWeylPositive, + RecurrenceContributionIntrinsicKind::WeylVectorToWeylNegative, + ] { + let stride = 5usize; + let mut current_re = vec![0.0; 8 * stride]; + let mut current_im = vec![0.0; 8 * stride]; + for component in 0..6 { + fill_component( + &mut current_re, + &mut current_im, + stride, + component, + 0.4 + component as f64, + ); + } + fill_component(&mut current_re, &mut current_im, stride, 6, 7.0); + fill_component(&mut current_re, &mut current_im, stride, 7, -3.0); + let before_re = current_re.clone(); + let before_im = current_im.clone(); + let parameters_re = [-0.4]; + let parameters_im = [0.3]; + let factors_re = [1.25]; + let factors_im = [-0.5]; + let (arena, momenta, parameters, factors) = views( + &mut current_re, + &mut current_im, + stride as u32, + ¶meters_re, + ¶meters_im, + &factors_re, + &factors_im, + ); + let intrinsic_literal = ReferenceComplex::new(0.75, -0.2) + .mul(ReferenceComplex::new(std::f64::consts::FRAC_1_SQRT_2, 0.0)); + let scale = + RecurrenceIntrinsicScale::new(intrinsic_literal.re, intrinsic_literal.im, Some(0)) + .unwrap(); + let executor = LoadedRecurrenceIntrinsicDirectExecutor::load(kind, scale); + let handle = executor.contribution_handle(); + let row = DirectContributionRow { + parent0_component_base: 0, + parent1_component_base_or_sentinel: 2, + parent0_momentum_form_id: 0, + parent1_momentum_form_id_or_sentinel: 0, + destination_component_base: 6, + exact_factor_id: 0, + selector_domain_id: 0, + flags: 0, + }; + let status = unsafe { + (handle.call)( + handle.context, + arena, + momenta, + parameters, + factors, + ptr::from_ref(&row), + 1, + stride as u32, + ) + }; + assert_eq!(status, DIRECT_STATUS_OK); + + let total_scale = effective_test_scale( + intrinsic_literal, + ReferenceComplex::new(parameters_re[0], parameters_im[0]), + ReferenceComplex::new(factors_re[0], factors_im[0]), + ); + for point in 0..stride { + let l0 = read_component(&before_re, &before_im, stride, 0, point); + let l1 = read_component(&before_re, &before_im, stride, 1, point); + let r0 = read_component(&before_re, &before_im, stride, 2, point); + let r1 = read_component(&before_re, &before_im, stride, 3, point); + let r2 = read_component(&before_re, &before_im, stride, 4, point); + let r3 = read_component(&before_re, &before_im, stride, 5, point); + let output = match kind { + RecurrenceContributionIntrinsicKind::WeylVectorToWeylPositive => [ + l0.mul(r3) + .mul_i() + .neg() + .sub(l1.mul(r1).mul_i()) + .add(l1.mul(r2)) + .add(l0.mul(r0).mul_i()), + l0.mul(r2) + .neg() + .sub(l0.mul(r1).mul_i()) + .add(l1.mul(r0).mul_i()) + .add(l1.mul(r3).mul_i()), + ], + RecurrenceContributionIntrinsicKind::WeylVectorToWeylNegative => [ + l1.mul(r2) + .neg() + .add(l0.mul(r0).mul_i()) + .add(l0.mul(r3).mul_i()) + .add(l1.mul(r1).mul_i()), + l1.mul(r3) + .mul_i() + .neg() + .add(l0.mul(r2)) + .add(l0.mul(r1).mul_i()) + .add(l1.mul(r0).mul_i()), + ], + _ => unreachable!(), + }; + for (component, value) in output.into_iter().enumerate() { + let initial = + read_component(&before_re, &before_im, stride, 6 + component, point); + let expected = initial.add(value.mul(total_scale)); + let actual = + read_component(¤t_re, ¤t_im, stride, 6 + component, point); + assert_close(actual, expected); + } + } + } + } + + #[test] + fn initializing_intrinsic_contribution_overwrites_poisoned_complex_multipoint_destination() { + let stride = 5usize; + let mut current_re = vec![0.0; 8 * stride]; + let mut current_im = vec![0.0; 8 * stride]; + for component in 0..6 { + fill_component( + &mut current_re, + &mut current_im, + stride, + component, + 0.4 + component as f64, + ); + } + let factors_re = [1.25]; + let factors_im = [-0.5]; + let (arena, momenta, parameters, factors) = views( + &mut current_re, + &mut current_im, + stride as u32, + &[], + &[], + &factors_re, + &factors_im, + ); + let executor = LoadedRecurrenceIntrinsicDirectExecutor::load( + RecurrenceContributionIntrinsicKind::WeylVectorToWeylPositive, + RecurrenceIntrinsicScale::new(0.75, -0.2, None).unwrap(), + ); + let handle = executor.contribution_handle(); + let row = DirectContributionRow { + parent0_component_base: 0, + parent1_component_base_or_sentinel: 2, + parent0_momentum_form_id: 0, + parent1_momentum_form_id_or_sentinel: 0, + destination_component_base: 6, + exact_factor_id: 0, + selector_domain_id: 0, + flags: 0, + }; + assert_eq!( + unsafe { + (handle.call)( + handle.context, + arena, + momenta, + parameters, + factors, + ptr::from_ref(&row), + 1, + stride as u32, + ) + }, + DIRECT_STATUS_OK + ); + let expected_re = current_re[6 * stride..].to_vec(); + let expected_im = current_im[6 * stride..].to_vec(); + current_re[6 * stride..].fill(f64::NAN); + current_im[6 * stride..].fill(f64::NAN); + let initializing = DirectContributionRow { + flags: DIRECT_CONTRIBUTION_FLAG_INITIALIZE_DESTINATION, + ..row + }; + assert_eq!( + unsafe { + (handle.call)( + handle.context, + arena, + momenta, + parameters, + factors, + ptr::from_ref(&initializing), + 1, + stride as u32, + ) + }, + DIRECT_STATUS_OK + ); + assert_eq!(¤t_re[6 * stride..], expected_re.as_slice()); + assert_eq!(¤t_im[6 * stride..], expected_im.as_slice()); + assert!(expected_im.iter().any(|value| *value != 0.0)); + } + + #[test] + fn tensor_vector_intrinsic_matches_reference_math() { + let stride = 4usize; + let mut current_re = vec![0.0; 14 * stride]; + let mut current_im = vec![0.0; 14 * stride]; + for component in 0..14 { + fill_component( + &mut current_re, + &mut current_im, + stride, + component, + 0.2 + component as f64 * 0.4, + ); + } + let before_re = current_re.clone(); + let before_im = current_im.clone(); + let factors_re = [0.8]; + let factors_im = [0.1]; + let (arena, momenta, parameters, factors) = views( + &mut current_re, + &mut current_im, + stride as u32, + &[], + &[], + &factors_re, + &factors_im, + ); + let executor = LoadedRecurrenceIntrinsicDirectExecutor::load( + RecurrenceContributionIntrinsicKind::AntisymmetricTensorVectorToVector, + RecurrenceIntrinsicScale::new(0.0, 0.5, None).unwrap(), + ); + let handle = executor.contribution_handle(); + let row = DirectContributionRow { + parent0_component_base: 0, + parent1_component_base_or_sentinel: 6, + parent0_momentum_form_id: 0, + parent1_momentum_form_id_or_sentinel: 0, + destination_component_base: 10, + exact_factor_id: 0, + selector_domain_id: 0, + flags: 0, + }; + assert_eq!( + unsafe { + (handle.call)( + handle.context, + arena, + momenta, + parameters, + factors, + ptr::from_ref(&row), + 1, + stride as u32, + ) + }, + DIRECT_STATUS_OK + ); + + let scale = ReferenceComplex::new(0.0, 0.5) + .mul(ReferenceComplex::new(factors_re[0], factors_im[0])); + for point in 0..stride { + let left = std::array::from_fn::<_, 6, _>(|component| { + read_component(&before_re, &before_im, stride, component, point) + }); + let right = std::array::from_fn::<_, 4, _>(|component| { + read_component(&before_re, &before_im, stride, 6 + component, point) + }); + let output = [ + left[0] + .mul(right[1]) + .add(left[1].mul(right[2])) + .add(left[2].mul(right[3])), + left[0] + .mul(right[0]) + .add(left[3].mul(right[2])) + .add(left[4].mul(right[3])), + left[1] + .mul(right[0]) + .sub(left[3].mul(right[1])) + .add(left[5].mul(right[3])), + left[2] + .mul(right[0]) + .sub(left[4].mul(right[1])) + .sub(left[5].mul(right[2])), + ]; + for (component, value) in output.into_iter().enumerate() { + let initial = read_component(&before_re, &before_im, stride, 10 + component, point); + let actual = + read_component(¤t_re, ¤t_im, stride, 10 + component, point); + assert_close(actual, initial.add(value.mul(scale))); + } + } + } + + #[test] + fn vector_wedge_intrinsic_matches_reference_math() { + let stride = 3usize; + let mut current_re = vec![0.0; 14 * stride]; + let mut current_im = vec![0.0; 14 * stride]; + for component in 0..14 { + fill_component( + &mut current_re, + &mut current_im, + stride, + component, + -0.7 + component as f64 * 0.6, + ); + } + let before_re = current_re.clone(); + let before_im = current_im.clone(); + let factors_re = [1.1]; + let factors_im = [-0.35]; + let (arena, momenta, parameters, factors) = views( + &mut current_re, + &mut current_im, + stride as u32, + &[], + &[], + &factors_re, + &factors_im, + ); + let executor = LoadedRecurrenceIntrinsicDirectExecutor::load_runtime_template( + VECTOR_WEDGE_VECTOR_TO_ANTISYMMETRIC_TENSOR_TEMPLATE, + RecurrenceIntrinsicScale::unit(), + ) + .unwrap(); + let handle = executor.contribution_handle(); + assert!( + executor + .contribution_fanout_handle() + .unwrap() + .interaction_call + .is_none(), + "wedge remains on its ordinary/fanout dispatch path" + ); + let row = DirectContributionRow { + parent0_component_base: 0, + parent1_component_base_or_sentinel: 4, + parent0_momentum_form_id: 0, + parent1_momentum_form_id_or_sentinel: 0, + destination_component_base: 8, + exact_factor_id: 0, + selector_domain_id: 0, + flags: 0, + }; + assert_eq!( + unsafe { + (handle.call)( + handle.context, + arena, + momenta, + parameters, + factors, + ptr::from_ref(&row), + 1, + stride as u32, + ) + }, + DIRECT_STATUS_OK + ); + + let scale = ReferenceComplex::new(factors_re[0], factors_im[0]); + for point in 0..stride { + let left = std::array::from_fn::<_, 4, _>(|component| { + read_component(&before_re, &before_im, stride, component, point) + }); + let right = std::array::from_fn::<_, 4, _>(|component| { + read_component(&before_re, &before_im, stride, 4 + component, point) + }); + let output = [ + left[0].mul(right[1]).sub(left[1].mul(right[0])), + left[0].mul(right[2]).sub(left[2].mul(right[0])), + left[0].mul(right[3]).sub(left[3].mul(right[0])), + left[1].mul(right[2]).sub(left[2].mul(right[1])), + left[1].mul(right[3]).sub(left[3].mul(right[1])), + left[2].mul(right[3]).sub(left[3].mul(right[2])), + ]; + for (component, value) in output.into_iter().enumerate() { + let initial = read_component(&before_re, &before_im, stride, 8 + component, point); + let actual = read_component(¤t_re, ¤t_im, stride, 8 + component, point); + assert_close(actual, initial.add(value.mul(scale))); + } + } + } + + #[test] + fn interaction_bundle_matches_rows_across_order_sides_and_live_mutation() { + let component_count = 56_usize; + let mut seed_re = vec![0.0; component_count]; + let mut seed_im = vec![0.0; component_count]; + for component in 0..component_count { + seed_re[component] = 0.13 + component as f64 * 0.071; + seed_im[component] = -0.29 + component as f64 * 0.043; + } + for component in 40..48 { + seed_re[component] = 91.0; + seed_im[component] = -37.0; + } + let momenta_values = (0..16) + .map(|index| -0.8 + index as f64 * 0.19) + .collect::>(); + let rows = [ + DirectContributionRow { + parent0_component_base: 0, + parent1_component_base_or_sentinel: 4, + parent0_momentum_form_id: 0, + parent1_momentum_form_id_or_sentinel: 1, + destination_component_base: 40, + exact_factor_id: 0, + selector_domain_id: 0, + flags: DIRECT_CONTRIBUTION_FLAG_INITIALIZE_DESTINATION, + }, + DirectContributionRow { + parent0_component_base: 0, + parent1_component_base_or_sentinel: 4, + parent0_momentum_form_id: 0, + parent1_momentum_form_id_or_sentinel: 1, + destination_component_base: 44, + exact_factor_id: 1, + selector_domain_id: 0, + flags: DIRECT_CONTRIBUTION_FLAG_INITIALIZE_DESTINATION, + }, + DirectContributionRow { + parent0_component_base: 8, + parent1_component_base_or_sentinel: 12, + parent0_momentum_form_id: 2, + parent1_momentum_form_id_or_sentinel: 3, + destination_component_base: 40, + exact_factor_id: 2, + selector_domain_id: 0, + flags: 0, + }, + DirectContributionRow { + parent0_component_base: 16, + parent1_component_base_or_sentinel: 0, + parent0_momentum_form_id: 0, + parent1_momentum_form_id_or_sentinel: 0, + destination_component_base: 44, + exact_factor_id: 3, + selector_domain_id: 0, + flags: 0, + }, + DirectContributionRow { + parent0_component_base: 16, + parent1_component_base_or_sentinel: 0, + parent0_momentum_form_id: 0, + parent1_momentum_form_id_or_sentinel: 0, + destination_component_base: 40, + exact_factor_id: 4, + selector_domain_id: 0, + flags: 0, + }, + DirectContributionRow { + parent0_component_base: 24, + parent1_component_base_or_sentinel: 12, + parent0_momentum_form_id: 0, + parent1_momentum_form_id_or_sentinel: 3, + destination_component_base: 40, + exact_factor_id: 5, + selector_domain_id: 0, + flags: 0, + }, + ]; + let anchors = [ + DirectInteractionAnchor { + parent_component_bases: [0, 4], + parent_momentum_form_ids: [0, 1], + operands: DirectInteractionOperands::One(DirectInteractionOperand { + tensor_component_base: 16, + vector_parent: DirectInteractionVectorParent::Parent0, + }), + output_start: 0, + output_end: 2, + tensor_output_start: 0, + tensor_output_end: 0, + }, + DirectInteractionAnchor { + parent_component_bases: [8, 12], + parent_momentum_form_ids: [2, 3], + operands: DirectInteractionOperands::One(DirectInteractionOperand { + tensor_component_base: 24, + vector_parent: DirectInteractionVectorParent::Parent1, + }), + output_start: 2, + output_end: 3, + tensor_output_start: 0, + tensor_output_end: 0, + }, + ]; + let outputs = [ + DirectInteractionOutput { + destination_component_base: 40, + exact_factor_ids: [0, 4, 0], + initialize: true, + }, + DirectInteractionOutput { + destination_component_base: 44, + exact_factor_ids: [1, 3, 0], + initialize: true, + }, + DirectInteractionOutput { + destination_component_base: 40, + exact_factor_ids: [2, 5, 0], + initialize: false, + }, + ]; + let color = LoadedRecurrenceIntrinsicDirectExecutor::load( + RecurrenceContributionIntrinsicKind::ColorOrderedThreeVector, + RecurrenceIntrinsicScale::new(0.73, -0.21, Some(0)).unwrap(), + ); + let atv = LoadedRecurrenceIntrinsicDirectExecutor::load( + RecurrenceContributionIntrinsicKind::AntisymmetricTensorVectorToVector, + RecurrenceIntrinsicScale::new(-0.37, 0.42, Some(0)).unwrap(), + ); + let color_handle = color.contribution_handle(); + let atv_handle = atv.contribution_handle(); + let contexts = DirectInteractionExecutorContexts { + color: color_handle.context, + vector_wedge_vector: ptr::null(), + antisymmetric_tensor_vector: atv_handle.context, + }; + + for atv_before_color in [false, true] { + for (parameters_re, parameters_im, factors_re, factors_im) in [ + ( + vec![0.61], + vec![-0.17], + vec![1.2, -0.8, 0.55, -1.4, 0.72, -0.31], + vec![0.1, 0.35, -0.22, 0.19, -0.44, 0.27], + ), + ( + vec![-0.43], + vec![0.29], + vec![-0.7, 1.6, -1.1, 0.48, -0.36, 1.23], + vec![0.31, -0.27, 0.08, -0.52, 0.41, -0.14], + ), + ] { + let mut expected_re = seed_re.clone(); + let mut expected_im = seed_im.clone(); + let mut candidate_re = seed_re.clone(); + let mut candidate_im = seed_im.clone(); + let parameter_view = DirectParameterView { + values_re: parameters_re.as_ptr(), + values_im: parameters_im.as_ptr(), + value_count: 1, + }; + let factor_view = DirectFactorView { + values_re: factors_re.as_ptr(), + values_im: factors_im.as_ptr(), + value_count: factors_re.len() as u32, + }; + let momentum_view = DirectMomentumView { + values: momenta_values.as_ptr(), + scalar_len: momenta_values.len() as u64, + form_count: 4, + lorentz_component_count: 4, + point_stride: 1, + }; + let arena = |re: &mut [f64], im: &mut [f64]| DirectArenaView { + current_re: re.as_mut_ptr(), + current_im: im.as_mut_ptr(), + current_scalar_len: re.len() as u64, + amplitude_re: ptr::null_mut(), + amplitude_im: ptr::null_mut(), + amplitude_scalar_len: 0, + point_stride: 1, + }; + let mut oracle_rows = rows; + if atv_before_color { + oracle_rows[0].flags = 0; + oracle_rows[1].flags = 0; + oracle_rows[3].flags = DIRECT_CONTRIBUTION_FLAG_INITIALIZE_DESTINATION; + oracle_rows[4].flags = DIRECT_CONTRIBUTION_FLAG_INITIALIZE_DESTINATION; + assert_eq!( + unsafe { + (atv_handle.call)( + atv_handle.context, + arena(&mut expected_re, &mut expected_im), + momentum_view, + parameter_view, + factor_view, + oracle_rows[3..].as_ptr(), + 3, + 1, + ) + }, + DIRECT_STATUS_OK + ); + assert_eq!( + unsafe { + (color_handle.call)( + color_handle.context, + arena(&mut expected_re, &mut expected_im), + momentum_view, + parameter_view, + factor_view, + oracle_rows[..3].as_ptr(), + 3, + 1, + ) + }, + DIRECT_STATUS_OK + ); + } else { + assert_eq!( + unsafe { + (color_handle.call)( + color_handle.context, + arena(&mut expected_re, &mut expected_im), + momentum_view, + parameter_view, + factor_view, + oracle_rows[..3].as_ptr(), + 3, + 1, + ) + }, + DIRECT_STATUS_OK + ); + assert_eq!( + unsafe { + (atv_handle.call)( + atv_handle.context, + arena(&mut expected_re, &mut expected_im), + momentum_view, + parameter_view, + factor_view, + oracle_rows[3..].as_ptr(), + 3, + 1, + ) + }, + DIRECT_STATUS_OK + ); + } + let candidate_pointer = candidate_re.as_ptr(); + unsafe { + execute_interaction_bundle( + contexts, + arena(&mut candidate_re, &mut candidate_im), + momentum_view, + parameter_view, + factor_view, + DirectInteractionBundleBatch { + anchors: &anchors, + outputs: &outputs, + tensor_outputs: &[], + terminal: None, + requirements: DirectInteractionRuntimeRequirements { + current_component_count: 48, + momentum_form_count: 4, + parameter_count: 1, + factor_count: 6, + }, + atv_before_color, + }, + ) + } + .unwrap(); + assert_eq!(candidate_re.as_ptr(), candidate_pointer); + for component in 40..48 { + let error = (candidate_re[component] - expected_re[component]) + .hypot(candidate_im[component] - expected_im[component]); + let scale = candidate_re[component] + .hypot(candidate_im[component]) + .max(expected_re[component].hypot(expected_im[component])) + .max(1.0); + assert!(error <= 2.0e-12 * scale, "component {component}: {error:e}"); + } + } + } + + let mut rejected_re = seed_re.clone(); + let mut rejected_im = seed_im.clone(); + let before_re = rejected_re.clone(); + let before_im = rejected_im.clone(); + let parameters_re = [0.61]; + let parameters_im = [-0.17]; + let short_factors_re = [1.2, -0.8, 0.55, -1.4, 0.72]; + let short_factors_im = [0.1, 0.35, -0.22, 0.19, -0.44]; + let error = unsafe { + execute_interaction_bundle( + contexts, + DirectArenaView { + current_re: rejected_re.as_mut_ptr(), + current_im: rejected_im.as_mut_ptr(), + current_scalar_len: rejected_re.len() as u64, + amplitude_re: ptr::null_mut(), + amplitude_im: ptr::null_mut(), + amplitude_scalar_len: 0, + point_stride: 1, + }, + DirectMomentumView { + values: momenta_values.as_ptr(), + scalar_len: momenta_values.len() as u64, + form_count: 4, + lorentz_component_count: 4, + point_stride: 1, + }, + DirectParameterView { + values_re: parameters_re.as_ptr(), + values_im: parameters_im.as_ptr(), + value_count: 1, + }, + DirectFactorView { + values_re: short_factors_re.as_ptr(), + values_im: short_factors_im.as_ptr(), + value_count: short_factors_re.len() as u32, + }, + DirectInteractionBundleBatch { + anchors: &anchors, + outputs: &outputs, + tensor_outputs: &[], + terminal: None, + requirements: DirectInteractionRuntimeRequirements { + current_component_count: 48, + momentum_form_count: 4, + parameter_count: 1, + factor_count: 6, + }, + atv_before_color: false, + }, + ) + } + .unwrap_err(); + assert!(error.to_string().contains("factor catalog")); + assert_eq!(rejected_re, before_re); + assert_eq!(rejected_im, before_im); + } + + #[test] + fn interaction_bundle_fuses_sibling_wedge_with_live_complex_scales() { + let mut seed_re = vec![0.0; 48]; + let mut seed_im = vec![0.0; 48]; + for component in 0..48 { + seed_re[component] = -0.8 + component as f64 * 0.071; + seed_im[component] = 0.3 - component as f64 * 0.043; + } + let momenta_values = [0.7, -1.1, 0.4, 1.6, -0.2, 0.9, -1.3, 0.5]; + let parameters_re = [0.61]; + let parameters_im = [-0.29]; + let factors_re = [1.2, -0.73, 0.44, -1.31]; + let factors_im = [-0.17, 0.36, 0.28, -0.09]; + let color_rows = [ + DirectContributionRow { + parent0_component_base: 0, + parent1_component_base_or_sentinel: 4, + parent0_momentum_form_id: 0, + parent1_momentum_form_id_or_sentinel: 1, + destination_component_base: 20, + exact_factor_id: 0, + selector_domain_id: 0, + flags: DIRECT_CONTRIBUTION_FLAG_INITIALIZE_DESTINATION, + }, + DirectContributionRow { + exact_factor_id: 1, + flags: 0, + ..DirectContributionRow { + parent0_component_base: 0, + parent1_component_base_or_sentinel: 4, + parent0_momentum_form_id: 0, + parent1_momentum_form_id_or_sentinel: 1, + destination_component_base: 20, + exact_factor_id: 0, + selector_domain_id: 0, + flags: 0, + } + }, + ]; + let atv_rows = [ + DirectContributionRow { + parent0_component_base: 8, + parent1_component_base_or_sentinel: 0, + parent0_momentum_form_id: 0, + parent1_momentum_form_id_or_sentinel: 0, + destination_component_base: 20, + exact_factor_id: 2, + selector_domain_id: 0, + flags: 0, + }, + DirectContributionRow { + exact_factor_id: 3, + ..DirectContributionRow { + parent0_component_base: 8, + parent1_component_base_or_sentinel: 0, + parent0_momentum_form_id: 0, + parent1_momentum_form_id_or_sentinel: 0, + destination_component_base: 20, + exact_factor_id: 2, + selector_domain_id: 0, + flags: 0, + } + }, + ]; + let wedge_rows = [ + DirectContributionRow { + destination_component_base: 40, + exact_factor_id: 0, + flags: DIRECT_CONTRIBUTION_FLAG_INITIALIZE_DESTINATION, + ..color_rows[0] + }, + DirectContributionRow { + destination_component_base: 40, + exact_factor_id: 1, + flags: 0, + ..color_rows[1] + }, + ]; + let color = LoadedRecurrenceIntrinsicDirectExecutor::load( + RecurrenceContributionIntrinsicKind::ColorOrderedThreeVector, + RecurrenceIntrinsicScale::new(0.73, -0.21, Some(0)).unwrap(), + ); + let atv = LoadedRecurrenceIntrinsicDirectExecutor::load( + RecurrenceContributionIntrinsicKind::AntisymmetricTensorVectorToVector, + RecurrenceIntrinsicScale::new(-0.37, 0.42, Some(0)).unwrap(), + ); + let wedge = LoadedRecurrenceIntrinsicDirectExecutor::load( + RecurrenceContributionIntrinsicKind::VectorWedgeVectorToAntisymmetricTensor, + RecurrenceIntrinsicScale::new(0.28, 0.63, Some(0)).unwrap(), + ); + let color_handle = color.contribution_handle(); + let atv_handle = atv.contribution_handle(); + let wedge_handle = wedge.contribution_handle(); + let momentum_view = DirectMomentumView { + values: momenta_values.as_ptr(), + scalar_len: momenta_values.len() as u64, + form_count: 2, + lorentz_component_count: 4, + point_stride: 1, + }; + let parameter_view = DirectParameterView { + values_re: parameters_re.as_ptr(), + values_im: parameters_im.as_ptr(), + value_count: 1, + }; + let factor_view = DirectFactorView { + values_re: factors_re.as_ptr(), + values_im: factors_im.as_ptr(), + value_count: factors_re.len() as u32, + }; + let arena = |re: &mut [f64], im: &mut [f64]| DirectArenaView { + current_re: re.as_mut_ptr(), + current_im: im.as_mut_ptr(), + current_scalar_len: re.len() as u64, + amplitude_re: ptr::null_mut(), + amplitude_im: ptr::null_mut(), + amplitude_scalar_len: 0, + point_stride: 1, + }; + let mut expected_re = seed_re.clone(); + let mut expected_im = seed_im.clone(); + for (handle, rows) in [ + (color_handle, color_rows.as_slice()), + (atv_handle, atv_rows.as_slice()), + (wedge_handle, wedge_rows.as_slice()), + ] { + assert_eq!( + unsafe { + (handle.call)( + handle.context, + arena(&mut expected_re, &mut expected_im), + momentum_view, + parameter_view, + factor_view, + rows.as_ptr(), + rows.len() as u32, + 1, + ) + }, + DIRECT_STATUS_OK + ); + } + + let anchors = [DirectInteractionAnchor { + parent_component_bases: [0, 4], + parent_momentum_form_ids: [0, 1], + operands: DirectInteractionOperands::One(DirectInteractionOperand { + tensor_component_base: 8, + vector_parent: DirectInteractionVectorParent::Parent0, + }), + output_start: 0, + output_end: 2, + tensor_output_start: 0, + tensor_output_end: 2, + }]; + let outputs = [ + DirectInteractionOutput { + destination_component_base: 20, + exact_factor_ids: [0, 2, 0], + initialize: true, + }, + DirectInteractionOutput { + destination_component_base: 20, + exact_factor_ids: [1, 3, 0], + initialize: false, + }, + ]; + let tensor_outputs = [ + DirectInteractionTensorOutput { + destination_component_base: 40, + exact_factor_id: 0, + initialize: true, + }, + DirectInteractionTensorOutput { + destination_component_base: 40, + exact_factor_id: 1, + initialize: false, + }, + ]; + let mut actual_re = seed_re.clone(); + let mut actual_im = seed_im.clone(); + unsafe { + execute_interaction_bundle( + DirectInteractionExecutorContexts { + color: color_handle.context, + vector_wedge_vector: wedge_handle.context, + antisymmetric_tensor_vector: atv_handle.context, + }, + arena(&mut actual_re, &mut actual_im), + momentum_view, + parameter_view, + factor_view, + DirectInteractionBundleBatch { + anchors: &anchors, + outputs: &outputs, + tensor_outputs: &tensor_outputs, + terminal: None, + requirements: DirectInteractionRuntimeRequirements { + current_component_count: 46, + momentum_form_count: 2, + parameter_count: 1, + factor_count: 4, + }, + atv_before_color: false, + }, + ) + } + .unwrap(); + for component in (20..24).chain(40..46) { + let error = (actual_re[component] - expected_re[component]) + .hypot(actual_im[component] - expected_im[component]); + let scale = actual_re[component] + .hypot(actual_im[component]) + .max(expected_re[component].hypot(expected_im[component])) + .max(1.0); + assert!(error <= 2.0e-12 * scale, "component {component}: {error:e}"); + } + } + + #[test] + fn terminal_interaction_scalarization_matches_materialized_closure_replay() { + let mut seed_re = vec![0.0; 40]; + let mut seed_im = vec![0.0; 40]; + for component in 0..40 { + seed_re[component] = 0.17 - component as f64 * 0.039; + seed_im[component] = -0.41 + component as f64 * 0.052; + } + let momenta_values = [0.8, -0.5, 1.2, -1.4, -0.3, 1.1, 0.6, -0.9]; + let parameters_re = [0.57]; + let parameters_im = [0.23]; + let factors_re = [1.0, -1.0, 1.0, -1.0, 1.0, -1.0, 1.0, -1.0, 1.0, -1.0]; + let factors_im = [0.0; 10]; + let closure_rows = [ + DirectClosureRow { + parent0_component_base: 20, + parent1_component_base_or_sentinel: 30, + parent0_momentum_form_id: 0, + parent1_momentum_form_id_or_sentinel: 0, + amplitude_destination_id: 0, + exact_factor_id: 8, + component_factor_start: 4, + component_count: 4, + selector_domain_id: 0, + flags: 0, + }, + DirectClosureRow { + amplitude_destination_id: 1, + exact_factor_id: 9, + ..DirectClosureRow { + parent0_component_base: 20, + parent1_component_base_or_sentinel: 30, + parent0_momentum_form_id: 0, + parent1_momentum_form_id_or_sentinel: 0, + amplitude_destination_id: 0, + exact_factor_id: 8, + component_factor_start: 4, + component_count: 4, + selector_domain_id: 0, + flags: 0, + } + }, + ]; + let terminal_closures = [ + DirectInteractionTerminalClosure { + root_component_base: 20, + amplitude_destination_id: 0, + exact_factor_id: 8, + }, + DirectInteractionTerminalClosure { + root_component_base: 20, + amplitude_destination_id: 1, + exact_factor_id: 9, + }, + ]; + let terminal = DirectInteractionTerminalBatch { + partner_component_base: 30, + component_factor_start: 4, + amplitude_destination_count: 2, + closures: &terminal_closures, + }; + let anchor = [DirectInteractionAnchor { + parent_component_bases: [0, 4], + parent_momentum_form_ids: [0, 1], + operands: DirectInteractionOperands::One(DirectInteractionOperand { + tensor_component_base: 8, + vector_parent: DirectInteractionVectorParent::Parent0, + }), + output_start: 0, + output_end: 2, + tensor_output_start: 0, + tensor_output_end: 0, + }]; + let outputs = [ + DirectInteractionOutput { + destination_component_base: 20, + exact_factor_ids: [0, 2, 0], + initialize: true, + }, + DirectInteractionOutput { + destination_component_base: 20, + exact_factor_ids: [1, 3, 0], + initialize: false, + }, + ]; + let color = LoadedRecurrenceIntrinsicDirectExecutor::load( + RecurrenceContributionIntrinsicKind::ColorOrderedThreeVector, + RecurrenceIntrinsicScale::new(0.0, 0.69, None).unwrap(), + ); + let atv = LoadedRecurrenceIntrinsicDirectExecutor::load( + RecurrenceContributionIntrinsicKind::AntisymmetricTensorVectorToVector, + RecurrenceIntrinsicScale::new(0.0, -0.41, None).unwrap(), + ); + let color_handle = color.contribution_handle(); + let atv_handle = atv.contribution_handle(); + let contexts = DirectInteractionExecutorContexts { + color: color_handle.context, + vector_wedge_vector: ptr::null(), + antisymmetric_tensor_vector: atv_handle.context, + }; + let momentum_view = DirectMomentumView { + values: momenta_values.as_ptr(), + scalar_len: momenta_values.len() as u64, + form_count: 2, + lorentz_component_count: 4, + point_stride: 1, + }; + let parameter_view = DirectParameterView { + values_re: parameters_re.as_ptr(), + values_im: parameters_im.as_ptr(), + value_count: 1, + }; + let factor_view = DirectFactorView { + values_re: factors_re.as_ptr(), + values_im: factors_im.as_ptr(), + value_count: factors_re.len() as u32, + }; + let arena = |re: &mut [f64], + im: &mut [f64], + amplitude_re: &mut [f64], + amplitude_im: &mut [f64]| + -> DirectArenaView { + DirectArenaView { + current_re: re.as_mut_ptr(), + current_im: im.as_mut_ptr(), + current_scalar_len: re.len() as u64, + amplitude_re: amplitude_re.as_mut_ptr(), + amplitude_im: amplitude_im.as_mut_ptr(), + amplitude_scalar_len: amplitude_re.len() as u64, + point_stride: 1, + } + }; + let requirements = DirectInteractionRuntimeRequirements { + current_component_count: 34, + momentum_form_count: 2, + parameter_count: 0, + factor_count: factors_re.len() as u32, + }; + + let mut expected_re = seed_re.clone(); + let mut expected_im = seed_im.clone(); + let mut expected_amplitude_re = vec![0.13, -0.27]; + let mut expected_amplitude_im = vec![-0.19, 0.08]; + unsafe { + execute_interaction_bundle( + contexts, + arena( + &mut expected_re, + &mut expected_im, + &mut expected_amplitude_re, + &mut expected_amplitude_im, + ), + momentum_view, + parameter_view, + factor_view, + DirectInteractionBundleBatch { + anchors: &anchor, + outputs: &outputs, + tensor_outputs: &[], + terminal: None, + requirements, + atv_before_color: false, + }, + ) + } + .unwrap(); + assert_eq!( + unsafe { + crate::engine::execute_closure_reduce_rows( + ptr::null(), + arena( + &mut expected_re, + &mut expected_im, + &mut expected_amplitude_re, + &mut expected_amplitude_im, + ), + momentum_view, + parameter_view, + factor_view, + closure_rows.as_ptr(), + closure_rows.len() as u32, + 1, + ) + }, + DIRECT_STATUS_OK + ); + + let mut actual_re = seed_re.clone(); + let mut actual_im = seed_im.clone(); + let mut actual_amplitude_re = vec![0.13, -0.27]; + let mut actual_amplitude_im = vec![-0.19, 0.08]; + let actual_arena = arena( + &mut actual_re, + &mut actual_im, + &mut actual_amplitude_re, + &mut actual_amplitude_im, + ); + unsafe { + execute_interaction_bundle( + contexts, + actual_arena, + momentum_view, + parameter_view, + factor_view, + DirectInteractionBundleBatch { + anchors: &anchor, + outputs: &outputs, + tensor_outputs: &[], + terminal: Some(terminal), + requirements, + atv_before_color: false, + }, + ) + } + .unwrap(); + unsafe { execute_interaction_terminal_closures(actual_arena, factor_view, terminal) } + .unwrap(); + + for destination in 0..2 { + let error = (actual_amplitude_re[destination] - expected_amplitude_re[destination]) + .hypot(actual_amplitude_im[destination] - expected_amplitude_im[destination]); + let scale = actual_amplitude_re[destination] + .hypot(actual_amplitude_im[destination]) + .max(expected_amplitude_re[destination].hypot(expected_amplitude_im[destination])) + .max(1.0); + assert!( + error <= 2.0e-12 * scale, + "destination {destination}: {error:e}" + ); + } + } + + #[test] + fn color_ordered_three_vector_matches_reference_math_and_accumulates() { + let stride = 5usize; + let point_count = 4usize; + let mut current_re = vec![0.0; 12 * stride]; + let mut current_im = vec![0.0; 12 * stride]; + for component in 0..12 { + fill_component( + &mut current_re, + &mut current_im, + stride, + component, + -0.9 + component as f64 * 0.37, + ); + } + let before_re = current_re.clone(); + let before_im = current_im.clone(); + let mut momentum_values = vec![0.0; 2 * 4 * stride]; + for form in 0..2 { + for component in 0..4 { + for point in 0..stride { + momentum_values[(form * 4 + component) * stride + point] = + 0.23 + form as f64 * 1.7 - component as f64 * 0.41 + point as f64 * 0.19; + } + } + } + let parameters_re = [-0.35]; + let parameters_im = [0.8]; + let factors_re = [1.2]; + let factors_im = [0.25]; + let (arena, _, parameters, factors) = views( + &mut current_re, + &mut current_im, + stride as u32, + ¶meters_re, + ¶meters_im, + &factors_re, + &factors_im, + ); + let momenta = DirectMomentumView { + values: momentum_values.as_ptr(), + scalar_len: momentum_values.len() as u64, + form_count: 2, + lorentz_component_count: 4, + point_stride: stride as u32, + }; + let literal = ReferenceComplex::new(0.45, -0.7); + let executor = LoadedRecurrenceIntrinsicDirectExecutor::load_runtime_template( + COLOR_ORDERED_THREE_VECTOR_TEMPLATE, + RecurrenceIntrinsicScale::new(literal.re, literal.im, Some(0)).unwrap(), + ) + .unwrap(); + let handle = executor.contribution_handle(); + let row = DirectContributionRow { + parent0_component_base: 0, + parent1_component_base_or_sentinel: 4, + parent0_momentum_form_id: 0, + parent1_momentum_form_id_or_sentinel: 1, + destination_component_base: 8, + exact_factor_id: 0, + selector_domain_id: 0, + flags: 0, + }; + assert_eq!( + unsafe { + (handle.call)( + handle.context, + arena, + momenta, + parameters, + factors, + ptr::from_ref(&row), + 1, + point_count as u32, + ) + }, + DIRECT_STATUS_OK + ); + + let alpha = effective_test_scale( + literal, + ReferenceComplex::new(parameters_re[0], parameters_im[0]), + ReferenceComplex::new(factors_re[0], factors_im[0]), + ); + for point in 0..point_count { + let left = std::array::from_fn::<_, 4, _>(|component| { + read_component(&before_re, &before_im, stride, component, point) + }); + let right = std::array::from_fn::<_, 4, _>(|component| { + read_component(&before_re, &before_im, stride, 4 + component, point) + }); + let parent0_momentum = std::array::from_fn::<_, 4, _>(|component| { + momentum_values[component * stride + point] + }); + let parent1_momentum = std::array::from_fn::<_, 4, _>(|component| { + momentum_values[(4 + component) * stride + point] + }); + let vector_dot = left[0] + .mul(right[0]) + .sub(left[1].mul(right[1])) + .sub(left[2].mul(right[2])) + .sub(left[3].mul(right[3])); + let left_dot_parent1 = left[0] + .mul_real(parent1_momentum[0]) + .sub(left[1].mul_real(parent1_momentum[1])) + .sub(left[2].mul_real(parent1_momentum[2])) + .sub(left[3].mul_real(parent1_momentum[3])); + let right_dot_parent0 = right[0] + .mul_real(parent0_momentum[0]) + .sub(right[1].mul_real(parent0_momentum[1])) + .sub(right[2].mul_real(parent0_momentum[2])) + .sub(right[3].mul_real(parent0_momentum[3])); + + for component in 0..4 { + let reference = vector_dot + .mul_real(parent0_momentum[component] - parent1_momentum[component]) + .add( + left_dot_parent1 + .mul(right[component]) + .sub(right_dot_parent0.mul(left[component])) + .mul_real(2.0), + ); + let initial = read_component(&before_re, &before_im, stride, 8 + component, point); + let actual = read_component(¤t_re, ¤t_im, stride, 8 + component, point); + assert_close(actual, initial.add(reference.mul(alpha))); + } + } + for component in 8..12 { + assert_close( + read_component(¤t_re, ¤t_im, stride, component, point_count), + read_component(&before_re, &before_im, stride, component, point_count), + ); + } + + let mut expected = Vec::new(); + for component in 8..12 { + for point in 0..point_count { + expected.push( + read_component(¤t_re, ¤t_im, stride, component, point).sub( + read_component(&before_re, &before_im, stride, component, point), + ), + ); + } + } + for component in 8..12 { + let start = component * stride; + current_re[start..start + point_count].fill(f64::NAN); + current_im[start..start + point_count].fill(f64::NAN); + } + let initializing = DirectContributionRow { + flags: DIRECT_CONTRIBUTION_FLAG_INITIALIZE_DESTINATION, + ..row + }; + assert_eq!( + unsafe { + (handle.call)( + handle.context, + arena, + momenta, + parameters, + factors, + ptr::from_ref(&initializing), + 1, + point_count as u32, + ) + }, + DIRECT_STATUS_OK + ); + for (index, (component, point)) in (8..12) + .flat_map(|component| (0..point_count).map(move |point| (component, point))) + .enumerate() + { + assert_close( + read_component(¤t_re, ¤t_im, stride, component, point), + expected[index], + ); + } + } + + #[test] + fn color_ordered_three_vector_rejects_malformed_momentum_views_and_rows() { + let stride = 3usize; + let mut current_re = vec![0.0; 12 * stride]; + let mut current_im = vec![0.0; 12 * stride]; + let momentum_values = vec![0.0; 2 * 4 * stride]; + let factors_re = [1.0]; + let factors_im = [0.0]; + let (arena, _, parameters, factors) = views( + &mut current_re, + &mut current_im, + stride as u32, + &[], + &[], + &factors_re, + &factors_im, + ); + let valid_momenta = DirectMomentumView { + values: momentum_values.as_ptr(), + scalar_len: momentum_values.len() as u64, + form_count: 2, + lorentz_component_count: 4, + point_stride: stride as u32, + }; + let executor = LoadedRecurrenceIntrinsicDirectExecutor::load( + RecurrenceContributionIntrinsicKind::ColorOrderedThreeVector, + RecurrenceIntrinsicScale::unit(), + ); + let handle = executor.contribution_handle(); + let valid_row = DirectContributionRow { + parent0_component_base: 0, + parent1_component_base_or_sentinel: 4, + parent0_momentum_form_id: 0, + parent1_momentum_form_id_or_sentinel: 1, + destination_component_base: 8, + exact_factor_id: 0, + selector_domain_id: 0, + flags: 0, + }; + let call = |momenta: DirectMomentumView, row: DirectContributionRow, point_count: u32| unsafe { + (handle.call)( + handle.context, + arena, + momenta, + parameters, + factors, + ptr::from_ref(&row), + 1, + point_count, + ) + }; + + assert_eq!( + call( + DirectMomentumView { + values: ptr::null(), + ..valid_momenta + }, + valid_row, + stride as u32, + ), + STATUS_INVALID_ARGUMENT + ); + assert_eq!( + call( + DirectMomentumView { + lorentz_component_count: 3, + ..valid_momenta + }, + valid_row, + stride as u32, + ), + STATUS_INVALID_ARGUMENT + ); + assert_eq!( + call( + DirectMomentumView { + point_stride: stride as u32 + 1, + ..valid_momenta + }, + valid_row, + stride as u32, + ), + STATUS_INVALID_ARGUMENT + ); + assert_eq!( + call( + valid_momenta, + DirectContributionRow { + parent0_momentum_form_id: DIRECT_NONE_U32, + ..valid_row + }, + stride as u32, + ), + STATUS_INVALID_ARGUMENT + ); + assert_eq!( + call( + valid_momenta, + DirectContributionRow { + parent1_momentum_form_id_or_sentinel: DIRECT_NONE_U32, + ..valid_row + }, + stride as u32, + ), + STATUS_INVALID_ARGUMENT + ); + assert_eq!( + call( + valid_momenta, + DirectContributionRow { + parent1_momentum_form_id_or_sentinel: 2, + ..valid_row + }, + stride as u32, + ), + STATUS_BOUNDS + ); + assert_eq!( + call( + DirectMomentumView { + scalar_len: valid_momenta.scalar_len - 1, + ..valid_momenta + }, + valid_row, + stride as u32, + ), + STATUS_BOUNDS + ); + assert_eq!( + call(valid_momenta, valid_row, stride as u32 + 1), + STATUS_INVALID_ARGUMENT + ); + } + + #[test] + fn callback_rejects_invalid_context_arguments_and_bounds() { + let stride = 2usize; + let mut current_re = vec![0.0; 8 * stride]; + let mut current_im = vec![0.0; 8 * stride]; + let factors_re = [1.0]; + let factors_im = [0.0]; + let (arena, momenta, parameters, factors) = views( + &mut current_re, + &mut current_im, + stride as u32, + &[], + &[], + &factors_re, + &factors_im, + ); + let executor = LoadedRecurrenceIntrinsicDirectExecutor::load( + RecurrenceContributionIntrinsicKind::WeylVectorToWeylPositive, + RecurrenceIntrinsicScale::unit(), + ); + let handle = executor.contribution_handle(); + let valid_row = DirectContributionRow { + parent0_component_base: 0, + parent1_component_base_or_sentinel: 2, + parent0_momentum_form_id: 0, + parent1_momentum_form_id_or_sentinel: 0, + destination_component_base: 6, + exact_factor_id: 0, + selector_domain_id: 0, + flags: 0, + }; + assert_eq!( + unsafe { + (handle.call)( + ptr::null(), + arena, + momenta, + parameters, + factors, + ptr::from_ref(&valid_row), + 1, + stride as u32, + ) + }, + STATUS_INVALID_CONTEXT + ); + assert_eq!( + unsafe { + (handle.call)( + handle.context, + arena, + momenta, + parameters, + factors, + ptr::from_ref(&valid_row), + 1, + stride as u32 + 1, + ) + }, + STATUS_INVALID_ARGUMENT + ); + + let missing_parent = DirectContributionRow { + parent1_component_base_or_sentinel: DIRECT_NONE_U32, + ..valid_row + }; + assert_eq!( + unsafe { + (handle.call)( + handle.context, + arena, + momenta, + parameters, + factors, + ptr::from_ref(&missing_parent), + 1, + stride as u32, + ) + }, + STATUS_INVALID_ARGUMENT + ); + + let bad_factor = DirectContributionRow { + exact_factor_id: 1, + ..valid_row + }; + assert_eq!( + unsafe { + (handle.call)( + handle.context, + arena, + momenta, + parameters, + factors, + ptr::from_ref(&bad_factor), + 1, + stride as u32, + ) + }, + STATUS_BOUNDS + ); + + let bad_destination = DirectContributionRow { + destination_component_base: 7, + ..valid_row + }; + assert_eq!( + unsafe { + (handle.call)( + handle.context, + arena, + momenta, + parameters, + factors, + ptr::from_ref(&bad_destination), + 1, + stride as u32, + ) + }, + STATUS_BOUNDS + ); + } + + #[test] + fn optional_parameter_index_is_checked_before_row_execution() { + let stride = 2usize; + let mut current_re = vec![0.0; 8 * stride]; + let mut current_im = vec![0.0; 8 * stride]; + let factors_re = [1.0]; + let factors_im = [0.0]; + let (arena, momenta, parameters, factors) = views( + &mut current_re, + &mut current_im, + stride as u32, + &[], + &[], + &factors_re, + &factors_im, + ); + let executor = LoadedRecurrenceIntrinsicDirectExecutor::load( + RecurrenceContributionIntrinsicKind::WeylVectorToWeylNegative, + RecurrenceIntrinsicScale::new(1.0, 0.0, Some(0)).unwrap(), + ); + let handle = executor.contribution_handle(); + let row = DirectContributionRow { + parent0_component_base: 0, + parent1_component_base_or_sentinel: 2, + parent0_momentum_form_id: 0, + parent1_momentum_form_id_or_sentinel: 0, + destination_component_base: 6, + exact_factor_id: 0, + selector_domain_id: 0, + flags: 0, + }; + assert_eq!( + unsafe { + (handle.call)( + handle.context, + arena, + momenta, + parameters, + factors, + ptr::from_ref(&row), + 1, + stride as u32, + ) + }, + STATUS_BOUNDS + ); + } + + #[test] + fn scale_constructor_rejects_nonfinite_values_and_reserved_parameter_index() { + assert!(RecurrenceIntrinsicScale::new(f64::NAN, 0.0, None).is_err()); + assert!(RecurrenceIntrinsicScale::new(1.0, f64::INFINITY, None).is_err()); + assert!(RecurrenceIntrinsicScale::new(1.0, 0.0, Some(DIRECT_NONE_U32)).is_err()); + } +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/evaluator/recurrence_source_direct.rs b/pyamplicol/rust/crates/rusticol-core/src/evaluator/recurrence_source_direct.rs new file mode 100644 index 0000000000..2c8fc31155 --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/evaluator/recurrence_source_direct.rs @@ -0,0 +1,2480 @@ +// SPDX-License-Identifier: 0BSD + +//! Model-generic SourceIR execution for Direct-Arena recurrence plans. +//! +//! The loader resolves SourceIR crossing semantics once into immutable +//! dispatch domains. Momentum crossing is already represented by each direct +//! row's momentum form, and its exact factor owns the crossing phase. The hot +//! call therefore reads one crossed momentum, evaluates the established +//! Rusticol source formula, multiplies the row factor, and writes directly to +//! persistent split-complex current planes. + +#[cfg(not(feature = "symbolica-runtime"))] +use num_complex::Complex; +#[cfg(feature = "symbolica-runtime")] +use symbolica::prelude::Complex; + +use crate::recurrence::direct_backend::{ + DIRECT_STATUS_OK, DirectArenaView, DirectFactorView, DirectMomentumView, DirectParameterView, + DirectResolvedSourceSelection, DirectSourceDispatchVariantDescriptor, DirectSourceEmbeddingRow, + DirectSourceExecutor, DirectUnionSourceDispatchHandle, +}; +use crate::recurrence::on_the_fly::{ + OnTheFlyBoundUnionSourceProgram, OnTheFlyUnionSourceBindingView, +}; +use crate::recurrence::{DIRECT_NONE_U32, DirectSourceRow, ExactComplexRational}; +use crate::{RusticolError, RusticolResult}; +use std::ffi::{c_int, c_void}; +use std::ptr; + +// Reuse the established generic source formulas instead of maintaining a +// second set of physics expressions. +use crate::engine::wavefunctions as source_wavefunctions; + +const STATUS_INVALID_CONTEXT: c_int = 1; +const STATUS_INVALID_ARGUMENT: c_int = 2; +const STATUS_EXECUTION_FAILED: c_int = 3; +const STATUS_BOUNDS: c_int = 4; + +/// Source families currently supported by the canonical SourceIR executor. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) enum DirectSourceWavefunctionFamily { + Scalar, + WeylFermion, + DiracFermion, + Vector, + Spin2, +} + +impl DirectSourceWavefunctionFamily { + const fn component_count(self) -> u16 { + match self { + Self::Scalar => 1, + Self::WeylFermion => 2, + Self::DiracFermion | Self::Vector => 4, + Self::Spin2 => 16, + } + } +} + +/// Particle orientation needed by fermion source formulas. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) enum DirectSourceOrientation { + Particle, + Antiparticle, + SelfConjugate, +} + +/// One crossed SourceIR state ready for direct execution. +/// +/// `helicity` and `chirality` are the effective values after SourceIR +/// crossing. `mass_parameter_index` addresses the prepared parameter view. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) struct DirectSourceTemplateSpec { + pub(crate) spin_state_class: i32, + pub(crate) family: DirectSourceWavefunctionFamily, + pub(crate) orientation: DirectSourceOrientation, + pub(crate) helicity: i32, + pub(crate) chirality: i32, + pub(crate) mass_parameter_index: Option, +} + +impl DirectSourceTemplateSpec { + fn validate(self) -> RusticolResult<()> { + if matches!( + self.family, + DirectSourceWavefunctionFamily::WeylFermion + | DirectSourceWavefunctionFamily::DiracFermion + ) && self.orientation == DirectSourceOrientation::SelfConjugate + { + return Err(RusticolError::invalid_argument( + "direct recurrence SourceIR fermion cannot use self-conjugate orientation", + )); + } + match self.family { + DirectSourceWavefunctionFamily::Scalar if self.helicity != 0 => { + return Err(RusticolError::invalid_argument( + "direct recurrence scalar source helicity must be zero", + )); + } + DirectSourceWavefunctionFamily::WeylFermion + | DirectSourceWavefunctionFamily::DiracFermion + if !matches!(self.helicity, -1 | 1) => + { + return Err(RusticolError::invalid_argument( + "direct recurrence fermion source helicity must be -1 or +1", + )); + } + DirectSourceWavefunctionFamily::WeylFermion if !matches!(self.chirality, -1 | 1) => { + return Err(RusticolError::invalid_argument( + "direct recurrence Weyl source chirality must be -1 or +1", + )); + } + DirectSourceWavefunctionFamily::Vector if !matches!(self.helicity, -1..=1) => { + return Err(RusticolError::invalid_argument( + "direct recurrence vector source helicity must be -1, 0, or +1", + )); + } + DirectSourceWavefunctionFamily::Spin2 if !matches!(self.helicity, -2..=2) => { + return Err(RusticolError::invalid_argument( + "direct recurrence spin-2 source helicity must be between -2 and +2", + )); + } + _ => {} + } + Ok(()) + } +} + +/// One row-addressable source-template or runtime-dispatch domain. +#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] +pub(crate) enum DirectSourceDispatchKey { + SpinStateClass(i32), + RuntimeVariant { + source_row_id: u32, + runtime_variant_id: u32, + }, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) struct DirectSourceDispatchVariantSpec { + pub(crate) key: DirectSourceDispatchKey, + pub(crate) template: DirectSourceTemplateSpec, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub(crate) struct DirectSourceDispatchDomainSpec { + pub(crate) variants: Vec, +} + +struct DirectSourceDispatchDomain { + variants: Box<[DirectSourceDispatchVariantSpec]>, +} + +impl DirectSourceDispatchDomain { + fn resolve(&self, key: DirectSourceDispatchKey) -> Option { + self.variants + .binary_search_by_key(&key, |variant| variant.key) + .ok() + .map(|index| self.variants[index].template) + } +} + +struct DirectSourceExecutorContext { + domains: Box<[DirectSourceDispatchDomain]>, +} + +#[derive(Clone, Copy)] +struct BoundUnionSourceEmbedding { + source_component_or_sentinel: u32, + scale_re: f64, + scale_im: f64, +} + +#[derive(Clone, Copy)] +struct BoundUnionSourceRow { + template: DirectSourceTemplateSpec, + destination_component_base: u32, + momentum_form_id: u32, + embedding_start: u32, + embedding_count: u32, +} + +struct LoadedBoundUnionSourceProgram { + rows: Box<[BoundUnionSourceRow]>, + embeddings: Box<[BoundUnionSourceEmbedding]>, + current_component_count: u32, + momentum_form_count: u32, + parameter_count: u32, +} + +fn source_binding_integrity(message: impl Into) -> RusticolError { + RusticolError::integrity(format!( + "cold-bound recurrence union source: {}", + message.into() + )) +} + +fn exact_factor_parts(value: ExactComplexRational) -> RusticolResult<(f64, f64)> { + let real = value.real().numerator() as f64 / value.real().denominator() as f64; + let imag = value.imag().numerator() as f64 / value.imag().denominator() as f64; + if !real.is_finite() || !imag.is_finite() { + return Err(source_binding_integrity( + "exact factor is not finite in binary64", + )); + } + Ok((real, imag)) +} + +/// Context-aware source handle published into the Direct-Arena catalog. +#[derive(Clone, Copy)] +pub(crate) struct ContextDirectSourceExecutorHandle { + pub(crate) call: DirectSourceExecutor, + pub(crate) context: *const c_void, +} + +/// Owns the immutable context addressed by a source handle. +pub(crate) struct LoadedDirectSourceExecutor { + context: Box, +} + +impl LoadedDirectSourceExecutor { + /// Build and authenticate source dispatch domains at load time. + /// + /// Domain indices must match `DirectSourceRow:: + /// source_template_or_dispatch_domain`. Singleton domains represent fixed + /// topology-replay templates; multi-variant domains support runtime + /// helicity dispatch without changing the hot-call ABI. + pub(crate) fn load(mut domains: Vec) -> RusticolResult { + if domains.is_empty() { + return Err(RusticolError::invalid_argument( + "direct recurrence source catalog must contain at least one dispatch domain", + )); + } + let mut loaded = Vec::with_capacity(domains.len()); + for (domain_index, domain) in domains.iter_mut().enumerate() { + if domain.variants.is_empty() { + return Err(RusticolError::invalid_argument(format!( + "direct recurrence source dispatch domain {domain_index} is empty" + ))); + } + domain.variants.sort_unstable_by_key(|variant| variant.key); + for variant in &domain.variants { + variant.template.validate()?; + } + if domain.variants.windows(2).any(|pair| { + std::mem::discriminant(&pair[0].key) != std::mem::discriminant(&pair[1].key) + }) { + return Err(RusticolError::invalid_argument(format!( + "direct recurrence source dispatch domain {domain_index} mixes spin and runtime-variant keys" + ))); + } + if domain + .variants + .windows(2) + .any(|pair| pair[0].key == pair[1].key) + { + return Err(RusticolError::invalid_argument(format!( + "direct recurrence source dispatch domain {domain_index} repeats a dispatch key" + ))); + } + loaded.push(DirectSourceDispatchDomain { + variants: std::mem::take(&mut domain.variants).into_boxed_slice(), + }); + } + Ok(Self { + context: Box::new(DirectSourceExecutorContext { + domains: loaded.into_boxed_slice(), + }), + }) + } + + pub(crate) fn handle(&self) -> ContextDirectSourceExecutorHandle { + ContextDirectSourceExecutorHandle { + call: execute_direct_source_rows, + context: ptr::from_ref(self.context.as_ref()).cast(), + } + } + + /// Return the all-flow-union source-dispatch handle backed by the same + /// immutable SourceIR domain catalog. + pub(crate) fn union_handle(&self) -> DirectUnionSourceDispatchHandle { + DirectUnionSourceDispatchHandle { + call: execute_direct_union_source_rows, + context: ptr::from_ref(self.context.as_ref()).cast(), + } + } + + /// Resolve and authenticate one selected-helicity union source program. + /// + /// The returned object owns compact copies of its templates and embedded + /// component scales. The complete variant/domain catalogs are consulted + /// only here and are absent from warmed execution. + pub(crate) fn bind_union_program( + &self, + view: OnTheFlyUnionSourceBindingView<'_>, + ) -> RusticolResult> { + if view.sources.is_empty() + || view.variants.is_empty() + || view.embeddings.is_empty() + || view.selections.is_empty() + || view.exact_factors.is_empty() + || view.current_component_count == 0 + || view.momentum_form_count == 0 + || view.sources.len() != view.selections.len() + { + return Err(source_binding_integrity( + "selected source tables or execution shape are empty or inconsistent", + )); + } + + let mut rows = Vec::with_capacity(view.selections.len()); + let mut embeddings = Vec::new(); + for (source_slot, selection) in view.selections.iter().copied().enumerate() { + if selection.source_slot != source_slot as u32 { + return Err(source_binding_integrity( + "source selections are not in canonical source-slot order", + )); + } + let variant = view + .variants + .get(selection.dispatch_variant_id as usize) + .ok_or_else(|| source_binding_integrity("dispatch variant is out of bounds"))?; + let row = view + .sources + .get(variant.source_row_id as usize) + .ok_or_else(|| source_binding_integrity("source row is out of bounds"))?; + if row.source_slot != selection.source_slot + || row.source_template_or_dispatch_domain != variant.dispatch_domain_id + || row.momentum_form_id >= view.momentum_form_count + { + return Err(source_binding_integrity( + "source row, dispatch domain, or momentum form is inconsistent", + )); + } + let row_factor = view + .exact_factors + .get(row.exact_factor_id as usize) + .ok_or_else(|| source_binding_integrity("source row factor is out of bounds"))?; + if *row_factor != ExactComplexRational::ONE { + return Err(source_binding_integrity( + "union source row has a non-unit structural factor", + )); + } + let crossing_factor = view + .exact_factors + .get(variant.crossing_exact_factor_id as usize) + .copied() + .ok_or_else(|| source_binding_integrity("crossing factor is out of bounds"))?; + let (crossing_re, crossing_im) = exact_factor_parts(crossing_factor)?; + + let domain = self + .context + .domains + .get(variant.dispatch_domain_id as usize) + .ok_or_else(|| source_binding_integrity("dispatch domain is out of bounds"))?; + let template = domain + .resolve(DirectSourceDispatchKey::RuntimeVariant { + source_row_id: variant.source_row_id, + runtime_variant_id: variant.runtime_variant_id, + }) + .ok_or_else(|| { + source_binding_integrity("runtime source template is absent from its domain") + })?; + if template.spin_state_class != variant.crossed_spin_state_class + || u32::from(template.family.component_count()) != variant.projection_count + { + return Err(source_binding_integrity( + "runtime source template disagrees with its variant descriptor", + )); + } + if template + .mass_parameter_index + .is_some_and(|index| index >= view.parameter_count) + { + return Err(source_binding_integrity( + "runtime source mass parameter is out of bounds", + )); + } + + let embedding_start = usize::try_from(variant.embedding_start) + .map_err(|_| source_binding_integrity("embedding start exceeds usize"))?; + let embedding_end = embedding_start + .checked_add(variant.embedding_count as usize) + .ok_or_else(|| source_binding_integrity("embedding range overflows usize"))?; + let selected_embeddings = view + .embeddings + .get(embedding_start..embedding_end) + .ok_or_else(|| source_binding_integrity("embedding range is out of bounds"))?; + if selected_embeddings.is_empty() { + return Err(source_binding_integrity("embedding range is empty")); + } + let destination_end = row + .destination_component_base + .checked_add(variant.embedding_count) + .ok_or_else(|| source_binding_integrity("destination range overflows u32"))?; + if destination_end > view.current_component_count { + return Err(source_binding_integrity( + "destination range is outside the current arena", + )); + } + + let bound_embedding_start = u32::try_from(embeddings.len()) + .map_err(|_| source_binding_integrity("bound embedding start exceeds u32"))?; + for (full_component, embedding) in selected_embeddings.iter().copied().enumerate() { + if embedding.full_component != full_component as u32 { + return Err(source_binding_integrity( + "embedding components are not in canonical order", + )); + } + let factor = view + .exact_factors + .get(embedding.exact_factor_id as usize) + .copied() + .ok_or_else(|| source_binding_integrity("embedding factor is out of bounds"))?; + let (embedding_re, embedding_im) = exact_factor_parts(factor)?; + if embedding.source_component_or_sentinel != DIRECT_NONE_U32 + && embedding.source_component_or_sentinel + >= u32::from(template.family.component_count()) + { + return Err(source_binding_integrity( + "embedding source component is out of bounds", + )); + } + // The legacy dispatcher does not combine an inactive + // embedding's otherwise-authenticated factor. Preserve that + // exact zero path even if two finite factors would overflow + // when multiplied. + let (scale_re, scale_im) = + if embedding.source_component_or_sentinel == DIRECT_NONE_U32 { + (0.0, 0.0) + } else { + ( + crossing_re * embedding_re - crossing_im * embedding_im, + crossing_re * embedding_im + crossing_im * embedding_re, + ) + }; + embeddings.push(BoundUnionSourceEmbedding { + source_component_or_sentinel: embedding.source_component_or_sentinel, + scale_re, + scale_im, + }); + } + rows.push(BoundUnionSourceRow { + template, + destination_component_base: row.destination_component_base, + momentum_form_id: row.momentum_form_id, + embedding_start: bound_embedding_start, + embedding_count: variant.embedding_count, + }); + } + + Ok(Box::new(LoadedBoundUnionSourceProgram { + rows: rows.into_boxed_slice(), + embeddings: embeddings.into_boxed_slice(), + current_component_count: view.current_component_count, + momentum_form_count: view.momentum_form_count, + parameter_count: view.parameter_count, + })) + } +} + +impl OnTheFlyBoundUnionSourceProgram for LoadedBoundUnionSourceProgram { + unsafe fn execute( + &self, + arena: DirectArenaView, + momenta: DirectMomentumView, + parameters: DirectParameterView, + point_count: u32, + ) -> RusticolResult<()> { + let expected_current_scalars = u64::from(self.current_component_count) + .checked_mul(u64::from(arena.point_stride)) + .ok_or_else(|| source_binding_integrity("current arena shape overflows u64"))?; + let expected_momentum_scalars = u64::from(self.momentum_form_count) + .checked_mul(4) + .and_then(|planes| planes.checked_mul(u64::from(arena.point_stride))) + .ok_or_else(|| source_binding_integrity("momentum arena shape overflows u64"))?; + if point_count == 0 + || arena.point_stride == 0 + || point_count > arena.point_stride + || arena.current_re.is_null() + || arena.current_im.is_null() + || arena.current_scalar_len != expected_current_scalars + || momenta.values.is_null() + || momenta.point_stride != arena.point_stride + || momenta.lorentz_component_count != 4 + || momenta.form_count != self.momentum_form_count + || momenta.scalar_len != expected_momentum_scalars + || parameters.value_count != self.parameter_count + || (self.parameter_count != 0 && parameters.values_re.is_null()) + { + return Err(source_binding_integrity( + "warm Direct-Arena views disagree with the cold-bound shape", + )); + } + usize::try_from(expected_current_scalars) + .and_then(|_| usize::try_from(expected_momentum_scalars)) + .map_err(|_| source_binding_integrity("warm Direct-Arena shape exceeds usize"))?; + + for row in &self.rows { + let start = row.embedding_start as usize; + let end = start + row.embedding_count as usize; + // Cold binding proved every compact range against the owned + // embedding table. Rechecking that invariant for every warmed + // source row would put a catalog bound back on the hot path. + let row_embeddings = unsafe { self.embeddings.get_unchecked(start..end) }; + let mass = match row.template.mass_parameter_index { + Some(index) => unsafe { *parameters.values_re.add(index as usize) }, + None => 0.0, + }; + for point in 0..point_count { + let momentum = read_momentum(momenta, row.momentum_form_id, point); + match row.template.family { + DirectSourceWavefunctionFamily::Scalar => unsafe { + write_bound_embedded_wavefunction( + arena, + row.destination_component_base, + point, + [Complex::new(1.0, 0.0)], + row_embeddings, + ) + }, + DirectSourceWavefunctionFamily::WeylFermion => { + let wave = match row.template.orientation { + DirectSourceOrientation::Particle => { + source_wavefunctions::ext_quark_weyl_array( + momentum, + row.template.helicity, + row.template.chirality, + ) + } + DirectSourceOrientation::Antiparticle => { + source_wavefunctions::ext_antiquark_weyl_array( + momentum, + row.template.helicity, + row.template.chirality, + ) + } + DirectSourceOrientation::SelfConjugate => { + return Err(source_binding_integrity( + "Weyl source has self-conjugate orientation", + )); + } + }; + unsafe { + write_bound_embedded_wavefunction( + arena, + row.destination_component_base, + point, + wave, + row_embeddings, + ) + }; + } + DirectSourceWavefunctionFamily::DiracFermion => { + let wave = match row.template.orientation { + DirectSourceOrientation::Particle => { + source_wavefunctions::ext_quark_dirac_massive( + momentum, + row.template.helicity, + mass, + ) + } + DirectSourceOrientation::Antiparticle => { + source_wavefunctions::ext_antiquark_dirac_massive( + momentum, + row.template.helicity, + mass, + ) + } + DirectSourceOrientation::SelfConjugate => { + return Err(source_binding_integrity( + "Dirac source has self-conjugate orientation", + )); + } + }; + unsafe { + write_bound_embedded_wavefunction( + arena, + row.destination_component_base, + point, + wave, + row_embeddings, + ) + }; + } + DirectSourceWavefunctionFamily::Vector => { + let wave = if mass == 0.0 { + source_wavefunctions::ext_gluon(momentum, row.template.helicity) + } else { + source_wavefunctions::ext_massive_vector( + momentum, + row.template.helicity, + mass, + ) + }; + unsafe { + write_bound_embedded_wavefunction( + arena, + row.destination_component_base, + point, + wave, + row_embeddings, + ) + }; + } + DirectSourceWavefunctionFamily::Spin2 => { + let wave = + source_wavefunctions::ext_spin2(momentum, row.template.helicity, mass) + .map_err(|_| { + RusticolError::evaluation( + "cold-bound recurrence spin-2 source evaluation failed", + ) + })?; + unsafe { + write_bound_embedded_wavefunction( + arena, + row.destination_component_base, + point, + wave, + row_embeddings, + ) + }; + } + } + } + } + Ok(()) + } +} + +unsafe extern "C" fn execute_direct_source_rows( + context: *const c_void, + arena: DirectArenaView, + momenta: DirectMomentumView, + parameters: DirectParameterView, + factors: DirectFactorView, + rows: *const DirectSourceRow, + row_count: u32, + point_count: u32, +) -> c_int { + if context.is_null() { + return STATUS_INVALID_CONTEXT; + } + if rows.is_null() + || row_count == 0 + || point_count == 0 + || arena.point_stride == 0 + || point_count > arena.point_stride + || momenta.point_stride != arena.point_stride + || momenta.lorentz_component_count != 4 + { + return STATUS_INVALID_ARGUMENT; + } + if arena.current_re.is_null() + || arena.current_im.is_null() + || momenta.values.is_null() + || factors.values_re.is_null() + || factors.values_im.is_null() + { + return STATUS_INVALID_ARGUMENT; + } + + let context = unsafe { &*context.cast::() }; + let rows = unsafe { std::slice::from_raw_parts(rows, row_count as usize) }; + for row in rows { + let Some(domain) = context + .domains + .get(row.source_template_or_dispatch_domain as usize) + else { + return STATUS_BOUNDS; + }; + let Some(template) = domain.resolve(DirectSourceDispatchKey::SpinStateClass( + row.spin_state_class, + )) else { + return STATUS_BOUNDS; + }; + if row.momentum_form_id >= momenta.form_count || row.exact_factor_id >= factors.value_count + { + return STATUS_BOUNDS; + } + let Some(destination_end) = u64::from(row.destination_component_base) + .checked_add(u64::from(template.family.component_count())) + .and_then(|planes| planes.checked_mul(u64::from(arena.point_stride))) + else { + return STATUS_BOUNDS; + }; + if destination_end > arena.current_scalar_len { + return STATUS_BOUNDS; + } + let Some(momentum_end) = u64::from(row.momentum_form_id) + .checked_mul(4) + .and_then(|base| base.checked_add(4)) + .and_then(|planes| planes.checked_mul(u64::from(momenta.point_stride))) + else { + return STATUS_BOUNDS; + }; + if momentum_end > momenta.scalar_len { + return STATUS_BOUNDS; + } + let mass = match template.mass_parameter_index { + Some(index) => { + if index >= parameters.value_count || parameters.values_re.is_null() { + return STATUS_BOUNDS; + } + unsafe { *parameters.values_re.add(index as usize) } + } + None => 0.0, + }; + let factor_re = unsafe { *factors.values_re.add(row.exact_factor_id as usize) }; + let factor_im = unsafe { *factors.values_im.add(row.exact_factor_id as usize) }; + + for point in 0..point_count { + let momentum = read_momentum(momenta, row.momentum_form_id, point); + let status = match template.family { + DirectSourceWavefunctionFamily::Scalar => write_wavefunction( + arena, + row.destination_component_base, + point, + factor_re, + factor_im, + [Complex::new(1.0, 0.0)], + ), + DirectSourceWavefunctionFamily::WeylFermion => { + let wave = match template.orientation { + DirectSourceOrientation::Particle => { + source_wavefunctions::ext_quark_weyl_array( + momentum, + template.helicity, + template.chirality, + ) + } + DirectSourceOrientation::Antiparticle => { + source_wavefunctions::ext_antiquark_weyl_array( + momentum, + template.helicity, + template.chirality, + ) + } + DirectSourceOrientation::SelfConjugate => { + return STATUS_EXECUTION_FAILED; + } + }; + write_wavefunction( + arena, + row.destination_component_base, + point, + factor_re, + factor_im, + wave, + ) + } + DirectSourceWavefunctionFamily::DiracFermion => { + let wave = match template.orientation { + DirectSourceOrientation::Particle => { + source_wavefunctions::ext_quark_dirac_massive( + momentum, + template.helicity, + mass, + ) + } + DirectSourceOrientation::Antiparticle => { + source_wavefunctions::ext_antiquark_dirac_massive( + momentum, + template.helicity, + mass, + ) + } + DirectSourceOrientation::SelfConjugate => { + return STATUS_EXECUTION_FAILED; + } + }; + write_wavefunction( + arena, + row.destination_component_base, + point, + factor_re, + factor_im, + wave, + ) + } + DirectSourceWavefunctionFamily::Vector => { + let wave = if mass == 0.0 { + source_wavefunctions::ext_gluon(momentum, template.helicity) + } else { + source_wavefunctions::ext_massive_vector(momentum, template.helicity, mass) + }; + write_wavefunction( + arena, + row.destination_component_base, + point, + factor_re, + factor_im, + wave, + ) + } + DirectSourceWavefunctionFamily::Spin2 => { + let Ok(wave) = + source_wavefunctions::ext_spin2(momentum, template.helicity, mass) + else { + return STATUS_EXECUTION_FAILED; + }; + write_wavefunction( + arena, + row.destination_component_base, + point, + factor_re, + factor_im, + wave, + ) + } + }; + if status != DIRECT_STATUS_OK { + return status; + } + } + } + DIRECT_STATUS_OK +} + +#[allow(clippy::too_many_arguments)] +unsafe extern "C" fn execute_direct_union_source_rows( + context: *const c_void, + arena: DirectArenaView, + momenta: DirectMomentumView, + parameters: DirectParameterView, + factors: DirectFactorView, + rows: *const DirectSourceRow, + row_count: u32, + variants: *const DirectSourceDispatchVariantDescriptor, + variant_count: u32, + embeddings: *const DirectSourceEmbeddingRow, + embedding_count: u32, + selections: *const DirectResolvedSourceSelection, + selection_count: u32, + point_count: u32, +) -> c_int { + if context.is_null() { + return STATUS_INVALID_CONTEXT; + } + if rows.is_null() + || variants.is_null() + || embeddings.is_null() + || selections.is_null() + || row_count == 0 + || variant_count == 0 + || embedding_count == 0 + || selection_count == 0 + || point_count == 0 + || arena.point_stride == 0 + || point_count > arena.point_stride + || momenta.point_stride != arena.point_stride + || momenta.lorentz_component_count != 4 + { + return STATUS_INVALID_ARGUMENT; + } + if arena.current_re.is_null() + || arena.current_im.is_null() + || momenta.values.is_null() + || factors.values_re.is_null() + || factors.values_im.is_null() + { + return STATUS_INVALID_ARGUMENT; + } + + let context = unsafe { &*context.cast::() }; + let rows = unsafe { std::slice::from_raw_parts(rows, row_count as usize) }; + let variants = unsafe { std::slice::from_raw_parts(variants, variant_count as usize) }; + let embeddings = unsafe { std::slice::from_raw_parts(embeddings, embedding_count as usize) }; + let selections = unsafe { std::slice::from_raw_parts(selections, selection_count as usize) }; + + for (source_slot, selection) in selections.iter().enumerate() { + if selection.source_slot != source_slot as u32 { + return STATUS_INVALID_ARGUMENT; + } + let Some(variant) = variants.get(selection.dispatch_variant_id as usize) else { + return STATUS_BOUNDS; + }; + let Some(row) = rows.get(variant.source_row_id as usize) else { + return STATUS_BOUNDS; + }; + if row.source_slot != selection.source_slot + || row.source_template_or_dispatch_domain != variant.dispatch_domain_id + || row.momentum_form_id >= momenta.form_count + || row.exact_factor_id >= factors.value_count + || variant.crossing_exact_factor_id >= factors.value_count + { + return STATUS_BOUNDS; + } + // Union source rows are only structural destinations. Crossing and + // embedding own the complete source factorization. + let row_factor_re = unsafe { *factors.values_re.add(row.exact_factor_id as usize) }; + let row_factor_im = unsafe { *factors.values_im.add(row.exact_factor_id as usize) }; + if row_factor_re != 1.0 || row_factor_im != 0.0 { + return STATUS_EXECUTION_FAILED; + } + + let Some(domain) = context.domains.get(variant.dispatch_domain_id as usize) else { + return STATUS_BOUNDS; + }; + let Some(template) = domain.resolve(DirectSourceDispatchKey::RuntimeVariant { + source_row_id: variant.source_row_id, + runtime_variant_id: variant.runtime_variant_id, + }) else { + return STATUS_BOUNDS; + }; + if template.spin_state_class != variant.crossed_spin_state_class { + return STATUS_EXECUTION_FAILED; + } + if u32::from(template.family.component_count()) != variant.projection_count { + return STATUS_EXECUTION_FAILED; + } + + let Some(embedding_end) = variant + .embedding_start + .checked_add(u64::from(variant.embedding_count)) + else { + return STATUS_BOUNDS; + }; + if embedding_end > embedding_count as u64 { + return STATUS_BOUNDS; + } + let embedding_start = variant.embedding_start as usize; + let embedding_end = embedding_end as usize; + let variant_embeddings = &embeddings[embedding_start..embedding_end]; + let Some(destination_end) = u64::from(row.destination_component_base) + .checked_add(u64::from(variant.embedding_count)) + .and_then(|planes| planes.checked_mul(u64::from(arena.point_stride))) + else { + return STATUS_BOUNDS; + }; + if destination_end > arena.current_scalar_len { + return STATUS_BOUNDS; + } + let Some(momentum_end) = u64::from(row.momentum_form_id) + .checked_mul(4) + .and_then(|base| base.checked_add(4)) + .and_then(|planes| planes.checked_mul(u64::from(momenta.point_stride))) + else { + return STATUS_BOUNDS; + }; + if momentum_end > momenta.scalar_len { + return STATUS_BOUNDS; + } + let mass = match template.mass_parameter_index { + Some(index) => { + if index >= parameters.value_count || parameters.values_re.is_null() { + return STATUS_BOUNDS; + } + unsafe { *parameters.values_re.add(index as usize) } + } + None => 0.0, + }; + let crossing_re = unsafe { + *factors + .values_re + .add(variant.crossing_exact_factor_id as usize) + }; + let crossing_im = unsafe { + *factors + .values_im + .add(variant.crossing_exact_factor_id as usize) + }; + + for point in 0..point_count { + let momentum = read_momentum(momenta, row.momentum_form_id, point); + let status = match template.family { + DirectSourceWavefunctionFamily::Scalar => write_embedded_wavefunction( + arena, + row.destination_component_base, + point, + crossing_re, + crossing_im, + [Complex::new(1.0, 0.0)], + variant_embeddings, + factors, + ), + DirectSourceWavefunctionFamily::WeylFermion => { + let wave = match template.orientation { + DirectSourceOrientation::Particle => { + source_wavefunctions::ext_quark_weyl_array( + momentum, + template.helicity, + template.chirality, + ) + } + DirectSourceOrientation::Antiparticle => { + source_wavefunctions::ext_antiquark_weyl_array( + momentum, + template.helicity, + template.chirality, + ) + } + DirectSourceOrientation::SelfConjugate => { + return STATUS_EXECUTION_FAILED; + } + }; + write_embedded_wavefunction( + arena, + row.destination_component_base, + point, + crossing_re, + crossing_im, + wave, + variant_embeddings, + factors, + ) + } + DirectSourceWavefunctionFamily::DiracFermion => { + let wave = match template.orientation { + DirectSourceOrientation::Particle => { + source_wavefunctions::ext_quark_dirac_massive( + momentum, + template.helicity, + mass, + ) + } + DirectSourceOrientation::Antiparticle => { + source_wavefunctions::ext_antiquark_dirac_massive( + momentum, + template.helicity, + mass, + ) + } + DirectSourceOrientation::SelfConjugate => { + return STATUS_EXECUTION_FAILED; + } + }; + write_embedded_wavefunction( + arena, + row.destination_component_base, + point, + crossing_re, + crossing_im, + wave, + variant_embeddings, + factors, + ) + } + DirectSourceWavefunctionFamily::Vector => { + let wave = if mass == 0.0 { + source_wavefunctions::ext_gluon(momentum, template.helicity) + } else { + source_wavefunctions::ext_massive_vector(momentum, template.helicity, mass) + }; + write_embedded_wavefunction( + arena, + row.destination_component_base, + point, + crossing_re, + crossing_im, + wave, + variant_embeddings, + factors, + ) + } + DirectSourceWavefunctionFamily::Spin2 => { + let Ok(wave) = + source_wavefunctions::ext_spin2(momentum, template.helicity, mass) + else { + return STATUS_EXECUTION_FAILED; + }; + write_embedded_wavefunction( + arena, + row.destination_component_base, + point, + crossing_re, + crossing_im, + wave, + variant_embeddings, + factors, + ) + } + }; + if status != DIRECT_STATUS_OK { + return status; + } + } + } + DIRECT_STATUS_OK +} + +fn read_momentum(momenta: DirectMomentumView, form: u32, point: u32) -> [f64; 4] { + std::array::from_fn(|component| { + let plane = form as usize * 4 + component; + let offset = plane * momenta.point_stride as usize + point as usize; + unsafe { *momenta.values.add(offset) } + }) +} + +#[allow(clippy::too_many_arguments)] +fn write_wavefunction( + arena: DirectArenaView, + destination_component_base: u32, + point: u32, + factor_re: f64, + factor_im: f64, + wave: [Complex; N], +) -> c_int { + for (component, value) in wave.into_iter().enumerate() { + let Some(plane) = (destination_component_base as usize).checked_add(component) else { + return STATUS_BOUNDS; + }; + let Some(offset) = plane + .checked_mul(arena.point_stride as usize) + .and_then(|base| base.checked_add(point as usize)) + else { + return STATUS_BOUNDS; + }; + if offset >= arena.current_scalar_len as usize { + return STATUS_BOUNDS; + } + unsafe { + *arena.current_re.add(offset) = factor_re * value.re - factor_im * value.im; + *arena.current_im.add(offset) = factor_re * value.im + factor_im * value.re; + } + } + DIRECT_STATUS_OK +} + +#[allow(clippy::too_many_arguments)] +fn write_embedded_wavefunction( + arena: DirectArenaView, + destination_component_base: u32, + point: u32, + crossing_re: f64, + crossing_im: f64, + wave: [Complex; N], + embeddings: &[DirectSourceEmbeddingRow], + factors: DirectFactorView, +) -> c_int { + for (full_component, embedding) in embeddings.iter().enumerate() { + if embedding.full_component != full_component as u32 + || embedding.exact_factor_id >= factors.value_count + { + return STATUS_BOUNDS; + } + let Some(plane) = (destination_component_base as usize).checked_add(full_component) else { + return STATUS_BOUNDS; + }; + let Some(offset) = plane + .checked_mul(arena.point_stride as usize) + .and_then(|base| base.checked_add(point as usize)) + else { + return STATUS_BOUNDS; + }; + if offset >= arena.current_scalar_len as usize { + return STATUS_BOUNDS; + } + + let (value_re, value_im) = if embedding.source_component_or_sentinel == DIRECT_NONE_U32 { + (0.0, 0.0) + } else { + let Some(value) = wave.get(embedding.source_component_or_sentinel as usize) else { + return STATUS_BOUNDS; + }; + let embedding_re = + unsafe { *factors.values_re.add(embedding.exact_factor_id as usize) }; + let embedding_im = + unsafe { *factors.values_im.add(embedding.exact_factor_id as usize) }; + let scale_re = crossing_re * embedding_re - crossing_im * embedding_im; + let scale_im = crossing_re * embedding_im + crossing_im * embedding_re; + ( + scale_re * value.re - scale_im * value.im, + scale_re * value.im + scale_im * value.re, + ) + }; + unsafe { + *arena.current_re.add(offset) = value_re; + *arena.current_im.add(offset) = value_im; + } + } + DIRECT_STATUS_OK +} + +/// Write one cold-authenticated embedded source without revisiting component, +/// factor, or arena bounds. `LoadedBoundUnionSourceProgram::execute` checks +/// the dynamic view shape once, while `bind_union_program` proves every +/// component index and precombines the immutable exact scales. +unsafe fn write_bound_embedded_wavefunction( + arena: DirectArenaView, + destination_component_base: u32, + point: u32, + wave: [Complex; N], + embeddings: &[BoundUnionSourceEmbedding], +) { + let stride = arena.point_stride as usize; + for (full_component, embedding) in embeddings.iter().copied().enumerate() { + let plane = destination_component_base as usize + full_component; + let offset = plane * stride + point as usize; + let (value_re, value_im) = if embedding.source_component_or_sentinel == DIRECT_NONE_U32 { + (0.0, 0.0) + } else { + let value = + unsafe { wave.get_unchecked(embedding.source_component_or_sentinel as usize) }; + ( + embedding.scale_re * value.re - embedding.scale_im * value.im, + embedding.scale_re * value.im + embedding.scale_im * value.re, + ) + }; + unsafe { + *arena.current_re.add(offset) = value_re; + *arena.current_im.add(offset) = value_im; + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::recurrence::ExactRational; + use crate::recurrence::direct_backend::{ + DirectArenaView, DirectFactorView, DirectMomentumView, DirectParameterView, + }; + + fn domain(variant: DirectSourceTemplateSpec) -> DirectSourceDispatchDomainSpec { + DirectSourceDispatchDomainSpec { + variants: vec![DirectSourceDispatchVariantSpec { + key: DirectSourceDispatchKey::SpinStateClass(variant.spin_state_class), + template: variant, + }], + } + } + + fn runtime_domain( + variants: impl IntoIterator, + ) -> DirectSourceDispatchDomainSpec { + DirectSourceDispatchDomainSpec { + variants: variants + .into_iter() + .map( + |(runtime_variant_id, template)| DirectSourceDispatchVariantSpec { + key: DirectSourceDispatchKey::RuntimeVariant { + source_row_id: 0, + runtime_variant_id, + }, + template, + }, + ) + .collect(), + } + } + + fn source_row( + domain: u32, + spin_state_class: i32, + destination_component_base: u32, + momentum_form_id: u32, + exact_factor_id: u32, + ) -> DirectSourceRow { + DirectSourceRow { + source_slot: domain, + destination_component_base, + momentum_form_id, + source_template_or_dispatch_domain: domain, + spin_state_class, + exact_factor_id, + selector_domain_id: 0, + } + } + + #[allow(clippy::too_many_arguments)] + fn invoke( + executor: &LoadedDirectSourceExecutor, + rows: &[DirectSourceRow], + current_re: &mut [f64], + current_im: &mut [f64], + momenta: &[f64], + momentum_forms: u32, + parameters: &[f64], + factors_re: &[f64], + factors_im: &[f64], + point_stride: u32, + point_count: u32, + ) -> c_int { + let handle = executor.handle(); + let mut amplitude_re = [0.0]; + let mut amplitude_im = [0.0]; + unsafe { + (handle.call)( + handle.context, + DirectArenaView { + current_re: current_re.as_mut_ptr(), + current_im: current_im.as_mut_ptr(), + current_scalar_len: current_re.len() as u64, + amplitude_re: amplitude_re.as_mut_ptr(), + amplitude_im: amplitude_im.as_mut_ptr(), + amplitude_scalar_len: 1, + point_stride, + }, + DirectMomentumView { + values: momenta.as_ptr(), + scalar_len: momenta.len() as u64, + form_count: momentum_forms, + lorentz_component_count: 4, + point_stride, + }, + DirectParameterView { + values_re: parameters.as_ptr(), + values_im: ptr::null(), + value_count: parameters.len() as u32, + }, + DirectFactorView { + values_re: factors_re.as_ptr(), + values_im: factors_im.as_ptr(), + value_count: factors_re.len() as u32, + }, + rows.as_ptr(), + rows.len() as u32, + point_count, + ) + } + } + + #[allow(clippy::too_many_arguments)] + fn invoke_union( + executor: &LoadedDirectSourceExecutor, + rows: &[DirectSourceRow], + variants: &[DirectSourceDispatchVariantDescriptor], + embeddings: &[DirectSourceEmbeddingRow], + selections: &[DirectResolvedSourceSelection], + current_re: &mut [f64], + current_im: &mut [f64], + momenta: &[f64], + momentum_forms: u32, + parameters: &[f64], + factors_re: &[f64], + factors_im: &[f64], + point_stride: u32, + point_count: u32, + ) -> c_int { + let handle = executor.union_handle(); + let mut amplitude_re = [0.0]; + let mut amplitude_im = [0.0]; + unsafe { + (handle.call)( + handle.context, + DirectArenaView { + current_re: current_re.as_mut_ptr(), + current_im: current_im.as_mut_ptr(), + current_scalar_len: current_re.len() as u64, + amplitude_re: amplitude_re.as_mut_ptr(), + amplitude_im: amplitude_im.as_mut_ptr(), + amplitude_scalar_len: 1, + point_stride, + }, + DirectMomentumView { + values: momenta.as_ptr(), + scalar_len: momenta.len() as u64, + form_count: momentum_forms, + lorentz_component_count: 4, + point_stride, + }, + DirectParameterView { + values_re: parameters.as_ptr(), + values_im: ptr::null(), + value_count: parameters.len() as u32, + }, + DirectFactorView { + values_re: factors_re.as_ptr(), + values_im: factors_im.as_ptr(), + value_count: factors_re.len() as u32, + }, + rows.as_ptr(), + rows.len() as u32, + variants.as_ptr(), + variants.len() as u32, + embeddings.as_ptr(), + embeddings.len() as u32, + selections.as_ptr(), + selections.len() as u32, + point_count, + ) + } + } + + #[allow(clippy::too_many_arguments)] + fn invoke_bound_union( + executor: &LoadedDirectSourceExecutor, + rows: &[DirectSourceRow], + variants: &[DirectSourceDispatchVariantDescriptor], + embeddings: &[DirectSourceEmbeddingRow], + selections: &[DirectResolvedSourceSelection], + exact_factors: &[ExactComplexRational], + current_re: &mut [f64], + current_im: &mut [f64], + momenta: &[f64], + momentum_forms: u32, + parameters: &[f64], + point_stride: u32, + point_count: u32, + ) -> RusticolResult<()> { + let program = executor.bind_union_program(OnTheFlyUnionSourceBindingView { + sources: rows, + variants, + embeddings, + selections, + exact_factors, + current_component_count: u32::try_from(current_re.len() / point_stride as usize) + .unwrap(), + momentum_form_count: momentum_forms, + parameter_count: parameters.len() as u32, + })?; + let mut amplitude_re = [0.0]; + let mut amplitude_im = [0.0]; + unsafe { + program.execute( + DirectArenaView { + current_re: current_re.as_mut_ptr(), + current_im: current_im.as_mut_ptr(), + current_scalar_len: current_re.len() as u64, + amplitude_re: amplitude_re.as_mut_ptr(), + amplitude_im: amplitude_im.as_mut_ptr(), + amplitude_scalar_len: 1, + point_stride, + }, + DirectMomentumView { + values: momenta.as_ptr(), + scalar_len: momenta.len() as u64, + form_count: momentum_forms, + lorentz_component_count: 4, + point_stride, + }, + DirectParameterView { + values_re: parameters.as_ptr(), + values_im: parameters.as_ptr(), + value_count: parameters.len() as u32, + }, + point_count, + ) + } + } + + fn exact_union_factors() -> [ExactComplexRational; 4] { + [ + ExactComplexRational::ONE, + ExactComplexRational::new(ExactRational::ZERO, ExactRational::ONE), + ExactComplexRational::new(ExactRational::new(2, 1).unwrap(), ExactRational::ZERO), + ExactComplexRational::ZERO, + ] + } + + #[test] + fn scalar_source_writes_exact_complex_factor_directly_into_arena() { + let executor = LoadedDirectSourceExecutor::load(vec![domain(DirectSourceTemplateSpec { + spin_state_class: 0, + family: DirectSourceWavefunctionFamily::Scalar, + orientation: DirectSourceOrientation::SelfConjugate, + helicity: 0, + chirality: 0, + mass_parameter_index: None, + })]) + .unwrap(); + let rows = [source_row(0, 0, 0, 0, 0)]; + let mut current_re = [99.0, 99.0]; + let mut current_im = [99.0, 99.0]; + let momenta = [10.0, 11.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0]; + assert_eq!( + invoke( + &executor, + &rows, + &mut current_re, + &mut current_im, + &momenta, + 1, + &[], + &[2.0], + &[-3.0], + 2, + 2, + ), + DIRECT_STATUS_OK + ); + assert_eq!(current_re, [2.0, 2.0]); + assert_eq!(current_im, [-3.0, -3.0]); + } + + #[test] + fn vector_source_matches_established_source_ir_formula_for_every_point() { + let executor = LoadedDirectSourceExecutor::load(vec![domain(DirectSourceTemplateSpec { + spin_state_class: 1, + family: DirectSourceWavefunctionFamily::Vector, + orientation: DirectSourceOrientation::SelfConjugate, + helicity: 1, + chirality: 0, + mass_parameter_index: None, + })]) + .unwrap(); + let rows = [source_row(0, 1, 0, 0, 0)]; + let points = [ + [100.0, 30.0, 40.0, 86.60254037844386], + [-120.0, -20.0, 50.0, -107.23805294763608], + ]; + let momenta = [ + points[0][0], + points[1][0], + points[0][1], + points[1][1], + points[0][2], + points[1][2], + points[0][3], + points[1][3], + ]; + let mut current_re = [0.0; 8]; + let mut current_im = [0.0; 8]; + assert_eq!( + invoke( + &executor, + &rows, + &mut current_re, + &mut current_im, + &momenta, + 1, + &[], + &[1.0], + &[0.0], + 2, + 2, + ), + DIRECT_STATUS_OK + ); + for (point_index, point) in points.into_iter().enumerate() { + let expected = source_wavefunctions::ext_gluon(point, 1); + for (component, value) in expected.into_iter().enumerate() { + let offset = component * 2 + point_index; + assert_eq!(current_re[offset], value.re); + assert_eq!(current_im[offset], value.im); + } + } + } + + #[test] + fn dispatch_domains_select_crossed_fermion_state_without_packing() { + let executor = LoadedDirectSourceExecutor::load(vec![DirectSourceDispatchDomainSpec { + variants: vec![ + DirectSourceDispatchVariantSpec { + key: DirectSourceDispatchKey::SpinStateClass(1), + template: DirectSourceTemplateSpec { + spin_state_class: 1, + family: DirectSourceWavefunctionFamily::WeylFermion, + orientation: DirectSourceOrientation::Particle, + helicity: 1, + chirality: 1, + mass_parameter_index: None, + }, + }, + DirectSourceDispatchVariantSpec { + key: DirectSourceDispatchKey::SpinStateClass(-1), + template: DirectSourceTemplateSpec { + spin_state_class: -1, + family: DirectSourceWavefunctionFamily::WeylFermion, + orientation: DirectSourceOrientation::Antiparticle, + helicity: -1, + chirality: -1, + mass_parameter_index: None, + }, + }, + ], + }]) + .unwrap(); + let rows = [source_row(0, -1, 0, 0, 0)]; + let momentum = [-50.0, -10.0, -20.0, -44.721359549995796]; + let momenta = [momentum[0], momentum[1], momentum[2], momentum[3]]; + let mut current_re = [0.0; 2]; + let mut current_im = [0.0; 2]; + assert_eq!( + invoke( + &executor, + &rows, + &mut current_re, + &mut current_im, + &momenta, + 1, + &[], + &[0.0], + &[1.0], + 1, + 1, + ), + DIRECT_STATUS_OK + ); + let expected = source_wavefunctions::ext_antiquark_weyl_array(momentum, -1, -1); + for (component, value) in expected.into_iter().enumerate() { + assert_eq!(current_re[component], -value.im); + assert_eq!(current_im[component], value.re); + } + } + + #[test] + fn runtime_variants_preserve_distinct_source_semantics_for_one_spin_class() { + let particle = DirectSourceTemplateSpec { + spin_state_class: 1, + family: DirectSourceWavefunctionFamily::WeylFermion, + orientation: DirectSourceOrientation::Particle, + helicity: 1, + chirality: 1, + mass_parameter_index: None, + }; + let antiparticle = DirectSourceTemplateSpec { + orientation: DirectSourceOrientation::Antiparticle, + ..particle + }; + let executor = LoadedDirectSourceExecutor::load(vec![runtime_domain([ + (3, particle), + (7, antiparticle), + ])]) + .expect("runtime variant identity must distinguish equal spin classes"); + let domain = &executor.context.domains[0]; + + assert_eq!( + domain + .resolve(DirectSourceDispatchKey::RuntimeVariant { + source_row_id: 0, + runtime_variant_id: 3, + }) + .expect("particle runtime variant") + .orientation, + DirectSourceOrientation::Particle + ); + assert_eq!( + domain + .resolve(DirectSourceDispatchKey::RuntimeVariant { + source_row_id: 0, + runtime_variant_id: 7, + }) + .expect("antiparticle runtime variant") + .orientation, + DirectSourceOrientation::Antiparticle + ); + assert!( + domain + .resolve(DirectSourceDispatchKey::SpinStateClass(1)) + .is_none() + ); + } + + #[test] + fn runtime_variants_distinguish_crossed_source_rows_in_one_contract() { + let incoming = DirectSourceTemplateSpec { + spin_state_class: -1, + family: DirectSourceWavefunctionFamily::Vector, + orientation: DirectSourceOrientation::SelfConjugate, + helicity: -1, + chirality: 0, + mass_parameter_index: None, + }; + let outgoing = DirectSourceTemplateSpec { + spin_state_class: 1, + helicity: 1, + ..incoming + }; + let executor = LoadedDirectSourceExecutor::load(vec![DirectSourceDispatchDomainSpec { + variants: vec![ + DirectSourceDispatchVariantSpec { + key: DirectSourceDispatchKey::RuntimeVariant { + source_row_id: 0, + runtime_variant_id: 3, + }, + template: incoming, + }, + DirectSourceDispatchVariantSpec { + key: DirectSourceDispatchKey::RuntimeVariant { + source_row_id: 2, + runtime_variant_id: 3, + }, + template: outgoing, + }, + ], + }]) + .expect("one runtime variant may have distinct crossed source-row semantics"); + let domain = &executor.context.domains[0]; + + assert_eq!( + domain + .resolve(DirectSourceDispatchKey::RuntimeVariant { + source_row_id: 0, + runtime_variant_id: 3, + }) + .expect("incoming source row"), + incoming + ); + assert_eq!( + domain + .resolve(DirectSourceDispatchKey::RuntimeVariant { + source_row_id: 2, + runtime_variant_id: 3, + }) + .expect("outgoing source row"), + outgoing + ); + } + + #[test] + fn every_supported_source_ir_spin_family_executes_from_persistent_views() { + let specs = [ + DirectSourceTemplateSpec { + spin_state_class: 0, + family: DirectSourceWavefunctionFamily::Scalar, + orientation: DirectSourceOrientation::SelfConjugate, + helicity: 0, + chirality: 0, + mass_parameter_index: None, + }, + DirectSourceTemplateSpec { + spin_state_class: 1, + family: DirectSourceWavefunctionFamily::WeylFermion, + orientation: DirectSourceOrientation::Particle, + helicity: 1, + chirality: 1, + mass_parameter_index: None, + }, + DirectSourceTemplateSpec { + spin_state_class: 2, + family: DirectSourceWavefunctionFamily::DiracFermion, + orientation: DirectSourceOrientation::Antiparticle, + helicity: -1, + chirality: 0, + mass_parameter_index: Some(0), + }, + DirectSourceTemplateSpec { + spin_state_class: 3, + family: DirectSourceWavefunctionFamily::Vector, + orientation: DirectSourceOrientation::SelfConjugate, + helicity: 0, + chirality: 0, + mass_parameter_index: Some(1), + }, + DirectSourceTemplateSpec { + spin_state_class: 4, + family: DirectSourceWavefunctionFamily::Spin2, + orientation: DirectSourceOrientation::SelfConjugate, + helicity: 2, + chirality: 0, + mass_parameter_index: Some(2), + }, + ]; + let executor = + LoadedDirectSourceExecutor::load(specs.into_iter().map(domain).collect()).unwrap(); + let bases = [0, 1, 3, 7, 11]; + let rows: Vec<_> = specs + .into_iter() + .enumerate() + .map(|(index, spec)| { + source_row(index as u32, spec.spin_state_class, bases[index], 0, 0) + }) + .collect(); + let momenta = [20.0, 3.0, 4.0, 19.364916731037084]; + let mut current_re = [0.0; 27]; + let mut current_im = [0.0; 27]; + assert_eq!( + invoke( + &executor, + &rows, + &mut current_re, + &mut current_im, + &momenta, + 1, + &[2.0, 5.0, 7.0], + &[1.0], + &[0.0], + 1, + 1, + ), + DIRECT_STATUS_OK + ); + assert!( + current_re + .iter() + .chain(¤t_im) + .all(|value| value.is_finite()) + ); + assert!( + current_re + .iter() + .chain(¤t_im) + .any(|value| *value != 0.0) + ); + } + + #[test] + fn source_handle_owns_stable_context_and_repeated_calls_allocate_no_scratch() { + let executor = LoadedDirectSourceExecutor::load(vec![domain(DirectSourceTemplateSpec { + spin_state_class: 0, + family: DirectSourceWavefunctionFamily::Scalar, + orientation: DirectSourceOrientation::SelfConjugate, + helicity: 0, + chirality: 0, + mass_parameter_index: None, + })]) + .unwrap(); + let first = executor.handle(); + let second = executor.handle(); + assert_eq!(first.context, second.context); + assert_eq!(first.call as usize, second.call as usize); + + let rows = [source_row(0, 0, 0, 0, 0)]; + let momenta = [1.0, 0.0, 0.0, 1.0]; + let mut current_re = [0.0]; + let mut current_im = [0.0]; + for _ in 0..32 { + assert_eq!( + invoke( + &executor, + &rows, + &mut current_re, + &mut current_im, + &momenta, + 1, + &[], + &[1.0], + &[0.0], + 1, + 1, + ), + DIRECT_STATUS_OK + ); + } + assert_eq!(current_re, [1.0]); + assert_eq!(current_im, [0.0]); + } + + #[test] + fn malformed_domains_and_views_fail_before_arena_mutation() { + let duplicate = DirectSourceTemplateSpec { + spin_state_class: 1, + family: DirectSourceWavefunctionFamily::Scalar, + orientation: DirectSourceOrientation::SelfConjugate, + helicity: 0, + chirality: 0, + mass_parameter_index: None, + }; + assert!( + LoadedDirectSourceExecutor::load(vec![DirectSourceDispatchDomainSpec { + variants: vec![ + DirectSourceDispatchVariantSpec { + key: DirectSourceDispatchKey::SpinStateClass(1), + template: duplicate, + }, + DirectSourceDispatchVariantSpec { + key: DirectSourceDispatchKey::SpinStateClass(1), + template: duplicate, + }, + ], + }]) + .is_err() + ); + + let executor = LoadedDirectSourceExecutor::load(vec![domain(duplicate)]).unwrap(); + let rows = [source_row(0, 99, 0, 0, 0)]; + let mut current_re = [17.0]; + let mut current_im = [19.0]; + assert_eq!( + invoke( + &executor, + &rows, + &mut current_re, + &mut current_im, + &[1.0, 0.0, 0.0, 1.0], + 1, + &[], + &[1.0], + &[0.0], + 1, + 1, + ), + STATUS_BOUNDS + ); + assert_eq!(current_re, [17.0]); + assert_eq!(current_im, [19.0]); + } + + #[test] + fn union_dispatch_embeds_weyl_source_into_full_state_and_zeros_inactive_components() { + let executor = LoadedDirectSourceExecutor::load(vec![runtime_domain([( + 0, + DirectSourceTemplateSpec { + spin_state_class: 1, + family: DirectSourceWavefunctionFamily::WeylFermion, + orientation: DirectSourceOrientation::Particle, + helicity: 1, + chirality: 1, + mass_parameter_index: None, + }, + )])]) + .unwrap(); + let rows = [source_row(0, 0, 0, 0, 0)]; + let variants = [DirectSourceDispatchVariantDescriptor { + embedding_start: 0, + projection_start: 0, + source_row_id: 0, + dispatch_domain_id: 0, + runtime_variant_id: 0, + source_state_index: 0, + source_template_id: 0, + source_state_template_id: 0, + crossed_state_template_id: 0, + crossed_spin_state_class: 1, + direct_executor_id: 0, + crossing_exact_factor_id: 1, + embedding_count: 4, + projection_count: 2, + }]; + let embeddings = [ + DirectSourceEmbeddingRow { + full_component: 0, + source_component_or_sentinel: 0, + exact_factor_id: 2, + }, + DirectSourceEmbeddingRow { + full_component: 1, + source_component_or_sentinel: DIRECT_NONE_U32, + exact_factor_id: 3, + }, + DirectSourceEmbeddingRow { + full_component: 2, + source_component_or_sentinel: 1, + exact_factor_id: 0, + }, + DirectSourceEmbeddingRow { + full_component: 3, + source_component_or_sentinel: DIRECT_NONE_U32, + exact_factor_id: 3, + }, + ]; + let selections = [DirectResolvedSourceSelection { + source_slot: 0, + dispatch_variant_id: 0, + }]; + let momentum = [50.0, 10.0, 20.0, 44.721359549995796]; + let mut current_re = [99.0; 4]; + let mut current_im = [-99.0; 4]; + let factors_re = [1.0, 0.0, 2.0, 0.0]; + let factors_im = [0.0, 1.0, 0.0, 0.0]; + assert_eq!( + invoke_union( + &executor, + &rows, + &variants, + &embeddings, + &selections, + &mut current_re, + &mut current_im, + &momentum, + 1, + &[], + &factors_re, + &factors_im, + 1, + 1, + ), + DIRECT_STATUS_OK + ); + let wave = source_wavefunctions::ext_quark_weyl_array(momentum, 1, 1); + assert_eq!(current_re[0], -2.0 * wave[0].im); + assert_eq!(current_im[0], 2.0 * wave[0].re); + assert_eq!((current_re[1], current_im[1]), (0.0, 0.0)); + assert_eq!(current_re[2], -wave[1].im); + assert_eq!(current_im[2], wave[1].re); + assert_eq!((current_re[3], current_im[3]), (0.0, 0.0)); + + current_re.fill(123.0); + current_im.fill(456.0); + assert_eq!( + invoke_union( + &executor, + &rows, + &variants, + &embeddings, + &selections, + &mut current_re, + &mut current_im, + &momentum, + 1, + &[], + &factors_re, + &factors_im, + 1, + 1, + ), + DIRECT_STATUS_OK + ); + assert_eq!((current_re[1], current_im[1]), (0.0, 0.0)); + assert_eq!((current_re[3], current_im[3]), (0.0, 0.0)); + } + + #[test] + fn cold_bound_union_source_is_bitwise_equal_to_the_authenticated_legacy_dispatch() { + let executor = LoadedDirectSourceExecutor::load(vec![runtime_domain([( + 0, + DirectSourceTemplateSpec { + spin_state_class: 1, + family: DirectSourceWavefunctionFamily::WeylFermion, + orientation: DirectSourceOrientation::Particle, + helicity: 1, + chirality: 1, + mass_parameter_index: None, + }, + )])]) + .unwrap(); + let rows = [source_row(0, 0, 0, 0, 0)]; + let variants = [DirectSourceDispatchVariantDescriptor { + embedding_start: 0, + projection_start: 0, + source_row_id: 0, + dispatch_domain_id: 0, + runtime_variant_id: 0, + source_state_index: 0, + source_template_id: 0, + source_state_template_id: 0, + crossed_state_template_id: 0, + crossed_spin_state_class: 1, + direct_executor_id: 0, + crossing_exact_factor_id: 1, + embedding_count: 4, + projection_count: 2, + }]; + let embeddings = [ + DirectSourceEmbeddingRow { + full_component: 0, + source_component_or_sentinel: 0, + exact_factor_id: 2, + }, + DirectSourceEmbeddingRow { + full_component: 1, + source_component_or_sentinel: DIRECT_NONE_U32, + exact_factor_id: 3, + }, + DirectSourceEmbeddingRow { + full_component: 2, + source_component_or_sentinel: 1, + exact_factor_id: 0, + }, + DirectSourceEmbeddingRow { + full_component: 3, + source_component_or_sentinel: DIRECT_NONE_U32, + exact_factor_id: 3, + }, + ]; + let selections = [DirectResolvedSourceSelection { + source_slot: 0, + dispatch_variant_id: 0, + }]; + let momentum = [50.0, 10.0, 20.0, 44.721359549995796]; + let factors_re = [1.0, 0.0, 2.0, 0.0]; + let factors_im = [0.0, 1.0, 0.0, 0.0]; + let mut legacy_re = [99.0; 4]; + let mut legacy_im = [-99.0; 4]; + assert_eq!( + invoke_union( + &executor, + &rows, + &variants, + &embeddings, + &selections, + &mut legacy_re, + &mut legacy_im, + &momentum, + 1, + &[], + &factors_re, + &factors_im, + 1, + 1, + ), + DIRECT_STATUS_OK + ); + + let mut bound_re = [99.0; 4]; + let mut bound_im = [-99.0; 4]; + invoke_bound_union( + &executor, + &rows, + &variants, + &embeddings, + &selections, + &exact_union_factors(), + &mut bound_re, + &mut bound_im, + &momentum, + 1, + &[], + 1, + 1, + ) + .unwrap(); + assert_eq!(bound_re, legacy_re); + assert_eq!(bound_im, legacy_im); + } + + #[test] + fn cold_bound_union_source_rejects_corrupt_catalogs_before_execution() { + let executor = LoadedDirectSourceExecutor::load(vec![runtime_domain([( + 0, + DirectSourceTemplateSpec { + spin_state_class: 0, + family: DirectSourceWavefunctionFamily::Scalar, + orientation: DirectSourceOrientation::SelfConjugate, + helicity: 0, + chirality: 0, + mass_parameter_index: None, + }, + )])]) + .unwrap(); + let rows = [source_row(0, 0, 0, 0, 0)]; + let variant = DirectSourceDispatchVariantDescriptor { + embedding_start: 0, + projection_start: 0, + source_row_id: 0, + dispatch_domain_id: 0, + runtime_variant_id: 0, + source_state_index: 0, + source_template_id: 0, + source_state_template_id: 0, + crossed_state_template_id: 0, + crossed_spin_state_class: 0, + direct_executor_id: 0, + crossing_exact_factor_id: 0, + embedding_count: 1, + projection_count: 1, + }; + let embedding = DirectSourceEmbeddingRow { + full_component: 0, + source_component_or_sentinel: 0, + exact_factor_id: 0, + }; + let selection = DirectResolvedSourceSelection { + source_slot: 0, + dispatch_variant_id: 0, + }; + let factors = [ExactComplexRational::ONE]; + let bind = |rows: &[DirectSourceRow], + variants: &[DirectSourceDispatchVariantDescriptor], + embeddings: &[DirectSourceEmbeddingRow], + selections: &[DirectResolvedSourceSelection], + exact_factors: &[ExactComplexRational], + current_component_count: u32, + momentum_form_count: u32| { + executor.bind_union_program(OnTheFlyUnionSourceBindingView { + sources: rows, + variants, + embeddings, + selections, + exact_factors, + current_component_count, + momentum_form_count, + parameter_count: 0, + }) + }; + + let bad_selection = DirectResolvedSourceSelection { + source_slot: 1, + ..selection + }; + assert!( + bind( + &rows, + &[variant], + &[embedding], + &[bad_selection], + &factors, + 1, + 1, + ) + .is_err() + ); + let bad_variant_id = DirectResolvedSourceSelection { + dispatch_variant_id: 1, + ..selection + }; + assert!( + bind( + &rows, + &[variant], + &[embedding], + &[bad_variant_id], + &factors, + 1, + 1, + ) + .is_err() + ); + let mut bad_domain = variant; + bad_domain.dispatch_domain_id = 1; + assert!( + bind( + &rows, + &[bad_domain], + &[embedding], + &[selection], + &factors, + 1, + 1, + ) + .is_err() + ); + let mut bad_template = variant; + bad_template.crossed_spin_state_class = 1; + assert!( + bind( + &rows, + &[bad_template], + &[embedding], + &[selection], + &factors, + 1, + 1, + ) + .is_err() + ); + let mut bad_range = variant; + bad_range.embedding_start = 1; + assert!( + bind( + &rows, + &[bad_range], + &[embedding], + &[selection], + &factors, + 1, + 1, + ) + .is_err() + ); + let bad_order = DirectSourceEmbeddingRow { + full_component: 1, + ..embedding + }; + assert!( + bind( + &rows, + &[variant], + &[bad_order], + &[selection], + &factors, + 1, + 1, + ) + .is_err() + ); + let bad_source_component = DirectSourceEmbeddingRow { + source_component_or_sentinel: 1, + ..embedding + }; + assert!( + bind( + &rows, + &[variant], + &[bad_source_component], + &[selection], + &factors, + 1, + 1, + ) + .is_err() + ); + let bad_factor = DirectSourceEmbeddingRow { + exact_factor_id: 1, + ..embedding + }; + assert!( + bind( + &rows, + &[variant], + &[bad_factor], + &[selection], + &factors, + 1, + 1, + ) + .is_err() + ); + assert!( + bind( + &rows, + &[variant], + &[embedding], + &[selection], + &factors, + 0, + 1, + ) + .is_err() + ); + assert!( + bind( + &rows, + &[variant], + &[embedding], + &[selection], + &factors, + 1, + 0, + ) + .is_err() + ); + let bad_destination = [source_row(0, 0, 1, 0, 0)]; + assert!( + bind( + &bad_destination, + &[variant], + &[embedding], + &[selection], + &factors, + 1, + 1, + ) + .is_err() + ); + let bad_momentum = [source_row(0, 0, 0, 1, 0)]; + assert!( + bind( + &bad_momentum, + &[variant], + &[embedding], + &[selection], + &factors, + 1, + 1, + ) + .is_err() + ); + let bad_row_factor = [source_row(0, 0, 0, 0, 1)]; + assert!( + bind( + &bad_row_factor, + &[variant], + &[embedding], + &[selection], + &factors, + 1, + 1, + ) + .is_err() + ); + let mut bad_crossing_factor = variant; + bad_crossing_factor.crossing_exact_factor_id = 1; + assert!( + bind( + &rows, + &[bad_crossing_factor], + &[embedding], + &[selection], + &factors, + 1, + 1, + ) + .is_err() + ); + let nonunit = + ExactComplexRational::new(ExactRational::new(2, 1).unwrap(), ExactRational::ZERO); + assert!( + bind( + &rows, + &[variant], + &[embedding], + &[selection], + &[nonunit], + 1, + 1, + ) + .is_err() + ); + } + + #[test] + fn union_dispatch_rejects_malformed_selection_variant_and_embedding_ranges() { + let executor = LoadedDirectSourceExecutor::load(vec![runtime_domain([( + 0, + DirectSourceTemplateSpec { + spin_state_class: 0, + family: DirectSourceWavefunctionFamily::Scalar, + orientation: DirectSourceOrientation::SelfConjugate, + helicity: 0, + chirality: 0, + mass_parameter_index: None, + }, + )])]) + .unwrap(); + let rows = [source_row(0, 0, 0, 0, 0)]; + let base_variant = DirectSourceDispatchVariantDescriptor { + embedding_start: 0, + projection_start: 0, + source_row_id: 0, + dispatch_domain_id: 0, + runtime_variant_id: 0, + source_state_index: 0, + source_template_id: 0, + source_state_template_id: 0, + crossed_state_template_id: 0, + crossed_spin_state_class: 0, + direct_executor_id: 0, + crossing_exact_factor_id: 0, + embedding_count: 1, + projection_count: 1, + }; + let embeddings = [DirectSourceEmbeddingRow { + full_component: 0, + source_component_or_sentinel: 0, + exact_factor_id: 0, + }]; + let mut current_re = [7.0]; + let mut current_im = [9.0]; + let momenta = [1.0, 0.0, 0.0, 1.0]; + + let bad_variant = [DirectResolvedSourceSelection { + source_slot: 0, + dispatch_variant_id: 1, + }]; + assert_eq!( + invoke_union( + &executor, + &rows, + &[base_variant], + &embeddings, + &bad_variant, + &mut current_re, + &mut current_im, + &momenta, + 1, + &[], + &[1.0], + &[0.0], + 1, + 1, + ), + STATUS_BOUNDS + ); + + let bad_order = [DirectResolvedSourceSelection { + source_slot: 1, + dispatch_variant_id: 0, + }]; + assert_eq!( + invoke_union( + &executor, + &rows, + &[base_variant], + &embeddings, + &bad_order, + &mut current_re, + &mut current_im, + &momenta, + 1, + &[], + &[1.0], + &[0.0], + 1, + 1, + ), + STATUS_INVALID_ARGUMENT + ); + + let mut bad_embedding = base_variant; + bad_embedding.embedding_start = 2; + let good_selection = [DirectResolvedSourceSelection { + source_slot: 0, + dispatch_variant_id: 0, + }]; + assert_eq!( + invoke_union( + &executor, + &rows, + &[bad_embedding], + &embeddings, + &good_selection, + &mut current_re, + &mut current_im, + &momenta, + 1, + &[], + &[1.0], + &[0.0], + 1, + 1, + ), + STATUS_BOUNDS + ); + assert_eq!(current_re, [7.0]); + assert_eq!(current_im, [9.0]); + } +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/evaluator/stage.rs b/pyamplicol/rust/crates/rusticol-core/src/evaluator/stage.rs new file mode 100644 index 0000000000..6ab96e8d62 --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/evaluator/stage.rs @@ -0,0 +1,940 @@ +// SPDX-License-Identifier: 0BSD + +use super::super::*; + +impl StageRuntime { + pub(crate) fn load( + stage: &GenericSerializedStageEvaluatorManifest, + payloads: &EvaluatorPayloadStore, + ) -> RusticolResult { + let evaluator = EvaluatorGroup::load_from_store(&stage.evaluator, payloads)?; + Self::load_with_evaluator(stage, evaluator, false) + } + + #[cfg(feature = "symbolica-runtime")] + pub(crate) fn load_exact_from_plane( + stage: &GenericSerializedStageEvaluatorManifest, + payloads: &EvaluatorPayloadStore, + ) -> RusticolResult { + let evaluator = EvaluatorGroup::load_exact_from_plane(stage, payloads)?; + Self::load_with_evaluator(stage, evaluator, true) + } + + fn load_with_evaluator( + stage: &GenericSerializedStageEvaluatorManifest, + evaluator: EvaluatorGroup, + plane_bindings: bool, + ) -> RusticolResult { + let mut outputs = Vec::new(); + if plane_bindings { + let direct = stage.compiled_plane_arena.as_ref().ok_or_else(|| { + RusticolError::integrity( + "compiled plane-arena exact stage has no serialized bindings", + ) + })?; + for binding in &direct.output_bindings { + if binding.arena != "current" { + return Err(RusticolError::integrity( + "compiled plane-arena exact current stage names the wrong output arena", + )); + } + outputs.push((binding.output_index, binding.component)); + } + } else { + for slot in &stage.output_slots { + let output_len = + slot.output_stop + .checked_sub(slot.output_start) + .ok_or_else(|| { + RusticolError::invalid_argument(format!( + "generic stage {} has an invalid output slot range", + stage.evaluator_label + )) + })?; + let component_len = slot + .component_stop + .checked_sub(slot.component_start) + .ok_or_else(|| { + RusticolError::invalid_argument(format!( + "generic stage {} has an invalid component slot range", + stage.evaluator_label + )) + })?; + if output_len != component_len { + return Err(RusticolError::invalid_argument(format!( + "generic stage {} output slot length does not match component length", + stage.evaluator_label + ))); + } + for component in 0..component_len { + outputs.push(( + slot.output_start + component, + slot.component_start + component, + )); + } + } + } + outputs.sort_unstable_by_key(|(column, _state_offset)| *column); + let (input_components, input_spans) = + if stage.parameter_layout == "stage-local-value-momentum" { + let mut map = vec![0usize; stage.parameter_count]; + if plane_bindings { + let direct = stage.compiled_plane_arena.as_ref().ok_or_else(|| { + RusticolError::integrity( + "compiled plane-arena exact stage has no serialized bindings", + ) + })?; + for component in &direct.input_bindings { + map[component.parameter_index] = component.global_component; + } + } else { + for component in &stage.input_components { + map[component.parameter_index] = component.global_component; + } + } + let spans = contiguous_input_spans(&map); + (Some(map), spans) + } else { + (None, Vec::new()) + }; + let output_spans = contiguous_output_spans(&outputs); + let (chunk_outputs, chunk_output_spans) = + localized_chunk_output_layouts(&evaluator.output_chunk_lengths(), &outputs)?; + Ok(Self { + outputs, + output_spans, + chunk_outputs, + chunk_output_spans, + input_components, + input_spans, + parameter_scratch_f64: Vec::new(), + output_scratch_f64: Vec::new(), + evaluator, + }) + } + + pub(crate) fn evaluate_f64_into_state( + &mut self, + batch_size: usize, + parameter_count: usize, + state: &mut [Complex], + ) -> RusticolResult<()> { + let evaluator_params = if let Some(input_components) = self.input_components.as_ref() { + let local_parameter_count = input_components.len(); + self.parameter_scratch_f64 + .resize(batch_size * local_parameter_count, c64(0.0, 0.0)); + for row in 0..batch_size { + let row_state = row * parameter_count; + let row_params = row * local_parameter_count; + if self.input_spans.is_empty() { + for (local_index, global_index) in input_components.iter().enumerate() { + self.parameter_scratch_f64[row_params + local_index] = + state[row_state + *global_index]; + } + } else { + for (local_start, global_start, len) in &self.input_spans { + let target_start = row_params + *local_start; + let source_start = row_state + *global_start; + self.parameter_scratch_f64[target_start..target_start + *len] + .copy_from_slice(&state[source_start..source_start + *len]); + } + } + } + if self.evaluator.is_chunked() { + return self.evaluator.evaluate_chunks_f64_into_state( + batch_size, + &self.parameter_scratch_f64, + parameter_count, + state, + &self.chunk_outputs, + &self.chunk_output_spans, + ); + } + self.parameter_scratch_f64.as_slice() + } else { + &*state + }; + self.evaluator.evaluate_batch_into( + batch_size, + evaluator_params, + &mut self.output_scratch_f64, + )?; + for row in 0..batch_size { + let row_state = row * parameter_count; + let row_eval = row * self.evaluator.output_len; + if self.output_spans.is_empty() { + for (column, state_offset) in &self.outputs { + state[row_state + *state_offset] = self.output_scratch_f64[row_eval + *column]; + } + } else { + for (column_start, state_offset_start, len) in &self.output_spans { + let source_start = row_eval + *column_start; + let target_start = row_state + *state_offset_start; + state[target_start..target_start + *len].copy_from_slice( + &self.output_scratch_f64[source_start..source_start + *len], + ); + } + } + } + Ok(()) + } + + pub(crate) fn evaluate_f64_into_state_profile( + &mut self, + batch_size: usize, + parameter_count: usize, + state: &mut [Complex], + ) -> RusticolResult { + let mut input_pack_elapsed = Duration::ZERO; + let evaluator; + let mut scratch_reallocation_count = 0; + if let Some(input_components) = self.input_components.as_ref() { + let local_parameter_count = input_components.len(); + let pack_start = Instant::now(); + let capacity = self.parameter_scratch_f64.capacity(); + self.parameter_scratch_f64 + .resize(batch_size * local_parameter_count, c64(0.0, 0.0)); + scratch_reallocation_count += + u64::from(self.parameter_scratch_f64.capacity() != capacity); + for row in 0..batch_size { + let row_state = row * parameter_count; + let row_params = row * local_parameter_count; + if self.input_spans.is_empty() { + for (local_index, global_index) in input_components.iter().enumerate() { + self.parameter_scratch_f64[row_params + local_index] = + state[row_state + *global_index]; + } + } else { + for (local_start, global_start, len) in &self.input_spans { + let target_start = row_params + *local_start; + let source_start = row_state + *global_start; + self.parameter_scratch_f64[target_start..target_start + *len] + .copy_from_slice(&state[source_start..source_start + *len]); + } + } + } + input_pack_elapsed = pack_start.elapsed(); + if self.evaluator.is_chunked() { + let (evaluator, assign_s) = self.evaluator.evaluate_chunks_f64_into_state_profile( + batch_size, + &self.parameter_scratch_f64, + parameter_count, + state, + &self.chunk_outputs, + &self.chunk_output_spans, + )?; + return Ok(StageEvaluationProfile { + input_pack_s: input_pack_elapsed.as_secs_f64(), + evaluator, + output_assign_s: assign_s, + input_copy_component_count: (batch_size * local_parameter_count) as u64, + output_assign_component_count: (batch_size * self.outputs.len()) as u64, + scratch_reallocation_count, + }); + } + evaluator = self.evaluator.evaluate_batch_into_profile( + batch_size, + &self.parameter_scratch_f64, + &mut self.output_scratch_f64, + false, + )?; + } else { + evaluator = self.evaluator.evaluate_batch_into_profile( + batch_size, + state, + &mut self.output_scratch_f64, + false, + )?; + } + + let assign_start = Instant::now(); + for row in 0..batch_size { + let row_state = row * parameter_count; + let row_eval = row * self.evaluator.output_len; + if self.output_spans.is_empty() { + for (column, state_offset) in &self.outputs { + state[row_state + *state_offset] = self.output_scratch_f64[row_eval + *column]; + } + } else { + for (column_start, state_offset_start, len) in &self.output_spans { + let source_start = row_eval + *column_start; + let target_start = row_state + *state_offset_start; + state[target_start..target_start + *len].copy_from_slice( + &self.output_scratch_f64[source_start..source_start + *len], + ); + } + } + } + Ok(StageEvaluationProfile { + input_pack_s: input_pack_elapsed.as_secs_f64(), + evaluator, + output_assign_s: assign_start.elapsed().as_secs_f64(), + input_copy_component_count: self + .input_components + .as_ref() + .map_or(0, |components| batch_size * components.len()) + as u64, + output_assign_component_count: (batch_size * self.outputs.len()) as u64, + scratch_reallocation_count, + }) + } + + pub(crate) fn evaluate_active_chunks_f64_into_state( + &mut self, + batch_size: usize, + parameter_count: usize, + state: &mut [Complex], + active_chunk_indices: &[usize], + ) -> RusticolResult<()> { + self.evaluator.evaluate_selected_chunks_f64_into_state( + batch_size, + parameter_count, + state, + self.input_components.as_deref(), + &self.input_spans, + &self.chunk_outputs, + &self.chunk_output_spans, + active_chunk_indices, + ) + } + + pub(crate) fn evaluate_active_chunks_f64_into_state_profile( + &mut self, + batch_size: usize, + parameter_count: usize, + state: &mut [Complex], + active_chunk_indices: &[usize], + ) -> RusticolResult { + let (evaluator, output_assign_s) = self + .evaluator + .evaluate_selected_chunks_f64_into_state_profile( + batch_size, + parameter_count, + state, + self.input_components.as_deref(), + &self.input_spans, + &self.chunk_outputs, + &self.chunk_output_spans, + active_chunk_indices, + )?; + Ok(StageEvaluationProfile { + input_pack_s: evaluator.leaf_input_pack_s, + output_assign_s, + // The selected-chunk evaluator gathers directly from global + // state into leaf inputs. There is no distinct parent stage-input + // copy to count in addition to the evaluator's leaf gather. + input_copy_component_count: 0, + output_assign_component_count: active_chunk_indices + .iter() + .map(|index| self.chunk_outputs[*index].len() as u64 * batch_size as u64) + .sum(), + scratch_reallocation_count: 0, + evaluator, + }) + } + + #[cfg(feature = "symbolica-runtime")] + pub(crate) fn evaluate_generic_into_state( + &mut self, + batch_size: usize, + parameter_count: usize, + state: &mut [Complex], + binary_precision: Option, + ) -> RusticolResult<(f64, f64, f64)> + where + T: RusticolHighPrecisionNumber, + Complex: Real + EvaluationDomain, + { + self.evaluate_selected_chunks_generic_into_state( + batch_size, + parameter_count, + state, + binary_precision, + None, + ) + } + + #[cfg(feature = "symbolica-runtime")] + pub(crate) fn evaluate_selected_chunks_generic_into_state( + &mut self, + batch_size: usize, + parameter_count: usize, + state: &mut [Complex], + binary_precision: Option, + active_chunk_indices: Option<&[usize]>, + ) -> RusticolResult<(f64, f64, f64)> + where + T: RusticolHighPrecisionNumber, + Complex: Real + EvaluationDomain, + { + let mut input_pack_s = 0.0; + let (evaluated, evaluator_s) = + if let Some(input_components) = self.input_components.as_ref() { + let local_parameter_count = input_components.len(); + let pack_start = Instant::now(); + let mut parameter_scratch = + vec![complex_zero::(); batch_size * local_parameter_count]; + for row in 0..batch_size { + let row_state = row * parameter_count; + let row_params = row * local_parameter_count; + if self.input_spans.is_empty() { + for (local_index, global_index) in input_components.iter().enumerate() { + parameter_scratch[row_params + local_index] = + state[row_state + *global_index].clone(); + } + } else { + for (local_start, global_start, len) in &self.input_spans { + let target_start = row_params + *local_start; + let source_start = row_state + *global_start; + parameter_scratch[target_start..target_start + *len] + .clone_from_slice(&state[source_start..source_start + *len]); + } + } + } + input_pack_s = pack_start.elapsed().as_secs_f64(); + let eval_start = Instant::now(); + let evaluated = self.evaluator.evaluate_selected_chunks_generic( + batch_size, + ¶meter_scratch, + binary_precision, + active_chunk_indices, + )?; + (evaluated, eval_start.elapsed().as_secs_f64()) + } else { + let eval_start = Instant::now(); + let evaluated = self.evaluator.evaluate_selected_chunks_generic( + batch_size, + state, + binary_precision, + active_chunk_indices, + )?; + (evaluated, eval_start.elapsed().as_secs_f64()) + }; + + if let Some(indices) = active_chunk_indices { + // Inactive chunks may share storage with still-live currents. + // Assign only the selected leaves, never their zero placeholders. + let assign_start = Instant::now(); + let mut offset = 0; + let chunk_offsets = self + .evaluator + .output_chunk_lengths() + .into_iter() + .map(|length| { + let start = offset; + offset += length; + start + }) + .collect::>(); + for row in 0..batch_size { + let row_state = row * parameter_count; + let row_eval = row * self.evaluator.output_len; + for &index in indices { + for &(column, state_offset) in &self.chunk_outputs[index] { + state[row_state + state_offset] = + evaluated[row_eval + chunk_offsets[index] + column].clone(); + } + } + } + return Ok(( + input_pack_s, + evaluator_s, + assign_start.elapsed().as_secs_f64(), + )); + } + self.assign_generic_outputs( + batch_size, + parameter_count, + state, + evaluated, + input_pack_s, + evaluator_s, + ) + } + + #[cfg(feature = "symbolica-runtime")] + pub(crate) fn assign_generic_outputs( + &self, + batch_size: usize, + parameter_count: usize, + state: &mut [Complex], + evaluated: Vec>, + input_pack_s: f64, + evaluator_s: f64, + ) -> RusticolResult<(f64, f64, f64)> + where + T: RusticolHighPrecisionNumber, + Complex: Real + EvaluationDomain, + { + let assign_start = Instant::now(); + for row in 0..batch_size { + let row_state = row * parameter_count; + let row_eval = row * self.evaluator.output_len; + if self.output_spans.is_empty() { + for (column, state_offset) in &self.outputs { + state[row_state + *state_offset] = evaluated[row_eval + *column].clone(); + } + } else { + for (column_start, state_offset_start, len) in &self.output_spans { + let source_start = row_eval + *column_start; + let target_start = row_state + *state_offset_start; + state[target_start..target_start + *len] + .clone_from_slice(&evaluated[source_start..source_start + *len]); + } + } + } + Ok(( + input_pack_s, + evaluator_s, + assign_start.elapsed().as_secs_f64(), + )) + } +} + +type ChunkOutputs = Vec>; +type ChunkOutputSpans = Vec>; + +fn localized_chunk_output_layouts( + chunk_lengths: &[usize], + outputs: &[(usize, usize)], +) -> RusticolResult<(ChunkOutputs, ChunkOutputSpans)> { + if chunk_lengths.iter().sum::() != outputs.len() { + return Err(RusticolError::invalid_argument( + "stage output layout length does not match evaluator output length", + )); + } + let mut chunk_outputs = Vec::with_capacity(chunk_lengths.len()); + let mut chunk_spans = Vec::with_capacity(chunk_lengths.len()); + let mut output_offset = 0usize; + for chunk_len in chunk_lengths { + let chunk_stop = output_offset + *chunk_len; + let mut localized = Vec::with_capacity(*chunk_len); + for (expected_column, (column, state_offset)) in outputs[output_offset..chunk_stop] + .iter() + .copied() + .enumerate() + { + let expected_global_column = output_offset + expected_column; + if column != expected_global_column { + return Err(RusticolError::invalid_argument( + "stage output columns are not contiguous in evaluator order", + )); + } + localized.push((expected_column, state_offset)); + } + chunk_spans.push(contiguous_output_spans(&localized)); + chunk_outputs.push(localized); + output_offset = chunk_stop; + } + Ok((chunk_outputs, chunk_spans)) +} + +pub(crate) fn contiguous_input_spans(input_components: &[usize]) -> Vec<(usize, usize, usize)> { + if input_components.is_empty() { + return Vec::new(); + } + let mut spans = Vec::new(); + let mut local_start = 0usize; + let mut global_start = input_components[0]; + let mut previous_local = local_start; + let mut previous_global = global_start; + let mut len = 1usize; + for (local, global) in input_components.iter().copied().enumerate().skip(1) { + if local == previous_local + 1 && global == previous_global + 1 { + previous_local = local; + previous_global = global; + len += 1; + continue; + } + spans.push((local_start, global_start, len)); + local_start = local; + global_start = global; + previous_local = local; + previous_global = global; + len = 1; + } + spans.push((local_start, global_start, len)); + if spans.len() >= input_components.len() { + Vec::new() + } else { + spans + } +} + +pub(crate) fn contiguous_output_spans(outputs: &[(usize, usize)]) -> Vec<(usize, usize, usize)> { + if outputs.is_empty() { + return Vec::new(); + } + let mut spans = Vec::new(); + let (mut output_start, mut state_start) = outputs[0]; + let mut previous_output = output_start; + let mut previous_state = state_start; + let mut len = 1usize; + for (output, state) in outputs.iter().copied().skip(1) { + if output == previous_output + 1 && state == previous_state + 1 { + previous_output = output; + previous_state = state; + len += 1; + continue; + } + spans.push((output_start, state_start, len)); + output_start = output; + state_start = state; + previous_output = output; + previous_state = state; + len = 1; + } + spans.push((output_start, state_start, len)); + if spans.len() >= outputs.len() { + Vec::new() + } else { + spans + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[cfg(all( + feature = "f64-compiled", + any(target_os = "linux", target_os = "macos") + ))] + use std::process::Command; + #[cfg(all( + feature = "f64-compiled", + any(target_os = "linux", target_os = "macos") + ))] + use std::time::{SystemTime, UNIX_EPOCH}; + + #[test] + fn chunk_output_layouts_are_localized_without_losing_state_offsets() { + let outputs = vec![(0, 10), (1, 11), (2, 20), (3, 22), (4, 23)]; + let (chunks, spans) = localized_chunk_output_layouts(&[2, 3], &outputs).unwrap(); + + assert_eq!(chunks[0], vec![(0, 10), (1, 11)]); + assert_eq!(chunks[1], vec![(0, 20), (1, 22), (2, 23)]); + assert_eq!(spans[0], vec![(0, 10, 2)]); + assert_eq!(spans[1], vec![(0, 20, 1), (1, 22, 2)]); + } + + #[test] + fn chunk_output_layouts_reject_inconsistent_evaluator_metadata() { + let length_error = localized_chunk_output_layouts(&[1], &[(0, 10), (1, 11)]) + .expect_err("chunk lengths must cover every output"); + assert!( + length_error + .to_string() + .contains("layout length does not match") + ); + + let order_error = localized_chunk_output_layouts(&[2], &[(0, 10), (2, 11)]) + .expect_err("output columns must be contiguous"); + assert!( + order_error + .to_string() + .contains("columns are not contiguous") + ); + } + + #[cfg(feature = "symbolica-runtime")] + #[test] + fn generic_selected_chunks_preserve_inactive_live_current_slots() { + use symbolica::{atom::AtomCore, parse}; + + let leaf = |expression: &str| LoadedEvaluator { + eval: F64Evaluator::ExactOnly, + exact_eval: Some( + parse!(expression) + .evaluator(&[parse!("selected_chunk_x")]) + .build() + .unwrap(), + ), + exact_eval_source: None, + double_eval: None, + arb_eval: None, + input_len: 1, + output_len: 1, + }; + let evaluator = EvaluatorGroup { + evaluators: vec![ + leaf("selected_chunk_x + 100"), + leaf("selected_chunk_x + 200"), + ], + input_len: 2, + input_mappings: vec![Some(vec![0]), Some(vec![1])], + input_mapping_spans: vec![Vec::new(), Vec::new()], + output_len: 2, + chunk_parameter_scratch_f64: Vec::new(), + chunk_scratch_f64: Vec::new(), + chunk_parameter_scratch_aosoa_f64: Vec::new(), + chunk_scratch_aosoa_f64: Vec::new(), + chunk_input_mapping_scratch: Vec::new(), + }; + let mut stage = StageRuntime { + outputs: vec![(0, 1), (1, 3)], + output_spans: Vec::new(), + chunk_outputs: vec![vec![(0, 1)], vec![(0, 3)]], + chunk_output_spans: vec![Vec::new(), Vec::new()], + input_components: Some(vec![2, 0]), + input_spans: Vec::new(), + parameter_scratch_f64: Vec::new(), + output_scratch_f64: Vec::new(), + evaluator, + }; + let number = |value| c_generic(DoubleFloat::from(value), DoubleFloat::from(0.0)); + let initial = [3.0, -99.0, 7.0, -99.0, 11.0, -99.0, 13.0, -99.0] + .map(number) + .to_vec(); + let mut state = initial.clone(); + stage + .evaluate_selected_chunks_generic_into_state(2, 4, &mut state, None, Some(&[1])) + .unwrap(); + assert!(stage.evaluator.evaluators[0].double_eval.is_none()); + assert!(stage.evaluator.evaluators[1].double_eval.is_some()); + assert_eq!(state[1], number(-99.0)); + assert_eq!(state[5], number(-99.0)); + assert_eq!(state[3], number(203.0)); + assert_eq!(state[7], number(211.0)); + + stage + .evaluate_selected_chunks_generic_into_state(2, 4, &mut state, None, Some(&[0])) + .unwrap(); + assert_eq!(state[1], number(107.0)); + assert_eq!(state[5], number(113.0)); + assert_eq!(state[3], number(203.0)); + assert_eq!(state[7], number(211.0)); + let mut complete = initial; + stage + .evaluate_generic_into_state(2, 4, &mut complete, None) + .unwrap(); + assert_eq!(state, complete); + for invalid in [&[1, 1][..], &[1, 0][..], &[2][..]] { + assert!( + stage + .evaluate_selected_chunks_generic_into_state( + 2, + 4, + &mut state, + None, + Some(invalid), + ) + .is_err() + ); + assert_eq!(state, complete); + } + } + + #[cfg(all( + feature = "f64-compiled", + any(target_os = "linux", target_os = "macos") + ))] + #[test] + fn alternating_selected_stage_chunks_cannot_leak_inactive_prior_state() { + let (directory, library, marker) = selective_stage_fixture(); + let library_name = library.file_name().unwrap().to_str().unwrap().to_string(); + let leaf = |function_name: &str| EvaluatorManifest::CompiledComplex { + runtime_capability: SYMBOLICA_COMPILED_CPP_RUNTIME_CAPABILITY.to_string(), + function_name: function_name.to_string(), + input_len: 1, + output_len: 1, + library_path: library_name.clone(), + evaluator_state_path: None, + number_type: "complex".to_string(), + native_direct_application: None, + }; + let evaluator = EvaluatorGroup::load( + &EvaluatorManifest::Chunked { + required_runtime_capabilities: vec![ + SYMBOLICA_COMPILED_CPP_RUNTIME_CAPABILITY.to_string(), + ], + input_len: Some(2), + chunk_input_indices: Some(vec![vec![0], vec![1]]), + chunks: vec![leaf("stage_chunk_0"), leaf("stage_chunk_1")], + }, + &directory, + ) + .unwrap(); + let outputs = vec![(0, 1), (1, 3)]; + let mut stage = StageRuntime { + output_spans: contiguous_output_spans(&outputs), + outputs, + chunk_outputs: vec![vec![(0, 1)], vec![(0, 3)]], + chunk_output_spans: vec![Vec::new(), Vec::new()], + input_components: Some(vec![2, 0]), + input_spans: Vec::new(), + parameter_scratch_f64: Vec::new(), + output_scratch_f64: Vec::new(), + evaluator, + }; + + let sentinel = c64(-99.0, 0.0); + let mut selected_state = vec![c64(3.0, 0.0), sentinel, c64(7.0, 0.0), sentinel]; + let selected_profile = stage + .evaluate_active_chunks_f64_into_state_profile(1, 4, &mut selected_state, &[1]) + .unwrap(); + assert_eq!(selected_state[1], sentinel, "inactive output was assigned"); + assert_eq!(selected_state[3], c64(203.0, 0.0)); + assert_eq!( + selected_profile.input_copy_component_count, 0, + "composed selected-chunk gather is not a separate parent input copy" + ); + assert_eq!( + selected_profile.evaluator.leaf_input_copy_component_count, 1, + "the composed selected-chunk gather must be counted once as a leaf input" + ); + assert_eq!(fs::read_to_string(&marker).unwrap(), "1"); + + let parameter_capacity = stage.evaluator.chunk_parameter_scratch_f64.capacity(); + let output_capacity = stage.evaluator.chunk_scratch_f64.capacity(); + selected_state[3] = sentinel; + stage + .evaluate_active_chunks_f64_into_state(1, 4, &mut selected_state, &[1]) + .unwrap(); + assert_eq!( + stage.evaluator.chunk_parameter_scratch_f64.capacity(), + parameter_capacity + ); + assert_eq!( + stage.evaluator.chunk_scratch_f64.capacity(), + output_capacity + ); + + let marker_before_invalid = fs::read_to_string(&marker).unwrap(); + for invalid in [&[1, 1][..], &[1, 0][..], &[2][..]] { + assert!( + stage + .evaluate_active_chunks_f64_into_state(1, 4, &mut selected_state, invalid) + .is_err() + ); + } + assert_eq!(fs::read_to_string(&marker).unwrap(), marker_before_invalid); + + fs::write(&marker, "").unwrap(); + let mut full_state = vec![c64(3.0, 0.0), sentinel, c64(7.0, 0.0), sentinel]; + stage + .evaluate_f64_into_state(1, 4, &mut full_state) + .unwrap(); + assert_eq!(full_state[1], c64(107.0, 0.0)); + assert_eq!(full_state[3], c64(203.0, 0.0)); + assert_eq!(fs::read_to_string(&marker).unwrap(), "01"); + + // The execution runtime clears its reusable state before every + // selector-pruned schedule. Alternate schedules on the same backing + // allocation to ensure inactive outputs cannot retain a prior value. + for chunk_index in [0, 1, 0, 1] { + selected_state.fill(c64(0.0, 0.0)); + selected_state[0] = c64(3.0, 0.0); + selected_state[2] = c64(7.0, 0.0); + stage + .evaluate_active_chunks_f64_into_state(1, 4, &mut selected_state, &[chunk_index]) + .unwrap(); + if chunk_index == 0 { + assert_eq!(selected_state[1], c64(107.0, 0.0)); + assert_eq!(selected_state[3], c64(0.0, 0.0)); + } else { + assert_eq!(selected_state[1], c64(0.0, 0.0)); + assert_eq!(selected_state[3], c64(203.0, 0.0)); + } + } + + let parent_inputs = [c64(7.0, 0.0), c64(3.0, 0.0)]; + let mut selected_outputs = vec![sentinel; 2]; + for chunk_index in [0, 1, 0, 1] { + stage + .evaluator + .evaluate_selected_chunks_f64_into_output( + 1, + &parent_inputs, + None, + &[], + &mut selected_outputs, + &[chunk_index], + ) + .unwrap(); + if chunk_index == 0 { + assert_eq!(selected_outputs, vec![c64(107.0, 0.0), c64(0.0, 0.0)]); + } else { + assert_eq!(selected_outputs, vec![c64(0.0, 0.0), c64(203.0, 0.0)]); + } + } + + drop(stage); + fs::remove_dir_all(directory).unwrap(); + } + + #[cfg(all( + feature = "f64-compiled", + any(target_os = "linux", target_os = "macos") + ))] + fn selective_stage_fixture() -> (PathBuf, PathBuf, PathBuf) { + let unique = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + let directory = std::env::temp_dir().join(format!( + "rusticol-selective-stage-test-{}-{unique}", + std::process::id() + )); + fs::create_dir_all(&directory).unwrap(); + let source = directory.join("fixture.cpp"); + let marker = directory.join("calls.txt"); + fs::write(&marker, "").unwrap(); + let library = directory.join(if cfg!(target_os = "macos") { + "libfixture.dylib" + } else { + "libfixture.so" + }); + let marker_literal = marker.to_string_lossy().replace('\\', "\\\\"); + fs::write( + &source, + format!( + r#"#include +#include +static void mark(const char* value) {{ + std::FILE* file = std::fopen("{marker_literal}", "a"); + std::fputs(value, file); + std::fclose(file); +}} +extern "C" unsigned long stage_chunk_0_complexf64_get_buffer_len() {{ return 1; }} +extern "C" void stage_chunk_0_complexf64( + std::complex* params, + std::complex*, + std::complex* out) {{ + mark("0"); + out[0] = params[0] + std::complex(100.0, 0.0); +}} +extern "C" unsigned long stage_chunk_1_complexf64_get_buffer_len() {{ return 1; }} +extern "C" void stage_chunk_1_complexf64( + std::complex* params, + std::complex*, + std::complex* out) {{ + mark("1"); + out[0] = params[0] + std::complex(200.0, 0.0); +}} +"# + ), + ) + .unwrap(); + let compiler = std::env::var("CXX").unwrap_or_else(|_| "c++".to_string()); + let mut command = Command::new(compiler); + command.arg("-std=c++17"); + if cfg!(target_os = "macos") { + command.arg("-dynamiclib"); + } else { + command.args(["-shared", "-fPIC"]); + } + let output = command + .arg(&source) + .arg("-o") + .arg(&library) + .output() + .unwrap(); + assert!( + output.status.success(), + "could not compile selective stage fixture: {}", + String::from_utf8_lossy(&output.stderr) + ); + (directory, library, marker) + } +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/evaluator/symjit.rs b/pyamplicol/rust/crates/rusticol-core/src/evaluator/symjit.rs new file mode 100644 index 0000000000..134f83de68 --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/evaluator/symjit.rs @@ -0,0 +1,1112 @@ +// SPDX-License-Identifier: 0BSD + +use super::super::*; +use std::any::Any; +use std::panic::{AssertUnwindSafe, catch_unwind}; +use symjit::{Applet, Application, Compiled, Config, Defuns, Storage}; + +pub(crate) struct SymjitApplicationEvaluator { + applet: Applet, + application_path: PathBuf, + input_len: usize, + output_len: usize, + input_tail_scratch: Vec>, + output_tail_scratch: Vec>, +} + +impl std::fmt::Debug for SymjitApplicationEvaluator { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter + .debug_struct("SymjitApplicationEvaluator") + .field("application_path", &self.application_path) + .field("input_len", &self.input_len) + .field("output_len", &self.output_len) + .finish_non_exhaustive() + } +} + +pub(crate) struct SymjitApplicationMetadata<'a> { + pub(crate) runtime_capability: &'a str, + pub(crate) application_abi: &'a str, + pub(crate) input_len: usize, + pub(crate) output_len: usize, + pub(crate) element_layout: &'a str, + pub(crate) batch_layout: &'a str, + pub(crate) compiler_type: &'a str, + pub(crate) translation_mode: &'a str, + pub(crate) optimization_level: u8, + pub(crate) word_bits: u8, + pub(crate) endianness: &'a str, + pub(crate) required_defuns: &'a [String], +} + +impl SymjitApplicationEvaluator { + #[cfg(test)] + pub(crate) fn load( + path: &Path, + metadata: SymjitApplicationMetadata<'_>, + ) -> RusticolResult { + let bytes = fs::read(path).map_err(|error| { + RusticolError::artifact(format!( + "could not read SymJIT application {}: {error}", + path.display() + )) + })?; + Self::load_bytes(&bytes, path.to_path_buf(), metadata) + } + + pub(crate) fn load_bytes( + bytes: &[u8], + display_path: PathBuf, + metadata: SymjitApplicationMetadata<'_>, + ) -> RusticolResult { + validate_manifest_metadata(&metadata)?; + + // This is the same trusted-input path used by Symbolica's + // JITCompiledEvaluator::load. + let mut loader_config = Config::default(); + loader_config.set_defuns(Defuns::new()); + let mut input = bytes; + let application = guard_symjit_panic( + || Application::load(&mut input, &loader_config), + |detail| { + RusticolError::compatibility(format!( + "SymJIT panicked while loading application {} with ABI {}: {detail}", + display_path.display(), + SYMJIT_APPLICATION_STORAGE_ABI + )) + }, + )? + .map_err(|error| { + RusticolError::compatibility(format!( + "could not load SymJIT application {} with ABI {}: {error}", + display_path.display(), + SYMJIT_APPLICATION_STORAGE_ABI + )) + })?; + validate_loaded_application(&display_path, &application, &metadata)?; + let applet = guard_symjit_panic( + || application.seal(), + |detail| { + RusticolError::compatibility(format!( + "SymJIT panicked while sealing application {}: {detail}", + display_path.display() + )) + }, + )? + .map_err(|error| { + RusticolError::evaluation(format!( + "could not seal SymJIT application {}: {error}", + display_path.display() + )) + })?; + Ok(Self { + applet, + application_path: display_path, + input_len: metadata.input_len, + output_len: metadata.output_len, + input_tail_scratch: Vec::new(), + output_tail_scratch: Vec::new(), + }) + } + + pub(crate) fn evaluate_batch( + &mut self, + batch_size: usize, + params: &[Complex], + out: &mut [Complex], + ) -> RusticolResult<()> { + validate_batch_lengths( + batch_size, + self.input_len, + self.output_len, + params.len(), + out.len(), + )?; + if batch_size == 0 { + return Ok(()); + } + let lane_width = super::native_f64_simd_lane_width(); + let remainder = batch_size % lane_width; + if remainder == 0 { + return evaluate_matrix( + &self.applet, + &self.application_path, + batch_size, + params, + out, + ); + } + + let aligned_batch_size = batch_size - remainder; + if aligned_batch_size != 0 { + evaluate_matrix( + &self.applet, + &self.application_path, + aligned_batch_size, + ¶ms[..aligned_batch_size * self.input_len], + &mut out[..aligned_batch_size * self.output_len], + )?; + } + + let padded_input_len = lane_width + .checked_mul(self.input_len) + .ok_or_else(|| RusticolError::invalid_argument("SymJIT SIMD input batch overflows"))?; + self.input_tail_scratch + .resize(padded_input_len, Complex::new(0.0, 0.0)); + let source_start = aligned_batch_size * self.input_len; + let source_end = source_start + remainder * self.input_len; + self.input_tail_scratch[..remainder * self.input_len] + .copy_from_slice(¶ms[source_start..source_end]); + let tail_row_start = (remainder - 1) * self.input_len; + for row in remainder..lane_width { + let target_start = row * self.input_len; + self.input_tail_scratch.copy_within( + tail_row_start..tail_row_start + self.input_len, + target_start, + ); + } + let padded_output_len = lane_width + .checked_mul(self.output_len) + .ok_or_else(|| RusticolError::invalid_argument("SymJIT SIMD output batch overflows"))?; + self.output_tail_scratch + .resize(padded_output_len, Complex::new(0.0, 0.0)); + evaluate_matrix( + &self.applet, + &self.application_path, + lane_width, + &self.input_tail_scratch, + &mut self.output_tail_scratch, + )?; + let output_start = aligned_batch_size * self.output_len; + let output_end = output_start + remainder * self.output_len; + out[output_start..output_end] + .copy_from_slice(&self.output_tail_scratch[..remainder * self.output_len]); + Ok(()) + } + + pub(crate) fn evaluate_batch_unpadded( + &self, + batch_size: usize, + params: &[Complex], + out: &mut [Complex], + ) -> RusticolResult<()> { + validate_batch_lengths( + batch_size, + self.input_len, + self.output_len, + params.len(), + out.len(), + )?; + evaluate_matrix( + &self.applet, + &self.application_path, + batch_size, + params, + out, + ) + } + + pub(crate) fn simd_lane_width(&self) -> Option { + if self.applet.use_threads { + // The direct AoSoA entry point below executes blocks serially. + // Preserve SymJIT's threaded matrix dispatch for applications that + // explicitly requested it. + return None; + } + self.applet + .compiled_simd + .as_ref() + .map(Compiled::count_lanes) + .filter(|lane_width| *lane_width != 0) + } + + /// Evaluates already-transposed SIMD blocks without a row-major staging copy. + /// + /// Each block contains `input_len` complex SIMD values in AoSoA order: all + /// real lanes followed by all imaginary lanes for one input, then the next + /// input. Outputs use the analogous `output_len` layout. `false` asks the + /// caller to fall back to the ordinary evaluator because no SIMD kernel is + /// available or the loaded kernel rejected a block. + pub(crate) fn evaluate_aosoa_blocks( + &self, + block_count: usize, + params: &[f64], + out: &mut [f64], + ) -> RusticolResult { + let Some(compiled) = self.applet.compiled_simd.as_ref() else { + return Ok(false); + }; + let lane_width = compiled.count_lanes(); + if lane_width == 0 { + return Ok(false); + } + validate_aosoa_lengths( + block_count, + lane_width, + self.input_len, + self.output_len, + params.len(), + out.len(), + )?; + if block_count == 0 { + return Ok(true); + } + + let input_block_len = lane_width * self.input_len * 2; + let output_block_len = lane_width * self.output_len * 2; + let function = compiled.func(); + guard_symjit_panic( + || { + for block in 0..block_count { + let status = function( + out[block * output_block_len..].as_mut_ptr(), + std::ptr::null(), + 0, + params[block * input_block_len..].as_ptr(), + ); + if status != 0 { + return false; + } + } + true + }, + |detail| { + RusticolError::evaluation(format!( + "SymJIT panicked while evaluating AoSoA blocks for application {}: {detail}", + self.application_path.display() + )) + }, + ) + } +} + +fn evaluate_matrix( + applet: &Applet, + application_path: &Path, + batch_size: usize, + params: &[Complex], + out: &mut [Complex], +) -> RusticolResult<()> { + let params = complex_slice_as_scalars(params); + let out = complex_slice_as_scalars_mut(out); + guard_symjit_panic( + || applet.evaluate_matrix(params, out, batch_size), + |detail| { + RusticolError::evaluation(format!( + "SymJIT panicked while evaluating application {}: {detail}", + application_path.display() + )) + }, + )?; + Ok(()) +} + +fn guard_symjit_panic( + operation: impl FnOnce() -> T, + error: impl FnOnce(String) -> RusticolError, +) -> RusticolResult { + catch_unwind(AssertUnwindSafe(operation)).map_err(|payload| error(panic_detail(payload))) +} + +fn panic_detail(payload: Box) -> String { + if let Some(message) = payload.downcast_ref::<&str>() { + (*message).to_string() + } else if let Some(message) = payload.downcast_ref::() { + message.clone() + } else { + "non-string panic payload".to_string() + } +} + +pub(crate) fn validate_manifest_metadata( + metadata: &SymjitApplicationMetadata<'_>, +) -> RusticolResult<()> { + if metadata.runtime_capability != SYMJIT_APPLICATION_RUNTIME_CAPABILITY { + return Err(RusticolError::unsupported_runtime_capability( + metadata.runtime_capability, + format!("a direct SymJIT evaluator requires {SYMJIT_APPLICATION_RUNTIME_CAPABILITY:?}"), + )); + } + if metadata.application_abi != SYMJIT_APPLICATION_STORAGE_ABI { + return Err(RusticolError::compatibility(format!( + "SymJIT application ABI {:?} is unsupported; this runtime requires {:?}", + metadata.application_abi, SYMJIT_APPLICATION_STORAGE_ABI + ))); + } + if metadata.input_len == 0 || metadata.output_len == 0 { + return Err(RusticolError::invalid_argument( + "SymJIT application input_len and output_len must both be positive", + )); + } + if metadata.element_layout != "complex-f64" || metadata.batch_layout != "row-major" { + return Err(RusticolError::compatibility(format!( + "unsupported SymJIT application layout element={:?}, batch={:?}", + metadata.element_layout, metadata.batch_layout + ))); + } + if metadata.compiler_type != "native" { + return Err(RusticolError::compatibility(format!( + "unsupported SymJIT compiler type {:?}; expected \"native\"", + metadata.compiler_type + ))); + } + if metadata.translation_mode != "indirect" { + return Err(RusticolError::compatibility(format!( + "unsupported SymJIT translation {:?}; direct translation is not a stable artifact ABI", + metadata.translation_mode + ))); + } + if metadata.word_bits != usize::BITS as u8 || metadata.word_bits != 64 { + return Err(RusticolError::compatibility(format!( + "SymJIT application requires a {}-bit runtime, but this host uses {} bits", + metadata.word_bits, + usize::BITS + ))); + } + let host_endianness = if cfg!(target_endian = "little") { + "little" + } else { + "big" + }; + if metadata.endianness != host_endianness || metadata.endianness != "little" { + return Err(RusticolError::compatibility(format!( + "SymJIT application endianness {:?} is incompatible with host endianness {host_endianness:?}", + metadata.endianness + ))); + } + if !metadata.required_defuns.is_empty() { + return Err(RusticolError::unsupported_runtime_capability( + SYMJIT_APPLICATION_RUNTIME_CAPABILITY, + format!( + "external functions are not self-contained: {}", + metadata.required_defuns.join(", ") + ), + )); + } + Ok(()) +} + +fn validate_loaded_application( + path: &Path, + application: &Application, + metadata: &SymjitApplicationMetadata<'_>, +) -> RusticolResult<()> { + let expected_params = metadata.input_len.checked_mul(2).ok_or_else(|| { + RusticolError::invalid_argument("SymJIT complex input count overflows usize") + })?; + let expected_outputs = metadata.output_len.checked_mul(2).ok_or_else(|| { + RusticolError::invalid_argument("SymJIT complex output count overflows usize") + })?; + if application.count_states != 0 + || application.count_diffs != 0 + || application.count_params != expected_params + || application.count_obs != expected_outputs + { + return Err(RusticolError::integrity(format!( + "SymJIT application {} has counts states={}, params={}, outputs={}, diffs={}; expected 0, {}, {}, 0", + path.display(), + application.count_states, + application.count_params, + application.count_obs, + application.count_diffs, + expected_params, + expected_outputs + ))); + } + validate_application_config(path, &application.config, metadata) +} + +fn validate_application_config( + path: &Path, + config: &Config, + metadata: &SymjitApplicationMetadata<'_>, +) -> RusticolResult<()> { + if !config.is_complex() || !config.symbolica() { + return Err(RusticolError::compatibility(format!( + "SymJIT application {} is not a complex Symbolica evaluator", + path.display() + ))); + } + if config.direct() { + return Err(RusticolError::compatibility(format!( + "SymJIT application {} uses direct translation; regenerate it with indirect translation", + path.display() + ))); + } + let host_native = if cfg!(target_arch = "aarch64") { + config.is_arm64() + } else if cfg!(target_arch = "x86_64") { + config.is_amd64() + } else if cfg!(target_arch = "riscv64") { + config.is_riscv64() + } else { + false + }; + if !host_native && !matches!(metadata.optimization_level, 1 | 2) { + return Err(RusticolError::compatibility(format!( + "SymJIT application {} uses compiler type {:?}, which is not native for this host; only optimization level 1 or 2 applications are cross-architecture portable", + path.display(), + config.compiler_type() + ))); + } + if config.opt_level() != metadata.optimization_level { + return Err(RusticolError::integrity(format!( + "SymJIT application {} declares optimization level {} but stores optimization level {}", + path.display(), + metadata.optimization_level, + config.opt_level() + ))); + } + Ok(()) +} + +fn validate_batch_lengths( + rows: usize, + input_len: usize, + output_len: usize, + params_len: usize, + output_buffer_len: usize, +) -> RusticolResult<()> { + let expected_params = rows.checked_mul(input_len).ok_or_else(|| { + RusticolError::invalid_argument("SymJIT parameter buffer length overflows usize") + })?; + let expected_outputs = rows.checked_mul(output_len).ok_or_else(|| { + RusticolError::invalid_argument("SymJIT output buffer length overflows usize") + })?; + if params_len != expected_params { + return Err(RusticolError::invalid_argument(format!( + "parameter buffer has length {params_len}, expected {expected_params}" + ))); + } + if output_buffer_len != expected_outputs { + return Err(RusticolError::invalid_argument(format!( + "output buffer has length {output_buffer_len}, expected {expected_outputs}" + ))); + } + Ok(()) +} + +fn validate_aosoa_lengths( + block_count: usize, + lane_width: usize, + input_len: usize, + output_len: usize, + params_len: usize, + output_buffer_len: usize, +) -> RusticolResult<()> { + let scalar_lanes = lane_width.checked_mul(2).ok_or_else(|| { + RusticolError::invalid_argument("SymJIT AoSoA complex lane count overflows usize") + })?; + let expected_params = block_count + .checked_mul(input_len) + .and_then(|count| count.checked_mul(scalar_lanes)) + .ok_or_else(|| { + RusticolError::invalid_argument("SymJIT AoSoA parameter buffer length overflows usize") + })?; + let expected_outputs = block_count + .checked_mul(output_len) + .and_then(|count| count.checked_mul(scalar_lanes)) + .ok_or_else(|| { + RusticolError::invalid_argument("SymJIT AoSoA output buffer length overflows usize") + })?; + if params_len != expected_params { + return Err(RusticolError::invalid_argument(format!( + "AoSoA parameter buffer has length {params_len}, expected {expected_params}" + ))); + } + if output_buffer_len != expected_outputs { + return Err(RusticolError::invalid_argument(format!( + "AoSoA output buffer has length {output_buffer_len}, expected {expected_outputs}" + ))); + } + Ok(()) +} + +fn complex_slice_as_scalars(values: &[Complex]) -> &[T] { + assert_eq!( + std::mem::size_of::>(), + 2 * std::mem::size_of::(), + "complex storage must be two adjacent scalars" + ); + assert_eq!( + std::mem::align_of::>(), + std::mem::align_of::(), + "complex storage must use scalar alignment" + ); + // SAFETY: Both supported Complex implementations are repr(C) structs containing exactly + // adjacent `re` and `im` fields. The size/alignment assertions guard this runtime contract. + unsafe { std::slice::from_raw_parts(values.as_ptr().cast::(), values.len() * 2) } +} + +fn complex_slice_as_scalars_mut(values: &mut [Complex]) -> &mut [T] { + assert_eq!( + std::mem::size_of::>(), + 2 * std::mem::size_of::(), + "complex storage must be two adjacent scalars" + ); + assert_eq!( + std::mem::align_of::>(), + std::mem::align_of::(), + "complex storage must use scalar alignment" + ); + // SAFETY: See complex_slice_as_scalars. The mutable borrow guarantees exclusive access. + unsafe { std::slice::from_raw_parts_mut(values.as_mut_ptr().cast::(), values.len() * 2) } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::io::Write; + use symjit::{Compiler, CompilerType, Storage}; + + fn application_bytes() -> Vec { + let mut config = Config::new(CompilerType::Native, 0).unwrap(); + config.set_complex(true); + config.set_symbolica(true); + config.set_opt_level(3); + config.set_simd(true); + let mut compiler = Compiler::with_config(config); + let instructions = r#"[[{"Add":[{"Out":0},[{"Param":0},{"Param":1}],0]}],1,[]]"#; + let application = compiler.translate(instructions.to_string(), 2).unwrap(); + let mut bytes = Vec::new(); + application.save(&mut bytes).unwrap(); + bytes + } + + fn metadata<'a>(required_defuns: &'a [String]) -> SymjitApplicationMetadata<'a> { + SymjitApplicationMetadata { + runtime_capability: SYMJIT_APPLICATION_RUNTIME_CAPABILITY, + application_abi: SYMJIT_APPLICATION_STORAGE_ABI, + input_len: 2, + output_len: 1, + element_layout: "complex-f64", + batch_layout: "row-major", + compiler_type: "native", + translation_mode: "indirect", + optimization_level: 3, + word_bits: 64, + endianness: "little", + required_defuns, + } + } + + fn nonnative_config(optimization_level: u8) -> Config { + let compiler_type = if cfg!(target_arch = "aarch64") { + CompilerType::Amd + } else { + CompilerType::Arm + }; + let mut config = Config::new(compiler_type, 0).unwrap(); + config.set_complex(true); + config.set_symbolica(true); + config.set_direct(false); + config.set_opt_level(optimization_level); + config + } + + fn write_application(bytes: &[u8], suffix: &str) -> PathBuf { + let path = std::env::temp_dir().join(format!( + "rusticol-symjit-{}-{suffix}.symjit", + std::process::id() + )); + let mut file = fs::File::create(&path).unwrap(); + file.write_all(bytes).unwrap(); + path + } + + fn direct_manifest(application_path: String) -> EvaluatorManifest { + EvaluatorManifest::SymjitApplication { + runtime_capability: SYMJIT_APPLICATION_RUNTIME_CAPABILITY.to_string(), + application_path, + application_abi: SYMJIT_APPLICATION_STORAGE_ABI.to_string(), + input_len: 2, + output_len: 1, + element_layout: "complex-f64".to_string(), + batch_layout: "row-major".to_string(), + compiler_type: "native".to_string(), + translation_mode: "indirect".to_string(), + optimization_level: 3, + word_bits: 64, + endianness: "little".to_string(), + required_defuns: Vec::new(), + plane_application: None, + evaluator_state_path: Some("absent-exact-evaluator-state.bin".to_string()), + evaluator_state_runtime_capability: Some( + SYMBOLICA_LEGACY_JIT_RUNTIME_CAPABILITY.to_string(), + ), + } + } + + fn pack_aosoa( + params: &[Complex], + batch_size: usize, + input_len: usize, + lane_width: usize, + ) -> Vec { + assert!(batch_size != 0); + let block_count = batch_size.div_ceil(lane_width); + let mut packed = vec![0.0; block_count * input_len * 2 * lane_width]; + for block in 0..block_count { + for lane in 0..lane_width { + let row = (block * lane_width + lane).min(batch_size - 1); + for input in 0..input_len { + let value = params[row * input_len + input]; + let start = (block * input_len + input) * 2 * lane_width; + packed[start + lane] = value.re; + packed[start + lane_width + lane] = value.im; + } + } + } + packed + } + + fn unpack_aosoa( + packed: &[f64], + batch_size: usize, + output_len: usize, + lane_width: usize, + ) -> Vec> { + let mut outputs = Vec::with_capacity(batch_size * output_len); + for row in 0..batch_size { + let block = row / lane_width; + let lane = row % lane_width; + for output in 0..output_len { + let start = (block * output_len + output) * 2 * lane_width; + outputs.push(Complex::new( + packed[start + lane], + packed[start + lane_width + lane], + )); + } + } + outputs + } + + #[test] + fn direct_application_loads_and_evaluates_complex_batches() { + let path = write_application(&application_bytes(), "batch"); + let mut evaluator = SymjitApplicationEvaluator::load(&path, metadata(&[])).unwrap(); + let params = [ + Complex::new(1.0, 2.0), + Complex::new(3.0, 4.0), + Complex::new(-2.0, 1.5), + Complex::new(5.0, -0.5), + ]; + let mut outputs = [Complex::new(0.0, 0.0); 2]; + evaluator.evaluate_batch(2, ¶ms, &mut outputs).unwrap(); + assert_eq!(outputs[0], Complex::new(4.0, 6.0)); + assert_eq!(outputs[1], Complex::new(3.0, 1.0)); + let _ = fs::remove_file(path); + } + + #[test] + fn direct_aosoa_blocks_match_complete_row_major_batches() { + let path = write_application(&application_bytes(), "aosoa-complete"); + let mut evaluator = SymjitApplicationEvaluator::load(&path, metadata(&[])).unwrap(); + let Some(lane_width) = evaluator.simd_lane_width() else { + let _ = fs::remove_file(path); + return; + }; + let batch_size = 2 * lane_width; + let mut params = Vec::with_capacity(batch_size * 2); + for row in 0..batch_size { + params.extend([ + Complex::new(row as f64 + 0.25, -(row as f64) - 0.5), + Complex::new(2.0 * row as f64 + 1.0, row as f64 + 0.75), + ]); + } + let mut expected = vec![Complex::new(0.0, 0.0); batch_size]; + evaluator + .evaluate_batch(batch_size, ¶ms, &mut expected) + .unwrap(); + + let packed = pack_aosoa(¶ms, batch_size, 2, lane_width); + let mut packed_outputs = vec![0.0; 2 * lane_width * 2]; + assert!( + evaluator + .evaluate_aosoa_blocks(2, &packed, &mut packed_outputs) + .unwrap() + ); + assert_eq!( + unpack_aosoa(&packed_outputs, batch_size, 1, lane_width), + expected + ); + + assert!( + evaluator + .evaluate_aosoa_blocks(2, &packed[..packed.len() - 1], &mut packed_outputs) + .is_err() + ); + let _ = fs::remove_file(path); + } + + #[test] + fn direct_aosoa_blocks_match_padded_row_major_tails() { + let path = write_application(&application_bytes(), "aosoa-tail"); + let mut evaluator = SymjitApplicationEvaluator::load(&path, metadata(&[])).unwrap(); + let Some(lane_width) = evaluator.simd_lane_width() else { + let _ = fs::remove_file(path); + return; + }; + let batch_size = lane_width + 1; + let mut params = Vec::with_capacity(batch_size * 2); + for row in 0..batch_size { + params.extend([ + Complex::new(row as f64 + 1.0, row as f64 + 2.0), + Complex::new(-(row as f64) - 3.0, 0.5 * row as f64), + ]); + } + let mut expected = vec![Complex::new(0.0, 0.0); batch_size]; + evaluator + .evaluate_batch(batch_size, ¶ms, &mut expected) + .unwrap(); + + let block_count = batch_size.div_ceil(lane_width); + let packed = pack_aosoa(¶ms, batch_size, 2, lane_width); + let mut packed_outputs = vec![0.0; block_count * lane_width * 2]; + assert!( + evaluator + .evaluate_aosoa_blocks(block_count, &packed, &mut packed_outputs) + .unwrap() + ); + assert_eq!( + unpack_aosoa(&packed_outputs, batch_size, 1, lane_width), + expected + ); + let _ = fs::remove_file(path); + } + + #[test] + fn direct_application_pads_incomplete_simd_tails() { + let path = write_application(&application_bytes(), "simd-tail"); + let mut evaluator = SymjitApplicationEvaluator::load(&path, metadata(&[])).unwrap(); + let lane_width = super::super::native_f64_simd_lane_width(); + let batch_size = lane_width.saturating_add(1); + let mut params = Vec::with_capacity(batch_size * 2); + let mut expected = Vec::with_capacity(batch_size); + for row in 0..batch_size { + let left = Complex::new(row as f64 + 1.0, 2.0); + let right = Complex::new(3.0, -(row as f64)); + params.extend([left, right]); + expected.push(left + right); + } + let mut outputs = vec![Complex::new(0.0, 0.0); batch_size]; + evaluator + .evaluate_batch(batch_size, ¶ms, &mut outputs) + .unwrap(); + assert_eq!(outputs, expected); + if lane_width > 1 { + assert_eq!(evaluator.input_tail_scratch.len(), lane_width * 2); + assert_eq!(evaluator.output_tail_scratch.len(), lane_width); + } + let _ = fs::remove_file(path); + } + + #[test] + fn direct_f64_group_load_does_not_read_exact_fallback_state() { + let path = write_application(&application_bytes(), "lazy-exact"); + let root = path.parent().unwrap(); + let manifest = direct_manifest(path.file_name().unwrap().to_str().unwrap().to_string()); + + let capabilities = evaluator_runtime_capabilities(&manifest).unwrap(); + assert_eq!( + capabilities, + BTreeSet::from([SYMJIT_APPLICATION_RUNTIME_CAPABILITY.to_string()]) + ); + + let mut group = EvaluatorGroup::load(&manifest, root).unwrap(); + let output = group + .evaluate_batch(1, &[Complex::new(2.0, 3.0), Complex::new(5.0, 7.0)]) + .unwrap(); + assert_eq!(output, vec![Complex::new(7.0, 10.0)]); + let _ = fs::remove_file(path); + } + + #[test] + fn chunked_group_gathers_mapped_parent_inputs() { + let path = write_application(&application_bytes(), "mapped-inputs"); + let root = path.parent().unwrap(); + let application_path = path.file_name().unwrap().to_str().unwrap().to_string(); + let manifest = EvaluatorManifest::Chunked { + required_runtime_capabilities: vec![SYMJIT_APPLICATION_RUNTIME_CAPABILITY.to_string()], + input_len: Some(3), + chunk_input_indices: Some(vec![vec![0, 2], vec![1, 2]]), + chunks: vec![ + direct_manifest(application_path.clone()), + direct_manifest(application_path), + ], + }; + + let mut group = EvaluatorGroup::load(&manifest, root).unwrap(); + let output = group + .evaluate_batch( + 2, + &[ + Complex::new(1.0, 0.0), + Complex::new(10.0, 0.0), + Complex::new(3.0, 0.0), + Complex::new(2.0, 0.0), + Complex::new(20.0, 0.0), + Complex::new(5.0, 0.0), + ], + ) + .unwrap(); + + assert_eq!( + output, + vec![ + Complex::new(4.0, 0.0), + Complex::new(13.0, 0.0), + Complex::new(7.0, 0.0), + Complex::new(25.0, 0.0), + ] + ); + let _ = fs::remove_file(path); + } + + #[test] + fn chunked_group_copies_contiguous_mapped_input_spans() { + let path = write_application(&application_bytes(), "mapped-input-spans"); + let root = path.parent().unwrap(); + let application_path = path.file_name().unwrap().to_str().unwrap().to_string(); + let manifest = EvaluatorManifest::Chunked { + required_runtime_capabilities: vec![SYMJIT_APPLICATION_RUNTIME_CAPABILITY.to_string()], + input_len: Some(4), + chunk_input_indices: Some(vec![vec![0, 1], vec![2, 3]]), + chunks: vec![ + direct_manifest(application_path.clone()), + direct_manifest(application_path), + ], + }; + + let mut group = EvaluatorGroup::load(&manifest, root).unwrap(); + assert_eq!( + group.input_mapping_spans, + vec![vec![(0, 0, 2)], vec![(0, 2, 2)]] + ); + let output = group + .evaluate_batch( + 2, + &[ + Complex::new(1.0, 0.0), + Complex::new(3.0, 0.0), + Complex::new(10.0, 0.0), + Complex::new(30.0, 0.0), + Complex::new(2.0, 0.0), + Complex::new(5.0, 0.0), + Complex::new(20.0, 0.0), + Complex::new(50.0, 0.0), + ], + ) + .unwrap(); + + assert_eq!( + output, + vec![ + Complex::new(4.0, 0.0), + Complex::new(40.0, 0.0), + Complex::new(7.0, 0.0), + Complex::new(70.0, 0.0), + ] + ); + let _ = fs::remove_file(path); + } + + #[test] + fn legacy_chunked_group_reuses_full_parent_inputs() { + let path = write_application(&application_bytes(), "legacy-chunks"); + let root = path.parent().unwrap(); + let application_path = path.file_name().unwrap().to_str().unwrap().to_string(); + let manifest = EvaluatorManifest::Chunked { + required_runtime_capabilities: vec![SYMJIT_APPLICATION_RUNTIME_CAPABILITY.to_string()], + input_len: None, + chunk_input_indices: None, + chunks: vec![ + direct_manifest(application_path.clone()), + direct_manifest(application_path), + ], + }; + + let mut group = EvaluatorGroup::load(&manifest, root).unwrap(); + let output = group + .evaluate_batch(1, &[Complex::new(2.0, 3.0), Complex::new(5.0, 7.0)]) + .unwrap(); + + assert_eq!( + output, + vec![Complex::new(7.0, 10.0), Complex::new(7.0, 10.0)] + ); + let _ = fs::remove_file(path); + } + + #[test] + fn direct_f64_group_loads_without_exact_fallback_metadata() { + let path = write_application(&application_bytes(), "no-exact-fallback"); + let root = path.parent().unwrap(); + let mut manifest = direct_manifest(path.file_name().unwrap().to_str().unwrap().to_string()); + let EvaluatorManifest::SymjitApplication { + evaluator_state_path, + evaluator_state_runtime_capability, + .. + } = &mut manifest + else { + unreachable!() + }; + *evaluator_state_path = None; + *evaluator_state_runtime_capability = None; + + let mut group = EvaluatorGroup::load(&manifest, root).unwrap(); + let output = group + .evaluate_batch(1, &[Complex::new(2.0, 3.0), Complex::new(5.0, 7.0)]) + .unwrap(); + + assert_eq!(output, vec![Complex::new(7.0, 10.0)]); + let _ = fs::remove_file(path); + } + + #[cfg(not(feature = "symbolica-runtime"))] + #[test] + fn unsupported_legacy_jit_evaluator_is_rejected_before_payload_access() { + let manifest = EvaluatorManifest::Jit { + runtime_capability: SYMBOLICA_LEGACY_JIT_RUNTIME_CAPABILITY.to_string(), + input_len: 2, + output_len: 1, + evaluator_state_path: "absent-legacy-jit-state.bin".to_string(), + }; + + let error = match EvaluatorGroup::load(&manifest, Path::new("/absent-artifact-root")) { + Ok(_) => panic!("unsupported capability must win over absent payload"), + Err(error) => error, + }; + assert_eq!( + error.kind(), + crate::RusticolErrorKind::UnsupportedRuntimeCapability + ); + } + + #[cfg(not(feature = "f64-compiled"))] + #[test] + fn unsupported_compiled_evaluators_are_rejected_before_payload_access() { + let manifests = [ + EvaluatorManifest::CompiledComplex { + runtime_capability: SYMBOLICA_COMPILED_CPP_RUNTIME_CAPABILITY.to_string(), + function_name: "evaluate".to_string(), + input_len: 2, + output_len: 1, + library_path: "absent-cpp-library.so".to_string(), + evaluator_state_path: None, + number_type: "complex".to_string(), + native_direct_application: None, + }, + EvaluatorManifest::CompiledComplex { + runtime_capability: SYMBOLICA_COMPILED_ASM_RUNTIME_CAPABILITY.to_string(), + function_name: "evaluate".to_string(), + input_len: 2, + output_len: 1, + library_path: "absent-asm-library.so".to_string(), + evaluator_state_path: None, + number_type: "complex".to_string(), + native_direct_application: None, + }, + ]; + + for manifest in manifests { + let error = match EvaluatorGroup::load(&manifest, Path::new("/absent-artifact-root")) { + Ok(_) => panic!("unsupported capability must win over absent payload"), + Err(error) => error, + }; + assert_eq!( + error.kind(), + crate::RusticolErrorKind::UnsupportedRuntimeCapability + ); + } + } + + #[test] + fn malformed_application_bytes_are_rejected() { + let path = write_application(b"not-a-symjit-application", "malformed"); + let error = SymjitApplicationEvaluator::load(&path, metadata(&[])).unwrap_err(); + assert_eq!(error.kind(), crate::RusticolErrorKind::Compatibility); + let _ = fs::remove_file(path); + } + + #[test] + fn cross_architecture_application_accepts_o1_o2_only() { + for optimization_level in [1, 2] { + let mut metadata = metadata(&[]); + metadata.optimization_level = optimization_level; + validate_application_config( + Path::new("portable.symjit"), + &nonnative_config(optimization_level), + &metadata, + ) + .unwrap(); + } + + for optimization_level in [0, 3] { + let mut metadata = metadata(&[]); + metadata.optimization_level = optimization_level; + let error = validate_application_config( + Path::new("target-specific.symjit"), + &nonnative_config(optimization_level), + &metadata, + ) + .unwrap_err(); + assert_eq!(error.kind(), crate::RusticolErrorKind::Compatibility); + assert!(error.to_string().contains("optimization level 1 or 2")); + } + } + + #[test] + fn symjit_panic_payloads_retain_dependency_context() { + assert_eq!( + panic_detail(Box::new(String::from( + "architecture-specific register allocation" + ))), + "architecture-specific register allocation" + ); + } + + #[test] + fn direct_application_requires_complex_f64_row_major_layout() { + let path = write_application(&application_bytes(), "layout"); + + let mut wrong_element_layout = metadata(&[]); + wrong_element_layout.element_layout = "complex-f32"; + let error = SymjitApplicationEvaluator::load(&path, wrong_element_layout).unwrap_err(); + assert_eq!(error.kind(), crate::RusticolErrorKind::Compatibility); + + let mut wrong_batch_layout = metadata(&[]); + wrong_batch_layout.batch_layout = "column-major"; + let error = SymjitApplicationEvaluator::load(&path, wrong_batch_layout).unwrap_err(); + assert_eq!(error.kind(), crate::RusticolErrorKind::Compatibility); + let _ = fs::remove_file(path); + } + + #[test] + fn capability_counts_and_external_functions_are_validated() { + let bytes = application_bytes(); + let path = write_application(&bytes, "metadata"); + + let mut wrong_counts = metadata(&[]); + wrong_counts.output_len = 2; + let error = SymjitApplicationEvaluator::load(&path, wrong_counts).unwrap_err(); + assert_eq!(error.kind(), crate::RusticolErrorKind::Integrity); + + let functions = vec!["model_function".to_string()]; + let error = SymjitApplicationEvaluator::load(&path, metadata(&functions)).unwrap_err(); + assert_eq!( + error.kind(), + crate::RusticolErrorKind::UnsupportedRuntimeCapability + ); + + let mut wrong_capability = metadata(&[]); + wrong_capability.runtime_capability = SYMBOLICA_LEGACY_JIT_RUNTIME_CAPABILITY; + let error = SymjitApplicationEvaluator::load(&path, wrong_capability).unwrap_err(); + assert_eq!( + error.kind(), + crate::RusticolErrorKind::UnsupportedRuntimeCapability + ); + let _ = fs::remove_file(path); + } +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/evaluator/symjit_compiled_direct.rs b/pyamplicol/rust/crates/rusticol-core/src/evaluator/symjit_compiled_direct.rs new file mode 100644 index 0000000000..dac23814bb --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/evaluator/symjit_compiled_direct.rs @@ -0,0 +1,904 @@ +// SPDX-License-Identifier: 0BSD + +//! Compiled-stage adapter for standard SymJIT direct-arena P-kernels. +//! +//! pyAmpliCol owns the fused-stage binding policy. SymJIT owns only the +//! ordinary P-kernel prologue, invariant body, and epilogue. Point-dependent +//! values bind directly to persistent arena planes. Point-independent inputs +//! bind through persistent broadcast planes which are refreshed only when the +//! backing model parameter changes. + +use std::path::{Path, PathBuf}; +use std::ptr; + +use super::symjit_plane::{ + PlaneDescriptor, SymjitPlaneKernel, SymjitPlaneLayout, SymjitPlaneTable, +}; +use crate::direct_arena::{ + AlignedF64Buffer, DirectArenaAllocationCounters, DirectArenaView, DirectFactorView, + DirectMomentumView, DirectParameterView, validate_direct_views, +}; +use crate::engine::SYMJIT_PLANE_APPLICATION_ABI; +use crate::{RusticolError, RusticolResult}; + +/// One split-complex plane in the persistent compiled-stage arena. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub(crate) enum CompiledDirectArenaPlane { + Current { component: u32, imaginary: bool }, + Amplitude { component: u32, imaginary: bool }, +} + +/// One point-dependent input consumed by a fused compiled stage. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub(crate) enum CompiledDirectPlaneBinding { + Arena(CompiledDirectArenaPlane), + Momentum { + form: u32, + lorentz_component: u16, + }, + /// The runtime-owned persistent structural-zero plane. + Zero, +} + +/// One point-independent scalar consumed by a fused compiled stage. +#[derive(Clone, Copy, Debug, PartialEq)] +pub(crate) enum CompiledDirectScalarBinding { + Parameter { + index: u32, + imaginary: bool, + }, + // The plane ABI supports persistent literal broadcasts even though the + // current production lowering emits parameter bindings only. + #[allow(dead_code)] + Literal(f64), +} + +/// One distinct output plane overwritten by a fused compiled stage. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub(crate) struct CompiledDirectOutputBinding(pub CompiledDirectArenaPlane); + +/// Routing from the P-kernel's split input order to runtime plane catalogs. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) enum CompiledDirectSourceInputBinding { + Plane(u32), + Scalar(u32), +} + +/// Loaded but not yet pinned compiled-stage P-kernel. +pub(crate) struct LoadedSymjitCompiledDirectStage { + kernel: SymjitPlaneKernel, + source_input_bindings: Box<[CompiledDirectSourceInputBinding]>, + input_bindings: Box<[CompiledDirectPlaneBinding]>, + scalar_bindings: Box<[CompiledDirectScalarBinding]>, + output_bindings: Box<[CompiledDirectOutputBinding]>, + display_path: PathBuf, +} + +#[derive(Clone, Copy)] +enum BroadcastSource { + Parameter { value: *const f64, cached_bits: u64 }, + Literal, +} + +/// A cold-bound P-kernel descriptor table for one persistent compiled arena. +/// +/// The unsafe bind contract guarantees that external arena, momentum, +/// parameter, and zero-plane allocations remain fixed until this object drops. +pub(crate) struct BoundSymjitCompiledDirectStage { + kernel: SymjitPlaneKernel, + table: SymjitPlaneTable<'static>, + broadcast_values: AlignedF64Buffer, + broadcast_sources: Box<[BroadcastSource]>, + point_stride: u32, + pending_broadcast_fill_bytes: u64, + display_path: PathBuf, +} + +impl LoadedSymjitCompiledDirectStage { + pub(crate) const fn broadcast_plane_count(&self) -> usize { + self.scalar_bindings.len() + } + + /// Load one persisted standard complex P-kernel and authenticate its + /// pyAmpliCol-owned split-plane routing. + #[allow(clippy::too_many_arguments)] + pub(crate) fn load_source_bytes( + bytes: &[u8], + display_path: impl AsRef, + source_application_abi: &str, + source_optimization_level: u8, + source_compression: bool, + source_input_bindings: Vec, + input_bindings: Vec, + scalar_bindings: Vec, + output_bindings: Vec, + ) -> RusticolResult { + let display_path = display_path.as_ref().to_path_buf(); + if source_application_abi != SYMJIT_PLANE_APPLICATION_ABI { + return Err(RusticolError::compatibility(format!( + "unsupported compiled plane application ABI \ + {source_application_abi:?}; expected \ + {SYMJIT_PLANE_APPLICATION_ABI:?}; regenerate the artifact" + ))); + } + validate_static_bindings( + &source_input_bindings, + &input_bindings, + &scalar_bindings, + &output_bindings, + )?; + let input_complex_count = source_input_bindings.len() / 2; + let output_complex_count = output_bindings.len() / 2; + let kernel = SymjitPlaneKernel::load_bytes( + bytes, + display_path.clone(), + SymjitPlaneLayout::complex(input_complex_count, output_complex_count), + )?; + if kernel.optimization_level() != source_optimization_level { + return Err(RusticolError::integrity(format!( + "compiled plane application {} declares optimization level \ + {source_optimization_level} but stores optimization level {}", + display_path.display(), + kernel.optimization_level() + ))); + } + if kernel.compression() != source_compression { + return Err(RusticolError::integrity(format!( + "compiled plane application {} declares compression \ + {source_compression} but stores compression {}; regenerate the artifact", + display_path.display(), + kernel.compression() + ))); + } + if kernel.input_plane_count() != source_input_bindings.len() + || kernel.output_plane_count() != output_bindings.len() + { + return Err(RusticolError::integrity(format!( + "compiled plane application {} shape disagrees with its bindings", + display_path.display() + ))); + } + Ok(Self { + kernel, + source_input_bindings: source_input_bindings.into_boxed_slice(), + input_bindings: input_bindings.into_boxed_slice(), + scalar_bindings: scalar_bindings.into_boxed_slice(), + output_bindings: output_bindings.into_boxed_slice(), + display_path, + }) + } + + /// Pin this stage to persistent Direct-Arena storage. + /// + /// # Safety + /// + /// Every allocation referenced by `arena`, `momenta`, `parameters`, and + /// `zero_plane` must remain allocated at the same address until the + /// returned stage is dropped. Parameter values may change only between + /// calls to `evaluate`. No other access may overlap a kernel invocation. + #[cfg(test)] + pub(crate) unsafe fn bind( + self, + arena: DirectArenaView, + momenta: DirectMomentumView, + parameters: DirectParameterView, + zero_plane: &AlignedF64Buffer, + ) -> RusticolResult { + // SAFETY: this is the identity-layout specialization of the complete + // fixed-storage contract above. + unsafe { self.bind_with_current_map(arena, momenta, parameters, zero_plane, &[]) } + } + + /// Pin canonical current IDs through a cold immutable physical map. + /// + /// # Safety + /// + /// Every allocation addressed by `arena`, `momenta`, `parameters`, and + /// `zero_plane` must remain live and at a stable address until the returned + /// bound stage drops. `current_component_map` must remain immutable for + /// this call. No access may race with stage execution; model-parameter + /// updates must use the runtime's synchronized update path. + pub(crate) unsafe fn bind_with_current_map( + self, + arena: DirectArenaView, + momenta: DirectMomentumView, + parameters: DirectParameterView, + zero_plane: &AlignedF64Buffer, + current_component_map: &[u32], + ) -> RusticolResult { + validate_direct_views( + arena, + momenta, + parameters, + DirectFactorView { + values_re: ptr::null(), + values_im: ptr::null(), + value_count: 0, + }, + )?; + validate_mapped_bindings( + &self.input_bindings, + &self.output_bindings, + current_component_map, + )?; + + let point_stride = arena.point_stride; + let stride = point_stride as usize; + if zero_plane.len() != stride { + return Err(RusticolError::integrity(format!( + "compiled shared zero plane has length {}, expected {point_stride}", + zero_plane.len() + ))); + } + if zero_plane.as_slice().iter().any(|value| *value != 0.0) { + return Err(RusticolError::integrity( + "compiled shared zero plane contains a nonzero value", + )); + } + + let broadcast_len = self + .scalar_bindings + .len() + .checked_mul(stride) + .ok_or_else(|| { + RusticolError::integrity("compiled scalar broadcast storage overflows usize") + })?; + let mut broadcast_values = + AlignedF64Buffer::zeroed(broadcast_len, "compiled scalar broadcasts")?; + let mut broadcast_sources = Vec::new(); + broadcast_sources + .try_reserve_exact(self.scalar_bindings.len()) + .map_err(|error| { + RusticolError::internal(format!( + "could not allocate compiled scalar broadcast metadata: {error}" + )) + })?; + for (slot, binding) in self.scalar_bindings.iter().copied().enumerate() { + let (value, source) = match binding { + CompiledDirectScalarBinding::Parameter { index, imaginary } => { + if index >= parameters.value_count { + return Err(RusticolError::integrity(format!( + "compiled parameter binding {index} is out of bounds" + ))); + } + let base = if imaginary { + parameters.values_im + } else { + parameters.values_re + }; + // SAFETY: the parameter view was validated above and the + // checked index lies in its fixed allocation. + let value_pointer = unsafe { base.add(index as usize) }; + // SAFETY: the pointer remains live by the bind contract. + let initial = unsafe { *value_pointer }; + ( + initial, + BroadcastSource::Parameter { + value: value_pointer, + cached_bits: initial.to_bits(), + }, + ) + } + CompiledDirectScalarBinding::Literal(value) => (value, BroadcastSource::Literal), + }; + let start = slot * stride; + broadcast_values.as_mut_slice()[start..start + stride].fill(value); + broadcast_sources.push(source); + } + + let mut resolved_inputs = Vec::new(); + resolved_inputs + .try_reserve_exact(self.input_bindings.len()) + .map_err(|error| { + RusticolError::internal(format!( + "could not allocate compiled input-plane catalog: {error}" + )) + })?; + for binding in self.input_bindings.iter().copied() { + resolved_inputs.push(resolve_input_plane_pointer( + binding, + arena, + momenta, + zero_plane, + current_component_map, + )?); + } + + let descriptor_count = self + .source_input_bindings + .len() + .checked_add(self.output_bindings.len()) + .ok_or_else(|| { + RusticolError::integrity("compiled plane descriptor count overflows usize") + })?; + let mut descriptors = Vec::new(); + descriptors + .try_reserve_exact(descriptor_count) + .map_err(|error| { + RusticolError::internal(format!( + "could not allocate compiled P-kernel descriptors: {error}" + )) + })?; + let broadcast_base = broadcast_values.as_mut_ptr(); + for source in self.source_input_bindings.iter().copied() { + let pointer = match source { + CompiledDirectSourceInputBinding::Plane(index) => resolved_inputs[index as usize], + CompiledDirectSourceInputBinding::Scalar(index) => { + // SAFETY: static binding validation proved this scalar + // index and the multiplication above proved the allocation. + unsafe { broadcast_base.add(index as usize * stride) } + } + }; + // SAFETY: every resolved pointer covers one complete fixed plane + // and remains live for the bound stage lifetime. + descriptors.push(unsafe { PlaneDescriptor::from_cached_raw_parts(pointer, stride)? }); + } + for output in self.output_bindings.iter().copied() { + let pointer = resolve_arena_plane_pointer(output.0, arena, current_component_map)?; + // SAFETY: mapped output validation proves distinct complete planes + // which remain live for the bound stage lifetime. + descriptors.push(unsafe { PlaneDescriptor::from_cached_raw_parts(pointer, stride)? }); + } + let table = self.kernel.build_raw_table_from_descriptors(&descriptors)?; + let initial_fill_bytes = u64::try_from(broadcast_len) + .unwrap_or(u64::MAX) + .saturating_mul(std::mem::size_of::() as u64); + Ok(BoundSymjitCompiledDirectStage { + kernel: self.kernel, + table, + broadcast_values, + broadcast_sources: broadcast_sources.into_boxed_slice(), + point_stride, + pending_broadcast_fill_bytes: initial_fill_bytes, + display_path: self.display_path, + }) + } +} + +impl BoundSymjitCompiledDirectStage { + pub(crate) fn allocation_counters(&self) -> DirectArenaAllocationCounters { + self.broadcast_values.allocation_counters() + } + + /// Evaluate a point range directly into its disjoint output planes. + /// + /// The returned byte count is internal scalar-broadcast traffic. It is + /// deliberately separate from boundary pack/gather/scatter counters. + pub(crate) fn evaluate(&mut self, point_start: u32, point_count: u32) -> RusticolResult { + if point_count == 0 { + return Err(RusticolError::invalid_argument( + "compiled Direct-Arena point count must be positive", + )); + } + let point_stop = point_start.checked_add(point_count).ok_or_else(|| { + RusticolError::invalid_argument("compiled Direct-Arena point range overflows u32") + })?; + if point_stop > self.point_stride { + return Err(RusticolError::invalid_argument(format!( + "compiled Direct-Arena point range {point_start}..{point_stop} \ + exceeds physical stride {}", + self.point_stride + ))); + } + + let mut fill_bytes = self.pending_broadcast_fill_bytes; + self.pending_broadcast_fill_bytes = 0; + let stride = self.point_stride as usize; + let values = self.broadcast_values.as_mut_slice(); + for (slot, source) in self.broadcast_sources.iter_mut().enumerate() { + let BroadcastSource::Parameter { value, cached_bits } = source else { + continue; + }; + // SAFETY: the bind contract keeps the parameter allocation live, + // and evaluation has exclusive synchronized access. + let current = unsafe { **value }; + if current.to_bits() == *cached_bits { + continue; + } + let start = slot * stride; + values[start..start + stride].fill(current); + *cached_bits = current.to_bits(); + fill_bytes = fill_bytes.saturating_add( + u64::try_from(stride) + .unwrap_or(u64::MAX) + .saturating_mul(std::mem::size_of::() as u64), + ); + } + self.kernel + .execute_table(&mut self.table, point_start as usize, point_count as usize) + .map_err(|error| { + RusticolError::evaluation(format!( + "compiled plane application {} failed: {}", + self.display_path.display(), + error.message() + )) + })?; + Ok(fill_bytes) + } +} + +fn validate_static_bindings( + sources: &[CompiledDirectSourceInputBinding], + inputs: &[CompiledDirectPlaneBinding], + scalars: &[CompiledDirectScalarBinding], + outputs: &[CompiledDirectOutputBinding], +) -> RusticolResult<()> { + if sources.is_empty() || !sources.len().is_multiple_of(2) { + return Err(RusticolError::integrity( + "compiled P-kernel split input binding count must be positive and even", + )); + } + if outputs.is_empty() || !outputs.len().is_multiple_of(2) { + return Err(RusticolError::integrity( + "compiled P-kernel split output binding count must be positive and even", + )); + } + for (index, source) in sources.iter().copied().enumerate() { + let valid = match source { + CompiledDirectSourceInputBinding::Plane(slot) => (slot as usize) < inputs.len(), + CompiledDirectSourceInputBinding::Scalar(slot) => (slot as usize) < scalars.len(), + }; + if !valid { + return Err(RusticolError::integrity(format!( + "compiled P-kernel source input binding {index} is out of bounds" + ))); + } + } + for (index, output) in outputs.iter().copied().enumerate() { + if outputs[..index].contains(&output) { + return Err(RusticolError::integrity(format!( + "compiled P-kernel output binding {index} aliases an earlier output" + ))); + } + if inputs.contains(&CompiledDirectPlaneBinding::Arena(output.0)) { + return Err(RusticolError::integrity(format!( + "compiled P-kernel output binding {index} aliases an input plane" + ))); + } + } + Ok(()) +} + +fn validate_mapped_bindings( + inputs: &[CompiledDirectPlaneBinding], + outputs: &[CompiledDirectOutputBinding], + current_component_map: &[u32], +) -> RusticolResult<()> { + let mapped_inputs = inputs + .iter() + .copied() + .filter_map(|binding| match binding { + CompiledDirectPlaneBinding::Arena(arena) => { + Some(map_arena_plane(arena, current_component_map)) + } + CompiledDirectPlaneBinding::Momentum { .. } | CompiledDirectPlaneBinding::Zero => None, + }) + .collect::>>()?; + let mut mapped_outputs = Vec::with_capacity(outputs.len()); + for (index, output) in outputs.iter().copied().enumerate() { + let mapped = map_arena_plane(output.0, current_component_map)?; + if mapped_outputs.contains(&mapped) { + return Err(RusticolError::integrity(format!( + "compiled P-kernel mapped output binding {index} aliases an earlier output" + ))); + } + if mapped_inputs.contains(&mapped) { + return Err(RusticolError::integrity(format!( + "compiled P-kernel mapped output binding {index} aliases an input plane" + ))); + } + mapped_outputs.push(mapped); + } + Ok(()) +} + +fn map_arena_plane( + binding: CompiledDirectArenaPlane, + current_component_map: &[u32], +) -> RusticolResult { + match binding { + CompiledDirectArenaPlane::Current { + component, + imaginary, + } if !current_component_map.is_empty() => { + let physical = current_component_map + .get(component as usize) + .copied() + .filter(|physical| *physical != u32::MAX) + .ok_or_else(|| { + RusticolError::integrity(format!( + "compiled current binding {component} has no physical mapping" + )) + })?; + Ok(CompiledDirectArenaPlane::Current { + component: physical, + imaginary, + }) + } + _ => Ok(binding), + } +} + +fn resolve_input_plane_pointer( + binding: CompiledDirectPlaneBinding, + arena: DirectArenaView, + momenta: DirectMomentumView, + zero_plane: &AlignedF64Buffer, + current_component_map: &[u32], +) -> RusticolResult<*mut f64> { + match binding { + CompiledDirectPlaneBinding::Arena(binding) => { + resolve_arena_plane_pointer(binding, arena, current_component_map) + } + CompiledDirectPlaneBinding::Momentum { + form, + lorentz_component, + } => { + if form >= momenta.form_count || lorentz_component >= momenta.lorentz_component_count { + return Err(RusticolError::integrity(format!( + "compiled momentum binding ({form}, {lorentz_component}) is out of bounds" + ))); + } + let component = form + .checked_mul(u32::from(momenta.lorentz_component_count)) + .and_then(|base| base.checked_add(u32::from(lorentz_component))) + .ok_or_else(|| { + RusticolError::integrity("compiled momentum component overflows u32") + })?; + let offset = usize::try_from(component) + .ok() + .and_then(|component| component.checked_mul(momenta.point_stride as usize)) + .ok_or_else(|| { + RusticolError::integrity("compiled momentum offset overflows usize") + })?; + // SAFETY: the momentum view and component bounds were validated. + Ok(unsafe { momenta.values.add(offset) }.cast_mut()) + } + CompiledDirectPlaneBinding::Zero => Ok(zero_plane.as_ptr().cast_mut()), + } +} + +fn resolve_arena_plane_pointer( + binding: CompiledDirectArenaPlane, + arena: DirectArenaView, + current_component_map: &[u32], +) -> RusticolResult<*mut f64> { + let binding = map_arena_plane(binding, current_component_map)?; + let (base, shape, label) = match binding { + CompiledDirectArenaPlane::Current { + component, + imaginary, + } => ( + if imaginary { + arena.current_im + } else { + arena.current_re + }, + arena.current_shape()?, + ("current", component), + ), + CompiledDirectArenaPlane::Amplitude { + component, + imaginary, + } => ( + if imaginary { + arena.amplitude_im + } else { + arena.amplitude_re + }, + arena.amplitude_shape()?, + ("amplitude", component), + ), + }; + let offset = shape.checked_scalar_index(label.1, 0, label.0)?; + // SAFETY: shape validation and the checked offset prove one complete + // pitched plane begins at this pointer. + Ok(unsafe { base.add(offset) }) +} + +#[cfg(test)] +mod tests { + use super::super::symjit_direct::tests::count_allocations; + use super::super::symjit_plane::compile_symbolica_program_to_plane_application_bytes; + use super::*; + use crate::RusticolErrorKind; + use crate::direct_arena::{DirectArenaWorkspace, DirectMomentumView}; + + const MODEL: &str = " +([('fun', ('temp', 0), 'square', [], [('param', 0)], False), + ('add', ('temp', 1), [('temp', 0), ('param', 1)], 0), + ('assign', ('out', 0), ('temp', 1))], + 2, + []) +"; + + fn application_bytes(level: u8) -> Vec { + compile_symbolica_program_to_plane_application_bytes(MODEL, 2, 1, level, true).unwrap() + } + + fn momentum_view(values: &AlignedF64Buffer, point_stride: u32) -> DirectMomentumView { + DirectMomentumView { + values: values.as_ptr(), + scalar_len: values.len() as u64, + form_count: 1, + lorentz_component_count: 4, + point_stride, + } + } + + fn loaded(level: u8) -> LoadedSymjitCompiledDirectStage { + try_loaded(level, true).unwrap() + } + + fn try_loaded( + level: u8, + expected_compression: bool, + ) -> RusticolResult { + LoadedSymjitCompiledDirectStage::load_source_bytes( + &application_bytes(level), + format!("compiled-plane-o{level}.symjit"), + SYMJIT_PLANE_APPLICATION_ABI, + level, + expected_compression, + vec![ + CompiledDirectSourceInputBinding::Plane(0), + CompiledDirectSourceInputBinding::Plane(1), + CompiledDirectSourceInputBinding::Scalar(0), + CompiledDirectSourceInputBinding::Scalar(1), + ], + vec![ + CompiledDirectPlaneBinding::Arena(CompiledDirectArenaPlane::Current { + component: 0, + imaginary: false, + }), + CompiledDirectPlaneBinding::Arena(CompiledDirectArenaPlane::Current { + component: 0, + imaginary: true, + }), + ], + vec![ + CompiledDirectScalarBinding::Parameter { + index: 0, + imaginary: false, + }, + CompiledDirectScalarBinding::Parameter { + index: 0, + imaginary: true, + }, + ], + vec![ + CompiledDirectOutputBinding(CompiledDirectArenaPlane::Current { + component: 1, + imaginary: false, + }), + CompiledDirectOutputBinding(CompiledDirectArenaPlane::Current { + component: 1, + imaginary: true, + }), + ], + ) + } + + #[test] + fn compiled_plane_stage_refreshes_parameters_and_allocates_zero_when_warm() { + const POINTS: usize = 1025; + const ODD_TAIL_LENGTHS: &[usize] = &[127, 129, 1023, 1025]; + let loaded = loaded(3); + assert!(matches!(loaded.kernel.simd_lanes(), 1 | 2 | 4)); + let mut workspace = DirectArenaWorkspace::new(2, 0, POINTS as u32).unwrap(); + workspace.begin_tile(POINTS as u32).unwrap(); + let stride = workspace.point_stride() as usize; + { + let (current_re, current_im, _, _) = workspace.split_slices_mut(); + for point in 0..POINTS { + current_re[point] = 0.25 + point as f64 / 32.0; + current_im[point] = -0.5 + point as f64 / 64.0; + } + current_re[stride..2 * stride].fill(f64::NAN); + current_im[stride..2 * stride].fill(f64::NAN); + } + let momenta = AlignedF64Buffer::zeroed(4 * stride, "compiled test momenta").unwrap(); + let zero = AlignedF64Buffer::zeroed(stride, "compiled test zero").unwrap(); + let mut parameter_re = [1.5]; + let mut parameter_im = [-0.75]; + let parameters = DirectParameterView { + values_re: parameter_re.as_ptr(), + values_im: parameter_im.as_ptr(), + value_count: 1, + }; + let arena = workspace.view().unwrap(); + let mut bound = unsafe { + loaded.bind( + arena, + momentum_view(&momenta, arena.point_stride), + parameters, + &zero, + ) + } + .unwrap(); + + let initial_bytes = bound.evaluate(1, 127).unwrap(); + assert_eq!(initial_bytes, (2 * stride * 8) as u64); + { + let (current_re, current_im) = workspace.current_slices(); + assert!(current_re[stride].is_nan()); + assert!(current_im[stride].is_nan()); + assert!(current_re[stride + 128].is_nan()); + assert!(current_im[stride + 128].is_nan()); + for point in 1..128 { + let xr = current_re[point]; + let xi = current_im[point]; + assert_eq!( + current_re[stride + point], + xr * xr - xi * xi + parameter_re[0] + ); + assert_eq!(current_im[stride + point], 2.0 * xr * xi + parameter_im[0]); + } + } + + for (case, &points) in ODD_TAIL_LENGTHS.iter().enumerate() { + parameter_re[0] = -2.0 - case as f64; + parameter_im[0] = 0.375 + case as f64 / 8.0; + { + let (current_re, current_im, _, _) = workspace.split_slices_mut(); + current_re[stride..2 * stride].fill(f64::NAN); + current_im[stride..2 * stride].fill(f64::NAN); + } + let (refresh, refresh_allocations, refresh_bytes) = + count_allocations(|| bound.evaluate(0, points as u32)); + assert_eq!(refresh.unwrap(), (2 * stride * 8) as u64); + assert_eq!( + (refresh_allocations, refresh_bytes), + (0, 0), + "compiled parameter refresh allocated for {points} points" + ); + let (cached, cached_allocations, cached_bytes) = + count_allocations(|| bound.evaluate(0, points as u32)); + assert_eq!(cached.unwrap(), 0); + assert_eq!( + (cached_allocations, cached_bytes), + (0, 0), + "warmed compiled call allocated for {points} points" + ); + + let (current_re, current_im) = workspace.current_slices(); + for point in 0..points { + let xr = current_re[point]; + let xi = current_im[point]; + assert_eq!( + current_re[stride + point], + xr * xr - xi * xi + parameter_re[0] + ); + assert_eq!(current_im[stride + point], 2.0 * xr * xi + parameter_im[0]); + } + if points < POINTS { + assert!(current_re[stride + points].is_nan()); + assert!(current_im[stride + points].is_nan()); + } + } + } + + #[test] + fn compiled_plane_stage_accepts_o0_through_o3_and_rejects_old_abi() { + for level in 0..=3 { + let stage = loaded(level); + assert_eq!(stage.kernel.optimization_level(), level); + } + let error = LoadedSymjitCompiledDirectStage::load_source_bytes( + &application_bytes(2), + "old-plane-application.symjit", + "pyamplicol-symjit-plane-application-v1", + 2, + true, + vec![ + CompiledDirectSourceInputBinding::Plane(0), + CompiledDirectSourceInputBinding::Plane(1), + CompiledDirectSourceInputBinding::Scalar(0), + CompiledDirectSourceInputBinding::Scalar(1), + ], + vec![ + CompiledDirectPlaneBinding::Zero, + CompiledDirectPlaneBinding::Zero, + ], + vec![ + CompiledDirectScalarBinding::Literal(1.0), + CompiledDirectScalarBinding::Literal(0.0), + ], + vec![ + CompiledDirectOutputBinding(CompiledDirectArenaPlane::Current { + component: 0, + imaginary: false, + }), + CompiledDirectOutputBinding(CompiledDirectArenaPlane::Current { + component: 0, + imaginary: true, + }), + ], + ) + .err() + .expect("old ABI must fail closed"); + assert_eq!(error.kind(), RusticolErrorKind::Compatibility); + assert!(error.message().contains("regenerate")); + } + + #[test] + fn authenticated_manifest_compression_mismatch_fails_closed() { + let error = try_loaded(2, false) + .err() + .expect("a manifest/application compression mismatch must fail closed"); + assert!(error.message().contains("compression")); + assert!(error.message().contains("regenerate")); + } + + #[test] + fn static_and_mapped_output_aliases_fail_before_execution() { + let bytes = application_bytes(2); + let error = LoadedSymjitCompiledDirectStage::load_source_bytes( + &bytes, + "static-alias.symjit", + SYMJIT_PLANE_APPLICATION_ABI, + 2, + true, + vec![ + CompiledDirectSourceInputBinding::Plane(0), + CompiledDirectSourceInputBinding::Plane(1), + CompiledDirectSourceInputBinding::Scalar(0), + CompiledDirectSourceInputBinding::Scalar(1), + ], + vec![ + CompiledDirectPlaneBinding::Arena(CompiledDirectArenaPlane::Current { + component: 0, + imaginary: false, + }), + CompiledDirectPlaneBinding::Arena(CompiledDirectArenaPlane::Current { + component: 0, + imaginary: true, + }), + ], + vec![ + CompiledDirectScalarBinding::Literal(0.0), + CompiledDirectScalarBinding::Literal(0.0), + ], + vec![ + CompiledDirectOutputBinding(CompiledDirectArenaPlane::Current { + component: 0, + imaginary: false, + }), + CompiledDirectOutputBinding(CompiledDirectArenaPlane::Current { + component: 1, + imaginary: true, + }), + ], + ) + .err() + .expect("input/output alias must fail"); + assert_eq!(error.kind(), RusticolErrorKind::Integrity); + + let loaded = loaded(2); + let mut workspace = DirectArenaWorkspace::new(2, 0, 8).unwrap(); + workspace.begin_tile(8).unwrap(); + let stride = workspace.point_stride() as usize; + let momenta = AlignedF64Buffer::zeroed(4 * stride, "mapped test momenta").unwrap(); + let zero = AlignedF64Buffer::zeroed(stride, "mapped test zero").unwrap(); + let parameter_re = [0.0]; + let parameter_im = [0.0]; + let parameters = DirectParameterView { + values_re: parameter_re.as_ptr(), + values_im: parameter_im.as_ptr(), + value_count: 1, + }; + let arena = workspace.view().unwrap(); + let error = unsafe { + loaded.bind_with_current_map( + arena, + momentum_view(&momenta, arena.point_stride), + parameters, + &zero, + &[0, 0], + ) + } + .err() + .expect("mapped alias must fail"); + assert_eq!(error.kind(), RusticolErrorKind::Integrity); + } +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/evaluator/symjit_direct.rs b/pyamplicol/rust/crates/rusticol-core/src/evaluator/symjit_direct.rs new file mode 100644 index 0000000000..fef0d0731d --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/evaluator/symjit_direct.rs @@ -0,0 +1,3349 @@ +// SPDX-License-Identifier: 0BSD + +//! Recurrence adapter for standard SymJIT direct-arena P-kernels. +//! +//! The adapter deliberately owns the recurrence policy which used to live in +//! pyAmpliCol's SymJIT fork. Proven-disjoint identity overwrites bind ordinary +//! P-kernel outputs directly to their arena destinations. Every accumulation, +//! non-identity factor, and before-write alias instead evaluates once into +//! persistent split-complex scratch planes, after which Rusticol applies the +//! exact complex factor and role-specific overwrite/add policy. Inputs which +//! are scalar at recurrence runtime are represented by persistent broadcast +//! planes and refreshed only when their value changes. + +use super::symjit_plane::{ + PlaneDescriptor, SymjitPlaneKernel, SymjitPlaneLayout, SymjitPlaneTable, +}; +use crate::recurrence::direct_backend::{ + DIRECT_STATUS_OK, DirectArenaView, DirectClosureExecutor, DirectContributionExecutor, + DirectExecutorHandle, DirectFactorView, DirectFinalizationExecutor, DirectMomentumView, + DirectParameterView, record_direct_executor_error_detail, +}; +use crate::recurrence::{ + DIRECT_CONTRIBUTION_FLAG_INITIALIZE_DESTINATION, DIRECT_NONE_U32, DirectClosureRow, + DirectContributionRow, DirectExecutorRole, DirectFinalizationRow, +}; +use crate::{RusticolError, RusticolResult}; +use std::cell::{Cell, RefCell}; +use std::collections::HashMap; +use std::ffi::{c_int, c_void}; +use std::hash::{Hash, Hasher}; +use std::mem::size_of; +use std::panic::{AssertUnwindSafe, catch_unwind}; +use std::path::PathBuf; +use std::ptr; +#[cfg(target_arch = "aarch64")] +use wide::f64x2; +#[cfg(target_arch = "x86_64")] +use wide::f64x4; + +pub(crate) const SYMJIT_RECURRENCE_PLANE_APPLICATION_ABI: &str = + "pyamplicol-symjit-plane-application-v2"; + +const STATUS_INVALID_CONTEXT: c_int = 1; +const STATUS_INVALID_ARGUMENT: c_int = 2; +const STATUS_ROLE_MISMATCH: c_int = 3; +const STATUS_EXECUTION_FAILED: c_int = 4; + +/// One model-fixed arena-plane projection. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) enum SymjitDirectPlaneProjection { + ParentCurrent { + parent: u8, + component: u16, + imaginary: bool, + }, + Momentum { + operand: u8, + lorentz_component: u16, + }, + DestinationCurrent { + component: u16, + imaginary: bool, + }, + DestinationAmplitude { + component: u16, + imaginary: bool, + }, +} + +/// One model-fixed scalar projection used to fill a broadcast plane. +#[derive(Clone, Copy, Debug, PartialEq)] +pub(crate) enum SymjitDirectScalarProjection { + Parameter { index: u32, imaginary: bool }, + ExactFactor { imaginary: bool }, + Literal(f64), +} + +/// Maps one split source-input plane in the P-kernel to recurrence storage. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) enum SymjitDirectParameterBinding { + Plane { index: u32 }, + Broadcast { index: u32 }, +} + +/// Owns the context addressed by a [`DirectExecutorHandle`]. +pub(crate) struct LoadedSymjitDirectExecutor { + context: Box, +} + +struct SymjitDirectExecutorContext { + role: DirectExecutorRole, + kernel: SymjitPlaneKernel, + parameter_bindings: Box<[SymjitDirectParameterBinding]>, + input_planes: Box<[SymjitDirectPlaneProjection]>, + scalars: Box<[SymjitDirectScalarProjection]>, + output_destinations: Box<[u32]>, + broadcast_slot_by_binding: Box<[Option]>, + broadcast_scalar_by_slot: Box<[u32]>, + exact_factor_broadcast_slots: Box<[usize]>, + scalar_workspace: RefCell, + tiled_workspace: RefCell, + internal_scratch_bytes: Cell, + internal_broadcast_bytes: Cell, + #[cfg(test)] + invariant_broadcast_resolution_count: Cell, + #[cfg(test)] + exact_factor_broadcast_resolution_count: Cell, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +struct DescriptorStorageIdentity { + current_re: usize, + current_im: usize, + current_scalar_len: u64, + amplitude_re: usize, + amplitude_im: usize, + amplitude_scalar_len: u64, + point_stride: u32, + momenta: usize, + momentum_scalar_len: u64, + momentum_form_count: u32, + momentum_lorentz_component_count: u16, +} + +impl DescriptorStorageIdentity { + fn new(arena: DirectArenaView, momenta: DirectMomentumView) -> Self { + Self { + current_re: arena.current_re.addr(), + current_im: arena.current_im.addr(), + current_scalar_len: arena.current_scalar_len, + amplitude_re: arena.amplitude_re.addr(), + amplitude_im: arena.amplitude_im.addr(), + amplitude_scalar_len: arena.amplitude_scalar_len, + point_stride: arena.point_stride, + momenta: momenta.values.addr(), + momentum_scalar_len: momenta.scalar_len, + momentum_form_count: momenta.form_count, + momentum_lorentz_component_count: momenta.lorentz_component_count, + } + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +struct RowTableIdentity { + role: DirectExecutorRole, + address: usize, + row_count: u32, +} + +impl Hash for RowTableIdentity { + fn hash(&self, state: &mut H) { + (self.role as u16).hash(state); + self.address.hash(state); + self.row_count.hash(state); + } +} + +const ROW_GROUP_LINEAR_LOOKUP_LIMIT: usize = 4; + +struct CachedRowGroup { + identity: RowTableIdentity, + rows: Vec, + descriptor_bytes: usize, +} + +struct CachedRowTables { + scratch: SymjitPlaneTable<'static>, + direct: Option>, +} + +/// Hot storage owned by one loaded recurrence executor. +/// +/// Every cached table contains only the generic raw descriptors exposed by the +/// SymJIT patch. Multiple tables may therefore name the same persistent +/// scratch/broadcast planes without constructing overlapping Rust references; +/// the executor calls them strictly sequentially. +#[derive(Default)] +struct PlaneWorkspace { + storage: Option, + row_groups: Vec, + row_group_indices: HashMap, + descriptor_bytes: usize, + scratch: Vec, + broadcasts: Vec, + broadcast_value_bits: Vec>, + point_stride: usize, +} + +impl PlaneWorkspace { + fn invalidate_row_tables(&mut self) { + self.row_groups.clear(); + self.row_group_indices.clear(); + self.storage = None; + self.descriptor_bytes = 0; + } + + fn cached_row_group_index(&self, identity: RowTableIdentity) -> Option { + // The usual full-schedule cache has only a few entries. Varied runtime + // selections can add many row slices, which must not slow warm lookup. + if self.row_groups.len() <= ROW_GROUP_LINEAR_LOOKUP_LIMIT { + self.row_groups + .iter() + .position(|cached| cached.identity == identity) + } else { + self.row_group_indices.get(&identity).copied() + } + } + + fn cache_row_group(&mut self, cached: CachedRowGroup) -> RusticolResult { + let total_bytes = + checked_descriptor_cache_bytes(self.descriptor_bytes, cached.descriptor_bytes)?; + self.row_groups.try_reserve(1).map_err(|error| { + RusticolError::compatibility(format!( + "could not reserve recurrence descriptor row group: {error}" + )) + })?; + let index = self.row_groups.len(); + if index >= ROW_GROUP_LINEAR_LOOKUP_LIMIT { + let additional = if index == ROW_GROUP_LINEAR_LOOKUP_LIMIT { + index + 1 + } else { + 1 + }; + self.row_group_indices + .try_reserve(additional) + .map_err(|error| { + RusticolError::compatibility(format!( + "could not reserve recurrence descriptor row index: {error}" + )) + })?; + if index == ROW_GROUP_LINEAR_LOOKUP_LIMIT { + self.row_group_indices.extend( + self.row_groups + .iter() + .enumerate() + .map(|(index, cached)| (cached.identity, index)), + ); + } + self.row_group_indices.insert(cached.identity, index); + } + self.descriptor_bytes = total_bytes; + self.row_groups.push(cached); + Ok(index) + } + + fn prepare( + &mut self, + point_stride: usize, + input_plane_count: usize, + output_plane_count: usize, + broadcast_plane_count: usize, + ) -> RusticolResult<()> { + let descriptor_count = input_plane_count + .checked_add(output_plane_count) + .ok_or_else(|| RusticolError::internal("recurrence descriptor count overflows"))?; + let expected_scratch = output_plane_count + .checked_mul(point_stride) + .ok_or_else(|| RusticolError::internal("recurrence scratch size overflows"))?; + let expected_broadcasts = broadcast_plane_count + .checked_mul(point_stride) + .ok_or_else(|| RusticolError::internal("recurrence broadcast size overflows"))?; + if self.point_stride == point_stride + && self.scratch.len() == expected_scratch + && self.broadcasts.len() == expected_broadcasts + { + return Ok(()); + } + + // Drop every raw table before replacing its backing storage. + self.invalidate_row_tables(); + let mut scratch = Vec::new(); + scratch + .try_reserve_exact(expected_scratch) + .map_err(|error| { + RusticolError::internal(format!( + "could not allocate recurrence scratch planes: {error}" + )) + })?; + scratch.resize(expected_scratch, 0.0); + let mut broadcasts = Vec::new(); + broadcasts + .try_reserve_exact(expected_broadcasts) + .map_err(|error| { + RusticolError::internal(format!( + "could not allocate recurrence broadcast planes: {error}" + )) + })?; + broadcasts.resize(expected_broadcasts, 0.0); + let mut broadcast_value_bits = Vec::new(); + broadcast_value_bits + .try_reserve_exact(broadcast_plane_count) + .map_err(|error| { + RusticolError::internal(format!( + "could not allocate recurrence broadcast cache: {error}" + )) + })?; + broadcast_value_bits.resize(broadcast_plane_count, None); + self.scratch = scratch; + self.broadcasts = broadcasts; + self.broadcast_value_bits = broadcast_value_bits; + self.point_stride = point_stride; + debug_assert_eq!(descriptor_count, input_plane_count + output_plane_count); + Ok(()) + } + + fn broadcast_pointer(&mut self, slot: usize, value: f64) -> RusticolResult<(*mut f64, bool)> { + let start = slot + .checked_mul(self.point_stride) + .ok_or_else(|| RusticolError::internal("broadcast plane offset overflows"))?; + let end = start + .checked_add(self.point_stride) + .ok_or_else(|| RusticolError::internal("broadcast plane range overflows"))?; + let plane = self.broadcasts.get_mut(start..end).ok_or_else(|| { + RusticolError::internal("broadcast plane is outside its persistent workspace") + })?; + let bits = value.to_bits(); + let cached = self.broadcast_value_bits.get_mut(slot).ok_or_else(|| { + RusticolError::internal("broadcast cache slot is outside its workspace") + })?; + let refreshed = *cached != Some(bits); + if refreshed { + plane.fill(value); + *cached = Some(bits); + } + Ok((plane.as_mut_ptr(), refreshed)) + } + + fn scratch_pointer(&mut self, plane: usize) -> RusticolResult<*mut f64> { + let start = plane + .checked_mul(self.point_stride) + .ok_or_else(|| RusticolError::internal("scratch plane offset overflows"))?; + let end = start + .checked_add(self.point_stride) + .ok_or_else(|| RusticolError::internal("scratch plane range overflows"))?; + self.scratch + .get_mut(start..end) + .map(<[f64]>::as_mut_ptr) + .ok_or_else(|| RusticolError::internal("scratch plane is outside its workspace")) + } + + fn broadcast_plane_pointer(&mut self, slot: usize) -> RusticolResult<*mut f64> { + let start = slot + .checked_mul(self.point_stride) + .ok_or_else(|| RusticolError::internal("broadcast plane offset overflows"))?; + let end = start + .checked_add(self.point_stride) + .ok_or_else(|| RusticolError::internal("broadcast plane range overflows"))?; + self.broadcasts + .get_mut(start..end) + .map(<[f64]>::as_mut_ptr) + .ok_or_else(|| RusticolError::internal("broadcast plane is outside its workspace")) + } +} + +impl LoadedSymjitDirectExecutor { + #[allow(clippy::too_many_arguments)] + pub(crate) fn load_prepared_application_bytes( + bytes: &[u8], + display_path: PathBuf, + application_abi: &str, + expected_optimization_level: u32, + expected_compression: bool, + role: DirectExecutorRole, + parameter_bindings: Vec, + input_planes: Vec, + scalars: Vec, + output_destinations: Vec, + ) -> RusticolResult { + if role == DirectExecutorRole::Source { + return Err(RusticolError::compatibility( + "recurrence source executors are Rusticol SourceIR intrinsics, not SymJIT plane applications", + )); + } + if application_abi != SYMJIT_RECURRENCE_PLANE_APPLICATION_ABI { + return Err(RusticolError::compatibility(format!( + "unsupported recurrence SymJIT plane-application ABI {application_abi:?}; regenerate the prepared model with this pyAmpliCol version" + ))); + } + if !parameter_bindings.len().is_multiple_of(2) + || !output_destinations.len().is_multiple_of(2) + { + return Err(RusticolError::integrity( + "recurrence SymJIT split-complex binding has an odd plane count", + )); + } + let layout = + SymjitPlaneLayout::complex(parameter_bindings.len() / 2, output_destinations.len() / 2); + let kernel = SymjitPlaneKernel::load_bytes(bytes, display_path.clone(), layout)?; + if u32::from(kernel.optimization_level()) != expected_optimization_level { + return Err(RusticolError::integrity(format!( + "recurrence SymJIT plane application {} stores optimization level {}, but its \ + authenticated template declares optimization level \ + {expected_optimization_level}; regenerate the prepared model", + display_path.display(), + kernel.optimization_level(), + ))); + } + if kernel.compression() != expected_compression { + return Err(RusticolError::integrity(format!( + "recurrence SymJIT plane application {} stores compression {}, but its \ + authenticated prepared-kernel manifest declares compression \ + {expected_compression}; regenerate the prepared model", + display_path.display(), + kernel.compression(), + ))); + } + validate_binding( + &kernel, + role, + ¶meter_bindings, + &input_planes, + &scalars, + &output_destinations, + )?; + + // A structured evaluator may bind one scalar projection to several + // source planes (most commonly the shared structural-zero projection). + // Intern those occurrences on the cold path so duplicate descriptors + // address one persistent broadcast plane. + let mut slot_by_scalar = Vec::new(); + slot_by_scalar + .try_reserve_exact(scalars.len()) + .map_err(|error| { + RusticolError::internal(format!( + "could not allocate recurrence scalar slot map: {error}" + )) + })?; + slot_by_scalar.resize(scalars.len(), None); + let mut broadcast_scalar_by_slot = Vec::new(); + broadcast_scalar_by_slot + .try_reserve_exact(scalars.len()) + .map_err(|error| { + RusticolError::internal(format!( + "could not allocate recurrence broadcast scalar catalog: {error}" + )) + })?; + let mut broadcast_slot_by_binding = Vec::new(); + broadcast_slot_by_binding + .try_reserve_exact(parameter_bindings.len()) + .map_err(|error| { + RusticolError::internal(format!( + "could not allocate recurrence broadcast binding map: {error}" + )) + })?; + for binding in ¶meter_bindings { + let SymjitDirectParameterBinding::Broadcast { index } = *binding else { + broadcast_slot_by_binding.push(None); + continue; + }; + let scalar_index = index as usize; + let slot = if let Some(slot) = slot_by_scalar[scalar_index] { + slot + } else { + let slot = u32::try_from(broadcast_scalar_by_slot.len()).map_err(|_| { + RusticolError::integrity( + "recurrence persistent broadcast plane count exceeds u32", + ) + })?; + slot_by_scalar[scalar_index] = Some(slot); + broadcast_scalar_by_slot.push(index); + slot + }; + broadcast_slot_by_binding.push(Some(slot)); + } + let mut exact_factor_broadcast_slots = Vec::new(); + exact_factor_broadcast_slots + .try_reserve_exact(broadcast_scalar_by_slot.len()) + .map_err(|error| { + RusticolError::internal(format!( + "could not allocate recurrence exact-factor broadcast catalog: {error}" + )) + })?; + for (slot, &scalar_index) in broadcast_scalar_by_slot.iter().enumerate() { + match scalars.get(scalar_index as usize).ok_or_else(|| { + RusticolError::integrity("recurrence scalar projection is out of bounds") + })? { + SymjitDirectScalarProjection::Literal(_) + | SymjitDirectScalarProjection::Parameter { .. } => {} + SymjitDirectScalarProjection::ExactFactor { .. } => { + exact_factor_broadcast_slots.push(slot); + } + } + } + + Ok(Self { + context: Box::new(SymjitDirectExecutorContext { + role, + kernel, + parameter_bindings: parameter_bindings.into_boxed_slice(), + input_planes: input_planes.into_boxed_slice(), + scalars: scalars.into_boxed_slice(), + output_destinations: output_destinations.into_boxed_slice(), + broadcast_slot_by_binding: broadcast_slot_by_binding.into_boxed_slice(), + broadcast_scalar_by_slot: broadcast_scalar_by_slot.into_boxed_slice(), + exact_factor_broadcast_slots: exact_factor_broadcast_slots.into_boxed_slice(), + scalar_workspace: RefCell::new(PlaneWorkspace::default()), + tiled_workspace: RefCell::new(PlaneWorkspace::default()), + internal_scratch_bytes: Cell::new(0), + internal_broadcast_bytes: Cell::new(0), + #[cfg(test)] + invariant_broadcast_resolution_count: Cell::new(0), + #[cfg(test)] + exact_factor_broadcast_resolution_count: Cell::new(0), + }), + }) + } + + pub(crate) fn handle(&self) -> DirectExecutorHandle { + let context = ptr::from_ref(self.context.as_ref()).cast(); + match self.context.role { + DirectExecutorRole::Contribution => DirectExecutorHandle::Contribution { + call: execute_contribution_rows as DirectContributionExecutor, + context, + }, + DirectExecutorRole::Finalization => DirectExecutorHandle::Finalization { + call: execute_finalization_rows as DirectFinalizationExecutor, + context, + }, + DirectExecutorRole::Closure => DirectExecutorHandle::Closure { + call: execute_closure_rows as DirectClosureExecutor, + context, + }, + DirectExecutorRole::Source => { + unreachable!("source roles are rejected while loading a SymJIT plane executor") + } + } + } + + /// Drop every cached table that borrows Direct-Arena row/storage pointers. + pub(crate) fn invalidate_row_tables(&self) -> RusticolResult<()> { + let mut scalar = self + .context + .scalar_workspace + .try_borrow_mut() + .map_err(|_| { + RusticolError::internal( + "cannot invalidate recurrence SymJIT row tables during an active call", + ) + })?; + let mut tiled = self.context.tiled_workspace.try_borrow_mut().map_err(|_| { + RusticolError::internal( + "cannot invalidate recurrence SymJIT row tables during an active call", + ) + })?; + scalar.invalidate_row_tables(); + tiled.invalidate_row_tables(); + Ok(()) + } + + pub(crate) fn internal_traffic_bytes(&self) -> (u64, u64) { + ( + self.context.internal_scratch_bytes.get(), + self.context.internal_broadcast_bytes.get(), + ) + } + + #[cfg(test)] + fn simd_lane_width(&self) -> usize { + self.context.kernel.simd_lanes() + } + + #[cfg(test)] + fn cached_direct_table_count(&self) -> usize { + [ + &self.context.scalar_workspace, + &self.context.tiled_workspace, + ] + .into_iter() + .map(|workspace| { + workspace + .borrow() + .row_groups + .iter() + .flat_map(|group| &group.rows) + .filter(|row| row.direct.is_some()) + .count() + }) + .sum() + } + + #[cfg(test)] + fn broadcast_plane_count(&self) -> usize { + self.context.broadcast_scalar_by_slot.len() + } + + #[cfg(test)] + fn broadcast_resolution_counts(&self) -> (u64, u64) { + ( + self.context.invariant_broadcast_resolution_count.get(), + self.context.exact_factor_broadcast_resolution_count.get(), + ) + } +} + +fn validate_binding( + kernel: &SymjitPlaneKernel, + role: DirectExecutorRole, + parameter_bindings: &[SymjitDirectParameterBinding], + input_planes: &[SymjitDirectPlaneProjection], + scalars: &[SymjitDirectScalarProjection], + output_destinations: &[u32], +) -> RusticolResult<()> { + if parameter_bindings.len() != kernel.input_plane_count() + || output_destinations.len() != kernel.output_plane_count() + { + return Err(RusticolError::integrity( + "recurrence plane binding shape does not match its SymJIT P-kernel", + )); + } + if scalars.first() != Some(&SymjitDirectScalarProjection::ExactFactor { imaginary: false }) + || scalars.get(1) != Some(&SymjitDirectScalarProjection::ExactFactor { imaginary: true }) + { + return Err(RusticolError::integrity( + "recurrence scalar sources 0 and 1 must be the exact factor real and imaginary values", + )); + } + for binding in parameter_bindings { + match *binding { + SymjitDirectParameterBinding::Plane { index } + if index as usize >= input_planes.len() => + { + return Err(RusticolError::integrity( + "recurrence P-kernel input plane projection is out of bounds", + )); + } + SymjitDirectParameterBinding::Broadcast { index } + if index as usize >= scalars.len() => + { + return Err(RusticolError::integrity( + "recurrence P-kernel broadcast projection is out of bounds", + )); + } + _ => {} + } + } + for (output, &destination) in output_destinations.iter().enumerate() { + let projection = input_planes + .get(destination as usize) + .copied() + .ok_or_else(|| { + RusticolError::integrity( + "recurrence P-kernel destination projection is out of bounds", + ) + })?; + let expected_imaginary = output % 2 == 1; + let valid = match (role, projection) { + ( + DirectExecutorRole::Contribution | DirectExecutorRole::Finalization, + SymjitDirectPlaneProjection::DestinationCurrent { imaginary, .. }, + ) + | ( + DirectExecutorRole::Closure, + SymjitDirectPlaneProjection::DestinationAmplitude { imaginary, .. }, + ) => imaginary == expected_imaginary, + _ => false, + }; + if !valid { + return Err(RusticolError::integrity( + "recurrence P-kernel output does not map to the expected split-complex destination", + )); + } + } + for pair in output_destinations.chunks_exact(2) { + let real = input_planes[pair[0] as usize]; + let imaginary = input_planes[pair[1] as usize]; + if !matching_split_destination(real, imaginary) { + return Err(RusticolError::integrity( + "recurrence P-kernel output pair maps to different destination components", + )); + } + } + Ok(()) +} + +fn matching_split_destination( + real: SymjitDirectPlaneProjection, + imaginary: SymjitDirectPlaneProjection, +) -> bool { + match (real, imaginary) { + ( + SymjitDirectPlaneProjection::DestinationCurrent { + component: left, + imaginary: false, + }, + SymjitDirectPlaneProjection::DestinationCurrent { + component: right, + imaginary: true, + }, + ) + | ( + SymjitDirectPlaneProjection::DestinationAmplitude { + component: left, + imaginary: false, + }, + SymjitDirectPlaneProjection::DestinationAmplitude { + component: right, + imaginary: true, + }, + ) => left == right, + _ => false, + } +} + +unsafe extern "C" fn execute_contribution_rows( + context: *const c_void, + arena: DirectArenaView, + momenta: DirectMomentumView, + parameters: DirectParameterView, + factors: DirectFactorView, + rows: *const DirectContributionRow, + row_count: u32, + point_count: u32, +) -> c_int { + invoke_typed_rows( + context, + arena, + momenta, + parameters, + factors, + rows, + row_count, + point_count, + ) +} + +unsafe extern "C" fn execute_finalization_rows( + context: *const c_void, + arena: DirectArenaView, + momenta: DirectMomentumView, + parameters: DirectParameterView, + factors: DirectFactorView, + rows: *const DirectFinalizationRow, + row_count: u32, + point_count: u32, +) -> c_int { + invoke_typed_rows( + context, + arena, + momenta, + parameters, + factors, + rows, + row_count, + point_count, + ) +} + +unsafe extern "C" fn execute_closure_rows( + context: *const c_void, + arena: DirectArenaView, + momenta: DirectMomentumView, + parameters: DirectParameterView, + factors: DirectFactorView, + rows: *const DirectClosureRow, + row_count: u32, + point_count: u32, +) -> c_int { + invoke_typed_rows( + context, + arena, + momenta, + parameters, + factors, + rows, + row_count, + point_count, + ) +} + +#[allow(clippy::too_many_arguments)] +fn invoke_typed_rows( + context: *const c_void, + arena: DirectArenaView, + momenta: DirectMomentumView, + parameters: DirectParameterView, + factors: DirectFactorView, + rows: *const T, + row_count: u32, + point_count: u32, +) -> c_int { + if context.is_null() { + return STATUS_INVALID_CONTEXT; + } + if rows.is_null() || row_count == 0 || point_count == 0 { + return STATUS_INVALID_ARGUMENT; + } + match catch_unwind(AssertUnwindSafe(|| { + let context = unsafe { &*context.cast::() }; + if context.role != T::ROLE { + return Ok(STATUS_ROLE_MISMATCH); + } + let rows = unsafe { std::slice::from_raw_parts(rows, row_count as usize) }; + context.execute_rows(arena, momenta, parameters, factors, rows, point_count)?; + Ok(DIRECT_STATUS_OK) + })) { + Ok(Ok(status)) => status, + Ok(Err(error)) => { + record_direct_executor_error_detail(error); + STATUS_EXECUTION_FAILED + } + Err(payload) => { + let message = if let Some(message) = payload.downcast_ref::<&str>() { + (*message).to_owned() + } else if let Some(message) = payload.downcast_ref::() { + message.clone() + } else { + "non-string panic payload".to_owned() + }; + record_direct_executor_error_detail(RusticolError::internal(format!( + "recurrence SymJIT executor panicked: {message}" + ))); + STATUS_EXECUTION_FAILED + } + } +} + +enum DirectRowRef<'a> { + Contribution(&'a DirectContributionRow), + Finalization(&'a DirectFinalizationRow), + Closure(&'a DirectClosureRow), +} + +trait DirectTypedRow { + const ROLE: DirectExecutorRole; + fn direct_row_ref(&self) -> DirectRowRef<'_>; +} + +impl DirectTypedRow for DirectContributionRow { + const ROLE: DirectExecutorRole = DirectExecutorRole::Contribution; + + fn direct_row_ref(&self) -> DirectRowRef<'_> { + DirectRowRef::Contribution(self) + } +} + +impl DirectTypedRow for DirectFinalizationRow { + const ROLE: DirectExecutorRole = DirectExecutorRole::Finalization; + + fn direct_row_ref(&self) -> DirectRowRef<'_> { + DirectRowRef::Finalization(self) + } +} + +impl DirectTypedRow for DirectClosureRow { + const ROLE: DirectExecutorRole = DirectExecutorRole::Closure; + + fn direct_row_ref(&self) -> DirectRowRef<'_> { + DirectRowRef::Closure(self) + } +} + +impl DirectRowRef<'_> { + fn exact_factor_id(&self) -> u32 { + match self { + Self::Contribution(row) => row.exact_factor_id, + Self::Finalization(row) => row.exact_factor_id, + Self::Closure(row) => row.exact_factor_id, + } + } + + fn parent_component_base(&self, parent: u8) -> Option { + match (self, parent) { + (Self::Contribution(row), 0) => Some(row.parent0_component_base), + (Self::Contribution(row), 1) + if row.parent1_component_base_or_sentinel != DIRECT_NONE_U32 => + { + Some(row.parent1_component_base_or_sentinel) + } + (Self::Finalization(row), 0) => Some(row.component_base), + (Self::Closure(row), 0) => Some(row.parent0_component_base), + (Self::Closure(row), 1) + if row.parent1_component_base_or_sentinel != DIRECT_NONE_U32 => + { + Some(row.parent1_component_base_or_sentinel) + } + _ => None, + } + } + + fn momentum_form_id(&self, operand: u8) -> Option { + match (self, operand) { + (Self::Contribution(row), 0) => Some(row.parent0_momentum_form_id), + (Self::Contribution(row), 1) + if row.parent1_momentum_form_id_or_sentinel != DIRECT_NONE_U32 => + { + Some(row.parent1_momentum_form_id_or_sentinel) + } + (Self::Finalization(row), 0) => Some(row.momentum_form_id), + (Self::Closure(row), 0) => Some(row.parent0_momentum_form_id), + (Self::Closure(row), 1) + if row.parent1_momentum_form_id_or_sentinel != DIRECT_NONE_U32 => + { + Some(row.parent1_momentum_form_id_or_sentinel) + } + _ => None, + } + } + + fn destination_current_base(&self) -> Option { + match self { + Self::Contribution(row) => Some(row.destination_component_base), + Self::Finalization(row) => Some(row.component_base), + Self::Closure(_) => None, + } + } + + fn destination_amplitude_base(&self) -> Option { + match self { + Self::Closure(row) => Some(row.amplitude_destination_id), + _ => None, + } + } + + fn accumulates_output(&self) -> bool { + match self { + Self::Contribution(row) => { + row.flags & DIRECT_CONTRIBUTION_FLAG_INITIALIZE_DESTINATION == 0 + } + Self::Finalization(_) => false, + Self::Closure(_) => true, + } + } +} + +impl SymjitDirectExecutorContext { + fn execute_rows( + &self, + arena: DirectArenaView, + momenta: DirectMomentumView, + parameters: DirectParameterView, + factors: DirectFactorView, + rows: &[T], + point_count: u32, + ) -> RusticolResult<()> { + if arena.point_stride == 0 + || point_count > arena.point_stride + || arena.point_stride != momenta.point_stride + { + return Err(RusticolError::invalid_argument( + "recurrence SymJIT point range or momentum stride is invalid", + )); + } + let point_stride = arena.point_stride as usize; + let broadcast_count = self.broadcast_scalar_by_slot.len(); + let workspace = if point_stride == 1 { + &self.scalar_workspace + } else { + &self.tiled_workspace + }; + let mut workspace = workspace.borrow_mut(); + workspace.prepare( + point_stride, + self.parameter_bindings.len(), + self.output_destinations.len(), + broadcast_count, + )?; + + let storage = DescriptorStorageIdentity::new(arena, momenta); + if workspace.storage.is_some_and(|cached| cached != storage) { + workspace.invalidate_row_tables(); + } + workspace.storage = Some(storage); + let row_count = u32::try_from(rows.len()) + .map_err(|_| RusticolError::integrity("recurrence row count exceeds u32"))?; + let identity = RowTableIdentity { + role: T::ROLE, + address: rows.as_ptr().addr(), + row_count, + }; + let group_index = if let Some(index) = workspace.cached_row_group_index(identity) { + index + } else { + let cached = + self.build_cached_row_group(&mut workspace, identity, rows, arena, momenta)?; + workspace.cache_row_group(cached)? + }; + + let mut call_scratch_bytes = 0_u64; + let mut call_broadcast_bytes = 0_u64; + let result = (|| { + for (row_index, typed_row) in rows.iter().enumerate() { + let row = typed_row.direct_row_ref(); + let refresh_bytes = if row_index == 0 { + // Preserve scalar-catalog order on the first row while + // refreshing every literal/model parameter exactly once + // for this executor call. Only exact factors can vary on + // later rows. + self.refresh_first_row_broadcasts(&mut workspace, &row, parameters, factors)? + } else { + self.refresh_exact_factor_broadcasts(&mut workspace, &row, parameters, factors)? + }; + call_broadcast_bytes = call_broadcast_bytes.saturating_add(refresh_bytes); + let (factor_re, factor_im) = exact_factor(&row, factors)?; + let use_direct = is_identity_factor(factor_re, factor_im) + && workspace + .row_groups + .get(group_index) + .and_then(|group| group.rows.get(row_index)) + .is_some_and(|tables| tables.direct.is_some()); + { + let tables = workspace + .row_groups + .get_mut(group_index) + .and_then(|group| group.rows.get_mut(row_index)) + .ok_or_else(|| { + RusticolError::internal( + "recurrence cached descriptor table is out of bounds", + ) + })?; + let table = if use_direct { + tables.direct.as_mut().ok_or_else(|| { + RusticolError::internal( + "recurrence direct-output table disappeared after selection", + ) + })? + } else { + &mut tables.scratch + }; + self.kernel.execute_table(table, 0, point_count as usize)?; + } + if !use_direct { + apply_outputs( + &self.input_planes, + &self.output_destinations, + &workspace.scratch, + point_stride, + point_count as usize, + factor_re, + factor_im, + &row, + arena, + momenta, + )?; + let scratch_one_way = internal_plane_bytes( + self.output_destinations.len(), + point_count as usize, + "recurrence scratch traffic", + )?; + call_scratch_bytes = + call_scratch_bytes.saturating_add(scratch_one_way.saturating_mul(2)); + } + call_broadcast_bytes = call_broadcast_bytes.saturating_add(internal_plane_bytes( + broadcast_count, + point_count as usize, + "recurrence broadcast traffic", + )?); + } + Ok(()) + })(); + self.record_internal_scratch_bytes(call_scratch_bytes); + self.record_internal_broadcast_bytes(call_broadcast_bytes); + result + } + + fn build_cached_row_group( + &self, + workspace: &mut PlaneWorkspace, + identity: RowTableIdentity, + rows: &[T], + arena: DirectArenaView, + momenta: DirectMomentumView, + ) -> RusticolResult { + let descriptor_count = self + .parameter_bindings + .len() + .checked_add(self.output_destinations.len()) + .ok_or_else(|| RusticolError::integrity("recurrence descriptor count overflows"))?; + let bytes_per_table = descriptor_count + .checked_mul(size_of::>()) + .ok_or_else(|| { + RusticolError::compatibility("recurrence descriptor cache size overflows") + })?; + let mut descriptor_bytes = 0_usize; + let mut cached_rows: Vec = Vec::new(); + cached_rows.try_reserve_exact(rows.len()).map_err(|_| { + RusticolError::compatibility("could not reserve recurrence descriptor tables") + })?; + for typed_row in rows { + let row = typed_row.direct_row_ref(); + let mut input_pointers = Vec::new(); + input_pointers + .try_reserve_exact(self.parameter_bindings.len()) + .map_err(|_| { + RusticolError::compatibility( + "could not reserve recurrence input plane pointers", + ) + })?; + for (binding_index, binding) in self.parameter_bindings.iter().enumerate() { + let pointer = match *binding { + SymjitDirectParameterBinding::Plane { index } => resolve_plane_pointer( + *self.input_planes.get(index as usize).ok_or_else(|| { + RusticolError::integrity("recurrence input projection is out of bounds") + })?, + &row, + arena, + momenta, + )?, + SymjitDirectParameterBinding::Broadcast { .. } => { + let slot = + self.broadcast_slot_by_binding[binding_index].ok_or_else(|| { + RusticolError::internal( + "recurrence broadcast binding has no persistent slot", + ) + })? as usize; + workspace.broadcast_plane_pointer(slot)? + } + }; + input_pointers.push(pointer); + } + let mut output_pointers = Vec::new(); + output_pointers + .try_reserve_exact(self.output_destinations.len()) + .map_err(|_| { + RusticolError::compatibility( + "could not reserve recurrence output plane pointers", + ) + })?; + for &destination in self.output_destinations.iter() { + output_pointers.push(resolve_plane_pointer( + *self.input_planes.get(destination as usize).ok_or_else(|| { + RusticolError::integrity("recurrence output projection is out of bounds") + })?, + &row, + arena, + momenta, + )?); + } + let mut scratch_pointers = Vec::new(); + scratch_pointers + .try_reserve_exact(self.output_destinations.len()) + .map_err(|_| { + RusticolError::compatibility( + "could not reserve recurrence scratch plane pointers", + ) + })?; + for output in 0..self.output_destinations.len() { + scratch_pointers.push(workspace.scratch_pointer(output)?); + } + let scratch = build_plane_table( + &self.kernel, + &input_pointers, + &scratch_pointers, + workspace.point_stride, + )?; + descriptor_bytes = descriptor_bytes + .checked_add(bytes_per_table) + .ok_or_else(|| { + RusticolError::compatibility("recurrence descriptor cache size overflows") + })?; + let direct = if direct_outputs_are_structurally_safe( + self.role, + &input_pointers, + &output_pointers, + workspace.point_stride, + )? { + descriptor_bytes = + descriptor_bytes + .checked_add(bytes_per_table) + .ok_or_else(|| { + RusticolError::compatibility( + "recurrence descriptor cache size overflows", + ) + })?; + Some(build_plane_table( + &self.kernel, + &input_pointers, + &output_pointers, + workspace.point_stride, + )?) + } else { + None + }; + cached_rows.push(CachedRowTables { scratch, direct }); + } + Ok(CachedRowGroup { + identity, + rows: cached_rows, + descriptor_bytes, + }) + } + + fn refresh_first_row_broadcasts( + &self, + workspace: &mut PlaneWorkspace, + row: &DirectRowRef<'_>, + parameters: DirectParameterView, + factors: DirectFactorView, + ) -> RusticolResult { + let mut refresh_bytes = 0_u64; + for (slot, &index) in self.broadcast_scalar_by_slot.iter().enumerate() { + let projection = self.scalars.get(index as usize).ok_or_else(|| { + RusticolError::integrity("recurrence scalar projection is out of bounds") + })?; + #[cfg(test)] + match projection { + SymjitDirectScalarProjection::Literal(_) + | SymjitDirectScalarProjection::Parameter { .. } => { + self.invariant_broadcast_resolution_count.set( + self.invariant_broadcast_resolution_count + .get() + .saturating_add(1), + ) + } + SymjitDirectScalarProjection::ExactFactor { .. } => { + self.exact_factor_broadcast_resolution_count.set( + self.exact_factor_broadcast_resolution_count + .get() + .saturating_add(1), + ) + } + } + let value = resolve_scalar_value(projection, row, parameters, factors)?; + let (_, refreshed) = workspace.broadcast_pointer(slot, value)?; + if refreshed { + refresh_bytes = refresh_bytes.saturating_add(internal_plane_bytes( + 1, + workspace.point_stride, + "recurrence broadcast refresh traffic", + )?); + } + } + Ok(refresh_bytes) + } + + fn refresh_exact_factor_broadcasts( + &self, + workspace: &mut PlaneWorkspace, + row: &DirectRowRef<'_>, + parameters: DirectParameterView, + factors: DirectFactorView, + ) -> RusticolResult { + let mut refresh_bytes = 0_u64; + for &slot in self.exact_factor_broadcast_slots.iter() { + let scalar_index = *self.broadcast_scalar_by_slot.get(slot).ok_or_else(|| { + RusticolError::internal( + "recurrence exact-factor broadcast slot is outside its catalog", + ) + })?; + let projection = self.scalars.get(scalar_index as usize).ok_or_else(|| { + RusticolError::integrity("recurrence scalar projection is out of bounds") + })?; + let SymjitDirectScalarProjection::ExactFactor { .. } = projection else { + return Err(RusticolError::internal( + "recurrence row-local broadcast is not an exact factor", + )); + }; + #[cfg(test)] + self.exact_factor_broadcast_resolution_count.set( + self.exact_factor_broadcast_resolution_count + .get() + .saturating_add(1), + ); + let value = resolve_scalar_value(projection, row, parameters, factors)?; + let (_, refreshed) = workspace.broadcast_pointer(slot, value)?; + if refreshed { + refresh_bytes = refresh_bytes.saturating_add(internal_plane_bytes( + 1, + workspace.point_stride, + "recurrence broadcast refresh traffic", + )?); + } + } + Ok(refresh_bytes) + } + + fn record_internal_scratch_bytes(&self, bytes: u64) { + self.internal_scratch_bytes + .set(self.internal_scratch_bytes.get().saturating_add(bytes)); + } + + fn record_internal_broadcast_bytes(&self, bytes: u64) { + self.internal_broadcast_bytes + .set(self.internal_broadcast_bytes.get().saturating_add(bytes)); + } +} + +fn checked_descriptor_cache_bytes(current: usize, additional: usize) -> RusticolResult { + current.checked_add(additional).ok_or_else(|| { + RusticolError::compatibility("recurrence SymJIT descriptor cache size overflows") + }) +} + +fn build_plane_table( + kernel: &SymjitPlaneKernel, + input_pointers: &[*mut f64], + output_pointers: &[*mut f64], + point_stride: usize, +) -> RusticolResult> { + let descriptor_count = input_pointers + .len() + .checked_add(output_pointers.len()) + .ok_or_else(|| RusticolError::integrity("recurrence descriptor count overflows"))?; + let mut descriptors: Vec> = Vec::new(); + descriptors + .try_reserve_exact(descriptor_count) + .map_err(|_| { + RusticolError::compatibility("could not reserve recurrence plane descriptors") + })?; + for &pointer in input_pointers.iter().chain(output_pointers) { + descriptors.push(unsafe { PlaneDescriptor::from_cached_raw_parts(pointer, point_stride)? }); + } + kernel.build_raw_table(descriptors) +} + +fn direct_outputs_are_structurally_safe( + role: DirectExecutorRole, + input_pointers: &[*mut f64], + output_pointers: &[*mut f64], + point_stride: usize, +) -> RusticolResult { + if role != DirectExecutorRole::Finalization { + return Ok(false); + } + for (output_index, &pointer) in output_pointers.iter().enumerate() { + let output = PlaneAddressRange::new(pointer, point_stride)?; + for &input in input_pointers { + if output.overlaps(PlaneAddressRange::new(input, point_stride)?) { + return Ok(false); + } + } + for &prior in &output_pointers[..output_index] { + if output.overlaps(PlaneAddressRange::new(prior, point_stride)?) { + return Ok(false); + } + } + } + Ok(true) +} + +#[derive(Clone, Copy)] +struct PlaneAddressRange { + start: usize, + end: usize, +} + +impl PlaneAddressRange { + fn new(pointer: *mut f64, point_stride: usize) -> RusticolResult { + if pointer.is_null() || point_stride == 0 { + return Err(RusticolError::integrity( + "recurrence direct-output plane has a null pointer or zero stride", + )); + } + let bytes = point_stride + .checked_mul(size_of::()) + .ok_or_else(|| RusticolError::integrity("recurrence plane byte range overflows"))?; + let start = pointer.addr(); + let end = start + .checked_add(bytes) + .ok_or_else(|| RusticolError::integrity("recurrence plane address range overflows"))?; + Ok(Self { start, end }) + } + + fn overlaps(self, other: Self) -> bool { + self.start < other.end && other.start < self.end + } +} + +fn is_identity_factor(real: f64, imaginary: f64) -> bool { + real.to_bits() == 1.0_f64.to_bits() && imaginary.to_bits() == 0.0_f64.to_bits() +} + +fn internal_plane_bytes( + plane_count: usize, + point_count: usize, + description: &str, +) -> RusticolResult { + let bytes = plane_count + .checked_mul(point_count) + .and_then(|count| count.checked_mul(size_of::())) + .ok_or_else(|| RusticolError::integrity(format!("{description} overflows usize")))?; + u64::try_from(bytes).map_err(|_| RusticolError::integrity(format!("{description} exceeds u64"))) +} + +#[allow(clippy::too_many_arguments)] +fn apply_outputs( + projections: &[SymjitDirectPlaneProjection], + output_destinations: &[u32], + scratch: &[f64], + point_stride: usize, + point_count: usize, + factor_re: f64, + factor_im: f64, + row: &DirectRowRef<'_>, + arena: DirectArenaView, + momenta: DirectMomentumView, +) -> RusticolResult<()> { + for (component, destination_pair) in output_destinations.chunks_exact(2).enumerate() { + let scratch_re_start = component + .checked_mul(2) + .and_then(|plane| plane.checked_mul(point_stride)) + .ok_or_else(|| RusticolError::internal("recurrence scratch range overflows"))?; + let scratch_im_start = scratch_re_start + .checked_add(point_stride) + .ok_or_else(|| RusticolError::internal("recurrence scratch range overflows"))?; + let scratch_re = scratch + .get(scratch_re_start..scratch_re_start + point_count) + .ok_or_else(|| RusticolError::internal("recurrence real scratch is out of bounds"))?; + let scratch_im = scratch + .get(scratch_im_start..scratch_im_start + point_count) + .ok_or_else(|| { + RusticolError::internal("recurrence imaginary scratch is out of bounds") + })?; + let destination_re = resolve_plane_pointer( + projections[destination_pair[0] as usize], + row, + arena, + momenta, + )?; + let destination_im = resolve_plane_pointer( + projections[destination_pair[1] as usize], + row, + arena, + momenta, + )?; + let destination_re = unsafe { std::slice::from_raw_parts_mut(destination_re, point_count) }; + let destination_im = unsafe { std::slice::from_raw_parts_mut(destination_im, point_count) }; + apply_complex_scaled( + destination_re, + destination_im, + scratch_re, + scratch_im, + factor_re, + factor_im, + row.accumulates_output(), + ); + } + Ok(()) +} + +#[inline] +fn apply_complex_scaled( + destination_re: &mut [f64], + destination_im: &mut [f64], + source_re: &[f64], + source_im: &[f64], + factor_re: f64, + factor_im: f64, + accumulate: bool, +) { + debug_assert_eq!(destination_re.len(), destination_im.len()); + debug_assert_eq!(destination_re.len(), source_re.len()); + debug_assert_eq!(destination_re.len(), source_im.len()); + #[cfg(target_arch = "x86_64")] + let mut point = apply_complex_scaled_x86_64( + destination_re, + destination_im, + source_re, + source_im, + factor_re, + factor_im, + accumulate, + ); + #[cfg(target_arch = "aarch64")] + let mut point = apply_complex_scaled_aarch64( + destination_re, + destination_im, + source_re, + source_im, + factor_re, + factor_im, + accumulate, + ); + #[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))] + let mut point = 0; + if accumulate { + while point < destination_re.len() { + let re = source_re[point]; + let im = source_im[point]; + destination_re[point] += re * factor_re - im * factor_im; + destination_im[point] += re * factor_im + im * factor_re; + point += 1; + } + } else { + while point < destination_re.len() { + let re = source_re[point]; + let im = source_im[point]; + destination_re[point] = re * factor_re - im * factor_im; + destination_im[point] = re * factor_im + im * factor_re; + point += 1; + } + } +} + +#[cfg(target_arch = "x86_64")] +#[inline] +#[allow(clippy::too_many_arguments)] +fn apply_complex_scaled_x86_64( + destination_re: &mut [f64], + destination_im: &mut [f64], + source_re: &[f64], + source_im: &[f64], + factor_re: f64, + factor_im: f64, + accumulate: bool, +) -> usize { + let factor_re = f64x4::new([factor_re; 4]); + let factor_im = f64x4::new([factor_im; 4]); + let mut point = 0; + while destination_re.len() - point >= 4 { + let re = f64x4::new([ + source_re[point], + source_re[point + 1], + source_re[point + 2], + source_re[point + 3], + ]); + let im = f64x4::new([ + source_im[point], + source_im[point + 1], + source_im[point + 2], + source_im[point + 3], + ]); + let mut scaled_re = re * factor_re - im * factor_im; + let mut scaled_im = re * factor_im + im * factor_re; + if accumulate { + scaled_re += f64x4::new([ + destination_re[point], + destination_re[point + 1], + destination_re[point + 2], + destination_re[point + 3], + ]); + scaled_im += f64x4::new([ + destination_im[point], + destination_im[point + 1], + destination_im[point + 2], + destination_im[point + 3], + ]); + } + destination_re[point..point + 4].copy_from_slice(&scaled_re.to_array()); + destination_im[point..point + 4].copy_from_slice(&scaled_im.to_array()); + point += 4; + } + point +} + +#[cfg(target_arch = "aarch64")] +#[inline] +#[allow(clippy::too_many_arguments)] +fn apply_complex_scaled_aarch64( + destination_re: &mut [f64], + destination_im: &mut [f64], + source_re: &[f64], + source_im: &[f64], + factor_re: f64, + factor_im: f64, + accumulate: bool, +) -> usize { + let factor_re = f64x2::new([factor_re; 2]); + let factor_im = f64x2::new([factor_im; 2]); + let mut point = 0; + while destination_re.len() - point >= 2 { + let re = f64x2::new([source_re[point], source_re[point + 1]]); + let im = f64x2::new([source_im[point], source_im[point + 1]]); + let mut scaled_re = re * factor_re - im * factor_im; + let mut scaled_im = re * factor_im + im * factor_re; + if accumulate { + scaled_re += f64x2::new([destination_re[point], destination_re[point + 1]]); + scaled_im += f64x2::new([destination_im[point], destination_im[point + 1]]); + } + destination_re[point..point + 2].copy_from_slice(&scaled_re.to_array()); + destination_im[point..point + 2].copy_from_slice(&scaled_im.to_array()); + point += 2; + } + point +} + +fn exact_factor(row: &DirectRowRef<'_>, factors: DirectFactorView) -> RusticolResult<(f64, f64)> { + let index = row.exact_factor_id(); + if index >= factors.value_count || factors.values_re.is_null() || factors.values_im.is_null() { + return Err(RusticolError::integrity( + "recurrence exact factor is outside its split-complex catalog", + )); + } + Ok(unsafe { + ( + *factors.values_re.add(index as usize), + *factors.values_im.add(index as usize), + ) + }) +} + +fn resolve_scalar_value( + projection: &SymjitDirectScalarProjection, + row: &DirectRowRef<'_>, + parameters: DirectParameterView, + factors: DirectFactorView, +) -> RusticolResult { + match *projection { + SymjitDirectScalarProjection::Literal(value) => Ok(value), + SymjitDirectScalarProjection::Parameter { index, imaginary } => { + if index >= parameters.value_count { + return Err(RusticolError::integrity( + "recurrence model parameter is outside its split-complex catalog", + )); + } + let base = if imaginary { + parameters.values_im + } else { + parameters.values_re + }; + if base.is_null() { + return Err(RusticolError::integrity( + "recurrence model parameter catalog has a null pointer", + )); + } + Ok(unsafe { *base.add(index as usize) }) + } + SymjitDirectScalarProjection::ExactFactor { imaginary } => { + let (real, imag) = exact_factor(row, factors)?; + Ok(if imaginary { imag } else { real }) + } + } +} + +fn resolve_plane_pointer( + projection: SymjitDirectPlaneProjection, + row: &DirectRowRef<'_>, + arena: DirectArenaView, + momenta: DirectMomentumView, +) -> RusticolResult<*mut f64> { + let pointer = match projection { + SymjitDirectPlaneProjection::ParentCurrent { + parent, + component, + imaginary, + } => { + let base = row.parent_component_base(parent).ok_or_else(|| { + RusticolError::integrity("recurrence row has no requested parent") + })?; + arena_plane_pointer( + if imaginary { + arena.current_im + } else { + arena.current_re + }, + arena.current_scalar_len, + u64::from(base) + u64::from(component), + arena.point_stride, + ) + } + SymjitDirectPlaneProjection::Momentum { + operand, + lorentz_component, + } => { + let form = row.momentum_form_id(operand).ok_or_else(|| { + RusticolError::integrity("recurrence row has no requested momentum operand") + })?; + if form >= momenta.form_count || lorentz_component >= momenta.lorentz_component_count { + return Err(RusticolError::integrity( + "recurrence momentum projection is out of bounds", + )); + } + let plane = u64::from(form) + .checked_mul(u64::from(momenta.lorentz_component_count)) + .and_then(|base| base.checked_add(u64::from(lorentz_component))) + .ok_or_else(|| RusticolError::integrity("momentum plane index overflows"))?; + arena_plane_pointer( + momenta.values.cast_mut(), + momenta.scalar_len, + plane, + momenta.point_stride, + ) + } + SymjitDirectPlaneProjection::DestinationCurrent { + component, + imaginary, + } => { + let base = row.destination_current_base().ok_or_else(|| { + RusticolError::integrity("recurrence row has no current destination") + })?; + arena_plane_pointer( + if imaginary { + arena.current_im + } else { + arena.current_re + }, + arena.current_scalar_len, + u64::from(base) + u64::from(component), + arena.point_stride, + ) + } + SymjitDirectPlaneProjection::DestinationAmplitude { + component, + imaginary, + } => { + let base = row.destination_amplitude_base().ok_or_else(|| { + RusticolError::integrity("recurrence row has no amplitude destination") + })?; + arena_plane_pointer( + if imaginary { + arena.amplitude_im + } else { + arena.amplitude_re + }, + arena.amplitude_scalar_len, + u64::from(base) + u64::from(component), + arena.point_stride, + ) + } + }?; + Ok(pointer) +} + +fn arena_plane_pointer( + pointer: *mut f64, + scalar_len: u64, + plane: u64, + point_stride: u32, +) -> RusticolResult<*mut f64> { + if pointer.is_null() || point_stride == 0 { + return Err(RusticolError::integrity( + "recurrence plane storage has a null pointer or zero stride", + )); + } + let offset = plane + .checked_mul(u64::from(point_stride)) + .ok_or_else(|| RusticolError::integrity("recurrence plane offset overflows"))?; + let end = offset + .checked_add(u64::from(point_stride)) + .ok_or_else(|| RusticolError::integrity("recurrence plane range overflows"))?; + if end > scalar_len { + return Err(RusticolError::integrity( + "recurrence plane projection is outside its arena", + )); + } + let offset = usize::try_from(offset) + .map_err(|_| RusticolError::integrity("recurrence plane offset exceeds usize"))?; + Ok(unsafe { pointer.add(offset) }) +} + +#[cfg(test)] +pub(crate) mod tests { + use super::super::symjit_plane::compile_symbolica_program_to_plane_application_bytes; + use super::*; + use crate::recurrence::DirectSourceRow; + use crate::recurrence::on_the_fly::{ + OnTheFlyExecutorKeyV1, OnTheFlyPreparedExecutorResolver, OnTheFlyStructuralInterpreter, + OnTheFlyStructuralTraceV1, OnTheFlyWorkspaceV1, ResolvedOnTheFlyExecutor, + }; + use std::alloc::{GlobalAlloc, Layout, System}; + use std::collections::BTreeMap; + + thread_local! { + static TRACK_ALLOCATIONS: Cell = const { Cell::new(false) }; + static ALLOCATION_COUNT: Cell = const { Cell::new(0) }; + static ALLOCATED_BYTES: Cell = const { Cell::new(0) }; + } + + struct CountingAllocator; + + #[global_allocator] + static GLOBAL_ALLOCATOR: CountingAllocator = CountingAllocator; + + unsafe impl GlobalAlloc for CountingAllocator { + unsafe fn alloc(&self, layout: Layout) -> *mut u8 { + count_allocation(layout.size()); + unsafe { System.alloc(layout) } + } + + unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 { + count_allocation(layout.size()); + unsafe { System.alloc_zeroed(layout) } + } + + unsafe fn realloc(&self, pointer: *mut u8, layout: Layout, new_size: usize) -> *mut u8 { + count_allocation(new_size); + unsafe { System.realloc(pointer, layout, new_size) } + } + + unsafe fn dealloc(&self, pointer: *mut u8, layout: Layout) { + unsafe { System.dealloc(pointer, layout) } + } + } + + fn count_allocation(bytes: usize) { + let tracking = TRACK_ALLOCATIONS.try_with(Cell::get).unwrap_or(false); + if tracking { + let _ = ALLOCATION_COUNT.try_with(|count| count.set(count.get() + 1)); + let _ = ALLOCATED_BYTES.try_with(|total| total.set(total.get().saturating_add(bytes))); + } + } + + pub(crate) fn count_allocations(operation: impl FnOnce() -> T) -> (T, usize, usize) { + ALLOCATION_COUNT.with(|count| count.set(0)); + ALLOCATED_BYTES.with(|total| total.set(0)); + TRACK_ALLOCATIONS.with(|tracking| tracking.set(true)); + let result = operation(); + TRACK_ALLOCATIONS.with(|tracking| tracking.set(false)); + let count = ALLOCATION_COUNT.with(Cell::get); + let bytes = ALLOCATED_BYTES.with(Cell::get); + (result, count, bytes) + } + + const IDENTITY_PROGRAM: &str = "([('assign', ('out', 0), ('param', 0))], 1, [])"; + const DOUBLE_PROGRAM: &str = " +([('add', ('temp', 0), [('param', 0), ('param', 1)], 0), + ('assign', ('out', 0), ('temp', 0))], + 2, + []) +"; + const LENGTHS: &[usize] = &[1, 2, 3, 7, 8, 127, 128, 129, 1023, 1024, 1025]; + + fn identity_executor(role: DirectExecutorRole) -> LoadedSymjitDirectExecutor { + try_identity_executor(role, 2, false).unwrap() + } + + fn try_identity_executor( + role: DirectExecutorRole, + expected_optimization_level: u32, + expected_compression: bool, + ) -> RusticolResult { + try_identity_executor_with_abi( + role, + expected_optimization_level, + expected_compression, + SYMJIT_RECURRENCE_PLANE_APPLICATION_ABI, + ) + } + + fn try_identity_executor_with_abi( + role: DirectExecutorRole, + expected_optimization_level: u32, + expected_compression: bool, + application_abi: &str, + ) -> RusticolResult { + let bytes = + compile_symbolica_program_to_plane_application_bytes(IDENTITY_PROGRAM, 1, 1, 2, false) + .unwrap(); + let destination = match role { + DirectExecutorRole::Closure => [ + SymjitDirectPlaneProjection::DestinationAmplitude { + component: 0, + imaginary: false, + }, + SymjitDirectPlaneProjection::DestinationAmplitude { + component: 0, + imaginary: true, + }, + ], + _ => [ + SymjitDirectPlaneProjection::DestinationCurrent { + component: 0, + imaginary: false, + }, + SymjitDirectPlaneProjection::DestinationCurrent { + component: 0, + imaginary: true, + }, + ], + }; + LoadedSymjitDirectExecutor::load_prepared_application_bytes( + &bytes, + PathBuf::from("identity-recurrence-plane.symjit"), + application_abi, + expected_optimization_level, + expected_compression, + role, + vec![ + SymjitDirectParameterBinding::Plane { index: 0 }, + SymjitDirectParameterBinding::Plane { index: 1 }, + ], + vec![ + SymjitDirectPlaneProjection::ParentCurrent { + parent: 0, + component: 0, + imaginary: false, + }, + SymjitDirectPlaneProjection::ParentCurrent { + parent: 0, + component: 0, + imaginary: true, + }, + destination[0], + destination[1], + ], + vec![ + SymjitDirectScalarProjection::ExactFactor { imaginary: false }, + SymjitDirectScalarProjection::ExactFactor { imaginary: true }, + ], + vec![2, 3], + ) + } + + #[test] + fn predecessor_plane_application_abi_fails_closed_with_regeneration_message() { + let error = try_identity_executor_with_abi( + DirectExecutorRole::Contribution, + 2, + false, + "pyamplicol-symjit-plane-application-v1", + ) + .err() + .expect("the predecessor recurrence plane-application ABI must fail closed"); + assert_eq!(error.kind(), crate::RusticolErrorKind::Compatibility); + assert!(error.message().contains("regenerate")); + } + + #[test] + fn authenticated_template_optimization_level_mismatch_fails_closed() { + let error = try_identity_executor(DirectExecutorRole::Contribution, 3, false) + .err() + .expect("a template/application optimization mismatch must fail closed"); + assert!(error.message().contains("optimization level")); + assert!(error.message().contains("authenticated template")); + assert!(error.message().contains("regenerate")); + } + + #[test] + fn authenticated_manifest_compression_mismatch_fails_closed() { + let error = try_identity_executor(DirectExecutorRole::Contribution, 2, true) + .err() + .expect("a manifest/application compression mismatch must fail closed"); + assert!(error.message().contains("compression")); + assert!(error.message().contains("regenerate")); + } + + #[test] + fn writer_valid_large_plane_and_scalar_bindings_are_not_artificially_capped() { + const INPUT_COMPLEX_COUNT: usize = 257; + let program = + format!("([('assign', ('out', 0), ('param', 0))], {INPUT_COMPLEX_COUNT}, [])"); + let bytes = compile_symbolica_program_to_plane_application_bytes( + &program, + INPUT_COMPLEX_COUNT, + 1, + 2, + false, + ) + .unwrap(); + let mut parameter_bindings = Vec::new(); + let mut input_planes = Vec::new(); + for index in 0..(2 * INPUT_COMPLEX_COUNT) { + parameter_bindings.push(SymjitDirectParameterBinding::Plane { + index: index as u32, + }); + input_planes.push(SymjitDirectPlaneProjection::ParentCurrent { + parent: 0, + component: 0, + imaginary: index % 2 == 1, + }); + } + input_planes.extend([ + SymjitDirectPlaneProjection::DestinationCurrent { + component: 0, + imaginary: false, + }, + SymjitDirectPlaneProjection::DestinationCurrent { + component: 0, + imaginary: true, + }, + ]); + let mut scalars = vec![ + SymjitDirectScalarProjection::ExactFactor { imaginary: false }, + SymjitDirectScalarProjection::ExactFactor { imaginary: true }, + ]; + scalars.resize(257, SymjitDirectScalarProjection::Literal(0.0)); + + LoadedSymjitDirectExecutor::load_prepared_application_bytes( + &bytes, + PathBuf::from("large-recurrence-plane.symjit"), + SYMJIT_RECURRENCE_PLANE_APPLICATION_ABI, + 2, + false, + DirectExecutorRole::Contribution, + parameter_bindings, + input_planes, + scalars, + vec![514, 515], + ) + .unwrap(); + } + + #[test] + fn descriptor_cache_accounting_accepts_the_observed_303_megabyte_geometry() { + assert_eq!( + checked_descriptor_cache_bytes(153_624_576, 149_667_840).unwrap(), + 303_292_416 + ); + assert!(checked_descriptor_cache_bytes(usize::MAX, 1).is_err()); + } + + #[test] + fn callback_failure_reaches_the_direct_backend_with_concrete_context() { + let loaded = identity_executor(DirectExecutorRole::Contribution); + let DirectExecutorHandle::Contribution { call, context } = loaded.handle() else { + unreachable!() + }; + let mut current_re = vec![1.0; 2]; + let mut current_im = vec![0.0; 2]; + let mut amplitude_re = vec![0.0; 1]; + let mut amplitude_im = vec![0.0; 1]; + let (arena, momenta, parameters, factors) = views( + &mut current_re, + &mut current_im, + &mut amplitude_re, + &mut amplitude_im, + &[], + &[], + &[], + &[], + 1, + ); + let row = DirectContributionRow { + parent0_component_base: 0, + parent1_component_base_or_sentinel: DIRECT_NONE_U32, + parent0_momentum_form_id: 0, + parent1_momentum_form_id_or_sentinel: DIRECT_NONE_U32, + destination_component_base: 1, + exact_factor_id: 0, + selector_domain_id: 0, + flags: 0, + }; + + crate::recurrence::direct_backend::clear_direct_executor_error_detail(); + let status = unsafe { call(context, arena, momenta, parameters, factors, &row, 1, 1) }; + let error = crate::recurrence::direct_backend::check_status( + DirectExecutorRole::Contribution, + 312, + status, + ) + .unwrap_err(); + + assert_eq!(error.kind(), crate::RusticolErrorKind::Integrity); + assert!(error.message().contains("Contribution executor 312 failed")); + assert!(error.message().contains("exact factor")); + } + + fn parameter_broadcast_executor(role: DirectExecutorRole) -> LoadedSymjitDirectExecutor { + let bytes = + compile_symbolica_program_to_plane_application_bytes(IDENTITY_PROGRAM, 1, 1, 2, false) + .unwrap(); + let destination = match role { + DirectExecutorRole::Closure => [ + SymjitDirectPlaneProjection::DestinationAmplitude { + component: 0, + imaginary: false, + }, + SymjitDirectPlaneProjection::DestinationAmplitude { + component: 0, + imaginary: true, + }, + ], + _ => [ + SymjitDirectPlaneProjection::DestinationCurrent { + component: 0, + imaginary: false, + }, + SymjitDirectPlaneProjection::DestinationCurrent { + component: 0, + imaginary: true, + }, + ], + }; + LoadedSymjitDirectExecutor::load_prepared_application_bytes( + &bytes, + PathBuf::from("parameter-recurrence-plane.symjit"), + SYMJIT_RECURRENCE_PLANE_APPLICATION_ABI, + 2, + false, + role, + vec![ + SymjitDirectParameterBinding::Broadcast { index: 2 }, + SymjitDirectParameterBinding::Broadcast { index: 3 }, + ], + vec![destination[0], destination[1]], + vec![ + SymjitDirectScalarProjection::ExactFactor { imaginary: false }, + SymjitDirectScalarProjection::ExactFactor { imaginary: true }, + SymjitDirectScalarProjection::Parameter { + index: 0, + imaginary: false, + }, + SymjitDirectScalarProjection::Parameter { + index: 0, + imaginary: true, + }, + ], + vec![0, 1], + ) + .unwrap() + } + + fn repeated_parameter_and_zero_broadcast_executor() -> LoadedSymjitDirectExecutor { + let bytes = + compile_symbolica_program_to_plane_application_bytes(DOUBLE_PROGRAM, 2, 1, 2, false) + .unwrap(); + LoadedSymjitDirectExecutor::load_prepared_application_bytes( + &bytes, + PathBuf::from("shared-recurrence-broadcasts.symjit"), + SYMJIT_RECURRENCE_PLANE_APPLICATION_ABI, + 2, + false, + DirectExecutorRole::Contribution, + vec![ + SymjitDirectParameterBinding::Broadcast { index: 2 }, + SymjitDirectParameterBinding::Broadcast { index: 3 }, + SymjitDirectParameterBinding::Broadcast { index: 2 }, + SymjitDirectParameterBinding::Broadcast { index: 3 }, + ], + vec![ + SymjitDirectPlaneProjection::DestinationCurrent { + component: 0, + imaginary: false, + }, + SymjitDirectPlaneProjection::DestinationCurrent { + component: 0, + imaginary: true, + }, + ], + vec![ + SymjitDirectScalarProjection::ExactFactor { imaginary: false }, + SymjitDirectScalarProjection::ExactFactor { imaginary: true }, + SymjitDirectScalarProjection::Parameter { + index: 0, + imaginary: false, + }, + SymjitDirectScalarProjection::Literal(0.0), + ], + vec![0, 1], + ) + .unwrap() + } + + fn mixed_invariant_and_exact_broadcast_executor() -> LoadedSymjitDirectExecutor { + let bytes = + compile_symbolica_program_to_plane_application_bytes(DOUBLE_PROGRAM, 2, 1, 2, false) + .unwrap(); + LoadedSymjitDirectExecutor::load_prepared_application_bytes( + &bytes, + PathBuf::from("mixed-recurrence-broadcasts.symjit"), + SYMJIT_RECURRENCE_PLANE_APPLICATION_ABI, + 2, + false, + DirectExecutorRole::Contribution, + vec![ + SymjitDirectParameterBinding::Broadcast { index: 2 }, + SymjitDirectParameterBinding::Broadcast { index: 3 }, + SymjitDirectParameterBinding::Broadcast { index: 0 }, + SymjitDirectParameterBinding::Broadcast { index: 1 }, + ], + vec![ + SymjitDirectPlaneProjection::DestinationCurrent { + component: 0, + imaginary: false, + }, + SymjitDirectPlaneProjection::DestinationCurrent { + component: 0, + imaginary: true, + }, + ], + vec![ + SymjitDirectScalarProjection::ExactFactor { imaginary: false }, + SymjitDirectScalarProjection::ExactFactor { imaginary: true }, + SymjitDirectScalarProjection::Parameter { + index: 0, + imaginary: false, + }, + SymjitDirectScalarProjection::Parameter { + index: 0, + imaginary: true, + }, + ], + vec![0, 1], + ) + .unwrap() + } + + #[allow(clippy::too_many_arguments)] + fn views( + current_re: &mut [f64], + current_im: &mut [f64], + amplitude_re: &mut [f64], + amplitude_im: &mut [f64], + parameters_re: &[f64], + parameters_im: &[f64], + factors_re: &[f64], + factors_im: &[f64], + stride: usize, + ) -> ( + DirectArenaView, + DirectMomentumView, + DirectParameterView, + DirectFactorView, + ) { + ( + DirectArenaView { + current_re: current_re.as_mut_ptr(), + current_im: current_im.as_mut_ptr(), + current_scalar_len: current_re.len() as u64, + amplitude_re: amplitude_re.as_mut_ptr(), + amplitude_im: amplitude_im.as_mut_ptr(), + amplitude_scalar_len: amplitude_re.len() as u64, + point_stride: stride as u32, + }, + DirectMomentumView { + values: ptr::NonNull::::dangling().as_ptr(), + scalar_len: 0, + form_count: 0, + lorentz_component_count: 4, + point_stride: stride as u32, + }, + DirectParameterView { + values_re: parameters_re.as_ptr(), + values_im: parameters_im.as_ptr(), + value_count: parameters_re.len() as u32, + }, + DirectFactorView { + values_re: factors_re.as_ptr(), + values_im: factors_im.as_ptr(), + value_count: factors_re.len() as u32, + }, + ) + } + + #[test] + fn direct_output_classifier_requires_finalization_and_disjoint_planes() { + let mut inputs = vec![0.0; 32]; + let mut output_re = vec![0.0; 32]; + let mut output_im = vec![0.0; 32]; + let input_pointers = [inputs.as_mut_ptr()]; + let disjoint_outputs = [output_re.as_mut_ptr(), output_im.as_mut_ptr()]; + assert!( + direct_outputs_are_structurally_safe( + DirectExecutorRole::Finalization, + &input_pointers, + &disjoint_outputs, + 32, + ) + .unwrap() + ); + assert!( + !direct_outputs_are_structurally_safe( + DirectExecutorRole::Contribution, + &input_pointers, + &disjoint_outputs, + 32, + ) + .unwrap() + ); + assert!( + !direct_outputs_are_structurally_safe( + DirectExecutorRole::Finalization, + &input_pointers, + &[inputs.as_mut_ptr()], + 32, + ) + .unwrap() + ); + assert!( + !direct_outputs_are_structurally_safe( + DirectExecutorRole::Finalization, + &input_pointers, + &[output_re.as_mut_ptr(), output_re.as_mut_ptr()], + 32, + ) + .unwrap() + ); + } + + #[test] + fn contribution_scalar_simd_tails_apply_complex_scale_and_accumulate() { + let loaded = identity_executor(DirectExecutorRole::Contribution); + assert!(matches!(loaded.simd_lane_width(), 1 | 2 | 4)); + let DirectExecutorHandle::Contribution { call, context } = loaded.handle() else { + unreachable!() + }; + for &len in LENGTHS { + let mut current_re = vec![0.0; len * 2]; + let mut current_im = vec![0.0; len * 2]; + for point in 0..len { + current_re[point] = point as f64 + 0.25; + current_im[point] = 0.5 - point as f64 * 0.125; + current_re[len + point] = 7.0; + current_im[len + point] = -2.0; + } + let mut amplitude_re = vec![0.0; len]; + let mut amplitude_im = vec![0.0; len]; + let factors_re = [2.0]; + let factors_im = [-0.5]; + let (arena, momenta, parameters, factors) = views( + &mut current_re, + &mut current_im, + &mut amplitude_re, + &mut amplitude_im, + &[], + &[], + &factors_re, + &factors_im, + len, + ); + let row = DirectContributionRow { + parent0_component_base: 0, + parent1_component_base_or_sentinel: DIRECT_NONE_U32, + parent0_momentum_form_id: 0, + parent1_momentum_form_id_or_sentinel: DIRECT_NONE_U32, + destination_component_base: 1, + exact_factor_id: 0, + selector_domain_id: 0, + flags: 0, + }; + assert_eq!( + unsafe { + call( + context, arena, momenta, parameters, factors, &row, 1, len as u32, + ) + }, + DIRECT_STATUS_OK + ); + for point in 0..len { + let re = point as f64 + 0.25; + let im = 0.5 - point as f64 * 0.125; + assert_eq!(current_re[len + point], 7.0 + re * 2.0 + im * 0.5); + assert_eq!(current_im[len + point], -2.0 - re * 0.5 + im * 2.0); + } + } + } + + #[test] + fn initializing_contribution_overwrites_poisoned_complex_multipoint_destination() { + let loaded = identity_executor(DirectExecutorRole::Contribution); + let DirectExecutorHandle::Contribution { call, context } = loaded.handle() else { + unreachable!() + }; + for &len in &[3, 7, 129] { + let mut current_re = vec![f64::NAN; len * 2]; + let mut current_im = vec![f64::NAN; len * 2]; + for point in 0..len { + current_re[point] = point as f64 + 0.25; + current_im[point] = 0.5 - point as f64 * 0.125; + } + let mut amplitude_re = vec![0.0; len]; + let mut amplitude_im = vec![0.0; len]; + let factors_re = [2.0]; + let factors_im = [-0.5]; + let (arena, momenta, parameters, factors) = views( + &mut current_re, + &mut current_im, + &mut amplitude_re, + &mut amplitude_im, + &[], + &[], + &factors_re, + &factors_im, + len, + ); + let row = DirectContributionRow { + parent0_component_base: 0, + parent1_component_base_or_sentinel: DIRECT_NONE_U32, + parent0_momentum_form_id: 0, + parent1_momentum_form_id_or_sentinel: DIRECT_NONE_U32, + destination_component_base: 1, + exact_factor_id: 0, + selector_domain_id: 0, + flags: DIRECT_CONTRIBUTION_FLAG_INITIALIZE_DESTINATION, + }; + assert_eq!( + unsafe { + call( + context, arena, momenta, parameters, factors, &row, 1, len as u32, + ) + }, + DIRECT_STATUS_OK + ); + for point in 0..len { + let re = point as f64 + 0.25; + let im = 0.5 - point as f64 * 0.125; + assert_eq!(current_re[len + point], re * 2.0 + im * 0.5); + assert_eq!(current_im[len + point], -re * 0.5 + im * 2.0); + } + } + } + + #[test] + fn disjoint_identity_finalization_uses_direct_outputs_for_odd_tails() { + let loaded = parameter_broadcast_executor(DirectExecutorRole::Finalization); + assert!(matches!(loaded.simd_lane_width(), 1 | 2 | 4)); + let DirectExecutorHandle::Finalization { call, context } = loaded.handle() else { + unreachable!() + }; + let row = DirectFinalizationRow { + component_base: 0, + component_count: 1, + momentum_form_id: 0, + exact_factor_id: 0, + selector_domain_id: 0, + flags: 0, + }; + let parameters_re = [2.5]; + let parameters_im = [-0.75]; + let factors_re = [1.0]; + let factors_im = [0.0]; + for &len in &[127, 129, 1023, 1025] { + let mut current_re = vec![f64::NAN; len]; + let mut current_im = vec![f64::NAN; len]; + let mut amplitude_re = vec![0.0; len]; + let mut amplitude_im = vec![0.0; len]; + let (arena, momenta, parameters, factors) = views( + &mut current_re, + &mut current_im, + &mut amplitude_re, + &mut amplitude_im, + ¶meters_re, + ¶meters_im, + &factors_re, + &factors_im, + len, + ); + assert_eq!( + unsafe { + call( + context, arena, momenta, parameters, factors, &row, 1, len as u32, + ) + }, + DIRECT_STATUS_OK + ); + assert_eq!(loaded.cached_direct_table_count(), 1); + assert!(current_re.iter().all(|&value| value == parameters_re[0])); + assert!(current_im.iter().all(|&value| value == parameters_im[0])); + + current_re.fill(f64::NAN); + current_im.fill(f64::NAN); + let (status, allocations, bytes) = count_allocations(|| unsafe { + call( + context, arena, momenta, parameters, factors, &row, 1, len as u32, + ) + }); + assert_eq!(status, DIRECT_STATUS_OK); + assert_eq!((allocations, bytes), (0, 0)); + assert!(current_re.iter().all(|&value| value == parameters_re[0])); + assert!(current_im.iter().all(|&value| value == parameters_im[0])); + } + assert_eq!(loaded.internal_traffic_bytes().0, 0); + } + + #[test] + fn scalar_and_tiled_row_workspaces_remain_warm_independently() { + let loaded = parameter_broadcast_executor(DirectExecutorRole::Finalization); + let DirectExecutorHandle::Finalization { call, context } = loaded.handle() else { + unreachable!() + }; + let row = DirectFinalizationRow { + component_base: 0, + component_count: 1, + momentum_form_id: 0, + exact_factor_id: 0, + selector_domain_id: 0, + flags: 0, + }; + let parameters_re = [2.5]; + let parameters_im = [-0.75]; + let factors_re = [1.0]; + let factors_im = [0.0]; + let mut scalar_re = [f64::NAN; 1]; + let mut scalar_im = [f64::NAN; 1]; + let mut scalar_amplitude_re = [0.0; 1]; + let mut scalar_amplitude_im = [0.0; 1]; + let scalar = views( + &mut scalar_re, + &mut scalar_im, + &mut scalar_amplitude_re, + &mut scalar_amplitude_im, + ¶meters_re, + ¶meters_im, + &factors_re, + &factors_im, + 1, + ); + let mut tiled_re = [f64::NAN; 8]; + let mut tiled_im = [f64::NAN; 8]; + let mut tiled_amplitude_re = [0.0; 8]; + let mut tiled_amplitude_im = [0.0; 8]; + let tiled = views( + &mut tiled_re, + &mut tiled_im, + &mut tiled_amplitude_re, + &mut tiled_amplitude_im, + ¶meters_re, + ¶meters_im, + &factors_re, + &factors_im, + 8, + ); + for (views, points) in [(scalar, 1), (tiled, 8)] { + assert_eq!( + unsafe { call(context, views.0, views.1, views.2, views.3, &row, 1, points,) }, + DIRECT_STATUS_OK + ); + } + let (status, allocations, bytes) = count_allocations(|| unsafe { + call(context, scalar.0, scalar.1, scalar.2, scalar.3, &row, 1, 1) + }); + assert_eq!(status, DIRECT_STATUS_OK); + assert_eq!((allocations, bytes), (0, 0)); + assert_eq!(loaded.context.scalar_workspace.borrow().row_groups.len(), 1); + assert_eq!(loaded.context.tiled_workspace.borrow().row_groups.len(), 1); + } + + #[test] + fn disjoint_nonidentity_finalization_uses_scratch_and_tracks_traffic() { + let loaded = parameter_broadcast_executor(DirectExecutorRole::Finalization); + let DirectExecutorHandle::Finalization { call, context } = loaded.handle() else { + unreachable!() + }; + let len = 129; + let mut current_re = vec![f64::NAN; len]; + let mut current_im = vec![f64::NAN; len]; + let mut amplitude_re = vec![0.0; len]; + let mut amplitude_im = vec![0.0; len]; + let parameters_re = [2.5]; + let parameters_im = [-0.75]; + let factors_re = [0.5]; + let factors_im = [-0.25]; + let (arena, momenta, parameters, factors) = views( + &mut current_re, + &mut current_im, + &mut amplitude_re, + &mut amplitude_im, + ¶meters_re, + ¶meters_im, + &factors_re, + &factors_im, + len, + ); + let row = DirectFinalizationRow { + component_base: 0, + component_count: 1, + momentum_form_id: 0, + exact_factor_id: 0, + selector_domain_id: 0, + flags: 0, + }; + assert_eq!( + unsafe { + call( + context, arena, momenta, parameters, factors, &row, 1, len as u32, + ) + }, + DIRECT_STATUS_OK + ); + assert_eq!(loaded.cached_direct_table_count(), 1); + let expected_re = parameters_re[0] * factors_re[0] - parameters_im[0] * factors_im[0]; + let expected_im = parameters_re[0] * factors_im[0] + parameters_im[0] * factors_re[0]; + assert!(current_re.iter().all(|&value| value == expected_re)); + assert!(current_im.iter().all(|&value| value == expected_im)); + let scratch_one_way = internal_plane_bytes(2, len, "test scratch traffic").unwrap(); + assert_eq!( + loaded.internal_traffic_bytes().0, + scratch_one_way.saturating_mul(2) + ); + } + + #[test] + fn aliased_identity_finalization_stays_on_scratch() { + let loaded = identity_executor(DirectExecutorRole::Finalization); + let DirectExecutorHandle::Finalization { call, context } = loaded.handle() else { + unreachable!() + }; + let len = 129; + let mut current_re = (0..len).map(|point| point as f64 + 1.0).collect::>(); + let mut current_im = (0..len).map(|point| 3.0 - point as f64).collect::>(); + let expected_re = current_re.clone(); + let expected_im = current_im.clone(); + let mut amplitude_re = vec![0.0; len]; + let mut amplitude_im = vec![0.0; len]; + let factors_re = [1.0]; + let factors_im = [0.0]; + let (arena, momenta, parameters, factors) = views( + &mut current_re, + &mut current_im, + &mut amplitude_re, + &mut amplitude_im, + &[], + &[], + &factors_re, + &factors_im, + len, + ); + let row = DirectFinalizationRow { + component_base: 0, + component_count: 1, + momentum_form_id: 0, + exact_factor_id: 0, + selector_domain_id: 0, + flags: 0, + }; + assert_eq!( + unsafe { + call( + context, arena, momenta, parameters, factors, &row, 1, len as u32, + ) + }, + DIRECT_STATUS_OK + ); + assert_eq!(loaded.cached_direct_table_count(), 0); + assert_eq!(current_re, expected_re); + assert_eq!(current_im, expected_im); + let scratch_one_way = internal_plane_bytes(2, len, "test scratch traffic").unwrap(); + assert_eq!( + loaded.internal_traffic_bytes().0, + scratch_one_way.saturating_mul(2) + ); + } + + #[test] + fn finalization_snapshots_input_before_overwrite() { + let loaded = identity_executor(DirectExecutorRole::Finalization); + let DirectExecutorHandle::Finalization { call, context } = loaded.handle() else { + unreachable!() + }; + let len = 129; + let mut current_re = (0..len).map(|point| point as f64 + 1.0).collect::>(); + let mut current_im = (0..len).map(|point| 3.0 - point as f64).collect::>(); + let original_re = current_re.clone(); + let original_im = current_im.clone(); + let mut amplitude_re = vec![0.0; len]; + let mut amplitude_im = vec![0.0; len]; + let factors_re = [0.25]; + let factors_im = [0.75]; + let (arena, momenta, parameters, factors) = views( + &mut current_re, + &mut current_im, + &mut amplitude_re, + &mut amplitude_im, + &[], + &[], + &factors_re, + &factors_im, + len, + ); + let row = DirectFinalizationRow { + component_base: 0, + component_count: 1, + momentum_form_id: 0, + exact_factor_id: 0, + selector_domain_id: 0, + flags: 0, + }; + assert_eq!( + unsafe { + call( + context, arena, momenta, parameters, factors, &row, 1, len as u32, + ) + }, + DIRECT_STATUS_OK + ); + for point in 0..len { + assert_eq!( + current_re[point], + original_re[point] * 0.25 - original_im[point] * 0.75 + ); + assert_eq!( + current_im[point], + original_re[point] * 0.75 + original_im[point] * 0.25 + ); + } + } + + #[test] + fn row_group_cache_preserves_exact_identity_across_lookup_modes() { + let mut workspace = PlaneWorkspace::default(); + let mut identities = vec![ + RowTableIdentity { + role: DirectExecutorRole::Contribution, + address: 100, + row_count: 1, + }, + RowTableIdentity { + role: DirectExecutorRole::Contribution, + address: 100, + row_count: 2, + }, + RowTableIdentity { + role: DirectExecutorRole::Closure, + address: 100, + row_count: 1, + }, + RowTableIdentity { + role: DirectExecutorRole::Contribution, + address: 101, + row_count: 1, + }, + ]; + identities.extend((102..230).map(|address| RowTableIdentity { + role: DirectExecutorRole::Finalization, + address, + row_count: 1, + })); + for (index, identity) in identities.iter().copied().enumerate() { + assert_eq!(workspace.cached_row_group_index(identity), None); + assert_eq!( + workspace + .cache_row_group(CachedRowGroup { + identity, + rows: Vec::new(), + descriptor_bytes: 0, + }) + .unwrap(), + index + ); + assert_eq!( + workspace.row_group_indices.is_empty(), + index < ROW_GROUP_LINEAR_LOOKUP_LIMIT + ); + for (expected_index, cached_identity) in identities[..=index].iter().enumerate() { + assert_eq!( + workspace.cached_row_group_index(*cached_identity), + Some(expected_index) + ); + } + } + let (_, allocations, _) = count_allocations(|| { + for (index, identity) in identities.iter().enumerate().rev() { + assert_eq!(workspace.cached_row_group_index(*identity), Some(index)); + } + }); + assert_eq!(allocations, 0); + } + + #[test] + fn row_group_cache_invalidation_restarts_small_lookup() { + let mut workspace = PlaneWorkspace::default(); + for address in 0..32 { + workspace + .cache_row_group(CachedRowGroup { + identity: RowTableIdentity { + role: DirectExecutorRole::Contribution, + address, + row_count: 1, + }, + rows: Vec::new(), + descriptor_bytes: 1, + }) + .unwrap(); + } + let old_identity = workspace.row_groups[31].identity; + assert_eq!(workspace.cached_row_group_index(old_identity), Some(31)); + workspace.invalidate_row_tables(); + assert!(workspace.row_groups.is_empty()); + assert!(workspace.row_group_indices.is_empty()); + assert_eq!(workspace.descriptor_bytes, 0); + assert_eq!(workspace.cached_row_group_index(old_identity), None); + assert_eq!( + workspace + .cache_row_group(CachedRowGroup { + identity: old_identity, + rows: Vec::new(), + descriptor_bytes: 1, + }) + .unwrap(), + 0 + ); + assert!(workspace.row_group_indices.is_empty()); + assert_eq!(workspace.cached_row_group_index(old_identity), Some(0)); + } + + #[test] + fn cached_row_group_reuses_one_scratch_set_sequentially() { + let loaded = identity_executor(DirectExecutorRole::Contribution); + let DirectExecutorHandle::Contribution { call, context } = loaded.handle() else { + unreachable!() + }; + let len = 7; + let mut current_re = vec![0.0; len * 4]; + let mut current_im = vec![0.0; len * 4]; + for point in 0..len { + current_re[point] = point as f64 + 1.0; + current_im[point] = 0.25; + current_re[2 * len + point] = 10.0 + point as f64; + current_im[2 * len + point] = -0.5; + } + let mut amplitude_re = vec![0.0; len]; + let mut amplitude_im = vec![0.0; len]; + let factors_re = [1.0]; + let factors_im = [0.0]; + let (arena, momenta, parameters, factors) = views( + &mut current_re, + &mut current_im, + &mut amplitude_re, + &mut amplitude_im, + &[], + &[], + &factors_re, + &factors_im, + len, + ); + let rows = [ + DirectContributionRow { + parent0_component_base: 0, + parent1_component_base_or_sentinel: DIRECT_NONE_U32, + parent0_momentum_form_id: 0, + parent1_momentum_form_id_or_sentinel: DIRECT_NONE_U32, + destination_component_base: 1, + exact_factor_id: 0, + selector_domain_id: 0, + flags: 0, + }, + DirectContributionRow { + parent0_component_base: 2, + parent1_component_base_or_sentinel: DIRECT_NONE_U32, + parent0_momentum_form_id: 0, + parent1_momentum_form_id_or_sentinel: DIRECT_NONE_U32, + destination_component_base: 3, + exact_factor_id: 0, + selector_domain_id: 0, + flags: 0, + }, + ]; + assert_eq!( + unsafe { + call( + context, + arena, + momenta, + parameters, + factors, + rows.as_ptr(), + rows.len() as u32, + len as u32, + ) + }, + DIRECT_STATUS_OK + ); + for point in 0..len { + assert_eq!(current_re[len + point], point as f64 + 1.0); + assert_eq!(current_im[len + point], 0.25); + assert_eq!(current_re[3 * len + point], 10.0 + point as f64); + assert_eq!(current_im[3 * len + point], -0.5); + } + // A selected dependency run can share the same first row as a longer + // selection. Pointer equality alone must not reject the second table. + for row_count in [1, 2, 1] { + assert_eq!( + unsafe { + call( + context, + arena, + momenta, + parameters, + factors, + rows.as_ptr(), + row_count, + len as u32, + ) + }, + DIRECT_STATUS_OK + ); + } + assert_eq!(loaded.context.tiled_workspace.borrow().row_groups.len(), 2); + } + + unsafe extern "C" fn initialize_stable_row_probe_sources( + _context: *const c_void, + arena: DirectArenaView, + _momenta: DirectMomentumView, + _parameters: DirectParameterView, + _factors: DirectFactorView, + rows: *const DirectSourceRow, + row_count: u32, + point_count: u32, + ) -> c_int { + if rows.is_null() || row_count == 0 { + return STATUS_INVALID_ARGUMENT; + } + let rows = unsafe { std::slice::from_raw_parts(rows, row_count as usize) }; + for row in rows { + for point in 0..point_count { + let destination = row.destination_component_base as usize + * arena.point_stride as usize + + point as usize; + unsafe { + *arena.current_re.add(destination) = + f64::from(row.destination_component_base + 1); + *arena.current_im.add(destination) = 0.0; + } + } + } + DIRECT_STATUS_OK + } + + struct StableRowProbeResolver { + source_key: OnTheFlyExecutorKeyV1, + contribution_key: OnTheFlyExecutorKeyV1, + contribution_handle: DirectExecutorHandle, + } + + impl OnTheFlyPreparedExecutorResolver for StableRowProbeResolver { + fn resolve(&self, key: OnTheFlyExecutorKeyV1) -> RusticolResult { + if key == self.source_key { + return Ok(ResolvedOnTheFlyExecutor { + direct_executor_id: 0, + handle: DirectExecutorHandle::Source { + call: initialize_stable_row_probe_sources, + context: ptr::null(), + }, + parent_permutation: [0, 1], + packed_singleton_capable: false, + interaction_capability: None, + }); + } + if key == self.contribution_key { + return Ok(ResolvedOnTheFlyExecutor { + direct_executor_id: 1, + handle: self.contribution_handle, + parent_permutation: [1, 0], + packed_singleton_capable: false, + interaction_capability: None, + }); + } + Err(RusticolError::integrity( + "stable-row probe received an unknown executor key", + )) + } + + fn invalidate_row_tables(&self) -> RusticolResult<()> { + Ok(()) + } + } + + #[test] + fn on_the_fly_uses_stable_permuted_rows_across_symjit_warm_reuse() { + let loaded = identity_executor(DirectExecutorRole::Contribution); + let (mut trace, source_key, contribution_key) = + OnTheFlyStructuralTraceV1::test_two_contribution_rows(); + let semantic_digest = trace.semantic_digest(); + let parent_permutations = + BTreeMap::from([(source_key, [0, 1]), (contribution_key, [1, 0])]); + trace + .bind_prepared_executor_rows(&parent_permutations) + .unwrap(); + assert_eq!(trace.semantic_digest(), semantic_digest); + assert!( + trace + .bind_prepared_executor_rows(&parent_permutations) + .is_err() + ); + let rows = trace.test_bound_contribution_rows(); + assert_eq!( + rows.iter() + .map(|row| { + ( + row.parent0_component_base, + row.parent1_component_base_or_sentinel, + row.parent0_momentum_form_id, + row.parent1_momentum_form_id_or_sentinel, + row.destination_component_base, + ) + }) + .collect::>(), + vec![(1, 0, 1, 0, 2), (3, 0, 3, 0, 4)] + ); + + let resolver = StableRowProbeResolver { + source_key, + contribution_key, + contribution_handle: loaded.handle(), + }; + let mut workspace = OnTheFlyWorkspaceV1::new(&trace, 1).unwrap(); + for _ in 0..2 { + OnTheFlyStructuralInterpreter::execute(&trace, &resolver, &mut workspace, 1).unwrap(); + assert_eq!( + workspace.observed_current_components(&trace, 2, 0).unwrap(), + vec![(2.0, 0.0)] + ); + assert_eq!( + workspace.observed_current_components(&trace, 4, 0).unwrap(), + vec![(4.0, 0.0)] + ); + } + // The legacy OTF interpreter keeps an aligned arena stride even for a + // one-point call, so its tables belong to the tiled cache. The newer + // packed-singleton path uses the independent scalar cache. + assert_eq!(loaded.context.tiled_workspace.borrow().row_groups.len(), 2); + loaded.invalidate_row_tables().unwrap(); + for workspace in [ + &loaded.context.scalar_workspace, + &loaded.context.tiled_workspace, + ] { + let workspace = workspace.borrow(); + assert!(workspace.row_groups.is_empty()); + assert!(workspace.row_group_indices.is_empty()); + assert!(workspace.storage.is_none()); + assert_eq!(workspace.descriptor_bytes, 0); + } + } + + #[test] + fn model_parameter_broadcast_planes_refresh_without_reloading_kernel() { + let loaded = parameter_broadcast_executor(DirectExecutorRole::Contribution); + let DirectExecutorHandle::Contribution { call, context } = loaded.handle() else { + unreachable!() + }; + let len = 17; + let mut current_re = vec![0.0; len]; + let mut current_im = vec![0.0; len]; + let mut amplitude_re = vec![0.0; len]; + let mut amplitude_im = vec![0.0; len]; + let factors_re = [1.0]; + let factors_im = [0.0]; + let row = DirectContributionRow { + parent0_component_base: 0, + parent1_component_base_or_sentinel: DIRECT_NONE_U32, + parent0_momentum_form_id: 0, + parent1_momentum_form_id_or_sentinel: DIRECT_NONE_U32, + destination_component_base: 0, + exact_factor_id: 0, + selector_domain_id: 0, + flags: 0, + }; + let mut expected_re = 0.0; + let mut expected_im = 0.0; + for (iteration, (parameter_re, parameter_im)) in [(2.5, -0.75), (-4.0, 1.25), (-4.0, 1.25)] + .into_iter() + .enumerate() + { + expected_re += parameter_re; + expected_im += parameter_im; + let parameters_re = [parameter_re]; + let parameters_im = [parameter_im]; + let (arena, momenta, parameters, factors) = views( + &mut current_re, + &mut current_im, + &mut amplitude_re, + &mut amplitude_im, + ¶meters_re, + ¶meters_im, + &factors_re, + &factors_im, + len, + ); + let invoke = || unsafe { + call( + context, arena, momenta, parameters, factors, &row, 1, len as u32, + ) + }; + let status = if iteration == 0 { + invoke() + } else { + let (status, allocations, bytes) = count_allocations(invoke); + assert_eq!((allocations, bytes), (0, 0)); + status + }; + assert_eq!(status, DIRECT_STATUS_OK); + for point in 0..len { + assert_eq!(current_re[point], expected_re); + assert_eq!(current_im[point], expected_im); + } + } + let active_plane_bytes = 2 * len as u64 * size_of::() as u64; + assert_eq!( + loaded.internal_traffic_bytes(), + ( + 3 * 2 * active_plane_bytes, + 3 * active_plane_bytes + 2 * active_plane_bytes, + ) + ); + } + + #[test] + fn migration_contract_interns_repeated_parameter_and_zero_broadcasts() { + let loaded = repeated_parameter_and_zero_broadcast_executor(); + assert_eq!(loaded.broadcast_plane_count(), 2); + assert_eq!( + loaded.context.broadcast_slot_by_binding.as_ref(), + [Some(0), Some(1), Some(0), Some(1)] + ); + let DirectExecutorHandle::Contribution { call, context } = loaded.handle() else { + unreachable!() + }; + let len = 129; + let mut current_re = vec![1.0; len]; + let mut current_im = vec![-2.0; len]; + let mut amplitude_re = vec![0.0; len]; + let mut amplitude_im = vec![0.0; len]; + let factors_re = [1.0]; + let factors_im = [0.0]; + let row = DirectContributionRow { + parent0_component_base: 0, + parent1_component_base_or_sentinel: DIRECT_NONE_U32, + parent0_momentum_form_id: 0, + parent1_momentum_form_id_or_sentinel: DIRECT_NONE_U32, + destination_component_base: 0, + exact_factor_id: 0, + selector_domain_id: 0, + flags: 0, + }; + let mut expected_re = 1.0; + for (iteration, parameter_re) in [2.5, 2.5, -4.0].into_iter().enumerate() { + expected_re += 2.0 * parameter_re; + let parameters_re = [parameter_re]; + let parameters_im = [123.0]; + let (arena, momenta, parameters, factors) = views( + &mut current_re, + &mut current_im, + &mut amplitude_re, + &mut amplitude_im, + ¶meters_re, + ¶meters_im, + &factors_re, + &factors_im, + len, + ); + let invoke = || unsafe { + call( + context, arena, momenta, parameters, factors, &row, 1, len as u32, + ) + }; + let status = if iteration == 0 { + invoke() + } else { + let (status, allocations, bytes) = count_allocations(invoke); + assert_eq!((allocations, bytes), (0, 0)); + status + }; + assert_eq!(status, DIRECT_STATUS_OK); + assert!(current_re.iter().all(|&value| value == expected_re)); + assert!(current_im.iter().all(|&value| value == -2.0)); + } + assert_eq!( + loaded.context.tiled_workspace.borrow().broadcasts.len(), + 2 * len + ); + let plane_bytes = len as u64 * size_of::() as u64; + assert_eq!( + loaded.internal_traffic_bytes(), + ( + 3 * 4 * plane_bytes, + // Two shared planes are read on each call. Both are filled + // once, and only the parameter plane changes on the last call. + 3 * 2 * plane_bytes + 3 * plane_bytes, + ) + ); + } + + #[test] + fn multirow_call_refreshes_invariants_once_and_exact_factors_per_row() { + let loaded = mixed_invariant_and_exact_broadcast_executor(); + assert_eq!(loaded.broadcast_plane_count(), 4); + assert_eq!(loaded.context.exact_factor_broadcast_slots.as_ref(), [2, 3]); + let DirectExecutorHandle::Contribution { call, context } = loaded.handle() else { + unreachable!() + }; + let len = 17; + let mut current_re = vec![0.0; 2 * len]; + let mut current_im = vec![0.0; 2 * len]; + let mut amplitude_re = vec![0.0; len]; + let mut amplitude_im = vec![0.0; len]; + let factors_re = [2.0, -1.0]; + let factors_im = [-0.25, 0.75]; + let first_row = DirectContributionRow { + parent0_component_base: 0, + parent1_component_base_or_sentinel: DIRECT_NONE_U32, + parent0_momentum_form_id: 0, + parent1_momentum_form_id_or_sentinel: DIRECT_NONE_U32, + destination_component_base: 0, + exact_factor_id: 0, + selector_domain_id: 0, + flags: 0, + }; + let rows = [ + first_row, + DirectContributionRow { + destination_component_base: 1, + exact_factor_id: 1, + ..first_row + }, + ]; + let expected_increment = |parameter_re: f64, parameter_im: f64, factor: usize| { + let kernel_re = parameter_re + factors_re[factor]; + let kernel_im = parameter_im + factors_im[factor]; + ( + kernel_re * factors_re[factor] - kernel_im * factors_im[factor], + kernel_re * factors_im[factor] + kernel_im * factors_re[factor], + ) + }; + let invoke = |parameter_re: f64, + parameter_im: f64, + current_re: &mut [f64], + current_im: &mut [f64], + amplitude_re: &mut [f64], + amplitude_im: &mut [f64]| { + let parameters_re = [parameter_re]; + let parameters_im = [parameter_im]; + let (arena, momenta, parameters, factors) = views( + current_re, + current_im, + amplitude_re, + amplitude_im, + ¶meters_re, + ¶meters_im, + &factors_re, + &factors_im, + len, + ); + unsafe { + call( + context, + arena, + momenta, + parameters, + factors, + rows.as_ptr(), + rows.len() as u32, + len as u32, + ) + } + }; + + assert_eq!( + invoke( + 3.0, + 0.5, + &mut current_re, + &mut current_im, + &mut amplitude_re, + &mut amplitude_im, + ), + DIRECT_STATUS_OK + ); + assert_eq!(loaded.broadcast_resolution_counts(), (2, 4)); + for factor in 0..2 { + let (expected_re, expected_im) = expected_increment(3.0, 0.5, factor); + assert!( + current_re[factor * len..(factor + 1) * len] + .iter() + .all(|&value| value == expected_re) + ); + assert!( + current_im[factor * len..(factor + 1) * len] + .iter() + .all(|&value| value == expected_im) + ); + } + + let (status, allocations, bytes) = count_allocations(|| { + invoke( + 3.0, + 0.5, + &mut current_re, + &mut current_im, + &mut amplitude_re, + &mut amplitude_im, + ) + }); + assert_eq!(status, DIRECT_STATUS_OK); + assert_eq!((allocations, bytes), (0, 0)); + assert_eq!(loaded.broadcast_resolution_counts(), (4, 8)); + + assert_eq!( + invoke( + -4.0, + 1.25, + &mut current_re, + &mut current_im, + &mut amplitude_re, + &mut amplitude_im, + ), + DIRECT_STATUS_OK + ); + assert_eq!(loaded.broadcast_resolution_counts(), (6, 12)); + for factor in 0..2 { + let (first_re, first_im) = expected_increment(3.0, 0.5, factor); + let (mutated_re, mutated_im) = expected_increment(-4.0, 1.25, factor); + assert!( + current_re[factor * len..(factor + 1) * len] + .iter() + .all(|&value| value == 2.0 * first_re + mutated_re) + ); + assert!( + current_im[factor * len..(factor + 1) * len] + .iter() + .all(|&value| value == 2.0 * first_im + mutated_im) + ); + } + + let plane_bytes = len as u64 * size_of::() as u64; + assert_eq!( + loaded.internal_traffic_bytes(), + ( + // Two scratch output planes are written and read for each of + // two rows on every call. + 3 * 2 * 2 * 2 * plane_bytes, + // Four broadcast planes are read per row. Refreshes are 6 on + // the cold call, 4 on the identical warm call, then 6 after + // both model-parameter planes mutate. + (3 * 2 * 4 + 6 + 4 + 6) * plane_bytes, + ) + ); + } + + #[test] + fn warmed_recurrence_plane_call_allocates_nothing() { + let loaded = identity_executor(DirectExecutorRole::Contribution); + let DirectExecutorHandle::Contribution { call, context } = loaded.handle() else { + unreachable!() + }; + let mut expected_scratch_bytes = 0; + for &len in &[127, 129, 1023, 1025] { + let mut current_re = vec![1.0; len * 2]; + let mut current_im = vec![0.5; len * 2]; + let mut amplitude_re = vec![0.0; len]; + let mut amplitude_im = vec![0.0; len]; + let factors_re = [1.0]; + let factors_im = [0.0]; + let (arena, momenta, parameters, factors) = views( + &mut current_re, + &mut current_im, + &mut amplitude_re, + &mut amplitude_im, + &[], + &[], + &factors_re, + &factors_im, + len, + ); + let row = DirectContributionRow { + parent0_component_base: 0, + parent1_component_base_or_sentinel: DIRECT_NONE_U32, + parent0_momentum_form_id: 0, + parent1_momentum_form_id_or_sentinel: DIRECT_NONE_U32, + destination_component_base: 1, + exact_factor_id: 0, + selector_domain_id: 0, + flags: 0, + }; + assert_eq!( + unsafe { + call( + context, arena, momenta, parameters, factors, &row, 1, len as u32, + ) + }, + DIRECT_STATUS_OK + ); + let (status, allocations, bytes) = count_allocations(|| unsafe { + call( + context, arena, momenta, parameters, factors, &row, 1, len as u32, + ) + }); + assert_eq!(status, DIRECT_STATUS_OK); + assert_eq!( + (allocations, bytes), + (0, 0), + "warmed recurrence call allocated for {len} points" + ); + let scratch_one_way = internal_plane_bytes(2, len, "test scratch traffic").unwrap(); + expected_scratch_bytes += scratch_one_way.saturating_mul(4); + } + assert_eq!(loaded.cached_direct_table_count(), 0); + assert_eq!(loaded.internal_traffic_bytes().0, expected_scratch_bytes); + } +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/evaluator/symjit_eager_direct.rs b/pyamplicol/rust/crates/rusticol-core/src/evaluator/symjit_eager_direct.rs new file mode 100644 index 0000000000..bbef7136d5 --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/evaluator/symjit_eager_direct.rs @@ -0,0 +1,2234 @@ +// SPDX-License-Identifier: 0BSD + +//! Eager-lane orchestration for standard SymJIT direct-arena P-kernels. +//! +//! SymJIT owns only the ordinary plane kernel. Rusticol authenticates and +//! decodes immutable invocation/attachment rows, preserves their order and +//! hazards, and implements factors, overwrite/add, and fanout. A single safe +//! identity overwrite binds outputs directly; every other invocation writes +//! once to persistent split-complex scratch and fans out in allocation-free +//! Rust loops. Native C++/ASM table callables retain their public C layout. + +use super::symjit_plane::{PlaneDescriptor, SymjitPlaneKernel, SymjitPlaneLayout}; +use crate::artifact::PinnedNativeLibrary; +use crate::direct_arena::{DirectArenaView, DirectArenaWorkspace}; +pub(crate) use crate::eager_layout::{ + EAGER_DIRECT_SOURCE_APPLICATION_ABI, EAGER_DIRECT_TABLE_BINDING_ABI, + EAGER_DIRECT_TABLE_DESCRIPTOR_ABI, EAGER_NATIVE_DIRECT_TABLE_APPLICATION_ABI, +}; +use crate::{RusticolError, RusticolResult}; +use sha2::{Digest, Sha256}; +use std::any::Any; +#[cfg(feature = "f64-compiled")] +use std::ffi::CStr; +#[cfg(feature = "f64-compiled")] +use std::os::raw::c_char; +use std::panic::{AssertUnwindSafe, catch_unwind}; +use std::path::{Path, PathBuf}; +use std::ptr; +use std::sync::Arc; +#[cfg(target_arch = "aarch64")] +use wide::f64x2; +#[cfg(target_arch = "x86_64")] +use wide::f64x4; + +const EAGER_PLANE_DESCRIPTOR_MAGIC: [u8; 8] = *b"PACETD01"; +const EAGER_PLANE_DESCRIPTOR_VERSION: u32 = 1; +const EAGER_PLANE_DESCRIPTOR_SIZE: usize = 64; +const EAGER_DESTINATION_OVERWRITE: u32 = 0; +const EAGER_DESTINATION_ACCUMULATE: u32 = 1; +const DIRECT_STATUS_OK: i32 = 0; +const DIRECT_STATUS_INVALID_CONTEXT: i32 = 1; +const DIRECT_STATUS_INVALID_ARGUMENT: i32 = 2; +const DIRECT_STATUS_EXECUTION_FAILED: i32 = 3; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +struct EagerDirectTableMetadata { + input_complex_count: u32, + output_complex_count: u32, + input_plane_count: usize, + output_plane_count: usize, + invocation_stride: u32, + attachment_stride: u32, +} + +impl EagerDirectTableMetadata { + fn new(input_complex_count: u32, output_complex_count: u32) -> RusticolResult { + if input_complex_count == 0 || output_complex_count == 0 { + return Err(RusticolError::invalid_argument( + "eager direct-table input and output widths must be positive", + )); + } + let input_plane_count_u32 = input_complex_count.checked_mul(2).ok_or_else(|| { + RusticolError::invalid_argument("eager direct-table input width overflows") + })?; + let output_plane_count_u32 = output_complex_count.checked_mul(2).ok_or_else(|| { + RusticolError::invalid_argument("eager direct-table output width overflows") + })?; + let invocation_stride = input_plane_count_u32 + .checked_add(2) + .and_then(|count| count.checked_mul(4)) + .ok_or_else(|| { + RusticolError::invalid_argument("eager direct-table invocation row overflows") + })?; + let attachment_stride = output_plane_count_u32 + .checked_add(2) + .and_then(|count| count.checked_mul(4)) + .ok_or_else(|| { + RusticolError::invalid_argument("eager direct-table attachment row overflows") + })?; + Ok(Self { + input_complex_count, + output_complex_count, + input_plane_count: input_plane_count_u32 as usize, + output_plane_count: output_plane_count_u32 as usize, + invocation_stride, + attachment_stride, + }) + } + + const fn layout(self) -> SymjitPlaneLayout { + SymjitPlaneLayout::complex( + self.input_complex_count as usize, + self.output_complex_count as usize, + ) + } + + const fn attachment_start_offset(self) -> usize { + self.input_plane_count * std::mem::size_of::() + } + + const fn attachment_count_offset(self) -> usize { + self.attachment_start_offset() + std::mem::size_of::() + } + + const fn scale_offset(self) -> usize { + self.output_plane_count * std::mem::size_of::() + } + + const fn operation_offset(self) -> usize { + self.scale_offset() + std::mem::size_of::() + } +} + +/// Derive the authenticated Rusticol plane-table descriptor for one prepared +/// P-kernel. The function signature is retained for Python artifact generation. +pub(crate) fn eager_direct_descriptor_for_source_application_bytes( + source_bytes: &[u8], + input_complex_count: u32, + output_complex_count: u32, + display_path: &Path, +) -> RusticolResult> { + let metadata = EagerDirectTableMetadata::new(input_complex_count, output_complex_count)?; + // Loading here proves that generation cannot bind a B-kernel, a malformed + // application, or mismatched split-plane dimensions. + let _kernel = SymjitPlaneKernel::load_bytes(source_bytes, display_path, metadata.layout())?; + Ok(encode_plane_descriptor(source_bytes, metadata)) +} + +fn encode_plane_descriptor(source_bytes: &[u8], metadata: EagerDirectTableMetadata) -> Vec { + let digest = Sha256::digest(source_bytes); + let mut bytes = Vec::with_capacity(EAGER_PLANE_DESCRIPTOR_SIZE); + bytes.extend_from_slice(&EAGER_PLANE_DESCRIPTOR_MAGIC); + push_descriptor_u32(&mut bytes, EAGER_PLANE_DESCRIPTOR_VERSION); + push_descriptor_u32(&mut bytes, EAGER_PLANE_DESCRIPTOR_SIZE as u32); + push_descriptor_u32(&mut bytes, metadata.input_complex_count); + push_descriptor_u32(&mut bytes, metadata.output_complex_count); + push_descriptor_u32(&mut bytes, metadata.invocation_stride); + push_descriptor_u32(&mut bytes, metadata.attachment_stride); + bytes.extend_from_slice(&digest); + debug_assert_eq!(bytes.len(), EAGER_PLANE_DESCRIPTOR_SIZE); + bytes +} + +fn decode_plane_descriptor( + source_bytes: &[u8], + descriptor_bytes: &[u8], + display_path: &Path, +) -> RusticolResult { + let regenerate = || { + RusticolError::compatibility(format!( + "eager plane-table descriptor {} is not compatible with the SymJIT 2.22 P-kernel ABI; regenerate the prepared model", + display_path.display() + )) + }; + if descriptor_bytes.len() != EAGER_PLANE_DESCRIPTOR_SIZE + || descriptor_bytes.get(..8) != Some(EAGER_PLANE_DESCRIPTOR_MAGIC.as_slice()) + || read_descriptor_u32(descriptor_bytes, 8)? != EAGER_PLANE_DESCRIPTOR_VERSION + || read_descriptor_u32(descriptor_bytes, 12)? != EAGER_PLANE_DESCRIPTOR_SIZE as u32 + { + return Err(regenerate()); + } + let metadata = EagerDirectTableMetadata::new( + read_descriptor_u32(descriptor_bytes, 16)?, + read_descriptor_u32(descriptor_bytes, 20)?, + )?; + if read_descriptor_u32(descriptor_bytes, 24)? != metadata.invocation_stride + || read_descriptor_u32(descriptor_bytes, 28)? != metadata.attachment_stride + { + return Err(RusticolError::integrity(format!( + "eager plane-table descriptor {} has inconsistent canonical row strides", + display_path.display() + ))); + } + let digest = Sha256::digest(source_bytes); + if descriptor_bytes[32..] != digest[..] { + return Err(RusticolError::integrity(format!( + "eager plane-table descriptor {} does not authenticate its P-kernel source", + display_path.display() + ))); + } + Ok(metadata) +} + +fn push_descriptor_u32(bytes: &mut Vec, value: u32) { + bytes.extend_from_slice(&value.to_le_bytes()); +} + +fn read_descriptor_u32(bytes: &[u8], offset: usize) -> RusticolResult { + let encoded = bytes + .get(offset..offset + 4) + .ok_or_else(|| RusticolError::integrity("eager plane-table descriptor is truncated"))?; + Ok(u32::from_le_bytes(encoded.try_into().map_err(|_| { + RusticolError::internal("could not decode eager descriptor field") + })?)) +} + +/// One persistent plane-catalog entry backed by the eager arena. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) enum EagerDirectArenaPlaneBinding { + CurrentReal(u32), + CurrentImag(u32), + AmplitudeReal(u32), + AmplitudeImag(u32), +} + +/// Rusticol-owned ABI twin retained for native C++/ASM callables. +#[derive(Clone, Copy, Debug)] +#[repr(C)] +struct EagerDirectPlane { + values: *mut f64, + len: usize, +} + +/// Rusticol-owned ABI twin retained for native C++/ASM callables. +#[derive(Clone, Copy, Debug)] +#[repr(C)] +struct EagerDirectScalar { + value: *const f64, +} + +#[derive(Clone, Copy, Debug)] +struct ParsedInvocation { + input_start: usize, + attachment_start: u32, + attachment_count: u32, +} + +#[derive(Clone, Copy, Debug)] +struct ParsedAttachment { + destination_start: usize, + factor: u32, + operation: u32, +} + +/// Immutable raw rows plus their cold-decoded Rust orchestration form. +#[derive(Debug)] +pub(crate) struct EagerDirectTableRows { + invocations_raw: Box<[u8]>, + attachments_raw: Box<[u8]>, + invocations: Box<[ParsedInvocation]>, + input_planes: Box<[u32]>, + attachments: Box<[ParsedAttachment]>, + destination_planes: Box<[u32]>, + input_plane_count: usize, + output_plane_count: usize, +} + +impl EagerDirectTableRows { + pub(crate) fn invocation_count(&self) -> u32 { + self.invocations.len() as u32 + } + + pub(crate) fn attachment_count(&self) -> u32 { + self.attachments.len() as u32 + } +} + +/// Persistent eager arena, native catalogs, P-kernel descriptors, and scratch. +pub(crate) struct EagerDirectTableWorkspace { + arena: DirectArenaWorkspace, + planes: Box<[EagerDirectPlane]>, + /// Plane-catalog entry reserved as an immutable structural zero for + /// real-only SymJIT inputs. Native tables never use this remapping. + read_only_zero_plane_index: Option, + // Own the stable allocations addressed by `scalars`. + _scalar_values: Box<[f64]>, + scalars: Box<[EagerDirectScalar]>, + factor_re: Box<[f64]>, + factor_im: Box<[f64]>, + symjit_descriptors: Vec>, + symjit_scratch: Vec, + internal_scratch_bytes: u64, +} + +impl EagerDirectTableWorkspace { + #[allow(clippy::too_many_arguments)] + pub(crate) fn new( + current_plane_count: u32, + amplitude_plane_count: u32, + tile_capacity: u32, + plane_bindings: &[EagerDirectArenaPlaneBinding], + read_only_zero_plane_index: Option, + scalar_values: Vec, + factor_re: Vec, + factor_im: Vec, + ) -> RusticolResult { + if plane_bindings.is_empty() { + return Err(RusticolError::invalid_argument( + "eager direct-table plane catalog must not be empty", + )); + } + if factor_re.is_empty() || factor_re.len() != factor_im.len() { + return Err(RusticolError::invalid_argument( + "eager direct-table factors must be nonempty split-complex pairs", + )); + } + let _ = u32::try_from(plane_bindings.len()).map_err(|_| { + RusticolError::invalid_argument("eager direct-table plane catalog exceeds u32") + })?; + if let Some(zero_plane) = read_only_zero_plane_index { + let zero_binding = plane_bindings.get(zero_plane as usize).ok_or_else(|| { + RusticolError::invalid_argument(format!( + "eager direct-table shared-zero plane {zero_plane} exceeds catalog size {}", + plane_bindings.len() + )) + })?; + if !matches!(zero_binding, EagerDirectArenaPlaneBinding::CurrentImag(_)) { + return Err(RusticolError::invalid_argument(format!( + "eager direct-table shared-zero plane {zero_plane} is not an imaginary current plane" + ))); + } + if plane_bindings + .iter() + .enumerate() + .any(|(index, binding)| index != zero_plane as usize && binding == zero_binding) + { + return Err(RusticolError::invalid_argument(format!( + "eager direct-table shared-zero plane {zero_plane} has an aliased catalog binding" + ))); + } + } + let _ = u32::try_from(scalar_values.len()).map_err(|_| { + RusticolError::invalid_argument("eager direct-table scalar catalog exceeds u32") + })?; + let _ = u32::try_from(factor_re.len()).map_err(|_| { + RusticolError::invalid_argument("eager direct-table factor catalog exceeds u32") + })?; + let mut arena = + DirectArenaWorkspace::new(current_plane_count, amplitude_plane_count, tile_capacity)?; + arena.begin_tile(tile_capacity)?; + let view = arena.view()?; + let mut planes = Vec::new(); + planes + .try_reserve_exact(plane_bindings.len()) + .map_err(|error| { + RusticolError::internal(format!( + "could not reserve eager direct-table plane catalog: {error}" + )) + })?; + for &binding in plane_bindings { + planes.push(bind_arena_plane(view, binding)?); + } + + let scalar_values = scalar_values.into_boxed_slice(); + let scalars = scalar_values + .iter() + .map(|value| EagerDirectScalar { + value: ptr::from_ref(value), + }) + .collect::>() + .into_boxed_slice(); + Ok(Self { + arena, + planes: planes.into_boxed_slice(), + read_only_zero_plane_index, + _scalar_values: scalar_values, + scalars, + factor_re: factor_re.into_boxed_slice(), + factor_im: factor_im.into_boxed_slice(), + symjit_descriptors: Vec::new(), + symjit_scratch: Vec::new(), + internal_scratch_bytes: 0, + }) + } + + pub(crate) fn begin_tile(&mut self, point_count: u32) -> RusticolResult<()> { + self.arena.begin_tile(point_count) + } + + pub(crate) fn arena(&self) -> &DirectArenaWorkspace { + &self.arena + } + + pub(crate) fn split_arena_slices_mut( + &mut self, + ) -> (&mut [f64], &mut [f64], &mut [f64], &mut [f64]) { + self.arena.split_slices_mut() + } + + pub(crate) fn clear_current_active( + &mut self, + component_base: u32, + component_count: u32, + ) -> RusticolResult<()> { + self.arena + .clear_current_active(component_base, component_count) + } + + pub(crate) fn clear_amplitude_active( + &mut self, + component_base: u32, + component_count: u32, + ) -> RusticolResult<()> { + self.arena + .clear_amplitude_active(component_base, component_count) + } + + /// Retained for cold scalar refresh paths; current eager manifests bind + /// immutable scalar catalogs. + #[allow(dead_code)] + pub(crate) fn scalar_values_mut(&mut self) -> &mut [f64] { + &mut self._scalar_values + } + + pub(crate) fn factors_mut(&mut self) -> (&mut [f64], &mut [f64]) { + (&mut self.factor_re, &mut self.factor_im) + } + + pub(crate) const fn internal_scratch_bytes(&self) -> u64 { + self.internal_scratch_bytes + } + + pub(crate) fn symjit_descriptor_storage_bytes( + descriptor_count: usize, + ) -> RusticolResult { + descriptor_count + .checked_mul(std::mem::size_of::>()) + .ok_or_else(|| RusticolError::internal("eager descriptor storage size overflows")) + } + + pub(crate) fn symjit_workspace_bytes(&self) -> RusticolResult { + Self::symjit_descriptor_storage_bytes(self.symjit_descriptors.capacity())? + .checked_add( + self.symjit_scratch + .capacity() + .checked_mul(std::mem::size_of::()) + .ok_or_else(|| { + RusticolError::internal("eager scratch storage size overflows") + })?, + ) + .ok_or_else(|| RusticolError::internal("eager SymJIT workspace size overflows")) + } + + fn prepare_symjit(&mut self, metadata: EagerDirectTableMetadata) -> RusticolResult<()> { + let descriptor_count = metadata + .input_plane_count + .checked_add(metadata.output_plane_count) + .ok_or_else(|| RusticolError::internal("eager descriptor count overflows"))?; + if self.symjit_descriptors.capacity() < descriptor_count { + self.symjit_descriptors + .try_reserve_exact(descriptor_count - self.symjit_descriptors.len()) + .map_err(|error| { + RusticolError::internal(format!( + "could not reserve eager P-kernel descriptors: {error}" + )) + })?; + } + let scratch_len = metadata + .output_plane_count + .checked_mul(self.arena.point_stride() as usize) + .ok_or_else(|| RusticolError::internal("eager scratch size overflows"))?; + if self.symjit_scratch.len() < scratch_len { + self.symjit_scratch + .try_reserve_exact(scratch_len - self.symjit_scratch.len()) + .map_err(|error| { + RusticolError::internal(format!( + "could not reserve eager P-kernel scratch: {error}" + )) + })?; + self.symjit_scratch.resize(scratch_len, 0.0); + } + Ok(()) + } + + fn checked_range(&self, point_start: u32, point_count: u32) -> RusticolResult<(usize, usize)> { + if point_count == 0 { + return Err(RusticolError::invalid_argument( + "eager direct-table point count must be positive", + )); + } + let point_stop = point_start.checked_add(point_count).ok_or_else(|| { + RusticolError::invalid_argument("eager direct-table point range overflows u32") + })?; + if point_stop > self.arena.active_point_count() { + return Err(RusticolError::invalid_argument(format!( + "eager direct-table point range {point_start}..{point_stop} exceeds active tile {}", + self.arena.active_point_count() + ))); + } + Ok((point_start as usize, point_stop as usize)) + } + + fn native_view( + &self, + rows: &EagerDirectTableRows, + metadata: EagerDirectTableMetadata, + point_start: u32, + point_count: u32, + ) -> RusticolResult { + let _ = self.checked_range(point_start, point_count)?; + Ok(EagerDirectTableCallViewV1 { + invocations: rows.invocations_raw.as_ptr(), + invocation_count: rows.invocation_count(), + invocation_stride: metadata.invocation_stride, + attachments: rows.attachments_raw.as_ptr(), + attachment_count: rows.attachment_count(), + attachment_stride: metadata.attachment_stride, + planes: self.planes.as_ptr(), + plane_count: self.planes.len() as u32, + scalar_count: self.scalars.len() as u32, + scalars: if self.scalars.is_empty() { + ptr::null() + } else { + self.scalars.as_ptr() + }, + scale_re: self.factor_re.as_ptr(), + scale_im: self.factor_im.as_ptr(), + scale_count: self.factor_re.len() as u32, + point_start, + point_count, + }) + } +} + +/// Loaded owner of one prepared eager callable. +pub(crate) struct LoadedSymjitEagerDirectTable { + callable: EagerDirectTableCallable, + display_path: PathBuf, +} + +// Keep the callable inline: it is cold-built once, while boxing it would add a +// separately tracked allocation and indirection to every warmed dispatch. +#[allow(clippy::large_enum_variant)] +enum EagerDirectTableCallable { + Symjit { + kernel: SymjitPlaneKernel, + metadata: EagerDirectTableMetadata, + }, + #[cfg(feature = "f64-compiled")] + Native(LoadedNativeEagerDirectTable), +} + +#[derive(Clone, Copy)] +#[repr(C)] +struct EagerDirectTableCallViewV1 { + invocations: *const u8, + invocation_count: u32, + invocation_stride: u32, + attachments: *const u8, + attachment_count: u32, + attachment_stride: u32, + planes: *const EagerDirectPlane, + plane_count: u32, + scalar_count: u32, + scalars: *const EagerDirectScalar, + scale_re: *const f64, + scale_im: *const f64, + scale_count: u32, + point_start: u32, + point_count: u32, +} + +#[cfg(feature = "f64-compiled")] +type NativeEagerDirectTableCall = unsafe extern "C" fn(*const EagerDirectTableCallViewV1) -> i32; + +#[cfg(feature = "f64-compiled")] +#[repr(C)] +struct NativeEagerDirectTableMetadataV1 { + struct_size: u32, + abi_version: u32, + flags: u32, + invocation_stride: u32, + attachment_stride: u32, + input_complex_count: u32, + output_complex_count: u32, + simd_lane_width: u32, + application_abi: *const c_char, + function_name: *const c_char, + target_triple: *const c_char, + evaluator_state_sha256: *const c_char, +} + +#[cfg(feature = "f64-compiled")] +type NativeEagerDirectTableMetadataCall = + unsafe extern "C" fn() -> *const NativeEagerDirectTableMetadataV1; + +#[cfg(feature = "f64-compiled")] +const NATIVE_EAGER_DIRECT_TABLE_METADATA_VERSION: u32 = 1; +#[cfg(feature = "f64-compiled")] +const NATIVE_EAGER_DIRECT_TABLE_REQUIRED_FLAGS: u32 = 0x1f; + +#[cfg(feature = "f64-compiled")] +struct LoadedNativeEagerDirectTable { + _library: Arc, + call: NativeEagerDirectTableCall, + metadata: EagerDirectTableMetadata, +} + +impl LoadedSymjitEagerDirectTable { + /// Whether this callable consumes SymJIT P-kernel plane descriptors. + /// + /// Native C++/ASM tables retain their original per-semantic plane IDs. + pub(crate) fn is_symjit(&self) -> bool { + matches!(&self.callable, EagerDirectTableCallable::Symjit { .. }) + } + + /// Fixed workspace shape required by this callable. + /// + /// The first element is the cold descriptor count. The second is the + /// number of point-pitched scalar scratch planes. Native table callables + /// own their workspace and therefore report no Rust-side SymJIT shape. + pub(crate) fn symjit_workspace_shape(&self) -> Option<(usize, usize)> { + match &self.callable { + EagerDirectTableCallable::Symjit { metadata, .. } => Some(( + metadata.input_plane_count + metadata.output_plane_count, + metadata.output_plane_count, + )), + #[cfg(feature = "f64-compiled")] + EagerDirectTableCallable::Native(_) => None, + } + } + + #[allow(clippy::too_many_arguments)] + pub(crate) fn load_prepared_application_bytes( + source_bytes: &[u8], + descriptor_bytes: &[u8], + display_path: PathBuf, + source_application_abi: &str, + descriptor_abi: &str, + binding_abi: &str, + expected_optimization_level: u32, + expected_compression: bool, + ) -> RusticolResult { + validate_portable_abis(source_application_abi, descriptor_abi, binding_abi)?; + let metadata = decode_plane_descriptor(source_bytes, descriptor_bytes, &display_path)?; + let kernel = SymjitPlaneKernel::load_bytes(source_bytes, &display_path, metadata.layout())?; + if u32::from(kernel.optimization_level()) != expected_optimization_level { + return Err(RusticolError::integrity(format!( + "eager SymJIT plane application {} stores optimization level {}, but its \ + authenticated prepared-kernel variant declares optimization level \ + {expected_optimization_level}; regenerate the prepared model", + display_path.display(), + kernel.optimization_level(), + ))); + } + if kernel.compression() != expected_compression { + return Err(RusticolError::integrity(format!( + "eager SymJIT plane application {} stores compression {}, but its \ + authenticated prepared-kernel manifest declares compression \ + {expected_compression}; regenerate the prepared model", + display_path.display(), + kernel.compression(), + ))); + } + Ok(Self { + callable: EagerDirectTableCallable::Symjit { kernel, metadata }, + display_path, + }) + } + + #[allow(clippy::too_many_arguments)] + pub(crate) fn load_native_application( + library: Arc, + function_name: &str, + display_path: PathBuf, + source_application_abi: &str, + invocation_stride: u32, + attachment_stride: u32, + input_complex_count: u32, + output_complex_count: u32, + expected_target_triple: &str, + expected_evaluator_state_sha256: &str, + expected_simd_lane_width: u32, + ) -> RusticolResult { + if source_application_abi != EAGER_NATIVE_DIRECT_TABLE_APPLICATION_ABI { + return Err(RusticolError::compatibility(format!( + "unsupported native eager DirectTable application ABI {source_application_abi:?}; expected {EAGER_NATIVE_DIRECT_TABLE_APPLICATION_ABI:?}" + ))); + } + validate_c_symbol(function_name)?; + let metadata = EagerDirectTableMetadata::new(input_complex_count, output_complex_count)?; + if metadata.invocation_stride != invocation_stride + || metadata.attachment_stride != attachment_stride + { + return Err(RusticolError::integrity(format!( + "native eager DirectTable {} row strides ({invocation_stride}, {attachment_stride}) do not match ({}, {})", + display_path.display(), + metadata.invocation_stride, + metadata.attachment_stride, + ))); + } + #[cfg(not(feature = "f64-compiled"))] + { + let _ = ( + library, + function_name, + metadata, + expected_target_triple, + expected_evaluator_state_sha256, + expected_simd_lane_width, + ); + return Err(RusticolError::compatibility( + "native eager DirectTable execution requires the f64-compiled feature", + )); + } + #[cfg(feature = "f64-compiled")] + { + let library_path = library.display_path(); + let metadata_symbol_name = format!("{function_name}_metadata_v1"); + let metadata_call = unsafe { + library + .get::(metadata_symbol_name.as_bytes()) + .map(|symbol| *symbol) + .map_err(|error| { + RusticolError::compatibility(format!( + "could not load native eager DirectTable metadata symbol {metadata_symbol_name:?} from {}: {error}", + library_path.display() + )) + })? + }; + let exported = unsafe { metadata_call() }; + if exported.is_null() { + return Err(RusticolError::integrity(format!( + "native eager DirectTable {} returned null ABI metadata", + display_path.display() + ))); + } + let exported = unsafe { &*exported }; + let expected_struct_size = + u32::try_from(std::mem::size_of::()) + .expect("native eager metadata size fits u32"); + if exported.struct_size != expected_struct_size + || exported.abi_version != NATIVE_EAGER_DIRECT_TABLE_METADATA_VERSION + || exported.flags != NATIVE_EAGER_DIRECT_TABLE_REQUIRED_FLAGS + || exported.invocation_stride != invocation_stride + || exported.attachment_stride != attachment_stride + || exported.input_complex_count != input_complex_count + || exported.output_complex_count != output_complex_count + || exported.simd_lane_width != expected_simd_lane_width + { + return Err(RusticolError::compatibility(format!( + "native eager DirectTable {} exports incompatible ABI metadata", + display_path.display() + ))); + } + for (label, pointer, expected) in [ + ( + "application ABI", + exported.application_abi, + source_application_abi, + ), + ("function name", exported.function_name, function_name), + ( + "target triple", + exported.target_triple, + expected_target_triple, + ), + ( + "evaluator-state digest", + exported.evaluator_state_sha256, + expected_evaluator_state_sha256, + ), + ] { + let actual = native_metadata_text(pointer, label, &display_path)?; + if actual != expected { + return Err(RusticolError::compatibility(format!( + "native eager DirectTable {} {label} is {actual:?}, expected {expected:?}", + display_path.display() + ))); + } + } + let call = unsafe { + library + .get::(function_name.as_bytes()) + .map(|symbol| *symbol) + .map_err(|error| { + RusticolError::evaluation(format!( + "could not load native eager DirectTable symbol {function_name:?} from {}: {error}", + library_path.display() + )) + })? + }; + Ok(Self { + callable: EagerDirectTableCallable::Native(LoadedNativeEagerDirectTable { + _library: library, + call, + metadata, + }), + display_path, + }) + } + } + + fn metadata(&self) -> EagerDirectTableMetadata { + match &self.callable { + EagerDirectTableCallable::Symjit { metadata, .. } => *metadata, + #[cfg(feature = "f64-compiled")] + EagerDirectTableCallable::Native(callable) => callable.metadata, + } + } + + pub(crate) fn load_rows( + &self, + invocation_bytes: Vec, + attachment_bytes: Vec, + ) -> RusticolResult { + parse_rows(self.metadata(), invocation_bytes, attachment_bytes) + } + + /// Validate immutable rows, catalogs, factors, hazards, and maximum range. + /// P-kernel descriptor/scratch capacity is also allocated here, never hot. + pub(crate) fn validate_call( + &self, + rows: &EagerDirectTableRows, + workspace: &mut EagerDirectTableWorkspace, + point_start: u32, + point_count: u32, + ) -> RusticolResult<()> { + let metadata = self.metadata(); + validate_call( + metadata, + rows, + workspace, + point_start, + point_count, + &self.display_path, + )?; + if matches!(&self.callable, EagerDirectTableCallable::Symjit { .. }) { + workspace.prepare_symjit(metadata)?; + } + Ok(()) + } + + #[cfg(test)] + pub(crate) fn evaluate_checked( + &self, + rows: &EagerDirectTableRows, + workspace: &mut EagerDirectTableWorkspace, + point_start: u32, + point_count: u32, + ) -> RusticolResult<()> { + self.validate_call(rows, workspace, point_start, point_count)?; + unsafe { self.evaluate_validated_unchecked(rows, workspace, point_start, point_count) } + } + + /// Execute rows previously authenticated by [`Self::validate_call`]. + /// + /// # Safety + /// + /// Rows, plane/factor catalogs, and arena allocation addresses must remain + /// unchanged. Scalar/factor values and active point ranges may change. + pub(crate) unsafe fn evaluate_validated_unchecked( + &self, + rows: &EagerDirectTableRows, + workspace: &mut EagerDirectTableWorkspace, + point_start: u32, + point_count: u32, + ) -> RusticolResult<()> { + let metadata = self.metadata(); + let _ = workspace.checked_range(point_start, point_count)?; + match &self.callable { + EagerDirectTableCallable::Symjit { kernel, .. } => execute_symjit_rows( + kernel, + metadata, + rows, + workspace, + point_start as usize, + point_count as usize, + ), + #[cfg(feature = "f64-compiled")] + EagerDirectTableCallable::Native(callable) => { + let view = workspace.native_view(rows, metadata, point_start, point_count)?; + let status = guard_dependency_panic( + || unsafe { (callable.call)(ptr::from_ref(&view)) }, + &self.display_path, + "execute native table call", + )?; + map_direct_status(status, &self.display_path) + } + } + } +} + +fn validate_portable_abis( + source_application_abi: &str, + descriptor_abi: &str, + binding_abi: &str, +) -> RusticolResult<()> { + if source_application_abi != EAGER_DIRECT_SOURCE_APPLICATION_ABI + || descriptor_abi != EAGER_DIRECT_TABLE_DESCRIPTOR_ABI + || binding_abi != EAGER_DIRECT_TABLE_BINDING_ABI + { + return Err(RusticolError::compatibility(format!( + "unsupported eager plane-table artifact ABIs ({source_application_abi:?}, {descriptor_abi:?}, {binding_abi:?}); regenerate the prepared model" + ))); + } + Ok(()) +} + +fn parse_rows( + metadata: EagerDirectTableMetadata, + invocation_bytes: Vec, + attachment_bytes: Vec, +) -> RusticolResult { + let invocation_count = table_row_count( + &invocation_bytes, + metadata.invocation_stride, + "invocation", + false, + )?; + let attachment_count = table_row_count( + &attachment_bytes, + metadata.attachment_stride, + "attachment", + true, + )?; + let mut invocations = Vec::with_capacity(invocation_count as usize); + let mut input_planes = + Vec::with_capacity(invocation_count as usize * metadata.input_plane_count); + for row in 0..invocation_count as usize { + let input_start = input_planes.len(); + for input in 0..metadata.input_plane_count { + input_planes.push(read_row_u32( + &invocation_bytes, + row, + metadata.invocation_stride as usize, + input * 4, + )?); + } + let attachment_start = read_row_u32( + &invocation_bytes, + row, + metadata.invocation_stride as usize, + metadata.attachment_start_offset(), + )?; + let row_attachment_count = read_row_u32( + &invocation_bytes, + row, + metadata.invocation_stride as usize, + metadata.attachment_count_offset(), + )?; + let stop = attachment_start + .checked_add(row_attachment_count) + .ok_or_else(|| { + RusticolError::integrity("eager invocation attachment range overflows") + })?; + if stop > attachment_count { + return Err(RusticolError::integrity(format!( + "eager invocation {row} attachment range {attachment_start}..{stop} exceeds {attachment_count}" + ))); + } + invocations.push(ParsedInvocation { + input_start, + attachment_start, + attachment_count: row_attachment_count, + }); + } + + let mut attachments = Vec::with_capacity(attachment_count as usize); + let mut destination_planes = + Vec::with_capacity(attachment_count as usize * metadata.output_plane_count); + for row in 0..attachment_count as usize { + let destination_start = destination_planes.len(); + for output in 0..metadata.output_plane_count { + destination_planes.push(read_row_u32( + &attachment_bytes, + row, + metadata.attachment_stride as usize, + output * 4, + )?); + } + attachments.push(ParsedAttachment { + destination_start, + factor: read_row_u32( + &attachment_bytes, + row, + metadata.attachment_stride as usize, + metadata.scale_offset(), + )?, + operation: read_row_u32( + &attachment_bytes, + row, + metadata.attachment_stride as usize, + metadata.operation_offset(), + )?, + }); + } + Ok(EagerDirectTableRows { + invocations_raw: invocation_bytes.into_boxed_slice(), + attachments_raw: attachment_bytes.into_boxed_slice(), + invocations: invocations.into_boxed_slice(), + input_planes: input_planes.into_boxed_slice(), + attachments: attachments.into_boxed_slice(), + destination_planes: destination_planes.into_boxed_slice(), + input_plane_count: metadata.input_plane_count, + output_plane_count: metadata.output_plane_count, + }) +} + +fn read_row_u32(bytes: &[u8], row: usize, stride: usize, offset: usize) -> RusticolResult { + let start = row + .checked_mul(stride) + .and_then(|start| start.checked_add(offset)) + .ok_or_else(|| RusticolError::integrity("eager row field offset overflows"))?; + let field = bytes + .get(start..start + 4) + .ok_or_else(|| RusticolError::integrity("eager row field is truncated"))?; + Ok(u32::from_le_bytes(field.try_into().map_err(|_| { + RusticolError::internal("could not decode eager row field") + })?)) +} + +fn validate_call( + metadata: EagerDirectTableMetadata, + rows: &EagerDirectTableRows, + workspace: &EagerDirectTableWorkspace, + point_start: u32, + point_count: u32, + display_path: &Path, +) -> RusticolResult<()> { + if rows.input_plane_count != metadata.input_plane_count + || rows.output_plane_count != metadata.output_plane_count + { + return Err(RusticolError::integrity( + "eager rows were loaded for a different P-kernel shape", + )); + } + let (_, point_stop) = workspace.checked_range(point_start, point_count)?; + if !workspace.scalars.is_empty() { + return Err(RusticolError::integrity( + "eager P-kernel plane tables do not accept scalar descriptors", + )); + } + if workspace.factor_re.is_empty() || workspace.factor_re.len() != workspace.factor_im.len() { + return Err(RusticolError::integrity( + "eager factor catalog is not a nonempty split-complex pair", + )); + } + for (index, plane) in workspace.planes.iter().enumerate() { + if plane.values.is_null() || plane.len < point_stop { + return Err(RusticolError::integrity(format!( + "eager plane {index} does not cover the validated point range" + ))); + } + } + for (row_index, invocation) in rows.invocations.iter().enumerate() { + let inputs = invocation_inputs(rows, *invocation, metadata); + for (binding, &plane) in inputs.iter().enumerate() { + if plane as usize >= workspace.planes.len() { + return Err(RusticolError::integrity(format!( + "eager invocation {row_index} input binding {binding} references plane {plane}, catalog has {}", + workspace.planes.len() + ))); + } + } + for attachment_index in + invocation.attachment_start..invocation.attachment_start + invocation.attachment_count + { + let attachment = rows.attachments[attachment_index as usize]; + let destinations = attachment_destinations(rows, attachment, metadata); + for (component, &plane) in destinations.iter().enumerate() { + if plane as usize >= workspace.planes.len() { + return Err(RusticolError::integrity(format!( + "eager attachment {attachment_index} destination component {component} references plane {plane}, catalog has {}", + workspace.planes.len() + ))); + } + if workspace.read_only_zero_plane_index == Some(plane) { + return Err(RusticolError::integrity(format!( + "eager attachment {attachment_index} destination component {component} \ + references read-only shared-zero plane {plane}" + ))); + } + } + if attachment.factor as usize >= workspace.factor_re.len() { + return Err(RusticolError::integrity(format!( + "eager attachment {attachment_index} factor {} exceeds {}", + attachment.factor, + workspace.factor_re.len() + ))); + } + if !matches!( + attachment.operation, + EAGER_DESTINATION_OVERWRITE | EAGER_DESTINATION_ACCUMULATE + ) { + return Err(RusticolError::integrity(format!( + "eager attachment {attachment_index} operation {} is not overwrite(0) or accumulate(1)", + attachment.operation + ))); + } + } + validate_invocation_hazards(row_index, *invocation, metadata, rows, workspace)?; + } + let _ = display_path; + Ok(()) +} + +fn validate_invocation_hazards( + row_index: usize, + invocation: ParsedInvocation, + metadata: EagerDirectTableMetadata, + rows: &EagerDirectTableRows, + workspace: &EagerDirectTableWorkspace, +) -> RusticolResult<()> { + let inputs = invocation_inputs(rows, invocation, metadata); + for (binding, &input_index) in inputs.iter().enumerate() { + let input = plane_range(workspace.planes[input_index as usize])?; + for attachment_index in + invocation.attachment_start..invocation.attachment_start + invocation.attachment_count + { + let attachment = rows.attachments[attachment_index as usize]; + for (component, &destination_index) in + attachment_destinations(rows, attachment, metadata) + .iter() + .enumerate() + { + if input.overlaps(plane_range(workspace.planes[destination_index as usize])?) { + return Err(RusticolError::integrity(format!( + "eager invocation {row_index} input binding {binding} overlaps attachment {attachment_index} destination component {component}" + ))); + } + } + } + } + + let destination_count = invocation.attachment_count as usize * metadata.output_plane_count; + for left in 0..destination_count { + let left_attachment = + invocation.attachment_start as usize + left / metadata.output_plane_count; + let left_component = left % metadata.output_plane_count; + let left_row = rows.attachments[left_attachment]; + let left_index = attachment_destinations(rows, left_row, metadata)[left_component] as usize; + let left_range = plane_range(workspace.planes[left_index])?; + for right in left + 1..destination_count { + let right_attachment = + invocation.attachment_start as usize + right / metadata.output_plane_count; + let right_component = right % metadata.output_plane_count; + let right_row = rows.attachments[right_attachment]; + let right_index = + attachment_destinations(rows, right_row, metadata)[right_component] as usize; + if left_range.overlaps(plane_range(workspace.planes[right_index])?) { + return Err(RusticolError::integrity(format!( + "eager invocation {row_index} attachment {left_attachment} destination component {left_component} overlaps attachment {right_attachment} destination component {right_component}" + ))); + } + } + } + Ok(()) +} + +#[derive(Clone, Copy)] +struct AddressRange { + start: usize, + end: usize, +} + +impl AddressRange { + const fn overlaps(self, other: Self) -> bool { + self.start < other.end && other.start < self.end + } +} + +fn plane_range(plane: EagerDirectPlane) -> RusticolResult { + let bytes = plane + .len + .checked_mul(std::mem::size_of::()) + .ok_or_else(|| RusticolError::integrity("eager plane byte range overflows"))?; + let start = plane.values.addr(); + let end = start + .checked_add(bytes) + .ok_or_else(|| RusticolError::integrity("eager plane address range overflows"))?; + Ok(AddressRange { start, end }) +} + +fn invocation_inputs( + rows: &EagerDirectTableRows, + invocation: ParsedInvocation, + metadata: EagerDirectTableMetadata, +) -> &[u32] { + &rows.input_planes[invocation.input_start..invocation.input_start + metadata.input_plane_count] +} + +fn attachment_destinations( + rows: &EagerDirectTableRows, + attachment: ParsedAttachment, + metadata: EagerDirectTableMetadata, +) -> &[u32] { + &rows.destination_planes + [attachment.destination_start..attachment.destination_start + metadata.output_plane_count] +} + +fn execute_symjit_rows( + kernel: &SymjitPlaneKernel, + metadata: EagerDirectTableMetadata, + rows: &EagerDirectTableRows, + workspace: &mut EagerDirectTableWorkspace, + point_start: usize, + point_count: usize, +) -> RusticolResult<()> { + let stride = workspace.arena.point_stride() as usize; + debug_assert!( + workspace.symjit_descriptors.capacity() + >= metadata.input_plane_count + metadata.output_plane_count + ); + debug_assert!(workspace.symjit_scratch.len() >= metadata.output_plane_count * stride); + + for invocation in rows.invocations.iter().copied() { + workspace.symjit_descriptors.clear(); + for &plane_index in invocation_inputs(rows, invocation, metadata) { + let plane = workspace.planes[plane_index as usize]; + workspace + .symjit_descriptors + .push(unsafe { PlaneDescriptor::from_cached_raw_parts(plane.values, plane.len)? }); + } + + let direct_attachment = (invocation.attachment_count == 1) + .then(|| rows.attachments[invocation.attachment_start as usize]) + .filter(|attachment| { + attachment.operation == EAGER_DESTINATION_OVERWRITE + && workspace.factor_re[attachment.factor as usize].to_bits() + == 1.0_f64.to_bits() + && workspace.factor_im[attachment.factor as usize].to_bits() + == 0.0_f64.to_bits() + }); + if let Some(attachment) = direct_attachment { + for &plane_index in attachment_destinations(rows, attachment, metadata) { + let plane = workspace.planes[plane_index as usize]; + workspace.symjit_descriptors.push(unsafe { + PlaneDescriptor::from_cached_raw_parts(plane.values, plane.len)? + }); + } + } else { + let scratch = workspace.symjit_scratch.as_mut_ptr(); + for output in 0..metadata.output_plane_count { + workspace.symjit_descriptors.push(unsafe { + PlaneDescriptor::from_cached_raw_parts(scratch.add(output * stride), stride)? + }); + } + } + + let execution = unsafe { + kernel.execute_raw_descriptors_unchecked( + &workspace.symjit_descriptors, + point_start, + point_count, + ) + }; + workspace.symjit_descriptors.clear(); + execution?; + + if direct_attachment.is_none() { + apply_scratch_fanout( + metadata, + invocation, + rows, + workspace, + point_start, + point_count, + ); + } + } + Ok(()) +} + +fn apply_scratch_fanout( + metadata: EagerDirectTableMetadata, + invocation: ParsedInvocation, + rows: &EagerDirectTableRows, + workspace: &mut EagerDirectTableWorkspace, + point_start: usize, + point_count: usize, +) { + let stride = workspace.arena.point_stride() as usize; + let output_bytes = (metadata.output_plane_count as u64) + .saturating_mul(point_count as u64) + .saturating_mul(std::mem::size_of::() as u64); + let read_bytes = output_bytes.saturating_mul(invocation.attachment_count as u64); + workspace.internal_scratch_bytes = workspace + .internal_scratch_bytes + .saturating_add(output_bytes) + .saturating_add(read_bytes); + + for attachment_index in + invocation.attachment_start..invocation.attachment_start + invocation.attachment_count + { + let attachment = rows.attachments[attachment_index as usize]; + let factor_re = workspace.factor_re[attachment.factor as usize]; + let factor_im = workspace.factor_im[attachment.factor as usize]; + for (output, destination_indices) in attachment_destinations(rows, attachment, metadata) + .chunks_exact(2) + .enumerate() + { + let source_re = unsafe { + std::slice::from_raw_parts( + workspace + .symjit_scratch + .as_ptr() + .add(2 * output * stride + point_start), + point_count, + ) + }; + let source_im = unsafe { + std::slice::from_raw_parts( + workspace + .symjit_scratch + .as_ptr() + .add((2 * output + 1) * stride + point_start), + point_count, + ) + }; + let destination_re_plane = workspace.planes[destination_indices[0] as usize]; + let destination_im_plane = workspace.planes[destination_indices[1] as usize]; + let destination_re = unsafe { + std::slice::from_raw_parts_mut( + destination_re_plane.values.add(point_start), + point_count, + ) + }; + let destination_im = unsafe { + std::slice::from_raw_parts_mut( + destination_im_plane.values.add(point_start), + point_count, + ) + }; + apply_complex_scaled( + destination_re, + destination_im, + source_re, + source_im, + factor_re, + factor_im, + attachment.operation == EAGER_DESTINATION_ACCUMULATE, + ); + } + } +} + +#[inline] +fn apply_complex_scaled( + destination_re: &mut [f64], + destination_im: &mut [f64], + source_re: &[f64], + source_im: &[f64], + factor_re: f64, + factor_im: f64, + accumulate: bool, +) { + debug_assert_eq!(destination_re.len(), destination_im.len()); + debug_assert_eq!(destination_re.len(), source_re.len()); + debug_assert_eq!(destination_re.len(), source_im.len()); + #[cfg(target_arch = "x86_64")] + let mut point = apply_complex_scaled_x86_64( + destination_re, + destination_im, + source_re, + source_im, + factor_re, + factor_im, + accumulate, + ); + #[cfg(target_arch = "aarch64")] + let mut point = apply_complex_scaled_aarch64( + destination_re, + destination_im, + source_re, + source_im, + factor_re, + factor_im, + accumulate, + ); + #[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))] + let mut point = 0; + if accumulate { + while point < destination_re.len() { + let re = source_re[point]; + let im = source_im[point]; + destination_re[point] += re * factor_re - im * factor_im; + destination_im[point] += re * factor_im + im * factor_re; + point += 1; + } + } else { + while point < destination_re.len() { + let re = source_re[point]; + let im = source_im[point]; + destination_re[point] = re * factor_re - im * factor_im; + destination_im[point] = re * factor_im + im * factor_re; + point += 1; + } + } +} + +#[cfg(target_arch = "x86_64")] +#[inline] +#[allow(clippy::too_many_arguments)] +fn apply_complex_scaled_x86_64( + destination_re: &mut [f64], + destination_im: &mut [f64], + source_re: &[f64], + source_im: &[f64], + factor_re: f64, + factor_im: f64, + accumulate: bool, +) -> usize { + let factor_re = f64x4::new([factor_re; 4]); + let factor_im = f64x4::new([factor_im; 4]); + let mut point = 0; + while destination_re.len() - point >= 4 { + let re = f64x4::new([ + source_re[point], + source_re[point + 1], + source_re[point + 2], + source_re[point + 3], + ]); + let im = f64x4::new([ + source_im[point], + source_im[point + 1], + source_im[point + 2], + source_im[point + 3], + ]); + let mut scaled_re = re * factor_re - im * factor_im; + let mut scaled_im = re * factor_im + im * factor_re; + if accumulate { + scaled_re += f64x4::new([ + destination_re[point], + destination_re[point + 1], + destination_re[point + 2], + destination_re[point + 3], + ]); + scaled_im += f64x4::new([ + destination_im[point], + destination_im[point + 1], + destination_im[point + 2], + destination_im[point + 3], + ]); + } + destination_re[point..point + 4].copy_from_slice(&scaled_re.to_array()); + destination_im[point..point + 4].copy_from_slice(&scaled_im.to_array()); + point += 4; + } + point +} + +#[cfg(target_arch = "aarch64")] +#[inline] +#[allow(clippy::too_many_arguments)] +fn apply_complex_scaled_aarch64( + destination_re: &mut [f64], + destination_im: &mut [f64], + source_re: &[f64], + source_im: &[f64], + factor_re: f64, + factor_im: f64, + accumulate: bool, +) -> usize { + let factor_re = f64x2::new([factor_re; 2]); + let factor_im = f64x2::new([factor_im; 2]); + let mut point = 0; + while destination_re.len() - point >= 2 { + let re = f64x2::new([source_re[point], source_re[point + 1]]); + let im = f64x2::new([source_im[point], source_im[point + 1]]); + let mut scaled_re = re * factor_re - im * factor_im; + let mut scaled_im = re * factor_im + im * factor_re; + if accumulate { + scaled_re += f64x2::new([destination_re[point], destination_re[point + 1]]); + scaled_im += f64x2::new([destination_im[point], destination_im[point + 1]]); + } + destination_re[point..point + 2].copy_from_slice(&scaled_re.to_array()); + destination_im[point..point + 2].copy_from_slice(&scaled_im.to_array()); + point += 2; + } + point +} + +#[cfg(feature = "f64-compiled")] +fn native_metadata_text( + pointer: *const c_char, + label: &str, + display_path: &Path, +) -> RusticolResult { + if pointer.is_null() { + return Err(RusticolError::integrity(format!( + "native eager DirectTable {} has a null {label}", + display_path.display() + ))); + } + unsafe { CStr::from_ptr(pointer) } + .to_str() + .map(str::to_owned) + .map_err(|error| { + RusticolError::integrity(format!( + "native eager DirectTable {} has non-UTF-8 {label}: {error}", + display_path.display() + )) + }) +} + +fn validate_c_symbol(symbol: &str) -> RusticolResult<()> { + let mut bytes = symbol.bytes(); + let Some(first) = bytes.next() else { + return Err(RusticolError::artifact( + "native eager DirectTable symbol must not be empty", + )); + }; + if !(first == b'_' || first.is_ascii_alphabetic()) + || bytes.any(|byte| !(byte == b'_' || byte.is_ascii_alphanumeric())) + { + return Err(RusticolError::artifact(format!( + "native eager DirectTable symbol {symbol:?} is not a portable C identifier" + ))); + } + Ok(()) +} + +fn bind_arena_plane( + arena: DirectArenaView, + binding: EagerDirectArenaPlaneBinding, +) -> RusticolResult { + let (base, shape, component, label) = match binding { + EagerDirectArenaPlaneBinding::CurrentReal(component) => ( + arena.current_re, + arena.current_shape()?, + component, + "current real", + ), + EagerDirectArenaPlaneBinding::CurrentImag(component) => ( + arena.current_im, + arena.current_shape()?, + component, + "current imaginary", + ), + EagerDirectArenaPlaneBinding::AmplitudeReal(component) => ( + arena.amplitude_re, + arena.amplitude_shape()?, + component, + "amplitude real", + ), + EagerDirectArenaPlaneBinding::AmplitudeImag(component) => ( + arena.amplitude_im, + arena.amplitude_shape()?, + component, + "amplitude imaginary", + ), + }; + let range = shape.checked_component_range(component, 1, label)?; + Ok(EagerDirectPlane { + values: unsafe { base.add(range.start) }, + len: range.len(), + }) +} + +fn table_row_count( + bytes: &[u8], + row_stride: u32, + label: &str, + allow_empty: bool, +) -> RusticolResult { + let stride = row_stride as usize; + if stride == 0 || !bytes.len().is_multiple_of(stride) { + return Err(RusticolError::integrity(format!( + "eager Direct-Arena {label} table has {} bytes, not a whole number of {stride}-byte rows", + bytes.len() + ))); + } + let count = u32::try_from(bytes.len() / stride).map_err(|_| { + RusticolError::integrity(format!("eager Direct-Arena {label} row count exceeds u32")) + })?; + if count == 0 && !allow_empty { + return Err(RusticolError::integrity(format!( + "eager Direct-Arena {label} table must not be empty" + ))); + } + Ok(count) +} + +fn map_direct_status(status: i32, path: &Path) -> RusticolResult<()> { + match status { + DIRECT_STATUS_OK => Ok(()), + DIRECT_STATUS_INVALID_ARGUMENT => Err(RusticolError::integrity(format!( + "eager Direct-Arena callable {} rejected its authenticated call view", + path.display() + ))), + DIRECT_STATUS_INVALID_CONTEXT => Err(RusticolError::internal(format!( + "eager Direct-Arena callable {} has an invalid context", + path.display() + ))), + DIRECT_STATUS_EXECUTION_FAILED => Err(RusticolError::evaluation(format!( + "eager Direct-Arena callable {} failed during generated execution", + path.display() + ))), + other => Err(RusticolError::evaluation(format!( + "eager Direct-Arena callable {} returned unknown status {other}", + path.display() + ))), + } +} + +fn guard_dependency_panic( + operation: impl FnOnce() -> T, + path: &Path, + action: &str, +) -> RusticolResult { + catch_unwind(AssertUnwindSafe(operation)).map_err(|payload| { + RusticolError::compatibility(format!( + "dependency panicked while trying to {action} eager Direct-Arena application {}: {}", + path.display(), + panic_detail(payload) + )) + }) +} + +fn panic_detail(payload: Box) -> String { + if let Some(message) = payload.downcast_ref::<&str>() { + (*message).to_string() + } else if let Some(message) = payload.downcast_ref::() { + message.clone() + } else { + "non-string panic payload".to_string() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::engine::count_allocations; + use std::mem; + use symjit::{Compiler, CompilerType, Config, Storage}; + + const MODEL: &str = " +([('add', ('temp', 0), [('param', 0), ('param', 1)], 0), + ('assign', ('out', 0), ('temp', 0))], + 2, + []) +"; + const LENGTHS: &[usize] = &[1, 2, 3, 7, 8, 127, 128, 129, 1023, 1024, 1025]; + const ODD_TAIL_LENGTHS: &[usize] = &[127, 129, 1023, 1025]; + + #[derive(Clone, Copy)] + #[repr(C)] + struct InvocationRow { + x_re: u32, + x_im: u32, + y_re: u32, + y_im: u32, + attachment_start: u32, + attachment_count: u32, + } + + #[derive(Clone, Copy)] + #[repr(C)] + struct AttachmentRow { + destination_re: u32, + destination_im: u32, + factor: u32, + operation: u32, + } + + fn source_bytes() -> Vec { + let mut config = Config::new(CompilerType::Native, 0).unwrap(); + config.set_opt_level(2); + config.set_cse(true); + config.set_fastmath(true); + config.set_simd(true); + config.enable_simd512(false); + config.set_simd_branch(false); + config.set_complex(true); + config.set_fast_complex(false); + config.set_threads(false); + config.set_symbolica(true); + config.set_compact(true); + config.set_compress(true); + config.set_direct(false); + config.set_huge(false); + config.set_parallel_mul(true); + config.set_direct_arena(true); + config.set_direct_arena_operation(0); + config.set_direct_arena_identity_output(true); + let mut application = Compiler::with_config(config) + .translate(MODEL.to_string(), 2) + .unwrap(); + application.prepare_simd(); + let mut bytes = Vec::new(); + application.save(&mut bytes).unwrap(); + bytes + } + + fn table_fixture( + point_capacity: u32, + ) -> ( + LoadedSymjitEagerDirectTable, + EagerDirectTableRows, + EagerDirectTableWorkspace, + ) { + let source = source_bytes(); + let descriptor = eager_direct_descriptor_for_source_application_bytes( + &source, + 2, + 1, + Path::new("eager-table-test.symjit"), + ) + .unwrap(); + let application = LoadedSymjitEagerDirectTable::load_prepared_application_bytes( + &source, + &descriptor, + PathBuf::from("eager-table-test.symjit"), + EAGER_DIRECT_SOURCE_APPLICATION_ABI, + EAGER_DIRECT_TABLE_DESCRIPTOR_ABI, + EAGER_DIRECT_TABLE_BINDING_ABI, + 2, + true, + ) + .unwrap(); + let invocations = [ + InvocationRow { + x_re: 0, + x_im: 1, + y_re: 2, + y_im: 3, + attachment_start: 0, + attachment_count: 2, + }, + InvocationRow { + x_re: 4, + x_im: 5, + y_re: 6, + y_im: 7, + attachment_start: 2, + attachment_count: 1, + }, + ]; + let attachments = [ + AttachmentRow { + destination_re: 4, + destination_im: 5, + factor: 0, + operation: 0, + }, + AttachmentRow { + destination_re: 6, + destination_im: 7, + factor: 1, + operation: 0, + }, + AttachmentRow { + destination_re: 8, + destination_im: 9, + factor: 0, + operation: 0, + }, + ]; + let rows = application + .load_rows(rows_as_bytes(&invocations), rows_as_bytes(&attachments)) + .unwrap(); + let bindings = [ + EagerDirectArenaPlaneBinding::CurrentReal(0), + EagerDirectArenaPlaneBinding::CurrentImag(0), + EagerDirectArenaPlaneBinding::CurrentReal(1), + EagerDirectArenaPlaneBinding::CurrentImag(1), + EagerDirectArenaPlaneBinding::CurrentReal(2), + EagerDirectArenaPlaneBinding::CurrentImag(2), + EagerDirectArenaPlaneBinding::CurrentReal(3), + EagerDirectArenaPlaneBinding::CurrentImag(3), + EagerDirectArenaPlaneBinding::AmplitudeReal(0), + EagerDirectArenaPlaneBinding::AmplitudeImag(0), + ]; + let workspace = EagerDirectTableWorkspace::new( + 4, + 1, + point_capacity, + &bindings, + None, + Vec::new(), + vec![1.0, 0.5], + vec![0.0, 0.0], + ) + .unwrap(); + (application, rows, workspace) + } + + fn shared_zero_fixture( + point_capacity: u32, + ) -> ( + LoadedSymjitEagerDirectTable, + EagerDirectTableRows, + EagerDirectTableWorkspace, + ) { + let source = source_bytes(); + let descriptor = eager_direct_descriptor_for_source_application_bytes( + &source, + 2, + 1, + Path::new("eager-shared-zero-test.symjit"), + ) + .unwrap(); + let application = LoadedSymjitEagerDirectTable::load_prepared_application_bytes( + &source, + &descriptor, + PathBuf::from("eager-shared-zero-test.symjit"), + EAGER_DIRECT_SOURCE_APPLICATION_ABI, + EAGER_DIRECT_TABLE_DESCRIPTOR_ABI, + EAGER_DIRECT_TABLE_BINDING_ABI, + 2, + true, + ) + .unwrap(); + let invocations = [InvocationRow { + x_re: 0, + x_im: 1, + y_re: 0, + y_im: 1, + attachment_start: 0, + attachment_count: 1, + }]; + let attachments = [AttachmentRow { + destination_re: 2, + destination_im: 3, + factor: 0, + operation: 0, + }]; + let rows = application + .load_rows(rows_as_bytes(&invocations), rows_as_bytes(&attachments)) + .unwrap(); + let bindings = [ + EagerDirectArenaPlaneBinding::CurrentReal(0), + EagerDirectArenaPlaneBinding::CurrentImag(0), + EagerDirectArenaPlaneBinding::AmplitudeReal(0), + EagerDirectArenaPlaneBinding::AmplitudeImag(0), + ]; + let workspace = EagerDirectTableWorkspace::new( + 1, + 1, + point_capacity, + &bindings, + Some(1), + Vec::new(), + vec![1.0], + vec![0.0], + ) + .unwrap(); + (application, rows, workspace) + } + + fn rows_as_bytes(rows: &[T]) -> Vec { + unsafe { std::slice::from_raw_parts(rows.as_ptr().cast::(), mem::size_of_val(rows)) } + .to_vec() + } + + fn initialize_inputs(workspace: &mut EagerDirectTableWorkspace, points: usize) { + workspace.begin_tile(points as u32).unwrap(); + let stride = workspace.arena().point_stride() as usize; + let (current_re, current_im, amplitude_re, amplitude_im) = + workspace.split_arena_slices_mut(); + for point in 0..points { + current_re[point] = 1.0 + point as f64; + current_im[point] = -0.25 * point as f64; + current_re[stride + point] = 10.0 + 2.0 * point as f64; + current_im[stride + point] = 0.5 * point as f64; + } + current_re[2 * stride..4 * stride].fill(-99.0); + current_im[2 * stride..4 * stride].fill(77.0); + amplitude_re.fill(-55.0); + amplitude_im.fill(44.0); + } + + fn assert_outputs(workspace: &EagerDirectTableWorkspace, points: usize) { + let stride = workspace.arena().point_stride() as usize; + let (current_re, current_im) = workspace.arena().current_slices(); + let (amplitude_re, amplitude_im) = workspace.arena().amplitude_slices(); + for point in 0..points { + let sum_re = 11.0 + 3.0 * point as f64; + let sum_im = 0.25 * point as f64; + assert_eq!(current_re[2 * stride + point], sum_re); + assert_eq!(current_im[2 * stride + point], sum_im); + assert_eq!(current_re[3 * stride + point], 0.5 * sum_re); + assert_eq!(current_im[3 * stride + point], 0.5 * sum_im); + assert_eq!(amplitude_re[point], 1.5 * sum_re); + assert_eq!(amplitude_im[point], 1.5 * sum_im); + } + } + + #[test] + fn ordered_fanout_cross_row_dependency_and_odd_tails() { + let (application, rows, mut workspace) = table_fixture(1025); + for &points in LENGTHS { + initialize_inputs(&mut workspace, points); + application + .validate_call(&rows, &mut workspace, 0, points as u32) + .unwrap(); + unsafe { + application + .evaluate_validated_unchecked(&rows, &mut workspace, 0, points as u32) + .unwrap(); + } + assert_outputs(&workspace, points); + assert!(workspace.internal_scratch_bytes() > 0); + } + } + + #[test] + fn complex_factors_and_accumulation_preserve_tails() { + let (application, _, mut workspace) = table_fixture(1025); + workspace.factor_re.copy_from_slice(&[0.5, -0.75]); + workspace.factor_im.copy_from_slice(&[-0.25, 0.5]); + let invocations = [InvocationRow { + x_re: 0, + x_im: 1, + y_re: 2, + y_im: 3, + attachment_start: 0, + attachment_count: 2, + }]; + let attachments = [ + AttachmentRow { + destination_re: 4, + destination_im: 5, + factor: 0, + operation: EAGER_DESTINATION_OVERWRITE, + }, + AttachmentRow { + destination_re: 8, + destination_im: 9, + factor: 1, + operation: EAGER_DESTINATION_ACCUMULATE, + }, + ]; + let rows = application + .load_rows(rows_as_bytes(&invocations), rows_as_bytes(&attachments)) + .unwrap(); + + for &points in LENGTHS { + initialize_inputs(&mut workspace, points); + application + .evaluate_checked(&rows, &mut workspace, 0, points as u32) + .unwrap(); + let stride = workspace.arena().point_stride() as usize; + let (current_re, current_im) = workspace.arena().current_slices(); + let (amplitude_re, amplitude_im) = workspace.arena().amplitude_slices(); + for point in 0..points { + let source_re = 11.0 + 3.0 * point as f64; + let source_im = 0.25 * point as f64; + assert_eq!( + current_re[2 * stride + point], + 0.5 * source_re + 0.25 * source_im + ); + assert_eq!( + current_im[2 * stride + point], + 0.5 * source_im - 0.25 * source_re + ); + assert_eq!( + amplitude_re[point], + -55.0 - 0.75 * source_re - 0.5 * source_im + ); + assert_eq!( + amplitude_im[point], + 44.0 - 0.75 * source_im + 0.5 * source_re + ); + } + if points < stride { + assert_eq!(current_re[2 * stride + points], -99.0); + assert_eq!(current_im[2 * stride + points], 77.0); + assert_eq!(amplitude_re[points], -55.0); + assert_eq!(amplitude_im[points], 44.0); + } + assert!(workspace.internal_scratch_bytes() > 0); + } + } + + #[test] + fn duplicate_real_only_inputs_share_a_read_only_zero_plane() { + let (application, rows, mut workspace) = shared_zero_fixture(1025); + for &points in LENGTHS { + workspace.begin_tile(points as u32).unwrap(); + let stride = workspace.arena().point_stride() as usize; + { + let (current_re, current_im, amplitude_re, amplitude_im) = + workspace.split_arena_slices_mut(); + current_re.fill(-77.0); + current_im.fill(0.0); + amplitude_re.fill(-55.0); + amplitude_im.fill(44.0); + for (point, value) in current_re[..points].iter_mut().enumerate() { + *value = 1.0 + point as f64; + } + } + application + .validate_call(&rows, &mut workspace, 0, points as u32) + .unwrap(); + unsafe { + application + .evaluate_validated_unchecked(&rows, &mut workspace, 0, points as u32) + .unwrap(); + } + let (_, current_im) = workspace.arena().current_slices(); + let (amplitude_re, amplitude_im) = workspace.arena().amplitude_slices(); + assert!( + current_im.iter().all(|value| *value == 0.0), + "shared-zero plane changed for {points} points" + ); + for point in 0..points { + assert_eq!(amplitude_re[point], 2.0 * (1.0 + point as f64)); + assert_eq!(amplitude_im[point], 0.0); + } + assert!( + amplitude_re[points..stride] + .iter() + .all(|value| *value == -55.0), + "real sentinel tail changed for {points} points" + ); + assert!( + amplitude_im[points..stride] + .iter() + .all(|value| *value == 44.0), + "imaginary sentinel tail changed for {points} points" + ); + } + } + + #[test] + fn shared_zero_destination_is_rejected_cold() { + let (application, _, mut workspace) = shared_zero_fixture(7); + let invocations = [InvocationRow { + x_re: 0, + x_im: 1, + y_re: 0, + y_im: 1, + attachment_start: 0, + attachment_count: 1, + }]; + let attachments = [AttachmentRow { + destination_re: 1, + destination_im: 3, + factor: 0, + operation: 0, + }]; + let rows = application + .load_rows(rows_as_bytes(&invocations), rows_as_bytes(&attachments)) + .unwrap(); + let error = application + .validate_call(&rows, &mut workspace, 0, 7) + .unwrap_err(); + assert!(error.message().contains("read-only shared-zero plane")); + } + + #[test] + fn same_row_input_destination_hazard_is_rejected() { + let (application, _, mut workspace) = table_fixture(7); + let invocations = [InvocationRow { + x_re: 0, + x_im: 1, + y_re: 2, + y_im: 3, + attachment_start: 0, + attachment_count: 1, + }]; + let attachments = [AttachmentRow { + destination_re: 0, + destination_im: 1, + factor: 0, + operation: 0, + }]; + let rows = application + .load_rows(rows_as_bytes(&invocations), rows_as_bytes(&attachments)) + .unwrap(); + let error = application + .validate_call(&rows, &mut workspace, 0, 7) + .unwrap_err(); + assert!(error.message().contains("overlaps")); + } + + #[test] + fn warmed_checked_and_validated_calls_allocate_zero() { + let (application, rows, mut workspace) = table_fixture(1025); + for &points in ODD_TAIL_LENGTHS { + initialize_inputs(&mut workspace, points); + application + .evaluate_checked(&rows, &mut workspace, 0, points as u32) + .unwrap(); + application + .validate_call(&rows, &mut workspace, 0, points as u32) + .unwrap(); + let (checked, checked_count, checked_bytes) = count_allocations(|| { + application.evaluate_checked(&rows, &mut workspace, 0, points as u32) + }); + checked.unwrap(); + assert_eq!( + (checked_count, checked_bytes), + (0, 0), + "checked eager call allocated for {points} points" + ); + let (unchecked, unchecked_count, unchecked_bytes) = count_allocations(|| unsafe { + application.evaluate_validated_unchecked(&rows, &mut workspace, 0, points as u32) + }); + unchecked.unwrap(); + assert_eq!( + (unchecked_count, unchecked_bytes), + (0, 0), + "validated eager call allocated for {points} points" + ); + assert_outputs(&workspace, points); + } + } + + #[test] + fn authenticated_variant_optimization_level_mismatch_fails_closed() { + let source = source_bytes(); + let descriptor = eager_direct_descriptor_for_source_application_bytes( + &source, + 2, + 1, + Path::new("descriptor.symjit"), + ) + .unwrap(); + let error = LoadedSymjitEagerDirectTable::load_prepared_application_bytes( + &source, + &descriptor, + PathBuf::from("wrong-optimization.symjit"), + EAGER_DIRECT_SOURCE_APPLICATION_ABI, + EAGER_DIRECT_TABLE_DESCRIPTOR_ABI, + EAGER_DIRECT_TABLE_BINDING_ABI, + 3, + true, + ) + .err() + .expect("a variant/application optimization mismatch must fail closed"); + assert!(error.message().contains("optimization level")); + assert!( + error + .message() + .contains("authenticated prepared-kernel variant") + ); + assert!(error.message().contains("regenerate")); + } + + #[test] + fn authenticated_manifest_compression_mismatch_fails_closed() { + let source = source_bytes(); + let descriptor = eager_direct_descriptor_for_source_application_bytes( + &source, + 2, + 1, + Path::new("descriptor.symjit"), + ) + .unwrap(); + let error = LoadedSymjitEagerDirectTable::load_prepared_application_bytes( + &source, + &descriptor, + PathBuf::from("wrong-compression.symjit"), + EAGER_DIRECT_SOURCE_APPLICATION_ABI, + EAGER_DIRECT_TABLE_DESCRIPTOR_ABI, + EAGER_DIRECT_TABLE_BINDING_ABI, + 2, + false, + ) + .err() + .expect("a manifest/application compression mismatch must fail closed"); + assert!(error.message().contains("compression")); + assert!(error.message().contains("regenerate")); + } + + #[test] + fn descriptor_authentication_and_predecessor_abis_fail_closed() { + let source = source_bytes(); + let descriptor = eager_direct_descriptor_for_source_application_bytes( + &source, + 2, + 1, + Path::new("descriptor.symjit"), + ) + .unwrap(); + let mut tampered = descriptor.clone(); + tampered[40] ^= 1; + let error = LoadedSymjitEagerDirectTable::load_prepared_application_bytes( + &source, + &tampered, + PathBuf::from("descriptor.symjit"), + EAGER_DIRECT_SOURCE_APPLICATION_ABI, + EAGER_DIRECT_TABLE_DESCRIPTOR_ABI, + EAGER_DIRECT_TABLE_BINDING_ABI, + 2, + true, + ) + .err() + .unwrap(); + assert!(error.message().contains("authenticate")); + let error = LoadedSymjitEagerDirectTable::load_prepared_application_bytes( + &source, + &descriptor, + PathBuf::from("old-binding.symjit"), + EAGER_DIRECT_SOURCE_APPLICATION_ABI, + EAGER_DIRECT_TABLE_DESCRIPTOR_ABI, + concat!("pyamplicol-eager-plane-", "table-binding-v1"), + 2, + true, + ) + .err() + .expect("the predecessor eager binding ABI must fail closed"); + assert_eq!(error.kind(), crate::RusticolErrorKind::Compatibility); + assert!(error.message().contains("regenerate")); + let error = LoadedSymjitEagerDirectTable::load_prepared_application_bytes( + &source, + &descriptor, + PathBuf::from("old-source-application.symjit"), + "pyamplicol-symjit-plane-application-v1", + EAGER_DIRECT_TABLE_DESCRIPTOR_ABI, + EAGER_DIRECT_TABLE_BINDING_ABI, + 2, + true, + ) + .err() + .expect("the predecessor plane-application ABI must fail closed"); + assert_eq!(error.kind(), crate::RusticolErrorKind::Compatibility); + assert!(error.message().contains("regenerate")); + let error = LoadedSymjitEagerDirectTable::load_prepared_application_bytes( + &source, + b"SJTD0001", + PathBuf::from("old.symjit"), + EAGER_DIRECT_SOURCE_APPLICATION_ABI, + EAGER_DIRECT_TABLE_DESCRIPTOR_ABI, + EAGER_DIRECT_TABLE_BINDING_ABI, + 2, + true, + ) + .err() + .unwrap(); + assert!(error.message().contains("regenerate")); + } + + #[test] + fn dependency_panics_are_contained_with_eager_application_context() { + let error = guard_dependency_panic( + || panic!("synthetic table panic"), + Path::new("panic-probe.symjit"), + "exercise the panic-containment probe", + ) + .unwrap_err(); + assert_eq!(error.kind(), crate::RusticolErrorKind::Compatibility); + assert!( + error.message().contains( + "dependency panicked while trying to exercise the panic-containment probe" + ) + ); + assert!(error.message().contains("panic-probe.symjit")); + assert!(error.message().contains("synthetic table panic")); + } +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/evaluator/symjit_plane.rs b/pyamplicol/rust/crates/rusticol-core/src/evaluator/symjit_plane.rs new file mode 100644 index 0000000000..b6031acc48 --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/evaluator/symjit_plane.rs @@ -0,0 +1,2175 @@ +// SPDX-License-Identifier: 0BSD + +//! Standard SymJIT P-kernel loading, binding, and execution. +//! +//! The generic raw-plane extension applied to SymJIT 2.22 exposes the existing +//! P-kernel machine code through a stable `#[repr(C)]` descriptor. Rusticol +//! keeps those descriptors in a cold-built vector, so duplicate inputs and +//! intentional input/output aliases never require overlapping Rust +//! references. Scalar/SIMD dispatch and descriptor rebinding allocate nothing. + +use crate::{RusticolError, RusticolResult}; +use std::any::Any; +use std::marker::PhantomData; +use std::panic::{AssertUnwindSafe, catch_unwind}; +use std::path::{Path, PathBuf}; +use std::ptr::NonNull; +#[cfg(any(test, feature = "python-generation-bridge"))] +use symjit::Compiler; +use symjit::{ + Applet, Application, Compiled, CompiledPlaneFunc, CompilerType, Config, Defuns, + PlaneDescriptor as SymjitPlaneDescriptor, Storage, +}; + +const SYMJIT_APPLICATION_MAGIC: usize = 0x4056_8795_410d_08e9; +const SYMJIT_APPLICATION_STORAGE_VERSION: usize = 3; +const MAX_OPTIMIZATION_LEVEL: u8 = 3; + +type PlaneKernelFn = CompiledPlaneFunc; + +/// Logical shape of a plane-oriented SymJIT application. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) struct SymjitPlaneLayout { + input_count: usize, + output_count: usize, + complex: bool, +} + +impl SymjitPlaneLayout { + #[cfg(test)] + pub(crate) const fn real(input_count: usize, output_count: usize) -> Self { + Self { + input_count, + output_count, + complex: false, + } + } + + pub(crate) const fn complex(input_count: usize, output_count: usize) -> Self { + Self { + input_count, + output_count, + complex: true, + } + } + + pub(crate) const fn is_complex(self) -> bool { + self.complex + } + + pub(crate) fn input_plane_count(self) -> RusticolResult { + self.input_count + .checked_mul(if self.complex { 2 } else { 1 }) + .ok_or_else(|| { + RusticolError::invalid_argument("SymJIT input plane count overflows usize") + }) + } + + pub(crate) fn output_plane_count(self) -> RusticolResult { + self.output_count + .checked_mul(if self.complex { 2 } else { 1 }) + .ok_or_else(|| { + RusticolError::invalid_argument("SymJIT output plane count overflows usize") + }) + } + + pub(crate) fn plane_count(self) -> RusticolResult { + let components = if self.complex { 2 } else { 1 }; + self.input_count + .checked_add(self.output_count) + .and_then(|count| count.checked_mul(components)) + .ok_or_else(|| RusticolError::invalid_argument("SymJIT plane count overflows usize")) + } +} + +/// A lifetime-tagged wrapper around SymJIT's stable raw plane descriptor. +/// +/// Safe slice construction ties the tag to a real exclusive borrow. Runtime +/// schedulers may instead use the unsafe cached constructor, whose `'static` +/// tag permits owner-managed storage but does not extend any allocation's +/// lifetime. +#[derive(Debug)] +#[repr(transparent)] +pub(crate) struct SymjitRawPlane<'a> { + descriptor: SymjitPlaneDescriptor, + _lifetime: PhantomData<&'a mut [f64]>, +} + +/// Single cold-path descriptor type used by Rusticol schedulers. +pub(crate) type PlaneDescriptor<'a> = SymjitRawPlane<'a>; + +impl<'a> SymjitRawPlane<'a> { + #[cfg(test)] + pub(crate) fn from_slice(values: &'a mut [f64]) -> Self { + Self { + // SAFETY: The returned wrapper retains the exclusive slice + // lifetime, so the allocation outlives every use of the raw + // descriptor. + descriptor: unsafe { + SymjitPlaneDescriptor::from_raw_parts(values.as_mut_ptr(), values.len()) + }, + _lifetime: PhantomData, + } + } + + /// Creates a cold-binding descriptor without creating a Rust reference. + /// + /// # Safety + /// + /// `pointer` must remain allocated, aligned, and writable for `len` + /// consecutive `f64` values during every invocation that receives this + /// descriptor. The caller must prevent every later invocation or + /// dereference after its allocation moves, reallocates, or drops, and + /// synchronize all accesses. + /// Duplicate descriptors and exact input/output aliases can be represented + /// because this wrapper never creates overlapping Rust references. A + /// complete kernel table rejects overlapping outputs and shifted or + /// otherwise partial input/output overlaps. + pub(crate) unsafe fn from_raw_parts(pointer: *mut f64, len: usize) -> RusticolResult { + let pointer = if len == 0 && pointer.is_null() { + NonNull::::dangling().as_ptr() + } else { + NonNull::new(pointer) + .ok_or_else(|| { + RusticolError::invalid_argument( + "a non-empty SymJIT plane has a null data pointer", + ) + })? + .as_ptr() + }; + if pointer.addr() % std::mem::align_of::() != 0 { + return Err(RusticolError::invalid_argument( + "a SymJIT plane data pointer is not aligned for f64", + )); + } + Ok(Self { + descriptor: unsafe { SymjitPlaneDescriptor::from_raw_parts(pointer, len) }, + _lifetime: PhantomData, + }) + } + + fn interval(&self) -> RusticolResult> { + if self.descriptor.len == 0 { + return Ok(None); + } + let start = self.descriptor.data.addr(); + let bytes = self + .descriptor + .len + .checked_mul(std::mem::size_of::()) + .ok_or_else(|| { + RusticolError::invalid_argument("SymJIT plane byte length overflows usize") + })?; + let end = start.checked_add(bytes).ok_or_else(|| { + RusticolError::invalid_argument("SymJIT plane address range overflows usize") + })?; + Ok(Some((start, end))) + } + + pub(crate) const fn len(&self) -> usize { + self.descriptor.len + } +} + +impl SymjitRawPlane<'static> { + /// Creates a descriptor for an owner-managed, address-stable runtime cache. + /// + /// The returned `'static` tag is a storage convenience, not an allocation + /// lifetime claim. + /// + /// # Safety + /// + /// The owning runtime must prevent every invocation or descriptor + /// dereference after the backing allocation moves, reallocates, or drops; + /// keep the memory aligned and writable for `len` values during each + /// invocation; and synchronize all descriptor use. Rebinding must finish + /// before the old allocation is invalidated. + pub(crate) unsafe fn from_cached_raw_parts( + pointer: *mut f64, + len: usize, + ) -> RusticolResult { + unsafe { SymjitRawPlane::from_raw_parts(pointer, len) } + } +} + +/// Loaded P-kernel code. The `Applet` owns the executable mappings referenced +/// by the cached function pointers. +pub(crate) struct SymjitPlaneKernel { + applet: Applet, + display_path: PathBuf, + layout: SymjitPlaneLayout, + optimization_level: u8, + compression: bool, + scalar: PlaneKernelFn, + simd: Option, + simd_lanes: usize, +} + +impl std::fmt::Debug for SymjitPlaneKernel { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter + .debug_struct("SymjitPlaneKernel") + .field("display_path", &self.display_path) + .field("layout", &self.layout) + .field("optimization_level", &self.optimization_level) + .field("compression", &self.compression) + .field("simd_lanes", &self.simd_lanes) + .finish_non_exhaustive() + } +} + +impl SymjitPlaneKernel { + /// Loads, authenticates, recompiles, and seals a storage-v3 P-kernel. + pub(crate) fn load_bytes( + bytes: &[u8], + display_path: impl Into, + layout: SymjitPlaneLayout, + ) -> RusticolResult { + let display_path = display_path.into(); + authenticate_storage_v3(bytes, &display_path)?; + + let mut loader_config = explicit_loader_config()?; + loader_config.set_defuns(Defuns::new()); + let mut input = bytes; + let mut application = guard_symjit_panic( + || Application::load(&mut input, &loader_config), + || format!("load plane application {}", display_path.display()), + )? + .map_err(|error| { + RusticolError::compatibility(format!( + "could not load SymJIT storage-v3 plane application {}: {error}", + display_path.display() + )) + })?; + if !input.is_empty() { + return Err(RusticolError::integrity(format!( + "SymJIT plane application {} has {} unauthenticated trailing bytes", + display_path.display(), + input.len() + ))); + } + let optimization_level = validate_application(&application, &display_path, layout)?; + let compression = application.config.compress(); + + // SIMD compilation is lazy in SymJIT. Calling this before sealing + // guarantees that a supported SIMD kernel is retained by the Applet. + guard_symjit_panic( + || application.prepare_simd(), + || { + format!( + "prepare SIMD for plane application {}", + display_path.display() + ) + }, + )?; + validate_compiled_kernels(&application, &display_path)?; + + let applet = guard_symjit_panic( + || application.seal(), + || format!("seal plane application {}", display_path.display()), + )? + .map_err(|error| { + RusticolError::evaluation(format!( + "could not seal SymJIT plane application {}: {error}", + display_path.display() + )) + })?; + let scalar = applet.scalar_plane_kernel().ok_or_else(|| { + RusticolError::compatibility(format!( + "SymJIT plane application {} has no scalar raw-plane kernel", + display_path.display() + )) + })?; + let (simd, simd_lanes) = match applet.compiled_simd.as_ref() { + Some(compiled) if compiled.support_indirect() => { + (applet.simd_plane_kernel(), compiled.count_lanes()) + } + _ => (None, 1), + }; + validate_simd_lane_width(simd.is_some(), simd_lanes, &display_path)?; + + Ok(Self { + applet, + display_path, + layout, + optimization_level, + compression, + scalar, + simd, + simd_lanes, + }) + } + + pub(crate) fn input_plane_count(&self) -> usize { + self.layout + .input_plane_count() + .expect("validated SymJIT input plane count") + } + + pub(crate) fn output_plane_count(&self) -> usize { + self.layout + .output_plane_count() + .expect("validated SymJIT output plane count") + } + + #[cfg(test)] + pub(crate) const fn simd_lanes(&self) -> usize { + self.simd_lanes + } + + pub(crate) fn optimization_level(&self) -> u8 { + self.optimization_level + } + + pub(crate) fn compression(&self) -> bool { + self.compression + } + + /// Cold-binds safe, mutually disjoint slices into one stable descriptor + /// table. The vector is never resized on the execution path. + #[cfg(test)] + pub(crate) fn bind<'kernel, 'planes>( + &'kernel self, + planes: Vec<&'planes mut [f64]>, + ) -> RusticolResult> { + let table = self.build_table(planes)?; + Ok(SymjitBoundPlaneKernel { + kernel: self, + table, + }) + } + + /// Cold-builds a descriptor table which is independent of the kernel + /// borrow and can be cached by recurrence/eager runtimes. + #[cfg(test)] + pub(crate) fn build_table<'planes>( + &self, + planes: Vec<&'planes mut [f64]>, + ) -> RusticolResult> { + validate_slice_table(self.layout, &planes)?; + let point_count = planes.first().map_or(0, |plane| plane.len()); + let descriptors = planes + .into_iter() + .map(|plane| { + // SAFETY: The table carries `'planes`, retaining each + // exclusive slice borrow for the descriptor's lifetime. + unsafe { SymjitPlaneDescriptor::from_raw_parts(plane.as_mut_ptr(), plane.len()) } + }) + .collect(); + Ok(SymjitPlaneTable { + descriptors, + point_count, + input_plane_count: self.input_plane_count(), + output_plane_count: self.output_plane_count(), + simd_safe: true, + _lifetime: PhantomData, + }) + } + + /// Cold-binds raw planes after validating their shape and address ranges. + /// Duplicate input planes and exact input/output aliases are valid. + #[cfg(test)] + pub(crate) fn bind_raw<'kernel, 'planes>( + &'kernel self, + planes: Vec>, + ) -> RusticolResult> { + let table = self.build_raw_table(planes)?; + Ok(SymjitBoundPlaneKernel { + kernel: self, + table, + }) + } + + /// Cold-builds a reusable table from raw plane descriptors. + pub(crate) fn build_raw_table<'planes>( + &self, + planes: Vec>, + ) -> RusticolResult> { + self.build_raw_table_from_descriptors(&planes) + } + + /// Cold-builds a reusable table while retaining the caller's descriptor + /// vector and capacity for subsequent allocation-free row preparation. + pub(crate) fn build_raw_table_from_descriptors<'planes>( + &self, + planes: &[PlaneDescriptor<'planes>], + ) -> RusticolResult> { + validate_raw_table(self.layout, planes)?; + let point_count = planes.first().map_or(0, PlaneDescriptor::len); + let input_plane_count = self.input_plane_count(); + let output_plane_count = self.output_plane_count(); + let simd_safe = validate_raw_aliases(input_plane_count, output_plane_count, planes)?; + Ok(SymjitPlaneTable { + descriptors: planes.iter().map(|plane| plane.descriptor).collect(), + point_count, + input_plane_count, + output_plane_count, + simd_safe, + _lifetime: PhantomData, + }) + } + + pub(crate) fn execute_table( + &self, + table: &mut SymjitPlaneTable<'_>, + point_start: usize, + point_count: usize, + ) -> RusticolResult<()> { + if table.input_plane_count != self.input_plane_count() + || table.output_plane_count != self.output_plane_count() + { + return Err(RusticolError::invalid_argument(format!( + "SymJIT plane table shape ({}, {}) does not match kernel shape ({}, {})", + table.input_plane_count, + table.output_plane_count, + self.input_plane_count(), + self.output_plane_count() + ))); + } + self.invoke_descriptors(table, point_start, point_count) + } + + /// Validates a scheduler-owned raw descriptor slice on the cold path. + /// + /// Retained as the checked counterpart required by the generic unsafe raw + /// callable contract. Current schedulers perform equivalent validation + /// while constructing their authenticated row storage. + #[allow(dead_code)] + pub(crate) fn validate_raw_descriptors( + &self, + planes: &[PlaneDescriptor<'_>], + ) -> RusticolResult { + validate_raw_table(self.layout, planes)?; + let _ = validate_raw_aliases(self.input_plane_count(), self.output_plane_count(), planes)?; + Ok(planes.first().map_or(0, PlaneDescriptor::len)) + } + + /// Executes a scheduler-owned descriptor slice without allocating or + /// copying its entries. + /// + /// # Safety + /// + /// [`Self::validate_raw_descriptors`] or equivalent scheduler-owned cold + /// validation must previously have succeeded for the same descriptor + /// shape. Every pointed-to allocation must still be live, synchronized, + /// and cover `point_start..point_start + point_count`. + /// When this kernel has a SIMD callable, every input byte range must be + /// disjoint from every output byte range. A divergent SIMD block writes + /// its epilogue before returning the replay signal, so an aliased output + /// could otherwise destroy an input before scalar replay. Callers that + /// need input/output aliases must use a cold-built [`SymjitPlaneTable`], + /// which detects the overlap and disables SIMD. + pub(crate) unsafe fn execute_raw_descriptors_unchecked( + &self, + planes: &[PlaneDescriptor<'_>], + point_start: usize, + point_count: usize, + ) -> RusticolResult<()> { + if planes.len() != self.layout.plane_count()? { + return Err(RusticolError::invalid_argument( + "SymJIT raw descriptor shape changed after cold validation", + )); + } + let point_end = point_start.checked_add(point_count).ok_or_else(|| { + RusticolError::invalid_argument("SymJIT plane execution range overflows usize") + })?; + if point_end > planes.first().map_or(0, PlaneDescriptor::len) { + return Err(RusticolError::invalid_argument( + "SymJIT raw descriptor range exceeds its validated plane capacity", + )); + } + if point_count == 0 { + return Ok(()); + } + // `SymjitRawPlane` is transparent over the upstream `#[repr(C)]` + // descriptor; the lifetime marker has no storage. + let table = planes.as_ptr().cast::>(); + let failure = invoke_range( + self.scalar, + self.simd, + self.simd_lanes, + table, + self.applet.params.as_ptr(), + point_start, + point_end, + )?; + if let Some(failure) = failure { + return Err(RusticolError::evaluation(format!( + "SymJIT plane application {} returned status {} from its scalar kernel at point index {}", + self.display_path.display(), + failure.status, + failure.index + ))); + } + Ok(()) + } + + /// Invokes a prebuilt, validated descriptor table without allocating or + /// changing its address. This is the only runtime method coupled to + /// SymJIT's raw P-kernel callable. + fn invoke_descriptors( + &self, + table: &SymjitPlaneTable<'_>, + point_start: usize, + point_count: usize, + ) -> RusticolResult<()> { + let point_end = point_start.checked_add(point_count).ok_or_else(|| { + RusticolError::invalid_argument("SymJIT plane execution range overflows usize") + })?; + if point_end > table.point_count { + return Err(RusticolError::invalid_argument(format!( + "SymJIT plane execution range {point_start}..{point_end} exceeds point capacity {}", + table.point_count + ))); + } + if point_count == 0 { + return Ok(()); + } + + let failure = invoke_range( + self.scalar, + self.simd.filter(|_| table.simd_safe), + self.simd_lanes, + table.as_symjit_ptr(), + self.applet.params.as_ptr(), + point_start, + point_end, + )?; + if let Some(failure) = failure { + return Err(RusticolError::evaluation(format!( + "SymJIT plane application {} returned status {} from its scalar kernel at point index {}", + self.display_path.display(), + failure.status, + failure.index + ))); + } + Ok(()) + } +} + +/// The single representation-sensitive descriptor table. Descriptors are +/// allocated and validated on the cold path and never resized while bound. +pub(crate) struct SymjitPlaneTable<'planes> { + descriptors: Vec>, + point_count: usize, + input_plane_count: usize, + output_plane_count: usize, + /// False when any input byte range overlaps any output byte range. + /// + /// SymJIT's vector epilogue runs before it returns a lane-divergence + /// signal. Such tables therefore use the scalar kernel from the outset so + /// replay can never observe inputs clobbered by a speculative epilogue. + simd_safe: bool, + _lifetime: PhantomData<&'planes mut [f64]>, +} + +impl SymjitPlaneTable<'_> { + fn as_symjit_ptr(&self) -> *const SymjitPlaneDescriptor { + self.descriptors.as_ptr() + } + + #[cfg(test)] + pub(crate) const fn point_count(&self) -> usize { + self.point_count + } + + #[cfg(test)] + const fn simd_safe(&self) -> bool { + self.simd_safe + } +} + +#[cfg(test)] +impl<'planes> SymjitPlaneTable<'planes> { + /// Rebinds the existing descriptor allocation to a new set of raw planes. + /// + /// The vector allocation and address remain unchanged. Validation finishes + /// before any slot is replaced. + /// + /// # Safety + /// + /// Every descriptor must remain allocated and writable for `'planes`. No + /// execution may overlap this call, the old descriptors must no longer be + /// used after rebinding begins, and the caller must synchronize access to + /// the new storage until this table is rebound again or dropped. Any + /// input/output alias must cover exactly the same byte range and be valid + /// for the particular P-kernel operation being invoked. + pub(crate) unsafe fn rebind_raw( + &mut self, + planes: &[PlaneDescriptor<'planes>], + ) -> RusticolResult<()> { + validate_raw_plane_count_and_ranges(self.descriptors.len(), planes)?; + let simd_safe = + validate_raw_aliases(self.input_plane_count, self.output_plane_count, planes)?; + + for (descriptor, plane) in self.descriptors.iter_mut().zip(planes) { + *descriptor = plane.descriptor; + } + self.point_count = planes.first().map_or(0, PlaneDescriptor::len); + self.simd_safe = simd_safe; + Ok(()) + } +} + +/// A stable, allocation-free hot binding of a P-kernel to split planes. +#[cfg(test)] +pub(crate) struct SymjitBoundPlaneKernel<'kernel, 'planes> { + // Keep the Applet reachable for at least as long as every machine-code + // function pointer invocation. + kernel: &'kernel SymjitPlaneKernel, + table: SymjitPlaneTable<'planes>, +} + +#[cfg(test)] +impl std::fmt::Debug for SymjitBoundPlaneKernel<'_, '_> { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter + .debug_struct("SymjitBoundPlaneKernel") + .field("kernel", &self.kernel) + .field("plane_count", &self.table.descriptors.len()) + .field("point_count", &self.table.point_count) + .finish() + } +} + +#[cfg(test)] +impl SymjitBoundPlaneKernel<'_, '_> { + pub(crate) fn execute_all(&mut self) -> RusticolResult<()> { + self.execute_range(0, self.table.point_count) + } + + /// Executes `[point_start, point_start + point_count)`. + /// + /// Scalar and SIMD calls both receive a point index. Unaligned heads and + /// incomplete tails are dispatched through the scalar kernel. + pub(crate) fn execute_range( + &mut self, + point_start: usize, + point_count: usize, + ) -> RusticolResult<()> { + self.kernel + .execute_table(&mut self.table, point_start, point_count) + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +struct StatusFailure { + index: usize, + status: i32, +} + +fn invoke_range( + scalar: PlaneKernelFn, + simd: Option, + simd_lanes: usize, + table: *const SymjitPlaneDescriptor, + params: *const f64, + point_start: usize, + point_end: usize, +) -> RusticolResult> { + catch_unwind(AssertUnwindSafe(|| { + let mut point = point_start; + + if let Some(simd) = simd.filter(|_| simd_lanes > 1) { + while point < point_end && !point.is_multiple_of(simd_lanes) { + // SAFETY: The caller has validated every descriptor and the + // point range; all allocations remain live for this call. + let status = unsafe { scalar(std::ptr::null(), table, point, params) }; + if status != 0 { + return Some(StatusFailure { + index: point, + status, + }); + } + point += 1; + } + + while point_end - point >= simd_lanes { + // SAFETY: As above; the SIMD row index starts an aligned block + // wholly inside the validated interior. + let status = unsafe { simd(std::ptr::null(), table, point, params) }; + if status != 0 { + // A nonzero vector status is SymJIT's lane-divergence + // signal when vector branching is disabled. Match the + // standard Applet contract by replaying every lane with + // the scalar kernel. Table callers disable SIMD for + // input/output aliases, because the speculative vector + // epilogue has already written outputs at this point. + for replay_point in point..point + simd_lanes { + // SAFETY: The aligned block lies entirely inside the + // validated range and scalar replay uses point indices. + let scalar_status = + unsafe { scalar(std::ptr::null(), table, replay_point, params) }; + if scalar_status != 0 { + return Some(StatusFailure { + index: replay_point, + status: scalar_status, + }); + } + } + } + point += simd_lanes; + } + } + + while point < point_end { + // SAFETY: The scalar tail lies inside the validated point range. + let status = unsafe { scalar(std::ptr::null(), table, point, params) }; + if status != 0 { + return Some(StatusFailure { + index: point, + status, + }); + } + point += 1; + } + None + })) + .map_err(|payload| { + RusticolError::evaluation(format!( + "SymJIT panicked while executing a plane kernel: {}", + panic_detail(payload) + )) + }) +} + +/// Compiles `repr(Evaluator.get_instructions())` into a complex, SIMD-enabled, +/// direct-arena SymJIT storage-v3 application. +/// +/// This is deliberately a cold-path function. It constructs every SymJIT +/// option explicitly so a process-local `symjit.toml` cannot alter generated +/// artifacts. +#[cfg(any(test, feature = "python-generation-bridge"))] +pub(crate) fn compile_symbolica_program_to_plane_application_bytes( + program_repr: &str, + input_complex_count: usize, + output_complex_count: usize, + opt_level: u8, + compress: bool, +) -> RusticolResult> { + if program_repr.trim().is_empty() { + return Err(RusticolError::invalid_argument( + "Symbolica instruction representation must not be empty", + )); + } + let layout = SymjitPlaneLayout::complex(input_complex_count, output_complex_count); + let config = explicit_plane_config(true, opt_level, compress)?; + let mut compiler = Compiler::with_config(config); + let mut application = guard_symjit_panic( + || compiler.translate(program_repr.to_owned(), input_complex_count), + || "translate Symbolica instructions to a SymJIT plane application".to_string(), + )? + .map_err(|error| { + RusticolError::serialization(format!( + "could not translate Symbolica instructions to a SymJIT plane application: {error}" + )) + })?; + let display_path = Path::new(""); + validate_application(&application, display_path, layout)?; + guard_symjit_panic( + || application.prepare_simd(), + || "prepare SIMD for generated SymJIT plane application".to_string(), + )?; + validate_compiled_kernels(&application, display_path)?; + + let mut bytes = Vec::new(); + guard_symjit_panic( + || application.save(&mut bytes), + || "serialize generated SymJIT plane application".to_string(), + )? + .map_err(|error| { + RusticolError::serialization(format!( + "could not serialize generated SymJIT plane application: {error}" + )) + })?; + authenticate_storage_v3(&bytes, display_path)?; + Ok(bytes) +} + +fn explicit_plane_config(complex: bool, opt_level: u8, compress: bool) -> RusticolResult { + if opt_level > MAX_OPTIMIZATION_LEVEL { + return Err(RusticolError::invalid_argument(format!( + "SymJIT optimization level {opt_level} is unsupported; expected 0 through {MAX_OPTIMIZATION_LEVEL}" + ))); + } + let mut config = Config::new(CompilerType::Native, 0).map_err(|error| { + RusticolError::internal(format!( + "could not construct explicit SymJIT native configuration: {error}" + )) + })?; + config.set_opt_level(opt_level); + config.set_cse(true); + config.set_fastmath(true); + config.set_simd(true); + // pyAmpliCol's portable direct-plane contract is f64x4 on x86 and + // f64x2 on AArch64. AVX-512 remains an explicit future ABI. + config.enable_simd512(false); + config.set_simd_branch(false); + config.set_complex(complex); + config.set_fast_complex(false); + config.set_threads(false); + config.set_symbolica(true); + config.set_compact(true); + config.set_compress(compress); + config.set_direct(false); + config.set_huge(false); + config.set_parallel_mul(true); + config.set_direct_arena(true); + // Standard P-kernels own only an identity overwrite. pyAmpliCol applies + // factors and accumulation in its alias-safe arena epilogues. Set the + // reserved operation option and the active 2.22 identity-output option + // explicitly so a future SymJIT implementation cannot silently change + // persisted application semantics. + config.set_direct_arena_operation(0); + config.set_direct_arena_identity_output(true); + Ok(config) +} + +fn explicit_loader_config() -> RusticolResult { + let mut config = Config::new(CompilerType::Native, 0).map_err(|error| { + RusticolError::internal(format!( + "could not construct explicit SymJIT loader configuration: {error}" + )) + })?; + config.set_opt_level(0); + config.set_cse(false); + config.set_fastmath(false); + config.set_simd(false); + config.enable_simd512(false); + config.set_simd_branch(false); + config.set_complex(false); + config.set_fast_complex(false); + config.set_threads(false); + config.set_symbolica(false); + config.set_compact(false); + config.set_compress(false); + config.set_direct(false); + config.set_huge(false); + config.set_parallel_mul(false); + config.set_direct_arena(false); + Ok(config) +} + +fn authenticate_storage_v3(bytes: &[u8], display_path: &Path) -> RusticolResult<()> { + let word = std::mem::size_of::(); + let header_len = word + .checked_mul(2) + .ok_or_else(|| RusticolError::internal("SymJIT storage header length overflows usize"))?; + if word != 8 { + return Err(RusticolError::compatibility( + "SymJIT storage-v3 plane applications require a 64-bit target", + )); + } + if bytes.len() < header_len { + return Err(RusticolError::integrity(format!( + "SymJIT plane application {} is truncated before its storage-v3 header", + display_path.display() + ))); + } + let magic = usize::from_le_bytes( + bytes[..word] + .try_into() + .map_err(|_| RusticolError::internal("could not decode SymJIT application magic"))?, + ); + if magic != SYMJIT_APPLICATION_MAGIC { + return Err(RusticolError::compatibility(format!( + "SymJIT plane application {} has invalid application magic", + display_path.display() + ))); + } + let version = usize::from_le_bytes( + bytes[word..header_len] + .try_into() + .map_err(|_| RusticolError::internal("could not decode SymJIT storage version"))?, + ); + if version != SYMJIT_APPLICATION_STORAGE_VERSION { + return Err(RusticolError::compatibility(format!( + "SymJIT plane application {} uses storage version {version}, expected {SYMJIT_APPLICATION_STORAGE_VERSION}", + display_path.display() + ))); + } + Ok(()) +} + +fn validate_application( + application: &Application, + display_path: &Path, + layout: SymjitPlaneLayout, +) -> RusticolResult { + let config = &application.config; + if config.is_enabled_simd512() { + return Err(RusticolError::compatibility(format!( + "SymJIT plane application {} enables unsupported SIMD512 code generation; \ + regenerate it for the portable scalar/f64x2/f64x4 arena ABI", + display_path.display() + ))); + } + let optimization_level = canonical_plane_optimization_level(config, display_path, layout)?; + if !config.symbolica() + || !config.direct_arena() + || config.direct() + || config.direct_arena_operation() != 0 + || !config.direct_arena_identity_output() + { + return Err(RusticolError::compatibility(format!( + "SymJIT application {} is not an identity-overwrite indirect Symbolica \ + direct-arena P-kernel", + display_path.display() + ))); + } + if config.is_complex() != layout.is_complex() { + return Err(RusticolError::integrity(format!( + "SymJIT plane application {} complex mode does not match its binding layout", + display_path.display() + ))); + } + let input_plane_count = layout.input_plane_count()?; + let output_plane_count = layout.output_plane_count()?; + if application.count_states != input_plane_count { + return Err(RusticolError::integrity(format!( + "SymJIT plane application {} stores {} input planes, expected {}", + display_path.display(), + application.count_states, + input_plane_count + ))); + } + if application.count_obs != output_plane_count { + return Err(RusticolError::integrity(format!( + "SymJIT plane application {} stores {} output planes, expected {}", + display_path.display(), + application.count_obs, + output_plane_count + ))); + } + if application.count_params != 0 { + return Err(RusticolError::compatibility(format!( + "SymJIT plane application {} retains {} scalar parameters; direct-arena inputs must all be states", + display_path.display(), + application.count_params + ))); + } + if application.count_diffs != 0 { + return Err(RusticolError::compatibility(format!( + "SymJIT plane application {} contains differential outputs", + display_path.display() + ))); + } + Ok(optimization_level) +} + +fn canonical_plane_optimization_level( + config: &Config, + display_path: &Path, + layout: SymjitPlaneLayout, +) -> RusticolResult { + if config.ty != CompilerType::Native { + return Err(RusticolError::compatibility(format!( + "SymJIT plane application {} uses a non-native compiler target; \ + regenerate it with the canonical pyAmpliCol plane configuration", + display_path.display() + ))); + } + let mut canonical_optimization_level = None; + for optimization_level in 0..=MAX_OPTIMIZATION_LEVEL { + let expected = + explicit_plane_config(layout.is_complex(), optimization_level, config.compress())?; + if config.opt == expected.opt { + canonical_optimization_level = Some(optimization_level); + break; + } + } + canonical_optimization_level.ok_or_else(|| { + RusticolError::compatibility(format!( + "SymJIT plane application {} uses noncanonical compiler options; \ + regenerate it with the canonical pyAmpliCol plane configuration", + display_path.display() + )) + }) +} + +fn validate_simd_lane_width( + has_simd_kernel: bool, + simd_lanes: usize, + display_path: &Path, +) -> RusticolResult<()> { + let valid = if has_simd_kernel { + matches!(simd_lanes, 2 | 4) + } else { + simd_lanes == 1 + }; + if !valid { + return Err(RusticolError::compatibility(format!( + "SymJIT plane application {} reports unsupported SIMD lane width {simd_lanes}; \ + expected scalar fallback, f64x2, or f64x4", + display_path.display() + ))); + } + Ok(()) +} + +fn validate_compiled_kernels(application: &Application, display_path: &Path) -> RusticolResult<()> { + let scalar = application.compiled.as_ref().ok_or_else(|| { + RusticolError::compatibility(format!( + "SymJIT plane application {} has no scalar native kernel", + display_path.display() + )) + })?; + if !scalar.support_indirect() { + return Err(RusticolError::compatibility(format!( + "SymJIT plane application {} scalar kernel does not support indirect plane descriptors", + display_path.display() + ))); + } + if let Some(simd) = application.compiled_simd.as_ref() + && (!simd.support_indirect() || simd.count_lanes() <= 1) + { + return Err(RusticolError::compatibility(format!( + "SymJIT plane application {} has an invalid indirect SIMD kernel", + display_path.display() + ))); + } + Ok(()) +} + +#[cfg(test)] +fn validate_slice_table(layout: SymjitPlaneLayout, planes: &[&mut [f64]]) -> RusticolResult<()> { + let expected = layout.plane_count()?; + if planes.len() != expected { + return Err(RusticolError::invalid_argument(format!( + "SymJIT plane table has {} planes, expected {expected}", + planes.len() + ))); + } + let point_count = planes.first().map_or(0, |plane| plane.len()); + if let Some((index, plane)) = planes + .iter() + .enumerate() + .find(|(_, plane)| plane.len() != point_count) + { + return Err(RusticolError::invalid_argument(format!( + "SymJIT plane {index} has length {}, expected {point_count}", + plane.len() + ))); + } + Ok(()) +} + +fn validate_raw_table( + layout: SymjitPlaneLayout, + planes: &[SymjitRawPlane<'_>], +) -> RusticolResult<()> { + let expected = layout.plane_count()?; + validate_raw_plane_count_and_ranges(expected, planes) +} + +fn validate_raw_plane_count_and_ranges( + expected: usize, + planes: &[SymjitRawPlane<'_>], +) -> RusticolResult<()> { + if planes.len() != expected { + return Err(RusticolError::invalid_argument(format!( + "SymJIT raw plane table has {} planes, expected {expected}", + planes.len() + ))); + } + let point_count = planes.first().map_or(0, PlaneDescriptor::len); + for (index, plane) in planes.iter().enumerate() { + if plane.len() != point_count { + return Err(RusticolError::invalid_argument(format!( + "SymJIT raw plane {index} has length {}, expected {point_count}", + plane.len() + ))); + } + let _ = plane.interval()?; + } + Ok(()) +} + +fn validate_raw_aliases( + input_plane_count: usize, + output_plane_count: usize, + planes: &[SymjitRawPlane<'_>], +) -> RusticolResult { + let expected = input_plane_count + .checked_add(output_plane_count) + .ok_or_else(|| RusticolError::invalid_argument("SymJIT plane count overflows usize"))?; + if planes.len() != expected { + return Err(RusticolError::invalid_argument(format!( + "SymJIT raw plane table has {} planes, expected {expected}", + planes.len() + ))); + } + + let (inputs, outputs) = planes.split_at(input_plane_count); + for (index, output) in outputs.iter().enumerate() { + let Some(output_interval) = output.interval()? else { + continue; + }; + for other in &outputs[index + 1..] { + let Some(other_interval) = other.interval()? else { + continue; + }; + if output_interval.0 < other_interval.1 && other_interval.0 < output_interval.1 { + return Err(RusticolError::invalid_argument( + "SymJIT output planes overlap; output planes must be mutually disjoint", + )); + } + } + } + + let mut disjoint = true; + for input in inputs { + let Some(input_interval) = input.interval()? else { + continue; + }; + for output in outputs { + let Some(output_interval) = output.interval()? else { + continue; + }; + if input_interval.0 < output_interval.1 && output_interval.0 < input_interval.1 { + if input_interval == output_interval { + disjoint = false; + continue; + } + return Err(RusticolError::invalid_argument( + "a SymJIT input/output plane pair partially overlaps; only disjoint planes or exact aliases are supported", + )); + } + } + } + Ok(disjoint) +} + +fn guard_symjit_panic( + operation: impl FnOnce() -> T, + action: impl FnOnce() -> String, +) -> RusticolResult { + catch_unwind(AssertUnwindSafe(operation)).map_err(|payload| { + RusticolError::compatibility(format!( + "SymJIT panicked while trying to {}: {}", + action(), + panic_detail(payload) + )) + }) +} + +fn panic_detail(payload: Box) -> String { + if let Some(message) = payload.downcast_ref::<&str>() { + (*message).to_string() + } else if let Some(message) = payload.downcast_ref::() { + message.clone() + } else { + "non-string panic payload".to_string() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::RusticolErrorKind; + + const MODEL: &str = " +([('fun', ('temp', 0), 'square', [], [('param', 0)], False), + ('add', ('temp', 1), [('temp', 0), ('param', 1)], 0), + ('assign', ('out', 0), ('temp', 1))], + 2, + []) +"; + const DIVERGENT_MODEL: &str = " +([('if_else', ('param', 0), 2), + ('goto', 3), + ('label', 2), + ('label', 3), + ('join', ('out', 0), ('param', 0), ('param', 1), ('param', 2))], + 2, + []) +"; + const LENGTHS: &[usize] = &[1, 2, 3, 7, 8, 127, 128, 129, 1023, 1024, 1025]; + const SENTINEL: f64 = -9.876_543_210_123_456e211; + + fn application_bytes(complex: bool, opt_level: u8) -> Vec { + let config = explicit_plane_config(complex, opt_level, true).unwrap(); + application_bytes_with_config(config) + } + + fn application_bytes_with_config(config: Config) -> Vec { + let mut compiler = Compiler::with_config(config); + let mut application = compiler.translate(MODEL.to_string(), 2).unwrap(); + application.prepare_simd(); + let mut bytes = Vec::new(); + application.save(&mut bytes).unwrap(); + bytes + } + + fn divergent_application_bytes() -> Vec { + let config = explicit_plane_config(false, 3, true).unwrap(); + let mut compiler = Compiler::with_config(config); + let mut application = compiler.translate(DIVERGENT_MODEL.to_string(), 3).unwrap(); + application.prepare_simd(); + let mut bytes = Vec::new(); + application.save(&mut bytes).unwrap(); + bytes + } + + fn guarded_plane(len: usize, values: impl Fn(usize) -> f64) -> Vec { + let mut plane = vec![SENTINEL; len + 2]; + for index in 0..len { + plane[index + 1] = values(index); + } + plane + } + + fn assert_guards(plane: &[f64]) { + assert_eq!(plane.first().copied(), Some(SENTINEL)); + assert_eq!(plane.last().copied(), Some(SENTINEL)); + } + + fn assert_native_simd_lanes(kernel: &SymjitPlaneKernel) { + #[cfg(target_arch = "aarch64")] + assert_eq!(kernel.simd_lanes(), 2); + #[cfg(target_arch = "x86_64")] + assert_eq!( + kernel.simd_lanes(), + if std::is_x86_feature_detected!("avx") { + 4 + } else { + 1 + } + ); + #[cfg(not(any(target_arch = "aarch64", target_arch = "x86_64")))] + assert_eq!(kernel.simd_lanes(), 1); + } + + fn execute_forced_scalar(kernel: &SymjitPlaneKernel, table: &SymjitPlaneTable<'_>, len: usize) { + for point in 0..len { + let status = unsafe { + (kernel.scalar)( + std::ptr::null(), + table.as_symjit_ptr(), + point, + kernel.applet.params.as_ptr(), + ) + }; + assert_eq!(status, 0, "scalar status at point {point}"); + } + } + + fn execute_forced_simd(kernel: &SymjitPlaneKernel, table: &SymjitPlaneTable<'_>, len: usize) { + let mut point = 0; + let mut simd_call_count = 0; + if let Some(simd) = kernel.simd.filter(|_| kernel.simd_lanes() > 1) { + while len - point >= kernel.simd_lanes() { + let status = unsafe { + simd( + std::ptr::null(), + table.as_symjit_ptr(), + point, + kernel.applet.params.as_ptr(), + ) + }; + assert_eq!(status, 0, "SIMD status at row {point}"); + point += kernel.simd_lanes(); + simd_call_count += 1; + } + } + if kernel.simd_lanes() > 1 && len >= kernel.simd_lanes() { + assert!(simd_call_count > 0, "forced SIMD path executed no blocks"); + } + while point < len { + let status = unsafe { + (kernel.scalar)( + std::ptr::null(), + table.as_symjit_ptr(), + point, + kernel.applet.params.as_ptr(), + ) + }; + assert_eq!(status, 0, "scalar tail status at point {point}"); + point += 1; + } + } + + #[test] + fn real_forced_scalar_and_simd_preserve_sentinels_for_all_lengths() { + let bytes = application_bytes(false, 3); + let kernel = SymjitPlaneKernel::load_bytes( + &bytes, + "real-contract.symjit", + SymjitPlaneLayout::real(2, 1), + ) + .unwrap(); + assert_eq!(kernel.input_plane_count(), 2); + assert_eq!(kernel.output_plane_count(), 1); + assert_native_simd_lanes(&kernel); + + for &len in LENGTHS { + let mut x = guarded_plane(len, |i| i as f64 * 0.25 - 3.0); + let mut y = guarded_plane(len, |i| 7.0 - i as f64 * 0.125); + let mut scalar_out = guarded_plane(len, |_| f64::NAN); + let mut simd_out = guarded_plane(len, |_| f64::NAN); + { + let table = kernel + .build_raw_table(vec![ + PlaneDescriptor::from_slice(&mut x[1..len + 1]), + PlaneDescriptor::from_slice(&mut y[1..len + 1]), + PlaneDescriptor::from_slice(&mut scalar_out[1..len + 1]), + ]) + .unwrap(); + assert_eq!(table.point_count(), len); + execute_forced_scalar(&kernel, &table, len); + } + { + let table = kernel + .build_raw_table(vec![ + PlaneDescriptor::from_slice(&mut x[1..len + 1]), + PlaneDescriptor::from_slice(&mut y[1..len + 1]), + PlaneDescriptor::from_slice(&mut simd_out[1..len + 1]), + ]) + .unwrap(); + execute_forced_simd(&kernel, &table, len); + } + for index in 0..len { + let expected = x[index + 1] * x[index + 1] + y[index + 1]; + assert_eq!( + scalar_out[index + 1], + expected, + "scalar length {len}, point {index}" + ); + assert_eq!( + simd_out[index + 1], + expected, + "SIMD length {len}, point {index}" + ); + } + assert_guards(&x); + assert_guards(&y); + assert_guards(&scalar_out); + assert_guards(&simd_out); + } + } + + #[test] + fn complex_forced_scalar_and_simd_cover_every_length() { + let bytes = + compile_symbolica_program_to_plane_application_bytes(MODEL, 2, 1, 3, true).unwrap(); + let kernel = SymjitPlaneKernel::load_bytes( + &bytes, + "complex-contract.symjit", + SymjitPlaneLayout::complex(2, 1), + ) + .unwrap(); + assert_eq!(kernel.input_plane_count(), 4); + assert_eq!(kernel.output_plane_count(), 2); + assert_native_simd_lanes(&kernel); + + for &len in LENGTHS { + let mut x_re = guarded_plane(len, |i| i as f64 * 0.25 - 1.0); + let mut x_im = guarded_plane(len, |i| 0.5 + i as f64 * 0.0625); + let mut y_re = guarded_plane(len, |i| 2.0 - i as f64 * 0.125); + let mut y_im = guarded_plane(len, |i| -0.75 + i as f64 * 0.03125); + let mut scalar_out_re = guarded_plane(len, |_| f64::NAN); + let mut scalar_out_im = guarded_plane(len, |_| f64::NAN); + let mut simd_out_re = guarded_plane(len, |_| f64::NAN); + let mut simd_out_im = guarded_plane(len, |_| f64::NAN); + { + let table = kernel + .build_raw_table(vec![ + PlaneDescriptor::from_slice(&mut x_re[1..len + 1]), + PlaneDescriptor::from_slice(&mut x_im[1..len + 1]), + PlaneDescriptor::from_slice(&mut y_re[1..len + 1]), + PlaneDescriptor::from_slice(&mut y_im[1..len + 1]), + PlaneDescriptor::from_slice(&mut scalar_out_re[1..len + 1]), + PlaneDescriptor::from_slice(&mut scalar_out_im[1..len + 1]), + ]) + .unwrap(); + execute_forced_scalar(&kernel, &table, len); + } + { + let table = kernel + .build_raw_table(vec![ + PlaneDescriptor::from_slice(&mut x_re[1..len + 1]), + PlaneDescriptor::from_slice(&mut x_im[1..len + 1]), + PlaneDescriptor::from_slice(&mut y_re[1..len + 1]), + PlaneDescriptor::from_slice(&mut y_im[1..len + 1]), + PlaneDescriptor::from_slice(&mut simd_out_re[1..len + 1]), + PlaneDescriptor::from_slice(&mut simd_out_im[1..len + 1]), + ]) + .unwrap(); + execute_forced_simd(&kernel, &table, len); + } + for index in 0..len { + let xr = x_re[index + 1]; + let xi = x_im[index + 1]; + let expected_re = xr * xr - xi * xi + y_re[index + 1]; + let expected_im = 2.0 * xr * xi + y_im[index + 1]; + for (lane, actual_re, actual_im) in [ + ("scalar", scalar_out_re[index + 1], scalar_out_im[index + 1]), + ("SIMD", simd_out_re[index + 1], simd_out_im[index + 1]), + ] { + assert_eq!( + actual_re, expected_re, + "{lane} length {len}, point {index}, real" + ); + assert_eq!( + actual_im, expected_im, + "{lane} length {len}, point {index}, imaginary" + ); + } + } + for plane in [ + &x_re, + &x_im, + &y_re, + &y_im, + &scalar_out_re, + &scalar_out_im, + &simd_out_re, + &simd_out_im, + ] { + assert_guards(plane); + } + } + } + + #[test] + fn unaligned_subrange_uses_scalar_head_and_tail() { + let bytes = application_bytes(false, 2); + let kernel = SymjitPlaneKernel::load_bytes( + &bytes, + "unaligned-contract.symjit", + SymjitPlaneLayout::real(2, 1), + ) + .unwrap(); + let len = 19; + let mut x: Vec = (0..len).map(|i| i as f64 + 1.0).collect(); + let mut y: Vec = (0..len).map(|i| i as f64 * 0.5).collect(); + let mut out = vec![SENTINEL; len]; + { + let mut bound = kernel + .bind(vec![x.as_mut_slice(), y.as_mut_slice(), out.as_mut_slice()]) + .unwrap(); + bound.execute_range(1, len - 3).unwrap(); + } + assert_eq!(out[0], SENTINEL); + assert_eq!(out[len - 2], SENTINEL); + assert_eq!(out[len - 1], SENTINEL); + for index in 1..len - 2 { + assert_eq!(out[index], x[index] * x[index] + y[index]); + } + } + + #[test] + fn raw_binding_executes_duplicate_inputs_without_overlapping_references() { + let bytes = application_bytes(false, 2); + let kernel = SymjitPlaneKernel::load_bytes( + &bytes, + "duplicate-contract.symjit", + SymjitPlaneLayout::real(2, 1), + ) + .unwrap(); + for &len in LENGTHS { + let mut input: Vec = (1..=len).map(|value| value as f64).collect(); + let mut output = vec![0.0; len]; + let duplicate = input.as_mut_ptr(); + { + let mut bound = kernel + .bind_raw(vec![ + // SAFETY: Both descriptors share live, synchronized + // read-only input storage for the duration of execution. + unsafe { PlaneDescriptor::from_raw_parts(duplicate, input.len()).unwrap() }, + unsafe { PlaneDescriptor::from_raw_parts(duplicate, input.len()).unwrap() }, + PlaneDescriptor::from_slice(output.as_mut_slice()), + ]) + .unwrap(); + bound.execute_all().unwrap(); + } + for (index, value) in input.iter().copied().enumerate() { + assert_eq!( + output[index], + value * value + value, + "length {len}, point {index}" + ); + } + } + } + + #[test] + fn raw_binding_executes_input_output_alias_from_prologue_snapshot() { + let bytes = application_bytes(false, 2); + let kernel = SymjitPlaneKernel::load_bytes( + &bytes, + "alias-contract.symjit", + SymjitPlaneLayout::real(2, 1), + ) + .unwrap(); + for &len in LENGTHS { + let mut first: Vec = (1..=len).map(|value| value as f64).collect(); + let original_first = first.clone(); + let mut second: Vec = (0..len).map(|index| 10.0 + index as f64).collect(); + let alias = first.as_mut_ptr(); + { + let mut bound = kernel + .bind_raw(vec![ + // SAFETY: The P-kernel snapshots its inputs in the + // prologue before overwriting the aliased output. + unsafe { PlaneDescriptor::from_raw_parts(alias, first.len()).unwrap() }, + PlaneDescriptor::from_slice(second.as_mut_slice()), + unsafe { PlaneDescriptor::from_raw_parts(alias, first.len()).unwrap() }, + ]) + .unwrap(); + assert!(!bound.table.simd_safe()); + bound.execute_all().unwrap(); + } + for index in 0..first.len() { + assert_eq!( + first[index], + original_first[index] * original_first[index] + second[index], + "length {len}, point {index}" + ); + } + } + } + + #[test] + fn complex_raw_binding_supports_duplicate_inputs_for_all_lengths() { + let bytes = + compile_symbolica_program_to_plane_application_bytes(MODEL, 2, 1, 2, true).unwrap(); + let kernel = SymjitPlaneKernel::load_bytes( + &bytes, + "complex-duplicate-contract.symjit", + SymjitPlaneLayout::complex(2, 1), + ) + .unwrap(); + for &len in LENGTHS { + let mut input_re: Vec = (0..len).map(|index| 0.5 + index as f64 * 0.25).collect(); + let mut input_im: Vec = + (0..len).map(|index| -0.75 + index as f64 * 0.125).collect(); + let mut output_re = vec![0.0; len]; + let mut output_im = vec![0.0; len]; + let input_re_pointer = input_re.as_mut_ptr(); + let input_im_pointer = input_im.as_mut_ptr(); + { + let mut bound = kernel + .bind_raw(vec![ + unsafe { + PlaneDescriptor::from_raw_parts(input_re_pointer, input_re.len()) + .unwrap() + }, + unsafe { + PlaneDescriptor::from_raw_parts(input_im_pointer, input_im.len()) + .unwrap() + }, + unsafe { + PlaneDescriptor::from_raw_parts(input_re_pointer, input_re.len()) + .unwrap() + }, + unsafe { + PlaneDescriptor::from_raw_parts(input_im_pointer, input_im.len()) + .unwrap() + }, + PlaneDescriptor::from_slice(output_re.as_mut_slice()), + PlaneDescriptor::from_slice(output_im.as_mut_slice()), + ]) + .unwrap(); + bound.execute_all().unwrap(); + } + for index in 0..len { + let real = input_re[index]; + let imaginary = input_im[index]; + let expected_re = real * real - imaginary * imaginary + real; + let expected_im = 2.0 * real * imaginary + imaginary; + assert_eq!( + output_re[index], expected_re, + "length {len}, point {index}, real" + ); + assert_eq!( + output_im[index], expected_im, + "length {len}, point {index}, imaginary" + ); + } + } + } + + #[test] + fn complex_raw_binding_supports_exact_output_aliases_for_all_lengths() { + let bytes = + compile_symbolica_program_to_plane_application_bytes(MODEL, 2, 1, 2, true).unwrap(); + let kernel = SymjitPlaneKernel::load_bytes( + &bytes, + "complex-alias-contract.symjit", + SymjitPlaneLayout::complex(2, 1), + ) + .unwrap(); + for &len in LENGTHS { + let mut first_re: Vec = (0..len).map(|index| 1.0 + index as f64 * 0.25).collect(); + let mut first_im: Vec = + (0..len).map(|index| -0.5 + index as f64 * 0.125).collect(); + let original_first: Vec<(f64, f64)> = first_re + .iter() + .copied() + .zip(first_im.iter().copied()) + .collect(); + let mut second_re: Vec = + (0..len).map(|index| 3.0 - index as f64 * 0.125).collect(); + let mut second_im: Vec = + (0..len).map(|index| 0.25 + index as f64 * 0.0625).collect(); + let first_re_pointer = first_re.as_mut_ptr(); + let first_im_pointer = first_im.as_mut_ptr(); + { + let mut bound = kernel + .bind_raw(vec![ + unsafe { + PlaneDescriptor::from_raw_parts(first_re_pointer, first_re.len()) + .unwrap() + }, + unsafe { + PlaneDescriptor::from_raw_parts(first_im_pointer, first_im.len()) + .unwrap() + }, + PlaneDescriptor::from_slice(second_re.as_mut_slice()), + PlaneDescriptor::from_slice(second_im.as_mut_slice()), + unsafe { + PlaneDescriptor::from_raw_parts(first_re_pointer, first_re.len()) + .unwrap() + }, + unsafe { + PlaneDescriptor::from_raw_parts(first_im_pointer, first_im.len()) + .unwrap() + }, + ]) + .unwrap(); + assert!(!bound.table.simd_safe()); + bound.execute_all().unwrap(); + } + for index in 0..len { + let (real, imaginary) = original_first[index]; + let expected_re = real * real - imaginary * imaginary + second_re[index]; + let expected_im = 2.0 * real * imaginary + second_im[index]; + assert_eq!( + first_re[index], expected_re, + "length {len}, point {index}, real" + ); + assert_eq!( + first_im[index], expected_im, + "length {len}, point {index}, imaginary" + ); + } + } + } + + #[test] + fn shifted_input_output_overlap_is_rejected() { + let bytes = application_bytes(false, 2); + let kernel = SymjitPlaneKernel::load_bytes( + &bytes, + "partial-overlap-contract.symjit", + SymjitPlaneLayout::real(2, 1), + ) + .unwrap(); + let len = 8; + let mut overlapping = vec![1.0; len + 1]; + let base = overlapping.as_mut_ptr(); + let checked = [ + unsafe { PlaneDescriptor::from_raw_parts(base, len).unwrap() }, + unsafe { PlaneDescriptor::from_raw_parts(base.add(1), len).unwrap() }, + unsafe { PlaneDescriptor::from_raw_parts(base, len).unwrap() }, + ]; + let checked_error = kernel.validate_raw_descriptors(&checked).unwrap_err(); + assert_eq!(checked_error.kind(), RusticolErrorKind::InvalidArgument); + assert!(checked_error.message().contains("partially overlaps")); + + let error = kernel + .bind_raw(vec![ + unsafe { PlaneDescriptor::from_raw_parts(base, len).unwrap() }, + unsafe { PlaneDescriptor::from_raw_parts(base.add(1), len).unwrap() }, + unsafe { PlaneDescriptor::from_raw_parts(base, len).unwrap() }, + ]) + .unwrap_err(); + assert_eq!(error.kind(), RusticolErrorKind::InvalidArgument); + assert!(error.message().contains("partially overlaps")); + assert!(error.message().contains("exact aliases")); + } + + #[test] + fn overlapping_output_planes_are_rejected() { + let len = 8; + let mut input = vec![1.0; len]; + let mut overlapping_outputs = vec![0.0; len + 1]; + let output_base = overlapping_outputs.as_mut_ptr(); + let descriptors = [ + PlaneDescriptor::from_slice(input.as_mut_slice()), + unsafe { PlaneDescriptor::from_raw_parts(output_base, len).unwrap() }, + unsafe { PlaneDescriptor::from_raw_parts(output_base.add(1), len).unwrap() }, + ]; + let error = validate_raw_aliases(1, 2, &descriptors).unwrap_err(); + assert_eq!(error.kind(), RusticolErrorKind::InvalidArgument); + assert!(error.message().contains("output planes overlap")); + assert!(error.message().contains("mutually disjoint")); + } + + #[test] + fn divergent_simd_block_replays_every_lane_with_the_scalar_kernel() { + let bytes = divergent_application_bytes(); + let kernel = SymjitPlaneKernel::load_bytes( + &bytes, + "divergent-contract.symjit", + SymjitPlaneLayout::real(3, 1), + ) + .unwrap(); + let len = kernel.simd_lanes().max(2) * 2; + let mut condition: Vec = (0..len) + .map(|index| if index.is_multiple_of(2) { 1.0 } else { 0.0 }) + .collect(); + let mut if_true: Vec = (0..len).map(|index| 10.0 + index as f64).collect(); + let mut if_false: Vec = (0..len).map(|index| -20.0 - index as f64).collect(); + let mut output = vec![SENTINEL; len]; + + let mut table = kernel + .build_raw_table(vec![ + PlaneDescriptor::from_slice(condition.as_mut_slice()), + PlaneDescriptor::from_slice(if_true.as_mut_slice()), + PlaneDescriptor::from_slice(if_false.as_mut_slice()), + PlaneDescriptor::from_slice(output.as_mut_slice()), + ]) + .unwrap(); + assert!(table.simd_safe()); + + if let Some(simd) = kernel.simd.filter(|_| kernel.simd_lanes() > 1) { + // Establish that this input really exercises SymJIT's nonzero + // lane-divergence status rather than only the scalar fallback. + let status = unsafe { + simd( + std::ptr::null(), + table.as_symjit_ptr(), + 0, + kernel.applet.params.as_ptr(), + ) + }; + assert_ne!(status, 0); + } + + kernel.execute_table(&mut table, 0, len).unwrap(); + for index in 0..len { + assert_eq!( + output[index], + if condition[index] != 0.0 { + if_true[index] + } else { + if_false[index] + } + ); + } + } + + #[test] + fn divergent_input_output_alias_disables_simd_before_execution() { + let bytes = divergent_application_bytes(); + let kernel = SymjitPlaneKernel::load_bytes( + &bytes, + "divergent-alias-contract.symjit", + SymjitPlaneLayout::real(3, 1), + ) + .unwrap(); + let len = kernel.simd_lanes().max(2) * 2; + let mut condition_and_output: Vec = (0..len) + .map(|index| if index.is_multiple_of(2) { 1.0 } else { 0.0 }) + .collect(); + let original_condition = condition_and_output.clone(); + let mut if_true: Vec = (0..len).map(|index| 30.0 + index as f64).collect(); + let mut if_false: Vec = (0..len).map(|index| -40.0 - index as f64).collect(); + let alias = condition_and_output.as_mut_ptr(); + + let mut table = kernel + .build_raw_table(vec![ + unsafe { + PlaneDescriptor::from_raw_parts(alias, condition_and_output.len()).unwrap() + }, + PlaneDescriptor::from_slice(if_true.as_mut_slice()), + PlaneDescriptor::from_slice(if_false.as_mut_slice()), + unsafe { + PlaneDescriptor::from_raw_parts(alias, condition_and_output.len()).unwrap() + }, + ]) + .unwrap(); + assert!(!table.simd_safe()); + + kernel.execute_table(&mut table, 0, len).unwrap(); + for index in 0..len { + assert_eq!( + condition_and_output[index], + if original_condition[index] != 0.0 { + if_true[index] + } else { + if_false[index] + } + ); + } + } + + #[test] + fn disjoint_raw_binding_executes_without_descriptor_repacking() { + let bytes = application_bytes(false, 2); + let kernel = SymjitPlaneKernel::load_bytes( + &bytes, + "raw-contract.symjit", + SymjitPlaneLayout::real(2, 1), + ) + .unwrap(); + let mut x = vec![2.0, 3.0, 4.0, 5.0]; + let mut y = vec![1.0, 2.0, 3.0, 4.0]; + let mut out = vec![0.0; 4]; + { + let mut table = kernel + .build_raw_table(vec![ + PlaneDescriptor::from_slice(x.as_mut_slice()), + PlaneDescriptor::from_slice(y.as_mut_slice()), + PlaneDescriptor::from_slice(out.as_mut_slice()), + ]) + .unwrap(); + assert_eq!(table.point_count(), 4); + let table_address = table.as_symjit_ptr().addr(); + kernel.execute_table(&mut table, 0, 4).unwrap(); + assert_eq!(table_address, table.as_symjit_ptr().addr()); + } + assert_eq!(out, [5.0, 11.0, 19.0, 29.0]); + } + + #[test] + fn raw_table_rebind_preserves_descriptor_allocation_and_executes_new_planes() { + let bytes = application_bytes(false, 2); + let kernel = SymjitPlaneKernel::load_bytes( + &bytes, + "raw-rebind-contract.symjit", + SymjitPlaneLayout::real(2, 1), + ) + .unwrap(); + let mut first_x = vec![2.0, 3.0, 4.0, 5.0]; + let mut first_y = vec![1.0, 2.0, 3.0, 4.0]; + let mut first_out = vec![0.0; 4]; + let mut second_x = vec![6.0, 7.0, 8.0, 9.0]; + let mut second_y = vec![5.0, 6.0, 7.0, 8.0]; + let mut second_out = vec![0.0; 4]; + + { + let mut table = kernel + .build_raw_table(vec![ + PlaneDescriptor::from_slice(first_x.as_mut_slice()), + PlaneDescriptor::from_slice(first_y.as_mut_slice()), + PlaneDescriptor::from_slice(first_out.as_mut_slice()), + ]) + .unwrap(); + let table_address = table.as_symjit_ptr().addr(); + kernel.execute_table(&mut table, 0, 4).unwrap(); + + let replacement = [ + // SAFETY: Each allocation is live, aligned, synchronized, and + // retains its length until the table is dropped. + unsafe { + PlaneDescriptor::from_raw_parts(second_x.as_mut_ptr(), second_x.len()).unwrap() + }, + unsafe { + PlaneDescriptor::from_raw_parts(second_y.as_mut_ptr(), second_y.len()).unwrap() + }, + unsafe { + PlaneDescriptor::from_raw_parts(second_out.as_mut_ptr(), second_out.len()) + .unwrap() + }, + ]; + // SAFETY: No kernel invocation overlaps the rebind and all + // replacement allocations outlive the table. + unsafe { table.rebind_raw(&replacement) }.unwrap(); + assert_eq!(table_address, table.as_symjit_ptr().addr()); + kernel.execute_table(&mut table, 0, 4).unwrap(); + assert_eq!(table_address, table.as_symjit_ptr().addr()); + } + + assert_eq!(first_out, [5.0, 11.0, 19.0, 29.0]); + assert_eq!(second_out, [41.0, 55.0, 71.0, 89.0]); + } + + #[test] + fn raw_table_rebind_recomputes_input_output_alias_simd_safety() { + let bytes = application_bytes(false, 2); + let kernel = SymjitPlaneKernel::load_bytes( + &bytes, + "raw-rebind-alias-contract.symjit", + SymjitPlaneLayout::real(2, 1), + ) + .unwrap(); + let mut x = vec![2.0, 3.0, 4.0, 5.0]; + let mut y = vec![1.0, 2.0, 3.0, 4.0]; + let mut first_output = vec![0.0; 4]; + let mut second_output = vec![0.0; 4]; + let x_pointer = x.as_mut_ptr(); + let y_pointer = y.as_mut_ptr(); + let first_output_pointer = first_output.as_mut_ptr(); + let second_output_pointer = second_output.as_mut_ptr(); + + let mut table = kernel + .build_raw_table(vec![ + unsafe { PlaneDescriptor::from_raw_parts(x_pointer, x.len()).unwrap() }, + unsafe { PlaneDescriptor::from_raw_parts(y_pointer, y.len()).unwrap() }, + unsafe { + PlaneDescriptor::from_raw_parts(first_output_pointer, first_output.len()) + .unwrap() + }, + ]) + .unwrap(); + assert!(table.simd_safe()); + + let aliased = [ + unsafe { PlaneDescriptor::from_raw_parts(x_pointer, x.len()).unwrap() }, + unsafe { PlaneDescriptor::from_raw_parts(y_pointer, y.len()).unwrap() }, + unsafe { PlaneDescriptor::from_raw_parts(x_pointer, x.len()).unwrap() }, + ]; + unsafe { table.rebind_raw(&aliased) }.unwrap(); + assert!(!table.simd_safe()); + + let disjoint = [ + unsafe { PlaneDescriptor::from_raw_parts(x_pointer, x.len()).unwrap() }, + unsafe { PlaneDescriptor::from_raw_parts(y_pointer, y.len()).unwrap() }, + unsafe { + PlaneDescriptor::from_raw_parts(second_output_pointer, second_output.len()).unwrap() + }, + ]; + unsafe { table.rebind_raw(&disjoint) }.unwrap(); + assert!(table.simd_safe()); + } + + unsafe fn status_kernel( + _mem: *const f64, + _states: *const SymjitPlaneDescriptor, + index: usize, + _params: *const f64, + ) -> i32 { + if index == 2 { 17 } else { 0 } + } + + unsafe fn replay_scalar_kernel( + _mem: *const f64, + states: *const SymjitPlaneDescriptor, + index: usize, + _params: *const f64, + ) -> i32 { + unsafe { + let input = *(*states).data.add(index); + *(*states.add(1)).data.add(index) = input + 0.5; + } + 0 + } + + unsafe fn divergent_vector_kernel( + _mem: *const f64, + states: *const SymjitPlaneDescriptor, + point: usize, + _params: *const f64, + ) -> i32 { + unsafe { + for lane in 0..4 { + *(*states.add(1)).data.add(point + lane) = SENTINEL; + } + } + 1 + } + + #[test] + fn synthetic_divergence_status_replays_the_whole_simd_block() { + let mut input: Vec = (0..11).map(|index| index as f64).collect(); + let mut output = vec![SENTINEL; input.len()]; + let descriptors = [ + unsafe { SymjitPlaneDescriptor::from_raw_parts(input.as_mut_ptr(), input.len()) }, + unsafe { SymjitPlaneDescriptor::from_raw_parts(output.as_mut_ptr(), output.len()) }, + ]; + + let failure = invoke_range( + replay_scalar_kernel, + Some(divergent_vector_kernel), + 4, + descriptors.as_ptr(), + std::ptr::null(), + 1, + input.len() - 1, + ) + .unwrap(); + assert_eq!(failure, None); + assert_eq!(output[0], SENTINEL); + assert_eq!(output[input.len() - 1], SENTINEL); + for index in 1..input.len() - 1 { + assert_eq!(output[index], input[index] + 0.5); + } + } + + unsafe fn replay_status_kernel( + _mem: *const f64, + _states: *const SymjitPlaneDescriptor, + index: usize, + _params: *const f64, + ) -> i32 { + if index == 5 { 29 } else { 0 } + } + + unsafe fn divergence_status_only_kernel( + _mem: *const f64, + _states: *const SymjitPlaneDescriptor, + _point: usize, + _params: *const f64, + ) -> i32 { + 1 + } + + #[test] + fn scalar_status_during_divergent_replay_is_reported_at_the_point() { + let failure = invoke_range( + replay_status_kernel, + Some(divergence_status_only_kernel), + 4, + std::ptr::null(), + std::ptr::null(), + 4, + 8, + ) + .unwrap() + .unwrap(); + assert_eq!( + failure, + StatusFailure { + index: 5, + status: 29, + } + ); + } + + #[test] + fn nonzero_kernel_status_is_contained() { + let failure = invoke_range( + status_kernel, + None, + 1, + std::ptr::null(), + std::ptr::null(), + 0, + 4, + ) + .unwrap() + .unwrap(); + assert_eq!( + failure, + StatusFailure { + index: 2, + status: 17, + } + ); + } + + #[test] + fn symjit_panics_are_contained_with_dependency_context() { + let error = guard_symjit_panic( + || panic!("probe panic"), + || "exercise the panic-containment probe".to_string(), + ) + .unwrap_err(); + assert_eq!(error.kind(), RusticolErrorKind::Compatibility); + assert!( + error + .message() + .contains("SymJIT panicked while trying to exercise the panic-containment probe") + ); + assert!(error.message().contains("probe panic")); + } + + #[test] + fn storage_header_dimensions_and_trailing_bytes_fail_closed() { + let bytes = application_bytes(false, 2); + + let mut bad_magic = bytes.clone(); + bad_magic[0] ^= 1; + let error = SymjitPlaneKernel::load_bytes( + &bad_magic, + "bad-magic.symjit", + SymjitPlaneLayout::real(2, 1), + ) + .unwrap_err(); + assert_eq!(error.kind(), RusticolErrorKind::Compatibility); + + let error = SymjitPlaneKernel::load_bytes( + &bytes, + "bad-shape.symjit", + SymjitPlaneLayout::real(3, 1), + ) + .unwrap_err(); + assert_eq!(error.kind(), RusticolErrorKind::Integrity); + + let mut trailing = bytes; + trailing.push(0); + let error = SymjitPlaneKernel::load_bytes( + &trailing, + "trailing.symjit", + SymjitPlaneLayout::real(2, 1), + ) + .unwrap_err(); + assert_eq!(error.kind(), RusticolErrorKind::Integrity); + } + + #[test] + fn migration_contract_rejects_stored_simd512_configuration() { + let mut config = explicit_plane_config(false, 2, true).unwrap(); + config.enable_simd512(true); + let bytes = application_bytes_with_config(config); + let error = + SymjitPlaneKernel::load_bytes(&bytes, "simd512.symjit", SymjitPlaneLayout::real(2, 1)) + .unwrap_err(); + assert_eq!(error.kind(), RusticolErrorKind::Compatibility); + assert!(error.message().contains("SIMD512")); + } + + #[test] + fn migration_contract_rejects_noncanonical_stored_configuration() { + let mut config = explicit_plane_config(false, 2, true).unwrap(); + config.set_simd(false); + let bytes = application_bytes_with_config(config); + let error = + SymjitPlaneKernel::load_bytes(&bytes, "scalar.symjit", SymjitPlaneLayout::real(2, 1)) + .unwrap_err(); + assert_eq!(error.kind(), RusticolErrorKind::Compatibility); + assert!(error.message().contains("noncanonical compiler options")); + assert!(error.message().contains("regenerate")); + } + + #[test] + fn migration_contract_rejects_non_native_compiler_target() { + let mut config = explicit_plane_config(false, 2, true).unwrap(); + config.ty = CompilerType::ByteCode; + let error = canonical_plane_optimization_level( + &config, + Path::new("bytecode.symjit"), + SymjitPlaneLayout::real(2, 1), + ) + .unwrap_err(); + assert_eq!(error.kind(), RusticolErrorKind::Compatibility); + assert!(error.message().contains("non-native compiler target")); + assert!(error.message().contains("regenerate")); + } + + #[test] + fn migration_contract_accepts_only_scalar_f64x2_or_f64x4_runtime_lanes() { + let path = Path::new("lane-contract.symjit"); + for (has_simd, lanes) in [(false, 1), (true, 2), (true, 4)] { + validate_simd_lane_width(has_simd, lanes, path).unwrap(); + } + for (has_simd, lanes) in [(true, 1), (false, 2), (true, 8)] { + let error = validate_simd_lane_width(has_simd, lanes, path).unwrap_err(); + assert_eq!(error.kind(), RusticolErrorKind::Compatibility); + assert!(error.message().contains("unsupported SIMD lane width")); + } + } + + #[test] + fn compile_helper_rejects_invalid_cold_path_inputs() { + let error = + compile_symbolica_program_to_plane_application_bytes("", 2, 1, 2, false).unwrap_err(); + assert_eq!(error.kind(), RusticolErrorKind::InvalidArgument); + let error = compile_symbolica_program_to_plane_application_bytes(MODEL, 2, 1, 4, false) + .unwrap_err(); + assert_eq!(error.kind(), RusticolErrorKind::InvalidArgument); + } +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/lib.rs b/pyamplicol/rust/crates/rusticol-core/src/lib.rs new file mode 100644 index 0000000000..4ba3c3c56c --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/lib.rs @@ -0,0 +1,237 @@ +// SPDX-License-Identifier: 0BSD + +//! Python-independent Rusticol runtime core. + +#[cfg(not(any( + feature = "f64-compiled", + feature = "f64-symjit", + feature = "symbolica-runtime" +)))] +compile_error!("rusticol-core requires at least one evaluator runtime feature"); + +mod artifact; +pub mod direct_arena; +pub mod eager_layout; +mod eager_lowering_v3; +mod eager_plan_v3_pacbin; +#[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] +mod eager_runtime; +mod eager_tables; +mod engine; +mod error; +mod metadata; +pub mod pacbin; +pub mod recurrence; + +pub use artifact::{ + ArtifactKind, ArtifactManifest, ArtifactProcess, ArtifactSelection, Payload, PayloadRole, + ProcessAlias, ProcessGroupRelation, Target, VerifiedArtifact, runtime_target_info, +}; +pub use eager_lowering_v3::*; +pub use eager_plan_v3_pacbin::*; +#[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] +pub use eager_runtime::{ + DEFAULT_EAGER_POINT_TILE_SIZE, DEFAULT_EAGER_WORKSPACE_MIB, + EAGER_HOMOGENEOUS_LINEAR_CURRENT_PROOF, EAGER_INDEPENDENT_BLOCK_SIZE, EagerComplex64, + EagerDirectClosureSpec, EagerExecutionPlan, EagerExecutionRuntime, EagerKernelBackend, + EagerKernelCall, EagerKernelInput, EagerKernelRole, EagerKernelSpec, EagerPlanDefinition, + EagerPlanDimensions, EagerPlanPayloads, EagerReductionEntry, EagerReductionGroup, + EagerRuntimeOptions, EagerSelectorPayloads, EagerSelectorStagePayload, EagerStagePayload, +}; +pub use eager_tables::{ + EAGER_KERNEL_ABI, EAGER_LC_TOPOLOGY_REPLAY_RUNTIME_CAPABILITY, + EAGER_OUTPUT_FACTOR_COUPLING_IMAG, EAGER_OUTPUT_FACTOR_COUPLING_REAL, EAGER_OUTPUT_FACTOR_NONE, + EAGER_PLAN_ABI, EAGER_RUNTIME_CAPABILITY, EAGER_SELECTOR_DOMAINS_ABI, EagerAttachmentRow, + EagerClosureRow, EagerCouplingRow, EagerFinalizationRow, EagerInvocationRow, + EagerSelectorDomainIdRow, EagerSelectorDomainRow, EagerSelectorGroupRow, MISSING_U32, +}; +#[cfg(feature = "f64-symjit")] +pub use engine::eager_direct_descriptor_for_source_application_bytes; +pub use engine::{ + NativeColorComponent, NativeCorrelatedEvaluation, NativeCorrelatedRequest, + NativeDecimalEvaluation, NativeDecimalResolvedEvaluation, NativeEagerExactAttachment, + NativeEagerExactClosure, NativeEagerExactCoupling, NativeEagerExactFinalization, + NativeEagerExactInvocation, NativeEagerExactSections, NativeEagerExactStage, + NativeExternalParticle, NativeHelicityConfiguration, NativeModelParameter, + NativeProfiledEvaluation, NativeRationalFactor, NativeRecurrenceExactExecutor, + NativeRecurrenceExactFactor, NativeRecurrenceExactSections, NativeRecurrenceSelectorPlan, + NativeResolvedEvaluation, NativeRuntime, NativeRuntimeMetadata, NativeRuntimeProfile, + NativeSpinCorrelationVectors, RuntimeCapability, preflight_prepared_kernel_pack, + supported_runtime_capabilities, +}; +#[cfg(feature = "on-the-fly-test-support")] +pub use engine::{ + NativeOnTheFlyArtifactProbeV1, NativeOnTheFlyCurrentProbeV1, + NativeOnTheFlyExecutionComponentV1, NativeOnTheFlyExecutionDiagnosticV1, + NativeOnTheFlyFamilyProbeV1, NativeOnTheFlyFamilyQueryProbeV1, +}; +#[cfg(any(feature = "f64-compiled", feature = "f64-symjit"))] +pub use engine::{ + NativeOnTheFlyWarmUpEvent, NativeOnTheFlyWarmUpEventKind, NativeOnTheFlyWarmUpObserver, + NativeOnTheFlyWarmUpResult, NativeOnTheFlyWarmUpStage, +}; +pub use error::{RusticolError, RusticolErrorKind, RusticolResult}; +pub use metadata::{ + ColorAccuracy, ColorComponent, ContractedColor, Coverage, ExternalParticle, Helicity, + LcColorFlow, ModelParameter, ParameterKind, ParticleRole, ProcessPhysics, Reduction, + ReductionGroup, ReductionKind, SelectorCapabilities, +}; + +pub const PYTHON_API_VERSION: u32 = 1; +pub const TOML_SCHEMA_VERSION: u32 = 1; +pub const COMPILED_MODEL_SCHEMA_VERSION: u32 = 9; +pub const PROCESS_ARTIFACT_SCHEMA_VERSION: u32 = 3; +pub const RUNTIME_PHYSICS_SCHEMA_VERSION: u32 = 1; +pub const C_ABI_VERSION: u32 = 1; +pub const SYMBOLICA_SERIALIZATION_ABI: &str = "symbolica-bincode2-v1"; + +/// Unstable implementation details shared with pyAmpliCol's private PyO3 +/// extension. This module is absent from ordinary/default rusticol-core builds. +#[cfg(feature = "python-generation-bridge")] +#[doc(hidden)] +pub mod __private { + use crate::RusticolResult; + use crate::recurrence::template::{ + OwnedRecurrenceTemplateInput, ValidatedRecurrenceTemplateInput, + }; + use crate::recurrence::{PreparedDirectExecutorCatalog, SemanticDigest}; + + /// Unstable cold-path bridge for the private PyO3 artifact generator. + #[doc(hidden)] + pub fn compile_symbolica_program_to_plane_application_bytes( + program_repr: &str, + input_complex_count: usize, + output_complex_count: usize, + optimization_level: u8, + compress: bool, + ) -> RusticolResult> { + crate::engine::compile_symbolica_program_to_plane_application_bytes( + program_repr, + input_complex_count, + output_complex_count, + optimization_level, + compress, + ) + } + + /// Build one authenticated compact recurrence cold-load image from the + /// already staged, ordinary JSON payloads and typed payload inventory. + #[doc(hidden)] + pub fn build_recurrence_bootstrap_image_v1( + context_json: &[u8], + execution_json: &[u8], + physics_json: &[u8], + ) -> RusticolResult> { + crate::engine::build_recurrence_bootstrap_image_v1( + context_json, + execution_json, + physics_json, + ) + } + + /// Unstable cold-path bridge for constructing the compact private + /// on-the-fly process seed from source-only projection JSON. + #[doc(hidden)] + pub fn project_recurrence_template_catalog_json_v1( + value: &serde_json::Value, + ) -> RusticolResult { + crate::recurrence::template_json::project_recurrence_template_catalog_json_v1(value) + } + + /// Unstable cold-path bridge for encoding ordered validated compact seeds. + #[doc(hidden)] + pub fn build_on_the_fly_process_seed_bytes_batch_v1( + ordered_source_projection_jsons: &[Vec], + templates: &ValidatedRecurrenceTemplateInput, + direct_catalog: &PreparedDirectExecutorCatalog, + prepared_pack_digest: SemanticDigest, + ) -> RusticolResult>> { + let mut projections = Vec::new(); + projections + .try_reserve_exact(ordered_source_projection_jsons.len()) + .map_err(|error| { + crate::RusticolError::invalid_argument(format!( + "process-projection allocation failed: {error}" + )) + })?; + for (index, source_projection_json) in ordered_source_projection_jsons.iter().enumerate() { + let projection = + crate::recurrence::on_the_fly::parse_on_the_fly_process_seed_projection_v1( + source_projection_json, + ) + .map_err(|error| { + crate::RusticolError::with_kind( + error.kind(), + format!( + "on-the-fly process seed projection at index {index} failed: {error}" + ), + ) + })?; + projections.push(projection); + } + let seeds = crate::recurrence::on_the_fly::build_on_the_fly_process_seeds_v1( + projections, + templates, + direct_catalog, + prepared_pack_digest, + )?; + let mut encoded = Vec::new(); + encoded.try_reserve_exact(seeds.len()).map_err(|error| { + crate::RusticolError::invalid_argument(format!( + "encoded process-seed allocation failed: {error}" + )) + })?; + for (index, seed) in seeds.iter().enumerate() { + encoded.push( + crate::recurrence::on_the_fly::seed_codec::encode_on_the_fly_process_seed_v1(seed) + .map_err(|error| { + crate::RusticolError::with_kind( + error.kind(), + format!( + "on-the-fly process seed encoding at index {index} failed: {error}" + ), + ) + })?, + ); + } + Ok(encoded) + } + + /// Unstable singleton wrapper retained for the private PyO3 generator. + #[doc(hidden)] + pub fn build_on_the_fly_process_seed_bytes_v1( + source_projection_json: &[u8], + templates: &ValidatedRecurrenceTemplateInput, + direct_catalog: &PreparedDirectExecutorCatalog, + prepared_pack_digest: SemanticDigest, + ) -> RusticolResult> { + let payloads = build_on_the_fly_process_seed_bytes_batch_v1( + &[source_projection_json.to_vec()], + templates, + direct_catalog, + prepared_pack_digest, + )?; + let [payload] = <[Vec; 1]>::try_from(payloads).map_err(|payloads| { + crate::RusticolError::internal(format!( + "singleton process-seed byte batch returned {} payloads", + payloads.len() + )) + })?; + Ok(payload) + } + + /// Unstable cold-path bridge for inspecting one authoritative native + /// process seed without exposing the internal seed representation. + #[doc(hidden)] + pub fn inspect_on_the_fly_process_seed_identity_json_v1( + payload: &[u8], + ) -> RusticolResult { + let seed = crate::recurrence::on_the_fly::decode_on_the_fly_process_seed_v1(payload)?; + serde_json::to_string(&seed.identity()).map_err(|error| { + crate::RusticolError::serialization(format!( + "could not serialize on-the-fly process-seed identity: {error}" + )) + }) + } +} +pub const ARTIFACT_MANIFEST_FILE: &str = "artifact.json"; diff --git a/pyamplicol/rust/crates/rusticol-core/src/metadata.rs b/pyamplicol/rust/crates/rusticol-core/src/metadata.rs new file mode 100644 index 0000000000..620072e96e --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/metadata.rs @@ -0,0 +1,581 @@ +// SPDX-License-Identifier: 0BSD + +use crate::{RUNTIME_PHYSICS_SCHEMA_VERSION, RusticolError, RusticolResult}; +use bincode::Decode; +#[cfg(any(feature = "python-generation-bridge", test))] +use bincode::Encode; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use std::collections::{BTreeMap, BTreeSet}; + +#[derive(Clone, Copy, Debug, Decode, Deserialize, Eq, PartialEq, Serialize)] +#[cfg_attr(any(feature = "python-generation-bridge", test), derive(Encode))] +#[serde(rename_all = "lowercase")] +pub enum ColorAccuracy { + Lc, + Nlc, + Full, +} + +impl ColorAccuracy { + pub fn as_str(self) -> &'static str { + match self { + Self::Lc => "lc", + Self::Nlc => "nlc", + Self::Full => "full", + } + } +} + +impl std::fmt::Display for ColorAccuracy { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str(self.as_str()) + } +} + +#[derive(Clone, Copy, Debug, Decode, Deserialize, Eq, PartialEq, Serialize)] +#[cfg_attr(any(feature = "python-generation-bridge", test), derive(Encode))] +#[serde(rename_all = "lowercase")] +pub enum ParticleRole { + Initial, + Final, +} + +#[derive(Clone, Copy, Debug, Decode, Deserialize, Eq, PartialEq, Serialize)] +#[cfg_attr(any(feature = "python-generation-bridge", test), derive(Encode))] +#[serde(rename_all = "kebab-case")] +pub enum ParameterKind { + Normalization, + Mass, + Width, + Coupling, + External, + Derived, +} + +#[derive(Clone, Copy, Debug, Decode, Deserialize, Eq, PartialEq, Serialize)] +#[cfg_attr(any(feature = "python-generation-bridge", test), derive(Encode))] +pub enum ReductionKind { + #[serde(rename = "lc-diagonal")] + LcDiagonal, + #[serde(rename = "contracted-color")] + ContractedColor, +} + +#[derive(Clone, Debug, Decode, Deserialize, PartialEq, Serialize)] +#[cfg_attr(any(feature = "python-generation-bridge", test), derive(Encode))] +#[serde(deny_unknown_fields)] +pub struct Coverage { + pub helicities: String, + pub color: String, + pub color_kind: String, + pub structural_zero_helicity_count: usize, +} + +#[derive(Clone, Debug, Decode, Deserialize, Eq, PartialEq, Serialize)] +#[cfg_attr(any(feature = "python-generation-bridge", test), derive(Encode))] +#[serde(deny_unknown_fields)] +pub struct ExternalParticle { + pub index: usize, + pub label: usize, + pub particle: String, + pub pdg: i32, + pub role: ParticleRole, + pub momentum_slot: usize, + pub momentum_components: [String; 4], +} + +#[derive(Clone, Debug, Decode, Deserialize, PartialEq, Serialize)] +#[cfg_attr(any(feature = "python-generation-bridge", test), derive(Encode))] +#[serde(deny_unknown_fields)] +pub struct Helicity { + pub id: String, + pub index: usize, + pub values: Vec, + pub computed: bool, + pub structural_zero: bool, + pub representative_id: String, + pub coefficient: f64, +} + +#[derive(Clone, Debug, Decode, Deserialize, PartialEq, Serialize)] +#[cfg_attr(any(feature = "python-generation-bridge", test), derive(Encode))] +#[serde(deny_unknown_fields)] +pub struct LcColorFlow { + pub id: String, + pub index: usize, + pub word: Vec, + pub computed: bool, + pub representative_id: String, + pub coefficient: f64, +} + +#[derive(Clone, Debug, Decode, Deserialize, Eq, PartialEq, Serialize)] +#[cfg_attr(any(feature = "python-generation-bridge", test), derive(Encode))] +#[serde(deny_unknown_fields)] +pub struct ContractedColor { + pub id: String, + pub index: usize, + pub description: String, +} + +#[derive(Clone, Debug, Decode, Deserialize, PartialEq, Serialize)] +#[cfg_attr(any(feature = "python-generation-bridge", test), derive(Encode))] +#[serde(tag = "kind")] +pub enum ColorComponent { + #[serde(rename = "lc-flow")] + LcFlow(LcColorFlow), + #[serde(rename = "contracted-color")] + ContractedColor(ContractedColor), +} + +impl ColorComponent { + pub fn id(&self) -> &str { + match self { + Self::LcFlow(value) => &value.id, + Self::ContractedColor(value) => &value.id, + } + } + + pub fn index(&self) -> usize { + match self { + Self::LcFlow(value) => value.index, + Self::ContractedColor(value) => value.index, + } + } + + pub fn coefficient(&self) -> f64 { + match self { + Self::LcFlow(value) => value.coefficient, + Self::ContractedColor(_) => 1.0, + } + } +} + +#[derive(Clone, Debug, Decode, Deserialize, PartialEq, Serialize)] +#[cfg_attr(any(feature = "python-generation-bridge", test), derive(Encode))] +#[serde(deny_unknown_fields)] +pub struct ReductionGroup { + pub id: String, + pub representative_helicity_id: String, + pub representative_color_id: String, + pub physical_helicity_ids: Vec, + pub physical_color_ids: Vec, +} + +#[derive(Clone, Debug, Decode, Deserialize, PartialEq, Serialize)] +#[cfg_attr(any(feature = "python-generation-bridge", test), derive(Encode))] +#[serde(deny_unknown_fields)] +pub struct Reduction { + pub kind: ReductionKind, + pub groups: Vec, +} + +#[derive(Clone, Debug, Decode, Deserialize, PartialEq, Serialize)] +#[cfg_attr(any(feature = "python-generation-bridge", test), derive(Encode))] +#[serde(deny_unknown_fields)] +pub struct ModelParameter { + pub name: String, + pub kind: ParameterKind, + pub default_real: f64, + pub default_imaginary: f64, + pub mutable: bool, +} + +#[derive(Clone, Debug, Decode, Deserialize, Eq, PartialEq, Serialize)] +#[cfg_attr(any(feature = "python-generation-bridge", test), derive(Encode))] +#[serde(deny_unknown_fields)] +pub struct SelectorCapabilities { + pub helicity: bool, + pub color_flow: bool, + pub contracted_color: bool, +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct ProcessPhysics { + pub schema_version: u32, + pub kind: String, + pub process_id: String, + pub process: String, + pub color_accuracy: ColorAccuracy, + pub coverage: Coverage, + pub external_particles: Vec, + pub helicities: Vec, + pub color_components: Vec, + pub reduction: Reduction, + pub model_parameters: Vec, + pub selectors: SelectorCapabilities, + #[serde(default)] + pub extensions: BTreeMap, +} + +impl ProcessPhysics { + pub fn from_json(bytes: &[u8], path: &str) -> RusticolResult { + let value: Self = serde_json::from_slice(bytes).map_err(|error| { + RusticolError::serialization(format!( + "could not parse runtime physics payload {path:?}: {error}" + )) + })?; + value.validate()?; + Ok(value) + } + + pub fn validate(&self) -> RusticolResult<()> { + if self.schema_version != RUNTIME_PHYSICS_SCHEMA_VERSION + || self.kind != "pyamplicol-resolved-physics" + { + return Err(RusticolError::compatibility(format!( + "unsupported runtime physics kind {:?} schema {}; regenerate the artifact with pyAmpliCol 0.1 or newer", + self.kind, self.schema_version + ))); + } + if self.external_particles.len() < 3 || self.helicities.is_empty() { + return Err(RusticolError::artifact( + "runtime physics requires at least three particles and one helicity", + )); + } + if self.process.is_empty() + || !matches!(self.coverage.helicities.as_str(), "complete" | "selected") + || !matches!( + self.coverage.color.as_str(), + "complete" | "selected" | "contracted" + ) + || !matches!( + self.coverage.color_kind.as_str(), + "physical-lc-flows" | "contracted-color" + ) + { + return Err(RusticolError::artifact( + "runtime physics has an invalid process or coverage value", + )); + } + validate_public_id(&self.process_id, "process id")?; + let expected_components = ["E", "px", "py", "pz"]; + let mut momentum_slots = BTreeSet::new(); + for (index, particle) in self.external_particles.iter().enumerate() { + if particle.index != index || particle.label != index + 1 { + return Err(RusticolError::artifact(format!( + "external particle {index} has inconsistent index/label metadata" + ))); + } + if particle.particle.is_empty() { + return Err(RusticolError::artifact(format!( + "external particle {index} has an empty particle name" + ))); + } + if !momentum_slots.insert(particle.momentum_slot) { + return Err(RusticolError::artifact(format!( + "external particle {index} reuses momentum slot {}", + particle.momentum_slot + ))); + } + if particle + .momentum_components + .iter() + .map(String::as_str) + .ne(expected_components) + { + return Err(RusticolError::artifact(format!( + "external particle {index} must use momentum components [E, px, py, pz]" + ))); + } + } + if momentum_slots != (0..self.external_particles.len()).collect() { + return Err(RusticolError::artifact( + "external particle momentum slots must form a complete zero-based permutation", + )); + } + + let mut helicity_ids = BTreeMap::new(); + for (index, helicity) in self.helicities.iter().enumerate() { + validate_public_id(&helicity.id, "helicity id")?; + validate_public_id(&helicity.representative_id, "representative helicity id")?; + if helicity.index != index || helicity.values.len() != self.external_particles.len() { + return Err(RusticolError::artifact(format!( + "helicity {} has inconsistent index or particle count", + helicity.id + ))); + } + if !helicity.coefficient.is_finite() || helicity.coefficient < 0.0 { + return Err(RusticolError::artifact(format!( + "helicity {} has an invalid reduction coefficient", + helicity.id + ))); + } + if helicity_ids + .insert(helicity.id.as_str(), helicity) + .is_some() + { + return Err(RusticolError::artifact(format!( + "duplicate helicity id {:?}", + helicity.id + ))); + } + } + for helicity in &self.helicities { + if !helicity_ids.contains_key(helicity.representative_id.as_str()) { + return Err(RusticolError::artifact(format!( + "helicity {} references unknown representative {:?}", + helicity.id, helicity.representative_id + ))); + } + if helicity.structural_zero && helicity.computed { + return Err(RusticolError::artifact(format!( + "structural-zero helicity {} may not be marked computed", + helicity.id + ))); + } + if helicity.structural_zero && helicity.coefficient != 0.0 { + return Err(RusticolError::artifact(format!( + "structural-zero helicity {} must have a zero reduction coefficient", + helicity.id + ))); + } + if !helicity.structural_zero { + if helicity.coefficient <= 0.0 { + return Err(RusticolError::artifact(format!( + "nonzero helicity {} must have a positive reduction coefficient", + helicity.id + ))); + } + let representative = helicity_ids + .get(helicity.representative_id.as_str()) + .expect("representative existence checked"); + if !representative.computed || representative.structural_zero { + return Err(RusticolError::artifact(format!( + "helicity {} references a non-computed representative {:?}", + helicity.id, helicity.representative_id + ))); + } + if helicity.computed && helicity.representative_id != helicity.id { + return Err(RusticolError::artifact(format!( + "computed helicity {} must represent itself", + helicity.id + ))); + } + } + } + let structural_zeros = self + .helicities + .iter() + .filter(|helicity| helicity.structural_zero) + .count(); + if structural_zeros != self.coverage.structural_zero_helicity_count { + return Err(RusticolError::artifact(format!( + "coverage declares {} structural-zero helicities, metadata contains {structural_zeros}", + self.coverage.structural_zero_helicity_count + ))); + } + + let mut color_ids = BTreeMap::new(); + for (index, color) in self.color_components.iter().enumerate() { + validate_public_id(color.id(), "color id")?; + if color.index() != index + || !color.coefficient().is_finite() + || color.coefficient() < 0.0 + { + return Err(RusticolError::artifact(format!( + "color component {} has an invalid index or coefficient", + color.id() + ))); + } + if color_ids.insert(color.id(), color).is_some() { + return Err(RusticolError::artifact(format!( + "duplicate color component id {:?}", + color.id() + ))); + } + if let ColorComponent::LcFlow(flow) = color { + if flow + .word + .iter() + .any(|label| *label == 0 || *label > self.external_particles.len()) + { + return Err(RusticolError::artifact(format!( + "LC color component {} contains an out-of-range external label", + flow.id + ))); + } + } else if let ColorComponent::ContractedColor(color) = color + && color.description.is_empty() + { + return Err(RusticolError::artifact(format!( + "contracted color component {} has an empty description", + color.id + ))); + } + } + for color in &self.color_components { + if let ColorComponent::LcFlow(flow) = color { + let Some(representative) = color_ids.get(flow.representative_id.as_str()) else { + return Err(RusticolError::artifact(format!( + "color component {} references unknown representative {:?}", + flow.id, flow.representative_id + ))); + }; + let ColorComponent::LcFlow(representative) = representative else { + return Err(RusticolError::artifact(format!( + "LC color component {} references a contracted representative", + flow.id + ))); + }; + if !representative.computed { + return Err(RusticolError::artifact(format!( + "LC color component {} references a non-computed representative {:?}", + flow.id, flow.representative_id + ))); + } + if flow.computed && flow.representative_id != flow.id { + return Err(RusticolError::artifact(format!( + "computed LC color component {} must represent itself", + flow.id + ))); + } + } + } + match self.color_accuracy { + ColorAccuracy::Lc => { + if self.color_components.is_empty() + || self + .color_components + .iter() + .any(|item| !matches!(item, ColorComponent::LcFlow(_))) + { + return Err(RusticolError::artifact( + "LC physics requires one or more physical LC color flows", + )); + } + if self.reduction.kind != ReductionKind::LcDiagonal + || self.coverage.color_kind != "physical-lc-flows" + || !self.selectors.color_flow + || self.selectors.contracted_color + { + return Err(RusticolError::artifact( + "LC physics has inconsistent reduction, coverage, or selector metadata", + )); + } + } + ColorAccuracy::Nlc | ColorAccuracy::Full => { + if self.color_components.len() != 1 + || !matches!( + self.color_components.first(), + Some(ColorComponent::ContractedColor(ContractedColor { + index: 0, + .. + })) + ) + { + return Err(RusticolError::artifact( + "NLC/full physics requires exactly one contracted color component", + )); + } + if self.reduction.kind != ReductionKind::ContractedColor + || self.coverage.color != "contracted" + || self.coverage.color_kind != "contracted-color" + || self.selectors.color_flow + || self.selectors.contracted_color + { + return Err(RusticolError::artifact( + "NLC/full physics has inconsistent reduction, coverage, or selector metadata", + )); + } + } + } + + let mut parameter_names = BTreeSet::new(); + for parameter in &self.model_parameters { + if parameter.name.is_empty() + || !parameter_names.insert(¶meter.name) + || !parameter.default_real.is_finite() + || !parameter.default_imaginary.is_finite() + || (parameter.kind == ParameterKind::Derived && parameter.mutable) + { + return Err(RusticolError::artifact(format!( + "invalid or duplicate model parameter {:?}", + parameter.name + ))); + } + } + + let mut seen_groups = BTreeSet::new(); + for group in &self.reduction.groups { + validate_public_id(&group.id, "reduction group id")?; + if !seen_groups.insert(&group.id) { + return Err(RusticolError::artifact(format!( + "duplicate reduction group id {:?}", + group.id + ))); + } + if group.physical_helicity_ids.is_empty() || group.physical_color_ids.is_empty() { + return Err(RusticolError::artifact(format!( + "reduction group {} has an empty physical expansion", + group.id + ))); + } + let has_representative_helicity = + helicity_ids.contains_key(group.representative_helicity_id.as_str()); + let has_representative_color = + color_ids.contains_key(group.representative_color_id.as_str()); + if !has_representative_helicity || !has_representative_color { + return Err(RusticolError::artifact(format!( + "reduction group {} references an unknown representative: helicity {:?} present={has_representative_helicity}, color {:?} present={has_representative_color}; available colors={:?}", + group.id, + group.representative_helicity_id, + group.representative_color_id, + color_ids.keys().collect::>(), + ))); + } + if !group + .physical_helicity_ids + .contains(&group.representative_helicity_id) + || !group + .physical_color_ids + .contains(&group.representative_color_id) + { + return Err(RusticolError::artifact(format!( + "reduction group {} does not include its representative in its physical expansion", + group.id + ))); + } + let mut physical_helicities = BTreeSet::new(); + for id in &group.physical_helicity_ids { + if !helicity_ids.contains_key(id.as_str()) || !physical_helicities.insert(id) { + return Err(RusticolError::artifact(format!( + "reduction group {} has an unknown or duplicate helicity {:?}", + group.id, id + ))); + } + } + let mut physical_colors = BTreeSet::new(); + for id in &group.physical_color_ids { + if !color_ids.contains_key(id.as_str()) || !physical_colors.insert(id) { + return Err(RusticolError::artifact(format!( + "reduction group {} has an unknown or duplicate color {:?}", + group.id, id + ))); + } + } + } + Ok(()) + } +} + +fn validate_public_id(value: &str, description: &str) -> RusticolResult<()> { + let valid = !value.is_empty() + && value.len() <= 255 + && value.bytes().enumerate().all(|(index, byte)| { + byte.is_ascii_alphanumeric() + || (index > 0 && matches!(byte, b'.' | b'_' | b':' | b'+' | b',' | b'~' | b'-')) + }); + if !valid { + return Err(RusticolError::artifact(format!( + "invalid {description} {value:?}" + ))); + } + Ok(()) +} + +#[cfg(test)] +#[path = "metadata_tests.rs"] +mod tests; diff --git a/pyamplicol/rust/crates/rusticol-core/src/metadata_tests.rs b/pyamplicol/rust/crates/rusticol-core/src/metadata_tests.rs new file mode 100644 index 0000000000..5066c1a3a5 --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/metadata_tests.rs @@ -0,0 +1,113 @@ +// SPDX-License-Identifier: 0BSD + +use super::*; + +#[test] +fn json_model_parameter_defaults_round_trip_binary64() { + let expected = f64::from_bits(0x3fdd_8fdb_d004_403d); + let encoded = serde_json::to_vec(&ModelParameter { + name: "derived_coupling_88".to_owned(), + kind: ParameterKind::Derived, + default_real: 0.0, + default_imaginary: expected, + mutable: false, + }) + .expect("serialize model parameter"); + let decoded: ModelParameter = serde_json::from_slice(&encoded).expect("parse model parameter"); + + assert_eq!(decoded.default_imaginary.to_bits(), expected.to_bits()); +} + +fn valid_physics() -> ProcessPhysics { + ProcessPhysics { + schema_version: RUNTIME_PHYSICS_SCHEMA_VERSION, + kind: "pyamplicol-resolved-physics".to_string(), + process_id: "p0".to_string(), + process: "a b > c".to_string(), + color_accuracy: ColorAccuracy::Full, + coverage: Coverage { + helicities: "complete".to_string(), + color: "contracted".to_string(), + color_kind: "contracted-color".to_string(), + structural_zero_helicity_count: 0, + }, + external_particles: (0..3) + .map(|index| ExternalParticle { + index, + label: index + 1, + particle: format!("particle-{index}"), + pdg: index as i32 + 1, + role: if index < 2 { + ParticleRole::Initial + } else { + ParticleRole::Final + }, + momentum_slot: index, + momentum_components: [ + "E".to_string(), + "px".to_string(), + "py".to_string(), + "pz".to_string(), + ], + }) + .collect(), + helicities: vec![Helicity { + id: "helicity:0".to_string(), + index: 0, + values: vec![1, -1, 1], + computed: true, + structural_zero: false, + representative_id: "helicity:0".to_string(), + coefficient: 1.0, + }], + color_components: vec![ColorComponent::ContractedColor(ContractedColor { + id: "contracted".to_string(), + index: 0, + description: "coherent contracted color".to_string(), + })], + reduction: Reduction { + kind: ReductionKind::ContractedColor, + groups: vec![ReductionGroup { + id: "group:0".to_string(), + representative_helicity_id: "helicity:0".to_string(), + representative_color_id: "contracted".to_string(), + physical_helicity_ids: vec!["helicity:0".to_string()], + physical_color_ids: vec!["contracted".to_string()], + }], + }, + model_parameters: Vec::new(), + selectors: SelectorCapabilities { + helicity: true, + color_flow: false, + contracted_color: false, + }, + extensions: BTreeMap::new(), + } +} + +#[test] +fn normative_physics_metadata_validates() { + valid_physics().validate().unwrap(); +} + +#[test] +fn nonzero_members_require_computed_representatives() { + let mut physics = valid_physics(); + physics.helicities[0].computed = false; + + let error = physics.validate().unwrap_err(); + + assert_eq!(error.kind(), crate::RusticolErrorKind::Artifact); + assert!(error.to_string().contains("non-computed representative")); +} + +#[test] +fn momentum_slots_must_be_a_complete_permutation() { + let mut physics = valid_physics(); + physics.external_particles[2].momentum_slot = 1; + + let error = physics.validate().unwrap_err(); + + assert_eq!(error.kind(), crate::RusticolErrorKind::Artifact); + assert!(error.to_string().contains("momentum slot")); +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/pacbin.rs b/pyamplicol/rust/crates/rusticol-core/src/pacbin.rs new file mode 100644 index 0000000000..cf72999a47 --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/pacbin.rs @@ -0,0 +1,1434 @@ +// SPDX-License-Identifier: 0BSD + +//! Strict reader for portable pyAmpliCol evaluator containers. +//! +//! `pacbin-v1` stores fixed-size metadata, aligned uncompressed member +//! payloads, a sorted variable-width index, and a footer. The index and every +//! payload are authenticated with SHA-256. This module is intentionally +//! independent of artifact loading: it owns one container as immutable bytes +//! and provides indexed borrowed slices without extracting files. + +use crate::{RusticolError, RusticolResult}; +use sha2::{Digest, Sha256}; +use std::collections::BTreeSet; +use std::ffi::OsString; +use std::fs::{File, OpenOptions}; +use std::io::{Read, Seek, SeekFrom, Write}; +use std::ops::Deref; +#[cfg(unix)] +use std::os::unix::fs::{OpenOptionsExt, PermissionsExt}; +use std::path::{Path, PathBuf}; +#[cfg(unix)] +use std::ptr::NonNull; +use std::sync::atomic::{AtomicU64, Ordering}; + +pub const PACBIN_VERSION: u16 = 1; +pub const PACBIN_ALIGNMENT: u32 = 64; +pub const PACBIN_DEFAULT_CHUNK_SIZE: usize = 1024 * 1024; +pub const PACBIN_DEFAULT_MODE: u32 = 0o644; + +const HEADER_MAGIC: &[u8; 8] = b"PACBIN\0\0"; +const INDEX_MAGIC: &[u8; 8] = b"PACIDX\0\0"; +const FOOTER_MAGIC: &[u8; 8] = b"PACEND\0\0"; +const SUPPORTED_FLAGS: u32 = 0; +const HEADER_SIZE: usize = 64; +const INDEX_HEADER_SIZE: usize = 32; +const INDEX_ENTRY_SIZE: usize = 56; +const FOOTER_SIZE: usize = 64; +const INDEX_ALIGNMENT: u64 = 8; +const TEMPORARY_FILE_ATTEMPTS: u64 = 128; + +static NEXT_TEMPORARY_FILE: AtomicU64 = AtomicU64::new(0); + +/// Portable payload kinds defined by `pacbin-v1`. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[repr(u16)] +pub enum PacbinMemberKind { + SymjitApplication = 1, + SymbolicaExactState = 2, + NativeLibrary = 3, + EagerRuntimeMetadata = 4, + EagerRuntimeTable = 5, + RecurrenceDirectPlan = 7, + RecurrenceColorProjectionCertificate = 8, + OnTheFlyProcessSeed = 9, + ColorContraction = 10, +} + +impl PacbinMemberKind { + fn parse(value: u16) -> RusticolResult { + match value { + 1 => Ok(Self::SymjitApplication), + 2 => Ok(Self::SymbolicaExactState), + 3 => Ok(Self::NativeLibrary), + 4 => Ok(Self::EagerRuntimeMetadata), + 5 => Ok(Self::EagerRuntimeTable), + 7 => Ok(Self::RecurrenceDirectPlan), + 8 => Ok(Self::RecurrenceColorProjectionCertificate), + 9 => Ok(Self::OnTheFlyProcessSeed), + 10 => Ok(Self::ColorContraction), + _ => Err(compatibility(format!( + "unknown pacbin member kind: {value}" + ))), + } + } +} + +/// One bounded source streamed into a `pacbin-v1` member. +pub enum PacbinWriteSource<'a> { + Bytes(&'a [u8]), + Path(PathBuf), + Reader(&'a mut dyn Read), +} + +/// One normalized member supplied to the deterministic Rust writer. +pub struct PacbinWriteMember<'a> { + logical_path: String, + kind: PacbinMemberKind, + source: PacbinWriteSource<'a>, +} + +impl<'a> PacbinWriteMember<'a> { + pub fn from_bytes( + logical_path: &str, + kind: PacbinMemberKind, + payload: &'a [u8], + ) -> RusticolResult { + Self::new(logical_path, kind, PacbinWriteSource::Bytes(payload)) + } + + pub fn from_path( + logical_path: &str, + kind: PacbinMemberKind, + source: impl Into, + ) -> RusticolResult { + Self::new(logical_path, kind, PacbinWriteSource::Path(source.into())) + } + + pub fn from_reader( + logical_path: &str, + kind: PacbinMemberKind, + source: &'a mut dyn Read, + ) -> RusticolResult { + Self::new(logical_path, kind, PacbinWriteSource::Reader(source)) + } + + fn new( + logical_path: &str, + kind: PacbinMemberKind, + source: PacbinWriteSource<'a>, + ) -> RusticolResult { + Ok(Self { + logical_path: normalize_logical_path(logical_path)?, + kind, + source, + }) + } + + pub fn logical_path(&self) -> &str { + &self.logical_path + } + + pub fn kind(&self) -> PacbinMemberKind { + self.kind + } +} + +/// Validated options for one atomic `pacbin-v1` publication. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct PacbinWriteOptions { + chunk_size: usize, + mode: u32, +} + +impl PacbinWriteOptions { + pub fn new(chunk_size: usize, mode: u32) -> RusticolResult { + if chunk_size == 0 { + return Err(RusticolError::invalid_argument( + "pacbin chunk size must be a positive integer", + )); + } + if mode > 0o777 { + return Err(RusticolError::invalid_argument( + "pacbin publication mode must be between 0o000 and 0o777", + )); + } + Ok(Self { chunk_size, mode }) + } + + pub fn chunk_size(&self) -> usize { + self.chunk_size + } + + pub fn mode(&self) -> u32 { + self.mode + } +} + +impl Default for PacbinWriteOptions { + fn default() -> Self { + Self { + chunk_size: PACBIN_DEFAULT_CHUNK_SIZE, + mode: PACBIN_DEFAULT_MODE, + } + } +} + +/// Validated metadata for one member. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct PacbinMember { + logical_path: String, + kind: PacbinMemberKind, + offset: u64, + length: u64, + sha256: [u8; 32], +} + +impl PacbinMember { + pub fn logical_path(&self) -> &str { + &self.logical_path + } + + pub fn kind(&self) -> PacbinMemberKind { + self.kind + } + + pub fn offset(&self) -> u64 { + self.offset + } + + pub fn length(&self) -> u64 { + self.length + } + + pub fn sha256(&self) -> &[u8; 32] { + &self.sha256 + } +} + +/// Validated top-level container metadata. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct PacbinIndex { + version: u16, + index_offset: u64, + index_sha256: [u8; 32], + file_size: u64, + members: Vec, +} + +impl PacbinIndex { + pub fn version(&self) -> u16 { + self.version + } + + pub fn index_offset(&self) -> u64 { + self.index_offset + } + + pub fn index_sha256(&self) -> &[u8; 32] { + &self.index_sha256 + } + + pub fn file_size(&self) -> u64 { + self.file_size + } + + pub fn members(&self) -> &[PacbinMember] { + &self.members + } +} + +/// An authenticated, indexed `pacbin-v1` container. +/// +/// Files are copied into an anonymous read-only mapping on Unix so large +/// evaluator packs remain outside the Rust heap without retaining a mutable +/// relationship to the source file. Containers constructed from bytes retain +/// owned storage. +#[derive(Debug)] +pub struct PacbinReader { + bytes: PacbinStorage, + index: PacbinIndex, +} + +#[derive(Debug)] +enum PacbinStorage { + Owned(Box<[u8]>), + #[cfg(unix)] + Mapped(ReadOnlyMmap), +} + +impl AsRef<[u8]> for PacbinStorage { + fn as_ref(&self) -> &[u8] { + match self { + Self::Owned(bytes) => bytes, + #[cfg(unix)] + Self::Mapped(bytes) => bytes, + } + } +} + +#[cfg(unix)] +struct ReadOnlyMmap { + pointer: NonNull, + length: usize, +} + +#[cfg(unix)] +impl std::fmt::Debug for ReadOnlyMmap { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter + .debug_struct("ReadOnlyMmap") + .field("length", &self.length) + .finish_non_exhaustive() + } +} + +#[cfg(unix)] +impl ReadOnlyMmap { + fn snapshot(source: &mut File, path: &Path, length: usize) -> RusticolResult<(Self, [u8; 32])> { + if length == 0 { + return Err(RusticolError::artifact(format!( + "cannot map empty pacbin container {}", + path.display() + ))); + } + // Anonymous memory is the only portable Unix backing store that never + // exposes a writable pathname or file descriptor. A temporary file, + // even after unlink, can still be mutated through a descriptor opened + // during its named lifetime. Copying directly into anonymous pages + // also ensures that the digest and parser consume one retained image. + let raw_pointer = unsafe { + libc::mmap( + std::ptr::null_mut(), + length, + libc::PROT_READ | libc::PROT_WRITE, + libc::MAP_PRIVATE | libc::MAP_ANON, + -1, + 0, + ) + }; + if raw_pointer == libc::MAP_FAILED { + return Err(RusticolError::artifact(format!( + "could not allocate the {length}-byte anonymous immutable snapshot required for \ + pacbin container {}: {}. Ensure the process has at least {length} bytes of \ + available virtual memory and RAM or swap", + path.display(), + std::io::Error::last_os_error() + ))); + } + let Some(pointer) = NonNull::new(raw_pointer.cast::()) else { + // SAFETY: this pointer/length pair came from mmap above and has + // not otherwise been retained. + unsafe { + libc::munmap(raw_pointer, length); + } + return Err(RusticolError::artifact(format!( + "anonymous memory-mapping returned a null pointer for pacbin container {}", + path.display() + ))); + }; + let snapshot = Self { pointer, length }; + + source.seek(SeekFrom::Start(0)).map_err(|error| { + RusticolError::artifact(format!( + "could not rewind pacbin container {}: {error}", + path.display() + )) + })?; + // SAFETY: `snapshot` exclusively owns a writable anonymous mapping of + // exactly `length` bytes until `mprotect` below. + let destination = + unsafe { std::slice::from_raw_parts_mut(snapshot.pointer.as_ptr(), snapshot.length) }; + for chunk in destination.chunks_mut(PACBIN_DEFAULT_CHUNK_SIZE) { + source.read_exact(chunk).map_err(|error| { + RusticolError::artifact(format!( + "could not snapshot pacbin container {} into anonymous memory: {error}", + path.display() + )) + })?; + } + let mut trailing = [0_u8; 1]; + let trailing_count = source.read(&mut trailing).map_err(|error| { + RusticolError::artifact(format!( + "could not finish snapshotting pacbin container {}: {error}", + path.display() + )) + })?; + if trailing_count != 0 { + return Err(RusticolError::artifact(format!( + "pacbin container {} grew while being snapshotted", + path.display() + ))); + } + + // Seal before authentication. No retained mutable reference exists, + // and mprotect removes write permission from the mapping itself. The + // SHA-256 digest and all parsing therefore address the same immutable + // bytes. + let protection_result = + unsafe { libc::mprotect(snapshot.pointer.as_ptr().cast(), length, libc::PROT_READ) }; + if protection_result != 0 { + return Err(RusticolError::artifact(format!( + "could not make the anonymous pacbin snapshot for {} read-only: {}", + path.display(), + std::io::Error::last_os_error() + ))); + } + let digest = Sha256::digest(&*snapshot).into(); + Ok((snapshot, digest)) + } +} + +#[cfg(unix)] +impl Deref for ReadOnlyMmap { + type Target = [u8]; + + fn deref(&self) -> &Self::Target { + // SAFETY: mmap established a read-only mapping of exactly `length` + // bytes and this object keeps it alive until Drop. + unsafe { std::slice::from_raw_parts(self.pointer.as_ptr(), self.length) } + } +} + +#[cfg(unix)] +unsafe impl Send for ReadOnlyMmap {} +#[cfg(unix)] +unsafe impl Sync for ReadOnlyMmap {} + +#[cfg(unix)] +impl Drop for ReadOnlyMmap { + fn drop(&mut self) { + // SAFETY: this pointer/length pair came from the successful mmap in + // `map` and is unmapped exactly once here. + unsafe { + libc::munmap(self.pointer.as_ptr().cast(), self.length); + } + } +} + +impl PacbinReader { + /// Open and fully authenticate one container file. + pub fn open(path: impl AsRef) -> RusticolResult { + Self::open_with_payload_verification(path, true) + } + + /// Open and authenticate the exact mapped bytes against an enclosing + /// manifest digest. Hashing and parsing share one storage object, so a + /// path replacement cannot substitute a different container between them. + #[cfg(test)] + pub(crate) fn open_with_sha256( + path: impl AsRef, + expected_sha256: &[u8; 32], + ) -> RusticolResult { + let path = path.as_ref(); + let file = File::open(path).map_err(|error| { + RusticolError::artifact(format!( + "could not open pacbin container {}: {error}", + path.display() + )) + })?; + Self::open_file_with_sha256(file, path, expected_sha256) + } + + /// Authenticate and snapshot an already-open, metadata-checked container. + #[cfg_attr(target_vendor = "apple", unsafe(link_section = "__TEXT,__rcl_load"))] + #[cfg_attr(target_vendor = "apple", inline(never))] + pub(crate) fn open_file_with_sha256( + file: File, + display_path: &Path, + expected_sha256: &[u8; 32], + ) -> RusticolResult { + let (storage, actual) = Self::open_file_storage(file, display_path)?; + if &actual != expected_sha256 { + return Err(RusticolError::integrity(format!( + "pacbin container {} payload digest mismatch", + display_path.display() + ))); + } + Self::from_storage(storage, false) + } + + fn open_with_payload_verification( + path: impl AsRef, + verify_payloads: bool, + ) -> RusticolResult { + let path = path.as_ref(); + let file = File::open(path).map_err(|error| { + RusticolError::artifact(format!( + "could not open pacbin container {}: {error}", + path.display() + )) + })?; + let (storage, _) = Self::open_file_storage(file, path)?; + Self::from_storage(storage, verify_payloads) + } + + #[cfg_attr(target_vendor = "apple", unsafe(link_section = "__TEXT,__rcl_load"))] + #[cfg_attr(target_vendor = "apple", inline(never))] + fn open_file_storage(mut file: File, path: &Path) -> RusticolResult<(PacbinStorage, [u8; 32])> { + let expected_length = file + .metadata() + .map_err(|error| { + RusticolError::artifact(format!( + "could not inspect pacbin container {}: {error}", + path.display() + )) + })? + .len(); + let capacity = usize::try_from(expected_length).map_err(|_| { + RusticolError::artifact(format!( + "pacbin container {} is too large for this platform", + path.display() + )) + })?; + #[cfg(unix)] + let (storage, digest) = ReadOnlyMmap::snapshot(&mut file, path, capacity)?; + #[cfg(unix)] + let storage = PacbinStorage::Mapped(storage); + #[cfg(not(unix))] + let (storage, digest) = { + let read_limit = expected_length.checked_add(1).ok_or_else(|| { + RusticolError::artifact(format!( + "pacbin container {} exceeds the supported file size", + path.display() + )) + })?; + let mut bytes = Vec::new(); + bytes.try_reserve_exact(capacity).map_err(|error| { + RusticolError::artifact(format!( + "could not allocate {capacity} bytes for pacbin container {}: {error}", + path.display() + )) + })?; + file.take(read_limit) + .read_to_end(&mut bytes) + .map_err(|error| { + RusticolError::artifact(format!( + "could not read pacbin container {}: {error}", + path.display() + )) + })?; + if bytes.len() != capacity { + return Err(RusticolError::artifact(format!( + "pacbin container {} changed while being read: expected {capacity} bytes, read {}", + path.display(), + bytes.len() + ))); + } + let digest = Sha256::digest(&bytes).into(); + (PacbinStorage::Owned(bytes.into_boxed_slice()), digest) + }; + Ok((storage, digest)) + } + + /// Own and fully authenticate already-read container bytes. + pub fn from_bytes(bytes: Vec) -> RusticolResult { + Self::from_storage(PacbinStorage::Owned(bytes.into_boxed_slice()), true) + } + + #[cfg_attr(target_vendor = "apple", unsafe(link_section = "__TEXT,__rcl_load"))] + #[cfg_attr(target_vendor = "apple", inline(never))] + fn from_storage(bytes: PacbinStorage, verify_payloads: bool) -> RusticolResult { + let index = parse_and_validate(bytes.as_ref(), verify_payloads)?; + Ok(Self { bytes, index }) + } + + pub fn index(&self) -> &PacbinIndex { + &self.index + } + + pub fn members(&self) -> &[PacbinMember] { + self.index.members() + } + + /// Resolve a member by normalized logical path in logarithmic time. + pub fn member(&self, logical_path: &str) -> RusticolResult<&PacbinMember> { + let normalized = normalize_logical_path(logical_path)?; + self.index + .members + .binary_search_by(|member| member.logical_path.as_bytes().cmp(normalized.as_bytes())) + .map(|index| &self.index.members[index]) + .map_err(|_| { + RusticolError::invalid_argument(format!("unknown pacbin member: {normalized}")) + }) + } + + /// Borrow a complete authenticated member without unpacking it. + pub fn member_bytes(&self, logical_path: &str) -> RusticolResult<&[u8]> { + let member = self.member(logical_path)?; + member_slice(self.bytes.as_ref(), member.offset, member.length) + } + + /// Borrow a bounded range relative to one authenticated member. + pub fn member_range( + &self, + logical_path: &str, + offset: u64, + length: u64, + ) -> RusticolResult<&[u8]> { + let member = self.member(logical_path)?; + if offset > member.length || length > member.length - offset { + return Err(RusticolError::invalid_argument( + "member read exceeds indexed payload bounds", + )); + } + let absolute_offset = member + .offset + .checked_add(offset) + .ok_or_else(|| RusticolError::invalid_argument("member read offset exceeds u64"))?; + member_slice(self.bytes.as_ref(), absolute_offset, length) + } + + /// Re-authenticate all member payloads held by this reader. + pub fn verify_payloads(&self) -> RusticolResult<()> { + verify_payload_digests(self.bytes.as_ref(), &self.index.members) + } + + /// Return the total owned container size. + pub fn container_size(&self) -> usize { + self.bytes.as_ref().len() + } +} + +/// Atomically stream and publish one deterministic `pacbin-v1` container. +/// +/// Member metadata is retained in memory, while payload bytes are copied and +/// hashed through one bounded buffer. The destination remains untouched until +/// a complete temporary file has been flushed and synced. +pub fn write_pacbin_atomic<'a>( + destination: impl AsRef, + members: impl IntoIterator>, + options: PacbinWriteOptions, +) -> RusticolResult { + let destination = destination.as_ref(); + let parent = destination + .parent() + .filter(|path| !path.as_os_str().is_empty()) + .unwrap_or_else(|| Path::new(".")); + if !parent.is_dir() { + return Err(RusticolError::invalid_argument(format!( + "pacbin destination directory does not exist: {}", + parent.display() + ))); + } + let mut members = prepare_write_members(members)?; + let expected_index_size = validate_write_index_bounds(&members)?; + let (temporary_path, mut temporary_file) = create_temporary_file(destination, parent)?; + + let result = (|| { + let index = write_pacbin_file( + &mut temporary_file, + &mut members, + options.chunk_size, + expected_index_size, + )?; + #[cfg(unix)] + temporary_file + .set_permissions(std::fs::Permissions::from_mode(options.mode)) + .map_err(|error| artifact_io_error("set permissions on", &temporary_path, error))?; + temporary_file + .flush() + .map_err(|error| artifact_io_error("flush", &temporary_path, error))?; + temporary_file + .sync_all() + .map_err(|error| artifact_io_error("sync", &temporary_path, error))?; + drop(temporary_file); + std::fs::rename(&temporary_path, destination).map_err(|error| { + RusticolError::artifact(format!( + "could not atomically publish pacbin container {}: {error}", + destination.display() + )) + })?; + sync_directory_best_effort(parent); + Ok(index) + })(); + + if result.is_err() { + let _ = std::fs::remove_file(&temporary_path); + } + result +} + +fn prepare_write_members<'a>( + members: impl IntoIterator>, +) -> RusticolResult>> { + let mut members: Vec<_> = members.into_iter().collect(); + members.sort_by(|left, right| { + left.logical_path + .as_bytes() + .cmp(right.logical_path.as_bytes()) + }); + let mut seen_paths = BTreeSet::new(); + let mut seen_folded = BTreeSet::new(); + for member in &members { + if !seen_paths.insert(member.logical_path.clone()) { + return Err(RusticolError::invalid_argument(format!( + "duplicate pacbin member path: {}", + member.logical_path + ))); + } + let folded = member.logical_path.to_ascii_lowercase(); + if !seen_folded.insert(folded) { + return Err(RusticolError::invalid_argument(format!( + "case-colliding pacbin member path: {}", + member.logical_path + ))); + } + } + Ok(members) +} + +fn validate_write_index_bounds(members: &[PacbinWriteMember<'_>]) -> RusticolResult { + u64::try_from(members.len()) + .map_err(|_| RusticolError::invalid_argument("pacbin member count exceeds u64"))?; + let mut index_size = INDEX_HEADER_SIZE as u64; + for member in members { + let path_length = u64::try_from(member.logical_path.len()).map_err(|_| { + RusticolError::invalid_argument("pacbin logical path exceeds u64 byte length") + })?; + if path_length > u64::from(u32::MAX) { + return Err(RusticolError::invalid_argument(format!( + "pacbin member path exceeds u32 byte length: {path_length} bytes" + ))); + } + let record_size = (INDEX_ENTRY_SIZE as u64) + .checked_add(path_length) + .and_then(|value| value.checked_add(padding_length(value, INDEX_ALIGNMENT))) + .ok_or_else(|| RusticolError::invalid_argument("pacbin index size exceeds u64"))?; + index_size = index_size + .checked_add(record_size) + .ok_or_else(|| RusticolError::invalid_argument("pacbin index size exceeds u64"))?; + } + Ok(index_size) +} + +pub(crate) fn create_temporary_file( + destination: &Path, + parent: &Path, +) -> RusticolResult<(PathBuf, File)> { + let file_name = destination + .file_name() + .ok_or_else(|| RusticolError::invalid_argument("pacbin destination must name a file"))?; + for _ in 0..TEMPORARY_FILE_ATTEMPTS { + let sequence = NEXT_TEMPORARY_FILE.fetch_add(1, Ordering::Relaxed); + let mut temporary_name = OsString::from("."); + temporary_name.push(file_name); + temporary_name.push(format!(".{}.{}.tmp", std::process::id(), sequence)); + let temporary_path = parent.join(temporary_name); + let mut options = OpenOptions::new(); + options.read(true).write(true).create_new(true); + #[cfg(unix)] + options.mode(0o600); + match options.open(&temporary_path) { + Ok(file) => return Ok((temporary_path, file)), + Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => continue, + Err(error) => { + return Err(artifact_io_error( + "create temporary pacbin container", + &temporary_path, + error, + )); + } + } + } + Err(RusticolError::artifact(format!( + "could not allocate a unique temporary pacbin path for {}", + destination.display() + ))) +} + +fn write_pacbin_file( + destination: &mut File, + members: &mut [PacbinWriteMember<'_>], + chunk_size: usize, + expected_index_size: u64, +) -> RusticolResult { + destination.seek(SeekFrom::Start(0)).map_err(|error| { + RusticolError::artifact(format!("could not seek pacbin output: {error}")) + })?; + destination.set_len(0).map_err(|error| { + RusticolError::artifact(format!("could not truncate pacbin output: {error}")) + })?; + write_all(destination, &[0; HEADER_SIZE], "pacbin header")?; + + let mut buffer = Vec::new(); + buffer.try_reserve_exact(chunk_size).map_err(|error| { + RusticolError::artifact(format!( + "could not allocate {chunk_size} byte pacbin streaming buffer: {error}" + )) + })?; + buffer.resize(chunk_size, 0); + + let mut indexed_members = Vec::new(); + indexed_members + .try_reserve_exact(members.len()) + .map_err(|error| { + RusticolError::artifact(format!( + "could not allocate pacbin index for {} members: {error}", + members.len() + )) + })?; + for member in members { + write_alignment_padding(destination, u64::from(PACBIN_ALIGNMENT))?; + let offset = stream_position(destination, "pacbin member offset")?; + let (length, sha256) = match &mut member.source { + PacbinWriteSource::Bytes(payload) => { + let mut source = std::io::Cursor::new(*payload); + stream_member(&mut source, destination, &mut buffer, &member.logical_path)? + } + PacbinWriteSource::Path(path) => { + if !path.is_file() { + return Err(RusticolError::invalid_argument(format!( + "pacbin member source is not a file: {}", + path.display() + ))); + } + let mut source = File::open(&*path) + .map_err(|error| artifact_io_error("open pacbin member source", path, error))?; + stream_member(&mut source, destination, &mut buffer, &member.logical_path)? + } + PacbinWriteSource::Reader(source) => stream_member( + &mut **source, + destination, + &mut buffer, + &member.logical_path, + )?, + }; + indexed_members.push(PacbinMember { + logical_path: member.logical_path.clone(), + kind: member.kind, + offset, + length, + sha256, + }); + } + + write_alignment_padding(destination, u64::from(PACBIN_ALIGNMENT))?; + let index_offset = stream_position(destination, "pacbin index offset")?; + let member_count = u64::try_from(indexed_members.len()) + .map_err(|_| RusticolError::artifact("pacbin member count exceeds u64"))?; + let mut index_digest = Sha256::new(); + let mut index_header = [0_u8; INDEX_HEADER_SIZE]; + index_header[0..8].copy_from_slice(INDEX_MAGIC); + encode_u16(&mut index_header, 8, PACBIN_VERSION); + encode_u16(&mut index_header, 10, INDEX_HEADER_SIZE as u16); + encode_u32(&mut index_header, 12, SUPPORTED_FLAGS); + encode_u64(&mut index_header, 16, member_count); + write_hashed( + destination, + &mut index_digest, + &index_header, + "pacbin index", + )?; + + for member in &indexed_members { + let path_bytes = member.logical_path.as_bytes(); + let path_length = u32::try_from(path_bytes.len()) + .map_err(|_| RusticolError::artifact("pacbin logical path exceeds u32 byte length"))?; + let mut entry = [0_u8; INDEX_ENTRY_SIZE]; + encode_u32(&mut entry, 0, path_length); + encode_u16(&mut entry, 4, member.kind as u16); + encode_u16(&mut entry, 6, SUPPORTED_FLAGS as u16); + encode_u64(&mut entry, 8, member.offset); + encode_u64(&mut entry, 16, member.length); + entry[24..56].copy_from_slice(&member.sha256); + write_hashed(destination, &mut index_digest, &entry, "pacbin index")?; + write_hashed(destination, &mut index_digest, path_bytes, "pacbin index")?; + let record_length = (INDEX_ENTRY_SIZE as u64) + u64::from(path_length); + let padding = padding_length(record_length, INDEX_ALIGNMENT); + if padding != 0 { + let zeros = [0_u8; INDEX_ALIGNMENT as usize]; + let padding = usize::try_from(padding) + .expect("pacbin index padding is smaller than INDEX_ALIGNMENT"); + write_hashed( + destination, + &mut index_digest, + &zeros[..padding], + "pacbin index", + )?; + } + } + let actual_index_size = stream_position(destination, "pacbin index end")? + .checked_sub(index_offset) + .ok_or_else(|| RusticolError::artifact("pacbin index position underflow"))?; + if actual_index_size != expected_index_size { + return Err(RusticolError::integrity( + "pacbin index size disagrees with preflight", + )); + } + let index_sha256: [u8; 32] = index_digest.finalize().into(); + + let mut footer = [0_u8; FOOTER_SIZE]; + footer[0..8].copy_from_slice(FOOTER_MAGIC); + encode_u16(&mut footer, 8, PACBIN_VERSION); + encode_u16(&mut footer, 10, FOOTER_SIZE as u16); + encode_u32(&mut footer, 12, SUPPORTED_FLAGS); + encode_u64(&mut footer, 16, index_offset); + encode_u64(&mut footer, 24, member_count); + footer[32..64].copy_from_slice(&index_sha256); + write_all(destination, &footer, "pacbin footer")?; + let file_size = stream_position(destination, "pacbin file size")?; + + let mut header = [0_u8; HEADER_SIZE]; + header[0..8].copy_from_slice(HEADER_MAGIC); + encode_u16(&mut header, 8, PACBIN_VERSION); + encode_u16(&mut header, 10, HEADER_SIZE as u16); + encode_u32(&mut header, 12, SUPPORTED_FLAGS); + encode_u32(&mut header, 16, PACBIN_ALIGNMENT); + encode_u64(&mut header, 24, index_offset); + encode_u64(&mut header, 32, member_count); + destination.seek(SeekFrom::Start(0)).map_err(|error| { + RusticolError::artifact(format!("could not seek pacbin output: {error}")) + })?; + write_all(destination, &header, "pacbin header")?; + destination + .seek(SeekFrom::Start(file_size)) + .map_err(|error| { + RusticolError::artifact(format!("could not seek pacbin output: {error}")) + })?; + destination.set_len(file_size).map_err(|error| { + RusticolError::artifact(format!("could not truncate pacbin output: {error}")) + })?; + + Ok(PacbinIndex { + version: PACBIN_VERSION, + index_offset, + index_sha256, + file_size, + members: indexed_members, + }) +} + +fn stream_member( + source: &mut R, + destination: &mut File, + buffer: &mut [u8], + logical_path: &str, +) -> RusticolResult<(u64, [u8; 32])> { + let mut digest = Sha256::new(); + let mut length = 0_u64; + loop { + let read = source.read(buffer).map_err(|error| { + RusticolError::artifact(format!( + "could not read pacbin member {logical_path}: {error}" + )) + })?; + if read == 0 { + break; + } + length = length + .checked_add(u64::try_from(read).expect("read byte count fits u64")) + .ok_or_else(|| RusticolError::artifact("pacbin member length exceeds u64"))?; + digest.update(&buffer[..read]); + write_all(destination, &buffer[..read], "pacbin member payload")?; + } + Ok((length, digest.finalize().into())) +} + +fn write_alignment_padding(destination: &mut File, alignment: u64) -> RusticolResult<()> { + let position = stream_position(destination, "pacbin output position")?; + let padding = padding_length(position, alignment); + if padding != 0 { + let zeros = [0_u8; PACBIN_ALIGNMENT as usize]; + let padding = usize::try_from(padding) + .expect("pacbin payload padding is smaller than PACBIN_ALIGNMENT"); + write_all(destination, &zeros[..padding], "pacbin payload padding")?; + } + Ok(()) +} + +fn write_hashed( + destination: &mut File, + digest: &mut Sha256, + payload: &[u8], + label: &str, +) -> RusticolResult<()> { + write_all(destination, payload, label)?; + digest.update(payload); + Ok(()) +} + +fn write_all(destination: &mut File, payload: &[u8], label: &str) -> RusticolResult<()> { + destination.write_all(payload).map_err(|error| { + RusticolError::artifact(format!("short write while writing {label}: {error}")) + }) +} + +fn stream_position(destination: &mut File, label: &str) -> RusticolResult { + destination + .stream_position() + .map_err(|error| RusticolError::artifact(format!("could not read {label}: {error}"))) +} + +fn encode_u16(bytes: &mut [u8], offset: usize, value: u16) { + bytes[offset..offset + 2].copy_from_slice(&value.to_le_bytes()); +} + +fn encode_u32(bytes: &mut [u8], offset: usize, value: u32) { + bytes[offset..offset + 4].copy_from_slice(&value.to_le_bytes()); +} + +fn encode_u64(bytes: &mut [u8], offset: usize, value: u64) { + bytes[offset..offset + 8].copy_from_slice(&value.to_le_bytes()); +} + +fn artifact_io_error(action: &str, path: &Path, error: std::io::Error) -> RusticolError { + RusticolError::artifact(format!("could not {action} {}: {error}", path.display())) +} + +#[cfg(unix)] +fn sync_directory_best_effort(path: &Path) { + if let Ok(directory) = File::open(path) { + let _ = directory.sync_all(); + } +} + +#[cfg(not(unix))] +fn sync_directory_best_effort(_path: &Path) {} + +/// Normalize a portable ASCII member path using the Python codec's POSIX rules. +/// +/// Generated evaluator paths are ASCII. Rejecting non-ASCII input is +/// intentional: without weakening Python's NFC and full Unicode case-folding +/// collision guarantees, the runtime accepts the complete generated-path +/// domain and fails closed for paths outside it. +pub fn normalize_logical_path(value: &str) -> RusticolResult { + normalize_logical_path_impl(value).map_err(RusticolError::invalid_argument) +} + +fn normalize_logical_path_impl(value: &str) -> Result { + if value.is_empty() || value.contains('\0') { + return Err("pacbin logical path must be non-empty and contain no NUL".to_string()); + } + if !value.is_ascii() { + return Err( + "pacbin logical path must use the portable ASCII evaluator-path subset".to_string(), + ); + } + if value.contains('\\') { + return Err("pacbin logical path must use POSIX '/' separators".to_string()); + } + if value.starts_with('/') { + return Err("pacbin logical path must be relative".to_string()); + } + + let mut parts = Vec::new(); + for part in value.split('/') { + if part.is_empty() || part == "." { + continue; + } + if part == ".." { + return Err("pacbin logical path must not contain '..'".to_string()); + } + parts.push(part); + } + if parts.is_empty() { + return Err("pacbin logical path must name a member".to_string()); + } + Ok(parts.join("/")) +} + +#[cfg_attr(target_vendor = "apple", unsafe(link_section = "__TEXT,__rcl_load"))] +#[cfg_attr(target_vendor = "apple", inline(never))] +fn parse_and_validate(bytes: &[u8], verify_payloads: bool) -> RusticolResult { + let minimum_size = HEADER_SIZE + .checked_add(INDEX_HEADER_SIZE) + .and_then(|value| value.checked_add(FOOTER_SIZE)) + .expect("pacbin fixed structure sizes fit usize"); + if bytes.len() < minimum_size { + return Err(integrity("truncated pacbin container")); + } + let file_size = + u64::try_from(bytes.len()).map_err(|_| integrity("pacbin file size exceeds u64"))?; + + let header = checked_slice(bytes, 0, HEADER_SIZE, "pacbin header")?; + if &header[0..8] != HEADER_MAGIC { + return Err(integrity("invalid pacbin header magic")); + } + let version = u16_at(header, 8, "pacbin header version")?; + let header_size = u16_at(header, 10, "pacbin header size")?; + let header_flags = u32_at(header, 12, "pacbin header flags")?; + validate_contract("header", version, header_size, HEADER_SIZE, header_flags)?; + let alignment = u32_at(header, 16, "pacbin payload alignment")?; + if alignment != PACBIN_ALIGNMENT { + return Err(compatibility(format!( + "unsupported pacbin payload alignment: {alignment}" + ))); + } + if u32_at(header, 20, "pacbin header reserved field")? != 0 + || header[40..64].iter().any(|value| *value != 0) + { + return Err(integrity("pacbin header reserved fields must be zero")); + } + let index_offset = u64_at(header, 24, "pacbin index offset")?; + let member_count = u64_at(header, 32, "pacbin member count")?; + if index_offset % u64::from(PACBIN_ALIGNMENT) != 0 { + return Err(integrity("pacbin index offset is not payload-aligned")); + } + + let footer_offset = bytes + .len() + .checked_sub(FOOTER_SIZE) + .ok_or_else(|| integrity("truncated pacbin footer"))?; + let footer_offset_u64 = + u64::try_from(footer_offset).map_err(|_| integrity("pacbin footer offset exceeds u64"))?; + if index_offset < HEADER_SIZE as u64 || index_offset >= footer_offset_u64 { + return Err(integrity("pacbin index offset is out of bounds")); + } + + let footer = checked_slice(bytes, footer_offset, FOOTER_SIZE, "pacbin footer")?; + if &footer[0..8] != FOOTER_MAGIC { + return Err(integrity("invalid pacbin footer magic")); + } + let footer_version = u16_at(footer, 8, "pacbin footer version")?; + let footer_size = u16_at(footer, 10, "pacbin footer size")?; + let footer_flags = u32_at(footer, 12, "pacbin footer flags")?; + validate_contract( + "footer", + footer_version, + footer_size, + FOOTER_SIZE, + footer_flags, + )?; + if u64_at(footer, 16, "pacbin footer index offset")? != index_offset { + return Err(integrity( + "pacbin footer index offset disagrees with header", + )); + } + if u64_at(footer, 24, "pacbin footer member count")? != member_count { + return Err(integrity( + "pacbin footer member count disagrees with header", + )); + } + let expected_index_digest: [u8; 32] = footer[32..64] + .try_into() + .map_err(|_| integrity("truncated pacbin footer digest"))?; + + let index_offset_usize = usize::try_from(index_offset) + .map_err(|_| integrity("pacbin index offset exceeds platform bounds"))?; + let available_index_bytes = footer_offset_u64 - index_offset; + validate_index_bounds(member_count, available_index_bytes)?; + let index_bytes = checked_slice( + bytes, + index_offset_usize, + usize::try_from(available_index_bytes) + .map_err(|_| integrity("pacbin index size exceeds platform bounds"))?, + "pacbin index", + )?; + let actual_index_digest: [u8; 32] = Sha256::digest(index_bytes).into(); + + let index_header = checked_slice(index_bytes, 0, INDEX_HEADER_SIZE, "pacbin index header")?; + if &index_header[0..8] != INDEX_MAGIC { + return Err(integrity("invalid pacbin index magic")); + } + let index_version = u16_at(index_header, 8, "pacbin index version")?; + let index_header_size = u16_at(index_header, 10, "pacbin index size")?; + let index_flags = u32_at(index_header, 12, "pacbin index flags")?; + validate_contract( + "index", + index_version, + index_header_size, + INDEX_HEADER_SIZE, + index_flags, + )?; + if u64_at(index_header, 24, "pacbin index reserved field")? != 0 { + return Err(integrity("pacbin index reserved field must be zero")); + } + if u64_at(index_header, 16, "pacbin index member count")? != member_count { + return Err(integrity("pacbin index member count disagrees with header")); + } + + let member_capacity = usize::try_from(member_count) + .map_err(|_| integrity("pacbin member count exceeds platform bounds"))?; + let mut members = Vec::new(); + members + .try_reserve_exact(member_capacity) + .map_err(|error| { + integrity(format!( + "could not allocate pacbin index for {member_count} members: {error}" + )) + })?; + let mut seen_paths = BTreeSet::new(); + let mut seen_folded_paths = BTreeSet::new(); + let mut previous_path_bytes: Option> = None; + let mut cursor = INDEX_HEADER_SIZE; + for _ in 0..member_count { + let entry = checked_slice(index_bytes, cursor, INDEX_ENTRY_SIZE, "pacbin index entry")?; + let path_length = u32_at(entry, 0, "pacbin member path length")?; + let kind = PacbinMemberKind::parse(u16_at(entry, 4, "pacbin member kind")?)?; + let entry_flags = u16_at(entry, 6, "pacbin member flags")?; + if entry_flags != SUPPORTED_FLAGS as u16 { + return Err(compatibility(format!( + "unknown pacbin member flags: {entry_flags}" + ))); + } + let offset = u64_at(entry, 8, "pacbin member offset")?; + let length = u64_at(entry, 16, "pacbin member length")?; + let sha256: [u8; 32] = entry[24..56] + .try_into() + .map_err(|_| integrity("truncated pacbin member digest"))?; + cursor = cursor + .checked_add(INDEX_ENTRY_SIZE) + .ok_or_else(|| integrity("pacbin index cursor overflow"))?; + let path_length_usize = usize::try_from(path_length) + .map_err(|_| integrity("pacbin member path length exceeds platform bounds"))?; + let path_bytes = + checked_slice(index_bytes, cursor, path_length_usize, "pacbin member path")?; + let logical_path = std::str::from_utf8(path_bytes) + .map_err(|_| integrity("pacbin member path is not valid UTF-8"))?; + let normalized = normalize_logical_path_impl(logical_path).map_err(integrity)?; + if normalized != logical_path { + return Err(integrity(format!( + "pacbin member path is not canonical: {logical_path:?}" + ))); + } + let folded = logical_path.to_ascii_lowercase(); + if !seen_paths.insert(logical_path.to_string()) { + return Err(integrity(format!( + "duplicate pacbin member path: {logical_path}" + ))); + } + if !seen_folded_paths.insert(folded) { + return Err(integrity(format!( + "case-colliding pacbin member path: {logical_path}" + ))); + } + if previous_path_bytes + .as_deref() + .is_some_and(|previous| path_bytes <= previous) + { + return Err(integrity("pacbin index paths are not strictly sorted")); + } + previous_path_bytes = Some(path_bytes.to_vec()); + cursor = cursor + .checked_add(path_length_usize) + .ok_or_else(|| integrity("pacbin index cursor overflow"))?; + let record_length = (INDEX_ENTRY_SIZE as u64) + .checked_add(u64::from(path_length)) + .ok_or_else(|| integrity("pacbin index record length exceeds u64"))?; + let padding = padding_length(record_length, INDEX_ALIGNMENT); + let padding_usize = usize::try_from(padding) + .map_err(|_| integrity("pacbin index padding exceeds platform bounds"))?; + let padding_bytes = + checked_slice(index_bytes, cursor, padding_usize, "pacbin index padding")?; + if padding_bytes.iter().any(|value| *value != 0) { + return Err(integrity("pacbin index padding must be zero")); + } + cursor = cursor + .checked_add(padding_usize) + .ok_or_else(|| integrity("pacbin index cursor overflow"))?; + members.push(PacbinMember { + logical_path: logical_path.to_string(), + kind, + offset, + length, + sha256, + }); + } + if cursor != index_bytes.len() { + return Err(integrity("pacbin index has trailing or missing bytes")); + } + if actual_index_digest != expected_index_digest { + return Err(integrity("pacbin index digest mismatch")); + } + + validate_canonical_payload_layout(bytes, &members, index_offset)?; + if verify_payloads { + verify_payload_digests(bytes, &members)?; + } + Ok(PacbinIndex { + version, + index_offset, + index_sha256: actual_index_digest, + file_size, + members, + }) +} + +fn validate_contract( + label: &str, + version: u16, + encoded_size: u16, + expected_size: usize, + flags: u32, +) -> RusticolResult<()> { + if version != PACBIN_VERSION { + return Err(compatibility(format!( + "unsupported pacbin {label} version: {version}" + ))); + } + if usize::from(encoded_size) != expected_size { + return Err(compatibility(format!( + "unsupported pacbin {label} size: {encoded_size}" + ))); + } + if flags != SUPPORTED_FLAGS { + return Err(compatibility(format!( + "unknown pacbin {label} flags: {flags}" + ))); + } + Ok(()) +} + +fn validate_index_bounds(member_count: u64, available_index_bytes: u64) -> RusticolResult<()> { + let minimum_record_size = (INDEX_ENTRY_SIZE as u64) + .checked_add(INDEX_ALIGNMENT) + .expect("pacbin minimum record size fits u64"); + if member_count > available_index_bytes / minimum_record_size { + return Err(integrity("pacbin member count cannot fit in index")); + } + Ok(()) +} + +fn validate_canonical_payload_layout( + bytes: &[u8], + members: &[PacbinMember], + index_offset: u64, +) -> RusticolResult<()> { + let mut expected_offset = HEADER_SIZE as u64; + for member in members { + member.offset.checked_add(member.length).ok_or_else(|| { + integrity(format!( + "pacbin member range overflows u64: {}", + member.logical_path + )) + })?; + let unaligned_offset = expected_offset; + let padding = padding_length(unaligned_offset, u64::from(PACBIN_ALIGNMENT)); + expected_offset = expected_offset + .checked_add(padding) + .ok_or_else(|| integrity("pacbin payload alignment exceeds u64"))?; + if member.offset < expected_offset { + return Err(integrity(format!( + "overlapping pacbin member payload: {}", + member.logical_path + ))); + } + if member.offset > expected_offset { + return Err(integrity(format!( + "non-canonical pacbin member gap: {}", + member.logical_path + ))); + } + if member.offset % u64::from(PACBIN_ALIGNMENT) != 0 { + return Err(integrity(format!( + "unaligned pacbin member payload: {}", + member.logical_path + ))); + } + let end = member.offset + member.length; + if end > index_offset { + return Err(integrity(format!( + "pacbin member payload is out of bounds: {}", + member.logical_path + ))); + } + validate_zero_region(bytes, unaligned_offset, padding, "pacbin payload padding")?; + expected_offset = end; + } + + let trailing_padding = padding_length(expected_offset, u64::from(PACBIN_ALIGNMENT)); + let canonical_index_offset = expected_offset + .checked_add(trailing_padding) + .ok_or_else(|| integrity("pacbin payload region exceeds u64"))?; + if canonical_index_offset != index_offset { + if canonical_index_offset > index_offset { + return Err(integrity("pacbin payloads overlap the index")); + } + return Err(integrity( + "pacbin payload region has a non-canonical trailing gap", + )); + } + validate_zero_region( + bytes, + expected_offset, + trailing_padding, + "pacbin payload padding", + )?; + Ok(()) +} + +fn verify_payload_digests(bytes: &[u8], members: &[PacbinMember]) -> RusticolResult<()> { + for member in members { + let payload = member_slice(bytes, member.offset, member.length)?; + let digest: [u8; 32] = Sha256::digest(payload).into(); + if digest != member.sha256 { + return Err(integrity(format!( + "pacbin member digest mismatch: {}", + member.logical_path + ))); + } + } + Ok(()) +} + +fn member_slice(bytes: &[u8], offset: u64, length: u64) -> RusticolResult<&[u8]> { + let start = usize::try_from(offset) + .map_err(|_| integrity("pacbin member offset exceeds platform bounds"))?; + let length = usize::try_from(length) + .map_err(|_| integrity("pacbin member length exceeds platform bounds"))?; + checked_slice(bytes, start, length, "pacbin member payload") +} + +fn validate_zero_region(bytes: &[u8], offset: u64, length: u64, label: &str) -> RusticolResult<()> { + let start = usize::try_from(offset) + .map_err(|_| integrity(format!("{label} offset exceeds platform bounds")))?; + let length = usize::try_from(length) + .map_err(|_| integrity(format!("{label} length exceeds platform bounds")))?; + let region = checked_slice(bytes, start, length, label)?; + if region.iter().any(|value| *value != 0) { + return Err(integrity(format!("{label} must be zero"))); + } + Ok(()) +} + +fn checked_slice<'a>( + bytes: &'a [u8], + offset: usize, + length: usize, + label: &str, +) -> RusticolResult<&'a [u8]> { + let end = offset + .checked_add(length) + .ok_or_else(|| integrity(format!("{label} range overflow")))?; + bytes + .get(offset..end) + .ok_or_else(|| integrity(format!("truncated {label}"))) +} + +fn u16_at(bytes: &[u8], offset: usize, label: &str) -> RusticolResult { + let value: [u8; 2] = checked_slice(bytes, offset, 2, label)? + .try_into() + .map_err(|_| integrity(format!("truncated {label}")))?; + Ok(u16::from_le_bytes(value)) +} + +fn u32_at(bytes: &[u8], offset: usize, label: &str) -> RusticolResult { + let value: [u8; 4] = checked_slice(bytes, offset, 4, label)? + .try_into() + .map_err(|_| integrity(format!("truncated {label}")))?; + Ok(u32::from_le_bytes(value)) +} + +fn u64_at(bytes: &[u8], offset: usize, label: &str) -> RusticolResult { + let value: [u8; 8] = checked_slice(bytes, offset, 8, label)? + .try_into() + .map_err(|_| integrity(format!("truncated {label}")))?; + Ok(u64::from_le_bytes(value)) +} + +fn padding_length(position: u64, alignment: u64) -> u64 { + (alignment - position % alignment) % alignment +} + +fn integrity(message: impl Into) -> RusticolError { + RusticolError::integrity(message) +} + +fn compatibility(message: impl Into) -> RusticolError { + RusticolError::compatibility(message) +} + +#[cfg(test)] +#[path = "pacbin_tests.rs"] +mod tests; diff --git a/pyamplicol/rust/crates/rusticol-core/src/pacbin_tests.rs b/pyamplicol/rust/crates/rusticol-core/src/pacbin_tests.rs new file mode 100644 index 0000000000..4afba1b5c4 --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/pacbin_tests.rs @@ -0,0 +1,902 @@ +// SPDX-License-Identifier: 0BSD + +use super::*; +use crate::RusticolErrorKind; +use sha2::{Digest, Sha256}; +use std::fs; +use std::io::{self, Read}; +use std::sync::atomic::{AtomicU64, Ordering}; + +const PYTHON_GOLDEN_HEX: &str = concat!( + "50414342494e000001004000000000004000000000000000c000000000000000", + "0200000000000000000000000000000000000000000000000000000000000000", + "6a69742d76310000000000000000000000000000000000000000000000000000", + "0000000000000000000000000000000000000000000000000000000000000000", + "65786163742d7374617465000000000000000000000000000000000000000000", + "0000000000000000000000000000000000000000000000000000000000000000", + "5041434944580000010020000000000002000000000000000000000000000000", + "0c0000000100000040000000000000000600000000000000e9a628134c31e4a5", + "7771ecd7a1dd51c5c5bc7daca10a2553c83f8af1652d2587612f6a69742e7379", + "6d6a6974000000000b0000000200000080000000000000000b00000000000000", + "a9f46d72eb6f1c7e5d62e97d3d8e67b80605aeba2df022a8a25b51ca3fc89805", + "7a2f65786163742e62696e0000000000504143454e4400000100400000000000", + "c0000000000000000200000000000000263b30eb85e5b2a82600201822438767", + "c5f66d9eecf2e08a0202383aafe0e3c1", +); + +#[derive(Clone)] +struct TestMember<'a> { + path: &'a str, + kind: PacbinMemberKind, + payload: &'a [u8], +} + +fn decode_hex(value: &str) -> Vec { + assert_eq!(value.len() % 2, 0); + value + .as_bytes() + .chunks_exact(2) + .map(|pair| { + let text = std::str::from_utf8(pair).expect("ASCII hex pair"); + u8::from_str_radix(text, 16).expect("valid hex pair") + }) + .collect() +} + +fn put_u16(bytes: &mut [u8], offset: usize, value: u16) { + bytes[offset..offset + 2].copy_from_slice(&value.to_le_bytes()); +} + +fn put_u32(bytes: &mut [u8], offset: usize, value: u32) { + bytes[offset..offset + 4].copy_from_slice(&value.to_le_bytes()); +} + +fn put_u64(bytes: &mut [u8], offset: usize, value: u64) { + bytes[offset..offset + 8].copy_from_slice(&value.to_le_bytes()); +} + +fn align(bytes: &mut Vec, alignment: usize) { + let padding = (alignment - bytes.len() % alignment) % alignment; + bytes.resize(bytes.len() + padding, 0); +} + +fn build_container(members: &[TestMember<'_>]) -> Vec { + let mut bytes = vec![0; HEADER_SIZE]; + let mut records = Vec::new(); + for member in members { + align(&mut bytes, PACBIN_ALIGNMENT as usize); + let offset = bytes.len() as u64; + bytes.extend_from_slice(member.payload); + records.push((member, offset, Sha256::digest(member.payload))); + } + align(&mut bytes, PACBIN_ALIGNMENT as usize); + let index_offset = bytes.len() as u64; + bytes.extend_from_slice(INDEX_MAGIC); + bytes.extend_from_slice(&PACBIN_VERSION.to_le_bytes()); + bytes.extend_from_slice(&(INDEX_HEADER_SIZE as u16).to_le_bytes()); + bytes.extend_from_slice(&SUPPORTED_FLAGS.to_le_bytes()); + bytes.extend_from_slice(&(records.len() as u64).to_le_bytes()); + bytes.extend_from_slice(&0_u64.to_le_bytes()); + for (member, offset, digest) in records { + let path = member.path.as_bytes(); + bytes.extend_from_slice(&(path.len() as u32).to_le_bytes()); + bytes.extend_from_slice(&(member.kind as u16).to_le_bytes()); + bytes.extend_from_slice(&0_u16.to_le_bytes()); + bytes.extend_from_slice(&offset.to_le_bytes()); + bytes.extend_from_slice(&(member.payload.len() as u64).to_le_bytes()); + bytes.extend_from_slice(&digest); + bytes.extend_from_slice(path); + align(&mut bytes, INDEX_ALIGNMENT as usize); + } + let index_digest = Sha256::digest(&bytes[index_offset as usize..]); + bytes.extend_from_slice(FOOTER_MAGIC); + bytes.extend_from_slice(&PACBIN_VERSION.to_le_bytes()); + bytes.extend_from_slice(&(FOOTER_SIZE as u16).to_le_bytes()); + bytes.extend_from_slice(&SUPPORTED_FLAGS.to_le_bytes()); + bytes.extend_from_slice(&index_offset.to_le_bytes()); + bytes.extend_from_slice(&(members.len() as u64).to_le_bytes()); + bytes.extend_from_slice(&index_digest); + + bytes[0..8].copy_from_slice(HEADER_MAGIC); + put_u16(&mut bytes, 8, PACBIN_VERSION); + put_u16(&mut bytes, 10, HEADER_SIZE as u16); + put_u32(&mut bytes, 12, SUPPORTED_FLAGS); + put_u32(&mut bytes, 16, PACBIN_ALIGNMENT); + put_u32(&mut bytes, 20, 0); + put_u64(&mut bytes, 24, index_offset); + put_u64(&mut bytes, 32, members.len() as u64); + bytes +} + +fn index_offset(bytes: &[u8]) -> usize { + u64_at(bytes, 24, "test index offset").expect("index offset") as usize +} + +fn footer_offset(bytes: &[u8]) -> usize { + bytes.len() - FOOTER_SIZE +} + +fn entry_offsets(bytes: &[u8]) -> Vec { + let index = index_offset(bytes); + let footer = footer_offset(bytes); + let count = u64_at(bytes, 32, "test member count").expect("member count"); + let mut cursor = index + INDEX_HEADER_SIZE; + let mut offsets = Vec::new(); + for _ in 0..count { + offsets.push(cursor); + let path_length = u32_at(bytes, cursor, "test path length").expect("path length"); + let record = INDEX_ENTRY_SIZE as u64 + u64::from(path_length); + cursor += (record + padding_length(record, INDEX_ALIGNMENT)) as usize; + } + assert!(cursor <= footer); + offsets +} + +fn rewrite_index_digest(bytes: &mut [u8]) { + let index = index_offset(bytes); + let footer = footer_offset(bytes); + let digest = Sha256::digest(&bytes[index..footer]); + bytes[footer + 32..footer + 64].copy_from_slice(&digest); +} + +fn error(bytes: Vec) -> (RusticolErrorKind, String) { + let error = PacbinReader::from_bytes(bytes).expect_err("container must be rejected"); + (error.kind(), error.to_string()) +} + +fn assert_error(bytes: Vec, kind: RusticolErrorKind, message: &str) { + let (actual_kind, actual_message) = error(bytes); + assert_eq!(actual_kind, kind); + assert!( + actual_message.contains(message), + "expected {actual_message:?} to contain {message:?}" + ); +} + +#[test] +fn python_golden_round_trips_and_supports_indexed_borrowed_ranges() { + let bytes = decode_hex(PYTHON_GOLDEN_HEX); + let reader = PacbinReader::from_bytes(bytes).expect("Python pacbin-v1 golden container"); + assert_eq!(reader.index().version(), 1); + assert_eq!(reader.index().index_offset(), 192); + assert_eq!(reader.index().file_size(), 432); + assert_eq!(reader.container_size(), 432); + assert_eq!(reader.members().len(), 2); + assert_eq!( + reader.member("a//./jit.symjit").unwrap().kind(), + PacbinMemberKind::SymjitApplication + ); + assert_eq!(reader.member_bytes("a/jit.symjit").unwrap(), b"jit-v1"); + assert_eq!(reader.member_range("z/exact.bin", 2, 5).unwrap(), b"act-s"); + reader.verify_payloads().unwrap(); + + let missing = reader.member("missing.bin").unwrap_err(); + assert_eq!(missing.kind(), RusticolErrorKind::InvalidArgument); + assert_eq!(missing.to_string(), "unknown pacbin member: missing.bin"); + let out_of_bounds = reader.member_range("a/jit.symjit", 5, 2).unwrap_err(); + assert_eq!(out_of_bounds.kind(), RusticolErrorKind::InvalidArgument); + assert_eq!( + out_of_bounds.to_string(), + "member read exceeds indexed payload bounds" + ); +} + +#[test] +fn open_keeps_one_immutable_snapshot_after_atomic_source_replacement() { + static NEXT_PATH: AtomicU64 = AtomicU64::new(0); + let path = std::env::temp_dir().join(format!( + "rusticol-pacbin-{}-{}.pacbin", + std::process::id(), + NEXT_PATH.fetch_add(1, Ordering::Relaxed) + )); + fs::write(&path, decode_hex(PYTHON_GOLDEN_HEX)).unwrap(); + let reader = PacbinReader::open(&path).unwrap(); + let replacement = path.with_extension("replacement"); + fs::write(&replacement, b"replaced after open").unwrap(); + fs::rename(&replacement, &path).unwrap(); + assert_eq!(reader.member_bytes("a/jit.symjit").unwrap(), b"jit-v1"); + fs::remove_file(path).unwrap(); +} + +#[test] +fn expected_container_digest_authenticates_an_immutable_snapshot() { + static NEXT_PATH: AtomicU64 = AtomicU64::new(0); + let path = std::env::temp_dir().join(format!( + "rusticol-pacbin-authenticated-{}-{}.pacbin", + std::process::id(), + NEXT_PATH.fetch_add(1, Ordering::Relaxed) + )); + let bytes = decode_hex(PYTHON_GOLDEN_HEX); + let digest: [u8; 32] = Sha256::digest(&bytes).into(); + fs::write(&path, &bytes).unwrap(); + let reader = PacbinReader::open_with_sha256(&path, &digest).unwrap(); + + fs::write(&path, vec![0_u8; bytes.len()]).unwrap(); + assert_eq!(reader.member_bytes("a/jit.symjit").unwrap(), b"jit-v1"); + reader.verify_payloads().unwrap(); + OpenOptions::new() + .write(true) + .open(&path) + .unwrap() + .set_len(0) + .unwrap(); + assert_eq!(reader.member_bytes("a/jit.symjit").unwrap(), b"jit-v1"); + reader.verify_payloads().unwrap(); + + let replacement = path.with_extension("replacement"); + fs::write(&replacement, b"replaced after authenticated open").unwrap(); + fs::rename(&replacement, &path).unwrap(); + assert_eq!(reader.member_bytes("a/jit.symjit").unwrap(), b"jit-v1"); + + let error = PacbinReader::open_with_sha256(&path, &digest).unwrap_err(); + assert_eq!(error.kind(), RusticolErrorKind::Integrity); + assert!(error.to_string().contains("payload digest mismatch")); + fs::remove_file(path).unwrap(); +} + +#[test] +fn empty_and_zero_length_members_are_valid() { + let empty = PacbinReader::from_bytes(build_container(&[])).unwrap(); + assert!(empty.members().is_empty()); + + let zero = build_container(&[ + TestMember { + path: "a.empty", + kind: PacbinMemberKind::SymjitApplication, + payload: b"", + }, + TestMember { + path: "b.empty", + kind: PacbinMemberKind::SymbolicaExactState, + payload: b"", + }, + ]); + let zero = PacbinReader::from_bytes(zero).unwrap(); + assert_eq!(zero.members()[0].offset(), zero.members()[1].offset()); + assert_eq!(zero.member_bytes("b.empty").unwrap(), b""); +} + +#[test] +fn header_footer_and_index_contracts_are_strict() { + let golden = decode_hex(PYTHON_GOLDEN_HEX); + for (offset, replacement, kind, message) in [ + (0, vec![b'X'], RusticolErrorKind::Integrity, "header magic"), + ( + 8, + 2_u16.to_le_bytes().to_vec(), + RusticolErrorKind::Compatibility, + "header version", + ), + ( + 10, + 1_u16.to_le_bytes().to_vec(), + RusticolErrorKind::Compatibility, + "header size", + ), + ( + 12, + 1_u32.to_le_bytes().to_vec(), + RusticolErrorKind::Compatibility, + "header flags", + ), + ( + 16, + 32_u32.to_le_bytes().to_vec(), + RusticolErrorKind::Compatibility, + "payload alignment", + ), + ( + 20, + 1_u32.to_le_bytes().to_vec(), + RusticolErrorKind::Integrity, + "reserved fields", + ), + ] { + let mut bytes = golden.clone(); + bytes[offset..offset + replacement.len()].copy_from_slice(&replacement); + assert_error(bytes, kind, message); + } + let mut reserved = golden.clone(); + reserved[63] = 1; + assert_error(reserved, RusticolErrorKind::Integrity, "reserved fields"); + let mut unaligned_index = golden.clone(); + put_u64(&mut unaligned_index, 24, 193); + assert_error( + unaligned_index, + RusticolErrorKind::Integrity, + "index offset is not payload-aligned", + ); + let mut out_of_bounds_index = golden.clone(); + put_u64(&mut out_of_bounds_index, 24, 0); + let footer = footer_offset(&out_of_bounds_index); + put_u64(&mut out_of_bounds_index, footer + 16, 0); + assert_error( + out_of_bounds_index, + RusticolErrorKind::Integrity, + "index offset is out of bounds", + ); + + let footer = footer_offset(&golden); + for (offset, replacement, kind, message) in [ + ( + footer, + vec![b'X'], + RusticolErrorKind::Integrity, + "footer magic", + ), + ( + footer + 8, + 2_u16.to_le_bytes().to_vec(), + RusticolErrorKind::Compatibility, + "footer version", + ), + ( + footer + 10, + 1_u16.to_le_bytes().to_vec(), + RusticolErrorKind::Compatibility, + "footer size", + ), + ( + footer + 12, + 1_u32.to_le_bytes().to_vec(), + RusticolErrorKind::Compatibility, + "footer flags", + ), + ( + footer + 16, + 0_u64.to_le_bytes().to_vec(), + RusticolErrorKind::Integrity, + "offset disagrees", + ), + ( + footer + 24, + 99_u64.to_le_bytes().to_vec(), + RusticolErrorKind::Integrity, + "count disagrees", + ), + ] { + let mut bytes = golden.clone(); + bytes[offset..offset + replacement.len()].copy_from_slice(&replacement); + assert_error(bytes, kind, message); + } + let index = index_offset(&golden); + let mut index_count = golden.clone(); + put_u64(&mut index_count, index + 16, 1); + rewrite_index_digest(&mut index_count); + assert_error( + index_count, + RusticolErrorKind::Integrity, + "index member count disagrees with header", + ); + + for (offset, replacement, kind, message) in [ + ( + index, + vec![b'X'], + RusticolErrorKind::Integrity, + "index magic", + ), + ( + index + 8, + 2_u16.to_le_bytes().to_vec(), + RusticolErrorKind::Compatibility, + "index version", + ), + ( + index + 10, + 1_u16.to_le_bytes().to_vec(), + RusticolErrorKind::Compatibility, + "index size", + ), + ( + index + 12, + 1_u32.to_le_bytes().to_vec(), + RusticolErrorKind::Compatibility, + "index flags", + ), + ( + index + 24, + 1_u64.to_le_bytes().to_vec(), + RusticolErrorKind::Integrity, + "reserved field", + ), + ] { + let mut bytes = golden.clone(); + bytes[offset..offset + replacement.len()].copy_from_slice(&replacement); + rewrite_index_digest(&mut bytes); + assert_error(bytes, kind, message); + } +} + +#[test] +fn count_and_index_size_bounds_fail_before_allocation() { + assert_error( + { + let mut bytes = decode_hex(PYTHON_GOLDEN_HEX); + put_u64(&mut bytes, 32, u64::MAX); + let footer = footer_offset(&bytes); + put_u64(&mut bytes, footer + 24, u64::MAX); + bytes + }, + RusticolErrorKind::Integrity, + "member count cannot fit in index", + ); + validate_index_bounds(0, 256 * 1024 * 1024 + 1).unwrap(); + let impossible = validate_index_bounds(2, 64).unwrap_err(); + assert_eq!( + impossible.to_string(), + "pacbin member count cannot fit in index" + ); +} + +#[test] +fn member_contract_path_bounds_and_utf8_are_strict() { + let golden = decode_hex(PYTHON_GOLDEN_HEX); + let entry = entry_offsets(&golden)[0]; + let mut kind = golden.clone(); + put_u16(&mut kind, entry + 4, 99); + rewrite_index_digest(&mut kind); + assert_error(kind, RusticolErrorKind::Compatibility, "member kind"); + + let mut flags = golden.clone(); + put_u16(&mut flags, entry + 6, 1); + rewrite_index_digest(&mut flags); + assert_error(flags, RusticolErrorKind::Compatibility, "member flags"); + + let mut path_bound = golden.clone(); + put_u32(&mut path_bound, entry, u32::MAX); + rewrite_index_digest(&mut path_bound); + assert_error( + path_bound, + RusticolErrorKind::Integrity, + "pacbin member path", + ); + + let mut utf8 = golden.clone(); + utf8[entry + INDEX_ENTRY_SIZE] = 0xff; + rewrite_index_digest(&mut utf8); + assert_error(utf8, RusticolErrorKind::Integrity, "not valid UTF-8"); + + let unicode = build_container(&[TestMember { + path: "café/state.bin", + kind: PacbinMemberKind::SymbolicaExactState, + payload: b"state", + }]); + assert_error(unicode, RusticolErrorKind::Integrity, "portable ASCII"); +} + +#[test] +fn paths_must_be_canonical_unique_case_distinct_and_sorted() { + for (members, message) in [ + ( + vec![ + TestMember { + path: "same.bin", + kind: PacbinMemberKind::SymjitApplication, + payload: b"a", + }, + TestMember { + path: "same.bin", + kind: PacbinMemberKind::NativeLibrary, + payload: b"b", + }, + ], + "duplicate", + ), + ( + vec![ + TestMember { + path: "Case.bin", + kind: PacbinMemberKind::SymjitApplication, + payload: b"a", + }, + TestMember { + path: "case.bin", + kind: PacbinMemberKind::NativeLibrary, + payload: b"b", + }, + ], + "case-colliding", + ), + ( + vec![ + TestMember { + path: "z.bin", + kind: PacbinMemberKind::SymjitApplication, + payload: b"a", + }, + TestMember { + path: "a.bin", + kind: PacbinMemberKind::NativeLibrary, + payload: b"b", + }, + ], + "strictly sorted", + ), + ] { + assert_error( + build_container(&members), + RusticolErrorKind::Integrity, + message, + ); + } + + let noncanonical = build_container(&[TestMember { + path: "../escape.bin", + kind: PacbinMemberKind::SymjitApplication, + payload: b"payload", + }]); + assert_error( + noncanonical, + RusticolErrorKind::Integrity, + "must not contain '..'", + ); +} + +#[test] +fn index_padding_and_exact_coverage_are_enforced() { + let mut padding = decode_hex(PYTHON_GOLDEN_HEX); + let first = entry_offsets(&padding)[0]; + let path_length = u32_at(&padding, first, "path length").unwrap() as usize; + padding[first + INDEX_ENTRY_SIZE + path_length] = 1; + rewrite_index_digest(&mut padding); + assert_error(padding, RusticolErrorKind::Integrity, "index padding"); + + let mut trailing = decode_hex(PYTHON_GOLDEN_HEX); + let footer = footer_offset(&trailing); + trailing.splice(footer..footer, [0_u8; 8]); + rewrite_index_digest(&mut trailing); + assert_error( + trailing, + RusticolErrorKind::Integrity, + "trailing or missing bytes", + ); +} + +#[test] +fn payload_layout_rejects_overlap_gap_bounds_overflow_and_nonzero_padding() { + let golden = decode_hex(PYTHON_GOLDEN_HEX); + let entries = entry_offsets(&golden); + let first_offset = u64_at(&golden, entries[0] + 8, "first offset").unwrap(); + + let mut overlap = golden.clone(); + put_u64(&mut overlap, entries[1] + 8, first_offset); + rewrite_index_digest(&mut overlap); + assert_error(overlap, RusticolErrorKind::Integrity, "overlapping"); + + let mut gap = golden.clone(); + put_u64(&mut gap, entries[1] + 8, 192); + rewrite_index_digest(&mut gap); + assert_error( + gap, + RusticolErrorKind::Integrity, + "non-canonical pacbin member gap", + ); + + let mut out_of_bounds = golden.clone(); + put_u64(&mut out_of_bounds, entries[0] + 16, 192); + rewrite_index_digest(&mut out_of_bounds); + assert_error(out_of_bounds, RusticolErrorKind::Integrity, "out of bounds"); + + let mut overflow = golden.clone(); + put_u64(&mut overflow, entries[0] + 16, u64::MAX); + rewrite_index_digest(&mut overflow); + assert_error(overflow, RusticolErrorKind::Integrity, "overflows u64"); + + let mut padding = golden.clone(); + padding[70] = 1; + assert_error(padding, RusticolErrorKind::Integrity, "payload padding"); + + let mut trailing_gap = golden.clone(); + let old_index = index_offset(&trailing_gap); + trailing_gap.splice(old_index..old_index, [0_u8; PACBIN_ALIGNMENT as usize]); + let new_index = old_index as u64 + u64::from(PACBIN_ALIGNMENT); + put_u64(&mut trailing_gap, 24, new_index); + let footer = footer_offset(&trailing_gap); + put_u64(&mut trailing_gap, footer + 16, new_index); + rewrite_index_digest(&mut trailing_gap); + assert_error( + trailing_gap, + RusticolErrorKind::Integrity, + "non-canonical trailing gap", + ); +} + +#[test] +fn payload_and_index_authentication_reject_corruption_and_truncation() { + let mut payload = decode_hex(PYTHON_GOLDEN_HEX); + payload[64] ^= 1; + assert_error( + payload, + RusticolErrorKind::Integrity, + "member digest mismatch", + ); + + let mut index = decode_hex(PYTHON_GOLDEN_HEX); + let footer = footer_offset(&index); + index[footer + 63] ^= 1; + assert_error(index, RusticolErrorKind::Integrity, "index digest mismatch"); + + let mut truncated = decode_hex(PYTHON_GOLDEN_HEX); + truncated.truncate(truncated.len() - 17); + let (_, message) = error(truncated); + assert!(message.contains("footer") || message.contains("truncated")); +} + +#[test] +fn normalization_follows_portable_python_posix_rules() { + assert_eq!(normalize_logical_path("a//./b.bin").unwrap(), "a/b.bin"); + for (path, message) in [ + ("", "non-empty"), + (".", "name a member"), + ("/absolute", "relative"), + ("safe/../escape", "must not contain"), + ("windows\\path", "POSIX"), + ("bad\0path", "NUL"), + ] { + let error = normalize_logical_path(path).unwrap_err(); + assert_eq!(error.kind(), RusticolErrorKind::InvalidArgument); + assert!(error.to_string().contains(message)); + } +} + +fn temporary_directory(label: &str) -> std::path::PathBuf { + static NEXT_DIRECTORY: AtomicU64 = AtomicU64::new(0); + let path = std::env::temp_dir().join(format!( + "rusticol-pacbin-{label}-{}-{}", + std::process::id(), + NEXT_DIRECTORY.fetch_add(1, Ordering::Relaxed) + )); + fs::create_dir(&path).unwrap(); + path +} + +#[test] +fn rust_writer_matches_python_golden_byte_for_byte() { + let directory = temporary_directory("python-golden"); + let destination = directory.join("golden.pacbin"); + let members = vec![ + PacbinWriteMember::from_bytes( + "z/exact.bin", + PacbinMemberKind::SymbolicaExactState, + b"exact-state", + ) + .unwrap(), + PacbinWriteMember::from_bytes( + "a//./jit.symjit", + PacbinMemberKind::SymjitApplication, + b"jit-v1", + ) + .unwrap(), + ]; + let index = write_pacbin_atomic(&destination, members, PacbinWriteOptions::default()).unwrap(); + let bytes = fs::read(&destination).unwrap(); + assert_eq!(bytes, decode_hex(PYTHON_GOLDEN_HEX)); + assert_eq!(index.index_offset(), 192); + assert_eq!(index.file_size(), 432); + assert_eq!(index.members().len(), 2); + fs::remove_dir_all(directory).unwrap(); +} + +#[test] +fn writer_is_deterministic_and_supports_runtime_member_kinds() { + let directory = temporary_directory("determinism"); + let first = directory.join("first.pacbin"); + let second = directory.join("second.pacbin"); + for destination in [&first, &second] { + write_pacbin_atomic( + destination, + vec![ + PacbinWriteMember::from_bytes( + "tables/invocations.bin", + PacbinMemberKind::EagerRuntimeTable, + b"table-payload", + ) + .unwrap(), + PacbinWriteMember::from_bytes( + "metadata/runtime.bin", + PacbinMemberKind::EagerRuntimeMetadata, + b"metadata-payload", + ) + .unwrap(), + PacbinWriteMember::from_bytes( + "schedule/recurrence-direct-schedule-v2.bin", + PacbinMemberKind::RecurrenceDirectPlan, + b"recurrence-direct-plan-payload", + ) + .unwrap(), + PacbinWriteMember::from_bytes( + "compiled/color.pacrclr3", + PacbinMemberKind::ColorContraction, + b"compiled-color-contraction-payload", + ) + .unwrap(), + ], + PacbinWriteOptions::new(3, 0o644).unwrap(), + ) + .unwrap(); + } + assert_eq!(fs::read(&first).unwrap(), fs::read(&second).unwrap()); + let reader = PacbinReader::open(&first).unwrap(); + assert_eq!( + reader.member("metadata/runtime.bin").unwrap().kind(), + PacbinMemberKind::EagerRuntimeMetadata + ); + assert_eq!( + reader.member("tables/invocations.bin").unwrap().kind(), + PacbinMemberKind::EagerRuntimeTable + ); + assert_eq!( + reader + .member("schedule/recurrence-direct-schedule-v2.bin") + .unwrap() + .kind(), + PacbinMemberKind::RecurrenceDirectPlan + ); + assert_eq!( + reader.member("compiled/color.pacrclr3").unwrap().kind(), + PacbinMemberKind::ColorContraction + ); + fs::remove_dir_all(directory).unwrap(); +} + +#[test] +fn deleted_recurrence_v1_member_kind_is_not_a_compatibility_alias() { + assert!(PacbinMemberKind::parse(6).is_err()); + assert_eq!( + PacbinMemberKind::parse(7).unwrap(), + PacbinMemberKind::RecurrenceDirectPlan + ); + assert_eq!( + PacbinMemberKind::parse(8).unwrap(), + PacbinMemberKind::RecurrenceColorProjectionCertificate + ); + assert_eq!( + PacbinMemberKind::parse(10).unwrap(), + PacbinMemberKind::ColorContraction + ); +} + +struct TrackingReader { + payload: Vec, + position: usize, + maximum_request: usize, + fail_at: Option, +} + +impl TrackingReader { + fn new(payload: impl Into>) -> Self { + Self { + payload: payload.into(), + position: 0, + maximum_request: 0, + fail_at: None, + } + } + + fn failing(payload: impl Into>, fail_at: usize) -> Self { + Self { + fail_at: Some(fail_at), + ..Self::new(payload) + } + } +} + +impl Read for TrackingReader { + fn read(&mut self, buffer: &mut [u8]) -> io::Result { + self.maximum_request = self.maximum_request.max(buffer.len()); + if self.fail_at.is_some_and(|limit| self.position >= limit) { + return Err(io::Error::other("injected member read failure")); + } + let remaining = self.payload.len().saturating_sub(self.position); + let mut length = remaining.min(buffer.len()); + if let Some(limit) = self.fail_at { + length = length.min(limit.saturating_sub(self.position)); + } + if length == 0 { + return Ok(0); + } + buffer[..length].copy_from_slice(&self.payload[self.position..self.position + length]); + self.position += length; + Ok(length) + } +} + +#[test] +fn writer_streams_reader_and_path_sources_in_bounded_chunks() { + let directory = temporary_directory("bounded-streaming"); + let source_path = directory.join("source.bin"); + fs::write(&source_path, b"path-source").unwrap(); + let destination = directory.join("container.pacbin"); + let mut source = TrackingReader::new(vec![0x5a; 100]); + write_pacbin_atomic( + &destination, + vec![ + PacbinWriteMember::from_reader( + "a/reader.bin", + PacbinMemberKind::EagerRuntimeTable, + &mut source, + ) + .unwrap(), + PacbinWriteMember::from_path( + "b/path.bin", + PacbinMemberKind::EagerRuntimeMetadata, + &source_path, + ) + .unwrap(), + ], + PacbinWriteOptions::new(7, 0o600).unwrap(), + ) + .unwrap(); + assert_eq!(source.maximum_request, 7); + let reader = PacbinReader::open(&destination).unwrap(); + assert_eq!(reader.member_bytes("a/reader.bin").unwrap(), &[0x5a; 100]); + assert_eq!(reader.member_bytes("b/path.bin").unwrap(), b"path-source"); + fs::remove_dir_all(directory).unwrap(); +} + +#[test] +fn atomic_writer_preserves_destination_and_cleans_temporary_file_on_error() { + let directory = temporary_directory("atomic-failure"); + let destination = directory.join("container.pacbin"); + fs::write(&destination, b"previous-container").unwrap(); + let mut source = TrackingReader::failing(vec![0x41; 32], 8); + let error = write_pacbin_atomic( + &destination, + vec![ + PacbinWriteMember::from_reader( + "tables/failing.bin", + PacbinMemberKind::EagerRuntimeTable, + &mut source, + ) + .unwrap(), + ], + PacbinWriteOptions::new(4, 0o644).unwrap(), + ) + .unwrap_err(); + assert_eq!(error.kind(), RusticolErrorKind::Artifact); + assert!(error.to_string().contains("injected member read failure")); + assert_eq!(fs::read(&destination).unwrap(), b"previous-container"); + let names: Vec<_> = fs::read_dir(&directory) + .unwrap() + .map(|entry| entry.unwrap().file_name()) + .collect(); + assert_eq!(names, vec![std::ffi::OsString::from("container.pacbin")]); + fs::remove_dir_all(directory).unwrap(); +} + +#[test] +fn writer_preflight_rejects_invalid_options_and_colliding_paths() { + assert!(PacbinWriteOptions::new(0, 0o644).is_err()); + assert!(PacbinWriteOptions::new(1, 0o1000).is_err()); + let directory = temporary_directory("preflight"); + let destination = directory.join("container.pacbin"); + let duplicate = write_pacbin_atomic( + &destination, + vec![ + PacbinWriteMember::from_bytes("same.bin", PacbinMemberKind::EagerRuntimeMetadata, b"a") + .unwrap(), + PacbinWriteMember::from_bytes("same.bin", PacbinMemberKind::EagerRuntimeTable, b"b") + .unwrap(), + ], + PacbinWriteOptions::default(), + ) + .unwrap_err(); + assert_eq!(duplicate.kind(), RusticolErrorKind::InvalidArgument); + assert!(duplicate.to_string().contains("duplicate")); + let case_collision = write_pacbin_atomic( + &destination, + vec![ + PacbinWriteMember::from_bytes("Case.bin", PacbinMemberKind::EagerRuntimeMetadata, b"a") + .unwrap(), + PacbinWriteMember::from_bytes("case.bin", PacbinMemberKind::EagerRuntimeTable, b"b") + .unwrap(), + ], + PacbinWriteOptions::default(), + ) + .unwrap_err(); + assert_eq!(case_collision.kind(), RusticolErrorKind::InvalidArgument); + assert!(case_collision.to_string().contains("case-colliding")); + assert!(!destination.exists()); + fs::remove_dir_all(directory).unwrap(); +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/recurrence/arena.rs b/pyamplicol/rust/crates/rusticol-core/src/recurrence/arena.rs new file mode 100644 index 0000000000..948d3a9a79 --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/recurrence/arena.rs @@ -0,0 +1,370 @@ +// SPDX-License-Identifier: 0BSD + +//! Deterministic physical component-range assignment for direct recurrence arenas. +//! +//! Semantic current IDs remain stable in the authenticated plan. The direct +//! runtime stores only simultaneously live currents in its split-complex +//! workspace, so non-overlapping liveness intervals may share component +//! planes. This module is independent of model and process semantics. + +use super::RecurrenceProgram; +use crate::direct_arena::{ + DirectArenaInterval as GenericDirectArenaInterval, + assign_direct_arena as assign_generic_direct_arena, +}; +use crate::{RusticolError, RusticolResult}; + +fn invalid(message: impl Into) -> RusticolError { + RusticolError::invalid_argument(message) +} + +/// Inclusive liveness interval and required component-plane width. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct DirectArenaInterval { + pub semantic_current_id: u32, + pub first_use: u64, + pub last_use: u64, + pub component_count: u32, +} + +impl DirectArenaInterval { + pub fn new( + semantic_current_id: u32, + first_use: u64, + last_use: u64, + component_count: u32, + ) -> RusticolResult { + if first_use > last_use { + return Err(invalid(format!( + "direct-arena current {semantic_current_id} starts at {first_use} after its last use {last_use}" + ))); + } + if component_count == 0 { + return Err(invalid(format!( + "direct-arena current {semantic_current_id} has no components" + ))); + } + Ok(Self { + semantic_current_id, + first_use, + last_use, + component_count, + }) + } +} + +/// One semantic current's physical range in the split-complex arena. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct DirectArenaAssignment { + pub semantic_current_id: u32, + pub component_base: u32, + pub component_count: u32, + pub first_use: u64, + pub last_use: u64, +} + +impl DirectArenaAssignment { + pub fn component_stop(self) -> RusticolResult { + self.component_base + .checked_add(self.component_count) + .ok_or_else(|| invalid("direct-arena component range overflows u32")) + } +} + +/// Deterministic interval-coloring result. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct DirectArenaLayout { + assignments: Box<[DirectArenaAssignment]>, + component_count: u32, + total_semantic_components: u64, + reused_semantic_components: u64, +} + +impl DirectArenaLayout { + pub fn assignments(&self) -> &[DirectArenaAssignment] { + &self.assignments + } + + pub const fn component_count(&self) -> u32 { + self.component_count + } + + pub const fn total_semantic_components(&self) -> u64 { + self.total_semantic_components + } + + pub const fn reused_semantic_components(&self) -> u64 { + self.reused_semantic_components + } + + pub fn assignment(&self, semantic_current_id: u32) -> Option { + self.assignments + .get(semantic_current_id as usize) + .copied() + .filter(|assignment| assignment.semantic_current_id == semantic_current_id) + } + + pub fn validate(&self) -> RusticolResult<()> { + if self.assignments.is_empty() { + if self.component_count != 0 + || self.total_semantic_components != 0 + || self.reused_semantic_components != 0 + { + return Err(invalid("empty direct-arena layout has nonempty accounting")); + } + return Ok(()); + } + + let mut semantic_components = 0u64; + for (index, assignment) in self.assignments.iter().copied().enumerate() { + if assignment.semantic_current_id != index as u32 { + return Err(invalid(format!( + "direct-arena assignment row {index} has semantic current ID {}", + assignment.semantic_current_id + ))); + } + if assignment.component_count == 0 || assignment.first_use > assignment.last_use { + return Err(invalid(format!( + "direct-arena assignment {index} has invalid width or liveness" + ))); + } + if assignment.component_stop()? > self.component_count { + return Err(invalid(format!( + "direct-arena assignment {index} exceeds the physical arena" + ))); + } + semantic_components = semantic_components + .checked_add(u64::from(assignment.component_count)) + .ok_or_else(|| invalid("direct-arena semantic component count overflows u64"))?; + } + if semantic_components != self.total_semantic_components { + return Err(invalid(format!( + "direct-arena semantic component total {} does not match recorded {}", + semantic_components, self.total_semantic_components + ))); + } + if self + .total_semantic_components + .checked_sub(u64::from(self.component_count)) + != Some(self.reused_semantic_components) + { + return Err(invalid( + "direct-arena reused component accounting is inconsistent", + )); + } + + for (left_index, left) in self.assignments.iter().copied().enumerate() { + for right in self.assignments[left_index + 1..].iter().copied() { + let lifetimes_overlap = + left.first_use <= right.last_use && right.first_use <= left.last_use; + if !lifetimes_overlap { + continue; + } + let ranges_overlap = left.component_base < right.component_stop()? + && right.component_base < left.component_stop()?; + if ranges_overlap { + return Err(invalid(format!( + "live direct-arena currents {} and {} overlap physical components", + left.semantic_current_id, right.semantic_current_id + ))); + } + } + } + Ok(()) + } +} + +/// Assign variable-width current intervals to reusable physical component ranges. +/// +/// Inputs must use dense semantic current IDs. The allocator processes +/// intervals by `(first_use, semantic_current_id)`, releases ranges whose +/// inclusive lifetime ended before the next current starts, and chooses the +/// smallest fitting free range with the lowest base as a deterministic tie +/// breaker. +pub fn assign_direct_arena(intervals: &[DirectArenaInterval]) -> RusticolResult { + let generic_intervals = intervals + .iter() + .map(|interval| { + GenericDirectArenaInterval::new( + interval.semantic_current_id, + interval.first_use, + interval.last_use, + interval.component_count, + ) + }) + .collect::>>()?; + let generic_layout = assign_generic_direct_arena(&generic_intervals)?; + let assignments = generic_layout + .assignments() + .iter() + .map(|assignment| DirectArenaAssignment { + semantic_current_id: assignment.semantic_value_id, + component_base: assignment.component_base, + component_count: assignment.component_count, + first_use: assignment.first_use, + last_use: assignment.last_use, + }) + .collect::>(); + Ok(DirectArenaLayout { + assignments: assignments.into_boxed_slice(), + component_count: generic_layout.component_count(), + total_semantic_components: generic_layout.total_semantic_components(), + reused_semantic_components: generic_layout.reused_semantic_components(), + }) +} + +/// Derive conservative stage-granular liveness for a semantic recurrence program. +/// +/// Direct contribution packets may interleave destinations within one stage, +/// so every current produced in that stage is considered simultaneously live. +/// A current remains live through the latest child stage that reads it or +/// through the final closure event. This permits reuse between dependency +/// stages without relying on packet ordering. +pub fn recurrence_direct_arena_layout( + program: &RecurrenceProgram, + component_counts: &[u32], +) -> RusticolResult { + if component_counts.len() != program.currents().len() { + return Err(invalid(format!( + "direct-arena component-count catalog has {} rows for {} currents", + component_counts.len(), + program.currents().len() + ))); + } + + let current_stages = program + .currents() + .iter() + .map(|current| { + u64::try_from(current.key().support_source_slots().len()) + .map_err(|_| invalid("direct-arena current stage exceeds u64")) + }) + .collect::>>()?; + let maximum_stage = current_stages.iter().copied().max().unwrap_or(0); + let closure_event = maximum_stage + .checked_add(1) + .ok_or_else(|| invalid("direct-arena closure event overflows u64"))?; + let mut last_uses = current_stages.clone(); + + for contribution in program.contributions() { + let result_stage = *current_stages + .get(contribution.result_current_id() as usize) + .ok_or_else(|| invalid("direct-arena contribution result current is absent"))?; + for &parent_id in contribution.parent_current_ids() { + let last_use = last_uses + .get_mut(parent_id as usize) + .ok_or_else(|| invalid("direct-arena contribution parent current is absent"))?; + *last_use = (*last_use).max(result_stage); + } + } + for closure in program.closure_terms() { + for &parent_id in closure.parent_current_ids() { + let last_use = last_uses + .get_mut(parent_id as usize) + .ok_or_else(|| invalid("direct-arena closure parent current is absent"))?; + *last_use = closure_event; + } + } + + let intervals = program + .currents() + .iter() + .zip(component_counts.iter().copied()) + .enumerate() + .map(|(index, (current, component_count))| { + if current.id() as usize != index { + return Err(invalid( + "direct-arena lowering requires dense semantic current IDs", + )); + } + DirectArenaInterval::new( + current.id(), + current_stages[index], + last_uses[index], + component_count, + ) + }) + .collect::>>()?; + assign_direct_arena(&intervals) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn interval(id: u32, first: u64, last: u64, count: u32) -> DirectArenaInterval { + DirectArenaInterval::new(id, first, last, count).unwrap() + } + + #[test] + fn non_overlapping_currents_reuse_the_same_range() { + let layout = assign_direct_arena(&[ + interval(0, 0, 2, 4), + interval(1, 3, 5, 4), + interval(2, 6, 8, 4), + ]) + .unwrap(); + assert_eq!(layout.component_count(), 4); + assert_eq!(layout.total_semantic_components(), 12); + assert_eq!(layout.reused_semantic_components(), 8); + assert_eq!(layout.assignment(0).unwrap().component_base, 0); + assert_eq!(layout.assignment(1).unwrap().component_base, 0); + assert_eq!(layout.assignment(2).unwrap().component_base, 0); + } + + #[test] + fn inclusive_lifetimes_do_not_reuse_at_the_same_event() { + let layout = assign_direct_arena(&[interval(0, 0, 3, 2), interval(1, 3, 4, 3)]).unwrap(); + assert_eq!(layout.component_count(), 5); + assert_eq!(layout.assignment(0).unwrap().component_base, 0); + assert_eq!(layout.assignment(1).unwrap().component_base, 2); + } + + #[test] + fn allocator_chooses_the_smallest_fitting_range_deterministically() { + let layout = assign_direct_arena(&[ + interval(0, 0, 1, 8), + interval(1, 0, 5, 2), + interval(2, 0, 1, 3), + interval(3, 2, 4, 2), + interval(4, 2, 4, 7), + ]) + .unwrap(); + assert_eq!(layout.assignment(3).unwrap().component_base, 10); + assert_eq!(layout.assignment(4).unwrap().component_base, 0); + assert_eq!(layout.component_count(), 13); + } + + #[test] + fn allocator_rejects_sparse_or_duplicate_semantic_ids() { + assert!(assign_direct_arena(&[interval(1, 0, 1, 1)]).is_err()); + assert!(assign_direct_arena(&[interval(0, 0, 1, 1), interval(0, 2, 3, 1)]).is_err()); + } + + #[test] + fn validation_rejects_overlapping_live_ranges() { + let layout = DirectArenaLayout { + assignments: vec![ + DirectArenaAssignment { + semantic_current_id: 0, + component_base: 0, + component_count: 2, + first_use: 0, + last_use: 2, + }, + DirectArenaAssignment { + semantic_current_id: 1, + component_base: 1, + component_count: 2, + first_use: 1, + last_use: 3, + }, + ] + .into_boxed_slice(), + component_count: 3, + total_semantic_components: 4, + reused_semantic_components: 1, + }; + assert!(layout.validate().is_err()); + } +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/recurrence/builder.rs b/pyamplicol/rust/crates/rusticol-core/src/recurrence/builder.rs new file mode 100644 index 0000000000..9aa5092662 --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/recurrence/builder.rs @@ -0,0 +1,211 @@ +// SPDX-License-Identifier: 0BSD + +//! Composite authentication and compact recurrence schedule construction. + +use super::process::{ProcessSemanticTemplateReference, ValidatedRecurrenceProcessInput}; +use super::template::{RecurrenceSemanticTemplateKind, ValidatedRecurrenceTemplateInput}; +use crate::{RusticolError, RusticolResult}; + +fn invalid(message: impl Into) -> RusticolError { + RusticolError::invalid_argument(message) +} + +/// Process and prepared-model inputs authenticated as one semantic unit. +#[derive(Clone, Debug)] +pub struct AuthenticatedRecurrenceBuilderInput { + process: ValidatedRecurrenceProcessInput, + template: ValidatedRecurrenceTemplateInput, +} + +impl AuthenticatedRecurrenceBuilderInput { + pub fn new( + process: ValidatedRecurrenceProcessInput, + template: ValidatedRecurrenceTemplateInput, + ) -> RusticolResult { + let process_identity = process.semantic_identity(); + let template_summary = template.summary(); + if process_identity.model_catalog_digest() != template_summary.compiled_model_digest { + return Err(invalid(format!( + "recurrence process model-catalog digest {} does not match prepared model {}", + process_identity.model_catalog_digest(), + template_summary.compiled_model_digest, + ))); + } + if process_identity.prepared_catalog_digest() != template_summary.catalog_digest { + return Err(invalid(format!( + "recurrence process prepared-catalog digest {} does not match template catalog {}", + process_identity.prepared_catalog_digest(), + template_summary.catalog_digest, + ))); + } + + let template_index = template.semantic_index()?; + for reference in process.template_references() { + authenticate_template_reference(reference, &template_index)?; + } + authenticate_singlet_closure_anchors(&process, &template)?; + + Ok(Self { process, template }) + } + + pub const fn process(&self) -> &ValidatedRecurrenceProcessInput { + &self.process + } + + pub const fn template(&self) -> &ValidatedRecurrenceTemplateInput { + &self.template + } + + pub fn into_parts( + self, + ) -> ( + ValidatedRecurrenceProcessInput, + ValidatedRecurrenceTemplateInput, + ) { + (self.process, self.template) + } + + pub fn build(&self) -> RusticolResult { + super::construct::build_recurrence_program(self) + } + + pub fn build_with_progress( + &self, + progress: &mut dyn FnMut(super::RecurrenceBuildProgress) -> RusticolResult<()>, + ) -> RusticolResult { + super::construct::build_recurrence_program_with_progress(self, progress) + } + + /// Build while returning construction-only generation diagnostics. + #[doc(hidden)] + pub fn build_with_progress_and_telemetry( + &self, + progress: &mut dyn FnMut(super::RecurrenceBuildProgress) -> RusticolResult<()>, + ) -> RusticolResult<( + super::RecurrenceProgram, + super::RecurrenceGenerationTelemetry, + )> { + super::construct::build_recurrence_program_with_progress_and_telemetry(self, progress) + } +} + +fn authenticate_singlet_closure_anchors( + process: &ValidatedRecurrenceProcessInput, + template: &ValidatedRecurrenceTemplateInput, +) -> RusticolResult<()> { + let process_input = process.input(); + let template_input = template.input(); + let mut fermionic_source_slots = Vec::new(); + + for leg in &process_input.external_legs { + let declared_fermionic = match leg.is_fermionic { + 0 => false, + 1 => true, + value => { + return Err(invalid(format!( + "source slot {} has invalid fermionic marker {value}", + leg.source_slot + ))); + } + }; + let state_range = leg.source_state_range.as_usize_range( + process_input.source_states.len(), + "external source states during closure-anchor authentication", + )?; + let mut statistics = None; + for source_state in &process_input.source_states[state_range] { + let state = template_input + .current_states + .get(source_state.current_state_template_id as usize) + .ok_or_else(|| { + invalid(format!( + "source slot {} references absent current-state template {}", + leg.source_slot, source_state.current_state_template_id + )) + })?; + match statistics { + None => statistics = Some(state.statistics), + Some(previous) if previous == state.statistics => {} + Some(previous) => { + return Err(invalid(format!( + "source slot {} mixes particle statistics {previous} and {}", + leg.source_slot, state.statistics + ))); + } + } + } + let template_fermionic = statistics == Some(1); + if template_fermionic != declared_fermionic { + return Err(invalid(format!( + "source slot {} fermionic marker does not match its authenticated current-state templates", + leg.source_slot + ))); + } + if declared_fermionic { + fermionic_source_slots.push(leg.source_slot); + } + } + + let expected_singlet_anchor = fermionic_source_slots.first().copied().unwrap_or(0); + for sector in &process_input.physical_lc_sectors { + let word = process_u32_sequence( + process_input, + sector.word_sequence_id, + "physical LC color word during closure-anchor authentication", + )?; + if word.is_empty() && sector.closure_source_slot != expected_singlet_anchor { + return Err(invalid(format!( + "all-singlet LC sector {} uses closure source slot {}, expected first fermionic source slot (or first source) {expected_singlet_anchor}", + sector.sector_id, sector.closure_source_slot + ))); + } + } + Ok(()) +} + +fn process_u32_sequence<'a>( + input: &'a super::process::OwnedRecurrenceProcessInput, + sequence_id: u32, + label: &str, +) -> RusticolResult<&'a [u32]> { + let range = input + .u32_sequence_ranges + .get(sequence_id as usize) + .copied() + .ok_or_else(|| invalid(format!("{label} references absent sequence {sequence_id}")))?; + let range = range.as_usize_range(input.u32_sequence_values.len(), label)?; + Ok(&input.u32_sequence_values[range]) +} + +fn authenticate_template_reference( + reference: &ProcessSemanticTemplateReference, + template_index: &super::template::RecurrenceTemplateSemanticIndex, +) -> RusticolResult<()> { + let kind = RecurrenceSemanticTemplateKind::parse(reference.typed_id.kind.as_str())?; + let Some(identity) = template_index.record(kind, reference.typed_id.template_id) else { + return Err(invalid(format!( + "recurrence process references absent prepared {} template {}", + kind.as_str(), + reference.typed_id.template_id, + ))); + }; + if identity.semantic_digest != reference.semantic_digest { + return Err(invalid(format!( + "recurrence process {} template {} has semantic digest {}, expected {}", + kind.as_str(), + reference.typed_id.template_id, + reference.semantic_digest, + identity.semantic_digest, + ))); + } + if identity.prepared_kernel_id != reference.prepared_kernel_id { + return Err(invalid(format!( + "recurrence process {} template {} has prepared kernel {:?}, expected {:?}", + kind.as_str(), + reference.typed_id.template_id, + reference.prepared_kernel_id, + identity.prepared_kernel_id, + ))); + } + Ok(()) +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/recurrence/color.rs b/pyamplicol/rust/crates/rusticol-core/src/recurrence/color.rs new file mode 100644 index 0000000000..4385ee64a0 --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/recurrence/color.rs @@ -0,0 +1,963 @@ +// SPDX-License-Identifier: 0BSD + +use std::collections::{BTreeSet, HashMap}; + +use crate::{RusticolError, RusticolResult}; + +use super::{DynamicLCColorStateId, ExactComplexRational, SemanticDigest}; + +fn invalid(message: impl Into) -> RusticolError { + RusticolError::invalid_argument(message) +} + +/// One ordered component in a dynamic partial LC color forest. +#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +#[repr(u8)] +pub enum LCColorComponentKind { + OpenString = 0, + AdjointSegment = 1, + Trace = 2, +} + +impl TryFrom for LCColorComponentKind { + type Error = RusticolError; + + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(Self::OpenString), + 1 => Ok(Self::AdjointSegment), + 2 => Ok(Self::Trace), + _ => Err(invalid(format!( + "unsupported LC color component kind {value}" + ))), + } + } +} + +#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +#[repr(u8)] +pub enum LCColorComponentRole { + Active = 0, + Passive = 1, + None = 2, +} + +impl TryFrom for LCColorComponentRole { + type Error = RusticolError; + + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(Self::Active), + 1 => Ok(Self::Passive), + 2 => Ok(Self::None), + _ => Err(invalid(format!( + "unsupported LC color component role {value}" + ))), + } + } +} + +#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +#[repr(u8)] +pub enum LCColorSourceSeedOperation { + Empty = 0, + Singleton = 1, +} + +impl TryFrom for LCColorSourceSeedOperation { + type Error = RusticolError; + + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(Self::Empty), + 1 => Ok(Self::Singleton), + _ => Err(invalid(format!( + "unsupported LC source seed operation {value}" + ))), + } + } +} + +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub struct LCColorSourceSeed { + operation: LCColorSourceSeedOperation, + output_color_shape_id: u32, + component_kind: Option, + component_role: LCColorComponentRole, + proof_digest: SemanticDigest, +} + +impl LCColorSourceSeed { + pub fn new( + operation: LCColorSourceSeedOperation, + output_color_shape_id: u32, + component_kind: Option, + component_role: LCColorComponentRole, + proof_digest: SemanticDigest, + ) -> RusticolResult { + match operation { + LCColorSourceSeedOperation::Empty + if component_kind.is_some() || component_role != LCColorComponentRole::None => + { + return Err(invalid( + "an empty LC source seed cannot declare a component", + )); + } + LCColorSourceSeedOperation::Singleton + if component_kind.is_none() || component_role == LCColorComponentRole::None => + { + return Err(invalid( + "a singleton LC source seed requires component kind and role", + )); + } + _ => {} + } + Ok(Self { + operation, + output_color_shape_id, + component_kind, + component_role, + proof_digest, + }) + } + + pub fn instantiate( + self, + source_slot: u32, + color_representation: i32, + ) -> RusticolResult { + match self.operation { + LCColorSourceSeedOperation::Empty => { + if color_representation != 1 { + return Err(invalid( + "an empty LC source seed requires the singlet representation", + )); + } + DynamicLCColorState::new(self.output_color_shape_id, None, Vec::new()) + } + LCColorSourceSeedOperation::Singleton => { + let component = LCColorComponent::new( + self.component_kind + .expect("validated singleton component kind"), + vec![source_slot], + )?; + let result_port_bindings = match color_representation { + 3 => vec![LCColorPortBinding::new(0, LCColorEndpoint::Back)], + -3 => vec![LCColorPortBinding::new(0, LCColorEndpoint::Front)], + 8 => vec![ + LCColorPortBinding::new(0, LCColorEndpoint::Back), + LCColorPortBinding::new(0, LCColorEndpoint::Front), + ], + value => { + return Err(invalid(format!( + "a singleton LC source seed has unsupported representation {value}", + ))); + } + }; + if self.component_role != LCColorComponentRole::Active { + return Err(invalid( + "a colored LC source seed must expose active result ports", + )); + } + DynamicLCColorState::new_port_wired( + self.output_color_shape_id, + result_port_bindings, + vec![component], + ) + } + } + } + + pub const fn proof_digest(self) -> SemanticDigest { + self.proof_digest + } +} + +/// Canonical ordered colored-source word for one LC component. +#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub struct LCColorComponent { + kind: LCColorComponentKind, + source_slots: Box<[u32]>, +} + +/// One endpoint of an ordered LC strand. +#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +#[repr(u8)] +pub enum LCColorEndpoint { + Front = 0, + Back = 1, +} + +impl LCColorEndpoint { + const fn reversed(self) -> Self { + match self { + Self::Front => Self::Back, + Self::Back => Self::Front, + } + } + + fn source_slot(self, component: &LCColorComponent) -> RusticolResult { + match self { + Self::Front => component.source_slots().first().copied(), + Self::Back => component.source_slots().last().copied(), + } + .ok_or_else(|| invalid("an LC color result port references an empty component")) + } +} + +/// Binding from one ordered current-result color port to a strand endpoint. +#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub struct LCColorPortBinding { + component_index: u32, + endpoint: LCColorEndpoint, +} + +impl LCColorPortBinding { + pub const fn new(component_index: u32, endpoint: LCColorEndpoint) -> Self { + Self { + component_index, + endpoint, + } + } + + pub const fn component_index(self) -> u32 { + self.component_index + } + + pub const fn endpoint(self) -> LCColorEndpoint { + self.endpoint + } +} + +/// Reference to one result port of one of the two transition parents. +#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub struct LCColorParentPort { + parent_index: u8, + port_index: u8, +} + +impl LCColorParentPort { + pub fn new(parent_index: u8, port_index: u8) -> RusticolResult { + if parent_index > 1 { + return Err(invalid( + "LC color parent-port parent index must be zero or one", + )); + } + Ok(Self { + parent_index, + port_index, + }) + } + + const fn parent_index(self) -> usize { + self.parent_index as usize + } + + const fn port_index(self) -> usize { + self.port_index as usize + } +} + +/// Exact port wiring certified for one local LC tensor term. +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +pub struct LCColorPortWiring { + component_parent_order: [u8; 2], + input_pairings: Box<[[LCColorParentPort; 2]]>, + result_port_bindings: Box<[LCColorParentPort]>, +} + +impl LCColorPortWiring { + pub fn new( + component_parent_order: [u8; 2], + input_pairings: Vec<[LCColorParentPort; 2]>, + result_port_bindings: Vec, + ) -> RusticolResult { + if component_parent_order != [0, 1] && component_parent_order != [1, 0] { + return Err(invalid( + "LC color component parent order must be [0, 1] or [1, 0]", + )); + } + let mut consumed = BTreeSet::new(); + for pairing in &input_pairings { + if pairing[0] == pairing[1] { + return Err(invalid("an LC color port cannot be paired with itself")); + } + for port in pairing { + if !consumed.insert(*port) { + return Err(invalid( + "an LC color parent port is consumed by more than one pairing", + )); + } + } + } + for port in &result_port_bindings { + if !consumed.insert(*port) { + return Err(invalid( + "an LC color parent port is both paired and exposed as a result port", + )); + } + } + Ok(Self { + component_parent_order, + input_pairings: input_pairings.into_boxed_slice(), + result_port_bindings: result_port_bindings.into_boxed_slice(), + }) + } + + pub fn apply( + &self, + left: &DynamicLCColorState, + right: &DynamicLCColorState, + result_color_shape_id: u32, + ) -> RusticolResult { + let parents = [left, right]; + let mut components = Vec::>::new(); + let mut parent_ports = [Vec::new(), Vec::new()]; + for parent_index in self.component_parent_order { + let parent = parents[parent_index as usize]; + let offset = components.len() as u32; + components.extend(parent.components().iter().cloned().map(Some)); + parent_ports[parent_index as usize] = parent + .result_port_bindings() + .iter() + .map(|binding| { + Some(LCColorPortBinding::new( + binding.component_index() + offset, + binding.endpoint(), + )) + }) + .collect(); + } + + let expected_parent_ports = parent_ports[0].len() + parent_ports[1].len(); + if expected_parent_ports != self.input_pairings.len() * 2 + self.result_port_bindings.len() + { + return Err(invalid( + "LC color wiring does not consume every parent result port exactly once", + )); + } + for pairing in self.input_pairings.iter().copied() { + let left_binding = take_parent_port(pairing[0], &mut parent_ports)?; + let right_binding = take_parent_port(pairing[1], &mut parent_ports)?; + join_color_endpoints( + &mut components, + &mut parent_ports, + left_binding, + right_binding, + )?; + } + let mut result_ports = Vec::with_capacity(self.result_port_bindings.len()); + for reference in self.result_port_bindings.iter().copied() { + result_ports.push(take_parent_port(reference, &mut parent_ports)?); + } + if parent_ports.iter().flatten().any(Option::is_some) { + return Err(invalid( + "LC color wiring leaves an unconsumed parent result port", + )); + } + + let mut remap = vec![u32::MAX; components.len()]; + let mut compact = Vec::new(); + for (old_index, component) in components.into_iter().enumerate() { + if let Some(component) = component { + remap[old_index] = compact.len() as u32; + compact.push(component); + } + } + for binding in &mut result_ports { + binding.component_index = *remap + .get(binding.component_index as usize) + .ok_or_else(|| invalid("LC color result port references an absent component"))?; + if binding.component_index == u32::MAX { + return Err(invalid( + "LC color result port references a consumed component", + )); + } + } + DynamicLCColorState::new_port_wired(result_color_shape_id, result_ports, compact) + } +} + +fn take_parent_port( + reference: LCColorParentPort, + parent_ports: &mut [Vec>; 2], +) -> RusticolResult { + parent_ports + .get_mut(reference.parent_index()) + .and_then(|ports| ports.get_mut(reference.port_index())) + .ok_or_else(|| invalid("LC color wiring references an absent parent result port"))? + .take() + .ok_or_else(|| invalid("LC color wiring consumes one parent result port twice")) +} + +fn join_color_endpoints( + components: &mut [Option], + parent_ports: &mut [Vec>; 2], + left_binding: LCColorPortBinding, + right_binding: LCColorPortBinding, +) -> RusticolResult<()> { + let left_index = left_binding.component_index() as usize; + let right_index = right_binding.component_index() as usize; + if left_index == right_index { + if left_binding.endpoint() == right_binding.endpoint() { + return Err(invalid( + "LC color wiring cannot join the same endpoint of one strand", + )); + } + if parent_ports.iter().flatten().any(|binding| { + binding + .as_ref() + .is_some_and(|binding| binding.component_index() as usize == left_index) + }) { + return Err(invalid( + "LC color wiring closes a strand that still exposes a result port", + )); + } + let component = components + .get_mut(left_index) + .and_then(Option::take) + .ok_or_else(|| invalid("LC color wiring closes an absent strand"))?; + components[left_index] = Some(LCColorComponent::new( + LCColorComponentKind::Trace, + component.source_slots().to_vec(), + )?); + return Ok(()); + } + + let mut left = components + .get_mut(left_index) + .and_then(Option::take) + .ok_or_else(|| invalid("LC color wiring joins an absent left strand"))?; + let mut right = components + .get_mut(right_index) + .and_then(Option::take) + .ok_or_else(|| invalid("LC color wiring joins an absent right strand"))?; + let reverse_left = left_binding.endpoint() == LCColorEndpoint::Front; + let reverse_right = right_binding.endpoint() == LCColorEndpoint::Back; + if reverse_left { + left = left.reversed()?; + } + if reverse_right { + right = right.reversed()?; + } + let result_kind = if left.kind() == LCColorComponentKind::OpenString + || right.kind() == LCColorComponentKind::OpenString + { + LCColorComponentKind::OpenString + } else { + LCColorComponentKind::AdjointSegment + }; + let word = left + .source_slots() + .iter() + .chain(right.source_slots()) + .copied() + .collect(); + let retained_index = left_index.min(right_index); + components[retained_index] = Some(LCColorComponent::new(result_kind, word)?); + + for binding in parent_ports.iter_mut().flatten().flatten() { + let component_index = binding.component_index() as usize; + if component_index == left_index { + binding.component_index = retained_index as u32; + if reverse_left { + binding.endpoint = binding.endpoint.reversed(); + } + } else if component_index == right_index { + binding.component_index = retained_index as u32; + if reverse_right { + binding.endpoint = binding.endpoint.reversed(); + } + } + } + Ok(()) +} + +impl LCColorComponent { + pub fn new(kind: LCColorComponentKind, source_slots: Vec) -> RusticolResult { + if source_slots.is_empty() { + return Err(invalid( + "an LC color component cannot have an empty source word", + )); + } + if source_slots.len() > u32::MAX as usize { + return Err(invalid("LC color component exceeds the u32 ABI domain")); + } + let mut seen = BTreeSet::new(); + if source_slots.iter().any(|slot| !seen.insert(*slot)) { + return Err(invalid( + "an LC color component cannot contain a source slot more than once", + )); + } + let source_slots = if kind == LCColorComponentKind::Trace { + canonical_trace_rotation(source_slots) + } else { + source_slots + }; + Ok(Self { + kind, + source_slots: source_slots.into_boxed_slice(), + }) + } + + pub const fn kind(&self) -> LCColorComponentKind { + self.kind + } + + pub fn source_slots(&self) -> &[u32] { + &self.source_slots + } + + fn reversed(&self) -> RusticolResult { + Self::new(self.kind, self.source_slots.iter().rev().copied().collect()) + } +} + +/// Internable dynamic LC state, independent of physical sector identity. +#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub struct DynamicLCColorState { + output_color_shape_id: u32, + active_component_index: Option, + result_port_bindings: Box<[LCColorPortBinding]>, + components: Box<[LCColorComponent]>, +} + +impl DynamicLCColorState { + pub fn new( + output_color_shape_id: u32, + active_component_index: Option, + components: Vec, + ) -> RusticolResult { + let result_port_bindings = active_component_index + .map(|index| LCColorPortBinding::new(index, LCColorEndpoint::Back)) + .into_iter() + .collect(); + Self::new_port_wired(output_color_shape_id, result_port_bindings, components) + } + + pub const fn output_color_shape_id(&self) -> u32 { + self.output_color_shape_id + } + + pub fn components(&self) -> &[LCColorComponent] { + &self.components + } + + pub const fn active_component_index(&self) -> Option { + self.active_component_index + } + + pub fn active_component(&self) -> Option<&LCColorComponent> { + self.active_component_index + .map(|index| &self.components[index as usize]) + } + + pub(crate) fn reversed(&self) -> RusticolResult { + let components = self + .components + .iter() + .rev() + .map(LCColorComponent::reversed) + .collect::>>()?; + let result_port_bindings = self + .result_port_bindings + .iter() + .rev() + .map(|binding| { + LCColorPortBinding::new( + self.components.len() as u32 - 1 - binding.component_index, + binding.endpoint.reversed(), + ) + }) + .collect(); + Self::new_port_wired(self.output_color_shape_id, result_port_bindings, components) + } + + pub(crate) fn pure_adjoint_word(&self) -> Option<&[u32]> { + if self.components.len() != 1 + || self.active_component_index != Some(0) + || self.result_port_bindings.len() != 2 + { + return None; + } + let exposes_front = self.result_port_bindings.iter().any(|binding| { + binding.component_index() == 0 && binding.endpoint() == LCColorEndpoint::Front + }); + let exposes_back = self.result_port_bindings.iter().any(|binding| { + binding.component_index() == 0 && binding.endpoint() == LCColorEndpoint::Back + }); + if !exposes_front || !exposes_back { + return None; + } + let component = &self.components[0]; + (component.kind() == LCColorComponentKind::AdjointSegment) + .then_some(component.source_slots()) + } + + pub fn new_port_wired( + output_color_shape_id: u32, + result_port_bindings: Vec, + components: Vec, + ) -> RusticolResult { + if components.len() > u32::MAX as usize { + return Err(invalid("LC color forest exceeds the u32 ABI domain")); + } + let mut seen_slots = BTreeSet::new(); + for component in &components { + if component + .source_slots() + .iter() + .any(|slot| !seen_slots.insert(*slot)) + { + return Err(invalid( + "a dynamic LC color state cannot duplicate colored source slots", + )); + } + } + let mut seen_ports = BTreeSet::new(); + for binding in &result_port_bindings { + let component = components + .get(binding.component_index as usize) + .ok_or_else(|| invalid("LC color result port is outside the color forest"))?; + if component.kind() == LCColorComponentKind::Trace { + return Err(invalid("a closed LC trace cannot expose a result port")); + } + if !seen_ports.insert(*binding) { + return Err(invalid( + "two LC color result ports cannot bind the same strand endpoint", + )); + } + } + let active_component_index = result_port_bindings + .first() + .map(|binding| binding.component_index()) + .filter(|index| { + result_port_bindings + .iter() + .all(|binding| binding.component_index() == *index) + }); + Ok(Self { + output_color_shape_id, + active_component_index, + result_port_bindings: result_port_bindings.into_boxed_slice(), + components: components.into_boxed_slice(), + }) + } + + pub fn result_port_bindings(&self) -> &[LCColorPortBinding] { + &self.result_port_bindings + } + + /// Source lineage exposed by this current's ordered result color ports. + /// + /// Closed neutral currents deliberately return an empty lineage. Open + /// fermion and adjoint currents retain only the source endpoints that can + /// flow into a later recurrence, rather than the history of already closed + /// quark lines used to construct them. + pub fn result_port_lineage_source_slots(&self) -> RusticolResult> { + self.result_port_bindings + .iter() + .map(|binding| { + let component = self + .components + .get(binding.component_index() as usize) + .ok_or_else(|| invalid("LC color result-port component disappeared"))?; + binding.endpoint().source_slot(component) + }) + .collect() + } +} + +/// Owns the only mapping from semantic LC color states to recurrence IDs. +#[derive(Clone, Debug, Default)] +pub struct DynamicLCColorStateInterner { + states: Vec, + ids: HashMap, +} + +impl DynamicLCColorStateInterner { + pub fn intern(&mut self, state: DynamicLCColorState) -> RusticolResult { + if let Some(id) = self.ids.get(&state) { + return Ok(*id); + } + let raw_id = u32::try_from(self.states.len()) + .map_err(|_| invalid("dynamic LC color-state interner exceeds u32"))?; + let id = DynamicLCColorStateId::from_interner(raw_id); + self.states.push(state.clone()); + self.ids.insert(state, id); + Ok(id) + } + + pub fn get(&self, id: DynamicLCColorStateId) -> Option<&DynamicLCColorState> { + self.states.get(id.get() as usize) + } + + pub fn len(&self) -> usize { + self.states.len() + } + + pub fn is_empty(&self) -> bool { + self.states.is_empty() + } + + pub fn into_states(self) -> Vec { + self.states + } +} + +/// Exact operation performed by one compiler-certified LC transition term. +#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +#[repr(u8)] +pub enum LCColorComponentOperation { + ConcatenateJoin = 0, + ConcatenateKeep = 1, + InheritLeft = 2, + InheritRight = 3, + Empty = 4, + Close = 5, +} + +impl TryFrom for LCColorComponentOperation { + type Error = RusticolError; + + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(Self::ConcatenateJoin), + 1 => Ok(Self::ConcatenateKeep), + 2 => Ok(Self::InheritLeft), + 3 => Ok(Self::InheritRight), + 4 => Ok(Self::Empty), + 5 => Ok(Self::Close), + _ => Err(invalid(format!( + "unsupported LC color component operation {value}" + ))), + } + } +} + +/// Executable color-state witness emitted by the model compiler. +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +pub struct LCColorTransitionWitness { + input_permutation: [u8; 2], + reverse_parent_mask: u8, + operation: LCColorComponentOperation, + result_component_kind: Option, + result_component_role: LCColorComponentRole, + result_color_shape_id: Option, + port_wiring: LCColorPortWiring, + exact_factor: ExactComplexRational, + proof_digest: SemanticDigest, +} + +impl LCColorTransitionWitness { + #[allow(clippy::too_many_arguments)] + pub fn new( + input_permutation: [u8; 2], + reverse_parent_mask: u8, + operation: LCColorComponentOperation, + result_component_kind: Option, + result_component_role: LCColorComponentRole, + result_color_shape_id: Option, + port_wiring: LCColorPortWiring, + exact_factor: ExactComplexRational, + proof_digest: SemanticDigest, + ) -> RusticolResult { + if input_permutation != [0, 1] && input_permutation != [1, 0] { + return Err(invalid( + "LC color witness input permutation must be [0, 1] or [1, 0]", + )); + } + if reverse_parent_mask > 0b11 { + return Err(invalid( + "LC color witness has an invalid parent-reversal mask", + )); + } + if exact_factor.is_zero() { + return Err(invalid( + "an executable LC color witness cannot have zero factor", + )); + } + match operation { + LCColorComponentOperation::Close => { + if result_component_role != LCColorComponentRole::None + || result_color_shape_id.is_some() + { + return Err(invalid( + "an LC closure witness cannot declare an active result color state", + )); + } + } + LCColorComponentOperation::ConcatenateJoin => { + if result_component_kind.is_none() + || result_component_role == LCColorComponentRole::None + || result_color_shape_id.is_none() + { + return Err(invalid( + "an LC join witness requires result component, role, and shape contracts", + )); + } + } + LCColorComponentOperation::InheritLeft | LCColorComponentOperation::InheritRight => { + if result_component_kind.is_some() + || result_component_role != LCColorComponentRole::Active + || result_color_shape_id.is_none() + { + return Err(invalid( + "an LC inheritance witness requires an active result role and shape", + )); + } + } + _ => { + if result_component_kind.is_some() + || result_component_role != LCColorComponentRole::None + || result_color_shape_id.is_none() + { + return Err(invalid( + "a passive LC witness requires only a result shape contract", + )); + } + } + } + Ok(Self { + input_permutation, + reverse_parent_mask, + operation, + result_component_kind, + result_component_role, + result_color_shape_id, + port_wiring, + exact_factor, + proof_digest, + }) + } + + pub const fn exact_factor(&self) -> ExactComplexRational { + self.exact_factor + } + + pub const fn proof_digest(&self) -> SemanticDigest { + self.proof_digest + } + + pub fn apply( + &self, + left: &DynamicLCColorState, + right: &DynamicLCColorState, + ) -> RusticolResult> { + let mut parents = [left.clone(), right.clone()]; + for (index, parent) in parents.iter_mut().enumerate() { + if self.reverse_parent_mask & (1 << index) != 0 { + *parent = parent.reversed()?; + } + } + if self.operation == LCColorComponentOperation::Close { + self.closed_components(&parents[0], &parents[1])?; + return Ok(None); + } + Ok(Some( + self.port_wiring.apply( + &parents[0], + &parents[1], + self.result_color_shape_id + .expect("validated non-closure result color shape"), + )?, + )) + } + + pub fn closed_components( + &self, + left: &DynamicLCColorState, + right: &DynamicLCColorState, + ) -> RusticolResult> { + if self.operation != LCColorComponentOperation::Close { + return Err(invalid( + "closed-components is available only for an LC closure witness", + )); + } + let mut parents = [left.clone(), right.clone()]; + for (index, parent) in parents.iter_mut().enumerate() { + if self.reverse_parent_mask & (1 << index) != 0 { + *parent = parent.reversed()?; + } + } + let closed = self.port_wiring.apply(&parents[0], &parents[1], 0)?; + if !closed.result_port_bindings().is_empty() { + return Err(invalid("an LC closure leaves an exposed color port")); + } + Ok(closed.components().to_vec()) + } +} + +fn canonical_trace_rotation(mut source_slots: Vec) -> Vec { + let length = source_slots.len(); + if length < 2 { + return source_slots; + } + + // Booth's least-rotation scan compares every cyclic suffix at most a + // constant number of times. This preserves the exact lexicographic + // canonical form without materializing all O(n) rotations. + let mut left = 0usize; + let mut right = 1usize; + let mut matched = 0usize; + while left < length && right < length && matched < length { + let left_value = source_slots[(left + matched) % length]; + let right_value = source_slots[(right + matched) % length]; + match left_value.cmp(&right_value) { + std::cmp::Ordering::Equal => matched += 1, + std::cmp::Ordering::Greater => { + left += matched + 1; + if left == right { + left += 1; + } + matched = 0; + } + std::cmp::Ordering::Less => { + right += matched + 1; + if left == right { + right += 1; + } + matched = 0; + } + } + } + source_slots.rotate_left(left.min(right) % length); + source_slots +} + +#[cfg(test)] +mod tests { + use super::canonical_trace_rotation; + + fn reference_trace_rotation(source_slots: &[u32]) -> Vec { + (0..source_slots.len()) + .map(|offset| { + source_slots[offset..] + .iter() + .chain(&source_slots[..offset]) + .copied() + .collect::>() + }) + .min() + .unwrap_or_default() + } + + #[test] + fn linear_trace_rotation_matches_exhaustive_reference() { + for length in 0..=8 { + let case_count = 3_usize.pow(length as u32); + for mut encoded in 0..case_count { + let mut source_slots = Vec::with_capacity(length); + for _ in 0..length { + source_slots.push((encoded % 3) as u32); + encoded /= 3; + } + assert_eq!( + canonical_trace_rotation(source_slots.clone()), + reference_trace_rotation(&source_slots), + "source slots {source_slots:?}", + ); + } + } + } +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/recurrence/color_contraction.rs b/pyamplicol/rust/crates/rusticol-core/src/recurrence/color_contraction.rs new file mode 100644 index 0000000000..0f11fdbffd --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/recurrence/color_contraction.rs @@ -0,0 +1,3677 @@ +// SPDX-License-Identifier: 0BSD + +//! Process-owned fixed-width recurrence color-contraction payload. + +use std::collections::{BTreeMap, BTreeSet}; +use std::iter::FusedIterator; +use std::sync::Arc; + +use sha2::{Digest, Sha256}; + +use super::exact::{ExactComplexRational, ExactRational}; +use super::symmetric_group_fft::{ + SymmetricGroupComplex64, SymmetricGroupFftPlan, SymmetricGroupFftWorkspace, +}; +use crate::{RusticolError, RusticolResult}; + +pub const RECURRENCE_COLOR_CONTRACTION_CODEC_ABI: &str = + "pyamplicol-recurrence-color-contraction-v3"; + +const MAGIC: &[u8; 8] = b"PACRCLR3"; +const VERSION: u32 = 3; +const HEADER_BYTES: usize = 120; +const ENTRY_BYTES: usize = 36; +const EXACT_FACTOR_BYTES: usize = 64; +// The subgroup order must fit the codec's u32 local-group count. +const MAX_FACTOR_RANK: u32 = u32::BITS - 1; +const MAX_SYMMETRIC_GROUP_DEGREE: u32 = 10; +// A batching target, not a resource limit: a single large lane and explicitly +// requested workspace growth may exceed it. +const TARGET_SYMMETRIC_GROUP_LANE_WORKSPACE_BYTES: usize = 512 * 1024 * 1024; +const ZERO_SECTOR_OWNER: u32 = u32::MAX; +const FLAG_INCLUDES_COLOR_FACTOR: u32 = 1 << 0; +const KNOWN_FLAGS: u32 = FLAG_INCLUDES_COLOR_FACTOR; + +fn malformed(message: impl Into) -> RusticolError { + RusticolError::artifact(format!( + "recurrence color-contraction codec: {}", + message.into() + )) +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[repr(u32)] +pub enum RecurrenceColorAccuracy { + Nlc = 1, + Full = 2, +} + +impl RecurrenceColorAccuracy { + fn decode(value: u32) -> RusticolResult { + match value { + 1 => Ok(Self::Nlc), + 2 => Ok(Self::Full), + _ => Err(malformed(format!( + "unknown color-accuracy discriminant {value}" + ))), + } + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[repr(u32)] +pub enum RecurrenceColorStorage { + Expanded = 1, + Repeated = 2, + ConvolutionKernels = 3, +} + +impl RecurrenceColorStorage { + fn decode(value: u32) -> RusticolResult { + match value { + 1 => Ok(Self::Expanded), + 2 => Ok(Self::Repeated), + 3 => Ok(Self::ConvolutionKernels), + _ => Err(malformed(format!( + "unknown color-contraction storage discriminant {value}" + ))), + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct RawColorContractionEntry { + pub left_group_id: u32, + pub right_group_id: u32, + pub weight_re: f64, + pub weight_im: f64, + pub symmetry_factor: f64, + pub exact_factor_id: u32, +} + +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct CanonicalColorContractionEntry { + pub left_group_id: u32, + pub right_group_id: u32, + pub left_destination_id: u32, + pub right_destination_id: u32, + pub weight_re: f64, + pub weight_im: f64, + pub symmetry_factor: f64, +} + +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct RuntimeColorContractionEntry { + pub left_destination_id: u32, + pub right_destination_id: u32, + pub coefficient_re: f64, + pub coefficient_im: f64, +} + +impl RuntimeColorContractionEntry { + /// Evaluate `Re(coefficient * left * conj(right))` in the canonical + /// expanded-contraction operation order. + /// + /// The coefficient supplied by [`RecurrenceColorContraction::runtime_entries`] + /// already includes the upper-triangle symmetry factor. + #[inline(always)] + pub fn contract_real_bilinear( + self, + left_re: f64, + left_im: f64, + right_re: f64, + right_im: f64, + ) -> f64 { + let product_re = left_re.mul_add(right_re, left_im * right_im); + let product_im = left_im.mul_add(right_re, -left_re * right_im); + self.coefficient_re + .mul_add(product_re, -self.coefficient_im * product_im) + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum FactorizedColorContractionKind { + KleinFourWalsh, + ElementaryAbelianWalsh, + SymmetricGroupFourier, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct FactorizedColorContraction { + kind: FactorizedColorContractionKind, + rank: u32, + coset_count: usize, + coset_indices: Vec, +} + +impl FactorizedColorContraction { + pub fn kind(&self) -> FactorizedColorContractionKind { + self.kind + } + + pub fn rank(&self) -> u32 { + self.rank + } + + pub fn subgroup_order(&self) -> usize { + match self.kind { + FactorizedColorContractionKind::KleinFourWalsh + | FactorizedColorContractionKind::ElementaryAbelianWalsh => 1usize << self.rank, + FactorizedColorContractionKind::SymmetricGroupFourier => { + checked_factorial(self.rank).expect("validated symmetric-group degree") + } + } + } + + pub fn coset_count(&self) -> usize { + self.coset_count + } + + pub fn coset(&self, index: usize) -> Option<&[u32]> { + let order = self.subgroup_order(); + let start = index.checked_mul(order)?; + self.coset_indices.get(start..start.checked_add(order)?) + } + + pub fn coset_indices(&self) -> &[u32] { + &self.coset_indices + } +} + +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct RuntimeFactorizedColorContractionEntry { + pub left_group_index: u32, + pub right_group_index: u32, + pub coefficient_re: f64, + pub coefficient_im: f64, +} + +#[derive(Clone, Debug, PartialEq)] +pub struct RuntimeFactorizedColorContraction { + subgroup_order: usize, + cosets: Vec>, + entries: Vec, + amplitude_scale: f64, +} + +#[derive(Clone, Debug, PartialEq)] +pub struct RuntimeSymmetricGroupKernel { + left_channel_index: u32, + right_channel_index: u32, + pair_scale: f64, + fourier_coefficients: Box<[f64]>, +} + +#[derive(Clone, Debug, PartialEq)] +struct RuntimeSymmetricGroupLowRankBlock { + rank: usize, + /// Column-major `dimension x rank` real factor in the FFT block's + /// original Young-basis order. + factors: Box<[f64]>, +} + +#[derive(Clone, Debug, PartialEq)] +struct RuntimeSymmetricGroupLowRankKernel { + /// Block-aligned scalar factors for the corresponding dense kernel. + /// Empty for cross-channel kernels and diagonal kernels with no accepted + /// factor. + blocks: Box<[Option]>, +} + +impl RuntimeSymmetricGroupKernel { + pub fn left_channel_index(&self) -> u32 { + self.left_channel_index + } + + pub fn right_channel_index(&self) -> u32 { + self.right_channel_index + } + + /// Upper-triangle channel-pair scale: one on the diagonal and two for a + /// cross-channel kernel whose Hermitian-adjoint partner is implicit. + pub fn pair_scale(&self) -> f64 { + self.pair_scale + } + + /// Packed real Young blocks for the unnormalised transform of + /// `r -> k_cd(r^-1)`. Blocks are column-major in the shared FFT plan's + /// canonical order. + pub fn fourier_coefficients(&self) -> &[f64] { + &self.fourier_coefficients + } +} + +#[derive(Clone, Debug)] +pub struct RuntimeSymmetricGroupColorContraction { + degree: u32, + group_order: usize, + channel_count: usize, + local_group_count: usize, + fft_plan: Arc, + kernels: Arc<[RuntimeSymmetricGroupKernel]>, + scalar_same_channel_low_rank_kernels: Option>, + residual_entries: Arc<[RuntimeFactorizedColorContractionEntry]>, +} + +impl PartialEq for RuntimeSymmetricGroupColorContraction { + fn eq(&self, other: &Self) -> bool { + self.degree == other.degree + && self.group_order == other.group_order + && self.channel_count == other.channel_count + && self.local_group_count == other.local_group_count + && self.kernels.as_ref() == other.kernels.as_ref() + && self.scalar_same_channel_low_rank_kernels + == other.scalar_same_channel_low_rank_kernels + && self.residual_entries.as_ref() == other.residual_entries.as_ref() + } +} + +impl RuntimeSymmetricGroupColorContraction { + pub fn degree(&self) -> u32 { + self.degree + } + + pub fn group_order(&self) -> usize { + self.group_order + } + + pub fn channel_count(&self) -> usize { + self.channel_count + } + + pub fn local_group_count(&self) -> usize { + self.local_group_count + } + + pub fn kernels(&self) -> &[RuntimeSymmetricGroupKernel] { + self.kernels.as_ref() + } + + pub fn residual_entries(&self) -> &[RuntimeFactorizedColorContractionEntry] { + self.residual_entries.as_ref() + } + + pub(crate) fn workspace( + &self, + lane_capacity: usize, + ) -> RusticolResult { + RuntimeSymmetricGroupColorWorkspace::new(self, lane_capacity) + } + + pub(crate) fn bounded_lane_capacity(&self, requested: usize) -> RusticolResult { + if requested == 0 { + return Err(RusticolError::invalid_argument( + "symmetric-group requested lane capacity must be positive", + )); + } + let complex_values_per_lane = self + .local_group_count + .checked_add( + self.channel_count + .checked_mul(self.group_order) + .ok_or_else(|| { + malformed("symmetric-group transformed lane size overflows usize") + })?, + ) + .and_then(|value| value.checked_add(self.group_order)) + .and_then(|value| { + value.checked_add( + self.fft_plan + .maximum_block_dimension() + .checked_mul(self.fft_plan.maximum_block_dimension())?, + ) + }) + .ok_or_else(|| malformed("symmetric-group lane workspace size overflows usize"))?; + let bytes_per_lane = complex_values_per_lane + .checked_mul(std::mem::size_of::()) + .and_then(|value| value.checked_add(std::mem::size_of::())) + .ok_or_else(|| malformed("symmetric-group lane workspace bytes overflow usize"))?; + let budget_lanes = (TARGET_SYMMETRIC_GROUP_LANE_WORKSPACE_BYTES / bytes_per_lane).max(1); + Ok(requested.min(budget_lanes)) + } + + /// Reduce one component/helicity tile. A call that exceeds the retained + /// workspace high-water mark may grow it fallibly; repeated calls within + /// the retained capacity allocate nothing. The amplitude accessor + /// receives a normalized local-group index and active lane. + pub(crate) fn reduce_lanes( + &self, + workspace: &mut RuntimeSymmetricGroupColorWorkspace, + lane_count: usize, + mut amplitude: impl FnMut(usize, usize) -> RusticolResult, + ) -> RusticolResult<()> { + workspace.ensure_lane_capacity(self, lane_count)?; + workspace.validate(self, lane_count)?; + workspace.reduced[..lane_count].fill(0.0); + + for group in 0..self.local_group_count { + let start = group * lane_count; + for lane in 0..lane_count { + workspace.gathered[start + lane] = amplitude(group, lane)?; + } + } + + // Residual and eligible/residual cross rows are contracted while the + // gathered amplitudes are still in their authenticated local order. + for entry in self.residual_entries.iter() { + let left = entry.left_group_index as usize * lane_count; + let right = entry.right_group_index as usize * lane_count; + for lane in 0..lane_count { + let left_value = workspace.gathered[left + lane]; + let right_value = workspace.gathered[right + lane]; + let product_re = left_value + .0 + .mul_add(right_value.0, left_value.1 * right_value.1); + let product_im = left_value + .1 + .mul_add(right_value.0, -left_value.0 * right_value.1); + workspace.reduced[lane] += entry + .coefficient_re + .mul_add(product_re, -entry.coefficient_im * product_im); + } + } + + let channel_stride = self.group_order * lane_count; + for channel in 0..self.channel_count { + let start = channel * channel_stride; + self.fft_plan.forward_lanes( + lane_count, + &workspace.gathered[start..start + channel_stride], + &mut workspace.transformed[start..start + channel_stride], + &mut workspace.fft, + )?; + } + + if let Some(low_rank_kernels) = self.scalar_same_channel_low_rank_kernels.as_deref() { + self.reduce_transformed_low_rank( + workspace, + lane_count, + channel_stride, + low_rank_kernels, + ); + } else { + self.reduce_transformed_dense(workspace, lane_count, channel_stride); + } + Ok(()) + } + + #[inline(never)] + fn reduce_transformed_dense( + &self, + workspace: &mut RuntimeSymmetricGroupColorWorkspace, + lane_count: usize, + channel_stride: usize, + ) { + let normalization = 1.0 / self.group_order as f64; + for kernel in self.kernels.iter() { + let left_channel = kernel.left_channel_index as usize; + let right_channel = kernel.right_channel_index as usize; + let left = &workspace.transformed + [left_channel * channel_stride..(left_channel + 1) * channel_stride]; + let right = &workspace.transformed + [right_channel * channel_stride..(right_channel + 1) * channel_stride]; + for block in self.fft_plan.blocks() { + let dimension = block.dimension(); + let offset = block.coefficient_offset(); + let coefficients = + &kernel.fourier_coefficients[offset..offset + block.coefficient_count()]; + let block_weight = dimension as f64 * normalization * kernel.pair_scale; + if left_channel == right_channel { + contract_real_hermitian_same_channel_block( + left, + coefficients, + dimension, + offset, + lane_count, + lane_count, + block_weight, + &mut workspace.reduced, + ); + } else { + contract_general_cross_block( + left, + right, + coefficients, + dimension, + offset, + lane_count, + lane_count, + block_weight, + &mut workspace.reduced, + ); + } + } + } + } + + #[inline(never)] + fn reduce_transformed_low_rank( + &self, + workspace: &mut RuntimeSymmetricGroupColorWorkspace, + lane_count: usize, + channel_stride: usize, + low_rank_kernels: &[RuntimeSymmetricGroupLowRankKernel], + ) { + debug_assert_eq!(low_rank_kernels.len(), self.kernels.len()); + let normalization = 1.0 / self.group_order as f64; + for (kernel_index, kernel) in self.kernels.iter().enumerate() { + let left_channel = kernel.left_channel_index as usize; + let right_channel = kernel.right_channel_index as usize; + let left = &workspace.transformed + [left_channel * channel_stride..(left_channel + 1) * channel_stride]; + let right = &workspace.transformed + [right_channel * channel_stride..(right_channel + 1) * channel_stride]; + if left_channel == right_channel { + let scalar_blocks: &[Option] = low_rank_kernels + .get(kernel_index) + .map(|kernel| kernel.blocks.as_ref()) + .unwrap_or_default(); + if scalar_blocks.is_empty() { + for block in self.fft_plan.blocks() { + let dimension = block.dimension(); + let offset = block.coefficient_offset(); + let coefficients = &kernel.fourier_coefficients + [offset..offset + block.coefficient_count()]; + let block_weight = dimension as f64 * normalization * kernel.pair_scale; + contract_real_hermitian_same_channel_block( + left, + coefficients, + dimension, + offset, + lane_count, + lane_count, + block_weight, + &mut workspace.reduced, + ); + } + } else { + for (block_index, block) in self.fft_plan.blocks().enumerate() { + let dimension = block.dimension(); + let offset = block.coefficient_offset(); + let coefficients = &kernel.fourier_coefficients + [offset..offset + block.coefficient_count()]; + let block_weight = dimension as f64 * normalization * kernel.pair_scale; + contract_real_hermitian_same_channel_block_opportunistic( + left, + coefficients, + scalar_blocks.get(block_index).and_then(Option::as_ref), + dimension, + offset, + lane_count, + lane_count, + block_weight, + &mut workspace.reduced, + ); + } + } + } else { + for block in self.fft_plan.blocks() { + let dimension = block.dimension(); + let offset = block.coefficient_offset(); + let coefficients = + &kernel.fourier_coefficients[offset..offset + block.coefficient_count()]; + let block_weight = dimension as f64 * normalization * kernel.pair_scale; + contract_general_cross_block( + left, + right, + coefficients, + dimension, + offset, + lane_count, + lane_count, + block_weight, + &mut workspace.reduced, + ); + } + } + } + } +} + +/// Process-local mutable storage shared by the recurrence and on-the-fly +/// contraction seams. Capacity starts at the first positive reduction size, +/// grows fallibly up to the bounded point-tile high-water mark, and never +/// shrinks; every transform keeps the point lane as its innermost coordinate. +#[derive(Debug)] +pub(crate) struct RuntimeSymmetricGroupColorWorkspace { + degree: u32, + group_order: usize, + channel_count: usize, + local_group_count: usize, + lane_capacity: usize, + gathered: Vec, + transformed: Vec, + reduced: Vec, + fft: SymmetricGroupFftWorkspace, +} + +impl RuntimeSymmetricGroupColorWorkspace { + fn new( + contraction: &RuntimeSymmetricGroupColorContraction, + lane_capacity: usize, + ) -> RusticolResult { + if lane_capacity == 0 { + return Err(RusticolError::invalid_argument( + "symmetric-group color workspace lane capacity must be positive", + )); + } + let gathered_count = contraction + .local_group_count + .checked_mul(lane_capacity) + .ok_or_else(|| malformed("symmetric-group gathered workspace size overflows usize"))?; + let transformed_count = contraction + .channel_count + .checked_mul(contraction.group_order) + .and_then(|value| value.checked_mul(lane_capacity)) + .ok_or_else(|| { + malformed("symmetric-group transformed workspace size overflows usize") + })?; + let mut gathered = Vec::new(); + gathered + .try_reserve_exact(gathered_count) + .map_err(|error| { + RusticolError::internal(format!( + "symmetric-group gathered workspace allocation failed: {error}" + )) + })?; + gathered.resize(gathered_count, (0.0, 0.0)); + let mut transformed = Vec::new(); + transformed + .try_reserve_exact(transformed_count) + .map_err(|error| { + RusticolError::internal(format!( + "symmetric-group transformed workspace allocation failed: {error}" + )) + })?; + transformed.resize(transformed_count, (0.0, 0.0)); + let mut reduced = Vec::new(); + reduced.try_reserve_exact(lane_capacity).map_err(|error| { + RusticolError::internal(format!( + "symmetric-group reduced workspace allocation failed: {error}" + )) + })?; + reduced.resize(lane_capacity, 0.0); + let fft = contraction.fft_plan.workspace(lane_capacity)?; + Ok(Self { + degree: contraction.degree, + group_order: contraction.group_order, + channel_count: contraction.channel_count, + local_group_count: contraction.local_group_count, + lane_capacity, + gathered, + transformed, + reduced, + fft, + }) + } + + fn validate( + &self, + contraction: &RuntimeSymmetricGroupColorContraction, + lane_count: usize, + ) -> RusticolResult<()> { + if lane_count == 0 || lane_count > self.lane_capacity { + return Err(RusticolError::invalid_argument( + "symmetric-group color reduction exceeds its positive lane capacity", + )); + } + if self.degree != contraction.degree + || self.group_order != contraction.group_order + || self.channel_count != contraction.channel_count + || self.local_group_count != contraction.local_group_count + { + return Err(RusticolError::integrity( + "symmetric-group color workspace does not match its contraction plan", + )); + } + Ok(()) + } + + pub(crate) fn ensure_lane_capacity( + &mut self, + contraction: &RuntimeSymmetricGroupColorContraction, + lane_count: usize, + ) -> RusticolResult<()> { + if lane_count <= self.lane_capacity { + return Ok(()); + } + *self = Self::new(contraction, lane_count)?; + Ok(()) + } + + #[cfg(test)] + pub(crate) const fn lane_capacity(&self) -> usize { + self.lane_capacity + } + + pub(crate) fn reduced(&self, lane_count: usize) -> RusticolResult<&[f64]> { + if lane_count == 0 || lane_count > self.lane_capacity { + return Err(RusticolError::invalid_argument( + "symmetric-group reduced lane view is outside its workspace", + )); + } + Ok(&self.reduced[..lane_count]) + } +} + +const LOW_RANK_PIVOT_TOLERANCE_MULTIPLIER: f64 = 64.0; +const LOW_RANK_MODE_BLOCK: usize = 4; + +fn compile_scalar_same_channel_low_rank_block( + kernel: &[f64], + dimension: usize, +) -> Option { + if dimension == 0 || kernel.len() != dimension.checked_mul(dimension)? { + return None; + } + + // The scalar dense evaluator pairs K_ci and K_ic. Factor exactly that + // effective real-symmetric matrix rather than either rounded triangle. + let mut effective = vec![0.0; kernel.len()]; + for column in 0..dimension { + for row in 0..=column { + let value = if row == column { + kernel[column * dimension + row] + } else { + 0.5 * kernel[column * dimension + row] + 0.5 * kernel[row * dimension + column] + }; + if !value.is_finite() { + return None; + } + effective[column * dimension + row] = value; + effective[row * dimension + column] = value; + } + } + + let scale = (0..dimension) + .map(|index| effective[index * dimension + index]) + .fold(f64::NEG_INFINITY, f64::max); + if !scale.is_finite() || scale < 0.0 { + return None; + } + let tolerance = LOW_RANK_PIVOT_TOLERANCE_MULTIPLIER * f64::EPSILON * dimension as f64 * scale; + if !tolerance.is_finite() { + return None; + } + + let mut residual_diagonal = (0..dimension) + .map(|index| effective[index * dimension + index]) + .collect::>(); + if residual_diagonal.iter().any(|&value| value < -tolerance) { + return None; + } + for value in &mut residual_diagonal { + if *value < 0.0 { + *value = 0.0; + } + } + + let mut selected = vec![false; dimension]; + let mut factors = Vec::::with_capacity(kernel.len()); + let mut rank = 0usize; + loop { + // Ascending traversal plus strict comparison makes equal pivots choose + // the lowest original Young-basis index deterministically. + let mut pivot = None; + for index in 0..dimension { + if selected[index] { + continue; + } + if residual_diagonal[index] < -tolerance { + return None; + } + if pivot.is_none_or(|best| residual_diagonal[index] > residual_diagonal[best]) { + pivot = Some(index); + } + } + let Some(pivot) = pivot else { + break; + }; + let pivot_value = residual_diagonal[pivot]; + if pivot_value <= tolerance { + break; + } + let pivot_root = pivot_value.sqrt(); + if !pivot_root.is_finite() || pivot_root == 0.0 { + return None; + } + + let mut factor_column = vec![0.0; dimension]; + factor_column[pivot] = pivot_root; + for row in 0..dimension { + if row == pivot || selected[row] { + continue; + } + let mut value = effective[pivot * dimension + row]; + for previous in 0..rank { + value = (-factors[previous * dimension + row]) + .mul_add(factors[previous * dimension + pivot], value); + } + value /= pivot_root; + if !value.is_finite() { + return None; + } + factor_column[row] = value; + } + + selected[pivot] = true; + residual_diagonal[pivot] = 0.0; + for row in 0..dimension { + if selected[row] { + continue; + } + let value = factor_column[row]; + let updated = (-value).mul_add(value, residual_diagonal[row]); + if !updated.is_finite() || updated < -tolerance { + return None; + } + residual_diagonal[row] = if updated < 0.0 { 0.0 } else { updated }; + } + factors.extend_from_slice(&factor_column); + rank += 1; + // Factor cost grows monotonically with rank. Once this block cannot + // clear the conservative warm-work gate, no later pivot can recover. + if !low_rank_scalar_cost_is_profitable(dimension, rank) { + return None; + } + } + + if !low_rank_scalar_cost_is_profitable(dimension, rank) + || !low_rank_factor_reconstructs_effective_kernel( + &effective, dimension, rank, &factors, tolerance, + ) + { + return None; + } + Some(RuntimeSymmetricGroupLowRankBlock { + rank, + factors: factors.into_boxed_slice(), + }) +} + +fn low_rank_scalar_cost_is_profitable(dimension: usize, rank: usize) -> bool { + let dimension = dimension as u128; + let rank = rank as u128; + let factor_cost = dimension * rank * (dimension + 1); + let dense_cost = dimension * (dimension + 1) * (dimension + 1) / 2; + factor_cost * 10 <= dense_cost * 9 +} + +fn compact_scalar_same_channel_blocks( + blocks: Vec>, +) -> Box<[Option]> { + if blocks.iter().all(Option::is_none) { + Box::default() + } else { + blocks.into_boxed_slice() + } +} + +fn low_rank_factor_reconstructs_effective_kernel( + effective: &[f64], + dimension: usize, + rank: usize, + factors: &[f64], + pivot_tolerance: f64, +) -> bool { + if effective.len() != dimension.saturating_mul(dimension) + || factors.len() != dimension.saturating_mul(rank) + { + return false; + } + if !pivot_tolerance.is_finite() { + return false; + } + for column in 0..dimension { + for row in 0..dimension { + let mut reconstructed = 0.0; + for mode in 0..rank { + reconstructed = factors[mode * dimension + row] + .mul_add(factors[mode * dimension + column], reconstructed); + } + let residual = effective[column * dimension + row] - reconstructed; + if !residual.is_finite() || residual.abs() > pivot_tolerance { + return false; + } + } + } + true +} + +#[allow(clippy::too_many_arguments)] +fn contract_real_hermitian_same_channel_block_opportunistic( + amplitudes: &[SymmetricGroupComplex64], + kernel: &[f64], + low_rank: Option<&RuntimeSymmetricGroupLowRankBlock>, + dimension: usize, + coefficient_offset: usize, + lane_capacity: usize, + lane_count: usize, + weight: f64, + reduced: &mut [f64], +) { + if lane_count == 1 + && let Some(low_rank) = low_rank + && low_rank.factors.len() == dimension.saturating_mul(low_rank.rank) + { + contract_real_hermitian_same_channel_low_rank_block_scalar( + amplitudes, + low_rank, + dimension, + coefficient_offset, + lane_capacity, + weight, + reduced, + ); + return; + } + contract_real_hermitian_same_channel_block( + amplitudes, + kernel, + dimension, + coefficient_offset, + lane_capacity, + lane_count, + weight, + reduced, + ); +} + +#[allow(clippy::too_many_arguments)] +fn contract_real_hermitian_same_channel_low_rank_block_scalar( + amplitudes: &[SymmetricGroupComplex64], + low_rank: &RuntimeSymmetricGroupLowRankBlock, + dimension: usize, + coefficient_offset: usize, + lane_capacity: usize, + weight: f64, + reduced: &mut [f64], +) { + let mut norm = 0.0; + for row in 0..dimension { + for mode_start in (0..low_rank.rank).step_by(LOW_RANK_MODE_BLOCK) { + let mode_width = (low_rank.rank - mode_start).min(LOW_RANK_MODE_BLOCK); + let mut projected_re = [0.0; LOW_RANK_MODE_BLOCK]; + let mut projected_im = [0.0; LOW_RANK_MODE_BLOCK]; + for column in 0..dimension { + let amplitude = + amplitudes[(coefficient_offset + column * dimension + row) * lane_capacity]; + for mode_local in 0..mode_width { + let factor = low_rank.factors[(mode_start + mode_local) * dimension + column]; + projected_re[mode_local] = + factor.mul_add(amplitude.0, projected_re[mode_local]); + projected_im[mode_local] = + factor.mul_add(amplitude.1, projected_im[mode_local]); + } + } + for mode_local in 0..mode_width { + norm = projected_re[mode_local].mul_add( + projected_re[mode_local], + projected_im[mode_local].mul_add(projected_im[mode_local], norm), + ); + } + } + } + reduced[0] = weight.mul_add(norm, reduced[0]); +} + +#[allow(clippy::too_many_arguments)] +fn contract_real_hermitian_same_channel_block( + amplitudes: &[SymmetricGroupComplex64], + kernel: &[f64], + dimension: usize, + coefficient_offset: usize, + lane_capacity: usize, + lane_count: usize, + weight: f64, + reduced: &mut [f64], +) { + // Diagonal channel kernels are authenticated at load as real and + // inversion-Hermitian. Their Young blocks are therefore real symmetric, + // while Re(A_c^dagger A_i) is symmetric even in the presence of harmless + // transform roundoff. Pairing K_ci and K_ic preserves the previous dense + // arithmetic for either representation and evaluates each overlap once. + if lane_count == 1 { + contract_real_hermitian_same_channel_block_scalar( + amplitudes, + kernel, + dimension, + coefficient_offset, + lane_capacity, + weight, + reduced, + ); + return; + } + + for column in 0..dimension { + let diagonal_kernel = kernel[column * dimension + column] * weight; + if diagonal_kernel != 0.0 { + for row in 0..dimension { + let value = (coefficient_offset + column * dimension + row) * lane_capacity; + for lane in 0..lane_count { + let amplitude = amplitudes[value + lane]; + let norm = amplitude.0.mul_add(amplitude.0, amplitude.1 * amplitude.1); + reduced[lane] = diagonal_kernel.mul_add(norm, reduced[lane]); + } + } + } + + for inner in column + 1..dimension { + let paired_kernel = + (kernel[column * dimension + inner] + kernel[inner * dimension + column]) * weight; + if paired_kernel == 0.0 { + continue; + } + for row in 0..dimension { + let left = (coefficient_offset + column * dimension + row) * lane_capacity; + let right = (coefficient_offset + inner * dimension + row) * lane_capacity; + for lane in 0..lane_count { + let left_value = amplitudes[left + lane]; + let right_value = amplitudes[right + lane]; + let product_re = left_value + .0 + .mul_add(right_value.0, left_value.1 * right_value.1); + reduced[lane] = paired_kernel.mul_add(product_re, reduced[lane]); + } + } + } + } +} + +const HERMITIAN_COLUMN_BLOCK: usize = 4; + +#[allow(clippy::too_many_arguments)] +fn contract_real_hermitian_same_channel_block_scalar( + amplitudes: &[SymmetricGroupComplex64], + kernel: &[f64], + dimension: usize, + coefficient_offset: usize, + lane_capacity: usize, + weight: f64, + reduced: &mut [f64], +) { + let mut result = reduced[0]; + for left_start in (0..dimension).step_by(HERMITIAN_COLUMN_BLOCK) { + let left_width = (dimension - left_start).min(HERMITIAN_COLUMN_BLOCK); + for right_start in (left_start..dimension).step_by(HERMITIAN_COLUMN_BLOCK) { + let right_width = (dimension - right_start).min(HERMITIAN_COLUMN_BLOCK); + let diagonal_block = left_start == right_start; + let mut overlaps = [[0.0_f64; HERMITIAN_COLUMN_BLOCK]; HERMITIAN_COLUMN_BLOCK]; + + // Four simultaneous unit-stride column streams reuse each loaded + // amplitude for all overlaps in the opposing four-column block. + for row in 0..dimension { + let mut left_values = [(0.0, 0.0); HERMITIAN_COLUMN_BLOCK]; + for (local, value) in left_values[..left_width].iter_mut().enumerate() { + let column = left_start + local; + *value = + amplitudes[(coefficient_offset + column * dimension + row) * lane_capacity]; + } + let mut right_values = [(0.0, 0.0); HERMITIAN_COLUMN_BLOCK]; + if diagonal_block { + right_values = left_values; + } else { + for (local, value) in right_values[..right_width].iter_mut().enumerate() { + let column = right_start + local; + *value = amplitudes + [(coefficient_offset + column * dimension + row) * lane_capacity]; + } + } + + for left_local in 0..left_width { + let first_right = if diagonal_block { left_local } else { 0 }; + let left_value = left_values[left_local]; + for right_local in first_right..right_width { + let right_value = right_values[right_local]; + overlaps[left_local][right_local] = left_value.0.mul_add( + right_value.0, + left_value + .1 + .mul_add(right_value.1, overlaps[left_local][right_local]), + ); + } + } + } + + for (left_local, overlap_row) in overlaps[..left_width].iter().enumerate() { + let column = left_start + left_local; + let first_right = if diagonal_block { left_local } else { 0 }; + for (right_local, overlap) in overlap_row[first_right..right_width] + .iter() + .copied() + .enumerate() + { + let inner_local = first_right + right_local; + let inner = right_start + inner_local; + let kernel_value = if column == inner { + kernel[column * dimension + column] + } else { + kernel[column * dimension + inner] + kernel[inner * dimension + column] + } * weight; + result = kernel_value.mul_add(overlap, result); + } + } + } + } + reduced[0] = result; +} + +#[allow(clippy::too_many_arguments)] +fn contract_general_cross_block( + left_amplitudes: &[SymmetricGroupComplex64], + right_amplitudes: &[SymmetricGroupComplex64], + kernel: &[f64], + dimension: usize, + coefficient_offset: usize, + lane_capacity: usize, + lane_count: usize, + weight: f64, + reduced: &mut [f64], +) { + for column in 0..dimension { + for inner in 0..dimension { + let kernel_value = kernel[column * dimension + inner] * weight; + for row in 0..dimension { + let left = (coefficient_offset + column * dimension + row) * lane_capacity; + let right = (coefficient_offset + inner * dimension + row) * lane_capacity; + for lane in 0..lane_count { + let left_value = left_amplitudes[left + lane]; + let right_value = right_amplitudes[right + lane]; + let product_re = left_value + .0 + .mul_add(right_value.0, left_value.1 * right_value.1); + reduced[lane] = kernel_value.mul_add(product_re, reduced[lane]); + } + } + } + } +} + +/// Runtime reduction shape. The symmetric-group variant deliberately owns +/// only authenticated raw kernels in codec phase one; lane integration adds a +/// shared immutable FFT plan without changing the payload or public C ABI. +#[derive(Clone, Debug, PartialEq)] +pub enum RuntimeColorContractionReducer { + Walsh(RuntimeFactorizedColorContraction), + SymmetricGroupFourier(RuntimeSymmetricGroupColorContraction), +} + +impl RuntimeFactorizedColorContraction { + pub fn subgroup_order(&self) -> usize { + self.subgroup_order + } + + pub fn cosets(&self) -> &[Vec] { + &self.cosets + } + + pub fn entries(&self) -> &[RuntimeFactorizedColorContractionEntry] { + &self.entries + } + + pub fn amplitude_scale(&self) -> f64 { + self.amplitude_scale + } +} + +#[derive(Clone, Debug, PartialEq)] +pub struct RecurrenceColorContraction { + accuracy: RecurrenceColorAccuracy, + storage: RecurrenceColorStorage, + includes_color_factor: bool, + group_count: u32, + sector_count: u32, + component_count: u32, + local_group_count: u32, + destination_count: u32, + stored_entry_count: usize, + stored_logical_entry_count: usize, + entries: Vec, + exact_factors: Vec, + ordered_group_ids: Vec, + destination_by_group: Vec, + sector_by_group: Vec, + component_by_group: Vec, + owner_by_sector: Vec, + ordered_destination_ids: Vec, + factorization: Option, + runtime_reducer: Option, +} + +impl RecurrenceColorContraction { + pub fn accuracy(&self) -> RecurrenceColorAccuracy { + self.accuracy + } + + pub fn storage(&self) -> RecurrenceColorStorage { + self.storage + } + + pub fn includes_color_factor(&self) -> bool { + self.includes_color_factor + } + + pub fn group_count(&self) -> u32 { + self.group_count + } + + pub fn sector_count(&self) -> u32 { + self.sector_count + } + + pub fn component_count(&self) -> u32 { + self.component_count + } + + pub fn local_group_count(&self) -> u32 { + self.local_group_count + } + + pub fn destination_count(&self) -> u32 { + self.destination_count + } + + pub fn entries(&self) -> &[RawColorContractionEntry] { + &self.entries + } + + pub(crate) fn stored_entry_count(&self) -> usize { + self.stored_entry_count + } + + pub fn exact_factors(&self) -> &[ExactComplexRational] { + &self.exact_factors + } + + pub fn ordered_group_ids(&self) -> &[u32] { + &self.ordered_group_ids + } + + pub fn destination_by_group(&self) -> &[u32] { + &self.destination_by_group + } + + pub fn sector_by_group(&self) -> &[u32] { + &self.sector_by_group + } + + pub fn component_by_group(&self) -> &[u32] { + &self.component_by_group + } + + pub fn owner_by_sector(&self) -> &[u32] { + &self.owner_by_sector + } + + pub fn active_sector_count(&self) -> usize { + self.owner_by_sector + .iter() + .enumerate() + .filter(|(sector, owner)| *sector == **owner as usize) + .count() + } + + pub fn factorization(&self) -> Option<&FactorizedColorContraction> { + self.factorization.as_ref() + } + + pub fn runtime_factorization(&self) -> Option<&RuntimeFactorizedColorContraction> { + match self.runtime_reducer.as_ref() { + Some(RuntimeColorContractionReducer::Walsh(value)) => Some(value), + _ => None, + } + } + + pub fn runtime_reducer(&self) -> Option<&RuntimeColorContractionReducer> { + self.runtime_reducer.as_ref() + } + + pub fn ordered_destination_id( + &self, + local_group_index: usize, + component_index: usize, + ) -> Option { + let ordered_index = local_group_index + .checked_mul(self.component_count as usize)? + .checked_add(component_index)?; + self.ordered_destination_ids.get(ordered_index).copied() + } + + pub fn logical_entry_count(&self) -> usize { + self.stored_logical_entry_count + } + + /// Iterate canonical logical entries without allocating. + pub fn canonical_logical_entries(&self) -> CanonicalColorContractionEntries<'_> { + assert_ne!( + self.storage, + RecurrenceColorStorage::ConvolutionKernels, + "convolution-kernel storage is not a dense logical color matrix; use its runtime reducer", + ); + CanonicalColorContractionEntries { + plan: self, + next_index: 0, + entry_count: self.logical_entry_count(), + } + } + + /// Iterate symmetry-folded Direct-Arena rows without allocating. + pub fn runtime_entries(&self) -> RuntimeColorContractionEntries<'_> { + RuntimeColorContractionEntries { + inner: self.canonical_logical_entries(), + } + } + + #[cfg(test)] + pub(crate) fn expanded_identity_for_runtime_test() -> Self { + Self { + accuracy: RecurrenceColorAccuracy::Full, + storage: RecurrenceColorStorage::Expanded, + includes_color_factor: true, + group_count: 1, + sector_count: 1, + component_count: 1, + local_group_count: 1, + destination_count: 1, + stored_entry_count: 1, + stored_logical_entry_count: 1, + entries: vec![RawColorContractionEntry { + left_group_id: 0, + right_group_id: 0, + weight_re: 1.0, + weight_im: 0.0, + symmetry_factor: 1.0, + exact_factor_id: 0, + }], + exact_factors: vec![ExactComplexRational::ONE], + ordered_group_ids: vec![0], + destination_by_group: vec![0], + sector_by_group: vec![0], + component_by_group: vec![0], + owner_by_sector: vec![0], + ordered_destination_ids: vec![0], + factorization: None, + runtime_reducer: None, + } + } + + #[cfg(test)] + pub(crate) fn symmetric_group_s3_for_runtime_test( + destination_by_group: Vec, + destination_count: u32, + ) -> Self { + assert_eq!(destination_by_group.len(), 13); + assert!( + destination_by_group + .iter() + .all(|destination| *destination < destination_count) + ); + let diagonal_left = [6.0, 1.0, 2.0, 3.0, 3.0, 4.0]; + let cross = [1.0, 2.0, 0.0, -1.0, 0.5, 3.0]; + let diagonal_right = [7.0, 0.0, 2.0, -1.0, -1.0, 0.0]; + let mut entries = Vec::new(); + let mut push = |left_group_id, right_group_id, weight_re, symmetry_factor| { + entries.push(RawColorContractionEntry { + left_group_id, + right_group_id, + weight_re, + weight_im: 0.0, + symmetry_factor, + exact_factor_id: 0, + }); + }; + for (relative, weight) in diagonal_left.into_iter().enumerate() { + push(0, relative as u32, weight, 1.0); + } + for (relative, weight) in cross.into_iter().enumerate() { + push(0, 6 + relative as u32, weight, 2.0); + } + for (relative, weight) in diagonal_right.into_iter().enumerate() { + push(6, 6 + relative as u32, weight, 1.0); + } + push(0, 12, 0.25, 2.0); + for left in 1..12 { + push(left, 12, 0.0, 2.0); + } + push(12, 12, 5.0, 1.0); + let factorization = FactorizedColorContraction { + kind: FactorizedColorContractionKind::SymmetricGroupFourier, + rank: 3, + coset_count: 2, + coset_indices: (0..12).collect(), + }; + let runtime = build_runtime_symmetric_group_convolution(&factorization, 13, &entries) + .expect("valid symmetric-group runtime fixture"); + let stored_entry_count = entries.len(); + let ordered_group_ids = (0..13).collect::>(); + let ordered_destination_ids = destination_by_group.clone(); + Self { + accuracy: RecurrenceColorAccuracy::Full, + storage: RecurrenceColorStorage::ConvolutionKernels, + includes_color_factor: true, + group_count: 13, + sector_count: 13, + component_count: 1, + local_group_count: 13, + destination_count, + stored_entry_count, + stored_logical_entry_count: stored_entry_count, + entries: Vec::new(), + exact_factors: Vec::new(), + ordered_group_ids, + destination_by_group, + sector_by_group: (0..13).collect(), + component_by_group: vec![0; 13], + owner_by_sector: (0..13).collect(), + ordered_destination_ids, + factorization: Some(factorization), + runtime_reducer: Some(RuntimeColorContractionReducer::SymmetricGroupFourier( + runtime, + )), + } + } + + #[cfg(test)] + pub(crate) fn with_sparse_sector_domain_for_runtime_test(mut self) -> Self { + self.sector_count = 27; + self.sector_by_group = self + .sector_by_group + .iter() + .map(|sector| 2 * sector + 1) + .collect(); + self.owner_by_sector = vec![ZERO_SECTOR_OWNER; self.sector_count as usize]; + for sector in &self.sector_by_group { + self.owner_by_sector[*sector as usize] = *sector; + } + self + } + + /// Evaluate the complete dense quadratic form owned by the synthetic S3 + /// runtime fixture without entering the symmetric-group reducer. + /// + /// The input is group-major and lane-minor. This deliberately retains the + /// original convolution kernels, both cross-channel orientations, and the + /// eligible/residual plus residual/residual rows so native lane tests have + /// an oracle independent of the transformed execution path. + #[cfg(test)] + pub(crate) fn symmetric_group_s3_dense_for_runtime_test( + amplitudes: &[(f64, f64)], + lane_count: usize, + ) -> Vec { + assert!(lane_count > 0); + assert_eq!(amplitudes.len(), 13 * lane_count); + + let diagonal_left = [6.0, 1.0, 2.0, 3.0, 3.0, 4.0]; + let cross = [1.0, 2.0, 0.0, -1.0, 0.5, 3.0]; + let diagonal_right = [7.0, 0.0, 2.0, -1.0, -1.0, 0.0]; + let kernels = [&diagonal_left[..], &cross[..], &diagonal_right[..]]; + let channel_pairs = [(0usize, 0usize), (0, 1), (1, 1)]; + let permutations = [ + [0usize, 1, 2], + [0, 2, 1], + [1, 0, 2], + [1, 2, 0], + [2, 0, 1], + [2, 1, 0], + ]; + let mut expected = vec![0.0; lane_count]; + for lane in 0..lane_count { + for ((left_channel, right_channel), kernel) in channel_pairs.into_iter().zip(kernels) { + let mut value = (0.0, 0.0); + for left_relative in 0..6 { + let mut inverse_left = [0usize; 3]; + for (position, image) in permutations[left_relative].iter().copied().enumerate() + { + inverse_left[image] = position; + } + for right_relative in 0..6 { + let relative = + permutations[right_relative].map(|image| inverse_left[image]); + let relative_index = permutations + .iter() + .position(|candidate| *candidate == relative) + .unwrap(); + let left = + amplitudes[(left_channel * 6 + left_relative) * lane_count + lane]; + let right = + amplitudes[(right_channel * 6 + right_relative) * lane_count + lane]; + let product_re = left.0.mul_add(right.0, left.1 * right.1); + let product_im = left.0.mul_add(right.1, -left.1 * right.0); + value.0 += kernel[relative_index] * product_re; + value.1 += kernel[relative_index] * product_im; + } + } + expected[lane] += if left_channel == right_channel { + value.0 + } else { + 2.0 * value.0 + }; + } + + // The fixture's residual suffix contains group 12. Its complete + // direct rows are 2 * 0.25 * Re(A0 conj(A12)) plus + // 5 * |A12|^2; the intervening cross rows are exact zeros. + let first = amplitudes[lane]; + let residual = amplitudes[12 * lane_count + lane]; + expected[lane] += 0.5 * first.0.mul_add(residual.0, first.1 * residual.1); + expected[lane] += 5.0 * residual.0.mul_add(residual.0, residual.1 * residual.1); + } + expected + } +} + +pub struct CanonicalColorContractionEntries<'a> { + plan: &'a RecurrenceColorContraction, + next_index: usize, + entry_count: usize, +} + +impl Iterator for CanonicalColorContractionEntries<'_> { + type Item = CanonicalColorContractionEntry; + + fn next(&mut self) -> Option { + if self.next_index >= self.entry_count { + return None; + } + let logical_index = self.next_index; + self.next_index += 1; + let (entry, left_group_id, right_group_id) = match self.plan.storage { + RecurrenceColorStorage::Expanded => { + let entry = *self.plan.entries.get(logical_index)?; + (entry, entry.left_group_id, entry.right_group_id) + } + RecurrenceColorStorage::Repeated | RecurrenceColorStorage::ConvolutionKernels => { + let template_count = self.plan.entries.len(); + if template_count == 0 { + return None; + } + let component_index = logical_index / template_count; + let entry = *self.plan.entries.get(logical_index % template_count)?; + let component_count = self.plan.component_count as usize; + let left_index = entry.left_group_id as usize * component_count + component_index; + let right_index = entry.right_group_id as usize * component_count + component_index; + ( + entry, + *self.plan.ordered_group_ids.get(left_index)?, + *self.plan.ordered_group_ids.get(right_index)?, + ) + } + }; + Some(CanonicalColorContractionEntry { + left_group_id, + right_group_id, + left_destination_id: self.plan.destination_by_group[left_group_id as usize], + right_destination_id: self.plan.destination_by_group[right_group_id as usize], + weight_re: entry.weight_re, + weight_im: entry.weight_im, + symmetry_factor: entry.symmetry_factor, + }) + } + + fn size_hint(&self) -> (usize, Option) { + let remaining = self.entry_count.saturating_sub(self.next_index); + (remaining, Some(remaining)) + } +} + +impl ExactSizeIterator for CanonicalColorContractionEntries<'_> {} +impl FusedIterator for CanonicalColorContractionEntries<'_> {} + +pub struct RuntimeColorContractionEntries<'a> { + inner: CanonicalColorContractionEntries<'a>, +} + +impl Iterator for RuntimeColorContractionEntries<'_> { + type Item = RuntimeColorContractionEntry; + + fn next(&mut self) -> Option { + let raw = self.inner.next()?; + Some(RuntimeColorContractionEntry { + left_destination_id: raw.left_destination_id, + right_destination_id: raw.right_destination_id, + coefficient_re: raw.weight_re * raw.symmetry_factor, + coefficient_im: raw.weight_im * raw.symmetry_factor, + }) + } + + fn size_hint(&self) -> (usize, Option) { + self.inner.size_hint() + } +} + +impl ExactSizeIterator for RuntimeColorContractionEntries<'_> {} +impl FusedIterator for RuntimeColorContractionEntries<'_> {} + +/// Decode and fully validate a caller-authenticated process payload. +pub fn decode_recurrence_color_contraction_v3( + bytes: &[u8], +) -> RusticolResult { + let mut reader = Reader::new(bytes); + if reader.take(8, "magic")? != MAGIC { + return Err(malformed("invalid payload magic")); + } + if reader.u32("version")? != VERSION { + return Err(malformed("unsupported payload version")); + } + if reader.u32("header size")? as usize != HEADER_BYTES { + return Err(malformed("header size does not match codec v3")); + } + let storage = RecurrenceColorStorage::decode(reader.u32("storage")?)?; + let accuracy = RecurrenceColorAccuracy::decode(reader.u32("color accuracy")?)?; + let flags = reader.u32("flags")?; + if flags & !KNOWN_FLAGS != 0 { + return Err(malformed("payload declares unknown flags")); + } + let group_count = reader.u32("group count")?; + let sector_count = reader.u32("sector count")?; + let component_count = reader.u32("component count")?; + let local_group_count = reader.u32("local group count")?; + let destination_count = reader.u32("destination count")?; + let factor_kind = reader.u32("factorization kind")?; + let factor_rank = reader.u32("factorization rank")?; + if reader.u32("entry stride")? as usize != ENTRY_BYTES { + return Err(malformed("entry stride does not match codec v3")); + } + if reader.u32("exact factor stride")? as usize != EXACT_FACTOR_BYTES { + return Err(malformed("exact factor stride does not match codec v3")); + } + let entry_count = reader.count("entry count")?; + let exact_factor_count = reader.count("exact factor count")?; + let coset_count = reader.count("coset count")?; + let coset_index_count = reader.count("coset index count")?; + let declared_logical_entry_count = reader.count("logical entry count")?; + let owner_map_count = reader.count("physical sector owner map count")?; + let payload_bytes = reader.count("payload byte count")?; + + if group_count == 0 || sector_count == 0 || component_count == 0 || destination_count == 0 { + return Err(malformed( + "group, sector, component, and destination counts must be positive", + )); + } + if owner_map_count != sector_count as usize { + return Err(malformed( + "physical sector owner map count does not match sector_count", + )); + } + let expected_payload_bytes = entry_count + .checked_mul(ENTRY_BYTES) + .and_then(|value| value.checked_add(exact_factor_count.checked_mul(EXACT_FACTOR_BYTES)?)) + .and_then(|value| value.checked_add((group_count as usize).checked_mul(16)?)) + .and_then(|value| value.checked_add(owner_map_count.checked_mul(4)?)) + .and_then(|value| value.checked_add(coset_index_count.checked_mul(4)?)) + .ok_or_else(|| malformed("payload byte count overflows usize"))?; + if payload_bytes != expected_payload_bytes + || Some(bytes.len()) != HEADER_BYTES.checked_add(payload_bytes) + { + return Err(malformed( + "declared payload size does not match its fixed-width sections", + )); + } + + let entry_domain = match storage { + RecurrenceColorStorage::Expanded => group_count, + RecurrenceColorStorage::Repeated | RecurrenceColorStorage::ConvolutionKernels => { + local_group_count + } + }; + let mut entries = Vec::with_capacity(entry_count); + let mut seen_entry_pairs = BTreeSet::new(); + for index in 0..entry_count { + let entry = RawColorContractionEntry { + left_group_id: reader.u32("entry left group ID")?, + right_group_id: reader.u32("entry right group ID")?, + weight_re: reader.f64("entry real weight")?, + weight_im: reader.f64("entry imaginary weight")?, + symmetry_factor: reader.f64("entry symmetry factor")?, + exact_factor_id: reader.u32("entry exact factor ID")?, + }; + validate_raw_entry(index, entry, entry_domain, &mut seen_entry_pairs)?; + entries.push(entry); + } + let mut exact_factors = Vec::with_capacity(exact_factor_count); + for index in 0..exact_factor_count { + let factor = ExactComplexRational::new( + ExactRational::new( + reader.i128("exact real numerator")?, + reader.i128("exact real denominator")?, + ) + .map_err(|error| malformed(format!("exact color factor {index} real part: {error}")))?, + ExactRational::new( + reader.i128("exact imaginary numerator")?, + reader.i128("exact imaginary denominator")?, + ) + .map_err(|error| { + malformed(format!( + "exact color factor {index} imaginary part: {error}" + )) + })?, + ); + exact_factors.push(factor); + } + if entries + .iter() + .any(|entry| entry.exact_factor_id as usize >= exact_factors.len()) + { + return Err(malformed( + "entry references an out-of-bounds exact color factor", + )); + } + for (index, entry) in entries.iter().copied().enumerate() { + validate_exact_matches_f64(index, entry, exact_factors[entry.exact_factor_id as usize])?; + } + let ordered_group_ids = reader.u32_vec(group_count as usize, "ordered group ID")?; + let destination_by_group = + reader.u32_vec(group_count as usize, "Direct-Arena destination ID")?; + let sector_by_group = reader.u32_vec(group_count as usize, "group physical-sector ID")?; + let component_by_group = reader.u32_vec(group_count as usize, "group resolved-helicity ID")?; + let owner_by_sector = reader.u32_vec(owner_map_count, "physical sector owner ID")?; + let coset_indices = reader.u32_vec(coset_index_count, "factorization coset index")?; + if !reader.is_finished() { + return Err(malformed("payload contains trailing bytes")); + } + + validate_permutation(&ordered_group_ids, group_count, "ordered group map")?; + validate_destination_map(&destination_by_group, destination_count)?; + validate_group_identities( + §or_by_group, + &component_by_group, + sector_count, + component_count, + )?; + validate_sector_owners(&owner_by_sector, §or_by_group, sector_count)?; + + let expected_logical_entry_count = match storage { + RecurrenceColorStorage::Expanded => { + if local_group_count != 0 + || factor_kind != 0 + || factor_rank != 0 + || coset_count != 0 + || coset_index_count != 0 + { + return Err(malformed( + "expanded storage is mixed with repeated/factorized fields", + )); + } + validate_expanded_components(&entries, &component_by_group)?; + entry_count + } + RecurrenceColorStorage::Repeated => { + if component_count < 2 { + return Err(malformed( + "repeated storage requires at least two components", + )); + } + if local_group_count == 0 + || local_group_count.checked_mul(component_count) != Some(group_count) + { + return Err(malformed( + "repeated local, sector, component, and group counts are inconsistent", + )); + } + validate_repeated_group_identities( + &ordered_group_ids, + §or_by_group, + &component_by_group, + local_group_count, + component_count, + )?; + entry_count + .checked_mul(component_count as usize) + .ok_or_else(|| malformed("logical entry count overflows usize"))? + } + RecurrenceColorStorage::ConvolutionKernels => { + if local_group_count == 0 + || local_group_count.checked_mul(component_count) != Some(group_count) + { + return Err(malformed( + "convolution-kernel local, component, and group counts are inconsistent", + )); + } + validate_repeated_group_identities( + &ordered_group_ids, + §or_by_group, + &component_by_group, + local_group_count, + component_count, + )?; + entry_count + .checked_mul(component_count as usize) + .ok_or_else(|| malformed("logical entry count overflows usize"))? + } + }; + if declared_logical_entry_count != expected_logical_entry_count { + return Err(malformed( + "declared logical entry count is inconsistent with storage", + )); + } + + let factorization = decode_factorization( + storage, + factor_kind, + factor_rank, + coset_count, + coset_indices, + local_group_count, + &entries, + )?; + let runtime_reducer = factorization + .as_ref() + .map(|factorization| match factorization.kind() { + FactorizedColorContractionKind::KleinFourWalsh + | FactorizedColorContractionKind::ElementaryAbelianWalsh => { + build_runtime_factorization(factorization, local_group_count, &entries) + .map(RuntimeColorContractionReducer::Walsh) + } + FactorizedColorContractionKind::SymmetricGroupFourier => { + build_runtime_symmetric_group_convolution( + factorization, + local_group_count, + &entries, + ) + .map(RuntimeColorContractionReducer::SymmetricGroupFourier) + } + }) + .transpose()?; + let ordered_destination_ids = ordered_group_ids + .iter() + .map(|group_id| destination_by_group[*group_id as usize]) + .collect(); + + let retain_wire_catalog = storage != RecurrenceColorStorage::ConvolutionKernels; + Ok(RecurrenceColorContraction { + accuracy, + storage, + includes_color_factor: flags & FLAG_INCLUDES_COLOR_FACTOR != 0, + group_count, + sector_count, + component_count, + local_group_count, + destination_count, + stored_entry_count: entry_count, + stored_logical_entry_count: declared_logical_entry_count, + entries: if retain_wire_catalog { + entries + } else { + Default::default() + }, + exact_factors: if retain_wire_catalog { + exact_factors + } else { + Default::default() + }, + ordered_group_ids, + destination_by_group, + sector_by_group, + component_by_group, + owner_by_sector, + ordered_destination_ids, + factorization, + runtime_reducer, + }) +} + +/// Return the caller-owned deterministic SHA-256 digest of canonical bytes. +pub fn recurrence_color_contraction_digest(bytes: &[u8]) -> [u8; 32] { + Sha256::digest(bytes).into() +} + +fn validate_raw_entry( + index: usize, + entry: RawColorContractionEntry, + group_count: u32, + seen_pairs: &mut BTreeSet<(u32, u32)>, +) -> RusticolResult<()> { + if entry.left_group_id >= group_count || entry.right_group_id >= group_count { + return Err(malformed(format!( + "entry {index} references an out-of-bounds group" + ))); + } + if entry.left_group_id > entry.right_group_id { + return Err(malformed(format!( + "entry {index} is not canonical upper triangular" + ))); + } + if !seen_pairs.insert((entry.left_group_id, entry.right_group_id)) { + return Err(malformed(format!( + "entry {index} duplicates a canonical group pair" + ))); + } + if !entry.weight_re.is_finite() + || !entry.weight_im.is_finite() + || !entry.symmetry_factor.is_finite() + { + return Err(malformed(format!( + "entry {index} contains a non-finite f64" + ))); + } + if !(entry.weight_re * entry.symmetry_factor).is_finite() + || !(entry.weight_im * entry.symmetry_factor).is_finite() + { + return Err(malformed(format!( + "entry {index} overflows after symmetry folding" + ))); + } + Ok(()) +} + +fn validate_exact_matches_f64( + index: usize, + entry: RawColorContractionEntry, + factor: ExactComplexRational, +) -> RusticolResult<()> { + let actual = [ + entry.weight_re * entry.symmetry_factor, + entry.weight_im * entry.symmetry_factor, + ]; + let expected = [ + factor.real().numerator() as f64 / factor.real().denominator() as f64, + factor.imag().numerator() as f64 / factor.imag().denominator() as f64, + ]; + for (component, actual, expected) in ["real", "imaginary"] + .into_iter() + .zip(actual) + .zip(expected) + .map(|((component, actual), expected)| (component, actual, expected)) + { + let tolerance = f64_ulp(actual).max(f64_ulp(expected)); + if (actual - expected).abs() > tolerance { + return Err(malformed(format!( + "entry {index} {component} f64 coefficient disagrees with its exact color factor", + ))); + } + } + Ok(()) +} + +fn f64_ulp(value: f64) -> f64 { + if value == 0.0 { + return f64::from_bits(1); + } + let magnitude = value.abs(); + if !magnitude.is_finite() { + return f64::INFINITY; + } + f64::from_bits(magnitude.to_bits() + 1) - magnitude +} + +fn validate_permutation(values: &[u32], count: u32, label: &str) -> RusticolResult<()> { + let mut seen = vec![false; count as usize]; + for value in values { + let Some(slot) = seen.get_mut(*value as usize) else { + return Err(malformed(format!("{label} contains an out-of-bounds ID"))); + }; + if *slot { + return Err(malformed(format!("{label} contains a duplicate ID"))); + } + *slot = true; + } + if seen.iter().any(|value| !value) { + return Err(malformed(format!("{label} is not a complete permutation"))); + } + Ok(()) +} + +fn validate_destination_map(values: &[u32], destination_count: u32) -> RusticolResult<()> { + let mut seen = BTreeSet::new(); + for value in values { + if *value >= destination_count { + return Err(malformed( + "destination map references an out-of-bounds Direct-Arena destination", + )); + } + if !seen.insert(*value) { + return Err(malformed( + "destination map contains a duplicate Direct-Arena destination", + )); + } + } + Ok(()) +} + +fn validate_expanded_components( + entries: &[RawColorContractionEntry], + component_by_group: &[u32], +) -> RusticolResult<()> { + for entry in entries { + if component_by_group[entry.left_group_id as usize] + != component_by_group[entry.right_group_id as usize] + { + return Err(malformed( + "expanded entry couples groups from different components", + )); + } + } + Ok(()) +} + +fn validate_group_identities( + sector_by_group: &[u32], + component_by_group: &[u32], + sector_count: u32, + component_count: u32, +) -> RusticolResult<()> { + if sector_by_group.len() != component_by_group.len() { + return Err(malformed("group identity maps have different lengths")); + } + let mut identities = BTreeSet::new(); + for (group_id, (§or_id, &component_id)) in + sector_by_group.iter().zip(component_by_group).enumerate() + { + if sector_id >= sector_count || component_id >= component_count { + return Err(malformed(format!( + "group {group_id} identity references an out-of-bounds sector or component" + ))); + } + if !identities.insert((sector_id, component_id)) { + return Err(malformed( + "group identity maps repeat a sector/component pair", + )); + } + } + Ok(()) +} + +fn validate_sector_owners( + owner_by_sector: &[u32], + sector_by_group: &[u32], + sector_count: u32, +) -> RusticolResult<()> { + if owner_by_sector.len() != sector_count as usize { + return Err(malformed("physical sector owner map has the wrong length")); + } + let mut fixed_points = BTreeSet::new(); + for (sector_id, owner_id) in owner_by_sector.iter().copied().enumerate() { + if owner_id == ZERO_SECTOR_OWNER { + continue; + } + if owner_id >= sector_count + || owner_id as usize > sector_id + || owner_by_sector[owner_id as usize] != owner_id + { + return Err(malformed(format!( + "physical sector {sector_id} has an invalid canonical owner", + ))); + } + if owner_id as usize == sector_id { + fixed_points.insert(owner_id); + } + } + let active = sector_by_group.iter().copied().collect::>(); + if active != fixed_points { + return Err(malformed( + "active recurrence sectors are not exactly the authenticated owner sectors", + )); + } + Ok(()) +} + +fn validate_repeated_group_identities( + ordered_group_ids: &[u32], + sector_by_group: &[u32], + component_by_group: &[u32], + local_group_count: u32, + component_count: u32, +) -> RusticolResult<()> { + for local_group in 0..local_group_count as usize { + let start = local_group + .checked_mul(component_count as usize) + .ok_or_else(|| malformed("repeated group identity offset overflows usize"))?; + let stop = start + .checked_add(component_count as usize) + .ok_or_else(|| malformed("repeated group identity range overflows usize"))?; + let group_ids = ordered_group_ids + .get(start..stop) + .ok_or_else(|| malformed("repeated group identity range is truncated"))?; + let first = *group_ids + .first() + .ok_or_else(|| malformed("repeated group identity row is empty"))?; + let sector_id = sector_by_group[first as usize]; + for (component_id, group_id) in group_ids.iter().copied().enumerate() { + if sector_by_group[group_id as usize] != sector_id + || component_by_group[group_id as usize] != component_id as u32 + { + return Err(malformed( + "repeated group identities are not local-color-major/component-minor", + )); + } + } + } + Ok(()) +} + +fn decode_factorization( + storage: RecurrenceColorStorage, + factor_kind: u32, + factor_rank: u32, + coset_count: usize, + coset_indices: Vec, + local_group_count: u32, + entries: &[RawColorContractionEntry], +) -> RusticolResult> { + if factor_kind == 0 { + if factor_rank != 0 || coset_count != 0 || !coset_indices.is_empty() { + return Err(malformed( + "factorization-none carries rank or coset metadata", + )); + } + if storage == RecurrenceColorStorage::ConvolutionKernels { + return Err(malformed( + "convolution-kernel storage requires symmetric-group Fourier metadata", + )); + } + return Ok(None); + } + let kind = match factor_kind { + 1 => { + if storage != RecurrenceColorStorage::Repeated { + return Err(malformed( + "Walsh factorization requires repeated color storage", + )); + } + if factor_rank != 2 { + return Err(malformed("Klein-four factorization must have rank two")); + } + FactorizedColorContractionKind::KleinFourWalsh + } + 2 => { + if storage != RecurrenceColorStorage::Repeated { + return Err(malformed( + "Walsh factorization requires repeated color storage", + )); + } + if !(3..=MAX_FACTOR_RANK).contains(&factor_rank) { + return Err(malformed(format!( + "elementary-Abelian factorization rank must be in [3, {MAX_FACTOR_RANK}]" + ))); + } + FactorizedColorContractionKind::ElementaryAbelianWalsh + } + 3 => { + if storage != RecurrenceColorStorage::ConvolutionKernels { + return Err(malformed( + "symmetric-group Fourier factorization requires convolution-kernel storage", + )); + } + if !(2..=MAX_SYMMETRIC_GROUP_DEGREE).contains(&factor_rank) { + return Err(malformed(format!( + "symmetric-group Fourier degree must be in [2, {MAX_SYMMETRIC_GROUP_DEGREE}]" + ))); + } + FactorizedColorContractionKind::SymmetricGroupFourier + } + _ => { + return Err(malformed(format!( + "unknown factorization discriminant {factor_kind}" + ))); + } + }; + let subgroup_order = match kind { + FactorizedColorContractionKind::KleinFourWalsh + | FactorizedColorContractionKind::ElementaryAbelianWalsh => 1usize + .checked_shl(factor_rank) + .ok_or_else(|| malformed("factorization subgroup order overflows usize"))?, + FactorizedColorContractionKind::SymmetricGroupFourier => checked_factorial(factor_rank) + .ok_or_else(|| malformed("symmetric-group order overflows usize"))?, + }; + if coset_count == 0 || coset_count.checked_mul(subgroup_order) != Some(coset_indices.len()) { + return Err(malformed( + "factorization channel shape does not match its group order", + )); + } + match kind { + FactorizedColorContractionKind::KleinFourWalsh + | FactorizedColorContractionKind::ElementaryAbelianWalsh => { + if coset_indices.len() != local_group_count as usize { + return Err(malformed( + "Walsh cosets do not cover every local color group", + )); + } + validate_permutation(&coset_indices, local_group_count, "factorization coset map")?; + validate_walsh_invariance(&coset_indices, coset_count, subgroup_order, entries)?; + } + FactorizedColorContractionKind::SymmetricGroupFourier => { + validate_symmetric_group_convolution( + factor_rank, + &coset_indices, + coset_count, + subgroup_order, + local_group_count, + entries, + )?; + } + } + Ok(Some(FactorizedColorContraction { + kind, + rank: factor_rank, + coset_count, + coset_indices, + })) +} + +fn validate_walsh_invariance( + coset_indices: &[u32], + coset_count: usize, + subgroup_order: usize, + entries: &[RawColorContractionEntry], +) -> RusticolResult<()> { + let mut matrix = BTreeMap::new(); + for entry in entries { + if entry.weight_im != 0.0 { + return Err(malformed( + "factorized color contraction requires real weights", + )); + } + let mut coefficient = entry.weight_re * entry.symmetry_factor; + if entry.left_group_id != entry.right_group_id { + coefficient *= 0.5; + } + if !coefficient.is_finite() { + return Err(malformed( + "factorized color contraction matrix coefficient is not finite", + )); + } + matrix.insert((entry.left_group_id, entry.right_group_id), coefficient); + } + let matrix_value = |left: u32, right: u32| { + let pair = if left <= right { + (left, right) + } else { + (right, left) + }; + matrix.get(&pair).copied().unwrap_or(0.0) + }; + for left_coset_index in 0..coset_count { + let left_start = left_coset_index * subgroup_order; + let left_coset = &coset_indices[left_start..left_start + subgroup_order]; + for right_coset_index in 0..coset_count { + let right_start = right_coset_index * subgroup_order; + let right_coset = &coset_indices[right_start..right_start + subgroup_order]; + for left_index in 0..subgroup_order { + for right_index in 0..subgroup_order { + let actual = matrix_value(left_coset[left_index], right_coset[right_index]); + let expected = + matrix_value(left_coset[0], right_coset[left_index ^ right_index]); + if actual != expected { + return Err(malformed( + "factorization cosets are inconsistent with the canonical color matrix", + )); + } + } + } + } + } + Ok(()) +} + +fn validate_symmetric_group_convolution( + _degree: u32, + channel_indices: &[u32], + channel_count: usize, + group_order: usize, + local_group_count: u32, + entries: &[RawColorContractionEntry], +) -> RusticolResult<()> { + let eligible_group_count = channel_count + .checked_mul(group_order) + .ok_or_else(|| malformed("symmetric-group eligible group count overflows usize"))?; + if eligible_group_count > local_group_count as usize { + return Err(malformed( + "symmetric-group channels exceed the local group domain", + )); + } + if channel_indices + .iter() + .copied() + .enumerate() + .any(|(expected, actual)| actual as usize != expected) + { + return Err(malformed( + "symmetric-group channels are not canonical channel-major/permutation-major indices", + )); + } + let channel_pair_count = channel_count + .checked_mul(channel_count + 1) + .and_then(|value| value.checked_div(2)) + .ok_or_else(|| malformed("symmetric-group channel-pair count overflows usize"))?; + let kernel_entry_count = channel_pair_count + .checked_mul(group_order) + .ok_or_else(|| malformed("symmetric-group kernel entry count overflows usize"))?; + if entries.len() < kernel_entry_count { + return Err(malformed( + "symmetric-group kernel rows do not cover every channel pair", + )); + } + + let mut offset = 0usize; + for left_channel in 0..channel_count { + let left_identity = (left_channel * group_order) as u32; + for right_channel in left_channel..channel_count { + let right_start = right_channel * group_order; + let expected_symmetry = if left_channel == right_channel { + 1.0 + } else { + 2.0 + }; + for relative_index in 0..group_order { + let entry = entries[offset]; + if entry.left_group_id != left_identity + || entry.right_group_id as usize != right_start + relative_index + || entry.symmetry_factor != expected_symmetry + { + return Err(malformed( + "symmetric-group kernel rows are not canonical (channel, channel, relative-permutation) records", + )); + } + if entry.weight_im != 0.0 { + return Err(malformed( + "symmetric-group color kernel contains a complex coefficient", + )); + } + offset += 1; + } + } + } + + let residual_entries = &entries[kernel_entry_count..]; + let total_pair_count = (local_group_count as usize) + .checked_mul(local_group_count as usize + 1) + .and_then(|value| value.checked_div(2)) + .ok_or_else(|| malformed("symmetric-group residual pair count overflows usize"))?; + let eligible_pair_count = eligible_group_count + .checked_mul(eligible_group_count + 1) + .and_then(|value| value.checked_div(2)) + .ok_or_else(|| malformed("symmetric-group eligible pair count overflows usize"))?; + let expected_residual_count = total_pair_count - eligible_pair_count; + if residual_entries.len() != expected_residual_count { + return Err(malformed( + "symmetric-group residual rows do not cover every pair touching the residual suffix", + )); + } + let mut residual_offset = 0usize; + for left in 0..local_group_count { + for right in left..local_group_count { + if right < eligible_group_count as u32 { + continue; + } + let entry = residual_entries[residual_offset]; + let pair = (left, right); + if (entry.left_group_id, entry.right_group_id) != pair { + return Err(malformed( + "symmetric-group residual rows are not the exhaustive canonical pair sequence", + )); + } + residual_offset += 1; + if entry.weight_im != 0.0 { + return Err(malformed( + "symmetric-group residual row contains a complex coefficient", + )); + } + let expected_symmetry = if pair.0 == pair.1 { 1.0 } else { 2.0 }; + if entry.symmetry_factor != expected_symmetry { + return Err(malformed( + "symmetric-group residual row has a noncanonical symmetry factor", + )); + } + } + } + debug_assert_eq!(residual_offset, residual_entries.len()); + Ok(()) +} + +fn checked_factorial(value: u32) -> Option { + (2..=value).try_fold(1usize, |result, factor| result.checked_mul(factor as usize)) +} + +fn build_runtime_factorization( + factorization: &FactorizedColorContraction, + local_group_count: u32, + entries: &[RawColorContractionEntry], +) -> RusticolResult { + let subgroup_order = factorization.subgroup_order(); + let cosets = (0..factorization.coset_count()) + .map(|index| { + factorization + .coset(index) + .expect("validated factorization coset") + .to_vec() + }) + .collect::>(); + let mut matrix = BTreeMap::new(); + for entry in entries { + let mut coefficient = entry.weight_re * entry.symmetry_factor; + if entry.left_group_id != entry.right_group_id { + coefficient *= 0.5; + } + matrix.insert((entry.left_group_id, entry.right_group_id), coefficient); + } + let matrix_value = |left: u32, right: u32| { + let pair = if left <= right { + (left, right) + } else { + (right, left) + }; + matrix.get(&pair).copied().unwrap_or(0.0) + }; + + let amplitude_scale = match factorization.kind() { + FactorizedColorContractionKind::KleinFourWalsh => 0.5, + FactorizedColorContractionKind::ElementaryAbelianWalsh => 1.0, + FactorizedColorContractionKind::SymmetricGroupFourier => { + return Err(malformed( + "symmetric-group convolution cannot use the Walsh runtime builder", + )); + } + }; + let weight_scale = match factorization.kind() { + FactorizedColorContractionKind::KleinFourWalsh => 1.0, + FactorizedColorContractionKind::ElementaryAbelianWalsh => 1.0 / subgroup_order as f64, + FactorizedColorContractionKind::SymmetricGroupFourier => unreachable!(), + }; + let mut transformed_entries = Vec::new(); + for left_coset_index in 0..cosets.len() { + for right_coset_index in left_coset_index..cosets.len() { + let left_coset = &cosets[left_coset_index]; + let right_coset = &cosets[right_coset_index]; + let mut weights = (0..subgroup_order) + .map(|subgroup_index| matrix_value(left_coset[0], right_coset[subgroup_index])) + .collect::>(); + walsh_butterfly_f64(&mut weights); + for (character_index, weight) in weights.into_iter().enumerate() { + let mut coefficient = weight * weight_scale; + if left_coset_index != right_coset_index { + coefficient *= 2.0; + } + if coefficient == 0.0 { + continue; + } + if !coefficient.is_finite() { + return Err(malformed( + "runtime factorized color coefficient is not finite", + )); + } + transformed_entries.push(RuntimeFactorizedColorContractionEntry { + left_group_index: left_coset[character_index], + right_group_index: right_coset[character_index], + coefficient_re: coefficient, + coefficient_im: 0.0, + }); + } + } + } + if cosets + .iter() + .flat_map(|coset| coset.iter()) + .any(|group| *group >= local_group_count) + { + return Err(malformed( + "runtime factorization references an out-of-bounds local group", + )); + } + Ok(RuntimeFactorizedColorContraction { + subgroup_order, + cosets, + entries: transformed_entries, + amplitude_scale, + }) +} + +fn build_runtime_symmetric_group_convolution( + factorization: &FactorizedColorContraction, + local_group_count: u32, + entries: &[RawColorContractionEntry], +) -> RusticolResult { + if factorization.kind() != FactorizedColorContractionKind::SymmetricGroupFourier { + return Err(malformed( + "non-symmetric factorization reached the symmetric-group runtime builder", + )); + } + let fft_plan = SymmetricGroupFftPlan::new(factorization.rank() as usize).map_err(|error| { + malformed(format!( + "could not construct the authenticated symmetric-group FFT plan: {error}" + )) + })?; + let group_order = fft_plan.order(); + if group_order != factorization.subgroup_order() { + return Err(malformed( + "symmetric-group FFT order disagrees with factorization metadata", + )); + } + let channel_count = factorization.coset_count(); + let kernel_entry_count = channel_count + .checked_mul(channel_count + 1) + .and_then(|value| value.checked_div(2)) + .and_then(|value| value.checked_mul(group_order)) + .ok_or_else(|| malformed("symmetric-group runtime kernel count overflows usize"))?; + let mut kernels = Vec::with_capacity(channel_count * (channel_count + 1) / 2); + let mut scalar_same_channel_low_rank_kernels = + Vec::with_capacity(channel_count * (channel_count + 1) / 2); + let mut has_scalar_same_channel_low_rank = false; + let mut inverse_kernel = vec![(0.0, 0.0); group_order]; + let mut transformed_kernel = vec![(0.0, 0.0); group_order]; + let inverse_relative_indices = (0..group_order) + .map(|index| { + fft_plan + .inverse_lexicographic_index(index) + .map_err(|error| { + malformed(format!( + "could not invert a symmetric-group relative index: {error}" + )) + }) + }) + .collect::>>()?; + let mut fft_workspace = fft_plan.workspace(1).map_err(|error| { + malformed(format!( + "could not allocate the authenticated symmetric-group FFT workspace: {error}" + )) + })?; + let mut offset = 0usize; + for left_channel_index in 0..channel_count { + for right_channel_index in left_channel_index..channel_count { + let raw_kernel = &entries[offset..offset + group_order]; + if left_channel_index == right_channel_index { + for (relative_index, entry) in raw_kernel.iter().enumerate() { + let inverse_index = inverse_relative_indices[relative_index]; + if entry.weight_re != raw_kernel[inverse_index].weight_re { + return Err(malformed( + "symmetric-group diagonal kernel violates inverse Hermiticity", + )); + } + } + } + for (relative_index, value) in inverse_kernel.iter_mut().enumerate() { + let inverse_index = inverse_relative_indices[relative_index]; + *value = (raw_kernel[inverse_index].weight_re, 0.0); + } + fft_plan + .forward(&inverse_kernel, &mut transformed_kernel, &mut fft_workspace) + .map_err(|error| { + malformed(format!( + "could not transform an authenticated symmetric-group kernel: {error}" + )) + })?; + if transformed_kernel + .iter() + .any(|value| value.1 != 0.0 || !value.0.is_finite()) + { + return Err(malformed( + "symmetric-group kernel transform did not produce finite real Young blocks", + )); + } + // Canonical zero rows remain mandatory on wire and participate in + // exact-factor authentication above. Once their transform is + // certified zero, retaining a runtime block would only add warmed + // traffic and immutable RSS. + if transformed_kernel.iter().any(|value| value.0 != 0.0) { + let fourier_coefficients = transformed_kernel + .iter() + .map(|value| value.0) + .collect::>() + .into_boxed_slice(); + let scalar_same_channel_blocks = if left_channel_index == right_channel_index { + let blocks = fft_plan + .blocks() + .map(|block| { + let start = block.coefficient_offset(); + let end = start + block.coefficient_count(); + compile_scalar_same_channel_low_rank_block( + &fourier_coefficients[start..end], + block.dimension(), + ) + }) + .collect::>(); + compact_scalar_same_channel_blocks(blocks) + } else { + Box::default() + }; + has_scalar_same_channel_low_rank |= + scalar_same_channel_blocks.iter().any(Option::is_some); + kernels.push(RuntimeSymmetricGroupKernel { + left_channel_index: left_channel_index as u32, + right_channel_index: right_channel_index as u32, + pair_scale: if left_channel_index == right_channel_index { + 1.0 + } else { + 2.0 + }, + fourier_coefficients, + }); + scalar_same_channel_low_rank_kernels.push(RuntimeSymmetricGroupLowRankKernel { + blocks: scalar_same_channel_blocks, + }); + } + offset += group_order; + } + } + debug_assert_eq!(offset, kernel_entry_count); + debug_assert_eq!(scalar_same_channel_low_rank_kernels.len(), kernels.len()); + let scalar_same_channel_low_rank_kernels: Option> = + has_scalar_same_channel_low_rank.then(|| Arc::from(scalar_same_channel_low_rank_kernels)); + let residual_entries: Vec = entries + [kernel_entry_count..] + .iter() + .filter_map(|entry| { + let coefficient_re = entry.weight_re * entry.symmetry_factor; + let coefficient_im = entry.weight_im * entry.symmetry_factor; + (coefficient_re != 0.0 || coefficient_im != 0.0).then_some( + RuntimeFactorizedColorContractionEntry { + left_group_index: entry.left_group_id, + right_group_index: entry.right_group_id, + coefficient_re, + coefficient_im, + }, + ) + }) + .collect(); + let eligible_group_count = channel_count + .checked_mul(group_order) + .ok_or_else(|| malformed("symmetric-group eligible group count overflows usize"))?; + if eligible_group_count > local_group_count as usize { + return Err(malformed( + "runtime symmetric-group channel references an out-of-bounds local group", + )); + } + Ok(RuntimeSymmetricGroupColorContraction { + degree: factorization.rank(), + group_order, + channel_count, + local_group_count: local_group_count as usize, + fft_plan: Arc::new(fft_plan), + kernels: kernels.into(), + scalar_same_channel_low_rank_kernels, + residual_entries: residual_entries.into(), + }) +} + +fn walsh_butterfly_f64(values: &mut [f64]) { + debug_assert!(values.len().is_power_of_two()); + let mut stride = 1; + while stride < values.len() { + for start in (0..values.len()).step_by(stride * 2) { + for offset in 0..stride { + let left = values[start + offset]; + let right = values[start + stride + offset]; + values[start + offset] = left + right; + values[start + stride + offset] = left - right; + } + } + stride *= 2; + } +} + +struct Reader<'a> { + bytes: &'a [u8], + offset: usize, +} + +impl<'a> Reader<'a> { + fn new(bytes: &'a [u8]) -> Self { + Self { bytes, offset: 0 } + } + + fn take(&mut self, count: usize, label: &str) -> RusticolResult<&'a [u8]> { + let end = self + .offset + .checked_add(count) + .ok_or_else(|| malformed(format!("{label} offset overflows usize")))?; + let result = self.bytes.get(self.offset..end).ok_or_else(|| { + malformed(format!( + "truncated {label} at byte {}: need {count}, have {}", + self.offset, + self.bytes.len().saturating_sub(self.offset) + )) + })?; + self.offset = end; + Ok(result) + } + + fn u32(&mut self, label: &str) -> RusticolResult { + Ok(u32::from_le_bytes( + self.take(4, label)?.try_into().expect("checked read"), + )) + } + + fn u64(&mut self, label: &str) -> RusticolResult { + Ok(u64::from_le_bytes( + self.take(8, label)?.try_into().expect("checked read"), + )) + } + + fn i128(&mut self, label: &str) -> RusticolResult { + Ok(i128::from_le_bytes( + self.take(16, label)?.try_into().expect("checked read"), + )) + } + + fn f64(&mut self, label: &str) -> RusticolResult { + Ok(f64::from_le_bytes( + self.take(8, label)?.try_into().expect("checked read"), + )) + } + + fn count(&mut self, label: &str) -> RusticolResult { + let value = self.u64(label)?; + usize::try_from(value).map_err(|_| malformed(format!("{label} exceeds usize"))) + } + + fn u32_vec(&mut self, count: usize, label: &str) -> RusticolResult> { + (0..count).map(|_| self.u32(label)).collect() + } + + fn is_finished(&self) -> bool { + self.offset == self.bytes.len() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[derive(Clone)] + struct TestWire { + storage: u32, + accuracy: u32, + flags: u32, + group_count: u32, + sector_count: u32, + component_count: u32, + local_group_count: u32, + destination_count: u32, + factor_kind: u32, + factor_rank: u32, + entries: Vec, + exact_factors: Vec, + ordered_group_ids: Vec, + destination_by_group: Vec, + sector_by_group: Vec, + component_by_group: Vec, + owner_by_sector: Vec, + cosets: Vec>, + logical_entry_count: u64, + } + + impl TestWire { + fn expanded() -> Self { + Self { + storage: 1, + accuracy: 1, + flags: FLAG_INCLUDES_COLOR_FACTOR, + group_count: 2, + sector_count: 2, + component_count: 1, + local_group_count: 0, + destination_count: 12, + factor_kind: 0, + factor_rank: 0, + entries: vec![ + RawColorContractionEntry { + left_group_id: 0, + right_group_id: 0, + weight_re: 3.0, + weight_im: 0.0, + symmetry_factor: 1.0, + exact_factor_id: 0, + }, + RawColorContractionEntry { + left_group_id: 0, + right_group_id: 1, + weight_re: 2.0, + weight_im: 0.5, + symmetry_factor: 2.0, + exact_factor_id: 1, + }, + ], + exact_factors: vec![ + ExactComplexRational::new( + ExactRational::new(3, 1).unwrap(), + ExactRational::ZERO, + ), + ExactComplexRational::new( + ExactRational::new(4, 1).unwrap(), + ExactRational::new(1, 1).unwrap(), + ), + ], + ordered_group_ids: vec![0, 1], + destination_by_group: vec![7, 9], + sector_by_group: vec![0, 1], + component_by_group: vec![0, 0], + owner_by_sector: vec![0, 1], + cosets: Vec::new(), + logical_entry_count: 2, + } + } + + fn repeated_k4() -> Self { + Self { + storage: 2, + accuracy: 2, + flags: FLAG_INCLUDES_COLOR_FACTOR, + group_count: 8, + sector_count: 4, + component_count: 2, + local_group_count: 4, + destination_count: 20, + factor_kind: 1, + factor_rank: 2, + entries: (0..4) + .map(|index| RawColorContractionEntry { + left_group_id: index, + right_group_id: index, + weight_re: 1.0, + weight_im: 0.0, + symmetry_factor: 1.0, + exact_factor_id: 0, + }) + .collect(), + exact_factors: vec![ExactComplexRational::ONE], + ordered_group_ids: vec![0, 4, 1, 5, 2, 6, 3, 7], + destination_by_group: vec![8, 9, 10, 11, 12, 13, 14, 15], + sector_by_group: vec![0, 1, 2, 3, 0, 1, 2, 3], + component_by_group: vec![0, 0, 0, 0, 1, 1, 1, 1], + owner_by_sector: vec![0, 1, 2, 3], + cosets: vec![vec![0, 1, 2, 3]], + logical_entry_count: 8, + } + } + + fn repeated_k4_multicoset() -> Self { + let diagonal_blocks = [[2.0, 1.0, 0.0, -1.0], [3.0, 0.5, -0.5, 1.5]]; + let cross_block = [1.0, 2.0, -1.0, 0.25]; + let matrix_value = |left: usize, right: usize| { + let left_coset = left / 4; + let right_coset = right / 4; + let xor = (left % 4) ^ (right % 4); + if left_coset == right_coset { + diagonal_blocks[left_coset][xor] + } else { + cross_block[xor] + } + }; + let mut entries = Vec::new(); + let mut exact_factors = Vec::new(); + for left in 0..8 { + for right in left..8 { + let weight = matrix_value(left, right); + if weight == 0.0 { + continue; + } + let symmetry = if left == right { 1.0 } else { 2.0 }; + let coefficient = weight * symmetry; + let exact_factor_id = exact_factors.len() as u32; + exact_factors.push(ExactComplexRational::new( + ExactRational::from_f64_exact(coefficient).unwrap(), + ExactRational::ZERO, + )); + entries.push(RawColorContractionEntry { + left_group_id: left as u32, + right_group_id: right as u32, + weight_re: weight, + weight_im: 0.0, + symmetry_factor: symmetry, + exact_factor_id, + }); + } + } + let logical_entry_count = (2 * entries.len()) as u64; + Self { + storage: 2, + accuracy: 2, + flags: FLAG_INCLUDES_COLOR_FACTOR, + group_count: 16, + sector_count: 8, + component_count: 2, + local_group_count: 8, + destination_count: 16, + factor_kind: 1, + factor_rank: 2, + entries, + exact_factors, + ordered_group_ids: (0..16).collect(), + destination_by_group: (0..16).collect(), + sector_by_group: (0..8).flat_map(|sector| [sector, sector]).collect(), + component_by_group: (0..8).flat_map(|_| [0, 1]).collect(), + owner_by_sector: (0..8).collect(), + cosets: vec![vec![0, 1, 2, 3], vec![4, 5, 6, 7]], + logical_entry_count, + } + } + + fn convolution_s3_with_residual() -> Self { + let diagonal_left = [6.0, 1.0, 2.0, 3.0, 3.0, 4.0]; + let cross = [1.0, 2.0, 0.0, -1.0, 0.5, 3.0]; + let diagonal_right = [7.0, 0.0, 2.0, -1.0, -1.0, 0.0]; + let mut entries = Vec::new(); + let mut exact_factors = Vec::new(); + let mut push = |left: u32, right: u32, weight: f64, symmetry: f64| { + let exact_factor_id = exact_factors.len() as u32; + exact_factors.push(ExactComplexRational::new( + ExactRational::from_f64_exact(weight * symmetry).unwrap(), + ExactRational::ZERO, + )); + entries.push(RawColorContractionEntry { + left_group_id: left, + right_group_id: right, + weight_re: weight, + weight_im: 0.0, + symmetry_factor: symmetry, + exact_factor_id, + }); + }; + for (relative, weight) in diagonal_left.into_iter().enumerate() { + push(0, relative as u32, weight, 1.0); + } + for (relative, weight) in cross.into_iter().enumerate() { + push(0, 6 + relative as u32, weight, 2.0); + } + for (relative, weight) in diagonal_right.into_iter().enumerate() { + push(6, 6 + relative as u32, weight, 1.0); + } + // Ordinary direct rows touching the residual suffix follow kernels. + push(0, 12, 0.25, 2.0); + for left in 1..12 { + push(left, 12, 0.0, 2.0); + } + push(12, 12, 5.0, 1.0); + let logical_entry_count = entries.len() as u64; + Self { + storage: 3, + accuracy: 2, + flags: FLAG_INCLUDES_COLOR_FACTOR, + group_count: 13, + sector_count: 13, + component_count: 1, + local_group_count: 13, + destination_count: 20, + factor_kind: 3, + factor_rank: 3, + entries, + exact_factors, + ordered_group_ids: (0..13).collect(), + destination_by_group: (0..13).map(|value| value + 2).collect(), + sector_by_group: (0..13).collect(), + component_by_group: vec![0; 13], + owner_by_sector: (0..13).collect(), + cosets: vec![(0..6).collect(), (6..12).collect()], + logical_entry_count, + } + } + + fn encode(&self) -> Vec { + let flattened_cosets = self.cosets.iter().flatten().copied().collect::>(); + let payload_bytes = self.entries.len() * ENTRY_BYTES + + self.exact_factors.len() * EXACT_FACTOR_BYTES + + self.ordered_group_ids.len() * 4 + + self.destination_by_group.len() * 4 + + self.sector_by_group.len() * 4 + + self.component_by_group.len() * 4 + + self.owner_by_sector.len() * 4 + + flattened_cosets.len() * 4; + let mut bytes = Vec::with_capacity(HEADER_BYTES + payload_bytes); + bytes.extend_from_slice(MAGIC); + for value in [ + VERSION, + HEADER_BYTES as u32, + self.storage, + self.accuracy, + self.flags, + self.group_count, + self.sector_count, + self.component_count, + self.local_group_count, + self.destination_count, + self.factor_kind, + self.factor_rank, + ENTRY_BYTES as u32, + EXACT_FACTOR_BYTES as u32, + ] { + bytes.extend_from_slice(&value.to_le_bytes()); + } + for value in [ + self.entries.len() as u64, + self.exact_factors.len() as u64, + self.cosets.len() as u64, + flattened_cosets.len() as u64, + self.logical_entry_count, + self.owner_by_sector.len() as u64, + payload_bytes as u64, + ] { + bytes.extend_from_slice(&value.to_le_bytes()); + } + assert_eq!(bytes.len(), HEADER_BYTES); + for entry in &self.entries { + bytes.extend_from_slice(&entry.left_group_id.to_le_bytes()); + bytes.extend_from_slice(&entry.right_group_id.to_le_bytes()); + bytes.extend_from_slice(&entry.weight_re.to_le_bytes()); + bytes.extend_from_slice(&entry.weight_im.to_le_bytes()); + bytes.extend_from_slice(&entry.symmetry_factor.to_le_bytes()); + bytes.extend_from_slice(&entry.exact_factor_id.to_le_bytes()); + } + for factor in &self.exact_factors { + for value in [ + factor.real().numerator(), + factor.real().denominator(), + factor.imag().numerator(), + factor.imag().denominator(), + ] { + bytes.extend_from_slice(&value.to_le_bytes()); + } + } + for value in self + .ordered_group_ids + .iter() + .chain(&self.destination_by_group) + .chain(&self.sector_by_group) + .chain(&self.component_by_group) + .chain(&self.owner_by_sector) + .chain(&flattened_cosets) + { + bytes.extend_from_slice(&value.to_le_bytes()); + } + bytes + } + } + + fn error_contains(bytes: &[u8], expected: &str) { + let error = decode_recurrence_color_contraction_v3(bytes).unwrap_err(); + assert!( + error.message().contains(expected), + "expected {expected:?} in {:?}", + error.message() + ); + } + + #[test] + fn expanded_payload_preserves_raw_and_derives_runtime_entries() { + let bytes = TestWire::expanded().encode(); + let plan = decode_recurrence_color_contraction_v3(&bytes).unwrap(); + assert_eq!(plan.accuracy(), RecurrenceColorAccuracy::Nlc); + assert_eq!(plan.storage(), RecurrenceColorStorage::Expanded); + assert!(plan.includes_color_factor()); + assert_eq!(plan.sector_count(), 2); + assert_eq!(plan.component_count(), 1); + assert_eq!(plan.ordered_group_ids(), [0, 1]); + assert_eq!(plan.destination_by_group(), [7, 9]); + let raw = plan.canonical_logical_entries().collect::>(); + assert_eq!(raw[1].weight_re, 2.0); + assert_eq!(raw[1].weight_im, 0.5); + assert_eq!(raw[1].symmetry_factor, 2.0); + let runtime = plan.runtime_entries().collect::>(); + assert_eq!(runtime[1].left_destination_id, 7); + assert_eq!(runtime[1].right_destination_id, 9); + assert_eq!(runtime[1].coefficient_re, 4.0); + assert_eq!(runtime[1].coefficient_im, 1.0); + assert_eq!( + recurrence_color_contraction_digest(&bytes), + recurrence_color_contraction_digest(&bytes) + ); + } + + #[test] + fn runtime_entry_contracts_complex_upper_triangle_row() { + let plan = decode_recurrence_color_contraction_v3(&TestWire::expanded().encode()).unwrap(); + let runtime = plan.runtime_entries().collect::>(); + + // The diagonal row contributes 3 * |1 + 2i|^2. + assert_eq!(runtime[0].contract_real_bilinear(1.0, 2.0, 1.0, 2.0), 15.0); + // The stored upper-triangle row has raw coefficient 2 + 0.5i and + // symmetry factor two, hence runtime coefficient 4 + i. + assert_eq!(runtime[1].contract_real_bilinear(1.0, 2.0, 3.0, 4.0), 42.0); + } + + #[test] + fn repeated_k4_payload_expands_logical_rows_without_runtime_allocation() { + let bytes = TestWire::repeated_k4().encode(); + let plan = decode_recurrence_color_contraction_v3(&bytes).unwrap(); + assert_eq!(plan.storage(), RecurrenceColorStorage::Repeated); + assert_eq!(plan.logical_entry_count(), 8); + let factor = plan.factorization().unwrap(); + assert_eq!( + factor.kind(), + FactorizedColorContractionKind::KleinFourWalsh + ); + assert_eq!(factor.coset(0), Some(&[0, 1, 2, 3][..])); + let runtime_factor = plan.runtime_factorization().unwrap(); + assert_eq!(runtime_factor.subgroup_order(), 4); + assert_eq!(runtime_factor.cosets(), [vec![0, 1, 2, 3]]); + assert_eq!(runtime_factor.amplitude_scale(), 0.5); + assert_eq!(runtime_factor.entries().len(), 4); + assert!(runtime_factor.entries().iter().all(|entry| { + entry.left_group_index == entry.right_group_index + && entry.coefficient_re == 1.0 + && entry.coefficient_im == 0.0 + })); + assert_eq!(plan.ordered_destination_id(0, 0), Some(8)); + assert_eq!(plan.ordered_destination_id(0, 1), Some(12)); + assert_eq!(plan.ordered_destination_id(3, 1), Some(15)); + let mut entries = plan.canonical_logical_entries(); + assert_eq!(entries.len(), 8); + assert_eq!(entries.next().unwrap().left_group_id, 0); + assert_eq!(entries.next().unwrap().left_group_id, 1); + assert_eq!(entries.next().unwrap().left_group_id, 2); + assert_eq!(entries.next().unwrap().left_group_id, 3); + assert_eq!(entries.next().unwrap().left_group_id, 4); + assert_eq!(entries.len(), 3); + } + + #[test] + fn repeated_multicoset_k4_preserves_the_nontrivial_quadratic_form() { + let plan = + decode_recurrence_color_contraction_v3(&TestWire::repeated_k4_multicoset().encode()) + .unwrap(); + let amplitudes = [0.5, -1.25, 2.0, 0.75, -0.4, 1.1, 0.2, -0.9]; + let direct = plan + .entries() + .iter() + .map(|entry| { + entry.weight_re + * entry.symmetry_factor + * amplitudes[entry.left_group_id as usize] + * amplitudes[entry.right_group_id as usize] + }) + .sum::(); + + let factorized = plan.runtime_factorization().unwrap(); + assert_eq!(factorized.cosets().len(), 2); + let mut transformed = [0.0; 8]; + for coset in factorized.cosets() { + let mut values = coset + .iter() + .map(|index| amplitudes[*index as usize]) + .collect::>(); + walsh_butterfly_f64(&mut values); + for (index, value) in coset.iter().zip(values) { + transformed[*index as usize] = value * factorized.amplitude_scale(); + } + } + let transformed_value = factorized + .entries() + .iter() + .map(|entry| { + entry.coefficient_re + * transformed[entry.left_group_index as usize] + * transformed[entry.right_group_index as usize] + }) + .sum::(); + assert!((direct - transformed_value).abs() <= 32.0 * f64::EPSILON); + } + + #[test] + fn symmetric_group_payload_preserves_kernel_orientation_and_residual_rows() { + let plan = decode_recurrence_color_contraction_v3( + &TestWire::convolution_s3_with_residual().encode(), + ) + .unwrap(); + assert_eq!(plan.storage(), RecurrenceColorStorage::ConvolutionKernels); + let factorization = plan.factorization().unwrap(); + assert_eq!( + factorization.kind(), + FactorizedColorContractionKind::SymmetricGroupFourier + ); + assert_eq!(factorization.rank(), 3); + assert_eq!(factorization.subgroup_order(), 6); + assert_eq!(factorization.coset(0), Some(&[0, 1, 2, 3, 4, 5][..])); + assert_eq!(factorization.coset(1), Some(&[6, 7, 8, 9, 10, 11][..])); + let RuntimeColorContractionReducer::SymmetricGroupFourier(runtime) = + plan.runtime_reducer().unwrap() + else { + panic!("expected symmetric-group runtime reducer") + }; + assert_eq!(runtime.degree(), 3); + assert_eq!(runtime.group_order(), 6); + assert_eq!(runtime.channel_count(), 2); + assert_eq!(runtime.local_group_count(), 13); + assert_eq!(runtime.kernels().len(), 3); + assert_eq!(runtime.kernels()[1].left_channel_index(), 0); + assert_eq!(runtime.kernels()[1].right_channel_index(), 1); + assert_eq!(runtime.kernels()[0].pair_scale(), 1.0); + assert_eq!(runtime.kernels()[1].pair_scale(), 2.0); + let fft_plan = SymmetricGroupFftPlan::new(3).unwrap(); + assert!(runtime.scalar_same_channel_low_rank_kernels.is_none()); + let raw_cross = [1.0, 2.0, 0.0, -1.0, 0.5, 3.0]; + let inverse_cross = (0..fft_plan.order()) + .map(|index| { + ( + raw_cross[fft_plan.inverse_lexicographic_index(index).unwrap()], + 0.0, + ) + }) + .collect::>(); + let mut expected_fourier = vec![(0.0, 0.0); fft_plan.order()]; + let mut fft_workspace = fft_plan.workspace(1).unwrap(); + fft_plan + .forward(&inverse_cross, &mut expected_fourier, &mut fft_workspace) + .unwrap(); + assert_eq!( + runtime.kernels()[1].fourier_coefficients(), + expected_fourier + .iter() + .map(|value| value.0) + .collect::>() + ); + assert_eq!(runtime.residual_entries().len(), 2); + assert_eq!(runtime.residual_entries()[0].left_group_index, 0); + assert_eq!(runtime.residual_entries()[0].right_group_index, 12); + assert_eq!(runtime.residual_entries()[0].coefficient_re, 0.5); + assert_eq!(plan.stored_entry_count(), 31); + assert_eq!(plan.logical_entry_count(), 31); + assert!(plan.entries().is_empty()); + assert!(plan.exact_factors().is_empty()); + } + + fn test_low_rank_effective_kernel() -> (usize, Vec) { + let dimension = 5; + let root_two = 2.0_f64.sqrt(); + let columns = [ + [root_two, 0.0, 1.0 / root_two, 0.0, 0.0], + [0.0, root_two, 0.0, 1.0 / root_two, 0.0], + ]; + let mut kernel = vec![0.0; dimension * dimension]; + for column in 0..dimension { + for row in 0..dimension { + kernel[column * dimension + row] = columns + .iter() + .map(|factor| factor[column] * factor[row]) + .sum(); + } + } + // Only the effective symmetric part is contracted. Opposite skew + // perturbations must therefore leave the derived factor unchanged. + kernel[2] += 0.125; + kernel[2 * dimension] -= 0.125; + (dimension, kernel) + } + + #[test] + fn scalar_low_rank_factorization_is_deterministic_scale_relative_and_certified() { + let (dimension, kernel) = test_low_rank_effective_kernel(); + let factor = compile_scalar_same_channel_low_rank_block(&kernel, dimension).unwrap(); + assert_eq!(factor.rank, 2); + // Equal leading residual diagonals choose original index zero first + // and original index one second. + assert!((factor.factors[0] - 2.0_f64.sqrt()).abs() <= 8.0 * f64::EPSILON); + assert_eq!(factor.factors[1], 0.0); + assert_eq!(factor.factors[dimension], 0.0); + assert!((factor.factors[dimension + 1] - 2.0_f64.sqrt()).abs() <= 8.0 * f64::EPSILON); + + let tiny_scale = 1.0e-200; + let mut tiny = vec![0.0; dimension * dimension]; + tiny[0] = tiny_scale; + let tiny_factor = compile_scalar_same_channel_low_rank_block(&tiny, dimension).unwrap(); + assert_eq!(tiny_factor.rank, 1); + assert!(tiny_factor.factors[0] > 0.0); + assert!(tiny_factor.factors[0] < 1.0e-90); + + let mut effective = kernel.clone(); + let symmetric = 0.5 * kernel[2] + 0.5 * kernel[2 * dimension]; + effective[2] = symmetric; + effective[2 * dimension] = symmetric; + let scale = (0..dimension) + .map(|index| effective[index * dimension + index]) + .fold(f64::NEG_INFINITY, f64::max); + let tolerance = + LOW_RANK_PIVOT_TOLERANCE_MULTIPLIER * f64::EPSILON * dimension as f64 * scale; + assert!(low_rank_factor_reconstructs_effective_kernel( + &effective, + dimension, + factor.rank, + &factor.factors, + tolerance, + )); + let mut corrupted = factor.factors.to_vec(); + corrupted[0] += 1.0e-5; + assert!(!low_rank_factor_reconstructs_effective_kernel( + &effective, + dimension, + factor.rank, + &corrupted, + tolerance, + )); + } + + #[test] + fn scalar_low_rank_factorization_falls_back_for_invalid_or_unprofitable_blocks() { + let dimension = 5; + let zero = vec![0.0; dimension * dimension]; + let zero_factor = compile_scalar_same_channel_low_rank_block(&zero, dimension).unwrap(); + assert_eq!(zero_factor.rank, 0); + assert!(zero_factor.factors.is_empty()); + + let mut negative_diagonal = zero; + negative_diagonal[0] = -1.0; + assert!( + compile_scalar_same_channel_low_rank_block(&negative_diagonal, dimension).is_none() + ); + + let mut indefinite = vec![0.0; dimension * dimension]; + for index in 0..dimension { + indefinite[index * dimension + index] = 1.0; + } + indefinite[1] = 2.0; + indefinite[dimension] = 2.0; + assert!(compile_scalar_same_channel_low_rank_block(&indefinite, dimension).is_none()); + + let mut nonfinite = indefinite; + nonfinite[0] = f64::NAN; + assert!(compile_scalar_same_channel_low_rank_block(&nonfinite, dimension).is_none()); + + let mut full_rank = vec![0.0; dimension * dimension]; + for index in 0..dimension { + full_rank[index * dimension + index] = 1.0; + } + assert!(compile_scalar_same_channel_low_rank_block(&full_rank, dimension).is_none()); + assert!(!low_rank_scalar_cost_is_profitable(dimension, dimension)); + assert!(low_rank_scalar_cost_is_profitable(dimension, 2)); + + let compacted = compact_scalar_same_channel_blocks(vec![None, None]); + assert!(compacted.is_empty()); + let compacted = compact_scalar_same_channel_blocks(vec![Some(zero_factor), None]); + assert_eq!(compacted.len(), 2); + assert!(compacted[0].is_some()); + assert!(compacted[1].is_none()); + } + + #[test] + fn scalar_low_rank_contraction_matches_dense_and_batches_fall_back_exactly() { + let (dimension, kernel) = test_low_rank_effective_kernel(); + let factor = compile_scalar_same_channel_low_rank_block(&kernel, dimension).unwrap(); + let coefficient_offset = 3; + let lane_capacity = 2; + let amplitudes = (0..(coefficient_offset + dimension * dimension) * lane_capacity) + .map(|index| { + ( + ((index * 17 + 5) % 31) as f64 / 11.0 - 1.2, + ((index * 13 + 3) % 29) as f64 / 9.0 - 0.8, + ) + }) + .collect::>(); + let weight = 0.375; + let mut dense = [0.25, -0.5]; + let mut low_rank = dense; + contract_real_hermitian_same_channel_block( + &litudes, + &kernel, + dimension, + coefficient_offset, + lane_capacity, + 1, + weight, + &mut dense, + ); + contract_real_hermitian_same_channel_block_opportunistic( + &litudes, + &kernel, + Some(&factor), + dimension, + coefficient_offset, + lane_capacity, + 1, + weight, + &mut low_rank, + ); + let scale = dense[0].abs().max(1.0); + assert!((low_rank[0] - dense[0]).abs() <= 2.0e-12 * scale); + assert_eq!(low_rank[1], dense[1]); + + let mut dense_batch = [0.25, -0.5]; + let mut fallback_batch = dense_batch; + contract_real_hermitian_same_channel_block( + &litudes, + &kernel, + dimension, + coefficient_offset, + lane_capacity, + 2, + weight, + &mut dense_batch, + ); + contract_real_hermitian_same_channel_block_opportunistic( + &litudes, + &kernel, + Some(&factor), + dimension, + coefficient_offset, + lane_capacity, + 2, + weight, + &mut fallback_batch, + ); + assert_eq!(fallback_batch, dense_batch); + + let malformed_factor = RuntimeSymmetricGroupLowRankBlock { + rank: factor.rank, + factors: factor.factors[..factor.factors.len() - 1] + .to_vec() + .into_boxed_slice(), + }; + let mut malformed_fallback = [0.25, -0.5]; + contract_real_hermitian_same_channel_block_opportunistic( + &litudes, + &kernel, + Some(&malformed_factor), + dimension, + coefficient_offset, + lane_capacity, + 1, + weight, + &mut malformed_fallback, + ); + assert_eq!(malformed_fallback, dense); + } + + #[test] + fn real_hermitian_same_channel_microkernel_matches_dense_complex_form() { + for &(dimension, lane_count, lane_capacity) in &[(1, 1, 2), (5, 1, 3), (9, 3, 4)] { + let coefficient_offset = 2; + let weight = 0.375; + let amplitude_count = (coefficient_offset + dimension * dimension) * lane_capacity; + let amplitudes = (0..amplitude_count) + .map(|index| { + ( + ((index * 17 + 3) % 31) as f64 / 11.0 - 1.2, + ((index * 13 + 7) % 29) as f64 / 9.0 - 0.7, + ) + }) + .collect::>(); + // The loader certifies mathematical symmetry. Keeping a tiny + // representational asymmetry here verifies that pairing K_ci and + // K_ic still reproduces the previous full dense traversal. + let kernel = (0..dimension * dimension) + .map(|index| { + let row = index % dimension; + let column = index / dimension; + 0.25 * (row + column + 1) as f64 + if row < column { 3.0e-14 } else { 0.0 } + }) + .collect::>(); + let initial = (0..lane_capacity) + .map(|lane| lane as f64 * 0.125 - 0.25) + .collect::>(); + let mut expected = initial.clone(); + for column in 0..dimension { + for inner in 0..dimension { + let kernel_value = kernel[column * dimension + inner] * weight; + for row in 0..dimension { + let left = (coefficient_offset + column * dimension + row) * lane_capacity; + let right = (coefficient_offset + inner * dimension + row) * lane_capacity; + for lane in 0..lane_count { + let left_value = amplitudes[left + lane]; + let right_value = amplitudes[right + lane]; + let product_re = left_value + .0 + .mul_add(right_value.0, left_value.1 * right_value.1); + expected[lane] = kernel_value.mul_add(product_re, expected[lane]); + } + } + } + } + + let mut actual = initial; + let amplitude_pointer = amplitudes.as_ptr(); + let kernel_pointer = kernel.as_ptr(); + let reduced_pointer = actual.as_ptr(); + contract_real_hermitian_same_channel_block( + &litudes, + &kernel, + dimension, + coefficient_offset, + lane_capacity, + lane_count, + weight, + &mut actual, + ); + assert_eq!(amplitudes.as_ptr(), amplitude_pointer); + assert_eq!(kernel.as_ptr(), kernel_pointer); + assert_eq!(actual.as_ptr(), reduced_pointer); + for lane in 0..lane_count { + let scale = expected[lane].abs().max(1.0); + assert!( + (actual[lane] - expected[lane]).abs() <= 2.0e-12 * scale, + "dimension={dimension}, lane={lane}: {} != {}", + actual[lane], + expected[lane] + ); + } + assert_eq!(&actual[lane_count..], &expected[lane_count..]); + } + } + + #[test] + fn symmetric_group_reducer_matches_dense_complex_multichannel_metric_and_reuses_scratch() { + let wire = TestWire::convolution_s3_with_residual(); + let plan = decode_recurrence_color_contraction_v3(&wire.encode()).unwrap(); + let RuntimeColorContractionReducer::SymmetricGroupFourier(reducer) = + plan.runtime_reducer().unwrap() + else { + panic!("expected symmetric-group runtime reducer") + }; + let covered_group_count = reducer.channel_count() * reducer.group_order(); + assert!(reducer.channel_count() > 0); + assert!(reducer.local_group_count() > covered_group_count); + assert!(reducer.residual_entries().iter().any(|entry| { + entry.left_group_index < covered_group_count as u32 + && entry.right_group_index >= covered_group_count as u32 + })); + let lane_count = 3; + let amplitudes = (0..reducer.local_group_count() * lane_count) + .map(|index| { + let real = ((index * 17 + 5) % 29) as f64 / 7.0 - 1.5; + let imaginary = ((index * 11 + 3) % 23) as f64 / 9.0 - 0.8; + (real, imaginary) + }) + .collect::>(); + // Native lanes allocate their bounded full-tile capacity before warm + // execution. A full tile followed by an odd tail must reuse every + // backing allocation. + let mut workspace = reducer.workspace(lane_count).unwrap(); + assert_eq!(workspace.lane_capacity, lane_count); + assert_eq!( + workspace.gathered.len(), + reducer.local_group_count() * lane_count + ); + reducer + .reduce_lanes(&mut workspace, lane_count, |group, lane| { + Ok(amplitudes[group * lane_count + lane]) + }) + .unwrap(); + + let expected = RecurrenceColorContraction::symmetric_group_s3_dense_for_runtime_test( + &litudes, + lane_count, + ); + for (actual, expected) in workspace.reduced(lane_count).unwrap().iter().zip(expected) { + let scale = expected.abs().max(1.0); + assert!((actual - expected).abs() <= 2.0e-11 * scale); + } + + let gathered_pointer = workspace.gathered.as_ptr(); + let transformed_pointer = workspace.transformed.as_ptr(); + let reduced_pointer = workspace.reduced.as_ptr(); + reducer + .reduce_lanes(&mut workspace, 2, |group, lane| { + Ok(amplitudes[group * lane_count + lane]) + }) + .unwrap(); + assert_eq!(workspace.lane_capacity, lane_count); + assert_eq!(workspace.gathered.as_ptr(), gathered_pointer); + assert_eq!(workspace.transformed.as_ptr(), transformed_pointer); + assert_eq!(workspace.reduced.as_ptr(), reduced_pointer); + } + + #[test] + fn symmetric_group_runtime_drops_authenticated_all_zero_kernel_blocks() { + let mut wire = TestWire::convolution_s3_with_residual(); + for entry in &mut wire.entries[6..12] { + entry.weight_re = 0.0; + wire.exact_factors[entry.exact_factor_id as usize] = ExactComplexRational::ZERO; + } + let plan = decode_recurrence_color_contraction_v3(&wire.encode()).unwrap(); + let RuntimeColorContractionReducer::SymmetricGroupFourier(reducer) = + plan.runtime_reducer().unwrap() + else { + panic!("expected symmetric-group runtime reducer") + }; + assert_eq!(reducer.kernels().len(), 2); + assert!( + reducer + .kernels() + .iter() + .all(|kernel| kernel.left_channel_index() == kernel.right_channel_index()) + ); + assert_eq!(plan.stored_entry_count(), wire.entries.len()); + } + + #[test] + fn symmetric_group_clones_share_immutable_data_and_keep_workspaces_isolated() { + let plan = decode_recurrence_color_contraction_v3( + &TestWire::convolution_s3_with_residual().encode(), + ) + .unwrap(); + let RuntimeColorContractionReducer::SymmetricGroupFourier(reducer) = + plan.runtime_reducer().unwrap() + else { + panic!("expected symmetric-group runtime reducer") + }; + let first = reducer.clone(); + let second = reducer.clone(); + assert!(Arc::ptr_eq(&first.fft_plan, &second.fft_plan)); + assert!(Arc::ptr_eq(&first.kernels, &second.kernels)); + assert!(Arc::ptr_eq( + &first.residual_entries, + &second.residual_entries + )); + + let run = |reducer: RuntimeSymmetricGroupColorContraction, scale: f64| { + std::thread::spawn(move || { + let mut workspace = reducer.workspace(1).unwrap(); + reducer + .reduce_lanes(&mut workspace, 1, |group, _| { + Ok((scale * (group + 1) as f64, -0.25 * scale)) + }) + .unwrap(); + workspace.reduced(1).unwrap()[0] + }) + }; + let first_value = run(first, 1.0).join().unwrap(); + let second_value = run(second, 2.0).join().unwrap(); + let scale = first_value.abs().max(1.0); + assert!((second_value - 4.0 * first_value).abs() <= 2.0e-11 * scale); + } + + #[test] + fn decoder_rejects_symmetric_group_kernel_and_residual_tampering() { + let mut missing_zero = TestWire::convolution_s3_with_residual(); + missing_zero.entries.remove(8); + missing_zero.logical_entry_count -= 1; + error_contains(&missing_zero.encode(), "canonical"); + + let mut bad_symmetry = TestWire::convolution_s3_with_residual(); + bad_symmetry.entries[6].symmetry_factor = 1.0; + bad_symmetry.entries[6].exact_factor_id = bad_symmetry.exact_factors.len() as u32; + bad_symmetry.exact_factors.push(ExactComplexRational::ONE); + error_contains(&bad_symmetry.encode(), "canonical"); + + let mut non_hermitian = TestWire::convolution_s3_with_residual(); + non_hermitian.entries[3].weight_re = 9.0; + non_hermitian.entries[3].exact_factor_id = non_hermitian.exact_factors.len() as u32; + non_hermitian.exact_factors.push(ExactComplexRational::new( + ExactRational::new(9, 1).unwrap(), + ExactRational::ZERO, + )); + error_contains(&non_hermitian.encode(), "inverse Hermiticity"); + + let mut eligible_only_residual = TestWire::convolution_s3_with_residual(); + eligible_only_residual.entries[18].left_group_id = 1; + eligible_only_residual.entries[18].right_group_id = 11; + error_contains(&eligible_only_residual.encode(), "residual row"); + + let mut complex_residual = TestWire::convolution_s3_with_residual(); + complex_residual.entries[18].weight_im = 0.5; + complex_residual.entries[18].exact_factor_id = complex_residual.exact_factors.len() as u32; + complex_residual + .exact_factors + .push(ExactComplexRational::new( + ExactRational::new(1, 2).unwrap(), + ExactRational::new(1, 1).unwrap(), + )); + error_contains(&complex_residual.encode(), "complex coefficient"); + + let mut bad_residual_symmetry = TestWire::convolution_s3_with_residual(); + bad_residual_symmetry.entries[18].symmetry_factor = 1.0; + bad_residual_symmetry.entries[18].exact_factor_id = + bad_residual_symmetry.exact_factors.len() as u32; + bad_residual_symmetry + .exact_factors + .push(ExactComplexRational::new( + ExactRational::new(1, 4).unwrap(), + ExactRational::ZERO, + )); + error_contains(&bad_residual_symmetry.encode(), "symmetry factor"); + } + + #[test] + fn decoder_rejects_mixed_duplicate_out_of_bounds_and_nonfinite_data() { + let mut mixed = TestWire::expanded(); + mixed.factor_kind = 1; + mixed.factor_rank = 2; + mixed.cosets = vec![vec![0, 1, 2, 3]]; + error_contains(&mixed.encode(), "mixed"); + + let mut duplicate = TestWire::expanded(); + duplicate.entries.push(duplicate.entries[0]); + duplicate.logical_entry_count += 1; + error_contains(&duplicate.encode(), "duplicates"); + + let mut out_of_bounds = TestWire::expanded(); + out_of_bounds.entries[0].left_group_id = 2; + error_contains(&out_of_bounds.encode(), "out-of-bounds"); + + let mut nonfinite = TestWire::expanded(); + nonfinite.entries[0].weight_re = f64::NAN; + error_contains(&nonfinite.encode(), "non-finite"); + + let mut duplicate_destination = TestWire::expanded(); + duplicate_destination.destination_by_group = vec![7, 7]; + error_contains(&duplicate_destination.encode(), "duplicate Direct-Arena"); + } + + #[test] + fn decoder_rejects_inconsistent_factorization_map_and_matrix() { + let mut duplicate_coset = TestWire::repeated_k4(); + duplicate_coset.cosets[0][3] = 2; + error_contains(&duplicate_coset.encode(), "duplicate ID"); + + let mut non_invariant = TestWire::repeated_k4(); + non_invariant.entries[0].weight_re = 2.0; + non_invariant.entries[0].exact_factor_id = 1; + non_invariant.exact_factors.push(ExactComplexRational::new( + ExactRational::new(2, 1).unwrap(), + ExactRational::ZERO, + )); + error_contains(&non_invariant.encode(), "inconsistent"); + } + + #[test] + fn decoder_rejects_incomplete_or_noncanonical_sector_ownership() { + let mut missing_owner = TestWire::expanded(); + missing_owner.owner_by_sector = vec![0, 0]; + error_contains(&missing_owner.encode(), "owner sectors"); + + let mut forward_owner = TestWire::expanded(); + forward_owner.owner_by_sector = vec![1, 1]; + error_contains(&forward_owner.encode(), "invalid canonical owner"); + } + + #[test] + fn decoder_rejects_cross_component_expanded_entries_and_trailing_bytes() { + let mut cross_component = TestWire::expanded(); + cross_component.sector_count = 1; + cross_component.component_count = 2; + cross_component.sector_by_group = vec![0, 0]; + cross_component.component_by_group = vec![0, 1]; + cross_component.owner_by_sector = vec![0]; + error_contains(&cross_component.encode(), "different components"); + + let mut trailing = TestWire::expanded().encode(); + trailing.push(0); + error_contains(&trailing, "fixed-width sections"); + } +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/recurrence/construct.rs b/pyamplicol/rust/crates/rusticol-core/src/recurrence/construct.rs new file mode 100644 index 0000000000..bd3b309662 --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/recurrence/construct.rs @@ -0,0 +1,16738 @@ +// SPDX-License-Identifier: 0BSD + +//! Compact model-generic recurrence construction. + +use std::cell::{Cell, RefCell}; +use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet, VecDeque}; +use std::iter::Peekable; +use std::rc::Rc; +use std::time::{Duration, Instant}; + +use sha2::{Digest, Sha256}; + +use super::contact_orbit_owner::{ + PreparedContactOrbitTransition, prepare_contact_orbit_transition, + selected_contact_orbit_owner_tokens, +}; +use super::fermion_ordering::{ + FermionOrderingContext, authenticated_source_requires_exterior_sign, fermion_ordering_factor, +}; +use super::layout::RuntimeSourceVariantBinding; +use super::process::{ + FermionPairingEndpointPairRow, FermionPairingRuleRow, OwnedRecurrenceProcessInput, + ProcessLCSectorKind, ProcessPhysicalLCSectorRow, ProcessSourceStateRow, + ValidatedFermionPairingCatalog, +}; +use super::program::closure_candidate_identity_digest_v1; +use super::template::{ + ClosureRow, ColorContractionRow, CurrentOrientation, EvaluatorCallableKind, + EvaluatorContractKind, LCColorTransitionWitnessRow, OutputFactorSource, + OwnedRecurrenceTemplateInput, ParticleStatistics, QuantumFlowRow, RuntimeHelicityContractRow, + RuntimeHelicityVariantRow, SourceRow, TransitionRow, +}; +use super::{ + AuthenticatedRecurrenceBuilderInput, CanonicalMomentumLinearForm, CheckedTableRange, + ClosureCandidateDomainCertificateV1, ClosureExecutionProofGroupV2, ClosureProofContributionV2, + ClosureProofMetadataV2, ClosureTargetDomainCertificateV1, ContributionKey, CurrentCoreKey, + CurrentHelicityIdentity, CurrentSourceBinding, DYNAMIC_UNION_SOURCE_SPIN_STATE_CLASS, + DynamicLCColorState, DynamicLCColorStateId, DynamicLCColorStateInterner, ExactComplexRational, + ExactRational, LCColorComponent, LCColorComponentKind, LCColorComponentOperation, + LCColorComponentRole, LCColorParentPort, LCColorPortWiring, LCColorSourceSeed, + LCColorSourceSeedOperation, LCColorTransitionWitness, LCColorWitnessTermId, MomentumTerm, + RecurrenceAmplitudeDestination, RecurrenceClosureTerm, RecurrenceContribution, + RecurrenceCurrent, RecurrenceFinalization, RecurrenceNodeKind, RecurrenceProgram, + RecurrenceReplayTarget, RecurrenceResolvedHelicity, RecurrenceStrategy, + ReflectionCertificateV1, SemanticDigest, SourceStateAssignment, + ThreeLineTraversalCertificateV1, ThreeLineTraversalKindV1, closure_component_factor_digest_v2, + closure_selector_domain_digest_v2, +}; +use crate::{RusticolError, RusticolResult}; + +const MISSING_U32: u32 = u32::MAX; +const PROGRESS_PAIR_INTERVAL: usize = 16_384; +const PROGRESS_TIME_INTERVAL: Duration = Duration::from_millis(250); +const PURE_MASSLESS_ADJOINT_HELICITY_SUPPORT_ROLE: &str = + "helicity-support:pure-massless-adjoint-tree-v1"; +const GLOBAL_HELICITY_FLIP_EQUIVALENCE_ROLE: &str = "helicity-equivalence:global-flip-v1"; +const CYCLIC_TRACE_CLOSURE_ANCHOR_ALGORITHM: &str = "canonical-lc-closure-anchor-v4"; +const REFLECTION_PROOF_ALGORITHM_ID: u32 = 1; +const THREE_LINE_DIRECT_CERTIFICATE_ID: u32 = 0; +const THREE_LINE_PARTNER_CERTIFICATE_ID: u32 = 1; + +#[cfg(any(test, feature = "on-the-fly-test-support"))] +thread_local! { + static ESTABLISHED_PAIRING_OWNER_OBSERVATION_ACTIVE: Cell = const { Cell::new(false) }; + static ESTABLISHED_PAIRING_OWNER_OBSERVATION: + RefCell>>>> = const { RefCell::new(None) }; +} + +#[cfg(any(test, feature = "on-the-fly-test-support"))] +#[allow(dead_code)] +pub(crate) fn begin_established_pairing_owner_observation() { + ESTABLISHED_PAIRING_OWNER_OBSERVATION_ACTIVE.with(|active| active.set(true)); + ESTABLISHED_PAIRING_OWNER_OBSERVATION.with(|observation| { + *observation.borrow_mut() = None; + }); +} + +#[cfg(any(test, feature = "on-the-fly-test-support"))] +#[allow(dead_code)] +pub(crate) fn take_established_pairing_owner_observation() -> RusticolResult>> { + ESTABLISHED_PAIRING_OWNER_OBSERVATION_ACTIVE.with(|active| active.set(false)); + ESTABLISHED_PAIRING_OWNER_OBSERVATION.with(|observation| { + observation.borrow_mut().take().ok_or_else(|| { + invalid("established builder did not publish its retained pairing owner") + })? + }) +} + +#[cfg(any(test, feature = "on-the-fly-test-support"))] +fn observe_established_pairing_owner( + selected_domain: Option<&(u32, Box<[SourceStateAssignment]>)>, + projection: Option<&PendingColorProjection>, + pending_closures: &BTreeMap, +) { + if !ESTABLISHED_PAIRING_OWNER_OBSERVATION_ACTIVE.with(Cell::get) { + return; + } + let result = (|| { + let (selected_sector, selected_source_states) = selected_domain.ok_or_else(|| { + invalid("established pairing-owner observation has no selected closure domain") + })?; + let owner_for_group = |group: &PendingClosureGroup| -> RusticolResult> { + let rule_ids = group + .contributions + .iter() + .flat_map(|contribution| contribution.pairing_certificate_ids.iter().copied()) + .collect::>(); + match rule_ids.len() { + 0 => Ok(None), + 1 => Ok(rule_ids.iter().next().copied()), + _ => Err(invalid(format!( + "established retained closure has {} pairing owners, expected exactly one", + rule_ids.len(), + ))), + } + }; + let mut owners = Vec::new(); + if let Some(projection) = projection { + for projected in projection.closures.values().filter(|projected| { + projected.identity.target_sector_id == *selected_sector + && projected.identity.complete_source_states.as_ref() + == selected_source_states.as_ref() + }) { + owners.push(owner_for_group(&projected.representative_group)?); + } + } else { + for group in pending_closures + .iter() + .filter(|(key, group)| { + key.target_sector_id == *selected_sector + && key.complete_source_states.as_ref() == selected_source_states.as_ref() + && !group.exact_factor.is_zero() + }) + .map(|(_, group)| group) + { + owners.push(owner_for_group(group)?); + } + } + if owners.is_empty() { + return Err(invalid( + "established selected closure domain has no retained pairing owners", + )); + } + Ok(owners) + })(); + ESTABLISHED_PAIRING_OWNER_OBSERVATION_ACTIVE.with(|active| active.set(false)); + ESTABLISHED_PAIRING_OWNER_OBSERVATION.with(|observation| { + *observation.borrow_mut() = Some(result); + }); +} + +fn invalid(message: impl Into) -> RusticolError { + RusticolError::invalid_argument(message) +} + +fn hash_exact_factor(hash: &mut Sha256, factor: ExactComplexRational) { + for rational in [factor.real(), factor.imag()] { + hash.update(rational.numerator().to_le_bytes()); + hash.update(rational.denominator().to_le_bytes()); + } +} + +fn semantic_digest_from_u32_fields( + domain: &[u8], + values: impl IntoIterator, +) -> RusticolResult { + let mut hash = Sha256::new(); + hash.update(domain); + for value in values { + hash.update(value.to_le_bytes()); + } + SemanticDigest::new(hash.finalize().into()) +} + +fn hash_digest_sequence(hash: &mut Sha256, values: &[SemanticDigest]) -> RusticolResult<()> { + hash.update( + u64::try_from(values.len()) + .map_err(|_| invalid("reflection proof digest count exceeds u64"))? + .to_le_bytes(), + ); + for value in values { + hash.update(value.as_bytes()); + } + Ok(()) +} + +pub(super) fn dynamic_color_identity_digest( + color: &DynamicLCColorState, +) -> RusticolResult { + let mut hash = Sha256::new(); + hash.update(b"pyamplicol-recurrence-dynamic-lc-color-identity-v1\0"); + hash.update(color.output_color_shape_id().to_le_bytes()); + hash.update( + color + .active_component_index() + .unwrap_or(u32::MAX) + .to_le_bytes(), + ); + hash.update( + u64::try_from(color.components().len()) + .map_err(|_| invalid("dynamic LC color component count exceeds u64"))? + .to_le_bytes(), + ); + for component in color.components() { + hash.update([component.kind() as u8]); + hash.update( + u64::try_from(component.source_slots().len()) + .map_err(|_| invalid("dynamic LC color word length exceeds u64"))? + .to_le_bytes(), + ); + for source_slot in component.source_slots() { + hash.update(source_slot.to_le_bytes()); + } + } + hash.update( + u64::try_from(color.result_port_bindings().len()) + .map_err(|_| invalid("dynamic LC color port count exceeds u64"))? + .to_le_bytes(), + ); + for binding in color.result_port_bindings() { + hash.update(binding.component_index().to_le_bytes()); + hash.update([binding.endpoint() as u8]); + } + SemanticDigest::new(hash.finalize().into()) +} + +fn source_reflection_proof_root( + source_slot: u32, + source_seed_proof: SemanticDigest, + result_color_identity: SemanticDigest, +) -> RusticolResult { + let mut hash = Sha256::new(); + hash.update(b"pyamplicol-recurrence-source-color-reflection-root-v1\0"); + hash.update(source_slot.to_le_bytes()); + hash.update(source_seed_proof.as_bytes()); + hash.update(result_color_identity.as_bytes()); + SemanticDigest::new(hash.finalize().into()) +} + +fn transition_reflection_proof_digest( + phase: ExactComplexRational, + semantic_digests: &[SemanticDigest], + witness_digests: &[SemanticDigest], +) -> RusticolResult { + let mut hash = Sha256::new(); + hash.update(b"pyamplicol-recurrence-transition-reflection-proof-v1\0"); + hash_exact_factor(&mut hash, phase); + hash_digest_sequence(&mut hash, semantic_digests)?; + hash_digest_sequence(&mut hash, witness_digests)?; + SemanticDigest::new(hash.finalize().into()) +} + +fn current_reflection_proof_digest( + phase: ExactComplexRational, + lineage_roots: &[SemanticDigest], + result_color_identity: SemanticDigest, +) -> RusticolResult { + let mut hash = Sha256::new(); + hash.update(b"pyamplicol-recurrence-current-reflection-proof-v1\0"); + hash_exact_factor(&mut hash, phase); + hash_digest_sequence(&mut hash, lineage_roots)?; + hash.update(result_color_identity.as_bytes()); + SemanticDigest::new(hash.finalize().into()) +} + +fn pending_reflection_certificate_digest( + canonical_old_current_id: u32, + reflected_old_current_id: u32, + canonical: &CurrentReflectionProof, + reflected: &CurrentReflectionProof, + source_permutation: &[u32], + fixed_point: bool, + orbit_size: u32, +) -> RusticolResult { + pending_reflection_certificate_fields_digest( + canonical_old_current_id, + reflected_old_current_id, + canonical.result_color_identity(), + reflected.result_color_identity(), + canonical.phase(), + reflected.phase(), + canonical.proof_digest(), + reflected.proof_digest(), + source_permutation, + fixed_point, + orbit_size, + ) +} + +#[allow(clippy::too_many_arguments)] +fn pending_reflection_certificate_fields_digest( + canonical_old_current_id: u32, + reflected_old_current_id: u32, + canonical_color_identity: SemanticDigest, + reflected_color_identity: SemanticDigest, + canonical_phase: ExactComplexRational, + reflected_phase: ExactComplexRational, + canonical_lineage_digest: SemanticDigest, + reflected_lineage_digest: SemanticDigest, + source_permutation: &[u32], + fixed_point: bool, + orbit_size: u32, +) -> RusticolResult { + let mut hash = Sha256::new(); + hash.update(b"pyamplicol-recurrence-pending-reflection-certificate-v1\0"); + hash.update(canonical_old_current_id.to_le_bytes()); + hash.update(reflected_old_current_id.to_le_bytes()); + hash.update(canonical_color_identity.as_bytes()); + hash.update(reflected_color_identity.as_bytes()); + hash_exact_factor(&mut hash, canonical_phase); + hash_exact_factor(&mut hash, reflected_phase); + hash.update(canonical_lineage_digest.as_bytes()); + hash.update(reflected_lineage_digest.as_bytes()); + hash.update( + u64::try_from(source_permutation.len()) + .map_err(|_| invalid("reflection source permutation length exceeds u64"))? + .to_le_bytes(), + ); + for source_slot in source_permutation { + hash.update(source_slot.to_le_bytes()); + } + hash.update([u8::from(fixed_point)]); + hash.update(orbit_size.to_le_bytes()); + SemanticDigest::new(hash.finalize().into()) +} + +/// One rate-limited snapshot of compact recurrence construction. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct RecurrenceBuildProgress { + pub phase: &'static str, + pub phase_index: usize, + pub phase_total: usize, + pub stage_index: Option, + pub stage_total: usize, + pub subset_size: Option, + pub candidate_parent_pair_count: usize, + pub candidate_parent_pair_total: Option, + pub current_count: usize, + pub contribution_count: usize, + pub dynamic_color_state_count: usize, + pub color_target_prune_count: usize, +} + +/// Construction-only recurrence-generation telemetry. +/// +/// This diagnostic record is deliberately excluded from recurrence programs, +/// runtime plans, persisted payloads, and their semantic digests. +#[doc(hidden)] +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub struct RecurrenceGenerationTelemetry { + pub transition_catalog_nanoseconds: u64, + pub structural_feasibility_nanoseconds: u64, + pub color_target_index_nanoseconds: u64, + pub structural_demand_nanoseconds: u64, + pub support_indexing_nanoseconds: u64, + pub candidate_processing_nanoseconds: u64, + pub closure_processing_nanoseconds: u64, + pub canonical_emission_nanoseconds: u64, + pub support_bucket_count: usize, + pub support_bucket_probe_count: usize, + pub support_bucket_cache_hit_count: usize, + pub support_bucket_cache_miss_count: usize, + pub candidate_parent_pair_theoretical_count: usize, + pub candidate_parent_pair_visited_count: usize, + pub structural_feasible_support_count: usize, + pub structural_decomposition_count: usize, + pub structural_forward_transition_probe_count: usize, + pub structural_demand_support_count: usize, + pub structural_demand_state_count: usize, + pub structural_reject_count: usize, + pub transition_index_hit_count: usize, + pub transition_index_miss_count: usize, + pub transition_candidate_count: usize, + pub quantum_match_count: usize, + pub coupling_match_count: usize, + pub transition_accept_count: usize, + pub color_shape_match_count: usize, + pub color_result_count: usize, + pub color_target_accept_count: usize, + pub color_target_reject_count: usize, + pub color_acceptance_cache_hit_count: usize, + pub color_acceptance_cache_miss_count: usize, + pub color_fragment_bucket_count: usize, + pub color_fragment_hash_lookup_count: usize, + pub color_posting_incidence_count: usize, + pub color_sparse_posting_bucket_count: usize, + pub color_dense_posting_bucket_count: usize, + pub color_sparse_posting_bytes: usize, + pub color_dense_posting_bytes: usize, + pub accepted_parent_key_clone_count: usize, + pub current_key_lookup_count: usize, + pub current_key_hit_count: usize, + pub current_insert_count: usize, + pub current_key_clone_count: usize, + pub indexed_hash_lookup_count: usize, + pub contribution_attempt_count: usize, + pub contribution_insert_count: usize, + pub contribution_merge_count: usize, + pub closure_candidate_theoretical_count: usize, + pub closure_candidate_count: usize, + pub closure_state_match_count: usize, + pub closure_support_lookup_count: usize, + /// Exact number of successful-path color-witness closure evaluations, + /// including repeated component forests. + pub closure_color_attempt_count: usize, + pub closure_group_count: usize, + pub closure_proof_contribution_count: usize, + pub constructed_current_count: usize, + pub constructed_contribution_count: usize, + pub constructed_interaction_count: usize, + pub constructed_dynamic_color_state_count: usize, + pub emitted_current_count: usize, + pub emitted_contribution_count: usize, + pub emitted_interaction_count: usize, + pub emitted_finalization_count: usize, + pub emitted_closure_count: usize, +} + +impl RecurrenceBuildProgress { + #[allow(clippy::too_many_arguments)] + fn snapshot( + phase: &'static str, + phase_index: usize, + phase_total: usize, + stage_index: Option, + stage_total: usize, + subset_size: Option, + candidate_parent_pair_count: usize, + candidate_parent_pair_total: Option, + current_count: usize, + contribution_count: usize, + dynamic_color_state_count: usize, + color_target_prune_count: usize, + ) -> Self { + Self { + phase, + phase_index, + phase_total, + stage_index, + stage_total, + subset_size, + candidate_parent_pair_count, + candidate_parent_pair_total, + current_count, + contribution_count, + dynamic_color_state_count, + color_target_prune_count, + } + } +} + +#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] +struct PendingContributionKey { + parent_current_ids: Box<[u32]>, + key: ContributionKey, +} + +#[derive(Clone, Debug)] +struct PendingCurrent { + key: CurrentCoreKey, + source_exact_factor: Option, + contributions: BTreeMap, + realized_pairing_rule_ids: BTreeSet, + reflection: CurrentReflection, + reflection_certificate_id: Option, +} + +/// Construction-only exact current lookup. +/// +/// Canonical order is owned exclusively by the `currents` vector and ordered +/// contribution maps. This index is only cloned, queried, inserted into, and +/// pruned; its randomized iteration order is never observed. +type TransientCurrentIdIndex = HashMap; + +/// Exact runtime-value identity after erasing only dynamic LC colour. +/// +/// The physical-sector domain is part of the identity. Without it, folding +/// two colour fragments from disjoint selector domains would make every +/// contribution of either fragment live in both domains. +#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] +struct ProjectedCurrentIdentity { + color_erased_key: CurrentCoreKey, + selector_sector_ids: Box<[u32]>, + source_builder_id: Option, +} + +/// Exact contribution identity after mapping parents to projected values. +/// +/// `color_witness_term_id` is deliberately absent. Every remaining runtime +/// field and the exact coefficient participates in equality. +#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] +struct ProjectedContributionIdentity { + destination_projection_id: u32, + transition_template_id: u32, + parent_projection_ids: Box<[u32]>, + parent_state_template_ids: Box<[u32]>, + parent_momenta: Box<[CanonicalMomentumLinearForm]>, + result_state_template_id: u32, + quantum_flow_witness_id: u32, + runtime_coupling_binding_digest: SemanticDigest, + output_projection_id: u32, + exact_factor: ExactComplexRational, +} + +#[derive(Clone, Debug)] +struct PendingProjectedContribution { + identity: ProjectedContributionIdentity, + representative_destination_builder_id: u32, + representative_key: ContributionKey, + destination_builder_ids: BTreeSet, + builder_parent_tuples: BTreeSet>, +} + +#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] +struct ProjectedClosureIdentity { + target_sector_id: u32, + complete_source_states: Box<[SourceStateAssignment]>, + closure_template_id: u32, + quantum_flow_template_id: Option, + parent_projection_ids: Box<[u32]>, + exact_factor: ExactComplexRational, +} + +#[derive(Clone, Debug)] +struct PendingProjectedClosure { + identity: ProjectedClosureIdentity, + representative_key: PendingClosureKey, + representative_group: PendingClosureGroup, + builder_parent_tuples: BTreeSet>, +} + +#[derive(Clone, Debug)] +struct PendingColorProjection { + old_to_projection: BTreeMap, + projection_members: Vec>, + projection_sector_ids: Vec>, + contributions: BTreeMap, + closures: BTreeMap, +} + +struct MaterializedPendingRows { + remap: BTreeMap, + currents: Vec, + contributions: Vec, + finalizations: Vec, +} + +fn materialize_live_pending_rows( + pending: &[PendingCurrent], + live: &BTreeSet, +) -> RusticolResult { + let remap = live + .iter() + .copied() + .enumerate() + .map(|(new, old)| { + u32::try_from(new) + .map(|new| (old, new)) + .map_err(|_| invalid("live recurrence current count exceeds u32")) + }) + .collect::>>()?; + let mut currents = Vec::with_capacity(live.len()); + let mut contributions = Vec::new(); + let mut finalizations = Vec::new(); + for old_id in live.iter().copied() { + let pending_current = &pending[old_id as usize]; + let start = u64::try_from(contributions.len()) + .map_err(|_| invalid("recurrence contribution count exceeds u64"))?; + for (pending_key, factor) in &pending_current.contributions { + if factor.is_zero() { + continue; + } + let parent_ids = pending_key + .parent_current_ids + .iter() + .map(|id| { + remap + .get(id) + .copied() + .ok_or_else(|| invalid("live parent is absent")) + }) + .collect::>>()?; + let old_key = &pending_key.key; + let key = ContributionKey::new( + old_key.transition_template_id(), + parent_ids.clone(), + old_key.parent_state_template_ids().to_vec(), + old_key.parent_momenta().to_vec(), + old_key.result_state_template_id(), + old_key.quantum_flow_witness_id(), + old_key.color_witness_term_id(), + old_key.runtime_coupling_binding_digest(), + old_key.output_projection_id(), + )?; + contributions.push(RecurrenceContribution::new( + u32::try_from(contributions.len()) + .map_err(|_| invalid("recurrence contribution count exceeds u32"))?, + remap[&old_id], + parent_ids, + key, + *factor, + )?); + } + let count = u64::try_from(contributions.len()) + .map_err(|_| invalid("recurrence contribution count exceeds u64"))? + .checked_sub(start) + .ok_or_else(|| invalid("recurrence contribution range underflows"))?; + let finalization_id = if pending_current.key.node_kind() == RecurrenceNodeKind::Current { + let id = u32::try_from(finalizations.len()) + .map_err(|_| invalid("recurrence finalization count exceeds u32"))?; + finalizations.push(RecurrenceFinalization::new( + id, + remap[&old_id], + pending_current.key.propagator_template_id(), + ExactComplexRational::ONE, + )?); + Some(id) + } else { + None + }; + currents.push(RecurrenceCurrent::new( + remap[&old_id], + pending_current.key.clone(), + pending_current.source_exact_factor, + CheckedTableRange::new(start, count), + finalization_id, + )?); + } + Ok(MaterializedPendingRows { + remap, + currents, + contributions, + finalizations, + }) +} + +fn materialize_projected_pending_rows( + pending: &[PendingCurrent], + projection: &PendingColorProjection, +) -> RusticolResult { + let mut currents = Vec::with_capacity(projection.projection_members.len()); + let mut contributions = Vec::with_capacity(projection.contributions.len()); + let mut finalizations = Vec::new(); + for (projection_index, members) in projection.projection_members.iter().enumerate() { + let projection_id = u32::try_from(projection_index) + .map_err(|_| invalid("projected recurrence current count exceeds u32"))?; + let representative_id = *members + .first() + .ok_or_else(|| invalid("projected recurrence current class is empty"))?; + let representative = pending + .get(representative_id as usize) + .ok_or_else(|| invalid("projected recurrence representative is absent"))?; + let start = u64::try_from(contributions.len()) + .map_err(|_| invalid("projected recurrence contribution count exceeds u64"))?; + for projected in projection + .contributions + .values() + .filter(|row| row.identity.destination_projection_id == projection_id) + { + if !projected.destination_builder_ids.iter().all(|builder_id| { + projection.old_to_projection.get(builder_id) == Some(&projection_id) + }) { + return Err(invalid( + "projected contribution destinations cross projection classes", + )); + } + let parent_ids = projected.identity.parent_projection_ids.to_vec(); + let witness = &projected.representative_key; + let key = ContributionKey::new( + projected.identity.transition_template_id, + parent_ids.clone(), + projected.identity.parent_state_template_ids.to_vec(), + projected.identity.parent_momenta.to_vec(), + projected.identity.result_state_template_id, + projected.identity.quantum_flow_witness_id, + witness.color_witness_term_id(), + projected.identity.runtime_coupling_binding_digest, + projected.identity.output_projection_id, + )?; + contributions.push(RecurrenceContribution::new( + u32::try_from(contributions.len()) + .map_err(|_| invalid("projected recurrence contribution count exceeds u32"))?, + projection_id, + parent_ids, + key, + projected.identity.exact_factor, + )?); + } + let count = u64::try_from(contributions.len()) + .map_err(|_| invalid("projected recurrence contribution count exceeds u64"))? + .checked_sub(start) + .ok_or_else(|| invalid("projected recurrence contribution range underflows"))?; + let finalization_id = if representative.key.node_kind() == RecurrenceNodeKind::Current { + let id = u32::try_from(finalizations.len()) + .map_err(|_| invalid("projected recurrence finalization count exceeds u32"))?; + finalizations.push(RecurrenceFinalization::new( + id, + projection_id, + representative.key.propagator_template_id(), + ExactComplexRational::ONE, + )?); + Some(id) + } else { + None + }; + currents.push(RecurrenceCurrent::new( + projection_id, + representative.key.clone(), + representative.source_exact_factor, + CheckedTableRange::new(start, count), + finalization_id, + )?); + } + Ok(MaterializedPendingRows { + remap: projection.old_to_projection.clone(), + currents, + contributions, + finalizations, + }) +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub(super) struct CurrentReflectionProof { + phase: ExactComplexRational, + lineage_roots: Box<[SemanticDigest]>, + result_color_identity: SemanticDigest, + proof_digest: SemanticDigest, +} + +impl CurrentReflectionProof { + pub(super) fn new( + phase: ExactComplexRational, + lineage_roots: impl IntoIterator, + result_color_identity: SemanticDigest, + ) -> RusticolResult { + if phase.is_zero() { + return Err(invalid("current reflection proof has zero phase")); + } + let mut lineage_roots = lineage_roots.into_iter().collect::>(); + lineage_roots.sort_unstable(); + lineage_roots.dedup(); + if lineage_roots.is_empty() { + return Err(invalid("current reflection proof has empty lineage")); + } + let proof_digest = + current_reflection_proof_digest(phase, &lineage_roots, result_color_identity)?; + Ok(Self { + phase, + lineage_roots: lineage_roots.into_boxed_slice(), + result_color_identity, + proof_digest, + }) + } + + pub(super) const fn phase(&self) -> ExactComplexRational { + self.phase + } + + pub(super) fn lineage_roots(&self) -> &[SemanticDigest] { + &self.lineage_roots + } + + pub(super) const fn result_color_identity(&self) -> SemanticDigest { + self.result_color_identity + } + + pub(super) const fn proof_digest(&self) -> SemanticDigest { + self.proof_digest + } + + fn merged_with(&self, other: &Self) -> RusticolResult> { + if self.phase != other.phase || self.result_color_identity != other.result_color_identity { + return Ok(None); + } + Self::new( + self.phase, + self.lineage_roots + .iter() + .copied() + .chain(other.lineage_roots.iter().copied()), + self.result_color_identity, + ) + .map(Some) + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub(super) enum CurrentReflection { + Unavailable, + Proven(CurrentReflectionProof), +} + +impl CurrentReflection { + pub(super) fn phase(&self) -> Option { + match self { + Self::Unavailable => None, + Self::Proven(proof) => Some(proof.phase()), + } + } + + pub(super) fn proof(&self) -> Option<&CurrentReflectionProof> { + match self { + Self::Unavailable => None, + Self::Proven(proof) => Some(proof), + } + } + + pub(super) fn include( + &mut self, + candidate: Option, + ) -> RusticolResult<()> { + let merged = match (&*self, candidate) { + (Self::Proven(existing), Some(candidate)) => existing.merged_with(&candidate)?, + _ => None, + }; + match merged { + Some(proof) => *self = Self::Proven(proof), + None => *self = Self::Unavailable, + } + Ok(()) + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub(super) struct TransitionReflectionProof { + phase: ExactComplexRational, + semantic_digests: Box<[SemanticDigest]>, + witness_digests: Box<[SemanticDigest]>, + proof_digest: SemanticDigest, +} + +impl TransitionReflectionProof { + fn new( + phase: ExactComplexRational, + semantic_digests: impl IntoIterator, + witness_digests: impl IntoIterator, + ) -> RusticolResult { + if phase.is_zero() { + return Err(invalid("transition reflection proof has zero phase")); + } + let mut semantic_digests = semantic_digests.into_iter().collect::>(); + semantic_digests.sort_unstable(); + semantic_digests.dedup(); + let mut witness_digests = witness_digests.into_iter().collect::>(); + witness_digests.sort_unstable(); + witness_digests.dedup(); + if semantic_digests.is_empty() || witness_digests.is_empty() { + return Err(invalid( + "transition reflection proof requires semantic and witness digests", + )); + } + let proof_digest = + transition_reflection_proof_digest(phase, &semantic_digests, &witness_digests)?; + Ok(Self { + phase, + semantic_digests: semantic_digests.into_boxed_slice(), + witness_digests: witness_digests.into_boxed_slice(), + proof_digest, + }) + } + + pub(super) const fn phase(&self) -> ExactComplexRational { + self.phase + } + + #[cfg(feature = "on-the-fly-test-support")] + pub(super) const fn proof_digest(&self) -> SemanticDigest { + self.proof_digest + } + + fn lineage_roots(&self) -> impl Iterator + '_ { + self.semantic_digests + .iter() + .chain(self.witness_digests.iter()) + .copied() + .chain(std::iter::once(self.proof_digest)) + } + + fn merged_with(&self, other: &Self) -> RusticolResult> { + if self.phase != other.phase { + return Ok(None); + } + Self::new( + self.phase, + self.semantic_digests + .iter() + .copied() + .chain(other.semantic_digests.iter().copied()), + self.witness_digests + .iter() + .copied() + .chain(other.witness_digests.iter().copied()), + ) + .map(Some) + } +} + +/// Cold proof record emitted before folded stage-local current IDs are compacted. +/// +/// This deliberately remains local until the orchestrator-owned program proof +/// API accepts reciprocal reflection orbits. +#[derive(Clone, Debug, Eq, PartialEq)] +pub(super) struct PendingReflectionCertificate { + id: u32, + canonical_old_current_id: u32, + reflected_old_current_id: u32, + canonical_color_identity: SemanticDigest, + reflected_color_identity: SemanticDigest, + canonical_phase: ExactComplexRational, + reflected_phase: ExactComplexRational, + canonical_lineage_digest: SemanticDigest, + reflected_lineage_digest: SemanticDigest, + source_permutation: Box<[u32]>, + fixed_point: bool, + orbit_size: u32, + proof_digest: SemanticDigest, +} + +impl PendingReflectionCertificate { + // The pair constructor authenticates both orbit members and both color + // witnesses explicitly; retaining the parallel arguments makes swaps visible. + #[allow(clippy::too_many_arguments)] + pub(super) fn reciprocal_pair( + id: u32, + canonical_old_current_id: u32, + reflected_old_current_id: u32, + canonical: &CurrentReflectionProof, + reflected: &CurrentReflectionProof, + canonical_color: &DynamicLCColorState, + reflected_color: &DynamicLCColorState, + source_count: usize, + ) -> RusticolResult { + if canonical_old_current_id == reflected_old_current_id + || canonical.result_color_identity() == reflected.result_color_identity() + { + return Err(invalid( + "folded reflection certificate requires two distinct orbit members", + )); + } + if canonical.phase().checked_mul(reflected.phase())? != ExactComplexRational::ONE { + return Err(invalid( + "folded reflection certificate phases are not reciprocal", + )); + } + let source_permutation = + reflection_source_permutation(canonical_color, reflected_color, source_count)?; + let proof_digest = pending_reflection_certificate_digest( + canonical_old_current_id, + reflected_old_current_id, + canonical, + reflected, + &source_permutation, + false, + 2, + )?; + Ok(Self { + id, + canonical_old_current_id, + reflected_old_current_id, + canonical_color_identity: canonical.result_color_identity(), + reflected_color_identity: reflected.result_color_identity(), + canonical_phase: canonical.phase(), + reflected_phase: reflected.phase(), + canonical_lineage_digest: canonical.proof_digest(), + reflected_lineage_digest: reflected.proof_digest(), + source_permutation: source_permutation.into_boxed_slice(), + fixed_point: false, + orbit_size: 2, + proof_digest, + }) + } + + pub(super) const fn canonical_color_identity(&self) -> SemanticDigest { + self.canonical_color_identity + } + + pub(super) const fn canonical_phase(&self) -> ExactComplexRational { + self.canonical_phase + } + + pub(super) const fn canonical_lineage_digest(&self) -> SemanticDigest { + self.canonical_lineage_digest + } + + pub(super) fn source_permutation(&self) -> &[u32] { + &self.source_permutation + } + + pub(super) const fn is_reciprocal_two_cycle(&self) -> bool { + !self.fixed_point && self.orbit_size == 2 + } +} + +fn reflection_source_permutation( + canonical_color: &DynamicLCColorState, + reflected_color: &DynamicLCColorState, + source_count: usize, +) -> RusticolResult> { + let canonical = canonical_color + .pure_adjoint_word() + .ok_or_else(|| invalid("canonical reflection color has no adjoint word"))?; + let reflected = reflected_color + .pure_adjoint_word() + .ok_or_else(|| invalid("reflected reflection color has no adjoint word"))?; + if canonical.len() != reflected.len() { + return Err(invalid( + "reflection color words have inconsistent source counts", + )); + } + let mut permutation = (0..source_count) + .map(|slot| u32::try_from(slot).map_err(|_| invalid("reflection source count exceeds u32"))) + .collect::>>()?; + for (source, target) in canonical.iter().copied().zip(reflected.iter().copied()) { + let destination = permutation + .get_mut(source as usize) + .ok_or_else(|| invalid("reflection word references an absent source slot"))?; + *destination = target; + } + let mut ordered = permutation.clone(); + ordered.sort_unstable(); + if ordered + != (0..source_count) + .map(|slot| u32::try_from(slot).expect("source count checked above")) + .collect::>() + { + return Err(invalid( + "reflection source mapping is not a complete permutation", + )); + } + Ok(permutation) +} + +fn current_color_for_index<'a>( + color_states: &'a DynamicLCColorStateInterner, + currents: &[PendingCurrent], + current_index: usize, +) -> RusticolResult<&'a DynamicLCColorState> { + let current = currents + .get(current_index) + .ok_or_else(|| invalid("reflection current index is absent"))?; + color_states + .get(current.key.dynamic_lc_color_state_id()) + .ok_or_else(|| invalid("reflection current color disappeared")) +} + +fn validate_pending_reflection_certificates( + certificates: &[PendingReflectionCertificate], +) -> RusticolResult<()> { + for (index, certificate) in certificates.iter().enumerate() { + if certificate.id != index as u32 { + return Err(invalid(format!( + "pending reflection certificate row {index} has non-dense id {}", + certificate.id + ))); + } + if certificate.canonical_old_current_id == certificate.reflected_old_current_id + || certificate.canonical_color_identity == certificate.reflected_color_identity + { + return Err(invalid(format!( + "pending reflection certificate {index} does not identify two orbit members" + ))); + } + if certificate.fixed_point || certificate.orbit_size != 2 { + return Err(invalid(format!( + "pending folded reflection certificate {index} is not a reciprocal two-cycle" + ))); + } + if certificate + .canonical_phase + .checked_mul(certificate.reflected_phase)? + != ExactComplexRational::ONE + { + return Err(invalid(format!( + "pending reflection certificate {index} phases are not reciprocal" + ))); + } + let expected = pending_reflection_certificate_fields_digest( + certificate.canonical_old_current_id, + certificate.reflected_old_current_id, + certificate.canonical_color_identity, + certificate.reflected_color_identity, + certificate.canonical_phase, + certificate.reflected_phase, + certificate.canonical_lineage_digest, + certificate.reflected_lineage_digest, + &certificate.source_permutation, + certificate.fixed_point, + certificate.orbit_size, + )?; + if expected != certificate.proof_digest { + return Err(invalid(format!( + "pending reflection certificate {index} proof digest is stale" + ))); + } + } + Ok(()) +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum HelicitySupportRule { + None, + PureMasslessAdjointTree, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum GlobalHelicityFlipRule { + None, + Proven, +} + +#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] +struct PendingClosureKey { + target_sector_id: u32, + complete_source_states: Box<[SourceStateAssignment]>, + closure_template_id: u32, + quantum_flow_template_id: Option, + parent_current_ids: Box<[u32]>, +} + +#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] +struct PendingThreeLineTraversalCertificate { + sector_id: u32, + kind: u8, + sink_block_ordinal: u32, + reference_block_order: Box<[u32]>, + witness_block_order: Box<[u32]>, + block_permutation: Box<[u32]>, + reference_source_order: Box<[u32]>, + witness_source_order: Box<[u32]>, + source_position_permutation: Box<[u32]>, + closure_anchor_source_slot: u32, + pairing_rule_id: u32, + proof_digest: SemanticDigest, +} + +/// One compiler/model-certified closure witness before equal runtime rows are +/// combined. These records are cold proof data and never enter the numeric +/// Direct-Arena loop. +#[derive(Clone, Debug, Eq, PartialEq)] +struct PendingClosureProofContribution { + construction_parent_ids: [u32; 2], + construction_parent_permutation: [u32; 2], + reconstruction_parent_permutation: [u32; 2], + evaluator_parent_permutation: [u32; 2], + closure_template_semantic_digest: SemanticDigest, + color_witness_term_id: u32, + color_witness_proof_digest: SemanticDigest, + three_line_certificate: Option, + pairing_certificate_ids: Box<[u32]>, + reflection_certificate_id: Option, + exact_factor: ExactComplexRational, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +struct PendingClosureGroup { + contributions: Vec, + exact_factor: ExactComplexRational, +} + +/// Closure rows together with the physical sectors exhaustively visited by +/// their native construction pass. Keeping the domain separate from the row +/// map prevents a later consumer from treating a missing row as proof that a +/// sector is zero. +#[derive(Debug, Eq, PartialEq)] +struct ExhaustiveClosureDomain { + groups: BTreeMap, + physical_sector_ids: BTreeSet, + physical_sector_owner_ids: BTreeMap, +} + +impl std::ops::Deref for ExhaustiveClosureDomain { + type Target = BTreeMap; + + fn deref(&self) -> &Self::Target { + &self.groups + } +} + +impl ExhaustiveClosureDomain { + fn new( + groups: BTreeMap, + physical_sector_ids: &BTreeSet, + physical_sector_owner_ids: BTreeMap, + ) -> RusticolResult { + if groups + .keys() + .any(|key| !physical_sector_ids.contains(&key.target_sector_id)) + { + return Err(invalid( + "closure result lies outside its exhaustive physical-sector domain", + )); + } + if physical_sector_owner_ids + .keys() + .copied() + .collect::>() + != *physical_sector_ids + || physical_sector_owner_ids + .iter() + .any(|(sector_id, owner_id)| { + !physical_sector_ids.contains(owner_id) + || owner_id > sector_id + || physical_sector_owner_ids.get(owner_id) != Some(owner_id) + }) + { + return Err(invalid( + "closure result has an invalid physical-sector owner domain", + )); + } + Ok(Self { + groups, + physical_sector_ids: physical_sector_ids.clone(), + physical_sector_owner_ids, + }) + } +} + +type ClosureColorAttemptDiagnostic = Vec<(LCColorComponentKind, Vec)>; + +impl Default for PendingClosureGroup { + fn default() -> Self { + Self { + contributions: Vec::new(), + exact_factor: ExactComplexRational::ZERO, + } + } +} + +impl PendingClosureGroup { + fn include(&mut self, contribution: PendingClosureProofContribution) -> RusticolResult<()> { + aggregate_factor(&mut self.exact_factor, contribution.exact_factor)?; + self.contributions.push(contribution); + Ok(()) + } +} + +#[derive(Clone, Debug, Default)] +struct StageConstructionDiagnostics { + target_size: usize, + candidate_parent_pair_count: usize, + support_bucket_count: usize, + support_bucket_probe_count: usize, + support_bucket_cache_hit_count: usize, + parent_pair_count: usize, + transition_index_hit_count: usize, + transition_candidate_count: usize, + state_order_count: usize, + quantum_match_count: usize, + coupling_match_count: usize, + structural_reject_count: usize, + transition_accept_count: usize, + color_shape_match_count: usize, + color_result_count: usize, + color_target_prune_count: usize, + accepted_parent_key_clone_count: usize, + current_key_lookup_count: usize, + current_key_hit_count: usize, + current_insert_count: usize, + current_key_clone_count: usize, + contribution_attempt_count: usize, + contribution_insert_count: usize, + contribution_merge_count: usize, + support_indexing_nanoseconds: u64, + candidate_processing_nanoseconds: u64, +} + +fn duration_nanoseconds(duration: Duration) -> u64 { + u64::try_from(duration.as_nanos()).unwrap_or(u64::MAX) +} + +fn add_elapsed_nanoseconds(total: &mut u64, started: Instant) { + *total = total.saturating_add(duration_nanoseconds(started.elapsed())); +} + +fn telemetry_timer(collect_telemetry: bool) -> Option { + collect_telemetry.then(Instant::now) +} + +fn add_optional_elapsed_nanoseconds(total: &mut u64, started: Option) { + if let Some(started) = started { + add_elapsed_nanoseconds(total, started); + } +} + +impl RecurrenceGenerationTelemetry { + fn include_stage(&mut self, stage: &StageConstructionDiagnostics) -> RusticolResult<()> { + for (target, value, label) in [ + ( + &mut self.support_bucket_count, + stage.support_bucket_count, + "telemetry support-bucket count", + ), + ( + &mut self.support_bucket_probe_count, + stage.support_bucket_probe_count, + "telemetry support-bucket probe count", + ), + ( + &mut self.support_bucket_cache_hit_count, + stage.support_bucket_cache_hit_count, + "telemetry support-bucket cache-hit count", + ), + ( + &mut self.candidate_parent_pair_theoretical_count, + stage.candidate_parent_pair_count, + "telemetry theoretical parent-pair count", + ), + ( + &mut self.candidate_parent_pair_visited_count, + stage.parent_pair_count, + "telemetry visited parent-pair count", + ), + ( + &mut self.structural_reject_count, + stage.structural_reject_count, + "telemetry structural-reject count", + ), + ( + &mut self.transition_index_hit_count, + stage.transition_index_hit_count, + "telemetry transition-index hit count", + ), + ( + &mut self.transition_candidate_count, + stage.transition_candidate_count, + "telemetry transition-candidate count", + ), + ( + &mut self.quantum_match_count, + stage.quantum_match_count, + "telemetry quantum-match count", + ), + ( + &mut self.coupling_match_count, + stage.coupling_match_count, + "telemetry coupling-match count", + ), + ( + &mut self.transition_accept_count, + stage.transition_accept_count, + "telemetry transition-accept count", + ), + ( + &mut self.color_shape_match_count, + stage.color_shape_match_count, + "telemetry color-shape-match count", + ), + ( + &mut self.color_result_count, + stage.color_result_count, + "telemetry color-result count", + ), + ( + &mut self.color_target_reject_count, + stage.color_target_prune_count, + "telemetry color-target-reject count", + ), + ( + &mut self.accepted_parent_key_clone_count, + stage.accepted_parent_key_clone_count, + "telemetry accepted-parent-key clone count", + ), + ( + &mut self.current_key_lookup_count, + stage.current_key_lookup_count, + "telemetry current-key lookup count", + ), + ( + &mut self.current_key_hit_count, + stage.current_key_hit_count, + "telemetry current-key hit count", + ), + ( + &mut self.current_insert_count, + stage.current_insert_count, + "telemetry current-insert count", + ), + ( + &mut self.current_key_clone_count, + stage.current_key_clone_count, + "telemetry current-key clone count", + ), + ( + &mut self.contribution_attempt_count, + stage.contribution_attempt_count, + "telemetry contribution-attempt count", + ), + ( + &mut self.contribution_insert_count, + stage.contribution_insert_count, + "telemetry contribution-insert count", + ), + ( + &mut self.contribution_merge_count, + stage.contribution_merge_count, + "telemetry contribution-merge count", + ), + ] { + checked_diagnostic_add(target, value, label)?; + } + checked_diagnostic_add( + &mut self.support_bucket_cache_miss_count, + stage + .support_bucket_probe_count + .checked_sub(stage.support_bucket_cache_hit_count) + .ok_or_else(|| invalid("support-bucket cache-hit count exceeds probes"))?, + "telemetry support-bucket cache-miss count", + )?; + checked_diagnostic_add( + &mut self.transition_index_miss_count, + stage + .parent_pair_count + .checked_sub(stage.transition_index_hit_count) + .ok_or_else(|| invalid("transition-index hit count exceeds parent pairs"))?, + "telemetry transition-index miss count", + )?; + checked_diagnostic_add( + &mut self.color_target_accept_count, + stage + .color_result_count + .checked_sub(stage.color_target_prune_count) + .ok_or_else(|| invalid("color-target prune count exceeds color results"))?, + "telemetry color-target-accept count", + )?; + self.support_indexing_nanoseconds = self + .support_indexing_nanoseconds + .saturating_add(stage.support_indexing_nanoseconds); + self.candidate_processing_nanoseconds = self + .candidate_processing_nanoseconds + .saturating_add(stage.candidate_processing_nanoseconds); + Ok(()) + } +} + +#[cfg(test)] +#[derive(Clone, Copy, Debug)] +struct IndexedTransition { + row: TransitionRow, + input_states: [u32; 2], +} + +#[cfg(test)] +impl IndexedTransition { + fn parent_ids( + self, + left_state: u32, + right_state: u32, + left_id: u32, + right_id: u32, + ) -> RusticolResult<[u32; 2]> { + if self.input_states == [left_state, right_state] { + return Ok([left_id, right_id]); + } + if left_state != right_state && self.input_states == [right_state, left_state] { + return Ok([right_id, left_id]); + } + Err(invalid("recurrence transition state index is inconsistent")) + } +} + +#[cfg(test)] +#[derive(Debug, Default)] +struct TransitionStateIndex { + rows_by_state_pair: BTreeMap<(u32, u32), Vec>, +} + +#[cfg(test)] +impl TransitionStateIndex { + fn new( + template: &OwnedRecurrenceTemplateInput, + catalog: &TemplateCatalog<'_>, + ) -> RusticolResult { + let mut result = Self::default(); + for transition in &template.transitions { + let input_states = catalog.u32_sequence( + transition.input_state_sequence_id, + "transition input states", + )?; + let input_states: [u32; 2] = input_states + .try_into() + .map_err(|_| invalid("direct recurrence requires binary prepared transitions"))?; + result.insert(*transition, input_states); + } + Ok(result) + } + + fn insert(&mut self, row: TransitionRow, input_states: [u32; 2]) { + self.rows_by_state_pair + .entry(canonical_state_pair(input_states)) + .or_default() + .push(IndexedTransition { row, input_states }); + } + + fn rows(&self, left_state: u32, right_state: u32) -> &[IndexedTransition] { + self.rows_by_state_pair + .get(&canonical_state_pair([left_state, right_state])) + .map(Vec::as_slice) + .unwrap_or_default() + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +struct PreparedTransitionWitness { + row: LCColorTransitionWitnessRow, + witness: LCColorTransitionWitness, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +enum PreparedFlavourFlow { + Constant(Box<[i32]>), + AppendLeft(i32), + AppendRight(i32), + ConcatLeftRight(i32), +} + +impl PreparedFlavourFlow { + fn new(quantum: QuantumFlowRow, catalog: &TemplateCatalog<'_>) -> RusticolResult { + let operation = catalog.string( + quantum.flavour_flow_operation_string_id, + "quantum-flow flavour operation", + )?; + let static_result = catalog.flavour_flow( + quantum.result_flavour_flow_id, + "quantum-flow result flavour", + )?; + let result_particle = *static_result + .last() + .ok_or_else(|| invalid("quantum-flow result flavour ancestry is empty"))?; + match operation { + "constant-result" => Ok(Self::Constant(static_result.into())), + "append-left-result" => Ok(Self::AppendLeft(result_particle)), + "append-right-result" => Ok(Self::AppendRight(result_particle)), + "concat-left-right-result" => Ok(Self::ConcatLeftRight(result_particle)), + value => Err(invalid(format!( + "unsupported quantum-flow flavour operation {value:?}" + ))), + } + } + + fn result_particle(&self) -> i32 { + match self { + Self::Constant(result) => *result + .last() + .expect("prepared constant flavour flow is nonempty"), + Self::AppendLeft(result_particle) + | Self::AppendRight(result_particle) + | Self::ConcatLeftRight(result_particle) => *result_particle, + } + } + + fn apply(&self, parents: &[&CurrentCoreKey; 2]) -> Vec { + // Runtime-helicity unions intentionally collapse construction ancestry + // to the physical result species. + if parents + .iter() + .all(|parent| parent.helicity_identity().strategy() == RecurrenceStrategy::AllFlowUnion) + { + return vec![self.result_particle()]; + } + + let append_result = |parent: &CurrentCoreKey, result_particle| { + let mut result = parent.flavour_flow().to_vec(); + if result.last().copied() != Some(result_particle) { + result.push(result_particle); + } + result + }; + + match self { + Self::Constant(result) => result.to_vec(), + Self::AppendLeft(result_particle) => append_result(parents[0], *result_particle), + Self::AppendRight(result_particle) => append_result(parents[1], *result_particle), + Self::ConcatLeftRight(result_particle) => { + let mut result = Vec::with_capacity( + parents[0] + .flavour_flow() + .len() + .saturating_add(parents[1].flavour_flow().len()) + .saturating_add(1), + ); + result.extend_from_slice(parents[0].flavour_flow()); + result.extend_from_slice(parents[1].flavour_flow()); + result.push(*result_particle); + result + } + } + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +struct PreparedTransition { + row: TransitionRow, + input_states: [u32; 2], + quantum: QuantumFlowRow, + quantum_input_states: [u32; 2], + quantum_input_spins: [i32; 2], + local_coupling_orders: Box<[u32]>, + contraction: ColorContractionRow, + canonical_input_order: [u32; 2], + input_exchange_factor: Option, + transition_exact_factor: ExactComplexRational, + contraction_exact_factor: ExactComplexRational, + coupling_authenticated: bool, + binding_coupling: ExactComplexRational, + output_factor_source: u8, + result_flavour: PreparedFlavourFlow, + quantum_semantic_digest: SemanticDigest, + #[cfg(feature = "on-the-fly-test-support")] + transition_semantic_digest: SemanticDigest, + #[cfg(feature = "on-the-fly-test-support")] + evaluator_binding_semantic_digest: SemanticDigest, + contact_orbit: Option, + witnesses: Box<[PreparedTransitionWitness]>, +} + +impl PreparedTransition { + fn new( + row: TransitionRow, + template: &OwnedRecurrenceTemplateInput, + catalog: &TemplateCatalog<'_>, + ) -> RusticolResult { + let input_states = + catalog.u32_sequence(row.input_state_sequence_id, "transition input states")?; + let input_states: [u32; 2] = input_states + .try_into() + .map_err(|_| invalid("direct recurrence requires binary prepared transitions"))?; + let quantum = template + .quantum_flows + .get(row.quantum_flow_template_id as usize) + .copied() + .ok_or_else(|| invalid("transition quantum-flow template is absent"))?; + let quantum_input_states = + catalog.u32_sequence(quantum.input_state_sequence_id, "quantum input states")?; + let quantum_input_spins = + catalog.i32_sequence(quantum.input_spin_sequence_id, "quantum input spins")?; + let quantum_input_flavour_ids = catalog.u32_sequence( + quantum.input_flavour_sequence_id, + "quantum input flavour flows", + )?; + let quantum_input_number_ids = catalog.u32_sequence( + quantum.input_quantum_sequence_id, + "quantum input number flows", + )?; + if quantum_input_states.len() != 2 + || quantum_input_spins.len() != 2 + || quantum_input_flavour_ids.len() != 2 + || quantum_input_number_ids.len() != 2 + { + return Err(invalid( + "direct recurrence requires binary quantum-flow contracts", + )); + } + let quantum_input_states = [quantum_input_states[0], quantum_input_states[1]]; + let quantum_input_spins = [quantum_input_spins[0], quantum_input_spins[1]]; + let quantum_input_flavour_ids = + [quantum_input_flavour_ids[0], quantum_input_flavour_ids[1]]; + for flavour_id in quantum_input_flavour_ids { + let _ = catalog.flavour_flow(flavour_id, "quantum parent flavour")?; + } + let _quantum_input_number_ids = [quantum_input_number_ids[0], quantum_input_number_ids[1]]; + + let local_coupling_orders = catalog + .coupling_orders(row.coupling_order_set_id)? + .into_boxed_slice(); + let contraction = template + .color_contractions + .get(row.color_contraction_template_id as usize) + .copied() + .ok_or_else(|| invalid("transition color contraction is absent"))?; + let quantum_coupling = catalog.factor( + quantum.exact_coupling_factor_id, + "transition quantum-flow coupling", + )?; + let binding_coupling = catalog.factor( + row.binding_coupling_factor_id, + "transition binding coupling", + )?; + let canonical_input_order = match catalog.u32_sequence( + row.canonical_input_order_sequence_id, + "transition canonical input order", + )? { + [0, 1] => [0, 1], + [1, 0] => [1, 0], + _ => { + return Err(invalid( + "transition canonical input order is not a binary permutation", + )); + } + }; + let input_exchange_factor = if row.input_exchange_factor_id == MISSING_U32 { + None + } else { + Some(catalog.factor(row.input_exchange_factor_id, "transition input-exchange")?) + }; + let transition_exact_factor = catalog.factor(row.exact_factor_id, "transition exact")?; + let contraction_exact_factor = + catalog.factor(contraction.exact_coefficient_factor_id, "color contraction")?; + let result_flavour = PreparedFlavourFlow::new(quantum, catalog)?; + let quantum_semantic_digest = + catalog.digest(quantum.semantic_digest_id, "quantum-flow semantic")?; + #[cfg(feature = "on-the-fly-test-support")] + let transition_semantic_digest = + catalog.digest(row.semantic_digest_id, "transition semantic")?; + #[cfg(feature = "on-the-fly-test-support")] + let evaluator_binding_semantic_digest = { + let binding = template + .evaluator_bindings + .get(row.evaluator_binding_id as usize) + .ok_or_else(|| invalid("transition evaluator binding is absent"))?; + if binding.id != row.evaluator_binding_id { + return Err(invalid("transition evaluator binding is not canonical")); + } + catalog.digest(binding.semantic_digest_id, "transition evaluator binding")? + }; + let witness_rows = catalog.witness_rows(row.color_contraction_template_id)?; + let contact_orbit = prepare_contact_orbit_transition( + template, + catalog, + row, + quantum_semantic_digest, + &local_coupling_orders, + binding_coupling, + transition_exact_factor, + contraction_exact_factor, + input_exchange_factor, + witness_rows, + )?; + let witnesses = witness_rows + .iter() + .copied() + .map(|witness_row| { + Ok(PreparedTransitionWitness { + row: witness_row, + witness: catalog.witness(witness_row)?, + }) + }) + .collect::>>()? + .into_boxed_slice(); + Ok(Self { + row, + input_states, + quantum, + quantum_input_states, + quantum_input_spins, + local_coupling_orders, + contraction, + canonical_input_order, + input_exchange_factor, + transition_exact_factor, + contraction_exact_factor, + coupling_authenticated: quantum_coupling == binding_coupling, + binding_coupling, + output_factor_source: row.output_factor_source, + result_flavour, + quantum_semantic_digest, + #[cfg(feature = "on-the-fly-test-support")] + transition_semantic_digest, + #[cfg(feature = "on-the-fly-test-support")] + evaluator_binding_semantic_digest, + contact_orbit, + witnesses, + }) + } + + fn parent_ids( + &self, + left_state: u32, + right_state: u32, + left_id: u32, + right_id: u32, + ) -> RusticolResult<[u32; 2]> { + if self.input_states == [left_state, right_state] { + return Ok([left_id, right_id]); + } + if left_state != right_state && self.input_states == [right_state, left_state] { + return Ok([right_id, left_id]); + } + Err(invalid("recurrence transition state index is inconsistent")) + } + + fn quantum_flow_matches(&self, parents: &[&CurrentCoreKey; 2]) -> bool { + (0..2).all(|index| { + self.quantum_input_states[index] == parents[index].current_state_template_id() + && quantum_parent_spin_matches(self.quantum_input_spins[index], parents[index]) + }) + } + + fn canonical_evaluator_parents( + &self, + concrete_parent_ids: [u32; 2], + ) -> ([u32; 2], ExactComplexRational) { + let mut ordered = match self.canonical_input_order { + [0, 1] => concrete_parent_ids, + [1, 0] => [concrete_parent_ids[1], concrete_parent_ids[0]], + _ => unreachable!("validated prepared transition input order"), + }; + let mut factor = ExactComplexRational::ONE; + if let Some(exchange_factor) = self.input_exchange_factor + && ordered[1] < ordered[0] + { + ordered.swap(0, 1); + factor = exchange_factor; + } + (ordered, factor) + } + + fn result_flavour_flow(&self, parents: &[&CurrentCoreKey; 2]) -> Vec { + self.result_flavour.apply(parents) + } + + fn output_factor(&self) -> RusticolResult { + if !self.coupling_authenticated { + return Err(invalid( + "transition binding coupling does not match its quantum-flow coupling witness", + )); + } + output_factor_from_binding( + self.binding_coupling, + self.output_factor_source, + "transition", + ) + } + + fn structural_transition(&self) -> RusticolResult { + if self.quantum.result_state_template_id != self.row.result_state_template_id { + return Err(invalid( + "structural-demand transition and quantum-flow result states differ", + )); + } + Ok(StructuralTransition { + parents: [ + StructuralState::new(self.quantum_input_states[0], self.quantum_input_spins[0]), + StructuralState::new(self.quantum_input_states[1], self.quantum_input_spins[1]), + ], + result: StructuralState::new( + self.quantum.result_state_template_id, + self.quantum.result_spin_state, + ), + }) + } +} + +#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] +struct PreparedTransitionLocation { + transition_id: u32, + state_pair: (u32, u32), + row_index: usize, +} + +fn reserve_prepared_transition_locations( + locations: &mut Vec, + count: usize, +) -> RusticolResult<()> { + locations.try_reserve_exact(count).map_err(|error| { + RusticolError::internal(format!( + "prepared transition-location allocation failed: {error}" + )) + }) +} + +#[derive(Debug, Default)] +struct PreparedTransitionCatalog { + rows_by_state_pair: BTreeMap<(u32, u32), Vec>, + transition_locations: Vec, + structural_transitions: Box<[StructuralTransition]>, + decoded_transition_count: usize, + decoded_witness_count: usize, +} + +impl PreparedTransitionCatalog { + fn new( + template: &OwnedRecurrenceTemplateInput, + catalog: &TemplateCatalog<'_>, + ) -> RusticolResult { + let mut rows_by_state_pair = BTreeMap::<(u32, u32), Vec>::new(); + let mut transition_locations = Vec::new(); + reserve_prepared_transition_locations( + &mut transition_locations, + template.transitions.len(), + )?; + let mut structural_transitions = BTreeSet::new(); + let mut decoded_transition_count = 0usize; + let mut decoded_witness_count = 0usize; + for row in template.transitions.iter().copied() { + let prepared = PreparedTransition::new(row, template, catalog)?; + decoded_transition_count += 1; + decoded_witness_count += prepared.witnesses.len(); + structural_transitions.insert(prepared.structural_transition()?); + let state_pair = canonical_state_pair(prepared.input_states); + let row_index = rows_by_state_pair.get(&state_pair).map_or(0, Vec::len); + transition_locations.push(PreparedTransitionLocation { + transition_id: prepared.row.id, + state_pair, + row_index, + }); + rows_by_state_pair + .entry(state_pair) + .or_default() + .push(prepared); + } + // Template transition rows retain their authenticated source order in + // `rows_by_state_pair`. Only this private lookup side index needs ID + // order for binary search, so sort the already-reserved locator rows + // without perturbing ordinary transition iteration or IDs. + transition_locations.sort_unstable_by_key(|location| location.transition_id); + if !transition_locations + .windows(2) + .all(|rows| rows[0].transition_id < rows[1].transition_id) + { + return Err(invalid( + "prepared transition-location index contains duplicate IDs", + )); + } + let result = Self { + rows_by_state_pair, + transition_locations, + structural_transitions: structural_transitions + .into_iter() + .collect::>() + .into_boxed_slice(), + decoded_transition_count, + decoded_witness_count, + }; + debug_assert_eq!( + result.decoded_transition_count(), + template.transitions.len() + ); + debug_assert_eq!( + result.decoded_witness_count(), + result + .rows_by_state_pair + .values() + .flatten() + .map(|transition| transition.witnesses.len()) + .sum::() + ); + Ok(result) + } + + fn rows(&self, left_state: u32, right_state: u32) -> &[PreparedTransition] { + self.rows_by_state_pair + .get(&canonical_state_pair([left_state, right_state])) + .map(Vec::as_slice) + .unwrap_or_default() + } + + fn contact_orbit(&self, transition_id: u32) -> Option<&PreparedContactOrbitTransition> { + let location = self + .transition_locations + .binary_search_by_key(&transition_id, |location| location.transition_id) + .ok() + .and_then(|index| self.transition_locations.get(index))?; + self.rows_by_state_pair + .get(&location.state_pair) + .and_then(|rows| rows.get(location.row_index)) + .filter(|row| row.row.id == transition_id) + .and_then(|row| row.contact_orbit.as_ref()) + } + + fn structural_transitions(&self) -> &[StructuralTransition] { + &self.structural_transitions + } + + fn decoded_transition_count(&self) -> usize { + self.decoded_transition_count + } + + fn decoded_witness_count(&self) -> usize { + self.decoded_witness_count + } +} + +#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] +struct EstablishedContactContributionToken { + destination_id: usize, + contribution_ordinal: usize, +} + +#[derive(Debug)] +struct EstablishedContactOwnerPlan { + stage_current_start: usize, + selected_tokens: Vec, + staged_contribution_count: usize, + retained_contribution_count: usize, + resident_contribution_count_after_commit: usize, +} + +impl EstablishedContactOwnerPlan { + /// Commit a completely validated owner plan without allocating or failing. + /// + /// The plan owns the complete selected-token output and every fallible + /// candidate/decode/count operation has already succeeded. `retain` keeps + /// the canonical `BTreeMap` order and performs no allocation, so live + /// construction state changes only in this final infallible phase. + fn commit(self, currents: &mut [PendingCurrent], resident_contribution_count: &mut usize) { + let mut selected_index = 0usize; + let mut observed_count = 0usize; + let mut retained_count = 0usize; + for (destination_id, current) in currents + .iter_mut() + .enumerate() + .skip(self.stage_current_start) + { + let mut contribution_ordinal = 0usize; + current.contributions.retain(|_, _| { + let token = EstablishedContactContributionToken { + destination_id, + contribution_ordinal, + }; + contribution_ordinal += 1; + observed_count += 1; + let selected = self.selected_tokens.get(selected_index) == Some(&token); + if selected { + selected_index += 1; + retained_count += 1; + } + selected + }); + } + debug_assert_eq!(observed_count, self.staged_contribution_count); + debug_assert_eq!(retained_count, self.retained_contribution_count); + debug_assert_eq!(selected_index, self.selected_tokens.len()); + *resident_contribution_count = self.resident_contribution_count_after_commit; + } +} + +/// Plan exact established-path contact ownership over the complete stage fan-in. +/// +/// Uncertified contributions are represented by `None` candidates and pass +/// through unchanged. The stable token is a snapshot of the canonical +/// destination/current order plus the contribution's ordered-map ordinal. +/// Every allocation and error-capable proof lookup happens before the caller +/// replaces any live contribution storage or resident-count telemetry. +fn plan_established_contact_orbit_owners( + stage_current_start: usize, + prepared_transitions: &PreparedTransitionCatalog, + currents: &[PendingCurrent], + resident_contribution_count: usize, +) -> RusticolResult> { + plan_established_contact_orbit_owners_with_resolver( + stage_current_start, + currents, + resident_contribution_count, + |transition_id| prepared_transitions.contact_orbit(transition_id), + ) +} + +fn reserve_established_contact_candidates( + candidates: &mut Vec, + count: usize, +) -> RusticolResult<()> { + candidates.try_reserve_exact(count).map_err(|error| { + RusticolError::internal(format!( + "established contact-orbit candidate allocation failed: {error}" + )) + }) +} + +fn plan_established_contact_orbit_owners_with_resolver<'a, F>( + stage_current_start: usize, + currents: &'a [PendingCurrent], + resident_contribution_count: usize, + mut contact_orbit: F, +) -> RusticolResult> +where + F: FnMut(u32) -> Option<&'a PreparedContactOrbitTransition>, +{ + if stage_current_start > currents.len() { + return Err(invalid( + "contact-orbit stage current boundary exceeds current storage", + )); + } + let has_certified_contact = currents[stage_current_start..].iter().any(|current| { + current + .contributions + .keys() + .any(|pending| contact_orbit(pending.key.transition_template_id()).is_some()) + }); + if !has_certified_contact { + return Ok(None); + } + let staged_contribution_count = + currents[stage_current_start..] + .iter() + .try_fold(0usize, |count, current| { + count + .checked_add(current.contributions.len()) + .ok_or_else(|| invalid("contact-orbit staged contribution count exceeds usize")) + })?; + let mut candidates = Vec::new(); + reserve_established_contact_candidates(&mut candidates, staged_contribution_count)?; + for (destination_id, current) in currents.iter().enumerate().skip(stage_current_start) { + for (contribution_ordinal, pending) in current.contributions.keys().enumerate() { + let token = EstablishedContactContributionToken { + destination_id, + contribution_ordinal, + }; + let candidate = + if let Some(contact_orbit) = contact_orbit(pending.key.transition_template_id()) { + let [left_id, right_id] = pending.parent_current_ids.as_ref() else { + return Err(invalid( + "certified contact-orbit contribution is not binary", + )); + }; + let left = currents + .get(*left_id as usize) + .ok_or_else(|| invalid("contact-orbit left parent is absent"))?; + let right = currents + .get(*right_id as usize) + .ok_or_else(|| invalid("contact-orbit right parent is absent"))?; + Some(contact_orbit.owner_candidate( + ¤t.key, + [&left.key, &right.key], + pending.key.color_witness_term_id(), + )?) + } else { + None + }; + candidates.push((token, candidate)); + } + } + if candidates.len() != staged_contribution_count { + return Err(invalid( + "contact-orbit staged contribution snapshot changed length", + )); + } + let selected_tokens = selected_contact_orbit_owner_tokens(candidates)?; + let retained_contribution_count = selected_tokens.len(); + let removed_contribution_count = staged_contribution_count + .checked_sub(retained_contribution_count) + .ok_or_else(|| invalid("contact-orbit retained contribution count exceeds snapshot"))?; + let resident_contribution_count_after_commit = resident_contribution_count + .checked_sub(removed_contribution_count) + .ok_or_else(|| invalid("contact-orbit resident contribution count underflows"))?; + Ok(Some(EstablishedContactOwnerPlan { + stage_current_start, + selected_tokens, + staged_contribution_count, + retained_contribution_count, + resident_contribution_count_after_commit, + })) +} + +#[derive(Clone, Debug, Eq, PartialEq)] +struct PreparedClosureWitness { + row: LCColorTransitionWitnessRow, + witness: LCColorTransitionWitness, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +struct PreparedClosureQuantumFlow { + row: Option, + input_states: Option<[u32; 2]>, + input_spins: Option<[i32; 2]>, + coupling_authenticated: bool, + binding_coupling: ExactComplexRational, + output_factor_source: u8, +} + +impl PreparedClosureQuantumFlow { + fn unbound(closure: ClosureRow, catalog: &TemplateCatalog<'_>) -> RusticolResult { + let binding_coupling = catalog.factor( + closure.binding_coupling_factor_id, + "closure binding coupling", + )?; + Ok(Self { + row: None, + input_states: None, + input_spins: None, + coupling_authenticated: true, + binding_coupling, + output_factor_source: closure.output_factor_source, + }) + } + + fn bound( + closure: ClosureRow, + quantum: QuantumFlowRow, + catalog: &TemplateCatalog<'_>, + ) -> RusticolResult { + let input_states = + catalog.u32_sequence(quantum.input_state_sequence_id, "quantum input states")?; + let input_spins = + catalog.i32_sequence(quantum.input_spin_sequence_id, "quantum input spins")?; + let input_flavours = catalog.u32_sequence( + quantum.input_flavour_sequence_id, + "quantum input flavour flows", + )?; + let input_quantum_numbers = catalog.u32_sequence( + quantum.input_quantum_sequence_id, + "quantum input number flows", + )?; + if input_states.len() != 2 + || input_spins.len() != 2 + || input_flavours.len() != 2 + || input_quantum_numbers.len() != 2 + { + return Err(invalid( + "direct recurrence requires binary quantum-flow contracts", + )); + } + for flavour_id in input_flavours { + let _ = catalog.flavour_flow(*flavour_id, "quantum parent flavour")?; + } + let quantum_coupling = catalog.factor( + quantum.exact_coupling_factor_id, + "closure quantum-flow coupling", + )?; + let binding_coupling = catalog.factor( + closure.binding_coupling_factor_id, + "closure binding coupling", + )?; + Ok(Self { + row: Some(quantum), + input_states: Some([input_states[0], input_states[1]]), + input_spins: Some([input_spins[0], input_spins[1]]), + coupling_authenticated: quantum_coupling == binding_coupling, + binding_coupling, + output_factor_source: closure.output_factor_source, + }) + } + + fn matches(&self, parents: &[&CurrentCoreKey; 2]) -> bool { + let (Some(input_states), Some(input_spins)) = (self.input_states, self.input_spins) else { + return true; + }; + (0..2).all(|index| { + input_states[index] == parents[index].current_state_template_id() + && quantum_parent_spin_matches(input_spins[index], parents[index]) + }) + } + + fn template_id(&self) -> Option { + self.row.map(|row| row.id) + } + + fn output_factor(&self) -> RusticolResult { + if !self.coupling_authenticated { + return Err(invalid( + "closure binding coupling does not match its quantum-flow coupling witness", + )); + } + output_factor_from_binding(self.binding_coupling, self.output_factor_source, "closure") + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +struct PreparedClosure { + row: ClosureRow, + input_states: [u32; 2], + quantum_flows: Box<[PreparedClosureQuantumFlow]>, + contraction: ColorContractionRow, + canonical_input_order: [u32; 2], + input_exchange_factor: Option, + closure_exact_factor: ExactComplexRational, + contraction_exact_factor: ExactComplexRational, + semantic_digest: SemanticDigest, + witnesses: Box<[PreparedClosureWitness]>, +} + +impl PreparedClosure { + fn new( + row: ClosureRow, + template: &OwnedRecurrenceTemplateInput, + catalog: &TemplateCatalog<'_>, + ) -> RusticolResult { + let input_states = + catalog.u32_sequence(row.input_state_sequence_id, "closure input states")?; + let input_states: [u32; 2] = input_states + .try_into() + .map_err(|_| invalid("direct recurrence requires binary prepared closures"))?; + let eligible = catalog.u32_sequence( + row.eligible_quantum_flow_sequence_id, + "closure eligible quantum flows", + )?; + let quantum_flows = if eligible.is_empty() { + vec![PreparedClosureQuantumFlow::unbound(row, catalog)?] + } else { + eligible + .iter() + .copied() + .map(|quantum_id| { + let quantum = template + .quantum_flows + .get(quantum_id as usize) + .copied() + .ok_or_else(|| invalid("closure quantum flow is absent"))?; + PreparedClosureQuantumFlow::bound(row, quantum, catalog) + }) + .collect::>>()? + } + .into_boxed_slice(); + let contraction = template + .color_contractions + .get(row.color_contraction_template_id as usize) + .copied() + .ok_or_else(|| invalid("closure color contraction is absent"))?; + let canonical_input_order = match catalog.u32_sequence( + row.canonical_input_order_sequence_id, + "closure canonical input order", + )? { + [0, 1] => [0, 1], + [1, 0] => [1, 0], + _ => { + return Err(invalid( + "closure canonical input order is not a binary permutation", + )); + } + }; + let input_exchange_factor = if row.input_exchange_factor_id == MISSING_U32 { + None + } else { + Some(catalog.factor(row.input_exchange_factor_id, "closure input-exchange")?) + }; + let witnesses = catalog + .witness_rows(row.color_contraction_template_id)? + .iter() + .copied() + .map(|witness_row| { + Ok(PreparedClosureWitness { + row: witness_row, + witness: catalog.witness(witness_row)?, + }) + }) + .collect::>>()? + .into_boxed_slice(); + Ok(Self { + row, + input_states, + quantum_flows, + contraction, + canonical_input_order, + input_exchange_factor, + closure_exact_factor: catalog.factor(row.exact_factor_id, "closure exact")?, + contraction_exact_factor: catalog + .factor(contraction.exact_coefficient_factor_id, "closure color")?, + semantic_digest: catalog.digest(row.semantic_digest_id, "closure semantic")?, + witnesses, + }) + } + + fn parent_ids( + &self, + anchor_state: u32, + complement_state: u32, + anchor_id: u32, + complement_id: u32, + ) -> RusticolResult<[u32; 2]> { + if self.input_states == [complement_state, anchor_state] { + return Ok([complement_id, anchor_id]); + } + if anchor_state != complement_state && self.input_states == [anchor_state, complement_state] + { + return Ok([anchor_id, complement_id]); + } + Err(invalid("recurrence closure state index is inconsistent")) + } + + fn canonical_evaluator_parents( + &self, + parent_ids: [u32; 2], + ) -> ([u32; 2], ExactComplexRational) { + let mut ordered = match self.canonical_input_order { + [0, 1] => parent_ids, + [1, 0] => [parent_ids[1], parent_ids[0]], + _ => unreachable!("validated prepared closure input order"), + }; + let mut factor = ExactComplexRational::ONE; + if let Some(exchange_factor) = self.input_exchange_factor + && ordered[1] < ordered[0] + { + ordered.swap(0, 1); + factor = exchange_factor; + } + (ordered, factor) + } +} + +#[derive(Debug, Default)] +struct PreparedClosureCatalog { + rows_by_state_pair: BTreeMap<(u32, u32), Vec>, + row_count: usize, +} + +impl PreparedClosureCatalog { + fn new( + template: &OwnedRecurrenceTemplateInput, + catalog: &TemplateCatalog<'_>, + ) -> RusticolResult { + let mut rows_by_state_pair = BTreeMap::<(u32, u32), Vec>::new(); + for row in template.closures.iter().copied() { + let prepared = PreparedClosure::new(row, template, catalog)?; + rows_by_state_pair + .entry(canonical_state_pair(prepared.input_states)) + .or_default() + .push(prepared); + } + Ok(Self { + rows_by_state_pair, + row_count: template.closures.len(), + }) + } + + fn rows(&self, left_state: u32, right_state: u32) -> &[PreparedClosure] { + self.rows_by_state_pair + .get(&canonical_state_pair([left_state, right_state])) + .map(Vec::as_slice) + .unwrap_or_default() + } + + fn row_count(&self) -> usize { + self.row_count + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +struct PreparedClosureSector { + row: ProcessPhysicalLCSectorRow, + expected_components: Box<[LCColorComponent]>, + contracted_color_canonical_owner: bool, + anchor_support: Box<[u32]>, + complement_support: Box<[u32]>, +} + +#[derive(Debug, Default)] +struct PreparedClosureSectorCatalog { + sectors: BTreeMap, + v4_trace_sector_by_anchored_word: BTreeMap, u32>, +} + +impl PreparedClosureSectorCatalog { + fn new( + strategy: RecurrenceStrategy, + process: &OwnedRecurrenceProcessInput, + catalog: &ProcessCatalog<'_>, + materialized_sector_ids: &BTreeSet, + ) -> RusticolResult { + let source_count = process.external_legs.len(); + let full_support = (0..source_count as u32).collect::>(); + let mut sectors = BTreeMap::new(); + let mut v4_trace_sector_by_anchored_word = BTreeMap::new(); + let mut contracted_open_forests = HashSet::>::new(); + for row in process.physical_lc_sectors.iter().copied() { + if !materialized_sector_ids.contains(&row.sector_id) + && strategy != RecurrenceStrategy::ContractedColorUnion + { + continue; + } + let expected_components = expected_sector_components(row, process, catalog)?; + let contracted_color_canonical_owner = if strategy + == RecurrenceStrategy::ContractedColorUnion + && row.kind()? == ProcessLCSectorKind::OpenLines + { + let mut canonical = expected_components.clone(); + canonical.sort_unstable(); + contracted_open_forests.insert(canonical.into_boxed_slice()) + } else { + true + }; + if !materialized_sector_ids.contains(&row.sector_id) { + continue; + } + let complement_support = full_support + .iter() + .copied() + .filter(|slot| *slot != row.closure_source_slot) + .collect::>() + .into_boxed_slice(); + let prepared = PreparedClosureSector { + row, + expected_components: expected_components.into_boxed_slice(), + contracted_color_canonical_owner, + anchor_support: vec![row.closure_source_slot].into_boxed_slice(), + complement_support, + }; + if sectors.insert(row.sector_id, prepared).is_some() { + return Err(invalid( + "prepared closure sector catalog contains a duplicate sector", + )); + } + if row.kind()? == ProcessLCSectorKind::SingleTrace + && catalog.string( + row.closure_proof_algorithm_string_id, + "prepared closure-anchor algorithm", + )? == CYCLIC_TRACE_CLOSURE_ANCHOR_ALGORITHM + { + let word = catalog + .u32_sequence(row.word_sequence_id, "prepared v4 physical LC sector word")?; + let key = anchored_cyclic_trace_word(word, row.closure_source_slot)?; + if v4_trace_sector_by_anchored_word + .insert(key, row.sector_id) + .is_some() + { + return Err(invalid( + "prepared v4 closure sectors contain an ambiguous cyclic trace word", + )); + } + } + } + if sectors.len() != materialized_sector_ids.len() { + return Err(invalid( + "prepared closure sector catalog omits a materialized sector", + )); + } + Ok(Self { + sectors, + v4_trace_sector_by_anchored_word, + }) + } + + fn get(&self, sector_id: u32) -> RusticolResult<&PreparedClosureSector> { + self.sectors + .get(§or_id) + .ok_or_else(|| invalid("prepared closure sector is absent")) + } + + fn reflected_v4_sector( + &self, + mapped_word: &[u32], + closure_source_slot: u32, + ) -> RusticolResult> { + let key = anchored_cyclic_trace_word(mapped_word, closure_source_slot)?; + Ok(self.v4_trace_sector_by_anchored_word.get(&key).copied()) + } +} + +fn closure_domain_sector_owners( + strategy: RecurrenceStrategy, + prepared_sectors: &PreparedClosureSectorCatalog, + physical_sector_ids: &BTreeSet, +) -> RusticolResult> { + let mut owner_by_open_forest = BTreeMap::, u32>::new(); + let mut result = BTreeMap::new(); + for sector_id in physical_sector_ids.iter().copied() { + let sector = prepared_sectors.get(sector_id)?; + let owner_id = if strategy == RecurrenceStrategy::ContractedColorUnion + && sector.row.kind()? == ProcessLCSectorKind::OpenLines + { + let mut canonical = sector.expected_components.to_vec(); + canonical.sort_unstable(); + let owner_id = *owner_by_open_forest + .entry(canonical.into_boxed_slice()) + .or_insert(sector_id); + if sector.contracted_color_canonical_owner != (owner_id == sector_id) { + return Err(invalid( + "contracted closure-sector owner classification is inconsistent", + )); + } + owner_id + } else { + sector_id + }; + result.insert(sector_id, owner_id); + } + Ok(result) +} + +#[derive(Debug, Default)] +pub(super) struct TransitionReflectionIndex { + proofs_by_transition: BTreeMap, +} + +impl TransitionReflectionIndex { + pub(super) fn new( + template: &OwnedRecurrenceTemplateInput, + catalog: &TemplateCatalog<'_>, + ) -> RusticolResult { + let transitions_by_subject = template + .transitions + .iter() + .map(|row| (row.template_string_id, row.id)) + .collect::>(); + let mut result = Self::default(); + for proof in &template.symmetry_proofs { + if catalog.string(proof.proof_algorithm_string_id, "symmetry proof algorithm")? + != "canonical-current-word-reversal-v1" + { + continue; + } + let subjects = catalog.u32_sequence( + proof.subject_template_sequence_id, + "current-word reversal proof subjects", + )?; + let [subject] = subjects else { + return Err(invalid( + "current-word reversal proof must reference one transition", + )); + }; + let transition_id = transitions_by_subject + .get(subject) + .copied() + .ok_or_else(|| { + invalid("current-word reversal proof subject is not a transition") + })?; + if catalog.u32_sequence( + proof.input_permutation_sequence_id, + "current-word reversal proof permutation", + )? != [1, 0] + { + return Err(invalid( + "current-word reversal proof must exchange two transition inputs", + )); + } + let phase = + catalog.factor(proof.exact_phase_factor_id, "current-word reversal proof")?; + let candidate = TransitionReflectionProof::new( + phase, + [catalog.digest( + proof.semantic_digest_id, + "current-word reversal semantic proof", + )?], + [catalog.digest( + proof.witness_digest_id, + "current-word reversal witness proof", + )?], + )?; + match result.proofs_by_transition.entry(transition_id) { + std::collections::btree_map::Entry::Vacant(entry) => { + entry.insert(candidate); + } + std::collections::btree_map::Entry::Occupied(mut entry) => { + let Some(merged) = entry.get().merged_with(&candidate)? else { + return Err(invalid( + "transition has conflicting current-word reversal phases", + )); + }; + entry.insert(merged); + } + } + } + Ok(result) + } + + pub(super) fn proof(&self, transition_id: u32) -> Option<&TransitionReflectionProof> { + self.proofs_by_transition.get(&transition_id) + } +} + +fn canonical_state_pair([left, right]: [u32; 2]) -> (u32, u32) { + if left <= right { + (left, right) + } else { + (right, left) + } +} + +/// Necessary physical-sector compatibility for a partial LC color forest. +/// +/// Recurrence witnesses can join or explicitly reverse ordered components, but +/// they never split or internally permute an existing component. A partial +/// component must therefore occur as an oriented contiguous word in at least +/// one materialized representative sector. Pure-adjoint reversed words are +/// admitted transiently for one construction stage so reflection proofs can be +/// finalized over the complete fan-in. A stage-final reconciliation then +/// removes only exactly certified aliases. This cheap forward filter prevents +/// the builder from interning unrelated color words; final backward liveness +/// remains authoritative. +#[derive(Clone, Debug, Eq, PartialEq)] +enum SectorPostingStorage { + Sparse(Box<[u32]>), + Dense(Box<[u64]>), +} + +#[derive(Clone, Debug, Eq, PartialEq)] +struct SectorPosting { + cardinality: usize, + storage: SectorPostingStorage, +} + +impl SectorPosting { + fn from_sorted_unique_sector_ids(sector_ids: Vec, sector_count: usize) -> Self { + debug_assert!(!sector_ids.is_empty()); + debug_assert!(sector_ids.windows(2).all(|pair| pair[0] < pair[1])); + debug_assert!( + sector_ids + .last() + .is_none_or(|sector_id| (*sector_id as usize) < sector_count) + ); + let cardinality = sector_ids.len(); + let sparse_bytes = cardinality + .checked_mul(std::mem::size_of::()) + .expect("allocated sparse posting byte size exceeds usize"); + let dense_word_count = sector_count.div_ceil(u64::BITS as usize); + let dense_bytes = dense_word_count + .checked_mul(std::mem::size_of::()) + .expect("allocated dense posting byte size exceeds usize"); + let storage = if dense_bytes < sparse_bytes { + let mut words = vec![0_u64; dense_word_count]; + for sector_id in sector_ids { + let sector_index = sector_id as usize; + words[sector_index / u64::BITS as usize] |= + 1_u64 << (sector_index % u64::BITS as usize); + } + SectorPostingStorage::Dense(words.into_boxed_slice()) + } else { + SectorPostingStorage::Sparse(sector_ids.into_boxed_slice()) + }; + Self { + cardinality, + storage, + } + } + + fn cardinality(&self) -> usize { + self.cardinality + } + + fn contains(&self, sector_id: u32) -> bool { + match &self.storage { + SectorPostingStorage::Sparse(sector_ids) => { + sector_ids.binary_search(§or_id).is_ok() + } + SectorPostingStorage::Dense(words) => { + let sector_index = sector_id as usize; + words + .get(sector_index / u64::BITS as usize) + .is_some_and(|word| word & (1_u64 << (sector_index % u64::BITS as usize)) != 0) + } + } + } + + fn any_sector(&self, mut predicate: impl FnMut(u32) -> bool) -> bool { + match &self.storage { + SectorPostingStorage::Sparse(sector_ids) => sector_ids.iter().copied().any(predicate), + SectorPostingStorage::Dense(words) => { + for (word_index, word) in words.iter().copied().enumerate() { + let mut remaining = word; + while remaining != 0 { + let bit_index = remaining.trailing_zeros() as usize; + let sector_index = word_index * u64::BITS as usize + bit_index; + let sector_id = + u32::try_from(sector_index).expect("sector posting exceeds u32"); + if predicate(sector_id) { + return true; + } + remaining &= remaining - 1; + } + } + false + } + } + } + + fn payload_bytes(&self) -> usize { + match &self.storage { + SectorPostingStorage::Sparse(sector_ids) => sector_ids + .len() + .checked_mul(std::mem::size_of::()) + .expect("allocated sparse posting byte size exceeds usize"), + SectorPostingStorage::Dense(words) => words + .len() + .checked_mul(std::mem::size_of::()) + .expect("allocated dense posting byte size exceeds usize"), + } + } + + fn dense_words(&self) -> Option<&[u64]> { + match &self.storage { + SectorPostingStorage::Sparse(_) => None, + SectorPostingStorage::Dense(words) => Some(words), + } + } + + fn is_sparse(&self) -> bool { + matches!(&self.storage, SectorPostingStorage::Sparse(_)) + } +} + +#[derive(Debug)] +struct MaterializedColorTargets { + collect_telemetry: bool, + non_trace_fragment_sectors: HashMap, SectorPosting>, + trace_component_sectors: HashMap, SectorPosting>, + accepted_component_forests: RefCell>>, + acceptance_cache_hit_count: Cell, + acceptance_cache_miss_count: Cell, + acceptance_accept_count: Cell, + acceptance_reject_count: Cell, + fragment_hash_lookup_count: Cell, +} + +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +struct MaterializedColorTargetTelemetry { + fragment_bucket_count: usize, + acceptance_cache_hit_count: usize, + acceptance_cache_miss_count: usize, + acceptance_accept_count: usize, + acceptance_reject_count: usize, + fragment_hash_lookup_count: usize, + posting_incidence_count: usize, + sparse_posting_bucket_count: usize, + dense_posting_bucket_count: usize, + sparse_posting_bytes: usize, + dense_posting_bytes: usize, +} + +#[derive(Clone, Copy, Debug)] +struct PendingConstructionDomain { + shared_source_end: usize, + lane_internal_start: usize, + lane_internal_end: usize, +} + +impl PendingConstructionDomain { + fn contains(self, current_id: usize) -> bool { + current_id < self.shared_source_end + || (self.lane_internal_start..self.lane_internal_end).contains(¤t_id) + } +} + +#[derive(Debug, Default)] +struct LaneClosureSupportIndex<'a> { + current_ids_by_support: BTreeMap<&'a [u32], Vec>, +} + +impl<'a> LaneClosureSupportIndex<'a> { + fn new( + currents: &'a [PendingCurrent], + construction_domain: Option, + ) -> RusticolResult { + let mut current_ids_by_support = BTreeMap::<&[u32], Vec>::new(); + for (current_id, current) in currents.iter().enumerate() { + if construction_domain.is_some_and(|domain| !domain.contains(current_id)) { + continue; + } + current_ids_by_support + .entry(current.key.support_source_slots()) + .or_default() + .push( + u32::try_from(current_id) + .map_err(|_| invalid("closure current ID exceeds u32"))?, + ); + } + Ok(Self { + current_ids_by_support, + }) + } + + fn current_ids(&self, support: &[u32]) -> &[u32] { + self.current_ids_by_support + .get(support) + .map(Vec::as_slice) + .unwrap_or_default() + } +} + +impl MaterializedColorTargets { + #[cfg(test)] + fn new( + materialized_sector_ids: &BTreeSet, + process: &OwnedRecurrenceProcessInput, + catalog: &ProcessCatalog<'_>, + ) -> RusticolResult { + Self::new_with_telemetry(materialized_sector_ids, process, catalog, false) + } + + #[cfg(test)] + fn new_with_telemetry( + materialized_sector_ids: &BTreeSet, + process: &OwnedRecurrenceProcessInput, + catalog: &ProcessCatalog<'_>, + collect_telemetry: bool, + ) -> RusticolResult { + let sectors = materialized_sector_ids + .iter() + .copied() + .map(|sector_id| { + let sector = process + .physical_lc_sectors + .get(sector_id as usize) + .copied() + .ok_or_else(|| invalid("materialized LC sector is absent"))?; + expected_sector_components(sector, process, catalog) + }) + .collect::>>()?; + Self::from_sectors_with_telemetry(sectors, collect_telemetry) + } + + fn new_with_prepared_sectors( + materialized_sector_ids: &BTreeSet, + sectors: &PreparedClosureSectorCatalog, + collect_telemetry: bool, + ) -> RusticolResult { + let sectors = materialized_sector_ids + .iter() + .copied() + .map(|sector_id| Ok(sectors.get(sector_id)?.expected_components.to_vec())) + .collect::>>()?; + Self::from_sectors_with_telemetry(sectors, collect_telemetry) + } + + #[cfg(test)] + fn from_sectors(sectors: Vec>) -> RusticolResult { + Self::from_sectors_with_telemetry(sectors, false) + } + + fn from_sectors_with_telemetry( + sectors: Vec>, + collect_telemetry: bool, + ) -> RusticolResult { + if sectors.is_empty() { + return Err(invalid( + "recurrence construction has no materialized LC sector", + )); + } + let sector_count = sectors.len(); + let mut non_trace_fragment_sector_ids = HashMap::, Vec>::new(); + let mut trace_component_sector_ids = HashMap::, Vec>::new(); + for (sector_index, sector) in sectors.iter().enumerate() { + let sector_id = u32::try_from(sector_index) + .map_err(|_| invalid("materialized LC sector index exceeds u32"))?; + for target in sector { + match target.kind() { + LCColorComponentKind::Trace => { + Self::include_fragment( + &mut trace_component_sector_ids, + target.source_slots(), + sector_id, + ); + Self::include_cyclic_fragments( + &mut non_trace_fragment_sector_ids, + target.source_slots(), + sector_id, + ); + } + LCColorComponentKind::OpenString | LCColorComponentKind::AdjointSegment => { + Self::include_linear_fragments( + &mut non_trace_fragment_sector_ids, + target.source_slots(), + sector_id, + ); + } + } + } + } + Ok(Self { + collect_telemetry, + non_trace_fragment_sectors: Self::freeze_posting_index( + non_trace_fragment_sector_ids, + sector_count, + ), + trace_component_sectors: Self::freeze_posting_index( + trace_component_sector_ids, + sector_count, + ), + accepted_component_forests: RefCell::new(HashSet::new()), + acceptance_cache_hit_count: Cell::new(0), + acceptance_cache_miss_count: Cell::new(0), + acceptance_accept_count: Cell::new(0), + acceptance_reject_count: Cell::new(0), + fragment_hash_lookup_count: Cell::new(0), + }) + } + + fn accepts(&self, state: &DynamicLCColorState) -> bool { + if self + .accepted_component_forests + .borrow() + .contains(state.components()) + { + if self.collect_telemetry { + self.acceptance_cache_hit_count + .set(self.acceptance_cache_hit_count.get().saturating_add(1)); + self.acceptance_accept_count + .set(self.acceptance_accept_count.get().saturating_add(1)); + } + return true; + } + if self.collect_telemetry { + self.acceptance_cache_miss_count + .set(self.acceptance_cache_miss_count.get().saturating_add(1)); + } + let accepted = self.accepts_uncached(state); + if accepted { + if self.collect_telemetry { + self.acceptance_accept_count + .set(self.acceptance_accept_count.get().saturating_add(1)); + } + self.accepted_component_forests + .borrow_mut() + .insert(state.components().into()); + } else if self.collect_telemetry { + self.acceptance_reject_count + .set(self.acceptance_reject_count.get().saturating_add(1)); + } + accepted + } + + fn accepts_up_to_reflection(&self, state: &DynamicLCColorState) -> RusticolResult { + if self.accepts(state) { + return Ok(true); + } + if state.pure_adjoint_word().is_none() { + return Ok(false); + } + Ok(self.accepts(&state.reversed()?)) + } + + fn accepts_uncached(&self, state: &DynamicLCColorState) -> bool { + if state.components().is_empty() { + return true; + } + let mut component_postings = Vec::with_capacity(state.components().len()); + for partial in state.components() { + if self.collect_telemetry { + self.fragment_hash_lookup_count + .set(self.fragment_hash_lookup_count.get().saturating_add(1)); + } + let fragment_sectors = if partial.kind() == LCColorComponentKind::Trace { + self.trace_component_sectors.get(partial.source_slots()) + } else { + self.non_trace_fragment_sectors.get(partial.source_slots()) + }; + let Some(fragment_sectors) = fragment_sectors else { + return false; + }; + component_postings.push(fragment_sectors); + } + if component_postings.len() == 1 { + return true; + } + if component_postings + .iter() + .all(|posting| posting.dense_words().is_some()) + { + let word_count = component_postings[0] + .dense_words() + .expect("all color postings are dense") + .len(); + debug_assert!(component_postings.iter().all(|posting| { + posting + .dense_words() + .is_some_and(|words| words.len() == word_count) + })); + return (0..word_count).any(|word_index| { + component_postings + .iter() + .fold(u64::MAX, |intersection, posting| { + intersection + & posting.dense_words().expect("all color postings are dense") + [word_index] + }) + != 0 + }); + } + let (candidate_posting_index, candidate_posting) = component_postings + .iter() + .enumerate() + .min_by_key(|(_, posting)| posting.cardinality()) + .expect("non-empty color forest has at least one posting"); + candidate_posting.any_sector(|sector_id| { + component_postings + .iter() + .enumerate() + .all(|(posting_index, posting)| { + posting_index == candidate_posting_index || posting.contains(sector_id) + }) + }) + } + + fn telemetry(&self) -> RusticolResult { + let postings = self + .non_trace_fragment_sectors + .values() + .chain(self.trace_component_sectors.values()); + let mut posting_incidence_count = 0usize; + let mut sparse_posting_bucket_count = 0usize; + let mut dense_posting_bucket_count = 0usize; + let mut sparse_posting_bytes = 0usize; + let mut dense_posting_bytes = 0usize; + for posting in postings { + posting_incidence_count = posting_incidence_count + .checked_add(posting.cardinality()) + .ok_or_else(|| invalid("color-posting incidence count exceeds usize"))?; + if posting.is_sparse() { + sparse_posting_bucket_count = sparse_posting_bucket_count + .checked_add(1) + .ok_or_else(|| invalid("sparse color-posting bucket count exceeds usize"))?; + sparse_posting_bytes = sparse_posting_bytes + .checked_add(posting.payload_bytes()) + .ok_or_else(|| invalid("sparse color-posting bytes exceed usize"))?; + } else { + dense_posting_bucket_count = dense_posting_bucket_count + .checked_add(1) + .ok_or_else(|| invalid("dense color-posting bucket count exceeds usize"))?; + dense_posting_bytes = dense_posting_bytes + .checked_add(posting.payload_bytes()) + .ok_or_else(|| invalid("dense color-posting bytes exceed usize"))?; + } + } + Ok(MaterializedColorTargetTelemetry { + fragment_bucket_count: self + .non_trace_fragment_sectors + .len() + .checked_add(self.trace_component_sectors.len()) + .ok_or_else(|| invalid("color-fragment bucket count exceeds usize"))?, + acceptance_cache_hit_count: self.acceptance_cache_hit_count.get(), + acceptance_cache_miss_count: self.acceptance_cache_miss_count.get(), + acceptance_accept_count: self.acceptance_accept_count.get(), + acceptance_reject_count: self.acceptance_reject_count.get(), + fragment_hash_lookup_count: self.fragment_hash_lookup_count.get(), + posting_incidence_count, + sparse_posting_bucket_count, + dense_posting_bucket_count, + sparse_posting_bytes, + dense_posting_bytes, + }) + } + + fn include_linear_fragments( + index: &mut HashMap, Vec>, + source_slots: &[u32], + sector_id: u32, + ) { + for start in 0..source_slots.len() { + for end in start + 1..=source_slots.len() { + Self::include_fragment(index, &source_slots[start..end], sector_id); + } + } + } + + fn include_cyclic_fragments( + index: &mut HashMap, Vec>, + source_slots: &[u32], + sector_id: u32, + ) { + for start in 0..source_slots.len() { + let mut fragment = Vec::with_capacity(source_slots.len()); + for offset in 0..source_slots.len() { + fragment.push(source_slots[(start + offset) % source_slots.len()]); + Self::include_fragment(index, &fragment, sector_id); + } + } + } + + fn include_fragment( + index: &mut HashMap, Vec>, + source_slots: &[u32], + sector_id: u32, + ) { + if let Some(sector_ids) = index.get_mut(source_slots) { + if sector_ids.last().copied() != Some(sector_id) { + sector_ids.push(sector_id); + } + } else { + index.insert(source_slots.to_vec().into_boxed_slice(), vec![sector_id]); + } + } + + fn freeze_posting_index( + index: HashMap, Vec>, + sector_count: usize, + ) -> HashMap, SectorPosting> { + index + .into_iter() + .map(|(fragment, sector_ids)| { + ( + fragment, + SectorPosting::from_sorted_unique_sector_ids(sector_ids, sector_count), + ) + }) + .collect() + } +} + +#[cfg(test)] +fn component_can_embed(partial: &LCColorComponent, target: &LCColorComponent) -> bool { + if partial.kind() == LCColorComponentKind::Trace { + return target.kind() == LCColorComponentKind::Trace + && partial.source_slots().len() == target.source_slots().len() + && cyclic_word_contains(target.source_slots(), partial.source_slots()); + } + match target.kind() { + LCColorComponentKind::Trace => { + cyclic_word_contains(target.source_slots(), partial.source_slots()) + } + LCColorComponentKind::OpenString | LCColorComponentKind::AdjointSegment => { + linear_word_contains(target.source_slots(), partial.source_slots()) + } + } +} + +#[cfg(test)] +fn linear_word_contains(target: &[u32], partial: &[u32]) -> bool { + !partial.is_empty() + && partial.len() <= target.len() + && target + .windows(partial.len()) + .any(|window| window == partial) +} + +#[cfg(test)] +fn cyclic_word_contains(target: &[u32], partial: &[u32]) -> bool { + !partial.is_empty() + && partial.len() <= target.len() + && (0..target.len()).any(|start| { + (0..partial.len()) + .all(|offset| target[(start + offset) % target.len()] == partial[offset]) + }) +} + +pub(super) struct TemplateCatalog<'a> { + input: &'a OwnedRecurrenceTemplateInput, + strings: Vec<&'a str>, + digests: Vec, + factors: Vec, + coupling_names: Vec<&'a str>, +} + +impl<'a> TemplateCatalog<'a> { + pub(super) fn new(input: &'a OwnedRecurrenceTemplateInput) -> RusticolResult { + let strings = decode_strings( + &input.string_ranges, + &input.string_bytes, + "recurrence template string", + )?; + let digests = input + .digest_catalog + .iter() + .map(|row| SemanticDigest::new(row.value)) + .collect::>>()?; + let factors = decode_template_factors(input, &strings)?; + let coupling_names = input + .coupling_order_terms + .iter() + .map(|row| required_string(&strings, row.name_string_id, "coupling-order name")) + .collect::>>()? + .into_iter() + .collect(); + Ok(Self { + input, + strings, + digests, + factors, + coupling_names, + }) + } + + pub(super) fn string(&self, id: u32, label: &str) -> RusticolResult<&'a str> { + required_string(&self.strings, id, label) + } + + pub(super) fn digest(&self, id: u32, label: &str) -> RusticolResult { + self.digests + .get(id as usize) + .copied() + .ok_or_else(|| invalid(format!("{label} digest {id} is absent"))) + } + + pub(super) fn factor(&self, id: u32, label: &str) -> RusticolResult { + self.factors + .get(id as usize) + .copied() + .ok_or_else(|| invalid(format!("{label} factor {id} is absent"))) + } + + pub(super) fn u32_sequence(&self, id: u32, label: &str) -> RusticolResult<&'a [u32]> { + indexed_sequence( + &self.input.u32_sequence_ranges, + &self.input.u32_sequence_values, + id, + label, + ) + } + + pub(super) fn i32_sequence(&self, id: u32, label: &str) -> RusticolResult<&'a [i32]> { + indexed_sequence( + &self.input.i32_sequence_ranges, + &self.input.i32_sequence_values, + id, + label, + ) + } + + pub(super) fn flavour_flow(&self, id: u32, label: &str) -> RusticolResult<&'a [i32]> { + indexed_sequence( + &self.input.flavour_flow_ranges, + &self.input.flavour_flow_values, + id, + label, + ) + } + + pub(super) fn source_seed(&self, row: SourceRow) -> RusticolResult { + let operation = LCColorSourceSeedOperation::try_from(row.lc_color_seed_operation)?; + let kind = (row.lc_color_seed_component_kind != u8::MAX) + .then(|| LCColorComponentKind::try_from(row.lc_color_seed_component_kind)) + .transpose()?; + LCColorSourceSeed::new( + operation, + row.lc_color_seed_shape_string_id, + kind, + LCColorComponentRole::try_from(row.lc_color_seed_component_role)?, + self.digest(row.lc_color_seed_proof_digest_id, "source color proof")?, + ) + } + + pub(super) fn witness( + &self, + row: LCColorTransitionWitnessRow, + ) -> RusticolResult { + let permutation = match row.input_permutation { + 0 => [0, 1], + 1 => [1, 0], + value => return Err(invalid(format!("invalid LC witness permutation {value}"))), + }; + let kind = (row.result_component_kind != u8::MAX) + .then(|| LCColorComponentKind::try_from(row.result_component_kind)) + .transpose()?; + let shape = + (row.result_shape_string_id != MISSING_U32).then_some(row.result_shape_string_id); + let pairing_values = self.u32_sequence( + row.input_port_pairing_sequence_id, + "LC witness input-port pairings", + )?; + if pairing_values.len() % 4 != 0 { + return Err(invalid( + "LC witness input-port pairing sequence is not divisible by four", + )); + } + let input_pairings = pairing_values + .chunks_exact(4) + .map(|chunk| { + Ok([ + LCColorParentPort::new( + u8::try_from(chunk[0]) + .map_err(|_| invalid("LC witness parent index exceeds u8"))?, + u8::try_from(chunk[1]) + .map_err(|_| invalid("LC witness local port index exceeds u8"))?, + )?, + LCColorParentPort::new( + u8::try_from(chunk[2]) + .map_err(|_| invalid("LC witness parent index exceeds u8"))?, + u8::try_from(chunk[3]) + .map_err(|_| invalid("LC witness local port index exceeds u8"))?, + )?, + ]) + }) + .collect::>>()?; + let binding_values = self.u32_sequence( + row.result_port_binding_sequence_id, + "LC witness result-port bindings", + )?; + if binding_values.len() % 2 != 0 { + return Err(invalid( + "LC witness result-port binding sequence is not divisible by two", + )); + } + let result_port_bindings = binding_values + .chunks_exact(2) + .map(|chunk| { + LCColorParentPort::new( + u8::try_from(chunk[0]) + .map_err(|_| invalid("LC witness parent index exceeds u8"))?, + u8::try_from(chunk[1]) + .map_err(|_| invalid("LC witness local port index exceeds u8"))?, + ) + }) + .collect::>>()?; + let component_parent_order = match row.input_permutation { + 0 => [0, 1], + 1 => [1, 0], + value => return Err(invalid(format!("invalid LC witness permutation {value}"))), + }; + LCColorTransitionWitness::new( + permutation, + row.reverse_parent_mask, + LCColorComponentOperation::try_from(row.component_operation)?, + kind, + LCColorComponentRole::try_from(row.result_component_role)?, + shape, + LCColorPortWiring::new(component_parent_order, input_pairings, result_port_bindings)?, + self.factor(row.exact_factor_id, "LC witness")?, + self.digest(row.proof_digest_id, "LC witness proof")?, + ) + } + + pub(super) fn witness_rows( + &self, + color_contraction_id: u32, + ) -> RusticolResult<&'a [LCColorTransitionWitnessRow]> { + let row = self + .input + .color_contractions + .get(color_contraction_id as usize) + .ok_or_else(|| invalid("color-contraction template is absent"))?; + let range = CheckedTableRange::new(row.witness_start, row.witness_count).as_usize_range( + self.input.lc_color_transition_witnesses.len(), + "LC witnesses", + )?; + Ok(&self.input.lc_color_transition_witnesses[range]) + } + + pub(super) fn coupling_orders(&self, set_id: u32) -> RusticolResult> { + let range = self + .input + .coupling_order_ranges + .get(set_id as usize) + .ok_or_else(|| invalid(format!("coupling-order set {set_id} is absent")))? + .range + .as_usize_range(self.input.coupling_order_terms.len(), "coupling-order set")?; + let mut result = vec![0_u32; self.coupling_names.len()]; + for term in &self.input.coupling_order_terms[range] { + let name = self.string(term.name_string_id, "coupling-order term")?; + let index = self + .coupling_names + .binary_search(&name) + .map_err(|_| invalid("coupling-order name disappeared"))?; + result[index] = term.power; + } + Ok(result) + } + + pub(super) fn coupling_order_dimension(&self) -> usize { + self.coupling_names.len() + } + + #[allow(dead_code)] + pub(super) fn coupling_order_names(&self) -> &[&'a str] { + &self.coupling_names + } +} + +struct ProcessCatalog<'a> { + input: &'a OwnedRecurrenceProcessInput, + strings: Vec<&'a str>, + factors: Vec, +} + +impl<'a> ProcessCatalog<'a> { + fn new(input: &'a OwnedRecurrenceProcessInput) -> RusticolResult { + let strings = decode_strings( + &input.string_ranges, + &input.string_bytes, + "recurrence process string", + )?; + let factors = decode_process_factors(input, &strings)?; + Ok(Self { + input, + strings, + factors, + }) + } + + fn string(&self, id: u32, label: &str) -> RusticolResult<&'a str> { + required_string(&self.strings, id, label) + } + + fn factor(&self, id: u32, label: &str) -> RusticolResult { + self.factors + .get(id as usize) + .copied() + .ok_or_else(|| invalid(format!("{label} factor {id} is absent"))) + } + + fn u32_sequence(&self, id: u32, label: &str) -> RusticolResult<&'a [u32]> { + let range = self + .input + .u32_sequence_ranges + .get(id as usize) + .copied() + .ok_or_else(|| invalid(format!("{label} sequence {id} is absent")))?; + let range = range.as_usize_range(self.input.u32_sequence_values.len(), label)?; + Ok(&self.input.u32_sequence_values[range]) + } + + fn public_helicities( + &self, + source_states: &[SourceStateAssignment], + ) -> RusticolResult> { + if source_states.len() != self.input.external_legs.len() { + return Err(invalid( + "resolved helicity does not cover every external source slot", + )); + } + source_states + .iter() + .copied() + .enumerate() + .map(|(source_slot, assignment)| { + if assignment.source_slot() as usize != source_slot { + return Err(invalid( + "resolved-helicity source-state ancestry is not canonical", + )); + } + let leg = self.input.external_legs[source_slot]; + if u64::from(assignment.state_index()) >= leg.source_state_range.count { + return Err(invalid(format!( + "resolved helicity references absent state {} for source slot {source_slot}", + assignment.state_index() + ))); + } + let row_index = leg + .source_state_range + .start + .checked_add(u64::from(assignment.state_index())) + .ok_or_else(|| invalid("resolved-helicity source-state index overflows"))?; + let row_index = usize::try_from(row_index) + .map_err(|_| invalid("resolved-helicity source-state index exceeds usize"))?; + let state = self + .input + .source_states + .get(row_index) + .ok_or_else(|| invalid("resolved-helicity source state is absent"))?; + if state.source_slot as usize != source_slot + || state.state_index != assignment.state_index() + { + return Err(invalid( + "resolved-helicity source-state catalog is inconsistent", + )); + } + Ok(state.public_helicity) + }) + .collect() + } +} + +fn validate_cyclic_trace_closure_anchor_contracts( + process: &OwnedRecurrenceProcessInput, + process_catalog: &ProcessCatalog<'_>, + template: &OwnedRecurrenceTemplateInput, + template_catalog: &TemplateCatalog<'_>, +) -> RusticolResult<()> { + let mut has_cyclic_trace_anchor = false; + for sector in &process.physical_lc_sectors { + if process_catalog.string( + sector.closure_proof_algorithm_string_id, + "closure-anchor proof algorithm", + )? == CYCLIC_TRACE_CLOSURE_ANCHOR_ALGORITHM + { + has_cyclic_trace_anchor = true; + break; + } + } + if !has_cyclic_trace_anchor { + return Ok(()); + } + + // The authenticated process catalog already proves that every source is + // nonfermionic and uses this same, nonempty current-state contract. + let state_template_id = process + .source_states + .first() + .ok_or_else(|| invalid("cyclic trace closure state contract is absent"))? + .current_state_template_id; + let state = template + .current_states + .get(state_template_id as usize) + .copied() + .ok_or_else(|| invalid("cyclic trace closure current state is absent"))?; + let color_shape = template_catalog.string( + state.lc_color_shape_string_id, + "cyclic trace closure current-state color shape", + )?; + if CurrentOrientation::try_from(state.orientation)? != CurrentOrientation::SelfConjugate + || ParticleStatistics::try_from(state.statistics)? != ParticleStatistics::Boson + || state.particle_id != state.anti_particle_id + || state.color_representation != 8 + || color_shape != "adjoint-segment" + || state.auxiliary_kind_string_id != MISSING_U32 + { + return Err(invalid( + "cyclic trace closure anchoring requires one non-auxiliary, bosonic, self-conjugate adjoint state", + )); + } + + let mut has_direct_closure = false; + for closure in template.closures.iter().copied() { + let input_states = template_catalog.u32_sequence( + closure.input_state_sequence_id, + "cyclic trace closure input states", + )?; + let coupling_order_set = template + .coupling_order_ranges + .get(closure.coupling_order_set_id as usize) + .ok_or_else(|| invalid("cyclic trace closure coupling-order set is absent"))?; + if input_states != [state_template_id, state_template_id] + || closure.result_state_template_id != MISSING_U32 + || !template_catalog + .u32_sequence( + closure.coupling_parameter_sequence_id, + "cyclic trace closure coupling parameters", + )? + .is_empty() + || coupling_order_set.range.count != 0 + || !template_catalog + .u32_sequence( + closure.eligible_quantum_flow_sequence_id, + "cyclic trace closure quantum flows", + )? + .is_empty() + || OutputFactorSource::try_from(closure.output_factor_source)? + != OutputFactorSource::None + || template_catalog.string( + closure.equivalence_class_string_id, + "cyclic trace closure equivalence class", + )? != "direct-contraction" + { + continue; + } + let evaluator = template + .evaluator_bindings + .get(closure.evaluator_binding_id as usize) + .copied() + .ok_or_else(|| invalid("cyclic trace closure evaluator is absent"))?; + if EvaluatorCallableKind::try_from(evaluator.callable_kind)? + != EvaluatorCallableKind::RusticolTemplate + || EvaluatorContractKind::try_from(evaluator.contract_kind)? + != EvaluatorContractKind::Closure + { + continue; + } + let color = template + .color_contractions + .get(closure.color_contraction_template_id as usize) + .copied() + .ok_or_else(|| invalid("cyclic trace closure color contract is absent"))?; + if template_catalog.i32_sequence( + color.input_representation_sequence_id, + "cyclic trace closure input representations", + )? == [8, 8] + && color.has_output_representation == 0 + { + has_direct_closure = true; + break; + } + } + if !has_direct_closure { + return Err(invalid( + "cyclic trace closure anchoring requires an exact direct binary adjoint closure", + )); + } + Ok(()) +} + +/// Construction-only exact support identity. +/// +/// Runtime and persisted current keys continue to carry canonical source-slot +/// vectors. This sidecar uses one machine-friendly scalar for ordinary +/// processes and falls back to an exact arbitrary-width bitset when required. +#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +enum TransientSupportKey { + Inline { + source_count: usize, + bits: u128, + }, + Wide { + source_count: usize, + words: Box<[u64]>, + }, +} + +impl TransientSupportKey { + fn from_source_slots(source_count: usize, source_slots: &[u32]) -> RusticolResult { + if source_count <= u128::BITS as usize { + let mut bits = 0_u128; + for source_slot in source_slots.iter().copied() { + let source_slot = usize::try_from(source_slot) + .map_err(|_| invalid("transient support source slot exceeds usize"))?; + if source_slot >= source_count { + return Err(invalid( + "transient support references an absent source slot", + )); + } + let bit = 1_u128 << source_slot; + if bits & bit != 0 { + return Err(invalid( + "transient support contains a duplicate source slot", + )); + } + bits |= bit; + } + let result = Self::Inline { source_count, bits }; + result.validate()?; + return Ok(result); + } + + let mut words = vec![0_u64; source_count.div_ceil(u64::BITS as usize)]; + for source_slot in source_slots.iter().copied() { + let source_slot = usize::try_from(source_slot) + .map_err(|_| invalid("transient support source slot exceeds usize"))?; + if source_slot >= source_count { + return Err(invalid( + "transient support references an absent source slot", + )); + } + let word = source_slot / u64::BITS as usize; + let bit = 1_u64 << (source_slot % u64::BITS as usize); + if words[word] & bit != 0 { + return Err(invalid( + "transient support contains a duplicate source slot", + )); + } + words[word] |= bit; + } + let result = Self::Wide { + source_count, + words: words.into_boxed_slice(), + }; + result.validate()?; + Ok(result) + } + + fn full(source_count: usize) -> RusticolResult { + if source_count <= u128::BITS as usize { + let bits = match source_count { + 0 => 0, + count if count == u128::BITS as usize => u128::MAX, + count => (1_u128 << count) - 1, + }; + let result = Self::Inline { source_count, bits }; + result.validate()?; + return Ok(result); + } + let mut words = + vec![u64::MAX; source_count.div_ceil(u64::BITS as usize)].into_boxed_slice(); + let trailing_bits = source_count % u64::BITS as usize; + if trailing_bits != 0 { + *words + .last_mut() + .expect("non-inline support has at least three words") = + (1_u64 << trailing_bits) - 1; + } + let result = Self::Wide { + source_count, + words, + }; + result.validate()?; + Ok(result) + } + + fn singleton(source_count: usize, source_slot: u32) -> RusticolResult { + Self::from_source_slots(source_count, &[source_slot]) + } + + const fn source_count(&self) -> usize { + match self { + Self::Inline { source_count, .. } | Self::Wide { source_count, .. } => *source_count, + } + } + + fn validate(&self) -> RusticolResult<()> { + match self { + Self::Inline { source_count, bits } => { + if *source_count > u128::BITS as usize { + return Err(invalid( + "inline transient support exceeds its source domain", + )); + } + let domain_mask = match *source_count { + 0 => 0, + count if count == u128::BITS as usize => u128::MAX, + count => (1_u128 << count) - 1, + }; + if bits & !domain_mask != 0 { + return Err(invalid( + "inline transient support has bits outside its source domain", + )); + } + } + Self::Wide { + source_count, + words, + } => { + if *source_count <= u128::BITS as usize { + return Err(invalid( + "wide transient support does not require wide storage", + )); + } + let expected_word_count = source_count.div_ceil(u64::BITS as usize); + if words.len() != expected_word_count { + return Err(invalid( + "wide transient support word count disagrees with its source domain", + )); + } + let trailing_bits = source_count % u64::BITS as usize; + if trailing_bits != 0 { + let domain_mask = (1_u64 << trailing_bits) - 1; + if words + .last() + .is_some_and(|last_word| last_word & !domain_mask != 0) + { + return Err(invalid( + "wide transient support has padding bits outside its source domain", + )); + } + } + } + } + Ok(()) + } + + fn without_source_slot(&self, source_slot: u32) -> RusticolResult { + self.validate()?; + let source_slot = usize::try_from(source_slot) + .map_err(|_| invalid("transient support source slot exceeds usize"))?; + if source_slot >= self.source_count() { + return Err(invalid( + "transient support references an absent source slot", + )); + } + match self { + Self::Inline { source_count, bits } => Ok(Self::Inline { + source_count: *source_count, + bits: bits & !(1_u128 << source_slot), + }), + Self::Wide { + source_count, + words, + } => { + let word = source_slot / u64::BITS as usize; + let mut result = words.clone(); + result[word] &= !(1_u64 << (source_slot % u64::BITS as usize)); + Ok(Self::Wide { + source_count: *source_count, + words: result, + }) + } + } + } + + fn cardinality(&self) -> usize { + match self { + Self::Inline { bits, .. } => bits.count_ones() as usize, + Self::Wide { words, .. } => words.iter().map(|word| word.count_ones() as usize).sum(), + } + } + + fn is_disjoint(&self, other: &Self) -> RusticolResult { + self.validate()?; + other.validate()?; + if self.source_count() != other.source_count() { + return Err(invalid( + "transient support keys use inconsistent source domains", + )); + } + match (self, other) { + (Self::Inline { bits: left, .. }, Self::Inline { bits: right, .. }) => { + Ok(left & right == 0) + } + (Self::Wide { words: left, .. }, Self::Wide { words: right, .. }) => Ok(left.len() + == right.len() + && left + .iter() + .zip(right.iter()) + .all(|(left, right)| left & right == 0)), + _ => Err(invalid( + "transient support representations disagree for one source domain", + )), + } + } + + fn union_disjoint(&self, other: &Self) -> RusticolResult { + if !self.is_disjoint(other)? { + return Err(invalid( + "recurrence parents have overlapping source support", + )); + } + let result = match (self, other) { + ( + Self::Inline { + source_count, + bits: left, + }, + Self::Inline { bits: right, .. }, + ) => Self::Inline { + source_count: *source_count, + bits: left | right, + }, + ( + Self::Wide { + source_count, + words: left, + }, + Self::Wide { words: right, .. }, + ) => Self::Wide { + source_count: *source_count, + words: left + .iter() + .zip(right.iter()) + .map(|(left, right)| left | right) + .collect::>() + .into_boxed_slice(), + }, + _ => Err(invalid( + "transient support representations use inconsistent source domains", + ))?, + }; + result.validate()?; + Ok(result) + } + + #[cfg(test)] + fn source_slots(&self) -> Vec { + match self { + Self::Inline { source_count, bits } => (0..*source_count) + .filter(|slot| bits & (1_u128 << slot) != 0) + .map(|slot| u32::try_from(slot).expect("test support source slot fits u32")) + .collect(), + Self::Wide { + source_count, + words, + } => words + .iter() + .copied() + .enumerate() + .flat_map(|(word_index, word)| { + (0..u64::BITS).filter_map(move |bit_index| { + let source_slot = word_index * u64::BITS as usize + bit_index as usize; + (source_slot < *source_count && word & (1_u64 << bit_index) != 0).then_some( + u32::try_from(source_slot).expect("test support source slot fits u32"), + ) + }) + }) + .collect(), + } + } +} + +#[derive(Debug)] +struct TransientCurrentSupportKeys { + source_count: usize, + keys_by_current: Vec, +} + +impl TransientCurrentSupportKeys { + fn from_currents(source_count: usize, currents: &[PendingCurrent]) -> RusticolResult { + let keys_by_current = currents + .iter() + .map(|current| { + TransientSupportKey::from_source_slots( + source_count, + current.key.support_source_slots(), + ) + }) + .collect::>>()?; + Ok(Self { + source_count, + keys_by_current, + }) + } + + fn get(&self, current_id: u32) -> RusticolResult<&TransientSupportKey> { + self.keys_by_current + .get(current_id as usize) + .ok_or_else(|| invalid("transient current support key is absent")) + } + + fn push(&mut self, current_id: u32, support_key: TransientSupportKey) -> RusticolResult<()> { + if current_id as usize != self.keys_by_current.len() { + return Err(invalid( + "transient current support keys are not dense by current ID", + )); + } + self.keys_by_current.push(support_key); + Ok(()) + } + + fn reconcile_stage_tail( + &mut self, + stage_start: usize, + currents: &[PendingCurrent], + ) -> RusticolResult<()> { + if stage_start > currents.len() || stage_start > self.keys_by_current.len() { + return Err(invalid( + "transient current support reconciliation has an invalid stage boundary", + )); + } + self.keys_by_current.truncate(stage_start); + for current in ¤ts[stage_start..] { + self.keys_by_current + .push(TransientSupportKey::from_source_slots( + self.source_count, + current.key.support_source_slots(), + )?); + } + if self.keys_by_current.len() != currents.len() { + return Err(invalid( + "transient current support reconciliation is not dense", + )); + } + Ok(()) + } +} + +#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] +struct StructuralState { + state_template_id: u32, + spin_state_class: i32, +} + +impl StructuralState { + const fn new(state_template_id: u32, spin_state_class: i32) -> Self { + Self { + state_template_id, + spin_state_class, + } + } + + fn from_current(current: &PendingCurrent) -> Self { + Self::new( + current.key.current_state_template_id(), + current.key.spin_state_class(), + ) + } +} + +#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] +struct StructuralTransition { + parents: [StructuralState; 2], + result: StructuralState, +} + +fn structural_state_matches(required: StructuralState, actual: StructuralState) -> bool { + required.state_template_id == actual.state_template_id + && (required.spin_state_class == actual.spin_state_class + || actual.spin_state_class == DYNAMIC_UNION_SOURCE_SPIN_STATE_CLASS) +} + +fn structural_parent_states_match( + required: [StructuralState; 2], + actual: [StructuralState; 2], +) -> bool { + structural_state_matches(required[0], actual[0]) + && structural_state_matches(required[1], actual[1]) +} + +/// Allocation-heavy reference implementation retained for exact-equivalence +/// tests of the split production indexes below. +#[cfg(test)] +#[derive(Debug)] +struct LazyStructuralDemandIndex { + demanded: BTreeSet<(Vec, StructuralState)>, +} + +#[cfg(test)] +impl LazyStructuralDemandIndex { + fn new( + process: &OwnedRecurrenceProcessInput, + template: &OwnedRecurrenceTemplateInput, + catalog: &TemplateCatalog<'_>, + prepared_transitions: &PreparedTransitionCatalog, + materialized_sectors: &BTreeSet, + source_currents: &[PendingCurrent], + ) -> RusticolResult { + let transitions = prepared_transitions.structural_transitions(); + let mut feasible = BTreeMap::, BTreeSet>::new(); + for current in source_currents + .iter() + .filter(|current| current.key.node_kind() == RecurrenceNodeKind::Source) + { + feasible + .entry(current.key.support_source_slots().to_vec()) + .or_default() + .insert(StructuralState::from_current(current)); + } + if feasible.is_empty() { + return Err(invalid( + "recurrence structural-demand construction has no source states", + )); + } + + let source_count = process.external_legs.len(); + for target_size in 2..source_count { + let prior = feasible + .iter() + .filter(|(support, _)| support.len() < target_size) + .map(|(support, states)| { + (support.clone(), states.iter().copied().collect::>()) + }) + .collect::>(); + let mut additions = BTreeMap::, BTreeSet>::new(); + for left_index in 0..prior.len() { + for right_index in left_index + 1..prior.len() { + let (left_support, left_states) = &prior[left_index]; + let (right_support, right_states) = &prior[right_index]; + if left_support.len() + right_support.len() != target_size + || !disjoint_support(left_support, right_support) + { + continue; + } + let support = merged_support(left_support, right_support)?; + for left in left_states.iter().copied() { + for right in right_states.iter().copied() { + for transition in transitions { + if structural_parent_states_match(transition.parents, [left, right]) + || structural_parent_states_match( + transition.parents, + [right, left], + ) + { + additions + .entry(support.clone()) + .or_default() + .insert(transition.result); + } + } + } + } + } + } + for (support, states) in additions { + feasible.entry(support).or_default().extend(states); + } + } + + let full_support = (0..source_count as u32).collect::>(); + let mut demanded = BTreeSet::<(Vec, StructuralState)>::new(); + for sector_id in materialized_sectors.iter().copied() { + let sector = process + .physical_lc_sectors + .get(sector_id as usize) + .copied() + .ok_or_else(|| invalid("structural-demand LC sector is absent"))?; + let anchor_support = vec![sector.closure_source_slot]; + let complement_support = full_support + .iter() + .copied() + .filter(|slot| *slot != sector.closure_source_slot) + .collect::>(); + let anchor_states = feasible + .get(&anchor_support) + .ok_or_else(|| invalid("structural-demand closure anchor is infeasible"))?; + let complement_states = feasible + .get(&complement_support) + .ok_or_else(|| invalid("structural-demand closure complement is infeasible"))?; + let mut sector_has_root = false; + for anchor in anchor_states.iter().copied() { + for complement in complement_states.iter().copied() { + for closure in &template.closures { + let closure_states = catalog.u32_sequence( + closure.input_state_sequence_id, + "structural-demand closure input states", + )?; + if closure_states.len() != 2 { + return Err(invalid( + "direct recurrence requires binary prepared closures", + )); + } + let ordered = if closure_states + == [complement.state_template_id, anchor.state_template_id] + { + Some([complement, anchor]) + } else if closure_states + == [anchor.state_template_id, complement.state_template_id] + && anchor.state_template_id != complement.state_template_id + { + Some([anchor, complement]) + } else { + None + }; + let Some(ordered) = ordered else { + continue; + }; + if structural_closure_admits(*closure, ordered, template, catalog)? { + demanded.insert((complement_support.clone(), complement)); + sector_has_root = true; + } + } + } + } + if !sector_has_root { + return Err(invalid(format!( + "recurrence structural-demand grammar found no closure root for LC sector {sector_id}" + ))); + } + } + + for target_size in (2..source_count).rev() { + let targets = demanded + .iter() + .filter(|(support, _)| support.len() == target_size) + .cloned() + .collect::>(); + for (target_support, target_state) in targets { + let feasible_rows = feasible.iter().collect::>(); + for left_index in 0..feasible_rows.len() { + for right_index in left_index + 1..feasible_rows.len() { + let (left_support, left_states) = feasible_rows[left_index]; + let (right_support, right_states) = feasible_rows[right_index]; + if left_support.len() + right_support.len() != target_size + || !disjoint_support(left_support, right_support) + || merged_support(left_support, right_support)? != target_support + { + continue; + } + for left in left_states.iter().copied() { + for right in right_states.iter().copied() { + for transition in transitions + .iter() + .filter(|transition| transition.result == target_state) + { + if structural_parent_states_match( + transition.parents, + [left, right], + ) || structural_parent_states_match( + transition.parents, + [right, left], + ) { + demanded.insert((left_support.clone(), left)); + demanded.insert((right_support.clone(), right)); + } + } + } + } + } + } + } + } + + Ok(Self { demanded }) + } + + fn accepts( + &self, + support_source_slots: &[u32], + state_template_id: u32, + spin_state_class: i32, + ) -> bool { + self.demanded.contains(&( + support_source_slots.to_vec(), + StructuralState::new(state_template_id, spin_state_class), + )) + } +} + +/// Process-global forward state/spin grammar. +/// +/// This half is independent of the materialized color lane. It is built once +/// from source states and the prepared transition catalog, then shared by all +/// lane-specific backward-demand passes. Together those passes form a +/// conservative exact filter: color, coupling, flavour, pairing, and proof +/// contracts remain authenticated by normal construction, while a full +/// current is never interned for a state/spin branch that cannot close. +#[derive(Debug)] +struct StructuralFeasibilityIndex { + source_count: usize, + feasible: BTreeMap>, + decompositions: BTreeMap>, + forward_transition_probe_count: usize, +} + +impl StructuralFeasibilityIndex { + fn new( + source_count: usize, + prepared_transitions: &PreparedTransitionCatalog, + source_currents: &[PendingCurrent], + ) -> RusticolResult { + let transitions = prepared_transitions.structural_transitions(); + let mut feasible = BTreeMap::>::new(); + for current in source_currents + .iter() + .filter(|current| current.key.node_kind() == RecurrenceNodeKind::Source) + { + feasible + .entry(TransientSupportKey::from_source_slots( + source_count, + current.key.support_source_slots(), + )?) + .or_default() + .insert(StructuralState::from_current(current)); + } + if feasible.is_empty() { + return Err(invalid( + "recurrence structural-demand construction has no source states", + )); + } + + let mut decompositions = + BTreeMap::>::new(); + let mut forward_transition_probe_count = 0usize; + for target_size in 2..source_count { + let prior = feasible + .iter() + .filter(|(support, _)| support.cardinality() < target_size) + .map(|(support, states)| { + (support.clone(), states.iter().copied().collect::>()) + }) + .collect::>(); + let mut additions = BTreeMap::>::new(); + for left_index in 0..prior.len() { + for right_index in left_index + 1..prior.len() { + let (left_support, left_states) = &prior[left_index]; + let (right_support, right_states) = &prior[right_index]; + if left_support.cardinality() + right_support.cardinality() != target_size + || !left_support.is_disjoint(right_support)? + { + continue; + } + let support = left_support.union_disjoint(right_support)?; + decompositions + .entry(support.clone()) + .or_default() + .push([left_support.clone(), right_support.clone()]); + for left in left_states.iter().copied() { + for right in right_states.iter().copied() { + for transition in transitions { + forward_transition_probe_count = + forward_transition_probe_count.checked_add(1).ok_or_else( + || { + invalid( + "structural forward transition-probe count exceeds usize", + ) + }, + )?; + if structural_parent_states_match(transition.parents, [left, right]) + || structural_parent_states_match( + transition.parents, + [right, left], + ) + { + additions + .entry(support.clone()) + .or_default() + .insert(transition.result); + } + } + } + } + } + } + for (support, states) in additions { + feasible.entry(support).or_default().extend(states); + } + } + + let result = Self { + source_count, + feasible, + decompositions, + forward_transition_probe_count, + }; + debug_assert_eq!(result.feasible_support_count(), result.feasible.len()); + debug_assert_eq!( + result.decomposition_count(), + result.decompositions.values().map(Vec::len).sum::() + ); + debug_assert_eq!( + result.forward_transition_probe_count(), + forward_transition_probe_count + ); + Ok(result) + } + + fn feasible_states(&self, support: &TransientSupportKey) -> Option<&BTreeSet> { + self.feasible.get(support) + } + + fn decompositions(&self, support: &TransientSupportKey) -> &[[TransientSupportKey; 2]] { + self.decompositions + .get(support) + .map(Vec::as_slice) + .unwrap_or_default() + } + + fn feasible_support_count(&self) -> usize { + self.feasible.len() + } + + fn decomposition_count(&self) -> usize { + self.decompositions.values().map(Vec::len).sum() + } + + fn forward_transition_probe_count(&self) -> usize { + self.forward_transition_probe_count + } +} + +#[derive(Debug)] +struct StructuralDemandIndex { + demanded: BTreeMap>, +} + +impl StructuralDemandIndex { + fn new( + process: &OwnedRecurrenceProcessInput, + template: &OwnedRecurrenceTemplateInput, + catalog: &TemplateCatalog<'_>, + prepared_transitions: &PreparedTransitionCatalog, + feasibility: &StructuralFeasibilityIndex, + materialized_sectors: &BTreeSet, + ) -> RusticolResult { + if process.external_legs.len() != feasibility.source_count { + return Err(invalid( + "structural-demand source domain disagrees with forward feasibility", + )); + } + let transitions = prepared_transitions.structural_transitions(); + let full_support = TransientSupportKey::full(feasibility.source_count)?; + let mut demanded = BTreeMap::>::new(); + for sector_id in materialized_sectors.iter().copied() { + let sector = process + .physical_lc_sectors + .get(sector_id as usize) + .copied() + .ok_or_else(|| invalid("structural-demand LC sector is absent"))?; + let anchor_support = TransientSupportKey::singleton( + feasibility.source_count, + sector.closure_source_slot, + )?; + let complement_support = + full_support.without_source_slot(sector.closure_source_slot)?; + let anchor_states = feasibility + .feasible_states(&anchor_support) + .ok_or_else(|| invalid("structural-demand closure anchor is infeasible"))?; + let complement_states = feasibility + .feasible_states(&complement_support) + .ok_or_else(|| invalid("structural-demand closure complement is infeasible"))?; + let mut sector_has_root = false; + for anchor in anchor_states.iter().copied() { + for complement in complement_states.iter().copied() { + for closure in &template.closures { + let closure_states = catalog.u32_sequence( + closure.input_state_sequence_id, + "structural-demand closure input states", + )?; + if closure_states.len() != 2 { + return Err(invalid( + "direct recurrence requires binary prepared closures", + )); + } + let ordered = if closure_states + == [complement.state_template_id, anchor.state_template_id] + { + Some([complement, anchor]) + } else if closure_states + == [anchor.state_template_id, complement.state_template_id] + && anchor.state_template_id != complement.state_template_id + { + Some([anchor, complement]) + } else { + None + }; + let Some(ordered) = ordered else { + continue; + }; + if structural_closure_admits(*closure, ordered, template, catalog)? { + demanded + .entry(complement_support.clone()) + .or_default() + .insert(complement); + sector_has_root = true; + } + } + } + } + if !sector_has_root { + return Err(invalid(format!( + "recurrence structural-demand grammar found no closure root for LC sector {sector_id}" + ))); + } + } + + for target_size in (2..feasibility.source_count).rev() { + let targets = demanded + .iter() + .filter(|(support, _)| support.cardinality() == target_size) + .flat_map(|(support, states)| { + states.iter().copied().map(|state| (support.clone(), state)) + }) + .collect::>(); + for (target_support, target_state) in targets { + for [left_support, right_support] in feasibility.decompositions(&target_support) { + let left_states = feasibility + .feasible_states(left_support) + .expect("forward decomposition left support is feasible"); + let right_states = feasibility + .feasible_states(right_support) + .expect("forward decomposition right support is feasible"); + for left in left_states.iter().copied() { + for right in right_states.iter().copied() { + for transition in transitions + .iter() + .filter(|transition| transition.result == target_state) + { + if structural_parent_states_match(transition.parents, [left, right]) + || structural_parent_states_match( + transition.parents, + [right, left], + ) + { + demanded + .entry(left_support.clone()) + .or_default() + .insert(left); + demanded + .entry(right_support.clone()) + .or_default() + .insert(right); + } + } + } + } + } + } + } + + Ok(Self { demanded }) + } + + fn accepts( + &self, + support: &TransientSupportKey, + state_template_id: u32, + spin_state_class: i32, + ) -> bool { + self.demanded.get(support).is_some_and(|states| { + states.contains(&StructuralState::new(state_template_id, spin_state_class)) + }) + } + + fn demanded_support_count(&self) -> usize { + self.demanded.len() + } + + fn demanded_state_count(&self) -> usize { + self.demanded.values().map(BTreeSet::len).sum() + } + + #[cfg(test)] + fn demanded_rows(&self) -> BTreeSet<(Vec, StructuralState)> { + self.demanded + .iter() + .flat_map(|(support, states)| { + states + .iter() + .copied() + .map(|state| (support.source_slots(), state)) + }) + .collect() + } +} + +#[cfg(test)] +fn structural_transitions( + template: &OwnedRecurrenceTemplateInput, + catalog: &TemplateCatalog<'_>, +) -> RusticolResult> { + let transitions = template + .transitions + .iter() + .map(|transition| { + let quantum = template + .quantum_flows + .get(transition.quantum_flow_template_id as usize) + .copied() + .ok_or_else(|| invalid("structural-demand quantum flow is absent"))?; + let states = + catalog.u32_sequence(quantum.input_state_sequence_id, "quantum input states")?; + let spins = + catalog.i32_sequence(quantum.input_spin_sequence_id, "quantum input spins")?; + if states.len() != 2 || spins.len() != 2 { + return Err(invalid( + "direct recurrence requires binary quantum-flow contracts", + )); + } + if quantum.result_state_template_id != transition.result_state_template_id { + return Err(invalid( + "structural-demand transition and quantum-flow result states differ", + )); + } + Ok(StructuralTransition { + parents: [ + StructuralState::new(states[0], spins[0]), + StructuralState::new(states[1], spins[1]), + ], + result: StructuralState::new( + quantum.result_state_template_id, + quantum.result_spin_state, + ), + }) + }) + .collect::>>()?; + Ok(transitions.into_iter().collect()) +} + +fn structural_closure_admits( + closure: ClosureRow, + ordered_parents: [StructuralState; 2], + template: &OwnedRecurrenceTemplateInput, + catalog: &TemplateCatalog<'_>, +) -> RusticolResult { + let eligible = catalog.u32_sequence( + closure.eligible_quantum_flow_sequence_id, + "structural-demand closure quantum flows", + )?; + if eligible.is_empty() { + return Ok(true); + } + for quantum_id in eligible { + let quantum = template + .quantum_flows + .get(*quantum_id as usize) + .copied() + .ok_or_else(|| invalid("structural-demand closure quantum flow is absent"))?; + let states = + catalog.u32_sequence(quantum.input_state_sequence_id, "quantum input states")?; + let spins = catalog.i32_sequence(quantum.input_spin_sequence_id, "quantum input spins")?; + if states.len() != 2 || spins.len() != 2 { + return Err(invalid( + "direct recurrence requires binary quantum-flow contracts", + )); + } + if structural_parent_states_match( + [ + StructuralState::new(states[0], spins[0]), + StructuralState::new(states[1], spins[1]), + ], + ordered_parents, + ) { + return Ok(true); + } + } + Ok(false) +} + +fn compatible_pairing_rules_for_current( + pairing_catalog: Option>, + template: &OwnedRecurrenceTemplateInput, + current_state_template_id: u32, + support_source_slots: &[u32], +) -> RusticolResult> { + let Some(pairing_catalog) = pairing_catalog else { + return Ok(BTreeSet::new()); + }; + let state = template + .current_states + .get(current_state_template_id as usize) + .ok_or_else(|| invalid("pairing support references an absent current state"))?; + let carries_colored_fermion_line = state.statistics == 1 && state.color_representation != 1; + let support = support_source_slots + .iter() + .copied() + .collect::>(); + let mut result = BTreeSet::new(); + for rule in pairing_catalog.rules().iter().copied() { + let crossing_count = pairing_catalog + .endpoint_pairings(rule)? + .iter() + .filter(|pair| { + support.contains(&pair.fundamental_source_slot) + != support.contains(&pair.antifundamental_source_slot) + }) + .count(); + let compatible = if carries_colored_fermion_line { + crossing_count == 1 + } else { + crossing_count == 0 + }; + if compatible { + result.insert(rule.rule_id); + } + } + Ok(result) +} + +fn realized_pairing_rules_for_transition( + compatible_result_rules: BTreeSet, + parent_rules: [&BTreeSet; 2], +) -> BTreeSet { + compatible_result_rules + .into_iter() + .filter(|rule_id| parent_rules[0].contains(rule_id) && parent_rules[1].contains(rule_id)) + .collect() +} + +pub(super) fn build_recurrence_program( + authenticated: &AuthenticatedRecurrenceBuilderInput, +) -> RusticolResult { + build_recurrence_program_impl(authenticated, &mut |_| Ok(()), false).map(|(program, _)| program) +} + +pub(super) fn build_recurrence_program_with_progress( + authenticated: &AuthenticatedRecurrenceBuilderInput, + progress: &mut dyn FnMut(RecurrenceBuildProgress) -> RusticolResult<()>, +) -> RusticolResult { + build_recurrence_program_impl(authenticated, progress, false).map(|(program, _)| program) +} + +pub(super) fn build_recurrence_program_with_progress_and_telemetry( + authenticated: &AuthenticatedRecurrenceBuilderInput, + progress: &mut dyn FnMut(RecurrenceBuildProgress) -> RusticolResult<()>, +) -> RusticolResult<(RecurrenceProgram, RecurrenceGenerationTelemetry)> { + build_recurrence_program_impl(authenticated, progress, true) +} + +fn established_fermion_ordering_context( + process: &OwnedRecurrenceProcessInput, + template: &OwnedRecurrenceTemplateInput, + pairing_catalog: Option>, +) -> RusticolResult { + let mut authenticated_colored_endpoints = vec![false; process.external_legs.len()]; + if let Some(pairing_catalog) = pairing_catalog { + for endpoint in pairing_catalog.endpoints().iter().copied() { + let selected = authenticated_colored_endpoints + .get_mut(endpoint.source_slot as usize) + .ok_or_else(|| invalid("fermion-ordering endpoint source slot is absent"))?; + if *selected { + return Err(invalid( + "fermion-ordering endpoint source slot is authenticated twice", + )); + } + *selected = true; + } + } + + let mut source_requires_exterior_sign = Vec::with_capacity(process.external_legs.len()); + for (source_slot, leg) in process.external_legs.iter().copied().enumerate() { + let source_slot = u32::try_from(source_slot) + .map_err(|_| invalid("fermion-ordering source slot exceeds u32"))?; + if leg.source_slot != source_slot { + return Err(invalid( + "fermion-ordering source slot disagrees with the process table", + )); + } + let state_range = leg.source_state_range.as_usize_range( + process.source_states.len(), + "fermion-ordering source-state range", + )?; + let color_representations = process.source_states[state_range] + .iter() + .map(|source_state| { + if source_state.source_slot != source_slot { + return Err(invalid( + "fermion-ordering source state belongs to a different source slot", + )); + } + template + .current_states + .get(source_state.current_state_template_id as usize) + .map(|state| state.color_representation) + .ok_or_else(|| { + invalid("fermion-ordering source current state is absent from the template") + }) + }) + .collect::>>()?; + source_requires_exterior_sign.push(authenticated_source_requires_exterior_sign( + leg.is_fermionic == 1, + authenticated_colored_endpoints[source_slot as usize], + color_representations, + )?); + } + Ok(FermionOrderingContext::new(source_requires_exterior_sign)) +} + +fn build_recurrence_program_impl( + authenticated: &AuthenticatedRecurrenceBuilderInput, + progress: &mut dyn FnMut(RecurrenceBuildProgress) -> RusticolResult<()>, + collect_telemetry: bool, +) -> RusticolResult<(RecurrenceProgram, RecurrenceGenerationTelemetry)> { + #[cfg(feature = "on-the-fly-test-support")] + super::on_the_fly::reject_forbidden_work_if_probed( + super::on_the_fly::OnTheFlyForbiddenWorkV1::EstablishedBuilder, + )?; + let mut telemetry = RecurrenceGenerationTelemetry::default(); + let strategy = authenticated.process().summary().strategy(); + let catalog_digest = authenticated.template().summary().catalog_digest; + let process_input = authenticated.process().input(); + let pairing_catalog = authenticated.process().fermion_pairing_catalog(); + let template_input = authenticated.template().input(); + let fermion_ordering = + established_fermion_ordering_context(process_input, template_input, pairing_catalog)?; + let process_catalog = ProcessCatalog::new(process_input)?; + let helicity_support_rule = helicity_support_rule(authenticated)?; + let global_helicity_flip_rule = global_helicity_flip_rule(authenticated)?; + let retained_helicity_count = retained_helicity_count(process_input)?; + let template_catalog = TemplateCatalog::new(template_input)?; + validate_cyclic_trace_closure_anchor_contracts( + process_input, + &process_catalog, + template_input, + &template_catalog, + )?; + let coupling_limits = coupling_limits(&process_catalog, &template_catalog)?; + let propagators = propagator_by_state(template_input)?; + let transition_reflections = TransitionReflectionIndex::new(template_input, &template_catalog)?; + let replay_targets = build_replay_targets(strategy, process_input, &process_catalog)?; + let materialized_sectors = materialized_sector_ids(strategy, process_input, &replay_targets); + let mut color_states = DynamicLCColorStateInterner::default(); + let mut currents = Vec::::new(); + let mut current_ids = TransientCurrentIdIndex::new(); + let mut currents_by_size = vec![Vec::::new(); process_input.external_legs.len()]; + let mut reflection_certificates = Vec::::new(); + + build_sources( + strategy, + catalog_digest, + process_input, + &process_catalog, + pairing_catalog, + template_input, + &template_catalog, + &mut color_states, + &mut currents, + &mut current_ids, + &mut currents_by_size, + )?; + let stage_count_per_lane = process_input.external_legs.len().saturating_sub(2); + let construction_sector_groups = + construction_sector_groups(strategy, &materialized_sectors, &replay_targets); + let stage_total = stage_count_per_lane + .checked_mul(construction_sector_groups.len()) + .ok_or_else(|| invalid("recurrence construction stage count exceeds usize"))?; + let phase_total = stage_total.saturating_add(3); + progress(RecurrenceBuildProgress::snapshot( + "source construction", + 1, + phase_total, + None, + stage_total, + Some(1), + 0, + None, + currents.len(), + 0, + color_states.len(), + 0, + ))?; + let transition_catalog_started = telemetry_timer(collect_telemetry); + let prepared_transitions = PreparedTransitionCatalog::new(template_input, &template_catalog)?; + let prepared_closures = PreparedClosureCatalog::new(template_input, &template_catalog)?; + let prepared_closure_sectors = PreparedClosureSectorCatalog::new( + strategy, + process_input, + &process_catalog, + &materialized_sectors, + )?; + add_optional_elapsed_nanoseconds( + &mut telemetry.transition_catalog_nanoseconds, + transition_catalog_started, + ); + let source_current_count = currents.len(); + let structural_feasibility_started = telemetry_timer(collect_telemetry); + let structural_feasibility = StructuralFeasibilityIndex::new( + process_input.external_legs.len(), + &prepared_transitions, + ¤ts[..source_current_count], + )?; + add_optional_elapsed_nanoseconds( + &mut telemetry.structural_feasibility_nanoseconds, + structural_feasibility_started, + ); + if collect_telemetry { + telemetry.structural_feasible_support_count = + structural_feasibility.feasible_support_count(); + telemetry.structural_decomposition_count = structural_feasibility.decomposition_count(); + telemetry.structural_forward_transition_probe_count = + structural_feasibility.forward_transition_probe_count(); + } + let mut current_support_keys = + TransientCurrentSupportKeys::from_currents(process_input.external_legs.len(), ¤ts)?; + let source_current_ids = current_ids; + let source_bucket = currents_by_size + .first() + .cloned() + .ok_or_else(|| invalid("recurrence construction has no source bucket"))?; + let isolate_replay_lanes = construction_sector_groups.len() > 1; + let mut resident_contribution_count = 0usize; + let mut stage_diagnostics = Vec::new(); + let mut closures = BTreeMap::new(); + let mut exhaustive_closure_sector_ids = BTreeSet::new(); + let mut exhaustive_closure_sector_owner_ids = BTreeMap::new(); + for (lane_index, construction_sectors) in construction_sector_groups.into_iter().enumerate() { + let stage_index_offset = lane_index + .checked_mul(stage_count_per_lane) + .ok_or_else(|| invalid("recurrence construction stage offset exceeds usize"))?; + let color_target_index_started = telemetry_timer(collect_telemetry); + let color_targets = MaterializedColorTargets::new_with_prepared_sectors( + &construction_sectors, + &prepared_closure_sectors, + collect_telemetry, + )?; + add_optional_elapsed_nanoseconds( + &mut telemetry.color_target_index_nanoseconds, + color_target_index_started, + ); + let structural_demand_started = telemetry_timer(collect_telemetry); + let structural_demands = StructuralDemandIndex::new( + process_input, + template_input, + &template_catalog, + &prepared_transitions, + &structural_feasibility, + &construction_sectors, + )?; + add_optional_elapsed_nanoseconds( + &mut telemetry.structural_demand_nanoseconds, + structural_demand_started, + ); + if collect_telemetry { + checked_diagnostic_add( + &mut telemetry.structural_demand_support_count, + structural_demands.demanded_support_count(), + "telemetry structural-demand support count", + )?; + checked_diagnostic_add( + &mut telemetry.structural_demand_state_count, + structural_demands.demanded_state_count(), + "telemetry structural-demand state count", + )?; + } + let mut lane_current_ids = source_current_ids.clone(); + let mut lane_currents_by_size = vec![Vec::::new(); process_input.external_legs.len()]; + lane_currents_by_size[0] = source_bucket.clone(); + let lane_internal_start = currents.len(); + let mut build_lane = || { + build_internal_currents( + catalog_digest, + process_input, + pairing_catalog, + template_input, + &fermion_ordering, + &prepared_transitions, + &transition_reflections, + &coupling_limits, + &propagators, + &color_targets, + &structural_demands, + &mut color_states, + &mut currents, + &mut lane_current_ids, + &mut lane_currents_by_size, + &mut current_support_keys, + &mut reflection_certificates, + &mut resident_contribution_count, + stage_index_offset, + stage_total, + phase_total, + &mut telemetry, + collect_telemetry, + progress, + ) + }; + #[cfg(feature = "on-the-fly-test-support")] + let lane_stage_diagnostics = + super::diagnostic::with_transition_diagnostic_materialized_sector( + (construction_sectors.len() == 1) + .then(|| construction_sectors.first().copied()) + .flatten(), + build_lane, + )?; + #[cfg(not(feature = "on-the-fly-test-support"))] + let lane_stage_diagnostics = build_lane()?; + if collect_telemetry { + let color_target_telemetry = color_targets.telemetry()?; + for (target, value, label) in [ + ( + &mut telemetry.color_fragment_bucket_count, + color_target_telemetry.fragment_bucket_count, + "telemetry color-fragment bucket count", + ), + ( + &mut telemetry.color_acceptance_cache_hit_count, + color_target_telemetry.acceptance_cache_hit_count, + "telemetry color-acceptance cache-hit count", + ), + ( + &mut telemetry.color_acceptance_cache_miss_count, + color_target_telemetry.acceptance_cache_miss_count, + "telemetry color-acceptance cache-miss count", + ), + ( + &mut telemetry.color_fragment_hash_lookup_count, + color_target_telemetry.fragment_hash_lookup_count, + "telemetry color-fragment hash-lookup count", + ), + ( + &mut telemetry.color_posting_incidence_count, + color_target_telemetry.posting_incidence_count, + "telemetry color-posting incidence count", + ), + ( + &mut telemetry.color_sparse_posting_bucket_count, + color_target_telemetry.sparse_posting_bucket_count, + "telemetry sparse color-posting bucket count", + ), + ( + &mut telemetry.color_dense_posting_bucket_count, + color_target_telemetry.dense_posting_bucket_count, + "telemetry dense color-posting bucket count", + ), + ( + &mut telemetry.color_sparse_posting_bytes, + color_target_telemetry.sparse_posting_bytes, + "telemetry sparse color-posting bytes", + ), + ( + &mut telemetry.color_dense_posting_bytes, + color_target_telemetry.dense_posting_bytes, + "telemetry dense color-posting bytes", + ), + ] { + checked_diagnostic_add(target, value, label)?; + } + debug_assert_eq!( + color_target_telemetry + .acceptance_accept_count + .saturating_add(color_target_telemetry.acceptance_reject_count), + color_target_telemetry + .acceptance_cache_hit_count + .saturating_add(color_target_telemetry.acceptance_cache_miss_count), + ); + } + let lane_internal_end = currents.len(); + let lane_domain = isolate_replay_lanes.then_some(PendingConstructionDomain { + shared_source_end: source_current_count, + lane_internal_start, + lane_internal_end, + }); + let closure_processing_started = telemetry_timer(collect_telemetry); + let lane_closure_domain = build_closures( + strategy, + process_input, + &process_catalog, + template_input, + &fermion_ordering, + pairing_catalog, + &prepared_closures, + &prepared_closure_sectors, + &color_states, + ¤ts, + &construction_sectors, + &lane_stage_diagnostics, + &reflection_certificates, + lane_domain, + &mut telemetry, + collect_telemetry, + )?; + for sector_id in lane_closure_domain.physical_sector_ids { + if !exhaustive_closure_sector_ids.insert(sector_id) { + return Err(invalid( + "recurrence closure construction visited one physical sector twice", + )); + } + } + for (sector_id, owner_id) in lane_closure_domain.physical_sector_owner_ids { + if exhaustive_closure_sector_owner_ids + .insert(sector_id, owner_id) + .is_some() + { + return Err(invalid( + "recurrence closure construction assigned one physical-sector owner twice", + )); + } + } + let lane_closures = lane_closure_domain.groups; + let lane_closures = if lane_domain.is_some() { + retain_supported_pending_closures( + strategy, + &process_catalog, + &replay_targets, + lane_closures, + helicity_support_rule, + global_helicity_flip_rule, + )? + } else { + lane_closures + }; + if let Some(domain) = lane_domain { + prune_inactive_lane_contributions( + &mut currents, + domain, + &lane_closures, + &mut resident_contribution_count, + )?; + } + for (key, group) in lane_closures { + if closures.insert(key, group).is_some() { + return Err(invalid( + "isolated recurrence construction produced a duplicate closure", + )); + } + } + add_optional_elapsed_nanoseconds( + &mut telemetry.closure_processing_nanoseconds, + closure_processing_started, + ); + stage_diagnostics.extend(lane_stage_diagnostics); + } + if pending_contribution_entry_count(¤ts)? != resident_contribution_count { + return Err(invalid( + "resident recurrence contribution count disagrees with pending storage", + )); + } + let color_target_prune_count = stage_diagnostics + .iter() + .map(|stage| stage.color_target_prune_count) + .try_fold(0usize, |total, count| { + total + .checked_add(count) + .ok_or_else(|| invalid("recurrence color-target prune count exceeds usize")) + })?; + progress(RecurrenceBuildProgress::snapshot( + "amplitude closure", + stage_total.saturating_add(2), + phase_total, + None, + stage_total, + None, + 0, + None, + currents.len(), + resident_contribution_count, + color_states.len(), + color_target_prune_count, + ))?; + progress(RecurrenceBuildProgress::snapshot( + "schedule finalization", + phase_total, + phase_total, + None, + stage_total, + None, + 0, + None, + currents.len(), + resident_contribution_count, + color_states.len(), + color_target_prune_count, + ))?; + if collect_telemetry { + telemetry.constructed_current_count = currents.len(); + telemetry.constructed_contribution_count = resident_contribution_count; + telemetry.constructed_interaction_count = resident_contribution_count; + telemetry.constructed_dynamic_color_state_count = color_states.len(); + telemetry.indexed_hash_lookup_count = telemetry + .support_bucket_probe_count + .checked_add(telemetry.color_acceptance_cache_hit_count) + .and_then(|count| count.checked_add(telemetry.color_acceptance_cache_miss_count)) + .and_then(|count| count.checked_add(telemetry.color_fragment_hash_lookup_count)) + .ok_or_else(|| invalid("telemetry indexed hash-lookup count exceeds usize"))?; + } + let canonical_emission_started = telemetry_timer(collect_telemetry); + let program = finish_program( + strategy, + &process_catalog, + color_states.into_states(), + currents, + closures, + exhaustive_closure_sector_ids, + exhaustive_closure_sector_owner_ids, + replay_targets, + retained_helicity_count, + if isolate_replay_lanes { + HelicitySupportRule::None + } else { + helicity_support_rule + }, + if isolate_replay_lanes { + GlobalHelicityFlipRule::None + } else { + global_helicity_flip_rule + }, + reflection_certificates, + )?; + add_optional_elapsed_nanoseconds( + &mut telemetry.canonical_emission_nanoseconds, + canonical_emission_started, + ); + if collect_telemetry { + telemetry.emitted_current_count = program.currents().len(); + telemetry.emitted_contribution_count = program.contributions().len(); + telemetry.emitted_interaction_count = program.contributions().len(); + telemetry.emitted_finalization_count = program.finalizations().len(); + telemetry.emitted_closure_count = program.closure_terms().len(); + } + Ok((program, telemetry)) +} + +#[allow(clippy::too_many_arguments)] +fn build_sources( + strategy: RecurrenceStrategy, + catalog_digest: SemanticDigest, + process: &OwnedRecurrenceProcessInput, + process_catalog: &ProcessCatalog<'_>, + pairing_catalog: Option>, + template: &OwnedRecurrenceTemplateInput, + template_catalog: &TemplateCatalog<'_>, + color_states: &mut DynamicLCColorStateInterner, + currents: &mut Vec, + current_ids: &mut TransientCurrentIdIndex, + currents_by_size: &mut [Vec], +) -> RusticolResult<()> { + let zero_orders = vec![0_u32; template_catalog.coupling_names.len()]; + for leg in &process.external_legs { + let range = leg + .source_state_range + .as_usize_range(process.source_states.len(), "recurrence source-state range")?; + let retained_state_indices = retained_source_state_indices(process, leg.source_slot)?; + let retained_states = retained_state_indices + .into_iter() + .map(|state_index| { + process + .source_states + .get(range.start + state_index as usize) + .ok_or_else(|| invalid("retained recurrence source state is absent")) + }) + .collect::>>()?; + for process_state in &retained_states { + let source = *template + .sources + .get(process_state.source_template_id as usize) + .ok_or_else(|| invalid("source template is absent"))?; + validate_crossed_source_state(leg.is_initial != 0, process_state, source, template)?; + } + + match strategy { + RecurrenceStrategy::TopologyReplay | RecurrenceStrategy::ContractedColorUnion => { + for process_state in retained_states { + let source = *template + .sources + .get(process_state.source_template_id as usize) + .ok_or_else(|| invalid("source template is absent"))?; + let color_id = source_color_state_id( + leg.source_slot, + source, + template, + template_catalog, + color_states, + )?; + let reflection = source_reflection( + color_states, + color_id, + leg.source_slot, + template_catalog.source_seed(source)?.proof_digest(), + )?; + let helicity_identity = match strategy { + RecurrenceStrategy::TopologyReplay => { + CurrentHelicityIdentity::topology_replay( + process_state.spin_state, + vec![SourceStateAssignment::new( + leg.source_slot, + process_state.state_index, + )], + )? + } + RecurrenceStrategy::ContractedColorUnion => { + CurrentHelicityIdentity::contracted_color_union( + process_state.spin_state, + vec![SourceStateAssignment::new( + leg.source_slot, + process_state.state_index, + )], + )? + } + RecurrenceStrategy::AllFlowUnion => unreachable!(), + }; + insert_source_current( + catalog_digest, + process_state.current_state_template_id, + color_id, + leg.source_slot, + process_state.momentum_sign, + helicity_identity, + source, + reflection, + CurrentSourceBinding::FixedTemplate(process_state.source_template_id), + Some(process_catalog.factor( + process_state.crossing_phase_factor_id, + "source crossing phase", + )?), + &zero_orders, + pairing_catalog, + template_catalog, + currents, + current_ids, + currents_by_size, + )?; + } + } + RecurrenceStrategy::AllFlowUnion => { + let groups = union_source_dispatch_groups( + &retained_states, + process_catalog, + template, + template_catalog, + )?; + for group in groups { + let source = group.representative_source; + let color_id = source_color_state_id( + leg.source_slot, + source, + template, + template_catalog, + color_states, + )?; + let reflection = source_reflection( + color_states, + color_id, + leg.source_slot, + template_catalog.source_seed(source)?.proof_digest(), + )?; + insert_source_current( + catalog_digest, + group.full_state_template_id, + color_id, + leg.source_slot, + group.momentum_sign, + CurrentHelicityIdentity::all_flow_union(group.full_spin_state_class), + source, + reflection, + CurrentSourceBinding::runtime_dispatch_with_variants( + group.contract_id, + group.variants, + )?, + None, + &zero_orders, + pairing_catalog, + template_catalog, + currents, + current_ids, + currents_by_size, + )?; + } + } + } + } + Ok(()) +} + +fn source_color_state_id( + source_slot: u32, + source: SourceRow, + template: &OwnedRecurrenceTemplateInput, + template_catalog: &TemplateCatalog<'_>, + color_states: &mut DynamicLCColorStateInterner, +) -> RusticolResult { + let dynamic_state = template_catalog.source_seed(source)?.instantiate( + source_slot, + template + .current_states + .get(source.state_template_id as usize) + .ok_or_else(|| invalid("source current-state template is absent"))? + .color_representation, + )?; + color_states.intern(dynamic_state) +} + +#[derive(Debug)] +struct UnionSourceDispatchGroup { + contract_id: u32, + full_state_template_id: u32, + full_spin_state_class: i32, + momentum_sign: i32, + representative_source: SourceRow, + variants: Vec, +} + +fn union_source_dispatch_groups( + retained_states: &[&ProcessSourceStateRow], + process_catalog: &ProcessCatalog<'_>, + template: &OwnedRecurrenceTemplateInput, + template_catalog: &TemplateCatalog<'_>, +) -> RusticolResult> { + let mut grouped = BTreeMap::< + u32, + ( + RuntimeHelicityContractRow, + Vec<(ProcessSourceStateRow, RuntimeHelicityVariantRow)>, + ), + >::new(); + for process_state in retained_states { + let (contract, variant) = + runtime_helicity_variant_for_source(template, process_state.source_template_id)?; + if variant.source_state_template_id + != template.sources[process_state.source_template_id as usize].state_template_id + { + return Err(invalid(format!( + "runtime-helicity variant {} source-state contract is stale", + variant.id + ))); + } + grouped + .entry(contract.id) + .or_insert_with(|| (contract, Vec::new())) + .1 + .push((**process_state, variant)); + } + if grouped.is_empty() { + return Err(invalid( + "all-flow-union source has no certified runtime-helicity dispatch domain", + )); + } + + grouped + .into_values() + .map(|(contract, mut entries)| { + entries.sort_by_key(|(state, _)| state.state_index); + if entries + .windows(2) + .any(|pair| pair[0].0.state_index == pair[1].0.state_index) + { + return Err(invalid(format!( + "runtime-helicity contract {} has an ambiguous process source-state mapping", + contract.id + ))); + } + let full_state = template + .current_states + .get(contract.full_state_template_id as usize) + .ok_or_else(|| invalid("runtime-helicity full state is absent"))?; + let representative_state = entries[0].0; + let representative_source = + template.sources[representative_state.source_template_id as usize]; + let momentum_sign = representative_state.momentum_sign; + for (process_state, variant) in &entries { + let source = template.sources[process_state.source_template_id as usize]; + validate_union_source_family( + representative_source, + source, + contract, + *variant, + *process_state, + full_state, + template, + template_catalog, + )?; + if process_state.momentum_sign != momentum_sign { + return Err(invalid(format!( + "runtime-helicity contract {} mixes source momentum signs", + contract.id + ))); + } + } + let full_spin_state_class = + union_full_spin_state_class(contract, &entries, template, template_catalog)?; + let variants = entries + .into_iter() + .map(|(process_state, variant)| { + RuntimeSourceVariantBinding::new( + process_state.state_index, + process_state.public_helicity, + variant.id, + variant.source_template_id, + variant.source_state_template_id, + process_state.current_state_template_id, + process_state.spin_state, + process_catalog.factor( + process_state.crossing_phase_factor_id, + "runtime source crossing phase", + )?, + ) + }) + .collect::>>()?; + Ok(UnionSourceDispatchGroup { + contract_id: contract.id, + full_state_template_id: contract.full_state_template_id, + full_spin_state_class, + momentum_sign, + representative_source, + variants, + }) + }) + .collect() +} + +fn runtime_helicity_variant_for_source( + template: &OwnedRecurrenceTemplateInput, + source_template_id: u32, +) -> RusticolResult<(RuntimeHelicityContractRow, RuntimeHelicityVariantRow)> { + let mut found = None; + for contract in template.runtime_helicity_contracts.iter().copied() { + let range = contract.variant_range.as_usize_range( + template.runtime_helicity_variants.len(), + "runtime-helicity variants", + )?; + for variant in template.runtime_helicity_variants[range].iter().copied() { + if variant.source_template_id != source_template_id { + continue; + } + if found.replace((contract, variant)).is_some() { + return Err(invalid(format!( + "source template {source_template_id} belongs to ambiguous runtime-helicity variants" + ))); + } + } + } + found.ok_or_else(|| { + invalid(format!( + "source template {source_template_id} has no certified runtime-helicity variant" + )) + }) +} + +#[allow(clippy::too_many_arguments)] +fn validate_union_source_family( + representative: SourceRow, + source: SourceRow, + contract: RuntimeHelicityContractRow, + variant: RuntimeHelicityVariantRow, + process_state: ProcessSourceStateRow, + full_state: &super::template::CurrentStateRow, + template: &OwnedRecurrenceTemplateInput, + template_catalog: &TemplateCatalog<'_>, +) -> RusticolResult<()> { + if variant.contract_id != contract.id + || variant.source_template_id != source.id + || variant.source_state_template_id != source.state_template_id + { + return Err(invalid(format!( + "runtime-helicity variant {} is inconsistent with source template {}", + variant.id, source.id + ))); + } + let effective = template + .current_states + .get(process_state.current_state_template_id as usize) + .ok_or_else(|| invalid("runtime source effective state is absent"))?; + let full_state_is_compatible = full_state.particle_id == effective.particle_id + && full_state.anti_particle_id == effective.anti_particle_id + && full_state.species_string_id == effective.species_string_id + && full_state.orientation == effective.orientation + && full_state.statistics == effective.statistics + && full_state.color_representation == effective.color_representation + && full_state.lc_color_shape_string_id == effective.lc_color_shape_string_id + && full_state.auxiliary_kind_string_id == effective.auxiliary_kind_string_id + && full_state.mass_parameter_id == effective.mass_parameter_id + && full_state.width_parameter_id == effective.width_parameter_id; + if !full_state_is_compatible { + return Err(invalid(format!( + "runtime-helicity full state {} is not crossing-compatible with process state {}", + contract.full_state_template_id, process_state.current_state_template_id + ))); + } + if !same_union_source_dispatch_semantics(representative, source) + || template_catalog.flavour_flow( + representative.flavour_flow_id, + "runtime source flavour flow", + )? != template_catalog + .flavour_flow(source.flavour_flow_id, "runtime source flavour flow")? + { + return Err(invalid(format!( + "runtime-helicity contract {} mixes incompatible source semantics", + contract.id + ))); + } + Ok(()) +} + +fn same_union_source_dispatch_semantics(left: SourceRow, right: SourceRow) -> bool { + left.crossing_string_id == right.crossing_string_id + && left.wavefunction_family_string_id == right.wavefunction_family_string_id + && left.flavour_flow_id == right.flavour_flow_id + && left.quantum_number_flow_id == right.quantum_number_flow_id + && left.lc_color_seed_operation == right.lc_color_seed_operation + && left.lc_color_seed_shape_string_id == right.lc_color_seed_shape_string_id + && left.lc_color_seed_component_kind == right.lc_color_seed_component_kind + && left.lc_color_seed_component_role == right.lc_color_seed_component_role + && left.lc_color_seed_provenance_sequence_id == right.lc_color_seed_provenance_sequence_id + && left.mass_parameter_id == right.mass_parameter_id + && left.width_parameter_id == right.width_parameter_id +} + +fn union_full_spin_state_class( + contract: RuntimeHelicityContractRow, + entries: &[(ProcessSourceStateRow, RuntimeHelicityVariantRow)], + template: &OwnedRecurrenceTemplateInput, + _catalog: &TemplateCatalog<'_>, +) -> RusticolResult { + let mut result_spins = BTreeSet::new(); + for flow in &template.quantum_flows { + if flow.result_state_template_id == contract.full_state_template_id { + result_spins.insert(flow.result_spin_state); + } + } + if result_spins.is_empty() { + result_spins.extend(entries.iter().map(|(state, _)| state.spin_state)); + } + union_spin_state_class(contract.id, &result_spins) +} + +fn union_spin_state_class(contract_id: u32, result_spins: &BTreeSet) -> RusticolResult { + if result_spins.is_empty() { + return Err(invalid(format!( + "runtime-helicity contract {} has no full-state spin class", + contract_id + ))); + } + if result_spins.contains(&DYNAMIC_UNION_SOURCE_SPIN_STATE_CLASS) { + return Err(invalid(format!( + "runtime-helicity contract {} contains the reserved dynamic source-spin sentinel", + contract_id + ))); + } + Ok(if result_spins.len() == 1 { + *result_spins.iter().next().expect("checked nonempty") + } else { + // One all-flow source current stores the full runtime-helicity vector. + // Its selected chiral embedding is populated at execution time, so a + // static source spin class would incorrectly discard the other + // certified branch during recurrence construction. + DYNAMIC_UNION_SOURCE_SPIN_STATE_CLASS + }) +} + +#[allow(clippy::too_many_arguments)] +fn insert_source_current( + catalog_digest: SemanticDigest, + current_state_template_id: u32, + color_id: super::DynamicLCColorStateId, + source_slot: u32, + momentum_sign: i32, + helicity_identity: CurrentHelicityIdentity, + source: SourceRow, + reflection: CurrentReflection, + source_binding: CurrentSourceBinding, + source_factor: Option, + zero_orders: &[u32], + pairing_catalog: Option>, + template_catalog: &TemplateCatalog<'_>, + currents: &mut Vec, + current_ids: &mut TransientCurrentIdIndex, + currents_by_size: &mut [Vec], +) -> RusticolResult<()> { + let key = CurrentCoreKey::new( + catalog_digest, + RecurrenceNodeKind::Source, + current_state_template_id, + color_id, + vec![source_slot], + CanonicalMomentumLinearForm::new(vec![MomentumTerm { + source_slot, + coefficient: momentum_sign, + }])?, + helicity_identity, + template_catalog + .flavour_flow(source.flavour_flow_id, "source flavour flow")? + .to_vec(), + source.quantum_number_flow_id, + zero_orders.to_vec(), + source_binding, + None, + )?; + let realized_pairing_rule_ids = compatible_pairing_rules_for_current( + pairing_catalog, + template_catalog.input, + current_state_template_id, + &[source_slot], + )?; + if let Some(existing) = current_ids.get(&key).copied() { + if currents[existing as usize].source_exact_factor != source_factor { + return Err(invalid( + "equivalent source currents have different exact factors", + )); + } + currents[existing as usize] + .reflection + .include(reflection.proof().cloned())?; + currents[existing as usize] + .realized_pairing_rule_ids + .extend(realized_pairing_rule_ids); + return Ok(()); + } + let id = u32::try_from(currents.len()) + .map_err(|_| invalid("recurrence current count exceeds u32"))?; + current_ids.insert(key.clone(), id); + currents.push(PendingCurrent { + key, + source_exact_factor: source_factor, + contributions: BTreeMap::new(), + realized_pairing_rule_ids, + reflection, + reflection_certificate_id: None, + }); + currents_by_size[0].push(id); + Ok(()) +} + +pub(super) fn source_reflection( + color_states: &DynamicLCColorStateInterner, + color_id: super::DynamicLCColorStateId, + source_slot: u32, + source_seed_proof: SemanticDigest, +) -> RusticolResult { + let color = color_states + .get(color_id) + .ok_or_else(|| invalid("source dynamic color state disappeared"))?; + Ok(if color.pure_adjoint_word().is_some() { + let color_identity = dynamic_color_identity_digest(color)?; + let root = source_reflection_proof_root(source_slot, source_seed_proof, color_identity)?; + CurrentReflection::Proven(CurrentReflectionProof::new( + ExactComplexRational::ONE, + [root], + color_identity, + )?) + } else { + CurrentReflection::Unavailable + }) +} + +pub(super) fn validate_crossed_source_state( + is_initial: bool, + process_state: &super::process::ProcessSourceStateRow, + source: SourceRow, + template: &OwnedRecurrenceTemplateInput, +) -> RusticolResult<()> { + let canonical = template + .current_states + .get(source.state_template_id as usize) + .ok_or_else(|| invalid("source canonical current-state template is absent"))?; + let effective = template + .current_states + .get(process_state.current_state_template_id as usize) + .ok_or_else(|| invalid("source effective current-state template is absent"))?; + if effective.chirality != process_state.chirality { + return Err(invalid(format!( + "process source chirality {} does not match effective current-state chirality {}", + process_state.chirality, effective.chirality, + ))); + } + let compatible = canonical.particle_id == effective.particle_id + && canonical.anti_particle_id == effective.anti_particle_id + && canonical.species_string_id == effective.species_string_id + && canonical.orientation == effective.orientation + && canonical.statistics == effective.statistics + && canonical.color_representation == effective.color_representation + && canonical.basis_string_id == effective.basis_string_id + && canonical.tensor_ordering_sequence_id == effective.tensor_ordering_sequence_id + && canonical.dimension == effective.dimension + && canonical.lc_color_shape_string_id == effective.lc_color_shape_string_id + && canonical.auxiliary_kind_string_id == effective.auxiliary_kind_string_id + && canonical.mass_parameter_id == effective.mass_parameter_id + && canonical.width_parameter_id == effective.width_parameter_id; + if !compatible || (!is_initial && canonical.id != effective.id) { + return Err(invalid(format!( + "source template {} and effective current-state template {} are not crossing-compatible", + process_state.source_template_id, process_state.current_state_template_id, + ))); + } + Ok(()) +} + +#[cfg(test)] +fn parent_pairs_for_target( + target_size: usize, + prior_currents_by_size: &[Vec], +) -> impl Iterator + '_ { + (1..=target_size / 2).flat_map(move |left_size| { + let right_size = target_size - left_size; + let same_size = left_size == right_size; + prior_currents_by_size[left_size - 1] + .iter() + .copied() + .flat_map(move |left_id| { + prior_currents_by_size[right_size - 1] + .iter() + .copied() + .filter_map(move |right_id| match left_id.cmp(&right_id) { + std::cmp::Ordering::Less => Some([left_id, right_id]), + std::cmp::Ordering::Equal => None, + std::cmp::Ordering::Greater if same_size => None, + std::cmp::Ordering::Greater => Some([right_id, left_id]), + }) + }) + }) +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +struct OrderedCurrentId { + current_id: u32, + bucket_position: usize, +} + +#[derive(Debug, Default)] +struct SupportBucketIndex { + ordered_supports: Vec, + current_ids_by_support: HashMap>, + disjoint_current_cache: HashMap>, + entry_count: usize, +} + +impl SupportBucketIndex { + fn insert(&mut self, current_id: u32, support: &TransientSupportKey) { + use std::collections::hash_map::Entry; + + let ordered = OrderedCurrentId { + current_id, + bucket_position: self.entry_count, + }; + self.entry_count += 1; + match self.current_ids_by_support.entry(support.clone()) { + Entry::Occupied(mut entry) => entry.get_mut().push(ordered), + Entry::Vacant(entry) => { + self.ordered_supports.push(support.clone()); + entry.insert(vec![ordered]); + } + } + self.disjoint_current_cache = HashMap::new(); + } + + fn disjoint_current_ids( + &mut self, + left_support: &TransientSupportKey, + ) -> RusticolResult<(Rc<[OrderedCurrentId]>, bool)> { + if let Some(cached) = self.disjoint_current_cache.get(left_support) { + return Ok((Rc::clone(cached), true)); + } + let mut result = Vec::new(); + // Emission never depends on HashMap iteration order. The support order + // is the first-current order of the original size bucket, and final + // positions restore that bucket's exact current-ID order. + for right_support in &self.ordered_supports { + if !left_support.is_disjoint(right_support)? { + continue; + } + result.extend( + self.current_ids_by_support + .get(right_support) + .expect("ordered support has an ID bucket") + .iter() + .copied(), + ); + } + result.sort_unstable_by_key(|entry| entry.bucket_position); + let result = Rc::<[OrderedCurrentId]>::from(result); + self.disjoint_current_cache + .insert(left_support.clone(), Rc::clone(&result)); + Ok((result, false)) + } + + fn clear_disjoint_cache(&mut self) { + self.disjoint_current_cache = HashMap::new(); + } +} + +#[derive(Debug)] +struct LaneSupportBuckets { + buckets_by_size: Vec, +} + +impl LaneSupportBuckets { + fn new( + currents_by_size: &[Vec], + support_keys: &TransientCurrentSupportKeys, + ) -> RusticolResult { + let mut buckets_by_size = (0..currents_by_size.len()) + .map(|_| SupportBucketIndex::default()) + .collect::>(); + for (size_index, current_ids) in currents_by_size.iter().enumerate() { + for current_id in current_ids.iter().copied() { + buckets_by_size[size_index].insert(current_id, support_keys.get(current_id)?); + } + } + Ok(Self { buckets_by_size }) + } + + fn bucket_mut(&mut self, support_size: usize) -> RusticolResult<&mut SupportBucketIndex> { + self.buckets_by_size + .get_mut( + support_size + .checked_sub(1) + .ok_or_else(|| invalid("transient support bucket size is zero"))?, + ) + .ok_or_else(|| invalid("transient support bucket is absent")) + } + + fn replace_bucket( + &mut self, + support_size: usize, + current_ids: &[u32], + support_keys: &TransientCurrentSupportKeys, + ) -> RusticolResult<()> { + let bucket = self.bucket_mut(support_size)?; + *bucket = SupportBucketIndex::default(); + for current_id in current_ids.iter().copied() { + bucket.insert(current_id, support_keys.get(current_id)?); + } + Ok(()) + } + + fn clear_disjoint_caches(&mut self) { + for bucket in &mut self.buckets_by_size { + bucket.clear_disjoint_cache(); + } + } + + #[cfg(test)] + fn cached_disjoint_query_count(&self) -> usize { + self.buckets_by_size + .iter() + .map(|bucket| bucket.disjoint_current_cache.len()) + .sum() + } + + #[cfg(test)] + fn cached_disjoint_capacity(&self) -> usize { + self.buckets_by_size + .iter() + .map(|bucket| bucket.disjoint_current_cache.capacity()) + .sum() + } +} + +#[derive(Clone, Copy, Debug)] +enum CandidateOrdinalMap { + SameSize { + first_greater_position: usize, + }, + DifferentSize { + equal_start: usize, + equal_end: usize, + }, +} + +impl CandidateOrdinalMap { + fn offset(self, right_position: usize) -> Option { + match self { + Self::SameSize { + first_greater_position, + } => right_position.checked_sub(first_greater_position), + Self::DifferentSize { + equal_start, + equal_end, + } => { + if (equal_start..equal_end).contains(&right_position) { + None + } else if right_position < equal_start { + Some(right_position) + } else { + Some(right_position - (equal_end - equal_start)) + } + } + } + } +} + +#[derive(Debug)] +struct ParentPairPlan { + left_id: u32, + right_ids: Rc<[OrderedCurrentId]>, + same_size: bool, + candidate_base: usize, + ordinal_map: CandidateOrdinalMap, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +struct OrderedDisjointParentPair { + parent_ids: [u32; 2], + theoretical_candidate_count: usize, +} + +#[derive(Debug)] +struct DisjointParentPairs { + plans: std::vec::IntoIter, + current_plan: Option, + right_index: usize, +} + +impl Iterator for DisjointParentPairs { + type Item = RusticolResult; + + fn next(&mut self) -> Option { + loop { + if self.current_plan.is_none() { + self.current_plan = self.plans.next(); + self.right_index = 0; + } + let plan = self.current_plan.as_ref()?; + let Some(right) = plan.right_ids.get(self.right_index).copied() else { + self.current_plan = None; + continue; + }; + self.right_index += 1; + let Some(candidate_offset) = plan.ordinal_map.offset(right.bucket_position) else { + continue; + }; + let parent_ids = match plan.left_id.cmp(&right.current_id) { + std::cmp::Ordering::Less => [plan.left_id, right.current_id], + std::cmp::Ordering::Equal => continue, + std::cmp::Ordering::Greater if plan.same_size => continue, + std::cmp::Ordering::Greater => [right.current_id, plan.left_id], + }; + let theoretical_candidate_count = match plan + .candidate_base + .checked_add(candidate_offset) + .and_then(|count| count.checked_add(1)) + { + Some(count) => count, + None => { + return Some(Err(invalid( + "recurrence candidate parent-pair count exceeds usize", + ))); + } + }; + return Some(Ok(OrderedDisjointParentPair { + parent_ids, + theoretical_candidate_count, + })); + } + } +} + +#[derive(Debug)] +struct DisjointParentPairSchedule { + pairs: DisjointParentPairs, + theoretical_candidate_count: usize, + support_bucket_count: usize, + support_bucket_probe_count: usize, + support_bucket_cache_hit_count: usize, +} + +fn disjoint_parent_pairs_for_target( + target_size: usize, + prior_currents_by_size: &[Vec], + support_keys: &TransientCurrentSupportKeys, + support_buckets: &mut LaneSupportBuckets, +) -> RusticolResult { + let mut plans = Vec::new(); + let mut theoretical_candidate_count = 0usize; + let mut support_bucket_count = 0usize; + let mut support_bucket_probe_count = 0usize; + let mut support_bucket_cache_hit_count = 0usize; + for left_size in 1..=target_size / 2 { + let right_size = target_size - left_size; + let same_size = left_size == right_size; + let right_bucket = &prior_currents_by_size[right_size - 1]; + let indexed_right_bucket = support_buckets.bucket_mut(right_size)?; + support_bucket_count = support_bucket_count + .checked_add(indexed_right_bucket.ordered_supports.len()) + .ok_or_else(|| invalid("support-bucket count exceeds usize"))?; + debug_assert!(right_bucket.iter().copied().is_sorted()); + for left_id in prior_currents_by_size[left_size - 1].iter().copied() { + let equal_start = right_bucket.partition_point(|right_id| *right_id < left_id); + let equal_end = right_bucket.partition_point(|right_id| *right_id <= left_id); + let (ordinal_map, candidate_count) = if same_size { + ( + CandidateOrdinalMap::SameSize { + first_greater_position: equal_end, + }, + right_bucket.len() - equal_end, + ) + } else { + ( + CandidateOrdinalMap::DifferentSize { + equal_start, + equal_end, + }, + right_bucket.len() - (equal_end - equal_start), + ) + }; + let (right_ids, cache_hit) = + indexed_right_bucket.disjoint_current_ids(support_keys.get(left_id)?)?; + support_bucket_probe_count = support_bucket_probe_count + .checked_add(1) + .ok_or_else(|| invalid("support-bucket probe count exceeds usize"))?; + if cache_hit { + support_bucket_cache_hit_count = support_bucket_cache_hit_count + .checked_add(1) + .ok_or_else(|| invalid("support-bucket cache-hit count exceeds usize"))?; + } + plans.push(ParentPairPlan { + left_id, + right_ids, + same_size, + candidate_base: theoretical_candidate_count, + ordinal_map, + }); + theoretical_candidate_count = theoretical_candidate_count + .checked_add(candidate_count) + .ok_or_else(|| invalid("recurrence parent-pair total exceeds usize"))?; + } + } + // The plans retain their shared lists until this stage finishes. Drop the + // bucket-owned cache references now so no list survives into later stages. + support_buckets.clear_disjoint_caches(); + Ok(DisjointParentPairSchedule { + pairs: DisjointParentPairs { + plans: plans.into_iter(), + current_plan: None, + right_index: 0, + }, + theoretical_candidate_count, + support_bucket_count, + support_bucket_probe_count, + support_bucket_cache_hit_count, + }) +} + +fn parent_pair_total_for_target( + target_size: usize, + prior_currents_by_size: &[Vec], +) -> RusticolResult { + (1..=target_size / 2).try_fold(0usize, |total, left_size| { + let right_size = target_size - left_size; + let left_count = prior_currents_by_size[left_size - 1].len(); + let right_count = prior_currents_by_size[right_size - 1].len(); + let pair_count = if left_size == right_size { + left_count + .checked_mul(left_count.saturating_sub(1)) + .and_then(|value| value.checked_div(2)) + } else { + left_count.checked_mul(right_count) + } + .ok_or_else(|| invalid("recurrence parent-pair total exceeds usize"))?; + total + .checked_add(pair_count) + .ok_or_else(|| invalid("recurrence parent-pair total exceeds usize")) + }) +} + +fn checked_diagnostic_add(counter: &mut usize, amount: usize, label: &str) -> RusticolResult<()> { + *counter = counter + .checked_add(amount) + .ok_or_else(|| invalid(format!("{label} exceeds usize")))?; + Ok(()) +} + +fn pending_contribution_entry_count(currents: &[PendingCurrent]) -> RusticolResult { + currents.iter().try_fold(0usize, |total, current| { + total + .checked_add(current.contributions.len()) + .ok_or_else(|| invalid("resident recurrence contribution count exceeds usize")) + }) +} + +fn prune_inactive_lane_contributions( + currents: &mut [PendingCurrent], + domain: PendingConstructionDomain, + closures: &BTreeMap, + resident_contribution_count: &mut usize, +) -> RusticolResult<()> { + if domain.shared_source_end > domain.lane_internal_start + || domain.lane_internal_start > domain.lane_internal_end + || domain.lane_internal_end > currents.len() + { + return Err(invalid( + "recurrence lane contribution compaction has an invalid domain", + )); + } + let mut live = BTreeSet::new(); + let mut queue = VecDeque::new(); + for (key, group) in closures { + if group.exact_factor.is_zero() { + continue; + } + for parent_id in key.parent_current_ids.iter().copied() { + if !domain.contains(parent_id as usize) { + return Err(invalid( + "recurrence lane closure references another construction lane", + )); + } + if live.insert(parent_id) { + queue.push_back(parent_id); + } + } + } + while let Some(current_id) = queue.pop_front() { + let current = currents + .get(current_id as usize) + .ok_or_else(|| invalid("recurrence lane liveness references an absent current"))?; + for (contribution, factor) in ¤t.contributions { + if factor.is_zero() { + continue; + } + for parent_id in contribution.parent_current_ids.iter().copied() { + if !domain.contains(parent_id as usize) { + return Err(invalid( + "recurrence lane contribution references another construction lane", + )); + } + if live.insert(parent_id) { + queue.push_back(parent_id); + } + } + } + } + + for (current_id, current) in currents + .iter_mut() + .enumerate() + .take(domain.lane_internal_end) + .skip(domain.lane_internal_start) + { + let current_id_u32 = u32::try_from(current_id) + .map_err(|_| invalid("recurrence lane current ID exceeds u32"))?; + let before = current.contributions.len(); + if live.contains(¤t_id_u32) { + current.contributions.retain(|_, factor| !factor.is_zero()); + } else { + current.contributions.clear(); + } + let removed = before + .checked_sub(current.contributions.len()) + .ok_or_else(|| invalid("recurrence lane contribution compaction underflowed"))?; + *resident_contribution_count = resident_contribution_count + .checked_sub(removed) + .ok_or_else(|| invalid("resident recurrence contribution count underflowed"))?; + } + Ok(()) +} + +#[allow(clippy::too_many_arguments)] +fn build_internal_currents( + catalog_digest: SemanticDigest, + process: &OwnedRecurrenceProcessInput, + pairing_catalog: Option>, + template: &OwnedRecurrenceTemplateInput, + fermion_ordering: &FermionOrderingContext, + prepared_transitions: &PreparedTransitionCatalog, + transition_reflections: &TransitionReflectionIndex, + coupling_limits: &[Option], + propagators: &BTreeMap>, + color_targets: &MaterializedColorTargets, + structural_demands: &StructuralDemandIndex, + color_states: &mut DynamicLCColorStateInterner, + currents: &mut Vec, + current_ids: &mut TransientCurrentIdIndex, + currents_by_size: &mut [Vec], + current_support_keys: &mut TransientCurrentSupportKeys, + reflection_certificates: &mut Vec, + resident_contribution_count: &mut usize, + stage_index_offset: usize, + reported_stage_total: usize, + phase_total: usize, + telemetry: &mut RecurrenceGenerationTelemetry, + collect_telemetry: bool, + progress: &mut dyn FnMut(RecurrenceBuildProgress) -> RusticolResult<()>, +) -> RusticolResult> { + if current_support_keys.source_count != process.external_legs.len() { + return Err(invalid( + "transient current support domain disagrees with the process", + )); + } + let initial_support_index_started = telemetry_timer(collect_telemetry); + let mut support_buckets = LaneSupportBuckets::new(currents_by_size, current_support_keys)?; + add_optional_elapsed_nanoseconds( + &mut telemetry.support_indexing_nanoseconds, + initial_support_index_started, + ); + let mut diagnostics = Vec::new(); + let mut completed_color_target_prune_count = 0usize; + for target_size in 2..process.external_legs.len() { + let stage_current_start = currents.len(); + let stage_contribution_start = *resident_contribution_count; + let mut stage = StageConstructionDiagnostics { + target_size, + ..StageConstructionDiagnostics::default() + }; + let (prior_buckets, target_and_later) = currents_by_size.split_at_mut(target_size - 1); + debug_assert!(prior_buckets.iter().flatten().copied().is_sorted()); + let target_bucket = &mut target_and_later[0]; + let candidate_parent_pair_total = parent_pair_total_for_target(target_size, prior_buckets)?; + let stage_index = stage_index_offset + .checked_add(target_size - 1) + .ok_or_else(|| invalid("recurrence construction stage index exceeds usize"))?; + progress(RecurrenceBuildProgress::snapshot( + "recurrence stage", + stage_index.saturating_add(1), + phase_total, + Some(stage_index), + reported_stage_total, + Some(target_size), + 0, + Some(candidate_parent_pair_total), + currents.len(), + *resident_contribution_count, + color_states.len(), + completed_color_target_prune_count, + ))?; + let mut last_progress = Instant::now(); + let support_index_started = telemetry_timer(collect_telemetry); + let pair_schedule = disjoint_parent_pairs_for_target( + target_size, + prior_buckets, + current_support_keys, + &mut support_buckets, + )?; + add_optional_elapsed_nanoseconds( + &mut stage.support_indexing_nanoseconds, + support_index_started, + ); + debug_assert_eq!( + pair_schedule.theoretical_candidate_count, + candidate_parent_pair_total + ); + stage.support_bucket_count = pair_schedule.support_bucket_count; + stage.support_bucket_probe_count = pair_schedule.support_bucket_probe_count; + stage.support_bucket_cache_hit_count = pair_schedule.support_bucket_cache_hit_count; + let mut next_progress_candidate = PROGRESS_PAIR_INTERVAL; + let candidate_processing_started = telemetry_timer(collect_telemetry); + for pair in pair_schedule.pairs { + let pair = pair?; + let [left_id, right_id] = pair.parent_ids; + stage.candidate_parent_pair_count = pair.theoretical_candidate_count; + if stage.candidate_parent_pair_count >= next_progress_candidate { + if last_progress.elapsed() >= PROGRESS_TIME_INTERVAL { + progress(RecurrenceBuildProgress::snapshot( + "recurrence stage", + stage_index.saturating_add(1), + phase_total, + Some(stage_index), + reported_stage_total, + Some(target_size), + stage.candidate_parent_pair_count, + Some(candidate_parent_pair_total), + currents.len(), + *resident_contribution_count, + color_states.len(), + completed_color_target_prune_count + .saturating_add(stage.color_target_prune_count), + ))?; + last_progress = Instant::now(); + } + next_progress_candidate = stage + .candidate_parent_pair_count + .checked_div(PROGRESS_PAIR_INTERVAL) + .and_then(|interval| interval.checked_add(1)) + .and_then(|interval| interval.checked_mul(PROGRESS_PAIR_INTERVAL)) + .unwrap_or(usize::MAX); + } + checked_diagnostic_add( + &mut stage.parent_pair_count, + 1, + "recurrence parent-pair count", + )?; + let left_state = currents[left_id as usize].key.current_state_template_id(); + let right_state = currents[right_id as usize].key.current_state_template_id(); + let indexed_transitions = prepared_transitions.rows(left_state, right_state); + if !indexed_transitions.is_empty() { + checked_diagnostic_add( + &mut stage.transition_index_hit_count, + 1, + "recurrence transition-index hit count", + )?; + } + checked_diagnostic_add( + &mut stage.transition_candidate_count, + indexed_transitions.len(), + "recurrence transition-candidate count", + )?; + if indexed_transitions.is_empty() { + continue; + } + let merged_support_key = current_support_keys + .get(left_id)? + .union_disjoint(current_support_keys.get(right_id)?)?; + let mut merged_support_source_slots = None; + for prepared in indexed_transitions { + checked_diagnostic_add( + &mut stage.state_order_count, + 1, + "recurrence state-order count", + )?; + add_transition_contributions( + catalog_digest, + prepared, + prepared.parent_ids(left_state, right_state, left_id, right_id)?, + &merged_support_key, + &mut merged_support_source_slots, + target_size + 1 < process.external_legs.len(), + pairing_catalog, + template, + fermion_ordering, + transition_reflections, + coupling_limits, + propagators, + color_targets, + structural_demands, + color_states, + currents, + current_ids, + target_bucket, + current_support_keys, + &mut stage, + resident_contribution_count, + collect_telemetry, + )?; + } + } + if let Some(contact_owner_plan) = plan_established_contact_orbit_owners( + stage_current_start, + prepared_transitions, + currents, + *resident_contribution_count, + )? { + contact_owner_plan.commit(currents, resident_contribution_count); + } + add_optional_elapsed_nanoseconds( + &mut stage.candidate_processing_nanoseconds, + candidate_processing_started, + ); + stage.candidate_parent_pair_count = candidate_parent_pair_total; + if collect_telemetry { + stage.current_insert_count = currents + .len() + .checked_sub(stage_current_start) + .ok_or_else(|| invalid("stage current insertion count underflowed"))?; + stage.current_key_clone_count = stage.current_insert_count; + stage.current_key_lookup_count = stage + .color_result_count + .checked_sub(stage.color_target_prune_count) + .ok_or_else(|| invalid("stage color-target acceptance count underflowed"))?; + stage.current_key_hit_count = stage + .current_key_lookup_count + .checked_sub(stage.current_insert_count) + .ok_or_else(|| invalid("stage current-key hit count underflowed"))?; + debug_assert_eq!(stage.accepted_parent_key_clone_count, 0); + stage.structural_reject_count = stage + .coupling_match_count + .checked_sub(stage.transition_accept_count) + .ok_or_else(|| invalid("stage structural-reject count underflowed"))?; + stage.contribution_insert_count = resident_contribution_count + .checked_sub(stage_contribution_start) + .ok_or_else(|| invalid("stage contribution insertion count underflowed"))?; + stage.contribution_merge_count = stage + .contribution_attempt_count + .checked_sub(stage.contribution_insert_count) + .ok_or_else(|| invalid("stage contribution merge count underflowed"))?; + } + progress(RecurrenceBuildProgress::snapshot( + "recurrence stage", + stage_index.saturating_add(1), + phase_total, + Some(stage_index), + reported_stage_total, + Some(target_size), + stage.candidate_parent_pair_count, + Some(candidate_parent_pair_total), + currents.len(), + *resident_contribution_count, + color_states.len(), + completed_color_target_prune_count.saturating_add(stage.color_target_prune_count), + ))?; + // Target-size currents cannot parent another current in this stage, so + // their support index is materialized only once after ID reconciliation. + debug_assert_eq!(support_buckets.bucket_mut(target_size)?.entry_count, 0); + reconcile_stage_reflections( + stage_current_start, + color_states, + currents, + current_ids, + target_bucket, + reflection_certificates, + process.external_legs.len(), + resident_contribution_count, + )?; + current_support_keys.reconcile_stage_tail(stage_current_start, currents)?; + let support_index_started = telemetry_timer(collect_telemetry); + support_buckets.replace_bucket(target_size, target_bucket, current_support_keys)?; + add_optional_elapsed_nanoseconds( + &mut stage.support_indexing_nanoseconds, + support_index_started, + ); + debug_assert_eq!(stage.target_size, target_size); + debug_assert_eq!(stage.transition_candidate_count, stage.state_order_count); + completed_color_target_prune_count = completed_color_target_prune_count + .checked_add(stage.color_target_prune_count) + .ok_or_else(|| invalid("recurrence color-target prune count exceeds usize"))?; + progress(RecurrenceBuildProgress::snapshot( + "recurrence stage", + stage_index.saturating_add(1), + phase_total, + Some(stage_index), + reported_stage_total, + Some(target_size), + stage.candidate_parent_pair_count, + Some(candidate_parent_pair_total), + currents.len(), + *resident_contribution_count, + color_states.len(), + completed_color_target_prune_count, + ))?; + if collect_telemetry { + telemetry.include_stage(&stage)?; + } + diagnostics.push(stage); + } + Ok(diagnostics) +} + +#[allow(clippy::too_many_arguments)] +fn add_transition_contributions( + catalog_digest: SemanticDigest, + prepared: &PreparedTransition, + parent_ids: [u32; 2], + support_key: &TransientSupportKey, + merged_support_source_slots: &mut Option>, + propagate_result: bool, + pairing_catalog: Option>, + template: &OwnedRecurrenceTemplateInput, + fermion_ordering: &FermionOrderingContext, + transition_reflections: &TransitionReflectionIndex, + coupling_limits: &[Option], + propagators: &BTreeMap>, + color_targets: &MaterializedColorTargets, + structural_demands: &StructuralDemandIndex, + color_states: &mut DynamicLCColorStateInterner, + currents: &mut Vec, + current_ids: &mut TransientCurrentIdIndex, + target_bucket: &mut Vec, + current_support_keys: &mut TransientCurrentSupportKeys, + diagnostics: &mut StageConstructionDiagnostics, + resident_contribution_count: &mut usize, + collect_telemetry: bool, +) -> RusticolResult<()> { + let transition = prepared.row; + let quantum = prepared.quantum; + let coupling_orders = { + let parents = [ + ¤ts[parent_ids[0] as usize].key, + ¤ts[parent_ids[1] as usize].key, + ]; + if let Some(contact_orbit) = prepared.contact_orbit.as_ref() + && !contact_orbit.accepts_parent_domain(parents)? + { + return Ok(()); + } + if !prepared.quantum_flow_matches(&parents) { + return Ok(()); + } + checked_diagnostic_add( + &mut diagnostics.quantum_match_count, + 1, + "recurrence quantum-match count", + )?; + let Some(coupling_orders) = combined_coupling_orders( + parents[0].coupling_orders(), + parents[1].coupling_orders(), + &prepared.local_coupling_orders, + coupling_limits, + )? + else { + return Ok(()); + }; + checked_diagnostic_add( + &mut diagnostics.coupling_match_count, + 1, + "recurrence coupling-match count", + )?; + if parents[0].helicity_identity().strategy() != parents[1].helicity_identity().strategy() { + return Err(invalid( + "cannot merge recurrence helicity identities from different strategies", + )); + } + if !structural_demands.accepts( + support_key, + transition.result_state_template_id, + quantum.result_spin_state, + ) { + return Ok(()); + } + if collect_telemetry { + checked_diagnostic_add( + &mut diagnostics.transition_accept_count, + 1, + "recurrence transition-accept count", + )?; + } + coupling_orders + }; + let (evaluator_parent_ids, exchange_factor) = prepared.canonical_evaluator_parents(parent_ids); + let output_factor = prepared.output_factor()?; + let fermion_factor = fermion_ordering_factor( + &template.current_states, + [ + currents[parent_ids[0] as usize] + .key + .current_state_template_id(), + currents[parent_ids[1] as usize] + .key + .current_state_template_id(), + ], + [ + currents[parent_ids[0] as usize].key.support_source_slots(), + currents[parent_ids[1] as usize].key.support_source_slots(), + ], + fermion_ordering, + )?; + let base_factor = multiply_factors(&[ + prepared.transition_exact_factor, + exchange_factor, + prepared.contraction_exact_factor, + output_factor, + fermion_factor, + ])?; + let parent_reflections = [ + currents[parent_ids[0] as usize].reflection.clone(), + currents[parent_ids[1] as usize].reflection.clone(), + ]; + let parent_colors = [ + color_states + .get( + currents[parent_ids[0] as usize] + .key + .dynamic_lc_color_state_id(), + ) + .ok_or_else(|| invalid("left dynamic color state disappeared"))? + .clone(), + color_states + .get( + currents[parent_ids[1] as usize] + .key + .dynamic_lc_color_state_id(), + ) + .ok_or_else(|| invalid("right dynamic color state disappeared"))? + .clone(), + ]; + let reversal_masks = current_reversal_masks(&parent_colors, &parent_reflections); + let local_reflection_proof = transition_reflections.proof(transition.id); + let propagator_template_id = if propagate_result { + propagators + .get(&transition.result_state_template_id) + .copied() + .flatten() + } else { + None + }; + let mut result_core_fields = None; + + for prepared_witness in prepared.witnesses.iter() { + let witness_row = prepared_witness.row; + if witness_row.left_shape_string_id != parent_colors[0].output_color_shape_id() + || witness_row.right_shape_string_id != parent_colors[1].output_color_shape_id() + { + continue; + } + checked_diagnostic_add( + &mut diagnostics.color_shape_match_count, + 1, + "recurrence color-shape-match count", + )?; + let witness = &prepared_witness.witness; + for reversal_mask in reversal_masks.iter().copied() { + let mut variant_colors = parent_colors.clone(); + let mut reversal_factor = ExactComplexRational::ONE; + for index in 0..2 { + if reversal_mask & (1 << index) == 0 { + continue; + } + variant_colors[index] = variant_colors[index].reversed()?; + reversal_factor = reversal_factor.checked_mul( + parent_reflections[index] + .phase() + .expect("reversal mask requires a proven parent phase"), + )?; + } + let Some(result_color) = witness.apply(&variant_colors[0], &variant_colors[1])? else { + continue; + }; + checked_diagnostic_add( + &mut diagnostics.color_result_count, + 1, + "recurrence color-result count", + )?; + if !color_targets.accepts_up_to_reflection(&result_color)? { + checked_diagnostic_add( + &mut diagnostics.color_target_prune_count, + 1, + "recurrence color-target prune count", + )?; + continue; + } + let result_reflection = current_reflection_candidate( + &result_color, + &parent_reflections, + local_reflection_proof, + )?; + #[cfg(feature = "on-the-fly-test-support")] + let diagnostic_result_reflection = result_reflection.clone(); + let result_color_id = color_states.intern(result_color)?; + if merged_support_source_slots.is_none() { + *merged_support_source_slots = Some(merged_disjoint_support( + currents[parent_ids[0] as usize].key.support_source_slots(), + currents[parent_ids[1] as usize].key.support_source_slots(), + )); + } + let support_source_slots = merged_support_source_slots + .as_ref() + .expect("merged support was initialized after color acceptance"); + debug_assert_eq!(support_source_slots.len(), support_key.cardinality()); + if result_core_fields.is_none() { + let parents = [ + ¤ts[parent_ids[0] as usize].key, + ¤ts[parent_ids[1] as usize].key, + ]; + result_core_fields = Some(( + merged_helicity_identity( + parents[0].helicity_identity(), + parents[1].helicity_identity(), + quantum.result_spin_state, + )?, + prepared.result_flavour_flow(&parents), + merged_momentum(parents[0].momentum(), parents[1].momentum())?, + )); + } + let (helicity_identity, result_flavour_flow, result_momentum) = result_core_fields + .as_ref() + .expect("result current fields were initialized after color acceptance"); + let key = CurrentCoreKey::new( + catalog_digest, + RecurrenceNodeKind::Current, + transition.result_state_template_id, + result_color_id, + support_source_slots.to_vec(), + result_momentum.clone(), + helicity_identity.clone(), + result_flavour_flow.clone(), + quantum.result_quantum_number_flow_id, + coupling_orders.clone(), + CurrentSourceBinding::None, + propagator_template_id, + )?; + let realized_pairing_rule_ids = realized_pairing_rules_for_transition( + compatible_pairing_rules_for_current( + pairing_catalog, + template, + transition.result_state_template_id, + key.support_source_slots(), + )?, + [ + ¤ts[parent_ids[0] as usize].realized_pairing_rule_ids, + ¤ts[parent_ids[1] as usize].realized_pairing_rule_ids, + ], + ); + let result_id = if let Some(id) = current_ids.get(&key).copied() { + debug_assert_eq!( + current_support_keys + .get(id) + .expect("existing current has a transient support key"), + support_key + ); + currents[id as usize] + .reflection + .include(result_reflection)?; + currents[id as usize] + .realized_pairing_rule_ids + .extend(realized_pairing_rule_ids); + id + } else { + let id = u32::try_from(currents.len()) + .map_err(|_| invalid("recurrence current count exceeds u32"))?; + current_ids.insert(key.clone(), id); + currents.push(PendingCurrent { + key, + source_exact_factor: None, + contributions: BTreeMap::new(), + realized_pairing_rule_ids, + reflection: result_reflection + .map_or(CurrentReflection::Unavailable, CurrentReflection::Proven), + reflection_certificate_id: None, + }); + target_bucket.push(id); + current_support_keys.push(id, support_key.clone())?; + id + }; + let contribution_key = ContributionKey::new( + transition.id, + evaluator_parent_ids.to_vec(), + evaluator_parent_ids + .iter() + .map(|id| currents[*id as usize].key.current_state_template_id()) + .collect(), + evaluator_parent_ids + .iter() + .map(|id| currents[*id as usize].key.momentum().clone()) + .collect(), + transition.result_state_template_id, + quantum.id, + LCColorWitnessTermId::new( + transition.color_contraction_template_id, + witness_row.ordinal, + ), + prepared.quantum_semantic_digest, + transition.output_projection_string_id, + )?; + let pending_key = PendingContributionKey { + parent_current_ids: evaluator_parent_ids.into(), + key: contribution_key, + }; + let factor = base_factor + .checked_mul(witness.exact_factor())? + .checked_mul(reversal_factor)?; + let pending_factor = match currents[result_id as usize] + .contributions + .entry(pending_key) + { + std::collections::btree_map::Entry::Occupied(entry) => entry.into_mut(), + std::collections::btree_map::Entry::Vacant(entry) => { + checked_diagnostic_add( + resident_contribution_count, + 1, + "resident recurrence contribution count", + )?; + entry.insert(ExactComplexRational::ZERO) + } + }; + aggregate_factor(pending_factor, factor)?; + #[cfg(feature = "on-the-fly-test-support")] + let aggregate_factor_after = *pending_factor; + checked_diagnostic_add( + &mut diagnostics.contribution_attempt_count, + 1, + "recurrence contribution-attempt count", + )?; + #[cfg(feature = "on-the-fly-test-support")] + 'transition_diagnostic: { + if !super::diagnostic::transition_diagnostic_observation_active() { + break 'transition_diagnostic; + } + use super::diagnostic::{ + ConstructionTransitionDiagnosticRowV1, observe_transition_diagnostic, + }; + + let digest = |current_id: u32| -> RusticolResult { + let current = currents + .get(current_id as usize) + .ok_or_else(|| invalid("diagnostic current is absent"))?; + let color = color_states + .get(current.key.dynamic_lc_color_state_id()) + .ok_or_else(|| invalid("diagnostic current color is absent"))?; + super::on_the_fly::hash_current_key(¤t.key, color) + }; + let result_color = color_states + .get(currents[result_id as usize].key.dynamic_lc_color_state_id()) + .ok_or_else(|| invalid("diagnostic result color is absent"))?; + observe_transition_diagnostic(ConstructionTransitionDiagnosticRowV1 { + materialized_sector_id: None, + output_current_digest: digest(result_id)?, + ordered_parent_digests: [ + digest(evaluator_parent_ids[0])?, + digest(evaluator_parent_ids[1])?, + ], + transition_template_id: transition.id, + transition_semantic_digest: prepared.transition_semantic_digest, + evaluator_binding_semantic_digest: prepared.evaluator_binding_semantic_digest, + result_state_template_id: transition.result_state_template_id, + quantum_flow_witness_id: quantum.id, + quantum_semantic_digest: prepared.quantum_semantic_digest, + color_contraction_template_id: transition.color_contraction_template_id, + color_witness_ordinal: witness_row.ordinal, + color_witness_proof_digest: witness.proof_digest(), + output_projection_id: transition.output_projection_string_id, + transition_factor: prepared.transition_exact_factor, + contraction_factor: prepared.contraction_exact_factor, + output_factor, + exchange_factor, + witness_factor: witness.exact_factor(), + reversal_mask, + reversal_factor, + candidate_factor: factor, + aggregate_factor_after, + parent_reflection_proof_digests: [ + parent_reflections[0] + .proof() + .map(CurrentReflectionProof::proof_digest), + parent_reflections[1] + .proof() + .map(CurrentReflectionProof::proof_digest), + ], + parent_reflection_phases: [ + parent_reflections[0].phase(), + parent_reflections[1].phase(), + ], + local_reflection_proof_digest: local_reflection_proof + .map(|proof| proof.proof_digest), + local_reflection_phase: local_reflection_proof + .map(TransitionReflectionProof::phase), + result_reflection_proof_digest: diagnostic_result_reflection + .as_ref() + .map(CurrentReflectionProof::proof_digest), + result_reflection_phase: diagnostic_result_reflection + .as_ref() + .map(CurrentReflectionProof::phase), + output_color_orientation: format!("{result_color:?}"), + }); + } + } + } + Ok(()) +} + +pub(super) fn current_key_with_dynamic_color( + key: &CurrentCoreKey, + dynamic_lc_color_state_id: DynamicLCColorStateId, +) -> RusticolResult { + CurrentCoreKey::new( + key.catalog_digest(), + key.node_kind(), + key.current_state_template_id(), + dynamic_lc_color_state_id, + key.support_source_slots().to_vec(), + key.momentum().clone(), + key.helicity_identity().clone(), + key.flavour_flow().to_vec(), + key.quantum_number_flow_id(), + key.coupling_orders().to_vec(), + key.source_binding().clone(), + key.propagator_template_id(), + ) +} + +pub(super) fn reciprocal_reflection_proof( + left: &CurrentReflection, + right: &CurrentReflection, +) -> RusticolResult { + let (Some(left), Some(right)) = (left.phase(), right.phase()) else { + return Ok(false); + }; + Ok(left.checked_mul(right)? == ExactComplexRational::ONE) +} + +/// Finalize pure-adjoint reflection aliases after every contribution to the +/// stage's currents has been seen. +/// +/// A contribution that lacks a reflection proof downgrades the whole current. +/// Both orientations are then retained as exact residual states. Only a pair +/// with reciprocal, complete fan-in proofs is compacted to its canonical +/// orientation. Stage currents cannot depend on one another, so compacting the +/// stage tail cannot invalidate any stored parent current ID. +#[allow(clippy::too_many_arguments)] +fn reconcile_stage_reflections( + stage_start: usize, + color_states: &mut DynamicLCColorStateInterner, + currents: &mut Vec, + current_ids: &mut TransientCurrentIdIndex, + target_bucket: &mut Vec, + reflection_certificates: &mut Vec, + source_count: usize, + resident_contribution_count: &mut usize, +) -> RusticolResult<()> { + if stage_start > currents.len() { + return Err(invalid( + "recurrence reflection stage starts beyond current storage", + )); + } + let stage_end = currents.len(); + let mut visited = vec![false; stage_end - stage_start]; + let mut prune = vec![false; stage_end - stage_start]; + + for current_index in stage_start..stage_end { + let local_index = current_index - stage_start; + if visited[local_index] { + continue; + } + let key = currents[current_index].key.clone(); + let color = color_states + .get(key.dynamic_lc_color_state_id()) + .ok_or_else(|| invalid("reflection current color state disappeared"))? + .clone(); + let Some(word) = color.pure_adjoint_word() else { + continue; + }; + if word.len() < 2 { + continue; + } + let canonical = pure_adjoint_word_is_canonical(word); + let reversed_color_id = color_states.intern(color.reversed()?)?; + if reversed_color_id == key.dynamic_lc_color_state_id() { + currents[current_index].reflection = CurrentReflection::Unavailable; + continue; + } + let reversed_key = current_key_with_dynamic_color(&key, reversed_color_id)?; + let Some(reversed_id) = current_ids.get(&reversed_key).copied() else { + currents[current_index].reflection = CurrentReflection::Unavailable; + continue; + }; + let reversed_index = reversed_id as usize; + if !(stage_start..stage_end).contains(&reversed_index) { + currents[current_index].reflection = CurrentReflection::Unavailable; + continue; + } + let reversed_local_index = reversed_index - stage_start; + visited[local_index] = true; + visited[reversed_local_index] = true; + + let reversed_color = color_states + .get(reversed_color_id) + .ok_or_else(|| invalid("reversed reflection color state disappeared"))?; + let Some(reversed_word) = reversed_color.pure_adjoint_word() else { + currents[current_index].reflection = CurrentReflection::Unavailable; + currents[reversed_index].reflection = CurrentReflection::Unavailable; + continue; + }; + let reversed_canonical = pure_adjoint_word_is_canonical(reversed_word); + let proof_is_complete = reciprocal_reflection_proof( + ¤ts[current_index].reflection, + ¤ts[reversed_index].reflection, + )?; + if !proof_is_complete || canonical == reversed_canonical { + currents[current_index].reflection = CurrentReflection::Unavailable; + currents[reversed_index].reflection = CurrentReflection::Unavailable; + continue; + } + let (canonical_index, reflected_index, pruned_local_index) = if canonical { + (current_index, reversed_index, reversed_local_index) + } else { + (reversed_index, current_index, local_index) + }; + let canonical_proof = currents[canonical_index] + .reflection + .proof() + .cloned() + .ok_or_else(|| invalid("canonical reflection proof disappeared"))?; + let reflected_proof = currents[reflected_index] + .reflection + .proof() + .cloned() + .ok_or_else(|| invalid("reflected reflection proof disappeared"))?; + for (label, current_index, proof) in [ + ("canonical", canonical_index, &canonical_proof), + ("reflected", reflected_index, &reflected_proof), + ] { + let current_color = color_states + .get(currents[current_index].key.dynamic_lc_color_state_id()) + .ok_or_else(|| invalid(format!("{label} reflection color disappeared")))?; + if dynamic_color_identity_digest(current_color)? != proof.result_color_identity() { + return Err(invalid(format!( + "{label} reflection proof carries a stale dynamic color identity" + ))); + } + } + let certificate_id = u32::try_from(reflection_certificates.len()) + .map_err(|_| invalid("pending reflection certificate count exceeds u32"))?; + reflection_certificates.push(PendingReflectionCertificate::reciprocal_pair( + certificate_id, + u32::try_from(canonical_index) + .map_err(|_| invalid("canonical reflection current ID exceeds u32"))?, + u32::try_from(reflected_index) + .map_err(|_| invalid("reflected reflection current ID exceeds u32"))?, + &canonical_proof, + &reflected_proof, + current_color_for_index(color_states, currents, canonical_index)?, + current_color_for_index(color_states, currents, reflected_index)?, + source_count, + )?); + currents[canonical_index].reflection_certificate_id = Some(certificate_id); + prune[pruned_local_index] = true; + } + + for current in ¤ts[stage_start..] { + current_ids.remove(¤t.key); + } + let stage_currents = currents.split_off(stage_start); + target_bucket.clear(); + for (local_index, current) in stage_currents.into_iter().enumerate() { + if prune[local_index] { + *resident_contribution_count = resident_contribution_count + .checked_sub(current.contributions.len()) + .ok_or_else(|| invalid("resident recurrence contribution count underflowed"))?; + continue; + } + if current + .contributions + .keys() + .flat_map(|contribution| contribution.parent_current_ids.iter().copied()) + .any(|parent_id| parent_id as usize >= stage_start) + { + return Err(invalid( + "recurrence stage current depends on another current in the same stage", + )); + } + let current_id = u32::try_from(currents.len()) + .map_err(|_| invalid("recurrence current count exceeds u32"))?; + if current_ids + .insert(current.key.clone(), current_id) + .is_some() + { + return Err(invalid( + "recurrence reflection reconciliation produced a duplicate current", + )); + } + target_bucket.push(current_id); + currents.push(current); + } + Ok(()) +} + +pub(super) fn current_reversal_masks( + colors: &[DynamicLCColorState; 2], + reflections: &[CurrentReflection; 2], +) -> Vec { + let mut masks = vec![0_u8]; + for index in 0..2 { + if reflections[index].phase().is_none() + || colors[index] + .pure_adjoint_word() + .is_none_or(|word| word.len() < 2) + { + continue; + } + let bit = 1_u8 << index; + masks.extend(masks.clone().into_iter().map(|mask| mask | bit)); + } + masks +} + +pub(super) fn current_reflection_candidate( + result_color: &DynamicLCColorState, + parent_reflections: &[CurrentReflection; 2], + local_proof: Option<&TransitionReflectionProof>, +) -> RusticolResult> { + if result_color.pure_adjoint_word().is_none() { + return Ok(None); + } + let Some(local_proof) = local_proof else { + return Ok(None); + }; + let Some(left) = parent_reflections[0].proof() else { + return Ok(None); + }; + let Some(right) = parent_reflections[1].proof() else { + return Ok(None); + }; + let phase = left + .phase() + .checked_mul(right.phase())? + .checked_mul(local_proof.phase())?; + let color_identity = dynamic_color_identity_digest(result_color)?; + CurrentReflectionProof::new( + phase, + left.lineage_roots() + .iter() + .copied() + .chain(right.lineage_roots().iter().copied()) + .chain(local_proof.lineage_roots()), + color_identity, + ) + .map(Some) +} + +pub(super) fn pure_adjoint_word_is_canonical(word: &[u32]) -> bool { + if word.len() < 2 { + return true; + } + let (minimum_index, _) = word + .iter() + .enumerate() + .min_by_key(|(_, slot)| *slot) + .expect("nonempty pure-adjoint word"); + let (maximum_index, _) = word + .iter() + .enumerate() + .max_by_key(|(_, slot)| *slot) + .expect("nonempty pure-adjoint word"); + minimum_index < maximum_index +} + +fn two_parent_permutation( + construction_parent_ids: [u32; 2], + ordered_parent_ids: [u32; 2], + label: &str, +) -> RusticolResult<[u32; 2]> { + if construction_parent_ids[0] == construction_parent_ids[1] { + return Err(invalid(format!( + "{label} cannot derive a permutation from duplicate parent IDs" + ))); + } + let mut result = [0_u32; 2]; + for (target, parent_id) in ordered_parent_ids.into_iter().enumerate() { + let source = construction_parent_ids + .iter() + .position(|candidate| *candidate == parent_id) + .ok_or_else(|| invalid(format!("{label} references a foreign parent ID")))?; + result[target] = + u32::try_from(source).map_err(|_| invalid(format!("{label} index exceeds u32")))?; + } + if result[0] == result[1] { + return Err(invalid(format!("{label} is not a permutation"))); + } + Ok(result) +} + +fn three_line_traversal_certificate( + closed: &[LCColorComponent], + sector: ProcessPhysicalLCSectorRow, + expected: &[LCColorComponent], + catalog: &ProcessCatalog<'_>, + pairing_rule_id: Option, +) -> RusticolResult> { + if sector.kind()? != ProcessLCSectorKind::OpenLines || sector.open_string_range.count != 3 { + return Ok(None); + } + let pairing_rule_id = pairing_rule_id + .ok_or_else(|| invalid("three-line traversal lacks a physical fermion pairing rule"))?; + if expected.len() != 3 || closed.len() != 3 { + return Err(invalid( + "three-line closure does not contain exactly three complete line blocks", + )); + } + let blocks = expected + .iter() + .map(|component| component.source_slots().to_vec()) + .collect::>(); + let sector_word = catalog.u32_sequence(sector.word_sequence_id, "three-line sector word")?; + let mut reference_order = Vec::::with_capacity(3); + let mut used_reference = [false; 3]; + let mut offset = 0usize; + while offset < sector_word.len() { + let matches = blocks + .iter() + .enumerate() + .filter_map(|(index, block)| { + (!used_reference[index] && sector_word[offset..].starts_with(block)) + .then_some(index) + }) + .collect::>(); + let [block_index] = matches.as_slice() else { + return Err(invalid( + "three-line sector word is not an unambiguous concatenation of its line blocks", + )); + }; + used_reference[*block_index] = true; + reference_order.push( + u32::try_from(*block_index) + .map_err(|_| invalid("three-line block index exceeds u32"))?, + ); + offset = offset + .checked_add(blocks[*block_index].len()) + .ok_or_else(|| invalid("three-line sector word offset overflows"))?; + } + if reference_order.len() != 3 || offset != sector_word.len() { + return Err(invalid( + "three-line sector word does not cover exactly three line blocks", + )); + } + let mut used = [false; 3]; + let mut witness_order = Vec::::with_capacity(3); + for component in closed { + let index = expected + .iter() + .enumerate() + .find_map(|(index, candidate)| { + (!used[index] && candidate == component).then_some(index) + }) + .ok_or_else(|| invalid("three-line closure contains a foreign line block"))?; + used[index] = true; + witness_order + .push(u32::try_from(index).map_err(|_| invalid("three-line index exceeds u32"))?); + } + let sink_candidates = expected + .iter() + .enumerate() + .filter_map(|(index, component)| { + (component.source_slots().last() == Some(§or.closure_source_slot)).then_some(index) + }) + .collect::>(); + let [sink_index] = sink_candidates.as_slice() else { + return Err(invalid( + "three-line closure anchor does not identify exactly one line block", + )); + }; + let sink_index = + u32::try_from(*sink_index).map_err(|_| invalid("three-line sink index exceeds u32"))?; + let rotate_sink_last = |order: &mut Vec| -> RusticolResult<()> { + let sink_position = order + .iter() + .position(|index| *index == sink_index) + .ok_or_else(|| invalid("three-line closure traversal omits its anchor block"))?; + let order_len = order.len(); + order.rotate_left((sink_position + 1) % order_len); + Ok(()) + }; + rotate_sink_last(&mut reference_order)?; + rotate_sink_last(&mut witness_order)?; + let kind = if witness_order == reference_order { + THREE_LINE_DIRECT_CERTIFICATE_ID as u8 + } else { + let mut partner = reference_order.clone(); + partner.swap(0, 1); + if witness_order != partner { + return Err(invalid( + "three-line closure traversal is neither the direct nor partner cyclic obligation", + )); + } + THREE_LINE_PARTNER_CERTIFICATE_ID as u8 + }; + let block_permutation = witness_order + .iter() + .map(|block| { + reference_order + .iter() + .position(|candidate| candidate == block) + .map(|position| { + u32::try_from(position) + .map_err(|_| invalid("three-line block permutation exceeds u32")) + }) + .transpose()? + .ok_or_else(|| invalid("three-line witness references a foreign block")) + }) + .collect::>>()?; + let flatten = |order: &[u32]| -> RusticolResult> { + let mut sources = Vec::new(); + for block_index in order { + sources.extend_from_slice( + blocks + .get(*block_index as usize) + .ok_or_else(|| invalid("three-line block order is out of range"))?, + ); + } + Ok(sources) + }; + let reference_source_order = flatten(&reference_order)?; + let witness_source_order = flatten(&witness_order)?; + let source_position_permutation = witness_source_order + .iter() + .map(|source| { + reference_source_order + .iter() + .position(|candidate| candidate == source) + .map(|position| { + u32::try_from(position) + .map_err(|_| invalid("three-line source permutation exceeds u32")) + }) + .transpose()? + .ok_or_else(|| invalid("three-line witness references a foreign source")) + }) + .collect::>>()?; + let mut hash = Sha256::new(); + hash.update(b"pyamplicol-recurrence-three-line-traversal-v1\0"); + hash.update(sector.sector_id.to_le_bytes()); + hash.update([kind]); + hash.update(sink_index.to_le_bytes()); + for values in [ + reference_order.as_slice(), + witness_order.as_slice(), + block_permutation.as_slice(), + reference_source_order.as_slice(), + witness_source_order.as_slice(), + source_position_permutation.as_slice(), + ] { + hash.update( + u64::try_from(values.len()) + .map_err(|_| invalid("three-line certificate sequence length exceeds u64"))? + .to_le_bytes(), + ); + for value in values { + hash.update(value.to_le_bytes()); + } + } + hash.update(sector.closure_source_slot.to_le_bytes()); + hash.update(pairing_rule_id.to_le_bytes()); + let proof_digest = SemanticDigest::new(hash.finalize().into())?; + Ok(Some(PendingThreeLineTraversalCertificate { + sector_id: sector.sector_id, + kind, + sink_block_ordinal: sink_index, + reference_block_order: reference_order.into_boxed_slice(), + witness_block_order: witness_order.into_boxed_slice(), + block_permutation: block_permutation.into_boxed_slice(), + reference_source_order: reference_source_order.into_boxed_slice(), + witness_source_order: witness_source_order.into_boxed_slice(), + source_position_permutation: source_position_permutation.into_boxed_slice(), + closure_anchor_source_slot: sector.closure_source_slot, + pairing_rule_id, + proof_digest, + })) +} + +fn closure_pairing_certificate_ids( + currents: &[PendingCurrent], + parent_ids: [u32; 2], + pairing_catalog: Option>, +) -> RusticolResult> { + let left = currents + .get(parent_ids[0] as usize) + .ok_or_else(|| invalid("closure pairing proof left parent is absent"))?; + let right = currents + .get(parent_ids[1] as usize) + .ok_or_else(|| invalid("closure pairing proof right parent is absent"))?; + let realized = left + .realized_pairing_rule_ids + .intersection(&right.realized_pairing_rule_ids) + .copied() + .collect::>(); + match pairing_catalog { + None if realized.is_empty() => Ok(Vec::new()), + None => Err(invalid( + "closure realizes fermion pairings without a pairing catalog", + )), + Some(catalog) => canonical_pairing_owner_id(realized.into_iter().map(|rule_id| { + let rule = catalog + .rules() + .get(rule_id as usize) + .copied() + .filter(|rule| rule.rule_id == rule_id) + .ok_or_else(|| { + invalid(format!( + "closure parent lineage references absent pairing rule {rule_id}" + )) + })?; + Ok((rule_id, catalog.endpoint_pairings(rule)?)) + }))? + .map(|rule_id| vec![rule_id]) + .ok_or_else(|| invalid("closure parent lineage realizes no fermion pairing rule")), + } +} + +fn canonical_pairing_owner_id<'a>( + candidates: impl IntoIterator>, +) -> RusticolResult> { + let mut selected = None::<(u32, &'a [FermionPairingEndpointPairRow])>; + for candidate in candidates { + let (rule_id, endpoint_pairs) = candidate?; + let candidate_precedes_selected = selected.is_none_or(|(selected_id, selected_pairs)| { + endpoint_pairs + .iter() + .map(|pair| { + ( + pair.fundamental_source_slot, + pair.antifundamental_source_slot, + ) + }) + .cmp(selected_pairs.iter().map(|pair| { + ( + pair.fundamental_source_slot, + pair.antifundamental_source_slot, + ) + })) + .then_with(|| rule_id.cmp(&selected_id)) + .is_lt() + }); + if candidate_precedes_selected { + selected = Some((rule_id, endpoint_pairs)); + } + } + Ok(selected.map(|(rule_id, _)| rule_id)) +} + +fn pairing_rule_for_certificate( + certificate_ids: &[u32], + catalog: Option>, +) -> RusticolResult> { + let Some(catalog) = catalog else { + if !certificate_ids.is_empty() { + return Err(invalid( + "closure pairing certificate has no pairing catalog", + )); + } + return Ok(None); + }; + let [rule_id] = certificate_ids else { + return Err(invalid( + "closure requires exactly one fermion pairing certificate", + )); + }; + catalog + .rules() + .iter() + .copied() + .find(|rule| rule.rule_id == *rule_id) + .map(Some) + .ok_or_else(|| invalid(format!("closure references absent pairing rule {rule_id}"))) +} + +fn pairing_reconstruction_factor(_rule: Option) -> ExactComplexRational { + // Canonical color-line construction already carries the exchange parity + // of colored Wick lineages. The exterior-algebra pass separately owns + // color-singlet fermions, so multiplying pairing parity here would count + // the colored sign twice. + ExactComplexRational::ONE +} + +// Closure certification deliberately receives each authenticated catalog and +// proof table separately so that no unchecked aggregate can cross this boundary. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +struct ClosureReflectionProjection { + certificate_id: u32, + partner_sector_id: Option, + partner_phase: ExactComplexRational, +} + +#[allow(clippy::too_many_arguments)] +fn closure_reflection_projection( + sector: ProcessPhysicalLCSectorRow, + closed: &[LCColorComponent], + process: &OwnedRecurrenceProcessInput, + process_catalog: &ProcessCatalog<'_>, + color_states: &DynamicLCColorStateInterner, + currents: &[PendingCurrent], + parent_ids: [u32; 2], + certificates: &[PendingReflectionCertificate], + prepared_sectors: &PreparedClosureSectorCatalog, + materialized_sectors: &BTreeSet, +) -> RusticolResult> { + let mut certified_parents = Vec::new(); + for parent_id in parent_ids { + let current = currents + .get(parent_id as usize) + .ok_or_else(|| invalid("closure reflection parent is absent"))?; + let Some(certificate_id) = current.reflection_certificate_id else { + continue; + }; + let certificate = certificates + .get(certificate_id as usize) + .filter(|certificate| certificate.id == certificate_id) + .ok_or_else(|| invalid("closure reflection certificate is absent"))?; + let proof = current + .reflection + .proof() + .ok_or_else(|| invalid("certified closure reflection parent has no lineage proof"))?; + if proof.proof_digest() != certificate.canonical_lineage_digest { + return Err(invalid( + "closure reflection parent lineage does not match its certificate", + )); + } + let color = color_states + .get(current.key.dynamic_lc_color_state_id()) + .ok_or_else(|| invalid("closure reflection parent color is absent"))?; + if dynamic_color_identity_digest(color)? != certificate.canonical_color_identity { + return Err(invalid( + "closure reflection parent color does not match its certificate", + )); + } + certified_parents.push(certificate_id); + } + match certified_parents.len() { + 0 => return Ok(None), + 1 => {} + _ => { + return Err(invalid( + "one closure references multiple folded reflection orbits", + )); + } + } + if sector.kind()? != ProcessLCSectorKind::SingleTrace { + return Ok(None); + } + let construction_word = + process_catalog.u32_sequence(sector.word_sequence_id, "reflection construction word")?; + if construction_word.len() <= 2 { + return Ok(None); + } + let [closed_trace] = closed else { + return Ok(None); + }; + if closed_trace.kind() != LCColorComponentKind::Trace + || closed_trace.source_slots().len() != construction_word.len() + { + return Ok(None); + } + let certificate_id = certified_parents[0]; + let certificate = certificates + .get(certificate_id as usize) + .filter(|certificate| certificate.id == certificate_id) + .ok_or_else(|| invalid("closure reflection certificate is absent"))?; + if certificate.fixed_point + || certificate.orbit_size != 2 + || certificate.canonical_color_identity == certificate.reflected_color_identity + { + return Err(invalid( + "closure reflection certificate does not describe a reciprocal two-cycle", + )); + } + let mapped_word = construction_word + .iter() + .map(|source_slot| { + certificate + .source_permutation + .get(*source_slot as usize) + .copied() + .ok_or_else(|| { + invalid("closure reflection permutation does not cover its trace word") + }) + }) + .collect::>>()?; + if cyclic_words_equal(construction_word, &mapped_word) { + return Err(invalid( + "closure reflection maps a trace to a cyclic fixed point", + )); + } + let closure_algorithm = process_catalog.string( + sector.closure_proof_algorithm_string_id, + "reflection closure-anchor algorithm", + )?; + if closure_algorithm == CYCLIC_TRACE_CLOSURE_ANCHOR_ALGORITHM + && let Some(partner_sector_id) = + prepared_sectors.reflected_v4_sector(&mapped_word, sector.closure_source_slot)? + && materialized_sectors.contains(&partner_sector_id) + { + if partner_sector_id == sector.sector_id { + return Err(invalid( + "closure reflection maps a v4 trace sector onto itself", + )); + } + return Ok(Some(ClosureReflectionProjection { + certificate_id, + partner_sector_id: Some(partner_sector_id), + partner_phase: certificate.canonical_phase, + })); + } + + let mut matching_public_flows = Vec::new(); + for flow in retained_public_flows(process)? + .into_iter() + .filter(|flow| flow.construction_sector_id == sector.sector_id) + { + let word = + process_catalog.u32_sequence(flow.word_sequence_id, "reflected public LC flow")?; + if cyclic_words_equal(word, &mapped_word) { + matching_public_flows.push(flow.flow_id); + } + } + match matching_public_flows.as_slice() { + [_] => Ok(Some(ClosureReflectionProjection { + certificate_id, + partner_sector_id: None, + partner_phase: certificate.canonical_phase, + })), + [] => Ok(None), + _ => Err(invalid( + "closure reflection maps to multiple retained public LC flows", + )), + } +} + +fn cyclic_words_equal(left: &[u32], right: &[u32]) -> bool { + if left.len() != right.len() { + return false; + } + if left.is_empty() { + return true; + } + (0..left.len()).any(|offset| { + left.iter() + .enumerate() + .all(|(index, value)| *value == right[(index + offset) % right.len()]) + }) +} + +fn anchored_cyclic_trace_word(word: &[u32], anchor: u32) -> RusticolResult> { + let mut positions = word + .iter() + .enumerate() + .filter_map(|(index, source_slot)| (*source_slot == anchor).then_some(index)); + let position = positions + .next() + .ok_or_else(|| invalid("v4 cyclic trace word omits its closure anchor"))?; + if positions.next().is_some() { + return Err(invalid("v4 cyclic trace word repeats its closure anchor")); + } + Ok(word[position..] + .iter() + .chain(&word[..position]) + .copied() + .collect::>() + .into_boxed_slice()) +} + +fn include_closure_with_reflection( + result: &mut BTreeMap, + key: PendingClosureKey, + contribution: PendingClosureProofContribution, + reflection: Option, +) -> RusticolResult<()> { + result + .entry(key.clone()) + .or_default() + .include(contribution.clone())?; + if let Some(reflection) = reflection + && let Some(partner_sector_id) = reflection.partner_sector_id + { + let mut partner_key = key; + partner_key.target_sector_id = partner_sector_id; + let mut partner_contribution = contribution; + partner_contribution.exact_factor = partner_contribution + .exact_factor + .checked_mul(reflection.partner_phase)?; + result + .entry(partner_key) + .or_default() + .include(partner_contribution)?; + } + Ok(()) +} + +fn materialize_reflection_certificates( + certificates: &[PendingReflectionCertificate], +) -> RusticolResult> { + certificates + .iter() + .map(|certificate| { + ReflectionCertificateV1::new( + certificate.id, + certificate.canonical_color_identity, + certificate.reflected_color_identity, + certificate.source_permutation.to_vec(), + certificate.canonical_phase, + certificate.fixed_point, + certificate.orbit_size, + REFLECTION_PROOF_ALGORITHM_ID, + certificate.proof_digest, + ) + }) + .collect() +} + +fn materialize_three_line_certificates( + certificate_ids: &BTreeMap, +) -> RusticolResult> { + let mut rows = certificate_ids + .iter() + .map(|(certificate, id)| { + ThreeLineTraversalCertificateV1::new( + *id, + certificate.sector_id, + ThreeLineTraversalKindV1::try_from(u32::from(certificate.kind))?, + certificate.sink_block_ordinal, + certificate.reference_block_order.to_vec(), + certificate.witness_block_order.to_vec(), + certificate.block_permutation.to_vec(), + certificate.reference_source_order.to_vec(), + certificate.witness_source_order.to_vec(), + certificate.source_position_permutation.to_vec(), + certificate.closure_anchor_source_slot, + certificate.pairing_rule_id, + certificate.proof_digest, + ) + }) + .collect::>>()?; + rows.sort_unstable_by_key(ThreeLineTraversalCertificateV1::id); + Ok(rows) +} + +type ClosureSupportBucketKey = (Box<[u32]>, Box<[u32]>); + +#[derive(Debug, Default)] +struct ContractedClosureSupportBucket { + sector_ids: Vec, + ordered_sector_ids_by_components: BTreeMap, Vec>, + open_line_sector_ids_by_components: BTreeMap, Vec>, +} + +impl ContractedClosureSupportBucket { + fn include( + &mut self, + strategy: RecurrenceStrategy, + sector: &PreparedClosureSector, + ) -> RusticolResult<()> { + self.sector_ids.push(sector.row.sector_id); + match sector.row.kind()? { + ProcessLCSectorKind::Singlet | ProcessLCSectorKind::SingleTrace => { + self.ordered_sector_ids_by_components + .entry(sector.expected_components.clone()) + .or_default() + .push(sector.row.sector_id); + } + ProcessLCSectorKind::OpenLines + if strategy == RecurrenceStrategy::AllFlowUnion + || sector.contracted_color_canonical_owner => + { + let mut canonical = sector.expected_components.to_vec(); + canonical.sort_unstable(); + self.open_line_sector_ids_by_components + .entry(canonical.into_boxed_slice()) + .or_default() + .push(sector.row.sector_id); + } + ProcessLCSectorKind::OpenLines => {} + } + Ok(()) + } + + fn matching_sector_ids(&self, closed: &[LCColorComponent]) -> Vec { + let mut matching = self + .ordered_sector_ids_by_components + .get(closed) + .into_iter() + .flatten() + .copied() + .collect::>(); + if !self.open_line_sector_ids_by_components.is_empty() { + let mut canonical = closed.to_vec(); + canonical.sort_unstable(); + if let Some(open_line_sectors) = self + .open_line_sector_ids_by_components + .get(canonical.as_slice()) + { + matching.extend(open_line_sectors.iter().copied()); + } + } + matching + } +} + +#[derive(Debug)] +struct ContractedClosureAttempt { + closed_components: Box<[LCColorComponent]>, + key: PendingClosureKey, + contribution: PendingClosureProofContribution, + pairing_rule_id: Option, +} + +fn contracted_closure_support_buckets( + strategy: RecurrenceStrategy, + prepared_sectors: &PreparedClosureSectorCatalog, + materialized_sectors: &BTreeSet, +) -> RusticolResult> { + if !matches!( + strategy, + RecurrenceStrategy::AllFlowUnion | RecurrenceStrategy::ContractedColorUnion + ) { + return Err(invalid( + "bulk closure support buckets require an all-flow or contracted-color union", + )); + } + let mut buckets = BTreeMap::::new(); + for sector_id in materialized_sectors.iter().copied() { + let sector = prepared_sectors.get(sector_id)?; + buckets + .entry(( + sector.anchor_support.clone(), + sector.complement_support.clone(), + )) + .or_default() + .include(strategy, sector)?; + } + Ok(buckets) +} + +#[allow(clippy::too_many_arguments)] +fn include_bulk_color_union_closure_attempt( + strategy: RecurrenceStrategy, + sector: &PreparedClosureSector, + attempt: &ContractedClosureAttempt, + process: &OwnedRecurrenceProcessInput, + process_catalog: &ProcessCatalog<'_>, + color_states: &DynamicLCColorStateInterner, + currents: &[PendingCurrent], + reflection_certificates: &[PendingReflectionCertificate], + prepared_sectors: &PreparedClosureSectorCatalog, + materialized_sectors: &BTreeSet, + result: &mut BTreeMap, +) -> RusticolResult<()> { + if !closed_components_match_prepared_sector(strategy, &attempt.closed_components, sector)? { + return Err(invalid(format!( + "bulk color-union closure routing selected non-matching sector {}", + sector.row.sector_id + ))); + } + let reflection = closure_reflection_projection( + sector.row, + &attempt.closed_components, + process, + process_catalog, + color_states, + currents, + attempt.contribution.construction_parent_ids, + reflection_certificates, + prepared_sectors, + materialized_sectors, + )?; + let mut key = attempt.key.clone(); + key.target_sector_id = sector.row.sector_id; + let mut contribution = attempt.contribution.clone(); + contribution.three_line_certificate = three_line_traversal_certificate( + &attempt.closed_components, + sector.row, + §or.expected_components, + process_catalog, + attempt.pairing_rule_id, + )?; + contribution.reflection_certificate_id = reflection.map(|row| row.certificate_id); + include_closure_with_reflection(result, key, contribution, reflection) +} + +#[allow(clippy::too_many_arguments)] +fn build_bulk_color_union_closures( + strategy: RecurrenceStrategy, + process: &OwnedRecurrenceProcessInput, + process_catalog: &ProcessCatalog<'_>, + template: &OwnedRecurrenceTemplateInput, + fermion_ordering: &FermionOrderingContext, + pairing_catalog: Option>, + prepared_closures: &PreparedClosureCatalog, + prepared_sectors: &PreparedClosureSectorCatalog, + color_states: &DynamicLCColorStateInterner, + currents: &[PendingCurrent], + materialized_sectors: &BTreeSet, + reflection_certificates: &[PendingReflectionCertificate], + construction_domain: Option, + telemetry: &mut RecurrenceGenerationTelemetry, + collect_telemetry: bool, +) -> RusticolResult { + if !matches!( + strategy, + RecurrenceStrategy::AllFlowUnion | RecurrenceStrategy::ContractedColorUnion + ) { + return Err(invalid( + "bulk closure construction requires an all-flow or contracted-color union", + )); + } + let support_index = LaneClosureSupportIndex::new(currents, construction_domain)?; + let support_buckets = + contracted_closure_support_buckets(strategy, prepared_sectors, materialized_sectors)?; + let mut attempts = Vec::::new(); + let mut attempt_indices_by_sector = BTreeMap::>::new(); + + for ((anchor_support, complement_support), bucket) in &support_buckets { + let anchor_ids = support_index + .current_ids(anchor_support) + .iter() + .copied() + .filter(|current_id| { + currents[*current_id as usize].key.node_kind() == RecurrenceNodeKind::Source + }) + .collect::>(); + let complement_ids = support_index.current_ids(complement_support); + let anchor_count = anchor_ids.len(); + let complement_count = complement_ids.len(); + if collect_telemetry { + checked_diagnostic_add( + &mut telemetry.closure_support_lookup_count, + 2, + "telemetry closure-support lookup count", + )?; + let theoretical_count = anchor_count + .checked_mul(complement_count) + .and_then(|count| count.checked_mul(prepared_closures.row_count())) + .and_then(|count| count.checked_mul(bucket.sector_ids.len())) + .ok_or_else(|| invalid("theoretical closure-candidate count exceeds usize"))?; + checked_diagnostic_add( + &mut telemetry.closure_candidate_theoretical_count, + theoretical_count, + "telemetry theoretical closure-candidate count", + )?; + } + for anchor_id in anchor_ids.iter().copied() { + let anchor_state = currents[anchor_id as usize].key.current_state_template_id(); + for &complement_id in complement_ids { + let complement_state = currents[complement_id as usize] + .key + .current_state_template_id(); + let matching_closures = prepared_closures.rows(anchor_state, complement_state); + if collect_telemetry { + checked_diagnostic_add( + &mut telemetry.closure_candidate_count, + matching_closures.len(), + "telemetry closure-candidate count", + )?; + checked_diagnostic_add( + &mut telemetry.closure_state_match_count, + matching_closures.len(), + "telemetry closure-state-match count", + )?; + } + for closure in matching_closures { + let parent_ids = closure.parent_ids( + anchor_state, + complement_state, + anchor_id, + complement_id, + )?; + let parents = [ + ¤ts[parent_ids[0] as usize].key, + ¤ts[parent_ids[1] as usize].key, + ]; + let pairing_certificate_ids = + closure_pairing_certificate_ids(currents, parent_ids, pairing_catalog)?; + let pairing_rule = + pairing_rule_for_certificate(&pairing_certificate_ids, pairing_catalog)?; + for quantum in closure + .quantum_flows + .iter() + .filter(|quantum| quantum.matches(&parents)) + { + let output_factor = quantum.output_factor()?; + let (evaluator_parent_ids, exchange_factor) = + closure.canonical_evaluator_parents(parent_ids); + let evaluator_parent_permutation = two_parent_permutation( + parent_ids, + evaluator_parent_ids, + "closure evaluator order", + )?; + let fermion_factor = fermion_ordering_factor( + &template.current_states, + [ + parents[0].current_state_template_id(), + parents[1].current_state_template_id(), + ], + [ + parents[0].support_source_slots(), + parents[1].support_source_slots(), + ], + fermion_ordering, + )?; + let base_factor = multiply_factors(&[ + closure.closure_exact_factor, + exchange_factor, + closure.contraction_exact_factor, + output_factor, + fermion_factor, + pairing_reconstruction_factor(pairing_rule), + ])?; + let left = color_states + .get(parents[0].dynamic_lc_color_state_id()) + .ok_or_else(|| invalid("closure left color state disappeared"))?; + let right = color_states + .get(parents[1].dynamic_lc_color_state_id()) + .ok_or_else(|| invalid("closure right color state disappeared"))?; + for witness in &closure.witnesses { + if witness.row.left_shape_string_id != left.output_color_shape_id() + || witness.row.right_shape_string_id + != right.output_color_shape_id() + { + continue; + } + let closed = witness.witness.closed_components(left, right)?; + if collect_telemetry { + checked_diagnostic_add( + &mut telemetry.closure_color_attempt_count, + 1, + "telemetry closure-color-attempt count", + )?; + } + let matching_sector_ids = bucket.matching_sector_ids(&closed); + if matching_sector_ids.is_empty() { + continue; + } + if pairing_catalog.is_some() && pairing_certificate_ids.is_empty() { + return Err(invalid(format!( + "closure witness {} for bulk color-union support bucket has no exactly realized fermion pairing", + witness.row.ordinal + ))); + } + let reconstruction_parent_permutation = match witness + .row + .input_permutation + { + 0 => [0, 1], + 1 => [1, 0], + value => { + return Err(invalid(format!( + "closure color witness has invalid parent permutation {value}" + ))); + } + }; + let color_witness_term_id = closure + .contraction + .witness_start + .checked_add(u64::from(witness.row.ordinal)) + .ok_or_else(|| { + invalid("closure color-witness term ID overflows") + })?; + let color_witness_term_id = u32::try_from(color_witness_term_id) + .map_err(|_| { + invalid("closure color-witness term ID exceeds u32") + })?; + let attempt_index = attempts.len(); + attempts.push(ContractedClosureAttempt { + closed_components: closed.into_boxed_slice(), + key: PendingClosureKey { + target_sector_id: MISSING_U32, + complete_source_states: complete_closure_source_states( + parents, + process.external_legs.len(), + )?, + closure_template_id: closure.row.id, + quantum_flow_template_id: quantum.template_id(), + parent_current_ids: evaluator_parent_ids + .to_vec() + .into_boxed_slice(), + }, + contribution: PendingClosureProofContribution { + construction_parent_ids: parent_ids, + construction_parent_permutation: [0, 1], + reconstruction_parent_permutation, + evaluator_parent_permutation, + closure_template_semantic_digest: closure.semantic_digest, + color_witness_term_id, + color_witness_proof_digest: witness.witness.proof_digest(), + three_line_certificate: None, + pairing_certificate_ids: pairing_certificate_ids + .clone() + .into_boxed_slice(), + reflection_certificate_id: None, + exact_factor: base_factor + .checked_mul(witness.witness.exact_factor())?, + }, + pairing_rule_id: pairing_rule.map(|rule| rule.rule_id), + }); + for sector_id in matching_sector_ids { + attempt_indices_by_sector + .entry(sector_id) + .or_default() + .push(attempt_index); + } + } + } + } + } + } + } + + let mut result = BTreeMap::new(); + for sector_id in materialized_sectors.iter().copied() { + let sector = prepared_sectors.get(sector_id)?; + for attempt_index in attempt_indices_by_sector + .get(§or_id) + .into_iter() + .flatten() + .copied() + { + let attempt = &attempts[attempt_index]; + include_bulk_color_union_closure_attempt( + strategy, + sector, + attempt, + process, + process_catalog, + color_states, + currents, + reflection_certificates, + prepared_sectors, + materialized_sectors, + &mut result, + )?; + } + } + if strategy == RecurrenceStrategy::AllFlowUnion { + let covered_sector_ids = result + .keys() + .map(|key| key.target_sector_id) + .collect::>(); + if let Some(sector_id) = materialized_sectors + .iter() + .copied() + .find(|sector_id| !covered_sector_ids.contains(sector_id)) + { + return Err(invalid(format!( + "recurrence builder found no exact closure for physical LC sector {sector_id} after bulk all-flow closure routing" + ))); + } + } + validate_pending_closure_obligations(&result, pairing_catalog, prepared_sectors)?; + if collect_telemetry { + checked_diagnostic_add( + &mut telemetry.closure_group_count, + result.len(), + "telemetry closure-group count", + )?; + checked_diagnostic_add( + &mut telemetry.closure_proof_contribution_count, + result.values().try_fold(0usize, |total, group| { + total + .checked_add(group.contributions.len()) + .ok_or_else(|| invalid("closure proof-contribution count exceeds usize")) + })?, + "telemetry closure-proof-contribution count", + )?; + } + ExhaustiveClosureDomain::new( + result, + materialized_sectors, + closure_domain_sector_owners(strategy, prepared_sectors, materialized_sectors)?, + ) +} + +#[allow(clippy::too_many_arguments)] +fn build_closures( + strategy: RecurrenceStrategy, + process: &OwnedRecurrenceProcessInput, + process_catalog: &ProcessCatalog<'_>, + template: &OwnedRecurrenceTemplateInput, + fermion_ordering: &FermionOrderingContext, + pairing_catalog: Option>, + prepared_closures: &PreparedClosureCatalog, + prepared_sectors: &PreparedClosureSectorCatalog, + color_states: &DynamicLCColorStateInterner, + currents: &[PendingCurrent], + materialized_sectors: &BTreeSet, + stage_diagnostics: &[StageConstructionDiagnostics], + reflection_certificates: &[PendingReflectionCertificate], + construction_domain: Option, + telemetry: &mut RecurrenceGenerationTelemetry, + collect_telemetry: bool, +) -> RusticolResult { + if matches!( + strategy, + RecurrenceStrategy::AllFlowUnion | RecurrenceStrategy::ContractedColorUnion + ) { + return build_bulk_color_union_closures( + strategy, + process, + process_catalog, + template, + fermion_ordering, + pairing_catalog, + prepared_closures, + prepared_sectors, + color_states, + currents, + materialized_sectors, + reflection_certificates, + construction_domain, + telemetry, + collect_telemetry, + ); + } + build_closures_per_sector( + strategy, + process, + process_catalog, + template, + fermion_ordering, + pairing_catalog, + prepared_closures, + prepared_sectors, + color_states, + currents, + materialized_sectors, + stage_diagnostics, + reflection_certificates, + construction_domain, + telemetry, + collect_telemetry, + ) +} + +#[allow(clippy::too_many_arguments)] +fn build_closures_per_sector( + strategy: RecurrenceStrategy, + process: &OwnedRecurrenceProcessInput, + process_catalog: &ProcessCatalog<'_>, + template: &OwnedRecurrenceTemplateInput, + fermion_ordering: &FermionOrderingContext, + pairing_catalog: Option>, + prepared_closures: &PreparedClosureCatalog, + prepared_sectors: &PreparedClosureSectorCatalog, + color_states: &DynamicLCColorStateInterner, + currents: &[PendingCurrent], + materialized_sectors: &BTreeSet, + stage_diagnostics: &[StageConstructionDiagnostics], + reflection_certificates: &[PendingReflectionCertificate], + construction_domain: Option, + telemetry: &mut RecurrenceGenerationTelemetry, + collect_telemetry: bool, +) -> RusticolResult { + let support_index = LaneClosureSupportIndex::new(currents, construction_domain)?; + let mut result = BTreeMap::new(); + let mut covered_sector_ids = BTreeSet::new(); + for sector_id in materialized_sectors.iter().copied() { + let sector = prepared_sectors.get(sector_id)?; + let anchor_ids = support_index + .current_ids(§or.anchor_support) + .iter() + .copied() + .filter(|current_id| { + currents[*current_id as usize].key.node_kind() == RecurrenceNodeKind::Source + }) + .collect::>(); + let complement_ids = support_index.current_ids(§or.complement_support); + let anchor_count = anchor_ids.len(); + let complement_count = complement_ids.len(); + if collect_telemetry { + checked_diagnostic_add( + &mut telemetry.closure_support_lookup_count, + 2, + "telemetry closure-support lookup count", + )?; + let theoretical_count = anchor_count + .checked_mul(complement_count) + .and_then(|count| count.checked_mul(prepared_closures.row_count())) + .ok_or_else(|| invalid("theoretical closure-candidate count exceeds usize"))?; + checked_diagnostic_add( + &mut telemetry.closure_candidate_theoretical_count, + theoretical_count, + "telemetry theoretical closure-candidate count", + )?; + } + let mut state_matched_attempts = 0usize; + let sector_result_start = result.len(); + for anchor_id in anchor_ids.iter().copied() { + let anchor_state = currents[anchor_id as usize].key.current_state_template_id(); + for &complement_id in complement_ids { + let complement_state = currents[complement_id as usize] + .key + .current_state_template_id(); + let matching_closures = prepared_closures.rows(anchor_state, complement_state); + if collect_telemetry { + checked_diagnostic_add( + &mut telemetry.closure_candidate_count, + matching_closures.len(), + "telemetry closure-candidate count", + )?; + checked_diagnostic_add( + &mut telemetry.closure_state_match_count, + matching_closures.len(), + "telemetry closure-state-match count", + )?; + } + state_matched_attempts = state_matched_attempts + .checked_add(matching_closures.len()) + .ok_or_else(|| invalid("closure-attempt count exceeds usize"))?; + for closure in matching_closures { + let parent_ids = closure.parent_ids( + anchor_state, + complement_state, + anchor_id, + complement_id, + )?; + add_closure_terms( + strategy, + sector, + closure, + parent_ids, + process, + process_catalog, + template, + fermion_ordering, + color_states, + currents, + pairing_catalog, + reflection_certificates, + prepared_sectors, + materialized_sectors, + &mut result, + &mut covered_sector_ids, + telemetry, + collect_telemetry, + )?; + } + } + } + if result.len() == sector_result_start && strategy != RecurrenceStrategy::AllFlowUnion { + if strategy == RecurrenceStrategy::ContractedColorUnion { + continue; + } + let closure_color_attempts = collect_closure_color_attempts( + prepared_closures, + color_states, + currents, + &anchor_ids, + complement_ids, + )?; + let mut support_histogram = BTreeMap::::new(); + let mut support_signatures = BTreeSet::new(); + for (id, current) in currents.iter().enumerate() { + if construction_domain.is_some_and(|domain| !domain.contains(id)) { + continue; + } + *support_histogram + .entry(current.key.support_source_slots().len()) + .or_default() += 1; + support_signatures.insert(( + current.key.support_source_slots().len(), + current.key.current_state_template_id(), + current.key.spin_state_class(), + current.key.flavour_flow().to_vec(), + current.key.quantum_number_flow_id(), + current.key.coupling_orders().to_vec(), + )); + } + return Err(invalid(format!( + "recurrence builder found no exact closure for physical LC sector {sector_id} \ + (anchors={anchor_count}, complement_currents={complement_count}, \ + state_matched_attempts={state_matched_attempts}, \ + currents_by_support_size={support_histogram:?}, \ + stage_diagnostics={stage_diagnostics:?}, \ + expected_color_components={:?}, \ + closure_color_attempts={closure_color_attempts:?}, \ + support_signatures={support_signatures:?})", + sector.expected_components, + ))); + } + } + // Do not reject an all-flow sector until every possible direct owner has + // run. Physical sector identifiers need not place a reflection owner + // before its partner; only the authenticated certificate establishes that + // relationship. Coverage is recorded exclusively after the corresponding + // exact row (and optional phase-bearing partner row) was inserted above. + if strategy == RecurrenceStrategy::AllFlowUnion + && let Some(sector_id) = materialized_sectors + .iter() + .copied() + .find(|sector_id| !covered_sector_ids.contains(sector_id)) + { + let sector = prepared_sectors.get(sector_id)?; + let anchor_ids = support_index + .current_ids(§or.anchor_support) + .iter() + .copied() + .filter(|current_id| { + currents[*current_id as usize].key.node_kind() == RecurrenceNodeKind::Source + }) + .collect::>(); + let complement_ids = support_index.current_ids(§or.complement_support); + let anchor_count = anchor_ids.len(); + let complement_count = complement_ids.len(); + let mut state_matched_attempts = 0usize; + for anchor_id in anchor_ids.iter().copied() { + let anchor_state = currents[anchor_id as usize].key.current_state_template_id(); + for complement_id in complement_ids.iter().copied() { + let complement_state = currents[complement_id as usize] + .key + .current_state_template_id(); + state_matched_attempts = state_matched_attempts + .checked_add(prepared_closures.rows(anchor_state, complement_state).len()) + .ok_or_else(|| invalid("closure-attempt count exceeds usize"))?; + } + } + let closure_color_attempts = collect_closure_color_attempts( + prepared_closures, + color_states, + currents, + &anchor_ids, + complement_ids, + )?; + let mut support_histogram = BTreeMap::::new(); + let mut support_signatures = BTreeSet::new(); + for (id, current) in currents.iter().enumerate() { + if construction_domain.is_some_and(|domain| !domain.contains(id)) { + continue; + } + *support_histogram + .entry(current.key.support_source_slots().len()) + .or_default() += 1; + support_signatures.insert(( + current.key.support_source_slots().len(), + current.key.current_state_template_id(), + current.key.spin_state_class(), + current.key.flavour_flow().to_vec(), + current.key.quantum_number_flow_id(), + current.key.coupling_orders().to_vec(), + )); + } + return Err(invalid(format!( + "recurrence builder found no exact closure for physical LC sector {sector_id} \ + (anchors={anchor_count}, complement_currents={complement_count}, \ + state_matched_attempts={state_matched_attempts}, \ + currents_by_support_size={support_histogram:?}, \ + stage_diagnostics={stage_diagnostics:?}, \ + expected_color_components={:?}, \ + closure_color_attempts={closure_color_attempts:?}, \ + support_signatures={support_signatures:?})", + sector.expected_components, + ))); + } + validate_pending_closure_obligations(&result, pairing_catalog, prepared_sectors)?; + if collect_telemetry { + checked_diagnostic_add( + &mut telemetry.closure_group_count, + result.len(), + "telemetry closure-group count", + )?; + checked_diagnostic_add( + &mut telemetry.closure_proof_contribution_count, + result.values().try_fold(0usize, |total, group| { + total + .checked_add(group.contributions.len()) + .ok_or_else(|| invalid("closure proof-contribution count exceeds usize")) + })?, + "telemetry closure-proof-contribution count", + )?; + } + ExhaustiveClosureDomain::new( + result, + materialized_sectors, + closure_domain_sector_owners(strategy, prepared_sectors, materialized_sectors)?, + ) +} + +fn collect_closure_color_attempts( + prepared_closures: &PreparedClosureCatalog, + color_states: &DynamicLCColorStateInterner, + currents: &[PendingCurrent], + anchor_ids: &[u32], + complement_ids: &[u32], +) -> RusticolResult> { + let mut attempts = BTreeSet::new(); + for anchor_id in anchor_ids.iter().copied() { + let anchor_state = currents[anchor_id as usize].key.current_state_template_id(); + for complement_id in complement_ids.iter().copied() { + let complement_state = currents[complement_id as usize] + .key + .current_state_template_id(); + for closure in prepared_closures.rows(anchor_state, complement_state) { + let parent_ids = + closure.parent_ids(anchor_state, complement_state, anchor_id, complement_id)?; + let parents = [ + ¤ts[parent_ids[0] as usize].key, + ¤ts[parent_ids[1] as usize].key, + ]; + if !closure + .quantum_flows + .iter() + .any(|quantum| quantum.matches(&parents)) + { + continue; + } + let left = color_states + .get(parents[0].dynamic_lc_color_state_id()) + .ok_or_else(|| invalid("closure left color state disappeared"))?; + let right = color_states + .get(parents[1].dynamic_lc_color_state_id()) + .ok_or_else(|| invalid("closure right color state disappeared"))?; + for witness in closure.witnesses.iter().filter(|witness| { + witness.row.left_shape_string_id == left.output_color_shape_id() + && witness.row.right_shape_string_id == right.output_color_shape_id() + }) { + let closed = witness.witness.closed_components(left, right)?; + attempts.insert( + closed + .iter() + .map(|component| (component.kind(), component.source_slots().to_vec())) + .collect(), + ); + } + } + } + } + Ok(attempts) +} + +fn validate_pending_closure_obligations( + closures: &BTreeMap, + pairing_catalog: Option>, + prepared_sectors: &PreparedClosureSectorCatalog, +) -> RusticolResult<()> { + let Some(pairing_catalog) = pairing_catalog else { + return Ok(()); + }; + let destinations = closures + .keys() + .map(|key| (key.target_sector_id, key.complete_source_states.clone())) + .collect::>(); + for (sector_id, source_states) in destinations { + let prepared_sector = prepared_sectors.get(sector_id)?; + let sector = prepared_sector.row; + let rows = closures + .iter() + .filter(|(key, _)| { + key.target_sector_id == sector_id && key.complete_source_states == source_states + }) + .flat_map(|(_, group)| group.contributions.iter()) + .collect::>(); + let realized_pairings = rows + .iter() + .flat_map(|row| row.pairing_certificate_ids.iter().copied()) + .collect::>(); + if realized_pairings.is_empty() { + return Err(invalid(format!( + "closure destination for sector {sector_id} realizes no fermion pairing rule" + ))); + } + for pairing_id in &realized_pairings { + if pairing_catalog + .rules() + .iter() + .all(|rule| rule.rule_id != *pairing_id) + { + return Err(invalid(format!( + "closure destination for sector {sector_id} references absent pairing rule {pairing_id}" + ))); + } + } + if sector.kind()? == ProcessLCSectorKind::OpenLines && sector.open_string_range.count == 3 { + for pairing_id in realized_pairings.iter().copied() { + let matching_rows = rows + .iter() + .filter(|row| row.pairing_certificate_ids.contains(&pairing_id)) + .collect::>(); + if matching_rows + .iter() + .any(|row| row.three_line_certificate.is_none()) + { + return Err(invalid(format!( + "three-line closure sector {sector_id} pairing {pairing_id} has an uncertified traversal" + ))); + } + let traversal_kinds = matching_rows + .into_iter() + .map(|row| { + let certificate = row + .three_line_certificate + .as_ref() + .expect("presence checked above"); + if certificate.sector_id != sector_id + || certificate.pairing_rule_id != pairing_id + { + return Err(invalid(format!( + "three-line closure sector {sector_id} pairing {pairing_id} \ + has a traversal certificate for sector {} pairing {}", + certificate.sector_id, certificate.pairing_rule_id, + ))); + } + match u32::from(certificate.kind) { + THREE_LINE_DIRECT_CERTIFICATE_ID + | THREE_LINE_PARTNER_CERTIFICATE_ID => Ok(u32::from(certificate.kind)), + kind => Err(invalid(format!( + "three-line closure sector {sector_id} pairing {pairing_id} \ + has invalid traversal kind {kind}" + ))), + } + }) + .collect::>>()?; + if traversal_kinds.is_empty() { + return Err(invalid(format!( + "three-line closure sector {sector_id} pairing {pairing_id} \ + realizes no certified traversal" + ))); + } + } + } + if prepared_sector.expected_components.len() != sector.open_string_range.count as usize + && sector.kind()? == ProcessLCSectorKind::OpenLines + { + return Err(invalid( + "closure obligation open-line component count is inconsistent", + )); + } + } + Ok(()) +} + +#[allow(clippy::too_many_arguments)] +fn add_closure_terms( + strategy: RecurrenceStrategy, + sector: &PreparedClosureSector, + closure: &PreparedClosure, + parent_ids: [u32; 2], + process: &OwnedRecurrenceProcessInput, + process_catalog: &ProcessCatalog<'_>, + template: &OwnedRecurrenceTemplateInput, + fermion_ordering: &FermionOrderingContext, + color_states: &DynamicLCColorStateInterner, + currents: &[PendingCurrent], + pairing_catalog: Option>, + reflection_certificates: &[PendingReflectionCertificate], + prepared_sectors: &PreparedClosureSectorCatalog, + materialized_sectors: &BTreeSet, + result: &mut BTreeMap, + covered_sector_ids: &mut BTreeSet, + telemetry: &mut RecurrenceGenerationTelemetry, + collect_telemetry: bool, +) -> RusticolResult<()> { + let parents = [ + ¤ts[parent_ids[0] as usize].key, + ¤ts[parent_ids[1] as usize].key, + ]; + let pairing_certificate_ids = + closure_pairing_certificate_ids(currents, parent_ids, pairing_catalog)?; + let pairing_rule = pairing_rule_for_certificate(&pairing_certificate_ids, pairing_catalog)?; + for quantum in closure + .quantum_flows + .iter() + .filter(|quantum| quantum.matches(&parents)) + { + let output_factor = quantum.output_factor()?; + let (evaluator_parent_ids, exchange_factor) = + closure.canonical_evaluator_parents(parent_ids); + let evaluator_parent_permutation = + two_parent_permutation(parent_ids, evaluator_parent_ids, "closure evaluator order")?; + let fermion_factor = fermion_ordering_factor( + &template.current_states, + [ + parents[0].current_state_template_id(), + parents[1].current_state_template_id(), + ], + [ + parents[0].support_source_slots(), + parents[1].support_source_slots(), + ], + fermion_ordering, + )?; + let base_factor = multiply_factors(&[ + closure.closure_exact_factor, + exchange_factor, + closure.contraction_exact_factor, + output_factor, + fermion_factor, + pairing_reconstruction_factor(pairing_rule), + ])?; + for witness in &closure.witnesses { + let left = color_states + .get(parents[0].dynamic_lc_color_state_id()) + .ok_or_else(|| invalid("closure left color state disappeared"))?; + let right = color_states + .get(parents[1].dynamic_lc_color_state_id()) + .ok_or_else(|| invalid("closure right color state disappeared"))?; + if witness.row.left_shape_string_id != left.output_color_shape_id() + || witness.row.right_shape_string_id != right.output_color_shape_id() + { + continue; + } + let closed = witness.witness.closed_components(left, right)?; + if collect_telemetry { + checked_diagnostic_add( + &mut telemetry.closure_color_attempt_count, + 1, + "telemetry closure-color-attempt count", + )?; + } + if !closed_components_match_prepared_sector(strategy, &closed, sector)? { + continue; + } + if pairing_catalog.is_some() && pairing_certificate_ids.is_empty() { + return Err(invalid(format!( + "closure witness {} for sector {} has no exactly realized fermion pairing", + witness.row.ordinal, sector.row.sector_id + ))); + } + let reconstruction_parent_permutation = match witness.row.input_permutation { + 0 => [0, 1], + 1 => [1, 0], + value => { + return Err(invalid(format!( + "closure color witness has invalid parent permutation {value}" + ))); + } + }; + let color_witness_term_id = closure + .contraction + .witness_start + .checked_add(u64::from(witness.row.ordinal)) + .ok_or_else(|| invalid("closure color-witness term ID overflows"))?; + let color_witness_term_id = u32::try_from(color_witness_term_id) + .map_err(|_| invalid("closure color-witness term ID exceeds u32"))?; + let key = PendingClosureKey { + target_sector_id: sector.row.sector_id, + complete_source_states: complete_closure_source_states( + parents, + process.external_legs.len(), + )?, + closure_template_id: closure.row.id, + quantum_flow_template_id: quantum.template_id(), + parent_current_ids: evaluator_parent_ids.into(), + }; + let factor = base_factor.checked_mul(witness.witness.exact_factor())?; + let reflection = closure_reflection_projection( + sector.row, + &closed, + process, + process_catalog, + color_states, + currents, + parent_ids, + reflection_certificates, + prepared_sectors, + materialized_sectors, + )?; + let contribution = PendingClosureProofContribution { + construction_parent_ids: parent_ids, + construction_parent_permutation: [0, 1], + reconstruction_parent_permutation, + evaluator_parent_permutation, + closure_template_semantic_digest: closure.semantic_digest, + color_witness_term_id, + color_witness_proof_digest: witness.witness.proof_digest(), + three_line_certificate: three_line_traversal_certificate( + &closed, + sector.row, + §or.expected_components, + process_catalog, + pairing_rule.map(|rule| rule.rule_id), + )?, + pairing_certificate_ids: pairing_certificate_ids.clone().into_boxed_slice(), + reflection_certificate_id: reflection.map(|row| row.certificate_id), + exact_factor: factor, + }; + let partner_sector_id = reflection.and_then(|row| row.partner_sector_id); + include_closure_with_reflection(result, key, contribution, reflection)?; + covered_sector_ids.insert(sector.row.sector_id); + if let Some(partner_sector_id) = partner_sector_id { + covered_sector_ids.insert(partner_sector_id); + } + } + } + Ok(()) +} + +fn closure_selector_proof_words( + sector_id: u32, + target_helicity_id: Option, + source_states: &[SourceStateAssignment], +) -> RusticolResult> { + let mut words = Vec::with_capacity( + 3usize + .checked_add(source_states.len().saturating_mul(2)) + .ok_or_else(|| invalid("closure selector-proof word count overflows usize"))?, + ); + words.push(u64::from(sector_id)); + words.push(u64::from(target_helicity_id.unwrap_or(MISSING_U32))); + words.push( + u64::try_from(source_states.len()) + .map_err(|_| invalid("closure selector source-state count exceeds u64"))?, + ); + for assignment in source_states { + words.push(u64::from(assignment.source_slot())); + words.push(u64::from(assignment.state_index())); + } + Ok(words) +} + +fn pending_closure_candidate_identity_digest( + key: &PendingClosureKey, + contribution: &PendingClosureProofContribution, + target_helicity_id: Option, + pending: &[PendingCurrent], + dynamic_color_states: &[DynamicLCColorState], + reflection_certificates: &[PendingReflectionCertificate], +) -> RusticolResult { + let selector_words = closure_selector_proof_words( + key.target_sector_id, + target_helicity_id, + &key.complete_source_states, + )?; + let selector_domain_digest = closure_selector_domain_digest_v2(&selector_words)?; + let mut parent_semantic_digests = + Vec::with_capacity(contribution.construction_parent_ids.len()); + let mut parent_color_digests = Vec::with_capacity(contribution.construction_parent_ids.len()); + for old_id in contribution.construction_parent_ids { + let current = pending + .get(old_id as usize) + .ok_or_else(|| invalid("closure candidate source parent is absent"))?; + parent_semantic_digests.push(semantic_digest_from_u32_fields( + b"recurrence-closure-parent-current-v2\0", + [ + old_id, + current.key.current_state_template_id(), + current.key.dynamic_lc_color_state_id().get(), + ], + )?); + parent_color_digests.push(dynamic_color_identity_digest( + dynamic_color_states + .get(current.key.dynamic_lc_color_state_id().get() as usize) + .ok_or_else(|| invalid("closure candidate parent color state is absent"))?, + )?); + } + let reflection_proof_digest = contribution + .reflection_certificate_id + .map(|certificate_id| { + reflection_certificates + .get(certificate_id as usize) + .map(|certificate| certificate.proof_digest) + .ok_or_else(|| { + invalid(format!( + "closure candidate references absent reflection certificate {certificate_id}" + )) + }) + }) + .transpose()?; + closure_candidate_identity_digest_v1( + selector_domain_digest, + key.target_sector_id, + key.closure_template_id, + contribution.closure_template_semantic_digest, + key.quantum_flow_template_id, + &contribution.construction_parent_ids, + &parent_semantic_digests, + &parent_color_digests, + &contribution.construction_parent_permutation, + &contribution.reconstruction_parent_permutation, + &contribution.evaluator_parent_permutation, + contribution.color_witness_term_id, + contribution.color_witness_proof_digest, + contribution + .three_line_certificate + .as_ref() + .map(|certificate| certificate.proof_digest), + &contribution.pairing_certificate_ids, + reflection_proof_digest, + contribution.exact_factor, + 1, + ) +} + +fn pending_selector_sector_domains( + pending: &[PendingCurrent], + pending_closures: &BTreeMap, +) -> RusticolResult>> { + let mut domains = vec![BTreeSet::new(); pending.len()]; + let mut queue = VecDeque::new(); + for (key, group) in pending_closures { + if group.exact_factor.is_zero() { + continue; + } + for parent in key.parent_current_ids.iter().copied() { + let domain = domains + .get_mut(parent as usize) + .ok_or_else(|| invalid("closure references an absent pending current"))?; + if domain.insert(key.target_sector_id) { + queue.push_back(parent); + } + } + } + while let Some(current_id) = queue.pop_front() { + let sectors = domains + .get(current_id as usize) + .ok_or_else(|| invalid("selector-domain queue references an absent current"))? + .clone(); + for (contribution, factor) in &pending[current_id as usize].contributions { + if factor.is_zero() { + continue; + } + for parent in contribution.parent_current_ids.iter().copied() { + let domain = domains + .get_mut(parent as usize) + .ok_or_else(|| invalid("contribution references an absent pending parent"))?; + let before = domain.len(); + domain.extend(sectors.iter().copied()); + if domain.len() != before { + queue.push_back(parent); + } + } + } + } + Ok(domains) +} + +fn projected_contribution_identity( + destination_projection_id: u32, + pending_key: &PendingContributionKey, + exact_factor: ExactComplexRational, + old_to_projection: &BTreeMap, +) -> RusticolResult { + let parent_projection_ids = pending_key + .parent_current_ids + .iter() + .map(|old_id| { + old_to_projection + .get(old_id) + .copied() + .ok_or_else(|| invalid("projected contribution has a dead parent")) + }) + .collect::>>()?; + let key = &pending_key.key; + Ok(ProjectedContributionIdentity { + destination_projection_id, + transition_template_id: key.transition_template_id(), + parent_projection_ids: parent_projection_ids.into_boxed_slice(), + parent_state_template_ids: key.parent_state_template_ids().into(), + parent_momenta: key.parent_momenta().into(), + result_state_template_id: key.result_state_template_id(), + quantum_flow_witness_id: key.quantum_flow_witness_id(), + runtime_coupling_binding_digest: key.runtime_coupling_binding_digest(), + output_projection_id: key.output_projection_id(), + exact_factor, + }) +} + +fn rectangular_parent_domain_is_complete( + parent_projection_ids: &[u32], + builder_parent_tuples: &BTreeSet>, + old_to_projection: &BTreeMap, + projection_members: &[Box<[u32]>], +) -> RusticolResult { + let expected_count = + parent_projection_ids + .iter() + .try_fold(1usize, |count, projection_id| { + let member_count = projection_members + .get(*projection_id as usize) + .ok_or_else(|| { + invalid("rectangular proof references an absent projection class") + })? + .len(); + count + .checked_mul(member_count) + .ok_or_else(|| invalid("rectangular parent-product count exceeds usize")) + })?; + if expected_count != builder_parent_tuples.len() { + return Ok(false); + } + for tuple in builder_parent_tuples { + if tuple.len() != parent_projection_ids.len() { + return Err(invalid( + "rectangular proof parent tuple has inconsistent arity", + )); + } + for (old_id, expected_projection_id) in tuple.iter().zip(parent_projection_ids.iter()) { + if old_to_projection.get(old_id) != Some(expected_projection_id) { + return Ok(false); + } + } + } + // The tuples are unique by construction. Membership in the declared + // classes plus cardinality equal to the complete Cartesian product proves + // that no combination is missing. + Ok(true) +} + +fn plan_topology_replay_color_projection( + pending: &[PendingCurrent], + pending_closures: &BTreeMap, +) -> RusticolResult> { + let selector_domains = pending_selector_sector_domains(pending, pending_closures)?; + let canonical_color_id = DynamicLCColorStateId::from_interner(0); + let mut identity_to_projection = BTreeMap::::new(); + let mut old_to_projection = BTreeMap::new(); + let mut projection_members = Vec::>::new(); + let mut projection_sector_ids = Vec::>::new(); + + for (old_id, current) in pending.iter().enumerate() { + let sectors = &selector_domains[old_id]; + if sectors.is_empty() { + continue; + } + let old_id = + u32::try_from(old_id).map_err(|_| invalid("pending current ID exceeds u32"))?; + let identity = ProjectedCurrentIdentity { + color_erased_key: current_key_with_dynamic_color(¤t.key, canonical_color_id)?, + selector_sector_ids: sectors.iter().copied().collect(), + source_builder_id: (current.key.node_kind() == RecurrenceNodeKind::Source) + .then_some(old_id), + }; + let projection_id = + if let Some(projection_id) = identity_to_projection.get(&identity).copied() { + projection_id + } else { + let projection_id = u32::try_from(projection_members.len()) + .map_err(|_| invalid("projected current count exceeds u32"))?; + projection_sector_ids.push(identity.selector_sector_ids.clone()); + projection_members.push(Vec::new()); + identity_to_projection.insert(identity, projection_id); + projection_id + }; + projection_members[projection_id as usize].push(old_id); + old_to_projection.insert(old_id, projection_id); + } + if projection_members.iter().all(|members| members.len() == 1) { + return Ok(None); + } + let projection_members = projection_members + .into_iter() + .map(Vec::into_boxed_slice) + .collect::>(); + + let mut contributions = + BTreeMap::::new(); + for (old_id, projection_id) in &old_to_projection { + for (pending_key, factor) in &pending[*old_id as usize].contributions { + if factor.is_zero() { + continue; + } + let identity = projected_contribution_identity( + *projection_id, + pending_key, + *factor, + &old_to_projection, + )?; + let parent_tuple = pending_key.parent_current_ids.clone(); + match contributions.entry(identity.clone()) { + std::collections::btree_map::Entry::Vacant(entry) => { + entry.insert(PendingProjectedContribution { + identity, + representative_destination_builder_id: *old_id, + representative_key: pending_key.key.clone(), + destination_builder_ids: BTreeSet::from([*old_id]), + builder_parent_tuples: BTreeSet::from([parent_tuple]), + }); + } + std::collections::btree_map::Entry::Occupied(mut entry) => { + let projected = entry.get_mut(); + projected.destination_builder_ids.insert(*old_id); + if !projected.builder_parent_tuples.insert(parent_tuple) { + // Two builder rows with the same complete numeric + // identity and old-parent tuple are additive, not an + // alias. Retain the unprojected program. + return Ok(None); + } + } + } + } + } + for projected in contributions.values() { + if !rectangular_parent_domain_is_complete( + &projected.identity.parent_projection_ids, + &projected.builder_parent_tuples, + &old_to_projection, + &projection_members, + )? { + return Ok(None); + } + } + + let mut closures = BTreeMap::::new(); + for (key, group) in pending_closures { + if group.exact_factor.is_zero() { + continue; + } + let parent_projection_ids = key + .parent_current_ids + .iter() + .map(|old_id| { + old_to_projection + .get(old_id) + .copied() + .ok_or_else(|| invalid("projected closure has a dead parent")) + }) + .collect::>>()?; + let identity = ProjectedClosureIdentity { + target_sector_id: key.target_sector_id, + complete_source_states: key.complete_source_states.clone(), + closure_template_id: key.closure_template_id, + quantum_flow_template_id: key.quantum_flow_template_id, + parent_projection_ids: parent_projection_ids.into_boxed_slice(), + exact_factor: group.exact_factor, + }; + let parent_tuple = key.parent_current_ids.clone(); + match closures.entry(identity.clone()) { + std::collections::btree_map::Entry::Vacant(entry) => { + entry.insert(PendingProjectedClosure { + identity, + representative_key: key.clone(), + representative_group: group.clone(), + builder_parent_tuples: BTreeSet::from([parent_tuple]), + }); + } + std::collections::btree_map::Entry::Occupied(mut entry) => { + if !entry.get_mut().builder_parent_tuples.insert(parent_tuple) { + return Ok(None); + } + } + } + } + for projected in closures.values() { + if !rectangular_parent_domain_is_complete( + &projected.identity.parent_projection_ids, + &projected.builder_parent_tuples, + &old_to_projection, + &projection_members, + )? { + return Ok(None); + } + } + + Ok(Some(PendingColorProjection { + old_to_projection, + projection_members, + projection_sector_ids, + contributions, + closures, + })) +} + +const COLOR_PROJECTION_CERTIFICATE_BODY_MAGIC: &[u8; 32] = b"PYAMP-COLOR-PROJECTION-BODY-V1\0\0"; + +fn certificate_push_len(output: &mut Vec, len: usize, label: &str) -> RusticolResult<()> { + output.extend_from_slice( + &u64::try_from(len) + .map_err(|_| invalid(format!("{label} length exceeds u64")))? + .to_le_bytes(), + ); + Ok(()) +} + +fn certificate_push_u32_sequence( + output: &mut Vec, + values: &[u32], + label: &str, +) -> RusticolResult<()> { + certificate_push_len(output, values.len(), label)?; + for value in values { + output.extend_from_slice(&value.to_le_bytes()); + } + Ok(()) +} + +fn certificate_push_builder_tuples( + output: &mut Vec, + tuples: &BTreeSet>, + label: &str, +) -> RusticolResult<()> { + certificate_push_len(output, tuples.len(), label)?; + for tuple in tuples { + certificate_push_u32_sequence(output, tuple, label)?; + } + Ok(()) +} + +fn hash_u32_sequence(hash: &mut Sha256, values: &[u32], label: &str) -> RusticolResult<()> { + hash.update( + u64::try_from(values.len()) + .map_err(|_| invalid(format!("{label} length exceeds u64")))? + .to_le_bytes(), + ); + for value in values { + hash.update(value.to_le_bytes()); + } + Ok(()) +} + +fn hash_i32_sequence(hash: &mut Sha256, values: &[i32], label: &str) -> RusticolResult<()> { + hash.update( + u64::try_from(values.len()) + .map_err(|_| invalid(format!("{label} length exceeds u64")))? + .to_le_bytes(), + ); + for value in values { + hash.update(value.to_le_bytes()); + } + Ok(()) +} + +fn hash_momentum(hash: &mut Sha256, momentum: &CanonicalMomentumLinearForm) -> RusticolResult<()> { + hash.update( + u64::try_from(momentum.terms().len()) + .map_err(|_| invalid("projection-certificate momentum length exceeds u64"))? + .to_le_bytes(), + ); + for term in momentum.terms() { + hash.update(term.source_slot.to_le_bytes()); + hash.update(term.coefficient.to_le_bytes()); + } + Ok(()) +} + +fn projected_current_identity_digest( + identity: &ProjectedCurrentIdentity, +) -> RusticolResult { + let key = &identity.color_erased_key; + let mut hash = Sha256::new(); + hash.update(b"pyamplicol-recurrence-color-projected-current-v1\0"); + hash.update(key.catalog_digest().as_bytes()); + hash.update((key.node_kind() as u32).to_le_bytes()); + hash.update(key.current_state_template_id().to_le_bytes()); + hash_u32_sequence( + &mut hash, + key.support_source_slots(), + "projection-certificate support source slots", + )?; + hash_momentum(&mut hash, key.momentum())?; + hash.update((key.helicity_identity().strategy() as u32).to_le_bytes()); + hash.update(key.helicity_identity().spin_state_class().to_le_bytes()); + hash.update( + u64::try_from(key.helicity_identity().local_source_states().len()) + .map_err(|_| invalid("projection-certificate helicity ancestry exceeds u64"))? + .to_le_bytes(), + ); + for assignment in key.helicity_identity().local_source_states() { + hash.update(assignment.source_slot().to_le_bytes()); + hash.update(assignment.state_index().to_le_bytes()); + } + hash_i32_sequence( + &mut hash, + key.flavour_flow(), + "projection-certificate flavour flow", + )?; + hash.update(key.quantum_number_flow_id().to_le_bytes()); + hash_u32_sequence( + &mut hash, + key.coupling_orders(), + "projection-certificate coupling orders", + )?; + match key.source_binding() { + CurrentSourceBinding::None => hash.update(0_u32.to_le_bytes()), + CurrentSourceBinding::FixedTemplate(template_id) => { + hash.update(1_u32.to_le_bytes()); + hash.update(template_id.to_le_bytes()); + } + CurrentSourceBinding::RuntimeDispatch { + domain, + source_template_ids, + variant_bindings, + } => { + hash.update(2_u32.to_le_bytes()); + hash.update(domain.to_le_bytes()); + hash_u32_sequence( + &mut hash, + source_template_ids, + "projection-certificate runtime source templates", + )?; + hash.update( + u64::try_from(variant_bindings.len()) + .map_err(|_| { + invalid("projection-certificate runtime source variants exceed u64") + })? + .to_le_bytes(), + ); + for variant in variant_bindings { + hash.update(variant.source_state_index().to_le_bytes()); + hash.update(variant.public_helicity().to_le_bytes()); + hash.update(variant.runtime_variant_id().to_le_bytes()); + hash.update(variant.source_template_id().to_le_bytes()); + hash.update(variant.source_state_template_id().to_le_bytes()); + hash.update(variant.crossed_state_template_id().to_le_bytes()); + hash.update(variant.crossed_spin_state_class().to_le_bytes()); + hash_exact_factor(&mut hash, variant.crossing_factor()); + } + } + } + hash.update( + key.propagator_template_id() + .unwrap_or(MISSING_U32) + .to_le_bytes(), + ); + hash_u32_sequence( + &mut hash, + &identity.selector_sector_ids, + "projection-certificate selector sectors", + )?; + hash.update( + identity + .source_builder_id + .unwrap_or(MISSING_U32) + .to_le_bytes(), + ); + SemanticDigest::new(hash.finalize().into()) +} + +fn projected_contribution_identity_digest( + identity: &ProjectedContributionIdentity, +) -> RusticolResult { + let mut hash = Sha256::new(); + hash.update(b"pyamplicol-recurrence-color-projected-contribution-v1\0"); + hash.update(identity.destination_projection_id.to_le_bytes()); + hash.update(identity.transition_template_id.to_le_bytes()); + hash_u32_sequence( + &mut hash, + &identity.parent_projection_ids, + "projection-certificate contribution parents", + )?; + hash_u32_sequence( + &mut hash, + &identity.parent_state_template_ids, + "projection-certificate contribution parent states", + )?; + hash.update( + u64::try_from(identity.parent_momenta.len()) + .map_err(|_| invalid("projection-certificate parent momenta exceed u64"))? + .to_le_bytes(), + ); + for momentum in &identity.parent_momenta { + hash_momentum(&mut hash, momentum)?; + } + hash.update(identity.result_state_template_id.to_le_bytes()); + hash.update(identity.quantum_flow_witness_id.to_le_bytes()); + hash.update(identity.runtime_coupling_binding_digest.as_bytes()); + hash.update(identity.output_projection_id.to_le_bytes()); + hash_exact_factor(&mut hash, identity.exact_factor); + SemanticDigest::new(hash.finalize().into()) +} + +fn projected_closure_identity_digest( + identity: &ProjectedClosureIdentity, +) -> RusticolResult { + let mut hash = Sha256::new(); + hash.update(b"pyamplicol-recurrence-color-projected-closure-v1\0"); + hash.update(identity.target_sector_id.to_le_bytes()); + hash.update( + u64::try_from(identity.complete_source_states.len()) + .map_err(|_| invalid("projection-certificate closure source states exceed u64"))? + .to_le_bytes(), + ); + for assignment in &identity.complete_source_states { + hash.update(assignment.source_slot().to_le_bytes()); + hash.update(assignment.state_index().to_le_bytes()); + } + hash.update(identity.closure_template_id.to_le_bytes()); + hash.update( + identity + .quantum_flow_template_id + .unwrap_or(MISSING_U32) + .to_le_bytes(), + ); + hash_u32_sequence( + &mut hash, + &identity.parent_projection_ids, + "projection-certificate closure parents", + )?; + hash_exact_factor(&mut hash, identity.exact_factor); + SemanticDigest::new(hash.finalize().into()) +} + +fn encode_color_projection_certificate_body( + pending: &[PendingCurrent], + dynamic_color_states: &[DynamicLCColorState], + projection: &PendingColorProjection, + original_candidate_identity_digests: &[SemanticDigest], +) -> RusticolResult> { + let mut output = Vec::new(); + output.extend_from_slice(COLOR_PROJECTION_CERTIFICATE_BODY_MAGIC); + output.extend_from_slice(&1_u32.to_le_bytes()); + output.extend_from_slice( + &u64::try_from(projection.old_to_projection.len()) + .map_err(|_| invalid("projection-certificate old current count exceeds u64"))? + .to_le_bytes(), + ); + output.extend_from_slice( + &u64::try_from(projection.projection_members.len()) + .map_err(|_| invalid("projection-certificate current count exceeds u64"))? + .to_le_bytes(), + ); + output.extend_from_slice( + &u64::try_from(projection.contributions.len()) + .map_err(|_| invalid("projection-certificate contribution count exceeds u64"))? + .to_le_bytes(), + ); + output.extend_from_slice( + &u64::try_from(projection.closures.len()) + .map_err(|_| invalid("projection-certificate closure count exceeds u64"))? + .to_le_bytes(), + ); + + for (projection_index, members) in projection.projection_members.iter().enumerate() { + let projection_id = u32::try_from(projection_index) + .map_err(|_| invalid("projection-certificate current ID exceeds u32"))?; + output.extend_from_slice(&projection_id.to_le_bytes()); + let representative_id = *members + .first() + .ok_or_else(|| invalid("projection-certificate current class is empty"))?; + let representative = pending + .get(representative_id as usize) + .ok_or_else(|| invalid("projection-certificate current representative is absent"))?; + let identity = ProjectedCurrentIdentity { + color_erased_key: current_key_with_dynamic_color( + &representative.key, + DynamicLCColorStateId::from_interner(0), + )?, + selector_sector_ids: projection.projection_sector_ids[projection_index].clone(), + source_builder_id: (representative.key.node_kind() == RecurrenceNodeKind::Source) + .then_some(representative_id), + }; + output.extend_from_slice(projected_current_identity_digest(&identity)?.as_bytes()); + certificate_push_u32_sequence( + &mut output, + &projection.projection_sector_ids[projection_index], + "projection-certificate current selector sectors", + )?; + certificate_push_len( + &mut output, + members.len(), + "projection-certificate current members", + )?; + for old_id in members { + output.extend_from_slice(&old_id.to_le_bytes()); + let current = pending + .get(*old_id as usize) + .ok_or_else(|| invalid("projection-certificate current member is absent"))?; + let color_state = dynamic_color_states + .get(current.key.dynamic_lc_color_state_id().get() as usize) + .ok_or_else(|| invalid("projection-certificate color state is absent"))?; + output.extend_from_slice(dynamic_color_identity_digest(color_state)?.as_bytes()); + } + } + + for projected in projection.contributions.values() { + output.extend_from_slice( + projected_contribution_identity_digest(&projected.identity)?.as_bytes(), + ); + output.extend_from_slice(&projected.identity.destination_projection_id.to_le_bytes()); + output.extend_from_slice( + &projected + .representative_destination_builder_id + .to_le_bytes(), + ); + let witness = projected.representative_key.color_witness_term_id(); + output.extend_from_slice(&witness.color_contraction_template_id().to_le_bytes()); + output.extend_from_slice(&witness.witness_ordinal().to_le_bytes()); + certificate_push_u32_sequence( + &mut output, + &projected.identity.parent_projection_ids, + "projection-certificate contribution projected parents", + )?; + certificate_push_len( + &mut output, + projected.destination_builder_ids.len(), + "projection-certificate contribution destinations", + )?; + for destination_id in &projected.destination_builder_ids { + output.extend_from_slice(&destination_id.to_le_bytes()); + } + certificate_push_builder_tuples( + &mut output, + &projected.builder_parent_tuples, + "projection-certificate contribution parent tuples", + )?; + for rational in [ + projected.identity.exact_factor.real(), + projected.identity.exact_factor.imag(), + ] { + output.extend_from_slice(&rational.numerator().to_le_bytes()); + output.extend_from_slice(&rational.denominator().to_le_bytes()); + } + } + + for projected in projection.closures.values() { + output + .extend_from_slice(projected_closure_identity_digest(&projected.identity)?.as_bytes()); + output.extend_from_slice(&projected.identity.target_sector_id.to_le_bytes()); + output.extend_from_slice(&projected.identity.closure_template_id.to_le_bytes()); + output.extend_from_slice( + &projected + .identity + .quantum_flow_template_id + .unwrap_or(MISSING_U32) + .to_le_bytes(), + ); + certificate_push_u32_sequence( + &mut output, + &projected.identity.parent_projection_ids, + "projection-certificate closure projected parents", + )?; + certificate_push_u32_sequence( + &mut output, + &projected.representative_key.parent_current_ids, + "projection-certificate closure representative parents", + )?; + certificate_push_builder_tuples( + &mut output, + &projected.builder_parent_tuples, + "projection-certificate closure parent tuples", + )?; + for rational in [ + projected.identity.exact_factor.real(), + projected.identity.exact_factor.imag(), + ] { + output.extend_from_slice(&rational.numerator().to_le_bytes()); + output.extend_from_slice(&rational.denominator().to_le_bytes()); + } + } + + let mut candidate_digests = original_candidate_identity_digests.to_vec(); + candidate_digests.sort_unstable(); + certificate_push_len( + &mut output, + candidate_digests.len(), + "projection-certificate original closure candidates", + )?; + for digest in candidate_digests { + output.extend_from_slice(digest.as_bytes()); + } + let digest: [u8; 32] = Sha256::digest(&output).into(); + output.extend_from_slice(&digest); + Ok(output) +} + +#[allow(clippy::too_many_arguments)] +fn append_closure_proof_group( + key: &PendingClosureKey, + group: &PendingClosureGroup, + pending: &[PendingCurrent], + dynamic_color_states: &[DynamicLCColorState], + remap: &BTreeMap, + target_destination_id: Option, + target_helicity_id: Option, + runtime_term_id: Option, + three_line_certificate_ids: &BTreeMap, + closure_proof_contributions: &mut Vec, + closure_proof_groups: &mut Vec, +) -> RusticolResult<()> { + if group.exact_factor.is_zero() != runtime_term_id.is_none() { + return Err(invalid( + "closure proof runtime binding does not match its exact aggregate factor", + )); + } + if runtime_term_id.is_some() && target_destination_id.is_none() { + return Err(invalid( + "nonzero closure proof group has no amplitude destination", + )); + } + let proof_start = u64::try_from(closure_proof_contributions.len()) + .map_err(|_| invalid("closure proof contribution count exceeds u64"))?; + let mut component_factors = Vec::with_capacity(group.contributions.len()); + for contribution in &group.contributions { + let builder_parent_ids = contribution.construction_parent_ids.to_vec(); + let runtime_parent_ids = contribution + .construction_parent_ids + .iter() + .map(|old_id| remap.get(old_id).copied()) + .collect::>(); + if runtime_term_id.is_some() && runtime_parent_ids.iter().any(Option::is_none) { + return Err(invalid( + "nonzero closure proof contribution has a dead runtime parent", + )); + } + let mut parent_semantic_digests = + Vec::with_capacity(contribution.construction_parent_ids.len()); + let mut parent_color_digests = + Vec::with_capacity(contribution.construction_parent_ids.len()); + for old_id in contribution.construction_parent_ids { + let current = pending + .get(old_id as usize) + .ok_or_else(|| invalid("closure proof source parent is absent"))?; + parent_semantic_digests.push(semantic_digest_from_u32_fields( + b"recurrence-closure-parent-current-v2\0", + [ + old_id, + current.key.current_state_template_id(), + current.key.dynamic_lc_color_state_id().get(), + ], + )?); + parent_color_digests.push(dynamic_color_identity_digest( + dynamic_color_states + .get(current.key.dynamic_lc_color_state_id().get() as usize) + .ok_or_else(|| invalid("closure proof parent color state is absent"))?, + )?); + } + let proof_id = u32::try_from(closure_proof_contributions.len()) + .map_err(|_| invalid("closure proof contribution count exceeds u32"))?; + closure_proof_contributions.push(ClosureProofContributionV2::new( + proof_id, + key.target_sector_id, + target_destination_id, + target_helicity_id, + key.closure_template_id, + contribution.closure_template_semantic_digest, + key.quantum_flow_template_id, + builder_parent_ids, + runtime_parent_ids, + parent_semantic_digests, + parent_color_digests, + contribution.construction_parent_permutation.to_vec(), + contribution.reconstruction_parent_permutation.to_vec(), + contribution.evaluator_parent_permutation.to_vec(), + contribution.color_witness_term_id, + contribution.color_witness_proof_digest, + contribution + .three_line_certificate + .as_ref() + .map(|certificate| three_line_certificate_ids[certificate]), + contribution.pairing_certificate_ids.to_vec(), + contribution.reflection_certificate_id, + contribution.exact_factor, + 1, + )?); + component_factors.push(contribution.exact_factor); + } + let proof_count = u64::try_from(closure_proof_contributions.len()) + .map_err(|_| invalid("closure proof contribution count exceeds u64"))? + .checked_sub(proof_start) + .ok_or_else(|| invalid("closure proof contribution range underflows"))?; + let selector_words = closure_selector_proof_words( + key.target_sector_id, + target_helicity_id, + &key.complete_source_states, + )?; + closure_proof_groups.push(ClosureExecutionProofGroupV2::new( + u32::try_from(closure_proof_groups.len()) + .map_err(|_| invalid("closure proof group count exceeds u32"))?, + runtime_term_id, + None, + CheckedTableRange::new(proof_start, proof_count), + group.exact_factor, + closure_component_factor_digest_v2(&component_factors)?, + closure_selector_domain_digest_v2(&selector_words)?, + )?); + Ok(()) +} + +#[allow(clippy::too_many_arguments)] +#[cfg(any(test, feature = "on-the-fly-test-support"))] +fn observe_established_selected_transition_slice( + process_catalog: &ProcessCatalog<'_>, + pending: &[PendingCurrent], + pending_closures: &BTreeMap, + replay_targets: &[RecurrenceReplayTarget], + dynamic_color_states: &[DynamicLCColorState], +) -> RusticolResult)>> { + let Some(selection) = super::diagnostic::transition_diagnostic_selection() else { + return Ok(None); + }; + let replay_target = replay_targets + .iter() + .find(|target| target.target_sector_id() == selection.public_flow_id) + .ok_or_else(|| { + invalid(format!( + "transition diagnostic public flow {} has no replay target", + selection.public_flow_id + )) + })?; + let source_states = super::diagnostic::representative_source_states_for_public_helicities( + replay_target.source_slot_permutation(), + &selection.public_helicities, + |representative_slot, public_helicity| { + let leg = process_catalog + .input + .external_legs + .get(representative_slot as usize) + .ok_or_else(|| { + invalid("transition diagnostic representative source leg is absent") + })?; + let range = leg.source_state_range.as_usize_range( + process_catalog.input.source_states.len(), + "transition diagnostic representative source states", + )?; + let mut matches = process_catalog.input.source_states[range] + .iter() + .filter(|state| { + state.source_slot == representative_slot + && state.public_helicity == public_helicity + }); + let state = matches.next().ok_or_else(|| { + invalid(format!( + "transition diagnostic representative source slot {representative_slot} has no state for transported public helicity {public_helicity}" + )) + })?; + if matches.next().is_some() { + return Err(invalid(format!( + "transition diagnostic representative source slot {representative_slot} has multiple states for transported public helicity {public_helicity}" + ))); + } + Ok(state.state_index) + }, + )? + .into_boxed_slice(); + let materialized_sector_id = replay_target.materialized_sector_id(); + let mut live = BTreeSet::new(); + let mut queue = VecDeque::new(); + for (key, _group) in pending_closures.iter().filter(|(key, group)| { + key.target_sector_id == materialized_sector_id + && key.complete_source_states == source_states + && !group.exact_factor.is_zero() + }) { + for parent in key.parent_current_ids.iter().copied() { + if live.insert(parent) { + queue.push_back(parent); + } + } + } + if live.is_empty() { + return Err(invalid(format!( + "transition diagnostic flow {} and source states {:?} have no established closure", + selection.public_flow_id, source_states + ))); + } + while let Some(current_id) = queue.pop_front() { + let current = pending + .get(current_id as usize) + .ok_or_else(|| invalid("transition diagnostic current is absent"))?; + for (contribution, factor) in ¤t.contributions { + if factor.is_zero() { + continue; + } + for parent in contribution.parent_current_ids.iter().copied() { + if live.insert(parent) { + queue.push_back(parent); + } + } + } + } + let digests = live + .into_iter() + .map(|current_id| { + let current = pending + .get(current_id as usize) + .ok_or_else(|| invalid("transition diagnostic live current is absent"))?; + let color = dynamic_color_states + .get(current.key.dynamic_lc_color_state_id().get() as usize) + .ok_or_else(|| invalid("transition diagnostic live color is absent"))?; + super::on_the_fly::hash_current_key(¤t.key, color) + }) + .collect::>>()?; + super::diagnostic::observe_transition_live_current_digests(digests)?; + Ok(Some((materialized_sector_id, source_states))) +} + +fn consume_ordered_closure_destination( + rows: &mut Peekable, + sector_id: u32, + source_states: &[SourceStateAssignment], + destination: Destination, + mut emit: Emit, +) -> RusticolResult<()> +where + I: Iterator, + Destination: for<'row> Fn(&'row Row) -> (u32, &'row [SourceStateAssignment]), + Emit: FnMut(Row) -> RusticolResult<()>, +{ + while let Some(row) = rows.peek() { + let (row_sector_id, row_source_states) = destination(row); + let ordering = row_sector_id + .cmp(§or_id) + .then_with(|| row_source_states.cmp(source_states)); + match ordering { + std::cmp::Ordering::Less => { + return Err(invalid( + "ordered closure emission skipped a destination prefix", + )); + } + std::cmp::Ordering::Equal => emit( + rows.next() + .expect("peeked closure destination row disappeared"), + )?, + std::cmp::Ordering::Greater => break, + } + } + Ok(()) +} + +fn require_ordered_closure_destinations_exhausted(rows: &mut Peekable) -> RusticolResult<()> +where + I: Iterator, +{ + if rows.peek().is_some() { + return Err(invalid( + "ordered closure emission left a destination prefix unconsumed", + )); + } + Ok(()) +} + +#[allow(clippy::too_many_arguments)] +fn complete_closure_target_domain_certificate( + strategy: RecurrenceStrategy, + process: &OwnedRecurrenceProcessInput, + sector_count: usize, + exhaustive_closure_sector_ids: &BTreeSet, + exhaustive_closure_sector_owner_ids: &BTreeMap, + replay_targets: &[RecurrenceReplayTarget], + helicity_ids: &BTreeMap, u32>, + pending_closures: &BTreeMap, + candidate_identity_digests_by_group: &[Vec], +) -> RusticolResult> { + if strategy != RecurrenceStrategy::ContractedColorUnion { + return Ok(None); + } + let physical_sector_count = u32::try_from(sector_count) + .map_err(|_| invalid("closure target-domain physical-sector count exceeds u32"))?; + let expected_sector_ids = (0..physical_sector_count).collect::>(); + if exhaustive_closure_sector_owner_ids + .keys() + .copied() + .collect::>() + != *exhaustive_closure_sector_ids + || exhaustive_closure_sector_owner_ids + .iter() + .any(|(sector_id, owner_id)| { + !exhaustive_closure_sector_ids.contains(owner_id) + || owner_id > sector_id + || exhaustive_closure_sector_owner_ids.get(owner_id) != Some(owner_id) + }) + { + return Err(invalid( + "contracted closure target-domain certificate has an invalid owner partition", + )); + } + let resolved_helicity_count = u32::try_from(helicity_ids.len()) + .map_err(|_| invalid("closure target-domain resolved-helicity count exceeds u32"))?; + if resolved_helicity_count == 0 { + return Err(invalid( + "contracted closure target-domain certificate has no resolved helicities", + )); + } + if candidate_identity_digests_by_group.len() != pending_closures.len() { + return Err(invalid( + "closure target-domain candidate groups do not match pending closures", + )); + } + + let mut ordered_helicities = helicity_ids + .iter() + .map(|(states, helicity_id)| (*helicity_id, states.as_ref())) + .collect::>(); + ordered_helicities.sort_unstable_by_key(|(helicity_id, _)| *helicity_id); + if ordered_helicities + .iter() + .enumerate() + .any(|(index, (helicity_id, _))| *helicity_id != index as u32) + { + return Err(invalid( + "closure target-domain resolved-helicity IDs are not dense", + )); + } + + let mut closure_source_sector_ids = BTreeMap::::new(); + let mut representative_helicity_ids_by_target = BTreeMap::>::new(); + if replay_targets.is_empty() { + if exhaustive_closure_sector_ids != &expected_sector_ids { + return Err(invalid( + "contracted closure target-domain certificate lacks exhaustive physical-sector coverage", + )); + } + let identity_helicity_ids = (0..resolved_helicity_count).collect::>(); + for sector_id in expected_sector_ids.iter().copied() { + closure_source_sector_ids + .insert(sector_id, exhaustive_closure_sector_owner_ids[§or_id]); + representative_helicity_ids_by_target.insert(sector_id, identity_helicity_ids.clone()); + } + } else { + if replay_targets.len() != sector_count { + return Err(invalid( + "contracted closure target-domain replay does not cover every physical sector", + )); + } + let replay_materialized_sector_ids = replay_targets + .iter() + .map(RecurrenceReplayTarget::materialized_sector_id) + .collect::>(); + if replay_materialized_sector_ids != *exhaustive_closure_sector_ids { + return Err(invalid( + "contracted closure target-domain replay representatives disagree with the exhaustive closure domain", + )); + } + + let mut source_template_by_state = BTreeMap::<(u32, u32), u32>::new(); + let mut source_state_by_template = BTreeMap::<(u32, u32), u32>::new(); + for state in &process.source_states { + if source_template_by_state + .insert( + (state.source_slot, state.state_index), + state.source_template_id, + ) + .is_some() + || source_state_by_template + .insert( + (state.source_slot, state.source_template_id), + state.state_index, + ) + .is_some() + { + return Err(invalid( + "contracted closure target-domain replay source states are not bijective", + )); + } + } + let source_count = process.external_legs.len(); + for (target_index, target) in replay_targets.iter().enumerate() { + let target_sector_id = u32::try_from(target_index) + .map_err(|_| invalid("closure target-domain replay target exceeds u32"))?; + if target.id() != target_sector_id + || target.target_sector_id() != target_sector_id + || !exhaustive_closure_sector_ids.contains(&target.materialized_sector_id()) + { + return Err(invalid( + "contracted closure target-domain replay targets are not canonical", + )); + } + let mut permutation = target.source_slot_permutation().to_vec(); + permutation.sort_unstable(); + if permutation.len() != source_count + || permutation + .iter() + .copied() + .enumerate() + .any(|(slot, value)| value as usize != slot) + || target.source_momentum_signs().len() != source_count + { + return Err(invalid( + "contracted closure target-domain replay source mapping is not bijective", + )); + } + + let mut inverse_helicity_ids = vec![MISSING_U32; helicity_ids.len()]; + for (representative_helicity_id, source_states) in &ordered_helicities { + let mut mapped = vec![None; source_count]; + for assignment in *source_states { + let representative_slot = assignment.source_slot(); + let source_template_id = source_template_by_state + .get(&(representative_slot, assignment.state_index())) + .copied() + .ok_or_else(|| { + invalid("closure target-domain replay helicity has no source template") + })?; + let target_slot = target + .source_slot_permutation() + .get(representative_slot as usize) + .copied() + .ok_or_else(|| { + invalid( + "closure target-domain replay source permutation is out of bounds", + ) + })?; + let target_state_index = source_state_by_template + .get(&(target_slot, source_template_id)) + .copied() + .ok_or_else(|| { + invalid( + "closure target-domain replay cannot transport a source template", + ) + })?; + let target_index = usize::try_from(target_slot) + .map_err(|_| invalid("closure replay source slot exceeds usize"))?; + let target_state = mapped.get_mut(target_index).ok_or_else(|| { + invalid("closure target-domain replay target slot is out of bounds") + })?; + if target_state + .replace(SourceStateAssignment::new(target_slot, target_state_index)) + .is_some() + { + return Err(invalid( + "closure target-domain replay helicity mapping is not bijective", + )); + } + } + let mapped = mapped + .into_iter() + .collect::>>() + .ok_or_else(|| { + invalid("closure target-domain replay does not map every source state") + })?; + let mapped_helicity_id = helicity_ids.get(mapped.as_slice()).copied().ok_or_else( + || { + invalid( + "closure target-domain replay maps outside retained helicity coverage", + ) + }, + )?; + let inverse_slot = inverse_helicity_ids + .get_mut(mapped_helicity_id as usize) + .ok_or_else(|| { + invalid("closure target-domain replay helicity ID is out of bounds") + })?; + if *inverse_slot != MISSING_U32 { + return Err(invalid( + "closure target-domain replay helicity mapping is not one-to-one", + )); + } + *inverse_slot = *representative_helicity_id; + } + if inverse_helicity_ids.contains(&MISSING_U32) { + return Err(invalid( + "closure target-domain replay helicity mapping is incomplete", + )); + } + closure_source_sector_ids.insert( + target_sector_id, + exhaustive_closure_sector_owner_ids[&target.materialized_sector_id()], + ); + representative_helicity_ids_by_target.insert(target_sector_id, inverse_helicity_ids); + } + if closure_source_sector_ids + .keys() + .copied() + .collect::>() + != expected_sector_ids + { + return Err(invalid( + "contracted closure target-domain replay target IDs are not dense", + )); + } + } + + let mut groups_by_target = BTreeMap::<(u32, u32), Vec>::new(); + let mut hash = Sha256::new(); + hash.update(b"pyamplicol-complete-recurrence-closure-target-domain-v1\0"); + hash.update(physical_sector_count.to_le_bytes()); + hash.update(resolved_helicity_count.to_le_bytes()); + hash.update( + u64::try_from(exhaustive_closure_sector_ids.len()) + .map_err(|_| invalid("closure target-domain exhaustive sector count exceeds u64"))? + .to_le_bytes(), + ); + for sector_id in exhaustive_closure_sector_ids { + hash.update(sector_id.to_le_bytes()); + hash.update(exhaustive_closure_sector_owner_ids[sector_id].to_le_bytes()); + } + hash.update( + u64::try_from(replay_targets.len()) + .map_err(|_| invalid("closure target-domain replay-target count exceeds u64"))? + .to_le_bytes(), + ); + for target in replay_targets { + hash.update(target.id().to_le_bytes()); + hash.update(target.materialized_sector_id().to_le_bytes()); + hash.update(target.target_sector_id().to_le_bytes()); + hash.update( + u64::try_from(target.source_slot_permutation().len()) + .map_err(|_| invalid("closure replay source count exceeds u64"))? + .to_le_bytes(), + ); + for source_slot in target.source_slot_permutation() { + hash.update(source_slot.to_le_bytes()); + } + for momentum_sign in target.source_momentum_signs() { + hash.update(momentum_sign.to_le_bytes()); + } + hash_exact_factor(&mut hash, target.amplitude_factor()); + } + for (helicity_id, source_states) in &ordered_helicities { + hash.update(helicity_id.to_le_bytes()); + hash.update( + u64::try_from(source_states.len()) + .map_err(|_| invalid("closure target-domain source-state count exceeds u64"))? + .to_le_bytes(), + ); + for assignment in *source_states { + hash.update(assignment.source_slot().to_le_bytes()); + hash.update(assignment.state_index().to_le_bytes()); + } + } + + hash.update( + u64::try_from(pending_closures.len()) + .map_err(|_| invalid("closure target-domain group count exceeds u64"))? + .to_le_bytes(), + ); + for (group_index, ((key, group), candidate_digests)) in pending_closures + .iter() + .zip(candidate_identity_digests_by_group) + .enumerate() + { + if candidate_digests.len() != group.contributions.len() || candidate_digests.is_empty() { + return Err(invalid( + "closure target-domain candidate identities do not match their proof group", + )); + } + hash.update(key.target_sector_id.to_le_bytes()); + hash.update( + u64::try_from(key.complete_source_states.len()) + .map_err(|_| invalid("closure target-domain source-state count exceeds u64"))? + .to_le_bytes(), + ); + for assignment in &key.complete_source_states { + hash.update(assignment.source_slot().to_le_bytes()); + hash.update(assignment.state_index().to_le_bytes()); + } + hash.update(key.closure_template_id.to_le_bytes()); + hash.update( + key.quantum_flow_template_id + .unwrap_or(MISSING_U32) + .to_le_bytes(), + ); + hash.update( + u64::try_from(key.parent_current_ids.len()) + .map_err(|_| invalid("closure target-domain parent count exceeds u64"))? + .to_le_bytes(), + ); + for parent_id in &key.parent_current_ids { + hash.update(parent_id.to_le_bytes()); + } + hash_exact_factor(&mut hash, group.exact_factor); + let mut candidate_digests = candidate_digests.clone(); + candidate_digests.sort_unstable(); + hash.update( + u64::try_from(candidate_digests.len()) + .map_err(|_| invalid("closure target-domain candidate count exceeds u64"))? + .to_le_bytes(), + ); + for digest in candidate_digests { + hash.update(digest.as_bytes()); + } + if let Some(helicity_id) = helicity_ids.get(&key.complete_source_states).copied() { + groups_by_target + .entry((key.target_sector_id, helicity_id)) + .or_default() + .push(group_index); + } else if !group.exact_factor.is_zero() { + return Err(invalid( + "nonzero contracted closure group is outside the resolved-helicity domain", + )); + } + } + + let pending_groups = pending_closures.values().collect::>(); + let mut structural_zero_sector_ids = Vec::new(); + for sector_id in 0..physical_sector_count { + let closure_source_sector_id = closure_source_sector_ids[§or_id]; + let representative_helicity_ids = &representative_helicity_ids_by_target[§or_id]; + let mut sector_has_nonzero_group = false; + for target_helicity_id in 0..resolved_helicity_count { + let representative_helicity_id = + representative_helicity_ids[target_helicity_id as usize]; + let group_indices = groups_by_target + .get(&(closure_source_sector_id, representative_helicity_id)) + .map(Vec::as_slice) + .unwrap_or_default(); + hash.update(sector_id.to_le_bytes()); + hash.update(closure_source_sector_id.to_le_bytes()); + hash.update(target_helicity_id.to_le_bytes()); + hash.update(representative_helicity_id.to_le_bytes()); + hash.update( + u64::try_from(group_indices.len()) + .map_err(|_| invalid("closure target-domain target group count exceeds u64"))? + .to_le_bytes(), + ); + for group_index in group_indices { + hash.update( + u64::try_from(*group_index) + .map_err(|_| invalid("closure target-domain group index exceeds u64"))? + .to_le_bytes(), + ); + sector_has_nonzero_group |= !pending_groups[*group_index].exact_factor.is_zero(); + } + } + if !sector_has_nonzero_group { + structural_zero_sector_ids.push(sector_id); + } + } + let semantic_digest = SemanticDigest::new(hash.finalize().into())?; + ClosureTargetDomainCertificateV1::new( + physical_sector_count, + resolved_helicity_count, + structural_zero_sector_ids, + semantic_digest, + ) + .map(Some) +} + +#[allow(clippy::too_many_arguments)] +fn finish_program( + strategy: RecurrenceStrategy, + process_catalog: &ProcessCatalog<'_>, + dynamic_color_states: Vec, + pending: Vec, + mut pending_closures: BTreeMap, + exhaustive_closure_sector_ids: BTreeSet, + exhaustive_closure_sector_owner_ids: BTreeMap, + replay_targets: Vec, + retained_helicity_count: u64, + helicity_support_rule: HelicitySupportRule, + global_helicity_flip_rule: GlobalHelicityFlipRule, + reflection_certificates: Vec, +) -> RusticolResult { + validate_pending_reflection_certificates(&reflection_certificates)?; + + pending_closures = retain_supported_pending_closures( + strategy, + process_catalog, + &replay_targets, + pending_closures, + helicity_support_rule, + global_helicity_flip_rule, + )?; + let sector_count = match strategy { + RecurrenceStrategy::TopologyReplay => process_catalog + .input + .physical_lc_sectors + .len() + .max(replay_targets.len()), + RecurrenceStrategy::AllFlowUnion | RecurrenceStrategy::ContractedColorUnion => { + process_catalog.input.physical_lc_sectors.len() + } + }; + let helicity_keys = pending_closures + .iter() + .filter(|(key, group)| { + !key.complete_source_states.is_empty() && !group.exact_factor.is_zero() + }) + .map(|(key, _)| key) + .map(|key| key.complete_source_states.clone()) + .collect::>(); + let helicity_ids = helicity_keys + .iter() + .cloned() + .enumerate() + .map(|(id, key)| (key, id as u32)) + .collect::>(); + let resolved_helicities = helicity_keys + .into_iter() + .enumerate() + .map(|(id, source_states)| { + let public_helicities = process_catalog.public_helicities(&source_states)?; + RecurrenceResolvedHelicity::new(id as u32, source_states.into(), public_helicities) + }) + .collect::>>()?; + #[cfg(any(test, feature = "on-the-fly-test-support"))] + let diagnostic_selected_closure_domain = observe_established_selected_transition_slice( + process_catalog, + &pending, + &pending_closures, + &replay_targets, + &dynamic_color_states, + )?; + let mut live = BTreeSet::new(); + let mut queue = VecDeque::new(); + for (key, group) in &pending_closures { + if group.exact_factor.is_zero() { + continue; + } + for parent in key.parent_current_ids.iter().copied() { + if live.insert(parent) { + queue.push_back(parent); + } + } + } + while let Some(current_id) = queue.pop_front() { + for (contribution, factor) in &pending[current_id as usize].contributions { + if factor.is_zero() { + continue; + } + for parent in contribution.parent_current_ids.iter().copied() { + if live.insert(parent) { + queue.push_back(parent); + } + } + } + } + let color_projection = if strategy == RecurrenceStrategy::TopologyReplay { + plan_topology_replay_color_projection(&pending, &pending_closures)? + } else { + None + }; + #[cfg(any(test, feature = "on-the-fly-test-support"))] + observe_established_pairing_owner( + diagnostic_selected_closure_domain.as_ref(), + color_projection.as_ref(), + &pending_closures, + ); + let materialized = if let Some(projection) = color_projection.as_ref() { + materialize_projected_pending_rows(&pending, projection)? + } else { + materialize_live_pending_rows(&pending, &live)? + }; + let MaterializedPendingRows { + remap, + currents, + contributions, + finalizations, + } = materialized; + + let destination_keys = pending_closures + .iter() + .filter(|(_, group)| !group.exact_factor.is_zero()) + .map(|(key, _)| (key.target_sector_id, key.complete_source_states.clone())) + .collect::>(); + let mut closure_terms = Vec::new(); + let mut closure_proof_contributions = Vec::new(); + let mut closure_proof_groups = Vec::new(); + let pending_three_line_certificates = pending_closures + .values() + .flat_map(|group| group.contributions.iter()) + .filter_map(|contribution| contribution.three_line_certificate.clone()) + .collect::>(); + let three_line_certificate_ids = pending_three_line_certificates + .iter() + .cloned() + .enumerate() + .map(|(id, certificate)| { + u32::try_from(id) + .map(|id| (certificate, id)) + .map_err(|_| invalid("three-line certificate count exceeds u32")) + }) + .collect::>>()?; + let mut candidate_identity_digests_by_group = Vec::with_capacity(pending_closures.len()); + for (key, group) in &pending_closures { + let target_helicity_id = if key.complete_source_states.is_empty() { + None + } else { + helicity_ids.get(&key.complete_source_states).copied() + }; + let mut group_candidate_identity_digests = Vec::with_capacity(group.contributions.len()); + for contribution in &group.contributions { + group_candidate_identity_digests.push(pending_closure_candidate_identity_digest( + key, + contribution, + target_helicity_id, + &pending, + &dynamic_color_states, + &reflection_certificates, + )?); + } + candidate_identity_digests_by_group.push(group_candidate_identity_digests); + } + let original_candidate_identity_digests = candidate_identity_digests_by_group + .iter() + .flatten() + .copied() + .collect::>(); + let closure_target_domain_certificate = complete_closure_target_domain_certificate( + strategy, + process_catalog.input, + sector_count, + &exhaustive_closure_sector_ids, + &exhaustive_closure_sector_owner_ids, + &replay_targets, + &helicity_ids, + &pending_closures, + &candidate_identity_digests_by_group, + )?; + let mut accepted_candidate_identity_digests = Vec::new(); + if let Some(projection) = color_projection.as_ref() { + for projected in projection.closures.values() { + let key = &projected.representative_key; + let target_helicity_id = if key.complete_source_states.is_empty() { + None + } else { + helicity_ids.get(&key.complete_source_states).copied() + }; + for contribution in &projected.representative_group.contributions { + accepted_candidate_identity_digests.push( + pending_closure_candidate_identity_digest( + key, + contribution, + target_helicity_id, + &pending, + &dynamic_color_states, + &reflection_certificates, + )?, + ); + } + } + for (key, group) in pending_closures + .iter() + .filter(|(_, group)| group.exact_factor.is_zero()) + { + let target_helicity_id = if key.complete_source_states.is_empty() { + None + } else { + helicity_ids.get(&key.complete_source_states).copied() + }; + for contribution in &group.contributions { + accepted_candidate_identity_digests.push( + pending_closure_candidate_identity_digest( + key, + contribution, + target_helicity_id, + &pending, + &dynamic_color_states, + &reflection_certificates, + )?, + ); + } + } + } else { + accepted_candidate_identity_digests = original_candidate_identity_digests.clone(); + } + let candidate_domain_certificate = ClosureCandidateDomainCertificateV1::from_identity_digests( + accepted_candidate_identity_digests, + )?; + let mut pending_closure_rows = pending_closures + .iter() + .filter(|(_, group)| !group.exact_factor.is_zero()) + .peekable(); + let mut projected_closure_rows = color_projection + .as_ref() + .map(|projection| projection.closures.values().peekable()); + let mut amplitude_destinations = Vec::with_capacity(destination_keys.len()); + let mut destination_ids = BTreeMap::new(); + for (destination_id, (sector_id, source_states)) in destination_keys.into_iter().enumerate() { + let destination_id = u32::try_from(destination_id) + .map_err(|_| invalid("closure destination ID exceeds u32"))?; + destination_ids.insert((sector_id, source_states.clone()), destination_id); + let start = closure_terms.len() as u64; + let target_helicity_id = if source_states.is_empty() { + None + } else { + Some( + *helicity_ids + .get(&source_states) + .ok_or_else(|| invalid("resolved-helicity destination disappeared"))?, + ) + }; + if let Some(rows) = projected_closure_rows.as_mut() { + consume_ordered_closure_destination( + rows, + sector_id, + &source_states, + |projected| { + ( + projected.identity.target_sector_id, + projected.identity.complete_source_states.as_ref(), + ) + }, + |projected| { + let key = &projected.representative_key; + let group = &projected.representative_group; + let runtime_term_id = u32::try_from(closure_terms.len()) + .map_err(|_| invalid("runtime closure-term count exceeds u32"))?; + closure_terms.push(RecurrenceClosureTerm::new( + runtime_term_id, + destination_id, + projected.identity.closure_template_id, + projected.identity.quantum_flow_template_id, + projected.identity.parent_projection_ids.to_vec(), + projected.identity.exact_factor, + )?); + append_closure_proof_group( + key, + group, + &pending, + &dynamic_color_states, + &remap, + Some(destination_id), + target_helicity_id, + Some(runtime_term_id), + &three_line_certificate_ids, + &mut closure_proof_contributions, + &mut closure_proof_groups, + )?; + Ok(()) + }, + )?; + } else { + consume_ordered_closure_destination( + &mut pending_closure_rows, + sector_id, + &source_states, + |(key, _)| (key.target_sector_id, key.complete_source_states.as_ref()), + |(key, group)| { + let parents = key + .parent_current_ids + .iter() + .map(|id| { + remap + .get(id) + .copied() + .ok_or_else(|| invalid("closure parent is absent")) + }) + .collect::>>()?; + let runtime_term_id = u32::try_from(closure_terms.len()) + .map_err(|_| invalid("runtime closure-term count exceeds u32"))?; + closure_terms.push(RecurrenceClosureTerm::new( + runtime_term_id, + destination_id, + key.closure_template_id, + key.quantum_flow_template_id, + parents, + group.exact_factor, + )?); + append_closure_proof_group( + key, + group, + &pending, + &dynamic_color_states, + &remap, + Some(destination_id), + target_helicity_id, + Some(runtime_term_id), + &three_line_certificate_ids, + &mut closure_proof_contributions, + &mut closure_proof_groups, + )?; + Ok(()) + }, + )?; + } + amplitude_destinations.push(RecurrenceAmplitudeDestination::new( + destination_id, + sector_id, + target_helicity_id, + CheckedTableRange::new(start, closure_terms.len() as u64 - start), + )?); + } + if let Some(rows) = projected_closure_rows.as_mut() { + require_ordered_closure_destinations_exhausted(rows)?; + } else { + require_ordered_closure_destinations_exhausted(&mut pending_closure_rows)?; + } + for (key, group) in pending_closures + .iter() + .filter(|(_, group)| group.exact_factor.is_zero()) + { + let target_destination_id = destination_ids + .get(&(key.target_sector_id, key.complete_source_states.clone())) + .copied(); + let target_helicity_id = if key.complete_source_states.is_empty() { + None + } else { + helicity_ids.get(&key.complete_source_states).copied() + }; + append_closure_proof_group( + key, + group, + &pending, + &dynamic_color_states, + &remap, + target_destination_id, + target_helicity_id, + None, + &three_line_certificate_ids, + &mut closure_proof_contributions, + &mut closure_proof_groups, + )?; + } + let closure_proofs = + ClosureProofMetadataV2::new_with_three_line_certificates_and_candidate_domain( + closure_proof_contributions, + closure_proof_groups, + materialize_reflection_certificates(&reflection_certificates)?, + materialize_three_line_certificates(&three_line_certificate_ids)?, + candidate_domain_certificate, + )?; + let mut program = RecurrenceProgram::new_with_closure_proofs( + strategy, + u32::try_from(sector_count).map_err(|_| invalid("physical sector count exceeds u32"))?, + retained_helicity_count, + dynamic_color_states, + currents, + contributions, + finalizations, + replay_targets, + resolved_helicities, + amplitude_destinations, + closure_terms, + closure_proofs, + )?; + if let Some(certificate) = closure_target_domain_certificate { + program = program.with_closure_target_domain_certificate(certificate)?; + } + if let Some(projection) = color_projection.as_ref() { + let body = encode_color_projection_certificate_body( + &pending, + program.dynamic_color_states(), + projection, + &original_candidate_identity_digests, + )?; + program.with_color_projection_certificate_body(body) + } else { + Ok(program) + } +} + +fn helicity_support_rule( + authenticated: &AuthenticatedRecurrenceBuilderInput, +) -> RusticolResult { + let extensions = authenticated + .process() + .semantic_identity() + .extension_digests(); + let mut rule = HelicitySupportRule::None; + for role in extensions + .keys() + .filter(|role| role.starts_with("helicity-support:")) + { + let candidate = match role.as_str() { + PURE_MASSLESS_ADJOINT_HELICITY_SUPPORT_ROLE => { + HelicitySupportRule::PureMasslessAdjointTree + } + _ => { + return Err(invalid(format!( + "unsupported recurrence helicity-support proof {role:?}" + ))); + } + }; + if rule != HelicitySupportRule::None { + return Err(invalid( + "recurrence process carries more than one helicity-support proof", + )); + } + rule = candidate; + } + Ok(rule) +} + +fn global_helicity_flip_rule( + authenticated: &AuthenticatedRecurrenceBuilderInput, +) -> RusticolResult { + let extensions = authenticated + .process() + .semantic_identity() + .extension_digests(); + let mut rule = GlobalHelicityFlipRule::None; + for role in extensions + .keys() + .filter(|role| role.starts_with("helicity-equivalence:")) + { + let candidate = match role.as_str() { + GLOBAL_HELICITY_FLIP_EQUIVALENCE_ROLE => GlobalHelicityFlipRule::Proven, + _ => { + return Err(invalid(format!( + "unsupported recurrence helicity-equivalence proof {role:?}" + ))); + } + }; + if rule != GlobalHelicityFlipRule::None { + return Err(invalid( + "recurrence process carries more than one helicity-equivalence proof", + )); + } + rule = candidate; + } + Ok(rule) +} + +fn retain_supported_pending_closures( + strategy: RecurrenceStrategy, + process_catalog: &ProcessCatalog<'_>, + replay_targets: &[RecurrenceReplayTarget], + pending_closures: BTreeMap, + helicity_support_rule: HelicitySupportRule, + global_helicity_flip_rule: GlobalHelicityFlipRule, +) -> RusticolResult> { + let pending_closures = if helicity_support_rule == HelicitySupportRule::None { + pending_closures + } else { + pending_closures + .into_iter() + .filter_map(|(key, group)| { + match closure_helicity_is_supported( + helicity_support_rule, + process_catalog, + &key.complete_source_states, + ) { + Ok(true) => Some(Ok((key, group))), + Ok(false) => None, + Err(error) => Some(Err(error)), + } + }) + .collect::>>()? + }; + if global_helicity_flip_rule == GlobalHelicityFlipRule::Proven + && strategy != RecurrenceStrategy::AllFlowUnion + { + retain_global_helicity_flip_representatives( + strategy, + process_catalog, + replay_targets, + pending_closures, + ) + } else { + Ok(pending_closures) + } +} + +fn retain_global_helicity_flip_representatives( + strategy: RecurrenceStrategy, + process_catalog: &ProcessCatalog<'_>, + replay_targets: &[RecurrenceReplayTarget], + pending_closures: BTreeMap, +) -> RusticolResult> { + let source_count = process_catalog.input.external_legs.len(); + let anchor_slot = if strategy == RecurrenceStrategy::TopologyReplay { + (0..source_count) + .find(|slot| { + replay_targets.iter().all(|target| { + target.source_slot_permutation().get(*slot).copied() + == u32::try_from(*slot).ok() + }) + }) + .ok_or_else(|| { + invalid( + "global-helicity-flip topology replay has no permutation-fixed source anchor", + ) + })? + } else { + 0 + }; + let mut source_states_by_public = + BTreeMap::<(u32, Vec), Box<[SourceStateAssignment]>>::new(); + for key in pending_closures.keys() { + if key.complete_source_states.is_empty() { + continue; + } + let public = process_catalog.public_helicities(&key.complete_source_states)?; + let map_key = (key.target_sector_id, public); + if source_states_by_public + .insert(map_key, key.complete_source_states.clone()) + .is_some_and(|previous| previous != key.complete_source_states) + { + return Err(invalid( + "global-helicity-flip proof maps one public helicity to multiple source assignments", + )); + } + } + + let mut retained_public = BTreeSet::new(); + for (sector_and_public, source_states) in &source_states_by_public { + let (sector_id, public) = sector_and_public; + let flipped = public.iter().map(|value| -*value).collect::>(); + if flipped == *public { + return Err(invalid( + "global-helicity-flip proof encountered a fixed public-helicity assignment", + )); + } + let flipped_key = (*sector_id, flipped.clone()); + if !source_states_by_public.contains_key(&flipped_key) { + return Err(invalid(format!( + "global-helicity-flip proof has no partner for sector {sector_id} helicity {public:?}", + ))); + } + let anchor_helicity = *public + .get(anchor_slot) + .ok_or_else(|| invalid("global-helicity-flip anchor is outside the public axis"))?; + if anchor_helicity == 0 { + return Err(invalid( + "global-helicity-flip anchor has a self-conjugate public helicity", + )); + } + if anchor_helicity < 0 { + retained_public.insert((*sector_id, public.clone())); + let partner_states = &source_states_by_public[&flipped_key]; + if partner_states == source_states { + return Err(invalid( + "global-helicity-flip pair reuses one source-state assignment", + )); + } + } + } + if retained_public + .len() + .checked_mul(2) + .is_none_or(|count| count != source_states_by_public.len()) + { + return Err(invalid( + "global-helicity-flip proof does not partition closure destinations into two-cycles", + )); + } + + pending_closures + .into_iter() + .filter_map(|(key, group)| { + if key.complete_source_states.is_empty() { + return Some(Ok((key, group))); + } + let public = match process_catalog.public_helicities(&key.complete_source_states) { + Ok(public) => public, + Err(error) => return Some(Err(error)), + }; + retained_public + .contains(&(key.target_sector_id, public)) + .then_some(Ok((key, group))) + }) + .collect() +} + +fn closure_helicity_is_supported( + rule: HelicitySupportRule, + process_catalog: &ProcessCatalog<'_>, + source_states: &[SourceStateAssignment], +) -> RusticolResult { + if rule == HelicitySupportRule::None || source_states.is_empty() { + return Ok(true); + } + let public_helicities = process_catalog.public_helicities(source_states)?; + let mut positive = 0usize; + let mut negative = 0usize; + for (leg, helicity) in process_catalog + .input + .external_legs + .iter() + .zip(public_helicities) + { + let physical = if leg.is_initial == 1 { + -helicity + } else { + helicity + }; + match physical { + 1 => positive += 1, + -1 => negative += 1, + value => { + return Err(invalid(format!( + "pure-massless-adjoint helicity proof received unsupported helicity {value}" + ))); + } + } + } + Ok(positive >= 2 && negative >= 2) +} + +fn retained_helicity_count(process: &OwnedRecurrenceProcessInput) -> RusticolResult { + process.external_legs.iter().try_fold(1_u64, |count, leg| { + let retained = + u64::try_from(retained_source_state_indices(process, leg.source_slot)?.len()) + .map_err(|_| invalid("retained source-state count exceeds u64"))?; + count + .checked_mul(retained) + .ok_or_else(|| invalid("retained public-helicity count exceeds u64")) + }) +} + +fn retained_source_state_indices( + process: &OwnedRecurrenceProcessInput, + source_slot: u32, +) -> RusticolResult> { + let leg = process + .external_legs + .get(source_slot as usize) + .ok_or_else(|| invalid("recurrence source slot is absent"))?; + if !process.header[0].selected_source_mode()? { + return (0..leg.source_state_range.count) + .map(|index| { + u32::try_from(index) + .map_err(|_| invalid("source-state index exceeds the u32 ID domain")) + }) + .collect(); + } + let retained = process + .selected_source_coverage + .iter() + .filter(|row| row.source_slot == source_slot) + .map(|row| row.source_state_index) + .collect::>(); + if retained.is_empty() { + return Err(invalid(format!( + "generation-selected recurrence coverage has no state for source slot {source_slot}" + ))); + } + Ok(retained) +} + +fn complete_closure_source_states( + parents: [&CurrentCoreKey; 2], + source_count: usize, +) -> RusticolResult> { + match ( + parents[0].helicity_identity(), + parents[1].helicity_identity(), + ) { + ( + CurrentHelicityIdentity::TopologyReplay { + local_source_states: left, + .. + }, + CurrentHelicityIdentity::TopologyReplay { + local_source_states: right, + .. + }, + ) => { + let mut result = left.iter().chain(right.iter()).copied().collect::>(); + result.sort_unstable(); + if result.len() != source_count { + return Err(invalid( + "topology-replay closure ancestry does not cover every external source", + )); + } + for (source_slot, assignment) in result.iter().copied().enumerate() { + if assignment.source_slot() as usize != source_slot { + return Err(invalid( + "topology-replay closure ancestry is incomplete or overlapping", + )); + } + } + Ok(result.into_boxed_slice()) + } + ( + CurrentHelicityIdentity::AllFlowUnion { .. }, + CurrentHelicityIdentity::AllFlowUnion { .. }, + ) => Ok(Box::new([])), + ( + CurrentHelicityIdentity::ContractedColorUnion { + local_source_states: left, + .. + }, + CurrentHelicityIdentity::ContractedColorUnion { + local_source_states: right, + .. + }, + ) => { + let mut result = left.iter().chain(right.iter()).copied().collect::>(); + result.sort_unstable(); + if result.len() != source_count { + return Err(invalid( + "contracted-color closure ancestry does not cover every external source", + )); + } + for (source_slot, assignment) in result.iter().copied().enumerate() { + if assignment.source_slot() as usize != source_slot { + return Err(invalid( + "contracted-color closure ancestry is incomplete or overlapping", + )); + } + } + Ok(result.into_boxed_slice()) + } + _ => Err(invalid( + "closure parents use incompatible recurrence helicity strategies", + )), + } +} + +#[cfg(test)] +fn quantum_flow_matches( + quantum: QuantumFlowRow, + parents: &[&CurrentCoreKey; 2], + catalog: &TemplateCatalog<'_>, +) -> RusticolResult { + let states = catalog.u32_sequence(quantum.input_state_sequence_id, "quantum input states")?; + let spins = catalog.i32_sequence(quantum.input_spin_sequence_id, "quantum input spins")?; + let flavours = catalog.u32_sequence( + quantum.input_flavour_sequence_id, + "quantum input flavour flows", + )?; + let quantum_numbers = catalog.u32_sequence( + quantum.input_quantum_sequence_id, + "quantum input number flows", + )?; + if states.len() != 2 || spins.len() != 2 || flavours.len() != 2 || quantum_numbers.len() != 2 { + return Err(invalid( + "direct recurrence requires binary quantum-flow contracts", + )); + } + for index in 0..2 { + // The authenticated model contract proves that branch admission is + // independent of accumulated flavour and quantum-number ancestry. + // Those template columns describe the seed probe used to certify the + // branch, while state and spin remain the actual admission keys. + let _ = catalog.flavour_flow(flavours[index], "quantum parent flavour")?; + let _ = quantum_numbers[index]; + if states[index] != parents[index].current_state_template_id() + || !quantum_parent_spin_matches(spins[index], parents[index]) + { + return Ok(false); + } + } + Ok(true) +} + +pub(super) fn quantum_parent_spin_matches(required_spin: i32, parent: &CurrentCoreKey) -> bool { + required_spin == parent.spin_state_class() + || (parent.node_kind() == RecurrenceNodeKind::Source + && parent.helicity_identity().strategy() == RecurrenceStrategy::AllFlowUnion + && parent.spin_state_class() == DYNAMIC_UNION_SOURCE_SPIN_STATE_CLASS) +} + +#[cfg(test)] +fn quantum_flow_result_flavour( + quantum: QuantumFlowRow, + parents: &[&CurrentCoreKey; 2], + catalog: &TemplateCatalog<'_>, +) -> RusticolResult> { + let operation = catalog.string( + quantum.flavour_flow_operation_string_id, + "quantum-flow flavour operation", + )?; + let static_result = catalog.flavour_flow( + quantum.result_flavour_flow_id, + "quantum-flow result flavour", + )?; + let result_particle = *static_result + .last() + .ok_or_else(|| invalid("quantum-flow result flavour ancestry is empty"))?; + + // The prepared recurrence contract has already proved that transition + // admission and kernel selection are independent of accumulated flavour + // ancestry. In the runtime-helicity union, retaining the construction + // history would nevertheless split one physical current into many + // numerically accumulated copies. Canonicalize it to the result species, + // matching the compact all-flow recurrence identity. + if parents + .iter() + .all(|parent| parent.helicity_identity().strategy() == RecurrenceStrategy::AllFlowUnion) + { + return Ok(vec![result_particle]); + } + + let append_result = |parent: &CurrentCoreKey| { + let mut result = parent.flavour_flow().to_vec(); + if result.last().copied() != Some(result_particle) { + result.push(result_particle); + } + result + }; + + match operation { + "constant-result" => Ok(static_result.to_vec()), + "append-left-result" => Ok(append_result(parents[0])), + "append-right-result" => Ok(append_result(parents[1])), + "concat-left-right-result" => { + let mut result = Vec::with_capacity( + parents[0] + .flavour_flow() + .len() + .saturating_add(parents[1].flavour_flow().len()) + .saturating_add(1), + ); + result.extend_from_slice(parents[0].flavour_flow()); + result.extend_from_slice(parents[1].flavour_flow()); + result.push(result_particle); + Ok(result) + } + value => Err(invalid(format!( + "unsupported quantum-flow flavour operation {value:?}" + ))), + } +} + +fn expected_sector_components( + sector: ProcessPhysicalLCSectorRow, + process: &OwnedRecurrenceProcessInput, + catalog: &ProcessCatalog<'_>, +) -> RusticolResult> { + let mut result = Vec::new(); + match sector.kind()? { + ProcessLCSectorKind::Singlet => {} + ProcessLCSectorKind::SingleTrace => result.push(LCColorComponent::new( + LCColorComponentKind::Trace, + catalog + .u32_sequence(sector.trace_sequence_id, "physical LC trace")? + .to_vec(), + )?), + ProcessLCSectorKind::OpenLines => { + let range = sector + .open_string_range + .as_usize_range(process.lc_open_strings.len(), "physical LC open strings")?; + for row in &process.lc_open_strings[range] { + let mut word = vec![row.fundamental_source_slot]; + word.extend_from_slice( + catalog.u32_sequence( + row.adjoint_sequence_id, + "physical LC open-string adjoints", + )?, + ); + word.push(row.antifundamental_source_slot); + result.push(LCColorComponent::new( + LCColorComponentKind::OpenString, + word, + )?); + } + } + } + Ok(result) +} + +fn closed_components_match_prepared_sector( + strategy: RecurrenceStrategy, + closed: &[LCColorComponent], + sector: &PreparedClosureSector, +) -> RusticolResult { + if sector.row.kind()? != ProcessLCSectorKind::OpenLines { + return Ok(closed == sector.expected_components.as_ref()); + } + if !unordered_color_components_match(closed, §or.expected_components) { + return Ok(false); + } + if strategy == RecurrenceStrategy::ContractedColorUnion + && !sector.contracted_color_canonical_owner + { + return Ok(false); + } + Ok(true) +} + +fn unordered_color_components_match(left: &[LCColorComponent], right: &[LCColorComponent]) -> bool { + if left.len() != right.len() { + return false; + } + let mut matched = vec![false; right.len()]; + for component in left { + let Some(index) = right.iter().enumerate().find_map(|(index, candidate)| { + (!matched[index] && candidate == component).then_some(index) + }) else { + return false; + }; + matched[index] = true; + } + true +} + +fn materialized_sector_ids( + strategy: RecurrenceStrategy, + process: &OwnedRecurrenceProcessInput, + replay_targets: &[RecurrenceReplayTarget], +) -> BTreeSet { + match strategy { + RecurrenceStrategy::AllFlowUnion => process + .physical_lc_sectors + .iter() + .map(|sector| sector.sector_id) + .collect(), + RecurrenceStrategy::TopologyReplay | RecurrenceStrategy::ContractedColorUnion + if !replay_targets.is_empty() => + { + replay_targets + .iter() + .map(RecurrenceReplayTarget::materialized_sector_id) + .collect() + } + RecurrenceStrategy::TopologyReplay | RecurrenceStrategy::ContractedColorUnion => process + .physical_lc_sectors + .iter() + .map(|sector| sector.sector_id) + .collect(), + } +} + +fn construction_sector_groups( + strategy: RecurrenceStrategy, + materialized_sectors: &BTreeSet, + replay_targets: &[RecurrenceReplayTarget], +) -> Vec> { + if strategy.uses_topology_replay_targets() + && !replay_targets.is_empty() + && materialized_sectors.len() > 1 + { + materialized_sectors + .iter() + .copied() + .map(|sector_id| BTreeSet::from([sector_id])) + .collect() + } else { + vec![materialized_sectors.clone()] + } +} + +fn build_replay_targets( + strategy: RecurrenceStrategy, + process: &OwnedRecurrenceProcessInput, + catalog: &ProcessCatalog<'_>, +) -> RusticolResult> { + if !strategy.uses_topology_replay_targets() + || (strategy == RecurrenceStrategy::ContractedColorUnion + && process.replay_partitions.is_empty()) + { + // Contracted color is replay-capable, but only an authenticated + // partition certificate activates replay. Synthesizing identity + // targets here would incorrectly materialize ordering aliases that + // contracted construction coherently accumulates in an owner sector. + return Ok(Vec::new()); + } + let source_momentum_signs = external_source_momentum_signs(process)?; + let retained_flows = retained_public_flows(process)?; + let retained_sectors = retained_flows + .iter() + .map(|flow| flow.construction_sector_id) + .collect::>(); + let mut base_by_sector = BTreeMap::, ExactComplexRational)>::new(); + for partition in &process.replay_partitions { + let range = partition + .target_range + .as_usize_range(process.replay_targets.len(), "recurrence replay targets")?; + for target in &process.replay_targets[range] { + if !retained_sectors.contains(&target.sector_id) { + continue; + } + let mut factor = catalog.factor( + target.amplitude_phase_factor_id, + "recurrence replay amplitude phase", + )?; + if target.fermion_sign == -1 { + factor = factor.checked_neg()?; + } + let previous = base_by_sector.insert( + target.sector_id, + ( + partition.materialized_sector_id, + catalog + .u32_sequence( + target.source_slot_permutation_sequence_id, + "recurrence replay source permutation", + )? + .to_vec(), + factor, + ), + ); + if previous.is_some() { + return Err(invalid(format!( + "construction sector {} has multiple recurrence replay targets", + target.sector_id + ))); + } + } + } + let identity = (0..process.external_legs.len()) + .map(|slot| u32::try_from(slot).map_err(|_| invalid("source-slot count exceeds u32"))) + .collect::>>()?; + for sector_id in retained_sectors { + base_by_sector + .entry(sector_id) + .or_insert_with(|| (sector_id, identity.clone(), ExactComplexRational::ONE)); + } + retained_flows + .into_iter() + .enumerate() + .map(|(target_sector_id, flow)| { + let (materialized, construction_permutation, factor) = base_by_sector + .get(&flow.construction_sector_id) + .ok_or_else(|| invalid("public flow construction replay target is absent"))?; + let public_permutation = catalog.u32_sequence( + flow.source_slot_permutation_sequence_id, + "public LC flow source permutation", + )?; + if construction_permutation.len() != public_permutation.len() { + return Err(invalid( + "public LC flow and construction replay permutations have different sizes", + )); + } + let permutation = + compose_gather_permutations(construction_permutation, public_permutation)?; + let replay_momentum_signs = + replay_momentum_signs(&source_momentum_signs, &permutation)?; + RecurrenceReplayTarget::new( + u32::try_from(target_sector_id) + .map_err(|_| invalid("replay-target count exceeds u32"))?, + *materialized, + u32::try_from(target_sector_id) + .map_err(|_| invalid("public-flow target ID exceeds u32"))?, + permutation, + replay_momentum_signs, + *factor, + ) + }) + .collect() +} + +fn external_source_momentum_signs( + process: &OwnedRecurrenceProcessInput, +) -> RusticolResult> { + process + .external_legs + .iter() + .enumerate() + .map(|(source_slot, leg)| { + let range = leg.source_state_range.as_usize_range( + process.source_states.len(), + "external source momentum signs", + )?; + let mut signs = process.source_states[range] + .iter() + .map(|state| { + if state.source_slot as usize != source_slot + || !matches!(state.momentum_sign, -1 | 1) + { + return Err(invalid(format!( + "source slot {source_slot} has an invalid momentum-sign contract" + ))); + } + Ok(state.momentum_sign) + }) + .collect::>>()?; + if signs.len() != 1 { + return Err(invalid(format!( + "source slot {source_slot} has helicity-dependent momentum crossing" + ))); + } + signs + .pop_first() + .ok_or_else(|| invalid(format!("source slot {source_slot} has no source states"))) + }) + .collect() +} + +fn replay_momentum_signs( + source_momentum_signs: &[i32], + representative_to_public: &[u32], +) -> RusticolResult> { + representative_to_public + .iter() + .copied() + .enumerate() + .map(|(representative_slot, public_slot)| { + let representative_sign = source_momentum_signs + .get(representative_slot) + .copied() + .filter(|sign| matches!(sign, -1 | 1)) + .ok_or_else(|| invalid("representative source momentum sign is absent"))?; + let public_sign = source_momentum_signs + .get(public_slot as usize) + .copied() + .filter(|sign| matches!(sign, -1 | 1)) + .ok_or_else(|| invalid("public source momentum sign is absent"))?; + Ok(representative_sign * public_sign) + }) + .collect() +} + +fn compose_gather_permutations( + representative_to_construction: &[u32], + construction_to_public: &[u32], +) -> RusticolResult> { + if representative_to_construction.len() != construction_to_public.len() { + return Err(invalid( + "public LC flow and construction replay permutations have different sizes", + )); + } + representative_to_construction + .iter() + .map(|construction_slot| { + construction_to_public + .get(*construction_slot as usize) + .copied() + .ok_or_else(|| invalid("construction replay permutation is out of range")) + }) + .collect() +} + +fn retained_public_flows( + process: &OwnedRecurrenceProcessInput, +) -> RusticolResult> { + if !process.header[0].selected_flow_mode()? { + return Ok(process.public_lc_flows.iter().collect()); + } + let selected = process + .selected_public_flow_coverage + .iter() + .map(|row| row.flow_id) + .collect::>(); + Ok(process + .public_lc_flows + .iter() + .filter(|flow| selected.contains(&flow.flow_id)) + .collect()) +} + +fn propagator_by_state( + template: &OwnedRecurrenceTemplateInput, +) -> RusticolResult>> { + let mut result = BTreeMap::new(); + for row in &template.propagators { + let value = (row.applies_propagator != 0).then_some(row.id); + if result.insert(row.state_template_id, value).is_some() { + return Err(invalid(format!( + "current-state template {} has multiple propagators", + row.state_template_id + ))); + } + } + Ok(result) +} + +fn coupling_limits( + process: &ProcessCatalog<'_>, + template: &TemplateCatalog<'_>, +) -> RusticolResult>> { + let mut limits = vec![None; template.coupling_names.len()]; + for row in &process.input.coupling_limits { + let name = process.string(row.name_string_id, "process coupling limit")?; + if let Ok(index) = template.coupling_names.binary_search(&name) { + limits[index] = Some(row.maximum); + } + } + Ok(limits) +} + +pub(super) fn combined_coupling_orders( + left: &[u32], + right: &[u32], + local: &[u32], + limits: &[Option], +) -> RusticolResult>> { + if left.len() != right.len() || left.len() != local.len() || left.len() != limits.len() { + return Err(invalid( + "coupling-order vectors have inconsistent dimensions", + )); + } + let mut result = Vec::with_capacity(left.len()); + for index in 0..left.len() { + let value = left[index] + .checked_add(right[index]) + .and_then(|value| value.checked_add(local[index])) + .ok_or_else(|| invalid("coupling order exceeds u32"))?; + if limits[index].is_some_and(|maximum| value > maximum) { + return Ok(None); + } + result.push(value); + } + Ok(Some(result)) +} + +pub(super) fn merged_helicity_identity( + left: &CurrentHelicityIdentity, + right: &CurrentHelicityIdentity, + result_spin: i32, +) -> RusticolResult { + match (left, right) { + ( + CurrentHelicityIdentity::TopologyReplay { + local_source_states: left, + .. + }, + CurrentHelicityIdentity::TopologyReplay { + local_source_states: right, + .. + }, + ) => { + let mut values = left.iter().chain(right.iter()).copied().collect::>(); + values.sort_unstable(); + CurrentHelicityIdentity::topology_replay(result_spin, values) + } + ( + CurrentHelicityIdentity::AllFlowUnion { .. }, + CurrentHelicityIdentity::AllFlowUnion { .. }, + ) => Ok(CurrentHelicityIdentity::all_flow_union(result_spin)), + ( + CurrentHelicityIdentity::ContractedColorUnion { + local_source_states: left, + .. + }, + CurrentHelicityIdentity::ContractedColorUnion { + local_source_states: right, + .. + }, + ) => { + let mut values = left.iter().chain(right.iter()).copied().collect::>(); + values.sort_unstable(); + CurrentHelicityIdentity::contracted_color_union(result_spin, values) + } + _ => Err(invalid( + "cannot merge recurrence helicity identities from different strategies", + )), + } +} + +fn merged_disjoint_support(left: &[u32], right: &[u32]) -> Vec { + debug_assert!(left.iter().copied().is_sorted()); + debug_assert!(right.iter().copied().is_sorted()); + let mut result = Vec::with_capacity(left.len() + right.len()); + let mut left_index = 0; + let mut right_index = 0; + while left_index < left.len() && right_index < right.len() { + match left[left_index].cmp(&right[right_index]) { + std::cmp::Ordering::Less => { + result.push(left[left_index]); + left_index += 1; + } + std::cmp::Ordering::Greater => { + result.push(right[right_index]); + right_index += 1; + } + std::cmp::Ordering::Equal => { + unreachable!("compact support index emitted overlapping parents") + } + } + } + result.extend_from_slice(&left[left_index..]); + result.extend_from_slice(&right[right_index..]); + result +} + +#[cfg(test)] +fn merged_support(left: &[u32], right: &[u32]) -> RusticolResult> { + if !disjoint_support(left, right) { + return Err(invalid( + "recurrence parents have overlapping source support", + )); + } + let mut result = left.iter().chain(right).copied().collect::>(); + result.sort_unstable(); + Ok(result) +} + +#[cfg(test)] +fn disjoint_support(left: &[u32], right: &[u32]) -> bool { + left.iter().all(|slot| right.binary_search(slot).is_err()) +} + +pub(super) fn merged_momentum( + left: &CanonicalMomentumLinearForm, + right: &CanonicalMomentumLinearForm, +) -> RusticolResult { + let mut terms = left + .terms() + .iter() + .chain(right.terms()) + .copied() + .collect::>(); + terms.sort_unstable_by_key(|term| term.source_slot); + CanonicalMomentumLinearForm::new(terms) +} + +#[cfg(test)] +fn authenticate_runtime_coupling( + catalog: &TemplateCatalog<'_>, + quantum: QuantumFlowRow, + binding_coupling_factor_id: u32, + label: &str, +) -> RusticolResult { + let quantum_coupling = catalog.factor( + quantum.exact_coupling_factor_id, + &format!("{label} quantum-flow coupling"), + )?; + let binding_coupling = catalog.factor( + binding_coupling_factor_id, + &format!("{label} binding coupling"), + )?; + if quantum_coupling != binding_coupling { + return Err(invalid(format!( + "{label} binding coupling does not match its quantum-flow coupling witness" + ))); + } + Ok(binding_coupling) +} + +pub(super) fn output_factor_from_binding( + binding_coupling: ExactComplexRational, + output_factor_source: u8, + label: &str, +) -> RusticolResult { + let component = match OutputFactorSource::try_from(output_factor_source)? { + OutputFactorSource::None => return Ok(ExactComplexRational::ONE), + OutputFactorSource::CouplingReal => binding_coupling.real(), + OutputFactorSource::CouplingImag => binding_coupling.imag(), + }; + if component.is_zero() { + return Err(invalid(format!( + "{label} selects a zero binding-coupling component" + ))); + } + Ok(ExactComplexRational::new(component, ExactRational::ZERO)) +} + +#[cfg(test)] +fn canonical_evaluator_parents( + concrete_parent_ids: [u32; 2], + canonical_input_order: &[u32], + input_exchange_factor_id: u32, + catalog: &TemplateCatalog<'_>, + label: &str, +) -> RusticolResult<([u32; 2], ExactComplexRational)> { + let mut ordered = match canonical_input_order { + [0, 1] => concrete_parent_ids, + [1, 0] => [concrete_parent_ids[1], concrete_parent_ids[0]], + _ => { + return Err(invalid(format!( + "{label} canonical input order is not a binary permutation" + ))); + } + }; + let mut factor = ExactComplexRational::ONE; + if input_exchange_factor_id != MISSING_U32 && ordered[1] < ordered[0] { + ordered.swap(0, 1); + factor = catalog.factor(input_exchange_factor_id, &format!("{label} input-exchange"))?; + } + Ok((ordered, factor)) +} + +pub(super) fn multiply_factors( + values: &[ExactComplexRational], +) -> RusticolResult { + values + .iter() + .copied() + .try_fold(ExactComplexRational::ONE, ExactComplexRational::checked_mul) +} + +pub(super) fn aggregate_factor( + target: &mut ExactComplexRational, + value: ExactComplexRational, +) -> RusticolResult<()> { + *target = target.checked_add(value)?; + Ok(()) +} + +fn decode_strings<'a>( + ranges: &[CheckedTableRange], + bytes: &'a [u8], + label: &str, +) -> RusticolResult> { + ranges + .iter() + .copied() + .enumerate() + .map(|(index, range)| { + let range = range.as_usize_range(bytes.len(), &format!("{label} {index}"))?; + std::str::from_utf8(&bytes[range]) + .map_err(|error| invalid(format!("{label} {index} is not UTF-8: {error}"))) + }) + .collect() +} + +fn required_string<'a>(strings: &[&'a str], id: u32, label: &str) -> RusticolResult<&'a str> { + strings + .get(id as usize) + .copied() + .ok_or_else(|| invalid(format!("{label} string {id} is absent"))) +} + +fn indexed_sequence<'a, T>( + ranges: &[super::template::IndexedRangeRow], + values: &'a [T], + id: u32, + label: &str, +) -> RusticolResult<&'a [T]> { + let range = ranges + .get(id as usize) + .ok_or_else(|| invalid(format!("{label} sequence {id} is absent")))? + .range + .as_usize_range(values.len(), label)?; + Ok(&values[range]) +} + +fn decode_template_factors( + input: &OwnedRecurrenceTemplateInput, + strings: &[&str], +) -> RusticolResult> { + input + .exact_factors + .iter() + .map(|row| { + ExactComplexRational::parse_parts( + required_string(strings, row.real_numerator_string_id, "factor numerator")?, + required_string( + strings, + row.real_denominator_string_id, + "factor denominator", + )?, + required_string(strings, row.imag_numerator_string_id, "factor numerator")?, + required_string( + strings, + row.imag_denominator_string_id, + "factor denominator", + )?, + ) + }) + .collect() +} + +fn decode_process_factors( + input: &OwnedRecurrenceProcessInput, + strings: &[&str], +) -> RusticolResult> { + input + .exact_factors + .iter() + .map(|row| { + ExactComplexRational::parse_parts( + required_string(strings, row.real_numerator_string_id, "factor numerator")?, + required_string( + strings, + row.real_denominator_string_id, + "factor denominator", + )?, + required_string(strings, row.imag_numerator_string_id, "factor numerator")?, + required_string( + strings, + row.imag_denominator_string_id, + "factor denominator", + )?, + ) + }) + .collect() +} + +#[cfg(test)] +mod tests { + use std::collections::{BTreeMap, BTreeSet}; + + use super::super::contact_orbit_owner::{ + ContactOrbitStepProof, contact_orbit_application_for_test, + final_contact_orbit_step_for_test, partial_contact_orbit_step_for_test, + prepared_contact_orbit_transition_for_test, + }; + use super::super::process::{ + ProcessExternalLegRow, ProcessHeaderRow, ProcessLCOpenStringRow, + ProcessPhysicalLCSectorRow, ProcessPublicLCFlowRow, ProcessSourceStateRow, + }; + use super::super::template::{ + ColorContractionRow, DigestCatalogRow, ExactFactorRow, IndexedRangeRow, + LCColorTransitionWitnessRow, PropagatorRow, QuantumFlowRow, + }; + use super::super::{LCColorEndpoint, LCColorPortBinding}; + use super::*; + + fn digest(byte: u8) -> SemanticDigest { + SemanticDigest::new([byte; 32]).expect("test digest must be nonzero") + } + + #[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] + struct OrderedClosureTestRow { + sector_id: u32, + source_states: Box<[SourceStateAssignment]>, + ordinal: u32, + } + + #[test] + fn ordered_closure_destination_cursor_preserves_filter_order_and_fails_closed() { + let states = + |state_index| vec![SourceStateAssignment::new(0, state_index)].into_boxed_slice(); + let mut rows = [ + OrderedClosureTestRow { + sector_id: 2, + source_states: states(0), + ordinal: 4, + }, + OrderedClosureTestRow { + sector_id: 0, + source_states: states(1), + ordinal: 1, + }, + OrderedClosureTestRow { + sector_id: 0, + source_states: states(1), + ordinal: 0, + }, + OrderedClosureTestRow { + sector_id: 1, + source_states: states(0), + ordinal: 3, + }, + OrderedClosureTestRow { + sector_id: 0, + source_states: states(0), + ordinal: 2, + }, + ]; + rows.sort_unstable(); + let destinations = rows + .iter() + .map(|row| (row.sector_id, row.source_states.clone())) + .collect::>(); + let expected = destinations + .iter() + .flat_map(|(sector_id, source_states)| { + rows.iter() + .filter(move |row| { + row.sector_id == *sector_id + && row.source_states.as_ref() == source_states.as_ref() + }) + .map(|row| row.ordinal) + }) + .collect::>(); + + let mut cursor = rows.iter().peekable(); + let mut observed = Vec::new(); + for (sector_id, source_states) in &destinations { + consume_ordered_closure_destination( + &mut cursor, + *sector_id, + source_states, + |row: &&OrderedClosureTestRow| (row.sector_id, row.source_states.as_ref()), + |row| { + observed.push(row.ordinal); + Ok(()) + }, + ) + .unwrap(); + } + require_ordered_closure_destinations_exhausted(&mut cursor).unwrap(); + assert_eq!(observed, expected); + assert_eq!(observed, [2, 0, 1, 3, 4]); + + let mut skipped_prefix = rows.iter().peekable(); + assert!( + consume_ordered_closure_destination( + &mut skipped_prefix, + 1, + &states(0), + |row: &&OrderedClosureTestRow| (row.sector_id, row.source_states.as_ref()), + |_| Ok(()), + ) + .is_err() + ); + + let mut unconsumed_suffix = rows.iter().peekable(); + consume_ordered_closure_destination( + &mut unconsumed_suffix, + 0, + &states(0), + |row: &&OrderedClosureTestRow| (row.sector_id, row.source_states.as_ref()), + |_| Ok(()), + ) + .unwrap(); + assert!(require_ordered_closure_destinations_exhausted(&mut unconsumed_suffix).is_err()); + } + + fn proven_reflection( + color: &DynamicLCColorState, + phase: ExactComplexRational, + root: u8, + ) -> CurrentReflection { + CurrentReflection::Proven( + CurrentReflectionProof::new( + phase, + [digest(root)], + dynamic_color_identity_digest(color).unwrap(), + ) + .unwrap(), + ) + } + + fn proven_interned_reflection( + colors: &DynamicLCColorStateInterner, + color_id: DynamicLCColorStateId, + phase: ExactComplexRational, + root: u8, + ) -> CurrentReflection { + proven_reflection(colors.get(color_id).unwrap(), phase, root) + } + + fn transition(id: u32) -> TransitionRow { + TransitionRow { + id, + template_string_id: 0, + input_state_sequence_id: 0, + result_state_template_id: 0, + quantum_flow_template_id: 0, + evaluator_binding_id: 0, + canonical_input_order_sequence_id: 0, + momentum_convention_sequence_id: 0, + coupling_parameter_sequence_id: 0, + coupling_order_set_id: 0, + color_contraction_template_id: 0, + binding_coupling_factor_id: 0, + exact_factor_id: 0, + output_factor_source: 0, + equivalence_class_string_id: 0, + input_exchange_factor_id: MISSING_U32, + output_projection_string_id: 0, + contact_orbit_step_sequence_id: 0, + contact_orbit_step_semantic_digest_sequence_id: 0, + semantic_digest_id: 0, + } + } + + fn source(id: u32) -> SourceRow { + SourceRow { + id, + template_string_id: 1, + state_template_id: id, + crossing_string_id: 2, + wavefunction_family_string_id: 3, + helicity: if id == 0 { -1 } else { 1 }, + spin_state: if id == 0 { -1 } else { 1 }, + flavour_flow_id: 4, + quantum_number_flow_id: 5, + lc_color_seed_operation: 6, + lc_color_seed_shape_string_id: 7, + lc_color_seed_component_kind: 8, + lc_color_seed_component_role: 9, + lc_color_seed_proof_digest_id: 10 + id, + lc_color_seed_provenance_sequence_id: 11, + wavefunction_expression_digest_id: 12 + id, + evaluator_binding_id: 13 + id, + mass_parameter_id: 14, + width_parameter_id: 15, + semantic_digest_id: 16 + id, + } + } + + #[test] + fn union_source_dispatch_ignores_proof_instance_identity_only() { + let left = source(0); + let right = source(1); + assert_ne!( + left.lc_color_seed_proof_digest_id, + right.lc_color_seed_proof_digest_id + ); + assert!(same_union_source_dispatch_semantics(left, right)); + + let mut incompatible = right; + incompatible.crossing_string_id += 1; + assert!(!same_union_source_dispatch_semantics(left, incompatible)); + + let mut incompatible = right; + incompatible.lc_color_seed_provenance_sequence_id += 1; + assert!(!same_union_source_dispatch_semantics(left, incompatible)); + } + + #[test] + fn all_flow_union_source_spin_class_is_dynamic_for_chiral_variants() { + assert_eq!( + union_spin_state_class(25, &BTreeSet::from([-1, 1])).unwrap(), + DYNAMIC_UNION_SOURCE_SPIN_STATE_CLASS + ); + assert_eq!( + union_spin_state_class(25, &BTreeSet::from([-1])).unwrap(), + -1 + ); + assert!( + union_spin_state_class(25, &BTreeSet::from([DYNAMIC_UNION_SOURCE_SPIN_STATE_CLASS]),) + .is_err() + ); + + let mut colors = DynamicLCColorStateInterner::default(); + let color_id = colors + .intern(DynamicLCColorState::new(0, None, vec![]).unwrap()) + .unwrap(); + let source = CurrentCoreKey::new( + digest(202), + RecurrenceNodeKind::Source, + 17, + color_id, + vec![0], + CanonicalMomentumLinearForm::new(vec![MomentumTerm { + source_slot: 0, + coefficient: 1, + }]) + .unwrap(), + CurrentHelicityIdentity::all_flow_union(DYNAMIC_UNION_SOURCE_SPIN_STATE_CLASS), + vec![0], + 0, + vec![], + CurrentSourceBinding::runtime_dispatch(25, vec![0, 1]).unwrap(), + None, + ) + .unwrap(); + + assert!(quantum_parent_spin_matches(-1, &source)); + assert!(quantum_parent_spin_matches(1, &source)); + assert!(structural_parent_states_match( + [StructuralState::new(17, -1), StructuralState::new(19, 0),], + [ + StructuralState::new(17, DYNAMIC_UNION_SOURCE_SPIN_STATE_CLASS), + StructuralState::new(19, 0), + ], + )); + } + + #[test] + fn pure_adjoint_reflection_masks_require_exact_parent_proofs() { + let left = DynamicLCColorState::new_port_wired( + 1, + vec![ + LCColorPortBinding::new(0, LCColorEndpoint::Back), + LCColorPortBinding::new(0, LCColorEndpoint::Front), + ], + vec![LCColorComponent::new(LCColorComponentKind::AdjointSegment, vec![1, 4]).unwrap()], + ) + .unwrap(); + let right = DynamicLCColorState::new_port_wired( + 1, + vec![ + LCColorPortBinding::new(0, LCColorEndpoint::Back), + LCColorPortBinding::new(0, LCColorEndpoint::Front), + ], + vec![LCColorComponent::new(LCColorComponentKind::AdjointSegment, vec![2, 3]).unwrap()], + ) + .unwrap(); + let minus_one = ExactComplexRational::ONE.checked_neg().unwrap(); + let reflections = [ + proven_reflection(&left, minus_one, 40), + proven_reflection(&right, minus_one, 41), + ]; + + assert_eq!( + current_reversal_masks(&[left.clone(), right.clone()], &reflections), + [0, 1, 2, 3], + ); + let partial_reflections = [ + proven_reflection(&left, minus_one, 42), + CurrentReflection::Unavailable, + ]; + assert_eq!( + current_reversal_masks(&[left, right], &partial_reflections), + [0, 1], + ); + } + + #[test] + fn source_reflection_lineage_is_deterministic_and_color_bound() { + let mut colors = DynamicLCColorStateInterner::default(); + let first_color = colors + .intern( + DynamicLCColorState::new_port_wired( + 1, + vec![ + LCColorPortBinding::new(0, LCColorEndpoint::Back), + LCColorPortBinding::new(0, LCColorEndpoint::Front), + ], + vec![ + LCColorComponent::new(LCColorComponentKind::AdjointSegment, vec![3]) + .unwrap(), + ], + ) + .unwrap(), + ) + .unwrap(); + let second_color = colors + .intern( + DynamicLCColorState::new_port_wired( + 2, + vec![ + LCColorPortBinding::new(0, LCColorEndpoint::Back), + LCColorPortBinding::new(0, LCColorEndpoint::Front), + ], + vec![ + LCColorComponent::new(LCColorComponentKind::AdjointSegment, vec![3]) + .unwrap(), + ], + ) + .unwrap(), + ) + .unwrap(); + let seed = digest(39); + let first = source_reflection(&colors, first_color, 3, seed) + .unwrap() + .proof() + .unwrap() + .clone(); + let repeated = source_reflection(&colors, first_color, 3, seed) + .unwrap() + .proof() + .unwrap() + .clone(); + let changed_slot = source_reflection(&colors, first_color, 4, seed) + .unwrap() + .proof() + .unwrap() + .clone(); + let changed_color = source_reflection(&colors, second_color, 3, seed) + .unwrap() + .proof() + .unwrap() + .clone(); + + assert_eq!(first, repeated); + assert_ne!(first.proof_digest(), changed_slot.proof_digest()); + assert_ne!(first.proof_digest(), changed_color.proof_digest()); + assert_ne!( + first.result_color_identity(), + changed_color.result_color_identity() + ); + } + + #[test] + fn pure_adjoint_result_reflection_is_phase_composed_and_canonical() { + let result = DynamicLCColorState::new_port_wired( + 1, + vec![ + LCColorPortBinding::new(0, LCColorEndpoint::Back), + LCColorPortBinding::new(0, LCColorEndpoint::Front), + ], + vec![ + LCColorComponent::new(LCColorComponentKind::AdjointSegment, vec![1, 3, 2]).unwrap(), + ], + ) + .unwrap(); + let minus_one = ExactComplexRational::ONE.checked_neg().unwrap(); + let left = proven_reflection(&result, minus_one, 43); + let right = proven_reflection(&result, minus_one, 44); + let local = TransitionReflectionProof::new(minus_one, [digest(45)], [digest(46)]).unwrap(); + let proof = current_reflection_candidate(&result, &[left, right], Some(&local)) + .unwrap() + .unwrap(); + assert_eq!(proof.phase(), minus_one); + assert_eq!( + proof.result_color_identity(), + dynamic_color_identity_digest(&result).unwrap() + ); + assert!(proof.lineage_roots().contains(&digest(43))); + assert!(proof.lineage_roots().contains(&digest(44))); + assert!(proof.lineage_roots().contains(&digest(45))); + assert!(proof.lineage_roots().contains(&digest(46))); + assert!(proof.lineage_roots().contains(&local.proof_digest)); + assert!(pure_adjoint_word_is_canonical( + result.pure_adjoint_word().unwrap() + )); + assert!(!pure_adjoint_word_is_canonical(&[3, 2, 1])); + } + + fn reflection_test_current( + color_id: DynamicLCColorStateId, + reflection: CurrentReflection, + ) -> PendingCurrent { + PendingCurrent { + key: CurrentCoreKey::new( + digest(201), + RecurrenceNodeKind::Current, + 0, + color_id, + vec![0, 1, 2], + CanonicalMomentumLinearForm::new(vec![ + MomentumTerm { + source_slot: 0, + coefficient: 1, + }, + MomentumTerm { + source_slot: 1, + coefficient: 1, + }, + MomentumTerm { + source_slot: 2, + coefficient: 1, + }, + ]) + .unwrap(), + CurrentHelicityIdentity::all_flow_union(0), + vec![0], + 0, + vec![], + CurrentSourceBinding::None, + None, + ) + .unwrap(), + source_exact_factor: None, + contributions: BTreeMap::new(), + realized_pairing_rule_ids: BTreeSet::new(), + reflection, + reflection_certificate_id: None, + } + } + + fn reflection_test_states() -> ( + DynamicLCColorStateInterner, + DynamicLCColorStateId, + DynamicLCColorStateId, + ) { + let canonical = DynamicLCColorState::new_port_wired( + 1, + vec![ + LCColorPortBinding::new(0, LCColorEndpoint::Back), + LCColorPortBinding::new(0, LCColorEndpoint::Front), + ], + vec![ + LCColorComponent::new(LCColorComponentKind::AdjointSegment, vec![1, 3, 2]).unwrap(), + ], + ) + .unwrap(); + let reversed = canonical.reversed().unwrap(); + let mut states = DynamicLCColorStateInterner::default(); + let canonical_id = states.intern(canonical).unwrap(); + let reversed_id = states.intern(reversed).unwrap(); + (states, canonical_id, reversed_id) + } + + #[test] + fn late_unproved_reflection_keeps_both_stage_orientations() { + let minus_one = ExactComplexRational::ONE.checked_neg().unwrap(); + let (mut color_states, canonical_id, reversed_id) = reflection_test_states(); + let mut late_downgrade = + proven_interned_reflection(&color_states, reversed_id, minus_one, 51); + late_downgrade.include(None).unwrap(); + let mut currents = vec![ + reflection_test_current( + canonical_id, + proven_interned_reflection(&color_states, canonical_id, minus_one, 50), + ), + reflection_test_current(reversed_id, late_downgrade), + ]; + let mut current_ids = currents + .iter() + .enumerate() + .map(|(id, current)| (current.key.clone(), id as u32)) + .collect::(); + let mut target_bucket = vec![0, 1]; + let mut certificates = Vec::new(); + let mut resident_contribution_count = 0; + + reconcile_stage_reflections( + 0, + &mut color_states, + &mut currents, + &mut current_ids, + &mut target_bucket, + &mut certificates, + 4, + &mut resident_contribution_count, + ) + .unwrap(); + + assert_eq!(currents.len(), 2); + assert_eq!(target_bucket, [0, 1]); + assert_eq!(current_ids.len(), 2); + assert!(certificates.is_empty()); + assert!( + currents + .iter() + .all(|current| current.reflection == CurrentReflection::Unavailable) + ); + } + + #[test] + fn late_conflicting_reflection_keeps_both_stage_orientations() { + let minus_one = ExactComplexRational::ONE.checked_neg().unwrap(); + let (mut color_states, canonical_id, reversed_id) = reflection_test_states(); + let mut late_downgrade = + proven_interned_reflection(&color_states, reversed_id, minus_one, 53); + late_downgrade + .include(Some( + CurrentReflectionProof::new(minus_one, [digest(54)], digest(55)).unwrap(), + )) + .unwrap(); + let mut currents = vec![ + reflection_test_current( + canonical_id, + proven_interned_reflection(&color_states, canonical_id, minus_one, 52), + ), + reflection_test_current(reversed_id, late_downgrade), + ]; + let mut current_ids = currents + .iter() + .enumerate() + .map(|(id, current)| (current.key.clone(), id as u32)) + .collect::(); + let mut target_bucket = vec![0, 1]; + let mut certificates = Vec::new(); + let mut resident_contribution_count = 0; + + reconcile_stage_reflections( + 0, + &mut color_states, + &mut currents, + &mut current_ids, + &mut target_bucket, + &mut certificates, + 4, + &mut resident_contribution_count, + ) + .unwrap(); + + assert_eq!(currents.len(), 2); + assert_eq!(target_bucket, [0, 1]); + assert_eq!(current_ids.len(), 2); + assert!(certificates.is_empty()); + assert!( + currents + .iter() + .all(|current| current.reflection == CurrentReflection::Unavailable) + ); + } + + #[test] + fn nonreciprocal_reflection_proofs_keep_both_stage_orientations() { + let minus_one = ExactComplexRational::ONE.checked_neg().unwrap(); + let (mut color_states, canonical_id, reversed_id) = reflection_test_states(); + let mut currents = vec![ + reflection_test_current( + canonical_id, + proven_interned_reflection(&color_states, canonical_id, minus_one, 56), + ), + reflection_test_current( + reversed_id, + proven_interned_reflection( + &color_states, + reversed_id, + ExactComplexRational::ONE, + 57, + ), + ), + ]; + let mut current_ids = currents + .iter() + .enumerate() + .map(|(id, current)| (current.key.clone(), id as u32)) + .collect::(); + let mut target_bucket = vec![0, 1]; + let mut certificates = Vec::new(); + let mut resident_contribution_count = 0; + + reconcile_stage_reflections( + 0, + &mut color_states, + &mut currents, + &mut current_ids, + &mut target_bucket, + &mut certificates, + 4, + &mut resident_contribution_count, + ) + .unwrap(); + + assert_eq!(currents.len(), 2); + assert_eq!(target_bucket, [0, 1]); + assert_eq!(current_ids.len(), 2); + assert!(certificates.is_empty()); + assert!( + currents + .iter() + .all(|current| current.reflection == CurrentReflection::Unavailable) + ); + } + + #[test] + fn complete_reciprocal_reflection_prunes_only_noncanonical_orientation() { + let minus_one = ExactComplexRational::ONE.checked_neg().unwrap(); + let (mut color_states, canonical_id, reversed_id) = reflection_test_states(); + let mut currents = vec![ + reflection_test_current( + canonical_id, + proven_interned_reflection(&color_states, canonical_id, minus_one, 58), + ), + reflection_test_current( + reversed_id, + proven_interned_reflection(&color_states, reversed_id, minus_one, 59), + ), + ]; + let mut current_ids = currents + .iter() + .enumerate() + .map(|(id, current)| (current.key.clone(), id as u32)) + .collect::(); + let mut target_bucket = vec![0, 1]; + let mut certificates = Vec::new(); + let mut resident_contribution_count = 0; + let mut support_keys = TransientCurrentSupportKeys::from_currents(4, ¤ts).unwrap(); + let mut support_buckets = + LaneSupportBuckets::new(&[vec![], vec![], vec![0, 1], vec![]], &support_keys).unwrap(); + + reconcile_stage_reflections( + 0, + &mut color_states, + &mut currents, + &mut current_ids, + &mut target_bucket, + &mut certificates, + 4, + &mut resident_contribution_count, + ) + .unwrap(); + support_keys.reconcile_stage_tail(0, ¤ts).unwrap(); + support_buckets + .replace_bucket(3, &target_bucket, &support_keys) + .unwrap(); + + assert_eq!(currents.len(), 1); + assert_eq!(target_bucket, [0]); + assert_eq!(current_ids.len(), 1); + assert_eq!(support_keys.keys_by_current.len(), 1); + let disjoint = support_buckets + .bucket_mut(3) + .unwrap() + .disjoint_current_ids(&TransientSupportKey::singleton(4, 3).unwrap()) + .unwrap() + .0; + assert_eq!( + disjoint + .iter() + .map(|entry| entry.current_id) + .collect::>(), + [0] + ); + assert_eq!(certificates.len(), 1); + let certificate = &certificates[0]; + assert_eq!(certificate.id, 0); + assert_eq!(certificate.canonical_old_current_id, 0); + assert_eq!(certificate.reflected_old_current_id, 1); + assert_eq!(certificate.canonical_phase, minus_one); + assert_eq!(certificate.reflected_phase, minus_one); + assert_eq!(certificate.source_permutation.as_ref(), [0, 2, 1, 3]); + assert!(!certificate.fixed_point); + assert_eq!(certificate.orbit_size, 2); + validate_pending_reflection_certificates(&certificates).unwrap(); + let mut stale_lineage = certificates.clone(); + stale_lineage[0].canonical_lineage_digest = digest(60); + assert!(validate_pending_reflection_certificates(&stale_lineage).is_err()); + assert_eq!(currents[0].reflection.phase(), Some(minus_one)); + let color = color_states + .get(currents[0].key.dynamic_lc_color_state_id()) + .unwrap(); + assert!(pure_adjoint_word_is_canonical( + color.pure_adjoint_word().unwrap() + )); + } + + #[test] + fn transient_current_index_insertion_order_cannot_change_canonical_reconciliation() { + let run = |reverse_index_insertion: bool| { + let minus_one = ExactComplexRational::ONE.checked_neg().unwrap(); + let (mut color_states, canonical_id, reversed_id) = reflection_test_states(); + let mut currents = vec![ + reflection_test_current( + canonical_id, + proven_interned_reflection(&color_states, canonical_id, minus_one, 91), + ), + reflection_test_current( + reversed_id, + proven_interned_reflection(&color_states, reversed_id, minus_one, 92), + ), + ]; + let mut insertion_order = [0_usize, 1]; + if reverse_index_insertion { + insertion_order.reverse(); + } + let mut current_ids = TransientCurrentIdIndex::new(); + for current_id in insertion_order { + assert!( + current_ids + .insert(currents[current_id].key.clone(), current_id as u32) + .is_none() + ); + } + let mut target_bucket = vec![0, 1]; + let mut certificates = Vec::new(); + let mut resident_contribution_count = 0; + reconcile_stage_reflections( + 0, + &mut color_states, + &mut currents, + &mut current_ids, + &mut target_bucket, + &mut certificates, + 4, + &mut resident_contribution_count, + ) + .unwrap(); + ( + currents + .into_iter() + .map(|current| current.key) + .collect::>(), + target_bucket, + certificates, + resident_contribution_count, + ) + }; + + assert_eq!(run(false), run(true)); + } + + #[test] + fn reflected_closure_emission_reuses_parents_and_applies_exact_phase() { + let factor = ExactComplexRational::parse_parts("2", "1", "0", "1").unwrap(); + let minus_one = ExactComplexRational::ONE.checked_neg().unwrap(); + let key = PendingClosureKey { + target_sector_id: 119, + complete_source_states: Vec::new().into_boxed_slice(), + closure_template_id: 3, + quantum_flow_template_id: Some(5), + parent_current_ids: vec![7, 9].into_boxed_slice(), + }; + let contribution = PendingClosureProofContribution { + construction_parent_ids: [7, 9], + construction_parent_permutation: [0, 1], + reconstruction_parent_permutation: [0, 1], + evaluator_parent_permutation: [0, 1], + closure_template_semantic_digest: digest(111), + color_witness_term_id: 13, + color_witness_proof_digest: digest(112), + three_line_certificate: None, + pairing_certificate_ids: Vec::new().into_boxed_slice(), + reflection_certificate_id: Some(17), + exact_factor: factor, + }; + let projection = ClosureReflectionProjection { + certificate_id: 17, + partner_sector_id: Some(153), + partner_phase: minus_one, + }; + let mut closures = BTreeMap::new(); + + include_closure_with_reflection(&mut closures, key, contribution, Some(projection)) + .unwrap(); + + assert_eq!(closures.len(), 2); + let canonical = closures + .iter() + .find(|(key, _)| key.target_sector_id == 119) + .unwrap(); + let reflected = closures + .iter() + .find(|(key, _)| key.target_sector_id == 153) + .unwrap(); + assert_eq!(canonical.1.exact_factor, factor); + assert_eq!( + reflected.1.exact_factor, + factor.checked_mul(minus_one).unwrap() + ); + assert_eq!( + canonical.0.parent_current_ids, + reflected.0.parent_current_ids + ); + assert_eq!(canonical.1.contributions[0].construction_parent_ids, [7, 9]); + assert_eq!(reflected.1.contributions[0].construction_parent_ids, [7, 9]); + assert_eq!( + reflected.1.contributions[0].reflection_certificate_id, + Some(17) + ); + + let mut ineligible = BTreeMap::new(); + include_closure_with_reflection( + &mut ineligible, + canonical.0.clone(), + canonical.1.contributions[0].clone(), + None, + ) + .unwrap(); + assert_eq!(ineligible.len(), 1); + } + + fn buffered_parent_pairs(target_size: usize, currents_by_size: &[Vec]) -> Vec<[u32; 2]> { + let mut pairs = Vec::new(); + for left_size in 1..target_size { + let right_size = target_size - left_size; + for &left_id in ¤ts_by_size[left_size - 1] { + for &right_id in ¤ts_by_size[right_size - 1] { + if left_id < right_id { + pairs.push([left_id, right_id]); + } + } + } + } + pairs.sort_unstable(); + pairs.dedup(); + pairs + } + + #[test] + fn streamed_parent_pairs_match_buffered_reference_order() { + let fixtures = [ + (2, vec![vec![0, 1, 2, 3]]), + (3, vec![vec![0, 1, 2, 3], vec![4, 5, 6, 7, 8, 9]]), + ( + 4, + vec![ + vec![0, 1, 2, 3], + vec![4, 5, 6, 7, 8, 9], + vec![10, 11, 12, 13], + ], + ), + ]; + for (target_size, currents_by_size) in fixtures { + assert_eq!( + parent_pairs_for_target(target_size, ¤ts_by_size).collect::>(), + buffered_parent_pairs(target_size, ¤ts_by_size), + "support size {target_size}", + ); + } + } + + #[test] + fn streamed_parent_pair_totals_match_iteration() { + for (target_size, currents_by_size) in [ + (2, vec![vec![0, 1, 2, 3]]), + (3, vec![vec![0, 1, 2, 3], vec![4, 5, 6, 7, 8, 9]]), + ( + 4, + vec![ + vec![0, 1, 2, 3], + vec![4, 5, 6, 7, 8, 9], + vec![10, 11, 12, 13], + ], + ), + ] { + assert_eq!( + parent_pair_total_for_target(target_size, ¤ts_by_size).unwrap(), + parent_pairs_for_target(target_size, ¤ts_by_size).count(), + "support size {target_size}", + ); + } + } + + fn compact_pair_schedule( + source_count: usize, + target_size: usize, + currents_by_size: &[Vec], + source_slots_by_current: &[Vec], + ) -> DisjointParentPairSchedule { + let support_keys = TransientCurrentSupportKeys { + source_count, + keys_by_current: source_slots_by_current + .iter() + .map(|source_slots| { + TransientSupportKey::from_source_slots(source_count, source_slots).unwrap() + }) + .collect(), + }; + let mut support_buckets = LaneSupportBuckets::new(currents_by_size, &support_keys).unwrap(); + disjoint_parent_pairs_for_target( + target_size, + currents_by_size, + &support_keys, + &mut support_buckets, + ) + .unwrap() + } + + fn reference_disjoint_parent_pairs( + target_size: usize, + currents_by_size: &[Vec], + source_slots_by_current: &[Vec], + ) -> Vec<[u32; 2]> { + reference_disjoint_parent_pair_schedule( + target_size, + currents_by_size, + source_slots_by_current, + ) + .into_iter() + .map(|pair| pair.parent_ids) + .collect() + } + + fn reference_disjoint_parent_pair_schedule( + target_size: usize, + currents_by_size: &[Vec], + source_slots_by_current: &[Vec], + ) -> Vec { + parent_pairs_for_target(target_size, currents_by_size) + .enumerate() + .filter_map(|(candidate_index, parent_ids)| { + let [left_id, right_id] = parent_ids; + disjoint_support( + &source_slots_by_current[left_id as usize], + &source_slots_by_current[right_id as usize], + ) + .then_some(OrderedDisjointParentPair { + parent_ids, + theoretical_candidate_count: candidate_index + 1, + }) + }) + .collect() + } + + fn next_random(seed: &mut u64) -> u64 { + *seed = seed + .wrapping_mul(6_364_136_223_846_793_005) + .wrapping_add(1_442_695_040_888_963_407); + *seed + } + + fn random_support(seed: &mut u64, source_count: usize, size: usize) -> Vec { + let mut support = BTreeSet::new(); + while support.len() < size { + support.insert((next_random(seed) as usize % source_count) as u32); + } + support.into_iter().collect() + } + + #[test] + fn compact_disjoint_parent_pairs_match_randomized_reference_order() { + let mut seed = 0x6a09_e667_f3bc_c909; + for case_index in 0..160 { + let source_count = 4 + next_random(&mut seed) as usize % 7; + let target_size = 2 + next_random(&mut seed) as usize % (source_count - 2); + let mut currents_by_size = vec![Vec::new(); target_size - 1]; + let mut source_slots_by_current = Vec::new(); + for support_size in 1..target_size { + let current_count = 2 + next_random(&mut seed) as usize % 9; + for _ in 0..current_count { + let source_slots = random_support(&mut seed, source_count, support_size); + let current_id = source_slots_by_current.len() as u32; + source_slots_by_current.push(source_slots); + currents_by_size[support_size - 1].push(current_id); + } + } + + let reference = reference_disjoint_parent_pair_schedule( + target_size, + ¤ts_by_size, + &source_slots_by_current, + ); + let unfiltered_count = parent_pairs_for_target(target_size, ¤ts_by_size).count(); + let schedule = compact_pair_schedule( + source_count, + target_size, + ¤ts_by_size, + &source_slots_by_current, + ); + assert_eq!( + schedule.theoretical_candidate_count, unfiltered_count, + "case {case_index}", + ); + let emitted = schedule.pairs.map(|pair| pair.unwrap()).collect::>(); + assert_eq!( + emitted, reference, + "case {case_index}, source_count={source_count}, target_size={target_size}", + ); + } + } + + #[test] + fn compact_disjoint_parent_pairs_preserve_duplicates_and_wide_supports() { + let source_count = 137; + let currents_by_size = vec![vec![0, 1, 2, 3], vec![4, 5, 6, 7]]; + let source_slots_by_current = vec![ + vec![0], + vec![128], + vec![128], + vec![136], + vec![0, 64], + vec![1, 128], + vec![64, 136], + vec![1, 128], + ]; + assert!(matches!( + TransientSupportKey::from_source_slots(source_count, &[0, 64, 128, 136]).unwrap(), + TransientSupportKey::Wide { .. } + )); + let left = TransientSupportKey::from_source_slots(source_count, &[0, 128]).unwrap(); + let right = TransientSupportKey::from_source_slots(source_count, &[64, 136]).unwrap(); + assert_eq!( + left.union_disjoint(&right).unwrap().source_slots(), + [0, 64, 128, 136] + ); + + let reference = + reference_disjoint_parent_pairs(3, ¤ts_by_size, &source_slots_by_current); + let schedule = + compact_pair_schedule(source_count, 3, ¤ts_by_size, &source_slots_by_current); + assert!(schedule.support_bucket_cache_hit_count > 0); + assert_eq!( + schedule + .pairs + .map(|pair| pair.unwrap().parent_ids) + .collect::>(), + reference + ); + } + + #[test] + fn disjoint_pair_schedule_releases_bucket_cache_before_emission() { + let source_count = 6; + let currents_by_size = vec![vec![0, 1, 2, 3], vec![4, 5, 6, 7]]; + let source_slots_by_current = vec![ + vec![0], + vec![0], + vec![1], + vec![2], + vec![0, 1], + vec![2, 3], + vec![3, 4], + vec![1, 5], + ]; + let support_keys = TransientCurrentSupportKeys { + source_count, + keys_by_current: source_slots_by_current + .iter() + .map(|support| { + TransientSupportKey::from_source_slots(source_count, support).unwrap() + }) + .collect(), + }; + let mut support_buckets = + LaneSupportBuckets::new(¤ts_by_size, &support_keys).unwrap(); + let schedule = disjoint_parent_pairs_for_target( + 3, + ¤ts_by_size, + &support_keys, + &mut support_buckets, + ) + .unwrap(); + + assert!(schedule.support_bucket_cache_hit_count > 0); + assert_eq!(support_buckets.cached_disjoint_query_count(), 0); + assert_eq!(support_buckets.cached_disjoint_capacity(), 0); + assert_eq!( + schedule + .pairs + .map(|pair| pair.unwrap().parent_ids) + .collect::>(), + reference_disjoint_parent_pairs(3, ¤ts_by_size, &source_slots_by_current), + ); + assert_eq!(support_buckets.cached_disjoint_query_count(), 0); + assert_eq!(support_buckets.cached_disjoint_capacity(), 0); + } + + #[test] + fn transient_wide_support_validates_padding_and_source_domain() { + let exact = TransientSupportKey::from_source_slots(129, &[0, 64, 128]).unwrap(); + assert_eq!(exact.source_count(), 129); + assert_eq!(exact.source_slots(), [0, 64, 128]); + assert!( + TransientSupportKey::full(129) + .unwrap() + .without_source_slot(129) + .is_err() + ); + + let padding_bit = TransientSupportKey::Wide { + source_count: 129, + words: vec![0, 0, 2].into_boxed_slice(), + }; + assert!(padding_bit.validate().is_err()); + let truncated = TransientSupportKey::Wide { + source_count: 129, + words: vec![0, 0].into_boxed_slice(), + }; + assert!(truncated.validate().is_err()); + + let left = TransientSupportKey::from_source_slots(129, &[0]).unwrap(); + let other_domain = TransientSupportKey::from_source_slots(130, &[1]).unwrap(); + assert!(left.is_disjoint(&other_domain).is_err()); + assert!(left.union_disjoint(&other_domain).is_err()); + assert_ne!(left, other_domain); + } + + #[test] + fn state_index_preserves_interleaved_reference_transition_order() { + let rows = [ + (transition(0), [1, 2]), + (transition(1), [2, 1]), + (transition(2), [1, 2]), + (transition(3), [3, 4]), + (transition(4), [2, 1]), + ]; + let mut index = TransitionStateIndex::default(); + for (row, input_states) in rows { + index.insert(row, input_states); + } + + let actual = index + .rows(1, 2) + .iter() + .map(|indexed| Ok((indexed.row.id, indexed.parent_ids(1, 2, 10, 20)?))) + .collect::>>() + .unwrap(); + let reference = rows + .iter() + .flat_map(|(row, input_states)| { + let mut applications = Vec::new(); + if *input_states == [1, 2] { + applications.push((row.id, [10, 20])); + } + if *input_states == [2, 1] { + applications.push((row.id, [20, 10])); + } + applications + }) + .collect::>(); + assert_eq!(actual, reference); + assert_eq!( + actual, + [(0, [10, 20]), (1, [20, 10]), (2, [10, 20]), (4, [20, 10])] + ); + } + + fn encoded_strings(values: &[&str]) -> (Vec, Vec) { + let mut ranges = Vec::new(); + let mut bytes = Vec::new(); + for value in values { + ranges.push(CheckedTableRange::new( + bytes.len() as u64, + value.len() as u64, + )); + bytes.extend_from_slice(value.as_bytes()); + } + (ranges, bytes) + } + + fn indexed_u32_sequences(sequences: &[&[u32]]) -> (Vec, Vec) { + let mut ranges = Vec::new(); + let mut values = Vec::new(); + for (id, sequence) in sequences.iter().enumerate() { + ranges.push(IndexedRangeRow { + id: id as u32, + range: CheckedTableRange::new(values.len() as u64, sequence.len() as u64), + }); + values.extend_from_slice(sequence); + } + (ranges, values) + } + + fn append_u32_sequence(template: &mut OwnedRecurrenceTemplateInput, sequence: &[u32]) -> u32 { + let id = template.u32_sequence_ranges.len() as u32; + template.u32_sequence_ranges.push(IndexedRangeRow { + id, + range: CheckedTableRange::new( + template.u32_sequence_values.len() as u64, + sequence.len() as u64, + ), + }); + template.u32_sequence_values.extend_from_slice(sequence); + id + } + + fn append_template_string(template: &mut OwnedRecurrenceTemplateInput, value: &str) -> u32 { + let id = template.string_ranges.len() as u32; + template.string_ranges.push(CheckedTableRange::new( + template.string_bytes.len() as u64, + value.len() as u64, + )); + template.string_bytes.extend_from_slice(value.as_bytes()); + id + } + + fn scalar_reference_template() -> OwnedRecurrenceTemplateInput { + const EMPTY: u32 = 0; + const STATE_PAIR: u32 = 1; + const CANONICAL_ORDER: u32 = 2; + const PARENT_FLAVOURS: u32 = 3; + const PARENT_QUANTUM_NUMBERS: u32 = 4; + + let (string_ranges, string_bytes) = encoded_strings(&["0", "1", "constant-result"]); + let (u32_sequence_ranges, u32_sequence_values) = + indexed_u32_sequences(&[&[], &[0, 0], &[0, 1], &[0, 0], &[0, 0]]); + OwnedRecurrenceTemplateInput { + input_abi: "scalar-reference-template-v1".to_owned(), + catalog_digest: digest(1), + compiled_model_digest: digest(2), + prepared_kernel_pack_digest: digest(3), + catalog_header: vec![], + coupling_order_ranges: vec![IndexedRangeRow { + id: 0, + range: CheckedTableRange::new(0, 0), + }], + coupling_order_terms: vec![], + contact_orbit_certificates: vec![], + contact_orbit_steps: vec![], + current_states: vec![], + digest_catalog: vec![DigestCatalogRow { + id: 0, + value: [4; 32], + }], + evaluator_bindings: vec![], + exact_factors: vec![ExactFactorRow { + id: 0, + real_numerator_string_id: 1, + real_denominator_string_id: 1, + imag_numerator_string_id: 0, + imag_denominator_string_id: 1, + }], + flavour_flow_ranges: vec![IndexedRangeRow { + id: 0, + range: CheckedTableRange::new(0, 1), + }], + flavour_flow_values: vec![0], + i32_sequence_ranges: vec![IndexedRangeRow { + id: 0, + range: CheckedTableRange::new(0, 2), + }], + i32_sequence_values: vec![0, 0], + parameters: vec![], + propagators: vec![], + quantum_flows: vec![QuantumFlowRow { + id: 0, + template_string_id: 0, + input_state_sequence_id: STATE_PAIR, + input_spin_sequence_id: 0, + input_flavour_sequence_id: PARENT_FLAVOURS, + input_quantum_sequence_id: PARENT_QUANTUM_NUMBERS, + flavour_flow_operation_string_id: 2, + quantum_number_flow_operation_string_id: 2, + coupling_order_set_id: 0, + result_state_template_id: 0, + result_spin_state: 0, + result_flavour_flow_id: 0, + result_quantum_number_flow_id: 0, + exact_coupling_factor_id: 0, + predicate_digest_id: 0, + semantic_digest_id: 0, + }], + quantum_number_flow_ranges: vec![], + quantum_number_flow_terms: vec![], + runtime_helicity_contracts: vec![], + runtime_helicity_variants: vec![], + runtime_helicity_embeddings: vec![], + runtime_helicity_projections: vec![], + sources: vec![], + string_ranges, + string_bytes, + symmetry_proofs: vec![], + transitions: vec![TransitionRow { + input_state_sequence_id: STATE_PAIR, + canonical_input_order_sequence_id: CANONICAL_ORDER, + input_exchange_factor_id: MISSING_U32, + ..transition(0) + }], + closures: vec![ClosureRow { + id: 0, + template_string_id: 0, + input_state_sequence_id: STATE_PAIR, + result_state_template_id: MISSING_U32, + evaluator_binding_id: 0, + canonical_input_order_sequence_id: CANONICAL_ORDER, + coupling_parameter_sequence_id: EMPTY, + coupling_order_set_id: 0, + eligible_quantum_flow_sequence_id: EMPTY, + color_contraction_template_id: 1, + binding_coupling_factor_id: 0, + exact_factor_id: 0, + output_factor_source: 0, + equivalence_class_string_id: 0, + input_exchange_factor_id: MISSING_U32, + projection_string_id: 0, + component_coefficient_sequence_id: EMPTY, + chirality_relation_string_id: 0, + metric_signature_string_id: 0, + semantic_digest_id: 0, + }], + color_contractions: vec![ + ColorContractionRow { + id: 0, + template_string_id: 0, + rule_kind_string_id: 0, + input_representation_sequence_id: EMPTY, + has_output_representation: 1, + output_representation: 1, + ordered_open_string_arity: 0, + exact_coefficient_factor_id: 0, + witness_start: 0, + witness_count: 1, + nc_term_start: 0, + nc_term_count: 0, + expression_digest_id: 0, + semantic_digest_id: 0, + }, + ColorContractionRow { + id: 1, + template_string_id: 0, + rule_kind_string_id: 0, + input_representation_sequence_id: EMPTY, + has_output_representation: 0, + output_representation: 0, + ordered_open_string_arity: 0, + exact_coefficient_factor_id: 0, + witness_start: 1, + witness_count: 1, + nc_term_start: 0, + nc_term_count: 0, + expression_digest_id: 0, + semantic_digest_id: 0, + }, + ], + lc_color_transition_witnesses: vec![ + LCColorTransitionWitnessRow { + color_contraction_id: 0, + ordinal: 0, + left_shape_string_id: 0, + right_shape_string_id: 0, + input_permutation: 0, + reverse_parent_mask: 0, + component_operation: LCColorComponentOperation::Empty as u8, + result_component_kind: u8::MAX, + result_component_role: LCColorComponentRole::None as u8, + result_shape_string_id: 0, + exact_factor_id: 0, + proof_digest_id: 0, + input_port_pairing_sequence_id: EMPTY, + result_port_binding_sequence_id: EMPTY, + provenance_sequence_id: EMPTY, + }, + LCColorTransitionWitnessRow { + color_contraction_id: 1, + ordinal: 0, + left_shape_string_id: 0, + right_shape_string_id: 0, + input_permutation: 0, + reverse_parent_mask: 0, + component_operation: LCColorComponentOperation::Close as u8, + result_component_kind: u8::MAX, + result_component_role: LCColorComponentRole::None as u8, + result_shape_string_id: MISSING_U32, + exact_factor_id: 0, + proof_digest_id: 0, + input_port_pairing_sequence_id: EMPTY, + result_port_binding_sequence_id: EMPTY, + provenance_sequence_id: EMPTY, + }, + ], + color_nc_terms: vec![], + u32_sequence_ranges, + u32_sequence_values, + } + } + + #[test] + fn established_prepared_transition_binds_only_certified_contact_metadata() { + use crate::recurrence::contact_orbit_owner::{ + ContactOrbitTestBinding, contact_orbit_test_template, + }; + + let none = contact_orbit_test_template(ContactOrbitTestBinding::None) + .validate() + .unwrap(); + let none_catalog = TemplateCatalog::new(none.input()).unwrap(); + let none_prepared = PreparedTransitionCatalog::new(none.input(), &none_catalog).unwrap(); + assert!(none_prepared.rows(0, 0)[0].contact_orbit.is_none()); + assert!(none_prepared.contact_orbit(0).is_none()); + + let one = contact_orbit_test_template(ContactOrbitTestBinding::One) + .validate() + .unwrap(); + let one_catalog = TemplateCatalog::new(one.input()).unwrap(); + let one_prepared = PreparedTransitionCatalog::new(one.input(), &one_catalog).unwrap(); + assert!(one_prepared.rows(0, 0)[0].contact_orbit.is_some()); + assert!(one_prepared.contact_orbit(0).is_some()); + assert!(one_prepared.contact_orbit(u32::MAX).is_none()); + + let mut currents = vec![ + projection_test_current(RecurrenceNodeKind::Source, 0, 0, &[0]), + projection_test_current(RecurrenceNodeKind::Source, 0, 1, &[1]), + projection_test_current(RecurrenceNodeKind::Current, 0, 2, &[0, 1]), + ]; + add_projection_test_contribution(&mut currents, 2, 0, [0, 1], 0); + let plan = plan_established_contact_orbit_owners(2, &one_prepared, ¤ts, 1) + .unwrap() + .expect("certified transition must enter established owner planning"); + let mut resident = 1; + plan.commit(&mut currents, &mut resident); + assert_eq!(resident, 1); + assert_eq!(established_contact_transition_ids(¤ts[2]), [0]); + } + + #[test] + fn prepared_transition_catalog_matches_lazy_reference_metadata_and_order() { + let mut template = scalar_reference_template(); + let seed = template.transitions[0]; + template.transitions = [2, 0, 1] + .map(|id| TransitionRow { + id, + input_exchange_factor_id: 0, + ..seed + }) + .to_vec(); + let catalog = TemplateCatalog::new(&template).unwrap(); + let lazy_index = TransitionStateIndex::new(&template, &catalog).unwrap(); + let prepared_catalog = PreparedTransitionCatalog::new(&template, &catalog).unwrap(); + + assert_eq!( + prepared_catalog.decoded_transition_count(), + template.transitions.len() + ); + assert_eq!(prepared_catalog.decoded_witness_count(), 3); + assert_eq!( + prepared_catalog.structural_transitions(), + structural_transitions(&template, &catalog) + .unwrap() + .as_slice() + ); + + let lazy_rows = lazy_index.rows(0, 0); + let prepared_rows = prepared_catalog.rows(0, 0); + assert_eq!( + prepared_rows + .iter() + .map(|prepared| prepared.row.id) + .collect::>(), + [2, 0, 1] + ); + assert!( + [2, 0, 1] + .into_iter() + .all(|transition_id| prepared_catalog.contact_orbit(transition_id).is_none()) + ); + let mut uncertified_currents = vec![ + projection_test_current(RecurrenceNodeKind::Source, 0, 0, &[0]), + projection_test_current(RecurrenceNodeKind::Source, 0, 1, &[1]), + projection_test_current(RecurrenceNodeKind::Current, 0, 2, &[0, 1]), + ]; + add_projection_test_contribution(&mut uncertified_currents, 2, 0, [0, 1], 0); + assert!( + plan_established_contact_orbit_owners(2, &prepared_catalog, &uncertified_currents, 1,) + .unwrap() + .is_none() + ); + assert_eq!(prepared_rows.len(), lazy_rows.len()); + + let mut color_states = DynamicLCColorStateInterner::default(); + let color_id = color_states + .intern(DynamicLCColorState::new(0, None, vec![]).unwrap()) + .unwrap(); + let parent = |source_slot, state_template_id, spin_state| { + CurrentCoreKey::new( + template.catalog_digest, + RecurrenceNodeKind::Source, + state_template_id, + color_id, + vec![source_slot], + CanonicalMomentumLinearForm::new(vec![MomentumTerm { + source_slot, + coefficient: 1, + }]) + .unwrap(), + CurrentHelicityIdentity::all_flow_union(spin_state), + vec![0], + 0, + vec![], + CurrentSourceBinding::runtime_dispatch(source_slot, vec![source_slot]).unwrap(), + None, + ) + .unwrap() + }; + let left = parent(0, 0, 0); + let right = parent(1, 0, 0); + let parents = [&left, &right]; + + for (lazy, prepared) in lazy_rows.iter().zip(prepared_rows) { + assert_eq!(prepared.row, lazy.row); + assert_eq!(prepared.input_states, lazy.input_states); + assert_eq!( + prepared.parent_ids(0, 0, 20, 10).unwrap(), + lazy.parent_ids(0, 0, 20, 10).unwrap() + ); + let quantum = template.quantum_flows[prepared.row.quantum_flow_template_id as usize]; + assert_eq!(prepared.quantum, quantum); + assert_eq!( + prepared.quantum_input_states.as_slice(), + catalog + .u32_sequence(quantum.input_state_sequence_id, "quantum input states") + .unwrap() + ); + assert_eq!( + prepared.quantum_input_spins.as_slice(), + catalog + .i32_sequence(quantum.input_spin_sequence_id, "quantum input spins") + .unwrap() + ); + assert_eq!( + prepared.quantum_flow_matches(&parents), + quantum_flow_matches(quantum, &parents, &catalog).unwrap() + ); + assert_eq!( + prepared.result_flavour_flow(&parents), + quantum_flow_result_flavour(quantum, &parents, &catalog).unwrap() + ); + assert_eq!( + prepared.local_coupling_orders.as_ref(), + catalog + .coupling_orders(prepared.row.coupling_order_set_id) + .unwrap() + ); + assert_eq!( + prepared.contraction, + template.color_contractions[prepared.row.color_contraction_template_id as usize] + ); + assert_eq!( + prepared.canonical_evaluator_parents([20, 10]), + canonical_evaluator_parents( + [20, 10], + catalog + .u32_sequence( + prepared.row.canonical_input_order_sequence_id, + "transition canonical input order", + ) + .unwrap(), + prepared.row.input_exchange_factor_id, + &catalog, + "transition", + ) + .unwrap() + ); + let (_, prepared_exchange_factor) = prepared.canonical_evaluator_parents([20, 10]); + let (_, lazy_exchange_factor) = canonical_evaluator_parents( + [20, 10], + catalog + .u32_sequence( + prepared.row.canonical_input_order_sequence_id, + "transition canonical input order", + ) + .unwrap(), + prepared.row.input_exchange_factor_id, + &catalog, + "transition", + ) + .unwrap(); + assert_eq!( + prepared.transition_exact_factor, + catalog + .factor(prepared.row.exact_factor_id, "transition exact") + .unwrap() + ); + assert_eq!( + prepared.contraction_exact_factor, + catalog + .factor( + prepared.contraction.exact_coefficient_factor_id, + "color contraction", + ) + .unwrap() + ); + let binding_coupling = authenticate_runtime_coupling( + &catalog, + quantum, + prepared.row.binding_coupling_factor_id, + "transition", + ) + .unwrap(); + assert_eq!( + prepared.output_factor().unwrap(), + output_factor_from_binding( + binding_coupling, + prepared.row.output_factor_source, + "transition", + ) + .unwrap() + ); + assert_eq!( + multiply_factors(&[ + prepared.transition_exact_factor, + prepared_exchange_factor, + prepared.contraction_exact_factor, + prepared.output_factor().unwrap(), + ]) + .unwrap(), + multiply_factors(&[ + catalog + .factor(prepared.row.exact_factor_id, "transition exact") + .unwrap(), + lazy_exchange_factor, + catalog + .factor( + prepared.contraction.exact_coefficient_factor_id, + "color contraction", + ) + .unwrap(), + output_factor_from_binding( + binding_coupling, + prepared.row.output_factor_source, + "transition", + ) + .unwrap(), + ]) + .unwrap() + ); + assert_eq!( + prepared.quantum_semantic_digest, + catalog + .digest(quantum.semantic_digest_id, "quantum-flow semantic") + .unwrap() + ); + let lazy_witnesses = catalog + .witness_rows(prepared.row.color_contraction_template_id) + .unwrap() + .iter() + .copied() + .map(|row| PreparedTransitionWitness { + row, + witness: catalog.witness(row).unwrap(), + }) + .collect::>(); + assert_eq!(prepared.witnesses.as_ref(), lazy_witnesses.as_slice()); + } + } + + #[test] + fn prepared_closure_catalog_matches_lazy_reference_order_orientation_factors_and_witnesses() { + let mut template = scalar_reference_template(); + let state_01 = append_u32_sequence(&mut template, &[0, 1]); + let state_10 = append_u32_sequence(&mut template, &[1, 0]); + let reverse_order = append_u32_sequence(&mut template, &[1, 0]); + let minus_one_string = append_template_string(&mut template, "-1"); + template.exact_factors.push(ExactFactorRow { + id: 1, + real_numerator_string_id: minus_one_string, + real_denominator_string_id: 1, + imag_numerator_string_id: 0, + imag_denominator_string_id: 1, + }); + template.lc_color_transition_witnesses[1].input_permutation = 1; + template.lc_color_transition_witnesses[1].exact_factor_id = 1; + let seed = template.closures[0]; + template.closures = vec![ + ClosureRow { + id: 7, + input_state_sequence_id: state_01, + canonical_input_order_sequence_id: reverse_order, + input_exchange_factor_id: 1, + exact_factor_id: 1, + ..seed + }, + ClosureRow { + id: 3, + input_state_sequence_id: state_10, + canonical_input_order_sequence_id: reverse_order, + input_exchange_factor_id: 1, + exact_factor_id: 1, + ..seed + }, + ClosureRow { + id: 9, + input_state_sequence_id: state_01, + canonical_input_order_sequence_id: reverse_order, + input_exchange_factor_id: 1, + exact_factor_id: 1, + ..seed + }, + ]; + let catalog = TemplateCatalog::new(&template).unwrap(); + let prepared = PreparedClosureCatalog::new(&template, &catalog).unwrap(); + + for (anchor_state, complement_state, anchor_id, complement_id) in [ + (0, 1, 10, 20), + (1, 0, 30, 40), + (0, 0, 50, 60), + (2, 3, 70, 80), + ] { + let actual = prepared + .rows(anchor_state, complement_state) + .iter() + .map(|closure| { + Ok(( + closure.row.id, + closure.parent_ids( + anchor_state, + complement_state, + anchor_id, + complement_id, + )?, + )) + }) + .collect::>>() + .unwrap(); + let reference = template + .closures + .iter() + .flat_map(|closure| { + let input_states = catalog + .u32_sequence(closure.input_state_sequence_id, "closure input states") + .unwrap(); + let mut applications = Vec::new(); + if input_states == [complement_state, anchor_state] { + applications.push((closure.id, [complement_id, anchor_id])); + } + if input_states == [anchor_state, complement_state] + && anchor_state != complement_state + { + applications.push((closure.id, [anchor_id, complement_id])); + } + applications + }) + .collect::>(); + assert_eq!(actual, reference); + } + assert_eq!( + prepared + .rows(0, 1) + .iter() + .map(|closure| closure.row.id) + .collect::>(), + [7, 3, 9], + ); + + let closure = &prepared.rows(0, 1)[0]; + let parent_ids = closure.parent_ids(0, 1, 10, 20).unwrap(); + assert_eq!(parent_ids, [10, 20]); + let (prepared_evaluator_parents, prepared_exchange_factor) = + closure.canonical_evaluator_parents(parent_ids); + let (lazy_evaluator_parents, lazy_exchange_factor) = canonical_evaluator_parents( + parent_ids, + catalog + .u32_sequence( + closure.row.canonical_input_order_sequence_id, + "closure canonical input order", + ) + .unwrap(), + closure.row.input_exchange_factor_id, + &catalog, + "closure", + ) + .unwrap(); + assert_eq!(prepared_evaluator_parents, lazy_evaluator_parents); + assert_eq!(prepared_exchange_factor, lazy_exchange_factor); + assert_eq!( + prepared_exchange_factor, + ExactComplexRational::ONE.checked_neg().unwrap(), + ); + assert_eq!( + closure.witnesses[0].row, + template.lc_color_transition_witnesses[1], + ); + assert_eq!( + closure.witnesses[0].witness, + catalog + .witness(template.lc_color_transition_witnesses[1]) + .unwrap(), + ); + let prepared_factor = multiply_factors(&[ + closure.closure_exact_factor, + prepared_exchange_factor, + closure.contraction_exact_factor, + closure.quantum_flows[0].output_factor().unwrap(), + closure.witnesses[0].witness.exact_factor(), + ]) + .unwrap(); + let binding_coupling = catalog + .factor( + closure.row.binding_coupling_factor_id, + "closure binding coupling", + ) + .unwrap(); + let lazy_factor = multiply_factors(&[ + catalog + .factor(closure.row.exact_factor_id, "closure exact") + .unwrap(), + lazy_exchange_factor, + catalog + .factor( + template.color_contractions[closure.row.color_contraction_template_id as usize] + .exact_coefficient_factor_id, + "closure color", + ) + .unwrap(), + output_factor_from_binding( + binding_coupling, + closure.row.output_factor_source, + "closure", + ) + .unwrap(), + catalog + .witness(template.lc_color_transition_witnesses[1]) + .unwrap() + .exact_factor(), + ]) + .unwrap(); + assert_eq!(prepared_factor, lazy_factor); + assert_eq!( + prepared_factor, + ExactComplexRational::ONE.checked_neg().unwrap(), + ); + } + + #[test] + fn failure_only_closure_color_diagnostics_match_the_lazy_reference() { + let template = scalar_reference_template(); + let catalog = TemplateCatalog::new(&template).unwrap(); + let prepared = PreparedClosureCatalog::new(&template, &catalog).unwrap(); + let currents = scalar_structural_sources(&template, 2); + let mut color_states = DynamicLCColorStateInterner::default(); + assert_eq!( + color_states + .intern(DynamicLCColorState::new(0, None, vec![]).unwrap()) + .unwrap() + .get(), + 0, + ); + + let actual = + collect_closure_color_attempts(&prepared, &color_states, ¤ts, &[0], &[1]) + .unwrap(); + let parents = [¤ts[1].key, ¤ts[0].key]; + let left = color_states + .get(parents[0].dynamic_lc_color_state_id()) + .unwrap(); + let right = color_states + .get(parents[1].dynamic_lc_color_state_id()) + .unwrap(); + let reference = catalog + .witness_rows(template.closures[0].color_contraction_template_id) + .unwrap() + .iter() + .filter(|row| { + row.left_shape_string_id == left.output_color_shape_id() + && row.right_shape_string_id == right.output_color_shape_id() + }) + .map(|row| { + catalog + .witness(*row) + .unwrap() + .closed_components(left, right) + .unwrap() + .iter() + .map(|component| (component.kind(), component.source_slots().to_vec())) + .collect::>() + }) + .collect::>(); + assert_eq!(actual, reference); + assert_eq!(actual, BTreeSet::from([Vec::new()])); + } + + #[test] + fn prepared_flavour_flow_preserves_parent_orientation_and_union_collapse() { + let mut color_states = DynamicLCColorStateInterner::default(); + let color_id = color_states + .intern(DynamicLCColorState::new(0, None, vec![]).unwrap()) + .unwrap(); + let parent = |source_slot, flavour_flow, helicity_identity| { + CurrentCoreKey::new( + digest(211), + RecurrenceNodeKind::Current, + 0, + color_id, + vec![source_slot], + CanonicalMomentumLinearForm::new(vec![MomentumTerm { + source_slot, + coefficient: 1, + }]) + .unwrap(), + helicity_identity, + flavour_flow, + 0, + vec![], + CurrentSourceBinding::None, + None, + ) + .unwrap() + }; + let left = parent( + 0, + vec![10, 20], + CurrentHelicityIdentity::topology_replay(0, vec![SourceStateAssignment::new(0, 0)]) + .unwrap(), + ); + let right = parent( + 1, + vec![30, 40], + CurrentHelicityIdentity::topology_replay(0, vec![SourceStateAssignment::new(1, 0)]) + .unwrap(), + ); + let parents = [&left, &right]; + let reversed = [&right, &left]; + + for (prepared, expected, reversed_expected) in [ + ( + PreparedFlavourFlow::Constant(vec![7, 9].into_boxed_slice()), + vec![7, 9], + vec![7, 9], + ), + ( + PreparedFlavourFlow::AppendLeft(9), + vec![10, 20, 9], + vec![30, 40, 9], + ), + ( + PreparedFlavourFlow::AppendRight(9), + vec![30, 40, 9], + vec![10, 20, 9], + ), + ( + PreparedFlavourFlow::ConcatLeftRight(9), + vec![10, 20, 30, 40, 9], + vec![30, 40, 10, 20, 9], + ), + ] { + assert_eq!(prepared.apply(&parents), expected); + assert_eq!(prepared.apply(&reversed), reversed_expected); + + let union_left = parent(0, vec![10, 20], CurrentHelicityIdentity::all_flow_union(0)); + let union_right = parent(1, vec![30, 40], CurrentHelicityIdentity::all_flow_union(0)); + assert_eq!(prepared.apply(&[&union_left, &union_right]), [9]); + } + } + + #[test] + fn prepared_transition_catalog_preserves_lazy_binary_contract_errors() { + let mut non_binary_transition = scalar_reference_template(); + non_binary_transition.u32_sequence_ranges[1].range = CheckedTableRange::new(0, 1); + let catalog = TemplateCatalog::new(&non_binary_transition).unwrap(); + assert_eq!( + PreparedTransitionCatalog::new(&non_binary_transition, &catalog).unwrap_err(), + TransitionStateIndex::new(&non_binary_transition, &catalog).unwrap_err() + ); + + let mut non_binary_quantum = scalar_reference_template(); + non_binary_quantum.i32_sequence_ranges[0].range = CheckedTableRange::new(0, 1); + let catalog = TemplateCatalog::new(&non_binary_quantum).unwrap(); + assert_eq!( + PreparedTransitionCatalog::new(&non_binary_quantum, &catalog).unwrap_err(), + structural_transitions(&non_binary_quantum, &catalog).unwrap_err() + ); + } + + #[test] + fn prepared_transition_catalog_preserves_lazy_metadata_errors() { + let mut invalid_order = scalar_reference_template(); + invalid_order.u32_sequence_values[2..4].copy_from_slice(&[0, 0]); + let catalog = TemplateCatalog::new(&invalid_order).unwrap(); + let row = invalid_order.transitions[0]; + let lazy_error = canonical_evaluator_parents( + [20, 10], + catalog + .u32_sequence( + row.canonical_input_order_sequence_id, + "transition canonical input order", + ) + .unwrap(), + row.input_exchange_factor_id, + &catalog, + "transition", + ) + .unwrap_err(); + assert_eq!( + PreparedTransitionCatalog::new(&invalid_order, &catalog).unwrap_err(), + lazy_error + ); + + let mut invalid_witness = scalar_reference_template(); + invalid_witness.lc_color_transition_witnesses[0].input_permutation = 2; + let catalog = TemplateCatalog::new(&invalid_witness).unwrap(); + let lazy_error = catalog + .witness(invalid_witness.lc_color_transition_witnesses[0]) + .unwrap_err(); + assert_eq!( + PreparedTransitionCatalog::new(&invalid_witness, &catalog).unwrap_err(), + lazy_error + ); + } + + fn scalar_reference_process(external_count: usize) -> OwnedRecurrenceProcessInput { + OwnedRecurrenceProcessInput { + input_abi: "scalar-reference-process-v1".to_owned(), + declared_input_digest: digest(5), + fermion_pairing: None, + bitset_ranges: vec![], + bitset_words: vec![], + coupling_limits: vec![], + digest_catalog: vec![], + exact_factors: vec![], + external_legs: (0..external_count) + .map(|slot| ProcessExternalLegRow { + source_slot: slot as u32, + public_label: slot as u32 + 1, + physical_pdg: 0, + outgoing_pdg: 0, + is_initial: 0, + is_fermionic: 0, + source_state_range: CheckedTableRange::new(0, 0), + momentum_mask_id: 0, + support_mask_id: 0, + }) + .collect(), + header: vec![], + header_digests: vec![], + lc_open_strings: vec![], + normalization: vec![], + parameter_projection: vec![], + physical_lc_sectors: vec![ProcessPhysicalLCSectorRow { + sector_id: 0, + public_id_string_id: 0, + kind: ProcessLCSectorKind::Singlet as u8, + closure_source_slot: 0, + closure_proof_algorithm_string_id: 0, + closure_proof_digest_id: 0, + open_string_range: CheckedTableRange::new(0, 0), + trace_sequence_id: 0, + singlet_sequence_id: 0, + word_sequence_id: 0, + support_mask_id: 0, + }], + public_lc_flows: vec![], + replay_partitions: vec![], + replay_targets: vec![], + selected_public_flow_coverage: vec![], + selected_source_coverage: vec![], + semantic_template_references: vec![], + source_states: vec![], + string_ranges: vec![], + string_bytes: vec![], + u32_sequence_ranges: vec![CheckedTableRange::new(0, 0)], + u32_sequence_values: vec![], + } + } + + fn cyclic_trace_anchor_contract_fixture() + -> (OwnedRecurrenceProcessInput, OwnedRecurrenceTemplateInput) { + let mut template = scalar_reference_template(); + let color_shape_string_id = append_template_string(&mut template, "adjoint-segment"); + let direct_closure_string_id = append_template_string(&mut template, "direct-contraction"); + template.current_states = vec![crate::recurrence::template::CurrentStateRow { + id: 0, + template_string_id: 0, + particle_id: 21, + anti_particle_id: 21, + species_string_id: 0, + orientation: CurrentOrientation::SelfConjugate as u8, + statistics: ParticleStatistics::Boson as u8, + color_representation: 8, + basis_string_id: 0, + tensor_ordering_sequence_id: 0, + dimension: 4, + chirality: 0, + lc_color_shape_string_id: color_shape_string_id, + auxiliary_kind_string_id: MISSING_U32, + mass_parameter_id: MISSING_U32, + width_parameter_id: MISSING_U32, + semantic_digest_id: 0, + }]; + template.evaluator_bindings = vec![crate::recurrence::template::EvaluatorBindingRow { + id: 0, + resolver_key_string_id: 0, + prepared_kernel_id: MISSING_U32, + contract_kind: EvaluatorContractKind::Closure as u8, + callable_signature_digest_id: 0, + input_state_sequence_id: 1, + output_state_template_id: MISSING_U32, + input_layout_sequence_id: 0, + output_layout_sequence_id: 0, + exact_expression_digest_sequence_id: 0, + semantic_template_sequence_id: 0, + callable_kind: EvaluatorCallableKind::RusticolTemplate as u8, + runtime_template_string_id: 0, + semantic_digest_id: 0, + }]; + template.closures[0].equivalence_class_string_id = direct_closure_string_id; + template.i32_sequence_values = vec![8, 8]; + + let mut process = scalar_reference_process(4); + let (string_ranges, string_bytes) = + encoded_strings(&[CYCLIC_TRACE_CLOSURE_ANCHOR_ALGORITHM]); + process.string_ranges = string_ranges; + process.string_bytes = string_bytes; + process.u32_sequence_ranges = + vec![CheckedTableRange::new(0, 0), CheckedTableRange::new(0, 4)]; + process.u32_sequence_values = vec![0, 1, 2, 3]; + process.physical_lc_sectors[0] = ProcessPhysicalLCSectorRow { + kind: ProcessLCSectorKind::SingleTrace as u8, + closure_source_slot: 0, + closure_proof_algorithm_string_id: 0, + trace_sequence_id: 1, + singlet_sequence_id: 0, + word_sequence_id: 1, + ..process.physical_lc_sectors[0] + }; + process.source_states = (0..4) + .map(|source_slot| ProcessSourceStateRow { + source_slot, + state_index: 0, + public_helicity: 1, + chirality: 0, + spin_state: 1, + current_state_template_id: 0, + source_template_id: source_slot, + momentum_sign: 1, + crossing_phase_factor_id: 0, + }) + .collect(); + for (source_slot, leg) in process.external_legs.iter_mut().enumerate() { + leg.physical_pdg = 21; + leg.outgoing_pdg = 21; + leg.source_state_range = CheckedTableRange::new(source_slot as u64, 1); + } + (process, template) + } + + #[test] + fn cyclic_trace_anchor_constructor_authenticates_template_contracts() { + let (process, template) = cyclic_trace_anchor_contract_fixture(); + let process_catalog = ProcessCatalog::new(&process).unwrap(); + let template_catalog = TemplateCatalog::new(&template).unwrap(); + validate_cyclic_trace_closure_anchor_contracts( + &process, + &process_catalog, + &template, + &template_catalog, + ) + .expect("authenticated adjoint trace closure contract must pass"); + + let mut non_adjoint = template.clone(); + non_adjoint.current_states[0].color_representation = 3; + let catalog = TemplateCatalog::new(&non_adjoint).unwrap(); + assert!( + validate_cyclic_trace_closure_anchor_contracts( + &process, + &process_catalog, + &non_adjoint, + &catalog, + ) + .unwrap_err() + .to_string() + .contains("self-conjugate adjoint") + ); + + let mut missing_direct_closure = template.clone(); + missing_direct_closure.closures[0].result_state_template_id = 0; + let catalog = TemplateCatalog::new(&missing_direct_closure).unwrap(); + assert!( + validate_cyclic_trace_closure_anchor_contracts( + &process, + &process_catalog, + &missing_direct_closure, + &catalog, + ) + .unwrap_err() + .to_string() + .contains("exact direct binary adjoint closure") + ); + + let mut coupling_order_closure = template.clone(); + let coupling_order_name = append_template_string(&mut coupling_order_closure, "QCD"); + coupling_order_closure.coupling_order_ranges[0].range = CheckedTableRange::new(0, 1); + coupling_order_closure.coupling_order_terms = + vec![crate::recurrence::template::CouplingOrderTermRow { + set_id: 0, + name_string_id: coupling_order_name, + power: 1, + }]; + let catalog = TemplateCatalog::new(&coupling_order_closure).unwrap(); + assert!( + validate_cyclic_trace_closure_anchor_contracts( + &process, + &process_catalog, + &coupling_order_closure, + &catalog, + ) + .unwrap_err() + .to_string() + .contains("exact direct binary adjoint closure") + ); + + let mut established_process = process.clone(); + let (string_ranges, string_bytes) = encoded_strings(&["canonical-lc-closure-anchor-v2"]); + established_process.string_ranges = string_ranges; + established_process.string_bytes = string_bytes; + let catalog = ProcessCatalog::new(&established_process).unwrap(); + validate_cyclic_trace_closure_anchor_contracts( + &established_process, + &catalog, + &non_adjoint, + &TemplateCatalog::new(&non_adjoint).unwrap(), + ) + .expect("non-v4 recurrence contracts must remain untouched"); + } + + fn scalar_structural_sources( + template: &OwnedRecurrenceTemplateInput, + external_count: usize, + ) -> Vec { + let mut color_states = DynamicLCColorStateInterner::default(); + let color_id = color_states + .intern(DynamicLCColorState::new(0, None, vec![]).unwrap()) + .unwrap(); + (0..external_count as u32) + .map(|source_slot| { + let key = CurrentCoreKey::new( + template.catalog_digest, + RecurrenceNodeKind::Source, + 0, + color_id, + vec![source_slot], + CanonicalMomentumLinearForm::new(vec![MomentumTerm { + source_slot, + coefficient: 1, + }]) + .unwrap(), + CurrentHelicityIdentity::all_flow_union(0), + vec![0], + 0, + vec![], + CurrentSourceBinding::runtime_dispatch(source_slot, vec![source_slot]).unwrap(), + None, + ) + .unwrap(); + PendingCurrent { + key, + source_exact_factor: None, + contributions: BTreeMap::new(), + realized_pairing_rule_ids: BTreeSet::new(), + reflection: CurrentReflection::Unavailable, + reflection_certificate_id: None, + } + }) + .collect() + } + + #[test] + fn lane_closure_support_index_matches_reference_filter_and_preserves_current_id_order() { + let template = scalar_reference_template(); + let mut currents = scalar_structural_sources(&template, 4); + let color_id = currents[0].key.dynamic_lc_color_state_id(); + let internal = |support: Vec| { + let momentum = CanonicalMomentumLinearForm::new( + support + .iter() + .copied() + .map(|source_slot| MomentumTerm { + source_slot, + coefficient: 1, + }) + .collect(), + ) + .unwrap(); + PendingCurrent { + key: CurrentCoreKey::new( + template.catalog_digest, + RecurrenceNodeKind::Current, + 0, + color_id, + support, + momentum, + CurrentHelicityIdentity::all_flow_union(0), + vec![0], + 0, + vec![], + CurrentSourceBinding::None, + None, + ) + .unwrap(), + source_exact_factor: None, + contributions: BTreeMap::new(), + realized_pairing_rule_ids: BTreeSet::new(), + reflection: CurrentReflection::Unavailable, + reflection_certificate_id: None, + } + }; + currents.push(internal(vec![1, 2, 3])); + currents.push(internal(vec![1, 2, 3])); + currents.push(internal(vec![0, 2])); + + let global = LaneClosureSupportIndex::new(¤ts, None).unwrap(); + assert_eq!(global.current_ids(&[0]), [0]); + assert_eq!(global.current_ids(&[1, 2, 3]), [4, 5]); + assert_eq!(global.current_ids(&[0, 2]), [6]); + + let domain = PendingConstructionDomain { + shared_source_end: 4, + lane_internal_start: 5, + lane_internal_end: 7, + }; + let lane = LaneClosureSupportIndex::new(¤ts, Some(domain)).unwrap(); + for support in [&[0][..], &[1, 2, 3][..], &[0, 2][..], &[0, 3][..]] { + let reference = currents + .iter() + .enumerate() + .filter(|(current_id, current)| { + domain.contains(*current_id) && current.key.support_source_slots() == support + }) + .map(|(current_id, _)| current_id as u32) + .collect::>(); + assert_eq!(lane.current_ids(support), reference); + } + } + + #[test] + fn shared_structural_feasibility_matches_lazy_multi_lane_demands() { + let template = scalar_reference_template(); + let catalog = TemplateCatalog::new(&template).unwrap(); + let prepared = PreparedTransitionCatalog::new(&template, &catalog).unwrap(); + let mut process = scalar_reference_process(4); + let mut second_sector = process.physical_lc_sectors[0]; + second_sector.sector_id = 1; + second_sector.closure_source_slot = 1; + process.physical_lc_sectors.push(second_sector); + let sources = scalar_structural_sources(&template, 4); + let feasibility = StructuralFeasibilityIndex::new(4, &prepared, &sources).unwrap(); + + assert!(feasibility.feasible_support_count() > sources.len()); + assert!(feasibility.decomposition_count() > 0); + assert!(feasibility.forward_transition_probe_count() > 0); + for materialized_sectors in [ + BTreeSet::from([0]), + BTreeSet::from([1]), + BTreeSet::from([0, 1]), + ] { + let lazy = LazyStructuralDemandIndex::new( + &process, + &template, + &catalog, + &prepared, + &materialized_sectors, + &sources, + ) + .unwrap(); + let indexed = StructuralDemandIndex::new( + &process, + &template, + &catalog, + &prepared, + &feasibility, + &materialized_sectors, + ) + .unwrap(); + assert_eq!(indexed.demanded_rows(), lazy.demanded); + for (support, state) in &lazy.demanded { + let support_key = TransientSupportKey::from_source_slots(4, support).unwrap(); + assert_eq!( + indexed.accepts( + &support_key, + state.state_template_id, + state.spin_state_class, + ), + lazy.accepts(support, state.state_template_id, state.spin_state_class,) + ); + } + } + } + + #[test] + fn contracted_color_without_replay_proof_keeps_the_direct_destination_domain() { + let mut process = scalar_reference_process(2); + process.header = vec![ProcessHeaderRow { + schema_version: 1, + abi_string_id: 0, + process_id_string_id: 0, + layout: u8::try_from(RecurrenceStrategy::ContractedColorUnion.as_u32()) + .expect("strategy discriminant fits u8"), + selected_flow_mode: 0, + selected_source_mode: 0, + external_leg_count: 2, + physical_sector_count: 1, + public_flow_count: 1, + replay_partition_count: 0, + coupling_limit_count: 0, + parameter_projection_count: 0, + process_support_mask_id: 0, + }]; + process.external_legs[0].source_state_range = CheckedTableRange::new(0, 1); + process.external_legs[1].source_state_range = CheckedTableRange::new(1, 1); + process.source_states = vec![ + ProcessSourceStateRow { + source_slot: 0, + state_index: 0, + public_helicity: -1, + chirality: -1, + spin_state: -1, + current_state_template_id: 0, + source_template_id: 0, + momentum_sign: 1, + crossing_phase_factor_id: 0, + }, + ProcessSourceStateRow { + source_slot: 1, + state_index: 0, + public_helicity: 1, + chirality: 1, + spin_state: 1, + current_state_template_id: 0, + source_template_id: 0, + momentum_sign: 1, + crossing_phase_factor_id: 0, + }, + ]; + process.public_lc_flows = vec![ProcessPublicLCFlowRow { + flow_id: 0, + public_id_string_id: 0, + construction_sector_id: 0, + word_sequence_id: 0, + source_slot_permutation_sequence_id: 0, + reduction_weight_factor_id: 0, + }]; + process.u32_sequence_ranges = vec![CheckedTableRange::new(0, 2)]; + process.u32_sequence_values = vec![0, 1]; + let catalog = ProcessCatalog::new(&process).unwrap(); + + assert!( + build_replay_targets(RecurrenceStrategy::ContractedColorUnion, &process, &catalog,) + .unwrap() + .is_empty() + ); + } + + #[test] + fn contracted_closure_index_preserves_open_line_canonical_owner() { + let mut process = scalar_reference_process(4); + process.lc_open_strings = vec![ + ProcessLCOpenStringRow { + sector_id: 0, + ordinal: 0, + fundamental_source_slot: 0, + antifundamental_source_slot: 1, + adjoint_sequence_id: 0, + singlet_sequence_id: 0, + }, + ProcessLCOpenStringRow { + sector_id: 0, + ordinal: 1, + fundamental_source_slot: 2, + antifundamental_source_slot: 3, + adjoint_sequence_id: 0, + singlet_sequence_id: 0, + }, + ProcessLCOpenStringRow { + sector_id: 1, + ordinal: 0, + fundamental_source_slot: 2, + antifundamental_source_slot: 3, + adjoint_sequence_id: 0, + singlet_sequence_id: 0, + }, + ProcessLCOpenStringRow { + sector_id: 1, + ordinal: 1, + fundamental_source_slot: 0, + antifundamental_source_slot: 1, + adjoint_sequence_id: 0, + singlet_sequence_id: 0, + }, + ]; + let seed = process.physical_lc_sectors[0]; + process.physical_lc_sectors = vec![ + ProcessPhysicalLCSectorRow { + kind: ProcessLCSectorKind::OpenLines as u8, + closure_source_slot: 1, + open_string_range: CheckedTableRange::new(0, 2), + ..seed + }, + ProcessPhysicalLCSectorRow { + sector_id: 1, + kind: ProcessLCSectorKind::OpenLines as u8, + closure_source_slot: 1, + open_string_range: CheckedTableRange::new(2, 2), + ..seed + }, + ]; + let catalog = ProcessCatalog::new(&process).unwrap(); + let sector_ids = BTreeSet::from([0, 1]); + let contracted = PreparedClosureSectorCatalog::new( + RecurrenceStrategy::ContractedColorUnion, + &process, + &catalog, + §or_ids, + ) + .unwrap(); + let first = contracted.get(0).unwrap(); + let alias = contracted.get(1).unwrap(); + assert!(first.contracted_color_canonical_owner); + assert!(!alias.contracted_color_canonical_owner); + assert!(unordered_color_components_match( + &first.expected_components, + &alias.expected_components, + )); + assert!( + closed_components_match_prepared_sector( + RecurrenceStrategy::ContractedColorUnion, + &first.expected_components, + first, + ) + .unwrap() + ); + assert!( + !closed_components_match_prepared_sector( + RecurrenceStrategy::ContractedColorUnion, + &alias.expected_components, + alias, + ) + .unwrap() + ); + assert_eq!(first.anchor_support.as_ref(), [1]); + assert_eq!(first.complement_support.as_ref(), [0, 2, 3]); + let buckets = contracted_closure_support_buckets( + RecurrenceStrategy::ContractedColorUnion, + &contracted, + §or_ids, + ) + .unwrap(); + let bucket = buckets.values().next().unwrap(); + assert_eq!(buckets.len(), 1); + assert_eq!(bucket.sector_ids, [0, 1]); + assert_eq!(bucket.matching_sector_ids(&first.expected_components), [0]); + assert_eq!(bucket.matching_sector_ids(&alias.expected_components), [0]); + + // Canonical ownership is established against the complete process + // catalog, not merely the materialized subset. An alias-only replay + // therefore remains suppressed instead of becoming a local fallback. + let alias_only_ids = BTreeSet::from([1]); + let alias_only = PreparedClosureSectorCatalog::new( + RecurrenceStrategy::ContractedColorUnion, + &process, + &catalog, + &alias_only_ids, + ) + .unwrap(); + let alias_bucket = contracted_closure_support_buckets( + RecurrenceStrategy::ContractedColorUnion, + &alias_only, + &alias_only_ids, + ) + .unwrap() + .into_values() + .next() + .unwrap(); + assert!( + alias_bucket + .matching_sector_ids(&alias.expected_components) + .is_empty() + ); + + let topology = PreparedClosureSectorCatalog::new( + RecurrenceStrategy::TopologyReplay, + &process, + &catalog, + §or_ids, + ) + .unwrap(); + assert!(topology.get(0).unwrap().contracted_color_canonical_owner); + assert!(topology.get(1).unwrap().contracted_color_canonical_owner); + + let all_flow = PreparedClosureSectorCatalog::new( + RecurrenceStrategy::AllFlowUnion, + &process, + &catalog, + §or_ids, + ) + .unwrap(); + let all_flow_bucket = contracted_closure_support_buckets( + RecurrenceStrategy::AllFlowUnion, + &all_flow, + §or_ids, + ) + .unwrap() + .into_values() + .next() + .unwrap(); + assert_eq!( + all_flow_bucket.matching_sector_ids(&first.expected_components), + [0, 1] + ); + } + + #[test] + fn precomputed_three_line_components_preserve_direct_and_partner_witness_order() { + let mut process = scalar_reference_process(6); + process.lc_open_strings = (0..3) + .map(|ordinal| ProcessLCOpenStringRow { + sector_id: 0, + ordinal, + fundamental_source_slot: ordinal * 2, + antifundamental_source_slot: ordinal * 2 + 1, + adjoint_sequence_id: 0, + singlet_sequence_id: 0, + }) + .collect(); + process.u32_sequence_ranges = + vec![CheckedTableRange::new(0, 0), CheckedTableRange::new(0, 6)]; + process.u32_sequence_values = vec![0, 1, 2, 3, 4, 5]; + process.physical_lc_sectors[0] = ProcessPhysicalLCSectorRow { + kind: ProcessLCSectorKind::OpenLines as u8, + closure_source_slot: 5, + open_string_range: CheckedTableRange::new(0, 3), + word_sequence_id: 1, + ..process.physical_lc_sectors[0] + }; + let catalog = ProcessCatalog::new(&process).unwrap(); + let prepared = PreparedClosureSectorCatalog::new( + RecurrenceStrategy::TopologyReplay, + &process, + &catalog, + &BTreeSet::from([0]), + ) + .unwrap(); + let sector = prepared.get(0).unwrap(); + let direct = three_line_traversal_certificate( + §or.expected_components, + sector.row, + §or.expected_components, + &catalog, + Some(17), + ) + .unwrap() + .unwrap(); + assert_eq!(u32::from(direct.kind), THREE_LINE_DIRECT_CERTIFICATE_ID); + assert_eq!(direct.pairing_rule_id, 17); + assert_eq!(direct.reference_block_order.as_ref(), [0, 1, 2]); + assert_eq!(direct.witness_block_order.as_ref(), [0, 1, 2]); + + let partner_components = vec![ + sector.expected_components[1].clone(), + sector.expected_components[0].clone(), + sector.expected_components[2].clone(), + ]; + let partner = three_line_traversal_certificate( + &partner_components, + sector.row, + §or.expected_components, + &catalog, + Some(17), + ) + .unwrap() + .unwrap(); + assert_eq!(u32::from(partner.kind), THREE_LINE_PARTNER_CERTIFICATE_ID); + assert_eq!(partner.reference_block_order.as_ref(), [0, 1, 2]); + assert_eq!(partner.witness_block_order.as_ref(), [1, 0, 2]); + assert_ne!(direct.proof_digest, partner.proof_digest); + } + + #[test] + fn multi_realized_closure_selects_semantic_pairing_owner_independent_of_rule_id() { + let later = [ + FermionPairingEndpointPairRow { + fundamental_source_slot: 0, + antifundamental_source_slot: 7, + }, + FermionPairingEndpointPairRow { + fundamental_source_slot: 2, + antifundamental_source_slot: 5, + }, + ]; + let canonical = [ + FermionPairingEndpointPairRow { + fundamental_source_slot: 0, + antifundamental_source_slot: 3, + }, + FermionPairingEndpointPairRow { + fundamental_source_slot: 2, + antifundamental_source_slot: 7, + }, + ]; + + for candidates in [ + vec![Ok((1, later.as_slice())), Ok((9, canonical.as_slice()))], + vec![Ok((9, canonical.as_slice())), Ok((1, later.as_slice()))], + ] { + assert_eq!(canonical_pairing_owner_id(candidates).unwrap(), Some(9)); + } + assert_eq!( + canonical_pairing_owner_id([Ok((4, later.as_slice()))]).unwrap(), + Some(4) + ); + assert_eq!( + canonical_pairing_owner_id(std::iter::empty()).unwrap(), + None + ); + } + + fn scalar_reference_program( + external_count: usize, + ) -> ( + RecurrenceProgram, + Vec, + (usize, usize, usize), + RecurrenceGenerationTelemetry, + ) { + let template = scalar_reference_template(); + let template_catalog = TemplateCatalog::new(&template).unwrap(); + let prepared_transitions = + PreparedTransitionCatalog::new(&template, &template_catalog).unwrap(); + let prepared_closures = PreparedClosureCatalog::new(&template, &template_catalog).unwrap(); + let process = scalar_reference_process(external_count); + let process_catalog = ProcessCatalog::new(&process).unwrap(); + let fermion_ordering = + established_fermion_ordering_context(&process, &template, None).unwrap(); + let materialized_sectors = BTreeSet::from([0]); + let prepared_closure_sectors = PreparedClosureSectorCatalog::new( + RecurrenceStrategy::AllFlowUnion, + &process, + &process_catalog, + &materialized_sectors, + ) + .unwrap(); + let color_targets = + MaterializedColorTargets::new(&materialized_sectors, &process, &process_catalog) + .unwrap(); + let mut color_states = DynamicLCColorStateInterner::default(); + let color_id = color_states + .intern(DynamicLCColorState::new(0, None, vec![]).unwrap()) + .unwrap(); + let mut currents = Vec::new(); + let mut current_ids = TransientCurrentIdIndex::new(); + let mut currents_by_size = vec![Vec::new(); external_count]; + for slot in 0..external_count as u32 { + let key = CurrentCoreKey::new( + template.catalog_digest, + RecurrenceNodeKind::Source, + 0, + color_id, + vec![slot], + CanonicalMomentumLinearForm::new(vec![MomentumTerm { + source_slot: slot, + coefficient: 1, + }]) + .unwrap(), + CurrentHelicityIdentity::all_flow_union(0), + vec![0], + 0, + vec![], + CurrentSourceBinding::runtime_dispatch(slot, vec![slot]).unwrap(), + None, + ) + .unwrap(); + let id = currents.len() as u32; + assert!(current_ids.insert(key.clone(), id).is_none()); + currents.push(PendingCurrent { + key, + source_exact_factor: None, + contributions: BTreeMap::new(), + realized_pairing_rule_ids: BTreeSet::new(), + reflection: CurrentReflection::Unavailable, + reflection_certificate_id: None, + }); + currents_by_size[0].push(id); + } + + let mut reflection_certificates = Vec::new(); + let mut resident_contribution_count = 0; + let mut generation_telemetry = RecurrenceGenerationTelemetry::default(); + let structural_feasibility = + StructuralFeasibilityIndex::new(external_count, &prepared_transitions, ¤ts) + .unwrap(); + let mut current_support_keys = + TransientCurrentSupportKeys::from_currents(external_count, ¤ts).unwrap(); + let structural_demands = StructuralDemandIndex::new( + &process, + &template, + &template_catalog, + &prepared_transitions, + &structural_feasibility, + &BTreeSet::from([0]), + ) + .unwrap(); + let stage_diagnostics = build_internal_currents( + template.catalog_digest, + &process, + None, + &template, + &fermion_ordering, + &prepared_transitions, + &TransitionReflectionIndex::new(&template, &template_catalog).unwrap(), + &[], + &BTreeMap::new(), + &color_targets, + &structural_demands, + &mut color_states, + &mut currents, + &mut current_ids, + &mut currents_by_size, + &mut current_support_keys, + &mut reflection_certificates, + &mut resident_contribution_count, + 0, + external_count.saturating_sub(2), + external_count.saturating_add(1), + &mut generation_telemetry, + true, + &mut |_| Ok(()), + ) + .unwrap(); + let closures = build_closures( + RecurrenceStrategy::AllFlowUnion, + &process, + &process_catalog, + &template, + &fermion_ordering, + None, + &prepared_closures, + &prepared_closure_sectors, + &color_states, + ¤ts, + &materialized_sectors, + &stage_diagnostics, + &reflection_certificates, + None, + &mut generation_telemetry, + true, + ) + .unwrap(); + let constructed_counts = ( + currents.len(), + currents + .iter() + .map(|current| current.contributions.len()) + .sum(), + closures.len(), + ); + assert_eq!(resident_contribution_count, constructed_counts.1); + let program = finish_program( + RecurrenceStrategy::AllFlowUnion, + &process_catalog, + color_states.into_states(), + currents, + closures.groups, + closures.physical_sector_ids, + closures.physical_sector_owner_ids, + vec![], + 1, + HelicitySupportRule::None, + GlobalHelicityFlipRule::None, + reflection_certificates, + ) + .unwrap(); + ( + program, + stage_diagnostics, + constructed_counts, + generation_telemetry, + ) + } + + #[test] + fn materialized_color_targets_keep_only_embeddable_ordered_words() { + let target_open = + LCColorComponent::new(LCColorComponentKind::OpenString, vec![0, 2, 3, 4, 1]).unwrap(); + let targets = MaterializedColorTargets::from_sectors(vec![vec![target_open]]).unwrap(); + let state = |slots: Vec| { + DynamicLCColorState::new( + 0, + Some(0), + vec![LCColorComponent::new(LCColorComponentKind::AdjointSegment, slots).unwrap()], + ) + .unwrap() + }; + let adjoint_state = |slots: Vec| { + DynamicLCColorState::new_port_wired( + 0, + vec![ + LCColorPortBinding::new(0, LCColorEndpoint::Back), + LCColorPortBinding::new(0, LCColorEndpoint::Front), + ], + vec![LCColorComponent::new(LCColorComponentKind::AdjointSegment, slots).unwrap()], + ) + .unwrap() + }; + + assert!(targets.accepts(&state(vec![2, 3, 4]))); + assert!(!targets.accepts(&state(vec![4, 3, 2]))); + assert!( + targets + .accepts_up_to_reflection(&adjoint_state(vec![4, 3, 2])) + .unwrap() + ); + assert!(!targets.accepts(&state(vec![2, 4]))); + assert!( + !targets + .accepts_up_to_reflection(&state(vec![2, 4])) + .unwrap() + ); + assert!(!targets.accepts(&state(vec![0, 3]))); + assert!(targets.accepts(&DynamicLCColorState::new(0, None, vec![]).unwrap())); + } + + #[test] + fn materialized_color_targets_accept_cyclic_trace_segments() { + let target_trace = + LCColorComponent::new(LCColorComponentKind::Trace, vec![1, 2, 3, 4]).unwrap(); + let targets = MaterializedColorTargets::from_sectors(vec![vec![target_trace]]).unwrap(); + let state = |kind, slots: Vec| { + DynamicLCColorState::new( + 0, + (kind != LCColorComponentKind::Trace).then_some(0), + vec![LCColorComponent::new(kind, slots).unwrap()], + ) + .unwrap() + }; + + assert!(targets.accepts(&state(LCColorComponentKind::AdjointSegment, vec![4, 1, 2],))); + assert!(!targets.accepts(&state(LCColorComponentKind::AdjointSegment, vec![2, 1, 4],))); + assert!(!targets.accepts(&state(LCColorComponentKind::AdjointSegment, vec![1, 3],))); + assert!(!targets.accepts(&state(LCColorComponentKind::Trace, vec![4, 3, 2, 1],))); + assert!(!targets.accepts(&state(LCColorComponentKind::Trace, vec![1, 2, 3],))); + } + + fn reference_color_targets_accept( + sectors: &[Vec], + state: &DynamicLCColorState, + ) -> bool { + if state.components().is_empty() { + return true; + } + sectors.iter().any(|sector| { + state.components().iter().all(|partial| { + sector + .iter() + .any(|target| component_can_embed(partial, target)) + }) + }) + } + + fn unique_source_words(source_slots: &[u32]) -> Vec> { + fn extend( + source_slots: &[u32], + used: &mut [bool], + prefix: &mut Vec, + result: &mut Vec>, + ) { + for index in 0..source_slots.len() { + if used[index] { + continue; + } + used[index] = true; + prefix.push(source_slots[index]); + result.push(prefix.clone()); + extend(source_slots, used, prefix, result); + prefix.pop(); + used[index] = false; + } + } + + let mut result = Vec::new(); + extend( + source_slots, + &mut vec![false; source_slots.len()], + &mut Vec::new(), + &mut result, + ); + result + } + + fn exhaustive_color_components(source_slots: &[u32]) -> Vec { + let mut result = BTreeSet::new(); + for kind in [ + LCColorComponentKind::OpenString, + LCColorComponentKind::AdjointSegment, + LCColorComponentKind::Trace, + ] { + for word in unique_source_words(source_slots) { + result.insert(LCColorComponent::new(kind, word).unwrap()); + } + } + result.into_iter().collect() + } + + fn components_are_disjoint(left: &LCColorComponent, right: &LCColorComponent) -> bool { + left.source_slots() + .iter() + .all(|slot| !right.source_slots().contains(slot)) + } + + #[test] + fn indexed_color_targets_match_exhaustive_single_component_reference() { + let components = exhaustive_color_components(&[0, 1, 2, 3]); + for target in &components { + let sectors = vec![vec![target.clone()]]; + let targets = MaterializedColorTargets::from_sectors(sectors.clone()).unwrap(); + for partial in &components { + let state = DynamicLCColorState::new(0, None, vec![partial.clone()]).unwrap(); + assert_eq!( + targets.accepts(&state), + reference_color_targets_accept(§ors, &state), + "partial {partial:?}, target {target:?}", + ); + } + } + } + + #[test] + fn indexed_color_targets_match_exhaustive_sector_intersection_reference() { + let sectors = vec![ + vec![ + LCColorComponent::new(LCColorComponentKind::OpenString, vec![0, 1]).unwrap(), + LCColorComponent::new(LCColorComponentKind::Trace, vec![2, 3]).unwrap(), + ], + vec![ + LCColorComponent::new(LCColorComponentKind::OpenString, vec![0, 2]).unwrap(), + LCColorComponent::new(LCColorComponentKind::AdjointSegment, vec![1, 3]).unwrap(), + ], + vec![ + LCColorComponent::new(LCColorComponentKind::Trace, vec![0, 3]).unwrap(), + LCColorComponent::new(LCColorComponentKind::OpenString, vec![1, 2]).unwrap(), + ], + ]; + let targets = MaterializedColorTargets::from_sectors(sectors.clone()).unwrap(); + let components = exhaustive_color_components(&[0, 1, 2, 3]); + let empty = DynamicLCColorState::new(0, None, vec![]).unwrap(); + assert_eq!( + targets.accepts(&empty), + reference_color_targets_accept(§ors, &empty) + ); + for left in &components { + let state = DynamicLCColorState::new(0, None, vec![left.clone()]).unwrap(); + assert_eq!( + targets.accepts(&state), + reference_color_targets_accept(§ors, &state), + "single partial {left:?}", + ); + for right in &components { + if !components_are_disjoint(left, right) { + continue; + } + let state = + DynamicLCColorState::new(0, None, vec![left.clone(), right.clone()]).unwrap(); + assert_eq!( + targets.accepts(&state), + reference_color_targets_accept(§ors, &state), + "partial forest [{left:?}, {right:?}]", + ); + } + } + + let split_sectors = vec![ + vec![LCColorComponent::new(LCColorComponentKind::OpenString, vec![0, 1]).unwrap()], + vec![LCColorComponent::new(LCColorComponentKind::OpenString, vec![2, 3]).unwrap()], + ]; + let split_targets = MaterializedColorTargets::from_sectors(split_sectors.clone()).unwrap(); + let split_state = DynamicLCColorState::new( + 0, + None, + vec![ + LCColorComponent::new(LCColorComponentKind::AdjointSegment, vec![0, 1]).unwrap(), + LCColorComponent::new(LCColorComponentKind::AdjointSegment, vec![2, 3]).unwrap(), + ], + ) + .unwrap(); + assert!(!reference_color_targets_accept( + &split_sectors, + &split_state + )); + assert!(!split_targets.accepts(&split_state)); + } + + #[test] + fn indexed_color_target_postings_span_multiple_words() { + let sectors = (0..130_u32) + .map(|source_slot| { + vec![ + LCColorComponent::new(LCColorComponentKind::OpenString, vec![source_slot]) + .unwrap(), + ] + }) + .collect::>(); + let targets = MaterializedColorTargets::from_sectors(sectors.clone()).unwrap(); + for source_slot in [0, 63, 64, 127, 129, 130] { + let state = DynamicLCColorState::new( + 0, + None, + vec![ + LCColorComponent::new(LCColorComponentKind::AdjointSegment, vec![source_slot]) + .unwrap(), + ], + ) + .unwrap(); + assert_eq!( + targets.accepts(&state), + reference_color_targets_accept(§ors, &state), + "source slot {source_slot}", + ); + } + } + + #[test] + fn sector_postings_use_dense_storage_only_when_strictly_cheaper() { + let sparse_on_tie = SectorPosting::from_sorted_unique_sector_ids((0..6_u32).collect(), 130); + assert!(sparse_on_tie.is_sparse()); + assert_eq!(sparse_on_tie.cardinality(), 6); + assert_eq!(sparse_on_tie.payload_bytes(), 24); + + let dense = SectorPosting::from_sorted_unique_sector_ids((0..7_u32).collect(), 130); + assert!(!dense.is_sparse()); + assert_eq!(dense.cardinality(), 7); + assert_eq!(dense.payload_bytes(), 24); + assert!(dense.contains(0)); + assert!(dense.contains(6)); + assert!(!dense.contains(7)); + assert!(!dense.contains(130)); + } + + #[test] + fn adaptive_color_postings_match_reference_and_account_exact_storage() { + let component = |source_slot| { + LCColorComponent::new(LCColorComponentKind::OpenString, vec![source_slot]).unwrap() + }; + let partial_state = |source_slots: &[u32]| { + DynamicLCColorState::new( + 0, + None, + source_slots + .iter() + .copied() + .map(|source_slot| { + LCColorComponent::new( + LCColorComponentKind::AdjointSegment, + vec![source_slot], + ) + .unwrap() + }) + .collect(), + ) + .unwrap() + }; + + let mut sectors = vec![Vec::new(); 256]; + for sector in &mut sectors[..128] { + sector.push(component(2_000)); + } + sectors[5].push(component(1_000)); + sectors[5].push(component(1_000)); + sectors[200].push(component(1_000)); + sectors[200].push(component(3_000)); + + let targets = + MaterializedColorTargets::from_sectors_with_telemetry(sectors.clone(), true).unwrap(); + let storage = targets.telemetry().unwrap(); + assert_eq!(storage.fragment_bucket_count, 3); + assert_eq!(storage.posting_incidence_count, 131); + assert_eq!(storage.sparse_posting_bucket_count, 2); + assert_eq!(storage.dense_posting_bucket_count, 1); + assert_eq!(storage.sparse_posting_bytes, 12); + assert_eq!(storage.dense_posting_bytes, 32); + let dense_only_baseline_bytes = storage + .fragment_bucket_count + .checked_mul(256_usize.div_ceil(u64::BITS as usize)) + .and_then(|words| words.checked_mul(std::mem::size_of::())) + .unwrap(); + assert_eq!(dense_only_baseline_bytes, 96); + assert_eq!( + storage + .sparse_posting_bytes + .checked_add(storage.dense_posting_bytes), + Some(44), + ); + + for source_slots in [ + &[][..], + &[1_000][..], + &[2_000][..], + &[3_000][..], + &[1_000, 2_000][..], + &[1_000, 3_000][..], + &[1_000, 2_000, 3_000][..], + &[4_000][..], + ] { + let state = partial_state(source_slots); + assert_eq!( + targets.accepts(&state), + reference_color_targets_accept(§ors, &state), + "partial source slots {source_slots:?}", + ); + } + + let memo_targets = + MaterializedColorTargets::from_sectors_with_telemetry(sectors.clone(), true).unwrap(); + let accepted = partial_state(&[1_000, 2_000]); + let rejected = partial_state(&[1_000, 2_000, 3_000]); + assert!(memo_targets.accepts(&accepted)); + assert_eq!(memo_targets.accepted_component_forests.borrow().len(), 1); + assert!(memo_targets.accepts(&accepted)); + assert_eq!(memo_targets.accepted_component_forests.borrow().len(), 1); + assert!(!memo_targets.accepts(&rejected)); + assert!(!memo_targets.accepts(&rejected)); + assert_eq!(memo_targets.accepted_component_forests.borrow().len(), 1); + let memo = memo_targets.telemetry().unwrap(); + assert_eq!(memo.acceptance_cache_hit_count, 1); + assert_eq!(memo.acceptance_cache_miss_count, 3); + assert_eq!(memo.acceptance_accept_count, 2); + assert_eq!(memo.acceptance_reject_count, 2); + } + + #[test] + fn all_dense_color_postings_match_exact_wordwise_intersection_reference() { + let component = |source_slot| { + LCColorComponent::new(LCColorComponentKind::OpenString, vec![source_slot]).unwrap() + }; + let mut sectors = vec![Vec::new(); 256]; + for sector in sectors.iter_mut().take(192) { + sector.push(component(10)); + } + for sector in sectors.iter_mut().take(256).skip(64) { + sector.push(component(20)); + } + for sector in sectors.iter_mut().take(256).step_by(2) { + sector.push(component(30)); + } + for sector in &mut sectors[192..] { + sector.push(component(40)); + } + let targets = MaterializedColorTargets::from_sectors(sectors.clone()).unwrap(); + assert!( + targets + .non_trace_fragment_sectors + .values() + .all(|posting| !posting.is_sparse()) + ); + + for source_slots in [ + &[10, 20][..], + &[10, 30][..], + &[20, 30][..], + &[10, 20, 30][..], + &[10, 40][..], + &[20, 40][..], + ] { + let state = DynamicLCColorState::new( + 0, + None, + source_slots + .iter() + .map(|source_slot| { + LCColorComponent::new( + LCColorComponentKind::AdjointSegment, + vec![*source_slot], + ) + .unwrap() + }) + .collect(), + ) + .unwrap(); + assert_eq!( + targets.accepts(&state), + reference_color_targets_accept(§ors, &state), + "all-dense partial source slots {source_slots:?}", + ); + } + } + + #[test] + fn indexed_color_targets_match_exhaustive_reflection_reference() { + let sectors = vec![ + vec![ + LCColorComponent::new(LCColorComponentKind::OpenString, vec![0, 1, 2, 3]).unwrap(), + ], + vec![LCColorComponent::new(LCColorComponentKind::Trace, vec![0, 2, 3, 1]).unwrap()], + ]; + let targets = MaterializedColorTargets::from_sectors(sectors.clone()).unwrap(); + let mut accepted = 0usize; + let mut rejected = 0usize; + for word in unique_source_words(&[0, 1, 2, 3]) { + let state = DynamicLCColorState::new_port_wired( + 0, + vec![ + LCColorPortBinding::new(0, LCColorEndpoint::Back), + LCColorPortBinding::new(0, LCColorEndpoint::Front), + ], + vec![LCColorComponent::new(LCColorComponentKind::AdjointSegment, word).unwrap()], + ) + .unwrap(); + let expected = reference_color_targets_accept(§ors, &state) + || reference_color_targets_accept(§ors, &state.reversed().unwrap()); + assert_eq!( + targets.accepts_up_to_reflection(&state).unwrap(), + expected, + "pure-adjoint state {state:?}", + ); + if expected { + accepted += 1; + } else { + rejected += 1; + } + } + assert!(accepted > 0); + assert!(rejected > 0); + } + + #[test] + fn color_target_memo_retains_only_accepted_forests_without_changing_reflection_eligibility() { + let targets = MaterializedColorTargets::from_sectors(vec![vec![ + LCColorComponent::new(LCColorComponentKind::OpenString, vec![0, 1, 2]).unwrap(), + ]]) + .unwrap(); + let component = + LCColorComponent::new(LCColorComponentKind::AdjointSegment, vec![2, 1]).unwrap(); + let non_reflectable = + DynamicLCColorState::new(0, Some(0), vec![component.clone()]).unwrap(); + assert!(!targets.accepts_up_to_reflection(&non_reflectable).unwrap()); + assert!(targets.accepted_component_forests.borrow().is_empty()); + assert!(!targets.accepts_up_to_reflection(&non_reflectable).unwrap()); + assert!(targets.accepted_component_forests.borrow().is_empty()); + + let reflectable = DynamicLCColorState::new_port_wired( + 99, + vec![ + LCColorPortBinding::new(0, LCColorEndpoint::Back), + LCColorPortBinding::new(0, LCColorEndpoint::Front), + ], + vec![component], + ) + .unwrap(); + assert!(targets.accepts_up_to_reflection(&reflectable).unwrap()); + assert_eq!(targets.accepted_component_forests.borrow().len(), 1); + assert!(targets.accepts_up_to_reflection(&reflectable).unwrap()); + assert_eq!(targets.accepted_component_forests.borrow().len(), 1); + } + + #[test] + fn replay_construction_isolates_authenticated_representatives() { + let replay_targets = [ + RecurrenceReplayTarget::new(0, 4, 0, vec![0, 1], vec![1, 1], ExactComplexRational::ONE) + .unwrap(), + RecurrenceReplayTarget::new(1, 9, 1, vec![1, 0], vec![1, 1], ExactComplexRational::ONE) + .unwrap(), + ]; + let sectors = BTreeSet::from([4, 9]); + assert_eq!( + construction_sector_groups( + RecurrenceStrategy::ContractedColorUnion, + §ors, + &replay_targets, + ), + [BTreeSet::from([4]), BTreeSet::from([9])] + ); + assert_eq!( + construction_sector_groups(RecurrenceStrategy::AllFlowUnion, §ors, &[]), + std::slice::from_ref(§ors) + ); + + let first_lane = PendingConstructionDomain { + shared_source_end: 4, + lane_internal_start: 4, + lane_internal_end: 10, + }; + assert!(first_lane.contains(0)); + assert!(first_lane.contains(9)); + assert!(!first_lane.contains(10)); + } + + #[test] + fn scalar_reference_fixtures_keep_structural_schedule_counts() { + // Closure-rooted admission materializes no current or contribution + // outside the final exact schedule for this scalar grammar. + for (name, external_count, expected_constructed, expected_schedule) in [ + ("three-point", 3, (4, 1, 1), (4, 1, 1)), + ("four-point", 4, (8, 6, 1), (8, 6, 1)), + ] { + let (program, _, constructed_counts, _) = scalar_reference_program(external_count); + assert_eq!( + constructed_counts, expected_constructed, + "{name} scalar construction fixture", + ); + assert_eq!( + ( + program.currents().len(), + program.contributions().len(), + program.closure_terms().len(), + ), + expected_schedule, + "{name} scalar reference fixture", + ); + } + } + + #[test] + fn scalar_reference_fixture_reports_streaming_selectivity() { + let (_, diagnostics, _, _) = scalar_reference_program(4); + assert_eq!(diagnostics.len(), 2); + assert_eq!( + diagnostics + .iter() + .map(|stage| ( + stage.target_size, + stage.candidate_parent_pair_count, + stage.parent_pair_count, + stage.transition_index_hit_count, + stage.transition_candidate_count, + stage.contribution_attempt_count, + )) + .collect::>(), + [(2, 6, 6, 6, 6, 3), (3, 12, 6, 6, 6, 3)], + ); + } + + #[test] + fn scalar_reference_fixture_reports_exact_closure_index_counters() { + let (_, _, _, telemetry) = scalar_reference_program(4); + assert_eq!(telemetry.closure_support_lookup_count, 2); + assert_eq!(telemetry.closure_candidate_theoretical_count, 1); + assert_eq!(telemetry.closure_candidate_count, 1); + assert_eq!(telemetry.closure_state_match_count, 1); + assert_eq!(telemetry.closure_color_attempt_count, 1); + assert_eq!(telemetry.closure_group_count, 1); + assert_eq!(telemetry.closure_proof_contribution_count, 1); + } + + #[test] + fn contracted_closure_buckets_match_legacy_rows_and_collapse_executed_attempts() { + let template = scalar_reference_template(); + let template_catalog = TemplateCatalog::new(&template).unwrap(); + let prepared_closures = PreparedClosureCatalog::new(&template, &template_catalog).unwrap(); + let mut process = scalar_reference_process(2); + process + .physical_lc_sectors + .push(ProcessPhysicalLCSectorRow { + sector_id: 1, + ..process.physical_lc_sectors[0] + }); + let process_catalog = ProcessCatalog::new(&process).unwrap(); + let materialized_sectors = BTreeSet::from([0, 1]); + let prepared_sectors = PreparedClosureSectorCatalog::new( + RecurrenceStrategy::ContractedColorUnion, + &process, + &process_catalog, + &materialized_sectors, + ) + .unwrap(); + let fermion_ordering = + established_fermion_ordering_context(&process, &template, None).unwrap(); + let mut color_states = DynamicLCColorStateInterner::default(); + let color_id = color_states + .intern(DynamicLCColorState::new(0, None, vec![]).unwrap()) + .unwrap(); + let currents = (0..2_u32) + .map(|source_slot| { + let key = CurrentCoreKey::new( + template.catalog_digest, + RecurrenceNodeKind::Source, + 0, + color_id, + vec![source_slot], + CanonicalMomentumLinearForm::new(vec![MomentumTerm { + source_slot, + coefficient: 1, + }]) + .unwrap(), + CurrentHelicityIdentity::contracted_color_union( + 0, + vec![SourceStateAssignment::new(source_slot, 0)], + ) + .unwrap(), + vec![0], + 0, + vec![], + CurrentSourceBinding::FixedTemplate(source_slot), + None, + ) + .unwrap(); + PendingCurrent { + key, + source_exact_factor: Some(ExactComplexRational::ONE), + contributions: BTreeMap::new(), + realized_pairing_rule_ids: BTreeSet::new(), + reflection: CurrentReflection::Unavailable, + reflection_certificate_id: None, + } + }) + .collect::>(); + + let mut bucketed_telemetry = RecurrenceGenerationTelemetry::default(); + let bucketed = build_closures( + RecurrenceStrategy::ContractedColorUnion, + &process, + &process_catalog, + &template, + &fermion_ordering, + None, + &prepared_closures, + &prepared_sectors, + &color_states, + ¤ts, + &materialized_sectors, + &[], + &[], + None, + &mut bucketed_telemetry, + true, + ) + .unwrap(); + let mut legacy_telemetry = RecurrenceGenerationTelemetry::default(); + let legacy = build_closures_per_sector( + RecurrenceStrategy::ContractedColorUnion, + &process, + &process_catalog, + &template, + &fermion_ordering, + None, + &prepared_closures, + &prepared_sectors, + &color_states, + ¤ts, + &materialized_sectors, + &[], + &[], + None, + &mut legacy_telemetry, + true, + ) + .unwrap(); + + // PendingClosureGroup equality covers key identity, aggregate exact + // factors, and every proof contribution field in vector order. + assert_eq!(bucketed, legacy); + assert_eq!( + bucketed + .keys() + .map(|key| key.target_sector_id) + .collect::>(), + [0, 1] + ); + assert_eq!(bucketed_telemetry.closure_candidate_theoretical_count, 2); + assert_eq!( + bucketed_telemetry.closure_candidate_theoretical_count, + legacy_telemetry.closure_candidate_theoretical_count + ); + assert_eq!(bucketed_telemetry.closure_support_lookup_count, 2); + assert_eq!(legacy_telemetry.closure_support_lookup_count, 4); + assert_eq!(bucketed_telemetry.closure_candidate_count, 1); + assert_eq!(legacy_telemetry.closure_candidate_count, 2); + assert_eq!(bucketed_telemetry.closure_state_match_count, 1); + assert_eq!(legacy_telemetry.closure_state_match_count, 2); + assert_eq!(bucketed_telemetry.closure_color_attempt_count, 1); + assert_eq!(legacy_telemetry.closure_color_attempt_count, 2); + assert_eq!(bucketed_telemetry.closure_group_count, 2); + assert_eq!(bucketed_telemetry.closure_proof_contribution_count, 2); + } + + #[test] + fn complete_closure_target_domain_certifies_only_an_exhaustive_empty_owner_class() { + let process = scalar_reference_process(2); + let sector_ids = BTreeSet::from([0, 1]); + let owner_ids = BTreeMap::from([(0, 0), (1, 0)]); + let helicity_ids = BTreeMap::from([( + vec![ + SourceStateAssignment::new(0, 0), + SourceStateAssignment::new(1, 0), + ] + .into_boxed_slice(), + 0, + )]); + let pending_closures = BTreeMap::new(); + let certificate = complete_closure_target_domain_certificate( + RecurrenceStrategy::ContractedColorUnion, + &process, + 2, + §or_ids, + &owner_ids, + &[], + &helicity_ids, + &pending_closures, + &[], + ) + .unwrap() + .unwrap(); + + assert_eq!(certificate.physical_sector_count(), 2); + assert_eq!(certificate.resolved_helicity_count(), 1); + assert_eq!( + certificate.certified_structural_zero_physical_sector_ids(), + [0, 1] + ); + assert_eq!( + complete_closure_target_domain_certificate( + RecurrenceStrategy::ContractedColorUnion, + &process, + 2, + §or_ids, + &owner_ids, + &[], + &helicity_ids, + &pending_closures, + &[], + ) + .unwrap() + .unwrap() + .semantic_digest(), + certificate.semantic_digest(), + ); + + let missing = complete_closure_target_domain_certificate( + RecurrenceStrategy::ContractedColorUnion, + &process, + 2, + &BTreeSet::from([0]), + &BTreeMap::from([(0, 0)]), + &[], + &helicity_ids, + &pending_closures, + &[], + ) + .unwrap_err(); + assert!( + missing + .message() + .contains("lacks exhaustive physical-sector coverage") + ); + } + + #[test] + fn complete_closure_target_domain_expands_authenticated_replay_representatives() { + let mut process = scalar_reference_process(2); + process.source_states = vec![ + ProcessSourceStateRow { + source_slot: 0, + state_index: 0, + public_helicity: 1, + chirality: 0, + spin_state: 1, + current_state_template_id: 0, + source_template_id: 0, + momentum_sign: 1, + crossing_phase_factor_id: 0, + }, + ProcessSourceStateRow { + source_slot: 1, + state_index: 0, + public_helicity: 1, + chirality: 0, + spin_state: 1, + current_state_template_id: 0, + source_template_id: 0, + momentum_sign: 1, + crossing_phase_factor_id: 0, + }, + ]; + let helicity_ids = BTreeMap::from([( + vec![ + SourceStateAssignment::new(0, 0), + SourceStateAssignment::new(1, 0), + ] + .into_boxed_slice(), + 0, + )]); + let replay_targets = vec![ + RecurrenceReplayTarget::new(0, 0, 0, vec![0, 1], vec![1, 1], ExactComplexRational::ONE) + .unwrap(), + RecurrenceReplayTarget::new(1, 0, 1, vec![1, 0], vec![1, 1], ExactComplexRational::ONE) + .unwrap(), + ]; + let certificate = complete_closure_target_domain_certificate( + RecurrenceStrategy::ContractedColorUnion, + &process, + 2, + &BTreeSet::from([0]), + &BTreeMap::from([(0, 0)]), + &replay_targets, + &helicity_ids, + &BTreeMap::new(), + &[], + ) + .unwrap() + .unwrap(); + + assert_eq!( + certificate.certified_structural_zero_physical_sector_ids(), + [0, 1] + ); + assert!( + complete_closure_target_domain_certificate( + RecurrenceStrategy::ContractedColorUnion, + &process, + 2, + &BTreeSet::from([0]), + &BTreeMap::from([(0, 0)]), + &replay_targets[..1], + &helicity_ids, + &BTreeMap::new(), + &[], + ) + .unwrap_err() + .message() + .contains("does not cover every physical sector") + ); + } + + #[test] + fn contracted_and_all_flow_closures_preserve_v4_reflection_partner_rows() { + let (mut process, mut template) = cyclic_trace_anchor_contract_fixture(); + let closure_pairings = append_u32_sequence(&mut template, &[0, 0, 1, 1, 0, 1, 1, 0]); + template.lc_color_transition_witnesses[1].input_port_pairing_sequence_id = closure_pairings; + let reflected_word_id = process.u32_sequence_ranges.len() as u32; + process.u32_sequence_ranges.push(CheckedTableRange::new( + process.u32_sequence_values.len() as u64, + 4, + )); + process.u32_sequence_values.extend_from_slice(&[0, 3, 2, 1]); + process + .physical_lc_sectors + .push(ProcessPhysicalLCSectorRow { + sector_id: 1, + trace_sequence_id: reflected_word_id, + word_sequence_id: reflected_word_id, + ..process.physical_lc_sectors[0] + }); + + let template_catalog = TemplateCatalog::new(&template).unwrap(); + let prepared_closures = PreparedClosureCatalog::new(&template, &template_catalog).unwrap(); + let process_catalog = ProcessCatalog::new(&process).unwrap(); + let materialized_sectors = BTreeSet::from([0, 1]); + let prepared_sectors = PreparedClosureSectorCatalog::new( + RecurrenceStrategy::ContractedColorUnion, + &process, + &process_catalog, + &materialized_sectors, + ) + .unwrap(); + let fermion_ordering = + established_fermion_ordering_context(&process, &template, None).unwrap(); + + let adjoint = + |word| LCColorComponent::new(LCColorComponentKind::AdjointSegment, word).unwrap(); + let color = |word| { + DynamicLCColorState::new_port_wired( + 0, + vec![ + LCColorPortBinding::new(0, LCColorEndpoint::Back), + LCColorPortBinding::new(0, LCColorEndpoint::Front), + ], + vec![adjoint(word)], + ) + .unwrap() + }; + let anchor_color = color(vec![0]); + let complement_color = color(vec![1, 2, 3]); + let reflected_color = color(vec![3, 2, 1]); + let minus_one = ExactComplexRational::ONE.checked_neg().unwrap(); + let canonical_proof = CurrentReflectionProof::new( + minus_one, + [digest(180)], + dynamic_color_identity_digest(&complement_color).unwrap(), + ) + .unwrap(); + let reflected_proof = CurrentReflectionProof::new( + minus_one, + [digest(181)], + dynamic_color_identity_digest(&reflected_color).unwrap(), + ) + .unwrap(); + let reflection_certificate = PendingReflectionCertificate::reciprocal_pair( + 0, + 1, + 2, + &canonical_proof, + &reflected_proof, + &complement_color, + &reflected_color, + 4, + ) + .unwrap(); + let mut color_states = DynamicLCColorStateInterner::default(); + let anchor_color_id = color_states.intern(anchor_color).unwrap(); + let complement_color_id = color_states.intern(complement_color).unwrap(); + let current = |strategy, + node_kind, + color_id, + support: &[u32], + reflection, + reflection_certificate_id| { + let helicity_identity = match strategy { + RecurrenceStrategy::AllFlowUnion => CurrentHelicityIdentity::all_flow_union(0), + RecurrenceStrategy::ContractedColorUnion => { + CurrentHelicityIdentity::contracted_color_union( + 0, + support + .iter() + .copied() + .map(|slot| SourceStateAssignment::new(slot, 0)) + .collect(), + ) + .unwrap() + } + RecurrenceStrategy::TopologyReplay => unreachable!("test fixture strategy"), + }; + let source_binding = if node_kind != RecurrenceNodeKind::Source { + CurrentSourceBinding::None + } else if strategy == RecurrenceStrategy::AllFlowUnion { + CurrentSourceBinding::runtime_dispatch(support[0], vec![support[0]]).unwrap() + } else { + CurrentSourceBinding::FixedTemplate(support[0]) + }; + let key = CurrentCoreKey::new( + template.catalog_digest, + node_kind, + 0, + color_id, + support.to_vec(), + CanonicalMomentumLinearForm::new( + support + .iter() + .copied() + .map(|source_slot| MomentumTerm { + source_slot, + coefficient: 1, + }) + .collect(), + ) + .unwrap(), + helicity_identity, + vec![0], + 0, + vec![], + source_binding, + None, + ) + .unwrap(); + PendingCurrent { + key, + source_exact_factor: (node_kind == RecurrenceNodeKind::Source + && strategy != RecurrenceStrategy::AllFlowUnion) + .then_some(ExactComplexRational::ONE), + contributions: BTreeMap::new(), + realized_pairing_rule_ids: BTreeSet::new(), + reflection, + reflection_certificate_id, + } + }; + let currents = vec![ + current( + RecurrenceStrategy::ContractedColorUnion, + RecurrenceNodeKind::Source, + anchor_color_id, + &[0], + CurrentReflection::Unavailable, + None, + ), + current( + RecurrenceStrategy::ContractedColorUnion, + RecurrenceNodeKind::Current, + complement_color_id, + &[1, 2, 3], + CurrentReflection::Proven(canonical_proof.clone()), + Some(0), + ), + ]; + let reflection_certificates = [reflection_certificate]; + + let mut bucketed_telemetry = RecurrenceGenerationTelemetry::default(); + let bucketed = build_closures( + RecurrenceStrategy::ContractedColorUnion, + &process, + &process_catalog, + &template, + &fermion_ordering, + None, + &prepared_closures, + &prepared_sectors, + &color_states, + ¤ts, + &materialized_sectors, + &[], + &reflection_certificates, + None, + &mut bucketed_telemetry, + true, + ) + .unwrap(); + let mut legacy_telemetry = RecurrenceGenerationTelemetry::default(); + let legacy = build_closures_per_sector( + RecurrenceStrategy::ContractedColorUnion, + &process, + &process_catalog, + &template, + &fermion_ordering, + None, + &prepared_closures, + &prepared_sectors, + &color_states, + ¤ts, + &materialized_sectors, + &[], + &reflection_certificates, + None, + &mut legacy_telemetry, + true, + ) + .unwrap(); + + assert_eq!(bucketed, legacy); + assert_eq!(bucketed.len(), 2); + let canonical = bucketed + .iter() + .find(|(key, _)| key.target_sector_id == 0) + .unwrap(); + let reflected = bucketed + .iter() + .find(|(key, _)| key.target_sector_id == 1) + .unwrap(); + assert_eq!(canonical.1.exact_factor, ExactComplexRational::ONE); + assert_eq!(reflected.1.exact_factor, minus_one); + assert_eq!( + reflected.1.contributions[0].reflection_certificate_id, + Some(0) + ); + assert_eq!(bucketed_telemetry.closure_color_attempt_count, 1); + assert_eq!(legacy_telemetry.closure_color_attempt_count, 2); + + // The all-flow lane retains the same canonical complement and emits + // the reverse physical trace through the authenticated reflection + // certificate. Completeness is therefore per physical target, not + // per-sector growth of the shared closure map. + let all_flow_sectors = PreparedClosureSectorCatalog::new( + RecurrenceStrategy::AllFlowUnion, + &process, + &process_catalog, + &materialized_sectors, + ) + .unwrap(); + let all_flow_currents = vec![ + current( + RecurrenceStrategy::AllFlowUnion, + RecurrenceNodeKind::Source, + anchor_color_id, + &[0], + CurrentReflection::Unavailable, + None, + ), + current( + RecurrenceStrategy::AllFlowUnion, + RecurrenceNodeKind::Current, + complement_color_id, + &[1, 2, 3], + CurrentReflection::Proven(canonical_proof), + Some(0), + ), + ]; + let mut all_flow_telemetry = RecurrenceGenerationTelemetry::default(); + let all_flow = build_closures( + RecurrenceStrategy::AllFlowUnion, + &process, + &process_catalog, + &template, + &fermion_ordering, + None, + &prepared_closures, + &all_flow_sectors, + &color_states, + &all_flow_currents, + &materialized_sectors, + &[], + &reflection_certificates, + None, + &mut all_flow_telemetry, + true, + ) + .unwrap(); + let mut all_flow_legacy_telemetry = RecurrenceGenerationTelemetry::default(); + let all_flow_legacy = build_closures_per_sector( + RecurrenceStrategy::AllFlowUnion, + &process, + &process_catalog, + &template, + &fermion_ordering, + None, + &prepared_closures, + &all_flow_sectors, + &color_states, + &all_flow_currents, + &materialized_sectors, + &[], + &reflection_certificates, + None, + &mut all_flow_legacy_telemetry, + true, + ) + .unwrap(); + + assert_eq!(all_flow, all_flow_legacy); + assert_eq!(all_flow.len(), 2); + let canonical = all_flow + .iter() + .find(|(key, _)| key.target_sector_id == 0) + .unwrap(); + let reflected = all_flow + .iter() + .find(|(key, _)| key.target_sector_id == 1) + .unwrap(); + assert_eq!(canonical.1.exact_factor, ExactComplexRational::ONE); + assert_eq!(reflected.1.exact_factor, minus_one); + assert_eq!( + reflected.1.contributions[0].reflection_certificate_id, + Some(0) + ); + assert_eq!(all_flow_telemetry.closure_support_lookup_count, 2); + assert_eq!(all_flow_legacy_telemetry.closure_support_lookup_count, 4); + assert_eq!(all_flow_telemetry.closure_color_attempt_count, 1); + assert_eq!(all_flow_legacy_telemetry.closure_color_attempt_count, 2); + assert_eq!(all_flow_telemetry.closure_group_count, 2); + assert_eq!(all_flow_telemetry.closure_proof_contribution_count, 2); + + let uncertified_currents = vec![ + current( + RecurrenceStrategy::AllFlowUnion, + RecurrenceNodeKind::Source, + anchor_color_id, + &[0], + CurrentReflection::Unavailable, + None, + ), + current( + RecurrenceStrategy::AllFlowUnion, + RecurrenceNodeKind::Current, + complement_color_id, + &[1, 2, 3], + CurrentReflection::Unavailable, + None, + ), + ]; + let error = build_closures( + RecurrenceStrategy::AllFlowUnion, + &process, + &process_catalog, + &template, + &fermion_ordering, + None, + &prepared_closures, + &all_flow_sectors, + &color_states, + &uncertified_currents, + &materialized_sectors, + &[], + &[], + None, + &mut RecurrenceGenerationTelemetry::default(), + false, + ) + .unwrap_err(); + assert!( + error + .message() + .contains("no exact closure for physical LC sector 1") + ); + } + + #[test] + fn accepted_transitions_clone_only_inserted_current_keys() { + let (_, _, _, telemetry) = scalar_reference_program(4); + assert!(telemetry.transition_accept_count > 0); + assert_eq!(telemetry.accepted_parent_key_clone_count, 0); + assert_eq!( + telemetry.current_key_clone_count, + telemetry.current_insert_count, + ); + } + + #[test] + fn non_propagating_states_use_identity_finalization() { + fn propagator(id: u32, state: u32, applies: bool) -> PropagatorRow { + PropagatorRow { + id, + template_string_id: 0, + state_template_id: state, + applies_propagator: u8::from(applies), + evaluator_binding_id: if applies { 0 } else { MISSING_U32 }, + numerator_expression_digest_id: 0, + denominator_expression_digest_id: 0, + mass_parameter_id: MISSING_U32, + width_parameter_id: MISSING_U32, + gauge_string_id: 0, + linearity_proof_template_id: MISSING_U32, + semantic_digest_id: 0, + } + } + + let mut template = scalar_reference_template(); + template.propagators = vec![propagator(0, 4, false), propagator(1, 7, true)]; + assert_eq!( + propagator_by_state(&template).unwrap(), + BTreeMap::from([(4, None), (7, Some(1))]), + ); + } + + #[test] + fn output_factor_uses_the_declared_binding_coupling_component() { + let coupling = ExactComplexRational::new( + ExactRational::new(2, 3).unwrap(), + ExactRational::new(-5, 7).unwrap(), + ); + assert_eq!( + output_factor_from_binding( + coupling, + OutputFactorSource::None as u8, + "test transition", + ) + .unwrap(), + ExactComplexRational::ONE, + ); + assert_eq!( + output_factor_from_binding( + coupling, + OutputFactorSource::CouplingReal as u8, + "test transition", + ) + .unwrap(), + ExactComplexRational::new(ExactRational::new(2, 3).unwrap(), ExactRational::ZERO,), + ); + assert_eq!( + output_factor_from_binding( + coupling, + OutputFactorSource::CouplingImag as u8, + "test transition", + ) + .unwrap(), + ExactComplexRational::new(ExactRational::new(-5, 7).unwrap(), ExactRational::ZERO,), + ); + } + + #[test] + fn prepared_transition_defers_reachable_only_output_factor_rejection() { + let mut template = scalar_reference_template(); + template.transitions[0].output_factor_source = OutputFactorSource::CouplingImag as u8; + let catalog = TemplateCatalog::new(&template).unwrap(); + let prepared = PreparedTransitionCatalog::new(&template, &catalog).unwrap(); + let transition = prepared + .rows_by_state_pair + .values() + .next() + .unwrap() + .first() + .unwrap(); + assert_eq!( + transition.output_factor().unwrap_err(), + output_factor_from_binding( + ExactComplexRational::ONE, + OutputFactorSource::CouplingImag as u8, + "transition", + ) + .unwrap_err() + ); + } + + #[test] + fn composes_construction_and_public_gather_permutations() { + let representative_to_construction = [0, 2, 3, 1]; + let construction_to_public = [0, 3, 1, 2]; + assert_eq!( + compose_gather_permutations(&representative_to_construction, &construction_to_public,) + .unwrap(), + [0, 1, 2, 3] + ); + } + + #[test] + fn replay_momentum_signs_cross_initial_and_final_sources() { + assert_eq!( + replay_momentum_signs(&[-1, -1, 1, 1], &[0, 3, 2, 1]).unwrap(), + [1, -1, 1, -1] + ); + assert_eq!( + replay_momentum_signs(&[-1, -1, 1, 1], &[0, 1, 3, 2]).unwrap(), + [1, 1, 1, 1] + ); + } + + fn projection_test_current( + node_kind: RecurrenceNodeKind, + state: u32, + color: u32, + support: &[u32], + ) -> PendingCurrent { + let helicity_identity = CurrentHelicityIdentity::topology_replay( + 0, + support + .iter() + .copied() + .map(|slot| SourceStateAssignment::new(slot, 0)) + .collect(), + ) + .unwrap(); + let key = CurrentCoreKey::new( + digest(230), + node_kind, + state, + DynamicLCColorStateId::from_interner(color), + support.to_vec(), + CanonicalMomentumLinearForm::new( + support + .iter() + .copied() + .map(|source_slot| MomentumTerm { + source_slot, + coefficient: 1, + }) + .collect(), + ) + .unwrap(), + helicity_identity, + vec![0], + 0, + vec![], + if node_kind == RecurrenceNodeKind::Source { + CurrentSourceBinding::FixedTemplate(support[0]) + } else { + CurrentSourceBinding::None + }, + None, + ) + .unwrap(); + PendingCurrent { + key, + source_exact_factor: (node_kind == RecurrenceNodeKind::Source) + .then_some(ExactComplexRational::ONE), + contributions: BTreeMap::new(), + realized_pairing_rule_ids: BTreeSet::new(), + reflection: CurrentReflection::Unavailable, + reflection_certificate_id: None, + } + } + + fn add_projection_test_contribution( + currents: &mut [PendingCurrent], + destination: u32, + transition: u32, + parents: [u32; 2], + witness: u32, + ) { + let parent_keys = parents.map(|id| ¤ts[id as usize].key); + let destination_key = ¤ts[destination as usize].key; + let key = ContributionKey::new( + transition, + parents.to_vec(), + parent_keys + .iter() + .map(|key| key.current_state_template_id()) + .collect(), + parent_keys + .iter() + .map(|key| key.momentum().clone()) + .collect(), + destination_key.current_state_template_id(), + transition, + LCColorWitnessTermId::new(transition, witness), + digest( + 231_u8 + .checked_add(u8::try_from(transition).unwrap()) + .unwrap(), + ), + 0, + ) + .unwrap(); + currents[destination as usize].contributions.insert( + PendingContributionKey { + parent_current_ids: parents.into(), + key, + }, + ExactComplexRational::ONE, + ); + } + + fn established_contact_test_transition( + step: ContactOrbitStepProof, + input_state_template_ids: [u32; 2], + transition_digest: u8, + ) -> PreparedContactOrbitTransition { + prepared_contact_orbit_transition_for_test( + step, + input_state_template_ids, + digest(transition_digest), + contact_orbit_application_for_test(), + ) + } + + fn add_established_contact_test_contribution( + currents: &mut [PendingCurrent], + destination: u32, + transition: u32, + parents: [u32; 2], + ) { + let parent_keys = parents.map(|id| ¤ts[id as usize].key); + let destination_key = ¤ts[destination as usize].key; + let key = ContributionKey::new( + transition, + parents.to_vec(), + parent_keys + .iter() + .map(|key| key.current_state_template_id()) + .collect(), + parent_keys + .iter() + .map(|key| key.momentum().clone()) + .collect(), + destination_key.current_state_template_id(), + 0, + LCColorWitnessTermId::new(4, 0), + digest(2), + 8, + ) + .unwrap(); + assert!( + currents[destination as usize] + .contributions + .insert( + PendingContributionKey { + parent_current_ids: parents.into(), + key, + }, + ExactComplexRational::ONE, + ) + .is_none() + ); + } + + fn established_contact_transition_ids(current: &PendingCurrent) -> Vec { + current + .contributions + .keys() + .map(|pending| pending.key.transition_template_id()) + .collect() + } + + fn commit_established_contact_test_plan( + currents: &mut [PendingCurrent], + contacts: &BTreeMap, + stage_current_start: usize, + resident_contribution_count: &mut usize, + ) -> RusticolResult<()> { + if let Some(plan) = plan_established_contact_orbit_owners_with_resolver( + stage_current_start, + currents, + *resident_contribution_count, + |transition_id| contacts.get(&transition_id), + )? { + plan.commit(currents, resident_contribution_count); + } + Ok(()) + } + + fn established_0000_contact_case(reverse_insertion: bool) -> (Vec, usize) { + let mut currents = vec![ + projection_test_current(RecurrenceNodeKind::Source, 10, 0, &[10]), + projection_test_current(RecurrenceNodeKind::Source, 10, 1, &[11]), + projection_test_current(RecurrenceNodeKind::Current, 20, 2, &[10, 11]), + ]; + let steps = [ + partial_contact_orbit_step_for_test(0, 1, 2, 20, [0, 0, 0, 0]), + partial_contact_orbit_step_for_test(1, 0, 2, 30, [0, 0, 0, 0]), + partial_contact_orbit_step_for_test(0, 2, 1, 21, [0, 0, 0, 0]), + partial_contact_orbit_step_for_test(2, 0, 1, 31, [0, 0, 0, 0]), + partial_contact_orbit_step_for_test(0, 3, 1, 22, [0, 0, 0, 0]), + partial_contact_orbit_step_for_test(3, 0, 1, 32, [0, 0, 0, 0]), + ]; + let contacts = steps + .into_iter() + .enumerate() + .map(|(id, step)| { + let id = u32::try_from(id).unwrap(); + ( + id, + established_contact_test_transition( + step, + [10, 10], + 40 + u8::try_from(id).unwrap(), + ), + ) + }) + .collect::>(); + let insertion_order = if reverse_insertion { + (0_u32..6).rev().collect::>() + } else { + (0_u32..6).collect::>() + }; + for transition in insertion_order { + let parent_ids = if transition % 2 == 0 { [0, 1] } else { [1, 0] }; + add_established_contact_test_contribution(&mut currents, 2, transition, parent_ids); + } + let mut resident_contribution_count = 6; + commit_established_contact_test_plan( + &mut currents, + &contacts, + 2, + &mut resident_contribution_count, + ) + .unwrap(); + assert_eq!( + pending_contribution_entry_count(¤ts).unwrap(), + resident_contribution_count + ); + ( + established_contact_transition_ids(¤ts[2]), + resident_contribution_count, + ) + } + + fn established_0000_final_contact_case(reverse_insertion: bool) -> (Vec, usize) { + let mut currents = vec![ + projection_test_current(RecurrenceNodeKind::Source, 10, 0, &[10]), + projection_test_current(RecurrenceNodeKind::Source, 10, 1, &[11]), + projection_test_current(RecurrenceNodeKind::Source, 10, 2, &[12]), + projection_test_current(RecurrenceNodeKind::Current, 20, 3, &[11, 12]), + projection_test_current(RecurrenceNodeKind::Current, 20, 4, &[10, 12]), + projection_test_current(RecurrenceNodeKind::Current, 20, 5, &[10, 11]), + projection_test_current(RecurrenceNodeKind::Current, 30, 6, &[10, 11, 12]), + ]; + let parent_ids = [[3, 0], [0, 3], [4, 1], [1, 4], [5, 2], [2, 5]]; + let steps = [ + final_contact_orbit_step_for_test(&[1, 2], &[3], 0, 60, [0, 0, 0, 0]), + final_contact_orbit_step_for_test(&[3], &[1, 2], 0, 70, [0, 0, 0, 0]), + final_contact_orbit_step_for_test(&[1, 2], &[3], 0, 61, [0, 0, 0, 0]), + final_contact_orbit_step_for_test(&[3], &[1, 2], 0, 71, [0, 0, 0, 0]), + final_contact_orbit_step_for_test(&[1, 2], &[3], 0, 62, [0, 0, 0, 0]), + final_contact_orbit_step_for_test(&[3], &[1, 2], 0, 72, [0, 0, 0, 0]), + ]; + let contacts = steps + .into_iter() + .enumerate() + .map(|(id, step)| { + let id = u32::try_from(id).unwrap(); + let parents = parent_ids[id as usize]; + ( + id, + established_contact_test_transition( + step, + parents.map(|parent| { + currents[parent as usize].key.current_state_template_id() + }), + 80 + u8::try_from(id).unwrap(), + ), + ) + }) + .collect::>(); + let insertion_order = if reverse_insertion { + (0_u32..6).rev().collect::>() + } else { + (0_u32..6).collect::>() + }; + for transition in insertion_order { + add_established_contact_test_contribution( + &mut currents, + 6, + transition, + parent_ids[transition as usize], + ); + } + let mut resident_contribution_count = 6; + commit_established_contact_test_plan( + &mut currents, + &contacts, + 6, + &mut resident_contribution_count, + ) + .unwrap(); + assert_eq!( + pending_contribution_entry_count(¤ts).unwrap(), + resident_contribution_count + ); + ( + established_contact_transition_ids(¤ts[6]), + resident_contribution_count, + ) + } + + #[test] + fn established_contact_0000_fan_in_keeps_one_certified_owner_deterministically() { + let forward = established_0000_contact_case(false); + let reverse = established_0000_contact_case(true); + assert_eq!(forward, (vec![0], 1)); + assert_eq!(reverse, forward); + } + + #[test] + fn established_contact_0000_final_fan_in_keeps_three_source_assignments() { + let forward = established_0000_final_contact_case(false); + let reverse = established_0000_final_contact_case(true); + assert_eq!(reverse, forward); + assert_eq!(forward.0.len(), 3); + assert_eq!(forward.1, 3); + } + + #[test] + fn established_contact_0012_partial_and_final_fan_in_each_keep_one_owner() { + let mut partial_currents = vec![ + projection_test_current(RecurrenceNodeKind::Source, 10, 0, &[10]), + projection_test_current(RecurrenceNodeKind::Source, 10, 1, &[11]), + projection_test_current(RecurrenceNodeKind::Current, 20, 2, &[10, 11]), + ]; + let partial_contacts = BTreeMap::from([ + ( + 10, + established_contact_test_transition( + partial_contact_orbit_step_for_test(0, 1, 2, 50, [0, 0, 1, 2]), + [10, 10], + 52, + ), + ), + ( + 11, + established_contact_test_transition( + partial_contact_orbit_step_for_test(1, 0, 2, 51, [0, 0, 1, 2]), + [10, 10], + 53, + ), + ), + ]); + add_established_contact_test_contribution(&mut partial_currents, 2, 11, [0, 1]); + add_established_contact_test_contribution(&mut partial_currents, 2, 10, [0, 1]); + let mut partial_resident = 2; + commit_established_contact_test_plan( + &mut partial_currents, + &partial_contacts, + 2, + &mut partial_resident, + ) + .unwrap(); + assert_eq!( + established_contact_transition_ids(&partial_currents[2]), + [10] + ); + assert_eq!(partial_resident, 1); + assert_eq!( + pending_contribution_entry_count(&partial_currents).unwrap(), + partial_resident + ); + + let mut final_currents = vec![ + projection_test_current(RecurrenceNodeKind::Current, 20, 0, &[10, 11]), + projection_test_current(RecurrenceNodeKind::Source, 30, 1, &[12]), + projection_test_current(RecurrenceNodeKind::Current, 40, 2, &[10, 11, 12]), + ]; + let final_contacts = BTreeMap::from([ + ( + 20, + established_contact_test_transition( + final_contact_orbit_step_for_test(&[0, 1], &[2], 3, 54, [0, 0, 1, 2]), + [20, 30], + 56, + ), + ), + ( + 21, + established_contact_test_transition( + final_contact_orbit_step_for_test(&[2], &[0, 1], 3, 55, [0, 0, 1, 2]), + [30, 20], + 57, + ), + ), + ]); + add_established_contact_test_contribution(&mut final_currents, 2, 20, [0, 1]); + add_established_contact_test_contribution(&mut final_currents, 2, 21, [1, 0]); + let mut final_resident = 2; + commit_established_contact_test_plan( + &mut final_currents, + &final_contacts, + 2, + &mut final_resident, + ) + .unwrap(); + assert_eq!(established_contact_transition_ids(&final_currents[2]), [21]); + assert_eq!(final_resident, 1); + assert_eq!( + pending_contribution_entry_count(&final_currents).unwrap(), + final_resident + ); + } + + #[test] + fn established_contact_plan_errors_roll_back_contributions_and_counts() { + let mut currents = vec![ + projection_test_current(RecurrenceNodeKind::Source, 10, 0, &[10]), + projection_test_current(RecurrenceNodeKind::Source, 10, 1, &[11]), + projection_test_current(RecurrenceNodeKind::Current, 20, 2, &[10, 11]), + ]; + let duplicate_step = partial_contact_orbit_step_for_test(0, 1, 2, 60, [0, 0, 0, 0]); + let contacts = BTreeMap::from([ + ( + 30, + established_contact_test_transition(duplicate_step.clone(), [10, 10], 61), + ), + ( + 31, + established_contact_test_transition(duplicate_step, [10, 10], 61), + ), + ]); + add_established_contact_test_contribution(&mut currents, 2, 30, [0, 1]); + add_established_contact_test_contribution(&mut currents, 2, 31, [0, 1]); + let before = currents[2].contributions.clone(); + let resident_before = 2usize; + assert!( + plan_established_contact_orbit_owners_with_resolver( + 2, + ¤ts, + resident_before, + |transition_id| contacts.get(&transition_id), + ) + .unwrap_err() + .to_string() + .contains("conflicting exact rank") + ); + assert_eq!(currents[2].contributions, before); + assert_eq!(resident_before, 2); + + let mut malformed = currents.clone(); + let (pending, factor) = malformed[2] + .contributions + .first_key_value() + .map(|(pending, factor)| (pending.clone(), *factor)) + .unwrap(); + malformed[2].contributions.remove(&pending); + malformed[2].contributions.insert( + PendingContributionKey { + parent_current_ids: vec![0].into_boxed_slice(), + key: pending.key, + }, + factor, + ); + let malformed_before = malformed[2].contributions.clone(); + assert!( + plan_established_contact_orbit_owners_with_resolver( + 2, + &malformed, + resident_before, + |transition_id| contacts.get(&transition_id), + ) + .unwrap_err() + .to_string() + .contains("is not binary") + ); + assert_eq!(malformed[2].contributions, malformed_before); + assert_eq!(resident_before, 2); + + let mut candidate_reservation = Vec::::new(); + assert!( + reserve_established_contact_candidates(&mut candidate_reservation, usize::MAX) + .unwrap_err() + .to_string() + .contains("candidate allocation failed") + ); + assert!(candidate_reservation.is_empty()); + assert_eq!(currents[2].contributions, before); + assert_eq!(resident_before, 2); + } + + #[test] + fn established_uncertified_v3_vector_fermion_and_qcd_controls_pass_through() { + let mut currents = vec![ + projection_test_current(RecurrenceNodeKind::Source, 10, 0, &[10]), + projection_test_current(RecurrenceNodeKind::Source, 11, 1, &[11]), + projection_test_current(RecurrenceNodeKind::Current, 20, 2, &[10, 11]), + ]; + // The owner path is deliberately blind to the model-level class of an + // uncertified row. These four distinct controls stand for ordinary V3, + // vector, fermion, and QCD transitions and must all pass through. + for transition in 70..74 { + add_established_contact_test_contribution(&mut currents, 2, transition, [0, 1]); + } + let before = currents[2].contributions.clone(); + let mut resident = 4usize; + commit_established_contact_test_plan(&mut currents, &BTreeMap::new(), 2, &mut resident) + .unwrap(); + assert_eq!(currents[2].contributions, before); + assert_eq!(resident, 4); + assert_eq!( + pending_contribution_entry_count(¤ts).unwrap(), + resident + ); + } + + #[test] + fn prepared_transition_location_allocation_failure_leaves_index_empty() { + let mut locations = Vec::new(); + assert!( + reserve_prepared_transition_locations(&mut locations, usize::MAX) + .unwrap_err() + .to_string() + .contains("transition-location allocation failed") + ); + assert!(locations.is_empty()); + } + + fn projection_test_graph( + omit_last_contribution: bool, + omit_last_closure: bool, + ) -> ( + Vec, + BTreeMap, + ) { + let mut currents = vec![ + projection_test_current(RecurrenceNodeKind::Source, 0, 0, &[0]), + projection_test_current(RecurrenceNodeKind::Source, 0, 1, &[1]), + projection_test_current(RecurrenceNodeKind::Source, 0, 2, &[2]), + projection_test_current(RecurrenceNodeKind::Current, 1, 3, &[0, 1]), + projection_test_current(RecurrenceNodeKind::Current, 1, 4, &[0, 1]), + projection_test_current(RecurrenceNodeKind::Current, 2, 5, &[0, 1, 2]), + projection_test_current(RecurrenceNodeKind::Current, 2, 6, &[0, 1, 2]), + ]; + // The first projected value is a sum of two distinct exact kernels. + add_projection_test_contribution(&mut currents, 3, 10, [0, 1], 0); + add_projection_test_contribution(&mut currents, 4, 11, [0, 1], 0); + // The next value has one common bilinear kernel over the complete + // Cartesian product {3,4} x {2}. + add_projection_test_contribution(&mut currents, 5, 20, [3, 2], 0); + if !omit_last_contribution { + add_projection_test_contribution(&mut currents, 6, 20, [4, 2], 1); + } + let mut closures = BTreeMap::new(); + for parent in [5, 6] { + if omit_last_closure && parent == 6 { + continue; + } + closures.insert( + PendingClosureKey { + target_sector_id: 0, + complete_source_states: Box::new([]), + closure_template_id: 0, + quantum_flow_template_id: None, + parent_current_ids: vec![parent, 0].into_boxed_slice(), + }, + PendingClosureGroup { + contributions: vec![], + exact_factor: ExactComplexRational::ONE, + }, + ); + } + (currents, closures) + } + + #[test] + fn isolated_lane_compaction_discards_only_unreachable_contributions() { + let (mut currents, closures) = projection_test_graph(false, false); + currents.push(projection_test_current( + RecurrenceNodeKind::Current, + 3, + 7, + &[0, 2], + )); + add_projection_test_contribution(&mut currents, 7, 21, [0, 2], 0); + let mut resident_contribution_count = pending_contribution_entry_count(¤ts).unwrap(); + assert_eq!(resident_contribution_count, 5); + + prune_inactive_lane_contributions( + &mut currents, + PendingConstructionDomain { + shared_source_end: 3, + lane_internal_start: 3, + lane_internal_end: 8, + }, + &closures, + &mut resident_contribution_count, + ) + .unwrap(); + + assert_eq!(resident_contribution_count, 4); + assert_eq!( + resident_contribution_count, + pending_contribution_entry_count(¤ts).unwrap(), + ); + assert!(currents[7].contributions.is_empty()); + assert!( + currents[3..7] + .iter() + .all(|current| !current.contributions.is_empty()) + ); + } + + #[test] + fn topology_replay_color_projection_requires_complete_rectangles() { + let (currents, closures) = projection_test_graph(false, false); + let projection = plan_topology_replay_color_projection(¤ts, &closures) + .unwrap() + .unwrap(); + assert_eq!(projection.projection_members.len(), 5); + assert_eq!( + projection.old_to_projection[&3], + projection.old_to_projection[&4] + ); + assert_eq!( + projection.old_to_projection[&5], + projection.old_to_projection[&6] + ); + assert_eq!(projection.contributions.len(), 3); + assert_eq!(projection.closures.len(), 1); + assert_eq!( + projection + .closures + .values() + .next() + .unwrap() + .builder_parent_tuples + .len(), + 2 + ); + let materialized = materialize_projected_pending_rows(¤ts, &projection).unwrap(); + assert_eq!(materialized.currents.len(), 5); + assert_eq!(materialized.contributions.len(), 3); + assert_eq!(materialized.finalizations.len(), 2); + assert_eq!(materialized.remap[&3], materialized.remap[&4]); + assert_eq!(materialized.remap[&5], materialized.remap[&6]); + } + + #[test] + fn topology_replay_color_projection_values_are_exact_member_sums() { + let (currents, closures) = projection_test_graph(false, false); + let projection = plan_topology_replay_color_projection(¤ts, &closures) + .unwrap() + .unwrap(); + + // Treat the three source values and transition kernels as arbitrary + // exact scalar witnesses. The first projected value is v3 + v4. + // Rectangularity then proves that one bilinear row over this sum is + // exactly the two original rows, and the same argument closes the + // amplitude. No representative color metadata enters the arithmetic. + let source0 = 2_i128; + let source1 = 3_i128; + let source2 = 5_i128; + let value3 = 2_i128 * source0 * source1; + let value4 = -3_i128 * source0 * source1; + let value5 = 5_i128 * value3 * source2; + let value6 = 5_i128 * value4 * source2; + let original_closure = value5 * source0 + value6 * source0; + + let projected_first = value3 + value4; + let projected_second = 5_i128 * projected_first * source2; + let projected_closure = projected_second * source0; + assert_eq!(projected_first, value3 + value4); + assert_eq!(projected_second, value5 + value6); + assert_eq!(projected_closure, original_closure); + assert_eq!(projected_closure, -300); + + let projected_second_id = projection.old_to_projection[&5]; + let row = projection + .contributions + .values() + .find(|row| row.identity.destination_projection_id == projected_second_id) + .unwrap(); + assert_eq!(row.builder_parent_tuples.len(), 2); + assert!( + rectangular_parent_domain_is_complete( + &row.identity.parent_projection_ids, + &row.builder_parent_tuples, + &projection.old_to_projection, + &projection.projection_members, + ) + .unwrap() + ); + } + + #[test] + fn projected_runtime_rows_do_not_depend_on_the_representative_color_witness() { + fn replace_only_witness(current: &mut PendingCurrent, witness: u32) { + assert_eq!(current.contributions.len(), 1); + let (pending_key, factor) = std::mem::take(&mut current.contributions) + .into_iter() + .next() + .unwrap(); + let old = pending_key.key; + let key = ContributionKey::new( + old.transition_template_id(), + old.parent_value_class_ids().to_vec(), + old.parent_state_template_ids().to_vec(), + old.parent_momenta().to_vec(), + old.result_state_template_id(), + old.quantum_flow_witness_id(), + LCColorWitnessTermId::new( + old.color_witness_term_id().color_contraction_template_id(), + witness, + ), + old.runtime_coupling_binding_digest(), + old.output_projection_id(), + ) + .unwrap(); + current.contributions.insert( + PendingContributionKey { + parent_current_ids: pending_key.parent_current_ids, + key, + }, + factor, + ); + } + fn runtime_signature( + rows: &MaterializedPendingRows, + ) -> Vec<(u32, Vec, u32, ExactComplexRational)> { + rows.contributions + .iter() + .map(|row| { + ( + row.result_current_id(), + row.parent_current_ids().to_vec(), + row.key().transition_template_id(), + row.exact_factor(), + ) + }) + .collect() + } + + let (currents, closures) = projection_test_graph(false, false); + let first_projection = plan_topology_replay_color_projection(¤ts, &closures) + .unwrap() + .unwrap(); + let first = materialize_projected_pending_rows(¤ts, &first_projection).unwrap(); + let first_witness = first + .contributions + .iter() + .find(|row| row.key().transition_template_id() == 20) + .unwrap() + .key() + .color_witness_term_id(); + + let mut swapped = currents.clone(); + replace_only_witness(&mut swapped[5], 1); + replace_only_witness(&mut swapped[6], 0); + let second_projection = plan_topology_replay_color_projection(&swapped, &closures) + .unwrap() + .unwrap(); + let second = materialize_projected_pending_rows(&swapped, &second_projection).unwrap(); + let second_witness = second + .contributions + .iter() + .find(|row| row.key().transition_template_id() == 20) + .unwrap() + .key() + .color_witness_term_id(); + + assert_ne!(first_witness, second_witness); + assert_eq!(runtime_signature(&first), runtime_signature(&second)); + } + + #[test] + fn three_line_selected_flow_projection_stays_within_legacy_work_budget() { + // Exact N=4..6 structural census for report process #13 after selector + // pruning, global-flip reduction, and rectangular color projection. + // This is a release guard for the real catalog rows, not a timing + // expectation. + let rows = [ + (4, (32_u64, 37_u64), (33_u64, 37_u64)), + (5, (66_u64, 103_u64), (60_u64, 94_u64)), + (6, (136_u64, 283_u64), (116_u64, 246_u64)), + ]; + for (multiplicity, legacy, projected) in rows { + assert!( + projected.0 * 100 <= legacy.0 * 105, + "N={multiplicity} projected currents exceed 1.05x legacy" + ); + assert!( + projected.1 * 100 <= legacy.1 * 105, + "N={multiplicity} projected interactions exceed 1.05x legacy" + ); + } + } + + #[test] + fn topology_replay_color_projection_certificate_is_deterministic_and_complete() { + let (currents, closures) = projection_test_graph(false, false); + let projection = plan_topology_replay_color_projection(¤ts, &closures) + .unwrap() + .unwrap(); + let color_states = (0..7) + .map(|shape| DynamicLCColorState::new(shape, None, vec![]).unwrap()) + .collect::>(); + let candidate_digests = [digest(240), digest(241)]; + let first = encode_color_projection_certificate_body( + ¤ts, + &color_states, + &projection, + &candidate_digests, + ) + .unwrap(); + let second = encode_color_projection_certificate_body( + ¤ts, + &color_states, + &projection, + &candidate_digests, + ) + .unwrap(); + assert_eq!(first, second); + assert!(first.starts_with(COLOR_PROJECTION_CERTIFICATE_BODY_MAGIC)); + let digest_start = first.len() - 32; + assert_eq!( + &first[digest_start..], + Sha256::digest(&first[..digest_start]).as_slice() + ); + for old_id in 0_u32..7 { + assert!( + first + .windows(4) + .any(|window| window == old_id.to_le_bytes()), + "certificate omits old current {old_id}", + ); + } + } + + #[test] + fn topology_replay_color_projection_rejects_a_missing_internal_tuple() { + let (currents, mut closures) = projection_test_graph(true, false); + // Keep both members of the first projected current class live through + // a separate, complete closure family. Otherwise omitting current 6's + // contribution makes parent current 4 selector-dead, so [3, 2] is a + // complete singleton rectangle rather than a missing active tuple. + for parent in [3, 4] { + closures.insert( + PendingClosureKey { + target_sector_id: 0, + complete_source_states: Box::new([]), + closure_template_id: 1, + quantum_flow_template_id: None, + parent_current_ids: vec![parent, 0].into_boxed_slice(), + }, + PendingClosureGroup { + contributions: vec![], + exact_factor: ExactComplexRational::ONE, + }, + ); + } + assert!( + plan_topology_replay_color_projection(¤ts, &closures) + .unwrap() + .is_none() + ); + } + + #[test] + fn topology_replay_color_projection_rejects_a_missing_closure_tuple() { + let (currents, closures) = projection_test_graph(false, true); + assert!( + plan_topology_replay_color_projection(¤ts, &closures) + .unwrap() + .is_none() + ); + } +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/recurrence/contact_orbit_owner.rs b/pyamplicol/rust/crates/rusticol-core/src/recurrence/contact_orbit_owner.rs new file mode 100644 index 0000000000..4dd80b3b43 --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/recurrence/contact_orbit_owner.rs @@ -0,0 +1,2682 @@ +// SPDX-License-Identifier: 0BSD + +//! Exact owner selection for compiler-certified scalar contact orbits. +//! +//! The helper is deliberately process-blind. It forgets only binary +//! traversal direction and the concrete evaluator-binding row while retaining +//! every physical, selector, color, coupling, and numerical evaluator witness. +//! Owner selection uses fallibly reserved vector scratch: it does not clone +//! current/application graphs or grow map/set nodes per transition. + +use std::{cmp::Ordering, mem::size_of}; + +use super::{ + CurrentCoreKey, CurrentSourceBinding, DynamicLCColorStateId, ExactComplexRational, + LCColorWitnessTermId, RecurrenceNodeKind, RecurrenceStrategy, SemanticDigest, + construct::TemplateCatalog, + template::{ + ContactOrbitStage, EvaluatorCallableKind, EvaluatorContractKind, + LCColorTransitionWitnessRow, OwnedRecurrenceTemplateInput, TransitionRow, + }, +}; +use crate::{RusticolError, RusticolResult}; + +fn integrity(message: impl Into) -> RusticolError { + RusticolError::integrity(message) +} + +fn allocation(message: impl Into) -> RusticolError { + RusticolError::internal(message) +} + +#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] +pub(super) struct ContactOrbitCoveredLegs { + len: u8, + values: [u32; 2], +} + +impl ContactOrbitCoveredLegs { + pub(super) fn new(values: &[u32]) -> RusticolResult { + let len = u8::try_from(values.len()) + .map_err(|_| integrity("contact-orbit covered-leg count exceeds u8"))?; + if !(1..=2).contains(&len) { + return Err(integrity( + "contact-orbit covered-leg count must be one or two", + )); + } + let mut canonical = [0_u32; 2]; + canonical[..values.len()].copy_from_slice(values); + canonical[..values.len()].sort_unstable(); + if values.len() == 2 && canonical[0] == canonical[1] { + return Err(integrity("contact-orbit covered legs are not unique")); + } + Ok(Self { + len, + values: canonical, + }) + } + + fn values(&self) -> &[u32] { + &self.values[..usize::from(self.len)] + } + + const fn len(self) -> usize { + self.len as usize + } +} + +#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] +pub(super) struct ContactOrbitStepProof { + pub(super) certificate_id: u32, + pub(super) certificate_semantic_digest: SemanticDigest, + pub(super) step_semantic_digest: SemanticDigest, + pub(super) stage: ContactOrbitStage, + pub(super) result_leg: u32, + pub(super) physical_leg_equivalence_classes: Vec, + pub(super) left_covered_legs: ContactOrbitCoveredLegs, + pub(super) right_covered_legs: ContactOrbitCoveredLegs, + pub(super) source_particle_legs: [i32; 3], + pub(super) certificate_reconstruction_factor: ExactComplexRational, + pub(super) step_reconstruction_factor: ExactComplexRational, + pub(super) evaluator_class: String, +} + +#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] +pub(super) struct ContactOrbitApplicationWitness { + pub(super) transition_equivalence_class: String, + pub(super) quantum_semantic_digest: SemanticDigest, + pub(super) color_contraction_semantic_digest: SemanticDigest, + pub(super) color_witness_term_id: LCColorWitnessTermId, + pub(super) color_witness_proof_digest: SemanticDigest, + pub(super) coupling_orders: Vec, + pub(super) coupling_parameter_ids: Vec, + pub(super) momentum_convention: Vec, + pub(super) binding_coupling: ExactComplexRational, + pub(super) transition_exact_factor: ExactComplexRational, + pub(super) color_exact_factor: ExactComplexRational, + pub(super) witness_exact_factor: ExactComplexRational, + pub(super) input_exchange_factor: Option, + pub(super) output_factor_source: u8, + pub(super) evaluator_contract_kind: u8, + pub(super) evaluator_callable_kind: u8, + pub(super) evaluator_callable_signature: SemanticDigest, + pub(super) evaluator_input_layout: Vec, + pub(super) evaluator_output_layout: Vec, + pub(super) evaluator_exact_expression_digests: Vec, + pub(super) evaluator_runtime_template: Option, + pub(super) output_projection_id: u32, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub(super) struct PreparedContactOrbitTransition { + step: ContactOrbitStepProof, + input_state_template_ids: [u32; 2], + transition_semantic_digest: SemanticDigest, + applications: Vec, +} + +/// Minimal parent facts needed by the contact-orbit construction-domain +/// predicate. The process-global on-the-fly coupling sweep deliberately has +/// no `CurrentCoreKey`; this keeps its admission decision identical to the +/// query-local sweep without manufacturing a recurrence current. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(super) struct ContactOrbitParentTopologyDomain<'a> { + current_state_template_id: u32, + support_source_slots: &'a [u32], + is_source: bool, + is_current: bool, +} + +impl<'a> ContactOrbitParentTopologyDomain<'a> { + pub(super) const fn source( + current_state_template_id: u32, + support_source_slots: &'a [u32], + ) -> Self { + Self { + current_state_template_id, + support_source_slots, + is_source: true, + is_current: false, + } + } + + pub(super) const fn current( + current_state_template_id: u32, + support_source_slots: &'a [u32], + ) -> Self { + Self { + current_state_template_id, + support_source_slots, + is_source: false, + is_current: true, + } + } + + fn from_current_key(current: &'a CurrentCoreKey) -> Self { + Self { + current_state_template_id: current.current_state_template_id(), + support_source_slots: current.support_source_slots(), + is_source: current.node_kind() == RecurrenceNodeKind::Source + && !matches!(current.source_binding(), &CurrentSourceBinding::None), + is_current: current.node_kind() == RecurrenceNodeKind::Current + && matches!(current.source_binding(), &CurrentSourceBinding::None), + } + } +} + +fn try_copy_u32(values: &[u32], label: &str) -> RusticolResult> { + let mut copied = Vec::new(); + copied + .try_reserve_exact(values.len()) + .map_err(|error| allocation(format!("{label} allocation failed: {error}")))?; + copied.extend_from_slice(values); + Ok(copied) +} + +fn try_copy_digests( + catalog: &TemplateCatalog<'_>, + digest_ids: &[u32], + label: &str, +) -> RusticolResult> { + let mut digests = Vec::new(); + digests + .try_reserve_exact(digest_ids.len()) + .map_err(|error| allocation(format!("{label} allocation failed: {error}")))?; + for digest_id in digest_ids { + digests.push(catalog.digest(*digest_id, label)?); + } + Ok(digests) +} + +fn try_clone_digests( + values: &[SemanticDigest], + label: &str, +) -> RusticolResult> { + let mut copied = Vec::new(); + copied + .try_reserve_exact(values.len()) + .map_err(|error| allocation(format!("{label} allocation failed: {error}")))?; + copied.extend_from_slice(values); + Ok(copied) +} + +fn try_copy_string(value: &str, label: &str) -> RusticolResult { + let mut copied = String::new(); + copied + .try_reserve_exact(value.len()) + .map_err(|error| allocation(format!("{label} allocation failed: {error}")))?; + copied.push_str(value); + Ok(copied) +} + +fn strict_contact_orbit_step( + input: &OwnedRecurrenceTemplateInput, + catalog: &TemplateCatalog<'_>, + transition: TransitionRow, +) -> RusticolResult> { + let step_ids = catalog.u32_sequence( + transition.contact_orbit_step_sequence_id, + "transition contact-orbit steps", + )?; + let step_digest_ids = catalog.u32_sequence( + transition.contact_orbit_step_semantic_digest_sequence_id, + "transition contact-orbit step digests", + )?; + if step_ids.len() != step_digest_ids.len() { + return Err(integrity( + "transition contact-orbit step bindings are incomplete", + )); + } + match step_ids { + [] => return Ok(None), + [_] => {} + _ => { + return Err(integrity( + "certified transition must bind exactly one contact-orbit step", + )); + } + } + let step_id = step_ids[0]; + let step = input + .contact_orbit_steps + .get(step_id as usize) + .copied() + .filter(|step| step.id == step_id) + .ok_or_else(|| integrity("transition contact-orbit step is absent"))?; + if step.semantic_digest_id != step_digest_ids[0] { + return Err(integrity( + "transition contact-orbit step digest binding is stale", + )); + } + let certificate = input + .contact_orbit_certificates + .get(step.certificate_id as usize) + .copied() + .filter(|certificate| certificate.id == step.certificate_id) + .ok_or_else(|| integrity("contact-orbit certificate is absent"))?; + let physical_leg_equivalence_classes = try_copy_u32( + catalog.u32_sequence( + certificate.physical_leg_equivalence_sequence_id, + "contact-orbit physical-leg equivalence classes", + )?, + "contact-orbit physical-leg equivalence classes", + )?; + if !matches!(physical_leg_equivalence_classes.len(), 4 | 5) { + return Err(integrity( + "contact-orbit certificate does not describe four or five legs", + )); + } + let source_particle_legs: [i32; 3] = catalog + .i32_sequence( + step.source_particle_leg_sequence_id, + "contact-orbit source-particle legs", + )? + .try_into() + .map_err(|_| integrity("contact-orbit step does not describe three source legs"))?; + let proof = ContactOrbitStepProof { + certificate_id: certificate.id, + certificate_semantic_digest: catalog.digest( + certificate.semantic_digest_id, + "contact-orbit certificate semantic", + )?, + step_semantic_digest: catalog + .digest(step.semantic_digest_id, "contact-orbit step semantic")?, + stage: ContactOrbitStage::try_from(step.stage)?, + result_leg: u32::from(step.result_leg), + physical_leg_equivalence_classes, + left_covered_legs: ContactOrbitCoveredLegs::new(catalog.u32_sequence( + step.left_covered_leg_sequence_id, + "contact-orbit left covered legs", + )?)?, + right_covered_legs: ContactOrbitCoveredLegs::new(catalog.u32_sequence( + step.right_covered_leg_sequence_id, + "contact-orbit right covered legs", + )?)?, + source_particle_legs, + certificate_reconstruction_factor: catalog.factor( + certificate.reconstruction_factor_id, + "contact-orbit certificate reconstruction", + )?, + step_reconstruction_factor: catalog.factor( + step.reconstruction_factor_id, + "contact-orbit step reconstruction", + )?, + evaluator_class: try_copy_string( + catalog.string( + certificate.evaluator_class_string_id, + "contact-orbit evaluator class", + )?, + "contact-orbit evaluator class", + )?, + }; + validate_step(&proof)?; + Ok(Some(proof)) +} + +#[allow(clippy::too_many_arguments)] +pub(super) fn prepare_contact_orbit_transition( + input: &OwnedRecurrenceTemplateInput, + catalog: &TemplateCatalog<'_>, + transition: TransitionRow, + quantum_semantic_digest: SemanticDigest, + local_coupling_orders: &[u32], + binding_coupling: ExactComplexRational, + transition_exact_factor: ExactComplexRational, + contraction_exact_factor: ExactComplexRational, + input_exchange_factor: Option, + witness_rows: &[LCColorTransitionWitnessRow], +) -> RusticolResult> { + let Some(step) = strict_contact_orbit_step(input, catalog, transition)? else { + return Ok(None); + }; + let input_state_template_ids = <[u32; 2]>::try_from(catalog.u32_sequence( + transition.input_state_sequence_id, + "contact-orbit transition input states", + )?) + .map_err(|_| integrity("certified contact-orbit transition is not binary"))?; + let contraction = input + .color_contractions + .get(transition.color_contraction_template_id as usize) + .copied() + .filter(|row| row.id == transition.color_contraction_template_id) + .ok_or_else(|| integrity("contact-orbit color contraction is absent"))?; + let binding = input + .evaluator_bindings + .get(transition.evaluator_binding_id as usize) + .copied() + .filter(|row| row.id == transition.evaluator_binding_id) + .ok_or_else(|| integrity("contact-orbit evaluator binding is absent"))?; + if EvaluatorContractKind::try_from(binding.contract_kind)? != EvaluatorContractKind::Vertex { + return Err(integrity("contact-orbit evaluator binding is not a vertex")); + } + let _ = EvaluatorCallableKind::try_from(binding.callable_kind)?; + let evaluator_input_layout = try_copy_u32( + catalog.u32_sequence( + binding.input_layout_sequence_id, + "contact-orbit evaluator input layout", + )?, + "contact-orbit evaluator input layout", + )?; + let evaluator_output_layout = try_copy_u32( + catalog.u32_sequence( + binding.output_layout_sequence_id, + "contact-orbit evaluator output layout", + )?, + "contact-orbit evaluator output layout", + )?; + let evaluator_exact_expression_digests = try_copy_digests( + catalog, + catalog.u32_sequence( + binding.exact_expression_digest_sequence_id, + "contact-orbit evaluator exact expressions", + )?, + "contact-orbit evaluator exact expression", + )?; + let coupling_orders = + try_copy_u32(local_coupling_orders, "contact-orbit local coupling orders")?; + let coupling_parameter_ids = try_copy_u32( + catalog.u32_sequence( + transition.coupling_parameter_sequence_id, + "contact-orbit coupling parameters", + )?, + "contact-orbit coupling parameters", + )?; + let momentum_convention = try_copy_u32( + catalog.u32_sequence( + transition.momentum_convention_sequence_id, + "contact-orbit momentum convention", + )?, + "contact-orbit momentum convention", + )?; + let transition_equivalence_class = try_copy_string( + catalog.string( + transition.equivalence_class_string_id, + "contact-orbit transition equivalence class", + )?, + "contact-orbit transition equivalence class", + )?; + let evaluator_runtime_template = if binding.runtime_template_string_id == u32::MAX { + None + } else { + Some(try_copy_string( + catalog.string( + binding.runtime_template_string_id, + "contact-orbit evaluator runtime template", + )?, + "contact-orbit evaluator runtime template", + )?) + }; + let color_contraction_semantic_digest = catalog.digest( + contraction.semantic_digest_id, + "contact-orbit color-contraction semantic", + )?; + let evaluator_callable_signature = catalog.digest( + binding.callable_signature_digest_id, + "contact-orbit evaluator callable signature", + )?; + let mut applications = Vec::new(); + applications + .try_reserve_exact(witness_rows.len()) + .map_err(|error| { + allocation(format!( + "contact-orbit application allocation failed: {error}" + )) + })?; + for witness in witness_rows { + if witness.color_contraction_id != contraction.id { + return Err(integrity( + "contact-orbit witness belongs to another color contraction", + )); + } + applications.push(ContactOrbitApplicationWitness { + transition_equivalence_class: try_copy_string( + &transition_equivalence_class, + "contact-orbit transition equivalence class", + )?, + quantum_semantic_digest, + color_contraction_semantic_digest, + color_witness_term_id: LCColorWitnessTermId::new(contraction.id, witness.ordinal), + color_witness_proof_digest: catalog + .digest(witness.proof_digest_id, "contact-orbit color witness proof")?, + coupling_orders: try_copy_u32(&coupling_orders, "contact-orbit local coupling orders")?, + coupling_parameter_ids: try_copy_u32( + &coupling_parameter_ids, + "contact-orbit coupling parameters", + )?, + momentum_convention: try_copy_u32( + &momentum_convention, + "contact-orbit momentum convention", + )?, + binding_coupling, + transition_exact_factor, + color_exact_factor: contraction_exact_factor, + witness_exact_factor: catalog + .factor(witness.exact_factor_id, "contact-orbit color witness")?, + input_exchange_factor, + output_factor_source: transition.output_factor_source, + evaluator_contract_kind: binding.contract_kind, + evaluator_callable_kind: binding.callable_kind, + evaluator_callable_signature, + evaluator_input_layout: try_copy_u32( + &evaluator_input_layout, + "contact-orbit evaluator input layout", + )?, + evaluator_output_layout: try_copy_u32( + &evaluator_output_layout, + "contact-orbit evaluator output layout", + )?, + evaluator_exact_expression_digests: try_clone_digests( + &evaluator_exact_expression_digests, + "contact-orbit evaluator exact expressions", + )?, + evaluator_runtime_template: evaluator_runtime_template + .as_deref() + .map(|value| try_copy_string(value, "contact-orbit evaluator runtime template")) + .transpose()?, + output_projection_id: transition.output_projection_string_id, + }); + } + Ok(Some(PreparedContactOrbitTransition { + step, + input_state_template_ids, + transition_semantic_digest: catalog.digest( + transition.semantic_digest_id, + "contact-orbit transition semantic", + )?, + applications, + })) +} + +impl PreparedContactOrbitTransition { + /// Topology-only counterpart of [`Self::accepts_parent_domain`]. It is + /// used by the process-global on-the-fly coupling sweep, which retains no + /// recurrence-current payload beyond the exact admission facts below. + pub(super) fn accepts_parent_topology_domain( + &self, + parents: [ContactOrbitParentTopologyDomain<'_>; 2], + ) -> RusticolResult { + validate_step(&self.step)?; + Ok(contact_orbit_parent_topology_mapping( + &self.step, + parents, + self.input_state_template_ids, + )? + .is_some()) + } + + /// Whether these concrete parents belong to the certified contact step's + /// physical construction domain. + /// + /// The established builder reaches this point only after its process-wide + /// structural-demand filter, so a missing mapping remains an integrity + /// error in [`Self::owner_candidate`]. The query-local on-the-fly sweep + /// deliberately has no such global demand graph and uses this predicate to + /// avoid instantiating a certified auxiliary step on wider, otherwise + /// state-compatible currents. + pub(super) fn accepts_parent_domain( + &self, + parents: [&CurrentCoreKey; 2], + ) -> RusticolResult { + self.accepts_parent_topology_domain( + parents.map(ContactOrbitParentTopologyDomain::from_current_key), + ) + } + + pub(super) fn owner_candidate<'a>( + &'a self, + destination: &'a CurrentCoreKey, + parents: [&'a CurrentCoreKey; 2], + color_witness_term_id: LCColorWitnessTermId, + ) -> RusticolResult> { + let mut matches = self + .applications + .iter() + .filter(|application| application.color_witness_term_id == color_witness_term_id); + let application = matches + .next() + .ok_or_else(|| integrity("certified contact-orbit color witness is absent"))?; + if matches.next().is_some() { + return Err(integrity( + "certified contact-orbit color witness is ambiguous", + )); + } + contact_orbit_owner_candidate( + Some(&self.step), + destination, + parents, + self.input_state_template_ids, + application, + self.transition_semantic_digest, + )? + .ok_or_else(|| integrity("certified contact-orbit owner candidate disappeared")) + } +} + +#[cfg(test)] +pub(super) fn prepared_contact_orbit_transition_for_test( + step: ContactOrbitStepProof, + input_state_template_ids: [u32; 2], + transition_semantic_digest: SemanticDigest, + application: ContactOrbitApplicationWitness, +) -> PreparedContactOrbitTransition { + PreparedContactOrbitTransition { + step, + input_state_template_ids, + transition_semantic_digest, + applications: vec![application], + } +} + +#[cfg(test)] +fn contact_orbit_test_digest(byte: u8) -> SemanticDigest { + SemanticDigest::new([byte; 32]).expect("test digest must be nonzero") +} + +#[cfg(test)] +pub(super) fn contact_orbit_application_for_test() -> ContactOrbitApplicationWitness { + ContactOrbitApplicationWitness { + transition_equivalence_class: "constant-scalar-contact".into(), + quantum_semantic_digest: contact_orbit_test_digest(2), + color_contraction_semantic_digest: contact_orbit_test_digest(3), + color_witness_term_id: LCColorWitnessTermId::new(4, 0), + color_witness_proof_digest: contact_orbit_test_digest(5), + coupling_orders: vec![1], + coupling_parameter_ids: vec![2], + momentum_convention: vec![0, 1], + binding_coupling: ExactComplexRational::ONE, + transition_exact_factor: ExactComplexRational::ONE, + color_exact_factor: ExactComplexRational::ONE, + witness_exact_factor: ExactComplexRational::ONE, + input_exchange_factor: None, + output_factor_source: 0, + evaluator_contract_kind: EvaluatorContractKind::Vertex as u8, + evaluator_callable_kind: EvaluatorCallableKind::PreparedKernel as u8, + evaluator_callable_signature: contact_orbit_test_digest(6), + evaluator_input_layout: vec![10, 11], + evaluator_output_layout: vec![12], + evaluator_exact_expression_digests: vec![contact_orbit_test_digest(7)], + evaluator_runtime_template: None, + output_projection_id: 8, + } +} + +#[cfg(test)] +pub(super) fn partial_contact_orbit_step_for_test( + left: u32, + right: u32, + result: u32, + digest_byte: u8, + equivalence_classes: [u32; 4], +) -> ContactOrbitStepProof { + ContactOrbitStepProof { + certificate_id: 0, + certificate_semantic_digest: contact_orbit_test_digest(9), + step_semantic_digest: contact_orbit_test_digest(digest_byte), + stage: ContactOrbitStage::Partial, + result_leg: result, + physical_leg_equivalence_classes: equivalence_classes.to_vec(), + left_covered_legs: ContactOrbitCoveredLegs::new(&[left]).unwrap(), + right_covered_legs: ContactOrbitCoveredLegs::new(&[right]).unwrap(), + source_particle_legs: [left as i32, right as i32, -1], + certificate_reconstruction_factor: ExactComplexRational::ONE, + step_reconstruction_factor: ExactComplexRational::ONE, + evaluator_class: "constant-scalar-contact-v1".into(), + } +} + +#[cfg(test)] +pub(super) fn final_contact_orbit_step_for_test( + left: &[u32], + right: &[u32], + result: u32, + digest_byte: u8, + equivalence_classes: [u32; 4], +) -> ContactOrbitStepProof { + ContactOrbitStepProof { + certificate_id: 0, + certificate_semantic_digest: contact_orbit_test_digest(9), + step_semantic_digest: contact_orbit_test_digest(digest_byte), + stage: ContactOrbitStage::Final, + result_leg: result, + physical_leg_equivalence_classes: equivalence_classes.to_vec(), + left_covered_legs: ContactOrbitCoveredLegs::new(left).unwrap(), + right_covered_legs: ContactOrbitCoveredLegs::new(right).unwrap(), + source_particle_legs: [ + if left.len() == 2 { -1 } else { left[0] as i32 }, + if right.len() == 2 { + -1 + } else { + right[0] as i32 + }, + result as i32, + ], + certificate_reconstruction_factor: ExactComplexRational::ONE, + step_reconstruction_factor: ExactComplexRational::ONE, + evaluator_class: "constant-scalar-contact-v1".into(), + } +} + +#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] +struct PhysicalLegAssignments { + len: u8, + values: [u32; 2], +} + +#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] +struct ContactOrbitPhysicalAssignment { + physical_leg_assignments: PhysicalLegAssignments, + source_particle_equivalence_class: Option, +} + +/// Physics identity retained while quotienting a certified contact traversal orbit. +/// +/// A certified physical source leg remains bound to its concrete external +/// source slot. Auxiliary support must also remain in the key: at five points +/// the final HEFT node can join two auxiliaries, and their support partition is +/// the physical identical-particle assignment whose multiplicity is carried by +/// the compiler-generated kernel. Forgetting it collapses distinct Hgggg +/// assignments. Every other current distinction stays in the owner key too. +#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] +struct ContactOrbitCurrentPhysics<'a> { + catalog_digest: SemanticDigest, + node_kind: RecurrenceNodeKind, + current_state_template_id: u32, + dynamic_lc_color_state_id: DynamicLCColorStateId, + recurrence_strategy: RecurrenceStrategy, + spin_state_class: i32, + flavour_flow: &'a [i32], + quantum_number_flow_id: u32, + coupling_orders: &'a [u32], + source_binding: &'a CurrentSourceBinding, + propagator_template_id: Option, + support_source_slots: &'a [u32], +} + +impl<'a> ContactOrbitCurrentPhysics<'a> { + fn from_current(current: &'a CurrentCoreKey) -> Self { + Self { + catalog_digest: current.catalog_digest(), + node_kind: current.node_kind(), + current_state_template_id: current.current_state_template_id(), + dynamic_lc_color_state_id: current.dynamic_lc_color_state_id(), + recurrence_strategy: current.helicity_identity().strategy(), + spin_state_class: current.spin_state_class(), + flavour_flow: current.flavour_flow(), + quantum_number_flow_id: current.quantum_number_flow_id(), + coupling_orders: current.coupling_orders(), + source_binding: current.source_binding(), + propagator_template_id: current.propagator_template_id(), + support_source_slots: current.support_source_slots(), + } + } +} + +#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] +struct ContactOrbitParentGroup<'a> { + current: ContactOrbitCurrentPhysics<'a>, + assignment: ContactOrbitPhysicalAssignment, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +struct ContactOrbitParentSide { + expected_state_template_id: u32, + covered_legs: ContactOrbitCoveredLegs, + source_particle_leg: i32, + assignment: ContactOrbitPhysicalAssignment, +} + +#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] +struct ContactOrbitOwnerGroupKey<'a> { + certificate_id: u32, + certificate_semantic_digest: SemanticDigest, + stage: ContactOrbitStage, + result_leg_equivalence_class: u32, + destination: &'a CurrentCoreKey, + parents: [ContactOrbitParentGroup<'a>; 2], + output_source_particle_equivalence_class: Option, + evaluator_class: &'a str, + coupling_parameter_ids: &'a [u32], + binding_coupling: ExactComplexRational, + application_exact_factor: ExactComplexRational, + input_exchange_factor: Option, + certificate_reconstruction_factor: ExactComplexRational, + step_reconstruction_factor: ExactComplexRational, +} + +#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] +struct ContactOrbitOwnerRank<'a> { + destination: &'a CurrentCoreKey, + parents: [&'a CurrentCoreKey; 2], + oriented_covered_legs: [ContactOrbitCoveredLegs; 2], + oriented_source_particle_legs: [i32; 3], + application: &'a ContactOrbitApplicationWitness, + step_semantic_digest: SemanticDigest, + transition_semantic_digest: SemanticDigest, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(super) struct ContactOrbitOwnerCandidate<'a> { + group: ContactOrbitOwnerGroupKey<'a>, + rank: ContactOrbitOwnerRank<'a>, +} + +fn canonical_physical_assignment( + covered_legs: ContactOrbitCoveredLegs, + equivalence_classes: &[u32], + source_particle_leg: i32, +) -> RusticolResult { + let mut assignments = [0_u32; 2]; + for (index, leg) in covered_legs.values().iter().copied().enumerate() { + assignments[index] = *equivalence_classes + .get(leg as usize) + .ok_or_else(|| integrity("contact-orbit covered leg is outside arity"))?; + } + assignments[..covered_legs.len()].sort_unstable(); + let source_particle_equivalence_class = match source_particle_leg { + -1 => None, + leg if leg >= 0 => Some( + *equivalence_classes + .get(leg as usize) + .ok_or_else(|| integrity("contact-orbit source particle leg is outside arity"))?, + ), + _ => { + return Err(integrity( + "contact-orbit source particle leg uses an unknown sentinel", + )); + } + }; + Ok(ContactOrbitPhysicalAssignment { + physical_leg_assignments: PhysicalLegAssignments { + len: covered_legs.len, + values: assignments, + }, + source_particle_equivalence_class, + }) +} + +fn physical_leg_equivalence_class( + leg: i32, + equivalence_classes: &[u32], + label: &str, +) -> RusticolResult> { + match leg { + -1 => Ok(None), + leg if leg >= 0 => equivalence_classes + .get(leg as usize) + .copied() + .map(Some) + .ok_or_else(|| integrity(format!("contact-orbit {label} is outside arity"))), + _ => Err(integrity(format!( + "contact-orbit {label} uses an unknown sentinel" + ))), + } +} + +fn validate_step(step: &ContactOrbitStepProof) -> RusticolResult<()> { + if step.certificate_reconstruction_factor != ExactComplexRational::ONE + || step.step_reconstruction_factor != ExactComplexRational::ONE + { + return Err(integrity( + "contact-orbit owner requires exact unit reconstruction", + )); + } + let arity = step.physical_leg_equivalence_classes.len(); + if !matches!(arity, 4 | 5) { + return Err(integrity( + "contact-orbit certificate does not describe four or five legs", + )); + } + let result_index = usize::try_from(step.result_leg) + .ok() + .filter(|index| *index < arity) + .ok_or_else(|| integrity("contact-orbit result leg is outside arity"))?; + let mut covered = vec![false; arity]; + for leg in step + .left_covered_legs + .values() + .iter() + .chain(step.right_covered_legs.values()) + .copied() + { + let index = usize::try_from(leg) + .ok() + .filter(|index| *index < arity) + .ok_or_else(|| integrity("contact-orbit covered leg is outside arity"))?; + if index == result_index || covered[index] { + return Err(integrity("contact-orbit covered-leg partition is invalid")); + } + covered[index] = true; + } + match step.stage { + ContactOrbitStage::Partial => { + if step.left_covered_legs.len() != 1 + || step.right_covered_legs.len() != 1 + || step.source_particle_legs + != [ + step.left_covered_legs.values()[0] as i32, + step.right_covered_legs.values()[0] as i32, + -1, + ] + { + return Err(integrity( + "contact-orbit partial owner lineage is inconsistent", + )); + } + } + ContactOrbitStage::Final => { + let expected_sources = [ + if step.left_covered_legs.len() > 1 { + -1 + } else { + step.left_covered_legs.values()[0] as i32 + }, + if step.right_covered_legs.len() > 1 { + -1 + } else { + step.right_covered_legs.values()[0] as i32 + }, + step.result_leg as i32, + ]; + if step.left_covered_legs.len() > 2 + || step.right_covered_legs.len() > 2 + || step.source_particle_legs != expected_sources + || covered + .iter() + .enumerate() + .any(|(index, value)| *value != (index != result_index)) + { + return Err(integrity( + "contact-orbit final owner lineage is inconsistent", + )); + } + } + } + Ok(()) +} + +fn validate_support_union( + destination: &CurrentCoreKey, + parents: [&CurrentCoreKey; 2], +) -> RusticolResult<()> { + let left = parents[0].support_source_slots(); + let right = parents[1].support_source_slots(); + let destination_support = destination.support_source_slots(); + if left + .len() + .checked_add(right.len()) + .filter(|combined| *combined == destination_support.len()) + .is_none() + { + return Err(integrity( + "contact-orbit parent supports do not reproduce the destination", + )); + } + let (mut left_index, mut right_index) = (0_usize, 0_usize); + for expected in destination_support.iter().copied() { + let next = match (left.get(left_index), right.get(right_index)) { + (Some(left_value), Some(right_value)) => match left_value.cmp(right_value) { + Ordering::Less => { + left_index += 1; + *left_value + } + Ordering::Greater => { + right_index += 1; + *right_value + } + Ordering::Equal => { + return Err(integrity("contact-orbit parent supports are not disjoint")); + } + }, + (Some(left_value), None) => { + left_index += 1; + *left_value + } + (None, Some(right_value)) => { + right_index += 1; + *right_value + } + (None, None) => { + return Err(integrity( + "contact-orbit parent supports do not reproduce the destination", + )); + } + }; + if next != expected { + return Err(integrity( + "contact-orbit parent supports do not reproduce the destination", + )); + } + } + Ok(()) +} + +fn topology_domain_matches_contact_orbit_side( + parent: ContactOrbitParentTopologyDomain<'_>, + side: ContactOrbitParentSide, +) -> bool { + if parent.current_state_template_id != side.expected_state_template_id + || parent.support_source_slots.is_empty() + { + return false; + } + match side.source_particle_leg { + -1 => parent.is_current, + // A physical contact leg may be fed either by an external source or by + // an ordinary off-shell current. Its certified leg number identifies + // a vertex slot, not an external-process source slot. + leg if leg >= 0 => parent.is_source || parent.is_current, + _ => false, + } +} + +fn contact_orbit_parent_topology_mapping( + step: &ContactOrbitStepProof, + parents: [ContactOrbitParentTopologyDomain<'_>; 2], + input_state_template_ids: [u32; 2], +) -> RusticolResult> { + let sides = [ + ContactOrbitParentSide { + expected_state_template_id: input_state_template_ids[0], + covered_legs: step.left_covered_legs, + source_particle_leg: step.source_particle_legs[0], + assignment: canonical_physical_assignment( + step.left_covered_legs, + &step.physical_leg_equivalence_classes, + step.source_particle_legs[0], + )?, + }, + ContactOrbitParentSide { + expected_state_template_id: input_state_template_ids[1], + covered_legs: step.right_covered_legs, + source_particle_leg: step.source_particle_legs[1], + assignment: canonical_physical_assignment( + step.right_covered_legs, + &step.physical_leg_equivalence_classes, + step.source_particle_legs[1], + )?, + }, + ]; + let direct = topology_domain_matches_contact_orbit_side(parents[0], sides[0]) + && topology_domain_matches_contact_orbit_side(parents[1], sides[1]); + let exchanged = topology_domain_matches_contact_orbit_side(parents[1], sides[0]) + && topology_domain_matches_contact_orbit_side(parents[0], sides[1]); + let parent_by_side = match (direct, exchanged) { + (true, false) => [0_usize, 1_usize], + (false, true) => [1_usize, 0_usize], + (true, true) + if sides[0].expected_state_template_id == sides[1].expected_state_template_id + && sides[0].assignment == sides[1].assignment => + { + [0_usize, 1_usize] + } + (true, true) => { + return Err(integrity( + "contact-orbit parent-to-side mapping is ambiguous across distinct physical assignments", + )); + } + (false, false) => return Ok(None), + }; + Ok(Some((sides, parent_by_side))) +} + +fn contact_orbit_parent_mapping( + step: &ContactOrbitStepProof, + parents: [&CurrentCoreKey; 2], + input_state_template_ids: [u32; 2], +) -> RusticolResult> { + contact_orbit_parent_topology_mapping( + step, + parents.map(ContactOrbitParentTopologyDomain::from_current_key), + input_state_template_ids, + ) +} + +pub(super) fn contact_orbit_owner_candidate<'a>( + step: Option<&'a ContactOrbitStepProof>, + destination: &'a CurrentCoreKey, + parents: [&'a CurrentCoreKey; 2], + input_state_template_ids: [u32; 2], + application: &'a ContactOrbitApplicationWitness, + transition_semantic_digest: SemanticDigest, +) -> RusticolResult>> { + let Some(step) = step else { + return Ok(None); + }; + validate_step(step)?; + validate_support_union(destination, parents)?; + let (sides, parent_by_side) = + contact_orbit_parent_mapping(step, parents, input_state_template_ids)? + .ok_or_else(|| integrity("contact-orbit parent-to-side mapping is absent"))?; + let mut canonical_parents = [ + ContactOrbitParentGroup { + current: ContactOrbitCurrentPhysics::from_current(parents[parent_by_side[0]]), + assignment: sides[0].assignment, + }, + ContactOrbitParentGroup { + current: ContactOrbitCurrentPhysics::from_current(parents[parent_by_side[1]]), + assignment: sides[1].assignment, + }, + ]; + canonical_parents.sort_unstable(); + let result_leg_equivalence_class = + step.physical_leg_equivalence_classes[step.result_leg as usize]; + let application_exact_factor = application + .transition_exact_factor + .checked_mul(application.color_exact_factor)? + .checked_mul(application.witness_exact_factor)?; + Ok(Some(ContactOrbitOwnerCandidate { + group: ContactOrbitOwnerGroupKey { + certificate_id: step.certificate_id, + certificate_semantic_digest: step.certificate_semantic_digest, + stage: step.stage, + result_leg_equivalence_class, + destination, + parents: canonical_parents, + output_source_particle_equivalence_class: physical_leg_equivalence_class( + step.source_particle_legs[2], + &step.physical_leg_equivalence_classes, + "output source particle leg", + )?, + evaluator_class: &step.evaluator_class, + coupling_parameter_ids: &application.coupling_parameter_ids, + binding_coupling: application.binding_coupling, + application_exact_factor, + input_exchange_factor: application.input_exchange_factor, + certificate_reconstruction_factor: step.certificate_reconstruction_factor, + step_reconstruction_factor: step.step_reconstruction_factor, + }, + rank: ContactOrbitOwnerRank { + destination, + parents, + oriented_covered_legs: [step.left_covered_legs, step.right_covered_legs], + oriented_source_particle_legs: step.source_particle_legs, + application, + step_semantic_digest: step.step_semantic_digest, + transition_semantic_digest, + }, + })) +} + +fn compare_items( + left: &(T, Option>), + right: &(T, Option>), +) -> Ordering { + match (&left.1, &right.1) { + (None, None) => left.0.cmp(&right.0), + (None, Some(_)) => Ordering::Less, + (Some(_), None) => Ordering::Greater, + (Some(left_candidate), Some(right_candidate)) => left_candidate + .group + .cmp(&right_candidate.group) + .then_with(|| left_candidate.rank.cmp(&right_candidate.rank)) + .then_with(|| left.0.cmp(&right.0)), + } +} + +/// Select one exact owner per certified group without mutating caller state. +/// +/// The exact-size iterator is copied into one fallibly reserved scratch vector, +/// then sorted in place. The returned token vector is separately reserved +/// before any selection, so every allocation failure occurs before caller-owned +/// construction state can be replaced. +pub(super) fn selected_contact_orbit_owner_tokens<'a, T, I>(items: I) -> RusticolResult> +where + T: Ord, + I: IntoIterator>)>, + I::IntoIter: ExactSizeIterator, +{ + let iterator = items.into_iter(); + let item_count = iterator.len(); + item_count + .checked_mul(size_of::<(T, Option>)>()) + .ok_or_else(|| integrity("contact-orbit owner scratch byte size overflows usize"))?; + let mut scratch = Vec::new(); + scratch.try_reserve_exact(item_count).map_err(|error| { + allocation(format!( + "contact-orbit owner scratch allocation failed: {error}" + )) + })?; + for item in iterator { + if scratch.len() == item_count { + return Err(integrity( + "contact-orbit exact-size owner iterator exceeded its declared length", + )); + } + scratch.push(item); + } + if scratch.len() != item_count { + return Err(integrity( + "contact-orbit exact-size owner iterator changed length", + )); + } + + let mut selected = Vec::new(); + selected.try_reserve_exact(item_count).map_err(|error| { + allocation(format!( + "contact-orbit selected-owner allocation failed: {error}" + )) + })?; + scratch.sort_unstable_by(compare_items); + let mut iterator = scratch.into_iter().peekable(); + while let Some((token, candidate)) = iterator.next() { + let Some(candidate) = candidate else { + selected.push(token); + continue; + }; + while let Some((next_token, Some(next_candidate))) = iterator.peek() { + if next_candidate.group != candidate.group { + break; + } + if next_candidate.rank == candidate.rank && next_token != &token { + return Err(integrity( + "contact-orbit owner candidates have a conflicting exact rank", + )); + } + iterator.next(); + } + selected.push(token); + } + selected.sort_unstable(); + if selected.windows(2).any(|pair| pair[0] == pair[1]) { + return Err(integrity( + "contact-orbit owner selection produced duplicate tokens", + )); + } + Ok(selected) +} + +#[cfg(test)] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(super) enum ContactOrbitTestBinding { + None, + One, + Two, + MissingStep, + DigestMismatch, +} + +#[cfg(test)] +pub(super) fn contact_orbit_test_template( + binding: ContactOrbitTestBinding, +) -> OwnedRecurrenceTemplateInput { + use super::{ + template::{ContactOrbitCertificateRow, ContactOrbitStepRow, DigestCatalogRow}, + validated_template_fixture, + }; + + fn string_id(input: &OwnedRecurrenceTemplateInput, value: &str) -> u32 { + input + .string_ranges + .iter() + .position(|range| { + let range = range + .as_usize_range(input.string_bytes.len(), "test string") + .unwrap(); + input.string_bytes[range] == *value.as_bytes() + }) + .unwrap() as u32 + } + + fn u32_sequence_id(input: &OwnedRecurrenceTemplateInput, values: &[u32]) -> u32 { + input + .u32_sequence_ranges + .iter() + .position(|row| { + let range = row + .range + .as_usize_range(input.u32_sequence_values.len(), "test u32 sequence") + .unwrap(); + &input.u32_sequence_values[range] == values + }) + .unwrap() as u32 + } + + fn i32_sequence_id(input: &OwnedRecurrenceTemplateInput, values: &[i32]) -> u32 { + input + .i32_sequence_ranges + .iter() + .position(|row| { + let range = row + .range + .as_usize_range(input.i32_sequence_values.len(), "test i32 sequence") + .unwrap(); + &input.i32_sequence_values[range] == values + }) + .unwrap() as u32 + } + + fn append_digest(input: &mut OwnedRecurrenceTemplateInput, byte: u8) -> u32 { + let id = input.digest_catalog.len() as u32; + input.digest_catalog.push(DigestCatalogRow { + id, + value: [byte; 32], + }); + id + } + + let mut input = validated_template_fixture().into_input(); + if binding == ContactOrbitTestBinding::None { + return input; + } + + let algorithm = string_id(&input, "compiler-certified-contact-orbit"); + let evaluator_class = string_id( + &input, + "constant-scalar-literal-singlet-self-conjugate-boson", + ); + let certificate_template = string_id(&input, "any"); + let first_step_template = string_id(&input, "basis"); + let second_step_template = string_id(&input, "component"); + let particle = string_id(&input, "scalar"); + let vertex = particle; + let particles = u32_sequence_id(&input, &[particle, particle, particle, particle]); + let equivalence = u32_sequence_id(&input, &[0, 0, 0, 0]); + let left_first = u32_sequence_id(&input, &[0]); + let right_first = u32_sequence_id(&input, &[1]); + let left_second = right_first; + let right_second = left_first; + let source_first = i32_sequence_id(&input, &[0, 1, -1]); + let source_second = i32_sequence_id(&input, &[1, 0, -1]); + let certificate_digest = append_digest(&mut input, 26); + let first_step_digest = append_digest(&mut input, 27); + let second_step_digest = append_digest(&mut input, 28); + input.contact_orbit_certificates = vec![ContactOrbitCertificateRow { + id: 0, + template_string_id: certificate_template, + algorithm_string_id: algorithm, + algorithm_version: 2, + term_id: 0, + vertex_string_id: vertex, + particle_string_sequence_id: particles, + evaluator_class_string_id: evaluator_class, + physical_leg_equivalence_sequence_id: equivalence, + reconstruction_factor_id: 0, + semantic_digest_id: certificate_digest, + }]; + input.contact_orbit_steps = vec![ + ContactOrbitStepRow { + id: 0, + template_string_id: first_step_template, + certificate_id: 0, + stage: ContactOrbitStage::Partial as u8, + result_leg: 2, + left_covered_leg_sequence_id: left_first, + right_covered_leg_sequence_id: right_first, + source_particle_leg_sequence_id: source_first, + reconstruction_factor_id: 0, + semantic_digest_id: first_step_digest, + }, + ContactOrbitStepRow { + id: 1, + template_string_id: second_step_template, + certificate_id: 0, + stage: ContactOrbitStage::Partial as u8, + result_leg: 2, + left_covered_leg_sequence_id: left_second, + right_covered_leg_sequence_id: right_second, + source_particle_leg_sequence_id: source_second, + reconstruction_factor_id: 0, + semantic_digest_id: second_step_digest, + }, + ]; + input.catalog_header[0].contact_orbit_certificate_count = 1; + input.catalog_header[0].contact_orbit_step_count = 2; + let (step_ids, digest_ids) = match binding { + ContactOrbitTestBinding::None => unreachable!(), + ContactOrbitTestBinding::One => (vec![0], vec![first_step_digest]), + ContactOrbitTestBinding::Two => (vec![0, 1], vec![first_step_digest, second_step_digest]), + ContactOrbitTestBinding::MissingStep => (vec![2], vec![first_step_digest]), + ContactOrbitTestBinding::DigestMismatch => (vec![0], vec![second_step_digest]), + }; + input.transitions[0].contact_orbit_step_sequence_id = u32_sequence_id(&input, &step_ids); + input.transitions[0].contact_orbit_step_semantic_digest_sequence_id = + u32_sequence_id(&input, &digest_ids); + input +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::recurrence::{ + CanonicalMomentumLinearForm, CurrentHelicityIdentity, CurrentSourceBinding, + DynamicLCColorStateId, ExactRational, MomentumTerm, RecurrenceNodeKind, + SourceStateAssignment, + }; + + fn digest(value: u8) -> SemanticDigest { + SemanticDigest::new([value; 32]).unwrap() + } + + fn exact(value: i128) -> ExactComplexRational { + ExactComplexRational::new(ExactRational::new(value, 1).unwrap(), ExactRational::ZERO) + } + + fn exact_ratio(numerator: i128, denominator: i128) -> ExactComplexRational { + ExactComplexRational::new( + ExactRational::new(numerator, denominator).unwrap(), + ExactRational::ZERO, + ) + } + + fn current(state: u32, color: u32, support: &[u32]) -> CurrentCoreKey { + let is_source = support.len() == 1; + current_with_physics( + if is_source { + RecurrenceNodeKind::Source + } else { + RecurrenceNodeKind::Current + }, + state, + color, + support, + if is_source { + RecurrenceStrategy::TopologyReplay + } else { + RecurrenceStrategy::AllFlowUnion + }, + 0, + vec![state as i32], + 0, + vec![0], + if is_source { + CurrentSourceBinding::FixedTemplate(support[0]) + } else { + CurrentSourceBinding::None + }, + None, + ) + } + + #[allow(clippy::too_many_arguments)] + fn current_with_physics( + node_kind: RecurrenceNodeKind, + state: u32, + color: u32, + support: &[u32], + strategy: RecurrenceStrategy, + spin_state_class: i32, + flavour_flow: Vec, + quantum_number_flow_id: u32, + coupling_orders: Vec, + source_binding: CurrentSourceBinding, + propagator_template_id: Option, + ) -> CurrentCoreKey { + let helicity_identity = match strategy { + RecurrenceStrategy::TopologyReplay => CurrentHelicityIdentity::topology_replay( + spin_state_class, + support + .iter() + .copied() + .map(|source_slot| SourceStateAssignment::new(source_slot, 0)) + .collect(), + ) + .unwrap(), + RecurrenceStrategy::AllFlowUnion => { + CurrentHelicityIdentity::all_flow_union(spin_state_class) + } + RecurrenceStrategy::ContractedColorUnion => { + CurrentHelicityIdentity::contracted_color_union( + spin_state_class, + support + .iter() + .copied() + .map(|source_slot| SourceStateAssignment::new(source_slot, 0)) + .collect(), + ) + .unwrap() + } + }; + CurrentCoreKey::new( + digest(1), + node_kind, + state, + DynamicLCColorStateId::from_interner(color), + support.to_vec(), + CanonicalMomentumLinearForm::new( + support + .iter() + .copied() + .map(|source_slot| MomentumTerm { + source_slot, + coefficient: 1, + }) + .collect(), + ) + .unwrap(), + helicity_identity, + flavour_flow, + quantum_number_flow_id, + coupling_orders, + source_binding, + propagator_template_id, + ) + .unwrap() + } + + fn topology_current(state: u32, color: u32, support: &[u32]) -> CurrentCoreKey { + let is_source = support.len() == 1; + current_with_physics( + if is_source { + RecurrenceNodeKind::Source + } else { + RecurrenceNodeKind::Current + }, + state, + color, + support, + RecurrenceStrategy::TopologyReplay, + 0, + vec![state as i32], + 0, + vec![0], + if is_source { + CurrentSourceBinding::FixedTemplate(0) + } else { + CurrentSourceBinding::None + }, + None, + ) + } + + fn topology_source(source_slot: u32, source_template_id: u32) -> CurrentCoreKey { + current_with_physics( + RecurrenceNodeKind::Source, + 1, + 1, + &[source_slot], + RecurrenceStrategy::TopologyReplay, + 0, + vec![1], + 0, + vec![0], + CurrentSourceBinding::FixedTemplate(source_template_id), + None, + ) + } + + fn application() -> ContactOrbitApplicationWitness { + ContactOrbitApplicationWitness { + transition_equivalence_class: "constant-scalar-contact".into(), + quantum_semantic_digest: digest(2), + color_contraction_semantic_digest: digest(3), + color_witness_term_id: LCColorWitnessTermId::new(4, 0), + color_witness_proof_digest: digest(5), + coupling_orders: vec![1], + coupling_parameter_ids: vec![2], + momentum_convention: vec![0, 1], + binding_coupling: ExactComplexRational::ONE, + transition_exact_factor: ExactComplexRational::ONE, + color_exact_factor: ExactComplexRational::ONE, + witness_exact_factor: ExactComplexRational::ONE, + input_exchange_factor: None, + output_factor_source: 0, + evaluator_contract_kind: EvaluatorContractKind::Vertex as u8, + evaluator_callable_kind: EvaluatorCallableKind::PreparedKernel as u8, + evaluator_callable_signature: digest(6), + evaluator_input_layout: vec![10, 11], + evaluator_output_layout: vec![12], + evaluator_exact_expression_digests: vec![digest(7)], + evaluator_runtime_template: None, + output_projection_id: 8, + } + } + + fn prepare_test_contact_orbit( + input: &OwnedRecurrenceTemplateInput, + ) -> RusticolResult> { + let catalog = TemplateCatalog::new(input)?; + let transition = input.transitions[0]; + let quantum = input.quantum_flows[transition.quantum_flow_template_id as usize]; + let contraction = + input.color_contractions[transition.color_contraction_template_id as usize]; + prepare_contact_orbit_transition( + input, + &catalog, + transition, + catalog.digest(quantum.semantic_digest_id, "test quantum semantic")?, + &catalog.coupling_orders(transition.coupling_order_set_id)?, + catalog.factor( + transition.binding_coupling_factor_id, + "test binding coupling", + )?, + catalog.factor(transition.exact_factor_id, "test transition exact")?, + catalog.factor( + contraction.exact_coefficient_factor_id, + "test contraction exact", + )?, + None, + catalog.witness_rows(transition.color_contraction_template_id)?, + ) + } + + #[test] + fn strict_prepared_contact_orbit_decode_is_zero_or_one_and_fail_closed() { + let none = contact_orbit_test_template(ContactOrbitTestBinding::None); + assert!(prepare_test_contact_orbit(&none).unwrap().is_none()); + + let one = contact_orbit_test_template(ContactOrbitTestBinding::One) + .validate() + .unwrap(); + assert!(prepare_test_contact_orbit(one.input()).unwrap().is_some()); + + let two = contact_orbit_test_template(ContactOrbitTestBinding::Two) + .validate() + .unwrap(); + assert!( + prepare_test_contact_orbit(two.input()) + .unwrap_err() + .to_string() + .contains("exactly one contact-orbit step") + ); + + let missing = contact_orbit_test_template(ContactOrbitTestBinding::MissingStep); + assert!( + prepare_test_contact_orbit(&missing) + .unwrap_err() + .to_string() + .contains("contact-orbit step is absent") + ); + + let mismatch = contact_orbit_test_template(ContactOrbitTestBinding::DigestMismatch); + assert!( + prepare_test_contact_orbit(&mismatch) + .unwrap_err() + .to_string() + .contains("digest binding is stale") + ); + } + + fn partial_step(left: u32, right: u32, result: u32, digest_byte: u8) -> ContactOrbitStepProof { + ContactOrbitStepProof { + certificate_id: 0, + certificate_semantic_digest: digest(9), + step_semantic_digest: digest(digest_byte), + stage: ContactOrbitStage::Partial, + result_leg: result, + physical_leg_equivalence_classes: vec![0, 0, 0, 0], + left_covered_legs: ContactOrbitCoveredLegs::new(&[left]).unwrap(), + right_covered_legs: ContactOrbitCoveredLegs::new(&[right]).unwrap(), + source_particle_legs: [left as i32, right as i32, -1], + certificate_reconstruction_factor: ExactComplexRational::ONE, + step_reconstruction_factor: ExactComplexRational::ONE, + evaluator_class: "constant-scalar-contact-v1".into(), + } + } + + fn final_step( + left: &[u32], + right: &[u32], + result: u32, + digest_byte: u8, + equivalence_classes: [u32; 4], + ) -> ContactOrbitStepProof { + ContactOrbitStepProof { + certificate_id: 0, + certificate_semantic_digest: digest(9), + step_semantic_digest: digest(digest_byte), + stage: ContactOrbitStage::Final, + result_leg: result, + physical_leg_equivalence_classes: equivalence_classes.to_vec(), + left_covered_legs: ContactOrbitCoveredLegs::new(left).unwrap(), + right_covered_legs: ContactOrbitCoveredLegs::new(right).unwrap(), + source_particle_legs: [ + if left.len() == 2 { -1 } else { left[0] as i32 }, + if right.len() == 2 { + -1 + } else { + right[0] as i32 + }, + result as i32, + ], + certificate_reconstruction_factor: ExactComplexRational::ONE, + step_reconstruction_factor: ExactComplexRational::ONE, + evaluator_class: "constant-scalar-contact-v1".into(), + } + } + + fn five_leg_final_step( + left: &[u32], + right: &[u32], + result: u32, + digest_byte: u8, + ) -> ContactOrbitStepProof { + ContactOrbitStepProof { + certificate_id: 0, + certificate_semantic_digest: digest(9), + step_semantic_digest: digest(digest_byte), + stage: ContactOrbitStage::Final, + result_leg: result, + physical_leg_equivalence_classes: vec![0, 0, 0, 0, 1], + left_covered_legs: ContactOrbitCoveredLegs::new(left).unwrap(), + right_covered_legs: ContactOrbitCoveredLegs::new(right).unwrap(), + source_particle_legs: [-1, -1, result as i32], + certificate_reconstruction_factor: ExactComplexRational::ONE, + step_reconstruction_factor: ExactComplexRational::ONE, + evaluator_class: "scalar-heft-colored-contact-tree".into(), + } + } + + fn candidate<'a>( + step: &'a ContactOrbitStepProof, + destination: &'a CurrentCoreKey, + parents: [&'a CurrentCoreKey; 2], + application: &'a ContactOrbitApplicationWitness, + transition_digest: u8, + ) -> ContactOrbitOwnerCandidate<'a> { + contact_orbit_owner_candidate( + Some(step), + destination, + parents, + parents.map(|parent| parent.current_state_template_id()), + application, + digest(transition_digest), + ) + .unwrap() + .unwrap() + } + + fn candidate_with_input_states<'a>( + step: &'a ContactOrbitStepProof, + destination: &'a CurrentCoreKey, + parents: [&'a CurrentCoreKey; 2], + input_state_template_ids: [u32; 2], + application: &'a ContactOrbitApplicationWitness, + transition_digest: u8, + ) -> RusticolResult>> { + contact_orbit_owner_candidate( + Some(step), + destination, + parents, + input_state_template_ids, + application, + digest(transition_digest), + ) + } + + #[test] + fn owner_group_forgets_only_left_right_traversal() { + let left = current(1, 1, &[10]); + let right = current(1, 1, &[11]); + let destination = current(2, 2, &[10, 11]); + let application = application(); + let forward_step = partial_step(0, 1, 2, 10); + let reverse_step = partial_step(1, 0, 2, 11); + let forward = candidate( + &forward_step, + &destination, + [&left, &right], + &application, + 12, + ); + let reverse = candidate( + &reverse_step, + &destination, + [&right, &left], + &application, + 13, + ); + + assert_eq!(forward.group, reverse.group); + assert_ne!(forward.rank, reverse.rank); + assert_eq!( + selected_contact_orbit_owner_tokens([(1_u32, Some(reverse)), (0_u32, Some(forward)),]) + .unwrap(), + vec![0], + ); + assert_eq!( + selected_contact_orbit_owner_tokens([(0_u32, Some(forward)), (1_u32, Some(reverse)),]) + .unwrap(), + vec![0], + ); + } + + #[test] + fn parent_assignment_binds_evaluator_swaps_to_certified_final_sides() { + let pair = current(20, 2, &[10, 11]); + let source = current(30, 1, &[12]); + let destination = current(40, 3, &[10, 11, 12]); + let application = application(); + let forward_step = final_step(&[0, 1], &[2], 3, 20, [0, 0, 1, 2]); + let mirror_step = final_step(&[2], &[0, 1], 3, 21, [0, 0, 1, 2]); + + let forward = candidate_with_input_states( + &forward_step, + &destination, + [&source, &pair], + [20, 30], + &application, + 22, + ) + .unwrap() + .unwrap(); + let mirror = candidate_with_input_states( + &mirror_step, + &destination, + [&pair, &source], + [30, 20], + &application, + 23, + ) + .unwrap() + .unwrap(); + + assert_eq!(forward.group, mirror.group); + assert_ne!(forward.rank, mirror.rank); + } + + #[test] + fn parent_assignment_handles_distinct_states_and_identical_ambiguity() { + let left = current(10, 1, &[10]); + let right = current(20, 1, &[11]); + let destination = current(30, 2, &[10, 11]); + let application = application(); + let distinct_step = partial_step(0, 1, 2, 24); + let direct = candidate_with_input_states( + &distinct_step, + &destination, + [&left, &right], + [10, 20], + &application, + 25, + ) + .unwrap() + .unwrap(); + let swapped = candidate_with_input_states( + &distinct_step, + &destination, + [&right, &left], + [10, 20], + &application, + 26, + ) + .unwrap() + .unwrap(); + assert_eq!(direct.group, swapped.group); + + let identical_right = current(10, 1, &[11]); + let identical_step = partial_step(0, 1, 2, 27); + assert!( + candidate_with_input_states( + &identical_step, + &destination, + [&left, &identical_right], + [10, 10], + &application, + 28, + ) + .unwrap() + .is_some() + ); + } + + #[test] + fn parent_assignment_rejects_ambiguous_or_invalid_side_semantics() { + let left = current(10, 1, &[10]); + let right = current(10, 1, &[11]); + let destination = current(30, 2, &[10, 11]); + let application = application(); + let mut non_equivalent = partial_step(0, 1, 2, 29); + non_equivalent.physical_leg_equivalence_classes = vec![0, 1, 2, 3]; + assert!( + candidate_with_input_states( + &non_equivalent, + &destination, + [&left, &right], + [10, 10], + &application, + 30, + ) + .unwrap_err() + .to_string() + .contains("mapping is ambiguous") + ); + + let step = partial_step(0, 1, 2, 31); + assert!( + candidate_with_input_states( + &step, + &destination, + [&left, &right], + [90, 91], + &application, + 32, + ) + .unwrap_err() + .to_string() + .contains("mapping is absent") + ); + let internal_leg = current_with_physics( + RecurrenceNodeKind::Current, + 10, + 1, + &[10], + RecurrenceStrategy::TopologyReplay, + 0, + vec![10], + 0, + vec![0], + CurrentSourceBinding::None, + None, + ); + assert!( + candidate_with_input_states( + &step, + &destination, + [&internal_leg, &right], + [10, 10], + &application, + 33, + ) + .unwrap() + .is_some() + ); + let pair = current(10, 1, &[11, 12]); + let wide_destination = current(30, 2, &[10, 11, 12]); + assert!( + candidate_with_input_states( + &step, + &wide_destination, + [&left, &pair], + [10, 10], + &application, + 34, + ) + .unwrap() + .is_some() + ); + } + + #[test] + fn prepared_parent_domain_accepts_sources_and_internal_physical_legs() { + let source_a = current(10, 1, &[10]); + let source_b = current(10, 1, &[11]); + let source_c = current(10, 1, &[12]); + let composite_scalar = current(10, 1, &[10, 11]); + let auxiliary_pair = current(20, 1, &[10, 11]); + + let partial = prepared_contact_orbit_transition_for_test( + partial_step(0, 1, 2, 35), + [10, 10], + digest(36), + application(), + ); + assert!( + partial + .accepts_parent_domain([&source_a, &source_b]) + .unwrap() + ); + assert!( + partial + .accepts_parent_domain([&composite_scalar, &source_c]) + .unwrap() + ); + for parents in [[&source_a, &source_b], [&composite_scalar, &source_c]] { + assert_eq!( + partial.accepts_parent_domain(parents).unwrap(), + partial + .accepts_parent_topology_domain( + parents.map(ContactOrbitParentTopologyDomain::from_current_key), + ) + .unwrap(), + "query-local and process-global topology admission diverged" + ); + } + + let final_transition = prepared_contact_orbit_transition_for_test( + final_step(&[0, 1], &[2], 3, 37, [0, 0, 0, 0]), + [20, 10], + digest(38), + application(), + ); + assert!( + final_transition + .accepts_parent_domain([&auxiliary_pair, &source_c]) + .unwrap() + ); + + let mut ambiguous_step = partial_step(0, 1, 2, 39); + ambiguous_step.physical_leg_equivalence_classes = vec![0, 1, 2, 3]; + let ambiguous = prepared_contact_orbit_transition_for_test( + ambiguous_step, + [10, 10], + digest(40), + application(), + ); + assert!( + ambiguous + .accepts_parent_domain([&source_a, &source_b]) + .unwrap_err() + .to_string() + .contains("mapping is ambiguous") + ); + } + + #[test] + fn certified_four_identical_scalar_channels_keep_assignment_multiplicity() { + let left = current(1, 1, &[10]); + let right = current(1, 1, &[11]); + let destination = current(2, 2, &[10, 11]); + let application = application(); + let forward_steps = [ + partial_step(0, 1, 2, 20), + partial_step(0, 2, 1, 21), + partial_step(0, 3, 1, 22), + ]; + let reverse_steps = [ + partial_step(1, 0, 2, 30), + partial_step(2, 0, 1, 31), + partial_step(3, 0, 1, 32), + ]; + let candidates = [ + ( + 0_u32, + Some(candidate( + &forward_steps[0], + &destination, + [&left, &right], + &application, + 40, + )), + ), + ( + 1_u32, + Some(candidate( + &reverse_steps[0], + &destination, + [&right, &left], + &application, + 41, + )), + ), + ( + 2_u32, + Some(candidate( + &forward_steps[1], + &destination, + [&left, &right], + &application, + 42, + )), + ), + ( + 3_u32, + Some(candidate( + &reverse_steps[1], + &destination, + [&right, &left], + &application, + 43, + )), + ), + ( + 4_u32, + Some(candidate( + &forward_steps[2], + &destination, + [&left, &right], + &application, + 44, + )), + ), + ( + 5_u32, + Some(candidate( + &reverse_steps[2], + &destination, + [&right, &left], + &application, + 45, + )), + ), + ]; + assert_eq!( + candidates + .iter() + .map(|(_, value)| value.unwrap().group) + .collect::>() + .len(), + 1, + ); + assert_eq!( + selected_contact_orbit_owner_tokens(candidates).unwrap(), + vec![0], + ); + + let singles = [ + current(3, 3, &[10]), + current(3, 3, &[11]), + current(3, 3, &[12]), + ]; + let pairs = [ + current(4, 4, &[11, 12]), + current(4, 4, &[10, 12]), + current(4, 4, &[10, 11]), + ]; + let final_destination = current(5, 5, &[10, 11, 12]); + let final_forward = final_step(&[3], &[1, 2], 0, 60, [0, 0, 0, 0]); + let final_reverse = final_step(&[1, 2], &[3], 0, 70, [0, 0, 0, 0]); + let final_candidates = singles + .iter() + .zip(pairs.iter()) + .enumerate() + .flat_map(|(index, (single, pair))| { + let index = u32::try_from(index).unwrap(); + [ + ( + 2 * index, + Some(candidate( + &final_forward, + &final_destination, + [single, pair], + &application, + 80 + index as u8, + )), + ), + ( + 2 * index + 1, + Some(candidate( + &final_reverse, + &final_destination, + [pair, single], + &application, + 90 + index as u8, + )), + ), + ] + }) + .collect::>(); + assert_eq!( + final_candidates[0].1.unwrap().group, + final_candidates[1].1.unwrap().group + ); + assert_eq!( + final_candidates[2].1.unwrap().group, + final_candidates[3].1.unwrap().group + ); + assert_eq!( + final_candidates[4].1.unwrap().group, + final_candidates[5].1.unwrap().group + ); + assert_ne!( + final_candidates[0].1.unwrap().group, + final_candidates[2].1.unwrap().group + ); + assert_ne!( + final_candidates[0].1.unwrap().group, + final_candidates[4].1.unwrap().group + ); + assert_ne!( + final_candidates[2].1.unwrap().group, + final_candidates[4].1.unwrap().group + ); + let forward_selection = + selected_contact_orbit_owner_tokens(final_candidates.iter().copied()).unwrap(); + let reverse_selection = + selected_contact_orbit_owner_tokens(final_candidates.iter().rev().copied()).unwrap(); + assert_eq!(forward_selection, reverse_selection); + assert_eq!(forward_selection.len(), 3,); + } + + #[test] + fn five_leg_auxiliary_partitions_keep_physical_assignment_multiplicity() { + let application = application(); + let destination = current(40, 4, &[10, 11, 12, 13]); + let first_left = current(20, 2, &[10, 11]); + let first_right = current(30, 2, &[12, 13]); + let second_left = current(20, 2, &[10, 12]); + let second_right = current(30, 2, &[11, 13]); + let forward_step = five_leg_final_step(&[0, 1], &[2, 3], 4, 100); + let mirror_step = five_leg_final_step(&[2, 3], &[0, 1], 4, 101); + + let first = candidate_with_input_states( + &forward_step, + &destination, + [&first_left, &first_right], + [20, 30], + &application, + 102, + ) + .unwrap() + .unwrap(); + let first_mirror = candidate_with_input_states( + &mirror_step, + &destination, + [&first_right, &first_left], + [30, 20], + &application, + 103, + ) + .unwrap() + .unwrap(); + let second = candidate_with_input_states( + &forward_step, + &destination, + [&second_left, &second_right], + [20, 30], + &application, + 104, + ) + .unwrap() + .unwrap(); + + assert_eq!(first.group, first_mirror.group); + assert_ne!(first.group, second.group); + assert_eq!( + selected_contact_orbit_owner_tokens([ + (0_u32, Some(first)), + (1_u32, Some(first_mirror)), + (2_u32, Some(second)), + ]) + .unwrap(), + vec![0, 2], + ); + } + + #[test] + fn certified_owner_preserves_concrete_destination_across_contact_stages() { + let application = application(); + let partial = partial_step(0, 1, 2, 10); + let first_left = topology_current(1, 1, &[10]); + let first_right = topology_current(1, 1, &[11]); + let first_destination = topology_current(2, 2, &[10, 11]); + let second_left = topology_current(1, 1, &[20]); + let second_right = topology_current(1, 1, &[21]); + let second_destination = topology_current(2, 2, &[20, 21]); + let first = candidate( + &partial, + &first_destination, + [&first_left, &first_right], + &application, + 12, + ); + let second = candidate( + &partial, + &second_destination, + [&second_left, &second_right], + &application, + 12, + ); + + assert_ne!(first.group, second.group); + assert_ne!(first.rank, second.rank); + assert_eq!( + selected_contact_orbit_owner_tokens([(0_u32, Some(first)), (1_u32, Some(second)),]) + .unwrap(), + vec![0, 1], + ); + + let final_step = final_step(&[0], &[1, 2], 3, 20, [0, 0, 0, 0]); + let first_pair = topology_current(3, 3, &[11, 12]); + let first_final_destination = topology_current(4, 4, &[10, 11, 12]); + let second_pair = topology_current(3, 3, &[21, 22]); + let second_final_destination = topology_current(4, 4, &[20, 21, 22]); + let first_final = candidate( + &final_step, + &first_final_destination, + [&first_left, &first_pair], + &application, + 22, + ); + let second_final = candidate( + &final_step, + &second_final_destination, + [&second_left, &second_pair], + &application, + 22, + ); + + assert_ne!(first_final.group, second_final.group); + assert_ne!(first_final.rank, second_final.rank); + assert_eq!( + selected_contact_orbit_owner_tokens([ + (0_u32, Some(first_final)), + (1_u32, Some(second_final)), + ]) + .unwrap(), + vec![0, 1], + ); + } + + #[test] + fn same_order_singleton_rows_collapse_partial_and_final_scalar_0012_aliases() { + let scalar0_left = current(10, 1, &[10]); + let scalar0_right = current(10, 1, &[11]); + let partial_destination = current(20, 2, &[10, 11]); + let application = application(); + let mut partial_forward = partial_step(0, 1, 2, 50); + partial_forward.physical_leg_equivalence_classes = vec![0, 0, 1, 2]; + let mut partial_reverse = partial_step(1, 0, 2, 51); + partial_reverse.physical_leg_equivalence_classes = vec![0, 0, 1, 2]; + let partial_forward = candidate( + &partial_forward, + &partial_destination, + [&scalar0_left, &scalar0_right], + &application, + 52, + ); + let partial_reverse = candidate( + &partial_reverse, + &partial_destination, + [&scalar0_right, &scalar0_left], + &application, + 53, + ); + assert_eq!(partial_forward.group, partial_reverse.group); + assert_eq!( + selected_contact_orbit_owner_tokens([ + (0_u32, Some(partial_forward)), + (1_u32, Some(partial_reverse)), + ]) + .unwrap(), + vec![0], + ); + assert_eq!( + selected_contact_orbit_owner_tokens([ + (1_u32, Some(partial_reverse)), + (0_u32, Some(partial_forward)), + ]) + .unwrap(), + vec![0], + ); + + let pair = current(20, 2, &[10, 11]); + let scalar1 = current(30, 3, &[12]); + let final_destination = current(40, 4, &[10, 11, 12]); + let final_forward = final_step(&[0, 1], &[2], 3, 54, [0, 0, 1, 2]); + let final_reverse = final_step(&[2], &[0, 1], 3, 55, [0, 0, 1, 2]); + let final_forward = candidate( + &final_forward, + &final_destination, + [&pair, &scalar1], + &application, + 56, + ); + let final_reverse = candidate( + &final_reverse, + &final_destination, + [&scalar1, &pair], + &application, + 57, + ); + assert_eq!(final_forward.group, final_reverse.group); + // The concrete rank orders the source-first mirror traversal first; + // owner choice therefore stays deterministic independently of insertion. + assert_eq!( + selected_contact_orbit_owner_tokens([ + (0_u32, Some(final_forward)), + (1_u32, Some(final_reverse)), + ]) + .unwrap(), + vec![1], + ); + assert_eq!( + selected_contact_orbit_owner_tokens([ + (1_u32, Some(final_reverse)), + (0_u32, Some(final_forward)), + ]) + .unwrap(), + vec![1], + ); + } + + #[test] + fn owner_current_projection_retains_non_traversal_physics() { + let destination = current(2, 2, &[10, 11]); + let base = ContactOrbitCurrentPhysics::from_current(&destination); + let mut changed = base; + changed.current_state_template_id = 3; + assert_ne!(base, changed); + changed = base; + changed.dynamic_lc_color_state_id = DynamicLCColorStateId::from_interner(3); + assert_ne!(base, changed); + changed = base; + changed.recurrence_strategy = RecurrenceStrategy::TopologyReplay; + assert_ne!(base, changed); + changed = base; + changed.spin_state_class = 1; + assert_ne!(base, changed); + let different_flavour = [3]; + changed = base; + changed.flavour_flow = &different_flavour; + assert_ne!(base, changed); + changed = base; + changed.quantum_number_flow_id = 1; + assert_ne!(base, changed); + let different_orders = [1]; + changed = base; + changed.coupling_orders = &different_orders; + assert_ne!(base, changed); + changed = base; + changed.propagator_template_id = Some(1); + assert_ne!(base, changed); + + let source_a = topology_source(10, 7); + let source_b = topology_source(20, 8); + assert_ne!( + ContactOrbitCurrentPhysics::from_current(&source_a), + ContactOrbitCurrentPhysics::from_current(&source_b), + ); + } + + #[test] + fn owner_group_quotients_application_metadata_but_keeps_physics_boundaries() { + let left = current(1, 1, &[10]); + let right = current(1, 1, &[11]); + let destination = current(2, 2, &[10, 11]); + let step = partial_step(0, 1, 2, 10); + let base_application = application(); + let base = candidate(&step, &destination, [&left, &right], &base_application, 12); + + let mut traversal_application = base_application.clone(); + traversal_application.transition_equivalence_class = "alternate-traversal".into(); + traversal_application.quantum_semantic_digest = digest(20); + traversal_application.color_contraction_semantic_digest = digest(21); + traversal_application.color_witness_term_id = LCColorWitnessTermId::new(4, 1); + traversal_application.color_witness_proof_digest = digest(22); + traversal_application.momentum_convention = vec![1, 0]; + traversal_application.output_factor_source = 1; + traversal_application.evaluator_contract_kind = EvaluatorContractKind::Closure as u8; + traversal_application.evaluator_callable_kind = + EvaluatorCallableKind::RusticolTemplate as u8; + traversal_application.evaluator_callable_signature = digest(23); + traversal_application.evaluator_input_layout = vec![13, 14]; + traversal_application.evaluator_output_layout = vec![15]; + traversal_application.evaluator_exact_expression_digests = vec![digest(24)]; + traversal_application.evaluator_runtime_template = + Some("alternate-runtime-template".into()); + traversal_application.output_projection_id = 9; + let traversal = candidate( + &step, + &destination, + [&left, &right], + &traversal_application, + 13, + ); + + assert_eq!(base.group, traversal.group); + assert_ne!(base.rank, traversal.rank); + let forward = + selected_contact_orbit_owner_tokens([(0_u32, Some(base)), (1_u32, Some(traversal))]) + .unwrap(); + let reverse = + selected_contact_orbit_owner_tokens([(1_u32, Some(traversal)), (0_u32, Some(base))]) + .unwrap(); + assert_eq!(forward, reverse); + assert_eq!(forward.len(), 1); + + let mut redistributed_factors = base_application.clone(); + redistributed_factors.transition_exact_factor = exact(2); + redistributed_factors.color_exact_factor = exact(3); + redistributed_factors.witness_exact_factor = exact_ratio(1, 6); + let redistributed = candidate( + &step, + &destination, + [&left, &right], + &redistributed_factors, + 12, + ); + assert_eq!(base.group, redistributed.group); + assert_ne!(base.rank, redistributed.rank); + let forward = selected_contact_orbit_owner_tokens([ + (0_u32, Some(base)), + (1_u32, Some(redistributed)), + ]) + .unwrap(); + let reverse = selected_contact_orbit_owner_tokens([ + (1_u32, Some(redistributed)), + (0_u32, Some(base)), + ]) + .unwrap(); + assert_eq!(forward, reverse); + assert_eq!(forward.len(), 1); + + let mut different_product = redistributed_factors.clone(); + different_product.witness_exact_factor = exact_ratio(1, 5); + assert_ne!( + base.group, + candidate(&step, &destination, [&left, &right], &different_product, 12,).group, + ); + let mut different_binding = base_application.clone(); + different_binding.binding_coupling = exact(2); + assert_ne!( + base.group, + candidate(&step, &destination, [&left, &right], &different_binding, 12,).group, + ); + let mut different_exchange = base_application.clone(); + different_exchange.input_exchange_factor = Some(exact(-1)); + assert_ne!( + base.group, + candidate( + &step, + &destination, + [&left, &right], + &different_exchange, + 12, + ) + .group, + ); + + let mut different_parameters = base_application.clone(); + different_parameters.coupling_parameter_ids = vec![99]; + assert_ne!( + base.group, + candidate( + &step, + &destination, + [&left, &right], + &different_parameters, + 12, + ) + .group, + ); + + let mut certificate_row = step.clone(); + certificate_row.certificate_id = 1; + assert_ne!( + base.group, + candidate( + &certificate_row, + &destination, + [&left, &right], + &base_application, + 12, + ) + .group, + ); + let mut certificate = step.clone(); + certificate.certificate_semantic_digest = digest(30); + assert_ne!( + base.group, + candidate( + &certificate, + &destination, + [&left, &right], + &base_application, + 12, + ) + .group, + ); + let mut equivalence = step.clone(); + equivalence.physical_leg_equivalence_classes[step.result_leg as usize] = 1; + assert_ne!( + base.group, + candidate( + &equivalence, + &destination, + [&left, &right], + &base_application, + 12, + ) + .group, + ); + let mut parent_equivalence = step.clone(); + parent_equivalence.physical_leg_equivalence_classes[1] = 1; + assert!( + candidate_with_input_states( + &parent_equivalence, + &destination, + [&left, &right], + [ + left.current_state_template_id(), + right.current_state_template_id(), + ], + &base_application, + 12, + ) + .unwrap_err() + .to_string() + .contains("mapping is ambiguous"), + ); + let mut evaluator_class = step.clone(); + evaluator_class.evaluator_class = "different-contact-class".into(); + assert_ne!( + base.group, + candidate( + &evaluator_class, + &destination, + [&left, &right], + &base_application, + 12, + ) + .group, + ); + + let mut invalid_reconstruction = step.clone(); + invalid_reconstruction.certificate_reconstruction_factor = exact(2); + assert!( + contact_orbit_owner_candidate( + Some(&invalid_reconstruction), + &destination, + [&left, &right], + [ + left.current_state_template_id(), + right.current_state_template_id() + ], + &base_application, + digest(12), + ) + .unwrap_err() + .to_string() + .contains("exact unit reconstruction") + ); + invalid_reconstruction.certificate_reconstruction_factor = ExactComplexRational::ONE; + invalid_reconstruction.step_reconstruction_factor = exact(2); + assert!( + contact_orbit_owner_candidate( + Some(&invalid_reconstruction), + &destination, + [&left, &right], + [ + left.current_state_template_id(), + right.current_state_template_id() + ], + &base_application, + digest(12), + ) + .unwrap_err() + .to_string() + .contains("exact unit reconstruction") + ); + + let mut changed_stage = base; + changed_stage.group.stage = ContactOrbitStage::Final; + assert_ne!(base.group, changed_stage.group); + let mut changed_output_source = base; + changed_output_source + .group + .output_source_particle_equivalence_class = Some(2); + assert_ne!(base.group, changed_output_source.group); + } + + #[test] + fn passthrough_rank_conflict_duplicate_token_and_capacity_fail_closed() { + assert_eq!( + selected_contact_orbit_owner_tokens([(7_u32, None)]).unwrap(), + vec![7], + ); + + let left = current(1, 1, &[10]); + let right = current(1, 1, &[11]); + let destination = current(2, 2, &[10, 11]); + let application = application(); + let step = partial_step(0, 1, 2, 10); + let duplicate = candidate(&step, &destination, [&left, &right], &application, 12); + let error = selected_contact_orbit_owner_tokens([ + (0_u32, Some(duplicate)), + (1_u32, Some(duplicate)), + ]) + .unwrap_err(); + assert!(error.to_string().contains("conflicting exact rank")); + + let mut different_group_step = step.clone(); + different_group_step.evaluator_class = "different-contact-class".into(); + let different_group = candidate( + &different_group_step, + &destination, + [&left, &right], + &application, + 13, + ); + let error = selected_contact_orbit_owner_tokens([ + (0_u32, Some(duplicate)), + (0_u32, Some(different_group)), + ]) + .unwrap_err(); + assert!(error.to_string().contains("duplicate tokens")); + + struct ImpossibleExactSize<'a> { + remaining: usize, + marker: std::marker::PhantomData>, + } + impl<'a> Iterator for ImpossibleExactSize<'a> { + type Item = (u64, Option>); + + fn next(&mut self) -> Option { + None + } + + fn size_hint(&self) -> (usize, Option) { + (self.remaining, Some(self.remaining)) + } + } + impl ExactSizeIterator for ImpossibleExactSize<'_> { + fn len(&self) -> usize { + self.remaining + } + } + let error = selected_contact_orbit_owner_tokens(ImpossibleExactSize { + remaining: usize::MAX, + marker: std::marker::PhantomData, + }) + .unwrap_err(); + assert!(error.to_string().contains("byte size overflows usize")); + + struct UnderreportedExactSize { + emitted: u8, + } + impl Iterator for UnderreportedExactSize { + type Item = (u64, Option>); + + fn next(&mut self) -> Option { + if self.emitted == 2 { + return None; + } + let token = u64::from(self.emitted); + self.emitted += 1; + Some((token, None)) + } + + fn size_hint(&self) -> (usize, Option) { + (1, Some(1)) + } + } + impl ExactSizeIterator for UnderreportedExactSize { + fn len(&self) -> usize { + 1 + } + } + let error = + selected_contact_orbit_owner_tokens(UnderreportedExactSize { emitted: 0 }).unwrap_err(); + assert!(error.to_string().contains("exceeded its declared length")); + } +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/recurrence/diagnostic.rs b/pyamplicol/rust/crates/rusticol-core/src/recurrence/diagnostic.rs new file mode 100644 index 0000000000..4be1738db7 --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/recurrence/diagnostic.rs @@ -0,0 +1,318 @@ +// SPDX-License-Identifier: 0BSD + +//! Feature-only construction observer used to diagnose compact/established +//! recurrence disagreements. Nothing in this module is compiled into default +//! or release builds and none of its records participate in artifact identity. + +use std::cell::RefCell; +use std::collections::BTreeSet; + +use super::{ExactComplexRational, SemanticDigest, SourceStateAssignment}; +use crate::{RusticolError, RusticolResult}; + +#[derive(Clone, Debug, Eq, PartialEq)] +#[doc(hidden)] +pub struct ConstructionTransitionDiagnosticRowV1 { + pub materialized_sector_id: Option, + pub output_current_digest: SemanticDigest, + pub ordered_parent_digests: [SemanticDigest; 2], + pub transition_template_id: u32, + pub transition_semantic_digest: SemanticDigest, + pub evaluator_binding_semantic_digest: SemanticDigest, + pub result_state_template_id: u32, + pub quantum_flow_witness_id: u32, + pub quantum_semantic_digest: SemanticDigest, + pub color_contraction_template_id: u32, + pub color_witness_ordinal: u32, + pub color_witness_proof_digest: SemanticDigest, + pub output_projection_id: u32, + pub transition_factor: ExactComplexRational, + pub contraction_factor: ExactComplexRational, + pub output_factor: ExactComplexRational, + pub exchange_factor: ExactComplexRational, + pub witness_factor: ExactComplexRational, + pub reversal_mask: u8, + pub reversal_factor: ExactComplexRational, + pub candidate_factor: ExactComplexRational, + pub aggregate_factor_after: ExactComplexRational, + pub parent_reflection_proof_digests: [Option; 2], + pub parent_reflection_phases: [Option; 2], + pub local_reflection_proof_digest: Option, + pub local_reflection_phase: Option, + pub result_reflection_proof_digest: Option, + pub result_reflection_phase: Option, + pub output_color_orientation: String, +} + +#[derive(Clone, Debug)] +pub(crate) struct ConstructionDiagnosticSelectionV1 { + pub(crate) public_flow_id: u32, + pub(crate) public_helicities: Vec, +} + +pub(crate) fn representative_source_states_for_public_helicities( + representative_to_public: &[u32], + public_helicities: &[i32], + mut state_index_for_public_helicity: impl FnMut(u32, i32) -> RusticolResult, +) -> RusticolResult> { + if representative_to_public.len() != public_helicities.len() { + return Err(RusticolError::integrity( + "transition diagnostic replay permutation and public-helicity axis have different sizes", + )); + } + let mut seen_public_slots = vec![false; public_helicities.len()]; + representative_to_public + .iter() + .copied() + .enumerate() + .map(|(representative_slot, public_slot)| { + let public_index = usize::try_from(public_slot).map_err(|_| { + RusticolError::integrity("transition diagnostic public source slot exceeds usize") + })?; + let seen = seen_public_slots.get_mut(public_index).ok_or_else(|| { + RusticolError::integrity( + "transition diagnostic replay permutation is out of bounds", + ) + })?; + if *seen { + return Err(RusticolError::integrity( + "transition diagnostic replay permutation repeats a public source slot", + )); + } + *seen = true; + let representative_slot = u32::try_from(representative_slot).map_err(|_| { + RusticolError::integrity( + "transition diagnostic representative source slot exceeds u32", + ) + })?; + let state_index = state_index_for_public_helicity( + representative_slot, + public_helicities[public_index], + )?; + Ok(SourceStateAssignment::new(representative_slot, state_index)) + }) + .collect() +} + +#[derive(Debug)] +struct ConstructionDiagnosticObserverV1 { + rows: Vec, + selection: Option, + live_current_digests: Option>, + active_materialized_sector_id: Option, +} + +thread_local! { + static TRANSITION_OBSERVER: RefCell> = + const { RefCell::new(None) }; +} + +pub(crate) fn begin_transition_diagnostic_observation( + selection: Option, +) -> RusticolResult<()> { + TRANSITION_OBSERVER.with(|slot| { + let mut slot = slot.borrow_mut(); + if slot.is_some() { + return Err(RusticolError::integrity( + "recurrence transition diagnostic observation is already active", + )); + } + *slot = Some(ConstructionDiagnosticObserverV1 { + rows: Vec::new(), + selection, + live_current_digests: None, + active_materialized_sector_id: None, + }); + Ok(()) + }) +} + +pub(crate) fn transition_diagnostic_observation_active() -> bool { + TRANSITION_OBSERVER.with(|slot| slot.borrow().is_some()) +} + +#[allow(dead_code)] +pub(crate) fn observe_transition_diagnostic(mut row: ConstructionTransitionDiagnosticRowV1) { + TRANSITION_OBSERVER.with(|slot| { + if let Some(observer) = slot.borrow_mut().as_mut() { + row.materialized_sector_id = observer.active_materialized_sector_id; + observer.rows.push(row); + } + }); +} + +pub(crate) fn with_transition_diagnostic_materialized_sector( + materialized_sector_id: Option, + build: impl FnOnce() -> RusticolResult, +) -> RusticolResult { + let activated = TRANSITION_OBSERVER.with(|slot| { + let mut slot = slot.borrow_mut(); + let Some(observer) = slot.as_mut() else { + return Ok(false); + }; + let materialized_sector_id = materialized_sector_id.ok_or_else(|| { + RusticolError::integrity( + "transition diagnostic construction group is not one materialized sector", + ) + })?; + if observer + .active_materialized_sector_id + .replace(materialized_sector_id) + .is_some() + { + return Err(RusticolError::integrity( + "transition diagnostic materialized-sector context is already active", + )); + } + Ok(true) + })?; + let result = build(); + if activated { + TRANSITION_OBSERVER.with(|slot| { + if let Some(observer) = slot.borrow_mut().as_mut() { + observer.active_materialized_sector_id = None; + } + }); + } + result +} + +pub(crate) fn retain_materialized_sector_rows( + rows: Vec, + materialized_sector_id: u32, + sector_of: impl Fn(&T) -> Option, +) -> Vec { + rows.into_iter() + .filter(|row| sector_of(row) == Some(materialized_sector_id)) + .collect() +} + +pub(crate) fn transition_diagnostic_selection() -> Option { + TRANSITION_OBSERVER.with(|slot| { + slot.borrow() + .as_ref() + .and_then(|observer| observer.selection.clone()) + }) +} + +pub(crate) fn observe_transition_live_current_digests( + digests: BTreeSet, +) -> RusticolResult<()> { + TRANSITION_OBSERVER.with(|slot| { + let mut slot = slot.borrow_mut(); + let observer = slot.as_mut().ok_or_else(|| { + RusticolError::integrity("recurrence transition diagnostic observation was not active") + })?; + if observer.live_current_digests.replace(digests).is_some() { + return Err(RusticolError::integrity( + "recurrence transition diagnostic live slice was recorded twice", + )); + } + Ok(()) + }) +} + +pub(crate) fn take_transition_diagnostic_observation() -> RusticolResult<( + Vec, + Option>, +)> { + TRANSITION_OBSERVER.with(|slot| { + let observer = slot.borrow_mut().take().ok_or_else(|| { + RusticolError::integrity("recurrence transition diagnostic observation was not active") + })?; + Ok((observer.rows, observer.live_current_digests)) + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn state_index_for_public_helicity( + _source_slot: u32, + public_helicity: i32, + ) -> RusticolResult { + match public_helicity { + -1 => Ok(0), + 1 => Ok(1), + value => Err(RusticolError::integrity(format!( + "test source has no state for public helicity {value}" + ))), + } + } + + #[test] + fn diagnostic_identity_replay_keeps_public_helicity_assignments() { + let assignments = representative_source_states_for_public_helicities( + &[0, 1], + &[-1, 1], + state_index_for_public_helicity, + ) + .unwrap(); + assert_eq!( + assignments, + [ + SourceStateAssignment::new(0, 0), + SourceStateAssignment::new(1, 1) + ] + ); + } + + #[test] + fn diagnostic_nonidentity_replay_transports_opposite_public_helicities() { + // The two representative slots have the same state contract. The + // composed replay map swaps them, so representative state ancestry + // must be selected from the opposite public coordinate. + let assignments = representative_source_states_for_public_helicities( + &[1, 0], + &[-1, 1], + state_index_for_public_helicity, + ) + .unwrap(); + assert_eq!( + assignments, + [ + SourceStateAssignment::new(0, 1), + SourceStateAssignment::new(1, 0) + ] + ); + } + + #[test] + fn diagnostic_lane_filter_keeps_selected_duplicates_without_digest_leakage() { + let shared_digest = "same-semantic-current"; + let rows = vec![ + (shared_digest, 6, 0), + (shared_digest, 8, 0), + (shared_digest, 8, 1), + ]; + let selected = retain_materialized_sector_rows(rows, 8, |row| Some(row.1)); + assert_eq!(selected, [(shared_digest, 8, 0), (shared_digest, 8, 1)]); + } + + #[test] + fn diagnostic_observer_active_query_tracks_scope() { + assert!(!transition_diagnostic_observation_active()); + begin_transition_diagnostic_observation(None).unwrap(); + assert!(transition_diagnostic_observation_active()); + take_transition_diagnostic_observation().unwrap(); + assert!(!transition_diagnostic_observation_active()); + } + + #[test] + fn diagnostic_lane_context_fails_closed_and_resets_after_build_error() { + begin_transition_diagnostic_observation(None).unwrap(); + let nonsingleton = with_transition_diagnostic_materialized_sector(None, || Ok(())); + assert!(nonsingleton.is_err()); + + let failed: RusticolResult<()> = + with_transition_diagnostic_materialized_sector(Some(8), || { + Err(RusticolError::integrity( + "injected diagnostic build failure", + )) + }); + assert!(failed.is_err()); + with_transition_diagnostic_materialized_sector(Some(6), || Ok(())).unwrap(); + take_transition_diagnostic_observation().unwrap(); + } +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/recurrence/direct_backend.rs b/pyamplicol/rust/crates/rusticol-core/src/recurrence/direct_backend.rs new file mode 100644 index 0000000000..b1b5aa7cb6 --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/recurrence/direct_backend.rs @@ -0,0 +1,5063 @@ +// SPDX-License-Identifier: 0BSD + +//! Direct-arena prepared executor ABI for compact recurrence schedules. +//! +//! This module deliberately does not depend on the eager runtime. Direct +//! executors consume fixed-width recurrence rows and persistent split-complex +//! arenas. They never receive a packed evaluator input or output buffer. + +use super::RecurrenceStrategy; +use super::SemanticDigest; +use super::direct_plan::{ + DIRECT_CONTRIBUTION_FLAG_CERTIFIED_REUSE, DIRECT_CONTRIBUTION_FLAG_INITIALIZE_DESTINATION, + DIRECT_NONE_U32, DirectClosureRow, DirectContributionRow, DirectCurrentDescriptor, + DirectExecutorRole, DirectFinalizationRow, DirectNodeKind, DirectRecurrencePlan, + DirectRowGroupDescriptor, DirectSourceRow, +}; +pub use super::direct_plan::{ + DirectResolvedSourceSelection, DirectSourceDispatchVariantDescriptor, DirectSourceEmbeddingRow, +}; +use crate::direct_arena::{DirectArenaTrafficCounters, clear_split_active_range}; +pub use crate::direct_arena::{ + DirectArenaView, DirectFactorView, DirectMomentumView, DirectParameterView, +}; +use crate::{RusticolError, RusticolResult}; +use std::cell::RefCell; +use std::collections::{BTreeMap, BTreeSet}; +use std::ffi::{c_int, c_void}; +use std::mem::size_of; +use std::time::{Duration, Instant}; + +pub const RECURRENCE_DIRECT_BACKEND_ABI: &str = "rusticol.recurrence-direct-backend.v1"; + +pub const DIRECT_STATUS_OK: c_int = 0; + +thread_local! { + static DIRECT_EXECUTOR_ERROR_DETAIL: RefCell> = const { RefCell::new(None) }; + #[cfg(any(test, feature = "on-the-fly-test-support"))] + static DIRECT_CURRENT_OBSERVATION: RefCell> = const { RefCell::new(None) }; +} + +/// One test-only semantic-current observation from the authenticated direct +/// runtime. This is deliberately absent from release builds and public ABIs. +#[cfg(any(test, feature = "on-the-fly-test-support"))] +#[derive(Clone, Debug, PartialEq)] +pub(crate) struct DirectObservedCurrentValue { + pub descriptor: super::direct_plan::DirectCurrentDescriptor, + /// Split-complex values in component-major, then point-major order. + pub values: Vec<(f64, f64)>, +} + +/// Test-only capture of the current values produced for one exact direct-plan +/// execution. The two plan digests and selected sector bind the values to the +/// authenticated schedule and query which produced them. +#[cfg(any(test, feature = "on-the-fly-test-support"))] +#[derive(Clone, Debug, PartialEq)] +pub(crate) struct DirectCurrentObservation { + pub plan_semantic_digest: SemanticDigest, + pub runtime_layout_digest: SemanticDigest, + pub selected_sector_id: Option, + pub point_count: u32, + pub currents: BTreeMap, + /// Representative amplitudes captured after closures and before replay + /// phase/multiplicity scaling, keyed by DirectPlan destination ID. + pub amplitudes_before_replay: BTreeMap>, +} + +#[cfg(any(test, feature = "on-the-fly-test-support"))] +pub(crate) fn begin_direct_current_observation( + plan: &DirectRecurrencePlan, + selected_sector_id: Option, + point_count: u32, +) -> RusticolResult<()> { + if point_count == 0 { + return Err(RusticolError::invalid_argument( + "direct current observation requires at least one point", + )); + } + DIRECT_CURRENT_OBSERVATION.with(|slot| { + let mut slot = slot.borrow_mut(); + if slot.is_some() { + return Err(RusticolError::invalid_argument( + "direct current observation is already active on this thread", + )); + } + *slot = Some(DirectCurrentObservation { + plan_semantic_digest: plan.semantic_digest(), + runtime_layout_digest: plan.runtime_layout_digest(), + selected_sector_id, + point_count, + currents: BTreeMap::new(), + amplitudes_before_replay: BTreeMap::new(), + }); + Ok(()) + }) +} + +#[cfg(any(test, feature = "on-the-fly-test-support"))] +pub(crate) fn take_direct_current_observation() -> RusticolResult { + DIRECT_CURRENT_OBSERVATION.with(|slot| { + slot.borrow_mut().take().ok_or_else(|| { + RusticolError::invalid_argument( + "direct current observation is not active on this thread", + ) + }) + }) +} + +#[cfg(any(test, feature = "on-the-fly-test-support"))] +fn direct_current_observation_active() -> bool { + DIRECT_CURRENT_OBSERVATION.with(|slot| slot.borrow().is_some()) +} + +#[cfg(not(any(test, feature = "on-the-fly-test-support")))] +const fn direct_current_observation_active() -> bool { + false +} + +#[cfg(any(test, feature = "on-the-fly-test-support"))] +pub(super) fn observe_direct_amplitudes_before_replay( + plan: &DirectRecurrencePlan, + selected_sector_id: Option, + workspace: &DirectWorkspace<'_>, + point_count: u32, +) -> RusticolResult<()> { + let active = DIRECT_CURRENT_OBSERVATION.with(|slot| slot.borrow().is_some()); + if !active { + return Ok(()); + } + DIRECT_CURRENT_OBSERVATION.with(|slot| { + let mut slot = slot.borrow_mut(); + let observation = slot.as_mut().ok_or_else(|| { + RusticolError::integrity("direct current observation disappeared before amplitudes") + })?; + if observation.plan_semantic_digest != plan.semantic_digest() + || observation.runtime_layout_digest != plan.runtime_layout_digest() + || observation.selected_sector_id != selected_sector_id + || observation.point_count != point_count + { + return Err(RusticolError::integrity( + "direct amplitude observation does not match this plan execution", + )); + } + let stride = workspace.point_stride as usize; + let active_points = point_count as usize; + for destination in plan.amplitude_destinations().iter().filter(|destination| { + selected_sector_id.is_none_or(|sector| destination.target_sector_id == sector) + }) { + let start = destination.id as usize * stride; + let end = start.checked_add(active_points).ok_or_else(|| { + RusticolError::integrity("direct amplitude observation range overflows usize") + })?; + let real = workspace.amplitude_re.get(start..end).ok_or_else(|| { + RusticolError::integrity("direct amplitude observation real range is absent") + })?; + let imag = workspace.amplitude_im.get(start..end).ok_or_else(|| { + RusticolError::integrity("direct amplitude observation imaginary range is absent") + })?; + observation.amplitudes_before_replay.insert( + destination.id, + real.iter().copied().zip(imag.iter().copied()).collect(), + ); + } + Ok(()) + }) +} + +/// Clear the synchronous detail channel before executing one direct plan. +pub(crate) fn clear_direct_executor_error_detail() { + DIRECT_EXECUTOR_ERROR_DETAIL.with(|detail| *detail.borrow_mut() = None); +} + +/// Preserve the first concrete callback failure across the narrow +/// integer-status ABI. +pub(crate) fn record_direct_executor_error_detail(detail: RusticolError) { + DIRECT_EXECUTOR_ERROR_DETAIL.with(|slot| { + let mut slot = slot.borrow_mut(); + if slot.is_none() { + *slot = Some(detail); + } + }); +} + +fn take_direct_executor_error_detail() -> Option { + DIRECT_EXECUTOR_ERROR_DETAIL.with(|detail| detail.borrow_mut().take()) +} + +pub type DirectSourceExecutor = unsafe extern "C" fn( + *const c_void, + DirectArenaView, + DirectMomentumView, + DirectParameterView, + DirectFactorView, + *const DirectSourceRow, + u32, + u32, +) -> c_int; + +/// Runtime-helicity source dispatcher used only by all-flow-union plans. +/// +/// Unlike a static source executor, this callable receives one authenticated +/// resolved selection plus the process-bound variant and embedding catalogs. +/// It fills every full source current before the remaining direct schedule is +/// executed. The pointed-to tables and context must outlive the call. +pub type DirectUnionSourceDispatchExecutor = unsafe extern "C" fn( + *const c_void, + DirectArenaView, + DirectMomentumView, + DirectParameterView, + DirectFactorView, + *const DirectSourceRow, + u32, + *const DirectSourceDispatchVariantDescriptor, + u32, + *const DirectSourceEmbeddingRow, + u32, + *const DirectResolvedSourceSelection, + u32, + u32, +) -> c_int; + +/// Typed handle for one immutable all-flow-union source dispatcher. +#[derive(Clone, Copy)] +pub struct DirectUnionSourceDispatchHandle { + pub call: DirectUnionSourceDispatchExecutor, + pub context: *const c_void, +} + +pub type DirectContributionExecutor = unsafe extern "C" fn( + *const c_void, + DirectArenaView, + DirectMomentumView, + DirectParameterView, + DirectFactorView, + *const DirectContributionRow, + u32, + u32, +) -> c_int; + +pub(crate) type DirectContributionFanoutExecutor = for<'a> unsafe fn( + *const c_void, + DirectArenaView, + DirectMomentumView, + DirectParameterView, + DirectFactorView, + DirectContributionFanoutBatch<'a>, +) -> RusticolResult<()>; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[allow(clippy::enum_variant_names)] +pub(crate) enum DirectInteractionIntrinsicKind { + ColorOrderedThreeVector, + VectorWedgeVector, + AntisymmetricTensorVector, +} + +pub(crate) type DirectInteractionBundleExecutor = for<'a> unsafe fn( + DirectInteractionExecutorContexts, + DirectArenaView, + DirectMomentumView, + DirectParameterView, + DirectFactorView, + DirectInteractionBundleBatch<'a>, +) -> RusticolResult<()>; + +#[derive(Clone, Copy)] +pub(crate) struct DirectInteractionExecutorContexts { + pub(crate) color: *const c_void, + pub(crate) vector_wedge_vector: *const c_void, + pub(crate) antisymmetric_tensor_vector: *const c_void, +} + +#[derive(Clone, Copy)] +pub(crate) struct DirectContributionFanoutExecutorHandle { + pub(crate) call: DirectContributionFanoutExecutor, + pub(crate) context: *const c_void, + pub(crate) destination_component_count: u32, + pub(crate) parent_component_counts: [u32; 2], + pub(crate) requires_two_momenta: bool, + pub(crate) required_parameter_count: u32, + pub(crate) interaction_kind: DirectInteractionIntrinsicKind, + pub(crate) interaction_call: Option, +} + +pub type DirectFinalizationExecutor = unsafe extern "C" fn( + *const c_void, + DirectArenaView, + DirectMomentumView, + DirectParameterView, + DirectFactorView, + *const DirectFinalizationRow, + u32, + u32, +) -> c_int; + +pub type DirectClosureExecutor = unsafe extern "C" fn( + *const c_void, + DirectArenaView, + DirectMomentumView, + DirectParameterView, + DirectFactorView, + *const DirectClosureRow, + u32, + u32, +) -> c_int; + +#[derive(Clone, Copy)] +pub enum DirectExecutorHandle { + Source { + call: DirectSourceExecutor, + context: *const c_void, + }, + Contribution { + call: DirectContributionExecutor, + context: *const c_void, + }, + Finalization { + call: DirectFinalizationExecutor, + context: *const c_void, + }, + Closure { + call: DirectClosureExecutor, + context: *const c_void, + }, +} + +impl DirectExecutorHandle { + pub(crate) const fn role(self) -> DirectExecutorRole { + match self { + Self::Source { .. } => DirectExecutorRole::Source, + Self::Contribution { .. } => DirectExecutorRole::Contribution, + Self::Finalization { .. } => DirectExecutorRole::Finalization, + Self::Closure { .. } => DirectExecutorRole::Closure, + } + } +} + +pub struct DirectExecutorCatalog { + handles: Box<[Option]>, + contribution_metadata: Box<[Option]>, + contribution_fanout: Box<[Option]>, + packed_singleton_capable: bool, + plan_layout_digest: SemanticDigest, + direct_template_catalog_digest: SemanticDigest, +} + +/// Authenticated contribution shape and row-local scalar dependency. +/// +/// `exact_factor_is_kernel_input` is fail-closed. It is false only when the +/// prepared template proves that the exact factor is applied exclusively by +/// the destination adapter (currently Rusticol contribution intrinsics). +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) struct DirectContributionExecutionMetadata { + pub(crate) destination_component_count: u32, + pub(crate) exact_factor_is_kernel_input: bool, +} + +impl DirectContributionExecutionMetadata { + pub(crate) fn new( + destination_component_count: u32, + exact_factor_is_kernel_input: bool, + ) -> RusticolResult { + if destination_component_count == 0 { + return Err(RusticolError::integrity( + "direct contribution executor has zero destination components", + )); + } + Ok(Self { + destination_component_count, + exact_factor_is_kernel_input, + }) + } +} + +impl DirectExecutorCatalog { + pub fn new( + plan: &DirectRecurrencePlan, + direct_template_catalog_digest: SemanticDigest, + handles: Vec, + ) -> RusticolResult { + let sparse_handles = handles.into_iter().map(Some).collect::>(); + let metadata = conservative_contribution_metadata(plan, sparse_handles.len())?; + Self::new_sparse_with_metadata( + plan, + direct_template_catalog_digest, + sparse_handles, + metadata, + ) + } + + #[allow(dead_code)] + pub(crate) fn new_sparse( + plan: &DirectRecurrencePlan, + direct_template_catalog_digest: SemanticDigest, + handles: Vec>, + ) -> RusticolResult { + let metadata = conservative_contribution_metadata(plan, handles.len())?; + Self::new_sparse_with_metadata(plan, direct_template_catalog_digest, handles, metadata) + } + + pub(crate) fn new_sparse_with_metadata( + plan: &DirectRecurrencePlan, + direct_template_catalog_digest: SemanticDigest, + handles: Vec>, + contribution_metadata: Vec>, + ) -> RusticolResult { + let contribution_fanout = vec![None; handles.len()]; + Self::new_sparse_with_metadata_and_fanout( + plan, + direct_template_catalog_digest, + handles, + contribution_metadata, + contribution_fanout, + ) + } + + pub(crate) fn new_sparse_with_metadata_and_fanout( + plan: &DirectRecurrencePlan, + direct_template_catalog_digest: SemanticDigest, + handles: Vec>, + contribution_metadata: Vec>, + contribution_fanout: Vec>, + ) -> RusticolResult { + if handles.is_empty() { + return Err(RusticolError::invalid_argument( + "direct recurrence executor catalog must not be empty", + )); + } + if contribution_metadata.len() != handles.len() { + return Err(RusticolError::integrity( + "direct contribution metadata does not cover the executor catalog", + )); + } + if contribution_fanout.len() != handles.len() { + return Err(RusticolError::integrity( + "direct contribution fanout capabilities do not cover the executor catalog", + )); + } + for (executor_id, capability) in contribution_fanout.iter().copied().enumerate() { + let Some(capability) = capability else { + continue; + }; + if !matches!( + handles[executor_id], + Some(DirectExecutorHandle::Contribution { .. }) + ) { + return Err(RusticolError::integrity(format!( + "direct fused fanout capability {executor_id} is not a loaded contribution executor" + ))); + } + let metadata = contribution_metadata[executor_id].ok_or_else(|| { + RusticolError::integrity(format!( + "direct fused fanout capability {executor_id} has no contribution metadata" + )) + })?; + if metadata.exact_factor_is_kernel_input + || metadata.destination_component_count != capability.destination_component_count + { + return Err(RusticolError::integrity(format!( + "direct fused fanout capability {executor_id} is incompatible with its contribution metadata" + ))); + } + } + if direct_template_catalog_digest != plan.direct_template_catalog_digest() { + return Err(RusticolError::integrity(format!( + "loaded direct-template catalog digest {direct_template_catalog_digest} does not match plan {}", + plan.direct_template_catalog_digest() + ))); + } + let catalog = Self { + handles: handles.into_boxed_slice(), + contribution_metadata: contribution_metadata.into_boxed_slice(), + contribution_fanout: contribution_fanout.into_boxed_slice(), + packed_singleton_capable: false, + plan_layout_digest: plan.runtime_layout_digest(), + direct_template_catalog_digest, + }; + catalog.validate_for_plan(plan)?; + Ok(catalog) + } + + pub(crate) const fn packed_singleton_capable(&self) -> bool { + self.packed_singleton_capable + } + + pub(crate) fn mark_packed_singleton_capable(mut self) -> Self { + self.packed_singleton_capable = true; + self + } + + fn contribution_metadata( + &self, + executor_id: u32, + ) -> RusticolResult { + self.contribution_metadata + .get(executor_id as usize) + .and_then(|metadata| *metadata) + .ok_or_else(|| { + RusticolError::integrity(format!( + "direct contribution executor {executor_id} has no authenticated execution metadata" + )) + }) + } + + fn contribution_fanout( + &self, + executor_id: u32, + ) -> Option { + self.contribution_fanout + .get(executor_id as usize) + .and_then(|capability| *capability) + } + + fn require( + &self, + executor_id: u32, + role: DirectExecutorRole, + ) -> RusticolResult { + let handle = self + .handles + .get(executor_id as usize) + .and_then(|handle| *handle) + .ok_or_else(|| { + RusticolError::evaluation(format!( + "direct recurrence executor {executor_id} is not loaded" + )) + })?; + if handle.role() != role { + return Err(RusticolError::integrity(format!( + "direct recurrence executor {executor_id} has role {:?}, expected {role:?}", + handle.role() + ))); + } + Ok(handle) + } + + fn validate_for_plan(&self, plan: &DirectRecurrencePlan) -> RusticolResult<()> { + if self.handles.len() != plan.direct_executor_count() as usize { + return Err(RusticolError::integrity(format!( + "direct recurrence executor catalog has {} entries, expected {}", + self.handles.len(), + plan.direct_executor_count() + ))); + } + if self.contribution_metadata.len() != self.handles.len() { + return Err(RusticolError::integrity( + "direct contribution metadata has the wrong executor domain", + )); + } + if self.contribution_fanout.len() != self.handles.len() { + return Err(RusticolError::integrity( + "direct contribution fanout capabilities have the wrong executor domain", + )); + } + if self.direct_template_catalog_digest != plan.direct_template_catalog_digest() { + return Err(RusticolError::integrity( + "direct recurrence executor catalog belongs to a different prepared template catalog", + )); + } + for descriptor in plan.row_groups() { + if descriptor.role == DirectExecutorRole::Source + && descriptor.direct_executor_id == DIRECT_NONE_U32 + && plan.strategy() == RecurrenceStrategy::AllFlowUnion + { + continue; + } + if descriptor.role == DirectExecutorRole::Contribution + && descriptor.direct_executor_id == DIRECT_NONE_U32 + { + continue; + } + self.require(descriptor.direct_executor_id, descriptor.role)?; + if descriptor.role == DirectExecutorRole::Contribution { + self.contribution_metadata(descriptor.direct_executor_id)?; + } + } + Ok(()) + } +} + +fn conservative_contribution_metadata( + plan: &DirectRecurrencePlan, + executor_count: usize, +) -> RusticolResult>> { + let current_components = plan + .currents() + .iter() + .filter(|current| current.node_kind == DirectNodeKind::Current) + .map(|current| { + ( + (current.stage, current.component_base), + u32::from(current.component_count), + ) + }) + .collect::>(); + let mut metadata = vec![None; executor_count]; + for descriptor in plan.row_groups().iter().filter(|descriptor| { + descriptor.role == DirectExecutorRole::Contribution + && descriptor.direct_executor_id != DIRECT_NONE_U32 + }) { + let start = usize::try_from(descriptor.row_start).map_err(|_| { + RusticolError::integrity("direct contribution row-group start exceeds usize") + })?; + let end = start + .checked_add(descriptor.row_count as usize) + .ok_or_else(|| { + RusticolError::integrity("direct contribution row-group range overflows usize") + })?; + let rows = plan.contributions().get(start..end).ok_or_else(|| { + RusticolError::integrity("direct contribution row-group range is out of bounds") + })?; + for row in rows { + let component_count = current_components + .get(&(descriptor.stage, row.destination_component_base)) + .copied() + .ok_or_else(|| { + RusticolError::integrity("direct contribution destination current is absent") + })?; + let candidate = DirectContributionExecutionMetadata::new(component_count, true)?; + let slot = metadata + .get_mut(descriptor.direct_executor_id as usize) + .ok_or_else(|| { + RusticolError::integrity("direct contribution executor is outside its catalog") + })?; + if slot.is_some_and(|existing| existing != candidate) { + return Err(RusticolError::integrity(format!( + "direct contribution executor {} has inconsistent destination shapes", + descriptor.direct_executor_id + ))); + } + *slot = Some(candidate); + } + } + Ok(metadata) +} + +const DIRECT_CONTRIBUTION_FANOUT_BATCH_CAPACITY: usize = 64; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) struct DirectContributionFanoutRun { + pub(crate) row_start: u64, + pub(crate) row_count: u32, + pub(crate) scratch_component_base: u32, + pub(crate) bundle_start: u64, + pub(crate) bundle_count: u32, +} + +/// Maximal consecutive fanout outputs which share their effective scale. +/// +/// `DIRECT_NONE_U32` denotes unity because factor-consuming executors already +/// applied the authenticated row factor while producing the representative. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) struct DirectContributionFanoutBundle { + pub(crate) row_start: u64, + pub(crate) row_count: u32, + pub(crate) effective_factor_id_or_sentinel: u32, +} + +#[derive(Clone, Copy)] +pub(crate) struct DirectContributionFanoutBatch<'a> { + pub(crate) rows: &'a [DirectContributionRow], + pub(crate) runs: &'a [DirectContributionFanoutRun], + pub(crate) bundles: &'a [DirectContributionFanoutBundle], +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) enum DirectInteractionVectorParent { + Parent0, + Parent1, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) struct DirectInteractionOperand { + pub(crate) tensor_component_base: u32, + pub(crate) vector_parent: DirectInteractionVectorParent, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) enum DirectInteractionOperands { + None, + One(DirectInteractionOperand), + Two([DirectInteractionOperand; 2]), +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) struct DirectInteractionAnchor { + pub(crate) parent_component_bases: [u32; 2], + pub(crate) parent_momentum_form_ids: [u32; 2], + pub(crate) operands: DirectInteractionOperands, + pub(crate) output_start: u32, + pub(crate) output_end: u32, + pub(crate) tensor_output_start: u32, + pub(crate) tensor_output_end: u32, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) struct DirectInteractionOutput { + pub(crate) destination_component_base: u32, + /// Color, followed by the zero, one, or two tensor-vector factors in the + /// cold-authenticated contribution order. Unused tail entries are ignored. + pub(crate) exact_factor_ids: [u32; 3], + pub(crate) initialize: bool, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) struct DirectInteractionTensorOutput { + pub(crate) destination_component_base: u32, + pub(crate) exact_factor_id: u32, + pub(crate) initialize: bool, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) struct DirectInteractionTerminalClosure { + pub(crate) root_component_base: u32, + pub(crate) amplitude_destination_id: u32, + pub(crate) exact_factor_id: u32, +} + +#[derive(Clone, Copy)] +pub(crate) struct DirectInteractionTerminalBatch<'a> { + pub(crate) partner_component_base: u32, + pub(crate) component_factor_start: u32, + pub(crate) amplitude_destination_count: u32, + pub(crate) closures: &'a [DirectInteractionTerminalClosure], +} + +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub(crate) struct DirectInteractionRuntimeRequirements { + pub(crate) current_component_count: u32, + pub(crate) momentum_form_count: u32, + pub(crate) parameter_count: u32, + pub(crate) factor_count: u32, +} + +impl DirectInteractionRuntimeRequirements { + fn include_current_span(&mut self, base: u32, component_count: u32) -> Option<()> { + self.current_component_count = self + .current_component_count + .max(base.checked_add(component_count)?); + Some(()) + } + + fn include_momentum(&mut self, momentum_form_id: u32) -> Option<()> { + self.momentum_form_count = self + .momentum_form_count + .max(momentum_form_id.checked_add(1)?); + Some(()) + } + + fn include_factor(&mut self, exact_factor_id: u32) -> Option<()> { + self.factor_count = self.factor_count.max(exact_factor_id.checked_add(1)?); + Some(()) + } +} + +#[derive(Clone, Copy)] +pub(crate) struct DirectInteractionBundleBatch<'a> { + pub(crate) anchors: &'a [DirectInteractionAnchor], + pub(crate) outputs: &'a [DirectInteractionOutput], + pub(crate) tensor_outputs: &'a [DirectInteractionTensorOutput], + pub(crate) terminal: Option>, + pub(crate) requirements: DirectInteractionRuntimeRequirements, + pub(crate) atv_before_color: bool, +} + +/// Authenticated native fanout capability for one contribution executor. +/// +/// Only intrinsic executors expose this metadata/handle pair. The generic +/// fanout schedule uses the fused handle directly; the interaction schedule +/// additionally requires its optional interaction call. Prepared SymJIT/native +/// calls and nonmatching model/process combinations retain the ordinary row +/// executor path. +#[derive(Clone, Copy)] +pub(crate) struct DirectInteractionExecutorCapability { + metadata: DirectContributionExecutionMetadata, + fanout: DirectContributionFanoutExecutorHandle, +} + +impl DirectInteractionExecutorCapability { + pub(crate) fn new( + handle: DirectExecutorHandle, + metadata: DirectContributionExecutionMetadata, + fanout: DirectContributionFanoutExecutorHandle, + ) -> RusticolResult { + if handle.role() != DirectExecutorRole::Contribution { + return Err(RusticolError::integrity( + "direct interaction capability is attached to a non-contribution executor", + )); + } + if metadata.exact_factor_is_kernel_input + || metadata.destination_component_count != fanout.destination_component_count + { + return Err(RusticolError::integrity( + "direct interaction capability conflicts with authenticated contribution metadata", + )); + } + Ok(Self { metadata, fanout }) + } +} + +#[cfg(test)] +mod interaction_capability_tests { + use super::*; + + unsafe extern "C" fn contribution_stub( + _context: *const c_void, + _arena: DirectArenaView, + _momenta: DirectMomentumView, + _parameters: DirectParameterView, + _factors: DirectFactorView, + _rows: *const DirectContributionRow, + _row_count: u32, + _point_count: u32, + ) -> c_int { + DIRECT_STATUS_OK + } + + unsafe extern "C" fn source_stub( + _context: *const c_void, + _arena: DirectArenaView, + _momenta: DirectMomentumView, + _parameters: DirectParameterView, + _factors: DirectFactorView, + _rows: *const DirectSourceRow, + _row_count: u32, + _point_count: u32, + ) -> c_int { + DIRECT_STATUS_OK + } + + unsafe fn fanout_stub( + _context: *const c_void, + _arena: DirectArenaView, + _momenta: DirectMomentumView, + _parameters: DirectParameterView, + _factors: DirectFactorView, + _batch: DirectContributionFanoutBatch<'_>, + ) -> RusticolResult<()> { + Ok(()) + } + + fn fanout(destination_component_count: u32) -> DirectContributionFanoutExecutorHandle { + DirectContributionFanoutExecutorHandle { + call: fanout_stub, + context: std::ptr::null(), + destination_component_count, + parent_component_counts: [4, 4], + requires_two_momenta: true, + required_parameter_count: 0, + interaction_kind: DirectInteractionIntrinsicKind::ColorOrderedThreeVector, + interaction_call: None, + } + } + + #[test] + fn interaction_capability_requires_authenticated_output_only_contribution_shape() { + let contribution = DirectExecutorHandle::Contribution { + call: contribution_stub, + context: std::ptr::null(), + }; + let output_only = DirectContributionExecutionMetadata::new(4, false).unwrap(); + assert!( + DirectInteractionExecutorCapability::new(contribution, output_only, fanout(4)).is_ok() + ); + assert!( + DirectInteractionExecutorCapability::new( + contribution, + DirectContributionExecutionMetadata::new(4, true).unwrap(), + fanout(4), + ) + .is_err() + ); + assert!( + DirectInteractionExecutorCapability::new(contribution, output_only, fanout(6)).is_err() + ); + assert!( + DirectInteractionExecutorCapability::new( + DirectExecutorHandle::Source { + call: source_stub, + context: std::ptr::null(), + }, + output_only, + fanout(4), + ) + .is_err() + ); + } +} + +#[derive(Clone, Copy)] +enum DirectInteractionRowsView<'a> { + Contribution(&'a [DirectContributionRow]), + Closure(&'a [DirectClosureRow]), + Other, +} + +/// Zero-copy view of one authenticated schedule group. +#[derive(Clone, Copy)] +pub(crate) struct DirectInteractionGroupView<'a> { + stage: u32, + role: DirectExecutorRole, + direct_executor_id: u32, + handle: Option, + capability: Option, + rows: DirectInteractionRowsView<'a>, +} + +impl<'a> DirectInteractionGroupView<'a> { + pub(crate) const fn contribution( + stage: u32, + direct_executor_id: u32, + handle: Option, + capability: Option, + rows: &'a [DirectContributionRow], + ) -> Self { + Self { + stage, + role: DirectExecutorRole::Contribution, + direct_executor_id, + handle, + capability, + rows: DirectInteractionRowsView::Contribution(rows), + } + } + + pub(crate) const fn closure( + stage: u32, + direct_executor_id: u32, + handle: DirectExecutorHandle, + rows: &'a [DirectClosureRow], + ) -> Self { + Self { + stage, + role: DirectExecutorRole::Closure, + direct_executor_id, + handle: Some(handle), + capability: None, + rows: DirectInteractionRowsView::Closure(rows), + } + } + + pub(crate) const fn other( + stage: u32, + role: DirectExecutorRole, + direct_executor_id: u32, + handle: Option, + ) -> Self { + Self { + stage, + role, + direct_executor_id, + handle, + capability: None, + rows: DirectInteractionRowsView::Other, + } + } + + fn contributions(self) -> Option<&'a [DirectContributionRow]> { + match self.rows { + DirectInteractionRowsView::Contribution(rows) => Some(rows), + DirectInteractionRowsView::Closure(_) | DirectInteractionRowsView::Other => None, + } + } + + fn closures(self) -> Option<&'a [DirectClosureRow]> { + match self.rows { + DirectInteractionRowsView::Closure(rows) => Some(rows), + DirectInteractionRowsView::Contribution(_) | DirectInteractionRowsView::Other => None, + } + } +} + +/// Borrowed authenticated inputs needed by the native interaction compiler. +/// Row storage remains owned by the direct plan or prepared OTF family. +pub(crate) struct DirectInteractionScheduleView<'a> { + strategy: RecurrenceStrategy, + groups: &'a [DirectInteractionGroupView<'a>], + currents: &'a [DirectCurrentDescriptor], + contribution_row_count: usize, +} + +impl<'a> DirectInteractionScheduleView<'a> { + pub(crate) const fn new( + strategy: RecurrenceStrategy, + groups: &'a [DirectInteractionGroupView<'a>], + currents: &'a [DirectCurrentDescriptor], + contribution_row_count: usize, + ) -> Self { + Self { + strategy, + groups, + currents, + contribution_row_count, + } + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum DirectContributionFanoutStep { + Direct { + row_start: u64, + row_count: u32, + }, + Fanout { + row_start: u64, + row_count: u32, + representative_start: u64, + representative_count: u32, + run_start: u64, + run_count: u32, + destination_component_count: u32, + }, +} + +impl DirectContributionFanoutStep { + const fn row_start(self) -> u64 { + match self { + Self::Direct { row_start, .. } | Self::Fanout { row_start, .. } => row_start, + } + } + + const fn row_count(self) -> u32 { + match self { + Self::Direct { row_count, .. } | Self::Fanout { row_count, .. } => row_count, + } + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +struct DirectContributionFanoutGroup { + stage: u16, + direct_executor_id: u32, + row_start: u64, + row_count: u32, + step_start: u64, + step_count: u32, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum DirectSingletonFanoutStep { + Direct { + row_start: u32, + row_count: u32, + }, + Fanout { + row_start: u32, + row_count: u32, + run_start: u64, + run_count: u32, + }, +} + +impl DirectSingletonFanoutStep { + const fn row_start(self) -> u32 { + match self { + Self::Direct { row_start, .. } | Self::Fanout { row_start, .. } => row_start, + } + } + + const fn row_count(self) -> u32 { + match self { + Self::Direct { row_count, .. } | Self::Fanout { row_count, .. } => row_count, + } + } +} + +#[derive(Clone, Copy)] +struct DirectSingletonFanoutGroup { + direct_executor_id: u32, + row_count: u32, + step_start: u64, + step_count: u32, + raw: DirectExecutorHandle, + fused: DirectContributionFanoutExecutorHandle, +} + +/// Cold family-local native fanout schedule for singleton-point execution. +/// +/// Unlike [`DirectContributionFanoutProgram`], this needs no private arena +/// suffix: authenticated intrinsic fanout executors evaluate one input class +/// and write every destination directly. Batched point execution remains on +/// the ordinary row executor path. +pub(crate) struct DirectSingletonContributionFanoutProgram { + groups: Box<[Option]>, + steps: Box<[DirectSingletonFanoutStep]>, + runs: Box<[DirectContributionFanoutRun]>, + bundles: Box<[DirectContributionFanoutBundle]>, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) enum DirectInteractionGroupAction { + Normal, + Execute(u32), + Consumed, +} + +struct DirectInteractionStage { + group_indices: Box<[u32]>, + closure_group_index: Option, + call: DirectInteractionBundleExecutor, + contexts: DirectInteractionExecutorContexts, + atv_before_color: bool, + anchors: Box<[DirectInteractionAnchor]>, + outputs: Box<[DirectInteractionOutput]>, + tensor_outputs: Box<[DirectInteractionTensorOutput]>, + terminal: Option, + requirements: DirectInteractionRuntimeRequirements, + logical_row_count: u64, +} + +struct DirectInteractionTerminalProgram { + partner_component_base: u32, + component_factor_start: u32, + amplitude_destination_count: u32, + closures: Box<[DirectInteractionTerminalClosure]>, +} + +/// Cold, schedule-local native interaction rewrite shared by persisted direct +/// plans and dynamically prepared on-the-fly families. +pub(crate) struct DirectInteractionProgram { + actions: Box<[DirectInteractionGroupAction]>, + stages: Box<[DirectInteractionStage]>, +} + +/// Immutable, plan-local execution rewrite for contribution kinematics. +/// +/// Rows remain the authoritative cold schedule. This program copies only the +/// compact input, destination, and live-factor metadata needed to evaluate +/// each authenticated input class once and replay its deterministic writes. +pub(crate) struct DirectContributionFanoutProgram { + plan_layout_digest: SemanticDigest, + groups: Box<[DirectContributionFanoutGroup]>, + steps: Box<[DirectContributionFanoutStep]>, + representatives: Box<[DirectContributionRow]>, + runs: Box<[DirectContributionFanoutRun]>, + bundles: Box<[DirectContributionFanoutBundle]>, + scratch_component_base: u32, + scratch_component_count: u32, + unit_factor_id: u32, + needs_unit_factor: bool, + interactions: DirectInteractionProgram, + full_singleton_steps: Box<[DirectFullSingletonStep]>, +} + +/// Cold-authenticated dispatch for the common full, unselected singleton. +/// +/// The persisted plan remains authoritative. These steps retain only handles +/// and already-validated row offsets, avoiding a warmed catalog lookup and +/// row-range reconstruction for every small group. Selected, observed, and +/// multi-point execution continue through the generic schedule. +#[derive(Clone, Copy)] +enum DirectFullSingletonStep { + Interaction { + stage_index: u32, + }, + Call { + direct_executor_id: u32, + handle: DirectExecutorHandle, + row_start: usize, + row_count: u32, + }, + CertifiedReuse { + row_start: usize, + row_count: u32, + }, + Fanout { + group_index: u32, + call: DirectContributionExecutor, + context: *const c_void, + fused: Option, + }, +} + +impl DirectContributionFanoutProgram { + pub(crate) fn build( + plan: &DirectRecurrencePlan, + executors: &DirectExecutorCatalog, + ) -> RusticolResult { + if executors.plan_layout_digest != plan.runtime_layout_digest() { + return Err(RusticolError::integrity( + "direct fanout executor catalog belongs to a different plan", + )); + } + let scratch_component_base = plan.current_arena_components(); + let unit_factor_id = u32::try_from(plan.exact_factors().len()) + .map_err(|_| RusticolError::integrity("direct exact-factor catalog exceeds u32"))?; + let mut groups = Vec::new(); + let mut steps = Vec::new(); + let mut representatives = Vec::new(); + let mut runs = Vec::new(); + let mut bundles = Vec::new(); + let mut scratch_component_count = 0_u32; + let mut needs_unit_factor = false; + let destination_component_counts = plan + .currents() + .iter() + .filter(|current| current.node_kind == DirectNodeKind::Current) + .map(|current| { + ( + (current.stage, current.component_base), + u32::from(current.component_count), + ) + }) + .collect::>(); + let arena_component_count = plan.current_arena_components(); + let momentum_form_count = u32::try_from(plan.momentum_forms().len()) + .map_err(|_| RusticolError::integrity("direct momentum-form count exceeds u32"))?; + + for descriptor in plan.row_groups().iter().filter(|descriptor| { + descriptor.role == DirectExecutorRole::Contribution + && descriptor.direct_executor_id != DIRECT_NONE_U32 + }) { + let metadata = executors.contribution_metadata(descriptor.direct_executor_id)?; + let fused = executors.contribution_fanout(descriptor.direct_executor_id); + let group_step_start = u64::try_from(steps.len()) + .map_err(|_| RusticolError::integrity("direct fanout step count exceeds u64"))?; + let start = usize::try_from(descriptor.row_start).map_err(|_| { + RusticolError::integrity("direct fanout row-group start exceeds usize") + })?; + let end = start + .checked_add(descriptor.row_count as usize) + .ok_or_else(|| { + RusticolError::integrity("direct fanout row-group range overflows usize") + })?; + let descriptor_rows = plan.contributions().get(start..end).ok_or_else(|| { + RusticolError::integrity("direct fanout row-group range is out of bounds") + })?; + for row in descriptor_rows { + let authenticated_component_count = destination_component_counts + .get(&(descriptor.stage, row.destination_component_base)) + .copied() + .ok_or_else(|| { + RusticolError::integrity( + "direct fanout contribution destination current is absent", + ) + })?; + if metadata.destination_component_count != authenticated_component_count { + return Err(RusticolError::integrity(format!( + "direct contribution executor {} declares {} destination components, expected {authenticated_component_count}", + descriptor.direct_executor_id, metadata.destination_component_count + ))); + } + if let Some(fused) = fused { + validate_fused_fanout_row( + *row, + fused.parent_component_counts, + fused.requires_two_momenta, + arena_component_count, + momentum_form_count, + )?; + } + } + let mut selector_offset = 0_usize; + while selector_offset < descriptor_rows.len() { + let selector_domain_id = descriptor_rows[selector_offset].selector_domain_id; + let selector_start = selector_offset; + while selector_offset < descriptor_rows.len() + && descriptor_rows[selector_offset].selector_domain_id == selector_domain_id + { + selector_offset += 1; + } + let selector_end = selector_offset; + let segment_step_start = steps.len(); + let mut pending_runs = Vec::<(usize, usize)>::new(); + let mut offset = selector_start; + while offset < selector_end { + let run_start = offset; + offset += 1; + while offset < selector_end + && contribution_fanout_inputs_equal( + descriptor_rows[run_start], + descriptor_rows[offset], + metadata.exact_factor_is_kernel_input, + ) + { + offset += 1; + } + let run_count = offset - run_start; + if run_count == 1 { + flush_pending_fanout_batch( + plan, + descriptor, + metadata, + scratch_component_base, + unit_factor_id, + &mut pending_runs, + &mut steps, + &mut representatives, + &mut runs, + &mut bundles, + &mut scratch_component_count, + &mut needs_unit_factor, + )?; + push_direct_fanout_step( + &mut steps, + segment_step_start, + descriptor.row_start + run_start as u64, + 1, + )?; + } else { + pending_runs.push((run_start, run_count)); + if pending_runs.len() == DIRECT_CONTRIBUTION_FANOUT_BATCH_CAPACITY { + flush_pending_fanout_batch( + plan, + descriptor, + metadata, + scratch_component_base, + unit_factor_id, + &mut pending_runs, + &mut steps, + &mut representatives, + &mut runs, + &mut bundles, + &mut scratch_component_count, + &mut needs_unit_factor, + )?; + } + } + } + flush_pending_fanout_batch( + plan, + descriptor, + metadata, + scratch_component_base, + unit_factor_id, + &mut pending_runs, + &mut steps, + &mut representatives, + &mut runs, + &mut bundles, + &mut scratch_component_count, + &mut needs_unit_factor, + )?; + } + groups.push(DirectContributionFanoutGroup { + stage: descriptor.stage, + direct_executor_id: descriptor.direct_executor_id, + row_start: descriptor.row_start, + row_count: descriptor.row_count, + step_start: group_step_start, + step_count: u32::try_from(steps.len() as u64 - group_step_start).map_err(|_| { + RusticolError::integrity("direct fanout group step count exceeds u32") + })?, + }); + } + let interactions = DirectInteractionProgram::build_for_direct_plan(plan, executors)?; + let mut program = Self { + plan_layout_digest: plan.runtime_layout_digest(), + groups: groups.into_boxed_slice(), + steps: steps.into_boxed_slice(), + representatives: representatives.into_boxed_slice(), + runs: runs.into_boxed_slice(), + bundles: bundles.into_boxed_slice(), + scratch_component_base, + scratch_component_count, + unit_factor_id, + needs_unit_factor, + interactions, + full_singleton_steps: Box::new([]), + }; + program.full_singleton_steps = program.compile_full_singleton_steps(plan, executors)?; + Ok(program) + } + + fn compile_full_singleton_steps( + &self, + plan: &DirectRecurrencePlan, + executors: &DirectExecutorCatalog, + ) -> RusticolResult> { + let mut compiled = Vec::with_capacity(plan.row_groups().len()); + for (group_index, descriptor) in plan.row_groups().iter().enumerate() { + match self.interactions.action(group_index) { + DirectInteractionGroupAction::Consumed => continue, + DirectInteractionGroupAction::Execute(stage_index) => { + compiled.push(DirectFullSingletonStep::Interaction { stage_index }); + continue; + } + DirectInteractionGroupAction::Normal => {} + } + if descriptor.role == DirectExecutorRole::Source + && descriptor.direct_executor_id == DIRECT_NONE_U32 + && plan.strategy() == RecurrenceStrategy::AllFlowUnion + { + continue; + } + let row_start = usize::try_from(descriptor.row_start).map_err(|_| { + RusticolError::integrity("flattened direct row-group start exceeds usize") + })?; + let row_end = row_start + .checked_add(descriptor.row_count as usize) + .ok_or_else(|| { + RusticolError::integrity("flattened direct row-group range overflows usize") + })?; + let authoritative_len = match descriptor.role { + DirectExecutorRole::Source => plan.sources().len(), + DirectExecutorRole::Contribution => plan.contributions().len(), + DirectExecutorRole::Finalization => plan.finalizations().len(), + DirectExecutorRole::Closure => plan.closures().len(), + }; + if row_end > authoritative_len { + return Err(RusticolError::integrity( + "flattened direct row-group range is out of bounds", + )); + } + if descriptor.role == DirectExecutorRole::Contribution + && descriptor.direct_executor_id == DIRECT_NONE_U32 + { + compiled.push(DirectFullSingletonStep::CertifiedReuse { + row_start, + row_count: descriptor.row_count, + }); + continue; + } + let handle = executors.require(descriptor.direct_executor_id, descriptor.role)?; + if descriptor.role == DirectExecutorRole::Contribution { + let steps = self.group_steps(descriptor)?; + if steps + .iter() + .all(|step| matches!(step, DirectContributionFanoutStep::Direct { .. })) + { + compiled.push(DirectFullSingletonStep::Call { + direct_executor_id: descriptor.direct_executor_id, + handle, + row_start, + row_count: descriptor.row_count, + }); + continue; + } + let DirectExecutorHandle::Contribution { call, context } = handle else { + return Err(RusticolError::integrity( + "flattened direct contribution handle changed role", + )); + }; + compiled.push(DirectFullSingletonStep::Fanout { + group_index: u32::try_from(group_index).map_err(|_| { + RusticolError::integrity("flattened direct group index exceeds u32") + })?, + call, + context, + fused: executors.contribution_fanout(descriptor.direct_executor_id), + }); + continue; + } + compiled.push(DirectFullSingletonStep::Call { + direct_executor_id: descriptor.direct_executor_id, + handle, + row_start, + row_count: descriptor.row_count, + }); + } + Ok(compiled.into_boxed_slice()) + } + + fn execute_full_singleton_unprofiled( + &self, + plan: &DirectRecurrencePlan, + executors: &DirectExecutorCatalog, + workspace: &mut DirectWorkspace<'_>, + ) -> RusticolResult<()> { + clear_direct_executor_error_detail(); + workspace.validate(1)?; + if executors.plan_layout_digest != plan.runtime_layout_digest() + || self.plan_layout_digest != plan.runtime_layout_digest() + { + return Err(RusticolError::integrity( + "flattened direct execution belongs to a different plan", + )); + } + let (arena, momenta, parameters, factors) = workspace.raw_views()?; + for step in &self.full_singleton_steps { + match *step { + DirectFullSingletonStep::Interaction { stage_index } => { + self.interactions.execute::( + stage_index, + arena, + momenta, + parameters, + factors, + )?; + } + DirectFullSingletonStep::Call { + direct_executor_id, + handle, + row_start, + row_count, + } => { + let status = unsafe { + match handle { + DirectExecutorHandle::Source { call, context } => call( + context, + arena, + momenta, + parameters, + factors, + plan.sources().as_ptr().add(row_start), + row_count, + 1, + ), + DirectExecutorHandle::Contribution { call, context } => call( + context, + arena, + momenta, + parameters, + factors, + plan.contributions().as_ptr().add(row_start), + row_count, + 1, + ), + DirectExecutorHandle::Finalization { call, context } => call( + context, + arena, + momenta, + parameters, + factors, + plan.finalizations().as_ptr().add(row_start), + row_count, + 1, + ), + DirectExecutorHandle::Closure { call, context } => call( + context, + arena, + momenta, + parameters, + factors, + plan.closures().as_ptr().add(row_start), + row_count, + 1, + ), + } + }; + check_status(handle.role(), direct_executor_id, status)?; + } + DirectFullSingletonStep::CertifiedReuse { + row_start, + row_count, + } => { + let row_end = row_start + row_count as usize; + execute_certified_reuse_rows( + &plan.contributions()[row_start..row_end], + workspace, + 1, + )?; + } + DirectFullSingletonStep::Fanout { + group_index, + call, + context, + fused, + } => { + let descriptor = &plan.row_groups()[group_index as usize]; + execute_contribution_group_with_fanout( + plan, descriptor, self, call, context, fused, workspace, 1, + )?; + } + } + } + Ok(()) + } + + pub(crate) const fn scratch_component_count(&self) -> u32 { + self.scratch_component_count + } + + pub(crate) const fn needs_unit_factor(&self) -> bool { + self.needs_unit_factor + } + + #[cfg(test)] + pub(crate) fn row_counts(&self) -> (u64, u64) { + let logical = self + .groups + .iter() + .map(|group| u64::from(group.row_count)) + .sum(); + let evaluated = self + .steps + .iter() + .map(|step| match step { + DirectContributionFanoutStep::Direct { row_count, .. } => u64::from(*row_count), + DirectContributionFanoutStep::Fanout { + representative_count, + .. + } => u64::from(*representative_count), + }) + .sum(); + (logical, evaluated) + } + + #[cfg(test)] + pub(crate) fn scatter_bundle_counts(&self) -> (u64, u64) { + ( + self.runs.iter().map(|run| u64::from(run.row_count)).sum(), + self.bundles.len() as u64, + ) + } + + fn group_steps( + &self, + descriptor: &DirectRowGroupDescriptor, + ) -> RusticolResult<&[DirectContributionFanoutStep]> { + if descriptor.role != DirectExecutorRole::Contribution + || descriptor.direct_executor_id == DIRECT_NONE_U32 + { + return Err(RusticolError::integrity( + "direct fanout requested for a non-callable contribution group", + )); + } + let requested_end = descriptor + .row_start + .checked_add(u64::from(descriptor.row_count)) + .ok_or_else(|| RusticolError::integrity("direct fanout row range overflows u64"))?; + // Groups retain the authenticated row-group order, so locate the one + // containing a selected subrange without a warmed linear catalog scan. + let group_index = self + .groups + .partition_point(|group| group.row_start <= descriptor.row_start) + .checked_sub(1) + .ok_or_else(|| { + RusticolError::integrity( + "direct contribution row group is absent from its fanout program", + ) + })?; + let group = self + .groups + .get(group_index) + .filter(|group| { + let group_end = group.row_start + u64::from(group.row_count); + group.stage == descriptor.stage + && group.direct_executor_id == descriptor.direct_executor_id + && descriptor.row_start >= group.row_start + && requested_end <= group_end + }) + .ok_or_else(|| { + RusticolError::integrity( + "direct contribution row group is absent from its fanout program", + ) + })?; + let step_start = usize::try_from(group.step_start) + .map_err(|_| RusticolError::integrity("direct fanout step start exceeds usize"))?; + let step_end = step_start + .checked_add(group.step_count as usize) + .ok_or_else(|| RusticolError::integrity("direct fanout step range overflows usize"))?; + let group_steps = self.steps.get(step_start..step_end).ok_or_else(|| { + RusticolError::integrity("direct fanout group step range is out of bounds") + })?; + let first = group_steps + .iter() + .position(|step| step.row_start() == descriptor.row_start) + .ok_or_else(|| { + RusticolError::integrity( + "selected direct fanout range does not begin on a step boundary", + ) + })?; + let mut stop = first; + let mut covered = descriptor.row_start; + while stop < group_steps.len() && covered < requested_end { + let step = group_steps[stop]; + if step.row_start() != covered { + return Err(RusticolError::integrity( + "direct fanout steps do not continuously cover their row group", + )); + } + covered = covered + .checked_add(u64::from(step.row_count())) + .ok_or_else(|| { + RusticolError::integrity("direct fanout step coverage overflows u64") + })?; + stop += 1; + } + if covered != requested_end { + return Err(RusticolError::integrity( + "selected direct fanout range does not end on a step boundary", + )); + } + Ok(&group_steps[first..stop]) + } +} + +impl DirectSingletonContributionFanoutProgram { + pub(crate) fn build( + schedule_groups: &[DirectInteractionGroupView<'_>], + arena_component_count: u32, + momentum_form_count: u32, + parameter_count: u32, + factor_count: u32, + ) -> RusticolResult { + let mut groups = vec![None; schedule_groups.len()]; + let mut steps = Vec::new(); + let mut runs = Vec::new(); + let mut bundles = Vec::new(); + for (group_index, group) in schedule_groups.iter().copied().enumerate() { + let Some(rows) = group.contributions() else { + continue; + }; + let Some(capability) = group.capability else { + continue; + }; + let fanout = capability.fanout; + if rows.is_empty() + || rows.len() > u32::MAX as usize + || fanout.required_parameter_count > parameter_count + || rows.iter().any(|row| { + row.flags & DIRECT_CONTRIBUTION_FLAG_CERTIFIED_REUSE != 0 + || row.exact_factor_id >= factor_count + || row + .destination_component_base + .checked_add(fanout.destination_component_count) + .is_none_or(|end| end > arena_component_count) + }) + { + continue; + } + for row in rows { + validate_fused_fanout_row( + *row, + fanout.parent_component_counts, + fanout.requires_two_momenta, + arena_component_count, + momentum_form_count, + )?; + } + + let step_start = u64::try_from(steps.len()) + .map_err(|_| RusticolError::integrity("singleton fanout step count exceeds u64"))?; + let mut pending_runs = Vec::<(u32, u32)>::new(); + let mut offset = 0_usize; + while offset < rows.len() { + let run_start = offset; + offset += 1; + while offset < rows.len() + && contribution_fanout_inputs_equal(rows[run_start], rows[offset], false) + { + offset += 1; + } + let run_count = offset - run_start; + if run_count == 1 { + flush_singleton_fanout_batch( + rows, + &mut pending_runs, + &mut steps, + &mut runs, + &mut bundles, + )?; + push_singleton_direct_step( + &mut steps, + step_start as usize, + u32::try_from(run_start).map_err(|_| { + RusticolError::integrity( + "singleton fanout direct-row start exceeds u32", + ) + })?, + 1, + )?; + } else { + pending_runs.push(( + u32::try_from(run_start).map_err(|_| { + RusticolError::integrity("singleton fanout run start exceeds u32") + })?, + u32::try_from(run_count).map_err(|_| { + RusticolError::integrity("singleton fanout run count exceeds u32") + })?, + )); + if pending_runs.len() == DIRECT_CONTRIBUTION_FANOUT_BATCH_CAPACITY { + flush_singleton_fanout_batch( + rows, + &mut pending_runs, + &mut steps, + &mut runs, + &mut bundles, + )?; + } + } + } + flush_singleton_fanout_batch( + rows, + &mut pending_runs, + &mut steps, + &mut runs, + &mut bundles, + )?; + let step_count = u32::try_from(steps.len() as u64 - step_start).map_err(|_| { + RusticolError::integrity("singleton fanout group step count exceeds u32") + })?; + let group_steps = &steps[step_start as usize..]; + if !group_steps + .iter() + .any(|step| matches!(step, DirectSingletonFanoutStep::Fanout { .. })) + { + steps.truncate(step_start as usize); + continue; + } + let raw = group.handle.ok_or_else(|| { + RusticolError::integrity("singleton fanout group has no raw contribution handle") + })?; + if raw.role() != DirectExecutorRole::Contribution { + return Err(RusticolError::integrity( + "singleton fanout group has a non-contribution raw handle", + )); + } + groups[group_index] = Some(DirectSingletonFanoutGroup { + direct_executor_id: group.direct_executor_id, + row_count: rows.len() as u32, + step_start, + step_count, + raw, + fused: fanout, + }); + } + Ok(Self { + groups: groups.into_boxed_slice(), + steps: steps.into_boxed_slice(), + runs: runs.into_boxed_slice(), + bundles: bundles.into_boxed_slice(), + }) + } + + #[allow(clippy::too_many_arguments)] + pub(crate) fn execute_group( + &self, + group_index: usize, + rows: &[DirectContributionRow], + arena: DirectArenaView, + momenta: DirectMomentumView, + parameters: DirectParameterView, + factors: DirectFactorView, + point_count: u32, + ) -> RusticolResult { + if point_count != 1 { + return Ok(false); + } + let Some(group) = self.groups.get(group_index).and_then(|group| *group) else { + return Ok(false); + }; + if rows.len() != group.row_count as usize { + return Err(RusticolError::integrity( + "singleton fanout rows changed after cold compilation", + )); + } + let step_start = usize::try_from(group.step_start) + .map_err(|_| RusticolError::integrity("singleton fanout step start exceeds usize"))?; + let step_end = step_start + .checked_add(group.step_count as usize) + .ok_or_else(|| RusticolError::integrity("singleton fanout step range overflows"))?; + let steps = self.steps.get(step_start..step_end).ok_or_else(|| { + RusticolError::integrity("singleton fanout step range is out of bounds") + })?; + let DirectExecutorHandle::Contribution { call, context } = group.raw else { + return Err(RusticolError::integrity( + "singleton fanout raw handle changed role", + )); + }; + let mut covered = 0_u32; + for step in steps { + if step.row_start() != covered { + return Err(RusticolError::integrity( + "singleton fanout steps do not continuously cover their row group", + )); + } + match *step { + DirectSingletonFanoutStep::Direct { + row_start, + row_count, + } => { + let start = row_start as usize; + let end = start.checked_add(row_count as usize).ok_or_else(|| { + RusticolError::integrity("singleton fanout direct range overflows") + })?; + let direct_rows = rows.get(start..end).ok_or_else(|| { + RusticolError::integrity("singleton fanout direct range is out of bounds") + })?; + let status = unsafe { + call( + context, + arena, + momenta, + parameters, + factors, + direct_rows.as_ptr(), + row_count, + 1, + ) + }; + check_status( + DirectExecutorRole::Contribution, + group.direct_executor_id, + status, + )?; + } + DirectSingletonFanoutStep::Fanout { + run_start, + run_count, + .. + } => { + let start = usize::try_from(run_start).map_err(|_| { + RusticolError::integrity("singleton fanout run start exceeds usize") + })?; + let end = start.checked_add(run_count as usize).ok_or_else(|| { + RusticolError::integrity("singleton fanout run range overflows") + })?; + let runs = self.runs.get(start..end).ok_or_else(|| { + RusticolError::integrity("singleton fanout run range is out of bounds") + })?; + unsafe { + (group.fused.call)( + group.fused.context, + arena, + momenta, + parameters, + factors, + DirectContributionFanoutBatch { + rows, + runs, + bundles: &self.bundles, + }, + ) + }?; + } + } + covered = covered.checked_add(step.row_count()).ok_or_else(|| { + RusticolError::integrity("singleton fanout step coverage overflows u32") + })?; + } + if covered != group.row_count { + return Err(RusticolError::integrity( + "singleton fanout steps do not cover their row group", + )); + } + Ok(true) + } + + #[cfg(test)] + pub(crate) fn row_counts(&self) -> (u64, u64) { + let logical = self + .groups + .iter() + .flatten() + .map(|group| u64::from(group.row_count)) + .sum(); + let evaluated = self + .steps + .iter() + .map(|step| match step { + DirectSingletonFanoutStep::Direct { row_count, .. } => u64::from(*row_count), + DirectSingletonFanoutStep::Fanout { run_count, .. } => u64::from(*run_count), + }) + .sum(); + (logical, evaluated) + } +} + +#[derive(Clone, Copy)] +struct DirectInteractionRawRun { + row_start: u32, + row_count: u32, +} + +type DirectInteractionInputKey = (u32, u32, u32, u32, u32); + +fn interaction_input_key(row: DirectContributionRow) -> DirectInteractionInputKey { + ( + row.selector_domain_id, + row.parent0_component_base, + row.parent1_component_base_or_sentinel, + row.parent0_momentum_form_id, + row.parent1_momentum_form_id_or_sentinel, + ) +} + +fn interaction_raw_runs(rows: &[DirectContributionRow]) -> Option> { + if rows + .iter() + .any(|row| row.flags & DIRECT_CONTRIBUTION_FLAG_CERTIFIED_REUSE != 0) + { + return None; + } + let mut runs = Vec::new(); + let mut offset = 0_usize; + while offset < rows.len() { + let run_start = offset; + let key = interaction_input_key(rows[offset]); + offset += 1; + while offset < rows.len() && interaction_input_key(rows[offset]) == key { + offset += 1; + } + runs.push(DirectInteractionRawRun { + row_start: u32::try_from(run_start).ok()?, + row_count: u32::try_from(offset - run_start).ok()?, + }); + } + Some(runs) +} + +fn interaction_rows( + rows: &[DirectContributionRow], + run: DirectInteractionRawRun, +) -> Option<&[DirectContributionRow]> { + let start = run.row_start as usize; + rows.get(start..start.checked_add(run.row_count as usize)?) +} + +fn interaction_destination_offsets(rows: &[DirectContributionRow]) -> Option> { + let destinations = rows + .iter() + .enumerate() + .map(|(offset, row)| Some((row.destination_component_base, u32::try_from(offset).ok()?))) + .collect::>>()?; + (destinations.len() == rows.len()).then_some(destinations) +} + +fn interaction_destination_bijection( + color: &BTreeMap, + atv: &BTreeMap, +) -> Option> { + if color.len() != atv.len() || !color.keys().eq(atv.keys()) { + return None; + } + let mut row_offsets = vec![DIRECT_NONE_U32; color.len()]; + for (&destination, &color_offset) in color { + let atv_offset = *atv.get(&destination)?; + *row_offsets.get_mut(color_offset as usize)? = atv_offset; + } + (!row_offsets.contains(&DIRECT_NONE_U32)).then_some(row_offsets) +} + +fn interaction_vector_initialization_is_canonical( + ordered_groups: &[&[DirectContributionRow]], +) -> bool { + let mut seen = BTreeSet::new(); + ordered_groups + .iter() + .flat_map(|rows| rows.iter()) + .all(|row| { + let first = seen.insert(row.destination_component_base); + let initializes = row.flags & DIRECT_CONTRIBUTION_FLAG_INITIALIZE_DESTINATION != 0; + initializes == first + }) +} + +#[cfg(test)] +mod interaction_initialization_tests { + use super::*; + + fn row(destination_component_base: u32, initializes: bool) -> DirectContributionRow { + DirectContributionRow { + parent0_component_base: 0, + parent1_component_base_or_sentinel: 4, + parent0_momentum_form_id: 0, + parent1_momentum_form_id_or_sentinel: 1, + destination_component_base, + exact_factor_id: 0, + selector_domain_id: 0, + flags: if initializes { + DIRECT_CONTRIBUTION_FLAG_INITIALIZE_DESTINATION + } else { + 0 + }, + } + } + + #[test] + fn interaction_initialization_requires_the_first_authoritative_write() { + let first_group = [row(20, true), row(24, true), row(20, false)]; + let second_group = [row(24, false)]; + assert!(interaction_vector_initialization_is_canonical(&[ + &first_group, + &second_group, + ])); + + let late_initializer = [row(20, false), row(20, true)]; + assert!(!interaction_vector_initialization_is_canonical(&[ + &late_initializer, + ])); + let duplicate_initializer = [row(20, true), row(20, true)]; + assert!(!interaction_vector_initialization_is_canonical(&[ + &duplicate_initializer, + ])); + } +} + +fn interaction_kind( + group: DirectInteractionGroupView<'_>, +) -> Option { + if group.role != DirectExecutorRole::Contribution + || group.direct_executor_id == DIRECT_NONE_U32 + || group.capability?.metadata.exact_factor_is_kernel_input + { + return None; + } + Some(group.capability?.fanout.interaction_kind) +} + +fn direct_interaction_group_views<'a>( + plan: &'a DirectRecurrencePlan, + executors: &DirectExecutorCatalog, +) -> RusticolResult>> { + let mut groups = Vec::with_capacity(plan.row_groups().len()); + for descriptor in plan.row_groups() { + let start = usize::try_from(descriptor.row_start) + .map_err(|_| RusticolError::integrity("direct interaction row start exceeds usize"))?; + let end = start + .checked_add(descriptor.row_count as usize) + .ok_or_else(|| { + RusticolError::integrity("direct interaction row range overflows usize") + })?; + let stage = u32::from(descriptor.stage); + groups.push(match descriptor.role { + DirectExecutorRole::Contribution => { + let rows = plan.contributions().get(start..end).ok_or_else(|| { + RusticolError::integrity("direct interaction contribution rows are absent") + })?; + let handle = (descriptor.direct_executor_id != DIRECT_NONE_U32) + .then(|| { + executors.require( + descriptor.direct_executor_id, + DirectExecutorRole::Contribution, + ) + }) + .transpose()?; + let capability = executors + .contribution_fanout(descriptor.direct_executor_id) + .map(|fanout| { + let handle = handle.ok_or_else(|| { + RusticolError::integrity( + "direct interaction capability has no contribution handle", + ) + })?; + DirectInteractionExecutorCapability::new( + handle, + executors.contribution_metadata(descriptor.direct_executor_id)?, + fanout, + ) + }) + .transpose()?; + DirectInteractionGroupView::contribution( + stage, + descriptor.direct_executor_id, + handle, + capability, + rows, + ) + } + DirectExecutorRole::Closure => { + let rows = plan.closures().get(start..end).ok_or_else(|| { + RusticolError::integrity("direct interaction closure rows are absent") + })?; + DirectInteractionGroupView::closure( + stage, + descriptor.direct_executor_id, + executors.require(descriptor.direct_executor_id, descriptor.role)?, + rows, + ) + } + role => DirectInteractionGroupView::other( + stage, + role, + descriptor.direct_executor_id, + (descriptor.direct_executor_id != DIRECT_NONE_U32) + .then(|| executors.require(descriptor.direct_executor_id, role)) + .transpose()?, + ), + }); + } + Ok(groups) +} + +impl DirectInteractionProgram { + fn build_for_direct_plan( + plan: &DirectRecurrencePlan, + executors: &DirectExecutorCatalog, + ) -> RusticolResult { + let groups = direct_interaction_group_views(plan, executors)?; + let schedule = DirectInteractionScheduleView::new( + plan.strategy(), + &groups, + plan.currents(), + plan.contributions().len(), + ); + Self::build(schedule) + } + + #[cfg_attr(target_vendor = "apple", unsafe(link_section = "__TEXT,__rcl_load"))] + #[cfg_attr(target_vendor = "apple", inline(never))] + pub(crate) fn build(schedule: DirectInteractionScheduleView<'_>) -> RusticolResult { + let mut actions = vec![DirectInteractionGroupAction::Normal; schedule.groups.len()]; + let mut stages = Vec::new(); + let mut stage_groups = BTreeMap::>::new(); + for (group_index, group) in schedule.groups.iter().enumerate() { + if group.role == DirectExecutorRole::Contribution { + stage_groups + .entry(group.stage) + .or_default() + .push(group_index); + } + } + let raw_runs = schedule + .groups + .iter() + .enumerate() + .filter_map(|(group_index, &group)| { + interaction_kind(group)?; + Some((group_index, interaction_raw_runs(group.contributions()?)?)) + }) + .collect::>(); + + for group_indices in stage_groups.values() { + let Some(candidate) = match_interaction_stage(&schedule, group_indices, &raw_runs) + else { + continue; + }; + let stage_index = u32::try_from(stages.len()).map_err(|_| { + RusticolError::integrity("direct interaction stage count exceeds u32") + })?; + let first_group = *candidate + .group_indices + .iter() + .min() + .expect("interaction has contribution groups"); + for &group_index in &candidate.group_indices { + actions[group_index] = if group_index == first_group { + DirectInteractionGroupAction::Execute(stage_index) + } else { + DirectInteractionGroupAction::Consumed + }; + } + if let Some(group_index) = candidate.closure_group_index { + actions[group_index] = DirectInteractionGroupAction::Consumed; + } + let stored_group_indices = candidate + .group_indices + .iter() + .copied() + .map(|group_index| { + u32::try_from(group_index).map_err(|_| { + RusticolError::integrity("direct interaction group index exceeds u32") + }) + }) + .collect::>>()? + .into_boxed_slice(); + let closure_group_index = candidate + .closure_group_index + .map(|group_index| { + u32::try_from(group_index).map_err(|_| { + RusticolError::integrity("direct terminal closure group index exceeds u32") + }) + }) + .transpose()?; + stages.push(DirectInteractionStage { + group_indices: stored_group_indices, + closure_group_index, + call: candidate.call, + contexts: candidate.contexts, + atv_before_color: candidate.atv_before_color, + anchors: candidate.anchors.into_boxed_slice(), + outputs: candidate.outputs.into_boxed_slice(), + tensor_outputs: candidate.tensor_outputs.into_boxed_slice(), + terminal: candidate.terminal, + requirements: candidate.requirements, + logical_row_count: candidate.logical_row_count, + }); + } + + Ok(Self { + actions: actions.into_boxed_slice(), + stages: stages.into_boxed_slice(), + }) + } + + pub(crate) fn action(&self, group_index: usize) -> DirectInteractionGroupAction { + self.actions + .get(group_index) + .copied() + .unwrap_or(DirectInteractionGroupAction::Normal) + } + + pub(crate) fn execute( + &self, + stage_index: u32, + arena: DirectArenaView, + momenta: DirectMomentumView, + parameters: DirectParameterView, + factors: DirectFactorView, + ) -> RusticolResult { + let stage = self.stages.get(stage_index as usize).ok_or_else(|| { + RusticolError::integrity("direct interaction stage index is out of bounds") + })?; + execute_direct_interaction_stage_views::( + stage, arena, momenta, parameters, factors, + ) + } + + pub(crate) fn logical_groups( + &self, + stage_index: u32, + ) -> RusticolResult<(&[u32], Option, u64)> { + let stage = self.stages.get(stage_index as usize).ok_or_else(|| { + RusticolError::integrity("direct interaction stage index is out of bounds") + })?; + Ok(( + &stage.group_indices, + stage.closure_group_index, + stage.logical_row_count, + )) + } +} + +struct DirectInteractionStageCandidate { + group_indices: Vec, + closure_group_index: Option, + call: DirectInteractionBundleExecutor, + contexts: DirectInteractionExecutorContexts, + atv_before_color: bool, + logical_row_count: u64, + anchors: Vec, + outputs: Vec, + tensor_outputs: Vec, + terminal: Option, + requirements: DirectInteractionRuntimeRequirements, +} + +struct DirectInteractionMatchedSupplement { + row_start: u32, + vector_parent_side: u8, + row_offsets: Vec, +} + +// The sibling-wedge rewrite adds enough hot code to pay for itself only on +// large schedules. Keep the narrower color/ATV interaction rewrite below this +// cold, process-independent workload boundary. Terminal scalarization is a +// separate win: once the cold liveness proof succeeds it removes an entire +// four-component root/closure round trip, including on small schedules. +const DIRECT_LARGE_SCHEDULE_INTERACTION_MIN_ROWS: usize = 1_000_000; + +#[inline] +fn direct_large_schedule_interaction_rewrite_enabled( + strategy: RecurrenceStrategy, + contribution_row_count: usize, +) -> bool { + strategy == RecurrenceStrategy::ContractedColorUnion + && contribution_row_count >= DIRECT_LARGE_SCHEDULE_INTERACTION_MIN_ROWS +} + +#[inline] +fn direct_terminal_interaction_rewrite_enabled(strategy: RecurrenceStrategy) -> bool { + strategy == RecurrenceStrategy::ContractedColorUnion +} + +fn match_interaction_stage( + schedule: &DirectInteractionScheduleView<'_>, + group_indices: &[usize], + raw_runs: &BTreeMap>, +) -> Option { + if !(2..=3).contains(&group_indices.len()) + || !group_indices.windows(2).all(|pair| pair[1] == pair[0] + 1) + { + return None; + } + let mut color = None; + let mut wedge = None; + let mut atv = None; + for &group_index in group_indices { + let group = schedule.groups[group_index]; + let slot = match interaction_kind(group)? { + DirectInteractionIntrinsicKind::ColorOrderedThreeVector => &mut color, + DirectInteractionIntrinsicKind::VectorWedgeVector => &mut wedge, + DirectInteractionIntrinsicKind::AntisymmetricTensorVector => &mut atv, + }; + if slot.replace(group_index).is_some() { + return None; + } + } + let (color_index, atv_index) = (color?, atv?); + if wedge.is_some() != (group_indices.len() == 3) { + return None; + } + let color = schedule.groups[color_index]; + let atv = schedule.groups[atv_index]; + let color_group_rows = color.contributions()?; + let atv_group_rows = atv.contributions()?; + let ordered_vector_groups = if color_index < atv_index { + [color_group_rows, atv_group_rows] + } else { + [atv_group_rows, color_group_rows] + }; + if !interaction_vector_initialization_is_canonical(&ordered_vector_groups) { + return None; + } + let color_capability = color.capability?.fanout; + let atv_capability = atv.capability?.fanout; + let call = color_capability.interaction_call?; + let color_runs = raw_runs.get(&color_index)?.clone(); + let atv_runs = raw_runs.get(&atv_index)?; + let fuse_wedge = direct_large_schedule_interaction_rewrite_enabled( + schedule.strategy, + schedule.contribution_row_count, + ); + let wedge_program = if let Some(wedge_index) = wedge.filter(|_| fuse_wedge) { + let group = schedule.groups[wedge_index]; + let capability = group.capability?.fanout; + if capability.destination_component_count != 6 + || capability.parent_component_counts != [4, 4] + || capability.requires_two_momenta + { + return None; + } + let wedge_runs = raw_runs.get(&wedge_index)?.clone(); + if wedge_runs.len() != color_runs.len() { + return None; + } + for (&color_run, &wedge_run) in color_runs.iter().zip(&wedge_runs) { + let color_rows = interaction_rows(color_group_rows, color_run)?; + let wedge_rows = interaction_rows(group.contributions()?, wedge_run)?; + if color_run.row_count != wedge_run.row_count + || interaction_input_key(*color_rows.first()?) + != interaction_input_key(*wedge_rows.first()?) + || !color_rows + .iter() + .zip(wedge_rows) + .all(|(color_row, wedge_row)| { + color_row.exact_factor_id == wedge_row.exact_factor_id + }) + { + return None; + } + } + Some((capability, wedge_runs)) + } else { + None + }; + + type AssociationKey = (u32, u32, u32, u32, u32, u32); + let mut anchors_by_vector = BTreeMap::>::new(); + let mut color_destination_offsets = Vec::with_capacity(color_runs.len()); + for (anchor_index, run) in color_runs.iter().copied().enumerate() { + let rows = interaction_rows(color_group_rows, run)?; + let first = *rows.first()?; + let destinations = interaction_destination_offsets(rows)?; + let minimum_destination = *destinations.keys().next()?; + let maximum_destination = *destinations.keys().next_back()?; + for (side, base, momentum) in [ + ( + 0_u8, + first.parent0_component_base, + first.parent0_momentum_form_id, + ), + ( + 1_u8, + first.parent1_component_base_or_sentinel, + first.parent1_momentum_form_id_or_sentinel, + ), + ] { + anchors_by_vector + .entry(( + first.selector_domain_id, + base, + momentum, + run.row_count, + minimum_destination, + maximum_destination, + )) + .or_default() + .push((anchor_index, side)); + } + color_destination_offsets.push(destinations); + } + let mut matched_supplements = (0..color_runs.len()) + .map(|_| Vec::new()) + .collect::>>(); + let mut used_sides = vec![[false; 2]; color_runs.len()]; + for &atv_run in atv_runs { + let atv_rows = interaction_rows(atv_group_rows, atv_run)?; + let representative = *atv_rows.first()?; + let atv_destination_offsets = interaction_destination_offsets(atv_rows)?; + let minimum_destination = *atv_destination_offsets.keys().next()?; + let maximum_destination = *atv_destination_offsets.keys().next_back()?; + let candidates = anchors_by_vector.get(&( + representative.selector_domain_id, + representative.parent1_component_base_or_sentinel, + representative.parent1_momentum_form_id_or_sentinel, + atv_run.row_count, + minimum_destination, + maximum_destination, + ))?; + let mut matches = candidates.iter().copied().filter(|(anchor_index, _)| { + color_destination_offsets[*anchor_index] + .keys() + .eq(atv_destination_offsets.keys()) + }); + let (anchor_index, vector_parent_side) = matches.next()?; + if matches.next().is_some() || used_sides[anchor_index][vector_parent_side as usize] { + return None; + } + used_sides[anchor_index][vector_parent_side as usize] = true; + let row_offsets = interaction_destination_bijection( + &color_destination_offsets[anchor_index], + &atv_destination_offsets, + )?; + matched_supplements[anchor_index].push(DirectInteractionMatchedSupplement { + row_start: atv_run.row_start, + vector_parent_side, + row_offsets, + }); + } + if matched_supplements.iter().flatten().count() == 0 { + return None; + } + + // Identical destination ranges are expected across formula groups and + // repeated interactions. Distinct partially-overlapping ranges would make + // a single aggregated write ambiguous, so reject that stage. + let mut destination_widths = BTreeMap::::new(); + for &group_index in group_indices { + let group = schedule.groups[group_index]; + let capability = group.capability?.fanout; + let rows = group.contributions()?; + for row in rows { + if destination_widths + .insert( + row.destination_component_base, + capability.destination_component_count, + ) + .is_some_and(|width| width != capability.destination_component_count) + { + return None; + } + } + } + let mut previous_end = 0_u32; + for (&base, &width) in &destination_widths { + if base < previous_end { + return None; + } + previous_end = base.checked_add(width)?; + } + + let mut seen_vector_destinations = BTreeSet::new(); + let mut candidate_anchors = Vec::with_capacity(color_runs.len()); + let mut candidate_outputs = Vec::with_capacity(color_group_rows.len()); + let mut candidate_tensor_outputs = Vec::with_capacity( + wedge + .and_then(|group_index| schedule.groups[group_index].contributions()) + .map_or(0, <[DirectContributionRow]>::len), + ); + let mut requirements = DirectInteractionRuntimeRequirements { + parameter_count: color_capability + .required_parameter_count + .max(atv_capability.required_parameter_count), + ..DirectInteractionRuntimeRequirements::default() + }; + if let Some((capability, _)) = &wedge_program { + requirements.parameter_count = requirements + .parameter_count + .max(capability.required_parameter_count); + } + for (anchor_index, color_run) in color_runs.into_iter().enumerate() { + let color_rows = interaction_rows(color_group_rows, color_run)?; + let representative = *color_rows.first()?; + requirements.include_current_span( + representative.parent0_component_base, + color_capability.parent_component_counts[0], + )?; + requirements.include_current_span( + representative.parent1_component_base_or_sentinel, + color_capability.parent_component_counts[1], + )?; + requirements.include_momentum(representative.parent0_momentum_form_id)?; + requirements.include_momentum(representative.parent1_momentum_form_id_or_sentinel)?; + + let supplements_for_anchor = &matched_supplements[anchor_index]; + if supplements_for_anchor.len() > 2 { + return None; + } + let mut compiled_operands = [DirectInteractionOperand { + tensor_component_base: 0, + vector_parent: DirectInteractionVectorParent::Parent0, + }; 2]; + let mut supplement_rows = [None; 2]; + for (slot, supplement) in supplements_for_anchor.iter().enumerate() { + let rows = interaction_rows( + atv_group_rows, + DirectInteractionRawRun { + row_start: supplement.row_start, + row_count: color_run.row_count, + }, + )?; + let atv_representative = *rows.first()?; + let vector_parent = match supplement.vector_parent_side { + 0 => DirectInteractionVectorParent::Parent0, + 1 => DirectInteractionVectorParent::Parent1, + _ => return None, + }; + requirements.include_current_span( + atv_representative.parent0_component_base, + atv_capability.parent_component_counts[0], + )?; + compiled_operands[slot] = DirectInteractionOperand { + tensor_component_base: atv_representative.parent0_component_base, + vector_parent, + }; + supplement_rows[slot] = Some(rows); + } + let operands = match supplements_for_anchor.len() { + 0 => DirectInteractionOperands::None, + 1 => DirectInteractionOperands::One(compiled_operands[0]), + 2 => DirectInteractionOperands::Two(compiled_operands), + _ => unreachable!("interaction supplement count was bounded above"), + }; + + let output_start = u32::try_from(candidate_outputs.len()).ok()?; + for (offset, color_row) in color_rows.iter().enumerate() { + requirements.include_current_span( + color_row.destination_component_base, + color_capability.destination_component_count, + )?; + requirements.include_factor(color_row.exact_factor_id)?; + let mut exact_factor_ids = [color_row.exact_factor_id, 0, 0]; + for (slot, supplement) in supplements_for_anchor.iter().enumerate() { + let atv_offset = *supplement.row_offsets.get(offset)?; + let atv_row = *supplement_rows[slot]?.get(atv_offset as usize)?; + if atv_row.destination_component_base != color_row.destination_component_base { + return None; + } + requirements.include_factor(atv_row.exact_factor_id)?; + exact_factor_ids[slot + 1] = atv_row.exact_factor_id; + } + candidate_outputs.push(DirectInteractionOutput { + destination_component_base: color_row.destination_component_base, + exact_factor_ids, + initialize: seen_vector_destinations.insert(color_row.destination_component_base), + }); + } + let output_end = u32::try_from(candidate_outputs.len()).ok()?; + let tensor_output_start = u32::try_from(candidate_tensor_outputs.len()).ok()?; + if let Some((wedge_capability, wedge_runs)) = &wedge_program { + let wedge_rows = interaction_rows( + schedule.groups[wedge?].contributions()?, + *wedge_runs.get(anchor_index)?, + )?; + for wedge_row in wedge_rows { + requirements.include_current_span( + wedge_row.destination_component_base, + wedge_capability.destination_component_count, + )?; + requirements.include_factor(wedge_row.exact_factor_id)?; + candidate_tensor_outputs.push(DirectInteractionTensorOutput { + destination_component_base: wedge_row.destination_component_base, + exact_factor_id: wedge_row.exact_factor_id, + initialize: wedge_row.flags & DIRECT_CONTRIBUTION_FLAG_INITIALIZE_DESTINATION + != 0, + }); + } + } + let tensor_output_end = u32::try_from(candidate_tensor_outputs.len()).ok()?; + candidate_anchors.push(DirectInteractionAnchor { + parent_component_bases: [ + representative.parent0_component_base, + representative.parent1_component_base_or_sentinel, + ], + parent_momentum_form_ids: [ + representative.parent0_momentum_form_id, + representative.parent1_momentum_form_id_or_sentinel, + ], + operands, + output_start, + output_end, + tensor_output_start, + tensor_output_end, + }); + } + + let tensor_destinations = candidate_tensor_outputs + .iter() + .map(|output| output.destination_component_base) + .collect::>(); + if candidate_anchors + .iter() + .any(|anchor| match anchor.operands { + DirectInteractionOperands::None => false, + DirectInteractionOperands::One(operand) => { + tensor_destinations.contains(&operand.tensor_component_base) + } + DirectInteractionOperands::Two(operands) => operands + .iter() + .any(|operand| tensor_destinations.contains(&operand.tensor_component_base)), + }) + { + return None; + } + + let mut native_group_indices = vec![color_index, atv_index]; + if let Some(wedge_index) = wedge.filter(|_| fuse_wedge) { + native_group_indices.push(wedge_index); + } + native_group_indices.sort_unstable(); + let logical_row_count = native_group_indices + .iter() + .map(|&group_index| { + schedule.groups[group_index] + .contributions() + .map(|rows| rows.len() as u64) + .unwrap_or(0) + }) + .sum(); + let mut candidate = DirectInteractionStageCandidate { + group_indices: native_group_indices, + closure_group_index: None, + call, + contexts: DirectInteractionExecutorContexts { + color: color_capability.context, + vector_wedge_vector: wedge_program + .as_ref() + .map_or(std::ptr::null(), |(capability, _)| capability.context), + antisymmetric_tensor_vector: atv_capability.context, + }, + atv_before_color: atv_index < color_index, + logical_row_count, + anchors: candidate_anchors, + outputs: candidate_outputs, + tensor_outputs: candidate_tensor_outputs, + terminal: None, + requirements, + }; + attach_terminal_interaction_program(schedule, &mut candidate); + Some(candidate) +} + +fn attach_terminal_interaction_program( + schedule: &DirectInteractionScheduleView<'_>, + candidate: &mut DirectInteractionStageCandidate, +) { + let Some((closure_group_index, terminal)) = + compile_terminal_interaction_program(schedule, candidate) + else { + return; + }; + let mut requirements = candidate.requirements; + if requirements + .include_current_span(terminal.partner_component_base, 4) + .is_none() + { + return; + } + for component in 0..4 { + let Some(factor_id) = terminal.component_factor_start.checked_add(component) else { + return; + }; + if requirements.include_factor(factor_id).is_none() { + return; + } + } + for closure in terminal.closures.iter() { + if requirements + .include_factor(closure.exact_factor_id) + .is_none() + { + return; + } + } + candidate.requirements = requirements; + candidate.closure_group_index = Some(closure_group_index); + candidate.terminal = Some(terminal); +} + +fn compile_terminal_interaction_program( + schedule: &DirectInteractionScheduleView<'_>, + candidate: &DirectInteractionStageCandidate, +) -> Option<(usize, DirectInteractionTerminalProgram)> { + if !direct_terminal_interaction_rewrite_enabled(schedule.strategy) + || !candidate.tensor_outputs.is_empty() + || candidate.group_indices.len() != 2 + || candidate.outputs.is_empty() + || schedule.currents.is_empty() + { + return None; + } + let mut closure_groups = schedule + .groups + .iter() + .enumerate() + .filter(|(_, group)| group.role == DirectExecutorRole::Closure); + let (closure_group_index, closure_group) = closure_groups.next()?; + if closure_groups.next().is_some() { + return None; + } + let DirectExecutorHandle::Closure { call, .. } = closure_group.handle? else { + return None; + }; + if !std::ptr::fn_addr_eq( + call, + crate::engine::execute_closure_reduce_rows as DirectClosureExecutor, + ) { + return None; + } + let interaction_stage = schedule.groups[*candidate.group_indices.first()?].stage; + if candidate + .group_indices + .iter() + .any(|&group_index| schedule.groups[group_index].stage != interaction_stage) + || interaction_stage.checked_add(1)? != closure_group.stage + { + return None; + } + let closure_event = closure_group.stage; + + let current_by_stage_and_base = schedule + .currents + .iter() + .filter(|current| current.node_kind == DirectNodeKind::Current) + .map(|current| ((u32::from(current.stage), current.component_base), current)) + .collect::>(); + let root_bases = candidate + .outputs + .iter() + .map(|output| output.destination_component_base) + .collect::>(); + let mut root_semantic_ids = BTreeSet::new(); + for &base in &root_bases { + let root = *current_by_stage_and_base.get(&(interaction_stage, base))?; + if root.component_count != 4 + || root.finalization_row_or_sentinel != DIRECT_NONE_U32 + || root.last_use != closure_event + { + return None; + } + root_semantic_ids.insert(root.semantic_current_id); + } + + // Every write to a scalarized root must belong to this native stage. + // Otherwise the scalar scratch would omit a contribution or close before + // the final writer. + let consumed_groups = candidate + .group_indices + .iter() + .copied() + .collect::>(); + for (group_index, group) in schedule.groups.iter().enumerate().filter(|(_, group)| { + group.role == DirectExecutorRole::Contribution && group.stage == interaction_stage + }) { + if group.contributions()?.iter().any(|row| { + root_bases.contains(&row.destination_component_base) + && !consumed_groups.contains(&group_index) + }) { + return None; + } + } + // No scalarized root may feed another contribution at this or a later + // stage; the closure group below must be its only remaining consumer. + for group in schedule.groups.iter().filter(|group| { + group.role == DirectExecutorRole::Contribution && group.stage >= interaction_stage + }) { + if group.contributions()?.iter().any(|row| { + root_bases.contains(&row.parent0_component_base) + || root_bases.contains(&row.parent1_component_base_or_sentinel) + }) { + return None; + } + } + + // At the closure event arena liveness makes these bases unambiguous even + // though the arena is reused between earlier stages. + let mut live_current_by_base = BTreeMap::new(); + for current in schedule + .currents + .iter() + .filter(|current| current.last_use == closure_event) + { + if live_current_by_base + .insert(current.component_base, current) + .is_some() + { + return None; + } + } + let mut partner = None; + let mut component_factor_start = None; + let mut attached_roots = BTreeSet::new(); + let mut amplitude_destinations = BTreeSet::new(); + let mut amplitude_destination_count = 0_u32; + let closure_rows = closure_group.closures()?; + if closure_rows.is_empty() { + return None; + } + let mut closures = Vec::with_capacity(closure_rows.len()); + for row in closure_rows { + if row.component_count != 4 || !amplitude_destinations.insert(row.amplitude_destination_id) + { + return None; + } + amplitude_destination_count = + amplitude_destination_count.max(row.amplitude_destination_id.checked_add(1)?); + let parent0 = *live_current_by_base.get(&row.parent0_component_base)?; + let parent1 = *live_current_by_base.get(&row.parent1_component_base_or_sentinel)?; + let parent0_is_root = root_semantic_ids.contains(&parent0.semantic_current_id); + let parent1_is_root = root_semantic_ids.contains(&parent1.semantic_current_id); + if parent0_is_root == parent1_is_root { + return None; + } + let (root, terminal_partner) = if parent0_is_root { + (parent0, parent1) + } else { + (parent1, parent0) + }; + if terminal_partner.component_count != 4 + || u32::from(terminal_partner.stage) >= interaction_stage + || terminal_partner.last_use != closure_event + || terminal_partner.finalization_row_or_sentinel != DIRECT_NONE_U32 + { + return None; + } + let partner_key = ( + terminal_partner.semantic_current_id, + terminal_partner.component_base, + ); + if partner + .replace(partner_key) + .is_some_and(|seen| seen != partner_key) + || component_factor_start + .replace(row.component_factor_start) + .is_some_and(|seen| seen != row.component_factor_start) + { + return None; + } + attached_roots.insert(root.semantic_current_id); + closures.push(DirectInteractionTerminalClosure { + root_component_base: root.component_base, + amplitude_destination_id: row.amplitude_destination_id, + exact_factor_id: row.exact_factor_id, + }); + } + if attached_roots != root_semantic_ids { + return None; + } + Some(( + closure_group_index, + DirectInteractionTerminalProgram { + partner_component_base: partner?.1, + component_factor_start: component_factor_start?, + amplitude_destination_count, + closures: closures.into_boxed_slice(), + }, + )) +} + +#[cfg(test)] +mod terminal_current_descriptor_tests { + use super::*; + + unsafe fn interaction_stub( + _contexts: DirectInteractionExecutorContexts, + _arena: DirectArenaView, + _momenta: DirectMomentumView, + _parameters: DirectParameterView, + _factors: DirectFactorView, + _batch: DirectInteractionBundleBatch<'_>, + ) -> RusticolResult<()> { + Ok(()) + } + + fn candidate() -> DirectInteractionStageCandidate { + DirectInteractionStageCandidate { + group_indices: vec![0, 1], + closure_group_index: None, + call: interaction_stub, + contexts: DirectInteractionExecutorContexts { + color: std::ptr::null(), + vector_wedge_vector: std::ptr::null(), + antisymmetric_tensor_vector: std::ptr::null(), + }, + atv_before_color: false, + logical_row_count: 2, + anchors: Vec::new(), + outputs: vec![DirectInteractionOutput { + destination_component_base: 20, + exact_factor_ids: [0; 3], + initialize: true, + }], + tensor_outputs: Vec::new(), + terminal: None, + requirements: DirectInteractionRuntimeRequirements::default(), + } + } + + #[test] + fn terminal_compiler_requires_and_consumes_current_descriptors() { + let contribution = DirectContributionRow { + parent0_component_base: 4, + parent1_component_base_or_sentinel: 8, + parent0_momentum_form_id: 0, + parent1_momentum_form_id_or_sentinel: 1, + destination_component_base: 20, + exact_factor_id: 0, + selector_domain_id: 0, + flags: DIRECT_CONTRIBUTION_FLAG_INITIALIZE_DESTINATION, + }; + let closure = DirectClosureRow { + parent0_component_base: 20, + parent1_component_base_or_sentinel: 0, + parent0_momentum_form_id: 0, + parent1_momentum_form_id_or_sentinel: 0, + amplitude_destination_id: 0, + exact_factor_id: 0, + component_factor_start: 0, + component_count: 4, + selector_domain_id: 0, + flags: 0, + }; + let contribution0 = [contribution]; + let contribution1 = [DirectContributionRow { + flags: 0, + ..contribution + }]; + let closures = [closure]; + let groups = [ + DirectInteractionGroupView::contribution(1, 1, None, None, &contribution0), + DirectInteractionGroupView::contribution(1, 2, None, None, &contribution1), + DirectInteractionGroupView::closure( + 2, + 3, + DirectExecutorHandle::Closure { + call: crate::engine::execute_closure_reduce_rows, + context: std::ptr::null(), + }, + &closures, + ), + ]; + let currents = [ + DirectCurrentDescriptor { + semantic_current_id: 0, + node_kind: DirectNodeKind::Current, + state_template_id: 0, + component_base: 20, + component_count: 4, + momentum_form_id: 0, + stage: 1, + selector_domain_id: 0, + first_use: 1, + last_use: 2, + source_row_or_sentinel: DIRECT_NONE_U32, + finalization_row_or_sentinel: DIRECT_NONE_U32, + }, + DirectCurrentDescriptor { + semantic_current_id: 1, + node_kind: DirectNodeKind::Source, + state_template_id: 0, + component_base: 0, + component_count: 4, + momentum_form_id: 0, + stage: 0, + selector_domain_id: 0, + first_use: 0, + last_use: 2, + source_row_or_sentinel: 0, + finalization_row_or_sentinel: DIRECT_NONE_U32, + }, + ]; + let without_currents = DirectInteractionScheduleView::new( + RecurrenceStrategy::ContractedColorUnion, + &groups, + &[], + 2, + ); + assert!(compile_terminal_interaction_program(&without_currents, &candidate()).is_none()); + + let with_currents = DirectInteractionScheduleView::new( + RecurrenceStrategy::ContractedColorUnion, + &groups, + ¤ts, + 2, + ); + let (closure_group, terminal) = + compile_terminal_interaction_program(&with_currents, &candidate()).unwrap(); + assert_eq!(closure_group, 2); + assert_eq!(terminal.partner_component_base, 0); + assert_eq!(terminal.closures.len(), 1); + assert_eq!(terminal.closures[0].root_component_base, 20); + } +} + +fn validate_fused_fanout_row( + row: DirectContributionRow, + parent_component_counts: [u32; 2], + requires_two_momenta: bool, + arena_component_count: u32, + momentum_form_count: u32, +) -> RusticolResult<()> { + for (base, required) in [ + (row.parent0_component_base, parent_component_counts[0]), + ( + row.parent1_component_base_or_sentinel, + parent_component_counts[1], + ), + ] { + if base == DIRECT_NONE_U32 + || base + .checked_add(required) + .is_none_or(|end| end > arena_component_count) + { + return Err(RusticolError::integrity( + "direct fused fanout parent component span is incompatible with its intrinsic", + )); + } + } + if requires_two_momenta + && (row.parent0_momentum_form_id >= momentum_form_count + || row.parent1_momentum_form_id_or_sentinel >= momentum_form_count) + { + return Err(RusticolError::integrity( + "direct fused fanout momentum forms are incompatible with its intrinsic", + )); + } + Ok(()) +} + +#[cfg(test)] +mod fused_fanout_validation_tests { + use super::*; + + fn interaction_row(destination_component_base: u32) -> DirectContributionRow { + DirectContributionRow { + parent0_component_base: 0, + parent1_component_base_or_sentinel: 4, + parent0_momentum_form_id: 0, + parent1_momentum_form_id_or_sentinel: 1, + destination_component_base, + exact_factor_id: 0, + selector_domain_id: 0, + flags: 0, + } + } + + #[test] + fn interaction_destination_bijection_accepts_nonidentity_and_rejects_nonsets() { + let color = [interaction_row(8), interaction_row(16), interaction_row(24)]; + let permuted = [interaction_row(24), interaction_row(8), interaction_row(16)]; + let color = interaction_destination_offsets(&color).unwrap(); + let permuted = interaction_destination_offsets(&permuted).unwrap(); + assert_eq!( + interaction_destination_bijection(&color, &permuted).unwrap(), + [1, 2, 0] + ); + + assert!( + interaction_destination_offsets(&[interaction_row(8), interaction_row(8),]).is_none() + ); + let different = interaction_destination_offsets(&[ + interaction_row(8), + interaction_row(16), + interaction_row(28), + ]) + .unwrap(); + assert!(interaction_destination_bijection(&color, &different).is_none()); + } + + #[test] + fn rejects_short_parent_spans_and_missing_momentum_forms() { + let row = DirectContributionRow { + parent0_component_base: 0, + parent1_component_base_or_sentinel: 4, + parent0_momentum_form_id: 0, + parent1_momentum_form_id_or_sentinel: 1, + destination_component_base: 8, + exact_factor_id: 0, + selector_domain_id: 0, + flags: 0, + }; + assert!(validate_fused_fanout_row(row, [9, 4], false, 8, 2).is_err()); + assert!(validate_fused_fanout_row(row, [4, 4], true, 8, 1).is_err()); + validate_fused_fanout_row(row, [4, 4], true, 8, 2).unwrap(); + } + + #[test] + fn interaction_execution_gate_is_full_unselected_single_point_only() { + let plan = crate::recurrence::direct_plan::tests::valid_plan(); + let groups = plan.row_groups(); + assert!(direct_interaction_execution_enabled::( + RecurrenceStrategy::TopologyReplay, + groups, + groups, + None, + 1, + )); + assert!(!direct_interaction_execution_enabled::( + RecurrenceStrategy::TopologyReplay, + groups, + groups, + None, + 1, + )); + assert!(!direct_interaction_execution_enabled::( + RecurrenceStrategy::TopologyReplay, + groups, + groups, + None, + 2, + )); + assert!(!direct_interaction_execution_enabled::( + RecurrenceStrategy::TopologyReplay, + groups, + groups, + Some(0), + 1, + )); + assert!(!direct_interaction_execution_enabled::( + RecurrenceStrategy::AllFlowUnion, + groups, + groups, + None, + 1, + )); + assert!(!direct_interaction_execution_enabled::( + RecurrenceStrategy::ContractedColorUnion, + groups, + &groups[1..], + None, + 1, + )); + } + + #[test] + fn large_schedule_interaction_rewrite_uses_only_strategy_and_row_count() { + assert!(!direct_large_schedule_interaction_rewrite_enabled( + RecurrenceStrategy::ContractedColorUnion, + DIRECT_LARGE_SCHEDULE_INTERACTION_MIN_ROWS - 1, + )); + assert!(direct_large_schedule_interaction_rewrite_enabled( + RecurrenceStrategy::ContractedColorUnion, + DIRECT_LARGE_SCHEDULE_INTERACTION_MIN_ROWS, + )); + assert!(!direct_large_schedule_interaction_rewrite_enabled( + RecurrenceStrategy::TopologyReplay, + DIRECT_LARGE_SCHEDULE_INTERACTION_MIN_ROWS, + )); + assert!(!direct_large_schedule_interaction_rewrite_enabled( + RecurrenceStrategy::AllFlowUnion, + usize::MAX, + )); + assert!(direct_terminal_interaction_rewrite_enabled( + RecurrenceStrategy::ContractedColorUnion, + )); + assert!(!direct_terminal_interaction_rewrite_enabled( + RecurrenceStrategy::TopologyReplay, + )); + assert!(!direct_terminal_interaction_rewrite_enabled( + RecurrenceStrategy::AllFlowUnion, + )); + } +} + +fn push_singleton_direct_step( + steps: &mut Vec, + group_step_start: usize, + row_start: u32, + row_count: u32, +) -> RusticolResult<()> { + if steps.len() > group_step_start + && let Some(DirectSingletonFanoutStep::Direct { + row_start: previous_start, + row_count: previous_count, + }) = steps.last_mut() + && previous_start + .checked_add(*previous_count) + .is_some_and(|end| end == row_start) + { + *previous_count = previous_count.checked_add(row_count).ok_or_else(|| { + RusticolError::integrity("singleton fanout direct-step count exceeds u32") + })?; + return Ok(()); + } + steps.push(DirectSingletonFanoutStep::Direct { + row_start, + row_count, + }); + Ok(()) +} + +fn flush_singleton_fanout_batch( + rows: &[DirectContributionRow], + pending_runs: &mut Vec<(u32, u32)>, + steps: &mut Vec, + runs: &mut Vec, + bundles: &mut Vec, +) -> RusticolResult<()> { + let Some(&(first_row_start, _)) = pending_runs.first() else { + return Ok(()); + }; + let run_start = u64::try_from(runs.len()) + .map_err(|_| RusticolError::integrity("singleton fanout run count exceeds u64"))?; + let mut logical_row_count = 0_u32; + let mut expected_start = first_row_start; + for &(row_start, row_count) in pending_runs.iter() { + if row_count < 2 || row_start != expected_start { + return Err(RusticolError::integrity( + "singleton fanout pending runs are not contiguous reusable classes", + )); + } + push_output_only_fanout_run(rows, u64::from(row_start), row_count, 0, runs, bundles)?; + logical_row_count = logical_row_count.checked_add(row_count).ok_or_else(|| { + RusticolError::integrity("singleton fanout batch row count exceeds u32") + })?; + expected_start = expected_start.checked_add(row_count).ok_or_else(|| { + RusticolError::integrity("singleton fanout pending-run coverage exceeds u32") + })?; + } + steps.push(DirectSingletonFanoutStep::Fanout { + row_start: first_row_start, + row_count: logical_row_count, + run_start, + run_count: u32::try_from(pending_runs.len()).map_err(|_| { + RusticolError::integrity("singleton fanout batch run count exceeds u32") + })?, + }); + pending_runs.clear(); + Ok(()) +} + +/// Append the shared run/bundle representation used by both persisted direct +/// plans and family-local singleton fanout. Output-only contribution kernels +/// leave the authenticated exact factor for this destination replay step. +fn push_output_only_fanout_run( + rows: &[DirectContributionRow], + row_start: u64, + row_count: u32, + scratch_component_base: u32, + runs: &mut Vec, + bundles: &mut Vec, +) -> RusticolResult<()> { + let start = usize::try_from(row_start) + .map_err(|_| RusticolError::integrity("direct fanout row start exceeds usize"))?; + let end = start + .checked_add(row_count as usize) + .ok_or_else(|| RusticolError::integrity("direct fanout row range overflows usize"))?; + let run_rows = rows + .get(start..end) + .ok_or_else(|| RusticolError::integrity("direct fanout row range is out of bounds"))?; + let bundle_start = u64::try_from(bundles.len()) + .map_err(|_| RusticolError::integrity("direct fanout bundle count exceeds u64"))?; + let mut offset = 0_usize; + while offset < run_rows.len() { + let factor_id = run_rows[offset].exact_factor_id; + let first = offset; + offset += 1; + while offset < run_rows.len() && run_rows[offset].exact_factor_id == factor_id { + offset += 1; + } + bundles.push(DirectContributionFanoutBundle { + row_start: row_start + .checked_add(u64::try_from(first).map_err(|_| { + RusticolError::integrity("direct fanout bundle offset exceeds u64") + })?) + .ok_or_else(|| RusticolError::integrity("direct fanout bundle start overflows"))?, + row_count: u32::try_from(offset - first) + .map_err(|_| RusticolError::integrity("direct fanout factor bundle exceeds u32"))?, + effective_factor_id_or_sentinel: factor_id, + }); + } + let bundle_count = u32::try_from( + u64::try_from(bundles.len()) + .map_err(|_| RusticolError::integrity("direct fanout bundle count exceeds u64"))? + .checked_sub(bundle_start) + .ok_or_else(|| RusticolError::integrity("direct fanout bundle range underflows"))?, + ) + .map_err(|_| RusticolError::integrity("direct fanout run bundle count exceeds u32"))?; + runs.push(DirectContributionFanoutRun { + row_start, + row_count, + scratch_component_base, + bundle_start, + bundle_count, + }); + Ok(()) +} + +fn contribution_fanout_inputs_equal( + left: DirectContributionRow, + right: DirectContributionRow, + exact_factor_is_kernel_input: bool, +) -> bool { + left.flags & DIRECT_CONTRIBUTION_FLAG_CERTIFIED_REUSE == 0 + && right.flags & DIRECT_CONTRIBUTION_FLAG_CERTIFIED_REUSE == 0 + && left.selector_domain_id == right.selector_domain_id + && left.parent0_component_base == right.parent0_component_base + && left.parent1_component_base_or_sentinel == right.parent1_component_base_or_sentinel + && left.parent0_momentum_form_id == right.parent0_momentum_form_id + && left.parent1_momentum_form_id_or_sentinel == right.parent1_momentum_form_id_or_sentinel + && (!exact_factor_is_kernel_input || left.exact_factor_id == right.exact_factor_id) +} + +fn push_direct_fanout_step( + steps: &mut Vec, + segment_step_start: usize, + row_start: u64, + row_count: u32, +) -> RusticolResult<()> { + if steps.len() > segment_step_start + && let Some(DirectContributionFanoutStep::Direct { + row_start: previous_start, + row_count: previous_count, + }) = steps.last_mut() + && previous_start + .checked_add(u64::from(*previous_count)) + .is_some_and(|end| end == row_start) + { + *previous_count = previous_count.checked_add(row_count).ok_or_else(|| { + RusticolError::integrity("direct fanout direct-step row count exceeds u32") + })?; + return Ok(()); + } + steps.push(DirectContributionFanoutStep::Direct { + row_start, + row_count, + }); + Ok(()) +} + +#[allow(clippy::too_many_arguments)] +fn flush_pending_fanout_batch( + plan: &DirectRecurrencePlan, + descriptor: &DirectRowGroupDescriptor, + metadata: DirectContributionExecutionMetadata, + scratch_component_base: u32, + unit_factor_id: u32, + pending_runs: &mut Vec<(usize, usize)>, + steps: &mut Vec, + representatives: &mut Vec, + runs: &mut Vec, + bundles: &mut Vec, + scratch_component_count: &mut u32, + needs_unit_factor: &mut bool, +) -> RusticolResult<()> { + if pending_runs.is_empty() { + return Ok(()); + } + let representative_start = u64::try_from(representatives.len()) + .map_err(|_| RusticolError::integrity("direct fanout representative count exceeds u64"))?; + let run_table_start = u64::try_from(runs.len()) + .map_err(|_| RusticolError::integrity("direct fanout run count exceeds u64"))?; + let first_row_start = descriptor + .row_start + .checked_add(pending_runs[0].0 as u64) + .ok_or_else(|| RusticolError::integrity("direct fanout row start overflows u64"))?; + let mut logical_row_count = 0_u32; + for (slot, &(relative_start, run_count)) in pending_runs.iter().enumerate() { + let row_index = usize::try_from(descriptor.row_start) + .ok() + .and_then(|start| start.checked_add(relative_start)) + .ok_or_else(|| RusticolError::integrity("direct fanout row index overflows usize"))?; + let mut representative = *plan.contributions().get(row_index).ok_or_else(|| { + RusticolError::integrity("direct fanout representative row is out of bounds") + })?; + let slot_base = u32::try_from(slot) + .ok() + .and_then(|slot| slot.checked_mul(metadata.destination_component_count)) + .and_then(|offset| scratch_component_base.checked_add(offset)) + .ok_or_else(|| { + RusticolError::integrity("direct fanout scratch component base exceeds u32") + })?; + representative.destination_component_base = slot_base; + representative.flags = DIRECT_CONTRIBUTION_FLAG_INITIALIZE_DESTINATION; + if !metadata.exact_factor_is_kernel_input { + representative.exact_factor_id = unit_factor_id; + *needs_unit_factor = true; + } + representatives.push(representative); + let run_row_start = descriptor + .row_start + .checked_add(relative_start as u64) + .ok_or_else(|| RusticolError::integrity("direct fanout run start overflows u64"))?; + let run_row_count = u32::try_from(run_count) + .map_err(|_| RusticolError::integrity("direct fanout run exceeds u32"))?; + if metadata.exact_factor_is_kernel_input { + let bundle_start = u64::try_from(bundles.len()) + .map_err(|_| RusticolError::integrity("direct fanout bundle count exceeds u64"))?; + bundles.push(DirectContributionFanoutBundle { + row_start: run_row_start, + row_count: run_row_count, + effective_factor_id_or_sentinel: DIRECT_NONE_U32, + }); + runs.push(DirectContributionFanoutRun { + row_start: run_row_start, + row_count: run_row_count, + scratch_component_base: slot_base, + bundle_start, + bundle_count: 1, + }); + } else { + push_output_only_fanout_run( + plan.contributions(), + run_row_start, + run_row_count, + slot_base, + runs, + bundles, + )?; + } + logical_row_count = logical_row_count + .checked_add(u32::try_from(run_count).map_err(|_| { + RusticolError::integrity("direct fanout logical run count exceeds u32") + })?) + .ok_or_else(|| RusticolError::integrity("direct fanout batch row count exceeds u32"))?; + } + let representative_count = u32::try_from(pending_runs.len()) + .map_err(|_| RusticolError::integrity("direct fanout batch exceeds u32"))?; + let required_scratch = representative_count + .checked_mul(metadata.destination_component_count) + .ok_or_else(|| RusticolError::integrity("direct fanout scratch size exceeds u32"))?; + *scratch_component_count = (*scratch_component_count).max(required_scratch); + steps.push(DirectContributionFanoutStep::Fanout { + row_start: first_row_start, + row_count: logical_row_count, + representative_start, + representative_count, + run_start: run_table_start, + run_count: representative_count, + destination_component_count: metadata.destination_component_count, + }); + pending_runs.clear(); + Ok(()) +} + +pub struct DirectWorkspace<'a> { + pub current_re: &'a mut [f64], + pub current_im: &'a mut [f64], + pub amplitude_re: &'a mut [f64], + pub amplitude_im: &'a mut [f64], + pub momenta: &'a [f64], + pub momentum_form_count: u32, + pub lorentz_component_count: u16, + pub parameters_re: &'a [f64], + pub parameters_im: &'a [f64], + pub factors_re: &'a [f64], + pub factors_im: &'a [f64], + pub point_stride: u32, +} + +impl DirectWorkspace<'_> { + fn validate(&self, point_count: u32) -> RusticolResult<()> { + if point_count == 0 || self.point_stride == 0 || point_count > self.point_stride { + return Err(RusticolError::invalid_argument( + "direct recurrence point count must be within the workspace stride", + )); + } + if self.current_re.len() != self.current_im.len() { + return Err(RusticolError::invalid_argument( + "direct recurrence current real and imaginary arenas differ in length", + )); + } + if self.amplitude_re.len() != self.amplitude_im.len() { + return Err(RusticolError::invalid_argument( + "direct recurrence amplitude real and imaginary arenas differ in length", + )); + } + let point_stride = self.point_stride as usize; + if !self.current_re.len().is_multiple_of(point_stride) + || !self.amplitude_re.len().is_multiple_of(point_stride) + { + return Err(RusticolError::invalid_argument( + "direct recurrence arena does not contain whole point-stride planes", + )); + } + if self.parameters_re.len() != self.parameters_im.len() + || self.factors_re.len() != self.factors_im.len() + { + return Err(RusticolError::invalid_argument( + "direct recurrence split-complex catalogs differ in length", + )); + } + let expected_momenta = usize::try_from(self.momentum_form_count) + .ok() + .and_then(|forms| forms.checked_mul(usize::from(self.lorentz_component_count))) + .and_then(|planes| planes.checked_mul(self.point_stride as usize)) + .ok_or_else(|| { + RusticolError::invalid_argument("direct recurrence momentum arena overflows") + })?; + if self.momenta.len() != expected_momenta { + return Err(RusticolError::invalid_argument( + "direct recurrence momentum arena has an inconsistent length", + )); + } + Ok(()) + } + + fn raw_views( + &mut self, + ) -> RusticolResult<( + DirectArenaView, + DirectMomentumView, + DirectParameterView, + DirectFactorView, + )> { + let current_scalar_len = u64::try_from(self.current_re.len()).map_err(|_| { + RusticolError::invalid_argument("direct recurrence current arena exceeds u64") + })?; + let amplitude_scalar_len = u64::try_from(self.amplitude_re.len()).map_err(|_| { + RusticolError::invalid_argument("direct recurrence amplitude arena exceeds u64") + })?; + let momentum_scalar_len = u64::try_from(self.momenta.len()).map_err(|_| { + RusticolError::invalid_argument("direct recurrence momentum arena exceeds u64") + })?; + let parameter_count = u32::try_from(self.parameters_re.len()).map_err(|_| { + RusticolError::invalid_argument("direct recurrence parameter catalog exceeds u32") + })?; + let factor_count = u32::try_from(self.factors_re.len()).map_err(|_| { + RusticolError::invalid_argument("direct recurrence factor catalog exceeds u32") + })?; + Ok(( + DirectArenaView { + current_re: self.current_re.as_mut_ptr(), + current_im: self.current_im.as_mut_ptr(), + current_scalar_len, + amplitude_re: self.amplitude_re.as_mut_ptr(), + amplitude_im: self.amplitude_im.as_mut_ptr(), + amplitude_scalar_len, + point_stride: self.point_stride, + }, + DirectMomentumView { + values: self.momenta.as_ptr(), + scalar_len: momentum_scalar_len, + form_count: self.momentum_form_count, + lorentz_component_count: self.lorentz_component_count, + point_stride: self.point_stride, + }, + DirectParameterView { + values_re: self.parameters_re.as_ptr(), + values_im: self.parameters_im.as_ptr(), + value_count: parameter_count, + }, + DirectFactorView { + values_re: self.factors_re.as_ptr(), + values_im: self.factors_im.as_ptr(), + value_count: factor_count, + }, + )) + } + + fn clear_current_stage( + &mut self, + plan: &DirectRecurrencePlan, + stage: u16, + point_count: u32, + selected_sector_id: Option, + ) -> RusticolResult<()> { + let current_plane_count = u32::try_from(self.current_re.len() / self.point_stride as usize) + .map_err(|_| { + RusticolError::integrity( + "direct recurrence current plane count exceeds u32 during stage clear", + ) + })?; + for current in plan.currents().iter().filter(|current| { + current.node_kind == DirectNodeKind::Current && current.stage == stage + }) { + if let Some(sector_id) = selected_sector_id + && !plan.selector_domain_contains(current.selector_domain_id, sector_id)? + { + continue; + } + clear_split_active_range( + self.current_re, + self.current_im, + current_plane_count, + self.point_stride, + point_count, + current.component_base, + u32::from(current.component_count), + "recurrence current arena", + )?; + } + Ok(()) + } +} + +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub struct DirectExecutionCounters { + pub source_calls: u64, + pub source_rows: u64, + pub contribution_calls: u64, + pub contribution_rows: u64, + pub finalization_calls: u64, + pub finalization_rows: u64, + pub closure_calls: u64, + pub closure_rows: u64, + pub packed_input_bytes: u64, + pub packed_output_bytes: u64, + pub scatter_bytes: u64, +} + +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub struct DirectExecutionRoleTimings { + pub source: Duration, + pub contribution: Duration, + pub finalization: Duration, + pub closure: Duration, +} + +pub fn execute_direct_plan( + plan: &DirectRecurrencePlan, + executors: &DirectExecutorCatalog, + workspace: &mut DirectWorkspace<'_>, + point_count: u32, + counters: &mut DirectExecutionCounters, +) -> RusticolResult<()> { + let mut unused_timings = DirectExecutionRoleTimings::default(); + execute_direct_plan_impl::( + plan, + plan.row_groups(), + None, + executors, + None, + workspace, + point_count, + counters, + &mut unused_timings, + None, + ) +} + +pub fn execute_direct_plan_profiled( + plan: &DirectRecurrencePlan, + executors: &DirectExecutorCatalog, + workspace: &mut DirectWorkspace<'_>, + point_count: u32, + counters: &mut DirectExecutionCounters, + timings: &mut DirectExecutionRoleTimings, +) -> RusticolResult<()> { + execute_direct_plan_impl::( + plan, + plan.row_groups(), + None, + executors, + None, + workspace, + point_count, + counters, + timings, + None, + ) +} + +#[allow(dead_code)] +pub(crate) fn execute_direct_plan_profiled_with_traffic( + plan: &DirectRecurrencePlan, + executors: &DirectExecutorCatalog, + workspace: &mut DirectWorkspace<'_>, + point_count: u32, + counters: &mut DirectExecutionCounters, + timings: &mut DirectExecutionRoleTimings, + traffic: &mut DirectArenaTrafficCounters, +) -> RusticolResult<()> { + execute_direct_plan_impl::( + plan, + plan.row_groups(), + None, + executors, + None, + workspace, + point_count, + counters, + timings, + Some(traffic), + ) +} + +#[allow(clippy::too_many_arguments)] +pub(crate) fn execute_direct_plan_profiled_with_fanout_and_traffic( + plan: &DirectRecurrencePlan, + executors: &DirectExecutorCatalog, + fanout: &DirectContributionFanoutProgram, + workspace: &mut DirectWorkspace<'_>, + point_count: u32, + counters: &mut DirectExecutionCounters, + timings: &mut DirectExecutionRoleTimings, + traffic: &mut DirectArenaTrafficCounters, +) -> RusticolResult<()> { + execute_direct_plan_impl::( + plan, + plan.row_groups(), + None, + executors, + Some(fanout), + workspace, + point_count, + counters, + timings, + Some(traffic), + ) +} + +/// Execute the authenticated schedule without touching profiling counters. +/// +/// This is a distinct const-generic specialization so release builds remove +/// every role-counter branch from the ordinary native evaluation path. +pub fn execute_direct_plan_unprofiled( + plan: &DirectRecurrencePlan, + executors: &DirectExecutorCatalog, + workspace: &mut DirectWorkspace<'_>, + point_count: u32, +) -> RusticolResult<()> { + let mut unused = DirectExecutionCounters::default(); + let mut unused_timings = DirectExecutionRoleTimings::default(); + execute_direct_plan_impl::( + plan, + plan.row_groups(), + None, + executors, + None, + workspace, + point_count, + &mut unused, + &mut unused_timings, + None, + ) +} + +pub(crate) fn execute_direct_plan_unprofiled_with_fanout( + plan: &DirectRecurrencePlan, + executors: &DirectExecutorCatalog, + fanout: &DirectContributionFanoutProgram, + workspace: &mut DirectWorkspace<'_>, + point_count: u32, +) -> RusticolResult<()> { + if point_count == 1 + && plan.strategy() != RecurrenceStrategy::AllFlowUnion + && !direct_current_observation_active() + { + return fanout.execute_full_singleton_unprofiled(plan, executors, workspace); + } + let mut unused = DirectExecutionCounters::default(); + let mut unused_timings = DirectExecutionRoleTimings::default(); + execute_direct_plan_impl::( + plan, + plan.row_groups(), + None, + executors, + Some(fanout), + workspace, + point_count, + &mut unused, + &mut unused_timings, + None, + ) +} + +#[allow(clippy::too_many_arguments)] +#[allow(dead_code)] +pub(crate) fn execute_direct_plan_selected_profiled_with_traffic( + plan: &DirectRecurrencePlan, + row_groups: &[DirectRowGroupDescriptor], + selected_sector_id: u32, + executors: &DirectExecutorCatalog, + workspace: &mut DirectWorkspace<'_>, + point_count: u32, + counters: &mut DirectExecutionCounters, + timings: &mut DirectExecutionRoleTimings, + traffic: &mut DirectArenaTrafficCounters, +) -> RusticolResult<()> { + execute_direct_plan_impl::( + plan, + row_groups, + Some(selected_sector_id), + executors, + None, + workspace, + point_count, + counters, + timings, + Some(traffic), + ) +} + +#[allow(clippy::too_many_arguments)] +pub(crate) fn execute_direct_plan_selected_profiled_with_fanout_and_traffic( + plan: &DirectRecurrencePlan, + row_groups: &[DirectRowGroupDescriptor], + selected_sector_id: u32, + executors: &DirectExecutorCatalog, + fanout: &DirectContributionFanoutProgram, + workspace: &mut DirectWorkspace<'_>, + point_count: u32, + counters: &mut DirectExecutionCounters, + timings: &mut DirectExecutionRoleTimings, + traffic: &mut DirectArenaTrafficCounters, +) -> RusticolResult<()> { + execute_direct_plan_impl::( + plan, + row_groups, + Some(selected_sector_id), + executors, + Some(fanout), + workspace, + point_count, + counters, + timings, + Some(traffic), + ) +} + +#[allow(dead_code)] +pub(crate) fn execute_direct_plan_selected_unprofiled( + plan: &DirectRecurrencePlan, + row_groups: &[DirectRowGroupDescriptor], + selected_sector_id: u32, + executors: &DirectExecutorCatalog, + workspace: &mut DirectWorkspace<'_>, + point_count: u32, +) -> RusticolResult<()> { + let mut unused = DirectExecutionCounters::default(); + let mut unused_timings = DirectExecutionRoleTimings::default(); + execute_direct_plan_impl::( + plan, + row_groups, + Some(selected_sector_id), + executors, + None, + workspace, + point_count, + &mut unused, + &mut unused_timings, + None, + ) +} + +pub(crate) fn execute_direct_plan_selected_unprofiled_with_fanout( + plan: &DirectRecurrencePlan, + row_groups: &[DirectRowGroupDescriptor], + selected_sector_id: u32, + executors: &DirectExecutorCatalog, + fanout: &DirectContributionFanoutProgram, + workspace: &mut DirectWorkspace<'_>, + point_count: u32, +) -> RusticolResult<()> { + let mut unused = DirectExecutionCounters::default(); + let mut unused_timings = DirectExecutionRoleTimings::default(); + execute_direct_plan_impl::( + plan, + row_groups, + Some(selected_sector_id), + executors, + Some(fanout), + workspace, + point_count, + &mut unused, + &mut unused_timings, + None, + ) +} + +#[allow(clippy::too_many_arguments)] +fn execute_direct_plan_impl( + plan: &DirectRecurrencePlan, + row_groups: &[DirectRowGroupDescriptor], + selected_sector_id: Option, + executors: &DirectExecutorCatalog, + fanout: Option<&DirectContributionFanoutProgram>, + workspace: &mut DirectWorkspace<'_>, + point_count: u32, + counters: &mut DirectExecutionCounters, + timings: &mut DirectExecutionRoleTimings, + traffic: Option<&mut DirectArenaTrafficCounters>, +) -> RusticolResult<()> { + execute_direct_plan_with_clears_impl::( + plan, + row_groups, + selected_sector_id, + executors, + fanout, + workspace, + point_count, + counters, + timings, + traffic, + None, + ) +} + +#[derive(Clone, Copy)] +pub(crate) struct DirectCurrentClearRange { + pub(crate) component_base: u32, + pub(crate) component_count: u32, +} + +pub(crate) struct DirectStageClearRanges { + pub(crate) stage: u16, + pub(crate) ranges: Box<[DirectCurrentClearRange]>, +} + +/// Restricted destination closures are prepared once. Their row runs may cut +/// through a full-plan fanout step, so execute the authenticated raw rows and +/// clear only dependency-closed stage spans whose first retained contribution +/// does not initialize them, with no current-table scan. +#[allow(clippy::too_many_arguments)] +pub(crate) fn execute_direct_plan_destination_selected( + plan: &DirectRecurrencePlan, + row_groups: &[DirectRowGroupDescriptor], + selected_sector_id: u32, + current_clear_stages: &[DirectStageClearRanges], + executors: &DirectExecutorCatalog, + workspace: &mut DirectWorkspace<'_>, + point_count: u32, + counters: &mut DirectExecutionCounters, + timings: &mut DirectExecutionRoleTimings, + traffic: &mut DirectArenaTrafficCounters, +) -> RusticolResult<()> { + execute_direct_plan_with_clears_impl::( + plan, + row_groups, + Some(selected_sector_id), + executors, + None, + workspace, + point_count, + counters, + timings, + PROFILE.then_some(traffic), + Some(current_clear_stages), + ) +} + +#[allow(clippy::too_many_arguments)] +fn execute_direct_plan_with_clears_impl< + const PROFILE: bool, + const ALLOW_INTERACTION_REWRITE: bool, + const PREPARED_CLEARS: bool, +>( + plan: &DirectRecurrencePlan, + row_groups: &[DirectRowGroupDescriptor], + selected_sector_id: Option, + executors: &DirectExecutorCatalog, + fanout: Option<&DirectContributionFanoutProgram>, + workspace: &mut DirectWorkspace<'_>, + point_count: u32, + counters: &mut DirectExecutionCounters, + timings: &mut DirectExecutionRoleTimings, + mut traffic: Option<&mut DirectArenaTrafficCounters>, + current_clear_stages: Option<&[DirectStageClearRanges]>, +) -> RusticolResult<()> { + clear_direct_executor_error_detail(); + workspace.validate(point_count)?; + if executors.plan_layout_digest != plan.runtime_layout_digest() { + return Err(RusticolError::integrity( + "direct recurrence executor catalog belongs to a different plan", + )); + } + if fanout.is_some_and(|fanout| fanout.plan_layout_digest != plan.runtime_layout_digest()) { + return Err(RusticolError::integrity( + "direct contribution fanout program belongs to a different plan", + )); + } + let observation_active = direct_current_observation_active(); + let interaction_enabled = direct_interaction_execution_enabled::( + plan.strategy(), + plan.row_groups(), + row_groups, + selected_sector_id, + point_count, + ) && !observation_active; + // Observations promise materialized semantic currents. Bypass both native + // interaction rewrites and generic fanout scratch/replay while a capture + // is active so every authenticated row writes its ordinary destination. + let fanout = if observation_active { None } else { fanout }; + // The existing dependency-closure preparation omits currents whose first + // retained contribution already overwrites their destination. Most fixed- + // source selections therefore need no stage clear at all. Unrestricted + // selection and all-flow-union retain their existing clearing semantics. + let requires_contribution_stage_clear = (selected_sector_id.is_some() + || plan.strategy() == RecurrenceStrategy::AllFlowUnion) + && (!PREPARED_CLEARS || current_clear_stages.is_none_or(|stages| !stages.is_empty())); + let mut initialized_contribution_stage = None; + for (group_index, descriptor) in row_groups.iter().enumerate() { + if descriptor.role == DirectExecutorRole::Source + && descriptor.direct_executor_id == DIRECT_NONE_U32 + && plan.strategy() == RecurrenceStrategy::AllFlowUnion + { + // The selected union source variants were filled once by + // `DirectRecurrenceExecutionRuntime` before entering this static + // contribution/finalization/closure schedule. + continue; + } + if interaction_enabled && let Some(fanout) = fanout { + match fanout.interactions.action(group_index) { + DirectInteractionGroupAction::Normal => {} + DirectInteractionGroupAction::Consumed => continue, + DirectInteractionGroupAction::Execute(stage_index) => { + let (logical_groups, closure_group_index, logical_row_count) = + fanout.interactions.logical_groups(stage_index)?; + let started = PROFILE.then(Instant::now); + let (arena, momenta, parameters, factors) = workspace.raw_views()?; + let closure_elapsed = fanout.interactions.execute::( + stage_index, + arena, + momenta, + parameters, + factors, + )?; + if PROFILE { + counters.contribution_calls += logical_groups.len() as u64; + counters.contribution_rows += logical_row_count; + if let Some(closure_group_index) = closure_group_index { + let closure = &row_groups[closure_group_index as usize]; + counters.closure_calls += 1; + counters.closure_rows += u64::from(closure.row_count); + } + let elapsed = started + .expect("profiled interaction stage has a start time") + .elapsed(); + timings.contribution += elapsed.saturating_sub(closure_elapsed); + timings.closure += closure_elapsed; + if let Some(traffic) = traffic.as_deref_mut() { + for &logical_group in logical_groups { + let logical_group = logical_group as usize; + let logical = &row_groups[logical_group]; + traffic.record_call(logical.row_count, point_count); + } + if let Some(closure_group_index) = closure_group_index { + let closure = &row_groups[closure_group_index as usize]; + traffic.record_call(closure.row_count, point_count); + } + } + } + #[cfg(any(test, feature = "on-the-fly-test-support"))] + for &logical_group in logical_groups { + let logical_group = logical_group as usize; + let logical = &row_groups[logical_group]; + let start = usize::try_from(logical.row_start).map_err(|_| { + RusticolError::integrity( + "direct interaction observation row start exceeds usize", + ) + })?; + let end = + start + .checked_add(logical.row_count as usize) + .ok_or_else(|| { + RusticolError::integrity( + "direct interaction observation row range overflows usize", + ) + })?; + observe_direct_current_rows( + plan, + logical, + start, + end, + selected_sector_id, + workspace, + point_count, + )?; + } + continue; + } + } + } + // A full topology/contracted schedule executes every authenticated + // initializer, so its writes replace the stage clear. All-flow-union + // sources are prepared outside this static row schedule and may leave + // additive destinations relying on the clear even without a selected + // sector. Selected execution likewise visits only a logical subset. + if requires_contribution_stage_clear + && descriptor.role == DirectExecutorRole::Contribution + && initialized_contribution_stage != Some(descriptor.stage) + { + if PREPARED_CLEARS { + let stages = current_clear_stages.ok_or_else(|| { + RusticolError::integrity("selected recurrence has no prepared stage clears") + })?; + if let Ok(stage_index) = + stages.binary_search_by_key(&descriptor.stage, |entry| entry.stage) + { + let current_plane_count = + u32::try_from(workspace.current_re.len() / workspace.point_stride as usize) + .map_err(|_| { + RusticolError::integrity( + "selected recurrence current plane count exceeds u32", + ) + })?; + for range in stages[stage_index].ranges.iter() { + clear_split_active_range( + workspace.current_re, + workspace.current_im, + current_plane_count, + workspace.point_stride, + point_count, + range.component_base, + range.component_count, + "selected recurrence current arena", + )?; + } + } + } else { + workspace.clear_current_stage( + plan, + descriptor.stage, + point_count, + selected_sector_id, + )?; + } + initialized_contribution_stage = Some(descriptor.stage); + } + let start = usize::try_from(descriptor.row_start).map_err(|_| { + RusticolError::integrity("direct recurrence row-group start exceeds usize") + })?; + let count = usize::try_from(descriptor.row_count).map_err(|_| { + RusticolError::integrity("direct recurrence row-group count exceeds usize") + })?; + let end = start.checked_add(count).ok_or_else(|| { + RusticolError::integrity("direct recurrence row-group range overflows usize") + })?; + if descriptor.role == DirectExecutorRole::Contribution + && descriptor.direct_executor_id == DIRECT_NONE_U32 + { + let rows = plan.contributions().get(start..end).ok_or_else(|| { + RusticolError::integrity("certified-reuse contribution row group is out of bounds") + })?; + let started = PROFILE.then(Instant::now); + execute_certified_reuse_rows(rows, workspace, point_count)?; + #[cfg(any(test, feature = "on-the-fly-test-support"))] + observe_direct_current_rows( + plan, + descriptor, + start, + end, + selected_sector_id, + workspace, + point_count, + )?; + if PROFILE { + counters.contribution_calls += 1; + counters.contribution_rows += u64::from(descriptor.row_count); + timings.contribution += started + .expect("profiled certified-reuse group has a start time") + .elapsed(); + if let Some(traffic) = traffic.as_deref_mut() { + traffic.record_call(descriptor.row_count, point_count); + } + } + continue; + } + if descriptor.role == DirectExecutorRole::Contribution + && let Some(fanout) = fanout + { + let handle = executors.require( + descriptor.direct_executor_id, + DirectExecutorRole::Contribution, + )?; + let DirectExecutorHandle::Contribution { call, context } = handle else { + return Err(RusticolError::integrity( + "direct fanout executor does not have the contribution role", + )); + }; + if PROFILE && let Some(traffic) = traffic.as_deref_mut() { + traffic.record_call(descriptor.row_count, point_count); + } + let started = PROFILE.then(Instant::now); + let scatter_bytes = execute_contribution_group_with_fanout( + plan, + descriptor, + fanout, + call, + context, + executors.contribution_fanout(descriptor.direct_executor_id), + workspace, + point_count, + )?; + if PROFILE { + counters.contribution_calls += 1; + counters.contribution_rows += u64::from(descriptor.row_count); + counters.scatter_bytes = counters.scatter_bytes.saturating_add(scatter_bytes); + timings.contribution += started + .expect("profiled direct fanout group has a start time") + .elapsed(); + } + #[cfg(any(test, feature = "on-the-fly-test-support"))] + observe_direct_current_rows( + plan, + descriptor, + start, + end, + selected_sector_id, + workspace, + point_count, + )?; + continue; + } + let (arena, momenta, parameters, factors) = workspace.raw_views()?; + let handle = executors.require(descriptor.direct_executor_id, descriptor.role)?; + if PROFILE && let Some(traffic) = traffic.as_deref_mut() { + traffic.record_call(descriptor.row_count, point_count); + } + let started = PROFILE.then(Instant::now); + let status = unsafe { + match handle { + DirectExecutorHandle::Source { call, context } => { + let rows = plan.sources().get(start..end).ok_or_else(|| { + RusticolError::integrity( + "direct recurrence source row group is out of bounds", + ) + })?; + if PROFILE { + counters.source_calls += 1; + counters.source_rows += u64::from(descriptor.row_count); + } + call( + context, + arena, + momenta, + parameters, + factors, + rows.as_ptr(), + descriptor.row_count, + point_count, + ) + } + DirectExecutorHandle::Contribution { call, context } => { + let rows = plan.contributions().get(start..end).ok_or_else(|| { + RusticolError::integrity( + "direct recurrence contribution row group is out of bounds", + ) + })?; + if PROFILE { + counters.contribution_calls += 1; + counters.contribution_rows += u64::from(descriptor.row_count); + } + call( + context, + arena, + momenta, + parameters, + factors, + rows.as_ptr(), + descriptor.row_count, + point_count, + ) + } + DirectExecutorHandle::Finalization { call, context } => { + let rows = plan.finalizations().get(start..end).ok_or_else(|| { + RusticolError::integrity( + "direct recurrence finalization row group is out of bounds", + ) + })?; + if PROFILE { + counters.finalization_calls += 1; + counters.finalization_rows += u64::from(descriptor.row_count); + } + call( + context, + arena, + momenta, + parameters, + factors, + rows.as_ptr(), + descriptor.row_count, + point_count, + ) + } + DirectExecutorHandle::Closure { call, context } => { + let rows = plan.closures().get(start..end).ok_or_else(|| { + RusticolError::integrity( + "direct recurrence closure row group is out of bounds", + ) + })?; + if PROFILE { + counters.closure_calls += 1; + counters.closure_rows += u64::from(descriptor.row_count); + } + call( + context, + arena, + momenta, + parameters, + factors, + rows.as_ptr(), + descriptor.row_count, + point_count, + ) + } + } + }; + if PROFILE { + let elapsed = started + .expect("profiled direct row group has a start time") + .elapsed(); + match descriptor.role { + DirectExecutorRole::Source => timings.source += elapsed, + DirectExecutorRole::Contribution => timings.contribution += elapsed, + DirectExecutorRole::Finalization => timings.finalization += elapsed, + DirectExecutorRole::Closure => timings.closure += elapsed, + } + } + check_status(descriptor.role, descriptor.direct_executor_id, status)?; + #[cfg(any(test, feature = "on-the-fly-test-support"))] + observe_direct_current_rows( + plan, + descriptor, + start, + end, + selected_sector_id, + workspace, + point_count, + )?; + } + Ok(()) +} + +#[inline(always)] +fn direct_interaction_execution_enabled( + strategy: RecurrenceStrategy, + authoritative_groups: &[DirectRowGroupDescriptor], + requested_groups: &[DirectRowGroupDescriptor], + selected_sector_id: Option, + point_count: u32, +) -> bool { + ALLOW_INTERACTION_REWRITE + && point_count == 1 + && selected_sector_id.is_none() + && strategy != RecurrenceStrategy::AllFlowUnion + && requested_groups.len() == authoritative_groups.len() + && std::ptr::eq(requested_groups.as_ptr(), authoritative_groups.as_ptr()) +} + +fn execute_direct_interaction_stage_views( + stage: &DirectInteractionStage, + arena: DirectArenaView, + momenta: DirectMomentumView, + parameters: DirectParameterView, + factors: DirectFactorView, +) -> RusticolResult { + let terminal = stage + .terminal + .as_ref() + .map(|terminal| DirectInteractionTerminalBatch { + partner_component_base: terminal.partner_component_base, + component_factor_start: terminal.component_factor_start, + amplitude_destination_count: terminal.amplitude_destination_count, + closures: &terminal.closures, + }); + unsafe { + (stage.call)( + stage.contexts, + arena, + momenta, + parameters, + factors, + DirectInteractionBundleBatch { + anchors: &stage.anchors, + outputs: &stage.outputs, + tensor_outputs: &stage.tensor_outputs, + terminal, + requirements: stage.requirements, + atv_before_color: stage.atv_before_color, + }, + )?; + } + let closure_started = (PROFILE && terminal.is_some()).then(Instant::now); + if let Some(terminal) = terminal { + unsafe { + crate::engine::execute_interaction_terminal_closures(arena, factors, terminal)?; + } + } + Ok(closure_started.map_or(Duration::ZERO, |started| started.elapsed())) +} + +#[allow(clippy::too_many_arguments)] +fn execute_contribution_group_with_fanout( + plan: &DirectRecurrencePlan, + descriptor: &DirectRowGroupDescriptor, + fanout: &DirectContributionFanoutProgram, + call: DirectContributionExecutor, + context: *const c_void, + fused: Option, + workspace: &mut DirectWorkspace<'_>, + point_count: u32, +) -> RusticolResult { + let current_plane_count = u32::try_from( + workspace.current_re.len() / workspace.point_stride as usize, + ) + .map_err(|_| RusticolError::integrity("direct fanout current plane count exceeds u32"))?; + let scratch_end = fanout + .scratch_component_base + .checked_add(fanout.scratch_component_count) + .ok_or_else(|| RusticolError::integrity("direct fanout scratch range overflows u32"))?; + if fanout.scratch_component_base != plan.current_arena_components() + || scratch_end > current_plane_count + { + return Err(RusticolError::integrity( + "direct fanout scratch range is outside its private current arena", + )); + } + if fanout.needs_unit_factor { + let factor = fanout.unit_factor_id as usize; + if workspace + .factors_re + .get(factor) + .map(|value| value.to_bits()) + != Some(1.0_f64.to_bits()) + || workspace + .factors_im + .get(factor) + .map(|value| value.to_bits()) + != Some(0.0_f64.to_bits()) + { + return Err(RusticolError::integrity( + "direct fanout private exact factor is not complex unity", + )); + } + } + let steps = fanout.group_steps(descriptor)?; + let (arena, momenta, parameters, factors) = workspace.raw_views()?; + let mut scatter_bytes = 0_u64; + for step in steps { + match *step { + DirectContributionFanoutStep::Direct { + row_start, + row_count, + } => { + let start = usize::try_from(row_start).map_err(|_| { + RusticolError::integrity("direct fanout direct-row start exceeds usize") + })?; + let end = start.checked_add(row_count as usize).ok_or_else(|| { + RusticolError::integrity("direct fanout direct-row range overflows usize") + })?; + let rows = plan.contributions().get(start..end).ok_or_else(|| { + RusticolError::integrity("direct fanout direct-row range is out of bounds") + })?; + let status = unsafe { + call( + context, + arena, + momenta, + parameters, + factors, + rows.as_ptr(), + row_count, + point_count, + ) + }; + check_status( + DirectExecutorRole::Contribution, + descriptor.direct_executor_id, + status, + )?; + } + DirectContributionFanoutStep::Fanout { + row_count, + representative_start, + representative_count, + run_start, + run_count, + destination_component_count, + .. + } => { + if point_count == 1 + && let Some(fused) = fused + { + let start = usize::try_from(run_start).map_err(|_| { + RusticolError::integrity("direct fanout run start exceeds usize") + })?; + let end = start.checked_add(run_count as usize).ok_or_else(|| { + RusticolError::integrity("direct fanout run range overflows usize") + })?; + let runs = fanout.runs.get(start..end).ok_or_else(|| { + RusticolError::integrity("direct fanout run range is out of bounds") + })?; + debug_assert_eq!( + fused.destination_component_count, + destination_component_count + ); + unsafe { + (fused.call)( + fused.context, + arena, + momenta, + parameters, + factors, + DirectContributionFanoutBatch { + rows: plan.contributions(), + runs, + bundles: &fanout.bundles, + }, + ) + }?; + continue; + } + let representative_start = usize::try_from(representative_start).map_err(|_| { + RusticolError::integrity("direct fanout representative start exceeds usize") + })?; + let representative_end = representative_start + .checked_add(representative_count as usize) + .ok_or_else(|| { + RusticolError::integrity( + "direct fanout representative range overflows usize", + ) + })?; + let representatives = fanout + .representatives + .get(representative_start..representative_end) + .ok_or_else(|| { + RusticolError::integrity( + "direct fanout representative range is out of bounds", + ) + })?; + let status = unsafe { + call( + context, + arena, + momenta, + parameters, + factors, + representatives.as_ptr(), + representative_count, + point_count, + ) + }; + check_status( + DirectExecutorRole::Contribution, + descriptor.direct_executor_id, + status, + )?; + let run_start = usize::try_from(run_start).map_err(|_| { + RusticolError::integrity("direct fanout run start exceeds usize") + })?; + let run_end = run_start.checked_add(run_count as usize).ok_or_else(|| { + RusticolError::integrity("direct fanout run range overflows usize") + })?; + let batch_runs = fanout.runs.get(run_start..run_end).ok_or_else(|| { + RusticolError::integrity("direct fanout run range is out of bounds") + })?; + debug_assert_eq!(run_count, representative_count); + debug_assert_eq!( + batch_runs + .iter() + .map(|run| u64::from(run.row_count)) + .sum::(), + u64::from(row_count) + ); + for run in batch_runs { + scatter_contribution_fanout_run( + plan, + fanout, + *run, + destination_component_count, + workspace, + point_count, + )?; + } + scatter_bytes = scatter_bytes.saturating_add( + u64::from(row_count) + .saturating_mul(u64::from(destination_component_count)) + .saturating_mul(u64::from(point_count)) + .saturating_mul(4 * size_of::() as u64), + ); + } + } + } + Ok(scatter_bytes) +} + +#[derive(Clone, Copy)] +pub(crate) enum DirectContributionFanoutScale { + Unity, + NegativeUnity, + Real(f64), + Imaginary(f64), + Complex(f64, f64), +} + +impl DirectContributionFanoutScale { + #[inline(always)] + pub(crate) fn new(real: f64, imaginary: f64) -> Self { + if imaginary == 0.0 { + if real == 1.0 { + Self::Unity + } else if real == -1.0 { + Self::NegativeUnity + } else { + Self::Real(real) + } + } else if real == 0.0 { + Self::Imaginary(imaginary) + } else { + Self::Complex(real, imaginary) + } + } + + #[inline(always)] + pub(crate) fn apply(self, real: f64, imaginary: f64) -> (f64, f64) { + match self { + Self::Unity => (real, imaginary), + Self::NegativeUnity => (-real, -imaginary), + Self::Real(scale) => (real * scale, imaginary * scale), + Self::Imaginary(scale) => (-imaginary * scale, real * scale), + Self::Complex(scale_re, scale_im) => ( + real * scale_re - imaginary * scale_im, + real * scale_im + imaginary * scale_re, + ), + } + } +} + +fn scatter_contribution_fanout_run( + plan: &DirectRecurrencePlan, + fanout: &DirectContributionFanoutProgram, + run: DirectContributionFanoutRun, + destination_component_count: u32, + workspace: &mut DirectWorkspace<'_>, + point_count: u32, +) -> RusticolResult<()> { + let stride = workspace.point_stride as usize; + let active_points = point_count as usize; + let source_end = run + .scratch_component_base + .checked_add(destination_component_count) + .ok_or_else(|| RusticolError::integrity("direct fanout source range overflows u32"))?; + let current_plane_count = u32::try_from(workspace.current_re.len() / stride) + .map_err(|_| RusticolError::integrity("direct fanout current plane count exceeds u32"))?; + if run.scratch_component_base < fanout.scratch_component_base + || source_end > current_plane_count + { + return Err(RusticolError::integrity( + "direct fanout source range is outside its private current arena", + )); + } + let bundle_start = usize::try_from(run.bundle_start) + .map_err(|_| RusticolError::integrity("direct fanout bundle start exceeds usize"))?; + let bundle_end = bundle_start + .checked_add(run.bundle_count as usize) + .ok_or_else(|| RusticolError::integrity("direct fanout bundle range overflows usize"))?; + let bundles = fanout + .bundles + .get(bundle_start..bundle_end) + .ok_or_else(|| RusticolError::integrity("direct fanout bundle range is out of bounds"))?; + debug_assert_eq!( + bundles + .iter() + .map(|bundle| u64::from(bundle.row_count)) + .sum::(), + u64::from(run.row_count) + ); + + let current_re = workspace.current_re.as_mut_ptr(); + let current_im = workspace.current_im.as_mut_ptr(); + for bundle in bundles { + let start = usize::try_from(bundle.row_start).map_err(|_| { + RusticolError::integrity("direct fanout bundle row start exceeds usize") + })?; + let end = start + .checked_add(bundle.row_count as usize) + .ok_or_else(|| { + RusticolError::integrity("direct fanout bundle row range overflows usize") + })?; + let rows = plan.contributions().get(start..end).ok_or_else(|| { + RusticolError::integrity("direct fanout bundle row range is out of bounds") + })?; + let scale = if bundle.effective_factor_id_or_sentinel == DIRECT_NONE_U32 { + DirectContributionFanoutScale::Unity + } else { + let factor = bundle.effective_factor_id_or_sentinel as usize; + DirectContributionFanoutScale::new( + *workspace.factors_re.get(factor).ok_or_else(|| { + RusticolError::integrity("direct fanout scatter factor is out of bounds") + })?, + *workspace.factors_im.get(factor).ok_or_else(|| { + RusticolError::integrity("direct fanout scatter factor is out of bounds") + })?, + ) + }; + for component in 0..destination_component_count { + let source_start = (run.scratch_component_base as usize + component as usize) * stride; + for point in 0..active_points { + let source = source_start + point; + // SAFETY: the source run is checked against the runtime arena + // above. The destination rows and their full component spans + // were authenticated by `DirectRecurrencePlan` and matched to + // this executor's destination shape when the fanout program + // was built. `workspace.validate` established point < stride. + let (scaled_re, scaled_im) = + unsafe { scale.apply(*current_re.add(source), *current_im.add(source)) }; + for row in rows { + let destination = + (row.destination_component_base as usize + component as usize) * stride + + point; + // SAFETY: see the source/destination proof immediately + // above. Scratch components form a private suffix, so none + // of these writes can alias the representative source. + unsafe { + if row.flags & DIRECT_CONTRIBUTION_FLAG_INITIALIZE_DESTINATION != 0 { + *current_re.add(destination) = scaled_re; + *current_im.add(destination) = scaled_im; + } else { + *current_re.add(destination) += scaled_re; + *current_im.add(destination) += scaled_im; + } + } + } + } + } + } + Ok(()) +} + +#[cfg(any(test, feature = "on-the-fly-test-support"))] +#[allow(clippy::too_many_arguments)] +fn observe_direct_current_rows( + plan: &DirectRecurrencePlan, + descriptor: &DirectRowGroupDescriptor, + start: usize, + end: usize, + selected_sector_id: Option, + workspace: &DirectWorkspace<'_>, + point_count: u32, +) -> RusticolResult<()> { + let active = DIRECT_CURRENT_OBSERVATION.with(|slot| slot.borrow().is_some()); + if !active || descriptor.role == DirectExecutorRole::Closure { + return Ok(()); + } + + let affected = match descriptor.role { + DirectExecutorRole::Source => plan + .currents() + .iter() + .filter(|current| { + current.source_row_or_sentinel != DIRECT_NONE_U32 + && (start..end).contains(&(current.source_row_or_sentinel as usize)) + }) + .map(|current| current.semantic_current_id) + .collect::>(), + DirectExecutorRole::Contribution => { + let destinations = plan + .contributions() + .get(start..end) + .ok_or_else(|| { + RusticolError::integrity( + "direct current observation contribution range is out of bounds", + ) + })? + .iter() + .map(|row| row.destination_component_base) + .collect::>(); + plan.currents() + .iter() + .filter(|current| { + current.node_kind == DirectNodeKind::Current + && current.stage == descriptor.stage + && destinations.iter().any(|destination| { + *destination >= current.component_base + && *destination + < current.component_base + u32::from(current.component_count) + }) + }) + .map(|current| current.semantic_current_id) + .collect::>() + } + DirectExecutorRole::Finalization => plan + .currents() + .iter() + .filter(|current| { + current.finalization_row_or_sentinel != DIRECT_NONE_U32 + && (start..end).contains(&(current.finalization_row_or_sentinel as usize)) + }) + .map(|current| current.semantic_current_id) + .collect::>(), + DirectExecutorRole::Closure => BTreeSet::new(), + }; + if affected.is_empty() { + return Ok(()); + } + + DIRECT_CURRENT_OBSERVATION.with(|slot| { + let mut slot = slot.borrow_mut(); + let observation = slot.as_mut().ok_or_else(|| { + RusticolError::integrity("direct current observation disappeared during execution") + })?; + if observation.plan_semantic_digest != plan.semantic_digest() + || observation.runtime_layout_digest != plan.runtime_layout_digest() + || observation.selected_sector_id != selected_sector_id + || observation.point_count != point_count + { + return Err(RusticolError::integrity( + "direct current observation does not match this plan execution", + )); + } + let stride = workspace.point_stride as usize; + let active_points = point_count as usize; + for semantic_current_id in affected { + let current = plan + .currents() + .get(semantic_current_id as usize) + .filter(|current| current.semantic_current_id == semantic_current_id) + .ok_or_else(|| { + RusticolError::integrity( + "direct current observation semantic current is absent", + ) + })?; + if let Some(sector_id) = selected_sector_id + && !plan.selector_domain_contains(current.selector_domain_id, sector_id)? + { + continue; + } + let mut values = Vec::with_capacity( + usize::from(current.component_count) + .checked_mul(active_points) + .ok_or_else(|| { + RusticolError::integrity( + "direct current observation value count overflows usize", + ) + })?, + ); + for component in 0..usize::from(current.component_count) { + let plane = (current.component_base as usize) + .checked_add(component) + .ok_or_else(|| { + RusticolError::integrity( + "direct current observation component range overflows usize", + ) + })?; + let begin = plane.checked_mul(stride).ok_or_else(|| { + RusticolError::integrity( + "direct current observation arena range overflows usize", + ) + })?; + let finish = begin.checked_add(active_points).ok_or_else(|| { + RusticolError::integrity( + "direct current observation arena range overflows usize", + ) + })?; + let real = workspace.current_re.get(begin..finish).ok_or_else(|| { + RusticolError::integrity( + "direct current observation real range is out of bounds", + ) + })?; + let imaginary = workspace.current_im.get(begin..finish).ok_or_else(|| { + RusticolError::integrity( + "direct current observation imaginary range is out of bounds", + ) + })?; + values.extend(real.iter().copied().zip(imaginary.iter().copied())); + } + observation.currents.insert( + semantic_current_id, + DirectObservedCurrentValue { + descriptor: *current, + values, + }, + ); + } + Ok(()) + }) +} + +pub(crate) fn execute_certified_reuse_rows( + rows: &[DirectContributionRow], + workspace: &mut DirectWorkspace<'_>, + point_count: u32, +) -> RusticolResult<()> { + let stride = workspace.point_stride as usize; + let point_count = point_count as usize; + for (index, row) in rows.iter().enumerate() { + if row.flags & DIRECT_CONTRIBUTION_FLAG_CERTIFIED_REUSE == 0 + || row.parent1_component_base_or_sentinel != DIRECT_NONE_U32 + || row.parent1_momentum_form_id_or_sentinel != DIRECT_NONE_U32 + { + return Err(RusticolError::integrity(format!( + "certified-reuse row {index} has an invalid relation encoding" + ))); + } + let component_count = row.parent0_momentum_form_id as usize; + if component_count == 0 { + return Err(RusticolError::integrity(format!( + "certified-reuse row {index} has zero components" + ))); + } + let factor_index = row.exact_factor_id as usize; + let factor_re = *workspace + .factors_re + .get(factor_index) + .ok_or_else(|| RusticolError::integrity("certified-reuse factor is out of bounds"))?; + let factor_im = *workspace + .factors_im + .get(factor_index) + .ok_or_else(|| RusticolError::integrity("certified-reuse factor is out of bounds"))?; + for component in 0..component_count { + let source_plane = row.parent0_component_base as usize + component; + let destination_plane = row.destination_component_base as usize + component; + let source_start = source_plane.checked_mul(stride).ok_or_else(|| { + RusticolError::integrity("certified-reuse source range overflows usize") + })?; + let destination_start = destination_plane.checked_mul(stride).ok_or_else(|| { + RusticolError::integrity("certified-reuse destination range overflows usize") + })?; + let source_end = source_start.checked_add(point_count).ok_or_else(|| { + RusticolError::integrity("certified-reuse source range overflows usize") + })?; + let destination_end = destination_start.checked_add(point_count).ok_or_else(|| { + RusticolError::integrity("certified-reuse destination range overflows usize") + })?; + if source_end > workspace.current_re.len() + || source_end > workspace.current_im.len() + || destination_end > workspace.current_re.len() + || destination_end > workspace.current_im.len() + { + return Err(RusticolError::integrity( + "certified-reuse source or destination range is out of bounds", + )); + } + // Certified reuse is an exact algebraic scale-copy. In + // particular, an exact zero factor overwrites with zero even when + // the representative aliases a freshly initialized destination. + // Do not evaluate `NaN * 0.0`: without the former unconditional + // stage clear that would preserve stale arena payloads instead of + // materializing the certified zero current. + if factor_re == 0.0 && factor_im == 0.0 { + workspace.current_re[destination_start..destination_end].fill(0.0); + workspace.current_im[destination_start..destination_end].fill(0.0); + continue; + } + for point in 0..point_count { + let source_index = source_start + point; + let destination_index = destination_start + point; + let source_re = *workspace.current_re.get(source_index).ok_or_else(|| { + RusticolError::integrity("certified-reuse source real range is out of bounds") + })?; + let source_im = *workspace.current_im.get(source_index).ok_or_else(|| { + RusticolError::integrity( + "certified-reuse source imaginary range is out of bounds", + ) + })?; + *workspace + .current_re + .get_mut(destination_index) + .ok_or_else(|| { + RusticolError::integrity( + "certified-reuse destination real range is out of bounds", + ) + })? = source_re * factor_re - source_im * factor_im; + *workspace + .current_im + .get_mut(destination_index) + .ok_or_else(|| { + RusticolError::integrity( + "certified-reuse destination imaginary range is out of bounds", + ) + })? = source_re * factor_im + source_im * factor_re; + } + } + } + Ok(()) +} + +pub(crate) fn check_status( + role: DirectExecutorRole, + executor_id: u32, + status: c_int, +) -> RusticolResult<()> { + if status == DIRECT_STATUS_OK { + return Ok(()); + } + if let Some(detail) = take_direct_executor_error_detail() { + Err(RusticolError::with_kind( + detail.kind(), + format!( + "direct recurrence {role:?} executor {executor_id} failed: {}", + detail.message() + ), + )) + } else { + Err(RusticolError::evaluation(format!( + "direct recurrence {role:?} executor {executor_id} returned status {status}" + ))) + } +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/recurrence/direct_backend_tests.rs b/pyamplicol/rust/crates/rusticol-core/src/recurrence/direct_backend_tests.rs new file mode 100644 index 0000000000..8c2afba696 --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/recurrence/direct_backend_tests.rs @@ -0,0 +1,571 @@ +// SPDX-License-Identifier: 0BSD + +use super::direct_backend::*; +use super::direct_plan::{ + DIRECT_CONTRIBUTION_FLAG_CERTIFIED_REUSE, DIRECT_CONTRIBUTION_FLAG_INITIALIZE_DESTINATION, + DIRECT_NONE_U32, DirectClosureRow, DirectContributionRow, DirectExecutorRole, + DirectFinalizationRow, DirectRecurrencePlan, DirectSourceRow, +}; +use super::exact::{ExactComplexRational, ExactRational}; +use crate::RusticolErrorKind; +use std::ffi::c_void; + +const STATUS_BOUNDS: i32 = 2; + +#[test] +fn executor_status_success_keeps_first_failure_detail_until_an_actual_failure() { + clear_direct_executor_error_detail(); + record_direct_executor_error_detail(crate::RusticolError::integrity( + "descriptor row 789823 is invalid", + )); + record_direct_executor_error_detail(crate::RusticolError::internal( + "later detail must not replace the first failure", + )); + + assert!(check_status(DirectExecutorRole::Source, 7, DIRECT_STATUS_OK).is_ok()); + + let error = check_status(DirectExecutorRole::Contribution, 312, 4).unwrap_err(); + assert_eq!(error.kind(), RusticolErrorKind::Integrity); + assert_eq!( + error.message(), + "direct recurrence Contribution executor 312 failed: descriptor row 789823 is invalid" + ); + + let generic = check_status(DirectExecutorRole::Contribution, 312, 4).unwrap_err(); + assert_eq!(generic.kind(), RusticolErrorKind::Evaluation); + assert_eq!( + generic.message(), + "direct recurrence Contribution executor 312 returned status 4" + ); +} + +#[test] +fn executor_status_entry_clear_prevents_stale_detail_leakage() { + record_direct_executor_error_detail(crate::RusticolError::integrity( + "detail from an earlier schedule", + )); + clear_direct_executor_error_detail(); + + let error = check_status(DirectExecutorRole::Closure, 41, 9).unwrap_err(); + assert_eq!(error.kind(), RusticolErrorKind::Evaluation); + assert_eq!( + error.message(), + "direct recurrence Closure executor 41 returned status 9" + ); +} + +unsafe extern "C" fn fill_sources( + _context: *const c_void, + arena: DirectArenaView, + momenta: DirectMomentumView, + _parameters: DirectParameterView, + _factors: DirectFactorView, + rows: *const DirectSourceRow, + row_count: u32, + point_count: u32, +) -> i32 { + let rows = unsafe { std::slice::from_raw_parts(rows, row_count as usize) }; + for row in rows { + for point in 0..point_count as usize { + let source = (row.momentum_form_id as usize + * momenta.lorentz_component_count as usize + * momenta.point_stride as usize) + + point; + let destination = + row.destination_component_base as usize * arena.point_stride as usize + point; + if source >= momenta.scalar_len as usize + || destination >= arena.current_scalar_len as usize + { + return STATUS_BOUNDS; + } + unsafe { + *arena.current_re.add(destination) = *momenta.values.add(source); + *arena.current_im.add(destination) = 0.0; + } + } + } + DIRECT_STATUS_OK +} + +unsafe extern "C" fn accumulate_contributions( + _context: *const c_void, + arena: DirectArenaView, + _momenta: DirectMomentumView, + parameters: DirectParameterView, + factors: DirectFactorView, + rows: *const DirectContributionRow, + row_count: u32, + point_count: u32, +) -> i32 { + let rows = unsafe { std::slice::from_raw_parts(rows, row_count as usize) }; + for row in rows { + if row.exact_factor_id >= factors.value_count || parameters.value_count == 0 { + return STATUS_BOUNDS; + } + let factor_re = unsafe { *factors.values_re.add(row.exact_factor_id as usize) }; + let factor_im = unsafe { *factors.values_im.add(row.exact_factor_id as usize) }; + let parameter_re = unsafe { *parameters.values_re }; + let parameter_im = unsafe { *parameters.values_im }; + let scale_re = factor_re * parameter_re - factor_im * parameter_im; + let scale_im = factor_re * parameter_im + factor_im * parameter_re; + for point in 0..point_count as usize { + let source = row.parent0_component_base as usize * arena.point_stride as usize + point; + let destination = + row.destination_component_base as usize * arena.point_stride as usize + point; + if source >= arena.current_scalar_len as usize + || destination >= arena.current_scalar_len as usize + { + return STATUS_BOUNDS; + } + let source_re = unsafe { *arena.current_re.add(source) }; + let source_im = unsafe { *arena.current_im.add(source) }; + unsafe { + *arena.current_re.add(destination) += source_re * scale_re - source_im * scale_im; + *arena.current_im.add(destination) += source_re * scale_im + source_im * scale_re; + } + } + } + DIRECT_STATUS_OK +} + +unsafe extern "C" fn finalize_currents( + _context: *const c_void, + arena: DirectArenaView, + _momenta: DirectMomentumView, + _parameters: DirectParameterView, + factors: DirectFactorView, + rows: *const DirectFinalizationRow, + row_count: u32, + point_count: u32, +) -> i32 { + let rows = unsafe { std::slice::from_raw_parts(rows, row_count as usize) }; + for row in rows { + if row.exact_factor_id >= factors.value_count { + return STATUS_BOUNDS; + } + let scale = unsafe { *factors.values_re.add(row.exact_factor_id as usize) }; + for component in 0..usize::from(row.component_count) { + for point in 0..point_count as usize { + let destination = + (row.component_base as usize + component) * arena.point_stride as usize + point; + if destination >= arena.current_scalar_len as usize { + return STATUS_BOUNDS; + } + unsafe { + *arena.current_re.add(destination) *= scale; + *arena.current_im.add(destination) *= scale; + } + } + } + } + DIRECT_STATUS_OK +} + +unsafe extern "C" fn accumulate_closures( + _context: *const c_void, + arena: DirectArenaView, + _momenta: DirectMomentumView, + _parameters: DirectParameterView, + factors: DirectFactorView, + rows: *const DirectClosureRow, + row_count: u32, + point_count: u32, +) -> i32 { + let rows = unsafe { std::slice::from_raw_parts(rows, row_count as usize) }; + for row in rows { + if row.exact_factor_id >= factors.value_count { + return STATUS_BOUNDS; + } + let scale = unsafe { *factors.values_re.add(row.exact_factor_id as usize) }; + for point in 0..point_count as usize { + let source = row.parent0_component_base as usize * arena.point_stride as usize + point; + let destination = + row.amplitude_destination_id as usize * arena.point_stride as usize + point; + if source >= arena.current_scalar_len as usize + || destination >= arena.amplitude_scalar_len as usize + { + return STATUS_BOUNDS; + } + unsafe { + *arena.amplitude_re.add(destination) += *arena.current_re.add(source) * scale; + *arena.amplitude_im.add(destination) += *arena.current_im.add(source) * scale; + } + } + } + DIRECT_STATUS_OK +} + +#[test] +fn synthetic_program_reads_rows_and_accumulates_directly_into_arenas() { + let source_rows = [DirectSourceRow { + source_slot: 0, + destination_component_base: 0, + momentum_form_id: 0, + source_template_or_dispatch_domain: 0, + spin_state_class: 1, + exact_factor_id: 0, + selector_domain_id: 0, + }]; + let contribution_rows = [DirectContributionRow { + parent0_component_base: 0, + parent1_component_base_or_sentinel: DIRECT_NONE_U32, + parent0_momentum_form_id: 0, + parent1_momentum_form_id_or_sentinel: u32::MAX, + destination_component_base: 1, + exact_factor_id: 0, + selector_domain_id: 0, + flags: 0, + }]; + let finalization_rows = [DirectFinalizationRow { + component_base: 1, + component_count: 1, + momentum_form_id: 0, + exact_factor_id: 1, + selector_domain_id: 0, + flags: 0, + }]; + let closure_rows = [DirectClosureRow { + parent0_component_base: 1, + parent1_component_base_or_sentinel: DIRECT_NONE_U32, + parent0_momentum_form_id: 0, + parent1_momentum_form_id_or_sentinel: u32::MAX, + amplitude_destination_id: 0, + exact_factor_id: 2, + component_factor_start: 0, + component_count: 1, + selector_domain_id: 0, + flags: 0, + }]; + let mut parts = super::direct_plan::tests::valid_parts(); + parts.current_arena_components = 2; + parts.currents[0].component_count = 1; + parts.currents[1].component_base = 1; + parts.sources = source_rows.to_vec(); + parts.contributions = contribution_rows.to_vec(); + parts.finalizations = finalization_rows.to_vec(); + parts.closures = closure_rows.to_vec(); + parts.exact_factors = vec![ExactComplexRational::ONE; 3]; + let plan = DirectRecurrencePlan::new(parts).unwrap(); + let executors = DirectExecutorCatalog::new( + &plan, + plan.direct_template_catalog_digest(), + vec![ + DirectExecutorHandle::Source { + call: fill_sources, + context: std::ptr::null(), + }, + DirectExecutorHandle::Contribution { + call: accumulate_contributions, + context: std::ptr::null(), + }, + DirectExecutorHandle::Finalization { + call: finalize_currents, + context: std::ptr::null(), + }, + DirectExecutorHandle::Closure { + call: accumulate_closures, + context: std::ptr::null(), + }, + ], + ) + .unwrap(); + + // `DirectWorkspace` is the compatibility-facing public adapter. It keeps + // accepting ordinary Rust slices and the pre-substrate stride contract; + // the owned generic workspace used by the runtime enforces 64-byte planes. + let mut current_re = [0.0; 8]; + let mut current_im = [0.0; 8]; + let mut amplitude_re = [0.0; 4]; + let mut amplitude_im = [0.0; 4]; + let momenta = [1.0, 2.0, 3.0, 4.0]; + let parameters_re = [3.0]; + let parameters_im = [0.0]; + let factors_re = [2.0, 0.5, -1.0]; + let factors_im = [0.0; 3]; + let mut workspace = DirectWorkspace { + current_re: &mut current_re, + current_im: &mut current_im, + amplitude_re: &mut amplitude_re, + amplitude_im: &mut amplitude_im, + momenta: &momenta, + momentum_form_count: 1, + lorentz_component_count: 1, + parameters_re: ¶meters_re, + parameters_im: ¶meters_im, + factors_re: &factors_re, + factors_im: &factors_im, + point_stride: 4, + }; + let current_re_pointer = workspace.current_re.as_ptr(); + let amplitude_re_pointer = workspace.amplitude_re.as_ptr(); + let mut counters = DirectExecutionCounters::default(); + + begin_direct_current_observation(&plan, None, 4).unwrap(); + execute_direct_plan(&plan, &executors, &mut workspace, 4, &mut counters).unwrap(); + let observation = take_direct_current_observation().unwrap(); + + assert_eq!(workspace.current_re.as_ptr(), current_re_pointer); + assert_eq!(workspace.amplitude_re.as_ptr(), amplitude_re_pointer); + assert_eq!(workspace.current_re[0..4], momenta); + assert_eq!(workspace.current_re[4..8], [3.0, 6.0, 9.0, 12.0]); + assert_eq!(workspace.amplitude_re, &[-3.0, -6.0, -9.0, -12.0]); + assert!(workspace.current_im.iter().all(|value| *value == 0.0)); + assert!(workspace.amplitude_im.iter().all(|value| *value == 0.0)); + assert_eq!(observation.plan_semantic_digest, plan.semantic_digest()); + assert_eq!( + observation.runtime_layout_digest, + plan.runtime_layout_digest() + ); + assert_eq!(observation.selected_sector_id, None); + assert_eq!(observation.point_count, 4); + assert_eq!( + observation.currents[&0].values, + [(1.0, 0.0), (2.0, 0.0), (3.0, 0.0), (4.0, 0.0)] + ); + assert_eq!( + observation.currents[&1].values, + [(3.0, 0.0), (6.0, 0.0), (9.0, 0.0), (12.0, 0.0)] + ); + assert_eq!( + counters, + DirectExecutionCounters { + source_calls: 1, + source_rows: 1, + contribution_calls: 1, + contribution_rows: 1, + finalization_calls: 1, + finalization_rows: 1, + closure_calls: 1, + closure_rows: 1, + packed_input_bytes: 0, + packed_output_bytes: 0, + scatter_bytes: 0, + } + ); +} + +#[test] +fn certified_reuse_row_scale_copies_without_calling_an_interaction_executor() { + let mut parts = super::direct_plan::tests::valid_parts(); + parts.currents[0].component_count = 1; + parts.contributions[0] = DirectContributionRow { + parent0_component_base: 0, + parent1_component_base_or_sentinel: DIRECT_NONE_U32, + // The certified-reuse encoding stores component count here. + parent0_momentum_form_id: 1, + parent1_momentum_form_id_or_sentinel: DIRECT_NONE_U32, + destination_component_base: 2, + exact_factor_id: 0, + selector_domain_id: 0, + flags: DIRECT_CONTRIBUTION_FLAG_INITIALIZE_DESTINATION + | DIRECT_CONTRIBUTION_FLAG_CERTIFIED_REUSE, + }; + parts.row_groups[1].direct_executor_id = DIRECT_NONE_U32; + parts.finalizations[0].exact_factor_id = 1; + parts.closures[0].exact_factor_id = 1; + parts.closures[0].component_factor_start = 1; + parts.exact_factors = vec![ + ExactComplexRational::new( + ExactRational::new(2, 1).unwrap(), + ExactRational::new(1, 1).unwrap(), + ), + ExactComplexRational::ONE, + ]; + let plan = DirectRecurrencePlan::new(parts).unwrap(); + let executors = DirectExecutorCatalog::new( + &plan, + plan.direct_template_catalog_digest(), + vec![ + DirectExecutorHandle::Source { + call: fill_sources, + context: std::ptr::null(), + }, + DirectExecutorHandle::Contribution { + call: accumulate_contributions, + context: std::ptr::null(), + }, + DirectExecutorHandle::Finalization { + call: finalize_currents, + context: std::ptr::null(), + }, + DirectExecutorHandle::Closure { + call: accumulate_closures, + context: std::ptr::null(), + }, + ], + ) + .unwrap(); + + let mut current_re = [0.0; 12]; + let mut current_im = [0.0; 12]; + let mut amplitude_re = [0.0; 4]; + let mut amplitude_im = [0.0; 4]; + let momenta = [1.0, 2.0, 3.0, 4.0]; + let parameters_re = [1.0]; + let parameters_im = [0.0]; + let factors_re = [2.0, 1.0]; + let factors_im = [1.0, 0.0]; + let mut workspace = DirectWorkspace { + current_re: &mut current_re, + current_im: &mut current_im, + amplitude_re: &mut amplitude_re, + amplitude_im: &mut amplitude_im, + momenta: &momenta, + momentum_form_count: 1, + lorentz_component_count: 1, + parameters_re: ¶meters_re, + parameters_im: ¶meters_im, + factors_re: &factors_re, + factors_im: &factors_im, + point_stride: 4, + }; + let mut counters = DirectExecutionCounters::default(); + execute_direct_plan(&plan, &executors, &mut workspace, 4, &mut counters).unwrap(); + + assert_eq!(workspace.current_re[8..12], [2.0, 4.0, 6.0, 8.0]); + assert_eq!(workspace.current_im[8..12], [1.0, 2.0, 3.0, 4.0]); + assert_eq!(counters.contribution_calls, 1); + assert_eq!(counters.contribution_rows, 1); +} + +#[test] +fn zero_self_reuse_clears_stale_destination_before_scale_copy() { + let mut parts = super::direct_plan::tests::valid_parts(); + parts.currents[0].component_count = 1; + parts.contributions[0] = DirectContributionRow { + // A certified zero current legitimately uses its own just-cleared + // destination as the execution representative. + parent0_component_base: 2, + parent1_component_base_or_sentinel: DIRECT_NONE_U32, + parent0_momentum_form_id: 1, + parent1_momentum_form_id_or_sentinel: DIRECT_NONE_U32, + destination_component_base: 2, + exact_factor_id: 0, + selector_domain_id: 0, + flags: DIRECT_CONTRIBUTION_FLAG_INITIALIZE_DESTINATION + | DIRECT_CONTRIBUTION_FLAG_CERTIFIED_REUSE, + }; + parts.row_groups[1].direct_executor_id = DIRECT_NONE_U32; + parts.finalizations[0].exact_factor_id = 1; + parts.closures[0].exact_factor_id = 1; + parts.closures[0].component_factor_start = 1; + parts.exact_factors = vec![ExactComplexRational::ZERO, ExactComplexRational::ONE]; + let plan = DirectRecurrencePlan::new(parts).unwrap(); + let executors = DirectExecutorCatalog::new( + &plan, + plan.direct_template_catalog_digest(), + vec![ + DirectExecutorHandle::Source { + call: fill_sources, + context: std::ptr::null(), + }, + DirectExecutorHandle::Contribution { + call: accumulate_contributions, + context: std::ptr::null(), + }, + DirectExecutorHandle::Finalization { + call: finalize_currents, + context: std::ptr::null(), + }, + DirectExecutorHandle::Closure { + call: accumulate_closures, + context: std::ptr::null(), + }, + ], + ) + .unwrap(); + + let mut current_re = [f64::NAN; 12]; + let mut current_im = [f64::NAN; 12]; + let mut amplitude_re = [0.0; 4]; + let mut amplitude_im = [0.0; 4]; + let momenta = [1.0, 2.0, 3.0, 4.0]; + let parameters_re = [1.0]; + let parameters_im = [0.0]; + let factors_re = [0.0, 1.0]; + let factors_im = [0.0, 0.0]; + let mut workspace = DirectWorkspace { + current_re: &mut current_re, + current_im: &mut current_im, + amplitude_re: &mut amplitude_re, + amplitude_im: &mut amplitude_im, + momenta: &momenta, + momentum_form_count: 1, + lorentz_component_count: 1, + parameters_re: ¶meters_re, + parameters_im: ¶meters_im, + factors_re: &factors_re, + factors_im: &factors_im, + point_stride: 4, + }; + let mut counters = DirectExecutionCounters::default(); + + execute_direct_plan(&plan, &executors, &mut workspace, 4, &mut counters).unwrap(); + + assert_eq!(workspace.current_re[8..12], [0.0; 4]); + assert_eq!(workspace.current_im[8..12], [0.0; 4]); + assert_eq!(counters.contribution_calls, 1); + assert_eq!(counters.contribution_rows, 1); + assert_eq!( + plan.row_groups()[1].direct_executor_id, + DIRECT_NONE_U32, + "certified zero reuse must not call an interaction executor" + ); +} + +#[test] +fn direct_backend_source_contains_no_eager_packing_or_batch_evaluator_route() { + let source = include_str!("direct_backend.rs"); + for forbidden in [ + "EagerKernelInput", + "EagerKernelBackend", + "EagerKernelCall", + "evaluate_batch", + ] { + assert!( + !source.contains(forbidden), + "direct backend must not contain {forbidden}" + ); + } +} + +#[test] +fn executor_catalog_authenticates_catalog_digest_and_stable_roles() { + let plan = super::direct_plan::tests::valid_plan(); + let handles = || { + vec![ + DirectExecutorHandle::Source { + call: fill_sources, + context: std::ptr::null(), + }, + DirectExecutorHandle::Contribution { + call: accumulate_contributions, + context: std::ptr::null(), + }, + DirectExecutorHandle::Finalization { + call: finalize_currents, + context: std::ptr::null(), + }, + DirectExecutorHandle::Closure { + call: accumulate_closures, + context: std::ptr::null(), + }, + ] + }; + let wrong_digest = crate::recurrence::SemanticDigest::new([0x44; 32]).unwrap(); + let error = DirectExecutorCatalog::new(&plan, wrong_digest, handles()) + .err() + .unwrap(); + assert!(error.to_string().contains("catalog digest")); + + let mut wrong_roles = handles(); + wrong_roles[1] = DirectExecutorHandle::Source { + call: fill_sources, + context: std::ptr::null(), + }; + let error = + DirectExecutorCatalog::new(&plan, plan.direct_template_catalog_digest(), wrong_roles) + .err() + .unwrap(); + assert!(error.to_string().contains("expected Contribution")); +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/recurrence/direct_codec.rs b/pyamplicol/rust/crates/rusticol-core/src/recurrence/direct_codec.rs new file mode 100644 index 0000000000..fae09835d3 --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/recurrence/direct_codec.rs @@ -0,0 +1,1169 @@ +// SPDX-License-Identifier: 0BSD + +//! Deterministic little-endian codec for recurrence direct-plan v2. + +use super::direct_plan::{ + DirectAmplitudeDestinationDescriptor, DirectClosureRow, DirectContributionRow, + DirectCurrentDescriptor, DirectDestinationOperation, DirectExecutorRole, DirectFinalizationRow, + DirectMomentumFormDescriptor, DirectMomentumTerm, DirectNodeKind, DirectRecurrencePlan, + DirectRecurrencePlanParts, DirectReplayTargetDescriptor, DirectResolvedHelicityDescriptor, + DirectResolvedSourceSelection, DirectRowGroupDescriptor, DirectSelectorDomainDescriptor, + DirectSourceDispatchVariantDescriptor, DirectSourceEmbeddingRow, DirectSourceProjectionRow, + DirectSourceRow, DirectSourceStateAssignment, +}; +use super::{ + CheckedTableRange, ClosureCandidateDomainCertificateV1, ClosureExecutionProofGroupV2, + ClosureProofContributionV2, ClosureProofMetadataV2, ExactComplexRational, ExactRational, + RecurrenceStrategy, ReflectionCertificateV1, SemanticDigest, ThreeLineTraversalCertificateV1, + ThreeLineTraversalKindV1, +}; +use crate::{RusticolError, RusticolResult}; +use std::io::Write; + +const MAGIC: &[u8; 8] = b"PACRDAP2"; +const VERSION: u32 = 2; +const MAX_ROWS: u64 = u32::MAX as u64; + +fn invalid(message: impl Into) -> RusticolError { + RusticolError::invalid_argument(format!("recurrence direct-plan codec: {}", message.into())) +} + +struct Writer { + destination: W, + bytes_written: u64, +} + +impl Writer { + fn new(destination: W) -> Self { + Self { + destination, + bytes_written: 0, + } + } + + #[cfg(test)] + fn with_bytes_written(destination: W, bytes_written: u64) -> Self { + Self { + destination, + bytes_written, + } + } + + fn raw(&mut self, bytes: &[u8]) -> RusticolResult<()> { + let byte_count = + u64::try_from(bytes.len()).map_err(|_| invalid("payload write length exceeds u64"))?; + let end = self + .bytes_written + .checked_add(byte_count) + .ok_or_else(|| invalid("payload length overflows u64"))?; + self.destination.write_all(bytes).map_err(|error| { + RusticolError::serialization(format!( + "could not stream recurrence direct-plan payload: {error}" + )) + })?; + self.bytes_written = end; + Ok(()) + } + + fn u16(&mut self, value: u16) -> RusticolResult<()> { + self.raw(&value.to_le_bytes()) + } + + fn u32(&mut self, value: u32) -> RusticolResult<()> { + self.raw(&value.to_le_bytes()) + } + + fn i32(&mut self, value: i32) -> RusticolResult<()> { + self.raw(&value.to_le_bytes()) + } + + fn u64(&mut self, value: u64) -> RusticolResult<()> { + self.raw(&value.to_le_bytes()) + } + + fn i128(&mut self, value: i128) -> RusticolResult<()> { + self.raw(&value.to_le_bytes()) + } + + fn digest(&mut self, digest: SemanticDigest) -> RusticolResult<()> { + self.raw(digest.as_bytes()) + } + + fn optional_u32(&mut self, value: Option) -> RusticolResult<()> { + self.u32(value.unwrap_or(u32::MAX)) + } + + fn exact(&mut self, value: ExactComplexRational) -> RusticolResult<()> { + for rational in [value.real(), value.imag()] { + self.i128(rational.numerator())?; + self.i128(rational.denominator())?; + } + Ok(()) + } + + fn u32_slice(&mut self, label: &str, values: &[u32]) -> RusticolResult<()> { + self.count(label, values.len())?; + for value in values { + self.u32(*value)?; + } + Ok(()) + } + + fn optional_u32_slice(&mut self, label: &str, values: &[Option]) -> RusticolResult<()> { + self.count(label, values.len())?; + for value in values { + self.optional_u32(*value)?; + } + Ok(()) + } + + fn digest_slice(&mut self, label: &str, values: &[SemanticDigest]) -> RusticolResult<()> { + self.count(label, values.len())?; + for value in values { + self.digest(*value)?; + } + Ok(()) + } + + fn count(&mut self, label: &str, count: usize) -> RusticolResult<()> { + let count = + u64::try_from(count).map_err(|_| invalid(format!("{label} count exceeds u64")))?; + if count > MAX_ROWS { + return Err(invalid(format!("{label} count exceeds the u32 ID domain"))); + } + self.u64(count) + } +} + +struct Reader<'a> { + bytes: &'a [u8], + offset: usize, +} + +impl<'a> Reader<'a> { + fn new(bytes: &'a [u8]) -> RusticolResult { + Ok(Self { bytes, offset: 0 }) + } + + fn take(&mut self, count: usize, label: &str) -> RusticolResult<&'a [u8]> { + let end = self + .offset + .checked_add(count) + .ok_or_else(|| invalid(format!("{label} offset overflows usize")))?; + let bytes = self.bytes.get(self.offset..end).ok_or_else(|| { + invalid(format!( + "truncated {label} at byte {}: need {count}, have {}", + self.offset, + self.bytes.len().saturating_sub(self.offset) + )) + })?; + self.offset = end; + Ok(bytes) + } + + fn u16(&mut self, label: &str) -> RusticolResult { + Ok(u16::from_le_bytes( + self.take(2, label)?.try_into().expect("checked read"), + )) + } + + fn u32(&mut self, label: &str) -> RusticolResult { + Ok(u32::from_le_bytes( + self.take(4, label)?.try_into().expect("checked read"), + )) + } + + fn i32(&mut self, label: &str) -> RusticolResult { + Ok(i32::from_le_bytes( + self.take(4, label)?.try_into().expect("checked read"), + )) + } + + fn u64(&mut self, label: &str) -> RusticolResult { + Ok(u64::from_le_bytes( + self.take(8, label)?.try_into().expect("checked read"), + )) + } + + fn i128(&mut self, label: &str) -> RusticolResult { + Ok(i128::from_le_bytes( + self.take(16, label)?.try_into().expect("checked read"), + )) + } + + fn digest(&mut self, label: &str) -> RusticolResult { + let bytes: [u8; 32] = self.take(32, label)?.try_into().expect("checked read"); + SemanticDigest::new(bytes).map_err(|error| invalid(error.message())) + } + + fn optional_u32(&mut self, label: &str) -> RusticolResult> { + let value = self.u32(label)?; + Ok((value != u32::MAX).then_some(value)) + } + + fn exact(&mut self, label: &str) -> RusticolResult { + Ok(ExactComplexRational::new( + read_rational(self, &format!("{label} real"))?, + read_rational(self, &format!("{label} imaginary"))?, + )) + } + + fn u32_vec(&mut self, label: &str) -> RusticolResult> { + let count = self.count(label, 4)?; + (0..count) + .map(|_| self.u32(label)) + .collect::>>() + } + + fn optional_u32_vec(&mut self, label: &str) -> RusticolResult>> { + let count = self.count(label, 4)?; + (0..count) + .map(|_| self.optional_u32(label)) + .collect::>>() + } + + fn digest_vec(&mut self, label: &str) -> RusticolResult> { + let count = self.count(label, 32)?; + (0..count) + .map(|_| self.digest(label)) + .collect::>>() + } + + fn count(&mut self, label: &str, row_bytes: usize) -> RusticolResult { + let count = self.u64(&format!("{label} count"))?; + if count > MAX_ROWS { + return Err(invalid(format!("{label} count exceeds the u32 ID domain"))); + } + let count = + usize::try_from(count).map_err(|_| invalid(format!("{label} count exceeds usize")))?; + if row_bytes != 0 && count > self.bytes.len().saturating_sub(self.offset) / row_bytes { + return Err(invalid(format!( + "{label} count cannot fit in the remaining payload" + ))); + } + Ok(count) + } + + fn finish(self) -> RusticolResult<()> { + if self.offset != self.bytes.len() { + return Err(invalid(format!( + "payload contains {} trailing bytes", + self.bytes.len() - self.offset + ))); + } + Ok(()) + } +} + +pub fn encode_recurrence_direct_plan_v2(plan: &DirectRecurrencePlan) -> RusticolResult> { + let mut payload = Vec::new(); + encode_recurrence_direct_plan_v2_to_writer(plan, &mut payload)?; + Ok(payload) +} + +pub(crate) fn encode_recurrence_direct_plan_v2_to_writer( + plan: &DirectRecurrencePlan, + destination: W, +) -> RusticolResult { + plan.ensure_serializable()?; + let mut writer = Writer::new(destination); + writer.raw(MAGIC)?; + writer.u32(VERSION)?; + writer.u32(0)?; + writer.u32(plan.strategy().as_u32())?; + writer.u32(plan.point_tile_size())?; + writer.u32(plan.workspace_mib())?; + writer.u32(plan.current_arena_components())?; + writer.u32(plan.physical_sector_count())?; + writer.u64(plan.retained_helicity_count())?; + writer.u32(plan.amplitude_destination_count())?; + writer.u32(plan.parameter_value_count())?; + writer.u32(plan.external_source_count())?; + writer.u32(plan.state_template_count())?; + writer.u32(plan.source_template_count())?; + writer.u32(plan.source_template_or_dispatch_count())?; + writer.u32(plan.runtime_helicity_contract_count())?; + writer.u32(plan.runtime_helicity_variant_count())?; + writer.u32(plan.direct_executor_count())?; + writer.digest(plan.semantic_digest())?; + writer.digest(plan.prepared_pack_digest())?; + writer.digest(plan.direct_template_catalog_digest())?; + writer.digest(plan.runtime_layout_digest())?; + writer.digest( + plan.closure_proofs() + .expected_semantic_completeness_digest(), + )?; + writer.u64( + plan.closure_proofs() + .candidate_domain_certificate() + .accepted_candidate_count(), + )?; + writer.digest( + plan.closure_proofs() + .candidate_domain_certificate() + .accepted_candidate_digest(), + )?; + + writer.count("currents", plan.currents().len())?; + writer.count("sources", plan.sources().len())?; + writer.count("contributions", plan.contributions().len())?; + writer.count("finalizations", plan.finalizations().len())?; + writer.count("closures", plan.closures().len())?; + writer.count("row groups", plan.row_groups().len())?; + writer.count("momentum forms", plan.momentum_forms().len())?; + writer.count("momentum terms", plan.momentum_terms().len())?; + writer.count("selector domains", plan.selector_domains().len())?; + writer.count("selector words", plan.selector_words().len())?; + writer.count("replay targets", plan.replay_targets().len())?; + writer.count("source permutations", plan.source_permutations().len())?; + writer.count("replay momentum signs", plan.replay_momentum_signs().len())?; + writer.count("replay helicity map", plan.replay_helicity_map().len())?; + writer.count( + "amplitude destinations", + plan.amplitude_destinations().len(), + )?; + writer.count("resolved helicities", plan.resolved_helicities().len())?; + writer.count( + "source-state assignments", + plan.source_state_assignments().len(), + )?; + writer.count( + "source dispatch variants", + plan.source_dispatch_variants().len(), + )?; + writer.count("source embeddings", plan.source_embeddings().len())?; + writer.count("source projections", plan.source_projections().len())?; + writer.count( + "resolved source selections", + plan.resolved_source_selections().len(), + )?; + writer.count("public helicities", plan.public_helicities().len())?; + writer.count("exact factors", plan.exact_factors().len())?; + writer.count( + "closure proof contributions", + plan.closure_proofs().contributions().len(), + )?; + writer.count("closure proof groups", plan.closure_proofs().groups().len())?; + writer.count( + "reflection certificates", + plan.closure_proofs().reflection_certificates().len(), + )?; + writer.count( + "three-line traversal certificates", + plan.closure_proofs() + .three_line_traversal_certificates() + .len(), + )?; + + for row in plan.currents() { + writer.u32(row.semantic_current_id)?; + writer.u16(row.node_kind as u16)?; + writer.u16(row.stage)?; + writer.u32(row.state_template_id)?; + writer.u32(row.component_base)?; + writer.u16(row.component_count)?; + writer.u16(0)?; + writer.u32(row.momentum_form_id)?; + writer.u32(row.selector_domain_id)?; + writer.u32(row.first_use)?; + writer.u32(row.last_use)?; + writer.u32(row.source_row_or_sentinel)?; + writer.u32(row.finalization_row_or_sentinel)?; + } + for row in plan.sources() { + writer.u32(row.source_slot)?; + writer.u32(row.destination_component_base)?; + writer.u32(row.momentum_form_id)?; + writer.u32(row.source_template_or_dispatch_domain)?; + writer.i32(row.spin_state_class)?; + writer.u32(row.exact_factor_id)?; + writer.u32(row.selector_domain_id)?; + } + for row in plan.contributions() { + for value in [ + row.parent0_component_base, + row.parent1_component_base_or_sentinel, + row.parent0_momentum_form_id, + row.parent1_momentum_form_id_or_sentinel, + row.destination_component_base, + row.exact_factor_id, + row.selector_domain_id, + row.flags, + ] { + writer.u32(value)?; + } + } + for row in plan.finalizations() { + writer.u32(row.component_base)?; + writer.u16(row.component_count)?; + writer.u16(0)?; + for value in [ + row.momentum_form_id, + row.exact_factor_id, + row.selector_domain_id, + row.flags, + ] { + writer.u32(value)?; + } + } + for row in plan.closures() { + for value in [ + row.parent0_component_base, + row.parent1_component_base_or_sentinel, + row.parent0_momentum_form_id, + row.parent1_momentum_form_id_or_sentinel, + row.amplitude_destination_id, + row.exact_factor_id, + row.component_factor_start, + ] { + writer.u32(value)?; + } + writer.u16(row.component_count)?; + writer.u16(0)?; + writer.u32(row.selector_domain_id)?; + writer.u32(row.flags)?; + } + for row in plan.row_groups() { + writer.u16(row.stage)?; + writer.u16(row.role as u16)?; + writer.u16(row.destination_operation as u16)?; + writer.u16(0)?; + writer.u32(row.direct_executor_id)?; + writer.u64(row.row_start)?; + writer.u32(row.row_count)?; + writer.u32(0)?; + } + for row in plan.momentum_forms() { + writer.u64(row.term_start)?; + writer.u32(row.term_count)?; + writer.u32(0)?; + } + for row in plan.momentum_terms() { + writer.u32(row.source_slot)?; + writer.i32(row.coefficient)?; + } + for row in plan.selector_domains() { + writer.u64(row.word_start)?; + writer.u32(row.word_count)?; + writer.u32(0)?; + } + for value in plan.selector_words() { + writer.u64(*value)?; + } + for row in plan.replay_targets() { + writer.u32(row.public_flow_id)?; + writer.u32(row.representative_id)?; + writer.u64(row.source_permutation_start)?; + writer.u32(row.source_permutation_count)?; + writer.u64(row.helicity_map_start)?; + writer.u32(row.helicity_map_count)?; + writer.u32(row.phase_exact_factor_id)?; + writer.u32(row.multiplicity)?; + writer.u32(row.selector_domain_id)?; + } + for value in plan.source_permutations() { + writer.u32(*value)?; + } + for value in plan.replay_momentum_signs() { + writer.i32(*value)?; + } + for value in plan.replay_helicity_map() { + writer.u32(*value)?; + } + for row in plan.amplitude_destinations() { + writer.u64(row.closure_row_start)?; + writer.u32(row.id)?; + writer.u32(row.target_sector_id)?; + writer.u32(row.target_helicity_id_or_sentinel)?; + writer.u32(row.closure_row_count)?; + writer.u32(row.selector_domain_id)?; + writer.u32(0)?; + } + for row in plan.resolved_helicities() { + writer.u64(row.source_state_start)?; + writer.u64(row.source_selection_start)?; + writer.u64(row.public_helicity_start)?; + writer.u32(row.id)?; + writer.u32(row.source_state_count)?; + writer.u32(row.source_selection_count)?; + writer.u32(row.public_helicity_count)?; + writer.u32(row.selector_domain_id)?; + } + for row in plan.source_state_assignments() { + writer.u32(row.source_slot)?; + writer.u32(row.state_index)?; + } + for row in plan.source_dispatch_variants() { + writer.u64(row.embedding_start)?; + writer.u64(row.projection_start)?; + for value in [ + row.source_row_id, + row.dispatch_domain_id, + row.runtime_variant_id, + row.source_state_index, + row.source_template_id, + row.source_state_template_id, + row.crossed_state_template_id, + ] { + writer.u32(value)?; + } + writer.i32(row.crossed_spin_state_class)?; + for value in [ + row.direct_executor_id, + row.crossing_exact_factor_id, + row.embedding_count, + row.projection_count, + ] { + writer.u32(value)?; + } + } + for row in plan.source_embeddings() { + writer.u32(row.full_component)?; + writer.u32(row.source_component_or_sentinel)?; + writer.u32(row.exact_factor_id)?; + } + for row in plan.source_projections() { + writer.u32(row.source_component)?; + writer.u32(row.full_component)?; + } + for row in plan.resolved_source_selections() { + writer.u32(row.source_slot)?; + writer.u32(row.dispatch_variant_id)?; + } + for value in plan.public_helicities() { + writer.i32(*value)?; + } + for value in plan.exact_factors() { + writer.exact(*value)?; + } + for row in plan.closure_proofs().contributions() { + writer.u32(row.id())?; + writer.u32(row.target_sector_id())?; + writer.optional_u32(row.target_destination_id())?; + writer.optional_u32(row.target_helicity_id())?; + writer.u32(row.closure_template_id())?; + writer.digest(row.closure_template_semantic_digest())?; + writer.optional_u32(row.quantum_flow_template_id())?; + writer.u32_slice( + "closure construction builder-parent IDs", + row.construction_parent_builder_ids(), + )?; + writer.optional_u32_slice( + "closure construction runtime-parent IDs", + row.construction_parent_runtime_ids(), + )?; + writer.digest_slice( + "closure construction parent semantic digests", + row.construction_parent_semantic_digests(), + )?; + writer.digest_slice( + "closure construction parent color digests", + row.construction_parent_color_digests(), + )?; + writer.u32_slice( + "closure construction parent permutation", + row.construction_parent_permutation(), + )?; + writer.u32_slice( + "closure reconstruction parent permutation", + row.reconstruction_parent_permutation(), + )?; + writer.u32_slice( + "closure evaluator parent permutation", + row.evaluator_parent_permutation(), + )?; + writer.u32(row.color_witness_term_id())?; + writer.digest(row.color_witness_proof_digest())?; + writer.optional_u32(row.three_line_certificate_id())?; + writer.u32_slice( + "closure pairing certificates", + row.pairing_certificate_ids(), + )?; + writer.optional_u32(row.reflection_certificate_id())?; + writer.exact(row.exact_factor())?; + writer.u32(row.multiplicity())?; + } + for row in plan.closure_proofs().groups() { + writer.u32(row.id())?; + writer.optional_u32(row.emitted_runtime_closure_term_id())?; + writer.optional_u32(row.emitted_direct_closure_row_id())?; + writer.u64(row.contribution_range().start)?; + writer.u64(row.contribution_range().count)?; + writer.exact(row.exact_summed_factor())?; + writer.digest(row.component_factor_digest())?; + writer.digest(row.candidate_selector_domain_digest())?; + writer.digest(row.selector_domain_digest())?; + } + for row in plan.closure_proofs().reflection_certificates() { + writer.u32(row.id())?; + writer.digest(row.orbit_identity())?; + writer.digest(row.reciprocal_identity())?; + writer.u32_slice("reflection source permutation", row.source_permutation())?; + writer.exact(row.exact_phase())?; + writer.u32(u32::from(row.fixed_point()))?; + writer.u32(row.orbit_size())?; + writer.u32(row.proof_algorithm_id())?; + writer.digest(row.proof_digest())?; + } + for row in plan.closure_proofs().three_line_traversal_certificates() { + writer.u32(row.id())?; + writer.u32(row.sector_id())?; + writer.u32(row.kind().code())?; + writer.u32(row.sink_block_ordinal())?; + writer.u32_slice( + "three-line reference block order", + row.reference_block_order(), + )?; + writer.u32_slice("three-line witness block order", row.witness_block_order())?; + writer.u32_slice("three-line block permutation", row.block_permutation())?; + writer.u32_slice( + "three-line reference source order", + row.reference_source_order(), + )?; + writer.u32_slice( + "three-line witness source order", + row.witness_source_order(), + )?; + writer.u32_slice( + "three-line source-position permutation", + row.source_position_permutation(), + )?; + writer.u32(row.closure_anchor_source_slot())?; + writer.u32(row.pairing_rule_id())?; + writer.digest(row.proof_digest())?; + } + Ok(writer.bytes_written) +} + +#[cfg_attr(target_vendor = "apple", unsafe(link_section = "__TEXT,__rcl_load"))] +#[cfg_attr(target_vendor = "apple", inline(never))] +#[cfg_attr(target_vendor = "apple", cold)] +pub fn decode_recurrence_direct_plan_v2(bytes: &[u8]) -> RusticolResult { + #[cfg(feature = "on-the-fly-test-support")] + super::on_the_fly::reject_forbidden_work_if_probed( + super::on_the_fly::OnTheFlyForbiddenWorkV1::DirectPlanDecode, + )?; + let mut reader = Reader::new(bytes)?; + if reader.take(8, "magic")? != MAGIC { + return Err(invalid( + "unsupported recurrence payload; regenerate with direct-plan v2", + )); + } + if reader.u32("version")? != VERSION { + return Err(invalid( + "unsupported recurrence version; regenerate with direct-plan v2", + )); + } + if reader.u32("header flags")? != 0 { + return Err(invalid("unsupported nonzero header flags")); + } + let strategy = RecurrenceStrategy::try_from(reader.u32("strategy")?) + .map_err(|error| invalid(error.message()))?; + let point_tile_size = reader.u32("point tile size")?; + let workspace_mib = reader.u32("workspace MiB")?; + let current_arena_components = reader.u32("current arena components")?; + let physical_sector_count = reader.u32("physical LC sectors")?; + let retained_helicity_count = reader.u64("retained helicities")?; + let amplitude_destination_count = reader.u32("amplitude destinations")?; + let parameter_value_count = reader.u32("parameter values")?; + let external_source_count = reader.u32("external sources")?; + let state_template_count = reader.u32("state templates")?; + let source_template_count = reader.u32("source templates")?; + let source_template_or_dispatch_count = reader.u32("source templates or dispatch domains")?; + let runtime_helicity_contract_count = reader.u32("runtime-helicity contracts")?; + let runtime_helicity_variant_count = reader.u32("runtime-helicity variants")?; + let direct_executor_count = reader.u32("direct executors")?; + let semantic_digest = reader.digest("semantic digest")?; + let prepared_pack_digest = reader.digest("prepared-pack digest")?; + let direct_template_catalog_digest = reader.digest("direct-template catalog digest")?; + let expected_runtime_layout_digest = reader.digest("runtime-layout digest")?; + let expected_closure_proof_digest = + reader.digest("closure-proof semantic completeness digest")?; + let closure_candidate_domain_certificate = ClosureCandidateDomainCertificateV1::new( + reader.u64("closure candidate-domain count")?, + reader.digest("closure candidate-domain digest")?, + ); + + let current_count = reader.count("currents", 44)?; + let source_count = reader.count("sources", 28)?; + let contribution_count = reader.count("contributions", 32)?; + let finalization_count = reader.count("finalizations", 24)?; + let closure_count = reader.count("closures", 40)?; + let row_group_count = reader.count("row groups", 28)?; + let momentum_form_count = reader.count("momentum forms", 16)?; + let momentum_term_count = reader.count("momentum terms", 8)?; + let selector_domain_count = reader.count("selector domains", 16)?; + let selector_word_count = reader.count("selector words", 8)?; + let replay_target_count = reader.count("replay targets", 44)?; + let source_permutation_count = reader.count("source permutations", 4)?; + let replay_momentum_sign_count = reader.count("replay momentum signs", 4)?; + let replay_helicity_map_count = reader.count("replay helicity map", 4)?; + let amplitude_destination_descriptor_count = reader.count("amplitude destinations", 32)?; + let resolved_helicity_count = reader.count("resolved helicities", 44)?; + let source_state_assignment_count = reader.count("source-state assignments", 8)?; + let source_dispatch_variant_count = reader.count("source dispatch variants", 64)?; + let source_embedding_count = reader.count("source embeddings", 12)?; + let source_projection_count = reader.count("source projections", 8)?; + let resolved_source_selection_count = reader.count("resolved source selections", 8)?; + let public_helicity_count = reader.count("public helicities", 4)?; + let exact_factor_count = reader.count("exact factors", 64)?; + let closure_proof_contribution_count = reader.count("closure proof contributions", 0)?; + let closure_proof_group_count = reader.count("closure proof groups", 0)?; + let reflection_certificate_count = reader.count("reflection certificates", 0)?; + let three_line_traversal_certificate_count = + reader.count("three-line traversal certificates", 0)?; + + let mut currents = Vec::with_capacity(current_count); + for _ in 0..current_count { + let semantic_current_id = reader.u32("current semantic ID")?; + let node_kind = DirectNodeKind::try_from(reader.u16("current node kind")?)?; + let stage = reader.u16("current stage")?; + let state_template_id = reader.u32("current state template")?; + let component_base = reader.u32("current component base")?; + let component_count = reader.u16("current component count")?; + require_zero(reader.u16("current reserved")?, "current reserved")?; + currents.push(DirectCurrentDescriptor { + semantic_current_id, + node_kind, + state_template_id, + component_base, + component_count, + momentum_form_id: reader.u32("current momentum form")?, + stage, + selector_domain_id: reader.u32("current selector domain")?, + first_use: reader.u32("current first use")?, + last_use: reader.u32("current last use")?, + source_row_or_sentinel: reader.u32("current source row")?, + finalization_row_or_sentinel: reader.u32("current finalization row")?, + }); + } + let mut sources = Vec::with_capacity(source_count); + for _ in 0..source_count { + let source_slot = reader.u32("source slot")?; + let destination_component_base = reader.u32("source destination")?; + let momentum_form_id = reader.u32("source momentum form")?; + let source_template_or_dispatch_domain = reader.u32("source template")?; + let spin_state_class = reader.i32("source spin state")?; + sources.push(DirectSourceRow { + source_slot, + destination_component_base, + momentum_form_id, + source_template_or_dispatch_domain, + spin_state_class, + exact_factor_id: reader.u32("source exact factor")?, + selector_domain_id: reader.u32("source selector domain")?, + }); + } + let mut contributions = Vec::with_capacity(contribution_count); + for _ in 0..contribution_count { + contributions.push(DirectContributionRow { + parent0_component_base: reader.u32("contribution parent 0")?, + parent1_component_base_or_sentinel: reader.u32("contribution parent 1")?, + parent0_momentum_form_id: reader.u32("contribution momentum 0")?, + parent1_momentum_form_id_or_sentinel: reader.u32("contribution momentum 1")?, + destination_component_base: reader.u32("contribution destination")?, + exact_factor_id: reader.u32("contribution exact factor")?, + selector_domain_id: reader.u32("contribution selector domain")?, + flags: reader.u32("contribution flags")?, + }); + } + let mut finalizations = Vec::with_capacity(finalization_count); + for _ in 0..finalization_count { + let component_base = reader.u32("finalization component base")?; + let component_count = reader.u16("finalization component count")?; + require_zero( + reader.u16("finalization reserved")?, + "finalization reserved", + )?; + finalizations.push(DirectFinalizationRow { + component_base, + component_count, + momentum_form_id: reader.u32("finalization momentum form")?, + exact_factor_id: reader.u32("finalization exact factor")?, + selector_domain_id: reader.u32("finalization selector domain")?, + flags: reader.u32("finalization flags")?, + }); + } + let mut closures = Vec::with_capacity(closure_count); + for _ in 0..closure_count { + closures.push(DirectClosureRow { + parent0_component_base: reader.u32("closure parent 0")?, + parent1_component_base_or_sentinel: reader.u32("closure parent 1")?, + parent0_momentum_form_id: reader.u32("closure momentum 0")?, + parent1_momentum_form_id_or_sentinel: reader.u32("closure momentum 1")?, + amplitude_destination_id: reader.u32("closure destination")?, + exact_factor_id: reader.u32("closure exact factor")?, + component_factor_start: reader.u32("closure component-factor start")?, + component_count: { + let count = reader.u16("closure component count")?; + require_zero(reader.u16("closure reserved")?, "closure reserved")?; + count + }, + selector_domain_id: reader.u32("closure selector domain")?, + flags: reader.u32("closure flags")?, + }); + } + let mut row_groups = Vec::with_capacity(row_group_count); + for _ in 0..row_group_count { + let stage = reader.u16("row-group stage")?; + let role = DirectExecutorRole::try_from(reader.u16("row-group role")?)?; + let destination_operation = + DirectDestinationOperation::try_from(reader.u16("row-group operation")?)?; + require_zero(reader.u16("row-group reserved")?, "row-group reserved")?; + let direct_executor_id = reader.u32("row-group executor")?; + let row_start = reader.u64("row-group row start")?; + let row_count = reader.u32("row-group row count")?; + require_zero( + reader.u32("row-group trailing reserved")?, + "row-group reserved", + )?; + row_groups.push(DirectRowGroupDescriptor { + stage, + role, + destination_operation, + direct_executor_id, + row_start, + row_count, + }); + } + let mut momentum_forms = Vec::with_capacity(momentum_form_count); + for _ in 0..momentum_form_count { + let term_start = reader.u64("momentum-form term start")?; + let term_count = reader.u32("momentum-form term count")?; + require_zero( + reader.u32("momentum-form reserved")?, + "momentum-form reserved", + )?; + momentum_forms.push(DirectMomentumFormDescriptor { + term_start, + term_count, + }); + } + let mut momentum_terms = Vec::with_capacity(momentum_term_count); + for _ in 0..momentum_term_count { + momentum_terms.push(DirectMomentumTerm { + source_slot: reader.u32("momentum-term source")?, + coefficient: reader.i32("momentum-term coefficient")?, + }); + } + let mut selector_domains = Vec::with_capacity(selector_domain_count); + for _ in 0..selector_domain_count { + let word_start = reader.u64("selector-domain word start")?; + let word_count = reader.u32("selector-domain word count")?; + require_zero( + reader.u32("selector-domain reserved")?, + "selector-domain reserved", + )?; + selector_domains.push(DirectSelectorDomainDescriptor { + word_start, + word_count, + }); + } + let mut selector_words = Vec::with_capacity(selector_word_count); + for _ in 0..selector_word_count { + selector_words.push(reader.u64("selector word")?); + } + let mut replay_targets = Vec::with_capacity(replay_target_count); + for _ in 0..replay_target_count { + replay_targets.push(DirectReplayTargetDescriptor { + public_flow_id: reader.u32("replay public flow")?, + representative_id: reader.u32("replay representative")?, + source_permutation_start: reader.u64("replay permutation start")?, + source_permutation_count: reader.u32("replay permutation count")?, + helicity_map_start: reader.u64("replay helicity-map start")?, + helicity_map_count: reader.u32("replay helicity-map count")?, + phase_exact_factor_id: reader.u32("replay phase factor")?, + multiplicity: reader.u32("replay multiplicity")?, + selector_domain_id: reader.u32("replay selector domain")?, + }); + } + let mut source_permutations = Vec::with_capacity(source_permutation_count); + for _ in 0..source_permutation_count { + source_permutations.push(reader.u32("source permutation")?); + } + let mut replay_momentum_signs = Vec::with_capacity(replay_momentum_sign_count); + for _ in 0..replay_momentum_sign_count { + replay_momentum_signs.push(reader.i32("replay momentum sign")?); + } + let mut replay_helicity_map = Vec::with_capacity(replay_helicity_map_count); + for _ in 0..replay_helicity_map_count { + replay_helicity_map.push(reader.u32("replay helicity map")?); + } + let mut amplitude_destinations = Vec::with_capacity(amplitude_destination_descriptor_count); + for _ in 0..amplitude_destination_descriptor_count { + let closure_row_start = reader.u64("amplitude destination closure start")?; + let id = reader.u32("amplitude destination ID")?; + let target_sector_id = reader.u32("amplitude destination sector")?; + let target_helicity_id_or_sentinel = reader.u32("amplitude destination helicity")?; + let closure_row_count = reader.u32("amplitude destination closure count")?; + let selector_domain_id = reader.u32("amplitude destination selector domain")?; + require_zero( + reader.u32("amplitude destination reserved")?, + "amplitude destination reserved", + )?; + amplitude_destinations.push(DirectAmplitudeDestinationDescriptor { + closure_row_start, + id, + target_sector_id, + target_helicity_id_or_sentinel, + closure_row_count, + selector_domain_id, + }); + } + let mut resolved_helicities = Vec::with_capacity(resolved_helicity_count); + for _ in 0..resolved_helicity_count { + resolved_helicities.push(DirectResolvedHelicityDescriptor { + source_state_start: reader.u64("resolved-helicity source-state start")?, + source_selection_start: reader.u64("resolved-helicity source-selection start")?, + public_helicity_start: reader.u64("resolved-helicity public-value start")?, + id: reader.u32("resolved-helicity ID")?, + source_state_count: reader.u32("resolved-helicity source-state count")?, + source_selection_count: reader.u32("resolved-helicity source-selection count")?, + public_helicity_count: reader.u32("resolved-helicity public-value count")?, + selector_domain_id: reader.u32("resolved-helicity selector domain")?, + }); + } + let mut source_state_assignments = Vec::with_capacity(source_state_assignment_count); + for _ in 0..source_state_assignment_count { + source_state_assignments.push(DirectSourceStateAssignment { + source_slot: reader.u32("source-state source slot")?, + state_index: reader.u32("source-state index")?, + }); + } + let mut source_dispatch_variants = Vec::with_capacity(source_dispatch_variant_count); + for _ in 0..source_dispatch_variant_count { + source_dispatch_variants.push(DirectSourceDispatchVariantDescriptor { + embedding_start: reader.u64("source-dispatch embedding start")?, + projection_start: reader.u64("source-dispatch projection start")?, + source_row_id: reader.u32("source-dispatch source row")?, + dispatch_domain_id: reader.u32("source-dispatch domain")?, + runtime_variant_id: reader.u32("source-dispatch runtime variant")?, + source_state_index: reader.u32("source-dispatch source-state index")?, + source_template_id: reader.u32("source-dispatch source template")?, + source_state_template_id: reader.u32("source-dispatch source state")?, + crossed_state_template_id: reader.u32("source-dispatch crossed state")?, + crossed_spin_state_class: reader.i32("source-dispatch crossed spin state")?, + direct_executor_id: reader.u32("source-dispatch direct executor")?, + crossing_exact_factor_id: reader.u32("source-dispatch crossing factor")?, + embedding_count: reader.u32("source-dispatch embedding count")?, + projection_count: reader.u32("source-dispatch projection count")?, + }); + } + let mut source_embeddings = Vec::with_capacity(source_embedding_count); + for _ in 0..source_embedding_count { + source_embeddings.push(DirectSourceEmbeddingRow { + full_component: reader.u32("source embedding full component")?, + source_component_or_sentinel: reader.u32("source embedding source component")?, + exact_factor_id: reader.u32("source embedding factor")?, + }); + } + let mut source_projections = Vec::with_capacity(source_projection_count); + for _ in 0..source_projection_count { + source_projections.push(DirectSourceProjectionRow { + source_component: reader.u32("source projection source component")?, + full_component: reader.u32("source projection full component")?, + }); + } + let mut resolved_source_selections = Vec::with_capacity(resolved_source_selection_count); + for _ in 0..resolved_source_selection_count { + resolved_source_selections.push(DirectResolvedSourceSelection { + source_slot: reader.u32("resolved source-selection slot")?, + dispatch_variant_id: reader.u32("resolved source-selection variant")?, + }); + } + let mut public_helicities = Vec::with_capacity(public_helicity_count); + for _ in 0..public_helicity_count { + public_helicities.push(reader.i32("public helicity")?); + } + let mut exact_factors = Vec::with_capacity(exact_factor_count); + for _ in 0..exact_factor_count { + exact_factors.push(reader.exact("exact factor")?); + } + let mut closure_proof_contributions = Vec::with_capacity(closure_proof_contribution_count); + for _ in 0..closure_proof_contribution_count { + closure_proof_contributions.push(ClosureProofContributionV2::new( + reader.u32("closure proof contribution ID")?, + reader.u32("closure proof target sector")?, + reader.optional_u32("closure proof target destination")?, + reader.optional_u32("closure proof target helicity")?, + reader.u32("closure proof template")?, + reader.digest("closure proof template semantic digest")?, + reader.optional_u32("closure proof quantum flow")?, + reader.u32_vec("closure construction builder-parent IDs")?, + reader.optional_u32_vec("closure construction runtime-parent IDs")?, + reader.digest_vec("closure construction parent semantic digests")?, + reader.digest_vec("closure construction parent color digests")?, + reader.u32_vec("closure construction parent permutation")?, + reader.u32_vec("closure reconstruction parent permutation")?, + reader.u32_vec("closure evaluator parent permutation")?, + reader.u32("closure color witness term")?, + reader.digest("closure color witness proof digest")?, + reader.optional_u32("closure three-line certificate")?, + reader.u32_vec("closure pairing certificates")?, + reader.optional_u32("closure reflection certificate")?, + reader.exact("closure proof contribution factor")?, + reader.u32("closure proof contribution multiplicity")?, + )?); + } + let mut closure_proof_groups = Vec::with_capacity(closure_proof_group_count); + for _ in 0..closure_proof_group_count { + closure_proof_groups.push( + ClosureExecutionProofGroupV2::new_with_candidate_selector_domain( + reader.u32("closure proof group ID")?, + reader.optional_u32("closure proof group runtime term")?, + reader.optional_u32("closure proof group direct row")?, + CheckedTableRange::new( + reader.u64("closure proof group contribution start")?, + reader.u64("closure proof group contribution count")?, + ), + reader.exact("closure proof group summed factor")?, + reader.digest("closure proof group component-factor digest")?, + reader.digest("closure proof group candidate-selector digest")?, + reader.digest("closure proof group selector-domain digest")?, + )?, + ); + } + let mut reflection_certificates = Vec::with_capacity(reflection_certificate_count); + for _ in 0..reflection_certificate_count { + let id = reader.u32("reflection certificate ID")?; + let orbit_identity = reader.digest("reflection orbit identity")?; + let reciprocal_identity = reader.digest("reflection reciprocal identity")?; + let source_permutation = reader.u32_vec("reflection source permutation")?; + let exact_phase = reader.exact("reflection exact phase")?; + let fixed_point = match reader.u32("reflection fixed-point flag")? { + 0 => false, + 1 => true, + value => { + return Err(invalid(format!( + "reflection fixed-point flag must be 0 or 1, found {value}" + ))); + } + }; + reflection_certificates.push(ReflectionCertificateV1::new( + id, + orbit_identity, + reciprocal_identity, + source_permutation, + exact_phase, + fixed_point, + reader.u32("reflection orbit size")?, + reader.u32("reflection proof algorithm")?, + reader.digest("reflection proof digest")?, + )?); + } + let mut three_line_traversal_certificates = + Vec::with_capacity(three_line_traversal_certificate_count); + for _ in 0..three_line_traversal_certificate_count { + let id = reader.u32("three-line traversal certificate ID")?; + let sector_id = reader.u32("three-line traversal sector")?; + let kind = ThreeLineTraversalKindV1::try_from(reader.u32("three-line traversal kind")?) + .map_err(|error| invalid(error.message()))?; + let sink_block_ordinal = reader.u32("three-line traversal sink block")?; + let reference_block_order = reader.u32_vec("three-line reference block order")?; + let witness_block_order = reader.u32_vec("three-line witness block order")?; + let block_permutation = reader.u32_vec("three-line block permutation")?; + let reference_source_order = reader.u32_vec("three-line reference source order")?; + let witness_source_order = reader.u32_vec("three-line witness source order")?; + let source_position_permutation = + reader.u32_vec("three-line source-position permutation")?; + three_line_traversal_certificates.push(ThreeLineTraversalCertificateV1::new( + id, + sector_id, + kind, + sink_block_ordinal, + reference_block_order, + witness_block_order, + block_permutation, + reference_source_order, + witness_source_order, + source_position_permutation, + reader.u32("three-line closure anchor source slot")?, + reader.u32("three-line pairing rule")?, + reader.digest("three-line traversal proof digest")?, + )?); + } + reader.finish()?; + let closure_proofs = + ClosureProofMetadataV2::new_with_three_line_certificates_candidate_domain_and_expected_digest( + closure_proof_contributions, + closure_proof_groups, + reflection_certificates, + three_line_traversal_certificates, + closure_candidate_domain_certificate, + expected_closure_proof_digest, + )?; + + let plan = DirectRecurrencePlan::new(DirectRecurrencePlanParts { + strategy, + semantic_digest, + prepared_pack_digest, + direct_template_catalog_digest, + point_tile_size, + workspace_mib, + current_arena_components, + physical_sector_count, + retained_helicity_count, + amplitude_destination_count, + parameter_value_count, + external_source_count, + state_template_count, + source_template_count, + source_template_or_dispatch_count, + runtime_helicity_contract_count, + runtime_helicity_variant_count, + direct_executor_count, + currents, + sources, + contributions, + finalizations, + closures, + row_groups, + momentum_forms, + momentum_terms, + selector_domains, + selector_words, + replay_targets, + source_permutations, + replay_momentum_signs, + replay_helicity_map, + amplitude_destinations, + resolved_helicities, + source_state_assignments, + source_dispatch_variants, + source_embeddings, + source_projections, + resolved_source_selections, + public_helicities, + exact_factors, + closure_proofs, + })?; + if plan.runtime_layout_digest() != expected_runtime_layout_digest { + return Err(invalid("runtime-layout digest mismatch")); + } + Ok(plan) +} + +fn read_rational(reader: &mut Reader<'_>, label: &str) -> RusticolResult { + let numerator = reader.i128(&format!("{label} numerator"))?; + let denominator = reader.i128(&format!("{label} denominator"))?; + let value = + ExactRational::new(numerator, denominator).map_err(|error| invalid(error.message()))?; + if value.numerator() != numerator || value.denominator() != denominator { + return Err(invalid(format!("{label} is not canonically reduced"))); + } + Ok(value) +} + +fn require_zero(value: T, label: &str) -> RusticolResult<()> +where + T: Default + PartialEq + std::fmt::Display, +{ + if value != T::default() { + return Err(invalid(format!("{label} must be zero, found {value}"))); + } + Ok(()) +} + +#[cfg(test)] +#[path = "direct_codec_tests.rs"] +mod tests; diff --git a/pyamplicol/rust/crates/rusticol-core/src/recurrence/direct_codec_tests.rs b/pyamplicol/rust/crates/rusticol-core/src/recurrence/direct_codec_tests.rs new file mode 100644 index 0000000000..0fdeb55c44 --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/recurrence/direct_codec_tests.rs @@ -0,0 +1,144 @@ +// SPDX-License-Identifier: 0BSD + +use super::*; +use crate::recurrence::direct_plan::tests::valid_plan; +use std::io::{self, Write}; + +#[derive(Default)] +struct CountingWriter { + written: u64, +} + +impl Write for CountingWriter { + fn write(&mut self, bytes: &[u8]) -> io::Result { + self.written += bytes.len() as u64; + Ok(bytes.len()) + } + + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } +} + +#[test] +fn direct_codec_round_trips_deterministically() { + let plan = valid_plan(); + let first = encode_recurrence_direct_plan_v2(&plan).unwrap(); + let second = encode_recurrence_direct_plan_v2(&plan).unwrap(); + assert_eq!(first, second); + let decoded = decode_recurrence_direct_plan_v2(&first).unwrap(); + assert_eq!( + decoded + .closure_proofs() + .three_line_traversal_certificates() + .len(), + 1 + ); + assert_eq!(decoded, plan); +} + +#[test] +fn direct_codec_stream_matches_the_vec_adapter() { + let plan = valid_plan(); + let expected = encode_recurrence_direct_plan_v2(&plan).unwrap(); + let mut streamed = Vec::new(); + let byte_count = encode_recurrence_direct_plan_v2_to_writer(&plan, &mut streamed).unwrap(); + assert_eq!(streamed, expected); + assert_eq!(byte_count, expected.len() as u64); +} + +#[test] +fn direct_codec_rejects_runtime_compacted_plans() { + let complete = valid_plan(); + let runtime_layout_digest = complete.runtime_layout_digest(); + let closure_proof_digest = complete + .closure_proofs() + .expected_semantic_completeness_digest(); + let plan = complete.into_runtime_compacted(); + assert_eq!(plan.runtime_layout_digest(), runtime_layout_digest); + assert_eq!( + plan.closure_proofs() + .expected_semantic_completeness_digest(), + closure_proof_digest + ); + assert!(plan.closure_proofs().contributions().is_empty()); + assert!(plan.closure_proofs().groups().is_empty()); + assert!( + encode_recurrence_direct_plan_v2(&plan) + .unwrap_err() + .to_string() + .contains("runtime-compacted plan cannot be serialized") + ); +} + +#[test] +fn direct_codec_stream_crosses_the_former_eight_gib_boundary_without_allocating() { + const EIGHT_GIB: u64 = 8 * 1024 * 1024 * 1024; + let mut writer = Writer::with_bytes_written(CountingWriter::default(), EIGHT_GIB - 1); + writer.raw(&[1, 2]).unwrap(); + assert_eq!(writer.bytes_written, EIGHT_GIB + 1); + assert_eq!(writer.destination.written, 2); +} + +#[test] +fn direct_codec_stream_rejects_only_true_u64_length_overflow() { + let mut writer = Writer::with_bytes_written(CountingWriter::default(), u64::MAX); + let error = writer.raw(&[1]).unwrap_err(); + assert!(error.to_string().contains("payload length overflows u64")); + assert_eq!(writer.destination.written, 0); +} + +#[test] +fn direct_codec_rejects_every_tested_truncation_boundary() { + let bytes = encode_recurrence_direct_plan_v2(&valid_plan()).unwrap(); + for length in [0, 1, 7, 8, 15, 64, bytes.len() / 2, bytes.len() - 1] { + let error = decode_recurrence_direct_plan_v2(&bytes[..length]).unwrap_err(); + assert!( + error.to_string().contains("truncated") + || error.to_string().contains("unsupported recurrence") + || error.to_string().contains("cannot fit"), + "unexpected error at truncation {length}: {error}" + ); + } +} + +#[test] +fn direct_codec_rejects_old_v1_magic_explicitly() { + let mut bytes = encode_recurrence_direct_plan_v2(&valid_plan()).unwrap(); + bytes[..8].copy_from_slice(b"PACRPLAN"); + let error = decode_recurrence_direct_plan_v2(&bytes).unwrap_err(); + assert!(error.to_string().contains("regenerate with direct-plan v2")); +} + +#[test] +fn direct_codec_detects_a_valid_field_mutation_through_layout_digest() { + let mut bytes = encode_recurrence_direct_plan_v2(&valid_plan()).unwrap(); + // Header offset 20 is point_tile_size. Keep it nonzero and structurally + // valid so the authenticated layout digest is the rejecting invariant. + bytes[20..24].copy_from_slice(&128_u32.to_le_bytes()); + let error = decode_recurrence_direct_plan_v2(&bytes).unwrap_err(); + assert!(error.to_string().contains("runtime-layout digest mismatch")); +} + +#[test] +fn direct_codec_rejects_nonzero_reserved_fields() { + let mut bytes = encode_recurrence_direct_plan_v2(&valid_plan()).unwrap(); + bytes[12..16].copy_from_slice(&1_u32.to_le_bytes()); + let error = decode_recurrence_direct_plan_v2(&bytes).unwrap_err(); + assert!(error.to_string().contains("header flags")); +} + +#[test] +fn direct_codec_authenticates_three_line_traversal_payloads() { + let mut bytes = encode_recurrence_direct_plan_v2(&valid_plan()).unwrap(); + let last = bytes + .last_mut() + .expect("encoded three-line proof payload is nonempty"); + *last ^= 1; + let error = decode_recurrence_direct_plan_v2(&bytes).unwrap_err(); + assert!( + error + .to_string() + .contains("three-line traversal proof digest mismatch") + ); +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/recurrence/direct_helicity_dispatch.rs b/pyamplicol/rust/crates/rusticol-core/src/recurrence/direct_helicity_dispatch.rs new file mode 100644 index 0000000000..6c0e6888f5 --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/recurrence/direct_helicity_dispatch.rs @@ -0,0 +1,1104 @@ +// SPDX-License-Identifier: 0BSD + +//! Authenticated helicity dispatch for one persisted all-flow direct plan. +//! +//! The direct-plan-v2 tables remain the canonical physical-color schedule. +//! This sidecar partitions their non-source rows into maximal +//! uniform-helicity runs and stores one canonical run-ID list per resolved +//! helicity. A cold selector bind can therefore copy only its active rows, +//! rederive destination initialization, and build schedule-local fanout and +//! interaction programs. Warm execution never consults this metadata. + +use std::collections::BTreeSet; +use std::io::{BufWriter, Write}; +#[cfg(unix)] +use std::os::unix::fs::PermissionsExt; +use std::path::Path; + +use super::direct_plan::{DIRECT_NONE_U32, DirectExecutorRole, DirectRecurrencePlan}; +use super::{RecurrenceStrategy, SemanticDigest}; +use crate::pacbin::{PACBIN_DEFAULT_CHUNK_SIZE, create_temporary_file}; +use crate::{RusticolError, RusticolResult}; +use sha2::{Digest, Sha256}; + +pub const RECURRENCE_HELICITY_DISPATCH_ABI: &str = "pyamplicol-recurrence-helicity-dispatch-v1"; + +const MAGIC: &[u8; 8] = b"PACRHDS1"; +const VERSION: u32 = 1; +const MAX_TABLE_ROWS: u64 = u32::MAX as u64; + +fn invalid(message: impl Into) -> RusticolError { + RusticolError::invalid_argument(format!("recurrence helicity dispatch: {}", message.into())) +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[repr(C)] +pub struct DirectHelicitySupportDomainDescriptor { + pub word_start: u64, + pub word_count: u32, +} + +/// One contiguous base-plan row run with a uniform helicity support mask. +/// +/// Source rows are deliberately absent. The all-flow source dispatcher +/// already consumes the exact per-helicity source-selection table. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[repr(C)] +pub struct DirectHelicityRowGroupDescriptor { + pub stage: u16, + pub role: DirectExecutorRole, + pub direct_executor_id: u32, + pub row_start: u64, + pub row_count: u32, + pub support_domain_id: u32, +} + +/// Canonical range into [`DirectHelicityDispatch::dispatch_group_ids`]. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[repr(C)] +pub struct DirectHelicityDispatchDescriptor { + pub resolved_helicity_id: u32, + pub group_id_start: u64, + pub group_id_count: u32, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct DirectHelicityDispatchParts { + pub runtime_layout_digest: SemanticDigest, + pub source_row_count: u64, + pub contribution_row_count: u64, + pub finalization_row_count: u64, + pub closure_row_count: u64, + pub amplitude_destination_count: u32, + pub direct_executor_count: u32, + pub resolved_helicity_count: u32, + pub support_domains: Vec, + pub support_words: Vec, + pub row_groups: Vec, + pub dispatches: Vec, + pub dispatch_group_ids: Vec, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct DirectHelicityDispatch { + parts: DirectHelicityDispatchParts, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct RecurrenceHelicityDispatchMetadata { + pub size_bytes: u64, + pub sha256: [u8; 32], +} + +impl DirectHelicityDispatch { + pub fn new(parts: DirectHelicityDispatchParts) -> RusticolResult { + validate_parts(&parts)?; + Ok(Self { parts }) + } + + pub const fn runtime_layout_digest(&self) -> SemanticDigest { + self.parts.runtime_layout_digest + } + + pub const fn source_row_count(&self) -> u64 { + self.parts.source_row_count + } + + pub const fn contribution_row_count(&self) -> u64 { + self.parts.contribution_row_count + } + + pub const fn finalization_row_count(&self) -> u64 { + self.parts.finalization_row_count + } + + pub const fn closure_row_count(&self) -> u64 { + self.parts.closure_row_count + } + + pub const fn amplitude_destination_count(&self) -> u32 { + self.parts.amplitude_destination_count + } + + pub const fn direct_executor_count(&self) -> u32 { + self.parts.direct_executor_count + } + + pub const fn resolved_helicity_count(&self) -> u32 { + self.parts.resolved_helicity_count + } + + pub fn support_domains(&self) -> &[DirectHelicitySupportDomainDescriptor] { + &self.parts.support_domains + } + + pub fn support_words(&self) -> &[u64] { + &self.parts.support_words + } + + pub fn row_groups(&self) -> &[DirectHelicityRowGroupDescriptor] { + &self.parts.row_groups + } + + pub fn dispatches(&self) -> &[DirectHelicityDispatchDescriptor] { + &self.parts.dispatches + } + + pub fn dispatch_group_ids(&self) -> &[u32] { + &self.parts.dispatch_group_ids + } + + pub fn group_ids_for_helicity(&self, resolved_helicity_id: u32) -> RusticolResult<&[u32]> { + let descriptor = self + .parts + .dispatches + .get(resolved_helicity_id as usize) + .filter(|descriptor| descriptor.resolved_helicity_id == resolved_helicity_id) + .ok_or_else(|| { + invalid(format!( + "resolved helicity {resolved_helicity_id} is absent" + )) + })?; + let start = usize::try_from(descriptor.group_id_start) + .map_err(|_| invalid("dispatch group-ID start exceeds usize"))?; + let end = start + .checked_add(descriptor.group_id_count as usize) + .ok_or_else(|| invalid("dispatch group-ID range overflows usize"))?; + self.parts + .dispatch_group_ids + .get(start..end) + .ok_or_else(|| invalid("dispatch group-ID range is out of bounds")) + } + + pub fn support_domain_contains( + &self, + support_domain_id: u32, + resolved_helicity_id: u32, + ) -> RusticolResult { + support_domain_contains_parts(&self.parts, support_domain_id, resolved_helicity_id) + } + + /// Pair the sidecar with its exact all-flow direct plan. + /// + /// Container/member SHA-256 authenticates the bytes. This check establishes + /// the semantic pairing once, at the authoritative load boundary. + pub fn validate_for_plan(&self, plan: &DirectRecurrencePlan) -> RusticolResult<()> { + if plan.strategy() != RecurrenceStrategy::AllFlowUnion { + return Err(invalid( + "helicity dispatch requires an all-flow-union direct plan", + )); + } + if self.runtime_layout_digest() != plan.runtime_layout_digest() { + return Err(RusticolError::integrity( + "recurrence helicity dispatch runtime-layout digest does not match its plan", + )); + } + let expected_counts = [ + u64::try_from(plan.sources().len()) + .map_err(|_| invalid("plan source-row count exceeds u64"))?, + u64::try_from(plan.contributions().len()) + .map_err(|_| invalid("plan contribution-row count exceeds u64"))?, + u64::try_from(plan.finalizations().len()) + .map_err(|_| invalid("plan finalization-row count exceeds u64"))?, + u64::try_from(plan.closures().len()) + .map_err(|_| invalid("plan closure-row count exceeds u64"))?, + ]; + let actual_counts = [ + self.source_row_count(), + self.contribution_row_count(), + self.finalization_row_count(), + self.closure_row_count(), + ]; + if actual_counts != expected_counts + || self.amplitude_destination_count() != plan.amplitude_destination_count() + || self.direct_executor_count() != plan.direct_executor_count() + || self.resolved_helicity_count() as usize != plan.resolved_helicities().len() + || u64::from(self.resolved_helicity_count()) != plan.retained_helicity_count() + { + return Err(RusticolError::integrity( + "recurrence helicity dispatch dimensions do not match its plan", + )); + } + validate_base_row_group_partition(plan, &self.parts.row_groups) + } + + pub fn into_parts(self) -> DirectHelicityDispatchParts { + self.parts + } +} + +fn checked_u32_table_count(label: &str, value: u64) -> RusticolResult<()> { + if value > MAX_TABLE_ROWS { + return Err(invalid(format!("{label} count exceeds the u32 ID domain"))); + } + Ok(()) +} + +fn support_domain_words( + parts: &DirectHelicityDispatchParts, + support_domain_id: u32, +) -> RusticolResult<&[u64]> { + let descriptor = parts + .support_domains + .get(support_domain_id as usize) + .ok_or_else(|| invalid("support-domain ID is out of bounds"))?; + let start = usize::try_from(descriptor.word_start) + .map_err(|_| invalid("support-domain word start exceeds usize"))?; + let end = start + .checked_add(descriptor.word_count as usize) + .ok_or_else(|| invalid("support-domain word range overflows usize"))?; + parts + .support_words + .get(start..end) + .ok_or_else(|| invalid("support-domain word range is out of bounds")) +} + +fn support_domain_contains_parts( + parts: &DirectHelicityDispatchParts, + support_domain_id: u32, + resolved_helicity_id: u32, +) -> RusticolResult { + if resolved_helicity_id >= parts.resolved_helicity_count { + return Err(invalid("resolved helicity is out of bounds")); + } + let words = support_domain_words(parts, support_domain_id)?; + let word = resolved_helicity_id as usize / 64; + let bit = resolved_helicity_id % 64; + Ok(words + .get(word) + .is_some_and(|value| value & (1_u64 << bit) != 0)) +} + +fn validate_parts(parts: &DirectHelicityDispatchParts) -> RusticolResult<()> { + for (label, count) in [ + ("source row", parts.source_row_count), + ("contribution row", parts.contribution_row_count), + ("finalization row", parts.finalization_row_count), + ("closure row", parts.closure_row_count), + ("support domain", parts.support_domains.len() as u64), + ("support word", parts.support_words.len() as u64), + ("split row group", parts.row_groups.len() as u64), + ("dispatch group ID", parts.dispatch_group_ids.len() as u64), + ] { + checked_u32_table_count(label, count)?; + } + if parts.source_row_count == 0 + || parts.contribution_row_count == 0 + || parts.closure_row_count == 0 + || parts.amplitude_destination_count == 0 + || parts.direct_executor_count == 0 + || parts.resolved_helicity_count == 0 + || parts.support_domains.is_empty() + || parts.row_groups.is_empty() + { + return Err(invalid( + "dispatch requires nonempty source, contribution, closure, destination, executor, helicity, support-domain, and row-group tables", + )); + } + if parts.dispatches.len() != parts.resolved_helicity_count as usize { + return Err(invalid( + "dispatch descriptor table does not cover every resolved helicity", + )); + } + + validate_support_domains(parts)?; + validate_split_row_groups(parts)?; + validate_dispatch_csr(parts) +} + +fn validate_support_domains(parts: &DirectHelicityDispatchParts) -> RusticolResult<()> { + let maximum_words = parts.resolved_helicity_count.div_ceil(64) as usize; + let mut next = 0_u64; + let mut unique = BTreeSet::>::new(); + for (domain_id, descriptor) in parts.support_domains.iter().enumerate() { + if descriptor.word_start != next { + return Err(invalid(format!( + "support domain {domain_id} does not continue the packed word table" + ))); + } + let words = support_domain_words(parts, domain_id as u32)?; + if words.len() > maximum_words || words.last() == Some(&0) { + return Err(invalid(format!( + "support domain {domain_id} is not a canonical compact helicity mask" + ))); + } + if words.len() == maximum_words && !parts.resolved_helicity_count.is_multiple_of(64) { + let live_bits = parts.resolved_helicity_count % 64; + let high_mask = !((1_u64 << live_bits) - 1); + if words.last().is_some_and(|word| word & high_mask != 0) { + return Err(invalid(format!( + "support domain {domain_id} sets bits outside the resolved-helicity axis" + ))); + } + } + if !unique.insert(words.to_vec()) { + return Err(invalid(format!( + "support domain {domain_id} duplicates an earlier interned mask" + ))); + } + next = next + .checked_add(u64::from(descriptor.word_count)) + .ok_or_else(|| invalid("support-domain word partition overflows u64"))?; + } + if next != parts.support_words.len() as u64 { + return Err(invalid( + "support domains do not partition the support-word table", + )); + } + Ok(()) +} + +fn role_partition_index(role: DirectExecutorRole) -> RusticolResult { + match role { + DirectExecutorRole::Contribution => Ok(0), + DirectExecutorRole::Finalization => Ok(1), + DirectExecutorRole::Closure => Ok(2), + DirectExecutorRole::Source => Err(invalid( + "static source groups must not appear in helicity dispatch", + )), + } +} + +fn validate_split_row_groups(parts: &DirectHelicityDispatchParts) -> RusticolResult<()> { + let mut next = [0_u64; 3]; + let mut previous_order = None; + for (group_id, group) in parts.row_groups.iter().enumerate() { + if group.row_count == 0 { + return Err(invalid(format!("split row group {group_id} is empty"))); + } + let role_index = role_partition_index(group.role)?; + if group.direct_executor_id != DIRECT_NONE_U32 + && group.direct_executor_id >= parts.direct_executor_count + { + return Err(invalid(format!( + "split row group {group_id} executor is out of bounds" + ))); + } + if group.direct_executor_id == DIRECT_NONE_U32 + && group.role != DirectExecutorRole::Contribution + { + return Err(invalid(format!( + "split row group {group_id} has a missing non-contribution executor" + ))); + } + if group.support_domain_id as usize >= parts.support_domains.len() { + return Err(invalid(format!( + "split row group {group_id} support domain is out of bounds" + ))); + } + let order = (group.stage, group.role); + if previous_order.is_some_and(|previous| order < previous) { + return Err(invalid( + "split row groups are not in canonical schedule order", + )); + } + previous_order = Some(order); + if group.row_start != next[role_index] { + return Err(invalid(format!( + "split row group {group_id} does not continue its role's row table" + ))); + } + next[role_index] = group + .row_start + .checked_add(u64::from(group.row_count)) + .ok_or_else(|| invalid("split row-group range overflows u64"))?; + } + let expected = [ + parts.contribution_row_count, + parts.finalization_row_count, + parts.closure_row_count, + ]; + if next != expected { + return Err(invalid( + "split row groups do not partition every non-source base-plan row", + )); + } + Ok(()) +} + +fn validate_dispatch_csr(parts: &DirectHelicityDispatchParts) -> RusticolResult<()> { + let mut next = 0_u64; + let mut starts = Vec::with_capacity(parts.dispatches.len()); + let mut ends = Vec::with_capacity(parts.dispatches.len()); + for (helicity_id, descriptor) in parts.dispatches.iter().enumerate() { + if descriptor.resolved_helicity_id != helicity_id as u32 + || descriptor.group_id_start != next + { + return Err(invalid(format!( + "resolved-helicity dispatch {helicity_id} is not canonical" + ))); + } + let start = usize::try_from(descriptor.group_id_start) + .map_err(|_| invalid("dispatch group-ID start exceeds usize"))?; + let end = start + .checked_add(descriptor.group_id_count as usize) + .ok_or_else(|| invalid("dispatch group-ID range overflows usize"))?; + let group_ids = parts + .dispatch_group_ids + .get(start..end) + .ok_or_else(|| invalid("dispatch group-ID range is out of bounds"))?; + if group_ids + .iter() + .any(|group_id| *group_id as usize >= parts.row_groups.len()) + || !group_ids.windows(2).all(|pair| pair[0] < pair[1]) + { + return Err(invalid(format!( + "resolved-helicity dispatch {helicity_id} has invalid or unordered group IDs" + ))); + } + starts.push(start); + ends.push(end); + next = next + .checked_add(u64::from(descriptor.group_id_count)) + .ok_or_else(|| invalid("dispatch group-ID partition overflows u64"))?; + } + if next != parts.dispatch_group_ids.len() as u64 { + return Err(invalid( + "resolved-helicity dispatches do not partition the group-ID table", + )); + } + + // The masks are the audit representation and the CSR is the cold-bind + // accelerator. Establish their exact equivalence once without allocating + // an H-by-group matrix. + let mut cursors = starts; + for (group_id, group) in parts.row_groups.iter().enumerate() { + for (word_index, mut word) in support_domain_words(parts, group.support_domain_id)? + .iter() + .copied() + .enumerate() + { + while word != 0 { + let bit = word.trailing_zeros() as usize; + let helicity_id = word_index * 64 + bit; + let cursor = cursors + .get_mut(helicity_id) + .ok_or_else(|| invalid("support mask exceeds the helicity axis"))?; + let actual = parts.dispatch_group_ids.get(*cursor).copied(); + if actual != Some(group_id as u32) { + return Err(invalid(format!( + "resolved-helicity dispatch {helicity_id} disagrees with support masks at group {group_id}" + ))); + } + *cursor += 1; + word &= word - 1; + } + } + } + if cursors != ends { + return Err(invalid( + "resolved-helicity dispatch contains groups absent from its support masks", + )); + } + Ok(()) +} + +fn validate_base_row_group_partition( + plan: &DirectRecurrencePlan, + groups: &[DirectHelicityRowGroupDescriptor], +) -> RusticolResult<()> { + let mut group_index = 0usize; + for base in plan + .row_groups() + .iter() + .filter(|descriptor| descriptor.role != DirectExecutorRole::Source) + { + let base_end = base + .row_start + .checked_add(u64::from(base.row_count)) + .ok_or_else(|| invalid("base row-group range overflows u64"))?; + let mut next = base.row_start; + while next < base_end { + let group = groups.get(group_index).ok_or_else(|| { + RusticolError::integrity( + "recurrence helicity dispatch omits a base-plan row-group suffix", + ) + })?; + if group.stage != base.stage + || group.role != base.role + || group.direct_executor_id != base.direct_executor_id + || group.row_start != next + { + return Err(RusticolError::integrity( + "recurrence helicity dispatch split group does not match its base row group", + )); + } + next = next + .checked_add(u64::from(group.row_count)) + .ok_or_else(|| invalid("split row-group range overflows u64"))?; + if next > base_end { + return Err(RusticolError::integrity( + "recurrence helicity dispatch split group crosses a base row-group boundary", + )); + } + group_index += 1; + } + } + if group_index != groups.len() { + return Err(RusticolError::integrity( + "recurrence helicity dispatch has groups beyond the base-plan schedule", + )); + } + Ok(()) +} + +struct Writer { + destination: W, + bytes_written: u64, +} + +impl Writer { + fn new(destination: W) -> Self { + Self { + destination, + bytes_written: 0, + } + } + + fn raw(&mut self, bytes: &[u8]) -> RusticolResult<()> { + let count = + u64::try_from(bytes.len()).map_err(|_| invalid("payload write length exceeds u64"))?; + self.bytes_written = self + .bytes_written + .checked_add(count) + .ok_or_else(|| invalid("payload length overflows u64"))?; + self.destination.write_all(bytes).map_err(|error| { + RusticolError::serialization(format!( + "could not stream recurrence helicity dispatch: {error}" + )) + }) + } + + fn u16(&mut self, value: u16) -> RusticolResult<()> { + self.raw(&value.to_le_bytes()) + } + + fn u32(&mut self, value: u32) -> RusticolResult<()> { + self.raw(&value.to_le_bytes()) + } + + fn u64(&mut self, value: u64) -> RusticolResult<()> { + self.raw(&value.to_le_bytes()) + } + + fn digest(&mut self, value: SemanticDigest) -> RusticolResult<()> { + self.raw(value.as_bytes()) + } +} + +pub fn encode_recurrence_helicity_dispatch_v1( + dispatch: &DirectHelicityDispatch, +) -> RusticolResult> { + let mut payload = Vec::new(); + encode_recurrence_helicity_dispatch_v1_to_writer(dispatch, &mut payload)?; + Ok(payload) +} + +pub(crate) fn encode_recurrence_helicity_dispatch_v1_to_writer( + dispatch: &DirectHelicityDispatch, + destination: W, +) -> RusticolResult { + let parts = &dispatch.parts; + let mut writer = Writer::new(destination); + writer.raw(MAGIC)?; + writer.u32(VERSION)?; + writer.u32(0)?; + writer.digest(parts.runtime_layout_digest)?; + writer.u64(parts.source_row_count)?; + writer.u64(parts.contribution_row_count)?; + writer.u64(parts.finalization_row_count)?; + writer.u64(parts.closure_row_count)?; + writer.u32(parts.amplitude_destination_count)?; + writer.u32(parts.direct_executor_count)?; + writer.u32(parts.resolved_helicity_count)?; + writer.u32(parts.support_domains.len() as u32)?; + writer.u32(parts.row_groups.len() as u32)?; + writer.u32(parts.dispatches.len() as u32)?; + writer.u64(parts.support_words.len() as u64)?; + writer.u64(parts.dispatch_group_ids.len() as u64)?; + + for descriptor in &parts.support_domains { + writer.u64(descriptor.word_start)?; + writer.u32(descriptor.word_count)?; + writer.u32(0)?; + } + for word in &parts.support_words { + writer.u64(*word)?; + } + for group in &parts.row_groups { + writer.u16(group.stage)?; + writer.u16(group.role as u16)?; + writer.u32(group.direct_executor_id)?; + writer.u64(group.row_start)?; + writer.u32(group.row_count)?; + writer.u32(group.support_domain_id)?; + } + for descriptor in &parts.dispatches { + writer.u32(descriptor.resolved_helicity_id)?; + writer.u32(0)?; + writer.u64(descriptor.group_id_start)?; + writer.u32(descriptor.group_id_count)?; + writer.u32(0)?; + } + for group_id in &parts.dispatch_group_ids { + writer.u32(*group_id)?; + } + Ok(writer.bytes_written) +} + +struct HashingWriter { + inner: W, + digest: Sha256, +} + +impl HashingWriter { + fn new(inner: W) -> Self { + Self { + inner, + digest: Sha256::new(), + } + } +} + +impl Write for HashingWriter { + fn write(&mut self, bytes: &[u8]) -> std::io::Result { + let written = self.inner.write(bytes)?; + self.digest.update(&bytes[..written]); + Ok(written) + } + + fn flush(&mut self) -> std::io::Result<()> { + self.inner.flush() + } +} + +/// Stream one dispatch sidecar to a same-directory temporary file and publish +/// it atomically. The returned digest is computed over the bytes being written, +/// so callers need no second read or hashing pass. +pub fn write_recurrence_helicity_dispatch_v1_atomic( + destination: impl AsRef, + dispatch: &DirectHelicityDispatch, +) -> RusticolResult { + let destination = destination.as_ref(); + let parent = destination + .parent() + .filter(|path| !path.as_os_str().is_empty()) + .unwrap_or_else(|| Path::new(".")); + if !parent.is_dir() { + return Err(invalid(format!( + "destination directory does not exist: {}", + parent.display() + ))); + } + let (temporary_path, temporary_file) = create_temporary_file(destination, parent)?; + let result = (|| { + let buffered = BufWriter::with_capacity(PACBIN_DEFAULT_CHUNK_SIZE, temporary_file); + let mut writer = HashingWriter::new(buffered); + let size_bytes = encode_recurrence_helicity_dispatch_v1_to_writer(dispatch, &mut writer)?; + writer.flush().map_err(|error| { + RusticolError::artifact(format!( + "could not flush recurrence helicity dispatch {}: {error}", + temporary_path.display() + )) + })?; + #[cfg(unix)] + writer + .inner + .get_ref() + .set_permissions(std::fs::Permissions::from_mode(0o644)) + .map_err(|error| { + RusticolError::artifact(format!( + "could not set recurrence helicity dispatch permissions {}: {error}", + temporary_path.display() + )) + })?; + writer.inner.get_ref().sync_all().map_err(|error| { + RusticolError::artifact(format!( + "could not sync recurrence helicity dispatch {}: {error}", + temporary_path.display() + )) + })?; + let HashingWriter { inner, digest } = writer; + let sha256: [u8; 32] = digest.finalize().into(); + drop(inner); + std::fs::rename(&temporary_path, destination).map_err(|error| { + RusticolError::artifact(format!( + "could not atomically publish recurrence helicity dispatch {}: {error}", + destination.display() + )) + })?; + // The containing artifact root is content-addressed. Best-effort + // directory sync mirrors PACBIN publication without adding another + // platform-specific durability contract. + if let Ok(directory) = std::fs::File::open(parent) { + let _ = directory.sync_all(); + } + Ok(RecurrenceHelicityDispatchMetadata { size_bytes, sha256 }) + })(); + if result.is_err() { + let _ = std::fs::remove_file(&temporary_path); + } + result +} + +struct Reader<'a> { + bytes: &'a [u8], + offset: usize, +} + +impl<'a> Reader<'a> { + fn new(bytes: &'a [u8]) -> Self { + Self { bytes, offset: 0 } + } + + fn take(&mut self, count: usize, label: &str) -> RusticolResult<&'a [u8]> { + let end = self + .offset + .checked_add(count) + .ok_or_else(|| invalid(format!("{label} offset overflows usize")))?; + let bytes = self.bytes.get(self.offset..end).ok_or_else(|| { + invalid(format!( + "truncated {label} at byte {}: need {count}, have {}", + self.offset, + self.bytes.len().saturating_sub(self.offset) + )) + })?; + self.offset = end; + Ok(bytes) + } + + fn u16(&mut self, label: &str) -> RusticolResult { + Ok(u16::from_le_bytes( + self.take(2, label)?.try_into().expect("checked read"), + )) + } + + fn u32(&mut self, label: &str) -> RusticolResult { + Ok(u32::from_le_bytes( + self.take(4, label)?.try_into().expect("checked read"), + )) + } + + fn u64(&mut self, label: &str) -> RusticolResult { + Ok(u64::from_le_bytes( + self.take(8, label)?.try_into().expect("checked read"), + )) + } + + fn digest(&mut self, label: &str) -> RusticolResult { + let bytes = self.take(32, label)?.try_into().expect("checked read"); + SemanticDigest::new(bytes).map_err(|error| invalid(error.message())) + } + + fn reserved_u32(&mut self, label: &str) -> RusticolResult<()> { + let value = self.u32(label)?; + if value != 0 { + return Err(invalid(format!("{label} must be zero, found {value}"))); + } + Ok(()) + } + + fn checked_count(&self, value: u64, row_bytes: usize, label: &str) -> RusticolResult { + checked_u32_table_count(label, value)?; + let count = + usize::try_from(value).map_err(|_| invalid(format!("{label} count exceeds usize")))?; + if count > self.bytes.len().saturating_sub(self.offset) / row_bytes { + return Err(invalid(format!( + "{label} count cannot fit in the remaining payload" + ))); + } + Ok(count) + } + + fn finish(self) -> RusticolResult<()> { + if self.offset != self.bytes.len() { + return Err(invalid(format!( + "payload contains {} trailing bytes", + self.bytes.len() - self.offset + ))); + } + Ok(()) + } +} + +pub fn decode_recurrence_helicity_dispatch_v1( + bytes: &[u8], +) -> RusticolResult { + let mut reader = Reader::new(bytes); + if reader.take(MAGIC.len(), "magic")? != MAGIC { + return Err(invalid("payload has the wrong magic")); + } + if reader.u32("version")? != VERSION { + return Err(invalid("payload has an unsupported version")); + } + reader.reserved_u32("header reserved")?; + let runtime_layout_digest = reader.digest("runtime-layout digest")?; + let source_row_count = reader.u64("source-row count")?; + let contribution_row_count = reader.u64("contribution-row count")?; + let finalization_row_count = reader.u64("finalization-row count")?; + let closure_row_count = reader.u64("closure-row count")?; + let amplitude_destination_count = reader.u32("amplitude-destination count")?; + let direct_executor_count = reader.u32("direct-executor count")?; + let resolved_helicity_count = reader.u32("resolved-helicity count")?; + let support_domain_count = reader.u32("support-domain count")? as usize; + let row_group_count = reader.u32("split row-group count")? as usize; + let dispatch_count = reader.u32("dispatch count")? as usize; + let support_word_count = reader.u64("support-word count")?; + let dispatch_group_id_count = reader.u64("dispatch group-ID count")?; + + reader.checked_count(support_domain_count as u64, 16, "support domain")?; + let mut support_domains = Vec::with_capacity(support_domain_count); + for _ in 0..support_domain_count { + let word_start = reader.u64("support-domain word start")?; + let word_count = reader.u32("support-domain word count")?; + reader.reserved_u32("support-domain reserved")?; + support_domains.push(DirectHelicitySupportDomainDescriptor { + word_start, + word_count, + }); + } + let support_word_count = reader.checked_count(support_word_count, 8, "support word")?; + let mut support_words = Vec::with_capacity(support_word_count); + for _ in 0..support_word_count { + support_words.push(reader.u64("support word")?); + } + reader.checked_count(row_group_count as u64, 24, "split row group")?; + let mut row_groups = Vec::with_capacity(row_group_count); + for _ in 0..row_group_count { + row_groups.push(DirectHelicityRowGroupDescriptor { + stage: reader.u16("split row-group stage")?, + role: DirectExecutorRole::try_from(reader.u16("split row-group role")?)?, + direct_executor_id: reader.u32("split row-group executor")?, + row_start: reader.u64("split row-group row start")?, + row_count: reader.u32("split row-group row count")?, + support_domain_id: reader.u32("split row-group support domain")?, + }); + } + reader.checked_count(dispatch_count as u64, 24, "dispatch")?; + let mut dispatches = Vec::with_capacity(dispatch_count); + for _ in 0..dispatch_count { + let resolved_helicity_id = reader.u32("dispatch helicity ID")?; + reader.reserved_u32("dispatch leading reserved")?; + let group_id_start = reader.u64("dispatch group-ID start")?; + let group_id_count = reader.u32("dispatch group-ID count")?; + reader.reserved_u32("dispatch trailing reserved")?; + dispatches.push(DirectHelicityDispatchDescriptor { + resolved_helicity_id, + group_id_start, + group_id_count, + }); + } + let dispatch_group_id_count = + reader.checked_count(dispatch_group_id_count, 4, "dispatch group ID")?; + let mut dispatch_group_ids = Vec::with_capacity(dispatch_group_id_count); + for _ in 0..dispatch_group_id_count { + dispatch_group_ids.push(reader.u32("dispatch group ID")?); + } + reader.finish()?; + + DirectHelicityDispatch::new(DirectHelicityDispatchParts { + runtime_layout_digest, + source_row_count, + contribution_row_count, + finalization_row_count, + closure_row_count, + amplitude_destination_count, + direct_executor_count, + resolved_helicity_count, + support_domains, + support_words, + row_groups, + dispatches, + dispatch_group_ids, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn digest(byte: u8) -> SemanticDigest { + SemanticDigest::new([byte; 32]).unwrap() + } + + fn valid_parts() -> DirectHelicityDispatchParts { + DirectHelicityDispatchParts { + runtime_layout_digest: digest(0x22), + source_row_count: 2, + contribution_row_count: 4, + finalization_row_count: 2, + closure_row_count: 2, + amplitude_destination_count: 2, + direct_executor_count: 6, + resolved_helicity_count: 3, + // Domain 0 is exact zero, 1 is H={0,2}, 2 is H={1}, and 3 is universal. + support_domains: vec![ + DirectHelicitySupportDomainDescriptor { + word_start: 0, + word_count: 0, + }, + DirectHelicitySupportDomainDescriptor { + word_start: 0, + word_count: 1, + }, + DirectHelicitySupportDomainDescriptor { + word_start: 1, + word_count: 1, + }, + DirectHelicitySupportDomainDescriptor { + word_start: 2, + word_count: 1, + }, + ], + support_words: vec![0b101, 0b010, 0b111], + row_groups: vec![ + DirectHelicityRowGroupDescriptor { + stage: 1, + role: DirectExecutorRole::Contribution, + direct_executor_id: 1, + row_start: 0, + row_count: 2, + support_domain_id: 1, + }, + DirectHelicityRowGroupDescriptor { + stage: 1, + role: DirectExecutorRole::Contribution, + direct_executor_id: 1, + row_start: 2, + row_count: 1, + support_domain_id: 2, + }, + DirectHelicityRowGroupDescriptor { + stage: 1, + role: DirectExecutorRole::Contribution, + direct_executor_id: 2, + row_start: 3, + row_count: 1, + support_domain_id: 3, + }, + DirectHelicityRowGroupDescriptor { + stage: 1, + role: DirectExecutorRole::Finalization, + direct_executor_id: 3, + row_start: 0, + row_count: 2, + support_domain_id: 3, + }, + DirectHelicityRowGroupDescriptor { + stage: 1, + role: DirectExecutorRole::Closure, + direct_executor_id: 4, + row_start: 0, + row_count: 1, + support_domain_id: 1, + }, + DirectHelicityRowGroupDescriptor { + stage: 1, + role: DirectExecutorRole::Closure, + direct_executor_id: 4, + row_start: 1, + row_count: 1, + support_domain_id: 2, + }, + ], + dispatches: vec![ + DirectHelicityDispatchDescriptor { + resolved_helicity_id: 0, + group_id_start: 0, + group_id_count: 4, + }, + DirectHelicityDispatchDescriptor { + resolved_helicity_id: 1, + group_id_start: 4, + group_id_count: 4, + }, + DirectHelicityDispatchDescriptor { + resolved_helicity_id: 2, + group_id_start: 8, + group_id_count: 4, + }, + ], + dispatch_group_ids: vec![0, 2, 3, 4, 1, 2, 3, 5, 0, 2, 3, 4], + } + } + + #[test] + fn dispatch_codec_round_trips_canonical_support_and_csr() { + let dispatch = DirectHelicityDispatch::new(valid_parts()).unwrap(); + let bytes = encode_recurrence_helicity_dispatch_v1(&dispatch).unwrap(); + let decoded = decode_recurrence_helicity_dispatch_v1(&bytes).unwrap(); + assert_eq!(decoded, dispatch); + assert_eq!(decoded.group_ids_for_helicity(1).unwrap(), &[1, 2, 3, 5]); + assert!(decoded.support_domain_contains(1, 2).unwrap()); + assert!(!decoded.support_domain_contains(1, 1).unwrap()); + assert_eq!( + encode_recurrence_helicity_dispatch_v1(&decoded).unwrap(), + bytes + ); + } + + #[test] + fn atomic_writer_reports_the_published_payload_identity() { + let dispatch = DirectHelicityDispatch::new(valid_parts()).unwrap(); + let path = std::env::temp_dir().join(format!( + "rusticol-helicity-dispatch-{}-{}.bin", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_nanos() + )); + let metadata = write_recurrence_helicity_dispatch_v1_atomic(&path, &dispatch).unwrap(); + let bytes = std::fs::read(&path).unwrap(); + let expected_sha256: [u8; 32] = Sha256::digest(&bytes).into(); + assert_eq!(metadata.size_bytes, bytes.len() as u64); + assert_eq!(metadata.sha256, expected_sha256); + assert_eq!( + decode_recurrence_helicity_dispatch_v1(&bytes).unwrap(), + dispatch + ); + std::fs::remove_file(path).unwrap(); + } + + #[test] + fn dispatch_rejects_csr_that_disagrees_with_support_masks() { + let mut parts = valid_parts(); + parts.dispatch_group_ids[4] = 0; + let error = DirectHelicityDispatch::new(parts).unwrap_err(); + assert!(error.message().contains("disagrees with support masks")); + } + + #[test] + fn dispatch_rejects_static_source_groups() { + let mut parts = valid_parts(); + parts.row_groups[0].role = DirectExecutorRole::Source; + let error = DirectHelicityDispatch::new(parts).unwrap_err(); + assert!(error.message().contains("static source groups")); + } + + #[test] + fn dispatch_rejects_noncanonical_support_masks_and_trailing_bytes() { + let mut parts = valid_parts(); + parts.support_words[2] |= 1 << 63; + let error = DirectHelicityDispatch::new(parts).unwrap_err(); + assert!( + error + .message() + .contains("outside the resolved-helicity axis") + ); + + let dispatch = DirectHelicityDispatch::new(valid_parts()).unwrap(); + let mut bytes = encode_recurrence_helicity_dispatch_v1(&dispatch).unwrap(); + bytes.push(0); + let error = decode_recurrence_helicity_dispatch_v1(&bytes).unwrap_err(); + assert!(error.message().contains("trailing bytes")); + } + + #[test] + fn plan_pairing_fails_closed_before_accepting_a_non_union_plan() { + let dispatch = DirectHelicityDispatch::new(valid_parts()).unwrap(); + let plan = crate::recurrence::valid_direct_plan_fixture(); + let error = dispatch.validate_for_plan(&plan).unwrap_err(); + assert!(error.message().contains("all-flow-union")); + } +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/recurrence/direct_lowering.rs b/pyamplicol/rust/crates/rusticol-core/src/recurrence/direct_lowering.rs new file mode 100644 index 0000000000..42c76a1b17 --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/recurrence/direct_lowering.rs @@ -0,0 +1,3824 @@ +// SPDX-License-Identifier: 0BSD + +//! Deterministic lowering from a validated semantic recurrence program to the +//! direct-arena layout. +//! +//! This is deliberately a lowering-only slice. It creates immutable row-group +//! descriptors and direct-arena rows; it does not construct or invoke a +//! prepared runtime backend. + +use std::borrow::Cow; +use std::collections::{BTreeMap, BTreeSet}; + +use super::arena::{DirectArenaLayout, recurrence_direct_arena_layout}; +use super::direct_helicity_dispatch::{ + DirectHelicityDispatch, DirectHelicityDispatchDescriptor, DirectHelicityDispatchParts, + DirectHelicityRowGroupDescriptor, DirectHelicitySupportDomainDescriptor, +}; +use super::direct_plan::{ + DIRECT_CONTRIBUTION_FLAG_CERTIFIED_REUSE, DIRECT_CONTRIBUTION_FLAG_INITIALIZE_DESTINATION, + DIRECT_NONE_U32, DirectAmplitudeDestinationDescriptor, DirectClosureRow, DirectContributionRow, + DirectCurrentDescriptor, DirectDestinationOperation, DirectExecutorRole, DirectFinalizationRow, + DirectMomentumFormDescriptor, DirectMomentumTerm, DirectNodeKind, DirectRecurrencePlan, + DirectRecurrencePlanParts, DirectReplayTargetDescriptor, DirectResolvedHelicityDescriptor, + DirectResolvedSourceSelection, DirectRowGroupDescriptor, DirectSelectorDomainDescriptor, + DirectSourceDispatchVariantDescriptor, DirectSourceEmbeddingRow, DirectSourceProjectionRow, + DirectSourceRow, DirectSourceStateAssignment, +}; +use super::helicity_support::{ + AllFlowHelicityMask, AllFlowHelicitySupportProjection, project_all_flow_helicity_support, +}; +use super::layout::RuntimeSourceVariantBinding; +use super::relation::{ + RecurrenceCurrentRelationCertificate, RecurrenceRelationDiscoveryMode, + RecurrenceRelationDiscoveryOptions, RecurrenceRelationDiscoveryOutcome, + RecurrenceRelationDiscoveryReport, RelationCurrentContract, RelationCurrentInput, + RelationTermInput, discover_recurrence_current_relations, relation_certificate_algorithm, +}; +use super::template::{ + EvaluatorContractKind, MISSING_U32, OwnedRecurrenceTemplateInput, RuntimeHelicityVariantRow, + ValidatedRecurrenceTemplateInput, +}; +use super::{ + CanonicalMomentumLinearForm, ClosureProofMetadataV2, CurrentSourceBinding, + ExactComplexRational, RecurrenceCurrent, RecurrenceNodeKind, RecurrenceProgram, + RecurrenceStrategy, SemanticDigest, SourceStateAssignment, closure_component_factor_digest_v2, + closure_selector_domain_digest_v2, +}; +use crate::{RusticolError, RusticolResult}; + +const UNIVERSAL_SELECTOR_DOMAIN_ID: u32 = 0; +const DIRECT_ROW_FLAGS_NONE: u32 = 0; + +fn invalid(message: impl Into) -> RusticolError { + RusticolError::invalid_argument(format!("recurrence direct lowering: {}", message.into())) +} + +fn is_sha256(value: &str) -> bool { + value.len() == 64 + && value + .bytes() + .all(|character| character.is_ascii_digit() || (b'a'..=b'f').contains(&character)) +} + +/// Runtime sizing recorded in the immutable direct plan. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct DirectRecurrenceRuntimeOptions { + pub point_tile_size: u32, + pub workspace_mib: u32, +} + +impl DirectRecurrenceRuntimeOptions { + pub fn new(point_tile_size: u32, workspace_mib: u32) -> RusticolResult { + if point_tile_size == 0 { + return Err(invalid("point tile size must be positive")); + } + if workspace_mib == 0 { + return Err(invalid("workspace MiB must be positive")); + } + Ok(Self { + point_tile_size, + workspace_mib, + }) + } +} + +/// Stable semantic key exported by prepared direct-template-v1. +#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] +pub enum PreparedDirectExecutorKey { + Evaluator { + role: DirectExecutorRole, + evaluator_binding_id: u32, + }, + IdentityFinalizer, +} + +impl PreparedDirectExecutorKey { + const fn role(self) -> DirectExecutorRole { + match self { + Self::Evaluator { role, .. } => role, + Self::IdentityFinalizer => DirectExecutorRole::Finalization, + } + } +} + +/// One authenticated prepared direct-template mapping. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct PreparedDirectExecutorBinding { + pub key: PreparedDirectExecutorKey, + pub direct_executor_id: u32, + pub parent_permutation: [u8; 2], +} + +impl PreparedDirectExecutorBinding { + pub const fn evaluator( + role: DirectExecutorRole, + evaluator_binding_id: u32, + direct_executor_id: u32, + ) -> Self { + Self { + key: PreparedDirectExecutorKey::Evaluator { + role, + evaluator_binding_id, + }, + direct_executor_id, + parent_permutation: [0, 1], + } + } + + pub const fn evaluator_with_parent_permutation( + role: DirectExecutorRole, + evaluator_binding_id: u32, + direct_executor_id: u32, + parent_permutation: [u8; 2], + ) -> Self { + Self { + key: PreparedDirectExecutorKey::Evaluator { + role, + evaluator_binding_id, + }, + direct_executor_id, + parent_permutation, + } + } + + pub const fn identity_finalizer(direct_executor_id: u32) -> Self { + Self { + key: PreparedDirectExecutorKey::IdentityFinalizer, + direct_executor_id, + parent_permutation: [0, 1], + } + } +} + +/// Authenticated model-level direct executor resolver. +/// +/// IDs are the globally stable dense IDs owned by the complete loaded +/// direct-template catalog. Lowering never compacts or renumbers a +/// process-specific subset. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct PreparedDirectExecutorCatalog { + direct_template_catalog_digest: SemanticDigest, + bindings: BTreeMap, + executor_roles: Box<[DirectExecutorRole]>, +} + +impl PreparedDirectExecutorCatalog { + pub fn new( + direct_template_catalog_digest: SemanticDigest, + bindings: Vec, + ) -> RusticolResult { + if bindings.is_empty() { + return Err(invalid( + "prepared direct-template executor catalog must not be empty", + )); + } + let mut by_key = BTreeMap::new(); + let mut roles_by_id = BTreeMap::new(); + for binding in bindings { + if matches!( + binding.key, + PreparedDirectExecutorKey::Evaluator { + evaluator_binding_id: MISSING_U32, + .. + } + ) { + return Err(invalid( + "prepared direct-template mapping uses the missing evaluator-binding sentinel", + )); + } + if !matches!(binding.parent_permutation, [0, 1] | [1, 0]) + || (binding.parent_permutation != [0, 1] + && binding.key.role() != DirectExecutorRole::Contribution) + { + return Err(invalid( + "prepared direct-template mapping has an invalid parent permutation", + )); + } + if by_key.insert(binding.key, binding).is_some() { + return Err(invalid(format!( + "prepared direct-template mapping repeats key {:?}", + binding.key + ))); + } + let role = binding.key.role(); + if let Some(previous) = roles_by_id.insert(binding.direct_executor_id, role) + && previous != role + { + return Err(invalid(format!( + "prepared direct executor {} is assigned both {previous:?} and {role:?} roles", + binding.direct_executor_id + ))); + } + } + for (expected, actual) in roles_by_id.keys().copied().enumerate() { + if actual != u32_len("prepared direct executor", expected)? { + return Err(invalid(format!( + "prepared direct executor IDs are not dense zero-based at {actual}" + ))); + } + } + let executor_roles = roles_by_id + .into_values() + .collect::>() + .into_boxed_slice(); + Ok(Self { + direct_template_catalog_digest, + bindings: by_key, + executor_roles, + }) + } + + pub const fn direct_template_catalog_digest(&self) -> SemanticDigest { + self.direct_template_catalog_digest + } + + pub fn direct_executor_count(&self) -> u32 { + self.executor_roles.len() as u32 + } + + pub fn resolve_evaluator( + &self, + role: DirectExecutorRole, + evaluator_binding_id: u32, + ) -> RusticolResult { + let key = PreparedDirectExecutorKey::Evaluator { + role, + evaluator_binding_id, + }; + if let Some(binding) = self.bindings.get(&key) { + return Ok(binding.direct_executor_id); + } + if self.bindings.keys().any(|key| { + matches!( + key, + PreparedDirectExecutorKey::Evaluator { + evaluator_binding_id: candidate, + .. + } if *candidate == evaluator_binding_id + ) + }) { + return Err(invalid(format!( + "prepared direct-template mapping for evaluator binding {evaluator_binding_id} has a role mismatch; expected {role:?}" + ))); + } + Err(invalid(format!( + "prepared direct-template catalog has no {role:?} mapping for evaluator binding {evaluator_binding_id}" + ))) + } + + pub fn resolve_contribution( + &self, + evaluator_binding_id: u32, + ) -> RusticolResult<(u32, [u8; 2])> { + let key = PreparedDirectExecutorKey::Evaluator { + role: DirectExecutorRole::Contribution, + evaluator_binding_id, + }; + let binding = self.bindings.get(&key).ok_or_else(|| { + invalid(format!( + "prepared direct-template catalog has no Contribution mapping for evaluator binding {evaluator_binding_id}" + )) + })?; + Ok((binding.direct_executor_id, binding.parent_permutation)) + } + + pub fn resolve_identity_finalizer(&self) -> RusticolResult { + self.bindings + .get(&PreparedDirectExecutorKey::IdentityFinalizer) + .map(|binding| binding.direct_executor_id) + .ok_or_else(|| { + invalid( + "prepared direct-template catalog has no generic identity-finalizer binding", + ) + }) + } +} + +#[derive(Clone, Copy, Debug)] +struct SourceDraft { + stage: u16, + executor_id: u32, + semantic_current_id: u32, + row: DirectSourceRow, +} + +#[derive(Clone, Copy, Debug)] +struct ContributionDraft { + stage: u16, + executor_id: u32, + semantic_contribution_id: u32, + semantic_result_current_id: u32, + semantic_parent_current_ids: [u32; 2], + semantic_parent_count: u8, + row: DirectContributionRow, +} + +/// Authenticated runtime inputs which determine one contribution callable's +/// kinematic result before its row-local destination/factor application. +/// +/// The prepared executor ID fixes the callable, component layouts, coupling, +/// and model-parameter projections. Physical current/momentum coordinates fix +/// every remaining kinematic operand. Exact factors are ordered inside this +/// key rather than being part of it: intrinsic output-only executors may fan +/// out across factors, while prepared callables conservatively split the +/// adjacent run by factor at load time when the factor is a kernel input. +#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] +struct ContributionFanoutOrderKey { + stage: u32, + executor_id: u32, + selector_domain_id: u32, + parent0_component_base: u32, + parent1_component_base_or_sentinel: u32, + parent0_momentum_form_id: u32, + parent1_momentum_form_id_or_sentinel: u32, +} + +fn contribution_fanout_order_key_parts( + stage: u32, + executor_id: u32, + row: DirectContributionRow, +) -> ContributionFanoutOrderKey { + ContributionFanoutOrderKey { + stage, + executor_id, + selector_domain_id: row.selector_domain_id, + parent0_component_base: row.parent0_component_base, + parent1_component_base_or_sentinel: row.parent1_component_base_or_sentinel, + parent0_momentum_form_id: row.parent0_momentum_form_id, + parent1_momentum_form_id_or_sentinel: row.parent1_momentum_form_id_or_sentinel, + } +} + +#[cfg(test)] +fn contribution_fanout_order_key(draft: &ContributionDraft) -> ContributionFanoutOrderKey { + contribution_fanout_order_key_parts(u32::from(draft.stage), draft.executor_id, draft.row) +} + +/// Place repeated kinematic inputs before singleton inputs and make every +/// reusable class contiguous. This is a deliberate deterministic change to +/// floating-point accumulation order, not a physics approximation: each +/// destination still receives every exact-factor-scaled contribution once, +/// with its first row initialized and all later rows added. Exact/high- +/// precision execution remains driven by the semantic schedule. +/// Certified copies retain their incoming topological order within each +/// stage/executor/domain: recycled arena addresses are not dependency ranks. +pub(crate) fn order_contributions_for_runtime_fanout_by( + drafts: &mut [T], + parts: impl Fn(&T) -> (u32, u32, DirectContributionRow, Tie), +) where + Tie: Ord, +{ + let mut multiplicity = BTreeMap::::new(); + for draft in drafts.iter() { + let (stage, executor_id, row, _) = parts(draft); + if executor_id != DIRECT_NONE_U32 + && row.flags & DIRECT_CONTRIBUTION_FLAG_CERTIFIED_REUSE == 0 + { + *multiplicity + .entry(contribution_fanout_order_key_parts(stage, executor_id, row)) + .or_default() += 1; + } + } + drafts.sort_by_key(|draft| { + let (stage, executor_id, row, tie) = parts(draft); + let key = contribution_fanout_order_key_parts(stage, executor_id, row); + let certified_copy = row.flags & DIRECT_CONTRIBUTION_FLAG_CERTIFIED_REUSE != 0; + let reusable = executor_id != DIRECT_NONE_U32 + && !certified_copy + && multiplicity.get(&key).copied().unwrap_or(0) > 1; + ( + stage, + executor_id, + row.selector_domain_id, + !reusable, + (!certified_copy).then_some(( + key.parent0_component_base, + key.parent1_component_base_or_sentinel, + key.parent0_momentum_form_id, + key.parent1_momentum_form_id_or_sentinel, + row.exact_factor_id, + tie, + )), + ) + }); +} + +fn order_contributions_for_runtime_fanout(drafts: &mut [ContributionDraft]) { + order_contributions_for_runtime_fanout_by(drafts, |draft| { + ( + u32::from(draft.stage), + draft.executor_id, + draft.row, + draft.semantic_contribution_id, + ) + }); +} + +/// Temporary generation-time locality information. Only the resulting row +/// order is persisted: loading or selecting helicities does not run this pass. +#[derive(Debug)] +struct FixedHelicityDemands { + word_count: usize, + words: Vec, +} + +impl FixedHelicityDemands { + fn current(&self, current_id: u32) -> &[u64] { + let start = current_id as usize * self.word_count; + &self.words[start..start + self.word_count] + } +} + +fn fixed_helicity_demands( + current_count: usize, + helicity_count: usize, + contributions: &[ContributionDraft], + seeds: impl Iterator, +) -> RusticolResult { + let word_count = helicity_count.div_ceil(64); + let scalar_count = current_count + .checked_mul(word_count) + .ok_or_else(|| invalid("fixed-helicity locality mask size overflows usize"))?; + let mut demands = FixedHelicityDemands { + word_count, + words: vec![0; scalar_count], + }; + for (current_id, helicity_id) in seeds { + if current_id as usize >= current_count || helicity_id as usize >= helicity_count { + return Err(invalid( + "fixed-helicity locality seed is outside the program", + )); + } + demands.words[current_id as usize * word_count + helicity_id as usize / 64] |= + 1_u64 << (helicity_id % 64); + } + + // Use the actual lowered edges, including certified numerical copies, not + // the original source-state ancestry. Flat linked row lists avoid making + // a second collection of all parent IDs or allocating one list per current. + let mut first_contribution = vec![usize::MAX; current_count]; + let mut next_contribution = vec![usize::MAX; contributions.len()]; + for (row_id, draft) in contributions.iter().enumerate() { + let result = draft.semantic_result_current_id as usize; + if result >= current_count { + return Err(invalid( + "fixed-helicity locality result is outside the program", + )); + } + let parents = draft + .semantic_parent_current_ids + .get(..usize::from(draft.semantic_parent_count)) + .ok_or_else(|| invalid("fixed-helicity locality parent count is invalid"))?; + if parents.iter().any(|&parent| { + parent as usize > result + || (parent as usize == result + && draft.row.flags & DIRECT_CONTRIBUTION_FLAG_CERTIFIED_REUSE == 0) + }) { + return Err(invalid( + "fixed-helicity locality requires topologically ordered current dependencies", + )); + } + next_contribution[row_id] = first_contribution[result]; + first_contribution[result] = row_id; + } + for result in (0..current_count).rev() { + let mut row_id = first_contribution[result]; + let (earlier, current_and_later) = demands.words.split_at_mut(result * word_count); + let result_words = ¤t_and_later[..word_count]; + while row_id != usize::MAX { + let draft = &contributions[row_id]; + for &parent in + &draft.semantic_parent_current_ids[..usize::from(draft.semantic_parent_count)] + { + // A certified zero current may keep a self-copy row. It + // introduces no additional dependency or demanded helicity. + if parent as usize == result { + continue; + } + let start = parent as usize * word_count; + for (word, demanded) in earlier[start..start + word_count] + .iter_mut() + .zip(result_words) + { + *word |= *demanded; + } + } + row_id = next_contribution[row_id]; + } + } + Ok(demands) +} + +fn topology_replay_helicity_demands( + program: &RecurrenceProgram, + contributions: &[ContributionDraft], +) -> RusticolResult> { + if program.strategy() != RecurrenceStrategy::TopologyReplay + || program.resolved_helicities().len() <= 1 + || program + .amplitude_destinations() + .iter() + .any(|destination| destination.target_helicity_id().is_none()) + { + return Ok(None); + } + let seeds = program.closure_terms().iter().flat_map(|closure| { + let helicity = program.amplitude_destinations()[closure.target_destination_id() as usize] + .target_helicity_id() + .expect("validated topology-replay destinations specify their helicity"); + closure + .parent_current_ids() + .iter() + .map(move |¤t| (current, helicity)) + }); + fixed_helicity_demands( + program.currents().len(), + program.resolved_helicities().len(), + contributions, + seeds, + ) + .map(Some) +} + +/// Improve selected-helicity locality without splitting an existing reusable +/// parent-input class. This follows the ordinary fanout ordering and leaves +/// each class's exact-factor/destination order intact. Same-stage certified +/// copies are left in their existing order to preserve copy dependencies. +fn order_contributions_for_helicity_locality( + drafts: &mut [ContributionDraft], + demands: &FixedHelicityDemands, +) { + let mut group_start = 0; + while group_start < drafts.len() { + let first = &drafts[group_start]; + let primary = (first.stage, first.executor_id, first.row.selector_domain_id); + let mut group_end = group_start + 1; + while group_end < drafts.len() { + let next = &drafts[group_end]; + if (next.stage, next.executor_id, next.row.selector_domain_id) != primary { + break; + } + group_end += 1; + } + let group = &mut drafts[group_start..group_end]; + if primary.1 != DIRECT_NONE_U32 + && !group + .iter() + .any(|draft| draft.row.flags & DIRECT_CONTRIBUTION_FLAG_CERTIFIED_REUSE != 0) + { + let mut blocks = Vec::new(); + let mut start = 0; + while start < group.len() { + let first = &group[start]; + let key = contribution_fanout_order_key_parts( + u32::from(first.stage), + first.executor_id, + first.row, + ); + let mut end = start + 1; + while end < group.len() + && contribution_fanout_order_key_parts( + u32::from(group[end].stage), + group[end].executor_id, + group[end].row, + ) == key + { + end += 1; + } + let mut support = Cow::Borrowed(demands.current(first.semantic_result_current_id)); + for draft in &group[start + 1..end] { + let next = demands.current(draft.semantic_result_current_id); + if support.as_ref() != next { + for (word, demanded) in support.to_mut().iter_mut().zip(next) { + *word |= *demanded; + } + } + } + blocks.push((start..end, support)); + start = end; + } + // Preserve the original repeated-input-before-singleton policy. + // Stable sorting retains the old order within equal support masks. + if !blocks.is_sorted_by(|left, right| { + (left.0.len() == 1, &left.1) <= (right.0.len() == 1, &right.1) + }) { + blocks.sort_by(|left, right| { + (left.0.len() == 1) + .cmp(&(right.0.len() == 1)) + .then_with(|| left.1.cmp(&right.1)) + }); + let reordered = blocks + .iter() + .flat_map(|(range, _)| group[range.clone()].iter().copied()) + .collect::>(); + group.copy_from_slice(&reordered); + } + } + group_start = group_end; + } +} + +#[derive(Clone, Copy, Debug)] +struct FinalizationDraft { + stage: u16, + executor_id: u32, + semantic_current_id: u32, + row: DirectFinalizationRow, +} + +#[derive(Clone, Copy, Debug)] +struct ClosureDraft { + stage: u16, + executor_id: u32, + semantic_closure_id: u32, + amplitude_destination_id: u32, + row: DirectClosureRow, +} + +#[derive(Debug)] +struct LoweredHelicityRowSupport { + resolved_helicity_count: u32, + contributions: Vec>, + finalizations: Vec>, + closures: Vec>, +} + +struct LoweredSelectorDomains { + descriptors: Vec, + words: Vec, + current_domain_ids: Vec, + closure_domain_ids: Vec, + destination_domain_ids: Vec, + sector_domain_ids: Vec>, +} + +fn canonical_selector_words(domain: &[u64]) -> &[u64] { + let mut retained = domain + .iter() + .rposition(|word| *word != 0) + .map_or(0, |index| index + 1); + // Direct-plan v2 reserves the one-word [u64::MAX] encoding for the + // universal selector. For a wider physical-sector domain, a legitimate + // selector containing only sectors 0..63 must retain one trailing zero so + // it cannot alias that sentinel. + if retained == 1 && domain.len() > 1 && domain[0] == u64::MAX { + retained = 2; + } + &domain[..retained] +} + +fn lower_selector_domains(program: &RecurrenceProgram) -> RusticolResult { + // Keep the historical universal row first. Besides making complete-color + // plans byte-for-byte stable, this is the backwards-compatible + // direct-plan-v2 encoding recognized by the runtime. + let mut descriptors = vec![DirectSelectorDomainDescriptor { + word_start: 0, + word_count: 1, + }]; + let mut words = vec![u64::MAX]; + if !program.strategy().uses_topology_replay_targets() || program.replay_targets().is_empty() { + return Ok(LoweredSelectorDomains { + descriptors, + words, + current_domain_ids: vec![UNIVERSAL_SELECTOR_DOMAIN_ID; program.currents().len()], + closure_domain_ids: vec![UNIVERSAL_SELECTOR_DOMAIN_ID; program.closure_terms().len()], + destination_domain_ids: vec![ + UNIVERSAL_SELECTOR_DOMAIN_ID; + program.amplitude_destinations().len() + ], + sector_domain_ids: vec![ + Some(UNIVERSAL_SELECTOR_DOMAIN_ID); + program.physical_sector_count() as usize + ], + }); + } + + let sector_count = program.physical_sector_count() as usize; + let word_count = sector_count + .checked_add(u64::BITS as usize - 1) + .ok_or_else(|| invalid("selector-domain sector count overflows usize"))? + / u64::BITS as usize; + if word_count == 0 { + return Err(invalid( + "topology-replay selector domains require physical sectors", + )); + } + let mut current_words = vec![vec![0_u64; word_count]; program.currents().len()]; + let mut closure_words = Vec::with_capacity(program.closure_terms().len()); + for closure in program.closure_terms() { + let destination = program + .amplitude_destinations() + .get(closure.target_destination_id() as usize) + .ok_or_else(|| invalid("closure selector destination is absent"))?; + let sector_id = destination.target_sector_id() as usize; + if sector_id >= sector_count { + return Err(invalid("closure selector sector is out of bounds")); + } + let mut domain = vec![0_u64; word_count]; + domain[sector_id / u64::BITS as usize] |= 1_u64 << (sector_id % u64::BITS as usize); + for parent_id in closure.parent_current_ids() { + let parent = current_words + .get_mut(*parent_id as usize) + .ok_or_else(|| invalid("closure selector parent current is absent"))?; + for (parent_word, sector_word) in parent.iter_mut().zip(&domain) { + *parent_word |= *sector_word; + } + } + closure_words.push(domain); + } + + // Current IDs are topological. Walking backwards propagates every + // selected closure sector through exactly the parent currents needed to + // construct it, retaining sharing as the union of dependent sectors. + for current in program.currents().iter().rev() { + let domain = current_words + .get(current.id() as usize) + .ok_or_else(|| invalid("selector current is absent"))? + .clone(); + // Builder programs may retain structurally valid dead currents. Keep + // their fixed-width all-zero domain: full execution remains + // byte-for-byte compatible, while selected replay can omit them. + let contributions = current.contribution_range().as_usize_range( + program.contributions().len(), + "selector current contribution", + )?; + for contribution in &program.contributions()[contributions] { + for parent_id in contribution.parent_current_ids() { + let parent = current_words + .get_mut(*parent_id as usize) + .ok_or_else(|| invalid("selector contribution parent current is absent"))?; + for (parent_word, current_word) in parent.iter_mut().zip(&domain) { + *parent_word |= *current_word; + } + } + } + } + + let mut interned = BTreeMap::, u32>::new(); + interned.insert(vec![u64::MAX], UNIVERSAL_SELECTOR_DOMAIN_ID); + let mut intern = |domain: &[u64]| -> RusticolResult { + let domain = canonical_selector_words(domain); + if let Some(id) = interned.get(domain) { + return Ok(*id); + } + let id = u32::try_from(descriptors.len()) + .map_err(|_| invalid("selector-domain count exceeds u32"))?; + let word_start = u64::try_from(words.len()) + .map_err(|_| invalid("selector-domain word table exceeds u64"))?; + let word_count = u32::try_from(domain.len()) + .map_err(|_| invalid("selector-domain word count exceeds u32"))?; + words.extend_from_slice(domain); + descriptors.push(DirectSelectorDomainDescriptor { + word_start, + word_count, + }); + interned.insert(domain.to_vec(), id); + Ok(id) + }; + + let referenced_sector_ids = program + .amplitude_destinations() + .iter() + .map(|destination| destination.target_sector_id()) + .chain( + program + .replay_targets() + .iter() + .map(|target| target.materialized_sector_id()), + ) + .collect::>(); + let mut sector_domain_ids = vec![None; sector_count]; + for sector_id in referenced_sector_ids { + let sector_index = sector_id as usize; + if sector_index >= sector_count { + return Err(invalid("referenced selector sector is out of bounds")); + } + let mut domain = vec![0_u64; word_count]; + domain[sector_index / u64::BITS as usize] |= 1_u64 << (sector_index % u64::BITS as usize); + sector_domain_ids[sector_index] = Some(intern(&domain)?); + } + let current_domain_ids = current_words + .iter() + .map(|domain| intern(domain)) + .collect::>>()?; + let closure_domain_ids = closure_words + .iter() + .map(|domain| intern(domain)) + .collect::>>()?; + let destination_domain_ids = program + .amplitude_destinations() + .iter() + .map(|destination| { + sector_domain_ids + .get(destination.target_sector_id() as usize) + .copied() + .flatten() + .ok_or_else(|| invalid("amplitude selector sector is out of bounds")) + }) + .collect::>>()?; + Ok(LoweredSelectorDomains { + descriptors, + words, + current_domain_ids, + closure_domain_ids, + destination_domain_ids, + sector_domain_ids, + }) +} + +#[derive(Clone, Copy, Debug)] +pub(super) struct DirectParents { + pub(super) parent0_component_base: u32, + pub(super) parent1_component_base_or_sentinel: u32, + pub(super) parent0_momentum_form_id: u32, + pub(super) parent1_momentum_form_id_or_sentinel: u32, +} + +impl DirectParents { + pub(super) fn permuted(self, permutation: [u8; 2]) -> RusticolResult { + match permutation { + [0, 1] => Ok(self), + [1, 0] if self.parent1_component_base_or_sentinel != DIRECT_NONE_U32 => Ok(Self { + parent0_component_base: self.parent1_component_base_or_sentinel, + parent1_component_base_or_sentinel: self.parent0_component_base, + parent0_momentum_form_id: self.parent1_momentum_form_id_or_sentinel, + parent1_momentum_form_id_or_sentinel: self.parent0_momentum_form_id, + }), + [1, 0] => Err(invalid( + "binary contribution parent permutation references an absent second parent", + )), + _ => Err(invalid( + "direct contribution has an invalid parent permutation", + )), + } + } +} + +#[derive(Clone, Copy, Debug)] +pub(super) struct RuntimeSourceChoice { + pub(super) source_state_index: u32, + pub(super) public_helicity: i32, + pub(super) dispatch_variant_id: u32, +} + +#[derive(Debug, Default)] +struct LoweredRuntimeSources { + variants: Vec, + embeddings: Vec, + projections: Vec, + choices_by_source: Vec>, +} + +/// Lower one validated recurrence program to validated plan-v2 parts. +/// +/// `semantic_digest` authenticates the process semantic plan. +/// `prepared_pack_digest` must authenticate the prepared pack whose evaluator +/// bindings are present in `templates`. `direct_template_catalog_digest` +/// authenticates `direct_executors`. +pub fn lower_recurrence_direct_v2( + program: &RecurrenceProgram, + templates: &ValidatedRecurrenceTemplateInput, + direct_executors: &PreparedDirectExecutorCatalog, + semantic_digest: SemanticDigest, + prepared_pack_digest: SemanticDigest, + direct_template_catalog_digest: SemanticDigest, + runtime_options: DirectRecurrenceRuntimeOptions, +) -> RusticolResult { + let (parts, _, _) = build_direct_parts( + program, + templates, + direct_executors, + semantic_digest, + prepared_pack_digest, + direct_template_catalog_digest, + runtime_options, + &RecurrenceRelationDiscoveryOptions::off(), + None, + )?; + DirectRecurrencePlan::new(parts).map(DirectRecurrencePlan::into_parts) +} + +/// Lower one validated recurrence program directly to an immutable plan. +pub fn lower_recurrence_direct_plan_v2( + program: &RecurrenceProgram, + templates: &ValidatedRecurrenceTemplateInput, + direct_executors: &PreparedDirectExecutorCatalog, + semantic_digest: SemanticDigest, + prepared_pack_digest: SemanticDigest, + direct_template_catalog_digest: SemanticDigest, + runtime_options: DirectRecurrenceRuntimeOptions, +) -> RusticolResult { + let (parts, _, _) = build_direct_parts( + program, + templates, + direct_executors, + semantic_digest, + prepared_pack_digest, + direct_template_catalog_digest, + runtime_options, + &RecurrenceRelationDiscoveryOptions::off(), + None, + )?; + DirectRecurrencePlan::new(parts) +} + +/// Lower one physical all-flow program together with its exact persisted +/// helicity-dispatch sidecar. +/// +/// The ordinary DirectPlan remains unchanged and executable on its own. The +/// companion sidecar selects dependency-closed non-source row runs for one +/// resolved helicity during cold schedule binding; warm execution uses only +/// that already-specialized schedule. +pub fn lower_recurrence_direct_plan_v2_with_helicity_dispatch( + program: &RecurrenceProgram, + templates: &ValidatedRecurrenceTemplateInput, + direct_executors: &PreparedDirectExecutorCatalog, + semantic_digest: SemanticDigest, + prepared_pack_digest: SemanticDigest, + direct_template_catalog_digest: SemanticDigest, + runtime_options: DirectRecurrenceRuntimeOptions, +) -> RusticolResult<(DirectRecurrencePlan, DirectHelicityDispatch)> { + let projection = project_all_flow_helicity_support(program, templates)?; + let (parts, _, lowered_support) = build_direct_parts( + program, + templates, + direct_executors, + semantic_digest, + prepared_pack_digest, + direct_template_catalog_digest, + runtime_options, + &RecurrenceRelationDiscoveryOptions::off(), + Some(&projection), + )?; + let lowered_support = lowered_support + .ok_or_else(|| invalid("all-flow helicity support disappeared during direct lowering"))?; + let plan = DirectRecurrencePlan::new(parts)?; + let dispatch = lower_direct_helicity_dispatch(&plan, lowered_support)?; + dispatch.validate_for_plan(&plan)?; + Ok((plan, dispatch)) +} + +/// Lower recurrence with opt-in relation diagnostics or certified scale-copy +/// reuse. The returned report is absent only when the mode is `off`. +#[allow(clippy::too_many_arguments)] +pub fn lower_recurrence_direct_plan_v2_with_relation_discovery( + program: &RecurrenceProgram, + templates: &ValidatedRecurrenceTemplateInput, + direct_executors: &PreparedDirectExecutorCatalog, + semantic_digest: SemanticDigest, + prepared_pack_digest: SemanticDigest, + direct_template_catalog_digest: SemanticDigest, + runtime_options: DirectRecurrenceRuntimeOptions, + relation_options: &RecurrenceRelationDiscoveryOptions, +) -> RusticolResult<( + DirectRecurrencePlan, + Option, +)> { + let (parts, report, _) = build_direct_parts( + program, + templates, + direct_executors, + semantic_digest, + prepared_pack_digest, + direct_template_catalog_digest, + runtime_options, + relation_options, + None, + )?; + Ok((DirectRecurrencePlan::new(parts)?, report)) +} + +#[allow(clippy::too_many_arguments)] +fn build_direct_parts( + program: &RecurrenceProgram, + templates: &ValidatedRecurrenceTemplateInput, + direct_executors: &PreparedDirectExecutorCatalog, + semantic_digest: SemanticDigest, + prepared_pack_digest: SemanticDigest, + direct_template_catalog_digest: SemanticDigest, + runtime_options: DirectRecurrenceRuntimeOptions, + relation_options: &RecurrenceRelationDiscoveryOptions, + helicity_support: Option<&AllFlowHelicitySupportProjection>, +) -> RusticolResult<( + DirectRecurrencePlanParts, + Option, + Option, +)> { + validate_lowering_boundary( + program, + templates, + direct_executors, + prepared_pack_digest, + direct_template_catalog_digest, + runtime_options, + )?; + + let input = templates.input(); + let external_source_count = external_source_count(program)?; + let component_counts = component_counts(program, templates)?; + let arena = recurrence_direct_arena_layout(program, &component_counts)?; + let selector_domains = lower_selector_domains(program)?; + + let (momentum_ids, momentum_forms, momentum_terms) = + intern_momentum_forms(program, external_source_count)?; + let (mut exact_factors, mut exact_factor_ids) = intern_exact_factors(program)?; + let mut closure_factor_blocks = BTreeMap::new(); + + let mut source_drafts = Vec::new(); + let mut runtime_sources = LoweredRuntimeSources { + choices_by_source: vec![Vec::new(); external_source_count as usize], + ..LoweredRuntimeSources::default() + }; + for current in program + .currents() + .iter() + .filter(|current| current.is_source()) + { + let assignment = arena_assignment(&arena, current.id())?; + let source_slot = only_source_slot(current)?; + match (program.strategy(), current.key().source_binding()) { + ( + RecurrenceStrategy::TopologyReplay | RecurrenceStrategy::ContractedColorUnion, + CurrentSourceBinding::FixedTemplate(source_template_id), + ) => { + let source_factor = current.source_exact_factor().ok_or_else(|| { + invalid(format!( + "{} source current {} has no exact source factor", + program.strategy(), + current.id(), + )) + })?; + let source = input + .sources + .get(*source_template_id as usize) + .ok_or_else(|| { + invalid(format!( + "source current {} references absent source template {source_template_id}", + current.id() + )) + })?; + let current_state = + state_row(templates, current.key().current_state_template_id())?; + let source_state = state_row(templates, source.state_template_id)?; + if current_state.dimension != source_state.dimension { + return Err(invalid(format!( + "source current {} and source template {source_template_id} have different component dimensions", + current.id() + ))); + } + let evaluator_binding_id = validated_evaluator_binding_id( + templates, + source.evaluator_binding_id, + EvaluatorContractKind::Source, + "source", + )?; + let executor_id = direct_executors + .resolve_evaluator(DirectExecutorRole::Source, evaluator_binding_id)?; + source_drafts.push(SourceDraft { + stage: current_stage(current)?, + executor_id, + semantic_current_id: current.id(), + row: DirectSourceRow { + source_slot, + destination_component_base: assignment.component_base, + momentum_form_id: momentum_id(&momentum_ids, current.key().momentum())?, + source_template_or_dispatch_domain: *source_template_id, + spin_state_class: current.key().spin_state_class(), + exact_factor_id: exact_factor_id(&exact_factor_ids, source_factor)?, + selector_domain_id: selector_domains.current_domain_ids + [current.id() as usize], + }, + }); + } + ( + RecurrenceStrategy::AllFlowUnion, + CurrentSourceBinding::RuntimeDispatch { + domain, + source_template_ids, + variant_bindings, + }, + ) => { + lower_union_source( + current, + source_slot, + *domain, + source_template_ids, + variant_bindings, + templates, + direct_executors, + &arena, + &momentum_ids, + &mut exact_factors, + &mut exact_factor_ids, + &mut source_drafts, + &mut runtime_sources, + )?; + } + (RecurrenceStrategy::TopologyReplay | RecurrenceStrategy::ContractedColorUnion, _) => { + return Err(invalid(format!( + "{} source current {} does not use one fixed source template", + program.strategy(), + current.id(), + ))); + } + (RecurrenceStrategy::AllFlowUnion, _) => { + return Err(invalid(format!( + "all-flow-union source current {} does not use runtime dispatch", + current.id() + ))); + } + } + } + + let mut contribution_drafts = Vec::with_capacity(program.contributions().len()); + for contribution in program.contributions() { + validate_direct_arity( + "contribution", + contribution.id(), + contribution.parent_current_ids().len(), + )?; + let result = program + .currents() + .get(contribution.result_current_id() as usize) + .ok_or_else(|| invalid("contribution result current is absent"))?; + let transition_id = contribution.key().transition_template_id(); + let transition = input + .transitions + .get(transition_id as usize) + .ok_or_else(|| { + invalid(format!( + "contribution {} references absent transition template {transition_id}", + contribution.id() + )) + })?; + let expected_parent_states = template_u32_sequence( + templates, + transition.input_state_sequence_id, + "transition input states", + )?; + let canonical_input_order = template_u32_sequence( + templates, + transition.canonical_input_order_sequence_id, + "transition canonical input order", + )?; + let actual_parent_states = + parent_state_template_ids(program, contribution.parent_current_ids())?; + if !parent_state_contract_matches( + expected_parent_states, + canonical_input_order, + transition.input_exchange_factor_id != MISSING_U32, + &actual_parent_states, + )? || contribution.key().parent_state_template_ids() != actual_parent_states + { + return Err(invalid(format!( + "contribution {} parent-state contract does not match transition {transition_id}", + contribution.id() + ))); + } + if transition.result_state_template_id != result.key().current_state_template_id() + || transition.result_state_template_id != contribution.key().result_state_template_id() + { + return Err(invalid(format!( + "contribution {} result-state contract does not match transition {transition_id}", + contribution.id() + ))); + } + if transition.quantum_flow_template_id != contribution.key().quantum_flow_witness_id() { + return Err(invalid(format!( + "contribution {} quantum-flow witness does not match transition {transition_id}", + contribution.id() + ))); + } + validate_coupling_digest(templates, contribution)?; + if transition.output_projection_string_id != contribution.key().output_projection_id() { + return Err(invalid(format!( + "contribution {} output projection does not match transition {transition_id}", + contribution.id() + ))); + } + let color_witness = contribution.key().color_witness_term_id(); + if color_witness.color_contraction_template_id() != transition.color_contraction_template_id + { + return Err(invalid(format!( + "contribution {} color witness does not belong to transition {transition_id}", + contribution.id() + ))); + } + let contraction = input + .color_contractions + .get(transition.color_contraction_template_id as usize) + .ok_or_else(|| invalid("transition color contraction is absent"))?; + if u64::from(color_witness.witness_ordinal()) >= contraction.witness_count { + return Err(invalid(format!( + "contribution {} color-witness ordinal is out of bounds", + contribution.id() + ))); + } + validate_parent_momentum_contract(program, contribution)?; + let parents = direct_parents( + program, + contribution.parent_current_ids(), + &arena, + &momentum_ids, + )?; + let evaluator_binding_id = validated_evaluator_binding_id( + templates, + transition.evaluator_binding_id, + EvaluatorContractKind::Vertex, + "contribution", + )?; + let (executor_id, parent_permutation) = + direct_executors.resolve_contribution(evaluator_binding_id)?; + let parents = parents.permuted(parent_permutation)?; + let mut semantic_parent_current_ids = [DIRECT_NONE_U32; 2]; + for (slot, parent_id) in contribution + .parent_current_ids() + .iter() + .copied() + .enumerate() + { + semantic_parent_current_ids[slot] = parent_id; + } + if contribution.parent_current_ids().len() == 2 && parent_permutation == [1, 0] { + semantic_parent_current_ids.swap(0, 1); + } + contribution_drafts.push(ContributionDraft { + stage: current_stage(result)?, + executor_id, + semantic_contribution_id: contribution.id(), + semantic_result_current_id: result.id(), + semantic_parent_current_ids, + semantic_parent_count: u8::try_from(contribution.parent_current_ids().len()) + .map_err(|_| invalid("contribution parent count exceeds u8"))?, + row: DirectContributionRow { + parent0_component_base: parents.parent0_component_base, + parent1_component_base_or_sentinel: parents.parent1_component_base_or_sentinel, + parent0_momentum_form_id: parents.parent0_momentum_form_id, + parent1_momentum_form_id_or_sentinel: parents.parent1_momentum_form_id_or_sentinel, + destination_component_base: arena_assignment(&arena, result.id())?.component_base, + exact_factor_id: exact_factor_id(&exact_factor_ids, contribution.exact_factor())?, + selector_domain_id: selector_domains.current_domain_ids[result.id() as usize], + flags: DIRECT_ROW_FLAGS_NONE, + }, + }); + } + + let mut finalization_drafts = Vec::with_capacity(program.finalizations().len()); + for finalization in program.finalizations() { + let current = program + .currents() + .get(finalization.current_id() as usize) + .ok_or_else(|| invalid("finalization current is absent"))?; + if finalization.propagator_template_id().is_none() + && finalization.exact_factor() == ExactComplexRational::ONE + { + continue; + } + let executor_id = if let Some(propagator_template_id) = + finalization.propagator_template_id() + { + let propagator = input + .propagators + .get(propagator_template_id as usize) + .ok_or_else(|| { + invalid(format!( + "finalization {} references absent propagator template {propagator_template_id}", + finalization.id() + )) + })?; + if propagator.applies_propagator == 0 + || propagator.state_template_id != current.key().current_state_template_id() + { + return Err(invalid(format!( + "finalization {} does not match an active propagator for current {}", + finalization.id(), + current.id() + ))); + } + let evaluator_binding_id = validated_evaluator_binding_id( + templates, + propagator.evaluator_binding_id, + EvaluatorContractKind::Propagator, + "finalization", + )?; + direct_executors + .resolve_evaluator(DirectExecutorRole::Finalization, evaluator_binding_id)? + } else { + direct_executors.resolve_identity_finalizer()? + }; + let assignment = arena_assignment(&arena, current.id())?; + finalization_drafts.push(FinalizationDraft { + stage: current_stage(current)?, + executor_id, + semantic_current_id: current.id(), + row: DirectFinalizationRow { + component_base: assignment.component_base, + component_count: u16::try_from(assignment.component_count).map_err(|_| { + invalid(format!( + "current {} component count exceeds u16", + current.id() + )) + })?, + momentum_form_id: momentum_id(&momentum_ids, current.key().momentum())?, + exact_factor_id: exact_factor_id(&exact_factor_ids, finalization.exact_factor())?, + selector_domain_id: selector_domains.current_domain_ids[current.id() as usize], + flags: DIRECT_ROW_FLAGS_NONE, + }, + }); + } + + let closure_component_coefficients = templates.closure_component_coefficient_catalog()?; + let closure_stage = program + .currents() + .iter() + .map(current_stage) + .collect::>>()? + .into_iter() + .max() + .unwrap_or(0) + .checked_add(1) + .ok_or_else(|| invalid("closure stage exceeds u16"))?; + let mut closure_drafts = Vec::with_capacity(program.closure_terms().len()); + for closure in program.closure_terms() { + validate_direct_arity("closure", closure.id(), closure.parent_current_ids().len())?; + let closure_template_id = closure.closure_template_id(); + let template = input + .closures + .get(closure_template_id as usize) + .ok_or_else(|| { + invalid(format!( + "closure {} references absent closure template {closure_template_id}", + closure.id() + )) + })?; + let expected_parent_states = template_u32_sequence( + templates, + template.input_state_sequence_id, + "closure input states", + )?; + let canonical_input_order = template_u32_sequence( + templates, + template.canonical_input_order_sequence_id, + "closure canonical input order", + )?; + let actual_parent_states = + parent_state_template_ids(program, closure.parent_current_ids())?; + if !parent_state_contract_matches( + expected_parent_states, + canonical_input_order, + template.input_exchange_factor_id != MISSING_U32, + &actual_parent_states, + )? { + return Err(invalid(format!( + "closure {} parent-state contract does not match template {closure_template_id}", + closure.id() + ))); + } + let eligible_flows = template_u32_sequence( + templates, + template.eligible_quantum_flow_sequence_id, + "closure eligible quantum flows", + )?; + match closure.quantum_flow_template_id() { + Some(flow_id) if eligible_flows.binary_search(&flow_id).is_ok() => {} + Some(flow_id) => { + return Err(invalid(format!( + "closure {} quantum-flow witness {flow_id} is not eligible for template {closure_template_id}", + closure.id() + ))); + } + None if eligible_flows.is_empty() => {} + None => { + return Err(invalid(format!( + "closure {} omits the quantum-flow witness required by template {closure_template_id}", + closure.id() + ))); + } + } + let evaluator_binding_id = validated_evaluator_binding_id( + templates, + template.evaluator_binding_id, + EvaluatorContractKind::Closure, + "closure", + )?; + let executor_id = direct_executors + .resolve_evaluator(DirectExecutorRole::Closure, evaluator_binding_id)?; + let parents = direct_parents(program, closure.parent_current_ids(), &arena, &momentum_ids)?; + let component_coefficients = closure_component_coefficients + .get(closure_template_id as usize) + .ok_or_else(|| { + invalid(format!( + "closure {} references absent component coefficients for template {closure_template_id}", + closure.id() + )) + })?; + let parent0_id = closure.parent_current_ids()[0] as usize; + let component_count = *component_counts + .get(parent0_id) + .ok_or_else(|| invalid("closure parent 0 component count is absent"))?; + if let Some(parent1_id) = closure.parent_current_ids().get(1) { + let parent1_component_count = *component_counts + .get(*parent1_id as usize) + .ok_or_else(|| invalid("closure parent 1 component count is absent"))?; + if parent1_component_count != component_count { + return Err(invalid(format!( + "closure {} parent component counts differ", + closure.id() + ))); + } + } + if component_coefficients.len() != component_count as usize { + return Err(invalid(format!( + "closure {} has {} component coefficients for {component_count} current components", + closure.id(), + component_coefficients.len() + ))); + } + let (component_factor_start, component_count) = intern_closure_factor_block( + component_coefficients, + &mut exact_factors, + &mut closure_factor_blocks, + )?; + closure_drafts.push(ClosureDraft { + stage: closure_stage, + executor_id, + semantic_closure_id: closure.id(), + amplitude_destination_id: closure.target_destination_id(), + row: DirectClosureRow { + parent0_component_base: parents.parent0_component_base, + parent1_component_base_or_sentinel: parents.parent1_component_base_or_sentinel, + parent0_momentum_form_id: parents.parent0_momentum_form_id, + parent1_momentum_form_id_or_sentinel: parents.parent1_momentum_form_id_or_sentinel, + amplitude_destination_id: closure.target_destination_id(), + exact_factor_id: exact_factor_id(&exact_factor_ids, closure.exact_factor())?, + component_factor_start, + component_count, + selector_domain_id: selector_domains.closure_domain_ids[closure.id() as usize], + flags: DIRECT_ROW_FLAGS_NONE, + }, + }); + } + + let relation_report = apply_recurrence_relation_discovery( + program, + semantic_digest, + &arena, + &momentum_ids, + &selector_domains.current_domain_ids, + &mut contribution_drafts, + &finalization_drafts, + &mut exact_factors, + &mut exact_factor_ids, + relation_options, + )?; + + let helicity_demands = topology_replay_helicity_demands(program, &contribution_drafts)?; + source_drafts.sort_by(|left, right| { + (left.stage, left.executor_id, left.row.selector_domain_id) + .cmp(&(right.stage, right.executor_id, right.row.selector_domain_id)) + .then_with(|| { + helicity_demands + .as_ref() + .map_or(std::cmp::Ordering::Equal, |demands| { + demands + .current(left.semantic_current_id) + .cmp(demands.current(right.semantic_current_id)) + }) + }) + .then_with(|| left.semantic_current_id.cmp(&right.semantic_current_id)) + }); + order_contributions_for_runtime_fanout(&mut contribution_drafts); + if let Some(demands) = &helicity_demands { + order_contributions_for_helicity_locality(&mut contribution_drafts, demands); + } + let mut initialized_currents = BTreeSet::new(); + for draft in &mut contribution_drafts { + if initialized_currents.insert(draft.semantic_result_current_id) { + draft.row.flags |= DIRECT_CONTRIBUTION_FLAG_INITIALIZE_DESTINATION; + } + } + finalization_drafts.sort_by(|left, right| { + (left.stage, left.executor_id, left.row.selector_domain_id) + .cmp(&(right.stage, right.executor_id, right.row.selector_domain_id)) + .then_with(|| { + helicity_demands + .as_ref() + .map_or(std::cmp::Ordering::Equal, |demands| { + demands + .current(left.semantic_current_id) + .cmp(demands.current(right.semantic_current_id)) + }) + }) + .then_with(|| left.semantic_current_id.cmp(&right.semantic_current_id)) + }); + drop(helicity_demands); + // Destination ownership is primary. Each destination retains exactly one + // contiguous closure range; executor changes create additional row groups. + closure_drafts.sort_by_key(|draft| { + ( + draft.amplitude_destination_id, + draft.stage, + draft.executor_id, + draft.semantic_closure_id, + ) + }); + let lowered_helicity_support = helicity_support + .map(|projection| { + lower_semantic_helicity_support( + program, + projection, + &contribution_drafts, + &finalization_drafts, + &closure_drafts, + ) + }) + .transpose()?; + let mut closure_row_by_term = vec![DIRECT_NONE_U32; program.closure_terms().len()]; + let mut proof_group_by_term = vec![None; program.closure_terms().len()]; + for group in program.closure_proofs().groups() { + if let Some(term_id) = group.emitted_runtime_closure_term_id() { + let slot = proof_group_by_term + .get_mut(term_id as usize) + .ok_or_else(|| invalid("closure proof group references an absent runtime term"))?; + *slot = Some(group.id()); + } + } + let mut lowered_proof_groups = program.closure_proofs().groups().to_vec(); + for (row_index, draft) in closure_drafts.iter_mut().enumerate() { + let row_id = u32_len("closure row", row_index)?; + let term_slot = closure_row_by_term + .get_mut(draft.semantic_closure_id as usize) + .ok_or_else(|| invalid("closure draft semantic term ID is out of bounds"))?; + if *term_slot != DIRECT_NONE_U32 { + return Err(invalid(format!( + "semantic closure term {} lowered more than once", + draft.semantic_closure_id + ))); + } + *term_slot = row_id; + let group_id = proof_group_by_term + .get(draft.semantic_closure_id as usize) + .copied() + .flatten() + .ok_or_else(|| { + invalid(format!( + "semantic closure term {} has no proof group", + draft.semantic_closure_id + )) + })?; + let group = program + .closure_proofs() + .groups() + .get(group_id as usize) + .filter(|group| group.id() == group_id) + .ok_or_else(|| invalid("closure proof group ID is not dense"))?; + draft.row.flags = group.id(); + let component_start = draft.row.component_factor_start as usize; + let component_end = component_start + .checked_add(draft.row.component_count as usize) + .ok_or_else(|| invalid("closure component-factor range exceeds usize"))?; + let component_digest = closure_component_factor_digest_v2( + exact_factors + .get(component_start..component_end) + .ok_or_else(|| invalid("closure component-factor range is out of bounds"))?, + ) + .map_err(|error| invalid(error.message()))?; + let selector = selector_domains + .descriptors + .get(draft.row.selector_domain_id as usize) + .ok_or_else(|| invalid("lowered closure selector domain is absent"))?; + let selector_start = selector.word_start as usize; + let selector_end = selector_start + .checked_add(selector.word_count as usize) + .ok_or_else(|| invalid("lowered closure selector range overflows usize"))?; + let selector_digest = closure_selector_domain_digest_v2( + selector_domains + .words + .get(selector_start..selector_end) + .ok_or_else(|| invalid("lowered closure selector range is out of bounds"))?, + ) + .map_err(|error| invalid(error.message()))?; + lowered_proof_groups[group.id() as usize] = + group.with_direct_closure_binding(Some(row_id), component_digest, selector_digest)?; + } + if closure_row_by_term.contains(&DIRECT_NONE_U32) { + return Err(invalid( + "not every semantic closure term was lowered to a direct row", + )); + } + let closure_proofs = + ClosureProofMetadataV2::new_with_three_line_certificates_and_candidate_domain( + program.closure_proofs().contributions().to_vec(), + lowered_proof_groups, + program.closure_proofs().reflection_certificates().to_vec(), + program + .closure_proofs() + .three_line_traversal_certificates() + .to_vec(), + program.closure_proofs().candidate_domain_certificate(), + ) + .map_err(|error| invalid(error.message()))?; + + let mut source_row_by_current = vec![DIRECT_NONE_U32; program.currents().len()]; + for (row_index, draft) in source_drafts.iter().enumerate() { + source_row_by_current[draft.semantic_current_id as usize] = + u32_len("source row", row_index)?; + } + let mut finalization_row_by_current = vec![DIRECT_NONE_U32; program.currents().len()]; + for (row_index, draft) in finalization_drafts.iter().enumerate() { + finalization_row_by_current[draft.semantic_current_id as usize] = + u32_len("finalization row", row_index)?; + } + + let currents = program + .currents() + .iter() + .map(|current| { + let assignment = arena_assignment(&arena, current.id())?; + Ok(DirectCurrentDescriptor { + semantic_current_id: current.id(), + node_kind: match current.key().node_kind() { + RecurrenceNodeKind::Source => DirectNodeKind::Source, + RecurrenceNodeKind::Current => DirectNodeKind::Current, + }, + state_template_id: current.key().current_state_template_id(), + component_base: assignment.component_base, + component_count: u16::try_from(assignment.component_count).map_err(|_| { + invalid(format!( + "current {} component count exceeds u16", + current.id() + )) + })?, + momentum_form_id: momentum_id(&momentum_ids, current.key().momentum())?, + stage: current_stage(current)?, + selector_domain_id: selector_domains.current_domain_ids[current.id() as usize], + first_use: direct_event(assignment.first_use, "arena first use")?, + last_use: direct_event(assignment.last_use, "arena last use")?, + source_row_or_sentinel: source_row_by_current[current.id() as usize], + finalization_row_or_sentinel: finalization_row_by_current[current.id() as usize], + }) + }) + .collect::>>()?; + + let mut row_groups = Vec::new(); + append_row_groups( + source_drafts + .iter() + .map(|draft| (draft.stage, draft.executor_id)), + DirectExecutorRole::Source, + DirectDestinationOperation::Initialize, + &mut row_groups, + )?; + append_row_groups( + contribution_drafts + .iter() + .map(|draft| (draft.stage, draft.executor_id)), + DirectExecutorRole::Contribution, + DirectDestinationOperation::Add, + &mut row_groups, + )?; + append_row_groups( + finalization_drafts + .iter() + .map(|draft| (draft.stage, draft.executor_id)), + DirectExecutorRole::Finalization, + DirectDestinationOperation::FinalizeInPlace, + &mut row_groups, + )?; + append_row_groups( + closure_drafts + .iter() + .map(|draft| (draft.stage, draft.executor_id)), + DirectExecutorRole::Closure, + DirectDestinationOperation::ClosureAdd, + &mut row_groups, + )?; + row_groups.sort_by_key(|row_group| (row_group.stage, row_group.role, row_group.row_start)); + + let amplitude_destinations = lower_amplitude_destinations( + program, + &closure_drafts, + &selector_domains.destination_domain_ids, + )?; + let ( + resolved_helicities, + source_state_assignments, + resolved_source_selections, + public_helicities, + ) = match program.strategy() { + RecurrenceStrategy::TopologyReplay | RecurrenceStrategy::ContractedColorUnion => { + let (descriptors, source_states, public_helicities) = + lower_resolved_helicities(program)?; + (descriptors, source_states, Vec::new(), public_helicities) + } + RecurrenceStrategy::AllFlowUnion => lower_union_resolved_helicities( + &runtime_sources.choices_by_source, + program.retained_helicity_count(), + )?, + }; + let (replay_targets, source_permutations, replay_momentum_signs, replay_helicity_map) = + lower_replay_targets( + program, + &exact_factor_ids, + &selector_domains.sector_domain_ids, + )?; + + let state_template_count = u32_len("state template", input.current_states.len())?; + let source_template_count = u32_len("source template", input.sources.len())?; + let direct_executor_count = direct_executors.direct_executor_count(); + if state_template_count == 0 || source_template_count == 0 || direct_executor_count == 0 { + return Err(invalid( + "direct lowering requires nonempty state, source, and executor catalogs", + )); + } + + let parts = DirectRecurrencePlanParts { + strategy: program.strategy(), + semantic_digest, + prepared_pack_digest, + direct_template_catalog_digest, + point_tile_size: runtime_options.point_tile_size, + workspace_mib: runtime_options.workspace_mib, + current_arena_components: arena.component_count(), + physical_sector_count: program.physical_sector_count(), + retained_helicity_count: program.retained_helicity_count(), + amplitude_destination_count: u32_len( + "amplitude destination", + program.amplitude_destinations().len(), + )?, + parameter_value_count: templates.summary().parameter_count, + external_source_count, + state_template_count, + source_template_count, + source_template_or_dispatch_count: match program.strategy() { + RecurrenceStrategy::TopologyReplay | RecurrenceStrategy::ContractedColorUnion => { + source_template_count + } + RecurrenceStrategy::AllFlowUnion => templates.summary().runtime_helicity_contract_count, + }, + runtime_helicity_contract_count: match program.strategy() { + RecurrenceStrategy::TopologyReplay | RecurrenceStrategy::ContractedColorUnion => 0, + RecurrenceStrategy::AllFlowUnion => templates.summary().runtime_helicity_contract_count, + }, + runtime_helicity_variant_count: match program.strategy() { + RecurrenceStrategy::TopologyReplay | RecurrenceStrategy::ContractedColorUnion => 0, + RecurrenceStrategy::AllFlowUnion => u32_len( + "runtime-helicity variant", + input.runtime_helicity_variants.len(), + )?, + }, + direct_executor_count, + currents, + sources: source_drafts.iter().map(|draft| draft.row).collect(), + contributions: contribution_drafts.iter().map(|draft| draft.row).collect(), + finalizations: finalization_drafts.iter().map(|draft| draft.row).collect(), + closures: closure_drafts.iter().map(|draft| draft.row).collect(), + row_groups, + momentum_forms, + momentum_terms, + // Topology replay persists one dependency-closed domain per physical + // sector. Other strategies retain the historical universal domain. + selector_domains: selector_domains.descriptors, + selector_words: selector_domains.words, + replay_targets, + source_permutations, + replay_momentum_signs, + replay_helicity_map, + amplitude_destinations, + resolved_helicities, + source_state_assignments, + source_dispatch_variants: runtime_sources.variants, + source_embeddings: runtime_sources.embeddings, + source_projections: runtime_sources.projections, + resolved_source_selections, + public_helicities, + exact_factors, + closure_proofs, + }; + Ok((parts, relation_report, lowered_helicity_support)) +} + +fn checked_projection_mask( + mask: &AllFlowHelicityMask, + expected_word_count: usize, + label: &str, +) -> RusticolResult> { + if mask.words().len() != expected_word_count { + return Err(invalid(format!( + "{label} support mask has {} words, expected {expected_word_count}", + mask.words().len() + ))); + } + Ok(mask.words().to_vec()) +} + +fn lower_semantic_helicity_support( + program: &RecurrenceProgram, + projection: &AllFlowHelicitySupportProjection, + contribution_drafts: &[ContributionDraft], + finalization_drafts: &[FinalizationDraft], + closure_drafts: &[ClosureDraft], +) -> RusticolResult { + if program.strategy() != RecurrenceStrategy::AllFlowUnion + || u64::from(projection.resolved_helicity_count()) != program.retained_helicity_count() + { + return Err(invalid( + "semantic helicity support does not match the all-flow program axis", + )); + } + let word_count = projection.resolved_helicity_count().div_ceil(64) as usize; + let mut contributions = Vec::with_capacity(contribution_drafts.len()); + for draft in contribution_drafts { + let mask = if draft.row.flags & DIRECT_CONTRIBUTION_FLAG_CERTIFIED_REUSE != 0 { + let result = projection + .current_demand() + .get(draft.semantic_result_current_id as usize) + .ok_or_else(|| invalid("certified-reuse result current support is absent"))?; + let representative_id = draft.semantic_parent_current_ids[0]; + if representative_id == DIRECT_NONE_U32 { + return Err(invalid( + "certified-reuse contribution has no representative current", + )); + } + let representative = projection + .current_availability() + .get(representative_id as usize) + .ok_or_else(|| { + invalid("certified-reuse representative current support is absent") + })?; + if result.words().len() != word_count || representative.words().len() != word_count { + return Err(invalid( + "certified-reuse current support mask has the wrong width", + )); + } + result + .words() + .iter() + .zip(representative.words()) + .map(|(result, representative)| result & representative) + .collect() + } else { + let mask = projection + .contribution_support() + .get(draft.semantic_contribution_id as usize) + .ok_or_else(|| invalid("semantic contribution support is absent"))?; + checked_projection_mask(mask, word_count, "contribution")? + }; + contributions.push(mask); + } + + let finalizations = finalization_drafts + .iter() + .map(|draft| { + let finalization_id = program + .currents() + .get(draft.semantic_current_id as usize) + .ok_or_else(|| invalid("finalization current is absent"))? + .finalization_id() + .ok_or_else(|| invalid("finalization current has no semantic finalization"))?; + let mask = projection + .finalization_support() + .get(finalization_id as usize) + .ok_or_else(|| invalid("semantic finalization support is absent"))?; + checked_projection_mask(mask, word_count, "finalization") + }) + .collect::>>()?; + let closures = closure_drafts + .iter() + .map(|draft| { + let mask = projection + .closure_support() + .get(draft.semantic_closure_id as usize) + .ok_or_else(|| invalid("semantic closure support is absent"))?; + checked_projection_mask(mask, word_count, "closure") + }) + .collect::>>()?; + + Ok(LoweredHelicityRowSupport { + resolved_helicity_count: projection.resolved_helicity_count(), + contributions, + finalizations, + closures, + }) +} + +#[derive(Default)] +struct HelicitySupportDomainInterner { + ids: BTreeMap, u32>, + descriptors: Vec, + words: Vec, +} + +impl HelicitySupportDomainInterner { + fn intern(&mut self, words: &[u64]) -> RusticolResult { + let retained = words + .iter() + .rposition(|word| *word != 0) + .map_or(0, |index| index + 1); + let canonical = &words[..retained]; + if let Some(domain_id) = self.ids.get(canonical) { + return Ok(*domain_id); + } + let domain_id = u32_len("helicity support domain", self.descriptors.len())?; + let word_start = u64::try_from(self.words.len()) + .map_err(|_| invalid("helicity support-word table exceeds u64"))?; + let word_count = u32_len("helicity support word", canonical.len())?; + self.words.extend_from_slice(canonical); + self.descriptors + .push(DirectHelicitySupportDomainDescriptor { + word_start, + word_count, + }); + self.ids.insert(canonical.to_vec(), domain_id); + Ok(domain_id) + } +} + +fn role_helicity_support( + support: &LoweredHelicityRowSupport, + role: DirectExecutorRole, +) -> RusticolResult<&[Vec]> { + match role { + DirectExecutorRole::Contribution => Ok(&support.contributions), + DirectExecutorRole::Finalization => Ok(&support.finalizations), + DirectExecutorRole::Closure => Ok(&support.closures), + DirectExecutorRole::Source => Err(invalid( + "source rows must not be lowered into helicity-dispatch groups", + )), + } +} + +fn lower_direct_helicity_dispatch( + plan: &DirectRecurrencePlan, + support: LoweredHelicityRowSupport, +) -> RusticolResult { + if plan.strategy() != RecurrenceStrategy::AllFlowUnion + || support.resolved_helicity_count as usize != plan.resolved_helicities().len() + { + return Err(invalid( + "helicity dispatch lowering requires one matching all-flow resolved axis", + )); + } + for (label, actual, expected) in [ + ( + "contribution", + support.contributions.len(), + plan.contributions().len(), + ), + ( + "finalization", + support.finalizations.len(), + plan.finalizations().len(), + ), + ("closure", support.closures.len(), plan.closures().len()), + ] { + if actual != expected { + return Err(invalid(format!( + "helicity {label} support has {actual} rows, expected {expected}" + ))); + } + } + + let mut interner = HelicitySupportDomainInterner::default(); + let mut row_groups = Vec::new(); + let mut group_masks = Vec::>::new(); + for base in plan + .row_groups() + .iter() + .filter(|group| group.role != DirectExecutorRole::Source) + { + let role_support = role_helicity_support(&support, base.role)?; + let start = usize::try_from(base.row_start) + .map_err(|_| invalid("base helicity row-group start exceeds usize"))?; + let end = start + .checked_add(base.row_count as usize) + .ok_or_else(|| invalid("base helicity row-group range overflows usize"))?; + let rows = role_support + .get(start..end) + .ok_or_else(|| invalid("base helicity row-group range is out of bounds"))?; + let mut run_start = 0usize; + while run_start < rows.len() { + let mut run_end = run_start + 1; + while run_end < rows.len() && rows[run_end] == rows[run_start] { + run_end += 1; + } + let support_domain_id = interner.intern(&rows[run_start])?; + row_groups.push(DirectHelicityRowGroupDescriptor { + stage: base.stage, + role: base.role, + direct_executor_id: base.direct_executor_id, + row_start: base + .row_start + .checked_add(run_start as u64) + .ok_or_else(|| invalid("split helicity row-group start overflows u64"))?, + row_count: u32_len("split helicity row-group row", run_end - run_start)?, + support_domain_id, + }); + group_masks.push(rows[run_start].clone()); + run_start = run_end; + } + } + + let resolved_count = support.resolved_helicity_count as usize; + let mut groups_by_helicity = vec![Vec::::new(); resolved_count]; + for (group_id, words) in group_masks.iter().enumerate() { + for (word_index, mut word) in words.iter().copied().enumerate() { + while word != 0 { + let bit = word.trailing_zeros() as usize; + let helicity_id = word_index + .checked_mul(u64::BITS as usize) + .and_then(|base| base.checked_add(bit)) + .ok_or_else(|| invalid("helicity support bit index overflows usize"))?; + let groups = groups_by_helicity + .get_mut(helicity_id) + .ok_or_else(|| invalid("helicity support mask exceeds the resolved axis"))?; + groups.push(u32_len("split helicity row-group", group_id)?); + word &= word - 1; + } + } + } + let mut dispatches = Vec::with_capacity(resolved_count); + let mut dispatch_group_ids = Vec::new(); + for (helicity_id, group_ids) in groups_by_helicity.into_iter().enumerate() { + let group_id_start = u64::try_from(dispatch_group_ids.len()) + .map_err(|_| invalid("helicity dispatch CSR exceeds u64"))?; + let group_id_count = u32_len("helicity dispatch group ID", group_ids.len())?; + dispatch_group_ids.extend(group_ids); + dispatches.push(DirectHelicityDispatchDescriptor { + resolved_helicity_id: u32_len("resolved helicity", helicity_id)?, + group_id_start, + group_id_count, + }); + } + + DirectHelicityDispatch::new(DirectHelicityDispatchParts { + runtime_layout_digest: plan.runtime_layout_digest(), + source_row_count: plan.sources().len() as u64, + contribution_row_count: plan.contributions().len() as u64, + finalization_row_count: plan.finalizations().len() as u64, + closure_row_count: plan.closures().len() as u64, + amplitude_destination_count: plan.amplitude_destination_count(), + direct_executor_count: plan.direct_executor_count(), + resolved_helicity_count: support.resolved_helicity_count, + support_domains: interner.descriptors, + support_words: interner.words, + row_groups, + dispatches, + dispatch_group_ids, + }) +} + +#[allow(clippy::too_many_arguments)] +fn apply_recurrence_relation_discovery( + program: &RecurrenceProgram, + semantic_digest: SemanticDigest, + arena: &DirectArenaLayout, + momentum_ids: &BTreeMap, + current_selector_domain_ids: &[u32], + contribution_drafts: &mut Vec, + finalization_drafts: &[FinalizationDraft], + exact_factors: &mut Vec, + exact_factor_ids: &mut BTreeMap, + options: &RecurrenceRelationDiscoveryOptions, +) -> RusticolResult> { + if options.mode == RecurrenceRelationDiscoveryMode::Off { + return Ok(None); + } + let contribution_count_before = contribution_drafts.len(); + let mut contribution_by_current = + vec![Vec::::new(); program.currents().len()]; + for draft in contribution_drafts.iter() { + let parent_count = usize::from(draft.semantic_parent_count); + let parent_current_ids = draft + .semantic_parent_current_ids + .get(..parent_count) + .ok_or_else(|| invalid("relation draft parent range is invalid"))? + .to_vec(); + let parent_momentum_form_ids = match parent_count { + 1 => vec![draft.row.parent0_momentum_form_id], + 2 => vec![ + draft.row.parent0_momentum_form_id, + draft.row.parent1_momentum_form_id_or_sentinel, + ], + _ => { + return Err(invalid(format!( + "relation draft {} has unsupported arity {parent_count}", + draft.semantic_contribution_id + ))); + } + }; + contribution_by_current + .get_mut(draft.semantic_result_current_id as usize) + .ok_or_else(|| invalid("relation result current is absent"))? + .push(RelationTermInput { + executor_id: draft.executor_id, + parent_current_ids: parent_current_ids.into_boxed_slice(), + parent_momentum_form_ids: parent_momentum_form_ids.into_boxed_slice(), + exact_factor: *exact_factors + .get(draft.row.exact_factor_id as usize) + .ok_or_else(|| invalid("relation contribution factor is absent"))?, + }); + } + let finalization_by_current = finalization_drafts + .iter() + .map(|draft| (draft.semantic_current_id, draft)) + .collect::>(); + let inputs = program + .currents() + .iter() + .map(|current| { + let assignment = arena_assignment(arena, current.id())?; + let finalization = finalization_by_current.get(¤t.id()).copied(); + Ok(RelationCurrentInput { + current_id: current.id(), + is_source: current.is_source(), + contract: RelationCurrentContract { + stage: current_stage(current)?, + state_template_id: current.key().current_state_template_id(), + component_count: u16::try_from(assignment.component_count).map_err(|_| { + invalid(format!( + "relation current {} component count exceeds u16", + current.id() + )) + })?, + momentum_form_id: momentum_id(momentum_ids, current.key().momentum())?, + selector_domain_id: *current_selector_domain_ids + .get(current.id() as usize) + .ok_or_else(|| invalid("relation selector domain is absent"))?, + finalization_executor_id: finalization + .map(|draft| draft.executor_id) + .unwrap_or(DIRECT_NONE_U32), + finalization_momentum_form_id: finalization + .map(|draft| draft.row.momentum_form_id) + .unwrap_or(DIRECT_NONE_U32), + finalization_exact_factor: finalization + .map(|draft| { + exact_factors + .get(draft.row.exact_factor_id as usize) + .copied() + .ok_or_else(|| invalid("relation finalization factor is absent")) + }) + .transpose()? + .unwrap_or(ExactComplexRational::ONE), + }, + terms: std::mem::take( + contribution_by_current + .get_mut(current.id() as usize) + .ok_or_else(|| invalid("relation current term vector is absent"))?, + ) + .into_boxed_slice(), + }) + }) + .collect::>>()?; + let (outcome, certificate_algorithm, tested_hypothesis_count, verification_rejected_count) = + if let Some(evidence) = options.numerical_evidence.as_ref() { + if evidence.schedule_semantic_digest != semantic_digest.to_string() { + return Err(invalid( + "numerical evidence selects a different semantic schedule", + )); + } + let expected_candidate_count = evidence + .mappings + .len() + .checked_add(evidence.verification_rejected_count) + .ok_or_else(|| invalid("numerical evidence candidate count overflows usize"))?; + if evidence.numerical_candidate_count != expected_candidate_count + || evidence.tested_hypothesis_count < evidence.numerical_candidate_count + || evidence.precision_digits != options.precision_digits + || evidence.probe_count != options.probe_count + || evidence.verification_probe_count != options.verification_probe_count + || evidence.relative_tolerance.to_bits() != options.relative_tolerance.to_bits() + || evidence.absolute_tolerance.to_bits() != options.absolute_tolerance.to_bits() + || evidence.seed != options.seed + { + return Err(invalid( + "numerical evidence probe contract or counters are inconsistent", + )); + } + for (label, value) in [ + ( + "baseline runtime-layout digest", + evidence.baseline_runtime_layout_digest.as_str(), + ), + ( + "source-semantics digest", + evidence.source_semantics_sha256.as_str(), + ), + ( + "certificate-set digest", + evidence.certificate_set_sha256.as_str(), + ), + ( + "runtime-parameter-schema digest", + evidence.runtime_parameter_schema_sha256.as_str(), + ), + ( + "candidate observation-batch digest", + evidence.candidate_observation_batch_sha256.as_str(), + ), + ( + "verification observation-batch digest", + evidence.verification_observation_batch_sha256.as_str(), + ), + ("decision digest", evidence.decision_sha256.as_str()), + ( + "rejection decision digest", + evidence.rejection_decision_sha256.as_str(), + ), + ] { + if !is_sha256(value) { + return Err(invalid(format!( + "numerical evidence {label} is not a lowercase SHA-256" + ))); + } + } + if evidence + .mappings + .windows(2) + .any(|pair| pair[0].current_id >= pair[1].current_id) + { + return Err(invalid( + "numerical evidence mappings are not in increasing current order", + )); + } + let mut relations = Vec::with_capacity(evidence.mappings.len()); + for mapping in &evidence.mappings { + let current = inputs + .get(mapping.current_id as usize) + .ok_or_else(|| invalid("numerical evidence current is absent"))?; + let representative = inputs + .get(mapping.execution_representative_id as usize) + .ok_or_else(|| { + invalid("numerical evidence execution representative is absent") + })?; + if current.current_id != mapping.current_id + || current.is_source + || representative.current_id != mapping.execution_representative_id + || current.contract != representative.contract + || mapping.current_dimension != current.contract.component_count + || !is_sha256(&mapping.certificate_proof_sha256) + || !is_sha256(&mapping.candidate_observations_sha256) + || !is_sha256(&mapping.verification_observations_sha256) + { + return Err(invalid( + "numerical evidence violates its exact current contract", + )); + } + let expected_factor = match mapping.relation_kind.as_str() { + "equal" => ExactComplexRational::ONE, + "opposite" => ExactComplexRational::ONE.checked_neg()?, + "zero" => ExactComplexRational::ZERO, + _ => { + return Err(invalid("numerical evidence relation kind is unsupported")); + } + }; + if mapping.factor != expected_factor { + return Err(invalid( + "numerical evidence relation factor is not exact ±1/zero", + )); + } + if mapping.relation_kind == "zero" { + if mapping.representative_id.is_some() + || mapping.execution_representative_id > mapping.current_id + { + return Err(invalid( + "zero-current evidence has an invalid representative", + )); + } + } else if mapping.representative_id != Some(mapping.execution_representative_id) + || mapping.execution_representative_id >= mapping.current_id + { + return Err(invalid( + "equal/opposite evidence is not topologically ordered", + )); + } + relations.push(RecurrenceCurrentRelationCertificate { + current_id: mapping.current_id, + representative_id: mapping.execution_representative_id, + factor: mapping.factor, + current_expression_sha256: mapping.candidate_observations_sha256.clone(), + representative_expression_sha256: mapping + .verification_observations_sha256 + .clone(), + proof_sha256: mapping.certificate_proof_sha256.clone(), + }); + } + ( + RecurrenceRelationDiscoveryOutcome { + relations, + numerical_candidate_count: evidence.numerical_candidate_count, + uncertified_candidate_count: evidence.verification_rejected_count, + rejected_hypothesis_count: evidence.rejected_hypothesis_count, + rejected_candidates: Vec::new(), + rejected_decision_sha256: evidence.rejection_decision_sha256.clone(), + effective_projection_count: options + .probe_count + .checked_add(options.verification_probe_count) + .ok_or_else(|| invalid("numerical probe count overflows u32"))?, + }, + evidence.certificate_algorithm.clone(), + evidence.tested_hypothesis_count, + evidence.verification_rejected_count, + ) + } else { + let outcome = discover_recurrence_current_relations(&inputs, options)?; + (outcome, relation_certificate_algorithm().to_owned(), 0, 0) + }; + + // Keep the complete independently verified evidence and report, including + // signed relations. Only their application is restricted: numerical + // opposite-current reuse is not enabled for contracted binary64 colour + // sums. Equal and zero currents do not require that signed rewrite, and + // must not be disabled merely because another certificate is opposite. + // Exact symbolic relations and diagnostic projections remain unchanged. + let restrict_numerical_sign = options.mode.applies_reuse() + && options.numerical_evidence.is_some() + && program.strategy() == RecurrenceStrategy::ContractedColorUnion; + let projected_relations = outcome.relations.iter().filter(|certificate| { + !restrict_numerical_sign + || certificate.factor == ExactComplexRational::ONE + || certificate.factor == ExactComplexRational::ZERO + }); + let projected_relation_count = projected_relations.clone().count(); + let applied_relation_count = if options.mode.applies_reuse() { + projected_relation_count + } else { + 0 + }; + let removed_contribution_count = if applied_relation_count != 0 { + let relation_by_current = projected_relations + .clone() + .map(|certificate| (certificate.current_id, certificate)) + .collect::>(); + let original_len = contribution_drafts.len(); + contribution_drafts + .retain(|draft| !relation_by_current.contains_key(&draft.semantic_result_current_id)); + let removed_contribution_count = original_len - contribution_drafts.len(); + for certificate in projected_relations.clone() { + let current = program + .currents() + .get(certificate.current_id as usize) + .ok_or_else(|| invalid("certified current is absent"))?; + let representative = program + .currents() + .get(certificate.representative_id as usize) + .ok_or_else(|| invalid("certified representative is absent"))?; + let current_assignment = arena_assignment(arena, current.id())?; + let representative_assignment = arena_assignment(arena, representative.id())?; + if current_stage(current)? != current_stage(representative)? + || current_assignment.component_count != representative_assignment.component_count + { + return Err(invalid(format!( + "certificate {} -> {} violates scale-copy layout", + current.id(), + representative.id() + ))); + } + let component_count = current_assignment.component_count; + contribution_drafts.push(ContributionDraft { + stage: current_stage(current)?, + executor_id: DIRECT_NONE_U32, + semantic_contribution_id: current.id(), + semantic_result_current_id: current.id(), + semantic_parent_current_ids: [representative.id(), DIRECT_NONE_U32], + semantic_parent_count: 1, + row: DirectContributionRow { + parent0_component_base: representative_assignment.component_base, + parent1_component_base_or_sentinel: DIRECT_NONE_U32, + // Certified-reuse rows interpret this field as the + // component count; no momentum plane is read. + parent0_momentum_form_id: component_count, + parent1_momentum_form_id_or_sentinel: DIRECT_NONE_U32, + destination_component_base: current_assignment.component_base, + exact_factor_id: intern_direct_factor( + certificate.factor, + exact_factors, + exact_factor_ids, + )?, + selector_domain_id: *current_selector_domain_ids + .get(current.id() as usize) + .ok_or_else(|| invalid("certified selector domain is absent"))?, + flags: DIRECT_CONTRIBUTION_FLAG_INITIALIZE_DESTINATION + | DIRECT_CONTRIBUTION_FLAG_CERTIFIED_REUSE, + }, + }); + } + removed_contribution_count + } else { + projected_relations + .clone() + .try_fold(0usize, |total, certificate| { + let count = usize::try_from( + program + .currents() + .get(certificate.current_id as usize) + .ok_or_else(|| invalid("diagnostic relation current is absent"))? + .fan_in(), + ) + .map_err(|_| invalid("diagnostic current fan-in exceeds usize"))?; + total + .checked_add(count) + .ok_or_else(|| invalid("diagnostic saved contribution count overflows")) + })? + }; + + let projected_contribution_count = contribution_count_before + .checked_sub(removed_contribution_count) + .and_then(|count| count.checked_add(projected_relation_count)) + .ok_or_else(|| invalid("relation contribution count overflows usize"))?; + let projected_interaction_count = contribution_count_before + .checked_sub(removed_contribution_count) + .ok_or_else(|| invalid("relation interaction count underflows"))?; + let applied = applied_relation_count != 0; + Ok(Some(RecurrenceRelationDiscoveryReport { + requested_mode: options.mode, + state: if applied { + "exact-certified-applied" + } else { + "diagnostic-only" + }, + strategy: program.strategy(), + color_accuracy: options.color_accuracy.clone(), + precision_digits: options.precision_digits, + probe_count: options.probe_count, + verification_probe_count: options.verification_probe_count, + seed: options.seed, + certificate_algorithm, + authenticated_certificate_set_sha256: options + .numerical_evidence + .as_ref() + .map(|evidence| evidence.certificate_set_sha256.clone()), + authenticated_runtime_parameter_schema_sha256: options + .numerical_evidence + .as_ref() + .map(|evidence| evidence.runtime_parameter_schema_sha256.clone()), + authenticated_candidate_observation_batch_sha256: options + .numerical_evidence + .as_ref() + .map(|evidence| evidence.candidate_observation_batch_sha256.clone()), + authenticated_verification_observation_batch_sha256: options + .numerical_evidence + .as_ref() + .map(|evidence| evidence.verification_observation_batch_sha256.clone()), + authenticated_decision_sha256: options + .numerical_evidence + .as_ref() + .map(|evidence| evidence.decision_sha256.clone()), + rejected_decision_sha256: outcome.rejected_decision_sha256, + tested_hypothesis_count, + verification_rejected_count, + rejected_hypothesis_count: outcome.rejected_hypothesis_count, + effective_projection_count: outcome.effective_projection_count, + numerical_candidate_count: outcome.numerical_candidate_count, + uncertified_candidate_count: outcome.uncertified_candidate_count, + exact_certified_relation_count: outcome.relations.len(), + applied_relation_count, + current_count_before: program.currents().len(), + // Direct-plan-v2 requires dense semantic current IDs and exact + // closure-proof bindings. Scale-copy reuse removes interaction work + // while retaining those descriptors. + current_count_after: program.currents().len(), + contribution_count_before, + contribution_count_after: projected_contribution_count, + interaction_evaluation_count_before: contribution_count_before, + interaction_evaluation_count_after: projected_interaction_count, + scale_copy_row_count: applied_relation_count, + certificates: outcome.relations, + rejected_candidates: outcome.rejected_candidates, + })) +} + +fn validate_lowering_boundary( + program: &RecurrenceProgram, + templates: &ValidatedRecurrenceTemplateInput, + direct_executors: &PreparedDirectExecutorCatalog, + prepared_pack_digest: SemanticDigest, + direct_template_catalog_digest: SemanticDigest, + runtime_options: DirectRecurrenceRuntimeOptions, +) -> RusticolResult<()> { + if runtime_options.point_tile_size == 0 || runtime_options.workspace_mib == 0 { + return Err(invalid( + "point tile size and workspace MiB must both be positive", + )); + } + let summary = templates.summary(); + if prepared_pack_digest != summary.prepared_kernel_pack_digest { + return Err(invalid(format!( + "prepared-pack digest {prepared_pack_digest} does not match validated template pack {}", + summary.prepared_kernel_pack_digest + ))); + } + if direct_template_catalog_digest != direct_executors.direct_template_catalog_digest() { + return Err(invalid(format!( + "direct-template catalog digest {direct_template_catalog_digest} does not match resolver catalog {}", + direct_executors.direct_template_catalog_digest() + ))); + } + validate_prepared_parameter_catalog(templates)?; + validate_direct_executor_catalog(templates, direct_executors)?; + for current in program.currents() { + if current.key().catalog_digest() != summary.catalog_digest { + return Err(invalid(format!( + "current {} template-catalog digest does not match the validated catalog", + current.id() + ))); + } + } + Ok(()) +} + +#[allow(clippy::too_many_arguments)] +fn lower_union_source( + current: &RecurrenceCurrent, + source_slot: u32, + dispatch_domain: u32, + advertised_source_templates: &[u32], + variant_bindings: &[RuntimeSourceVariantBinding], + templates: &ValidatedRecurrenceTemplateInput, + direct_executors: &PreparedDirectExecutorCatalog, + arena: &DirectArenaLayout, + momentum_ids: &BTreeMap, + exact_factors: &mut Vec, + exact_factor_ids: &mut BTreeMap, + source_drafts: &mut Vec, + runtime_sources: &mut LoweredRuntimeSources, +) -> RusticolResult<()> { + if current.source_exact_factor().is_some() { + return Err(invalid(format!( + "all-flow-union source current {} must take its factor from runtime dispatch", + current.id() + ))); + } + if variant_bindings.is_empty() { + return Err(invalid(format!( + "all-flow-union source current {} has no runtime variants", + current.id() + ))); + } + let input = templates.input(); + let contract = input + .runtime_helicity_contracts + .get(dispatch_domain as usize) + .ok_or_else(|| { + invalid(format!( + "source current {} references absent runtime-helicity contract {dispatch_domain}", + current.id() + )) + })?; + if contract.id != dispatch_domain { + return Err(invalid(format!( + "runtime-helicity contract {dispatch_domain} has noncanonical ID {}", + contract.id + ))); + } + if contract.full_state_template_id != current.key().current_state_template_id() { + return Err(invalid(format!( + "source current {} state does not match runtime-helicity contract {dispatch_domain}", + current.id() + ))); + } + let contract_variant_range = contract.variant_range.as_usize_range( + input.runtime_helicity_variants.len(), + "runtime-helicity contract variants", + )?; + let assignment = arena_assignment(arena, current.id())?; + let full_state = state_row(templates, contract.full_state_template_id)?; + if assignment.component_count != full_state.dimension { + return Err(invalid(format!( + "source current {} arena width does not match runtime-helicity full state", + current.id() + ))); + } + let source_row_id = u32_len("source row", source_drafts.len())?; + let source_choices = runtime_sources + .choices_by_source + .get_mut(source_slot as usize) + .ok_or_else(|| invalid(format!("source slot {source_slot} is out of range")))?; + if !source_choices.is_empty() { + return Err(invalid(format!( + "all-flow-union source slot {source_slot} has more than one dispatch current" + ))); + } + source_drafts.push(SourceDraft { + stage: current_stage(current)?, + executor_id: DIRECT_NONE_U32, + semantic_current_id: current.id(), + row: DirectSourceRow { + source_slot, + destination_component_base: assignment.component_base, + momentum_form_id: momentum_id(momentum_ids, current.key().momentum())?, + source_template_or_dispatch_domain: dispatch_domain, + spin_state_class: current.key().spin_state_class(), + exact_factor_id: intern_direct_factor( + ExactComplexRational::ONE, + exact_factors, + exact_factor_ids, + )?, + selector_domain_id: UNIVERSAL_SELECTOR_DOMAIN_ID, + }, + }); + + for binding in variant_bindings.iter().copied() { + if advertised_source_templates + .binary_search(&binding.source_template_id()) + .is_err() + { + return Err(invalid(format!( + "source current {} runtime variant {} is absent from its advertised source-template catalog", + current.id(), + binding.runtime_variant_id() + ))); + } + let variant_index = usize::try_from(binding.runtime_variant_id()) + .map_err(|_| invalid("runtime-helicity variant ID exceeds usize"))?; + if !contract_variant_range.contains(&variant_index) { + return Err(invalid(format!( + "source current {} runtime variant {} is outside contract {dispatch_domain}", + current.id(), + binding.runtime_variant_id() + ))); + } + let variant = input + .runtime_helicity_variants + .get(variant_index) + .ok_or_else(|| invalid("runtime-helicity variant is absent"))?; + if variant.id != binding.runtime_variant_id() + || variant.contract_id != dispatch_domain + || variant.source_template_id != binding.source_template_id() + || variant.source_state_template_id != binding.source_state_template_id() + { + return Err(invalid(format!( + "source current {} runtime variant {} disagrees with its prepared contract", + current.id(), + binding.runtime_variant_id() + ))); + } + let source = input + .sources + .get(binding.source_template_id() as usize) + .ok_or_else(|| invalid("runtime source template is absent"))?; + if source.id != binding.source_template_id() + || source.state_template_id != binding.source_state_template_id() + { + return Err(invalid(format!( + "runtime source template {} has a stale state contract", + binding.source_template_id() + ))); + } + state_row(templates, binding.crossed_state_template_id())?; + let evaluator_binding_id = validated_evaluator_binding_id( + templates, + source.evaluator_binding_id, + EvaluatorContractKind::Source, + "runtime source", + )?; + let direct_executor_id = + direct_executors.resolve_evaluator(DirectExecutorRole::Source, evaluator_binding_id)?; + + let effective_variant = effective_runtime_helicity_variant( + input, + contract_variant_range.clone(), + binding.crossed_state_template_id(), + )?; + let embedding_start = u64::try_from(runtime_sources.embeddings.len()) + .map_err(|_| invalid("runtime source embeddings exceed u64"))?; + let embedding_range = effective_variant.embedding_range.as_usize_range( + input.runtime_helicity_embeddings.len(), + "runtime source embeddings", + )?; + for embedding in &input.runtime_helicity_embeddings[embedding_range] { + if embedding.variant_id != effective_variant.id { + return Err(invalid(format!( + "runtime variant {} owns a stale embedding row", + effective_variant.id + ))); + } + let factor = template_exact_factor(templates, embedding.factor_id)?; + runtime_sources.embeddings.push(DirectSourceEmbeddingRow { + full_component: embedding.full_component, + source_component_or_sentinel: if embedding.source_component == MISSING_U32 { + DIRECT_NONE_U32 + } else { + embedding.source_component + }, + exact_factor_id: intern_direct_factor(factor, exact_factors, exact_factor_ids)?, + }); + } + + let projection_start = u64::try_from(runtime_sources.projections.len()) + .map_err(|_| invalid("runtime source projections exceed u64"))?; + let projection_range = effective_variant.projection_range.as_usize_range( + input.runtime_helicity_projections.len(), + "runtime source projections", + )?; + for projection in &input.runtime_helicity_projections[projection_range] { + if projection.variant_id != effective_variant.id { + return Err(invalid(format!( + "runtime variant {} owns a stale projection row", + effective_variant.id + ))); + } + runtime_sources.projections.push(DirectSourceProjectionRow { + source_component: projection.source_component, + full_component: projection.full_component, + }); + } + + let dispatch_variant_id = u32_len( + "lowered source dispatch variant", + runtime_sources.variants.len(), + )?; + runtime_sources + .variants + .push(DirectSourceDispatchVariantDescriptor { + embedding_start, + projection_start, + source_row_id, + dispatch_domain_id: dispatch_domain, + runtime_variant_id: binding.runtime_variant_id(), + source_state_index: binding.source_state_index(), + source_template_id: binding.source_template_id(), + source_state_template_id: binding.source_state_template_id(), + crossed_state_template_id: binding.crossed_state_template_id(), + crossed_spin_state_class: binding.crossed_spin_state_class(), + direct_executor_id, + crossing_exact_factor_id: intern_direct_factor( + binding.crossing_factor(), + exact_factors, + exact_factor_ids, + )?, + embedding_count: u32_len( + "runtime source embedding", + runtime_sources.embeddings.len() + - usize::try_from(embedding_start) + .map_err(|_| invalid("embedding start exceeds usize"))?, + )?, + projection_count: u32_len( + "runtime source projection", + runtime_sources.projections.len() + - usize::try_from(projection_start) + .map_err(|_| invalid("projection start exceeds usize"))?, + )?, + }); + source_choices.push(RuntimeSourceChoice { + source_state_index: binding.source_state_index(), + public_helicity: binding.public_helicity(), + dispatch_variant_id, + }); + } + source_choices.sort_by_key(|choice| choice.source_state_index); + if source_choices + .windows(2) + .any(|pair| pair[0].source_state_index == pair[1].source_state_index) + { + return Err(invalid(format!( + "all-flow-union source slot {source_slot} repeats a source-state variant" + ))); + } + Ok(()) +} + +pub(super) fn effective_runtime_helicity_variant( + input: &OwnedRecurrenceTemplateInput, + contract_variant_range: std::ops::Range, + crossed_state_template_id: u32, +) -> RusticolResult { + let candidates = input.runtime_helicity_variants[contract_variant_range] + .iter() + .copied() + .filter(|variant| variant.source_state_template_id == crossed_state_template_id) + .collect::>(); + let Some(representative) = candidates.first().copied() else { + return Err(invalid(format!( + "runtime-helicity contract has no embedding for crossed state {crossed_state_template_id}" + ))); + }; + for candidate in candidates.iter().copied().skip(1) { + if !runtime_helicity_variants_share_embedding(input, representative, candidate)? { + return Err(invalid(format!( + "runtime-helicity contract gives crossed state {crossed_state_template_id} incompatible embeddings" + ))); + } + } + Ok(representative) +} + +fn runtime_helicity_variants_share_embedding( + input: &OwnedRecurrenceTemplateInput, + left: RuntimeHelicityVariantRow, + right: RuntimeHelicityVariantRow, +) -> RusticolResult { + let left_embeddings = left.embedding_range.as_usize_range( + input.runtime_helicity_embeddings.len(), + "left runtime source embeddings", + )?; + let right_embeddings = right.embedding_range.as_usize_range( + input.runtime_helicity_embeddings.len(), + "right runtime source embeddings", + )?; + let left_embeddings = &input.runtime_helicity_embeddings[left_embeddings]; + let right_embeddings = &input.runtime_helicity_embeddings[right_embeddings]; + if left_embeddings.len() != right_embeddings.len() + || !left_embeddings + .iter() + .zip(right_embeddings) + .all(|(left, right)| { + left.full_component == right.full_component + && left.source_component == right.source_component + && left.factor_id == right.factor_id + }) + { + return Ok(false); + } + + let left_projections = left.projection_range.as_usize_range( + input.runtime_helicity_projections.len(), + "left runtime source projections", + )?; + let right_projections = right.projection_range.as_usize_range( + input.runtime_helicity_projections.len(), + "right runtime source projections", + )?; + let left_projections = &input.runtime_helicity_projections[left_projections]; + let right_projections = &input.runtime_helicity_projections[right_projections]; + Ok(left_projections.len() == right_projections.len() + && left_projections + .iter() + .zip(right_projections) + .all(|(left, right)| { + left.source_component == right.source_component + && left.full_component == right.full_component + })) +} + +fn validate_prepared_parameter_catalog( + templates: &ValidatedRecurrenceTemplateInput, +) -> RusticolResult<()> { + let input = templates.input(); + // Prepared IDs address the model-wide parameter arena. A process may + // reference only a sparse subset of those slots, especially for UFO + // models, so only bounded uniqueness is required here. + let prepared_parameter_count = input.parameters.len(); + let mut seen = vec![false; prepared_parameter_count]; + for parameter in &input.parameters { + if parameter.prepared_parameter_id == MISSING_U32 { + continue; + } + let slot = usize::try_from(parameter.prepared_parameter_id) + .map_err(|_| invalid("prepared parameter slot exceeds usize"))?; + let present = seen.get_mut(slot).ok_or_else(|| { + invalid(format!( + "parameter template {} prepared runtime slot {} is out of bounds {prepared_parameter_count}", + parameter.id, parameter.prepared_parameter_id + )) + })?; + if std::mem::replace(present, true) { + return Err(invalid(format!( + "prepared runtime parameter slot {} is assigned more than once", + parameter.prepared_parameter_id + ))); + } + } + Ok(()) +} + +fn validate_direct_executor_catalog( + templates: &ValidatedRecurrenceTemplateInput, + catalog: &PreparedDirectExecutorCatalog, +) -> RusticolResult<()> { + let evaluators = &templates.input().evaluator_bindings; + for key in catalog.bindings.keys().copied() { + let PreparedDirectExecutorKey::Evaluator { + role, + evaluator_binding_id, + } = key + else { + continue; + }; + let evaluator = evaluators + .get(evaluator_binding_id as usize) + .ok_or_else(|| { + invalid(format!( + "prepared direct-template mapping references absent evaluator binding {evaluator_binding_id}" + )) + })?; + let expected_role = direct_role_for_contract(EvaluatorContractKind::try_from( + evaluator.contract_kind, + )?)? + .ok_or_else(|| { + invalid(format!( + "model-parameter evaluator binding {evaluator_binding_id} cannot be a direct row executor" + )) + })?; + if role != expected_role { + return Err(invalid(format!( + "prepared direct-template mapping for evaluator binding {evaluator_binding_id} has role {role:?}, expected {expected_role:?}" + ))); + } + } + for evaluator in evaluators { + let Some(role) = + direct_role_for_contract(EvaluatorContractKind::try_from(evaluator.contract_kind)?)? + else { + continue; + }; + catalog.resolve_evaluator(role, evaluator.id)?; + } + Ok(()) +} + +fn direct_role_for_contract( + contract: EvaluatorContractKind, +) -> RusticolResult> { + Ok(match contract { + EvaluatorContractKind::Source => Some(DirectExecutorRole::Source), + EvaluatorContractKind::Vertex => Some(DirectExecutorRole::Contribution), + EvaluatorContractKind::Propagator => Some(DirectExecutorRole::Finalization), + EvaluatorContractKind::Closure => Some(DirectExecutorRole::Closure), + EvaluatorContractKind::ModelParameter => None, + }) +} + +fn component_counts( + program: &RecurrenceProgram, + templates: &ValidatedRecurrenceTemplateInput, +) -> RusticolResult> { + program + .currents() + .iter() + .map(|current| { + let dimension = + state_row(templates, current.key().current_state_template_id())?.dimension; + u16::try_from(dimension).map_err(|_| { + invalid(format!( + "current {} state dimension {dimension} exceeds the direct-row u16 component domain", + current.id() + )) + })?; + Ok(dimension) + }) + .collect() +} + +fn state_row( + templates: &ValidatedRecurrenceTemplateInput, + state_template_id: u32, +) -> RusticolResult<&super::template::CurrentStateRow> { + templates + .input() + .current_states + .get(state_template_id as usize) + .ok_or_else(|| { + invalid(format!( + "state template {state_template_id} is absent from the validated catalog" + )) + }) +} + +fn validated_evaluator_binding_id( + templates: &ValidatedRecurrenceTemplateInput, + evaluator_binding_id: u32, + expected_contract: EvaluatorContractKind, + role: &str, +) -> RusticolResult { + if evaluator_binding_id == MISSING_U32 { + return Err(invalid(format!( + "{role} has no prepared evaluator binding to use as a provisional direct executor" + ))); + } + let binding = templates + .input() + .evaluator_bindings + .get(evaluator_binding_id as usize) + .ok_or_else(|| { + invalid(format!( + "{role} references absent evaluator binding {evaluator_binding_id}" + )) + })?; + if binding.id != evaluator_binding_id + || EvaluatorContractKind::try_from(binding.contract_kind)? != expected_contract + { + return Err(invalid(format!( + "{role} evaluator binding {evaluator_binding_id} has the wrong direct role" + ))); + } + + Ok(evaluator_binding_id) +} + +fn template_u32_sequence<'a>( + templates: &'a ValidatedRecurrenceTemplateInput, + sequence_id: u32, + label: &str, +) -> RusticolResult<&'a [u32]> { + let input = templates.input(); + let range = input + .u32_sequence_ranges + .get(sequence_id as usize) + .ok_or_else(|| invalid(format!("{label} sequence {sequence_id} is absent")))?; + if range.id != sequence_id { + return Err(invalid(format!( + "{label} sequence {sequence_id} has noncanonical ID {}", + range.id + ))); + } + let range = range + .range + .as_usize_range(input.u32_sequence_values.len(), label)?; + Ok(&input.u32_sequence_values[range]) +} + +fn validate_coupling_digest( + templates: &ValidatedRecurrenceTemplateInput, + contribution: &super::RecurrenceContribution, +) -> RusticolResult<()> { + let flow_id = contribution.key().quantum_flow_witness_id(); + let flow = templates + .input() + .quantum_flows + .get(flow_id as usize) + .ok_or_else(|| { + invalid(format!( + "contribution {} references absent quantum-flow witness {flow_id}", + contribution.id() + )) + })?; + let digest = templates + .input() + .digest_catalog + .get(flow.semantic_digest_id as usize) + .ok_or_else(|| invalid("quantum-flow semantic digest is absent"))?; + let digest = SemanticDigest::new(digest.value)?; + if digest != contribution.key().runtime_coupling_binding_digest() { + return Err(invalid(format!( + "contribution {} runtime coupling digest does not match quantum flow {flow_id}", + contribution.id() + ))); + } + Ok(()) +} + +fn parent_state_template_ids( + program: &RecurrenceProgram, + parent_ids: &[u32], +) -> RusticolResult> { + parent_ids + .iter() + .map(|&parent_id| { + program + .currents() + .get(parent_id as usize) + .map(|parent| parent.key().current_state_template_id()) + .ok_or_else(|| invalid(format!("parent current {parent_id} is absent"))) + }) + .collect() +} + +fn parent_state_contract_matches( + concrete_states: &[u32], + canonical_input_order: &[u32], + exchange_is_proven: bool, + actual_states: &[u32], +) -> RusticolResult { + let [left, right] = concrete_states else { + return Err(invalid("direct parent-state contract is not binary")); + }; + let canonical = match canonical_input_order { + [0, 1] => [*left, *right], + [1, 0] => [*right, *left], + _ => { + return Err(invalid( + "direct canonical input order is not a binary permutation", + )); + } + }; + Ok(actual_states == canonical + || (exchange_is_proven && actual_states == [canonical[1], canonical[0]])) +} + +fn validate_parent_momentum_contract( + program: &RecurrenceProgram, + contribution: &super::RecurrenceContribution, +) -> RusticolResult<()> { + if contribution.key().parent_momenta().len() != contribution.parent_current_ids().len() { + return Err(invalid(format!( + "contribution {} parent-momentum arity is inconsistent", + contribution.id() + ))); + } + for (ordinal, (&parent_id, expected)) in contribution + .parent_current_ids() + .iter() + .zip(contribution.key().parent_momenta()) + .enumerate() + { + let parent = program + .currents() + .get(parent_id as usize) + .ok_or_else(|| invalid(format!("parent current {parent_id} is absent")))?; + if parent.key().momentum() != expected { + return Err(invalid(format!( + "contribution {} parent {ordinal} momentum does not match current {parent_id}", + contribution.id() + ))); + } + } + Ok(()) +} + +fn validate_direct_arity(label: &str, id: u32, arity: usize) -> RusticolResult<()> { + if !(1..=2).contains(&arity) { + return Err(invalid(format!( + "{label} {id} has unsupported parent arity {arity}; direct plan-v2 supports only one or two parents" + ))); + } + Ok(()) +} + +fn direct_parents( + program: &RecurrenceProgram, + parent_ids: &[u32], + arena: &DirectArenaLayout, + momentum_ids: &BTreeMap, +) -> RusticolResult { + validate_direct_arity("direct row", 0, parent_ids.len())?; + let parent0 = program + .currents() + .get(parent_ids[0] as usize) + .ok_or_else(|| invalid("direct parent 0 current is absent"))?; + let parent0_assignment = arena_assignment(arena, parent0.id())?; + let (parent1_component_base_or_sentinel, parent1_momentum_form_id_or_sentinel) = + if let Some(&parent1_id) = parent_ids.get(1) { + let parent1 = program + .currents() + .get(parent1_id as usize) + .ok_or_else(|| invalid("direct parent 1 current is absent"))?; + ( + arena_assignment(arena, parent1.id())?.component_base, + momentum_id(momentum_ids, parent1.key().momentum())?, + ) + } else { + (DIRECT_NONE_U32, DIRECT_NONE_U32) + }; + Ok(DirectParents { + parent0_component_base: parent0_assignment.component_base, + parent1_component_base_or_sentinel, + parent0_momentum_form_id: momentum_id(momentum_ids, parent0.key().momentum())?, + parent1_momentum_form_id_or_sentinel, + }) +} + +fn external_source_count(program: &RecurrenceProgram) -> RusticolResult { + let slots = program + .currents() + .iter() + .filter(|current| current.is_source()) + .map(only_source_slot) + .collect::>>()?; + let count = slots + .iter() + .next_back() + .copied() + .and_then(|slot| slot.checked_add(1)) + .ok_or_else(|| invalid("semantic program contains no source currents"))?; + if slots.iter().copied().ne(0..count) { + return Err(invalid( + "source currents do not cover a contiguous external-source domain", + )); + } + Ok(count) +} + +fn only_source_slot(current: &RecurrenceCurrent) -> RusticolResult { + let [source_slot] = current.key().support_source_slots() else { + return Err(invalid(format!( + "source current {} does not cover exactly one source slot", + current.id() + ))); + }; + Ok(*source_slot) +} + +fn current_stage(current: &RecurrenceCurrent) -> RusticolResult { + let stage = current + .key() + .support_source_slots() + .len() + .checked_sub(1) + .ok_or_else(|| invalid(format!("current {} has empty source support", current.id())))?; + u16::try_from(stage).map_err(|_| invalid(format!("current {} stage exceeds u16", current.id()))) +} + +fn direct_event(event: u64, label: &str) -> RusticolResult { + let event = event + .checked_sub(1) + .ok_or_else(|| invalid(format!("{label} precedes direct stage zero")))?; + u32::try_from(event).map_err(|_| invalid(format!("{label} exceeds u32"))) +} + +fn arena_assignment( + arena: &DirectArenaLayout, + current_id: u32, +) -> RusticolResult { + arena.assignment(current_id).ok_or_else(|| { + invalid(format!( + "arena assignment for semantic current {current_id} is absent" + )) + }) +} + +type DirectMomentumTables = ( + BTreeMap, + Vec, + Vec, +); + +fn intern_momentum_forms( + program: &RecurrenceProgram, + external_source_count: u32, +) -> RusticolResult { + let forms = program + .currents() + .iter() + .map(|current| current.key().momentum().clone()) + .collect::>(); + let mut ids = BTreeMap::new(); + let mut descriptors = Vec::with_capacity(forms.len()); + let mut terms = Vec::new(); + for form in forms { + for term in form.terms() { + if term.source_slot >= external_source_count { + return Err(invalid(format!( + "momentum form references source slot {} outside the source domain", + term.source_slot + ))); + } + } + let id = u32_len("momentum form", descriptors.len())?; + let term_start = + u64::try_from(terms.len()).map_err(|_| invalid("momentum term table exceeds u64"))?; + terms.extend(form.terms().iter().map(|term| DirectMomentumTerm { + source_slot: term.source_slot, + coefficient: term.coefficient, + })); + descriptors.push(DirectMomentumFormDescriptor { + term_start, + term_count: u32_len("momentum term", form.terms().len())?, + }); + ids.insert(form, id); + } + Ok((ids, descriptors, terms)) +} + +fn momentum_id( + ids: &BTreeMap, + form: &CanonicalMomentumLinearForm, +) -> RusticolResult { + ids.get(form) + .copied() + .ok_or_else(|| invalid("semantic momentum form was not interned")) +} + +fn intern_exact_factors( + program: &RecurrenceProgram, +) -> RusticolResult<( + Vec, + BTreeMap, +)> { + let mut unique = BTreeSet::from([ExactComplexRational::ONE]); + unique.extend( + program + .currents() + .iter() + .filter_map(RecurrenceCurrent::source_exact_factor), + ); + unique.extend( + program + .contributions() + .iter() + .map(super::RecurrenceContribution::exact_factor), + ); + unique.extend( + program + .finalizations() + .iter() + .map(super::RecurrenceFinalization::exact_factor), + ); + unique.extend( + program + .closure_terms() + .iter() + .map(super::RecurrenceClosureTerm::exact_factor), + ); + unique.extend( + program + .replay_targets() + .iter() + .map(super::RecurrenceReplayTarget::amplitude_factor), + ); + + let mut factors = vec![ExactComplexRational::ONE]; + factors.extend( + unique + .into_iter() + .filter(|factor| *factor != ExactComplexRational::ONE), + ); + let ids = factors + .iter() + .copied() + .enumerate() + .map(|(id, factor)| Ok((factor, u32_len("exact factor", id)?))) + .collect::>>()?; + Ok((factors, ids)) +} + +fn exact_factor_id( + ids: &BTreeMap, + factor: ExactComplexRational, +) -> RusticolResult { + ids.get(&factor) + .copied() + .ok_or_else(|| invalid("semantic exact factor was not interned")) +} + +fn intern_direct_factor( + factor: ExactComplexRational, + factors: &mut Vec, + ids: &mut BTreeMap, +) -> RusticolResult { + if let Some(id) = ids.get(&factor).copied() { + return Ok(id); + } + let id = u32_len("exact factor", factors.len())?; + factors.push(factor); + ids.insert(factor, id); + Ok(id) +} + +fn template_exact_factor( + templates: &ValidatedRecurrenceTemplateInput, + factor_id: u32, +) -> RusticolResult { + let input = templates.input(); + let row = input + .exact_factors + .get(factor_id as usize) + .ok_or_else(|| invalid(format!("template exact factor {factor_id} is absent")))?; + if row.id != factor_id { + return Err(invalid(format!( + "template exact factor {factor_id} has noncanonical ID {}", + row.id + ))); + } + ExactComplexRational::parse_parts( + template_string(input, row.real_numerator_string_id, "exact real numerator")?, + template_string( + input, + row.real_denominator_string_id, + "exact real denominator", + )?, + template_string( + input, + row.imag_numerator_string_id, + "exact imaginary numerator", + )?, + template_string( + input, + row.imag_denominator_string_id, + "exact imaginary denominator", + )?, + ) + .map_err(|error| { + invalid(format!( + "template exact factor {factor_id} is invalid: {error}" + )) + }) +} + +fn template_string<'a>( + input: &'a super::template::OwnedRecurrenceTemplateInput, + string_id: u32, + label: &str, +) -> RusticolResult<&'a str> { + let range = input + .string_ranges + .get(string_id as usize) + .ok_or_else(|| invalid(format!("{label} string {string_id} is absent")))?; + let bytes = &input.string_bytes[range.as_usize_range(input.string_bytes.len(), label)?]; + std::str::from_utf8(bytes) + .map_err(|error| invalid(format!("{label} string {string_id} is not UTF-8: {error}"))) +} + +fn intern_closure_factor_block( + coefficients: &[ExactComplexRational], + factors: &mut Vec, + blocks: &mut BTreeMap, (u32, u16)>, +) -> RusticolResult<(u32, u16)> { + if let Some(block) = blocks.get(coefficients) { + return Ok(*block); + } + let start = u32_len("closure component-factor start", factors.len())?; + let count = u16::try_from(coefficients.len()) + .map_err(|_| invalid("closure component count exceeds u16"))?; + if count == 0 { + return Err(invalid("closure component-factor block is empty")); + } + factors.extend_from_slice(coefficients); + let block = (start, count); + blocks.insert(coefficients.to_vec(), block); + Ok(block) +} + +fn lower_amplitude_destinations( + program: &RecurrenceProgram, + closures: &[ClosureDraft], + destination_domain_ids: &[u32], +) -> RusticolResult> { + let mut next_closure = 0usize; + let mut result = Vec::with_capacity(program.amplitude_destinations().len()); + for destination in program.amplitude_destinations().iter().copied() { + let start = next_closure; + while next_closure < closures.len() + && closures[next_closure].amplitude_destination_id == destination.id() + { + next_closure += 1; + } + let count = next_closure - start; + if count as u64 != destination.closure_range().count { + return Err(invalid(format!( + "amplitude destination {} lowered {count} closure rows, expected {}", + destination.id(), + destination.closure_range().count + ))); + } + result.push(DirectAmplitudeDestinationDescriptor { + closure_row_start: u64::try_from(start) + .map_err(|_| invalid("closure row start exceeds u64"))?, + id: destination.id(), + target_sector_id: destination.target_sector_id(), + target_helicity_id_or_sentinel: destination + .target_helicity_id() + .unwrap_or(DIRECT_NONE_U32), + closure_row_count: u32_len("amplitude-destination closure row", count)?, + selector_domain_id: *destination_domain_ids + .get(destination.id() as usize) + .ok_or_else(|| invalid("amplitude destination selector domain is absent"))?, + }); + } + if next_closure != closures.len() { + return Err(invalid( + "closure rows are not fully owned by amplitude destinations", + )); + } + Ok(result) +} + +type DirectReplayTables = ( + Vec, + Vec, + Vec, + Vec, +); + +fn lower_replay_targets( + program: &RecurrenceProgram, + exact_factor_ids: &BTreeMap, + sector_domain_ids: &[Option], +) -> RusticolResult { + if !program.strategy().uses_topology_replay_targets() { + if !program.replay_targets().is_empty() { + return Err(invalid( + "non-replay recurrence program unexpectedly carries replay targets", + )); + } + return Ok((Vec::new(), Vec::new(), Vec::new(), Vec::new())); + } + + let mut source_template_by_state = BTreeMap::<(u32, u32), u32>::new(); + let mut source_state_by_template = BTreeMap::<(u32, u32), u32>::new(); + for current in program + .currents() + .iter() + .filter(|current| current.is_source()) + { + let source_slot = only_source_slot(current)?; + let source_template_id = match current.key().source_binding() { + CurrentSourceBinding::FixedTemplate(source_template_id) => *source_template_id, + _ => { + return Err(invalid(format!( + "topology-replay source current {} has no fixed source template", + current.id() + ))); + } + }; + let ancestry = current.key().helicity_identity().local_source_states(); + if ancestry.len() != 1 || ancestry[0].source_slot() != source_slot { + return Err(invalid(format!( + "topology-replay source current {} has invalid local source ancestry", + current.id() + ))); + } + let state_index = ancestry[0].state_index(); + if source_template_by_state + .insert((source_slot, state_index), source_template_id) + .is_some_and(|previous| previous != source_template_id) + { + return Err(invalid(format!( + "source slot {source_slot} state {state_index} has inconsistent source templates" + ))); + } + if source_state_by_template + .insert((source_slot, source_template_id), state_index) + .is_some_and(|previous| previous != state_index) + { + return Err(invalid(format!( + "source slot {source_slot} template {source_template_id} is not a unique source state" + ))); + } + } + + let resolved_by_states = program + .resolved_helicities() + .iter() + .map(|helicity| (helicity.source_states().to_vec(), helicity.id())) + .collect::>(); + if resolved_by_states.len() != program.resolved_helicities().len() { + return Err(invalid( + "topology-replay program repeats a resolved source-state assignment", + )); + } + + let source_count = usize::try_from(external_source_count(program)?) + .map_err(|_| invalid("external source count exceeds usize"))?; + let mut source_permutations = Vec::new(); + let mut replay_momentum_signs = Vec::new(); + let mut replay_helicity_map = Vec::new(); + let mut replay_targets = Vec::with_capacity(program.replay_targets().len()); + for target in program.replay_targets() { + let source_permutation_start = u64::try_from(source_permutations.len()) + .map_err(|_| invalid("source permutation table exceeds u64"))?; + source_permutations.extend_from_slice(target.source_slot_permutation()); + replay_momentum_signs.extend_from_slice(target.source_momentum_signs()); + + let helicity_map_start = u64::try_from(replay_helicity_map.len()) + .map_err(|_| invalid("replay helicity-map table exceeds u64"))?; + for helicity in program.resolved_helicities() { + let mut mapped = vec![None; source_count]; + for assignment in helicity.source_states().iter().copied() { + let representative_slot = assignment.source_slot(); + let source_template_id = source_template_by_state + .get(&(representative_slot, assignment.state_index())) + .copied() + .ok_or_else(|| { + invalid(format!( + "resolved helicity {} source slot {representative_slot} state {} has no source template", + helicity.id(), + assignment.state_index() + )) + })?; + let target_slot = target + .source_slot_permutation() + .get(representative_slot as usize) + .copied() + .ok_or_else(|| invalid("replay source permutation is out of bounds"))?; + let target_state_index = source_state_by_template + .get(&(target_slot, source_template_id)) + .copied() + .ok_or_else(|| { + invalid(format!( + "replay target {} cannot transport source template {source_template_id} from slot {representative_slot} to slot {target_slot}", + target.target_sector_id() + )) + })?; + let target_index = usize::try_from(target_slot) + .map_err(|_| invalid("replay target source slot exceeds usize"))?; + if target_index >= source_count || mapped[target_index].is_some() { + return Err(invalid(format!( + "replay target {} source mapping is not bijective", + target.target_sector_id() + ))); + } + mapped[target_index] = + Some(SourceStateAssignment::new(target_slot, target_state_index)); + } + let mapped = mapped + .into_iter() + .collect::>>() + .ok_or_else(|| { + invalid(format!( + "replay target {} does not map every source state", + target.target_sector_id() + )) + })?; + let mapped_id = resolved_by_states.get(&mapped).copied().ok_or_else(|| { + invalid(format!( + "replay target {} maps resolved helicity {} outside retained source-state coverage", + target.target_sector_id(), + helicity.id() + )) + })?; + replay_helicity_map.push(mapped_id); + } + + replay_targets.push(DirectReplayTargetDescriptor { + public_flow_id: target.target_sector_id(), + representative_id: target.materialized_sector_id(), + source_permutation_start, + source_permutation_count: u32_len( + "replay source permutation", + target.source_slot_permutation().len(), + )?, + helicity_map_start, + helicity_map_count: u32_len( + "replay helicity map", + program.resolved_helicities().len(), + )?, + phase_exact_factor_id: exact_factor_id(exact_factor_ids, target.amplitude_factor())?, + // RecurrenceProgram stores one exact row per public target and has + // no separate orbit-multiplicity field. + multiplicity: 1, + selector_domain_id: *sector_domain_ids + .get(target.materialized_sector_id() as usize) + .and_then(Option::as_ref) + .ok_or_else(|| invalid("replay representative selector domain is absent"))?, + }); + } + Ok(( + replay_targets, + source_permutations, + replay_momentum_signs, + replay_helicity_map, + )) +} + +fn lower_resolved_helicities( + program: &RecurrenceProgram, +) -> RusticolResult<( + Vec, + Vec, + Vec, +)> { + let mut descriptors = Vec::with_capacity(program.resolved_helicities().len()); + let mut source_states = Vec::new(); + let mut public_helicities = Vec::new(); + for helicity in program.resolved_helicities() { + let source_state_start = u64::try_from(source_states.len()) + .map_err(|_| invalid("resolved-helicity source states exceed u64"))?; + let public_helicity_start = u64::try_from(public_helicities.len()) + .map_err(|_| invalid("resolved-helicity public values exceed u64"))?; + source_states.extend(helicity.source_states().iter().map(|assignment| { + DirectSourceStateAssignment { + source_slot: assignment.source_slot(), + state_index: assignment.state_index(), + } + })); + public_helicities.extend_from_slice(helicity.public_helicities()); + descriptors.push(DirectResolvedHelicityDescriptor { + source_state_start, + source_selection_start: 0, + public_helicity_start, + id: helicity.id(), + source_state_count: u32_len( + "resolved-helicity source state", + helicity.source_states().len(), + )?, + source_selection_count: 0, + public_helicity_count: u32_len( + "resolved-helicity public value", + helicity.public_helicities().len(), + )?, + selector_domain_id: UNIVERSAL_SELECTOR_DOMAIN_ID, + }); + } + Ok((descriptors, source_states, public_helicities)) +} + +type DirectUnionResolvedHelicityTables = ( + Vec, + Vec, + Vec, + Vec, +); + +pub(super) fn lower_union_resolved_helicities( + choices_by_source: &[Vec], + retained_helicity_count: u64, +) -> RusticolResult { + if choices_by_source.is_empty() || choices_by_source.iter().any(Vec::is_empty) { + return Err(invalid( + "all-flow-union runtime source choices do not cover every external source", + )); + } + let cartesian_count = choices_by_source.iter().try_fold(1_u64, |count, choices| { + count + .checked_mul( + u64::try_from(choices.len()) + .map_err(|_| invalid("runtime source-choice count exceeds u64"))?, + ) + .ok_or_else(|| invalid("resolved-helicity Cartesian product exceeds u64")) + })?; + if cartesian_count != retained_helicity_count { + return Err(invalid(format!( + "all-flow-union runtime source choices span {cartesian_count} helicities, expected {retained_helicity_count}" + ))); + } + let descriptor_capacity = usize::try_from(cartesian_count) + .map_err(|_| invalid("resolved-helicity count exceeds usize"))?; + let mut descriptors = Vec::with_capacity(descriptor_capacity); + let mut source_states = Vec::with_capacity( + descriptor_capacity + .checked_mul(choices_by_source.len()) + .ok_or_else(|| invalid("resolved source-state capacity exceeds usize"))?, + ); + let mut source_selections = Vec::with_capacity(source_states.capacity()); + let mut public_helicities = Vec::with_capacity(source_states.capacity()); + let mut indices = vec![0usize; choices_by_source.len()]; + + for id in 0..descriptor_capacity { + let source_state_start = u64::try_from(source_states.len()) + .map_err(|_| invalid("resolved source states exceed u64"))?; + let source_selection_start = u64::try_from(source_selections.len()) + .map_err(|_| invalid("resolved source selections exceed u64"))?; + let public_helicity_start = u64::try_from(public_helicities.len()) + .map_err(|_| invalid("resolved public helicities exceed u64"))?; + for (source_slot, (&choice_index, choices)) in + indices.iter().zip(choices_by_source).enumerate() + { + let choice = choices[choice_index]; + let source_slot = u32_len("resolved source slot", source_slot)?; + source_states.push(DirectSourceStateAssignment { + source_slot, + state_index: choice.source_state_index, + }); + source_selections.push(DirectResolvedSourceSelection { + source_slot, + dispatch_variant_id: choice.dispatch_variant_id, + }); + public_helicities.push(choice.public_helicity); + } + let source_count = u32_len("resolved source", choices_by_source.len())?; + descriptors.push(DirectResolvedHelicityDescriptor { + source_state_start, + source_selection_start, + public_helicity_start, + id: u32_len("resolved helicity", id)?, + source_state_count: source_count, + source_selection_count: source_count, + public_helicity_count: source_count, + selector_domain_id: UNIVERSAL_SELECTOR_DOMAIN_ID, + }); + + for source_slot in (0..indices.len()).rev() { + indices[source_slot] += 1; + if indices[source_slot] < choices_by_source[source_slot].len() { + break; + } + indices[source_slot] = 0; + } + } + Ok(( + descriptors, + source_states, + source_selections, + public_helicities, + )) +} + +fn append_row_groups( + keys: impl IntoIterator, + role: DirectExecutorRole, + destination_operation: DirectDestinationOperation, + row_groups: &mut Vec, +) -> RusticolResult<()> { + let keys = keys.into_iter().collect::>(); + let mut start = 0usize; + while start < keys.len() { + let (stage, direct_executor_id) = keys[start]; + let mut stop = start + 1; + while stop < keys.len() && keys[stop] == (stage, direct_executor_id) { + stop += 1; + } + row_groups.push(DirectRowGroupDescriptor { + stage, + role, + destination_operation, + direct_executor_id, + row_start: u64::try_from(start) + .map_err(|_| invalid("direct row_group row start exceeds u64"))?, + row_count: u32_len("direct row_group row", stop - start)?, + }); + start = stop; + } + Ok(()) +} + +fn u32_len(label: &str, value: usize) -> RusticolResult { + u32::try_from(value).map_err(|_| invalid(format!("{label} count exceeds u32"))) +} + +#[cfg(test)] +#[path = "direct_lowering_tests.rs"] +mod tests; +#[cfg(test)] +pub(crate) use tests::validated_template as validated_template_fixture; diff --git a/pyamplicol/rust/crates/rusticol-core/src/recurrence/direct_lowering_tests.rs b/pyamplicol/rust/crates/rusticol-core/src/recurrence/direct_lowering_tests.rs new file mode 100644 index 0000000000..c37bfaac76 --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/recurrence/direct_lowering_tests.rs @@ -0,0 +1,2877 @@ +// SPDX-License-Identifier: 0BSD + +use std::collections::BTreeMap; + +use super::*; +use crate::recurrence::direct_lowering::{ + DirectParents, RuntimeSourceChoice, effective_runtime_helicity_variant, + lower_union_resolved_helicities, +}; +use crate::recurrence::direct_plan::{ + DIRECT_CONTRIBUTION_FLAG_CERTIFIED_REUSE, DIRECT_CONTRIBUTION_FLAG_INITIALIZE_DESTINATION, +}; +use crate::recurrence::layout::RuntimeSourceVariantBinding; +use crate::recurrence::template::{ + CatalogHeaderRow, ClosureRow, ColorContractionRow, CurrentOrientation, CurrentStateRow, + DigestCatalogRow, EvaluatorBindingRow, EvaluatorCallableKind, EvaluatorContractKind, + ExactFactorRow, IndexedRangeRow, LCColorTransitionWitnessRow, MISSING_U32, + OwnedRecurrenceTemplateInput, ParticleStatistics, PropagatorRow, QuantumFlowRow, + RECURRENCE_TEMPLATE_CANONICALIZATION_ABI, RECURRENCE_TEMPLATE_EXACT_SCALAR_ABI, + RECURRENCE_TEMPLATE_INPUT_ABI, RECURRENCE_TEMPLATE_INPUT_SCHEMA_VERSION, + RuntimeHelicityContractRow, RuntimeHelicityEmbeddingRow, RuntimeHelicityProjectionRow, + RuntimeHelicityVariantRow, SourceRow, TransitionRow, ValidatedRecurrenceTemplateInput, +}; +use crate::recurrence::{ + CanonicalMomentumLinearForm, CheckedTableRange, ContributionKey, CurrentCoreKey, + CurrentHelicityIdentity, CurrentSourceBinding, DynamicLCColorState, DynamicLCColorStateId, + ExactRational, LCColorComponentOperation, LCColorComponentRole, LCColorSourceSeedOperation, + LCColorWitnessTermId, MomentumTerm, NUMERICAL_RELATION_CERTIFICATE_ALGORITHM, + RECURRENCE_TEMPLATE_ABI, RecurrenceAmplitudeDestination, RecurrenceClosureTerm, + RecurrenceContribution, RecurrenceCurrent, RecurrenceFinalization, + RecurrenceNumericalCurrentMapping, RecurrenceNumericalRelationEvidence, RecurrenceReplayTarget, + RecurrenceResolvedHelicity, RecurrenceStrategy, SourceStateAssignment, + load_recurrence_direct_plan_pacbin, write_recurrence_direct_plan_pacbin, +}; + +const CATALOG_DIGEST_SEED: u8 = 3; +const QUANTUM_FLOW_DIGEST_SEED: u8 = 6; + +fn digest(seed: u8) -> SemanticDigest { + SemanticDigest::new([seed; 32]).unwrap() +} + +fn rational(numerator: i128) -> ExactComplexRational { + ExactComplexRational::new( + ExactRational::new(numerator, 1).unwrap(), + ExactRational::ZERO, + ) +} + +fn encoded_strings(values: &[&str]) -> (Vec, Vec) { + let mut values = values.to_vec(); + values.sort_unstable(); + values.dedup(); + let mut ranges = Vec::new(); + let mut bytes = Vec::new(); + for value in values { + ranges.push(CheckedTableRange::new( + bytes.len() as u64, + value.len() as u64, + )); + bytes.extend_from_slice(value.as_bytes()); + } + (ranges, bytes) +} + +fn string_id(strings: &[&str], value: &str) -> u32 { + let mut strings = strings.to_vec(); + strings.sort_unstable(); + strings.dedup(); + strings + .iter() + .position(|candidate| *candidate == value) + .unwrap() as u32 +} + +fn indexed_u32_sequences( + sequences: &[Vec], +) -> (Vec, Vec, BTreeMap, u32>) { + let mut sequences = sequences.to_vec(); + sequences.sort(); + sequences.dedup(); + let mut ranges = Vec::new(); + let mut values = Vec::new(); + let mut ids = BTreeMap::new(); + for (id, sequence) in sequences.into_iter().enumerate() { + ranges.push(IndexedRangeRow { + id: id as u32, + range: CheckedTableRange::new(values.len() as u64, sequence.len() as u64), + }); + values.extend_from_slice(&sequence); + ids.insert(sequence, id as u32); + } + (ranges, values, ids) +} + +fn indexed_i32_sequences(sequences: &[Vec]) -> (Vec, Vec) { + let mut sequences = sequences.to_vec(); + sequences.sort(); + sequences.dedup(); + let mut ranges = Vec::new(); + let mut values = Vec::new(); + for (id, sequence) in sequences.into_iter().enumerate() { + ranges.push(IndexedRangeRow { + id: id as u32, + range: CheckedTableRange::new(values.len() as u64, sequence.len() as u64), + }); + values.extend_from_slice(&sequence); + } + (ranges, values) +} + +pub(crate) fn validated_template() -> ValidatedRecurrenceTemplateInput { + let strings = [ + "0", + "1", + "a-source-resolver", + "any", + "b-vertex-resolver", + "basis", + "c-propagator-resolver", + "closure-a", + "closure-b", + "a-color-transition", + "b-color-close", + "component", + "compiler-certified-contact-orbit", + "constant-scalar-literal-singlet-self-conjugate-boson", + "constant-result", + "crossing", + "d-closure-a-resolver", + "e-closure-b-resolver", + "equivalence", + "input-layout", + "momentum", + "particle-static-result", + "projection", + "propagator", + "quantum-flow", + "rule", + "scalar", + "singlet-forest", + "source", + "species", + "state", + "transition", + RECURRENCE_TEMPLATE_ABI, + RECURRENCE_TEMPLATE_CANONICALIZATION_ABI, + RECURRENCE_TEMPLATE_EXACT_SCALAR_ABI, + RECURRENCE_TEMPLATE_INPUT_ABI, + ]; + let sid = |value| string_id(&strings, value); + let empty = vec![]; + let state = vec![0]; + let state_pair = vec![0, 0]; + let order_pair = vec![0, 1]; + let momentum_pair = vec![sid("momentum"), sid("momentum")]; + let component = vec![sid("component")]; + let input_layout = vec![sid("input-layout")]; + let eligible_flow = vec![0]; + let source_semantic = vec![sid("source")]; + let transition_semantic = vec![sid("transition")]; + let propagator_semantic = vec![sid("propagator")]; + let closure_a_semantic = vec![sid("closure-a")]; + let closure_b_semantic = vec![sid("closure-b")]; + let expression_source = vec![16]; + let expression_vertex = vec![17]; + let expression_propagator = vec![19]; + let expression_closure_a = vec![10]; + let expression_closure_b = vec![11]; + let contact_particles = vec![sid("scalar"); 4]; + let contact_equivalence = vec![0, 0, 0, 0]; + let contact_right_leg = vec![1]; + let contact_missing_step = vec![2]; + let contact_first_digest = vec![26]; + let contact_two_digests = vec![26, 27]; + let contact_second_digest = vec![27]; + let sequences = vec![ + empty.clone(), + state.clone(), + state_pair.clone(), + order_pair.clone(), + momentum_pair.clone(), + component.clone(), + input_layout.clone(), + eligible_flow.clone(), + source_semantic.clone(), + transition_semantic.clone(), + propagator_semantic.clone(), + closure_a_semantic.clone(), + closure_b_semantic.clone(), + expression_source.clone(), + expression_vertex.clone(), + expression_propagator.clone(), + expression_closure_a.clone(), + expression_closure_b.clone(), + contact_particles, + contact_equivalence, + contact_right_leg, + contact_missing_step, + contact_first_digest, + contact_two_digests, + contact_second_digest, + ]; + let (u32_sequence_ranges, u32_sequence_values, sequence_ids) = + indexed_u32_sequences(&sequences); + let seq = |value: &Vec| *sequence_ids.get(value).unwrap(); + let (i32_sequence_ranges, i32_sequence_values) = + indexed_i32_sequences(&[vec![0, 0], vec![1, 1], vec![0, 1, -1], vec![1, 0, -1]]); + let i32_seq = |value: &[i32]| { + i32_sequence_ranges + .iter() + .position(|range| { + let rows = range + .range + .as_usize_range(i32_sequence_values.len(), "test i32 sequence") + .unwrap(); + &i32_sequence_values[rows] == value + }) + .unwrap() as u32 + }; + let spin_pair_sequence_id = i32_seq(&[0, 0]); + let singlet_pair_sequence_id = i32_seq(&[1, 1]); + let (string_ranges, string_bytes) = encoded_strings(&strings); + let digest_catalog = (1_u8..=25) + .map(|seed| DigestCatalogRow { + id: u32::from(seed - 1), + value: [seed; 32], + }) + .collect::>(); + let digest_id = |seed: u8| u32::from(seed - 1); + let evaluator = |id: u32, + resolver: &'static str, + prepared_kernel_id: u32, + contract_kind: EvaluatorContractKind, + input_states: &Vec, + output_state: u32, + exact_expression: &Vec, + semantic_template: &Vec, + signature_seed: u8, + semantic_seed: u8| EvaluatorBindingRow { + id, + resolver_key_string_id: sid(resolver), + prepared_kernel_id, + contract_kind: contract_kind as u8, + callable_signature_digest_id: digest_id(signature_seed), + input_state_sequence_id: seq(input_states), + output_state_template_id: output_state, + input_layout_sequence_id: seq(&input_layout), + output_layout_sequence_id: seq(&component), + exact_expression_digest_sequence_id: seq(exact_expression), + semantic_template_sequence_id: seq(semantic_template), + callable_kind: EvaluatorCallableKind::PreparedKernel as u8, + runtime_template_string_id: MISSING_U32, + semantic_digest_id: digest_id(semantic_seed), + }; + + OwnedRecurrenceTemplateInput { + input_abi: RECURRENCE_TEMPLATE_INPUT_ABI.to_owned(), + catalog_digest: digest(CATALOG_DIGEST_SEED), + compiled_model_digest: digest(1), + prepared_kernel_pack_digest: digest(2), + catalog_header: vec![CatalogHeaderRow { + schema_version: RECURRENCE_TEMPLATE_INPUT_SCHEMA_VERSION, + abi_string_id: sid(RECURRENCE_TEMPLATE_ABI), + canonicalization_abi_string_id: sid(RECURRENCE_TEMPLATE_CANONICALIZATION_ABI), + exact_scalar_abi_string_id: sid(RECURRENCE_TEMPLATE_EXACT_SCALAR_ABI), + compiled_model_digest_id: digest_id(1), + prepared_kernel_pack_digest_id: digest_id(2), + catalog_digest_id: digest_id(3), + parameter_count: 0, + current_state_count: 1, + source_count: 1, + quantum_flow_count: 1, + contact_orbit_certificate_count: 0, + contact_orbit_step_count: 0, + transition_count: 1, + propagator_count: 1, + closure_count: 2, + color_contraction_count: 2, + symmetry_proof_count: 0, + runtime_helicity_contract_count: 0, + evaluator_binding_count: 5, + }], + coupling_order_ranges: vec![IndexedRangeRow { + id: 0, + range: CheckedTableRange::new(0, 0), + }], + coupling_order_terms: vec![], + contact_orbit_certificates: vec![], + contact_orbit_steps: vec![], + current_states: vec![CurrentStateRow { + id: 0, + template_string_id: sid("state"), + particle_id: 1, + anti_particle_id: 1, + species_string_id: sid("species"), + orientation: CurrentOrientation::SelfConjugate as u8, + statistics: ParticleStatistics::Boson as u8, + color_representation: 1, + basis_string_id: sid("basis"), + tensor_ordering_sequence_id: seq(&component), + dimension: 1, + chirality: 0, + lc_color_shape_string_id: sid("singlet-forest"), + auxiliary_kind_string_id: MISSING_U32, + mass_parameter_id: MISSING_U32, + width_parameter_id: MISSING_U32, + semantic_digest_id: digest_id(4), + }], + digest_catalog, + evaluator_bindings: vec![ + evaluator( + 0, + "a-source-resolver", + 0, + EvaluatorContractKind::Source, + &empty, + 0, + &expression_source, + &source_semantic, + 20, + 13, + ), + evaluator( + 1, + "b-vertex-resolver", + 1, + EvaluatorContractKind::Vertex, + &state_pair, + 0, + &expression_vertex, + &transition_semantic, + 21, + 14, + ), + evaluator( + 2, + "c-propagator-resolver", + 2, + EvaluatorContractKind::Propagator, + &state, + 0, + &expression_propagator, + &propagator_semantic, + 22, + 15, + ), + evaluator( + 3, + "d-closure-a-resolver", + 3, + EvaluatorContractKind::Closure, + &state_pair, + MISSING_U32, + &expression_closure_a, + &closure_a_semantic, + 23, + 16, + ), + evaluator( + 4, + "e-closure-b-resolver", + 4, + EvaluatorContractKind::Closure, + &state_pair, + MISSING_U32, + &expression_closure_b, + &closure_b_semantic, + 24, + 17, + ), + ], + exact_factors: vec![ExactFactorRow { + id: 0, + real_numerator_string_id: sid("1"), + real_denominator_string_id: sid("1"), + imag_numerator_string_id: sid("0"), + imag_denominator_string_id: sid("1"), + }], + flavour_flow_ranges: vec![IndexedRangeRow { + id: 0, + range: CheckedTableRange::new(0, 1), + }], + flavour_flow_values: vec![1], + i32_sequence_ranges, + i32_sequence_values, + parameters: vec![], + propagators: vec![PropagatorRow { + id: 0, + template_string_id: sid("propagator"), + state_template_id: 0, + applies_propagator: 1, + evaluator_binding_id: 2, + numerator_expression_digest_id: digest_id(18), + denominator_expression_digest_id: digest_id(19), + mass_parameter_id: MISSING_U32, + width_parameter_id: MISSING_U32, + gauge_string_id: MISSING_U32, + linearity_proof_template_id: MISSING_U32, + semantic_digest_id: digest_id(10), + }], + quantum_flows: vec![QuantumFlowRow { + id: 0, + template_string_id: sid("quantum-flow"), + input_state_sequence_id: seq(&state_pair), + input_spin_sequence_id: spin_pair_sequence_id, + input_flavour_sequence_id: seq(&state_pair), + input_quantum_sequence_id: seq(&state_pair), + flavour_flow_operation_string_id: sid("constant-result"), + quantum_number_flow_operation_string_id: sid("particle-static-result"), + coupling_order_set_id: 0, + result_state_template_id: 0, + result_spin_state: 0, + result_flavour_flow_id: 0, + result_quantum_number_flow_id: 0, + exact_coupling_factor_id: 0, + predicate_digest_id: digest_id(17), + semantic_digest_id: digest_id(QUANTUM_FLOW_DIGEST_SEED), + }], + quantum_number_flow_ranges: vec![IndexedRangeRow { + id: 0, + range: CheckedTableRange::new(0, 0), + }], + quantum_number_flow_terms: vec![], + runtime_helicity_contracts: vec![], + runtime_helicity_variants: vec![], + runtime_helicity_embeddings: vec![], + runtime_helicity_projections: vec![], + sources: vec![SourceRow { + id: 0, + template_string_id: sid("source"), + state_template_id: 0, + crossing_string_id: sid("crossing"), + wavefunction_family_string_id: sid("scalar"), + helicity: 0, + spin_state: 50_000, + flavour_flow_id: 0, + quantum_number_flow_id: 0, + lc_color_seed_operation: LCColorSourceSeedOperation::Empty as u8, + lc_color_seed_shape_string_id: sid("singlet-forest"), + lc_color_seed_component_kind: u8::MAX, + lc_color_seed_component_role: LCColorComponentRole::None as u8, + lc_color_seed_proof_digest_id: digest_id(17), + lc_color_seed_provenance_sequence_id: seq(&empty), + wavefunction_expression_digest_id: digest_id(17), + evaluator_binding_id: 0, + mass_parameter_id: MISSING_U32, + width_parameter_id: MISSING_U32, + semantic_digest_id: digest_id(5), + }], + string_ranges, + string_bytes, + symmetry_proofs: vec![], + transitions: vec![TransitionRow { + id: 0, + template_string_id: sid("transition"), + input_state_sequence_id: seq(&state_pair), + result_state_template_id: 0, + quantum_flow_template_id: 0, + evaluator_binding_id: 1, + canonical_input_order_sequence_id: seq(&order_pair), + momentum_convention_sequence_id: seq(&momentum_pair), + coupling_parameter_sequence_id: seq(&empty), + coupling_order_set_id: 0, + color_contraction_template_id: 0, + binding_coupling_factor_id: 0, + exact_factor_id: 0, + output_factor_source: 0, + equivalence_class_string_id: sid("equivalence"), + input_exchange_factor_id: MISSING_U32, + output_projection_string_id: sid("projection"), + contact_orbit_step_sequence_id: 0, + contact_orbit_step_semantic_digest_sequence_id: 0, + semantic_digest_id: digest_id(9), + }], + closures: vec![ + ClosureRow { + id: 0, + template_string_id: sid("closure-a"), + input_state_sequence_id: seq(&state_pair), + result_state_template_id: 0, + evaluator_binding_id: 3, + canonical_input_order_sequence_id: seq(&order_pair), + coupling_parameter_sequence_id: seq(&empty), + coupling_order_set_id: 0, + eligible_quantum_flow_sequence_id: seq(&eligible_flow), + color_contraction_template_id: 1, + binding_coupling_factor_id: 0, + exact_factor_id: 0, + output_factor_source: 0, + equivalence_class_string_id: sid("equivalence"), + input_exchange_factor_id: MISSING_U32, + projection_string_id: sid("projection"), + component_coefficient_sequence_id: seq(&eligible_flow), + chirality_relation_string_id: sid("any"), + metric_signature_string_id: MISSING_U32, + semantic_digest_id: digest_id(7), + }, + ClosureRow { + id: 1, + template_string_id: sid("closure-b"), + input_state_sequence_id: seq(&state_pair), + result_state_template_id: 0, + evaluator_binding_id: 4, + canonical_input_order_sequence_id: seq(&order_pair), + coupling_parameter_sequence_id: seq(&empty), + coupling_order_set_id: 0, + eligible_quantum_flow_sequence_id: seq(&eligible_flow), + color_contraction_template_id: 1, + binding_coupling_factor_id: 0, + exact_factor_id: 0, + output_factor_source: 0, + equivalence_class_string_id: sid("equivalence"), + input_exchange_factor_id: MISSING_U32, + projection_string_id: sid("projection"), + component_coefficient_sequence_id: seq(&eligible_flow), + chirality_relation_string_id: sid("any"), + metric_signature_string_id: MISSING_U32, + semantic_digest_id: digest_id(8), + }, + ], + color_contractions: vec![ + ColorContractionRow { + id: 0, + template_string_id: sid("a-color-transition"), + rule_kind_string_id: sid("rule"), + input_representation_sequence_id: singlet_pair_sequence_id, + has_output_representation: 1, + output_representation: 1, + ordered_open_string_arity: 0, + exact_coefficient_factor_id: 0, + witness_start: 0, + witness_count: 1, + nc_term_start: 0, + nc_term_count: 0, + expression_digest_id: digest_id(17), + semantic_digest_id: digest_id(11), + }, + ColorContractionRow { + id: 1, + template_string_id: sid("b-color-close"), + rule_kind_string_id: sid("rule"), + input_representation_sequence_id: singlet_pair_sequence_id, + has_output_representation: 0, + output_representation: 0, + ordered_open_string_arity: 0, + exact_coefficient_factor_id: 0, + witness_start: 1, + witness_count: 1, + nc_term_start: 0, + nc_term_count: 0, + expression_digest_id: digest_id(17), + semantic_digest_id: digest_id(12), + }, + ], + lc_color_transition_witnesses: vec![ + LCColorTransitionWitnessRow { + color_contraction_id: 0, + ordinal: 0, + left_shape_string_id: sid("singlet-forest"), + right_shape_string_id: sid("singlet-forest"), + input_permutation: 0, + reverse_parent_mask: 0, + component_operation: LCColorComponentOperation::Empty as u8, + result_component_kind: u8::MAX, + result_component_role: LCColorComponentRole::None as u8, + result_shape_string_id: sid("singlet-forest"), + exact_factor_id: 0, + proof_digest_id: digest_id(17), + input_port_pairing_sequence_id: seq(&empty), + result_port_binding_sequence_id: seq(&empty), + provenance_sequence_id: seq(&empty), + }, + LCColorTransitionWitnessRow { + color_contraction_id: 1, + ordinal: 0, + left_shape_string_id: sid("singlet-forest"), + right_shape_string_id: sid("singlet-forest"), + input_permutation: 0, + reverse_parent_mask: 0, + component_operation: LCColorComponentOperation::Close as u8, + result_component_kind: u8::MAX, + result_component_role: LCColorComponentRole::None as u8, + result_shape_string_id: MISSING_U32, + exact_factor_id: 0, + proof_digest_id: digest_id(17), + input_port_pairing_sequence_id: seq(&empty), + result_port_binding_sequence_id: seq(&empty), + provenance_sequence_id: seq(&empty), + }, + ], + color_nc_terms: vec![], + u32_sequence_ranges, + u32_sequence_values, + } + .validate() + .unwrap() +} + +fn validated_union_template() -> ValidatedRecurrenceTemplateInput { + let mut input = validated_template().into_input(); + input.catalog_header[0].runtime_helicity_contract_count = 1; + let source_resolver_string_id = input.evaluator_bindings[0].resolver_key_string_id; + let proof_string_id = input.transitions[0].equivalence_class_string_id; + let proof_digest_id = input.evaluator_bindings[0].callable_signature_digest_id; + input.runtime_helicity_contracts = vec![RuntimeHelicityContractRow { + id: 0, + template_string_id: source_resolver_string_id, + full_state_template_id: 0, + variant_range: CheckedTableRange::new(0, 1), + proof_algorithm_string_id: proof_string_id, + proof_digest_id, + semantic_digest_id: proof_digest_id, + }]; + input.runtime_helicity_variants = vec![RuntimeHelicityVariantRow { + id: 0, + contract_id: 0, + source_template_id: 0, + source_state_template_id: 0, + embedding_range: CheckedTableRange::new(0, 1), + projection_range: CheckedTableRange::new(0, 1), + proof_digest_id, + }]; + input.runtime_helicity_embeddings = vec![RuntimeHelicityEmbeddingRow { + variant_id: 0, + full_component: 0, + source_component: 0, + factor_id: 0, + }]; + input.runtime_helicity_projections = vec![RuntimeHelicityProjectionRow { + variant_id: 0, + source_component: 0, + full_component: 0, + }]; + input.validate().unwrap() +} + +#[test] +fn union_crossing_uses_the_effective_state_embedding() { + let mut input = validated_union_template().into_input(); + input + .runtime_helicity_variants + .push(RuntimeHelicityVariantRow { + id: 1, + contract_id: 0, + source_template_id: 0, + source_state_template_id: 17, + embedding_range: CheckedTableRange::new(1, 1), + projection_range: CheckedTableRange::new(1, 1), + proof_digest_id: 0, + }); + input + .runtime_helicity_embeddings + .push(RuntimeHelicityEmbeddingRow { + variant_id: 1, + full_component: 0, + source_component: MISSING_U32, + factor_id: 0, + }); + input + .runtime_helicity_projections + .push(RuntimeHelicityProjectionRow { + variant_id: 1, + source_component: 0, + full_component: 1, + }); + + let effective = effective_runtime_helicity_variant(&input, 0..2, 17).unwrap(); + assert_eq!(effective.id, 1); + assert_eq!(effective.source_state_template_id, 17); + + input + .runtime_helicity_variants + .push(RuntimeHelicityVariantRow { + id: 2, + contract_id: 0, + source_template_id: 0, + source_state_template_id: 17, + embedding_range: CheckedTableRange::new(2, 1), + projection_range: CheckedTableRange::new(2, 1), + proof_digest_id: 1, + }); + input + .runtime_helicity_embeddings + .push(RuntimeHelicityEmbeddingRow { + variant_id: 2, + full_component: 0, + source_component: MISSING_U32, + factor_id: 0, + }); + input + .runtime_helicity_projections + .push(RuntimeHelicityProjectionRow { + variant_id: 2, + source_component: 0, + full_component: 1, + }); + assert_eq!( + effective_runtime_helicity_variant(&input, 0..3, 17) + .unwrap() + .id, + 1 + ); + + input.runtime_helicity_embeddings[2].source_component = 0; + assert!(effective_runtime_helicity_variant(&input, 0..3, 17).is_err()); +} + +fn momentum(slots: &[u32]) -> CanonicalMomentumLinearForm { + CanonicalMomentumLinearForm::new( + slots + .iter() + .copied() + .map(|source_slot| MomentumTerm { + source_slot, + coefficient: 1, + }) + .collect(), + ) + .unwrap() +} + +fn source_key(slot: u32) -> CurrentCoreKey { + CurrentCoreKey::new( + digest(CATALOG_DIGEST_SEED), + RecurrenceNodeKind::Source, + 0, + DynamicLCColorStateId::from_interner(slot), + vec![slot], + momentum(&[slot]), + CurrentHelicityIdentity::topology_replay(50_000, vec![SourceStateAssignment::new(slot, 0)]) + .unwrap(), + vec![1], + 0, + vec![], + CurrentSourceBinding::FixedTemplate(0), + None, + ) + .unwrap() +} + +fn propagated_key(id: u32, slots: &[u32]) -> CurrentCoreKey { + CurrentCoreKey::new( + digest(CATALOG_DIGEST_SEED), + RecurrenceNodeKind::Current, + 0, + DynamicLCColorStateId::from_interner(id), + slots.to_vec(), + momentum(slots), + CurrentHelicityIdentity::topology_replay( + 0, + slots + .iter() + .copied() + .map(|slot| SourceStateAssignment::new(slot, 0)) + .collect(), + ) + .unwrap(), + vec![1], + 0, + vec![], + CurrentSourceBinding::None, + Some(0), + ) + .unwrap() +} + +fn contribution_key( + parent_ids: &[u32], + parents: &[&CurrentCoreKey], + output_projection_id: u32, +) -> ContributionKey { + ContributionKey::new( + 0, + parent_ids.to_vec(), + vec![0; parents.len()], + parents + .iter() + .map(|parent| parent.momentum().clone()) + .collect(), + 0, + 0, + LCColorWitnessTermId::new(0, 0), + digest(QUANTUM_FLOW_DIGEST_SEED), + output_projection_id, + ) + .unwrap() +} + +fn count_fixture_program( + templates: &ValidatedRecurrenceTemplateInput, + external_count: usize, + current_count: usize, + contribution_count: usize, + closure_count: usize, + physical_sector_count: u32, +) -> RecurrenceProgram { + let source_keys = (0..external_count) + .map(|slot| source_key(slot as u32)) + .collect::>(); + let mut propagated_specs = Vec::<(Vec, Vec)>::new(); + if external_count == 4 { + propagated_specs.push((vec![0, 1], vec![0, 1])); + propagated_specs.push((vec![2, 3], vec![2, 3])); + } else { + propagated_specs.push((vec![0, 1], vec![0, 1])); + propagated_specs.push((vec![2, 3], vec![2, 3])); + propagated_specs.push((vec![2, 3, 4], vec![(external_count + 1) as u32, 4])); + } + while propagated_specs.len() < current_count - external_count { + let pair = if propagated_specs.len().is_multiple_of(2) { + vec![0, 1] + } else { + vec![2, 3] + }; + propagated_specs.push((pair.clone(), pair)); + } + let propagated_keys = propagated_specs + .iter() + .enumerate() + .map(|(index, (support, _))| propagated_key((external_count + index) as u32, support)) + .collect::>(); + + let output_projection_id = templates.input().transitions[0].output_projection_string_id; + let non_source_count = current_count - external_count; + let extra_contributions = contribution_count - non_source_count; + let mut contributions = Vec::with_capacity(contribution_count); + let mut currents = source_keys + .iter() + .enumerate() + .map(|(id, key)| { + RecurrenceCurrent::new( + id as u32, + key.clone(), + Some(ExactComplexRational::ONE), + CheckedTableRange::new(0, 0), + None, + ) + .unwrap() + }) + .collect::>(); + let all_keys = source_keys + .iter() + .chain(propagated_keys.iter()) + .collect::>(); + let mut finalizations = Vec::with_capacity(non_source_count); + for (index, (support, parent_ids)) in propagated_specs.iter().enumerate() { + let current_id = (external_count + index) as u32; + let start = contributions.len() as u64; + let fan_in = 1 + usize::from(index < extra_contributions); + for _ in 0..fan_in { + let parent_keys = parent_ids + .iter() + .map(|id| all_keys[*id as usize]) + .collect::>(); + contributions.push( + RecurrenceContribution::new( + contributions.len() as u32, + current_id, + parent_ids.clone(), + contribution_key(parent_ids, &parent_keys, output_projection_id), + ExactComplexRational::ONE, + ) + .unwrap(), + ); + } + finalizations.push( + RecurrenceFinalization::new( + index as u32, + current_id, + Some(0), + ExactComplexRational::ONE, + ) + .unwrap(), + ); + currents.push( + RecurrenceCurrent::new( + current_id, + propagated_keys[index].clone(), + None, + CheckedTableRange::new(start, fan_in as u64), + Some(index as u32), + ) + .unwrap(), + ); + assert_eq!(support, propagated_keys[index].support_source_slots()); + } + + let closure_parents = if external_count == 4 { + vec![external_count as u32, external_count as u32 + 1] + } else { + vec![external_count as u32, external_count as u32 + 2] + }; + let closure_terms = (0..closure_count) + .map(|id| { + RecurrenceClosureTerm::new( + id as u32, + 0, + (id % 2) as u32, + Some(0), + closure_parents.clone(), + if id % 3 == 0 { + rational(-1) + } else { + ExactComplexRational::ONE + }, + ) + .unwrap() + }) + .collect::>(); + let replay_targets = (0..physical_sector_count) + .map(|target| { + let mut permutation = (0..external_count as u32).collect::>(); + if target != 0 { + permutation.swap(external_count - 2, external_count - 1); + } + RecurrenceReplayTarget::new( + target, + 0, + target, + permutation, + vec![1; external_count], + if target == 0 { + ExactComplexRational::ONE + } else { + rational(-1) + }, + ) + .unwrap() + }) + .collect(); + let resolved_helicities = vec![ + RecurrenceResolvedHelicity::new( + 0, + (0..external_count as u32) + .map(|slot| SourceStateAssignment::new(slot, 0)) + .collect(), + vec![0; external_count], + ) + .unwrap(), + ]; + RecurrenceProgram::new( + RecurrenceStrategy::TopologyReplay, + physical_sector_count, + 1, + (0..current_count) + .map(|id| DynamicLCColorState::new(id as u32, None, vec![]).unwrap()) + .collect(), + currents, + contributions, + finalizations, + replay_targets, + resolved_helicities, + vec![ + RecurrenceAmplitudeDestination::new( + 0, + 0, + Some(0), + CheckedTableRange::new(0, closure_count as u64), + ) + .unwrap(), + ], + closure_terms, + ) + .unwrap() +} + +fn union_key(key: &CurrentCoreKey, runtime_variant_id: u32) -> CurrentCoreKey { + let source_binding = if key.node_kind() == RecurrenceNodeKind::Source { + CurrentSourceBinding::runtime_dispatch_with_variants( + 0, + vec![ + RuntimeSourceVariantBinding::new( + 0, + 0, + runtime_variant_id, + 0, + 0, + 0, + 0, + ExactComplexRational::ONE, + ) + .unwrap(), + ], + ) + .unwrap() + } else { + CurrentSourceBinding::None + }; + CurrentCoreKey::new( + key.catalog_digest(), + key.node_kind(), + key.current_state_template_id(), + key.dynamic_lc_color_state_id(), + key.support_source_slots().to_vec(), + key.momentum().clone(), + CurrentHelicityIdentity::all_flow_union(if key.node_kind() == RecurrenceNodeKind::Source { + crate::recurrence::DYNAMIC_UNION_SOURCE_SPIN_STATE_CLASS + } else { + key.spin_state_class() + }), + key.flavour_flow().to_vec(), + key.quantum_number_flow_id(), + key.coupling_orders().to_vec(), + source_binding, + key.propagator_template_id(), + ) + .unwrap() +} + +fn zgg_union_program( + templates: &ValidatedRecurrenceTemplateInput, + runtime_variant_id: u32, +) -> RecurrenceProgram { + let base = count_fixture_program(templates, 5, 69, 126, 24, 2); + let currents = base + .currents() + .iter() + .map(|current| { + RecurrenceCurrent::new( + current.id(), + union_key(current.key(), runtime_variant_id), + None, + current.contribution_range(), + current.finalization_id(), + ) + .unwrap() + }) + .collect::>(); + let closures = base + .closure_terms() + .iter() + .enumerate() + .map(|(index, closure)| { + RecurrenceClosureTerm::new( + closure.id(), + u32::from(index >= 12), + closure.closure_template_id(), + closure.quantum_flow_template_id(), + closure.parent_current_ids().to_vec(), + closure.exact_factor(), + ) + .unwrap() + }) + .collect::>(); + RecurrenceProgram::new( + RecurrenceStrategy::AllFlowUnion, + 2, + 1, + base.dynamic_color_states().to_vec(), + currents, + base.contributions().to_vec(), + base.finalizations().to_vec(), + vec![], + vec![], + vec![ + RecurrenceAmplitudeDestination::new(0, 0, None, CheckedTableRange::new(0, 12)).unwrap(), + RecurrenceAmplitudeDestination::new(1, 1, None, CheckedTableRange::new(12, 12)) + .unwrap(), + ], + closures, + ) + .unwrap() +} + +fn runtime_options() -> DirectRecurrenceRuntimeOptions { + DirectRecurrenceRuntimeOptions::new(128, 16).unwrap() +} + +fn direct_catalog(catalog_digest: SemanticDigest) -> PreparedDirectExecutorCatalog { + PreparedDirectExecutorCatalog::new( + catalog_digest, + vec![ + PreparedDirectExecutorBinding::evaluator(DirectExecutorRole::Source, 0, 0), + PreparedDirectExecutorBinding::evaluator(DirectExecutorRole::Contribution, 1, 1), + PreparedDirectExecutorBinding::evaluator(DirectExecutorRole::Finalization, 2, 2), + PreparedDirectExecutorBinding::evaluator(DirectExecutorRole::Closure, 3, 3), + PreparedDirectExecutorBinding::evaluator(DirectExecutorRole::Closure, 4, 4), + PreparedDirectExecutorBinding::identity_finalizer(5), + ], + ) + .unwrap() +} + +fn lower( + program: &RecurrenceProgram, + templates: &ValidatedRecurrenceTemplateInput, + semantic_digest: SemanticDigest, +) -> crate::RusticolResult { + let catalog_digest = digest(40); + lower_recurrence_direct_v2( + program, + templates, + &direct_catalog(catalog_digest), + semantic_digest, + digest(2), + catalog_digest, + runtime_options(), + ) +} + +#[test] +fn deterministic_lowering_uses_stable_prepared_executor_ids_and_i32_spin() { + let templates = validated_template(); + let program = count_fixture_program(&templates, 4, 31, 34, 12, 1); + let first = lower(&program, &templates, digest(30)).unwrap(); + let second = lower(&program, &templates, digest(30)).unwrap(); + assert_eq!(first, second); + assert_eq!(first.direct_executor_count, 6); + assert_eq!(first.direct_template_catalog_digest, digest(40)); + assert_eq!(first.sources[0].spin_state_class, 50_000); + assert_eq!( + first + .row_groups + .iter() + .find(|row_group| row_group.role == DirectExecutorRole::Source) + .unwrap() + .direct_executor_id, + 0 + ); + assert!(first.row_groups.iter().any(|row_group| { + row_group.role == DirectExecutorRole::Contribution && row_group.direct_executor_id == 1 + })); + assert!(first.row_groups.iter().any(|row_group| { + row_group.role == DirectExecutorRole::Finalization && row_group.direct_executor_id == 2 + })); +} + +#[test] +fn contribution_fanout_preserves_copy_dependencies_with_recycled_arena_addresses() { + let copy = |parent, destination, factor| DirectContributionRow { + parent0_component_base: parent, + parent1_component_base_or_sentinel: DIRECT_NONE_U32, + parent0_momentum_form_id: 4, + parent1_momentum_form_id_or_sentinel: DIRECT_NONE_U32, + destination_component_base: destination, + exact_factor_id: factor, + selector_domain_id: 5, + flags: DIRECT_CONTRIBUTION_FLAG_INITIALIZE_DESTINATION + | DIRECT_CONTRIBUTION_FLAG_CERTIFIED_REUSE, + }; + // A is at 100, B at 10, C at 50. Validated semantic order is A -> B -> C, + // but best-fit arena reuse need not assign increasing physical addresses. + // Sorting by the parent address would read B before its copy from A. + let expected = vec![copy(100, 10, 1), copy(10, 50, 2), copy(5, 5, 0)]; + let mut rows = expected.clone(); + order_contributions_for_runtime_fanout_by(&mut rows, |row| { + ( + 3, + DIRECT_NONE_U32, + *row, + (row.destination_component_base, row.exact_factor_id), + ) + }); + // The address-based tie is also used by persisted selected families; + // certified zero self-copies must preserve their position as well. + assert_eq!(rows, expected); +} + +#[test] +fn contribution_fanout_order_uses_only_authenticated_kinematic_inputs() { + fn draft( + semantic_id: u32, + parent_base: u32, + destination_base: u32, + exact_factor_id: u32, + flags: u32, + ) -> ContributionDraft { + ContributionDraft { + stage: 3, + executor_id: 7, + semantic_contribution_id: semantic_id, + semantic_result_current_id: destination_base, + semantic_parent_current_ids: [parent_base, DIRECT_NONE_U32], + semantic_parent_count: 1, + row: DirectContributionRow { + parent0_component_base: parent_base, + parent1_component_base_or_sentinel: DIRECT_NONE_U32, + parent0_momentum_form_id: 9, + parent1_momentum_form_id_or_sentinel: DIRECT_NONE_U32, + destination_component_base: destination_base, + exact_factor_id, + selector_domain_id: 5, + flags, + }, + } + } + + let mut drafts = vec![ + draft(10, 2, 100, 2, 0), + draft(20, 4, 200, 1, 0), + draft(8, 2, 300, 0, DIRECT_CONTRIBUTION_FLAG_CERTIFIED_REUSE), + draft( + 11, + 2, + 101, + 1, + DIRECT_CONTRIBUTION_FLAG_INITIALIZE_DESTINATION, + ), + draft(21, 4, 201, 1, 0), + draft(9, 2, 102, 1, 0), + draft(30, 6, 400, 1, 0), + ]; + order_contributions_for_runtime_fanout(&mut drafts); + + let semantic_ids = drafts + .iter() + .map(|draft| draft.semantic_contribution_id) + .collect::>(); + assert_eq!(&semantic_ids[..5], &[9, 11, 10, 20, 21]); + assert_eq!( + semantic_ids[5..].iter().copied().collect::>(), + BTreeSet::from([8, 30]) + ); + assert!( + drafts[..3] + .windows(2) + .all(|pair| contribution_fanout_order_key(&pair[0]) + == contribution_fanout_order_key(&pair[1])) + ); + assert_ne!( + contribution_fanout_order_key(&drafts[2]), + contribution_fanout_order_key(&drafts[3]) + ); + assert!( + drafts + .iter() + .find(|draft| draft.semantic_contribution_id == 8) + .unwrap() + .row + .flags + & DIRECT_CONTRIBUTION_FLAG_CERTIFIED_REUSE + != 0 + ); + + let base = draft(40, 12, 500, 1, 0); + let mut output_local = base; + output_local.row.destination_component_base = 999; + output_local.row.exact_factor_id = 17; + output_local.row.flags = DIRECT_CONTRIBUTION_FLAG_INITIALIZE_DESTINATION; + assert_eq!( + contribution_fanout_order_key(&base), + contribution_fanout_order_key(&output_local) + ); + for changed in [ + { + let mut changed = base; + changed.stage += 1; + changed + }, + { + let mut changed = base; + changed.executor_id += 1; + changed + }, + { + let mut changed = base; + changed.row.selector_domain_id += 1; + changed + }, + { + let mut changed = base; + changed.row.parent0_component_base += 1; + changed + }, + { + let mut changed = base; + changed.row.parent1_component_base_or_sentinel = 7; + changed + }, + { + let mut changed = base; + changed.row.parent0_momentum_form_id += 1; + changed + }, + { + let mut changed = base; + changed.row.parent1_momentum_form_id_or_sentinel = 8; + changed + }, + ] { + assert_ne!( + contribution_fanout_order_key(&base), + contribution_fanout_order_key(&changed) + ); + } +} + +#[test] +fn fixed_helicity_locality_follows_certified_reuse_and_multiple_mask_words() { + let mut copied = locality_draft(3, 5, &[4], 2, 0); + copied.executor_id = DIRECT_NONE_U32; + copied.row.flags = DIRECT_CONTRIBUTION_FLAG_CERTIFIED_REUSE; + let contributions = [ + locality_draft(0, 2, &[0], 1, 0), + locality_draft(1, 3, &[1], 1, 0), + locality_draft(2, 4, &[2], 2, 0), + copied, + locality_draft(4, 6, &[3], 2, 0), + ]; + let demands = + fixed_helicity_demands(8, 71, &contributions, [(5, 70), (6, 1), (4, 2)].into_iter()) + .unwrap(); + for id in [0, 2, 4] { + assert_eq!(demands.current(id), &[1 << 2, 1 << 6]); + } + assert_eq!(demands.current(5), &[0, 1 << 6]); + for id in [1, 3, 6] { + assert_eq!(demands.current(id), &[1 << 1, 0]); + } + assert_eq!(demands.current(7), &[0, 0]); + + let mut zero_self_copy = locality_draft(5, 7, &[7], 2, 0); + zero_self_copy.executor_id = DIRECT_NONE_U32; + zero_self_copy.row.flags = DIRECT_CONTRIBUTION_FLAG_CERTIFIED_REUSE; + let zero_demand = + fixed_helicity_demands(8, 71, &[zero_self_copy], [(7, 70)].into_iter()).unwrap(); + assert_eq!(zero_demand.current(7), &[0, 1 << 6]); + assert!(zero_demand.words[..7 * 2].iter().all(|&word| word == 0)); + + let repeated = fixed_helicity_demands( + 8, + 71, + &contributions, + [(4, 2), (6, 1), (5, 70), (5, 70)].into_iter(), + ) + .unwrap(); + assert_eq!(demands.words, repeated.words); + assert!(fixed_helicity_demands(8, 71, &contributions, [(5, 71)].into_iter()).is_err()); + let mut invalid_copy = copied; + invalid_copy.semantic_parent_current_ids[0] = 7; + assert!( + fixed_helicity_demands(8, 71, &[invalid_copy], [(5, 70)].into_iter()) + .unwrap_err() + .to_string() + .contains("topologically ordered") + ); +} + +fn locality_draft( + id: u32, + result: u32, + parents: &[u32], + stage: u16, + selector_domain_id: u32, +) -> ContributionDraft { + let second = parents.get(1).copied().unwrap_or(DIRECT_NONE_U32); + ContributionDraft { + stage, + executor_id: 7, + semantic_contribution_id: id, + semantic_result_current_id: result, + semantic_parent_current_ids: [parents[0], second], + semantic_parent_count: parents.len() as u8, + row: DirectContributionRow { + parent0_component_base: parents[0], + parent1_component_base_or_sentinel: second, + parent0_momentum_form_id: 0, + parent1_momentum_form_id_or_sentinel: DIRECT_NONE_U32, + destination_component_base: result, + exact_factor_id: id % 3, + selector_domain_id, + flags: 0, + }, + } +} + +#[test] +fn fixed_helicity_locality_preserves_fanout_classes_rows_and_primary_groups() { + let mut drafts = vec![ + locality_draft(0, 4, &[0], 1, 0), + locality_draft(1, 5, &[0], 1, 0), + locality_draft(2, 6, &[1], 1, 0), + locality_draft(3, 7, &[2], 1, 0), + locality_draft(4, 8, &[3], 1, 0), + locality_draft(5, 9, &[3], 1, 0), + locality_draft(6, 10, &[0], 1, 1), + locality_draft(7, 11, &[0], 2, 0), + ]; + let demands = fixed_helicity_demands( + 12, + 5, + &drafts, + [ + (4, 4), + (5, 0), + (6, 0), + (7, 1), + (8, 2), + (9, 2), + (10, 0), + (11, 0), + ] + .into_iter(), + ) + .unwrap(); + order_contributions_for_runtime_fanout(&mut drafts); + let original = drafts.clone(); + order_contributions_for_helicity_locality(&mut drafts, &demands); + assert_eq!( + drafts + .iter() + .map(|draft| draft.semantic_result_current_id) + .collect::>(), + // Intact repeated classes first, ordered by union demand: {2}, {0,4}. + // Singleton classes follow, ordered by their own demand: {0}, {1}. + vec![8, 9, 4, 5, 6, 7, 10, 11] + ); + for pair in [&drafts[..2], &drafts[2..4]] { + assert_eq!( + contribution_fanout_order_key(&pair[0]), + contribution_fanout_order_key(&pair[1]) + ); + } + for draft in &drafts { + let before = original + .iter() + .find(|before| before.semantic_contribution_id == draft.semantic_contribution_id) + .unwrap(); + assert_eq!(draft.row, before.row); + assert_eq!(draft.stage, before.stage); + assert_eq!(draft.executor_id, before.executor_id); + assert_eq!( + draft.semantic_parent_current_ids, + before.semantic_parent_current_ids + ); + } + assert_eq!(drafts.len(), original.len()); + let first_order = drafts + .iter() + .map(|draft| draft.semantic_contribution_id) + .collect::>(); + order_contributions_for_helicity_locality(&mut drafts, &demands); + assert_eq!( + first_order, + drafts + .iter() + .map(|draft| draft.semantic_contribution_id) + .collect::>() + ); + let mut repeated = original.clone(); + order_contributions_for_helicity_locality(&mut repeated, &demands); + assert_eq!( + first_order, + repeated + .iter() + .map(|draft| draft.semantic_contribution_id) + .collect::>() + ); +} + +#[test] +fn fixed_helicity_locality_does_not_reorder_same_stage_copies() { + let mut drafts = vec![ + locality_draft(0, 4, &[2], 2, 0), + locality_draft(1, 5, &[4], 2, 0), + ]; + for draft in &mut drafts { + draft.executor_id = DIRECT_NONE_U32; + draft.row.flags = DIRECT_CONTRIBUTION_FLAG_CERTIFIED_REUSE; + } + let demands = fixed_helicity_demands(6, 2, &drafts, [(4, 1), (5, 0)].into_iter()).unwrap(); + order_contributions_for_helicity_locality(&mut drafts, &demands); + assert_eq!(drafts[0].semantic_result_current_id, 4); + assert_eq!(drafts[1].semantic_result_current_id, 5); +} + +#[test] +fn prepared_executor_ids_do_not_depend_on_process_encounter_order() { + let templates = validated_template(); + let program = count_fixture_program(&templates, 4, 31, 34, 12, 1); + let catalog_digest = digest(43); + let catalog = PreparedDirectExecutorCatalog::new( + catalog_digest, + vec![ + PreparedDirectExecutorBinding::evaluator(DirectExecutorRole::Closure, 4, 0), + PreparedDirectExecutorBinding::evaluator(DirectExecutorRole::Closure, 3, 1), + PreparedDirectExecutorBinding::evaluator(DirectExecutorRole::Contribution, 1, 2), + PreparedDirectExecutorBinding::identity_finalizer(3), + PreparedDirectExecutorBinding::evaluator(DirectExecutorRole::Source, 0, 4), + PreparedDirectExecutorBinding::evaluator(DirectExecutorRole::Finalization, 2, 5), + ], + ) + .unwrap(); + let parts = lower_recurrence_direct_v2( + &program, + &templates, + &catalog, + digest(44), + digest(2), + catalog_digest, + runtime_options(), + ) + .unwrap(); + + assert!(parts.row_groups.iter().any(|group| { + group.role == DirectExecutorRole::Source && group.direct_executor_id == 4 + })); + assert!(parts.row_groups.iter().any(|group| { + group.role == DirectExecutorRole::Contribution && group.direct_executor_id == 2 + })); + assert!(parts.row_groups.iter().any(|group| { + group.role == DirectExecutorRole::Finalization && group.direct_executor_id == 5 + })); + assert!(parts.row_groups.iter().any(|group| { + group.role == DirectExecutorRole::Closure && matches!(group.direct_executor_id, 0 | 1) + })); +} + +#[test] +fn zg_and_zgg_gate_counts_are_preserved_exactly() { + let templates = validated_template(); + for (external, currents, contributions, closures, sectors) in + [(4, 31, 34, 12, 1), (5, 69, 126, 24, 2)] + { + let program = count_fixture_program( + &templates, + external, + currents, + contributions, + closures, + sectors, + ); + let parts = lower(&program, &templates, digest(31)).unwrap(); + assert_eq!(parts.currents.len(), currents); + assert_eq!(parts.contributions.len(), contributions); + assert_eq!(parts.finalizations.len(), currents - external); + assert_eq!(parts.closures.len(), closures); + } +} + +#[test] +fn topology_replay_persists_dependency_closed_selector_work() { + let templates = validated_template(); + let program = count_fixture_program(&templates, 5, 69, 126, 24, 2); + let parts = lower(&program, &templates, digest(47)).unwrap(); + assert!(parts.selector_domains.len() > 1); + let plan = DirectRecurrencePlan::new(parts).unwrap(); + let selected = plan.selector_work_summary(0).unwrap(); + + assert!(selected.current_count < plan.currents().len() as u64); + assert!(selected.contribution_count < plan.contributions().len() as u64); + assert_eq!(selected.closure_count, plan.closures().len() as u64); + assert!( + selected.row_count() + < (plan.sources().len() + + plan.contributions().len() + + plan.finalizations().len() + + plan.closures().len()) as u64 + ); +} + +#[test] +fn topology_replay_interns_only_referenced_canonical_selector_domains() { + let templates = validated_template(); + let base = count_fixture_program(&templates, 5, 69, 126, 24, 130); + let replay_targets = base + .replay_targets() + .iter() + .map(|target| { + RecurrenceReplayTarget::new( + target.id(), + 0, + target.target_sector_id(), + target.source_slot_permutation().to_vec(), + target.source_momentum_signs().to_vec(), + target.amplitude_factor(), + ) + .unwrap() + }) + .collect(); + let program = RecurrenceProgram::new( + base.strategy(), + base.physical_sector_count(), + base.retained_helicity_count(), + base.dynamic_color_states().to_vec(), + base.currents().to_vec(), + base.contributions().to_vec(), + base.finalizations().to_vec(), + replay_targets, + base.resolved_helicities().to_vec(), + base.amplitude_destinations().to_vec(), + base.closure_terms().to_vec(), + ) + .unwrap(); + + let selectors = lower_selector_domains(&program).unwrap(); + assert_eq!( + selectors + .sector_domain_ids + .iter() + .filter(|domain| domain.is_some()) + .count(), + 1 + ); + assert!(selectors.sector_domain_ids[0].is_some()); + assert!(selectors.sector_domain_ids[1..].iter().all(Option::is_none)); + assert_eq!(selectors.words, [u64::MAX, 1]); + let encoded_domains = selectors + .descriptors + .iter() + .map(|descriptor| { + let start = descriptor.word_start as usize; + let end = start + descriptor.word_count as usize; + &selectors.words[start..end] + }) + .collect::>(); + assert_eq!( + encoded_domains + .iter() + .filter(|domain| **domain == [u64::MAX]) + .count(), + 1 + ); + assert_eq!( + encoded_domains + .iter() + .filter(|domain| **domain == [1]) + .count(), + 1 + ); + assert_eq!( + encoded_domains + .iter() + .filter(|domain| domain.is_empty()) + .count(), + 1 + ); + + let singleton_domain_id = selectors.sector_domain_ids[0].unwrap(); + let plan = DirectRecurrencePlan::new(lower(&program, &templates, digest(48)).unwrap()).unwrap(); + let encoded = crate::recurrence::encode_recurrence_direct_plan_v2(&plan).unwrap(); + let decoded = crate::recurrence::decode_recurrence_direct_plan_v2(&encoded).unwrap(); + assert!( + decoded + .selector_domain_contains(singleton_domain_id, 0) + .unwrap() + ); + assert!( + !decoded + .selector_domain_contains(singleton_domain_id, 64) + .unwrap() + ); + assert!( + !decoded + .selector_domain_contains(singleton_domain_id, 129) + .unwrap() + ); +} + +#[test] +fn selector_domain_canonicalization_preserves_the_universal_sentinel() { + assert!( + canonical_selector_words(&[0, 0, 0]).is_empty(), + "all-zero selector words canonicalize to an empty slice" + ); + assert_eq!(canonical_selector_words(&[1, 0, 0]), [1]); + assert_eq!(canonical_selector_words(&[u64::MAX]), [u64::MAX]); + assert_eq!(canonical_selector_words(&[u64::MAX, 0, 0]), [u64::MAX, 0]); +} + +fn contracted_fixture_program(base: &RecurrenceProgram) -> RecurrenceProgram { + let currents = base + .currents() + .iter() + .map(|current| { + let key = current.key(); + let contracted_key = CurrentCoreKey::new( + key.catalog_digest(), + key.node_kind(), + key.current_state_template_id(), + key.dynamic_lc_color_state_id(), + key.support_source_slots().to_vec(), + key.momentum().clone(), + CurrentHelicityIdentity::contracted_color_union( + key.spin_state_class(), + key.helicity_identity().local_source_states().to_vec(), + ) + .unwrap(), + key.flavour_flow().to_vec(), + key.quantum_number_flow_id(), + key.coupling_orders().to_vec(), + key.source_binding().clone(), + key.propagator_template_id(), + ) + .unwrap(); + RecurrenceCurrent::new( + current.id(), + contracted_key, + current.source_exact_factor(), + current.contribution_range(), + current.finalization_id(), + ) + .unwrap() + }) + .collect(); + RecurrenceProgram::new( + RecurrenceStrategy::ContractedColorUnion, + base.physical_sector_count(), + base.retained_helicity_count(), + base.dynamic_color_states().to_vec(), + currents, + base.contributions().to_vec(), + base.finalizations().to_vec(), + base.replay_targets().to_vec(), + base.resolved_helicities().to_vec(), + base.amplitude_destinations().to_vec(), + base.closure_terms().to_vec(), + ) + .unwrap() +} + +#[test] +fn contracted_replay_uses_dependency_closed_selector_work() { + let templates = validated_template(); + let base = count_fixture_program(&templates, 5, 69, 126, 24, 2); + let program = contracted_fixture_program(&base); + let parts = lower(&program, &templates, digest(48)).unwrap(); + assert!(parts.selector_domains.len() > 1); + let plan = DirectRecurrencePlan::new(parts).unwrap(); + let selected = plan.selector_work_summary(0).unwrap(); + assert!(selected.current_count < plan.currents().len() as u64); + assert!(selected.contribution_count < plan.contributions().len() as u64); +} + +#[test] +fn zgg_all_flow_union_lowers_runtime_sources_without_replay_expansion() { + let templates = validated_union_template(); + let program = zgg_union_program(&templates, 0); + let first = lower(&program, &templates, digest(46)).unwrap(); + let second = lower(&program, &templates, digest(46)).unwrap(); + + assert_eq!(first, second); + assert_eq!(first.strategy, RecurrenceStrategy::AllFlowUnion); + assert_eq!(first.currents.len(), 69); + assert_eq!(first.contributions.len(), 126); + assert_eq!(first.closures.len(), 24); + assert_eq!(first.amplitude_destinations.len(), 2); + assert_eq!( + first + .amplitude_destinations + .iter() + .map(|destination| destination.target_sector_id) + .collect::>(), + [0, 1] + ); + assert!(first.replay_targets.is_empty()); + assert!(first.source_permutations.is_empty()); + assert_eq!(first.sources.len(), 5); + assert_eq!(first.source_dispatch_variants.len(), 5); + assert_eq!(first.resolved_helicities.len(), 1); + assert_eq!(first.resolved_source_selections.len(), 5); + assert_eq!(first.source_state_assignments.len(), 5); + assert_eq!(first.public_helicities, [0; 5]); + assert_eq!(first.selector_domains.len(), 1); + assert_eq!(first.selector_words, [u64::MAX]); + assert!( + first + .currents + .iter() + .all(|current| current.selector_domain_id == 0) + ); + assert!(first.row_groups.iter().any(|group| { + group.role == DirectExecutorRole::Source + && group.direct_executor_id == DIRECT_NONE_U32 + && group.row_count == 5 + })); + for (source_slot, selection) in first.resolved_source_selections.iter().enumerate() { + assert_eq!(selection.source_slot, source_slot as u32); + let variant = first.source_dispatch_variants[selection.dispatch_variant_id as usize]; + assert_eq!( + first.sources[variant.source_row_id as usize].source_slot, + source_slot as u32 + ); + assert_eq!( + first.exact_factors[variant.crossing_exact_factor_id as usize], + ExactComplexRational::ONE + ); + assert_eq!(variant.embedding_count, 1); + assert_eq!(variant.projection_count, 1); + } +} + +#[test] +fn all_flow_helicity_dispatch_lowering_preserves_the_base_plan_and_splits_exact_support() { + let templates = validated_union_template(); + let program = zgg_union_program(&templates, 0); + let catalog_digest = digest(40); + let catalog = direct_catalog(catalog_digest); + let ordinary = lower_recurrence_direct_plan_v2( + &program, + &templates, + &catalog, + digest(49), + digest(2), + catalog_digest, + runtime_options(), + ) + .unwrap(); + let (plan, dispatch) = lower_recurrence_direct_plan_v2_with_helicity_dispatch( + &program, + &templates, + &catalog, + digest(49), + digest(2), + catalog_digest, + runtime_options(), + ) + .unwrap(); + + assert_eq!(plan, ordinary); + dispatch.validate_for_plan(&plan).unwrap(); + assert_eq!(dispatch.resolved_helicity_count(), 1); + assert_eq!(dispatch.dispatches().len(), 1); + assert!( + dispatch.row_groups().len() + >= plan + .row_groups() + .iter() + .filter(|group| group.role != DirectExecutorRole::Source) + .count() + ); + assert!( + dispatch.group_ids_for_helicity(0).unwrap().len() < dispatch.row_groups().len(), + "the synthetic union contains dead rows which backward demand must omit" + ); +} + +#[test] +fn union_resolved_helicities_are_a_deterministic_source_cartesian_product() { + let choices = vec![ + vec![ + RuntimeSourceChoice { + source_state_index: 0, + public_helicity: -1, + dispatch_variant_id: 4, + }, + RuntimeSourceChoice { + source_state_index: 1, + public_helicity: 1, + dispatch_variant_id: 5, + }, + ], + vec![ + RuntimeSourceChoice { + source_state_index: 0, + public_helicity: 0, + dispatch_variant_id: 8, + }, + RuntimeSourceChoice { + source_state_index: 1, + public_helicity: 1, + dispatch_variant_id: 9, + }, + ], + ]; + let (descriptors, states, selections, public) = + lower_union_resolved_helicities(&choices, 4).unwrap(); + assert_eq!(descriptors.len(), 4); + assert_eq!( + public + .chunks_exact(2) + .map(|values| values.to_vec()) + .collect::>(), + [vec![-1, 0], vec![-1, 1], vec![1, 0], vec![1, 1]] + ); + assert_eq!( + states + .chunks_exact(2) + .map(|values| values.iter().map(|row| row.state_index).collect::>()) + .collect::>(), + [vec![0, 0], vec![0, 1], vec![1, 0], vec![1, 1]] + ); + assert_eq!( + selections + .chunks_exact(2) + .map(|values| { + values + .iter() + .map(|row| row.dispatch_variant_id) + .collect::>() + }) + .collect::>(), + [vec![4, 8], vec![4, 9], vec![5, 8], vec![5, 9]] + ); +} + +#[test] +fn union_runtime_variant_outside_its_prepared_contract_fails_closed() { + let templates = validated_union_template(); + let program = zgg_union_program(&templates, 1); + let error = lower(&program, &templates, digest(47)) + .unwrap_err() + .to_string(); + assert!( + error.contains("runtime variant 1 is outside contract 0"), + "{error}" + ); +} + +#[test] +fn union_source_choice_count_must_match_retained_helicity_count() { + let choices = vec![vec![RuntimeSourceChoice { + source_state_index: 0, + public_helicity: 0, + dispatch_variant_id: 0, + }]]; + let error = lower_union_resolved_helicities(&choices, 2) + .unwrap_err() + .to_string(); + assert!(error.contains("span 1 helicities, expected 2"), "{error}"); +} + +#[test] +fn closure_ranges_remain_destination_contiguous_with_extra_executor_groups() { + let templates = validated_template(); + let base = count_fixture_program(&templates, 4, 31, 34, 12, 1); + let mut closures = base.closure_terms().to_vec(); + for (id, closure) in closures.iter_mut().enumerate() { + *closure = RecurrenceClosureTerm::new( + id as u32, + u32::from(id >= 6), + (id % 2) as u32, + Some(0), + closure.parent_current_ids().to_vec(), + closure.exact_factor(), + ) + .unwrap(); + } + let program = RecurrenceProgram::new( + base.strategy(), + base.physical_sector_count(), + base.retained_helicity_count(), + base.dynamic_color_states().to_vec(), + base.currents().to_vec(), + base.contributions().to_vec(), + base.finalizations().to_vec(), + base.replay_targets().to_vec(), + base.resolved_helicities().to_vec(), + vec![ + RecurrenceAmplitudeDestination::new(0, 0, Some(0), CheckedTableRange::new(0, 6)) + .unwrap(), + RecurrenceAmplitudeDestination::new(1, 0, Some(0), CheckedTableRange::new(6, 6)) + .unwrap(), + ], + closures, + ) + .unwrap(); + let parts = lower(&program, &templates, digest(32)).unwrap(); + assert_eq!( + parts + .amplitude_destinations + .iter() + .map(|row| (row.closure_row_start, row.closure_row_count)) + .collect::>(), + [(0, 6), (6, 6)] + ); + assert!( + parts.closures[..6] + .iter() + .all(|row| row.amplitude_destination_id == 0) + ); + assert!( + parts.closures[6..] + .iter() + .all(|row| row.amplitude_destination_id == 1) + ); + assert_eq!( + parts + .row_groups + .iter() + .filter(|row_group| row_group.role == DirectExecutorRole::Closure) + .count(), + 4 + ); +} + +#[test] +fn unsupported_parent_arity_fails_closed() { + let templates = validated_template(); + let base = count_fixture_program(&templates, 4, 31, 34, 12, 1); + let result_id = 4_u32; + let result = &base.currents()[result_id as usize]; + let parent_ids = vec![0, 1, 2]; + let parent_keys = parent_ids + .iter() + .map(|id| base.currents()[*id as usize].key()) + .collect::>(); + let mut contributions = base.contributions().to_vec(); + contributions[0] = RecurrenceContribution::new( + 0, + result_id, + parent_ids.clone(), + contribution_key( + &parent_ids, + &parent_keys, + templates.input().transitions[0].output_projection_string_id, + ), + ExactComplexRational::ONE, + ) + .unwrap(); + let program = RecurrenceProgram::new( + base.strategy(), + base.physical_sector_count(), + base.retained_helicity_count(), + base.dynamic_color_states().to_vec(), + base.currents().to_vec(), + contributions, + base.finalizations().to_vec(), + base.replay_targets().to_vec(), + base.resolved_helicities().to_vec(), + base.amplitude_destinations().to_vec(), + base.closure_terms().to_vec(), + ) + .unwrap(); + let error = lower(&program, &templates, digest(33)) + .unwrap_err() + .to_string(); + assert!(error.contains("unsupported parent arity 3")); + assert_eq!(result.key().support_source_slots(), &[0, 1]); +} + +#[test] +fn non_unit_source_factor_is_interned_and_referenced_by_the_source_row() { + let templates = validated_template(); + let base = count_fixture_program(&templates, 4, 31, 34, 12, 1); + let mut currents = base.currents().to_vec(); + currents[0] = RecurrenceCurrent::new( + 0, + currents[0].key().clone(), + Some(rational(-1)), + CheckedTableRange::new(0, 0), + None, + ) + .unwrap(); + let program = RecurrenceProgram::new( + base.strategy(), + base.physical_sector_count(), + base.retained_helicity_count(), + base.dynamic_color_states().to_vec(), + currents, + base.contributions().to_vec(), + base.finalizations().to_vec(), + base.replay_targets().to_vec(), + base.resolved_helicities().to_vec(), + base.amplitude_destinations().to_vec(), + base.closure_terms().to_vec(), + ) + .unwrap(); + let parts = lower(&program, &templates, digest(34)).unwrap(); + let factor_id = parts.sources[0].exact_factor_id as usize; + assert_eq!(parts.exact_factors[factor_id], rational(-1)); +} + +#[test] +fn prepared_pack_digest_must_match_the_validated_catalog() { + let templates = validated_template(); + let program = count_fixture_program(&templates, 4, 31, 34, 12, 1); + let catalog_digest = digest(40); + let error = lower_recurrence_direct_v2( + &program, + &templates, + &direct_catalog(catalog_digest), + digest(35), + digest(36), + catalog_digest, + runtime_options(), + ) + .unwrap_err() + .to_string(); + assert!(error.contains("does not match validated template pack")); +} + +#[test] +fn direct_template_catalog_digest_must_match_the_prepared_resolver() { + let templates = validated_template(); + let program = count_fixture_program(&templates, 4, 31, 34, 12, 1); + let error = lower_recurrence_direct_v2( + &program, + &templates, + &direct_catalog(digest(40)), + digest(35), + digest(2), + digest(41), + runtime_options(), + ) + .unwrap_err() + .to_string(); + assert!(error.contains("does not match resolver catalog")); +} + +fn with_first_identity_finalizer( + base: &RecurrenceProgram, + exact_factor: ExactComplexRational, +) -> RecurrenceProgram { + let mut finalizations = base.finalizations().to_vec(); + let current_id = finalizations[0].current_id(); + finalizations[0] = + RecurrenceFinalization::new(finalizations[0].id(), current_id, None, exact_factor).unwrap(); + let mut currents = base.currents().to_vec(); + let current = ¤ts[current_id as usize]; + let key = current.key(); + let identity_key = CurrentCoreKey::new( + key.catalog_digest(), + key.node_kind(), + key.current_state_template_id(), + key.dynamic_lc_color_state_id(), + key.support_source_slots().to_vec(), + key.momentum().clone(), + key.helicity_identity().clone(), + key.flavour_flow().to_vec(), + key.quantum_number_flow_id(), + key.coupling_orders().to_vec(), + key.source_binding().clone(), + None, + ) + .unwrap(); + currents[current_id as usize] = RecurrenceCurrent::new( + current.id(), + identity_key, + current.source_exact_factor(), + current.contribution_range(), + current.finalization_id(), + ) + .unwrap(); + RecurrenceProgram::new( + base.strategy(), + base.physical_sector_count(), + base.retained_helicity_count(), + base.dynamic_color_states().to_vec(), + currents, + base.contributions().to_vec(), + finalizations, + base.replay_targets().to_vec(), + base.resolved_helicities().to_vec(), + base.amplitude_destinations().to_vec(), + base.closure_terms().to_vec(), + ) + .unwrap() +} + +#[test] +fn unit_identity_finalizer_is_elided() { + let templates = validated_template(); + let base = count_fixture_program(&templates, 4, 31, 34, 12, 1); + let current_id = base.finalizations()[0].current_id(); + let program = with_first_identity_finalizer(&base, ExactComplexRational::ONE); + + let parts = lower(&program, &templates, digest(42)).unwrap(); + assert_eq!( + parts.currents[current_id as usize].finalization_row_or_sentinel, + DIRECT_NONE_U32 + ); + assert_eq!(parts.finalizations.len(), base.finalizations().len() - 1); + assert!(!parts.row_groups.iter().any(|group| { + group.role == DirectExecutorRole::Finalization && group.direct_executor_id == 5 + })); +} + +#[test] +fn nonunit_identity_and_nonidentity_finalizers_are_preserved() { + let templates = validated_template(); + let base = count_fixture_program(&templates, 4, 31, 34, 12, 1); + let identity = with_first_identity_finalizer(&base, rational(2)); + let identity_parts = lower(&identity, &templates, digest(43)).unwrap(); + assert_eq!( + identity_parts.finalizations.len(), + base.finalizations().len() + ); + assert!(identity_parts.row_groups.iter().any(|group| { + group.role == DirectExecutorRole::Finalization && group.direct_executor_id == 5 + })); + + let propagated_parts = lower(&base, &templates, digest(44)).unwrap(); + assert_eq!( + propagated_parts.finalizations.len(), + base.finalizations().len() + ); + assert!(propagated_parts.row_groups.iter().any(|group| { + group.role == DirectExecutorRole::Finalization && group.direct_executor_id == 2 + })); +} + +#[test] +fn lowering_marks_exactly_one_initializing_contribution_per_destination() { + let templates = validated_template(); + let program = count_fixture_program(&templates, 4, 31, 34, 12, 1); + let parts = lower(&program, &templates, digest(45)).unwrap(); + let mut initialization_counts = BTreeMap::<(u16, u32), usize>::new(); + let mut contribution_counts = BTreeMap::<(u16, u32), usize>::new(); + for group in parts + .row_groups + .iter() + .filter(|group| group.role == DirectExecutorRole::Contribution) + { + let start = group.row_start as usize; + let end = start + group.row_count as usize; + for row in &parts.contributions[start..end] { + let key = (group.stage, row.destination_component_base); + *contribution_counts.entry(key).or_default() += 1; + if row.flags & DIRECT_CONTRIBUTION_FLAG_INITIALIZE_DESTINATION != 0 { + *initialization_counts.entry(key).or_default() += 1; + } + } + } + assert!( + contribution_counts.values().any(|count| *count > 1), + "fixture must exercise a multi-contribution destination" + ); + assert!( + contribution_counts + .keys() + .all(|key| initialization_counts.get(key) == Some(&1)) + ); +} +#[test] +fn certified_parent_permutation_swaps_arena_and_momentum_operands() { + let parents = DirectParents { + parent0_component_base: 11, + parent1_component_base_or_sentinel: 22, + parent0_momentum_form_id: 33, + parent1_momentum_form_id_or_sentinel: 44, + }; + assert_eq!(parents.permuted([0, 1]).unwrap().parent0_component_base, 11); + let swapped = parents.permuted([1, 0]).unwrap(); + assert_eq!(swapped.parent0_component_base, 22); + assert_eq!(swapped.parent1_component_base_or_sentinel, 11); + assert_eq!(swapped.parent0_momentum_form_id, 44); + assert_eq!(swapped.parent1_momentum_form_id_or_sentinel, 33); +} + +#[test] +fn certified_parent_permutation_fails_closed_for_nonbinary_rows() { + let parents = DirectParents { + parent0_component_base: 11, + parent1_component_base_or_sentinel: DIRECT_NONE_U32, + parent0_momentum_form_id: 33, + parent1_momentum_form_id_or_sentinel: DIRECT_NONE_U32, + }; + assert!(parents.permuted([1, 0]).is_err()); + assert!(parents.permuted([0, 0]).is_err()); +} + +#[test] +fn certified_relation_lowering_produces_a_round_trippable_tiny_artifact() { + let templates = validated_template(); + let program = count_fixture_program(&templates, 4, 31, 34, 12, 1); + let catalog_digest = digest(40); + let catalog = direct_catalog(catalog_digest); + let baseline = lower_recurrence_direct_plan_v2( + &program, + &templates, + &catalog, + digest(51), + digest(2), + catalog_digest, + runtime_options(), + ) + .unwrap(); + let diagnostic_options = RecurrenceRelationDiscoveryOptions::new( + RecurrenceRelationDiscoveryMode::Diagnostic, + 96, + 4, + 4, + 1.0e-70, + 1.0e-80, + 0x5059_414d, + "lc", + ) + .unwrap(); + let (diagnostic_plan, diagnostic_report) = + lower_recurrence_direct_plan_v2_with_relation_discovery( + &program, + &templates, + &catalog, + digest(51), + digest(2), + catalog_digest, + runtime_options(), + &diagnostic_options, + ) + .unwrap(); + assert_eq!(diagnostic_plan, baseline); + assert_eq!( + diagnostic_report.unwrap().state, + "diagnostic-only", + "diagnostic mode must report without mutating the plan" + ); + + let options = RecurrenceRelationDiscoveryOptions::new( + RecurrenceRelationDiscoveryMode::CertifiedReuse, + 96, + 4, + 4, + 1.0e-70, + 1.0e-80, + 0x5059_414d, + "lc", + ) + .unwrap(); + let (plan, report) = lower_recurrence_direct_plan_v2_with_relation_discovery( + &program, + &templates, + &catalog, + digest(51), + digest(2), + catalog_digest, + runtime_options(), + &options, + ) + .unwrap(); + let report = report.expect("certified lowering report"); + assert_eq!( + report.requested_mode, + RecurrenceRelationDiscoveryMode::CertifiedReuse + ); + assert_eq!(report.state, "exact-certified-applied"); + assert_eq!(report.current_count_before, plan.currents().len()); + assert!(report.exact_certified_relation_count > 0); + assert_eq!( + report.applied_relation_count, + report.exact_certified_relation_count + ); + assert_eq!(report.contribution_count_after, plan.contributions().len()); + assert!(report.interaction_evaluation_count_after < report.interaction_evaluation_count_before); + assert_eq!( + plan.contributions() + .iter() + .filter(|row| row.flags & DIRECT_CONTRIBUTION_FLAG_CERTIFIED_REUSE != 0) + .count(), + report.scale_copy_row_count + ); + assert_eq!(report.effective_projection_count, 7); + + let directory = std::env::temp_dir().join(format!( + "rusticol-recurrence-relation-producer-{}", + std::process::id() + )); + let _ = std::fs::remove_dir_all(&directory); + std::fs::create_dir(&directory).unwrap(); + let path = directory.join("recurrence-runtime.pacbin"); + write_recurrence_direct_plan_pacbin(&path, &plan).unwrap(); + assert_eq!(load_recurrence_direct_plan_pacbin(&path).unwrap(), plan); + std::fs::remove_dir_all(directory).unwrap(); +} + +#[test] +fn authenticated_numerical_evidence_applies_only_its_verified_mapping() { + let templates = validated_template(); + let program = count_fixture_program(&templates, 4, 31, 34, 12, 1); + let catalog_digest = digest(40); + let catalog = direct_catalog(catalog_digest); + let semantic_digest = digest(51); + let diagnostic_options = RecurrenceRelationDiscoveryOptions::new( + RecurrenceRelationDiscoveryMode::Diagnostic, + 96, + 4, + 4, + 1.0e-70, + 1.0e-80, + 0x5059_414d, + "lc", + ) + .unwrap(); + let (diagnostic_plan, diagnostic_report) = + lower_recurrence_direct_plan_v2_with_relation_discovery( + &program, + &templates, + &catalog, + semantic_digest, + digest(2), + catalog_digest, + runtime_options(), + &diagnostic_options, + ) + .unwrap(); + let structural_certificate = diagnostic_report + .unwrap() + .certificates + .into_iter() + .next() + .expect("fixture must expose at least one compatible exact mapping"); + let current = diagnostic_plan + .currents() + .get(structural_certificate.current_id as usize) + .unwrap(); + let evidence = RecurrenceNumericalRelationEvidence { + requested_mode: RecurrenceRelationDiscoveryMode::CertifiedReuse, + schedule_semantic_digest: semantic_digest.to_string(), + baseline_runtime_layout_digest: diagnostic_plan.runtime_layout_digest().to_string(), + source_semantics_sha256: crate::recurrence::recurrence_numerical_source_semantics_sha256( + &diagnostic_plan, + "authenticated-numerical-test", + ) + .unwrap(), + runtime_parameter_schema_sha256: digest(60).to_string(), + candidate_observation_batch_sha256: digest(61).to_string(), + verification_observation_batch_sha256: digest(62).to_string(), + decision_sha256: digest(64).to_string(), + rejection_decision_sha256: digest(65).to_string(), + certificate_algorithm: NUMERICAL_RELATION_CERTIFICATE_ALGORITHM.to_owned(), + certificate_set_sha256: digest(63).to_string(), + precision_digits: 96, + probe_count: 4, + verification_probe_count: 4, + relative_tolerance: 1.0e-70, + absolute_tolerance: 1.0e-80, + seed: 0x5059_414d, + numerical_candidate_count: 4, + verification_rejected_count: 3, + rejected_hypothesis_count: 46, + tested_hypothesis_count: 47, + mappings: vec![RecurrenceNumericalCurrentMapping { + current_id: structural_certificate.current_id, + representative_id: Some(structural_certificate.representative_id), + execution_representative_id: structural_certificate.representative_id, + relation_kind: if structural_certificate.factor == ExactComplexRational::ONE { + "equal".to_owned() + } else { + "opposite".to_owned() + }, + factor: structural_certificate.factor, + current_dimension: current.component_count, + certificate_proof_sha256: structural_certificate.proof_sha256, + candidate_observations_sha256: structural_certificate.current_expression_sha256, + verification_observations_sha256: structural_certificate + .representative_expression_sha256, + }], + }; + crate::recurrence::authenticate_recurrence_numerical_relation_provenance( + &evidence, + &diagnostic_plan, + "authenticated-numerical-test", + ) + .unwrap(); + let mut stale_layout = evidence.clone(); + stale_layout.baseline_runtime_layout_digest = digest(61).to_string(); + assert!( + crate::recurrence::authenticate_recurrence_numerical_relation_provenance( + &stale_layout, + &diagnostic_plan, + "authenticated-numerical-test", + ) + .unwrap_err() + .to_string() + .contains("runtime-layout digest") + ); + let mut stale_source = evidence.clone(); + stale_source.source_semantics_sha256 = digest(62).to_string(); + assert!( + crate::recurrence::authenticate_recurrence_numerical_relation_provenance( + &stale_source, + &diagnostic_plan, + "authenticated-numerical-test", + ) + .unwrap_err() + .to_string() + .contains("source-semantics digest") + ); + let options = RecurrenceRelationDiscoveryOptions::new( + RecurrenceRelationDiscoveryMode::CertifiedReuse, + 96, + 4, + 4, + 1.0e-70, + 1.0e-80, + 0x5059_414d, + "lc", + ) + .unwrap() + .with_numerical_evidence(evidence) + .unwrap(); + let (plan, report) = lower_recurrence_direct_plan_v2_with_relation_discovery( + &program, + &templates, + &catalog, + semantic_digest, + digest(2), + catalog_digest, + runtime_options(), + &options, + ) + .unwrap(); + let report = report.unwrap(); + assert_eq!( + report.certificate_algorithm, + NUMERICAL_RELATION_CERTIFICATE_ALGORITHM + ); + assert_eq!(report.tested_hypothesis_count, 47); + assert_eq!(report.verification_rejected_count, 3); + assert_eq!(report.numerical_candidate_count, 4); + assert_eq!(report.exact_certified_relation_count, 1); + assert_eq!(report.applied_relation_count, 1); + assert_eq!(report.scale_copy_row_count, 1); + assert_eq!( + plan.contributions() + .iter() + .filter(|row| row.flags & DIRECT_CONTRIBUTION_FLAG_CERTIFIED_REUSE != 0) + .count(), + 1 + ); + + // Signed numerical reuse remains available outside contracted colour. + let mut opposite_options = options.clone(); + let opposite = &mut opposite_options + .numerical_evidence + .as_mut() + .unwrap() + .mappings[0]; + opposite.relation_kind = "opposite".to_owned(); + opposite.factor = rational(-1); + let (opposite_plan, opposite_report) = lower_recurrence_direct_plan_v2_with_relation_discovery( + &program, + &templates, + &catalog, + semantic_digest, + digest(2), + catalog_digest, + runtime_options(), + &opposite_options, + ) + .unwrap(); + assert_eq!(opposite_report.unwrap().applied_relation_count, 1); + let opposite_copy = opposite_plan + .contributions() + .iter() + .find(|row| row.flags & DIRECT_CONTRIBUTION_FLAG_CERTIFIED_REUSE != 0) + .unwrap(); + assert_eq!( + opposite_plan.exact_factors()[opposite_copy.exact_factor_id as usize], + rational(-1) + ); + + let mut zero_options = options.clone(); + let zero_mapping = &mut zero_options.numerical_evidence.as_mut().unwrap().mappings[0]; + zero_mapping.representative_id = None; + zero_mapping.execution_representative_id = zero_mapping.current_id; + zero_mapping.relation_kind = "zero".to_owned(); + zero_mapping.factor = ExactComplexRational::ZERO; + let (zero_plan, zero_report) = lower_recurrence_direct_plan_v2_with_relation_discovery( + &program, + &templates, + &catalog, + semantic_digest, + digest(2), + catalog_digest, + runtime_options(), + &zero_options, + ) + .unwrap(); + let zero_report = zero_report.unwrap(); + assert_eq!(zero_report.applied_relation_count, 1); + assert_eq!(zero_report.scale_copy_row_count, 1); + assert!( + zero_report.interaction_evaluation_count_after + < zero_report.interaction_evaluation_count_before + ); + let zero_row = zero_plan + .contributions() + .iter() + .find(|row| row.flags & DIRECT_CONTRIBUTION_FLAG_CERTIFIED_REUSE != 0) + .unwrap(); + assert_eq!( + zero_row.parent0_component_base, zero_row.destination_component_base, + "a certified zero current must self-reference its cleared stage destination" + ); + assert_eq!( + zero_plan.exact_factors()[zero_row.exact_factor_id as usize], + ExactComplexRational::ZERO + ); + + let mut mismatched = options.clone(); + mismatched + .numerical_evidence + .as_mut() + .unwrap() + .schedule_semantic_digest = digest(64).to_string(); + assert!( + lower_recurrence_direct_plan_v2_with_relation_discovery( + &program, + &templates, + &catalog, + semantic_digest, + digest(2), + catalog_digest, + runtime_options(), + &mismatched, + ) + .unwrap_err() + .to_string() + .contains("different semantic schedule") + ); + + let mut inconsistent_counters = options.clone(); + inconsistent_counters + .numerical_evidence + .as_mut() + .unwrap() + .verification_rejected_count = 0; + assert!( + lower_recurrence_direct_plan_v2_with_relation_discovery( + &program, + &templates, + &catalog, + semantic_digest, + digest(2), + catalog_digest, + runtime_options(), + &inconsistent_counters, + ) + .unwrap_err() + .to_string() + .contains("counters are inconsistent") + ); + + let mut source_representative = options.clone(); + let mapping = &mut source_representative + .numerical_evidence + .as_mut() + .unwrap() + .mappings[0]; + mapping.representative_id = Some(0); + mapping.execution_representative_id = 0; + assert!( + lower_recurrence_direct_plan_v2_with_relation_discovery( + &program, + &templates, + &catalog, + semantic_digest, + digest(2), + catalog_digest, + runtime_options(), + &source_representative, + ) + .unwrap_err() + .to_string() + .contains("exact current contract") + ); + + let mut source_target = options; + let mapping = &mut source_target.numerical_evidence.as_mut().unwrap().mappings[0]; + mapping.current_id = 0; + mapping.representative_id = None; + mapping.execution_representative_id = 0; + mapping.relation_kind = "zero".to_owned(); + mapping.factor = ExactComplexRational::ZERO; + mapping.current_dimension = diagnostic_plan.currents()[0].component_count; + assert!( + lower_recurrence_direct_plan_v2_with_relation_discovery( + &program, + &templates, + &catalog, + semantic_digest, + digest(2), + catalog_digest, + runtime_options(), + &source_target, + ) + .unwrap_err() + .to_string() + .contains("exact current contract") + ); +} + +#[test] +fn contracted_numerical_reuse_applies_equal_and_zero_without_discarding_signed_evidence() { + let templates = validated_template(); + let base = count_fixture_program(&templates, 4, 31, 34, 12, 1); + let program = contracted_fixture_program(&base); + let catalog_digest = digest(40); + let catalog = direct_catalog(catalog_digest); + let semantic_digest = digest(51); + let options = RecurrenceRelationDiscoveryOptions::new( + RecurrenceRelationDiscoveryMode::Diagnostic, + 96, + 4, + 4, + 1.0e-70, + 1.0e-80, + 0x5059_414d, + "full", + ) + .unwrap(); + let lower = |options: &RecurrenceRelationDiscoveryOptions| { + lower_recurrence_direct_plan_v2_with_relation_discovery( + &program, + &templates, + &catalog, + semantic_digest, + digest(2), + catalog_digest, + runtime_options(), + options, + ) + }; + let (baseline, structural) = lower(&options).unwrap(); + let structural = structural.unwrap(); + assert!(structural.certificates.len() >= 3); + let opposite = &structural.certificates[0]; + let equal = structural + .certificates + .iter() + .find(|certificate| { + certificate.current_id > opposite.current_id + && certificate.representative_id == opposite.representative_id + }) + .expect("fixture contains a later equal current with the same original representative"); + let zero = structural + .certificates + .iter() + .find(|certificate| certificate.current_id > equal.current_id) + .expect("fixture contains another later current"); + let mappings = [opposite, equal, zero] + .into_iter() + .enumerate() + .map(|(index, certificate)| { + let zero = index == 2; + let representative = if index == 1 { + opposite.current_id + } else { + certificate.representative_id + }; + RecurrenceNumericalCurrentMapping { + current_id: certificate.current_id, + representative_id: (!zero).then_some(representative), + execution_representative_id: if zero { + certificate.current_id + } else { + representative + }, + relation_kind: ["opposite", "equal", "zero"][index].to_owned(), + factor: [ + rational(-1), + ExactComplexRational::ONE, + ExactComplexRational::ZERO, + ][index], + current_dimension: baseline.currents()[certificate.current_id as usize] + .component_count, + certificate_proof_sha256: certificate.proof_sha256.clone(), + candidate_observations_sha256: certificate.current_expression_sha256.clone(), + verification_observations_sha256: certificate + .representative_expression_sha256 + .clone(), + } + }) + .collect::>(); + let removed = [1, 2] + .into_iter() + .map(|index| program.currents()[mappings[index].current_id as usize].fan_in() as usize) + .sum::(); + let opposite_base = baseline.currents()[mappings[0].current_id as usize].component_base; + let evidence = RecurrenceNumericalRelationEvidence { + requested_mode: RecurrenceRelationDiscoveryMode::CertifiedReuse, + schedule_semantic_digest: semantic_digest.to_string(), + baseline_runtime_layout_digest: baseline.runtime_layout_digest().to_string(), + source_semantics_sha256: crate::recurrence::recurrence_numerical_source_semantics_sha256( + &baseline, + "contracted-subset-test", + ) + .unwrap(), + runtime_parameter_schema_sha256: digest(60).to_string(), + candidate_observation_batch_sha256: digest(61).to_string(), + verification_observation_batch_sha256: digest(62).to_string(), + decision_sha256: digest(64).to_string(), + rejection_decision_sha256: digest(65).to_string(), + certificate_algorithm: NUMERICAL_RELATION_CERTIFICATE_ALGORITHM.to_owned(), + certificate_set_sha256: digest(63).to_string(), + precision_digits: 96, + probe_count: 4, + verification_probe_count: 4, + relative_tolerance: 1.0e-70, + absolute_tolerance: 1.0e-80, + seed: 0x5059_414d, + numerical_candidate_count: 3, + verification_rejected_count: 0, + rejected_hypothesis_count: 0, + tested_hypothesis_count: 3, + mappings, + }; + let mut apply = options.clone(); + apply.mode = RecurrenceRelationDiscoveryMode::CertifiedReuse; + apply = apply.with_numerical_evidence(evidence).unwrap(); + let (plan, report) = lower(&apply).unwrap(); + let report = report.unwrap(); + assert_eq!(report.exact_certified_relation_count, 3); + assert_eq!(report.certificates.len(), 3); + assert_eq!(report.certificates[0].factor, rational(-1)); + assert_eq!( + report.authenticated_certificate_set_sha256, + Some(digest(63).to_string()) + ); + assert_eq!(report.applied_relation_count, 2); + assert_eq!(report.scale_copy_row_count, 2); + assert_eq!( + report.contribution_count_after, + baseline.contributions().len() - removed + 2 + ); + assert_eq!(report.contribution_count_after, plan.contributions().len()); + assert_eq!( + report.interaction_evaluation_count_after, + baseline.contributions().len() - removed + ); + let copies = plan + .contributions() + .iter() + .filter(|row| row.flags & DIRECT_CONTRIBUTION_FLAG_CERTIFIED_REUSE != 0) + .collect::>(); + assert_eq!(copies.len(), 2); + assert!(copies.iter().all(|row| { + let factor = plan.exact_factors()[row.exact_factor_id as usize]; + factor == ExactComplexRational::ONE || factor == ExactComplexRational::ZERO + })); + assert_eq!( + plan.contributions() + .iter() + .filter(|row| row.destination_component_base == opposite_base) + .count(), + baseline + .contributions() + .iter() + .filter(|row| row.destination_component_base == opposite_base) + .count(), + "the suppressed opposite current keeps its original interactions", + ); + assert!( + copies + .iter() + .all(|row| row.destination_component_base != opposite_base) + ); + assert!( + copies.iter().any(|row| { + row.parent0_component_base == opposite_base + && plan.exact_factors()[row.exact_factor_id as usize] == ExactComplexRational::ONE + }), + "an applied equal relation may read an opposite current whose original work was retained" + ); + + // The non-applied certificate is still authenticated, not discarded. + let mut invalid = apply.clone(); + invalid.numerical_evidence.as_mut().unwrap().mappings[0].current_dimension += 1; + assert!( + lower(&invalid) + .unwrap_err() + .to_string() + .contains("exact current contract") + ); + + let mut diagnostic = apply.clone(); + diagnostic.mode = RecurrenceRelationDiscoveryMode::Diagnostic; + diagnostic + .numerical_evidence + .as_mut() + .unwrap() + .requested_mode = diagnostic.mode; + let (unchanged, diagnostic_report) = lower(&diagnostic).unwrap(); + assert_eq!(unchanged, baseline); + let diagnostic_report = diagnostic_report.unwrap(); + assert_eq!(diagnostic_report.certificates, report.certificates); + assert_eq!(diagnostic_report.applied_relation_count, 0); + + let mut opposite_only = apply; + let evidence = opposite_only.numerical_evidence.as_mut().unwrap(); + evidence.mappings = vec![evidence.mappings[0].clone()]; + evidence.numerical_candidate_count = 1; + evidence.tested_hypothesis_count = 1; + let (unchanged, suppressed) = lower(&opposite_only).unwrap(); + let suppressed = suppressed.unwrap(); + assert_eq!(unchanged, baseline); + assert_eq!(suppressed.certificates.len(), 1); + assert_eq!(suppressed.state, "diagnostic-only"); + assert_eq!(suppressed.applied_relation_count, 0); + assert_eq!(suppressed.scale_copy_row_count, 0); + assert_eq!( + suppressed.contribution_count_after, + baseline.contributions().len() + ); +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/recurrence/direct_pacbin.rs b/pyamplicol/rust/crates/rusticol-core/src/recurrence/direct_pacbin.rs new file mode 100644 index 0000000000..1ed1e5d9dd --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/recurrence/direct_pacbin.rs @@ -0,0 +1,189 @@ +// SPDX-License-Identifier: 0BSD + +//! PACBIN publication and authenticated loading for direct-plan v2. + +use std::io::{BufWriter, Write}; +use std::path::{Path, PathBuf}; + +#[cfg(test)] +use super::direct_codec::encode_recurrence_direct_plan_v2; +use super::direct_codec::{ + decode_recurrence_direct_plan_v2, encode_recurrence_direct_plan_v2_to_writer, +}; +use super::direct_plan::DirectRecurrencePlan; +use crate::pacbin::{ + PACBIN_DEFAULT_CHUNK_SIZE, PacbinMemberKind, PacbinReader, PacbinWriteMember, + PacbinWriteOptions, create_temporary_file, write_pacbin_atomic, +}; +use crate::{RusticolError, RusticolResult}; +use sha2::{Digest, Sha256}; + +pub const RECURRENCE_DIRECT_SCHEDULE_MEMBER: &str = "schedule/recurrence-direct-schedule-v2.bin"; +pub const RECURRENCE_COLOR_PROJECTION_CERTIFICATE_MEMBER: &str = + "proof/recurrence-color-projection-v1.bin"; +const COLOR_PROJECTION_CERTIFICATE_BODY_MAGIC: &[u8] = b"PYAMP-COLOR-PROJECTION-BODY-V1\0\0"; + +struct TemporaryPlanPayload(PathBuf); + +impl Drop for TemporaryPlanPayload { + fn drop(&mut self) { + let _ = std::fs::remove_file(&self.0); + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct RecurrenceDirectPacbinMetadata { + pub container_size: u64, + pub member_count: u64, + pub unpacked_size_bytes: u64, + pub index_sha256: [u8; 32], + pub plan_payload_size: u64, + pub plan_sha256: [u8; 32], + pub projection_certificate_payload_size: Option, + pub projection_certificate_sha256: Option<[u8; 32]>, +} + +pub fn write_recurrence_direct_plan_pacbin( + destination: impl AsRef, + plan: &DirectRecurrencePlan, +) -> RusticolResult { + write_recurrence_direct_plan_pacbin_with_projection_certificate(destination, plan, None) +} + +pub fn write_recurrence_direct_plan_pacbin_with_projection_certificate( + destination: impl AsRef, + plan: &DirectRecurrencePlan, + projection_certificate: Option<&[u8]>, +) -> RusticolResult { + let destination = destination.as_ref(); + let parent = destination + .parent() + .filter(|path| !path.as_os_str().is_empty()) + .unwrap_or_else(|| Path::new(".")); + let (payload_path, payload_file) = create_temporary_file(destination, parent)?; + let payload_guard = TemporaryPlanPayload(payload_path); + let mut payload_writer = BufWriter::with_capacity(PACBIN_DEFAULT_CHUNK_SIZE, payload_file); + let encoded_payload_size = + encode_recurrence_direct_plan_v2_to_writer(plan, &mut payload_writer)?; + payload_writer.flush().map_err(|error| { + RusticolError::artifact(format!( + "could not flush recurrence direct-plan temporary payload {}: {error}", + payload_guard.0.display() + )) + })?; + drop(payload_writer); + let plan_member = PacbinWriteMember::from_path( + RECURRENCE_DIRECT_SCHEDULE_MEMBER, + PacbinMemberKind::RecurrenceDirectPlan, + &payload_guard.0, + )?; + let mut members = vec![plan_member]; + if let Some(certificate) = projection_certificate { + validate_recurrence_color_projection_certificate(certificate)?; + members.push(PacbinWriteMember::from_bytes( + RECURRENCE_COLOR_PROJECTION_CERTIFICATE_MEMBER, + PacbinMemberKind::RecurrenceColorProjectionCertificate, + certificate, + )?); + } + let index = write_pacbin_atomic(destination, members, PacbinWriteOptions::default())?; + let indexed = index + .members() + .iter() + .find(|member| member.logical_path() == RECURRENCE_DIRECT_SCHEDULE_MEMBER) + .ok_or_else(|| RusticolError::artifact("direct recurrence PACBIN has no plan member"))?; + let projection_indexed = index + .members() + .iter() + .find(|member| member.logical_path() == RECURRENCE_COLOR_PROJECTION_CERTIFICATE_MEMBER); + if indexed.length() != encoded_payload_size { + return Err(RusticolError::artifact(format!( + "direct recurrence PACBIN plan length differs from the streamed payload: encoded={encoded_payload_size}, indexed={}", + indexed.length() + ))); + } + Ok(RecurrenceDirectPacbinMetadata { + container_size: index.file_size(), + member_count: index.members().len() as u64, + unpacked_size_bytes: index.members().iter().map(|member| member.length()).sum(), + index_sha256: *index.index_sha256(), + plan_payload_size: indexed.length(), + plan_sha256: *indexed.sha256(), + projection_certificate_payload_size: projection_indexed.map(|member| member.length()), + projection_certificate_sha256: projection_indexed.map(|member| *member.sha256()), + }) +} + +fn validate_projection_certificate_body(body: &[u8]) -> RusticolResult<()> { + let minimum = COLOR_PROJECTION_CERTIFICATE_BODY_MAGIC.len() + 4 + 32; + if body.len() < minimum + || !body.starts_with(COLOR_PROJECTION_CERTIFICATE_BODY_MAGIC) + || body[COLOR_PROJECTION_CERTIFICATE_BODY_MAGIC.len() + ..COLOR_PROJECTION_CERTIFICATE_BODY_MAGIC.len() + 4] + != 1_u32.to_le_bytes() + { + return Err(RusticolError::integrity( + "recurrence color-projection certificate body has invalid framing", + )); + } + let digest_start = body.len() - 32; + let actual: [u8; 32] = Sha256::digest(&body[..digest_start]).into(); + if body[digest_start..] != actual { + return Err(RusticolError::integrity( + "recurrence color-projection certificate body digest mismatch", + )); + } + Ok(()) +} + +pub(crate) fn validate_recurrence_color_projection_certificate( + payload: &[u8], +) -> RusticolResult<()> { + validate_projection_certificate_body(payload) +} + +pub fn load_recurrence_direct_plan_pacbin( + source: impl AsRef, +) -> RusticolResult { + let reader = PacbinReader::open(source)?; + if !matches!(reader.members().len(), 1 | 2) { + return Err(RusticolError::compatibility(format!( + "direct recurrence PACBIN must contain one plan and at most one projection certificate, found {} members", + reader.members().len() + ))); + } + let member = reader + .member(RECURRENCE_DIRECT_SCHEDULE_MEMBER) + .map_err(|_| { + RusticolError::compatibility( + "unsupported recurrence payload; regenerate with direct-plan v2", + ) + })?; + if member.kind() != PacbinMemberKind::RecurrenceDirectPlan { + return Err(RusticolError::compatibility( + "direct recurrence plan has the wrong PACBIN member kind", + )); + } + if reader.members().len() == 2 { + let certificate = reader + .member(RECURRENCE_COLOR_PROJECTION_CERTIFICATE_MEMBER) + .map_err(|_| { + RusticolError::compatibility( + "direct recurrence PACBIN has an unsupported second member", + ) + })?; + if certificate.kind() != PacbinMemberKind::RecurrenceColorProjectionCertificate { + return Err(RusticolError::compatibility( + "recurrence color-projection certificate has the wrong PACBIN member kind", + )); + } + validate_recurrence_color_projection_certificate( + reader.member_bytes(RECURRENCE_COLOR_PROJECTION_CERTIFICATE_MEMBER)?, + )?; + } + decode_recurrence_direct_plan_v2(reader.member_bytes(RECURRENCE_DIRECT_SCHEDULE_MEMBER)?) +} + +#[cfg(test)] +#[path = "direct_pacbin_tests.rs"] +mod tests; diff --git a/pyamplicol/rust/crates/rusticol-core/src/recurrence/direct_pacbin_tests.rs b/pyamplicol/rust/crates/rusticol-core/src/recurrence/direct_pacbin_tests.rs new file mode 100644 index 0000000000..6fae5979d2 --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/recurrence/direct_pacbin_tests.rs @@ -0,0 +1,254 @@ +// SPDX-License-Identifier: 0BSD + +use std::fs; +use std::sync::atomic::{AtomicU64, Ordering}; + +use sha2::{Digest, Sha256}; + +use super::*; +use crate::pacbin::{ + PacbinMemberKind, PacbinReader, PacbinWriteMember, PacbinWriteOptions, write_pacbin_atomic, +}; +use crate::recurrence::direct_plan::tests::valid_plan; + +fn temporary_directory(label: &str) -> std::path::PathBuf { + static NEXT: AtomicU64 = AtomicU64::new(0); + let module = module_path!().replace("::", "-"); + let path = std::env::temp_dir().join(format!( + "rusticol-recurrence-direct-{module}-{label}-{}-{}", + std::process::id(), + NEXT.fetch_add(1, Ordering::Relaxed) + )); + fs::create_dir(&path).unwrap(); + path +} + +fn projection_certificate() -> Vec { + let mut body = COLOR_PROJECTION_CERTIFICATE_BODY_MAGIC.to_vec(); + body.extend_from_slice(&1_u32.to_le_bytes()); + body.extend_from_slice(b"deterministic-structural-proof"); + let digest: [u8; 32] = Sha256::digest(&body).into(); + body.extend_from_slice(&digest); + body +} + +#[test] +fn direct_pacbin_round_trips_with_one_aligned_indexed_member() { + let directory = temporary_directory("roundtrip"); + let path = directory.join("recurrence-runtime.pacbin"); + let plan = valid_plan(); + let metadata = write_recurrence_direct_plan_pacbin(&path, &plan).unwrap(); + assert_eq!(load_recurrence_direct_plan_pacbin(&path).unwrap(), plan); + + let reader = PacbinReader::open(&path).unwrap(); + let member = reader.member(RECURRENCE_DIRECT_SCHEDULE_MEMBER).unwrap(); + assert_eq!(member.kind(), PacbinMemberKind::RecurrenceDirectPlan); + assert_eq!(member.offset() % 64, 0); + assert_eq!(member.length(), metadata.plan_payload_size); + assert_eq!(member.sha256(), &metadata.plan_sha256); + assert_eq!( + reader + .member_range(RECURRENCE_DIRECT_SCHEDULE_MEMBER, 0, 8) + .unwrap(), + b"PACRDAP2" + ); + let retained: Vec<_> = fs::read_dir(&directory) + .unwrap() + .map(|entry| entry.unwrap().file_name()) + .collect(); + assert_eq!( + retained, + [std::ffi::OsString::from("recurrence-runtime.pacbin")] + ); + fs::remove_dir_all(directory).unwrap(); +} + +#[test] +fn direct_pacbin_is_deterministic() { + let directory = temporary_directory("deterministic"); + let first = directory.join("first.pacbin"); + let second = directory.join("second.pacbin"); + write_recurrence_direct_plan_pacbin(&first, &valid_plan()).unwrap(); + write_recurrence_direct_plan_pacbin(&second, &valid_plan()).unwrap(); + assert_eq!(fs::read(&first).unwrap(), fs::read(&second).unwrap()); + fs::remove_dir_all(directory).unwrap(); +} + +#[test] +fn direct_pacbin_removes_the_streaming_temporary_payload_on_error() { + let directory = temporary_directory("stream-cleanup"); + let path = directory.join("recurrence-runtime.pacbin"); + let error = write_recurrence_direct_plan_pacbin_with_projection_certificate( + &path, + &valid_plan(), + Some(b"invalid certificate"), + ) + .unwrap_err(); + assert!(error.to_string().contains("certificate")); + assert_eq!(fs::read_dir(&directory).unwrap().count(), 0); + fs::remove_dir_all(directory).unwrap(); +} + +#[test] +fn direct_pacbin_round_trips_with_a_structural_projection_certificate() { + let directory = temporary_directory("projection-certificate"); + let path = directory.join("recurrence-runtime.pacbin"); + let plan = valid_plan(); + let certificate = projection_certificate(); + let metadata = write_recurrence_direct_plan_pacbin_with_projection_certificate( + &path, + &plan, + Some(&certificate), + ) + .unwrap(); + assert_eq!(metadata.member_count, 2); + assert_eq!( + metadata.projection_certificate_payload_size, + Some(certificate.len() as u64) + ); + assert_eq!(load_recurrence_direct_plan_pacbin(&path).unwrap(), plan); + let reader = PacbinReader::open(&path).unwrap(); + let member = reader + .member(RECURRENCE_COLOR_PROJECTION_CERTIFICATE_MEMBER) + .unwrap(); + assert_eq!( + member.kind(), + PacbinMemberKind::RecurrenceColorProjectionCertificate + ); + assert_eq!( + reader + .member_bytes(RECURRENCE_COLOR_PROJECTION_CERTIFICATE_MEMBER) + .unwrap(), + certificate + ); + fs::remove_dir_all(directory).unwrap(); +} + +#[test] +fn direct_pacbin_rejects_a_corrupt_projection_certificate() { + let directory = temporary_directory("projection-certificate-corrupt"); + let path = directory.join("recurrence-runtime.pacbin"); + let payload = encode_recurrence_direct_plan_v2(&valid_plan()).unwrap(); + let mut certificate = projection_certificate(); + certificate[COLOR_PROJECTION_CERTIFICATE_BODY_MAGIC.len() + 12] ^= 1; + write_pacbin_atomic( + &path, + [ + PacbinWriteMember::from_bytes( + RECURRENCE_DIRECT_SCHEDULE_MEMBER, + PacbinMemberKind::RecurrenceDirectPlan, + &payload, + ) + .unwrap(), + PacbinWriteMember::from_bytes( + RECURRENCE_COLOR_PROJECTION_CERTIFICATE_MEMBER, + PacbinMemberKind::RecurrenceColorProjectionCertificate, + &certificate, + ) + .unwrap(), + ], + PacbinWriteOptions::default(), + ) + .unwrap(); + assert!(load_recurrence_direct_plan_pacbin(&path).is_err()); + fs::remove_dir_all(directory).unwrap(); +} + +#[test] +fn direct_pacbin_rejects_truncation_and_payload_corruption() { + let directory = temporary_directory("corruption"); + let path = directory.join("recurrence-runtime.pacbin"); + write_recurrence_direct_plan_pacbin(&path, &valid_plan()).unwrap(); + let original = fs::read(&path).unwrap(); + + fs::write(&path, &original[..original.len() - 1]).unwrap(); + assert!(load_recurrence_direct_plan_pacbin(&path).is_err()); + + fs::write(&path, &original).unwrap(); + let reader = PacbinReader::open(&path).unwrap(); + let payload_offset = usize::try_from( + reader + .member(RECURRENCE_DIRECT_SCHEDULE_MEMBER) + .unwrap() + .offset(), + ) + .unwrap(); + drop(reader); + let mut corrupted = original; + corrupted[payload_offset + 20] ^= 1; + fs::write(&path, corrupted).unwrap(); + assert!(load_recurrence_direct_plan_pacbin(&path).is_err()); + fs::remove_dir_all(directory).unwrap(); +} + +#[test] +fn direct_pacbin_rejects_old_v1_path_without_aliasing() { + let directory = temporary_directory("old-path"); + let path = directory.join("recurrence-runtime.pacbin"); + let payload = encode_recurrence_direct_plan_v2(&valid_plan()).unwrap(); + write_pacbin_atomic( + &path, + [PacbinWriteMember::from_bytes( + "plan/recurrence-plan-v1.bin", + PacbinMemberKind::RecurrenceDirectPlan, + &payload, + ) + .unwrap()], + PacbinWriteOptions::default(), + ) + .unwrap(); + let error = load_recurrence_direct_plan_pacbin(&path).unwrap_err(); + assert!(error.to_string().contains("regenerate with direct-plan v2")); + fs::remove_dir_all(directory).unwrap(); +} + +#[test] +fn direct_pacbin_rejects_wrong_member_kind_and_extra_members() { + let directory = temporary_directory("allowlist"); + let path = directory.join("recurrence-runtime.pacbin"); + let payload = encode_recurrence_direct_plan_v2(&valid_plan()).unwrap(); + write_pacbin_atomic( + &path, + [PacbinWriteMember::from_bytes( + RECURRENCE_DIRECT_SCHEDULE_MEMBER, + PacbinMemberKind::EagerRuntimeTable, + &payload, + ) + .unwrap()], + PacbinWriteOptions::default(), + ) + .unwrap(); + assert!( + load_recurrence_direct_plan_pacbin(&path) + .unwrap_err() + .to_string() + .contains("wrong PACBIN member kind") + ); + + write_pacbin_atomic( + &path, + [ + PacbinWriteMember::from_bytes( + RECURRENCE_DIRECT_SCHEDULE_MEMBER, + PacbinMemberKind::RecurrenceDirectPlan, + &payload, + ) + .unwrap(), + PacbinWriteMember::from_bytes( + "metadata/extra.bin", + PacbinMemberKind::EagerRuntimeMetadata, + b"unexpected", + ) + .unwrap(), + ], + PacbinWriteOptions::default(), + ) + .unwrap(); + assert!( + load_recurrence_direct_plan_pacbin(&path) + .unwrap_err() + .to_string() + .contains("unsupported second member") + ); + fs::remove_dir_all(directory).unwrap(); +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/recurrence/direct_plan.rs b/pyamplicol/rust/crates/rusticol-core/src/recurrence/direct_plan.rs new file mode 100644 index 0000000000..8fd1d13fa1 --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/recurrence/direct_plan.rs @@ -0,0 +1,2424 @@ +// SPDX-License-Identifier: 0BSD + +//! Immutable direct-arena recurrence plan. +//! +//! Semantic current IDs are deliberately distinct from physical arena +//! component ranges. The latter may be reused after interval coloring. + +use std::collections::BTreeSet; + +use sha2::{Digest, Sha256}; + +use super::{ + ClosureProofMetadataV2, ExactComplexRational, RecurrenceStrategy, SemanticDigest, + closure_component_factor_digest_v2, closure_selector_domain_digest_v2, +}; +use crate::{RusticolError, RusticolResult}; + +pub const RECURRENCE_DIRECT_PLAN_ABI: &str = "pyamplicol-recurrence-plan-v2"; +pub const RECURRENCE_DIRECT_RUNTIME_LAYOUT_ABI: &str = "pyamplicol-recurrence-runtime-layout-v2"; +pub const RECURRENCE_DIRECT_TEMPLATE_ABI: &str = "pyamplicol-recurrence-direct-template-v1"; +pub const RECURRENCE_DIRECT_RUNTIME_CAPABILITY: &str = + "rusticol.recurrence-direct-arena.complex-f64.v1"; +pub const DIRECT_NONE_U32: u32 = u32::MAX; +pub const DIRECT_CONTRIBUTION_FLAG_INITIALIZE_DESTINATION: u32 = 1 << 0; +/// The row scale-copies a certified representative current. For these rows +/// `parent0_momentum_form_id` stores the component count; no momentum is read. +pub const DIRECT_CONTRIBUTION_FLAG_CERTIFIED_REUSE: u32 = 1 << 1; +const DIRECT_CONTRIBUTION_FLAGS_KNOWN: u32 = + DIRECT_CONTRIBUTION_FLAG_INITIALIZE_DESTINATION | DIRECT_CONTRIBUTION_FLAG_CERTIFIED_REUSE; + +fn invalid(message: impl Into) -> RusticolError { + RusticolError::invalid_argument(format!("recurrence direct plan: {}", message.into())) +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[repr(u16)] +pub enum DirectNodeKind { + Source = 0, + Current = 1, +} + +impl TryFrom for DirectNodeKind { + type Error = RusticolError; + + fn try_from(value: u16) -> Result { + match value { + 0 => Ok(Self::Source), + 1 => Ok(Self::Current), + _ => Err(invalid(format!("unknown node-kind discriminant {value}"))), + } + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, bincode::Encode, bincode::Decode)] +#[repr(u16)] +pub enum DirectExecutorRole { + Source = 0, + Contribution = 1, + Finalization = 2, + Closure = 3, +} + +impl TryFrom for DirectExecutorRole { + type Error = RusticolError; + + fn try_from(value: u16) -> Result { + match value { + 0 => Ok(Self::Source), + 1 => Ok(Self::Contribution), + 2 => Ok(Self::Finalization), + 3 => Ok(Self::Closure), + _ => Err(invalid(format!( + "unknown direct-executor-role discriminant {value}" + ))), + } + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[repr(u16)] +pub enum DirectDestinationOperation { + Initialize = 0, + Add = 1, + FinalizeInPlace = 2, + ClosureAdd = 3, +} + +impl TryFrom for DirectDestinationOperation { + type Error = RusticolError; + + fn try_from(value: u16) -> Result { + match value { + 0 => Ok(Self::Initialize), + 1 => Ok(Self::Add), + 2 => Ok(Self::FinalizeInPlace), + 3 => Ok(Self::ClosureAdd), + _ => Err(invalid(format!( + "unknown destination-operation discriminant {value}" + ))), + } + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[repr(C)] +pub struct DirectCurrentDescriptor { + pub semantic_current_id: u32, + pub node_kind: DirectNodeKind, + pub state_template_id: u32, + pub component_base: u32, + pub component_count: u16, + pub momentum_form_id: u32, + pub stage: u16, + pub selector_domain_id: u32, + pub first_use: u32, + pub last_use: u32, + pub source_row_or_sentinel: u32, + pub finalization_row_or_sentinel: u32, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq, bincode::Encode, bincode::Decode)] +#[repr(C)] +pub struct DirectSourceRow { + pub source_slot: u32, + pub destination_component_base: u32, + pub momentum_form_id: u32, + pub source_template_or_dispatch_domain: u32, + pub spin_state_class: i32, + pub exact_factor_id: u32, + pub selector_domain_id: u32, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq, bincode::Encode, bincode::Decode)] +#[repr(C)] +pub struct DirectContributionRow { + pub parent0_component_base: u32, + pub parent1_component_base_or_sentinel: u32, + pub parent0_momentum_form_id: u32, + pub parent1_momentum_form_id_or_sentinel: u32, + pub destination_component_base: u32, + pub exact_factor_id: u32, + pub selector_domain_id: u32, + pub flags: u32, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq, bincode::Encode, bincode::Decode)] +#[repr(C)] +pub struct DirectFinalizationRow { + pub component_base: u32, + pub component_count: u16, + pub momentum_form_id: u32, + pub exact_factor_id: u32, + pub selector_domain_id: u32, + pub flags: u32, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq, bincode::Encode, bincode::Decode)] +#[repr(C)] +pub struct DirectClosureRow { + pub parent0_component_base: u32, + pub parent1_component_base_or_sentinel: u32, + pub parent0_momentum_form_id: u32, + pub parent1_momentum_form_id_or_sentinel: u32, + pub amplitude_destination_id: u32, + pub exact_factor_id: u32, + pub component_factor_start: u32, + pub component_count: u16, + pub selector_domain_id: u32, + /// Cold closure-proof group ID. Runtime executors deliberately ignore it. + /// + /// This occupies the former unused closure flags word, preserving the + /// native 40-byte row layout. + pub flags: u32, +} + +impl DirectClosureRow { + pub const fn proof_group_id(&self) -> u32 { + self.flags + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[repr(C)] +pub struct DirectRowGroupDescriptor { + pub stage: u16, + pub role: DirectExecutorRole, + pub destination_operation: DirectDestinationOperation, + pub direct_executor_id: u32, + pub row_start: u64, + pub row_count: u32, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[repr(C)] +pub struct DirectMomentumFormDescriptor { + pub term_start: u64, + pub term_count: u32, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[repr(C)] +pub struct DirectMomentumTerm { + pub source_slot: u32, + pub coefficient: i32, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[repr(C)] +pub struct DirectSelectorDomainDescriptor { + pub word_start: u64, + pub word_count: u32, +} + +/// Auditable amount of persisted direct work active for one physical sector. +/// +/// Topology-replay artifacts retain one union plan so public flows can share +/// currents. These counters make the distinction between that persisted +/// union and the rows actually executed by a selected-flow call explicit. +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub struct DirectSelectorWorkSummary { + pub physical_sector_id: u32, + pub current_count: u64, + pub semantic_component_count: u64, + pub source_row_count: u64, + pub contribution_count: u64, + pub finalization_count: u64, + pub closure_count: u64, + pub amplitude_destination_count: u64, +} + +impl DirectSelectorWorkSummary { + pub const fn row_count(&self) -> u64 { + self.source_row_count + + self.contribution_count + + self.finalization_count + + self.closure_count + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[repr(C)] +pub struct DirectReplayTargetDescriptor { + pub public_flow_id: u32, + pub representative_id: u32, + pub source_permutation_start: u64, + pub source_permutation_count: u32, + pub helicity_map_start: u64, + pub helicity_map_count: u32, + pub phase_exact_factor_id: u32, + pub multiplicity: u32, + pub selector_domain_id: u32, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[repr(C)] +pub struct DirectAmplitudeDestinationDescriptor { + pub closure_row_start: u64, + pub id: u32, + pub target_sector_id: u32, + pub target_helicity_id_or_sentinel: u32, + pub closure_row_count: u32, + pub selector_domain_id: u32, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[repr(C)] +pub struct DirectResolvedHelicityDescriptor { + pub source_state_start: u64, + pub source_selection_start: u64, + pub public_helicity_start: u64, + pub id: u32, + pub source_state_count: u32, + pub source_selection_count: u32, + pub public_helicity_count: u32, + pub selector_domain_id: u32, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[repr(C)] +pub struct DirectSourceStateAssignment { + pub source_slot: u32, + pub state_index: u32, +} + +/// One concrete process-bound source variant referenced by resolved helicities. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[repr(C)] +pub struct DirectSourceDispatchVariantDescriptor { + pub embedding_start: u64, + pub projection_start: u64, + pub source_row_id: u32, + pub dispatch_domain_id: u32, + pub runtime_variant_id: u32, + pub source_state_index: u32, + pub source_template_id: u32, + pub source_state_template_id: u32, + pub crossed_state_template_id: u32, + pub crossed_spin_state_class: i32, + pub direct_executor_id: u32, + pub crossing_exact_factor_id: u32, + pub embedding_count: u32, + pub projection_count: u32, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[repr(C)] +pub struct DirectSourceEmbeddingRow { + pub full_component: u32, + pub source_component_or_sentinel: u32, + pub exact_factor_id: u32, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[repr(C)] +pub struct DirectSourceProjectionRow { + pub source_component: u32, + pub full_component: u32, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[repr(C)] +pub struct DirectResolvedSourceSelection { + pub source_slot: u32, + pub dispatch_variant_id: u32, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct DirectRecurrencePlanParts { + pub strategy: RecurrenceStrategy, + pub semantic_digest: SemanticDigest, + pub prepared_pack_digest: SemanticDigest, + pub direct_template_catalog_digest: SemanticDigest, + pub point_tile_size: u32, + pub workspace_mib: u32, + pub current_arena_components: u32, + pub physical_sector_count: u32, + pub retained_helicity_count: u64, + pub amplitude_destination_count: u32, + pub parameter_value_count: u32, + pub external_source_count: u32, + pub state_template_count: u32, + pub source_template_count: u32, + pub source_template_or_dispatch_count: u32, + pub runtime_helicity_contract_count: u32, + pub runtime_helicity_variant_count: u32, + pub direct_executor_count: u32, + pub currents: Vec, + pub sources: Vec, + pub contributions: Vec, + pub finalizations: Vec, + pub closures: Vec, + pub row_groups: Vec, + pub momentum_forms: Vec, + pub momentum_terms: Vec, + pub selector_domains: Vec, + pub selector_words: Vec, + pub replay_targets: Vec, + pub source_permutations: Vec, + pub replay_momentum_signs: Vec, + pub replay_helicity_map: Vec, + pub amplitude_destinations: Vec, + pub resolved_helicities: Vec, + pub source_state_assignments: Vec, + pub source_dispatch_variants: Vec, + pub source_embeddings: Vec, + pub source_projections: Vec, + pub resolved_source_selections: Vec, + pub public_helicities: Vec, + pub exact_factors: Vec, + pub closure_proofs: ClosureProofMetadataV2, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct DirectRecurrencePlan { + parts: DirectRecurrencePlanParts, + runtime_layout_digest: SemanticDigest, + closure_proof_rows_retained: bool, +} + +fn selector_domain_contains_parts( + parts: &DirectRecurrencePlanParts, + selector_domain_id: u32, + physical_sector_id: u32, +) -> RusticolResult { + if physical_sector_id >= parts.physical_sector_count { + return Err(invalid("physical selector sector is out of bounds")); + } + let descriptor = parts + .selector_domains + .get(selector_domain_id as usize) + .ok_or_else(|| invalid("selector domain is absent"))?; + let start = usize::try_from(descriptor.word_start) + .map_err(|_| invalid("selector-domain word start exceeds usize"))?; + let end = start + .checked_add(descriptor.word_count as usize) + .ok_or_else(|| invalid("selector-domain word range overflows usize"))?; + let words = parts + .selector_words + .get(start..end) + .ok_or_else(|| invalid("selector-domain word range is out of bounds"))?; + if words == [u64::MAX] { + return Ok(true); + } + let word = physical_sector_id as usize / u64::BITS as usize; + let bit = physical_sector_id % u64::BITS; + Ok(words + .get(word) + .is_some_and(|value| value & (1_u64 << bit) != 0)) +} + +impl DirectRecurrencePlan { + #[cfg_attr(target_vendor = "apple", unsafe(link_section = "__TEXT,__rcl_load"))] + #[cfg_attr(target_vendor = "apple", inline(never))] + pub fn new(mut parts: DirectRecurrencePlanParts) -> RusticolResult { + #[cfg(feature = "on-the-fly-test-support")] + super::on_the_fly::reject_forbidden_work_if_probed( + super::on_the_fly::OnTheFlyForbiddenWorkV1::DirectPlanMaterialization, + )?; + canonicalize_contribution_initialization(&mut parts)?; + validate_parts(&parts)?; + let runtime_layout_digest = digest_parts(&parts)?; + Ok(Self { + parts, + runtime_layout_digest, + closure_proof_rows_retained: true, + }) + } + + pub(crate) fn into_runtime_compacted(mut self) -> Self { + self.parts.closure_proofs.discard_rows_for_runtime(); + self.closure_proof_rows_retained = false; + self + } + + pub(crate) fn ensure_serializable(&self) -> RusticolResult<()> { + if !self.closure_proof_rows_retained { + return Err(invalid( + "runtime-compacted plan cannot be serialized without closure-proof rows", + )); + } + Ok(()) + } + + pub const fn strategy(&self) -> RecurrenceStrategy { + self.parts.strategy + } + + pub const fn semantic_digest(&self) -> SemanticDigest { + self.parts.semantic_digest + } + + pub const fn prepared_pack_digest(&self) -> SemanticDigest { + self.parts.prepared_pack_digest + } + + pub const fn direct_template_catalog_digest(&self) -> SemanticDigest { + self.parts.direct_template_catalog_digest + } + + pub const fn runtime_layout_digest(&self) -> SemanticDigest { + self.runtime_layout_digest + } + + pub const fn point_tile_size(&self) -> u32 { + self.parts.point_tile_size + } + + pub const fn workspace_mib(&self) -> u32 { + self.parts.workspace_mib + } + + pub const fn current_arena_components(&self) -> u32 { + self.parts.current_arena_components + } + + pub const fn physical_sector_count(&self) -> u32 { + self.parts.physical_sector_count + } + + pub const fn retained_helicity_count(&self) -> u64 { + self.parts.retained_helicity_count + } + + pub const fn amplitude_destination_count(&self) -> u32 { + self.parts.amplitude_destination_count + } + + pub const fn parameter_value_count(&self) -> u32 { + self.parts.parameter_value_count + } + + pub const fn external_source_count(&self) -> u32 { + self.parts.external_source_count + } + + pub const fn state_template_count(&self) -> u32 { + self.parts.state_template_count + } + + pub const fn source_template_count(&self) -> u32 { + self.parts.source_template_count + } + + pub const fn source_template_or_dispatch_count(&self) -> u32 { + self.parts.source_template_or_dispatch_count + } + + pub const fn runtime_helicity_contract_count(&self) -> u32 { + self.parts.runtime_helicity_contract_count + } + + pub const fn runtime_helicity_variant_count(&self) -> u32 { + self.parts.runtime_helicity_variant_count + } + + pub const fn direct_executor_count(&self) -> u32 { + self.parts.direct_executor_count + } + + pub fn currents(&self) -> &[DirectCurrentDescriptor] { + &self.parts.currents + } + + pub fn sources(&self) -> &[DirectSourceRow] { + &self.parts.sources + } + + pub fn contributions(&self) -> &[DirectContributionRow] { + &self.parts.contributions + } + + pub fn finalizations(&self) -> &[DirectFinalizationRow] { + &self.parts.finalizations + } + + pub fn closures(&self) -> &[DirectClosureRow] { + &self.parts.closures + } + + pub fn row_groups(&self) -> &[DirectRowGroupDescriptor] { + &self.parts.row_groups + } + + pub fn momentum_forms(&self) -> &[DirectMomentumFormDescriptor] { + &self.parts.momentum_forms + } + + pub fn momentum_terms(&self) -> &[DirectMomentumTerm] { + &self.parts.momentum_terms + } + + pub fn selector_domains(&self) -> &[DirectSelectorDomainDescriptor] { + &self.parts.selector_domains + } + + pub fn selector_words(&self) -> &[u64] { + &self.parts.selector_words + } + + /// Return whether one physical selector bit belongs to a persisted domain. + /// + /// Direct-plan-v2 artifacts produced before selector-aware lowering used + /// one literal `[u64::MAX]` domain for every row, even when a process had + /// more than 64 physical sectors. Preserve that encoding as the + /// backwards-compatible universal domain while allowing newer plans to + /// persist ordinary multiword sector masks. + pub fn selector_domain_contains( + &self, + selector_domain_id: u32, + physical_sector_id: u32, + ) -> RusticolResult { + selector_domain_contains_parts(&self.parts, selector_domain_id, physical_sector_id) + } + + /// Count the persisted currents and executor rows active for one sector. + /// + /// This is derived only from authenticated plan tables and therefore forms + /// a compact structural certificate for selected-flow pruning. + pub fn selector_work_summary( + &self, + physical_sector_id: u32, + ) -> RusticolResult { + if physical_sector_id >= self.parts.physical_sector_count { + return Err(invalid("physical selector sector is out of bounds")); + } + let mut summary = DirectSelectorWorkSummary { + physical_sector_id, + ..DirectSelectorWorkSummary::default() + }; + for current in &self.parts.currents { + if self.selector_domain_contains(current.selector_domain_id, physical_sector_id)? { + summary.current_count = summary + .current_count + .checked_add(1) + .ok_or_else(|| invalid("selector current count overflows u64"))?; + summary.semantic_component_count = summary + .semantic_component_count + .checked_add(u64::from(current.component_count)) + .ok_or_else(|| invalid("selector semantic component count overflows u64"))?; + } + } + for row in &self.parts.sources { + if self.selector_domain_contains(row.selector_domain_id, physical_sector_id)? { + summary.source_row_count += 1; + } + } + for row in &self.parts.contributions { + if self.selector_domain_contains(row.selector_domain_id, physical_sector_id)? { + summary.contribution_count += 1; + } + } + for row in &self.parts.finalizations { + if self.selector_domain_contains(row.selector_domain_id, physical_sector_id)? { + summary.finalization_count += 1; + } + } + for row in &self.parts.closures { + if self.selector_domain_contains(row.selector_domain_id, physical_sector_id)? { + summary.closure_count += 1; + } + } + for destination in &self.parts.amplitude_destinations { + if self.selector_domain_contains(destination.selector_domain_id, physical_sector_id)? { + summary.amplitude_destination_count += 1; + } + } + Ok(summary) + } + + pub fn replay_targets(&self) -> &[DirectReplayTargetDescriptor] { + &self.parts.replay_targets + } + + pub fn source_permutations(&self) -> &[u32] { + &self.parts.source_permutations + } + + pub fn replay_momentum_signs(&self) -> &[i32] { + &self.parts.replay_momentum_signs + } + + pub fn replay_helicity_map(&self) -> &[u32] { + &self.parts.replay_helicity_map + } + + pub fn amplitude_destinations(&self) -> &[DirectAmplitudeDestinationDescriptor] { + &self.parts.amplitude_destinations + } + + pub fn resolved_helicities(&self) -> &[DirectResolvedHelicityDescriptor] { + &self.parts.resolved_helicities + } + + pub fn source_state_assignments(&self) -> &[DirectSourceStateAssignment] { + &self.parts.source_state_assignments + } + + pub fn source_dispatch_variants(&self) -> &[DirectSourceDispatchVariantDescriptor] { + &self.parts.source_dispatch_variants + } + + pub fn source_embeddings(&self) -> &[DirectSourceEmbeddingRow] { + &self.parts.source_embeddings + } + + pub fn source_projections(&self) -> &[DirectSourceProjectionRow] { + &self.parts.source_projections + } + + pub fn resolved_source_selections(&self) -> &[DirectResolvedSourceSelection] { + &self.parts.resolved_source_selections + } + + pub fn public_helicities(&self) -> &[i32] { + &self.parts.public_helicities + } + + pub fn exact_factors(&self) -> &[ExactComplexRational] { + &self.parts.exact_factors + } + + pub const fn closure_proofs(&self) -> &ClosureProofMetadataV2 { + &self.parts.closure_proofs + } + + pub fn into_parts(self) -> DirectRecurrencePlanParts { + assert!( + self.closure_proof_rows_retained, + "runtime-compacted DirectRecurrencePlan cannot be converted back into complete parts" + ); + self.parts + } +} + +fn canonicalize_contribution_initialization( + parts: &mut DirectRecurrencePlanParts, +) -> RusticolResult<()> { + let current_by_stage_and_base = parts + .currents + .iter() + .filter(|current| current.node_kind == DirectNodeKind::Current) + .map(|current| { + ( + (current.stage, current.component_base), + current.semantic_current_id, + ) + }) + .collect::>(); + let mut first_row_by_current = std::collections::BTreeMap::::new(); + let mut initialization_count_by_current = std::collections::BTreeMap::::new(); + + for row_group in parts + .row_groups + .iter() + .filter(|row_group| row_group.role == DirectExecutorRole::Contribution) + { + let start = usize::try_from(row_group.row_start) + .map_err(|_| invalid("contribution row-group start exceeds usize"))?; + let Some(end) = start.checked_add(row_group.row_count as usize) else { + continue; + }; + let Some(rows) = parts.contributions.get(start..end) else { + continue; + }; + for (offset, row) in rows.iter().enumerate() { + let Some(current_id) = current_by_stage_and_base + .get(&(row_group.stage, row.destination_component_base)) + .copied() + else { + continue; + }; + first_row_by_current + .entry(current_id) + .or_insert(start + offset); + if row.flags & DIRECT_CONTRIBUTION_FLAG_INITIALIZE_DESTINATION != 0 { + *initialization_count_by_current + .entry(current_id) + .or_default() += 1; + } + } + } + + for (current_id, row_index) in first_row_by_current { + if initialization_count_by_current + .get(¤t_id) + .copied() + .unwrap_or(0) + == 0 + { + parts.contributions[row_index].flags |= DIRECT_CONTRIBUTION_FLAG_INITIALIZE_DESTINATION; + } + } + Ok(()) +} + +#[cfg_attr(target_vendor = "apple", unsafe(link_section = "__TEXT,__rcl_load"))] +#[cfg_attr(target_vendor = "apple", inline(never))] +#[cfg_attr(target_vendor = "apple", cold)] +fn validate_parts(parts: &DirectRecurrencePlanParts) -> RusticolResult<()> { + if parts.point_tile_size == 0 { + return Err(invalid("point tile size must be positive")); + } + if parts.workspace_mib == 0 { + return Err(invalid("workspace MiB must be positive")); + } + if parts.current_arena_components == 0 { + return Err(invalid("current arena must contain at least one component")); + } + if parts.physical_sector_count == 0 { + return Err(invalid("plan must retain at least one physical LC sector")); + } + if parts.retained_helicity_count == 0 { + return Err(invalid("plan must retain at least one physical helicity")); + } + if parts.amplitude_destination_count == 0 { + return Err(invalid( + "plan must contain at least one amplitude destination", + )); + } + if usize::try_from(parts.amplitude_destination_count).ok() + != Some(parts.amplitude_destinations.len()) + { + return Err(invalid( + "amplitude destination count does not match its descriptor catalog", + )); + } + if parts.external_source_count == 0 { + return Err(invalid("plan must contain at least one external source")); + } + for (label, count) in [ + ("state templates", parts.state_template_count), + ("source templates", parts.source_template_count), + ( + "source templates or dispatch domains", + parts.source_template_or_dispatch_count, + ), + ("direct executors", parts.direct_executor_count), + ] { + if count == 0 { + return Err(invalid(format!("{label} catalog must not be empty"))); + } + } + if parts.strategy == RecurrenceStrategy::AllFlowUnion + && (parts.runtime_helicity_contract_count == 0 || parts.runtime_helicity_variant_count == 0) + { + return Err(invalid( + "all-flow-union requires runtime-helicity contracts and variants", + )); + } + if parts.momentum_forms.is_empty() { + return Err(invalid("plan must contain at least one momentum form")); + } + if parts.selector_domains.is_empty() { + return Err(invalid("plan must contain at least one selector domain")); + } + if parts.exact_factors.is_empty() { + return Err(invalid("plan must contain at least one exact factor")); + } + if parts.replay_momentum_signs.len() != parts.source_permutations.len() + || parts + .replay_momentum_signs + .iter() + .any(|sign| !matches!(sign, -1 | 1)) + { + return Err(invalid( + "replay momentum signs must align one-to-one with source permutations", + )); + } + + validate_ranges( + "momentum form", + parts + .momentum_forms + .iter() + .map(|row| (row.term_start, row.term_count)), + parts.momentum_terms.len(), + true, + )?; + validate_ranges( + "selector domain", + parts + .selector_domains + .iter() + .map(|row| (row.word_start, row.word_count)), + parts.selector_words.len(), + true, + )?; + validate_ranges( + "replay source permutation", + parts + .replay_targets + .iter() + .map(|row| (row.source_permutation_start, row.source_permutation_count)), + parts.source_permutations.len(), + true, + )?; + validate_ranges( + "replay helicity map", + parts + .replay_targets + .iter() + .map(|row| (row.helicity_map_start, row.helicity_map_count)), + parts.replay_helicity_map.len(), + true, + )?; + validate_ranges( + "amplitude destination closure", + parts + .amplitude_destinations + .iter() + .map(|row| (row.closure_row_start, row.closure_row_count)), + parts.closures.len(), + true, + )?; + validate_ranges( + "resolved-helicity source state", + parts + .resolved_helicities + .iter() + .map(|row| (row.source_state_start, row.source_state_count)), + parts.source_state_assignments.len(), + true, + )?; + validate_ranges( + "resolved-helicity source selection", + parts + .resolved_helicities + .iter() + .map(|row| (row.source_selection_start, row.source_selection_count)), + parts.resolved_source_selections.len(), + true, + )?; + validate_ranges( + "resolved-helicity public value", + parts + .resolved_helicities + .iter() + .map(|row| (row.public_helicity_start, row.public_helicity_count)), + parts.public_helicities.len(), + true, + )?; + validate_ranges( + "source dispatch embedding", + parts + .source_dispatch_variants + .iter() + .map(|row| (row.embedding_start, row.embedding_count)), + parts.source_embeddings.len(), + true, + )?; + validate_ranges( + "source dispatch projection", + parts + .source_dispatch_variants + .iter() + .map(|row| (row.projection_start, row.projection_count)), + parts.source_projections.len(), + true, + )?; + + let momentum_count = u32_len("momentum forms", parts.momentum_forms.len())?; + let selector_count = u32_len("selector domains", parts.selector_domains.len())?; + let exact_factor_count = u32_len("exact factors", parts.exact_factors.len())?; + let finalization_count = u32_len("finalizations", parts.finalizations.len())?; + let resolved_helicity_count = u32_len("resolved helicities", parts.resolved_helicities.len())?; + let source_dispatch_variant_count = u32_len( + "source dispatch variants", + parts.source_dispatch_variants.len(), + )?; + + let mut current_slots = BTreeSet::new(); + let mut current_by_source_row = vec![None; parts.sources.len()]; + let mut current_by_finalization_row = vec![None; parts.finalizations.len()]; + for (index, current) in parts.currents.iter().enumerate() { + if current.semantic_current_id != index as u32 { + return Err(invalid(format!( + "current {index} has non-canonical semantic ID {}", + current.semantic_current_id + ))); + } + if current.component_count == 0 { + return Err(invalid(format!("current {index} has zero components"))); + } + require_ref( + "current state template", + current.state_template_id, + parts.state_template_count, + )?; + let end = current + .component_base + .checked_add(u32::from(current.component_count)) + .ok_or_else(|| invalid(format!("current {index} component range overflows u32")))?; + if end > parts.current_arena_components { + return Err(invalid(format!( + "current {index} component range exceeds the arena" + ))); + } + require_ref( + "current momentum form", + current.momentum_form_id, + momentum_count, + )?; + require_ref( + "current selector domain", + current.selector_domain_id, + selector_count, + )?; + if current.first_use > current.last_use { + return Err(invalid(format!( + "current {index} has an inverted liveness interval" + ))); + } + if !current_slots.insert((current.stage, current.component_base)) { + return Err(invalid(format!( + "stage {} has duplicate current component base {}", + current.stage, current.component_base + ))); + } + match current.node_kind { + DirectNodeKind::Source => { + if current.source_row_or_sentinel == DIRECT_NONE_U32 { + return Err(invalid(format!("source current {index} has no source row"))); + } + if current.finalization_row_or_sentinel != DIRECT_NONE_U32 { + return Err(invalid(format!( + "source current {index} must not have a finalization row" + ))); + } + let slot = current_by_source_row + .get_mut(current.source_row_or_sentinel as usize) + .ok_or_else(|| { + invalid(format!("source current {index} row is out of bounds")) + })?; + if slot.replace(index).is_some() { + return Err(invalid(format!( + "source row {} is referenced more than once", + current.source_row_or_sentinel + ))); + } + } + DirectNodeKind::Current => { + if current.source_row_or_sentinel != DIRECT_NONE_U32 { + return Err(invalid(format!( + "propagated current {index} must not have a source row" + ))); + } + if current.finalization_row_or_sentinel != DIRECT_NONE_U32 { + require_ref( + "current finalization", + current.finalization_row_or_sentinel, + finalization_count, + )?; + let slot = current_by_finalization_row + .get_mut(current.finalization_row_or_sentinel as usize) + .expect("finalization reference was bounds checked"); + if slot.replace(index).is_some() { + return Err(invalid(format!( + "finalization row {} is referenced more than once", + current.finalization_row_or_sentinel + ))); + } + } + } + } + } + if current_by_source_row.iter().any(Option::is_none) { + return Err(invalid("not every source row is referenced exactly once")); + } + if current_by_finalization_row.iter().any(Option::is_none) { + return Err(invalid( + "not every finalization row is referenced exactly once", + )); + } + validate_arena_liveness(parts)?; + + for (index, source) in parts.sources.iter().enumerate() { + require_ref( + "source external slot", + source.source_slot, + parts.external_source_count, + )?; + require_ref( + "source momentum form", + source.momentum_form_id, + momentum_count, + )?; + require_ref( + "source selector domain", + source.selector_domain_id, + selector_count, + )?; + require_ref( + "source exact factor", + source.exact_factor_id, + exact_factor_count, + )?; + require_ref( + "source template or dispatch domain", + source.source_template_or_dispatch_domain, + parts.source_template_or_dispatch_count, + )?; + let current = &parts.currents[current_by_source_row[index].expect("source row is bound")]; + if source.destination_component_base != current.component_base + || source.momentum_form_id != current.momentum_form_id + || source.selector_domain_id != current.selector_domain_id + { + return Err(invalid(format!( + "source row {index} does not match its current descriptor" + ))); + } + } + match parts.strategy { + RecurrenceStrategy::TopologyReplay | RecurrenceStrategy::ContractedColorUnion => { + if !parts.source_dispatch_variants.is_empty() + || !parts.source_embeddings.is_empty() + || !parts.source_projections.is_empty() + || !parts.resolved_source_selections.is_empty() + { + return Err(invalid( + "fixed-source recurrence must not carry runtime source-dispatch tables", + )); + } + } + RecurrenceStrategy::AllFlowUnion => { + if parts.source_dispatch_variants.is_empty() + || parts.resolved_source_selections.is_empty() + { + return Err(invalid( + "all-flow-union requires runtime source-dispatch tables", + )); + } + } + } + let mut source_variant_keys = BTreeSet::new(); + let mut referenced_source_rows = BTreeSet::new(); + for (index, variant) in parts.source_dispatch_variants.iter().enumerate() { + require_ref( + "source-dispatch source row", + variant.source_row_id, + u32_len("sources", parts.sources.len())?, + )?; + require_ref( + "source-dispatch domain", + variant.dispatch_domain_id, + parts.runtime_helicity_contract_count, + )?; + require_ref( + "source-dispatch runtime variant", + variant.runtime_variant_id, + parts.runtime_helicity_variant_count, + )?; + require_ref( + "source-dispatch concrete template", + variant.source_template_id, + parts.source_template_count, + )?; + require_ref( + "source-dispatch source state", + variant.source_state_template_id, + parts.state_template_count, + )?; + require_ref( + "source-dispatch crossed state", + variant.crossed_state_template_id, + parts.state_template_count, + )?; + require_ref( + "source-dispatch executor", + variant.direct_executor_id, + parts.direct_executor_count, + )?; + require_ref( + "source-dispatch crossing factor", + variant.crossing_exact_factor_id, + exact_factor_count, + )?; + if parts.exact_factors[variant.crossing_exact_factor_id as usize].is_zero() { + return Err(invalid(format!( + "source-dispatch variant {index} has a zero crossing factor" + ))); + } + let source = &parts.sources[variant.source_row_id as usize]; + if source.source_template_or_dispatch_domain != variant.dispatch_domain_id { + return Err(invalid(format!( + "source-dispatch variant {index} does not match source row {} domain", + variant.source_row_id + ))); + } + let current = &parts.currents[current_by_source_row[variant.source_row_id as usize] + .expect("source-dispatch row is bound")]; + if variant.embedding_count != u32::from(current.component_count) { + return Err(invalid(format!( + "source-dispatch variant {index} embedding does not cover its full current" + ))); + } + let embedding_start = usize::try_from(variant.embedding_start) + .map_err(|_| invalid("source embedding start exceeds usize"))?; + let embedding_end = embedding_start + .checked_add(variant.embedding_count as usize) + .ok_or_else(|| invalid("source embedding range overflows usize"))?; + let projection_start = usize::try_from(variant.projection_start) + .map_err(|_| invalid("source projection start exceeds usize"))?; + let projection_end = projection_start + .checked_add(variant.projection_count as usize) + .ok_or_else(|| invalid("source projection range overflows usize"))?; + let embeddings = &parts.source_embeddings[embedding_start..embedding_end]; + let projections = &parts.source_projections[projection_start..projection_end]; + for (full_component, embedding) in embeddings.iter().enumerate() { + if embedding.full_component != full_component as u32 { + return Err(invalid(format!( + "source-dispatch variant {index} embedding is not in full-component order" + ))); + } + require_ref( + "source embedding factor", + embedding.exact_factor_id, + exact_factor_count, + )?; + let factor = parts.exact_factors[embedding.exact_factor_id as usize]; + if (embedding.source_component_or_sentinel == DIRECT_NONE_U32) != factor.is_zero() { + return Err(invalid(format!( + "source-dispatch variant {index} has inconsistent zero embedding" + ))); + } + if embedding.source_component_or_sentinel != DIRECT_NONE_U32 + && embedding.source_component_or_sentinel >= variant.projection_count + { + return Err(invalid(format!( + "source-dispatch variant {index} embedding source component is out of bounds" + ))); + } + } + for (source_component, projection) in projections.iter().enumerate() { + if projection.source_component != source_component as u32 + || projection.full_component >= variant.embedding_count + || embeddings[projection.full_component as usize].source_component_or_sentinel + != projection.source_component + { + return Err(invalid(format!( + "source-dispatch variant {index} projection does not invert its embedding" + ))); + } + } + if !source_variant_keys.insert(( + source.source_slot, + variant.source_state_index, + variant.source_row_id, + )) { + return Err(invalid(format!( + "source-dispatch variant {index} repeats a source-state mapping" + ))); + } + referenced_source_rows.insert(variant.source_row_id); + } + if parts.strategy == RecurrenceStrategy::AllFlowUnion + && referenced_source_rows.len() != parts.sources.len() + { + return Err(invalid( + "not every all-flow-union source row has dispatch variants", + )); + } + for (index, term) in parts.momentum_terms.iter().enumerate() { + if term.source_slot >= parts.external_source_count { + return Err(invalid(format!( + "momentum term {index} source slot is out of bounds" + ))); + } + if term.coefficient == 0 { + return Err(invalid(format!( + "momentum term {index} has a zero coefficient" + ))); + } + } + + let arena_bases = parts + .currents + .iter() + .map(|current| current.component_base) + .collect::>(); + for (index, row) in parts.contributions.iter().enumerate() { + if row.flags & DIRECT_CONTRIBUTION_FLAG_CERTIFIED_REUSE != 0 { + require_arena_base( + &arena_bases, + row.parent0_component_base, + "certified-reuse source", + index, + )?; + if row.parent1_component_base_or_sentinel != DIRECT_NONE_U32 + || row.parent1_momentum_form_id_or_sentinel != DIRECT_NONE_U32 + { + return Err(invalid(format!( + "certified-reuse contribution {index} must not carry a second parent" + ))); + } + if row.parent0_momentum_form_id == 0 { + return Err(invalid(format!( + "certified-reuse contribution {index} has zero components" + ))); + } + require_arena_base( + &arena_bases, + row.destination_component_base, + "certified-reuse destination", + index, + )?; + require_ref( + "certified-reuse exact factor", + row.exact_factor_id, + exact_factor_count, + )?; + require_ref( + "certified-reuse selector domain", + row.selector_domain_id, + selector_count, + )?; + let component_count = u16::try_from(row.parent0_momentum_form_id).map_err(|_| { + invalid(format!( + "certified-reuse contribution {index} component count exceeds u16" + )) + })?; + if !parts.currents.iter().any(|current| { + current.component_base == row.parent0_component_base + && current.component_count == component_count + }) || !parts.currents.iter().any(|current| { + current.component_base == row.destination_component_base + && current.component_count == component_count + }) { + return Err(invalid(format!( + "certified-reuse contribution {index} component count disagrees with its source or destination current" + ))); + } + continue; + } + require_arena_base( + &arena_bases, + row.parent0_component_base, + "contribution parent 0", + index, + )?; + require_optional_pair( + row.parent1_component_base_or_sentinel, + row.parent1_momentum_form_id_or_sentinel, + "contribution parent 1", + index, + )?; + if row.parent1_component_base_or_sentinel != DIRECT_NONE_U32 { + require_arena_base( + &arena_bases, + row.parent1_component_base_or_sentinel, + "contribution parent 1", + index, + )?; + require_ref( + "contribution parent 1 momentum form", + row.parent1_momentum_form_id_or_sentinel, + momentum_count, + )?; + } + require_ref( + "contribution parent 0 momentum form", + row.parent0_momentum_form_id, + momentum_count, + )?; + require_arena_base( + &arena_bases, + row.destination_component_base, + "contribution destination", + index, + )?; + require_ref( + "contribution exact factor", + row.exact_factor_id, + exact_factor_count, + )?; + require_ref( + "contribution selector domain", + row.selector_domain_id, + selector_count, + )?; + } + + for (index, row) in parts.finalizations.iter().enumerate() { + require_arena_base( + &arena_bases, + row.component_base, + "finalization component", + index, + )?; + if row.component_count == 0 { + return Err(invalid(format!("finalization {index} has zero components"))); + } + require_ref( + "finalization momentum form", + row.momentum_form_id, + momentum_count, + )?; + require_ref( + "finalization exact factor", + row.exact_factor_id, + exact_factor_count, + )?; + require_ref( + "finalization selector domain", + row.selector_domain_id, + selector_count, + )?; + let current = + &parts.currents[current_by_finalization_row[index].expect("finalization row is bound")]; + if row.component_base != current.component_base + || row.component_count != current.component_count + || row.momentum_form_id != current.momentum_form_id + || row.selector_domain_id != current.selector_domain_id + { + return Err(invalid(format!( + "finalization {index} does not match its current descriptor" + ))); + } + } + + for (index, row) in parts.closures.iter().enumerate() { + require_arena_base( + &arena_bases, + row.parent0_component_base, + "closure parent 0", + index, + )?; + require_optional_pair( + row.parent1_component_base_or_sentinel, + row.parent1_momentum_form_id_or_sentinel, + "closure parent 1", + index, + )?; + if row.parent1_component_base_or_sentinel != DIRECT_NONE_U32 { + require_arena_base( + &arena_bases, + row.parent1_component_base_or_sentinel, + "closure parent 1", + index, + )?; + require_ref( + "closure parent 1 momentum form", + row.parent1_momentum_form_id_or_sentinel, + momentum_count, + )?; + } + require_ref( + "closure parent 0 momentum form", + row.parent0_momentum_form_id, + momentum_count, + )?; + require_ref( + "closure exact factor", + row.exact_factor_id, + exact_factor_count, + )?; + if row.component_count == 0 { + return Err(invalid(format!( + "closure {index} has zero contraction components" + ))); + } + let component_factor_end = row + .component_factor_start + .checked_add(u32::from(row.component_count)) + .ok_or_else(|| { + invalid(format!( + "closure {index} component-factor range overflows u32" + )) + })?; + if component_factor_end > exact_factor_count { + return Err(invalid(format!( + "closure {index} component-factor range is out of bounds" + ))); + } + require_ref( + "closure selector domain", + row.selector_domain_id, + selector_count, + )?; + if row.amplitude_destination_id >= parts.amplitude_destination_count { + return Err(invalid(format!( + "closure {index} amplitude destination is out of bounds" + ))); + } + } + + for (index, target) in parts.replay_targets.iter().enumerate() { + require_ref( + "replay public flow", + target.public_flow_id, + parts.physical_sector_count, + )?; + require_ref( + "replay representative flow", + target.representative_id, + parts.physical_sector_count, + )?; + require_ref( + "replay phase exact factor", + target.phase_exact_factor_id, + exact_factor_count, + )?; + require_ref( + "replay selector domain", + target.selector_domain_id, + selector_count, + )?; + if !selector_domain_contains_parts( + parts, + target.selector_domain_id, + target.representative_id, + )? { + return Err(invalid(format!( + "replay target {index} selector domain excludes its representative sector" + ))); + } + if target.multiplicity == 0 { + return Err(invalid(format!( + "replay target {index} has zero multiplicity" + ))); + } + if target.source_permutation_count != parts.external_source_count { + return Err(invalid(format!( + "replay target {index} does not map every external source" + ))); + } + let start = usize::try_from(target.source_permutation_start) + .map_err(|_| invalid("replay permutation start exceeds usize"))?; + let count = usize::try_from(target.source_permutation_count) + .map_err(|_| invalid("replay permutation count exceeds usize"))?; + let end = start + .checked_add(count) + .ok_or_else(|| invalid("replay permutation range overflows usize"))?; + let values = &parts.source_permutations[start..end]; + let unique = values.iter().copied().collect::>(); + let external_source_count = usize::try_from(parts.external_source_count) + .map_err(|_| invalid("external source count exceeds usize"))?; + if unique.len() != external_source_count + || unique.iter().copied().ne(0..parts.external_source_count) + { + return Err(invalid(format!( + "replay target {index} source mapping is not a permutation" + ))); + } + match parts.strategy { + RecurrenceStrategy::TopologyReplay => { + if target.helicity_map_count != resolved_helicity_count { + return Err(invalid(format!( + "replay target {index} helicity map covers {} of {resolved_helicity_count} resolved helicities", + target.helicity_map_count + ))); + } + let start = usize::try_from(target.helicity_map_start) + .map_err(|_| invalid("replay helicity-map start exceeds usize"))?; + let count = usize::try_from(target.helicity_map_count) + .map_err(|_| invalid("replay helicity-map count exceeds usize"))?; + let end = start + .checked_add(count) + .ok_or_else(|| invalid("replay helicity-map range overflows usize"))?; + let values = &parts.replay_helicity_map[start..end]; + let unique = values.iter().copied().collect::>(); + if unique.len() != count || unique.iter().copied().ne(0..resolved_helicity_count) { + return Err(invalid(format!( + "replay target {index} helicity mapping is not a bijection" + ))); + } + } + RecurrenceStrategy::AllFlowUnion => { + if target.helicity_map_count != 0 { + return Err(invalid(format!( + "all-flow-union replay target {index} carries a helicity map" + ))); + } + } + RecurrenceStrategy::ContractedColorUnion => { + if target.helicity_map_count != resolved_helicity_count { + return Err(invalid(format!( + "contracted replay target {index} helicity map covers {} of {resolved_helicity_count} resolved helicities", + target.helicity_map_count + ))); + } + let start = usize::try_from(target.helicity_map_start) + .map_err(|_| invalid("replay helicity-map start exceeds usize"))?; + let count = usize::try_from(target.helicity_map_count) + .map_err(|_| invalid("replay helicity-map count exceeds usize"))?; + let end = start + .checked_add(count) + .ok_or_else(|| invalid("replay helicity-map range overflows usize"))?; + let values = &parts.replay_helicity_map[start..end]; + let unique = values.iter().copied().collect::>(); + if unique.len() != count || unique.iter().copied().ne(0..resolved_helicity_count) { + return Err(invalid(format!( + "contracted replay target {index} helicity mapping is not a bijection" + ))); + } + } + } + } + + for (index, destination) in parts.amplitude_destinations.iter().enumerate() { + if destination.id != index as u32 { + return Err(invalid(format!( + "amplitude destination {index} has non-canonical ID {}", + destination.id + ))); + } + require_ref( + "amplitude destination physical sector", + destination.target_sector_id, + parts.physical_sector_count, + )?; + if destination.target_helicity_id_or_sentinel != DIRECT_NONE_U32 { + require_ref( + "amplitude destination resolved helicity", + destination.target_helicity_id_or_sentinel, + resolved_helicity_count, + )?; + } + require_ref( + "amplitude destination selector domain", + destination.selector_domain_id, + selector_count, + )?; + if !selector_domain_contains_parts( + parts, + destination.selector_domain_id, + destination.target_sector_id, + )? { + return Err(invalid(format!( + "amplitude destination {index} selector domain excludes its target sector" + ))); + } + let start = usize::try_from(destination.closure_row_start) + .map_err(|_| invalid("amplitude destination closure start exceeds usize"))?; + let count = usize::try_from(destination.closure_row_count) + .map_err(|_| invalid("amplitude destination closure count exceeds usize"))?; + let end = start + .checked_add(count) + .ok_or_else(|| invalid("amplitude destination closure range overflows usize"))?; + if parts.closures[start..end] + .iter() + .any(|row| row.amplitude_destination_id != destination.id) + { + return Err(invalid(format!( + "amplitude destination {index} does not own every closure in its range" + ))); + } + if parts.closures[start..end] + .iter() + .any(|row| row.selector_domain_id != destination.selector_domain_id) + { + return Err(invalid(format!( + "amplitude destination {index} disagrees with a closure selector domain" + ))); + } + } + + for (index, helicity) in parts.resolved_helicities.iter().enumerate() { + if helicity.id != index as u32 { + return Err(invalid(format!( + "resolved helicity {index} has non-canonical ID {}", + helicity.id + ))); + } + if helicity.source_state_count != parts.external_source_count + || helicity.public_helicity_count != parts.external_source_count + { + return Err(invalid(format!( + "resolved helicity {index} does not cover every external source" + ))); + } + match parts.strategy { + RecurrenceStrategy::TopologyReplay | RecurrenceStrategy::ContractedColorUnion + if helicity.source_selection_count != 0 => + { + return Err(invalid(format!( + "fixed-source resolved helicity {index} carries source dispatch selections" + ))); + } + RecurrenceStrategy::AllFlowUnion + if helicity.source_selection_count != parts.external_source_count => + { + return Err(invalid(format!( + "all-flow-union resolved helicity {index} does not select every external source" + ))); + } + _ => {} + } + require_ref( + "resolved-helicity selector domain", + helicity.selector_domain_id, + selector_count, + )?; + let start = usize::try_from(helicity.source_state_start) + .map_err(|_| invalid("resolved-helicity source-state start exceeds usize"))?; + let count = usize::try_from(helicity.source_state_count) + .map_err(|_| invalid("resolved-helicity source-state count exceeds usize"))?; + let end = start + .checked_add(count) + .ok_or_else(|| invalid("resolved-helicity source-state range overflows usize"))?; + for (source_slot, assignment) in parts.source_state_assignments[start..end] + .iter() + .enumerate() + { + if assignment.source_slot != source_slot as u32 { + return Err(invalid(format!( + "resolved helicity {index} source states are not in source-slot order" + ))); + } + } + let selection_start = usize::try_from(helicity.source_selection_start) + .map_err(|_| invalid("resolved-helicity source-selection start exceeds usize"))?; + let selection_count = usize::try_from(helicity.source_selection_count) + .map_err(|_| invalid("resolved-helicity source-selection count exceeds usize"))?; + let selection_end = selection_start + .checked_add(selection_count) + .ok_or_else(|| invalid("resolved-helicity source-selection range overflows usize"))?; + for (source_slot, selection) in parts.resolved_source_selections + [selection_start..selection_end] + .iter() + .enumerate() + { + if selection.source_slot != source_slot as u32 { + return Err(invalid(format!( + "resolved helicity {index} source selections are not in source-slot order" + ))); + } + require_ref( + "resolved-helicity source dispatch variant", + selection.dispatch_variant_id, + source_dispatch_variant_count, + )?; + let variant = parts.source_dispatch_variants[selection.dispatch_variant_id as usize]; + let source = parts.sources[variant.source_row_id as usize]; + let assignment = parts.source_state_assignments[start + source_slot]; + if source.source_slot != selection.source_slot + || variant.source_state_index != assignment.state_index + { + return Err(invalid(format!( + "resolved helicity {index} source selection does not match its public source-state assignment" + ))); + } + } + } + if parts.strategy == RecurrenceStrategy::AllFlowUnion + && parts.resolved_helicities.len() as u64 != parts.retained_helicity_count + { + return Err(invalid( + "all-flow-union resolved-helicity catalog does not cover retained helicities", + )); + } + + validate_row_groups(parts)?; + validate_contribution_initialization(parts)?; + validate_closure_proofs(parts)?; + Ok(()) +} + +#[cfg_attr(target_vendor = "apple", unsafe(link_section = "__TEXT,__rcl_load"))] +#[cfg_attr(target_vendor = "apple", inline(never))] +fn validate_closure_proofs(parts: &DirectRecurrencePlanParts) -> RusticolResult<()> { + parts + .closure_proofs + .validate_tables() + .map_err(|error| invalid(error.message()))?; + let current_bases = parts + .currents + .iter() + .map(|current| (current.semantic_current_id, current.component_base)) + .collect::>(); + let mut row_owner = vec![None; parts.closures.len()]; + let mut term_owner = vec![None; parts.closures.len()]; + + for group in parts.closure_proofs.groups() { + let Some(term_id) = group.emitted_runtime_closure_term_id() else { + if group.emitted_direct_closure_row_id().is_some() { + return Err(invalid(format!( + "exact-zero closure proof group {} references a direct closure row", + group.id() + ))); + } + continue; + }; + let term_slot = term_owner.get_mut(term_id as usize).ok_or_else(|| { + invalid(format!( + "closure proof group {} references out-of-range runtime term {term_id}", + group.id() + )) + })?; + if term_slot.replace(group.id()).is_some() { + return Err(invalid(format!( + "runtime closure term {term_id} belongs to multiple proof groups" + ))); + } + let row_id = group.emitted_direct_closure_row_id().ok_or_else(|| { + invalid(format!( + "nonzero closure proof group {} has no direct closure row", + group.id() + )) + })?; + let row = parts.closures.get(row_id as usize).ok_or_else(|| { + invalid(format!( + "closure proof group {} references out-of-range direct row {row_id}", + group.id() + )) + })?; + if row_owner[row_id as usize].replace(group.id()).is_some() { + return Err(invalid(format!( + "direct closure row {row_id} belongs to multiple proof groups" + ))); + } + if row.proof_group_id() != group.id() { + return Err(invalid(format!( + "direct closure row {row_id} names proof group {}, expected {}", + row.proof_group_id(), + group.id() + ))); + } + let runtime_factor = parts.exact_factors[row.exact_factor_id as usize]; + if runtime_factor != group.exact_summed_factor() { + return Err(invalid(format!( + "direct closure row {row_id} factor does not match proof group {}", + group.id() + ))); + } + + let component_start = row.component_factor_start as usize; + let component_end = component_start + .checked_add(row.component_count as usize) + .ok_or_else(|| invalid("closure component-factor range overflows usize"))?; + let component_factors = parts + .exact_factors + .get(component_start..component_end) + .ok_or_else(|| invalid("closure component-factor range is out of bounds"))?; + if closure_component_factor_digest_v2(component_factors) + .map_err(|error| invalid(error.message()))? + != group.component_factor_digest() + { + return Err(invalid(format!( + "direct closure row {row_id} component-factor digest does not match proof group {}", + group.id() + ))); + } + + let selector = parts + .selector_domains + .get(row.selector_domain_id as usize) + .ok_or_else(|| invalid("closure selector domain is absent"))?; + let selector_start = selector.word_start as usize; + let selector_end = selector_start + .checked_add(selector.word_count as usize) + .ok_or_else(|| invalid("closure selector-domain range overflows usize"))?; + let selector_words = parts + .selector_words + .get(selector_start..selector_end) + .ok_or_else(|| invalid("closure selector-domain range is out of bounds"))?; + if closure_selector_domain_digest_v2(selector_words) + .map_err(|error| invalid(error.message()))? + != group.selector_domain_digest() + { + return Err(invalid(format!( + "direct closure row {row_id} selector-domain digest does not match proof group {}", + group.id() + ))); + } + + let contribution_range = group + .contribution_range() + .as_usize_range( + parts.closure_proofs.contributions().len(), + &format!("closure proof group {} contribution", group.id()), + ) + .map_err(|error| invalid(error.message()))?; + let contributions = &parts.closure_proofs.contributions()[contribution_range]; + let representative = &contributions[0]; + if Some(row.amplitude_destination_id) != representative.target_destination_id() { + return Err(invalid(format!( + "direct closure row {row_id} destination does not match proof group {}", + group.id() + ))); + } + let parent_ids = representative + .evaluator_parent_runtime_ids() + .ok_or_else(|| { + invalid(format!( + "nonzero closure proof group {} has no complete runtime-parent binding", + group.id() + )) + })?; + let expected_parent0 = *current_bases.get(&parent_ids[0]).ok_or_else(|| { + invalid(format!( + "closure proof group {} references absent semantic parent {}", + group.id(), + parent_ids[0] + )) + })?; + let expected_parent1 = parent_ids + .get(1) + .map(|parent_id| { + current_bases.get(parent_id).copied().ok_or_else(|| { + invalid(format!( + "closure proof group {} references absent semantic parent {parent_id}", + group.id() + )) + }) + }) + .transpose()? + .unwrap_or(DIRECT_NONE_U32); + if parent_ids.len() > 2 + || row.parent0_component_base != expected_parent0 + || row.parent1_component_base_or_sentinel != expected_parent1 + { + return Err(invalid(format!( + "direct closure row {row_id} parents do not match proof group {}", + group.id() + ))); + } + } + + if let Some((row_id, _)) = row_owner + .iter() + .enumerate() + .find(|(_, owner)| owner.is_none()) + { + return Err(invalid(format!( + "direct closure row {row_id} has no closure proof group" + ))); + } + if let Some((term_id, _)) = term_owner + .iter() + .enumerate() + .find(|(_, owner)| owner.is_none()) + { + return Err(invalid(format!( + "runtime closure term {term_id} has no closure proof group" + ))); + } + Ok(()) +} + +fn validate_row_groups(parts: &DirectRecurrencePlanParts) -> RusticolResult<()> { + let mut next = [0_u64; 4]; + let mut previous = None; + for (index, row_group) in parts.row_groups.iter().enumerate() { + if row_group.row_count == 0 { + return Err(invalid(format!("row group {index} has zero rows"))); + } + if (row_group.role == DirectExecutorRole::Source + && parts.strategy == RecurrenceStrategy::AllFlowUnion) + || (row_group.role == DirectExecutorRole::Contribution + && row_group.direct_executor_id == DIRECT_NONE_U32) + { + if row_group.direct_executor_id != DIRECT_NONE_U32 { + return Err(invalid(format!( + "internal row group {index} must use the missing-executor sentinel" + ))); + } + if row_group.role == DirectExecutorRole::Contribution { + let start = usize::try_from(row_group.row_start) + .map_err(|_| invalid("certified-reuse row-group start exceeds usize"))?; + let end = start + .checked_add(row_group.row_count as usize) + .ok_or_else(|| invalid("certified-reuse row-group range overflows usize"))?; + let rows = parts.contributions.get(start..end).ok_or_else(|| { + invalid(format!( + "certified-reuse row-group {index} range is out of bounds" + )) + })?; + if !rows + .iter() + .all(|row| row.flags & DIRECT_CONTRIBUTION_FLAG_CERTIFIED_REUSE != 0) + { + return Err(invalid(format!( + "internal contribution row group {index} contains an uncertified row" + ))); + } + } + } else { + require_ref( + "row-group direct executor", + row_group.direct_executor_id, + parts.direct_executor_count, + )?; + } + let order = (row_group.stage, row_group.role); + if previous.is_some_and(|previous| order < previous) { + return Err(invalid( + "row groups are not in deterministic stage/role order", + )); + } + previous = Some(order); + let role_index = row_group.role as usize; + if row_group.row_start != next[role_index] { + return Err(invalid(format!( + "row group {index} does not continue its role's row partition" + ))); + } + next[role_index] = row_group + .row_start + .checked_add(u64::from(row_group.row_count)) + .ok_or_else(|| invalid(format!("row group {index} row range overflows u64")))?; + let start = usize::try_from(row_group.row_start) + .map_err(|_| invalid(format!("row group {index} row start exceeds usize")))?; + let end = start + .checked_add(row_group.row_count as usize) + .ok_or_else(|| invalid(format!("row group {index} row range overflows usize")))?; + let expected_operation = match row_group.role { + DirectExecutorRole::Source => DirectDestinationOperation::Initialize, + DirectExecutorRole::Contribution => DirectDestinationOperation::Add, + DirectExecutorRole::Finalization => DirectDestinationOperation::FinalizeInPlace, + DirectExecutorRole::Closure => DirectDestinationOperation::ClosureAdd, + }; + if row_group.destination_operation != expected_operation { + return Err(invalid(format!( + "row group {index} has an operation incompatible with its role" + ))); + } + let role_row_count = match row_group.role { + DirectExecutorRole::Source => parts.sources.len(), + DirectExecutorRole::Contribution => parts.contributions.len(), + DirectExecutorRole::Finalization => parts.finalizations.len(), + DirectExecutorRole::Closure => parts.closures.len(), + }; + if end > role_row_count { + return Err(invalid(format!( + "row group {index} range is out of bounds for its role" + ))); + } + } + let expected = [ + parts.sources.len(), + parts.contributions.len(), + parts.finalizations.len(), + parts.closures.len(), + ]; + for (index, expected) in expected.into_iter().enumerate() { + if next[index] != expected as u64 { + return Err(invalid(format!( + "row groups do not partition role {index}: covered {}, expected {expected}", + next[index] + ))); + } + } + Ok(()) +} + +fn validate_contribution_initialization(parts: &DirectRecurrencePlanParts) -> RusticolResult<()> { + let current_by_stage_and_base = parts + .currents + .iter() + .filter(|current| current.node_kind == DirectNodeKind::Current) + .map(|current| { + ( + (current.stage, current.component_base), + current.semantic_current_id, + ) + }) + .collect::>(); + let mut contribution_counts = vec![0_u32; parts.currents.len()]; + let mut initialization_counts = vec![0_u32; parts.currents.len()]; + + for row_group in parts + .row_groups + .iter() + .filter(|row_group| row_group.role == DirectExecutorRole::Contribution) + { + let start = usize::try_from(row_group.row_start) + .map_err(|_| invalid("contribution row-group start exceeds usize"))?; + let end = start + .checked_add(row_group.row_count as usize) + .ok_or_else(|| invalid("contribution row-group range overflows usize"))?; + for (offset, row) in parts.contributions[start..end].iter().enumerate() { + if row.flags & !DIRECT_CONTRIBUTION_FLAGS_KNOWN != 0 { + return Err(invalid(format!( + "contribution {} has unknown flags {:#x}", + start + offset, + row.flags + ))); + } + let current_id = current_by_stage_and_base + .get(&(row_group.stage, row.destination_component_base)) + .copied() + .ok_or_else(|| { + invalid(format!( + "contribution {} does not target a current in stage {}", + start + offset, + row_group.stage + )) + })? as usize; + contribution_counts[current_id] = contribution_counts[current_id] + .checked_add(1) + .ok_or_else(|| invalid("contribution count overflows u32"))?; + if row.flags & DIRECT_CONTRIBUTION_FLAG_INITIALIZE_DESTINATION != 0 { + initialization_counts[current_id] = initialization_counts[current_id] + .checked_add(1) + .ok_or_else(|| invalid("contribution initialization count overflows u32"))?; + } + } + } + + for current in parts + .currents + .iter() + .filter(|current| current.node_kind == DirectNodeKind::Current) + { + let current_id = current.semantic_current_id as usize; + if contribution_counts[current_id] == 0 { + return Err(invalid(format!( + "current {} has no contribution rows", + current.semantic_current_id + ))); + } + if initialization_counts[current_id] != 1 { + return Err(invalid(format!( + "current {} must have exactly one destination-initializing contribution, found {}", + current.semantic_current_id, initialization_counts[current_id] + ))); + } + } + Ok(()) +} + +fn validate_arena_liveness(parts: &DirectRecurrencePlanParts) -> RusticolResult<()> { + let component_count = usize::try_from(parts.current_arena_components) + .map_err(|_| invalid("current arena component count exceeds usize"))?; + let mut intervals = vec![Vec::<(u32, u32, u32)>::new(); component_count]; + for current in &parts.currents { + let end = current.component_base + u32::from(current.component_count); + for component in current.component_base..end { + let component = usize::try_from(component) + .map_err(|_| invalid("current arena component ID exceeds usize"))?; + intervals[component].push(( + current.first_use, + current.last_use, + current.semantic_current_id, + )); + } + } + for (component, intervals) in intervals.iter_mut().enumerate() { + intervals.sort_unstable(); + for pair in intervals.windows(2) { + let (_, left_last, left_id) = pair[0]; + let (right_first, _, right_id) = pair[1]; + if left_last >= right_first { + return Err(invalid(format!( + "arena component {component} is shared by live currents {left_id} and {right_id}" + ))); + } + } + } + Ok(()) +} + +fn validate_ranges( + label: &str, + ranges: impl IntoIterator, + item_count: usize, + require_partition: bool, +) -> RusticolResult<()> { + let item_count = u64::try_from(item_count) + .map_err(|_| invalid(format!("{label} item count exceeds u64")))?; + let mut next = 0_u64; + for (index, (start, count)) in ranges.into_iter().enumerate() { + let end = start + .checked_add(u64::from(count)) + .ok_or_else(|| invalid(format!("{label} range {index} overflows u64")))?; + if end > item_count { + return Err(invalid(format!("{label} range {index} is out of bounds"))); + } + if require_partition && start != next { + return Err(invalid(format!( + "{label} range {index} does not continue the canonical partition" + ))); + } + if require_partition { + next = end; + } + } + if require_partition && next != item_count { + return Err(invalid(format!( + "{label} ranges cover {next} items, expected {item_count}" + ))); + } + Ok(()) +} + +fn require_ref(label: &str, value: u32, count: u32) -> RusticolResult<()> { + if value >= count { + return Err(invalid(format!("{label} {value} is out of bounds {count}"))); + } + Ok(()) +} + +fn require_arena_base( + bases: &BTreeSet, + value: u32, + label: &str, + index: usize, +) -> RusticolResult<()> { + if !bases.contains(&value) { + return Err(invalid(format!( + "{label} in row {index} does not name a current arena base" + ))); + } + Ok(()) +} + +fn require_optional_pair(left: u32, right: u32, label: &str, index: usize) -> RusticolResult<()> { + if (left == DIRECT_NONE_U32) != (right == DIRECT_NONE_U32) { + return Err(invalid(format!( + "{label} in row {index} has mismatched optional fields" + ))); + } + Ok(()) +} + +fn u32_len(label: &str, length: usize) -> RusticolResult { + u32::try_from(length).map_err(|_| invalid(format!("{label} length exceeds u32"))) +} + +fn digest_parts(parts: &DirectRecurrencePlanParts) -> RusticolResult { + let mut hash = Sha256::new(); + hash.update(b"pyamplicol-recurrence-runtime-layout-v2\0"); + hash.update(parts.strategy.as_u32().to_le_bytes()); + hash.update(parts.semantic_digest.as_bytes()); + hash.update(parts.prepared_pack_digest.as_bytes()); + hash.update(parts.direct_template_catalog_digest.as_bytes()); + for value in [ + parts.point_tile_size, + parts.workspace_mib, + parts.current_arena_components, + parts.physical_sector_count, + parts.amplitude_destination_count, + parts.parameter_value_count, + parts.external_source_count, + parts.state_template_count, + parts.source_template_count, + parts.source_template_or_dispatch_count, + parts.runtime_helicity_contract_count, + parts.runtime_helicity_variant_count, + parts.direct_executor_count, + ] { + hash.update(value.to_le_bytes()); + } + hash.update(parts.retained_helicity_count.to_le_bytes()); + macro_rules! count { + ($rows:expr) => { + hash.update( + u64::try_from($rows.len()) + .map_err(|_| invalid("digest row count exceeds u64"))? + .to_le_bytes(), + ); + }; + } + count!(parts.currents); + for row in &parts.currents { + hash.update(row.semantic_current_id.to_le_bytes()); + hash.update((row.node_kind as u16).to_le_bytes()); + hash.update(row.state_template_id.to_le_bytes()); + hash.update(row.component_base.to_le_bytes()); + hash.update(row.component_count.to_le_bytes()); + hash.update(row.momentum_form_id.to_le_bytes()); + hash.update(row.stage.to_le_bytes()); + hash.update(row.selector_domain_id.to_le_bytes()); + hash.update(row.first_use.to_le_bytes()); + hash.update(row.last_use.to_le_bytes()); + hash.update(row.source_row_or_sentinel.to_le_bytes()); + hash.update(row.finalization_row_or_sentinel.to_le_bytes()); + } + macro_rules! hash_u32s { + ($($value:expr),+ $(,)?) => { + $(hash.update($value.to_le_bytes());)+ + }; + } + count!(parts.sources); + for row in &parts.sources { + hash_u32s!( + row.source_slot, + row.destination_component_base, + row.momentum_form_id, + row.source_template_or_dispatch_domain, + ); + hash.update(row.spin_state_class.to_le_bytes()); + hash.update(row.exact_factor_id.to_le_bytes()); + hash.update(row.selector_domain_id.to_le_bytes()); + } + count!(parts.contributions); + for row in &parts.contributions { + hash_u32s!( + row.parent0_component_base, + row.parent1_component_base_or_sentinel, + row.parent0_momentum_form_id, + row.parent1_momentum_form_id_or_sentinel, + row.destination_component_base, + row.exact_factor_id, + row.selector_domain_id, + row.flags, + ); + } + count!(parts.finalizations); + for row in &parts.finalizations { + hash.update(row.component_base.to_le_bytes()); + hash.update(row.component_count.to_le_bytes()); + hash_u32s!( + row.momentum_form_id, + row.exact_factor_id, + row.selector_domain_id, + row.flags, + ); + } + count!(parts.closures); + for row in &parts.closures { + hash_u32s!( + row.parent0_component_base, + row.parent1_component_base_or_sentinel, + row.parent0_momentum_form_id, + row.parent1_momentum_form_id_or_sentinel, + row.amplitude_destination_id, + row.exact_factor_id, + row.component_factor_start, + ); + hash.update(row.component_count.to_le_bytes()); + hash_u32s!(row.selector_domain_id, row.flags,); + } + count!(parts.row_groups); + for row in &parts.row_groups { + hash.update(row.stage.to_le_bytes()); + hash.update((row.role as u16).to_le_bytes()); + hash.update((row.destination_operation as u16).to_le_bytes()); + hash.update(row.direct_executor_id.to_le_bytes()); + hash.update(row.row_start.to_le_bytes()); + hash.update(row.row_count.to_le_bytes()); + } + count!(parts.momentum_forms); + for row in &parts.momentum_forms { + hash.update(row.term_start.to_le_bytes()); + hash.update(row.term_count.to_le_bytes()); + } + count!(parts.momentum_terms); + for row in &parts.momentum_terms { + hash.update(row.source_slot.to_le_bytes()); + hash.update(row.coefficient.to_le_bytes()); + } + count!(parts.selector_domains); + for row in &parts.selector_domains { + hash.update(row.word_start.to_le_bytes()); + hash.update(row.word_count.to_le_bytes()); + } + count!(parts.selector_words); + for value in &parts.selector_words { + hash.update(value.to_le_bytes()); + } + count!(parts.replay_targets); + for row in &parts.replay_targets { + hash_u32s!(row.public_flow_id, row.representative_id); + hash.update(row.source_permutation_start.to_le_bytes()); + hash.update(row.helicity_map_start.to_le_bytes()); + hash_u32s!( + row.source_permutation_count, + row.helicity_map_count, + row.phase_exact_factor_id, + row.multiplicity, + row.selector_domain_id, + ); + } + count!(parts.source_permutations); + for value in &parts.source_permutations { + hash.update(value.to_le_bytes()); + } + count!(parts.replay_momentum_signs); + for value in &parts.replay_momentum_signs { + hash.update(value.to_le_bytes()); + } + count!(parts.replay_helicity_map); + for value in &parts.replay_helicity_map { + hash.update(value.to_le_bytes()); + } + count!(parts.amplitude_destinations); + for row in &parts.amplitude_destinations { + hash.update(row.closure_row_start.to_le_bytes()); + hash_u32s!( + row.id, + row.target_sector_id, + row.target_helicity_id_or_sentinel, + row.closure_row_count, + row.selector_domain_id, + ); + } + count!(parts.resolved_helicities); + for row in &parts.resolved_helicities { + hash.update(row.source_state_start.to_le_bytes()); + hash.update(row.source_selection_start.to_le_bytes()); + hash.update(row.public_helicity_start.to_le_bytes()); + hash_u32s!( + row.id, + row.source_state_count, + row.source_selection_count, + row.public_helicity_count, + row.selector_domain_id, + ); + } + count!(parts.source_state_assignments); + for row in &parts.source_state_assignments { + hash_u32s!(row.source_slot, row.state_index); + } + count!(parts.source_dispatch_variants); + for row in &parts.source_dispatch_variants { + hash.update(row.embedding_start.to_le_bytes()); + hash.update(row.projection_start.to_le_bytes()); + hash_u32s!( + row.source_row_id, + row.dispatch_domain_id, + row.runtime_variant_id, + row.source_state_index, + row.source_template_id, + row.source_state_template_id, + row.crossed_state_template_id, + ); + hash.update(row.crossed_spin_state_class.to_le_bytes()); + hash_u32s!( + row.direct_executor_id, + row.crossing_exact_factor_id, + row.embedding_count, + row.projection_count, + ); + } + count!(parts.source_embeddings); + for row in &parts.source_embeddings { + hash_u32s!( + row.full_component, + row.source_component_or_sentinel, + row.exact_factor_id, + ); + } + count!(parts.source_projections); + for row in &parts.source_projections { + hash_u32s!(row.source_component, row.full_component); + } + count!(parts.resolved_source_selections); + for row in &parts.resolved_source_selections { + hash_u32s!(row.source_slot, row.dispatch_variant_id); + } + count!(parts.public_helicities); + for value in &parts.public_helicities { + hash.update(value.to_le_bytes()); + } + count!(parts.exact_factors); + for factor in &parts.exact_factors { + for rational in [factor.real(), factor.imag()] { + hash.update(rational.numerator().to_le_bytes()); + hash.update(rational.denominator().to_le_bytes()); + } + } + hash.update( + parts + .closure_proofs + .expected_semantic_completeness_digest() + .as_bytes(), + ); + SemanticDigest::new(hash.finalize().into()) +} + +#[cfg(test)] +#[path = "direct_plan_tests.rs"] +pub(crate) mod tests; diff --git a/pyamplicol/rust/crates/rusticol-core/src/recurrence/direct_plan_tests.rs b/pyamplicol/rust/crates/rusticol-core/src/recurrence/direct_plan_tests.rs new file mode 100644 index 0000000000..952c38ca87 --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/recurrence/direct_plan_tests.rs @@ -0,0 +1,809 @@ +// SPDX-License-Identifier: 0BSD + +use super::*; +use crate::recurrence::direct_plan::{ + DIRECT_CONTRIBUTION_FLAG_INITIALIZE_DESTINATION, DirectResolvedSourceSelection, + DirectSourceDispatchVariantDescriptor, DirectSourceEmbeddingRow, DirectSourceProjectionRow, +}; +use crate::recurrence::{ + CheckedTableRange, ClosureExecutionProofGroupV2, ClosureProofContributionV2, ExactRational, + ThreeLineTraversalCertificateV1, ThreeLineTraversalKindV1, + three_line_traversal_proof_digest_v1, +}; + +pub(crate) fn valid_three_line_traversal_certificate() -> ThreeLineTraversalCertificateV1 { + let reference_block_order = vec![0, 1, 2]; + let witness_block_order = vec![1, 0, 2]; + let block_permutation = vec![1, 0, 2]; + let reference_source_order = vec![10, 11, 12]; + let witness_source_order = vec![11, 10, 12]; + let source_position_permutation = vec![1, 0, 2]; + let proof_digest = three_line_traversal_proof_digest_v1( + 0, + ThreeLineTraversalKindV1::Partner, + 2, + &reference_block_order, + &witness_block_order, + &block_permutation, + &reference_source_order, + &witness_source_order, + &source_position_permutation, + 12, + 7, + ) + .unwrap(); + ThreeLineTraversalCertificateV1::new( + 0, + 0, + ThreeLineTraversalKindV1::Partner, + 2, + reference_block_order, + witness_block_order, + block_permutation, + reference_source_order, + witness_source_order, + source_position_permutation, + 12, + 7, + proof_digest, + ) + .unwrap() +} + +pub(crate) fn valid_parts() -> DirectRecurrencePlanParts { + let identity = vec![0]; + let proof_contribution = ClosureProofContributionV2::new( + 0, + 0, + Some(0), + Some(0), + 0, + SemanticDigest::new([0x41; 32]).unwrap(), + None, + vec![1], + vec![Some(1)], + vec![SemanticDigest::new([0x42; 32]).unwrap()], + vec![SemanticDigest::new([0x43; 32]).unwrap()], + identity.clone(), + identity.clone(), + identity, + 0, + SemanticDigest::new([0x44; 32]).unwrap(), + Some(0), + vec![], + None, + ExactComplexRational::ONE, + 1, + ) + .unwrap(); + let proof_group = ClosureExecutionProofGroupV2::new( + 0, + Some(0), + Some(0), + CheckedTableRange::new(0, 1), + ExactComplexRational::ONE, + closure_component_factor_digest_v2(&[ExactComplexRational::ONE]).unwrap(), + closure_selector_domain_digest_v2(&[1]).unwrap(), + ) + .unwrap(); + DirectRecurrencePlanParts { + strategy: RecurrenceStrategy::TopologyReplay, + semantic_digest: SemanticDigest::new([0x11; 32]).unwrap(), + prepared_pack_digest: SemanticDigest::new([0x22; 32]).unwrap(), + direct_template_catalog_digest: SemanticDigest::new([0x33; 32]).unwrap(), + point_tile_size: 1024, + workspace_mib: 256, + current_arena_components: 3, + physical_sector_count: 1, + retained_helicity_count: 1, + amplitude_destination_count: 1, + parameter_value_count: 1, + external_source_count: 1, + state_template_count: 2, + source_template_count: 8, + source_template_or_dispatch_count: 8, + runtime_helicity_contract_count: 0, + runtime_helicity_variant_count: 0, + direct_executor_count: 4, + currents: vec![ + DirectCurrentDescriptor { + semantic_current_id: 0, + node_kind: DirectNodeKind::Source, + state_template_id: 0, + component_base: 0, + component_count: 2, + momentum_form_id: 0, + stage: 0, + selector_domain_id: 0, + first_use: 0, + last_use: 1, + source_row_or_sentinel: 0, + finalization_row_or_sentinel: DIRECT_NONE_U32, + }, + DirectCurrentDescriptor { + semantic_current_id: 1, + node_kind: DirectNodeKind::Current, + state_template_id: 1, + component_base: 2, + component_count: 1, + momentum_form_id: 0, + stage: 1, + selector_domain_id: 0, + first_use: 1, + last_use: 2, + source_row_or_sentinel: DIRECT_NONE_U32, + finalization_row_or_sentinel: 0, + }, + ], + sources: vec![DirectSourceRow { + source_slot: 0, + destination_component_base: 0, + momentum_form_id: 0, + source_template_or_dispatch_domain: 7, + spin_state_class: -1, + exact_factor_id: 0, + selector_domain_id: 0, + }], + contributions: vec![DirectContributionRow { + parent0_component_base: 0, + parent1_component_base_or_sentinel: DIRECT_NONE_U32, + parent0_momentum_form_id: 0, + parent1_momentum_form_id_or_sentinel: DIRECT_NONE_U32, + destination_component_base: 2, + exact_factor_id: 0, + selector_domain_id: 0, + flags: DIRECT_CONTRIBUTION_FLAG_INITIALIZE_DESTINATION, + }], + finalizations: vec![DirectFinalizationRow { + component_base: 2, + component_count: 1, + momentum_form_id: 0, + exact_factor_id: 0, + selector_domain_id: 0, + flags: 0, + }], + closures: vec![DirectClosureRow { + parent0_component_base: 2, + parent1_component_base_or_sentinel: DIRECT_NONE_U32, + parent0_momentum_form_id: 0, + parent1_momentum_form_id_or_sentinel: DIRECT_NONE_U32, + amplitude_destination_id: 0, + exact_factor_id: 0, + component_factor_start: 0, + component_count: 1, + selector_domain_id: 0, + flags: 0, + }], + row_groups: vec![ + DirectRowGroupDescriptor { + stage: 0, + role: DirectExecutorRole::Source, + destination_operation: DirectDestinationOperation::Initialize, + direct_executor_id: 0, + row_start: 0, + row_count: 1, + }, + DirectRowGroupDescriptor { + stage: 1, + role: DirectExecutorRole::Contribution, + destination_operation: DirectDestinationOperation::Add, + direct_executor_id: 1, + row_start: 0, + row_count: 1, + }, + DirectRowGroupDescriptor { + stage: 1, + role: DirectExecutorRole::Finalization, + destination_operation: DirectDestinationOperation::FinalizeInPlace, + direct_executor_id: 2, + row_start: 0, + row_count: 1, + }, + DirectRowGroupDescriptor { + stage: 2, + role: DirectExecutorRole::Closure, + destination_operation: DirectDestinationOperation::ClosureAdd, + direct_executor_id: 3, + row_start: 0, + row_count: 1, + }, + ], + momentum_forms: vec![DirectMomentumFormDescriptor { + term_start: 0, + term_count: 1, + }], + momentum_terms: vec![DirectMomentumTerm { + source_slot: 0, + coefficient: 1, + }], + selector_domains: vec![DirectSelectorDomainDescriptor { + word_start: 0, + word_count: 1, + }], + selector_words: vec![1], + replay_targets: vec![DirectReplayTargetDescriptor { + public_flow_id: 0, + representative_id: 0, + source_permutation_start: 0, + source_permutation_count: 1, + helicity_map_start: 0, + helicity_map_count: 1, + phase_exact_factor_id: 0, + multiplicity: 1, + selector_domain_id: 0, + }], + source_permutations: vec![0], + replay_momentum_signs: vec![1], + replay_helicity_map: vec![0], + amplitude_destinations: vec![DirectAmplitudeDestinationDescriptor { + closure_row_start: 0, + id: 0, + target_sector_id: 0, + target_helicity_id_or_sentinel: 0, + closure_row_count: 1, + selector_domain_id: 0, + }], + resolved_helicities: vec![DirectResolvedHelicityDescriptor { + source_state_start: 0, + source_selection_start: 0, + public_helicity_start: 0, + id: 0, + source_state_count: 1, + source_selection_count: 0, + public_helicity_count: 1, + selector_domain_id: 0, + }], + source_state_assignments: vec![DirectSourceStateAssignment { + source_slot: 0, + state_index: 0, + }], + source_dispatch_variants: vec![], + source_embeddings: vec![], + source_projections: vec![], + resolved_source_selections: vec![], + public_helicities: vec![-1], + exact_factors: vec![ExactComplexRational::ONE], + closure_proofs: ClosureProofMetadataV2::new_with_three_line_certificates( + vec![proof_contribution], + vec![proof_group], + vec![], + vec![valid_three_line_traversal_certificate()], + ) + .unwrap(), + } +} + +pub(crate) fn valid_plan() -> DirectRecurrencePlan { + DirectRecurrencePlan::new(valid_parts()).unwrap() +} + +#[cfg(feature = "on-the-fly-test-support")] +#[test] +fn genuine_probe_direct_plan_materialization_is_poisoned() { + let guard = crate::recurrence::on_the_fly::OnTheFlyForbiddenWorkGuardV1::begin().unwrap(); + assert!( + DirectRecurrencePlan::new(valid_parts()) + .unwrap_err() + .to_string() + .contains("DirectRecurrencePlan materialization is poisoned") + ); + let counts = guard.finish().unwrap(); + assert_eq!(counts.direct_plan_load_attempts, 0); + assert_eq!(counts.direct_plan_decode_attempts, 0); + assert_eq!(counts.direct_plan_materialization_attempts, 1); + assert_eq!(counts.established_builder_attempts, 0); + assert!(DirectRecurrencePlan::new(valid_parts()).is_ok()); +} + +#[test] +fn direct_descriptor_native_layouts_are_fixed_width() { + assert_eq!(std::mem::size_of::(), 48); + assert_eq!(std::mem::size_of::(), 28); + assert_eq!(std::mem::size_of::(), 32); + assert_eq!(std::mem::size_of::(), 24); + assert_eq!(std::mem::size_of::(), 40); + assert_eq!(std::mem::size_of::(), 32); + assert_eq!(std::mem::size_of::(), 16); + assert_eq!(std::mem::size_of::(), 8); + assert_eq!(std::mem::size_of::(), 16); + assert_eq!(std::mem::size_of::(), 48); + assert_eq!( + std::mem::size_of::(), + 32 + ); + assert_eq!(std::mem::size_of::(), 48); + assert_eq!(std::mem::size_of::(), 8); + assert_eq!( + std::mem::size_of::(), + 64 + ); + assert_eq!(std::mem::size_of::(), 12); + assert_eq!(std::mem::size_of::(), 8); + assert_eq!(std::mem::size_of::(), 8); +} + +#[test] +fn direct_plan_validates_and_has_a_stable_nonzero_layout_digest() { + let first = valid_plan(); + let second = valid_plan(); + assert_eq!(first, second); + assert_eq!( + first.runtime_layout_digest(), + second.runtime_layout_digest() + ); + assert_ne!(first.runtime_layout_digest().as_bytes(), &[0; 32]); +} + +#[test] +fn selector_work_summary_reports_active_rows_and_components() { + let plan = valid_plan(); + let summary = plan.selector_work_summary(0).unwrap(); + assert_eq!(summary.physical_sector_id, 0); + assert_eq!(summary.current_count, 2); + assert_eq!(summary.semantic_component_count, 3); + assert_eq!(summary.source_row_count, 1); + assert_eq!(summary.contribution_count, 1); + assert_eq!(summary.finalization_count, 1); + assert_eq!(summary.closure_count, 1); + assert_eq!(summary.amplitude_destination_count, 1); + assert_eq!(summary.row_count(), 4); +} + +#[test] +fn selector_domains_support_multiword_masks_and_legacy_universal_encoding() { + let mut parts = valid_parts(); + parts.physical_sector_count = 71; + parts.selector_domains.extend([ + DirectSelectorDomainDescriptor { + word_start: 1, + word_count: 2, + }, + DirectSelectorDomainDescriptor { + word_start: 3, + word_count: 1, + }, + ]); + parts.selector_words.extend([0, 1_u64 << 6, u64::MAX]); + let plan = DirectRecurrencePlan::new(parts).unwrap(); + + assert!(!plan.selector_domain_contains(1, 63).unwrap()); + assert!(plan.selector_domain_contains(1, 70).unwrap()); + assert!(plan.selector_domain_contains(2, 70).unwrap()); + assert!(plan.selector_domain_contains(2, 0).unwrap()); + assert!( + plan.selector_domain_contains(2, 71) + .unwrap_err() + .to_string() + .contains("out of bounds") + ); +} + +#[test] +fn direct_plan_rejects_selector_domains_inconsistent_with_bound_rows() { + let mut parts = valid_parts(); + parts.selector_domains.push(DirectSelectorDomainDescriptor { + word_start: 1, + word_count: 1, + }); + parts.selector_words.push(0); + parts.sources[0].selector_domain_id = 1; + let error = DirectRecurrencePlan::new(parts).unwrap_err(); + assert!(error.to_string().contains("does not match its current")); + + let mut parts = valid_parts(); + parts.selector_domains.push(DirectSelectorDomainDescriptor { + word_start: 1, + word_count: 1, + }); + parts.selector_words.push(0); + parts.amplitude_destinations[0].selector_domain_id = 1; + parts.closures[0].selector_domain_id = 1; + let error = DirectRecurrencePlan::new(parts).unwrap_err(); + assert!(error.to_string().contains("excludes its target sector")); +} + +#[test] +fn direct_plan_accepts_an_elided_identity_finalization() { + let mut parts = valid_parts(); + parts.currents[1].finalization_row_or_sentinel = DIRECT_NONE_U32; + parts.finalizations.clear(); + parts + .row_groups + .retain(|group| group.role != DirectExecutorRole::Finalization); + DirectRecurrencePlan::new(parts).unwrap(); +} + +#[test] +fn direct_plan_canonicalizes_a_missing_initialization_marker() { + let mut parts = valid_parts(); + parts.contributions[0].flags = 0; + let plan = DirectRecurrencePlan::new(parts).unwrap(); + assert_eq!( + plan.contributions()[0].flags, + DIRECT_CONTRIBUTION_FLAG_INITIALIZE_DESTINATION + ); +} + +#[test] +fn direct_plan_rejects_multiple_initialization_markers_for_one_current() { + let mut parts = valid_parts(); + parts.contributions.push(parts.contributions[0]); + parts.row_groups[1].row_count = 2; + let error = DirectRecurrencePlan::new(parts).unwrap_err(); + assert!(error.to_string().contains("exactly one")); +} + +#[test] +fn direct_plan_rejects_invalid_arena_and_optional_parent_references() { + let mut parts = valid_parts(); + parts.contributions[0].destination_component_base = 99; + assert!( + DirectRecurrencePlan::new(parts) + .unwrap_err() + .to_string() + .contains("arena base") + ); + + let mut parts = valid_parts(); + parts.contributions[0].parent1_component_base_or_sentinel = 0; + assert!( + DirectRecurrencePlan::new(parts) + .unwrap_err() + .to_string() + .contains("mismatched optional") + ); +} + +#[test] +fn direct_plan_rejects_overlapping_live_arena_ranges() { + let mut parts = valid_parts(); + parts.currents[1].component_base = 1; + parts.contributions[0].destination_component_base = 1; + parts.finalizations[0].component_base = 1; + parts.closures[0].parent0_component_base = 1; + let error = DirectRecurrencePlan::new(parts).unwrap_err(); + assert!(error.to_string().contains("shared by live currents")); +} + +#[test] +fn direct_plan_rejects_row_group_gaps_and_role_operation_mismatch() { + let mut parts = valid_parts(); + parts.row_groups[1].row_start = 1; + assert!( + DirectRecurrencePlan::new(parts) + .unwrap_err() + .to_string() + .contains("row partition") + ); + + let mut parts = valid_parts(); + parts.row_groups[1].destination_operation = DirectDestinationOperation::Initialize; + assert!( + DirectRecurrencePlan::new(parts) + .unwrap_err() + .to_string() + .contains("incompatible") + ); +} + +#[test] +fn direct_plan_rejects_non_permutation_replay_mappings() { + let mut parts = valid_parts(); + parts.external_source_count = 2; + parts.replay_targets[0].source_permutation_count = 2; + parts.source_permutations = vec![0, 0]; + parts.replay_momentum_signs = vec![1, 1]; + let error = DirectRecurrencePlan::new(parts).unwrap_err(); + assert!(error.to_string().contains("not a permutation")); +} + +#[test] +fn direct_plan_rejects_incomplete_or_out_of_range_replay_helicity_maps() { + let mut parts = valid_parts(); + parts.replay_targets[0].helicity_map_count = 0; + let error = DirectRecurrencePlan::new(parts).unwrap_err(); + assert!(error.to_string().contains("helicity map ranges cover")); + + let mut parts = valid_parts(); + parts.replay_helicity_map[0] = 1; + let error = DirectRecurrencePlan::new(parts).unwrap_err(); + assert!(error.to_string().contains("not a bijection")); +} + +#[test] +fn direct_plan_rejects_invalid_replay_momentum_signs() { + let mut parts = valid_parts(); + parts.replay_momentum_signs.clear(); + let error = DirectRecurrencePlan::new(parts).unwrap_err(); + assert!(error.to_string().contains("momentum signs")); + + let mut parts = valid_parts(); + parts.replay_momentum_signs[0] = 0; + let error = DirectRecurrencePlan::new(parts).unwrap_err(); + assert!(error.to_string().contains("momentum signs")); +} + +#[test] +fn direct_plan_requires_packed_replay_tables() { + let mut parts = valid_parts(); + parts.source_permutations.push(0); + parts.replay_momentum_signs.push(1); + let error = DirectRecurrencePlan::new(parts).unwrap_err(); + assert!(error.to_string().contains("ranges cover")); + + let mut parts = valid_parts(); + parts.replay_helicity_map.push(0); + let error = DirectRecurrencePlan::new(parts).unwrap_err(); + assert!(error.to_string().contains("ranges cover")); + + let mut parts = valid_parts(); + let mut overlapping = parts.replay_targets[0]; + overlapping.public_flow_id = 1; + overlapping.helicity_map_start = 1; + parts.replay_targets.push(overlapping); + parts.source_permutations.push(0); + parts.replay_momentum_signs.push(1); + parts.replay_helicity_map.push(0); + let error = DirectRecurrencePlan::new(parts).unwrap_err(); + assert!(error.to_string().contains("canonical partition")); + + let mut parts = valid_parts(); + let mut gapped = parts.replay_targets[0]; + gapped.public_flow_id = 1; + gapped.source_permutation_start = 2; + gapped.helicity_map_start = 1; + parts.replay_targets.push(gapped); + parts.source_permutations.extend_from_slice(&[0, 0]); + parts.replay_momentum_signs.extend_from_slice(&[1, 1]); + parts.replay_helicity_map.push(0); + let error = DirectRecurrencePlan::new(parts).unwrap_err(); + assert!(error.to_string().contains("canonical partition")); +} + +#[test] +fn direct_plan_authenticates_resolved_helicity_and_destination_contracts() { + let mut parts = valid_parts(); + parts.amplitude_destinations[0].target_sector_id = 1; + assert!( + DirectRecurrencePlan::new(parts) + .unwrap_err() + .to_string() + .contains("physical sector") + ); + + let mut parts = valid_parts(); + parts.resolved_helicities[0].source_state_count = 0; + assert!( + DirectRecurrencePlan::new(parts) + .unwrap_err() + .to_string() + .contains("source state") + ); + + let mut parts = valid_parts(); + parts.amplitude_destinations[0].target_helicity_id_or_sentinel = 1; + assert!( + DirectRecurrencePlan::new(parts) + .unwrap_err() + .to_string() + .contains("resolved helicity") + ); +} + +#[test] +fn changing_physical_layout_changes_only_the_layout_digest() { + let first = valid_plan(); + let mut parts = valid_parts(); + parts.point_tile_size = 128; + let second = DirectRecurrencePlan::new(parts).unwrap(); + assert_eq!(first.semantic_digest(), second.semantic_digest()); + assert_ne!( + first.runtime_layout_digest(), + second.runtime_layout_digest() + ); +} + +fn rebuild_proof_group( + parts: &DirectRecurrencePlanParts, + direct_row_id: Option, + component_factor_digest: SemanticDigest, + selector_domain_digest: SemanticDigest, +) -> ClosureProofMetadataV2 { + let group = &parts.closure_proofs.groups()[0]; + ClosureProofMetadataV2::new_with_three_line_certificates( + parts.closure_proofs.contributions().to_vec(), + vec![ + ClosureExecutionProofGroupV2::new( + group.id(), + group.emitted_runtime_closure_term_id(), + direct_row_id, + group.contribution_range(), + group.exact_summed_factor(), + component_factor_digest, + selector_domain_digest, + ) + .unwrap(), + ], + parts.closure_proofs.reflection_certificates().to_vec(), + parts + .closure_proofs + .three_line_traversal_certificates() + .to_vec(), + ) + .unwrap() +} + +#[test] +fn direct_plan_rejects_missing_or_misbound_runtime_closure_rows() { + let mut parts = valid_parts(); + parts.closure_proofs = rebuild_proof_group( + &parts, + None, + parts.closure_proofs.groups()[0].component_factor_digest(), + parts.closure_proofs.groups()[0].selector_domain_digest(), + ); + let error = DirectRecurrencePlan::new(parts).unwrap_err(); + assert!(error.to_string().contains("no direct closure row")); + + let mut parts = valid_parts(); + parts.closures[0].flags = 1; + let error = DirectRecurrencePlan::new(parts).unwrap_err(); + assert!(error.to_string().contains("names proof group")); +} + +#[test] +fn direct_plan_rejects_mutated_runtime_factor_parent_and_cold_digests() { + let mut parts = valid_parts(); + parts.exact_factors.push(ExactComplexRational::new( + ExactRational::new(-1, 1).unwrap(), + ExactRational::ZERO, + )); + parts.closures[0].exact_factor_id = 1; + let error = DirectRecurrencePlan::new(parts).unwrap_err(); + assert!(error.to_string().contains("factor does not match")); + + let mut parts = valid_parts(); + let original = &parts.closure_proofs.contributions()[0]; + let parent_mutation = ClosureProofContributionV2::new( + original.id(), + original.target_sector_id(), + original.target_destination_id(), + original.target_helicity_id(), + original.closure_template_id(), + original.closure_template_semantic_digest(), + original.quantum_flow_template_id(), + vec![0], + vec![Some(0)], + original.construction_parent_semantic_digests().to_vec(), + original.construction_parent_color_digests().to_vec(), + original.construction_parent_permutation().to_vec(), + original.reconstruction_parent_permutation().to_vec(), + original.evaluator_parent_permutation().to_vec(), + original.color_witness_term_id(), + original.color_witness_proof_digest(), + original.three_line_certificate_id(), + original.pairing_certificate_ids().to_vec(), + original.reflection_certificate_id(), + original.exact_factor(), + original.multiplicity(), + ) + .unwrap(); + parts.closure_proofs = ClosureProofMetadataV2::new_with_three_line_certificates( + vec![parent_mutation], + parts.closure_proofs.groups().to_vec(), + vec![], + parts + .closure_proofs + .three_line_traversal_certificates() + .to_vec(), + ) + .unwrap(); + let error = DirectRecurrencePlan::new(parts).unwrap_err(); + assert!(error.to_string().contains("parents do not match")); + + let mut parts = valid_parts(); + parts.closure_proofs = rebuild_proof_group( + &parts, + Some(0), + SemanticDigest::new([0x51; 32]).unwrap(), + parts.closure_proofs.groups()[0].selector_domain_digest(), + ); + let error = DirectRecurrencePlan::new(parts).unwrap_err(); + assert!(error.to_string().contains("component-factor digest")); + + let mut parts = valid_parts(); + parts.closure_proofs = rebuild_proof_group( + &parts, + Some(0), + parts.closure_proofs.groups()[0].component_factor_digest(), + SemanticDigest::new([0x52; 32]).unwrap(), + ); + let error = DirectRecurrencePlan::new(parts).unwrap_err(); + assert!(error.to_string().contains("selector-domain digest")); +} + +#[test] +fn direct_plan_accepts_cold_exact_zero_groups_without_runtime_rows() { + let mut parts = valid_parts(); + let original = parts.closure_proofs.contributions()[0].clone(); + let negative = ClosureProofContributionV2::new( + 2, + original.target_sector_id(), + None, + original.target_helicity_id(), + original.closure_template_id(), + original.closure_template_semantic_digest(), + original.quantum_flow_template_id(), + original.construction_parent_builder_ids().to_vec(), + vec![None], + original.construction_parent_semantic_digests().to_vec(), + original.construction_parent_color_digests().to_vec(), + original.construction_parent_permutation().to_vec(), + original.reconstruction_parent_permutation().to_vec(), + original.evaluator_parent_permutation().to_vec(), + original.color_witness_term_id(), + original.color_witness_proof_digest(), + original.three_line_certificate_id(), + original.pairing_certificate_ids().to_vec(), + original.reflection_certificate_id(), + ExactComplexRational::new(ExactRational::new(-1, 1).unwrap(), ExactRational::ZERO), + 1, + ) + .unwrap(); + let positive = ClosureProofContributionV2::new( + 1, + original.target_sector_id(), + None, + original.target_helicity_id(), + original.closure_template_id(), + original.closure_template_semantic_digest(), + original.quantum_flow_template_id(), + original.construction_parent_builder_ids().to_vec(), + vec![None], + original.construction_parent_semantic_digests().to_vec(), + original.construction_parent_color_digests().to_vec(), + original.construction_parent_permutation().to_vec(), + original.reconstruction_parent_permutation().to_vec(), + original.evaluator_parent_permutation().to_vec(), + original.color_witness_term_id(), + original.color_witness_proof_digest(), + original.three_line_certificate_id(), + original.pairing_certificate_ids().to_vec(), + original.reflection_certificate_id(), + ExactComplexRational::ONE, + 1, + ) + .unwrap(); + parts.closure_proofs = ClosureProofMetadataV2::new_with_three_line_certificates( + vec![original, positive, negative], + vec![ + ClosureExecutionProofGroupV2::new( + 0, + Some(0), + Some(0), + CheckedTableRange::new(0, 1), + ExactComplexRational::ONE, + closure_component_factor_digest_v2(&[ExactComplexRational::ONE]).unwrap(), + closure_selector_domain_digest_v2(&[1]).unwrap(), + ) + .unwrap(), + ClosureExecutionProofGroupV2::new( + 1, + None, + None, + CheckedTableRange::new(1, 2), + ExactComplexRational::ZERO, + SemanticDigest::new([0x53; 32]).unwrap(), + SemanticDigest::new([0x54; 32]).unwrap(), + ) + .unwrap(), + ], + vec![], + parts + .closure_proofs + .three_line_traversal_certificates() + .to_vec(), + ) + .unwrap(); + DirectRecurrencePlan::new(parts).unwrap(); +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/recurrence/direct_runtime.rs b/pyamplicol/rust/crates/rusticol-core/src/recurrence/direct_runtime.rs new file mode 100644 index 0000000000..a49aeabbf1 --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/recurrence/direct_runtime.rs @@ -0,0 +1,2669 @@ +// SPDX-License-Identifier: 0BSD + +//! Owned workspace for direct-arena recurrence execution. +//! +//! Ordinary construction performs every allocation needed by a point tile. +//! Packed-singleton-capable runtimes allocate only their scalar point storage +//! initially and materialize the aligned tiled storage on first use. Callers +//! fill the persistent momentum and parameter arenas, then execute the +//! authenticated direct plan without resizing the active runtime storage. + +use std::collections::{BTreeMap, BTreeSet}; +use std::mem::size_of; +use std::ops::Range; +use std::sync::Arc; +use std::time::{Duration, Instant}; + +#[cfg(any(test, feature = "on-the-fly-test-support"))] +use super::direct_backend::observe_direct_amplitudes_before_replay; +use super::direct_backend::{ + DIRECT_STATUS_OK, DirectContributionFanoutProgram, DirectCurrentClearRange, + DirectExecutionCounters, DirectExecutionRoleTimings, DirectExecutorCatalog, DirectFactorView, + DirectMomentumView, DirectParameterView, DirectStageClearRanges, + DirectUnionSourceDispatchHandle, DirectWorkspace, execute_direct_plan_destination_selected, + execute_direct_plan_profiled_with_fanout_and_traffic, + execute_direct_plan_selected_profiled_with_fanout_and_traffic, + execute_direct_plan_selected_unprofiled_with_fanout, + execute_direct_plan_unprofiled_with_fanout, +}; +use super::direct_plan::{ + DIRECT_CONTRIBUTION_FLAG_CERTIFIED_REUSE, DIRECT_CONTRIBUTION_FLAG_INITIALIZE_DESTINATION, + DirectAmplitudeDestinationDescriptor, DirectExecutorRole, DirectRecurrencePlan, + DirectResolvedHelicityDescriptor, DirectRowGroupDescriptor, +}; +use super::{DIRECT_NONE_U32, DirectNodeKind, RecurrenceStrategy, SemanticDigest}; +use crate::direct_arena::{ + AlignedF64Buffer, DIRECT_ARENA_ALIGNMENT, DirectArenaAllocationCounters, + DirectArenaTrafficCounters, DirectArenaWorkspace, checked_plane_scalar_len, + deterministic_point_tile_size, validate_direct_views, +}; +use crate::{RusticolError, RusticolResult}; + +/// Alignment used for the SIMD-facing numeric arenas. +pub const DIRECT_RUNTIME_ARENA_ALIGNMENT: usize = DIRECT_ARENA_ALIGNMENT; +const DIRECT_RUNTIME_CACHE_TARGET_BYTES: usize = 4 * 1024 * 1024; + +/// Borrowed component-major amplitude output for the most recent tile. +#[derive(Clone, Copy, Debug)] +pub struct DirectRecurrenceTileOutput<'a> { + amplitude_re: &'a [f64], + amplitude_im: &'a [f64], + amplitude_destinations: &'a [DirectAmplitudeDestinationDescriptor], + point_count: u32, + point_stride: u32, + destination_count: u32, + public_flow_id: Option, + representative_flow_id: Option, + selected_destination_ids: Option<&'a [u32]>, +} + +/// One already-selected amplitude and its active point planes. The public +/// random-access methods retain their independent selector checks; internal +/// reductions can consume this borrowed view without repeating those checks. +pub(crate) struct DirectRecurrenceAmplitudeView<'a> { + pub(crate) destination: &'a DirectAmplitudeDestinationDescriptor, + pub(crate) real: &'a [f64], + pub(crate) imaginary: &'a [f64], +} + +impl<'a> DirectRecurrenceTileOutput<'a> { + pub const fn point_count(&self) -> u32 { + self.point_count + } + + pub const fn point_stride(&self) -> u32 { + self.point_stride + } + + pub const fn destination_count(&self) -> u32 { + self.destination_count + } + + pub const fn public_flow_id(&self) -> Option { + self.public_flow_id + } + + pub const fn representative_flow_id(&self) -> Option { + self.representative_flow_id + } + + /// Full persistent real arena, including inactive tile tails. + pub const fn storage_re(&self) -> &'a [f64] { + self.amplitude_re + } + + /// Full persistent imaginary arena, including inactive tile tails. + pub const fn storage_im(&self) -> &'a [f64] { + self.amplitude_im + } + + pub fn destination_re(&self, destination_id: u32) -> Option<&[f64]> { + self.destination(self.amplitude_re, destination_id) + } + + pub fn destination_im(&self, destination_id: u32) -> Option<&[f64]> { + self.destination(self.amplitude_im, destination_id) + } + + pub fn selected_destination_ids(&self) -> impl Iterator + '_ { + let unrestricted = if self.selected_destination_ids.is_some() { + &[][..] + } else { + self.amplitude_destinations + }; + self.selected_destination_ids + .unwrap_or_default() + .iter() + .copied() + .chain( + unrestricted + .iter() + .filter(|destination| { + self.representative_flow_id.is_none_or(|representative| { + destination.target_sector_id == representative + }) + }) + .map(|destination| destination.id), + ) + } + + pub(crate) fn selected_amplitudes( + &self, + ) -> impl Iterator>> + '_ { + self.selected_destination_ids().map(|destination_id| { + // Explicit destination selections were checked against the + // representative when their selector was prepared. Unrestricted + // iteration above already filters by that representative. + let destination = self + .amplitude_destinations + .get(destination_id as usize) + .ok_or_else(|| invalid("selected amplitude descriptor is absent"))?; + let start = (destination_id as usize) + .checked_mul(self.point_stride as usize) + .ok_or_else(|| invalid("selected amplitude offset exceeds usize"))?; + let end = start + .checked_add(self.point_count as usize) + .ok_or_else(|| invalid("selected amplitude range exceeds usize"))?; + let real = self + .amplitude_re + .get(start..end) + .ok_or_else(|| invalid("selected real amplitude plane is absent"))?; + let imaginary = self + .amplitude_im + .get(start..end) + .ok_or_else(|| invalid("selected imaginary amplitude plane is absent"))?; + Ok(DirectRecurrenceAmplitudeView { + destination, + real, + imaginary, + }) + }) + } + + fn destination<'b>(&self, values: &'b [f64], destination_id: u32) -> Option<&'b [f64]> { + if destination_id >= self.destination_count { + return None; + } + if self + .selected_destination_ids + .is_some_and(|ids| ids.binary_search(&destination_id).is_err()) + { + return None; + } + let descriptor = self.amplitude_destinations.get(destination_id as usize)?; + if self + .representative_flow_id + .is_some_and(|representative| descriptor.target_sector_id != representative) + { + return None; + } + let start = destination_id as usize * self.point_stride as usize; + values.get(start..start + self.point_count as usize) + } +} + +/// Prepared, plan-authenticated topology-replay selection. +#[derive(Clone)] +pub struct DirectReplaySelectorPlan { + runtime_layout_digest: SemanticDigest, + public_flow_id: u32, + representative_flow_id: u32, + source_permutation: Arc<[u32]>, + source_momentum_signs: Arc<[i32]>, + helicity_map: Arc<[u32]>, + phase_re: f64, + phase_im: f64, + multiplicity: u32, + row_groups: Arc<[DirectRowGroupDescriptor]>, + amplitude_clear_ranges: Arc<[Range]>, + destination_program: Option>, +} + +/// A cold dependency closure, reusable across flows with the same representative. +struct DirectReplayDestinationProgram { + destination_ids: Arc<[u32]>, + row_groups: Box<[DirectRowGroupDescriptor]>, + amplitude_clear_ranges: Box<[Range]>, + current_clear_stages: Box<[DirectStageClearRanges]>, +} + +/// Prepared, plan-authenticated all-flow-union helicity selection. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct DirectUnionHelicitySelectorPlan { + runtime_layout_digest: SemanticDigest, + resolved_helicity_id: u32, + source_selection_start: usize, + source_selection_count: usize, +} + +impl DirectUnionHelicitySelectorPlan { + pub const fn resolved_helicity_id(&self) -> u32 { + self.resolved_helicity_id + } +} + +impl DirectReplaySelectorPlan { + pub const fn public_flow_id(&self) -> u32 { + self.public_flow_id + } + + pub const fn representative_flow_id(&self) -> u32 { + self.representative_flow_id + } + + pub const fn phase(&self) -> (f64, f64) { + (self.phase_re, self.phase_im) + } + + pub const fn multiplicity(&self) -> u32 { + self.multiplicity + } + + pub fn mapped_external_source_slot(&self, representative_source_slot: u32) -> Option { + self.source_permutation + .get(representative_source_slot as usize) + .copied() + } + + pub(crate) fn helicity_map(&self) -> &[u32] { + &self.helicity_map + } + + pub fn selected_row_group_count(&self) -> usize { + self.selected_row_groups().len() + } + + pub fn selected_row_count(&self) -> u64 { + self.selected_row_groups() + .iter() + .map(|group| u64::from(group.row_count)) + .sum() + } + + fn selected_row_groups(&self) -> &[DirectRowGroupDescriptor] { + self.destination_program + .as_ref() + .map_or(self.row_groups.as_ref(), |program| { + program.row_groups.as_ref() + }) + } + + fn identity(&self) -> DirectReplaySelectorIdentity { + DirectReplaySelectorIdentity { + runtime_layout_digest: self.runtime_layout_digest, + public_flow_id: self.public_flow_id, + representative_flow_id: self.representative_flow_id, + } + } +} + +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub struct DirectRuntimeActivityCounters { + pub momentum_fill_calls: u64, + pub momentum_forms_filled: u64, + pub momentum_terms_filled: u64, + pub momentum_scalar_values_filled: u64, + pub schedule_executions: u64, + pub replay_schedule_executions: u64, + pub replay_output_values_scaled: u64, + pub union_source_dispatch_calls: u64, + pub union_source_rows: u64, + pub union_schedule_executions: u64, +} + +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub struct DirectRuntimePhaseTimings { + pub momentum_fill: Duration, + pub union_source_fill: Duration, + pub direct_execution: Duration, + pub replay_output_mapping: Duration, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +struct DirectReplaySelectorIdentity { + runtime_layout_digest: SemanticDigest, + public_flow_id: u32, + representative_flow_id: u32, +} + +struct DirectPointStorage { + arena: DirectArenaWorkspace, + momenta: AlignedF64Buffer, +} + +#[allow(clippy::too_many_arguments)] +fn allocate_point_storage( + current_component_count: u32, + amplitude_destination_count: u32, + momentum_plane_count: u32, + momentum_form_count: u32, + lorentz_component_count: u16, + tile_capacity: u32, + packed_singleton: bool, + parameters: DirectParameterView, + factors: DirectFactorView, +) -> RusticolResult { + let mut arena = if packed_singleton { + DirectArenaWorkspace::new_packed_singleton( + current_component_count, + amplitude_destination_count, + )? + } else { + DirectArenaWorkspace::new( + current_component_count, + amplitude_destination_count, + tile_capacity, + )? + }; + let point_stride = arena.point_stride(); + arena.begin_tile(if packed_singleton { 1 } else { tile_capacity })?; + let momentum_label = if packed_singleton { + "packed singleton momentum arena" + } else { + "momentum arena" + }; + let momenta = AlignedF64Buffer::zeroed( + checked_plane_scalar_len(momentum_plane_count, point_stride, momentum_label)?, + if packed_singleton { + "packed singleton momentum" + } else { + "momentum" + }, + )?; + validate_direct_views( + arena.view()?, + DirectMomentumView { + values: momenta.as_ptr(), + scalar_len: u64::try_from(momenta.len()) + .map_err(|_| invalid("momentum arena exceeds u64"))?, + form_count: momentum_form_count, + lorentz_component_count, + point_stride, + }, + parameters, + factors, + )?; + Ok(DirectPointStorage { arena, momenta }) +} + +/// One owned, thread-local direct recurrence execution workspace. +pub struct DirectRecurrenceExecutionRuntime { + plan: DirectRecurrencePlan, + executors: DirectExecutorCatalog, + arena: DirectArenaWorkspace, + momenta: AlignedF64Buffer, + parameters_re: AlignedF64Buffer, + parameters_im: AlignedF64Buffer, + factors_re: AlignedF64Buffer, + factors_im: AlignedF64Buffer, + contribution_fanout: DirectContributionFanoutProgram, + union_source_dispatch: Option, + additive_amplitude_ranges: Vec>, + momentum_form_count: u32, + lorentz_component_count: u16, + runtime_current_component_count: u32, + momentum_plane_count: u32, + tile_capacity: u32, + point_stride: u32, + alternate_point_storage: Option, + packed_singleton_active: bool, + momenta_are_compact: bool, + momentum_filled_points: u32, + momentum_replay_selector: Option, + last_point_count: u32, + last_public_flow_id: Option, + last_representative_flow_id: Option, + last_selected_destination_ids: Option>, + counters: DirectExecutionCounters, + allocation_counters: DirectArenaAllocationCounters, + traffic_counters: DirectArenaTrafficCounters, + role_timings: DirectExecutionRoleTimings, + activity_counters: DirectRuntimeActivityCounters, + timings: DirectRuntimePhaseTimings, +} + +impl DirectRecurrenceExecutionRuntime { + /// Allocate the initial workspace and convert the plan's exact factors to + /// their persistent binary64 split-complex catalog. Packed-capable + /// runtimes defer aligned tiled point storage until it is needed. + pub fn new( + plan: DirectRecurrencePlan, + executors: DirectExecutorCatalog, + lorentz_component_count: u16, + ) -> RusticolResult { + Self::new_inner(plan, executors, lorentz_component_count, None) + } + + /// Construct an all-flow-union workspace with its authenticated SourceIR + /// dispatcher. The dispatcher context is owned by the same loaded backend + /// owner that backs `executors` and must outlive this runtime. + pub fn new_with_union_source_dispatch( + plan: DirectRecurrencePlan, + executors: DirectExecutorCatalog, + lorentz_component_count: u16, + union_source_dispatch: DirectUnionSourceDispatchHandle, + ) -> RusticolResult { + if plan.strategy() != RecurrenceStrategy::AllFlowUnion { + return Err(invalid( + "union source dispatch requires an all-flow-union recurrence plan", + )); + } + Self::new_inner( + plan, + executors, + lorentz_component_count, + Some(union_source_dispatch), + ) + } + + #[cfg_attr(target_vendor = "apple", unsafe(link_section = "__TEXT,__rcl_load"))] + #[cfg_attr(target_vendor = "apple", inline(never))] + fn new_inner( + plan: DirectRecurrencePlan, + executors: DirectExecutorCatalog, + lorentz_component_count: u16, + union_source_dispatch: Option, + ) -> RusticolResult { + if lorentz_component_count == 0 { + return Err(invalid("Lorentz component count must be positive")); + } + + let contribution_fanout = DirectContributionFanoutProgram::build(&plan, &executors)?; + let runtime_current_component_count = plan + .current_arena_components() + .checked_add(contribution_fanout.scratch_component_count()) + .ok_or_else(|| invalid("direct fanout current arena exceeds u32"))?; + + let momentum_form_count = u32::try_from(plan.momentum_forms().len()) + .map_err(|_| invalid("momentum form count exceeds u32"))?; + let split_complex_scalar_count = usize::try_from(runtime_current_component_count) + .ok() + .and_then(|current| current.checked_mul(2)) + .and_then(|current| { + usize::try_from(plan.amplitude_destination_count()) + .ok() + .and_then(|amplitude| amplitude.checked_mul(2)) + .and_then(|amplitude| current.checked_add(amplitude)) + }) + .ok_or_else(|| invalid("split-complex per-point workspace size overflows usize"))?; + let cache_split_complex_scalar_count = replay_cache_split_complex_scalar_count( + &plan, + split_complex_scalar_count, + contribution_fanout.scratch_component_count(), + )?; + let momentum_scalar_count = usize::try_from(momentum_form_count) + .ok() + .and_then(|forms| forms.checked_mul(usize::from(lorentz_component_count))) + .ok_or_else(|| invalid("per-point momentum workspace size overflows usize"))?; + let per_point_scalar_count = split_complex_scalar_count + .checked_add(momentum_scalar_count) + .ok_or_else(|| invalid("per-point workspace size overflows usize"))?; + let workspace_bytes = usize::try_from(plan.workspace_mib()) + .ok() + .and_then(|mib| mib.checked_mul(1024 * 1024)) + .ok_or_else(|| invalid("workspace byte target overflows usize"))?; + let workspace_tile = deterministic_point_tile_size( + plan.point_tile_size(), + workspace_bytes, + usize::MAX, + per_point_scalar_count, + )?; + let split_complex_per_point_bytes = cache_split_complex_scalar_count + .checked_mul(size_of::()) + .filter(|bytes| *bytes != 0) + .ok_or_else(|| invalid("split-complex per-point bytes overflow usize"))?; + let cache_tile = greatest_power_of_two_not_exceeding( + DIRECT_RUNTIME_CACHE_TARGET_BYTES / split_complex_per_point_bytes, + ); + // The accepted recurrence cache policy is deliberately lane-local: + // it counts current+amplitude split halves, while the workspace + // target above counts those halves plus every momentum plane. + // Fill at least one already allocated aligned pitch when the request + // allows it, rather than repeatedly running a few lanes. + let minimum_tiled_points = (DIRECT_RUNTIME_ARENA_ALIGNMENT / size_of::()) as u32; + let packed_singleton_capable = executors.packed_singleton_capable(); + let tile_capacity = workspace_tile.min( + u32::try_from(cache_tile) + .unwrap_or(u32::MAX) + .max(minimum_tiled_points), + ); + let momentum_plane_count = usize::try_from(momentum_form_count) + .ok() + .and_then(|forms| forms.checked_mul(usize::from(lorentz_component_count))) + .ok_or_else(|| invalid("momentum plane count overflows usize"))?; + let momentum_plane_count_u32 = u32::try_from(momentum_plane_count) + .map_err(|_| invalid("momentum plane count exceeds u32"))?; + let parameter_len = usize::try_from(plan.parameter_value_count()) + .map_err(|_| invalid("parameter value count exceeds usize"))?; + let factor_len = plan + .exact_factors() + .len() + .checked_add(usize::from(contribution_fanout.needs_unit_factor())) + .ok_or_else(|| invalid("direct fanout factor catalog exceeds usize"))?; + + let additive_amplitude_ranges = amplitude_clear_ranges(&plan)?; + let parameters_re = AlignedF64Buffer::zeroed(parameter_len, "parameter real")?; + let parameters_im = AlignedF64Buffer::zeroed(parameter_len, "parameter imaginary")?; + let mut factors_re = AlignedF64Buffer::zeroed(factor_len, "factor real")?; + let mut factors_im = AlignedF64Buffer::zeroed(factor_len, "factor imaginary")?; + for ((factor_re, factor_im), factor) in factors_re + .as_mut_slice() + .iter_mut() + .zip(factors_im.as_mut_slice()) + .zip(plan.exact_factors()) + { + *factor_re = factor.real().numerator() as f64 / factor.real().denominator() as f64; + *factor_im = factor.imag().numerator() as f64 / factor.imag().denominator() as f64; + } + if contribution_fanout.needs_unit_factor() { + let unit = plan.exact_factors().len(); + factors_re.as_mut_slice()[unit] = 1.0; + factors_im.as_mut_slice()[unit] = 0.0; + } + let parameter_view = DirectParameterView { + values_re: parameters_re.as_ptr(), + values_im: parameters_im.as_ptr(), + value_count: u32::try_from(parameters_re.len()) + .map_err(|_| invalid("parameter catalog exceeds u32"))?, + }; + let factor_view = DirectFactorView { + values_re: factors_re.as_ptr(), + values_im: factors_im.as_ptr(), + value_count: u32::try_from(factors_re.len()) + .map_err(|_| invalid("factor catalog exceeds u32"))?, + }; + let point_storage = allocate_point_storage( + runtime_current_component_count, + plan.amplitude_destination_count(), + momentum_plane_count_u32, + momentum_form_count, + lorentz_component_count, + tile_capacity, + packed_singleton_capable, + parameter_view, + factor_view, + )?; + let DirectPointStorage { arena, momenta } = point_storage; + let point_stride = arena.point_stride(); + let allocation_counters = [ + momenta.allocation_counters(), + parameters_re.allocation_counters(), + parameters_im.allocation_counters(), + factors_re.allocation_counters(), + factors_im.allocation_counters(), + ] + .into_iter() + .try_fold(arena.allocation_counters(), |total, counters| { + total.checked_add(counters) + })?; + + Ok(Self { + plan, + executors, + arena, + momenta, + parameters_re, + parameters_im, + factors_re, + factors_im, + contribution_fanout, + union_source_dispatch, + additive_amplitude_ranges, + momentum_form_count, + lorentz_component_count, + runtime_current_component_count, + momentum_plane_count: momentum_plane_count_u32, + tile_capacity, + point_stride, + alternate_point_storage: None, + packed_singleton_active: packed_singleton_capable, + momenta_are_compact: false, + momentum_filled_points: 0, + momentum_replay_selector: None, + last_point_count: 0, + last_public_flow_id: None, + last_representative_flow_id: None, + last_selected_destination_ids: None, + counters: DirectExecutionCounters::default(), + allocation_counters, + traffic_counters: DirectArenaTrafficCounters::default(), + role_timings: DirectExecutionRoleTimings::default(), + activity_counters: DirectRuntimeActivityCounters::default(), + timings: DirectRuntimePhaseTimings::default(), + }) + } + + pub const fn plan(&self) -> &DirectRecurrencePlan { + &self.plan + } + + pub const fn point_tile_size(&self) -> u32 { + self.tile_capacity + } + + /// Physical component-plane pitch passed through every direct ABI view. + /// This is `1` while packed singleton storage is active and changes to the + /// aligned tiled pitch after a successful layout transition. + pub const fn point_stride(&self) -> u32 { + self.point_stride + } + + pub const fn momentum_form_count(&self) -> u32 { + self.momentum_form_count + } + + pub const fn lorentz_component_count(&self) -> u16 { + self.lorentz_component_count + } + + /// Resolve one physical flow once. The returned selector owns its fixed + /// source permutation and can be reused for every tile without allocation. + pub fn prepare_replay_selector( + &self, + public_flow_id: u32, + ) -> RusticolResult { + if !self.plan.strategy().uses_topology_replay_targets() { + return Err(invalid( + "replay selectors require a replay-capable recurrence plan", + )); + } + let mut matches = self + .plan + .replay_targets() + .iter() + .filter(|target| target.public_flow_id == public_flow_id); + let target = matches + .next() + .ok_or_else(|| invalid(format!("public flow {public_flow_id} is not retained")))?; + if matches.next().is_some() { + return Err(RusticolError::integrity(format!( + "direct recurrence public flow {public_flow_id} has multiple replay targets" + ))); + } + if !self + .plan + .amplitude_destinations() + .iter() + .any(|destination| destination.target_sector_id == target.representative_id) + { + return Err(RusticolError::integrity(format!( + "direct recurrence replay representative {} has no amplitude destination", + target.representative_id + ))); + } + + let start = usize::try_from(target.source_permutation_start).map_err(|_| { + RusticolError::integrity( + "direct recurrence replay source permutation start exceeds usize", + ) + })?; + let count = usize::try_from(target.source_permutation_count).map_err(|_| { + RusticolError::integrity( + "direct recurrence replay source permutation count exceeds usize", + ) + })?; + let end = start.checked_add(count).ok_or_else(|| { + RusticolError::integrity( + "direct recurrence replay source permutation range overflows usize", + ) + })?; + let permutation = self + .plan + .source_permutations() + .get(start..end) + .ok_or_else(|| { + RusticolError::integrity( + "direct recurrence replay source permutation is out of bounds", + ) + })?; + let mut source_permutation = Vec::new(); + source_permutation + .try_reserve_exact(permutation.len()) + .map_err(|error| { + RusticolError::internal(format!( + "could not allocate direct recurrence replay selector: {error}" + )) + })?; + source_permutation.extend_from_slice(permutation); + let momentum_signs = self + .plan + .replay_momentum_signs() + .get(start..end) + .ok_or_else(|| { + RusticolError::integrity( + "direct recurrence replay momentum-sign map is out of bounds", + ) + })?; + let mut source_momentum_signs = Vec::new(); + source_momentum_signs + .try_reserve_exact(momentum_signs.len()) + .map_err(|error| { + RusticolError::internal(format!( + "could not allocate direct recurrence replay momentum signs: {error}" + )) + })?; + source_momentum_signs.extend_from_slice(momentum_signs); + let helicity_start = usize::try_from(target.helicity_map_start).map_err(|_| { + RusticolError::integrity("direct recurrence replay helicity-map start exceeds usize") + })?; + let helicity_count = usize::try_from(target.helicity_map_count).map_err(|_| { + RusticolError::integrity("direct recurrence replay helicity-map count exceeds usize") + })?; + let helicity_end = helicity_start.checked_add(helicity_count).ok_or_else(|| { + RusticolError::integrity("direct recurrence replay helicity-map range overflows usize") + })?; + let mapped_helicities = self + .plan + .replay_helicity_map() + .get(helicity_start..helicity_end) + .ok_or_else(|| { + RusticolError::integrity("direct recurrence replay helicity map is out of bounds") + })?; + let mut helicity_map = Vec::new(); + helicity_map + .try_reserve_exact(mapped_helicities.len()) + .map_err(|error| { + RusticolError::internal(format!( + "could not allocate direct recurrence replay helicity map: {error}" + )) + })?; + helicity_map.extend_from_slice(mapped_helicities); + + let phase = *self + .plan + .exact_factors() + .get(target.phase_exact_factor_id as usize) + .ok_or_else(|| { + RusticolError::integrity("direct recurrence replay phase factor is out of bounds") + })?; + let row_groups = selected_replay_row_groups(&self.plan, target.representative_id)?; + let amplitude_clear_ranges = + selected_amplitude_clear_ranges(&self.plan, target.representative_id)?; + Ok(DirectReplaySelectorPlan { + runtime_layout_digest: self.plan.runtime_layout_digest(), + public_flow_id, + representative_flow_id: target.representative_id, + source_permutation: source_permutation.into(), + source_momentum_signs: source_momentum_signs.into(), + helicity_map: helicity_map.into(), + phase_re: phase.real().numerator() as f64 / phase.real().denominator() as f64, + phase_im: phase.imag().numerator() as f64 / phase.imag().denominator() as f64, + multiplicity: target.multiplicity, + row_groups: row_groups.into(), + amplitude_clear_ranges: amplitude_clear_ranges.into(), + destination_program: None, + }) + } + + /// Prepare just the dependency closure of the requested representative + /// amplitude destinations. Sources remain helicity-generic in the plan; + /// numerical current identities, including identities between helicities, + /// are followed through their real parent dependencies here. + pub fn prepare_replay_selector_for_destinations( + &self, + base: &DirectReplaySelectorPlan, + needed_destinations: &[u32], + ) -> RusticolResult { + self.validate_replay_selector(base)?; + let mut selector = base.clone(); + selector.destination_program = Some(Arc::new(selected_replay_destination_program( + &self.plan, + selector.representative_flow_id, + &selector.row_groups, + needed_destinations, + )?)); + Ok(selector) + } + + /// Reuse an already prepared closure for another public flow represented + /// by the same sector. Only the momentum/helicity replay mapping differs. + pub fn prepare_replay_selector_with_destination_program( + &self, + base: &DirectReplaySelectorPlan, + prepared: &DirectReplaySelectorPlan, + ) -> RusticolResult { + self.validate_replay_selector(base)?; + self.validate_replay_selector(prepared)?; + let mut selector = base.clone(); + if selector.representative_flow_id != prepared.representative_flow_id { + return Err(invalid( + "replay destination program has a different representative", + )); + } + selector.destination_program = Some(Arc::clone( + prepared + .destination_program + .as_ref() + .ok_or_else(|| invalid("replay selector has no restricted destination program"))?, + )); + Ok(selector) + } + + /// Resolve one retained helicity once for all-flow-union execution. + /// + /// The returned selector is a pair of authenticated ranges into the + /// immutable plan. Reusing it for later tiles performs no allocation. + pub fn prepare_union_helicity_selector( + &self, + resolved_helicity_id: u32, + ) -> RusticolResult { + if self.plan.strategy() != RecurrenceStrategy::AllFlowUnion { + return Err(invalid( + "union helicity selectors require an all-flow-union recurrence plan", + )); + } + if self.union_source_dispatch.is_none() { + return Err(invalid( + "all-flow-union recurrence runtime has no source dispatcher", + )); + } + let descriptor = self + .plan + .resolved_helicities() + .get(resolved_helicity_id as usize) + .filter(|descriptor| descriptor.id == resolved_helicity_id) + .ok_or_else(|| { + invalid(format!( + "resolved helicity {resolved_helicity_id} is not retained" + )) + })?; + let (source_selection_start, source_selection_count) = + self.validate_union_helicity_descriptor(descriptor)?; + Ok(DirectUnionHelicitySelectorPlan { + runtime_layout_digest: self.plan.runtime_layout_digest(), + resolved_helicity_id, + source_selection_start, + source_selection_count, + }) + } + + /// Mutable form-major, Lorentz-component-major momentum arena using the + /// compatibility-facing semantic tile pitch returned by + /// [`Self::point_tile_size`]. + /// + /// The generic Direct-Arena ABI uses [`Self::point_stride`] internally. + /// When alignment padding separates physical planes, this accessor + /// compacts active plane storage in place and restores it before the next + /// direct call. A packed runtime allocates its aligned tiled storage on + /// first legacy access; no auxiliary packet buffer is involved. + pub fn momenta_mut(&mut self) -> RusticolResult<&mut [f64]> { + self.force_tiled_layout()?; + self.compact_momenta_for_legacy_access(); + self.momentum_filled_points = self.tile_capacity; + self.momentum_replay_selector = None; + let compact_len = self.compact_momentum_len(); + Ok(&mut self.momenta.as_mut_slice()[..compact_len]) + } + + /// Mutable physical Direct-Arena momentum storage. + /// + /// Plane starts use [`Self::point_stride`], including alignment padding. + /// A packed runtime allocates its aligned tiled storage on first access. + pub fn physical_momenta_mut(&mut self) -> RusticolResult<&mut [f64]> { + self.force_tiled_layout()?; + self.restore_physical_momenta(); + self.momentum_filled_points = self.tile_capacity; + self.momentum_replay_selector = None; + Ok(self.momenta.as_mut_slice()) + } + + /// One persistent momentum plane using the compatibility-facing semantic + /// tile pitch, including its inactive semantic tile tail. A packed runtime + /// allocates its aligned tiled storage on first valid access. + pub fn momentum_plane_mut( + &mut self, + momentum_form_id: u32, + lorentz_component: u16, + ) -> RusticolResult> { + if momentum_form_id >= self.momentum_form_count + || lorentz_component >= self.lorentz_component_count + { + return Ok(None); + } + self.force_tiled_layout()?; + self.compact_momenta_for_legacy_access(); + self.momentum_filled_points = self.tile_capacity; + self.momentum_replay_selector = None; + let plane = momentum_form_id as usize * usize::from(self.lorentz_component_count) + + usize::from(lorentz_component); + let start = plane * self.tile_capacity as usize; + Ok(self + .momenta + .as_mut_slice() + .get_mut(start..start + self.tile_capacity as usize)) + } + + /// Fill every canonical momentum form from point-major external + /// `[point][source_slot][lorentz_component]` four-momenta. + pub fn fill_momenta_from_external( + &mut self, + selector: &DirectReplaySelectorPlan, + point_count: u32, + external_four_momenta: &[f64], + ) -> RusticolResult<()> { + self.fill_momenta_from_external_impl::( + selector, + point_count, + external_four_momenta, + ) + } + + pub fn fill_momenta_from_external_unprofiled( + &mut self, + selector: &DirectReplaySelectorPlan, + point_count: u32, + external_four_momenta: &[f64], + ) -> RusticolResult<()> { + self.fill_momenta_from_external_impl::( + selector, + point_count, + external_four_momenta, + ) + } + + fn fill_momenta_from_external_impl( + &mut self, + selector: &DirectReplaySelectorPlan, + point_count: u32, + external_four_momenta: &[f64], + ) -> RusticolResult<()> { + self.validate_replay_selector(selector)?; + self.validate_point_count(point_count)?; + if self.lorentz_component_count != 4 { + return Err(invalid(format!( + "external four-momentum fill requires 4 Lorentz components, runtime has {}", + self.lorentz_component_count + ))); + } + let source_count = usize::try_from(self.plan.external_source_count()) + .map_err(|_| invalid("external source count exceeds usize"))?; + let expected_len = (point_count as usize) + .checked_mul(source_count) + .and_then(|values| values.checked_mul(4)) + .ok_or_else(|| invalid("point-major external momentum length overflows usize"))?; + if external_four_momenta.len() != expected_len { + return Err(invalid(format!( + "point-major external momentum length is {}, expected {expected_len}", + external_four_momenta.len() + ))); + } + + self.activate_point_layout(point_count)?; + self.restore_physical_momenta(); + let started = PROFILE.then(Instant::now); + let point_stride = self.point_stride as usize; + let active_points = point_count as usize; + let previous_points = self.momentum_filled_points as usize; + let form_count = self.plan.momentum_forms().len(); + let terms = self.plan.momentum_terms(); + let momenta = self.momenta.as_mut_slice(); + for (form_id, form) in self.plan.momentum_forms().iter().enumerate() { + // The immutable plan has already validated term ranges, source + // slots and replay maps. Consume those persisted instructions once + // per term, not once per component and phase-space point. + let term_start = form.term_start as usize; + let form_terms = &terms[term_start..term_start + form.term_count as usize]; + let form_start = form_id * 4 * point_stride; + fill_momentum_form_from_external::( + &mut momenta[form_start..form_start + 4 * point_stride], + point_stride, + active_points, + external_four_momenta, + source_count, + form_terms.iter().map(|term| { + let slot = term.source_slot as usize; + ( + selector.source_permutation[slot] as usize, + f64::from(term.coefficient) + * f64::from(selector.source_momentum_signs[slot]), + ) + }), + ); + } + if active_points < previous_points { + for plane in 0..form_count * 4 { + let start = plane * point_stride + active_points; + let end = plane * point_stride + previous_points; + momenta[start..end].fill(0.0); + } + } + + self.momentum_filled_points = point_count; + self.momentum_replay_selector = Some(selector.identity()); + if PROFILE { + self.activity_counters.momentum_fill_calls = + self.activity_counters.momentum_fill_calls.saturating_add(1); + self.activity_counters.momentum_forms_filled = self + .activity_counters + .momentum_forms_filled + .saturating_add(form_count as u64); + self.activity_counters.momentum_terms_filled = self + .activity_counters + .momentum_terms_filled + .saturating_add(terms.len() as u64); + let scalar_values = (form_count as u64) + .saturating_mul(4) + .saturating_mul(u64::from(point_count)); + self.activity_counters.momentum_scalar_values_filled = self + .activity_counters + .momentum_scalar_values_filled + .saturating_add(scalar_values); + self.timings.momentum_fill += started + .expect("profiled momentum fill has a start time") + .elapsed(); + } + Ok(()) + } + + /// Fill canonical momentum forms without a replay permutation. + fn fill_identity_momenta_from_external( + &mut self, + point_count: u32, + external_four_momenta: &[f64], + ) -> RusticolResult<()> { + if !self.plan.strategy().materializes_all_color_sectors() { + return Err(invalid( + "identity momentum fill requires a complete-color recurrence plan", + )); + } + self.validate_point_count(point_count)?; + if self.lorentz_component_count != 4 { + return Err(invalid(format!( + "external four-momentum fill requires 4 Lorentz components, runtime has {}", + self.lorentz_component_count + ))); + } + let source_count = usize::try_from(self.plan.external_source_count()) + .map_err(|_| invalid("external source count exceeds usize"))?; + let expected_len = (point_count as usize) + .checked_mul(source_count) + .and_then(|values| values.checked_mul(4)) + .ok_or_else(|| invalid("point-major external momentum length overflows usize"))?; + if external_four_momenta.len() != expected_len { + return Err(invalid(format!( + "point-major external momentum length is {}, expected {expected_len}", + external_four_momenta.len() + ))); + } + + self.activate_point_layout(point_count)?; + self.restore_physical_momenta(); + let started = PROFILE.then(Instant::now); + let point_stride = self.point_stride as usize; + let active_points = point_count as usize; + let previous_points = self.momentum_filled_points as usize; + let form_count = self.plan.momentum_forms().len(); + let terms = self.plan.momentum_terms(); + let momenta = self.momenta.as_mut_slice(); + for (form_id, form) in self.plan.momentum_forms().iter().enumerate() { + let term_start = form.term_start as usize; + let form_terms = &terms[term_start..term_start + form.term_count as usize]; + let form_start = form_id * 4 * point_stride; + fill_momentum_form_from_external::( + &mut momenta[form_start..form_start + 4 * point_stride], + point_stride, + active_points, + external_four_momenta, + source_count, + form_terms + .iter() + .map(|term| (term.source_slot as usize, f64::from(term.coefficient))), + ); + } + if active_points < previous_points { + for plane in 0..form_count * 4 { + let start = plane * point_stride + active_points; + let end = plane * point_stride + previous_points; + momenta[start..end].fill(0.0); + } + } + + self.momentum_filled_points = point_count; + self.momentum_replay_selector = None; + if PROFILE { + self.activity_counters.momentum_fill_calls = + self.activity_counters.momentum_fill_calls.saturating_add(1); + self.activity_counters.momentum_forms_filled = self + .activity_counters + .momentum_forms_filled + .saturating_add(form_count as u64); + self.activity_counters.momentum_terms_filled = self + .activity_counters + .momentum_terms_filled + .saturating_add(terms.len() as u64); + self.activity_counters.momentum_scalar_values_filled = self + .activity_counters + .momentum_scalar_values_filled + .saturating_add( + (form_count as u64) + .saturating_mul(4) + .saturating_mul(u64::from(point_count)), + ); + self.timings.momentum_fill += started + .expect("profiled union momentum fill has a start time") + .elapsed(); + } + Ok(()) + } + + pub fn parameters_mut(&mut self) -> (&mut [f64], &mut [f64]) { + ( + self.parameters_re.as_mut_slice(), + self.parameters_im.as_mut_slice(), + ) + } + + pub fn factors_mut(&mut self) -> (&mut [f64], &mut [f64]) { + let public_count = self.plan.exact_factors().len(); + ( + &mut self.factors_re.as_mut_slice()[..public_count], + &mut self.factors_im.as_mut_slice()[..public_count], + ) + } + + pub fn set_parameters(&mut self, values_re: &[f64], values_im: &[f64]) -> RusticolResult<()> { + validate_split_values("parameter", self.parameters_re.len(), values_re, values_im)?; + self.parameters_re.as_mut_slice().copy_from_slice(values_re); + self.parameters_im.as_mut_slice().copy_from_slice(values_im); + Ok(()) + } + + pub fn set_factors(&mut self, values_re: &[f64], values_im: &[f64]) -> RusticolResult<()> { + let public_count = self.plan.exact_factors().len(); + validate_split_values("factor", public_count, values_re, values_im)?; + self.factors_re.as_mut_slice()[..public_count].copy_from_slice(values_re); + self.factors_im.as_mut_slice()[..public_count].copy_from_slice(values_im); + Ok(()) + } + + /// Current planes in the active packed or tiled physical layout. + pub fn current_arenas(&self) -> (&[f64], &[f64]) { + let (real, imaginary) = self.arena.current_slices(); + let public_len = self.plan.current_arena_components() as usize * self.point_stride as usize; + (&real[..public_len], &imaginary[..public_len]) + } + + /// Amplitude planes in the active packed or tiled physical layout. + pub fn amplitude_arenas(&self) -> (&[f64], &[f64]) { + self.arena.amplitude_slices() + } + + /// Cumulative direct calls and rows since construction or the last reset. + pub const fn counters(&self) -> DirectExecutionCounters { + self.counters + } + + pub const fn activity_counters(&self) -> DirectRuntimeActivityCounters { + self.activity_counters + } + + pub const fn allocation_counters(&self) -> DirectArenaAllocationCounters { + self.allocation_counters + } + + pub const fn traffic_counters(&self) -> DirectArenaTrafficCounters { + self.traffic_counters + } + + pub const fn role_timings(&self) -> DirectExecutionRoleTimings { + self.role_timings + } + + pub const fn phase_timings(&self) -> DirectRuntimePhaseTimings { + self.timings + } + + pub fn reset_counters(&mut self) { + self.counters = DirectExecutionCounters::default(); + self.traffic_counters = DirectArenaTrafficCounters::default(); + self.role_timings = DirectExecutionRoleTimings::default(); + self.activity_counters = DirectRuntimeActivityCounters::default(); + self.timings = DirectRuntimePhaseTimings::default(); + } + + pub fn outputs(&self) -> Option> { + (self.last_point_count != 0).then(|| { + self.borrowed_output( + self.last_point_count, + self.last_public_flow_id, + self.last_representative_flow_id, + ) + }) + } + + /// Execute one tile using the persistent inputs already held by this + /// workspace. + pub fn execute_tile( + &mut self, + point_count: u32, + ) -> RusticolResult> { + if self.plan.strategy() == RecurrenceStrategy::AllFlowUnion { + return Err(invalid( + "all-flow-union execution requires a prepared runtime helicity selector", + )); + } + self.execute_direct_tile(point_count)?; + self.last_point_count = point_count; + self.last_public_flow_id = None; + self.last_representative_flow_id = None; + Ok(self.borrowed_output(point_count, None, None)) + } + + /// Execute a tile whose momentum forms were filled for `selector`, then + /// map representative amplitudes in place to the public flow convention. + pub fn execute_replay_tile( + &mut self, + selector: &DirectReplaySelectorPlan, + point_count: u32, + ) -> RusticolResult> { + self.execute_replay_tile_impl::(selector, point_count) + } + + pub fn execute_replay_tile_unprofiled( + &mut self, + selector: &DirectReplaySelectorPlan, + point_count: u32, + ) -> RusticolResult> { + self.execute_replay_tile_impl::(selector, point_count) + } + + fn execute_replay_tile_impl( + &mut self, + selector: &DirectReplaySelectorPlan, + point_count: u32, + ) -> RusticolResult> { + self.validate_replay_selector(selector)?; + self.validate_point_count(point_count)?; + self.activate_point_layout(point_count)?; + if self.momentum_filled_points != point_count + || self.momentum_replay_selector != Some(selector.identity()) + { + return Err(invalid( + "replay tile momentum forms were not filled for this selector and point count", + )); + } + + self.execute_selected_direct_tile_impl::(selector, point_count)?; + self.scale_replay_amplitudes::( + selector, + selector + .destination_program + .as_ref() + .map(|program| program.destination_ids.as_ref()), + point_count, + )?; + self.last_point_count = point_count; + self.last_public_flow_id = Some(selector.public_flow_id); + self.last_representative_flow_id = Some(selector.representative_flow_id); + self.last_selected_destination_ids = selector + .destination_program + .as_ref() + .map(|program| Arc::clone(&program.destination_ids)); + Ok(self.borrowed_output( + point_count, + self.last_public_flow_id, + self.last_representative_flow_id, + )) + } + + fn scale_replay_amplitudes( + &mut self, + selector: &DirectReplaySelectorPlan, + selected_destination_ids: Option<&[u32]>, + point_count: u32, + ) -> RusticolResult<()> { + let started = PROFILE.then(Instant::now); + let scale_re = selector.phase_re * f64::from(selector.multiplicity); + let scale_im = selector.phase_im * f64::from(selector.multiplicity); + let point_stride = self.point_stride as usize; + let active_points = point_count as usize; + let mut scaled_values = 0_u64; + let (_, _, amplitude_re, amplitude_im) = self.arena.split_slices_mut(); + let unrestricted = if selected_destination_ids.is_some() { + 0 + } else { + self.plan.amplitude_destinations().len() + }; + for destination_id in selected_destination_ids + .unwrap_or_default() + .iter() + .map(|&id| id as usize) + .chain(0..unrestricted) + { + let destination = self.plan.amplitude_destinations()[destination_id]; + if destination.target_sector_id != selector.representative_flow_id { + continue; + } + let start = destination_id * point_stride; + let end = start + active_points; + let values_re = &mut amplitude_re[start..end]; + let values_im = &mut amplitude_im[start..end]; + for point in 0..active_points { + let value_re = values_re[point]; + let value_im = values_im[point]; + values_re[point] = value_re * scale_re - value_im * scale_im; + values_im[point] = value_re * scale_im + value_im * scale_re; + } + scaled_values = scaled_values.saturating_add(u64::from(point_count)); + } + if PROFILE { + self.activity_counters.replay_schedule_executions = self + .activity_counters + .replay_schedule_executions + .saturating_add(1); + self.activity_counters.replay_output_values_scaled = self + .activity_counters + .replay_output_values_scaled + .saturating_add(scaled_values); + self.timings.replay_output_mapping += started + .expect("profiled replay mapping has a start time") + .elapsed(); + } + Ok(()) + } + + /// Fill and execute one selected replay tile without any intermediate + /// momentum-form materialization by the caller. + pub fn execute_replay_tile_from_external( + &mut self, + selector: &DirectReplaySelectorPlan, + point_count: u32, + external_four_momenta: &[f64], + ) -> RusticolResult> { + self.fill_momenta_from_external(selector, point_count, external_four_momenta)?; + self.execute_replay_tile(selector, point_count) + } + + pub fn execute_replay_tile_from_external_unprofiled( + &mut self, + selector: &DirectReplaySelectorPlan, + point_count: u32, + external_four_momenta: &[f64], + ) -> RusticolResult> { + self.fill_momenta_from_external_unprofiled(selector, point_count, external_four_momenta)?; + self.execute_replay_tile_unprofiled(selector, point_count) + } + + /// Internal lane input is `[source][component][point]`, written directly + /// into its existing external-input workspace. The public point-major + /// entrypoints remain unchanged; this adds no preparation or transpose. + pub(crate) fn execute_replay_tile_from_external_planes( + &mut self, + selector: &DirectReplaySelectorPlan, + point_count: u32, + external_four_momenta: &[f64], + ) -> RusticolResult> { + self.fill_momenta_from_external_impl::( + selector, + point_count, + external_four_momenta, + )?; + self.execute_replay_tile_impl::(selector, point_count) + } + + /// Fill one runtime helicity's union sources and execute the compact + /// all-flow schedule exactly once. + pub fn execute_union_tile_from_external( + &mut self, + selector: &DirectUnionHelicitySelectorPlan, + point_count: u32, + external_four_momenta: &[f64], + ) -> RusticolResult> { + self.execute_union_tile_from_external_impl::( + selector, + point_count, + external_four_momenta, + ) + } + + pub fn execute_union_tile_from_external_unprofiled( + &mut self, + selector: &DirectUnionHelicitySelectorPlan, + point_count: u32, + external_four_momenta: &[f64], + ) -> RusticolResult> { + self.execute_union_tile_from_external_impl::( + selector, + point_count, + external_four_momenta, + ) + } + + pub(crate) fn execute_union_tile_from_external_planes( + &mut self, + selector: &DirectUnionHelicitySelectorPlan, + point_count: u32, + external_four_momenta: &[f64], + ) -> RusticolResult> { + self.execute_union_tile_from_external_impl::( + selector, + point_count, + external_four_momenta, + ) + } + + fn execute_union_tile_from_external_impl( + &mut self, + selector: &DirectUnionHelicitySelectorPlan, + point_count: u32, + external_four_momenta: &[f64], + ) -> RusticolResult> { + self.validate_union_selector(selector)?; + self.fill_identity_momenta_from_external::( + point_count, + external_four_momenta, + )?; + self.execute_union_sources::(selector, point_count)?; + self.execute_direct_tile_impl::(point_count)?; + if PROFILE { + self.activity_counters.union_schedule_executions = self + .activity_counters + .union_schedule_executions + .saturating_add(1); + } + self.last_point_count = point_count; + self.last_public_flow_id = None; + self.last_representative_flow_id = None; + Ok(self.borrowed_output(point_count, None, None)) + } + + /// Execute a complete contracted-color schedule with fixed source templates. + pub fn execute_contracted_tile_from_external( + &mut self, + point_count: u32, + external_four_momenta: &[f64], + ) -> RusticolResult> { + self.execute_contracted_tile_from_external_impl::( + point_count, + external_four_momenta, + ) + } + + pub fn execute_contracted_tile_from_external_unprofiled( + &mut self, + point_count: u32, + external_four_momenta: &[f64], + ) -> RusticolResult> { + self.execute_contracted_tile_from_external_impl::( + point_count, + external_four_momenta, + ) + } + + pub(crate) fn execute_contracted_tile_from_external_planes( + &mut self, + point_count: u32, + external_four_momenta: &[f64], + ) -> RusticolResult> { + self.execute_contracted_tile_from_external_impl::( + point_count, + external_four_momenta, + ) + } + + fn execute_contracted_tile_from_external_impl( + &mut self, + point_count: u32, + external_four_momenta: &[f64], + ) -> RusticolResult> { + if self.plan.strategy() != RecurrenceStrategy::ContractedColorUnion { + return Err(invalid( + "contracted-color execution requires a contracted-color recurrence plan", + )); + } + self.fill_identity_momenta_from_external::( + point_count, + external_four_momenta, + )?; + self.execute_direct_tile_impl::(point_count)?; + self.last_point_count = point_count; + self.last_public_flow_id = None; + self.last_representative_flow_id = None; + Ok(self.borrowed_output(point_count, None, None)) + } + + fn execute_union_sources( + &mut self, + selector: &DirectUnionHelicitySelectorPlan, + point_count: u32, + ) -> RusticolResult<()> { + self.restore_physical_momenta(); + let handle = self + .union_source_dispatch + .ok_or_else(|| invalid("all-flow-union recurrence runtime has no source dispatcher"))?; + let selection_end = selector + .source_selection_start + .checked_add(selector.source_selection_count) + .ok_or_else(|| { + RusticolError::integrity( + "direct recurrence union source-selection range overflows usize", + ) + })?; + let selections = self + .plan + .resolved_source_selections() + .get(selector.source_selection_start..selection_end) + .ok_or_else(|| { + RusticolError::integrity( + "direct recurrence union source-selection range is out of bounds", + ) + })?; + + let momentum_scalar_len = + u64::try_from(self.momenta.len()).map_err(|_| invalid("momentum arena exceeds u64"))?; + let parameter_count = u32::try_from(self.parameters_re.len()) + .map_err(|_| invalid("parameter catalog exceeds u32"))?; + let factor_count = u32::try_from(self.factors_re.len()) + .map_err(|_| invalid("factor catalog exceeds u32"))?; + let source_count = u32::try_from(self.plan.sources().len()) + .map_err(|_| invalid("source row count exceeds u32"))?; + let variant_count = u32::try_from(self.plan.source_dispatch_variants().len()) + .map_err(|_| invalid("source-dispatch variant count exceeds u32"))?; + let embedding_count = u32::try_from(self.plan.source_embeddings().len()) + .map_err(|_| invalid("source embedding count exceeds u32"))?; + let selection_count = u32::try_from(selections.len()) + .map_err(|_| invalid("source selection count exceeds u32"))?; + + self.arena.begin_tile(point_count)?; + let arena = self.arena.view()?; + let momenta = DirectMomentumView { + values: self.momenta.as_ptr(), + scalar_len: momentum_scalar_len, + form_count: self.momentum_form_count, + lorentz_component_count: self.lorentz_component_count, + point_stride: self.point_stride, + }; + let parameters = DirectParameterView { + values_re: self.parameters_re.as_ptr(), + values_im: self.parameters_im.as_ptr(), + value_count: parameter_count, + }; + let factors = DirectFactorView { + values_re: self.factors_re.as_ptr(), + values_im: self.factors_im.as_ptr(), + value_count: factor_count, + }; + validate_direct_views(arena, momenta, parameters, factors)?; + + let started = PROFILE.then(Instant::now); + if PROFILE { + self.traffic_counters + .record_call(selection_count, point_count); + } + let status = unsafe { + (handle.call)( + handle.context, + arena, + momenta, + parameters, + factors, + self.plan.sources().as_ptr(), + source_count, + self.plan.source_dispatch_variants().as_ptr(), + variant_count, + self.plan.source_embeddings().as_ptr(), + embedding_count, + selections.as_ptr(), + selection_count, + point_count, + ) + }; + if PROFILE { + self.timings.union_source_fill += started + .expect("profiled union source fill has a start time") + .elapsed(); + } + if status != DIRECT_STATUS_OK { + return Err(RusticolError::evaluation(format!( + "direct recurrence union source dispatcher returned status {status}" + ))); + } + if PROFILE { + self.counters.source_calls = self.counters.source_calls.saturating_add(1); + self.counters.source_rows = self + .counters + .source_rows + .saturating_add(u64::from(selection_count)); + self.activity_counters.union_source_dispatch_calls = self + .activity_counters + .union_source_dispatch_calls + .saturating_add(1); + self.activity_counters.union_source_rows = self + .activity_counters + .union_source_rows + .saturating_add(u64::from(selection_count)); + } + Ok(()) + } + + fn execute_direct_tile(&mut self, point_count: u32) -> RusticolResult<()> { + self.execute_direct_tile_impl::(point_count) + } + + fn execute_direct_tile_impl( + &mut self, + point_count: u32, + ) -> RusticolResult<()> { + self.execute_direct_tile_rows_impl::(point_count, None) + } + + fn execute_selected_direct_tile_impl( + &mut self, + selector: &DirectReplaySelectorPlan, + point_count: u32, + ) -> RusticolResult<()> { + self.execute_direct_tile_rows_impl::(point_count, Some(selector)) + } + + fn execute_direct_tile_rows_impl( + &mut self, + point_count: u32, + selector: Option<&DirectReplaySelectorPlan>, + ) -> RusticolResult<()> { + self.validate_point_count(point_count)?; + self.activate_point_layout(point_count)?; + self.restore_physical_momenta(); + self.arena.begin_tile(point_count)?; + self.last_point_count = 0; + self.last_public_flow_id = None; + self.last_representative_flow_id = None; + // `execute_direct_plan` initializes every current stage exactly once + self.last_selected_destination_ids = None; + // before its first contribution group. Re-clearing all current planes + // here only duplicates that work. + let amplitude_ranges = + selector.map_or(self.additive_amplitude_ranges.as_slice(), |selector| { + selector + .destination_program + .as_ref() + .map_or(selector.amplitude_clear_ranges.as_ref(), |program| { + program.amplitude_clear_ranges.as_ref() + }) + }); + for range in amplitude_ranges { + let component_base = u32::try_from(range.start).map_err(|_| { + RusticolError::integrity("direct recurrence amplitude clear base exceeds u32") + })?; + let component_count = u32::try_from(range.end - range.start).map_err(|_| { + RusticolError::integrity("direct recurrence amplitude clear count exceeds u32") + })?; + self.arena + .clear_amplitude_active(component_base, component_count)?; + } + + { + let (current_re, current_im, amplitude_re, amplitude_im) = + self.arena.split_slices_mut(); + let mut workspace = DirectWorkspace { + current_re, + current_im, + amplitude_re, + amplitude_im, + momenta: self.momenta.as_slice(), + momentum_form_count: self.momentum_form_count, + lorentz_component_count: self.lorentz_component_count, + parameters_re: self.parameters_re.as_slice(), + parameters_im: self.parameters_im.as_slice(), + factors_re: self.factors_re.as_slice(), + factors_im: self.factors_im.as_slice(), + point_stride: self.point_stride, + }; + let started = PROFILE.then(Instant::now); + let result = if let Some((selector, program)) = selector.and_then(|selector| { + selector + .destination_program + .as_ref() + .map(|program| (selector, program)) + }) { + execute_direct_plan_destination_selected::( + &self.plan, + &program.row_groups, + selector.representative_flow_id, + &program.current_clear_stages, + &self.executors, + &mut workspace, + point_count, + &mut self.counters, + &mut self.role_timings, + &mut self.traffic_counters, + ) + } else { + match (PROFILE, selector) { + (true, Some(selector)) => { + execute_direct_plan_selected_profiled_with_fanout_and_traffic( + &self.plan, + &selector.row_groups, + selector.representative_flow_id, + &self.executors, + &self.contribution_fanout, + &mut workspace, + point_count, + &mut self.counters, + &mut self.role_timings, + &mut self.traffic_counters, + ) + } + (false, Some(selector)) => execute_direct_plan_selected_unprofiled_with_fanout( + &self.plan, + &selector.row_groups, + selector.representative_flow_id, + &self.executors, + &self.contribution_fanout, + &mut workspace, + point_count, + ), + (true, None) => execute_direct_plan_profiled_with_fanout_and_traffic( + &self.plan, + &self.executors, + &self.contribution_fanout, + &mut workspace, + point_count, + &mut self.counters, + &mut self.role_timings, + &mut self.traffic_counters, + ), + (false, None) => execute_direct_plan_unprofiled_with_fanout( + &self.plan, + &self.executors, + &self.contribution_fanout, + &mut workspace, + point_count, + ), + } + }; + if PROFILE { + self.timings.direct_execution += started + .expect("profiled direct execution has a start time") + .elapsed(); + } + result?; + #[cfg(any(test, feature = "on-the-fly-test-support"))] + observe_direct_amplitudes_before_replay( + &self.plan, + selector.map(DirectReplaySelectorPlan::representative_flow_id), + &workspace, + point_count, + )?; + } + + if PROFILE { + self.activity_counters.schedule_executions = + self.activity_counters.schedule_executions.saturating_add(1); + } + Ok(()) + } + + fn borrowed_output( + &self, + point_count: u32, + public_flow_id: Option, + representative_flow_id: Option, + ) -> DirectRecurrenceTileOutput<'_> { + let (amplitude_re, amplitude_im) = self.arena.amplitude_slices(); + DirectRecurrenceTileOutput { + amplitude_re, + amplitude_im, + amplitude_destinations: self.plan.amplitude_destinations(), + point_count, + point_stride: self.point_stride, + destination_count: self.plan.amplitude_destination_count(), + public_flow_id, + representative_flow_id, + selected_destination_ids: self.last_selected_destination_ids.as_deref(), + } + } + + fn validate_point_count(&self, point_count: u32) -> RusticolResult<()> { + if point_count == 0 { + return Err(invalid("point count must be positive")); + } + if point_count > self.tile_capacity { + return Err(invalid(format!( + "point count {point_count} exceeds point tile size {}", + self.tile_capacity + ))); + } + Ok(()) + } + + fn compact_momentum_len(&self) -> usize { + self.momentum_form_count as usize + * usize::from(self.lorentz_component_count) + * self.tile_capacity as usize + } + + fn activate_point_layout(&mut self, point_count: u32) -> RusticolResult<()> { + let packed = point_count == 1; + if packed == self.packed_singleton_active || !self.executors.packed_singleton_capable() { + return Ok(()); + } + if !packed { + self.ensure_tiled_point_storage()?; + } + if self.alternate_point_storage.is_none() { + return Ok(()); + } + if packed { + self.restore_physical_momenta(); + self.copy_momentum_point_zero_to_alternate(); + let exact_singleton = self.momentum_filled_points == 1; + let legacy_full_tile = self.momentum_replay_selector.is_none() + && self.momentum_filled_points == self.tile_capacity; + let replay_selector = exact_singleton + .then_some(self.momentum_replay_selector) + .flatten(); + self.swap_point_storage(); + self.momentum_filled_points = u32::from(exact_singleton || legacy_full_tile); + self.momentum_replay_selector = replay_selector; + return Ok(()); + } + self.copy_momentum_point_zero_to_alternate(); + self.swap_point_storage(); + Ok(()) + } + + fn force_tiled_layout(&mut self) -> RusticolResult<()> { + if self.packed_singleton_active { + self.ensure_tiled_point_storage()?; + self.copy_momentum_point_zero_to_alternate(); + self.swap_point_storage(); + } + Ok(()) + } + + fn ensure_tiled_point_storage(&mut self) -> RusticolResult<()> { + if self.alternate_point_storage.is_some() { + return Ok(()); + } + let parameter_view = DirectParameterView { + values_re: self.parameters_re.as_ptr(), + values_im: self.parameters_im.as_ptr(), + value_count: u32::try_from(self.parameters_re.len()) + .map_err(|_| invalid("parameter catalog exceeds u32"))?, + }; + let factor_view = DirectFactorView { + values_re: self.factors_re.as_ptr(), + values_im: self.factors_im.as_ptr(), + value_count: u32::try_from(self.factors_re.len()) + .map_err(|_| invalid("factor catalog exceeds u32"))?, + }; + let storage = allocate_point_storage( + self.runtime_current_component_count, + self.plan.amplitude_destination_count(), + self.momentum_plane_count, + self.momentum_form_count, + self.lorentz_component_count, + self.tile_capacity, + false, + parameter_view, + factor_view, + )?; + let allocation_counters = self + .allocation_counters + .checked_add(storage.arena.allocation_counters())? + .checked_add(storage.momenta.allocation_counters())?; + self.alternate_point_storage = Some(storage); + self.allocation_counters = allocation_counters; + Ok(()) + } + + fn copy_momentum_point_zero_to_alternate(&mut self) { + let source_stride = self.point_stride as usize; + let alternate = self + .alternate_point_storage + .as_mut() + .expect("point-zero copy requires alternate point storage"); + let destination_stride = alternate.arena.point_stride() as usize; + let plane_count = self.momentum_plane_count as usize; + assert_eq!( + self.momenta.len(), + plane_count * source_stride, + "active momentum storage must cover every physical plane" + ); + assert_eq!( + alternate.momenta.len(), + plane_count * destination_stride, + "alternate momentum storage must cover every physical plane" + ); + let source = self.momenta.as_slice(); + let destination = alternate.momenta.as_mut_slice(); + for plane in 0..plane_count { + destination[plane * destination_stride] = source[plane * source_stride]; + } + } + + fn swap_point_storage(&mut self) { + let alternate = self + .alternate_point_storage + .as_mut() + .expect("layout switching requires alternate point storage"); + std::mem::swap(&mut self.arena, &mut alternate.arena); + std::mem::swap(&mut self.momenta, &mut alternate.momenta); + self.point_stride = self.arena.point_stride(); + self.momenta_are_compact = false; + self.momentum_filled_points = 0; + self.momentum_replay_selector = None; + self.packed_singleton_active = !self.packed_singleton_active; + self.last_point_count = 0; + self.last_public_flow_id = None; + self.last_representative_flow_id = None; + } + + fn compact_momenta_for_legacy_access(&mut self) { + if self.momenta_are_compact { + return; + } + let plane_count = + self.momentum_form_count as usize * usize::from(self.lorentz_component_count); + let tile_capacity = self.tile_capacity as usize; + let point_stride = self.point_stride as usize; + if tile_capacity != point_stride { + let momenta = self.momenta.as_mut_slice(); + for plane in 0..plane_count { + let source_start = plane * point_stride; + let destination_start = plane * tile_capacity; + momenta.copy_within( + source_start..source_start + tile_capacity, + destination_start, + ); + } + } + self.momenta_are_compact = true; + } + + fn restore_physical_momenta(&mut self) { + if !self.momenta_are_compact { + return; + } + let plane_count = + self.momentum_form_count as usize * usize::from(self.lorentz_component_count); + let tile_capacity = self.tile_capacity as usize; + let point_stride = self.point_stride as usize; + if tile_capacity != point_stride { + let momenta = self.momenta.as_mut_slice(); + for plane in (0..plane_count).rev() { + let source_start = plane * tile_capacity; + let destination_start = plane * point_stride; + momenta.copy_within( + source_start..source_start + tile_capacity, + destination_start, + ); + } + } + self.momenta_are_compact = false; + } + + fn validate_replay_selector(&self, selector: &DirectReplaySelectorPlan) -> RusticolResult<()> { + if selector.runtime_layout_digest != self.plan.runtime_layout_digest() { + return Err(RusticolError::integrity( + "direct recurrence replay selector belongs to a different plan", + )); + } + if selector.source_permutation.len() != self.plan.external_source_count() as usize { + return Err(RusticolError::integrity( + "direct recurrence replay selector source permutation has an inconsistent length", + )); + } + if selector.source_momentum_signs.len() != selector.source_permutation.len() { + return Err(RusticolError::integrity( + "direct recurrence replay selector has inconsistent momentum-sign coverage", + )); + } + Ok(()) + } + + fn validate_union_helicity_descriptor( + &self, + descriptor: &DirectResolvedHelicityDescriptor, + ) -> RusticolResult<(usize, usize)> { + let start = usize::try_from(descriptor.source_selection_start).map_err(|_| { + RusticolError::integrity("direct recurrence union source-selection start exceeds usize") + })?; + let count = usize::try_from(descriptor.source_selection_count).map_err(|_| { + RusticolError::integrity("direct recurrence union source-selection count exceeds usize") + })?; + if count != self.plan.external_source_count() as usize { + return Err(RusticolError::integrity( + "direct recurrence union helicity does not select every external source", + )); + } + let end = start.checked_add(count).ok_or_else(|| { + RusticolError::integrity( + "direct recurrence union source-selection range overflows usize", + ) + })?; + let selections = self + .plan + .resolved_source_selections() + .get(start..end) + .ok_or_else(|| { + RusticolError::integrity( + "direct recurrence union source-selection range is out of bounds", + ) + })?; + for (source_slot, selection) in selections.iter().enumerate() { + if selection.source_slot != source_slot as u32 { + return Err(RusticolError::integrity( + "direct recurrence union source selections are not in source-slot order", + )); + } + let variant = self + .plan + .source_dispatch_variants() + .get(selection.dispatch_variant_id as usize) + .ok_or_else(|| { + RusticolError::integrity( + "direct recurrence union source selection references an absent variant", + ) + })?; + let source = self + .plan + .sources() + .get(variant.source_row_id as usize) + .ok_or_else(|| { + RusticolError::integrity( + "direct recurrence union source variant references an absent row", + ) + })?; + if source.source_slot != selection.source_slot + || source.source_template_or_dispatch_domain != variant.dispatch_domain_id + { + return Err(RusticolError::integrity( + "direct recurrence union source selection disagrees with its variant", + )); + } + let embedding_start = usize::try_from(variant.embedding_start).map_err(|_| { + RusticolError::integrity( + "direct recurrence union source embedding start exceeds usize", + ) + })?; + let embedding_end = embedding_start + .checked_add(variant.embedding_count as usize) + .ok_or_else(|| { + RusticolError::integrity( + "direct recurrence union source embedding range overflows usize", + ) + })?; + if self + .plan + .source_embeddings() + .get(embedding_start..embedding_end) + .is_none() + { + return Err(RusticolError::integrity( + "direct recurrence union source embedding range is out of bounds", + )); + } + } + Ok((start, count)) + } + + fn validate_union_selector( + &self, + selector: &DirectUnionHelicitySelectorPlan, + ) -> RusticolResult<()> { + if self.plan.strategy() != RecurrenceStrategy::AllFlowUnion { + return Err(invalid( + "union helicity selector requires an all-flow-union recurrence plan", + )); + } + if selector.runtime_layout_digest != self.plan.runtime_layout_digest() { + return Err(RusticolError::integrity( + "direct recurrence union helicity selector belongs to a different plan", + )); + } + let descriptor = self + .plan + .resolved_helicities() + .get(selector.resolved_helicity_id as usize) + .filter(|descriptor| descriptor.id == selector.resolved_helicity_id) + .ok_or_else(|| { + RusticolError::integrity( + "direct recurrence union helicity selector references an absent helicity", + ) + })?; + let (start, count) = self.validate_union_helicity_descriptor(descriptor)?; + if start != selector.source_selection_start || count != selector.source_selection_count { + return Err(RusticolError::integrity( + "direct recurrence union helicity selector ranges do not match the plan", + )); + } + Ok(()) + } +} + +fn replay_cache_split_complex_scalar_count( + plan: &DirectRecurrencePlan, + persisted_split_complex_scalar_count: usize, + fanout_scratch_component_count: u32, +) -> RusticolResult { + if plan.strategy() != RecurrenceStrategy::TopologyReplay { + return Ok(persisted_split_complex_scalar_count); + } + let representative_sectors = plan + .replay_targets() + .iter() + .map(|target| target.representative_id) + .collect::>(); + if representative_sectors.is_empty() { + return Ok(persisted_split_complex_scalar_count); + } + let current_arena_components = u64::from(plan.current_arena_components()); + let mut maximum = 0_u64; + for representative_sector_id in representative_sectors { + let summary = plan.selector_work_summary(representative_sector_id)?; + let active_current_components = summary + .semantic_component_count + .min(current_arena_components); + let active_complex_planes = active_current_components + .checked_add(summary.amplitude_destination_count) + .and_then(|count| count.checked_add(u64::from(fanout_scratch_component_count))) + .ok_or_else(|| invalid("selected replay cache plane count overflows u64"))?; + let active_split_scalars = active_complex_planes + .checked_mul(2) + .ok_or_else(|| invalid("selected replay cache scalar count overflows u64"))?; + maximum = maximum.max(active_split_scalars); + } + let maximum = usize::try_from(maximum) + .map_err(|_| invalid("selected replay cache scalar count exceeds usize"))?; + if maximum == 0 { + return Err(invalid( + "topology-replay selector domains expose no cache-resident work", + )); + } + Ok(maximum.min(persisted_split_complex_scalar_count)) +} + +fn greatest_power_of_two_not_exceeding(value: usize) -> usize { + if value == 0 { + return 1; + } + 1_usize << (usize::BITS - 1 - value.leading_zeros()) +} + +/// Component storage can be recycled after a current's last use. Resolve +/// parents by lifetime, not by their physical component address alone. +struct ReplayCurrentIndex { + by_stage_and_base: BTreeMap<(u16, u32), usize>, + by_base: BTreeMap>, +} + +impl ReplayCurrentIndex { + fn new(plan: &DirectRecurrencePlan) -> RusticolResult { + let mut by_stage_and_base = BTreeMap::new(); + let mut by_base = BTreeMap::>::new(); + for (id, current) in plan.currents().iter().enumerate() { + if by_stage_and_base + .insert((current.stage, current.component_base), id) + .is_some() + { + return Err(invalid("replay current address is repeated within a stage")); + } + by_base.entry(current.component_base).or_default().push(id); + } + for ids in by_base.values_mut() { + ids.sort_unstable_by_key(|&id| { + let current = &plan.currents()[id]; + (current.first_use, current.last_use, id) + }); + } + Ok(Self { + by_stage_and_base, + by_base, + }) + } + + fn destination(&self, stage: u16, base: u32) -> RusticolResult { + self.by_stage_and_base + .get(&(stage, base)) + .copied() + .ok_or_else(|| invalid("replay contribution has no current at its stage")) + } + + fn parent(&self, plan: &DirectRecurrencePlan, stage: u16, base: u32) -> RusticolResult { + let ids = self + .by_base + .get(&base) + .ok_or_else(|| invalid("replay parent current address is absent"))?; + let end = ids.partition_point(|&id| plan.currents()[id].first_use <= u32::from(stage)); + ids.get(end.wrapping_sub(1)) + .copied() + .filter(|&id| plan.currents()[id].last_use >= u32::from(stage)) + .ok_or_else(|| invalid("replay parent current is not live at its use stage")) + } +} + +fn selected_replay_destination_program( + plan: &DirectRecurrencePlan, + representative: u32, + flow_groups: &[DirectRowGroupDescriptor], + destination_ids: &[u32], +) -> RusticolResult { + if destination_ids.windows(2).any(|pair| pair[0] >= pair[1]) { + return Err(invalid( + "replay amplitude destination IDs must be sorted and unique", + )); + } + let mut amplitude_marks = + zeroed_marks(plan.amplitude_destinations().len(), "destination selection")?; + for &id in destination_ids { + let destination = plan + .amplitude_destinations() + .get(id as usize) + .filter(|destination| destination.target_sector_id == representative) + .ok_or_else(|| { + invalid("replay amplitude destination is outside the selected representative") + })?; + amplitude_marks[destination.id as usize] = 1; + } + + let index = ReplayCurrentIndex::new(plan)?; + let mut source_marks = zeroed_marks(plan.sources().len(), "source selection")?; + let mut contribution_marks = + zeroed_marks(plan.contributions().len(), "contribution selection")?; + let mut finalization_marks = + zeroed_marks(plan.finalizations().len(), "finalization selection")?; + let mut closure_marks = zeroed_marks(plan.closures().len(), "closure selection")?; + let mut current_marks = zeroed_marks(plan.currents().len(), "current selection")?; + let mut contributions_by_current = vec![Vec::<(usize, u16)>::new(); plan.currents().len()]; + let mut finalizations_by_current = vec![Vec::::new(); plan.currents().len()]; + let mut pending = Vec::new(); + for group in flow_groups { + let start = group.row_start as usize; + let end = start + group.row_count as usize; + match group.role { + DirectExecutorRole::Contribution => { + for row_id in start..end { + let row = &plan.contributions()[row_id]; + let current = index.destination(group.stage, row.destination_component_base)?; + contributions_by_current[current].push((row_id, group.stage)); + } + } + DirectExecutorRole::Finalization => { + for row_id in start..end { + let row = &plan.finalizations()[row_id]; + let current = index.destination(group.stage, row.component_base)?; + finalizations_by_current[current].push(row_id); + } + } + DirectExecutorRole::Closure => { + for (row, mark) in plan.closures()[start..end] + .iter() + .zip(&mut closure_marks[start..end]) + { + if amplitude_marks[row.amplitude_destination_id as usize] == 0 { + continue; + } + *mark = 1; + pending.push(index.parent(plan, group.stage, row.parent0_component_base)?); + if row.parent1_component_base_or_sentinel != DIRECT_NONE_U32 { + pending.push(index.parent( + plan, + group.stage, + row.parent1_component_base_or_sentinel, + )?); + } + } + } + DirectExecutorRole::Source => {} + } + } + while let Some(current_id) = pending.pop() { + if current_marks[current_id] != 0 { + continue; + } + current_marks[current_id] = 1; + let current = &plan.currents()[current_id]; + if current.source_row_or_sentinel != DIRECT_NONE_U32 { + source_marks[current.source_row_or_sentinel as usize] = 1; + } + for &row_id in &finalizations_by_current[current_id] { + finalization_marks[row_id] = 1; + } + for &(row_id, stage) in &contributions_by_current[current_id] { + let row = &plan.contributions()[row_id]; + contribution_marks[row_id] = 1; + pending.push(index.parent(plan, stage, row.parent0_component_base)?); + if row.parent1_component_base_or_sentinel != DIRECT_NONE_U32 { + pending.push(index.parent(plan, stage, row.parent1_component_base_or_sentinel)?); + } + } + } + + let mut row_groups = Vec::new(); + for group in flow_groups { + let marks = match group.role { + DirectExecutorRole::Source => &source_marks, + DirectExecutorRole::Contribution => &contribution_marks, + DirectExecutorRole::Finalization => &finalization_marks, + DirectExecutorRole::Closure => &closure_marks, + }; + let end = group.row_start as usize + group.row_count as usize; + let mut row = group.row_start as usize; + while row < end { + if marks[row] == 0 { + row += 1; + continue; + } + let start = row; + while row < end && marks[row] != 0 { + row += 1; + } + row_groups.push(DirectRowGroupDescriptor { + row_start: start as u64, + row_count: (row - start) as u32, + ..*group + }); + } + } + let mut clear_by_stage = BTreeMap::>>::new(); + for (id, current) in plan.currents().iter().enumerate() { + if current_marks[id] == 0 || current.node_kind != DirectNodeKind::Current { + continue; + } + // Dependency closure retains every contribution to a live current in + // this flow selection, in its persisted execution order. When that + // first retained row overwrites the destination, clearing it would + // duplicate the write. Check the already-built row list here rather + // than adding a separate analysis or any work to tile execution. + // A valid custom plan may select away the global initializer through + // a narrower row selector domain; retain its ordinary clear below. + if contributions_by_current[id] + .first() + .is_some_and(|&(row_id, _)| { + let row = &plan.contributions()[row_id]; + if row.flags & DIRECT_CONTRIBUTION_FLAG_INITIALIZE_DESTINATION == 0 { + return false; + } + // Retain self-read clears even for a persisted zero factor: + // the low-level runtime allows callers to replace factors. + // Native output widths were checked during runtime loading; + // copy widths live in the row and must cover the full target. + row.parent0_component_base != row.destination_component_base + && row.parent1_component_base_or_sentinel != row.destination_component_base + && (row.flags & DIRECT_CONTRIBUTION_FLAG_CERTIFIED_REUSE == 0 + || row.parent0_momentum_form_id == u32::from(current.component_count)) + }) + { + continue; + } + clear_by_stage.entry(current.stage).or_default().push( + current.component_base..current.component_base + u32::from(current.component_count), + ); + } + let current_clear_stages = clear_by_stage + .into_iter() + .map(|(stage, mut ranges)| { + ranges.sort_unstable_by_key(|range| range.start); + let mut merged = Vec::::new(); + for range in ranges { + if let Some(last) = merged.last_mut() + && range.start <= last.component_base + last.component_count + { + last.component_count = (last.component_base + last.component_count) + .max(range.end) + - last.component_base; + continue; + } + merged.push(DirectCurrentClearRange { + component_base: range.start, + component_count: range.end - range.start, + }); + } + DirectStageClearRanges { + stage, + ranges: merged.into_boxed_slice(), + } + }) + .collect::>(); + Ok(DirectReplayDestinationProgram { + destination_ids: Arc::from(destination_ids), + row_groups: row_groups.into_boxed_slice(), + amplitude_clear_ranges: compact_ranges(&litude_marks, "destination selection")? + .into_boxed_slice(), + current_clear_stages: current_clear_stages.into_boxed_slice(), + }) +} + +fn selected_replay_row_groups( + plan: &DirectRecurrencePlan, + representative_sector_id: u32, +) -> RusticolResult> { + let mut selected = Vec::new(); + for descriptor in plan.row_groups() { + let start = usize::try_from(descriptor.row_start).map_err(|_| { + RusticolError::integrity("direct recurrence row-group start exceeds usize") + })?; + let count = descriptor.row_count as usize; + let mut offset = 0usize; + while offset < count { + let run_start = offset; + let selector_domain_id = + row_selector_domain_id(plan, descriptor.role, start + run_start)?; + while offset < count + && row_selector_domain_id(plan, descriptor.role, start + offset)? + == selector_domain_id + { + offset += 1; + } + // Preserve selector-domain boundaries even when adjacent domains + // are both active, so full-flow fanout runs retain their original + // authenticated partitions. + if !plan.selector_domain_contains(selector_domain_id, representative_sector_id)? { + continue; + } + let mut run = *descriptor; + run.row_start = descriptor + .row_start + .checked_add(run_start as u64) + .ok_or_else(|| { + RusticolError::integrity( + "direct recurrence selected row-group start overflows u64", + ) + })?; + run.row_count = u32::try_from(offset - run_start).map_err(|_| { + RusticolError::integrity("direct recurrence selected row-group count exceeds u32") + })?; + selected.push(run); + } + } + if selected.is_empty() { + return Err(RusticolError::integrity(format!( + "direct recurrence representative sector {representative_sector_id} has no executable rows" + ))); + } + Ok(selected) +} + +fn row_selector_domain_id( + plan: &DirectRecurrencePlan, + role: DirectExecutorRole, + row_index: usize, +) -> RusticolResult { + match role { + DirectExecutorRole::Source => plan + .sources() + .get(row_index) + .map(|row| row.selector_domain_id), + DirectExecutorRole::Contribution => plan + .contributions() + .get(row_index) + .map(|row| row.selector_domain_id), + DirectExecutorRole::Finalization => plan + .finalizations() + .get(row_index) + .map(|row| row.selector_domain_id), + DirectExecutorRole::Closure => plan + .closures() + .get(row_index) + .map(|row| row.selector_domain_id), + } + .ok_or_else(|| RusticolError::integrity("direct recurrence selected row is out of bounds")) +} + +fn selected_amplitude_clear_ranges( + plan: &DirectRecurrencePlan, + representative_sector_id: u32, +) -> RusticolResult>> { + let destination_count = usize::try_from(plan.amplitude_destination_count()) + .map_err(|_| invalid("amplitude destination count exceeds usize"))?; + let mut marked = zeroed_marks(destination_count, "selected amplitude clear map")?; + for destination in plan.amplitude_destinations() { + if plan + .selector_domain_contains(destination.selector_domain_id, representative_sector_id)? + { + mark_range( + &mut marked, + destination.id as usize, + 1, + "selected amplitude", + )?; + } + } + compact_ranges(&marked, "selected amplitude") +} + +fn amplitude_clear_ranges(plan: &DirectRecurrencePlan) -> RusticolResult>> { + let destination_count = usize::try_from(plan.amplitude_destination_count()) + .map_err(|_| invalid("amplitude destination count exceeds usize"))?; + let mut marked = zeroed_marks(destination_count, "amplitude clear map")?; + for row in plan.closures() { + mark_range( + &mut marked, + row.amplitude_destination_id as usize, + 1, + "amplitude", + )?; + } + compact_ranges(&marked, "amplitude") +} + +fn zeroed_marks(len: usize, label: &str) -> RusticolResult> { + let mut values = Vec::new(); + values.try_reserve_exact(len).map_err(|error| { + RusticolError::internal(format!( + "could not allocate direct recurrence {label}: {error}" + )) + })?; + values.resize(len, 0); + Ok(values) +} + +fn mark_range(marked: &mut [u8], start: usize, count: usize, label: &str) -> RusticolResult<()> { + let end = start + .checked_add(count) + .ok_or_else(|| RusticolError::integrity(format!("{label} clear range overflows usize")))?; + let range = marked.get_mut(start..end).ok_or_else(|| { + RusticolError::integrity(format!("{label} clear range exceeds its arena")) + })?; + range.fill(1); + Ok(()) +} + +fn compact_ranges(marked: &[u8], label: &str) -> RusticolResult>> { + let mut ranges = Vec::new(); + ranges.try_reserve_exact(marked.len()).map_err(|error| { + RusticolError::internal(format!( + "could not allocate direct recurrence {label} clear ranges: {error}" + )) + })?; + let mut index = 0; + while index < marked.len() { + if marked[index] == 0 { + index += 1; + continue; + } + let start = index; + while index < marked.len() && marked[index] != 0 { + index += 1; + } + ranges.push(start..index); + } + Ok(ranges) +} + +/// Interpret an already validated, generation-time momentum form. Only +/// independent phase-space points are reordered: each point keeps its original +/// initial addition and term order, including cancellation and signed zero. +/// There is no auxiliary input buffer or selection-dependent preparation. +#[inline] +fn fill_momentum_form_from_external( + form_planes: &mut [f64], + point_stride: usize, + active_points: usize, + external_four_momenta: &[f64], + source_count: usize, + mut terms: impl Iterator, +) { + let Some(first) = terms.next() else { + for plane in form_planes.chunks_exact_mut(point_stride) { + plane[..active_points].fill(0.0); + } + return; + }; + fill_external_momentum_term::( + form_planes, + point_stride, + active_points, + external_four_momenta, + source_count, + first, + ); + for term in terms { + fill_external_momentum_term::( + form_planes, + point_stride, + active_points, + external_four_momenta, + source_count, + term, + ); + } +} + +#[inline] +fn fill_external_momentum_term( + form_planes: &mut [f64], + point_stride: usize, + active_points: usize, + external_four_momenta: &[f64], + source_count: usize, + (external_slot, coefficient): (usize, f64), +) { + let external_point_stride = source_count * 4; + for (component, plane) in form_planes.chunks_exact_mut(point_stride).enumerate() { + if SOURCE_MAJOR { + let input_start = (external_slot * 4 + component) * active_points; + for (value, source) in plane[..active_points] + .iter_mut() + .zip(&external_four_momenta[input_start..input_start + active_points]) + { + if INITIALIZE { + *value = 0.0 + coefficient * source; + } else { + *value += coefficient * source; + } + } + } else { + let input_start = external_slot * 4 + component; + for (value, source) in plane[..active_points].iter_mut().zip( + external_four_momenta[input_start..] + .iter() + .step_by(external_point_stride), + ) { + if INITIALIZE { + *value = 0.0 + coefficient * source; + } else { + *value += coefficient * source; + } + } + } + } +} + +fn validate_split_values( + label: &str, + expected_len: usize, + values_re: &[f64], + values_im: &[f64], +) -> RusticolResult<()> { + if values_re.len() != expected_len || values_im.len() != expected_len { + return Err(invalid(format!( + "{label} split-complex lengths are {}, {}, expected {expected_len}", + values_re.len(), + values_im.len() + ))); + } + Ok(()) +} + +fn invalid(message: impl Into) -> RusticolError { + RusticolError::invalid_argument(format!( + "direct recurrence execution runtime: {}", + message.into() + )) +} + +#[cfg(test)] +#[path = "direct_runtime_tests.rs"] +mod tests; diff --git a/pyamplicol/rust/crates/rusticol-core/src/recurrence/direct_runtime_tests.rs b/pyamplicol/rust/crates/rusticol-core/src/recurrence/direct_runtime_tests.rs new file mode 100644 index 0000000000..5828acc188 --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/recurrence/direct_runtime_tests.rs @@ -0,0 +1,3178 @@ +// SPDX-License-Identifier: 0BSD + +use crate::recurrence::direct_backend::{ + DIRECT_STATUS_OK, DirectArenaView, DirectContributionExecutionMetadata, + DirectContributionExecutor, DirectContributionFanoutBatch, + DirectContributionFanoutExecutorHandle, DirectContributionFanoutProgram, + DirectExecutionCounters, DirectExecutorCatalog, DirectExecutorHandle, DirectFactorView, + DirectFinalizationExecutor, DirectInteractionBundleBatch, DirectInteractionExecutorContexts, + DirectInteractionIntrinsicKind, DirectInteractionOperands, DirectInteractionVectorParent, + DirectMomentumView, DirectParameterView, DirectSourceExecutor, DirectWorkspace, + begin_direct_current_observation, execute_direct_plan, + execute_direct_plan_unprofiled_with_fanout, take_direct_current_observation, +}; +use crate::recurrence::direct_plan::{ + DIRECT_CONTRIBUTION_FLAG_INITIALIZE_DESTINATION, DirectClosureRow, DirectContributionRow, + DirectFinalizationRow, DirectMomentumTerm, DirectRecurrencePlan, DirectReplayTargetDescriptor, + DirectSourceRow, DirectSourceStateAssignment, +}; +use crate::recurrence::direct_runtime::{ + DIRECT_RUNTIME_ARENA_ALIGNMENT, DirectRecurrenceExecutionRuntime, DirectRecurrenceTileOutput, + DirectRuntimeActivityCounters, replay_cache_split_complex_scalar_count, +}; +use crate::recurrence::exact::{ExactComplexRational, ExactRational}; +#[allow(unused_imports)] +use crate::recurrence::{ + CheckedTableRange, ClosureExecutionProofGroupV2, ClosureProofContributionV2, + ClosureProofMetadataV2, DIRECT_NONE_U32, DirectAmplitudeDestinationDescriptor, + DirectCurrentDescriptor, DirectDestinationOperation, DirectExecutorRole, + DirectMomentumFormDescriptor, DirectNodeKind, DirectResolvedHelicityDescriptor, + DirectRowGroupDescriptor, DirectSelectorDomainDescriptor, RecurrenceStrategy, SemanticDigest, + closure_component_factor_digest_v2, closure_selector_domain_digest_v2, +}; +use std::ffi::c_void; +use std::sync::atomic::{AtomicU64, Ordering}; + +const STATUS_BOUNDS: i32 = 2; + +fn direct_executor_handles() -> Vec { + vec![ + DirectExecutorHandle::Source { + call: fill_sources as DirectSourceExecutor, + context: std::ptr::null(), + }, + DirectExecutorHandle::Contribution { + call: accumulate_contributions as DirectContributionExecutor, + context: std::ptr::null(), + }, + DirectExecutorHandle::Finalization { + call: finalize_currents as DirectFinalizationExecutor, + context: std::ptr::null(), + }, + DirectExecutorHandle::Closure { + call: accumulate_closures, + context: std::ptr::null(), + }, + ] +} + +unsafe extern "C" fn fill_sources( + _context: *const c_void, + arena: DirectArenaView, + momenta: DirectMomentumView, + _parameters: DirectParameterView, + _factors: DirectFactorView, + rows: *const DirectSourceRow, + row_count: u32, + point_count: u32, +) -> i32 { + let rows = unsafe { std::slice::from_raw_parts(rows, row_count as usize) }; + for row in rows { + for point in 0..point_count as usize { + let source = row.momentum_form_id as usize + * momenta.lorentz_component_count as usize + * momenta.point_stride as usize + + point; + let destination = + row.destination_component_base as usize * arena.point_stride as usize + point; + if source >= momenta.scalar_len as usize + || destination >= arena.current_scalar_len as usize + { + return STATUS_BOUNDS; + } + unsafe { + *arena.current_re.add(destination) = *momenta.values.add(source); + *arena.current_im.add(destination) = 0.0; + } + } + } + DIRECT_STATUS_OK +} + +unsafe extern "C" fn accumulate_contributions( + _context: *const c_void, + arena: DirectArenaView, + _momenta: DirectMomentumView, + parameters: DirectParameterView, + factors: DirectFactorView, + rows: *const DirectContributionRow, + row_count: u32, + point_count: u32, +) -> i32 { + let rows = unsafe { std::slice::from_raw_parts(rows, row_count as usize) }; + for row in rows { + if row.exact_factor_id >= factors.value_count || parameters.value_count == 0 { + return STATUS_BOUNDS; + } + let factor_re = unsafe { *factors.values_re.add(row.exact_factor_id as usize) }; + let factor_im = unsafe { *factors.values_im.add(row.exact_factor_id as usize) }; + let parameter_re = unsafe { *parameters.values_re }; + let parameter_im = unsafe { *parameters.values_im }; + let scale_re = factor_re * parameter_re - factor_im * parameter_im; + let scale_im = factor_re * parameter_im + factor_im * parameter_re; + for point in 0..point_count as usize { + let source = row.parent0_component_base as usize * arena.point_stride as usize + point; + let destination = + row.destination_component_base as usize * arena.point_stride as usize + point; + if source >= arena.current_scalar_len as usize + || destination >= arena.current_scalar_len as usize + { + return STATUS_BOUNDS; + } + let source_re = unsafe { *arena.current_re.add(source) }; + let source_im = unsafe { *arena.current_im.add(source) }; + let value_re = source_re * scale_re - source_im * scale_im; + let value_im = source_re * scale_im + source_im * scale_re; + unsafe { + if row.flags & DIRECT_CONTRIBUTION_FLAG_INITIALIZE_DESTINATION != 0 { + *arena.current_re.add(destination) = value_re; + *arena.current_im.add(destination) = value_im; + } else { + *arena.current_re.add(destination) += value_re; + *arena.current_im.add(destination) += value_im; + } + } + } + } + DIRECT_STATUS_OK +} + +#[derive(Default)] +struct ContributionCallCensus { + calls: AtomicU64, + rows: AtomicU64, +} + +#[derive(Default)] +struct InteractionDispatchCensus { + calls: AtomicU64, +} + +unsafe extern "C" fn counted_accumulate_contributions( + context: *const c_void, + arena: DirectArenaView, + momenta: DirectMomentumView, + parameters: DirectParameterView, + factors: DirectFactorView, + rows: *const DirectContributionRow, + row_count: u32, + point_count: u32, +) -> i32 { + if context.is_null() { + return STATUS_BOUNDS; + } + let census = unsafe { &*context.cast::() }; + census.calls.fetch_add(1, Ordering::Relaxed); + census + .rows + .fetch_add(u64::from(row_count), Ordering::Relaxed); + unsafe { + accumulate_contributions( + std::ptr::null(), + arena, + momenta, + parameters, + factors, + rows, + row_count, + point_count, + ) + } +} + +unsafe fn unexpected_single_point_fanout( + _context: *const c_void, + _arena: DirectArenaView, + _momenta: DirectMomentumView, + _parameters: DirectParameterView, + _factors: DirectFactorView, + _batch: DirectContributionFanoutBatch<'_>, +) -> crate::RusticolResult<()> { + panic!("single-point contribution fanout bypassed the interaction action") +} + +unsafe fn inspect_nonidentity_interaction_dispatch( + contexts: DirectInteractionExecutorContexts, + _arena: DirectArenaView, + _momenta: DirectMomentumView, + _parameters: DirectParameterView, + _factors: DirectFactorView, + batch: DirectInteractionBundleBatch<'_>, +) -> crate::RusticolResult<()> { + assert!(!contexts.color.is_null()); + assert_eq!(contexts.color, contexts.antisymmetric_tensor_vector); + assert_eq!(batch.anchors.len(), 1); + assert_eq!(batch.outputs.len(), 2); + assert!(batch.atv_before_color); + + let anchor = batch.anchors[0]; + assert_eq!(anchor.output_start, 0); + assert_eq!(anchor.output_end, 2); + assert!(matches!( + anchor.operands, + DirectInteractionOperands::One(operand) + if operand.tensor_component_base == 1 + && operand.vector_parent == DirectInteractionVectorParent::Parent0 + )); + assert_eq!( + batch + .outputs + .iter() + .map(|output| output.destination_component_base) + .collect::>(), + [3, 2] + ); + assert!(batch.outputs.iter().all(|output| output.initialize)); + + let census = unsafe { &*contexts.color.cast::() }; + census.calls.fetch_add(1, Ordering::Relaxed); + Ok(()) +} + +unsafe extern "C" fn finalize_currents( + _context: *const c_void, + arena: DirectArenaView, + _momenta: DirectMomentumView, + _parameters: DirectParameterView, + factors: DirectFactorView, + rows: *const DirectFinalizationRow, + row_count: u32, + point_count: u32, +) -> i32 { + let rows = unsafe { std::slice::from_raw_parts(rows, row_count as usize) }; + for row in rows { + if row.exact_factor_id >= factors.value_count { + return STATUS_BOUNDS; + } + let factor = unsafe { *factors.values_re.add(row.exact_factor_id as usize) }; + for component in 0..usize::from(row.component_count) { + for point in 0..point_count as usize { + let destination = + (row.component_base as usize + component) * arena.point_stride as usize + point; + if destination >= arena.current_scalar_len as usize { + return STATUS_BOUNDS; + } + unsafe { + *arena.current_re.add(destination) *= factor; + *arena.current_im.add(destination) *= factor; + } + } + } + } + DIRECT_STATUS_OK +} + +unsafe extern "C" fn accumulate_closures( + _context: *const c_void, + arena: DirectArenaView, + _momenta: DirectMomentumView, + _parameters: DirectParameterView, + factors: DirectFactorView, + rows: *const DirectClosureRow, + row_count: u32, + point_count: u32, +) -> i32 { + let rows = unsafe { std::slice::from_raw_parts(rows, row_count as usize) }; + for row in rows { + if row.exact_factor_id >= factors.value_count { + return STATUS_BOUNDS; + } + let factor = unsafe { *factors.values_re.add(row.exact_factor_id as usize) }; + for point in 0..point_count as usize { + let source = row.parent0_component_base as usize * arena.point_stride as usize + point; + let destination = + row.amplitude_destination_id as usize * arena.point_stride as usize + point; + if source >= arena.current_scalar_len as usize + || destination >= arena.amplitude_scalar_len as usize + { + return STATUS_BOUNDS; + } + unsafe { + *arena.amplitude_re.add(destination) += *arena.current_re.add(source) * factor; + *arena.amplitude_im.add(destination) += *arena.current_im.add(source) * factor; + } + } + } + DIRECT_STATUS_OK +} + +fn rational(numerator: i128, denominator: i128) -> ExactComplexRational { + complex_rational(numerator, denominator, 0, 1) +} + +fn complex_rational( + real_numerator: i128, + real_denominator: i128, + imag_numerator: i128, + imag_denominator: i128, +) -> ExactComplexRational { + ExactComplexRational::new( + ExactRational::new(real_numerator, real_denominator).unwrap(), + ExactRational::new(imag_numerator, imag_denominator).unwrap(), + ) +} + +fn replace_single_closure_proof_factor( + parts: &mut crate::recurrence::DirectRecurrencePlanParts, + exact_factor: ExactComplexRational, + runtime_parent_ids: Option>>, +) { + let contribution = &parts.closure_proofs.contributions()[0]; + let contribution = ClosureProofContributionV2::new( + contribution.id(), + contribution.target_sector_id(), + contribution.target_destination_id(), + contribution.target_helicity_id(), + contribution.closure_template_id(), + contribution.closure_template_semantic_digest(), + contribution.quantum_flow_template_id(), + contribution.construction_parent_builder_ids().to_vec(), + runtime_parent_ids + .unwrap_or_else(|| contribution.construction_parent_runtime_ids().to_vec()), + contribution.construction_parent_semantic_digests().to_vec(), + contribution.construction_parent_color_digests().to_vec(), + contribution.construction_parent_permutation().to_vec(), + contribution.reconstruction_parent_permutation().to_vec(), + contribution.evaluator_parent_permutation().to_vec(), + contribution.color_witness_term_id(), + contribution.color_witness_proof_digest(), + contribution.three_line_certificate_id(), + contribution.pairing_certificate_ids().to_vec(), + contribution.reflection_certificate_id(), + exact_factor, + contribution.multiplicity(), + ) + .unwrap(); + let group = &parts.closure_proofs.groups()[0]; + let closure = &parts.closures[group.emitted_direct_closure_row_id().unwrap() as usize]; + let component_start = closure.component_factor_start as usize; + let component_end = component_start + closure.component_count as usize; + let component_factor_digest = + closure_component_factor_digest_v2(&parts.exact_factors[component_start..component_end]) + .unwrap(); + let group = ClosureExecutionProofGroupV2::new_with_candidate_selector_domain( + group.id(), + group.emitted_runtime_closure_term_id(), + group.emitted_direct_closure_row_id(), + group.contribution_range(), + exact_factor, + component_factor_digest, + group.candidate_selector_domain_digest(), + group.selector_domain_digest(), + ) + .unwrap(); + parts.closure_proofs = ClosureProofMetadataV2::new_with_three_line_certificates( + vec![contribution], + vec![group], + parts.closure_proofs.reflection_certificates().to_vec(), + parts + .closure_proofs + .three_line_traversal_certificates() + .to_vec(), + ) + .unwrap(); +} + +#[cfg(any())] +fn synthetic_plan_and_executors() -> (DirectRecurrencePlan, DirectExecutorCatalog) { + let plan = DirectRecurrencePlan::new(DirectRecurrencePlanParts { + strategy: RecurrenceStrategy::TopologyReplay, + semantic_digest: SemanticDigest::new([0x11; 32]).unwrap(), + prepared_pack_digest: SemanticDigest::new([0x22; 32]).unwrap(), + direct_template_catalog_digest: SemanticDigest::new([0x33; 32]).unwrap(), + point_tile_size: 4, + workspace_mib: 1, + current_arena_components: 2, + physical_sector_count: 2, + retained_helicity_count: 2, + amplitude_destination_count: 1, + parameter_value_count: 1, + external_source_count: 2, + state_template_count: 2, + source_template_or_dispatch_count: 1, + direct_executor_count: 4, + currents: vec![ + DirectCurrentDescriptor { + semantic_current_id: 0, + node_kind: DirectNodeKind::Source, + state_template_id: 0, + component_base: 0, + component_count: 1, + momentum_form_id: 0, + stage: 0, + selector_domain_id: 0, + first_use: 0, + last_use: 1, + source_row_or_sentinel: 0, + finalization_row_or_sentinel: DIRECT_NONE_U32, + }, + DirectCurrentDescriptor { + semantic_current_id: 1, + node_kind: DirectNodeKind::Current, + state_template_id: 1, + component_base: 1, + component_count: 1, + momentum_form_id: 0, + stage: 1, + selector_domain_id: 0, + first_use: 1, + last_use: 2, + source_row_or_sentinel: DIRECT_NONE_U32, + finalization_row_or_sentinel: 0, + }, + ], + sources: vec![DirectSourceRow { + source_slot: 0, + destination_component_base: 0, + momentum_form_id: 0, + source_template_or_dispatch_domain: 0, + spin_state_class: 1, + exact_factor_id: 3, + selector_domain_id: 0, + }], + contributions: vec![DirectContributionRow { + parent0_component_base: 0, + parent1_component_base_or_sentinel: DIRECT_NONE_U32, + parent0_momentum_form_id: 0, + parent1_momentum_form_id_or_sentinel: DIRECT_NONE_U32, + destination_component_base: 1, + exact_factor_id: 0, + selector_domain_id: 0, + flags: 0, + }], + finalizations: vec![DirectFinalizationRow { + component_base: 1, + component_count: 1, + momentum_form_id: 0, + exact_factor_id: 1, + selector_domain_id: 0, + flags: 0, + }], + closures: vec![DirectClosureRow { + parent0_component_base: 1, + parent1_component_base_or_sentinel: DIRECT_NONE_U32, + parent0_momentum_form_id: 0, + parent1_momentum_form_id_or_sentinel: DIRECT_NONE_U32, + amplitude_destination_id: 0, + exact_factor_id: 2, + component_factor_start: 0, + component_count: 1, + selector_domain_id: 0, + flags: 0, + }], + row_groups: vec![ + DirectRowGroupDescriptor { + stage: 0, + role: DirectExecutorRole::Source, + destination_operation: DirectDestinationOperation::Initialize, + direct_executor_id: 0, + row_start: 0, + row_count: 1, + }, + DirectRowGroupDescriptor { + stage: 1, + role: DirectExecutorRole::Contribution, + destination_operation: DirectDestinationOperation::Add, + direct_executor_id: 1, + row_start: 0, + row_count: 1, + }, + DirectRowGroupDescriptor { + stage: 1, + role: DirectExecutorRole::Finalization, + destination_operation: DirectDestinationOperation::FinalizeInPlace, + direct_executor_id: 2, + row_start: 0, + row_count: 1, + }, + DirectRowGroupDescriptor { + stage: 2, + role: DirectExecutorRole::Closure, + destination_operation: DirectDestinationOperation::ClosureAdd, + direct_executor_id: 3, + row_start: 0, + row_count: 1, + }, + ], + momentum_forms: vec![DirectMomentumFormDescriptor { + term_start: 0, + term_count: 2, + }], + momentum_terms: vec![ + DirectMomentumTerm { + source_slot: 0, + coefficient: 1, + }, + DirectMomentumTerm { + source_slot: 1, + coefficient: 2, + }, + ], + selector_domains: vec![DirectSelectorDomainDescriptor { + word_start: 0, + word_count: 1, + }], + selector_words: vec![1], + replay_targets: vec![ + DirectReplayTargetDescriptor { + public_flow_id: 0, + representative_id: 0, + source_permutation_start: 0, + source_permutation_count: 2, + helicity_map_start: 0, + helicity_map_count: 1, + phase_exact_factor_id: 3, + multiplicity: 1, + selector_domain_id: 0, + }, + DirectReplayTargetDescriptor { + public_flow_id: 1, + representative_id: 0, + source_permutation_start: 2, + source_permutation_count: 2, + helicity_map_start: 1, + helicity_map_count: 1, + phase_exact_factor_id: 4, + multiplicity: 2, + selector_domain_id: 0, + }, + ], + source_permutations: vec![0, 1, 1, 0], + replay_momentum_signs: vec![1, 1, 1, 1], + replay_helicity_map: vec![0, 0], + amplitude_destinations: vec![DirectAmplitudeDestinationDescriptor { + closure_row_start: 0, + id: 0, + target_sector_id: 0, + target_helicity_id_or_sentinel: 0, + closure_row_count: 1, + selector_domain_id: 0, + }], + resolved_helicities: vec![DirectResolvedHelicityDescriptor { + source_state_start: 0, + public_helicity_start: 0, + id: 0, + source_state_count: 2, + public_helicity_count: 2, + selector_domain_id: 0, + }], + source_state_assignments: vec![ + DirectSourceStateAssignment { + source_slot: 0, + state_index: 0, + }, + DirectSourceStateAssignment { + source_slot: 1, + state_index: 0, + }, + ], + public_helicities: vec![-1, 1], + exact_factors: vec![ + rational(2, 1), + rational(1, 2), + rational(-1, 1), + ExactComplexRational::ONE, + complex_rational(0, 1, 1, 1), + ], + }) + .unwrap(); + let executors = DirectExecutorCatalog::new( + &plan, + plan.direct_template_catalog_digest(), + direct_executor_handles(), + ) + .unwrap(); + (plan, executors) +} + +fn synthetic_plan_and_executors() -> (DirectRecurrencePlan, DirectExecutorCatalog) { + let mut parts = crate::recurrence::direct_plan::tests::valid_parts(); + parts.point_tile_size = 4; + parts.current_arena_components = 2; + parts.physical_sector_count = 2; + parts.retained_helicity_count = 2; + parts.external_source_count = 2; + parts.currents[0].component_count = 1; + parts.currents[1].component_base = 1; + parts.sources[0].destination_component_base = 0; + parts.contributions[0].destination_component_base = 1; + parts.contributions[0].exact_factor_id = 0; + parts.finalizations[0].component_base = 1; + parts.finalizations[0].exact_factor_id = 1; + parts.closures[0].parent0_component_base = 1; + parts.closures[0].exact_factor_id = 2; + parts.momentum_forms[0].term_count = 2; + parts.momentum_terms.push(DirectMomentumTerm { + source_slot: 1, + coefficient: 2, + }); + parts.replay_targets = vec![ + DirectReplayTargetDescriptor { + public_flow_id: 0, + representative_id: 0, + source_permutation_start: 0, + source_permutation_count: 2, + helicity_map_start: 0, + helicity_map_count: 1, + phase_exact_factor_id: 3, + multiplicity: 1, + selector_domain_id: 0, + }, + DirectReplayTargetDescriptor { + public_flow_id: 1, + representative_id: 0, + source_permutation_start: 2, + source_permutation_count: 2, + helicity_map_start: 1, + helicity_map_count: 1, + phase_exact_factor_id: 4, + multiplicity: 2, + selector_domain_id: 0, + }, + ]; + parts.source_permutations = vec![0, 1, 1, 0]; + parts.replay_momentum_signs = vec![1, 1, 1, 1]; + parts.replay_helicity_map = vec![0, 0]; + parts.resolved_helicities[0].source_state_count = 2; + parts.resolved_helicities[0].public_helicity_count = 2; + parts.source_state_assignments = vec![ + DirectSourceStateAssignment { + source_slot: 0, + state_index: 0, + }, + DirectSourceStateAssignment { + source_slot: 1, + state_index: 0, + }, + ]; + parts.public_helicities = vec![-1, 1]; + parts.exact_factors = vec![ + rational(2, 1), + rational(1, 2), + rational(-1, 1), + ExactComplexRational::ONE, + complex_rational(0, 1, 1, 1), + ]; + replace_single_closure_proof_factor(&mut parts, rational(-1, 1), None); + let plan = DirectRecurrencePlan::new(parts).unwrap(); + let executors = DirectExecutorCatalog::new( + &plan, + plan.direct_template_catalog_digest(), + direct_executor_handles(), + ) + .unwrap(); + (plan, executors) +} + +fn synthetic_runtime_with_lorentz( + lorentz_component_count: u16, +) -> DirectRecurrenceExecutionRuntime { + let (plan, executors) = synthetic_plan_and_executors(); + let mut runtime = + DirectRecurrenceExecutionRuntime::new(plan, executors, lorentz_component_count).unwrap(); + runtime.set_parameters(&[3.0], &[1.0]).unwrap(); + runtime +} + +fn synthetic_packed_runtime_with_lorentz( + lorentz_component_count: u16, +) -> DirectRecurrenceExecutionRuntime { + let (plan, executors) = synthetic_plan_and_executors(); + let mut runtime = DirectRecurrenceExecutionRuntime::new( + plan, + executors.mark_packed_singleton_capable(), + lorentz_component_count, + ) + .unwrap(); + runtime.set_parameters(&[3.0], &[1.0]).unwrap(); + runtime +} + +fn synthetic_runtime() -> DirectRecurrenceExecutionRuntime { + synthetic_runtime_with_lorentz(1) +} + +fn fanout_test_plan() -> DirectRecurrencePlan { + let mut parts = crate::recurrence::direct_plan::tests::valid_parts(); + parts.point_tile_size = 8; + parts.exact_factors = vec![ + ExactComplexRational::ONE, + rational(-1, 1), + complex_rational(0, 1, 1, 1), + complex_rational(0, 1, -1, 1), + rational(5, 7), + complex_rational(0, 1, 2, 5), + complex_rational(11, 13, -7, 17), + complex_rational(10_000, 1, 3, 1), + complex_rational(-10_000, 1, -3, 1), + ]; + let prototype = parts.contributions[0]; + parts.contributions = (0_u32..=8) + .flat_map(|exact_factor_id| [exact_factor_id; 2]) + .enumerate() + .map(|(row_index, exact_factor_id)| DirectContributionRow { + exact_factor_id, + flags: u32::from(row_index == 0) * DIRECT_CONTRIBUTION_FLAG_INITIALIZE_DESTINATION, + ..prototype + }) + .collect(); + parts + .row_groups + .iter_mut() + .find(|group| group.role == DirectExecutorRole::Contribution) + .unwrap() + .row_count = parts.contributions.len() as u32; + DirectRecurrencePlan::new(parts).unwrap() +} + +fn fanout_test_catalog( + plan: &DirectRecurrencePlan, + census: &ContributionCallCensus, + exact_factor_is_kernel_input: bool, +) -> DirectExecutorCatalog { + let mut handles = direct_executor_handles(); + handles[1] = DirectExecutorHandle::Contribution { + call: counted_accumulate_contributions, + context: census as *const ContributionCallCensus as *const c_void, + }; + DirectExecutorCatalog::new_sparse_with_metadata( + plan, + plan.direct_template_catalog_digest(), + handles.into_iter().map(Some).collect(), + vec![ + None, + Some( + DirectContributionExecutionMetadata::new(1, exact_factor_is_kernel_input).unwrap(), + ), + None, + None, + ], + ) + .unwrap() +} + +fn interaction_dispatch_test_plan() -> DirectRecurrencePlan { + let mut parts = crate::recurrence::direct_plan::tests::valid_parts(); + parts.strategy = RecurrenceStrategy::TopologyReplay; + parts.point_tile_size = 4; + parts.current_arena_components = 4; + parts.direct_executor_count = 5; + + let source = parts.currents[0]; + let current = parts.currents[1]; + parts.currents[0].component_count = 1; + parts.currents[1] = DirectCurrentDescriptor { + semantic_current_id: 2, + component_base: 2, + component_count: 1, + last_use: 1, + ..current + }; + parts.currents.insert( + 1, + DirectCurrentDescriptor { + semantic_current_id: 1, + component_base: 1, + component_count: 1, + source_row_or_sentinel: 1, + last_use: 2, + ..source + }, + ); + parts.currents.push(DirectCurrentDescriptor { + semantic_current_id: 3, + component_base: 3, + component_count: 1, + last_use: 1, + finalization_row_or_sentinel: DIRECT_NONE_U32, + ..current + }); + + let source_row = parts.sources[0]; + parts.sources.push(DirectSourceRow { + destination_component_base: 1, + ..source_row + }); + + let contribution = parts.contributions[0]; + parts.contributions = vec![ + DirectContributionRow { + parent0_component_base: 1, + parent1_component_base_or_sentinel: 0, + parent1_momentum_form_id_or_sentinel: 0, + destination_component_base: 2, + flags: 0, + ..contribution + }, + DirectContributionRow { + parent0_component_base: 1, + parent1_component_base_or_sentinel: 0, + parent1_momentum_form_id_or_sentinel: 0, + destination_component_base: 3, + flags: 0, + ..contribution + }, + DirectContributionRow { + parent0_component_base: 0, + parent1_component_base_or_sentinel: 1, + parent1_momentum_form_id_or_sentinel: 0, + destination_component_base: 3, + flags: 0, + ..contribution + }, + DirectContributionRow { + parent0_component_base: 0, + parent1_component_base_or_sentinel: 1, + parent1_momentum_form_id_or_sentinel: 0, + destination_component_base: 2, + flags: 0, + ..contribution + }, + ]; + parts.finalizations[0].component_base = 2; + parts.closures[0].parent0_component_base = 1; + parts.row_groups[0].row_count = 2; + parts.row_groups[1].row_count = 2; + parts.row_groups.insert( + 2, + DirectRowGroupDescriptor { + direct_executor_id: 4, + row_start: 2, + row_count: 2, + ..parts.row_groups[1] + }, + ); + DirectRecurrencePlan::new(parts).unwrap() +} + +fn interaction_dispatch_test_catalog( + plan: &DirectRecurrencePlan, + ordinary: &ContributionCallCensus, + interaction: &InteractionDispatchCensus, +) -> DirectExecutorCatalog { + let ordinary_context = ordinary as *const ContributionCallCensus as *const c_void; + let interaction_context = interaction as *const InteractionDispatchCensus as *const c_void; + let mut handles = direct_executor_handles(); + handles[1] = DirectExecutorHandle::Contribution { + call: counted_accumulate_contributions, + context: ordinary_context, + }; + handles.push(DirectExecutorHandle::Contribution { + call: counted_accumulate_contributions, + context: ordinary_context, + }); + let mut metadata = vec![None; 5]; + metadata[1] = Some(DirectContributionExecutionMetadata::new(1, false).unwrap()); + metadata[4] = Some(DirectContributionExecutionMetadata::new(1, false).unwrap()); + let mut fanout = vec![None; 5]; + fanout[1] = Some(DirectContributionFanoutExecutorHandle { + call: unexpected_single_point_fanout, + context: interaction_context, + destination_component_count: 1, + parent_component_counts: [1, 1], + requires_two_momenta: false, + required_parameter_count: 0, + interaction_kind: DirectInteractionIntrinsicKind::AntisymmetricTensorVector, + interaction_call: None, + }); + fanout[4] = Some(DirectContributionFanoutExecutorHandle { + call: unexpected_single_point_fanout, + context: interaction_context, + destination_component_count: 1, + parent_component_counts: [1, 1], + requires_two_momenta: false, + required_parameter_count: 0, + interaction_kind: DirectInteractionIntrinsicKind::ColorOrderedThreeVector, + interaction_call: Some(inspect_nonidentity_interaction_dispatch), + }); + DirectExecutorCatalog::new_sparse_with_metadata_and_fanout( + plan, + plan.direct_template_catalog_digest(), + handles.into_iter().map(Some).collect(), + metadata, + fanout, + ) + .unwrap() +} + +fn direct_baseline( + plan: &DirectRecurrencePlan, + executors: &DirectExecutorCatalog, + point_stride: u32, + point_count: u32, + momenta: &[f64], + parameter: (f64, f64), +) -> Vec<(f64, f64)> { + let mut current_re = + vec![0.0; plan.current_arena_components() as usize * point_stride as usize]; + let mut current_im = vec![0.0; current_re.len()]; + let mut amplitude_re = + vec![0.0; plan.amplitude_destination_count() as usize * point_stride as usize]; + let mut amplitude_im = vec![0.0; amplitude_re.len()]; + let factors_re = plan + .exact_factors() + .iter() + .map(|factor| factor.real().numerator() as f64 / factor.real().denominator() as f64) + .collect::>(); + let factors_im = plan + .exact_factors() + .iter() + .map(|factor| factor.imag().numerator() as f64 / factor.imag().denominator() as f64) + .collect::>(); + let mut workspace = DirectWorkspace { + current_re: &mut current_re, + current_im: &mut current_im, + amplitude_re: &mut amplitude_re, + amplitude_im: &mut amplitude_im, + momenta, + momentum_form_count: plan.momentum_forms().len() as u32, + lorentz_component_count: 1, + parameters_re: &[parameter.0], + parameters_im: &[parameter.1], + factors_re: &factors_re, + factors_im: &factors_im, + point_stride, + }; + let mut counters = DirectExecutionCounters::default(); + execute_direct_plan(plan, executors, &mut workspace, point_count, &mut counters).unwrap(); + assert_eq!( + counters.contribution_rows, + plan.contributions().len() as u64 + ); + amplitude_re[..point_count as usize] + .iter() + .copied() + .zip(amplitude_im[..point_count as usize].iter().copied()) + .collect() +} + +fn assert_scale_relative_complex_parity(baseline: &[(f64, f64)], candidate: &[(f64, f64)]) { + assert_eq!(baseline.len(), candidate.len()); + for (point, (&expected, &observed)) in baseline.iter().zip(candidate).enumerate() { + let error = (expected.0 - observed.0).hypot(expected.1 - observed.1); + let scale = expected + .0 + .hypot(expected.1) + .max(observed.0.hypot(observed.1)) + .max(1.0); + assert!( + error <= 1.0e-10 * scale, + "point {point} differs by {error:e} at scale {scale:e}: expected {expected:?}, observed {observed:?}" + ); + } +} + +#[test] +fn sparse_executor_catalog_resolves_arbitrary_referenced_ids_and_keeps_unused_holes() { + let mut parts = crate::recurrence::direct_plan::tests::valid_parts(); + parts.direct_executor_count = 8; + let selected_ids = [6_u32, 1, 7, 3]; + for (descriptor, executor_id) in parts.row_groups.iter_mut().zip(selected_ids) { + descriptor.direct_executor_id = executor_id; + } + let plan = DirectRecurrencePlan::new(parts).unwrap(); + let dense_handles = direct_executor_handles(); + let mut sparse_handles = vec![None; plan.direct_executor_count() as usize]; + for (handle, executor_id) in dense_handles.into_iter().zip(selected_ids) { + sparse_handles[executor_id as usize] = Some(handle); + } + + DirectExecutorCatalog::new_sparse( + &plan, + plan.direct_template_catalog_digest(), + sparse_handles.clone(), + ) + .unwrap(); + + sparse_handles[selected_ids[2] as usize] = None; + let error = DirectExecutorCatalog::new_sparse( + &plan, + plan.direct_template_catalog_digest(), + sparse_handles, + ) + .err() + .unwrap(); + assert_eq!(error.kind(), crate::RusticolErrorKind::Evaluation); + assert!(error.to_string().contains("executor 7 is not loaded")); +} + +#[test] +fn interaction_dispatch_uses_nonidentity_bijection_and_multipoint_falls_back() { + let plan = interaction_dispatch_test_plan(); + let ordinary = ContributionCallCensus::default(); + let interaction = InteractionDispatchCensus::default(); + let catalog = interaction_dispatch_test_catalog(&plan, &ordinary, &interaction); + let mut runtime = DirectRecurrenceExecutionRuntime::new(plan, catalog, 1).unwrap(); + runtime.set_parameters(&[1.0], &[0.0]).unwrap(); + + runtime.execute_tile(1).unwrap(); + assert_eq!(interaction.calls.load(Ordering::Relaxed), 1); + assert_eq!(ordinary.calls.load(Ordering::Relaxed), 0); + + runtime.execute_tile(2).unwrap(); + assert_eq!(interaction.calls.load(Ordering::Relaxed), 1); + assert_eq!(ordinary.calls.load(Ordering::Relaxed), 2); + assert_eq!(ordinary.rows.load(Ordering::Relaxed), 2); + + begin_direct_current_observation(runtime.plan(), None, 1).unwrap(); + runtime.execute_tile(1).unwrap(); + let observation = take_direct_current_observation().unwrap(); + assert!(!observation.currents.is_empty()); + assert_eq!(interaction.calls.load(Ordering::Relaxed), 1); + assert_eq!(ordinary.calls.load(Ordering::Relaxed), 4); + assert_eq!(ordinary.rows.load(Ordering::Relaxed), 6); +} + +#[test] +fn output_only_contribution_fanout_matches_direct_under_complex_cancellation() { + let plan = fanout_test_plan(); + let point_stride = 8; + let point_count = 7; + let momenta = [0.125, -0.875, 1.75, -3.5, 0.0625, 7.25, -11.0, 0.0]; + let parameter = (0.375, -1.125); + + let factor_norm_sum = plan + .exact_factors() + .iter() + .skip(1) + .map(|factor| { + let real = factor.real().numerator() as f64 / factor.real().denominator() as f64; + let imaginary = factor.imag().numerator() as f64 / factor.imag().denominator() as f64; + real.hypot(imaginary) + }) + .sum::(); + let factor_sum = plan + .exact_factors() + .iter() + .skip(1) + .fold((0.0_f64, 0.0_f64), |sum, factor| { + ( + sum.0 + factor.real().numerator() as f64 / factor.real().denominator() as f64, + sum.1 + factor.imag().numerator() as f64 / factor.imag().denominator() as f64, + ) + }); + assert!(factor_norm_sum > 1_000.0 * factor_sum.0.hypot(factor_sum.1)); + + let baseline_census = ContributionCallCensus::default(); + let baseline_catalog = fanout_test_catalog(&plan, &baseline_census, false); + let baseline = direct_baseline( + &plan, + &baseline_catalog, + point_stride, + point_count, + &momenta, + parameter, + ); + assert_eq!(baseline_census.calls.load(Ordering::Relaxed), 1); + assert_eq!(baseline_census.rows.load(Ordering::Relaxed), 18); + + let optimized_census = ContributionCallCensus::default(); + let optimized_catalog = fanout_test_catalog(&plan, &optimized_census, false); + let fanout = DirectContributionFanoutProgram::build(&plan, &optimized_catalog).unwrap(); + assert_eq!(fanout.row_counts(), (18, 1)); + assert_eq!(fanout.scatter_bundle_counts(), (18, 9)); + assert_eq!(fanout.scratch_component_count(), 1); + assert!(fanout.needs_unit_factor()); + let mut runtime = + DirectRecurrenceExecutionRuntime::new(plan.clone(), optimized_catalog, 1).unwrap(); + runtime + .set_parameters(&[parameter.0], &[parameter.1]) + .unwrap(); + runtime + .momentum_plane_mut(0, 0) + .unwrap() + .unwrap() + .copy_from_slice(&momenta); + assert_eq!(runtime.factors_mut().0.len(), plan.exact_factors().len()); + assert_eq!( + runtime.current_arenas().0.len(), + plan.current_arena_components() as usize * runtime.point_stride() as usize + ); + let storage = storage_identity(&mut runtime); + let output = runtime.execute_tile(point_count).unwrap(); + let candidate = output + .destination_re(0) + .unwrap() + .iter() + .copied() + .zip(output.destination_im(0).unwrap().iter().copied()) + .collect::>(); + assert_scale_relative_complex_parity(&baseline, &candidate); + assert_eq!(optimized_census.calls.load(Ordering::Relaxed), 1); + assert_eq!(optimized_census.rows.load(Ordering::Relaxed), 1); + + runtime.execute_tile(point_count).unwrap(); + assert_eq!(storage_identity(&mut runtime), storage); + assert_eq!(optimized_census.calls.load(Ordering::Relaxed), 2); + assert_eq!(optimized_census.rows.load(Ordering::Relaxed), 2); + + let tail_count = 5; + let tail_baseline = direct_baseline( + &plan, + &baseline_catalog, + point_stride, + tail_count, + &momenta, + parameter, + ); + let tail_output = runtime.execute_tile(tail_count).unwrap(); + let tail_candidate = tail_output + .destination_re(0) + .unwrap() + .iter() + .copied() + .zip(tail_output.destination_im(0).unwrap().iter().copied()) + .collect::>(); + assert_scale_relative_complex_parity(&tail_baseline, &tail_candidate); + assert_eq!(storage_identity(&mut runtime), storage); +} + +#[test] +fn full_fanout_execution_overwrites_poisoned_destination_without_stage_clear() { + let plan = fanout_test_plan(); + let point_stride = 8_u32; + let point_count = 7_u32; + let momenta = [0.125, -0.875, 1.75, -3.5, 0.0625, 7.25, -11.0, 0.0]; + let parameter = (0.375, -1.125); + let census = ContributionCallCensus::default(); + let catalog = fanout_test_catalog(&plan, &census, false); + let fanout = DirectContributionFanoutProgram::build(&plan, &catalog).unwrap(); + let current_plane_count = plan.current_arena_components() + fanout.scratch_component_count(); + let mut current_re = vec![f64::NAN; current_plane_count as usize * point_stride as usize]; + let mut current_im = vec![f64::NAN; current_re.len()]; + let mut amplitude_re = vec![0.0; point_stride as usize]; + let mut amplitude_im = vec![0.0; point_stride as usize]; + let mut factors_re = plan + .exact_factors() + .iter() + .map(|factor| factor.real().numerator() as f64 / factor.real().denominator() as f64) + .collect::>(); + let mut factors_im = plan + .exact_factors() + .iter() + .map(|factor| factor.imag().numerator() as f64 / factor.imag().denominator() as f64) + .collect::>(); + factors_re.push(1.0); + factors_im.push(0.0); + + let execute = |current_re: &mut [f64], + current_im: &mut [f64], + amplitude_re: &mut [f64], + amplitude_im: &mut [f64]| { + let mut workspace = DirectWorkspace { + current_re, + current_im, + amplitude_re, + amplitude_im, + momenta: &momenta, + momentum_form_count: plan.momentum_forms().len() as u32, + lorentz_component_count: 1, + parameters_re: &[parameter.0], + parameters_im: &[parameter.1], + factors_re: &factors_re, + factors_im: &factors_im, + point_stride, + }; + execute_direct_plan_unprofiled_with_fanout( + &plan, + &catalog, + &fanout, + &mut workspace, + point_count, + ) + .unwrap(); + }; + + execute( + &mut current_re, + &mut current_im, + &mut amplitude_re, + &mut amplitude_im, + ); + let first = amplitude_re[..point_count as usize] + .iter() + .copied() + .zip(amplitude_im[..point_count as usize].iter().copied()) + .collect::>(); + current_re.fill(f64::NAN); + current_im.fill(f64::NAN); + amplitude_re.fill(0.0); + amplitude_im.fill(0.0); + execute( + &mut current_re, + &mut current_im, + &mut amplitude_re, + &mut amplitude_im, + ); + let second = amplitude_re[..point_count as usize] + .iter() + .copied() + .zip(amplitude_im[..point_count as usize].iter().copied()) + .collect::>(); + + assert!( + second + .iter() + .all(|(real, imaginary)| real.is_finite() && imaginary.is_finite()) + ); + assert_scale_relative_complex_parity(&first, &second); +} + +#[test] +fn factor_consuming_contribution_fanout_is_fail_closed() { + let plan = fanout_test_plan(); + let census = ContributionCallCensus::default(); + let catalog = fanout_test_catalog(&plan, &census, true); + let fanout = DirectContributionFanoutProgram::build(&plan, &catalog).unwrap(); + assert_eq!(fanout.row_counts(), (18, 9)); + assert_eq!(fanout.scatter_bundle_counts(), (18, 9)); + assert_eq!(fanout.scratch_component_count(), 9); + assert!(!fanout.needs_unit_factor()); + + let mut bad_metadata = vec![None; plan.direct_executor_count() as usize]; + bad_metadata[1] = Some(DirectContributionExecutionMetadata::new(2, false).unwrap()); + let error = DirectExecutorCatalog::new_sparse_with_metadata( + &plan, + plan.direct_template_catalog_digest(), + direct_executor_handles().into_iter().map(Some).collect(), + bad_metadata, + ) + .and_then(|catalog| DirectContributionFanoutProgram::build(&plan, &catalog)) + .err() + .unwrap(); + assert!( + error + .to_string() + .contains("declares 2 destination components") + ); +} + +#[test] +fn direct_plan_rejects_a_row_group_executor_outside_the_catalog_domain() { + let mut parts = crate::recurrence::direct_plan::tests::valid_parts(); + parts.row_groups[0].direct_executor_id = parts.direct_executor_count; + let error = DirectRecurrencePlan::new(parts).err().unwrap(); + assert_eq!(error.kind(), crate::RusticolErrorKind::InvalidArgument); +} + +#[test] +fn low_footprint_runtime_retains_the_requested_point_tile() { + let (plan, executors) = synthetic_plan_and_executors(); + assert_eq!(plan.point_tile_size(), 4); + let mut runtime = DirectRecurrenceExecutionRuntime::new(plan, executors, 1).unwrap(); + assert_eq!(runtime.point_tile_size(), 4); + assert_eq!(runtime.point_stride(), 8); + assert_eq!(runtime.momenta_mut().unwrap().len(), 4); + assert_eq!(runtime.physical_momenta_mut().unwrap().len(), 8); +} + +#[test] +fn packed_singleton_is_exact_and_scalar_batch_scalar_keeps_tiled_tail() { + let mut aligned = synthetic_runtime_with_lorentz(4); + let mut dual = synthetic_packed_runtime_with_lorentz(4); + assert!(dual.packed_singleton_active); + assert!(dual.alternate_point_storage.is_none()); + assert_eq!(dual.point_stride(), 1); + let packed_allocation_counters = dual.allocation_counters(); + let aligned_selector = aligned.prepare_replay_selector(0).unwrap(); + let dual_selector = dual.prepare_replay_selector(0).unwrap(); + let one = &external_two_point_momenta()[..8]; + + let aligned_scalar = aligned + .execute_replay_tile_from_external(&aligned_selector, 1, one) + .unwrap(); + assert_eq!(aligned_scalar.point_stride(), 8); + let aligned_scalar = ( + aligned_scalar.destination_re(0).unwrap().to_vec(), + aligned_scalar.destination_im(0).unwrap().to_vec(), + ); + let dual_scalar = dual + .execute_replay_tile_from_external(&dual_selector, 1, one) + .unwrap(); + assert_eq!(dual_scalar.point_stride(), 1); + assert_eq!( + ( + dual_scalar.destination_re(0).unwrap(), + dual_scalar.destination_im(0).unwrap(), + ), + (aligned_scalar.0.as_slice(), aligned_scalar.1.as_slice()) + ); + assert!(dual.alternate_point_storage.is_none()); + assert_eq!(dual.allocation_counters(), packed_allocation_counters); + + let four = [external_two_point_momenta(), external_two_point_momenta()].concat(); + let aligned_batch = aligned + .execute_replay_tile_from_external(&aligned_selector, 4, &four) + .unwrap(); + let aligned_batch = ( + aligned_batch.destination_re(0).unwrap().to_vec(), + aligned_batch.destination_im(0).unwrap().to_vec(), + ); + let dual_batch = dual + .execute_replay_tile_from_external(&dual_selector, 4, &four) + .unwrap(); + assert_eq!(dual_batch.point_stride(), 8); + let dual_batch = ( + dual_batch.destination_re(0).unwrap().to_vec(), + dual_batch.destination_im(0).unwrap().to_vec(), + dual_batch.storage_re()[2..4].to_vec(), + ); + assert!(!dual.packed_singleton_active); + assert!(dual.alternate_point_storage.is_some()); + assert_eq!( + dual.allocation_counters().allocation_requests, + packed_allocation_counters.allocation_requests + 5 + ); + assert_eq!(dual_batch.0, aligned_batch.0); + assert_eq!(dual_batch.1, aligned_batch.1); + let retained_tail = dual_batch.2; + + let dual_tail = dual + .execute_replay_tile_from_external(&dual_selector, 2, &external_two_point_momenta()) + .unwrap(); + assert_eq!(dual_tail.point_stride(), 8); + assert_eq!(&dual_tail.storage_re()[2..4], retained_tail.as_slice()); + + let second_one = &external_two_point_momenta()[8..]; + let aligned_scalar = aligned + .execute_replay_tile_from_external(&aligned_selector, 1, second_one) + .unwrap(); + let aligned_scalar = ( + aligned_scalar.destination_re(0).unwrap().to_vec(), + aligned_scalar.destination_im(0).unwrap().to_vec(), + ); + let dual_scalar = dual + .execute_replay_tile_from_external(&dual_selector, 1, second_one) + .unwrap(); + assert_eq!(dual_scalar.point_stride(), 1); + assert_eq!( + dual_scalar.destination_re(0).unwrap(), + aligned_scalar.0.as_slice() + ); + assert_eq!( + dual_scalar.destination_im(0).unwrap(), + aligned_scalar.1.as_slice() + ); + + let tiled_stride = aligned.point_stride() as usize; + let expected_point_zero = { + let momenta = aligned.physical_momenta_mut().unwrap(); + (0..4) + .map(|plane| momenta[plane * tiled_stride]) + .collect::>() + }; + { + let momenta = dual.physical_momenta_mut().unwrap(); + for plane in 0..4 { + assert_eq!(momenta[plane * tiled_stride], expected_point_zero[plane]); + let point_one = 100.0 + plane as f64; + momenta[plane * tiled_stride + 1] = point_one; + } + } + assert!(dual.outputs().is_none()); + { + let momenta = aligned.physical_momenta_mut().unwrap(); + for plane in 0..4 { + momenta[plane * tiled_stride + 1] = 100.0 + plane as f64; + } + } + assert_eq!(dual.point_stride() as usize, tiled_stride); + let aligned_tiled = aligned.execute_tile(2).unwrap(); + let aligned_tiled = ( + aligned_tiled.destination_re(0).unwrap().to_vec(), + aligned_tiled.destination_im(0).unwrap().to_vec(), + ); + let dual_tiled = dual.execute_tile(2).unwrap(); + assert_eq!( + dual_tiled.destination_re(0).unwrap(), + aligned_tiled.0.as_slice() + ); + assert_eq!( + dual_tiled.destination_im(0).unwrap(), + aligned_tiled.1.as_slice() + ); +} + +#[test] +fn packed_singleton_legacy_access_promotes_once_and_preserves_point_zero() { + let mut runtime = synthetic_packed_runtime_with_lorentz(4); + let selector = runtime.prepare_replay_selector(0).unwrap(); + let one = &external_two_point_momenta()[..8]; + runtime + .execute_replay_tile_from_external(&selector, 1, one) + .unwrap(); + let packed_allocation_counters = runtime.allocation_counters(); + assert!(runtime.outputs().is_some()); + assert!(runtime.momentum_plane_mut(u32::MAX, 0).unwrap().is_none()); + assert!(runtime.momentum_plane_mut(0, u16::MAX).unwrap().is_none()); + assert!(runtime.packed_singleton_active); + assert!(runtime.alternate_point_storage.is_none()); + assert_eq!(runtime.allocation_counters(), packed_allocation_counters); + + let tiled_stride = (runtime.point_tile_size().div_ceil(8) * 8) as usize; + let momenta = runtime.physical_momenta_mut().unwrap(); + for (component, expected) in [9.0, 90.0, 900.0, 9000.0].into_iter().enumerate() { + assert_eq!(momenta[component * tiled_stride], expected); + } + assert_eq!(runtime.point_stride() as usize, tiled_stride); + assert!(!runtime.packed_singleton_active); + assert!(runtime.outputs().is_none()); + assert_eq!( + runtime.allocation_counters().allocation_requests, + packed_allocation_counters.allocation_requests + 5 + ); + + let promoted_allocation_counters = runtime.allocation_counters(); + runtime.physical_momenta_mut().unwrap(); + assert_eq!(runtime.allocation_counters(), promoted_allocation_counters); +} + +#[test] +fn replay_cache_footprint_uses_authenticated_active_selector_work() { + let mut parts = crate::recurrence::direct_plan::tests::valid_parts(); + parts.current_arena_components = 128; + let plan = DirectRecurrencePlan::new(parts).unwrap(); + let persisted_split_scalars = + 2 * (plan.current_arena_components() + plan.amplitude_destination_count()) as usize; + + assert_eq!(persisted_split_scalars, 258); + assert_eq!( + replay_cache_split_complex_scalar_count(&plan, persisted_split_scalars, 0).unwrap(), + 8 + ); +} + +#[test] +fn legacy_flat_momentum_access_round_trips_every_padded_plane_in_place() { + let (plan, executors) = synthetic_plan_and_executors(); + let mut runtime = DirectRecurrenceExecutionRuntime::new(plan, executors, 4).unwrap(); + let tile_capacity = runtime.point_tile_size() as usize; + let point_stride = runtime.point_stride() as usize; + assert_eq!(tile_capacity, 4); + assert_eq!(point_stride, 8); + + let compact = runtime.momenta_mut().unwrap(); + for plane in 0..4 { + for point in 0..tile_capacity { + compact[plane * tile_capacity + point] = (plane * 10 + point) as f64; + } + } + let physical = runtime.physical_momenta_mut().unwrap(); + for plane in 0..4 { + for point in 0..tile_capacity { + assert_eq!( + physical[plane * point_stride + point], + (plane * 10 + point) as f64 + ); + } + } + + let compact = runtime.momenta_mut().unwrap(); + for plane in 0..4 { + for point in 0..tile_capacity { + assert_eq!( + compact[plane * tile_capacity + point], + (plane * 10 + point) as f64 + ); + } + } +} + +#[test] +fn high_footprint_runtime_uses_a_power_of_two_cache_tile() { + let mut parts = crate::recurrence::direct_plan::tests::valid_parts(); + parts.strategy = RecurrenceStrategy::ContractedColorUnion; + parts.point_tile_size = 1024; + parts.workspace_mib = 256; + parts.current_arena_components = 4_000; + let plan = DirectRecurrencePlan::new(parts).unwrap(); + assert_eq!(plan.point_tile_size(), 1024); + let executors = DirectExecutorCatalog::new( + &plan, + plan.direct_template_catalog_digest(), + direct_executor_handles(), + ) + .unwrap(); + let runtime = DirectRecurrenceExecutionRuntime::new(plan, executors, 4).unwrap(); + assert_eq!(runtime.point_tile_size(), 64); +} + +#[test] +fn cache_tile_fills_one_aligned_pitch_without_overriding_request_or_workspace() { + for (requested_points, workspace_mib, expected_points) in [ + (1, 256, 1), + (2, 256, 2), + (4, 256, 4), + (8, 256, 8), + (1024, 256, 8), + (1024, 17, 8), + ] { + let mut parts = crate::recurrence::direct_plan::tests::valid_parts(); + parts.strategy = RecurrenceStrategy::ContractedColorUnion; + parts.point_tile_size = requested_points; + parts.workspace_mib = workspace_mib; + parts.current_arena_components = 131_071; + // Including the one amplitude, the split-complex footprint is exactly + // 2 MiB per point. The old 4 MiB cache target admitted only two lanes, + // although every tiled arena already allocated an eight-lane pitch. + assert_eq!(parts.amplitude_destination_count, 1); + let plan = DirectRecurrencePlan::new(parts).unwrap(); + let executors = DirectExecutorCatalog::new( + &plan, + plan.direct_template_catalog_digest(), + direct_executor_handles(), + ) + .unwrap(); + let runtime = DirectRecurrenceExecutionRuntime::new(plan, executors, 4).unwrap(); + assert_eq!(runtime.point_tile_size(), expected_points); + assert_eq!(runtime.point_stride(), 8); + let (current_re, current_im) = runtime.current_arenas(); + let (amplitude_re, amplitude_im) = runtime.amplitude_arenas(); + assert_eq!(current_re.len(), 131_071 * 8); + assert_eq!(current_im.len(), current_re.len()); + assert_eq!(amplitude_re.len(), 8); + assert_eq!(amplitude_im.len(), amplitude_re.len()); + } +} + +#[test] +fn cache_target_never_rejects_a_point_that_fits_the_workspace_limit() { + let mut parts = crate::recurrence::direct_plan::tests::valid_parts(); + parts.point_tile_size = 1024; + parts.workspace_mib = 64; + parts.current_arena_components = 262_144; + let plan = DirectRecurrencePlan::new(parts).unwrap(); + let executors = DirectExecutorCatalog::new( + &plan, + plan.direct_template_catalog_digest(), + direct_executor_handles(), + ) + .unwrap(); + let runtime = DirectRecurrenceExecutionRuntime::new(plan, executors, 4).unwrap(); + assert_eq!(runtime.point_tile_size(), 8); + assert_eq!(runtime.point_stride(), 8); +} + +#[test] +fn workspace_target_allows_the_minimum_aligned_physical_pitch() { + let mut parts = crate::recurrence::direct_plan::tests::valid_parts(); + parts.point_tile_size = 1024; + parts.workspace_mib = 8; + parts.current_arena_components = 262_144; + let plan = DirectRecurrencePlan::new(parts).unwrap(); + let executors = DirectExecutorCatalog::new( + &plan, + plan.direct_template_catalog_digest(), + direct_executor_handles(), + ) + .unwrap(); + let runtime = DirectRecurrenceExecutionRuntime::new(plan, executors, 4).unwrap(); + assert_eq!(runtime.point_tile_size(), 8); + assert_eq!(runtime.point_stride(), 8); +} + +#[test] +fn packed_singleton_can_switch_to_tiles_larger_than_the_workspace_target() { + let mut parts = crate::recurrence::direct_plan::tests::valid_parts(); + parts.point_tile_size = 1024; + parts.workspace_mib = 8; + parts.current_arena_components = 262_144; + let plan = DirectRecurrencePlan::new(parts).unwrap(); + let ordinary_executors = DirectExecutorCatalog::new( + &plan, + plan.direct_template_catalog_digest(), + direct_executor_handles(), + ) + .unwrap(); + let ordinary_runtime = + DirectRecurrenceExecutionRuntime::new(plan.clone(), ordinary_executors, 4).unwrap(); + let packed_executors = DirectExecutorCatalog::new( + &plan, + plan.direct_template_catalog_digest(), + direct_executor_handles(), + ) + .unwrap() + .mark_packed_singleton_capable(); + let mut runtime = DirectRecurrenceExecutionRuntime::new(plan, packed_executors, 4).unwrap(); + assert_eq!( + runtime.point_tile_size(), + ordinary_runtime.point_tile_size() + ); + assert_eq!(runtime.point_stride(), 1); + assert!(runtime.packed_singleton_active); + assert!(runtime.alternate_point_storage.is_none()); + + runtime.set_parameters(&[3.0], &[1.0]).unwrap(); + let output = runtime.execute_tile(1).unwrap(); + assert_eq!(output.destination_re(0).unwrap(), &[0.0]); + assert_eq!(output.destination_im(0).unwrap(), &[0.0]); + assert!(runtime.outputs().is_some()); + + runtime.execute_tile(2).unwrap(); + assert!(runtime.outputs().is_some()); + assert!(!runtime.packed_singleton_active); + assert!(runtime.alternate_point_storage.is_some()); + runtime.physical_momenta_mut().unwrap(); + + runtime.execute_tile(1).unwrap(); +} + +fn storage_identity(runtime: &mut DirectRecurrenceExecutionRuntime) -> ([usize; 9], [usize; 9]) { + let ((current_re_pointer, current_re_len), (current_im_pointer, current_im_len)) = { + let (current_re, current_im) = runtime.current_arenas(); + ( + (current_re.as_ptr() as usize, current_re.len()), + (current_im.as_ptr() as usize, current_im.len()), + ) + }; + let ((amplitude_re_pointer, amplitude_re_len), (amplitude_im_pointer, amplitude_im_len)) = { + let (amplitude_re, amplitude_im) = runtime.amplitude_arenas(); + ( + (amplitude_re.as_ptr() as usize, amplitude_re.len()), + (amplitude_im.as_ptr() as usize, amplitude_im.len()), + ) + }; + let (momenta_pointer, momenta_len) = { + let values = runtime.momenta_mut().unwrap(); + (values.as_ptr() as usize, values.len()) + }; + let ((parameters_re_pointer, parameters_re_len), (parameters_im_pointer, parameters_im_len)) = { + let (parameters_re, parameters_im) = runtime.parameters_mut(); + ( + (parameters_re.as_ptr() as usize, parameters_re.len()), + (parameters_im.as_ptr() as usize, parameters_im.len()), + ) + }; + let ((factors_re_pointer, factors_re_len), (factors_im_pointer, factors_im_len)) = { + let (factors_re, factors_im) = runtime.factors_mut(); + ( + (factors_re.as_ptr() as usize, factors_re.len()), + (factors_im.as_ptr() as usize, factors_im.len()), + ) + }; + ( + [ + current_re_pointer, + current_im_pointer, + amplitude_re_pointer, + amplitude_im_pointer, + momenta_pointer, + parameters_re_pointer, + parameters_im_pointer, + factors_re_pointer, + factors_im_pointer, + ], + [ + current_re_len, + current_im_len, + amplitude_re_len, + amplitude_im_len, + momenta_len, + parameters_re_len, + parameters_im_len, + factors_re_len, + factors_im_len, + ], + ) +} + +fn external_two_point_momenta() -> [f64; 16] { + [ + 1.0, 10.0, 100.0, 1000.0, 4.0, 40.0, 400.0, 4000.0, 2.0, 20.0, 200.0, 2000.0, 5.0, 50.0, + 500.0, 5000.0, + ] +} + +fn external_three_point_momenta() -> [f64; 24] { + [ + 1.0, 10.0, 100.0, 1000.0, 4.0, 40.0, 400.0, 4000.0, 2.0, 20.0, 200.0, 2000.0, 5.0, 50.0, + 500.0, 5000.0, 7.0, 70.0, 700.0, 7000.0, 8.0, 80.0, 800.0, 8000.0, + ] +} + +#[test] +fn warmed_tiles_reuse_stable_aligned_storage_and_return_correct_borrowed_outputs() { + let mut runtime = synthetic_runtime(); + let identity = storage_identity(&mut runtime); + assert!( + identity + .0 + .iter() + .all(|pointer| pointer.is_multiple_of(DIRECT_RUNTIME_ARENA_ALIGNMENT)) + ); + + for _ in 0..8 { + runtime + .momentum_plane_mut(0, 0) + .unwrap() + .unwrap() + .copy_from_slice(&[1.0, 2.0, 3.0, 4.0]); + let output = runtime.execute_tile(4).unwrap(); + assert_eq!( + output.destination_re(0).unwrap(), + &[-3.0, -6.0, -9.0, -12.0] + ); + assert_eq!(output.destination_im(0).unwrap(), &[-1.0, -2.0, -3.0, -4.0]); + assert_eq!(output.destination_re(1), None); + assert_eq!(storage_identity(&mut runtime), identity); + } + + let (current_re, current_im) = runtime.current_arenas(); + assert_eq!(¤t_re[0..4], &[1.0, 2.0, 3.0, 4.0]); + let point_stride = runtime.point_stride() as usize; + assert_eq!( + ¤t_re[point_stride..point_stride + 4], + &[3.0, 6.0, 9.0, 12.0] + ); + assert_eq!(¤t_im[0..4], &[0.0; 4]); + assert_eq!( + ¤t_im[point_stride..point_stride + 4], + &[1.0, 2.0, 3.0, 4.0] + ); + let counters = runtime.counters(); + assert_eq!(counters.source_calls, 8); + assert_eq!(counters.source_rows, 8); + assert_eq!(counters.contribution_calls, 8); + assert_eq!(counters.contribution_rows, 8); + assert_eq!(counters.finalization_calls, 8); + assert_eq!(counters.finalization_rows, 8); + assert_eq!(counters.closure_calls, 8); + assert_eq!(counters.closure_rows, 8); + assert_eq!(counters.packed_input_bytes, 0); + assert_eq!(counters.packed_output_bytes, 0); + assert_eq!(counters.scatter_bytes, 0); + let allocation_counters = runtime.allocation_counters(); + assert_eq!(allocation_counters.allocation_requests, 9); + assert!(allocation_counters.requested_bytes != 0); + let traffic = runtime.traffic_counters(); + assert_eq!(traffic.calls, 32); + assert_eq!(traffic.rows, 32); + assert_eq!(traffic.points, 128); + traffic.validate_direct().unwrap(); +} + +#[test] +fn elided_identity_finalizer_remains_correct_across_repeated_evaluations() { + let mut parts = crate::recurrence::direct_plan::tests::valid_parts(); + parts.point_tile_size = 2; + parts.currents[1].finalization_row_or_sentinel = DIRECT_NONE_U32; + parts.finalizations.clear(); + parts + .row_groups + .retain(|group| group.role != DirectExecutorRole::Finalization); + let plan = DirectRecurrencePlan::new(parts).unwrap(); + let executors = DirectExecutorCatalog::new( + &plan, + plan.direct_template_catalog_digest(), + direct_executor_handles(), + ) + .unwrap(); + let mut runtime = DirectRecurrenceExecutionRuntime::new(plan, executors, 1).unwrap(); + runtime.set_parameters(&[1.0], &[0.0]).unwrap(); + + for values in [[2.0, 3.0], [5.0, 7.0], [11.0, 13.0]] { + runtime + .momentum_plane_mut(0, 0) + .unwrap() + .unwrap() + .copy_from_slice(&values); + let output = runtime.execute_tile(2).unwrap(); + assert_eq!(output.destination_re(0).unwrap(), values.as_slice()); + assert_eq!(output.destination_im(0).unwrap(), &[0.0, 0.0]); + } + assert_eq!(runtime.counters().finalization_calls, 0); + assert_eq!(runtime.counters().finalization_rows, 0); +} + +#[test] +fn a_reused_source_slot_is_cleared_before_the_later_current_accumulates() { + let mut parts = crate::recurrence::direct_plan::tests::valid_parts(); + parts.point_tile_size = 1; + parts.current_arena_components = 2; + parts.currents[0].component_count = 1; + parts.currents[0].last_use = 1; + parts.currents[1].component_base = 1; + parts.currents[1].first_use = 1; + parts.currents[1].last_use = 2; + parts.currents.push(DirectCurrentDescriptor { + semantic_current_id: 2, + node_kind: DirectNodeKind::Current, + state_template_id: 1, + component_base: 0, + component_count: 1, + momentum_form_id: 0, + stage: 2, + selector_domain_id: 0, + first_use: 2, + last_use: 3, + source_row_or_sentinel: DIRECT_NONE_U32, + finalization_row_or_sentinel: 1, + }); + parts.contributions[0].destination_component_base = 1; + parts.contributions.push(DirectContributionRow { + parent0_component_base: 1, + parent1_component_base_or_sentinel: DIRECT_NONE_U32, + parent0_momentum_form_id: 0, + parent1_momentum_form_id_or_sentinel: DIRECT_NONE_U32, + destination_component_base: 0, + exact_factor_id: 0, + selector_domain_id: 0, + flags: DIRECT_CONTRIBUTION_FLAG_INITIALIZE_DESTINATION, + }); + parts.finalizations[0].component_base = 1; + parts.finalizations.push(DirectFinalizationRow { + component_base: 0, + component_count: 1, + momentum_form_id: 0, + exact_factor_id: 0, + selector_domain_id: 0, + flags: 0, + }); + parts.closures[0].parent0_component_base = 0; + replace_single_closure_proof_factor(&mut parts, ExactComplexRational::ONE, Some(vec![Some(2)])); + parts.row_groups = vec![ + DirectRowGroupDescriptor { + stage: 0, + role: DirectExecutorRole::Source, + destination_operation: DirectDestinationOperation::Initialize, + direct_executor_id: 0, + row_start: 0, + row_count: 1, + }, + DirectRowGroupDescriptor { + stage: 1, + role: DirectExecutorRole::Contribution, + destination_operation: DirectDestinationOperation::Add, + direct_executor_id: 1, + row_start: 0, + row_count: 1, + }, + DirectRowGroupDescriptor { + stage: 1, + role: DirectExecutorRole::Finalization, + destination_operation: DirectDestinationOperation::FinalizeInPlace, + direct_executor_id: 2, + row_start: 0, + row_count: 1, + }, + DirectRowGroupDescriptor { + stage: 2, + role: DirectExecutorRole::Contribution, + destination_operation: DirectDestinationOperation::Add, + direct_executor_id: 1, + row_start: 1, + row_count: 1, + }, + DirectRowGroupDescriptor { + stage: 2, + role: DirectExecutorRole::Finalization, + destination_operation: DirectDestinationOperation::FinalizeInPlace, + direct_executor_id: 2, + row_start: 1, + row_count: 1, + }, + DirectRowGroupDescriptor { + stage: 3, + role: DirectExecutorRole::Closure, + destination_operation: DirectDestinationOperation::ClosureAdd, + direct_executor_id: 3, + row_start: 0, + row_count: 1, + }, + ]; + let plan = DirectRecurrencePlan::new(parts).unwrap(); + let executors = DirectExecutorCatalog::new( + &plan, + plan.direct_template_catalog_digest(), + direct_executor_handles(), + ) + .unwrap(); + let mut runtime = DirectRecurrenceExecutionRuntime::new(plan, executors, 1).unwrap(); + runtime.set_parameters(&[3.0], &[1.0]).unwrap(); + runtime.momentum_plane_mut(0, 0).unwrap().unwrap()[0] = 2.0; + + let output = runtime.execute_tile(1).unwrap(); + assert_eq!(output.destination_re(0).unwrap(), &[16.0]); + assert_eq!(output.destination_im(0).unwrap(), &[12.0]); +} + +#[test] +fn external_momentum_planes_keep_bitwise_term_order_and_untouched_padding() { + let values = [0.0, -0.0, 1.0e16, 1.0, -1.0e16, f64::MIN_POSITIVE, -3.5]; + for source_count in [1, 2, 5] { + for active_points in [1, 3, 4, 7, 8, 9, 31, 128] { + let point_stride = active_points + 8; + let external = (0..active_points * source_count * 4) + .map(|index| values[index % values.len()]) + .collect::>(); + let mut external_planes = vec![0.0; external.len()]; + for source in 0..source_count { + for component in 0..4 { + for point in 0..active_points { + external_planes[(source * 4 + component) * active_points + point] = + external[(point * source_count + source) * 4 + component]; + } + } + } + for reverse in [false, true] { + // Repeated sources deliberately exercise non-associative + // cancellation: the new loop may not reorder any terms. + let terms = [(0, 1_i32), (source_count - 1, -2), (0, -1)]; + for term_count in 0..=terms.len() { + let mut observed = vec![17.0; 4 * point_stride]; + let permutation = |slot| { + if reverse { + source_count - 1 - slot + } else { + slot + } + }; + let sign = |slot| if reverse && slot % 2 == 0 { -1_i32 } else { 1 }; + super::fill_momentum_form_from_external::( + &mut observed, + point_stride, + active_points, + &external, + source_count, + terms[..term_count].iter().map(|&(slot, coefficient)| { + ( + permutation(slot), + f64::from(coefficient) * f64::from(sign(slot)), + ) + }), + ); + let mut observed_planes = vec![17.0; 4 * point_stride]; + super::fill_momentum_form_from_external::( + &mut observed_planes, + point_stride, + active_points, + &external_planes, + source_count, + terms[..term_count].iter().map(|&(slot, coefficient)| { + ( + permutation(slot), + f64::from(coefficient) * f64::from(sign(slot)), + ) + }), + ); + assert_eq!( + observed + .iter() + .map(|value| value.to_bits()) + .collect::>(), + observed_planes + .iter() + .map(|value| value.to_bits()) + .collect::>() + ); + for component in 0..4 { + for point in 0..active_points { + let mut expected = 0.0_f64; + for &(slot, coefficient) in &terms[..term_count] { + let source = + (point * source_count + permutation(slot)) * 4 + component; + expected += f64::from(coefficient) + * f64::from(sign(slot)) + * external[source]; + } + assert_eq!( + observed[component * point_stride + point].to_bits(), + expected.to_bits(), + "sources={source_count}, points={active_points}, reverse={reverse}, terms={term_count}, component={component}, point={point}" + ); + } + assert!( + observed[component * point_stride + active_points + ..(component + 1) * point_stride] + .iter() + .all(|&value| value == 17.0) + ); + } + } + } + } + } +} + +#[test] +fn external_momentum_planes_profiled_and_unprofiled_execution_match_point_major() { + fn bits(output: DirectRecurrenceTileOutput<'_>) -> (Vec, Vec) { + ( + output + .storage_re() + .iter() + .map(|value| value.to_bits()) + .collect(), + output + .storage_im() + .iter() + .map(|value| value.to_bits()) + .collect(), + ) + } + for contracted in [false, true] { + let (base, _) = synthetic_plan_and_executors(); + let mut parts = base.into_parts(); + if contracted { + parts.strategy = RecurrenceStrategy::ContractedColorUnion; + } + parts.replay_momentum_signs = vec![-1, 1, 1, -1]; + let plan = DirectRecurrencePlan::new(parts).unwrap(); + let executors = DirectExecutorCatalog::new( + &plan, + plan.direct_template_catalog_digest(), + direct_executor_handles(), + ) + .unwrap(); + let mut runtime = DirectRecurrenceExecutionRuntime::new(plan, executors, 4).unwrap(); + runtime.set_parameters(&[3.0], &[1.0]).unwrap(); + for count in [4, 3, 1, 4] { + let point_major = (0..count * 8) + .map(|index| { + if index % 7 == 0 { + -0.0 + } else { + index as f64 + 0.25 + } + }) + .collect::>(); + let mut planes = vec![0.0; point_major.len()]; + for source in 0..2 { + for component in 0..4 { + for point in 0..count { + planes[(source * 4 + component) * count + point] = + point_major[(point * 2 + source) * 4 + component]; + } + } + } + for flow in 0..2 { + let selector = runtime.prepare_replay_selector(flow).unwrap(); + let expected = bits(if contracted { + runtime + .execute_contracted_tile_from_external(count as u32, &point_major) + .unwrap() + } else { + runtime + .execute_replay_tile_from_external(&selector, count as u32, &point_major) + .unwrap() + }); + let profiled = bits(if contracted { + runtime + .execute_contracted_tile_from_external_planes::(count as u32, &planes) + .unwrap() + } else { + runtime + .execute_replay_tile_from_external_planes::( + &selector, + count as u32, + &planes, + ) + .unwrap() + }); + assert_eq!(profiled, expected); + let before_unprofiled = runtime.activity_counters(); + let unprofiled = bits(if contracted { + runtime + .execute_contracted_tile_from_external_planes::( + count as u32, + &planes, + ) + .unwrap() + } else { + runtime + .execute_replay_tile_from_external_planes::( + &selector, + count as u32, + &planes, + ) + .unwrap() + }); + assert_eq!(unprofiled, expected); + assert_eq!(runtime.activity_counters(), before_unprofiled); + } + } + } +} + +#[test] +fn external_momentum_fill_crossings_and_identity_preserve_bits_across_tile_tails() { + let (base, _) = synthetic_plan_and_executors(); + let mut parts = base.into_parts(); + parts.strategy = RecurrenceStrategy::ContractedColorUnion; + parts.point_tile_size = 9; + parts.momentum_terms[1].coefficient = -2; + parts.replay_momentum_signs = vec![-1, 1, 1, -1]; + let plan = DirectRecurrencePlan::new(parts).unwrap(); + let executors = DirectExecutorCatalog::new( + &plan, + plan.direct_template_catalog_digest(), + direct_executor_handles(), + ) + .unwrap(); + let mut runtime = DirectRecurrenceExecutionRuntime::new(plan, executors, 4).unwrap(); + let selectors = [ + runtime.prepare_replay_selector(0).unwrap(), + runtime.prepare_replay_selector(1).unwrap(), + ]; + let values = [-0.0, 0.0, 1.0e16, 1.0, -1.0e16, f64::MIN_POSITIVE]; + for point_count in [9, 7, 1, 9, 3] { + let external = (0..point_count as usize * 8) + .map(|index| values[index % values.len()]) + .collect::>(); + for selector in [Some(&selectors[0]), Some(&selectors[1]), None] { + if let Some(selector) = selector { + runtime + .fill_momenta_from_external_unprofiled(selector, point_count, &external) + .unwrap(); + } else { + runtime + .fill_identity_momenta_from_external::(point_count, &external) + .unwrap(); + } + let stride = runtime.point_stride() as usize; + for component in 0..4 { + for point in 0..point_count as usize { + let mut expected = 0.0_f64; + for term in runtime.plan.momentum_terms() { + let slot = term.source_slot as usize; + let (external_slot, coefficient) = + selector.map_or((slot, f64::from(term.coefficient)), |selector| { + ( + selector.source_permutation[slot] as usize, + f64::from(term.coefficient) + * f64::from(selector.source_momentum_signs[slot]), + ) + }); + expected += + coefficient * external[(point * 2 + external_slot) * 4 + component]; + } + assert_eq!( + runtime.momenta.as_slice()[component * stride + point].to_bits(), + expected.to_bits() + ); + } + assert!( + runtime.momenta.as_slice()[component * stride + point_count as usize + ..component * stride + runtime.point_tile_size() as usize] + .iter() + .all(|value| value.to_bits() == 0.0_f64.to_bits()) + ); + } + } + } +} + +#[test] +fn external_momentum_fill_resolves_forms_once_and_clears_newly_inactive_tails() { + let mut runtime = synthetic_runtime_with_lorentz(4); + let flow_zero = runtime.prepare_replay_selector(0).unwrap(); + let flow_one = runtime.prepare_replay_selector(1).unwrap(); + let identity = storage_identity(&mut runtime); + + assert!( + runtime + .fill_momenta_from_external(&flow_zero, 3, &external_two_point_momenta()) + .unwrap_err() + .to_string() + .contains("expected 24") + ); + runtime + .fill_momenta_from_external(&flow_zero, 3, &external_three_point_momenta()) + .unwrap(); + let point_stride = runtime.point_tile_size() as usize; + assert_eq!(runtime.momenta_mut().unwrap().len(), 4 * point_stride); + assert_eq!( + &runtime.momenta_mut().unwrap()[0..4], + &[9.0, 12.0, 23.0, 0.0] + ); + assert_eq!( + &runtime.momenta_mut().unwrap()[point_stride..point_stride + 4], + &[90.0, 120.0, 230.0, 0.0] + ); + assert_eq!( + &runtime.momenta_mut().unwrap()[2 * point_stride..2 * point_stride + 4], + &[900.0, 1200.0, 2300.0, 0.0] + ); + assert_eq!( + &runtime.momenta_mut().unwrap()[3 * point_stride..3 * point_stride + 4], + &[9000.0, 12000.0, 23000.0, 0.0] + ); + + runtime + .fill_momenta_from_external(&flow_one, 2, &external_two_point_momenta()) + .unwrap(); + assert_eq!(&runtime.momenta_mut().unwrap()[0..4], &[6.0, 9.0, 0.0, 0.0]); + for plane in 0..4 { + assert_eq!( + runtime.momenta_mut().unwrap()[plane * point_stride + 2], + 0.0 + ); + } + assert_eq!(storage_identity(&mut runtime), identity); + assert_eq!( + runtime.activity_counters(), + DirectRuntimeActivityCounters { + momentum_fill_calls: 2, + momentum_forms_filled: 2, + momentum_terms_filled: 4, + momentum_scalar_values_filled: 20, + ..DirectRuntimeActivityCounters::default() + } + ); + + let mut scalar_runtime = synthetic_runtime(); + let scalar_selector = scalar_runtime.prepare_replay_selector(0).unwrap(); + assert!( + scalar_runtime + .fill_momenta_from_external(&scalar_selector, 2, &external_two_point_momenta(),) + .unwrap_err() + .to_string() + .contains("requires 4 Lorentz components") + ); +} + +#[test] +fn prepared_replay_selectors_cover_both_physical_flows_without_regeneration() { + let mut runtime = synthetic_runtime_with_lorentz(4); + let flow_zero = runtime.prepare_replay_selector(0).unwrap(); + let flow_one = runtime.prepare_replay_selector(1).unwrap(); + assert_eq!(flow_zero.mapped_external_source_slot(0), Some(0)); + assert_eq!(flow_zero.mapped_external_source_slot(1), Some(1)); + assert_eq!(flow_one.mapped_external_source_slot(0), Some(1)); + assert_eq!(flow_one.mapped_external_source_slot(1), Some(0)); + assert_eq!(flow_zero.phase(), (1.0, 0.0)); + assert_eq!(flow_one.phase(), (0.0, 1.0)); + assert_eq!(flow_one.multiplicity(), 2); + let identity = storage_identity(&mut runtime); + + let output = runtime + .execute_replay_tile_from_external(&flow_zero, 2, &external_two_point_momenta()) + .unwrap(); + assert_eq!(output.public_flow_id(), Some(0)); + assert_eq!(output.representative_flow_id(), Some(0)); + assert_eq!( + output.selected_destination_ids().collect::>(), + vec![0] + ); + assert_eq!(output.destination_re(0).unwrap(), &[-27.0, -36.0]); + assert_eq!(output.destination_im(0).unwrap(), &[-9.0, -12.0]); + + runtime + .fill_momenta_from_external(&flow_zero, 2, &external_two_point_momenta()) + .unwrap(); + assert!( + runtime + .execute_replay_tile(&flow_one, 2) + .unwrap_err() + .to_string() + .contains("were not filled for this selector") + ); + + let output = runtime + .execute_replay_tile_from_external(&flow_one, 2, &external_two_point_momenta()) + .unwrap(); + assert_eq!(output.public_flow_id(), Some(1)); + assert_eq!(output.representative_flow_id(), Some(0)); + assert_eq!(output.destination_re(0).unwrap(), &[12.0, 18.0]); + assert_eq!(output.destination_im(0).unwrap(), &[-36.0, -54.0]); + assert_eq!(storage_identity(&mut runtime), identity); + + let direct = runtime.counters(); + assert_eq!(direct.source_calls, 2); + assert_eq!(direct.contribution_calls, 2); + assert_eq!(direct.finalization_calls, 2); + assert_eq!(direct.closure_calls, 2); + assert_eq!(direct.packed_input_bytes, 0); + assert_eq!(direct.packed_output_bytes, 0); + assert_eq!(direct.scatter_bytes, 0); + let activity = runtime.activity_counters(); + assert_eq!(activity.momentum_fill_calls, 3); + assert_eq!(activity.schedule_executions, 2); + assert_eq!(activity.replay_schedule_executions, 2); + assert_eq!(activity.replay_output_values_scaled, 4); +} + +#[test] +fn replay_observation_captures_representative_amplitude_before_single_public_scaling() { + let mut runtime = synthetic_runtime_with_lorentz(4); + let selector = runtime.prepare_replay_selector(1).unwrap(); + begin_direct_current_observation(runtime.plan(), Some(selector.representative_flow_id()), 2) + .unwrap(); + + let output = runtime + .execute_replay_tile_from_external(&selector, 2, &external_two_point_momenta()) + .unwrap(); + let public = output + .destination_re(0) + .unwrap() + .iter() + .copied() + .zip(output.destination_im(0).unwrap().iter().copied()) + .collect::>(); + let observation = take_direct_current_observation().unwrap(); + let representative = &observation.amplitudes_before_replay[&0]; + + assert_eq!(representative, &[(-18.0, -6.0), (-27.0, -9.0)]); + let phase = selector.phase(); + let multiplicity = f64::from(selector.multiplicity()); + let expected_public = representative + .iter() + .map(|&(value_re, value_im)| { + let scale_re = phase.0 * multiplicity; + let scale_im = phase.1 * multiplicity; + ( + value_re * scale_re - value_im * scale_im, + value_re * scale_im + value_im * scale_re, + ) + }) + .collect::>(); + assert_eq!(public, expected_public); + assert_eq!(public, [(12.0, -36.0), (18.0, -54.0)]); +} + +#[test] +fn replay_selector_executes_only_its_dependency_closed_rows() { + let (base_plan, _) = synthetic_plan_and_executors(); + let mut parts = base_plan.into_parts(); + parts.current_arena_components = 3; + parts.selector_domains.push(DirectSelectorDomainDescriptor { + word_start: 1, + word_count: 1, + }); + parts.selector_words.push(2); + + let mut inactive_current = parts.currents[0]; + inactive_current.semantic_current_id = 2; + inactive_current.component_base = 2; + inactive_current.selector_domain_id = 1; + inactive_current.source_row_or_sentinel = 1; + parts.currents.push(inactive_current); + + let mut inactive_source = parts.sources[0]; + inactive_source.destination_component_base = 2; + inactive_source.selector_domain_id = 1; + parts.sources.push(inactive_source); + parts + .row_groups + .iter_mut() + .find(|group| group.role == DirectExecutorRole::Source) + .unwrap() + .row_count = 2; + + let plan = DirectRecurrencePlan::new(parts).unwrap(); + let executors = DirectExecutorCatalog::new( + &plan, + plan.direct_template_catalog_digest(), + direct_executor_handles(), + ) + .unwrap(); + let mut runtime = DirectRecurrenceExecutionRuntime::new(plan, executors, 4).unwrap(); + runtime.set_parameters(&[3.0], &[1.0]).unwrap(); + let selector = runtime.prepare_replay_selector(0).unwrap(); + + assert_eq!(selector.selected_row_group_count(), 4); + assert_eq!(selector.selected_row_count(), 4); + let output = runtime + .execute_replay_tile_from_external(&selector, 2, &external_two_point_momenta()) + .unwrap(); + assert_eq!(output.destination_re(0).unwrap(), &[-27.0, -36.0]); + assert_eq!(runtime.counters().source_rows, 1); +} + +/// The second helicity uses a numerically identified current from the first, +/// copied into the storage formerly occupied by its (now dead) source. +fn two_helicity_reused_storage_runtime() -> DirectRecurrenceExecutionRuntime { + let (plan, _) = synthetic_plan_and_executors(); + let mut parts = plan.into_parts(); + parts.amplitude_destination_count = 2; + parts.currents.push(DirectCurrentDescriptor { + semantic_current_id: 2, + component_base: 0, + stage: 2, + first_use: 2, + last_use: 3, + finalization_row_or_sentinel: DIRECT_NONE_U32, + ..parts.currents[1] + }); + parts.contributions.push(DirectContributionRow { + parent0_component_base: 1, + parent0_momentum_form_id: 1, // certified-reuse component count + destination_component_base: 0, + exact_factor_id: 3, + flags: DIRECT_CONTRIBUTION_FLAG_INITIALIZE_DESTINATION + | crate::recurrence::direct_plan::DIRECT_CONTRIBUTION_FLAG_CERTIFIED_REUSE, + ..parts.contributions[0] + }); + parts.row_groups.insert( + 3, + DirectRowGroupDescriptor { + stage: 2, + row_start: 1, + direct_executor_id: DIRECT_NONE_U32, + ..parts.row_groups[1] + }, + ); + parts.closures.push(DirectClosureRow { + parent0_component_base: 0, + amplitude_destination_id: 1, + flags: 1, + ..parts.closures[0] + }); + parts.row_groups.push(DirectRowGroupDescriptor { + stage: 3, + row_start: 1, + ..parts.row_groups[4] + }); + parts + .amplitude_destinations + .push(DirectAmplitudeDestinationDescriptor { + id: 1, + target_helicity_id_or_sentinel: 1, + closure_row_start: 1, + ..parts.amplitude_destinations[0] + }); + parts + .resolved_helicities + .push(DirectResolvedHelicityDescriptor { + id: 1, + source_state_start: 2, + public_helicity_start: 2, + ..parts.resolved_helicities[0] + }); + parts.source_state_assignments.extend_from_within(0..2); + parts.public_helicities.extend([1, -1]); + parts.replay_helicity_map = vec![0, 1, 0, 1]; + parts.replay_targets[0].helicity_map_count = 2; + parts.replay_targets[1].helicity_map_start = 2; + parts.replay_targets[1].helicity_map_count = 2; + let prototype = &parts.closure_proofs.contributions()[0]; + let contribution = ClosureProofContributionV2::new( + 1, + 0, + Some(1), + Some(1), + prototype.closure_template_id(), + prototype.closure_template_semantic_digest(), + prototype.quantum_flow_template_id(), + vec![2], + vec![Some(2)], + prototype.construction_parent_semantic_digests().to_vec(), + prototype.construction_parent_color_digests().to_vec(), + vec![0], + vec![0], + vec![0], + prototype.color_witness_term_id(), + prototype.color_witness_proof_digest(), + prototype.three_line_certificate_id(), + prototype.pairing_certificate_ids().to_vec(), + prototype.reflection_certificate_id(), + prototype.exact_factor(), + prototype.multiplicity(), + ) + .unwrap(); + let old_group = &parts.closure_proofs.groups()[0]; + let group = ClosureExecutionProofGroupV2::new_with_candidate_selector_domain( + 1, + Some(1), + Some(1), + CheckedTableRange::new(1, 1), + old_group.exact_summed_factor(), + old_group.component_factor_digest(), + old_group.candidate_selector_domain_digest(), + old_group.selector_domain_digest(), + ) + .unwrap(); + parts.closure_proofs = ClosureProofMetadataV2::new_with_three_line_certificates( + vec![prototype.clone(), contribution], + vec![old_group.clone(), group], + parts.closure_proofs.reflection_certificates().to_vec(), + parts + .closure_proofs + .three_line_traversal_certificates() + .to_vec(), + ) + .unwrap(); + let plan = DirectRecurrencePlan::new(parts).unwrap(); + let executors = DirectExecutorCatalog::new( + &plan, + plan.direct_template_catalog_digest(), + direct_executor_handles(), + ) + .unwrap(); + let mut runtime = DirectRecurrenceExecutionRuntime::new(plan, executors, 4).unwrap(); + runtime.set_parameters(&[3.0], &[1.0]).unwrap(); + runtime +} + +#[test] +fn selected_amplitude_views_match_public_accessors_and_check_active_ranges() { + let runtime = two_helicity_reused_storage_runtime(); + let mut destinations = runtime.plan().amplitude_destinations().to_vec(); + destinations.push(DirectAmplitudeDestinationDescriptor { + id: 2, + target_sector_id: 1, + ..destinations[0] + }); + let real = (0..12).map(|value| value as f64).collect::>(); + let imaginary = real.iter().map(|value| -value).collect::>(); + for point_count in [1, 3, 4] { + for (representative, selection, expected_ids) in [ + (None, None, &[0, 1, 2][..]), + (Some(0), None, &[0, 1][..]), + (Some(1), None, &[2][..]), + (Some(0), Some(&[1][..]), &[1][..]), + (Some(0), Some(&[][..]), &[][..]), + ] { + let output = DirectRecurrenceTileOutput { + amplitude_re: &real, + amplitude_im: &imaginary, + amplitude_destinations: &destinations, + point_count, + point_stride: 4, + destination_count: 3, + public_flow_id: representative, + representative_flow_id: representative, + selected_destination_ids: selection, + }; + let views = output + .selected_amplitudes() + .collect::>>() + .unwrap(); + assert_eq!( + views + .iter() + .map(|view| view.destination.id) + .collect::>(), + expected_ids + ); + for view in views { + let id = view.destination.id; + assert_eq!(view.real, output.destination_re(id).unwrap()); + assert_eq!(view.imaginary, output.destination_im(id).unwrap()); + assert_eq!( + view.real.as_ptr(), + output.destination_re(id).unwrap().as_ptr() + ); + assert_eq!( + view.imaginary.as_ptr(), + output.destination_im(id).unwrap().as_ptr() + ); + assert_eq!(view.destination, &destinations[id as usize]); + } + } + } + let malformed = DirectRecurrenceTileOutput { + amplitude_re: &real, + amplitude_im: &imaginary[..2], + amplitude_destinations: &destinations, + point_count: 3, + point_stride: 4, + destination_count: 3, + public_flow_id: Some(0), + representative_flow_id: Some(0), + selected_destination_ids: Some(&[0]), + }; + assert!(malformed.selected_amplitudes().next().unwrap().is_err()); +} + +#[test] +fn destination_replay_preserves_shared_helicity_currents_and_recycled_storage() { + let mut runtime = two_helicity_reused_storage_runtime(); + let all = runtime.prepare_replay_selector(0).unwrap(); + let selected = [ + runtime + .prepare_replay_selector_for_destinations(&all, &[0]) + .unwrap(), + runtime + .prepare_replay_selector_for_destinations(&all, &[1]) + .unwrap(), + ]; + assert_eq!(all.selected_row_count(), 6); + assert_eq!(selected[0].selected_row_count(), 4); + assert_eq!(selected[1].selected_row_count(), 5); + let empty = runtime + .prepare_replay_selector_for_destinations(&all, &[]) + .unwrap(); + let both = runtime + .prepare_replay_selector_for_destinations(&all, &[0, 1]) + .unwrap(); + for selector in [&selected[0], &selected[1], &empty, &both] { + assert!( + selector + .destination_program + .as_ref() + .unwrap() + .current_clear_stages + .is_empty() + ); + } + let momenta = external_two_point_momenta(); + let expected = { + let output = runtime + .execute_replay_tile_from_external(&all, 2, &momenta) + .unwrap(); + [0, 1].map(|id| { + ( + output.destination_re(id).unwrap().to_vec(), + output.destination_im(id).unwrap().to_vec(), + ) + }) + }; + let allocations = runtime.allocation_counters(); + for id in [0, 1, 0, 1] { + let output = runtime + .execute_replay_tile_from_external(&selected[id], 2, &momenta) + .unwrap(); + assert_eq!( + output.selected_destination_ids().collect::>(), + vec![id as u32] + ); + assert_eq!(output.destination_re(id as u32).unwrap(), expected[id].0); + assert_eq!(output.destination_im(id as u32).unwrap(), expected[id].1); + assert!(output.destination_re(1 - id as u32).is_none()); + } + let output = runtime + .execute_replay_tile_from_external(&empty, 2, &momenta) + .unwrap(); + assert_eq!(output.selected_destination_ids().count(), 0); + assert!(output.destination_re(0).is_none()); + let output = runtime + .execute_replay_tile_from_external(&both, 2, &momenta) + .unwrap(); + for id in [0, 1] { + assert_eq!(output.destination_re(id).unwrap(), expected[id as usize].0); + } + assert_eq!(runtime.allocation_counters(), allocations); + assert!( + runtime + .prepare_replay_selector_for_destinations(&all, &[1, 0]) + .is_err() + ); + assert!( + runtime + .prepare_replay_selector_for_destinations(&all, &[0, 0]) + .is_err() + ); + assert!( + runtime + .prepare_replay_selector_for_destinations(&all, &[2]) + .is_err() + ); +} + +#[test] +fn destination_replay_initializers_overwrite_poisoned_recycled_storage_and_tile_tails() { + let mut runtime = two_helicity_reused_storage_runtime(); + let all = runtime.prepare_replay_selector(0).unwrap(); + let momenta = [1.25, -3.5, 7.0, 0.125] + .into_iter() + .flat_map(|energy| [energy, 0.0, 0.0, 0.0, energy * 0.5, 0.0, 0.0, 0.0]) + .collect::>(); + for destination_ids in [&[0][..], &[1][..], &[0, 1][..]] { + let selected = runtime + .prepare_replay_selector_for_destinations(&all, destination_ids) + .unwrap(); + assert!( + selected + .destination_program + .as_ref() + .unwrap() + .current_clear_stages + .is_empty() + ); + for point_count in [4, 3, 1, 4] { + let input = &momenta[..point_count as usize * 8]; + let expected = { + let output = runtime + .execute_replay_tile_from_external(&all, point_count, input) + .unwrap(); + destination_ids + .iter() + .map(|&id| { + ( + output.destination_re(id).unwrap().to_vec(), + output.destination_im(id).unwrap().to_vec(), + ) + }) + .collect::>() + }; + for profiled in [true, false] { + let (current_re, current_im, _, _) = runtime.arena.split_slices_mut(); + current_re.fill(f64::NAN); + current_im.fill(f64::NAN); + let output = if profiled { + runtime.execute_replay_tile_from_external(&selected, point_count, input) + } else { + runtime.execute_replay_tile_from_external_unprofiled( + &selected, + point_count, + input, + ) + } + .unwrap(); + for (&id, (real, imaginary)) in destination_ids.iter().zip(&expected) { + assert_eq!(output.destination_re(id).unwrap(), real); + assert_eq!(output.destination_im(id).unwrap(), imaginary); + } + let stride = runtime.point_stride() as usize; + let (current_re, current_im) = runtime.current_arenas(); + for plane in current_re + .chunks_exact(stride) + .chain(current_im.chunks_exact(stride)) + { + assert!( + plane[point_count as usize..] + .iter() + .all(|value| value.is_nan()) + ); + } + } + } + } +} + +#[test] +fn destination_replay_retains_clear_when_row_selector_omits_the_initializer() { + let mut parts = two_helicity_reused_storage_runtime() + .plan() + .clone() + .into_parts(); + parts.selector_domains.push(DirectSelectorDomainDescriptor { + word_start: parts.selector_words.len() as u64, + word_count: 1, + }); + parts.selector_words.push(2); + let mut additive = parts.contributions[0]; + additive.flags = 0; + parts.contributions[0].selector_domain_id = 1; + parts.contributions.insert(1, additive); + parts.row_groups[1].row_count = 2; + parts.row_groups[3].row_start = 2; + let plan = DirectRecurrencePlan::new(parts).unwrap(); + let executors = DirectExecutorCatalog::new( + &plan, + plan.direct_template_catalog_digest(), + direct_executor_handles(), + ) + .unwrap(); + let mut runtime = DirectRecurrenceExecutionRuntime::new(plan, executors, 4).unwrap(); + runtime.set_parameters(&[3.0], &[1.0]).unwrap(); + let all = runtime.prepare_replay_selector(0).unwrap(); + let selected = runtime + .prepare_replay_selector_for_destinations(&all, &[0, 1]) + .unwrap(); + let stages = &selected + .destination_program + .as_ref() + .unwrap() + .current_clear_stages; + assert_eq!(stages.len(), 1); + assert_eq!(stages[0].stage, 1); + assert_eq!(stages[0].ranges.len(), 1); + assert_eq!(stages[0].ranges[0].component_base, 1); + assert_eq!(stages[0].ranges[0].component_count, 1); + let momenta = external_two_point_momenta(); + for point_count in [2, 1, 2] { + let input = &momenta[..point_count as usize * 8]; + let expected = { + let output = runtime + .execute_replay_tile_from_external(&all, point_count, input) + .unwrap(); + [0, 1].map(|id| { + ( + output.destination_re(id).unwrap().to_vec(), + output.destination_im(id).unwrap().to_vec(), + ) + }) + }; + for profiled in [true, false] { + let (current_re, current_im, _, _) = runtime.arena.split_slices_mut(); + current_re.fill(f64::NAN); + current_im.fill(f64::NAN); + let output = if profiled { + runtime.execute_replay_tile_from_external(&selected, point_count, input) + } else { + runtime.execute_replay_tile_from_external_unprofiled(&selected, point_count, input) + } + .unwrap(); + for (id, (real, imaginary)) in expected.iter().enumerate() { + assert_eq!(output.destination_re(id as u32).unwrap(), real); + assert_eq!(output.destination_im(id as u32).unwrap(), imaginary); + } + } + } +} + +#[test] +fn destination_replay_self_copy_preserves_clear_for_mutable_zero_factor() { + let mut parts = two_helicity_reused_storage_runtime() + .plan() + .clone() + .into_parts(); + parts.contributions[1].parent0_component_base = + parts.contributions[1].destination_component_base; + parts.contributions[1].exact_factor_id = parts.exact_factors.len() as u32; + parts.exact_factors.push(ExactComplexRational::ZERO); + let plan = DirectRecurrencePlan::new(parts).unwrap(); + let executors = DirectExecutorCatalog::new( + &plan, + plan.direct_template_catalog_digest(), + direct_executor_handles(), + ) + .unwrap(); + let mut runtime = DirectRecurrenceExecutionRuntime::new(plan, executors, 4).unwrap(); + runtime.set_parameters(&[3.0], &[1.0]).unwrap(); + let all = runtime.prepare_replay_selector(0).unwrap(); + let selected = runtime + .prepare_replay_selector_for_destinations(&all, &[1]) + .unwrap(); + let stages = &selected + .destination_program + .as_ref() + .unwrap() + .current_clear_stages; + assert_eq!(stages.len(), 1); + assert_eq!(stages[0].stage, 2); + let momenta = external_two_point_momenta(); + let factor_id = runtime.plan().contributions()[1].exact_factor_id as usize; + for (point_count, factor) in [(2, 0.0), (1, 1.0), (2, 0.0)] { + runtime.factors_mut().0[factor_id] = factor; + for profiled in [true, false] { + runtime.activate_point_layout(point_count).unwrap(); + let (current_re, current_im, _, _) = runtime.arena.split_slices_mut(); + current_re.fill(f64::NAN); + current_im.fill(f64::NAN); + let input = &momenta[..point_count as usize * 8]; + let output = if profiled { + runtime.execute_replay_tile_from_external(&selected, point_count, input) + } else { + runtime.execute_replay_tile_from_external_unprofiled(&selected, point_count, input) + } + .unwrap(); + assert!( + output + .destination_re(1) + .unwrap() + .iter() + .all(|&value| value == 0.0) + ); + assert!( + output + .destination_im(1) + .unwrap() + .iter() + .all(|&value| value == 0.0) + ); + } + } +} + +#[test] +fn destination_replay_partial_copy_retains_the_entire_target_clear() { + let mut parts = two_helicity_reused_storage_runtime() + .plan() + .clone() + .into_parts(); + // The later current has a wider span than the old source occupying its + // first slot. Such a custom copy remains valid, but initializes only the + // copied prefix and therefore still needs the full current clear. + parts.current_arena_components = 3; + parts.currents[1].component_base = 2; + parts.currents[2].component_count = 2; + parts.contributions[0].destination_component_base = 2; + parts.finalizations[0].component_base = 2; + parts.closures[0].parent0_component_base = 2; + parts.contributions[1].parent0_component_base = 2; + let plan = DirectRecurrencePlan::new(parts).unwrap(); + let executors = DirectExecutorCatalog::new( + &plan, + plan.direct_template_catalog_digest(), + direct_executor_handles(), + ) + .unwrap(); + let mut runtime = DirectRecurrenceExecutionRuntime::new(plan, executors, 4).unwrap(); + runtime.set_parameters(&[3.0], &[1.0]).unwrap(); + let all = runtime.prepare_replay_selector(0).unwrap(); + let selected = runtime + .prepare_replay_selector_for_destinations(&all, &[1]) + .unwrap(); + let stages = &selected + .destination_program + .as_ref() + .unwrap() + .current_clear_stages; + assert_eq!(stages.len(), 1); + assert_eq!(stages[0].stage, 2); + assert_eq!(stages[0].ranges[0].component_base, 0); + assert_eq!(stages[0].ranges[0].component_count, 2); + let (current_re, current_im, _, _) = runtime.arena.split_slices_mut(); + current_re.fill(f64::NAN); + current_im.fill(f64::NAN); + runtime + .execute_replay_tile_from_external(&selected, 2, &external_two_point_momenta()) + .unwrap(); + let stride = runtime.point_stride() as usize; + let (current_re, current_im) = runtime.current_arenas(); + assert_eq!(¤t_re[stride..stride + 2], &[0.0, 0.0]); + assert_eq!(¤t_im[stride..stride + 2], &[0.0, 0.0]); +} + +#[test] +fn destination_replay_shared_program_preserves_public_flow_phase() { + let mut runtime = synthetic_runtime_with_lorentz(4); + let base = runtime.prepare_replay_selector(0).unwrap(); + let unrestricted = runtime.prepare_replay_selector(1).unwrap(); + let first = runtime + .prepare_replay_selector_for_destinations(&base, &[0]) + .unwrap(); + let second = runtime + .prepare_replay_selector_with_destination_program(&unrestricted, &first) + .unwrap(); + // Adding selected helicities must not duplicate the full helicity map, + // immutable source mapping, or unrestricted per-flow row schedule. + for (base, selected) in [(&base, &first), (&unrestricted, &second)] { + assert!(std::sync::Arc::ptr_eq( + &base.source_permutation, + &selected.source_permutation + )); + assert!(std::sync::Arc::ptr_eq( + &base.source_momentum_signs, + &selected.source_momentum_signs + )); + assert!(std::sync::Arc::ptr_eq( + &base.helicity_map, + &selected.helicity_map + )); + assert!(std::sync::Arc::ptr_eq( + &base.row_groups, + &selected.row_groups + )); + assert!(std::sync::Arc::ptr_eq( + &base.amplitude_clear_ranges, + &selected.amplitude_clear_ranges + )); + } + assert!(std::sync::Arc::ptr_eq( + first.destination_program.as_ref().unwrap(), + second.destination_program.as_ref().unwrap() + )); + let momenta = external_two_point_momenta(); + let expected = { + let output = runtime + .execute_replay_tile_from_external(&unrestricted, 2, &momenta) + .unwrap(); + ( + output.destination_re(0).unwrap().to_vec(), + output.destination_im(0).unwrap().to_vec(), + ) + }; + let output = runtime + .execute_replay_tile_from_external(&second, 2, &momenta) + .unwrap(); + assert_eq!(output.destination_re(0).unwrap(), expected.0); + assert_eq!(output.destination_im(0).unwrap(), expected.1); +} + +#[test] +fn destination_replay_matches_fanout_under_complex_cancellation() { + let plan = fanout_test_plan(); + let census = ContributionCallCensus::default(); + let catalog = fanout_test_catalog(&plan, &census, false); + let mut runtime = DirectRecurrenceExecutionRuntime::new(plan, catalog, 4).unwrap(); + runtime.set_parameters(&[0.375], &[-1.125]).unwrap(); + let all = runtime.prepare_replay_selector(0).unwrap(); + let selected = runtime + .prepare_replay_selector_for_destinations(&all, &[0]) + .unwrap(); + let momenta = [0.125, -0.875, 1.75, -3.5, 0.0625, 7.25, -11.0] + .into_iter() + .flat_map(|energy| [energy, 0.0, 0.0, 0.0]) + .collect::>(); + let expected = { + let output = runtime + .execute_replay_tile_from_external(&all, 7, &momenta) + .unwrap(); + output + .destination_re(0) + .unwrap() + .iter() + .copied() + .zip(output.destination_im(0).unwrap().iter().copied()) + .collect::>() + }; + assert_eq!(census.rows.load(Ordering::Relaxed), 1); + let output = runtime + .execute_replay_tile_from_external_unprofiled(&selected, 7, &momenta) + .unwrap(); + let observed = output + .destination_re(0) + .unwrap() + .iter() + .copied() + .zip(output.destination_im(0).unwrap().iter().copied()) + .collect::>(); + assert_scale_relative_complex_parity(&expected, &observed); + assert_eq!(census.rows.load(Ordering::Relaxed), 19); +} + +#[test] +fn tile_execution_clears_only_active_additive_regions() { + let mut runtime = synthetic_runtime(); + runtime + .momentum_plane_mut(0, 0) + .unwrap() + .unwrap() + .copy_from_slice(&[2.0, 4.0, 100.0, 200.0]); + runtime.execute_tile(4).unwrap(); + let point_stride = runtime.point_stride() as usize; + let (prior_current_re_tail, prior_current_im_tail) = { + let (current_re, current_im) = runtime.current_arenas(); + ( + current_re[point_stride + 2..point_stride + 4].to_vec(), + current_im[point_stride + 2..point_stride + 4].to_vec(), + ) + }; + let (prior_amplitude_re_tail, prior_amplitude_im_tail) = { + let (amplitude_re, amplitude_im) = runtime.amplitude_arenas(); + (amplitude_re[2..4].to_vec(), amplitude_im[2..4].to_vec()) + }; + + let output = runtime.execute_tile(2).unwrap(); + assert_eq!(output.destination_re(0).unwrap(), &[-6.0, -12.0]); + assert_eq!(output.destination_im(0).unwrap(), &[-2.0, -4.0]); + assert_eq!(&output.storage_re()[2..4], prior_amplitude_re_tail); + assert_eq!(&output.storage_im()[2..4], prior_amplitude_im_tail); + let (current_re, current_im) = runtime.current_arenas(); + assert_eq!(¤t_re[point_stride..point_stride + 2], &[6.0, 12.0]); + assert_eq!( + ¤t_re[point_stride + 2..point_stride + 4], + prior_current_re_tail + ); + assert_eq!( + ¤t_im[point_stride + 2..point_stride + 4], + prior_current_im_tail + ); +} + +#[test] +fn tile_bounds_fail_before_direct_execution() { + let mut runtime = synthetic_runtime(); + assert!( + runtime + .execute_tile(0) + .unwrap_err() + .to_string() + .contains("must be positive") + ); + assert!( + runtime + .execute_tile(5) + .unwrap_err() + .to_string() + .contains("exceeds point tile size 4") + ); + assert!( + runtime + .set_parameters(&[], &[]) + .unwrap_err() + .to_string() + .contains("expected 1") + ); + assert!( + runtime + .set_parameters(&[1.0], &[0.0, 0.0]) + .unwrap_err() + .to_string() + .contains("expected 1") + ); + assert!(runtime.outputs().is_none()); + assert_eq!(runtime.counters(), DirectExecutionCounters::default()); + + let (plan, executors) = synthetic_plan_and_executors(); + let error = DirectRecurrenceExecutionRuntime::new(plan, executors, 0) + .err() + .unwrap(); + assert!(error.to_string().contains("Lorentz component count")); +} + +#[test] +fn runtime_parameter_storage_is_sized_from_the_authenticated_plan() { + let (plan, executors) = synthetic_plan_and_executors(); + assert_eq!(plan.parameter_value_count(), 1); + let mut runtime = DirectRecurrenceExecutionRuntime::new(plan, executors, 1).unwrap(); + let (parameters_re, parameters_im) = runtime.parameters_mut(); + assert_eq!(parameters_re.len(), 1); + assert_eq!(parameters_im.len(), 1); +} + +#[test] +fn runtime_clamps_the_effective_tile_but_accepts_a_point_above_the_target() { + let mut parts = crate::recurrence::direct_plan::tests::valid_parts(); + parts.point_tile_size = 1024; + parts.workspace_mib = 1; + parts.current_arena_components = 256; + let plan = DirectRecurrencePlan::new(parts).unwrap(); + let executors = DirectExecutorCatalog::new( + &plan, + plan.direct_template_catalog_digest(), + direct_executor_handles(), + ) + .unwrap(); + let runtime = DirectRecurrenceExecutionRuntime::new(plan, executors, 4).unwrap(); + let per_point_bytes = (256 * 2 + 2 + 4) * std::mem::size_of::(); + let expected_tile = ((1024 * 1024 / per_point_bytes) / 8 * 8) as u32; + assert_eq!(runtime.point_tile_size(), expected_tile); + assert_eq!(runtime.point_stride(), expected_tile); + assert!(runtime.point_tile_size() < 1024); + + let mut parts = crate::recurrence::direct_plan::tests::valid_parts(); + parts.point_tile_size = 1024; + parts.workspace_mib = 1; + parts.current_arena_components = 70_000; + let plan = DirectRecurrencePlan::new(parts).unwrap(); + let executors = DirectExecutorCatalog::new( + &plan, + plan.direct_template_catalog_digest(), + direct_executor_handles(), + ) + .unwrap(); + let runtime = DirectRecurrenceExecutionRuntime::new(plan, executors, 4).unwrap(); + assert_eq!(runtime.point_tile_size(), 8); + assert_eq!(runtime.point_stride(), 8); + + let plan = DirectRecurrencePlan::new({ + let mut parts = crate::recurrence::direct_plan::tests::valid_parts(); + parts.point_tile_size = 1024; + parts.workspace_mib = 1; + parts.current_arena_components = 70_000; + parts + }) + .unwrap(); + let packed_executors = DirectExecutorCatalog::new( + &plan, + plan.direct_template_catalog_digest(), + direct_executor_handles(), + ) + .unwrap() + .mark_packed_singleton_capable(); + let runtime = DirectRecurrenceExecutionRuntime::new(plan, packed_executors, 4).unwrap(); + assert_eq!(runtime.point_tile_size(), 8); + assert_eq!(runtime.point_stride(), 1); +} + +#[test] +fn runtime_source_has_no_eager_or_scatter_execution_route() { + let source = include_str!("direct_runtime.rs"); + for forbidden in [ + "EagerKernelInput", + "EagerKernelBackend", + "EagerKernelCall", + "evaluate_batch", + "Attachment", + ] { + assert!( + !source.contains(forbidden), + "direct runtime must not contain {forbidden}" + ); + } +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/recurrence/exact.rs b/pyamplicol/rust/crates/rusticol-core/src/recurrence/exact.rs new file mode 100644 index 0000000000..e6f1a17ab3 --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/recurrence/exact.rs @@ -0,0 +1,481 @@ +// SPDX-License-Identifier: 0BSD + +use std::cmp::Ordering; +use std::fmt; +use std::str::FromStr; + +use crate::{RusticolError, RusticolResult}; + +fn invalid(message: impl Into) -> RusticolError { + RusticolError::invalid_argument(message) +} + +fn gcd(mut left: u128, mut right: u128) -> u128 { + while right != 0 { + let remainder = left % right; + left = right; + right = remainder; + } + left +} + +fn checked_i128(value: u128, negative: bool, context: &str) -> RusticolResult { + if value > i128::MAX as u128 { + return Err(invalid(format!( + "{context} exceeds the dependency-free exact i128 domain" + ))); + } + let value = value as i128; + if negative { + value + .checked_neg() + .ok_or_else(|| invalid(format!("{context} cannot be negated exactly"))) + } else { + Ok(value) + } +} + +fn checked_divisor(value: u128, context: &str) -> RusticolResult { + i128::try_from(value).map_err(|_| { + invalid(format!( + "{context} exceeds the dependency-free exact i128 domain" + )) + }) +} + +/// A canonical exact rational in the dependency-free, checked i128 domain. +/// +/// Operations never round. Values or intermediate results outside this +/// domain fail closed; callers must not replace such failures with binary64 +/// arithmetic. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, bincode::Encode)] +pub struct ExactRational { + numerator: i128, + denominator: i128, +} + +impl bincode::Decode for ExactRational { + fn decode>( + decoder: &mut D, + ) -> Result { + let numerator = bincode::Decode::decode(decoder)?; + let denominator = bincode::Decode::decode(decoder)?; + Self::new(numerator, denominator) + .map_err(|error| bincode::error::DecodeError::OtherString(error.to_string())) + } +} + +bincode::impl_borrow_decode!(ExactRational); + +impl ExactRational { + pub const ZERO: Self = Self { + numerator: 0, + denominator: 1, + }; + pub const ONE: Self = Self { + numerator: 1, + denominator: 1, + }; + + pub fn new(numerator: i128, denominator: i128) -> RusticolResult { + if denominator <= 0 { + return Err(invalid("exact rational denominator must be positive")); + } + if numerator == 0 { + return Ok(Self::ZERO); + } + if numerator == i128::MIN { + return Err(invalid( + "exact rational numerator exceeds the symmetric dependency-free i128 domain", + )); + } + let divisor = gcd(numerator.unsigned_abs(), denominator as u128); + let divisor = checked_divisor(divisor, "exact rational gcd")?; + Ok(Self { + numerator: numerator / divisor, + denominator: denominator / divisor, + }) + } + + pub fn parse_parts(numerator: &str, denominator: &str) -> RusticolResult { + let numerator = parse_signed_decimal(numerator, "numerator")?; + let denominator = parse_positive_decimal(denominator, "denominator")?; + Self::new(numerator, denominator) + } + + /// Convert a finite binary64 value to its exact dyadic rational whenever + /// that rational fits in the checked i128 domain. + pub fn from_f64_exact(value: f64) -> RusticolResult { + if !value.is_finite() { + return Err(invalid("exact rational source must be finite binary64")); + } + let bits = value.to_bits(); + let negative = bits >> 63 != 0; + let exponent_bits = ((bits >> 52) & 0x7ff) as i32; + let fraction = bits & ((1_u64 << 52) - 1); + if exponent_bits == 0 && fraction == 0 { + return Ok(Self::ZERO); + } + + let (mut significand, mut exponent) = if exponent_bits == 0 { + (u128::from(fraction), -1074_i32) + } else { + ( + u128::from((1_u64 << 52) | fraction), + exponent_bits - 1023 - 52, + ) + }; + let trailing = significand.trailing_zeros() as i32; + significand >>= trailing; + exponent += trailing; + + if exponent >= 0 { + let shifted = significand + .checked_shl(exponent as u32) + .ok_or_else(|| invalid("binary64 numerator exceeds exact i128 domain"))?; + return Self::new(checked_i128(shifted, negative, "binary64 numerator")?, 1); + } + + let denominator = 1_u128 + .checked_shl((-exponent) as u32) + .ok_or_else(|| invalid("binary64 denominator exceeds exact i128 domain"))?; + Self::new( + checked_i128(significand, negative, "binary64 numerator")?, + checked_divisor(denominator, "binary64 denominator")?, + ) + } + + pub const fn numerator(self) -> i128 { + self.numerator + } + + pub const fn denominator(self) -> i128 { + self.denominator + } + + pub const fn is_zero(self) -> bool { + self.numerator == 0 + } + + pub const fn is_one(self) -> bool { + self.numerator == 1 && self.denominator == 1 + } + + pub fn checked_neg(self) -> RusticolResult { + Self::new( + self.numerator + .checked_neg() + .ok_or_else(|| invalid("exact rational negation overflow"))?, + self.denominator, + ) + } + + pub fn checked_add(self, right: Self) -> RusticolResult { + let common = checked_divisor( + gcd(self.denominator as u128, right.denominator as u128), + "exact rational denominator gcd", + )?; + let left_factor = right.denominator / common; + let right_factor = self.denominator / common; + let left = self + .numerator + .checked_mul(left_factor) + .ok_or_else(|| invalid("exact rational addition numerator overflow"))?; + let right_term = right + .numerator + .checked_mul(right_factor) + .ok_or_else(|| invalid("exact rational addition numerator overflow"))?; + let numerator = left + .checked_add(right_term) + .ok_or_else(|| invalid("exact rational addition numerator overflow"))?; + let denominator = right_factor + .checked_mul(right.denominator) + .ok_or_else(|| invalid("exact rational addition denominator overflow"))?; + Self::new(numerator, denominator) + } + + pub fn checked_sub(self, right: Self) -> RusticolResult { + self.checked_add(right.checked_neg()?) + } + + pub fn checked_mul(self, right: Self) -> RusticolResult { + if self.is_zero() || right.is_zero() { + return Ok(Self::ZERO); + } + let left_cross = checked_divisor( + gcd(self.numerator.unsigned_abs(), right.denominator as u128), + "exact rational multiplication cross-gcd", + )?; + let right_cross = checked_divisor( + gcd(right.numerator.unsigned_abs(), self.denominator as u128), + "exact rational multiplication cross-gcd", + )?; + let numerator = (self.numerator / left_cross) + .checked_mul(right.numerator / right_cross) + .ok_or_else(|| invalid("exact rational multiplication numerator overflow"))?; + let denominator = (self.denominator / right_cross) + .checked_mul(right.denominator / left_cross) + .ok_or_else(|| invalid("exact rational multiplication denominator overflow"))?; + Self::new(numerator, denominator) + } + + pub fn checked_div(self, right: Self) -> RusticolResult { + if right.is_zero() { + return Err(invalid("exact rational division by zero")); + } + let reciprocal_numerator = if right.numerator < 0 { + right + .denominator + .checked_neg() + .ok_or_else(|| invalid("exact rational reciprocal overflow"))? + } else { + right.denominator + }; + let reciprocal_denominator = checked_divisor( + right.numerator.unsigned_abs(), + "exact rational reciprocal denominator", + )?; + self.checked_mul(Self::new(reciprocal_numerator, reciprocal_denominator)?) + } + + pub fn checked_square(self) -> RusticolResult { + self.checked_mul(self) + } +} + +impl Ord for ExactRational { + fn cmp(&self, other: &Self) -> Ordering { + match (self.numerator.signum(), other.numerator.signum()) { + (left, right) if left != right => left.cmp(&right), + (0, 0) => Ordering::Equal, + (1, 1) => compare_positive( + self.numerator as u128, + self.denominator as u128, + other.numerator as u128, + other.denominator as u128, + ), + (-1, -1) => compare_positive( + self.numerator.unsigned_abs(), + self.denominator as u128, + other.numerator.unsigned_abs(), + other.denominator as u128, + ) + .reverse(), + _ => unreachable!("i128 signum has only -1, 0, and 1"), + } + } +} + +impl PartialOrd for ExactRational { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl fmt::Display for ExactRational { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(formatter, "{}/{}", self.numerator, self.denominator) + } +} + +impl FromStr for ExactRational { + type Err = RusticolError; + + fn from_str(value: &str) -> Result { + let (numerator, denominator) = value + .split_once('/') + .ok_or_else(|| invalid("exact rational must use numerator/denominator syntax"))?; + if denominator.contains('/') { + return Err(invalid( + "exact rational must contain exactly one slash separator", + )); + } + Self::parse_parts(numerator, denominator) + } +} + +/// Canonical exact complex rational used by recurrence proof coefficients. +#[derive( + Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, bincode::Encode, bincode::Decode, +)] +pub struct ExactComplexRational { + real: ExactRational, + imag: ExactRational, +} + +impl ExactComplexRational { + pub const ZERO: Self = Self { + real: ExactRational::ZERO, + imag: ExactRational::ZERO, + }; + pub const ONE: Self = Self { + real: ExactRational::ONE, + imag: ExactRational::ZERO, + }; + + pub const fn new(real: ExactRational, imag: ExactRational) -> Self { + Self { real, imag } + } + + pub fn parse_parts( + real_numerator: &str, + real_denominator: &str, + imag_numerator: &str, + imag_denominator: &str, + ) -> RusticolResult { + Ok(Self::new( + ExactRational::parse_parts(real_numerator, real_denominator)?, + ExactRational::parse_parts(imag_numerator, imag_denominator)?, + )) + } + + pub const fn real(self) -> ExactRational { + self.real + } + + pub const fn imag(self) -> ExactRational { + self.imag + } + + pub const fn is_zero(self) -> bool { + self.real.is_zero() && self.imag.is_zero() + } + + pub fn checked_neg(self) -> RusticolResult { + Ok(Self::new( + self.real.checked_neg()?, + self.imag.checked_neg()?, + )) + } + + pub fn checked_add(self, right: Self) -> RusticolResult { + Ok(Self::new( + self.real.checked_add(right.real)?, + self.imag.checked_add(right.imag)?, + )) + } + + pub fn checked_sub(self, right: Self) -> RusticolResult { + Ok(Self::new( + self.real.checked_sub(right.real)?, + self.imag.checked_sub(right.imag)?, + )) + } + + pub fn checked_mul(self, right: Self) -> RusticolResult { + let real = self + .real + .checked_mul(right.real)? + .checked_sub(self.imag.checked_mul(right.imag)?)?; + let imag = self + .real + .checked_mul(right.imag)? + .checked_add(self.imag.checked_mul(right.real)?)?; + Ok(Self::new(real, imag)) + } + + pub fn checked_div(self, right: Self) -> RusticolResult { + if right.is_zero() { + return Err(invalid("exact complex rational division by zero")); + } + let denominator = right + .real + .checked_square()? + .checked_add(right.imag.checked_square()?)?; + let real = self + .real + .checked_mul(right.real)? + .checked_add(self.imag.checked_mul(right.imag)?)? + .checked_div(denominator)?; + let imag = self + .imag + .checked_mul(right.real)? + .checked_sub(self.real.checked_mul(right.imag)?)? + .checked_div(denominator)?; + Ok(Self::new(real, imag)) + } + + pub fn conjugate(self) -> RusticolResult { + Ok(Self::new(self.real, self.imag.checked_neg()?)) + } +} + +fn parse_signed_decimal(value: &str, field: &str) -> RusticolResult { + if value.is_empty() || value.trim() != value || value.as_bytes().contains(&b'_') { + return Err(invalid(format!( + "exact rational {field} must be an unseparated decimal integer" + ))); + } + let parsed = value.parse::().map_err(|_| { + invalid(format!( + "exact rational {field} exceeds the dependency-free exact i128 domain" + )) + })?; + if parsed == i128::MIN { + return Err(invalid(format!( + "exact rational {field} exceeds the symmetric dependency-free i128 domain" + ))); + } + Ok(parsed) +} + +fn parse_positive_decimal(value: &str, field: &str) -> RusticolResult { + if value.starts_with('-') { + return Err(invalid(format!("exact rational {field} must be positive"))); + } + let parsed = parse_signed_decimal(value, field)?; + if parsed <= 0 { + return Err(invalid(format!("exact rational {field} must be positive"))); + } + Ok(parsed) +} + +/// Compare positive fractions without cross multiplication, so ordering stays +/// exact even when the cross-products exceed i128/u128. +fn compare_positive( + mut left_numerator: u128, + mut left_denominator: u128, + mut right_numerator: u128, + mut right_denominator: u128, +) -> Ordering { + let mut reverse = false; + loop { + let left_quotient = left_numerator / left_denominator; + let right_quotient = right_numerator / right_denominator; + let quotient_order = left_quotient.cmp(&right_quotient); + if quotient_order != Ordering::Equal { + return if reverse { + quotient_order.reverse() + } else { + quotient_order + }; + } + + let left_remainder = left_numerator % left_denominator; + let right_remainder = right_numerator % right_denominator; + match (left_remainder == 0, right_remainder == 0) { + (true, true) => return Ordering::Equal, + (true, false) => { + return if reverse { + Ordering::Greater + } else { + Ordering::Less + }; + } + (false, true) => { + return if reverse { + Ordering::Less + } else { + Ordering::Greater + }; + } + (false, false) => {} + } + + left_numerator = left_denominator; + left_denominator = left_remainder; + right_numerator = right_denominator; + right_denominator = right_remainder; + reverse = !reverse; + } +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/recurrence/fermion_ordering.rs b/pyamplicol/rust/crates/rusticol-core/src/recurrence/fermion_ordering.rs new file mode 100644 index 0000000000..db49cf59bd --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/recurrence/fermion_ordering.rs @@ -0,0 +1,343 @@ +// SPDX-License-Identifier: 0BSD + +//! Cold construction-time exterior-algebra signs for external fermions. + +use super::ExactComplexRational; +use super::template::{CurrentOrientation, CurrentStateRow, ParticleStatistics}; +use crate::{RusticolError, RusticolResult}; + +fn invalid(message: impl Into) -> RusticolError { + RusticolError::invalid_argument(message) +} + +#[derive(Clone, Debug)] +pub(super) struct FermionOrderingContext { + source_requires_exterior_sign: Box<[bool]>, + active: bool, +} + +impl FermionOrderingContext { + pub(super) fn new(source_requires_exterior_sign: Vec) -> Self { + let active = source_requires_exterior_sign + .iter() + .copied() + .any(|value| value); + Self { + source_requires_exterior_sign: source_requires_exterior_sign.into_boxed_slice(), + active, + } + } + + fn source_requires_exterior_sign(&self, source_slot: u32) -> RusticolResult { + self.source_requires_exterior_sign + .get(source_slot as usize) + .copied() + .ok_or_else(|| invalid("fermion-ordering source slot is absent")) + } +} + +pub(super) fn authenticated_source_requires_exterior_sign( + is_fermionic: bool, + is_authenticated_colored_endpoint: bool, + color_representations: impl IntoIterator, +) -> RusticolResult { + if !is_fermionic { + if is_authenticated_colored_endpoint { + return Err(invalid( + "fermion-ordering endpoint authentication selected a bosonic source", + )); + } + return Ok(false); + } + let mut expected_ownership = None; + for color_representation in color_representations { + let ownership = match color_representation { + 1 => true, + 3 | -3 => false, + value => { + return Err(invalid(format!( + "external-fermion ordering does not support fermion color representation \ + {value}" + ))); + } + }; + if expected_ownership.is_some_and(|expected| expected != ownership) { + return Err(invalid( + "external-fermion ordering source states have mixed color-sign ownership", + )); + } + expected_ownership = Some(ownership); + } + let requires_exterior_sign = expected_ownership.ok_or_else(|| { + invalid("external-fermion ordering fermion source has no authenticated state") + })?; + if is_authenticated_colored_endpoint == requires_exterior_sign { + return Err(invalid(if is_authenticated_colored_endpoint { + "fermion-ordering authenticated colored endpoint has singlet source states" + } else { + "fermion-ordering fundamental source lacks authenticated endpoint ownership" + })); + } + Ok(requires_exterior_sign) +} + +fn external_support_sign( + left_support: &[u32], + right_support: &[u32], + context: &FermionOrderingContext, +) -> RusticolResult { + let mut odd = false; + for left in left_support.iter().copied() { + if !context.source_requires_exterior_sign(left)? { + continue; + } + for right in right_support.iter().copied() { + if left > right && context.source_requires_exterior_sign(right)? { + odd = !odd; + } + } + } + Ok(if odd { -1 } else { 1 }) +} + +fn input_orientation_sign(left: CurrentStateRow, right: CurrentStateRow) -> RusticolResult { + let left_statistics = ParticleStatistics::try_from(left.statistics)?; + let right_statistics = ParticleStatistics::try_from(right.statistics)?; + if left_statistics != ParticleStatistics::Fermion + || right_statistics != ParticleStatistics::Fermion + { + return Ok(1); + } + let left_requires_exterior_sign = match left.color_representation { + 1 => true, + 3 | -3 => false, + value => { + return Err(invalid(format!( + "external-fermion ordering does not support fermion color representation {value}" + ))); + } + }; + let right_requires_exterior_sign = match right.color_representation { + 1 => true, + 3 | -3 => false, + value => { + return Err(invalid(format!( + "external-fermion ordering does not support fermion color representation {value}" + ))); + } + }; + if left_requires_exterior_sign != right_requires_exterior_sign { + return Err(invalid( + "external-fermion ordering encountered mixed color-sign ownership", + )); + } + if !left_requires_exterior_sign { + return Ok(1); + } + let left_orientation = CurrentOrientation::try_from(left.orientation)?; + let right_orientation = CurrentOrientation::try_from(right.orientation)?; + match (left_orientation, right_orientation) { + (CurrentOrientation::Antiparticle, CurrentOrientation::Particle) => Ok(1), + (CurrentOrientation::Particle, CurrentOrientation::Antiparticle) => Ok(-1), + (CurrentOrientation::SelfConjugate, _) | (_, CurrentOrientation::SelfConjugate) => Err( + invalid("external-fermion ordering does not support self-conjugate fermion currents"), + ), + _ => Err(invalid( + "external-fermion ordering requires opposite Dirac orientations", + )), + } +} + +/// Return the exact sign for one construction-ordered binary current merge. +pub(super) fn fermion_ordering_factor( + current_states: &[CurrentStateRow], + parent_state_ids: [u32; 2], + parent_supports: [&[u32]; 2], + context: &FermionOrderingContext, +) -> RusticolResult { + if !context.active { + return Ok(ExactComplexRational::ONE); + } + let left_state = current_states + .get(parent_state_ids[0] as usize) + .copied() + .ok_or_else(|| invalid("fermion-ordering left current state is absent"))?; + let right_state = current_states + .get(parent_state_ids[1] as usize) + .copied() + .ok_or_else(|| invalid("fermion-ordering right current state is absent"))?; + let sign = external_support_sign(parent_supports[0], parent_supports[1], context)? + * input_orientation_sign(left_state, right_state)?; + match sign { + 1 => Ok(ExactComplexRational::ONE), + -1 => ExactComplexRational::ONE.checked_neg(), + _ => unreachable!("the product of two parities is a parity"), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn state( + id: u32, + statistics: ParticleStatistics, + orientation: CurrentOrientation, + color_representation: i32, + ) -> CurrentStateRow { + CurrentStateRow { + id, + template_string_id: id, + particle_id: id as i32, + anti_particle_id: id as i32, + species_string_id: id, + orientation: orientation as u8, + statistics: statistics as u8, + color_representation, + basis_string_id: id, + tensor_ordering_sequence_id: id, + dimension: 1, + chirality: 0, + lc_color_shape_string_id: id, + auxiliary_kind_string_id: u32::MAX, + mass_parameter_id: u32::MAX, + width_parameter_id: u32::MAX, + semantic_digest_id: id, + } + } + + #[test] + fn direct_and_exchanged_dirac_pairings_have_opposite_cached_products() { + let anti = state( + 0, + ParticleStatistics::Fermion, + CurrentOrientation::Antiparticle, + 1, + ); + let particle = state( + 1, + ParticleStatistics::Fermion, + CurrentOrientation::Particle, + 1, + ); + let boson = state( + 2, + ParticleStatistics::Boson, + CurrentOrientation::SelfConjugate, + 1, + ); + let states = [anti, particle, boson]; + let context = FermionOrderingContext::new(vec![true; 4]); + + let direct_first = + fermion_ordering_factor(&states, [0, 1], [&[0], &[1]], &context).unwrap(); + let direct_attach = + fermion_ordering_factor(&states, [2, 0], [&[0, 1], &[2]], &context).unwrap(); + let exchange_first = + fermion_ordering_factor(&states, [1, 0], [&[1], &[2]], &context).unwrap(); + let exchange_attach = + fermion_ordering_factor(&states, [0, 2], [&[0], &[1, 2]], &context).unwrap(); + + assert_eq!( + direct_first.checked_mul(direct_attach).unwrap(), + ExactComplexRational::ONE + ); + assert_eq!( + exchange_first.checked_mul(exchange_attach).unwrap(), + ExactComplexRational::ONE.checked_neg().unwrap() + ); + } + + #[test] + fn bosonic_sources_do_not_participate_in_support_parity() { + let boson = state( + 0, + ParticleStatistics::Boson, + CurrentOrientation::SelfConjugate, + 1, + ); + let context = FermionOrderingContext::new(vec![true, true, true, false]); + let factor = fermion_ordering_factor(&[boson], [0, 0], [&[3], &[0]], &context).unwrap(); + assert_eq!(factor, ExactComplexRational::ONE); + } + + #[test] + fn authenticated_source_filter_separates_singlets_from_colored_endpoints() { + assert!(authenticated_source_requires_exterior_sign(true, false, [1, 1]).unwrap()); + assert!(!authenticated_source_requires_exterior_sign(true, true, [3, -3]).unwrap()); + assert!(!authenticated_source_requires_exterior_sign(false, false, [8]).unwrap()); + } + + #[test] + fn authenticated_source_filter_fails_closed_on_mixed_or_unauthenticated_color() { + for error in [ + authenticated_source_requires_exterior_sign(true, false, [1, 3]).unwrap_err(), + authenticated_source_requires_exterior_sign(true, false, [3, 3]).unwrap_err(), + authenticated_source_requires_exterior_sign(true, true, [1, 1]).unwrap_err(), + authenticated_source_requires_exterior_sign(true, false, [8, 8]).unwrap_err(), + ] { + assert!( + error.to_string().contains("fermion-ordering") + || error.to_string().contains("external-fermion ordering") + ); + } + } + + #[test] + fn colored_open_line_orientation_is_not_counted_twice() { + let anti = state( + 0, + ParticleStatistics::Fermion, + CurrentOrientation::Antiparticle, + -3, + ); + let particle = state( + 1, + ParticleStatistics::Fermion, + CurrentOrientation::Particle, + 3, + ); + let context = FermionOrderingContext::new(vec![false, false]); + let factor = + fermion_ordering_factor(&[anti, particle], [1, 0], [&[1], &[0]], &context).unwrap(); + assert_eq!(factor, ExactComplexRational::ONE); + } + + #[test] + fn mixed_current_ownership_fails_during_construction() { + let singlet = state( + 0, + ParticleStatistics::Fermion, + CurrentOrientation::Antiparticle, + 1, + ); + let colored = state( + 1, + ParticleStatistics::Fermion, + CurrentOrientation::Particle, + 3, + ); + let context = FermionOrderingContext::new(vec![true, false]); + let error = fermion_ordering_factor(&[singlet, colored], [0, 1], [&[0], &[1]], &context) + .unwrap_err(); + assert!(error.to_string().contains("mixed color-sign ownership")); + } + + #[test] + fn same_dirac_orientation_fails_during_construction() { + for orientation in [ + CurrentOrientation::Particle, + CurrentOrientation::Antiparticle, + ] { + let state = state(0, ParticleStatistics::Fermion, orientation, 1); + let context = FermionOrderingContext::new(vec![true, true]); + let error = + fermion_ordering_factor(&[state], [0, 0], [&[0], &[1]], &context).unwrap_err(); + assert!( + error + .to_string() + .contains("requires opposite Dirac orientations") + ); + } + } +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/recurrence/helicity_support.rs b/pyamplicol/rust/crates/rusticol-core/src/recurrence/helicity_support.rs new file mode 100644 index 0000000000..3cd23011e2 --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/recurrence/helicity_support.rs @@ -0,0 +1,1077 @@ +// SPDX-License-Identifier: 0BSD + +//! Exact resolved-helicity support for one physical all-flow recurrence. +//! +//! All-flow construction deliberately erases source-helicity ancestry from +//! current identity. That makes the semantic graph compact, but executing +//! every retained row for every resolved helicity loses the principal warm +//! benefit of the concrete on-the-fly schedules. This module recovers the +//! exact row support without rebuilding one graph per helicity: +//! +//! 1. enumerate the all-flow source-dispatch Cartesian product; +//! 2. propagate availability forward, admitting a transition/closure only +//! when every dynamic source parent has the selected crossed spin required +//! by its authenticated quantum-flow witness; and +//! 3. propagate demand backwards from physically materialized closure rows. +//! +//! The result is semantic and independent of DirectPlan row ordering. Direct +//! lowering maps these masks through its deterministic row reordering and +//! interns the final sidecar domains once. + +use std::collections::BTreeMap; + +use super::template::{MISSING_U32, ValidatedRecurrenceTemplateInput}; +use super::{ + CurrentSourceBinding, RecurrenceCurrent, RecurrenceNodeKind, RecurrenceProgram, + RecurrenceStrategy, +}; +use crate::{RusticolError, RusticolResult}; + +fn invalid(message: impl Into) -> RusticolError { + RusticolError::invalid_argument(format!( + "recurrence all-flow helicity support: {}", + message.into() + )) +} + +/// One temporary fixed-width mask over the auxiliary all-flow plan's own +/// canonical resolved-helicity axis. +/// +/// These masks intentionally retain equal widths while semantic projection is +/// in progress. The direct helicity-dispatch lowering is the sole authority +/// which compacts and interns the persisted domains. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct AllFlowHelicityMask { + words: Box<[u64]>, +} + +impl AllFlowHelicityMask { + pub fn words(&self) -> &[u64] { + &self.words + } + + pub fn contains(&self, resolved_helicity_id: u32) -> bool { + let word = resolved_helicity_id as usize / u64::BITS as usize; + let bit = resolved_helicity_id % u64::BITS; + self.words + .get(word) + .is_some_and(|value| value & (1_u64 << bit) != 0) + } + + pub fn is_empty(&self) -> bool { + self.words.iter().all(|word| *word == 0) + } +} + +/// Semantic support masks for one physical [`RecurrenceStrategy::AllFlowUnion`] +/// program. +/// +/// Table indices are the corresponding dense semantic IDs. `current_demand` +/// is the final live-current support after backward projection; for source +/// currents it is also the exact source-row support. Contribution masks have +/// already been intersected with result demand, and finalization masks equal +/// their owning current's final demand. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct AllFlowHelicitySupportProjection { + resolved_helicity_count: u32, + current_availability: Box<[AllFlowHelicityMask]>, + current_demand: Box<[AllFlowHelicityMask]>, + contribution_support: Box<[AllFlowHelicityMask]>, + finalization_support: Box<[AllFlowHelicityMask]>, + closure_support: Box<[AllFlowHelicityMask]>, +} + +impl AllFlowHelicitySupportProjection { + pub const fn resolved_helicity_count(&self) -> u32 { + self.resolved_helicity_count + } + + pub fn current_availability(&self) -> &[AllFlowHelicityMask] { + &self.current_availability + } + + pub fn current_demand(&self) -> &[AllFlowHelicityMask] { + &self.current_demand + } + + pub fn contribution_support(&self) -> &[AllFlowHelicityMask] { + &self.contribution_support + } + + pub fn finalization_support(&self) -> &[AllFlowHelicityMask] { + &self.finalization_support + } + + pub fn closure_support(&self) -> &[AllFlowHelicityMask] { + &self.closure_support + } +} + +#[derive(Clone, Copy, Debug)] +struct SourceChoice { + source_state_index: u32, + crossed_spin_state_class: i32, +} + +#[derive(Debug)] +struct SourceAxis { + current_id: u32, + choices: Vec, + spin_domains: BTreeMap>, +} + +fn zero_mask(word_count: usize) -> Vec { + vec![0; word_count] +} + +fn full_mask(resolved_helicity_count: u32, word_count: usize) -> Vec { + let mut words = vec![u64::MAX; word_count]; + if let Some(last) = words.last_mut() + && !resolved_helicity_count.is_multiple_of(u64::BITS) + { + *last = (1_u64 << (resolved_helicity_count % u64::BITS)) - 1; + } + words +} + +fn mask_and_assign(destination: &mut [u64], source: &[u64]) { + debug_assert_eq!(destination.len(), source.len()); + for (destination, source) in destination.iter_mut().zip(source) { + *destination &= *source; + } +} + +fn mask_or_assign(destination: &mut [u64], source: &[u64]) { + debug_assert_eq!(destination.len(), source.len()); + for (destination, source) in destination.iter_mut().zip(source) { + *destination |= *source; + } +} + +fn mask_intersection(left: &[u64], right: &[u64]) -> Vec { + debug_assert_eq!(left.len(), right.len()); + left.iter() + .zip(right) + .map(|(left, right)| left & right) + .collect() +} + +fn boxed_masks(rows: Vec>) -> Box<[AllFlowHelicityMask]> { + rows.into_iter() + .map(|words| AllFlowHelicityMask { + words: words.into_boxed_slice(), + }) + .collect::>() + .into_boxed_slice() +} + +fn only_source_slot(current: &RecurrenceCurrent) -> RusticolResult { + match current.key().support_source_slots() { + [source_slot] => Ok(*source_slot), + _ => Err(invalid(format!( + "source current {} does not own exactly one source slot", + current.id() + ))), + } +} + +fn source_axes(program: &RecurrenceProgram) -> RusticolResult<(Vec, u32, usize)> { + let source_count = program + .currents() + .iter() + .filter(|current| current.is_source()) + .count(); + if source_count == 0 { + return Err(invalid("all-flow program has no source currents")); + } + let mut axes = (0..source_count) + .map(|_| None) + .collect::>>(); + for current in program + .currents() + .iter() + .filter(|current| current.is_source()) + { + let source_slot = only_source_slot(current)?; + let axis = axes.get_mut(source_slot as usize).ok_or_else(|| { + invalid(format!( + "source current {} uses non-dense source slot {source_slot}", + current.id() + )) + })?; + if axis.is_some() { + return Err(invalid(format!( + "source slot {source_slot} has more than one all-flow dispatch current" + ))); + } + let CurrentSourceBinding::RuntimeDispatch { + variant_bindings, .. + } = current.key().source_binding() + else { + return Err(invalid(format!( + "source current {} has no runtime dispatch binding", + current.id() + ))); + }; + if variant_bindings.is_empty() { + return Err(invalid(format!( + "source current {} has no runtime source choices", + current.id() + ))); + } + let mut choices = variant_bindings + .iter() + .map(|binding| SourceChoice { + source_state_index: binding.source_state_index(), + crossed_spin_state_class: binding.crossed_spin_state_class(), + }) + .collect::>(); + if choices.iter().any(|choice| { + choice.crossed_spin_state_class == super::DYNAMIC_UNION_SOURCE_SPIN_STATE_CLASS + }) { + return Err(invalid(format!( + "source slot {source_slot} has a non-concrete crossed-spin choice" + ))); + } + choices.sort_by_key(|choice| choice.source_state_index); + if choices + .windows(2) + .any(|pair| pair[0].source_state_index == pair[1].source_state_index) + { + return Err(invalid(format!( + "source slot {source_slot} repeats a source-state choice" + ))); + } + *axis = Some(SourceAxis { + current_id: current.id(), + choices, + spin_domains: BTreeMap::new(), + }); + } + let mut axes = axes + .into_iter() + .enumerate() + .map(|(source_slot, axis)| { + axis.ok_or_else(|| invalid(format!("source slot {source_slot} is absent"))) + }) + .collect::>>()?; + + let cartesian_count = axes.iter().try_fold(1_u64, |count, axis| { + count + .checked_mul( + u64::try_from(axis.choices.len()) + .map_err(|_| invalid("runtime source-choice count exceeds u64"))?, + ) + .ok_or_else(|| invalid("resolved-helicity Cartesian product exceeds u64")) + })?; + if cartesian_count != program.retained_helicity_count() { + return Err(invalid(format!( + "runtime source choices span {cartesian_count} helicities, expected {}", + program.retained_helicity_count() + ))); + } + let resolved_helicity_count = u32::try_from(cartesian_count) + .map_err(|_| invalid("resolved-helicity count exceeds the u32 sidecar domain"))?; + let word_count = usize::try_from(resolved_helicity_count.div_ceil(u64::BITS)) + .map_err(|_| invalid("resolved-helicity mask width exceeds usize"))?; + + for axis in &mut axes { + for choice in &axis.choices { + axis.spin_domains + .entry(choice.crossed_spin_state_class) + .or_insert_with(|| zero_mask(word_count)); + } + } + let mut indices = vec![0usize; axes.len()]; + for helicity_id in 0..resolved_helicity_count { + for (axis, choice_index) in axes.iter_mut().zip(&indices) { + let choice = axis.choices[*choice_index]; + let words = axis + .spin_domains + .get_mut(&choice.crossed_spin_state_class) + .expect("source spin domain was reserved"); + words[helicity_id as usize / u64::BITS as usize] |= 1_u64 << (helicity_id % u64::BITS); + } + for source_slot in (0..indices.len()).rev() { + indices[source_slot] += 1; + if indices[source_slot] < axes[source_slot].choices.len() { + break; + } + indices[source_slot] = 0; + } + } + Ok((axes, resolved_helicity_count, word_count)) +} + +fn template_u32_sequence<'a>( + templates: &'a ValidatedRecurrenceTemplateInput, + sequence_id: u32, + label: &str, +) -> RusticolResult<&'a [u32]> { + let input = templates.input(); + let range = input + .u32_sequence_ranges + .get(sequence_id as usize) + .filter(|range| range.id == sequence_id) + .ok_or_else(|| invalid(format!("{label} sequence {sequence_id} is absent")))?; + let range = range + .range + .as_usize_range(input.u32_sequence_values.len(), label)?; + Ok(&input.u32_sequence_values[range]) +} + +fn template_i32_sequence<'a>( + templates: &'a ValidatedRecurrenceTemplateInput, + sequence_id: u32, + label: &str, +) -> RusticolResult<&'a [i32]> { + let input = templates.input(); + let range = input + .i32_sequence_ranges + .get(sequence_id as usize) + .filter(|range| range.id == sequence_id) + .ok_or_else(|| invalid(format!("{label} sequence {sequence_id} is absent")))?; + let range = range + .range + .as_usize_range(input.i32_sequence_values.len(), label)?; + Ok(&input.i32_sequence_values[range]) +} + +fn quantum_flow_contract( + templates: &ValidatedRecurrenceTemplateInput, + quantum_flow_id: u32, +) -> RusticolResult<(&[u32], &[i32])> { + let flow = templates + .input() + .quantum_flows + .get(quantum_flow_id as usize) + .filter(|flow| flow.id == quantum_flow_id) + .ok_or_else(|| invalid(format!("quantum flow {quantum_flow_id} is absent")))?; + let states = template_u32_sequence( + templates, + flow.input_state_sequence_id, + "quantum-flow input state", + )?; + let spins = template_i32_sequence( + templates, + flow.input_spin_sequence_id, + "quantum-flow input spin", + )?; + if states.len() != 2 || spins.len() != 2 { + return Err(invalid(format!( + "quantum flow {quantum_flow_id} is not binary" + ))); + } + Ok((states, spins)) +} + +fn construction_parent_orders_for_evaluator_tuple( + evaluator_parents: [u32; 2], + parent_states: [u32; 2], + transition_input_states: [u32; 2], + canonical_input_order: [u32; 2], + exchange_is_proven: bool, +) -> Vec<[u32; 2]> { + let mut orders = Vec::new(); + for construction in [ + evaluator_parents, + [evaluator_parents[1], evaluator_parents[0]], + ] { + let construction_states = construction.map(|parent_id| { + if parent_id == evaluator_parents[0] { + parent_states[0] + } else { + parent_states[1] + } + }); + if construction_states != transition_input_states { + continue; + } + let mut evaluator = [ + construction[canonical_input_order[0] as usize], + construction[canonical_input_order[1] as usize], + ]; + if exchange_is_proven && evaluator[1] < evaluator[0] { + evaluator.swap(0, 1); + } + if evaluator == evaluator_parents && !orders.contains(&construction) { + orders.push(construction); + } + } + orders +} + +fn contribution_construction_parent_orders( + program: &RecurrenceProgram, + templates: &ValidatedRecurrenceTemplateInput, + contribution_id: u32, +) -> RusticolResult> { + let contribution = program + .contributions() + .get(contribution_id as usize) + .filter(|row| row.id() == contribution_id) + .ok_or_else(|| invalid(format!("contribution {contribution_id} is absent")))?; + let [first, second] = contribution.parent_current_ids() else { + return Err(invalid(format!( + "contribution {contribution_id} is not binary" + ))); + }; + let transition_id = contribution.key().transition_template_id(); + let transition = templates + .input() + .transitions + .get(transition_id as usize) + .filter(|transition| transition.id == transition_id) + .ok_or_else(|| invalid(format!("transition {transition_id} is absent")))?; + if transition.quantum_flow_template_id != contribution.key().quantum_flow_witness_id() { + return Err(invalid(format!( + "contribution {contribution_id} quantum-flow witness disagrees with transition {transition_id}" + ))); + } + let input_states = template_u32_sequence( + templates, + transition.input_state_sequence_id, + "transition input state", + )?; + let [left_state, right_state] = input_states else { + return Err(invalid(format!("transition {transition_id} is not binary"))); + }; + let canonical_input_order = template_u32_sequence( + templates, + transition.canonical_input_order_sequence_id, + "transition canonical input order", + )?; + let canonical_input_order = match canonical_input_order { + [0, 1] => [0, 1], + [1, 0] => [1, 0], + _ => { + return Err(invalid(format!( + "transition {transition_id} canonical input order is not binary" + ))); + } + }; + let first_state = program.currents()[*first as usize] + .key() + .current_state_template_id(); + let second_state = program.currents()[*second as usize] + .key() + .current_state_template_id(); + let orders = construction_parent_orders_for_evaluator_tuple( + [*first, *second], + [first_state, second_state], + [*left_state, *right_state], + canonical_input_order, + transition.input_exchange_factor_id != MISSING_U32, + ); + if orders.is_empty() { + return Err(invalid(format!( + "contribution {contribution_id} parents cannot reconstruct transition {transition_id} input order" + ))); + } + Ok(orders) +} + +fn flow_gate( + program: &RecurrenceProgram, + source_axis_by_current: &[Option], + source_axes: &[SourceAxis], + parent_ids: [u32; 2], + required_states: &[u32], + required_spins: &[i32], + word_count: usize, +) -> RusticolResult> { + let mut gate = vec![u64::MAX; word_count]; + for (ordinal, parent_id) in parent_ids.into_iter().enumerate() { + let parent = program + .currents() + .get(parent_id as usize) + .ok_or_else(|| invalid(format!("flow parent current {parent_id} is absent")))?; + if parent.key().current_state_template_id() != required_states[ordinal] { + gate.fill(0); + return Ok(gate); + } + if parent.key().node_kind() == RecurrenceNodeKind::Source + && parent.key().spin_state_class() == super::DYNAMIC_UNION_SOURCE_SPIN_STATE_CLASS + { + let axis_id = source_axis_by_current + .get(parent_id as usize) + .copied() + .flatten() + .ok_or_else(|| invalid(format!("source current {parent_id} has no source axis")))?; + let axis = &source_axes[axis_id]; + if let Some(spin_domain) = axis.spin_domains.get(&required_spins[ordinal]) { + mask_and_assign(&mut gate, spin_domain); + } else { + gate.fill(0); + } + } else if parent.key().spin_state_class() != required_spins[ordinal] { + gate.fill(0); + return Ok(gate); + } + } + Ok(gate) +} + +fn parent_availability( + parent_ids: &[u32], + current_availability: &[Vec], + word_count: usize, +) -> RusticolResult> { + let mut availability = vec![u64::MAX; word_count]; + for parent_id in parent_ids { + let parent = current_availability + .get(*parent_id as usize) + .ok_or_else(|| invalid(format!("parent current {parent_id} is absent")))?; + mask_and_assign(&mut availability, parent); + } + Ok(availability) +} + +fn closure_construction_parent_orders( + program: &RecurrenceProgram, + closure_id: u32, +) -> RusticolResult> { + let group = program + .closure_proofs() + .groups() + .iter() + .find(|group| group.emitted_runtime_closure_term_id() == Some(closure_id)) + .ok_or_else(|| invalid(format!("closure term {closure_id} has no proof group")))?; + let range = group.contribution_range().as_usize_range( + program.closure_proofs().contributions().len(), + "closure support proof contribution", + )?; + let mut orders = Vec::new(); + for contribution in &program.closure_proofs().contributions()[range] { + let [left, right] = contribution.construction_parent_runtime_ids() else { + return Err(invalid(format!( + "closure term {closure_id} has a non-binary construction proof" + ))); + }; + let (Some(left), Some(right)) = (*left, *right) else { + return Err(invalid(format!( + "closure term {closure_id} proof lacks runtime parents" + ))); + }; + let order = [left, right]; + if !orders.contains(&order) { + orders.push(order); + } + } + if orders.is_empty() { + return Err(invalid(format!( + "closure term {closure_id} proof group is empty" + ))); + } + Ok(orders) +} + +/// Compute exact per-resolved-helicity support for a physical all-flow graph. +/// +/// This function is deliberately pure and lowering-order independent. It +/// performs no topology replay, no numerical evaluation, and no per-sector +/// recurrence construction. +pub fn project_all_flow_helicity_support( + program: &RecurrenceProgram, + templates: &ValidatedRecurrenceTemplateInput, +) -> RusticolResult { + if program.strategy() != RecurrenceStrategy::AllFlowUnion { + return Err(invalid("projection requires an all-flow-union program")); + } + if !program.replay_targets().is_empty() || !program.resolved_helicities().is_empty() { + return Err(invalid( + "physical all-flow support must not use topology replay or fixed-helicity destinations", + )); + } + if program + .amplitude_destinations() + .iter() + .any(|destination| destination.target_helicity_id().is_some()) + { + return Err(invalid( + "physical all-flow amplitude destinations must remain helicity-parametric", + )); + } + + let (source_axes, resolved_helicity_count, word_count) = source_axes(program)?; + let all_helicities = full_mask(resolved_helicity_count, word_count); + let mut source_axis_by_current = vec![None; program.currents().len()]; + for (axis_id, axis) in source_axes.iter().enumerate() { + source_axis_by_current[axis.current_id as usize] = Some(axis_id); + } + + let mut current_availability = vec![zero_mask(word_count); program.currents().len()]; + let mut contribution_support = vec![zero_mask(word_count); program.contributions().len()]; + for current in program.currents() { + if current.is_source() { + current_availability[current.id() as usize].clone_from(&all_helicities); + continue; + } + let range = current.contribution_range().as_usize_range( + program.contributions().len(), + "current helicity-support contribution", + )?; + let mut result_availability = zero_mask(word_count); + for contribution in &program.contributions()[range] { + let construction_parent_orders = + contribution_construction_parent_orders(program, templates, contribution.id())?; + let (required_states, required_spins) = + quantum_flow_contract(templates, contribution.key().quantum_flow_witness_id())?; + let mut availability = parent_availability( + contribution.parent_current_ids(), + ¤t_availability, + word_count, + )?; + let mut aggregate_gate = zero_mask(word_count); + for construction_parents in construction_parent_orders { + let gate = flow_gate( + program, + &source_axis_by_current, + &source_axes, + construction_parents, + required_states, + required_spins, + word_count, + )?; + mask_or_assign(&mut aggregate_gate, &gate); + } + mask_and_assign(&mut availability, &aggregate_gate); + contribution_support[contribution.id() as usize].clone_from(&availability); + mask_or_assign(&mut result_availability, &availability); + } + current_availability[current.id() as usize] = result_availability; + } + + let mut closure_support = vec![zero_mask(word_count); program.closure_terms().len()]; + let mut current_demand = vec![zero_mask(word_count); program.currents().len()]; + for closure in program.closure_terms() { + let mut support = parent_availability( + closure.parent_current_ids(), + ¤t_availability, + word_count, + )?; + if let Some(flow_id) = closure.quantum_flow_template_id() { + let (required_states, required_spins) = quantum_flow_contract(templates, flow_id)?; + let mut aggregate_gate = zero_mask(word_count); + for construction_parents in closure_construction_parent_orders(program, closure.id())? { + let gate = flow_gate( + program, + &source_axis_by_current, + &source_axes, + construction_parents, + required_states, + required_spins, + word_count, + )?; + mask_or_assign(&mut aggregate_gate, &gate); + } + mask_and_assign(&mut support, &aggregate_gate); + } + closure_support[closure.id() as usize].clone_from(&support); + for parent_id in closure.parent_current_ids() { + mask_or_assign(&mut current_demand[*parent_id as usize], &support); + } + } + + // Current IDs are topological, so reverse traversal always propagates + // demand to parents which have not yet been finalized. + for current in program.currents().iter().rev() { + let current_id = current.id() as usize; + let live = mask_intersection( + ¤t_demand[current_id], + ¤t_availability[current_id], + ); + current_demand[current_id] = live.clone(); + if current.is_source() { + continue; + } + let range = current.contribution_range().as_usize_range( + program.contributions().len(), + "backward helicity-support contribution", + )?; + for contribution in &program.contributions()[range] { + let contribution_id = contribution.id() as usize; + let live_contribution = + mask_intersection(&contribution_support[contribution_id], &live); + contribution_support[contribution_id] = live_contribution.clone(); + for parent_id in contribution.parent_current_ids() { + mask_or_assign(&mut current_demand[*parent_id as usize], &live_contribution); + } + } + } + + let mut finalization_support = vec![zero_mask(word_count); program.finalizations().len()]; + for finalization in program.finalizations() { + finalization_support[finalization.id() as usize] + .clone_from(¤t_demand[finalization.current_id() as usize]); + } + + Ok(AllFlowHelicitySupportProjection { + resolved_helicity_count, + current_availability: boxed_masks(current_availability), + current_demand: boxed_masks(current_demand), + contribution_support: boxed_masks(contribution_support), + finalization_support: boxed_masks(finalization_support), + closure_support: boxed_masks(closure_support), + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::recurrence::layout::RuntimeSourceVariantBinding; + use crate::recurrence::{ + CanonicalMomentumLinearForm, CheckedTableRange, ContributionKey, CurrentCoreKey, + CurrentHelicityIdentity, DynamicLCColorState, DynamicLCColorStateId, ExactComplexRational, + LCColorWitnessTermId, MomentumTerm, RecurrenceAmplitudeDestination, RecurrenceClosureTerm, + RecurrenceContribution, RecurrenceCurrent, RecurrenceFinalization, SemanticDigest, + }; + + fn digest(seed: u8) -> SemanticDigest { + SemanticDigest::new([seed; 32]).unwrap() + } + + fn momentum(slots: &[u32]) -> CanonicalMomentumLinearForm { + CanonicalMomentumLinearForm::new( + slots + .iter() + .map(|source_slot| MomentumTerm { + source_slot: *source_slot, + coefficient: 1, + }) + .collect(), + ) + .unwrap() + } + + fn source_key_with_spins( + slot: u32, + source_spin_state_class: i32, + crossed_spin_state_classes: [i32; 2], + ) -> CurrentCoreKey { + CurrentCoreKey::new( + digest(3), + RecurrenceNodeKind::Source, + 0, + DynamicLCColorStateId::from_interner(slot), + vec![slot], + momentum(&[slot]), + CurrentHelicityIdentity::all_flow_union(source_spin_state_class), + vec![1], + 0, + vec![], + CurrentSourceBinding::runtime_dispatch_with_variants( + 0, + vec![ + RuntimeSourceVariantBinding::new( + 0, + -1, + 0, + 0, + 0, + 0, + crossed_spin_state_classes[0], + ExactComplexRational::ONE, + ) + .unwrap(), + RuntimeSourceVariantBinding::new( + 1, + 1, + 1, + 0, + 0, + 0, + crossed_spin_state_classes[1], + ExactComplexRational::ONE, + ) + .unwrap(), + ], + ) + .unwrap(), + None, + ) + .unwrap() + } + + fn source_key(slot: u32) -> CurrentCoreKey { + source_key_with_spins( + slot, + super::super::DYNAMIC_UNION_SOURCE_SPIN_STATE_CLASS, + [0, 1], + ) + } + + fn internal_key(id: u32, slots: &[u32]) -> CurrentCoreKey { + CurrentCoreKey::new( + digest(3), + RecurrenceNodeKind::Current, + 0, + DynamicLCColorStateId::from_interner(id), + slots.to_vec(), + momentum(slots), + CurrentHelicityIdentity::all_flow_union(0), + vec![1], + 0, + vec![], + CurrentSourceBinding::None, + Some(0), + ) + .unwrap() + } + + fn contribution( + id: u32, + result_id: u32, + parents: [u32; 2], + currents: &[RecurrenceCurrent], + ) -> RecurrenceContribution { + RecurrenceContribution::new( + id, + result_id, + parents.to_vec(), + ContributionKey::new( + 0, + parents.to_vec(), + vec![0, 0], + parents + .iter() + .map(|parent| currents[*parent as usize].key().momentum().clone()) + .collect(), + 0, + 0, + LCColorWitnessTermId::new(0, 0), + digest(6), + 0, + ) + .unwrap(), + ExactComplexRational::ONE, + ) + .unwrap() + } + + fn physical_union_program_with_source_keys( + source_keys: [CurrentCoreKey; 3], + ) -> RecurrenceProgram { + let sources = source_keys + .into_iter() + .enumerate() + .map(|(id, key)| { + RecurrenceCurrent::new(id as u32, key, None, CheckedTableRange::new(0, 0), None) + .unwrap() + }) + .collect::>(); + let mut currents = sources; + currents.push( + RecurrenceCurrent::new( + 3, + internal_key(3, &[0, 1]), + None, + CheckedTableRange::new(0, 1), + Some(0), + ) + .unwrap(), + ); + currents.push( + RecurrenceCurrent::new( + 4, + internal_key(4, &[0, 2]), + None, + CheckedTableRange::new(1, 1), + Some(1), + ) + .unwrap(), + ); + let contributions = vec![ + contribution(0, 3, [0, 1], ¤ts), + contribution(1, 4, [0, 2], ¤ts), + ]; + RecurrenceProgram::new( + RecurrenceStrategy::AllFlowUnion, + 1, + 8, + (0..5) + .map(|id| DynamicLCColorState::new(id, None, vec![]).unwrap()) + .collect(), + currents, + contributions, + vec![ + RecurrenceFinalization::new(0, 3, Some(0), ExactComplexRational::ONE).unwrap(), + RecurrenceFinalization::new(1, 4, Some(0), ExactComplexRational::ONE).unwrap(), + ], + vec![], + vec![], + vec![ + RecurrenceAmplitudeDestination::new(0, 0, None, CheckedTableRange::new(0, 1)) + .unwrap(), + ], + vec![ + RecurrenceClosureTerm::new(0, 0, 0, Some(0), vec![3, 2], ExactComplexRational::ONE) + .unwrap(), + ], + ) + .unwrap() + } + + fn physical_union_program() -> RecurrenceProgram { + physical_union_program_with_source_keys([source_key(0), source_key(1), source_key(2)]) + } + + fn bits(mask: &AllFlowHelicityMask) -> Vec { + (0..8).filter(|id| mask.contains(*id)).collect() + } + + #[test] + fn forward_availability_and_backward_demand_prune_exact_helicity_rows() { + let templates = super::super::validated_template_fixture(); + let projection = + project_all_flow_helicity_support(&physical_union_program(), &templates).unwrap(); + + assert_eq!(projection.resolved_helicity_count(), 8); + assert_eq!(bits(&projection.current_availability()[3]), [0, 1]); + assert_eq!(bits(&projection.current_availability()[4]), [0, 2]); + assert_eq!(bits(&projection.closure_support()[0]), [0]); + + assert_eq!(bits(&projection.current_demand()[0]), [0]); + assert_eq!(bits(&projection.current_demand()[1]), [0]); + assert_eq!(bits(&projection.current_demand()[2]), [0]); + assert_eq!(bits(&projection.current_demand()[3]), [0]); + assert!(projection.current_demand()[4].is_empty()); + assert_eq!(bits(&projection.contribution_support()[0]), [0]); + assert!(projection.contribution_support()[1].is_empty()); + assert_eq!(bits(&projection.finalization_support()[0]), [0]); + assert!(projection.finalization_support()[1].is_empty()); + } + + #[test] + fn fixed_and_dynamic_sources_share_exact_transition_and_open_line_support() { + let templates = super::super::validated_template_fixture(); + let program = physical_union_program_with_source_keys([ + source_key_with_spins(0, 0, [0, 1]), + source_key(1), + source_key_with_spins(2, 0, [0, 1]), + ]); + let projection = project_all_flow_helicity_support(&program, &templates).unwrap(); + + assert_eq!(projection.resolved_helicity_count(), 8); + // Source 0 is a fixed full-state representation even though its + // embedded source-evaluator variants retain distinct crossed spins. + // Source 1 is dynamically gated by the transition witness, while + // source 2 is unconstrained until closure. + assert_eq!(bits(&projection.current_availability()[3]), [0, 1, 4, 5]); + // Both source parents of this otherwise dead current are fixed. + assert_eq!( + bits(&projection.current_availability()[4]), + (0..8).collect::>() + ); + // The physical closure owns source 2 directly, exercising the open-line + // source-parent path without applying a dynamic mask to its fixed spin. + assert_eq!(bits(&projection.closure_support()[0]), [0, 1, 4, 5]); + assert_eq!(bits(&projection.current_demand()[0]), [0, 1, 4, 5]); + assert_eq!(bits(&projection.current_demand()[1]), [0, 1, 4, 5]); + assert_eq!(bits(&projection.current_demand()[2]), [0, 1, 4, 5]); + assert_eq!(bits(&projection.current_demand()[3]), [0, 1, 4, 5]); + assert!(projection.current_demand()[4].is_empty()); + } + + #[test] + fn source_axis_rejects_a_reserved_dynamic_crossed_spin_choice() { + let templates = super::super::validated_template_fixture(); + let program = physical_union_program_with_source_keys([ + source_key_with_spins( + 0, + super::super::DYNAMIC_UNION_SOURCE_SPIN_STATE_CLASS, + [0, super::super::DYNAMIC_UNION_SOURCE_SPIN_STATE_CLASS], + ), + source_key(1), + source_key(2), + ]); + let error = project_all_flow_helicity_support(&program, &templates) + .unwrap_err() + .to_string(); + assert!( + error.contains("source slot 0 has a non-concrete crossed-spin choice"), + "{error}" + ); + } + + #[test] + fn equal_state_evaluator_order_is_inverted_before_unequal_spin_gating() { + let program = physical_union_program(); + let (source_axes, resolved_helicity_count, word_count) = source_axes(&program).unwrap(); + let mut source_axis_by_current = vec![None; program.currents().len()]; + for (axis_id, axis) in source_axes.iter().enumerate() { + source_axis_by_current[axis.current_id as usize] = Some(axis_id); + } + + // Construction [0, 1] is persisted as evaluator [1, 0] by a reversed + // canonical input order. Quantum-flow spins remain in construction + // order and therefore must not be applied directly to the stored row. + let orders = + construction_parent_orders_for_evaluator_tuple([1, 0], [0, 0], [0, 0], [1, 0], false); + assert_eq!(orders, [[0, 1]]); + let gate = flow_gate( + &program, + &source_axis_by_current, + &source_axes, + orders[0], + &[0, 0], + &[0, 1], + word_count, + ) + .unwrap(); + let gate = AllFlowHelicityMask { + words: gate.into_boxed_slice(), + }; + assert_eq!(resolved_helicity_count, 8); + assert_eq!(bits(&gate), [2, 3]); + + // A certified exchange collapses both reciprocal construction orders + // to the same ascending evaluator tuple. The stored row is live on + // the union of both exact spin assignments. + let exchange_orders = + construction_parent_orders_for_evaluator_tuple([0, 1], [0, 0], [0, 0], [0, 1], true); + assert_eq!(exchange_orders, [[0, 1], [1, 0]]); + let mut exchange_gate = zero_mask(word_count); + for order in exchange_orders { + let gate = flow_gate( + &program, + &source_axis_by_current, + &source_axes, + order, + &[0, 0], + &[0, 1], + word_count, + ) + .unwrap(); + mask_or_assign(&mut exchange_gate, &gate); + } + let exchange_gate = AllFlowHelicityMask { + words: exchange_gate.into_boxed_slice(), + }; + assert_eq!(bits(&exchange_gate), [2, 3, 4, 5]); + } + + #[test] + fn projection_rejects_a_noncanonical_resolved_helicity_axis() { + let templates = super::super::validated_template_fixture(); + let base = physical_union_program(); + let program = RecurrenceProgram::new( + RecurrenceStrategy::AllFlowUnion, + base.physical_sector_count(), + 7, + base.dynamic_color_states().to_vec(), + base.currents().to_vec(), + base.contributions().to_vec(), + base.finalizations().to_vec(), + vec![], + vec![], + base.amplitude_destinations().to_vec(), + base.closure_terms().to_vec(), + ) + .unwrap(); + let error = project_all_flow_helicity_support(&program, &templates) + .unwrap_err() + .to_string(); + assert!(error.contains("span 8 helicities, expected 7"), "{error}"); + } +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/recurrence/input.rs b/pyamplicol/rust/crates/rusticol-core/src/recurrence/input.rs new file mode 100644 index 0000000000..3310b8057e --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/recurrence/input.rs @@ -0,0 +1,473 @@ +// SPDX-License-Identifier: 0BSD + +use std::ops::Range; + +use sha2::{Digest, Sha256}; + +use super::{ + RECURRENCE_BUILDER_INPUT_ABI, RECURRENCE_BUILDER_RESULT_ABI, + RECURRENCE_CONTRACTED_COLOR_CAPABILITY, RECURRENCE_INPUT_ENDIANNESS, + RECURRENCE_LC_COLOR_CAPABILITY, RECURRENCE_PLAN_ABI, RECURRENCE_RUNTIME_CAPABILITY, + RECURRENCE_RUNTIME_KIND, RECURRENCE_RUNTIME_LAYOUT_ABI, RECURRENCE_TEMPLATE_ABI, + RecurrenceStrategy, SemanticDigest, +}; +use crate::{RusticolError, RusticolResult}; + +fn invalid(message: impl Into) -> RusticolError { + RusticolError::invalid_argument(message) +} + +/// Checked offset/count pair used by all flat recurrence catalogs. +#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub struct CheckedTableRange { + pub start: u64, + pub count: u64, +} + +impl CheckedTableRange { + pub const fn new(start: u64, count: u64) -> Self { + Self { start, count } + } + + pub fn end(self, label: &str) -> RusticolResult { + self.start.checked_add(self.count).ok_or_else(|| { + invalid(format!( + "{label} range start {} plus count {} exceeds u64", + self.start, self.count + )) + }) + } + + pub fn as_usize_range(self, value_len: usize, label: &str) -> RusticolResult> { + let end = self.end(label)?; + let start = checked_usize(self.start, label)?; + let end = checked_usize(end, label)?; + if end > value_len { + return Err(invalid(format!( + "{label} range {}..{} exceeds table length {value_len}", + self.start, + self.start + self.count + ))); + } + Ok(start..end) + } +} + +/// Borrowed canonical multiword-mask catalog. +/// +/// Words are little-endian by significance. Zero is encoded as an empty +/// range and nonzero masks must not contain a trailing zero word. +#[derive(Clone, Copy, Debug)] +pub struct MultiwordMaskCatalogView<'a> { + pub ranges: &'a [CheckedTableRange], + pub populations: &'a [u64], + pub words: &'a [u64], +} + +impl<'a> MultiwordMaskCatalogView<'a> { + pub fn validate(self, require_strict_order: bool) -> RusticolResult<()> { + validate_equal_column_lengths( + "multiword mask catalog", + &[ + ("ranges", self.ranges.len()), + ("populations", self.populations.len()), + ], + )?; + validate_packed_ranges("multiword mask", self.ranges, self.words.len())?; + + let mut previous: Option<&[u64]> = None; + for (index, (range, expected_population)) in self + .ranges + .iter() + .copied() + .zip(self.populations.iter().copied()) + .enumerate() + { + let words = &self.words + [range.as_usize_range(self.words.len(), &format!("multiword mask {index}"))?]; + if words.last() == Some(&0) { + return Err(invalid(format!( + "multiword mask {index} has a noncanonical trailing zero word" + ))); + } + let population = words.iter().try_fold(0_u64, |total, word| { + total + .checked_add(u64::from(word.count_ones())) + .ok_or_else(|| invalid("multiword mask population exceeds u64")) + })?; + if population != expected_population { + return Err(invalid(format!( + "multiword mask {index} declares population {expected_population}, found {population}" + ))); + } + if require_strict_order + && let Some(previous) = previous + && compare_masks(previous, words) != std::cmp::Ordering::Less + { + return Err(invalid(format!( + "multiword mask catalog is not in strict canonical order at row {index}" + ))); + } + previous = Some(words); + } + Ok(()) + } + + pub fn words(self, mask_id: u32) -> RusticolResult<&'a [u64]> { + let row = self.ranges.get(mask_id as usize).ok_or_else(|| { + invalid(format!( + "multiword mask id {mask_id} exceeds catalog length {}", + self.ranges.len() + )) + })?; + let range = row.as_usize_range(self.words.len(), "multiword mask")?; + Ok(&self.words[range]) + } + + pub fn contains(self, mask_id: u32, bit: u64) -> RusticolResult { + let words = self.words(mask_id)?; + let word_index = checked_usize(bit / 64, "multiword mask bit index")?; + Ok(words + .get(word_index) + .is_some_and(|word| word & (1_u64 << (bit % 64)) != 0)) + } +} + +/// A named, fixed-row-width little-endian builder-input section. +#[derive(Clone, Copy, Debug)] +pub struct CanonicalInputSection<'a> { + pub name: &'a str, + pub row_width: u32, + pub row_count: u64, + pub bytes: &'a [u8], +} + +impl CanonicalInputSection<'_> { + pub fn validate(self) -> RusticolResult<()> { + validate_section_name(self.name)?; + if self.row_width == 0 { + return Err(invalid(format!( + "recurrence input section {:?} has zero row width", + self.name + ))); + } + let expected = u64::from(self.row_width) + .checked_mul(self.row_count) + .ok_or_else(|| { + invalid(format!( + "recurrence input section {:?} byte count exceeds u64", + self.name + )) + })?; + let actual = checked_u64_len(self.bytes.len(), self.name)?; + if actual != expected { + return Err(invalid(format!( + "recurrence input section {:?} expects {expected} bytes, found {actual}", + self.name + ))); + } + Ok(()) + } +} + +/// Deterministic header for `pyamplicol-recurrence-builder-input-v2`. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct RecurrenceBuilderInputHeader { + pub template_abi: String, + pub builder_input_abi: String, + pub builder_result_abi: String, + pub plan_abi: String, + pub runtime_layout_abi: String, + pub runtime_kind: String, + pub runtime_capability: String, + pub color_capability: String, + pub endianness: String, + pub strategy: RecurrenceStrategy, + pub section_count: u32, + pub template_catalog_digest: SemanticDigest, + pub process_digest: SemanticDigest, + pub semantic_digest: SemanticDigest, + pub input_digest: SemanticDigest, +} + +impl RecurrenceBuilderInputHeader { + pub fn canonical( + strategy: RecurrenceStrategy, + section_count: u32, + template_catalog_digest: SemanticDigest, + process_digest: SemanticDigest, + semantic_digest: SemanticDigest, + input_digest: SemanticDigest, + ) -> Self { + Self { + template_abi: RECURRENCE_TEMPLATE_ABI.to_owned(), + builder_input_abi: RECURRENCE_BUILDER_INPUT_ABI.to_owned(), + builder_result_abi: RECURRENCE_BUILDER_RESULT_ABI.to_owned(), + plan_abi: RECURRENCE_PLAN_ABI.to_owned(), + runtime_layout_abi: RECURRENCE_RUNTIME_LAYOUT_ABI.to_owned(), + runtime_kind: RECURRENCE_RUNTIME_KIND.to_owned(), + runtime_capability: RECURRENCE_RUNTIME_CAPABILITY.to_owned(), + color_capability: match strategy { + RecurrenceStrategy::ContractedColorUnion => RECURRENCE_CONTRACTED_COLOR_CAPABILITY, + RecurrenceStrategy::TopologyReplay | RecurrenceStrategy::AllFlowUnion => { + RECURRENCE_LC_COLOR_CAPABILITY + } + } + .to_owned(), + endianness: RECURRENCE_INPUT_ENDIANNESS.to_owned(), + strategy, + section_count, + template_catalog_digest, + process_digest, + semantic_digest, + input_digest, + } + } + + pub fn validate_identity(&self) -> RusticolResult<()> { + validate_constant("template ABI", &self.template_abi, RECURRENCE_TEMPLATE_ABI)?; + validate_constant( + "builder input ABI", + &self.builder_input_abi, + RECURRENCE_BUILDER_INPUT_ABI, + )?; + validate_constant( + "builder result ABI", + &self.builder_result_abi, + RECURRENCE_BUILDER_RESULT_ABI, + )?; + validate_constant("plan ABI", &self.plan_abi, RECURRENCE_PLAN_ABI)?; + validate_constant( + "runtime layout ABI", + &self.runtime_layout_abi, + RECURRENCE_RUNTIME_LAYOUT_ABI, + )?; + validate_constant("runtime kind", &self.runtime_kind, RECURRENCE_RUNTIME_KIND)?; + validate_constant( + "runtime capability", + &self.runtime_capability, + RECURRENCE_RUNTIME_CAPABILITY, + )?; + let expected_color_capability = match self.strategy { + RecurrenceStrategy::ContractedColorUnion => RECURRENCE_CONTRACTED_COLOR_CAPABILITY, + RecurrenceStrategy::TopologyReplay | RecurrenceStrategy::AllFlowUnion => { + RECURRENCE_LC_COLOR_CAPABILITY + } + }; + validate_constant( + "color capability", + &self.color_capability, + expected_color_capability, + )?; + validate_constant( + "input endianness", + &self.endianness, + RECURRENCE_INPUT_ENDIANNESS, + )?; + Ok(()) + } +} + +pub fn checked_u32_len(length: usize, label: &str) -> RusticolResult { + u32::try_from(length).map_err(|_| invalid(format!("{label} length {length} exceeds u32"))) +} + +pub fn checked_u64_len(length: usize, label: &str) -> RusticolResult { + u64::try_from(length).map_err(|_| invalid(format!("{label} length {length} exceeds u64"))) +} + +pub fn checked_usize(value: u64, label: &str) -> RusticolResult { + usize::try_from(value).map_err(|_| invalid(format!("{label} value {value} exceeds usize"))) +} + +pub fn validate_equal_column_lengths( + table: &str, + columns: &[(&str, usize)], +) -> RusticolResult { + let Some((first_name, expected)) = columns.first().copied() else { + return Err(invalid(format!("{table} has no columns"))); + }; + checked_u32_len(expected, &format!("{table}.{first_name}"))?; + for (name, length) in columns.iter().copied().skip(1) { + checked_u32_len(length, &format!("{table}.{name}"))?; + if length != expected { + return Err(invalid(format!( + "{table} column {name:?} has length {length}, expected {expected} from {first_name:?}" + ))); + } + } + Ok(expected) +} + +pub fn validate_u32_references( + values: &[u32], + target_len: usize, + label: &str, +) -> RusticolResult<()> { + checked_u32_len(target_len, &format!("{label} target"))?; + for (row, value) in values.iter().copied().enumerate() { + let value_index = usize::try_from(value).map_err(|_| { + invalid(format!( + "{label} row {row} id {value} exceeds usize on this platform" + )) + })?; + if value_index >= target_len { + return Err(invalid(format!( + "{label} row {row} references id {value}, target length is {target_len}" + ))); + } + } + Ok(()) +} + +pub fn validate_ranges_within( + label: &str, + ranges: &[CheckedTableRange], + value_len: usize, +) -> RusticolResult<()> { + checked_u32_len(ranges.len(), &format!("{label} ranges"))?; + for (index, range) in ranges.iter().copied().enumerate() { + range.as_usize_range(value_len, &format!("{label} {index}"))?; + } + Ok(()) +} + +pub fn validate_packed_ranges( + label: &str, + ranges: &[CheckedTableRange], + value_len: usize, +) -> RusticolResult<()> { + validate_ranges_within(label, ranges, value_len)?; + let mut expected_start = 0_u64; + for (index, range) in ranges.iter().copied().enumerate() { + if range.start != expected_start { + return Err(invalid(format!( + "{label} range {index} starts at {}, expected packed offset {expected_start}", + range.start + ))); + } + expected_start = range.end(&format!("{label} {index}"))?; + } + if checked_usize(expected_start, label)? != value_len { + return Err(invalid(format!( + "{label} packed ranges cover {expected_start} values, table contains {value_len}" + ))); + } + Ok(()) +} + +pub fn canonical_input_digest( + header: &RecurrenceBuilderInputHeader, + sections: &[CanonicalInputSection<'_>], +) -> RusticolResult { + header.validate_identity()?; + validate_sections(sections)?; + let actual_section_count = checked_u32_len(sections.len(), "recurrence input sections")?; + if header.section_count != actual_section_count { + return Err(invalid(format!( + "recurrence input header declares {} sections, found {actual_section_count}", + header.section_count + ))); + } + let mut digest = Sha256::new(); + digest_field(&mut digest, RECURRENCE_BUILDER_INPUT_ABI.as_bytes())?; + digest_field(&mut digest, RECURRENCE_TEMPLATE_ABI.as_bytes())?; + digest_field(&mut digest, RECURRENCE_PLAN_ABI.as_bytes())?; + digest_field(&mut digest, RECURRENCE_RUNTIME_LAYOUT_ABI.as_bytes())?; + digest.update(header.strategy.as_u32().to_le_bytes()); + digest.update(header.section_count.to_le_bytes()); + digest.update(header.template_catalog_digest.as_bytes()); + digest.update(header.process_digest.as_bytes()); + digest.update(header.semantic_digest.as_bytes()); + for section in sections { + digest_field(&mut digest, section.name.as_bytes())?; + digest.update(section.row_width.to_le_bytes()); + digest.update(section.row_count.to_le_bytes()); + digest_field(&mut digest, section.bytes)?; + } + SemanticDigest::new(digest.finalize().into()) +} + +pub fn validate_header_and_sections( + header: &RecurrenceBuilderInputHeader, + sections: &[CanonicalInputSection<'_>], +) -> RusticolResult<()> { + header.validate_identity()?; + let actual_count = checked_u32_len(sections.len(), "recurrence input sections")?; + if header.section_count != actual_count { + return Err(invalid(format!( + "recurrence input header declares {} sections, found {actual_count}", + header.section_count + ))); + } + let actual = canonical_input_digest(header, sections)?; + if actual != header.input_digest { + return Err(invalid(format!( + "recurrence input digest mismatch: expected {}, found {}", + header.input_digest, actual + ))); + } + Ok(()) +} + +fn validate_sections(sections: &[CanonicalInputSection<'_>]) -> RusticolResult<()> { + checked_u32_len(sections.len(), "recurrence input sections")?; + let mut previous = None; + for section in sections { + section.validate()?; + if let Some(previous) = previous + && previous >= section.name + { + return Err(invalid(format!( + "recurrence input sections are not in strict canonical name order at {:?}", + section.name + ))); + } + previous = Some(section.name); + } + Ok(()) +} + +fn validate_section_name(name: &str) -> RusticolResult<()> { + if name.is_empty() + || name.len() > 255 + || !name.bytes().all(|byte| { + byte.is_ascii_lowercase() + || byte.is_ascii_digit() + || matches!(byte, b'_' | b'-' | b'.' | b'/') + }) + || name.starts_with('/') + || name.ends_with('/') + || name.contains("//") + || name + .split('/') + .any(|component| component == "." || component == "..") + { + return Err(invalid(format!( + "invalid recurrence input section name {name:?}" + ))); + } + Ok(()) +} + +fn validate_constant(label: &str, value: &str, expected: &str) -> RusticolResult<()> { + if value != expected { + return Err(RusticolError::compatibility(format!( + "unsupported recurrence {label} {value:?}; expected {expected:?}" + ))); + } + Ok(()) +} + +fn digest_field(digest: &mut Sha256, bytes: &[u8]) -> RusticolResult<()> { + let length = checked_u64_len(bytes.len(), "recurrence digest field")?; + digest.update(length.to_le_bytes()); + digest.update(bytes); + Ok(()) +} + +fn compare_masks(left: &[u64], right: &[u64]) -> std::cmp::Ordering { + left.len() + .cmp(&right.len()) + .then_with(|| left.iter().rev().cmp(right.iter().rev())) +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/recurrence/layout.rs b/pyamplicol/rust/crates/rusticol-core/src/recurrence/layout.rs new file mode 100644 index 0000000000..105b237eef --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/recurrence/layout.rs @@ -0,0 +1,765 @@ +// SPDX-License-Identifier: 0BSD + +use std::fmt; + +use super::ExactComplexRational; +use crate::{RusticolError, RusticolResult}; + +fn invalid(message: impl Into) -> RusticolError { + RusticolError::invalid_argument(message) +} + +/// A nonzero SHA-256 semantic digest. +#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, bincode::Encode)] +pub struct SemanticDigest([u8; 32]); + +impl bincode::Decode for SemanticDigest { + fn decode>( + decoder: &mut D, + ) -> Result { + Self::new(bincode::Decode::decode(decoder)?) + .map_err(|error| bincode::error::DecodeError::OtherString(error.to_string())) + } +} + +bincode::impl_borrow_decode!(SemanticDigest); + +impl SemanticDigest { + pub fn new(bytes: [u8; 32]) -> RusticolResult { + if bytes == [0; 32] { + return Err(invalid("semantic digest must not be all zero")); + } + Ok(Self(bytes)) + } + + pub const fn as_bytes(&self) -> &[u8; 32] { + &self.0 + } +} + +impl fmt::Display for SemanticDigest { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + for byte in self.0 { + write!(formatter, "{byte:02x}")?; + } + Ok(()) + } +} + +/// Workload-specific recurrence schedule strategy. +#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +#[repr(u32)] +pub enum RecurrenceStrategy { + TopologyReplay = 0, + AllFlowUnion = 1, + ContractedColorUnion = 2, +} + +impl RecurrenceStrategy { + pub const fn as_u32(self) -> u32 { + self as u32 + } + + pub const fn as_str(self) -> &'static str { + match self { + Self::TopologyReplay => "topology-replay", + Self::AllFlowUnion => "all-flow-union", + Self::ContractedColorUnion => "contracted-color-union", + } + } + + pub const fn uses_runtime_source_dispatch(self) -> bool { + matches!(self, Self::AllFlowUnion) + } + + pub const fn retains_local_source_ancestry(self) -> bool { + !self.uses_runtime_source_dispatch() + } + + pub const fn materializes_all_color_sectors(self) -> bool { + !matches!(self, Self::TopologyReplay) + } + + pub const fn uses_topology_replay_targets(self) -> bool { + matches!(self, Self::TopologyReplay | Self::ContractedColorUnion) + } +} + +impl TryFrom for RecurrenceStrategy { + type Error = RusticolError; + + fn try_from(value: u32) -> Result { + match value { + 0 => Ok(Self::TopologyReplay), + 1 => Ok(Self::AllFlowUnion), + 2 => Ok(Self::ContractedColorUnion), + _ => Err(invalid(format!( + "unknown recurrence strategy discriminant {value}" + ))), + } + } +} + +impl fmt::Display for RecurrenceStrategy { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str(self.as_str()) + } +} + +/// Semantic kind of recurrence state. +#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +#[repr(u32)] +pub enum RecurrenceNodeKind { + Source = 0, + Current = 1, +} + +/// Identifier issued only by the dynamic LC color-state interner. +/// +/// This newtype prevents physical sector IDs from accidentally becoming +/// recurrence current identity. +#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub struct DynamicLCColorStateId(u32); + +impl DynamicLCColorStateId { + pub(crate) const fn from_interner(value: u32) -> Self { + Self(value) + } + + pub const fn get(self) -> u32 { + self.0 + } +} + +/// One exact term of one prepared LC color-transition witness. +#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub struct LCColorWitnessTermId { + color_contraction_template_id: u32, + witness_ordinal: u32, +} + +impl LCColorWitnessTermId { + pub const fn new(color_contraction_template_id: u32, witness_ordinal: u32) -> Self { + Self { + color_contraction_template_id, + witness_ordinal, + } + } + + pub const fn color_contraction_template_id(self) -> u32 { + self.color_contraction_template_id + } + + pub const fn witness_ordinal(self) -> u32 { + self.witness_ordinal + } +} + +/// One source-state choice in a topology-replay current's local ancestry. +#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub struct SourceStateAssignment { + source_slot: u32, + state_index: u32, +} + +impl SourceStateAssignment { + pub const fn new(source_slot: u32, state_index: u32) -> Self { + Self { + source_slot, + state_index, + } + } + + pub const fn source_slot(self) -> u32 { + self.source_slot + } + + pub const fn state_index(self) -> u32 { + self.state_index + } +} + +/// Layout-specific helicity identity of one recurrence current. +#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub enum CurrentHelicityIdentity { + /// Static spin class plus one retained source state for every local source. + TopologyReplay { + spin_state_class: i32, + local_source_states: Box<[SourceStateAssignment]>, + }, + /// Static transition spin class with source helicities selected at runtime. + AllFlowUnion { spin_state_class: i32 }, + /// Static spin class and local source ancestry across the complete color basis. + ContractedColorUnion { + spin_state_class: i32, + local_source_states: Box<[SourceStateAssignment]>, + }, +} + +impl CurrentHelicityIdentity { + pub fn topology_replay( + spin_state_class: i32, + local_source_states: Vec, + ) -> RusticolResult { + validate_sequence_len("local source-state ancestry", local_source_states.len())?; + let source_slots = local_source_states + .iter() + .map(|assignment| assignment.source_slot) + .collect::>(); + validate_strict_u32_sequence("local source-state ancestry", &source_slots)?; + Ok(Self::TopologyReplay { + spin_state_class, + local_source_states: local_source_states.into_boxed_slice(), + }) + } + + pub const fn all_flow_union(spin_state_class: i32) -> Self { + Self::AllFlowUnion { spin_state_class } + } + + pub fn contracted_color_union( + spin_state_class: i32, + local_source_states: Vec, + ) -> RusticolResult { + validate_sequence_len("local source-state ancestry", local_source_states.len())?; + let source_slots = local_source_states + .iter() + .map(|assignment| assignment.source_slot) + .collect::>(); + validate_strict_u32_sequence("local source-state ancestry", &source_slots)?; + Ok(Self::ContractedColorUnion { + spin_state_class, + local_source_states: local_source_states.into_boxed_slice(), + }) + } + + pub const fn strategy(&self) -> RecurrenceStrategy { + match self { + Self::TopologyReplay { .. } => RecurrenceStrategy::TopologyReplay, + Self::AllFlowUnion { .. } => RecurrenceStrategy::AllFlowUnion, + Self::ContractedColorUnion { .. } => RecurrenceStrategy::ContractedColorUnion, + } + } + + pub const fn spin_state_class(&self) -> i32 { + match self { + Self::TopologyReplay { + spin_state_class, .. + } + | Self::AllFlowUnion { spin_state_class } + | Self::ContractedColorUnion { + spin_state_class, .. + } => *spin_state_class, + } + } + + pub fn local_source_states(&self) -> &[SourceStateAssignment] { + match self { + Self::TopologyReplay { + local_source_states, + .. + } => local_source_states, + Self::AllFlowUnion { .. } => &[], + Self::ContractedColorUnion { + local_source_states, + .. + } => local_source_states, + } + } +} + +/// One process-bound runtime source variant for an all-flow-union source. +/// +/// The complete variant catalog belongs to the source dispatch domain. No +/// selected numerical helicity enters the current identity. +#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub struct RuntimeSourceVariantBinding { + source_state_index: u32, + public_helicity: i32, + runtime_variant_id: u32, + source_template_id: u32, + source_state_template_id: u32, + crossed_state_template_id: u32, + crossed_spin_state_class: i32, + crossing_factor: ExactComplexRational, +} + +impl RuntimeSourceVariantBinding { + #[allow(clippy::too_many_arguments)] + pub fn new( + source_state_index: u32, + public_helicity: i32, + runtime_variant_id: u32, + source_template_id: u32, + source_state_template_id: u32, + crossed_state_template_id: u32, + crossed_spin_state_class: i32, + crossing_factor: ExactComplexRational, + ) -> RusticolResult { + if runtime_variant_id == u32::MAX + || source_template_id == u32::MAX + || source_state_template_id == u32::MAX + || crossed_state_template_id == u32::MAX + { + return Err(invalid( + "runtime source variant reserves the u32 missing-value sentinel", + )); + } + if crossing_factor.is_zero() { + return Err(invalid( + "runtime source variant requires a nonzero crossing factor", + )); + } + Ok(Self { + source_state_index, + public_helicity, + runtime_variant_id, + source_template_id, + source_state_template_id, + crossed_state_template_id, + crossed_spin_state_class, + crossing_factor, + }) + } + + pub const fn source_state_index(self) -> u32 { + self.source_state_index + } + + pub const fn public_helicity(self) -> i32 { + self.public_helicity + } + + pub const fn runtime_variant_id(self) -> u32 { + self.runtime_variant_id + } + + pub const fn source_template_id(self) -> u32 { + self.source_template_id + } + + pub const fn source_state_template_id(self) -> u32 { + self.source_state_template_id + } + + pub const fn crossed_state_template_id(self) -> u32 { + self.crossed_state_template_id + } + + pub const fn crossed_spin_state_class(self) -> i32 { + self.crossed_spin_state_class + } + + pub const fn crossing_factor(self) -> ExactComplexRational { + self.crossing_factor + } +} + +/// Source semantics referenced by a recurrence key. +#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub enum CurrentSourceBinding { + None, + FixedTemplate(u32), + RuntimeDispatch { + domain: u32, + source_template_ids: Box<[u32]>, + variant_bindings: Box<[RuntimeSourceVariantBinding]>, + }, +} + +impl CurrentSourceBinding { + pub fn runtime_dispatch(domain: u32, source_template_ids: Vec) -> RusticolResult { + validate_sequence_len( + "runtime source-template variants", + source_template_ids.len(), + )?; + validate_strict_u32_sequence("runtime source-template variants", &source_template_ids)?; + if source_template_ids.contains(&u32::MAX) { + return Err(invalid( + "runtime source-template variants reserve the u32 sentinel", + )); + } + Ok(Self::RuntimeDispatch { + domain, + source_template_ids: source_template_ids.into_boxed_slice(), + variant_bindings: Box::new([]), + }) + } + + pub fn runtime_dispatch_with_variants( + domain: u32, + variants: Vec, + ) -> RusticolResult { + if domain == u32::MAX { + return Err(invalid( + "runtime source-dispatch domain reserves the u32 sentinel", + )); + } + validate_sequence_len("runtime source variants", variants.len())?; + if variants.is_empty() { + return Err(invalid( + "runtime source-dispatch domain requires concrete variants", + )); + } + let state_indices = variants + .iter() + .map(|variant| variant.source_state_index) + .collect::>(); + validate_strict_u32_sequence("runtime source-state variants", &state_indices)?; + let mut runtime_variant_ids = variants + .iter() + .map(|variant| variant.runtime_variant_id) + .collect::>(); + runtime_variant_ids.sort_unstable(); + validate_strict_u32_sequence("runtime-helicity variant IDs", &runtime_variant_ids)?; + let mut source_template_ids = variants + .iter() + .map(|variant| variant.source_template_id) + .collect::>(); + source_template_ids.sort_unstable(); + source_template_ids.dedup(); + validate_strict_u32_sequence("runtime source-template variants", &source_template_ids)?; + Ok(Self::RuntimeDispatch { + domain, + source_template_ids: source_template_ids.into_boxed_slice(), + variant_bindings: variants.into_boxed_slice(), + }) + } +} + +impl TryFrom for RecurrenceNodeKind { + type Error = RusticolError; + + fn try_from(value: u32) -> Result { + match value { + 0 => Ok(Self::Source), + 1 => Ok(Self::Current), + _ => Err(invalid(format!( + "unknown recurrence node-kind discriminant {value}" + ))), + } + } +} + +/// One canonical term in a momentum linear form. +#[derive( + Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, bincode::Encode, bincode::Decode, +)] +pub struct MomentumTerm { + pub source_slot: u32, + pub coefficient: i32, +} + +/// Canonical source-slot-ordered momentum linear form. +#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, bincode::Encode)] +pub struct CanonicalMomentumLinearForm(Box<[MomentumTerm]>); + +impl bincode::Decode for CanonicalMomentumLinearForm { + fn decode>( + decoder: &mut D, + ) -> Result { + Self::new(bincode::Decode::decode(decoder)?) + .map_err(|error| bincode::error::DecodeError::OtherString(error.to_string())) + } +} + +bincode::impl_borrow_decode!(CanonicalMomentumLinearForm); + +impl CanonicalMomentumLinearForm { + pub fn new(terms: Vec) -> RusticolResult { + validate_sequence_len("momentum terms", terms.len())?; + let mut previous = None; + for (index, term) in terms.iter().enumerate() { + if term.coefficient == 0 { + return Err(invalid(format!( + "momentum term {index} has zero coefficient" + ))); + } + if let Some(previous) = previous + && previous >= term.source_slot + { + return Err(invalid(format!( + "momentum terms are not strictly source-slot ordered at row {index}" + ))); + } + previous = Some(term.source_slot); + } + Ok(Self(terms.into_boxed_slice())) + } + + pub fn terms(&self) -> &[MomentumTerm] { + &self.0 + } +} + +/// Exact semantic identity before contribution-vector comparison. +#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub struct CurrentCoreKey { + catalog_digest: SemanticDigest, + node_kind: RecurrenceNodeKind, + current_state_template_id: u32, + dynamic_lc_color_state_id: DynamicLCColorStateId, + support_source_slots: Box<[u32]>, + momentum: CanonicalMomentumLinearForm, + helicity_identity: CurrentHelicityIdentity, + flavour_flow: Box<[i32]>, + quantum_number_flow_id: u32, + coupling_orders: Box<[u32]>, + source_binding: CurrentSourceBinding, + propagator_template_id: Option, +} + +impl CurrentCoreKey { + #[allow(clippy::too_many_arguments)] + pub fn new( + catalog_digest: SemanticDigest, + node_kind: RecurrenceNodeKind, + current_state_template_id: u32, + dynamic_lc_color_state_id: DynamicLCColorStateId, + support_source_slots: Vec, + momentum: CanonicalMomentumLinearForm, + helicity_identity: CurrentHelicityIdentity, + flavour_flow: Vec, + quantum_number_flow_id: u32, + coupling_orders: Vec, + source_binding: CurrentSourceBinding, + propagator_template_id: Option, + ) -> RusticolResult { + validate_sequence_len("support source slots", support_source_slots.len())?; + validate_sequence_len("flavour flow", flavour_flow.len())?; + validate_sequence_len("coupling orders", coupling_orders.len())?; + validate_strict_u32_sequence("support source slots", &support_source_slots)?; + let ancestry_slots = helicity_identity + .local_source_states() + .iter() + .map(|assignment| assignment.source_slot) + .collect::>(); + if helicity_identity.strategy().retains_local_source_ancestry() + && ancestry_slots != support_source_slots + { + return Err(invalid( + "topology-replay helicity ancestry must cover every local source slot", + )); + } + match (node_kind, helicity_identity.strategy(), &source_binding) { + ( + RecurrenceNodeKind::Source, + RecurrenceStrategy::TopologyReplay, + CurrentSourceBinding::FixedTemplate(_), + ) + | ( + RecurrenceNodeKind::Source, + RecurrenceStrategy::ContractedColorUnion, + CurrentSourceBinding::FixedTemplate(_), + ) + | ( + RecurrenceNodeKind::Source, + RecurrenceStrategy::AllFlowUnion, + CurrentSourceBinding::RuntimeDispatch { .. }, + ) + | (RecurrenceNodeKind::Current, _, CurrentSourceBinding::None) => {} + (RecurrenceNodeKind::Source, RecurrenceStrategy::TopologyReplay, _) => { + return Err(invalid( + "topology-replay source requires one fixed source template", + )); + } + (RecurrenceNodeKind::Source, RecurrenceStrategy::ContractedColorUnion, _) => { + return Err(invalid( + "contracted-color-union source requires one fixed source template", + )); + } + (RecurrenceNodeKind::Source, RecurrenceStrategy::AllFlowUnion, _) => { + return Err(invalid( + "all-flow-union source requires a runtime dispatch domain", + )); + } + (RecurrenceNodeKind::Current, _, _) => { + return Err(invalid( + "non-source recurrence key must not carry source binding state", + )); + } + } + if node_kind == RecurrenceNodeKind::Source && support_source_slots.len() != 1 { + return Err(invalid( + "source recurrence key must cover exactly one external source slot", + )); + } + Ok(Self { + catalog_digest, + node_kind, + current_state_template_id, + dynamic_lc_color_state_id, + support_source_slots: support_source_slots.into_boxed_slice(), + momentum, + helicity_identity, + flavour_flow: flavour_flow.into_boxed_slice(), + quantum_number_flow_id, + coupling_orders: coupling_orders.into_boxed_slice(), + source_binding, + propagator_template_id, + }) + } + + pub const fn catalog_digest(&self) -> SemanticDigest { + self.catalog_digest + } + + pub const fn node_kind(&self) -> RecurrenceNodeKind { + self.node_kind + } + + pub const fn current_state_template_id(&self) -> u32 { + self.current_state_template_id + } + + pub const fn dynamic_lc_color_state_id(&self) -> DynamicLCColorStateId { + self.dynamic_lc_color_state_id + } + + pub fn support_source_slots(&self) -> &[u32] { + &self.support_source_slots + } + + pub const fn momentum(&self) -> &CanonicalMomentumLinearForm { + &self.momentum + } + + pub const fn helicity_identity(&self) -> &CurrentHelicityIdentity { + &self.helicity_identity + } + + pub const fn spin_state_class(&self) -> i32 { + self.helicity_identity.spin_state_class() + } + + pub fn flavour_flow(&self) -> &[i32] { + &self.flavour_flow + } + + pub const fn quantum_number_flow_id(&self) -> u32 { + self.quantum_number_flow_id + } + + pub fn coupling_orders(&self) -> &[u32] { + &self.coupling_orders + } + + pub const fn source_binding(&self) -> &CurrentSourceBinding { + &self.source_binding + } + + pub const fn propagator_template_id(&self) -> Option { + self.propagator_template_id + } +} + +/// Exact identity of one contribution before coefficient aggregation. +#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub struct ContributionKey { + transition_template_id: u32, + parent_value_class_ids: Box<[u32]>, + parent_state_template_ids: Box<[u32]>, + parent_momenta: Box<[CanonicalMomentumLinearForm]>, + result_state_template_id: u32, + quantum_flow_witness_id: u32, + color_witness_term_id: LCColorWitnessTermId, + runtime_coupling_binding_digest: SemanticDigest, + output_projection_id: u32, +} + +impl ContributionKey { + #[allow(clippy::too_many_arguments)] + pub fn new( + transition_template_id: u32, + parent_value_class_ids: Vec, + parent_state_template_ids: Vec, + parent_momenta: Vec, + result_state_template_id: u32, + quantum_flow_witness_id: u32, + color_witness_term_id: LCColorWitnessTermId, + runtime_coupling_binding_digest: SemanticDigest, + output_projection_id: u32, + ) -> RusticolResult { + let parent_count = parent_value_class_ids.len(); + if parent_count == 0 { + return Err(invalid( + "recurrence contribution requires at least one parent", + )); + } + if parent_state_template_ids.len() != parent_count || parent_momenta.len() != parent_count { + return Err(invalid(format!( + "recurrence contribution parent columns have lengths {parent_count}, {}, and {}", + parent_state_template_ids.len(), + parent_momenta.len() + ))); + } + u32::try_from(parent_count) + .map_err(|_| invalid("recurrence contribution parent count exceeds u32"))?; + Ok(Self { + transition_template_id, + parent_value_class_ids: parent_value_class_ids.into_boxed_slice(), + parent_state_template_ids: parent_state_template_ids.into_boxed_slice(), + parent_momenta: parent_momenta.into_boxed_slice(), + result_state_template_id, + quantum_flow_witness_id, + color_witness_term_id, + runtime_coupling_binding_digest, + output_projection_id, + }) + } + + pub const fn transition_template_id(&self) -> u32 { + self.transition_template_id + } + + pub fn parent_value_class_ids(&self) -> &[u32] { + &self.parent_value_class_ids + } + + pub fn parent_state_template_ids(&self) -> &[u32] { + &self.parent_state_template_ids + } + + pub fn parent_momenta(&self) -> &[CanonicalMomentumLinearForm] { + &self.parent_momenta + } + + pub const fn result_state_template_id(&self) -> u32 { + self.result_state_template_id + } + + pub const fn quantum_flow_witness_id(&self) -> u32 { + self.quantum_flow_witness_id + } + + pub const fn color_witness_term_id(&self) -> LCColorWitnessTermId { + self.color_witness_term_id + } + + pub const fn runtime_coupling_binding_digest(&self) -> SemanticDigest { + self.runtime_coupling_binding_digest + } + + pub const fn output_projection_id(&self) -> u32 { + self.output_projection_id + } +} + +fn validate_strict_u32_sequence(label: &str, values: &[u32]) -> RusticolResult<()> { + let mut previous = None; + for (index, value) in values.iter().copied().enumerate() { + if let Some(previous) = previous + && previous >= value + { + return Err(invalid(format!( + "{label} are not strictly ordered at row {index}" + ))); + } + previous = Some(value); + } + Ok(()) +} + +fn validate_sequence_len(label: &str, length: usize) -> RusticolResult { + u32::try_from(length).map_err(|_| invalid(format!("{label} length {length} exceeds u32"))) +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/recurrence/mod.rs b/pyamplicol/rust/crates/rusticol-core/src/recurrence/mod.rs new file mode 100644 index 0000000000..3fbb3c1e3e --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/recurrence/mod.rs @@ -0,0 +1,181 @@ +// SPDX-License-Identifier: 0BSD + +//! Model-generic compact recurrence construction and execution. + +mod arena; +mod builder; +mod color; +mod color_contraction; +mod construct; +mod contact_orbit_owner; +#[cfg(any(test, feature = "on-the-fly-test-support"))] +mod diagnostic; +pub mod direct_backend; +mod direct_codec; +mod direct_helicity_dispatch; +mod direct_lowering; +mod direct_pacbin; +mod direct_plan; +pub mod direct_runtime; +mod exact; +mod fermion_ordering; +mod helicity_support; +mod input; +mod layout; +pub(crate) mod on_the_fly; +pub mod process; +mod program; +mod relation; +mod symmetric_group_fft; +pub mod template; +pub(crate) mod template_json; + +pub use arena::{ + DirectArenaAssignment, DirectArenaInterval, DirectArenaLayout, assign_direct_arena, + recurrence_direct_arena_layout, +}; +pub use builder::AuthenticatedRecurrenceBuilderInput; +pub use color::{ + DynamicLCColorState, DynamicLCColorStateInterner, LCColorComponent, LCColorComponentKind, + LCColorComponentOperation, LCColorComponentRole, LCColorEndpoint, LCColorParentPort, + LCColorPortBinding, LCColorPortWiring, LCColorSourceSeed, LCColorSourceSeedOperation, + LCColorTransitionWitness, +}; +pub(crate) use color_contraction::RuntimeSymmetricGroupColorWorkspace; +pub use color_contraction::{ + CanonicalColorContractionEntries, CanonicalColorContractionEntry, FactorizedColorContraction, + FactorizedColorContractionKind, RECURRENCE_COLOR_CONTRACTION_CODEC_ABI, + RawColorContractionEntry, RecurrenceColorAccuracy, RecurrenceColorContraction, + RecurrenceColorStorage, RuntimeColorContractionEntries, RuntimeColorContractionEntry, + RuntimeColorContractionReducer, RuntimeFactorizedColorContraction, + RuntimeFactorizedColorContractionEntry, RuntimeSymmetricGroupColorContraction, + RuntimeSymmetricGroupKernel, decode_recurrence_color_contraction_v3, + recurrence_color_contraction_digest, +}; +pub use construct::RecurrenceBuildProgress; +#[doc(hidden)] +pub use construct::RecurrenceGenerationTelemetry; +#[cfg(any(test, feature = "on-the-fly-test-support"))] +#[doc(hidden)] +pub use diagnostic::ConstructionTransitionDiagnosticRowV1; +pub use direct_codec::{decode_recurrence_direct_plan_v2, encode_recurrence_direct_plan_v2}; +pub use direct_helicity_dispatch::{ + DirectHelicityDispatch, DirectHelicityDispatchDescriptor, DirectHelicityDispatchParts, + DirectHelicityRowGroupDescriptor, DirectHelicitySupportDomainDescriptor, + RECURRENCE_HELICITY_DISPATCH_ABI, RecurrenceHelicityDispatchMetadata, + decode_recurrence_helicity_dispatch_v1, encode_recurrence_helicity_dispatch_v1, + write_recurrence_helicity_dispatch_v1_atomic, +}; +#[cfg(test)] +pub(crate) use direct_lowering::validated_template_fixture; +pub use direct_lowering::{ + DirectRecurrenceRuntimeOptions, PreparedDirectExecutorBinding, PreparedDirectExecutorCatalog, + PreparedDirectExecutorKey, lower_recurrence_direct_plan_v2, + lower_recurrence_direct_plan_v2_with_helicity_dispatch, + lower_recurrence_direct_plan_v2_with_relation_discovery, lower_recurrence_direct_v2, +}; +pub(crate) use direct_pacbin::validate_recurrence_color_projection_certificate; +pub use direct_pacbin::{ + RECURRENCE_COLOR_PROJECTION_CERTIFICATE_MEMBER, RECURRENCE_DIRECT_SCHEDULE_MEMBER, + RecurrenceDirectPacbinMetadata, load_recurrence_direct_plan_pacbin, + write_recurrence_direct_plan_pacbin, + write_recurrence_direct_plan_pacbin_with_projection_certificate, +}; +pub(crate) use direct_plan::DIRECT_CONTRIBUTION_FLAG_INITIALIZE_DESTINATION; +#[cfg(test)] +pub(crate) use direct_plan::tests::{ + valid_parts as valid_direct_plan_parts_fixture, valid_plan as valid_direct_plan_fixture, +}; +pub use direct_plan::{ + DIRECT_CONTRIBUTION_FLAG_CERTIFIED_REUSE, DIRECT_NONE_U32, + DirectAmplitudeDestinationDescriptor, DirectClosureRow, DirectContributionRow, + DirectCurrentDescriptor, DirectDestinationOperation, DirectExecutorRole, DirectFinalizationRow, + DirectMomentumFormDescriptor, DirectMomentumTerm, DirectNodeKind, DirectRecurrencePlan, + DirectRecurrencePlanParts, DirectReplayTargetDescriptor, DirectResolvedHelicityDescriptor, + DirectResolvedSourceSelection, DirectRowGroupDescriptor, DirectSelectorDomainDescriptor, + DirectSelectorWorkSummary, DirectSourceDispatchVariantDescriptor, DirectSourceEmbeddingRow, + DirectSourceProjectionRow, DirectSourceRow, DirectSourceStateAssignment, + RECURRENCE_DIRECT_PLAN_ABI, RECURRENCE_DIRECT_RUNTIME_CAPABILITY, + RECURRENCE_DIRECT_RUNTIME_LAYOUT_ABI, RECURRENCE_DIRECT_TEMPLATE_ABI, +}; +pub use exact::{ExactComplexRational, ExactRational}; +pub use helicity_support::{ + AllFlowHelicityMask, AllFlowHelicitySupportProjection, project_all_flow_helicity_support, +}; +pub use input::{ + CanonicalInputSection, CheckedTableRange, MultiwordMaskCatalogView, + RecurrenceBuilderInputHeader, canonical_input_digest, checked_u32_len, checked_u64_len, + checked_usize, validate_equal_column_lengths, validate_header_and_sections, + validate_packed_ranges, validate_ranges_within, validate_u32_references, +}; +pub use layout::{ + CanonicalMomentumLinearForm, ContributionKey, CurrentCoreKey, CurrentHelicityIdentity, + CurrentSourceBinding, DynamicLCColorStateId, LCColorWitnessTermId, MomentumTerm, + RecurrenceNodeKind, RecurrenceStrategy, SemanticDigest, SourceStateAssignment, +}; +#[cfg(feature = "on-the-fly-test-support")] +#[doc(hidden)] +pub use on_the_fly::{ + OnTheFlyQueryFamilyCensusV1, OnTheFlyTestSupportReportV1, on_the_fly_query_family_census_v1, + on_the_fly_test_support_probe_v1, +}; +pub use program::{ + CLOSURE_TARGET_DOMAIN_PROOF_KIND, ClosureCandidateDomainCertificateV1, + ClosureExecutionProofGroupV2, ClosureProofContributionV2, ClosureProofMetadataV2, + ClosureTargetDomainCertificateV1, RecurrenceAmplitudeDestination, RecurrenceClosureTerm, + RecurrenceContribution, RecurrenceCurrent, RecurrenceFinalization, RecurrenceProgram, + RecurrenceReplayTarget, RecurrenceResolvedHelicity, ReflectionCertificateV1, + ThreeLineTraversalCertificateV1, ThreeLineTraversalKindV1, closure_component_factor_digest_v2, + closure_proof_semantic_completeness_digest_v2, + closure_proof_semantic_completeness_digest_with_three_line_v2, + closure_selector_domain_digest_v2, three_line_traversal_proof_digest_v1, +}; +pub use relation::{ + NUMERICAL_RELATION_CERTIFICATE_ALGORITHM, RecurrenceCurrentRelationCertificate, + RecurrenceNumericalCurrentMapping, RecurrenceNumericalRelationEvidence, + RecurrenceRelationDiscoveryMode, RecurrenceRelationDiscoveryOptions, + RecurrenceRelationDiscoveryReport, authenticate_recurrence_numerical_relation_provenance, + recurrence_numerical_source_semantics_sha256, relation_certificate_algorithm, +}; +pub use symmetric_group_fft::{ + MAX_SYMMETRIC_GROUP_FFT_DEGREE, SymmetricGroupComplex64, SymmetricGroupFftBlock, + SymmetricGroupFftPlan, SymmetricGroupFftWorkspace, +}; +/// Semantic prepared-model companion ABI. +pub const RECURRENCE_TEMPLATE_ABI: &str = "pyamplicol-recurrence-template-v1"; +/// Python-to-Rust recurrence builder input ABI. +pub const RECURRENCE_BUILDER_INPUT_ABI: &str = "pyamplicol-recurrence-builder-input-v2"; +/// Bounded Rust-to-Python recurrence builder result ABI. +pub const RECURRENCE_BUILDER_RESULT_ABI: &str = "pyamplicol-recurrence-builder-result-v2"; +/// Direct-arena recurrence plan ABI. +pub const RECURRENCE_PLAN_ABI: &str = RECURRENCE_DIRECT_PLAN_ABI; +/// Direct-arena native recurrence layout ABI. +pub const RECURRENCE_RUNTIME_LAYOUT_ABI: &str = RECURRENCE_DIRECT_RUNTIME_LAYOUT_ABI; +/// Process runtime kind stored in execution metadata. +pub const RECURRENCE_RUNTIME_KIND: &str = "pyamplicol-runtime-recurrence-execution"; +/// Native complex-f64 recurrence capability. +pub const RECURRENCE_RUNTIME_CAPABILITY: &str = RECURRENCE_DIRECT_RUNTIME_CAPABILITY; +/// LC color capability required by recurrence. +pub const RECURRENCE_LC_COLOR_CAPABILITY: &str = "rusticol.recurrence-color.lc.v1"; +/// Contracted NLC/full-color capability required by recurrence. +pub const RECURRENCE_CONTRACTED_COLOR_CAPABILITY: &str = "rusticol.recurrence-color.contracted.v1"; +/// The builder input always consists of explicitly little-endian columns. +pub const RECURRENCE_INPUT_ENDIANNESS: &str = "little"; + +/// Internal all-flow source marker; authenticated model/process inputs must +/// never carry this value as an ordinary concrete spin state. +pub(super) const DYNAMIC_UNION_SOURCE_SPIN_STATE_CLASS: i32 = i32::MIN; + +fn validate_concrete_spin_state(value: i32, context: &str) -> crate::RusticolResult<()> { + if value == DYNAMIC_UNION_SOURCE_SPIN_STATE_CLASS { + return Err(crate::RusticolError::invalid_argument(format!( + "{context} uses the reserved dynamic-union source-spin sentinel" + ))); + } + Ok(()) +} + +#[cfg(test)] +mod direct_backend_tests; +#[cfg(test)] +mod tests; diff --git a/pyamplicol/rust/crates/rusticol-core/src/recurrence/on_the_fly/coupling_policy.rs b/pyamplicol/rust/crates/rusticol-core/src/recurrence/on_the_fly/coupling_policy.rs new file mode 100644 index 0000000000..c4d36cfb84 --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/recurrence/on_the_fly/coupling_policy.rs @@ -0,0 +1,999 @@ +// SPDX-License-Identifier: 0BSD + +//! Selector-independent resolution of the process coupling-order policy. +//! +//! The public minimal policy is process-global. Resolving it from one chosen +//! helicity or colour selector can accidentally promote a higher-order branch +//! when the true leading-order amplitude vanishes only for that selector. The +//! cold sweep below therefore erases public helicity labels while retaining +//! every structural contract that controls reachability: source support, +//! current state and spin, flavour/quantum ancestry, exact LC colour state, +//! and fermion-pairing lineage. It never constructs a public colour-flow +//! table, a recurrence DAG, or executable contributions. + +use std::collections::BTreeMap; + +use super::*; +use crate::recurrence::contact_orbit_owner::ContactOrbitParentTopologyDomain; + +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, bincode::Encode, bincode::Decode)] +pub(crate) struct OnTheFlyCouplingPolicyCensusV1 { + pub(crate) source_topology_count: u64, + pub(crate) retained_topology_count: u64, + pub(crate) transition_witness_attempt_count: u64, + pub(crate) closure_witness_attempt_count: u64, + pub(crate) viable_total_order_count: u64, +} + +#[derive(Clone, Debug, Eq, PartialEq, bincode::Encode, bincode::Decode)] +pub(crate) struct OnTheFlyResolvedCouplingPolicyV1 { + seed_digest: SemanticDigest, + effective_limits: Box<[Option]>, + census: OnTheFlyCouplingPolicyCensusV1, +} + +impl OnTheFlyResolvedCouplingPolicyV1 { + pub(crate) const fn seed_digest(&self) -> SemanticDigest { + self.seed_digest + } + + pub(crate) fn effective_limits(&self) -> &[Option] { + &self.effective_limits + } + + #[allow(dead_code)] // Retained for the feature-only structural census API. + pub(crate) const fn census(&self) -> OnTheFlyCouplingPolicyCensusV1 { + self.census + } +} + +#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] +struct GlobalTopologyKey { + support: Box<[u32]>, + current_state_template_id: u32, + spin_state: i32, + flavour_flow: Box<[i32]>, + quantum_number_flow_id: u32, + color: DynamicLCColorState, + pairing_lineage: PendingPairingLineage, +} + +#[derive(Clone, Debug)] +struct GlobalTopologyNode { + key: GlobalTopologyKey, + /// Componentwise Pareto-minimal order vectors reaching this exact state. + orders: Vec>, +} + +/// Index the structural condition on parent sizes before inspecting their +/// exact colour states. Many states share one support size, and scanning all +/// pairs of those states only to reject their sizes makes the cold sweep +/// unnecessarily quadratic in its largest, already-complete currents. +struct SupportSizePairIndex { + by_size: BTreeMap>, +} + +impl SupportSizePairIndex { + fn new(sizes: impl IntoIterator) -> Self { + let mut by_size = BTreeMap::>::new(); + for (index, size) in sizes.into_iter().enumerate() { + by_size.entry(size).or_default().push(index); + } + Self { by_size } + } + + fn partners_after(&self, left_index: usize, left_size: usize, total_size: usize) -> &[usize] { + let Some(right_size) = total_size.checked_sub(left_size) else { + return &[]; + }; + let Some(indices) = self.by_size.get(&right_size) else { + return &[]; + }; + // Stable insertion order preserves the original left-major, + // increasing-right enumeration, including equal-size parent pairs. + &indices[indices.partition_point(|&index| index <= left_index)..] + } +} + +fn checked_increment(value: &mut u64, label: &str) -> RusticolResult<()> { + *value = value + .checked_add(1) + .ok_or_else(|| invalid(format!("{label} exceeds u64")))?; + Ok(()) +} + +fn weakly_dominates(left: &[u32], right: &[u32]) -> bool { + left.len() == right.len() + && left + .iter() + .zip(right.iter()) + .all(|(left, right)| left <= right) +} + +fn insert_pareto_order(orders: &mut Vec>, candidate: Vec) -> bool { + if orders + .iter() + .any(|existing| weakly_dominates(existing, &candidate)) + { + return false; + } + orders.retain(|existing| !weakly_dominates(&candidate, existing)); + orders.push(candidate.into_boxed_slice()); + orders.sort_unstable(); + true +} + +fn insert_topology_order( + nodes: &mut Vec, + node_ids: &mut BTreeMap, + key: GlobalTopologyKey, + orders: Vec, +) -> RusticolResult<()> { + if let Some(id) = node_ids.get(&key).copied() { + insert_pareto_order(&mut nodes[id].orders, orders); + return Ok(()); + } + let id = nodes.len(); + nodes + .try_reserve(1) + .map_err(|error| invalid(format!("global topology allocation failed: {error}")))?; + let mut retained_orders = Vec::new(); + retained_orders + .try_reserve_exact(1) + .map_err(|error| invalid(format!("global coupling-order allocation failed: {error}")))?; + retained_orders.push(orders.into_boxed_slice()); + node_ids.insert(key.clone(), id); + nodes.push(GlobalTopologyNode { + key, + orders: retained_orders, + }); + Ok(()) +} + +fn ordered_transition_parents<'a>( + prepared: &PreparedTransition, + left: &'a GlobalTopologyKey, + right: &'a GlobalTopologyKey, +) -> Option<[&'a GlobalTopologyKey; 2]> { + prepared + .parent_ids( + left.current_state_template_id, + right.current_state_template_id, + 0, + 1, + ) + .map(|ids| { + let parents = [left, right]; + [parents[ids[0] as usize], parents[ids[1] as usize]] + }) +} + +fn ordered_closure_parents<'a>( + prepared: &PreparedClosure, + left: &'a GlobalTopologyKey, + right: &'a GlobalTopologyKey, +) -> Option<[&'a GlobalTopologyKey; 2]> { + prepared + .parent_ids( + left.current_state_template_id, + right.current_state_template_id, + 0, + 1, + ) + .map(|ids| { + let parents = [left, right]; + [parents[ids[0] as usize], parents[ids[1] as usize]] + }) +} + +fn topology_quantum_matches( + states: [u32; 2], + spins: [i32; 2], + parents: [&GlobalTopologyKey; 2], +) -> bool { + (0..2).all(|index| { + states[index] == parents[index].current_state_template_id + && spins[index] == parents[index].spin_state + }) +} + +fn contact_orbit_topology_domain( + parent: &GlobalTopologyKey, +) -> ContactOrbitParentTopologyDomain<'_> { + if parent.support.len() == 1 { + ContactOrbitParentTopologyDomain::source(parent.current_state_template_id, &parent.support) + } else { + ContactOrbitParentTopologyDomain::current(parent.current_state_template_id, &parent.support) + } +} + +fn closure_quantum_matches_topology( + quantum: &PreparedClosureQuantum, + parents: [&GlobalTopologyKey; 2], +) -> bool { + match (quantum.input_states, quantum.input_spins) { + (None, None) => true, + (Some(states), Some(spins)) => topology_quantum_matches(states, spins, parents), + _ => false, + } +} + +fn insert_all_sources( + seed: &OnTheFlyProcessSeedV1, + grammar: &PreparedOnTheFlyGrammarV1, + nodes: &mut Vec, + node_ids: &mut BTreeMap, +) -> RusticolResult { + let zero_orders = vec![0_u32; seed.explicit_coupling_limits().len()]; + let mut source_topology_count = 0_u64; + for anchor in &seed.source_anchors { + for state in &anchor.states { + let contract = grammar + .sources + .get(&(state.source_template_id, state.current_state_template_id)) + .ok_or_else(|| integrity("global source has no prepared source contract"))?; + let color = contract.color_seed.instantiate( + anchor.source_slot, + contract.current_state.color_representation, + )?; + let key = GlobalTopologyKey { + support: vec![anchor.source_slot].into_boxed_slice(), + current_state_template_id: state.current_state_template_id, + spin_state: state.spin_state, + flavour_flow: state.flavour_flow.clone(), + quantum_number_flow_id: state.quantum_number_flow_id, + color, + pairing_lineage: PendingPairingLineage::source(seed, anchor.source_slot), + }; + let previous = nodes.len(); + insert_topology_order(nodes, node_ids, key, zero_orders.clone())?; + if nodes.len() != previous { + checked_increment(&mut source_topology_count, "source topology count")?; + } + } + } + Ok(source_topology_count) +} + +#[allow(clippy::too_many_arguments)] +fn apply_transition( + templates: &ValidatedRecurrenceTemplateInput, + prepared: &PreparedTransition, + left: &GlobalTopologyNode, + right: &GlobalTopologyNode, + source_count: usize, + seed: &OnTheFlyProcessSeedV1, + hard_limits: &[Option], + nodes: &mut Vec, + node_ids: &mut BTreeMap, + census: &mut OnTheFlyCouplingPolicyCensusV1, +) -> RusticolResult<()> { + let Some(parents) = ordered_transition_parents(prepared, &left.key, &right.key) else { + return Ok(()); + }; + if !topology_quantum_matches(prepared.input_states, prepared.input_spins, parents) { + return Ok(()); + } + if let Some(contact_orbit) = prepared.contact_orbit.as_ref() + && !contact_orbit.accepts_parent_topology_domain([ + contact_orbit_topology_domain(parents[0]), + contact_orbit_topology_domain(parents[1]), + ])? + { + return Ok(()); + } + // Contact-orbit certificates choose one amplitude owner among the + // admitted equivalent contributions. Retaining all certified owner + // orientations cannot introduce a distinct coupling-order vector. + let _ = prepared.output_factor()?; + let support = merge_disjoint_support(&parents[0].support, &parents[1].support)?; + if support.len() >= source_count { + return Ok(()); + } + let result_state = templates + .input() + .current_states + .get(prepared.row.result_state_template_id as usize) + .ok_or_else(|| integrity("global topology result state is absent"))?; + let carries_colored_fermion_line = + result_state.statistics == 1 && result_state.color_representation != 1; + let pairing_lineages = combine_pairing_lineage_sets( + // Each topology key carries one exact lineage alternative. Keeping + // it in the key prevents an invalid endpoint pairing from borrowing + // the coupling order of another alternative. + // + // No public selector enters this operation. + seed, + std::slice::from_ref(&parents[0].pairing_lineage), + std::slice::from_ref(&parents[1].pairing_lineage), + carries_colored_fermion_line, + )?; + if pairing_lineages.is_empty() { + return Ok(()); + } + let flavour = prepared + .flavour + .apply_flows(&parents[0].flavour_flow, &parents[1].flavour_flow); + for witness in &prepared.witnesses { + if witness.row.left_shape_string_id != parents[0].color.output_color_shape_id() + || witness.row.right_shape_string_id != parents[1].color.output_color_shape_id() + { + continue; + } + checked_increment( + &mut census.transition_witness_attempt_count, + "global transition witness-attempt count", + )?; + let Some(color) = witness + .witness + .apply(&parents[0].color, &parents[1].color)? + else { + continue; + }; + for lineage in &pairing_lineages { + let key = GlobalTopologyKey { + support: support.clone().into_boxed_slice(), + current_state_template_id: prepared.row.result_state_template_id, + spin_state: prepared.quantum.result_spin_state, + flavour_flow: flavour.clone().into_boxed_slice(), + quantum_number_flow_id: prepared.quantum.result_quantum_number_flow_id, + color: color.clone(), + pairing_lineage: lineage.clone(), + }; + for left_orders in &left.orders { + for right_orders in &right.orders { + let Some(orders) = combined_coupling_orders( + left_orders, + right_orders, + &prepared.local_orders, + hard_limits, + )? + else { + continue; + }; + insert_topology_order(nodes, node_ids, key.clone(), orders)?; + } + } + } + } + Ok(()) +} + +fn build_global_topologies( + templates: &ValidatedRecurrenceTemplateInput, + seed: &OnTheFlyProcessSeedV1, + grammar: &PreparedOnTheFlyGrammarV1, + census: &mut OnTheFlyCouplingPolicyCensusV1, +) -> RusticolResult> { + let source_count = seed.source_anchors.len(); + let hard_limits = seed.explicit_coupling_limits(); + let mut nodes = Vec::new(); + let mut node_ids = BTreeMap::new(); + census.source_topology_count = insert_all_sources(seed, grammar, &mut nodes, &mut node_ids)?; + + for target_size in 2..source_count { + // New nodes produced at this stage have support `target_size`, so a + // stable snapshot of smaller parents is complete for the stage. + let parents = nodes + .iter() + .filter(|node| node.key.support.len() < target_size) + .cloned() + .collect::>(); + let pair_index = + SupportSizePairIndex::new(parents.iter().map(|node| node.key.support.len())); + for (left_index, left) in parents.iter().enumerate() { + for &right_index in + pair_index.partners_after(left_index, left.key.support.len(), target_size) + { + let right = &parents[right_index]; + if !supports_are_disjoint(&left.key.support, &right.key.support) { + continue; + } + let Some(rows) = grammar.transitions.get(&canonical_state_pair( + left.key.current_state_template_id, + right.key.current_state_template_id, + )) else { + continue; + }; + for prepared in rows { + apply_transition( + templates, + prepared, + left, + right, + source_count, + seed, + hard_limits, + &mut nodes, + &mut node_ids, + census, + )?; + } + } + } + } + census.retained_topology_count = u64::try_from(nodes.len()) + .map_err(|_| invalid("global retained-topology count exceeds u64"))?; + Ok(nodes) +} + +fn closure_has_canonical_anchor( + left: &GlobalTopologyKey, + right: &GlobalTopologyKey, + anchor: u32, + source_count: usize, +) -> bool { + (left.support.as_ref() == [anchor] && right.support.len() + 1 == source_count) + || (right.support.as_ref() == [anchor] && left.support.len() + 1 == source_count) +} + +fn collect_viable_total_orders( + seed: &OnTheFlyProcessSeedV1, + grammar: &PreparedOnTheFlyGrammarV1, + nodes: &[GlobalTopologyNode], + census: &mut OnTheFlyCouplingPolicyCensusV1, +) -> RusticolResult>> { + let source_count = seed.source_anchors.len(); + let mut totals = Vec::new(); + let pair_index = SupportSizePairIndex::new(nodes.iter().map(|node| node.key.support.len())); + for (left_index, left) in nodes.iter().enumerate() { + // Physical closure below requires a singleton canonical anchor. + // Reject impossible size classes before visiting their colour states; + // the exact anchor and colour/pairing checks remain unchanged. + if left.key.support.len() != 1 + && left.key.support.len().checked_add(1) != Some(source_count) + { + continue; + } + for &right_index in + pair_index.partners_after(left_index, left.key.support.len(), source_count) + { + let right = &nodes[right_index]; + if !supports_are_disjoint(&left.key.support, &right.key.support) { + continue; + } + let Some(rows) = grammar.closures.get(&canonical_state_pair( + left.key.current_state_template_id, + right.key.current_state_template_id, + )) else { + continue; + }; + for closure in rows { + let Some(parents) = ordered_closure_parents(closure, &left.key, &right.key) else { + continue; + }; + let pairing_lineages = combine_pairing_lineage_sets( + seed, + std::slice::from_ref(&parents[0].pairing_lineage), + std::slice::from_ref(&parents[1].pairing_lineage), + false, + )?; + if !pairing_lineages + .iter() + .try_fold(false, |complete, lineage| { + Ok::<_, RusticolError>(complete || complete_pairing_lineage(seed, lineage)?) + })? + { + continue; + } + for quantum in closure + .quantum_flows + .iter() + .filter(|quantum| closure_quantum_matches_topology(quantum, parents)) + { + let _ = quantum.output_factor()?; + for witness in &closure.witnesses { + if witness.row.left_shape_string_id + != parents[0].color.output_color_shape_id() + || witness.row.right_shape_string_id + != parents[1].color.output_color_shape_id() + { + continue; + } + checked_increment( + &mut census.closure_witness_attempt_count, + "global closure witness-attempt count", + )?; + let closed = witness + .witness + .closed_components(&parents[0].color, &parents[1].color)?; + let Some(anchor) = grammar.closure_anchor_for_components(seed, &closed) + else { + continue; + }; + if !closure_has_canonical_anchor( + &left.key, + &right.key, + anchor, + source_count, + ) { + continue; + } + for left_orders in &left.orders { + for right_orders in &right.orders { + let Some(total) = combined_coupling_orders( + left_orders, + right_orders, + &closure.local_orders, + seed.explicit_coupling_limits(), + )? + else { + continue; + }; + insert_pareto_order(&mut totals, total); + } + } + } + } + } + } + } + census.viable_total_order_count = u64::try_from(totals.len()) + .map_err(|_| invalid("global viable-total-order count exceeds u64"))?; + Ok(totals) +} + +fn hierarchy_degree(orders: &[u32], hierarchies: &[u32]) -> RusticolResult { + if orders.len() != hierarchies.len() { + return Err(integrity( + "global coupling order and hierarchy dimensions disagree", + )); + } + orders + .iter() + .zip(hierarchies.iter()) + .try_fold(0_u64, |total, (order, hierarchy)| { + total + .checked_add(u64::from(*order) * u64::from(*hierarchy)) + .ok_or_else(|| invalid("hierarchy-weighted coupling degree exceeds u64")) + }) +} + +fn minimal_envelope( + totals: &[Box<[u32]>], + hierarchies: &[u32], +) -> RusticolResult>>> { + let Some(minimum) = totals + .iter() + .map(|orders| hierarchy_degree(orders, hierarchies)) + .collect::>>()? + .into_iter() + .min() + else { + return Ok(None); + }; + let mut envelope = vec![0_u32; hierarchies.len()]; + for orders in totals { + if hierarchy_degree(orders, hierarchies)? != minimum { + continue; + } + for (maximum, value) in envelope.iter_mut().zip(orders.iter()) { + *maximum = (*maximum).max(*value); + } + } + Ok(Some(envelope.into_iter().map(Some).collect())) +} + +#[allow(dead_code)] // Compatibility entry point for query-local/test-support callers. +pub(crate) fn resolve_on_the_fly_coupling_policy_v1( + templates: &ValidatedRecurrenceTemplateInput, + seed: &OnTheFlyProcessSeedV1, +) -> RusticolResult { + let catalog = validate_seed_against_templates(templates, seed)?; + let grammar = prepare_on_the_fly_grammar_v1(templates, &catalog, seed)?; + resolve_on_the_fly_coupling_policy_from_grammar_v1(templates, seed, &grammar) +} + +pub(super) fn resolve_on_the_fly_coupling_policy_from_grammar_v1( + templates: &ValidatedRecurrenceTemplateInput, + seed: &OnTheFlyProcessSeedV1, + grammar: &PreparedOnTheFlyGrammarV1, +) -> RusticolResult { + if seed.coupling_order_policy() == OnTheFlyCouplingOrderPolicyV1::Explicit { + return Ok(OnTheFlyResolvedCouplingPolicyV1 { + seed_digest: seed.semantic_digest(), + effective_limits: seed.explicit_coupling_limits().to_vec().into_boxed_slice(), + census: OnTheFlyCouplingPolicyCensusV1::default(), + }); + } + let mut census = OnTheFlyCouplingPolicyCensusV1::default(); + let nodes = build_global_topologies(templates, seed, grammar, &mut census)?; + let totals = collect_viable_total_orders(seed, grammar, &nodes, &mut census)?; + let effective_limits = minimal_envelope(&totals, seed.coupling_hierarchies())? + .unwrap_or_else(|| seed.explicit_coupling_limits().to_vec()); + Ok(OnTheFlyResolvedCouplingPolicyV1 { + seed_digest: seed.semantic_digest(), + effective_limits: effective_limits.into_boxed_slice(), + census, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::recurrence::template::{CouplingOrderTermRow, IndexedRangeRow}; + use crate::recurrence::{CheckedTableRange, validated_template_fixture}; + + fn digest(byte: u8) -> SemanticDigest { + SemanticDigest::new([byte; 32]).unwrap() + } + + #[test] + fn support_size_index_matches_exhaustive_parent_order() { + let cases = [ + vec![], + vec![0], + vec![1, 1], + vec![1, 2, 1, 3, 2, 1, 4, 3], + vec![0, 2, 0, 1, 3, 0, 2], + vec![usize::MAX, 0, 1, usize::MAX, 2], + ]; + for sizes in cases { + let index = SupportSizePairIndex::new(sizes.iter().copied()); + for target in (0..=9).chain([usize::MAX]) { + let exhaustive = sizes + .iter() + .enumerate() + .flat_map(|(left, &left_size)| { + sizes.iter().enumerate().skip(left + 1).filter_map( + move |(right, &right_size)| { + (left_size.checked_add(right_size) == Some(target)) + .then_some((left, right)) + }, + ) + }) + .collect::>(); + let indexed = sizes + .iter() + .enumerate() + .flat_map(|(left, &size)| { + index + .partners_after(left, size, target) + .iter() + .map(move |&right| (left, right)) + }) + .collect::>(); + assert_eq!(indexed, exhaustive, "sizes={sizes:?}, target={target}"); + + // Closure's early size filter must remove exactly the pairs + // that cannot possibly contain its singleton anchor. For two + // sources it must not duplicate the symmetric singleton pair. + let expected_closure = exhaustive + .iter() + .copied() + .filter(|&(left, right)| sizes[left] == 1 || sizes[right] == 1) + .collect::>(); + let indexed_closure = sizes + .iter() + .enumerate() + .filter(|&(_, &size)| size == 1 || size.checked_add(1) == Some(target)) + .flat_map(|(left, &size)| { + index + .partners_after(left, size, target) + .iter() + .map(move |&right| (left, right)) + }) + .collect::>(); + assert_eq!(indexed_closure, expected_closure); + } + } + } + + #[test] + fn support_size_index_does_not_scan_pairs_of_complete_currents() { + let sizes = std::iter::repeat_n(1, 4) + .chain(std::iter::repeat_n(8, 10_000)) + .collect::>(); + let index = SupportSizePairIndex::new(sizes.iter().copied()); + let candidate_count = sizes + .iter() + .enumerate() + .map(|(left, &size)| index.partners_after(left, size, 9).len()) + .sum::(); + assert_eq!(candidate_count, 40_000); + assert!(index.partners_after(0, 10, 9).is_empty()); + assert!(index.partners_after(0, 1, 8).is_empty()); + } + + fn scalar_templates() -> ValidatedRecurrenceTemplateInput { + let mut input = validated_template_fixture().into_input(); + let spin_start = u64::try_from(input.i32_sequence_values.len()).unwrap(); + let spin_sequence_id = u32::try_from(input.i32_sequence_ranges.len()).unwrap(); + input.i32_sequence_ranges.push(IndexedRangeRow { + id: spin_sequence_id, + range: CheckedTableRange::new(spin_start, 2), + }); + input.i32_sequence_values.extend([50_000, 50_000]); + input.quantum_flows[0].input_spin_sequence_id = spin_sequence_id; + input.coupling_order_ranges.push(IndexedRangeRow { + id: 1, + range: CheckedTableRange::new(0, 1), + }); + input.coupling_order_terms.push(CouplingOrderTermRow { + set_id: 1, + name_string_id: 0, + power: 1, + }); + input.validate().unwrap() + } + + fn with_policy( + seed: OnTheFlyProcessSeedV1, + policy: OnTheFlyCouplingOrderPolicyV1, + hierarchies: Vec, + limits: Vec>, + ) -> OnTheFlyProcessSeedV1 { + let OnTheFlyProcessSeedV1 { + process_digest, + model_digest, + template_catalog_digest, + prepared_pack_digest, + direct_catalog_digest, + normalization_semantic_digest, + normalization_convention, + source_anchors, + external_permutation, + coupling_order_policy: _, + coupling_hierarchies: _, + coupling_limits: _, + pairing_classes, + semantic_digest: _, + } = seed; + OnTheFlyProcessSeedV1::new( + process_digest, + model_digest, + template_catalog_digest, + prepared_pack_digest, + direct_catalog_digest, + normalization_semantic_digest, + normalization_convention, + ExactComplexRational::ONE, + source_anchors.into_vec(), + external_permutation.into_vec(), + policy, + hierarchies, + limits, + pairing_classes.into_vec(), + ) + .unwrap() + } + + fn with_selector_local_zero(seed: OnTheFlyProcessSeedV1) -> OnTheFlyProcessSeedV1 { + let OnTheFlyProcessSeedV1 { + process_digest, + model_digest, + template_catalog_digest, + prepared_pack_digest, + direct_catalog_digest, + normalization_semantic_digest, + normalization_convention, + source_anchors, + external_permutation, + coupling_order_policy, + coupling_hierarchies, + coupling_limits, + pairing_classes, + semantic_digest: _, + } = seed; + let anchors = source_anchors + .into_vec() + .into_iter() + .map(|anchor| { + let OnTheFlySourceAnchorV1 { + source_slot, + external_label, + is_initial, + color_role, + is_fermionic, + pairing_source_contract_digest, + states, + } = anchor; + let mut states = states.into_vec(); + let mut zero = states[0].clone(); + zero.state_index = 1; + zero.public_helicity = 1; + zero.spin_state = 50_001; + states.push(zero); + OnTheFlySourceAnchorV1::new( + source_slot, + external_label, + is_initial, + color_role, + is_fermionic, + pairing_source_contract_digest, + states, + ) + }) + .collect::>>() + .unwrap(); + OnTheFlyProcessSeedV1::new( + process_digest, + model_digest, + template_catalog_digest, + prepared_pack_digest, + direct_catalog_digest, + normalization_semantic_digest, + normalization_convention, + ExactComplexRational::ONE, + anchors, + external_permutation.into_vec(), + coupling_order_policy, + coupling_hierarchies.into_vec(), + coupling_limits.into_vec(), + pairing_classes.into_vec(), + ) + .unwrap() + } + + #[test] + fn explicit_policy_is_returned_without_a_structural_sweep() { + let templates = scalar_templates(); + let summary = templates.summary(); + let seed = scalar_adapter_test_seed( + summary.compiled_model_digest, + summary.catalog_digest, + summary.prepared_kernel_pack_digest, + digest(80), + ) + .unwrap(); + let catalog = validate_seed_against_templates(&templates, &seed).unwrap(); + let grammar = prepare_on_the_fly_grammar_v1(&templates, &catalog, &seed).unwrap(); + let policy = + resolve_on_the_fly_coupling_policy_from_grammar_v1(&templates, &seed, &grammar) + .unwrap(); + + assert_eq!( + policy.effective_limits(), + [Some(0)], + "census={:?}", + policy.census() + ); + assert_eq!(policy.census(), OnTheFlyCouplingPolicyCensusV1::default()); + } + + #[test] + fn minimal_scalar_policy_is_inferred_from_all_sources_and_physical_closure() { + let templates = scalar_templates(); + let summary = templates.summary(); + let seed = with_policy( + scalar_adapter_test_seed( + summary.compiled_model_digest, + summary.catalog_digest, + summary.prepared_kernel_pack_digest, + digest(81), + ) + .unwrap(), + OnTheFlyCouplingOrderPolicyV1::Minimal, + vec![1], + vec![None], + ); + let catalog = validate_seed_against_templates(&templates, &seed).unwrap(); + let grammar = prepare_on_the_fly_grammar_v1(&templates, &catalog, &seed).unwrap(); + let policy = + resolve_on_the_fly_coupling_policy_from_grammar_v1(&templates, &seed, &grammar) + .unwrap(); + + assert_eq!(policy.effective_limits(), [Some(0)]); + assert_eq!( + policy.census(), + OnTheFlyCouplingPolicyCensusV1 { + source_topology_count: 2, + retained_topology_count: 2, + transition_witness_attempt_count: 0, + closure_witness_attempt_count: 2, + viable_total_order_count: 1, + } + ); + } + + #[test] + fn global_leading_order_is_not_promoted_by_a_selector_local_zero() { + // Think of [2, 0] as the process-global QCD LO path. The chosen + // selector happens to vanish there and has only the higher weighted + // [0, 1] electroweak path. Resolving globally keeps QED=0, so the + // selected trace is a structural zero instead of silently promoting + // the process definition to its selector-local higher order. + let global_totals = vec![ + vec![2_u32, 0].into_boxed_slice(), + vec![0, 1].into_boxed_slice(), + ]; + let global = minimal_envelope(&global_totals, &[1, 3]).unwrap().unwrap(); + let selector_only = minimal_envelope(&global_totals[1..], &[1, 3]) + .unwrap() + .unwrap(); + + assert_eq!(global, [Some(2), Some(0)]); + assert_eq!(selector_only, [Some(0), Some(1)]); + assert!( + combined_coupling_orders(&[0, 0], &[0, 0], &[0, 1], &global) + .unwrap() + .is_none() + ); + } + + #[test] + fn cached_minimal_policy_is_selector_global_when_one_helicity_is_zero() { + let templates = scalar_templates(); + let summary = templates.summary(); + let seed = with_selector_local_zero(with_policy( + scalar_adapter_test_seed( + summary.compiled_model_digest, + summary.catalog_digest, + summary.prepared_kernel_pack_digest, + digest(82), + ) + .unwrap(), + OnTheFlyCouplingOrderPolicyV1::Minimal, + vec![1], + vec![None], + )); + let catalog = validate_seed_against_templates(&templates, &seed).unwrap(); + let grammar = prepare_on_the_fly_grammar_v1(&templates, &catalog, &seed).unwrap(); + let policy = + resolve_on_the_fly_coupling_policy_from_grammar_v1(&templates, &seed, &grammar) + .unwrap(); + assert_eq!(policy.effective_limits(), [Some(0)]); + + let nonzero = + DecodedLcQueryV1::new(&seed, vec![0, 1], &[0, 0], OnTheFlyLcSelectorV1::Singlet) + .unwrap(); + let selector_zero = + DecodedLcQueryV1::new(&seed, vec![0, 1], &[1, 1], OnTheFlyLcSelectorV1::Singlet) + .unwrap(); + assert!( + build_selected_lc_trace_impl( + &templates, + &seed, + &grammar, + policy.effective_limits(), + &nonzero, + false, + false, + false, + ) + .unwrap() + .is_some() + ); + assert!( + build_selected_lc_trace_impl( + &templates, + &seed, + &grammar, + policy.effective_limits(), + &selector_zero, + false, + false, + false, + ) + .unwrap() + .is_none() + ); + assert_eq!(policy.effective_limits(), [Some(0)]); + } + + #[test] + fn equal_minimum_hierarchy_paths_form_a_componentwise_envelope() { + let totals = vec![ + vec![2_u32, 0].into_boxed_slice(), + vec![0, 1].into_boxed_slice(), + vec![4, 0].into_boxed_slice(), + ]; + assert_eq!( + minimal_envelope(&totals, &[1, 2]).unwrap().unwrap(), + [Some(2), Some(1)] + ); + } + + #[test] + fn pareto_front_discards_only_componentwise_higher_orders() { + let mut orders = Vec::new(); + assert!(insert_pareto_order(&mut orders, vec![2, 0])); + assert!(insert_pareto_order(&mut orders, vec![0, 2])); + assert!(!insert_pareto_order(&mut orders, vec![3, 1])); + assert!(insert_pareto_order(&mut orders, vec![1, 0])); + assert_eq!( + orders, + [vec![0, 2].into_boxed_slice(), vec![1, 0].into_boxed_slice()] + ); + } +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/recurrence/on_the_fly/family.rs b/pyamplicol/rust/crates/rusticol-core/src/recurrence/on_the_fly/family.rs new file mode 100644 index 0000000000..eea838cdeb --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/recurrence/on_the_fly/family.rs @@ -0,0 +1,5649 @@ +// SPDX-License-Identifier: 0BSD + +//! Feature-only structural union and execution for requested LC query families. +//! +//! Independently constructed query-local traces are merged into stable boxed +//! Direct-Arena rows, one shared numeric workspace, and an ordered grouped +//! invocation schedule. Optional cache snapshots persist these same rows, +//! without introducing a different evaluation plan. Release/default builds do +//! not retain the cold contribution identities consumed here. + +use super::trace::{OnTheFlyTraceContributionProofRowV1, OnTheFlyTraceOperationV1}; +use super::*; +use crate::recurrence::PreparedDirectExecutorCatalog; +use crate::recurrence::construct::current_key_with_dynamic_color; +use sha2::{Digest, Sha256}; +use std::ops::Range; + +pub(crate) mod snapshot; + +const STREAMED_QUERY_FAMILY_IDENTITY_DOMAIN: &[u8] = + b"pyamplicol-on-the-fly-streamed-query-family-v1\0"; + +#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] +struct FamilySourceIdentity { + executor: OnTheFlyExecutorKeyV1, + source_slot: u32, + momentum: CanonicalMomentumLinearForm, + source_template_or_dispatch_domain: u32, + spin_state_class: i32, + exact_factor: ExactComplexRational, + selector_domain_id: u32, +} + +#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] +struct FamilyContributionIdentity { + executor: OnTheFlyExecutorKeyV1, + key: ContributionKey, + exact_factor: ExactComplexRational, + selector_domain_id: u32, +} + +#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] +struct FamilyFinalizationIdentity { + executor: OnTheFlyExecutorKeyV1, + momentum: CanonicalMomentumLinearForm, + component_count: u16, + exact_factor: ExactComplexRational, + selector_domain_id: u32, + flags: u32, +} + +#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] +struct FamilyClosureIdentity { + seed_digest: SemanticDigest, + query_digest: SemanticDigest, + stage: u32, + executor: OnTheFlyExecutorKeyV1, + parent_current_ids: [u32; 2], + parent_momenta: [CanonicalMomentumLinearForm; 2], + exact_factor: ExactComplexRational, + component_factors: Box<[ExactComplexRational]>, + component_count: u16, + selector_domain_id: u32, + proof_group_id: u32, + amplitude_destination_id: u32, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +struct FamilyCurrentDefinition { + component_count: u32, + source: Option, + contributions: BTreeSet, + finalization: Option, +} + +struct FamilyUnionCurrentRecord { + id: u32, + definition: Option, +} + +#[derive(Clone, Copy)] +pub(crate) struct QueryFamilyTraceInput<'a> { + pub(crate) trace: &'a OnTheFlyStructuralTraceV1, + pub(crate) projection: OnTheFlyProjectionProbeV1, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +enum QueryFamilyCacheIdentityV1 { + /// A restored family is addressed by its retained lane handle. A new + /// selection always obtains its ordinary exact or streamed identity. + Restored, + Exact { + direct_catalog: PreparedDirectExecutorCatalog, + queries: Box< + [( + SemanticDigest, + SemanticDigest, + SemanticDigest, + OnTheFlyProjectionProbeV1, + )], + >, + }, + Streamed { + direct_catalog_digest: SemanticDigest, + query_count: u32, + ordered_query_digest: SemanticDigest, + }, +} + +#[derive(Debug, Eq, PartialEq, bincode::Encode, bincode::Decode)] +enum OnTheFlyFamilyRowsV1 { + Source(Box<[DirectSourceRow]>), + Contribution(Box<[DirectContributionRow]>), + Finalization(Box<[DirectFinalizationRow]>), + Closure(Box<[DirectClosureRow]>), +} + +impl OnTheFlyFamilyRowsV1 { + fn len(&self) -> usize { + match self { + Self::Source(rows) => rows.len(), + Self::Contribution(rows) => rows.len(), + Self::Finalization(rows) => rows.len(), + Self::Closure(rows) => rows.len(), + } + } +} + +#[derive(Debug, Eq, PartialEq, bincode::Encode, bincode::Decode)] +struct OnTheFlyFamilyRowGroupV1 { + seed_digest: SemanticDigest, + stage: u32, + role: DirectExecutorRole, + direct_executor_id: u32, + representative_key: OnTheFlyExecutorKeyV1, + member_bindings: Box<[(OnTheFlyExecutorKeyV1, [u8; 2])]>, + rows: OnTheFlyFamilyRowsV1, +} + +#[derive(Debug, Eq, PartialEq)] +struct OnTheFlyBuiltQueryFamilyV1 { + census: OnTheFlyQueryFamilyCensusV1, + source_count: u32, + lorentz_component_count: u16, + parameter_count: u32, + current_component_count: u32, + amplitude_destination_count: u32, + momentum_forms: Box<[CanonicalMomentumLinearForm]>, + exact_factors: Box<[ExactComplexRational]>, + row_groups: Box<[OnTheFlyFamilyRowGroupV1]>, + #[cfg(any(test, feature = "on-the-fly-test-support"))] + observed_currents: Box<[OnTheFlyFamilyObservedCurrentDescriptorV1]>, +} + +/// Opaque, fully materialized streamed family. Its compact cache identity is +/// computed while chunks are consumed, so no query-local identity table +/// survives the cold construction boundary. +#[derive(Debug)] +pub(crate) struct OnTheFlyStreamedQueryFamilyCandidateV1 { + identity: QueryFamilyCacheIdentityV1, + family: OnTheFlyBuiltQueryFamilyV1, +} + +/// Synchronous chunk consumer used by contracted color construction. A +/// producer may create and bind a bounded batch of query-local traces, hand +/// borrowed inputs to this interface, and immediately drop the whole batch. +/// Returned destinations are dense across every previously consumed chunk. +pub(crate) trait OnTheFlyQueryFamilyChunkConsumerV1 { + fn push_chunk(&mut self, selected: &[QueryFamilyTraceInput<'_>]) -> RusticolResult>; +} + +impl OnTheFlyQueryFamilyChunkConsumerV1 for F +where + F: for<'trace> FnMut(&[QueryFamilyTraceInput<'trace>]) -> RusticolResult>, +{ + fn push_chunk(&mut self, selected: &[QueryFamilyTraceInput<'_>]) -> RusticolResult> { + self(selected) + } +} + +#[cfg(any(test, feature = "on-the-fly-test-support"))] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +struct OnTheFlyFamilyObservedCurrentDescriptorV1 { + semantic_digest: SemanticDigest, + stage: u32, + component_base: u32, + component_count: u32, +} + +#[cfg(any(test, feature = "on-the-fly-test-support"))] +#[derive(Clone, Debug, PartialEq)] +pub(crate) struct OnTheFlyFamilyObservedCurrentV1 { + pub(crate) semantic_digest: SemanticDigest, + pub(crate) stage: u32, + pub(crate) values: Vec<(f64, f64)>, +} + +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub(crate) struct OnTheFlyQueryFamilyExecutionReportV1 { + pub(crate) cache_hit: bool, + pub(crate) source_calls: u32, + pub(crate) source_rows: u32, + pub(crate) contribution_calls: u32, + pub(crate) contribution_rows: u32, + pub(crate) finalization_calls: u32, + pub(crate) finalization_rows: u32, + pub(crate) closure_calls: u32, + pub(crate) closure_rows: u32, +} + +#[derive(Clone, Debug)] +struct FamilySourceDraft { + seed_digest: SemanticDigest, + stage: u32, + direct_executor_id: u32, + key: OnTheFlyExecutorKeyV1, + row: DirectSourceRow, +} + +#[derive(Clone, Debug)] +struct FamilyContributionDraft { + seed_digest: SemanticDigest, + stage: u32, + destination_current_id: u32, + direct_executor_id: u32, + key: OnTheFlyExecutorKeyV1, + parent_permutation: [u8; 2], + row: DirectContributionRow, +} + +#[derive(Clone, Debug)] +struct FamilyFinalizationDraft { + seed_digest: SemanticDigest, + stage: u32, + direct_executor_id: u32, + key: OnTheFlyExecutorKeyV1, + row: DirectFinalizationRow, +} + +#[derive(Clone, Debug)] +struct FamilyClosureDraft { + seed_digest: SemanticDigest, + stage: u32, + direct_executor_id: u32, + key: OnTheFlyExecutorKeyV1, + row: DirectClosureRow, +} + +impl FamilyCurrentDefinition { + const fn new(component_count: u32) -> Self { + Self { + component_count, + source: None, + contributions: BTreeSet::new(), + finalization: None, + } + } +} + +/// Cold projection of both today's serialized-query execution and a +/// proof-identical query-family union. +#[derive(Clone, Copy, Debug, Eq, PartialEq, bincode::Encode, bincode::Decode)] +#[doc(hidden)] +pub struct OnTheFlyQueryFamilyCensusV1 { + pub query_count: u32, + pub source_frame_partition_count: u32, + pub projection_applied_query_count: u32, + pub projection_pre_current_count: u32, + pub projection_pre_contribution_count: u32, + pub projection_pre_closure_count: u32, + pub projection_post_current_count: u32, + pub projection_post_contribution_count: u32, + pub projection_post_closure_count: u32, + pub dynamic_current_occurrence_count: u32, + pub dynamic_current_component_occurrence_count: u32, + pub dynamic_source_rows: u32, + pub dynamic_contribution_rows: u32, + pub dynamic_finalization_rows: u32, + pub dynamic_closure_rows: u32, + pub dynamic_source_calls: u32, + pub dynamic_contribution_calls: u32, + pub dynamic_finalization_calls: u32, + pub dynamic_closure_calls: u32, + pub union_unique_current_count: u32, + pub union_unique_current_component_count: u32, + pub union_source_rows: u32, + pub union_contribution_rows: u32, + pub union_finalization_rows: u32, + pub union_closure_rows: u32, + pub union_amplitude_destination_count: u32, + pub union_source_executor_call_groups: u32, + pub union_contribution_executor_call_groups: u32, + pub union_finalization_executor_call_groups: u32, + pub union_closure_executor_call_groups: u32, +} + +impl OnTheFlyQueryFamilyCensusV1 { + #[allow(dead_code)] // Exposed by the non-default structural census feature. + pub(crate) fn union_kernel_application_count(self) -> RusticolResult { + self.union_source_rows + .checked_add(self.union_contribution_rows) + .and_then(|value| value.checked_add(self.union_finalization_rows)) + .and_then(|value| value.checked_add(self.union_closure_rows)) + .ok_or_else(|| integrity("query-family union kernel-application count exceeds u32")) + } +} + +fn add(value: &mut u32, increment: u32, label: &str) -> RusticolResult<()> { + *value = value + .checked_add(increment) + .ok_or_else(|| invalid(format!("{label} exceeds u32")))?; + Ok(()) +} + +fn trace_factor( + trace: &OnTheFlyStructuralTraceV1, + factor_id: u32, + label: &str, +) -> RusticolResult { + trace + .exact_factors + .get(factor_id as usize) + .copied() + .ok_or_else(|| integrity(format!("{label} exact factor is absent"))) +} + +fn trace_momentum( + trace: &OnTheFlyStructuralTraceV1, + momentum_id: u32, + label: &str, +) -> RusticolResult { + trace + .momentum_forms + .get(momentum_id as usize) + .cloned() + .ok_or_else(|| integrity(format!("{label} momentum form is absent"))) +} + +fn remapped_contribution_key( + row: &OnTheFlyTraceContributionProofRowV1, + local_to_union: &[u32], +) -> RusticolResult { + if row.key.parent_value_class_ids() != row.parent_current_ids { + return Err(integrity( + "cold contribution identity does not retain its local parent IDs", + )); + } + let parent_ids = row + .parent_current_ids + .iter() + .map(|id| { + local_to_union + .get(*id as usize) + .copied() + .ok_or_else(|| integrity("contribution parent current is absent")) + }) + .collect::>>()?; + ContributionKey::new( + row.key.transition_template_id(), + parent_ids, + row.key.parent_state_template_ids().to_vec(), + row.key.parent_momenta().to_vec(), + row.key.result_state_template_id(), + row.key.quantum_flow_witness_id(), + row.key.color_witness_term_id(), + row.key.runtime_coupling_binding_digest(), + row.key.output_projection_id(), + ) +} + +fn current_id_by_component_base( + bases: &BTreeMap, + base: u32, + label: &str, +) -> RusticolResult { + bases + .get(&base) + .copied() + .ok_or_else(|| integrity(format!("{label} current component base is absent"))) +} + +fn prepared_executor_id( + catalog: &PreparedDirectExecutorCatalog, + key: OnTheFlyExecutorKeyV1, +) -> RusticolResult { + if key.direct_catalog_digest() != catalog.direct_template_catalog_digest() { + return Err(integrity( + "query-family executor key belongs to a different prepared catalog", + )); + } + match key.evaluator_binding_id() { + Some(binding_id) => catalog.resolve_evaluator(key.role(), binding_id), + None => catalog.resolve_identity_finalizer(), + } +} + +fn current_stage(key: &CurrentCoreKey) -> RusticolResult { + u32::try_from( + key.support_source_slots() + .len() + .checked_sub(1) + .ok_or_else(|| integrity("query-family current has empty source support"))?, + ) + .map_err(|_| invalid("query-family current stage exceeds u32")) +} + +fn intern_factor( + ids: &mut BTreeMap, + factors: &mut Vec, + value: ExactComplexRational, +) -> RusticolResult { + if let Some(id) = ids.get(&value).copied() { + return Ok(id); + } + let id = checked_u32(factors.len(), "query-family exact-factor ID")?; + factors.try_reserve(1).map_err(|error| { + invalid(format!( + "query-family exact-factor allocation failed: {error}" + )) + })?; + factors.push(value); + ids.insert(value, id); + Ok(id) +} + +fn intern_factor_block( + block_ids: &mut BTreeMap, u32>, + scalar_ids: &mut BTreeMap, + factors: &mut Vec, + block: &[ExactComplexRational], +) -> RusticolResult { + if block.is_empty() { + return Err(integrity("query-family exact-factor block is empty")); + } + if let Some(start) = block_ids.get(block).copied() { + return Ok(start); + } + let start = checked_u32(factors.len(), "query-family exact-factor block start")?; + let _end = factors + .len() + .checked_add(block.len()) + .and_then(|end| u32::try_from(end).ok()) + .ok_or_else(|| invalid("query-family exact-factor block exceeds u32"))?; + factors.try_reserve(block.len()).map_err(|error| { + invalid(format!( + "query-family exact-factor block allocation failed: {error}" + )) + })?; + for (offset, value) in block.iter().copied().enumerate() { + let id = start + .checked_add(checked_u32( + offset, + "query-family exact-factor block offset", + )?) + .ok_or_else(|| invalid("query-family exact-factor block ID exceeds u32"))?; + factors.push(value); + scalar_ids.entry(value).or_insert(id); + } + block_ids.insert(block.to_vec().into_boxed_slice(), start); + Ok(start) +} + +fn intern_momentum( + ids: &mut BTreeMap, + forms: &mut Vec, + form: &CanonicalMomentumLinearForm, +) -> RusticolResult { + if let Some(id) = ids.get(form).copied() { + return Ok(id); + } + let id = checked_u32(forms.len(), "query-family momentum-form ID")?; + forms.try_reserve(1).map_err(|error| { + invalid(format!( + "query-family momentum-form allocation failed: {error}" + )) + })?; + forms.push(form.clone()); + ids.insert(form.clone(), id); + Ok(id) +} + +fn prepared_binding( + catalog: &PreparedDirectExecutorCatalog, + key: OnTheFlyExecutorKeyV1, +) -> RusticolResult<(u32, [u8; 2])> { + if key.direct_catalog_digest() != catalog.direct_template_catalog_digest() { + return Err(integrity( + "query-family executor key belongs to a different prepared catalog", + )); + } + match key.evaluator_binding_id() { + Some(binding_id) if key.role() == DirectExecutorRole::Contribution => { + catalog.resolve_contribution(binding_id) + } + Some(binding_id) => Ok((catalog.resolve_evaluator(key.role(), binding_id)?, [0, 1])), + None => Ok((catalog.resolve_identity_finalizer()?, [0, 1])), + } +} + +/// Analyze the exact traces requested by one existing selector-family call. +/// +/// Equal normalized [`CurrentCoreKey`] values are merged only when their +/// complete source/contribution/finalization definitions agree. Conflicting +/// duplicates fail closed. Closures remain distinct per requested query so a +/// future executable union cannot lose an amplitude destination. +fn build_query_family_from_chunks( + direct_catalog: &PreparedDirectExecutorCatalog, + produce: impl FnOnce(&mut dyn OnTheFlyQueryFamilyChunkConsumerV1) -> RusticolResult<()>, +) -> RusticolResult> { + let mut layout = None; + let mut seen_queries = BTreeSet::new(); + let mut seed_partitions = BTreeSet::new(); + let mut colors = DynamicLCColorStateInterner::default(); + let mut union_currents = + BTreeMap::<(SemanticDigest, CurrentCoreKey), FamilyUnionCurrentRecord>::new(); + #[cfg(any(test, feature = "on-the-fly-test-support"))] + let mut observed_current_identities = BTreeMap::::new(); + let mut closure_definitions = Vec::::new(); + let mut source_groups = BTreeSet::new(); + let mut contribution_groups = BTreeSet::new(); + let mut finalization_groups = BTreeSet::new(); + let mut closure_groups = BTreeSet::new(); + let mut census = OnTheFlyQueryFamilyCensusV1 { + query_count: 0, + source_frame_partition_count: 0, + projection_applied_query_count: 0, + projection_pre_current_count: 0, + projection_pre_contribution_count: 0, + projection_pre_closure_count: 0, + projection_post_current_count: 0, + projection_post_contribution_count: 0, + projection_post_closure_count: 0, + dynamic_current_occurrence_count: 0, + dynamic_current_component_occurrence_count: 0, + dynamic_source_rows: 0, + dynamic_contribution_rows: 0, + dynamic_finalization_rows: 0, + dynamic_closure_rows: 0, + dynamic_source_calls: 0, + dynamic_contribution_calls: 0, + dynamic_finalization_calls: 0, + dynamic_closure_calls: 0, + union_unique_current_count: 0, + union_unique_current_component_count: 0, + union_source_rows: 0, + union_contribution_rows: 0, + union_finalization_rows: 0, + union_closure_rows: 0, + union_amplitude_destination_count: 0, + union_source_executor_call_groups: 0, + union_contribution_executor_call_groups: 0, + union_finalization_executor_call_groups: 0, + union_closure_executor_call_groups: 0, + }; + let mut identity_hash = Sha256::new(); + identity_hash.update(STREAMED_QUERY_FAMILY_IDENTITY_DOMAIN); + identity_hash.update(direct_catalog.direct_template_catalog_digest().as_bytes()); + + { + let mut consume = |selected: &[QueryFamilyTraceInput<'_>]| -> RusticolResult> { + let first_destination = census.query_count; + for selected_query in selected { + let query_index = census.query_count as usize; + let trace = selected_query.trace; + let trace_layout = ( + trace.layout.source_count, + trace.layout.lorentz_component_count, + trace.layout.parameter_count, + ); + if layout.is_some_and(|established| established != trace_layout) { + return Err(integrity( + "query-family traces do not share one source-frame workspace shape", + )); + } + layout.get_or_insert(trace_layout); + identity_hash.update(trace.seed_digest.as_bytes()); + identity_hash.update(trace.query_digest.as_bytes()); + identity_hash.update(trace.semantic_digest().as_bytes()); + identity_hash.update([u8::from(selected_query.projection.enabled)]); + identity_hash.update([u8::from(selected_query.projection.applied)]); + for value in selected_query + .projection + .pre + .into_iter() + .chain(selected_query.projection.post) + { + identity_hash.update(value.to_le_bytes()); + } + seed_partitions.insert(trace.seed_digest); + if !seen_queries.insert(trace.query_digest) { + return Err(invalid("query-family repeats a selected query")); + } + if trace.layout.amplitude_component_count != 1 { + return Err(integrity( + "query-local trace does not expose exactly one amplitude destination", + )); + } + if selected_query.projection.applied { + add( + &mut census.projection_applied_query_count, + 1, + "projection-applied query count", + )?; + } + for (target, value, label) in [ + ( + &mut census.projection_pre_current_count, + selected_query.projection.pre[0], + "projection pre-current count", + ), + ( + &mut census.projection_pre_contribution_count, + selected_query.projection.pre[1], + "projection pre-contribution count", + ), + ( + &mut census.projection_pre_closure_count, + selected_query.projection.pre[2], + "projection pre-closure count", + ), + ( + &mut census.projection_post_current_count, + selected_query.projection.post[0], + "projection post-current count", + ), + ( + &mut census.projection_post_contribution_count, + selected_query.projection.post[1], + "projection post-contribution count", + ), + ( + &mut census.projection_post_closure_count, + selected_query.projection.post[2], + "projection post-closure count", + ), + ] { + add(target, value, label)?; + } + if selected_query.projection.post + != [ + trace.proof.current_count(), + trace.proof.contribution_count(), + trace.proof.closure_count(), + ] + { + return Err(integrity( + "query-local projection proof does not describe its retained trace", + )); + } + let current_count = trace.current_keys.len(); + if current_count != trace.current_colors.len() + || current_count != trace.current_component_ranges.len() + || current_count != trace.proof.current_count() as usize + { + return Err(integrity( + "query-local current identity columns have inconsistent lengths", + )); + } + add( + &mut census.dynamic_current_occurrence_count, + checked_u32(current_count, "dynamic current occurrence count")?, + "dynamic current occurrence count", + )?; + + let mut local_to_union = Vec::new(); + local_to_union + .try_reserve_exact(current_count) + .map_err(|error| { + invalid(format!( + "query-family current map allocation failed: {error}" + )) + })?; + let mut normalized_keys = Vec::new(); + normalized_keys + .try_reserve_exact(current_count) + .map_err(|error| { + invalid(format!( + "query-family current key allocation failed: {error}" + )) + })?; + let mut bases = BTreeMap::new(); + for (local_id, ((key, color), [base, count])) in trace + .current_keys + .iter() + .zip(trace.current_colors.iter()) + .zip(trace.current_component_ranges.iter().copied()) + .enumerate() + { + if count == 0 || bases.insert(base, local_id as u32).is_some() { + return Err(integrity( + "query-local current component ranges are empty or ambiguous", + )); + } + add( + &mut census.dynamic_current_component_occurrence_count, + count, + "dynamic current component occurrence count", + )?; + let color_id = colors.intern(color.clone())?; + let normalized = current_key_with_dynamic_color(key, color_id)?; + let family_key = (trace.seed_digest, normalized); + let next = + checked_u32(union_currents.len(), "query-family union current count")?; + let union_id = match union_currents.entry(family_key.clone()) { + std::collections::btree_map::Entry::Vacant(entry) => { + entry.insert(FamilyUnionCurrentRecord { + id: next, + definition: None, + }); + next + } + std::collections::btree_map::Entry::Occupied(entry) => entry.get().id, + }; + local_to_union.push(union_id); + normalized_keys.push(family_key); + } + #[cfg(any(test, feature = "on-the-fly-test-support"))] + { + if trace.current_semantic_digests.len() != local_to_union.len() { + return Err(integrity( + "query-local current semantic identities have an inconsistent length", + )); + } + for (local_id, union_id) in local_to_union.iter().copied().enumerate() { + let semantic_digest = trace.current_semantic_digests[local_id]; + let stage = current_stage(&trace.current_keys[local_id])?; + let component_count = trace.current_component_ranges[local_id][1]; + match observed_current_identities.entry(union_id) { + std::collections::btree_map::Entry::Vacant(entry) => { + entry.insert((semantic_digest, stage, component_count)); + } + std::collections::btree_map::Entry::Occupied(entry) + if *entry.get() == (semantic_digest, stage, component_count) => {} + std::collections::btree_map::Entry::Occupied(_) => { + return Err(integrity( + "query-family union current has inconsistent observed identity", + )); + } + } + } + } + + let mut local_definitions = trace + .current_component_ranges + .iter() + .map(|range| FamilyCurrentDefinition::new(range[1])) + .collect::>(); + let mut contribution_proofs = trace.contribution_proof_rows.iter(); + for operation in trace.operations.iter() { + match operation { + OnTheFlyTraceOperationV1::Source { key, row } => { + let current_id = current_id_by_component_base( + &bases, + row.destination_component_base, + "source destination", + )?; + let definition = &mut local_definitions[current_id as usize]; + let identity = FamilySourceIdentity { + executor: *key, + source_slot: row.source_slot, + momentum: trace_momentum(trace, row.momentum_form_id, "source")?, + source_template_or_dispatch_domain: row + .source_template_or_dispatch_domain, + spin_state_class: row.spin_state_class, + exact_factor: trace_factor(trace, row.exact_factor_id, "source")?, + selector_domain_id: row.selector_domain_id, + }; + if definition.source.replace(identity).is_some() { + return Err(integrity( + "query-local current repeats its source row", + )); + } + add(&mut census.dynamic_source_rows, 1, "dynamic source rows")?; + } + OnTheFlyTraceOperationV1::Contribution { key: executor, row } => { + let proof = contribution_proofs.next().ok_or_else(|| { + integrity("query-local contribution has no cold semantic identity") + })?; + let result = current_id_by_component_base( + &bases, + row.destination_component_base, + "contribution destination", + )?; + if result != proof.result_current_id + || trace_factor(trace, row.exact_factor_id, "contribution")? + != proof.exact_factor + { + return Err(integrity( + "query-local contribution row disagrees with its cold identity", + )); + } + let mut physical_parents = [ + current_id_by_component_base( + &bases, + row.parent0_component_base, + "contribution parent 0", + )?, + current_id_by_component_base( + &bases, + row.parent1_component_base_or_sentinel, + "contribution parent 1", + )?, + ]; + let mut semantic_parents = proof.parent_current_ids; + physical_parents.sort_unstable(); + semantic_parents.sort_unstable(); + if physical_parents != semantic_parents + || !matches!( + row.flags, + 0 | DIRECT_CONTRIBUTION_FLAG_INITIALIZE_DESTINATION + ) + { + return Err(integrity( + "query-local contribution physical row is not its semantic binary row", + )); + } + let identity = FamilyContributionIdentity { + executor: *executor, + key: remapped_contribution_key(proof, &local_to_union)?, + exact_factor: proof.exact_factor, + selector_domain_id: row.selector_domain_id, + }; + if !local_definitions[result as usize] + .contributions + .insert(identity) + { + return Err(integrity( + "query-local current repeats an exact contribution row", + )); + } + add( + &mut census.dynamic_contribution_rows, + 1, + "dynamic contribution rows", + )?; + } + OnTheFlyTraceOperationV1::Finalization { key, row } => { + let current_id = current_id_by_component_base( + &bases, + row.component_base, + "finalization destination", + )?; + let identity = FamilyFinalizationIdentity { + executor: *key, + momentum: trace_momentum( + trace, + row.momentum_form_id, + "finalization", + )?, + component_count: row.component_count, + exact_factor: trace_factor( + trace, + row.exact_factor_id, + "finalization", + )?, + selector_domain_id: row.selector_domain_id, + flags: row.flags, + }; + let definition = &mut local_definitions[current_id as usize]; + if u32::from(row.component_count) != definition.component_count + || definition.finalization.replace(identity).is_some() + { + return Err(integrity( + "query-local current has an invalid finalization row", + )); + } + add( + &mut census.dynamic_finalization_rows, + 1, + "dynamic finalization rows", + )?; + } + OnTheFlyTraceOperationV1::Closure { key, row } => { + if row.amplitude_destination_id != 0 { + return Err(integrity( + "query-local closure does not target its sole amplitude", + )); + } + let parent0 = current_id_by_component_base( + &bases, + row.parent0_component_base, + "closure parent 0", + )?; + let parent1 = current_id_by_component_base( + &bases, + row.parent1_component_base_or_sentinel, + "closure parent 1", + )?; + let component_start = row.component_factor_start as usize; + let component_end = component_start + .checked_add(usize::from(row.component_count)) + .ok_or_else(|| { + integrity("closure component-factor span overflows") + })?; + let component_factors = trace + .exact_factors + .get(component_start..component_end) + .ok_or_else(|| { + integrity("closure component-factor span is absent") + })? + .to_vec() + .into_boxed_slice(); + let amplitude_destination_id = + checked_u32(query_index, "query-family amplitude destination ID")?; + closure_definitions.push(FamilyClosureIdentity { + seed_digest: trace.seed_digest, + query_digest: trace.query_digest, + stage: current_stage(&trace.current_keys[parent0 as usize])? + .max(current_stage(&trace.current_keys[parent1 as usize])?), + executor: *key, + parent_current_ids: [ + local_to_union[parent0 as usize], + local_to_union[parent1 as usize], + ], + parent_momenta: [ + trace_momentum( + trace, + row.parent0_momentum_form_id, + "closure parent 0", + )?, + trace_momentum( + trace, + row.parent1_momentum_form_id_or_sentinel, + "closure parent 1", + )?, + ], + exact_factor: trace_factor(trace, row.exact_factor_id, "closure")?, + component_factors, + component_count: row.component_count, + selector_domain_id: row.selector_domain_id, + proof_group_id: row.flags, + amplitude_destination_id, + }); + closure_groups.insert(( + trace.seed_digest, + current_stage(&trace.current_keys[parent0 as usize])? + .max(current_stage(&trace.current_keys[parent1 as usize])?), + prepared_executor_id(direct_catalog, *key)?, + )); + add(&mut census.dynamic_closure_rows, 1, "dynamic closure rows")?; + } + } + } + if contribution_proofs.next().is_some() { + return Err(integrity( + "query-local cold contribution identities outnumber direct rows", + )); + } + + for ((family_key, definition), union_id) in normalized_keys + .into_iter() + .zip(local_definitions) + .zip(local_to_union) + { + let key = &family_key.1; + match key.node_kind() { + RecurrenceNodeKind::Source + if definition.source.is_none() + || !definition.contributions.is_empty() + || definition.finalization.is_some() => + { + return Err(integrity( + "query-family source current has a non-source definition", + )); + } + RecurrenceNodeKind::Current + if definition.source.is_some() + || definition.contributions.is_empty() => + { + return Err(integrity( + "query-family non-source current has no contribution definition", + )); + } + _ => {} + } + let record = union_currents.get_mut(&family_key).ok_or_else(|| { + integrity("query-family normalized current lost its union identity") + })?; + if record.id != union_id { + return Err(integrity( + "query-family normalized current changed its union identity", + )); + } + match record.definition.as_ref() { + None => record.definition = Some(definition), + Some(established) if established == &definition => {} + Some(_) => { + return Err(integrity(format!( + "query-family semantic current {union_id} has conflicting definitions" + ))); + } + } + } + add(&mut census.query_count, 1, "query-family query count")?; + add( + &mut census.union_amplitude_destination_count, + 1, + "query-family amplitude destination count", + )?; + } + Ok(first_destination..census.query_count) + }; + produce(&mut consume)?; + } + + let Some((source_count, lorentz_component_count, parameter_count)) = layout else { + return Ok(None); + }; + identity_hash.update(census.query_count.to_le_bytes()); + let ordered_query_digest = SemanticDigest::new(identity_hash.finalize().into())?; + + census.source_frame_partition_count = + checked_u32(seed_partitions.len(), "source-frame partition count")?; + let closure_queries = closure_definitions + .iter() + .map(|closure| closure.query_digest) + .collect::>(); + let closure_destinations = closure_definitions + .iter() + .map(|closure| closure.amplitude_destination_id) + .collect::>(); + if closure_queries != seen_queries + || closure_destinations.len() != census.query_count as usize + || closure_destinations + .iter() + .copied() + .enumerate() + .any(|(expected, actual)| actual as usize != expected) + { + return Err(integrity( + "query-family closures do not retain one distinct destination per requested query", + )); + } + for ((seed_digest, key), record) in &union_currents { + let definition = record + .definition + .as_ref() + .ok_or_else(|| integrity("query-family union current has no semantic definition"))?; + add( + &mut census.union_unique_current_component_count, + definition.component_count, + "union current component count", + )?; + let stage = u32::try_from( + key.support_source_slots() + .len() + .checked_sub(1) + .ok_or_else(|| integrity("query-family current has empty source support"))?, + ) + .map_err(|_| invalid("query-family current stage exceeds u32"))?; + if let Some(source) = &definition.source { + add(&mut census.union_source_rows, 1, "union source rows")?; + source_groups.insert(( + *seed_digest, + stage, + prepared_executor_id(direct_catalog, source.executor)?, + )); + } + for contribution in &definition.contributions { + add( + &mut census.union_contribution_rows, + 1, + "union contribution rows", + )?; + contribution_groups.insert(( + *seed_digest, + stage, + prepared_executor_id(direct_catalog, contribution.executor)?, + )); + } + if let Some(finalization) = &definition.finalization { + add( + &mut census.union_finalization_rows, + 1, + "union finalization rows", + )?; + finalization_groups.insert(( + *seed_digest, + stage, + prepared_executor_id(direct_catalog, finalization.executor)?, + )); + } + } + census.union_unique_current_count = checked_u32(union_currents.len(), "union current count")?; + census.union_closure_rows = census.dynamic_closure_rows; + census.union_source_executor_call_groups = + checked_u32(source_groups.len(), "union source executor groups")?; + census.union_contribution_executor_call_groups = checked_u32( + contribution_groups.len(), + "union contribution executor groups", + )?; + census.union_finalization_executor_call_groups = checked_u32( + finalization_groups.len(), + "union finalization executor groups", + )?; + census.union_closure_executor_call_groups = + checked_u32(closure_groups.len(), "union closure executor groups")?; + census.dynamic_source_calls = census.dynamic_source_rows; + census.dynamic_contribution_calls = census.dynamic_contribution_rows; + census.dynamic_finalization_calls = census.dynamic_finalization_rows; + census.dynamic_closure_calls = census.dynamic_closure_rows; + + if census.projection_post_current_count != census.dynamic_current_occurrence_count + || census.projection_post_contribution_count != census.dynamic_contribution_rows + || census.projection_post_closure_count != census.dynamic_closure_rows + || census.union_unique_current_count > census.dynamic_current_occurrence_count + || census.union_unique_current_component_count + > census.dynamic_current_component_occurrence_count + || census.union_source_rows > census.dynamic_source_rows + || census.union_contribution_rows > census.dynamic_contribution_rows + || census.union_finalization_rows > census.dynamic_finalization_rows + { + return Err(integrity( + "query-family union census is inconsistent with its local traces", + )); + } + let family = materialize_query_family( + direct_catalog, + census, + source_count, + lorentz_component_count, + parameter_count, + union_currents, + closure_definitions, + #[cfg(any(test, feature = "on-the-fly-test-support"))] + observed_current_identities, + )?; + Ok(Some((family, ordered_query_digest))) +} + +fn build_query_family_from_traces( + direct_catalog: &PreparedDirectExecutorCatalog, + selected: &[QueryFamilyTraceInput<'_>], +) -> RusticolResult { + build_query_family_from_chunks(direct_catalog, |consumer| { + let destinations = consumer.push_chunk(selected)?; + if destinations.start != 0 || destinations.end as usize != selected.len() { + return Err(integrity( + "query-family one-shot destination range is inconsistent", + )); + } + Ok(()) + })? + .map(|(family, _)| family) + .ok_or_else(|| invalid("query-family census requires at least one query")) +} + +pub(crate) fn build_streamed_query_family_candidate_v1( + direct_catalog: &PreparedDirectExecutorCatalog, + produce: impl FnOnce(&mut dyn OnTheFlyQueryFamilyChunkConsumerV1) -> RusticolResult<()>, +) -> RusticolResult> { + let direct_catalog_digest = direct_catalog.direct_template_catalog_digest(); + build_query_family_from_chunks(direct_catalog, produce).map(|candidate| { + candidate.map( + |(family, ordered_query_digest)| OnTheFlyStreamedQueryFamilyCandidateV1 { + identity: QueryFamilyCacheIdentityV1::Streamed { + direct_catalog_digest, + query_count: family.census.query_count, + ordered_query_digest, + }, + family, + }, + ) + }) +} + +#[allow(dead_code)] // Used only by the feature-only query-family census API. +fn query_family_census_from_traces( + direct_catalog: &PreparedDirectExecutorCatalog, + selected: &[QueryFamilyTraceInput<'_>], +) -> RusticolResult { + Ok(build_query_family_from_traces(direct_catalog, selected)?.census) +} + +#[allow(clippy::too_many_arguments)] +fn materialize_query_family( + direct_catalog: &PreparedDirectExecutorCatalog, + census: OnTheFlyQueryFamilyCensusV1, + source_count: u32, + lorentz_component_count: u16, + parameter_count: u32, + union_currents: BTreeMap<(SemanticDigest, CurrentCoreKey), FamilyUnionCurrentRecord>, + closure_definitions: Vec, + #[cfg(any(test, feature = "on-the-fly-test-support"))] observed_current_identities: BTreeMap< + u32, + (SemanticDigest, u32, u32), + >, +) -> RusticolResult { + type GroupKey = (SemanticDigest, u32); + let mut records = std::iter::repeat_with(|| None) + .take(union_currents.len()) + .collect::>>(); + for (family_key, record) in union_currents { + let definition = record + .definition + .ok_or_else(|| integrity("query-family union current has no semantic definition"))?; + let slot = records + .get_mut(record.id as usize) + .ok_or_else(|| integrity("query-family union current ID is out of bounds"))?; + if slot + .replace((family_key.0, family_key.1, definition)) + .is_some() + { + return Err(integrity( + "query-family union current ID repeats a definition", + )); + } + } + if records.iter().any(Option::is_none) { + return Err(integrity( + "query-family union current IDs are not dense zero-based", + )); + } + let mut component_bases = Vec::new(); + let mut component_counts = Vec::new(); + let mut stages = Vec::new(); + let mut current_component_count = 0_u32; + component_bases + .try_reserve_exact(records.len()) + .map_err(|error| { + invalid(format!( + "query-family component-base allocation failed: {error}" + )) + })?; + component_counts + .try_reserve_exact(records.len()) + .map_err(|error| { + invalid(format!( + "query-family component-count allocation failed: {error}" + )) + })?; + stages + .try_reserve_exact(records.len()) + .map_err(|error| invalid(format!("query-family stage allocation failed: {error}")))?; + for record in &records { + let (_, key, definition) = record + .as_ref() + .expect("validated dense query-family record"); + component_bases.push(current_component_count); + component_counts.push(definition.component_count); + current_component_count = current_component_count + .checked_add(definition.component_count) + .ok_or_else(|| invalid("query-family current component count exceeds u32"))?; + stages.push(current_stage(key)?); + } + if current_component_count != census.union_unique_current_component_count { + return Err(integrity( + "query-family materialized current shape differs from its census", + )); + } + #[cfg(any(test, feature = "on-the-fly-test-support"))] + let observed_currents = component_bases + .iter() + .copied() + .enumerate() + .map(|(current_id, component_base)| { + let current_id = checked_u32(current_id, "observed query-family current ID")?; + let (semantic_digest, stage, component_count) = observed_current_identities + .get(¤t_id) + .copied() + .ok_or_else(|| integrity("query-family observed current identity is absent"))?; + Ok(OnTheFlyFamilyObservedCurrentDescriptorV1 { + semantic_digest, + stage, + component_base, + component_count, + }) + }) + .collect::>>()? + .into_boxed_slice(); + + let mut momentum_ids = BTreeMap::new(); + let mut momentum_forms = Vec::new(); + let mut factor_ids = BTreeMap::new(); + let mut factor_block_ids = BTreeMap::new(); + let mut exact_factors = Vec::new(); + let mut source_groups = BTreeMap::>::new(); + let mut contribution_groups = + BTreeMap::>>::new(); + let mut finalization_groups = + BTreeMap::>>::new(); + let mut closure_groups = BTreeMap::>>::new(); + + // Consume semantic definitions as their compact executable drafts are + // produced, rather than retaining both complete representations until + // closure materialization. + for (current_id, record) in records.into_iter().enumerate() { + let (seed_digest, _key, definition) = record.expect("validated dense query-family record"); + let current_id = checked_u32(current_id, "query-family current ID")?; + let stage = stages[current_id as usize]; + let destination_component_base = component_bases[current_id as usize]; + if let Some(source) = &definition.source { + if stage != 0 { + return Err(integrity( + "query-family source current is not in support stage zero", + )); + } + let (direct_executor_id, parent_permutation) = + prepared_binding(direct_catalog, source.executor)?; + if parent_permutation != [0, 1] { + return Err(integrity( + "query-family source executor has a parent permutation", + )); + } + let row = DirectSourceRow { + source_slot: source.source_slot, + destination_component_base, + momentum_form_id: intern_momentum( + &mut momentum_ids, + &mut momentum_forms, + &source.momentum, + )?, + source_template_or_dispatch_domain: source.source_template_or_dispatch_domain, + spin_state_class: source.spin_state_class, + exact_factor_id: intern_factor( + &mut factor_ids, + &mut exact_factors, + source.exact_factor, + )?, + selector_domain_id: source.selector_domain_id, + }; + source_groups + .entry((seed_digest, direct_executor_id)) + .or_default() + .push(FamilySourceDraft { + seed_digest, + stage, + direct_executor_id, + key: source.executor, + row, + }); + } + for contribution in &definition.contributions { + let parent_ids = contribution.key.parent_value_class_ids(); + let parent_momenta = contribution.key.parent_momenta(); + if parent_ids.len() != 2 || parent_momenta.len() != 2 { + return Err(integrity("query-family direct contribution is not binary")); + } + let mut parent_ids = [parent_ids[0], parent_ids[1]]; + let mut parent_momenta = [&parent_momenta[0], &parent_momenta[1]]; + for parent_id in parent_ids { + let parent_stage = stages + .get(parent_id as usize) + .copied() + .ok_or_else(|| integrity("query-family contribution parent is absent"))?; + if parent_stage >= stage { + return Err(integrity( + "query-family contribution parent is not in an earlier support stage", + )); + } + } + let (direct_executor_id, parent_permutation) = + prepared_binding(direct_catalog, contribution.executor)?; + match parent_permutation { + [0, 1] => {} + [1, 0] => { + parent_ids.swap(0, 1); + parent_momenta.swap(0, 1); + } + _ => { + return Err(integrity( + "query-family contribution has an invalid prepared parent permutation", + )); + } + } + let row = DirectContributionRow { + parent0_component_base: component_bases[parent_ids[0] as usize], + parent1_component_base_or_sentinel: component_bases[parent_ids[1] as usize], + parent0_momentum_form_id: intern_momentum( + &mut momentum_ids, + &mut momentum_forms, + parent_momenta[0], + )?, + parent1_momentum_form_id_or_sentinel: intern_momentum( + &mut momentum_ids, + &mut momentum_forms, + parent_momenta[1], + )?, + destination_component_base, + exact_factor_id: intern_factor( + &mut factor_ids, + &mut exact_factors, + contribution.exact_factor, + )?, + selector_domain_id: contribution.selector_domain_id, + flags: 0, + }; + contribution_groups + .entry(stage) + .or_default() + .entry((seed_digest, direct_executor_id)) + .or_default() + .push(FamilyContributionDraft { + seed_digest, + stage, + destination_current_id: current_id, + direct_executor_id, + key: contribution.executor, + parent_permutation, + row, + }); + } + if let Some(finalization) = &definition.finalization { + let (direct_executor_id, parent_permutation) = + prepared_binding(direct_catalog, finalization.executor)?; + if parent_permutation != [0, 1] { + return Err(integrity( + "query-family finalization executor has a parent permutation", + )); + } + let row = DirectFinalizationRow { + component_base: destination_component_base, + component_count: finalization.component_count, + momentum_form_id: intern_momentum( + &mut momentum_ids, + &mut momentum_forms, + &finalization.momentum, + )?, + exact_factor_id: intern_factor( + &mut factor_ids, + &mut exact_factors, + finalization.exact_factor, + )?, + selector_domain_id: finalization.selector_domain_id, + flags: finalization.flags, + }; + finalization_groups + .entry(stage) + .or_default() + .entry((seed_digest, direct_executor_id)) + .or_default() + .push(FamilyFinalizationDraft { + seed_digest, + stage, + direct_executor_id, + key: finalization.executor, + row, + }); + } + } + for closure in closure_definitions { + let [parent0, parent1] = closure.parent_current_ids; + let parent0_count = component_counts + .get(parent0 as usize) + .copied() + .ok_or_else(|| integrity("query-family closure parent 0 is absent"))?; + let parent1_count = component_counts + .get(parent1 as usize) + .copied() + .ok_or_else(|| integrity("query-family closure parent 1 is absent"))?; + if parent0_count != u32::from(closure.component_count) + || parent1_count != u32::from(closure.component_count) + || closure.component_factors.len() != usize::from(closure.component_count) + { + return Err(integrity( + "query-family closure component shape disagrees with its parents", + )); + } + let (direct_executor_id, parent_permutation) = + prepared_binding(direct_catalog, closure.executor)?; + if parent_permutation != [0, 1] { + return Err(integrity( + "query-family closure executor has a parent permutation", + )); + } + let exact_factor_id = + intern_factor(&mut factor_ids, &mut exact_factors, closure.exact_factor)?; + let component_factor_start = intern_factor_block( + &mut factor_block_ids, + &mut factor_ids, + &mut exact_factors, + &closure.component_factors, + )?; + let stage = stages[parent0 as usize].max(stages[parent1 as usize]); + if stage != closure.stage { + return Err(integrity( + "query-family closure stage changed after current remapping", + )); + } + let row = DirectClosureRow { + parent0_component_base: component_bases[parent0 as usize], + parent1_component_base_or_sentinel: component_bases[parent1 as usize], + parent0_momentum_form_id: intern_momentum( + &mut momentum_ids, + &mut momentum_forms, + &closure.parent_momenta[0], + )?, + parent1_momentum_form_id_or_sentinel: intern_momentum( + &mut momentum_ids, + &mut momentum_forms, + &closure.parent_momenta[1], + )?, + amplitude_destination_id: closure.amplitude_destination_id, + exact_factor_id, + component_factor_start, + component_count: closure.component_count, + selector_domain_id: closure.selector_domain_id, + flags: closure.proof_group_id, + }; + closure_groups + .entry(stage) + .or_default() + .entry((closure.seed_digest, direct_executor_id)) + .or_default() + .push(FamilyClosureDraft { + seed_digest: closure.seed_digest, + stage, + direct_executor_id, + key: closure.executor, + row, + }); + } + + let row_groups = ordered_family_row_groups( + source_groups, + contribution_groups, + finalization_groups, + closure_groups, + )?; + validate_ordered_family_schedule(&row_groups)?; + let group_count = |role| row_groups.iter().filter(|group| group.role == role).count(); + if checked_u32( + group_count(DirectExecutorRole::Source), + "source group count", + )? != census.union_source_executor_call_groups + || checked_u32( + group_count(DirectExecutorRole::Contribution), + "contribution group count", + )? != census.union_contribution_executor_call_groups + || checked_u32( + group_count(DirectExecutorRole::Finalization), + "finalization group count", + )? != census.union_finalization_executor_call_groups + || checked_u32( + group_count(DirectExecutorRole::Closure), + "closure group count", + )? != census.union_closure_executor_call_groups + { + return Err(integrity( + "query-family executable groups differ from the structural census", + )); + } + + Ok(OnTheFlyBuiltQueryFamilyV1 { + census, + source_count, + lorentz_component_count, + parameter_count, + current_component_count, + amplitude_destination_count: census.union_amplitude_destination_count, + momentum_forms: momentum_forms.into_boxed_slice(), + exact_factors: exact_factors.into_boxed_slice(), + row_groups: row_groups.into_boxed_slice(), + #[cfg(any(test, feature = "on-the-fly-test-support"))] + observed_currents, + }) +} + +fn validate_ordered_family_schedule(groups: &[OnTheFlyFamilyRowGroupV1]) -> RusticolResult<()> { + let mut group_keys = BTreeSet::new(); + let mut saw_non_source = false; + let mut saw_closure = false; + let mut current_stage = 0_u32; + let mut finalization_seen_in_stage = false; + for group in groups { + if group.rows.len() == 0 + || !group_keys.insert(( + group.seed_digest, + group.stage, + group.role, + group.direct_executor_id, + )) + { + return Err(integrity( + "query-family schedule has an empty or duplicate row group", + )); + } + match group.role { + DirectExecutorRole::Source => { + if saw_non_source || group.stage != 0 { + return Err(integrity( + "query-family sources are not the first stage-zero groups", + )); + } + } + DirectExecutorRole::Contribution => { + saw_non_source = true; + if saw_closure || group.stage < current_stage { + return Err(integrity( + "query-family contribution stages are not topological", + )); + } + if group.stage > current_stage { + current_stage = group.stage; + finalization_seen_in_stage = false; + } + if finalization_seen_in_stage { + return Err(integrity( + "query-family contribution follows finalization in one stage", + )); + } + } + DirectExecutorRole::Finalization => { + saw_non_source = true; + if saw_closure || group.stage < current_stage { + return Err(integrity( + "query-family finalization stages are not topological", + )); + } + if group.stage > current_stage { + current_stage = group.stage; + } + finalization_seen_in_stage = true; + } + DirectExecutorRole::Closure => { + saw_non_source = true; + saw_closure = true; + } + } + } + Ok(()) +} + +fn binding_members( + bindings: impl IntoIterator, +) -> Box<[(OnTheFlyExecutorKeyV1, [u8; 2])]> { + bindings + .into_iter() + .collect::>() + .into_iter() + .collect::>() + .into_boxed_slice() +} + +fn ordered_family_row_groups( + source_groups: BTreeMap<(SemanticDigest, u32), Vec>, + mut contribution_groups: BTreeMap< + u32, + BTreeMap<(SemanticDigest, u32), Vec>, + >, + mut finalization_groups: BTreeMap< + u32, + BTreeMap<(SemanticDigest, u32), Vec>, + >, + closure_groups: BTreeMap>>, +) -> RusticolResult> { + let mut groups = Vec::new(); + for ((seed_digest, direct_executor_id), mut drafts) in source_groups { + drafts.sort_by_key(|draft| { + ( + draft.row.destination_component_base, + draft.row.source_slot, + draft.key, + ) + }); + let representative = drafts + .first() + .ok_or_else(|| integrity("query-family source group is empty"))?; + if drafts.iter().any(|draft| { + draft.seed_digest != seed_digest + || draft.stage != 0 + || draft.direct_executor_id != direct_executor_id + }) { + return Err(integrity("query-family source group key is inconsistent")); + } + groups.push(OnTheFlyFamilyRowGroupV1 { + seed_digest, + stage: 0, + role: DirectExecutorRole::Source, + direct_executor_id, + representative_key: representative.key, + member_bindings: binding_members(drafts.iter().map(|draft| (draft.key, [0, 1]))), + rows: OnTheFlyFamilyRowsV1::Source( + drafts + .into_iter() + .map(|draft| draft.row) + .collect::>() + .into_boxed_slice(), + ), + }); + } + + let mut schedule_stages = contribution_groups + .keys() + .chain(finalization_groups.keys()) + .copied() + .collect::>(); + let mut initialized_destinations = BTreeSet::::new(); + for stage in std::mem::take(&mut schedule_stages) { + for ((seed_digest, direct_executor_id), mut drafts) in + contribution_groups.remove(&stage).unwrap_or_default() + { + crate::recurrence::direct_lowering::order_contributions_for_runtime_fanout_by( + &mut drafts, + |draft| { + ( + draft.stage, + draft.direct_executor_id, + draft.row, + (draft.destination_current_id, draft.key), + ) + }, + ); + if drafts.iter().any(|draft| { + draft.seed_digest != seed_digest + || draft.stage != stage + || draft.direct_executor_id != direct_executor_id + }) { + return Err(integrity( + "query-family contribution group key is inconsistent", + )); + } + for draft in &mut drafts { + draft.row.flags = if initialized_destinations.insert(draft.destination_current_id) { + DIRECT_CONTRIBUTION_FLAG_INITIALIZE_DESTINATION + } else { + 0 + }; + } + let representative = drafts + .first() + .ok_or_else(|| integrity("query-family contribution group is empty"))?; + groups.push(OnTheFlyFamilyRowGroupV1 { + seed_digest, + stage, + role: DirectExecutorRole::Contribution, + direct_executor_id, + representative_key: representative.key, + member_bindings: binding_members( + drafts + .iter() + .map(|draft| (draft.key, draft.parent_permutation)), + ), + rows: OnTheFlyFamilyRowsV1::Contribution( + drafts + .into_iter() + .map(|draft| draft.row) + .collect::>() + .into_boxed_slice(), + ), + }); + } + for ((seed_digest, direct_executor_id), mut drafts) in + finalization_groups.remove(&stage).unwrap_or_default() + { + drafts.sort_by_key(|draft| (draft.row.component_base, draft.key)); + let representative = drafts + .first() + .ok_or_else(|| integrity("query-family finalization group is empty"))?; + if drafts.iter().any(|draft| { + draft.seed_digest != seed_digest + || draft.stage != stage + || draft.direct_executor_id != direct_executor_id + }) { + return Err(integrity( + "query-family finalization group key is inconsistent", + )); + } + groups.push(OnTheFlyFamilyRowGroupV1 { + seed_digest, + stage, + role: DirectExecutorRole::Finalization, + direct_executor_id, + representative_key: representative.key, + member_bindings: binding_members(drafts.iter().map(|draft| (draft.key, [0, 1]))), + rows: OnTheFlyFamilyRowsV1::Finalization( + drafts + .into_iter() + .map(|draft| draft.row) + .collect::>() + .into_boxed_slice(), + ), + }); + } + } + if !contribution_groups.is_empty() || !finalization_groups.is_empty() { + return Err(integrity( + "query-family stage schedule did not consume every row group", + )); + } + + for (stage, stage_groups) in closure_groups { + for ((seed_digest, direct_executor_id), mut drafts) in stage_groups { + drafts.sort_by_key(|draft| { + ( + draft.row.amplitude_destination_id, + draft.row.parent0_component_base, + draft.row.parent1_component_base_or_sentinel, + draft.key, + ) + }); + let representative = drafts + .first() + .ok_or_else(|| integrity("query-family closure group is empty"))?; + if drafts.iter().any(|draft| { + draft.seed_digest != seed_digest + || draft.stage != stage + || draft.direct_executor_id != direct_executor_id + }) { + return Err(integrity("query-family closure group key is inconsistent")); + } + groups.push(OnTheFlyFamilyRowGroupV1 { + seed_digest, + stage, + role: DirectExecutorRole::Closure, + direct_executor_id, + representative_key: representative.key, + member_bindings: binding_members(drafts.iter().map(|draft| (draft.key, [0, 1]))), + rows: OnTheFlyFamilyRowsV1::Closure( + drafts + .into_iter() + .map(|draft| draft.row) + .collect::>() + .into_boxed_slice(), + ), + }); + } + } + Ok(groups) +} + +fn family_factor_parts(value: ExactComplexRational) -> RusticolResult<(f64, f64)> { + let real = value.real().numerator() as f64 / value.real().denominator() as f64; + let imag = value.imag().numerator() as f64 / value.imag().denominator() as f64; + if !real.is_finite() || !imag.is_finite() { + return Err(invalid( + "query-family exact factor cannot be represented as finite binary64", + )); + } + Ok((real, imag)) +} + +fn family_scalar_len(planes: u32, stride: u32, label: &str) -> RusticolResult { + usize::try_from(planes) + .ok() + .and_then(|planes| planes.checked_mul(stride as usize)) + .ok_or_else(|| invalid(format!("query-family {label} scalar length exceeds usize"))) +} + +struct OnTheFlyFamilyWorkspaceV1 { + current_re: AlignedF64Buffer, + current_im: AlignedF64Buffer, + amplitude_re: AlignedF64Buffer, + amplitude_im: AlignedF64Buffer, + momenta: AlignedF64Buffer, + parameters_re: AlignedF64Buffer, + parameters_im: AlignedF64Buffer, + factors_re: AlignedF64Buffer, + factors_im: AlignedF64Buffer, + logical_point_capacity: u32, + active_point_count: u32, + source_count: u32, + momentum_form_count: u32, + lorentz_component_count: u16, + point_stride: u32, +} + +impl OnTheFlyFamilyWorkspaceV1 { + fn new( + family: &OnTheFlyBuiltQueryFamilyV1, + logical_point_capacity: u32, + packed_singleton_capable: bool, + ) -> RusticolResult { + if family.source_count == 0 + || family.lorentz_component_count == 0 + || logical_point_capacity == 0 + || family.momentum_forms.is_empty() + || family.exact_factors.is_empty() + || family.current_component_count == 0 + || family.amplitude_destination_count == 0 + { + return Err(integrity( + "query-family has an empty authenticated workspace shape", + )); + } + let point_stride = if logical_point_capacity == 1 && packed_singleton_capable { + 1 + } else { + checked_aligned_point_stride(logical_point_capacity)? + }; + let current_len = family_scalar_len( + family.current_component_count, + point_stride, + "current arena", + )?; + let amplitude_len = family_scalar_len( + family.amplitude_destination_count, + point_stride, + "amplitude arena", + )?; + let momentum_form_count = checked_u32( + family.momentum_forms.len(), + "query-family momentum-form count", + )?; + let momentum_planes = momentum_form_count + .checked_mul(u32::from(family.lorentz_component_count)) + .ok_or_else(|| invalid("query-family momentum plane count exceeds u32"))?; + let momentum_len = family_scalar_len(momentum_planes, point_stride, "momentum arena")?; + // The family is immutable after preparation. Check its momentum + // sources here rather than once per term and phase-space point on + // every warmed evaluation. + if family + .momentum_forms + .iter() + .flat_map(CanonicalMomentumLinearForm::terms) + .any(|term| term.source_slot >= family.source_count) + { + return Err(integrity( + "query-family momentum source slot is out of bounds", + )); + } + let mut factors_re = + AlignedF64Buffer::zeroed(family.exact_factors.len(), "query-family factor real")?; + let mut factors_im = + AlignedF64Buffer::zeroed(family.exact_factors.len(), "query-family factor imaginary")?; + for (index, factor) in family.exact_factors.iter().copied().enumerate() { + let (real, imag) = family_factor_parts(factor)?; + factors_re.as_mut_slice()[index] = real; + factors_im.as_mut_slice()[index] = imag; + } + let parameter_count = usize::try_from(family.parameter_count) + .map_err(|_| invalid("query-family parameter count exceeds usize"))?; + Ok(Self { + current_re: AlignedF64Buffer::zeroed(current_len, "query-family current real")?, + current_im: AlignedF64Buffer::zeroed(current_len, "query-family current imaginary")?, + amplitude_re: AlignedF64Buffer::zeroed(amplitude_len, "query-family amplitude real")?, + amplitude_im: AlignedF64Buffer::zeroed( + amplitude_len, + "query-family amplitude imaginary", + )?, + momenta: AlignedF64Buffer::zeroed(momentum_len, "query-family momenta")?, + parameters_re: AlignedF64Buffer::zeroed( + parameter_count, + "query-family parameter real", + )?, + parameters_im: AlignedF64Buffer::zeroed( + parameter_count, + "query-family parameter imaginary", + )?, + factors_re, + factors_im, + logical_point_capacity, + active_point_count: 0, + source_count: family.source_count, + momentum_form_count, + lorentz_component_count: family.lorentz_component_count, + point_stride, + }) + } + + fn refresh_inputs( + &mut self, + family: &OnTheFlyBuiltQueryFamilyV1, + external_momenta: &[f64], + point_count: u32, + ) -> RusticolResult<()> { + self.active_point_count = 0; + if point_count == 0 || point_count > self.logical_point_capacity { + return Err(invalid( + "query-family active point count is outside the workspace", + )); + } + let expected = usize::try_from(self.source_count) + .ok() + .and_then(|sources| sources.checked_mul(usize::from(self.lorentz_component_count))) + .and_then(|planes| planes.checked_mul(point_count as usize)) + .ok_or_else(|| invalid("query-family external momentum shape exceeds usize"))?; + if external_momenta.len() != expected { + return Err(invalid(format!( + "query-family received {} momentum scalars, expected {expected}", + external_momenta.len() + ))); + } + let point_count = point_count as usize; + let lorentz_count = usize::from(self.lorentz_component_count); + let point_stride = self.point_stride as usize; + for (form, form_planes) in family.momentum_forms.iter().zip( + self.momenta + .as_mut_slice() + .chunks_exact_mut(lorentz_count * point_stride), + ) { + for (lorentz, plane) in form_planes.chunks_exact_mut(point_stride).enumerate() { + let output = &mut plane[..point_count]; + let Some((first, remaining)) = form.terms().split_first() else { + output.fill(0.0); + continue; + }; + let input_start = + (first.source_slot as usize * lorentz_count + lorentz) * point_count; + let coefficient = f64::from(first.coefficient); + for (value, source) in output + .iter_mut() + .zip(&external_momenta[input_start..input_start + point_count]) + { + // Retain the original initial addition, including its + // treatment of signed zero. Terms remain in their original + // order; only independent phase-space points are grouped. + *value = 0.0 + coefficient * source; + } + for term in remaining { + let input_start = + (term.source_slot as usize * lorentz_count + lorentz) * point_count; + let coefficient = f64::from(term.coefficient); + for (value, source) in output + .iter_mut() + .zip(&external_momenta[input_start..input_start + point_count]) + { + *value += coefficient * source; + } + } + } + } + // Source rows overwrite their complete destination and family lowering + // marks exactly the first contribution to every recursive current with + // INITIALIZE_DESTINATION. Finalization is in-place only after that + // write. Clearing the current arena here therefore duplicates writes + // on every warmed evaluation. Closures accumulate into amplitudes, so + // their active prefix must still start at zero. + for plane in 0..self.amplitude_re.len() / self.point_stride as usize { + let start = plane * self.point_stride as usize; + let end = start + point_count; + self.amplitude_re.as_mut_slice()[start..end].fill(0.0); + self.amplitude_im.as_mut_slice()[start..end].fill(0.0); + } + Ok(()) + } + + fn refresh_parameters(&mut self, parameters: &[(f64, f64)]) -> RusticolResult<()> { + self.active_point_count = 0; + if parameters.len() != self.parameters_re.len() { + return Err(invalid(format!( + "query-family received {} parameters, expected {}", + parameters.len(), + self.parameters_re.len() + ))); + } + for (index, &(real, imag)) in parameters.iter().enumerate() { + if !real.is_finite() || !imag.is_finite() { + return Err(invalid("query-family parameter value is not finite")); + } + self.parameters_re.as_mut_slice()[index] = real; + self.parameters_im.as_mut_slice()[index] = imag; + } + Ok(()) + } + + fn raw_views( + &mut self, + ) -> RusticolResult<( + DirectArenaView, + DirectMomentumView, + DirectParameterView, + DirectFactorView, + )> { + let arena = DirectArenaView { + current_re: self.current_re.as_mut_ptr(), + current_im: self.current_im.as_mut_ptr(), + current_scalar_len: self.current_re.len() as u64, + amplitude_re: self.amplitude_re.as_mut_ptr(), + amplitude_im: self.amplitude_im.as_mut_ptr(), + amplitude_scalar_len: self.amplitude_re.len() as u64, + point_stride: self.point_stride, + }; + let momenta = DirectMomentumView { + values: self.momenta.as_ptr(), + scalar_len: self.momenta.len() as u64, + form_count: self.momentum_form_count, + lorentz_component_count: self.lorentz_component_count, + point_stride: self.point_stride, + }; + let parameters = DirectParameterView { + values_re: self.parameters_re.as_ptr(), + values_im: self.parameters_im.as_ptr(), + value_count: self.parameters_re.len() as u32, + }; + let factors = DirectFactorView { + values_re: self.factors_re.as_ptr(), + values_im: self.factors_im.as_ptr(), + value_count: self.factors_re.len() as u32, + }; + validate_direct_views(arena, momenta, parameters, factors)?; + Ok((arena, momenta, parameters, factors)) + } + + fn write_outputs( + &self, + destination_count: u32, + point_count: u32, + outputs: &mut [(f64, f64)], + ) -> RusticolResult<()> { + if self.active_point_count != point_count || destination_count == 0 { + return Err(integrity( + "query-family outputs do not belong to the last successful execution", + )); + } + Self::validate_output_shape(destination_count, point_count, outputs.len())?; + for destination in 0..destination_count { + for point in 0..point_count { + let arena_index = usize::try_from(destination) + .ok() + .and_then(|destination| destination.checked_mul(self.point_stride as usize)) + .and_then(|base| base.checked_add(point as usize)) + .ok_or_else(|| integrity("query-family amplitude index exceeds usize"))?; + let output_index = usize::try_from(destination) + .ok() + .and_then(|destination| destination.checked_mul(point_count as usize)) + .and_then(|base| base.checked_add(point as usize)) + .ok_or_else(|| integrity("query-family output index exceeds usize"))?; + outputs[output_index] = ( + *self + .amplitude_re + .as_slice() + .get(arena_index) + .ok_or_else(|| integrity("query-family amplitude real value is absent"))?, + *self + .amplitude_im + .as_slice() + .get(arena_index) + .ok_or_else(|| { + integrity("query-family amplitude imaginary value is absent") + })?, + ); + } + } + Ok(()) + } + + #[cfg(any(test, feature = "on-the-fly-test-support"))] + fn observed_currents( + &self, + descriptors: &[OnTheFlyFamilyObservedCurrentDescriptorV1], + point_index: u32, + ) -> RusticolResult> { + if point_index >= self.active_point_count { + return Err(invalid( + "query-family observed current point is outside the last successful execution", + )); + } + let stride = self.point_stride as usize; + let point_index = point_index as usize; + descriptors + .iter() + .map(|descriptor| { + let mut values = Vec::new(); + values + .try_reserve_exact(descriptor.component_count as usize) + .map_err(|error| { + invalid(format!( + "query-family observed current allocation failed: {error}" + )) + })?; + for component in 0..descriptor.component_count as usize { + let plane = descriptor.component_base as usize + component; + let index = plane + .checked_mul(stride) + .and_then(|base| base.checked_add(point_index)) + .ok_or_else(|| { + integrity("query-family observed current index exceeds usize") + })?; + values.push(( + *self.current_re.as_slice().get(index).ok_or_else(|| { + integrity("query-family observed current real value is absent") + })?, + *self.current_im.as_slice().get(index).ok_or_else(|| { + integrity("query-family observed current imaginary value is absent") + })?, + )); + } + Ok(OnTheFlyFamilyObservedCurrentV1 { + semantic_digest: descriptor.semantic_digest, + stage: descriptor.stage, + values, + }) + }) + .collect() + } + + fn validate_output_shape( + destination_count: u32, + point_count: u32, + output_len: usize, + ) -> RusticolResult<()> { + if destination_count == 0 || point_count == 0 { + return Err(integrity("query-family output shape is empty")); + } + let expected = usize::try_from(destination_count) + .ok() + .and_then(|destinations| destinations.checked_mul(point_count as usize)) + .ok_or_else(|| invalid("query-family output shape exceeds usize"))?; + if output_len != expected { + return Err(invalid(format!( + "query-family output has {} complex values, expected {expected}", + output_len + ))); + } + Ok(()) + } +} + +struct BoundOnTheFlyQueryFamilyV1 { + identity: QueryFamilyCacheIdentityV1, + strategy: RecurrenceStrategy, + // Single ownership keeps every row address stable for prepared descriptor caches. + family: OnTheFlyBuiltQueryFamilyV1, + workspace: OnTheFlyFamilyWorkspaceV1, + resolved_groups: Box<[ResolvedOnTheFlyExecutor]>, + /// Cold, process-bound conjunction over every executor used by this + /// family. It is reused when a retained workspace grows, so a packed + /// singleton can never survive a later batched activation. + packed_singleton_capable: bool, + interaction_program: DirectInteractionProgram, + singleton_fanout_program: DirectSingletonContributionFanoutProgram, + applied_parameter_version: u64, + descriptor_exposed: bool, +} + +/// Runtime-private stable address of one successfully warmed family. The +/// generation prevents a handle retained above this executor from aliasing a +/// different family after replacement or `clear_families` resets the arena. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) struct OnTheFlyQueryFamilyHandleV1 { + generation: u64, + index: usize, +} + +pub(crate) struct OnTheFlyQueryFamilyExecutorV1 { + // Field order is part of the ownership contract. Retained and pending rows + // must disappear before the resolver drops the prepared contexts that + // their copied handles address. + families: Vec, + last_used: Option, + pending: Option, + resolver: R, + parameter_state: Vec<(f64, f64)>, + parameter_version: u64, + family_generation: u64, +} + +impl OnTheFlyQueryFamilyExecutorV1 { + pub(crate) const fn new(resolver: R) -> Self { + Self { + families: Vec::new(), + last_used: None, + pending: None, + resolver, + parameter_state: Vec::new(), + parameter_version: 0, + family_generation: 0, + } + } + + pub(crate) const fn resolver(&self) -> &R { + &self.resolver + } + + pub(crate) const fn resolver_mut(&mut self) -> &mut R { + &mut self.resolver + } + + pub(crate) fn set_parameters(&mut self, parameters: &[(f64, f64)]) -> RusticolResult<()> { + if parameters + .iter() + .any(|(real, imag)| !real.is_finite() || !imag.is_finite()) + { + return Err(invalid("query-family parameter value is not finite")); + } + if self.parameter_state == parameters { + return Ok(()); + } + let mut replacement = Vec::new(); + replacement + .try_reserve_exact(parameters.len()) + .map_err(|error| { + invalid(format!( + "query-family parameter-state allocation failed: {error}" + )) + })?; + replacement.extend_from_slice(parameters); + self.parameter_state = replacement; + self.parameter_version = self + .parameter_version + .checked_add(1) + .ok_or_else(|| invalid("query-family parameter version exceeds u64"))?; + Ok(()) + } + + /// Exact census of the row family selected by the last cold prepare. + pub(crate) fn prepared_census(&self) -> Option { + self.pending + .as_ref() + .or_else(|| self.last_used.and_then(|index| self.families.get(index))) + .map(|family| family.family.census) + } + + #[allow(dead_code)] // Test-support inspection seam. + pub(crate) const fn retained_family_count(&self) -> usize { + self.families.len() + } + + pub(crate) fn active_retained_handle(&self) -> Option { + self.pending.is_none().then_some(())?; + Some(OnTheFlyQueryFamilyHandleV1 { + generation: self.family_generation, + index: self.last_used?, + }) + } + + fn cache_identity( + direct_catalog: &PreparedDirectExecutorCatalog, + selected: &[QueryFamilyTraceInput<'_>], + ) -> QueryFamilyCacheIdentityV1 { + QueryFamilyCacheIdentityV1::Exact { + direct_catalog: direct_catalog.clone(), + queries: selected + .iter() + .map(|selected| { + ( + selected.trace.seed_digest, + selected.trace.query_digest, + selected.trace.semantic_digest(), + selected.projection, + ) + }) + .collect::>() + .into_boxed_slice(), + } + } + + fn matches_inputs( + family: &BoundOnTheFlyQueryFamilyV1, + direct_catalog: &PreparedDirectExecutorCatalog, + selected: &[QueryFamilyTraceInput<'_>], + ) -> bool { + let QueryFamilyCacheIdentityV1::Exact { + direct_catalog: cached_catalog, + queries, + } = &family.identity + else { + return false; + }; + *cached_catalog == *direct_catalog + && queries.len() == selected.len() + && queries.iter().zip(selected).all(|(cached, selected)| { + *cached + == ( + selected.trace.seed_digest, + selected.trace.query_digest, + selected.trace.semantic_digest(), + selected.projection, + ) + }) + } + + fn bind_groups( + &self, + family: &OnTheFlyBuiltQueryFamilyV1, + ) -> RusticolResult> { + family + .row_groups + .iter() + .map(|group| { + let mut all_members_are_packed_singleton_capable = true; + for &(key, expected_permutation) in group.member_bindings.iter() { + let resolved = self.resolver.resolve(key)?; + if resolved.direct_executor_id != group.direct_executor_id + || resolved.handle.role() != group.role + || resolved.parent_permutation != expected_permutation + { + return Err(integrity( + "query-family prepared executor binding changed after row materialization", + )); + } + all_members_are_packed_singleton_capable &= + resolved.packed_singleton_capable; + } + let mut resolved = self.resolver.resolve(group.representative_key)?; + if resolved.direct_executor_id != group.direct_executor_id + || resolved.handle.role() != group.role + { + return Err(integrity( + "query-family representative executor differs from its row group", + )); + } + resolved.packed_singleton_capable &= + all_members_are_packed_singleton_capable; + Ok(resolved) + }) + .collect::>>() + .map(Vec::into_boxed_slice) + } + + fn resolved_groups_are_packed_singleton_capable( + family: &OnTheFlyBuiltQueryFamilyV1, + resolved_groups: &[ResolvedOnTheFlyExecutor], + ) -> bool { + !family.row_groups.is_empty() + && family.row_groups.len() == resolved_groups.len() + && family + .row_groups + .iter() + .zip(resolved_groups) + .all(|(group, resolved)| { + resolved.packed_singleton_capable + && resolved.direct_executor_id == group.direct_executor_id + && resolved.handle.role() == group.role + }) + } + + fn direct_group_views<'a>( + family: &'a OnTheFlyBuiltQueryFamilyV1, + resolved_groups: &[ResolvedOnTheFlyExecutor], + ) -> RusticolResult<(Vec>, usize)> { + if family.row_groups.len() != resolved_groups.len() { + return Err(integrity( + "query-family direct-program binding has the wrong group count", + )); + } + let mut groups = Vec::with_capacity(family.row_groups.len()); + let mut contribution_row_count = 0_usize; + for (group, resolved) in family.row_groups.iter().zip(resolved_groups) { + groups.push(match &group.rows { + OnTheFlyFamilyRowsV1::Contribution(rows) => { + contribution_row_count = contribution_row_count + .checked_add(rows.len()) + .ok_or_else(|| invalid("query-family contribution count exceeds usize"))?; + DirectInteractionGroupView::contribution( + group.stage, + group.direct_executor_id, + Some(resolved.handle), + resolved.interaction_capability, + rows, + ) + } + OnTheFlyFamilyRowsV1::Closure(rows) => DirectInteractionGroupView::closure( + group.stage, + group.direct_executor_id, + resolved.handle, + rows, + ), + OnTheFlyFamilyRowsV1::Source(_) | OnTheFlyFamilyRowsV1::Finalization(_) => { + DirectInteractionGroupView::other( + group.stage, + group.role, + group.direct_executor_id, + Some(resolved.handle), + ) + } + }); + } + Ok((groups, contribution_row_count)) + } + + fn bind_interaction_program( + family: &OnTheFlyBuiltQueryFamilyV1, + resolved_groups: &[ResolvedOnTheFlyExecutor], + strategy: RecurrenceStrategy, + ) -> RusticolResult { + let (groups, contribution_row_count) = Self::direct_group_views(family, resolved_groups)?; + // OTF families do not persist authenticated liveness descriptors. + // Empty currents deliberately keep the terminal scalarization rewrite + // disabled while sharing the base color/ATV interaction program. + DirectInteractionProgram::build(DirectInteractionScheduleView::new( + strategy, + &groups, + &[], + contribution_row_count, + )) + } + + fn bind_singleton_fanout_program( + family: &OnTheFlyBuiltQueryFamilyV1, + resolved_groups: &[ResolvedOnTheFlyExecutor], + ) -> RusticolResult { + let (groups, _) = Self::direct_group_views(family, resolved_groups)?; + DirectSingletonContributionFanoutProgram::build( + &groups, + family.current_component_count, + checked_u32( + family.momentum_forms.len(), + "query-family momentum-form count", + )?, + family.parameter_count, + checked_u32( + family.exact_factors.len(), + "query-family exact-factor count", + )?, + ) + } + + fn invalidate_exposed_row_tables(&mut self) -> RusticolResult<()> { + if !self.families.iter().any(|family| family.descriptor_exposed) + && !self + .pending + .as_ref() + .is_some_and(|family| family.descriptor_exposed) + { + return Ok(()); + } + self.resolver.invalidate_row_tables()?; + for family in &mut self.families { + family.descriptor_exposed = false; + } + if let Some(pending) = &mut self.pending { + pending.descriptor_exposed = false; + } + Ok(()) + } + + /// Drop every retained selector family while keeping the loaded executor + /// pool and parameter state. Descriptor invalidation happens first so no + /// prepared kernel can retain a pointer into the rows being dropped. + pub(crate) fn clear_families(&mut self) -> RusticolResult<()> { + let next_generation = self + .family_generation + .checked_add(1) + .ok_or_else(|| invalid("query-family generation exceeds u64"))?; + self.invalidate_exposed_row_tables()?; + self.pending = None; + self.last_used = None; + self.families.clear(); + self.family_generation = next_generation; + Ok(()) + } + + /// Abandon a cold candidate without disturbing the last successfully + /// retained family. Any exposed candidate rows are invalidated before + /// their owner is dropped. + pub(crate) fn discard_pending_family(&mut self) -> RusticolResult<()> { + if self.pending.is_none() { + return Ok(()); + } + self.invalidate_exposed_row_tables()?; + self.pending = None; + Ok(()) + } + + fn commit_pending_family(&mut self) -> RusticolResult<()> { + let next_generation = self + .family_generation + .checked_add(1) + .ok_or_else(|| invalid("query-family generation exceeds u64"))?; + // A different family was invalidated before the candidate became + // pending. Be defensive if an exposed retained owner is nevertheless + // present: invalidate it before replacement drops its rows. + if self.families.iter().any(|family| family.descriptor_exposed) { + self.invalidate_exposed_row_tables()?; + } + let candidate = self + .pending + .take() + .ok_or_else(|| integrity("query-family commit has no pending candidate"))?; + self.families.clear(); + self.families.push(candidate); + self.last_used = Some(0); + self.family_generation = next_generation; + Ok(()) + } + + fn activate_retained_index( + &mut self, + index: usize, + logical_point_capacity: u32, + ) -> RusticolResult { + let replacement = self + .families + .get(index) + .ok_or_else(|| integrity("query-family handle index is absent"))? + .workspace + .logical_point_capacity + .lt(&logical_point_capacity) + .then(|| { + OnTheFlyFamilyWorkspaceV1::new( + &self.families[index].family, + logical_point_capacity, + self.families[index].packed_singleton_capable, + ) + }) + .transpose()?; + if self + .pending + .as_ref() + .is_some_and(|pending| pending.descriptor_exposed) + || replacement.is_some() && self.families[index].descriptor_exposed + || self.last_used != Some(index) + && self.families.iter().any(|family| family.descriptor_exposed) + { + self.invalidate_exposed_row_tables()?; + } + self.pending = None; + self.last_used = Some(index); + if let Some(replacement) = replacement { + let family = self + .families + .get_mut(index) + .expect("validated query-family handle disappeared"); + family.workspace = replacement; + family.applied_parameter_version = u64::MAX; + } + Ok(true) + } + + pub(crate) fn activate_retained_family( + &mut self, + handle: OnTheFlyQueryFamilyHandleV1, + logical_point_capacity: u32, + ) -> RusticolResult { + if handle.generation != self.family_generation { + return Err(integrity( + "query-family handle belongs to a replaced or cleared arena", + )); + } + self.activate_retained_index(handle.index, logical_point_capacity) + } + + pub(crate) fn resize_active_family( + &mut self, + logical_point_capacity: u32, + ) -> RusticolResult { + if let Some(pending) = self.pending.as_ref() { + if logical_point_capacity <= pending.workspace.logical_point_capacity { + return Ok(false); + } + let replacement = OnTheFlyFamilyWorkspaceV1::new( + &pending.family, + logical_point_capacity, + pending.packed_singleton_capable, + )?; + self.invalidate_exposed_row_tables()?; + let pending = self + .pending + .as_mut() + .expect("query-family pending state disappeared"); + pending.workspace = replacement; + pending.applied_parameter_version = u64::MAX; + return Ok(false); + } + let index = self + .last_used + .ok_or_else(|| invalid("query-family resize requires a cold prepare"))?; + self.activate_retained_index(index, logical_point_capacity) + } + + /// Cold family selection/binding. This must remain outside warmed timing. + pub(crate) fn prepare( + &mut self, + direct_catalog: &PreparedDirectExecutorCatalog, + selected: &[QueryFamilyTraceInput<'_>], + logical_point_capacity: u32, + ) -> RusticolResult { + // The only retained family is the complete cache. A selector evicted + // by a later successful cold execution is deliberately rebuilt. + let retained = self.last_used.filter(|&index| { + self.families + .get(index) + .is_some_and(|family| Self::matches_inputs(family, direct_catalog, selected)) + }); + if let Some(index) = retained { + return self.activate_retained_index(index, logical_point_capacity); + } + + let identity = Self::cache_identity(direct_catalog, selected); + if self + .pending + .as_ref() + .is_some_and(|pending| pending.identity == identity) + { + let replacement = self + .pending + .as_ref() + .filter(|pending| logical_point_capacity > pending.workspace.logical_point_capacity) + .map(|pending| { + OnTheFlyFamilyWorkspaceV1::new( + &pending.family, + logical_point_capacity, + pending.packed_singleton_capable, + ) + }) + .transpose()?; + if let Some(replacement) = replacement { + self.invalidate_exposed_row_tables()?; + let pending = self + .pending + .as_mut() + .expect("matching pending query family disappeared"); + pending.workspace = replacement; + pending.applied_parameter_version = u64::MAX; + } + return Ok(false); + } + + // A superseded or failed candidate must never accumulate alongside a + // new one. This leaves the last successful family and its handle valid. + self.discard_pending_family()?; + + let family = build_query_family_from_traces(direct_catalog, selected)?; + if family.census.source_frame_partition_count != 1 { + return Err(invalid( + "executable query families currently require one compact source frame", + )); + } + let resolved_groups = self.bind_groups(&family)?; + let packed_singleton_capable = + Self::resolved_groups_are_packed_singleton_capable(&family, &resolved_groups); + let workspace = OnTheFlyFamilyWorkspaceV1::new( + &family, + logical_point_capacity, + packed_singleton_capable, + )?; + let interaction_program = Self::bind_interaction_program( + &family, + &resolved_groups, + RecurrenceStrategy::TopologyReplay, + )?; + let singleton_fanout_program = + Self::bind_singleton_fanout_program(&family, &resolved_groups)?; + if self.families.capacity() == 0 { + self.families.try_reserve_exact(1).map_err(|error| { + invalid(format!( + "query-family retained-family allocation failed: {error}" + )) + })?; + } + let candidate = BoundOnTheFlyQueryFamilyV1 { + identity, + strategy: RecurrenceStrategy::TopologyReplay, + family, + workspace, + resolved_groups, + packed_singleton_capable, + interaction_program, + singleton_fanout_program, + applied_parameter_version: u64::MAX, + descriptor_exposed: false, + }; + // Keep every old pointer owner alive until both the replacement rows + // and workspace exist, then clear every prepared SymJIT row table + // before exposing the pending candidate. + self.invalidate_exposed_row_tables()?; + self.pending = Some(candidate); + Ok(false) + } + + /// Install one bounded-memory streamed candidate. Unlike the LC seam, + /// its cache identity is a compact ordered digest produced together with + /// the materialized family, so no H x S query identity table is retained. + pub(crate) fn prepare_streamed_candidate( + &mut self, + candidate: OnTheFlyStreamedQueryFamilyCandidateV1, + logical_point_capacity: u32, + ) -> RusticolResult { + let OnTheFlyStreamedQueryFamilyCandidateV1 { identity, family } = candidate; + if let Some(index) = self.last_used.filter(|&index| { + self.families + .get(index) + .is_some_and(|family| family.identity == identity) + }) { + return self.activate_retained_index(index, logical_point_capacity); + } + if self + .pending + .as_ref() + .is_some_and(|pending| pending.identity == identity) + { + let replacement = self + .pending + .as_ref() + .filter(|pending| logical_point_capacity > pending.workspace.logical_point_capacity) + .map(|pending| { + OnTheFlyFamilyWorkspaceV1::new( + &pending.family, + logical_point_capacity, + pending.packed_singleton_capable, + ) + }) + .transpose()?; + if let Some(replacement) = replacement { + self.invalidate_exposed_row_tables()?; + let pending = self + .pending + .as_mut() + .expect("matching streamed query family disappeared"); + pending.workspace = replacement; + pending.applied_parameter_version = u64::MAX; + } + return Ok(false); + } + + self.discard_pending_family()?; + if family.census.source_frame_partition_count != 1 { + return Err(invalid( + "executable query families currently require one compact source frame", + )); + } + let resolved_groups = self.bind_groups(&family)?; + let packed_singleton_capable = + Self::resolved_groups_are_packed_singleton_capable(&family, &resolved_groups); + let workspace = OnTheFlyFamilyWorkspaceV1::new( + &family, + logical_point_capacity, + packed_singleton_capable, + )?; + let interaction_program = Self::bind_interaction_program( + &family, + &resolved_groups, + RecurrenceStrategy::ContractedColorUnion, + )?; + let singleton_fanout_program = + Self::bind_singleton_fanout_program(&family, &resolved_groups)?; + if self.families.capacity() == 0 { + self.families.try_reserve_exact(1).map_err(|error| { + invalid(format!( + "query-family retained-family allocation failed: {error}" + )) + })?; + } + let candidate = BoundOnTheFlyQueryFamilyV1 { + identity, + strategy: RecurrenceStrategy::ContractedColorUnion, + family, + workspace, + resolved_groups, + packed_singleton_capable, + interaction_program, + singleton_fanout_program, + applied_parameter_version: u64::MAX, + descriptor_exposed: false, + }; + self.invalidate_exposed_row_tables()?; + self.pending = Some(candidate); + Ok(false) + } + + /// Allocation-free warmed execution after one cold [`Self::prepare`] call. + pub(crate) fn execute_into( + &mut self, + external_momenta: &[f64], + point_count: u32, + outputs: &mut [(f64, f64)], + ) -> RusticolResult { + let cache_hit = self.pending.is_none(); + let mut report = self.execute_into_impl( + external_momenta, + point_count, + outputs, + OnTheFlyQueryFamilyExecutionReportV1::default(), + |report, role, row_count| match role { + DirectExecutorRole::Source => { + add(&mut report.source_calls, 1, "executed source calls")?; + add(&mut report.source_rows, row_count, "executed source rows") + } + DirectExecutorRole::Contribution => { + add( + &mut report.contribution_calls, + 1, + "executed contribution calls", + )?; + add( + &mut report.contribution_rows, + row_count, + "executed contribution rows", + ) + } + DirectExecutorRole::Finalization => { + add( + &mut report.finalization_calls, + 1, + "executed finalization calls", + )?; + add( + &mut report.finalization_rows, + row_count, + "executed finalization rows", + ) + } + DirectExecutorRole::Closure => { + add(&mut report.closure_calls, 1, "executed closure calls")?; + add(&mut report.closure_rows, row_count, "executed closure rows") + } + }, + )?; + report.cache_hit = cache_hit; + Ok(report) + } + + /// Allocation-free warmed execution without diagnostic report counters. + pub(crate) fn execute_into_unprofiled( + &mut self, + external_momenta: &[f64], + point_count: u32, + outputs: &mut [(f64, f64)], + ) -> RusticolResult<()> { + self.execute_into_impl(external_momenta, point_count, outputs, (), |_, _, _| Ok(())) + } + + fn execute_into_impl( + &mut self, + external_momenta: &[f64], + point_count: u32, + outputs: &mut [(f64, f64)], + report: Report, + record_group: RecordGroup, + ) -> RusticolResult + where + RecordGroup: FnMut(&mut Report, DirectExecutorRole, u32) -> RusticolResult<()>, + { + let pending = self.pending.is_some(); + let result = (|| { + let family = if pending { + self.pending + .as_mut() + .expect("pending query family disappeared before execution") + } else { + self.last_used + .and_then(|index| self.families.get_mut(index)) + .ok_or_else(|| invalid("query-family execute requires a cold prepare"))? + }; + OnTheFlyFamilyWorkspaceV1::validate_output_shape( + family.family.amplitude_destination_count, + point_count, + outputs.len(), + )?; + if family.applied_parameter_version != self.parameter_version { + family.workspace.refresh_parameters(&self.parameter_state)?; + family.applied_parameter_version = self.parameter_version; + } + family + .workspace + .refresh_inputs(&family.family, external_momenta, point_count)?; + family.descriptor_exposed = true; + let report = execute_bound_family(family, point_count, report, record_group)?; + family.workspace.write_outputs( + family.family.amplitude_destination_count, + point_count, + outputs, + )?; + Ok(report) + })(); + + match result { + Ok(report) => { + if pending { + self.commit_pending_family()?; + } + Ok(report) + } + Err(error) => { + if pending { + self.discard_pending_family()?; + } + Err(error) + } + } + } + + #[cfg(any(test, feature = "on-the-fly-test-support"))] + pub(crate) fn observed_currents( + &self, + point_index: u32, + ) -> RusticolResult> { + let family = self + .pending + .as_ref() + .or_else(|| self.last_used.and_then(|index| self.families.get(index))) + .ok_or_else(|| invalid("query-family observation requires a prepared execution"))?; + family + .workspace + .observed_currents(&family.family.observed_currents, point_index) + } +} + +impl Drop for OnTheFlyQueryFamilyExecutorV1 { + fn drop(&mut self) { + if self.invalidate_exposed_row_tables().is_ok() { + return; + } + // Fail closed if a prepared context is unexpectedly borrowed during + // teardown: leaked private storage is safer than dangling row tables. + for family in self.families.drain(..) { + if family.descriptor_exposed { + std::mem::forget(family); + } + } + if self + .pending + .as_ref() + .is_some_and(|family| family.descriptor_exposed) + && let Some(pending) = self.pending.take() + { + std::mem::forget(pending); + } + } +} + +fn execute_bound_family( + cached: &mut BoundOnTheFlyQueryFamilyV1, + point_count: u32, + mut report: Report, + mut record_group: RecordGroup, +) -> RusticolResult +where + RecordGroup: FnMut(&mut Report, DirectExecutorRole, u32) -> RusticolResult<()>, +{ + clear_direct_executor_error_detail(); + let (arena, momenta, parameters, factors) = cached.workspace.raw_views()?; + for (group_index, (group, resolved)) in cached + .family + .row_groups + .iter() + .zip(cached.resolved_groups.iter().copied()) + .enumerate() + { + let row_count = checked_u32(group.rows.len(), "query-family row-group count")?; + record_group(&mut report, group.role, row_count)?; + // The native bundle intentionally specializes point zero. Preserve + // the ordinary authenticated row path for every batched execution. + if point_count == 1 { + match cached.interaction_program.action(group_index) { + DirectInteractionGroupAction::Normal => { + if let OnTheFlyFamilyRowsV1::Contribution(rows) = &group.rows + && cached.singleton_fanout_program.execute_group( + group_index, + rows, + arena, + momenta, + parameters, + factors, + point_count, + )? + { + continue; + } + } + DirectInteractionGroupAction::Consumed => continue, + DirectInteractionGroupAction::Execute(stage_index) => { + cached.interaction_program.execute::( + stage_index, + arena, + momenta, + parameters, + factors, + )?; + continue; + } + } + } + let status: c_int = unsafe { + match (&group.rows, resolved.handle) { + ( + OnTheFlyFamilyRowsV1::Source(rows), + DirectExecutorHandle::Source { call, context }, + ) => call( + context, + arena, + momenta, + parameters, + factors, + rows.as_ptr(), + row_count, + point_count, + ), + ( + OnTheFlyFamilyRowsV1::Contribution(rows), + DirectExecutorHandle::Contribution { call, context }, + ) => call( + context, + arena, + momenta, + parameters, + factors, + rows.as_ptr(), + row_count, + point_count, + ), + ( + OnTheFlyFamilyRowsV1::Finalization(rows), + DirectExecutorHandle::Finalization { call, context }, + ) => call( + context, + arena, + momenta, + parameters, + factors, + rows.as_ptr(), + row_count, + point_count, + ), + ( + OnTheFlyFamilyRowsV1::Closure(rows), + DirectExecutorHandle::Closure { call, context }, + ) => call( + context, + arena, + momenta, + parameters, + factors, + rows.as_ptr(), + row_count, + point_count, + ), + _ => { + return Err(integrity( + "query-family resolved handle differs from its row storage", + )); + } + } + }; + crate::recurrence::direct_backend::check_status( + group.role, + group.direct_executor_id, + status, + )?; + } + cached.workspace.active_point_count = point_count; + Ok(report) +} + +/// Build exactly the requested LC queries with the existing query-local +/// machinery, then compute their cold structural union census. +#[doc(hidden)] +#[cfg(feature = "on-the-fly-test-support")] +pub fn on_the_fly_query_family_census_v1( + authenticated: &crate::recurrence::AuthenticatedRecurrenceBuilderInput, + direct_catalog: &PreparedDirectExecutorCatalog, + queries: &[(u32, Vec)], + enable_projection: bool, +) -> RusticolResult { + let selected = queries + .iter() + .map(|(flow, helicities)| { + super::test_support::build_on_the_fly_selected_trace_v1( + authenticated, + direct_catalog, + *flow, + helicities, + enable_projection, + ) + }) + .collect::>>()?; + for selected_query in &selected { + if selected_query.seed.semantic_digest() != selected_query.trace.seed_digest + || selected_query.query.semantic_digest() != selected_query.trace.query_digest + { + return Err(integrity( + "selected query-family trace is detached from its compact inputs", + )); + } + } + let traces = selected + .iter() + .map(|selected_query| QueryFamilyTraceInput { + trace: &selected_query.trace, + projection: selected_query.projection, + }) + .collect::>(); + Ok(build_query_family_from_traces(direct_catalog, &traces)?.census) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::recurrence::direct_backend::{ + DIRECT_STATUS_OK, DirectContributionExecutionMetadata, DirectContributionFanoutBatch, + DirectContributionFanoutExecutorHandle, DirectInteractionBundleBatch, + DirectInteractionExecutorContexts, DirectInteractionIntrinsicKind, + }; + use crate::recurrence::direct_plan::DIRECT_CONTRIBUTION_FLAG_CERTIFIED_REUSE; + use crate::recurrence::{ + ExactRational, PreparedDirectExecutorBinding, PreparedDirectExecutorCatalog, + }; + use std::cell::{Cell, RefCell}; + use std::ffi::c_void; + use std::sync::atomic::{AtomicU64, Ordering}; + + const TEST_STATUS_BOUNDS: c_int = 2; + const TEST_STATUS_FAILED: c_int = 4; + + #[derive(Default)] + struct ProbeState { + calls: RefCell>, + fail_role: Cell>, + record_failure_detail: Cell, + invalidations: Cell, + } + + #[derive(Default)] + struct InteractionPathCensus { + native_calls: AtomicU64, + raw_calls: AtomicU64, + raw_rows: AtomicU64, + } + + #[derive(Default)] + struct FanoutPathCensus { + native_calls: AtomicU64, + native_runs: AtomicU64, + raw_calls: AtomicU64, + raw_rows: AtomicU64, + } + + unsafe fn execute_fanout_test_rows( + arena: DirectArenaView, + factors: DirectFactorView, + rows: &[DirectContributionRow], + point_count: u32, + ) -> c_int { + let stride = arena.point_stride as usize; + for row in rows { + if row.exact_factor_id >= factors.value_count { + return TEST_STATUS_BOUNDS; + } + let factor = unsafe { *factors.values_re.add(row.exact_factor_id as usize) }; + for point in 0..point_count as usize { + let parent0 = row.parent0_component_base as usize * stride + point; + let parent1 = row.parent1_component_base_or_sentinel as usize * stride + point; + let destination = row.destination_component_base as usize * stride + point; + if parent0 >= arena.current_scalar_len as usize + || parent1 >= arena.current_scalar_len as usize + || destination >= arena.current_scalar_len as usize + { + return TEST_STATUS_BOUNDS; + } + let value = unsafe { + (10.0 * *arena.current_re.add(parent0) + *arena.current_re.add(parent1)) + * factor + }; + unsafe { + if row.flags & DIRECT_CONTRIBUTION_FLAG_INITIALIZE_DESTINATION != 0 { + *arena.current_re.add(destination) = value; + *arena.current_im.add(destination) = 0.0; + } else { + *arena.current_re.add(destination) += value; + } + } + } + } + DIRECT_STATUS_OK + } + + unsafe extern "C" fn fanout_test_raw( + context: *const c_void, + arena: DirectArenaView, + _momenta: DirectMomentumView, + _parameters: DirectParameterView, + factors: DirectFactorView, + rows: *const DirectContributionRow, + row_count: u32, + point_count: u32, + ) -> c_int { + let census = unsafe { &*context.cast::() }; + census.raw_calls.fetch_add(1, Ordering::Relaxed); + census + .raw_rows + .fetch_add(u64::from(row_count), Ordering::Relaxed); + let rows = unsafe { std::slice::from_raw_parts(rows, row_count as usize) }; + unsafe { execute_fanout_test_rows(arena, factors, rows, point_count) } + } + + unsafe fn fanout_test_native( + context: *const c_void, + arena: DirectArenaView, + _momenta: DirectMomentumView, + _parameters: DirectParameterView, + factors: DirectFactorView, + batch: DirectContributionFanoutBatch<'_>, + ) -> RusticolResult<()> { + let census = unsafe { &*context.cast::() }; + census.native_calls.fetch_add(1, Ordering::Relaxed); + census + .native_runs + .fetch_add(batch.runs.len() as u64, Ordering::Relaxed); + for run in batch.runs { + let start = run.row_start as usize; + let end = start + run.row_count as usize; + let rows = batch + .rows + .get(start..end) + .ok_or_else(|| integrity("test native fanout run is outside its row group"))?; + let representative = *rows + .first() + .ok_or_else(|| integrity("test native fanout run is empty"))?; + let stride = arena.point_stride as usize; + let parent0 = representative.parent0_component_base as usize * stride; + let parent1 = representative.parent1_component_base_or_sentinel as usize * stride; + let value = + unsafe { 10.0 * *arena.current_re.add(parent0) + *arena.current_re.add(parent1) }; + let bundle_start = run.bundle_start as usize; + let bundle_end = bundle_start + run.bundle_count as usize; + for bundle in batch + .bundles + .get(bundle_start..bundle_end) + .ok_or_else(|| integrity("test native fanout bundle range is out of bounds"))? + { + let factor = unsafe { + *factors + .values_re + .add(bundle.effective_factor_id_or_sentinel as usize) + }; + let start = bundle.row_start as usize; + let end = start + bundle.row_count as usize; + for row in batch + .rows + .get(start..end) + .ok_or_else(|| integrity("test native fanout rows are out of bounds"))? + { + let destination = row.destination_component_base as usize * stride; + unsafe { + if row.flags & DIRECT_CONTRIBUTION_FLAG_INITIALIZE_DESTINATION != 0 { + *arena.current_re.add(destination) = value * factor; + *arena.current_im.add(destination) = 0.0; + } else { + *arena.current_re.add(destination) += value * factor; + } + } + } + } + } + Ok(()) + } + + unsafe extern "C" fn interaction_raw_contributions( + context: *const c_void, + _arena: DirectArenaView, + _momenta: DirectMomentumView, + _parameters: DirectParameterView, + _factors: DirectFactorView, + _rows: *const DirectContributionRow, + row_count: u32, + _point_count: u32, + ) -> c_int { + let census = unsafe { &*context.cast::() }; + census.raw_calls.fetch_add(1, Ordering::Relaxed); + census + .raw_rows + .fetch_add(u64::from(row_count), Ordering::Relaxed); + DIRECT_STATUS_OK + } + + unsafe fn interaction_fanout_unreachable( + _context: *const c_void, + _arena: DirectArenaView, + _momenta: DirectMomentumView, + _parameters: DirectParameterView, + _factors: DirectFactorView, + _batch: DirectContributionFanoutBatch<'_>, + ) -> RusticolResult<()> { + panic!("OTF interaction dispatch unexpectedly used generic contribution fanout") + } + + unsafe fn count_otf_interaction( + contexts: DirectInteractionExecutorContexts, + _arena: DirectArenaView, + _momenta: DirectMomentumView, + _parameters: DirectParameterView, + _factors: DirectFactorView, + batch: DirectInteractionBundleBatch<'_>, + ) -> RusticolResult<()> { + assert_eq!(contexts.color, contexts.antisymmetric_tensor_vector); + assert_eq!(batch.anchors.len(), 1); + assert_eq!(batch.outputs.len(), 2); + assert!(batch.atv_before_color); + let census = unsafe { &*contexts.color.cast::() }; + census.native_calls.fetch_add(1, Ordering::Relaxed); + Ok(()) + } + + impl ProbeState { + fn record(&self, role: DirectExecutorRole, rows: *const c_void, row_count: u32) -> bool { + self.calls + .borrow_mut() + .push((role, rows as usize, row_count)); + let failed = self.fail_role.get() == Some(role); + if failed && self.record_failure_detail.get() { + crate::recurrence::direct_backend::record_direct_executor_error_detail( + crate::RusticolError::integrity(format!("probe {role:?} first failure detail")), + ); + crate::recurrence::direct_backend::record_direct_executor_error_detail( + crate::RusticolError::internal( + "later probe detail must not replace the first failure", + ), + ); + } + failed + } + } + + unsafe fn probe_state<'a>(context: *const c_void) -> &'a ProbeState { + unsafe { &*context.cast::() } + } + + unsafe extern "C" fn probe_sources( + context: *const c_void, + arena: DirectArenaView, + momenta: DirectMomentumView, + _parameters: DirectParameterView, + factors: DirectFactorView, + rows: *const DirectSourceRow, + row_count: u32, + point_count: u32, + ) -> c_int { + let state = unsafe { probe_state(context) }; + if state.record(DirectExecutorRole::Source, rows.cast(), row_count) { + return TEST_STATUS_FAILED; + } + let rows = unsafe { std::slice::from_raw_parts(rows, row_count as usize) }; + for row in rows { + if row.exact_factor_id >= factors.value_count { + return TEST_STATUS_BOUNDS; + } + let factor = unsafe { *factors.values_re.add(row.exact_factor_id as usize) }; + for point in 0..point_count as usize { + let source = row.momentum_form_id as usize + * momenta.lorentz_component_count as usize + * momenta.point_stride as usize + + point; + let destination = + row.destination_component_base as usize * arena.point_stride as usize + point; + if source >= momenta.scalar_len as usize + || destination >= arena.current_scalar_len as usize + { + return TEST_STATUS_BOUNDS; + } + unsafe { + *arena.current_re.add(destination) = *momenta.values.add(source) * factor; + *arena.current_im.add(destination) = 0.0; + } + } + } + crate::recurrence::direct_backend::DIRECT_STATUS_OK + } + + unsafe extern "C" fn probe_contributions( + context: *const c_void, + arena: DirectArenaView, + _momenta: DirectMomentumView, + _parameters: DirectParameterView, + factors: DirectFactorView, + rows: *const DirectContributionRow, + row_count: u32, + point_count: u32, + ) -> c_int { + let state = unsafe { probe_state(context) }; + if state.record(DirectExecutorRole::Contribution, rows.cast(), row_count) { + return TEST_STATUS_FAILED; + } + let rows = unsafe { std::slice::from_raw_parts(rows, row_count as usize) }; + for row in rows { + if row.exact_factor_id >= factors.value_count { + return TEST_STATUS_BOUNDS; + } + let factor = unsafe { *factors.values_re.add(row.exact_factor_id as usize) }; + for point in 0..point_count as usize { + let parent0 = + row.parent0_component_base as usize * arena.point_stride as usize + point; + let parent1 = row.parent1_component_base_or_sentinel as usize + * arena.point_stride as usize + + point; + let destination = + row.destination_component_base as usize * arena.point_stride as usize + point; + if parent0 >= arena.current_scalar_len as usize + || parent1 >= arena.current_scalar_len as usize + || destination >= arena.current_scalar_len as usize + { + return TEST_STATUS_BOUNDS; + } + let contribution = unsafe { + (10.0 * *arena.current_re.add(parent0) + *arena.current_re.add(parent1)) + * factor + }; + unsafe { + if row.flags & DIRECT_CONTRIBUTION_FLAG_INITIALIZE_DESTINATION != 0 { + *arena.current_re.add(destination) = contribution; + *arena.current_im.add(destination) = 0.0; + } else { + *arena.current_re.add(destination) += contribution; + } + } + } + } + crate::recurrence::direct_backend::DIRECT_STATUS_OK + } + + unsafe extern "C" fn probe_finalizations( + context: *const c_void, + arena: DirectArenaView, + _momenta: DirectMomentumView, + parameters: DirectParameterView, + factors: DirectFactorView, + rows: *const DirectFinalizationRow, + row_count: u32, + point_count: u32, + ) -> c_int { + let state = unsafe { probe_state(context) }; + if state.record(DirectExecutorRole::Finalization, rows.cast(), row_count) { + return TEST_STATUS_FAILED; + } + let rows = unsafe { std::slice::from_raw_parts(rows, row_count as usize) }; + let parameter = if parameters.value_count == 0 { + 1.0 + } else { + unsafe { *parameters.values_re } + }; + for row in rows { + if row.exact_factor_id >= factors.value_count { + return TEST_STATUS_BOUNDS; + } + let factor = unsafe { *factors.values_re.add(row.exact_factor_id as usize) }; + for component in 0..usize::from(row.component_count) { + for point in 0..point_count as usize { + let destination = (row.component_base as usize + component) + * arena.point_stride as usize + + point; + if destination >= arena.current_scalar_len as usize { + return TEST_STATUS_BOUNDS; + } + unsafe { + *arena.current_re.add(destination) *= factor * parameter; + *arena.current_im.add(destination) *= factor * parameter; + } + } + } + } + crate::recurrence::direct_backend::DIRECT_STATUS_OK + } + + unsafe extern "C" fn probe_closures( + context: *const c_void, + arena: DirectArenaView, + _momenta: DirectMomentumView, + _parameters: DirectParameterView, + factors: DirectFactorView, + rows: *const DirectClosureRow, + row_count: u32, + point_count: u32, + ) -> c_int { + let state = unsafe { probe_state(context) }; + if state.record(DirectExecutorRole::Closure, rows.cast(), row_count) { + return TEST_STATUS_FAILED; + } + let rows = unsafe { std::slice::from_raw_parts(rows, row_count as usize) }; + for row in rows { + let factor_end = match row + .component_factor_start + .checked_add(u32::from(row.component_count)) + { + Some(value) => value, + None => return TEST_STATUS_BOUNDS, + }; + if row.exact_factor_id >= factors.value_count || factor_end > factors.value_count { + return TEST_STATUS_BOUNDS; + } + let common = unsafe { *factors.values_re.add(row.exact_factor_id as usize) }; + for component in 0..usize::from(row.component_count) { + let component_factor = unsafe { + *factors + .values_re + .add(row.component_factor_start as usize + component) + }; + for point in 0..point_count as usize { + let parent0 = (row.parent0_component_base as usize + component) + * arena.point_stride as usize + + point; + let parent1 = (row.parent1_component_base_or_sentinel as usize + component) + * arena.point_stride as usize + + point; + let destination = + row.amplitude_destination_id as usize * arena.point_stride as usize + point; + if parent0 >= arena.current_scalar_len as usize + || parent1 >= arena.current_scalar_len as usize + || destination >= arena.amplitude_scalar_len as usize + { + return TEST_STATUS_BOUNDS; + } + unsafe { + *arena.amplitude_re.add(destination) += *arena.current_re.add(parent0) + * *arena.current_re.add(parent1) + * common + * component_factor; + } + } + } + } + crate::recurrence::direct_backend::DIRECT_STATUS_OK + } + + struct ProbeResolver { + catalog: PreparedDirectExecutorCatalog, + state: Box, + packed_singleton_capabilities: [bool; 4], + } + + impl ProbeResolver { + fn new(catalog: PreparedDirectExecutorCatalog) -> Self { + Self { + catalog, + state: Box::new(ProbeState::default()), + packed_singleton_capabilities: [false; 4], + } + } + + fn packed_singleton(catalog: PreparedDirectExecutorCatalog) -> Self { + Self { + catalog, + state: Box::new(ProbeState::default()), + packed_singleton_capabilities: [true; 4], + } + } + + fn role_index(role: DirectExecutorRole) -> usize { + match role { + DirectExecutorRole::Source => 0, + DirectExecutorRole::Contribution => 1, + DirectExecutorRole::Finalization => 2, + DirectExecutorRole::Closure => 3, + } + } + + fn disable_packed_singleton_role(&mut self, role: DirectExecutorRole) { + self.packed_singleton_capabilities[Self::role_index(role)] = false; + } + } + + impl OnTheFlyPreparedExecutorResolver for ProbeResolver { + fn resolve(&self, key: OnTheFlyExecutorKeyV1) -> RusticolResult { + let (direct_executor_id, parent_permutation) = prepared_binding(&self.catalog, key)?; + let context = (&*self.state as *const ProbeState).cast::(); + let handle = match key.role() { + DirectExecutorRole::Source => DirectExecutorHandle::Source { + call: probe_sources, + context, + }, + DirectExecutorRole::Contribution => DirectExecutorHandle::Contribution { + call: probe_contributions, + context, + }, + DirectExecutorRole::Finalization => DirectExecutorHandle::Finalization { + call: probe_finalizations, + context, + }, + DirectExecutorRole::Closure => DirectExecutorHandle::Closure { + call: probe_closures, + context, + }, + }; + Ok(ResolvedOnTheFlyExecutor { + direct_executor_id, + handle, + parent_permutation, + packed_singleton_capable: self.packed_singleton_capabilities + [Self::role_index(key.role())], + interaction_capability: None, + }) + } + + fn invalidate_row_tables(&self) -> RusticolResult<()> { + self.state + .invalidations + .set(self.state.invalidations.get() + 1); + Ok(()) + } + } + + fn factor(numerator: i128) -> ExactComplexRational { + ExactComplexRational::new( + ExactRational::new(numerator, 1).unwrap(), + ExactRational::ZERO, + ) + } + + fn direct_catalog() -> PreparedDirectExecutorCatalog { + let digest = SemanticDigest::new([0x71; 32]).unwrap(); + PreparedDirectExecutorCatalog::new( + digest, + vec![ + PreparedDirectExecutorBinding::evaluator(DirectExecutorRole::Source, 0, 0), + PreparedDirectExecutorBinding::evaluator(DirectExecutorRole::Source, 1, 0), + PreparedDirectExecutorBinding::evaluator(DirectExecutorRole::Contribution, 2, 1), + PreparedDirectExecutorBinding::evaluator(DirectExecutorRole::Finalization, 3, 2), + PreparedDirectExecutorBinding::evaluator(DirectExecutorRole::Closure, 4, 3), + PreparedDirectExecutorBinding::evaluator(DirectExecutorRole::Contribution, 5, 4), + PreparedDirectExecutorBinding::evaluator_with_parent_permutation( + DirectExecutorRole::Contribution, + 6, + 1, + [1, 0], + ), + ], + ) + .unwrap() + } + + #[test] + fn family_contributions_use_direct_fanout_order_before_first_write_flags() { + let seed_digest = SemanticDigest::new([0x31; 32]).unwrap(); + let catalog_digest = SemanticDigest::new([0x32; 32]).unwrap(); + let operation_digest = SemanticDigest::new([0x33; 32]).unwrap(); + let binding_digest = SemanticDigest::new([0x34; 32]).unwrap(); + let executor_key = OnTheFlyExecutorKeyV1::PreparedOperation { + direct_catalog_digest: catalog_digest, + role: DirectExecutorRole::Contribution, + operation_kind: OnTheFlyOperationKindV1::Transition, + operation_id: 7, + operation_semantic_digest: operation_digest, + evaluator_binding_id: 9, + evaluator_binding_semantic_digest: binding_digest, + }; + let row = |parents: [u32; 2], destination: u32, factor: u32| DirectContributionRow { + parent0_component_base: parents[0], + parent1_component_base_or_sentinel: parents[1], + parent0_momentum_form_id: parents[0] / 4, + parent1_momentum_form_id_or_sentinel: parents[1] / 4, + destination_component_base: destination, + exact_factor_id: factor, + selector_domain_id: 0, + flags: 0, + }; + // A and C are reusable input classes; B is deliberately interleaved + // in the cold semantic input to catch destination-major ordering. + let specs = [ + ([0, 4], 20, 1), + ([8, 12], 24, 2), + ([0, 4], 28, 1), + ([16, 20], 20, 4), + ([16, 20], 24, 0), + ]; + let drafts = specs + .iter() + .map(|&(parents, destination, factor)| FamilyContributionDraft { + seed_digest, + stage: 1, + destination_current_id: destination, + direct_executor_id: 2, + key: executor_key, + parent_permutation: [0, 1], + row: row(parents, destination, factor), + }) + .collect::>(); + let expected_multiset = drafts + .iter() + .map(|draft| { + let row = draft.row; + ( + row.parent0_component_base, + row.parent1_component_base_or_sentinel, + row.parent0_momentum_form_id, + row.parent1_momentum_form_id_or_sentinel, + row.destination_component_base, + row.exact_factor_id, + row.selector_domain_id, + ) + }) + .collect::>(); + let mut contribution_groups = BTreeMap::new(); + contribution_groups.insert(1, BTreeMap::from([((seed_digest, 2), drafts)])); + let groups = ordered_family_row_groups( + BTreeMap::new(), + contribution_groups, + BTreeMap::new(), + BTreeMap::new(), + ) + .unwrap(); + let [group] = groups.as_slice() else { + panic!("expected one contribution row group"); + }; + let OnTheFlyFamilyRowsV1::Contribution(rows) = &group.rows else { + panic!("expected contribution rows"); + }; + let actual_multiset = rows + .iter() + .map(|row| { + ( + row.parent0_component_base, + row.parent1_component_base_or_sentinel, + row.parent0_momentum_form_id, + row.parent1_momentum_form_id_or_sentinel, + row.destination_component_base, + row.exact_factor_id, + row.selector_domain_id, + ) + }) + .collect::>(); + assert_eq!(actual_multiset, expected_multiset); + assert_eq!( + rows.iter() + .map(|row| ( + row.parent0_component_base, + row.destination_component_base, + row.exact_factor_id, + )) + .collect::>(), + vec![(0, 20, 1), (0, 28, 1), (16, 24, 0), (16, 20, 4), (8, 24, 2)], + ); + + let input_key = |row: &DirectContributionRow| { + ( + row.selector_domain_id, + row.parent0_component_base, + row.parent1_component_base_or_sentinel, + row.parent0_momentum_form_id, + row.parent1_momentum_form_id_or_sentinel, + ) + }; + let mut positions = BTreeMap::<_, Vec>::new(); + for (index, row) in rows.iter().enumerate() { + positions.entry(input_key(row)).or_default().push(index); + } + for indices in positions.values() { + assert_eq!(indices.last().unwrap() - indices[0] + 1, indices.len()); + } + let repeated_end = positions + .values() + .filter(|indices| indices.len() > 1) + .flat_map(|indices| indices.iter().copied()) + .max() + .unwrap(); + let singleton_start = positions + .values() + .filter(|indices| indices.len() == 1) + .map(|indices| indices[0]) + .min() + .unwrap(); + assert!(repeated_end < singleton_start); + + let mut initialize_counts = BTreeMap::::new(); + let mut seen_destinations = BTreeSet::new(); + for row in rows { + let initializes = row.flags & DIRECT_CONTRIBUTION_FLAG_INITIALIZE_DESTINATION != 0; + assert_eq!( + initializes, + seen_destinations.insert(row.destination_component_base) + ); + if initializes { + *initialize_counts + .entry(row.destination_component_base) + .or_default() += 1; + } + } + assert_eq!( + initialize_counts, + BTreeMap::from([(20, 1), (24, 1), (28, 1)]) + ); + } + + #[test] + fn otf_singleton_fanout_preserves_values_and_batched_raw_fallback() { + let catalog = direct_catalog(); + let (trace, projection) = + OnTheFlyStructuralTraceV1::test_query_family_trace(0x81, factor(1)); + let mut family = build_query_family_from_traces( + &catalog, + &[QueryFamilyTraceInput { + trace: &trace, + projection, + }], + ) + .unwrap(); + let seed_digest = trace.seed_digest; + let key = OnTheFlyExecutorKeyV1::PreparedOperation { + direct_catalog_digest: catalog.direct_template_catalog_digest(), + role: DirectExecutorRole::Contribution, + operation_kind: OnTheFlyOperationKindV1::Transition, + operation_id: 0, + operation_semantic_digest: SemanticDigest::new([0x82; 32]).unwrap(), + evaluator_binding_id: 1, + evaluator_binding_semantic_digest: SemanticDigest::new([0x83; 32]).unwrap(), + }; + let row = |parents: [u32; 2], destination_component_base, exact_factor_id| { + DirectContributionRow { + parent0_component_base: parents[0], + parent1_component_base_or_sentinel: parents[1], + parent0_momentum_form_id: 0, + parent1_momentum_form_id_or_sentinel: 0, + destination_component_base, + exact_factor_id, + selector_domain_id: 0, + flags: DIRECT_CONTRIBUTION_FLAG_INITIALIZE_DESTINATION, + } + }; + family.source_count = 1; + family.lorentz_component_count = 1; + family.parameter_count = 0; + family.current_component_count = 5; + family.amplitude_destination_count = 1; + family.momentum_forms = vec![ + CanonicalMomentumLinearForm::new(vec![MomentumTerm { + source_slot: 0, + coefficient: 1, + }]) + .unwrap(), + ] + .into_boxed_slice(); + family.exact_factors = vec![factor(1), factor(2)].into_boxed_slice(); + family.row_groups = vec![OnTheFlyFamilyRowGroupV1 { + seed_digest, + stage: 1, + role: DirectExecutorRole::Contribution, + direct_executor_id: 2, + representative_key: key, + member_bindings: vec![(key, [0, 1])].into_boxed_slice(), + rows: OnTheFlyFamilyRowsV1::Contribution( + vec![row([0, 1], 2, 0), row([0, 1], 3, 1), row([1, 0], 4, 0)].into_boxed_slice(), + ), + }] + .into_boxed_slice(); + #[cfg(any(test, feature = "on-the-fly-test-support"))] + { + family.observed_currents = Box::new([]); + } + + let census = FanoutPathCensus::default(); + let context = (&census as *const FanoutPathCensus).cast::(); + let raw_handle = DirectExecutorHandle::Contribution { + call: fanout_test_raw, + context, + }; + let capability = DirectInteractionExecutorCapability::new( + raw_handle, + DirectContributionExecutionMetadata::new(1, false).unwrap(), + DirectContributionFanoutExecutorHandle { + call: fanout_test_native, + context, + destination_component_count: 1, + parent_component_counts: [1, 1], + requires_two_momenta: false, + required_parameter_count: 0, + interaction_kind: DirectInteractionIntrinsicKind::VectorWedgeVector, + interaction_call: None, + }, + ) + .unwrap(); + let raw_only_groups = vec![ResolvedOnTheFlyExecutor { + direct_executor_id: 2, + handle: raw_handle, + parent_permutation: [0, 1], + packed_singleton_capable: false, + interaction_capability: None, + }] + .into_boxed_slice(); + let raw_only_program = + OnTheFlyQueryFamilyExecutorV1::::bind_singleton_fanout_program( + &family, + &raw_only_groups, + ) + .unwrap(); + assert_eq!(raw_only_program.row_counts(), (0, 0)); + let resolved_groups = vec![ResolvedOnTheFlyExecutor { + direct_executor_id: 2, + handle: raw_handle, + parent_permutation: [0, 1], + packed_singleton_capable: false, + interaction_capability: Some(capability), + }] + .into_boxed_slice(); + let OnTheFlyFamilyRowsV1::Contribution(rows) = &mut family.row_groups[0].rows else { + panic!("expected contribution rows"); + }; + rows[0].flags |= DIRECT_CONTRIBUTION_FLAG_CERTIFIED_REUSE; + let reuse_program = + OnTheFlyQueryFamilyExecutorV1::::bind_singleton_fanout_program( + &family, + &resolved_groups, + ) + .unwrap(); + assert_eq!(reuse_program.row_counts(), (0, 0)); + let OnTheFlyFamilyRowsV1::Contribution(rows) = &mut family.row_groups[0].rows else { + unreachable!(); + }; + rows[0].flags &= !DIRECT_CONTRIBUTION_FLAG_CERTIFIED_REUSE; + let workspace = OnTheFlyFamilyWorkspaceV1::new(&family, 2, false).unwrap(); + let interaction_program = + OnTheFlyQueryFamilyExecutorV1::::bind_interaction_program( + &family, + &resolved_groups, + RecurrenceStrategy::ContractedColorUnion, + ) + .unwrap(); + assert_eq!( + interaction_program.action(0), + DirectInteractionGroupAction::Normal + ); + let singleton_fanout_program = + OnTheFlyQueryFamilyExecutorV1::::bind_singleton_fanout_program( + &family, + &resolved_groups, + ) + .unwrap(); + assert_eq!(singleton_fanout_program.row_counts(), (3, 2)); + let mut bound = BoundOnTheFlyQueryFamilyV1 { + identity: QueryFamilyCacheIdentityV1::Streamed { + direct_catalog_digest: catalog.direct_template_catalog_digest(), + query_count: 1, + ordered_query_digest: SemanticDigest::new([0x84; 32]).unwrap(), + }, + strategy: RecurrenceStrategy::ContractedColorUnion, + family, + workspace, + resolved_groups, + packed_singleton_capable: false, + interaction_program, + singleton_fanout_program, + applied_parameter_version: 0, + descriptor_exposed: false, + }; + let record = |report: &mut OnTheFlyQueryFamilyExecutionReportV1, role, row_count| { + assert_eq!(role, DirectExecutorRole::Contribution); + add(&mut report.contribution_calls, 1, "test contribution calls")?; + add( + &mut report.contribution_rows, + row_count, + "test contribution rows", + ) + }; + let stride = bound.workspace.point_stride as usize; + + bound.workspace.current_re.as_mut_slice()[0] = 3.0; + bound.workspace.current_re.as_mut_slice()[stride] = 4.0; + let singleton = execute_bound_family( + &mut bound, + 1, + OnTheFlyQueryFamilyExecutionReportV1::default(), + record, + ) + .unwrap(); + assert_eq!( + (singleton.contribution_calls, singleton.contribution_rows), + (1, 3) + ); + assert_eq!(census.native_calls.load(Ordering::Relaxed), 1); + assert_eq!(census.native_runs.load(Ordering::Relaxed), 1); + assert_eq!(census.raw_calls.load(Ordering::Relaxed), 1); + assert_eq!(census.raw_rows.load(Ordering::Relaxed), 1); + assert_eq!(bound.workspace.current_re.as_slice()[2 * stride], 34.0); + assert_eq!(bound.workspace.current_re.as_slice()[3 * stride], 68.0); + assert_eq!(bound.workspace.current_re.as_slice()[4 * stride], 43.0); + + bound.workspace.current_re.as_mut_slice().fill(0.0); + bound.workspace.current_im.as_mut_slice().fill(0.0); + bound.workspace.current_re.as_mut_slice()[0] = 3.0; + bound.workspace.current_re.as_mut_slice()[1] = 5.0; + bound.workspace.current_re.as_mut_slice()[stride] = 4.0; + bound.workspace.current_re.as_mut_slice()[stride + 1] = 6.0; + let batched = execute_bound_family( + &mut bound, + 2, + OnTheFlyQueryFamilyExecutionReportV1::default(), + record, + ) + .unwrap(); + assert_eq!( + (batched.contribution_calls, batched.contribution_rows), + (1, 3) + ); + assert_eq!(census.native_calls.load(Ordering::Relaxed), 1); + assert_eq!(census.native_runs.load(Ordering::Relaxed), 1); + assert_eq!(census.raw_calls.load(Ordering::Relaxed), 2); + assert_eq!(census.raw_rows.load(Ordering::Relaxed), 4); + assert_eq!( + &bound.workspace.current_re.as_slice()[2 * stride..2 * stride + 2], + &[34.0, 56.0] + ); + assert_eq!( + &bound.workspace.current_re.as_slice()[3 * stride..3 * stride + 2], + &[68.0, 112.0] + ); + assert_eq!( + &bound.workspace.current_re.as_slice()[4 * stride..4 * stride + 2], + &[43.0, 65.0] + ); + } + + #[test] + fn otf_interaction_dispatch_is_singleton_only_and_preserves_logical_counters() { + let catalog = direct_catalog(); + let (trace, projection) = + OnTheFlyStructuralTraceV1::test_query_family_trace(0x91, factor(1)); + let mut family = build_query_family_from_traces( + &catalog, + &[QueryFamilyTraceInput { + trace: &trace, + projection, + }], + ) + .unwrap(); + let seed_digest = trace.seed_digest; + let key = OnTheFlyExecutorKeyV1::PreparedOperation { + direct_catalog_digest: catalog.direct_template_catalog_digest(), + role: DirectExecutorRole::Contribution, + operation_kind: OnTheFlyOperationKindV1::Transition, + operation_id: 0, + operation_semantic_digest: SemanticDigest::new([0x92; 32]).unwrap(), + evaluator_binding_id: 1, + evaluator_binding_semantic_digest: SemanticDigest::new([0x93; 32]).unwrap(), + }; + let row = |parents: [u32; 2], destination_component_base: u32, initializes: bool| { + DirectContributionRow { + parent0_component_base: parents[0], + parent1_component_base_or_sentinel: parents[1], + parent0_momentum_form_id: 0, + parent1_momentum_form_id_or_sentinel: 0, + destination_component_base, + exact_factor_id: 0, + selector_domain_id: 0, + flags: if initializes { + DIRECT_CONTRIBUTION_FLAG_INITIALIZE_DESTINATION + } else { + 0 + }, + } + }; + family.source_count = 1; + family.lorentz_component_count = 1; + family.parameter_count = 0; + family.current_component_count = 4; + family.amplitude_destination_count = 1; + family.momentum_forms = vec![ + CanonicalMomentumLinearForm::new(vec![MomentumTerm { + source_slot: 0, + coefficient: 1, + }]) + .unwrap(), + ] + .into_boxed_slice(); + family.exact_factors = vec![ExactComplexRational::ONE].into_boxed_slice(); + family.row_groups = vec![ + OnTheFlyFamilyRowGroupV1 { + seed_digest, + stage: 1, + role: DirectExecutorRole::Contribution, + direct_executor_id: 2, + representative_key: key, + member_bindings: vec![(key, [0, 1])].into_boxed_slice(), + rows: OnTheFlyFamilyRowsV1::Contribution( + vec![row([1, 0], 2, true), row([1, 0], 3, true)].into_boxed_slice(), + ), + }, + OnTheFlyFamilyRowGroupV1 { + seed_digest, + stage: 1, + role: DirectExecutorRole::Contribution, + direct_executor_id: 5, + representative_key: key, + member_bindings: vec![(key, [0, 1])].into_boxed_slice(), + rows: OnTheFlyFamilyRowsV1::Contribution( + vec![row([0, 1], 3, false), row([0, 1], 2, false)].into_boxed_slice(), + ), + }, + ] + .into_boxed_slice(); + #[cfg(any(test, feature = "on-the-fly-test-support"))] + { + family.observed_currents = Box::new([]); + } + + let census = InteractionPathCensus::default(); + let context = (&census as *const InteractionPathCensus).cast::(); + let raw_handle = DirectExecutorHandle::Contribution { + call: interaction_raw_contributions, + context, + }; + let capability = |kind, interaction_call| { + DirectInteractionExecutorCapability::new( + raw_handle, + DirectContributionExecutionMetadata::new(1, false).unwrap(), + DirectContributionFanoutExecutorHandle { + call: interaction_fanout_unreachable, + context, + destination_component_count: 1, + parent_component_counts: [1, 1], + requires_two_momenta: false, + required_parameter_count: 0, + interaction_kind: kind, + interaction_call, + }, + ) + .unwrap() + }; + let resolved_groups = vec![ + ResolvedOnTheFlyExecutor { + direct_executor_id: 2, + handle: raw_handle, + parent_permutation: [0, 1], + packed_singleton_capable: false, + interaction_capability: Some(capability( + DirectInteractionIntrinsicKind::AntisymmetricTensorVector, + None, + )), + }, + ResolvedOnTheFlyExecutor { + direct_executor_id: 5, + handle: raw_handle, + parent_permutation: [0, 1], + packed_singleton_capable: false, + interaction_capability: Some(capability( + DirectInteractionIntrinsicKind::ColorOrderedThreeVector, + Some(count_otf_interaction), + )), + }, + ] + .into_boxed_slice(); + let workspace = OnTheFlyFamilyWorkspaceV1::new(&family, 2, false).unwrap(); + let interaction_program = + OnTheFlyQueryFamilyExecutorV1::::bind_interaction_program( + &family, + &resolved_groups, + RecurrenceStrategy::ContractedColorUnion, + ) + .unwrap(); + let singleton_fanout_program = + OnTheFlyQueryFamilyExecutorV1::::bind_singleton_fanout_program( + &family, + &resolved_groups, + ) + .unwrap(); + let mut bound = BoundOnTheFlyQueryFamilyV1 { + identity: QueryFamilyCacheIdentityV1::Streamed { + direct_catalog_digest: catalog.direct_template_catalog_digest(), + query_count: 1, + ordered_query_digest: SemanticDigest::new([0x94; 32]).unwrap(), + }, + strategy: RecurrenceStrategy::ContractedColorUnion, + family, + workspace, + resolved_groups, + packed_singleton_capable: false, + interaction_program, + singleton_fanout_program, + applied_parameter_version: 0, + descriptor_exposed: false, + }; + let record = |report: &mut OnTheFlyQueryFamilyExecutionReportV1, role, row_count| { + assert_eq!(role, DirectExecutorRole::Contribution); + add(&mut report.contribution_calls, 1, "test contribution calls")?; + add( + &mut report.contribution_rows, + row_count, + "test contribution rows", + ) + }; + + let singleton = execute_bound_family( + &mut bound, + 1, + OnTheFlyQueryFamilyExecutionReportV1::default(), + record, + ) + .unwrap(); + assert_eq!( + (singleton.contribution_calls, singleton.contribution_rows), + (2, 4) + ); + assert_eq!(census.native_calls.load(Ordering::Relaxed), 1); + assert_eq!(census.raw_calls.load(Ordering::Relaxed), 0); + + let batched = execute_bound_family( + &mut bound, + 2, + OnTheFlyQueryFamilyExecutionReportV1::default(), + record, + ) + .unwrap(); + assert_eq!( + (batched.contribution_calls, batched.contribution_rows), + (2, 4) + ); + assert_eq!(census.native_calls.load(Ordering::Relaxed), 1); + assert_eq!(census.raw_calls.load(Ordering::Relaxed), 2); + assert_eq!(census.raw_rows.load(Ordering::Relaxed), 4); + } + + fn streamed_pair_candidate( + catalog: &PreparedDirectExecutorCatalog, + first: &OnTheFlyStructuralTraceV1, + first_projection: OnTheFlyProjectionProbeV1, + second: &OnTheFlyStructuralTraceV1, + second_projection: OnTheFlyProjectionProbeV1, + split: bool, + ) -> OnTheFlyStreamedQueryFamilyCandidateV1 { + build_streamed_query_family_candidate_v1(catalog, |consumer| { + if split { + assert_eq!( + consumer.push_chunk(&[QueryFamilyTraceInput { + trace: first, + projection: first_projection, + }])?, + 0..1 + ); + assert_eq!( + consumer.push_chunk(&[QueryFamilyTraceInput { + trace: second, + projection: second_projection, + }])?, + 1..2 + ); + } else { + assert_eq!( + consumer.push_chunk(&[ + QueryFamilyTraceInput { + trace: first, + projection: first_projection, + }, + QueryFamilyTraceInput { + trace: second, + projection: second_projection, + }, + ])?, + 0..2 + ); + } + Ok(()) + }) + .unwrap() + .expect("two nonzero traces must materialize one streamed family") + } + + #[test] + fn streamed_query_family_is_chunk_boundary_independent_and_warm_reusable() { + let catalog = direct_catalog(); + let (first, first_projection) = + OnTheFlyStructuralTraceV1::test_query_family_trace(0x81, factor(1)); + let (mut second, second_projection) = + OnTheFlyStructuralTraceV1::test_query_family_trace(0x82, factor(1)); + second.test_remap_dynamic_color_id(7); + + let monolithic = streamed_pair_candidate( + &catalog, + &first, + first_projection, + &second, + second_projection, + false, + ); + let exact_lc_family = build_query_family_from_traces( + &catalog, + &[ + QueryFamilyTraceInput { + trace: &first, + projection: first_projection, + }, + QueryFamilyTraceInput { + trace: &second, + projection: second_projection, + }, + ], + ) + .unwrap(); + let chunked = streamed_pair_candidate( + &catalog, + &first, + first_projection, + &second, + second_projection, + true, + ); + assert_eq!(exact_lc_family, monolithic.family); + assert_eq!(monolithic.identity, chunked.identity); + assert_eq!(monolithic.family, chunked.family); + + let reversed = build_streamed_query_family_candidate_v1(&catalog, |consumer| { + assert_eq!( + consumer.push_chunk(&[ + QueryFamilyTraceInput { + trace: &second, + projection: second_projection, + }, + QueryFamilyTraceInput { + trace: &first, + projection: first_projection, + }, + ])?, + 0..2 + ); + Ok(()) + }) + .unwrap() + .unwrap(); + assert_ne!(chunked.identity, reversed.identity); + + let mut monolithic_executor = + OnTheFlyQueryFamilyExecutorV1::new(ProbeResolver::new(catalog.clone())); + let mut chunked_executor = + OnTheFlyQueryFamilyExecutorV1::new(ProbeResolver::new(catalog.clone())); + monolithic_executor + .prepare_streamed_candidate(monolithic, 1) + .unwrap(); + chunked_executor + .prepare_streamed_candidate(chunked, 1) + .unwrap(); + let mut monolithic_outputs = [(0.0, 0.0); 2]; + let mut chunked_outputs = [(0.0, 0.0); 2]; + let monolithic_report = monolithic_executor + .execute_into(&one_point_momenta(2.0, 3.0), 1, &mut monolithic_outputs) + .unwrap(); + let chunked_report = chunked_executor + .execute_into(&one_point_momenta(2.0, 3.0), 1, &mut chunked_outputs) + .unwrap(); + assert_eq!(monolithic_report, chunked_report); + assert_eq!(monolithic_outputs, chunked_outputs); + assert_eq!(chunked_outputs, [(46.0, 0.0), (46.0, 0.0)]); + assert_eq!(chunked_executor.retained_family_count(), 1); + let retained_handle = chunked_executor.active_retained_handle().unwrap(); + assert!(matches!( + chunked_executor.families[0].identity, + QueryFamilyCacheIdentityV1::Streamed { query_count: 2, .. } + )); + + let same_candidate = streamed_pair_candidate( + &catalog, + &first, + first_projection, + &second, + second_projection, + true, + ); + assert!( + chunked_executor + .prepare_streamed_candidate(same_candidate, 1) + .unwrap() + ); + assert_eq!( + chunked_executor.active_retained_handle(), + Some(retained_handle) + ); + assert_eq!(chunked_executor.retained_family_count(), 1); + assert!( + chunked_executor + .execute_into(&one_point_momenta(2.0, 3.0), 1, &mut chunked_outputs) + .unwrap() + .cache_hit + ); + } + + #[test] + fn streamed_query_family_rejects_cross_chunk_duplicates_and_conflicts() { + let catalog = direct_catalog(); + let (first, first_projection) = + OnTheFlyStructuralTraceV1::test_query_family_trace(0x81, factor(1)); + let repeated = build_streamed_query_family_candidate_v1(&catalog, |consumer| { + consumer.push_chunk(&[QueryFamilyTraceInput { + trace: &first, + projection: first_projection, + }])?; + consumer.push_chunk(&[QueryFamilyTraceInput { + trace: &first, + projection: first_projection, + }])?; + Ok(()) + }) + .unwrap_err(); + assert!(repeated.to_string().contains("repeats a selected query")); + + let (second, second_projection) = + OnTheFlyStructuralTraceV1::test_query_family_trace(0x82, factor(2)); + let conflicting = build_streamed_query_family_candidate_v1(&catalog, |consumer| { + consumer.push_chunk(&[QueryFamilyTraceInput { + trace: &first, + projection: first_projection, + }])?; + consumer.push_chunk(&[QueryFamilyTraceInput { + trace: &second, + projection: second_projection, + }])?; + Ok(()) + }) + .unwrap_err(); + assert!(conflicting.to_string().contains("conflicting definitions")); + } + + #[test] + fn query_family_union_interns_exact_currents_and_retains_destinations() { + let (first, first_projection) = + OnTheFlyStructuralTraceV1::test_query_family_trace(0x81, factor(1)); + let (mut second, second_projection) = + OnTheFlyStructuralTraceV1::test_query_family_trace(0x82, factor(1)); + second.test_remap_dynamic_color_id(7); + let census = query_family_census_from_traces( + &direct_catalog(), + &[ + QueryFamilyTraceInput { + trace: &first, + projection: first_projection, + }, + QueryFamilyTraceInput { + trace: &second, + projection: second_projection, + }, + ], + ) + .unwrap(); + + assert_eq!(census.query_count, 2); + assert_eq!(census.source_frame_partition_count, 1); + assert_eq!(census.projection_applied_query_count, 0); + assert_eq!(census.projection_pre_current_count, 6); + assert_eq!(census.projection_pre_contribution_count, 2); + assert_eq!(census.projection_pre_closure_count, 2); + assert_eq!(census.projection_post_current_count, 6); + assert_eq!(census.projection_post_contribution_count, 2); + assert_eq!(census.projection_post_closure_count, 2); + assert_eq!(census.dynamic_current_occurrence_count, 6); + assert_eq!(census.dynamic_current_component_occurrence_count, 6); + assert_eq!(census.dynamic_source_rows, 4); + assert_eq!(census.dynamic_contribution_rows, 2); + assert_eq!(census.dynamic_finalization_rows, 2); + assert_eq!(census.dynamic_closure_rows, 2); + assert_eq!(census.dynamic_source_calls, 4); + assert_eq!(census.dynamic_contribution_calls, 2); + assert_eq!(census.dynamic_finalization_calls, 2); + assert_eq!(census.dynamic_closure_calls, 2); + assert_eq!(census.union_unique_current_count, 3); + assert_eq!(census.union_unique_current_component_count, 3); + assert_eq!(census.union_source_rows, 2); + assert_eq!(census.union_contribution_rows, 1); + assert_eq!(census.union_finalization_rows, 1); + assert_eq!(census.union_closure_rows, 2); + assert_eq!(census.union_kernel_application_count().unwrap(), 6); + assert_eq!(census.union_amplitude_destination_count, 2); + assert_eq!(census.union_source_executor_call_groups, 1); + assert_eq!(census.union_contribution_executor_call_groups, 1); + assert_eq!(census.union_finalization_executor_call_groups, 1); + assert_eq!(census.union_closure_executor_call_groups, 1); + } + + #[test] + fn query_family_groups_distinct_semantic_operations_by_prepared_executor() { + let (trace, projection) = + OnTheFlyStructuralTraceV1::test_query_family_trace(0x81, factor(1)); + let census = query_family_census_from_traces( + &direct_catalog(), + &[QueryFamilyTraceInput { + trace: &trace, + projection, + }], + ) + .unwrap(); + + // The two source operations have different authenticated operation and + // evaluator-binding identities, but the prepared catalog deliberately + // maps both onto one Direct-Arena source executor. + assert_eq!(census.union_source_rows, 2); + assert_eq!(census.union_source_executor_call_groups, 1); + } + + #[test] + fn query_family_union_rejects_conflicting_semantic_current_definitions() { + let (first, first_projection) = + OnTheFlyStructuralTraceV1::test_query_family_trace(0x81, factor(1)); + let (second, second_projection) = + OnTheFlyStructuralTraceV1::test_query_family_trace(0x82, factor(2)); + let error = query_family_census_from_traces( + &direct_catalog(), + &[ + QueryFamilyTraceInput { + trace: &first, + projection: first_projection, + }, + QueryFamilyTraceInput { + trace: &second, + projection: second_projection, + }, + ], + ) + .unwrap_err(); + assert!(error.to_string().contains("conflicting definitions")); + } + + #[test] + fn query_family_union_keeps_distinct_source_frame_partitions() { + let (first, first_projection) = + OnTheFlyStructuralTraceV1::test_query_family_trace(0x81, factor(1)); + let (mut second, second_projection) = + OnTheFlyStructuralTraceV1::test_query_family_trace(0x82, factor(1)); + second.seed_digest = SemanticDigest::new([0x83; 32]).unwrap(); + + let census = query_family_census_from_traces( + &direct_catalog(), + &[ + QueryFamilyTraceInput { + trace: &first, + projection: first_projection, + }, + QueryFamilyTraceInput { + trace: &second, + projection: second_projection, + }, + ], + ) + .unwrap(); + assert_eq!(census.source_frame_partition_count, 2); + assert_eq!(census.union_unique_current_count, 6); + assert_eq!(census.union_source_rows, 4); + assert_eq!(census.union_contribution_rows, 2); + assert_eq!(census.union_finalization_rows, 2); + assert_eq!(census.union_closure_rows, 2); + assert_eq!(census.union_source_executor_call_groups, 2); + assert_eq!(census.union_contribution_executor_call_groups, 2); + assert_eq!(census.union_finalization_executor_call_groups, 2); + assert_eq!(census.union_closure_executor_call_groups, 2); + } + + #[test] + fn query_family_union_rejects_repeated_queries_and_stale_projection_proofs() { + let (trace, projection) = + OnTheFlyStructuralTraceV1::test_query_family_trace(0x81, factor(1)); + let repeated = query_family_census_from_traces( + &direct_catalog(), + &[ + QueryFamilyTraceInput { + trace: &trace, + projection, + }, + QueryFamilyTraceInput { + trace: &trace, + projection, + }, + ], + ) + .unwrap_err(); + assert!(repeated.to_string().contains("repeats a selected query")); + + let stale = OnTheFlyProjectionProbeV1 { + post: [2, 1, 1], + ..projection + }; + let error = query_family_census_from_traces( + &direct_catalog(), + &[QueryFamilyTraceInput { + trace: &trace, + projection: stale, + }], + ) + .unwrap_err(); + assert!( + error + .to_string() + .contains("projection proof does not describe") + ); + } + + fn one_point_momenta(source0: f64, source1: f64) -> Vec { + vec![source0, 0.0, 0.0, 0.0, source1, 0.0, 0.0, 0.0] + } + + fn source_major_momenta(source0: &[f64], source1: &[f64]) -> Vec { + assert_eq!(source0.len(), source1.len()); + let point_count = source0.len(); + let mut momenta = Vec::with_capacity(8 * point_count); + for source in [source0, source1] { + momenta.extend_from_slice(source); + momenta.extend(std::iter::repeat_n(0.0, 3 * point_count)); + } + momenta + } + + #[test] + fn momentum_plane_refresh_preserves_scalar_order_and_inactive_lanes() { + let (trace, projection) = + OnTheFlyStructuralTraceV1::test_query_family_trace(0x81, factor(1)); + let mut family = build_query_family_from_traces( + &direct_catalog(), + &[QueryFamilyTraceInput { + trace: &trace, + projection, + }], + ) + .unwrap(); + family.source_count = 3; + family.momentum_forms = [ + vec![], + vec![(0, -1)], + vec![(0, 1), (1, 1), (2, 1)], + vec![(0, 3), (1, -2), (2, 7)], + ] + .into_iter() + .map(|terms| { + CanonicalMomentumLinearForm::new( + terms + .into_iter() + .map(|(source_slot, coefficient)| MomentumTerm { + source_slot, + coefficient, + }) + .collect(), + ) + .unwrap() + }) + .collect::>() + .into_boxed_slice(); + let mut workspace = OnTheFlyFamilyWorkspaceV1::new(&family, 9, false).unwrap(); + let stride = workspace.point_stride as usize; + let lorentz_count = usize::from(family.lorentz_component_count); + for point_count in [1, 9, 3, 5, 1] { + let mut inputs = Vec::new(); + for source in 0..family.source_count as usize { + for lorentz in 0..lorentz_count { + for point in 0..point_count { + inputs.push(match lorentz { + 0 => [1.0e16, -1.0e16, 1.0][source] * (point + 1) as f64, + 1 if (point + source) % 2 == 0 => -0.0, + 1 => 0.0, + 2 => (source + point + 1) as f64 * f64::MIN_POSITIVE, + _ => (source * 31 + point * 7) as f64 / 13.0, + }); + } + } + } + workspace.momenta.as_mut_slice().fill(31.0); + workspace.amplitude_re.as_mut_slice().fill(37.0); + workspace.amplitude_im.as_mut_slice().fill(41.0); + workspace.current_re.as_mut_slice().fill(43.0); + workspace.current_im.as_mut_slice().fill(47.0); + workspace + .refresh_inputs(&family, &inputs, point_count as u32) + .unwrap(); + for (form_id, form) in family.momentum_forms.iter().enumerate() { + for lorentz in 0..lorentz_count { + let plane = (form_id * lorentz_count + lorentz) * stride; + for point in 0..point_count { + let mut expected = 0.0_f64; + for term in form.terms() { + let source = (term.source_slot as usize * lorentz_count + lorentz) + * point_count + + point; + expected += f64::from(term.coefficient) * inputs[source]; + } + assert_eq!( + workspace.momenta.as_slice()[plane + point].to_bits(), + expected.to_bits(), + ); + } + assert!( + workspace.momenta.as_slice()[plane + point_count..plane + stride] + .iter() + .all(|value| *value == 31.0) + ); + } + } + for (amplitudes, sentinel) in [ + (&workspace.amplitude_re, 37.0), + (&workspace.amplitude_im, 41.0), + ] { + for plane in amplitudes.as_slice().chunks_exact(stride) { + assert!(plane[..point_count].iter().all(|value| *value == 0.0)); + assert!(plane[point_count..].iter().all(|value| *value == sentinel)); + } + } + assert!( + workspace + .current_re + .as_slice() + .iter() + .all(|value| *value == 43.0) + ); + assert!( + workspace + .current_im + .as_slice() + .iter() + .all(|value| *value == 47.0) + ); + } + assert!(workspace.refresh_inputs(&family, &[], 1).is_err()); + assert!(workspace.refresh_inputs(&family, &[], 0).is_err()); + assert!(workspace.refresh_inputs(&family, &[], 10).is_err()); + + family.momentum_forms = vec![ + CanonicalMomentumLinearForm::new(vec![MomentumTerm { + source_slot: family.source_count, + coefficient: 1, + }]) + .unwrap(), + ] + .into_boxed_slice(); + let error = OnTheFlyFamilyWorkspaceV1::new(&family, 1, false) + .err() + .expect("out-of-bounds sources must fail during preparation"); + assert!( + error + .to_string() + .contains("momentum source slot is out of bounds") + ); + } + + #[test] + fn packed_singleton_family_requires_every_executor_role() { + let catalog = direct_catalog(); + let (trace, projection) = + OnTheFlyStructuralTraceV1::test_query_family_trace(0x81, factor(1)); + let selected = [QueryFamilyTraceInput { + trace: &trace, + projection, + }]; + + for missing_role in [ + DirectExecutorRole::Source, + DirectExecutorRole::Contribution, + DirectExecutorRole::Finalization, + DirectExecutorRole::Closure, + ] { + let mut resolver = ProbeResolver::packed_singleton(catalog.clone()); + resolver.disable_packed_singleton_role(missing_role); + let mut executor = OnTheFlyQueryFamilyExecutorV1::new(resolver); + assert!(!executor.prepare(&catalog, &selected, 1).unwrap()); + let pending = executor.pending.as_ref().unwrap(); + assert!( + !pending.packed_singleton_capable, + "missing {missing_role:?}" + ); + assert_eq!( + pending.workspace.point_stride, + checked_aligned_point_stride(1).unwrap(), + "missing {missing_role:?}" + ); + } + } + + #[test] + fn packed_singleton_family_promotes_once_for_batched_execution() { + let catalog = direct_catalog(); + let resolver = ProbeResolver::packed_singleton(catalog.clone()); + let (trace, projection) = + OnTheFlyStructuralTraceV1::test_query_family_trace(0x81, factor(1)); + let selected = [QueryFamilyTraceInput { + trace: &trace, + projection, + }]; + let mut executor = OnTheFlyQueryFamilyExecutorV1::new(resolver); + + assert!(!executor.prepare(&catalog, &selected, 1).unwrap()); + let pending = executor.pending.as_ref().unwrap(); + assert!(pending.packed_singleton_capable); + assert_eq!(pending.workspace.point_stride, 1); + let mut scalar_output = [(0.0, 0.0)]; + executor + .execute_into(&one_point_momenta(2.0, 3.0), 1, &mut scalar_output) + .unwrap(); + assert_eq!(scalar_output, [(46.0, 0.0)]); + assert_eq!(executor.resolver().state.invalidations.get(), 0); + + assert!(executor.resize_active_family(2).unwrap()); + assert_eq!(executor.resolver().state.invalidations.get(), 1); + let retained = &executor.families[executor.last_used.unwrap()]; + assert!(retained.packed_singleton_capable); + assert_eq!(retained.workspace.logical_point_capacity, 2); + assert_eq!( + retained.workspace.point_stride, + checked_aligned_point_stride(2).unwrap() + ); + + let mut batched_output = [(0.0, 0.0); 2]; + executor + .execute_into( + &source_major_momenta(&[2.0, 4.0], &[3.0, 5.0]), + 2, + &mut batched_output, + ) + .unwrap(); + assert_eq!(batched_output, [(46.0, 0.0), (180.0, 0.0)]); + + executor + .execute_into(&one_point_momenta(2.0, 3.0), 1, &mut scalar_output) + .unwrap(); + assert_eq!(scalar_output, [(46.0, 0.0)]); + assert_eq!( + executor.families[executor.last_used.unwrap()] + .workspace + .point_stride, + checked_aligned_point_stride(2).unwrap() + ); + } + + #[test] + fn query_family_snapshot_reuses_rows_with_new_inputs_and_ignores_pending_candidate() { + let catalog = direct_catalog(); + let (first, projection) = + OnTheFlyStructuralTraceV1::test_query_family_trace(0x81, factor(1)); + let mut source = + OnTheFlyQueryFamilyExecutorV1::new(ProbeResolver::packed_singleton(catalog.clone())); + assert!(source.snapshot().unwrap().is_none()); + source + .prepare( + &catalog, + &[QueryFamilyTraceInput { + trace: &first, + projection, + }], + 1, + ) + .unwrap(); + // A constructed but unevaluated candidate is not a completed cache. + assert!(source.snapshot().unwrap().is_none()); + let mut first_output = [(0.0, 0.0)]; + source + .execute_into(&one_point_momenta(2.0, 3.0), 1, &mut first_output) + .unwrap(); + assert_eq!(first_output, [(46.0, 0.0)]); + let (second, second_projection) = + OnTheFlyStructuralTraceV1::test_query_family_trace(0x82, factor(2)); + source + .prepare( + &catalog, + &[QueryFamilyTraceInput { + trace: &second, + projection: second_projection, + }], + 1, + ) + .unwrap(); + let bytes = bincode::encode_to_vec(source.snapshot().unwrap(), bincode::config::standard()) + .unwrap(); + let (saved, used): (Option, usize) = + bincode::decode_from_slice(&bytes, bincode::config::standard()).unwrap(); + assert_eq!(used, bytes.len()); + let mut restored = + OnTheFlyQueryFamilyExecutorV1::new(ProbeResolver::packed_singleton(catalog)); + let handle = restored.restore_snapshot(saved.unwrap(), 1).unwrap(); + assert!(restored.activate_retained_family(handle, 2).unwrap()); + let mut outputs = [(0.0, 0.0); 2]; + let report = restored + .execute_into( + &source_major_momenta(&[2.0, 4.0], &[3.0, 5.0]), + 2, + &mut outputs, + ) + .unwrap(); + assert!(report.cache_hit); + assert_eq!(outputs, [(46.0, 0.0), (180.0, 0.0)]); + assert!(restored.snapshot().unwrap().is_some()); + + // Even the counter-exhaustion boundary preserves the old cache. + let (saved, _): (Option, usize) = + bincode::decode_from_slice(&bytes, bincode::config::standard()).unwrap(); + restored.family_generation = u64::MAX; + assert!(restored.restore_snapshot(saved.unwrap(), 1).is_err()); + assert!(restored.pending.is_none()); + let report = restored + .execute_into(&one_point_momenta(2.0, 3.0), 1, &mut first_output) + .unwrap(); + assert!(report.cache_hit); + assert_eq!(first_output, [(46.0, 0.0)]); + } + + #[test] + fn query_family_executes_union_once_and_reuses_stable_warm_rows() { + let catalog = direct_catalog(); + let resolver = ProbeResolver::new(catalog.clone()); + let (mut first, first_projection) = + OnTheFlyStructuralTraceV1::test_query_family_trace(0x81, factor(1)); + let (mut second, second_projection) = + OnTheFlyStructuralTraceV1::test_query_family_trace(0x82, factor(1)); + first.layout.parameter_count = 1; + second.layout.parameter_count = 1; + second.test_remap_dynamic_color_id(7); + let selected = [ + QueryFamilyTraceInput { + trace: &first, + projection: first_projection, + }, + QueryFamilyTraceInput { + trace: &second, + projection: second_projection, + }, + ]; + let mut executor = OnTheFlyQueryFamilyExecutorV1::new(resolver); + assert!(!executor.prepare(&catalog, &selected, 3).unwrap()); + let census = executor.prepared_census().unwrap(); + assert_eq!(census.union_unique_current_count, 3); + assert_eq!(census.union_amplitude_destination_count, 2); + executor.set_parameters(&[(1.0, 0.0)]).unwrap(); + let momenta = source_major_momenta(&[2.0, 4.0, -1.0], &[3.0, 5.0, 2.0]); + let mut outputs = [(0.0, 0.0); 6]; + let cold = executor.execute_into(&momenta, 3, &mut outputs).unwrap(); + assert!(!cold.cache_hit); + assert_eq!( + cold, + OnTheFlyQueryFamilyExecutionReportV1 { + cache_hit: false, + source_calls: 1, + source_rows: 2, + contribution_calls: 1, + contribution_rows: 1, + finalization_calls: 1, + finalization_rows: 1, + closure_calls: 1, + closure_rows: 2, + } + ); + assert_eq!( + outputs, + [ + (46.0, 0.0), + (180.0, 0.0), + (8.0, 0.0), + (46.0, 0.0), + (180.0, 0.0), + (8.0, 0.0), + ] + ); + let first_calls = executor.resolver().state.calls.borrow().clone(); + assert_eq!( + first_calls.iter().map(|call| call.0).collect::>(), + vec![ + DirectExecutorRole::Source, + DirectExecutorRole::Contribution, + DirectExecutorRole::Finalization, + DirectExecutorRole::Closure, + ] + ); + + assert!(executor.prepare(&catalog, &selected, 3).unwrap()); + executor.set_parameters(&[(2.0, 0.0)]).unwrap(); + let retained = executor + .last_used + .and_then(|index| executor.families.get_mut(index)) + .expect("cold execution did not retain its query family"); + retained.workspace.current_re.as_mut_slice().fill(f64::NAN); + retained.workspace.current_im.as_mut_slice().fill(f64::NAN); + let warm = executor.execute_into(&momenta, 3, &mut outputs).unwrap(); + assert!(warm.cache_hit); + assert_eq!( + outputs, + [ + (92.0, 0.0), + (360.0, 0.0), + (16.0, 0.0), + (92.0, 0.0), + (360.0, 0.0), + (16.0, 0.0), + ] + ); + let calls = executor.resolver().state.calls.borrow(); + assert_eq!(calls.len(), 8); + assert_eq!( + calls[..4] + .iter() + .map(|call| (call.0, call.1, call.2)) + .collect::>(), + calls[4..] + .iter() + .map(|call| (call.0, call.1, call.2)) + .collect::>() + ); + } + + #[test] + fn query_family_interns_shared_scalars_and_exact_component_blocks() { + let mut factors = Vec::new(); + let mut scalar_ids = BTreeMap::new(); + let mut block_ids = BTreeMap::new(); + let one = factor(1); + let two = factor(2); + assert_eq!( + intern_factor(&mut scalar_ids, &mut factors, one).unwrap(), + 0 + ); + assert_eq!( + intern_factor(&mut scalar_ids, &mut factors, one).unwrap(), + 0 + ); + let forward = + intern_factor_block(&mut block_ids, &mut scalar_ids, &mut factors, &[one, two]) + .unwrap(); + assert_eq!(forward, 1); + assert_eq!( + intern_factor_block(&mut block_ids, &mut scalar_ids, &mut factors, &[one, two],) + .unwrap(), + forward + ); + let reversed = + intern_factor_block(&mut block_ids, &mut scalar_ids, &mut factors, &[two, one]) + .unwrap(); + assert_eq!(reversed, 3); + assert_eq!(factors, vec![one, one, two, two, one]); + + let catalog = direct_catalog(); + let (first, first_projection) = + OnTheFlyStructuralTraceV1::test_query_family_trace(0x81, one); + let (mut second, second_projection) = + OnTheFlyStructuralTraceV1::test_query_family_trace(0x82, one); + second.test_remap_dynamic_color_id(7); + let family = build_query_family_from_traces( + &catalog, + &[ + QueryFamilyTraceInput { + trace: &first, + projection: first_projection, + }, + QueryFamilyTraceInput { + trace: &second, + projection: second_projection, + }, + ], + ) + .unwrap(); + assert_eq!(family.exact_factors.as_ref(), &[one, one]); + let starts = family + .row_groups + .iter() + .flat_map(|group| match &group.rows { + OnTheFlyFamilyRowsV1::Closure(rows) => rows + .iter() + .map(|row| row.component_factor_start) + .collect::>(), + _ => Vec::new(), + }) + .collect::>(); + assert_eq!(starts, vec![1, 1]); + } + + #[test] + fn query_family_initializes_once_across_executor_groups() { + let catalog = direct_catalog(); + let resolver = ProbeResolver::new(catalog.clone()); + let (mut trace, mut projection) = + OnTheFlyStructuralTraceV1::test_query_family_trace(0x81, factor(1)); + trace.test_add_query_family_contribution(5, 5, factor(1)); + projection.pre[1] = 2; + projection.post[1] = 2; + let selected = [QueryFamilyTraceInput { + trace: &trace, + projection, + }]; + let family = build_query_family_from_traces(&catalog, &selected).unwrap(); + let contribution_groups = family + .row_groups + .iter() + .filter_map(|group| match &group.rows { + OnTheFlyFamilyRowsV1::Contribution(rows) => Some(rows.as_ref()), + _ => None, + }) + .collect::>(); + assert_eq!(contribution_groups.len(), 2); + assert_eq!( + contribution_groups + .iter() + .flat_map(|rows| rows.iter()) + .filter(|row| row.flags & DIRECT_CONTRIBUTION_FLAG_INITIALIZE_DESTINATION != 0) + .count(), + 1 + ); + + let mut executor = OnTheFlyQueryFamilyExecutorV1::new(resolver); + assert!(!executor.prepare(&catalog, &selected, 1).unwrap()); + let mut output = [(0.0, 0.0)]; + let report = executor + .execute_into(&one_point_momenta(2.0, 3.0), 1, &mut output) + .unwrap(); + assert_eq!(report.contribution_calls, 2); + assert_eq!(report.contribution_rows, 2); + assert_eq!(output, [(92.0, 0.0)]); + } + + #[test] + fn query_family_applies_each_prepared_parent_permutation_before_grouping() { + let catalog = direct_catalog(); + let resolver = ProbeResolver::new(catalog.clone()); + let (mut trace, mut projection) = + OnTheFlyStructuralTraceV1::test_query_family_trace(0x81, factor(1)); + trace.test_add_query_family_contribution(6, 6, factor(1)); + trace.test_set_last_contribution_selector_domain(17); + projection.pre[1] = 2; + projection.post[1] = 2; + let selected = [QueryFamilyTraceInput { + trace: &trace, + projection, + }]; + let family = build_query_family_from_traces(&catalog, &selected).unwrap(); + let rows = family + .row_groups + .iter() + .find_map(|group| match &group.rows { + OnTheFlyFamilyRowsV1::Contribution(rows) => Some(rows.as_ref()), + _ => None, + }) + .unwrap(); + assert_eq!(rows.len(), 2); + assert_eq!( + rows.iter() + .map(|row| row.selector_domain_id) + .collect::>(), + BTreeSet::from([0, 17]) + ); + assert_eq!( + rows.iter() + .map(|row| { + ( + row.parent0_component_base, + row.parent1_component_base_or_sentinel, + ) + }) + .collect::>(), + BTreeSet::from([(0, 1), (1, 0)]) + ); + + let mut executor = OnTheFlyQueryFamilyExecutorV1::new(resolver); + executor.prepare(&catalog, &selected, 1).unwrap(); + let mut output = [(0.0, 0.0)]; + let report = executor + .execute_into(&one_point_momenta(2.0, 3.0), 1, &mut output) + .unwrap(); + assert_eq!(report.contribution_calls, 1); + assert_eq!(report.contribution_rows, 2); + assert_eq!(output, [(110.0, 0.0)]); + } + + #[test] + fn query_family_rejects_non_topological_group_order() { + let catalog = direct_catalog(); + let (trace, projection) = + OnTheFlyStructuralTraceV1::test_query_family_trace(0x81, factor(1)); + let mut family = build_query_family_from_traces( + &catalog, + &[QueryFamilyTraceInput { + trace: &trace, + projection, + }], + ) + .unwrap(); + let contribution = family + .row_groups + .iter() + .position(|group| group.role == DirectExecutorRole::Contribution) + .unwrap(); + let finalization = family + .row_groups + .iter() + .position(|group| group.role == DirectExecutorRole::Finalization) + .unwrap(); + family.row_groups.swap(contribution, finalization); + let error = validate_ordered_family_schedule(&family.row_groups).unwrap_err(); + assert!(error.to_string().contains("follows finalization")); + } + + #[test] + fn failed_execution_discards_candidate_and_same_family_can_be_prepared_again() { + let catalog = direct_catalog(); + let resolver = ProbeResolver::new(catalog.clone()); + let (trace, projection) = + OnTheFlyStructuralTraceV1::test_query_family_trace(0x81, factor(1)); + let selected = [QueryFamilyTraceInput { + trace: &trace, + projection, + }]; + let mut executor = OnTheFlyQueryFamilyExecutorV1::new(resolver); + executor.prepare(&catalog, &selected, 1).unwrap(); + executor + .resolver_mut() + .state + .fail_role + .set(Some(DirectExecutorRole::Contribution)); + executor.resolver().state.record_failure_detail.set(true); + crate::recurrence::direct_backend::record_direct_executor_error_detail( + crate::RusticolError::integrity("stale detail from an earlier family schedule"), + ); + let mut output = [(123.0, 456.0)]; + let error = executor + .execute_into_unprofiled(&one_point_momenta(2.0, 3.0), 1, &mut output) + .unwrap_err(); + assert!( + error + .to_string() + .contains("probe Contribution first failure detail") + ); + assert!(!error.to_string().contains("stale detail")); + assert!(!error.to_string().contains("later probe detail")); + assert_eq!(output, [(123.0, 456.0)]); + assert!(executor.pending.is_none()); + assert_eq!(executor.retained_family_count(), 0); + assert_eq!(executor.resolver().state.invalidations.get(), 1); + + executor.resolver().state.fail_role.set(None); + assert!(!executor.prepare(&catalog, &selected, 1).unwrap()); + let report = executor + .execute_into(&one_point_momenta(2.0, 3.0), 1, &mut output) + .unwrap(); + assert!(!report.cache_hit); + assert_eq!(output, [(46.0, 0.0)]); + } + + #[test] + fn partially_failed_family_is_invalidated_before_switching_rows() { + let catalog = direct_catalog(); + let resolver = ProbeResolver::new(catalog.clone()); + let (first, first_projection) = + OnTheFlyStructuralTraceV1::test_query_family_trace(0x81, factor(1)); + let first_selected = [QueryFamilyTraceInput { + trace: &first, + projection: first_projection, + }]; + let mut executor = OnTheFlyQueryFamilyExecutorV1::new(resolver); + executor.prepare(&catalog, &first_selected, 1).unwrap(); + executor + .resolver() + .state + .fail_role + .set(Some(DirectExecutorRole::Finalization)); + let mut output = [(0.0, 0.0)]; + executor + .execute_into(&one_point_momenta(2.0, 3.0), 1, &mut output) + .unwrap_err(); + assert_eq!(executor.retained_family_count(), 0); + assert!(executor.pending.is_none()); + let failed_calls = executor.resolver().state.calls.borrow().clone(); + assert_eq!(failed_calls.len(), 3); + assert_eq!(executor.resolver().state.invalidations.get(), 1); + + let (second, second_projection) = + OnTheFlyStructuralTraceV1::test_query_family_trace(0x82, factor(1)); + let second_selected = [QueryFamilyTraceInput { + trace: &second, + projection: second_projection, + }]; + assert!(!executor.prepare(&catalog, &second_selected, 1).unwrap()); + assert_eq!(executor.resolver().state.invalidations.get(), 1); + executor.resolver().state.fail_role.set(None); + executor + .execute_into(&one_point_momenta(2.0, 3.0), 1, &mut output) + .unwrap(); + assert_eq!(executor.retained_family_count(), 1); + assert_eq!(output, [(46.0, 0.0)]); + let calls = executor.resolver().state.calls.borrow(); + assert_ne!(failed_calls[0].1, calls[3].1); + assert_ne!(failed_calls[1].1, calls[4].1); + } + + #[test] + fn successful_cached_family_is_invalidated_before_new_candidate_runs() { + let catalog = direct_catalog(); + let resolver = ProbeResolver::new(catalog.clone()); + let (first, first_projection) = + OnTheFlyStructuralTraceV1::test_query_family_trace(0x81, factor(1)); + let first_selected = [QueryFamilyTraceInput { + trace: &first, + projection: first_projection, + }]; + let mut executor = OnTheFlyQueryFamilyExecutorV1::new(resolver); + executor.prepare(&catalog, &first_selected, 1).unwrap(); + let mut output = [(0.0, 0.0)]; + executor + .execute_into(&one_point_momenta(2.0, 3.0), 1, &mut output) + .unwrap(); + assert_eq!(executor.resolver().state.invalidations.get(), 0); + let calls_before_prepare = executor.resolver().state.calls.borrow().len(); + + let (second, second_projection) = + OnTheFlyStructuralTraceV1::test_query_family_trace(0x82, factor(1)); + let second_selected = [QueryFamilyTraceInput { + trace: &second, + projection: second_projection, + }]; + executor.prepare(&catalog, &second_selected, 1).unwrap(); + assert_eq!(executor.resolver().state.invalidations.get(), 1); + assert_eq!( + executor.resolver().state.calls.borrow().len(), + calls_before_prepare + ); + assert!( + executor + .families + .iter() + .all(|family| !family.descriptor_exposed) + ); + let report = executor + .execute_into(&one_point_momenta(2.0, 3.0), 1, &mut output) + .unwrap(); + assert!(!report.cache_hit); + assert_eq!(output, [(46.0, 0.0)]); + } + + #[test] + fn failed_candidate_restores_observation_of_last_successful_family() { + let catalog = direct_catalog(); + let resolver = ProbeResolver::new(catalog.clone()); + let (first, first_projection) = + OnTheFlyStructuralTraceV1::test_query_family_trace(0x81, factor(1)); + let first_selected = [QueryFamilyTraceInput { + trace: &first, + projection: first_projection, + }]; + let mut executor = OnTheFlyQueryFamilyExecutorV1::new(resolver); + let mut output = [(0.0, 0.0)]; + executor.prepare(&catalog, &first_selected, 1).unwrap(); + executor + .execute_into(&one_point_momenta(2.0, 3.0), 1, &mut output) + .unwrap(); + let first_handle = executor.active_retained_handle().unwrap(); + assert!(!executor.observed_currents(0).unwrap().is_empty()); + + let (second, second_projection) = + OnTheFlyStructuralTraceV1::test_query_family_trace(0x82, factor(2)); + let second_selected = [QueryFamilyTraceInput { + trace: &second, + projection: second_projection, + }]; + executor.prepare(&catalog, &second_selected, 1).unwrap(); + let prepared_error = executor.observed_currents(0).unwrap_err(); + assert!( + prepared_error + .to_string() + .contains("outside the last successful execution") + ); + + executor + .resolver() + .state + .fail_role + .set(Some(DirectExecutorRole::Contribution)); + executor + .execute_into(&one_point_momenta(2.0, 3.0), 1, &mut output) + .unwrap_err(); + assert!(executor.pending.is_none()); + assert!(!executor.observed_currents(0).unwrap().is_empty()); + assert_eq!(executor.retained_family_count(), 1); + executor.resolver().state.fail_role.set(None); + assert!(executor.activate_retained_family(first_handle, 1).unwrap()); + let report = executor + .execute_into(&one_point_momenta(2.0, 3.0), 1, &mut output) + .unwrap(); + assert!(report.cache_hit); + assert_eq!(output, [(46.0, 0.0)]); + + executor.clear_families().unwrap(); + assert!( + executor + .observed_currents(0) + .unwrap_err() + .to_string() + .contains("requires a prepared execution") + ); + } + + #[test] + fn last_family_only_makes_evicted_a_cold_again_across_a_b_a() { + let catalog = direct_catalog(); + let resolver = ProbeResolver::new(catalog.clone()); + let (first, first_projection) = + OnTheFlyStructuralTraceV1::test_query_family_trace(0x81, factor(1)); + let first_selected = [QueryFamilyTraceInput { + trace: &first, + projection: first_projection, + }]; + let (second, second_projection) = + OnTheFlyStructuralTraceV1::test_query_family_trace(0x82, factor(2)); + let second_selected = [QueryFamilyTraceInput { + trace: &second, + projection: second_projection, + }]; + let mut executor = OnTheFlyQueryFamilyExecutorV1::new(resolver); + let mut output = [(0.0, 0.0)]; + + assert!(!executor.prepare(&catalog, &first_selected, 1).unwrap()); + executor + .execute_into(&one_point_momenta(2.0, 3.0), 1, &mut output) + .unwrap(); + let first_output = output; + let first_schedule = executor + .resolver() + .state + .calls + .borrow() + .iter() + .map(|(role, _, row_count)| (*role, *row_count)) + .collect::>(); + let first_handle = executor.active_retained_handle().unwrap(); + executor.resolver().state.calls.borrow_mut().clear(); + + assert!(!executor.prepare(&catalog, &second_selected, 1).unwrap()); + executor + .execute_into(&one_point_momenta(2.0, 3.0), 1, &mut output) + .unwrap(); + assert_ne!(output, first_output); + assert_eq!(executor.retained_family_count(), 1); + executor.resolver().state.calls.borrow_mut().clear(); + + assert!( + executor + .activate_retained_family(first_handle, 1) + .unwrap_err() + .to_string() + .contains("cleared arena") + ); + assert!(!executor.prepare(&catalog, &first_selected, 1).unwrap()); + let report = executor + .execute_into(&one_point_momenta(2.0, 3.0), 1, &mut output) + .unwrap(); + assert!(!report.cache_hit); + assert_eq!(output, first_output); + assert_eq!(executor.retained_family_count(), 1); + let rebuilt_schedule = executor + .resolver() + .state + .calls + .borrow() + .iter() + .map(|(role, _, row_count)| (*role, *row_count)) + .collect::>(); + assert_eq!(rebuilt_schedule, first_schedule); + assert_eq!(executor.resolver().state.invalidations.get(), 2); + } + + #[test] + fn clear_invalidates_rows_before_dropping_all_retained_families() { + let catalog = direct_catalog(); + let resolver = ProbeResolver::new(catalog.clone()); + let (trace, projection) = + OnTheFlyStructuralTraceV1::test_query_family_trace(0x81, factor(1)); + let selected = [QueryFamilyTraceInput { + trace: &trace, + projection, + }]; + let mut executor = OnTheFlyQueryFamilyExecutorV1::new(resolver); + let mut output = [(0.0, 0.0)]; + executor.prepare(&catalog, &selected, 1).unwrap(); + executor + .execute_into(&one_point_momenta(2.0, 3.0), 1, &mut output) + .unwrap(); + assert_eq!(executor.retained_family_count(), 1); + let handle = executor.active_retained_handle().unwrap(); + + executor.clear_families().unwrap(); + assert_eq!(executor.resolver().state.invalidations.get(), 1); + assert_eq!(executor.retained_family_count(), 0); + assert!(executor.prepared_census().is_none()); + assert!( + executor + .activate_retained_family(handle, 1) + .unwrap_err() + .to_string() + .contains("cleared arena") + ); + assert!(!executor.prepare(&catalog, &selected, 1).unwrap()); + } + + #[test] + fn wrong_output_shape_fails_before_rows_are_exposed() { + let catalog = direct_catalog(); + let resolver = ProbeResolver::new(catalog.clone()); + let (trace, projection) = + OnTheFlyStructuralTraceV1::test_query_family_trace(0x81, factor(1)); + let selected = [QueryFamilyTraceInput { + trace: &trace, + projection, + }]; + let mut executor = OnTheFlyQueryFamilyExecutorV1::new(resolver); + executor.prepare(&catalog, &selected, 1).unwrap(); + let error = executor + .execute_into(&one_point_momenta(2.0, 3.0), 1, &mut []) + .unwrap_err(); + assert!(error.to_string().contains("expected 1")); + assert!(executor.resolver().state.calls.borrow().is_empty()); + assert!(executor.pending.is_none()); + + let mut output = [(0.0, 0.0)]; + assert!(!executor.prepare(&catalog, &selected, 1).unwrap()); + executor + .execute_into(&one_point_momenta(2.0, 3.0), 1, &mut output) + .unwrap(); + assert_eq!(output, [(46.0, 0.0)]); + } + + #[test] + fn failed_candidate_prepare_keeps_last_valid_family_and_handle() { + let catalog = direct_catalog(); + let resolver = ProbeResolver::new(catalog.clone()); + let (first, first_projection) = + OnTheFlyStructuralTraceV1::test_query_family_trace(0x81, factor(1)); + let first_selected = [QueryFamilyTraceInput { + trace: &first, + projection: first_projection, + }]; + let mut executor = OnTheFlyQueryFamilyExecutorV1::new(resolver); + let mut output = [(0.0, 0.0)]; + executor.prepare(&catalog, &first_selected, 1).unwrap(); + executor + .execute_into(&one_point_momenta(2.0, 3.0), 1, &mut output) + .unwrap(); + let first_handle = executor.active_retained_handle().unwrap(); + + let (mut second, second_projection) = + OnTheFlyStructuralTraceV1::test_query_family_trace(0x82, factor(1)); + second.seed_digest = SemanticDigest::new([0x83; 32]).unwrap(); + let error = executor + .prepare( + &catalog, + &[ + QueryFamilyTraceInput { + trace: &first, + projection: first_projection, + }, + QueryFamilyTraceInput { + trace: &second, + projection: second_projection, + }, + ], + 1, + ) + .unwrap_err(); + assert!(error.to_string().contains("one compact source frame")); + assert!(executor.pending.is_none()); + assert_eq!(executor.retained_family_count(), 1); + assert!(executor.activate_retained_family(first_handle, 1).unwrap()); + let report = executor + .execute_into(&one_point_momenta(2.0, 3.0), 1, &mut output) + .unwrap(); + assert!(report.cache_hit); + assert_eq!(output, [(46.0, 0.0)]); + } + + #[test] + fn failed_streamed_candidate_prepare_keeps_last_valid_family_and_handle() { + let catalog = direct_catalog(); + let resolver = ProbeResolver::new(catalog.clone()); + let (first, first_projection) = + OnTheFlyStructuralTraceV1::test_query_family_trace(0x81, factor(1)); + let first_selected = [QueryFamilyTraceInput { + trace: &first, + projection: first_projection, + }]; + let mut executor = OnTheFlyQueryFamilyExecutorV1::new(resolver); + let mut output = [(0.0, 0.0)]; + executor.prepare(&catalog, &first_selected, 1).unwrap(); + executor + .execute_into(&one_point_momenta(2.0, 3.0), 1, &mut output) + .unwrap(); + let first_handle = executor.active_retained_handle().unwrap(); + + let (mut second, second_projection) = + OnTheFlyStructuralTraceV1::test_query_family_trace(0x82, factor(1)); + second.seed_digest = SemanticDigest::new([0x83; 32]).unwrap(); + let streamed = streamed_pair_candidate( + &catalog, + &first, + first_projection, + &second, + second_projection, + true, + ); + let error = executor + .prepare_streamed_candidate(streamed, 1) + .unwrap_err(); + assert!(error.to_string().contains("one compact source frame")); + assert!(executor.pending.is_none()); + assert_eq!(executor.retained_family_count(), 1); + assert!(executor.activate_retained_family(first_handle, 1).unwrap()); + let report = executor + .execute_into(&one_point_momenta(2.0, 3.0), 1, &mut output) + .unwrap(); + assert!(report.cache_hit); + assert_eq!(output, [(46.0, 0.0)]); + } +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/recurrence/on_the_fly/family/snapshot.rs b/pyamplicol/rust/crates/rusticol-core/src/recurrence/on_the_fly/family/snapshot.rs new file mode 100644 index 0000000000..e17254fabb --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/recurrence/on_the_fly/family/snapshot.rs @@ -0,0 +1,192 @@ +// SPDX-License-Identifier: 0BSD + +//! Portable rows only: no kernel pointers, descriptor caches, or numeric workspaces. + +use super::*; + +#[derive(bincode::Encode, bincode::Decode)] +pub(crate) struct OnTheFlyFamilySnapshotV1 { + strategy: u32, + census: OnTheFlyQueryFamilyCensusV1, + source_count: u32, + lorentz_component_count: u16, + parameter_count: u32, + current_component_count: u32, + amplitude_destination_count: u32, + momentum_forms: Box<[CanonicalMomentumLinearForm]>, + exact_factors: Box<[ExactComplexRational]>, + row_groups: Box<[OnTheFlyFamilyRowGroupV1]>, +} + +/// Same wire layout as the owned decoder, borrowing the potentially large rows. +#[derive(bincode::Encode)] +pub(crate) struct OnTheFlyFamilySnapshotRefV1<'a> { + strategy: u32, + census: OnTheFlyQueryFamilyCensusV1, + source_count: u32, + lorentz_component_count: u16, + parameter_count: u32, + current_component_count: u32, + amplitude_destination_count: u32, + momentum_forms: &'a [CanonicalMomentumLinearForm], + exact_factors: &'a [ExactComplexRational], + row_groups: &'a [OnTheFlyFamilyRowGroupV1], +} + +impl OnTheFlyFamilySnapshotV1 { + pub(crate) fn executor_keys(&self) -> impl Iterator + '_ { + self.row_groups.iter().flat_map(|group| { + std::iter::once(group.representative_key) + .chain(group.member_bindings.iter().map(|(key, _)| *key)) + }) + } + + pub(crate) const fn census(&self) -> OnTheFlyQueryFamilyCensusV1 { + self.census + } + + pub(crate) const fn amplitude_destination_count(&self) -> usize { + self.amplitude_destination_count as usize + } + + fn into_family(self) -> RusticolResult<(RecurrenceStrategy, OnTheFlyBuiltQueryFamilyV1)> { + let strategy = match self.strategy { + 0 => RecurrenceStrategy::TopologyReplay, + 2 => RecurrenceStrategy::ContractedColorUnion, + _ => return Err(integrity("saved OTF family has an unsupported strategy")), + }; + let family = OnTheFlyBuiltQueryFamilyV1 { + census: self.census, + source_count: self.source_count, + lorentz_component_count: self.lorentz_component_count, + parameter_count: self.parameter_count, + current_component_count: self.current_component_count, + amplitude_destination_count: self.amplitude_destination_count, + momentum_forms: self.momentum_forms, + exact_factors: self.exact_factors, + row_groups: self.row_groups, + #[cfg(any(test, feature = "on-the-fly-test-support"))] + observed_currents: Box::new([]), + }; + validate_ordered_family_schedule(&family.row_groups)?; + if family.census.source_frame_partition_count != 1 + || family.census.union_amplitude_destination_count != family.amplitude_destination_count + || family.census.union_unique_current_component_count != family.current_component_count + { + return Err(integrity( + "saved OTF family dimensions disagree with its census", + )); + } + Ok((strategy, family)) + } +} + +impl OnTheFlyQueryFamilyExecutorV1 { + pub(crate) fn snapshot(&self) -> RusticolResult>> { + if self.families.len() > 1 { + return Err(integrity( + "OTF family retention policy changed; update the cache format", + )); + } + Ok(self.families.first().map(|bound| { + let family = &bound.family; + OnTheFlyFamilySnapshotRefV1 { + strategy: bound.strategy.as_u32(), + census: family.census, + source_count: family.source_count, + lorentz_component_count: family.lorentz_component_count, + parameter_count: family.parameter_count, + current_component_count: family.current_component_count, + amplitude_destination_count: family.amplitude_destination_count, + momentum_forms: &family.momentum_forms, + exact_factors: &family.exact_factors, + row_groups: &family.row_groups, + } + })) + } + + /// All fallible allocation and binding happens before replacing the old + /// committed owner. No execution or numeric input is required to restore it. + pub(crate) fn restore_snapshot( + &mut self, + snapshot: OnTheFlyFamilySnapshotV1, + logical_point_capacity: u32, + ) -> RusticolResult { + let (strategy, family) = snapshot.into_family()?; + if family.parameter_count as usize != self.parameter_state.len() { + return Err(integrity( + "saved OTF parameter layout differs from the loaded model", + )); + } + let resolved_groups = self.bind_groups(&family)?; + let packed_singleton_capable = + Self::resolved_groups_are_packed_singleton_capable(&family, &resolved_groups); + let workspace = OnTheFlyFamilyWorkspaceV1::new( + &family, + logical_point_capacity, + packed_singleton_capable, + )?; + let interaction_program = + Self::bind_interaction_program(&family, &resolved_groups, strategy)?; + let singleton_fanout_program = + Self::bind_singleton_fanout_program(&family, &resolved_groups)?; + if self.families.capacity() == 0 { + self.families.try_reserve_exact(1).map_err(|error| { + invalid(format!("OTF restored-family allocation failed: {error}")) + })?; + } + let candidate = BoundOnTheFlyQueryFamilyV1 { + identity: QueryFamilyCacheIdentityV1::Restored, + strategy, + family, + workspace, + resolved_groups, + packed_singleton_capable, + interaction_program, + singleton_fanout_program, + applied_parameter_version: u64::MAX, + descriptor_exposed: false, + }; + self.invalidate_exposed_row_tables()?; + self.pending = Some(candidate); + if let Err(error) = self.commit_pending_family() { + // The fresh candidate has never exposed a row pointer. In + // particular, generation-counter exhaustion must not leave it + // selected after a failed restore of a previously warm runner. + self.pending = None; + return Err(error); + } + self.active_retained_handle() + .ok_or_else(|| integrity("restored OTF family has no handle")) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn snapshot_decode_keeps_canonical_type_invariants() { + let config = bincode::config::standard(); + for denominator in [0_i128, -1] { + let bytes = bincode::encode_to_vec((1_i128, denominator), config).unwrap(); + assert!( + bincode::decode_from_slice::(&bytes, config) + .is_err() + ); + } + let bytes = bincode::encode_to_vec([0_u8; 32], config).unwrap(); + assert!(bincode::decode_from_slice::(&bytes, config).is_err()); + let bytes = bincode::encode_to_vec( + vec![crate::recurrence::MomentumTerm { + source_slot: 0, + coefficient: 0, + }], + config, + ) + .unwrap(); + assert!( + bincode::decode_from_slice::(&bytes, config).is_err() + ); + } +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/recurrence/on_the_fly/interpreter.rs b/pyamplicol/rust/crates/rusticol-core/src/recurrence/on_the_fly/interpreter.rs new file mode 100644 index 0000000000..d174c18f87 --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/recurrence/on_the_fly/interpreter.rs @@ -0,0 +1,569 @@ +// SPDX-License-Identifier: 0BSD + +#![allow(dead_code)] // Query-local compatibility path; production uses persisted families. + +use super::*; +use crate::recurrence::{ + DirectResolvedSourceSelection, DirectSourceDispatchVariantDescriptor, DirectSourceEmbeddingRow, +}; + +/// Borrowed authenticated tables used to cold-bind one selected-helicity +/// source program. +/// +/// The direct plan remains the serialized authority. A process-owned source +/// resolver consumes this view once when a persisted helicity family is +/// prepared and returns compact executable rows; no table from this view is +/// retained by the warm program. +#[derive(Clone, Copy)] +pub(crate) struct OnTheFlyUnionSourceBindingView<'a> { + pub(crate) sources: &'a [DirectSourceRow], + pub(crate) variants: &'a [DirectSourceDispatchVariantDescriptor], + pub(crate) embeddings: &'a [DirectSourceEmbeddingRow], + pub(crate) selections: &'a [DirectResolvedSourceSelection], + pub(crate) exact_factors: &'a [ExactComplexRational], + pub(crate) current_component_count: u32, + pub(crate) momentum_form_count: u32, + pub(crate) parameter_count: u32, +} + +/// Opaque, family-local source program produced by one cold authenticated +/// binding. Implementations own every template and embedding descriptor they +/// need, so execution neither borrows the direct plan nor searches a source +/// domain. +pub(crate) trait OnTheFlyBoundUnionSourceProgram { + /// Execute against views already authenticated by `validate_direct_views`. + /// + /// # Safety + /// + /// The caller must provide live, nonaliasing Direct-Arena views whose + /// declared shapes match the plan used for cold binding. + unsafe fn execute( + &self, + arena: DirectArenaView, + momenta: DirectMomentumView, + parameters: DirectParameterView, + point_count: u32, + ) -> RusticolResult<()>; +} + +/// Resolved prepared executor borrowed from a lane-owned pool/binding. +#[derive(Clone, Copy)] +pub(crate) struct ResolvedOnTheFlyExecutor { + pub(crate) direct_executor_id: u32, + pub(crate) handle: DirectExecutorHandle, + pub(crate) parent_permutation: [u8; 2], + /// Exact process-bound proof that this prepared callback accepts a + /// singleton arena whose physical point stride is one. Query families + /// use the packed layout only when every resolved row-group member + /// carries this capability. + pub(crate) packed_singleton_capable: bool, + /// Authenticated intrinsic-only native contribution capability used by + /// fanout and interaction rewrites. SymJIT, compiled-native, source, + /// finalization, and closure bindings keep None. + pub(crate) interaction_capability: Option, +} + +/// Semantic resolver implemented by the lane-owned prepared executor pool. +pub(crate) trait OnTheFlyPreparedExecutorResolver { + fn resolve(&self, key: OnTheFlyExecutorKeyV1) -> RusticolResult; + + /// Resolve an executor already authenticated by a persisted direct plan. + /// Query-local traces use [`Self::resolve`]; persisted helicity families + /// need no second semantic-key catalog because the plan layout owns the + /// dense executor IDs and row parent order. + fn resolve_direct_executor( + &self, + _direct_executor_id: u32, + _role: DirectExecutorRole, + ) -> RusticolResult { + Err(integrity( + "prepared executor resolver does not support persisted direct-plan IDs", + )) + } + + /// Runtime-helicity source dispatcher owned beside the prepared pool. + fn union_source_dispatch(&self) -> RusticolResult { + Err(integrity( + "prepared executor resolver has no all-flow-union source dispatcher", + )) + } + + /// Cold-bind one selected all-flow source schedule. The default keeps + /// query-only resolvers deliberately incapable of executing a persisted + /// selector family. + fn bind_union_source_program( + &self, + _view: OnTheFlyUnionSourceBindingView<'_>, + ) -> RusticolResult> { + Err(integrity( + "prepared executor resolver cannot cold-bind all-flow-union sources", + )) + } + + fn union_source_packed_singleton_capable(&self) -> bool { + false + } + + /// Invalidate any prepared-context caches that borrow row or arena pointers. + /// Native/Rust callbacks retain no such pointers and need no work here. + fn invalidate_row_tables(&self) -> RusticolResult<()>; +} + +fn checked_scalar_len(planes: u32, stride: u32, label: &str) -> RusticolResult { + usize::try_from(planes) + .ok() + .and_then(|planes| planes.checked_mul(stride as usize)) + .ok_or_else(|| invalid(format!("{label} scalar length exceeds usize"))) +} + +fn exact_factor_parts(value: ExactComplexRational) -> RusticolResult<(f64, f64)> { + let real = value.real().numerator() as f64 / value.real().denominator() as f64; + let imag = value.imag().numerator() as f64 / value.imag().denominator() as f64; + if !real.is_finite() || !imag.is_finite() { + return Err(invalid( + "exact factor cannot be represented as finite binary64", + )); + } + Ok((real, imag)) +} + +/// Owned aligned numeric state for one on-the-fly lane. Structural traces do +/// not own this storage and parameter mutation never rebuilds them. +pub(crate) struct OnTheFlyWorkspaceV1 { + current_re: AlignedF64Buffer, + current_im: AlignedF64Buffer, + amplitude_re: AlignedF64Buffer, + amplitude_im: AlignedF64Buffer, + momenta: AlignedF64Buffer, + parameters_re: AlignedF64Buffer, + parameters_im: AlignedF64Buffer, + factors_re: AlignedF64Buffer, + factors_im: AlignedF64Buffer, + trace_digest: SemanticDigest, + source_count: u32, + logical_point_capacity: u32, + active_point_count: u32, + momentum_form_count: u32, + lorentz_component_count: u16, + point_stride: u32, +} + +impl OnTheFlyWorkspaceV1 { + pub(crate) fn new( + trace: &OnTheFlyStructuralTraceV1, + logical_point_capacity: u32, + ) -> RusticolResult { + let layout = trace.layout; + if layout.source_count == 0 + || logical_point_capacity == 0 + || layout.lorentz_component_count == 0 + { + return Err(integrity( + "trace has an empty authenticated workspace shape", + )); + } + if trace.layout.momentum_form_count == 0 + || trace.layout.exact_factor_count as usize != trace.exact_factors.len() + { + return Err(integrity("trace workspace layout is inconsistent")); + } + let point_stride = checked_aligned_point_stride(logical_point_capacity)?; + let current_len = checked_scalar_len( + trace.layout.current_component_count, + point_stride, + "current arena", + )?; + let amplitude_len = checked_scalar_len( + trace.layout.amplitude_component_count, + point_stride, + "amplitude arena", + )?; + let momentum_planes = trace + .layout + .momentum_form_count + .checked_mul(u32::from(layout.lorentz_component_count)) + .ok_or_else(|| invalid("momentum plane count exceeds u32"))?; + let momentum_len = checked_scalar_len(momentum_planes, point_stride, "momentum arena")?; + let factor_count = trace.exact_factors.len(); + let mut factors_re = AlignedF64Buffer::zeroed(factor_count, "on-the-fly factor real")?; + let mut factors_im = AlignedF64Buffer::zeroed(factor_count, "on-the-fly factor imaginary")?; + for (index, factor) in trace.exact_factors.iter().copied().enumerate() { + let (real, imag) = exact_factor_parts(factor)?; + factors_re.as_mut_slice()[index] = real; + factors_im.as_mut_slice()[index] = imag; + } + Ok(Self { + current_re: AlignedF64Buffer::zeroed(current_len, "on-the-fly current real")?, + current_im: AlignedF64Buffer::zeroed(current_len, "on-the-fly current imaginary")?, + amplitude_re: AlignedF64Buffer::zeroed(amplitude_len, "on-the-fly amplitude real")?, + amplitude_im: AlignedF64Buffer::zeroed( + amplitude_len, + "on-the-fly amplitude imaginary", + )?, + momenta: AlignedF64Buffer::zeroed(momentum_len, "on-the-fly momenta")?, + parameters_re: AlignedF64Buffer::zeroed( + usize::try_from(layout.parameter_count) + .map_err(|_| invalid("parameter count exceeds usize"))?, + "on-the-fly parameter real", + )?, + parameters_im: AlignedF64Buffer::zeroed( + usize::try_from(layout.parameter_count) + .map_err(|_| invalid("parameter count exceeds usize"))?, + "on-the-fly parameter imaginary", + )?, + factors_re, + factors_im, + trace_digest: trace.semantic_digest(), + source_count: layout.source_count, + logical_point_capacity, + active_point_count: 0, + momentum_form_count: trace.layout.momentum_form_count, + lorentz_component_count: layout.lorentz_component_count, + point_stride, + }) + } + + pub(crate) const fn point_stride(&self) -> u32 { + self.point_stride + } + + pub(crate) fn set_parameter( + &mut self, + parameter_id: u32, + real: f64, + imag: f64, + ) -> RusticolResult<()> { + self.active_point_count = 0; + if !real.is_finite() || !imag.is_finite() { + return Err(invalid("runtime parameter value must be finite")); + } + let index = parameter_id as usize; + *self + .parameters_re + .as_mut_slice() + .get_mut(index) + .ok_or_else(|| invalid("runtime parameter ID is out of bounds"))? = real; + *self + .parameters_im + .as_mut_slice() + .get_mut(index) + .ok_or_else(|| invalid("runtime parameter ID is out of bounds"))? = imag; + Ok(()) + } + + pub(crate) fn set_momentum_value( + &mut self, + momentum_form_id: u32, + lorentz_component: u16, + point_index: u32, + value: f64, + ) -> RusticolResult<()> { + self.active_point_count = 0; + if !value.is_finite() + || momentum_form_id >= self.momentum_form_count + || lorentz_component >= self.lorentz_component_count + || point_index >= self.logical_point_capacity + { + return Err(invalid("momentum workspace coordinate or value is invalid")); + } + let plane = usize::try_from(momentum_form_id) + .ok() + .and_then(|form| form.checked_mul(usize::from(self.lorentz_component_count))) + .and_then(|base| base.checked_add(usize::from(lorentz_component))) + .ok_or_else(|| invalid("momentum plane index exceeds usize"))?; + let index = plane + .checked_mul(self.point_stride as usize) + .and_then(|base| base.checked_add(point_index as usize)) + .ok_or_else(|| invalid("momentum scalar index exceeds usize"))?; + self.momenta.as_mut_slice()[index] = value; + Ok(()) + } + + pub(crate) fn fill_momenta_from_external( + &mut self, + trace: &OnTheFlyStructuralTraceV1, + external_momenta: &[f64], + point_count: u32, + ) -> RusticolResult<()> { + self.active_point_count = 0; + if self.trace_digest != trace.semantic_digest() { + return Err(integrity( + "workspace belongs to a different structural trace", + )); + } + if point_count == 0 || point_count > self.logical_point_capacity { + return Err(invalid("active point count is outside the workspace")); + } + let expected = usize::try_from(self.source_count) + .ok() + .and_then(|sources| sources.checked_mul(usize::from(self.lorentz_component_count))) + .and_then(|planes| planes.checked_mul(point_count as usize)) + .ok_or_else(|| invalid("external momentum shape exceeds usize"))?; + if external_momenta.len() != expected { + return Err(invalid(format!( + "external momentum input has {} scalars, expected {expected}", + external_momenta.len() + ))); + } + for (form_id, form) in trace.momentum_forms.iter().enumerate() { + for lorentz in 0..usize::from(self.lorentz_component_count) { + for point in 0..point_count as usize { + let mut value = 0.0; + for term in form.terms() { + if term.source_slot >= self.source_count { + return Err(integrity("momentum form source slot is out of bounds")); + } + let input_index = (term.source_slot as usize + * usize::from(self.lorentz_component_count) + + lorentz) + * point_count as usize + + point; + value += f64::from(term.coefficient) * external_momenta[input_index]; + } + self.set_momentum_value(form_id as u32, lorentz as u16, point as u32, value)?; + } + } + } + Ok(()) + } + + pub(crate) fn amplitude(&self, point_index: u32) -> RusticolResult<(f64, f64)> { + if point_index >= self.active_point_count { + return Err(invalid( + "amplitude point is outside the last successful execution", + )); + } + let index = point_index as usize; + Ok(( + self.amplitude_re.as_slice()[index], + self.amplitude_im.as_slice()[index], + )) + } + + fn clear_active(&mut self, point_count: u32) -> RusticolResult<()> { + if point_count == 0 || point_count > self.logical_point_capacity { + return Err(invalid("active point count is outside the workspace")); + } + for (real, imag) in [ + (&mut self.current_re, &mut self.current_im), + (&mut self.amplitude_re, &mut self.amplitude_im), + ] { + for plane in 0..real.len() / self.point_stride as usize { + let start = plane * self.point_stride as usize; + let end = start + point_count as usize; + real.as_mut_slice()[start..end].fill(0.0); + imag.as_mut_slice()[start..end].fill(0.0); + } + } + Ok(()) + } + + fn raw_views( + &mut self, + ) -> RusticolResult<( + DirectArenaView, + DirectMomentumView, + DirectParameterView, + DirectFactorView, + )> { + let arena = DirectArenaView { + current_re: self.current_re.as_mut_ptr(), + current_im: self.current_im.as_mut_ptr(), + current_scalar_len: self.current_re.len() as u64, + amplitude_re: self.amplitude_re.as_mut_ptr(), + amplitude_im: self.amplitude_im.as_mut_ptr(), + amplitude_scalar_len: self.amplitude_re.len() as u64, + point_stride: self.point_stride, + }; + let momenta = DirectMomentumView { + values: self.momenta.as_ptr(), + scalar_len: self.momenta.len() as u64, + form_count: self.momentum_form_count, + lorentz_component_count: self.lorentz_component_count, + point_stride: self.point_stride, + }; + let parameters = DirectParameterView { + values_re: self.parameters_re.as_ptr(), + values_im: self.parameters_im.as_ptr(), + value_count: self.parameters_re.len() as u32, + }; + let factors = DirectFactorView { + values_re: self.factors_re.as_ptr(), + values_im: self.factors_im.as_ptr(), + value_count: self.factors_re.len() as u32, + }; + validate_direct_views(arena, momenta, parameters, factors)?; + Ok((arena, momenta, parameters, factors)) + } +} + +fn validate_resolved_executor( + key: OnTheFlyExecutorKeyV1, + resolved: ResolvedOnTheFlyExecutor, +) -> RusticolResult<()> { + if resolved.handle.role() != key.role() { + return Err(integrity( + "resolved executor role differs from its semantic key", + )); + } + match key.role() { + DirectExecutorRole::Contribution => match resolved.parent_permutation { + [0, 1] | [1, 0] => {} + _ => { + return Err(integrity( + "resolved binary executor has an invalid permutation", + )); + } + }, + DirectExecutorRole::Source + | DirectExecutorRole::Finalization + | DirectExecutorRole::Closure => { + if resolved.parent_permutation != [0, 1] { + return Err(integrity( + "unary executor unexpectedly permutes parent rows", + )); + } + } + } + Ok(()) +} + +/// Direct structural interpreter. It resolves every operation by semantic +/// identity and invokes genuine prepared Direct-Arena kernels without a plan. +pub(crate) struct OnTheFlyStructuralInterpreter; + +impl OnTheFlyStructuralInterpreter { + pub(crate) fn execute( + trace: &OnTheFlyStructuralTraceV1, + resolver: &impl OnTheFlyPreparedExecutorResolver, + workspace: &mut OnTheFlyWorkspaceV1, + point_count: u32, + ) -> RusticolResult<()> { + if !trace.prepared_executor_rows_bound() { + return Err(integrity( + "prepared executor parent order is not bound into stable trace rows", + )); + } + if workspace.trace_digest != trace.semantic_digest() { + return Err(integrity( + "workspace belongs to a different structural trace", + )); + } + workspace.active_point_count = 0; + workspace.clear_active(point_count)?; + let (arena, momenta, parameters, factors) = workspace.raw_views()?; + for operation in trace.operations.iter() { + let key = operation.key(); + let resolved = resolver.resolve(key)?; + validate_resolved_executor(key, resolved)?; + clear_direct_executor_error_detail(); + let status: c_int = unsafe { + match (operation, resolved.handle) { + ( + OnTheFlyTraceOperationV1::Source { row, .. }, + DirectExecutorHandle::Source { call, context }, + ) => call( + context, + arena, + momenta, + parameters, + factors, + row, + 1, + point_count, + ), + ( + OnTheFlyTraceOperationV1::Contribution { row, .. }, + DirectExecutorHandle::Contribution { call, context }, + ) => call( + context, + arena, + momenta, + parameters, + factors, + row, + 1, + point_count, + ), + ( + OnTheFlyTraceOperationV1::Finalization { row, .. }, + DirectExecutorHandle::Finalization { call, context }, + ) => call( + context, + arena, + momenta, + parameters, + factors, + row, + 1, + point_count, + ), + ( + OnTheFlyTraceOperationV1::Closure { row, .. }, + DirectExecutorHandle::Closure { call, context }, + ) => call( + context, + arena, + momenta, + parameters, + factors, + row, + 1, + point_count, + ), + _ => { + return Err(integrity( + "resolved executor handle variant differs from trace operation", + )); + } + } + }; + crate::recurrence::direct_backend::check_status( + key.role(), + resolved.direct_executor_id, + status, + )?; + } + workspace.active_point_count = point_count; + Ok(()) + } +} + +#[cfg(any(test, feature = "on-the-fly-test-support"))] +impl OnTheFlyWorkspaceV1 { + pub(crate) fn observed_current_components( + &self, + trace: &OnTheFlyStructuralTraceV1, + current_id: u32, + point_index: u32, + ) -> RusticolResult> { + if self.trace_digest != trace.semantic_digest() || point_index >= self.active_point_count { + return Err(invalid( + "current observation does not match the last successful trace execution", + )); + } + let [component_base, component_count] = trace.current_component_range(current_id)?; + (0..component_count) + .map(|component_offset| { + let plane = component_base + .checked_add(component_offset) + .ok_or_else(|| integrity("current observation plane overflows"))?; + let index = usize::try_from(plane) + .ok() + .and_then(|plane| plane.checked_mul(self.point_stride as usize)) + .and_then(|base| base.checked_add(point_index as usize)) + .ok_or_else(|| integrity("current observation index exceeds usize"))?; + Ok(( + *self + .current_re + .as_slice() + .get(index) + .ok_or_else(|| integrity("current observation real value is absent"))?, + *self.current_im.as_slice().get(index).ok_or_else(|| { + integrity("current observation imaginary value is absent") + })?, + )) + }) + .collect() + } +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/recurrence/on_the_fly/mod.rs b/pyamplicol/rust/crates/rusticol-core/src/recurrence/on_the_fly/mod.rs new file mode 100644 index 0000000000..6eb8d95519 --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/recurrence/on_the_fly/mod.rs @@ -0,0 +1,791 @@ +// SPDX-License-Identifier: 0BSD + +//! Private query-local LC recurrence construction and direct interpretation. +//! +//! This module deliberately consumes one decoded physical LC selector. It +//! never constructs a process-wide color plan, selector projection, replay +//! catalog, feasibility index, or [`super::DirectRecurrencePlan`]. + +use std::collections::{BTreeMap, BTreeSet, VecDeque}; +use std::ffi::c_int; + +use rayon::prelude::*; +use sha2::{Digest, Sha256}; + +use super::construct::{ + TemplateCatalog, aggregate_factor, combined_coupling_orders, merged_helicity_identity, + merged_momentum, multiply_factors, output_factor_from_binding, quantum_parent_spin_matches, + validate_crossed_source_state, +}; +use super::contact_orbit_owner::{ + PreparedContactOrbitTransition, prepare_contact_orbit_transition, + selected_contact_orbit_owner_tokens, +}; +use super::direct_backend::{ + DirectExecutorHandle, DirectInteractionExecutorCapability, DirectInteractionGroupAction, + DirectInteractionGroupView, DirectInteractionProgram, DirectInteractionScheduleView, + DirectSingletonContributionFanoutProgram, DirectUnionSourceDispatchHandle, + clear_direct_executor_error_detail, +}; +use super::direct_plan::{ + DIRECT_CONTRIBUTION_FLAG_INITIALIZE_DESTINATION, DirectClosureRow, DirectContributionRow, + DirectExecutorRole, DirectFinalizationRow, DirectSourceRow, +}; +use super::process::ProcessSourceStateRow; +use super::template::{ + ClosureRow, CurrentStateRow, EvaluatorContractKind, LCColorTransitionWitnessRow, + QuantumFlowRow, SourceRow, TransitionRow, ValidatedRecurrenceTemplateInput, +}; +use super::{ + CanonicalMomentumLinearForm, ContributionKey, CurrentCoreKey, CurrentHelicityIdentity, + CurrentSourceBinding, DynamicLCColorState, DynamicLCColorStateInterner, ExactComplexRational, + LCColorComponent, LCColorComponentKind, LCColorSourceSeed, LCColorWitnessTermId, MomentumTerm, + PreparedDirectExecutorBinding, PreparedDirectExecutorCatalog, RecurrenceNodeKind, + RecurrenceStrategy, SemanticDigest, SourceStateAssignment, +}; +use crate::direct_arena::{ + AlignedF64Buffer, DirectArenaView, DirectFactorView, DirectMomentumView, DirectParameterView, + checked_aligned_point_stride, validate_direct_views, +}; +use crate::{RusticolError, RusticolResult}; + +const MISSING_U32: u32 = u32::MAX; +const ON_THE_FLY_PROOF_DOMAIN: &[u8] = b"pyamplicol-on-the-fly-structural-proof-v1\0"; + +fn invalid(message: impl Into) -> RusticolError { + RusticolError::invalid_argument(format!("on-the-fly recurrence: {}", message.into())) +} + +fn integrity(message: impl Into) -> RusticolError { + RusticolError::integrity(format!("on-the-fly recurrence: {}", message.into())) +} + +#[allow(dead_code)] // Query-local compatibility and probe error constructor. +fn evaluation(message: impl Into) -> RusticolError { + RusticolError::evaluation(format!("on-the-fly recurrence: {}", message.into())) +} + +fn checked_u32(value: usize, label: &str) -> RusticolResult { + u32::try_from(value).map_err(|_| invalid(format!("{label} exceeds u32"))) +} + +fn hash_len(hash: &mut Sha256, value: usize, label: &str) -> RusticolResult<()> { + hash.update( + u64::try_from(value) + .map_err(|_| invalid(format!("{label} length exceeds u64")))? + .to_le_bytes(), + ); + Ok(()) +} + +fn hash_digest(hash: &mut Sha256, digest: SemanticDigest) { + hash.update(digest.as_bytes()); +} + +fn hash_exact(hash: &mut Sha256, value: ExactComplexRational) { + for part in [value.real(), value.imag()] { + hash.update(part.numerator().to_le_bytes()); + hash.update(part.denominator().to_le_bytes()); + } +} + +fn final_digest(hash: Sha256) -> RusticolResult { + SemanticDigest::new(hash.finalize().into()) +} + +mod coupling_policy; +mod family; +mod interpreter; +mod persisted; +#[cfg(any(test, feature = "on-the-fly-test-support"))] +mod probe_guard; +mod projection; +mod public_query; +pub(crate) mod seed_codec; +mod source_builder; +mod source_seed; +mod sweep; +mod templates; +#[cfg(any(test, feature = "on-the-fly-test-support"))] +mod test_support; +mod trace; + +#[cfg(any(test, feature = "on-the-fly-test-support"))] +pub(crate) use coupling_policy::OnTheFlyCouplingPolicyCensusV1; +pub(crate) use coupling_policy::OnTheFlyResolvedCouplingPolicyV1; +pub use family::OnTheFlyQueryFamilyCensusV1; +#[cfg(feature = "on-the-fly-test-support")] +pub use family::on_the_fly_query_family_census_v1; +pub(crate) use family::snapshot::{OnTheFlyFamilySnapshotRefV1, OnTheFlyFamilySnapshotV1}; +pub(crate) use family::{ + OnTheFlyQueryFamilyExecutionReportV1, OnTheFlyQueryFamilyExecutorV1, + OnTheFlyQueryFamilyHandleV1, QueryFamilyTraceInput, build_streamed_query_family_candidate_v1, +}; +#[allow(unused_imports)] // Query-local interpreter remains available to opt-in test support. +pub(crate) use interpreter::{ + OnTheFlyBoundUnionSourceProgram, OnTheFlyPreparedExecutorResolver, + OnTheFlyStructuralInterpreter, OnTheFlyUnionSourceBindingView, OnTheFlyWorkspaceV1, + ResolvedOnTheFlyExecutor, +}; +pub(crate) use persisted::{ + PersistedHelicityAmplitudeTileV1, PersistedHelicityFamilyCacheCensusV1, + PersistedHelicityFamilyExecutionReportV1, PersistedHelicityFamilyExecutorV1, + PersistedHelicityFamilyInspectionCensusV1, +}; +#[cfg(any(test, feature = "on-the-fly-test-support"))] +pub(crate) use probe_guard::OnTheFlyForbiddenWorkGuardV1; +#[cfg(feature = "on-the-fly-test-support")] +pub(crate) use probe_guard::{OnTheFlyForbiddenWorkV1, reject_forbidden_work_if_probed}; +use projection::*; +use public_query::OnTheFlyClosureAnchorPolicyV1; +pub(crate) use public_query::OnTheFlyLcSelectorV1; +pub(crate) use public_query::{DecodedLcQueryV1, OnTheFlySelectedSourceV1}; +pub(crate) use seed_codec::decode_on_the_fly_process_seed_v1; +#[allow(unused_imports)] // Generation-only entries share this runtime validation seam. +pub(crate) use source_builder::{ + build_on_the_fly_process_seed_v1, build_on_the_fly_process_seeds_v1, + parse_on_the_fly_process_seed_projection_v1, validate_on_the_fly_source_mass_bindings_v1, +}; +#[cfg(test)] +pub(crate) use source_seed::scalar_adapter_test_seed; +#[allow(unused_imports)] // Pairing records are consumed by nested generation/test modules. +pub(crate) use source_seed::{ + ON_THE_FLY_PROCESS_SEED_IDENTITY_ABI, OnTheFlyCouplingOrderPolicyV1, + OnTheFlyExternalColorRoleV1, OnTheFlyPairingClassV1, OnTheFlyPairingEndpointV1, + OnTheFlyProcessSeedIdentityV1, OnTheFlyProcessSeedV1, OnTheFlySourceAnchorV1, + OnTheFlySourceExecutionSpecV1, OnTheFlySourceOrientationV1, OnTheFlySourceStateV1, + OnTheFlySourceWavefunctionFamilyV1, +}; +use sweep::*; +pub(crate) use templates::PreparedOnTheFlyGrammarV1; +use templates::*; +#[cfg(feature = "on-the-fly-test-support")] +pub use test_support::{OnTheFlyTestSupportReportV1, on_the_fly_test_support_probe_v1}; +#[cfg(feature = "on-the-fly-test-support")] +pub(crate) use test_support::{ + build_on_the_fly_selected_trace_against_seed_v1, build_on_the_fly_selected_trace_v1, +}; +#[cfg(feature = "on-the-fly-test-support")] +pub(crate) use trace::ON_THE_FLY_WORK_CENSUS_BASIS_V1; +#[cfg(any(test, feature = "on-the-fly-test-support"))] +pub(crate) use trace::hash_current_key; +#[cfg(test)] +pub(crate) use trace::scalar_adapter_test_trace; +use trace::*; +pub(crate) use trace::{ + OnTheFlyExecutorKeyV1, OnTheFlyStructuralTraceV1, authenticated_prepared_executor_binding, +}; +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) struct OnTheFlyProjectionProbeV1 { + pub(crate) enabled: bool, + pub(crate) applied: bool, + pub(crate) pre: [u32; 3], + pub(crate) post: [u32; 3], +} + +/// One compact, selector-local LC trace ready for prepared-executor binding. +/// +/// This production seam owns no global recurrence builder or direct plan. Its +/// inputs are precisely the compact process seed, one already-decoded public +/// query, and the authenticated prepared template catalogs. +pub(crate) struct OnTheFlySelectedQueryTraceV1 { + pub(crate) query: DecodedLcQueryV1, + pub(crate) trace: OnTheFlyStructuralTraceV1, + pub(crate) projection: OnTheFlyProjectionProbeV1, +} + +#[allow(clippy::large_enum_variant)] // Cold-path trace ownership avoids an extra allocation. +pub(crate) enum OnTheFlySelectedQueryOutcomeV1 { + Trace(OnTheFlySelectedQueryTraceV1), + StructuralZero { query: DecodedLcQueryV1 }, +} + +#[allow(dead_code)] // Compatibility entry point for the query-local probe path. +pub(crate) fn build_selected_lc_query_trace_v1( + templates: &ValidatedRecurrenceTemplateInput, + direct: &PreparedDirectExecutorCatalog, + seed: &OnTheFlyProcessSeedV1, + coupling_policy: &OnTheFlyResolvedCouplingPolicyV1, + query: DecodedLcQueryV1, +) -> RusticolResult { + let catalog = validate_seed_against_templates(templates, seed)?; + let grammar = prepare_on_the_fly_grammar_v1(templates, &catalog, seed)?; + let query = grammar.canonicalize_query(seed, query)?; + build_selected_lc_query_trace_impl( + templates, + direct, + seed, + coupling_policy, + &grammar, + query, + true, + false, + ) +} + +/// Prepare the immutable process grammar and process-global coupling policy +/// in one pass over the validated template catalog. Runtime load deliberately +/// does not call this; the first genuinely cold query family does. +pub(crate) fn prepare_on_the_fly_process_v1( + templates: &ValidatedRecurrenceTemplateInput, + seed: &OnTheFlyProcessSeedV1, +) -> RusticolResult<(PreparedOnTheFlyGrammarV1, OnTheFlyResolvedCouplingPolicyV1)> { + let catalog = validate_seed_against_templates(templates, seed)?; + let grammar = prepare_on_the_fly_grammar_v1(templates, &catalog, seed)?; + let policy = coupling_policy::resolve_on_the_fly_coupling_policy_from_grammar_v1( + templates, seed, &grammar, + )?; + Ok((grammar, policy)) +} + +/// Recreate model-only grammar while retaining the saved process-global +/// coupling decision; in particular, do not repeat the topology sweep. +pub(crate) fn restore_on_the_fly_grammar_v1( + templates: &ValidatedRecurrenceTemplateInput, + seed: &OnTheFlyProcessSeedV1, + policy: &OnTheFlyResolvedCouplingPolicyV1, +) -> RusticolResult { + if policy.seed_digest() != seed.semantic_digest() + || policy.effective_limits().len() != seed.explicit_coupling_limits().len() + { + return Err(integrity( + "saved on-the-fly coupling policy belongs to a different process", + )); + } + let catalog = validate_seed_against_templates(templates, seed)?; + prepare_on_the_fly_grammar_v1(templates, &catalog, seed) +} + +fn resolve_indexed_query_results(outcomes: Vec>) -> RusticolResult> { + outcomes.into_iter().collect() +} + +/// One cold, bounded construction pool reused across every streamed query +/// chunk. Keeping the pool outside the chunk loop avoids creating O(H x S) +/// thread pools while retaining deterministic indexed outcomes. +pub(crate) struct OnTheFlyQueryConstructionPoolV1 { + pool: Option, + thread_count: usize, +} + +impl OnTheFlyQueryConstructionPoolV1 { + pub(crate) fn new(thread_count: usize) -> RusticolResult { + if thread_count == 0 { + return Err(invalid( + "on-the-fly query construction thread count must be positive", + )); + } + let pool = (thread_count > 1) + .then(|| { + rayon::ThreadPoolBuilder::new() + .num_threads(thread_count) + .build() + .map_err(|error| { + RusticolError::internal(format!( + "could not create bounded on-the-fly query construction pool: {error}" + )) + }) + }) + .transpose()?; + Ok(Self { pool, thread_count }) + } + + pub(crate) const fn chunk_capacity(&self) -> usize { + self.thread_count + } + + #[allow(clippy::too_many_arguments)] + pub(crate) fn build_chunk( + &self, + templates: &ValidatedRecurrenceTemplateInput, + direct: &PreparedDirectExecutorCatalog, + seed: &OnTheFlyProcessSeedV1, + coupling_policy: &OnTheFlyResolvedCouplingPolicyV1, + grammar: &PreparedOnTheFlyGrammarV1, + enable_cyclic_trace_reflection: bool, + queries: Vec, + ) -> RusticolResult> { + if queries.len() > self.thread_count { + return Err(invalid( + "streamed on-the-fly query chunk exceeds its bounded construction pool", + )); + } + let build = |query| { + build_selected_lc_query_trace_impl( + templates, + direct, + seed, + coupling_policy, + grammar, + query, + true, + enable_cyclic_trace_reflection, + ) + }; + let outcomes = if let Some(pool) = &self.pool { + pool.install(|| queries.into_par_iter().map(build).collect::>()) + } else { + queries.into_iter().map(build).collect::>() + }; + resolve_indexed_query_results(outcomes) + } +} + +fn build_selected_lc_query_family_serial_v1( + templates: &ValidatedRecurrenceTemplateInput, + direct: &PreparedDirectExecutorCatalog, + seed: &OnTheFlyProcessSeedV1, + coupling_policy: &OnTheFlyResolvedCouplingPolicyV1, + grammar: &PreparedOnTheFlyGrammarV1, + enable_cyclic_trace_reflection: bool, + queries: impl IntoIterator, +) -> RusticolResult> { + queries + .into_iter() + .map(|query| { + build_selected_lc_query_trace_impl( + templates, + direct, + seed, + coupling_policy, + grammar, + query, + true, + enable_cyclic_trace_reflection, + ) + }) + .collect() +} + +/// Build one exact public query family while sharing all immutable grammar +/// preparation. Independent query-local sweeps run in parallel; their +/// outcomes and errors retain public selector order. +#[allow(clippy::too_many_arguments)] // Explicit cold-build context and execution policy. +pub(crate) fn build_selected_lc_query_family_v1( + templates: &ValidatedRecurrenceTemplateInput, + direct: &PreparedDirectExecutorCatalog, + seed: &OnTheFlyProcessSeedV1, + coupling_policy: &OnTheFlyResolvedCouplingPolicyV1, + grammar: &PreparedOnTheFlyGrammarV1, + enable_cyclic_trace_reflection: bool, + query_construction_threads: usize, + queries: impl IntoIterator, +) -> RusticolResult> { + if query_construction_threads == 0 { + return Err(invalid( + "on-the-fly query construction thread count must be positive", + )); + } + if query_construction_threads == 1 { + return build_selected_lc_query_family_serial_v1( + templates, + direct, + seed, + coupling_policy, + grammar, + enable_cyclic_trace_reflection, + queries, + ); + } + + let queries = queries.into_iter().collect::>(); + if queries.len() < 2 { + return build_selected_lc_query_family_serial_v1( + templates, + direct, + seed, + coupling_policy, + grammar, + enable_cyclic_trace_reflection, + queries, + ); + } + + let pool = rayon::ThreadPoolBuilder::new() + .num_threads(query_construction_threads.min(queries.len())) + .build() + .map_err(|error| { + RusticolError::internal(format!( + "could not create bounded on-the-fly query construction pool: {error}" + )) + })?; + let outcomes = pool.install(|| { + queries + .into_par_iter() + .map(|query| { + build_selected_lc_query_trace_impl( + templates, + direct, + seed, + coupling_policy, + grammar, + query, + true, + enable_cyclic_trace_reflection, + ) + }) + .collect::>() + }); + resolve_indexed_query_results(outcomes) +} + +#[cfg(any(test, feature = "on-the-fly-test-support"))] +#[allow(dead_code)] // Feature-only probe entry point. +pub(crate) fn build_selected_lc_query_trace_for_probe_v1( + templates: &ValidatedRecurrenceTemplateInput, + direct: &PreparedDirectExecutorCatalog, + seed: &OnTheFlyProcessSeedV1, + query: DecodedLcQueryV1, + enable_projection: bool, + enable_cyclic_trace_reflection: bool, +) -> RusticolResult { + let (grammar, coupling_policy) = prepare_on_the_fly_process_v1(templates, seed)?; + let query = grammar.canonicalize_query(seed, query)?; + match build_selected_lc_query_trace_impl( + templates, + direct, + seed, + &coupling_policy, + &grammar, + query, + enable_projection, + enable_cyclic_trace_reflection, + )? { + OnTheFlySelectedQueryOutcomeV1::Trace(selected) => Ok(selected), + OnTheFlySelectedQueryOutcomeV1::StructuralZero { .. } => Err(evaluation( + "selected probe query is an exact structural zero", + )), + } +} + +#[allow(clippy::too_many_arguments)] // Explicit cold-build context and probe switches. +fn build_selected_lc_query_trace_impl( + templates: &ValidatedRecurrenceTemplateInput, + direct: &PreparedDirectExecutorCatalog, + seed: &OnTheFlyProcessSeedV1, + coupling_policy: &OnTheFlyResolvedCouplingPolicyV1, + grammar: &PreparedOnTheFlyGrammarV1, + query: DecodedLcQueryV1, + enable_projection: bool, + enable_cyclic_trace_reflection: bool, +) -> RusticolResult { + if seed.direct_catalog_digest() != direct.direct_template_catalog_digest() { + return Err(integrity( + "compact query direct-template catalog differs from its prepared catalog", + )); + } + if coupling_policy.seed_digest() != seed.semantic_digest() + || coupling_policy.effective_limits().len() != seed.explicit_coupling_limits().len() + { + return Err(integrity( + "resolved coupling policy belongs to a different compact seed", + )); + } + grammar.authenticate_canonical_query(seed, &query)?; + let Some((trace, projection)) = build_selected_lc_trace_impl( + templates, + seed, + grammar, + coupling_policy.effective_limits(), + &query, + enable_projection, + enable_cyclic_trace_reflection, + true, + )? + else { + return Ok(OnTheFlySelectedQueryOutcomeV1::StructuralZero { query }); + }; + Ok(OnTheFlySelectedQueryOutcomeV1::Trace( + OnTheFlySelectedQueryTraceV1 { + query, + trace, + projection: projection + .ok_or_else(|| integrity("compact query projection proof was not collected"))?, + }, + )) +} + +fn selected_graph_counts( + currents: &[PendingCurrent], + closures: &[PendingClosure], + live: &BTreeSet, +) -> RusticolResult<[u32; 3]> { + let contributions = live.iter().try_fold(0usize, |total, id| { + let count = currents + .get(*id as usize) + .ok_or_else(|| integrity("selected graph current is absent"))? + .contributions + .values() + .filter(|factor| !factor.is_zero()) + .count(); + total + .checked_add(count) + .ok_or_else(|| invalid("selected graph contribution count exceeds usize")) + })?; + Ok([ + checked_u32(live.len(), "selected graph current count")?, + checked_u32(contributions, "selected graph contribution count")?, + checked_u32(closures.len(), "selected graph closure count")?, + ]) +} + +/// Build one selected LC structural trace without constructing any global +/// recurrence/process projection. +#[allow(dead_code)] // Compatibility entry point for the query-local probe path. +pub(crate) fn build_selected_lc_trace( + templates: &ValidatedRecurrenceTemplateInput, + seed: &OnTheFlyProcessSeedV1, + query: &DecodedLcQueryV1, +) -> RusticolResult { + let (grammar, coupling_policy) = prepare_on_the_fly_process_v1(templates, seed)?; + let query = grammar.canonicalize_query(seed, query.clone())?; + build_selected_lc_trace_impl( + templates, + seed, + &grammar, + coupling_policy.effective_limits(), + &query, + true, + false, + false, + )? + .map(|(trace, _)| trace) + .ok_or_else(|| evaluation("selected query is an exact structural zero")) +} + +#[cfg(any(test, feature = "on-the-fly-test-support"))] +#[allow(dead_code)] // Feature-only probe entry point. +pub(crate) fn build_selected_lc_trace_for_probe( + templates: &ValidatedRecurrenceTemplateInput, + seed: &OnTheFlyProcessSeedV1, + query: &DecodedLcQueryV1, + enable_projection: bool, +) -> RusticolResult<(OnTheFlyStructuralTraceV1, OnTheFlyProjectionProbeV1)> { + let (grammar, coupling_policy) = prepare_on_the_fly_process_v1(templates, seed)?; + let query = grammar.canonicalize_query(seed, query.clone())?; + let (trace, probe) = build_selected_lc_trace_impl( + templates, + seed, + &grammar, + coupling_policy.effective_limits(), + &query, + enable_projection, + false, + true, + )? + .ok_or_else(|| evaluation("selected probe query is an exact structural zero"))?; + Ok(( + trace, + probe.ok_or_else(|| integrity("projection probe was not collected"))?, + )) +} + +#[allow(clippy::too_many_arguments)] // Explicit cold-build context and probe switches. +fn build_selected_lc_trace_impl( + templates: &ValidatedRecurrenceTemplateInput, + seed: &OnTheFlyProcessSeedV1, + grammar: &PreparedOnTheFlyGrammarV1, + coupling_limits: &[Option], + query: &DecodedLcQueryV1, + enable_projection: bool, + enable_cyclic_trace_reflection: bool, + collect_projection_probe: bool, +) -> RusticolResult)>> { + if query.seed_digest != seed.semantic_digest() { + return Err(integrity( + "decoded query belongs to a different compact seed", + )); + } + let mut colors = DynamicLCColorStateInterner::default(); + let mut currents = Vec::new(); + let mut current_ids = BTreeMap::new(); + let enable_cyclic_trace_reflection = enable_cyclic_trace_reflection + && grammar + .closure_anchor_policy + .certifies_cyclic_trace_reflection(seed, query); + insert_selected_sources( + grammar, + seed, + coupling_limits, + query, + enable_cyclic_trace_reflection, + &mut colors, + &mut currents, + &mut current_ids, + )?; + build_forward_currents( + templates, + &grammar.transitions, + &grammar.transition_reflections, + &grammar.contact_orbits, + enable_cyclic_trace_reflection, + seed, + &grammar.fermion_ordering, + query.closure_anchor_slot, + coupling_limits, + &grammar.propagators, + &mut colors, + &mut currents, + &mut current_ids, + )?; + let constructed_contribution_count = currents + .iter() + .map(|current| current.contributions.len()) + .try_fold(0usize, |total, count| { + total + .checked_add(count) + .ok_or_else(|| invalid("constructed contribution count exceeds usize")) + })?; + let Some(selected_closures) = build_selected_closures( + templates, + &grammar.closures, + seed, + &grammar.fermion_ordering, + query, + enable_cyclic_trace_reflection, + &colors, + ¤ts, + )? + else { + return Ok(None); + }; + let live = live_current_ids(¤ts, &selected_closures)?; + let pre_counts = collect_projection_probe + .then(|| selected_graph_counts(¤ts, &selected_closures, &live)) + .transpose()?; + let projected = enable_projection + .then(|| project_query_local_color_aliases(¤ts, &selected_closures, &live)) + .transpose()? + .flatten(); + let projection_applied = projected.is_some(); + let (currents, selected_closures, live) = match projected { + Some(projected) => { + let live = (0..projected.currents.len()) + .map(|index| checked_u32(index, "projected query-local current ID")) + .collect::>>()?; + (projected.currents, projected.closures, live) + } + None => (currents, selected_closures, live), + }; + let post_counts = collect_projection_probe + .then(|| selected_graph_counts(¤ts, &selected_closures, &live)) + .transpose()?; + let pairing_owners = resolve_projected_pairing_owners(seed, &selected_closures)?; + let trace = lower_trace( + templates, + grammar, + seed, + query, + &colors, + ¤ts, + &selected_closures, + pairing_owners, + &live, + constructed_contribution_count, + )?; + Ok(Some(( + trace, + pre_counts + .zip(post_counts) + .map(|(pre, post)| OnTheFlyProjectionProbeV1 { + enabled: enable_projection, + applied: projection_applied, + pre, + post, + }), + ))) +} + +#[cfg(test)] +mod structural_zero_tests { + use super::*; + use crate::recurrence::validated_template_fixture; + + #[test] + fn indexed_query_results_keep_order_and_report_the_lowest_error() { + let pool = rayon::ThreadPoolBuilder::new() + .num_threads(4) + .build() + .unwrap(); + let ordered = pool.install(|| { + (0..32_u32) + .into_par_iter() + .map(Ok::<_, RusticolError>) + .collect::>() + }); + assert_eq!( + resolve_indexed_query_results(ordered).unwrap(), + (0..32_u32).collect::>() + ); + + let failures = pool.install(|| { + (0..32_u32) + .into_par_iter() + .map(|index| match index { + 5 | 19 => Err(invalid(format!("query index {index}"))), + _ => Ok(index), + }) + .collect::>() + }); + let error = resolve_indexed_query_results(failures).unwrap_err(); + assert!(error.to_string().contains("query index 5")); + } + + #[test] + fn selected_closure_domain_fails_closed_when_missing_or_inconsistent() { + let templates = validated_template_fixture(); + let summary = templates.summary(); + let seed = scalar_adapter_test_seed( + summary.compiled_model_digest, + summary.catalog_digest, + summary.prepared_kernel_pack_digest, + SemanticDigest::new([0x7d; 32]).unwrap(), + ) + .unwrap(); + let query = + DecodedLcQueryV1::new(&seed, vec![0, 1], &[0, 0], OnTheFlyLcSelectorV1::Singlet) + .unwrap(); + let catalog = TemplateCatalog::new(templates.input()).unwrap(); + let grammar = prepare_on_the_fly_grammar_v1(&templates, &catalog, &seed).unwrap(); + let mut colors = DynamicLCColorStateInterner::default(); + let mut currents = Vec::new(); + let mut current_ids = BTreeMap::new(); + insert_selected_sources( + &grammar, + &seed, + seed.explicit_coupling_limits(), + &query, + false, + &mut colors, + &mut currents, + &mut current_ids, + ) + .unwrap(); + + let absent = BTreeMap::new(); + let error = validate_prepared_closure_domain(&templates, &catalog, &absent).unwrap_err(); + assert!(error.to_string().contains("prepared closure domain")); + + let mut inconsistent = prepared_closures(&templates, &catalog).unwrap(); + let rows = inconsistent.values_mut().next().unwrap(); + rows[0].input_states = [4, 9]; + let error = + validate_prepared_closure_domain(&templates, &catalog, &inconsistent).unwrap_err(); + assert!(error.to_string().contains("parent-state pair")); + + currents.clear(); + let complete = prepared_closures(&templates, &catalog).unwrap(); + let error = build_selected_closures( + &templates, + &complete, + &seed, + &grammar.fermion_ordering, + &query, + false, + &colors, + ¤ts, + ) + .unwrap_err(); + assert!(error.to_string().contains("closure anchor")); + } +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/recurrence/on_the_fly/persisted.rs b/pyamplicol/rust/crates/rusticol-core/src/recurrence/on_the_fly/persisted.rs new file mode 100644 index 0000000000..27e8f826f6 --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/recurrence/on_the_fly/persisted.rs @@ -0,0 +1,2506 @@ +// SPDX-License-Identifier: 0BSD + +//! Cold-bound helicity families projected from one persisted all-flow plan. +//! +//! The sidecar's CSR is consulted only by [`PersistedHelicityFamilyExecutorV1::prepare`]. +//! Each retained helicity owns compact boxed non-source rows with authoritative +//! initialization flags and the same schedule-local singleton fanout and +//! interaction programs used by query-built families. Warm execution invokes +//! one cold-bound exact union-source program followed by those rows; it +//! performs no support-mask scan, selector branch, or source-catalog lookup. + +use super::*; +use crate::direct_arena::{ + AlignedF64Buffer, DirectArenaView, DirectFactorView, DirectMomentumView, DirectParameterView, + checked_aligned_point_stride, validate_direct_views, +}; +#[cfg(test)] +use crate::recurrence::direct_backend::DIRECT_STATUS_OK; +use crate::recurrence::direct_backend::{ + DirectWorkspace, check_status, execute_certified_reuse_rows, +}; +use crate::recurrence::{ + DIRECT_CONTRIBUTION_FLAG_CERTIFIED_REUSE, DIRECT_NONE_U32, DirectHelicityDispatch, + DirectRecurrencePlan, DirectResolvedHelicityDescriptor, DirectResolvedSourceSelection, + DirectSourceDispatchVariantDescriptor, DirectSourceEmbeddingRow, +}; + +#[derive(Debug, Eq, PartialEq)] +enum PersistedFamilyRowsV1 { + Contribution(Box<[DirectContributionRow]>), + Finalization(Box<[DirectFinalizationRow]>), + Closure(Box<[DirectClosureRow]>), +} + +impl PersistedFamilyRowsV1 { + fn len(&self) -> usize { + match self { + Self::Contribution(rows) => rows.len(), + Self::Finalization(rows) => rows.len(), + Self::Closure(rows) => rows.len(), + } + } +} + +enum PersistedFamilyRowsDraftV1 { + Contribution(Vec), + Finalization(Vec), + Closure(Vec), +} + +impl PersistedFamilyRowsDraftV1 { + const fn role(&self) -> DirectExecutorRole { + match self { + Self::Contribution(_) => DirectExecutorRole::Contribution, + Self::Finalization(_) => DirectExecutorRole::Finalization, + Self::Closure(_) => DirectExecutorRole::Closure, + } + } + + fn append_from_plan( + &mut self, + plan: &DirectRecurrencePlan, + role: DirectExecutorRole, + row_start: u64, + row_count: u32, + ) -> RusticolResult<()> { + let start = usize::try_from(row_start) + .map_err(|_| integrity("persisted family row start exceeds usize"))?; + let end = start + .checked_add(row_count as usize) + .ok_or_else(|| integrity("persisted family row range overflows usize"))?; + match (self, role) { + (Self::Contribution(destination), DirectExecutorRole::Contribution) => destination + .extend_from_slice( + plan.contributions().get(start..end).ok_or_else(|| { + integrity("persisted family contribution range is absent") + })?, + ), + (Self::Finalization(destination), DirectExecutorRole::Finalization) => destination + .extend_from_slice( + plan.finalizations().get(start..end).ok_or_else(|| { + integrity("persisted family finalization range is absent") + })?, + ), + (Self::Closure(destination), DirectExecutorRole::Closure) => destination + .extend_from_slice( + plan.closures() + .get(start..end) + .ok_or_else(|| integrity("persisted family closure range is absent"))?, + ), + (_, DirectExecutorRole::Source) => { + return Err(integrity( + "persisted family must not contain static source rows", + )); + } + _ => { + return Err(integrity( + "persisted family row storage differs from its role", + )); + } + } + Ok(()) + } + + fn freeze(self) -> PersistedFamilyRowsV1 { + match self { + Self::Contribution(rows) => { + PersistedFamilyRowsV1::Contribution(rows.into_boxed_slice()) + } + Self::Finalization(rows) => { + PersistedFamilyRowsV1::Finalization(rows.into_boxed_slice()) + } + Self::Closure(rows) => PersistedFamilyRowsV1::Closure(rows.into_boxed_slice()), + } + } +} + +struct PersistedFamilyGroupDraftV1 { + stage: u32, + direct_executor_id: u32, + rows: PersistedFamilyRowsDraftV1, +} + +impl PersistedFamilyGroupDraftV1 { + fn accepts(&self, stage: u32, role: DirectExecutorRole, direct_executor_id: u32) -> bool { + self.stage == stage + && self.rows.role() == role + && self.direct_executor_id == direct_executor_id + } +} + +struct PersistedFamilyGroupV1 { + stage: u32, + role: DirectExecutorRole, + direct_executor_id: u32, + rows: PersistedFamilyRowsV1, +} + +#[allow(dead_code)] // Retains authenticated helicity descriptors for inspection/probe builds. +struct PersistedPlanLayoutV1 { + runtime_layout_digest: SemanticDigest, + dispatch_address: usize, + source_count: u32, + lorentz_component_count: u16, + parameter_count: u32, + current_component_count: u32, + amplitude_destination_count: u32, + resolved_helicity_count: u32, + resolved_helicities: Box<[DirectResolvedHelicityDescriptor]>, + public_helicities: Box<[i32]>, + momentum_forms: Box<[CanonicalMomentumLinearForm]>, + exact_factors: Box<[ExactComplexRational]>, + sources: Box<[DirectSourceRow]>, + source_dispatch_variants: Box<[DirectSourceDispatchVariantDescriptor]>, + source_embeddings: Box<[DirectSourceEmbeddingRow]>, +} + +impl PersistedPlanLayoutV1 { + fn new( + plan: &DirectRecurrencePlan, + dispatch: &DirectHelicityDispatch, + lorentz_component_count: u16, + ) -> RusticolResult { + if lorentz_component_count == 0 { + return Err(invalid( + "persisted helicity family Lorentz component count is zero", + )); + } + let mut momentum_forms = Vec::with_capacity(plan.momentum_forms().len()); + for descriptor in plan.momentum_forms() { + let start = usize::try_from(descriptor.term_start) + .map_err(|_| integrity("persisted momentum-form start exceeds usize"))?; + let end = start + .checked_add(descriptor.term_count as usize) + .ok_or_else(|| integrity("persisted momentum-form range overflows usize"))?; + let terms = plan + .momentum_terms() + .get(start..end) + .ok_or_else(|| integrity("persisted momentum-form term range is out of bounds"))?; + momentum_forms.push(CanonicalMomentumLinearForm::new( + terms + .iter() + .map(|term| MomentumTerm { + source_slot: term.source_slot, + coefficient: term.coefficient, + }) + .collect(), + )?); + } + Ok(Self { + runtime_layout_digest: plan.runtime_layout_digest(), + dispatch_address: std::ptr::from_ref(dispatch).addr(), + source_count: plan.external_source_count(), + lorentz_component_count, + parameter_count: plan.parameter_value_count(), + current_component_count: plan.current_arena_components(), + amplitude_destination_count: plan.amplitude_destination_count(), + resolved_helicity_count: u32::try_from(plan.resolved_helicities().len()) + .map_err(|_| integrity("persisted resolved-helicity count exceeds u32"))?, + resolved_helicities: plan.resolved_helicities().to_vec().into_boxed_slice(), + public_helicities: plan.public_helicities().to_vec().into_boxed_slice(), + momentum_forms: momentum_forms.into_boxed_slice(), + exact_factors: plan.exact_factors().to_vec().into_boxed_slice(), + sources: plan.sources().to_vec().into_boxed_slice(), + source_dispatch_variants: plan.source_dispatch_variants().to_vec().into_boxed_slice(), + source_embeddings: plan.source_embeddings().to_vec().into_boxed_slice(), + }) + } +} + +struct PersistedHelicityFamilyV1 { + resolved_helicity_id: u32, + source_selections: Box<[DirectResolvedSourceSelection]>, + groups: Box<[PersistedFamilyGroupV1]>, + unique_current_count: u32, + unique_current_component_count: u32, +} + +struct BoundPersistedHelicityFamilyV1 { + family: PersistedHelicityFamilyV1, + workspace: PersistedHelicityWorkspaceV1, + resolved_groups: Box<[Option]>, + union_source_program: Box, + packed_singleton_capable: bool, + interaction_program: DirectInteractionProgram, + singleton_fanout_program: DirectSingletonContributionFanoutProgram, + applied_parameter_version: u64, + descriptor_exposed: bool, + last_execution_report: Option, +} + +/// Borrowed dense physical-color amplitude tile for one selected helicity. +#[derive(Clone, Copy, Debug)] +#[allow(dead_code)] // Accessors are consumed by runtime adapters outside default core builds. +pub(crate) struct PersistedHelicityAmplitudeTileV1<'a> { + amplitude_re: &'a [f64], + amplitude_im: &'a [f64], + destination_count: u32, + point_count: u32, + point_stride: u32, + resolved_helicity_id: u32, +} + +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub(crate) struct PersistedHelicityFamilyExecutionReportV1 { + pub(crate) cache_hit: bool, + pub(crate) source_calls: u32, + pub(crate) source_rows: u32, + pub(crate) contribution_calls: u32, + pub(crate) contribution_rows: u32, + pub(crate) finalization_calls: u32, + pub(crate) finalization_rows: u32, + pub(crate) closure_calls: u32, + pub(crate) closure_rows: u32, +} + +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub(crate) struct PersistedHelicityFamilyCacheCensusV1 { + pub(crate) resolved_helicity_count: u32, + pub(crate) retained_family_count: u32, + pub(crate) retained_row_count: u64, + pub(crate) active_resolved_helicity_id: Option, +} + +/// Legacy query-family-shaped census for the retained persisted family. +/// +/// This is derived at cold bind plus the most recent execution. Inspecting it +/// never revisits the dispatch CSR or scans support masks. +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub(crate) struct PersistedHelicityFamilyInspectionCensusV1 { + pub(crate) query_count: u32, + pub(crate) union_unique_current_count: u32, + pub(crate) union_unique_current_component_count: u32, + pub(crate) union_source_rows: u32, + pub(crate) union_contribution_rows: u32, + pub(crate) union_finalization_rows: u32, + pub(crate) union_closure_rows: u32, + pub(crate) union_amplitude_destination_count: u32, + pub(crate) union_source_executor_call_groups: u32, + pub(crate) union_contribution_executor_call_groups: u32, + pub(crate) union_finalization_executor_call_groups: u32, + pub(crate) union_closure_executor_call_groups: u32, + pub(crate) semantic_executor_binding_count: u32, +} + +#[allow(dead_code)] // Accessors are consumed by runtime adapters outside default core builds. +impl<'a> PersistedHelicityAmplitudeTileV1<'a> { + pub(crate) const fn amplitude_re(self) -> &'a [f64] { + self.amplitude_re + } + + pub(crate) const fn amplitude_im(self) -> &'a [f64] { + self.amplitude_im + } + + pub(crate) const fn destination_count(self) -> u32 { + self.destination_count + } + + pub(crate) const fn point_count(self) -> u32 { + self.point_count + } + + pub(crate) const fn point_stride(self) -> u32 { + self.point_stride + } + + pub(crate) const fn resolved_helicity_id(self) -> u32 { + self.resolved_helicity_id + } + + pub(crate) fn destination_re(self, destination_id: u32) -> RusticolResult<&'a [f64]> { + let start = destination_id as usize * self.point_stride as usize; + let end = start + .checked_add(self.point_count as usize) + .ok_or_else(|| integrity("persisted amplitude destination range overflows usize"))?; + self.amplitude_re + .get(start..end) + .ok_or_else(|| integrity("persisted amplitude real destination is out of bounds")) + } + + pub(crate) fn destination_im(self, destination_id: u32) -> RusticolResult<&'a [f64]> { + let start = destination_id as usize * self.point_stride as usize; + let end = start + .checked_add(self.point_count as usize) + .ok_or_else(|| integrity("persisted amplitude destination range overflows usize"))?; + self.amplitude_im + .get(start..end) + .ok_or_else(|| integrity("persisted amplitude imaginary destination is out of bounds")) + } +} + +fn scalar_len(planes: u32, stride: u32, label: &str) -> RusticolResult { + usize::try_from(planes) + .ok() + .and_then(|planes| planes.checked_mul(stride as usize)) + .ok_or_else(|| invalid(format!("persisted helicity {label} size exceeds usize"))) +} + +fn factor_parts(value: ExactComplexRational) -> RusticolResult<(f64, f64)> { + let real = value.real().numerator() as f64 / value.real().denominator() as f64; + let imag = value.imag().numerator() as f64 / value.imag().denominator() as f64; + if !real.is_finite() || !imag.is_finite() { + return Err(invalid( + "persisted helicity exact factor is not finite in binary64", + )); + } + Ok((real, imag)) +} + +struct PersistedHelicityWorkspaceV1 { + current_re: AlignedF64Buffer, + current_im: AlignedF64Buffer, + amplitude_re: AlignedF64Buffer, + amplitude_im: AlignedF64Buffer, + momenta: AlignedF64Buffer, + parameters_re: AlignedF64Buffer, + parameters_im: AlignedF64Buffer, + factors_re: AlignedF64Buffer, + factors_im: AlignedF64Buffer, + logical_point_capacity: u32, + active_point_count: u32, + point_stride: u32, +} + +impl PersistedHelicityWorkspaceV1 { + fn new( + layout: &PersistedPlanLayoutV1, + logical_point_capacity: u32, + packed_singleton_capable: bool, + ) -> RusticolResult { + if layout.source_count == 0 + || layout.lorentz_component_count == 0 + || layout.current_component_count == 0 + || layout.amplitude_destination_count == 0 + || layout.momentum_forms.is_empty() + || layout.exact_factors.is_empty() + || logical_point_capacity == 0 + { + return Err(integrity( + "persisted helicity family has an empty workspace shape", + )); + } + let point_stride = if logical_point_capacity == 1 && packed_singleton_capable { + 1 + } else { + checked_aligned_point_stride(logical_point_capacity)? + }; + let current_len = scalar_len( + layout.current_component_count, + point_stride, + "current arena", + )?; + let amplitude_len = scalar_len( + layout.amplitude_destination_count, + point_stride, + "amplitude arena", + )?; + let momentum_form_count = u32::try_from(layout.momentum_forms.len()) + .map_err(|_| invalid("persisted helicity momentum-form count exceeds u32"))?; + let momentum_planes = momentum_form_count + .checked_mul(u32::from(layout.lorentz_component_count)) + .ok_or_else(|| invalid("persisted helicity momentum-plane count exceeds u32"))?; + let momentum_len = scalar_len(momentum_planes, point_stride, "momentum arena")?; + let mut factors_re = + AlignedF64Buffer::zeroed(layout.exact_factors.len(), "persisted helicity factor real")?; + let mut factors_im = AlignedF64Buffer::zeroed( + layout.exact_factors.len(), + "persisted helicity factor imaginary", + )?; + for (index, factor) in layout.exact_factors.iter().copied().enumerate() { + let (real, imag) = factor_parts(factor)?; + factors_re.as_mut_slice()[index] = real; + factors_im.as_mut_slice()[index] = imag; + } + let parameter_count = usize::try_from(layout.parameter_count) + .map_err(|_| invalid("persisted helicity parameter count exceeds usize"))?; + Ok(Self { + current_re: AlignedF64Buffer::zeroed(current_len, "persisted helicity current real")?, + current_im: AlignedF64Buffer::zeroed( + current_len, + "persisted helicity current imaginary", + )?, + amplitude_re: AlignedF64Buffer::zeroed( + amplitude_len, + "persisted helicity amplitude real", + )?, + amplitude_im: AlignedF64Buffer::zeroed( + amplitude_len, + "persisted helicity amplitude imaginary", + )?, + momenta: AlignedF64Buffer::zeroed(momentum_len, "persisted helicity momenta")?, + parameters_re: AlignedF64Buffer::zeroed( + parameter_count, + "persisted helicity parameter real", + )?, + parameters_im: AlignedF64Buffer::zeroed( + parameter_count, + "persisted helicity parameter imaginary", + )?, + factors_re, + factors_im, + logical_point_capacity, + active_point_count: 0, + point_stride, + }) + } + + fn refresh_parameters(&mut self, parameters: &[(f64, f64)]) -> RusticolResult<()> { + self.active_point_count = 0; + if parameters.len() != self.parameters_re.len() { + return Err(invalid(format!( + "persisted helicity received {} parameters, expected {}", + parameters.len(), + self.parameters_re.len() + ))); + } + for (index, &(real, imag)) in parameters.iter().enumerate() { + if !real.is_finite() || !imag.is_finite() { + return Err(invalid("persisted helicity parameter value is not finite")); + } + self.parameters_re.as_mut_slice()[index] = real; + self.parameters_im.as_mut_slice()[index] = imag; + } + Ok(()) + } + + fn refresh_inputs( + &mut self, + layout: &PersistedPlanLayoutV1, + external_momenta: &[f64], + point_count: u32, + ) -> RusticolResult<()> { + self.active_point_count = 0; + if point_count == 0 || point_count > self.logical_point_capacity { + return Err(invalid( + "persisted helicity point count is outside its workspace", + )); + } + let expected = usize::try_from(layout.source_count) + .ok() + .and_then(|sources| sources.checked_mul(usize::from(layout.lorentz_component_count))) + .and_then(|planes| planes.checked_mul(point_count as usize)) + .ok_or_else(|| invalid("persisted helicity external momentum shape exceeds usize"))?; + if external_momenta.len() != expected { + return Err(invalid(format!( + "persisted helicity received {} momentum scalars, expected {expected}", + external_momenta.len() + ))); + } + for (form_id, form) in layout.momentum_forms.iter().enumerate() { + for lorentz in 0..usize::from(layout.lorentz_component_count) { + for point in 0..point_count as usize { + let mut value = 0.0; + for term in form.terms() { + if term.source_slot >= layout.source_count { + return Err(integrity( + "persisted momentum source slot is out of bounds", + )); + } + // Recurrence lanes expose point-major external input: + // [point][source][Lorentz]. Keep that contract here so + // the persisted companion needs no transpose scratch. + let input_index = (point * layout.source_count as usize + + term.source_slot as usize) + * usize::from(layout.lorentz_component_count) + + lorentz; + value += f64::from(term.coefficient) * external_momenta[input_index]; + } + let plane = form_id + .checked_mul(usize::from(layout.lorentz_component_count)) + .and_then(|base| base.checked_add(lorentz)) + .ok_or_else(|| invalid("persisted momentum plane exceeds usize"))?; + let index = plane + .checked_mul(self.point_stride as usize) + .and_then(|base| base.checked_add(point)) + .ok_or_else(|| invalid("persisted momentum index exceeds usize"))?; + self.momenta.as_mut_slice()[index] = value; + } + } + } + // A selected H may have no live closure for a physical destination. + // Clear every physical-color plane, not merely the selected closure + // list, so H=A -> H=B -> H=A cannot expose a stale amplitude. + for destination in 0..layout.amplitude_destination_count as usize { + let start = destination * self.point_stride as usize; + let end = start + point_count as usize; + self.amplitude_re.as_mut_slice()[start..end].fill(0.0); + self.amplitude_im.as_mut_slice()[start..end].fill(0.0); + } + Ok(()) + } + + fn raw_views( + &mut self, + layout: &PersistedPlanLayoutV1, + ) -> RusticolResult<( + DirectArenaView, + DirectMomentumView, + DirectParameterView, + DirectFactorView, + )> { + let arena = DirectArenaView { + current_re: self.current_re.as_mut_ptr(), + current_im: self.current_im.as_mut_ptr(), + current_scalar_len: self.current_re.len() as u64, + amplitude_re: self.amplitude_re.as_mut_ptr(), + amplitude_im: self.amplitude_im.as_mut_ptr(), + amplitude_scalar_len: self.amplitude_re.len() as u64, + point_stride: self.point_stride, + }; + let momenta = DirectMomentumView { + values: self.momenta.as_ptr(), + scalar_len: self.momenta.len() as u64, + form_count: u32::try_from(layout.momentum_forms.len()) + .map_err(|_| integrity("persisted momentum-form count exceeds u32"))?, + lorentz_component_count: layout.lorentz_component_count, + point_stride: self.point_stride, + }; + let parameters = DirectParameterView { + values_re: self.parameters_re.as_ptr(), + values_im: self.parameters_im.as_ptr(), + value_count: self.parameters_re.len() as u32, + }; + let factors = DirectFactorView { + values_re: self.factors_re.as_ptr(), + values_im: self.factors_im.as_ptr(), + value_count: self.factors_re.len() as u32, + }; + validate_direct_views(arena, momenta, parameters, factors)?; + Ok((arena, momenta, parameters, factors)) + } + + fn direct_workspace(&mut self, layout: &PersistedPlanLayoutV1) -> DirectWorkspace<'_> { + DirectWorkspace { + current_re: self.current_re.as_mut_slice(), + current_im: self.current_im.as_mut_slice(), + amplitude_re: self.amplitude_re.as_mut_slice(), + amplitude_im: self.amplitude_im.as_mut_slice(), + momenta: self.momenta.as_slice(), + momentum_form_count: layout.momentum_forms.len() as u32, + lorentz_component_count: layout.lorentz_component_count, + parameters_re: self.parameters_re.as_slice(), + parameters_im: self.parameters_im.as_slice(), + factors_re: self.factors_re.as_slice(), + factors_im: self.factors_im.as_slice(), + point_stride: self.point_stride, + } + } + + fn tile( + &self, + layout: &PersistedPlanLayoutV1, + resolved_helicity_id: u32, + point_count: u32, + ) -> RusticolResult> { + if self.active_point_count != point_count { + return Err(integrity( + "persisted helicity tile is not the last successful execution", + )); + } + Ok(PersistedHelicityAmplitudeTileV1 { + amplitude_re: self.amplitude_re.as_slice(), + amplitude_im: self.amplitude_im.as_slice(), + destination_count: layout.amplitude_destination_count, + point_count, + point_stride: self.point_stride, + resolved_helicity_id, + }) + } +} + +fn selected_source_rows( + plan: &DirectRecurrencePlan, + resolved_helicity_id: u32, +) -> RusticolResult> { + let descriptor = plan + .resolved_helicities() + .get(resolved_helicity_id as usize) + .filter(|descriptor| descriptor.id == resolved_helicity_id) + .ok_or_else(|| integrity("persisted resolved helicity is absent from its plan"))?; + let start = usize::try_from(descriptor.source_selection_start) + .map_err(|_| integrity("persisted source-selection start exceeds usize"))?; + let end = start + .checked_add(descriptor.source_selection_count as usize) + .ok_or_else(|| integrity("persisted source-selection range overflows usize"))?; + let selections = plan + .resolved_source_selections() + .get(start..end) + .ok_or_else(|| integrity("persisted source-selection range is out of bounds"))?; + if selections.is_empty() { + return Err(integrity( + "persisted resolved helicity has no source selections", + )); + } + Ok(selections.to_vec().into_boxed_slice()) +} + +fn new_rows_draft(role: DirectExecutorRole) -> RusticolResult { + match role { + DirectExecutorRole::Contribution => { + Ok(PersistedFamilyRowsDraftV1::Contribution(Vec::new())) + } + DirectExecutorRole::Finalization => { + Ok(PersistedFamilyRowsDraftV1::Finalization(Vec::new())) + } + DirectExecutorRole::Closure => Ok(PersistedFamilyRowsDraftV1::Closure(Vec::new())), + DirectExecutorRole::Source => Err(integrity( + "persisted helicity dispatch contains a static source group", + )), + } +} + +fn rederive_contribution_initialization(groups: &mut [PersistedFamilyGroupDraftV1]) { + let mut initialized = BTreeSet::<(u32, u32)>::new(); + for group in groups { + let PersistedFamilyRowsDraftV1::Contribution(rows) = &mut group.rows else { + continue; + }; + for row in rows { + row.flags &= !DIRECT_CONTRIBUTION_FLAG_INITIALIZE_DESTINATION; + if initialized.insert((group.stage, row.destination_component_base)) { + row.flags |= DIRECT_CONTRIBUTION_FLAG_INITIALIZE_DESTINATION; + } + } + } +} + +fn order_contributions_for_fanout(groups: &mut [PersistedFamilyGroupDraftV1]) { + for group in groups { + let PersistedFamilyRowsDraftV1::Contribution(rows) = &mut group.rows else { + continue; + }; + let stage = group.stage; + let executor_id = group.direct_executor_id; + crate::recurrence::direct_lowering::order_contributions_for_runtime_fanout_by( + rows, + |row| { + ( + stage, + executor_id, + *row, + ( + row.destination_component_base, + row.exact_factor_id, + row.flags & !DIRECT_CONTRIBUTION_FLAG_INITIALIZE_DESTINATION, + ), + ) + }, + ); + } +} + +/// Cold semantic lookup over a physically reused Direct-Arena layout. +/// +/// A component base is unique only among currents live in the same stage. The +/// persisted family census therefore must not treat it as a global current ID. +struct PersistedCurrentIndexV1 { + current_by_source_row: BTreeMap, + current_by_stage_and_base: BTreeMap<(u32, u32), u32>, + currents_by_base: BTreeMap>, +} + +impl PersistedCurrentIndexV1 { + fn new(plan: &DirectRecurrencePlan) -> RusticolResult { + let mut current_by_source_row = BTreeMap::new(); + let mut current_by_stage_and_base = BTreeMap::new(); + let mut currents_by_base = BTreeMap::>::new(); + for current in plan.currents() { + let current_id = current.semantic_current_id; + if current_by_stage_and_base + .insert( + (u32::from(current.stage), current.component_base), + current_id, + ) + .is_some() + { + return Err(integrity( + "persisted plan repeats a current component base within one stage", + )); + } + if current.source_row_or_sentinel != DIRECT_NONE_U32 + && current_by_source_row + .insert(current.source_row_or_sentinel, current_id) + .is_some() + { + return Err(integrity( + "persisted plan repeats a source-row current binding", + )); + } + currents_by_base + .entry(current.component_base) + .or_default() + .push(current_id); + } + for current_ids in currents_by_base.values_mut() { + current_ids.sort_unstable_by_key(|current_id| { + let current = &plan.currents()[*current_id as usize]; + (current.first_use, current.last_use, *current_id) + }); + } + Ok(Self { + current_by_source_row, + current_by_stage_and_base, + currents_by_base, + }) + } + + fn source_current(&self, source_row_id: u32, label: &str) -> RusticolResult { + self.current_by_source_row + .get(&source_row_id) + .copied() + .ok_or_else(|| integrity(format!("persisted {label} has no semantic source current"))) + } + + fn destination_current( + &self, + stage: u32, + component_base: u32, + label: &str, + ) -> RusticolResult { + self.current_by_stage_and_base + .get(&(stage, component_base)) + .copied() + .ok_or_else(|| { + integrity(format!( + "persisted {label} references no current defined at its stage" + )) + }) + } + + fn live_current( + &self, + plan: &DirectRecurrencePlan, + stage: u32, + component_base: u32, + label: &str, + ) -> RusticolResult { + let current_ids = self.currents_by_base.get(&component_base).ok_or_else(|| { + integrity(format!( + "persisted {label} references a missing current component base" + )) + })?; + let end = current_ids + .partition_point(|current_id| plan.currents()[*current_id as usize].first_use <= stage); + let current_id = current_ids + .get(end.saturating_sub(1)) + .copied() + .filter(|_| end != 0) + .ok_or_else(|| { + integrity(format!( + "persisted {label} references no current live at its stage" + )) + })?; + let current = plan + .currents() + .get(current_id as usize) + .filter(|current| current.semantic_current_id == current_id) + .ok_or_else(|| integrity("persisted current index has a noncanonical semantic ID"))?; + if stage > current.last_use { + return Err(integrity(format!( + "persisted {label} references no current live at its stage" + ))); + } + Ok(current_id) + } +} + +fn family_current_census( + plan: &DirectRecurrencePlan, + source_selections: &[DirectResolvedSourceSelection], + groups: &[PersistedFamilyGroupV1], +) -> RusticolResult<(u32, u32)> { + let current_index = PersistedCurrentIndexV1::new(plan)?; + let mut active_currents = BTreeSet::::new(); + + for selection in source_selections { + let variant = plan + .source_dispatch_variants() + .get(selection.dispatch_variant_id as usize) + .ok_or_else(|| integrity("persisted source selection has no dispatch variant"))?; + let source = plan + .sources() + .get(variant.source_row_id as usize) + .ok_or_else(|| integrity("persisted source dispatch has no source row"))?; + let current_id = current_index.source_current(variant.source_row_id, "source row")?; + let current = &plan.currents()[current_id as usize]; + if current.component_base != source.destination_component_base { + return Err(integrity( + "persisted source row differs from its semantic current binding", + )); + } + active_currents.insert(current_id); + } + for group in groups { + match &group.rows { + PersistedFamilyRowsV1::Contribution(rows) => { + for row in rows { + active_currents.insert(current_index.live_current( + plan, + group.stage, + row.parent0_component_base, + "contribution parent", + )?); + if row.parent1_component_base_or_sentinel != DIRECT_NONE_U32 { + active_currents.insert(current_index.live_current( + plan, + group.stage, + row.parent1_component_base_or_sentinel, + "contribution parent", + )?); + } + active_currents.insert(current_index.destination_current( + group.stage, + row.destination_component_base, + "contribution destination", + )?); + } + } + PersistedFamilyRowsV1::Finalization(rows) => { + for row in rows { + active_currents.insert(current_index.destination_current( + group.stage, + row.component_base, + "finalization row", + )?); + } + } + PersistedFamilyRowsV1::Closure(rows) => { + for row in rows { + active_currents.insert(current_index.live_current( + plan, + group.stage, + row.parent0_component_base, + "closure parent", + )?); + if row.parent1_component_base_or_sentinel != DIRECT_NONE_U32 { + active_currents.insert(current_index.live_current( + plan, + group.stage, + row.parent1_component_base_or_sentinel, + "closure parent", + )?); + } + } + } + } + } + + let unique_current_count = u32::try_from(active_currents.len()) + .map_err(|_| integrity("persisted active current count exceeds u32"))?; + let unique_current_component_count = + active_currents + .into_iter() + .try_fold(0u32, |count, current_id| { + count + .checked_add(u32::from( + plan.currents()[current_id as usize].component_count, + )) + .ok_or_else(|| { + integrity("persisted active current component count exceeds u32") + }) + })?; + Ok((unique_current_count, unique_current_component_count)) +} + +fn build_family( + plan: &DirectRecurrencePlan, + dispatch: &DirectHelicityDispatch, + resolved_helicity_id: u32, +) -> RusticolResult { + let mut drafts = Vec::::new(); + for &group_id in dispatch.group_ids_for_helicity(resolved_helicity_id)? { + let descriptor = dispatch + .row_groups() + .get(group_id as usize) + .ok_or_else(|| integrity("persisted helicity group ID is out of bounds"))?; + let stage = u32::from(descriptor.stage); + if !drafts.last().is_some_and(|draft| { + draft.accepts(stage, descriptor.role, descriptor.direct_executor_id) + }) { + drafts.push(PersistedFamilyGroupDraftV1 { + stage, + direct_executor_id: descriptor.direct_executor_id, + rows: new_rows_draft(descriptor.role)?, + }); + } + drafts + .last_mut() + .expect("persisted group draft was just inserted") + .rows + .append_from_plan( + plan, + descriptor.role, + descriptor.row_start, + descriptor.row_count, + )?; + } + // A resolved helicity can be a certified structural zero. Its empty CSR + // is executable: source dispatch remains exact and the full amplitude + // clear below produces a deterministic dense zero tile. + order_contributions_for_fanout(&mut drafts); + rederive_contribution_initialization(&mut drafts); + let groups = drafts + .into_iter() + .map(|draft| PersistedFamilyGroupV1 { + stage: draft.stage, + role: draft.rows.role(), + direct_executor_id: draft.direct_executor_id, + rows: draft.rows.freeze(), + }) + .collect::>() + .into_boxed_slice(); + let source_selections = selected_source_rows(plan, resolved_helicity_id)?; + let (unique_current_count, unique_current_component_count) = + family_current_census(plan, &source_selections, &groups)?; + Ok(PersistedHelicityFamilyV1 { + resolved_helicity_id, + source_selections, + groups, + unique_current_count, + unique_current_component_count, + }) +} + +fn bind_groups( + resolver: &R, + family: &PersistedHelicityFamilyV1, +) -> RusticolResult]>> { + family + .groups + .iter() + .map(|group| { + if group.role == DirectExecutorRole::Contribution + && group.direct_executor_id == DIRECT_NONE_U32 + { + let PersistedFamilyRowsV1::Contribution(rows) = &group.rows else { + return Err(integrity( + "certified-reuse persisted group has non-contribution storage", + )); + }; + if rows + .iter() + .any(|row| row.flags & DIRECT_CONTRIBUTION_FLAG_CERTIFIED_REUSE == 0) + { + return Err(integrity( + "missing-executor persisted group is not certified reuse", + )); + } + return Ok(None); + } + let resolved = + resolver.resolve_direct_executor(group.direct_executor_id, group.role)?; + if resolved.direct_executor_id != group.direct_executor_id + || resolved.handle.role() != group.role + { + return Err(integrity( + "persisted family executor binding differs from its direct row group", + )); + } + Ok(Some(resolved)) + }) + .collect::>>() + .map(Vec::into_boxed_slice) +} + +fn direct_group_views<'a>( + family: &'a PersistedHelicityFamilyV1, + resolved_groups: &[Option], +) -> RusticolResult<(Vec>, usize)> { + if family.groups.len() != resolved_groups.len() { + return Err(integrity( + "persisted family resolved group count is inconsistent", + )); + } + let mut views = Vec::with_capacity(family.groups.len()); + let mut contribution_count = 0usize; + for (group, resolved) in family.groups.iter().zip(resolved_groups) { + views.push(match &group.rows { + PersistedFamilyRowsV1::Contribution(rows) => { + contribution_count = + contribution_count.checked_add(rows.len()).ok_or_else(|| { + integrity("persisted family contribution count exceeds usize") + })?; + DirectInteractionGroupView::contribution( + group.stage, + group.direct_executor_id, + resolved.map(|resolved| resolved.handle), + resolved.and_then(|resolved| resolved.interaction_capability), + rows, + ) + } + PersistedFamilyRowsV1::Finalization(_) => DirectInteractionGroupView::other( + group.stage, + group.role, + group.direct_executor_id, + resolved.map(|resolved| resolved.handle), + ), + PersistedFamilyRowsV1::Closure(rows) => { + let resolved = resolved + .ok_or_else(|| integrity("persisted closure group has no resolved executor"))?; + DirectInteractionGroupView::closure( + group.stage, + group.direct_executor_id, + resolved.handle, + rows, + ) + } + }); + } + Ok((views, contribution_count)) +} + +fn bind_programs( + plan: &DirectRecurrencePlan, + layout: &PersistedPlanLayoutV1, + family: &PersistedHelicityFamilyV1, + resolved_groups: &[Option], +) -> RusticolResult<( + DirectInteractionProgram, + DirectSingletonContributionFanoutProgram, +)> { + let (views, contribution_count) = direct_group_views(family, resolved_groups)?; + let schedule = DirectInteractionScheduleView::new( + RecurrenceStrategy::ContractedColorUnion, + &views, + plan.currents(), + contribution_count, + ); + let interaction = DirectInteractionProgram::build(schedule)?; + let fanout = DirectSingletonContributionFanoutProgram::build( + &views, + layout.current_component_count, + u32::try_from(layout.momentum_forms.len()) + .map_err(|_| integrity("persisted momentum-form count exceeds u32"))?, + layout.parameter_count, + u32::try_from(layout.exact_factors.len()) + .map_err(|_| integrity("persisted exact-factor count exceeds u32"))?, + )?; + Ok((interaction, fanout)) +} + +fn increment(value: &mut u32, by: u32, label: &str) -> RusticolResult<()> { + *value = value + .checked_add(by) + .ok_or_else(|| integrity(format!("persisted helicity {label} exceeds u32")))?; + Ok(()) +} + +fn record_profiled_group( + report: &mut PersistedHelicityFamilyExecutionReportV1, + role: DirectExecutorRole, + row_count: u32, +) -> RusticolResult<()> { + match role { + DirectExecutorRole::Source => { + increment(&mut report.source_calls, 1, "source calls")?; + increment(&mut report.source_rows, row_count, "source rows") + } + DirectExecutorRole::Contribution => { + increment(&mut report.contribution_calls, 1, "contribution calls")?; + increment( + &mut report.contribution_rows, + row_count, + "contribution rows", + ) + } + DirectExecutorRole::Finalization => { + increment(&mut report.finalization_calls, 1, "finalization calls")?; + increment( + &mut report.finalization_rows, + row_count, + "finalization rows", + ) + } + DirectExecutorRole::Closure => { + increment(&mut report.closure_calls, 1, "closure calls")?; + increment(&mut report.closure_rows, row_count, "closure rows") + } + } +} + +fn execute_union_source( + program: &dyn OnTheFlyBoundUnionSourceProgram, + arena: DirectArenaView, + momenta: DirectMomentumView, + parameters: DirectParameterView, + point_count: u32, +) -> RusticolResult<()> { + // `raw_views` authenticated the descriptor bundle immediately before this + // call. The source program authenticated every plan-owned table and bound + // all catalog lookups during `prepare`. + unsafe { program.execute(arena, momenta, parameters, point_count) } +} + +fn execute_bound_family( + layout: &PersistedPlanLayoutV1, + cached: &mut BoundPersistedHelicityFamilyV1, + point_count: u32, + mut report: Report, + mut record_group: RecordGroup, +) -> RusticolResult +where + RecordGroup: FnMut(&mut Report, DirectExecutorRole, u32) -> RusticolResult<()>, +{ + clear_direct_executor_error_detail(); + // The workspace shape is immutable while a family is bound. Authenticate + // its raw descriptors once per tile, then reuse the Copy views for the + // source dispatcher and every row group, exactly as the OTF family path + // does. Revalidating their pointer ranges inside each group is redundant. + let (arena, momenta, parameters, factors) = cached.workspace.raw_views(layout)?; + execute_union_source( + cached.union_source_program.as_ref(), + arena, + momenta, + parameters, + point_count, + )?; + let source_row_count = u32::try_from(cached.family.source_selections.len()) + .map_err(|_| integrity("persisted source-selection count exceeds u32"))?; + record_group(&mut report, DirectExecutorRole::Source, source_row_count)?; + + for (group_index, (group, resolved)) in cached + .family + .groups + .iter() + .zip(cached.resolved_groups.iter().copied()) + .enumerate() + { + let row_count = u32::try_from(group.rows.len()) + .map_err(|_| integrity("persisted row-group count exceeds u32"))?; + if group.role == DirectExecutorRole::Source { + return Err(integrity( + "persisted non-source schedule contains a source group", + )); + } + record_group(&mut report, group.role, row_count)?; + + if point_count == 1 { + match cached.interaction_program.action(group_index) { + DirectInteractionGroupAction::Normal => { + if let PersistedFamilyRowsV1::Contribution(rows) = &group.rows + && group.direct_executor_id != DIRECT_NONE_U32 + && cached.singleton_fanout_program.execute_group( + group_index, + rows, + arena, + momenta, + parameters, + factors, + point_count, + )? + { + continue; + } + } + DirectInteractionGroupAction::Consumed => continue, + DirectInteractionGroupAction::Execute(stage_index) => { + cached.interaction_program.execute::( + stage_index, + arena, + momenta, + parameters, + factors, + )?; + continue; + } + } + } + + if group.role == DirectExecutorRole::Contribution + && group.direct_executor_id == DIRECT_NONE_U32 + { + let PersistedFamilyRowsV1::Contribution(rows) = &group.rows else { + return Err(integrity("certified-reuse group has non-contribution rows")); + }; + let mut workspace = cached.workspace.direct_workspace(layout); + execute_certified_reuse_rows(rows, &mut workspace, point_count)?; + continue; + } + + let resolved = resolved + .ok_or_else(|| integrity("persisted callable row group has no resolved executor"))?; + let status = unsafe { + match (&group.rows, resolved.handle) { + ( + PersistedFamilyRowsV1::Contribution(rows), + DirectExecutorHandle::Contribution { call, context }, + ) => call( + context, + arena, + momenta, + parameters, + factors, + rows.as_ptr(), + row_count, + point_count, + ), + ( + PersistedFamilyRowsV1::Finalization(rows), + DirectExecutorHandle::Finalization { call, context }, + ) => call( + context, + arena, + momenta, + parameters, + factors, + rows.as_ptr(), + row_count, + point_count, + ), + ( + PersistedFamilyRowsV1::Closure(rows), + DirectExecutorHandle::Closure { call, context }, + ) => call( + context, + arena, + momenta, + parameters, + factors, + rows.as_ptr(), + row_count, + point_count, + ), + _ => { + return Err(integrity( + "persisted family handle differs from its row storage", + )); + } + } + }; + check_status(group.role, group.direct_executor_id, status)?; + } + cached.workspace.active_point_count = point_count; + Ok(report) +} + +/// One retained cold binding for the active auxiliary-plan helicity. +/// Repeated calls for that H stay on the bound row/program path and never +/// revisit the dispatch CSR. Switching H replaces this workspace deliberately +/// instead of multiplying recurrence arenas by the full helicity count. +pub(crate) struct PersistedHelicityFamilyExecutorV1 { + family: Option, + layout: Option, + resolver: R, + parameter_state: Vec<(f64, f64)>, + parameter_version: u64, + active_cache_hit: bool, +} + +#[allow(dead_code)] // Inspection/control methods are used by feature-specific runtime adapters. +impl PersistedHelicityFamilyExecutorV1 { + pub(crate) const fn new(resolver: R) -> Self { + Self { + family: None, + layout: None, + resolver, + parameter_state: Vec::new(), + parameter_version: 0, + active_cache_hit: false, + } + } + + pub(crate) const fn resolver(&self) -> &R { + &self.resolver + } + + pub(crate) const fn resolver_mut(&mut self) -> &mut R { + &mut self.resolver + } + + fn invalidate_exposed_row_tables(&mut self) -> RusticolResult<()> { + if !self + .family + .as_ref() + .is_some_and(|family| family.descriptor_exposed) + { + return Ok(()); + } + self.resolver.invalidate_row_tables()?; + if let Some(family) = &mut self.family { + family.descriptor_exposed = false; + } + Ok(()) + } + + pub(crate) fn clear_families(&mut self) -> RusticolResult<()> { + self.invalidate_exposed_row_tables()?; + self.family = None; + self.layout = None; + self.active_cache_hit = false; + Ok(()) + } + + pub(crate) fn cache_census(&self) -> PersistedHelicityFamilyCacheCensusV1 { + PersistedHelicityFamilyCacheCensusV1 { + resolved_helicity_count: self + .layout + .as_ref() + .map_or(0, |layout| layout.resolved_helicity_count), + retained_family_count: u32::from(self.family.is_some()), + retained_row_count: self + .family + .as_ref() + .into_iter() + .flat_map(|family| family.family.groups.iter()) + .map(|group| group.rows.len() as u64) + .sum(), + active_resolved_helicity_id: self + .family + .as_ref() + .map(|family| family.family.resolved_helicity_id), + } + } + + pub(crate) fn active_family_inspection_census( + &self, + ) -> Option { + let layout = self.layout.as_ref()?; + let cached = self.family.as_ref()?; + // The option is the successful-execution marker. Derive diagnostic + // counts only when inspection is requested so ordinary unprofiled + // evaluation does no per-group report bookkeeping. + cached.last_execution_report?; + let mut report = PersistedHelicityFamilyExecutionReportV1 { + source_calls: 1, + source_rows: u32::try_from(cached.family.source_selections.len()).ok()?, + ..PersistedHelicityFamilyExecutionReportV1::default() + }; + let mut executor_ids = BTreeSet::::new(); + for selection in &cached.family.source_selections { + let variant = layout + .source_dispatch_variants + .get(selection.dispatch_variant_id as usize)?; + if variant.direct_executor_id != DIRECT_NONE_U32 { + executor_ids.insert(variant.direct_executor_id); + } + } + for group in &cached.family.groups { + let row_count = u32::try_from(group.rows.len()).ok()?; + match group.role { + DirectExecutorRole::Source => return None, + DirectExecutorRole::Contribution => { + increment(&mut report.contribution_calls, 1, "contribution calls").ok()?; + increment( + &mut report.contribution_rows, + row_count, + "contribution rows", + ) + .ok()?; + } + DirectExecutorRole::Finalization => { + increment(&mut report.finalization_calls, 1, "finalization calls").ok()?; + increment( + &mut report.finalization_rows, + row_count, + "finalization rows", + ) + .ok()?; + } + DirectExecutorRole::Closure => { + increment(&mut report.closure_calls, 1, "closure calls").ok()?; + increment(&mut report.closure_rows, row_count, "closure rows").ok()?; + } + } + if group.direct_executor_id != DIRECT_NONE_U32 { + executor_ids.insert(group.direct_executor_id); + } + } + let semantic_executor_binding_count = u32::try_from(executor_ids.len()).ok()?; + Some(PersistedHelicityFamilyInspectionCensusV1 { + query_count: layout.amplitude_destination_count, + union_unique_current_count: cached.family.unique_current_count, + union_unique_current_component_count: cached.family.unique_current_component_count, + union_source_rows: report.source_rows, + union_contribution_rows: report.contribution_rows, + union_finalization_rows: report.finalization_rows, + union_closure_rows: report.closure_rows, + union_amplitude_destination_count: layout.amplitude_destination_count, + union_source_executor_call_groups: report.source_calls, + union_contribution_executor_call_groups: report.contribution_calls, + union_finalization_executor_call_groups: report.finalization_calls, + union_closure_executor_call_groups: report.closure_calls, + semantic_executor_binding_count, + }) + } + + pub(crate) fn resolved_helicities( + &self, + ) -> RusticolResult<&[DirectResolvedHelicityDescriptor]> { + self.layout + .as_ref() + .map(|layout| layout.resolved_helicities.as_ref()) + .ok_or_else(|| invalid("persisted helicity descriptors require a cold prepare")) + } + + pub(crate) fn public_helicities(&self) -> RusticolResult<&[i32]> { + self.layout + .as_ref() + .map(|layout| layout.public_helicities.as_ref()) + .ok_or_else(|| invalid("persisted public helicities require a cold prepare")) + } + + pub(crate) fn set_parameters(&mut self, parameters: &[(f64, f64)]) -> RusticolResult<()> { + if parameters + .iter() + .any(|(real, imag)| !real.is_finite() || !imag.is_finite()) + { + return Err(invalid("persisted helicity parameter value is not finite")); + } + if self.parameter_state == parameters { + return Ok(()); + } + let mut replacement = Vec::new(); + replacement + .try_reserve_exact(parameters.len()) + .map_err(|error| { + invalid(format!( + "persisted helicity parameter allocation failed: {error}" + )) + })?; + replacement.extend_from_slice(parameters); + self.parameter_state = replacement; + self.parameter_version = self + .parameter_version + .checked_add(1) + .ok_or_else(|| invalid("persisted helicity parameter version exceeds u64"))?; + Ok(()) + } + + /// Update from the recurrence lane's persistent split-complex parameter + /// planes. Equality is checked in place; unchanged warm preparation does + /// not allocate or rewrite any retained workspace. + pub(crate) fn set_parameter_planes( + &mut self, + parameters_re: &[f64], + parameters_im: &[f64], + ) -> RusticolResult<()> { + if parameters_re.len() != parameters_im.len() { + return Err(invalid( + "persisted helicity parameter planes have different lengths", + )); + } + if parameters_re + .iter() + .zip(parameters_im) + .any(|(real, imag)| !real.is_finite() || !imag.is_finite()) + { + return Err(invalid("persisted helicity parameter value is not finite")); + } + if self.parameter_state.len() == parameters_re.len() + && self + .parameter_state + .iter() + .zip(parameters_re.iter().zip(parameters_im)) + .all(|(&(cached_re, cached_im), (&real, &imag))| { + cached_re == real && cached_im == imag + }) + { + return Ok(()); + } + let mut replacement = Vec::new(); + replacement + .try_reserve_exact(parameters_re.len()) + .map_err(|error| { + invalid(format!( + "persisted helicity parameter allocation failed: {error}" + )) + })?; + replacement.extend( + parameters_re + .iter() + .copied() + .zip(parameters_im.iter().copied()), + ); + self.parameter_state = replacement; + self.parameter_version = self + .parameter_version + .checked_add(1) + .ok_or_else(|| invalid("persisted helicity parameter version exceeds u64"))?; + Ok(()) + } + + fn ensure_layout( + &mut self, + plan: &DirectRecurrencePlan, + dispatch: &DirectHelicityDispatch, + lorentz_component_count: u16, + ) -> RusticolResult<()> { + let matches = self.layout.as_ref().is_some_and(|layout| { + layout.runtime_layout_digest == plan.runtime_layout_digest() + && layout.dispatch_address == std::ptr::from_ref(dispatch).addr() + && layout.lorentz_component_count == lorentz_component_count + }); + if matches { + return Ok(()); + } + dispatch.validate_for_plan(plan)?; + self.clear_families()?; + self.layout = Some(PersistedPlanLayoutV1::new( + plan, + dispatch, + lorentz_component_count, + )?); + Ok(()) + } + + /// Cold-select one full auxiliary-plan resolved helicity. + /// + /// Returns true when the optimized family was already retained. All CSR + /// traversal, row copying, INIT derivation, and program construction occur + /// here and are absent from warmed execution. + pub(crate) fn prepare( + &mut self, + plan: &DirectRecurrencePlan, + dispatch: &DirectHelicityDispatch, + resolved_helicity_id: u32, + lorentz_component_count: u16, + logical_point_capacity: u32, + ) -> RusticolResult { + self.ensure_layout(plan, dispatch, lorentz_component_count)?; + let layout = self + .layout + .as_ref() + .expect("persisted layout was just established"); + if resolved_helicity_id >= layout.resolved_helicity_count { + return Err(invalid( + "persisted resolved helicity is outside the auxiliary plan", + )); + } + + if self + .family + .as_ref() + .is_some_and(|family| family.family.resolved_helicity_id == resolved_helicity_id) + { + let needs_resize = self.family.as_ref().is_some_and(|family| { + logical_point_capacity > family.workspace.logical_point_capacity + }); + if needs_resize { + let family = self + .family + .as_ref() + .expect("retained persisted family disappeared"); + let replacement = PersistedHelicityWorkspaceV1::new( + layout, + logical_point_capacity, + family.packed_singleton_capable, + )?; + self.invalidate_exposed_row_tables()?; + let family = self + .family + .as_mut() + .expect("retained persisted family disappeared after invalidation"); + family.workspace = replacement; + family.applied_parameter_version = u64::MAX; + } + self.active_cache_hit = true; + return Ok(true); + } + + let family = build_family(plan, dispatch, resolved_helicity_id)?; + let resolved_groups = bind_groups(&self.resolver, &family)?; + let packed_singleton_capable = self.resolver.union_source_packed_singleton_capable() + && resolved_groups + .iter() + .flatten() + .all(|resolved| resolved.packed_singleton_capable); + let workspace = PersistedHelicityWorkspaceV1::new( + layout, + logical_point_capacity, + packed_singleton_capable, + )?; + let (interaction_program, singleton_fanout_program) = + bind_programs(plan, layout, &family, &resolved_groups)?; + let union_source_program = + self.resolver + .bind_union_source_program(OnTheFlyUnionSourceBindingView { + sources: &layout.sources, + variants: &layout.source_dispatch_variants, + embeddings: &layout.source_embeddings, + selections: &family.source_selections, + exact_factors: &layout.exact_factors, + current_component_count: layout.current_component_count, + momentum_form_count: u32::try_from(layout.momentum_forms.len()) + .map_err(|_| integrity("persisted momentum-form count exceeds u32"))?, + parameter_count: layout.parameter_count, + })?; + let bound = BoundPersistedHelicityFamilyV1 { + family, + workspace, + resolved_groups, + union_source_program, + packed_singleton_capable, + interaction_program, + singleton_fanout_program, + applied_parameter_version: u64::MAX, + descriptor_exposed: false, + last_execution_report: None, + }; + self.invalidate_exposed_row_tables()?; + self.family = Some(bound); + self.active_cache_hit = false; + Ok(false) + } + + fn execute_active( + &mut self, + external_momenta: &[f64], + point_count: u32, + report: Report, + record_group: RecordGroup, + ) -> RusticolResult + where + RecordGroup: FnMut(&mut Report, DirectExecutorRole, u32) -> RusticolResult<()>, + { + let layout = self + .layout + .as_ref() + .ok_or_else(|| invalid("persisted helicity execution requires a cold prepare"))?; + let family = self + .family + .as_mut() + .ok_or_else(|| integrity("active persisted helicity family is absent"))?; + if family.applied_parameter_version != self.parameter_version { + family.workspace.refresh_parameters(&self.parameter_state)?; + family.applied_parameter_version = self.parameter_version; + } + family + .workspace + .refresh_inputs(layout, external_momenta, point_count)?; + family.descriptor_exposed = true; + let report = execute_bound_family(layout, family, point_count, report, record_group)?; + self.active_cache_hit = true; + Ok(report) + } + + fn mark_execution_report(&mut self, report: PersistedHelicityFamilyExecutionReportV1) { + self.family + .as_mut() + .expect("successful persisted execution lost its active family") + .last_execution_report = Some(report); + } + + pub(crate) fn execute_tile_unprofiled( + &mut self, + external_momenta: &[f64], + point_count: u32, + ) -> RusticolResult> { + self.execute_active(external_momenta, point_count, (), |_, _, _| Ok(()))?; + self.mark_execution_report(PersistedHelicityFamilyExecutionReportV1::default()); + self.active_tile(point_count) + } + + pub(crate) fn execute_tile_profiled( + &mut self, + external_momenta: &[f64], + point_count: u32, + ) -> RusticolResult<( + PersistedHelicityAmplitudeTileV1<'_>, + PersistedHelicityFamilyExecutionReportV1, + )> { + let report = self.execute_active( + external_momenta, + point_count, + PersistedHelicityFamilyExecutionReportV1 { + cache_hit: self.active_cache_hit, + ..PersistedHelicityFamilyExecutionReportV1::default() + }, + record_profiled_group, + )?; + self.mark_execution_report(report); + let tile = self.active_tile(point_count)?; + Ok((tile, report)) + } + + fn active_tile( + &self, + point_count: u32, + ) -> RusticolResult> { + let layout = self + .layout + .as_ref() + .ok_or_else(|| integrity("persisted helicity layout disappeared"))?; + let family = self + .family + .as_ref() + .ok_or_else(|| integrity("persisted helicity active family is absent"))?; + family + .workspace + .tile(layout, family.family.resolved_helicity_id, point_count) + } + + fn validate_output_shape( + &self, + point_count: u32, + output_re: &[f64], + output_im: &[f64], + ) -> RusticolResult<()> { + let destination_count = self + .layout + .as_ref() + .ok_or_else(|| invalid("persisted helicity output requires a cold prepare"))? + .amplitude_destination_count; + let expected = destination_count as usize * point_count as usize; + if output_re.len() != expected || output_im.len() != expected { + return Err(invalid(format!( + "persisted helicity outputs have shapes ({}, {}), expected ({expected}, {expected})", + output_re.len(), + output_im.len() + ))); + } + Ok(()) + } + + fn copy_active_outputs( + &self, + point_count: u32, + output_re: &mut [f64], + output_im: &mut [f64], + ) -> RusticolResult<()> { + self.validate_output_shape(point_count, output_re, output_im)?; + let tile = self.active_tile(point_count)?; + for destination in 0..tile.destination_count() as usize { + let source_start = destination * tile.point_stride() as usize; + let source_end = source_start + point_count as usize; + let destination_start = destination * point_count as usize; + let destination_end = destination_start + point_count as usize; + output_re[destination_start..destination_end] + .copy_from_slice(&tile.amplitude_re()[source_start..source_end]); + output_im[destination_start..destination_end] + .copy_from_slice(&tile.amplitude_im()[source_start..source_end]); + } + Ok(()) + } + + pub(crate) fn execute_into_unprofiled( + &mut self, + external_momenta: &[f64], + point_count: u32, + output_re: &mut [f64], + output_im: &mut [f64], + ) -> RusticolResult<()> { + self.validate_output_shape(point_count, output_re, output_im)?; + self.execute_active(external_momenta, point_count, (), |_, _, _| Ok(()))?; + self.mark_execution_report(PersistedHelicityFamilyExecutionReportV1::default()); + self.copy_active_outputs(point_count, output_re, output_im) + } + + pub(crate) fn execute_into_profiled( + &mut self, + external_momenta: &[f64], + point_count: u32, + output_re: &mut [f64], + output_im: &mut [f64], + ) -> RusticolResult { + self.validate_output_shape(point_count, output_re, output_im)?; + let mut report = PersistedHelicityFamilyExecutionReportV1 { + cache_hit: self.active_cache_hit, + ..PersistedHelicityFamilyExecutionReportV1::default() + }; + report = + self.execute_active(external_momenta, point_count, report, record_profiled_group)?; + self.mark_execution_report(report); + self.copy_active_outputs(point_count, output_re, output_im)?; + Ok(report) + } +} + +impl Drop for PersistedHelicityFamilyExecutorV1 { + fn drop(&mut self) { + if self.invalidate_exposed_row_tables().is_ok() { + return; + } + // A callback unexpectedly retaining row pointers is safer with leaked + // private rows than with a dangling descriptor during teardown. + if let Some(family) = self.family.take() + && family.descriptor_exposed + { + std::mem::forget(family); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::recurrence::{ + DirectHelicityDispatchDescriptor, DirectHelicityDispatchParts, + DirectHelicityRowGroupDescriptor, DirectHelicitySupportDomainDescriptor, + DirectSourceProjectionRow, + }; + use std::cell::Cell; + use std::ffi::c_void; + + fn digest(byte: u8) -> SemanticDigest { + SemanticDigest::new([byte; 32]).unwrap() + } + + fn test_layout() -> PersistedPlanLayoutV1 { + PersistedPlanLayoutV1 { + runtime_layout_digest: digest(0x44), + dispatch_address: 0, + source_count: 2, + lorentz_component_count: 2, + parameter_count: 0, + current_component_count: 4, + amplitude_destination_count: 2, + resolved_helicity_count: 2, + resolved_helicities: Box::new([]), + public_helicities: Box::new([]), + momentum_forms: vec![ + CanonicalMomentumLinearForm::new(vec![MomentumTerm { + source_slot: 0, + coefficient: 1, + }]) + .unwrap(), + CanonicalMomentumLinearForm::new(vec![MomentumTerm { + source_slot: 1, + coefficient: -1, + }]) + .unwrap(), + ] + .into_boxed_slice(), + exact_factors: vec![ExactComplexRational::ONE].into_boxed_slice(), + sources: Box::new([]), + source_dispatch_variants: Box::new([]), + source_embeddings: Box::new([]), + } + } + + fn contribution(destination: u32, flags: u32) -> DirectContributionRow { + DirectContributionRow { + parent0_component_base: 0, + parent1_component_base_or_sentinel: DIRECT_NONE_U32, + parent0_momentum_form_id: 1, + parent1_momentum_form_id_or_sentinel: DIRECT_NONE_U32, + destination_component_base: destination, + exact_factor_id: 0, + selector_domain_id: 0, + flags, + } + } + + struct ProbeBoundUnionSourceProgram { + value: f64, + } + + impl OnTheFlyBoundUnionSourceProgram for ProbeBoundUnionSourceProgram { + unsafe fn execute( + &self, + arena: DirectArenaView, + _momenta: DirectMomentumView, + _parameters: DirectParameterView, + point_count: u32, + ) -> RusticolResult<()> { + let current_re = unsafe { + std::slice::from_raw_parts_mut(arena.current_re, arena.current_scalar_len as usize) + }; + let current_im = unsafe { + std::slice::from_raw_parts_mut(arena.current_im, arena.current_scalar_len as usize) + }; + for component in 0..2usize { + for point in 0..point_count as usize { + let index = component * arena.point_stride as usize + point; + current_re[index] = self.value; + current_im[index] = 0.0; + } + } + Ok(()) + } + } + + unsafe extern "C" fn contribution_copy( + _context: *const c_void, + arena: DirectArenaView, + _momenta: DirectMomentumView, + _parameters: DirectParameterView, + _factors: DirectFactorView, + rows: *const DirectContributionRow, + row_count: u32, + point_count: u32, + ) -> c_int { + let rows = unsafe { std::slice::from_raw_parts(rows, row_count as usize) }; + let current_re = unsafe { + std::slice::from_raw_parts_mut(arena.current_re, arena.current_scalar_len as usize) + }; + let current_im = unsafe { + std::slice::from_raw_parts_mut(arena.current_im, arena.current_scalar_len as usize) + }; + for row in rows { + for point in 0..point_count as usize { + let source = + row.parent0_component_base as usize * arena.point_stride as usize + point; + let destination = + row.destination_component_base as usize * arena.point_stride as usize + point; + if row.flags & DIRECT_CONTRIBUTION_FLAG_INITIALIZE_DESTINATION != 0 { + current_re[destination] = current_re[source]; + current_im[destination] = current_im[source]; + } else { + current_re[destination] += current_re[source]; + current_im[destination] += current_im[source]; + } + } + } + DIRECT_STATUS_OK + } + + unsafe extern "C" fn finalize_noop( + _context: *const c_void, + _arena: DirectArenaView, + _momenta: DirectMomentumView, + _parameters: DirectParameterView, + _factors: DirectFactorView, + _rows: *const DirectFinalizationRow, + _row_count: u32, + _point_count: u32, + ) -> c_int { + DIRECT_STATUS_OK + } + + unsafe extern "C" fn close_copy( + _context: *const c_void, + arena: DirectArenaView, + _momenta: DirectMomentumView, + _parameters: DirectParameterView, + _factors: DirectFactorView, + rows: *const DirectClosureRow, + row_count: u32, + point_count: u32, + ) -> c_int { + let rows = unsafe { std::slice::from_raw_parts(rows, row_count as usize) }; + let current_re = unsafe { + std::slice::from_raw_parts(arena.current_re, arena.current_scalar_len as usize) + }; + let current_im = unsafe { + std::slice::from_raw_parts(arena.current_im, arena.current_scalar_len as usize) + }; + let amplitude_re = unsafe { + std::slice::from_raw_parts_mut(arena.amplitude_re, arena.amplitude_scalar_len as usize) + }; + let amplitude_im = unsafe { + std::slice::from_raw_parts_mut(arena.amplitude_im, arena.amplitude_scalar_len as usize) + }; + for row in rows { + for point in 0..point_count as usize { + let source = + row.parent0_component_base as usize * arena.point_stride as usize + point; + let destination = + row.amplitude_destination_id as usize * arena.point_stride as usize + point; + amplitude_re[destination] += current_re[source]; + amplitude_im[destination] += current_im[source]; + } + } + DIRECT_STATUS_OK + } + + struct ProbeResolver { + invalidations: Cell, + source_bindings: Cell, + } + + impl OnTheFlyPreparedExecutorResolver for ProbeResolver { + fn resolve(&self, _key: OnTheFlyExecutorKeyV1) -> RusticolResult { + Err(integrity("semantic resolver is unused by persisted test")) + } + + fn resolve_direct_executor( + &self, + direct_executor_id: u32, + role: DirectExecutorRole, + ) -> RusticolResult { + let handle = match (direct_executor_id, role) { + (1, DirectExecutorRole::Contribution) => DirectExecutorHandle::Contribution { + call: contribution_copy, + context: std::ptr::null(), + }, + (2, DirectExecutorRole::Finalization) => DirectExecutorHandle::Finalization { + call: finalize_noop, + context: std::ptr::null(), + }, + (3, DirectExecutorRole::Closure) => DirectExecutorHandle::Closure { + call: close_copy, + context: std::ptr::null(), + }, + _ => return Err(integrity("unexpected persisted test executor")), + }; + Ok(ResolvedOnTheFlyExecutor { + direct_executor_id, + handle, + parent_permutation: [0, 1], + packed_singleton_capable: true, + interaction_capability: None, + }) + } + + fn bind_union_source_program( + &self, + view: OnTheFlyUnionSourceBindingView<'_>, + ) -> RusticolResult> { + let selection = view + .selections + .first() + .ok_or_else(|| integrity("persisted test source selection is absent"))?; + self.source_bindings.set(self.source_bindings.get() + 1); + Ok(Box::new(ProbeBoundUnionSourceProgram { + value: selection.dispatch_variant_id as f64 + 1.0, + })) + } + + fn union_source_packed_singleton_capable(&self) -> bool { + true + } + + fn invalidate_row_tables(&self) -> RusticolResult<()> { + self.invalidations.set(self.invalidations.get() + 1); + Ok(()) + } + } + + fn all_flow_plan() -> DirectRecurrencePlan { + let mut parts = crate::recurrence::valid_direct_plan_parts_fixture(); + parts.strategy = RecurrenceStrategy::AllFlowUnion; + parts.retained_helicity_count = 2; + parts.runtime_helicity_contract_count = 1; + parts.runtime_helicity_variant_count = 2; + parts.sources[0].source_template_or_dispatch_domain = 0; + parts.row_groups[0].direct_executor_id = DIRECT_NONE_U32; + parts.replay_targets[0].helicity_map_count = 0; + parts.replay_helicity_map.clear(); + parts.amplitude_destinations[0].target_helicity_id_or_sentinel = DIRECT_NONE_U32; + parts.resolved_helicities = vec![ + DirectResolvedHelicityDescriptor { + source_state_start: 0, + source_selection_start: 0, + public_helicity_start: 0, + id: 0, + source_state_count: 1, + source_selection_count: 1, + public_helicity_count: 1, + selector_domain_id: 0, + }, + DirectResolvedHelicityDescriptor { + source_state_start: 1, + source_selection_start: 1, + public_helicity_start: 1, + id: 1, + source_state_count: 1, + source_selection_count: 1, + public_helicity_count: 1, + selector_domain_id: 0, + }, + ]; + parts.source_state_assignments = vec![ + crate::recurrence::DirectSourceStateAssignment { + source_slot: 0, + state_index: 0, + }, + crate::recurrence::DirectSourceStateAssignment { + source_slot: 0, + state_index: 1, + }, + ]; + parts.public_helicities = vec![-1, 1]; + parts.source_dispatch_variants = (0..2) + .map(|id| DirectSourceDispatchVariantDescriptor { + embedding_start: id * 2, + projection_start: id * 2, + source_row_id: 0, + dispatch_domain_id: 0, + runtime_variant_id: id as u32, + source_state_index: id as u32, + source_template_id: 0, + source_state_template_id: 0, + crossed_state_template_id: 0, + crossed_spin_state_class: id as i32, + direct_executor_id: 0, + crossing_exact_factor_id: 0, + embedding_count: 2, + projection_count: 2, + }) + .collect(); + parts.source_embeddings = vec![ + DirectSourceEmbeddingRow { + full_component: 0, + source_component_or_sentinel: 0, + exact_factor_id: 0, + }, + DirectSourceEmbeddingRow { + full_component: 1, + source_component_or_sentinel: 1, + exact_factor_id: 0, + }, + DirectSourceEmbeddingRow { + full_component: 0, + source_component_or_sentinel: 0, + exact_factor_id: 0, + }, + DirectSourceEmbeddingRow { + full_component: 1, + source_component_or_sentinel: 1, + exact_factor_id: 0, + }, + ]; + parts.source_projections = vec![ + DirectSourceProjectionRow { + source_component: 0, + full_component: 0, + }, + DirectSourceProjectionRow { + source_component: 1, + full_component: 1, + }, + DirectSourceProjectionRow { + source_component: 0, + full_component: 0, + }, + DirectSourceProjectionRow { + source_component: 1, + full_component: 1, + }, + ]; + parts.resolved_source_selections = vec![ + DirectResolvedSourceSelection { + source_slot: 0, + dispatch_variant_id: 0, + }, + DirectResolvedSourceSelection { + source_slot: 0, + dispatch_variant_id: 1, + }, + ]; + DirectRecurrencePlan::new(parts).unwrap() + } + + fn all_flow_dispatch(plan: &DirectRecurrencePlan) -> DirectHelicityDispatch { + DirectHelicityDispatch::new(DirectHelicityDispatchParts { + runtime_layout_digest: plan.runtime_layout_digest(), + source_row_count: 1, + contribution_row_count: 1, + finalization_row_count: 1, + closure_row_count: 1, + amplitude_destination_count: 1, + direct_executor_count: 4, + resolved_helicity_count: 2, + support_domains: vec![DirectHelicitySupportDomainDescriptor { + word_start: 0, + word_count: 1, + }], + support_words: vec![0b01], + row_groups: vec![ + DirectHelicityRowGroupDescriptor { + stage: 1, + role: DirectExecutorRole::Contribution, + direct_executor_id: 1, + row_start: 0, + row_count: 1, + support_domain_id: 0, + }, + DirectHelicityRowGroupDescriptor { + stage: 1, + role: DirectExecutorRole::Finalization, + direct_executor_id: 2, + row_start: 0, + row_count: 1, + support_domain_id: 0, + }, + DirectHelicityRowGroupDescriptor { + stage: 2, + role: DirectExecutorRole::Closure, + direct_executor_id: 3, + row_start: 0, + row_count: 1, + support_domain_id: 0, + }, + ], + dispatches: vec![ + DirectHelicityDispatchDescriptor { + resolved_helicity_id: 0, + group_id_start: 0, + group_id_count: 3, + }, + DirectHelicityDispatchDescriptor { + resolved_helicity_id: 1, + group_id_start: 3, + group_id_count: 0, + }, + ], + dispatch_group_ids: vec![0, 1, 2], + }) + .unwrap() + } + + fn all_flow_plan_with_reused_component_base() -> DirectRecurrencePlan { + let mut parts = all_flow_plan().into_parts(); + parts.current_arena_components = 2; + parts.currents[0].last_use = 0; + parts.currents[1].component_base = 0; + parts.contributions[0].destination_component_base = 0; + parts.finalizations[0].component_base = 0; + parts.closures[0].parent0_component_base = 0; + DirectRecurrencePlan::new(parts).unwrap() + } + + #[test] + fn persisted_census_distinguishes_reused_component_bases_by_liveness() { + let plan = all_flow_plan_with_reused_component_base(); + let index = PersistedCurrentIndexV1::new(&plan).unwrap(); + assert_eq!(index.source_current(0, "test source").unwrap(), 0); + assert_eq!( + index.live_current(&plan, 0, 0, "test stage zero").unwrap(), + 0 + ); + assert_eq!( + index.live_current(&plan, 1, 0, "test stage one").unwrap(), + 1 + ); + + let dispatch = all_flow_dispatch(&plan); + let family = build_family(&plan, &dispatch, 0).unwrap(); + assert_eq!(family.unique_current_count, 2); + assert_eq!(family.unique_current_component_count, 3); + } + + #[test] + fn selected_rows_rederive_init_without_dropping_certified_reuse() { + let mut groups = vec![ + PersistedFamilyGroupDraftV1 { + stage: 1, + direct_executor_id: DIRECT_NONE_U32, + rows: PersistedFamilyRowsDraftV1::Contribution(vec![contribution( + 8, + DIRECT_CONTRIBUTION_FLAG_CERTIFIED_REUSE, + )]), + }, + PersistedFamilyGroupDraftV1 { + stage: 1, + direct_executor_id: 2, + rows: PersistedFamilyRowsDraftV1::Contribution(vec![ + contribution(8, DIRECT_CONTRIBUTION_FLAG_INITIALIZE_DESTINATION), + contribution(12, 0), + ]), + }, + ]; + rederive_contribution_initialization(&mut groups); + let PersistedFamilyRowsDraftV1::Contribution(first) = &groups[0].rows else { + panic!("wrong row kind") + }; + let PersistedFamilyRowsDraftV1::Contribution(second) = &groups[1].rows else { + panic!("wrong row kind") + }; + assert_ne!(first[0].flags & DIRECT_CONTRIBUTION_FLAG_CERTIFIED_REUSE, 0); + assert_ne!( + first[0].flags & DIRECT_CONTRIBUTION_FLAG_INITIALIZE_DESTINATION, + 0 + ); + assert_eq!( + second[0].flags & DIRECT_CONTRIBUTION_FLAG_INITIALIZE_DESTINATION, + 0 + ); + assert_ne!( + second[1].flags & DIRECT_CONTRIBUTION_FLAG_INITIALIZE_DESTINATION, + 0 + ); + } + + #[test] + fn point_major_batch_input_builds_asymmetric_momentum_planes() { + let layout = test_layout(); + let mut workspace = PersistedHelicityWorkspaceV1::new(&layout, 2, false).unwrap(); + // [p0: s0=(1,2), s1=(3,4)], [p1: s0=(10,20), s1=(30,40)] + workspace + .refresh_inputs(&layout, &[1.0, 2.0, 3.0, 4.0, 10.0, 20.0, 30.0, 40.0], 2) + .unwrap(); + let stride = workspace.point_stride as usize; + let momenta = workspace.momenta.as_slice(); + assert_eq!(&momenta[0..2], &[1.0, 10.0]); + assert_eq!(&momenta[stride..stride + 2], &[2.0, 20.0]); + assert_eq!(&momenta[2 * stride..2 * stride + 2], &[-3.0, -30.0]); + assert_eq!(&momenta[3 * stride..3 * stride + 2], &[-4.0, -40.0]); + } + + #[test] + fn full_destination_clear_prevents_a_b_a_stale_outputs() { + let layout = test_layout(); + let mut workspace = PersistedHelicityWorkspaceV1::new(&layout, 1, true).unwrap(); + let input = [1.0, 2.0, 3.0, 4.0]; + + // H=A writes both destinations. + workspace.refresh_inputs(&layout, &input, 1).unwrap(); + workspace.amplitude_re.as_mut_slice()[0] = 7.0; + workspace.amplitude_re.as_mut_slice()[1] = 11.0; + + // H=B has no closure for destination 1; it must observe zero there. + workspace.refresh_inputs(&layout, &input, 1).unwrap(); + workspace.amplitude_re.as_mut_slice()[0] = 13.0; + assert_eq!(workspace.amplitude_re.as_slice(), &[13.0, 0.0]); + assert_eq!(workspace.amplitude_im.as_slice(), &[0.0, 0.0]); + + // Returning to A also starts from a completely clean dense C tile. + workspace.refresh_inputs(&layout, &input, 1).unwrap(); + workspace.amplitude_re.as_mut_slice()[1] = 17.0; + assert_eq!(workspace.amplitude_re.as_slice(), &[0.0, 17.0]); + } + + #[test] + fn unprofiled_execution_defers_diagnostic_counts_until_inspection() { + let plan = all_flow_plan(); + let dispatch = all_flow_dispatch(&plan); + let resolver = ProbeResolver { + invalidations: Cell::new(0), + source_bindings: Cell::new(0), + }; + let mut executor = PersistedHelicityFamilyExecutorV1::new(resolver); + executor.set_parameter_planes(&[4.0], &[-2.0]).unwrap(); + assert!(!executor.prepare(&plan, &dispatch, 0, 4, 1).unwrap()); + + let input = [10.0, 20.0, 30.0, 40.0]; + let tile = executor.execute_tile_unprofiled(&input, 1).unwrap(); + assert_eq!(tile.destination_re(0).unwrap(), &[1.0]); + assert_eq!( + executor.family.as_ref().unwrap().last_execution_report, + Some(PersistedHelicityFamilyExecutionReportV1::default()) + ); + + // Inspection reconstructs the exact logical census outside the warm + // execution path even though the unprofiled marker has no counters. + let census = executor.active_family_inspection_census().unwrap(); + assert_eq!(census.union_source_executor_call_groups, 1); + assert_eq!(census.union_contribution_executor_call_groups, 1); + assert_eq!(census.union_finalization_executor_call_groups, 1); + assert_eq!(census.union_closure_executor_call_groups, 1); + assert_eq!(census.union_source_rows, 1); + assert_eq!(census.union_contribution_rows, 1); + assert_eq!(census.union_finalization_rows, 1); + assert_eq!(census.union_closure_rows, 1); + + let (_, report) = executor.execute_tile_profiled(&input, 1).unwrap(); + assert!(report.cache_hit); + assert_eq!(report.source_calls, 1); + assert_eq!(report.contribution_calls, 1); + assert_eq!(report.finalization_calls, 1); + assert_eq!(report.closure_calls, 1); + assert_eq!( + executor.family.as_ref().unwrap().last_execution_report, + Some(report) + ); + assert_eq!(executor.resolver().source_bindings.get(), 1); + } + + #[test] + fn persisted_executor_retains_one_h_and_keeps_warm_execution_borrowed() { + let plan = all_flow_plan(); + let dispatch = all_flow_dispatch(&plan); + dispatch.validate_for_plan(&plan).unwrap(); + let resolver = ProbeResolver { + invalidations: Cell::new(0), + source_bindings: Cell::new(0), + }; + let mut executor = PersistedHelicityFamilyExecutorV1::new(resolver); + + executor.set_parameter_planes(&[3.0], &[-2.0]).unwrap(); + let parameter_pointer = executor.parameter_state.as_ptr(); + let parameter_capacity = executor.parameter_state.capacity(); + let parameter_version = executor.parameter_version; + executor.set_parameter_planes(&[3.0], &[-2.0]).unwrap(); + assert_eq!(executor.parameter_state.as_ptr(), parameter_pointer); + assert_eq!(executor.parameter_state.capacity(), parameter_capacity); + assert_eq!(executor.parameter_version, parameter_version); + executor.set_parameter_planes(&[4.0], &[-2.0]).unwrap(); + assert_ne!(executor.parameter_state.as_ptr(), parameter_pointer); + assert_eq!(executor.parameter_version, parameter_version + 1); + + let input = [10.0, 20.0, 30.0, 40.0]; + assert!(!executor.prepare(&plan, &dispatch, 0, 4, 1).unwrap()); + assert_eq!(executor.resolver().source_bindings.get(), 1); + assert_eq!( + executor.cache_census(), + PersistedHelicityFamilyCacheCensusV1 { + resolved_helicity_count: 2, + retained_family_count: 1, + retained_row_count: 3, + active_resolved_helicity_id: Some(0), + } + ); + let (tile, report) = executor.execute_tile_profiled(&input, 1).unwrap(); + assert_eq!(tile.resolved_helicity_id(), 0); + assert_eq!(tile.destination_count(), 1); + assert_eq!(tile.point_count(), 1); + assert_eq!(tile.point_stride(), 1); + assert_eq!(tile.destination_re(0).unwrap(), &[1.0]); + assert_eq!(tile.destination_im(0).unwrap(), &[0.0]); + assert_eq!( + report, + PersistedHelicityFamilyExecutionReportV1 { + cache_hit: false, + source_calls: 1, + source_rows: 1, + contribution_calls: 1, + contribution_rows: 1, + finalization_calls: 1, + finalization_rows: 1, + closure_calls: 1, + closure_rows: 1, + } + ); + let census = executor.active_family_inspection_census().unwrap(); + assert_eq!(census.query_count, 1); + assert!(census.union_unique_current_count > 0); + assert!(census.union_unique_current_count <= census.union_unique_current_component_count); + assert_eq!(census.union_source_rows, 1); + assert_eq!(census.union_contribution_rows, 1); + assert_eq!(census.union_finalization_rows, 1); + assert_eq!(census.union_closure_rows, 1); + assert_eq!(census.union_amplitude_destination_count, 1); + assert_eq!(census.union_source_executor_call_groups, 1); + assert_eq!(census.union_contribution_executor_call_groups, 1); + assert_eq!(census.union_finalization_executor_call_groups, 1); + assert_eq!(census.union_closure_executor_call_groups, 1); + assert_eq!(census.semantic_executor_binding_count, 4); + + assert!(executor.prepare(&plan, &dispatch, 0, 4, 1).unwrap()); + assert_eq!(executor.resolver().source_bindings.get(), 1); + let (tile, report) = executor.execute_tile_profiled(&input, 1).unwrap(); + assert!(report.cache_hit); + assert_eq!(tile.destination_re(0).unwrap(), &[1.0]); + + // H=1 has exact source dispatch but no live non-source rows. Replacing + // H=0 still retains one family and leaves the dense destination zero. + assert!(!executor.prepare(&plan, &dispatch, 1, 4, 1).unwrap()); + assert_eq!(executor.resolver().source_bindings.get(), 2); + assert_eq!( + executor.cache_census(), + PersistedHelicityFamilyCacheCensusV1 { + resolved_helicity_count: 2, + retained_family_count: 1, + retained_row_count: 0, + active_resolved_helicity_id: Some(1), + } + ); + let tile = executor.execute_tile_unprofiled(&input, 1).unwrap(); + assert_eq!(tile.destination_re(0).unwrap(), &[0.0]); + assert_eq!(tile.destination_im(0).unwrap(), &[0.0]); + let census = executor.active_family_inspection_census().unwrap(); + assert_eq!(census.union_source_rows, 1); + assert_eq!(census.union_contribution_rows, 0); + assert_eq!(census.union_finalization_rows, 0); + assert_eq!(census.union_closure_rows, 0); + assert_eq!(census.semantic_executor_binding_count, 1); + + // A -> B -> A is a cold switch, but never multiplies retained arenas. + assert!(!executor.prepare(&plan, &dispatch, 0, 4, 1).unwrap()); + assert_eq!(executor.resolver().source_bindings.get(), 3); + assert_eq!(executor.cache_census().retained_family_count, 1); + let tile = executor.execute_tile_unprofiled(&input, 1).unwrap(); + assert_eq!(tile.destination_re(0).unwrap(), &[1.0]); + assert!(executor.resolver().invalidations.get() >= 2); + + executor.clear_families().unwrap(); + assert_eq!( + executor.cache_census(), + PersistedHelicityFamilyCacheCensusV1::default() + ); + assert!(executor.active_family_inspection_census().is_none()); + } +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/recurrence/on_the_fly/probe_guard.rs b/pyamplicol/rust/crates/rusticol-core/src/recurrence/on_the_fly/probe_guard.rs new file mode 100644 index 0000000000..e14c466acc --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/recurrence/on_the_fly/probe_guard.rs @@ -0,0 +1,99 @@ +// SPDX-License-Identifier: 0BSD + +#![allow(dead_code)] // Poison hooks are exercised only by the opt-in probe harness. + +//! Feature-only poison guard for the genuine on-the-fly artifact probe. + +use super::*; +use std::cell::RefCell; + +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub(crate) struct OnTheFlyForbiddenWorkCountsV1 { + pub(crate) direct_plan_load_attempts: u32, + pub(crate) direct_plan_decode_attempts: u32, + pub(crate) direct_plan_materialization_attempts: u32, + pub(crate) established_builder_attempts: u32, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) enum OnTheFlyForbiddenWorkV1 { + DirectPlanLoad, + DirectPlanDecode, + DirectPlanMaterialization, + EstablishedBuilder, +} + +thread_local! { + static ACTIVE_PROBE: RefCell> = const { + RefCell::new(None) + }; +} + +pub(crate) struct OnTheFlyForbiddenWorkGuardV1 { + active: bool, +} + +impl OnTheFlyForbiddenWorkGuardV1 { + pub(crate) fn begin() -> RusticolResult { + ACTIVE_PROBE.with(|slot| { + let mut slot = slot.borrow_mut(); + if slot.is_some() { + return Err(invalid("a forbidden-work probe is already active")); + } + *slot = Some(OnTheFlyForbiddenWorkCountsV1::default()); + Ok(Self { active: true }) + }) + } + + pub(crate) fn finish(mut self) -> RusticolResult { + self.active = false; + ACTIVE_PROBE.with(|slot| { + slot.borrow_mut() + .take() + .ok_or_else(|| integrity("forbidden-work probe state disappeared")) + }) + } +} + +impl Drop for OnTheFlyForbiddenWorkGuardV1 { + fn drop(&mut self) { + if self.active { + ACTIVE_PROBE.with(|slot| { + slot.borrow_mut().take(); + }); + } + } +} + +pub(crate) fn reject_forbidden_work_if_probed(kind: OnTheFlyForbiddenWorkV1) -> RusticolResult<()> { + ACTIVE_PROBE.with(|slot| { + let mut slot = slot.borrow_mut(); + let Some(counts) = slot.as_mut() else { + return Ok(()); + }; + let (counter, label) = match kind { + OnTheFlyForbiddenWorkV1::DirectPlanLoad => ( + &mut counts.direct_plan_load_attempts, + "DirectRecurrencePlan loading", + ), + OnTheFlyForbiddenWorkV1::DirectPlanDecode => ( + &mut counts.direct_plan_decode_attempts, + "DirectRecurrencePlan deserialization", + ), + OnTheFlyForbiddenWorkV1::DirectPlanMaterialization => ( + &mut counts.direct_plan_materialization_attempts, + "DirectRecurrencePlan materialization", + ), + OnTheFlyForbiddenWorkV1::EstablishedBuilder => ( + &mut counts.established_builder_attempts, + "established global recurrence construction", + ), + }; + *counter = counter + .checked_add(1) + .ok_or_else(|| integrity("forbidden-work attempt counter overflowed"))?; + Err(invalid(format!( + "{label} is poisoned inside the genuine on-the-fly artifact probe" + ))) + }) +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/recurrence/on_the_fly/projection.rs b/pyamplicol/rust/crates/rusticol-core/src/recurrence/on_the_fly/projection.rs new file mode 100644 index 0000000000..0b7392e79e --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/recurrence/on_the_fly/projection.rs @@ -0,0 +1,604 @@ +// SPDX-License-Identifier: 0BSD + +//! Query-local counterpart of the established topology-replay color projection. +//! +//! The compact builder may discover multiple dynamic-color witnesses for one +//! color-erased current. They are aliases only when their selected-query +//! contribution and closure domains form the same complete rectangular proof +//! used by the materialized builder. This pass proves that condition before +//! retaining one representative; otherwise it leaves the trace unprojected. + +use super::*; +use crate::recurrence::DynamicLCColorStateId; +use crate::recurrence::construct::current_key_with_dynamic_color; + +#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] +struct ProjectedCurrentIdentity { + color_erased_key: CurrentCoreKey, + source_builder_id: Option, +} + +#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] +struct ProjectedContributionIdentity { + destination_projection_id: u32, + transition_template_id: u32, + parent_projection_ids: [u32; 2], + parent_state_template_ids: Box<[u32]>, + parent_momenta: Box<[CanonicalMomentumLinearForm]>, + result_state_template_id: u32, + quantum_flow_witness_id: u32, + runtime_coupling_binding_digest: SemanticDigest, + output_projection_id: u32, + exact_factor: ExactComplexRational, +} + +#[derive(Clone, Debug)] +struct ProjectedContribution { + identity: ProjectedContributionIdentity, + representative_key: ContributionKey, + destination_builder_ids: BTreeSet, + builder_parent_tuples: BTreeSet<[u32; 2]>, +} + +#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] +struct ProjectedClosureIdentity { + closure_template_id: u32, + quantum_flow_template_id: Option, + parent_projection_ids: [u32; 2], + exact_factor: ExactComplexRational, + component_coefficients: Box<[ExactComplexRational]>, +} + +#[derive(Debug)] +struct ProjectedClosure { + identity: ProjectedClosureIdentity, + representative_order: ProjectedClosureRepresentativeOrder, + representative_key: PendingClosureKey, + builder_parent_tuples: BTreeSet<[u32; 2]>, + pairing_lineages: Vec, +} + +#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] +struct ProjectedClosureRepresentativeOrder { + closure_template_id: u32, + quantum_flow_template_id: Option, + parent_keys: Box<[CurrentCoreKey]>, + color_witness_term_id: LCColorWitnessTermId, +} + +fn closure_representative_order( + pending: &[PendingCurrent], + closure: &PendingClosure, +) -> RusticolResult { + let parent_keys = closure + .key + .parent_current_ids + .iter() + .map(|id| { + pending + .get(*id as usize) + .map(|current| current.key.clone()) + .ok_or_else(|| integrity("projected closure representative parent is absent")) + }) + .collect::>>()? + .into_boxed_slice(); + Ok(ProjectedClosureRepresentativeOrder { + closure_template_id: closure.key.closure_template_id, + quantum_flow_template_id: closure.key.quantum_flow_template_id, + parent_keys, + color_witness_term_id: closure.key.color_witness_term_id, + }) +} + +pub(super) struct QueryLocalColorProjection { + pub(super) currents: Vec, + pub(super) closures: Vec, +} + +fn parent_rectangle_is_complete( + parent_projection_ids: [u32; 2], + builder_parent_tuples: &BTreeSet<[u32; 2]>, + old_to_projection: &BTreeMap, + projection_members: &[Vec], +) -> RusticolResult { + let expected = parent_projection_ids + .iter() + .try_fold(1usize, |count, projection_id| { + count + .checked_mul( + projection_members + .get(*projection_id as usize) + .ok_or_else(|| integrity("projected parent class is absent"))? + .len(), + ) + .ok_or_else(|| invalid("projected parent-product count exceeds usize")) + })?; + if expected != builder_parent_tuples.len() { + return Ok(false); + } + Ok(builder_parent_tuples.iter().all(|tuple| { + tuple + .iter() + .zip(parent_projection_ids.iter()) + .all(|(builder_id, projection_id)| { + old_to_projection.get(builder_id) == Some(projection_id) + }) + })) +} + +fn contribution_identity( + destination_projection_id: u32, + key: &PendingContributionKey, + factor: ExactComplexRational, + old_to_projection: &BTreeMap, +) -> RusticolResult { + let parent_projection_ids = key + .parent_current_ids + .map(|id| { + old_to_projection + .get(&id) + .copied() + .ok_or_else(|| integrity("projected contribution has a dead parent")) + }) + .into_iter() + .collect::>>()? + .try_into() + .map_err(|_| integrity("projected contribution is not binary"))?; + Ok(ProjectedContributionIdentity { + destination_projection_id, + transition_template_id: key.key.transition_template_id(), + parent_projection_ids, + parent_state_template_ids: key.key.parent_state_template_ids().into(), + parent_momenta: key.key.parent_momenta().into(), + result_state_template_id: key.key.result_state_template_id(), + quantum_flow_witness_id: key.key.quantum_flow_witness_id(), + runtime_coupling_binding_digest: key.key.runtime_coupling_binding_digest(), + output_projection_id: key.key.output_projection_id(), + exact_factor: factor, + }) +} + +pub(super) fn project_query_local_color_aliases( + pending: &[PendingCurrent], + pending_closures: &[PendingClosure], + live: &BTreeSet, +) -> RusticolResult> { + let canonical_color_id = DynamicLCColorStateId::from_interner(0); + let mut identity_to_projection = BTreeMap::::new(); + let mut old_to_projection = BTreeMap::::new(); + let mut projection_members = Vec::>::new(); + for old_id in live.iter().copied() { + let current = pending + .get(old_id as usize) + .ok_or_else(|| integrity("live query-local current is absent"))?; + let identity = ProjectedCurrentIdentity { + color_erased_key: current_key_with_dynamic_color(¤t.key, canonical_color_id)?, + source_builder_id: (current.key.node_kind() == RecurrenceNodeKind::Source) + .then_some(old_id), + }; + let projection_id = if let Some(id) = identity_to_projection.get(&identity).copied() { + id + } else { + let id = checked_u32(projection_members.len(), "query-local projection class")?; + projection_members.push(Vec::new()); + identity_to_projection.insert(identity, id); + id + }; + projection_members[projection_id as usize].push(old_id); + old_to_projection.insert(old_id, projection_id); + } + if projection_members.iter().all(|members| members.len() == 1) { + return Ok(None); + } + + let mut contributions = BTreeMap::::new(); + for (old_id, projection_id) in &old_to_projection { + for (key, factor) in &pending[*old_id as usize].contributions { + if factor.is_zero() { + continue; + } + let identity = contribution_identity(*projection_id, key, *factor, &old_to_projection)?; + match contributions.entry(identity.clone()) { + std::collections::btree_map::Entry::Vacant(entry) => { + entry.insert(ProjectedContribution { + identity, + representative_key: key.key.clone(), + destination_builder_ids: BTreeSet::from([*old_id]), + builder_parent_tuples: BTreeSet::from([key.parent_current_ids]), + }); + } + std::collections::btree_map::Entry::Occupied(mut entry) => { + let row = entry.get_mut(); + row.destination_builder_ids.insert(*old_id); + if !row.builder_parent_tuples.insert(key.parent_current_ids) { + return Ok(None); + } + } + } + } + } + for row in contributions.values() { + let destination_members = projection_members + .get(row.identity.destination_projection_id as usize) + .ok_or_else(|| integrity("projected contribution destination class is absent"))?; + if row.destination_builder_ids.len() != destination_members.len() + || !destination_members + .iter() + .all(|builder_id| row.destination_builder_ids.contains(builder_id)) + { + return Ok(None); + } + if !parent_rectangle_is_complete( + row.identity.parent_projection_ids, + &row.builder_parent_tuples, + &old_to_projection, + &projection_members, + )? { + return Ok(None); + } + } + + let mut closures = BTreeMap::::new(); + for closure in pending_closures { + let parent_projection_ids = closure + .key + .parent_current_ids + .map(|id| { + old_to_projection + .get(&id) + .copied() + .ok_or_else(|| integrity("projected closure has a dead parent")) + }) + .into_iter() + .collect::>>()? + .try_into() + .map_err(|_| integrity("projected closure is not binary"))?; + let identity = ProjectedClosureIdentity { + closure_template_id: closure.key.closure_template_id, + quantum_flow_template_id: closure.key.quantum_flow_template_id, + parent_projection_ids, + exact_factor: closure.factor, + component_coefficients: closure.component_coefficients.clone(), + }; + let representative_order = closure_representative_order(pending, closure)?; + match closures.entry(identity.clone()) { + std::collections::btree_map::Entry::Vacant(entry) => { + entry.insert(ProjectedClosure { + identity, + representative_order, + representative_key: closure.key.clone(), + builder_parent_tuples: BTreeSet::from([closure.key.parent_current_ids]), + pairing_lineages: try_clone_pairing_lineages(&closure.pairing_lineages)?, + }); + } + std::collections::btree_map::Entry::Occupied(mut entry) => { + if !entry + .get_mut() + .builder_parent_tuples + .insert(closure.key.parent_current_ids) + { + return Ok(None); + } + // Pairing lineage is proof owned by the canonical root-bearing + // representative, not an additive property of color aliases. + // Keep the same semantic/current ordering regardless of input + // enumeration and never union a later alias's Wick lineage. + if representative_order < entry.get().representative_order { + let projected = entry.get_mut(); + projected.representative_order = representative_order; + projected.representative_key = closure.key.clone(); + projected.pairing_lineages = + try_clone_pairing_lineages(&closure.pairing_lineages)?; + } + } + } + } + for row in closures.values() { + if !parent_rectangle_is_complete( + row.identity.parent_projection_ids, + &row.builder_parent_tuples, + &old_to_projection, + &projection_members, + )? { + return Ok(None); + } + } + + let mut projected_currents = Vec::with_capacity(projection_members.len()); + for (projection_index, members) in projection_members.iter().enumerate() { + let projection_id = checked_u32(projection_index, "projected query-local current ID")?; + let representative_id = *members + .first() + .ok_or_else(|| integrity("query-local projection class is empty"))?; + let representative = &pending[representative_id as usize]; + let mut projected = PendingCurrent { + key: representative.key.clone(), + source_factor: representative.source_factor, + contributions: BTreeMap::new(), + pairing_lineages: Vec::new(), + stage: representative.stage, + reflection: crate::recurrence::construct::CurrentReflection::Unavailable, + reflection_certificate: None, + }; + for member in members { + extend_pairing_lineages( + &mut projected.pairing_lineages, + &pending[*member as usize].pairing_lineages, + )?; + } + for row in contributions + .values() + .filter(|row| row.identity.destination_projection_id == projection_id) + { + if !row + .destination_builder_ids + .iter() + .all(|builder_id| old_to_projection.get(builder_id) == Some(&projection_id)) + { + return Err(integrity( + "projected contribution destinations cross projection classes", + )); + } + let identity = &row.identity; + let key = ContributionKey::new( + identity.transition_template_id, + identity.parent_projection_ids.to_vec(), + identity.parent_state_template_ids.to_vec(), + identity.parent_momenta.to_vec(), + identity.result_state_template_id, + identity.quantum_flow_witness_id, + row.representative_key.color_witness_term_id(), + identity.runtime_coupling_binding_digest, + identity.output_projection_id, + )?; + projected.contributions.insert( + PendingContributionKey { + parent_current_ids: identity.parent_projection_ids, + key, + }, + identity.exact_factor, + ); + } + projected_currents.push(projected); + } + + let projected_closures = closures + .into_values() + .map(|row| { + Ok(PendingClosure { + key: PendingClosureKey { + closure_template_id: row.identity.closure_template_id, + quantum_flow_template_id: row.identity.quantum_flow_template_id, + parent_current_ids: row.identity.parent_projection_ids, + color_witness_term_id: row.representative_key.color_witness_term_id, + }, + factor: row.identity.exact_factor, + component_coefficients: row.identity.component_coefficients, + pairing_lineages: row.pairing_lineages, + }) + }) + .collect::>>()?; + Ok(Some(QueryLocalColorProjection { + currents: projected_currents, + closures: projected_closures, + })) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn digest(byte: u8) -> SemanticDigest { + SemanticDigest::new([byte; 32]).unwrap() + } + + fn current(state: u32, color: u32, support: u32) -> PendingCurrent { + PendingCurrent { + key: CurrentCoreKey::new( + digest(1), + RecurrenceNodeKind::Current, + state, + DynamicLCColorStateId::from_interner(color), + vec![support], + CanonicalMomentumLinearForm::new(vec![MomentumTerm { + source_slot: support, + coefficient: 1, + }]) + .unwrap(), + CurrentHelicityIdentity::topology_replay( + 0, + vec![SourceStateAssignment::new(support, 0)], + ) + .unwrap(), + vec![state as i32], + 0, + vec![0], + CurrentSourceBinding::None, + None, + ) + .unwrap(), + source_factor: None, + contributions: BTreeMap::new(), + pairing_lineages: vec![PendingPairingLineage { + completed_pairs: Vec::new(), + unmatched_endpoint: None, + }], + stage: 0, + reflection: crate::recurrence::construct::CurrentReflection::Unavailable, + reflection_certificate: None, + } + } + + fn closure(alias_parent: u32) -> PendingClosure { + closure_with_lineage(alias_parent, Vec::new()) + } + + fn closure_with_lineage(alias_parent: u32, completed_pairs: Vec<[u32; 2]>) -> PendingClosure { + PendingClosure { + key: PendingClosureKey { + closure_template_id: 7, + quantum_flow_template_id: None, + parent_current_ids: [0, alias_parent], + color_witness_term_id: LCColorWitnessTermId::new(9, 0), + }, + factor: ExactComplexRational::ONE, + component_coefficients: vec![ExactComplexRational::ONE].into_boxed_slice(), + pairing_lineages: vec![PendingPairingLineage { + completed_pairs, + unmatched_endpoint: None, + }], + } + } + + fn add_contribution( + pending: &mut [PendingCurrent], + destination: u32, + parents: [u32; 2], + witness_ordinal: u32, + factor: ExactComplexRational, + ) { + let destination_key = &pending[destination as usize].key; + let parent_keys = parents.map(|id| &pending[id as usize].key); + let key = ContributionKey::new( + 11, + parents.to_vec(), + parent_keys + .iter() + .map(|key| key.current_state_template_id()) + .collect(), + parent_keys + .iter() + .map(|key| key.momentum().clone()) + .collect(), + destination_key.current_state_template_id(), + 3, + LCColorWitnessTermId::new(5, witness_ordinal), + digest(7), + 13, + ) + .unwrap(); + assert!( + pending[destination as usize] + .contributions + .insert( + PendingContributionKey { + parent_current_ids: parents, + key, + }, + factor, + ) + .is_none() + ); + } + + #[test] + fn query_local_projection_requires_a_complete_parent_rectangle() { + let pending = vec![current(1, 0, 0), current(2, 1, 1), current(2, 2, 1)]; + let live = BTreeSet::from([0, 1, 2]); + assert!( + project_query_local_color_aliases(&pending, &[closure(1)], &live) + .unwrap() + .is_none(), + "one of two alias parents is not a complete rectangular proof", + ); + + let projected = + project_query_local_color_aliases(&pending, &[closure(1), closure(2)], &live) + .unwrap() + .expect("complete alias rectangle projects"); + assert_eq!(projected.currents.len(), 2); + assert_eq!(projected.closures.len(), 1); + assert_eq!(projected.closures[0].key.parent_current_ids, [0, 1]); + } + + #[test] + fn query_local_projection_requires_each_contribution_on_every_destination_alias() { + let mut pending = vec![current(1, 0, 0), current(2, 1, 1), current(2, 2, 1)]; + add_contribution(&mut pending, 1, [0, 0], 0, ExactComplexRational::ONE); + add_contribution( + &mut pending, + 2, + [0, 0], + 1, + ExactComplexRational::ONE.checked_neg().unwrap(), + ); + let live = BTreeSet::from([0, 1, 2]); + + assert!( + project_query_local_color_aliases(&pending, &[], &live) + .unwrap() + .is_none(), + "reciprocal destination aliases with distinct exact fan-ins are not one equation", + ); + } + + #[test] + fn query_local_projection_retains_a_complete_destination_and_parent_domain() { + let mut pending = vec![ + current(1, 0, 0), + current(2, 1, 1), + current(2, 2, 1), + current(3, 3, 2), + current(3, 4, 2), + ]; + add_contribution(&mut pending, 3, [0, 1], 0, ExactComplexRational::ONE); + add_contribution(&mut pending, 4, [0, 2], 1, ExactComplexRational::ONE); + let live = BTreeSet::from([0, 1, 2, 3, 4]); + + let projected = project_query_local_color_aliases(&pending, &[], &live) + .unwrap() + .expect("complete destination and parent domains project"); + assert_eq!(projected.currents.len(), 3); + assert_eq!(projected.currents[2].contributions.len(), 1); + } + + #[test] + fn query_local_projection_ignores_poison_outside_the_selected_live_set() { + let base = vec![current(1, 0, 0), current(2, 1, 1), current(2, 2, 1)]; + let live = BTreeSet::from([0, 1, 2]); + let expected = project_query_local_color_aliases(&base, &[closure(1), closure(2)], &live) + .unwrap() + .unwrap(); + let mut poisoned = base; + poisoned.push(current(99, u32::MAX, 99)); + let observed = + project_query_local_color_aliases(&poisoned, &[closure(1), closure(2)], &live) + .unwrap() + .unwrap(); + assert_eq!(observed.currents.len(), expected.currents.len()); + assert_eq!(observed.closures.len(), expected.closures.len()); + assert!( + observed + .currents + .iter() + .all(|row| row.key.current_state_template_id() != 99) + ); + } + + #[test] + fn projected_pairing_owner_uses_semantic_representative_not_input_order() { + let pending = vec![current(1, 0, 0), current(2, 2, 1), current(2, 1, 1)]; + let live = BTreeSet::from([0, 1, 2]); + for closures in [ + vec![ + closure_with_lineage(1, vec![[0, 5]]), + closure_with_lineage(2, vec![[0, 3]]), + ], + vec![ + closure_with_lineage(2, vec![[0, 3]]), + closure_with_lineage(1, vec![[0, 5]]), + ], + ] { + let projected = project_query_local_color_aliases(&pending, &closures, &live) + .unwrap() + .expect("complete alias rectangle projects"); + assert_eq!( + projected.closures[0].pairing_lineages, + vec![PendingPairingLineage { + completed_pairs: vec![[0, 3]], + unmatched_endpoint: None, + }] + ); + } + } +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/recurrence/on_the_fly/public_query.rs b/pyamplicol/rust/crates/rusticol-core/src/recurrence/on_the_fly/public_query.rs new file mode 100644 index 0000000000..c33a6fca4c --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/recurrence/on_the_fly/public_query.rs @@ -0,0 +1,955 @@ +// SPDX-License-Identifier: 0BSD + +use super::source_seed::validate_permutation; +use super::*; + +/// One selected source state in a decoded query. Source slots are always in +/// construction order after applying the seed's authenticated gather map. +#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] +pub(crate) struct OnTheFlySelectedSourceV1 { + pub(crate) source_slot: u32, + pub(crate) state_index: u32, + pub(crate) public_helicity: i32, +} + +/// One already-decoded public LC selector. Slots use public external order; +/// [`DecodedLcQueryV1::new`] authenticates and maps them to construction order. +#[derive(Clone, Debug, Eq, PartialEq)] +pub(crate) enum OnTheFlyLcSelectorV1 { + Singlet, + SingleTrace { public_word: Box<[u32]> }, + OpenLines { public_blocks: Box<[Box<[u32]>]> }, +} + +impl OnTheFlyLcSelectorV1 { + pub(crate) fn single_trace(public_word: Vec) -> Self { + Self::SingleTrace { + public_word: public_word.into_boxed_slice(), + } + } + + pub(crate) fn open_lines(public_blocks: Vec>) -> Self { + Self::OpenLines { + public_blocks: public_blocks + .into_iter() + .map(Vec::into_boxed_slice) + .collect::>() + .into_boxed_slice(), + } + } +} + +/// One exact physical LC selector. It contains only the selected structural +/// values; selector identity is their digest and never a process-wide flow ID. +#[derive(Clone, Debug, Eq, PartialEq)] +pub(crate) struct DecodedLcQueryV1 { + pub(super) seed_digest: SemanticDigest, + pub(super) external_permutation: Box<[u32]>, + pub(super) selected_sources: Box<[OnTheFlySelectedSourceV1]>, + pub(super) target_components: Box<[LCColorComponent]>, + pub(super) closure_anchor_slot: u32, + pub(super) selector_digest: SemanticDigest, + pub(super) semantic_digest: SemanticDigest, +} + +impl DecodedLcQueryV1 { + pub(crate) fn new( + seed: &OnTheFlyProcessSeedV1, + external_permutation: Vec, + public_helicities: &[i32], + selector: OnTheFlyLcSelectorV1, + ) -> RusticolResult { + if external_permutation.as_slice() != seed.external_permutation.as_ref() { + return Err(integrity( + "query external permutation differs from its compact seed", + )); + } + validate_permutation( + &external_permutation, + seed.source_anchors.len(), + "query external permutation", + )?; + if public_helicities.len() != seed.source_anchors.len() { + return Err(invalid( + "public-helicity selector does not cover every external slot exactly once", + )); + } + let inverse_permutation = inverse_permutation(&external_permutation)?; + let selected_sources = selected_sources(seed, public_helicities)?; + let decoded = decode_selector(seed, &inverse_permutation, selector)?; + let seed_digest = seed.semantic_digest(); + let mut query = Self { + seed_digest, + external_permutation: external_permutation.into_boxed_slice(), + selected_sources: selected_sources.into_boxed_slice(), + target_components: decoded.target_components.into_boxed_slice(), + closure_anchor_slot: decoded.closure_anchor_slot, + selector_digest: decoded.selector_digest, + semantic_digest: SemanticDigest::new([1; 32])?, + }; + query.semantic_digest = query.compute_digest()?; + Ok(query) + } + + fn compute_digest(&self) -> RusticolResult { + let mut hash = Sha256::new(); + hash.update(b"pyamplicol-on-the-fly-decoded-lc-query-v2\0"); + hash_digest(&mut hash, self.seed_digest); + hash_digest(&mut hash, self.selector_digest); + hash_len( + &mut hash, + self.external_permutation.len(), + "query external permutation", + )?; + for source_slot in &self.external_permutation { + hash.update(source_slot.to_le_bytes()); + } + hash.update(self.closure_anchor_slot.to_le_bytes()); + hash_len(&mut hash, self.selected_sources.len(), "selected sources")?; + for selected in &self.selected_sources { + hash.update(selected.source_slot.to_le_bytes()); + hash.update(selected.state_index.to_le_bytes()); + hash.update(selected.public_helicity.to_le_bytes()); + } + hash_len(&mut hash, self.target_components.len(), "target components")?; + for component in &self.target_components { + hash.update([component.kind() as u8]); + hash_len(&mut hash, component.source_slots().len(), "component word")?; + for slot in component.source_slots() { + hash.update(slot.to_le_bytes()); + } + } + final_digest(hash) + } + + pub(crate) const fn semantic_digest(&self) -> SemanticDigest { + self.semantic_digest + } + + pub(crate) const fn process_seed_digest(&self) -> SemanticDigest { + self.seed_digest + } + + fn with_closure_anchor_slot(mut self, closure_anchor_slot: u32) -> RusticolResult { + if !self + .selected_sources + .iter() + .any(|source| source.source_slot == closure_anchor_slot) + { + return Err(integrity( + "certified query closure anchor is outside its selected source domain", + )); + } + if self.closure_anchor_slot != closure_anchor_slot { + self.closure_anchor_slot = closure_anchor_slot; + self.semantic_digest = self.compute_digest()?; + } + Ok(self) + } +} + +/// Authenticated private choice of closure ancestry for one prepared process. +/// +/// The ordinary selector endpoint remains authoritative for every ineligible +/// generic, open-line, singlet, or heterogeneous shape. Residual/cross is a +/// downstream color-metric classification, so eligible pure-adjoint trace +/// groups use the cyclic minimum regardless of that grouping. A prepared +/// grammar enables it only after proving the same homogeneous adjoint state +/// and direct binary closure contract used by recurrence v4 sectors. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(super) struct OnTheFlyClosureAnchorPolicyV1 { + cyclic_trace_minimum_source_slot: Option, +} + +impl OnTheFlyClosureAnchorPolicyV1 { + pub(super) const fn selector_endpoint() -> Self { + Self { + cyclic_trace_minimum_source_slot: None, + } + } + + pub(super) const fn certified_cyclic_minimum(source_slot: u32) -> Self { + Self { + cyclic_trace_minimum_source_slot: Some(source_slot), + } + } + + pub(super) fn closure_anchor_for_components( + self, + seed: &OnTheFlyProcessSeedV1, + components: &[LCColorComponent], + ) -> Option { + let selector_endpoint = physical_lc_selector_closure_anchor(seed, components)?; + let Some(minimum) = self.cyclic_trace_minimum_source_slot else { + return Some(selector_endpoint); + }; + let [component] = components else { + return Some(selector_endpoint); + }; + if component.kind() != LCColorComponentKind::Trace + || component.source_slots().len() != seed.source_anchors.len() + || seed + .source_anchors + .iter() + .any(|anchor| anchor.color_role != OnTheFlyExternalColorRoleV1::Adjoint) + || component.source_slots().iter().copied().min() != Some(minimum) + { + return Some(selector_endpoint); + } + Some(minimum) + } + + pub(super) fn canonicalize_query( + self, + seed: &OnTheFlyProcessSeedV1, + query: DecodedLcQueryV1, + ) -> RusticolResult { + let closure_anchor_slot = self + .closure_anchor_for_components(seed, &query.target_components) + .ok_or_else(|| integrity("decoded query lost its physical selector shape"))?; + query.with_closure_anchor_slot(closure_anchor_slot) + } + + pub(super) fn authenticate_canonical_query( + self, + seed: &OnTheFlyProcessSeedV1, + query: &DecodedLcQueryV1, + ) -> RusticolResult<()> { + if &self.canonicalize_query(seed, query.clone())? != query { + return Err(integrity( + "decoded query was not canonicalized at its prepared-grammar boundary", + )); + } + Ok(()) + } + + pub(super) fn certifies_cyclic_trace_reflection( + self, + seed: &OnTheFlyProcessSeedV1, + query: &DecodedLcQueryV1, + ) -> bool { + let Some(minimum) = self.cyclic_trace_minimum_source_slot else { + return false; + }; + let [component] = query.target_components.as_ref() else { + return false; + }; + query.seed_digest == seed.semantic_digest() + && query.closure_anchor_slot == minimum + && component.kind() == LCColorComponentKind::Trace + && component.source_slots().len() == seed.source_anchors.len() + && component.source_slots().iter().copied().min() == Some(minimum) + && seed + .source_anchors + .iter() + .all(|anchor| anchor.color_role == OnTheFlyExternalColorRoleV1::Adjoint) + && self.closure_anchor_for_components(seed, &query.target_components) == Some(minimum) + } +} + +struct DecodedSelector { + target_components: Vec, + closure_anchor_slot: u32, + selector_digest: SemanticDigest, +} + +fn selected_sources( + seed: &OnTheFlyProcessSeedV1, + public_helicities: &[i32], +) -> RusticolResult> { + seed.source_anchors + .iter() + .map(|anchor| { + let public_slot = *seed + .external_permutation + .get(anchor.source_slot as usize) + .ok_or_else(|| integrity("external permutation source slot is absent"))?; + let public_helicity = public_helicities[public_slot as usize]; + let matches = anchor + .states + .iter() + .filter(|state| state.public_helicity == public_helicity) + .collect::>(); + let [state] = matches.as_slice() else { + return Err(invalid(format!( + "source slot {} has {} states for public helicity {public_helicity}, expected exactly one", + anchor.source_slot, + matches.len(), + ))); + }; + Ok(OnTheFlySelectedSourceV1 { + source_slot: anchor.source_slot, + state_index: state.state_index, + public_helicity, + }) + }) + .collect() +} + +fn inverse_permutation(permutation: &[u32]) -> RusticolResult> { + let mut inverse = vec![MISSING_U32; permutation.len()]; + for (construction_slot, public_slot) in permutation.iter().copied().enumerate() { + let entry = inverse + .get_mut(public_slot as usize) + .ok_or_else(|| invalid("external permutation public slot is out of bounds"))?; + if *entry != MISSING_U32 { + return Err(invalid("external permutation repeats a public slot")); + } + *entry = checked_u32(construction_slot, "construction source slot")?; + } + if inverse.contains(&MISSING_U32) { + return Err(invalid("external permutation omits a public slot")); + } + Ok(inverse) +} + +fn map_public_word(word: &[u32], inverse: &[u32], label: &str) -> RusticolResult> { + if word.is_empty() { + return Err(invalid(format!("{label} is empty"))); + } + word.iter() + .map(|public_slot| { + inverse + .get(*public_slot as usize) + .copied() + .ok_or_else(|| invalid(format!("{label} public slot is out of bounds"))) + }) + .collect() +} + +fn colored_source_slots(seed: &OnTheFlyProcessSeedV1) -> BTreeSet { + seed.source_anchors + .iter() + .filter(|anchor| anchor.color_role.is_colored()) + .map(|anchor| anchor.source_slot) + .collect() +} + +fn require_exact_colored_coverage( + seed: &OnTheFlyProcessSeedV1, + words: &[Vec], +) -> RusticolResult<()> { + let mut observed = BTreeSet::new(); + for slot in words.iter().flatten().copied() { + if !observed.insert(slot) { + return Err(invalid("decoded selector repeats a colored source slot")); + } + } + if observed != colored_source_slots(seed) { + return Err(invalid( + "decoded selector does not cover every colored source exactly once", + )); + } + Ok(()) +} + +/// Whether closed LC components form any physical selector admitted by this +/// compact process seed. This is deliberately selector-independent: the +/// cold minimal-coupling sweep uses it to prove that a structural closure can +/// feed at least one public query without constructing a public-flow table. +pub(super) fn components_match_physical_lc_selector( + seed: &OnTheFlyProcessSeedV1, + components: &[LCColorComponent], +) -> bool { + let expected = colored_source_slots(seed); + let mut observed = BTreeSet::new(); + if components + .iter() + .flat_map(|component| component.source_slots().iter().copied()) + .any(|slot| !observed.insert(slot)) + || observed != expected + { + return false; + } + + if expected.is_empty() { + return components.is_empty() && seed.pairing_classes.is_empty(); + } + if seed.pairing_classes.is_empty() { + return components.len() == 1 + && components[0].kind() == LCColorComponentKind::Trace + && components[0].source_slots().iter().all(|slot| { + seed.source_anchors[*slot as usize].color_role + == OnTheFlyExternalColorRoleV1::Adjoint + }); + } + !components.is_empty() + && components.iter().all(|component| { + let slots = component.source_slots(); + component.kind() == LCColorComponentKind::OpenString + && slots.len() >= 2 + && seed.source_anchors[slots[0] as usize].color_role + == OnTheFlyExternalColorRoleV1::Fundamental + && seed.source_anchors[*slots.last().expect("nonempty") as usize].color_role + == OnTheFlyExternalColorRoleV1::Antifundamental + && slots[1..slots.len() - 1].iter().all(|slot| { + seed.source_anchors[*slot as usize].color_role + == OnTheFlyExternalColorRoleV1::Adjoint + }) + }) +} + +/// Recover the same private closure anchor that query decoding chooses for a +/// valid physical selector. `None` means the components are not a selector +/// admitted by this process seed. +pub(super) fn physical_lc_selector_closure_anchor( + seed: &OnTheFlyProcessSeedV1, + components: &[LCColorComponent], +) -> Option { + if !components_match_physical_lc_selector(seed, components) { + return None; + } + if components.is_empty() { + return Some( + seed.source_anchors + .iter() + .find(|anchor| anchor.is_fermionic) + .unwrap_or(&seed.source_anchors[0]) + .source_slot, + ); + } + if seed.pairing_classes.is_empty() { + return components[0].source_slots().last().copied(); + } + + let minimum_colored_slot = colored_source_slots(seed).into_iter().next()?; + let mut blocks = components + .iter() + .map(LCColorComponent::source_slots) + .collect::>(); + blocks.sort_unstable_by(|left, right| left[0].cmp(&right[0]).then_with(|| left.cmp(right))); + let first = blocks + .iter() + .position(|block| block.contains(&minimum_colored_slot))?; + blocks.rotate_left(first); + blocks.last()?.last().copied() +} + +fn decode_selector( + seed: &OnTheFlyProcessSeedV1, + inverse: &[u32], + selector: OnTheFlyLcSelectorV1, +) -> RusticolResult { + match selector { + OnTheFlyLcSelectorV1::Singlet => decode_singlet(seed), + OnTheFlyLcSelectorV1::SingleTrace { public_word } => { + decode_single_trace(seed, inverse, &public_word) + } + OnTheFlyLcSelectorV1::OpenLines { public_blocks } => { + decode_open_lines(seed, inverse, &public_blocks) + } + } +} + +fn decode_singlet(seed: &OnTheFlyProcessSeedV1) -> RusticolResult { + if seed + .source_anchors + .iter() + .any(|anchor| anchor.color_role.is_colored()) + { + return Err(invalid( + "singlet selector is invalid for a process with colored external sources", + )); + } + if !seed.pairing_classes.is_empty() { + return Err(integrity( + "singlet seed unexpectedly contains pairing classes", + )); + } + let closure_anchor_slot = seed + .source_anchors + .iter() + .find(|anchor| anchor.is_fermionic) + .unwrap_or(&seed.source_anchors[0]) + .source_slot; + let selector_digest = selector_digest(&[])?; + if !components_match_physical_lc_selector(seed, &[]) { + return Err(integrity( + "singlet selector failed its physical-shape proof", + )); + } + Ok(DecodedSelector { + target_components: Vec::new(), + closure_anchor_slot, + selector_digest, + }) +} + +fn decode_single_trace( + seed: &OnTheFlyProcessSeedV1, + inverse: &[u32], + public_word: &[u32], +) -> RusticolResult { + if !seed.pairing_classes.is_empty() { + return Err(invalid( + "single-trace selector is invalid for a seed with open-line pairing classes", + )); + } + let word = map_public_word(public_word, inverse, "single-trace word")?; + require_exact_colored_coverage(seed, std::slice::from_ref(&word))?; + if word.iter().any(|slot| { + seed.source_anchors[*slot as usize].color_role != OnTheFlyExternalColorRoleV1::Adjoint + }) { + return Err(invalid( + "single-trace selector contains a non-adjoint external source", + )); + } + let component = LCColorComponent::new(LCColorComponentKind::Trace, word)?; + if !components_match_physical_lc_selector(seed, std::slice::from_ref(&component)) { + return Err(integrity( + "single-trace selector failed its physical-shape proof", + )); + } + let closure_anchor_slot = *component + .source_slots() + .last() + .ok_or_else(|| invalid("single-trace word is empty"))?; + let selector_digest = selector_digest(std::slice::from_ref(&component))?; + Ok(DecodedSelector { + target_components: vec![component], + closure_anchor_slot, + selector_digest, + }) +} + +fn decode_open_lines( + seed: &OnTheFlyProcessSeedV1, + inverse: &[u32], + public_blocks: &[Box<[u32]>], +) -> RusticolResult { + if seed.pairing_classes.is_empty() || public_blocks.is_empty() { + return Err(invalid( + "open-line selector requires pairing classes and nonempty blocks", + )); + } + let mut blocks = public_blocks + .iter() + .map(|word| map_public_word(word, inverse, "open-line block")) + .collect::>>()?; + require_exact_colored_coverage(seed, &blocks)?; + for block in &blocks { + if block.len() < 2 + || seed.source_anchors[block[0] as usize].color_role + != OnTheFlyExternalColorRoleV1::Fundamental + || seed.source_anchors[*block.last().expect("nonempty") as usize].color_role + != OnTheFlyExternalColorRoleV1::Antifundamental + || block[1..block.len() - 1].iter().any(|slot| { + seed.source_anchors[*slot as usize].color_role + != OnTheFlyExternalColorRoleV1::Adjoint + }) + { + return Err(invalid( + "open-line block must be fundamental, zero or more adjoints, antifundamental", + )); + } + } + + // Public callers may enumerate the same tensor product in any block + // order. Map into construction slots first, then recover the canonical + // product order used by the established LC builder before choosing its + // cyclic closure anchor. Reversing a block remains a distinct (and + // invalid above) selector; only whole-block enumeration order is erased. + blocks.sort_unstable_by(|left, right| left[0].cmp(&right[0]).then_with(|| left.cmp(right))); + + let minimum_colored_slot = colored_source_slots(seed) + .into_iter() + .next() + .ok_or_else(|| invalid("open-line selector has no colored source"))?; + let first_block = blocks + .iter() + .position(|block| block.contains(&minimum_colored_slot)) + .ok_or_else(|| integrity("minimum colored source is absent from open-line blocks"))?; + blocks.rotate_left(first_block); + let closure_anchor_slot = *blocks + .last() + .and_then(|block| block.last()) + .ok_or_else(|| invalid("open-line selector has no closure anchor"))?; + let mut components = blocks + .into_iter() + .map(|block| LCColorComponent::new(LCColorComponentKind::OpenString, block)) + .collect::>>()?; + components.sort_unstable(); + if !components_match_physical_lc_selector(seed, &components) { + return Err(integrity( + "open-line selector failed its physical-shape proof", + )); + } + let selector_digest = selector_digest(&components)?; + Ok(DecodedSelector { + target_components: components, + closure_anchor_slot, + selector_digest, + }) +} + +fn selector_digest(components: &[LCColorComponent]) -> RusticolResult { + let mut hash = Sha256::new(); + hash.update(b"pyamplicol-on-the-fly-structural-selector-v1\0"); + hash_len(&mut hash, components.len(), "selector components")?; + for component in components { + hash.update([component.kind() as u8]); + hash_len(&mut hash, component.source_slots().len(), "selector word")?; + for source_slot in component.source_slots() { + hash.update(source_slot.to_le_bytes()); + } + } + final_digest(hash) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn digest(byte: u8) -> SemanticDigest { + SemanticDigest::new([byte; 32]).unwrap() + } + + fn anchor( + source_slot: u32, + role: OnTheFlyExternalColorRoleV1, + contract: Option, + ) -> OnTheFlySourceAnchorV1 { + let fermionic = role.is_pairing_endpoint(); + let family = if fermionic { + OnTheFlySourceWavefunctionFamilyV1::DiracFermion + } else { + OnTheFlySourceWavefunctionFamilyV1::Vector + }; + let orientation = match role { + OnTheFlyExternalColorRoleV1::Fundamental => OnTheFlySourceOrientationV1::Particle, + OnTheFlyExternalColorRoleV1::Antifundamental => { + OnTheFlySourceOrientationV1::Antiparticle + } + OnTheFlyExternalColorRoleV1::Singlet | OnTheFlyExternalColorRoleV1::Adjoint => { + OnTheFlySourceOrientationV1::SelfConjugate + } + }; + let state = OnTheFlySourceStateV1::new( + 0, + 1, + 1, + source_slot, + source_slot, + digest(20 + source_slot as u8), + digest(40 + source_slot as u8), + 1, + ExactComplexRational::ONE, + 1, + 0, + vec![source_slot as i32 + 1], + source_slot, + digest(60 + source_slot as u8), + family, + orientation, + None, + ) + .unwrap(); + OnTheFlySourceAnchorV1::new( + source_slot, + 100 + source_slot, + false, + role, + fermionic, + contract, + vec![state], + ) + .unwrap() + } + + fn seed( + roles: &[OnTheFlyExternalColorRoleV1], + external_permutation: Vec, + ) -> OnTheFlyProcessSeedV1 { + let anchors = roles + .iter() + .copied() + .enumerate() + .map(|(slot, role)| { + let contract = role.is_pairing_endpoint().then(|| digest(100 + slot as u8)); + anchor(slot as u32, role, contract) + }) + .collect::>(); + let fundamental = anchors + .iter() + .filter(|anchor| anchor.color_role == OnTheFlyExternalColorRoleV1::Fundamental) + .map(|anchor| OnTheFlyPairingEndpointV1 { + source_slot: anchor.source_slot, + source_contract_digest: anchor.pairing_source_contract_digest.unwrap(), + }) + .collect::>(); + let antifundamental = anchors + .iter() + .filter(|anchor| anchor.color_role == OnTheFlyExternalColorRoleV1::Antifundamental) + .map(|anchor| OnTheFlyPairingEndpointV1 { + source_slot: anchor.source_slot, + source_contract_digest: anchor.pairing_source_contract_digest.unwrap(), + }) + .collect::>(); + let classes = if fundamental.is_empty() { + Vec::new() + } else { + vec![ + OnTheFlyPairingClassV1::new("q", digest(88), fundamental, antifundamental).unwrap(), + ] + }; + OnTheFlyProcessSeedV1::new( + digest(1), + digest(2), + digest(3), + digest(4), + digest(5), + digest(6), + "raw amplitude", + ExactComplexRational::ONE, + anchors, + external_permutation, + OnTheFlyCouplingOrderPolicyV1::Explicit, + vec![1], + vec![Some(2)], + classes, + ) + .unwrap() + } + + #[test] + fn inverse_permutation_binds_public_and_construction_slots_exactly() { + assert_eq!(inverse_permutation(&[2, 0, 3, 1]).unwrap(), [1, 3, 0, 2]); + assert!(inverse_permutation(&[0, 0]).is_err()); + assert!(inverse_permutation(&[0, 2]).is_err()); + } + + #[test] + fn selector_domains_are_derived_from_external_roles() { + let singlet_seed = seed( + &[ + OnTheFlyExternalColorRoleV1::Singlet, + OnTheFlyExternalColorRoleV1::Singlet, + ], + vec![1, 0], + ); + let singlet = DecodedLcQueryV1::new( + &singlet_seed, + vec![1, 0], + &[1, 1], + OnTheFlyLcSelectorV1::Singlet, + ) + .unwrap(); + assert!(singlet.target_components.is_empty()); + assert_eq!(singlet.closure_anchor_slot, 0); + + let trace_seed = seed( + &[ + OnTheFlyExternalColorRoleV1::Adjoint, + OnTheFlyExternalColorRoleV1::Adjoint, + ], + vec![1, 0], + ); + assert!( + DecodedLcQueryV1::new( + &trace_seed, + vec![1, 0], + &[1, 1], + OnTheFlyLcSelectorV1::Singlet, + ) + .is_err() + ); + let trace = DecodedLcQueryV1::new( + &trace_seed, + vec![1, 0], + &[1, 1], + OnTheFlyLcSelectorV1::single_trace(vec![1, 0]), + ) + .unwrap(); + assert_eq!(trace.target_components[0].source_slots(), [0, 1]); + assert_eq!(trace.closure_anchor_slot, 1); + } + + #[test] + fn single_trace_anchor_is_cyclic_invariant_but_reversal_remains_distinct() { + let trace_seed = seed( + &[ + OnTheFlyExternalColorRoleV1::Adjoint, + OnTheFlyExternalColorRoleV1::Adjoint, + OnTheFlyExternalColorRoleV1::Adjoint, + ], + vec![0, 1, 2], + ); + let decode = |word| { + DecodedLcQueryV1::new( + &trace_seed, + vec![0, 1, 2], + &[1, 1, 1], + OnTheFlyLcSelectorV1::single_trace(word), + ) + .unwrap() + }; + let canonical = decode(vec![0, 1, 2]); + let rotated = decode(vec![1, 2, 0]); + let reversed = decode(vec![0, 2, 1]); + assert_eq!(canonical.target_components, rotated.target_components); + assert_eq!(canonical.closure_anchor_slot, rotated.closure_anchor_slot); + assert_eq!(canonical.selector_digest, rotated.selector_digest); + assert_eq!(canonical.semantic_digest(), rotated.semantic_digest()); + assert_ne!(canonical.target_components, reversed.target_components); + assert_ne!(canonical.selector_digest, reversed.selector_digest); + assert_ne!(canonical.closure_anchor_slot, reversed.closure_anchor_slot); + } + + #[test] + fn certified_cyclic_policy_shares_one_anchor_without_touching_generic_shapes() { + let trace_seed = seed( + &[ + OnTheFlyExternalColorRoleV1::Adjoint, + OnTheFlyExternalColorRoleV1::Adjoint, + OnTheFlyExternalColorRoleV1::Adjoint, + OnTheFlyExternalColorRoleV1::Adjoint, + ], + vec![0, 1, 2, 3], + ); + let decode_trace = |word| { + DecodedLcQueryV1::new( + &trace_seed, + vec![0, 1, 2, 3], + &[1, 1, 1, 1], + OnTheFlyLcSelectorV1::single_trace(word), + ) + .unwrap() + }; + let queries = [ + vec![0, 1, 2, 3], + vec![0, 1, 3, 2], + vec![0, 2, 1, 3], + vec![0, 2, 3, 1], + vec![0, 3, 1, 2], + vec![0, 3, 2, 1], + ] + .map(decode_trace); + assert_eq!( + queries + .iter() + .map(|query| query.closure_anchor_slot) + .collect::>(), + BTreeSet::from([1, 2, 3]), + ); + + let fallback = OnTheFlyClosureAnchorPolicyV1::selector_endpoint(); + for query in &queries { + assert_eq!( + fallback + .canonicalize_query(&trace_seed, query.clone()) + .unwrap(), + query.clone(), + ); + } + let fixed = OnTheFlyClosureAnchorPolicyV1::certified_cyclic_minimum(0); + let error = fixed + .authenticate_canonical_query(&trace_seed, &queries[0]) + .unwrap_err(); + assert!(error.to_string().contains("prepared-grammar boundary")); + let fixed_queries = + queries.map(|query| fixed.canonicalize_query(&trace_seed, query).unwrap()); + assert!( + fixed_queries + .iter() + .all(|query| query.closure_anchor_slot == 0) + ); + for query in &fixed_queries { + fixed + .authenticate_canonical_query(&trace_seed, query) + .unwrap(); + assert!(fixed.certifies_cyclic_trace_reflection(&trace_seed, query)); + assert!(!fallback.certifies_cyclic_trace_reflection(&trace_seed, query)); + } + assert_eq!( + fixed_queries + .iter() + .map(DecodedLcQueryV1::semantic_digest) + .collect::>() + .len(), + 6, + ); + + let open_seed = seed( + &[ + OnTheFlyExternalColorRoleV1::Fundamental, + OnTheFlyExternalColorRoleV1::Adjoint, + OnTheFlyExternalColorRoleV1::Antifundamental, + ], + vec![0, 1, 2], + ); + let open = DecodedLcQueryV1::new( + &open_seed, + vec![0, 1, 2], + &[1, 1, 1], + OnTheFlyLcSelectorV1::open_lines(vec![vec![0, 1, 2]]), + ) + .unwrap(); + assert_eq!(open.closure_anchor_slot, 2); + assert_eq!( + fixed.canonicalize_query(&open_seed, open.clone()).unwrap(), + open, + ); + assert!(!fixed.certifies_cyclic_trace_reflection(&open_seed, &open)); + } + + #[test] + fn open_line_selector_binds_only_the_selected_color_tensor() { + let open_seed = seed( + &[ + OnTheFlyExternalColorRoleV1::Fundamental, + OnTheFlyExternalColorRoleV1::Adjoint, + OnTheFlyExternalColorRoleV1::Antifundamental, + ], + vec![2, 0, 1], + ); + let query = DecodedLcQueryV1::new( + &open_seed, + vec![2, 0, 1], + &[1, 1, 1], + OnTheFlyLcSelectorV1::open_lines(vec![vec![2, 0, 1]]), + ) + .unwrap(); + assert_eq!(query.target_components[0].source_slots(), [0, 1, 2]); + assert_eq!(query.closure_anchor_slot, 2); + } + + #[test] + fn multi_open_line_block_order_is_canonical_after_public_alias_permutation() { + let roles = [ + OnTheFlyExternalColorRoleV1::Fundamental, + OnTheFlyExternalColorRoleV1::Adjoint, + OnTheFlyExternalColorRoleV1::Antifundamental, + OnTheFlyExternalColorRoleV1::Fundamental, + OnTheFlyExternalColorRoleV1::Adjoint, + OnTheFlyExternalColorRoleV1::Antifundamental, + ]; + // Construction slot -> public slot. The two physical open strings are + // [0,1,2] and [3,4,5], hence [3,0,5] and [2,1,4] publicly. + let permutation = vec![3, 0, 5, 2, 1, 4]; + let open_seed = seed(&roles, permutation.clone()); + let decode = |blocks| { + DecodedLcQueryV1::new( + &open_seed, + permutation.clone(), + &[1; 6], + OnTheFlyLcSelectorV1::open_lines(blocks), + ) + .unwrap() + }; + let forward = decode(vec![vec![3, 0, 5], vec![2, 1, 4]]); + let reversed_blocks = decode(vec![vec![2, 1, 4], vec![3, 0, 5]]); + + assert_eq!(forward.target_components, reversed_blocks.target_components); + assert_eq!(forward.selector_digest, reversed_blocks.selector_digest); + assert_eq!(forward.semantic_digest(), reversed_blocks.semantic_digest()); + assert_eq!(forward.closure_anchor_slot, 5); + assert_eq!( + forward + .target_components + .iter() + .map(|component| component.source_slots()) + .collect::>(), + vec![&[0, 1, 2][..], &[3, 4, 5][..]], + ); + } +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/recurrence/on_the_fly/seed_codec.rs b/pyamplicol/rust/crates/rusticol-core/src/recurrence/on_the_fly/seed_codec.rs new file mode 100644 index 0000000000..8a32124fef --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/recurrence/on_the_fly/seed_codec.rs @@ -0,0 +1,1038 @@ +// SPDX-License-Identifier: 0BSD + +//! Canonical private payload codec for the compact on-the-fly process seed. +//! +//! The eventual PACBIN member owns payload hashing and authentication. This +//! codec therefore carries only the seed's semantic inputs; derived semantic +//! digests are recomputed by the existing constructors during decoding. The +//! raw-amplitude normalization factor is exact one by seed construction, so +//! schema v1 implies it instead of storing a redundant field. + +use std::io::{self, Write}; +use std::str; + +use super::source_seed::{ + OnTheFlyCouplingOrderPolicyV1, OnTheFlyExternalColorRoleV1, OnTheFlyPairingClassV1, + OnTheFlyPairingEndpointV1, OnTheFlyProcessSeedV1, OnTheFlySourceAnchorV1, + OnTheFlySourceOrientationV1, OnTheFlySourceStateV1, OnTheFlySourceWavefunctionFamilyV1, +}; +use crate::recurrence::{ExactComplexRational, ExactRational, SemanticDigest}; +use crate::{RusticolError, RusticolResult}; + +const MAGIC: &[u8; 8] = b"PACOTFSD"; +const SCHEMA_VERSION: u32 = 1; +const SEMANTIC_HASH_REVISION: u32 = 2; +#[cfg(test)] +const RETAINED_DIGEST_COUNT: u64 = 6; +const DIGEST_BYTES: u64 = 32; + +fn invalid(message: impl Into) -> RusticolError { + RusticolError::invalid_argument(format!("on-the-fly process-seed codec: {}", message.into())) +} + +struct Writer { + destination: W, + bytes_written: u64, +} + +impl Writer { + fn new(destination: W) -> Self { + Self { + destination, + bytes_written: 0, + } + } + + #[cfg(test)] + fn with_bytes_written(destination: W, bytes_written: u64) -> Self { + Self { + destination, + bytes_written, + } + } + + fn raw(&mut self, bytes: &[u8]) -> RusticolResult<()> { + let byte_count = + u64::try_from(bytes.len()).map_err(|_| invalid("payload write length exceeds u64"))?; + let end = self + .bytes_written + .checked_add(byte_count) + .ok_or_else(|| invalid("payload byte accounting overflows u64"))?; + self.destination.write_all(bytes).map_err(|error| { + RusticolError::serialization(format!( + "could not stream on-the-fly process-seed payload: {error}" + )) + })?; + self.bytes_written = end; + Ok(()) + } + + fn u8(&mut self, value: u8) -> RusticolResult<()> { + self.raw(&[value]) + } + + fn u32(&mut self, value: u32) -> RusticolResult<()> { + self.raw(&value.to_le_bytes()) + } + + fn i32(&mut self, value: i32) -> RusticolResult<()> { + self.raw(&value.to_le_bytes()) + } + + fn u64(&mut self, value: u64) -> RusticolResult<()> { + self.raw(&value.to_le_bytes()) + } + + fn i128(&mut self, value: i128) -> RusticolResult<()> { + self.raw(&value.to_le_bytes()) + } + + fn count(&mut self, value: usize, label: &str) -> RusticolResult<()> { + self.u64(u64::try_from(value).map_err(|_| invalid(format!("{label} count exceeds u64")))?) + } + + fn boolean(&mut self, value: bool) -> RusticolResult<()> { + self.u8(u8::from(value)) + } + + fn digest(&mut self, value: SemanticDigest) -> RusticolResult<()> { + self.raw(value.as_bytes()) + } + + fn optional_digest(&mut self, value: Option) -> RusticolResult<()> { + match value { + None => self.u8(0), + Some(value) => { + self.u8(1)?; + self.digest(value) + } + } + } + + fn optional_u32(&mut self, value: Option) -> RusticolResult<()> { + match value { + None => self.u8(0), + Some(value) => { + self.u8(1)?; + self.u32(value) + } + } + } + + fn exact(&mut self, value: ExactComplexRational) -> RusticolResult<()> { + for part in [value.real(), value.imag()] { + self.i128(part.numerator())?; + self.i128(part.denominator())?; + } + Ok(()) + } + + fn string(&mut self, value: &str, label: &str) -> RusticolResult<()> { + self.count(value.len(), label)?; + self.raw(value.as_bytes()) + } +} + +struct Reader<'a> { + bytes: &'a [u8], + offset: u64, + length: u64, +} + +impl<'a> Reader<'a> { + fn new(bytes: &'a [u8]) -> RusticolResult { + Ok(Self { + bytes, + offset: 0, + length: u64::try_from(bytes.len()) + .map_err(|_| invalid("payload length exceeds u64"))?, + }) + } + + fn take(&mut self, count: u64, label: &str) -> RusticolResult<&'a [u8]> { + let end = self + .offset + .checked_add(count) + .ok_or_else(|| invalid(format!("{label} byte range overflows u64")))?; + if end > self.length { + return Err(invalid(format!( + "truncated {label} at byte {}: need {count}, have {}", + self.offset, + self.length.saturating_sub(self.offset) + ))); + } + let start = usize::try_from(self.offset) + .map_err(|_| invalid(format!("{label} offset exceeds usize")))?; + let end = usize::try_from(end) + .map_err(|_| invalid(format!("{label} end offset exceeds usize")))?; + let result = self + .bytes + .get(start..end) + .ok_or_else(|| invalid(format!("{label} byte range is outside the payload")))?; + self.offset = + u64::try_from(end).map_err(|_| invalid(format!("{label} end offset exceeds u64")))?; + Ok(result) + } + + fn u8(&mut self, label: &str) -> RusticolResult { + Ok(self.take(1, label)?[0]) + } + + fn u32(&mut self, label: &str) -> RusticolResult { + Ok(u32::from_le_bytes( + self.take(4, label)?.try_into().expect("checked read"), + )) + } + + fn i32(&mut self, label: &str) -> RusticolResult { + Ok(i32::from_le_bytes( + self.take(4, label)?.try_into().expect("checked read"), + )) + } + + fn u64(&mut self, label: &str) -> RusticolResult { + Ok(u64::from_le_bytes( + self.take(8, label)?.try_into().expect("checked read"), + )) + } + + fn i128(&mut self, label: &str) -> RusticolResult { + Ok(i128::from_le_bytes( + self.take(16, label)?.try_into().expect("checked read"), + )) + } + + fn count(&mut self, minimum_item_bytes: u64, label: &str) -> RusticolResult { + let count = self.u64(&format!("{label} count"))?; + let minimum_bytes = count + .checked_mul(minimum_item_bytes) + .ok_or_else(|| invalid(format!("{label} minimum byte length overflows u64")))?; + if minimum_bytes > self.length.saturating_sub(self.offset) { + return Err(invalid(format!( + "{label} count cannot fit in the remaining payload" + ))); + } + usize::try_from(count).map_err(|_| invalid(format!("{label} count exceeds usize"))) + } + + fn boolean(&mut self, label: &str) -> RusticolResult { + match self.u8(label)? { + 0 => Ok(false), + 1 => Ok(true), + value => Err(invalid(format!( + "{label} has malformed boolean discriminant {value}" + ))), + } + } + + fn digest(&mut self, label: &str) -> RusticolResult { + let value: [u8; 32] = self + .take(DIGEST_BYTES, label)? + .try_into() + .expect("checked read"); + SemanticDigest::new(value).map_err(|error| invalid(error.message())) + } + + fn optional_digest(&mut self, label: &str) -> RusticolResult> { + match self.u8(&format!("{label} presence"))? { + 0 => Ok(None), + 1 => Ok(Some(self.digest(label)?)), + value => Err(invalid(format!( + "{label} has malformed optional discriminant {value}" + ))), + } + } + + fn optional_u32(&mut self, label: &str) -> RusticolResult> { + match self.u8(&format!("{label} presence"))? { + 0 => Ok(None), + 1 => Ok(Some(self.u32(label)?)), + value => Err(invalid(format!( + "{label} has malformed optional discriminant {value}" + ))), + } + } + + fn exact(&mut self, label: &str) -> RusticolResult { + Ok(ExactComplexRational::new( + self.rational(&format!("{label} real"))?, + self.rational(&format!("{label} imaginary"))?, + )) + } + + fn rational(&mut self, label: &str) -> RusticolResult { + let numerator = self.i128(&format!("{label} numerator"))?; + let denominator = self.i128(&format!("{label} denominator"))?; + let value = + ExactRational::new(numerator, denominator).map_err(|error| invalid(error.message()))?; + if value.numerator() != numerator || value.denominator() != denominator { + return Err(invalid(format!("{label} is not canonically reduced"))); + } + Ok(value) + } + + fn string(&mut self, label: &str) -> RusticolResult { + let byte_count = self.u64(&format!("{label} byte count"))?; + let bytes = self.take(byte_count, label)?; + let value = str::from_utf8(bytes) + .map_err(|error| invalid(format!("{label} is not UTF-8: {error}")))?; + let byte_count = usize::try_from(byte_count) + .map_err(|_| invalid(format!("{label} byte count exceeds usize")))?; + let mut owned = String::new(); + owned.try_reserve_exact(byte_count).map_err(|error| { + invalid(format!( + "could not reserve {byte_count} bytes for {label}: {error}" + )) + })?; + owned.push_str(value); + Ok(owned) + } + + fn finish(self) -> RusticolResult<()> { + if self.offset != self.length { + return Err(invalid(format!( + "payload contains {} trailing bytes", + self.length - self.offset + ))); + } + Ok(()) + } +} + +fn reserved_vec(count: usize, label: &str) -> RusticolResult> { + let mut result = Vec::new(); + result.try_reserve_exact(count).map_err(|error| { + invalid(format!( + "could not reserve {count} rows for {label}: {error}" + )) + })?; + Ok(result) +} + +fn encode_to_writer(seed: &OnTheFlyProcessSeedV1, destination: W) -> RusticolResult { + let mut writer = Writer::new(destination); + writer.raw(MAGIC)?; + writer.u32(SCHEMA_VERSION)?; + writer.u32(SEMANTIC_HASH_REVISION)?; + for digest in [ + seed.process_digest, + seed.model_digest, + seed.template_catalog_digest, + seed.prepared_pack_digest, + seed.direct_catalog_digest, + seed.normalization_semantic_digest, + ] { + writer.digest(digest)?; + } + writer.string(&seed.normalization_convention, "normalization convention")?; + writer.count(seed.source_anchors.len(), "source anchors")?; + for anchor in &seed.source_anchors { + writer.u32(anchor.source_slot)?; + writer.u32(anchor.external_label)?; + writer.boolean(anchor.is_initial)?; + writer.u8(anchor.color_role as u8)?; + writer.boolean(anchor.is_fermionic)?; + writer.optional_digest(anchor.pairing_source_contract_digest)?; + writer.count(anchor.states.len(), "source states")?; + for state in &anchor.states { + writer.u32(state.state_index)?; + writer.i32(state.public_helicity)?; + writer.i32(state.source_helicity)?; + writer.u32(state.source_template_id)?; + writer.u32(state.current_state_template_id)?; + writer.digest(state.source_semantic_digest)?; + writer.digest(state.current_state_semantic_digest)?; + writer.i32(state.momentum_sign)?; + writer.exact(state.crossing_phase)?; + writer.i32(state.spin_state)?; + writer.i32(state.chirality)?; + writer.count(state.flavour_flow.len(), "source flavour flow")?; + for flavour in &state.flavour_flow { + writer.i32(*flavour)?; + } + writer.u32(state.quantum_number_flow_id)?; + writer.digest(state.color_seed_proof_digest)?; + writer.u8(state.source_family as u8)?; + writer.u8(state.source_orientation as u8)?; + writer.optional_u32(state.prepared_mass_parameter_slot)?; + } + } + writer.count( + seed.external_permutation.len(), + "external gather permutation", + )?; + for slot in &seed.external_permutation { + writer.u32(*slot)?; + } + writer.u8(seed.coupling_order_policy as u8)?; + writer.count(seed.coupling_limits.len(), "coupling policy rows")?; + for (hierarchy, limit) in seed + .coupling_hierarchies + .iter() + .zip(seed.coupling_limits.iter()) + { + writer.u32(*hierarchy)?; + writer.optional_u32(*limit)?; + } + writer.count(seed.pairing_classes.len(), "pairing classes")?; + for pairing_class in &seed.pairing_classes { + writer.string(&pairing_class.species, "pairing species")?; + writer.digest(pairing_class.species_semantic_digest)?; + for (label, endpoints) in [ + ( + "fundamental pairing endpoints", + pairing_class.fundamental_endpoints.as_ref(), + ), + ( + "antifundamental pairing endpoints", + pairing_class.antifundamental_endpoints.as_ref(), + ), + ] { + writer.count(endpoints.len(), label)?; + for endpoint in endpoints { + writer.u32(endpoint.source_slot)?; + writer.digest(endpoint.source_contract_digest)?; + } + } + } + Ok(writer.bytes_written) +} + +/// Encode the canonical schema-v1 payload for a compact semantic seed. +/// +/// This remains crate-private until the PACBIN member integration owns it. +#[allow(dead_code)] +pub(crate) fn encode_on_the_fly_process_seed_v1( + seed: &OnTheFlyProcessSeedV1, +) -> RusticolResult> { + let byte_count = encode_to_writer(seed, io::sink())?; + let capacity = + usize::try_from(byte_count).map_err(|_| invalid("encoded payload length exceeds usize"))?; + let mut payload = Vec::new(); + payload.try_reserve_exact(capacity).map_err(|error| { + invalid(format!( + "could not reserve {capacity} bytes for encoded payload: {error}" + )) + })?; + let actual = encode_to_writer(seed, &mut payload)?; + if actual != byte_count || payload.len() != capacity { + return Err(RusticolError::internal( + "on-the-fly process-seed encoder produced an inconsistent byte count", + )); + } + Ok(payload) +} + +fn read_color_role(reader: &mut Reader<'_>) -> RusticolResult { + match reader.u8("source anchor color role")? { + 0 => Ok(OnTheFlyExternalColorRoleV1::Singlet), + 1 => Ok(OnTheFlyExternalColorRoleV1::Fundamental), + 2 => Ok(OnTheFlyExternalColorRoleV1::Antifundamental), + 3 => Ok(OnTheFlyExternalColorRoleV1::Adjoint), + value => Err(invalid(format!( + "malformed source anchor color-role discriminant {value}" + ))), + } +} + +fn read_source_family( + reader: &mut Reader<'_>, +) -> RusticolResult { + match reader.u8("source wavefunction family")? { + 0 => Ok(OnTheFlySourceWavefunctionFamilyV1::Scalar), + 1 => Ok(OnTheFlySourceWavefunctionFamilyV1::WeylFermion), + 2 => Ok(OnTheFlySourceWavefunctionFamilyV1::DiracFermion), + 3 => Ok(OnTheFlySourceWavefunctionFamilyV1::Vector), + 4 => Ok(OnTheFlySourceWavefunctionFamilyV1::Spin2), + value => Err(invalid(format!( + "malformed source wavefunction-family discriminant {value}" + ))), + } +} + +fn read_source_orientation(reader: &mut Reader<'_>) -> RusticolResult { + match reader.u8("source orientation")? { + 0 => Ok(OnTheFlySourceOrientationV1::Particle), + 1 => Ok(OnTheFlySourceOrientationV1::Antiparticle), + 2 => Ok(OnTheFlySourceOrientationV1::SelfConjugate), + value => Err(invalid(format!( + "malformed source-orientation discriminant {value}" + ))), + } +} + +fn read_coupling_order_policy( + reader: &mut Reader<'_>, +) -> RusticolResult { + match reader.u8("coupling-order policy")? { + 0 => Ok(OnTheFlyCouplingOrderPolicyV1::Minimal), + 1 => Ok(OnTheFlyCouplingOrderPolicyV1::Explicit), + value => Err(invalid(format!( + "malformed coupling-order policy discriminant {value}" + ))), + } +} + +fn read_source_state(reader: &mut Reader<'_>) -> RusticolResult { + let state_index = reader.u32("source state index")?; + let public_helicity = reader.i32("source state public helicity")?; + let source_helicity = reader.i32("source state source helicity")?; + let source_template_id = reader.u32("source state source-template ID")?; + let current_state_template_id = reader.u32("source state current-template ID")?; + let source_semantic_digest = reader.digest("source state source semantic digest")?; + let current_state_semantic_digest = reader.digest("source state current semantic digest")?; + let momentum_sign = reader.i32("source state momentum sign")?; + let crossing_phase = reader.exact("source state crossing phase")?; + let spin_state = reader.i32("source state spin state")?; + let chirality = reader.i32("source state chirality")?; + let flavour_count = reader.count(4, "source flavour flow")?; + let mut flavour_flow = reserved_vec(flavour_count, "source flavour flow")?; + for _ in 0..flavour_count { + flavour_flow.push(reader.i32("source flavour-flow entry")?); + } + let quantum_number_flow_id = reader.u32("source state quantum-number flow ID")?; + let color_seed_proof_digest = reader.digest("source state color-seed proof digest")?; + let source_family = read_source_family(reader)?; + let source_orientation = read_source_orientation(reader)?; + let prepared_mass_parameter_slot = + reader.optional_u32("source state prepared-mass parameter slot")?; + OnTheFlySourceStateV1::new( + state_index, + public_helicity, + source_helicity, + source_template_id, + current_state_template_id, + source_semantic_digest, + current_state_semantic_digest, + momentum_sign, + crossing_phase, + spin_state, + chirality, + flavour_flow, + quantum_number_flow_id, + color_seed_proof_digest, + source_family, + source_orientation, + prepared_mass_parameter_slot, + ) +} + +fn read_pairing_endpoints( + reader: &mut Reader<'_>, + label: &str, +) -> RusticolResult> { + const MINIMUM_ENDPOINT_BYTES: u64 = 4 + DIGEST_BYTES; + let count = reader.count(MINIMUM_ENDPOINT_BYTES, label)?; + let mut endpoints = reserved_vec(count, label)?; + let mut previous_slot = None; + for _ in 0..count { + let source_slot = reader.u32(&format!("{label} source slot"))?; + if previous_slot.is_some_and(|previous| previous >= source_slot) { + return Err(invalid(format!( + "{label} keys are not in canonical strict order" + ))); + } + previous_slot = Some(source_slot); + endpoints.push(OnTheFlyPairingEndpointV1 { + source_slot, + source_contract_digest: reader.digest(&format!("{label} source-contract digest"))?, + }); + } + Ok(endpoints) +} + +/// Decode exactly one canonical schema-v1 compact semantic seed payload. +/// +/// Version mismatches are rejected rather than guessed, leaving a clean seam +/// for a future schema that carries NLC/full-color seed fields. +#[allow(dead_code)] +pub(crate) fn decode_on_the_fly_process_seed_v1( + bytes: &[u8], +) -> RusticolResult { + const MINIMUM_STATE_BYTES: u64 = + 5 * 4 + 2 * DIGEST_BYTES + 4 + 4 * 16 + 2 * 4 + 8 + 4 + DIGEST_BYTES + 1 + 1 + 1 + 4; + const MINIMUM_ANCHOR_BYTES: u64 = 4 + 4 + 1 + 1 + 1 + 1 + 8 + MINIMUM_STATE_BYTES; + const MINIMUM_ENDPOINT_BYTES: u64 = 4 + DIGEST_BYTES; + const MINIMUM_PAIRING_CLASS_BYTES: u64 = + 8 + 1 + DIGEST_BYTES + 8 + MINIMUM_ENDPOINT_BYTES + 8 + MINIMUM_ENDPOINT_BYTES; + + let mut reader = Reader::new(bytes)?; + if reader.take(8, "magic")? != MAGIC { + return Err(invalid( + "unsupported payload magic; expected on-the-fly process-seed schema v1", + )); + } + let schema_version = reader.u32("schema version")?; + if schema_version != SCHEMA_VERSION { + return Err(invalid(format!( + "unsupported on-the-fly process-seed schema version {schema_version}; expected {SCHEMA_VERSION}" + ))); + } + let semantic_hash_revision = reader.u32("semantic hash revision")?; + if semantic_hash_revision != SEMANTIC_HASH_REVISION { + return Err(invalid(format!( + "unsupported on-the-fly process-seed semantic hash revision {semantic_hash_revision}; expected {SEMANTIC_HASH_REVISION}" + ))); + } + + let process_digest = reader.digest("process digest")?; + let model_digest = reader.digest("model digest")?; + let template_catalog_digest = reader.digest("template-catalog digest")?; + let prepared_pack_digest = reader.digest("prepared-pack digest")?; + let direct_catalog_digest = reader.digest("direct-catalog digest")?; + let normalization_semantic_digest = reader.digest("normalization semantic digest")?; + let normalization_convention = reader.string("normalization convention")?; + + let anchor_count = reader.count(MINIMUM_ANCHOR_BYTES, "source anchors")?; + let mut source_anchors = reserved_vec(anchor_count, "source anchors")?; + for anchor_index in 0..anchor_count { + let source_slot = reader.u32("source anchor slot")?; + let expected_slot = u32::try_from(anchor_index) + .map_err(|_| invalid("source anchor index exceeds its u32 semantic domain"))?; + if source_slot != expected_slot { + return Err(invalid( + "source anchor keys are not in canonical dense order", + )); + } + let external_label = reader.u32("source anchor external label")?; + let is_initial = reader.boolean("source anchor initial-state flag")?; + let color_role = read_color_role(&mut reader)?; + let is_fermionic = reader.boolean("source anchor fermion flag")?; + let pairing_source_contract_digest = + reader.optional_digest("source anchor pairing-source contract digest")?; + let state_count = reader.count(MINIMUM_STATE_BYTES, "source states")?; + let mut states = reserved_vec(state_count, "source states")?; + let mut previous_state_index = None; + for _ in 0..state_count { + let state = read_source_state(&mut reader)?; + if previous_state_index.is_some_and(|previous| previous >= state.state_index) { + return Err(invalid( + "source state keys are not in canonical strict order", + )); + } + previous_state_index = Some(state.state_index); + states.push(state); + } + source_anchors.push(OnTheFlySourceAnchorV1::new( + source_slot, + external_label, + is_initial, + color_role, + is_fermionic, + pairing_source_contract_digest, + states, + )?); + } + + let permutation_count = reader.count(4, "external gather permutation")?; + let mut external_permutation = reserved_vec(permutation_count, "external gather permutation")?; + for _ in 0..permutation_count { + external_permutation.push(reader.u32("external gather-permutation slot")?); + } + + let coupling_order_policy = read_coupling_order_policy(&mut reader)?; + let coupling_row_count = reader.count(1, "coupling policy rows")?; + let mut coupling_hierarchies = reserved_vec(coupling_row_count, "coupling hierarchies")?; + let mut coupling_limits = reserved_vec(coupling_row_count, "coupling limits")?; + for _ in 0..coupling_row_count { + coupling_hierarchies.push(reader.u32("coupling hierarchy")?); + coupling_limits.push(reader.optional_u32("explicit coupling limit")?); + } + + let pairing_class_count = reader.count(MINIMUM_PAIRING_CLASS_BYTES, "pairing classes")?; + let mut pairing_classes = reserved_vec(pairing_class_count, "pairing classes")?; + for _ in 0..pairing_class_count { + let species = reader.string("pairing species")?; + if pairing_classes + .last() + .is_some_and(|previous: &OnTheFlyPairingClassV1| { + previous.species.as_ref() >= species.as_str() + }) + { + return Err(invalid( + "pairing class species keys are not in canonical strict order", + )); + } + let species_semantic_digest = reader.digest("pairing species semantic digest")?; + let fundamental_endpoints = + read_pairing_endpoints(&mut reader, "fundamental pairing endpoints")?; + let antifundamental_endpoints = + read_pairing_endpoints(&mut reader, "antifundamental pairing endpoints")?; + pairing_classes.push(OnTheFlyPairingClassV1::new( + species, + species_semantic_digest, + fundamental_endpoints, + antifundamental_endpoints, + )?); + } + reader.finish()?; + + OnTheFlyProcessSeedV1::new( + process_digest, + model_digest, + template_catalog_digest, + prepared_pack_digest, + direct_catalog_digest, + normalization_semantic_digest, + normalization_convention, + ExactComplexRational::ONE, + source_anchors, + external_permutation, + coupling_order_policy, + coupling_hierarchies, + coupling_limits, + pairing_classes, + ) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::recurrence::on_the_fly::{ + ON_THE_FLY_PROCESS_SEED_IDENTITY_ABI, OnTheFlyProcessSeedIdentityV1, + scalar_adapter_test_seed, + }; + + #[derive(Default)] + struct CountingWriter { + bytes_written: u64, + } + + impl Write for CountingWriter { + fn write(&mut self, bytes: &[u8]) -> io::Result { + self.bytes_written += bytes.len() as u64; + Ok(bytes.len()) + } + + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } + } + + fn digest(byte: u8) -> SemanticDigest { + SemanticDigest::new([byte; 32]).unwrap() + } + + fn state(source_slot: u32, state_index: u32) -> OnTheFlySourceStateV1 { + let crossing_phase = if source_slot == 0 && state_index == 0 { + ExactComplexRational::new( + ExactRational::new(-1, 2).unwrap(), + ExactRational::new(1, 3).unwrap(), + ) + } else { + ExactComplexRational::ONE + }; + OnTheFlySourceStateV1::new( + state_index, + if state_index == 0 { -1 } else { 1 }, + if source_slot.is_multiple_of(2) { -1 } else { 1 }, + 100 + source_slot * 2 + state_index, + 200 + source_slot * 2 + state_index, + digest(20 + (source_slot * 2 + state_index) as u8), + digest(30 + (source_slot * 2 + state_index) as u8), + if source_slot.is_multiple_of(2) { -1 } else { 1 }, + crossing_phase, + 50_000 + source_slot as i32, + if source_slot.is_multiple_of(2) { -1 } else { 1 }, + if state_index == 0 { + vec![11 + source_slot as i32, 21 + source_slot as i32] + } else { + vec![31 + source_slot as i32] + }, + 300 + source_slot, + digest(40 + source_slot as u8), + OnTheFlySourceWavefunctionFamilyV1::DiracFermion, + if source_slot.is_multiple_of(2) { + OnTheFlySourceOrientationV1::Particle + } else { + OnTheFlySourceOrientationV1::Antiparticle + }, + (state_index == 0).then_some(400 + source_slot), + ) + .unwrap() + } + + fn ordinary_seed(reverse_inputs: bool) -> OnTheFlyProcessSeedV1 { + ordinary_seed_with_limits(reverse_inputs, vec![Some(0), Some(3)]) + } + + fn ordinary_seed_with_limits( + reverse_inputs: bool, + coupling_limits: Vec>, + ) -> OnTheFlyProcessSeedV1 { + let contracts = [digest(60), digest(61), digest(62), digest(63)]; + let roles = [ + OnTheFlyExternalColorRoleV1::Fundamental, + OnTheFlyExternalColorRoleV1::Antifundamental, + OnTheFlyExternalColorRoleV1::Fundamental, + OnTheFlyExternalColorRoleV1::Antifundamental, + ]; + let mut anchors = (0..4) + .map(|source_slot| { + let mut states = vec![state(source_slot, 0)]; + if source_slot == 0 { + states.push(state(source_slot, 1)); + } + if reverse_inputs { + states.reverse(); + } + OnTheFlySourceAnchorV1::new( + source_slot, + 10 + source_slot, + source_slot < 2, + roles[source_slot as usize], + true, + Some(contracts[source_slot as usize]), + states, + ) + .unwrap() + }) + .collect::>(); + if reverse_inputs { + anchors.reverse(); + } + let alpha = OnTheFlyPairingClassV1::new( + "alpha", + digest(70), + vec![OnTheFlyPairingEndpointV1 { + source_slot: 0, + source_contract_digest: contracts[0], + }], + vec![OnTheFlyPairingEndpointV1 { + source_slot: 1, + source_contract_digest: contracts[1], + }], + ) + .unwrap(); + let bravo = OnTheFlyPairingClassV1::new( + "bravo", + digest(71), + vec![OnTheFlyPairingEndpointV1 { + source_slot: 2, + source_contract_digest: contracts[2], + }], + vec![OnTheFlyPairingEndpointV1 { + source_slot: 3, + source_contract_digest: contracts[3], + }], + ) + .unwrap(); + let pairing_classes = if reverse_inputs { + vec![bravo, alpha] + } else { + vec![alpha, bravo] + }; + OnTheFlyProcessSeedV1::new( + digest(1), + digest(2), + digest(3), + digest(4), + digest(5), + digest(6), + "raw-amplitude-v1", + ExactComplexRational::ONE, + anchors, + vec![2, 0, 3, 1], + OnTheFlyCouplingOrderPolicyV1::Minimal, + vec![1, 2], + coupling_limits, + pairing_classes, + ) + .unwrap() + } + + fn little_u64(bytes: &[u8], offset: usize) -> u64 { + u64::from_le_bytes(bytes[offset..offset + 8].try_into().unwrap()) + } + + fn normalization_length_offset() -> usize { + MAGIC.len() + 4 + 4 + (RETAINED_DIGEST_COUNT * DIGEST_BYTES) as usize + } + + fn source_anchor_count_offset(bytes: &[u8]) -> usize { + let normalization_length_offset = normalization_length_offset(); + normalization_length_offset + 8 + little_u64(bytes, normalization_length_offset) as usize + } + + fn first_source_anchor_offset(bytes: &[u8]) -> usize { + source_anchor_count_offset(bytes) + 8 + } + + fn find_unique_bytes(bytes: &[u8], needle: &[u8]) -> usize { + let mut matches = bytes + .windows(needle.len()) + .enumerate() + .filter_map(|(offset, window)| (window == needle).then_some(offset)); + let offset = matches.next().unwrap(); + assert!(matches.next().is_none(), "test needle is not unique"); + offset + } + + #[test] + fn on_the_fly_seed_codec_round_trips_deterministically() { + let canonical = ordinary_seed(false); + let reordered_inputs = ordinary_seed(true); + assert_eq!(canonical, reordered_inputs); + let first = encode_on_the_fly_process_seed_v1(&canonical).unwrap(); + let second = encode_on_the_fly_process_seed_v1(&reordered_inputs).unwrap(); + assert_eq!(first, second); + let decoded = decode_on_the_fly_process_seed_v1(&first).unwrap(); + assert_eq!(decoded, canonical); + let identity = decoded.identity(); + assert_eq!(identity.abi, ON_THE_FLY_PROCESS_SEED_IDENTITY_ABI); + assert_eq!(identity.process_digest, digest(1).to_string()); + assert_eq!(identity.external_permutation, [2, 0, 3, 1]); + assert_eq!(identity.external_sources.len(), 4); + assert_eq!(identity.external_sources[0].states.len(), 2); + assert_eq!( + identity.external_sources[0].states[0].prepared_mass_parameter_slot, + Some(400) + ); + let encoded_identity = serde_json::to_string(&identity).unwrap(); + let decoded_identity: OnTheFlyProcessSeedIdentityV1 = + serde_json::from_str(&encoded_identity).unwrap(); + assert_eq!(decoded_identity, identity); + } + + #[test] + fn on_the_fly_seed_codec_round_trips_multiple_unbounded_orders() { + let seed = ordinary_seed_with_limits(false, vec![None, None]); + let bytes = encode_on_the_fly_process_seed_v1(&seed).unwrap(); + let decoded = decode_on_the_fly_process_seed_v1(&bytes).unwrap(); + + assert_eq!(decoded.explicit_coupling_limits(), [None, None]); + assert_eq!(decoded, seed); + } + + #[test] + fn on_the_fly_seed_codec_preserves_live_scalar_seed_identity() { + let original = + scalar_adapter_test_seed(digest(81), digest(82), digest(83), digest(84)).unwrap(); + let payload = encode_on_the_fly_process_seed_v1(&original).unwrap(); + let decoded = decode_on_the_fly_process_seed_v1(&payload).unwrap(); + + assert_eq!(decoded.semantic_digest(), original.semantic_digest()); + assert_eq!(decoded, original); + } + + #[test] + fn on_the_fly_seed_codec_decode_encode_is_byte_identical() { + let bytes = encode_on_the_fly_process_seed_v1(&ordinary_seed(false)).unwrap(); + let decoded = decode_on_the_fly_process_seed_v1(&bytes).unwrap(); + assert_eq!(encode_on_the_fly_process_seed_v1(&decoded).unwrap(), bytes); + } + + #[test] + fn on_the_fly_seed_codec_rejects_representative_truncations() { + let bytes = encode_on_the_fly_process_seed_v1(&ordinary_seed(false)).unwrap(); + for length in [ + 0, + 1, + MAGIC.len() - 1, + MAGIC.len(), + MAGIC.len() + 4, + MAGIC.len() + 8, + normalization_length_offset(), + bytes.len() / 2, + bytes.len() - 1, + ] { + assert!( + decode_on_the_fly_process_seed_v1(&bytes[..length]).is_err(), + "truncation at byte {length} was accepted" + ); + } + } + + #[test] + fn on_the_fly_seed_codec_rejects_trailing_bytes() { + let mut bytes = encode_on_the_fly_process_seed_v1(&ordinary_seed(false)).unwrap(); + bytes.push(0xa5); + let error = decode_on_the_fly_process_seed_v1(&bytes).unwrap_err(); + assert!(error.to_string().contains("trailing bytes")); + } + + #[test] + fn on_the_fly_seed_codec_rejects_future_schema_and_hash_revisions() { + let bytes = encode_on_the_fly_process_seed_v1(&ordinary_seed(false)).unwrap(); + let mut future_schema = bytes.clone(); + future_schema[MAGIC.len()..MAGIC.len() + 4] + .copy_from_slice(&(SCHEMA_VERSION + 1).to_le_bytes()); + assert!( + decode_on_the_fly_process_seed_v1(&future_schema) + .unwrap_err() + .to_string() + .contains("schema version") + ); + let mut future_hash = bytes; + future_hash[MAGIC.len() + 4..MAGIC.len() + 8] + .copy_from_slice(&(SEMANTIC_HASH_REVISION + 1).to_le_bytes()); + assert!( + decode_on_the_fly_process_seed_v1(&future_hash) + .unwrap_err() + .to_string() + .contains("semantic hash revision") + ); + } + + #[test] + fn on_the_fly_seed_codec_rejects_malformed_discriminants() { + let bytes = encode_on_the_fly_process_seed_v1(&ordinary_seed(false)).unwrap(); + let anchor = first_source_anchor_offset(&bytes); + for (offset, expected) in [ + (anchor + 8, "boolean discriminant"), + (anchor + 9, "color-role discriminant"), + (anchor + 11, "optional discriminant"), + ] { + let mut malformed = bytes.clone(); + malformed[offset] = 0xff; + let error = decode_on_the_fly_process_seed_v1(&malformed).unwrap_err(); + assert!( + error.to_string().contains(expected), + "unexpected malformed-discriminant error: {error}" + ); + } + } + + #[test] + fn on_the_fly_seed_codec_rejects_duplicate_and_noncanonical_pairing_keys() { + let bytes = encode_on_the_fly_process_seed_v1(&ordinary_seed(false)).unwrap(); + let alpha = find_unique_bytes(&bytes, b"alpha"); + let bravo = find_unique_bytes(&bytes, b"bravo"); + for (offset, replacement) in [(bravo, b"alpha"), (alpha, b"zebra")] { + let mut malformed = bytes.clone(); + malformed[offset..offset + replacement.len()].copy_from_slice(replacement); + let error = decode_on_the_fly_process_seed_v1(&malformed).unwrap_err(); + assert!( + error.to_string().contains("canonical strict order"), + "unexpected pairing-key error: {error}" + ); + } + } + + #[test] + fn on_the_fly_seed_codec_rejects_u64_count_overflow_before_allocation() { + let mut bytes = encode_on_the_fly_process_seed_v1(&ordinary_seed(false)).unwrap(); + let offset = source_anchor_count_offset(&bytes); + bytes[offset..offset + 8].copy_from_slice(&u64::MAX.to_le_bytes()); + let error = decode_on_the_fly_process_seed_v1(&bytes).unwrap_err(); + assert!(error.to_string().contains("overflows u64")); + } + + #[test] + fn on_the_fly_seed_codec_rejects_count_that_cannot_fit_before_allocation() { + let mut bytes = encode_on_the_fly_process_seed_v1(&ordinary_seed(false)).unwrap(); + let offset = source_anchor_count_offset(&bytes); + let hostile_count = u64::try_from(bytes.len()).unwrap() + 1; + bytes[offset..offset + 8].copy_from_slice(&hostile_count.to_le_bytes()); + let error = decode_on_the_fly_process_seed_v1(&bytes).unwrap_err(); + assert!(error.to_string().contains("cannot fit")); + } + + #[test] + fn on_the_fly_seed_codec_checked_writer_rejects_injected_u64_overflow() { + let mut writer = Writer::with_bytes_written(CountingWriter::default(), u64::MAX - 1); + let error = writer.raw(&[1, 2]).unwrap_err(); + assert!(error.to_string().contains("accounting overflows u64")); + assert_eq!(writer.destination.bytes_written, 0); + } +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/recurrence/on_the_fly/source_builder.rs b/pyamplicol/rust/crates/rusticol-core/src/recurrence/on_the_fly/source_builder.rs new file mode 100644 index 0000000000..f85e1dbb94 --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/recurrence/on_the_fly/source_builder.rs @@ -0,0 +1,1725 @@ +// SPDX-License-Identifier: 0BSD + +#![allow(dead_code)] // Generation-bridge helpers are absent from default runtime builds. + +//! Cold-path construction of the compact on-the-fly process seed. +//! +//! The source projection is process-owned but contains no materialized +//! recurrence graph, color plan, public-flow table, or direct plan. Model +//! semantics are recovered from the already validated recurrence-template and +//! prepared direct-executor catalogs before the existing seed codec is used. + +#[cfg(test)] +use std::cell::Cell; +use std::collections::BTreeMap; + +use serde::Deserialize; + +use super::source_seed::{ + OnTheFlyCouplingOrderPolicyV1, OnTheFlyExternalColorRoleV1, OnTheFlyPairingClassV1, + OnTheFlyPairingEndpointV1, OnTheFlyProcessSeedV1, OnTheFlySourceAnchorV1, + OnTheFlySourceOrientationV1, OnTheFlySourceStateV1, OnTheFlySourceWavefunctionFamilyV1, + validate_permutation, +}; +use super::{TemplateCatalog, integrity, invalid}; +use crate::recurrence::process::ProcessSourceStateRow; +use crate::recurrence::template::{ + CurrentOrientation, CurrentStateRow, MISSING_U32, ParameterValueType, ParticleStatistics, + SourceRow, ValidatedRecurrenceTemplateInput, +}; +use crate::recurrence::{ + DirectExecutorRole, ExactComplexRational, PreparedDirectExecutorCatalog, SemanticDigest, +}; +use crate::{RusticolError, RusticolResult}; + +const SOURCE_PROJECTION_SCHEMA_VERSION: u32 = 1; + +#[cfg(test)] +thread_local! { + static PROCESS_SEED_TEMPLATE_CATALOG_BUILD_COUNT: Cell = const { Cell::new(0) }; +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct OnTheFlyProcessSeedProjectionV1 { + schema_version: u32, + process_digest: String, + external_permutation: Vec, + external_sources: Vec, + parameter_projection: Vec, + coupling_order_policy: CouplingOrderPolicyProjection, + coupling_hierarchies: Vec, + coupling_limits: Vec, + fermion_pairing: Option, + normalization: NormalizationProjection, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct SourceAnchorProjection { + source_slot: u32, + public_label: u32, + is_initial: bool, + states: Vec, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct SourceStateProjection { + state_index: u32, + public_helicity: i32, + source_helicity: i32, + source_template_id: u32, + current_state_template_id: u32, + momentum_sign: i32, + crossing_phase: ExactFactorProjection, + spin_state: i32, + chirality: i32, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct ParameterProjection { + parameter_template_id: u32, + prepared_parameter_id: Option, + component: u32, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct CouplingLimitProjection { + name: String, + maximum: u32, +} + +#[derive(Clone, Copy, Debug, Deserialize)] +#[serde(rename_all = "snake_case")] +enum CouplingOrderPolicyProjection { + Minimal, + Explicit, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct CouplingHierarchyProjection { + name: String, + hierarchy: u32, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct FermionPairingProjection { + endpoints: Vec, + classes: Vec, +} + +#[derive(Clone, Copy, Debug, Deserialize)] +#[serde(rename_all = "snake_case")] +enum PairingEndpointOrientation { + Fundamental, + Antifundamental, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct PairingEndpointProjection { + source_slot: u32, + color_orientation: PairingEndpointOrientation, + contract_digest: String, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct PairingClassProjection { + species: String, + proof_digest: String, + fundamental_source_slots: Vec, + antifundamental_source_slots: Vec, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct NormalizationProjection { + factor: ExactFactorProjection, + convention: String, + semantic_digest: String, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct ExactFactorProjection { + real_numerator: String, + real_denominator: String, + imag_numerator: String, + imag_denominator: String, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct SourceCrossingProjection { + momentum_transform: SourceMomentumTransformProjection, + helicity_factor: i32, + chirality_factor: i32, + spin_state_factor: i32, + phase: ExactFactorProjection, +} + +#[derive(Clone, Copy, Debug, Deserialize)] +enum SourceMomentumTransformProjection { + #[serde(rename = "identity")] + Identity, + #[serde(rename = "negate-four-momentum")] + NegateFourMomentum, +} + +#[derive(Clone, Copy, Debug)] +struct AppliedSourceCrossing { + momentum_sign: i32, + helicity_factor: i32, + chirality_factor: i32, + spin_state_factor: i32, + phase: ExactComplexRational, +} + +impl AppliedSourceCrossing { + const IDENTITY: Self = Self { + momentum_sign: 1, + helicity_factor: 1, + chirality_factor: 1, + spin_state_factor: 1, + phase: ExactComplexRational::ONE, + }; +} + +impl ExactFactorProjection { + fn exact(&self, label: &str) -> RusticolResult { + ExactComplexRational::parse_parts( + &self.real_numerator, + &self.real_denominator, + &self.imag_numerator, + &self.imag_denominator, + ) + .map_err(|error| invalid(format!("{label} is invalid: {error}"))) + } +} + +pub(crate) fn parse_on_the_fly_process_seed_projection_v1( + bytes: &[u8], +) -> RusticolResult { + if bytes.is_empty() { + return Err(invalid( + "on-the-fly source projection JSON must not be empty", + )); + } + let projection: OnTheFlyProcessSeedProjectionV1 = + serde_json::from_slice(bytes).map_err(|error| { + invalid(format!( + "invalid on-the-fly source projection JSON: {error}" + )) + })?; + if projection.schema_version != SOURCE_PROJECTION_SCHEMA_VERSION { + return Err(invalid(format!( + "unsupported on-the-fly source projection schema version {}", + projection.schema_version + ))); + } + Ok(projection) +} + +/// Build one compact process seed from source-only process projection and +/// already validated model-wide catalogs. +/// +/// This deliberately has no argument capable of carrying the established +/// recurrence builder, a color-flow plan, a DAG, or a `DirectRecurrencePlan`. +pub(crate) fn build_on_the_fly_process_seed_v1( + projection: OnTheFlyProcessSeedProjectionV1, + templates: &ValidatedRecurrenceTemplateInput, + direct: &PreparedDirectExecutorCatalog, + prepared_pack_digest: SemanticDigest, +) -> RusticolResult { + let mut seeds = build_on_the_fly_process_seeds_v1( + vec![projection], + templates, + direct, + prepared_pack_digest, + )?; + seeds + .pop() + .ok_or_else(|| RusticolError::internal("singleton process-seed batch returned no seed")) +} + +/// Build ordered compact process seeds while authenticating and indexing the +/// shared model catalogs exactly once. +pub(crate) fn build_on_the_fly_process_seeds_v1( + projections: Vec, + templates: &ValidatedRecurrenceTemplateInput, + direct: &PreparedDirectExecutorCatalog, + prepared_pack_digest: SemanticDigest, +) -> RusticolResult> { + let summary = templates.summary(); + if prepared_pack_digest != summary.prepared_kernel_pack_digest { + return Err(integrity( + "on-the-fly source projection names a different prepared-kernel pack", + )); + } + + #[cfg(test)] + PROCESS_SEED_TEMPLATE_CATALOG_BUILD_COUNT.with(|count| count.set(count.get() + 1)); + let template_catalog = TemplateCatalog::new(templates.input())?; + let mut seeds = Vec::new(); + seeds + .try_reserve_exact(projections.len()) + .map_err(|error| invalid(format!("process-seed allocation failed: {error}")))?; + for (index, projection) in projections.into_iter().enumerate() { + let process_digest = projection.process_digest.clone(); + let seed = build_on_the_fly_process_seed_from_catalog_v1( + projection, + templates, + &template_catalog, + direct, + prepared_pack_digest, + ) + .map_err(|error| { + RusticolError::with_kind( + error.kind(), + format!( + "on-the-fly process seed at index {index} (process digest {process_digest:?}) failed: {error}" + ), + ) + })?; + seeds.push(seed); + } + Ok(seeds) +} + +fn build_on_the_fly_process_seed_from_catalog_v1( + projection: OnTheFlyProcessSeedProjectionV1, + templates: &ValidatedRecurrenceTemplateInput, + template_catalog: &TemplateCatalog<'_>, + direct: &PreparedDirectExecutorCatalog, + prepared_pack_digest: SemanticDigest, +) -> RusticolResult { + let summary = templates.summary(); + validate_permutation( + &projection.external_permutation, + projection.external_sources.len(), + "source-projection external permutation", + )?; + + let endpoint_contracts = pairing_endpoint_contracts(projection.fermion_pairing.as_ref())?; + validate_parameter_projection(&projection.parameter_projection)?; + let source_anchors = source_anchors( + &projection.external_sources, + templates, + template_catalog, + direct, + &endpoint_contracts, + &projection.parameter_projection, + )?; + let pairing_classes = + pairing_classes(projection.fermion_pairing.as_ref(), &endpoint_contracts)?; + let (coupling_order_policy, coupling_hierarchies, coupling_limits) = coupling_policy( + projection.coupling_order_policy, + &projection.coupling_hierarchies, + &projection.coupling_limits, + template_catalog, + )?; + let normalization_factor = projection + .normalization + .factor + .exact("on-the-fly normalization factor")?; + + OnTheFlyProcessSeedV1::new( + parse_digest(&projection.process_digest, "process digest")?, + summary.compiled_model_digest, + summary.catalog_digest, + prepared_pack_digest, + direct.direct_template_catalog_digest(), + parse_digest( + &projection.normalization.semantic_digest, + "normalization semantic digest", + )?, + projection.normalization.convention, + normalization_factor, + source_anchors, + projection.external_permutation, + coupling_order_policy, + coupling_hierarchies, + coupling_limits, + pairing_classes, + ) +} + +fn source_anchors( + projections: &[SourceAnchorProjection], + templates: &ValidatedRecurrenceTemplateInput, + catalog: &TemplateCatalog<'_>, + direct: &PreparedDirectExecutorCatalog, + endpoint_contracts: &BTreeMap, + parameter_projection: &[ParameterProjection], +) -> RusticolResult> { + let input = templates.input(); + let mut anchors = Vec::new(); + anchors + .try_reserve_exact(projections.len()) + .map_err(|error| invalid(format!("source-anchor allocation failed: {error}")))?; + for projection in projections { + let mut states = Vec::new(); + states + .try_reserve_exact(projection.states.len()) + .map_err(|error| invalid(format!("source-state allocation failed: {error}")))?; + let mut role = None; + let mut anchor_fermionic = None; + let mut pairing_contract = None; + for state in &projection.states { + let source = canonical_source(input, state.source_template_id)?; + let current_state = canonical_current_state(input, state.current_state_template_id)?; + validate_source_projection( + projection.source_slot, + projection.is_initial, + state, + source, + current_state, + input, + catalog, + )?; + direct.resolve_evaluator(DirectExecutorRole::Source, source.evaluator_binding_id)?; + let family = source_family(catalog, source, current_state)?; + let (state_role, state_pairing_contract) = color_role( + current_state.color_representation, + endpoint_contracts.get(&projection.source_slot).copied(), + )?; + merge_equal(&mut role, state_role, "one source anchor mixes color roles")?; + merge_equal( + &mut anchor_fermionic, + family.is_fermionic(), + "one source anchor mixes statistics", + )?; + merge_equal_option( + &mut pairing_contract, + state_pairing_contract, + "one source anchor mixes pairing contracts", + )?; + let color_seed = catalog.source_seed(source)?; + states.push(OnTheFlySourceStateV1::new( + state.state_index, + state.public_helicity, + state.source_helicity, + source.id, + current_state.id, + catalog.digest(source.semantic_digest_id, "source semantic")?, + catalog.digest(current_state.semantic_digest_id, "current-state semantic")?, + state.momentum_sign, + state.crossing_phase.exact("source crossing phase")?, + state.spin_state, + state.chirality, + catalog + .flavour_flow(source.flavour_flow_id, "source flavour flow")? + .to_vec(), + source.quantum_number_flow_id, + color_seed.proof_digest(), + family, + source_orientation(current_state)?, + resolve_prepared_mass_slot( + source, + current_state, + input, + catalog, + parameter_projection.iter().map(|row| { + ( + row.parameter_template_id, + row.prepared_parameter_id, + row.component, + ) + }), + )?, + )?); + } + anchors.push(OnTheFlySourceAnchorV1::new( + projection.source_slot, + projection.public_label, + projection.is_initial, + role.ok_or_else(|| integrity("source anchor is empty"))?, + anchor_fermionic.ok_or_else(|| integrity("source statistics are absent"))?, + pairing_contract, + states, + )?); + } + Ok(anchors) +} + +fn canonical_source( + input: &crate::recurrence::template::OwnedRecurrenceTemplateInput, + source_template_id: u32, +) -> RusticolResult { + let source = *input + .sources + .get(source_template_id as usize) + .ok_or_else(|| integrity("source template is absent"))?; + if source.id != source_template_id { + return Err(integrity("source-template catalog is not canonical")); + } + Ok(source) +} + +fn canonical_current_state( + input: &crate::recurrence::template::OwnedRecurrenceTemplateInput, + current_state_template_id: u32, +) -> RusticolResult { + let state = *input + .current_states + .get(current_state_template_id as usize) + .ok_or_else(|| integrity("current-state template is absent"))?; + if state.id != current_state_template_id { + return Err(integrity("current-state catalog is not canonical")); + } + Ok(state) +} + +fn validate_source_projection( + source_slot: u32, + is_initial: bool, + projected: &SourceStateProjection, + source: SourceRow, + effective: CurrentStateRow, + input: &crate::recurrence::template::OwnedRecurrenceTemplateInput, + catalog: &TemplateCatalog<'_>, +) -> RusticolResult<()> { + let canonical = canonical_current_state(input, source.state_template_id)?; + super::validate_crossed_source_state( + is_initial, + &ProcessSourceStateRow { + source_slot, + state_index: projected.state_index, + public_helicity: projected.public_helicity, + chirality: projected.chirality, + spin_state: projected.spin_state, + current_state_template_id: projected.current_state_template_id, + source_template_id: projected.source_template_id, + momentum_sign: projected.momentum_sign, + crossing_phase_factor_id: 0, + }, + source, + input, + )?; + + let crossing = applied_source_crossing(is_initial, source, catalog)?; + let expected_helicity = source + .helicity + .checked_mul(crossing.helicity_factor) + .ok_or_else(|| integrity("source crossing overflows the helicity state"))?; + let expected_chirality = canonical + .chirality + .checked_mul(crossing.chirality_factor) + .ok_or_else(|| integrity("source crossing overflows the chirality state"))?; + let expected_spin_state = source + .spin_state + .checked_mul(crossing.spin_state_factor) + .ok_or_else(|| integrity("source crossing overflows the spin state"))?; + let projected_phase = projected.crossing_phase.exact("source crossing phase")?; + if projected.public_helicity != expected_helicity + || projected.source_helicity != expected_helicity + || projected.chirality != expected_chirality + || effective.chirality != expected_chirality + || projected.spin_state != expected_spin_state + || projected.momentum_sign != crossing.momentum_sign + || projected_phase != crossing.phase + { + return Err(integrity( + "crossed source execution values disagree with their prepared source/current templates", + )); + } + Ok(()) +} + +fn applied_source_crossing( + is_initial: bool, + source: SourceRow, + catalog: &TemplateCatalog<'_>, +) -> RusticolResult { + if !is_initial { + return Ok(AppliedSourceCrossing::IDENTITY); + } + let encoded = catalog.string(source.crossing_string_id, "source crossing")?; + let crossing: SourceCrossingProjection = serde_json::from_str(encoded) + .map_err(|error| integrity(format!("source crossing contract is invalid: {error}")))?; + if !matches!(crossing.helicity_factor, -1 | 1) + || !matches!(crossing.chirality_factor, -1 | 1) + || !matches!(crossing.spin_state_factor, -1 | 1) + { + return Err(integrity( + "source crossing contract has a non-sign state factor", + )); + } + let phase = crossing.phase.exact("source crossing phase")?; + if phase.is_zero() { + return Err(integrity("source crossing phase is zero")); + } + Ok(AppliedSourceCrossing { + momentum_sign: match crossing.momentum_transform { + SourceMomentumTransformProjection::Identity => 1, + SourceMomentumTransformProjection::NegateFourMomentum => -1, + }, + helicity_factor: crossing.helicity_factor, + chirality_factor: crossing.chirality_factor, + spin_state_factor: crossing.spin_state_factor, + phase, + }) +} + +fn source_family( + catalog: &TemplateCatalog<'_>, + source: SourceRow, + current_state: CurrentStateRow, +) -> RusticolResult { + let family = catalog.string( + source.wavefunction_family_string_id, + "source wavefunction family", + )?; + let statistics = ParticleStatistics::try_from(current_state.statistics)?; + match (family, statistics, current_state.dimension) { + ("scalar", ParticleStatistics::Boson, 1) => Ok(OnTheFlySourceWavefunctionFamilyV1::Scalar), + ("fermion", ParticleStatistics::Fermion, 2) => { + Ok(OnTheFlySourceWavefunctionFamilyV1::WeylFermion) + } + ("fermion", ParticleStatistics::Fermion, 4) => { + Ok(OnTheFlySourceWavefunctionFamilyV1::DiracFermion) + } + ("vector", ParticleStatistics::Boson, 4) => Ok(OnTheFlySourceWavefunctionFamilyV1::Vector), + ("spin2", ParticleStatistics::Boson, 16) => Ok(OnTheFlySourceWavefunctionFamilyV1::Spin2), + ("ghost" | "auxiliary", _, _) => Err(invalid( + "on-the-fly source execution does not support ghost or auxiliary external states", + )), + _ => Err(integrity( + "source family, statistics, and component dimension disagree", + )), + } +} + +fn source_orientation( + current_state: CurrentStateRow, +) -> RusticolResult { + Ok( + match CurrentOrientation::try_from(current_state.orientation)? { + CurrentOrientation::Particle => OnTheFlySourceOrientationV1::Particle, + CurrentOrientation::Antiparticle => OnTheFlySourceOrientationV1::Antiparticle, + CurrentOrientation::SelfConjugate => OnTheFlySourceOrientationV1::SelfConjugate, + }, + ) +} + +fn color_role( + representation: i32, + endpoint: Option<(OnTheFlyExternalColorRoleV1, SemanticDigest)>, +) -> RusticolResult<(OnTheFlyExternalColorRoleV1, Option)> { + match (representation, endpoint) { + (1, None) => Ok((OnTheFlyExternalColorRoleV1::Singlet, None)), + (8, None) => Ok((OnTheFlyExternalColorRoleV1::Adjoint, None)), + (3, Some((OnTheFlyExternalColorRoleV1::Fundamental, digest))) => { + Ok((OnTheFlyExternalColorRoleV1::Fundamental, Some(digest))) + } + (-3, Some((OnTheFlyExternalColorRoleV1::Antifundamental, digest))) => { + Ok((OnTheFlyExternalColorRoleV1::Antifundamental, Some(digest))) + } + _ => Err(integrity( + "source color representation disagrees with the compact pairing endpoints", + )), + } +} + +fn validate_parameter_projection(rows: &[ParameterProjection]) -> RusticolResult<()> { + let mut result = BTreeMap::new(); + for row in rows.iter().filter(|row| row.component == 0) { + let Some(prepared) = row.prepared_parameter_id else { + continue; + }; + if result.insert(row.parameter_template_id, prepared).is_some() { + return Err(integrity( + "parameter projection repeats a real prepared parameter", + )); + } + } + Ok(()) +} + +/// Resolve the prepared real-parameter slot used by one authenticated source. +/// +/// Explicit template IDs remain authoritative. Legacy catalogs in which both +/// source rows omit that ID may recover it only from the exact canonical +/// particle-mass parameter name authenticated by the current-state +/// orientation. Keeping this resolver shared with the feature-only oracle +/// prevents diagnostic reconstruction from silently using different mass +/// semantics than the compact production seed. +pub(super) fn resolve_prepared_mass_slot( + source: SourceRow, + current_state: CurrentStateRow, + input: &crate::recurrence::template::OwnedRecurrenceTemplateInput, + catalog: &TemplateCatalog<'_>, + projection: impl IntoIterator, u32)>, +) -> RusticolResult> { + if source.mass_parameter_id != current_state.mass_parameter_id { + return Err(integrity( + "source and current state disagree on their mass parameter", + )); + } + + let parameter_id = if source.mass_parameter_id != MISSING_U32 { + source.mass_parameter_id + } else { + let canonical_particle = match CurrentOrientation::try_from(current_state.orientation)? { + CurrentOrientation::Particle => current_state.particle_id, + CurrentOrientation::Antiparticle => current_state.anti_particle_id, + CurrentOrientation::SelfConjugate => { + if current_state.particle_id != current_state.anti_particle_id { + return Err(integrity( + "self-conjugate source mass orientation has distinct particle labels", + )); + } + current_state.particle_id + } + }; + let expected_name = format!("particle.{canonical_particle}.mass"); + let mut match_id = None; + for (index, parameter) in input.parameters.iter().enumerate() { + if usize::try_from(parameter.id).ok() != Some(index) { + return Err(integrity("mass parameter catalog is not canonical")); + } + if catalog.string(parameter.name_string_id, "mass parameter name")? != expected_name { + continue; + } + if match_id.replace(parameter.id).is_some() { + return Err(integrity(format!( + "canonical source mass parameter {expected_name:?} is ambiguous" + ))); + } + } + let Some(parameter_id) = match_id else { + return Ok(None); + }; + parameter_id + }; + + let parameter = input + .parameters + .get(parameter_id as usize) + .ok_or_else(|| integrity("source mass parameter is absent"))?; + if parameter.id != parameter_id { + return Err(integrity("source mass parameter catalog is not canonical")); + } + if ParameterValueType::try_from(parameter.value_type)? != ParameterValueType::Real { + return Err(integrity("source mass parameter is not real")); + } + let catalog_prepared = parameter.prepared_parameter_id; + if catalog_prepared == MISSING_U32 { + return Err(invalid( + "source mass parameter has no prepared execution slot", + )); + } + let prepared_slot = usize::try_from(catalog_prepared) + .map_err(|_| integrity("source mass prepared slot exceeds usize"))?; + if prepared_slot >= input.parameters.len() { + return Err(integrity( + "source mass prepared slot is outside the dense parameter arena", + )); + } + if input + .parameters + .iter() + .filter(|candidate| candidate.prepared_parameter_id == catalog_prepared) + .count() + != 1 + { + return Err(integrity( + "source mass prepared slot has conflicting catalog owners", + )); + } + + let matches = projection + .into_iter() + .filter(|(projected_id, _, component)| *projected_id == parameter_id && *component == 0) + .collect::>(); + let [(_, projected_prepared, _)] = matches.as_slice() else { + return Err(integrity(format!( + "source mass parameter has {} component-0 projections, expected one", + matches.len() + ))); + }; + let projected_prepared = (*projected_prepared) + .ok_or_else(|| invalid("source mass parameter has no prepared real projection"))?; + if projected_prepared != catalog_prepared { + return Err(integrity( + "source mass projection disagrees with the prepared template catalog", + )); + } + Ok(Some(catalog_prepared)) +} + +/// Rebind every decoded seed source to the authenticated model-wide parameter +/// catalog before the runtime turns its prepared slots into executor inputs. +/// This is a cold load-time check; selected-query execution never reaches it. +pub(crate) fn validate_on_the_fly_source_mass_bindings_v1( + seed: &OnTheFlyProcessSeedV1, + templates: &ValidatedRecurrenceTemplateInput, + parameter_projection: &[(u32, Option, u32)], +) -> RusticolResult<()> { + let input = templates.input(); + let catalog = TemplateCatalog::new(input)?; + for anchor in seed.source_anchors() { + for state in anchor.states() { + let source = canonical_source(input, state.source_template_id)?; + let current_state = canonical_current_state(input, state.current_state_template_id)?; + let expected = resolve_prepared_mass_slot( + source, + current_state, + input, + &catalog, + parameter_projection.iter().copied(), + )?; + if state.prepared_mass_parameter_slot != expected { + return Err(integrity( + "on-the-fly source prepared mass slot disagrees with the authenticated recurrence template", + )); + } + } + } + Ok(()) +} + +fn coupling_policy( + policy: CouplingOrderPolicyProjection, + hierarchy_rows: &[CouplingHierarchyProjection], + limit_rows: &[CouplingLimitProjection], + templates: &TemplateCatalog<'_>, +) -> RusticolResult<(OnTheFlyCouplingOrderPolicyV1, Vec, Vec>)> { + let mut hierarchies_by_name = BTreeMap::new(); + for row in hierarchy_rows { + if row.name.is_empty() + || row.hierarchy == 0 + || hierarchies_by_name + .insert(row.name.as_str(), row.hierarchy) + .is_some() + { + return Err(invalid( + "coupling hierarchies require nonempty unique names and positive weights", + )); + } + } + let mut limits_by_name = BTreeMap::new(); + for row in limit_rows { + if row.name.is_empty() + || limits_by_name + .insert(row.name.as_str(), row.maximum) + .is_some() + { + return Err(invalid( + "coupling limits require nonempty unique model-order names", + )); + } + } + let expected = templates.coupling_order_names(); + if limit_rows + .iter() + .any(|row| expected.binary_search(&row.name.as_str()).is_err()) + { + return Err(integrity( + "source projection names a coupling limit absent from the template catalog", + )); + } + let hierarchies = expected + .iter() + .map(|name| { + hierarchies_by_name.get(name).copied().ok_or_else(|| { + integrity(format!( + "source projection has no coupling hierarchy for model order {name:?}" + )) + }) + }) + .collect::>>()?; + let limits = expected + .iter() + .map(|name| limits_by_name.get(name).copied()) + .collect::>(); + if hierarchies_by_name.len() != expected.len() || limits_by_name.len() > expected.len() { + return Err(integrity( + "source projection names a coupling hierarchy or limit absent from the template catalog", + )); + } + Ok(( + match policy { + CouplingOrderPolicyProjection::Minimal => OnTheFlyCouplingOrderPolicyV1::Minimal, + CouplingOrderPolicyProjection::Explicit => OnTheFlyCouplingOrderPolicyV1::Explicit, + }, + hierarchies, + limits, + )) +} + +fn pairing_endpoint_contracts( + pairing: Option<&FermionPairingProjection>, +) -> RusticolResult> { + let Some(pairing) = pairing else { + return Ok(BTreeMap::new()); + }; + let mut result = BTreeMap::new(); + for endpoint in &pairing.endpoints { + let role = match endpoint.color_orientation { + PairingEndpointOrientation::Fundamental => OnTheFlyExternalColorRoleV1::Fundamental, + PairingEndpointOrientation::Antifundamental => { + OnTheFlyExternalColorRoleV1::Antifundamental + } + }; + let digest = parse_digest( + &endpoint.contract_digest, + "pairing endpoint contract digest", + )?; + if result + .insert(endpoint.source_slot, (role, digest)) + .is_some() + { + return Err(integrity("pairing endpoint repeats a source slot")); + } + } + Ok(result) +} + +fn pairing_classes( + pairing: Option<&FermionPairingProjection>, + contracts: &BTreeMap, +) -> RusticolResult> { + let Some(pairing) = pairing else { + if contracts.is_empty() { + return Ok(Vec::new()); + } + return Err(integrity("pairing endpoints have no pairing classes")); + }; + pairing + .classes + .iter() + .map(|class| { + let endpoints = |slots: &[u32], + role: OnTheFlyExternalColorRoleV1| + -> RusticolResult> { + slots + .iter() + .copied() + .map(|source_slot| { + let (actual_role, digest) = contracts + .get(&source_slot) + .copied() + .ok_or_else(|| integrity("pairing class endpoint is absent"))?; + if actual_role != role { + return Err(integrity("pairing class endpoint role changed")); + } + Ok(OnTheFlyPairingEndpointV1 { + source_slot, + source_contract_digest: digest, + }) + }) + .collect() + }; + OnTheFlyPairingClassV1::new( + class.species.as_str(), + parse_digest(&class.proof_digest, "pairing class proof digest")?, + endpoints( + &class.fundamental_source_slots, + OnTheFlyExternalColorRoleV1::Fundamental, + )?, + endpoints( + &class.antifundamental_source_slots, + OnTheFlyExternalColorRoleV1::Antifundamental, + )?, + ) + }) + .collect() +} + +fn parse_digest(value: &str, label: &str) -> RusticolResult { + if value.len() != 64 + || !value + .bytes() + .all(|byte| byte.is_ascii_digit() || matches!(byte, b'a'..=b'f')) + { + return Err(invalid(format!( + "{label} must be a lowercase SHA-256 digest" + ))); + } + let mut bytes = [0_u8; 32]; + for (index, byte) in bytes.iter_mut().enumerate() { + let offset = index * 2; + *byte = u8::from_str_radix(&value[offset..offset + 2], 16) + .map_err(|_| invalid(format!("{label} is not hexadecimal")))?; + } + SemanticDigest::new(bytes).map_err(|error| RusticolError::integrity(error.message())) +} + +fn merge_equal(slot: &mut Option, value: T, message: &str) -> RusticolResult<()> { + match slot { + None => *slot = Some(value), + Some(previous) if *previous == value => {} + Some(_) => return Err(integrity(message)), + } + Ok(()) +} + +fn merge_equal_option( + slot: &mut Option, + value: Option, + message: &str, +) -> RusticolResult<()> { + match (*slot, value) { + (None, Some(value)) => *slot = Some(value), + (None, None) | (Some(_), None) => {} + (Some(previous), Some(value)) if previous == value => {} + (Some(_), Some(_)) => return Err(integrity(message)), + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::recurrence::on_the_fly::seed_codec::{ + decode_on_the_fly_process_seed_v1, encode_on_the_fly_process_seed_v1, + }; + use crate::recurrence::on_the_fly::{OnTheFlyForbiddenWorkGuardV1, scalar_adapter_test_seed}; + use crate::recurrence::template::{ + CouplingOrderTermRow, IndexedRangeRow, ParameterKind, ParameterRow, ParameterValueType, + }; + use crate::recurrence::{ + CheckedTableRange, PreparedDirectExecutorBinding, validated_template_fixture, + }; + use serde_json::{Value, json}; + + fn digest(seed: u8) -> SemanticDigest { + SemanticDigest::new([seed; 32]).unwrap() + } + + fn scalar_inputs() -> ( + Value, + ValidatedRecurrenceTemplateInput, + PreparedDirectExecutorCatalog, + ) { + let mut input = validated_template_fixture().into_input(); + input.coupling_order_ranges.push(IndexedRangeRow { + id: 1, + range: CheckedTableRange::new(0, 1), + }); + input.coupling_order_terms.push(CouplingOrderTermRow { + set_id: 1, + name_string_id: 0, + power: 1, + }); + let templates = input.validate().unwrap(); + let direct = PreparedDirectExecutorCatalog::new( + digest(40), + vec![ + PreparedDirectExecutorBinding::evaluator(DirectExecutorRole::Source, 0, 0), + PreparedDirectExecutorBinding::evaluator(DirectExecutorRole::Closure, 3, 1), + PreparedDirectExecutorBinding::identity_finalizer(2), + ], + ) + .unwrap(); + let source = json!({ + "schema_version": 1, + "process_digest": digest(91).to_string(), + "external_permutation": [0, 1], + "external_sources": [ + { + "source_slot": 0, + "public_label": 0, + "is_initial": false, + "states": [{ + "state_index": 0, + "public_helicity": 0, + "source_helicity": 0, + "source_template_id": 0, + "current_state_template_id": 0, + "momentum_sign": 1, + "crossing_phase": { + "real_numerator": "1", "real_denominator": "1", + "imag_numerator": "0", "imag_denominator": "1" + }, + "spin_state": 50000, + "chirality": 0 + }] + }, + { + "source_slot": 1, + "public_label": 1, + "is_initial": false, + "states": [{ + "state_index": 0, + "public_helicity": 0, + "source_helicity": 0, + "source_template_id": 0, + "current_state_template_id": 0, + "momentum_sign": 1, + "crossing_phase": { + "real_numerator": "1", "real_denominator": "1", + "imag_numerator": "0", "imag_denominator": "1" + }, + "spin_state": 50000, + "chirality": 0 + }] + } + ], + "parameter_projection": [], + "coupling_order_policy": "explicit", + "coupling_hierarchies": [{"name": "0", "hierarchy": 1}], + "coupling_limits": [{"name": "0", "maximum": 0}], + "fermion_pairing": null, + "normalization": { + "factor": { + "real_numerator": "1", "real_denominator": "1", + "imag_numerator": "0", "imag_denominator": "1" + }, + "convention": "raw-amplitude-test", + "semantic_digest": digest(92).to_string() + } + }); + (source, templates, direct) + } + + fn crossed_scalar_inputs() -> ( + Value, + ValidatedRecurrenceTemplateInput, + PreparedDirectExecutorCatalog, + ) { + let (mut projection, templates, direct) = scalar_inputs(); + let mut input = templates.into_input(); + let crossing = serde_json::to_vec(&json!({ + "chirality_factor": -1, + "helicity_factor": -1, + "momentum_transform": "negate-four-momentum", + "phase": { + "imag_denominator": "1", + "imag_numerator": "0", + "real_denominator": "1", + "real_numerator": "-1" + }, + "spin_state_factor": -1 + })) + .unwrap(); + let previous = input.string_ranges.last().unwrap(); + let previous = previous + .as_usize_range(input.string_bytes.len(), "template string") + .unwrap(); + assert!(&input.string_bytes[previous] < crossing.as_slice()); + let crossing_string_id = u32::try_from(input.string_ranges.len()).unwrap(); + input.string_ranges.push(CheckedTableRange::new( + u64::try_from(input.string_bytes.len()).unwrap(), + u64::try_from(crossing.len()).unwrap(), + )); + input.string_bytes.extend_from_slice(&crossing); + input.sources[0].crossing_string_id = crossing_string_id; + input.sources[0].helicity = 1; + let templates = input.validate().unwrap(); + + for source_slot in 0..2 { + projection["external_sources"][source_slot]["is_initial"] = json!(true); + let state = &mut projection["external_sources"][source_slot]["states"][0]; + state["public_helicity"] = json!(-1); + state["source_helicity"] = json!(-1); + state["momentum_sign"] = json!(-1); + state["crossing_phase"]["real_numerator"] = json!("-1"); + state["spin_state"] = json!(-50_000); + } + (projection, templates, direct) + } + + fn mass_bound_scalar_inputs() -> ( + Value, + ValidatedRecurrenceTemplateInput, + PreparedDirectExecutorCatalog, + ) { + let (mut projection, templates, direct) = scalar_inputs(); + let mut input = templates.into_input(); + let source_evaluator = input.evaluator_bindings[0]; + input.catalog_header[0].parameter_count = 1; + input.parameters.push(ParameterRow { + id: 0, + template_string_id: source_evaluator.resolver_key_string_id, + name_string_id: source_evaluator.resolver_key_string_id, + kind: ParameterKind::External as u8, + value_type: ParameterValueType::Real as u8, + mutable: 1, + default_factor_id: 0, + exact_expression_digest_id: MISSING_U32, + dependency_sequence_id: source_evaluator.input_state_sequence_id, + prepared_parameter_id: 0, + semantic_digest_id: source_evaluator.callable_signature_digest_id, + }); + input.current_states[0].mass_parameter_id = 0; + input.sources[0].mass_parameter_id = 0; + projection["parameter_projection"] = json!([{ + "parameter_template_id": 0, + "prepared_parameter_id": 0, + "component": 0 + }]); + (projection, input.validate().unwrap(), direct) + } + + fn replace_template_string( + input: &mut crate::recurrence::template::OwnedRecurrenceTemplateInput, + old: &str, + new: &str, + ) -> u32 { + let mut strings = input + .string_ranges + .iter() + .map(|range| { + let range = range + .as_usize_range(input.string_bytes.len(), "test template string") + .unwrap(); + std::str::from_utf8(&input.string_bytes[range]) + .unwrap() + .to_owned() + }) + .collect::>(); + let id = strings.iter().position(|value| value == old).unwrap(); + strings[id] = new.to_owned(); + assert!(strings.windows(2).all(|pair| pair[0] < pair[1])); + + input.string_ranges.clear(); + input.string_bytes.clear(); + for value in strings { + input.string_ranges.push(CheckedTableRange::new( + u64::try_from(input.string_bytes.len()).unwrap(), + u64::try_from(value.len()).unwrap(), + )); + input.string_bytes.extend_from_slice(value.as_bytes()); + } + u32::try_from(id).unwrap() + } + + fn legacy_null_mass_inputs( + particle_id: i32, + anti_particle_id: i32, + orientation: CurrentOrientation, + ) -> ( + Value, + ValidatedRecurrenceTemplateInput, + PreparedDirectExecutorCatalog, + ) { + let (mut projection, templates, direct) = scalar_inputs(); + let mut input = templates.into_input(); + let source_evaluator = input.evaluator_bindings[0]; + let mass_name_string_id = + replace_template_string(&mut input, "projection", "particle.6.mass"); + input.catalog_header[0].parameter_count = 1; + input.parameters.push(ParameterRow { + id: 0, + template_string_id: source_evaluator.resolver_key_string_id, + name_string_id: mass_name_string_id, + kind: ParameterKind::External as u8, + value_type: ParameterValueType::Real as u8, + mutable: 1, + default_factor_id: 0, + exact_expression_digest_id: MISSING_U32, + dependency_sequence_id: source_evaluator.input_state_sequence_id, + prepared_parameter_id: 0, + semantic_digest_id: source_evaluator.callable_signature_digest_id, + }); + input.current_states[0].particle_id = particle_id; + input.current_states[0].anti_particle_id = anti_particle_id; + input.current_states[0].orientation = orientation as u8; + input.flavour_flow_values[0] = particle_id; + assert_eq!(input.current_states[0].mass_parameter_id, MISSING_U32); + assert_eq!(input.sources[0].mass_parameter_id, MISSING_U32); + projection["parameter_projection"] = json!([{ + "parameter_template_id": 0, + "prepared_parameter_id": 0, + "component": 0 + }]); + (projection, input.validate().unwrap(), direct) + } + + fn build_test_seed( + source: &Value, + templates: &ValidatedRecurrenceTemplateInput, + direct: &PreparedDirectExecutorCatalog, + ) -> RusticolResult { + build_on_the_fly_process_seed_v1( + parse_on_the_fly_process_seed_projection_v1(&serde_json::to_vec(source).unwrap())?, + templates, + direct, + templates.summary().prepared_kernel_pack_digest, + ) + } + + #[test] + fn compact_projection_matches_the_established_seed_codec_exactly() { + let (source, templates, direct) = scalar_inputs(); + let bytes = serde_json::to_vec(&source).unwrap(); + let projection = parse_on_the_fly_process_seed_projection_v1(&bytes).unwrap(); + let guard = OnTheFlyForbiddenWorkGuardV1::begin().unwrap(); + let seed = build_on_the_fly_process_seed_v1( + projection, + &templates, + &direct, + templates.summary().prepared_kernel_pack_digest, + ) + .unwrap(); + let forbidden = guard.finish().unwrap(); + assert_eq!(forbidden.direct_plan_load_attempts, 0); + assert_eq!(forbidden.direct_plan_decode_attempts, 0); + assert_eq!(forbidden.direct_plan_materialization_attempts, 0); + assert_eq!(forbidden.established_builder_attempts, 0); + + let expected = scalar_adapter_test_seed( + templates.summary().compiled_model_digest, + templates.summary().catalog_digest, + templates.summary().prepared_kernel_pack_digest, + direct.direct_template_catalog_digest(), + ) + .unwrap(); + assert_eq!(seed, expected); + assert_eq!( + encode_on_the_fly_process_seed_v1(&seed).unwrap(), + encode_on_the_fly_process_seed_v1(&expected).unwrap() + ); + } + + #[test] + fn crossed_execution_state_is_authenticated_and_round_trips_exactly() { + let (source, templates, direct) = crossed_scalar_inputs(); + let build = |value: &Value| { + build_on_the_fly_process_seed_v1( + parse_on_the_fly_process_seed_projection_v1(&serde_json::to_vec(value).unwrap())?, + &templates, + &direct, + templates.summary().prepared_kernel_pack_digest, + ) + }; + let seed = build(&source).unwrap(); + for anchor in seed.source_anchors() { + let state = &anchor.states()[0]; + assert!(anchor.is_initial()); + assert_eq!(state.public_helicity, -1); + assert_eq!(state.source_helicity, -1); + assert_eq!(state.spin_state, -50_000); + assert_eq!(state.momentum_sign, -1); + assert_eq!( + state.crossing_phase, + ExactComplexRational::ONE.checked_neg().unwrap() + ); + } + let encoded = encode_on_the_fly_process_seed_v1(&seed).unwrap(); + let decoded = decode_on_the_fly_process_seed_v1(&encoded).unwrap(); + assert_eq!(decoded, seed); + assert_eq!( + encode_on_the_fly_process_seed_v1(&decoded).unwrap(), + encoded + ); + + for (field, stale) in [ + ("public_helicity", json!(1)), + ("source_helicity", json!(1)), + ("spin_state", json!(50_000)), + ("momentum_sign", json!(1)), + ] { + let mut tampered = source.clone(); + tampered["external_sources"][0]["states"][0][field] = stale; + assert!( + build(&tampered) + .unwrap_err() + .message() + .contains("crossed source execution values disagree") + ); + } + let mut stale_phase = source.clone(); + stale_phase["external_sources"][0]["states"][0]["crossing_phase"]["real_numerator"] = + json!("1"); + assert!( + build(&stale_phase) + .unwrap_err() + .message() + .contains("crossed source execution values disagree") + ); + let mut stale_chirality = source.clone(); + stale_chirality["external_sources"][0]["states"][0]["chirality"] = json!(1); + assert!( + build(&stale_chirality) + .unwrap_err() + .message() + .contains("process source chirality") + ); + } + + #[test] + fn compact_mass_binding_projects_one_prepared_slot_and_fails_closed() { + let (source, templates, direct) = mass_bound_scalar_inputs(); + let build = |value: &Value| { + build_on_the_fly_process_seed_v1( + parse_on_the_fly_process_seed_projection_v1(&serde_json::to_vec(value).unwrap())?, + &templates, + &direct, + templates.summary().prepared_kernel_pack_digest, + ) + }; + + let seed = build(&source).unwrap(); + assert!(seed.source_anchors().iter().all(|anchor| { + anchor + .states() + .iter() + .all(|state| state.prepared_mass_parameter_slot == Some(0)) + })); + let encoded = encode_on_the_fly_process_seed_v1(&seed).unwrap(); + assert_eq!(decode_on_the_fly_process_seed_v1(&encoded).unwrap(), seed); + + let mut missing = source.clone(); + missing["parameter_projection"] = json!([]); + assert!( + build(&missing) + .unwrap_err() + .message() + .contains("component-0 projections") + ); + + let mut mismatched = source; + mismatched["parameter_projection"][0]["prepared_parameter_id"] = json!(2); + assert!( + build(&mismatched) + .unwrap_err() + .message() + .contains("mass projection disagrees with the prepared template catalog") + ); + } + + #[test] + fn cold_mass_binding_rejects_wrong_unique_and_missing_prepared_slots() { + let (source, templates, direct) = mass_bound_scalar_inputs(); + let mut seed = build_test_seed(&source, &templates, &direct).unwrap(); + let mut input = templates.into_input(); + let second_evaluator = input.evaluator_bindings[1]; + let mut second_parameter = input.parameters[0]; + second_parameter.id = 1; + second_parameter.template_string_id = second_evaluator.resolver_key_string_id; + second_parameter.prepared_parameter_id = 1; + second_parameter.semantic_digest_id = second_evaluator.callable_signature_digest_id; + input.parameters.push(second_parameter); + input.catalog_header[0].parameter_count = 2; + let templates = input.validate().unwrap(); + let projection = [(0, Some(0), 0), (1, Some(1), 0)]; + + validate_on_the_fly_source_mass_bindings_v1(&seed, &templates, &projection) + .expect("the source mass parameter owns prepared slot zero"); + + seed.source_anchors[0].states[0].prepared_mass_parameter_slot = Some(1); + let wrong = validate_on_the_fly_source_mass_bindings_v1(&seed, &templates, &projection) + .expect_err("another uniquely owned prepared slot is not this source's mass"); + assert!( + wrong + .to_string() + .contains("authenticated recurrence template") + ); + + seed.source_anchors[0].states[0].prepared_mass_parameter_slot = None; + let missing = validate_on_the_fly_source_mass_bindings_v1(&seed, &templates, &projection) + .expect_err("an explicit source mass parameter cannot lose its slot"); + assert!( + missing + .to_string() + .contains("authenticated recurrence template") + ); + } + + #[test] + fn legacy_null_mass_binding_uses_authenticated_orientation_without_abs_shortcuts() { + for (particle, anti_particle, orientation, expected) in [ + (6, -6, CurrentOrientation::Particle, Some(0)), + (-6, 6, CurrentOrientation::Antiparticle, Some(0)), + (-6, 6, CurrentOrientation::Particle, None), + (1, -1, CurrentOrientation::Particle, None), + (21, 21, CurrentOrientation::SelfConjugate, None), + ] { + let (source, templates, direct) = + legacy_null_mass_inputs(particle, anti_particle, orientation); + let seed = build_test_seed(&source, &templates, &direct).unwrap(); + assert!(seed.source_anchors().iter().all(|anchor| { + anchor + .states() + .iter() + .all(|state| state.prepared_mass_parameter_slot == expected) + })); + let encoded = encode_on_the_fly_process_seed_v1(&seed).unwrap(); + assert_eq!(decode_on_the_fly_process_seed_v1(&encoded).unwrap(), seed); + } + } + + #[test] + fn legacy_null_mass_binding_fails_closed_on_ambiguity_type_and_orientation_conflicts() { + let (source, templates, direct) = + legacy_null_mass_inputs(6, -6, CurrentOrientation::Particle); + + let mut ambiguous_input = templates.clone().into_input(); + let second_evaluator = ambiguous_input.evaluator_bindings[1]; + let mut second_parameter = ambiguous_input.parameters[0]; + second_parameter.id = 1; + second_parameter.template_string_id = second_evaluator.resolver_key_string_id; + second_parameter.prepared_parameter_id = 1; + second_parameter.semantic_digest_id = second_evaluator.callable_signature_digest_id; + ambiguous_input.parameters.push(second_parameter); + ambiguous_input.catalog_header[0].parameter_count = 2; + let ambiguous_templates = ambiguous_input.validate().unwrap(); + let error = build_test_seed(&source, &ambiguous_templates, &direct).unwrap_err(); + assert!( + error + .message() + .contains("mass parameter \"particle.6.mass\" is ambiguous") + ); + + let mut complex_input = templates.clone().into_input(); + complex_input.parameters[0].value_type = ParameterValueType::Complex as u8; + let complex_templates = complex_input.validate().unwrap(); + assert!( + build_test_seed(&source, &complex_templates, &direct) + .unwrap_err() + .message() + .contains("mass parameter is not real") + ); + + let mut out_of_range_source = source.clone(); + out_of_range_source["parameter_projection"][0]["prepared_parameter_id"] = json!(1); + let mut out_of_range_input = templates.clone().into_input(); + out_of_range_input.parameters[0].prepared_parameter_id = 1; + let out_of_range_templates = out_of_range_input.validate().unwrap(); + assert!( + build_test_seed(&out_of_range_source, &out_of_range_templates, &direct) + .unwrap_err() + .message() + .contains("outside the dense parameter arena") + ); + + let (self_conjugate_source, self_conjugate_templates, self_conjugate_direct) = + legacy_null_mass_inputs(21, 22, CurrentOrientation::SelfConjugate); + assert!( + build_test_seed( + &self_conjugate_source, + &self_conjugate_templates, + &self_conjugate_direct, + ) + .unwrap_err() + .message() + .contains("self-conjugate source mass orientation") + ); + + let mut mismatched_input = templates.into_input(); + mismatched_input.sources[0].mass_parameter_id = 0; + let mismatched_templates = mismatched_input.validate().unwrap(); + assert!( + build_test_seed(&source, &mismatched_templates, &direct) + .unwrap_err() + .message() + .contains("source and current state disagree") + ); + } + + #[test] + fn minimal_projection_keeps_unspecified_model_orders_unbounded_for_cold_resolution() { + let (mut source, templates, direct) = scalar_inputs(); + source["coupling_order_policy"] = json!("minimal"); + source["coupling_limits"] = json!([]); + let projection = + parse_on_the_fly_process_seed_projection_v1(&serde_json::to_vec(&source).unwrap()) + .unwrap(); + let seed = build_on_the_fly_process_seed_v1( + projection, + &templates, + &direct, + templates.summary().prepared_kernel_pack_digest, + ) + .unwrap(); + + assert_eq!( + seed.coupling_order_policy(), + OnTheFlyCouplingOrderPolicyV1::Minimal + ); + assert_eq!(seed.coupling_hierarchies(), [1]); + assert_eq!(seed.explicit_coupling_limits(), [None]); + } + + #[test] + fn ordered_process_seed_batch_is_deterministic_and_builds_one_template_catalog() { + let (first_source, templates, direct) = scalar_inputs(); + let mut second_source = first_source.clone(); + second_source["process_digest"] = json!(digest(93).to_string()); + let pack_digest = templates.summary().prepared_kernel_pack_digest; + let parse = |value: &Value| { + parse_on_the_fly_process_seed_projection_v1(&serde_json::to_vec(value).unwrap()) + .unwrap() + }; + let singleton_bytes = |value: &Value| { + encode_on_the_fly_process_seed_v1( + &build_on_the_fly_process_seed_v1(parse(value), &templates, &direct, pack_digest) + .unwrap(), + ) + .unwrap() + }; + let expected = [ + singleton_bytes(&second_source), + singleton_bytes(&first_source), + ]; + + PROCESS_SEED_TEMPLATE_CATALOG_BUILD_COUNT.with(|count| count.set(0)); + let guard = OnTheFlyForbiddenWorkGuardV1::begin().unwrap(); + let first = build_on_the_fly_process_seeds_v1( + vec![parse(&second_source), parse(&first_source)], + &templates, + &direct, + pack_digest, + ) + .unwrap() + .iter() + .map(encode_on_the_fly_process_seed_v1) + .collect::>>() + .unwrap(); + let forbidden = guard.finish().unwrap(); + PROCESS_SEED_TEMPLATE_CATALOG_BUILD_COUNT.with(|count| assert_eq!(count.get(), 1)); + + PROCESS_SEED_TEMPLATE_CATALOG_BUILD_COUNT.with(|count| count.set(0)); + let second = build_on_the_fly_process_seeds_v1( + vec![parse(&second_source), parse(&first_source)], + &templates, + &direct, + pack_digest, + ) + .unwrap() + .iter() + .map(encode_on_the_fly_process_seed_v1) + .collect::>>() + .unwrap(); + PROCESS_SEED_TEMPLATE_CATALOG_BUILD_COUNT.with(|count| assert_eq!(count.get(), 1)); + + assert_eq!(first, expected); + assert_eq!(second, expected); + assert_eq!(forbidden.direct_plan_load_attempts, 0); + assert_eq!(forbidden.direct_plan_decode_attempts, 0); + assert_eq!(forbidden.direct_plan_materialization_attempts, 0); + assert_eq!(forbidden.established_builder_attempts, 0); + } + + #[test] + fn ordered_process_seed_batch_attributes_failure_to_index_and_identity() { + let (source, templates, direct) = scalar_inputs(); + let mut invalid_source = source.clone(); + invalid_source["process_digest"] = json!(digest(93).to_string()); + invalid_source["external_sources"][0]["states"][0]["source_template_id"] = json!(99); + let parse = |value: &Value| { + parse_on_the_fly_process_seed_projection_v1(&serde_json::to_vec(value).unwrap()) + .unwrap() + }; + + let error = build_on_the_fly_process_seeds_v1( + vec![parse(&source), parse(&invalid_source)], + &templates, + &direct, + templates.summary().prepared_kernel_pack_digest, + ) + .unwrap_err(); + + assert!(error.message().contains("process seed at index 1")); + assert!(error.message().contains(&digest(93).to_string())); + assert!(error.message().contains("source template is absent")); + } + + #[cfg(feature = "python-generation-bridge")] + #[test] + fn process_seed_byte_singleton_matches_one_element_batch_exactly() { + let (source, templates, direct) = scalar_inputs(); + let source_bytes = serde_json::to_vec(&source).unwrap(); + let pack_digest = templates.summary().prepared_kernel_pack_digest; + + let singleton = crate::__private::build_on_the_fly_process_seed_bytes_v1( + &source_bytes, + &templates, + &direct, + pack_digest, + ) + .unwrap(); + let batch = crate::__private::build_on_the_fly_process_seed_bytes_batch_v1( + &[source_bytes], + &templates, + &direct, + pack_digest, + ) + .unwrap(); + assert_eq!(batch, vec![singleton]); + + let malformed = b"{".to_vec(); + let singleton_error = crate::__private::build_on_the_fly_process_seed_bytes_v1( + &malformed, + &templates, + &direct, + pack_digest, + ) + .unwrap_err(); + let batch_error = crate::__private::build_on_the_fly_process_seed_bytes_batch_v1( + &[malformed], + &templates, + &direct, + pack_digest, + ) + .unwrap_err(); + assert_eq!(singleton_error, batch_error); + } + + #[cfg(feature = "python-generation-bridge")] + #[test] + fn empty_process_seed_byte_batch_builds_one_template_catalog() { + let (_, templates, direct) = scalar_inputs(); + PROCESS_SEED_TEMPLATE_CATALOG_BUILD_COUNT.with(|count| count.set(0)); + + let batch = crate::__private::build_on_the_fly_process_seed_bytes_batch_v1( + &[], + &templates, + &direct, + templates.summary().prepared_kernel_pack_digest, + ) + .unwrap(); + + assert!(batch.is_empty()); + PROCESS_SEED_TEMPLATE_CATALOG_BUILD_COUNT.with(|count| assert_eq!(count.get(), 1)); + } + + #[test] + fn compact_projection_is_deterministic_and_tampering_fails_closed() { + let (source, templates, direct) = scalar_inputs(); + let build = |value: &Value| { + let projection = + parse_on_the_fly_process_seed_projection_v1(&serde_json::to_vec(value).unwrap())?; + let seed = build_on_the_fly_process_seed_v1( + projection, + &templates, + &direct, + templates.summary().prepared_kernel_pack_digest, + )?; + encode_on_the_fly_process_seed_v1(&seed) + }; + assert_eq!(build(&source).unwrap(), build(&source).unwrap()); + + let mut stale_source = source.clone(); + stale_source["external_sources"][0]["states"][0]["source_template_id"] = json!(99); + assert!(build(&stale_source).is_err()); + + let mut stale_pack = templates + .summary() + .prepared_kernel_pack_digest + .as_bytes() + .to_owned(); + stale_pack[0] ^= 0xff; + let projection = + parse_on_the_fly_process_seed_projection_v1(&serde_json::to_vec(&source).unwrap()) + .unwrap(); + assert!( + build_on_the_fly_process_seed_v1( + projection, + &templates, + &direct, + SemanticDigest::new(stale_pack).unwrap(), + ) + .is_err() + ); + } +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/recurrence/on_the_fly/source_seed.rs b/pyamplicol/rust/crates/rusticol-core/src/recurrence/on_the_fly/source_seed.rs new file mode 100644 index 0000000000..9e617f9d1b --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/recurrence/on_the_fly/source_seed.rs @@ -0,0 +1,947 @@ +// SPDX-License-Identifier: 0BSD + +use super::*; + +pub(crate) const ON_THE_FLY_PROCESS_SEED_IDENTITY_ABI: &str = + "pyamplicol-on-the-fly-process-seed-identity-v1"; + +/// Compact publicable identity of one decoded process-seed source state. +#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct OnTheFlyProcessSeedStateIdentityV1 { + pub(crate) state_index: u32, + pub(crate) public_helicity: i32, + pub(crate) prepared_mass_parameter_slot: Option, +} + +/// Compact publicable identity of one decoded process-seed source anchor. +#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct OnTheFlyProcessSeedSourceIdentityV1 { + pub(crate) source_slot: u32, + pub(crate) public_label: u32, + pub(crate) is_initial: bool, + pub(crate) states: Vec, +} + +/// Exact compact identity embedded beside the sole native process seed. +/// +/// This is derived only from a successfully decoded seed. It deliberately +/// records source-domain facts rather than reproducing the binary seed. +#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct OnTheFlyProcessSeedIdentityV1 { + pub(crate) abi: String, + pub(crate) process_digest: String, + pub(crate) compiled_model_digest: String, + pub(crate) recurrence_template_catalog_digest: String, + pub(crate) prepared_kernel_pack_digest: String, + pub(crate) recurrence_direct_template_catalog_digest: String, + pub(crate) semantic_digest: String, + pub(crate) external_permutation: Vec, + pub(crate) external_sources: Vec, +} + +/// Model-neutral source family needed by the private direct-source adapter. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[repr(u8)] +pub(crate) enum OnTheFlySourceWavefunctionFamilyV1 { + Scalar = 0, + WeylFermion = 1, + DiracFermion = 2, + Vector = 3, + Spin2 = 4, +} +impl OnTheFlySourceWavefunctionFamilyV1 { + pub(super) const fn is_fermionic(self) -> bool { + matches!(self, Self::WeylFermion | Self::DiracFermion) + } +} + +/// Model-neutral particle orientation retained by one source state. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[repr(u8)] +pub(crate) enum OnTheFlySourceOrientationV1 { + Particle = 0, + Antiparticle = 1, + SelfConjugate = 2, +} + +/// Exact external LC role; this is structural input, not a public-flow row. +#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] +#[repr(u8)] +pub(crate) enum OnTheFlyExternalColorRoleV1 { + Singlet = 0, + Fundamental = 1, + Antifundamental = 2, + Adjoint = 3, +} + +impl OnTheFlyExternalColorRoleV1 { + pub(super) const fn is_colored(self) -> bool { + !matches!(self, Self::Singlet) + } + + pub(super) const fn is_pairing_endpoint(self) -> bool { + matches!(self, Self::Fundamental | Self::Antifundamental) + } +} + +/// One authenticated source execution row exposed to the eventual engine +/// adapter. Source-template IDs may be sparse; callers must retain them. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) struct OnTheFlySourceExecutionSpecV1 { + pub(crate) source_slot: u32, + pub(crate) source_template_id: u32, + pub(crate) spin_state_class: i32, + pub(crate) family: OnTheFlySourceWavefunctionFamilyV1, + pub(crate) orientation: OnTheFlySourceOrientationV1, + pub(crate) helicity: i32, + pub(crate) chirality: i32, + pub(crate) prepared_mass_parameter_slot: Option, +} + +/// One concrete selected-helicity source state retained in the compact seed. +#[derive(Clone, Debug, Eq, PartialEq)] +pub(crate) struct OnTheFlySourceStateV1 { + pub(super) state_index: u32, + pub(super) public_helicity: i32, + pub(super) source_helicity: i32, + pub(super) source_template_id: u32, + pub(super) current_state_template_id: u32, + pub(super) source_semantic_digest: SemanticDigest, + pub(super) current_state_semantic_digest: SemanticDigest, + pub(super) momentum_sign: i32, + pub(super) crossing_phase: ExactComplexRational, + pub(super) spin_state: i32, + pub(super) chirality: i32, + pub(super) flavour_flow: Box<[i32]>, + pub(super) quantum_number_flow_id: u32, + pub(super) color_seed_proof_digest: SemanticDigest, + pub(super) source_family: OnTheFlySourceWavefunctionFamilyV1, + pub(super) source_orientation: OnTheFlySourceOrientationV1, + pub(super) prepared_mass_parameter_slot: Option, +} + +impl OnTheFlySourceStateV1 { + #[allow(clippy::too_many_arguments)] + pub(crate) fn new( + state_index: u32, + public_helicity: i32, + source_helicity: i32, + source_template_id: u32, + current_state_template_id: u32, + source_semantic_digest: SemanticDigest, + current_state_semantic_digest: SemanticDigest, + momentum_sign: i32, + crossing_phase: ExactComplexRational, + spin_state: i32, + chirality: i32, + flavour_flow: Vec, + quantum_number_flow_id: u32, + color_seed_proof_digest: SemanticDigest, + source_family: OnTheFlySourceWavefunctionFamilyV1, + source_orientation: OnTheFlySourceOrientationV1, + prepared_mass_parameter_slot: Option, + ) -> RusticolResult { + if source_template_id == MISSING_U32 || current_state_template_id == MISSING_U32 { + return Err(invalid("source state reserves the missing u32 sentinel")); + } + if momentum_sign != -1 && momentum_sign != 1 { + return Err(invalid("source momentum sign must be -1 or +1")); + } + if crossing_phase.is_zero() || flavour_flow.is_empty() { + return Err(invalid( + "source state requires a nonzero crossing phase and flavour ancestry", + )); + } + if source_family.is_fermionic() + && source_orientation == OnTheFlySourceOrientationV1::SelfConjugate + { + return Err(invalid( + "fermion source state cannot have self-conjugate orientation", + )); + } + Ok(Self { + state_index, + public_helicity, + source_helicity, + source_template_id, + current_state_template_id, + source_semantic_digest, + current_state_semantic_digest, + momentum_sign, + crossing_phase, + spin_state, + chirality, + flavour_flow: flavour_flow.into_boxed_slice(), + quantum_number_flow_id, + color_seed_proof_digest, + source_family, + source_orientation, + prepared_mass_parameter_slot, + }) + } + + pub(crate) const fn state_index(&self) -> u32 { + self.state_index + } + + pub(crate) const fn public_helicity(&self) -> i32 { + self.public_helicity + } + + pub(crate) const fn prepared_mass_parameter_slot(&self) -> Option { + self.prepared_mass_parameter_slot + } +} + +/// All concrete source alternatives for one external source slot. +#[derive(Clone, Debug, Eq, PartialEq)] +pub(crate) struct OnTheFlySourceAnchorV1 { + pub(super) source_slot: u32, + pub(super) external_label: u32, + pub(super) is_initial: bool, + pub(super) color_role: OnTheFlyExternalColorRoleV1, + pub(super) is_fermionic: bool, + pub(super) pairing_source_contract_digest: Option, + pub(super) states: Box<[OnTheFlySourceStateV1]>, +} + +impl OnTheFlySourceAnchorV1 { + pub(crate) fn new( + source_slot: u32, + external_label: u32, + is_initial: bool, + color_role: OnTheFlyExternalColorRoleV1, + is_fermionic: bool, + pairing_source_contract_digest: Option, + mut states: Vec, + ) -> RusticolResult { + if states.is_empty() { + return Err(invalid("source anchor has no concrete state")); + } + states.sort_unstable_by_key(|state| state.state_index); + if states + .windows(2) + .any(|pair| pair[0].state_index == pair[1].state_index) + { + return Err(invalid("source anchor repeats a state index")); + } + if states + .iter() + .any(|state| state.source_family.is_fermionic() != is_fermionic) + { + return Err(invalid( + "source anchor statistics disagree with its execution states", + )); + } + if color_role.is_pairing_endpoint() != pairing_source_contract_digest.is_some() + || (color_role.is_pairing_endpoint() && !is_fermionic) + { + return Err(invalid( + "open-line endpoint role requires one fermionic source contract", + )); + } + Ok(Self { + source_slot, + external_label, + is_initial, + color_role, + is_fermionic, + pairing_source_contract_digest, + states: states.into_boxed_slice(), + }) + } + + pub(super) fn selected( + &self, + state_index: u32, + public_helicity: i32, + ) -> RusticolResult<&OnTheFlySourceStateV1> { + self.states + .iter() + .find(|state| { + state.state_index == state_index && state.public_helicity == public_helicity + }) + .ok_or_else(|| { + invalid(format!( + "source slot {} has no state {state_index} with public helicity {public_helicity}", + self.source_slot + )) + }) + } + + pub(crate) const fn source_slot(&self) -> u32 { + self.source_slot + } + + pub(crate) const fn external_label(&self) -> u32 { + self.external_label + } + + pub(crate) const fn is_initial(&self) -> bool { + self.is_initial + } + + pub(crate) const fn color_role(&self) -> OnTheFlyExternalColorRoleV1 { + self.color_role + } + + pub(crate) fn states(&self) -> &[OnTheFlySourceStateV1] { + &self.states + } +} + +/// One explicit endpoint in a compact species pairing class. +#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] +pub(crate) struct OnTheFlyPairingEndpointV1 { + pub(crate) source_slot: u32, + pub(crate) source_contract_digest: SemanticDigest, +} + +/// O(external-leg) pairing-class input. Rules and permutations are never +/// enumerated: one selected permutation is validated from Lehmer digits. +#[derive(Clone, Debug, Eq, PartialEq)] +pub(crate) struct OnTheFlyPairingClassV1 { + pub(super) species: Box, + pub(super) species_semantic_digest: SemanticDigest, + pub(super) fundamental_endpoints: Box<[OnTheFlyPairingEndpointV1]>, + pub(super) antifundamental_endpoints: Box<[OnTheFlyPairingEndpointV1]>, + pub(super) semantic_digest: SemanticDigest, +} + +impl OnTheFlyPairingClassV1 { + pub(crate) fn new( + species: impl Into>, + species_semantic_digest: SemanticDigest, + mut fundamental_endpoints: Vec, + mut antifundamental_endpoints: Vec, + ) -> RusticolResult { + let species = species.into(); + if species.is_empty() { + return Err(invalid("pairing class species identity is empty")); + } + fundamental_endpoints.sort_unstable_by_key(|endpoint| endpoint.source_slot); + antifundamental_endpoints.sort_unstable_by_key(|endpoint| endpoint.source_slot); + if fundamental_endpoints.is_empty() + || fundamental_endpoints.len() != antifundamental_endpoints.len() + || fundamental_endpoints + .windows(2) + .any(|pair| pair[0].source_slot == pair[1].source_slot) + || antifundamental_endpoints + .windows(2) + .any(|pair| pair[0].source_slot == pair[1].source_slot) + { + return Err(invalid( + "pairing class endpoints must be nonempty, balanced, and unique", + )); + } + let mut result = Self { + species, + species_semantic_digest, + fundamental_endpoints: fundamental_endpoints.into_boxed_slice(), + antifundamental_endpoints: antifundamental_endpoints.into_boxed_slice(), + semantic_digest: SemanticDigest::new([1; 32])?, + }; + result.semantic_digest = result.compute_digest()?; + Ok(result) + } + + pub(crate) fn fundamental_endpoints(&self) -> &[OnTheFlyPairingEndpointV1] { + &self.fundamental_endpoints + } + + pub(crate) fn antifundamental_endpoints(&self) -> &[OnTheFlyPairingEndpointV1] { + &self.antifundamental_endpoints + } + + fn compute_digest(&self) -> RusticolResult { + let mut hash = Sha256::new(); + hash.update(b"pyamplicol-on-the-fly-pairing-class-v1\0"); + hash_len(&mut hash, self.species.len(), "pairing species")?; + hash.update(self.species.as_bytes()); + hash_digest(&mut hash, self.species_semantic_digest); + for endpoints in [&self.fundamental_endpoints, &self.antifundamental_endpoints] { + hash_len(&mut hash, endpoints.len(), "pairing endpoints")?; + for endpoint in endpoints.iter() { + hash.update(endpoint.source_slot.to_le_bytes()); + hash_digest(&mut hash, endpoint.source_contract_digest); + } + } + final_digest(hash) + } +} + +/// Compact immutable process input for one on-the-fly lane. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[repr(u8)] +pub(crate) enum OnTheFlyCouplingOrderPolicyV1 { + Minimal = 0, + Explicit = 1, +} + +/// Compact immutable process input for one on-the-fly lane. +#[derive(Clone, Debug, Eq, PartialEq)] +pub(crate) struct OnTheFlyProcessSeedV1 { + pub(super) process_digest: SemanticDigest, + pub(super) model_digest: SemanticDigest, + pub(super) template_catalog_digest: SemanticDigest, + pub(super) prepared_pack_digest: SemanticDigest, + pub(super) direct_catalog_digest: SemanticDigest, + pub(super) normalization_semantic_digest: SemanticDigest, + pub(super) normalization_convention: Box, + pub(super) source_anchors: Box<[OnTheFlySourceAnchorV1]>, + /// Gather map: construction source slot i receives public external slot + /// external_permutation[i]. + pub(super) external_permutation: Box<[u32]>, + pub(super) coupling_order_policy: OnTheFlyCouplingOrderPolicyV1, + pub(super) coupling_hierarchies: Box<[u32]>, + /// User-supplied maxima only. `None` means that this model order has no + /// explicit hard cap; it never means that the default minimal policy has + /// already been resolved. + pub(super) coupling_limits: Box<[Option]>, + pub(super) pairing_classes: Box<[OnTheFlyPairingClassV1]>, + pub(super) semantic_digest: SemanticDigest, +} + +impl OnTheFlyProcessSeedV1 { + #[allow(clippy::too_many_arguments)] + pub(crate) fn new( + process_digest: SemanticDigest, + model_digest: SemanticDigest, + template_catalog_digest: SemanticDigest, + prepared_pack_digest: SemanticDigest, + direct_catalog_digest: SemanticDigest, + normalization_semantic_digest: SemanticDigest, + normalization_convention: impl Into>, + normalization_factor: ExactComplexRational, + mut source_anchors: Vec, + external_permutation: Vec, + coupling_order_policy: OnTheFlyCouplingOrderPolicyV1, + coupling_hierarchies: Vec, + coupling_limits: Vec>, + mut pairing_classes: Vec, + ) -> RusticolResult { + if normalization_factor != ExactComplexRational::ONE { + return Err(integrity( + "raw-amplitude seed normalization factor must be exact one", + )); + } + let normalization_convention = normalization_convention.into(); + if normalization_convention.is_empty() { + return Err(invalid("normalization convention is empty")); + } + source_anchors.sort_unstable_by_key(|anchor| anchor.source_slot); + if source_anchors.len() < 2 + || source_anchors + .iter() + .enumerate() + .any(|(index, anchor)| anchor.source_slot as usize != index) + { + return Err(invalid( + "source anchors must form a dense domain with at least two slots", + )); + } + if coupling_limits.is_empty() + || coupling_hierarchies.len() != coupling_limits.len() + || coupling_hierarchies.contains(&0) + { + return Err(invalid( + "on-the-fly coupling policy requires one positive hierarchy and optional hard cap per model order", + )); + } + validate_permutation( + &external_permutation, + source_anchors.len(), + "external source permutation", + )?; + pairing_classes.sort_unstable_by(|left, right| { + left.species + .cmp(&right.species) + .then_with(|| left.semantic_digest.cmp(&right.semantic_digest)) + }); + if pairing_classes + .windows(2) + .any(|pair| pair[0].species == pair[1].species) + { + return Err(invalid("pairing class repeats a species identity")); + } + let mut covered_endpoints = BTreeSet::new(); + for pairing_class in &pairing_classes { + for (endpoints, role) in [ + ( + pairing_class.fundamental_endpoints.as_ref(), + OnTheFlyExternalColorRoleV1::Fundamental, + ), + ( + pairing_class.antifundamental_endpoints.as_ref(), + OnTheFlyExternalColorRoleV1::Antifundamental, + ), + ] { + for endpoint in endpoints { + let anchor = source_anchors + .get(endpoint.source_slot as usize) + .ok_or_else(|| invalid("pairing endpoint is outside the source domain"))?; + if anchor.color_role != role + || anchor.pairing_source_contract_digest + != Some(endpoint.source_contract_digest) + || !covered_endpoints.insert(endpoint.source_slot) + { + return Err(integrity( + "pairing endpoint role or source-contract identity is inconsistent", + )); + } + } + } + } + let required_endpoints = source_anchors + .iter() + .filter(|anchor| anchor.color_role.is_pairing_endpoint()) + .map(|anchor| anchor.source_slot) + .collect::>(); + if covered_endpoints != required_endpoints { + return Err(integrity( + "pairing classes do not cover every open-line endpoint exactly once", + )); + } + + let mut result = Self { + process_digest, + model_digest, + template_catalog_digest, + prepared_pack_digest, + direct_catalog_digest, + normalization_semantic_digest, + normalization_convention, + source_anchors: source_anchors.into_boxed_slice(), + external_permutation: external_permutation.into_boxed_slice(), + coupling_order_policy, + coupling_hierarchies: coupling_hierarchies.into_boxed_slice(), + coupling_limits: coupling_limits.into_boxed_slice(), + pairing_classes: pairing_classes.into_boxed_slice(), + semantic_digest: SemanticDigest::new([1; 32])?, + }; + result.semantic_digest = result.compute_digest()?; + Ok(result) + } + + #[cfg(test)] + pub(crate) fn with_selector_local_zero(self) -> RusticolResult { + let Self { + process_digest, + model_digest, + template_catalog_digest, + prepared_pack_digest, + direct_catalog_digest, + normalization_semantic_digest, + normalization_convention, + source_anchors, + external_permutation, + coupling_order_policy, + coupling_hierarchies, + coupling_limits, + pairing_classes, + semantic_digest: _, + } = self; + let source_anchors = source_anchors + .into_vec() + .into_iter() + .map(|anchor| { + let OnTheFlySourceAnchorV1 { + source_slot, + external_label, + is_initial, + color_role, + is_fermionic, + pairing_source_contract_digest, + states, + } = anchor; + let mut states = states.into_vec(); + let mut zero = states[0].clone(); + zero.state_index = 1; + zero.public_helicity = 1; + zero.spin_state = 50_001; + states.push(zero); + OnTheFlySourceAnchorV1::new( + source_slot, + external_label, + is_initial, + color_role, + is_fermionic, + pairing_source_contract_digest, + states, + ) + }) + .collect::>>()?; + Self::new( + process_digest, + model_digest, + template_catalog_digest, + prepared_pack_digest, + direct_catalog_digest, + normalization_semantic_digest, + normalization_convention, + ExactComplexRational::ONE, + source_anchors, + external_permutation.into_vec(), + coupling_order_policy, + coupling_hierarchies.into_vec(), + coupling_limits.into_vec(), + pairing_classes.into_vec(), + ) + } + + fn compute_digest(&self) -> RusticolResult { + let mut hash = Sha256::new(); + hash.update(b"pyamplicol-on-the-fly-process-seed-v2\0"); + for digest in [ + self.process_digest, + self.model_digest, + self.template_catalog_digest, + self.prepared_pack_digest, + self.direct_catalog_digest, + self.normalization_semantic_digest, + ] { + hash_digest(&mut hash, digest); + } + hash_len( + &mut hash, + self.normalization_convention.len(), + "normalization convention", + )?; + hash.update(self.normalization_convention.as_bytes()); + hash_len(&mut hash, self.source_anchors.len(), "source anchors")?; + for anchor in &self.source_anchors { + hash.update(anchor.source_slot.to_le_bytes()); + hash.update(anchor.external_label.to_le_bytes()); + hash.update([ + anchor.color_role as u8, + u8::from(anchor.is_fermionic), + u8::from(anchor.is_initial), + ]); + match anchor.pairing_source_contract_digest { + None => hash.update([0]), + Some(value) => { + hash.update([1]); + hash_digest(&mut hash, value); + } + } + hash_len(&mut hash, anchor.states.len(), "source states")?; + for state in &anchor.states { + hash.update(state.state_index.to_le_bytes()); + hash.update(state.public_helicity.to_le_bytes()); + hash.update(state.source_helicity.to_le_bytes()); + hash.update(state.source_template_id.to_le_bytes()); + hash.update(state.current_state_template_id.to_le_bytes()); + hash_digest(&mut hash, state.source_semantic_digest); + hash_digest(&mut hash, state.current_state_semantic_digest); + hash.update(state.momentum_sign.to_le_bytes()); + hash_exact(&mut hash, state.crossing_phase); + hash.update(state.spin_state.to_le_bytes()); + hash.update(state.chirality.to_le_bytes()); + hash_len(&mut hash, state.flavour_flow.len(), "source flavour flow")?; + for flavour in &state.flavour_flow { + hash.update(flavour.to_le_bytes()); + } + hash.update(state.quantum_number_flow_id.to_le_bytes()); + hash_digest(&mut hash, state.color_seed_proof_digest); + hash.update([state.source_family as u8, state.source_orientation as u8]); + match state.prepared_mass_parameter_slot { + None => hash.update([0]), + Some(slot) => { + hash.update([1]); + hash.update(slot.to_le_bytes()); + } + } + } + } + hash_len( + &mut hash, + self.external_permutation.len(), + "external permutation", + )?; + for slot in &self.external_permutation { + hash.update(slot.to_le_bytes()); + } + hash.update([self.coupling_order_policy as u8]); + hash_len( + &mut hash, + self.coupling_hierarchies.len(), + "coupling hierarchies", + )?; + for (hierarchy, limit) in self + .coupling_hierarchies + .iter() + .zip(self.coupling_limits.iter()) + { + hash.update(hierarchy.to_le_bytes()); + match limit { + None => hash.update([0]), + Some(limit) => { + hash.update([1]); + hash.update(limit.to_le_bytes()); + } + } + } + hash_len(&mut hash, self.pairing_classes.len(), "pairing classes")?; + for pairing_class in &self.pairing_classes { + hash_digest(&mut hash, pairing_class.semantic_digest); + } + final_digest(hash) + } + + pub(crate) const fn semantic_digest(&self) -> SemanticDigest { + self.semantic_digest + } + + pub(crate) const fn process_digest(&self) -> SemanticDigest { + self.process_digest + } + + pub(crate) fn identity(&self) -> OnTheFlyProcessSeedIdentityV1 { + OnTheFlyProcessSeedIdentityV1 { + abi: ON_THE_FLY_PROCESS_SEED_IDENTITY_ABI.to_owned(), + process_digest: self.process_digest().to_string(), + compiled_model_digest: self.model_digest().to_string(), + recurrence_template_catalog_digest: self.template_catalog_digest().to_string(), + prepared_kernel_pack_digest: self.prepared_pack_digest().to_string(), + recurrence_direct_template_catalog_digest: self.direct_catalog_digest().to_string(), + semantic_digest: self.semantic_digest().to_string(), + external_permutation: self.external_permutation().to_vec(), + external_sources: self + .source_anchors() + .iter() + .map(|anchor| OnTheFlyProcessSeedSourceIdentityV1 { + source_slot: anchor.source_slot(), + public_label: anchor.external_label(), + is_initial: anchor.is_initial(), + states: anchor + .states() + .iter() + .map(|state| OnTheFlyProcessSeedStateIdentityV1 { + state_index: state.state_index(), + public_helicity: state.public_helicity(), + prepared_mass_parameter_slot: state.prepared_mass_parameter_slot(), + }) + .collect(), + }) + .collect(), + } + } + + pub(crate) fn external_permutation(&self) -> &[u32] { + &self.external_permutation + } + + pub(crate) fn source_anchors(&self) -> &[OnTheFlySourceAnchorV1] { + &self.source_anchors + } + + pub(crate) fn pairing_classes(&self) -> &[OnTheFlyPairingClassV1] { + &self.pairing_classes + } + + pub(crate) const fn coupling_order_policy(&self) -> OnTheFlyCouplingOrderPolicyV1 { + self.coupling_order_policy + } + + pub(crate) fn coupling_hierarchies(&self) -> &[u32] { + &self.coupling_hierarchies + } + + pub(crate) fn explicit_coupling_limits(&self) -> &[Option] { + &self.coupling_limits + } + + pub(crate) const fn model_digest(&self) -> SemanticDigest { + self.model_digest + } + + pub(crate) const fn template_catalog_digest(&self) -> SemanticDigest { + self.template_catalog_digest + } + + pub(crate) const fn prepared_pack_digest(&self) -> SemanticDigest { + self.prepared_pack_digest + } + + pub(crate) const fn direct_catalog_digest(&self) -> SemanticDigest { + self.direct_catalog_digest + } + + pub(crate) fn source_execution_specs( + &self, + ) -> impl Iterator + '_ { + self.source_anchors.iter().flat_map(|anchor| { + anchor + .states + .iter() + .map(move |state| OnTheFlySourceExecutionSpecV1 { + source_slot: anchor.source_slot, + source_template_id: state.source_template_id, + spin_state_class: state.spin_state, + family: state.source_family, + orientation: state.source_orientation, + helicity: state.source_helicity, + chirality: state.chirality, + prepared_mass_parameter_slot: state.prepared_mass_parameter_slot, + }) + }) + } +} + +#[cfg(test)] +pub(crate) fn scalar_adapter_test_seed( + model_digest: SemanticDigest, + template_catalog_digest: SemanticDigest, + prepared_pack_digest: SemanticDigest, + direct_catalog_digest: SemanticDigest, +) -> RusticolResult { + let source_semantic_digest = SemanticDigest::new([5; 32])?; + let current_semantic_digest = SemanticDigest::new([4; 32])?; + let color_seed_digest = SemanticDigest::new([17; 32])?; + let anchors = (0..2) + .map(|source_slot| { + let state = OnTheFlySourceStateV1::new( + 0, + 0, + 0, + 0, + 0, + source_semantic_digest, + current_semantic_digest, + 1, + ExactComplexRational::ONE, + 50_000, + 0, + vec![1], + 0, + color_seed_digest, + OnTheFlySourceWavefunctionFamilyV1::Scalar, + OnTheFlySourceOrientationV1::SelfConjugate, + None, + )?; + OnTheFlySourceAnchorV1::new( + source_slot, + source_slot, + false, + OnTheFlyExternalColorRoleV1::Singlet, + false, + None, + vec![state], + ) + }) + .collect::>>()?; + OnTheFlyProcessSeedV1::new( + SemanticDigest::new([91; 32])?, + model_digest, + template_catalog_digest, + prepared_pack_digest, + direct_catalog_digest, + SemanticDigest::new([92; 32])?, + "raw-amplitude-test", + ExactComplexRational::ONE, + anchors, + vec![0, 1], + OnTheFlyCouplingOrderPolicyV1::Explicit, + vec![1], + vec![Some(0)], + Vec::new(), + ) +} + +pub(super) fn validate_permutation( + values: &[u32], + expected_len: usize, + label: &str, +) -> RusticolResult<()> { + if values.len() != expected_len { + return Err(invalid(format!( + "{label} has length {}, expected {expected_len}", + values.len(), + ))); + } + let mut sorted = values.to_vec(); + sorted.sort_unstable(); + if sorted + != (0..expected_len) + .map(|value| checked_u32(value, label)) + .collect::>>()? + { + return Err(invalid(format!("{label} is not a bijection"))); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn rebuild_with_coupling_policy( + seed: OnTheFlyProcessSeedV1, + coupling_hierarchies: Vec, + coupling_limits: Vec>, + ) -> RusticolResult { + let OnTheFlyProcessSeedV1 { + process_digest, + model_digest, + template_catalog_digest, + prepared_pack_digest, + direct_catalog_digest, + normalization_semantic_digest, + normalization_convention, + source_anchors, + external_permutation, + coupling_order_policy, + coupling_hierarchies: _, + coupling_limits: _, + pairing_classes, + semantic_digest: _, + } = seed; + OnTheFlyProcessSeedV1::new( + process_digest, + model_digest, + template_catalog_digest, + prepared_pack_digest, + direct_catalog_digest, + normalization_semantic_digest, + normalization_convention, + ExactComplexRational::ONE, + source_anchors.into_vec(), + external_permutation.into_vec(), + coupling_order_policy, + coupling_hierarchies, + coupling_limits, + pairing_classes.into_vec(), + ) + } + + #[test] + fn genuine_probe_explicit_model_order_limits_fail_closed() { + let seed = scalar_adapter_test_seed( + SemanticDigest::new([1; 32]).unwrap(), + SemanticDigest::new([2; 32]).unwrap(), + SemanticDigest::new([3; 32]).unwrap(), + SemanticDigest::new([4; 32]).unwrap(), + ) + .unwrap(); + for (hierarchies, limits) in [ + (vec![], vec![]), + (vec![1], vec![Some(0), None]), + (vec![1, 0], vec![Some(0), None]), + ] { + assert!( + rebuild_with_coupling_policy(seed.clone(), hierarchies, limits) + .unwrap_err() + .to_string() + .contains("one positive hierarchy and optional hard cap") + ); + } + assert!(rebuild_with_coupling_policy(seed.clone(), vec![1], vec![Some(3)]).is_ok()); + assert!( + rebuild_with_coupling_policy(seed.clone(), vec![1, 2], vec![Some(0), None]).is_ok() + ); + assert!(rebuild_with_coupling_policy(seed, vec![1, 2], vec![Some(0), Some(3)]).is_ok()); + } +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/recurrence/on_the_fly/sweep.rs b/pyamplicol/rust/crates/rusticol-core/src/recurrence/on_the_fly/sweep.rs new file mode 100644 index 0000000000..4db258cd63 --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/recurrence/on_the_fly/sweep.rs @@ -0,0 +1,3815 @@ +// SPDX-License-Identifier: 0BSD + +use super::source_seed::validate_permutation; +use super::*; +use crate::recurrence::construct::{ + CurrentReflection, PendingReflectionCertificate, TransitionReflectionIndex, + current_key_with_dynamic_color, current_reflection_candidate, current_reversal_masks, + dynamic_color_identity_digest, pure_adjoint_word_is_canonical, reciprocal_reflection_proof, + source_reflection, +}; +use crate::recurrence::fermion_ordering::{FermionOrderingContext, fermion_ordering_factor}; + +fn on_the_fly_source_requires_exterior_sign( + is_fermionic: bool, + color_role: OnTheFlyExternalColorRoleV1, +) -> RusticolResult { + if !is_fermionic { + if color_role.is_pairing_endpoint() { + return Err(integrity( + "fermion-ordering endpoint role belongs to a bosonic source", + )); + } + return Ok(false); + } + match color_role { + OnTheFlyExternalColorRoleV1::Singlet => Ok(true), + OnTheFlyExternalColorRoleV1::Fundamental | OnTheFlyExternalColorRoleV1::Antifundamental => { + Ok(false) + } + OnTheFlyExternalColorRoleV1::Adjoint => Err(invalid( + "external-fermion ordering does not support adjoint fermion sources", + )), + } +} + +pub(super) fn on_the_fly_fermion_ordering_context( + seed: &OnTheFlyProcessSeedV1, +) -> RusticolResult { + let mut source_requires_exterior_sign = Vec::with_capacity(seed.source_anchors.len()); + for (source_slot, anchor) in seed.source_anchors.iter().enumerate() { + let source_slot = checked_u32(source_slot, "fermion-ordering source slot")?; + if anchor.source_slot != source_slot { + return Err(integrity( + "fermion-ordering source anchor disagrees with its slot", + )); + } + source_requires_exterior_sign.push(on_the_fly_source_requires_exterior_sign( + anchor.is_fermionic, + anchor.color_role, + )?); + } + Ok(FermionOrderingContext::new(source_requires_exterior_sign)) +} + +#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] +pub(super) struct PendingContributionKey { + pub(super) parent_current_ids: [u32; 2], + pub(super) key: ContributionKey, +} + +#[derive(Debug)] +pub(super) struct PendingCurrent { + pub(super) key: CurrentCoreKey, + pub(super) source_factor: Option, + pub(super) contributions: BTreeMap, + /// Canonical proof-only lineage alternatives. As in the established + /// builder's realized-rule set, exact contribution factors remain owned + /// by `contributions`; lineage does not multiply or normalize amplitudes. + pub(super) pairing_lineages: Vec, + pub(super) stage: u32, + /// Exact reflection lineage is populated only for the certified cyclic + /// pure-adjoint companion lane. Generic/direct construction leaves it + /// unavailable and therefore cannot enter the fold. + pub(super) reflection: CurrentReflection, + /// Cold reciprocal-orbit proof retained only on the canonical member + /// until reflected public closures have been resolved. + pub(super) reflection_certificate: Option, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +struct PreparedContactOrbitLocation { + transition_id: u32, + state_pair: (u32, u32), + row_index: usize, +} + +#[derive(Debug, Default, Eq, PartialEq)] +pub(super) struct PreparedContactOrbitIndex { + locations: Vec, +} + +fn reserve_on_the_fly_contact_locations( + locations: &mut Vec, + count: usize, +) -> RusticolResult<()> { + locations.try_reserve_exact(count).map_err(|error| { + invalid(format!( + "contact-orbit transition-index allocation failed: {error}" + )) + }) +} + +impl PreparedContactOrbitIndex { + pub(super) fn new( + transitions: &BTreeMap<(u32, u32), Vec>, + ) -> RusticolResult { + let transition_count = transitions.values().try_fold(0usize, |count, rows| { + count + .checked_add(rows.len()) + .ok_or_else(|| invalid("contact-orbit transition count exceeds usize")) + })?; + let contact_count = transitions + .values() + .flatten() + .filter(|prepared| prepared.contact_orbit.is_some()) + .count(); + if contact_count == 0 { + return Ok(Self::default()); + } + let mut transition_ids = Vec::new(); + reserve_on_the_fly_contact_locations(&mut transition_ids, transition_count)?; + let mut locations = Vec::new(); + reserve_on_the_fly_contact_locations(&mut locations, contact_count)?; + for (state_pair, rows) in transitions { + for (row_index, prepared) in rows.iter().enumerate() { + transition_ids.push(prepared.row.id); + if prepared.contact_orbit.is_some() { + locations.push(PreparedContactOrbitLocation { + transition_id: prepared.row.id, + state_pair: *state_pair, + row_index, + }); + } + } + } + transition_ids.sort_unstable(); + if transition_ids.windows(2).any(|rows| rows[0] == rows[1]) { + return Err(integrity( + "contact-orbit transition index contains duplicate IDs", + )); + } + locations.sort_unstable_by_key(|location| location.transition_id); + Ok(Self { locations }) + } + + fn is_empty(&self) -> bool { + self.locations.is_empty() + } + + fn get<'a>( + &self, + transitions: &'a BTreeMap<(u32, u32), Vec>, + transition_id: u32, + ) -> Option<&'a PreparedContactOrbitTransition> { + self.locations + .binary_search_by_key(&transition_id, |location| location.transition_id) + .ok() + .and_then(|index| self.locations.get(index)) + .and_then(|location| { + transitions + .get(&location.state_pair) + .and_then(|rows| rows.get(location.row_index)) + }) + .filter(|prepared| prepared.row.id == transition_id) + .and_then(|prepared| prepared.contact_orbit.as_ref()) + } + + #[cfg(test)] + fn len(&self) -> usize { + self.locations.len() + } +} + +#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] +struct OnTheFlyContactContributionToken { + destination_id: usize, + contribution_ordinal: usize, +} + +#[derive(Debug)] +struct OnTheFlyContactOwnerPlan { + stage_current_start: usize, + selected_tokens: Vec, + staged_contribution_count: usize, + retained_contribution_count: usize, +} + +impl OnTheFlyContactOwnerPlan { + /// Apply a completely selected plan without allocating or failing. + fn commit(self, currents: &mut [PendingCurrent]) { + let mut selected_index = 0usize; + let mut observed_count = 0usize; + let mut retained_count = 0usize; + for (destination_id, current) in currents + .iter_mut() + .enumerate() + .skip(self.stage_current_start) + { + let mut contribution_ordinal = 0usize; + current.contributions.retain(|_, _| { + let token = OnTheFlyContactContributionToken { + destination_id, + contribution_ordinal, + }; + contribution_ordinal += 1; + observed_count += 1; + let selected = self.selected_tokens.get(selected_index) == Some(&token); + if selected { + selected_index += 1; + retained_count += 1; + } + selected + }); + } + debug_assert_eq!(observed_count, self.staged_contribution_count); + debug_assert_eq!(retained_count, self.retained_contribution_count); + debug_assert_eq!(selected_index, self.selected_tokens.len()); + } +} + +fn reserve_on_the_fly_contact_candidates( + candidates: &mut Vec, + count: usize, +) -> RusticolResult<()> { + candidates.try_reserve_exact(count).map_err(|error| { + invalid(format!( + "contact-orbit candidate allocation failed: {error}" + )) + }) +} + +fn plan_on_the_fly_contact_orbit_owners( + stage_current_start: usize, + transitions: &BTreeMap<(u32, u32), Vec>, + contact_orbits: &PreparedContactOrbitIndex, + currents: &[PendingCurrent], +) -> RusticolResult> { + plan_on_the_fly_contact_orbit_owners_with_resolver( + stage_current_start, + currents, + |transition_id| contact_orbits.get(transitions, transition_id), + ) +} + +fn plan_on_the_fly_contact_orbit_owners_with_resolver<'a, F>( + stage_current_start: usize, + currents: &'a [PendingCurrent], + mut contact_orbit: F, +) -> RusticolResult> +where + F: FnMut(u32) -> Option<&'a PreparedContactOrbitTransition>, +{ + if stage_current_start > currents.len() { + return Err(invalid( + "contact-orbit stage current boundary exceeds current storage", + )); + } + let has_certified_contact = currents[stage_current_start..].iter().any(|current| { + current + .contributions + .keys() + .any(|pending| contact_orbit(pending.key.transition_template_id()).is_some()) + }); + if !has_certified_contact { + return Ok(None); + } + let staged_contribution_count = + currents[stage_current_start..] + .iter() + .try_fold(0usize, |count, current| { + count + .checked_add(current.contributions.len()) + .ok_or_else(|| invalid("contact-orbit staged contribution count exceeds usize")) + })?; + let mut candidates = Vec::new(); + reserve_on_the_fly_contact_candidates(&mut candidates, staged_contribution_count)?; + for (destination_id, current) in currents.iter().enumerate().skip(stage_current_start) { + for (contribution_ordinal, pending) in current.contributions.keys().enumerate() { + let token = OnTheFlyContactContributionToken { + destination_id, + contribution_ordinal, + }; + let candidate = contact_orbit(pending.key.transition_template_id()) + .map(|contact_orbit| { + let left = currents + .get(pending.parent_current_ids[0] as usize) + .map(|parent| &parent.key) + .ok_or_else(|| integrity("contact-orbit left parent is absent"))?; + let right = currents + .get(pending.parent_current_ids[1] as usize) + .map(|parent| &parent.key) + .ok_or_else(|| integrity("contact-orbit right parent is absent"))?; + contact_orbit.owner_candidate( + ¤t.key, + [left, right], + pending.key.color_witness_term_id(), + ) + }) + .transpose()?; + candidates.push((token, candidate)); + } + } + if candidates.len() != staged_contribution_count { + return Err(integrity( + "contact-orbit staged contribution snapshot changed length", + )); + } + let selected_tokens = selected_contact_orbit_owner_tokens(candidates)?; + let retained_contribution_count = selected_tokens.len(); + if retained_contribution_count > staged_contribution_count { + return Err(integrity( + "contact-orbit retained contribution count exceeds snapshot", + )); + } + Ok(Some(OnTheFlyContactOwnerPlan { + stage_current_start, + selected_tokens, + staged_contribution_count, + retained_contribution_count, + })) +} + +#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] +pub(super) struct PendingPairingLineage { + pub(super) completed_pairs: Vec<[u32; 2]>, + pub(super) unmatched_endpoint: Option, +} + +impl PendingPairingLineage { + pub(super) fn source(seed: &OnTheFlyProcessSeedV1, source_slot: u32) -> Self { + let unmatched_endpoint = seed.source_anchors[source_slot as usize] + .color_role + .is_pairing_endpoint() + .then_some(source_slot); + Self { + completed_pairs: Vec::new(), + unmatched_endpoint, + } + } +} + +#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] +pub(super) struct PendingClosureKey { + pub(super) closure_template_id: u32, + pub(super) quantum_flow_template_id: Option, + pub(super) parent_current_ids: [u32; 2], + pub(super) color_witness_term_id: LCColorWitnessTermId, +} + +#[derive(Debug)] +pub(super) struct PendingClosure { + pub(super) key: PendingClosureKey, + pub(super) factor: ExactComplexRational, + pub(super) component_coefficients: Box<[ExactComplexRational]>, + pub(super) pairing_lineages: Vec, +} + +/// Query-local Wick-lineage proof selected only after physical closure and +/// complete-rectangle color-alias projection have identified a canonical +/// root-bearing representative. +#[derive(Debug, Eq, PartialEq)] +pub(super) struct ResolvedPairingOwnerV1 { + pub(super) endpoint_pairs: Vec<[u32; 2]>, + pub(super) proof_digest: Option, + pub(super) source_slot_permutation: Vec, + pub(super) source_lineage: Vec, + pub(super) fermion_parity: i32, +} + +fn checked_pairing_capacity(left: usize, right: usize, extra: usize) -> RusticolResult { + left.checked_add(right) + .and_then(|combined| combined.checked_add(extra)) + .ok_or_else(|| invalid("pairing-lineage length exceeds usize")) +} + +fn try_copy_pairing_pairs(source: &[[u32; 2]]) -> RusticolResult> { + let mut copied = Vec::new(); + copied + .try_reserve_exact(source.len()) + .map_err(|error| invalid(format!("pairing-lineage allocation failed: {error}")))?; + copied.extend_from_slice(source); + Ok(copied) +} + +fn try_clone_pairing_lineage( + source: &PendingPairingLineage, +) -> RusticolResult { + Ok(PendingPairingLineage { + completed_pairs: try_copy_pairing_pairs(&source.completed_pairs)?, + unmatched_endpoint: source.unmatched_endpoint, + }) +} + +pub(super) fn try_clone_pairing_lineages( + source: &[PendingPairingLineage], +) -> RusticolResult> { + let mut copied = Vec::new(); + copied + .try_reserve_exact(source.len()) + .map_err(|error| invalid(format!("pairing-lineage set allocation failed: {error}")))?; + for lineage in source { + copied.push(try_clone_pairing_lineage(lineage)?); + } + Ok(copied) +} + +fn pairing_endpoint_class( + seed: &OnTheFlyProcessSeedV1, + source_slot: u32, +) -> RusticolResult<(OnTheFlyExternalColorRoleV1, SemanticDigest)> { + let anchor = seed + .source_anchors + .get(source_slot as usize) + .ok_or_else(|| integrity("pairing-lineage endpoint is outside the source domain"))?; + let contract = anchor + .pairing_source_contract_digest + .ok_or_else(|| integrity("pairing-lineage endpoint lacks its source contract"))?; + let class = seed + .pairing_classes + .iter() + .find(|class| { + class + .fundamental_endpoints + .iter() + .chain(class.antifundamental_endpoints.iter()) + .any(|endpoint| endpoint.source_slot == source_slot) + }) + .ok_or_else(|| integrity("pairing-lineage endpoint lacks its compact class"))?; + let class_endpoint = class + .fundamental_endpoints + .iter() + .chain(class.antifundamental_endpoints.iter()) + .find(|endpoint| endpoint.source_slot == source_slot) + .ok_or_else(|| integrity("pairing-lineage endpoint disappeared from its class"))?; + if class_endpoint.source_contract_digest != contract { + return Err(integrity( + "pairing-lineage endpoint source contract differs from its class", + )); + } + Ok((anchor.color_role, class.semantic_digest)) +} + +fn close_pairing_endpoints( + seed: &OnTheFlyProcessSeedV1, + left: u32, + right: u32, +) -> RusticolResult> { + let (left_role, left_class) = pairing_endpoint_class(seed, left)?; + let (right_role, right_class) = pairing_endpoint_class(seed, right)?; + if left_class != right_class { + return Ok(None); + } + Ok(match (left_role, right_role) { + ( + OnTheFlyExternalColorRoleV1::Fundamental, + OnTheFlyExternalColorRoleV1::Antifundamental, + ) => Some([left, right]), + ( + OnTheFlyExternalColorRoleV1::Antifundamental, + OnTheFlyExternalColorRoleV1::Fundamental, + ) => Some([right, left]), + _ => None, + }) +} + +fn combine_pairing_lineage( + seed: &OnTheFlyProcessSeedV1, + left: &PendingPairingLineage, + right: &PendingPairingLineage, + carries_colored_fermion_line: bool, +) -> RusticolResult> { + let possible_new_pair = usize::from( + left.unmatched_endpoint.is_some() + && right.unmatched_endpoint.is_some() + && !carries_colored_fermion_line, + ); + let capacity = checked_pairing_capacity( + left.completed_pairs.len(), + right.completed_pairs.len(), + possible_new_pair, + )?; + let mut completed_pairs = Vec::new(); + completed_pairs + .try_reserve_exact(capacity) + .map_err(|error| invalid(format!("pairing-lineage allocation failed: {error}")))?; + completed_pairs.extend_from_slice(&left.completed_pairs); + completed_pairs.extend_from_slice(&right.completed_pairs); + let unmatched_endpoint = match ( + left.unmatched_endpoint, + right.unmatched_endpoint, + carries_colored_fermion_line, + ) { + (Some(endpoint), None, true) | (None, Some(endpoint), true) => Some(endpoint), + (None, None, false) => None, + (Some(left), Some(right), false) => { + let Some(pair) = close_pairing_endpoints(seed, left, right)? else { + return Ok(None); + }; + completed_pairs.push(pair); + None + } + _ => return Ok(None), + }; + completed_pairs.sort_unstable(); + if completed_pairs.windows(2).any(|pair| pair[0] == pair[1]) { + return Err(integrity( + "pairing lineage repeats a completed endpoint pair", + )); + } + Ok(Some(PendingPairingLineage { + completed_pairs, + unmatched_endpoint, + })) +} + +pub(super) fn combine_pairing_lineage_sets( + seed: &OnTheFlyProcessSeedV1, + left: &[PendingPairingLineage], + right: &[PendingPairingLineage], + carries_colored_fermion_line: bool, +) -> RusticolResult> { + let capacity = left + .len() + .checked_mul(right.len()) + .ok_or_else(|| invalid("pairing-lineage product exceeds usize"))?; + let mut result = Vec::new(); + result + .try_reserve_exact(capacity) + .map_err(|error| invalid(format!("pairing-lineage allocation failed: {error}")))?; + for left in left { + for right in right { + if let Some(lineage) = + combine_pairing_lineage(seed, left, right, carries_colored_fermion_line)? + { + result.push(lineage); + } + } + } + result.sort_unstable(); + result.dedup(); + Ok(result) +} + +pub(super) fn extend_pairing_lineages( + target: &mut Vec, + source: &[PendingPairingLineage], +) -> RusticolResult<()> { + let mut additions = try_clone_pairing_lineages(source)?; + target + .try_reserve(additions.len()) + .map_err(|error| invalid(format!("pairing-lineage merge allocation failed: {error}")))?; + target.append(&mut additions); + target.sort_unstable(); + target.dedup(); + Ok(()) +} + +fn retain_canonical_pairing_lineage( + lineages: &mut Vec, +) -> RusticolResult<()> { + if lineages.is_empty() { + return Err(integrity( + "physical closure has no complete Wick-lineage owner", + )); + } + // Pairing lineage is cold proof state, not an additive amplitude term. + // Equal runtime closures therefore retain one semantic witness without + // duplicating or rescaling their already aggregated numerical factor. + lineages.sort_unstable(); + lineages.dedup(); + lineages.truncate(1); + Ok(()) +} + +pub(super) fn complete_pairing_lineage( + seed: &OnTheFlyProcessSeedV1, + lineage: &PendingPairingLineage, +) -> RusticolResult { + if lineage.unmatched_endpoint.is_some() { + return Ok(false); + } + let expected_endpoint_count = seed + .source_anchors + .iter() + .filter(|anchor| anchor.color_role.is_pairing_endpoint()) + .count(); + let observed_endpoint_count = lineage + .completed_pairs + .len() + .checked_mul(2) + .ok_or_else(|| invalid("pairing endpoint count exceeds usize"))?; + if observed_endpoint_count != expected_endpoint_count { + return Ok(false); + } + for (index, pair) in lineage.completed_pairs.iter().copied().enumerate() { + if close_pairing_endpoints(seed, pair[0], pair[1])? != Some(pair) { + return Ok(false); + } + if lineage.completed_pairs[..index] + .iter() + .flatten() + .any(|endpoint| *endpoint == pair[0] || *endpoint == pair[1]) + { + return Ok(false); + } + } + Ok(true) +} + +fn identity_source_permutation(source_count: usize) -> RusticolResult> { + let mut result = Vec::new(); + result + .try_reserve_exact(source_count) + .map_err(|error| invalid(format!("pairing identity allocation failed: {error}")))?; + for value in 0..source_count { + result.push(checked_u32(value, "pairing identity source slot")?); + } + Ok(result) +} + +fn lehmer_digits(reference: &[u32], selected: &[u32]) -> RusticolResult> { + if reference.len() != selected.len() { + return Err(invalid( + "discovered pairing length differs from its compact class", + )); + } + let mut remaining = Vec::new(); + remaining + .try_reserve_exact(reference.len()) + .map_err(|error| invalid(format!("pairing permutation allocation failed: {error}")))?; + remaining.extend_from_slice(reference); + let mut digits = Vec::new(); + digits + .try_reserve_exact(selected.len()) + .map_err(|error| invalid(format!("pairing Lehmer allocation failed: {error}")))?; + for selected_slot in selected { + let position = remaining + .iter() + .position(|candidate| candidate == selected_slot) + .ok_or_else(|| { + invalid("discovered pairing is not a permutation of its compact class") + })?; + digits.push(checked_u32(position, "pairing Lehmer digit")?); + remaining.remove(position); + } + if !remaining.is_empty() { + return Err(invalid("discovered pairing omits a compact-class endpoint")); + } + Ok(digits) +} + +fn resolve_pairing_owner( + seed: &OnTheFlyProcessSeedV1, + owner: &PendingPairingLineage, +) -> RusticolResult { + let mut source_slot_permutation = identity_source_permutation(seed.source_anchors.len())?; + let mut source_lineage = Vec::new(); + source_lineage + .try_reserve_exact(seed.source_anchors.len()) + .map_err(|error| invalid(format!("pairing source-lineage allocation failed: {error}")))?; + source_lineage.resize(seed.source_anchors.len(), MISSING_U32); + if seed.pairing_classes.is_empty() { + if owner + != &(PendingPairingLineage { + completed_pairs: Vec::new(), + unmatched_endpoint: None, + }) + { + return Err(integrity( + "pairing-free query retained a nontrivial closure lineage", + )); + } + return Ok(ResolvedPairingOwnerV1 { + endpoint_pairs: Vec::new(), + proof_digest: None, + source_slot_permutation, + source_lineage, + fermion_parity: 1, + }); + } + if !complete_pairing_lineage(seed, owner)? { + return Err(integrity( + "canonical projected closure has an incomplete Wick lineage", + )); + } + let endpoint_pairs = try_copy_pairing_pairs(&owner.completed_pairs)?; + if endpoint_pairs.iter().enumerate().any(|(index, pair)| { + endpoint_pairs[..index] + .iter() + .any(|other| other[0] == pair[0]) + }) { + return Err(integrity( + "canonical projected closure repeats a fundamental endpoint", + )); + } + let mut parity = 1_i32; + let mut proof_hash = Sha256::new(); + proof_hash.update(b"pyamplicol-on-the-fly-discovered-pairing-owner-v1\0"); + for pairing_class in &seed.pairing_classes { + let mut reference = Vec::new(); + reference + .try_reserve_exact(pairing_class.antifundamental_endpoints.len()) + .map_err(|error| invalid(format!("pairing reference allocation failed: {error}")))?; + reference.extend( + pairing_class + .antifundamental_endpoints + .iter() + .map(|endpoint| endpoint.source_slot), + ); + let mut selected = Vec::new(); + selected + .try_reserve_exact(pairing_class.fundamental_endpoints.len()) + .map_err(|error| invalid(format!("selected pairing allocation failed: {error}")))?; + for endpoint in &pairing_class.fundamental_endpoints { + let selected_slot = endpoint_pairs + .iter() + .find(|pair| pair[0] == endpoint.source_slot) + .map(|pair| pair[1]) + .ok_or_else(|| { + integrity("canonical Wick lineage omits a pairing-class fundamental") + })?; + selected.push(selected_slot); + } + let digits = lehmer_digits(&reference, &selected)?; + if digits.iter().map(|digit| u64::from(*digit)).sum::() % 2 == 1 { + parity = -parity; + } + hash_digest(&mut proof_hash, pairing_class.semantic_digest); + hash_len(&mut proof_hash, selected.len(), "discovered pairing class")?; + for (reference_slot, selected_slot) in + reference.iter().copied().zip(selected.iter().copied()) + { + source_slot_permutation[reference_slot as usize] = selected_slot; + proof_hash.update(reference_slot.to_le_bytes()); + proof_hash.update(selected_slot.to_le_bytes()); + } + for digit in digits { + proof_hash.update(digit.to_le_bytes()); + } + } + validate_permutation( + &source_slot_permutation, + seed.source_anchors.len(), + "discovered pairing source permutation", + )?; + for (line_id, pair) in endpoint_pairs.iter().copied().enumerate() { + let line_id = checked_u32(line_id, "discovered pairing line ID")?; + for source_slot in pair { + let entry = source_lineage + .get_mut(source_slot as usize) + .ok_or_else(|| integrity("discovered pairing endpoint is out of range"))?; + if *entry != MISSING_U32 { + return Err(integrity( + "discovered pairing endpoint belongs to multiple lines", + )); + } + *entry = line_id; + } + } + proof_hash.update(parity.to_le_bytes()); + Ok(ResolvedPairingOwnerV1 { + endpoint_pairs, + proof_digest: Some(final_digest(proof_hash)?), + source_slot_permutation, + source_lineage, + fermion_parity: parity, + }) +} + +fn projected_pairing_lineages( + closures: &[PendingClosure], +) -> RusticolResult> { + if closures.is_empty() { + return Err(integrity("canonical projected closure has no Wick lineage")); + } + let mut lineages = Vec::new(); + lineages + .try_reserve_exact(closures.len()) + .map_err(|error| { + invalid(format!( + "projected pairing-owner allocation failed: {error}" + )) + })?; + for closure in closures { + let [lineage] = closure.pairing_lineages.as_slice() else { + return Err(integrity(format!( + "canonical projected closure has {} Wick lineages, expected exactly one", + closure.pairing_lineages.len(), + ))); + }; + lineages.push(lineage); + } + Ok(lineages) +} + +pub(super) fn resolve_projected_pairing_owners( + seed: &OnTheFlyProcessSeedV1, + closures: &[PendingClosure], +) -> RusticolResult> { + projected_pairing_lineages(closures)? + .into_iter() + .map(|lineage| resolve_pairing_owner(seed, lineage)) + .collect() +} + +pub(super) fn supports_are_disjoint(left: &[u32], right: &[u32]) -> bool { + let mut left_index = 0usize; + let mut right_index = 0usize; + while left_index < left.len() && right_index < right.len() { + match left[left_index].cmp(&right[right_index]) { + std::cmp::Ordering::Less => left_index += 1, + std::cmp::Ordering::Greater => right_index += 1, + std::cmp::Ordering::Equal => return false, + } + } + true +} + +pub(super) fn merge_disjoint_support(left: &[u32], right: &[u32]) -> RusticolResult> { + if !supports_are_disjoint(left, right) { + return Err(invalid( + "query-local parents have overlapping source support", + )); + } + let mut result = left.iter().chain(right).copied().collect::>(); + result.sort_unstable(); + Ok(result) +} + +fn selected_source_state( + seed: &OnTheFlyProcessSeedV1, + selected: OnTheFlySelectedSourceV1, +) -> RusticolResult<(&OnTheFlySourceAnchorV1, &OnTheFlySourceStateV1)> { + let anchor = seed + .source_anchors + .get(selected.source_slot as usize) + .ok_or_else(|| invalid("selected source slot is absent from the compact seed"))?; + let state = anchor.selected(selected.state_index, selected.public_helicity)?; + Ok((anchor, state)) +} + +pub(super) fn validate_seed_against_templates<'a>( + templates: &'a ValidatedRecurrenceTemplateInput, + seed: &OnTheFlyProcessSeedV1, +) -> RusticolResult> { + let summary = templates.summary(); + if summary.catalog_digest != seed.template_catalog_digest + || summary.compiled_model_digest != seed.model_digest + || summary.prepared_kernel_pack_digest != seed.prepared_pack_digest + { + return Err(integrity( + "compact seed belongs to a different model/template/prepared catalog", + )); + } + let catalog = TemplateCatalog::new(templates.input())?; + if catalog.coupling_order_dimension() != seed.explicit_coupling_limits().len() { + return Err(integrity( + "compact coupling-limit dimension differs from the template catalog", + )); + } + Ok(catalog) +} + +pub(super) fn validate_source_contract( + templates: &ValidatedRecurrenceTemplateInput, + catalog: &TemplateCatalog<'_>, + anchor: &OnTheFlySourceAnchorV1, + state: &OnTheFlySourceStateV1, +) -> RusticolResult<(SourceRow, crate::recurrence::template::CurrentStateRow)> { + let input = templates.input(); + let source = *input + .sources + .get(state.source_template_id as usize) + .ok_or_else(|| integrity("compact source template is absent"))?; + let current_state = *input + .current_states + .get(state.current_state_template_id as usize) + .ok_or_else(|| integrity("compact current-state template is absent"))?; + if source.id != state.source_template_id + || current_state.id != state.current_state_template_id + || source.flavour_flow_id as usize >= input.flavour_flow_ranges.len() + || source.quantum_number_flow_id != state.quantum_number_flow_id + || current_state.chirality != state.chirality + { + return Err(integrity("compact source-state contract is stale")); + } + if catalog.flavour_flow(source.flavour_flow_id, "source flavour flow")? + != state.flavour_flow.as_ref() + || catalog.digest(source.semantic_digest_id, "source semantic")? + != state.source_semantic_digest + || catalog.digest(current_state.semantic_digest_id, "current-state semantic")? + != state.current_state_semantic_digest + || catalog.source_seed(source)?.proof_digest() != state.color_seed_proof_digest + { + return Err(integrity( + "compact source-state semantic evidence differs from the template catalog", + )); + } + validate_crossed_source_state( + anchor.is_initial, + &ProcessSourceStateRow { + source_slot: anchor.source_slot, + state_index: state.state_index, + public_helicity: state.public_helicity, + chirality: state.chirality, + spin_state: state.spin_state, + current_state_template_id: state.current_state_template_id, + source_template_id: state.source_template_id, + momentum_sign: state.momentum_sign, + // The helper does not consume the process-owned factor ID; the + // compact seed authenticates the exact crossing phase directly. + crossing_phase_factor_id: 0, + }, + source, + input, + )?; + let binding = input + .evaluator_bindings + .get(source.evaluator_binding_id as usize) + .ok_or_else(|| integrity("source evaluator binding is absent"))?; + if binding.id != source.evaluator_binding_id + || EvaluatorContractKind::try_from(binding.contract_kind)? != EvaluatorContractKind::Source + { + return Err(integrity("source evaluator binding has the wrong role")); + } + Ok((source, current_state)) +} + +fn query_target_matches(closed: &[LCColorComponent], query: &DecodedLcQueryV1) -> bool { + let mut closed = closed.to_vec(); + closed.sort_unstable(); + closed.as_slice() == query.target_components.as_ref() +} + +fn reflected_query_closure_factor( + query: &DecodedLcQueryV1, + closed: &[LCColorComponent], + colors: &DynamicLCColorStateInterner, + currents: &[PendingCurrent], + parent_ids: [u32; 2], +) -> RusticolResult> { + let mut certified_parent = None; + for parent_id in parent_ids { + let current = currents + .get(parent_id as usize) + .ok_or_else(|| integrity("query-local closure reflection parent is absent"))?; + let Some(certificate) = current.reflection_certificate.as_ref() else { + continue; + }; + if certified_parent.replace((current, certificate)).is_some() { + return Err(integrity( + "one query-local closure references multiple folded reflection orbits", + )); + } + } + let Some((current, certificate)) = certified_parent else { + return Ok(None); + }; + let proof = current.reflection.proof().ok_or_else(|| { + integrity("certified query-local closure parent has no reflection lineage") + })?; + if proof.proof_digest() != certificate.canonical_lineage_digest() { + return Err(integrity( + "query-local closure reflection lineage differs from its certificate", + )); + } + let color = colors + .get(current.key.dynamic_lc_color_state_id()) + .ok_or_else(|| integrity("query-local closure reflection color is absent"))?; + if dynamic_color_identity_digest(color)? != certificate.canonical_color_identity() { + return Err(integrity( + "query-local closure reflection color differs from its certificate", + )); + } + if !certificate.is_reciprocal_two_cycle() { + return Err(integrity( + "query-local closure reflection certificate is not a reciprocal two-cycle", + )); + } + let [closed_trace] = closed else { + return Ok(None); + }; + let [target_trace] = query.target_components.as_ref() else { + return Ok(None); + }; + let source_count = query.selected_sources.len(); + if source_count <= 2 + || closed_trace.kind() != LCColorComponentKind::Trace + || target_trace.kind() != LCColorComponentKind::Trace + || closed_trace.source_slots().len() != source_count + || target_trace.source_slots().len() != source_count + { + return Ok(None); + } + validate_permutation( + closed_trace.source_slots(), + source_count, + "query-local reflected closure word", + )?; + let permutation = certificate.source_permutation(); + validate_permutation( + permutation, + source_count, + "query-local closure reflection permutation", + )?; + if permutation.get(query.closure_anchor_slot as usize).copied() + != Some(query.closure_anchor_slot) + { + return Err(integrity( + "query-local closure reflection does not fix its certified anchor", + )); + } + let mapped_word = closed_trace + .source_slots() + .iter() + .map(|source_slot| { + permutation + .get(*source_slot as usize) + .copied() + .ok_or_else(|| integrity("closure reflection omits a source slot")) + }) + .collect::>>()?; + let mapped_trace = LCColorComponent::new(LCColorComponentKind::Trace, mapped_word)?; + if &mapped_trace == closed_trace { + return Err(integrity( + "query-local closure reflection maps a trace to a cyclic fixed point", + )); + } + Ok((&mapped_trace == target_trace).then_some(certificate.canonical_phase())) +} + +#[allow(clippy::too_many_arguments)] // Explicitly mirrors the query-local sweep state. +pub(super) fn insert_selected_sources( + grammar: &PreparedOnTheFlyGrammarV1, + seed: &OnTheFlyProcessSeedV1, + coupling_limits: &[Option], + query: &DecodedLcQueryV1, + enable_cyclic_trace_reflection: bool, + colors: &mut DynamicLCColorStateInterner, + currents: &mut Vec, + current_ids: &mut BTreeMap, +) -> RusticolResult<()> { + if coupling_limits.len() != seed.explicit_coupling_limits().len() { + return Err(integrity( + "resolved coupling-limit dimension differs from the compact seed", + )); + } + let zero_orders = vec![0_u32; coupling_limits.len()]; + for selected in query.selected_sources.iter().copied() { + let (_, state) = selected_source_state(seed, selected)?; + let contract = grammar + .sources + .get(&(state.source_template_id, state.current_state_template_id)) + .ok_or_else(|| integrity("selected source has no prepared source contract"))?; + let color = contract.color_seed.instantiate( + selected.source_slot, + contract.current_state.color_representation, + )?; + let color_id = colors.intern(color)?; + let reflection = if enable_cyclic_trace_reflection { + source_reflection( + colors, + color_id, + selected.source_slot, + state.color_seed_proof_digest, + )? + } else { + CurrentReflection::Unavailable + }; + let key = CurrentCoreKey::new( + seed.template_catalog_digest, + RecurrenceNodeKind::Source, + state.current_state_template_id, + color_id, + vec![selected.source_slot], + CanonicalMomentumLinearForm::new(vec![MomentumTerm { + source_slot: selected.source_slot, + coefficient: state.momentum_sign, + }])?, + CurrentHelicityIdentity::topology_replay( + state.spin_state, + vec![SourceStateAssignment::new( + selected.source_slot, + selected.state_index, + )], + )?, + state.flavour_flow.to_vec(), + state.quantum_number_flow_id, + zero_orders.clone(), + CurrentSourceBinding::FixedTemplate(state.source_template_id), + None, + )?; + let id = checked_u32(currents.len(), "query-local current count")?; + if current_ids.insert(key.clone(), id).is_some() { + return Err(integrity( + "selected source construction produced a duplicate current", + )); + } + currents.push(PendingCurrent { + key, + source_factor: Some(state.crossing_phase), + contributions: BTreeMap::new(), + pairing_lineages: vec![PendingPairingLineage::source(seed, selected.source_slot)], + stage: 0, + reflection, + reflection_certificate: None, + }); + } + Ok(()) +} + +#[allow(clippy::too_many_arguments)] +fn include_transition( + templates: &ValidatedRecurrenceTemplateInput, + prepared: &PreparedTransition, + transition_reflections: &TransitionReflectionIndex, + enable_cyclic_trace_reflection: bool, + concrete_parent_ids: [u32; 2], + source_count: usize, + seed: &OnTheFlyProcessSeedV1, + fermion_ordering: &FermionOrderingContext, + coupling_limits: &[Option], + propagators: &BTreeMap>, + colors: &mut DynamicLCColorStateInterner, + currents: &mut Vec, + current_ids: &mut BTreeMap, +) -> RusticolResult<()> { + let parents = [ + ¤ts[concrete_parent_ids[0] as usize].key, + ¤ts[concrete_parent_ids[1] as usize].key, + ]; + if !(0..2).all(|index| { + prepared.input_states[index] == parents[index].current_state_template_id() + && quantum_parent_spin_matches(prepared.input_spins[index], parents[index]) + }) { + return Ok(()); + } + if let Some(contact_orbit) = prepared.contact_orbit.as_ref() + && !contact_orbit.accepts_parent_domain(parents)? + { + return Ok(()); + } + let Some(coupling_orders) = combined_coupling_orders( + parents[0].coupling_orders(), + parents[1].coupling_orders(), + &prepared.local_orders, + coupling_limits, + )? + else { + return Ok(()); + }; + let support = merge_disjoint_support( + parents[0].support_source_slots(), + parents[1].support_source_slots(), + )?; + if support.len() >= source_count { + return Ok(()); + } + let helicity = merged_helicity_identity( + parents[0].helicity_identity(), + parents[1].helicity_identity(), + prepared.quantum.result_spin_state, + )?; + let flavour = prepared.flavour.apply(parents); + let momentum = merged_momentum(parents[0].momentum(), parents[1].momentum())?; + let parent_colors = [ + colors + .get(parents[0].dynamic_lc_color_state_id()) + .ok_or_else(|| integrity("left query-local color state disappeared"))? + .clone(), + colors + .get(parents[1].dynamic_lc_color_state_id()) + .ok_or_else(|| integrity("right query-local color state disappeared"))? + .clone(), + ]; + let (evaluator_parent_ids, exchange_factor) = prepared.evaluator_parents(concrete_parent_ids); + let fermion_factor = fermion_ordering_factor( + &templates.input().current_states, + [ + parents[0].current_state_template_id(), + parents[1].current_state_template_id(), + ], + [ + parents[0].support_source_slots(), + parents[1].support_source_slots(), + ], + fermion_ordering, + )?; + let result_state = templates + .input() + .current_states + .get(prepared.row.result_state_template_id as usize) + .ok_or_else(|| integrity("pairing-lineage result state is absent"))?; + let carries_colored_fermion_line = + result_state.statistics == 1 && result_state.color_representation != 1; + let pairing_lineages = combine_pairing_lineage_sets( + seed, + ¤ts[concrete_parent_ids[0] as usize].pairing_lineages, + ¤ts[concrete_parent_ids[1] as usize].pairing_lineages, + carries_colored_fermion_line, + )?; + if pairing_lineages.is_empty() { + return Ok(()); + } + let parent_reflections = if enable_cyclic_trace_reflection { + [ + currents[concrete_parent_ids[0] as usize].reflection.clone(), + currents[concrete_parent_ids[1] as usize].reflection.clone(), + ] + } else { + [ + CurrentReflection::Unavailable, + CurrentReflection::Unavailable, + ] + }; + let reversal_masks = if enable_cyclic_trace_reflection { + current_reversal_masks(&parent_colors, &parent_reflections) + } else { + vec![0] + }; + let local_reflection_proof = enable_cyclic_trace_reflection + .then(|| transition_reflections.proof(prepared.row.id)) + .flatten(); + let output_factor = prepared.output_factor()?; + for prepared_witness in &prepared.witnesses { + if prepared_witness.row.left_shape_string_id != parent_colors[0].output_color_shape_id() + || prepared_witness.row.right_shape_string_id + != parent_colors[1].output_color_shape_id() + { + continue; + } + for reversal_mask in reversal_masks.iter().copied() { + let mut variant_colors = parent_colors.clone(); + let mut reversal_factor = ExactComplexRational::ONE; + for index in 0..2 { + if reversal_mask & (1 << index) == 0 { + continue; + } + variant_colors[index] = variant_colors[index].reversed()?; + reversal_factor = reversal_factor.checked_mul( + parent_reflections[index] + .phase() + .expect("reversal mask requires a proven parent phase"), + )?; + } + let Some(result_color) = prepared_witness + .witness + .apply(&variant_colors[0], &variant_colors[1])? + else { + continue; + }; + let result_reflection = current_reflection_candidate( + &result_color, + &parent_reflections, + local_reflection_proof, + )?; + #[cfg(feature = "on-the-fly-test-support")] + let diagnostic_result_reflection = result_reflection.clone(); + let color_id = colors.intern(result_color)?; + let propagator_template_id = if support.len() + 1 < source_count { + propagators + .get(&prepared.row.result_state_template_id) + .copied() + .flatten() + } else { + None + }; + let key = CurrentCoreKey::new( + seed.template_catalog_digest, + RecurrenceNodeKind::Current, + prepared.row.result_state_template_id, + color_id, + support.clone(), + momentum.clone(), + helicity.clone(), + flavour.clone(), + prepared.quantum.result_quantum_number_flow_id, + coupling_orders.clone(), + CurrentSourceBinding::None, + propagator_template_id, + )?; + let result_id = if let Some(id) = current_ids.get(&key).copied() { + currents[id as usize] + .reflection + .include(result_reflection)?; + id + } else { + let id = checked_u32(currents.len(), "query-local current count")?; + let map_key = key.clone(); + let pending = PendingCurrent { + key, + source_factor: None, + contributions: BTreeMap::new(), + pairing_lineages: try_clone_pairing_lineages(&pairing_lineages)?, + stage: checked_u32(support.len() - 1, "query-local current stage")?, + reflection: result_reflection + .map_or(CurrentReflection::Unavailable, CurrentReflection::Proven), + reflection_certificate: None, + }; + current_ids.insert(map_key, id); + currents.push(pending); + id + }; + extend_pairing_lineages( + &mut currents[result_id as usize].pairing_lineages, + &pairing_lineages, + )?; + let contribution_key = ContributionKey::new( + prepared.row.id, + evaluator_parent_ids.to_vec(), + evaluator_parent_ids + .iter() + .map(|id| currents[*id as usize].key.current_state_template_id()) + .collect(), + evaluator_parent_ids + .iter() + .map(|id| currents[*id as usize].key.momentum().clone()) + .collect(), + prepared.row.result_state_template_id, + prepared.quantum.id, + LCColorWitnessTermId::new( + prepared.row.color_contraction_template_id, + prepared_witness.row.ordinal, + ), + prepared.quantum_semantic_digest, + prepared.row.output_projection_string_id, + )?; + let pending_key = PendingContributionKey { + parent_current_ids: evaluator_parent_ids, + key: contribution_key, + }; + let witness_factor = prepared_witness.witness.exact_factor(); + let factor = multiply_factors(&[ + prepared.base_factor, + output_factor, + exchange_factor, + witness_factor, + fermion_factor, + reversal_factor, + ])?; + let aggregate = currents[result_id as usize] + .contributions + .entry(pending_key) + .or_insert(ExactComplexRational::ZERO); + aggregate_factor(aggregate, factor)?; + #[cfg(feature = "on-the-fly-test-support")] + let aggregate_factor_after = *aggregate; + #[cfg(feature = "on-the-fly-test-support")] + 'transition_diagnostic: { + if !crate::recurrence::diagnostic::transition_diagnostic_observation_active() { + break 'transition_diagnostic; + } + use crate::recurrence::diagnostic::{ + ConstructionTransitionDiagnosticRowV1, observe_transition_diagnostic, + }; + + let digest = |current_id: u32| -> RusticolResult { + let current = currents + .get(current_id as usize) + .ok_or_else(|| integrity("diagnostic current is absent"))?; + let color = colors + .get(current.key.dynamic_lc_color_state_id()) + .ok_or_else(|| integrity("diagnostic current color is absent"))?; + super::trace::hash_current_key(¤t.key, color) + }; + let result_color = colors + .get(currents[result_id as usize].key.dynamic_lc_color_state_id()) + .ok_or_else(|| integrity("diagnostic result color is absent"))?; + observe_transition_diagnostic(ConstructionTransitionDiagnosticRowV1 { + materialized_sector_id: None, + output_current_digest: digest(result_id)?, + ordered_parent_digests: [ + digest(evaluator_parent_ids[0])?, + digest(evaluator_parent_ids[1])?, + ], + transition_template_id: prepared.row.id, + transition_semantic_digest: prepared.transition_semantic_digest, + evaluator_binding_semantic_digest: prepared.evaluator_binding_digest, + result_state_template_id: prepared.row.result_state_template_id, + quantum_flow_witness_id: prepared.quantum.id, + quantum_semantic_digest: prepared.quantum_semantic_digest, + color_contraction_template_id: prepared.row.color_contraction_template_id, + color_witness_ordinal: prepared_witness.row.ordinal, + color_witness_proof_digest: prepared_witness.witness.proof_digest(), + output_projection_id: prepared.row.output_projection_string_id, + transition_factor: prepared.transition_factor, + contraction_factor: prepared.contraction_factor, + output_factor, + exchange_factor, + witness_factor, + reversal_mask, + reversal_factor, + candidate_factor: factor, + aggregate_factor_after, + parent_reflection_proof_digests: [ + parent_reflections[0] + .proof() + .map(|proof| proof.proof_digest()), + parent_reflections[1] + .proof() + .map(|proof| proof.proof_digest()), + ], + parent_reflection_phases: [ + parent_reflections[0].phase(), + parent_reflections[1].phase(), + ], + local_reflection_proof_digest: local_reflection_proof + .map(|proof| proof.proof_digest()), + local_reflection_phase: local_reflection_proof.map(|proof| proof.phase()), + result_reflection_proof_digest: diagnostic_result_reflection + .as_ref() + .map(|proof| proof.proof_digest()), + result_reflection_phase: diagnostic_result_reflection + .as_ref() + .map(|proof| proof.phase()), + output_color_orientation: format!("{result_color:?}"), + }); + } + } + } + Ok(()) +} + +#[derive(Debug)] +struct QuerySupportSizeIndex { + support_size_by_current: Vec, + composite_parent_eligible_by_current: Vec, + current_ids_by_size: Vec>, +} + +impl QuerySupportSizeIndex { + fn new( + currents: &[PendingCurrent], + source_count: usize, + closure_anchor_slot: Option, + ) -> RusticolResult { + if closure_anchor_slot.is_some_and(|slot| slot as usize >= source_count) { + return Err(invalid( + "query closure anchor is outside the forward source domain", + )); + } + let mut index = Self { + support_size_by_current: Vec::with_capacity(currents.len()), + composite_parent_eligible_by_current: Vec::with_capacity(currents.len()), + current_ids_by_size: (0..source_count).map(|_| Vec::new()).collect(), + }; + let mut anchor_source_count = 0usize; + for (current_id, current) in currents.iter().enumerate() { + let support = current.key.support_source_slots(); + let contains_anchor = + closure_anchor_slot.is_some_and(|anchor| support.binary_search(&anchor).is_ok()); + if contains_anchor { + let anchor = closure_anchor_slot.expect("tested present closure anchor"); + if current.key.node_kind() != RecurrenceNodeKind::Source || support != [anchor] { + return Err(integrity( + "initial forward domain contains a non-singleton closure-anchor current", + )); + } + anchor_source_count += 1; + } + index.append(current_id, support.len(), !contains_anchor)?; + } + if closure_anchor_slot.is_some() && anchor_source_count != 1 { + return Err(integrity( + "initial forward domain must contain exactly one singleton closure-anchor source", + )); + } + Ok(index) + } + + fn append( + &mut self, + current_id: usize, + support_size: usize, + composite_parent_eligible: bool, + ) -> RusticolResult<()> { + if current_id != self.support_size_by_current.len() + || current_id != self.composite_parent_eligible_by_current.len() + { + return Err(integrity( + "query-local support-size index lost current-ID order", + )); + } + let bucket = self + .current_ids_by_size + .get_mut(support_size) + .filter(|_| support_size > 0) + .ok_or_else(|| integrity("query-local current has invalid forward support size"))?; + self.support_size_by_current.push(support_size); + self.composite_parent_eligible_by_current + .push(composite_parent_eligible); + if composite_parent_eligible { + bucket.push(current_id); + } + Ok(()) + } + + fn append_stage( + &mut self, + currents: &[PendingCurrent], + stage_current_start: usize, + target_size: usize, + closure_anchor_slot: Option, + ) -> RusticolResult<()> { + if stage_current_start != self.support_size_by_current.len() + || stage_current_start != self.composite_parent_eligible_by_current.len() + || stage_current_start > currents.len() + { + return Err(integrity( + "query-local support-size index differs from the stage prefix", + )); + } + for (current_id, current) in currents.iter().enumerate().skip(stage_current_start) { + let support = current.key.support_source_slots(); + let support_size = support.len(); + if support_size != target_size { + return Err(integrity( + "query-local sweep produced a current in the wrong support-size stage", + )); + } + if closure_anchor_slot.is_some_and(|anchor| support.binary_search(&anchor).is_ok()) { + return Err(integrity( + "query-local sweep produced a composite containing the closure anchor", + )); + } + self.append(current_id, support_size, true)?; + } + Ok(()) + } + + fn for_each_parent_pair( + &self, + target_size: usize, + stage_current_end: usize, + mut visit: impl FnMut([usize; 2]) -> RusticolResult<()>, + ) -> RusticolResult<()> { + if stage_current_end != self.support_size_by_current.len() + || stage_current_end != self.composite_parent_eligible_by_current.len() + { + return Err(integrity( + "query-local support-size schedule differs from its current prefix", + )); + } + for left_id in 0..stage_current_end { + if !self.composite_parent_eligible_by_current[left_id] { + continue; + } + let left_size = self.support_size_by_current[left_id]; + if left_size >= target_size { + continue; + } + let right_size = target_size - left_size; + let right_ids = self + .current_ids_by_size + .get(right_size) + .ok_or_else(|| integrity("query-local right support-size bucket is absent"))?; + let first_right = right_ids.partition_point(|right_id| *right_id <= left_id); + for right_id in right_ids[first_right..].iter().copied() { + visit([left_id, right_id])?; + } + } + Ok(()) + } +} + +/// Fold only a completely certified reciprocal pure-adjoint orbit after the +/// stage fan-in and contact-owner selection are final. Any missing, stale, or +/// contradictory proof retains both orientations. +fn reconcile_on_the_fly_stage_reflections( + stage_start: usize, + colors: &mut DynamicLCColorStateInterner, + currents: &mut Vec, + current_ids: &mut BTreeMap, + source_count: usize, +) -> RusticolResult<()> { + if stage_start > currents.len() { + return Err(invalid( + "query-local reflection stage starts beyond current storage", + )); + } + let stage_end = currents.len(); + let mut visited = vec![false; stage_end - stage_start]; + let mut prune = vec![false; stage_end - stage_start]; + + for current_index in stage_start..stage_end { + let local_index = current_index - stage_start; + if visited[local_index] { + continue; + } + let key = currents[current_index].key.clone(); + let color = colors + .get(key.dynamic_lc_color_state_id()) + .ok_or_else(|| integrity("query-local reflection color disappeared"))? + .clone(); + let Some(word) = color.pure_adjoint_word() else { + continue; + }; + if word.len() < 2 { + continue; + } + let canonical = pure_adjoint_word_is_canonical(word); + let reversed_color_id = colors.intern(color.reversed()?)?; + if reversed_color_id == key.dynamic_lc_color_state_id() { + currents[current_index].reflection = CurrentReflection::Unavailable; + continue; + } + let reversed_key = current_key_with_dynamic_color(&key, reversed_color_id)?; + let Some(reversed_id) = current_ids.get(&reversed_key).copied() else { + currents[current_index].reflection = CurrentReflection::Unavailable; + continue; + }; + let reversed_index = reversed_id as usize; + if !(stage_start..stage_end).contains(&reversed_index) { + currents[current_index].reflection = CurrentReflection::Unavailable; + continue; + } + let reversed_local_index = reversed_index - stage_start; + visited[local_index] = true; + visited[reversed_local_index] = true; + + let reversed_color = colors + .get(reversed_color_id) + .ok_or_else(|| integrity("query-local reversed reflection color disappeared"))?; + let Some(reversed_word) = reversed_color.pure_adjoint_word() else { + currents[current_index].reflection = CurrentReflection::Unavailable; + currents[reversed_index].reflection = CurrentReflection::Unavailable; + continue; + }; + let reciprocal = reciprocal_reflection_proof( + ¤ts[current_index].reflection, + ¤ts[reversed_index].reflection, + )?; + if !reciprocal || canonical == pure_adjoint_word_is_canonical(reversed_word) { + currents[current_index].reflection = CurrentReflection::Unavailable; + currents[reversed_index].reflection = CurrentReflection::Unavailable; + continue; + } + let (canonical_index, reflected_index, pruned_local_index) = if canonical { + (current_index, reversed_index, reversed_local_index) + } else { + (reversed_index, current_index, local_index) + }; + let canonical_proof = currents[canonical_index] + .reflection + .proof() + .cloned() + .ok_or_else(|| integrity("canonical query-local reflection proof disappeared"))?; + let reflected_proof = currents[reflected_index] + .reflection + .proof() + .cloned() + .ok_or_else(|| integrity("reflected query-local reflection proof disappeared"))?; + for (label, index, proof) in [ + ("canonical", canonical_index, &canonical_proof), + ("reflected", reflected_index, &reflected_proof), + ] { + let current_color = colors + .get(currents[index].key.dynamic_lc_color_state_id()) + .ok_or_else(|| integrity(format!("{label} query-local color disappeared")))?; + if dynamic_color_identity_digest(current_color)? != proof.result_color_identity() { + return Err(integrity(format!( + "{label} query-local reflection proof has a stale color identity" + ))); + } + } + let canonical_color = colors + .get(currents[canonical_index].key.dynamic_lc_color_state_id()) + .ok_or_else(|| integrity("canonical query-local reflection color disappeared"))?; + let reflected_color = colors + .get(currents[reflected_index].key.dynamic_lc_color_state_id()) + .ok_or_else(|| integrity("reflected query-local reflection color disappeared"))?; + currents[canonical_index].reflection_certificate = + Some(PendingReflectionCertificate::reciprocal_pair( + checked_u32(canonical_index, "query-local reflection certificate ID")?, + checked_u32( + canonical_index, + "canonical query-local reflection current ID", + )?, + checked_u32( + reflected_index, + "reflected query-local reflection current ID", + )?, + &canonical_proof, + &reflected_proof, + canonical_color, + reflected_color, + source_count, + )?); + prune[pruned_local_index] = true; + } + + for current in ¤ts[stage_start..] { + current_ids.remove(¤t.key); + } + let stage_currents = currents.split_off(stage_start); + for (local_index, current) in stage_currents.into_iter().enumerate() { + if prune[local_index] { + continue; + } + if current + .contributions + .keys() + .flat_map(|contribution| contribution.parent_current_ids) + .any(|parent_id| parent_id as usize >= stage_start) + { + return Err(integrity( + "query-local reflection stage depends on another current in the same stage", + )); + } + let current_id = checked_u32(currents.len(), "query-local reflected current ID")?; + if current_ids + .insert(current.key.clone(), current_id) + .is_some() + { + return Err(integrity( + "query-local reflection reconciliation produced a duplicate current", + )); + } + currents.push(current); + } + Ok(()) +} + +#[allow(clippy::too_many_arguments)] // Explicitly mirrors the query-local sweep state. +pub(super) fn build_forward_currents( + templates: &ValidatedRecurrenceTemplateInput, + transitions: &BTreeMap<(u32, u32), Vec>, + transition_reflections: &TransitionReflectionIndex, + contact_orbits: &PreparedContactOrbitIndex, + enable_cyclic_trace_reflection: bool, + seed: &OnTheFlyProcessSeedV1, + fermion_ordering: &FermionOrderingContext, + closure_anchor_slot: u32, + coupling_limits: &[Option], + propagators: &BTreeMap>, + colors: &mut DynamicLCColorStateInterner, + currents: &mut Vec, + current_ids: &mut BTreeMap, +) -> RusticolResult<()> { + build_forward_currents_with_anchor_exclusion( + templates, + transitions, + transition_reflections, + contact_orbits, + enable_cyclic_trace_reflection, + seed, + fermion_ordering, + Some(closure_anchor_slot), + coupling_limits, + propagators, + colors, + currents, + current_ids, + ) +} + +#[allow(clippy::too_many_arguments)] +fn build_forward_currents_with_anchor_exclusion( + templates: &ValidatedRecurrenceTemplateInput, + transitions: &BTreeMap<(u32, u32), Vec>, + transition_reflections: &TransitionReflectionIndex, + contact_orbits: &PreparedContactOrbitIndex, + enable_cyclic_trace_reflection: bool, + seed: &OnTheFlyProcessSeedV1, + fermion_ordering: &FermionOrderingContext, + closure_anchor_slot: Option, + coupling_limits: &[Option], + propagators: &BTreeMap>, + colors: &mut DynamicLCColorStateInterner, + currents: &mut Vec, + current_ids: &mut BTreeMap, +) -> RusticolResult<()> { + let source_count = seed.source_anchors.len(); + // The selected closure always combines its singleton anchor source with + // the exact complementary support. Supports only grow by disjoint union, + // so a composite containing the anchor can never feed that complement. + // Keep the source current itself for closure, but exclude it from the + // ordered composite-parent schedule before any transition is considered. + let mut support_size_index = + QuerySupportSizeIndex::new(currents, source_count, closure_anchor_slot)?; + for target_size in 2..source_count { + let stage_current_start = currents.len(); + support_size_index.for_each_parent_pair( + target_size, + stage_current_start, + |[left_index, right_index]| { + let left = ¤ts[left_index].key; + let right = ¤ts[right_index].key; + debug_assert_eq!( + left.support_source_slots().len() + right.support_source_slots().len(), + target_size, + ); + if !supports_are_disjoint(left.support_source_slots(), right.support_source_slots()) + { + return Ok(()); + } + let left_state = left.current_state_template_id(); + let right_state = right.current_state_template_id(); + let Some(rows) = transitions.get(&canonical_state_pair(left_state, right_state)) + else { + return Ok(()); + }; + let left_id = checked_u32(left_index, "query-local parent ID")?; + let right_id = checked_u32(right_index, "query-local parent ID")?; + for prepared in rows { + let Some(parent_ids) = + prepared.parent_ids(left_state, right_state, left_id, right_id) + else { + continue; + }; + include_transition( + templates, + prepared, + transition_reflections, + enable_cyclic_trace_reflection, + parent_ids, + source_count, + seed, + fermion_ordering, + coupling_limits, + propagators, + colors, + currents, + current_ids, + )?; + } + Ok(()) + }, + )?; + if !contact_orbits.is_empty() + && let Some(plan) = plan_on_the_fly_contact_orbit_owners( + stage_current_start, + transitions, + contact_orbits, + currents, + )? + { + plan.commit(currents); + } + if enable_cyclic_trace_reflection { + reconcile_on_the_fly_stage_reflections( + stage_current_start, + colors, + currents, + current_ids, + source_count, + )?; + } + support_size_index.append_stage( + currents, + stage_current_start, + target_size, + closure_anchor_slot, + )?; + } + Ok(()) +} + +fn closure_quantum_matches( + quantum: &PreparedClosureQuantum, + parents: [&CurrentCoreKey; 2], +) -> bool { + match (quantum.input_states, quantum.input_spins) { + (None, None) => true, + (Some(states), Some(spins)) => (0..2).all(|index| { + states[index] == parents[index].current_state_template_id() + && quantum_parent_spin_matches(spins[index], parents[index]) + }), + _ => false, + } +} + +#[allow(clippy::too_many_arguments)] // Explicitly mirrors the query-local sweep state. +pub(super) fn build_selected_closures( + templates: &ValidatedRecurrenceTemplateInput, + closures: &BTreeMap<(u32, u32), Vec>, + seed: &OnTheFlyProcessSeedV1, + fermion_ordering: &FermionOrderingContext, + query: &DecodedLcQueryV1, + enable_cyclic_trace_reflection: bool, + colors: &DynamicLCColorStateInterner, + currents: &[PendingCurrent], +) -> RusticolResult>> { + let anchor_support = [query.closure_anchor_slot]; + let complement_support = (0..query.selected_sources.len() as u32) + .filter(|slot| *slot != query.closure_anchor_slot) + .collect::>(); + let anchor_ids = currents + .iter() + .enumerate() + .filter(|(_, current)| { + current.key.node_kind() == RecurrenceNodeKind::Source + && current.key.support_source_slots() == anchor_support + }) + .map(|(id, _)| checked_u32(id, "closure anchor current ID")) + .collect::>>()?; + if anchor_ids.len() != 1 { + return Err(invalid( + "decoded closure anchor does not identify exactly one source", + )); + } + let complement_ids = currents + .iter() + .enumerate() + .filter(|(_, current)| current.key.support_source_slots() == complement_support) + .map(|(id, _)| checked_u32(id, "closure complement current ID")) + .collect::>>()?; + + let mut retained = BTreeMap::::new(); + let anchor_id = anchor_ids[0]; + let anchor_state = currents[anchor_id as usize].key.current_state_template_id(); + for complement_id in complement_ids { + let complement_state = currents[complement_id as usize] + .key + .current_state_template_id(); + let Some(rows) = closures.get(&canonical_state_pair(anchor_state, complement_state)) else { + continue; + }; + for closure in rows { + let Some(concrete_parent_ids) = + closure.parent_ids(anchor_state, complement_state, anchor_id, complement_id) + else { + continue; + }; + let parents = [ + ¤ts[concrete_parent_ids[0] as usize].key, + ¤ts[concrete_parent_ids[1] as usize].key, + ]; + let pairing_lineages = combine_pairing_lineage_sets( + // A physical closure is bosonic and must close the final + // unmatched fundamental/antifundamental pair, if any. + // Completed pairs remain query-local proof state only. + seed, + ¤ts[concrete_parent_ids[0] as usize].pairing_lineages, + ¤ts[concrete_parent_ids[1] as usize].pairing_lineages, + false, + )?; + let mut complete_lineages = Vec::new(); + complete_lineages + .try_reserve_exact(pairing_lineages.len()) + .map_err(|error| { + invalid(format!( + "complete pairing-lineage allocation failed: {error}" + )) + })?; + for lineage in pairing_lineages { + if complete_pairing_lineage(seed, &lineage)? { + complete_lineages.push(lineage); + } + } + let pairing_lineages = complete_lineages; + if pairing_lineages.is_empty() { + continue; + } + let parent_colors = [ + colors + .get(parents[0].dynamic_lc_color_state_id()) + .ok_or_else(|| integrity("closure left color state disappeared"))?, + colors + .get(parents[1].dynamic_lc_color_state_id()) + .ok_or_else(|| integrity("closure right color state disappeared"))?, + ]; + let (evaluator_parent_ids, exchange_factor) = + closure.evaluator_parents(concrete_parent_ids); + let fermion_factor = fermion_ordering_factor( + &templates.input().current_states, + [ + parents[0].current_state_template_id(), + parents[1].current_state_template_id(), + ], + [ + parents[0].support_source_slots(), + parents[1].support_source_slots(), + ], + fermion_ordering, + )?; + for quantum in closure + .quantum_flows + .iter() + .filter(|quantum| closure_quantum_matches(quantum, parents)) + { + for witness in &closure.witnesses { + if witness.row.left_shape_string_id != parent_colors[0].output_color_shape_id() + || witness.row.right_shape_string_id + != parent_colors[1].output_color_shape_id() + { + continue; + } + let closed = witness + .witness + .closed_components(parent_colors[0], parent_colors[1])?; + let reflection_factor = if query_target_matches(&closed, query) { + ExactComplexRational::ONE + } else if enable_cyclic_trace_reflection { + let Some(factor) = reflected_query_closure_factor( + query, + &closed, + colors, + currents, + concrete_parent_ids, + )? + else { + continue; + }; + factor + } else { + continue; + }; + let key = PendingClosureKey { + closure_template_id: closure.row.id, + quantum_flow_template_id: quantum.row.map(|row| row.id), + parent_current_ids: evaluator_parent_ids, + color_witness_term_id: LCColorWitnessTermId::new( + closure.row.color_contraction_template_id, + witness.row.ordinal, + ), + }; + let factor = multiply_factors(&[ + closure.base_factor, + quantum.output_factor()?, + exchange_factor, + witness.witness.exact_factor(), + fermion_factor, + reflection_factor, + ])?; + let coefficients = closure.component_coefficients.clone(); + match retained.entry(key.clone()) { + std::collections::btree_map::Entry::Vacant(entry) => { + entry.insert(PendingClosure { + key, + factor, + component_coefficients: coefficients, + pairing_lineages: try_clone_pairing_lineages(&pairing_lineages)?, + }); + } + std::collections::btree_map::Entry::Occupied(mut entry) => { + if entry.get().component_coefficients != coefficients { + return Err(integrity( + "equal closure identities have different component coefficients", + )); + } + aggregate_factor(&mut entry.get_mut().factor, factor)?; + extend_pairing_lineages( + &mut entry.get_mut().pairing_lineages, + &pairing_lineages, + )?; + } + } + } + } + } + } + retained.retain(|_, closure| !closure.factor.is_zero()); + for closure in retained.values_mut() { + retain_canonical_pairing_lineage(&mut closure.pairing_lineages)?; + } + if retained.is_empty() { + // The process-global immutable grammar proved this closure domain + // complete before it entered the runtime cache. + return Ok(None); + } + Ok(Some(retained.into_values().collect())) +} + +pub(super) fn validate_prepared_closure_domain( + templates: &ValidatedRecurrenceTemplateInput, + catalog: &TemplateCatalog<'_>, + closures: &BTreeMap<(u32, u32), Vec>, +) -> RusticolResult<()> { + let expected = &templates.input().closures; + let observed_count = closures.values().try_fold(0usize, |count, rows| { + count + .checked_add(rows.len()) + .ok_or_else(|| invalid("prepared closure-domain row count exceeds usize")) + })?; + if observed_count != expected.len() { + return Err(integrity(format!( + "prepared closure domain has {observed_count} rows, expected {}", + expected.len() + ))); + } + + let mut observed = vec![false; expected.len()]; + for (state_pair, rows) in closures { + for closure in rows { + let expected_row = expected + .get(closure.row.id as usize) + .filter(|row| row.id == closure.row.id) + .ok_or_else(|| integrity("prepared closure domain contains an unknown row"))?; + if closure.row != *expected_row { + return Err(integrity( + "prepared closure row differs from its authenticated template", + )); + } + let expected_states: [u32; 2] = catalog + .u32_sequence(expected_row.input_state_sequence_id, "closure input states")? + .try_into() + .map_err(|_| integrity("authenticated closure input-state pair is not binary"))?; + if closure.input_states != expected_states + || *state_pair != canonical_state_pair(expected_states[0], expected_states[1]) + { + return Err(integrity( + "prepared closure parent-state pair differs from its authenticated template", + )); + } + let seen = observed + .get_mut(closure.row.id as usize) + .ok_or_else(|| integrity("prepared closure row ID exceeds its domain"))?; + if std::mem::replace(seen, true) { + return Err(integrity( + "prepared closure domain repeats an authenticated row", + )); + } + } + } + if observed.iter().any(|seen| !seen) { + return Err(integrity( + "prepared closure domain omits an authenticated row", + )); + } + Ok(()) +} + +pub(super) fn live_current_ids( + currents: &[PendingCurrent], + closures: &[PendingClosure], +) -> RusticolResult> { + let mut live = BTreeSet::new(); + let mut queue = VecDeque::new(); + for closure in closures { + for parent in closure.key.parent_current_ids { + if live.insert(parent) { + queue.push_back(parent); + } + } + } + while let Some(current_id) = queue.pop_front() { + let current = currents + .get(current_id as usize) + .ok_or_else(|| integrity("liveness queue references an absent current"))?; + for (contribution, factor) in ¤t.contributions { + if factor.is_zero() { + continue; + } + for parent in contribution.parent_current_ids { + if live.insert(parent) { + queue.push_back(parent); + } + } + } + } + Ok(live) +} + +#[cfg(test)] +mod tests { + use crate::recurrence::DynamicLCColorStateId; + use crate::recurrence::construct::CurrentReflectionProof; + use crate::recurrence::contact_orbit_owner::{ + ContactOrbitStepProof, ContactOrbitTestBinding, contact_orbit_application_for_test, + contact_orbit_test_template, final_contact_orbit_step_for_test, + partial_contact_orbit_step_for_test, prepared_contact_orbit_transition_for_test, + }; + use crate::recurrence::{ExactRational, LCColorEndpoint, LCColorPortBinding}; + + use super::*; + + #[test] + fn fermion_ordering_filter_uses_authenticated_source_color_role() { + assert!( + on_the_fly_source_requires_exterior_sign(true, OnTheFlyExternalColorRoleV1::Singlet,) + .unwrap() + ); + assert!( + !on_the_fly_source_requires_exterior_sign( + true, + OnTheFlyExternalColorRoleV1::Fundamental, + ) + .unwrap() + ); + assert!( + !on_the_fly_source_requires_exterior_sign( + true, + OnTheFlyExternalColorRoleV1::Antifundamental, + ) + .unwrap() + ); + assert!( + !on_the_fly_source_requires_exterior_sign(false, OnTheFlyExternalColorRoleV1::Adjoint,) + .unwrap() + ); + } + + #[test] + fn fermion_ordering_filter_rejects_unsupported_or_unauthenticated_roles() { + let adjoint = + on_the_fly_source_requires_exterior_sign(true, OnTheFlyExternalColorRoleV1::Adjoint) + .unwrap_err(); + assert!(adjoint.to_string().contains("adjoint fermion")); + let bosonic_endpoint = on_the_fly_source_requires_exterior_sign( + false, + OnTheFlyExternalColorRoleV1::Fundamental, + ) + .unwrap_err(); + assert!(bosonic_endpoint.to_string().contains("bosonic source")); + } + + fn contact_digest(byte: u8) -> SemanticDigest { + SemanticDigest::new([byte; 32]).expect("test digest must be nonzero") + } + + fn reflection_test_states() -> ( + DynamicLCColorStateInterner, + DynamicLCColorStateId, + DynamicLCColorStateId, + ) { + let canonical = DynamicLCColorState::new_port_wired( + 1, + vec![ + LCColorPortBinding::new(0, LCColorEndpoint::Back), + LCColorPortBinding::new(0, LCColorEndpoint::Front), + ], + vec![ + LCColorComponent::new(LCColorComponentKind::AdjointSegment, vec![1, 3, 2]).unwrap(), + ], + ) + .unwrap(); + let reversed = canonical.reversed().unwrap(); + let mut colors = DynamicLCColorStateInterner::default(); + let canonical_id = colors.intern(canonical).unwrap(); + let reversed_id = colors.intern(reversed).unwrap(); + (colors, canonical_id, reversed_id) + } + + fn proven_reflection( + colors: &DynamicLCColorStateInterner, + color_id: DynamicLCColorStateId, + phase: ExactComplexRational, + lineage: u8, + ) -> CurrentReflection { + let color = colors.get(color_id).unwrap(); + CurrentReflection::Proven( + CurrentReflectionProof::new( + phase, + [contact_digest(lineage)], + dynamic_color_identity_digest(color).unwrap(), + ) + .unwrap(), + ) + } + + fn reflection_current( + node_kind: RecurrenceNodeKind, + color_id: DynamicLCColorStateId, + support: &[u32], + reflection: CurrentReflection, + ) -> PendingCurrent { + PendingCurrent { + key: CurrentCoreKey::new( + contact_digest(230), + node_kind, + 0, + color_id, + support.to_vec(), + CanonicalMomentumLinearForm::new( + support + .iter() + .copied() + .map(|source_slot| MomentumTerm { + source_slot, + coefficient: 1, + }) + .collect(), + ) + .unwrap(), + CurrentHelicityIdentity::topology_replay( + 0, + support + .iter() + .copied() + .map(|source_slot| SourceStateAssignment::new(source_slot, 0)) + .collect(), + ) + .unwrap(), + vec![1], + 0, + vec![0], + if node_kind == RecurrenceNodeKind::Source { + CurrentSourceBinding::FixedTemplate(0) + } else { + CurrentSourceBinding::None + }, + None, + ) + .unwrap(), + source_factor: (node_kind == RecurrenceNodeKind::Source) + .then_some(ExactComplexRational::ONE), + contributions: BTreeMap::new(), + pairing_lineages: vec![PendingPairingLineage { + completed_pairs: Vec::new(), + unmatched_endpoint: None, + }], + stage: u32::try_from(support.len().saturating_sub(1)).unwrap(), + reflection, + reflection_certificate: None, + } + } + + #[test] + fn query_local_reflection_fold_is_insertion_invariant_and_fail_closed() { + let minus_one = ExactComplexRational::ONE.checked_neg().unwrap(); + let run = |reverse_insertion: bool, prove_both: bool| { + let (mut colors, canonical_id, reversed_id) = reflection_test_states(); + let mut currents = vec![ + reflection_current( + RecurrenceNodeKind::Current, + canonical_id, + &[1, 2, 3], + proven_reflection(&colors, canonical_id, minus_one, 201), + ), + reflection_current( + RecurrenceNodeKind::Current, + reversed_id, + &[1, 2, 3], + if prove_both { + proven_reflection(&colors, reversed_id, minus_one, 202) + } else { + let mut late_unproved = + proven_reflection(&colors, reversed_id, minus_one, 202); + late_unproved.include(None).unwrap(); + late_unproved + }, + ), + ]; + if reverse_insertion { + currents.reverse(); + } + let mut current_ids = currents + .iter() + .enumerate() + .map(|(id, current)| (current.key.clone(), id as u32)) + .collect::>(); + reconcile_on_the_fly_stage_reflections( + 0, + &mut colors, + &mut currents, + &mut current_ids, + 4, + ) + .unwrap(); + (colors, currents, current_ids) + }; + + let (_, folded, folded_ids) = run(false, true); + let (_, folded_reordered, folded_reordered_ids) = run(true, true); + assert_eq!(folded.len(), 1); + assert_eq!(folded_ids.len(), 1); + assert_eq!(folded[0].key, folded_reordered[0].key); + assert_eq!(folded_ids, folded_reordered_ids); + for current in [&folded[0], &folded_reordered[0]] { + let certificate = current.reflection_certificate.as_ref().unwrap(); + assert!(certificate.is_reciprocal_two_cycle()); + assert_eq!(certificate.source_permutation(), [0, 2, 1, 3]); + assert_eq!(certificate.canonical_phase(), minus_one); + } + + let (_, residual, residual_ids) = run(false, false); + assert_eq!(residual.len(), 2); + assert_eq!(residual_ids.len(), 2); + assert!( + residual + .iter() + .all(|current| current.reflection_certificate.is_none()) + ); + } + + #[test] + fn reflected_query_closure_uses_only_the_certified_exact_phase() { + let canonical_phase = + ExactComplexRational::new(ExactRational::new(2, 1).unwrap(), ExactRational::ZERO); + let reflected_phase = + ExactComplexRational::new(ExactRational::new(1, 2).unwrap(), ExactRational::ZERO); + let (mut colors, canonical_id, reversed_id) = reflection_test_states(); + let mut currents = vec![ + reflection_current( + RecurrenceNodeKind::Source, + canonical_id, + &[0], + CurrentReflection::Unavailable, + ), + reflection_current( + RecurrenceNodeKind::Current, + canonical_id, + &[1, 2, 3], + proven_reflection(&colors, canonical_id, canonical_phase, 203), + ), + reflection_current( + RecurrenceNodeKind::Current, + reversed_id, + &[1, 2, 3], + proven_reflection(&colors, reversed_id, reflected_phase, 204), + ), + ]; + let mut current_ids = currents + .iter() + .enumerate() + .map(|(id, current)| (current.key.clone(), id as u32)) + .collect::>(); + reconcile_on_the_fly_stage_reflections(1, &mut colors, &mut currents, &mut current_ids, 4) + .unwrap(); + assert_eq!(currents.len(), 2); + + let seed = adjoint_reflection_seed(); + let query = DecodedLcQueryV1::new( + &seed, + vec![0, 1, 2, 3], + &[0, 0, 0, 0], + OnTheFlyLcSelectorV1::single_trace(vec![0, 2, 3, 1]), + ) + .unwrap(); + let query = OnTheFlyClosureAnchorPolicyV1::certified_cyclic_minimum(0) + .canonicalize_query(&seed, query) + .unwrap(); + let closed = + vec![LCColorComponent::new(LCColorComponentKind::Trace, vec![0, 1, 3, 2]).unwrap()]; + assert!(!query_target_matches(&closed, &query)); + assert_eq!( + reflected_query_closure_factor(&query, &closed, &colors, ¤ts, [0, 1]).unwrap(), + Some(canonical_phase), + ); + + let certificate = currents[1].reflection_certificate.take(); + assert_eq!( + reflected_query_closure_factor(&query, &closed, &colors, ¤ts, [0, 1]).unwrap(), + None, + ); + currents[1].reflection_certificate = certificate; + currents[1].reflection = proven_reflection(&colors, canonical_id, canonical_phase, 205); + assert!( + reflected_query_closure_factor(&query, &closed, &colors, ¤ts, [0, 1]).is_err() + ); + } + + fn contact_current( + node_kind: RecurrenceNodeKind, + state: u32, + color: u32, + support: &[u32], + ) -> PendingCurrent { + contact_current_with_source_template( + node_kind, + state, + color, + support, + support.first().copied().unwrap_or_default(), + ) + } + + fn contact_current_with_source_template( + node_kind: RecurrenceNodeKind, + state: u32, + color: u32, + support: &[u32], + source_template_id: u32, + ) -> PendingCurrent { + let key = CurrentCoreKey::new( + contact_digest(230), + node_kind, + state, + DynamicLCColorStateId::from_interner(color), + support.to_vec(), + CanonicalMomentumLinearForm::new( + support + .iter() + .copied() + .map(|source_slot| MomentumTerm { + source_slot, + coefficient: 1, + }) + .collect(), + ) + .unwrap(), + CurrentHelicityIdentity::topology_replay( + 0, + support + .iter() + .copied() + .map(|source_slot| SourceStateAssignment::new(source_slot, 0)) + .collect(), + ) + .unwrap(), + vec![state as i32], + 0, + vec![0], + if node_kind == RecurrenceNodeKind::Source { + CurrentSourceBinding::FixedTemplate(source_template_id) + } else { + CurrentSourceBinding::None + }, + None, + ) + .unwrap(); + PendingCurrent { + key, + source_factor: (node_kind == RecurrenceNodeKind::Source) + .then_some(ExactComplexRational::ONE), + contributions: BTreeMap::new(), + pairing_lineages: vec![PendingPairingLineage { + completed_pairs: Vec::new(), + unmatched_endpoint: None, + }], + stage: u32::try_from(support.len().saturating_sub(1)).unwrap(), + reflection: CurrentReflection::Unavailable, + reflection_certificate: None, + } + } + + fn legacy_support_size_pairs(support_sizes: &[usize], target_size: usize) -> Vec<[usize; 2]> { + let mut pairs = Vec::new(); + for left_id in 0..support_sizes.len() { + if support_sizes[left_id] >= target_size { + continue; + } + for right_id in (left_id + 1)..support_sizes.len() { + if support_sizes[right_id] < target_size + && support_sizes[left_id] + support_sizes[right_id] == target_size + { + pairs.push([left_id, right_id]); + } + } + } + pairs + } + + fn legacy_anchor_filtered_pairs( + supports: &[Vec], + target_size: usize, + closure_anchor_slot: u32, + ) -> Vec<[usize; 2]> { + legacy_support_size_pairs( + &supports.iter().map(Vec::len).collect::>(), + target_size, + ) + .into_iter() + .filter(|[left, right]| { + !supports[*left].contains(&closure_anchor_slot) + && !supports[*right].contains(&closure_anchor_slot) + }) + .collect() + } + + #[test] + fn query_support_size_index_preserves_legacy_pair_order() { + let mut random_state = 0x6a09_e667_f3bc_c909_u64; + for source_count in 3..=12 { + for case_index in 0..64 { + let current_count = source_count + case_index % (source_count * 3); + let mut support_sizes = Vec::with_capacity(current_count); + for _ in 0..current_count { + random_state = random_state + .wrapping_mul(6_364_136_223_846_793_005) + .wrapping_add(1_442_695_040_888_963_407); + support_sizes.push( + 1 + usize::try_from(random_state % (source_count as u64 - 1)).unwrap(), + ); + } + + let mut index = QuerySupportSizeIndex { + support_size_by_current: Vec::new(), + composite_parent_eligible_by_current: Vec::new(), + current_ids_by_size: (0..source_count).map(|_| Vec::new()).collect(), + }; + for (current_id, support_size) in support_sizes.iter().copied().enumerate() { + index.append(current_id, support_size, true).unwrap(); + } + + for target_size in 2..source_count { + let mut indexed_pairs = Vec::new(); + index + .for_each_parent_pair(target_size, support_sizes.len(), |pair| { + indexed_pairs.push(pair); + Ok(()) + }) + .unwrap(); + assert_eq!( + indexed_pairs, + legacy_support_size_pairs(&support_sizes, target_size), + "source_count={source_count}, case_index={case_index}, target_size={target_size}" + ); + } + } + } + } + + #[test] + fn all_flow_queries_use_distinct_anchor_filters_without_reordering_pairs() { + // An all-flow family constructs each decoded query independently. + // Rebuild the same support domain for every possible query anchor to + // prove that no anchor eligibility leaks from one query to the next. + for source_count in 2_u32..=8 { + let mut supports = (1_u32..(1_u32 << source_count)) + .filter(|mask| mask.count_ones() < source_count) + .map(|mask| { + (0..source_count) + .filter(|slot| mask & (1 << slot) != 0) + .collect::>() + }) + .collect::>(); + supports.sort_by_key(|support| (support.len(), support.clone())); + + for closure_anchor_slot in 0..source_count { + let mut index = QuerySupportSizeIndex { + support_size_by_current: Vec::new(), + composite_parent_eligible_by_current: Vec::new(), + current_ids_by_size: (0..source_count as usize).map(|_| Vec::new()).collect(), + }; + for (current_id, support) in supports.iter().enumerate() { + index + .append( + current_id, + support.len(), + !support.contains(&closure_anchor_slot), + ) + .unwrap(); + } + + for target_size in 2..source_count as usize { + let mut filtered_pairs = Vec::new(); + index + .for_each_parent_pair(target_size, supports.len(), |pair| { + filtered_pairs.push(pair); + Ok(()) + }) + .unwrap(); + assert_eq!( + filtered_pairs, + legacy_anchor_filtered_pairs(&supports, target_size, closure_anchor_slot,), + "source_count={source_count}, closure_anchor_slot={closure_anchor_slot}, target_size={target_size}", + ); + } + } + } + } + + #[test] + fn two_source_anchor_is_retained_but_cannot_enter_composite_schedule() { + let currents = vec![ + contact_current(RecurrenceNodeKind::Source, 0, 0, &[0]), + contact_current(RecurrenceNodeKind::Source, 0, 0, &[1]), + ]; + let index = QuerySupportSizeIndex::new(¤ts, 2, Some(1)).unwrap(); + assert_eq!(index.support_size_by_current, [1, 1]); + assert_eq!(index.composite_parent_eligible_by_current, [true, false]); + let mut pairs = Vec::new(); + index + .for_each_parent_pair(2, currents.len(), |pair| { + pairs.push(pair); + Ok(()) + }) + .unwrap(); + assert!(pairs.is_empty()); + + let error = QuerySupportSizeIndex::new(¤ts, 2, Some(2)).unwrap_err(); + assert!( + error + .to_string() + .contains("outside the forward source domain") + ); + + let missing_anchor = vec![contact_current(RecurrenceNodeKind::Source, 0, 0, &[0])]; + let error = QuerySupportSizeIndex::new(&missing_anchor, 2, Some(1)).unwrap_err(); + assert!(error.to_string().contains("exactly one singleton")); + + let malformed_anchor = vec![ + contact_current(RecurrenceNodeKind::Source, 0, 0, &[0]), + contact_current(RecurrenceNodeKind::Current, 0, 0, &[0, 1]), + ]; + let error = QuerySupportSizeIndex::new(&malformed_anchor, 2, Some(1)).unwrap_err(); + assert!(error.to_string().contains("non-singleton closure-anchor")); + } + + #[test] + fn query_support_size_index_enforces_stage_prefix() { + let mut index = QuerySupportSizeIndex { + support_size_by_current: Vec::new(), + composite_parent_eligible_by_current: Vec::new(), + current_ids_by_size: (0..4).map(|_| Vec::new()).collect(), + }; + index.append(0, 1, true).unwrap(); + index.append(1, 1, true).unwrap(); + index.append(2, 1, true).unwrap(); + + let error = index + .for_each_parent_pair(2, 2, |_| Ok(())) + .expect_err("a size bucket extending past the stage prefix must be rejected"); + assert!( + error + .to_string() + .contains("differs from its current prefix") + ); + } + + fn contact_transition( + step: ContactOrbitStepProof, + input_state_template_ids: [u32; 2], + transition_digest: u8, + ) -> PreparedContactOrbitTransition { + contact_transition_with_witness( + step, + input_state_template_ids, + transition_digest, + LCColorWitnessTermId::new(4, 0), + ) + } + + fn contact_transition_with_witness( + step: ContactOrbitStepProof, + input_state_template_ids: [u32; 2], + transition_digest: u8, + color_witness_term_id: LCColorWitnessTermId, + ) -> PreparedContactOrbitTransition { + let mut application = contact_orbit_application_for_test(); + application.color_witness_term_id = color_witness_term_id; + prepared_contact_orbit_transition_for_test( + step, + input_state_template_ids, + contact_digest(transition_digest), + application, + ) + } + + fn add_contact_contribution( + currents: &mut [PendingCurrent], + destination: u32, + transition: u32, + parents: [u32; 2], + ) { + let parent_keys = parents.map(|id| ¤ts[id as usize].key); + let destination_key = ¤ts[destination as usize].key; + let key = ContributionKey::new( + transition, + parents.to_vec(), + parent_keys + .iter() + .map(|key| key.current_state_template_id()) + .collect(), + parent_keys + .iter() + .map(|key| key.momentum().clone()) + .collect(), + destination_key.current_state_template_id(), + 0, + LCColorWitnessTermId::new(4, 0), + contact_digest(2), + 8, + ) + .unwrap(); + assert!( + currents[destination as usize] + .contributions + .insert( + PendingContributionKey { + parent_current_ids: parents, + key, + }, + ExactComplexRational::ONE, + ) + .is_none() + ); + } + + fn contact_transition_ids(current: &PendingCurrent) -> Vec { + current + .contributions + .keys() + .map(|pending| pending.key.transition_template_id()) + .collect() + } + + fn pending_contact_counts(currents: &[PendingCurrent]) -> (usize, usize) { + ( + currents.len(), + currents + .iter() + .map(|current| current.contributions.len()) + .sum(), + ) + } + + fn scalar_contact_seed(source_count: u32) -> OnTheFlyProcessSeedV1 { + let state = OnTheFlySourceStateV1::new( + 0, + 0, + 0, + 0, + 0, + contact_digest(231), + contact_digest(232), + 1, + ExactComplexRational::ONE, + 0, + 0, + vec![1], + 0, + contact_digest(233), + OnTheFlySourceWavefunctionFamilyV1::Scalar, + OnTheFlySourceOrientationV1::SelfConjugate, + None, + ) + .unwrap(); + let anchors = (0_u32..source_count) + .map(|source_slot| { + OnTheFlySourceAnchorV1::new( + source_slot, + source_slot, + false, + OnTheFlyExternalColorRoleV1::Singlet, + false, + None, + vec![state.clone()], + ) + .unwrap() + }) + .collect(); + OnTheFlyProcessSeedV1::new( + contact_digest(234), + contact_digest(235), + contact_digest(230), + contact_digest(236), + contact_digest(237), + contact_digest(238), + "raw-amplitude-contact-test", + ExactComplexRational::ONE, + anchors, + (0_u32..source_count).collect(), + OnTheFlyCouplingOrderPolicyV1::Explicit, + vec![1], + vec![Some(0)], + Vec::new(), + ) + .unwrap() + } + + fn adjoint_reflection_seed() -> OnTheFlyProcessSeedV1 { + let state = OnTheFlySourceStateV1::new( + 0, + 0, + 0, + 0, + 0, + contact_digest(241), + contact_digest(242), + 1, + ExactComplexRational::ONE, + 0, + 0, + vec![1], + 0, + contact_digest(243), + OnTheFlySourceWavefunctionFamilyV1::Vector, + OnTheFlySourceOrientationV1::SelfConjugate, + None, + ) + .unwrap(); + let anchors = (0_u32..4) + .map(|source_slot| { + OnTheFlySourceAnchorV1::new( + source_slot, + source_slot, + source_slot < 2, + OnTheFlyExternalColorRoleV1::Adjoint, + false, + None, + vec![state.clone()], + ) + .unwrap() + }) + .collect(); + OnTheFlyProcessSeedV1::new( + contact_digest(244), + contact_digest(245), + contact_digest(230), + contact_digest(246), + contact_digest(247), + contact_digest(248), + "raw-amplitude-reflection-test", + ExactComplexRational::ONE, + anchors, + vec![0, 1, 2, 3], + OnTheFlyCouplingOrderPolicyV1::Explicit, + vec![1], + vec![Some(0)], + Vec::new(), + ) + .unwrap() + } + + fn production_contact_barrier_currents( + reverse_transition_order: bool, + include_ordinary_scalar_transition: bool, + closure_anchor_slot: Option, + ) -> Vec { + let mut template_input = contact_orbit_test_template(ContactOrbitTestBinding::One); + let mut intermediate_state = template_input.current_states[0]; + intermediate_state.id = 1; + let mut append_canonical_string = |value: &[u8]| { + let previous_string = template_input.string_ranges.last().unwrap(); + let previous_range = previous_string + .as_usize_range(template_input.string_bytes.len(), "test string") + .unwrap(); + assert!( + &template_input.string_bytes[previous_range] < value, + "the intermediate-state fixture must extend the canonical string catalog" + ); + let id = u32::try_from(template_input.string_ranges.len()).unwrap(); + template_input + .string_ranges + .push(crate::recurrence::CheckedTableRange::new( + u64::try_from(template_input.string_bytes.len()).unwrap(), + u64::try_from(value.len()).unwrap(), + )); + template_input.string_bytes.extend_from_slice(value); + id + }; + let intermediate_propagator_template_id = + append_canonical_string(b"zz-on-the-fly-intermediate-propagator"); + let intermediate_state_template_id = + append_canonical_string(b"zz-on-the-fly-intermediate-state"); + intermediate_state.template_string_id = intermediate_state_template_id; + let intermediate_digest_id = u32::try_from(template_input.digest_catalog.len()).unwrap(); + template_input + .digest_catalog + .push(crate::recurrence::template::DigestCatalogRow { + id: intermediate_digest_id, + value: [240; 32], + }); + intermediate_state.semantic_digest_id = intermediate_digest_id; + template_input.current_states.push(intermediate_state); + let intermediate_propagator_digest_id = + u32::try_from(template_input.digest_catalog.len()).unwrap(); + template_input + .digest_catalog + .push(crate::recurrence::template::DigestCatalogRow { + id: intermediate_propagator_digest_id, + value: [241; 32], + }); + let mut intermediate_propagator = template_input.propagators[0]; + intermediate_propagator.id = 1; + intermediate_propagator.template_string_id = intermediate_propagator_template_id; + intermediate_propagator.state_template_id = 1; + intermediate_propagator.applies_propagator = 0; + intermediate_propagator.evaluator_binding_id = MISSING_U32; + intermediate_propagator.numerator_expression_digest_id = MISSING_U32; + intermediate_propagator.denominator_expression_digest_id = MISSING_U32; + intermediate_propagator.semantic_digest_id = intermediate_propagator_digest_id; + template_input.propagators.push(intermediate_propagator); + template_input.catalog_header[0].current_state_count = 2; + template_input.catalog_header[0].propagator_count = 2; + let templates = template_input.validate().unwrap(); + let catalog = TemplateCatalog::new(templates.input()).unwrap(); + let prepared = prepared_transitions(&templates, &catalog).unwrap(); + let base = prepared.values().next().unwrap()[0].clone(); + let partial_steps = [ + partial_contact_orbit_step_for_test(0, 1, 2, 20, [0, 0, 0, 0]), + partial_contact_orbit_step_for_test(1, 0, 2, 30, [0, 0, 0, 0]), + partial_contact_orbit_step_for_test(0, 2, 1, 21, [0, 0, 0, 0]), + partial_contact_orbit_step_for_test(2, 0, 1, 31, [0, 0, 0, 0]), + partial_contact_orbit_step_for_test(0, 3, 1, 22, [0, 0, 0, 0]), + partial_contact_orbit_step_for_test(3, 0, 1, 32, [0, 0, 0, 0]), + ]; + let final_steps = [ + final_contact_orbit_step_for_test(&[0], &[1, 2], 3, 60, [0, 0, 0, 0]), + final_contact_orbit_step_for_test(&[1, 2], &[0], 3, 70, [0, 0, 0, 0]), + final_contact_orbit_step_for_test(&[0], &[1, 3], 2, 61, [0, 0, 0, 0]), + final_contact_orbit_step_for_test(&[1, 3], &[0], 2, 71, [0, 0, 0, 0]), + final_contact_orbit_step_for_test(&[0], &[2, 3], 1, 62, [0, 0, 0, 0]), + final_contact_orbit_step_for_test(&[2, 3], &[0], 1, 72, [0, 0, 0, 0]), + ]; + let transition_order = if reverse_transition_order { + (0_usize..partial_steps.len()).rev().collect::>() + } else { + (0_usize..partial_steps.len()).collect::>() + }; + let build_row = |index: usize, + id_offset: u32, + step: ContactOrbitStepProof, + input_states: [u32; 2], + result_state: u32| { + let id = id_offset + u32::try_from(index).unwrap(); + let digest_byte = 40 + u8::try_from(id).unwrap(); + let mut row = base.clone(); + row.row.id = id; + row.input_states = input_states; + row.row.result_state_template_id = result_state; + row.quantum.result_state_template_id = result_state; + row.local_orders = vec![0].into_boxed_slice(); + row.transition_semantic_digest = contact_digest(digest_byte); + row.contact_orbit = Some(contact_transition_with_witness( + step, + input_states, + digest_byte, + LCColorWitnessTermId::new( + row.row.color_contraction_template_id, + row.witnesses[0].row.ordinal, + ), + )); + row + }; + let mut partial_rows = transition_order + .iter() + .copied() + .map(|index| build_row(index, 0, partial_steps[index].clone(), [0, 0], 1)) + .collect::>(); + let final_rows = transition_order + .into_iter() + .map(|index| { + let input_states = if index % 2 == 0 { [0, 1] } else { [1, 0] }; + build_row(index, 6, final_steps[index].clone(), input_states, 1) + }) + .collect::>(); + if include_ordinary_scalar_transition { + let mut ordinary = base.clone(); + ordinary.row.id = 12; + ordinary.input_states = [0, 0]; + ordinary.row.result_state_template_id = 0; + ordinary.quantum.result_state_template_id = 0; + ordinary.local_orders = vec![0].into_boxed_slice(); + ordinary.transition_semantic_digest = contact_digest(120); + ordinary.contact_orbit = None; + partial_rows.push(ordinary); + } + let transitions = BTreeMap::from([((0, 0), partial_rows), ((0, 1), final_rows)]); + let propagators = propagator_by_state(&templates).unwrap(); + let seed = scalar_contact_seed(4); + let mut colors = DynamicLCColorStateInterner::default(); + let color_id = colors + .intern( + DynamicLCColorState::new( + base.witnesses[0].row.left_shape_string_id, + None, + Vec::new(), + ) + .unwrap(), + ) + .unwrap(); + assert_eq!(color_id, DynamicLCColorStateId::from_interner(0)); + let mut currents = (0_u32..4) + .map(|source_slot| { + contact_current_with_source_template( + RecurrenceNodeKind::Source, + 0, + 0, + &[source_slot], + 0, + ) + }) + .collect::>(); + let mut current_ids = currents + .iter() + .enumerate() + .map(|(id, current)| (current.key.clone(), u32::try_from(id).unwrap())) + .collect::>(); + let contact_orbits = PreparedContactOrbitIndex::new(&transitions).unwrap(); + let fermion_ordering = on_the_fly_fermion_ordering_context(&seed).unwrap(); + + build_forward_currents_with_anchor_exclusion( + &templates, + &transitions, + &TransitionReflectionIndex::default(), + &contact_orbits, + false, + &seed, + &fermion_ordering, + closure_anchor_slot, + seed.explicit_coupling_limits(), + &propagators, + &mut colors, + &mut currents, + &mut current_ids, + ) + .unwrap(); + + currents + } + + #[allow(clippy::type_complexity)] // Compact assertion fixture returned only inside tests. + fn production_contact_barrier_case( + reverse_transition_order: bool, + include_ordinary_scalar_transition: bool, + ) -> (Vec<(Vec, u32, Vec)>, (usize, usize), bool) { + let currents = production_contact_barrier_currents( + reverse_transition_order, + include_ordinary_scalar_transition, + None, + ); + + assert!( + currents[4..].iter().all(|current| { + let expected = if include_ordinary_scalar_transition + && current.stage == 2 + && current.key.current_state_template_id() == 1 + { + 6 + } else if current.stage == 1 { + 1 + } else { + 3 + }; + current.contributions.len() == expected + }), + "certified pair/final destinations retain one owner per physical assignment", + ); + assert!( + currents[4..].iter().all(|current| { + current.contributions.keys().all(|contribution| { + contribution.parent_current_ids.iter().all(|parent_id| { + let parent = ¤ts[*parent_id as usize]; + parent.source_factor.is_some() || !parent.contributions.is_empty() + }) + }) + }), + "a selected contact transition must not reference an empty parent", + ); + + let staged = currents[4..] + .iter() + .map(|current| { + ( + current.key.support_source_slots().to_vec(), + current.stage, + contact_transition_ids(current), + ) + }) + .collect(); + let retained_partial_is_consumed = currents[10..].iter().all(|current| { + current.contributions.keys().all(|contribution| { + let mut partial_parents = contribution + .parent_current_ids + .iter() + .copied() + .filter(|parent| (4..10).contains(&usize::try_from(*parent).unwrap())); + let Some(partial_parent) = partial_parents.next() else { + return false; + }; + partial_parents.next().is_none() + && currents[partial_parent as usize] + .key + .current_state_template_id() + == 1 + && contribution + .parent_current_ids + .iter() + .copied() + .any(|parent| { + parent != partial_parent + && currents[parent as usize].key.current_state_template_id() == 0 + }) + }) + }); + ( + staged, + pending_contact_counts(¤ts), + retained_partial_is_consumed, + ) + } + + type OrderedExactForwardSignature = Vec<( + Vec, + RecurrenceNodeKind, + u32, + Vec<(u32, [Vec; 2], ExactComplexRational)>, + )>; + + fn ordered_exact_forward_signature( + currents: &[PendingCurrent], + ) -> OrderedExactForwardSignature { + currents + .iter() + .map(|current| { + ( + current.key.support_source_slots().to_vec(), + current.key.node_kind(), + current.stage, + current + .contributions + .iter() + .map(|(pending, factor)| { + ( + pending.key.transition_template_id(), + pending.parent_current_ids.map(|parent_id| { + currents[parent_id as usize] + .key + .support_source_slots() + .to_vec() + }), + *factor, + ) + }) + .collect(), + ) + }) + .collect() + } + + fn commit_contact_plan( + currents: &mut [PendingCurrent], + contacts: &BTreeMap, + stage_current_start: usize, + ) -> RusticolResult<()> { + if let Some(plan) = plan_on_the_fly_contact_orbit_owners_with_resolver( + stage_current_start, + currents, + |transition_id| contacts.get(&transition_id), + )? { + plan.commit(currents); + } + Ok(()) + } + + fn contact_0000_case(final_stage: bool, reverse_insertion: bool) -> (Vec, (usize, usize)) { + let (mut currents, steps, parent_ids, destination_id) = if final_stage { + ( + vec![ + contact_current(RecurrenceNodeKind::Source, 10, 0, &[10]), + contact_current(RecurrenceNodeKind::Source, 10, 1, &[11]), + contact_current(RecurrenceNodeKind::Source, 10, 2, &[12]), + contact_current(RecurrenceNodeKind::Current, 11, 3, &[11, 12]), + contact_current(RecurrenceNodeKind::Current, 11, 4, &[10, 12]), + contact_current(RecurrenceNodeKind::Current, 11, 5, &[10, 11]), + contact_current(RecurrenceNodeKind::Current, 20, 6, &[10, 11, 12]), + ], + vec![ + final_contact_orbit_step_for_test(&[1, 2], &[3], 0, 60, [0, 0, 0, 0]), + final_contact_orbit_step_for_test(&[3], &[1, 2], 0, 70, [0, 0, 0, 0]), + final_contact_orbit_step_for_test(&[1, 2], &[3], 0, 61, [0, 0, 0, 0]), + final_contact_orbit_step_for_test(&[3], &[1, 2], 0, 71, [0, 0, 0, 0]), + final_contact_orbit_step_for_test(&[1, 2], &[3], 0, 62, [0, 0, 0, 0]), + final_contact_orbit_step_for_test(&[3], &[1, 2], 0, 72, [0, 0, 0, 0]), + ], + vec![[3, 0], [0, 3], [4, 1], [1, 4], [5, 2], [2, 5]], + 6_u32, + ) + } else { + ( + vec![ + contact_current(RecurrenceNodeKind::Source, 10, 0, &[10]), + contact_current(RecurrenceNodeKind::Source, 10, 1, &[11]), + contact_current(RecurrenceNodeKind::Current, 20, 2, &[10, 11]), + ], + vec![ + partial_contact_orbit_step_for_test(0, 1, 2, 20, [0, 0, 0, 0]), + partial_contact_orbit_step_for_test(1, 0, 2, 30, [0, 0, 0, 0]), + partial_contact_orbit_step_for_test(0, 2, 1, 21, [0, 0, 0, 0]), + partial_contact_orbit_step_for_test(2, 0, 1, 31, [0, 0, 0, 0]), + partial_contact_orbit_step_for_test(0, 3, 1, 22, [0, 0, 0, 0]), + partial_contact_orbit_step_for_test(3, 0, 1, 32, [0, 0, 0, 0]), + ], + vec![[0, 1], [1, 0], [0, 1], [1, 0], [0, 1], [1, 0]], + 2_u32, + ) + }; + let contacts = steps + .into_iter() + .enumerate() + .map(|(transition, step)| { + let transition = u32::try_from(transition).unwrap(); + let parents = parent_ids[transition as usize]; + ( + transition, + contact_transition( + step, + parents.map(|parent| { + currents[parent as usize].key.current_state_template_id() + }), + 40 + u8::try_from(transition).unwrap(), + ), + ) + }) + .collect::>(); + let order = if reverse_insertion { + (0_u32..6).rev().collect::>() + } else { + (0_u32..6).collect::>() + }; + for transition in order { + add_contact_contribution( + &mut currents, + destination_id, + transition, + parent_ids[transition as usize], + ); + } + commit_contact_plan(&mut currents, &contacts, destination_id as usize).unwrap(); + ( + contact_transition_ids(¤ts[destination_id as usize]), + pending_contact_counts(¤ts), + ) + } + + fn closure(lineages: Vec) -> PendingClosure { + PendingClosure { + key: PendingClosureKey { + closure_template_id: 0, + quantum_flow_template_id: None, + parent_current_ids: [0, 1], + color_witness_term_id: LCColorWitnessTermId::new(0, 0), + }, + factor: ExactComplexRational::ONE, + component_coefficients: vec![ExactComplexRational::ONE].into_boxed_slice(), + pairing_lineages: lineages, + } + } + + fn lineage(pair: [u32; 2]) -> PendingPairingLineage { + PendingPairingLineage { + completed_pairs: vec![pair], + unmatched_endpoint: None, + } + } + + #[test] + fn contact_0000_fan_in_keeps_exact_assignment_multiplicity_deterministically() { + let partial_forward = contact_0000_case(false, false); + let partial_reverse = contact_0000_case(false, true); + assert_eq!(partial_forward, (vec![0], (3, 1))); + assert_eq!(partial_reverse, partial_forward); + + let final_forward = contact_0000_case(true, false); + let final_reverse = contact_0000_case(true, true); + assert_eq!(final_reverse, final_forward); + assert_eq!(final_forward.0.len(), 3); + assert_eq!(final_forward.1, (7, 3)); + } + + #[test] + fn forward_sweep_barrier_prunes_complete_contact_fan_in_before_next_stage() { + let forward = production_contact_barrier_case(false, false); + let reverse = production_contact_barrier_case(true, false); + assert_eq!(reverse, forward); + let (staged, counts, retained_partial_is_consumed) = forward; + assert_eq!( + staged + .iter() + .map(|(support, stage, _)| (support.clone(), *stage)) + .collect::>(), + vec![ + (vec![0, 1], 1), + (vec![0, 2], 1), + (vec![0, 3], 1), + (vec![1, 2], 1), + (vec![1, 3], 1), + (vec![2, 3], 1), + (vec![0, 1, 2], 2), + (vec![0, 1, 3], 2), + (vec![0, 2, 3], 2), + (vec![1, 2, 3], 2), + ], + ); + assert!(staged.iter().all(|(_, stage, transition_ids)| { + transition_ids.len() == if *stage == 1 { 1 } else { 3 } + })); + assert_eq!(counts, (14, 18)); + assert!(retained_partial_is_consumed); + } + + #[test] + fn anchor_exclusion_preserves_order_exact_factors_and_contact_owners() { + let baseline = production_contact_barrier_currents(false, false, None); + let baseline_signature = ordered_exact_forward_signature(&baseline); + + for closure_anchor_slot in 0_u32..4 { + let pruned = + production_contact_barrier_currents(false, false, Some(closure_anchor_slot)); + assert_eq!( + pruned + .iter() + .filter(|current| { + current.key.node_kind() == RecurrenceNodeKind::Source + && current.key.support_source_slots() == [closure_anchor_slot] + }) + .count(), + 1, + "the singleton closure anchor must remain materialized", + ); + assert!(pruned.iter().all(|current| { + current.key.node_kind() == RecurrenceNodeKind::Source + || !current + .key + .support_source_slots() + .contains(&closure_anchor_slot) + })); + + let expected = baseline_signature + .iter() + .filter(|(support, node_kind, _, _)| { + *node_kind == RecurrenceNodeKind::Source + || !support.contains(&closure_anchor_slot) + }) + .cloned() + .collect::>(); + assert_eq!( + ordered_exact_forward_signature(&pruned), + expected, + "anchor filtering must preserve surviving current order, exact factors, ordered parent supports, and contact-orbit owners", + ); + + let anchor_id = pruned + .iter() + .position(|current| { + current.key.node_kind() == RecurrenceNodeKind::Source + && current.key.support_source_slots() == [closure_anchor_slot] + }) + .unwrap() as u32; + let complement = (0_u32..4) + .filter(|slot| *slot != closure_anchor_slot) + .collect::>(); + let complement_id = pruned + .iter() + .position(|current| current.key.support_source_slots() == complement) + .unwrap() as u32; + let closure = PendingClosure { + key: PendingClosureKey { + closure_template_id: 0, + quantum_flow_template_id: None, + parent_current_ids: [anchor_id, complement_id], + color_witness_term_id: LCColorWitnessTermId::new(0, 0), + }, + factor: ExactComplexRational::ONE, + component_coefficients: vec![ExactComplexRational::ONE].into_boxed_slice(), + pairing_lineages: vec![PendingPairingLineage { + completed_pairs: Vec::new(), + unmatched_endpoint: None, + }], + }; + let live = live_current_ids(&pruned, &[closure]).unwrap(); + assert!(live.contains(&anchor_id)); + assert!(live.iter().all(|current_id| { + let current = &pruned[*current_id as usize]; + current.key.node_kind() == RecurrenceNodeKind::Source + || !current + .key + .support_source_slots() + .contains(&closure_anchor_slot) + })); + } + } + + #[test] + fn anchor_exclusion_preserves_structural_zero_when_no_complement_can_be_built() { + let templates = contact_orbit_test_template(ContactOrbitTestBinding::None) + .validate() + .unwrap(); + let propagators = propagator_by_state(&templates).unwrap(); + let seed = scalar_contact_seed(3); + let query = DecodedLcQueryV1::new( + &seed, + vec![0, 1, 2], + &[0, 0, 0], + OnTheFlyLcSelectorV1::Singlet, + ) + .unwrap(); + let transitions = BTreeMap::new(); + let closures = BTreeMap::new(); + let contact_orbits = PreparedContactOrbitIndex::new(&transitions).unwrap(); + let fermion_ordering = on_the_fly_fermion_ordering_context(&seed).unwrap(); + let build = |closure_anchor_slot| { + let mut colors = DynamicLCColorStateInterner::default(); + let mut currents = (0_u32..3) + .map(|slot| { + contact_current_with_source_template( + RecurrenceNodeKind::Source, + 0, + 0, + &[slot], + 0, + ) + }) + .collect::>(); + let mut current_ids = currents + .iter() + .enumerate() + .map(|(id, current)| (current.key.clone(), id as u32)) + .collect::>(); + build_forward_currents_with_anchor_exclusion( + &templates, + &transitions, + &TransitionReflectionIndex::default(), + &contact_orbits, + false, + &seed, + &fermion_ordering, + closure_anchor_slot, + seed.explicit_coupling_limits(), + &propagators, + &mut colors, + &mut currents, + &mut current_ids, + ) + .unwrap(); + assert!( + build_selected_closures( + &templates, + &closures, + &seed, + &fermion_ordering, + &query, + false, + &colors, + ¤ts, + ) + .unwrap() + .is_none() + ); + currents + }; + let baseline = build(None); + let pruned = build(Some(query.closure_anchor_slot)); + assert_eq!( + ordered_exact_forward_signature(&pruned), + ordered_exact_forward_signature(&baseline), + ); + } + + #[test] + fn forward_sweep_accepts_contact_partial_in_its_certified_internal_parent_domain() { + let forward = production_contact_barrier_case(false, true); + let reverse = production_contact_barrier_case(true, true); + assert_eq!(reverse, forward); + let (staged, _, _) = forward; + + assert!( + staged + .iter() + .filter(|(_, stage, _)| *stage == 1) + .any(|(_, _, transition_ids)| { + transition_ids + .iter() + .any(|transition_id| *transition_id < 6) + }), + "one-plus-one source parents must retain a certified contact partial", + ); + assert!( + staged + .iter() + .filter(|(_, stage, _)| *stage == 1) + .any(|(_, _, transition_ids)| transition_ids.as_slice() == [12]), + "the ordinary scalar transition must construct a composite scalar parent", + ); + assert!( + staged + .iter() + .filter(|(_, stage, _)| *stage == 2) + .flat_map(|(_, _, transition_ids)| transition_ids) + .any(|transition_id| *transition_id < 6), + "a certified contact partial must accept a matching internal scalar parent", + ); + assert!( + staged + .iter() + .filter(|(_, stage, _)| *stage == 2) + .flat_map(|(_, _, transition_ids)| transition_ids) + .any(|transition_id| (6..12).contains(transition_id)), + "a one-plus-two certified contact final must remain available", + ); + } + + #[test] + fn contact_0012_partial_and_final_fan_in_each_keep_one_owner() { + let mut partial_currents = vec![ + contact_current(RecurrenceNodeKind::Source, 10, 0, &[10]), + contact_current(RecurrenceNodeKind::Source, 10, 1, &[11]), + contact_current(RecurrenceNodeKind::Current, 20, 2, &[10, 11]), + ]; + let partial_contacts = BTreeMap::from([ + ( + 10, + contact_transition( + partial_contact_orbit_step_for_test(0, 1, 2, 50, [0, 0, 1, 2]), + [10, 10], + 52, + ), + ), + ( + 11, + contact_transition( + partial_contact_orbit_step_for_test(1, 0, 2, 51, [0, 0, 1, 2]), + [10, 10], + 53, + ), + ), + ]); + add_contact_contribution(&mut partial_currents, 2, 11, [0, 1]); + add_contact_contribution(&mut partial_currents, 2, 10, [0, 1]); + commit_contact_plan(&mut partial_currents, &partial_contacts, 2).unwrap(); + assert_eq!(contact_transition_ids(&partial_currents[2]), [10]); + assert_eq!(pending_contact_counts(&partial_currents), (3, 1)); + + let mut final_currents = vec![ + contact_current(RecurrenceNodeKind::Current, 20, 0, &[10, 11]), + contact_current(RecurrenceNodeKind::Source, 30, 1, &[12]), + contact_current(RecurrenceNodeKind::Current, 40, 2, &[10, 11, 12]), + ]; + let final_contacts = BTreeMap::from([ + ( + 20, + contact_transition( + final_contact_orbit_step_for_test(&[0, 1], &[2], 3, 54, [0, 0, 1, 2]), + [20, 30], + 56, + ), + ), + ( + 21, + contact_transition( + final_contact_orbit_step_for_test(&[2], &[0, 1], 3, 55, [0, 0, 1, 2]), + [30, 20], + 57, + ), + ), + ]); + add_contact_contribution(&mut final_currents, 2, 20, [0, 1]); + add_contact_contribution(&mut final_currents, 2, 21, [1, 0]); + commit_contact_plan(&mut final_currents, &final_contacts, 2).unwrap(); + assert_eq!(contact_transition_ids(&final_currents[2]), [21]); + assert_eq!(pending_contact_counts(&final_currents), (3, 1)); + } + + #[test] + fn contact_owner_planning_rolls_back_conflict_decode_and_allocation_errors() { + let mut currents = vec![ + contact_current(RecurrenceNodeKind::Source, 10, 0, &[10]), + contact_current(RecurrenceNodeKind::Source, 10, 1, &[11]), + contact_current(RecurrenceNodeKind::Current, 20, 2, &[10, 11]), + ]; + let duplicate_step = partial_contact_orbit_step_for_test(0, 1, 2, 60, [0, 0, 0, 0]); + let contacts = BTreeMap::from([ + (30, contact_transition(duplicate_step.clone(), [10, 10], 61)), + (31, contact_transition(duplicate_step, [10, 10], 61)), + ]); + add_contact_contribution(&mut currents, 2, 30, [0, 1]); + add_contact_contribution(&mut currents, 2, 31, [0, 1]); + let before = currents[2].contributions.clone(); + assert!( + plan_on_the_fly_contact_orbit_owners_with_resolver(2, ¤ts, |transition_id| { + contacts.get(&transition_id) + }) + .unwrap_err() + .to_string() + .contains("conflicting exact rank") + ); + assert_eq!(currents[2].contributions, before); + + let mut malformed = currents; + let (pending, factor) = malformed[2] + .contributions + .first_key_value() + .map(|(pending, factor)| (pending.clone(), *factor)) + .unwrap(); + malformed[2].contributions.remove(&pending); + malformed[2].contributions.insert( + PendingContributionKey { + parent_current_ids: [u32::MAX, pending.parent_current_ids[1]], + key: pending.key, + }, + factor, + ); + let malformed_before = malformed[2].contributions.clone(); + assert!( + plan_on_the_fly_contact_orbit_owners_with_resolver(2, &malformed, |transition_id| { + contacts.get(&transition_id) + },) + .unwrap_err() + .to_string() + .contains("left parent is absent") + ); + assert_eq!(malformed[2].contributions, malformed_before); + + let mut candidate_reservation = Vec::::new(); + assert!( + reserve_on_the_fly_contact_candidates(&mut candidate_reservation, usize::MAX) + .unwrap_err() + .to_string() + .contains("candidate allocation failed") + ); + assert!(candidate_reservation.is_empty()); + let mut location_reservation = Vec::::new(); + assert!( + reserve_on_the_fly_contact_locations(&mut location_reservation, usize::MAX) + .unwrap_err() + .to_string() + .contains("transition-index allocation failed") + ); + assert!(location_reservation.is_empty()); + } + + #[test] + fn contact_index_and_uncertified_controls_use_production_lookup_and_fast_path() { + let none = contact_orbit_test_template(ContactOrbitTestBinding::None) + .validate() + .unwrap(); + let none_catalog = TemplateCatalog::new(none.input()).unwrap(); + let none_transitions = prepared_transitions(&none, &none_catalog).unwrap(); + assert!( + PreparedContactOrbitIndex::new(&none_transitions) + .unwrap() + .is_empty() + ); + + let one = contact_orbit_test_template(ContactOrbitTestBinding::One) + .validate() + .unwrap(); + let one_catalog = TemplateCatalog::new(one.input()).unwrap(); + let mut one_transitions = prepared_transitions(&one, &one_catalog).unwrap(); + let mut ordinary = one_transitions.values().flatten().next().unwrap().clone(); + ordinary.row.id = u32::MAX - 1; + ordinary.contact_orbit = None; + assert!( + one_transitions + .values() + .flatten() + .all(|prepared| prepared.row.id != ordinary.row.id) + ); + one_transitions.values_mut().next().unwrap().push(ordinary); + let index = PreparedContactOrbitIndex::new(&one_transitions).unwrap(); + assert!(!index.is_empty()); + assert_eq!(index.len(), 1); + for prepared in one_transitions.values().flatten() { + assert_eq!( + index.get(&one_transitions, prepared.row.id), + prepared.contact_orbit.as_ref(), + ); + } + assert!(index.get(&one_transitions, u32::MAX).is_none()); + + let mut currents = vec![ + contact_current(RecurrenceNodeKind::Source, 10, 0, &[10]), + contact_current(RecurrenceNodeKind::Source, 11, 1, &[11]), + contact_current(RecurrenceNodeKind::Current, 20, 2, &[10, 11]), + ]; + // Distinct uncertified rows represent ordinary V3, vector, fermion, + // and QCD controls. No model-level class is inspected by this path. + for transition in 70..74 { + add_contact_contribution(&mut currents, 2, transition, [0, 1]); + } + let before = currents[2].contributions.clone(); + assert!( + plan_on_the_fly_contact_orbit_owners( + 2, + &BTreeMap::new(), + &PreparedContactOrbitIndex::default(), + ¤ts + ) + .unwrap() + .is_none() + ); + assert_eq!(currents[2].contributions, before); + assert_eq!(pending_contact_counts(¤ts), (3, 4)); + } + + #[test] + fn pairing_lineage_capacity_is_checked_without_an_arbitrary_limit() { + assert_eq!( + checked_pairing_capacity(1 << 20, 1 << 20, 1).unwrap(), + 2_097_153 + ); + assert!(checked_pairing_capacity(usize::MAX, 1, 0).is_err()); + } + + #[test] + fn large_pairing_lineage_clone_is_fallible_and_exact() { + let pair_count = 16_384usize; + let mut completed_pairs = Vec::new(); + completed_pairs.try_reserve_exact(pair_count).unwrap(); + for index in 0..pair_count { + completed_pairs.push([index as u32, (index + pair_count) as u32]); + } + let lineage = PendingPairingLineage { + completed_pairs, + unmatched_endpoint: None, + }; + let copied = try_clone_pairing_lineage(&lineage).unwrap(); + assert_eq!(copied, lineage); + } + + #[test] + fn projected_pairing_owners_are_exactly_one_per_closure_not_global() { + assert!( + projected_pairing_lineages(&[closure(vec![lineage([0, 1]), lineage([0, 3])])]).is_err() + ); + let closures = [ + closure(vec![lineage([0, 1])]), + closure(vec![lineage([0, 3])]), + ]; + let owners = projected_pairing_lineages(&closures).unwrap(); + assert_eq!(owners, [&lineage([0, 1]), &lineage([0, 3])]); + assert!(projected_pairing_lineages(&[]).is_err()); + } + + #[test] + fn multi_realized_closure_keeps_one_semantic_owner_without_rescaling() { + let mut pending = closure(vec![ + lineage([4, 7]), + lineage([0, 3]), + lineage([2, 5]), + lineage([0, 3]), + ]); + pending.factor = ExactComplexRational::ONE.checked_neg().unwrap(); + let expected_factor = pending.factor; + + retain_canonical_pairing_lineage(&mut pending.pairing_lineages).unwrap(); + + assert_eq!(pending.pairing_lineages, vec![lineage([0, 3])]); + assert_eq!(pending.factor, expected_factor); + } +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/recurrence/on_the_fly/templates.rs b/pyamplicol/rust/crates/rusticol-core/src/recurrence/on_the_fly/templates.rs new file mode 100644 index 0000000000..24a5cf4c46 --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/recurrence/on_the_fly/templates.rs @@ -0,0 +1,1050 @@ +// SPDX-License-Identifier: 0BSD + +use super::*; +use crate::recurrence::construct::TransitionReflectionIndex; +use crate::recurrence::fermion_ordering::FermionOrderingContext; +use crate::recurrence::template::{ + CurrentOrientation, EvaluatorCallableKind, OutputFactorSource, ParticleStatistics, +}; + +/// Immutable grammar prepared once for one loaded on-the-fly runtime. +/// Query families borrow these exact rows and query-invariant contexts; they +/// never rebuild transition, closure, contact-orbit, propagator, or fermion +/// ordering state per selected query. +#[derive(Debug)] +pub(crate) struct PreparedOnTheFlyGrammarV1 { + pub(super) transitions: BTreeMap<(u32, u32), Vec>, + pub(super) transition_reflections: TransitionReflectionIndex, + pub(super) contact_orbits: PreparedContactOrbitIndex, + pub(super) closures: BTreeMap<(u32, u32), Vec>, + pub(super) propagators: BTreeMap>, + pub(super) sources: BTreeMap<(u32, u32), PreparedOnTheFlySourceContractV1>, + pub(super) propagator_executors: BTreeMap, + pub(super) fermion_ordering: FermionOrderingContext, + pub(super) closure_anchor_policy: OnTheFlyClosureAnchorPolicyV1, +} + +impl PreparedOnTheFlyGrammarV1 { + pub(crate) fn canonicalize_query( + &self, + seed: &OnTheFlyProcessSeedV1, + query: DecodedLcQueryV1, + ) -> RusticolResult { + self.closure_anchor_policy.canonicalize_query(seed, query) + } + + pub(crate) fn authenticate_canonical_query( + &self, + seed: &OnTheFlyProcessSeedV1, + query: &DecodedLcQueryV1, + ) -> RusticolResult<()> { + self.closure_anchor_policy + .authenticate_canonical_query(seed, query) + } + + pub(super) fn closure_anchor_for_components( + &self, + seed: &OnTheFlyProcessSeedV1, + components: &[LCColorComponent], + ) -> Option { + self.closure_anchor_policy + .closure_anchor_for_components(seed, components) + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(super) struct PreparedOnTheFlyExecutorContractV1 { + pub(super) operation_id: u32, + pub(super) operation_semantic_digest: SemanticDigest, + pub(super) evaluator_binding_id: u32, + pub(super) evaluator_binding_semantic_digest: SemanticDigest, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(super) struct PreparedOnTheFlySourceContractV1 { + pub(super) current_state: CurrentStateRow, + pub(super) color_seed: LCColorSourceSeed, + pub(super) executor: PreparedOnTheFlyExecutorContractV1, +} + +pub(super) fn prepare_on_the_fly_grammar_v1( + templates: &ValidatedRecurrenceTemplateInput, + catalog: &TemplateCatalog<'_>, + seed: &OnTheFlyProcessSeedV1, +) -> RusticolResult { + let transitions = prepared_transitions(templates, catalog)?; + let transition_reflections = TransitionReflectionIndex::new(templates.input(), catalog)?; + let contact_orbits = PreparedContactOrbitIndex::new(&transitions)?; + let closures = prepared_closures(templates, catalog)?; + // Structural-zero queries may trust this immutable domain only because it + // is proved complete once, before the grammar enters the runtime cache. + validate_prepared_closure_domain(templates, catalog, &closures)?; + let propagators = propagator_by_state(templates)?; + let fermion_ordering = on_the_fly_fermion_ordering_context(seed)?; + let closure_anchor_policy = + certified_cyclic_trace_closure_anchor_policy(templates.input(), catalog, seed)?; + Ok(PreparedOnTheFlyGrammarV1 { + transitions, + transition_reflections, + contact_orbits, + closures, + sources: prepared_source_contracts(templates, catalog, seed)?, + propagator_executors: prepared_propagator_executors(templates, catalog, &propagators)?, + propagators, + fermion_ordering, + closure_anchor_policy, + }) +} + +/// Infer the v4 cyclic-anchor capability from the compact process seed and +/// the already-authenticated template catalog. +/// +/// This deliberately mirrors the generation-side v4 admission proof. A +/// false predicate retains the established selector-endpoint policy; malformed +/// catalog references remain hard errors rather than silently widening the +/// optimization domain. +fn certified_cyclic_trace_closure_anchor_policy( + input: &crate::recurrence::template::OwnedRecurrenceTemplateInput, + catalog: &TemplateCatalog<'_>, + seed: &OnTheFlyProcessSeedV1, +) -> RusticolResult { + let fallback = || Ok(OnTheFlyClosureAnchorPolicyV1::selector_endpoint()); + if !seed.pairing_classes.is_empty() + || seed.source_anchors.iter().any(|anchor| { + anchor.is_fermionic || anchor.color_role != OnTheFlyExternalColorRoleV1::Adjoint + }) + { + return fallback(); + } + let state_template_ids = seed + .source_anchors + .iter() + .flat_map(|anchor| anchor.states.iter()) + .map(|state| state.current_state_template_id) + .collect::>(); + if state_template_ids.len() != 1 { + return fallback(); + } + let state_template_id = *state_template_ids + .iter() + .next() + .expect("tested one cyclic-anchor state template"); + let state = input + .current_states + .get(state_template_id as usize) + .copied() + .ok_or_else(|| integrity("cyclic-anchor current-state template is absent"))?; + if state.id != state_template_id + || CurrentOrientation::try_from(state.orientation)? != CurrentOrientation::SelfConjugate + || ParticleStatistics::try_from(state.statistics)? != ParticleStatistics::Boson + || state.particle_id != state.anti_particle_id + || state.color_representation != 8 + || catalog.string( + state.lc_color_shape_string_id, + "cyclic-anchor current-state color shape", + )? != "adjoint-segment" + || state.auxiliary_kind_string_id != MISSING_U32 + { + return fallback(); + } + + let mut has_direct_closure = false; + for closure in input.closures.iter().copied() { + let coupling_order_set = input + .coupling_order_ranges + .get(closure.coupling_order_set_id as usize) + .ok_or_else(|| integrity("cyclic-anchor coupling-order set is absent"))?; + if catalog.u32_sequence( + closure.input_state_sequence_id, + "cyclic-anchor closure input states", + )? != [state_template_id, state_template_id] + || closure.result_state_template_id != MISSING_U32 + || !catalog + .u32_sequence( + closure.coupling_parameter_sequence_id, + "cyclic-anchor closure coupling parameters", + )? + .is_empty() + || coupling_order_set.range.count != 0 + || !catalog + .u32_sequence( + closure.eligible_quantum_flow_sequence_id, + "cyclic-anchor closure quantum flows", + )? + .is_empty() + || OutputFactorSource::try_from(closure.output_factor_source)? + != OutputFactorSource::None + || catalog.string( + closure.equivalence_class_string_id, + "cyclic-anchor closure equivalence class", + )? != "direct-contraction" + { + continue; + } + let binding = input + .evaluator_bindings + .get(closure.evaluator_binding_id as usize) + .copied() + .ok_or_else(|| integrity("cyclic-anchor closure evaluator is absent"))?; + if binding.id != closure.evaluator_binding_id + || EvaluatorCallableKind::try_from(binding.callable_kind)? + != EvaluatorCallableKind::RusticolTemplate + || EvaluatorContractKind::try_from(binding.contract_kind)? + != EvaluatorContractKind::Closure + || catalog.u32_sequence( + binding.input_state_sequence_id, + "cyclic-anchor evaluator input states", + )? != [state_template_id, state_template_id] + || binding.output_state_template_id != MISSING_U32 + { + continue; + } + let color = input + .color_contractions + .get(closure.color_contraction_template_id as usize) + .copied() + .ok_or_else(|| integrity("cyclic-anchor color contraction is absent"))?; + if color.id == closure.color_contraction_template_id + && catalog.i32_sequence( + color.input_representation_sequence_id, + "cyclic-anchor color input representations", + )? == [8, 8] + && color.has_output_representation == 0 + { + has_direct_closure = true; + break; + } + } + if !has_direct_closure { + return fallback(); + } + let minimum = seed + .source_anchors + .iter() + .map(|anchor| anchor.source_slot) + .min() + .ok_or_else(|| integrity("cyclic-anchor source domain is empty"))?; + Ok(OnTheFlyClosureAnchorPolicyV1::certified_cyclic_minimum( + minimum, + )) +} + +#[derive(Clone, Debug)] +pub(super) struct PreparedFlavourFlow { + operation: PreparedFlavourFlowOperation, + result_particle: i32, + static_result: Box<[i32]>, +} + +#[derive(Clone, Copy, Debug)] +enum PreparedFlavourFlowOperation { + Constant, + AppendLeft, + AppendRight, + ConcatLeftRight, +} + +impl PreparedFlavourFlow { + fn new(quantum: QuantumFlowRow, catalog: &TemplateCatalog<'_>) -> RusticolResult { + let operation = match catalog.string( + quantum.flavour_flow_operation_string_id, + "quantum-flow flavour operation", + )? { + "constant-result" => PreparedFlavourFlowOperation::Constant, + "append-left-result" => PreparedFlavourFlowOperation::AppendLeft, + "append-right-result" => PreparedFlavourFlowOperation::AppendRight, + "concat-left-right-result" => PreparedFlavourFlowOperation::ConcatLeftRight, + value => { + return Err(invalid(format!( + "unsupported quantum-flow flavour operation {value:?}" + ))); + } + }; + let static_result = catalog + .flavour_flow(quantum.result_flavour_flow_id, "quantum result flavour")? + .to_vec(); + let result_particle = *static_result + .last() + .ok_or_else(|| invalid("quantum result flavour ancestry is empty"))?; + Ok(Self { + operation, + result_particle, + static_result: static_result.into_boxed_slice(), + }) + } + + pub(super) fn apply(&self, parents: [&CurrentCoreKey; 2]) -> Vec { + self.apply_flows(parents[0].flavour_flow(), parents[1].flavour_flow()) + } + + pub(super) fn apply_flows(&self, left: &[i32], right: &[i32]) -> Vec { + let append = |parent: &[i32], result_particle| { + let mut result = parent.to_vec(); + if result.last().copied() != Some(result_particle) { + result.push(result_particle); + } + result + }; + match self.operation { + PreparedFlavourFlowOperation::Constant => self.static_result.to_vec(), + PreparedFlavourFlowOperation::AppendLeft => append(left, self.result_particle), + PreparedFlavourFlowOperation::AppendRight => append(right, self.result_particle), + PreparedFlavourFlowOperation::ConcatLeftRight => { + let mut result = left.to_vec(); + result.extend_from_slice(right); + result.push(self.result_particle); + result + } + } + } +} + +#[derive(Clone, Debug)] +pub(super) struct PreparedWitness { + pub(super) row: LCColorTransitionWitnessRow, + pub(super) witness: crate::recurrence::LCColorTransitionWitness, +} + +#[derive(Clone, Debug)] +pub(super) struct PreparedTransition { + pub(super) row: TransitionRow, + pub(super) input_states: [u32; 2], + pub(super) quantum: QuantumFlowRow, + pub(super) input_spins: [i32; 2], + pub(super) local_orders: Box<[u32]>, + canonical_input_order: [u32; 2], + input_exchange_factor: Option, + pub(super) base_factor: ExactComplexRational, + #[cfg(feature = "on-the-fly-test-support")] + pub(super) transition_factor: ExactComplexRational, + #[cfg(feature = "on-the-fly-test-support")] + pub(super) contraction_factor: ExactComplexRational, + coupling_authenticated: bool, + binding_coupling: ExactComplexRational, + output_factor_source: u8, + pub(super) flavour: PreparedFlavourFlow, + pub(super) quantum_semantic_digest: SemanticDigest, + pub(super) transition_semantic_digest: SemanticDigest, + pub(super) evaluator_binding_digest: SemanticDigest, + pub(super) contact_orbit: Option, + pub(super) witnesses: Box<[PreparedWitness]>, +} + +impl PreparedTransition { + fn new( + row: TransitionRow, + templates: &ValidatedRecurrenceTemplateInput, + catalog: &TemplateCatalog<'_>, + ) -> RusticolResult { + let input = templates.input(); + let input_states: [u32; 2] = catalog + .u32_sequence(row.input_state_sequence_id, "transition input states")? + .try_into() + .map_err(|_| invalid("on-the-fly transition must be binary"))?; + let quantum = *input + .quantum_flows + .get(row.quantum_flow_template_id as usize) + .ok_or_else(|| invalid("transition quantum flow is absent"))?; + let input_spins: [i32; 2] = catalog + .i32_sequence(quantum.input_spin_sequence_id, "quantum input spins")? + .try_into() + .map_err(|_| invalid("on-the-fly quantum flow must be binary"))?; + let quantum_states = + catalog.u32_sequence(quantum.input_state_sequence_id, "quantum input states")?; + if quantum_states != input_states { + return Err(invalid("transition and quantum-flow input states disagree")); + } + let canonical_input_order = match catalog.u32_sequence( + row.canonical_input_order_sequence_id, + "transition canonical input order", + )? { + [0, 1] => [0, 1], + [1, 0] => [1, 0], + _ => return Err(invalid("transition canonical input order is not binary")), + }; + let input_exchange_factor = (row.input_exchange_factor_id != MISSING_U32) + .then(|| catalog.factor(row.input_exchange_factor_id, "transition input exchange")) + .transpose()?; + let contraction = input + .color_contractions + .get(row.color_contraction_template_id as usize) + .ok_or_else(|| invalid("transition color contraction is absent"))?; + let quantum_coupling = + catalog.factor(quantum.exact_coupling_factor_id, "quantum coupling")?; + let binding_coupling = catalog.factor( + row.binding_coupling_factor_id, + "transition binding coupling", + )?; + let transition_exact_factor = catalog.factor(row.exact_factor_id, "transition exact")?; + let contraction_exact_factor = catalog.factor( + contraction.exact_coefficient_factor_id, + "transition color contraction", + )?; + let base_factor = multiply_factors(&[transition_exact_factor, contraction_exact_factor])?; + let binding = input + .evaluator_bindings + .get(row.evaluator_binding_id as usize) + .ok_or_else(|| invalid("transition evaluator binding is absent"))?; + if binding.id != row.evaluator_binding_id + || EvaluatorContractKind::try_from(binding.contract_kind)? + != EvaluatorContractKind::Vertex + { + return Err(invalid("transition evaluator binding has the wrong role")); + } + let local_orders = catalog + .coupling_orders(row.coupling_order_set_id)? + .into_boxed_slice(); + let quantum_semantic_digest = + catalog.digest(quantum.semantic_digest_id, "quantum semantic")?; + let witness_rows = catalog.witness_rows(row.color_contraction_template_id)?; + let contact_orbit = prepare_contact_orbit_transition( + input, + catalog, + row, + quantum_semantic_digest, + &local_orders, + binding_coupling, + transition_exact_factor, + contraction_exact_factor, + input_exchange_factor, + witness_rows, + )?; + let witnesses = witness_rows + .iter() + .copied() + .map(|witness_row| { + Ok(PreparedWitness { + row: witness_row, + witness: catalog.witness(witness_row)?, + }) + }) + .collect::>>()?; + Ok(Self { + row, + input_states, + quantum, + input_spins, + local_orders, + canonical_input_order, + input_exchange_factor, + base_factor, + #[cfg(feature = "on-the-fly-test-support")] + transition_factor: transition_exact_factor, + #[cfg(feature = "on-the-fly-test-support")] + contraction_factor: contraction_exact_factor, + coupling_authenticated: quantum_coupling == binding_coupling, + binding_coupling, + output_factor_source: row.output_factor_source, + flavour: PreparedFlavourFlow::new(quantum, catalog)?, + quantum_semantic_digest, + transition_semantic_digest: catalog + .digest(row.semantic_digest_id, "transition semantic")?, + evaluator_binding_digest: catalog + .digest(binding.semantic_digest_id, "transition evaluator binding")?, + contact_orbit, + witnesses: witnesses.into_boxed_slice(), + }) + } + + pub(super) fn parent_ids( + &self, + left_state: u32, + right_state: u32, + left_id: u32, + right_id: u32, + ) -> Option<[u32; 2]> { + if self.input_states == [left_state, right_state] { + Some([left_id, right_id]) + } else if left_state != right_state && self.input_states == [right_state, left_state] { + Some([right_id, left_id]) + } else { + None + } + } + + pub(super) fn evaluator_parents(&self, concrete: [u32; 2]) -> ([u32; 2], ExactComplexRational) { + let mut ordered = match self.canonical_input_order { + [0, 1] => concrete, + [1, 0] => [concrete[1], concrete[0]], + _ => unreachable!("validated transition input order"), + }; + let mut factor = ExactComplexRational::ONE; + if let Some(exchange) = self.input_exchange_factor + && ordered[1] < ordered[0] + { + ordered.swap(0, 1); + factor = exchange; + } + (ordered, factor) + } + + pub(super) fn output_factor(&self) -> RusticolResult { + if !self.coupling_authenticated { + return Err(invalid( + "transition binding coupling does not match its quantum-flow coupling witness", + )); + } + output_factor_from_binding( + self.binding_coupling, + self.output_factor_source, + "on-the-fly transition", + ) + } +} + +#[derive(Clone, Debug)] +pub(super) struct PreparedClosureQuantum { + pub(super) row: Option, + pub(super) input_states: Option<[u32; 2]>, + pub(super) input_spins: Option<[i32; 2]>, + coupling_authenticated: bool, + binding_coupling: ExactComplexRational, + output_factor_source: u8, +} + +impl PreparedClosureQuantum { + pub(super) fn output_factor(&self) -> RusticolResult { + if !self.coupling_authenticated { + return Err(invalid( + "closure binding coupling does not match its quantum-flow coupling witness", + )); + } + output_factor_from_binding( + self.binding_coupling, + self.output_factor_source, + "on-the-fly closure quantum flow", + ) + } +} + +#[derive(Clone, Debug)] +pub(super) struct PreparedClosure { + pub(super) row: ClosureRow, + pub(super) input_states: [u32; 2], + pub(super) local_orders: Box<[u32]>, + pub(super) component_coefficients: Box<[ExactComplexRational]>, + canonical_input_order: [u32; 2], + input_exchange_factor: Option, + pub(super) base_factor: ExactComplexRational, + pub(super) closure_semantic_digest: SemanticDigest, + pub(super) evaluator_binding_digest: SemanticDigest, + pub(super) quantum_flows: Box<[PreparedClosureQuantum]>, + pub(super) witnesses: Box<[PreparedWitness]>, +} + +impl PreparedClosure { + fn new( + row: ClosureRow, + templates: &ValidatedRecurrenceTemplateInput, + catalog: &TemplateCatalog<'_>, + ) -> RusticolResult { + let input = templates.input(); + let input_states: [u32; 2] = catalog + .u32_sequence(row.input_state_sequence_id, "closure input states")? + .try_into() + .map_err(|_| invalid("on-the-fly closure must be binary"))?; + let local_orders = catalog + .coupling_orders(row.coupling_order_set_id)? + .into_boxed_slice(); + let component_coefficients = catalog + .u32_sequence( + row.component_coefficient_sequence_id, + "closure component coefficients", + )? + .iter() + .map(|factor_id| catalog.factor(*factor_id, "closure component coefficient")) + .collect::>>()? + .into_boxed_slice(); + let canonical_input_order = match catalog.u32_sequence( + row.canonical_input_order_sequence_id, + "closure canonical input order", + )? { + [0, 1] => [0, 1], + [1, 0] => [1, 0], + _ => return Err(invalid("closure canonical input order is not binary")), + }; + let input_exchange_factor = (row.input_exchange_factor_id != MISSING_U32) + .then(|| catalog.factor(row.input_exchange_factor_id, "closure input exchange")) + .transpose()?; + let contraction = input + .color_contractions + .get(row.color_contraction_template_id as usize) + .ok_or_else(|| invalid("closure color contraction is absent"))?; + let binding_coupling = + catalog.factor(row.binding_coupling_factor_id, "closure binding coupling")?; + let base_factor = multiply_factors(&[ + catalog.factor(row.exact_factor_id, "closure exact")?, + catalog.factor( + contraction.exact_coefficient_factor_id, + "closure color contraction", + )?, + ])?; + let binding = input + .evaluator_bindings + .get(row.evaluator_binding_id as usize) + .ok_or_else(|| invalid("closure evaluator binding is absent"))?; + if binding.id != row.evaluator_binding_id + || EvaluatorContractKind::try_from(binding.contract_kind)? + != EvaluatorContractKind::Closure + { + return Err(invalid("closure evaluator binding has the wrong role")); + } + let eligible = catalog.u32_sequence( + row.eligible_quantum_flow_sequence_id, + "closure eligible quantum flows", + )?; + let quantum_flows = if eligible.is_empty() { + vec![PreparedClosureQuantum { + row: None, + input_states: None, + input_spins: None, + coupling_authenticated: true, + binding_coupling, + output_factor_source: row.output_factor_source, + }] + } else { + eligible + .iter() + .copied() + .map(|id| { + let quantum = *input + .quantum_flows + .get(id as usize) + .ok_or_else(|| invalid("closure quantum flow is absent"))?; + let states: [u32; 2] = catalog + .u32_sequence(quantum.input_state_sequence_id, "closure quantum states")? + .try_into() + .map_err(|_| invalid("closure quantum flow must be binary"))?; + let spins: [i32; 2] = catalog + .i32_sequence(quantum.input_spin_sequence_id, "closure quantum spins")? + .try_into() + .map_err(|_| invalid("closure quantum flow must be binary"))?; + let quantum_coupling = catalog + .factor(quantum.exact_coupling_factor_id, "closure quantum coupling")?; + Ok(PreparedClosureQuantum { + row: Some(quantum), + input_states: Some(states), + input_spins: Some(spins), + coupling_authenticated: quantum_coupling == binding_coupling, + binding_coupling, + output_factor_source: row.output_factor_source, + }) + }) + .collect::>>()? + }; + let witnesses = catalog + .witness_rows(row.color_contraction_template_id)? + .iter() + .copied() + .map(|witness_row| { + Ok(PreparedWitness { + row: witness_row, + witness: catalog.witness(witness_row)?, + }) + }) + .collect::>>()?; + Ok(Self { + row, + input_states, + local_orders, + component_coefficients, + canonical_input_order, + input_exchange_factor, + base_factor, + closure_semantic_digest: catalog.digest(row.semantic_digest_id, "closure semantic")?, + evaluator_binding_digest: catalog + .digest(binding.semantic_digest_id, "closure evaluator binding")?, + quantum_flows: quantum_flows.into_boxed_slice(), + witnesses: witnesses.into_boxed_slice(), + }) + } + + pub(super) fn parent_ids( + &self, + anchor_state: u32, + complement_state: u32, + anchor_id: u32, + complement_id: u32, + ) -> Option<[u32; 2]> { + if self.input_states == [complement_state, anchor_state] { + Some([complement_id, anchor_id]) + } else if anchor_state != complement_state + && self.input_states == [anchor_state, complement_state] + { + Some([anchor_id, complement_id]) + } else { + None + } + } + + pub(super) fn evaluator_parents(&self, concrete: [u32; 2]) -> ([u32; 2], ExactComplexRational) { + let mut ordered = match self.canonical_input_order { + [0, 1] => concrete, + [1, 0] => [concrete[1], concrete[0]], + _ => unreachable!("validated closure input order"), + }; + let mut factor = ExactComplexRational::ONE; + if let Some(exchange) = self.input_exchange_factor + && ordered[1] < ordered[0] + { + ordered.swap(0, 1); + factor = exchange; + } + (ordered, factor) + } +} + +fn prepared_source_contracts( + templates: &ValidatedRecurrenceTemplateInput, + catalog: &TemplateCatalog<'_>, + seed: &OnTheFlyProcessSeedV1, +) -> RusticolResult> { + let mut result = BTreeMap::new(); + for anchor in &seed.source_anchors { + for state in &anchor.states { + let (source, current_state) = + validate_source_contract(templates, catalog, anchor, state)?; + let binding = templates + .input() + .evaluator_bindings + .get(source.evaluator_binding_id as usize) + .ok_or_else(|| integrity("source evaluator binding is absent"))?; + let contract = PreparedOnTheFlySourceContractV1 { + current_state, + color_seed: catalog.source_seed(source)?, + executor: PreparedOnTheFlyExecutorContractV1 { + operation_id: source.id, + operation_semantic_digest: catalog + .digest(source.semantic_digest_id, "source semantic")?, + evaluator_binding_id: binding.id, + evaluator_binding_semantic_digest: catalog + .digest(binding.semantic_digest_id, "source evaluator semantic")?, + }, + }; + let key = (state.source_template_id, state.current_state_template_id); + if let Some(previous) = result.insert(key, contract) + && previous != contract + { + return Err(integrity( + "equal compact source identities have different prepared contracts", + )); + } + } + } + Ok(result) +} + +fn prepared_propagator_executors( + templates: &ValidatedRecurrenceTemplateInput, + catalog: &TemplateCatalog<'_>, + propagators: &BTreeMap>, +) -> RusticolResult> { + let mut result = BTreeMap::new(); + for propagator_id in propagators.values().flatten().copied() { + let row = templates + .input() + .propagators + .get(propagator_id as usize) + .ok_or_else(|| integrity("propagator executor template is absent"))?; + let binding = templates + .input() + .evaluator_bindings + .get(row.evaluator_binding_id as usize) + .ok_or_else(|| integrity("propagator executor binding is absent"))?; + let contract = PreparedOnTheFlyExecutorContractV1 { + operation_id: row.id, + operation_semantic_digest: catalog + .digest(row.semantic_digest_id, "propagator semantic")?, + evaluator_binding_id: binding.id, + evaluator_binding_semantic_digest: catalog + .digest(binding.semantic_digest_id, "propagator evaluator semantic")?, + }; + if let Some(previous) = result.insert(propagator_id, contract) + && previous != contract + { + return Err(integrity( + "equal propagator identities have different prepared contracts", + )); + } + } + Ok(result) +} + +pub(super) fn canonical_state_pair(left: u32, right: u32) -> (u32, u32) { + if left <= right { + (left, right) + } else { + (right, left) + } +} + +pub(super) fn propagator_by_state( + templates: &ValidatedRecurrenceTemplateInput, +) -> RusticolResult>> { + let mut result = BTreeMap::new(); + for row in &templates.input().propagators { + let value = (row.applies_propagator != 0).then_some(row.id); + if result.insert(row.state_template_id, value).is_some() { + return Err(invalid(format!( + "current state {} has multiple propagator contracts", + row.state_template_id + ))); + } + } + Ok(result) +} + +pub(super) fn prepared_transitions( + templates: &ValidatedRecurrenceTemplateInput, + catalog: &TemplateCatalog<'_>, +) -> RusticolResult>> { + let mut result = BTreeMap::<(u32, u32), Vec>::new(); + for row in templates.input().transitions.iter().copied() { + let prepared = PreparedTransition::new(row, templates, catalog)?; + result + .entry(canonical_state_pair( + prepared.input_states[0], + prepared.input_states[1], + )) + .or_default() + .push(prepared); + } + Ok(result) +} + +pub(super) fn prepared_closures( + templates: &ValidatedRecurrenceTemplateInput, + catalog: &TemplateCatalog<'_>, +) -> RusticolResult>> { + let mut result = BTreeMap::<(u32, u32), Vec>::new(); + for row in templates.input().closures.iter().copied() { + let prepared = PreparedClosure::new(row, templates, catalog)?; + result + .entry(canonical_state_pair( + prepared.input_states[0], + prepared.input_states[1], + )) + .or_default() + .push(prepared); + } + Ok(result) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::recurrence::contact_orbit_owner::{ + ContactOrbitTestBinding, contact_orbit_test_template, + }; + use crate::recurrence::fermion_ordering::fermion_ordering_factor; + use crate::recurrence::validated_template_fixture; + + fn append_string( + input: &mut crate::recurrence::template::OwnedRecurrenceTemplateInput, + value: &str, + ) -> u32 { + let id = u32::try_from(input.string_ranges.len()).unwrap(); + let start = u64::try_from(input.string_bytes.len()).unwrap(); + input.string_bytes.extend_from_slice(value.as_bytes()); + input + .string_ranges + .push(crate::recurrence::CheckedTableRange::new( + start, + u64::try_from(value.len()).unwrap(), + )); + id + } + + fn four_adjoint_seed( + summary: crate::recurrence::template::RecurrenceTemplateValidationSummary, + ) -> OnTheFlyProcessSeedV1 { + let anchors = (0..4_u32) + .map(|source_slot| { + let state = OnTheFlySourceStateV1::new( + 0, + 1, + 1, + 0, + 0, + SemanticDigest::new([5; 32]).unwrap(), + SemanticDigest::new([4; 32]).unwrap(), + 1, + ExactComplexRational::ONE, + 50_000, + 0, + vec![21], + 0, + SemanticDigest::new([17; 32]).unwrap(), + OnTheFlySourceWavefunctionFamilyV1::Vector, + OnTheFlySourceOrientationV1::SelfConjugate, + None, + ) + .unwrap(); + OnTheFlySourceAnchorV1::new( + source_slot, + source_slot, + source_slot < 2, + OnTheFlyExternalColorRoleV1::Adjoint, + false, + None, + vec![state], + ) + .unwrap() + }) + .collect(); + OnTheFlyProcessSeedV1::new( + SemanticDigest::new([91; 32]).unwrap(), + summary.compiled_model_digest, + summary.catalog_digest, + summary.prepared_kernel_pack_digest, + SemanticDigest::new([0x7d; 32]).unwrap(), + SemanticDigest::new([92; 32]).unwrap(), + "raw-amplitude-test", + ExactComplexRational::ONE, + anchors, + vec![0, 1, 2, 3], + OnTheFlyCouplingOrderPolicyV1::Explicit, + vec![1], + vec![Some(0)], + Vec::new(), + ) + .unwrap() + } + + fn cyclic_anchor_template_fixture() -> ( + crate::recurrence::template::OwnedRecurrenceTemplateInput, + OnTheFlyProcessSeedV1, + ) { + let templates = validated_template_fixture(); + let seed = four_adjoint_seed(templates.summary()); + let mut input = templates.into_input(); + let adjoint_shape = append_string(&mut input, "adjoint-segment"); + let direct_contraction = append_string(&mut input, "direct-contraction"); + let empty_sequence = input + .u32_sequence_ranges + .iter() + .find(|row| row.range.count == 0) + .unwrap() + .id; + + input.current_states[0].color_representation = 8; + input.current_states[0].lc_color_shape_string_id = adjoint_shape; + let closure = &mut input.closures[0]; + closure.result_state_template_id = MISSING_U32; + closure.eligible_quantum_flow_sequence_id = empty_sequence; + closure.equivalence_class_string_id = direct_contraction; + input.evaluator_bindings[closure.evaluator_binding_id as usize].callable_kind = + EvaluatorCallableKind::RusticolTemplate as u8; + let color = &mut input.color_contractions[closure.color_contraction_template_id as usize]; + let representations = input.i32_sequence_ranges + [color.input_representation_sequence_id as usize] + .range + .as_usize_range(input.i32_sequence_values.len(), "test color inputs") + .unwrap(); + input.i32_sequence_values[representations].copy_from_slice(&[8, 8]); + (input, seed) + } + + #[test] + fn on_the_fly_prepared_transition_binds_only_certified_contact_metadata() { + let none = contact_orbit_test_template(ContactOrbitTestBinding::None) + .validate() + .unwrap(); + let none_catalog = TemplateCatalog::new(none.input()).unwrap(); + let none_prepared = prepared_transitions(&none, &none_catalog).unwrap(); + assert!( + none_prepared + .values() + .flatten() + .next() + .unwrap() + .contact_orbit + .is_none() + ); + + let one = contact_orbit_test_template(ContactOrbitTestBinding::One) + .validate() + .unwrap(); + let one_catalog = TemplateCatalog::new(one.input()).unwrap(); + let one_prepared = prepared_transitions(&one, &one_catalog).unwrap(); + assert!( + one_prepared + .values() + .flatten() + .next() + .unwrap() + .contact_orbit + .is_some() + ); + } + + #[test] + fn prepared_grammar_caches_exact_query_invariant_contexts() { + let templates = validated_template_fixture(); + let summary = templates.summary(); + let seed = scalar_adapter_test_seed( + summary.compiled_model_digest, + summary.catalog_digest, + summary.prepared_kernel_pack_digest, + SemanticDigest::new([0x7d; 32]).unwrap(), + ) + .unwrap(); + let catalog = TemplateCatalog::new(templates.input()).unwrap(); + let grammar = prepare_on_the_fly_grammar_v1(&templates, &catalog, &seed).unwrap(); + + assert_eq!( + grammar.contact_orbits, + PreparedContactOrbitIndex::new(&grammar.transitions).unwrap(), + ); + assert_eq!( + grammar.closure_anchor_policy, + OnTheFlyClosureAnchorPolicyV1::selector_endpoint(), + ); + let fresh_fermion_ordering = on_the_fly_fermion_ordering_context(&seed).unwrap(); + assert_eq!( + fermion_ordering_factor( + &templates.input().current_states, + [0, 0], + [&[0], &[1]], + &grammar.fermion_ordering, + ) + .unwrap(), + fermion_ordering_factor( + &templates.input().current_states, + [0, 0], + [&[0], &[1]], + &fresh_fermion_ordering, + ) + .unwrap(), + ); + } + + #[test] + fn cyclic_anchor_policy_requires_the_exact_v4_template_contract() { + let (input, seed) = cyclic_anchor_template_fixture(); + let catalog = TemplateCatalog::new(&input).unwrap(); + assert_eq!( + certified_cyclic_trace_closure_anchor_policy(&input, &catalog, &seed).unwrap(), + OnTheFlyClosureAnchorPolicyV1::certified_cyclic_minimum(0), + ); + + let mut ineligible = input.clone(); + let closure = ineligible.closures[0]; + ineligible.evaluator_bindings[closure.evaluator_binding_id as usize].callable_kind = + EvaluatorCallableKind::PreparedKernel as u8; + let catalog = TemplateCatalog::new(&ineligible).unwrap(); + assert_eq!( + certified_cyclic_trace_closure_anchor_policy(&ineligible, &catalog, &seed).unwrap(), + OnTheFlyClosureAnchorPolicyV1::selector_endpoint(), + ); + + let mut auxiliary = input; + auxiliary.current_states[0].auxiliary_kind_string_id = + auxiliary.current_states[0].lc_color_shape_string_id; + let catalog = TemplateCatalog::new(&auxiliary).unwrap(); + assert_eq!( + certified_cyclic_trace_closure_anchor_policy(&auxiliary, &catalog, &seed).unwrap(), + OnTheFlyClosureAnchorPolicyV1::selector_endpoint(), + ); + } +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/recurrence/on_the_fly/test_support.rs b/pyamplicol/rust/crates/rusticol-core/src/recurrence/on_the_fly/test_support.rs new file mode 100644 index 0000000000..f300e7c4c1 --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/recurrence/on_the_fly/test_support.rs @@ -0,0 +1,1469 @@ +// SPDX-License-Identifier: 0BSD + +#![allow(dead_code)] // This module is intentionally a non-default test-support surface. + +//! Feature-gated development oracle for the compact on-the-fly builder. +//! +//! This is the only on-the-fly module allowed to consume the materialized +//! recurrence builder input. It maps exactly one authenticated public row +//! into the compact seed/query API, then compares that query-local trace with +//! the established builder. Release/default builds do not compile this file. + +use super::source_seed::validate_permutation; +use super::trace::{contribution_proof_digest, hash_current_key, multiset_digest}; +use super::*; +use crate::recurrence::construct::TemplateCatalog; +use crate::recurrence::process::{ + OwnedFermionPairingInput, OwnedRecurrenceProcessInput, ProcessLCSectorKind, + ProcessSourceStateRow, +}; +use crate::recurrence::template::{CurrentOrientation, CurrentStateRow, ParticleStatistics}; +use crate::recurrence::{ + AuthenticatedRecurrenceBuilderInput, ConstructionTransitionDiagnosticRowV1, RecurrenceProgram, + RecurrenceStrategy, +}; + +#[derive(Clone, Debug, Eq, PartialEq)] +#[doc(hidden)] +pub struct OnTheFlyTestSupportReportV1 { + pub seed_digest: SemanticDigest, + pub query_digest: SemanticDigest, + pub selector_digest: SemanticDigest, + pub trace_digest: SemanticDigest, + pub current_count: u32, + pub contribution_count: u32, + pub closure_count: u32, + pub established_current_count: u32, + pub established_contribution_count: u32, + pub established_closure_count: u32, + pub current_multiset_digest: SemanticDigest, + pub established_current_multiset_digest: SemanticDigest, + pub contribution_multiset_digest: SemanticDigest, + pub established_contribution_multiset_digest: SemanticDigest, + pub closure_multiset_digest: SemanticDigest, + pub closure_parity_multiset_digest: SemanticDigest, + pub established_closure_parity_multiset_digest: SemanticDigest, + pub negative_contribution_factor_count: u32, + pub established_negative_contribution_factor_count: u32, + pub source_domain_equal: bool, + pub pairing_oracle_equal: bool, + pub pairing_fermion_parities: Vec, + pub established_pairing_fermion_parities: Vec, + pub workspace_capacity_independent: bool, + pub compact_transition_candidates: Vec, + pub established_transition_candidates: Vec, +} + +impl OnTheFlyTestSupportReportV1 { + pub fn structural_parity(&self) -> bool { + self.current_count == self.established_current_count + && self.contribution_count == self.established_contribution_count + && self.closure_count == self.established_closure_count + && self.current_multiset_digest == self.established_current_multiset_digest + && self.contribution_multiset_digest == self.established_contribution_multiset_digest + && self.closure_parity_multiset_digest + == self.established_closure_parity_multiset_digest + && self.source_domain_equal + && self.pairing_oracle_equal + } +} + +struct OnePublicRowOracle { + external_permutation: Vec, + public_helicities: Vec, + selector: OnTheFlyLcSelectorV1, +} + +/// Test-support convenience bundle retaining one generated compact seed. +/// Production families instead keep one lane-owned seed and build borrowed +/// [`OnTheFlySelectedQueryTraceV1`] values for each selected query. +pub(crate) struct OnTheFlySelectedTraceV1 { + pub(crate) seed: OnTheFlyProcessSeedV1, + pub(crate) query: DecodedLcQueryV1, + pub(crate) trace: OnTheFlyStructuralTraceV1, + pub(crate) projection: OnTheFlyProjectionProbeV1, +} + +fn process_string<'a>( + process: &'a OwnedRecurrenceProcessInput, + string_id: u32, + label: &str, +) -> RusticolResult<&'a str> { + let range = process + .string_ranges + .get(string_id as usize) + .copied() + .ok_or_else(|| integrity(format!("{label} string is absent")))?; + let bytes = &process.string_bytes[range.as_usize_range(process.string_bytes.len(), label)?]; + std::str::from_utf8(bytes) + .map_err(|error| integrity(format!("{label} string is not UTF-8: {error}"))) +} + +fn process_factor( + process: &OwnedRecurrenceProcessInput, + factor_id: u32, + label: &str, +) -> RusticolResult { + let row = process + .exact_factors + .get(factor_id as usize) + .ok_or_else(|| integrity(format!("{label} factor is absent")))?; + if row.id != factor_id { + return Err(integrity(format!( + "{label} factor catalog is not canonical" + ))); + } + ExactComplexRational::parse_parts( + process_string(process, row.real_numerator_string_id, label)?, + process_string(process, row.real_denominator_string_id, label)?, + process_string(process, row.imag_numerator_string_id, label)?, + process_string(process, row.imag_denominator_string_id, label)?, + ) + .map_err(|error| integrity(format!("{label} exact factor is invalid: {error}"))) +} + +fn process_sequence<'a>( + process: &'a OwnedRecurrenceProcessInput, + sequence_id: u32, + label: &str, +) -> RusticolResult<&'a [u32]> { + let range = process + .u32_sequence_ranges + .get(sequence_id as usize) + .copied() + .ok_or_else(|| integrity(format!("{label} sequence is absent")))?; + Ok(&process.u32_sequence_values + [range.as_usize_range(process.u32_sequence_values.len(), label)?]) +} + +fn process_digest( + process: &OwnedRecurrenceProcessInput, + digest_id: u32, + label: &str, +) -> RusticolResult { + let row = process + .digest_catalog + .get(digest_id as usize) + .copied() + .ok_or_else(|| integrity(format!("{label} digest is absent")))?; + if row.id != digest_id { + return Err(integrity(format!( + "{label} digest catalog is not canonical" + ))); + } + SemanticDigest::new(row.value) + .map_err(|error| integrity(format!("{label} digest is invalid: {error}"))) +} + +fn pairing_string<'a>( + input: &'a OwnedFermionPairingInput, + string_id: u32, + label: &str, +) -> RusticolResult<&'a str> { + let range = input + .string_ranges + .get(string_id as usize) + .copied() + .ok_or_else(|| integrity(format!("{label} pairing string is absent")))?; + let bytes = &input.string_bytes[range.as_usize_range(input.string_bytes.len(), label)?]; + std::str::from_utf8(bytes) + .map_err(|error| integrity(format!("{label} pairing string is not UTF-8: {error}"))) +} + +fn authenticated_source_family( + catalog: &TemplateCatalog<'_>, + source: SourceRow, + current_state: CurrentStateRow, +) -> RusticolResult { + let family = catalog.string( + source.wavefunction_family_string_id, + "source wavefunction family", + )?; + let statistics = ParticleStatistics::try_from(current_state.statistics)?; + match (family, statistics, current_state.dimension) { + ("scalar", ParticleStatistics::Boson, 1) => Ok(OnTheFlySourceWavefunctionFamilyV1::Scalar), + ("fermion", ParticleStatistics::Fermion, 2) => { + Ok(OnTheFlySourceWavefunctionFamilyV1::WeylFermion) + } + ("fermion", ParticleStatistics::Fermion, 4) => { + Ok(OnTheFlySourceWavefunctionFamilyV1::DiracFermion) + } + ("vector", ParticleStatistics::Boson, 4) => Ok(OnTheFlySourceWavefunctionFamilyV1::Vector), + ("spin2", ParticleStatistics::Boson, 16) => Ok(OnTheFlySourceWavefunctionFamilyV1::Spin2), + ("ghost" | "auxiliary", _, _) => Err(invalid( + "on-the-fly source execution does not support ghost or auxiliary external states", + )), + _ => Err(integrity( + "authenticated source family, statistics, and component dimension disagree", + )), + } +} + +fn authenticated_source_orientation( + current_state: CurrentStateRow, +) -> RusticolResult { + Ok( + match CurrentOrientation::try_from(current_state.orientation)? { + CurrentOrientation::Particle => OnTheFlySourceOrientationV1::Particle, + CurrentOrientation::Antiparticle => OnTheFlySourceOrientationV1::Antiparticle, + CurrentOrientation::SelfConjugate => OnTheFlySourceOrientationV1::SelfConjugate, + }, + ) +} + +fn authenticated_prepared_mass_slot( + authenticated: &AuthenticatedRecurrenceBuilderInput, + catalog: &TemplateCatalog<'_>, + source: SourceRow, + current_state: CurrentStateRow, +) -> RusticolResult> { + super::source_builder::resolve_prepared_mass_slot( + source, + current_state, + authenticated.template().input(), + catalog, + authenticated + .process() + .input() + .parameter_projection + .iter() + .map(|row| { + ( + row.parameter_template_id, + row.prepared_parameter_id(), + row.component, + ) + }), + ) +} + +fn pairing_endpoint_contracts( + authenticated: &AuthenticatedRecurrenceBuilderInput, +) -> RusticolResult> { + let Some(pairing) = authenticated.process().input().fermion_pairing.as_ref() else { + return Ok(BTreeMap::new()); + }; + let mut result = BTreeMap::new(); + for endpoint in &pairing.endpoints { + let role = match endpoint.color_orientation { + 0 => OnTheFlyExternalColorRoleV1::Fundamental, + 1 => OnTheFlyExternalColorRoleV1::Antifundamental, + _ => { + return Err(integrity( + "pairing endpoint has an invalid color orientation", + )); + } + }; + let digest = SemanticDigest::new(endpoint.contract_digest) + .map_err(|error| integrity(format!("pairing endpoint digest is invalid: {error}")))?; + if result + .insert(endpoint.source_slot, (role, digest)) + .is_some() + { + return Err(integrity("pairing endpoint repeats a source slot")); + } + } + Ok(result) +} + +fn color_role( + representation: i32, + endpoint: Option<(OnTheFlyExternalColorRoleV1, SemanticDigest)>, +) -> RusticolResult<(OnTheFlyExternalColorRoleV1, Option)> { + match (representation, endpoint) { + (1, None) => Ok((OnTheFlyExternalColorRoleV1::Singlet, None)), + (8, None) => Ok((OnTheFlyExternalColorRoleV1::Adjoint, None)), + (3, Some((OnTheFlyExternalColorRoleV1::Fundamental, digest))) => { + Ok((OnTheFlyExternalColorRoleV1::Fundamental, Some(digest))) + } + (-3, Some((OnTheFlyExternalColorRoleV1::Antifundamental, digest))) => { + Ok((OnTheFlyExternalColorRoleV1::Antifundamental, Some(digest))) + } + _ => Err(integrity( + "source color representation disagrees with the pairing endpoint catalog", + )), + } +} + +fn authenticated_source_anchors( + authenticated: &AuthenticatedRecurrenceBuilderInput, +) -> RusticolResult> { + let process = authenticated.process().input(); + let selected_source_mode = authenticated.process().summary().selected_source_mode(); + let selected_states = process + .selected_source_coverage + .iter() + .map(|row| (row.source_slot, row.source_state_index)) + .collect::>(); + let input = authenticated.template().input(); + let catalog = TemplateCatalog::new(input)?; + let endpoint_contracts = pairing_endpoint_contracts(authenticated)?; + let mut anchors = Vec::new(); + anchors + .try_reserve_exact(process.external_legs.len()) + .map_err(|error| invalid(format!("source-anchor allocation failed: {error}")))?; + for (source_slot, leg) in process.external_legs.iter().copied().enumerate() { + if leg.source_slot as usize != source_slot { + return Err(integrity("external source slots are not canonical")); + } + let range = leg.source_state_range.as_usize_range( + process.source_states.len(), + "on-the-fly authenticated source states", + )?; + let mut states = Vec::new(); + let mut role = None; + let mut anchor_fermionic = None; + let mut pairing_contract = None; + for process_state in process.source_states[range] + .iter() + .copied() + .filter(|state| { + !selected_source_mode + || selected_states.contains(&(leg.source_slot, state.state_index)) + }) + { + let source = *input + .sources + .get(process_state.source_template_id as usize) + .ok_or_else(|| integrity("authenticated source template is absent"))?; + let current_state = *input + .current_states + .get(process_state.current_state_template_id as usize) + .ok_or_else(|| integrity("authenticated current-state template is absent"))?; + if source.id != process_state.source_template_id + || current_state.id != process_state.current_state_template_id + { + return Err(integrity( + "authenticated process source row differs from its template contract", + )); + } + validate_crossed_source_state(leg.is_initial != 0, &process_state, source, input)?; + let family = authenticated_source_family(&catalog, source, current_state)?; + let (state_role, state_pairing_contract) = color_role( + current_state.color_representation, + endpoint_contracts.get(&leg.source_slot).copied(), + )?; + match role { + None => role = Some(state_role), + Some(previous) if previous == state_role => {} + Some(_) => return Err(integrity("one source anchor mixes color roles")), + } + match anchor_fermionic { + None => anchor_fermionic = Some(family.is_fermionic()), + Some(previous) if previous == family.is_fermionic() => {} + Some(_) => return Err(integrity("one source anchor mixes statistics")), + } + match pairing_contract { + None => pairing_contract = state_pairing_contract, + Some(previous) if Some(previous) == state_pairing_contract => {} + Some(_) => return Err(integrity("one source anchor mixes pairing contracts")), + } + let seed = catalog.source_seed(source)?; + states.push(OnTheFlySourceStateV1::new( + process_state.state_index, + process_state.public_helicity, + process_state.public_helicity, + source.id, + current_state.id, + catalog.digest(source.semantic_digest_id, "source semantic")?, + catalog.digest(current_state.semantic_digest_id, "current-state semantic")?, + process_state.momentum_sign, + process_factor( + process, + process_state.crossing_phase_factor_id, + "source crossing phase", + )?, + process_state.spin_state, + process_state.chirality, + catalog + .flavour_flow(source.flavour_flow_id, "source flavour flow")? + .to_vec(), + source.quantum_number_flow_id, + seed.proof_digest(), + family, + authenticated_source_orientation(current_state)?, + authenticated_prepared_mass_slot(authenticated, &catalog, source, current_state)?, + )?); + } + anchors.push(OnTheFlySourceAnchorV1::new( + leg.source_slot, + leg.public_label, + leg.is_initial != 0, + role.ok_or_else(|| integrity("authenticated source anchor is empty"))?, + anchor_fermionic.ok_or_else(|| integrity("source statistics are absent"))?, + pairing_contract, + states, + )?); + } + Ok(anchors) +} + +fn pairing_classes( + authenticated: &AuthenticatedRecurrenceBuilderInput, + contracts: &BTreeMap, +) -> RusticolResult> { + let Some(input) = authenticated.process().input().fermion_pairing.as_ref() else { + return Ok(Vec::new()); + }; + input + .pairing_classes + .iter() + .map(|class| { + let endpoints = |range: crate::recurrence::CheckedTableRange, + role: OnTheFlyExternalColorRoleV1, + label: &str| + -> RusticolResult> { + let values = match role { + OnTheFlyExternalColorRoleV1::Fundamental => &input.class_fundamental_slots, + OnTheFlyExternalColorRoleV1::Antifundamental => { + &input.class_antifundamental_slots + } + _ => unreachable!(), + }; + values[range.as_usize_range(values.len(), label)?] + .iter() + .copied() + .map(|source_slot| { + let (actual_role, digest) = contracts + .get(&source_slot) + .copied() + .ok_or_else(|| integrity("pairing class endpoint is absent"))?; + if actual_role != role { + return Err(integrity("pairing class endpoint role changed")); + } + Ok(OnTheFlyPairingEndpointV1 { + source_slot, + source_contract_digest: digest, + }) + }) + .collect() + }; + OnTheFlyPairingClassV1::new( + pairing_string(input, class.species_string_id, "pairing class species")?, + SemanticDigest::new(class.proof_digest).map_err(|error| { + integrity(format!("pairing class proof digest is invalid: {error}")) + })?, + endpoints( + class.fundamental_slot_range, + OnTheFlyExternalColorRoleV1::Fundamental, + "pairing class fundamental slots", + )?, + endpoints( + class.antifundamental_slot_range, + OnTheFlyExternalColorRoleV1::Antifundamental, + "pairing class antifundamental slots", + )?, + ) + }) + .collect() +} + +fn compact_seed( + authenticated: &AuthenticatedRecurrenceBuilderInput, + direct_catalog_digest: SemanticDigest, + external_permutation: Vec, +) -> RusticolResult { + if authenticated.process().summary().strategy() != RecurrenceStrategy::TopologyReplay { + return Err(invalid( + "the on-the-fly test oracle requires topology-replay input", + )); + } + let process = authenticated.process().input(); + let normalization = *process + .normalization + .first() + .ok_or_else(|| integrity("authenticated process normalization is absent"))?; + if process.normalization.len() != 1 { + return Err(integrity( + "on-the-fly raw-amplitude input requires one normalization row", + )); + } + let normalization_factor = + process_factor(process, normalization.factor_id, "process normalization")?; + if normalization_factor != ExactComplexRational::ONE { + return Err(integrity( + "on-the-fly raw-amplitude normalization must be exact one", + )); + } + let contracts = pairing_endpoint_contracts(authenticated)?; + let template_catalog = TemplateCatalog::new(authenticated.template().input())?; + let mut process_limits = BTreeMap::new(); + for row in &process.coupling_limits { + let name = process_string(process, row.name_string_id, "process coupling limit")?; + process_limits.insert(name, row.maximum); + } + let coupling_limits = template_catalog + .coupling_order_names() + .iter() + .map(|name| { + process_limits.get(name).copied().map(Some).ok_or_else(|| { + integrity(format!( + "authenticated process has no finalized coupling limit for model order {name:?} required by the prepared template catalog" + )) + }) + }) + .collect::>>()?; + OnTheFlyProcessSeedV1::new( + authenticated.process().semantic_identity().process_digest(), + authenticated.template().summary().compiled_model_digest, + authenticated.template().summary().catalog_digest, + authenticated + .template() + .summary() + .prepared_kernel_pack_digest, + direct_catalog_digest, + process_digest( + process, + normalization.semantic_digest_id, + "process normalization semantic", + )?, + process_string( + process, + normalization.convention_string_id, + "process normalization convention", + )?, + normalization_factor, + authenticated_source_anchors(authenticated)?, + external_permutation, + OnTheFlyCouplingOrderPolicyV1::Explicit, + vec![1; coupling_limits.len()], + coupling_limits, + pairing_classes(authenticated, &contracts)?, + ) +} + +fn open_blocks_for_sector( + process: &OwnedRecurrenceProcessInput, + sector: crate::recurrence::process::ProcessPhysicalLCSectorRow, +) -> RusticolResult>> { + let range = sector + .open_string_range + .as_usize_range(process.lc_open_strings.len(), "one-row open strings")?; + process.lc_open_strings[range] + .iter() + .map(|row| { + let mut block = vec![row.fundamental_source_slot]; + block.extend_from_slice(process_sequence( + process, + row.adjoint_sequence_id, + "one-row open-string adjoints", + )?); + block.push(row.antifundamental_source_slot); + Ok(block) + }) + .collect() +} + +fn established_pairing_rule_by_id( + authenticated: &AuthenticatedRecurrenceBuilderInput, + rule_id: Option, +) -> RusticolResult> { + let Some(catalog) = authenticated.process().fermion_pairing_catalog() else { + if rule_id.is_none() { + return Ok(None); + } + return Err(integrity( + "established pairing owner has no pairing catalog", + )); + }; + let Some(rule_id) = rule_id else { + return Err(integrity( + "established retained closure has no pairing owner", + )); + }; + catalog + .rules() + .iter() + .copied() + .find(|rule| rule.rule_id == rule_id) + .map(Some) + .ok_or_else(|| integrity(format!("established pairing owner {rule_id} is absent"))) +} + +fn one_public_row_oracle( + authenticated: &AuthenticatedRecurrenceBuilderInput, + selected_public_flow_id: u32, + public_helicities: &[i32], +) -> RusticolResult { + let process = authenticated.process().input(); + if public_helicities.len() != process.external_legs.len() { + return Err(invalid( + "one-row oracle requires one public helicity per external source", + )); + } + let selected_mode = authenticated.process().summary().selected_flow_mode(); + if selected_mode + && !process + .selected_public_flow_coverage + .iter() + .any(|row| row.flow_id == selected_public_flow_id) + { + return Err(invalid( + "requested public flow is absent from the selected generation slice", + )); + } + let flow = process + .public_lc_flows + .iter() + .copied() + .find(|row| row.flow_id == selected_public_flow_id) + .ok_or_else(|| invalid("requested public flow is absent"))?; + let sector = *process + .physical_lc_sectors + .get(flow.construction_sector_id as usize) + .ok_or_else(|| integrity("public row construction sector is absent"))?; + if sector.sector_id != flow.construction_sector_id { + return Err(integrity( + "public row construction-sector catalog is not canonical", + )); + } + let external_permutation = process_sequence( + process, + flow.source_slot_permutation_sequence_id, + "one-row source permutation", + )? + .to_vec(); + validate_permutation( + &external_permutation, + process.external_legs.len(), + "one-row external permutation", + )?; + let selector = match sector.kind()? { + ProcessLCSectorKind::Singlet => OnTheFlyLcSelectorV1::Singlet, + ProcessLCSectorKind::SingleTrace => OnTheFlyLcSelectorV1::single_trace( + process_sequence(process, flow.word_sequence_id, "one-row public trace")?.to_vec(), + ), + ProcessLCSectorKind::OpenLines => { + let construction_blocks = open_blocks_for_sector(process, sector)?; + let public_blocks = construction_blocks + .iter() + .map(|block| { + block + .iter() + .map(|slot| { + external_permutation + .get(*slot as usize) + .copied() + .ok_or_else(|| integrity("one-row block source is out of range")) + }) + .collect::>>() + }) + .collect::>>()?; + OnTheFlyLcSelectorV1::open_lines(public_blocks) + } + }; + Ok(OnePublicRowOracle { + external_permutation, + public_helicities: public_helicities.to_vec(), + selector, + }) +} + +#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] +struct SourceDomainRow { + source_slot: u32, + source_template_id: u32, + spin_state_class: i32, + family: u8, + orientation: u8, + source_helicity: i32, + chirality: i32, + prepared_mass_parameter_slot: Option, +} + +fn compact_source_domain(seed: &OnTheFlyProcessSeedV1) -> Vec { + let mut rows = seed + .source_execution_specs() + .map(|row| SourceDomainRow { + source_slot: row.source_slot, + source_template_id: row.source_template_id, + spin_state_class: row.spin_state_class, + family: row.family as u8, + orientation: row.orientation as u8, + source_helicity: row.helicity, + chirality: row.chirality, + prepared_mass_parameter_slot: row.prepared_mass_parameter_slot, + }) + .collect::>(); + rows.sort_unstable(); + rows +} + +fn established_source_state_for_template( + source_slot: u32, + states: &[ProcessSourceStateRow], + source_template_id: u32, +) -> RusticolResult { + let mut matches = states + .iter() + .copied() + .filter(|state| state.source_template_id == source_template_id); + let state = matches.next().ok_or_else(|| { + integrity(format!( + "established source slot {source_slot} template {source_template_id} is absent from its authenticated state domain" + )) + })?; + if matches.next().is_some() { + return Err(integrity(format!( + "established source slot {source_slot} template {source_template_id} is ambiguous in its authenticated state domain" + ))); + } + Ok(state) +} + +fn established_source_domain( + authenticated: &AuthenticatedRecurrenceBuilderInput, + program: &RecurrenceProgram, +) -> RusticolResult> { + let input = authenticated.template().input(); + let catalog = TemplateCatalog::new(input)?; + let mut rows = Vec::new(); + for current in program + .currents() + .iter() + .filter(|current| current.is_source()) + { + let key = current.key(); + let [source_slot] = key.support_source_slots() else { + return Err(integrity( + "established source current has non-unary support", + )); + }; + let source_template_id = match key.source_binding() { + CurrentSourceBinding::FixedTemplate(value) => *value, + _ => return Err(integrity("established source has no fixed source template")), + }; + let source = *input + .sources + .get(source_template_id as usize) + .ok_or_else(|| integrity("established source template is absent"))?; + let current_state = *input + .current_states + .get(key.current_state_template_id() as usize) + .ok_or_else(|| integrity("established source current state is absent"))?; + let process = authenticated.process().input(); + let source_state_range = process + .external_legs + .get(*source_slot as usize) + .ok_or_else(|| integrity("established source leg is absent"))? + .source_state_range + .as_usize_range(process.source_states.len(), "established source states")?; + let source_helicity = established_source_state_for_template( + *source_slot, + &process.source_states[source_state_range], + source_template_id, + )?; + if source_helicity.current_state_template_id != key.current_state_template_id() { + return Err(integrity(format!( + "established source state and current template disagree: source slot {}, source state {}, source template {}, authenticated current template {}, established current template {}", + source_slot, + source_helicity.state_index, + source_template_id, + source_helicity.current_state_template_id, + key.current_state_template_id(), + ))); + } + rows.push(SourceDomainRow { + source_slot: *source_slot, + source_template_id, + spin_state_class: key.spin_state_class(), + family: authenticated_source_family(&catalog, source, current_state)? as u8, + orientation: authenticated_source_orientation(current_state)? as u8, + source_helicity: source_helicity.public_helicity, + chirality: current_state.chirality, + prepared_mass_parameter_slot: authenticated_prepared_mass_slot( + authenticated, + &catalog, + source, + current_state, + )?, + }); + } + rows.sort_unstable(); + Ok(rows) +} + +fn established_current_digests(program: &RecurrenceProgram) -> RusticolResult> { + program + .currents() + .iter() + .map(|current| { + let color = program + .dynamic_color_states() + .get(current.key().dynamic_lc_color_state_id().get() as usize) + .ok_or_else(|| integrity("established current color state is absent"))?; + hash_current_key(current.key(), color) + }) + .collect() +} + +fn established_contribution_digest( + program: &RecurrenceProgram, + current_digests: &[SemanticDigest], +) -> RusticolResult { + let mut rows = Vec::new(); + for contribution in program.contributions() { + let [parent0, parent1] = contribution.parent_current_ids() else { + return Err(integrity( + "established selected-query contribution is not binary", + )); + }; + let result = *current_digests + .get(contribution.result_current_id() as usize) + .ok_or_else(|| integrity("established contribution result current is absent"))?; + let parents = [ + *current_digests + .get(*parent0 as usize) + .ok_or_else(|| integrity("established contribution parent is absent"))?, + *current_digests + .get(*parent1 as usize) + .ok_or_else(|| integrity("established contribution parent is absent"))?, + ]; + rows.push(contribution_proof_digest( + result, + parents, + contribution.key(), + contribution.exact_factor(), + )?); + } + multiset_digest(b"pyamplicol-on-the-fly-contribution-multiset-v1\0", rows) +} + +fn closure_parity_row_digest( + parent_digests: [SemanticDigest; 2], + closure_template_id: u32, + factor: ExactComplexRational, +) -> RusticolResult { + let mut hash = Sha256::new(); + hash.update(b"pyamplicol-on-the-fly-closure-parity-row-v1\0"); + hash_digest(&mut hash, parent_digests[0]); + hash_digest(&mut hash, parent_digests[1]); + hash.update(closure_template_id.to_le_bytes()); + hash_exact(&mut hash, factor); + final_digest(hash) +} + +fn compact_closure_parity_digest( + trace: &OnTheFlyStructuralTraceV1, +) -> RusticolResult { + let mut current_by_component_base = BTreeMap::new(); + for (index, range) in trace.current_component_ranges.iter().copied().enumerate() { + current_by_component_base.insert(range[0], trace.current_semantic_digests[index]); + } + let mut rows = Vec::new(); + for operation in trace.operations.iter() { + let OnTheFlyTraceOperationV1::Closure { key, row } = operation else { + continue; + }; + let closure_template_id = key.operation_id().ok_or_else(|| { + integrity("compact closure executor has no closure-template identity") + })?; + let factor = *trace + .exact_factors + .get(row.exact_factor_id as usize) + .ok_or_else(|| integrity("compact closure factor is absent"))?; + rows.push(closure_parity_row_digest( + [ + *current_by_component_base + .get(&row.parent0_component_base) + .ok_or_else(|| integrity("compact closure parent 0 is absent"))?, + *current_by_component_base + .get(&row.parent1_component_base_or_sentinel) + .ok_or_else(|| integrity("compact closure parent 1 is absent"))?, + ], + closure_template_id, + factor, + )?); + } + multiset_digest(b"pyamplicol-on-the-fly-closure-parity-multiset-v1\0", rows) +} + +fn established_closure_parity_digest( + program: &RecurrenceProgram, + current_digests: &[SemanticDigest], +) -> RusticolResult { + let mut rows = Vec::new(); + for closure in program.closure_terms() { + let [parent0, parent1] = closure.parent_current_ids() else { + return Err(integrity( + "established selected-query closure is not binary", + )); + }; + rows.push(closure_parity_row_digest( + [ + *current_digests + .get(*parent0 as usize) + .ok_or_else(|| integrity("established closure parent 0 is absent"))?, + *current_digests + .get(*parent1 as usize) + .ok_or_else(|| integrity("established closure parent 1 is absent"))?, + ], + closure.closure_template_id(), + closure.exact_factor(), + )?); + } + multiset_digest(b"pyamplicol-on-the-fly-closure-parity-multiset-v1\0", rows) +} + +fn is_negative_one(value: ExactComplexRational) -> RusticolResult { + Ok(value == ExactComplexRational::ONE.checked_neg()?) +} + +#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] +struct PairingOracleRow { + endpoint_pairs: Vec<[u32; 2]>, + source_slot_permutation: Vec, + source_lineage: Vec, + fermion_parity: i32, +} + +fn pairing_oracle_equal( + authenticated: &AuthenticatedRecurrenceBuilderInput, + owners: &[ResolvedPairingOwnerV1], + rules: &[Option], +) -> RusticolResult<(bool, Vec, Vec)> { + let mut compact = owners + .iter() + .map(|owner| PairingOracleRow { + endpoint_pairs: owner.endpoint_pairs.clone(), + source_slot_permutation: owner.source_slot_permutation.clone(), + source_lineage: owner.source_lineage.clone(), + fermion_parity: owner.fermion_parity, + }) + .collect::>(); + let source_count = authenticated.process().input().external_legs.len(); + let identity = (0..source_count) + .map(|value| checked_u32(value, "identity source slot")) + .collect::>>()?; + let catalog = authenticated.process().fermion_pairing_catalog(); + let mut established = rules + .iter() + .copied() + .map(|rule| { + let Some(rule) = rule else { + return Ok(PairingOracleRow { + endpoint_pairs: Vec::new(), + source_slot_permutation: identity.clone(), + source_lineage: vec![MISSING_U32; source_count], + fermion_parity: 1, + }); + }; + let catalog = + catalog.ok_or_else(|| integrity("established pairing rule has no catalog"))?; + let mut endpoint_pairs = catalog + .endpoint_pairings(rule)? + .iter() + .map(|pair| { + [ + pair.fundamental_source_slot, + pair.antifundamental_source_slot, + ] + }) + .collect::>(); + endpoint_pairs.sort_unstable(); + Ok(PairingOracleRow { + endpoint_pairs, + source_slot_permutation: catalog.source_slot_permutation(rule)?.to_vec(), + source_lineage: catalog.lineage(rule)?.to_vec(), + fermion_parity: rule.fermion_parity, + }) + }) + .collect::>>()?; + compact.sort_unstable(); + established.sort_unstable(); + let compact_parities = compact.iter().map(|row| row.fermion_parity).collect(); + let established_parities = established.iter().map(|row| row.fermion_parity).collect(); + Ok(( + compact == established, + compact_parities, + established_parities, + )) +} + +pub(crate) fn build_on_the_fly_selected_trace_v1( + authenticated: &AuthenticatedRecurrenceBuilderInput, + direct: &PreparedDirectExecutorCatalog, + selected_public_flow_id: u32, + public_helicities: &[i32], + enable_projection: bool, +) -> RusticolResult { + let oracle = one_public_row_oracle(authenticated, selected_public_flow_id, public_helicities)?; + let seed = compact_seed( + authenticated, + direct.direct_template_catalog_digest(), + oracle.external_permutation.clone(), + )?; + let query = DecodedLcQueryV1::new( + &seed, + oracle.external_permutation, + &oracle.public_helicities, + oracle.selector, + )?; + let selected = build_selected_lc_query_trace_for_probe_v1( + authenticated.template(), + direct, + &seed, + query, + enable_projection, + false, + )?; + Ok(OnTheFlySelectedTraceV1 { + seed, + query: selected.query, + trace: selected.trace, + projection: selected.projection, + }) +} + +fn reconstruct_seed_in_projection_domain( + retained_seed: OnTheFlyProcessSeedV1, + artifact_seed: &OnTheFlyProcessSeedV1, +) -> RusticolResult { + let OnTheFlyProcessSeedV1 { + process_digest, + model_digest, + template_catalog_digest, + prepared_pack_digest, + direct_catalog_digest, + normalization_semantic_digest, + normalization_convention, + source_anchors, + external_permutation: _, + coupling_order_policy: _, + coupling_hierarchies: _, + coupling_limits: _, + pairing_classes, + semantic_digest: _, + } = retained_seed; + let reconstructed = OnTheFlyProcessSeedV1::new( + process_digest, + model_digest, + template_catalog_digest, + prepared_pack_digest, + direct_catalog_digest, + normalization_semantic_digest, + normalization_convention, + ExactComplexRational::ONE, + source_anchors.into_vec(), + artifact_seed.external_permutation().to_vec(), + artifact_seed.coupling_order_policy(), + artifact_seed.coupling_hierarchies().to_vec(), + artifact_seed.explicit_coupling_limits().to_vec(), + pairing_classes.into_vec(), + )?; + if reconstructed.semantic_digest() != artifact_seed.semantic_digest() { + return Err(integrity( + "retained diagnostic semantic constituents do not authenticate in the on-the-fly artifact seed projection domain", + )); + } + Ok(reconstructed) +} + +/// Build one retained recurrence row against an already-authenticated compact +/// artifact seed. The retained input supplies semantic constituents and the +/// public selector oracle; projection policy remains owned by the artifact. +pub(crate) fn build_on_the_fly_selected_trace_against_seed_v1( + authenticated: &AuthenticatedRecurrenceBuilderInput, + direct: &PreparedDirectExecutorCatalog, + artifact_seed: &OnTheFlyProcessSeedV1, + selected_public_flow_id: u32, + public_helicities: &[i32], + enable_projection: bool, + enable_cyclic_trace_reflection: bool, +) -> RusticolResult { + let oracle = one_public_row_oracle(authenticated, selected_public_flow_id, public_helicities)?; + let retained_seed = compact_seed( + authenticated, + direct.direct_template_catalog_digest(), + oracle.external_permutation, + )?; + let _authenticated = reconstruct_seed_in_projection_domain(retained_seed, artifact_seed)?; + let query = DecodedLcQueryV1::new( + artifact_seed, + artifact_seed.external_permutation().to_vec(), + &oracle.public_helicities, + oracle.selector, + )?; + let selected = build_selected_lc_query_trace_for_probe_v1( + authenticated.template(), + direct, + artifact_seed, + query, + enable_projection, + enable_cyclic_trace_reflection, + )?; + Ok(OnTheFlySelectedTraceV1 { + seed: artifact_seed.clone(), + query: selected.query, + trace: selected.trace, + projection: selected.projection, + }) +} + +fn observed_transition_build( + selection: Option, + build: impl FnOnce() -> RusticolResult, +) -> RusticolResult<( + T, + Vec, + Option>, +)> { + crate::recurrence::diagnostic::begin_transition_diagnostic_observation(selection)?; + let built = build(); + let observed = crate::recurrence::diagnostic::take_transition_diagnostic_observation(); + match (built, observed) { + (Ok(value), Ok((rows, live))) => Ok((value, rows, live)), + (Err(error), _) => Err(error), + (Ok(_), Err(error)) => Err(error), + } +} + +/// Build one compact selected query and compare it with the established +/// materialized builder. This API exists only under `on-the-fly-test-support`. +#[doc(hidden)] +pub fn on_the_fly_test_support_probe_v1( + authenticated: &AuthenticatedRecurrenceBuilderInput, + direct: &PreparedDirectExecutorCatalog, + selected_public_flow_id: u32, + public_helicities: &[i32], +) -> RusticolResult { + let (compact_pre_projection, compact_transition_candidates, _) = + observed_transition_build(None, || { + build_on_the_fly_selected_trace_v1( + authenticated, + direct, + selected_public_flow_id, + public_helicities, + false, + ) + })?; + let compact_live = compact_pre_projection + .trace + .current_semantic_digests + .iter() + .copied() + .collect::>(); + let compact_transition_candidates = compact_transition_candidates + .into_iter() + .filter(|row| { + compact_live.contains(&row.output_current_digest) + && row + .ordered_parent_digests + .iter() + .all(|digest| compact_live.contains(digest)) + }) + .collect::>(); + let selected = build_on_the_fly_selected_trace_v1( + authenticated, + direct, + selected_public_flow_id, + public_helicities, + true, + )?; + let OnTheFlySelectedTraceV1 { + seed, + query, + trace, + projection: _, + } = selected; + crate::recurrence::construct::begin_established_pairing_owner_observation(); + let diagnostic_selection = crate::recurrence::diagnostic::ConstructionDiagnosticSelectionV1 { + public_flow_id: selected_public_flow_id, + public_helicities: public_helicities.to_vec(), + }; + let (established, established_transition_candidates, established_live) = + observed_transition_build(Some(diagnostic_selection), || authenticated.build())?; + let established_live = established_live.ok_or_else(|| { + integrity("established transition diagnostic did not record its selected live slice") + })?; + let established_materialized_sector_id = established + .replay_targets() + .iter() + .find(|target| target.target_sector_id() == selected_public_flow_id) + .map(crate::recurrence::RecurrenceReplayTarget::materialized_sector_id) + .ok_or_else(|| { + integrity(format!( + "established transition diagnostic public flow {selected_public_flow_id} has no replay target" + )) + })?; + let established_transition_candidates = + crate::recurrence::diagnostic::retain_materialized_sector_rows( + established_transition_candidates, + established_materialized_sector_id, + |row| row.materialized_sector_id, + ) + .into_iter() + .filter(|row| { + established_live.contains(&row.output_current_digest) + && row + .ordered_parent_digests + .iter() + .all(|digest| established_live.contains(digest)) + }) + .collect::>(); + let established_pairing_rules = + crate::recurrence::construct::take_established_pairing_owner_observation()? + .into_iter() + .map(|rule_id| established_pairing_rule_by_id(authenticated, rule_id)) + .collect::>>()?; + let established_current_rows = established_current_digests(&established)?; + let established_current_multiset_digest = multiset_digest( + b"pyamplicol-on-the-fly-current-multiset-v1\0", + established_current_rows.clone(), + )?; + let established_contribution_multiset_digest = + established_contribution_digest(&established, &established_current_rows)?; + let closure_parity_multiset_digest = compact_closure_parity_digest(&trace)?; + let established_closure_parity_multiset_digest = + established_closure_parity_digest(&established, &established_current_rows)?; + let negative_contribution_factor_count = checked_u32( + trace + .operations + .iter() + .filter_map(|operation| match operation { + OnTheFlyTraceOperationV1::Contribution { row, .. } => trace + .exact_factors + .get(row.exact_factor_id as usize) + .copied(), + _ => None, + }) + .map(is_negative_one) + .collect::>>()? + .into_iter() + .filter(|negative| *negative) + .count(), + "compact negative contribution count", + )?; + let established_negative_contribution_factor_count = checked_u32( + established + .contributions() + .iter() + .map(|row| is_negative_one(row.exact_factor())) + .collect::>>()? + .into_iter() + .filter(|negative| *negative) + .count(), + "established negative contribution count", + )?; + let (pairing_oracle_equal, pairing_fermion_parities, established_pairing_fermion_parities) = + pairing_oracle_equal( + authenticated, + &trace.pairing_owners, + &established_pairing_rules, + )?; + let one = OnTheFlyWorkspaceV1::new(&trace, 1)?; + let many = OnTheFlyWorkspaceV1::new(&trace, 17)?; + let workspace_capacity_independent = one.point_stride() != many.point_stride(); + let proof = trace.proof(); + Ok(OnTheFlyTestSupportReportV1 { + seed_digest: seed.semantic_digest(), + query_digest: query.semantic_digest(), + selector_digest: query.selector_digest, + trace_digest: trace.semantic_digest(), + current_count: proof.current_count(), + contribution_count: proof.contribution_count(), + closure_count: proof.closure_count(), + established_current_count: checked_u32( + established.currents().len(), + "established current count", + )?, + established_contribution_count: checked_u32( + established.contributions().len(), + "established contribution count", + )?, + established_closure_count: checked_u32( + established.closure_terms().len(), + "established closure count", + )?, + current_multiset_digest: proof.current_multiset_digest(), + established_current_multiset_digest, + contribution_multiset_digest: proof.contribution_multiset_digest(), + established_contribution_multiset_digest, + closure_multiset_digest: proof.closure_multiset_digest(), + closure_parity_multiset_digest, + established_closure_parity_multiset_digest, + negative_contribution_factor_count, + established_negative_contribution_factor_count, + source_domain_equal: compact_source_domain(&seed) + == established_source_domain(authenticated, &established)?, + pairing_oracle_equal, + pairing_fermion_parities, + established_pairing_fermion_parities, + workspace_capacity_independent, + compact_transition_candidates, + established_transition_candidates, + }) +} + +#[cfg(test)] +mod source_domain_tests { + use super::*; + + fn digest(value: u8) -> SemanticDigest { + SemanticDigest::new([value; 32]).unwrap() + } + + #[allow(clippy::too_many_arguments)] + fn rebuild_test_seed( + seed: OnTheFlyProcessSeedV1, + external_permutation: Vec, + coupling_order_policy: OnTheFlyCouplingOrderPolicyV1, + coupling_hierarchies: Vec, + coupling_limits: Vec>, + normalization_semantic_digest: Option, + source_semantic_digest: Option, + ) -> OnTheFlyProcessSeedV1 { + let OnTheFlyProcessSeedV1 { + process_digest, + model_digest, + template_catalog_digest, + prepared_pack_digest, + direct_catalog_digest, + normalization_semantic_digest: original_normalization_semantic_digest, + normalization_convention, + source_anchors, + external_permutation: _, + coupling_order_policy: _, + coupling_hierarchies: _, + coupling_limits: _, + pairing_classes, + semantic_digest: _, + } = seed; + let mut source_anchors = source_anchors.into_vec(); + if let Some(source_semantic_digest) = source_semantic_digest { + source_anchors[0].states[0].source_semantic_digest = source_semantic_digest; + } + OnTheFlyProcessSeedV1::new( + process_digest, + model_digest, + template_catalog_digest, + prepared_pack_digest, + direct_catalog_digest, + normalization_semantic_digest.unwrap_or(original_normalization_semantic_digest), + normalization_convention, + ExactComplexRational::ONE, + source_anchors, + external_permutation, + coupling_order_policy, + coupling_hierarchies, + coupling_limits, + pairing_classes.into_vec(), + ) + .unwrap() + } + + fn projection_domain_fixture( + normalization_semantic_digest: Option, + source_semantic_digest: Option, + ) -> (OnTheFlyProcessSeedV1, OnTheFlyProcessSeedV1) { + let base = scalar_adapter_test_seed(digest(1), digest(2), digest(3), digest(4)).unwrap(); + let artifact_seed = rebuild_test_seed( + base.clone(), + vec![0, 1], + OnTheFlyCouplingOrderPolicyV1::Minimal, + vec![1, 2], + vec![None, None], + None, + None, + ); + let retained_seed = rebuild_test_seed( + base, + vec![1, 0], + OnTheFlyCouplingOrderPolicyV1::Explicit, + vec![1, 1], + vec![Some(4), Some(0)], + normalization_semantic_digest, + source_semantic_digest, + ); + (artifact_seed, retained_seed) + } + + fn source_state( + state_index: u32, + public_helicity: i32, + current_state_template_id: u32, + source_template_id: u32, + ) -> ProcessSourceStateRow { + ProcessSourceStateRow { + source_slot: 0, + state_index, + public_helicity, + chirality: public_helicity, + spin_state: public_helicity, + current_state_template_id, + source_template_id, + momentum_sign: 1, + crossing_phase_factor_id: 0, + } + } + + #[test] + fn established_source_domain_resolves_each_valid_state_by_its_own_template() { + let states = [source_state(0, -1, 16, 57), source_state(1, 1, 76, 23)]; + let negative = established_source_state_for_template(0, &states, 57).unwrap(); + let positive = established_source_state_for_template(0, &states, 23).unwrap(); + assert_eq!( + (negative.state_index, negative.current_state_template_id), + (0, 16) + ); + assert_eq!( + (positive.state_index, positive.current_state_template_id), + (1, 76) + ); + } + + #[test] + fn minimal_process_global_artifact_authenticates_explicit_flow_local_oracle_in_artifact_domain() + { + let (artifact_seed, retained_seed) = projection_domain_fixture(None, None); + assert_ne!( + retained_seed.semantic_digest(), + artifact_seed.semantic_digest() + ); + let oracle = OnePublicRowOracle { + external_permutation: retained_seed.external_permutation().to_vec(), + public_helicities: vec![0, 0], + selector: OnTheFlyLcSelectorV1::Singlet, + }; + assert!( + DecodedLcQueryV1::new( + &artifact_seed, + oracle.external_permutation.clone(), + &oracle.public_helicities, + oracle.selector.clone(), + ) + .is_err() + ); + let reconstructed = + reconstruct_seed_in_projection_domain(retained_seed, &artifact_seed).unwrap(); + assert_eq!( + reconstructed.semantic_digest(), + artifact_seed.semantic_digest() + ); + let query = DecodedLcQueryV1::new( + &artifact_seed, + artifact_seed.external_permutation().to_vec(), + &oracle.public_helicities, + oracle.selector, + ) + .unwrap(); + assert_eq!(query.process_seed_digest(), artifact_seed.semantic_digest()); + } + + #[test] + fn projection_domain_reconstruction_rejects_mutated_semantic_constituents() { + for (normalization_semantic_digest, source_semantic_digest) in + [(Some(digest(93)), None), (None, Some(digest(94)))] + { + let (artifact_seed, retained_seed) = + projection_domain_fixture(normalization_semantic_digest, source_semantic_digest); + let error = + reconstruct_seed_in_projection_domain(retained_seed, &artifact_seed).unwrap_err(); + assert!( + error + .to_string() + .contains("semantic constituents do not authenticate") + ); + } + } +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/recurrence/on_the_fly/trace.rs b/pyamplicol/rust/crates/rusticol-core/src/recurrence/on_the_fly/trace.rs new file mode 100644 index 0000000000..487a1693ca --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/recurrence/on_the_fly/trace.rs @@ -0,0 +1,2196 @@ +// SPDX-License-Identifier: 0BSD + +#![allow(dead_code)] // Query-local proof inspection is retained for opt-in support builds. + +use super::sweep::*; +use super::*; + +#[cfg(feature = "on-the-fly-test-support")] +pub(crate) const ON_THE_FLY_WORK_CENSUS_BASIS_V1: &str = "fully-resident-query-local-trace-v1"; + +/// Authenticated model operation addressed by one semantic executor key. +#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, bincode::Encode, bincode::Decode)] +#[repr(u8)] +pub(crate) enum OnTheFlyOperationKindV1 { + Source = 0, + Transition = 1, + Propagator = 2, + Closure = 3, + IdentityFinalizer = 4, +} + +/// Plan-independent semantic address of one prepared Direct-Arena executor. +#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, bincode::Encode, bincode::Decode)] +pub(crate) enum OnTheFlyExecutorKeyV1 { + PreparedOperation { + direct_catalog_digest: SemanticDigest, + role: DirectExecutorRole, + operation_kind: OnTheFlyOperationKindV1, + operation_id: u32, + operation_semantic_digest: SemanticDigest, + evaluator_binding_id: u32, + evaluator_binding_semantic_digest: SemanticDigest, + }, + /// The prepared direct catalog authenticates this one typed synthetic + /// binding as a whole. It has no recurrence-operation or evaluator row, + /// so no sentinel semantic digests are fabricated for it. + IdentityFinalizer { + direct_catalog_digest: SemanticDigest, + }, +} + +impl OnTheFlyExecutorKeyV1 { + #[allow(clippy::too_many_arguments)] + fn new( + direct_catalog_digest: SemanticDigest, + role: DirectExecutorRole, + operation_kind: OnTheFlyOperationKindV1, + operation_id: u32, + operation_semantic_digest: SemanticDigest, + evaluator_binding_id: u32, + evaluator_binding_semantic_digest: SemanticDigest, + ) -> RusticolResult { + let expected_role = match operation_kind { + OnTheFlyOperationKindV1::Source => DirectExecutorRole::Source, + OnTheFlyOperationKindV1::Transition => DirectExecutorRole::Contribution, + OnTheFlyOperationKindV1::Propagator => DirectExecutorRole::Finalization, + OnTheFlyOperationKindV1::Closure => DirectExecutorRole::Closure, + OnTheFlyOperationKindV1::IdentityFinalizer => { + return Err(invalid( + "identity finalizer must use its typed semantic key", + )); + } + }; + if role != expected_role { + return Err(invalid("semantic executor key has an inconsistent role")); + } + Ok(Self::PreparedOperation { + direct_catalog_digest, + role, + operation_kind, + operation_id, + operation_semantic_digest, + evaluator_binding_id, + evaluator_binding_semantic_digest, + }) + } + + pub(crate) const fn identity_finalizer(direct_catalog_digest: SemanticDigest) -> Self { + Self::IdentityFinalizer { + direct_catalog_digest, + } + } + + pub(crate) const fn direct_catalog_digest(self) -> SemanticDigest { + match self { + Self::PreparedOperation { + direct_catalog_digest, + .. + } + | Self::IdentityFinalizer { + direct_catalog_digest, + } => direct_catalog_digest, + } + } + + pub(crate) const fn role(self) -> DirectExecutorRole { + match self { + Self::PreparedOperation { role, .. } => role, + Self::IdentityFinalizer { .. } => DirectExecutorRole::Finalization, + } + } + + pub(crate) const fn operation_kind(self) -> OnTheFlyOperationKindV1 { + match self { + Self::PreparedOperation { operation_kind, .. } => operation_kind, + Self::IdentityFinalizer { .. } => OnTheFlyOperationKindV1::IdentityFinalizer, + } + } + + pub(crate) const fn operation_id(self) -> Option { + match self { + Self::PreparedOperation { operation_id, .. } => Some(operation_id), + Self::IdentityFinalizer { .. } => None, + } + } + + pub(crate) const fn operation_semantic_digest(self) -> Option { + match self { + Self::PreparedOperation { + operation_semantic_digest, + .. + } => Some(operation_semantic_digest), + Self::IdentityFinalizer { .. } => None, + } + } + + pub(crate) const fn evaluator_binding_id(self) -> Option { + match self { + Self::PreparedOperation { + evaluator_binding_id, + .. + } => Some(evaluator_binding_id), + Self::IdentityFinalizer { .. } => None, + } + } + + pub(crate) const fn evaluator_binding_semantic_digest(self) -> Option { + match self { + Self::PreparedOperation { + evaluator_binding_semantic_digest, + .. + } => Some(evaluator_binding_semantic_digest), + Self::IdentityFinalizer { .. } => None, + } + } +} + +#[derive(Clone, Debug)] +pub(super) enum OnTheFlyTraceOperationV1 { + Source { + key: OnTheFlyExecutorKeyV1, + row: DirectSourceRow, + }, + Contribution { + key: OnTheFlyExecutorKeyV1, + row: DirectContributionRow, + }, + Finalization { + key: OnTheFlyExecutorKeyV1, + row: DirectFinalizationRow, + }, + Closure { + key: OnTheFlyExecutorKeyV1, + row: DirectClosureRow, + }, +} + +/// Cold semantic contribution row retained only by the development feature. +/// +/// Direct rows carry trace-local component bases and, after prepared binding, +/// may use a physical parent permutation. A query-family union instead needs +/// the exact construction identity before those physical choices. Keeping +/// this row beside the trace lets the prototype remap parent current IDs +/// without materializing an established recurrence program or DirectPlan. +#[derive(Clone, Debug, Eq, PartialEq)] +pub(super) struct OnTheFlyTraceContributionProofRowV1 { + pub(super) result_current_id: u32, + pub(super) parent_current_ids: [u32; 2], + pub(super) key: ContributionKey, + pub(super) exact_factor: ExactComplexRational, +} + +impl OnTheFlyTraceOperationV1 { + pub(super) const fn key(&self) -> OnTheFlyExecutorKeyV1 { + match self { + Self::Source { key, .. } + | Self::Contribution { key, .. } + | Self::Finalization { key, .. } + | Self::Closure { key, .. } => *key, + } + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) struct OnTheFlyWorkspaceLayoutV1 { + pub(super) source_count: u32, + pub(super) lorentz_component_count: u16, + pub(super) parameter_count: u32, + pub(super) current_component_count: u32, + pub(super) amplitude_component_count: u32, + pub(super) momentum_form_count: u32, + pub(super) exact_factor_count: u32, +} + +impl OnTheFlyWorkspaceLayoutV1 { + pub(crate) const fn source_count(self) -> u32 { + self.source_count + } + + pub(crate) const fn current_component_count(self) -> u32 { + self.current_component_count + } + + pub(crate) const fn amplitude_component_count(self) -> u32 { + self.amplitude_component_count + } + + #[cfg(feature = "on-the-fly-test-support")] + pub(crate) const fn lorentz_component_count(self) -> u16 { + self.lorentz_component_count + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) struct OnTheFlyStructuralProofV1 { + current_count: u32, + contribution_count: u32, + closure_count: u32, + constructed_current_count: u32, + constructed_contribution_count: u32, + current_multiset_digest: SemanticDigest, + contribution_multiset_digest: SemanticDigest, + closure_multiset_digest: SemanticDigest, + owner_digest: SemanticDigest, + semantic_digest: SemanticDigest, +} + +/// Warming-independent work represented by one selected-query trace. +/// +/// The current arena is fully resident for the lifetime of the trace. These +/// current counts therefore describe allocated resident work, not a temporal +/// liveness estimate. Operation counts describe one execution of the trace; +/// the probe must not multiply them by warm-up or benchmark repetitions. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[cfg(any(test, feature = "on-the-fly-test-support"))] +pub(crate) struct OnTheFlyExecutionWorkCensusV1 { + pub(crate) logical_current_count: u32, + pub(crate) resident_current_count: u32, + pub(crate) resident_current_component_count: u32, + pub(crate) source_operation_count: u32, + pub(crate) contribution_operation_count: u32, + pub(crate) finalization_operation_count: u32, + pub(crate) closure_operation_count: u32, + pub(crate) total_kernel_application_count: u32, +} + +impl OnTheFlyStructuralProofV1 { + pub(crate) const fn current_count(self) -> u32 { + self.current_count + } + + pub(crate) const fn contribution_count(self) -> u32 { + self.contribution_count + } + + pub(crate) const fn closure_count(self) -> u32 { + self.closure_count + } + + pub(crate) const fn semantic_digest(self) -> SemanticDigest { + self.semantic_digest + } + + #[cfg(any(test, feature = "on-the-fly-test-support"))] + pub(super) const fn current_multiset_digest(self) -> SemanticDigest { + self.current_multiset_digest + } + + #[cfg(any(test, feature = "on-the-fly-test-support"))] + pub(super) const fn contribution_multiset_digest(self) -> SemanticDigest { + self.contribution_multiset_digest + } + + #[cfg(any(test, feature = "on-the-fly-test-support"))] + pub(super) const fn closure_multiset_digest(self) -> SemanticDigest { + self.closure_multiset_digest + } +} + +/// Immutable selected-query trace. Exact current keys remain available for +/// development parity joins; numeric IDs are deliberately query-local. +#[derive(Debug)] +pub(crate) struct OnTheFlyStructuralTraceV1 { + pub(super) seed_digest: SemanticDigest, + pub(super) query_digest: SemanticDigest, + pub(super) current_keys: Box<[CurrentCoreKey]>, + pub(super) current_colors: Box<[DynamicLCColorState]>, + pub(super) operations: Box<[OnTheFlyTraceOperationV1]>, + pub(super) momentum_forms: Box<[CanonicalMomentumLinearForm]>, + pub(super) exact_factors: Box<[ExactComplexRational]>, + pub(super) pairing_owners: Box<[ResolvedPairingOwnerV1]>, + pub(super) layout: OnTheFlyWorkspaceLayoutV1, + pub(super) proof: OnTheFlyStructuralProofV1, + pub(super) semantic_digest: SemanticDigest, + prepared_executor_rows_bound: bool, + pub(super) current_component_ranges: Box<[[u32; 2]]>, + pub(super) current_semantic_digests: Box<[SemanticDigest]>, + pub(super) contribution_proof_rows: Box<[OnTheFlyTraceContributionProofRowV1]>, +} + +impl OnTheFlyStructuralTraceV1 { + pub(crate) fn executor_keys(&self) -> impl Iterator + '_ { + self.operations.iter().map(OnTheFlyTraceOperationV1::key) + } + + /// Bind authenticated prepared-executor parent order into the trace rows + /// once, before any executor can cache descriptors for their addresses. + /// The semantic trace identity is unchanged: this is a private physical + /// executor binding, not a different selected recurrence graph. + pub(crate) fn bind_prepared_executor_rows( + &mut self, + parent_permutations: &BTreeMap, + ) -> RusticolResult<()> { + if self.prepared_executor_rows_bound { + return Err(integrity( + "prepared executor parent order is already bound into the trace", + )); + } + + let mut contribution_permutations = Vec::new(); + for (operation_index, operation) in self.operations.iter().enumerate() { + let key = operation.key(); + let permutation = parent_permutations.get(&key).copied().ok_or_else(|| { + integrity("trace operation has no authenticated prepared parent order") + })?; + match operation { + OnTheFlyTraceOperationV1::Contribution { .. } => match permutation { + [0, 1] | [1, 0] => { + contribution_permutations.push((operation_index, permutation)); + } + _ => { + return Err(integrity( + "prepared contribution executor has a non-binary parent order", + )); + } + }, + OnTheFlyTraceOperationV1::Source { .. } + | OnTheFlyTraceOperationV1::Finalization { .. } + | OnTheFlyTraceOperationV1::Closure { .. } => { + if permutation != [0, 1] { + return Err(integrity( + "non-contribution executor unexpectedly permutes parent rows", + )); + } + } + } + } + + // The first pass made every remaining operation infallible. Mutate the + // existing boxed rows in place so their stable storage is preserved. + for (operation_index, permutation) in contribution_permutations { + if permutation == [0, 1] { + continue; + } + let Some(OnTheFlyTraceOperationV1::Contribution { row, .. }) = + self.operations.get_mut(operation_index) + else { + unreachable!("validated contribution operation changed during parent binding"); + }; + std::mem::swap( + &mut row.parent0_component_base, + &mut row.parent1_component_base_or_sentinel, + ); + std::mem::swap( + &mut row.parent0_momentum_form_id, + &mut row.parent1_momentum_form_id_or_sentinel, + ); + } + self.prepared_executor_rows_bound = true; + Ok(()) + } + + pub(crate) const fn prepared_executor_rows_bound(&self) -> bool { + self.prepared_executor_rows_bound + } + + pub(crate) const fn proof(&self) -> OnTheFlyStructuralProofV1 { + self.proof + } + + pub(crate) const fn seed_digest(&self) -> SemanticDigest { + self.seed_digest + } + + pub(crate) fn current_keys(&self) -> &[CurrentCoreKey] { + &self.current_keys + } + + pub(crate) const fn layout(&self) -> OnTheFlyWorkspaceLayoutV1 { + self.layout + } + + #[cfg(any(test, feature = "on-the-fly-test-support"))] + pub(crate) fn execution_work_census(&self) -> RusticolResult { + let mut source_operation_count = 0_u32; + let mut contribution_operation_count = 0_u32; + let mut finalization_operation_count = 0_u32; + let mut closure_operation_count = 0_u32; + for operation in &self.operations { + let count = match operation.key().role() { + DirectExecutorRole::Source => &mut source_operation_count, + DirectExecutorRole::Contribution => &mut contribution_operation_count, + DirectExecutorRole::Finalization => &mut finalization_operation_count, + DirectExecutorRole::Closure => &mut closure_operation_count, + }; + *count = count + .checked_add(1) + .ok_or_else(|| invalid("execution operation count overflows u32"))?; + } + let total_kernel_application_count = checked_u32( + self.operations.len(), + "selected-query kernel application count", + )?; + let counted = source_operation_count + .checked_add(contribution_operation_count) + .and_then(|value| value.checked_add(finalization_operation_count)) + .and_then(|value| value.checked_add(closure_operation_count)) + .ok_or_else(|| invalid("execution operation total overflows u32"))?; + if counted != total_kernel_application_count { + return Err(integrity( + "execution operation census does not cover the selected-query trace", + )); + } + let logical_current_count = self.proof.current_count(); + Ok(OnTheFlyExecutionWorkCensusV1 { + logical_current_count, + resident_current_count: logical_current_count, + resident_current_component_count: self.layout.current_component_count(), + source_operation_count, + contribution_operation_count, + finalization_operation_count, + closure_operation_count, + total_kernel_application_count, + }) + } + + pub(crate) fn momentum_forms(&self) -> &[CanonicalMomentumLinearForm] { + &self.momentum_forms + } + + pub(crate) fn exact_factors(&self) -> &[ExactComplexRational] { + &self.exact_factors + } + + pub(crate) const fn semantic_digest(&self) -> SemanticDigest { + self.semantic_digest + } + + #[cfg(feature = "on-the-fly-test-support")] + pub(crate) fn tamper_first_prepared_executor_key_for_probe(&mut self) -> RusticolResult<()> { + let replacement = SemanticDigest::new([0xa5; 32])?; + for operation in self.operations.iter_mut() { + let key = operation.key(); + let OnTheFlyExecutorKeyV1::PreparedOperation { + direct_catalog_digest, + role, + operation_kind, + operation_id, + operation_semantic_digest, + evaluator_binding_id, + evaluator_binding_semantic_digest, + } = key + else { + continue; + }; + let replacement = if replacement != operation_semantic_digest { + replacement + } else { + SemanticDigest::new([0x5a; 32])? + }; + let tampered = OnTheFlyExecutorKeyV1::PreparedOperation { + direct_catalog_digest, + role, + operation_kind, + operation_id, + operation_semantic_digest: replacement, + evaluator_binding_id, + evaluator_binding_semantic_digest, + }; + match operation { + OnTheFlyTraceOperationV1::Source { key, .. } + | OnTheFlyTraceOperationV1::Contribution { key, .. } + | OnTheFlyTraceOperationV1::Finalization { key, .. } + | OnTheFlyTraceOperationV1::Closure { key, .. } => *key = tampered, + } + return Ok(()); + } + Err(integrity( + "selected trace has no prepared executor key to tamper", + )) + } + + #[cfg(any(test, feature = "on-the-fly-test-support"))] + pub(crate) fn current_component_range(&self, current_id: u32) -> RusticolResult<[u32; 2]> { + self.current_component_ranges + .get(current_id as usize) + .copied() + .ok_or_else(|| invalid("observed current ID is outside the trace")) + } + + #[cfg(any(test, feature = "on-the-fly-test-support"))] + pub(crate) fn current_semantic_digest( + &self, + current_id: u32, + ) -> RusticolResult { + self.current_semantic_digests + .get(current_id as usize) + .copied() + .ok_or_else(|| invalid("observed current ID is outside the trace")) + } +} + +/// Join one query-local operation key to the exact authenticated recurrence +/// row and prepared direct-template binding that implements it. No global +/// operation map or direct recurrence plan participates in the address. +pub(crate) fn authenticated_prepared_executor_binding<'a>( + templates: &'a ValidatedRecurrenceTemplateInput, + direct: &'a PreparedDirectExecutorCatalog, +) -> RusticolResult< + impl Fn(OnTheFlyExecutorKeyV1) -> RusticolResult + 'a, +> { + let direct_digest = direct.direct_template_catalog_digest(); + Ok(move |key: OnTheFlyExecutorKeyV1| { + if key.direct_catalog_digest() != direct_digest { + return Err(integrity( + "operation key belongs to a different direct-template catalog", + )); + } + if matches!(key, OnTheFlyExecutorKeyV1::IdentityFinalizer { .. }) { + return Ok(PreparedDirectExecutorBinding::identity_finalizer( + direct.resolve_identity_finalizer()?, + )); + } + let operation_id = key + .operation_id() + .ok_or_else(|| integrity("prepared operation key has no operation ID"))?; + let (row_id, semantic_digest_id, evaluator_binding_id, expected_role, label) = + match key.operation_kind() { + OnTheFlyOperationKindV1::Source => { + let row = templates + .input() + .sources + .get(operation_id as usize) + .ok_or_else(|| integrity("source operation row is absent"))?; + ( + row.id, + row.semantic_digest_id, + row.evaluator_binding_id, + DirectExecutorRole::Source, + "source", + ) + } + OnTheFlyOperationKindV1::Transition => { + let row = templates + .input() + .transitions + .get(operation_id as usize) + .ok_or_else(|| integrity("transition operation row is absent"))?; + ( + row.id, + row.semantic_digest_id, + row.evaluator_binding_id, + DirectExecutorRole::Contribution, + "transition", + ) + } + OnTheFlyOperationKindV1::Propagator => { + let row = templates + .input() + .propagators + .get(operation_id as usize) + .ok_or_else(|| integrity("propagator operation row is absent"))?; + ( + row.id, + row.semantic_digest_id, + row.evaluator_binding_id, + DirectExecutorRole::Finalization, + "propagator", + ) + } + OnTheFlyOperationKindV1::Closure => { + let row = templates + .input() + .closures + .get(operation_id as usize) + .ok_or_else(|| integrity("closure operation row is absent"))?; + ( + row.id, + row.semantic_digest_id, + row.evaluator_binding_id, + DirectExecutorRole::Closure, + "closure", + ) + } + OnTheFlyOperationKindV1::IdentityFinalizer => { + return Err(integrity( + "identity finalizer is not a prepared-operation key", + )); + } + }; + if row_id != operation_id || key.role() != expected_role { + return Err(integrity(format!( + "{label} operation catalog or executor role is not canonical" + ))); + } + let evaluator = templates + .input() + .evaluator_bindings + .get(evaluator_binding_id as usize) + .ok_or_else(|| integrity(format!("{label} evaluator binding is absent")))?; + if evaluator.id != evaluator_binding_id + || key.operation_semantic_digest() + != Some(authenticated_digest_row( + templates, + semantic_digest_id, + &format!("{label} semantic"), + )?) + || key.evaluator_binding_id() != Some(evaluator.id) + || key.evaluator_binding_semantic_digest() + != Some(authenticated_digest_row( + templates, + evaluator.semantic_digest_id, + &format!("{label} evaluator semantic"), + )?) + { + return Err(integrity(format!( + "{label} operation key disagrees with authenticated template semantics" + ))); + } + let binding = match expected_role { + DirectExecutorRole::Contribution => { + let (executor_id, parent_permutation) = + direct.resolve_contribution(evaluator.id)?; + PreparedDirectExecutorBinding::evaluator_with_parent_permutation( + expected_role, + evaluator.id, + executor_id, + parent_permutation, + ) + } + role => PreparedDirectExecutorBinding::evaluator( + role, + evaluator.id, + direct.resolve_evaluator(role, evaluator.id)?, + ), + }; + Ok(binding) + }) +} + +fn authenticated_digest_row( + templates: &ValidatedRecurrenceTemplateInput, + digest_id: u32, + label: &str, +) -> RusticolResult { + let row = templates + .input() + .digest_catalog + .get(digest_id as usize) + .ok_or_else(|| integrity(format!("{label} digest is absent")))?; + if row.id != digest_id { + return Err(integrity(format!( + "{label} digest catalog is not canonical" + ))); + } + SemanticDigest::new(row.value) + .map_err(|error| integrity(format!("{label} digest is invalid: {error}"))) +} + +#[cfg(test)] +pub(crate) fn scalar_adapter_test_trace( + templates: &ValidatedRecurrenceTemplateInput, + seed: &OnTheFlyProcessSeedV1, +) -> RusticolResult { + let source = templates + .input() + .sources + .first() + .ok_or_else(|| integrity("scalar adapter fixture has no source"))?; + let source_evaluator = templates + .input() + .evaluator_bindings + .get(source.evaluator_binding_id as usize) + .ok_or_else(|| integrity("scalar adapter source evaluator is absent"))?; + let source_key = OnTheFlyExecutorKeyV1::new( + seed.direct_catalog_digest(), + DirectExecutorRole::Source, + OnTheFlyOperationKindV1::Source, + source.id, + authenticated_digest_row(templates, source.semantic_digest_id, "source semantic")?, + source_evaluator.id, + authenticated_digest_row( + templates, + source_evaluator.semantic_digest_id, + "source evaluator semantic", + )?, + )?; + let closure = templates + .input() + .closures + .first() + .ok_or_else(|| integrity("scalar adapter fixture has no closure"))?; + let closure_evaluator = templates + .input() + .evaluator_bindings + .get(closure.evaluator_binding_id as usize) + .ok_or_else(|| integrity("scalar adapter closure evaluator is absent"))?; + let closure_key = OnTheFlyExecutorKeyV1::new( + seed.direct_catalog_digest(), + DirectExecutorRole::Closure, + OnTheFlyOperationKindV1::Closure, + closure.id, + authenticated_digest_row(templates, closure.semantic_digest_id, "closure semantic")?, + closure_evaluator.id, + authenticated_digest_row( + templates, + closure_evaluator.semantic_digest_id, + "closure evaluator semantic", + )?, + )?; + let momentum_forms = (0..2) + .map(|source_slot| { + CanonicalMomentumLinearForm::new(vec![MomentumTerm { + source_slot, + coefficient: 1, + }]) + }) + .collect::>>()?; + let test_digest = SemanticDigest::new([93; 32])?; + Ok(OnTheFlyStructuralTraceV1 { + seed_digest: seed.semantic_digest(), + query_digest: SemanticDigest::new([94; 32])?, + current_keys: Vec::new().into_boxed_slice(), + current_colors: Vec::new().into_boxed_slice(), + operations: vec![ + OnTheFlyTraceOperationV1::Source { + key: source_key, + row: DirectSourceRow { + source_slot: 0, + destination_component_base: 0, + momentum_form_id: 0, + source_template_or_dispatch_domain: 0, + spin_state_class: 50_000, + exact_factor_id: 0, + selector_domain_id: 0, + }, + }, + OnTheFlyTraceOperationV1::Source { + key: source_key, + row: DirectSourceRow { + source_slot: 1, + destination_component_base: 1, + momentum_form_id: 1, + source_template_or_dispatch_domain: 0, + spin_state_class: 50_000, + exact_factor_id: 0, + selector_domain_id: 0, + }, + }, + OnTheFlyTraceOperationV1::Closure { + key: closure_key, + row: DirectClosureRow { + parent0_component_base: 0, + parent1_component_base_or_sentinel: 1, + parent0_momentum_form_id: 0, + parent1_momentum_form_id_or_sentinel: 1, + amplitude_destination_id: 0, + exact_factor_id: 0, + component_factor_start: 1, + component_count: 1, + selector_domain_id: 0, + flags: 0, + }, + }, + ] + .into_boxed_slice(), + momentum_forms: momentum_forms.into_boxed_slice(), + exact_factors: vec![ + ExactComplexRational::ONE, + ExactComplexRational::ONE, + ExactComplexRational::new( + crate::recurrence::ExactRational::new(2, 1)?, + crate::recurrence::ExactRational::ZERO, + ), + ] + .into_boxed_slice(), + pairing_owners: vec![ResolvedPairingOwnerV1 { + endpoint_pairs: Vec::new(), + proof_digest: None, + source_slot_permutation: vec![0, 1], + source_lineage: vec![0, 1], + fermion_parity: 1, + }] + .into_boxed_slice(), + layout: OnTheFlyWorkspaceLayoutV1 { + source_count: 2, + lorentz_component_count: 4, + parameter_count: 0, + current_component_count: 2, + amplitude_component_count: 1, + momentum_form_count: 2, + exact_factor_count: 3, + }, + proof: OnTheFlyStructuralProofV1 { + current_count: 2, + contribution_count: 0, + closure_count: 1, + constructed_current_count: 2, + constructed_contribution_count: 0, + current_multiset_digest: test_digest, + contribution_multiset_digest: test_digest, + closure_multiset_digest: test_digest, + owner_digest: test_digest, + semantic_digest: test_digest, + }, + semantic_digest: test_digest, + prepared_executor_rows_bound: false, + current_component_ranges: vec![[0, 1], [1, 1]].into_boxed_slice(), + current_semantic_digests: vec![test_digest; 2].into_boxed_slice(), + contribution_proof_rows: Box::new([]), + }) +} + +#[cfg(test)] +impl OnTheFlyStructuralTraceV1 { + pub(crate) fn test_two_contribution_rows() + -> (Self, OnTheFlyExecutorKeyV1, OnTheFlyExecutorKeyV1) { + let test_digest = SemanticDigest::new([0x5a; 32]).unwrap(); + let direct_digest = SemanticDigest::new([0x61; 32]).unwrap(); + let source_key = OnTheFlyExecutorKeyV1::new( + direct_digest, + DirectExecutorRole::Source, + OnTheFlyOperationKindV1::Source, + 0, + SemanticDigest::new([0x62; 32]).unwrap(), + 0, + SemanticDigest::new([0x63; 32]).unwrap(), + ) + .unwrap(); + let contribution_key = OnTheFlyExecutorKeyV1::new( + direct_digest, + DirectExecutorRole::Contribution, + OnTheFlyOperationKindV1::Transition, + 1, + SemanticDigest::new([0x64; 32]).unwrap(), + 1, + SemanticDigest::new([0x65; 32]).unwrap(), + ) + .unwrap(); + let momentum_forms = (0..4) + .map(|source_slot| { + CanonicalMomentumLinearForm::new(vec![MomentumTerm { + source_slot, + coefficient: 1, + }]) + .unwrap() + }) + .collect::>() + .into_boxed_slice(); + let trace = Self { + seed_digest: SemanticDigest::new([0x51; 32]).unwrap(), + query_digest: SemanticDigest::new([0x52; 32]).unwrap(), + current_keys: Box::new([]), + current_colors: Box::new([]), + operations: vec![ + OnTheFlyTraceOperationV1::Source { + key: source_key, + row: DirectSourceRow { + source_slot: 0, + destination_component_base: 1, + momentum_form_id: 1, + source_template_or_dispatch_domain: 0, + spin_state_class: 50_000, + exact_factor_id: 0, + selector_domain_id: 0, + }, + }, + OnTheFlyTraceOperationV1::Source { + key: source_key, + row: DirectSourceRow { + source_slot: 1, + destination_component_base: 3, + momentum_form_id: 3, + source_template_or_dispatch_domain: 0, + spin_state_class: 50_000, + exact_factor_id: 0, + selector_domain_id: 0, + }, + }, + OnTheFlyTraceOperationV1::Contribution { + key: contribution_key, + row: DirectContributionRow { + parent0_component_base: 0, + parent1_component_base_or_sentinel: 1, + parent0_momentum_form_id: 0, + parent1_momentum_form_id_or_sentinel: 1, + destination_component_base: 2, + exact_factor_id: 0, + selector_domain_id: 0, + flags: DIRECT_CONTRIBUTION_FLAG_INITIALIZE_DESTINATION, + }, + }, + OnTheFlyTraceOperationV1::Contribution { + key: contribution_key, + row: DirectContributionRow { + parent0_component_base: 0, + parent1_component_base_or_sentinel: 3, + parent0_momentum_form_id: 0, + parent1_momentum_form_id_or_sentinel: 3, + destination_component_base: 4, + exact_factor_id: 0, + selector_domain_id: 0, + flags: DIRECT_CONTRIBUTION_FLAG_INITIALIZE_DESTINATION, + }, + }, + ] + .into_boxed_slice(), + momentum_forms, + exact_factors: vec![ExactComplexRational::ONE].into_boxed_slice(), + pairing_owners: Box::new([]), + layout: OnTheFlyWorkspaceLayoutV1 { + source_count: 4, + lorentz_component_count: 4, + parameter_count: 0, + current_component_count: 5, + amplitude_component_count: 1, + momentum_form_count: 4, + exact_factor_count: 1, + }, + proof: OnTheFlyStructuralProofV1 { + current_count: 5, + contribution_count: 2, + closure_count: 0, + constructed_current_count: 5, + constructed_contribution_count: 2, + current_multiset_digest: test_digest, + contribution_multiset_digest: test_digest, + closure_multiset_digest: test_digest, + owner_digest: test_digest, + semantic_digest: test_digest, + }, + semantic_digest: test_digest, + prepared_executor_rows_bound: false, + current_component_ranges: (0..5) + .map(|base| [base, 1]) + .collect::>() + .into_boxed_slice(), + current_semantic_digests: vec![test_digest; 5].into_boxed_slice(), + contribution_proof_rows: Box::new([]), + }; + (trace, source_key, contribution_key) + } + + pub(crate) fn test_bound_contribution_rows(&self) -> Vec { + self.operations + .iter() + .filter_map(|operation| match operation { + OnTheFlyTraceOperationV1::Contribution { row, .. } => Some(*row), + _ => None, + }) + .collect() + } + + pub(crate) fn test_query_family_trace( + query_digest_byte: u8, + contribution_factor: ExactComplexRational, + ) -> (Self, OnTheFlyProjectionProbeV1) { + let digest = |byte| SemanticDigest::new([byte; 32]).unwrap(); + let direct_digest = digest(0x71); + let color_id = crate::recurrence::DynamicLCColorStateId::from_interner(0); + let momentum = |slots: &[u32]| { + CanonicalMomentumLinearForm::new( + slots + .iter() + .copied() + .map(|source_slot| MomentumTerm { + source_slot, + coefficient: 1, + }) + .collect(), + ) + .unwrap() + }; + let helicity = |slots: &[u32]| { + CurrentHelicityIdentity::topology_replay( + 0, + slots + .iter() + .copied() + .map(|source_slot| SourceStateAssignment::new(source_slot, 0)) + .collect(), + ) + .unwrap() + }; + let current_key = |node_kind, state, support: &[u32], source_binding| { + CurrentCoreKey::new( + digest(0x72), + node_kind, + state, + color_id, + support.to_vec(), + momentum(support), + helicity(support), + vec![state as i32], + 0, + vec![0], + source_binding, + (node_kind == RecurrenceNodeKind::Current).then_some(0), + ) + .unwrap() + }; + let current_keys = vec![ + current_key( + RecurrenceNodeKind::Source, + 0, + &[0], + CurrentSourceBinding::FixedTemplate(0), + ), + current_key( + RecurrenceNodeKind::Source, + 1, + &[1], + CurrentSourceBinding::FixedTemplate(1), + ), + current_key( + RecurrenceNodeKind::Current, + 2, + &[0, 1], + CurrentSourceBinding::None, + ), + ]; + let source_key = |id| { + OnTheFlyExecutorKeyV1::new( + direct_digest, + DirectExecutorRole::Source, + OnTheFlyOperationKindV1::Source, + id, + digest(0x73 + id as u8), + id, + digest(0x75 + id as u8), + ) + .unwrap() + }; + let contribution_executor = OnTheFlyExecutorKeyV1::new( + direct_digest, + DirectExecutorRole::Contribution, + OnTheFlyOperationKindV1::Transition, + 2, + digest(0x77), + 2, + digest(0x78), + ) + .unwrap(); + let finalization_executor = OnTheFlyExecutorKeyV1::new( + direct_digest, + DirectExecutorRole::Finalization, + OnTheFlyOperationKindV1::Propagator, + 0, + digest(0x79), + 3, + digest(0x7a), + ) + .unwrap(); + let closure_executor = OnTheFlyExecutorKeyV1::new( + direct_digest, + DirectExecutorRole::Closure, + OnTheFlyOperationKindV1::Closure, + 0, + digest(0x7b), + 4, + digest(0x7c), + ) + .unwrap(); + let contribution_key = ContributionKey::new( + 2, + vec![0, 1], + vec![0, 1], + vec![momentum(&[0]), momentum(&[1])], + 2, + 0, + LCColorWitnessTermId::new(0, 0), + digest(0x7d), + 0, + ) + .unwrap(); + let color = DynamicLCColorState::new(0, None, vec![]).unwrap(); + let test_digest = digest(0x7e); + let current_semantic_digests = current_keys + .iter() + .map(|key| hash_current_key(key, &color).unwrap()) + .collect::>(); + let trace = Self { + seed_digest: digest(0x70), + query_digest: digest(query_digest_byte), + current_keys: current_keys.into_boxed_slice(), + current_colors: vec![color; 3].into_boxed_slice(), + operations: vec![ + OnTheFlyTraceOperationV1::Source { + key: source_key(0), + row: DirectSourceRow { + source_slot: 0, + destination_component_base: 0, + momentum_form_id: 0, + source_template_or_dispatch_domain: 0, + spin_state_class: 0, + exact_factor_id: 0, + selector_domain_id: 0, + }, + }, + OnTheFlyTraceOperationV1::Source { + key: source_key(1), + row: DirectSourceRow { + source_slot: 1, + destination_component_base: 1, + momentum_form_id: 1, + source_template_or_dispatch_domain: 1, + spin_state_class: 0, + exact_factor_id: 0, + selector_domain_id: 0, + }, + }, + OnTheFlyTraceOperationV1::Contribution { + key: contribution_executor, + row: DirectContributionRow { + parent0_component_base: 0, + parent1_component_base_or_sentinel: 1, + parent0_momentum_form_id: 0, + parent1_momentum_form_id_or_sentinel: 1, + destination_component_base: 2, + exact_factor_id: 1, + selector_domain_id: 0, + flags: DIRECT_CONTRIBUTION_FLAG_INITIALIZE_DESTINATION, + }, + }, + OnTheFlyTraceOperationV1::Finalization { + key: finalization_executor, + row: DirectFinalizationRow { + component_base: 2, + component_count: 1, + momentum_form_id: 2, + exact_factor_id: 0, + selector_domain_id: 0, + flags: 0, + }, + }, + OnTheFlyTraceOperationV1::Closure { + key: closure_executor, + row: DirectClosureRow { + parent0_component_base: 0, + parent1_component_base_or_sentinel: 2, + parent0_momentum_form_id: 0, + parent1_momentum_form_id_or_sentinel: 2, + amplitude_destination_id: 0, + exact_factor_id: 0, + component_factor_start: 2, + component_count: 1, + selector_domain_id: 0, + flags: 0, + }, + }, + ] + .into_boxed_slice(), + momentum_forms: vec![momentum(&[0]), momentum(&[1]), momentum(&[0, 1])] + .into_boxed_slice(), + exact_factors: vec![ + ExactComplexRational::ONE, + contribution_factor, + ExactComplexRational::ONE, + ] + .into_boxed_slice(), + pairing_owners: vec![ResolvedPairingOwnerV1 { + endpoint_pairs: Vec::new(), + proof_digest: None, + source_slot_permutation: vec![0, 1], + source_lineage: vec![0, 1], + fermion_parity: 1, + }] + .into_boxed_slice(), + layout: OnTheFlyWorkspaceLayoutV1 { + source_count: 2, + lorentz_component_count: 4, + parameter_count: 0, + current_component_count: 3, + amplitude_component_count: 1, + momentum_form_count: 3, + exact_factor_count: 3, + }, + proof: OnTheFlyStructuralProofV1 { + current_count: 3, + contribution_count: 1, + closure_count: 1, + constructed_current_count: 3, + constructed_contribution_count: 1, + current_multiset_digest: test_digest, + contribution_multiset_digest: test_digest, + closure_multiset_digest: test_digest, + owner_digest: test_digest, + semantic_digest: test_digest, + }, + semantic_digest: test_digest, + prepared_executor_rows_bound: true, + current_component_ranges: vec![[0, 1], [1, 1], [2, 1]].into_boxed_slice(), + current_semantic_digests: current_semantic_digests.into_boxed_slice(), + contribution_proof_rows: vec![OnTheFlyTraceContributionProofRowV1 { + result_current_id: 2, + parent_current_ids: [0, 1], + key: contribution_key, + exact_factor: contribution_factor, + }] + .into_boxed_slice(), + }; + ( + trace, + OnTheFlyProjectionProbeV1 { + enabled: true, + applied: false, + pre: [3, 1, 1], + post: [3, 1, 1], + }, + ) + } + + pub(crate) fn test_remap_dynamic_color_id(&mut self, id: u32) { + let color_id = crate::recurrence::DynamicLCColorStateId::from_interner(id); + for ((key, color), digest) in self + .current_keys + .iter_mut() + .zip(self.current_colors.iter()) + .zip(self.current_semantic_digests.iter_mut()) + { + *key = crate::recurrence::construct::current_key_with_dynamic_color(key, color_id) + .unwrap(); + *digest = hash_current_key(key, color).unwrap(); + } + } + + pub(crate) fn test_add_query_family_contribution( + &mut self, + operation_id: u32, + evaluator_binding_id: u32, + exact_factor: ExactComplexRational, + ) { + let digest = |byte| SemanticDigest::new([byte; 32]).unwrap(); + let existing = self.contribution_proof_rows[0].clone(); + let executor = OnTheFlyExecutorKeyV1::new( + self.executor_keys().next().unwrap().direct_catalog_digest(), + DirectExecutorRole::Contribution, + OnTheFlyOperationKindV1::Transition, + operation_id, + digest(0x90_u8.wrapping_add(operation_id as u8)), + evaluator_binding_id, + digest(0xa0_u8.wrapping_add(evaluator_binding_id as u8)), + ) + .unwrap(); + let proof_key = ContributionKey::new( + operation_id, + existing.key.parent_value_class_ids().to_vec(), + existing.key.parent_state_template_ids().to_vec(), + existing.key.parent_momenta().to_vec(), + existing.key.result_state_template_id(), + existing.key.quantum_flow_witness_id(), + existing.key.color_witness_term_id(), + digest(0xb0_u8.wrapping_add(evaluator_binding_id as u8)), + existing.key.output_projection_id(), + ) + .unwrap(); + let mut factors = std::mem::take(&mut self.exact_factors).into_vec(); + let exact_factor_id = factors.len() as u32; + factors.push(exact_factor); + self.exact_factors = factors.into_boxed_slice(); + self.layout.exact_factor_count += 1; + let mut operations = std::mem::take(&mut self.operations).into_vec(); + let insertion = operations + .iter() + .position(|operation| { + matches!(operation, OnTheFlyTraceOperationV1::Finalization { .. }) + }) + .unwrap(); + operations.insert( + insertion, + OnTheFlyTraceOperationV1::Contribution { + key: executor, + row: DirectContributionRow { + parent0_component_base: 0, + parent1_component_base_or_sentinel: 1, + parent0_momentum_form_id: 0, + parent1_momentum_form_id_or_sentinel: 1, + destination_component_base: 2, + exact_factor_id, + selector_domain_id: 0, + flags: 0, + }, + }, + ); + self.operations = operations.into_boxed_slice(); + let mut proofs = std::mem::take(&mut self.contribution_proof_rows).into_vec(); + proofs.push(OnTheFlyTraceContributionProofRowV1 { + result_current_id: 2, + parent_current_ids: [0, 1], + key: proof_key, + exact_factor, + }); + self.contribution_proof_rows = proofs.into_boxed_slice(); + self.proof.contribution_count += 1; + self.proof.constructed_contribution_count += 1; + let replacement = digest(0xc0_u8.wrapping_add(evaluator_binding_id as u8)); + self.semantic_digest = replacement; + self.proof.semantic_digest = replacement; + } + + pub(crate) fn test_set_last_contribution_selector_domain(&mut self, selector_domain_id: u32) { + let row = self + .operations + .iter_mut() + .rev() + .find_map(|operation| match operation { + OnTheFlyTraceOperationV1::Contribution { row, .. } => Some(row), + _ => None, + }) + .expect("test trace has no contribution row"); + row.selector_domain_id = selector_domain_id; + let replacement = SemanticDigest::new([0xd1; 32]).unwrap(); + self.semantic_digest = replacement; + self.proof.semantic_digest = replacement; + } + + pub(crate) fn test_insert_identity_finalizer(&mut self, direct_catalog_digest: SemanticDigest) { + let operation = OnTheFlyTraceOperationV1::Finalization { + key: OnTheFlyExecutorKeyV1::identity_finalizer(direct_catalog_digest), + row: DirectFinalizationRow { + component_base: 0, + component_count: 1, + momentum_form_id: 0, + exact_factor_id: 2, + selector_domain_id: 0, + flags: 0, + }, + }; + let mut operations = std::mem::take(&mut self.operations).into_vec(); + operations.insert(2, operation); + self.operations = operations.into_boxed_slice(); + } + + pub(crate) fn test_tamper_first_operation_semantic_digest(&mut self, digest: SemanticDigest) { + if let Some(OnTheFlyTraceOperationV1::Source { + key: + OnTheFlyExecutorKeyV1::PreparedOperation { + operation_semantic_digest, + .. + }, + .. + }) = self.operations.first_mut() + { + *operation_semantic_digest = digest; + } + } +} + +pub(crate) fn hash_current_key( + key: &CurrentCoreKey, + color: &DynamicLCColorState, +) -> RusticolResult { + let mut hash = Sha256::new(); + hash.update(b"pyamplicol-on-the-fly-current-key-v1\0"); + hash_digest(&mut hash, key.catalog_digest()); + hash.update([match key.node_kind() { + RecurrenceNodeKind::Source => 0, + RecurrenceNodeKind::Current => 1, + }]); + hash.update(key.current_state_template_id().to_le_bytes()); + hash_len( + &mut hash, + key.support_source_slots().len(), + "current support", + )?; + for slot in key.support_source_slots() { + hash.update(slot.to_le_bytes()); + } + hash_len(&mut hash, key.momentum().terms().len(), "current momentum")?; + for term in key.momentum().terms() { + hash.update(term.source_slot.to_le_bytes()); + hash.update(term.coefficient.to_le_bytes()); + } + hash.update(key.spin_state_class().to_le_bytes()); + hash_len( + &mut hash, + key.helicity_identity().local_source_states().len(), + "current source ancestry", + )?; + for assignment in key.helicity_identity().local_source_states() { + hash.update(assignment.source_slot().to_le_bytes()); + hash.update(assignment.state_index().to_le_bytes()); + } + hash_len( + &mut hash, + key.flavour_flow().len(), + "current flavour ancestry", + )?; + for value in key.flavour_flow() { + hash.update(value.to_le_bytes()); + } + hash.update(key.quantum_number_flow_id().to_le_bytes()); + hash_len( + &mut hash, + key.coupling_orders().len(), + "current coupling orders", + )?; + for value in key.coupling_orders() { + hash.update(value.to_le_bytes()); + } + match key.source_binding() { + CurrentSourceBinding::None => hash.update([0]), + CurrentSourceBinding::FixedTemplate(id) => { + hash.update([1]); + hash.update(id.to_le_bytes()); + } + CurrentSourceBinding::RuntimeDispatch { .. } => { + return Err(integrity( + "selected on-the-fly current unexpectedly uses runtime source dispatch", + )); + } + } + hash.update( + key.propagator_template_id() + .unwrap_or(MISSING_U32) + .to_le_bytes(), + ); + hash.update(color.output_color_shape_id().to_le_bytes()); + hash_len( + &mut hash, + color.components().len(), + "current color components", + )?; + for component in color.components() { + hash.update([component.kind() as u8]); + hash_len(&mut hash, component.source_slots().len(), "color component")?; + for slot in component.source_slots() { + hash.update(slot.to_le_bytes()); + } + } + hash_len( + &mut hash, + color.result_port_bindings().len(), + "current color ports", + )?; + for binding in color.result_port_bindings() { + hash.update(binding.component_index().to_le_bytes()); + hash.update([binding.endpoint() as u8]); + } + final_digest(hash) +} + +pub(super) fn multiset_digest( + domain: &[u8], + mut rows: Vec, +) -> RusticolResult { + rows.sort_unstable(); + let mut hash = Sha256::new(); + hash.update(domain); + hash_len(&mut hash, rows.len(), "proof rows")?; + for row in rows { + hash_digest(&mut hash, row); + } + final_digest(hash) +} + +pub(super) fn contribution_proof_digest( + result_digest: SemanticDigest, + parent_digests: [SemanticDigest; 2], + key: &ContributionKey, + factor: ExactComplexRational, +) -> RusticolResult { + let mut hash = Sha256::new(); + hash.update(b"pyamplicol-on-the-fly-contribution-row-v1\0"); + hash_digest(&mut hash, result_digest); + hash_digest(&mut hash, parent_digests[0]); + hash_digest(&mut hash, parent_digests[1]); + hash.update(key.transition_template_id().to_le_bytes()); + hash.update(key.result_state_template_id().to_le_bytes()); + hash.update(key.quantum_flow_witness_id().to_le_bytes()); + hash.update( + key.color_witness_term_id() + .color_contraction_template_id() + .to_le_bytes(), + ); + hash.update(key.color_witness_term_id().witness_ordinal().to_le_bytes()); + hash_digest(&mut hash, key.runtime_coupling_binding_digest()); + hash.update(key.output_projection_id().to_le_bytes()); + hash_exact(&mut hash, factor); + final_digest(hash) +} + +fn closure_proof_digest( + parent_digests: [SemanticDigest; 2], + closure: &PendingClosure, +) -> RusticolResult { + let mut hash = Sha256::new(); + hash.update(b"pyamplicol-on-the-fly-closure-row-v1\0"); + hash_digest(&mut hash, parent_digests[0]); + hash_digest(&mut hash, parent_digests[1]); + hash.update(closure.key.closure_template_id.to_le_bytes()); + hash.update( + closure + .key + .quantum_flow_template_id + .unwrap_or(MISSING_U32) + .to_le_bytes(), + ); + hash.update( + closure + .key + .color_witness_term_id + .color_contraction_template_id() + .to_le_bytes(), + ); + hash.update( + closure + .key + .color_witness_term_id + .witness_ordinal() + .to_le_bytes(), + ); + hash_exact(&mut hash, closure.factor); + hash_len( + &mut hash, + closure.component_coefficients.len(), + "closure component factors", + )?; + for coefficient in &closure.component_coefficients { + hash_exact(&mut hash, *coefficient); + } + final_digest(hash) +} + +fn pairing_owner_association_multiset_digest( + closure_proof_rows: &[SemanticDigest], + pairing_owners: &[ResolvedPairingOwnerV1], +) -> RusticolResult { + if closure_proof_rows.len() != pairing_owners.len() { + return Err(integrity( + "closure proof and pairing-owner domains have different sizes", + )); + } + let mut rows = Vec::new(); + rows.try_reserve_exact(pairing_owners.len()) + .map_err(|error| invalid(format!("pairing-owner proof allocation failed: {error}")))?; + for (closure_proof, owner) in closure_proof_rows.iter().zip(pairing_owners) { + let mut hash = Sha256::new(); + hash.update(b"pyamplicol-on-the-fly-closure-pairing-owner-row-v1\0"); + hash_digest(&mut hash, *closure_proof); + match owner.proof_digest { + None => hash.update([0]), + Some(digest) => { + hash.update([1]); + hash_digest(&mut hash, digest); + } + } + rows.push(final_digest(hash)?); + } + multiset_digest( + b"pyamplicol-on-the-fly-closure-pairing-owner-multiset-v1\0", + rows, + ) +} + +fn intern_factor( + factor: ExactComplexRational, + factors: &mut Vec, + ids: &mut BTreeMap, +) -> RusticolResult { + if let Some(id) = ids.get(&factor).copied() { + return Ok(id); + } + let id = checked_u32(factors.len(), "exact factor count")?; + factors.push(factor); + ids.insert(factor, id); + Ok(id) +} + +fn intern_momentum( + form: &CanonicalMomentumLinearForm, + forms: &mut Vec, + ids: &mut BTreeMap, +) -> RusticolResult { + if let Some(id) = ids.get(form).copied() { + return Ok(id); + } + let id = checked_u32(forms.len(), "momentum form count")?; + forms.push(form.clone()); + ids.insert(form.clone(), id); + Ok(id) +} + +fn operation_key( + seed: &OnTheFlyProcessSeedV1, + role: DirectExecutorRole, + kind: OnTheFlyOperationKindV1, + operation_id: u32, + operation_semantic_digest: SemanticDigest, + evaluator_binding_id: u32, + evaluator_binding_semantic_digest: SemanticDigest, +) -> RusticolResult { + OnTheFlyExecutorKeyV1::new( + seed.direct_catalog_digest, + role, + kind, + operation_id, + operation_semantic_digest, + evaluator_binding_id, + evaluator_binding_semantic_digest, + ) +} + +fn source_executor_key( + grammar: &PreparedOnTheFlyGrammarV1, + seed: &OnTheFlyProcessSeedV1, + source_template_id: u32, + current_state_template_id: u32, +) -> RusticolResult { + let executor = grammar + .sources + .get(&(source_template_id, current_state_template_id)) + .ok_or_else(|| integrity("source executor contract is absent"))? + .executor; + operation_key( + seed, + DirectExecutorRole::Source, + OnTheFlyOperationKindV1::Source, + executor.operation_id, + executor.operation_semantic_digest, + executor.evaluator_binding_id, + executor.evaluator_binding_semantic_digest, + ) +} + +fn transition_executor_key( + seed: &OnTheFlyProcessSeedV1, + transition: &PreparedTransition, +) -> RusticolResult { + operation_key( + seed, + DirectExecutorRole::Contribution, + OnTheFlyOperationKindV1::Transition, + transition.row.id, + transition.transition_semantic_digest, + transition.row.evaluator_binding_id, + transition.evaluator_binding_digest, + ) +} + +fn propagator_executor_key( + grammar: &PreparedOnTheFlyGrammarV1, + seed: &OnTheFlyProcessSeedV1, + propagator_template_id: u32, +) -> RusticolResult { + let executor = grammar + .propagator_executors + .get(&propagator_template_id) + .ok_or_else(|| integrity("propagator executor contract is absent"))?; + operation_key( + seed, + DirectExecutorRole::Finalization, + OnTheFlyOperationKindV1::Propagator, + executor.operation_id, + executor.operation_semantic_digest, + executor.evaluator_binding_id, + executor.evaluator_binding_semantic_digest, + ) +} + +fn closure_executor_key( + seed: &OnTheFlyProcessSeedV1, + closure: &PreparedClosure, +) -> RusticolResult { + operation_key( + seed, + DirectExecutorRole::Closure, + OnTheFlyOperationKindV1::Closure, + closure.row.id, + closure.closure_semantic_digest, + closure.row.evaluator_binding_id, + closure.evaluator_binding_digest, + ) +} + +#[allow(clippy::too_many_arguments)] +pub(super) fn lower_trace( + templates: &ValidatedRecurrenceTemplateInput, + grammar: &PreparedOnTheFlyGrammarV1, + seed: &OnTheFlyProcessSeedV1, + query: &DecodedLcQueryV1, + colors: &DynamicLCColorStateInterner, + currents: &[PendingCurrent], + closures: &[PendingClosure], + pairing_owners: Vec, + live: &BTreeSet, + constructed_contribution_count: usize, +) -> RusticolResult { + if pairing_owners.len() != closures.len() { + return Err(integrity( + "selected closures and resolved pairing owners have different sizes", + )); + } + let input = templates.input(); + let mut old_to_new = BTreeMap::new(); + let mut current_keys = Vec::with_capacity(live.len()); + let mut current_colors = Vec::with_capacity(live.len()); + let mut component_bases = BTreeMap::new(); + let mut component_counts = BTreeMap::new(); + let mut current_component_count = 0u32; + for old_id in live.iter().copied() { + let current = currents + .get(old_id as usize) + .ok_or_else(|| integrity("live current is absent"))?; + let state = input + .current_states + .get(current.key.current_state_template_id() as usize) + .ok_or_else(|| integrity("live current state template is absent"))?; + let count = u16::try_from(state.dimension) + .map_err(|_| invalid("on-the-fly current component count exceeds u16"))?; + let new_id = checked_u32(current_keys.len(), "live current count")?; + old_to_new.insert(old_id, new_id); + component_bases.insert(old_id, current_component_count); + component_counts.insert(old_id, count); + current_component_count = current_component_count + .checked_add(u32::from(count)) + .ok_or_else(|| invalid("on-the-fly current arena exceeds u32 components"))?; + current_keys.push(current.key.clone()); + current_colors.push( + colors + .get(current.key.dynamic_lc_color_state_id()) + .ok_or_else(|| integrity("live current color state is absent"))? + .clone(), + ); + } + + let mut momentum_forms = Vec::new(); + let mut momentum_ids = BTreeMap::new(); + for old_id in live.iter().copied() { + intern_momentum( + currents[old_id as usize].key.momentum(), + &mut momentum_forms, + &mut momentum_ids, + )?; + } + let mut exact_factors = Vec::new(); + let mut factor_ids = BTreeMap::new(); + intern_factor( + ExactComplexRational::ONE, + &mut exact_factors, + &mut factor_ids, + )?; + let mut operations = Vec::new(); + + for old_id in live + .iter() + .copied() + .filter(|old_id| currents[*old_id as usize].key.node_kind() == RecurrenceNodeKind::Source) + { + let current = ¤ts[old_id as usize]; + let source_template_id = match current.key.source_binding() { + CurrentSourceBinding::FixedTemplate(id) => *id, + _ => return Err(integrity("selected source has no fixed template binding")), + }; + operations.push(OnTheFlyTraceOperationV1::Source { + key: source_executor_key( + grammar, + seed, + source_template_id, + current.key.current_state_template_id(), + )?, + row: DirectSourceRow { + source_slot: current.key.support_source_slots()[0], + destination_component_base: component_bases[&old_id], + momentum_form_id: momentum_ids[current.key.momentum()], + source_template_or_dispatch_domain: source_template_id, + spin_state_class: current.key.spin_state_class(), + exact_factor_id: intern_factor( + current + .source_factor + .ok_or_else(|| integrity("source current has no exact factor"))?, + &mut exact_factors, + &mut factor_ids, + )?, + selector_domain_id: 0, + }, + }); + } + + let maximum_stage = live + .iter() + .map(|id| currents[*id as usize].stage) + .max() + .unwrap_or(0); + let mut contribution_proof_rows = Vec::new(); + let mut family_contribution_rows = Vec::new(); + let current_digest_by_old = live + .iter() + .copied() + .map(|old_id| { + let current = ¤ts[old_id as usize]; + Ok(( + old_id, + hash_current_key( + ¤t.key, + colors + .get(current.key.dynamic_lc_color_state_id()) + .ok_or_else(|| integrity("proof current color state is absent"))?, + )?, + )) + }) + .collect::>>()?; + + for stage in 1..=maximum_stage { + for old_id in live + .iter() + .copied() + .filter(|old_id| currents[*old_id as usize].stage == stage) + { + let current = ¤ts[old_id as usize]; + let mut initialized = false; + for (pending, factor) in current.contributions.iter().filter(|(pending, factor)| { + !factor.is_zero() + && pending + .parent_current_ids + .iter() + .all(|parent| live.contains(parent)) + }) { + let transition_id = pending.key.transition_template_id(); + let prepared = grammar + .transitions + .values() + .flatten() + .find(|prepared| prepared.row.id == transition_id) + .ok_or_else(|| integrity("prepared live transition disappeared"))?; + let parent_ids = pending.parent_current_ids; + operations.push(OnTheFlyTraceOperationV1::Contribution { + key: transition_executor_key(seed, prepared)?, + row: DirectContributionRow { + parent0_component_base: component_bases[&parent_ids[0]], + parent1_component_base_or_sentinel: component_bases[&parent_ids[1]], + parent0_momentum_form_id: momentum_ids + [currents[parent_ids[0] as usize].key.momentum()], + parent1_momentum_form_id_or_sentinel: momentum_ids + [currents[parent_ids[1] as usize].key.momentum()], + destination_component_base: component_bases[&old_id], + exact_factor_id: intern_factor( + *factor, + &mut exact_factors, + &mut factor_ids, + )?, + selector_domain_id: 0, + flags: if initialized { + 0 + } else { + DIRECT_CONTRIBUTION_FLAG_INITIALIZE_DESTINATION + }, + }, + }); + initialized = true; + contribution_proof_rows.push(contribution_proof_digest( + current_digest_by_old[&old_id], + [ + current_digest_by_old[&parent_ids[0]], + current_digest_by_old[&parent_ids[1]], + ], + &pending.key, + *factor, + )?); + { + let local_parent_ids = [old_to_new[&parent_ids[0]], old_to_new[&parent_ids[1]]]; + family_contribution_rows.push(OnTheFlyTraceContributionProofRowV1 { + result_current_id: old_to_new[&old_id], + parent_current_ids: local_parent_ids, + key: ContributionKey::new( + pending.key.transition_template_id(), + local_parent_ids.to_vec(), + pending.key.parent_state_template_ids().to_vec(), + pending.key.parent_momenta().to_vec(), + pending.key.result_state_template_id(), + pending.key.quantum_flow_witness_id(), + pending.key.color_witness_term_id(), + pending.key.runtime_coupling_binding_digest(), + pending.key.output_projection_id(), + )?, + exact_factor: *factor, + }); + } + } + if !initialized { + return Err(integrity( + "live non-source current has no live contribution", + )); + } + if let Some(propagator_id) = current.key.propagator_template_id() { + let component_count = component_counts[&old_id]; + operations.push(OnTheFlyTraceOperationV1::Finalization { + key: propagator_executor_key(grammar, seed, propagator_id)?, + row: DirectFinalizationRow { + component_base: component_bases[&old_id], + component_count, + momentum_form_id: momentum_ids[current.key.momentum()], + exact_factor_id: factor_ids[&ExactComplexRational::ONE], + selector_domain_id: 0, + flags: 0, + }, + }); + } + } + } + + let mut closure_proof_rows = Vec::new(); + for closure in closures { + let prepared = grammar + .closures + .values() + .flatten() + .find(|prepared| prepared.row.id == closure.key.closure_template_id) + .ok_or_else(|| integrity("prepared live closure disappeared"))?; + let parent_ids = closure.key.parent_current_ids; + let parent0_count = component_counts[&parent_ids[0]]; + let parent1_count = component_counts[&parent_ids[1]]; + if parent0_count != parent1_count + || closure.component_coefficients.len() != usize::from(parent0_count) + { + return Err(integrity( + "closure component coefficients do not match parent dimensions", + )); + } + let component_factor_start = checked_u32(exact_factors.len(), "closure factor start")?; + exact_factors.extend_from_slice(&closure.component_coefficients); + operations.push(OnTheFlyTraceOperationV1::Closure { + key: closure_executor_key(seed, prepared)?, + row: DirectClosureRow { + parent0_component_base: component_bases[&parent_ids[0]], + parent1_component_base_or_sentinel: component_bases[&parent_ids[1]], + parent0_momentum_form_id: momentum_ids + [currents[parent_ids[0] as usize].key.momentum()], + parent1_momentum_form_id_or_sentinel: momentum_ids + [currents[parent_ids[1] as usize].key.momentum()], + amplitude_destination_id: 0, + exact_factor_id: intern_factor( + closure.factor, + &mut exact_factors, + &mut factor_ids, + )?, + component_factor_start, + component_count: parent0_count, + selector_domain_id: 0, + flags: 0, + }, + }); + closure_proof_rows.push(closure_proof_digest( + [ + current_digest_by_old[&parent_ids[0]], + current_digest_by_old[&parent_ids[1]], + ], + closure, + )?); + } + + let current_multiset_digest = multiset_digest( + b"pyamplicol-on-the-fly-current-multiset-v1\0", + current_digest_by_old.values().copied().collect(), + )?; + let contribution_multiset_digest = multiset_digest( + b"pyamplicol-on-the-fly-contribution-multiset-v1\0", + contribution_proof_rows, + )?; + let pairing_owner_multiset_digest = + pairing_owner_association_multiset_digest(&closure_proof_rows, &pairing_owners)?; + let closure_multiset_digest = multiset_digest( + b"pyamplicol-on-the-fly-closure-multiset-v1\0", + closure_proof_rows, + )?; + let mut owner_hash = Sha256::new(); + owner_hash.update(b"pyamplicol-on-the-fly-owner-v2\0"); + hash_digest(&mut owner_hash, seed.semantic_digest()); + hash_digest(&mut owner_hash, query.semantic_digest()); + owner_hash.update(query.closure_anchor_slot.to_le_bytes()); + hash_digest(&mut owner_hash, query.selector_digest); + hash_digest(&mut owner_hash, pairing_owner_multiset_digest); + let owner_digest = final_digest(owner_hash)?; + let mut proof_hash = Sha256::new(); + proof_hash.update(ON_THE_FLY_PROOF_DOMAIN); + for digest in [ + current_multiset_digest, + contribution_multiset_digest, + closure_multiset_digest, + owner_digest, + ] { + hash_digest(&mut proof_hash, digest); + } + let live_contribution_count = operations + .iter() + .filter(|operation| matches!(operation, OnTheFlyTraceOperationV1::Contribution { .. })) + .count(); + for value in [ + live.len(), + live_contribution_count, + closures.len(), + currents.len(), + constructed_contribution_count, + ] { + proof_hash.update(checked_u32(value, "structural proof count")?.to_le_bytes()); + } + let proof = OnTheFlyStructuralProofV1 { + current_count: checked_u32(live.len(), "live current count")?, + contribution_count: checked_u32(live_contribution_count, "live contribution count")?, + closure_count: checked_u32(closures.len(), "live closure count")?, + constructed_current_count: checked_u32(currents.len(), "constructed current count")?, + constructed_contribution_count: checked_u32( + constructed_contribution_count, + "constructed contribution count", + )?, + current_multiset_digest, + contribution_multiset_digest, + closure_multiset_digest, + owner_digest, + semantic_digest: final_digest(proof_hash)?, + }; + let exact_factor_count = checked_u32(exact_factors.len(), "exact factor count")?; + let layout = OnTheFlyWorkspaceLayoutV1 { + source_count: checked_u32(query.selected_sources.len(), "selected source count")?, + lorentz_component_count: 4, + parameter_count: checked_u32(templates.input().parameters.len(), "parameter count")?, + current_component_count, + amplitude_component_count: 1, + momentum_form_count: checked_u32(momentum_ids.len(), "momentum form count")?, + exact_factor_count, + }; + let mut trace_hash = Sha256::new(); + trace_hash.update(b"pyamplicol-on-the-fly-structural-trace-v1\0"); + hash_digest(&mut trace_hash, seed.semantic_digest()); + hash_digest(&mut trace_hash, query.semantic_digest()); + hash_digest(&mut trace_hash, proof.semantic_digest()); + for value in [ + layout.source_count, + u32::from(layout.lorentz_component_count), + layout.parameter_count, + layout.current_component_count, + layout.amplitude_component_count, + layout.momentum_form_count, + layout.exact_factor_count, + ] { + trace_hash.update(value.to_le_bytes()); + } + let semantic_digest = final_digest(trace_hash)?; + let current_component_ranges = live + .iter() + .copied() + .map(|old_id| { + [ + component_bases[&old_id], + u32::from(component_counts[&old_id]), + ] + }) + .collect::>() + .into_boxed_slice(); + let current_semantic_digests = live + .iter() + .copied() + .map(|old_id| current_digest_by_old[&old_id]) + .collect::>() + .into_boxed_slice(); + Ok(OnTheFlyStructuralTraceV1 { + seed_digest: seed.semantic_digest(), + query_digest: query.semantic_digest(), + current_keys: current_keys.into_boxed_slice(), + current_colors: current_colors.into_boxed_slice(), + operations: operations.into_boxed_slice(), + momentum_forms: momentum_forms.into_boxed_slice(), + exact_factors: exact_factors.into_boxed_slice(), + pairing_owners: pairing_owners.into_boxed_slice(), + layout, + proof, + semantic_digest, + prepared_executor_rows_bound: false, + current_component_ranges, + current_semantic_digests, + contribution_proof_rows: family_contribution_rows.into_boxed_slice(), + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn digest(byte: u8) -> SemanticDigest { + SemanticDigest::new([byte; 32]).unwrap() + } + + fn pairing_owner(byte: u8) -> ResolvedPairingOwnerV1 { + ResolvedPairingOwnerV1 { + endpoint_pairs: Vec::new(), + proof_digest: Some(digest(byte)), + source_slot_permutation: Vec::new(), + source_lineage: Vec::new(), + fermion_parity: 1, + } + } + + #[test] + fn pairing_owner_proof_binds_closure_associations_order_independently() { + let closures = [digest(1), digest(2)]; + let owners = [pairing_owner(3), pairing_owner(4)]; + let expected = pairing_owner_association_multiset_digest(&closures, &owners).unwrap(); + assert_eq!( + expected, + pairing_owner_association_multiset_digest( + &[closures[1], closures[0]], + &[pairing_owner(4), pairing_owner(3)], + ) + .unwrap(), + ); + assert_ne!( + expected, + pairing_owner_association_multiset_digest( + &closures, + &[pairing_owner(4), pairing_owner(3)], + ) + .unwrap(), + ); + assert_ne!( + pairing_owner_association_multiset_digest(&[closures[0]], &[pairing_owner(3)],) + .unwrap(), + pairing_owner_association_multiset_digest( + &[closures[0], closures[0]], + &[pairing_owner(3), pairing_owner(3)], + ) + .unwrap(), + ); + assert!(pairing_owner_association_multiset_digest(&closures, &owners[..1]).is_err()); + } + + #[test] + fn logical_batch_capacity_belongs_to_workspace_not_structural_trace() { + let trace = OnTheFlyStructuralTraceV1 { + seed_digest: digest(1), + query_digest: digest(2), + current_keys: Box::new([]), + current_colors: Box::new([]), + operations: Box::new([]), + momentum_forms: vec![ + CanonicalMomentumLinearForm::new(vec![MomentumTerm { + source_slot: 0, + coefficient: 1, + }]) + .unwrap(), + ] + .into_boxed_slice(), + exact_factors: vec![ExactComplexRational::ONE].into_boxed_slice(), + pairing_owners: Box::new([]), + layout: OnTheFlyWorkspaceLayoutV1 { + source_count: 1, + lorentz_component_count: 4, + parameter_count: 0, + current_component_count: 1, + amplitude_component_count: 1, + momentum_form_count: 1, + exact_factor_count: 1, + }, + proof: OnTheFlyStructuralProofV1 { + current_count: 0, + contribution_count: 0, + closure_count: 0, + constructed_current_count: 0, + constructed_contribution_count: 0, + current_multiset_digest: digest(3), + contribution_multiset_digest: digest(4), + closure_multiset_digest: digest(5), + owner_digest: digest(6), + semantic_digest: digest(7), + }, + semantic_digest: digest(8), + prepared_executor_rows_bound: false, + current_component_ranges: Box::new([]), + current_semantic_digests: Box::new([]), + contribution_proof_rows: Box::new([]), + }; + let small = OnTheFlyWorkspaceV1::new(&trace, 1).unwrap(); + let large = OnTheFlyWorkspaceV1::new(&trace, 65).unwrap(); + assert_ne!(small.point_stride(), large.point_stride()); + assert_eq!(trace.semantic_digest(), digest(8)); + } +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/recurrence/process.rs b/pyamplicol/rust/crates/rusticol-core/src/recurrence/process.rs new file mode 100644 index 0000000000..e06aa1bdfe --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/recurrence/process.rs @@ -0,0 +1,4339 @@ +// SPDX-License-Identifier: 0BSD + +//! Checked process-wide input for the compact recurrence builder. +//! +//! The Python producer encodes `pyamplicol-recurrence-builder-input-v2` as +//! little-endian structure-of-arrays tables. A boundary decoder can zip those +//! columns into the fixed-width row types below and then construct +//! [`OwnedRecurrenceProcessInput`]. This module validates process semantics +//! and exposes typed template references and semantic identities for a later +//! composite process-plus-template authentication step. It deliberately does +//! not construct recurrence states or an execution schedule. + +use std::collections::{BTreeMap, BTreeSet}; + +use serde_json::json; +use sha2::{Digest, Sha256}; + +use super::{ + CheckedTableRange, ExactComplexRational, MultiwordMaskCatalogView, + RECURRENCE_BUILDER_INPUT_ABI, RecurrenceStrategy, SemanticDigest, checked_u32_len, + validate_concrete_spin_state, validate_packed_ranges, validate_u32_references, +}; +use crate::{RusticolError, RusticolResult}; + +pub const RECURRENCE_PROCESS_INPUT_SCHEMA_VERSION: u32 = 1; +pub const RECURRENCE_FERMION_PAIRING_COLUMNAR_ABI: &str = + "pyamplicol-recurrence-fermion-pairing-columnar-v1"; +pub const RECURRENCE_FERMION_PAIRING_SCHEMA_VERSION: u32 = 1; +pub const RECURRENCE_NO_FERMION_LINE: u32 = u32::MAX; + +const RECURRENCE_FERMION_PAIRING_PROOF_ALGORITHM: &str = "canonical-external-fermion-pairing-v1"; + +const MISSING_ID: u32 = u32::MAX; +const REQUIRED_DIGEST_ROLE_COUNT: usize = 4; + +fn invalid(message: impl Into) -> RusticolError { + RusticolError::invalid_argument(message) +} + +struct FermionPairingCatalogs<'a> { + strings: Vec<&'a str>, +} + +impl<'a> FermionPairingInputView<'a> { + fn validate_internal( + self, + expected_process_id: &str, + ) -> RusticolResult { + if self.input_abi != RECURRENCE_FERMION_PAIRING_COLUMNAR_ABI { + return Err(RusticolError::compatibility(format!( + "unsupported fermion-pairing columnar ABI {:?}; expected {:?}", + self.input_abi, RECURRENCE_FERMION_PAIRING_COLUMNAR_ABI + ))); + } + if self.header.len() != 1 { + return Err(invalid(format!( + "fermion-pairing header must contain one row, found {}", + self.header.len() + ))); + } + if self.rules.is_empty() { + return Err(invalid("fermion-pairing catalog has no rules")); + } + + let catalogs = self.validate_catalogs()?; + let header = self.header[0]; + if header.schema_version != RECURRENCE_FERMION_PAIRING_SCHEMA_VERSION { + return Err(RusticolError::compatibility(format!( + "unsupported fermion-pairing schema {}; expected {}", + header.schema_version, RECURRENCE_FERMION_PAIRING_SCHEMA_VERSION + ))); + } + require_string_value( + &catalogs.strings, + header.abi_string_id, + RECURRENCE_FERMION_PAIRING_COLUMNAR_ABI, + "fermion-pairing ABI", + )?; + require_string_value( + &catalogs.strings, + header.proof_algorithm_string_id, + RECURRENCE_FERMION_PAIRING_PROOF_ALGORITHM, + "fermion-pairing proof algorithm", + )?; + let process_key = required_string( + &catalogs.strings, + header.process_key_string_id, + "fermion-pairing process key", + )?; + if process_key != expected_process_id { + return Err(invalid(format!( + "fermion-pairing process key {process_key:?} does not match {expected_process_id:?}" + ))); + } + self.validate_counts_and_ranges(header)?; + self.validate_endpoints(&catalogs, header)?; + self.validate_classes(&catalogs, header)?; + self.validate_exact_integers()?; + self.validate_rules(&catalogs, header)?; + + let topology_digest = SemanticDigest::new(header.topology_digest).map_err(|error| { + invalid(format!( + "fermion-pairing topology digest is invalid: {error}" + )) + })?; + let semantic_digest = SemanticDigest::new(header.semantic_digest).map_err(|error| { + invalid(format!( + "fermion-pairing semantic digest is invalid: {error}" + )) + })?; + Ok(FermionPairingValidationSummary { + columnar_digest: self.declared_columnar_digest, + topology_digest, + semantic_digest, + source_count: header.source_count, + endpoint_count: header.endpoint_count, + pairing_class_count: header.pairing_class_count, + rule_count: header.rule_count, + }) + } + + fn validate_catalogs(self) -> RusticolResult> { + validate_packed_ranges( + "fermion-pairing strings", + self.string_ranges, + self.string_bytes.len(), + )?; + let mut strings = Vec::with_capacity(self.string_ranges.len()); + let mut previous: Option<&[u8]> = None; + for (index, range) in self.string_ranges.iter().copied().enumerate() { + let bytes = &self.string_bytes[range.as_usize_range( + self.string_bytes.len(), + &format!("fermion-pairing string {index}"), + )?]; + if bytes.is_empty() || previous.is_some_and(|value| value >= bytes) { + return Err(invalid(format!( + "fermion-pairing string row {index} is empty or not canonical" + ))); + } + let value = std::str::from_utf8(bytes).map_err(|error| { + invalid(format!( + "fermion-pairing string row {index} is not UTF-8: {error}" + )) + })?; + previous = Some(bytes); + strings.push(value); + } + Ok(FermionPairingCatalogs { strings }) + } + + fn validate_counts_and_ranges(self, header: FermionPairingHeaderRow) -> RusticolResult<()> { + for (label, declared, actual) in [ + ("endpoints", header.endpoint_count, self.endpoints.len()), + ( + "pairing classes", + header.pairing_class_count, + self.pairing_classes.len(), + ), + ("rules", header.rule_count, self.rules.len()), + ( + "exact integers", + header.exact_integer_count, + self.exact_integers.len(), + ), + ("strings", header.string_count, self.string_ranges.len()), + ] { + if declared != checked_u32_len(actual, label)? { + return Err(invalid(format!( + "fermion-pairing header {label} count does not match its table" + ))); + } + } + for (label, declared, actual) in [ + ( + "endpoint state IDs", + header.endpoint_state_template_count, + self.endpoint_state_template_ids.len(), + ), + ( + "endpoint anti-state IDs", + header.endpoint_anti_state_template_count, + self.endpoint_anti_state_template_ids.len(), + ), + ( + "endpoint basis IDs", + header.endpoint_basis_count, + self.endpoint_basis_ids.len(), + ), + ( + "endpoint color representations", + header.endpoint_color_representation_count, + self.endpoint_color_representations.len(), + ), + ( + "class fundamental slots", + header.class_fundamental_slot_count, + self.class_fundamental_slots.len(), + ), + ( + "class antifundamental slots", + header.class_antifundamental_slot_count, + self.class_antifundamental_slots.len(), + ), + ( + "class reference pairings", + header.class_reference_pairing_count, + self.class_reference_pairings.len(), + ), + ( + "rule class indices", + header.rule_class_pairing_index_count, + self.rule_class_pairing_indices.len(), + ), + ( + "rule endpoint pairings", + header.rule_endpoint_pairing_count, + self.rule_endpoint_pairings.len(), + ), + ( + "rule source permutations", + header.rule_source_permutation_count, + self.rule_source_slot_permutations.len(), + ), + ( + "rule lineages", + header.rule_lineage_count, + self.rule_lineages.len(), + ), + ( + "exact-integer limbs", + header.exact_integer_limb_count, + self.exact_integer_limbs.len(), + ), + ( + "string bytes", + header.string_byte_count, + self.string_bytes.len(), + ), + ] { + let actual = u64::try_from(actual) + .map_err(|_| invalid(format!("fermion-pairing {label} count exceeds u64")))?; + if declared != actual { + return Err(invalid(format!( + "fermion-pairing header {label} count does not match its table" + ))); + } + } + if header.no_fermion_line != RECURRENCE_NO_FERMION_LINE { + return Err(invalid("fermion-pairing lineage sentinel is stale")); + } + validate_pairing_ranges( + "endpoint state IDs", + self.endpoints.iter().map(|row| row.state_template_range), + self.endpoint_state_template_ids.len(), + )?; + validate_pairing_ranges( + "endpoint anti-state IDs", + self.endpoints + .iter() + .map(|row| row.anti_state_template_range), + self.endpoint_anti_state_template_ids.len(), + )?; + validate_pairing_ranges( + "endpoint basis IDs", + self.endpoints.iter().map(|row| row.basis_range), + self.endpoint_basis_ids.len(), + )?; + validate_pairing_ranges( + "endpoint color representations", + self.endpoints + .iter() + .map(|row| row.color_representation_range), + self.endpoint_color_representations.len(), + )?; + validate_pairing_ranges( + "class fundamental slots", + self.pairing_classes + .iter() + .map(|row| row.fundamental_slot_range), + self.class_fundamental_slots.len(), + )?; + validate_pairing_ranges( + "class antifundamental slots", + self.pairing_classes + .iter() + .map(|row| row.antifundamental_slot_range), + self.class_antifundamental_slots.len(), + )?; + validate_pairing_ranges( + "class reference pairings", + self.pairing_classes + .iter() + .map(|row| row.reference_pairing_range), + self.class_reference_pairings.len(), + )?; + validate_pairing_ranges( + "rule class indices", + self.rules.iter().map(|row| row.class_pairing_index_range), + self.rule_class_pairing_indices.len(), + )?; + validate_pairing_ranges( + "rule endpoint pairings", + self.rules.iter().map(|row| row.endpoint_pairing_range), + self.rule_endpoint_pairings.len(), + )?; + validate_pairing_ranges( + "rule source permutations", + self.rules.iter().map(|row| row.source_permutation_range), + self.rule_source_slot_permutations.len(), + )?; + validate_pairing_ranges( + "rule lineages", + self.rules.iter().map(|row| row.lineage_range), + self.rule_lineages.len(), + )?; + validate_pairing_ranges( + "exact-integer limbs", + self.exact_integers.iter().map(|row| row.limb_range), + self.exact_integer_limbs.len(), + ) + } + + fn validate_endpoints( + self, + catalogs: &FermionPairingCatalogs<'_>, + header: FermionPairingHeaderRow, + ) -> RusticolResult<()> { + validate_canonical_ids( + "fermion-pairing endpoint IDs", + self.endpoints.iter().map(|row| row.endpoint_id), + )?; + let mut previous_slot = None; + for (index, endpoint) in self.endpoints.iter().copied().enumerate() { + required_reference( + endpoint.source_slot, + header.source_count as usize, + "fermion endpoint source", + )?; + if previous_slot.is_some_and(|value| value >= endpoint.source_slot) { + return Err(invalid(format!( + "fermion endpoints are not in source-slot order at row {index}" + ))); + } + previous_slot = Some(endpoint.source_slot); + required_reference( + endpoint.species_class_id, + self.pairing_classes.len(), + "fermion endpoint species class", + )?; + required_string( + &catalogs.strings, + endpoint.species_string_id, + "fermion endpoint species", + )?; + if !matches!(endpoint.particle_orientation, 0 | 1) + || !matches!(endpoint.color_orientation, 0 | 1) + { + return Err(invalid(format!( + "fermion endpoint {index} has an invalid orientation" + ))); + } + SemanticDigest::new(endpoint.contract_digest).map_err(|error| { + invalid(format!( + "fermion endpoint {index} digest is invalid: {error}" + )) + })?; + for (ids, range, label) in [ + ( + self.endpoint_state_template_ids, + endpoint.state_template_range, + "endpoint state IDs", + ), + ( + self.endpoint_anti_state_template_ids, + endpoint.anti_state_template_range, + "endpoint anti-state IDs", + ), + ( + self.endpoint_basis_ids, + endpoint.basis_range, + "endpoint basis IDs", + ), + ] { + let ids = pairing_range_slice(ids, range, label)?; + if ids.is_empty() { + return Err(invalid(format!( + "fermion endpoint {index} has empty {label}" + ))); + } + validate_strict_u32_values(ids, catalogs.strings.len(), label)?; + } + let representations = pairing_range_slice( + self.endpoint_color_representations, + endpoint.color_representation_range, + "endpoint color representations", + )?; + if representations.is_empty() { + return Err(invalid(format!( + "fermion endpoint {index} has no color representation" + ))); + } + validate_strict_i32_values(representations, "endpoint color representations")?; + } + Ok(()) + } + + fn validate_classes( + self, + catalogs: &FermionPairingCatalogs<'_>, + header: FermionPairingHeaderRow, + ) -> RusticolResult<()> { + validate_canonical_ids( + "fermion-pairing class IDs", + self.pairing_classes.iter().map(|row| row.class_id), + )?; + let mut species = BTreeSet::new(); + for (index, class) in self.pairing_classes.iter().copied().enumerate() { + if class.species_class_id != class.class_id { + return Err(invalid(format!( + "fermion class {index} species-class ID is not aligned" + ))); + } + let species_id = required_string( + &catalogs.strings, + class.species_string_id, + "fermion class species", + )?; + if !species.insert(species_id) { + return Err(invalid(format!( + "fermion class {index} repeats species {species_id:?}" + ))); + } + let fundamental = pairing_range_slice( + self.class_fundamental_slots, + class.fundamental_slot_range, + "class fundamental slots", + )?; + let antifundamental = pairing_range_slice( + self.class_antifundamental_slots, + class.antifundamental_slot_range, + "class antifundamental slots", + )?; + let references = pairing_range_slice( + self.class_reference_pairings, + class.reference_pairing_range, + "class reference pairings", + )?; + validate_strict_u32_values( + fundamental, + header.source_count as usize, + "class fundamental slots", + )?; + validate_strict_u32_values( + antifundamental, + header.source_count as usize, + "class antifundamental slots", + )?; + if fundamental.is_empty() + || fundamental.len() != antifundamental.len() + || references.len() != fundamental.len() + { + return Err(invalid(format!( + "fermion class {index} has unbalanced endpoint ranges" + ))); + } + for ((fundamental, antifundamental), reference) in fundamental + .iter() + .copied() + .zip(antifundamental.iter().copied()) + .zip(references.iter().copied()) + { + if reference.fundamental_source_slot != fundamental + || reference.antifundamental_source_slot != antifundamental + { + return Err(invalid(format!( + "fermion class {index} reference pairings are stale" + ))); + } + } + let expected_pairing_count = checked_factorial_u64( + fundamental.len(), + &format!("fermion class {index} pairing count"), + )?; + if class.pairing_count != expected_pairing_count { + return Err(invalid(format!( + "fermion class {index} declares {} pairings, expected {expected_pairing_count}", + class.pairing_count + ))); + } + SemanticDigest::new(class.proof_digest).map_err(|error| { + invalid(format!( + "fermion class {index} proof digest is invalid: {error}" + )) + })?; + } + for endpoint in self.endpoints { + let class = self.pairing_classes[endpoint.species_class_id as usize]; + if endpoint.species_string_id != class.species_string_id { + return Err(invalid( + "fermion endpoint species disagrees with its pairing class", + )); + } + let slots = if endpoint.color_orientation == 0 { + pairing_range_slice( + self.class_fundamental_slots, + class.fundamental_slot_range, + "class fundamental slots", + )? + } else { + pairing_range_slice( + self.class_antifundamental_slots, + class.antifundamental_slot_range, + "class antifundamental slots", + )? + }; + if slots.binary_search(&endpoint.source_slot).is_err() { + return Err(invalid( + "fermion endpoint is absent from its pairing class orientation", + )); + } + } + Ok(()) + } + + fn validate_exact_integers(self) -> RusticolResult<()> { + validate_canonical_ids( + "fermion exact-integer IDs", + self.exact_integers.iter().map(|row| row.integer_id), + )?; + for (index, row) in self.exact_integers.iter().copied().enumerate() { + if !matches!(row.sign, -1..=1) { + return Err(invalid(format!( + "exact integer {index} has an invalid sign" + ))); + } + let limbs = pairing_range_slice( + self.exact_integer_limbs, + row.limb_range, + "exact-integer limbs", + )?; + if (row.sign == 0 && !limbs.is_empty()) + || (row.sign != 0 && (limbs.is_empty() || limbs.last() == Some(&0))) + { + return Err(invalid(format!( + "exact integer {index} has a noncanonical magnitude" + ))); + } + } + Ok(()) + } + + fn validate_rules( + self, + catalogs: &FermionPairingCatalogs<'_>, + header: FermionPairingHeaderRow, + ) -> RusticolResult<()> { + let expected_rule_count = + self.pairing_classes + .iter() + .enumerate() + .try_fold(1_u64, |count, (index, class)| { + count.checked_mul(class.pairing_count).ok_or_else(|| { + invalid(format!( + "combined fermion-pairing rule count overflows u64 at class {index}" + )) + }) + })?; + let actual_rule_count = u64::try_from(self.rules.len()) + .map_err(|_| invalid("fermion-pairing rule count exceeds u64"))?; + if actual_rule_count != expected_rule_count { + return Err(invalid(format!( + "fermion-pairing catalog contains {actual_rule_count} rules, expected {expected_rule_count}" + ))); + } + validate_canonical_ids( + "fermion-pairing rule IDs", + self.rules.iter().map(|row| row.rule_id), + )?; + for (index, rule) in self.rules.iter().copied().enumerate() { + let class_indices = pairing_range_slice( + self.rule_class_pairing_indices, + rule.class_pairing_index_range, + "rule class indices", + )?; + if class_indices.len() != self.pairing_classes.len() { + return Err(invalid(format!( + "fermion rule {index} does not reference every class" + ))); + } + for (expected_class, encoded) in class_indices.iter().copied().enumerate() { + let expected_class = u32::try_from(expected_class) + .map_err(|_| invalid("fermion pairing class index exceeds u32"))?; + if encoded.class_id != expected_class + || encoded.pairing_index + >= self.pairing_classes[expected_class as usize].pairing_count + { + return Err(invalid(format!( + "fermion rule {index} has an invalid class-local pairing index" + ))); + } + } + let pairs = pairing_range_slice( + self.rule_endpoint_pairings, + rule.endpoint_pairing_range, + "rule endpoint pairings", + )?; + if pairs.windows(2).any(|pair| { + ( + pair[0].fundamental_source_slot, + pair[0].antifundamental_source_slot, + ) >= ( + pair[1].fundamental_source_slot, + pair[1].antifundamental_source_slot, + ) + }) { + return Err(invalid(format!( + "fermion rule {index} endpoint pairs are not canonical" + ))); + } + for pair in pairs { + for source_slot in [ + pair.fundamental_source_slot, + pair.antifundamental_source_slot, + ] { + required_reference( + source_slot, + header.source_count as usize, + "rule endpoint source", + )?; + } + } + let permutation = pairing_range_slice( + self.rule_source_slot_permutations, + rule.source_permutation_range, + "rule source permutation", + )?; + validate_permutation( + permutation, + header.source_count as usize, + &format!("fermion rule {index} source permutation"), + )?; + let lineage = + pairing_range_slice(self.rule_lineages, rule.lineage_range, "rule lineage")?; + if lineage.len() != header.source_count as usize + || lineage.iter().any(|line| { + *line != RECURRENCE_NO_FERMION_LINE && (*line as usize) >= pairs.len() + }) + { + return Err(invalid(format!("fermion rule {index} lineage is invalid"))); + } + if !matches!(rule.fermion_parity, -1 | 1) || rule.multiplicity != 1 { + return Err(invalid(format!( + "fermion rule {index} parity or multiplicity is invalid" + ))); + } + require_string_value( + &catalogs.strings, + rule.proof_algorithm_string_id, + RECURRENCE_FERMION_PAIRING_PROOF_ALGORITHM, + "fermion rule proof algorithm", + )?; + SemanticDigest::new(rule.proof_digest).map_err(|error| { + invalid(format!( + "fermion rule {index} proof digest is invalid: {error}" + )) + })?; + self.validate_rule_factor(rule, index)?; + } + Ok(()) + } + + fn validate_rule_factor( + self, + rule: FermionPairingRuleRow, + rule_index: usize, + ) -> RusticolResult<()> { + for (id, expected, label) in [ + ( + rule.real_numerator_integer_id, + rule.fermion_parity, + "real numerator", + ), + (rule.real_denominator_integer_id, 1, "real denominator"), + (rule.imag_numerator_integer_id, 0, "imaginary numerator"), + (rule.imag_denominator_integer_id, 1, "imaginary denominator"), + ] { + if !exact_integer_equals(self, id, expected)? { + return Err(invalid(format!( + "fermion rule {rule_index} {label} does not match its exact parity factor" + ))); + } + } + Ok(()) + } +} + +/// Physical leading-colour sector representation used by process input v1. +#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +#[repr(u8)] +pub enum ProcessLCSectorKind { + Singlet = 0, + OpenLines = 1, + SingleTrace = 2, +} + +impl TryFrom for ProcessLCSectorKind { + type Error = RusticolError; + + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(Self::Singlet), + 1 => Ok(Self::OpenLines), + 2 => Ok(Self::SingleTrace), + _ => Err(invalid(format!( + "unsupported recurrence LC sector kind {value}" + ))), + } + } +} + +/// Closed semantic-template kinds referenced by process input v1. +#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub enum ProcessSemanticTemplateKind { + Parameter, + CurrentState, + Source, + QuantumFlow, + Transition, + Propagator, + Closure, + ColorContraction, + SymmetryProof, +} + +impl ProcessSemanticTemplateKind { + pub const fn as_str(self) -> &'static str { + match self { + Self::Parameter => "parameter", + Self::CurrentState => "current-state", + Self::Source => "source", + Self::QuantumFlow => "quantum-flow", + Self::Transition => "transition", + Self::Propagator => "propagator", + Self::Closure => "closure", + Self::ColorContraction => "color-contraction", + Self::SymmetryProof => "symmetry-proof", + } + } +} + +impl TryFrom<&str> for ProcessSemanticTemplateKind { + type Error = RusticolError; + + fn try_from(value: &str) -> Result { + match value { + "parameter" => Ok(Self::Parameter), + "current-state" => Ok(Self::CurrentState), + "source" => Ok(Self::Source), + "quantum-flow" => Ok(Self::QuantumFlow), + "transition" => Ok(Self::Transition), + "propagator" => Ok(Self::Propagator), + "closure" => Ok(Self::Closure), + "color-contraction" => Ok(Self::ColorContraction), + "symmetry-proof" => Ok(Self::SymmetryProof), + _ => Err(invalid(format!( + "unsupported process semantic-template kind {value:?}" + ))), + } + } +} + +/// Typed identity of one model-wide semantic template. +#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub struct ProcessSemanticTemplateId { + pub kind: ProcessSemanticTemplateKind, + pub template_id: u32, +} + +/// Decoded process-side claim about one model-wide semantic template. +#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub struct ProcessSemanticTemplateReference { + pub typed_id: ProcessSemanticTemplateId, + pub semantic_digest: SemanticDigest, + pub prepared_kernel_id: Option, +} + +/// Semantic digest role carried by the process header. +#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] +pub enum ProcessDigestRole { + Process, + ModelCatalog, + PreparedCatalog, + ColorPlan, + Extension(String), +} + +impl ProcessDigestRole { + pub fn decode(value: &str) -> RusticolResult { + if value.is_empty() { + return Err(invalid("process semantic digest role must not be empty")); + } + Ok(match value { + "process" => Self::Process, + "model-catalog" => Self::ModelCatalog, + "prepared-catalog" => Self::PreparedCatalog, + "color-plan" => Self::ColorPlan, + extension => Self::Extension(extension.to_owned()), + }) + } + + pub fn as_str(&self) -> &str { + match self { + Self::Process => "process", + Self::ModelCatalog => "model-catalog", + Self::PreparedCatalog => "prepared-catalog", + Self::ColorPlan => "color-plan", + Self::Extension(value) => value, + } + } +} + +/// Decoded semantic identity needed by composite catalog authentication. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct RecurrenceProcessSemanticIdentity { + input_digest: SemanticDigest, + process_digest: SemanticDigest, + model_catalog_digest: SemanticDigest, + prepared_catalog_digest: SemanticDigest, + color_plan_digest: SemanticDigest, + extension_digests: BTreeMap, +} + +impl RecurrenceProcessSemanticIdentity { + pub const fn input_digest(&self) -> SemanticDigest { + self.input_digest + } + + pub const fn process_digest(&self) -> SemanticDigest { + self.process_digest + } + + pub const fn model_catalog_digest(&self) -> SemanticDigest { + self.model_catalog_digest + } + + pub const fn prepared_catalog_digest(&self) -> SemanticDigest { + self.prepared_catalog_digest + } + + pub const fn color_plan_digest(&self) -> SemanticDigest { + self.color_plan_digest + } + + pub const fn extension_digests(&self) -> &BTreeMap { + &self.extension_digests + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct ProcessBitsetRangeRow { + pub id: u32, + pub range: CheckedTableRange, + pub bit_count: u64, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct ProcessCouplingLimitRow { + pub name_string_id: u32, + pub minimum: u32, + pub maximum: u32, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct ProcessDigestCatalogRow { + pub id: u32, + pub value: [u8; 32], +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct ProcessExactFactorRow { + pub id: u32, + pub real_numerator_string_id: u32, + pub real_denominator_string_id: u32, + pub imag_numerator_string_id: u32, + pub imag_denominator_string_id: u32, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct ProcessExternalLegRow { + pub source_slot: u32, + pub public_label: u32, + pub physical_pdg: i32, + pub outgoing_pdg: i32, + pub is_initial: u8, + pub is_fermionic: u8, + pub source_state_range: CheckedTableRange, + pub momentum_mask_id: u32, + pub support_mask_id: u32, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct ProcessHeaderRow { + pub schema_version: u32, + pub abi_string_id: u32, + pub process_id_string_id: u32, + pub layout: u8, + pub selected_flow_mode: u8, + pub selected_source_mode: u8, + pub external_leg_count: u32, + pub physical_sector_count: u32, + pub public_flow_count: u32, + pub replay_partition_count: u32, + pub coupling_limit_count: u32, + pub parameter_projection_count: u32, + pub process_support_mask_id: u32, +} + +impl ProcessHeaderRow { + pub fn strategy(self) -> RusticolResult { + RecurrenceStrategy::try_from(u32::from(self.layout)) + } + + pub fn selected_flow_mode(self) -> RusticolResult { + decode_bool(self.selected_flow_mode, "header.selected_flow_mode") + } + + pub fn selected_source_mode(self) -> RusticolResult { + decode_bool(self.selected_source_mode, "header.selected_source_mode") + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct ProcessHeaderDigestRow { + pub role_string_id: u32, + pub digest_id: u32, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct ProcessLCOpenStringRow { + pub sector_id: u32, + pub ordinal: u32, + pub fundamental_source_slot: u32, + pub antifundamental_source_slot: u32, + pub adjoint_sequence_id: u32, + pub singlet_sequence_id: u32, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct ProcessNormalizationRow { + pub factor_id: u32, + pub convention_string_id: u32, + pub semantic_digest_id: u32, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct ProcessParameterProjectionRow { + pub runtime_slot: u32, + pub runtime_name_string_id: u32, + pub parameter_template_id: u32, + pub prepared_parameter_id: u32, + pub component: u32, +} + +impl ProcessParameterProjectionRow { + pub const fn prepared_parameter_id(self) -> Option { + if self.prepared_parameter_id == MISSING_ID { + None + } else { + Some(self.prepared_parameter_id) + } + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct ProcessPhysicalLCSectorRow { + pub sector_id: u32, + pub public_id_string_id: u32, + pub kind: u8, + pub closure_source_slot: u32, + pub closure_proof_algorithm_string_id: u32, + pub closure_proof_digest_id: u32, + pub open_string_range: CheckedTableRange, + pub trace_sequence_id: u32, + pub singlet_sequence_id: u32, + pub word_sequence_id: u32, + pub support_mask_id: u32, +} + +impl ProcessPhysicalLCSectorRow { + pub fn kind(self) -> RusticolResult { + ProcessLCSectorKind::try_from(self.kind) + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct ProcessPublicLCFlowRow { + pub flow_id: u32, + pub public_id_string_id: u32, + pub construction_sector_id: u32, + pub word_sequence_id: u32, + pub source_slot_permutation_sequence_id: u32, + pub reduction_weight_factor_id: u32, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct ProcessReplayPartitionRow { + pub partition_id: u32, + pub representative_sector_id: u32, + pub materialized_sector_id: u32, + pub target_range: CheckedTableRange, + pub proof_algorithm_string_id: u32, + pub proof_digest_id: u32, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct ProcessReplayTargetRow { + pub partition_id: u32, + pub sector_id: u32, + pub external_permutation_sequence_id: u32, + pub source_slot_permutation_sequence_id: u32, + pub amplitude_phase_factor_id: u32, + pub fermion_sign: i32, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct ProcessSelectedPublicFlowRow { + pub flow_id: u32, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct ProcessSelectedSourceStateRow { + pub source_slot: u32, + pub source_state_index: u32, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct ProcessSemanticTemplateReferenceRow { + pub kind_string_id: u32, + pub template_id: u32, + pub semantic_digest_id: u32, + pub prepared_kernel_id: u32, +} + +impl ProcessSemanticTemplateReferenceRow { + pub const fn prepared_kernel_id(self) -> Option { + if self.prepared_kernel_id == MISSING_ID { + None + } else { + Some(self.prepared_kernel_id) + } + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct ProcessSourceStateRow { + pub source_slot: u32, + pub state_index: u32, + pub public_helicity: i32, + pub chirality: i32, + pub spin_state: i32, + pub current_state_template_id: u32, + pub source_template_id: u32, + pub momentum_sign: i32, + pub crossing_phase_factor_id: u32, +} + +/// Fixed-width header for one process-owned external-fermion pairing catalog. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct FermionPairingHeaderRow { + pub schema_version: u32, + pub abi_string_id: u32, + pub process_key_string_id: u32, + pub proof_algorithm_string_id: u32, + pub source_count: u32, + pub endpoint_count: u32, + pub pairing_class_count: u32, + pub rule_count: u32, + pub endpoint_state_template_count: u64, + pub endpoint_anti_state_template_count: u64, + pub endpoint_basis_count: u64, + pub endpoint_color_representation_count: u64, + pub class_fundamental_slot_count: u64, + pub class_antifundamental_slot_count: u64, + pub class_reference_pairing_count: u64, + pub rule_class_pairing_index_count: u64, + pub rule_endpoint_pairing_count: u64, + pub rule_source_permutation_count: u64, + pub rule_lineage_count: u64, + pub exact_integer_count: u32, + pub exact_integer_limb_count: u64, + pub string_count: u32, + pub string_byte_count: u64, + pub no_fermion_line: u32, + pub topology_digest: [u8; 32], + pub semantic_digest: [u8; 32], +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct FermionPairingEndpointRow { + pub endpoint_id: u32, + pub source_slot: u32, + pub public_label: u32, + pub species_class_id: u32, + pub species_string_id: u32, + pub particle_orientation: u8, + pub color_orientation: u8, + pub state_template_range: CheckedTableRange, + pub anti_state_template_range: CheckedTableRange, + pub basis_range: CheckedTableRange, + pub color_representation_range: CheckedTableRange, + pub contract_digest: [u8; 32], +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct FermionPairingClassRow { + pub class_id: u32, + pub species_class_id: u32, + pub species_string_id: u32, + pub fundamental_slot_range: CheckedTableRange, + pub antifundamental_slot_range: CheckedTableRange, + pub reference_pairing_range: CheckedTableRange, + pub pairing_count: u64, + pub proof_digest: [u8; 32], +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct FermionPairingRuleRow { + pub rule_id: u32, + pub class_pairing_index_range: CheckedTableRange, + pub endpoint_pairing_range: CheckedTableRange, + pub source_permutation_range: CheckedTableRange, + pub lineage_range: CheckedTableRange, + pub fermion_parity: i32, + pub real_numerator_integer_id: u32, + pub real_denominator_integer_id: u32, + pub imag_numerator_integer_id: u32, + pub imag_denominator_integer_id: u32, + pub multiplicity: u64, + pub proof_algorithm_string_id: u32, + pub proof_digest: [u8; 32], +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct FermionPairingClassPairingIndexRow { + pub class_id: u32, + pub pairing_index: u64, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct FermionPairingEndpointPairRow { + pub fundamental_source_slot: u32, + pub antifundamental_source_slot: u32, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct FermionPairingExactIntegerRow { + pub integer_id: u32, + pub sign: i32, + pub limb_range: CheckedTableRange, +} + +#[derive(Clone, Copy, Debug)] +pub struct FermionPairingInputView<'a> { + pub input_abi: &'a str, + pub declared_columnar_digest: SemanticDigest, + pub header: &'a [FermionPairingHeaderRow], + pub endpoints: &'a [FermionPairingEndpointRow], + pub endpoint_state_template_ids: &'a [u32], + pub endpoint_anti_state_template_ids: &'a [u32], + pub endpoint_basis_ids: &'a [u32], + pub endpoint_color_representations: &'a [i32], + pub pairing_classes: &'a [FermionPairingClassRow], + pub class_fundamental_slots: &'a [u32], + pub class_antifundamental_slots: &'a [u32], + pub class_reference_pairings: &'a [FermionPairingEndpointPairRow], + pub rules: &'a [FermionPairingRuleRow], + pub rule_class_pairing_indices: &'a [FermionPairingClassPairingIndexRow], + pub rule_endpoint_pairings: &'a [FermionPairingEndpointPairRow], + pub rule_source_slot_permutations: &'a [u32], + pub rule_lineages: &'a [u32], + pub exact_integers: &'a [FermionPairingExactIntegerRow], + pub exact_integer_limbs: &'a [u64], + pub string_ranges: &'a [CheckedTableRange], + pub string_bytes: &'a [u8], +} + +#[derive(Clone, Debug)] +pub struct OwnedFermionPairingInput { + pub input_abi: String, + pub declared_columnar_digest: SemanticDigest, + pub header: Vec, + pub endpoints: Vec, + pub endpoint_state_template_ids: Vec, + pub endpoint_anti_state_template_ids: Vec, + pub endpoint_basis_ids: Vec, + pub endpoint_color_representations: Vec, + pub pairing_classes: Vec, + pub class_fundamental_slots: Vec, + pub class_antifundamental_slots: Vec, + pub class_reference_pairings: Vec, + pub rules: Vec, + pub rule_class_pairing_indices: Vec, + pub rule_endpoint_pairings: Vec, + pub rule_source_slot_permutations: Vec, + pub rule_lineages: Vec, + pub exact_integers: Vec, + pub exact_integer_limbs: Vec, + pub string_ranges: Vec, + pub string_bytes: Vec, +} + +impl OwnedFermionPairingInput { + pub fn as_view(&self) -> FermionPairingInputView<'_> { + FermionPairingInputView { + input_abi: &self.input_abi, + declared_columnar_digest: self.declared_columnar_digest, + header: &self.header, + endpoints: &self.endpoints, + endpoint_state_template_ids: &self.endpoint_state_template_ids, + endpoint_anti_state_template_ids: &self.endpoint_anti_state_template_ids, + endpoint_basis_ids: &self.endpoint_basis_ids, + endpoint_color_representations: &self.endpoint_color_representations, + pairing_classes: &self.pairing_classes, + class_fundamental_slots: &self.class_fundamental_slots, + class_antifundamental_slots: &self.class_antifundamental_slots, + class_reference_pairings: &self.class_reference_pairings, + rules: &self.rules, + rule_class_pairing_indices: &self.rule_class_pairing_indices, + rule_endpoint_pairings: &self.rule_endpoint_pairings, + rule_source_slot_permutations: &self.rule_source_slot_permutations, + rule_lineages: &self.rule_lineages, + exact_integers: &self.exact_integers, + exact_integer_limbs: &self.exact_integer_limbs, + string_ranges: &self.string_ranges, + string_bytes: &self.string_bytes, + } + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct FermionPairingValidationSummary { + columnar_digest: SemanticDigest, + topology_digest: SemanticDigest, + semantic_digest: SemanticDigest, + source_count: u32, + endpoint_count: u32, + pairing_class_count: u32, + rule_count: u32, +} + +impl FermionPairingValidationSummary { + pub const fn columnar_digest(&self) -> SemanticDigest { + self.columnar_digest + } + + pub const fn topology_digest(&self) -> SemanticDigest { + self.topology_digest + } + + pub const fn semantic_digest(&self) -> SemanticDigest { + self.semantic_digest + } + + pub const fn source_count(&self) -> u32 { + self.source_count + } + + pub const fn endpoint_count(&self) -> u32 { + self.endpoint_count + } + + pub const fn pairing_class_count(&self) -> u32 { + self.pairing_class_count + } + + pub const fn rule_count(&self) -> u32 { + self.rule_count + } +} + +macro_rules! define_process_inputs { + ($( $field:ident : $item:ty ),+ $(,)?) => { + #[derive(Clone, Copy, Debug)] + pub struct RecurrenceProcessInputView<'a> { + pub input_abi: &'a str, + pub declared_input_digest: SemanticDigest, + pub fermion_pairing: Option>, + $(pub $field: &'a [$item],)+ + } + + #[derive(Clone, Debug)] + pub struct OwnedRecurrenceProcessInput { + pub input_abi: String, + pub declared_input_digest: SemanticDigest, + pub fermion_pairing: Option, + $(pub $field: Vec<$item>,)+ + } + + impl OwnedRecurrenceProcessInput { + pub fn as_view(&self) -> RecurrenceProcessInputView<'_> { + RecurrenceProcessInputView { + input_abi: &self.input_abi, + declared_input_digest: self.declared_input_digest, + fermion_pairing: self.fermion_pairing.as_ref().map(OwnedFermionPairingInput::as_view), + $($field: &self.$field,)+ + } + } + + pub fn validate(self) -> RusticolResult { + let validated = self.as_view().validate()?; + Ok(ValidatedRecurrenceProcessInput { + input: self, + summary: validated.summary, + semantic_identity: validated.semantic_identity, + template_references: validated.template_references, + fermion_pairing_summary: validated.fermion_pairing_summary, + }) + } + } + }; +} + +define_process_inputs! { + bitset_ranges: ProcessBitsetRangeRow, + bitset_words: u64, + coupling_limits: ProcessCouplingLimitRow, + digest_catalog: ProcessDigestCatalogRow, + exact_factors: ProcessExactFactorRow, + external_legs: ProcessExternalLegRow, + header: ProcessHeaderRow, + header_digests: ProcessHeaderDigestRow, + lc_open_strings: ProcessLCOpenStringRow, + normalization: ProcessNormalizationRow, + parameter_projection: ProcessParameterProjectionRow, + physical_lc_sectors: ProcessPhysicalLCSectorRow, + public_lc_flows: ProcessPublicLCFlowRow, + replay_partitions: ProcessReplayPartitionRow, + replay_targets: ProcessReplayTargetRow, + selected_public_flow_coverage: ProcessSelectedPublicFlowRow, + selected_source_coverage: ProcessSelectedSourceStateRow, + semantic_template_references: ProcessSemanticTemplateReferenceRow, + source_states: ProcessSourceStateRow, + string_ranges: CheckedTableRange, + string_bytes: u8, + u32_sequence_ranges: CheckedTableRange, + u32_sequence_values: u32, +} + +/// Counts and process identity produced by process-input validation. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct RecurrenceProcessValidationSummary { + process_id: String, + strategy: RecurrenceStrategy, + selected_flow_mode: bool, + selected_source_mode: bool, + external_leg_count: u32, + source_state_count: u32, + physical_sector_count: u32, + public_flow_count: u32, + replay_partition_count: u32, + replay_target_count: u32, + template_reference_count: u32, +} + +impl RecurrenceProcessValidationSummary { + pub fn process_id(&self) -> &str { + &self.process_id + } + + pub const fn strategy(&self) -> RecurrenceStrategy { + self.strategy + } + + pub const fn selected_flow_mode(&self) -> bool { + self.selected_flow_mode + } + + pub const fn selected_source_mode(&self) -> bool { + self.selected_source_mode + } + + pub const fn external_leg_count(&self) -> u32 { + self.external_leg_count + } + + pub const fn source_state_count(&self) -> u32 { + self.source_state_count + } + + pub const fn physical_sector_count(&self) -> u32 { + self.physical_sector_count + } + + pub const fn public_flow_count(&self) -> u32 { + self.public_flow_count + } + + pub const fn replay_partition_count(&self) -> u32 { + self.replay_partition_count + } + + pub const fn replay_target_count(&self) -> u32 { + self.replay_target_count + } + + pub const fn template_reference_count(&self) -> u32 { + self.template_reference_count + } +} + +/// Validated process payload retained for later builder and catalog checks. +#[derive(Clone, Debug)] +pub struct ValidatedRecurrenceProcessInput { + input: OwnedRecurrenceProcessInput, + summary: RecurrenceProcessValidationSummary, + semantic_identity: RecurrenceProcessSemanticIdentity, + template_references: Vec, + fermion_pairing_summary: Option, +} + +/// Zero-copy access to a process-owned pairing catalog after full validation. +#[derive(Clone, Copy, Debug)] +pub struct ValidatedFermionPairingCatalog<'a> { + input: FermionPairingInputView<'a>, + summary: &'a FermionPairingValidationSummary, +} + +impl<'a> ValidatedFermionPairingCatalog<'a> { + pub const fn summary(self) -> &'a FermionPairingValidationSummary { + self.summary + } + + pub const fn endpoints(self) -> &'a [FermionPairingEndpointRow] { + self.input.endpoints + } + + pub const fn pairing_classes(self) -> &'a [FermionPairingClassRow] { + self.input.pairing_classes + } + + pub const fn rules(self) -> &'a [FermionPairingRuleRow] { + self.input.rules + } + + pub fn class_pairing_indices( + self, + rule: FermionPairingRuleRow, + ) -> RusticolResult<&'a [FermionPairingClassPairingIndexRow]> { + pairing_range_slice( + self.input.rule_class_pairing_indices, + rule.class_pairing_index_range, + "fermion pairing rule class indices", + ) + } + + pub fn endpoint_pairings( + self, + rule: FermionPairingRuleRow, + ) -> RusticolResult<&'a [FermionPairingEndpointPairRow]> { + pairing_range_slice( + self.input.rule_endpoint_pairings, + rule.endpoint_pairing_range, + "fermion pairing rule endpoint pairs", + ) + } + + pub fn source_slot_permutation(self, rule: FermionPairingRuleRow) -> RusticolResult<&'a [u32]> { + pairing_range_slice( + self.input.rule_source_slot_permutations, + rule.source_permutation_range, + "fermion pairing rule source permutation", + ) + } + + pub fn lineage(self, rule: FermionPairingRuleRow) -> RusticolResult<&'a [u32]> { + pairing_range_slice( + self.input.rule_lineages, + rule.lineage_range, + "fermion pairing rule lineage", + ) + } +} + +impl ValidatedRecurrenceProcessInput { + pub const fn input(&self) -> &OwnedRecurrenceProcessInput { + &self.input + } + + pub const fn summary(&self) -> &RecurrenceProcessValidationSummary { + &self.summary + } + + pub const fn semantic_identity(&self) -> &RecurrenceProcessSemanticIdentity { + &self.semantic_identity + } + + pub fn template_references(&self) -> &[ProcessSemanticTemplateReference] { + &self.template_references + } + + pub fn template_reference( + &self, + typed_id: ProcessSemanticTemplateId, + ) -> Option<&ProcessSemanticTemplateReference> { + self.template_references + .binary_search_by_key(&typed_id, |reference| reference.typed_id) + .ok() + .map(|index| &self.template_references[index]) + } + + pub fn fermion_pairing_catalog(&self) -> Option> { + Some(ValidatedFermionPairingCatalog { + input: self.input.fermion_pairing.as_ref()?.as_view(), + summary: self.fermion_pairing_summary.as_ref()?, + }) + } + + pub const fn fermion_pairing_summary(&self) -> Option<&FermionPairingValidationSummary> { + self.fermion_pairing_summary.as_ref() + } + + pub fn into_input(self) -> OwnedRecurrenceProcessInput { + self.input + } +} + +struct ValidatedProcessMetadata { + summary: RecurrenceProcessValidationSummary, + semantic_identity: RecurrenceProcessSemanticIdentity, + template_references: Vec, + fermion_pairing_summary: Option, +} + +struct ProcessCatalogs<'a> { + strings: Vec<&'a str>, + digests: Vec, + factors: Vec, +} + +impl<'a> RecurrenceProcessInputView<'a> { + fn validate(self) -> RusticolResult { + if self.input_abi != RECURRENCE_BUILDER_INPUT_ABI { + return Err(RusticolError::compatibility(format!( + "unsupported recurrence process input ABI {:?}; expected {:?}", + self.input_abi, RECURRENCE_BUILDER_INPUT_ABI + ))); + } + + let catalogs = self.validate_catalogs()?; + let (strategy, selected_flow_mode, selected_source_mode, process_id) = + self.validate_header(&catalogs)?; + let semantic_identity = self.validate_semantic_identity(&catalogs)?; + let template_references = self.validate_template_references(&catalogs)?; + + self.validate_dense_ids()?; + self.validate_common_references(&catalogs)?; + self.validate_external_legs(&catalogs, &template_references)?; + self.validate_color_sectors(&catalogs)?; + self.validate_public_flows(&catalogs)?; + self.validate_replay(&catalogs, strategy)?; + self.validate_generation_coverage(selected_flow_mode, selected_source_mode)?; + self.validate_couplings_and_parameters(&catalogs, &template_references)?; + self.validate_normalization(&catalogs)?; + let fermion_pairing_summary = + self.validate_fermion_pairing(process_id, &semantic_identity)?; + + Ok(ValidatedProcessMetadata { + summary: RecurrenceProcessValidationSummary { + process_id: process_id.to_owned(), + strategy, + selected_flow_mode, + selected_source_mode, + external_leg_count: checked_u32_len( + self.external_legs.len(), + "process external legs", + )?, + source_state_count: checked_u32_len( + self.source_states.len(), + "process source states", + )?, + physical_sector_count: checked_u32_len( + self.physical_lc_sectors.len(), + "physical LC sectors", + )?, + public_flow_count: checked_u32_len(self.public_lc_flows.len(), "public LC flows")?, + replay_partition_count: checked_u32_len( + self.replay_partitions.len(), + "replay partitions", + )?, + replay_target_count: checked_u32_len(self.replay_targets.len(), "replay targets")?, + template_reference_count: checked_u32_len( + template_references.len(), + "semantic-template references", + )?, + }, + semantic_identity, + template_references, + fermion_pairing_summary, + }) + } + + fn validate_catalogs(self) -> RusticolResult> { + validate_packed_ranges( + "recurrence process strings", + self.string_ranges, + self.string_bytes.len(), + )?; + let mut strings = Vec::with_capacity(self.string_ranges.len()); + let mut previous: Option<&[u8]> = None; + for (index, range) in self.string_ranges.iter().copied().enumerate() { + let bytes = &self.string_bytes[range + .as_usize_range(self.string_bytes.len(), &format!("process string {index}"))?]; + if bytes.is_empty() { + return Err(invalid(format!( + "recurrence process string catalog row {index} is empty" + ))); + } + if let Some(previous) = previous + && previous >= bytes + { + return Err(invalid(format!( + "recurrence process string catalog is not in strict canonical byte order at row {index}" + ))); + } + let value = std::str::from_utf8(bytes).map_err(|error| { + invalid(format!( + "recurrence process string catalog row {index} is not UTF-8: {error}" + )) + })?; + previous = Some(bytes); + strings.push(value); + } + + validate_packed_ranges( + "recurrence process u32 sequences", + self.u32_sequence_ranges, + self.u32_sequence_values.len(), + )?; + let mut previous_sequence: Option<&[u32]> = None; + for (index, range) in self.u32_sequence_ranges.iter().copied().enumerate() { + let values = &self.u32_sequence_values[range.as_usize_range( + self.u32_sequence_values.len(), + &format!("process u32 sequence {index}"), + )?]; + if let Some(previous) = previous_sequence + && previous >= values + { + return Err(invalid(format!( + "recurrence process u32-sequence catalog is not in strict canonical order at row {index}" + ))); + } + previous_sequence = Some(values); + } + + validate_canonical_ids( + "process bitset catalog", + self.bitset_ranges.iter().map(|row| row.id), + )?; + let mask_ranges = self + .bitset_ranges + .iter() + .map(|row| row.range) + .collect::>(); + let populations = self + .bitset_ranges + .iter() + .map(|row| row.bit_count) + .collect::>(); + MultiwordMaskCatalogView { + ranges: &mask_ranges, + populations: &populations, + words: self.bitset_words, + } + .validate(true)?; + + validate_canonical_ids( + "process digest catalog", + self.digest_catalog.iter().map(|row| row.id), + )?; + let mut digests = Vec::with_capacity(self.digest_catalog.len()); + let mut previous_digest = None; + for (index, row) in self.digest_catalog.iter().enumerate() { + let value = SemanticDigest::new(row.value).map_err(|error| { + invalid(format!( + "recurrence process digest catalog row {index} is invalid: {error}" + )) + })?; + if let Some(previous) = previous_digest + && previous >= value + { + return Err(invalid(format!( + "recurrence process digest catalog is not in strict canonical order at row {index}" + ))); + } + previous_digest = Some(value); + digests.push(value); + } + + validate_canonical_ids( + "process exact-factor catalog", + self.exact_factors.iter().map(|row| row.id), + )?; + let mut factors = Vec::with_capacity(self.exact_factors.len()); + let mut previous_factor = None; + for (index, row) in self.exact_factors.iter().enumerate() { + let real_numerator = required_string( + &strings, + row.real_numerator_string_id, + "process exact real numerator", + )?; + let real_denominator = required_string( + &strings, + row.real_denominator_string_id, + "process exact real denominator", + )?; + let imag_numerator = required_string( + &strings, + row.imag_numerator_string_id, + "process exact imaginary numerator", + )?; + let imag_denominator = required_string( + &strings, + row.imag_denominator_string_id, + "process exact imaginary denominator", + )?; + let factor = ExactComplexRational::parse_parts( + real_numerator, + real_denominator, + imag_numerator, + imag_denominator, + ) + .map_err(|error| { + invalid(format!( + "recurrence process exact factor {index} is invalid: {error}" + )) + })?; + let canonical = ( + factor.real().numerator(), + factor.real().denominator(), + factor.imag().numerator(), + factor.imag().denominator(), + ); + if canonical.0.to_string() != real_numerator + || canonical.1.to_string() != real_denominator + || canonical.2.to_string() != imag_numerator + || canonical.3.to_string() != imag_denominator + { + return Err(invalid(format!( + "recurrence process exact factor {index} is not in canonical reduced decimal form" + ))); + } + if let Some(previous) = previous_factor + && previous >= canonical + { + return Err(invalid(format!( + "recurrence process exact-factor catalog is not in strict canonical order at row {index}" + ))); + } + previous_factor = Some(canonical); + factors.push(factor); + } + + Ok(ProcessCatalogs { + strings, + digests, + factors, + }) + } + + fn validate_header<'b>( + self, + catalogs: &'b ProcessCatalogs<'_>, + ) -> RusticolResult<(RecurrenceStrategy, bool, bool, &'b str)> { + if self.header.len() != 1 { + return Err(invalid(format!( + "recurrence process header must contain one row, found {}", + self.header.len() + ))); + } + let header = self.header[0]; + if header.schema_version != RECURRENCE_PROCESS_INPUT_SCHEMA_VERSION { + return Err(RusticolError::compatibility(format!( + "unsupported recurrence process input schema {}; expected {}", + header.schema_version, RECURRENCE_PROCESS_INPUT_SCHEMA_VERSION + ))); + } + require_string_value( + &catalogs.strings, + header.abi_string_id, + RECURRENCE_BUILDER_INPUT_ABI, + "recurrence process ABI", + )?; + let process_id = required_string( + &catalogs.strings, + header.process_id_string_id, + "recurrence process ID", + )?; + let strategy = header.strategy()?; + let selected_flow_mode = header.selected_flow_mode()?; + let selected_source_mode = header.selected_source_mode()?; + + let expected = [ + ( + "external legs", + header.external_leg_count, + self.external_legs.len(), + ), + ( + "physical LC sectors", + header.physical_sector_count, + self.physical_lc_sectors.len(), + ), + ( + "public LC flows", + header.public_flow_count, + self.public_lc_flows.len(), + ), + ( + "replay partitions", + header.replay_partition_count, + self.replay_partitions.len(), + ), + ( + "coupling limits", + header.coupling_limit_count, + self.coupling_limits.len(), + ), + ( + "parameter projections", + header.parameter_projection_count, + self.parameter_projection.len(), + ), + ]; + for (label, declared, actual) in expected { + let actual = checked_u32_len(actual, label)?; + if declared != actual { + return Err(invalid(format!( + "recurrence process header declares {declared} {label}, found {actual}" + ))); + } + } + required_reference( + header.process_support_mask_id, + self.bitset_ranges.len(), + "process support mask", + )?; + if selected_flow_mode == self.selected_public_flow_coverage.is_empty() { + return Err(invalid( + "selected-flow header mode disagrees with process coverage rows", + )); + } + if selected_source_mode == self.selected_source_coverage.is_empty() { + return Err(invalid( + "selected-source header mode disagrees with process coverage rows", + )); + } + Ok(( + strategy, + selected_flow_mode, + selected_source_mode, + process_id, + )) + } + + fn validate_semantic_identity( + self, + catalogs: &ProcessCatalogs<'_>, + ) -> RusticolResult { + if self.header_digests.len() < REQUIRED_DIGEST_ROLE_COUNT { + return Err(invalid( + "recurrence process header has too few semantic digest roles", + )); + } + let mut values = BTreeMap::new(); + let mut previous_role = None; + for (index, row) in self.header_digests.iter().enumerate() { + let role_text = required_string( + &catalogs.strings, + row.role_string_id, + "process semantic digest role", + )?; + if let Some(previous) = previous_role + && previous >= role_text + { + return Err(invalid(format!( + "process semantic digest roles are not in strict canonical order at row {index}" + ))); + } + previous_role = Some(role_text); + let role = ProcessDigestRole::decode(role_text)?; + let digest = + required_digest(&catalogs.digests, row.digest_id, "process semantic digest")?; + if values.insert(role, digest).is_some() { + return Err(invalid(format!( + "process semantic digest role {role_text:?} is repeated" + ))); + } + } + + let process_digest = take_required_role(&mut values, ProcessDigestRole::Process)?; + let model_catalog_digest = + take_required_role(&mut values, ProcessDigestRole::ModelCatalog)?; + let prepared_catalog_digest = + take_required_role(&mut values, ProcessDigestRole::PreparedCatalog)?; + let color_plan_digest = take_required_role(&mut values, ProcessDigestRole::ColorPlan)?; + let extension_digests = values + .into_iter() + .map(|(role, digest)| (role.as_str().to_owned(), digest)) + .collect(); + Ok(RecurrenceProcessSemanticIdentity { + input_digest: self.declared_input_digest, + process_digest, + model_catalog_digest, + prepared_catalog_digest, + color_plan_digest, + extension_digests, + }) + } + + fn validate_template_references( + self, + catalogs: &ProcessCatalogs<'_>, + ) -> RusticolResult> { + let mut references = Vec::with_capacity(self.semantic_template_references.len()); + let mut seen_ids = BTreeSet::new(); + let mut seen_digests = BTreeSet::new(); + let mut previous_raw = None; + for (index, row) in self + .semantic_template_references + .iter() + .copied() + .enumerate() + { + let kind_text = required_string( + &catalogs.strings, + row.kind_string_id, + "process semantic-template kind", + )?; + let raw_key = (kind_text, row.template_id); + if let Some(previous) = previous_raw + && previous >= raw_key + { + return Err(invalid(format!( + "process semantic-template references are not in strict canonical order at row {index}" + ))); + } + previous_raw = Some(raw_key); + let typed_id = ProcessSemanticTemplateId { + kind: ProcessSemanticTemplateKind::try_from(kind_text)?, + template_id: row.template_id, + }; + if !seen_ids.insert(typed_id) { + return Err(invalid(format!( + "process repeats semantic-template reference ({kind_text:?}, {})", + row.template_id + ))); + } + let semantic_digest = required_digest( + &catalogs.digests, + row.semantic_digest_id, + "semantic-template digest", + )?; + if !seen_digests.insert(semantic_digest) { + return Err(invalid(format!( + "process semantic-template reference row {index} reuses semantic digest {semantic_digest}" + ))); + } + references.push(ProcessSemanticTemplateReference { + typed_id, + semantic_digest, + prepared_kernel_id: row.prepared_kernel_id(), + }); + } + references.sort_unstable_by_key(|reference| reference.typed_id); + Ok(references) + } + + fn validate_dense_ids(self) -> RusticolResult<()> { + validate_canonical_ids( + "external source slots", + self.external_legs.iter().map(|row| row.source_slot), + )?; + validate_canonical_ids( + "physical LC sector IDs", + self.physical_lc_sectors.iter().map(|row| row.sector_id), + )?; + validate_canonical_ids( + "public LC flow IDs", + self.public_lc_flows.iter().map(|row| row.flow_id), + )?; + validate_canonical_ids( + "replay partition IDs", + self.replay_partitions.iter().map(|row| row.partition_id), + )?; + validate_canonical_ids( + "runtime parameter slots", + self.parameter_projection.iter().map(|row| row.runtime_slot), + )?; + Ok(()) + } + + fn validate_common_references(self, catalogs: &ProcessCatalogs<'_>) -> RusticolResult<()> { + let string_count = catalogs.strings.len(); + validate_u32_references( + &self + .coupling_limits + .iter() + .map(|row| row.name_string_id) + .collect::>(), + string_count, + "coupling-limit names", + )?; + validate_u32_references( + &self + .physical_lc_sectors + .iter() + .flat_map(|row| { + [ + row.public_id_string_id, + row.closure_proof_algorithm_string_id, + ] + }) + .chain( + self.public_lc_flows + .iter() + .map(|row| row.public_id_string_id), + ) + .chain( + self.replay_partitions + .iter() + .map(|row| row.proof_algorithm_string_id), + ) + .chain( + self.normalization + .iter() + .map(|row| row.convention_string_id), + ) + .chain( + self.parameter_projection + .iter() + .map(|row| row.runtime_name_string_id), + ) + .collect::>(), + string_count, + "process string references", + )?; + + validate_u32_references( + &self + .physical_lc_sectors + .iter() + .map(|row| row.closure_proof_digest_id) + .collect::>(), + catalogs.digests.len(), + "physical LC closure proof digests", + )?; + + let sequence_count = self.u32_sequence_ranges.len(); + validate_u32_references( + &self + .lc_open_strings + .iter() + .flat_map(|row| [row.adjoint_sequence_id, row.singlet_sequence_id]) + .chain(self.physical_lc_sectors.iter().flat_map(|row| { + [ + row.trace_sequence_id, + row.singlet_sequence_id, + row.word_sequence_id, + ] + })) + .chain(self.public_lc_flows.iter().flat_map(|row| { + [ + row.word_sequence_id, + row.source_slot_permutation_sequence_id, + ] + })) + .chain(self.replay_targets.iter().flat_map(|row| { + [ + row.external_permutation_sequence_id, + row.source_slot_permutation_sequence_id, + ] + })) + .collect::>(), + sequence_count, + "process u32-sequence references", + )?; + + let bitset_count = self.bitset_ranges.len(); + validate_u32_references( + &self + .external_legs + .iter() + .flat_map(|row| [row.momentum_mask_id, row.support_mask_id]) + .chain( + self.physical_lc_sectors + .iter() + .map(|row| row.support_mask_id), + ) + .collect::>(), + bitset_count, + "process bitset references", + )?; + + let factor_count = catalogs.factors.len(); + validate_u32_references( + &self + .source_states + .iter() + .map(|row| row.crossing_phase_factor_id) + .chain( + self.public_lc_flows + .iter() + .map(|row| row.reduction_weight_factor_id), + ) + .chain( + self.replay_targets + .iter() + .map(|row| row.amplitude_phase_factor_id), + ) + .chain(self.normalization.iter().map(|row| row.factor_id)) + .collect::>(), + factor_count, + "process exact-factor references", + )?; + + validate_u32_references( + &self + .normalization + .iter() + .map(|row| row.semantic_digest_id) + .chain(self.replay_partitions.iter().map(|row| row.proof_digest_id)) + .collect::>(), + catalogs.digests.len(), + "process semantic-digest references", + )?; + Ok(()) + } + + fn validate_external_legs( + self, + catalogs: &ProcessCatalogs<'_>, + references: &[ProcessSemanticTemplateReference], + ) -> RusticolResult<()> { + if self.external_legs.is_empty() { + return Err(invalid( + "recurrence process input requires at least one external leg", + )); + } + let ranges = self + .external_legs + .iter() + .map(|row| row.source_state_range) + .collect::>(); + validate_packed_ranges("external source states", &ranges, self.source_states.len())?; + + let mask_ranges = self + .bitset_ranges + .iter() + .map(|row| row.range) + .collect::>(); + let populations = self + .bitset_ranges + .iter() + .map(|row| row.bit_count) + .collect::>(); + let masks = MultiwordMaskCatalogView { + ranges: &mask_ranges, + populations: &populations, + words: self.bitset_words, + }; + + let mut public_labels = BTreeSet::new(); + for (source_slot, leg) in self.external_legs.iter().copied().enumerate() { + decode_bool(leg.is_initial, "external_legs.is_initial")?; + decode_bool(leg.is_fermionic, "external_legs.is_fermionic")?; + if !public_labels.insert(leg.public_label) { + return Err(invalid(format!( + "external leg {source_slot} repeats public label {}", + leg.public_label + ))); + } + if leg.source_state_range.count == 0 { + return Err(invalid(format!( + "external leg {source_slot} retains no source states" + ))); + } + let source_slot_u32 = u32::try_from(source_slot) + .map_err(|_| invalid("external source slot exceeds u32"))?; + let momentum_population = self.bitset_ranges[leg.momentum_mask_id as usize].bit_count; + if momentum_population != 1 + || !masks.contains(leg.momentum_mask_id, source_slot as u64)? + { + return Err(invalid(format!( + "external leg {source_slot} momentum mask must contain exactly its source slot" + ))); + } + + let rows = leg.source_state_range.as_usize_range( + self.source_states.len(), + &format!("external leg {source_slot} source states"), + )?; + let mut helicities = BTreeSet::new(); + let mut source_template_ids = BTreeSet::new(); + for (state_index, row_index) in rows.enumerate() { + let state = self.source_states[row_index]; + if state.source_slot != source_slot_u32 { + return Err(invalid(format!( + "source-state row {row_index} belongs to source slot {}, expected {source_slot_u32}", + state.source_slot + ))); + } + let expected_state_index = u32::try_from(state_index) + .map_err(|_| invalid("source-state index exceeds u32"))?; + if state.state_index != expected_state_index { + return Err(invalid(format!( + "source-state row {row_index} has local index {}, expected {expected_state_index}", + state.state_index + ))); + } + if !helicities.insert(state.public_helicity) { + return Err(invalid(format!( + "external leg {source_slot} repeats public helicity {}", + state.public_helicity + ))); + } + if !source_template_ids.insert(state.source_template_id) { + return Err(invalid(format!( + "external leg {source_slot} repeats source template {}", + state.source_template_id + ))); + } + validate_concrete_spin_state( + state.spin_state, + &format!("source-state row {row_index} spin state"), + )?; + if !matches!(state.momentum_sign, -1 | 1) { + return Err(invalid(format!( + "source-state row {row_index} momentum sign must be -1 or 1" + ))); + } + require_process_template( + references, + ProcessSemanticTemplateId { + kind: ProcessSemanticTemplateKind::CurrentState, + template_id: state.current_state_template_id, + }, + &format!("source-state row {row_index} current state"), + )?; + require_process_template( + references, + ProcessSemanticTemplateId { + kind: ProcessSemanticTemplateKind::Source, + template_id: state.source_template_id, + }, + &format!("source-state row {row_index} source template"), + )?; + required_reference( + state.crossing_phase_factor_id, + catalogs.factors.len(), + "source-state crossing phase", + )?; + } + } + Ok(()) + } + + fn validate_color_sectors(self, catalogs: &ProcessCatalogs<'_>) -> RusticolResult<()> { + if self.physical_lc_sectors.is_empty() { + return Err(invalid( + "recurrence process input requires at least one physical LC sector", + )); + } + let ranges = self + .physical_lc_sectors + .iter() + .map(|row| row.open_string_range) + .collect::>(); + validate_packed_ranges( + "physical LC open strings", + &ranges, + self.lc_open_strings.len(), + )?; + + let mut public_ids = BTreeSet::new(); + for (sector_index, sector) in self.physical_lc_sectors.iter().copied().enumerate() { + let public_id = required_string( + &catalogs.strings, + sector.public_id_string_id, + "physical LC sector public ID", + )?; + if !public_ids.insert(public_id) { + return Err(invalid(format!( + "physical LC sector {sector_index} repeats public identifier {public_id:?}" + ))); + } + let kind = sector.kind()?; + required_reference( + sector.closure_source_slot, + self.external_legs.len(), + "physical LC closure source slot", + )?; + let closure_algorithm = required_string( + &catalogs.strings, + sector.closure_proof_algorithm_string_id, + "physical LC closure-anchor proof algorithm", + )?; + if !matches!( + closure_algorithm, + "canonical-lc-closure-anchor-v2" + | "canonical-lc-closure-anchor-v3" + | "canonical-lc-closure-anchor-v4" + ) { + return Err(invalid(format!( + "physical LC sector {sector_index} uses unsupported closure-anchor proof algorithm {closure_algorithm:?}" + ))); + } + let closure_proof_digest = required_digest( + &catalogs.digests, + sector.closure_proof_digest_id, + "physical LC closure-anchor proof digest", + )?; + let trace = self.sequence( + sector.trace_sequence_id, + &format!("physical LC sector {sector_index} trace"), + )?; + let singlets = self.sequence( + sector.singlet_sequence_id, + &format!("physical LC sector {sector_index} singlets"), + )?; + let word = self.sequence( + sector.word_sequence_id, + &format!("physical LC sector {sector_index} word"), + )?; + validate_source_slot_sequence( + trace, + self.external_legs.len(), + &format!("physical LC sector {sector_index} trace"), + )?; + validate_source_slot_sequence( + singlets, + self.external_legs.len(), + &format!("physical LC sector {sector_index} singlets"), + )?; + validate_source_slot_sequence( + word, + self.external_legs.len(), + &format!("physical LC sector {sector_index} word"), + )?; + if let Some(expected_anchor) = word.last().copied() { + if closure_algorithm == "canonical-lc-closure-anchor-v2" + && sector.closure_source_slot != expected_anchor + { + return Err(invalid(format!( + "physical LC sector {sector_index} v2 closure source slot {} does not match terminal color-word endpoint {expected_anchor}", + sector.closure_source_slot + ))); + } + if closure_algorithm == "canonical-lc-closure-anchor-v3" + && kind != ProcessLCSectorKind::OpenLines + && sector.closure_source_slot != expected_anchor + { + return Err(invalid(format!( + "physical LC sector {sector_index} v3 non-open-line closure source slot {} does not match terminal color-word endpoint {expected_anchor}", + sector.closure_source_slot + ))); + } + } else if kind != ProcessLCSectorKind::Singlet { + return Err(invalid(format!( + "non-singlet LC sector {sector_index} has no color word for its closure anchor" + ))); + } + + match kind { + ProcessLCSectorKind::OpenLines if sector.open_string_range.count == 0 => { + return Err(invalid(format!( + "open-lines LC sector {sector_index} has no open strings" + ))); + } + ProcessLCSectorKind::OpenLines if !trace.is_empty() => { + return Err(invalid(format!( + "open-lines LC sector {sector_index} unexpectedly carries a trace" + ))); + } + ProcessLCSectorKind::SingleTrace if trace.is_empty() => { + return Err(invalid(format!( + "single-trace LC sector {sector_index} has an empty trace" + ))); + } + ProcessLCSectorKind::SingleTrace if sector.open_string_range.count != 0 => { + return Err(invalid(format!( + "single-trace LC sector {sector_index} carries open strings" + ))); + } + ProcessLCSectorKind::Singlet + if sector.open_string_range.count != 0 || !trace.is_empty() => + { + return Err(invalid(format!( + "singlet LC sector {sector_index} carries colored strings" + ))); + } + _ => {} + } + + let rows = sector.open_string_range.as_usize_range( + self.lc_open_strings.len(), + &format!("physical LC sector {sector_index} open strings"), + )?; + let mut open_blocks = Vec::new(); + for (ordinal, row_index) in rows.enumerate() { + let line = self.lc_open_strings[row_index]; + let expected_sector = u32::try_from(sector_index) + .map_err(|_| invalid("physical LC sector index exceeds u32"))?; + let expected_ordinal = u32::try_from(ordinal) + .map_err(|_| invalid("LC open-string ordinal exceeds u32"))?; + if line.sector_id != expected_sector || line.ordinal != expected_ordinal { + return Err(invalid(format!( + "LC open-string row {row_index} has parent/ordinal ({}, {}), expected ({expected_sector}, {expected_ordinal})", + line.sector_id, line.ordinal + ))); + } + required_reference( + line.fundamental_source_slot, + self.external_legs.len(), + "LC fundamental source slot", + )?; + required_reference( + line.antifundamental_source_slot, + self.external_legs.len(), + "LC antifundamental source slot", + )?; + let adjoints = self.sequence( + line.adjoint_sequence_id, + &format!("LC open-string row {row_index} adjoints"), + )?; + let line_singlets = self.sequence( + line.singlet_sequence_id, + &format!("LC open-string row {row_index} singlets"), + )?; + validate_source_slot_sequence( + adjoints, + self.external_legs.len(), + &format!("LC open-string row {row_index} adjoints"), + )?; + validate_source_slot_sequence( + line_singlets, + self.external_legs.len(), + &format!("LC open-string row {row_index} singlets"), + )?; + let mut block = Vec::with_capacity(adjoints.len() + 2); + block.push(line.fundamental_source_slot); + block.extend_from_slice(adjoints); + block.push(line.antifundamental_source_slot); + open_blocks.push(block); + } + if closure_algorithm == "canonical-lc-closure-anchor-v4" { + if kind != ProcessLCSectorKind::SingleTrace + || sector.open_string_range.count != 0 + || !singlets.is_empty() + || trace != word + { + return Err(invalid(format!( + "physical LC sector {sector_index} v4 closure anchoring requires one complete cyclic single-trace source domain" + ))); + } + let mut sorted_word = word.to_vec(); + sorted_word.sort_unstable(); + let expected_domain = (0..self.external_legs.len()) + .map(|slot| { + u32::try_from(slot).map_err(|_| invalid("external source slot exceeds u32")) + }) + .collect::>>()?; + if sorted_word != expected_domain { + return Err(invalid(format!( + "physical LC sector {sector_index} v4 trace does not cover every external source exactly once" + ))); + } + let expected_anchor = word.iter().copied().min().ok_or_else(|| { + invalid(format!( + "physical LC sector {sector_index} v4 colour word is empty" + )) + })?; + if sector.closure_source_slot != expected_anchor { + return Err(invalid(format!( + "physical LC sector {sector_index} v4 closure source slot {} is not the minimum cyclic trace source {expected_anchor}", + sector.closure_source_slot + ))); + } + let fermionic_source_slots = self + .external_legs + .iter() + .enumerate() + .filter(|(_, leg)| leg.is_fermionic == 1) + .map(|(slot, _)| { + u32::try_from(slot).map_err(|_| invalid("external source slot exceeds u32")) + }) + .collect::>>()?; + let mut state_template_ids = BTreeSet::new(); + for (source_slot, leg) in self.external_legs.iter().copied().enumerate() { + let rows = leg.source_state_range.as_usize_range( + self.source_states.len(), + &format!( + "physical LC sector {sector_index} v4 source {source_slot} states" + ), + )?; + if rows.is_empty() { + return Err(invalid(format!( + "physical LC sector {sector_index} v4 source {source_slot} has no current-state contract" + ))); + } + state_template_ids.extend( + self.source_states[rows] + .iter() + .map(|state| state.current_state_template_id), + ); + } + if !fermionic_source_slots.is_empty() || state_template_ids.len() != 1 { + return Err(invalid(format!( + "physical LC sector {sector_index} v4 closure anchoring requires one nonfermionic current-state template across every source" + ))); + } + } + if closure_algorithm == "canonical-lc-closure-anchor-v3" { + if kind != ProcessLCSectorKind::OpenLines { + return Err(invalid(format!( + "physical LC sector {sector_index} uses v3 closure anchoring outside an open-line sector" + ))); + } + let mut physical_blocks = Vec::with_capacity(open_blocks.len()); + let mut used = vec![false; open_blocks.len()]; + let mut cursor = 0usize; + while cursor < word.len() { + let matches = open_blocks + .iter() + .enumerate() + .filter_map(|(index, block)| { + (!used[index] + && word.get(cursor..cursor.saturating_add(block.len())) + == Some(block.as_slice())) + .then_some(index) + }) + .collect::>(); + if matches.len() != 1 { + return Err(invalid(format!( + "physical LC sector {sector_index} v3 public word has {} open-line block matches at source position {cursor}", + matches.len() + ))); + } + let block_index = matches[0]; + used[block_index] = true; + cursor = cursor.saturating_add(open_blocks[block_index].len()); + physical_blocks.push(open_blocks[block_index].clone()); + } + if cursor != word.len() + || physical_blocks.len() != open_blocks.len() + || used.iter().any(|value| !value) + { + return Err(invalid(format!( + "physical LC sector {sector_index} v3 public word is not an exact complete-open-line block traversal" + ))); + } + let distinguished_source_slot = word.iter().copied().min().ok_or_else(|| { + invalid(format!( + "physical LC sector {sector_index} v3 colour word is empty" + )) + })?; + let distinguished_positions = physical_blocks + .iter() + .enumerate() + .filter_map(|(index, block)| { + block.contains(&distinguished_source_slot).then_some(index) + }) + .collect::>(); + if distinguished_positions.len() != 1 { + return Err(invalid(format!( + "physical LC sector {sector_index} v3 minimum coloured source slot belongs to {} open-line blocks", + distinguished_positions.len() + ))); + } + let first = distinguished_positions[0]; + let closure_blocks = physical_blocks[first..] + .iter() + .chain(physical_blocks[..first].iter()) + .cloned() + .collect::>(); + let closure_traversal = closure_blocks + .iter() + .flat_map(|block| block.iter().copied()) + .collect::>(); + let expected_anchor = closure_traversal.last().copied().ok_or_else(|| { + invalid(format!( + "physical LC sector {sector_index} v3 closure traversal is empty" + )) + })?; + if sector.closure_source_slot != expected_anchor { + return Err(invalid(format!( + "physical LC sector {sector_index} v3 closure source slot {} does not match independently reconstructed minimum-coloured-source block rotation endpoint {expected_anchor}", + sector.closure_source_slot + ))); + } + let process_source_label_order = self + .external_legs + .iter() + .map(|leg| leg.public_label) + .collect::>(); + let fermionic_source_slots = self + .external_legs + .iter() + .enumerate() + .filter(|(_, leg)| leg.is_fermionic == 1) + .map(|(slot, _)| { + u32::try_from(slot).map_err(|_| invalid("external source slot exceeds u32")) + }) + .collect::>>()?; + let sector_kind = match kind { + ProcessLCSectorKind::OpenLines => "open-lines", + ProcessLCSectorKind::SingleTrace => "single-trace", + ProcessLCSectorKind::Singlet => "singlet", + }; + let payload = json!({ + "algorithm": "canonical-lc-closure-anchor-v3", + "closure_block_source_slots": closure_blocks, + "closure_source_slot": sector.closure_source_slot, + "closure_traversal_source_slots": closure_traversal, + "component_order_policy": if open_blocks.len() > 1 { + "independent-open-string-block-permutation" + } else { + "exact-ordered-components" + }, + "distinguished_source_block": closure_blocks[0], + "distinguished_source_slot": distinguished_source_slot, + "external_source_count": self.external_legs.len(), + "fermionic_source_slots": fermionic_source_slots, + "open_string_count": open_blocks.len(), + "physical_block_source_slots": physical_blocks, + "policy": "minimum-coloured-source-slot-open-line-block-rotation", + "process_source_label_order": process_source_label_order, + "sector_id": sector.sector_id, + "sector_kind": sector_kind, + "selected_closure_block": closure_blocks.last().ok_or_else(|| { + invalid(format!( + "physical LC sector {sector_index} v3 has no selected closure block" + )) + })?, + "singlet_source_slots": singlets, + "word_source_slots": word, + }); + let canonical = serde_json::to_vec(&payload).map_err(|error| { + invalid(format!( + "physical LC sector {sector_index} v3 closure proof cannot be canonicalized: {error}" + )) + })?; + let expected_digest: [u8; 32] = Sha256::digest(canonical).into(); + if closure_proof_digest.as_bytes() != &expected_digest { + return Err(invalid(format!( + "physical LC sector {sector_index} v3 closure proof digest does not match the independently reconstructed minimum-coloured-source block rotation" + ))); + } + } + } + Ok(()) + } + + fn validate_public_flows(self, catalogs: &ProcessCatalogs<'_>) -> RusticolResult<()> { + if self.public_lc_flows.is_empty() { + return Err(invalid( + "recurrence process input requires at least one public LC flow", + )); + } + let mut public_ids = BTreeSet::new(); + for (flow_index, flow) in self.public_lc_flows.iter().copied().enumerate() { + let public_id = required_string( + &catalogs.strings, + flow.public_id_string_id, + "public LC flow identifier", + )?; + if !public_ids.insert(public_id) { + return Err(invalid(format!( + "public LC flow {flow_index} repeats identifier {public_id:?}" + ))); + } + let construction_sector = self + .physical_lc_sectors + .get(required_reference( + flow.construction_sector_id, + self.physical_lc_sectors.len(), + "public-flow construction sector", + )?) + .copied() + .ok_or_else(|| invalid("validated construction sector disappeared"))?; + let permutation = self.sequence( + flow.source_slot_permutation_sequence_id, + &format!("public LC flow {flow_index} source permutation"), + )?; + validate_permutation( + permutation, + self.external_legs.len(), + &format!("public LC flow {flow_index} source permutation"), + )?; + self.validate_source_contract_permutation( + permutation, + &format!("public LC flow {flow_index}"), + )?; + let construction_word = self.sequence( + construction_sector.word_sequence_id, + &format!("public LC flow {flow_index} construction word"), + )?; + let public_word = self.sequence( + flow.word_sequence_id, + &format!("public LC flow {flow_index} word"), + )?; + let reflected_trace = construction_sector.kind()? == ProcessLCSectorKind::SingleTrace + && construction_word.len() > 2 + && permutation + .iter() + .copied() + .eq(0..u32::try_from(permutation.len()) + .map_err(|_| invalid("public LC flow source count exceeds u32"))?) + && public_word.first() == construction_word.first() + && public_word[1..] + .iter() + .copied() + .eq(construction_word[1..].iter().rev().copied()); + if !reflected_trace { + validate_mapped_word( + construction_word, + permutation, + public_word, + &format!("public LC flow {flow_index}"), + )?; + } + } + Ok(()) + } + + fn validate_replay( + self, + _catalogs: &ProcessCatalogs<'_>, + strategy: RecurrenceStrategy, + ) -> RusticolResult<()> { + if !strategy.uses_topology_replay_targets() + && (!self.replay_partitions.is_empty() || !self.replay_targets.is_empty()) + { + return Err(invalid( + "non-replay recurrence input must not carry topology-replay rows", + )); + } + let ranges = self + .replay_partitions + .iter() + .map(|row| row.target_range) + .collect::>(); + validate_packed_ranges( + "replay partition targets", + &ranges, + self.replay_targets.len(), + )?; + + let mut covered_sectors = BTreeSet::new(); + for (partition_index, partition) in self.replay_partitions.iter().copied().enumerate() { + required_reference( + partition.representative_sector_id, + self.physical_lc_sectors.len(), + "replay representative sector", + )?; + required_reference( + partition.materialized_sector_id, + self.physical_lc_sectors.len(), + "replay materialized sector", + )?; + if partition.target_range.count == 0 { + return Err(invalid(format!( + "replay partition {partition_index} has no targets" + ))); + } + let representative_word_id = self.physical_lc_sectors + [partition.representative_sector_id as usize] + .word_sequence_id; + let representative_word = self.sequence( + representative_word_id, + &format!("replay partition {partition_index} representative word"), + )?; + let representative_anchor = self.physical_lc_sectors + [partition.representative_sector_id as usize] + .closure_source_slot; + let rows = partition.target_range.as_usize_range( + self.replay_targets.len(), + &format!("replay partition {partition_index} targets"), + )?; + let mut contains_representative = false; + for row_index in rows { + let target = self.replay_targets[row_index]; + let expected_partition = u32::try_from(partition_index) + .map_err(|_| invalid("replay partition index exceeds u32"))?; + if target.partition_id != expected_partition { + return Err(invalid(format!( + "replay target row {row_index} belongs to partition {}, expected {expected_partition}", + target.partition_id + ))); + } + required_reference( + target.sector_id, + self.physical_lc_sectors.len(), + "replay target sector", + )?; + if !covered_sectors.insert(target.sector_id) { + return Err(invalid(format!( + "physical LC sector {} belongs to multiple replay partitions", + target.sector_id + ))); + } + contains_representative |= target.sector_id == partition.representative_sector_id; + if !matches!(target.fermion_sign, -1 | 1) { + return Err(invalid(format!( + "replay target row {row_index} fermion sign must be -1 or 1" + ))); + } + let external_permutation = self.sequence( + target.external_permutation_sequence_id, + &format!("replay target row {row_index} external permutation"), + )?; + let source_permutation = self.sequence( + target.source_slot_permutation_sequence_id, + &format!("replay target row {row_index} source permutation"), + )?; + validate_permutation( + external_permutation, + self.external_legs.len(), + &format!("replay target row {row_index} external permutation"), + )?; + validate_permutation( + source_permutation, + self.external_legs.len(), + &format!("replay target row {row_index} source permutation"), + )?; + if external_permutation != source_permutation { + return Err(invalid(format!( + "replay target row {row_index} external and source permutations differ" + ))); + } + self.validate_source_contract_permutation( + source_permutation, + &format!("replay target row {row_index}"), + )?; + let target_word_id = + self.physical_lc_sectors[target.sector_id as usize].word_sequence_id; + let target_word = self.sequence( + target_word_id, + &format!("replay target row {row_index} target word"), + )?; + validate_mapped_word( + representative_word, + source_permutation, + target_word, + &format!("replay target row {row_index}"), + )?; + let target_anchor = + self.physical_lc_sectors[target.sector_id as usize].closure_source_slot; + let mapped_anchor = *source_permutation + .get(representative_anchor as usize) + .ok_or_else(|| { + invalid("replay representative closure anchor is out of range") + })?; + if mapped_anchor != target_anchor { + return Err(invalid(format!( + "replay target row {row_index} maps representative closure anchor {representative_anchor} to {mapped_anchor}, expected {target_anchor}" + ))); + } + } + if !contains_representative { + return Err(invalid(format!( + "replay partition {partition_index} does not contain its representative sector" + ))); + } + } + Ok(()) + } + + fn validate_source_contract_permutation( + self, + permutation: &[u32], + context: &str, + ) -> RusticolResult<()> { + for (representative_slot, target_slot) in permutation.iter().copied().enumerate() { + let target_slot = required_reference( + target_slot, + self.external_legs.len(), + "replay target source slot", + )?; + let representative = self.external_legs[representative_slot]; + let target = self.external_legs[target_slot]; + if representative.is_initial != target.is_initial { + return Err(invalid(format!( + "{context} maps source slot {representative_slot} across the initial/final crossing boundary" + ))); + } + let contracts = |leg: ProcessExternalLegRow, + label: &str| + -> RusticolResult> { + let rows = leg.source_state_range.as_usize_range( + self.source_states.len(), + &format!("{context} {label} source states"), + )?; + let mut result = BTreeMap::new(); + for row_index in rows { + let state = self.source_states[row_index]; + if result + .insert( + state.source_template_id, + (state.momentum_sign, state.crossing_phase_factor_id), + ) + .is_some() + { + return Err(invalid(format!( + "{context} {label} repeats source template {}", + state.source_template_id + ))); + } + } + Ok(result) + }; + let representative_contracts = contracts(representative, "representative")?; + let target_contracts = contracts(target, "target")?; + if representative_contracts != target_contracts { + return Err(invalid(format!( + "{context} maps source slot {representative_slot} onto slot {target_slot} with a different source crossing contract" + ))); + } + } + Ok(()) + } + + fn validate_generation_coverage( + self, + selected_flow_mode: bool, + selected_source_mode: bool, + ) -> RusticolResult<()> { + if selected_flow_mode == self.selected_public_flow_coverage.is_empty() { + return Err(invalid( + "selected-flow mode disagrees with generation coverage rows", + )); + } + if selected_source_mode == self.selected_source_coverage.is_empty() { + return Err(invalid( + "selected-source mode disagrees with generation coverage rows", + )); + } + let selected_flows = self + .selected_public_flow_coverage + .iter() + .map(|row| row.flow_id) + .collect::>(); + validate_strict_ids("selected public-flow coverage", &selected_flows)?; + validate_u32_references( + &selected_flows, + self.public_lc_flows.len(), + "selected public-flow coverage", + )?; + + let mut previous = None; + let mut covered_source_slots = BTreeSet::new(); + for (row_index, row) in self.selected_source_coverage.iter().copied().enumerate() { + let key = (row.source_slot, row.source_state_index); + if let Some(previous) = previous + && previous >= key + { + return Err(invalid(format!( + "selected source coverage is not in strict canonical order at row {row_index}" + ))); + } + previous = Some(key); + let leg = self + .external_legs + .get(required_reference( + row.source_slot, + self.external_legs.len(), + "selected source slot", + )?) + .ok_or_else(|| invalid("validated selected source slot disappeared"))?; + if u64::from(row.source_state_index) >= leg.source_state_range.count { + return Err(invalid(format!( + "selected source coverage row {row_index} references absent state {} for source slot {}", + row.source_state_index, row.source_slot + ))); + } + covered_source_slots.insert(row.source_slot); + } + if selected_source_mode && covered_source_slots.len() != self.external_legs.len() { + return Err(invalid( + "selected source coverage must retain at least one state for every external source slot", + )); + } + Ok(()) + } + + fn validate_couplings_and_parameters( + self, + catalogs: &ProcessCatalogs<'_>, + references: &[ProcessSemanticTemplateReference], + ) -> RusticolResult<()> { + let mut previous_name = None; + for (row_index, row) in self.coupling_limits.iter().copied().enumerate() { + let name = + required_string(&catalogs.strings, row.name_string_id, "coupling-limit name")?; + if let Some(previous) = previous_name + && previous >= name + { + return Err(invalid(format!( + "coupling limits are not in strict canonical name order at row {row_index}" + ))); + } + previous_name = Some(name); + if row.minimum > row.maximum { + return Err(invalid(format!( + "coupling limit {name:?} minimum {} exceeds maximum {}", + row.minimum, row.maximum + ))); + } + } + + let mut runtime_components = BTreeSet::new(); + for (row_index, row) in self.parameter_projection.iter().copied().enumerate() { + let runtime_name = required_string( + &catalogs.strings, + row.runtime_name_string_id, + "runtime parameter name", + )?; + if !runtime_components.insert((runtime_name, row.component)) { + return Err(invalid(format!( + "parameter projection row {row_index} repeats runtime parameter component ({runtime_name:?}, {})", + row.component + ))); + } + require_process_template( + references, + ProcessSemanticTemplateId { + kind: ProcessSemanticTemplateKind::Parameter, + template_id: row.parameter_template_id, + }, + &format!("parameter projection row {row_index}"), + )?; + } + Ok(()) + } + + fn validate_normalization(self, catalogs: &ProcessCatalogs<'_>) -> RusticolResult<()> { + if self.normalization.len() != 1 { + return Err(invalid(format!( + "recurrence process normalization must contain one row, found {}", + self.normalization.len() + ))); + } + let row = self.normalization[0]; + required_reference( + row.factor_id, + catalogs.factors.len(), + "process normalization factor", + )?; + required_string( + &catalogs.strings, + row.convention_string_id, + "process normalization convention", + )?; + required_digest( + &catalogs.digests, + row.semantic_digest_id, + "process normalization semantic digest", + )?; + Ok(()) + } + + fn validate_fermion_pairing( + self, + process_id: &str, + semantic_identity: &RecurrenceProcessSemanticIdentity, + ) -> RusticolResult> { + let topology_role = semantic_identity + .extension_digests() + .get("fermion-pairing-topology") + .copied(); + let semantic_role = semantic_identity + .extension_digests() + .get("fermion-pairing-semantic") + .copied(); + let Some(pairing) = self.fermion_pairing else { + if topology_role.is_some() || semantic_role.is_some() { + return Err(invalid( + "recurrence process claims fermion-pairing digests without pairing tables", + )); + } + return Ok(None); + }; + let topology_role = topology_role.ok_or_else(|| { + invalid("recurrence fermion-pairing tables lack their topology digest role") + })?; + let semantic_role = semantic_role.ok_or_else(|| { + invalid("recurrence fermion-pairing tables lack their semantic digest role") + })?; + let summary = pairing.validate_internal(process_id)?; + if summary.topology_digest != topology_role { + return Err(invalid( + "fermion-pairing topology digest disagrees with the process semantic identity", + )); + } + if summary.semantic_digest != semantic_role { + return Err(invalid( + "fermion-pairing semantic digest disagrees with the process semantic identity", + )); + } + if summary.source_count as usize != self.external_legs.len() { + return Err(invalid( + "fermion-pairing source count does not match recurrence external legs", + )); + } + + let mut expected_orientations = BTreeMap::::new(); + for line in self.lc_open_strings { + for (source_slot, orientation) in [ + (line.fundamental_source_slot, 0_u8), + (line.antifundamental_source_slot, 1_u8), + ] { + if let Some(previous) = expected_orientations.insert(source_slot, orientation) + && previous != orientation + { + return Err(invalid( + "LC sectors assign inconsistent orientations to one fermion endpoint", + )); + } + } + } + let endpoint_slots = pairing + .endpoints + .iter() + .map(|row| row.source_slot) + .collect::>(); + if endpoint_slots != expected_orientations.keys().copied().collect() { + return Err(invalid( + "fermion-pairing endpoints do not cover the LC open-line endpoints", + )); + } + for endpoint in pairing.endpoints { + let source = self + .external_legs + .get(required_reference( + endpoint.source_slot, + self.external_legs.len(), + "fermion-pairing endpoint source", + )?) + .ok_or_else(|| invalid("validated pairing endpoint source disappeared"))?; + if !decode_bool(source.is_fermionic, "external_legs.is_fermionic")? { + return Err(invalid( + "fermion-pairing endpoint references a non-fermionic source", + )); + } + if endpoint.public_label != source.public_label { + return Err(invalid( + "fermion-pairing endpoint public label disagrees with its source", + )); + } + if Some(&endpoint.color_orientation) != expected_orientations.get(&endpoint.source_slot) + { + return Err(invalid( + "fermion-pairing endpoint orientation disagrees with LC sectors", + )); + } + } + Ok(Some(summary)) + } + + fn sequence(self, sequence_id: u32, label: &str) -> RusticolResult<&'a [u32]> { + let range = self + .u32_sequence_ranges + .get(required_reference( + sequence_id, + self.u32_sequence_ranges.len(), + label, + )?) + .copied() + .ok_or_else(|| invalid(format!("validated {label} disappeared")))?; + let values = &self.u32_sequence_values + [range.as_usize_range(self.u32_sequence_values.len(), label)?]; + Ok(values) + } +} + +fn pairing_range_slice<'a, T>( + values: &'a [T], + range: CheckedTableRange, + label: &str, +) -> RusticolResult<&'a [T]> { + Ok(&values[range.as_usize_range(values.len(), label)?]) +} + +fn checked_factorial_u64(value: usize, label: &str) -> RusticolResult { + (2..=value).try_fold(1_u64, |product, factor| { + let factor = + u64::try_from(factor).map_err(|_| invalid(format!("{label} factor exceeds u64")))?; + product + .checked_mul(factor) + .ok_or_else(|| invalid(format!("{label} exceeds u64"))) + }) +} + +fn validate_pairing_ranges( + label: &str, + ranges: impl Iterator, + value_len: usize, +) -> RusticolResult<()> { + let ranges = ranges.collect::>(); + validate_packed_ranges(label, &ranges, value_len) +} + +fn validate_strict_u32_values(values: &[u32], bound: usize, label: &str) -> RusticolResult<()> { + let mut previous = None; + for (index, value) in values.iter().copied().enumerate() { + required_reference(value, bound, label)?; + if let Some(previous) = previous + && previous >= value + { + return Err(invalid(format!( + "{label} are not in strict canonical order at row {index}" + ))); + } + previous = Some(value); + } + Ok(()) +} + +fn validate_strict_i32_values(values: &[i32], label: &str) -> RusticolResult<()> { + if values.windows(2).any(|pair| pair[0] >= pair[1]) { + return Err(invalid(format!( + "{label} are not in strict canonical order" + ))); + } + Ok(()) +} + +fn exact_integer_equals( + input: FermionPairingInputView<'_>, + integer_id: u32, + expected: i32, +) -> RusticolResult { + let row = input + .exact_integers + .get(required_reference( + integer_id, + input.exact_integers.len(), + "fermion-pairing exact integer", + )?) + .copied() + .ok_or_else(|| invalid("validated fermion-pairing exact integer disappeared"))?; + let limbs = pairing_range_slice( + input.exact_integer_limbs, + row.limb_range, + "fermion-pairing exact-integer magnitude", + )?; + Ok(match expected { + -1 => row.sign == -1 && limbs == [1], + 0 => row.sign == 0 && limbs.is_empty(), + 1 => row.sign == 1 && limbs == [1], + _ => false, + }) +} + +fn decode_bool(value: u8, label: &str) -> RusticolResult { + match value { + 0 => Ok(false), + 1 => Ok(true), + _ => Err(invalid(format!( + "{label} must be zero or one, found {value}" + ))), + } +} + +fn required_reference(id: u32, target_len: usize, label: &str) -> RusticolResult { + let index = + usize::try_from(id).map_err(|_| invalid(format!("{label} id {id} exceeds usize")))?; + if index >= target_len { + return Err(invalid(format!( + "{label} references id {id}, target length is {target_len}" + ))); + } + Ok(index) +} + +fn required_string<'a>(strings: &'a [&str], id: u32, label: &str) -> RusticolResult<&'a str> { + strings + .get(required_reference(id, strings.len(), label)?) + .copied() + .ok_or_else(|| invalid(format!("validated {label} disappeared"))) +} + +fn require_string_value( + strings: &[&str], + id: u32, + expected: &str, + label: &str, +) -> RusticolResult<()> { + let actual = required_string(strings, id, label)?; + if actual != expected { + return Err(RusticolError::compatibility(format!( + "unsupported {label} {actual:?}; expected {expected:?}" + ))); + } + Ok(()) +} + +fn required_digest( + digests: &[SemanticDigest], + id: u32, + label: &str, +) -> RusticolResult { + digests + .get(required_reference(id, digests.len(), label)?) + .copied() + .ok_or_else(|| invalid(format!("validated {label} disappeared"))) +} + +fn take_required_role( + values: &mut BTreeMap, + role: ProcessDigestRole, +) -> RusticolResult { + values.remove(&role).ok_or_else(|| { + invalid(format!( + "recurrence process header is missing semantic digest role {:?}", + role.as_str() + )) + }) +} + +fn validate_canonical_ids( + label: &str, + values: impl IntoIterator, +) -> RusticolResult<()> { + for (index, value) in values.into_iter().enumerate() { + let expected = + u32::try_from(index).map_err(|_| invalid(format!("{label} row index exceeds u32")))?; + if value != expected { + return Err(invalid(format!( + "{label} row {index} contains id {value}, expected {expected}" + ))); + } + } + Ok(()) +} + +fn validate_strict_ids(label: &str, values: &[u32]) -> RusticolResult<()> { + let mut previous = None; + for (index, value) in values.iter().copied().enumerate() { + if let Some(previous) = previous + && previous >= value + { + return Err(invalid(format!( + "{label} is not in strict ascending order at row {index}" + ))); + } + previous = Some(value); + } + Ok(()) +} + +fn validate_source_slot_sequence( + values: &[u32], + external_leg_count: usize, + label: &str, +) -> RusticolResult<()> { + let mut seen = BTreeSet::new(); + for value in values.iter().copied() { + required_reference(value, external_leg_count, label)?; + if !seen.insert(value) { + return Err(invalid(format!( + "{label} repeats external source slot {value}" + ))); + } + } + Ok(()) +} + +fn validate_permutation(values: &[u32], expected_len: usize, label: &str) -> RusticolResult<()> { + if values.len() != expected_len { + return Err(invalid(format!( + "{label} has {} entries, expected {expected_len}", + values.len() + ))); + } + let mut seen = vec![false; expected_len]; + for value in values.iter().copied() { + let index = required_reference(value, expected_len, label)?; + if seen[index] { + return Err(invalid(format!("{label} repeats source slot {value}"))); + } + seen[index] = true; + } + Ok(()) +} + +fn validate_mapped_word( + source_word: &[u32], + permutation: &[u32], + target_word: &[u32], + label: &str, +) -> RusticolResult<()> { + if source_word.len() != target_word.len() { + return Err(invalid(format!( + "{label} maps a word of length {} onto length {}", + source_word.len(), + target_word.len() + ))); + } + for (position, (source_slot, expected_target)) in source_word + .iter() + .copied() + .zip(target_word.iter().copied()) + .enumerate() + { + let actual_target = permutation + .get(required_reference( + source_slot, + permutation.len(), + &format!("{label} source word"), + )?) + .copied() + .ok_or_else(|| invalid(format!("validated {label} permutation entry disappeared")))?; + if actual_target != expected_target { + return Err(invalid(format!( + "{label} maps word position {position} to {actual_target}, expected {expected_target}" + ))); + } + } + Ok(()) +} + +fn require_process_template( + references: &[ProcessSemanticTemplateReference], + typed_id: ProcessSemanticTemplateId, + label: &str, +) -> RusticolResult<()> { + if references + .binary_search_by_key(&typed_id, |reference| reference.typed_id) + .is_err() + { + return Err(invalid(format!( + "{label} references absent semantic template ({:?}, {})", + typed_id.kind.as_str(), + typed_id.template_id + ))); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use std::collections::{BTreeMap, BTreeSet}; + + use super::*; + + const TEST_PROCESS_ID: &str = "max-quark-pairs-scaling"; + + fn test_digest(seed: u8) -> [u8; 32] { + [seed; 32] + } + + fn test_range(start: usize, count: usize) -> CheckedTableRange { + CheckedTableRange::new( + u64::try_from(start).expect("test table start fits u64"), + u64::try_from(count).expect("test table count fits u64"), + ) + } + + fn v3_no_rotation_digest() -> [u8; 32] { + let payload = json!({ + "algorithm": "canonical-lc-closure-anchor-v3", + "closure_block_source_slots": [[1, 0], [2, 3]], + "closure_source_slot": 3, + "closure_traversal_source_slots": [1, 0, 2, 3], + "component_order_policy": "independent-open-string-block-permutation", + "distinguished_source_block": [1, 0], + "distinguished_source_slot": 0, + "external_source_count": 4, + "fermionic_source_slots": [0, 1, 2, 3], + "open_string_count": 2, + "physical_block_source_slots": [[1, 0], [2, 3]], + "policy": "minimum-coloured-source-slot-open-line-block-rotation", + "process_source_label_order": [1, 2, 3, 4], + "sector_id": 0, + "sector_kind": "open-lines", + "selected_closure_block": [2, 3], + "singlet_source_slots": [], + "word_source_slots": [1, 0, 2, 3], + }); + Sha256::digest(serde_json::to_vec(&payload).expect("test v3 payload must canonicalize")) + .into() + } + + fn validate_v3_color_fixture( + word: &[u32], + public_labels: [u32; 4], + open_strings: &[ProcessLCOpenStringRow], + closure_source_slot: u32, + closure_digest: [u8; 32], + ) -> RusticolResult<()> { + let external_legs = public_labels + .into_iter() + .enumerate() + .map(|(source_slot, public_label)| ProcessExternalLegRow { + source_slot: u32::try_from(source_slot).expect("test source slot fits u32"), + public_label, + physical_pdg: 1, + outgoing_pdg: 1, + is_initial: u8::from(source_slot < 2), + is_fermionic: 1, + source_state_range: test_range(0, 0), + momentum_mask_id: 0, + support_mask_id: 0, + }) + .collect::>(); + let physical_sectors = [ProcessPhysicalLCSectorRow { + sector_id: 0, + public_id_string_id: 0, + kind: ProcessLCSectorKind::OpenLines as u8, + closure_source_slot, + closure_proof_algorithm_string_id: 1, + closure_proof_digest_id: 0, + open_string_range: test_range(0, open_strings.len()), + trace_sequence_id: 0, + singlet_sequence_id: 0, + word_sequence_id: 1, + support_mask_id: 0, + }]; + let mut sequence_values = word.to_vec(); + sequence_values.push(2); + let sequence_ranges = [ + test_range(0, 0), + test_range(0, word.len()), + test_range(word.len(), 1), + ]; + let strings = vec!["flow:2,1,3,4", "canonical-lc-closure-anchor-v3"]; + let catalogs = ProcessCatalogs { + strings, + digests: vec![ + SemanticDigest::new(closure_digest).expect("test closure digest must be nonzero"), + ], + factors: Vec::new(), + }; + let input = RecurrenceProcessInputView { + input_abi: RECURRENCE_BUILDER_INPUT_ABI, + declared_input_digest: SemanticDigest::new(test_digest(31)) + .expect("test process digest is nonzero"), + fermion_pairing: None, + bitset_ranges: &[], + bitset_words: &[], + coupling_limits: &[], + digest_catalog: &[], + exact_factors: &[], + external_legs: &external_legs, + header: &[], + header_digests: &[], + lc_open_strings: open_strings, + normalization: &[], + parameter_projection: &[], + physical_lc_sectors: &physical_sectors, + public_lc_flows: &[], + replay_partitions: &[], + replay_targets: &[], + selected_public_flow_coverage: &[], + selected_source_coverage: &[], + semantic_template_references: &[], + source_states: &[], + string_ranges: &[], + string_bytes: &[], + u32_sequence_ranges: &sequence_ranges, + u32_sequence_values: &sequence_values, + }; + input.validate_color_sectors(&catalogs) + } + + fn validate_v4_color_fixture( + word: &[u32], + closure_source_slot: u32, + state_template_ids: [u32; 4], + fermionic_source_slot: Option, + ) -> RusticolResult<()> { + let external_legs = (0..4) + .map(|source_slot| ProcessExternalLegRow { + source_slot, + public_label: source_slot + 1, + physical_pdg: 21, + outgoing_pdg: 21, + is_initial: u8::from(source_slot < 2), + is_fermionic: u8::from(fermionic_source_slot == Some(source_slot as usize)), + source_state_range: test_range(source_slot as usize, 1), + momentum_mask_id: 0, + support_mask_id: 0, + }) + .collect::>(); + let source_states = state_template_ids + .into_iter() + .enumerate() + .map( + |(source_slot, current_state_template_id)| ProcessSourceStateRow { + source_slot: source_slot as u32, + state_index: 0, + public_helicity: 1, + chirality: 0, + spin_state: 1, + current_state_template_id, + source_template_id: source_slot as u32, + momentum_sign: 1, + crossing_phase_factor_id: 0, + }, + ) + .collect::>(); + let physical_sectors = [ProcessPhysicalLCSectorRow { + sector_id: 0, + public_id_string_id: 0, + kind: ProcessLCSectorKind::SingleTrace as u8, + closure_source_slot, + closure_proof_algorithm_string_id: 1, + closure_proof_digest_id: 0, + open_string_range: test_range(0, 0), + trace_sequence_id: 1, + singlet_sequence_id: 0, + word_sequence_id: 1, + support_mask_id: 0, + }]; + let sequence_ranges = [test_range(0, 0), test_range(0, word.len())]; + let catalogs = ProcessCatalogs { + strings: vec!["flow:1,2,3,4", "canonical-lc-closure-anchor-v4"], + digests: vec![ + SemanticDigest::new(test_digest(33)).expect("test closure digest must be nonzero"), + ], + factors: Vec::new(), + }; + let input = RecurrenceProcessInputView { + input_abi: RECURRENCE_BUILDER_INPUT_ABI, + declared_input_digest: SemanticDigest::new(test_digest(32)) + .expect("test process digest is nonzero"), + fermion_pairing: None, + bitset_ranges: &[], + bitset_words: &[], + coupling_limits: &[], + digest_catalog: &[], + exact_factors: &[], + external_legs: &external_legs, + header: &[], + header_digests: &[], + lc_open_strings: &[], + normalization: &[], + parameter_projection: &[], + physical_lc_sectors: &physical_sectors, + public_lc_flows: &[], + replay_partitions: &[], + replay_targets: &[], + selected_public_flow_coverage: &[], + selected_source_coverage: &[], + semantic_template_references: &[], + source_states: &source_states, + string_ranges: &[], + string_bytes: &[], + u32_sequence_ranges: &sequence_ranges, + u32_sequence_values: word, + }; + input.validate_color_sectors(&catalogs) + } + + #[test] + fn v3_closure_recomputes_no_rotation_mapping_and_rejects_stale_inputs() { + let open_strings = [ + ProcessLCOpenStringRow { + sector_id: 0, + ordinal: 0, + fundamental_source_slot: 1, + antifundamental_source_slot: 0, + adjoint_sequence_id: 0, + singlet_sequence_id: 0, + }, + ProcessLCOpenStringRow { + sector_id: 0, + ordinal: 1, + fundamental_source_slot: 2, + antifundamental_source_slot: 3, + adjoint_sequence_id: 0, + singlet_sequence_id: 0, + }, + ]; + let digest = v3_no_rotation_digest(); + validate_v3_color_fixture(&[1, 0, 2, 3], [1, 2, 3, 4], &open_strings, 3, digest) + .expect("valid no-rotation v3 fixture must pass"); + + for error in [ + validate_v3_color_fixture(&[2, 3, 1, 0], [1, 2, 3, 4], &open_strings, 3, digest) + .expect_err("stale public block order must fail"), + validate_v3_color_fixture(&[1, 0, 2, 3], [99, 2, 3, 4], &open_strings, 3, digest) + .expect_err("stale public-label/source map must fail"), + validate_v3_color_fixture(&[1, 0, 2, 3], [1, 2, 3, 4], &open_strings, 0, digest) + .expect_err("stale closure source slot must fail"), + ] { + assert!( + error.to_string().contains("v3 closure"), + "unexpected v3 tamper error: {error}" + ); + } + + let mut endpoint_tamper = open_strings; + endpoint_tamper[0].fundamental_source_slot = 2; + assert!( + validate_v3_color_fixture(&[1, 0, 2, 3], [1, 2, 3, 4], &endpoint_tamper, 3, digest,) + .expect_err("stale endpoint map must fail") + .to_string() + .contains("v3 public word") + ); + + let mut adjoint_tamper = open_strings; + adjoint_tamper[0].adjoint_sequence_id = 2; + assert!( + validate_v3_color_fixture(&[1, 0, 2, 3], [1, 2, 3, 4], &adjoint_tamper, 3, digest,) + .expect_err("stale adjoint map must fail") + .to_string() + .contains("v3 public word") + ); + } + + fn encode_test_strings( + values: impl IntoIterator, + ) -> (Vec, Vec, BTreeMap) { + let values = values.into_iter().collect::>(); + let mut bytes = Vec::new(); + let mut ranges = Vec::with_capacity(values.len()); + let mut ids = BTreeMap::new(); + for (index, value) in values.into_iter().enumerate() { + let start = bytes.len(); + bytes.extend_from_slice(value.as_bytes()); + ranges.push(test_range(start, value.len())); + ids.insert( + value, + u32::try_from(index).expect("test string count fits u32"), + ); + } + (bytes, ranges, ids) + } + + fn unrank_test_permutation(values: &[u32], mut rank: u64) -> Vec { + let mut available = values.to_vec(); + let mut permutation = Vec::with_capacity(values.len()); + for remaining in (1..=values.len()).rev() { + let block = checked_factorial_u64(remaining - 1, "test permutation block") + .expect("test permutation factorial fits u64"); + let index = usize::try_from(rank / block).expect("test permutation index fits usize"); + rank %= block; + permutation.push(available.remove(index)); + } + assert_eq!(rank, 0); + permutation + } + + fn test_permutation_parity(values: &[u32]) -> i32 { + let inversions = values + .iter() + .enumerate() + .map(|(index, value)| { + values[index + 1..] + .iter() + .filter(|other| value > *other) + .count() + }) + .sum::(); + if inversions % 2 == 0 { 1 } else { -1 } + } + + fn pairing_fixture(pair_counts_by_species: &[usize]) -> OwnedFermionPairingInput { + assert!(pair_counts_by_species.iter().all(|count| *count > 0)); + let species = (0..pair_counts_by_species.len()) + .map(|index| format!("species-{index:03}")) + .collect::>(); + let strings = [ + RECURRENCE_FERMION_PAIRING_COLUMNAR_ABI.to_owned(), + RECURRENCE_FERMION_PAIRING_PROOF_ALGORITHM.to_owned(), + TEST_PROCESS_ID.to_owned(), + "anti-state".to_owned(), + "basis".to_owned(), + "state".to_owned(), + ] + .into_iter() + .chain(species.iter().cloned()); + let (string_bytes, string_ranges, string_ids) = encode_test_strings(strings); + let string_id = |value: &str| { + *string_ids + .get(value) + .unwrap_or_else(|| panic!("missing test string {value:?}")) + }; + + let mut endpoints = Vec::new(); + let mut endpoint_state_template_ids = Vec::new(); + let mut endpoint_anti_state_template_ids = Vec::new(); + let mut endpoint_basis_ids = Vec::new(); + let mut endpoint_color_representations = Vec::new(); + let mut pairing_classes = Vec::new(); + let mut class_fundamental_slots = Vec::new(); + let mut class_antifundamental_slots = Vec::new(); + let mut class_reference_pairings = Vec::new(); + let mut class_fundamentals = Vec::new(); + let mut class_antifundamentals = Vec::new(); + let mut next_source_slot = 0_u32; + + for (class_id, (&pair_count, species)) in pair_counts_by_species + .iter() + .zip(species.iter()) + .enumerate() + { + let mut fundamental = Vec::with_capacity(pair_count); + let mut antifundamental = Vec::with_capacity(pair_count); + for _ in 0..pair_count { + fundamental.push(next_source_slot); + antifundamental.push(next_source_slot + 1); + next_source_slot += 2; + } + + for (fundamental_slot, antifundamental_slot) in fundamental + .iter() + .copied() + .zip(antifundamental.iter().copied()) + { + for (source_slot, particle_orientation, color_orientation, representation) in [ + (fundamental_slot, 0, 0, 3), + (antifundamental_slot, 1, 1, -3), + ] { + let state_start = endpoint_state_template_ids.len(); + endpoint_state_template_ids.push(string_id("state")); + let anti_state_start = endpoint_anti_state_template_ids.len(); + endpoint_anti_state_template_ids.push(string_id("anti-state")); + let basis_start = endpoint_basis_ids.len(); + endpoint_basis_ids.push(string_id("basis")); + let representation_start = endpoint_color_representations.len(); + endpoint_color_representations.push(representation); + endpoints.push(FermionPairingEndpointRow { + endpoint_id: u32::try_from(endpoints.len()) + .expect("test endpoint count fits u32"), + source_slot, + public_label: source_slot + 1, + species_class_id: u32::try_from(class_id) + .expect("test class count fits u32"), + species_string_id: string_id(species), + particle_orientation, + color_orientation, + state_template_range: test_range(state_start, 1), + anti_state_template_range: test_range(anti_state_start, 1), + basis_range: test_range(basis_start, 1), + color_representation_range: test_range(representation_start, 1), + contract_digest: test_digest(4), + }); + } + } + + let fundamental_start = class_fundamental_slots.len(); + class_fundamental_slots.extend_from_slice(&fundamental); + let antifundamental_start = class_antifundamental_slots.len(); + class_antifundamental_slots.extend_from_slice(&antifundamental); + let reference_start = class_reference_pairings.len(); + class_reference_pairings.extend( + fundamental + .iter() + .copied() + .zip(antifundamental.iter().copied()) + .map(|(fundamental_source_slot, antifundamental_source_slot)| { + FermionPairingEndpointPairRow { + fundamental_source_slot, + antifundamental_source_slot, + } + }), + ); + pairing_classes.push(FermionPairingClassRow { + class_id: u32::try_from(class_id).expect("test class count fits u32"), + species_class_id: u32::try_from(class_id).expect("test class count fits u32"), + species_string_id: string_id(species), + fundamental_slot_range: test_range(fundamental_start, pair_count), + antifundamental_slot_range: test_range(antifundamental_start, pair_count), + reference_pairing_range: test_range(reference_start, pair_count), + pairing_count: checked_factorial_u64(pair_count, "test pairing count") + .expect("test pairing count fits u64"), + proof_digest: test_digest(5), + }); + class_fundamentals.push(fundamental); + class_antifundamentals.push(antifundamental); + } + + let source_count = usize::try_from(next_source_slot).expect("test source count fits usize"); + let total_rule_count = pairing_classes + .iter() + .try_fold(1_u64, |count, class| count.checked_mul(class.pairing_count)) + .expect("test rule count fits u64"); + let total_rule_count = + usize::try_from(total_rule_count).expect("test rule count fits usize"); + let mut rules = Vec::with_capacity(total_rule_count); + let mut rule_class_pairing_indices = Vec::new(); + let mut rule_endpoint_pairings = Vec::new(); + let mut rule_source_slot_permutations = Vec::new(); + let mut rule_lineages = Vec::new(); + + for rule_id in 0..total_rule_count { + let class_index_start = rule_class_pairing_indices.len(); + let endpoint_pairing_start = rule_endpoint_pairings.len(); + let mut mixed_index = u64::try_from(rule_id).expect("test rule ID fits u64"); + let mut parity = 1; + for (class_id, class) in pairing_classes.iter().enumerate() { + let local_index = mixed_index % class.pairing_count; + mixed_index /= class.pairing_count; + rule_class_pairing_indices.push(FermionPairingClassPairingIndexRow { + class_id: u32::try_from(class_id).expect("test class count fits u32"), + pairing_index: local_index, + }); + let permutation = + unrank_test_permutation(&class_antifundamentals[class_id], local_index); + parity *= test_permutation_parity(&permutation); + rule_endpoint_pairings.extend( + class_fundamentals[class_id] + .iter() + .copied() + .zip(permutation) + .map(|(fundamental_source_slot, antifundamental_source_slot)| { + FermionPairingEndpointPairRow { + fundamental_source_slot, + antifundamental_source_slot, + } + }), + ); + } + assert_eq!(mixed_index, 0); + let source_permutation_start = rule_source_slot_permutations.len(); + rule_source_slot_permutations.extend(0..next_source_slot); + let lineage_start = rule_lineages.len(); + rule_lineages.extend(std::iter::repeat_n( + RECURRENCE_NO_FERMION_LINE, + source_count, + )); + rules.push(FermionPairingRuleRow { + rule_id: u32::try_from(rule_id).expect("test rule count fits u32"), + class_pairing_index_range: test_range(class_index_start, pairing_classes.len()), + endpoint_pairing_range: test_range( + endpoint_pairing_start, + pair_counts_by_species.iter().sum(), + ), + source_permutation_range: test_range(source_permutation_start, source_count), + lineage_range: test_range(lineage_start, source_count), + fermion_parity: parity, + real_numerator_integer_id: if parity < 0 { 0 } else { 2 }, + real_denominator_integer_id: 2, + imag_numerator_integer_id: 1, + imag_denominator_integer_id: 2, + multiplicity: 1, + proof_algorithm_string_id: string_id(RECURRENCE_FERMION_PAIRING_PROOF_ALGORITHM), + proof_digest: test_digest(6), + }); + } + + let exact_integers = vec![ + FermionPairingExactIntegerRow { + integer_id: 0, + sign: -1, + limb_range: test_range(0, 1), + }, + FermionPairingExactIntegerRow { + integer_id: 1, + sign: 0, + limb_range: test_range(1, 0), + }, + FermionPairingExactIntegerRow { + integer_id: 2, + sign: 1, + limb_range: test_range(1, 1), + }, + ]; + let exact_integer_limbs = vec![1, 1]; + let u32_len = |value: usize| u32::try_from(value).expect("test count fits u32"); + let u64_len = |value: usize| u64::try_from(value).expect("test count fits u64"); + let header = FermionPairingHeaderRow { + schema_version: RECURRENCE_FERMION_PAIRING_SCHEMA_VERSION, + abi_string_id: string_id(RECURRENCE_FERMION_PAIRING_COLUMNAR_ABI), + process_key_string_id: string_id(TEST_PROCESS_ID), + proof_algorithm_string_id: string_id(RECURRENCE_FERMION_PAIRING_PROOF_ALGORITHM), + source_count: u32_len(source_count), + endpoint_count: u32_len(endpoints.len()), + pairing_class_count: u32_len(pairing_classes.len()), + rule_count: u32_len(rules.len()), + endpoint_state_template_count: u64_len(endpoint_state_template_ids.len()), + endpoint_anti_state_template_count: u64_len(endpoint_anti_state_template_ids.len()), + endpoint_basis_count: u64_len(endpoint_basis_ids.len()), + endpoint_color_representation_count: u64_len(endpoint_color_representations.len()), + class_fundamental_slot_count: u64_len(class_fundamental_slots.len()), + class_antifundamental_slot_count: u64_len(class_antifundamental_slots.len()), + class_reference_pairing_count: u64_len(class_reference_pairings.len()), + rule_class_pairing_index_count: u64_len(rule_class_pairing_indices.len()), + rule_endpoint_pairing_count: u64_len(rule_endpoint_pairings.len()), + rule_source_permutation_count: u64_len(rule_source_slot_permutations.len()), + rule_lineage_count: u64_len(rule_lineages.len()), + exact_integer_count: u32_len(exact_integers.len()), + exact_integer_limb_count: u64_len(exact_integer_limbs.len()), + string_count: u32_len(string_ranges.len()), + string_byte_count: u64_len(string_bytes.len()), + no_fermion_line: RECURRENCE_NO_FERMION_LINE, + topology_digest: test_digest(7), + semantic_digest: test_digest(8), + }; + OwnedFermionPairingInput { + input_abi: RECURRENCE_FERMION_PAIRING_COLUMNAR_ABI.to_owned(), + declared_columnar_digest: SemanticDigest::new(test_digest(9)) + .expect("test digest is nonzero"), + header: vec![header], + endpoints, + endpoint_state_template_ids, + endpoint_anti_state_template_ids, + endpoint_basis_ids, + endpoint_color_representations, + pairing_classes, + class_fundamental_slots, + class_antifundamental_slots, + class_reference_pairings, + rules, + rule_class_pairing_indices, + rule_endpoint_pairings, + rule_source_slot_permutations, + rule_lineages, + exact_integers, + exact_integer_limbs, + string_ranges, + string_bytes, + } + } + + #[test] + fn v4_closure_validates_cyclic_anchor_and_homogeneous_source_contract() { + let word = [0, 2, 1, 3]; + validate_v4_color_fixture(&word, 0, [7; 4], None) + .expect("valid cyclic trace anchor must pass"); + + for error in [ + validate_v4_color_fixture(&word, 1, [7; 4], None) + .expect_err("noncanonical anchor must fail"), + validate_v4_color_fixture(&word, 0, [7, 7, 8, 7], None) + .expect_err("heterogeneous current states must fail"), + validate_v4_color_fixture(&word, 0, [7; 4], Some(2)) + .expect_err("fermionic trace source must fail"), + ] { + assert!( + error.to_string().contains("v4"), + "unexpected validation error: {error}" + ); + } + } + + fn generation_coverage_view<'a>( + header: &'a [ProcessHeaderRow], + external_legs: &'a [ProcessExternalLegRow], + public_lc_flows: &'a [ProcessPublicLCFlowRow], + selected_public_flow_coverage: &'a [ProcessSelectedPublicFlowRow], + selected_source_coverage: &'a [ProcessSelectedSourceStateRow], + ) -> RecurrenceProcessInputView<'a> { + RecurrenceProcessInputView { + input_abi: RECURRENCE_BUILDER_INPUT_ABI, + declared_input_digest: SemanticDigest::new(test_digest(31)) + .expect("test process digest is nonzero"), + fermion_pairing: None, + bitset_ranges: &[], + bitset_words: &[], + coupling_limits: &[], + digest_catalog: &[], + exact_factors: &[], + external_legs, + header, + header_digests: &[], + lc_open_strings: &[], + normalization: &[], + parameter_projection: &[], + physical_lc_sectors: &[], + public_lc_flows, + replay_partitions: &[], + replay_targets: &[], + selected_public_flow_coverage, + selected_source_coverage, + semantic_template_references: &[], + source_states: &[], + string_ranges: &[], + string_bytes: &[], + u32_sequence_ranges: &[], + u32_sequence_values: &[], + } + } + + fn generation_coverage_header( + strategy: RecurrenceStrategy, + selected_flow_mode: bool, + selected_source_mode: bool, + ) -> ProcessHeaderRow { + ProcessHeaderRow { + schema_version: RECURRENCE_PROCESS_INPUT_SCHEMA_VERSION, + abi_string_id: 0, + process_id_string_id: 0, + layout: u8::try_from(strategy.as_u32()).expect("strategy fits u8"), + selected_flow_mode: u8::from(selected_flow_mode), + selected_source_mode: u8::from(selected_source_mode), + external_leg_count: 2, + physical_sector_count: 0, + public_flow_count: 2, + replay_partition_count: 0, + coupling_limit_count: 0, + parameter_projection_count: 0, + process_support_mask_id: 0, + } + } + + fn generation_coverage_external_legs() -> [ProcessExternalLegRow; 2] { + [ + ProcessExternalLegRow { + source_slot: 0, + public_label: 1, + physical_pdg: 1, + outgoing_pdg: 1, + is_initial: 1, + is_fermionic: 1, + source_state_range: test_range(0, 2), + momentum_mask_id: 0, + support_mask_id: 0, + }, + ProcessExternalLegRow { + source_slot: 1, + public_label: 2, + physical_pdg: -1, + outgoing_pdg: -1, + is_initial: 1, + is_fermionic: 1, + source_state_range: test_range(2, 2), + momentum_mask_id: 0, + support_mask_id: 0, + }, + ] + } + + fn generation_coverage_public_flows() -> [ProcessPublicLCFlowRow; 2] { + [ + ProcessPublicLCFlowRow { + flow_id: 0, + public_id_string_id: 0, + construction_sector_id: 0, + word_sequence_id: 0, + source_slot_permutation_sequence_id: 0, + reduction_weight_factor_id: 0, + }, + ProcessPublicLCFlowRow { + flow_id: 1, + public_id_string_id: 0, + construction_sector_id: 0, + word_sequence_id: 0, + source_slot_permutation_sequence_id: 0, + reduction_weight_factor_id: 0, + }, + ] + } + + #[test] + fn all_flow_union_accepts_generation_specialized_coverage() { + let header = [generation_coverage_header( + RecurrenceStrategy::AllFlowUnion, + true, + true, + )]; + let external_legs = generation_coverage_external_legs(); + let public_flows = generation_coverage_public_flows(); + let selected_flows = [ProcessSelectedPublicFlowRow { flow_id: 1 }]; + let selected_sources = [ + ProcessSelectedSourceStateRow { + source_slot: 0, + source_state_index: 1, + }, + ProcessSelectedSourceStateRow { + source_slot: 1, + source_state_index: 0, + }, + ]; + let input = generation_coverage_view( + &header, + &external_legs, + &public_flows, + &selected_flows, + &selected_sources, + ); + + input + .validate_generation_coverage(true, true) + .expect("all-flow-union accepts valid generation specialization"); + } + + #[test] + fn all_flow_union_accepts_omitted_generation_axes() { + let header = [generation_coverage_header( + RecurrenceStrategy::AllFlowUnion, + false, + false, + )]; + let external_legs = generation_coverage_external_legs(); + let public_flows = generation_coverage_public_flows(); + let input = generation_coverage_view(&header, &external_legs, &public_flows, &[], &[]); + + input + .validate_generation_coverage(false, false) + .expect("omitted union axes retain complete coverage"); + } + + #[test] + fn selected_source_coverage_rejects_missing_external_slot() { + let header = [generation_coverage_header( + RecurrenceStrategy::AllFlowUnion, + false, + true, + )]; + let external_legs = generation_coverage_external_legs(); + let public_flows = generation_coverage_public_flows(); + let selected_sources = [ProcessSelectedSourceStateRow { + source_slot: 0, + source_state_index: 1, + }]; + let input = generation_coverage_view( + &header, + &external_legs, + &public_flows, + &[], + &selected_sources, + ); + + let error = input + .validate_generation_coverage(false, true) + .expect_err("partial source coverage must fail closed"); + assert!(error.to_string().contains("every external source slot")); + } + + #[test] + fn selected_union_coverage_rejects_out_of_range_rows() { + let header = [generation_coverage_header( + RecurrenceStrategy::AllFlowUnion, + true, + true, + )]; + let external_legs = generation_coverage_external_legs(); + let public_flows = generation_coverage_public_flows(); + let selected_flows = [ProcessSelectedPublicFlowRow { flow_id: 2 }]; + let selected_sources = [ + ProcessSelectedSourceStateRow { + source_slot: 0, + source_state_index: 0, + }, + ProcessSelectedSourceStateRow { + source_slot: 1, + source_state_index: 2, + }, + ]; + let input = generation_coverage_view( + &header, + &external_legs, + &public_flows, + &selected_flows, + &selected_sources, + ); + + assert!(input.validate_generation_coverage(true, true).is_err()); + } + + #[test] + fn semantic_template_kinds_are_closed_and_round_trip() { + for kind in [ + ProcessSemanticTemplateKind::Parameter, + ProcessSemanticTemplateKind::CurrentState, + ProcessSemanticTemplateKind::Source, + ProcessSemanticTemplateKind::QuantumFlow, + ProcessSemanticTemplateKind::Transition, + ProcessSemanticTemplateKind::Propagator, + ProcessSemanticTemplateKind::Closure, + ProcessSemanticTemplateKind::ColorContraction, + ProcessSemanticTemplateKind::SymmetryProof, + ] { + assert_eq!( + ProcessSemanticTemplateKind::try_from(kind.as_str()), + Ok(kind) + ); + } + assert!(ProcessSemanticTemplateKind::try_from("model-specific").is_err()); + } + + #[test] + fn process_digest_roles_decode_required_and_extension_roles() { + assert_eq!( + ProcessDigestRole::decode("process").unwrap(), + ProcessDigestRole::Process + ); + assert_eq!( + ProcessDigestRole::decode("future-proof").unwrap(), + ProcessDigestRole::Extension("future-proof".to_owned()) + ); + assert!(ProcessDigestRole::decode("").is_err()); + } + + #[test] + fn permutation_validation_rejects_duplicates_and_missing_entries() { + assert!(validate_permutation(&[2, 0, 1], 3, "test permutation").is_ok()); + assert!(validate_permutation(&[0, 0, 2], 3, "test permutation").is_err()); + assert!(validate_permutation(&[0, 1], 3, "test permutation").is_err()); + } + + #[test] + fn user_max_quark_pairs_above_eight_has_no_hidden_endpoint_cap() { + let input = pairing_fixture(&[1; 9]); + let summary = input + .as_view() + .validate_internal(TEST_PROCESS_ID) + .expect("nine independently flavored quark pairs must validate"); + assert_eq!(summary.endpoint_count, 18); + assert_eq!(summary.pairing_class_count, 9); + assert_eq!(summary.rule_count, 1); + } + + #[test] + fn user_max_quark_pairs_allows_more_than_legacy_pairing_and_rule_caps() { + let input = pairing_fixture(&[7]); + assert_eq!(input.pairing_classes[0].pairing_count, 5_040); + let summary = input + .as_view() + .validate_internal(TEST_PROCESS_ID) + .expect("seven same-species quark pairs and all 5,040 rules must validate"); + assert_eq!(summary.endpoint_count, 14); + assert_eq!(summary.pairing_class_count, 1); + assert_eq!(summary.rule_count, 5_040); + } + + #[test] + fn pairing_growth_stops_only_at_checked_integer_representation() { + assert_eq!( + checked_factorial_u64(20, "test pairing count").unwrap(), + 2_432_902_008_176_640_000 + ); + let error = checked_factorial_u64(21, "test pairing count").unwrap_err(); + assert!(error.to_string().contains("exceeds u64")); + } +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/recurrence/program.rs b/pyamplicol/rust/crates/rusticol-core/src/recurrence/program.rs new file mode 100644 index 0000000000..74a4c9b8f6 --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/recurrence/program.rs @@ -0,0 +1,3781 @@ +// SPDX-License-Identifier: 0BSD + +//! Compact, immutable recurrence schedule produced by the recurrence builder. +//! +//! The owned vectors are build-time storage. Evaluation consumes only packed +//! slices and ranges, so traversing a validated program requires no maps or +//! heap allocation. + +use std::collections::BTreeSet; + +use sha2::{Digest, Sha256}; + +use super::{ + CheckedTableRange, ContributionKey, CurrentCoreKey, DynamicLCColorState, ExactComplexRational, + ExactRational, RecurrenceNodeKind, RecurrenceStrategy, SemanticDigest, SourceStateAssignment, +}; +use crate::{RusticolError, RusticolResult}; + +pub const CLOSURE_TARGET_DOMAIN_PROOF_KIND: &str = "complete-recurrence-closure-target-domain-v1"; + +fn invalid(message: impl Into) -> RusticolError { + RusticolError::invalid_argument(message) +} + +fn checked_table_len(label: &str, length: usize) -> RusticolResult { + u64::try_from(length).map_err(|_| invalid(format!("{label} length {length} exceeds u64"))) +} + +fn validate_permutation(label: &str, values: &[u32], expected_len: usize) -> RusticolResult<()> { + if values.len() != expected_len { + return Err(invalid(format!( + "{label} has length {}, expected {expected_len}", + values.len() + ))); + } + let mut seen = vec![false; expected_len]; + for (index, value) in values.iter().copied().enumerate() { + let value = usize::try_from(value) + .map_err(|_| invalid(format!("{label} row {index} exceeds usize")))?; + let Some(slot) = seen.get_mut(value) else { + return Err(invalid(format!( + "{label} row {index} references out-of-range slot {value}" + ))); + }; + if std::mem::replace(slot, true) { + return Err(invalid(format!( + "{label} repeats slot {value} at row {index}" + ))); + } + } + Ok(()) +} + +fn hash_exact_factor(hash: &mut Sha256, factor: ExactComplexRational) { + for rational in [factor.real(), factor.imag()] { + hash.update(rational.numerator().to_le_bytes()); + hash.update(rational.denominator().to_le_bytes()); + } +} + +fn hash_optional_u32(hash: &mut Sha256, value: Option) { + hash.update(value.unwrap_or(u32::MAX).to_le_bytes()); +} + +fn hash_optional_digest(hash: &mut Sha256, value: Option) { + hash.update(value.map_or([0; 32], |digest| *digest.as_bytes())); +} + +fn semantic_digest_from_fields( + domain: &[u8], + values: impl IntoIterator, +) -> RusticolResult { + let mut hash = Sha256::new(); + hash.update(domain); + for value in values { + hash.update(value.to_le_bytes()); + } + SemanticDigest::new(hash.finalize().into()) +} + +/// One unaggregated exact closure obligation. +/// +/// These rows are cold proof metadata. Runtime closure rows are formed only +/// after the complete contribution multiset has been validated and grouped. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ClosureProofContributionV2 { + id: u32, + target_sector_id: u32, + target_destination_id: Option, + target_helicity_id: Option, + closure_template_id: u32, + closure_template_semantic_digest: SemanticDigest, + quantum_flow_template_id: Option, + construction_parent_builder_ids: Box<[u32]>, + construction_parent_runtime_ids: Box<[Option]>, + construction_parent_semantic_digests: Box<[SemanticDigest]>, + construction_parent_color_digests: Box<[SemanticDigest]>, + construction_parent_permutation: Box<[u32]>, + reconstruction_parent_permutation: Box<[u32]>, + evaluator_parent_permutation: Box<[u32]>, + color_witness_term_id: u32, + color_witness_proof_digest: SemanticDigest, + three_line_certificate_id: Option, + pairing_certificate_ids: Box<[u32]>, + reflection_certificate_id: Option, + exact_factor: ExactComplexRational, + multiplicity: u32, +} + +/// Compact commitment to the complete accepted closure-candidate multiset. +/// +/// The builder computes this before aggregation. Loading recomputes it from the +/// persisted proof contributions and rejects omitted, duplicated, or altered +/// candidates without retaining a second expanded closure table. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct ClosureCandidateDomainCertificateV1 { + accepted_candidate_count: u64, + accepted_candidate_digest: SemanticDigest, +} + +/// Exact proof that complete physical colour sectors have no retained closure. +/// +/// The construction pass produces this certificate only after enumerating the +/// complete physical-sector x resolved-helicity closure domain. Its digest +/// binds every target row, including rows with an empty candidate list, plus +/// every original candidate identity and exact aggregate. The compact sector +/// list is therefore a consequence of an exhaustive native proof, not an +/// inference from missing runtime destinations. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ClosureTargetDomainCertificateV1 { + physical_sector_count: u32, + resolved_helicity_count: u32, + certified_structural_zero_physical_sector_ids: Box<[u32]>, + semantic_digest: SemanticDigest, +} + +impl ClosureTargetDomainCertificateV1 { + pub fn new( + physical_sector_count: u32, + resolved_helicity_count: u32, + certified_structural_zero_physical_sector_ids: Vec, + semantic_digest: SemanticDigest, + ) -> RusticolResult { + if physical_sector_count == 0 { + return Err(invalid( + "closure target-domain certificate requires physical sectors", + )); + } + if resolved_helicity_count == 0 { + return Err(invalid( + "closure target-domain certificate requires resolved helicities", + )); + } + if certified_structural_zero_physical_sector_ids + .windows(2) + .any(|pair| pair[0] >= pair[1]) + || certified_structural_zero_physical_sector_ids + .iter() + .any(|sector_id| *sector_id >= physical_sector_count) + { + return Err(invalid( + "closure target-domain structural-zero sectors are not canonical", + )); + } + Ok(Self { + physical_sector_count, + resolved_helicity_count, + certified_structural_zero_physical_sector_ids: + certified_structural_zero_physical_sector_ids.into_boxed_slice(), + semantic_digest, + }) + } + + pub const fn physical_sector_count(&self) -> u32 { + self.physical_sector_count + } + + pub const fn resolved_helicity_count(&self) -> u32 { + self.resolved_helicity_count + } + + pub fn certified_structural_zero_physical_sector_ids(&self) -> &[u32] { + &self.certified_structural_zero_physical_sector_ids + } + + pub const fn semantic_digest(&self) -> SemanticDigest { + self.semantic_digest + } +} + +impl ClosureCandidateDomainCertificateV1 { + pub const fn new( + accepted_candidate_count: u64, + accepted_candidate_digest: SemanticDigest, + ) -> Self { + Self { + accepted_candidate_count, + accepted_candidate_digest, + } + } + + pub(crate) fn from_identity_digests( + mut identity_digests: Vec, + ) -> RusticolResult { + identity_digests.sort_unstable(); + let accepted_candidate_count = u64::try_from(identity_digests.len()) + .map_err(|_| invalid("closure candidate-domain count exceeds u64"))?; + let mut hash = Sha256::new(); + hash.update(b"pyamplicol-recurrence-closure-candidate-domain-v1\0"); + hash.update(accepted_candidate_count.to_le_bytes()); + for digest in identity_digests { + hash.update(digest.as_bytes()); + } + Ok(Self::new( + accepted_candidate_count, + SemanticDigest::new(hash.finalize().into())?, + )) + } + + pub const fn accepted_candidate_count(self) -> u64 { + self.accepted_candidate_count + } + + pub const fn accepted_candidate_digest(self) -> SemanticDigest { + self.accepted_candidate_digest + } +} + +#[allow(clippy::too_many_arguments)] +pub(crate) fn closure_candidate_identity_digest_v1( + selector_domain_digest: SemanticDigest, + target_sector_id: u32, + closure_template_id: u32, + closure_template_semantic_digest: SemanticDigest, + quantum_flow_template_id: Option, + construction_parent_builder_ids: &[u32], + construction_parent_semantic_digests: &[SemanticDigest], + construction_parent_color_digests: &[SemanticDigest], + construction_parent_permutation: &[u32], + reconstruction_parent_permutation: &[u32], + evaluator_parent_permutation: &[u32], + color_witness_term_id: u32, + color_witness_proof_digest: SemanticDigest, + three_line_proof_digest: Option, + pairing_certificate_ids: &[u32], + reflection_proof_digest: Option, + exact_factor: ExactComplexRational, + multiplicity: u32, +) -> RusticolResult { + let parent_count = construction_parent_builder_ids.len(); + if construction_parent_semantic_digests.len() != parent_count + || construction_parent_color_digests.len() != parent_count + { + return Err(invalid( + "closure candidate parent IDs and semantic/color digests have inconsistent lengths", + )); + } + let mut hash = Sha256::new(); + hash.update(b"pyamplicol-recurrence-closure-candidate-v1\0"); + hash.update(selector_domain_digest.as_bytes()); + hash.update(target_sector_id.to_le_bytes()); + hash.update(closure_template_id.to_le_bytes()); + hash.update(closure_template_semantic_digest.as_bytes()); + hash_optional_u32(&mut hash, quantum_flow_template_id); + for values in [ + construction_parent_builder_ids, + construction_parent_permutation, + reconstruction_parent_permutation, + evaluator_parent_permutation, + pairing_certificate_ids, + ] { + hash.update( + u64::try_from(values.len()) + .map_err(|_| invalid("closure candidate sequence length exceeds u64"))? + .to_le_bytes(), + ); + for value in values { + hash.update(value.to_le_bytes()); + } + } + for digests in [ + construction_parent_semantic_digests, + construction_parent_color_digests, + ] { + hash.update( + u64::try_from(digests.len()) + .map_err(|_| invalid("closure candidate digest sequence length exceeds u64"))? + .to_le_bytes(), + ); + for digest in digests { + hash.update(digest.as_bytes()); + } + } + hash.update(color_witness_term_id.to_le_bytes()); + hash.update(color_witness_proof_digest.as_bytes()); + hash_optional_digest(&mut hash, three_line_proof_digest); + hash_optional_digest(&mut hash, reflection_proof_digest); + hash_exact_factor(&mut hash, exact_factor); + hash.update(multiplicity.to_le_bytes()); + SemanticDigest::new(hash.finalize().into()) +} + +impl ClosureProofContributionV2 { + #[allow(clippy::too_many_arguments)] + pub fn new( + id: u32, + target_sector_id: u32, + target_destination_id: Option, + target_helicity_id: Option, + closure_template_id: u32, + closure_template_semantic_digest: SemanticDigest, + quantum_flow_template_id: Option, + construction_parent_builder_ids: Vec, + construction_parent_runtime_ids: Vec>, + construction_parent_semantic_digests: Vec, + construction_parent_color_digests: Vec, + construction_parent_permutation: Vec, + reconstruction_parent_permutation: Vec, + evaluator_parent_permutation: Vec, + color_witness_term_id: u32, + color_witness_proof_digest: SemanticDigest, + three_line_certificate_id: Option, + mut pairing_certificate_ids: Vec, + reflection_certificate_id: Option, + exact_factor: ExactComplexRational, + multiplicity: u32, + ) -> RusticolResult { + let parent_count = construction_parent_builder_ids.len(); + if parent_count == 0 { + return Err(invalid( + "closure proof contribution requires at least one parent", + )); + } + u32::try_from(parent_count) + .map_err(|_| invalid("closure proof parent count exceeds u32"))?; + if construction_parent_runtime_ids.len() != parent_count + || construction_parent_semantic_digests.len() != parent_count + || construction_parent_color_digests.len() != parent_count + { + return Err(invalid( + "closure proof builder/runtime parent IDs and semantic/color digests have inconsistent lengths", + )); + } + validate_permutation( + "closure construction-parent permutation", + &construction_parent_permutation, + parent_count, + )?; + validate_permutation( + "closure reconstruction-parent permutation", + &reconstruction_parent_permutation, + parent_count, + )?; + validate_permutation( + "closure evaluator-parent permutation", + &evaluator_parent_permutation, + parent_count, + )?; + if exact_factor.is_zero() { + return Err(invalid( + "closure proof contribution factor must not be zero", + )); + } + if multiplicity == 0 { + return Err(invalid( + "closure proof contribution multiplicity must be positive", + )); + } + pairing_certificate_ids.sort_unstable(); + pairing_certificate_ids.dedup(); + Ok(Self { + id, + target_sector_id, + target_destination_id, + target_helicity_id, + closure_template_id, + closure_template_semantic_digest, + quantum_flow_template_id, + construction_parent_builder_ids: construction_parent_builder_ids.into_boxed_slice(), + construction_parent_runtime_ids: construction_parent_runtime_ids.into_boxed_slice(), + construction_parent_semantic_digests: construction_parent_semantic_digests + .into_boxed_slice(), + construction_parent_color_digests: construction_parent_color_digests.into_boxed_slice(), + construction_parent_permutation: construction_parent_permutation.into_boxed_slice(), + reconstruction_parent_permutation: reconstruction_parent_permutation.into_boxed_slice(), + evaluator_parent_permutation: evaluator_parent_permutation.into_boxed_slice(), + color_witness_term_id, + color_witness_proof_digest, + three_line_certificate_id, + pairing_certificate_ids: pairing_certificate_ids.into_boxed_slice(), + reflection_certificate_id, + exact_factor, + multiplicity, + }) + } + + pub const fn id(&self) -> u32 { + self.id + } + + pub const fn target_sector_id(&self) -> u32 { + self.target_sector_id + } + + pub const fn target_destination_id(&self) -> Option { + self.target_destination_id + } + + pub const fn target_helicity_id(&self) -> Option { + self.target_helicity_id + } + + pub const fn closure_template_id(&self) -> u32 { + self.closure_template_id + } + + pub const fn closure_template_semantic_digest(&self) -> SemanticDigest { + self.closure_template_semantic_digest + } + + pub const fn quantum_flow_template_id(&self) -> Option { + self.quantum_flow_template_id + } + + pub fn construction_parent_builder_ids(&self) -> &[u32] { + &self.construction_parent_builder_ids + } + + pub fn construction_parent_runtime_ids(&self) -> &[Option] { + &self.construction_parent_runtime_ids + } + + pub fn construction_parent_semantic_digests(&self) -> &[SemanticDigest] { + &self.construction_parent_semantic_digests + } + + pub fn construction_parent_color_digests(&self) -> &[SemanticDigest] { + &self.construction_parent_color_digests + } + + pub fn construction_parent_permutation(&self) -> &[u32] { + &self.construction_parent_permutation + } + + pub fn reconstruction_parent_permutation(&self) -> &[u32] { + &self.reconstruction_parent_permutation + } + + pub fn evaluator_parent_permutation(&self) -> &[u32] { + &self.evaluator_parent_permutation + } + + pub fn evaluator_parent_builder_ids(&self) -> Vec { + self.evaluator_parent_permutation + .iter() + .map(|slot| self.construction_parent_builder_ids[*slot as usize]) + .collect() + } + + pub fn evaluator_parent_runtime_ids(&self) -> Option> { + self.evaluator_parent_permutation + .iter() + .map(|slot| self.construction_parent_runtime_ids[*slot as usize]) + .collect() + } + + pub const fn color_witness_term_id(&self) -> u32 { + self.color_witness_term_id + } + + pub const fn color_witness_proof_digest(&self) -> SemanticDigest { + self.color_witness_proof_digest + } + + pub const fn three_line_certificate_id(&self) -> Option { + self.three_line_certificate_id + } + + pub fn pairing_certificate_ids(&self) -> &[u32] { + &self.pairing_certificate_ids + } + + pub const fn reflection_certificate_id(&self) -> Option { + self.reflection_certificate_id + } + + pub const fn exact_factor(&self) -> ExactComplexRational { + self.exact_factor + } + + pub const fn multiplicity(&self) -> u32 { + self.multiplicity + } +} + +/// One exact group of proof contributions represented by at most one runtime +/// closure row. Exact cancellation is retained with both runtime IDs absent. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ClosureExecutionProofGroupV2 { + id: u32, + emitted_runtime_closure_term_id: Option, + emitted_direct_closure_row_id: Option, + contribution_range: CheckedTableRange, + exact_summed_factor: ExactComplexRational, + component_factor_digest: SemanticDigest, + candidate_selector_domain_digest: SemanticDigest, + selector_domain_digest: SemanticDigest, +} + +impl ClosureExecutionProofGroupV2 { + #[allow(clippy::too_many_arguments)] + pub fn new( + id: u32, + emitted_runtime_closure_term_id: Option, + emitted_direct_closure_row_id: Option, + contribution_range: CheckedTableRange, + exact_summed_factor: ExactComplexRational, + component_factor_digest: SemanticDigest, + selector_domain_digest: SemanticDigest, + ) -> RusticolResult { + Self::new_with_candidate_selector_domain( + id, + emitted_runtime_closure_term_id, + emitted_direct_closure_row_id, + contribution_range, + exact_summed_factor, + component_factor_digest, + selector_domain_digest, + selector_domain_digest, + ) + } + + #[allow(clippy::too_many_arguments)] + pub fn new_with_candidate_selector_domain( + id: u32, + emitted_runtime_closure_term_id: Option, + emitted_direct_closure_row_id: Option, + contribution_range: CheckedTableRange, + exact_summed_factor: ExactComplexRational, + component_factor_digest: SemanticDigest, + candidate_selector_domain_digest: SemanticDigest, + selector_domain_digest: SemanticDigest, + ) -> RusticolResult { + contribution_range.end("closure proof-group contribution")?; + if exact_summed_factor.is_zero() { + if emitted_runtime_closure_term_id.is_some() || emitted_direct_closure_row_id.is_some() + { + return Err(invalid( + "exact-zero closure proof group must not reference a runtime closure", + )); + } + } else if emitted_runtime_closure_term_id.is_none() { + return Err(invalid( + "nonzero closure proof group requires a runtime closure term", + )); + } + Ok(Self { + id, + emitted_runtime_closure_term_id, + emitted_direct_closure_row_id, + contribution_range, + exact_summed_factor, + component_factor_digest, + candidate_selector_domain_digest, + selector_domain_digest, + }) + } + + pub const fn id(&self) -> u32 { + self.id + } + + pub const fn emitted_runtime_closure_term_id(&self) -> Option { + self.emitted_runtime_closure_term_id + } + + pub const fn emitted_direct_closure_row_id(&self) -> Option { + self.emitted_direct_closure_row_id + } + + pub const fn contribution_range(&self) -> CheckedTableRange { + self.contribution_range + } + + pub const fn exact_summed_factor(&self) -> ExactComplexRational { + self.exact_summed_factor + } + + pub const fn component_factor_digest(&self) -> SemanticDigest { + self.component_factor_digest + } + + pub const fn candidate_selector_domain_digest(&self) -> SemanticDigest { + self.candidate_selector_domain_digest + } + + pub const fn selector_domain_digest(&self) -> SemanticDigest { + self.selector_domain_digest + } + + pub fn with_direct_closure_row_id(&self, row_id: Option) -> RusticolResult { + self.with_direct_closure_binding( + row_id, + self.component_factor_digest, + self.selector_domain_digest, + ) + } + + pub fn with_direct_closure_binding( + &self, + row_id: Option, + component_factor_digest: SemanticDigest, + selector_domain_digest: SemanticDigest, + ) -> RusticolResult { + Self::new_with_candidate_selector_domain( + self.id, + self.emitted_runtime_closure_term_id, + row_id, + self.contribution_range, + self.exact_summed_factor, + component_factor_digest, + self.candidate_selector_domain_digest, + selector_domain_digest, + ) + } +} + +/// Exact reciprocal-orbit certificate used by folded pure-gluon closures. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ReflectionCertificateV1 { + id: u32, + orbit_identity: SemanticDigest, + reciprocal_identity: SemanticDigest, + source_permutation: Box<[u32]>, + exact_phase: ExactComplexRational, + fixed_point: bool, + orbit_size: u32, + proof_algorithm_id: u32, + proof_digest: SemanticDigest, +} + +impl ReflectionCertificateV1 { + #[allow(clippy::too_many_arguments)] + pub fn new( + id: u32, + orbit_identity: SemanticDigest, + reciprocal_identity: SemanticDigest, + source_permutation: Vec, + exact_phase: ExactComplexRational, + fixed_point: bool, + orbit_size: u32, + proof_algorithm_id: u32, + proof_digest: SemanticDigest, + ) -> RusticolResult { + validate_permutation( + "reflection source permutation", + &source_permutation, + source_permutation.len(), + )?; + if exact_phase.is_zero() { + return Err(invalid("reflection phase must not be zero")); + } + match ( + fixed_point, + orbit_size, + orbit_identity == reciprocal_identity, + ) { + (true, 1, true) | (false, 2, false) => {} + _ => { + return Err(invalid( + "reflection fixed-point flag, orbit size, and reciprocal identities disagree", + )); + } + } + Ok(Self { + id, + orbit_identity, + reciprocal_identity, + source_permutation: source_permutation.into_boxed_slice(), + exact_phase, + fixed_point, + orbit_size, + proof_algorithm_id, + proof_digest, + }) + } + + pub const fn id(&self) -> u32 { + self.id + } + + pub const fn orbit_identity(&self) -> SemanticDigest { + self.orbit_identity + } + + pub const fn reciprocal_identity(&self) -> SemanticDigest { + self.reciprocal_identity + } + + pub fn source_permutation(&self) -> &[u32] { + &self.source_permutation + } + + pub const fn exact_phase(&self) -> ExactComplexRational { + self.exact_phase + } + + pub const fn fixed_point(&self) -> bool { + self.fixed_point + } + + pub const fn orbit_size(&self) -> u32 { + self.orbit_size + } + + pub const fn proof_algorithm_id(&self) -> u32 { + self.proof_algorithm_id + } + + pub const fn proof_digest(&self) -> SemanticDigest { + self.proof_digest + } +} + +/// Certified traversal class for a three-open-line closure. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[repr(u8)] +pub enum ThreeLineTraversalKindV1 { + Direct = 0, + Partner = 1, +} + +impl ThreeLineTraversalKindV1 { + pub const fn code(self) -> u32 { + self as u32 + } +} + +impl TryFrom for ThreeLineTraversalKindV1 { + type Error = RusticolError; + + fn try_from(value: u32) -> Result { + match value { + 0 => Ok(Self::Direct), + 1 => Ok(Self::Partner), + _ => Err(invalid(format!( + "three-line traversal kind must be direct (0) or partner (1), found {value}" + ))), + } + } +} + +/// Cold exact witness for one direct or partner traversal of three open lines. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ThreeLineTraversalCertificateV1 { + id: u32, + sector_id: u32, + kind: ThreeLineTraversalKindV1, + sink_block_ordinal: u32, + reference_block_order: Box<[u32]>, + witness_block_order: Box<[u32]>, + block_permutation: Box<[u32]>, + reference_source_order: Box<[u32]>, + witness_source_order: Box<[u32]>, + source_position_permutation: Box<[u32]>, + closure_anchor_source_slot: u32, + pairing_rule_id: u32, + proof_digest: SemanticDigest, +} + +impl ThreeLineTraversalCertificateV1 { + #[allow(clippy::too_many_arguments)] + pub fn new( + id: u32, + sector_id: u32, + kind: ThreeLineTraversalKindV1, + sink_block_ordinal: u32, + reference_block_order: Vec, + witness_block_order: Vec, + block_permutation: Vec, + reference_source_order: Vec, + witness_source_order: Vec, + source_position_permutation: Vec, + closure_anchor_source_slot: u32, + pairing_rule_id: u32, + proof_digest: SemanticDigest, + ) -> RusticolResult { + const BLOCK_COUNT: usize = 3; + validate_permutation( + "three-line reference block order", + &reference_block_order, + BLOCK_COUNT, + )?; + validate_permutation( + "three-line witness block order", + &witness_block_order, + BLOCK_COUNT, + )?; + validate_permutation( + "three-line block permutation", + &block_permutation, + BLOCK_COUNT, + )?; + if sink_block_ordinal >= BLOCK_COUNT as u32 { + return Err(invalid(format!( + "three-line sink block ordinal {sink_block_ordinal} is out of range" + ))); + } + if reference_block_order[BLOCK_COUNT - 1] != sink_block_ordinal + || witness_block_order[BLOCK_COUNT - 1] != sink_block_ordinal + { + return Err(invalid( + "three-line reference and witness traversals must end at the sink block", + )); + } + for (witness_position, reference_position) in block_permutation.iter().copied().enumerate() + { + if witness_block_order[witness_position] + != reference_block_order[reference_position as usize] + { + return Err(invalid( + "three-line block permutation does not transport reference order to witness order", + )); + } + } + let expected_witness_order = match kind { + ThreeLineTraversalKindV1::Direct => reference_block_order.clone(), + ThreeLineTraversalKindV1::Partner => { + let mut partner = reference_block_order.clone(); + partner.swap(0, 1); + partner + } + }; + if witness_block_order != expected_witness_order { + return Err(invalid(format!( + "three-line {:?} traversal has an inconsistent witness block order", + kind + ))); + } + + if reference_source_order.is_empty() { + return Err(invalid( + "three-line traversal requires a nonempty source order", + )); + } + if witness_source_order.len() != reference_source_order.len() { + return Err(invalid( + "three-line reference and witness source orders have inconsistent lengths", + )); + } + validate_permutation( + "three-line source-position permutation", + &source_position_permutation, + reference_source_order.len(), + )?; + if reference_source_order + .iter() + .copied() + .collect::>() + .len() + != reference_source_order.len() + || witness_source_order + .iter() + .copied() + .collect::>() + .len() + != witness_source_order.len() + { + return Err(invalid( + "three-line source orders must contain unique source slots", + )); + } + for (witness_position, reference_position) in + source_position_permutation.iter().copied().enumerate() + { + if witness_source_order[witness_position] + != reference_source_order[reference_position as usize] + { + return Err(invalid( + "three-line source-position permutation does not transport reference order to witness order", + )); + } + } + if reference_source_order.last() != Some(&closure_anchor_source_slot) + || witness_source_order.last() != Some(&closure_anchor_source_slot) + { + return Err(invalid( + "three-line reference and witness source orders must end at the closure anchor", + )); + } + let expected_proof_digest = three_line_traversal_proof_digest_v1( + sector_id, + kind, + sink_block_ordinal, + &reference_block_order, + &witness_block_order, + &block_permutation, + &reference_source_order, + &witness_source_order, + &source_position_permutation, + closure_anchor_source_slot, + pairing_rule_id, + )?; + if proof_digest != expected_proof_digest { + return Err(invalid("three-line traversal proof digest mismatch")); + } + Ok(Self { + id, + sector_id, + kind, + sink_block_ordinal, + reference_block_order: reference_block_order.into_boxed_slice(), + witness_block_order: witness_block_order.into_boxed_slice(), + block_permutation: block_permutation.into_boxed_slice(), + reference_source_order: reference_source_order.into_boxed_slice(), + witness_source_order: witness_source_order.into_boxed_slice(), + source_position_permutation: source_position_permutation.into_boxed_slice(), + closure_anchor_source_slot, + pairing_rule_id, + proof_digest, + }) + } + + pub const fn id(&self) -> u32 { + self.id + } + + pub const fn sector_id(&self) -> u32 { + self.sector_id + } + + pub const fn kind(&self) -> ThreeLineTraversalKindV1 { + self.kind + } + + pub const fn sink_block_ordinal(&self) -> u32 { + self.sink_block_ordinal + } + + pub fn reference_block_order(&self) -> &[u32] { + &self.reference_block_order + } + + pub fn witness_block_order(&self) -> &[u32] { + &self.witness_block_order + } + + pub fn block_permutation(&self) -> &[u32] { + &self.block_permutation + } + + pub fn reference_source_order(&self) -> &[u32] { + &self.reference_source_order + } + + pub fn witness_source_order(&self) -> &[u32] { + &self.witness_source_order + } + + pub fn source_position_permutation(&self) -> &[u32] { + &self.source_position_permutation + } + + pub const fn closure_anchor_source_slot(&self) -> u32 { + self.closure_anchor_source_slot + } + + pub const fn pairing_rule_id(&self) -> u32 { + self.pairing_rule_id + } + + pub const fn proof_digest(&self) -> SemanticDigest { + self.proof_digest + } +} + +#[allow(clippy::too_many_arguments)] +pub fn three_line_traversal_proof_digest_v1( + sector_id: u32, + kind: ThreeLineTraversalKindV1, + sink_block_ordinal: u32, + reference_block_order: &[u32], + witness_block_order: &[u32], + block_permutation: &[u32], + reference_source_order: &[u32], + witness_source_order: &[u32], + source_position_permutation: &[u32], + closure_anchor_source_slot: u32, + pairing_rule_id: u32, +) -> RusticolResult { + let mut hash = Sha256::new(); + hash.update(b"pyamplicol-recurrence-three-line-traversal-v1\0"); + hash.update(sector_id.to_le_bytes()); + hash.update([kind as u8]); + hash.update(sink_block_ordinal.to_le_bytes()); + for values in [ + reference_block_order, + witness_block_order, + block_permutation, + reference_source_order, + witness_source_order, + source_position_permutation, + ] { + hash.update( + u64::try_from(values.len()) + .map_err(|_| invalid("three-line certificate sequence length exceeds u64"))? + .to_le_bytes(), + ); + for value in values { + hash.update(value.to_le_bytes()); + } + } + hash.update(closure_anchor_source_slot.to_le_bytes()); + hash.update(pairing_rule_id.to_le_bytes()); + SemanticDigest::new(hash.finalize().into()) +} + +fn closure_candidate_domain_certificate_from_tables_v1( + contributions: &[ClosureProofContributionV2], + groups: &[ClosureExecutionProofGroupV2], + reflection_certificates: &[ReflectionCertificateV1], + three_line_traversal_certificates: &[ThreeLineTraversalCertificateV1], +) -> RusticolResult { + let mut identity_digests = Vec::with_capacity(contributions.len()); + for group in groups { + let range = group + .contribution_range + .as_usize_range(contributions.len(), "closure candidate-domain contribution")?; + for contribution in &contributions[range] { + let three_line_proof_digest = contribution + .three_line_certificate_id + .map(|certificate_id| { + three_line_traversal_certificates + .get(certificate_id as usize) + .map(ThreeLineTraversalCertificateV1::proof_digest) + .ok_or_else(|| { + invalid(format!( + "closure candidate references absent three-line certificate {certificate_id}" + )) + }) + }) + .transpose()?; + let reflection_proof_digest = contribution + .reflection_certificate_id + .map(|certificate_id| { + reflection_certificates + .get(certificate_id as usize) + .map(ReflectionCertificateV1::proof_digest) + .ok_or_else(|| { + invalid(format!( + "closure candidate references absent reflection certificate {certificate_id}" + )) + }) + }) + .transpose()?; + identity_digests.push(closure_candidate_identity_digest_v1( + group.candidate_selector_domain_digest, + contribution.target_sector_id, + contribution.closure_template_id, + contribution.closure_template_semantic_digest, + contribution.quantum_flow_template_id, + &contribution.construction_parent_builder_ids, + &contribution.construction_parent_semantic_digests, + &contribution.construction_parent_color_digests, + &contribution.construction_parent_permutation, + &contribution.reconstruction_parent_permutation, + &contribution.evaluator_parent_permutation, + contribution.color_witness_term_id, + contribution.color_witness_proof_digest, + three_line_proof_digest, + &contribution.pairing_certificate_ids, + reflection_proof_digest, + contribution.exact_factor, + contribution.multiplicity, + )?); + } + } + ClosureCandidateDomainCertificateV1::from_identity_digests(identity_digests) +} + +/// Cold exact closure-completeness metadata shared by program and direct plan. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ClosureProofMetadataV2 { + contributions: Box<[ClosureProofContributionV2]>, + groups: Box<[ClosureExecutionProofGroupV2]>, + reflection_certificates: Box<[ReflectionCertificateV1]>, + three_line_traversal_certificates: Box<[ThreeLineTraversalCertificateV1]>, + candidate_domain_certificate: ClosureCandidateDomainCertificateV1, + expected_semantic_completeness_digest: SemanticDigest, +} + +impl ClosureProofMetadataV2 { + pub fn new( + contributions: Vec, + groups: Vec, + reflection_certificates: Vec, + ) -> RusticolResult { + Self::new_with_three_line_certificates( + contributions, + groups, + reflection_certificates, + Vec::new(), + ) + } + + pub fn new_with_three_line_certificates( + contributions: Vec, + groups: Vec, + reflection_certificates: Vec, + three_line_traversal_certificates: Vec, + ) -> RusticolResult { + validate_closure_proof_tables( + &contributions, + &groups, + &reflection_certificates, + &three_line_traversal_certificates, + )?; + let candidate_domain_certificate = closure_candidate_domain_certificate_from_tables_v1( + &contributions, + &groups, + &reflection_certificates, + &three_line_traversal_certificates, + )?; + Self::new_with_three_line_certificates_and_candidate_domain( + contributions, + groups, + reflection_certificates, + three_line_traversal_certificates, + candidate_domain_certificate, + ) + } + + pub fn new_with_three_line_certificates_and_candidate_domain( + contributions: Vec, + groups: Vec, + reflection_certificates: Vec, + three_line_traversal_certificates: Vec, + candidate_domain_certificate: ClosureCandidateDomainCertificateV1, + ) -> RusticolResult { + validate_closure_proof_tables( + &contributions, + &groups, + &reflection_certificates, + &three_line_traversal_certificates, + )?; + let actual_candidate_domain_certificate = + closure_candidate_domain_certificate_from_tables_v1( + &contributions, + &groups, + &reflection_certificates, + &three_line_traversal_certificates, + )?; + if actual_candidate_domain_certificate != candidate_domain_certificate { + return Err(invalid( + "closure candidate-domain certificate does not match persisted proof rows", + )); + } + let expected_semantic_completeness_digest = + closure_proof_semantic_completeness_digest_with_three_line_v2( + &contributions, + &groups, + &reflection_certificates, + &three_line_traversal_certificates, + )?; + Ok(Self { + contributions: contributions.into_boxed_slice(), + groups: groups.into_boxed_slice(), + reflection_certificates: reflection_certificates.into_boxed_slice(), + three_line_traversal_certificates: three_line_traversal_certificates.into_boxed_slice(), + candidate_domain_certificate, + expected_semantic_completeness_digest, + }) + } + + pub fn new_with_expected_digest( + contributions: Vec, + groups: Vec, + reflection_certificates: Vec, + expected_semantic_completeness_digest: SemanticDigest, + ) -> RusticolResult { + let metadata = Self::new(contributions, groups, reflection_certificates)?; + metadata.validate_expected_digest(expected_semantic_completeness_digest) + } + + pub fn new_with_three_line_certificates_and_expected_digest( + contributions: Vec, + groups: Vec, + reflection_certificates: Vec, + three_line_traversal_certificates: Vec, + expected_semantic_completeness_digest: SemanticDigest, + ) -> RusticolResult { + let metadata = Self::new_with_three_line_certificates( + contributions, + groups, + reflection_certificates, + three_line_traversal_certificates, + )?; + metadata.validate_expected_digest(expected_semantic_completeness_digest) + } + + #[allow(clippy::too_many_arguments)] + pub fn new_with_three_line_certificates_candidate_domain_and_expected_digest( + contributions: Vec, + groups: Vec, + reflection_certificates: Vec, + three_line_traversal_certificates: Vec, + candidate_domain_certificate: ClosureCandidateDomainCertificateV1, + expected_semantic_completeness_digest: SemanticDigest, + ) -> RusticolResult { + let metadata = Self::new_with_three_line_certificates_and_candidate_domain( + contributions, + groups, + reflection_certificates, + three_line_traversal_certificates, + candidate_domain_certificate, + )?; + metadata.validate_expected_digest(expected_semantic_completeness_digest) + } + + fn validate_expected_digest( + self, + expected_semantic_completeness_digest: SemanticDigest, + ) -> RusticolResult { + if self.expected_semantic_completeness_digest != expected_semantic_completeness_digest { + return Err(invalid( + "closure proof semantic completeness digest mismatch", + )); + } + Ok(self) + } + + pub fn contributions(&self) -> &[ClosureProofContributionV2] { + &self.contributions + } + + pub fn groups(&self) -> &[ClosureExecutionProofGroupV2] { + &self.groups + } + + pub fn reflection_certificates(&self) -> &[ReflectionCertificateV1] { + &self.reflection_certificates + } + + pub fn three_line_traversal_certificates(&self) -> &[ThreeLineTraversalCertificateV1] { + &self.three_line_traversal_certificates + } + + pub const fn candidate_domain_certificate(&self) -> ClosureCandidateDomainCertificateV1 { + self.candidate_domain_certificate + } + + pub const fn expected_semantic_completeness_digest(&self) -> SemanticDigest { + self.expected_semantic_completeness_digest + } + + pub fn group_for_runtime_term( + &self, + closure_term_id: u32, + ) -> Option<&ClosureExecutionProofGroupV2> { + self.groups + .iter() + .find(|group| group.emitted_runtime_closure_term_id == Some(closure_term_id)) + } + + pub fn with_direct_closure_rows(&self, row_by_runtime_term: &[u32]) -> RusticolResult { + let groups = self + .groups + .iter() + .map(|group| { + let row_id = group + .emitted_runtime_closure_term_id + .map(|term_id| { + row_by_runtime_term + .get(term_id as usize) + .copied() + .ok_or_else(|| { + invalid(format!( + "closure proof group {} references absent runtime term {term_id}", + group.id + )) + }) + }) + .transpose()?; + group.with_direct_closure_row_id(row_id) + }) + .collect::>>()?; + Self::new_with_three_line_certificates_and_candidate_domain( + self.contributions.to_vec(), + groups, + self.reflection_certificates.to_vec(), + self.three_line_traversal_certificates.to_vec(), + self.candidate_domain_certificate, + ) + } + + pub(crate) fn validate_tables(&self) -> RusticolResult<()> { + validate_closure_proof_tables( + &self.contributions, + &self.groups, + &self.reflection_certificates, + &self.three_line_traversal_certificates, + )?; + if closure_proof_semantic_completeness_digest_with_three_line_v2( + &self.contributions, + &self.groups, + &self.reflection_certificates, + &self.three_line_traversal_certificates, + )? != self.expected_semantic_completeness_digest + { + return Err(invalid( + "closure proof semantic completeness digest mismatch", + )); + } + if closure_candidate_domain_certificate_from_tables_v1( + &self.contributions, + &self.groups, + &self.reflection_certificates, + &self.three_line_traversal_certificates, + )? != self.candidate_domain_certificate + { + return Err(invalid( + "closure candidate-domain certificate does not match persisted proof rows", + )); + } + Ok(()) + } + + pub(crate) fn discard_rows_for_runtime(&mut self) { + self.contributions = Box::default(); + self.groups = Box::default(); + self.reflection_certificates = Box::default(); + self.three_line_traversal_certificates = Box::default(); + } +} + +pub fn closure_component_factor_digest_v2( + factors: &[ExactComplexRational], +) -> RusticolResult { + let mut hash = Sha256::new(); + hash.update(b"pyamplicol-recurrence-closure-component-factors-v2\0"); + hash.update( + u64::try_from(factors.len()) + .map_err(|_| invalid("closure component-factor count exceeds u64"))? + .to_le_bytes(), + ); + for factor in factors { + hash_exact_factor(&mut hash, *factor); + } + SemanticDigest::new(hash.finalize().into()) +} + +pub fn closure_selector_domain_digest_v2(words: &[u64]) -> RusticolResult { + let mut hash = Sha256::new(); + hash.update(b"pyamplicol-recurrence-closure-selector-domain-v2\0"); + hash.update( + u64::try_from(words.len()) + .map_err(|_| invalid("closure selector-domain word count exceeds u64"))? + .to_le_bytes(), + ); + for word in words { + hash.update(word.to_le_bytes()); + } + SemanticDigest::new(hash.finalize().into()) +} + +pub fn closure_proof_semantic_completeness_digest_v2( + contributions: &[ClosureProofContributionV2], + groups: &[ClosureExecutionProofGroupV2], + reflection_certificates: &[ReflectionCertificateV1], +) -> RusticolResult { + closure_proof_semantic_completeness_digest_with_three_line_v2( + contributions, + groups, + reflection_certificates, + &[], + ) +} + +pub fn closure_proof_semantic_completeness_digest_with_three_line_v2( + contributions: &[ClosureProofContributionV2], + groups: &[ClosureExecutionProofGroupV2], + reflection_certificates: &[ReflectionCertificateV1], + three_line_traversal_certificates: &[ThreeLineTraversalCertificateV1], +) -> RusticolResult { + let mut hash = Sha256::new(); + hash.update(b"pyamplicol-recurrence-closure-proof-v2\0"); + hash.update( + u64::try_from(contributions.len()) + .map_err(|_| invalid("closure proof contribution count exceeds u64"))? + .to_le_bytes(), + ); + for row in contributions { + hash.update(row.id.to_le_bytes()); + hash.update(row.target_sector_id.to_le_bytes()); + hash_optional_u32(&mut hash, row.target_destination_id); + hash_optional_u32(&mut hash, row.target_helicity_id); + hash.update(row.closure_template_id.to_le_bytes()); + hash.update(row.closure_template_semantic_digest.as_bytes()); + hash_optional_u32(&mut hash, row.quantum_flow_template_id); + for values in [ + row.construction_parent_builder_ids.as_ref(), + row.construction_parent_permutation.as_ref(), + row.reconstruction_parent_permutation.as_ref(), + row.evaluator_parent_permutation.as_ref(), + ] { + hash.update( + u64::try_from(values.len()) + .map_err(|_| invalid("closure proof sequence length exceeds u64"))? + .to_le_bytes(), + ); + for value in values { + hash.update(value.to_le_bytes()); + } + } + hash.update( + u64::try_from(row.construction_parent_runtime_ids.len()) + .map_err(|_| invalid("closure proof runtime-parent sequence length exceeds u64"))? + .to_le_bytes(), + ); + for value in row.construction_parent_runtime_ids.iter().copied() { + hash_optional_u32(&mut hash, value); + } + for digests in [ + row.construction_parent_semantic_digests.as_ref(), + row.construction_parent_color_digests.as_ref(), + ] { + hash.update( + u64::try_from(digests.len()) + .map_err(|_| invalid("closure proof digest sequence length exceeds u64"))? + .to_le_bytes(), + ); + for digest in digests { + hash.update(digest.as_bytes()); + } + } + hash.update(row.color_witness_term_id.to_le_bytes()); + hash.update(row.color_witness_proof_digest.as_bytes()); + hash_optional_u32(&mut hash, row.three_line_certificate_id); + hash.update( + u64::try_from(row.pairing_certificate_ids.len()) + .map_err(|_| invalid("closure pairing-certificate count exceeds u64"))? + .to_le_bytes(), + ); + for certificate_id in row.pairing_certificate_ids.iter().copied() { + hash.update(certificate_id.to_le_bytes()); + } + hash_optional_u32(&mut hash, row.reflection_certificate_id); + hash_exact_factor(&mut hash, row.exact_factor); + hash.update(row.multiplicity.to_le_bytes()); + } + hash.update( + u64::try_from(groups.len()) + .map_err(|_| invalid("closure proof group count exceeds u64"))? + .to_le_bytes(), + ); + for row in groups { + hash.update(row.id.to_le_bytes()); + hash_optional_u32(&mut hash, row.emitted_runtime_closure_term_id); + hash_optional_u32(&mut hash, row.emitted_direct_closure_row_id); + hash.update(row.contribution_range.start.to_le_bytes()); + hash.update(row.contribution_range.count.to_le_bytes()); + hash_exact_factor(&mut hash, row.exact_summed_factor); + hash.update(row.component_factor_digest.as_bytes()); + hash.update(row.candidate_selector_domain_digest.as_bytes()); + hash.update(row.selector_domain_digest.as_bytes()); + } + hash.update( + u64::try_from(reflection_certificates.len()) + .map_err(|_| invalid("reflection certificate count exceeds u64"))? + .to_le_bytes(), + ); + for row in reflection_certificates { + hash.update(row.id.to_le_bytes()); + hash.update(row.orbit_identity.as_bytes()); + hash.update(row.reciprocal_identity.as_bytes()); + hash.update( + u64::try_from(row.source_permutation.len()) + .map_err(|_| invalid("reflection permutation length exceeds u64"))? + .to_le_bytes(), + ); + for value in row.source_permutation.iter() { + hash.update(value.to_le_bytes()); + } + hash_exact_factor(&mut hash, row.exact_phase); + hash.update(u8::from(row.fixed_point).to_le_bytes()); + hash.update(row.orbit_size.to_le_bytes()); + hash.update(row.proof_algorithm_id.to_le_bytes()); + hash.update(row.proof_digest.as_bytes()); + } + hash.update( + u64::try_from(three_line_traversal_certificates.len()) + .map_err(|_| invalid("three-line traversal certificate count exceeds u64"))? + .to_le_bytes(), + ); + for row in three_line_traversal_certificates { + hash.update(row.id.to_le_bytes()); + hash.update(row.sector_id.to_le_bytes()); + hash.update([row.kind as u8]); + hash.update(row.sink_block_ordinal.to_le_bytes()); + for values in [ + row.reference_block_order.as_ref(), + row.witness_block_order.as_ref(), + row.block_permutation.as_ref(), + row.reference_source_order.as_ref(), + row.witness_source_order.as_ref(), + row.source_position_permutation.as_ref(), + ] { + hash.update( + u64::try_from(values.len()) + .map_err(|_| invalid("three-line certificate sequence length exceeds u64"))? + .to_le_bytes(), + ); + for value in values { + hash.update(value.to_le_bytes()); + } + } + hash.update(row.closure_anchor_source_slot.to_le_bytes()); + hash.update(row.pairing_rule_id.to_le_bytes()); + hash.update(row.proof_digest.as_bytes()); + } + let candidate_domain_certificate = closure_candidate_domain_certificate_from_tables_v1( + contributions, + groups, + reflection_certificates, + three_line_traversal_certificates, + )?; + hash.update( + candidate_domain_certificate + .accepted_candidate_count() + .to_le_bytes(), + ); + hash.update( + candidate_domain_certificate + .accepted_candidate_digest() + .as_bytes(), + ); + SemanticDigest::new(hash.finalize().into()) +} + +fn validate_closure_proof_tables( + contributions: &[ClosureProofContributionV2], + groups: &[ClosureExecutionProofGroupV2], + reflection_certificates: &[ReflectionCertificateV1], + three_line_traversal_certificates: &[ThreeLineTraversalCertificateV1], +) -> RusticolResult<()> { + u32::try_from(contributions.len()) + .map_err(|_| invalid("closure proof contribution count exceeds u32"))?; + u32::try_from(groups.len()).map_err(|_| invalid("closure proof group count exceeds u32"))?; + u32::try_from(reflection_certificates.len()) + .map_err(|_| invalid("reflection certificate count exceeds u32"))?; + u32::try_from(three_line_traversal_certificates.len()) + .map_err(|_| invalid("three-line traversal certificate count exceeds u32"))?; + + for (index, certificate) in reflection_certificates.iter().enumerate() { + if certificate.id != index as u32 { + return Err(invalid(format!( + "reflection certificate row {index} has non-dense id {}", + certificate.id + ))); + } + } + for (index, certificate) in three_line_traversal_certificates.iter().enumerate() { + if certificate.id != index as u32 { + return Err(invalid(format!( + "three-line traversal certificate row {index} has non-dense id {}", + certificate.id + ))); + } + } + for (index, contribution) in contributions.iter().enumerate() { + if contribution.id != index as u32 { + return Err(invalid(format!( + "closure proof contribution row {index} has non-dense id {}", + contribution.id + ))); + } + if let Some(certificate_id) = contribution.reflection_certificate_id + && certificate_id as usize >= reflection_certificates.len() + { + return Err(invalid(format!( + "closure proof contribution {index} references absent reflection certificate {certificate_id}" + ))); + } + if let Some(certificate_id) = contribution.three_line_certificate_id { + let Some(certificate) = three_line_traversal_certificates.get(certificate_id as usize) + else { + return Err(invalid(format!( + "closure proof contribution {index} references absent three-line traversal certificate {certificate_id}" + ))); + }; + if certificate.sector_id != contribution.target_sector_id { + return Err(invalid(format!( + "closure proof contribution {index} sector {} does not match three-line traversal certificate {certificate_id} sector {}", + contribution.target_sector_id, certificate.sector_id + ))); + } + } + } + + let mut next_contribution = 0u64; + for (index, group) in groups.iter().enumerate() { + if group.id != index as u32 { + return Err(invalid(format!( + "closure proof group row {index} has non-dense id {}", + group.id + ))); + } + if group.contribution_range.start != next_contribution { + return Err(invalid(format!( + "closure proof group {index} starts at {}, expected packed offset {next_contribution}", + group.contribution_range.start + ))); + } + let range = group.contribution_range.as_usize_range( + contributions.len(), + &format!("closure proof group {index} contribution"), + )?; + if range.is_empty() { + return Err(invalid(format!( + "closure proof group {index} has no contributions" + ))); + } + let group_contributions = &contributions[range]; + next_contribution = group + .contribution_range + .end("closure proof-group contribution")?; + let mut exact_sum = ExactComplexRational::ZERO; + let mut execution_identity = None; + for contribution in group_contributions { + let multiplicity = ExactComplexRational::new( + ExactRational::new(i128::from(contribution.multiplicity), 1)?, + ExactRational::ZERO, + ); + exact_sum = + exact_sum.checked_add(contribution.exact_factor.checked_mul(multiplicity)?)?; + let proof_identity = ( + contribution.target_sector_id, + contribution.target_helicity_id, + contribution.closure_template_id, + contribution.quantum_flow_template_id, + contribution.evaluator_parent_builder_ids(), + ); + if let Some(expected) = &execution_identity { + if expected != &proof_identity { + return Err(invalid(format!( + "closure proof group {index} mixes distinct construction identities" + ))); + } + } else { + execution_identity = Some(proof_identity); + } + } + if exact_sum != group.exact_summed_factor { + return Err(invalid(format!( + "closure proof group {index} exact factor does not equal its contribution sum" + ))); + } + match ( + exact_sum.is_zero(), + group.emitted_runtime_closure_term_id, + group.emitted_direct_closure_row_id, + ) { + (true, None, None) | (false, Some(_), None | Some(_)) => {} + (true, _, _) => { + return Err(invalid(format!( + "exact-zero closure proof group {index} references a runtime closure" + ))); + } + (false, None, _) => { + return Err(invalid(format!( + "nonzero closure proof group {index} has no runtime closure term" + ))); + } + } + if !exact_sum.is_zero() { + for contribution in group_contributions { + if contribution.target_destination_id.is_none() + || contribution.evaluator_parent_runtime_ids().is_none() + { + return Err(invalid(format!( + "nonzero closure proof group {index} lacks a complete runtime binding" + ))); + } + } + } + } + if next_contribution != contributions.len() as u64 { + return Err(invalid(format!( + "closure proof groups consume {next_contribution} of {} contributions", + contributions.len() + ))); + } + Ok(()) +} + +/// One source or propagated current in topological execution order. +/// +/// For a source, the key's dynamic color-state ID refers to the materialized +/// result of a compiler-owned source-color seed. This schedule never infers a +/// seed from particle identity, representation, or source position. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct RecurrenceCurrent { + id: u32, + key: CurrentCoreKey, + source_exact_factor: Option, + contribution_range: CheckedTableRange, + finalization_id: Option, +} + +impl RecurrenceCurrent { + pub fn new( + id: u32, + key: CurrentCoreKey, + source_exact_factor: Option, + contribution_range: CheckedTableRange, + finalization_id: Option, + ) -> RusticolResult { + contribution_range.end("recurrence current contribution")?; + if source_exact_factor.is_some_and(ExactComplexRational::is_zero) { + return Err(invalid("recurrence source factor must not be zero")); + } + Ok(Self { + id, + key, + source_exact_factor, + contribution_range, + finalization_id, + }) + } + + pub const fn id(&self) -> u32 { + self.id + } + + pub const fn key(&self) -> &CurrentCoreKey { + &self.key + } + + pub const fn contribution_range(&self) -> CheckedTableRange { + self.contribution_range + } + + pub const fn source_exact_factor(&self) -> Option { + self.source_exact_factor + } + + pub const fn fan_in(&self) -> u64 { + self.contribution_range.count + } + + pub const fn finalization_id(&self) -> Option { + self.finalization_id + } + + pub const fn is_source(&self) -> bool { + matches!(self.key.node_kind(), RecurrenceNodeKind::Source) + } +} + +/// One exact contribution accumulated into one result current. +/// +/// The key's color-witness term owns the compiler-certified result-component +/// role (active, passive, or absent). The program only stores its materialized +/// result current and does not derive that role from parent ordering. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct RecurrenceContribution { + id: u32, + result_current_id: u32, + parent_current_ids: Box<[u32]>, + key: ContributionKey, + exact_factor: ExactComplexRational, +} + +impl RecurrenceContribution { + pub fn new( + id: u32, + result_current_id: u32, + parent_current_ids: Vec, + key: ContributionKey, + exact_factor: ExactComplexRational, + ) -> RusticolResult { + if parent_current_ids.is_empty() { + return Err(invalid( + "recurrence contribution requires at least one parent current", + )); + } + u32::try_from(parent_current_ids.len()) + .map_err(|_| invalid("recurrence contribution parent count exceeds u32"))?; + if exact_factor.is_zero() { + return Err(invalid("recurrence contribution factor must not be zero")); + } + Ok(Self { + id, + result_current_id, + parent_current_ids: parent_current_ids.into_boxed_slice(), + key, + exact_factor, + }) + } + + pub const fn id(&self) -> u32 { + self.id + } + + pub const fn result_current_id(&self) -> u32 { + self.result_current_id + } + + pub fn parent_current_ids(&self) -> &[u32] { + &self.parent_current_ids + } + + pub const fn key(&self) -> &ContributionKey { + &self.key + } + + pub const fn exact_factor(&self) -> ExactComplexRational { + self.exact_factor + } +} + +/// Exactly one propagation/finalization operation for one non-source current. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct RecurrenceFinalization { + id: u32, + current_id: u32, + propagator_template_id: Option, + exact_factor: ExactComplexRational, +} + +/// One complete physical helicity retained by a topology-replay schedule. +/// +/// Currents retain only their local source-state ancestry. This catalog names +/// the complete ancestry reached by closure terms so amplitudes from distinct +/// helicities are never added coherently before squaring. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct RecurrenceResolvedHelicity { + id: u32, + source_states: Box<[SourceStateAssignment]>, + public_helicities: Box<[i32]>, +} + +/// One live amplitude destination and its packed closure-term range. +/// +/// Destinations are sparse: topology replay stores only live combinations of +/// a materialized representative sector and a resolved helicity, never their +/// Cartesian product. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct RecurrenceAmplitudeDestination { + id: u32, + target_sector_id: u32, + target_helicity_id: Option, + closure_range: CheckedTableRange, +} + +/// One exact topology-replay route from a materialized recurrence to a public sector. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct RecurrenceReplayTarget { + id: u32, + materialized_sector_id: u32, + target_sector_id: u32, + source_slot_permutation: Box<[u32]>, + source_momentum_signs: Box<[i32]>, + amplitude_factor: ExactComplexRational, +} + +impl RecurrenceReplayTarget { + pub fn new( + id: u32, + materialized_sector_id: u32, + target_sector_id: u32, + source_slot_permutation: Vec, + source_momentum_signs: Vec, + amplitude_factor: ExactComplexRational, + ) -> RusticolResult { + if source_slot_permutation.is_empty() { + return Err(invalid( + "recurrence replay target requires a source permutation", + )); + } + if source_momentum_signs.len() != source_slot_permutation.len() + || source_momentum_signs + .iter() + .any(|sign| !matches!(sign, -1 | 1)) + { + return Err(invalid( + "recurrence replay target requires one signed momentum map per source", + )); + } + if amplitude_factor.is_zero() { + return Err(invalid("recurrence replay target factor must not be zero")); + } + Ok(Self { + id, + materialized_sector_id, + target_sector_id, + source_slot_permutation: source_slot_permutation.into_boxed_slice(), + source_momentum_signs: source_momentum_signs.into_boxed_slice(), + amplitude_factor, + }) + } + + pub const fn id(&self) -> u32 { + self.id + } + + pub const fn materialized_sector_id(&self) -> u32 { + self.materialized_sector_id + } + + pub const fn target_sector_id(&self) -> u32 { + self.target_sector_id + } + + pub fn source_slot_permutation(&self) -> &[u32] { + &self.source_slot_permutation + } + + pub fn source_momentum_signs(&self) -> &[i32] { + &self.source_momentum_signs + } + + pub const fn amplitude_factor(&self) -> ExactComplexRational { + self.amplitude_factor + } +} + +impl RecurrenceAmplitudeDestination { + pub fn new( + id: u32, + target_sector_id: u32, + target_helicity_id: Option, + closure_range: CheckedTableRange, + ) -> RusticolResult { + closure_range.end("recurrence amplitude destination")?; + if closure_range.count == 0 { + return Err(invalid( + "recurrence amplitude destination requires closure terms", + )); + } + Ok(Self { + id, + target_sector_id, + target_helicity_id, + closure_range, + }) + } + + pub const fn id(self) -> u32 { + self.id + } + + pub const fn target_sector_id(self) -> u32 { + self.target_sector_id + } + + pub const fn target_helicity_id(self) -> Option { + self.target_helicity_id + } + + pub const fn closure_range(self) -> CheckedTableRange { + self.closure_range + } +} + +impl RecurrenceResolvedHelicity { + pub fn new( + id: u32, + source_states: Vec, + public_helicities: Vec, + ) -> RusticolResult { + if source_states.is_empty() || source_states.len() != public_helicities.len() { + return Err(invalid( + "resolved helicity requires one public value per source-state assignment", + )); + } + for (source_slot, assignment) in source_states.iter().copied().enumerate() { + if assignment.source_slot() as usize != source_slot { + return Err(invalid( + "resolved-helicity source states must cover every source slot in order", + )); + } + } + Ok(Self { + id, + source_states: source_states.into_boxed_slice(), + public_helicities: public_helicities.into_boxed_slice(), + }) + } + + pub const fn id(&self) -> u32 { + self.id + } + + pub fn source_states(&self) -> &[SourceStateAssignment] { + &self.source_states + } + + pub fn public_helicities(&self) -> &[i32] { + &self.public_helicities + } +} + +impl RecurrenceFinalization { + pub fn new( + id: u32, + current_id: u32, + propagator_template_id: Option, + exact_factor: ExactComplexRational, + ) -> RusticolResult { + if exact_factor.is_zero() { + return Err(invalid("recurrence finalization factor must not be zero")); + } + Ok(Self { + id, + current_id, + propagator_template_id, + exact_factor, + }) + } + + pub const fn id(&self) -> u32 { + self.id + } + + pub const fn current_id(&self) -> u32 { + self.current_id + } + + pub const fn propagator_template_id(&self) -> Option { + self.propagator_template_id + } + + pub const fn exact_factor(&self) -> ExactComplexRational { + self.exact_factor + } +} + +/// One exact signed term contributing to one physical LC target sector. +/// +/// The closure template owns the result-component kind. `target_sector_id` is +/// only the reduction destination selected after validating the physical +/// sector's compiler-owned closure anchor; it is not itself an anchor or an +/// instruction for reconstructing one. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct RecurrenceClosureTerm { + id: u32, + target_destination_id: u32, + closure_template_id: u32, + quantum_flow_template_id: Option, + parent_current_ids: Box<[u32]>, + exact_factor: ExactComplexRational, +} + +impl RecurrenceClosureTerm { + pub fn new( + id: u32, + target_destination_id: u32, + closure_template_id: u32, + quantum_flow_template_id: Option, + parent_current_ids: Vec, + exact_factor: ExactComplexRational, + ) -> RusticolResult { + if parent_current_ids.is_empty() { + return Err(invalid( + "recurrence closure term requires at least one parent current", + )); + } + u32::try_from(parent_current_ids.len()) + .map_err(|_| invalid("recurrence closure parent count exceeds u32"))?; + if exact_factor.is_zero() { + return Err(invalid("recurrence closure term factor must not be zero")); + } + Ok(Self { + id, + target_destination_id, + closure_template_id, + quantum_flow_template_id, + parent_current_ids: parent_current_ids.into_boxed_slice(), + exact_factor, + }) + } + + pub const fn id(&self) -> u32 { + self.id + } + + pub const fn target_destination_id(&self) -> u32 { + self.target_destination_id + } + + pub const fn closure_template_id(&self) -> u32 { + self.closure_template_id + } + + /// Prepared quantum-flow witness for a kernel closure. + /// + /// Direct Rusticol closure templates have no vertex coupling and therefore + /// carry `None`. + pub const fn quantum_flow_template_id(&self) -> Option { + self.quantum_flow_template_id + } + + pub fn parent_current_ids(&self) -> &[u32] { + &self.parent_current_ids + } + + pub const fn exact_factor(&self) -> ExactComplexRational { + self.exact_factor + } +} + +/// Validated compact recurrence program ready for serialization or execution. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct RecurrenceProgram { + strategy: RecurrenceStrategy, + physical_sector_count: u32, + retained_helicity_count: u64, + dynamic_color_states: Box<[DynamicLCColorState]>, + currents: Box<[RecurrenceCurrent]>, + contributions: Box<[RecurrenceContribution]>, + finalizations: Box<[RecurrenceFinalization]>, + replay_targets: Box<[RecurrenceReplayTarget]>, + resolved_helicities: Box<[RecurrenceResolvedHelicity]>, + amplitude_destinations: Box<[RecurrenceAmplitudeDestination]>, + closure_terms: Box<[RecurrenceClosureTerm]>, + closure_proofs: ClosureProofMetadataV2, + closure_target_domain_certificate: Option, + color_projection_certificate_body: Option>, +} + +impl RecurrenceProgram { + #[allow(clippy::too_many_arguments)] + pub fn new( + strategy: RecurrenceStrategy, + physical_sector_count: u32, + retained_helicity_count: u64, + dynamic_color_states: Vec, + currents: Vec, + contributions: Vec, + finalizations: Vec, + replay_targets: Vec, + resolved_helicities: Vec, + amplitude_destinations: Vec, + closure_terms: Vec, + ) -> RusticolResult { + let closure_proofs = closure_proofs_from_materialized_terms( + ¤ts, + &litude_destinations, + &closure_terms, + )?; + Self::new_with_closure_proofs( + strategy, + physical_sector_count, + retained_helicity_count, + dynamic_color_states, + currents, + contributions, + finalizations, + replay_targets, + resolved_helicities, + amplitude_destinations, + closure_terms, + closure_proofs, + ) + } + + #[allow(clippy::too_many_arguments)] + pub fn new_with_closure_proofs( + strategy: RecurrenceStrategy, + physical_sector_count: u32, + retained_helicity_count: u64, + dynamic_color_states: Vec, + currents: Vec, + contributions: Vec, + finalizations: Vec, + replay_targets: Vec, + resolved_helicities: Vec, + amplitude_destinations: Vec, + closure_terms: Vec, + closure_proofs: ClosureProofMetadataV2, + ) -> RusticolResult { + let program = Self { + strategy, + physical_sector_count, + retained_helicity_count, + dynamic_color_states: dynamic_color_states.into_boxed_slice(), + currents: currents.into_boxed_slice(), + contributions: contributions.into_boxed_slice(), + finalizations: finalizations.into_boxed_slice(), + replay_targets: replay_targets.into_boxed_slice(), + resolved_helicities: resolved_helicities.into_boxed_slice(), + amplitude_destinations: amplitude_destinations.into_boxed_slice(), + closure_terms: closure_terms.into_boxed_slice(), + closure_proofs, + closure_target_domain_certificate: None, + color_projection_certificate_body: None, + }; + program.validate()?; + Ok(program) + } + + pub const fn strategy(&self) -> RecurrenceStrategy { + self.strategy + } + + pub const fn physical_sector_count(&self) -> u32 { + self.physical_sector_count + } + + pub const fn retained_helicity_count(&self) -> u64 { + self.retained_helicity_count + } + + pub fn dynamic_color_states(&self) -> &[DynamicLCColorState] { + &self.dynamic_color_states + } + + pub fn currents(&self) -> &[RecurrenceCurrent] { + &self.currents + } + + pub fn contributions(&self) -> &[RecurrenceContribution] { + &self.contributions + } + + pub fn finalizations(&self) -> &[RecurrenceFinalization] { + &self.finalizations + } + + pub fn replay_targets(&self) -> &[RecurrenceReplayTarget] { + &self.replay_targets + } + + pub fn resolved_helicities(&self) -> &[RecurrenceResolvedHelicity] { + &self.resolved_helicities + } + + pub fn amplitude_destinations(&self) -> &[RecurrenceAmplitudeDestination] { + &self.amplitude_destinations + } + + pub fn closure_terms(&self) -> &[RecurrenceClosureTerm] { + &self.closure_terms + } + + pub const fn closure_proofs(&self) -> &ClosureProofMetadataV2 { + &self.closure_proofs + } + + pub fn closure_target_domain_certificate(&self) -> Option<&ClosureTargetDomainCertificateV1> { + self.closure_target_domain_certificate.as_ref() + } + + pub(crate) fn with_closure_target_domain_certificate( + mut self, + certificate: ClosureTargetDomainCertificateV1, + ) -> RusticolResult { + if self.strategy != RecurrenceStrategy::ContractedColorUnion { + return Err(invalid( + "closure target-domain certificate is restricted to contracted color", + )); + } + if certificate.physical_sector_count() != self.physical_sector_count + || certificate.resolved_helicity_count() as usize != self.resolved_helicities.len() + { + return Err(invalid( + "closure target-domain certificate disagrees with its recurrence domain", + )); + } + let active_sector_ids = self + .amplitude_destinations + .iter() + .map(|destination| destination.target_sector_id()) + .collect::>(); + if certificate + .certified_structural_zero_physical_sector_ids() + .iter() + .any(|sector_id| active_sector_ids.contains(sector_id)) + { + return Err(invalid( + "closure target-domain certificate marks an active sector as structural zero", + )); + } + self.closure_target_domain_certificate = Some(certificate); + Ok(self) + } + + pub(crate) fn with_color_projection_certificate_body( + mut self, + body: Vec, + ) -> RusticolResult { + if body.is_empty() { + return Err(invalid("recurrence color-projection certificate is empty")); + } + if self.strategy != RecurrenceStrategy::TopologyReplay { + return Err(invalid( + "recurrence color projection is restricted to topology replay", + )); + } + self.color_projection_certificate_body = Some(body.into_boxed_slice()); + Ok(self) + } + + pub fn color_projection_certificate_body(&self) -> Option<&[u8]> { + self.color_projection_certificate_body.as_deref() + } + + pub fn current_range(&self) -> CheckedTableRange { + CheckedTableRange::new(0, self.currents.len() as u64) + } + + pub fn contribution_range(&self) -> CheckedTableRange { + CheckedTableRange::new(0, self.contributions.len() as u64) + } + + pub fn closure_term_range(&self) -> CheckedTableRange { + CheckedTableRange::new(0, self.closure_terms.len() as u64) + } + + pub fn closure_range_for_destination(&self, destination_id: u32) -> Option { + self.amplitude_destinations + .get(destination_id as usize) + .map(|destination| destination.closure_range()) + } + + pub fn validate(&self) -> RusticolResult<()> { + let current_count = checked_table_len("recurrence current", self.currents.len())?; + let color_state_count = checked_table_len( + "recurrence dynamic LC color state", + self.dynamic_color_states.len(), + )?; + let contribution_count = + checked_table_len("recurrence contribution", self.contributions.len())?; + let finalization_count = + checked_table_len("recurrence finalization", self.finalizations.len())?; + let replay_target_count = + checked_table_len("recurrence replay target", self.replay_targets.len())?; + let closure_count = checked_table_len("recurrence closure term", self.closure_terms.len())?; + let helicity_count = checked_table_len( + "recurrence resolved helicity", + self.resolved_helicities.len(), + )?; + let destination_count = checked_table_len( + "recurrence amplitude destination", + self.amplitude_destinations.len(), + )?; + + if self.physical_sector_count == 0 { + return Err(invalid("recurrence requires physical LC sectors")); + } + if self.retained_helicity_count == 0 { + return Err(invalid("recurrence retains no public helicities")); + } + + u32::try_from(current_count) + .map_err(|_| invalid("recurrence current count exceeds the u32 ID domain"))?; + u32::try_from(color_state_count) + .map_err(|_| invalid("recurrence dynamic color-state count exceeds u32"))?; + u32::try_from(contribution_count) + .map_err(|_| invalid("recurrence contribution count exceeds the u32 ID domain"))?; + u32::try_from(finalization_count) + .map_err(|_| invalid("recurrence finalization count exceeds the u32 ID domain"))?; + u32::try_from(replay_target_count) + .map_err(|_| invalid("recurrence replay-target count exceeds the u32 ID domain"))?; + u32::try_from(closure_count) + .map_err(|_| invalid("recurrence closure count exceeds the u32 ID domain"))?; + u32::try_from(helicity_count) + .map_err(|_| invalid("recurrence resolved-helicity count exceeds u32"))?; + u32::try_from(destination_count) + .map_err(|_| invalid("recurrence amplitude-destination count exceeds u32"))?; + + let mut next_contribution = 0u64; + let mut fan_in_sum = 0u64; + let mut non_source_count = 0u64; + for (index, current) in self.currents.iter().enumerate() { + let expected_id = index as u32; + if current.id != expected_id { + return Err(invalid(format!( + "recurrence current row {index} has non-dense id {}", + current.id + ))); + } + if current.key.helicity_identity().strategy() != self.strategy { + return Err(invalid(format!( + "recurrence current {expected_id} uses strategy {} in a {} program", + current.key.helicity_identity().strategy(), + self.strategy + ))); + } + if u64::from(current.key.dynamic_lc_color_state_id().get()) >= color_state_count { + return Err(invalid(format!( + "recurrence current {expected_id} references unknown dynamic LC color state {}", + current.key.dynamic_lc_color_state_id().get() + ))); + } + if current.contribution_range.start != next_contribution { + return Err(invalid(format!( + "recurrence current {expected_id} contribution range starts at {}, expected packed offset {next_contribution}", + current.contribution_range.start + ))); + } + let range = current.contribution_range.as_usize_range( + self.contributions.len(), + &format!("recurrence current {expected_id} contribution"), + )?; + next_contribution = current + .contribution_range + .end("recurrence current contribution")?; + fan_in_sum = fan_in_sum + .checked_add(current.fan_in()) + .ok_or_else(|| invalid("recurrence fan-in sum exceeds u64"))?; + + if current.is_source() { + if !range.is_empty() || current.finalization_id.is_some() { + return Err(invalid(format!( + "source current {expected_id} must not have contributions or finalization" + ))); + } + match self.strategy { + RecurrenceStrategy::TopologyReplay + | RecurrenceStrategy::ContractedColorUnion + if current.source_exact_factor.is_none() => + { + return Err(invalid(format!( + "{} source current {expected_id} requires an exact source factor", + self.strategy + ))); + } + RecurrenceStrategy::AllFlowUnion if current.source_exact_factor.is_some() => { + return Err(invalid(format!( + "all-flow-union source current {expected_id} must take its factor from runtime dispatch" + ))); + } + _ => {} + } + } else { + if current.source_exact_factor.is_some() { + return Err(invalid(format!( + "non-source current {expected_id} cannot carry a source factor" + ))); + } + non_source_count += 1; + if range.is_empty() { + return Err(invalid(format!( + "non-source current {expected_id} requires at least one contribution" + ))); + } + let finalization_id = current.finalization_id.ok_or_else(|| { + invalid(format!( + "non-source current {expected_id} requires exactly one finalization" + )) + })?; + let finalization = self + .finalizations + .get(finalization_id as usize) + .ok_or_else(|| { + invalid(format!( + "current {expected_id} references unknown finalization {finalization_id}" + )) + })?; + if finalization.current_id != expected_id { + return Err(invalid(format!( + "current {expected_id} references finalization {finalization_id} owned by current {}", + finalization.current_id + ))); + } + } + + for contribution in &self.contributions[range] { + if contribution.result_current_id != expected_id { + return Err(invalid(format!( + "contribution {} is packed under current {expected_id} but targets current {}", + contribution.id, contribution.result_current_id + ))); + } + } + } + if next_contribution != contribution_count || fan_in_sum != contribution_count { + return Err(invalid(format!( + "stored contribution count {contribution_count} does not equal packed fan-in sum {fan_in_sum}" + ))); + } + + for (index, contribution) in self.contributions.iter().enumerate() { + let expected_id = index as u32; + if contribution.id != expected_id { + return Err(invalid(format!( + "recurrence contribution row {index} has non-dense id {}", + contribution.id + ))); + } + let result = self + .currents + .get(contribution.result_current_id as usize) + .ok_or_else(|| { + invalid(format!( + "contribution {expected_id} references unknown result current {}", + contribution.result_current_id + )) + })?; + if contribution.parent_current_ids.len() + != contribution.key.parent_state_template_ids().len() + { + return Err(invalid(format!( + "contribution {expected_id} has {} parent current IDs but {} semantic parent states", + contribution.parent_current_ids.len(), + contribution.key.parent_state_template_ids().len() + ))); + } + if contribution.key.result_state_template_id() != result.key.current_state_template_id() + { + return Err(invalid(format!( + "contribution {expected_id} result-state template {} does not match current {} state template {}", + contribution.key.result_state_template_id(), + contribution.result_current_id, + result.key.current_state_template_id() + ))); + } + for (parent_ordinal, parent_id) in + contribution.parent_current_ids.iter().copied().enumerate() + { + if parent_id >= contribution.result_current_id { + return Err(invalid(format!( + "contribution {expected_id} parent {parent_ordinal} current {parent_id} does not precede result current {}", + contribution.result_current_id + ))); + } + let parent = &self.currents[parent_id as usize]; + let expected_state = contribution.key.parent_state_template_ids()[parent_ordinal]; + if parent.key.current_state_template_id() != expected_state { + return Err(invalid(format!( + "contribution {expected_id} parent {parent_ordinal} current {parent_id} has state template {}, expected {expected_state}", + parent.key.current_state_template_id() + ))); + } + } + if contribution.exact_factor.is_zero() { + return Err(invalid(format!( + "recurrence contribution {expected_id} has zero exact factor" + ))); + } + } + + if finalization_count != non_source_count { + return Err(invalid(format!( + "recurrence has {finalization_count} finalizations for {non_source_count} non-source currents" + ))); + } + for (index, finalization) in self.finalizations.iter().enumerate() { + let expected_id = index as u32; + if finalization.id != expected_id { + return Err(invalid(format!( + "recurrence finalization row {index} has non-dense id {}", + finalization.id + ))); + } + let current = self + .currents + .get(finalization.current_id as usize) + .ok_or_else(|| { + invalid(format!( + "finalization {expected_id} references unknown current {}", + finalization.current_id + )) + })?; + if current.is_source() { + return Err(invalid(format!( + "finalization {expected_id} references source current {}", + finalization.current_id + ))); + } + if current.finalization_id != Some(expected_id) { + return Err(invalid(format!( + "finalization {expected_id} is not the unique finalization recorded by current {}", + finalization.current_id + ))); + } + if finalization.propagator_template_id != current.key.propagator_template_id() { + return Err(invalid(format!( + "finalization {expected_id} propagator template {:?} does not match current {} template {:?}", + finalization.propagator_template_id, + finalization.current_id, + current.key.propagator_template_id() + ))); + } + if finalization.exact_factor.is_zero() { + return Err(invalid(format!( + "recurrence finalization {expected_id} has zero exact factor" + ))); + } + } + + let source_slot_count = self + .currents + .iter() + .flat_map(|current| current.key.support_source_slots().iter().copied()) + .max() + .map_or(0usize, |slot| slot as usize + 1); + let mut replayed_sectors = BTreeSet::new(); + let mut materialized_sectors = BTreeSet::new(); + for (index, target) in self.replay_targets.iter().enumerate() { + let expected_id = index as u32; + if target.id != expected_id { + return Err(invalid(format!( + "recurrence replay-target row {index} has non-dense id {}", + target.id + ))); + } + if target.materialized_sector_id >= self.physical_sector_count + || target.target_sector_id >= self.physical_sector_count + { + return Err(invalid(format!( + "recurrence replay target {expected_id} references an unknown physical sector" + ))); + } + if !replayed_sectors.insert(target.target_sector_id) { + return Err(invalid(format!( + "physical sector {} has multiple recurrence replay targets", + target.target_sector_id + ))); + } + materialized_sectors.insert(target.materialized_sector_id); + if target.source_slot_permutation.len() != source_slot_count { + return Err(invalid(format!( + "recurrence replay target {expected_id} source permutation has length {}, expected {source_slot_count}", + target.source_slot_permutation.len() + ))); + } + if target.source_momentum_signs.len() != source_slot_count + || target + .source_momentum_signs + .iter() + .any(|sign| !matches!(sign, -1 | 1)) + { + return Err(invalid(format!( + "recurrence replay target {expected_id} has an invalid momentum-sign mapping" + ))); + } + let mut permutation = target.source_slot_permutation.to_vec(); + permutation.sort_unstable(); + if permutation + .iter() + .copied() + .enumerate() + .any(|(slot, value)| value as usize != slot) + { + return Err(invalid(format!( + "recurrence replay target {expected_id} source mapping is not a permutation" + ))); + } + } + + match self.strategy { + RecurrenceStrategy::TopologyReplay if self.replay_targets.is_empty() => { + return Err(invalid( + "topology-replay recurrence requires replay targets", + )); + } + RecurrenceStrategy::AllFlowUnion if !self.replay_targets.is_empty() => { + return Err(invalid( + "all-flow-union recurrence must not carry topology-replay targets", + )); + } + _ => {} + } + if self.strategy.uses_topology_replay_targets() + && !self.replay_targets.is_empty() + && replayed_sectors.len() != self.physical_sector_count as usize + { + return Err(invalid(format!( + "{} recurrence replay covers {} of {} physical sectors", + self.strategy, + replayed_sectors.len(), + self.physical_sector_count + ))); + } + let mut destination_sectors = BTreeSet::new(); + for destination in &self.amplitude_destinations { + destination_sectors.insert(destination.target_sector_id); + if self.strategy.uses_topology_replay_targets() + && !self.replay_targets.is_empty() + && !materialized_sectors.contains(&destination.target_sector_id) + { + return Err(invalid(format!( + "{} amplitude destination {} targets non-materialized sector {}", + self.strategy, destination.id, destination.target_sector_id + ))); + } + } + for sector in materialized_sectors { + if !destination_sectors.contains(§or) { + return Err(invalid(format!( + "recurrence replay materialized sector {sector} has no amplitude destination" + ))); + } + } + + match self.strategy { + RecurrenceStrategy::TopologyReplay if self.resolved_helicities.is_empty() => { + return Err(invalid( + "topology-replay recurrence requires resolved-helicity destinations", + )); + } + RecurrenceStrategy::AllFlowUnion if !self.resolved_helicities.is_empty() => { + return Err(invalid( + "all-flow-union recurrence must select source helicity at runtime", + )); + } + RecurrenceStrategy::ContractedColorUnion if self.resolved_helicities.is_empty() => { + return Err(invalid( + "contracted-color recurrence requires resolved-helicity destinations", + )); + } + _ => {} + } + if helicity_count > self.retained_helicity_count { + return Err(invalid(format!( + "recurrence has {helicity_count} active helicities but retains only {} public assignments", + self.retained_helicity_count + ))); + } + for (index, helicity) in self.resolved_helicities.iter().enumerate() { + if helicity.id != index as u32 { + return Err(invalid(format!( + "resolved-helicity row {index} has non-dense id {}", + helicity.id + ))); + } + if helicity.source_states.len() != helicity.public_helicities.len() { + return Err(invalid(format!( + "resolved helicity {index} has inconsistent source-state and public-helicity dimensions" + ))); + } + for (source_slot, assignment) in helicity.source_states.iter().copied().enumerate() { + if assignment.source_slot() as usize != source_slot { + return Err(invalid(format!( + "resolved helicity {index} does not cover source slot {source_slot} canonically" + ))); + } + } + } + + let mut next_closure = 0u64; + for (destination_index, destination) in + self.amplitude_destinations.iter().copied().enumerate() + { + let destination_id = destination_index as u32; + if destination.id != destination_id { + return Err(invalid(format!( + "amplitude destination row {destination_index} has non-dense id {}", + destination.id + ))); + } + if destination.target_sector_id >= self.physical_sector_count { + return Err(invalid(format!( + "amplitude destination {destination_id} references unknown physical sector {}", + destination.target_sector_id + ))); + } + match (self.strategy, destination.target_helicity_id) { + (RecurrenceStrategy::TopologyReplay, Some(helicity_id)) + if u64::from(helicity_id) < helicity_count => {} + (RecurrenceStrategy::TopologyReplay, Some(helicity_id)) => { + return Err(invalid(format!( + "amplitude destination {destination_id} references unknown resolved helicity {helicity_id}" + ))); + } + (RecurrenceStrategy::TopologyReplay, None) => { + return Err(invalid(format!( + "topology-replay amplitude destination {destination_id} lacks a helicity" + ))); + } + (RecurrenceStrategy::AllFlowUnion, None) => {} + (RecurrenceStrategy::AllFlowUnion, Some(_)) => { + return Err(invalid(format!( + "all-flow-union amplitude destination {destination_id} fixes a helicity" + ))); + } + (RecurrenceStrategy::ContractedColorUnion, Some(helicity_id)) + if u64::from(helicity_id) < helicity_count => {} + (RecurrenceStrategy::ContractedColorUnion, Some(helicity_id)) => { + return Err(invalid(format!( + "contracted-color amplitude destination {destination_id} references unknown resolved helicity {helicity_id}" + ))); + } + (RecurrenceStrategy::ContractedColorUnion, None) => { + return Err(invalid(format!( + "contracted-color amplitude destination {destination_id} lacks a helicity" + ))); + } + } + let range = destination.closure_range; + if range.start != next_closure { + return Err(invalid(format!( + "amplitude destination {destination_id} closure range starts at {}, expected packed offset {next_closure}", + range.start + ))); + } + let term_range = range.as_usize_range( + self.closure_terms.len(), + &format!("amplitude destination {destination_id} closure"), + )?; + next_closure = range.end("amplitude-destination closure")?; + for term in &self.closure_terms[term_range] { + if term.target_destination_id != destination_id { + return Err(invalid(format!( + "closure term {} is packed under destination {destination_id} but points to destination {}", + term.id, term.target_destination_id + ))); + } + } + } + if next_closure != closure_count { + return Err(invalid(format!( + "packed amplitude-destination ranges cover {next_closure} of {closure_count} terms" + ))); + } + + for (index, term) in self.closure_terms.iter().enumerate() { + let expected_id = index as u32; + if term.id != expected_id { + return Err(invalid(format!( + "recurrence closure row {index} has non-dense id {}", + term.id + ))); + } + let destination = self + .amplitude_destinations + .get(term.target_destination_id as usize) + .ok_or_else(|| { + invalid(format!( + "closure term {expected_id} references unknown amplitude destination {}", + term.target_destination_id + )) + })?; + if term.exact_factor.is_zero() { + return Err(invalid(format!( + "recurrence closure term {expected_id} has zero exact factor" + ))); + } + for parent_id in term.parent_current_ids.iter().copied() { + if u64::from(parent_id) >= current_count { + return Err(invalid(format!( + "closure term {expected_id} references unknown parent current {parent_id}" + ))); + } + } + if let Some(helicity_id) = destination.target_helicity_id { + let expected = self.resolved_helicities[helicity_id as usize].source_states(); + let actual = closure_parent_source_states(term, &self.currents)?; + if actual != expected { + return Err(invalid(format!( + "closure term {expected_id} ancestry does not match amplitude destination {}", + destination.id + ))); + } + } + } + + validate_program_closure_proofs(self)?; + + Ok(()) + } +} + +fn closure_proofs_from_materialized_terms( + currents: &[RecurrenceCurrent], + amplitude_destinations: &[RecurrenceAmplitudeDestination], + closure_terms: &[RecurrenceClosureTerm], +) -> RusticolResult { + let mut contributions = Vec::with_capacity(closure_terms.len()); + let mut groups = Vec::with_capacity(closure_terms.len()); + for term in closure_terms { + let destination = amplitude_destinations + .get(term.target_destination_id as usize) + .ok_or_else(|| { + invalid(format!( + "closure term {} references absent destination {}", + term.id, term.target_destination_id + )) + })?; + let parent_count = term.parent_current_ids.len(); + let identity_permutation = (0..parent_count) + .map(|index| { + u32::try_from(index).map_err(|_| invalid("closure proof parent index exceeds u32")) + }) + .collect::>>()?; + let mut parent_semantic_digests = Vec::with_capacity(parent_count); + let mut parent_color_digests = Vec::with_capacity(parent_count); + for parent_id in term.parent_current_ids.iter().copied() { + let parent = currents.get(parent_id as usize).ok_or_else(|| { + invalid(format!( + "closure term {} references absent parent {parent_id}", + term.id + )) + })?; + parent_semantic_digests.push(parent.key.catalog_digest()); + parent_color_digests.push(semantic_digest_from_fields( + b"recurrence-closure-parent-color-v2\0", + [parent_id, parent.key.dynamic_lc_color_state_id().get()], + )?); + } + let contribution_id = u32::try_from(contributions.len()) + .map_err(|_| invalid("closure proof contribution count exceeds u32"))?; + contributions.push(ClosureProofContributionV2::new( + contribution_id, + destination.target_sector_id, + Some(destination.id), + destination.target_helicity_id, + term.closure_template_id, + semantic_digest_from_fields( + b"recurrence-closure-template-v2\0", + [term.closure_template_id], + )?, + term.quantum_flow_template_id, + term.parent_current_ids.to_vec(), + term.parent_current_ids.iter().copied().map(Some).collect(), + parent_semantic_digests, + parent_color_digests, + identity_permutation.clone(), + identity_permutation.clone(), + identity_permutation, + u32::MAX, + semantic_digest_from_fields( + b"recurrence-materialized-closure-witness-v2\0", + [term.id], + )?, + None, + vec![], + None, + term.exact_factor, + 1, + )?); + groups.push(ClosureExecutionProofGroupV2::new( + term.id, + Some(term.id), + None, + CheckedTableRange::new(u64::from(contribution_id), 1), + term.exact_factor, + semantic_digest_from_fields( + b"recurrence-materialized-component-factor-v2\0", + [term.closure_template_id], + )?, + semantic_digest_from_fields( + b"recurrence-materialized-selector-domain-v2\0", + [destination.id], + )?, + )?); + } + ClosureProofMetadataV2::new(contributions, groups, Vec::new()) +} + +fn validate_program_closure_proofs(program: &RecurrenceProgram) -> RusticolResult<()> { + validate_closure_proof_tables( + program.closure_proofs.contributions(), + program.closure_proofs.groups(), + program.closure_proofs.reflection_certificates(), + program.closure_proofs.three_line_traversal_certificates(), + )?; + if closure_proof_semantic_completeness_digest_with_three_line_v2( + program.closure_proofs.contributions(), + program.closure_proofs.groups(), + program.closure_proofs.reflection_certificates(), + program.closure_proofs.three_line_traversal_certificates(), + )? != program + .closure_proofs + .expected_semantic_completeness_digest() + { + return Err(invalid( + "recurrence closure proof semantic completeness digest mismatch", + )); + } + + for (index, contribution) in program.closure_proofs.contributions().iter().enumerate() { + if let Some(destination_id) = contribution.target_destination_id { + let destination = program + .amplitude_destinations + .get(destination_id as usize) + .ok_or_else(|| { + invalid(format!( + "closure proof contribution {index} references absent destination {destination_id}" + )) + })?; + if contribution.target_sector_id != destination.target_sector_id + || contribution.target_helicity_id != destination.target_helicity_id + { + return Err(invalid(format!( + "closure proof contribution {index} target does not match destination {}", + destination.id + ))); + } + } + for parent_id in contribution + .construction_parent_runtime_ids + .iter() + .copied() + .flatten() + { + if parent_id as usize >= program.currents.len() { + return Err(invalid(format!( + "closure proof contribution {index} references absent runtime parent current {parent_id}" + ))); + } + } + } + + let mut term_owner = vec![None; program.closure_terms.len()]; + for group in program.closure_proofs.groups() { + if group.emitted_direct_closure_row_id.is_some() { + return Err(invalid(format!( + "program closure proof group {} must not contain a lowered direct row ID", + group.id + ))); + } + let Some(term_id) = group.emitted_runtime_closure_term_id else { + continue; + }; + let term = program.closure_terms.get(term_id as usize).ok_or_else(|| { + invalid(format!( + "closure proof group {} references absent runtime closure term {term_id}", + group.id + )) + })?; + if term_owner[term_id as usize].replace(group.id).is_some() { + return Err(invalid(format!( + "runtime closure term {term_id} is owned by multiple proof groups" + ))); + } + if term.exact_factor != group.exact_summed_factor { + return Err(invalid(format!( + "closure proof group {} factor does not match runtime closure term {term_id}", + group.id + ))); + } + let range = group.contribution_range.as_usize_range( + program.closure_proofs.contributions.len(), + &format!("closure proof group {} contribution", group.id), + )?; + for contribution in &program.closure_proofs.contributions[range] { + if contribution.target_destination_id != Some(term.target_destination_id) + || contribution.closure_template_id != term.closure_template_id + || contribution.quantum_flow_template_id != term.quantum_flow_template_id + || contribution.evaluator_parent_runtime_ids().as_deref() + != Some(term.parent_current_ids.as_ref()) + { + return Err(invalid(format!( + "closure proof group {} does not describe runtime closure term {term_id}", + group.id + ))); + } + } + } + if let Some((term_id, _)) = term_owner + .iter() + .enumerate() + .find(|(_, owner)| owner.is_none()) + { + return Err(invalid(format!( + "runtime closure term {term_id} has no closure proof group" + ))); + } + Ok(()) +} + +fn closure_parent_source_states( + term: &RecurrenceClosureTerm, + currents: &[RecurrenceCurrent], +) -> RusticolResult> { + let mut states = Vec::new(); + for parent_id in term.parent_current_ids.iter().copied() { + let parent = currents + .get(parent_id as usize) + .ok_or_else(|| invalid("closure parent current is absent"))?; + states.extend_from_slice(parent.key.helicity_identity().local_source_states()); + } + states.sort_unstable(); + if states + .windows(2) + .any(|pair| pair[0].source_slot() == pair[1].source_slot()) + { + return Err(invalid("closure parent helicity ancestries overlap")); + } + Ok(states) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::recurrence::{ + CanonicalMomentumLinearForm, CurrentHelicityIdentity, CurrentSourceBinding, + DynamicLCColorStateId, LCColorWitnessTermId, MomentumTerm, SemanticDigest, + SourceStateAssignment, + }; + + fn digest(byte: u8) -> SemanticDigest { + SemanticDigest::new([byte; 32]).unwrap() + } + + fn momentum() -> CanonicalMomentumLinearForm { + CanonicalMomentumLinearForm::new(vec![MomentumTerm { + source_slot: 0, + coefficient: 1, + }]) + .unwrap() + } + + fn source_key() -> CurrentCoreKey { + CurrentCoreKey::new( + digest(1), + RecurrenceNodeKind::Source, + 0, + DynamicLCColorStateId::from_interner(0), + vec![0], + momentum(), + CurrentHelicityIdentity::topology_replay(-1, vec![SourceStateAssignment::new(0, 0)]) + .unwrap(), + vec![], + 0, + vec![], + CurrentSourceBinding::FixedTemplate(0), + None, + ) + .unwrap() + } + + fn propagated_key() -> CurrentCoreKey { + CurrentCoreKey::new( + digest(1), + RecurrenceNodeKind::Current, + 1, + DynamicLCColorStateId::from_interner(0), + vec![0], + momentum(), + CurrentHelicityIdentity::topology_replay(-1, vec![SourceStateAssignment::new(0, 0)]) + .unwrap(), + vec![], + 0, + vec![], + CurrentSourceBinding::None, + Some(0), + ) + .unwrap() + } + + fn contribution(parent_id: u32) -> RecurrenceContribution { + RecurrenceContribution::new( + 0, + 1, + vec![parent_id], + ContributionKey::new( + 0, + vec![0], + vec![0], + vec![momentum()], + 1, + 0, + LCColorWitnessTermId::new(0, 0), + digest(2), + 0, + ) + .unwrap(), + ExactComplexRational::ONE, + ) + .unwrap() + } + + fn resolved_helicity() -> RecurrenceResolvedHelicity { + RecurrenceResolvedHelicity::new(0, vec![SourceStateAssignment::new(0, 0)], vec![-1]) + .unwrap() + } + + fn identity_replay_target() -> RecurrenceReplayTarget { + RecurrenceReplayTarget::new(0, 0, 0, vec![0], vec![1], ExactComplexRational::ONE).unwrap() + } + + fn minus_one() -> ExactComplexRational { + ExactComplexRational::new(ExactRational::new(-1, 1).unwrap(), ExactRational::ZERO) + } + + fn proof_contribution( + id: u32, + parent_id: u32, + witness_id: u32, + witness_digest: SemanticDigest, + reflection_certificate_id: Option, + exact_factor: ExactComplexRational, + ) -> ClosureProofContributionV2 { + proof_contribution_with_runtime_binding( + id, + parent_id, + witness_id, + witness_digest, + reflection_certificate_id, + exact_factor, + true, + ) + } + + #[allow(clippy::too_many_arguments)] + fn proof_contribution_with_runtime_binding( + id: u32, + parent_id: u32, + witness_id: u32, + witness_digest: SemanticDigest, + reflection_certificate_id: Option, + exact_factor: ExactComplexRational, + has_runtime_binding: bool, + ) -> ClosureProofContributionV2 { + ClosureProofContributionV2::new( + id, + 0, + has_runtime_binding.then_some(0), + Some(0), + 0, + digest(10), + None, + vec![parent_id], + vec![has_runtime_binding.then_some(parent_id)], + vec![digest(11)], + vec![digest(12)], + vec![0], + vec![0], + vec![0], + witness_id, + witness_digest, + None, + vec![], + reflection_certificate_id, + exact_factor, + 1, + ) + .unwrap() + } + + fn reflection_certificate(proof_digest: SemanticDigest) -> ReflectionCertificateV1 { + ReflectionCertificateV1::new( + 0, + digest(20), + digest(21), + vec![0], + minus_one(), + false, + 2, + 1, + proof_digest, + ) + .unwrap() + } + + fn three_line_certificate( + id: u32, + sector_id: u32, + pairing_rule_id: u32, + ) -> ThreeLineTraversalCertificateV1 { + let reference_block_order = vec![0, 1, 2]; + let witness_block_order = vec![1, 0, 2]; + let block_permutation = vec![1, 0, 2]; + let reference_source_order = vec![10, 11, 12]; + let witness_source_order = vec![11, 10, 12]; + let source_position_permutation = vec![1, 0, 2]; + let proof_digest = three_line_traversal_proof_digest_v1( + sector_id, + ThreeLineTraversalKindV1::Partner, + 2, + &reference_block_order, + &witness_block_order, + &block_permutation, + &reference_source_order, + &witness_source_order, + &source_position_permutation, + 12, + pairing_rule_id, + ) + .unwrap(); + ThreeLineTraversalCertificateV1::new( + id, + sector_id, + ThreeLineTraversalKindV1::Partner, + 2, + reference_block_order, + witness_block_order, + block_permutation, + reference_source_order, + witness_source_order, + source_position_permutation, + 12, + pairing_rule_id, + proof_digest, + ) + .unwrap() + } + + fn three_line_proof_contribution( + certificate_id: u32, + sector_id: u32, + ) -> ClosureProofContributionV2 { + ClosureProofContributionV2::new( + 0, + sector_id, + Some(0), + Some(0), + 0, + digest(10), + None, + vec![1], + vec![Some(1)], + vec![digest(11)], + vec![digest(12)], + vec![0], + vec![0], + vec![0], + 0, + digest(13), + Some(certificate_id), + vec![], + None, + ExactComplexRational::ONE, + 1, + ) + .unwrap() + } + + fn valid_program() -> RecurrenceProgram { + RecurrenceProgram::new( + RecurrenceStrategy::TopologyReplay, + 1, + 1, + vec![DynamicLCColorState::new(0, None, vec![]).unwrap()], + vec![ + RecurrenceCurrent::new( + 0, + source_key(), + Some(ExactComplexRational::ONE), + CheckedTableRange::new(0, 0), + None, + ) + .unwrap(), + RecurrenceCurrent::new( + 1, + propagated_key(), + None, + CheckedTableRange::new(0, 1), + Some(0), + ) + .unwrap(), + ], + vec![contribution(0)], + vec![RecurrenceFinalization::new(0, 1, Some(0), ExactComplexRational::ONE).unwrap()], + vec![identity_replay_target()], + vec![resolved_helicity()], + vec![ + RecurrenceAmplitudeDestination::new(0, 0, Some(0), CheckedTableRange::new(0, 1)) + .unwrap(), + ], + vec![ + RecurrenceClosureTerm::new(0, 0, 0, None, vec![1], ExactComplexRational::ONE) + .unwrap(), + ], + ) + .unwrap() + } + + #[test] + fn validates_one_compact_recurrence_program() { + let program = valid_program(); + assert_eq!(program.current_range(), CheckedTableRange::new(0, 2)); + assert_eq!(program.contribution_range(), CheckedTableRange::new(0, 1)); + assert_eq!(program.closure_term_range(), CheckedTableRange::new(0, 1)); + assert_eq!( + program.closure_range_for_destination(0), + Some(CheckedTableRange::new(0, 1)) + ); + assert!(program.validate().is_ok()); + } + + #[test] + fn rejects_a_parent_that_does_not_precede_its_result() { + let error = RecurrenceProgram::new( + RecurrenceStrategy::TopologyReplay, + 1, + 1, + vec![DynamicLCColorState::new(0, None, vec![]).unwrap()], + vec![ + RecurrenceCurrent::new( + 0, + source_key(), + Some(ExactComplexRational::ONE), + CheckedTableRange::new(0, 0), + None, + ) + .unwrap(), + RecurrenceCurrent::new( + 1, + propagated_key(), + None, + CheckedTableRange::new(0, 1), + Some(0), + ) + .unwrap(), + ], + vec![contribution(1)], + vec![RecurrenceFinalization::new(0, 1, Some(0), ExactComplexRational::ONE).unwrap()], + vec![identity_replay_target()], + vec![resolved_helicity()], + vec![ + RecurrenceAmplitudeDestination::new(0, 0, Some(0), CheckedTableRange::new(0, 1)) + .unwrap(), + ], + vec![ + RecurrenceClosureTerm::new(0, 0, 0, None, vec![1], ExactComplexRational::ONE) + .unwrap(), + ], + ) + .unwrap_err(); + assert!(error.message().contains("does not precede result current")); + } + + #[test] + fn rejects_a_closure_destination_with_mismatched_source_ancestry() { + let error = RecurrenceProgram::new( + RecurrenceStrategy::TopologyReplay, + 1, + 2, + vec![DynamicLCColorState::new(0, None, vec![]).unwrap()], + vec![ + RecurrenceCurrent::new( + 0, + source_key(), + Some(ExactComplexRational::ONE), + CheckedTableRange::new(0, 0), + None, + ) + .unwrap(), + RecurrenceCurrent::new( + 1, + propagated_key(), + None, + CheckedTableRange::new(0, 1), + Some(0), + ) + .unwrap(), + ], + vec![contribution(0)], + vec![RecurrenceFinalization::new(0, 1, Some(0), ExactComplexRational::ONE).unwrap()], + vec![identity_replay_target()], + vec![ + RecurrenceResolvedHelicity::new(0, vec![SourceStateAssignment::new(0, 1)], vec![1]) + .unwrap(), + ], + vec![ + RecurrenceAmplitudeDestination::new(0, 0, Some(0), CheckedTableRange::new(0, 1)) + .unwrap(), + ], + vec![ + RecurrenceClosureTerm::new(0, 0, 0, None, vec![1], ExactComplexRational::ONE) + .unwrap(), + ], + ) + .unwrap_err(); + assert!(error.message().contains("ancestry does not match")); + } + + #[test] + fn closure_proof_tables_reject_missing_contributions_and_wrong_group_sums() { + let group = ClosureExecutionProofGroupV2::new( + 0, + Some(0), + None, + CheckedTableRange::new(0, 1), + ExactComplexRational::ONE, + digest(30), + digest(31), + ) + .unwrap(); + let missing = ClosureProofMetadataV2::new(vec![], vec![group.clone()], vec![]).unwrap_err(); + assert!(missing.message().contains("exceeds table length")); + + let wrong_sum = ClosureExecutionProofGroupV2::new( + 0, + Some(0), + None, + CheckedTableRange::new(0, 1), + minus_one(), + digest(30), + digest(31), + ) + .unwrap(); + let error = ClosureProofMetadataV2::new( + vec![proof_contribution( + 0, + 1, + 0, + digest(13), + None, + ExactComplexRational::ONE, + )], + vec![wrong_sum], + vec![], + ) + .unwrap_err(); + assert!(error.message().contains("does not equal")); + } + + #[test] + fn closure_proof_tables_reject_a_stale_candidate_domain_certificate() { + let contribution = proof_contribution(0, 1, 0, digest(13), None, ExactComplexRational::ONE); + let group = ClosureExecutionProofGroupV2::new( + 0, + Some(0), + None, + CheckedTableRange::new(0, 1), + ExactComplexRational::ONE, + digest(30), + digest(31), + ) + .unwrap(); + let metadata = + ClosureProofMetadataV2::new(vec![contribution.clone()], vec![group.clone()], vec![]) + .unwrap(); + let certificate = metadata.candidate_domain_certificate(); + let stale = ClosureCandidateDomainCertificateV1::new( + certificate.accepted_candidate_count() + 1, + certificate.accepted_candidate_digest(), + ); + let error = ClosureProofMetadataV2::new_with_three_line_certificates_and_candidate_domain( + vec![contribution], + vec![group], + vec![], + vec![], + stale, + ) + .unwrap_err(); + assert!(error.message().contains("candidate-domain certificate")); + } + + #[test] + fn closure_proof_digest_rejects_parent_witness_proof_and_certificate_mutations() { + let group = ClosureExecutionProofGroupV2::new( + 0, + Some(0), + None, + CheckedTableRange::new(0, 1), + ExactComplexRational::ONE, + digest(30), + digest(31), + ) + .unwrap(); + let original = ClosureProofMetadataV2::new( + vec![proof_contribution( + 0, + 1, + 7, + digest(13), + Some(0), + ExactComplexRational::ONE, + )], + vec![group.clone()], + vec![reflection_certificate(digest(22))], + ) + .unwrap(); + let expected = original.expected_semantic_completeness_digest(); + + for contribution in [ + proof_contribution(0, 2, 7, digest(13), Some(0), ExactComplexRational::ONE), + proof_contribution(0, 1, 8, digest(13), Some(0), ExactComplexRational::ONE), + proof_contribution(0, 1, 7, digest(14), Some(0), ExactComplexRational::ONE), + ] { + let error = ClosureProofMetadataV2::new_with_expected_digest( + vec![contribution], + vec![group.clone()], + vec![reflection_certificate(digest(22))], + expected, + ) + .unwrap_err(); + assert!(error.message().contains("digest mismatch")); + } + + let error = ClosureProofMetadataV2::new_with_expected_digest( + original.contributions().to_vec(), + original.groups().to_vec(), + vec![reflection_certificate(digest(23))], + expected, + ) + .unwrap_err(); + assert!(error.message().contains("digest mismatch")); + } + + #[test] + fn closure_proof_tables_retain_exact_zero_groups_without_runtime_rows() { + let metadata = ClosureProofMetadataV2::new( + vec![ + proof_contribution_with_runtime_binding( + 0, + 91, + 0, + digest(13), + None, + ExactComplexRational::ONE, + false, + ), + proof_contribution_with_runtime_binding( + 1, + 91, + 0, + digest(13), + None, + minus_one(), + false, + ), + ], + vec![ + ClosureExecutionProofGroupV2::new( + 0, + None, + None, + CheckedTableRange::new(0, 2), + ExactComplexRational::ZERO, + digest(30), + digest(31), + ) + .unwrap(), + ], + vec![], + ) + .unwrap(); + assert_eq!(metadata.groups().len(), 1); + assert!(metadata.groups()[0].exact_summed_factor().is_zero()); + assert_eq!(metadata.groups()[0].emitted_runtime_closure_term_id(), None); + assert_eq!(metadata.groups()[0].emitted_direct_closure_row_id(), None); + } + + #[test] + fn closure_proof_tables_reject_nonzero_groups_without_runtime_bindings() { + let contribution = proof_contribution_with_runtime_binding( + 0, + 91, + 0, + digest(13), + None, + ExactComplexRational::ONE, + false, + ); + let group = ClosureExecutionProofGroupV2::new( + 0, + Some(0), + None, + CheckedTableRange::new(0, 1), + ExactComplexRational::ONE, + digest(30), + digest(31), + ) + .unwrap(); + let error = + ClosureProofMetadataV2::new(vec![contribution], vec![group], vec![]).unwrap_err(); + assert!(error.message().contains("lacks a complete runtime binding")); + } + + #[test] + fn three_line_traversal_certificate_authenticates_orders_anchor_and_kind() { + let certificate = three_line_certificate(0, 4, 7); + assert_eq!(certificate.kind(), ThreeLineTraversalKindV1::Partner); + assert_eq!(certificate.sink_block_ordinal(), 2); + assert_eq!(certificate.closure_anchor_source_slot(), 12); + assert!(ThreeLineTraversalKindV1::try_from(2).is_err()); + + let error = ThreeLineTraversalCertificateV1::new( + 0, + 4, + ThreeLineTraversalKindV1::Partner, + 2, + vec![0, 2], + vec![1, 0, 2], + vec![1, 0, 2], + vec![10, 11, 12], + vec![11, 10, 12], + vec![1, 0, 2], + 12, + 7, + digest(60), + ) + .unwrap_err(); + assert!(error.message().contains("expected 3")); + + let error = ThreeLineTraversalCertificateV1::new( + 0, + 4, + ThreeLineTraversalKindV1::Partner, + 2, + vec![0, 1, 2], + vec![1, 0, 2], + vec![1, 0, 2], + vec![10, 11, 12], + vec![11, 10, 12], + vec![0, 1, 2], + 12, + 7, + digest(60), + ) + .unwrap_err(); + assert!(error.message().contains("source-position permutation")); + + let original = three_line_certificate(0, 4, 7); + let error = ThreeLineTraversalCertificateV1::new( + original.id(), + original.sector_id(), + original.kind(), + original.sink_block_ordinal(), + original.reference_block_order().to_vec(), + original.witness_block_order().to_vec(), + original.block_permutation().to_vec(), + original.reference_source_order().to_vec(), + original.witness_source_order().to_vec(), + original.source_position_permutation().to_vec(), + original.closure_anchor_source_slot(), + original.pairing_rule_id(), + digest(61), + ) + .unwrap_err(); + assert!(error.message().contains("proof digest mismatch")); + } + + #[test] + fn closure_proofs_authenticate_three_line_catalog_and_references() { + let group = ClosureExecutionProofGroupV2::new( + 0, + Some(0), + None, + CheckedTableRange::new(0, 1), + ExactComplexRational::ONE, + digest(30), + digest(31), + ) + .unwrap(); + let original = ClosureProofMetadataV2::new_with_three_line_certificates( + vec![three_line_proof_contribution(0, 4)], + vec![group.clone()], + vec![], + vec![three_line_certificate(0, 4, 7)], + ) + .unwrap(); + let expected = original.expected_semantic_completeness_digest(); + + let error = ClosureProofMetadataV2::new_with_three_line_certificates( + original.contributions().to_vec(), + original.groups().to_vec(), + vec![], + vec![], + ) + .unwrap_err(); + assert!(error.message().contains("absent three-line")); + + let error = ClosureProofMetadataV2::new_with_three_line_certificates( + vec![three_line_proof_contribution(1, 4)], + vec![group.clone()], + vec![], + vec![three_line_certificate(1, 4, 7)], + ) + .unwrap_err(); + assert!(error.message().contains("non-dense id")); + + let error = ClosureProofMetadataV2::new_with_three_line_certificates( + vec![three_line_proof_contribution(0, 5)], + vec![group.clone()], + vec![], + vec![three_line_certificate(0, 4, 7)], + ) + .unwrap_err(); + assert!(error.message().contains("does not match")); + + let error = ClosureProofMetadataV2::new_with_three_line_certificates_and_expected_digest( + original.contributions().to_vec(), + original.groups().to_vec(), + vec![], + vec![three_line_certificate(0, 4, 8)], + expected, + ) + .unwrap_err(); + assert!(error.message().contains("digest mismatch")); + } +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/recurrence/relation.rs b/pyamplicol/rust/crates/rusticol-core/src/recurrence/relation.rs new file mode 100644 index 0000000000..780980fed1 --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/recurrence/relation.rs @@ -0,0 +1,1124 @@ +// SPDX-License-Identifier: 0BSD + +//! Opt-in numerical nomination and exact replay for recurrence-current reuse. +//! +//! Numerical projections are deliberately only an index. A current is +//! reusable only when the complete lowered contribution vector is +//! proportional over [`ExactComplexRational`]. The resulting certificate is +//! replayed once more before lowering may emit a scale-copy row. + +use std::collections::BTreeMap; + +use serde_json::{Value as JsonValue, json}; +use sha2::{Digest, Sha256}; + +use super::{ + DIRECT_NONE_U32, DirectExecutorRole, DirectNodeKind, DirectRecurrencePlan, + ExactComplexRational, RecurrenceStrategy, +}; +use crate::{RusticolError, RusticolResult}; + +const PROJECTION_PRIME: u64 = (1_u64 << 61) - 1; +const RELATION_CERTIFICATE_ALGORITHM: &str = "recurrence-exact-rational-row-vector-replay-v1"; +pub const NUMERICAL_RELATION_CERTIFICATE_ALGORITHM: &str = + "authenticated-independent-recursive-decimal-raw-probes-v2"; +pub const RECURRENCE_NUMERICAL_SOURCE_ABI: &str = + "pyamplicol-recurrence-numerical-current-source-v3"; + +fn invalid(message: impl Into) -> RusticolError { + RusticolError::invalid_argument(format!("recurrence relation discovery: {}", message.into())) +} + +/// User-visible recurrence relation-discovery mode. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum RecurrenceRelationDiscoveryMode { + Off, + Diagnostic, + CertifiedReuse, +} + +impl RecurrenceRelationDiscoveryMode { + pub fn parse(value: &str) -> RusticolResult { + match value { + "off" => Ok(Self::Off), + "diagnostic" => Ok(Self::Diagnostic), + "certified-reuse" => Ok(Self::CertifiedReuse), + _ => Err(invalid(format!("unsupported mode {value:?}"))), + } + } + + pub const fn as_str(self) -> &'static str { + match self { + Self::Off => "off", + Self::Diagnostic => "diagnostic", + Self::CertifiedReuse => "certified-reuse", + } + } + + pub const fn applies_reuse(self) -> bool { + matches!(self, Self::CertifiedReuse) + } +} + +/// Authenticated options passed through the recurrence lowering boundary. +#[derive(Clone, Debug, PartialEq)] +pub struct RecurrenceRelationDiscoveryOptions { + pub mode: RecurrenceRelationDiscoveryMode, + pub precision_digits: u32, + pub probe_count: u32, + pub verification_probe_count: u32, + pub relative_tolerance: f64, + pub absolute_tolerance: f64, + pub seed: u64, + pub color_accuracy: String, + pub numerical_evidence: Option, +} + +impl RecurrenceRelationDiscoveryOptions { + #[allow(clippy::too_many_arguments)] + pub fn new( + mode: RecurrenceRelationDiscoveryMode, + precision_digits: u32, + probe_count: u32, + verification_probe_count: u32, + relative_tolerance: f64, + absolute_tolerance: f64, + seed: u64, + color_accuracy: impl Into, + ) -> RusticolResult { + if precision_digits < 80 { + return Err(invalid("precision must be at least 80 decimal digits")); + } + if probe_count < 2 { + return Err(invalid("at least two probes are required")); + } + if verification_probe_count < 2 { + return Err(invalid("at least two verification probes are required")); + } + if !relative_tolerance.is_finite() + || relative_tolerance < 0.0 + || !absolute_tolerance.is_finite() + || absolute_tolerance < 0.0 + || (relative_tolerance == 0.0 && absolute_tolerance == 0.0) + { + return Err(invalid( + "relative and absolute tolerances must be finite and nonnegative, and at least one must be positive", + )); + } + let color_accuracy = color_accuracy.into(); + if !matches!(color_accuracy.as_str(), "lc" | "nlc" | "full") { + return Err(invalid(format!( + "unsupported color accuracy {color_accuracy:?}" + ))); + } + Ok(Self { + mode, + precision_digits, + probe_count, + verification_probe_count, + relative_tolerance, + absolute_tolerance, + seed, + color_accuracy, + numerical_evidence: None, + }) + } + + pub fn with_numerical_evidence( + mut self, + evidence: RecurrenceNumericalRelationEvidence, + ) -> RusticolResult { + if self.mode == RecurrenceRelationDiscoveryMode::Off { + return Err(invalid("off mode cannot carry numerical relation evidence")); + } + if evidence.requested_mode != self.mode { + return Err(invalid( + "numerical evidence mode disagrees with lowering options", + )); + } + if evidence.certificate_algorithm != NUMERICAL_RELATION_CERTIFICATE_ALGORITHM { + return Err(invalid( + "numerical evidence uses an unsupported certificate algorithm", + )); + } + if evidence.precision_digits != self.precision_digits + || evidence.probe_count != self.probe_count + || evidence.verification_probe_count != self.verification_probe_count + || evidence.relative_tolerance.to_bits() != self.relative_tolerance.to_bits() + || evidence.absolute_tolerance.to_bits() != self.absolute_tolerance.to_bits() + || evidence.seed != self.seed + { + return Err(invalid( + "numerical evidence probe contract disagrees with lowering options", + )); + } + let expected_candidate_count = evidence + .mappings + .len() + .checked_add(evidence.verification_rejected_count) + .ok_or_else(|| invalid("numerical evidence candidate count overflows usize"))?; + let expected_rejected_count = evidence + .tested_hypothesis_count + .checked_sub(evidence.mappings.len()) + .ok_or_else(|| invalid("numerical evidence rejected-hypothesis count underflows"))?; + if evidence.numerical_candidate_count != expected_candidate_count + || evidence.tested_hypothesis_count < evidence.numerical_candidate_count + || evidence.rejected_hypothesis_count != expected_rejected_count + { + return Err(invalid( + "numerical evidence candidate counters are inconsistent", + )); + } + self.numerical_evidence = Some(evidence); + Ok(self) + } + + pub fn off() -> Self { + Self { + mode: RecurrenceRelationDiscoveryMode::Off, + precision_digits: 96, + probe_count: 4, + verification_probe_count: 4, + relative_tolerance: 1.0e-70, + absolute_tolerance: 1.0e-80, + seed: 0x5059_414d, + color_accuracy: "lc".to_owned(), + numerical_evidence: None, + } + } +} + +/// One Python-authenticated exact ±1/zero scale mapping. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct RecurrenceNumericalCurrentMapping { + pub current_id: u32, + pub representative_id: Option, + pub execution_representative_id: u32, + pub relation_kind: String, + pub factor: ExactComplexRational, + pub current_dimension: u16, + pub certificate_proof_sha256: String, + pub candidate_observations_sha256: String, + pub verification_observations_sha256: String, +} + +/// Canonical evidence supplied only after an unpublished exact baseline pass. +#[derive(Clone, Debug, PartialEq)] +pub struct RecurrenceNumericalRelationEvidence { + pub requested_mode: RecurrenceRelationDiscoveryMode, + pub schedule_semantic_digest: String, + pub baseline_runtime_layout_digest: String, + pub source_semantics_sha256: String, + pub runtime_parameter_schema_sha256: String, + pub candidate_observation_batch_sha256: String, + pub verification_observation_batch_sha256: String, + pub decision_sha256: String, + pub rejection_decision_sha256: String, + pub certificate_algorithm: String, + pub certificate_set_sha256: String, + pub precision_digits: u32, + pub probe_count: u32, + pub verification_probe_count: u32, + pub relative_tolerance: f64, + pub absolute_tolerance: f64, + pub seed: u64, + pub numerical_candidate_count: usize, + pub verification_rejected_count: usize, + pub rejected_hypothesis_count: usize, + pub tested_hypothesis_count: usize, + pub mappings: Vec, +} + +/// Recompute the source contract consumed by the exact Python probe pass. +/// +/// This deliberately binds the baseline physical layout as well as every +/// current comparison contract. The numerical evidence is rejected unless +/// its claimed digest is byte-for-byte identical to this native result. +pub fn recurrence_numerical_source_semantics_sha256( + plan: &DirectRecurrencePlan, + process_id: &str, +) -> RusticolResult { + let mut finalization_executors = BTreeMap::::new(); + for group in plan + .row_groups() + .iter() + .filter(|group| group.role == DirectExecutorRole::Finalization) + { + let start = u32::try_from(group.row_start) + .map_err(|_| invalid("finalization row-group start exceeds u32"))?; + let stop = start + .checked_add(group.row_count) + .ok_or_else(|| invalid("finalization row-group range overflows u32"))?; + for row_id in start..stop { + if finalization_executors + .insert(row_id, group.direct_executor_id) + .is_some() + { + return Err(invalid( + "baseline finalization row belongs to multiple executor groups", + )); + } + } + } + + let currents = plan + .currents() + .iter() + .map(|current| { + let (executor_id, momentum_form_id, factor) = if current.finalization_row_or_sentinel + == DIRECT_NONE_U32 + { + ( + DIRECT_NONE_U32, + DIRECT_NONE_U32, + [ + "1".to_owned(), + "1".to_owned(), + "0".to_owned(), + "1".to_owned(), + ], + ) + } else { + let row = plan + .finalizations() + .get(current.finalization_row_or_sentinel as usize) + .ok_or_else(|| invalid("baseline current finalization row is absent"))?; + let executor_id = *finalization_executors + .get(¤t.finalization_row_or_sentinel) + .ok_or_else(|| invalid("baseline current finalization executor is absent"))?; + let factor = plan + .exact_factors() + .get(row.exact_factor_id as usize) + .ok_or_else(|| invalid("baseline finalization factor is absent"))?; + ( + executor_id, + row.momentum_form_id, + [ + factor.real().numerator().to_string(), + factor.real().denominator().to_string(), + factor.imag().numerator().to_string(), + factor.imag().denominator().to_string(), + ], + ) + }; + Ok(json!({ + "current_id": current.semantic_current_id, + "is_source": current.node_kind == DirectNodeKind::Source, + "contract": [ + current.node_kind as u16, + current.stage, + current.state_template_id, + current.component_count, + current.momentum_form_id, + current.selector_domain_id, + executor_id, + momentum_form_id, + factor, + ], + })) + }) + .collect::>>()?; + let selector_schedule = match plan.strategy() { + RecurrenceStrategy::TopologyReplay => json!({ + "policy": "seeded-replay-target-per-physical-point-v1", + "replay_targets": plan + .replay_targets() + .iter() + .enumerate() + .map(|(target_index, target)| json!({ + "target_index": target_index, + "public_flow_id": target.public_flow_id, + "representative_id": target.representative_id, + "selector_domain_id": target.selector_domain_id, + })) + .collect::>(), + }), + RecurrenceStrategy::AllFlowUnion => json!({ + "policy": "seeded-helicity-per-selector-domain-and-physical-point-v1", + "resolved_helicities": plan + .resolved_helicities() + .iter() + .enumerate() + .map(|(helicity_index, helicity)| json!({ + "helicity_index": helicity_index, + "helicity_id": helicity.id, + "selector_domain_id": helicity.selector_domain_id, + })) + .collect::>(), + }), + RecurrenceStrategy::ContractedColorUnion => json!({ + "policy": "fixed-contracted-source-schedule-v1", + "fixed_source_schedule": true, + }), + }; + let payload = json!({ + "abi": RECURRENCE_NUMERICAL_SOURCE_ABI, + "process_id": process_id, + "strategy": plan.strategy().as_str(), + "schedule_semantic_digest": plan.semantic_digest().to_string(), + "baseline_runtime_layout_digest": plan.runtime_layout_digest().to_string(), + "selector_schedule": selector_schedule, + "currents": currents, + }); + let bytes = serde_json::to_vec(&payload) + .map_err(|error| invalid(format!("could not canonicalize source semantics: {error}")))?; + Ok(format!("{:x}", Sha256::digest(bytes))) +} + +pub fn authenticate_recurrence_numerical_relation_provenance( + evidence: &RecurrenceNumericalRelationEvidence, + baseline_plan: &DirectRecurrencePlan, + process_id: &str, +) -> RusticolResult<()> { + if evidence.baseline_runtime_layout_digest != baseline_plan.runtime_layout_digest().to_string() + { + return Err(RusticolError::integrity( + "numerical relation evidence baseline runtime-layout digest does not match the native baseline", + )); + } + let expected_source_semantics = + recurrence_numerical_source_semantics_sha256(baseline_plan, process_id)?; + if evidence.source_semantics_sha256 != expected_source_semantics { + return Err(RusticolError::integrity( + "numerical relation evidence source-semantics digest does not match the native baseline", + )); + } + Ok(()) +} + +/// Runtime contract which must agree before two current values are comparable. +#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] +pub(crate) struct RelationCurrentContract { + pub stage: u16, + pub state_template_id: u32, + pub component_count: u16, + pub momentum_form_id: u32, + pub selector_domain_id: u32, + pub finalization_executor_id: u32, + pub finalization_momentum_form_id: u32, + pub finalization_exact_factor: ExactComplexRational, +} + +/// One exact lowered contribution before parent relation substitution. +#[derive(Clone, Debug, Eq, PartialEq)] +pub(crate) struct RelationTermInput { + pub executor_id: u32, + pub parent_current_ids: Box<[u32]>, + pub parent_momentum_form_ids: Box<[u32]>, + pub exact_factor: ExactComplexRational, +} + +/// Exact schedule input for one current in topological ID order. +#[derive(Clone, Debug, Eq, PartialEq)] +pub(crate) struct RelationCurrentInput { + pub current_id: u32, + pub is_source: bool, + pub contract: RelationCurrentContract, + pub terms: Box<[RelationTermInput]>, +} + +#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] +struct RelationTermKey { + executor_id: u32, + parent_class_ids: Box<[u32]>, + parent_momentum_form_ids: Box<[u32]>, +} + +type ExactTermVector = BTreeMap; + +/// Exact, independently replayable relation between two lowered currents. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct RecurrenceCurrentRelationCertificate { + pub current_id: u32, + pub representative_id: u32, + pub factor: ExactComplexRational, + pub current_expression_sha256: String, + pub representative_expression_sha256: String, + pub proof_sha256: String, +} + +/// Manifest-facing result of one recurrence relation-discovery pass. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct RecurrenceRelationDiscoveryReport { + pub requested_mode: RecurrenceRelationDiscoveryMode, + pub state: &'static str, + pub strategy: RecurrenceStrategy, + pub color_accuracy: String, + pub precision_digits: u32, + pub probe_count: u32, + pub verification_probe_count: u32, + pub seed: u64, + pub certificate_algorithm: String, + pub authenticated_certificate_set_sha256: Option, + pub authenticated_runtime_parameter_schema_sha256: Option, + pub authenticated_candidate_observation_batch_sha256: Option, + pub authenticated_verification_observation_batch_sha256: Option, + pub authenticated_decision_sha256: Option, + pub rejected_decision_sha256: String, + pub tested_hypothesis_count: usize, + pub verification_rejected_count: usize, + pub rejected_hypothesis_count: usize, + pub effective_projection_count: u32, + pub numerical_candidate_count: usize, + pub uncertified_candidate_count: usize, + pub exact_certified_relation_count: usize, + pub applied_relation_count: usize, + pub current_count_before: usize, + pub current_count_after: usize, + pub contribution_count_before: usize, + pub contribution_count_after: usize, + pub interaction_evaluation_count_before: usize, + pub interaction_evaluation_count_after: usize, + pub scale_copy_row_count: usize, + pub certificates: Vec, + pub rejected_candidates: Vec, +} + +#[derive(Clone, Debug)] +pub(crate) struct RecurrenceRelationDiscoveryOutcome { + pub relations: Vec, + pub numerical_candidate_count: usize, + pub uncertified_candidate_count: usize, + pub rejected_hypothesis_count: usize, + pub rejected_candidates: Vec, + pub rejected_decision_sha256: String, + pub effective_projection_count: u32, +} + +#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] +struct Fp2 { + re: u64, + im: u64, +} + +impl Fp2 { + const ZERO: Self = Self { re: 0, im: 0 }; + + fn add(self, right: Self) -> Self { + Self { + re: add_mod(self.re, right.re), + im: add_mod(self.im, right.im), + } + } + + fn mul(self, right: Self) -> Self { + Self { + re: sub_mod(mul_mod(self.re, right.re), mul_mod(self.im, right.im)), + im: add_mod(mul_mod(self.re, right.im), mul_mod(self.im, right.re)), + } + } + + fn inverse(self) -> Option { + let norm = add_mod(mul_mod(self.re, self.re), mul_mod(self.im, self.im)); + if norm == 0 { + return None; + } + let inverse = pow_mod(norm, PROJECTION_PRIME - 2); + Some(Self { + re: mul_mod(self.re, inverse), + im: sub_mod(0, mul_mod(self.im, inverse)), + }) + } +} + +/// Discover recurrence-current relations in topological order. +/// +/// The finite-field fingerprint nominates candidates with a collision bound +/// controlled by `precision_digits` and `probe_count`. It is never accepted +/// as proof: `exact_relation_factor` checks every exact coefficient, and +/// `replay_certificate` repeats that check from the stored vectors. +pub(crate) fn discover_recurrence_current_relations( + inputs: &[RelationCurrentInput], + options: &RecurrenceRelationDiscoveryOptions, +) -> RusticolResult { + let rejection_root = format!( + "{:x}", + Sha256::digest( + serde_json::to_vec(&json!({ + "abi": "rusticol-recurrence-rejected-decision-chain-v1", + "precision_digits": options.precision_digits, + "probe_count": options.probe_count, + "seed": options.seed, + })) + .map_err(|error| invalid(format!("could not encode rejection root: {error}")))?, + ) + ); + if options.mode == RecurrenceRelationDiscoveryMode::Off { + return Ok(RecurrenceRelationDiscoveryOutcome { + relations: Vec::new(), + numerical_candidate_count: 0, + uncertified_candidate_count: 0, + rejected_hypothesis_count: 0, + rejected_candidates: Vec::new(), + rejected_decision_sha256: rejection_root, + effective_projection_count: 0, + }); + } + for (index, input) in inputs.iter().enumerate() { + if input.current_id != index as u32 { + return Err(invalid(format!( + "current {index} has non-canonical ID {}", + input.current_id + ))); + } + } + + // Four bits per requested decimal digit is conservative relative to + // log2(10). Each independent projection lives in F_(2^61-1)^2. + let precision_projection_count = options.precision_digits.saturating_mul(4).div_ceil(61); + let effective_projection_count = options.probe_count.max(precision_projection_count).max(2); + + let mut class_by_current = Vec::::with_capacity(inputs.len()); + let mut factor_by_current = Vec::::with_capacity(inputs.len()); + let mut vector_by_current = Vec::::with_capacity(inputs.len()); + let mut groups = BTreeMap::<(RelationCurrentContract, Vec), Vec>::new(); + let mut relations = Vec::new(); + let mut numerical_candidate_count = 0usize; + let mut uncertified_candidate_count = 0usize; + let mut rejected_candidates = Vec::new(); + let mut rejected_decision_sha256 = rejection_root; + + for input in inputs { + if input.is_source { + class_by_current.push(input.current_id); + factor_by_current.push(ExactComplexRational::ONE); + vector_by_current.push(BTreeMap::new()); + continue; + } + let vector = exact_term_vector(input, &class_by_current, &factor_by_current)?; + let Some(fingerprint) = + numerical_fingerprint(&vector, options.seed, effective_projection_count)? + else { + class_by_current.push(input.current_id); + factor_by_current.push(ExactComplexRational::ONE); + vector_by_current.push(vector); + continue; + }; + + let key = (input.contract.clone(), fingerprint); + let mut relation = None; + if let Some(representatives) = groups.get(&key) { + for representative_id in representatives { + numerical_candidate_count = numerical_candidate_count + .checked_add(1) + .ok_or_else(|| invalid("candidate count overflows usize"))?; + let representative = vector_by_current + .get(*representative_id as usize) + .ok_or_else(|| invalid("candidate representative is absent"))?; + match exact_relation_factor(&vector, representative)? { + Some(factor) => { + let certificate = build_certificate( + input.current_id, + *representative_id, + factor, + &vector, + representative, + ); + if replay_certificate(&certificate, &vector, representative)? { + relation = Some(certificate); + break; + } + uncertified_candidate_count += 1; + let reason = format!( + "current {} and representative {} failed exact certificate replay", + input.current_id, representative_id + ); + let mut digest = Sha256::new(); + digest.update(rejected_decision_sha256.as_bytes()); + digest.update(reason.as_bytes()); + rejected_decision_sha256 = format!("{:x}", digest.finalize()); + if rejected_candidates.len() < 16 { + rejected_candidates.push(reason); + } + } + None => { + uncertified_candidate_count += 1; + let reason = format!( + "current {} and representative {} collided numerically without an exact relation", + input.current_id, representative_id + ); + let mut digest = Sha256::new(); + digest.update(rejected_decision_sha256.as_bytes()); + digest.update(reason.as_bytes()); + rejected_decision_sha256 = format!("{:x}", digest.finalize()); + if rejected_candidates.len() < 16 { + rejected_candidates.push(reason); + } + } + } + } + } + if let Some(certificate) = relation { + let representative_id = certificate.representative_id; + class_by_current.push(class_by_current[representative_id as usize]); + factor_by_current.push(certificate.factor); + relations.push(certificate); + } else { + class_by_current.push(input.current_id); + factor_by_current.push(ExactComplexRational::ONE); + groups.entry(key).or_default().push(input.current_id); + } + vector_by_current.push(vector); + } + + Ok(RecurrenceRelationDiscoveryOutcome { + relations, + numerical_candidate_count, + uncertified_candidate_count, + rejected_hypothesis_count: uncertified_candidate_count, + rejected_candidates, + rejected_decision_sha256, + effective_projection_count, + }) +} + +fn exact_term_vector( + input: &RelationCurrentInput, + class_by_current: &[u32], + factor_by_current: &[ExactComplexRational], +) -> RusticolResult { + let mut vector = BTreeMap::::new(); + for term in input.terms.iter() { + if term.parent_current_ids.len() != term.parent_momentum_form_ids.len() + || term.parent_current_ids.is_empty() + || term.parent_current_ids.len() > 2 + { + return Err(invalid(format!( + "current {} has an invalid relation term arity", + input.current_id + ))); + } + let mut coefficient = term.exact_factor; + let mut parent_class_ids = Vec::with_capacity(term.parent_current_ids.len()); + for parent_id in term.parent_current_ids.iter().copied() { + if parent_id >= input.current_id { + return Err(invalid(format!( + "current {} relation term references non-topological parent {parent_id}", + input.current_id + ))); + } + parent_class_ids.push( + *class_by_current + .get(parent_id as usize) + .ok_or_else(|| invalid("relation parent class is absent"))?, + ); + coefficient = coefficient.checked_mul( + *factor_by_current + .get(parent_id as usize) + .ok_or_else(|| invalid("relation parent factor is absent"))?, + )?; + } + let key = RelationTermKey { + executor_id: term.executor_id, + parent_class_ids: parent_class_ids.into_boxed_slice(), + parent_momentum_form_ids: term.parent_momentum_form_ids.clone(), + }; + let combined = vector + .get(&key) + .copied() + .unwrap_or(ExactComplexRational::ZERO) + .checked_add(coefficient)?; + if combined.is_zero() { + vector.remove(&key); + } else { + vector.insert(key, combined); + } + } + Ok(vector) +} + +fn numerical_fingerprint( + vector: &ExactTermVector, + seed: u64, + projection_count: u32, +) -> RusticolResult>> { + if vector.is_empty() { + return Ok(None); + } + let mut values = Vec::with_capacity(projection_count as usize); + for projection in 0..projection_count { + let mut value = Fp2::ZERO; + for (key, coefficient) in vector { + value = + value.add(exact_to_fp2(*coefficient)?.mul(term_projection(seed, projection, key))); + } + values.push(value); + } + let Some(pivot) = values.iter().copied().find(|value| *value != Fp2::ZERO) else { + return Ok(None); + }; + let Some(inverse) = pivot.inverse() else { + return Ok(None); + }; + Ok(Some( + values.into_iter().map(|value| value.mul(inverse)).collect(), + )) +} + +fn term_projection(seed: u64, projection: u32, key: &RelationTermKey) -> Fp2 { + let mut hash = Sha256::new(); + hash.update(b"pyamplicol-recurrence-relation-probe-v1"); + hash.update(seed.to_le_bytes()); + hash.update(projection.to_le_bytes()); + hash.update(key.executor_id.to_le_bytes()); + hash.update((key.parent_class_ids.len() as u64).to_le_bytes()); + for value in key.parent_class_ids.iter() { + hash.update(value.to_le_bytes()); + } + for value in key.parent_momentum_form_ids.iter() { + hash.update(value.to_le_bytes()); + } + let digest = hash.finalize(); + let re = u64::from_le_bytes(digest[0..8].try_into().expect("SHA slice")); + let im = u64::from_le_bytes(digest[8..16].try_into().expect("SHA slice")); + Fp2 { + re: re % PROJECTION_PRIME, + im: im % PROJECTION_PRIME, + } +} + +fn exact_to_fp2(value: ExactComplexRational) -> RusticolResult { + Ok(Fp2 { + re: rational_to_field(value.real())?, + im: rational_to_field(value.imag())?, + }) +} + +fn rational_to_field(value: super::ExactRational) -> RusticolResult { + let numerator = field_residue(value.numerator()); + let denominator = field_residue(value.denominator()); + if denominator == 0 { + return Err(invalid( + "an exact denominator is zero in the numerical projection field", + )); + } + Ok(mul_mod( + numerator, + pow_mod(denominator, PROJECTION_PRIME - 2), + )) +} + +fn field_residue(value: i128) -> u64 { + value.rem_euclid(i128::from(PROJECTION_PRIME)) as u64 +} + +fn exact_relation_factor( + current: &ExactTermVector, + representative: &ExactTermVector, +) -> RusticolResult> { + if current.len() != representative.len() || current.is_empty() { + return Ok(None); + } + let Some(( + (current_key, current_coefficient), + (representative_key, representative_coefficient), + )) = current + .first_key_value() + .zip(representative.first_key_value()) + else { + return Ok(None); + }; + if current_key != representative_key { + return Ok(None); + } + let factor = current_coefficient.checked_div(*representative_coefficient)?; + if factor.is_zero() { + return Ok(None); + } + for ((current_key, current_coefficient), (representative_key, representative_coefficient)) in + current.iter().zip(representative) + { + if current_key != representative_key + || *current_coefficient != representative_coefficient.checked_mul(factor)? + { + return Ok(None); + } + } + Ok(Some(factor)) +} + +fn build_certificate( + current_id: u32, + representative_id: u32, + factor: ExactComplexRational, + current: &ExactTermVector, + representative: &ExactTermVector, +) -> RecurrenceCurrentRelationCertificate { + let current_expression_sha256 = expression_digest(current); + let representative_expression_sha256 = expression_digest(representative); + let proof_sha256 = proof_digest( + current_id, + representative_id, + factor, + ¤t_expression_sha256, + &representative_expression_sha256, + ); + RecurrenceCurrentRelationCertificate { + current_id, + representative_id, + factor, + current_expression_sha256, + representative_expression_sha256, + proof_sha256, + } +} + +fn replay_certificate( + certificate: &RecurrenceCurrentRelationCertificate, + current: &ExactTermVector, + representative: &ExactTermVector, +) -> RusticolResult { + Ok( + exact_relation_factor(current, representative)? == Some(certificate.factor) + && expression_digest(current) == certificate.current_expression_sha256 + && expression_digest(representative) == certificate.representative_expression_sha256 + && proof_digest( + certificate.current_id, + certificate.representative_id, + certificate.factor, + &certificate.current_expression_sha256, + &certificate.representative_expression_sha256, + ) == certificate.proof_sha256, + ) +} + +fn expression_digest(vector: &ExactTermVector) -> String { + let mut hash = Sha256::new(); + hash.update(b"pyamplicol-recurrence-exact-row-vector-v1"); + hash.update((vector.len() as u64).to_le_bytes()); + for (key, coefficient) in vector { + hash.update(key.executor_id.to_le_bytes()); + hash.update((key.parent_class_ids.len() as u64).to_le_bytes()); + for value in key.parent_class_ids.iter() { + hash.update(value.to_le_bytes()); + } + for value in key.parent_momentum_form_ids.iter() { + hash.update(value.to_le_bytes()); + } + update_exact_hash(&mut hash, *coefficient); + } + format!("{:x}", hash.finalize()) +} + +fn proof_digest( + current_id: u32, + representative_id: u32, + factor: ExactComplexRational, + current_digest: &str, + representative_digest: &str, +) -> String { + let mut hash = Sha256::new(); + hash.update(RELATION_CERTIFICATE_ALGORITHM.as_bytes()); + hash.update(current_id.to_le_bytes()); + hash.update(representative_id.to_le_bytes()); + update_exact_hash(&mut hash, factor); + hash.update(current_digest.as_bytes()); + hash.update(representative_digest.as_bytes()); + format!("{:x}", hash.finalize()) +} + +fn update_exact_hash(hash: &mut Sha256, value: ExactComplexRational) { + for part in [ + value.real().numerator(), + value.real().denominator(), + value.imag().numerator(), + value.imag().denominator(), + ] { + let text = part.to_string(); + hash.update((text.len() as u64).to_le_bytes()); + hash.update(text.as_bytes()); + } +} + +fn add_mod(left: u64, right: u64) -> u64 { + ((u128::from(left) + u128::from(right)) % u128::from(PROJECTION_PRIME)) as u64 +} + +fn sub_mod(left: u64, right: u64) -> u64 { + ((u128::from(left) + u128::from(PROJECTION_PRIME) - u128::from(right)) + % u128::from(PROJECTION_PRIME)) as u64 +} + +fn mul_mod(left: u64, right: u64) -> u64 { + ((u128::from(left) * u128::from(right)) % u128::from(PROJECTION_PRIME)) as u64 +} + +fn pow_mod(mut value: u64, mut exponent: u64) -> u64 { + let mut result = 1_u64; + while exponent != 0 { + if exponent & 1 != 0 { + result = mul_mod(result, value); + } + value = mul_mod(value, value); + exponent >>= 1; + } + result +} + +pub fn relation_certificate_algorithm() -> &'static str { + RELATION_CERTIFICATE_ALGORITHM +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::recurrence::ExactRational; + + fn factor(value: i128) -> ExactComplexRational { + ExactComplexRational::new( + ExactRational::new(value, 1).expect("integer factor"), + ExactRational::ZERO, + ) + } + + fn contract(stage: u16) -> RelationCurrentContract { + RelationCurrentContract { + stage, + state_template_id: 11, + component_count: 2, + momentum_form_id: 17, + selector_domain_id: 0, + finalization_executor_id: 9, + finalization_momentum_form_id: 17, + finalization_exact_factor: ExactComplexRational::ONE, + } + } + + fn term( + executor_id: u32, + parent_current_id: u32, + exact_factor: ExactComplexRational, + ) -> RelationTermInput { + RelationTermInput { + executor_id, + parent_current_ids: vec![parent_current_id].into_boxed_slice(), + parent_momentum_form_ids: vec![3].into_boxed_slice(), + exact_factor, + } + } + + #[test] + fn relation_options_reject_a_zero_width_tolerance_contract() { + let error = RecurrenceRelationDiscoveryOptions::new( + RecurrenceRelationDiscoveryMode::CertifiedReuse, + 96, + 4, + 4, + 0.0, + 0.0, + 0x5059_414d, + "lc", + ) + .unwrap_err(); + assert!(error.to_string().contains("at least one must be positive")); + } + + #[test] + fn numerical_nomination_requires_exact_row_vector_replay() { + let inputs = vec![ + RelationCurrentInput { + current_id: 0, + is_source: true, + contract: contract(0), + terms: Vec::new().into_boxed_slice(), + }, + RelationCurrentInput { + current_id: 1, + is_source: false, + contract: contract(1), + terms: vec![term(7, 0, factor(1))].into_boxed_slice(), + }, + RelationCurrentInput { + current_id: 2, + is_source: false, + contract: contract(1), + terms: vec![term(7, 0, factor(2))].into_boxed_slice(), + }, + // The same projected algebra under an incompatible runtime + // contract must never be nominated for reuse. + RelationCurrentInput { + current_id: 3, + is_source: false, + contract: contract(2), + terms: vec![term(7, 0, factor(2))].into_boxed_slice(), + }, + ]; + let options = RecurrenceRelationDiscoveryOptions::new( + RecurrenceRelationDiscoveryMode::Diagnostic, + 96, + 4, + 4, + 1.0e-70, + 1.0e-80, + 0x5059_414d, + "full", + ) + .unwrap(); + let outcome = discover_recurrence_current_relations(&inputs, &options).unwrap(); + assert_eq!(outcome.numerical_candidate_count, 1); + assert_eq!(outcome.uncertified_candidate_count, 0); + assert_eq!(outcome.relations.len(), 1); + let certificate = &outcome.relations[0]; + assert_eq!(certificate.current_id, 2); + assert_eq!(certificate.representative_id, 1); + assert_eq!(certificate.factor, factor(2)); + assert_eq!(certificate.proof_sha256.len(), 64); + } + + #[test] + fn exact_parent_relations_propagate_to_later_currents() { + let inputs = vec![ + RelationCurrentInput { + current_id: 0, + is_source: true, + contract: contract(0), + terms: Vec::new().into_boxed_slice(), + }, + RelationCurrentInput { + current_id: 1, + is_source: false, + contract: contract(1), + terms: vec![term(7, 0, factor(1))].into_boxed_slice(), + }, + RelationCurrentInput { + current_id: 2, + is_source: false, + contract: contract(1), + terms: vec![term(7, 0, factor(2))].into_boxed_slice(), + }, + RelationCurrentInput { + current_id: 3, + is_source: false, + contract: contract(2), + terms: vec![term(8, 2, factor(1))].into_boxed_slice(), + }, + RelationCurrentInput { + current_id: 4, + is_source: false, + contract: contract(2), + terms: vec![term(8, 1, factor(2))].into_boxed_slice(), + }, + ]; + let options = RecurrenceRelationDiscoveryOptions::new( + RecurrenceRelationDiscoveryMode::CertifiedReuse, + 96, + 4, + 4, + 1.0e-70, + 1.0e-80, + 7, + "nlc", + ) + .unwrap(); + let outcome = discover_recurrence_current_relations(&inputs, &options).unwrap(); + assert_eq!( + outcome + .relations + .iter() + .map(|relation| (relation.current_id, relation.representative_id)) + .collect::>(), + vec![(2, 1), (4, 3)] + ); + assert_eq!(outcome.relations[1].factor, ExactComplexRational::ONE); + } +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/recurrence/symmetric_group_fft.rs b/pyamplicol/rust/crates/rusticol-core/src/recurrence/symmetric_group_fft.rs new file mode 100644 index 0000000000..b72d45446e --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/recurrence/symmetric_group_fft.rs @@ -0,0 +1,1679 @@ +// SPDX-License-Identifier: 0BSD + +//! Fast, unnormalised Fourier transforms on symmetric groups. +//! +//! The transform follows the subgroup tower `S_0 < S_1 < ... < S_m`. Its +//! output contains one column-major matrix for every Young diagram `lambda`, +//! packed consecutively, and uses the convention +//! +//! `F_lambda = sum_g f(g) rho_lambda(g)`. +//! +//! Complex values are represented as `(real, imaginary)` pairs so this module +//! remains independent of any evaluator backend. In the batched interface the +//! lane is the innermost index: `value[group_element * lane_count + lane]`. + +use std::collections::BTreeMap; + +use crate::{RusticolError, RusticolResult}; + +/// Largest supported permutation degree. +/// +/// `10!` still fits in `u32`, which keeps the persistent reorder map compact. +pub const MAX_SYMMETRIC_GROUP_FFT_DEGREE: usize = 10; + +/// Backend-independent complex-f64 storage used by the FFT. +pub type SymmetricGroupComplex64 = (f64, f64); + +type YoungTableau = Box<[(u8, u8)]>; + +#[derive(Clone, Copy, Debug, PartialEq)] +struct YoungGeneratorAction { + partner: u32, + diagonal: f64, + mixing: f64, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +struct YoungBranch { + child_irrep: u32, + basis_offset: u32, +} + +#[derive(Debug)] +struct YoungIrrep { + shape: Box<[u8]>, + dimension: usize, + coefficient_offset: usize, + branches: Box<[YoungBranch]>, + tableaux: Box<[YoungTableau]>, + generators: Box<[YoungGeneratorAction]>, +} + +impl YoungIrrep { + fn generator(&self, generator: usize, basis: usize) -> YoungGeneratorAction { + self.generators[generator * self.dimension + basis] + } +} + +#[derive(Debug)] +struct SymmetricGroupLevel { + degree: usize, + order: usize, + maximum_dimension: usize, + irreps: Box<[YoungIrrep]>, +} + +/// Public description of one packed Fourier block. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct SymmetricGroupFftBlock<'a> { + shape: &'a [u8], + dimension: usize, + coefficient_offset: usize, +} + +impl<'a> SymmetricGroupFftBlock<'a> { + /// Row lengths of the Young diagram, in non-increasing order. + pub fn shape(self) -> &'a [u8] { + self.shape + } + + /// Dimension of this irreducible representation. + pub fn dimension(self) -> usize { + self.dimension + } + + /// Zero-based offset of the column-major matrix in the packed transform. + pub fn coefficient_offset(self) -> usize { + self.coefficient_offset + } + + /// Number of scalar complex coefficients in this block. + pub fn coefficient_count(self) -> usize { + self.dimension * self.dimension + } +} + +/// Immutable representation and reorder metadata for one `S_m` transform. +#[derive(Debug)] +pub struct SymmetricGroupFftPlan { + degree: usize, + order: usize, + maximum_dimension: usize, + factorials: Box<[usize]>, + levels: Box<[SymmetricGroupLevel]>, + recursive_to_lexicographic: Box<[u32]>, + scalar_degree_three: Option, +} + +/// Load-time compiled scalar Fourier rows for the smallest non-abelian group. +/// +/// The generic subgroup-tower implementation remains the source of truth: it +/// is applied once to each basis vector while constructing an `S_3` plan. +/// Warm scalar calls can then execute the same real representation transform +/// without repeatedly walking Young-branch metadata and clearing temporary +/// blocks. This is still the complete `S_3` Fourier transform, merely a +/// compact base case of the general recursion. +#[derive(Debug)] +struct ScalarDegreeThreeTransform { + row_offsets: Box<[u8]>, + terms: Box<[(u8, f64)]>, +} + +impl SymmetricGroupFftPlan { + /// Build a transform plan for `S_degree`. + #[cfg_attr(target_vendor = "apple", unsafe(link_section = "__TEXT,__rcl_load"))] + #[cfg_attr(target_vendor = "apple", inline(never))] + pub fn new(degree: usize) -> RusticolResult { + if degree > MAX_SYMMETRIC_GROUP_FFT_DEGREE { + return Err(invalid(format!( + "symmetric-group FFT degree {degree} exceeds supported maximum {MAX_SYMMETRIC_GROUP_FFT_DEGREE}" + ))); + } + let factorials = checked_factorials(degree)?; + let order = factorials[degree]; + let levels = build_levels(degree, &factorials)?; + let maximum_dimension = levels + .iter() + .map(|level| level.maximum_dimension) + .max() + .unwrap_or(1); + let recursive_to_lexicographic = build_recursive_to_lexicographic(degree, &factorials)?; + let mut plan = Self { + degree, + order, + maximum_dimension, + factorials: factorials.into_boxed_slice(), + levels: levels.into_boxed_slice(), + recursive_to_lexicographic: recursive_to_lexicographic.into_boxed_slice(), + scalar_degree_three: None, + }; + if degree == 3 { + plan.scalar_degree_three = Some(compile_scalar_degree_three_transform(&plan)?); + } + Ok(plan) + } + + /// Permutation degree `m`. + pub fn degree(&self) -> usize { + self.degree + } + + /// Group order `m!`, equal to the number of scalar Fourier coefficients. + pub fn order(&self) -> usize { + self.order + } + + /// Largest matrix dimension needed by a reusable workspace. + pub fn maximum_block_dimension(&self) -> usize { + self.maximum_dimension + } + + /// Number of Young-irrep blocks in the packed transform. + pub fn block_count(&self) -> usize { + self.levels[self.degree].irreps.len() + } + + /// Describe one Young-irrep block. + pub fn block(&self, index: usize) -> Option> { + self.levels[self.degree] + .irreps + .get(index) + .map(block_description) + } + + /// Iterate over Young-irrep blocks in packed-output order. + pub fn blocks( + &self, + ) -> impl ExactSizeIterator> + DoubleEndedIterator + '_ { + self.levels[self.degree] + .irreps + .iter() + .map(block_description) + } + + /// Map recursive coset-order indices to lexicographic image-permutation indices. + pub fn recursive_to_lexicographic(&self) -> &[u32] { + &self.recursive_to_lexicographic + } + + /// Return the lexicographic index of the inverse of one indexed permutation. + /// + /// This is the convention boundary needed when a raw colour kernel is + /// stored as `k(g)` but the block contraction requires the transform of + /// `g -> k(g^-1)`. The calculation uses fixed-size stack storage and does + /// not allocate, so callers may build and then discard a compact reorder + /// vector during setup. + pub fn inverse_lexicographic_index(&self, index: usize) -> RusticolResult { + if index >= self.order { + return Err(invalid(format!( + "symmetric-group permutation index {index} exceeds group order {}", + self.order + ))); + } + let permutation = lexicographic_unrank(self.degree, index, &self.factorials)?; + let mut inverse_code = 0_u64; + for (position, value) in permutation[..self.degree].iter().copied().enumerate() { + inverse_code |= (position as u64) << (4 * usize::from(value)); + } + lexicographic_rank(inverse_code, self.degree, &self.factorials) + } + + /// Allocate mutable scratch for at most `lane_capacity` innermost lanes. + pub fn workspace(&self, lane_capacity: usize) -> RusticolResult { + SymmetricGroupFftWorkspace::new(self, lane_capacity) + } + + /// Transform one complex function on `S_m`. + pub fn forward( + &self, + values: &[SymmetricGroupComplex64], + coefficients: &mut [SymmetricGroupComplex64], + workspace: &mut SymmetricGroupFftWorkspace, + ) -> RusticolResult<()> { + self.forward_lanes(1, values, coefficients, workspace) + } + + /// Transform independent lanes stored contiguously for every group element. + pub fn forward_lanes( + &self, + active_lanes: usize, + values: &[SymmetricGroupComplex64], + coefficients: &mut [SymmetricGroupComplex64], + workspace: &mut SymmetricGroupFftWorkspace, + ) -> RusticolResult<()> { + if active_lanes == 0 { + return Err(invalid("symmetric-group FFT lane count must be positive")); + } + let value_count = self + .order + .checked_mul(active_lanes) + .ok_or_else(|| invalid("symmetric-group FFT value count overflows usize"))?; + if values.len() != value_count || coefficients.len() != value_count { + return Err(invalid(format!( + "symmetric-group FFT expected {value_count} input and output values, got {} and {}", + values.len(), + coefficients.len() + ))); + } + workspace.validate(self, active_lanes)?; + + if active_lanes == 1 + && let Some(transform) = self.scalar_degree_three.as_ref() + { + transform.forward(values, coefficients); + return Ok(()); + } + + for (recursive_index, lexicographic_index) in + self.recursive_to_lexicographic.iter().copied().enumerate() + { + let source = usize::try_from(lexicographic_index) + .map_err(|_| internal("symmetric-group reorder index exceeds usize"))? + * active_lanes; + let target = recursive_index * active_lanes; + workspace.buffer[target..target + active_lanes] + .copy_from_slice(&values[source..source + active_lanes]); + } + + let mut current_is_workspace = true; + for current_degree in 2..=self.degree { + let level = &self.levels[current_degree]; + let child_level = &self.levels[current_degree - 1]; + if active_lanes == 1 { + if current_is_workspace { + transform_level_scalar( + level, + child_level, + &workspace.buffer[..value_count], + coefficients, + &mut workspace.block, + self.order, + ); + } else { + transform_level_scalar( + level, + child_level, + coefficients, + &mut workspace.buffer[..value_count], + &mut workspace.block, + self.order, + ); + } + } else if current_is_workspace { + transform_level_batched( + level, + child_level, + &workspace.buffer[..value_count], + coefficients, + &mut workspace.block, + active_lanes, + self.order, + ); + } else { + transform_level_batched( + level, + child_level, + coefficients, + &mut workspace.buffer[..value_count], + &mut workspace.block, + active_lanes, + self.order, + ); + } + current_is_workspace = !current_is_workspace; + } + if current_is_workspace { + coefficients.copy_from_slice(&workspace.buffer[..value_count]); + } + Ok(()) + } +} + +impl ScalarDegreeThreeTransform { + #[inline(always)] + fn forward( + &self, + values: &[SymmetricGroupComplex64], + coefficients: &mut [SymmetricGroupComplex64], + ) { + debug_assert_eq!(values.len(), 6); + debug_assert_eq!(coefficients.len(), 6); + debug_assert_eq!(self.row_offsets.len(), 7); + for (output, coefficient) in coefficients.iter_mut().enumerate().take(6) { + let start = usize::from(self.row_offsets[output]); + let end = usize::from(self.row_offsets[output + 1]); + let mut real = 0.0; + let mut imaginary = 0.0; + for &(source, factor) in &self.terms[start..end] { + let value = values[usize::from(source)]; + real = factor.mul_add(value.0, real); + imaginary = factor.mul_add(value.1, imaginary); + } + *coefficient = (real, imaginary); + } + } +} + +fn compile_scalar_degree_three_transform( + plan: &SymmetricGroupFftPlan, +) -> RusticolResult { + debug_assert_eq!(plan.degree, 3); + debug_assert_eq!(plan.order, 6); + debug_assert!(plan.scalar_degree_three.is_none()); + let mut values = vec![(0.0, 0.0); plan.order]; + let mut coefficients = vec![(0.0, 0.0); plan.order]; + let mut workspace = plan.workspace(1)?; + let mut columns = vec![vec![0.0; plan.order]; plan.order]; + for source in 0..plan.order { + values[source] = (1.0, 0.0); + plan.forward(&values, &mut coefficients, &mut workspace)?; + values[source] = (0.0, 0.0); + for (output, &(real, imaginary)) in coefficients.iter().enumerate() { + if imaginary != 0.0 || !real.is_finite() { + return Err(internal( + "compiled scalar S3 Fourier transform is not finite and real", + )); + } + columns[source][output] = real; + } + } + + let mut row_offsets = Vec::with_capacity(plan.order + 1); + let mut terms = Vec::new(); + row_offsets.push(0_u8); + for output in 0..plan.order { + for (source, column) in columns.iter().enumerate() { + let factor = column[output]; + if factor != 0.0 { + terms.push((source as u8, factor)); + } + } + row_offsets.push( + u8::try_from(terms.len()).map_err(|_| { + internal("compiled scalar S3 Fourier transform term count exceeds u8") + })?, + ); + } + Ok(ScalarDegreeThreeTransform { + row_offsets: row_offsets.into_boxed_slice(), + terms: terms.into_boxed_slice(), + }) +} + +fn block_description(irrep: &YoungIrrep) -> SymmetricGroupFftBlock<'_> { + SymmetricGroupFftBlock { + shape: &irrep.shape, + dimension: irrep.dimension, + coefficient_offset: irrep.coefficient_offset, + } +} + +/// Mutable scratch that can be reused for transforms with the same shape. +#[derive(Debug)] +pub struct SymmetricGroupFftWorkspace { + degree: usize, + order: usize, + lane_capacity: usize, + buffer: Vec, + block: Vec, +} + +impl SymmetricGroupFftWorkspace { + /// Allocate scratch for at most `lane_capacity` independent functions. + pub fn new(plan: &SymmetricGroupFftPlan, lane_capacity: usize) -> RusticolResult { + if lane_capacity == 0 { + return Err(invalid("symmetric-group FFT lane count must be positive")); + } + let buffer_count = plan + .order + .checked_mul(lane_capacity) + .ok_or_else(|| invalid("symmetric-group FFT workspace buffer size overflows usize"))?; + let block_count = plan + .maximum_dimension + .checked_mul(plan.maximum_dimension) + .and_then(|value| value.checked_mul(lane_capacity)) + .ok_or_else(|| invalid("symmetric-group FFT block workspace size overflows usize"))?; + Ok(Self { + degree: plan.degree, + order: plan.order, + lane_capacity, + buffer: zeroed_complex_values(buffer_count, "FFT permutation buffer")?, + block: zeroed_complex_values(block_count, "FFT block workspace")?, + }) + } + + /// Maximum number of independent lanes accepted by this workspace. + pub fn lane_capacity(&self) -> usize { + self.lane_capacity + } + + fn validate(&self, plan: &SymmetricGroupFftPlan, active_lanes: usize) -> RusticolResult<()> { + if self.degree != plan.degree || self.order != plan.order { + return Err(invalid( + "symmetric-group FFT workspace does not match the plan", + )); + } + if active_lanes > self.lane_capacity { + return Err(invalid(format!( + "symmetric-group FFT requested {active_lanes} active lanes from workspace capacity {}", + self.lane_capacity + ))); + } + Ok(()) + } +} + +fn transform_level_batched( + level: &SymmetricGroupLevel, + child_level: &SymmetricGroupLevel, + input: &[SymmetricGroupComplex64], + output: &mut [SymmetricGroupComplex64], + block: &mut [SymmetricGroupComplex64], + lane_count: usize, + total_order: usize, +) { + debug_assert_eq!(input.len(), total_order * lane_count); + debug_assert_eq!(output.len(), total_order * lane_count); + debug_assert_eq!(level.order, level.degree * child_level.order); + debug_assert!(block.len() >= level.maximum_dimension.pow(2) * lane_count); + + for group_start in (0..total_order).step_by(level.order) { + let group_value_start = group_start * lane_count; + let group_value_end = (group_start + level.order) * lane_count; + output[group_value_start..group_value_end].fill((0.0, 0.0)); + + for coset in 0..level.degree { + let child_start = group_start + coset * child_level.order; + for irrep in &level.irreps { + let dimension = irrep.dimension; + let block_value_count = dimension * dimension * lane_count; + let block = &mut block[..block_value_count]; + block.fill((0.0, 0.0)); + + for branch in &irrep.branches { + let child = &child_level.irreps[branch.child_irrep as usize]; + let basis_offset = branch.basis_offset as usize; + for column in 0..child.dimension { + for row in 0..child.dimension { + let source_coefficient = child_start + + child.coefficient_offset + + column * child.dimension + + row; + let target_coefficient = + (basis_offset + column) * dimension + basis_offset + row; + let source = source_coefficient * lane_count; + let target = target_coefficient * lane_count; + block[target..target + lane_count] + .copy_from_slice(&input[source..source + lane_count]); + } + } + } + + // The coset representative is s_i s_(i+1) ... s_(m-1). + // Left multiplication therefore applies its rightmost factor first. + for generator in (coset..level.degree - 1).rev() { + apply_generator_on_left(irrep, generator, block, lane_count); + } + + let output_offset = (group_start + irrep.coefficient_offset) * lane_count; + for (target, value) in output[output_offset..output_offset + block_value_count] + .iter_mut() + .zip(block.iter().copied()) + { + target.0 += value.0; + target.1 += value.1; + } + } + } + } +} + +/// Scalar transforms keep each temporary matrix transposed. A Young +/// generator then mixes two contiguous rows rather than walking two strided +/// rows of the public column-major output. The transpose is folded into the +/// branch gather and final accumulation, so it needs no additional storage or +/// pass over the block. +fn transform_level_scalar( + level: &SymmetricGroupLevel, + child_level: &SymmetricGroupLevel, + input: &[SymmetricGroupComplex64], + output: &mut [SymmetricGroupComplex64], + block: &mut [SymmetricGroupComplex64], + total_order: usize, +) { + debug_assert_eq!(input.len(), total_order); + debug_assert_eq!(output.len(), total_order); + debug_assert_eq!(level.order, level.degree * child_level.order); + debug_assert!(block.len() >= level.maximum_dimension.pow(2)); + + for group_start in (0..total_order).step_by(level.order) { + output[group_start..group_start + level.order].fill((0.0, 0.0)); + + for coset in 0..level.degree { + let child_start = group_start + coset * child_level.order; + for irrep in &level.irreps { + let dimension = irrep.dimension; + let block_value_count = dimension * dimension; + let block = &mut block[..block_value_count]; + block.fill((0.0, 0.0)); + + for branch in &irrep.branches { + let child = &child_level.irreps[branch.child_irrep as usize]; + let basis_offset = branch.basis_offset as usize; + for column in 0..child.dimension { + for row in 0..child.dimension { + let source = child_start + + child.coefficient_offset + + column * child.dimension + + row; + // Fold the column-major -> row-major transpose into + // the sparse branch embedding. + let target = (basis_offset + row) * dimension + basis_offset + column; + block[target] = input[source]; + } + } + } + + // The coset representative is s_i s_(i+1) ... s_(m-1). + // Left multiplication therefore applies its rightmost factor first. + for generator in (coset..level.degree - 1).rev() { + apply_generator_on_left_scalar_transposed(irrep, generator, block); + } + + let output_offset = group_start + irrep.coefficient_offset; + for row in 0..dimension { + for column in 0..dimension { + let target = output_offset + column * dimension + row; + let value = block[row * dimension + column]; + output[target].0 += value.0; + output[target].1 += value.1; + } + } + } + } + } +} + +fn apply_generator_on_left( + irrep: &YoungIrrep, + generator: usize, + matrix: &mut [SymmetricGroupComplex64], + lane_count: usize, +) { + let dimension = irrep.dimension; + for first in 0..dimension { + let first_action = irrep.generator(generator, first); + let other = first_action.partner as usize; + if other < first { + continue; + } + if other == first { + for column in 0..dimension { + let base = (column * dimension + first) * lane_count; + for value in &mut matrix[base..base + lane_count] { + value.0 *= first_action.diagonal; + value.1 *= first_action.diagonal; + } + } + continue; + } + let other_action = irrep.generator(generator, other); + for column in 0..dimension { + let first_base = (column * dimension + first) * lane_count; + let other_base = (column * dimension + other) * lane_count; + for lane in 0..lane_count { + let first_value = matrix[first_base + lane]; + let other_value = matrix[other_base + lane]; + matrix[first_base + lane] = linear_combination( + first_action.diagonal, + first_value, + first_action.mixing, + other_value, + ); + matrix[other_base + lane] = linear_combination( + other_action.mixing, + first_value, + other_action.diagonal, + other_value, + ); + } + } + } +} + +fn apply_generator_on_left_scalar_transposed( + irrep: &YoungIrrep, + generator: usize, + matrix: &mut [SymmetricGroupComplex64], +) { + let dimension = irrep.dimension; + debug_assert_eq!(matrix.len(), dimension * dimension); + for first in 0..dimension { + let first_action = irrep.generator(generator, first); + let other = first_action.partner as usize; + if other < first { + continue; + } + if other == first { + // A fixed Young-basis vector has axial distance +/-1. The +1 + // action is the identity and is common enough to skip outright. + if first_action.diagonal == 1.0 { + continue; + } + debug_assert_eq!(first_action.diagonal, -1.0); + let row = &mut matrix[first * dimension..(first + 1) * dimension]; + for value in row { + value.0 = -value.0; + value.1 = -value.1; + } + continue; + } + + let other_action = irrep.generator(generator, other); + let (before_other, from_other) = matrix.split_at_mut(other * dimension); + let first_row = &mut before_other[first * dimension..(first + 1) * dimension]; + let other_row = &mut from_other[..dimension]; + for (first_value, other_value) in first_row.iter_mut().zip(other_row) { + let old_first = *first_value; + let old_other = *other_value; + *first_value = linear_combination( + first_action.diagonal, + old_first, + first_action.mixing, + old_other, + ); + *other_value = linear_combination( + other_action.mixing, + old_first, + other_action.diagonal, + old_other, + ); + } + } +} + +fn linear_combination( + first_factor: f64, + first: SymmetricGroupComplex64, + second_factor: f64, + second: SymmetricGroupComplex64, +) -> SymmetricGroupComplex64 { + ( + first_factor * first.0 + second_factor * second.0, + first_factor * first.1 + second_factor * second.1, + ) +} + +fn build_levels(degree: usize, factorials: &[usize]) -> RusticolResult> { + let trivial_tableau: Box<[(u8, u8)]> = Vec::new().into_boxed_slice(); + let mut levels = vec![SymmetricGroupLevel { + degree: 0, + order: 1, + maximum_dimension: 1, + irreps: vec![YoungIrrep { + shape: Vec::new().into_boxed_slice(), + dimension: 1, + coefficient_offset: 0, + branches: Vec::new().into_boxed_slice(), + tableaux: vec![trivial_tableau].into_boxed_slice(), + generators: Vec::new().into_boxed_slice(), + }] + .into_boxed_slice(), + }]; + for current_degree in 1..=degree { + let level = build_level( + current_degree, + factorials[current_degree], + &levels[current_degree - 1], + )?; + levels.push(level); + } + Ok(levels) +} + +fn build_level( + degree: usize, + order: usize, + child_level: &SymmetricGroupLevel, +) -> RusticolResult { + debug_assert_eq!(child_level.degree + 1, degree); + let shapes = integer_partitions(degree)?; + let mut irreps = Vec::new(); + irreps + .try_reserve_exact(shapes.len()) + .map_err(|error| internal(format!("Young-irrep allocation failed: {error}")))?; + let mut coefficient_offset = 0_usize; + let mut maximum_dimension = 1_usize; + + for shape in shapes { + let removable_rows = (0..shape.len()) + .filter(|row| shape[*row] > shape.get(*row + 1).copied().unwrap_or(0)) + .collect::>(); + let mut branches = Vec::new(); + let mut tableaux = Vec::new(); + let mut dimension = 0_usize; + for row in removable_rows { + let mut child_shape = shape.clone(); + child_shape[row] -= 1; + if child_shape.last() == Some(&0) { + child_shape.pop(); + } + let child_irrep = child_level + .irreps + .iter() + .position(|candidate| candidate.shape.as_ref() == child_shape) + .ok_or_else(|| internal("Young branching child shape is absent"))?; + let child = &child_level.irreps[child_irrep]; + let basis_offset = dimension; + dimension = dimension + .checked_add(child.dimension) + .ok_or_else(|| internal("Young-irrep dimension overflows usize"))?; + branches.push(YoungBranch { + child_irrep: u32::try_from(child_irrep) + .map_err(|_| internal("Young child-irrep index exceeds u32"))?, + basis_offset: u32::try_from(basis_offset) + .map_err(|_| internal("Young branch basis offset exceeds u32"))?, + }); + for child_tableau in &child.tableaux { + let mut tableau = child_tableau.to_vec(); + tableau.push(( + u8::try_from(row).map_err(|_| internal("Young tableau row exceeds u8"))?, + shape[row] - 1, + )); + tableaux.push(tableau.into_boxed_slice()); + } + } + if dimension == 0 || tableaux.len() != dimension { + return Err(internal("Young branching produced an invalid dimension")); + } + let generators = build_young_generators(degree, &tableaux)?; + let coefficient_count = dimension + .checked_mul(dimension) + .ok_or_else(|| internal("Young Fourier block size overflows usize"))?; + let irrep = YoungIrrep { + shape: shape.into_boxed_slice(), + dimension, + coefficient_offset, + branches: branches.into_boxed_slice(), + tableaux: tableaux.into_boxed_slice(), + generators: generators.into_boxed_slice(), + }; + coefficient_offset = coefficient_offset + .checked_add(coefficient_count) + .ok_or_else(|| internal("Young coefficient count overflows usize"))?; + maximum_dimension = maximum_dimension.max(dimension); + irreps.push(irrep); + } + if coefficient_offset != order { + return Err(internal(format!( + "Young dimensions square to {coefficient_offset}, expected group order {order}" + ))); + } + Ok(SymmetricGroupLevel { + degree, + order, + maximum_dimension, + irreps: irreps.into_boxed_slice(), + }) +} + +fn integer_partitions(degree: usize) -> RusticolResult>> { + fn visit( + remaining: usize, + maximum_part: usize, + shape: &mut Vec, + output: &mut Vec>, + ) -> RusticolResult<()> { + if remaining == 0 { + output.push(shape.clone()); + return Ok(()); + } + for part in (1..=remaining.min(maximum_part)).rev() { + shape.push( + u8::try_from(part) + .map_err(|_| internal("Young partition row length exceeds u8"))?, + ); + visit(remaining - part, part, shape, output)?; + shape.pop(); + } + Ok(()) + } + + let mut output = Vec::new(); + visit(degree, degree, &mut Vec::new(), &mut output)?; + Ok(output) +} + +fn build_young_generators( + degree: usize, + tableaux: &[Box<[(u8, u8)]>], +) -> RusticolResult> { + if degree <= 1 { + return Ok(Vec::new()); + } + let dimension = tableaux.len(); + let action_count = (degree - 1) + .checked_mul(dimension) + .ok_or_else(|| internal("Young generator table size overflows usize"))?; + let mut actions = Vec::new(); + actions + .try_reserve_exact(action_count) + .map_err(|error| internal(format!("Young generator allocation failed: {error}")))?; + + let tableau_index = tableaux + .iter() + .enumerate() + .map(|(index, tableau)| (tableau.to_vec(), index)) + .collect::>(); + for generator in 0..degree - 1 { + for (basis, tableau) in tableaux.iter().enumerate() { + let first = tableau[generator]; + let second = tableau[generator + 1]; + let first_content = i16::from(first.1) - i16::from(first.0); + let second_content = i16::from(second.1) - i16::from(second.0); + let axial_distance = second_content - first_content; + if axial_distance == 0 { + return Err(internal("standard Young tableau has zero axial distance")); + } + let diagonal = 1.0 / f64::from(axial_distance); + let partner = if axial_distance.unsigned_abs() == 1 { + basis + } else { + let mut swapped = tableau.to_vec(); + swapped.swap(generator, generator + 1); + *tableau_index + .get(&swapped) + .ok_or_else(|| internal("Young generator partner tableau is absent"))? + }; + actions.push(YoungGeneratorAction { + partner: u32::try_from(partner) + .map_err(|_| internal("Young generator partner exceeds u32"))?, + diagonal, + mixing: (1.0 - diagonal * diagonal).max(0.0).sqrt(), + }); + } + } + Ok(actions) +} + +fn checked_factorials(degree: usize) -> RusticolResult> { + let mut factorials = Vec::new(); + factorials + .try_reserve_exact(degree + 1) + .map_err(|error| invalid(format!("factorial table allocation failed: {error}")))?; + factorials.push(1_usize); + for factor in 1..=degree { + let value = factorials[factor - 1] + .checked_mul(factor) + .ok_or_else(|| invalid("symmetric-group order overflows usize"))?; + if value > u32::MAX as usize { + return Err(invalid( + "symmetric-group order exceeds compact reorder-index capacity", + )); + } + factorials.push(value); + } + Ok(factorials) +} + +fn build_recursive_to_lexicographic( + degree: usize, + factorials: &[usize], +) -> RusticolResult> { + if degree == 0 { + return Ok(vec![0]); + } + let mut child_codes = vec![0_u64]; + let mut final_map = Vec::new(); + for current_degree in 1..=degree { + let order = factorials[current_degree]; + let is_final = current_degree == degree; + let mut next_codes = Vec::new(); + if is_final { + final_map.try_reserve_exact(order).map_err(|error| { + invalid(format!("permutation reorder allocation failed: {error}")) + })?; + } else { + next_codes + .try_reserve_exact(order) + .map_err(|error| invalid(format!("permutation-code allocation failed: {error}")))?; + } + for coset in 0..current_degree { + for child_code in child_codes.iter().copied() { + let code = extend_recursive_permutation(child_code, current_degree, coset); + if is_final { + let rank = lexicographic_rank(code, current_degree, factorials)?; + final_map.push( + u32::try_from(rank) + .map_err(|_| internal("lexicographic rank exceeds u32"))?, + ); + } else { + next_codes.push(code); + } + } + } + if !is_final { + child_codes = next_codes; + } + } + validate_reorder_map(&final_map, factorials[degree])?; + Ok(final_map) +} + +fn extend_recursive_permutation(child: u64, degree: usize, coset: usize) -> u64 { + debug_assert!((1..=MAX_SYMMETRIC_GROUP_FFT_DEGREE).contains(°ree)); + debug_assert!(coset < degree); + let mut result = 0_u64; + for position in 0..degree - 1 { + let child_value = ((child >> (4 * position)) & 0xf) as usize; + let value = if child_value < coset { + child_value + } else { + child_value + 1 + }; + result |= (value as u64) << (4 * position); + } + result | (coset as u64) << (4 * (degree - 1)) +} + +fn lexicographic_rank(code: u64, degree: usize, factorials: &[usize]) -> RusticolResult { + let mut used = 0_u16; + let mut rank = 0_usize; + for position in 0..degree { + let value = ((code >> (4 * position)) & 0xf) as usize; + if value >= degree || used & (1_u16 << value) != 0 { + return Err(internal("recursive permutation code is invalid")); + } + let smaller_mask = (1_u16 << value) - 1; + let used_smaller = (used & smaller_mask).count_ones() as usize; + let unused_smaller = value - used_smaller; + rank = rank + .checked_add(unused_smaller * factorials[degree - position - 1]) + .ok_or_else(|| internal("lexicographic permutation rank overflows usize"))?; + used |= 1_u16 << value; + } + Ok(rank) +} + +fn lexicographic_unrank( + degree: usize, + mut rank: usize, + factorials: &[usize], +) -> RusticolResult<[u8; MAX_SYMMETRIC_GROUP_FFT_DEGREE]> { + if factorials + .get(degree) + .copied() + .is_none_or(|order| rank >= order) + { + return Err(invalid("lexicographic permutation rank is out of bounds")); + } + let mut available = [0_u8; MAX_SYMMETRIC_GROUP_FFT_DEGREE]; + for (index, value) in available[..degree].iter_mut().enumerate() { + *value = u8::try_from(index) + .map_err(|_| internal("permutation value exceeds compact storage"))?; + } + let mut available_count = degree; + let mut permutation = [0_u8; MAX_SYMMETRIC_GROUP_FFT_DEGREE]; + for (position, value) in permutation[..degree].iter_mut().enumerate() { + let block = factorials[degree - position - 1]; + let choice = rank / block; + rank %= block; + if choice >= available_count { + return Err(internal("lexicographic unrank selected an invalid digit")); + } + *value = available[choice]; + available.copy_within(choice + 1..available_count, choice); + available_count -= 1; + } + Ok(permutation) +} + +fn validate_reorder_map(values: &[u32], order: usize) -> RusticolResult<()> { + if values.len() != order { + return Err(internal("permutation reorder map has the wrong length")); + } + let mut seen = Vec::new(); + seen.try_reserve_exact(order) + .map_err(|error| invalid(format!("reorder validation allocation failed: {error}")))?; + seen.resize(order, false); + for value in values.iter().copied() { + let index = usize::try_from(value) + .map_err(|_| internal("permutation reorder index exceeds usize"))?; + if index >= order || std::mem::replace(&mut seen[index], true) { + return Err(internal("permutation reorder map is not bijective")); + } + } + Ok(()) +} + +fn zeroed_complex_values( + count: usize, + label: &str, +) -> RusticolResult> { + let mut values = Vec::new(); + values + .try_reserve_exact(count) + .map_err(|error| invalid(format!("{label} allocation failed: {error}")))?; + values.resize(count, (0.0, 0.0)); + Ok(values) +} + +fn invalid(message: impl Into) -> RusticolError { + RusticolError::invalid_argument(message) +} + +fn internal(message: impl Into) -> RusticolError { + RusticolError::internal(message) +} + +#[cfg(test)] +mod tests { + use super::*; + + const TOLERANCE: f64 = 2.0e-11; + + #[test] + fn known_young_layouts_and_dimensions() { + let plan = SymmetricGroupFftPlan::new(4).unwrap(); + let blocks = plan + .blocks() + .map(|block| (block.shape().to_vec(), block.dimension())) + .collect::>(); + assert_eq!( + blocks, + vec![ + (vec![4], 1), + (vec![3, 1], 3), + (vec![2, 2], 2), + (vec![2, 1, 1], 3), + (vec![1, 1, 1, 1], 1), + ] + ); + assert_eq!( + plan.blocks() + .map(|block| block.coefficient_count()) + .sum::(), + 24 + ); + } + + #[test] + fn transform_matches_direct_definition_through_six() { + for degree in 0..=6 { + let plan = SymmetricGroupFftPlan::new(degree).unwrap(); + let values = deterministic_values(plan.order(), 0); + let mut fast = vec![(0.0, 0.0); plan.order()]; + let mut workspace = plan.workspace(1).unwrap(); + plan.forward(&values, &mut fast, &mut workspace).unwrap(); + let direct = direct_transform(&plan, &values); + assert_complex_slices_close(&fast, &direct, TOLERANCE); + } + } + + #[test] + fn inverse_kernel_blocks_match_direct_quadratic_form() { + let plan = SymmetricGroupFftPlan::new(4).unwrap(); + let left = deterministic_values(plan.order(), 2); + let right = deterministic_values(plan.order(), 11); + // Deliberately not inversion-symmetric: this catches a silently + // reversed raw-kernel convention that a diagonal colour kernel cannot. + let kernel = deterministic_values(plan.order(), 23); + let inverse_kernel = (0..plan.order()) + .map(|index| kernel[plan.inverse_lexicographic_index(index).unwrap()]) + .collect::>(); + + let mut workspace = plan.workspace(1).unwrap(); + let mut left_fourier = vec![(0.0, 0.0); plan.order()]; + let mut right_fourier = vec![(0.0, 0.0); plan.order()]; + let mut kernel_fourier = vec![(0.0, 0.0); plan.order()]; + plan.forward(&left, &mut left_fourier, &mut workspace) + .unwrap(); + plan.forward(&right, &mut right_fourier, &mut workspace) + .unwrap(); + plan.forward(&inverse_kernel, &mut kernel_fourier, &mut workspace) + .unwrap(); + + let direct = direct_relative_kernel_form(&plan, &left, &right, &kernel); + let blocked = + fourier_relative_kernel_form(&plan, &left_fourier, &right_fourier, &kernel_fourier); + let scale = complex_norm(direct).max(1.0); + assert!( + complex_norm((blocked.0 - direct.0, blocked.1 - direct.1)) <= 5.0e-11 * scale, + "inverse-kernel block form {blocked:?} != direct form {direct:?}" + ); + } + + #[test] + fn identity_impulse_transforms_to_irrep_identities() { + for degree in 0..=7 { + let plan = SymmetricGroupFftPlan::new(degree).unwrap(); + let mut values = vec![(0.0, 0.0); plan.order()]; + values[0] = (1.0, 0.0); + let mut transformed = vec![(0.0, 0.0); plan.order()]; + let mut workspace = plan.workspace(1).unwrap(); + plan.forward(&values, &mut transformed, &mut workspace) + .unwrap(); + for block in plan.blocks() { + for column in 0..block.dimension() { + for row in 0..block.dimension() { + let value = transformed + [block.coefficient_offset() + column * block.dimension() + row]; + let expected = if row == column { 1.0 } else { 0.0 }; + assert!((value.0 - expected).abs() <= TOLERANCE); + assert!(value.1.abs() <= TOLERANCE); + } + } + } + } + } + + #[test] + fn young_generators_obey_coxeter_relations() { + for degree in 2..=7 { + let plan = SymmetricGroupFftPlan::new(degree).unwrap(); + let level = &plan.levels[degree]; + for irrep in &level.irreps { + let identity = dense_identity(irrep.dimension); + let generators = (0..degree - 1) + .map(|generator| dense_generator(irrep, generator)) + .collect::>(); + for generator in &generators { + assert_real_matrices_close( + &dense_multiply(generator, generator, irrep.dimension), + &identity, + TOLERANCE, + ); + } + for generator in 0..degree - 2 { + let left = dense_multiply( + &dense_multiply( + &generators[generator], + &generators[generator + 1], + irrep.dimension, + ), + &generators[generator], + irrep.dimension, + ); + let right = dense_multiply( + &dense_multiply( + &generators[generator + 1], + &generators[generator], + irrep.dimension, + ), + &generators[generator + 1], + irrep.dimension, + ); + assert_real_matrices_close(&left, &right, TOLERANCE); + } + for first in 0..degree - 1 { + for second in first + 2..degree - 1 { + let left = dense_multiply( + &generators[first], + &generators[second], + irrep.dimension, + ); + let right = dense_multiply( + &generators[second], + &generators[first], + irrep.dimension, + ); + assert_real_matrices_close(&left, &right, TOLERANCE); + } + } + } + } + } + + #[test] + fn parseval_identity_holds_through_s_eight() { + for degree in 0..=8 { + let plan = SymmetricGroupFftPlan::new(degree).unwrap(); + let values = deterministic_values(plan.order(), degree); + let mut transformed = vec![(0.0, 0.0); plan.order()]; + let mut workspace = plan.workspace(1).unwrap(); + plan.forward(&values, &mut transformed, &mut workspace) + .unwrap(); + let input_norm = values + .iter() + .map(|value| value.0 * value.0 + value.1 * value.1) + .sum::(); + let fourier_norm = plan + .blocks() + .map(|block| { + let start = block.coefficient_offset(); + let end = start + block.coefficient_count(); + block.dimension() as f64 + * transformed[start..end] + .iter() + .map(|value| value.0 * value.0 + value.1 * value.1) + .sum::() + }) + .sum::() + / plan.order() as f64; + let scale = input_norm.abs().max(1.0); + assert!((fourier_norm - input_norm).abs() <= 5.0e-11 * scale); + } + } + + #[test] + fn workspace_reuse_does_not_retain_transform_state() { + let plan = SymmetricGroupFftPlan::new(7).unwrap(); + let first = deterministic_values(plan.order(), 3); + let second = deterministic_values(plan.order(), 19); + let mut workspace = plan.workspace(1).unwrap(); + let mut discarded = vec![(0.0, 0.0); plan.order()]; + let mut reused = vec![(0.0, 0.0); plan.order()]; + plan.forward(&first, &mut discarded, &mut workspace) + .unwrap(); + plan.forward(&second, &mut reused, &mut workspace).unwrap(); + let mut fresh = vec![(0.0, 0.0); plan.order()]; + let mut fresh_workspace = plan.workspace(1).unwrap(); + plan.forward(&second, &mut fresh, &mut fresh_workspace) + .unwrap(); + assert_complex_slices_close(&reused, &fresh, TOLERANCE); + } + + #[test] + fn batched_lanes_match_independent_transforms() { + let plan = SymmetricGroupFftPlan::new(6).unwrap(); + let lanes = 3; + let lane_values = (0..lanes) + .map(|lane| deterministic_values(plan.order(), lane * 7)) + .collect::>(); + let mut values = vec![(0.0, 0.0); plan.order() * lanes]; + for group_element in 0..plan.order() { + for lane in 0..lanes { + values[group_element * lanes + lane] = lane_values[lane][group_element]; + } + } + let mut batched = vec![(0.0, 0.0); values.len()]; + let mut workspace = plan.workspace(lanes).unwrap(); + plan.forward_lanes(lanes, &values, &mut batched, &mut workspace) + .unwrap(); + for lane in 0..lanes { + let mut expected = vec![(0.0, 0.0); plan.order()]; + let mut scalar_workspace = plan.workspace(1).unwrap(); + plan.forward(&lane_values[lane], &mut expected, &mut scalar_workspace) + .unwrap(); + let actual = (0..plan.order()) + .map(|coefficient| batched[coefficient * lanes + lane]) + .collect::>(); + assert_complex_slices_close(&actual, &expected, TOLERANCE); + } + } + + #[test] + fn scalar_transposed_path_matches_batched_path_through_s_seven() { + for degree in 0..=7 { + let plan = SymmetricGroupFftPlan::new(degree).unwrap(); + let scalar_values = deterministic_values(plan.order(), degree * 13 + 5); + let other_values = deterministic_values(plan.order(), degree * 17 + 9); + let mut scalar = vec![(0.0, 0.0); plan.order()]; + let mut scalar_workspace = plan.workspace(1).unwrap(); + plan.forward(&scalar_values, &mut scalar, &mut scalar_workspace) + .unwrap(); + + let mut batched_values = vec![(0.0, 0.0); plan.order() * 2]; + for group in 0..plan.order() { + batched_values[group * 2] = scalar_values[group]; + batched_values[group * 2 + 1] = other_values[group]; + } + let mut batched = vec![(0.0, 0.0); batched_values.len()]; + let mut batched_workspace = plan.workspace(2).unwrap(); + plan.forward_lanes(2, &batched_values, &mut batched, &mut batched_workspace) + .unwrap(); + let first_lane = batched + .chunks_exact(2) + .map(|lanes| lanes[0]) + .collect::>(); + assert_complex_slices_close(&scalar, &first_lane, TOLERANCE); + } + } + + #[test] + fn capacity_workspace_reuses_active_prefix_without_state_leakage() { + let plan = SymmetricGroupFftPlan::new(5).unwrap(); + let lane_capacity = 4; + let mut workspace = plan.workspace(lane_capacity).unwrap(); + let buffer_address = workspace.buffer.as_ptr(); + let block_address = workspace.block.as_ptr(); + + assert_eq!(workspace.lane_capacity(), lane_capacity); + for active_lanes in 1..=lane_capacity { + let lane_values = (0..active_lanes) + .map(|lane| deterministic_values(plan.order(), active_lanes * 11 + lane * 7)) + .collect::>(); + let mut values = vec![(0.0, 0.0); plan.order() * active_lanes]; + for group_element in 0..plan.order() { + for lane in 0..active_lanes { + values[group_element * active_lanes + lane] = lane_values[lane][group_element]; + } + } + + let mut transformed = vec![(f64::NAN, f64::NAN); values.len()]; + plan.forward_lanes(active_lanes, &values, &mut transformed, &mut workspace) + .unwrap(); + + for lane in 0..active_lanes { + let mut expected = vec![(0.0, 0.0); plan.order()]; + let mut scalar_workspace = plan.workspace(1).unwrap(); + plan.forward(&lane_values[lane], &mut expected, &mut scalar_workspace) + .unwrap(); + let actual = (0..plan.order()) + .map(|coefficient| transformed[coefficient * active_lanes + lane]) + .collect::>(); + assert_complex_slices_close(&actual, &expected, TOLERANCE); + } + + assert_eq!(workspace.buffer.as_ptr(), buffer_address); + assert_eq!(workspace.block.as_ptr(), block_address); + assert_eq!(workspace.lane_capacity(), lane_capacity); + } + + let values = vec![(0.0, 0.0); plan.order() * (lane_capacity + 1)]; + let mut coefficients = values.clone(); + assert!( + plan.forward_lanes( + lane_capacity + 1, + &values, + &mut coefficients, + &mut workspace, + ) + .is_err() + ); + } + + #[test] + fn reorder_map_is_a_lexicographic_bijection() { + for degree in 0..=8 { + let plan = SymmetricGroupFftPlan::new(degree).unwrap(); + let mut values = plan.recursive_to_lexicographic().to_vec(); + values.sort_unstable(); + assert_eq!( + values, + (0..plan.order() as u32).collect::>(), + "degree {degree}" + ); + for index in 0..plan.order() { + let inverse = plan.inverse_lexicographic_index(index).unwrap(); + assert_eq!( + plan.inverse_lexicographic_index(inverse).unwrap(), + index, + "inverse-index involution failed at degree {degree}, index {index}" + ); + } + assert!(plan.inverse_lexicographic_index(plan.order()).is_err()); + } + } + + #[test] + fn degree_ten_is_supported_and_larger_degrees_are_rejected() { + let plan = SymmetricGroupFftPlan::new(10).unwrap(); + assert_eq!(plan.order(), 3_628_800); + assert_eq!(plan.recursive_to_lexicographic().len(), plan.order()); + assert_eq!( + plan.blocks() + .map(|block| block.coefficient_count()) + .sum::(), + plan.order() + ); + assert!(SymmetricGroupFftPlan::new(11).is_err()); + } + + fn deterministic_values(count: usize, salt: usize) -> Vec { + (0..count) + .map(|index| { + let value = (index + salt + 1) as f64; + ( + ((value * 0.173).sin() + (value * 0.037).cos()) / (value + 1.0).sqrt(), + ((value * 0.113).cos() - (value * 0.071).sin()) / (value + 2.0).sqrt(), + ) + }) + .collect() + } + + fn direct_transform( + plan: &SymmetricGroupFftPlan, + values: &[SymmetricGroupComplex64], + ) -> Vec { + let mut result = vec![(0.0, 0.0); plan.order()]; + for (rank, value) in values.iter().copied().enumerate() { + let permutation = lexicographic_unrank(plan.degree(), rank); + for (block_index, block) in plan.blocks().enumerate() { + let representation = representation_matrix(plan, block_index, &permutation); + for (offset, coefficient) in representation.into_iter().enumerate() { + result[block.coefficient_offset() + offset].0 += value.0 * coefficient; + result[block.coefficient_offset() + offset].1 += value.1 * coefficient; + } + } + } + result + } + + fn direct_relative_kernel_form( + plan: &SymmetricGroupFftPlan, + left: &[SymmetricGroupComplex64], + right: &[SymmetricGroupComplex64], + kernel: &[SymmetricGroupComplex64], + ) -> SymmetricGroupComplex64 { + let permutations = (0..plan.order()) + .map(|index| lexicographic_unrank(plan.degree(), index)) + .collect::>(); + let mut result = (0.0, 0.0); + for (left_index, left_value) in left.iter().copied().enumerate() { + let mut inverse_left = vec![0_usize; plan.degree()]; + for (position, value) in permutations[left_index].iter().copied().enumerate() { + inverse_left[value] = position; + } + for (right_index, right_value) in right.iter().copied().enumerate() { + let relative = permutations[right_index] + .iter() + .map(|value| inverse_left[*value]) + .collect::>(); + let relative_index = permutation_rank(&relative); + let bilinear = complex_multiply( + complex_multiply((left_value.0, -left_value.1), right_value), + kernel[relative_index], + ); + result.0 += bilinear.0; + result.1 += bilinear.1; + } + } + result + } + + fn fourier_relative_kernel_form( + plan: &SymmetricGroupFftPlan, + left: &[SymmetricGroupComplex64], + right: &[SymmetricGroupComplex64], + kernel: &[SymmetricGroupComplex64], + ) -> SymmetricGroupComplex64 { + let mut result = (0.0, 0.0); + for block in plan.blocks() { + let dimension = block.dimension(); + let offset = block.coefficient_offset(); + let mut product = vec![(0.0, 0.0); dimension * dimension]; + for column in 0..dimension { + for inner in 0..dimension { + let kernel_value = kernel[offset + column * dimension + inner]; + for row in 0..dimension { + let value = + complex_multiply(right[offset + inner * dimension + row], kernel_value); + let target = &mut product[column * dimension + row]; + target.0 += value.0; + target.1 += value.1; + } + } + } + let mut trace = (0.0, 0.0); + for (index, right_kernel) in product.into_iter().enumerate() { + let left_value = left[offset + index]; + let value = complex_multiply((left_value.0, -left_value.1), right_kernel); + trace.0 += value.0; + trace.1 += value.1; + } + let weight = dimension as f64 / plan.order() as f64; + result.0 += weight * trace.0; + result.1 += weight * trace.1; + } + result + } + + fn permutation_rank(permutation: &[usize]) -> usize { + let factorials = checked_factorials(permutation.len()).unwrap(); + permutation + .iter() + .enumerate() + .map(|(position, value)| { + let smaller = permutation[position + 1..] + .iter() + .filter(|other| *other < value) + .count(); + smaller * factorials[permutation.len() - position - 1] + }) + .sum() + } + + fn complex_multiply( + left: SymmetricGroupComplex64, + right: SymmetricGroupComplex64, + ) -> SymmetricGroupComplex64 { + ( + left.0 * right.0 - left.1 * right.1, + left.0 * right.1 + left.1 * right.0, + ) + } + + fn complex_norm(value: SymmetricGroupComplex64) -> f64 { + value.0.hypot(value.1) + } + + fn representation_matrix( + plan: &SymmetricGroupFftPlan, + block_index: usize, + permutation: &[usize], + ) -> Vec { + let irrep = &plan.levels[plan.degree()].irreps[block_index]; + let mut current = (0..plan.degree()).collect::>(); + let mut matrix = dense_identity(irrep.dimension); + for position in 0..plan.degree() { + let mut source = current[position..] + .iter() + .position(|value| *value == permutation[position]) + .unwrap() + + position; + while source > position { + let generator = source - 1; + current.swap(generator, generator + 1); + apply_dense_generator_on_right(irrep, generator, &mut matrix); + source -= 1; + } + } + assert_eq!(current, permutation); + matrix + } + + fn lexicographic_unrank(degree: usize, mut rank: usize) -> Vec { + let factorials = checked_factorials(degree).unwrap(); + let mut available = (0..degree).collect::>(); + let mut permutation = Vec::with_capacity(degree); + for position in 0..degree { + let block = factorials[degree - position - 1]; + let choice = rank / block; + rank %= block; + permutation.push(available.remove(choice)); + } + permutation + } + + fn dense_generator(irrep: &YoungIrrep, generator: usize) -> Vec { + let mut matrix = vec![0.0; irrep.dimension * irrep.dimension]; + for column in 0..irrep.dimension { + let action = irrep.generator(generator, column); + matrix[column * irrep.dimension + column] = action.diagonal; + if action.partner as usize != column { + matrix[column * irrep.dimension + action.partner as usize] = action.mixing; + } + } + matrix + } + + fn dense_identity(dimension: usize) -> Vec { + let mut matrix = vec![0.0; dimension * dimension]; + for index in 0..dimension { + matrix[index * dimension + index] = 1.0; + } + matrix + } + + fn apply_dense_generator_on_right(irrep: &YoungIrrep, generator: usize, matrix: &mut [f64]) { + for first in 0..irrep.dimension { + let first_action = irrep.generator(generator, first); + let other = first_action.partner as usize; + if other < first { + continue; + } + if other == first { + for row in 0..irrep.dimension { + matrix[first * irrep.dimension + row] *= first_action.diagonal; + } + continue; + } + let other_action = irrep.generator(generator, other); + for row in 0..irrep.dimension { + let first_index = first * irrep.dimension + row; + let other_index = other * irrep.dimension + row; + let first_value = matrix[first_index]; + let other_value = matrix[other_index]; + matrix[first_index] = + first_action.diagonal * first_value + first_action.mixing * other_value; + matrix[other_index] = + other_action.mixing * first_value + other_action.diagonal * other_value; + } + } + } + + fn dense_multiply(first: &[f64], second: &[f64], dimension: usize) -> Vec { + let mut product = vec![0.0; dimension * dimension]; + for column in 0..dimension { + for inner in 0..dimension { + let second_value = second[column * dimension + inner]; + for row in 0..dimension { + product[column * dimension + row] += + first[inner * dimension + row] * second_value; + } + } + } + product + } + + fn assert_complex_slices_close( + actual: &[SymmetricGroupComplex64], + expected: &[SymmetricGroupComplex64], + tolerance: f64, + ) { + assert_eq!(actual.len(), expected.len()); + let scale = expected + .iter() + .map(|value| value.0.abs().max(value.1.abs())) + .fold(1.0_f64, f64::max); + for (index, (actual, expected)) in actual.iter().zip(expected).enumerate() { + assert!( + (actual.0 - expected.0).abs() <= tolerance * scale + && (actual.1 - expected.1).abs() <= tolerance * scale, + "complex mismatch at {index}: {actual:?} != {expected:?}, scale {scale}" + ); + } + } + + fn assert_real_matrices_close(actual: &[f64], expected: &[f64], tolerance: f64) { + assert_eq!(actual.len(), expected.len()); + let scale = expected.iter().copied().map(f64::abs).fold(1.0, f64::max); + for (index, (actual, expected)) in actual.iter().zip(expected).enumerate() { + assert!( + (actual - expected).abs() <= tolerance * scale, + "matrix mismatch at {index}: {actual} != {expected}" + ); + } + } +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/recurrence/template.rs b/pyamplicol/rust/crates/rusticol-core/src/recurrence/template.rs new file mode 100644 index 0000000000..dedc07713a --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/recurrence/template.rs @@ -0,0 +1,4443 @@ +// SPDX-License-Identifier: 0BSD + +//! Checked model-wide input for the compact recurrence builder. +//! +//! The Python producer projects a semantic recurrence-template catalog into +//! fixed-width primitive columns. A Python decoder can zip those columns +//! directly into the row types below, construct [`OwnedRecurrenceTemplateInput`], +//! and call [`OwnedRecurrenceTemplateInput::validate`]. Validation resolves no +//! model-specific assumptions: it checks canonical catalogs, exact factors, +//! references, evaluator contracts, and propagator coverage before the state +//! builder is allowed to consume the input. + +use std::collections::{BTreeMap, BTreeSet}; + +use super::{ + CheckedTableRange, ExactComplexRational, LCColorComponentKind, LCColorComponentRole, + LCColorSourceSeed, LCColorSourceSeedOperation, RECURRENCE_TEMPLATE_ABI, SemanticDigest, + validate_concrete_spin_state, validate_packed_ranges, validate_u32_references, +}; +use crate::{RusticolError, RusticolResult}; + +pub const RECURRENCE_TEMPLATE_INPUT_ABI: &str = "pyamplicol-recurrence-template-input-v1"; +pub const RECURRENCE_TEMPLATE_INPUT_SCHEMA_VERSION: u32 = 2; +pub const RECURRENCE_TEMPLATE_CANONICALIZATION_ABI: &str = "pyamplicol-canonical-json-v1"; +pub const RECURRENCE_TEMPLATE_EXACT_SCALAR_ABI: &str = "pyamplicol-exact-complex-rational-v1"; +pub const MISSING_U32: u32 = u32::MAX; +const CONTACT_ORBIT_ALGORITHM: &str = "compiler-certified-contact-orbit"; +const CONTACT_ORBIT_ALGORITHM_VERSION: u32 = 2; +const CONTACT_ORBIT_EVALUATOR_CLASS: &str = "constant-scalar-literal-singlet-self-conjugate-boson"; +const HEFT_CONTACT_ORBIT_EVALUATOR_CLASS: &str = "scalar-heft-colored-contact-tree"; + +fn invalid(message: impl Into) -> RusticolError { + RusticolError::invalid_argument(message) +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[repr(u8)] +pub enum ParameterKind { + External = 0, + Derived = 1, + Constant = 2, +} + +impl TryFrom for ParameterKind { + type Error = RusticolError; + + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(Self::External), + 1 => Ok(Self::Derived), + 2 => Ok(Self::Constant), + _ => Err(invalid(format!("unsupported parameter kind {value}"))), + } + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[repr(u8)] +pub enum ParameterValueType { + Real = 0, + Complex = 1, +} + +impl TryFrom for ParameterValueType { + type Error = RusticolError; + + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(Self::Real), + 1 => Ok(Self::Complex), + _ => Err(invalid(format!("unsupported parameter value type {value}"))), + } + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[repr(u8)] +pub enum CurrentOrientation { + Particle = 0, + Antiparticle = 1, + SelfConjugate = 2, +} + +impl TryFrom for CurrentOrientation { + type Error = RusticolError; + + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(Self::Particle), + 1 => Ok(Self::Antiparticle), + 2 => Ok(Self::SelfConjugate), + _ => Err(invalid(format!("unsupported current orientation {value}"))), + } + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[repr(u8)] +pub enum ParticleStatistics { + Boson = 0, + Fermion = 1, +} + +impl TryFrom for ParticleStatistics { + type Error = RusticolError; + + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(Self::Boson), + 1 => Ok(Self::Fermion), + _ => Err(invalid(format!("unsupported particle statistics {value}"))), + } + } +} + +#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +#[repr(u8)] +pub enum EvaluatorContractKind { + Source = 0, + Vertex = 1, + Propagator = 2, + Closure = 3, + ModelParameter = 4, +} + +impl TryFrom for EvaluatorContractKind { + type Error = RusticolError; + + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(Self::Source), + 1 => Ok(Self::Vertex), + 2 => Ok(Self::Propagator), + 3 => Ok(Self::Closure), + 4 => Ok(Self::ModelParameter), + _ => Err(invalid(format!( + "unsupported evaluator contract kind {value}" + ))), + } + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[repr(u8)] +pub enum EvaluatorCallableKind { + PreparedKernel = 0, + RusticolTemplate = 1, +} + +impl TryFrom for EvaluatorCallableKind { + type Error = RusticolError; + + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(Self::PreparedKernel), + 1 => Ok(Self::RusticolTemplate), + _ => Err(invalid(format!( + "unsupported evaluator callable kind {value}" + ))), + } + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[repr(u8)] +pub enum OutputFactorSource { + None = 0, + CouplingReal = 1, + CouplingImag = 2, +} + +#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +#[repr(u8)] +pub enum ContactOrbitStage { + Partial = 0, + Final = 1, +} + +impl TryFrom for ContactOrbitStage { + type Error = RusticolError; + + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(Self::Partial), + 1 => Ok(Self::Final), + _ => Err(invalid(format!("unsupported contact-orbit stage {value}"))), + } + } +} + +impl TryFrom for OutputFactorSource { + type Error = RusticolError; + + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(Self::None), + 1 => Ok(Self::CouplingReal), + 2 => Ok(Self::CouplingImag), + _ => Err(invalid(format!("unsupported output-factor source {value}"))), + } + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct IndexedRangeRow { + pub id: u32, + pub range: CheckedTableRange, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct CatalogHeaderRow { + pub schema_version: u32, + pub abi_string_id: u32, + pub canonicalization_abi_string_id: u32, + pub exact_scalar_abi_string_id: u32, + pub compiled_model_digest_id: u32, + pub prepared_kernel_pack_digest_id: u32, + pub catalog_digest_id: u32, + pub parameter_count: u32, + pub current_state_count: u32, + pub source_count: u32, + pub quantum_flow_count: u32, + pub contact_orbit_certificate_count: u32, + pub contact_orbit_step_count: u32, + pub transition_count: u32, + pub propagator_count: u32, + pub closure_count: u32, + pub color_contraction_count: u32, + pub symmetry_proof_count: u32, + pub runtime_helicity_contract_count: u32, + pub evaluator_binding_count: u32, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct CouplingOrderTermRow { + pub set_id: u32, + pub name_string_id: u32, + pub power: u32, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct DigestCatalogRow { + pub id: u32, + pub value: [u8; 32], +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct ExactFactorRow { + pub id: u32, + pub real_numerator_string_id: u32, + pub real_denominator_string_id: u32, + pub imag_numerator_string_id: u32, + pub imag_denominator_string_id: u32, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct QuantumNumberFlowTermRow { + pub flow_id: u32, + pub name_string_id: u32, + pub expression_string_id: u32, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct ParameterRow { + pub id: u32, + pub template_string_id: u32, + pub name_string_id: u32, + pub kind: u8, + pub value_type: u8, + pub mutable: u8, + pub default_factor_id: u32, + pub exact_expression_digest_id: u32, + pub dependency_sequence_id: u32, + pub prepared_parameter_id: u32, + pub semantic_digest_id: u32, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct CurrentStateRow { + pub id: u32, + pub template_string_id: u32, + pub particle_id: i32, + pub anti_particle_id: i32, + pub species_string_id: u32, + pub orientation: u8, + pub statistics: u8, + pub color_representation: i32, + pub basis_string_id: u32, + pub tensor_ordering_sequence_id: u32, + pub dimension: u32, + pub chirality: i32, + pub lc_color_shape_string_id: u32, + pub auxiliary_kind_string_id: u32, + pub mass_parameter_id: u32, + pub width_parameter_id: u32, + pub semantic_digest_id: u32, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct SourceRow { + pub id: u32, + pub template_string_id: u32, + pub state_template_id: u32, + pub crossing_string_id: u32, + pub wavefunction_family_string_id: u32, + pub helicity: i32, + pub spin_state: i32, + pub flavour_flow_id: u32, + pub quantum_number_flow_id: u32, + pub lc_color_seed_operation: u8, + pub lc_color_seed_shape_string_id: u32, + pub lc_color_seed_component_kind: u8, + pub lc_color_seed_component_role: u8, + pub lc_color_seed_proof_digest_id: u32, + pub lc_color_seed_provenance_sequence_id: u32, + pub wavefunction_expression_digest_id: u32, + pub evaluator_binding_id: u32, + pub mass_parameter_id: u32, + pub width_parameter_id: u32, + pub semantic_digest_id: u32, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct RuntimeHelicityContractRow { + pub id: u32, + pub template_string_id: u32, + pub full_state_template_id: u32, + pub variant_range: CheckedTableRange, + pub proof_algorithm_string_id: u32, + pub proof_digest_id: u32, + pub semantic_digest_id: u32, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct RuntimeHelicityVariantRow { + pub id: u32, + pub contract_id: u32, + pub source_template_id: u32, + pub source_state_template_id: u32, + pub embedding_range: CheckedTableRange, + pub projection_range: CheckedTableRange, + pub proof_digest_id: u32, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct RuntimeHelicityEmbeddingRow { + pub variant_id: u32, + pub full_component: u32, + pub source_component: u32, + pub factor_id: u32, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct RuntimeHelicityProjectionRow { + pub variant_id: u32, + pub source_component: u32, + pub full_component: u32, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct QuantumFlowRow { + pub id: u32, + pub template_string_id: u32, + pub input_state_sequence_id: u32, + pub input_spin_sequence_id: u32, + pub input_flavour_sequence_id: u32, + pub input_quantum_sequence_id: u32, + pub flavour_flow_operation_string_id: u32, + pub quantum_number_flow_operation_string_id: u32, + pub coupling_order_set_id: u32, + pub result_state_template_id: u32, + pub result_spin_state: i32, + pub result_flavour_flow_id: u32, + pub result_quantum_number_flow_id: u32, + pub exact_coupling_factor_id: u32, + pub predicate_digest_id: u32, + pub semantic_digest_id: u32, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct ContactOrbitCertificateRow { + pub id: u32, + pub template_string_id: u32, + pub algorithm_string_id: u32, + pub algorithm_version: u32, + pub term_id: u32, + pub vertex_string_id: u32, + pub particle_string_sequence_id: u32, + pub evaluator_class_string_id: u32, + pub physical_leg_equivalence_sequence_id: u32, + pub reconstruction_factor_id: u32, + pub semantic_digest_id: u32, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct ContactOrbitStepRow { + pub id: u32, + pub template_string_id: u32, + pub certificate_id: u32, + pub stage: u8, + pub result_leg: u8, + pub left_covered_leg_sequence_id: u32, + pub right_covered_leg_sequence_id: u32, + pub source_particle_leg_sequence_id: u32, + pub reconstruction_factor_id: u32, + pub semantic_digest_id: u32, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct TransitionRow { + pub id: u32, + pub template_string_id: u32, + pub input_state_sequence_id: u32, + pub result_state_template_id: u32, + pub quantum_flow_template_id: u32, + pub evaluator_binding_id: u32, + pub canonical_input_order_sequence_id: u32, + pub momentum_convention_sequence_id: u32, + pub coupling_parameter_sequence_id: u32, + pub coupling_order_set_id: u32, + pub color_contraction_template_id: u32, + pub binding_coupling_factor_id: u32, + pub exact_factor_id: u32, + pub output_factor_source: u8, + pub equivalence_class_string_id: u32, + pub input_exchange_factor_id: u32, + pub output_projection_string_id: u32, + pub contact_orbit_step_sequence_id: u32, + pub contact_orbit_step_semantic_digest_sequence_id: u32, + pub semantic_digest_id: u32, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct PropagatorRow { + pub id: u32, + pub template_string_id: u32, + pub state_template_id: u32, + pub applies_propagator: u8, + pub evaluator_binding_id: u32, + pub numerator_expression_digest_id: u32, + pub denominator_expression_digest_id: u32, + pub mass_parameter_id: u32, + pub width_parameter_id: u32, + pub gauge_string_id: u32, + pub linearity_proof_template_id: u32, + pub semantic_digest_id: u32, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct ClosureRow { + pub id: u32, + pub template_string_id: u32, + pub input_state_sequence_id: u32, + pub result_state_template_id: u32, + pub evaluator_binding_id: u32, + pub canonical_input_order_sequence_id: u32, + pub coupling_parameter_sequence_id: u32, + pub coupling_order_set_id: u32, + pub eligible_quantum_flow_sequence_id: u32, + pub color_contraction_template_id: u32, + pub binding_coupling_factor_id: u32, + pub exact_factor_id: u32, + pub output_factor_source: u8, + pub equivalence_class_string_id: u32, + pub input_exchange_factor_id: u32, + pub projection_string_id: u32, + pub component_coefficient_sequence_id: u32, + pub chirality_relation_string_id: u32, + pub metric_signature_string_id: u32, + pub semantic_digest_id: u32, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct ColorContractionRow { + pub id: u32, + pub template_string_id: u32, + pub rule_kind_string_id: u32, + pub input_representation_sequence_id: u32, + pub has_output_representation: u8, + pub output_representation: i32, + pub ordered_open_string_arity: u32, + pub exact_coefficient_factor_id: u32, + pub witness_start: u64, + pub witness_count: u64, + pub nc_term_start: u64, + pub nc_term_count: u64, + pub expression_digest_id: u32, + pub semantic_digest_id: u32, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct LCColorTransitionWitnessRow { + pub color_contraction_id: u32, + pub ordinal: u32, + pub left_shape_string_id: u32, + pub right_shape_string_id: u32, + pub input_permutation: u8, + pub reverse_parent_mask: u8, + pub component_operation: u8, + pub result_component_kind: u8, + pub result_component_role: u8, + pub result_shape_string_id: u32, + pub exact_factor_id: u32, + pub proof_digest_id: u32, + pub input_port_pairing_sequence_id: u32, + pub result_port_binding_sequence_id: u32, + pub provenance_sequence_id: u32, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct ColorNcTermRow { + pub color_contraction_id: u32, + pub exponent: i32, + pub factor_id: u32, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct SymmetryProofRow { + pub id: u32, + pub template_string_id: u32, + pub proof_algorithm_string_id: u32, + pub subject_template_sequence_id: u32, + pub input_permutation_sequence_id: u32, + pub exact_phase_factor_id: u32, + pub expression_digest_sequence_id: u32, + pub witness_digest_id: u32, + pub semantic_digest_id: u32, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct EvaluatorBindingRow { + pub id: u32, + pub resolver_key_string_id: u32, + pub prepared_kernel_id: u32, + pub contract_kind: u8, + pub callable_signature_digest_id: u32, + pub input_state_sequence_id: u32, + pub output_state_template_id: u32, + pub input_layout_sequence_id: u32, + pub output_layout_sequence_id: u32, + pub exact_expression_digest_sequence_id: u32, + pub semantic_template_sequence_id: u32, + pub callable_kind: u8, + pub runtime_template_string_id: u32, + pub semantic_digest_id: u32, +} + +macro_rules! define_template_inputs { + ($( $field:ident : $item:ty ),+ $(,)?) => { + #[derive(Clone, Copy, Debug)] + pub struct RecurrenceTemplateInputView<'a> { + pub input_abi: &'a str, + pub catalog_digest: SemanticDigest, + pub compiled_model_digest: SemanticDigest, + pub prepared_kernel_pack_digest: SemanticDigest, + $(pub $field: &'a [$item],)+ + } + + #[derive(Clone, Debug)] + pub struct OwnedRecurrenceTemplateInput { + pub input_abi: String, + pub catalog_digest: SemanticDigest, + pub compiled_model_digest: SemanticDigest, + pub prepared_kernel_pack_digest: SemanticDigest, + $(pub $field: Vec<$item>,)+ + } + + impl OwnedRecurrenceTemplateInput { + pub fn as_view(&self) -> RecurrenceTemplateInputView<'_> { + RecurrenceTemplateInputView { + input_abi: &self.input_abi, + catalog_digest: self.catalog_digest, + compiled_model_digest: self.compiled_model_digest, + prepared_kernel_pack_digest: self.prepared_kernel_pack_digest, + $($field: &self.$field,)+ + } + } + + pub fn validate(self) -> RusticolResult { + let summary = self.as_view().validate()?; + Ok(ValidatedRecurrenceTemplateInput { input: self, summary }) + } + } + }; +} + +define_template_inputs! { + catalog_header: CatalogHeaderRow, + coupling_order_ranges: IndexedRangeRow, + coupling_order_terms: CouplingOrderTermRow, + contact_orbit_certificates: ContactOrbitCertificateRow, + contact_orbit_steps: ContactOrbitStepRow, + current_states: CurrentStateRow, + digest_catalog: DigestCatalogRow, + evaluator_bindings: EvaluatorBindingRow, + exact_factors: ExactFactorRow, + flavour_flow_ranges: IndexedRangeRow, + flavour_flow_values: i32, + i32_sequence_ranges: IndexedRangeRow, + i32_sequence_values: i32, + parameters: ParameterRow, + propagators: PropagatorRow, + quantum_flows: QuantumFlowRow, + quantum_number_flow_ranges: IndexedRangeRow, + quantum_number_flow_terms: QuantumNumberFlowTermRow, + runtime_helicity_contracts: RuntimeHelicityContractRow, + runtime_helicity_variants: RuntimeHelicityVariantRow, + runtime_helicity_embeddings: RuntimeHelicityEmbeddingRow, + runtime_helicity_projections: RuntimeHelicityProjectionRow, + sources: SourceRow, + string_ranges: CheckedTableRange, + string_bytes: u8, + symmetry_proofs: SymmetryProofRow, + transitions: TransitionRow, + closures: ClosureRow, + color_contractions: ColorContractionRow, + lc_color_transition_witnesses: LCColorTransitionWitnessRow, + color_nc_terms: ColorNcTermRow, + u32_sequence_ranges: IndexedRangeRow, + u32_sequence_values: u32, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct RecurrenceTemplateValidationSummary { + pub catalog_digest: SemanticDigest, + pub compiled_model_digest: SemanticDigest, + pub prepared_kernel_pack_digest: SemanticDigest, + pub parameter_count: u32, + pub current_state_count: u32, + pub source_count: u32, + pub quantum_flow_count: u32, + pub contact_orbit_certificate_count: u32, + pub contact_orbit_step_count: u32, + pub transition_count: u32, + pub propagator_count: u32, + pub closure_count: u32, + pub color_contraction_count: u32, + pub lc_color_transition_witness_count: u32, + pub symmetry_proof_count: u32, + pub runtime_helicity_contract_count: u32, + pub evaluator_binding_count: u32, + pub prepared_kernel_count: u32, +} + +#[derive(Clone, Debug)] +pub struct ValidatedRecurrenceTemplateInput { + input: OwnedRecurrenceTemplateInput, + summary: RecurrenceTemplateValidationSummary, +} + +/// Stable semantic section names used by process/template cross-authentication. +#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub enum RecurrenceSemanticTemplateKind { + Parameter, + CurrentState, + Source, + QuantumFlow, + ContactOrbitCertificate, + ContactOrbitStep, + Transition, + Propagator, + Closure, + ColorContraction, + SymmetryProof, +} + +impl RecurrenceSemanticTemplateKind { + pub const fn as_str(self) -> &'static str { + match self { + Self::Parameter => "parameter", + Self::CurrentState => "current-state", + Self::Source => "source", + Self::QuantumFlow => "quantum-flow", + Self::ContactOrbitCertificate => "contact-orbit-certificate", + Self::ContactOrbitStep => "contact-orbit-step", + Self::Transition => "transition", + Self::Propagator => "propagator", + Self::Closure => "closure", + Self::ColorContraction => "color-contraction", + Self::SymmetryProof => "symmetry-proof", + } + } + + pub fn parse(value: &str) -> RusticolResult { + match value { + "parameter" => Ok(Self::Parameter), + "current-state" => Ok(Self::CurrentState), + "source" => Ok(Self::Source), + "quantum-flow" => Ok(Self::QuantumFlow), + "contact-orbit-certificate" => Ok(Self::ContactOrbitCertificate), + "contact-orbit-step" => Ok(Self::ContactOrbitStep), + "transition" => Ok(Self::Transition), + "propagator" => Ok(Self::Propagator), + "closure" => Ok(Self::Closure), + "color-contraction" => Ok(Self::ColorContraction), + "symmetry-proof" => Ok(Self::SymmetryProof), + _ => Err(invalid(format!( + "unsupported recurrence semantic-template kind {value:?}" + ))), + } + } +} + +/// One authenticated model-wide semantic record addressable by process input. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct RecurrenceSemanticTemplateIdentity { + pub kind: RecurrenceSemanticTemplateKind, + pub template_id: u32, + pub semantic_digest: SemanticDigest, + pub prepared_kernel_id: Option, +} + +/// Bounded index used to bind one process input to its prepared model catalog. +#[derive(Clone, Debug)] +pub struct RecurrenceTemplateSemanticIndex { + pub compiled_model_digest: SemanticDigest, + pub catalog_digest: SemanticDigest, + records: BTreeMap<(RecurrenceSemanticTemplateKind, u32), RecurrenceSemanticTemplateIdentity>, +} + +impl RecurrenceTemplateSemanticIndex { + pub fn record( + &self, + kind: RecurrenceSemanticTemplateKind, + template_id: u32, + ) -> Option { + self.records.get(&(kind, template_id)).copied() + } + + pub fn len(&self) -> usize { + self.records.len() + } + + pub fn is_empty(&self) -> bool { + self.records.is_empty() + } +} + +impl ValidatedRecurrenceTemplateInput { + pub const fn summary(&self) -> RecurrenceTemplateValidationSummary { + self.summary + } + + pub const fn input(&self) -> &OwnedRecurrenceTemplateInput { + &self.input + } + + pub fn into_input(self) -> OwnedRecurrenceTemplateInput { + self.input + } + + /// Return the authenticated kind/default contract for every parameter. + /// + /// Numerical recurrence probes use this model-owned catalog to anchor + /// mutable runtime defaults. Derived parameters deliberately retain no + /// semantic probe default because their prepared slots are overwritten by + /// the authenticated parameter-derivation evaluator. + pub fn parameter_kind_and_default_factors( + &self, + ) -> RusticolResult)>> { + let input = self.input.as_view(); + let catalogs = input.validate_catalogs()?; + input + .parameters + .iter() + .map(|row| { + Ok(( + ParameterKind::try_from(row.kind)?, + optional_factor( + &catalogs.factors, + row.default_factor_id, + "parameter default", + )? + .copied(), + )) + }) + .collect() + } + + pub(crate) fn closure_component_coefficient_catalog( + &self, + ) -> RusticolResult>> { + let input = self.input.as_view(); + let catalogs = input.validate_catalogs()?; + input + .closures + .iter() + .enumerate() + .map(|(expected_id, row)| { + let expected_id = u32::try_from(expected_id) + .map_err(|_| invalid("closure template count exceeds u32"))?; + if row.id != expected_id { + return Err(invalid(format!( + "closure template {expected_id} has noncanonical ID {}", + row.id + ))); + } + u32_sequence( + input, + row.component_coefficient_sequence_id, + "closure component coefficients", + )? + .iter() + .map(|factor_id| { + catalogs + .factors + .get(*factor_id as usize) + .copied() + .ok_or_else(|| invalid("closure component factor ID is out of range")) + }) + .collect::>>() + .map(Vec::into_boxed_slice) + }) + .collect() + } + + pub fn semantic_index(&self) -> RusticolResult { + let input = self.input.as_view(); + let catalogs = input.validate_catalogs()?; + + let mut prepared_kernel_by_template_string_id = BTreeMap::new(); + for binding in input.evaluator_bindings { + let prepared_kernel_id = + (binding.prepared_kernel_id != MISSING_U32).then_some(binding.prepared_kernel_id); + for template_string_id in u32_sequence( + input, + binding.semantic_template_sequence_id, + "evaluator semantic templates", + )? { + if let Some(previous) = prepared_kernel_by_template_string_id + .insert(*template_string_id, prepared_kernel_id) + && previous != prepared_kernel_id + { + return Err(invalid( + "one semantic template has inconsistent prepared-kernel ownership", + )); + } + } + } + + let mut records = BTreeMap::new(); + macro_rules! register { + ($kind:expr, $rows:expr) => { + for row in $rows { + let semantic_digest = *catalogs + .digests + .get(row.semantic_digest_id as usize) + .ok_or_else(|| invalid("semantic-template digest ID is out of range"))?; + let identity = RecurrenceSemanticTemplateIdentity { + kind: $kind, + template_id: row.id, + semantic_digest, + prepared_kernel_id: prepared_kernel_by_template_string_id + .get(&row.template_string_id) + .copied() + .flatten(), + }; + if records.insert(($kind, row.id), identity).is_some() { + return Err(invalid(format!( + "duplicate {} recurrence template ID {}", + $kind.as_str(), + row.id + ))); + } + } + }; + } + register!(RecurrenceSemanticTemplateKind::Parameter, input.parameters); + register!( + RecurrenceSemanticTemplateKind::CurrentState, + input.current_states + ); + register!(RecurrenceSemanticTemplateKind::Source, input.sources); + register!( + RecurrenceSemanticTemplateKind::QuantumFlow, + input.quantum_flows + ); + register!( + RecurrenceSemanticTemplateKind::ContactOrbitCertificate, + input.contact_orbit_certificates + ); + register!( + RecurrenceSemanticTemplateKind::ContactOrbitStep, + input.contact_orbit_steps + ); + register!( + RecurrenceSemanticTemplateKind::Transition, + input.transitions + ); + register!( + RecurrenceSemanticTemplateKind::Propagator, + input.propagators + ); + register!(RecurrenceSemanticTemplateKind::Closure, input.closures); + register!( + RecurrenceSemanticTemplateKind::ColorContraction, + input.color_contractions + ); + register!( + RecurrenceSemanticTemplateKind::SymmetryProof, + input.symmetry_proofs + ); + + Ok(RecurrenceTemplateSemanticIndex { + compiled_model_digest: self.summary.compiled_model_digest, + catalog_digest: self.summary.catalog_digest, + records, + }) + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum TemplateKind { + Parameter, + CurrentState, + Source, + RuntimeHelicity, + QuantumFlow, + ContactOrbitCertificate, + ContactOrbitStep, + Transition, + Propagator, + Closure, + ColorContraction, + SymmetryProof, +} + +impl TemplateKind { + const fn evaluator_contract(self) -> Option { + match self { + Self::Parameter => Some(EvaluatorContractKind::ModelParameter), + Self::Source => Some(EvaluatorContractKind::Source), + Self::Transition => Some(EvaluatorContractKind::Vertex), + Self::Propagator => Some(EvaluatorContractKind::Propagator), + Self::Closure => Some(EvaluatorContractKind::Closure), + Self::CurrentState + | Self::RuntimeHelicity + | Self::QuantumFlow + | Self::ContactOrbitCertificate + | Self::ContactOrbitStep + | Self::ColorContraction + | Self::SymmetryProof => None, + } + } +} + +struct ValidatedCatalogs<'a> { + strings: Vec<&'a str>, + digests: Vec, + factors: Vec, +} + +#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] +struct CallableContractKey { + contract_kind: EvaluatorContractKind, + callable_signature_digest_id: u32, + input_layout: Vec, + output_layout: Vec, + exact_expression_digests: Vec, +} + +impl<'a> RecurrenceTemplateInputView<'a> { + pub fn validate(self) -> RusticolResult { + if self.input_abi != RECURRENCE_TEMPLATE_INPUT_ABI { + return Err(RusticolError::compatibility(format!( + "unsupported recurrence template input ABI {:?}; expected {:?}", + self.input_abi, RECURRENCE_TEMPLATE_INPUT_ABI + ))); + } + let catalogs = self.validate_catalogs()?; + self.validate_header(&catalogs)?; + + let mut template_kinds = BTreeMap::new(); + let mut semantic_digests = BTreeSet::new(); + self.validate_parameters(&catalogs, &mut template_kinds, &mut semantic_digests)?; + self.validate_current_states(&catalogs, &mut template_kinds, &mut semantic_digests)?; + self.validate_sources_basic(&catalogs, &mut template_kinds, &mut semantic_digests)?; + self.validate_runtime_helicity_contracts( + &catalogs, + &mut template_kinds, + &mut semantic_digests, + )?; + self.validate_quantum_flows(&catalogs, &mut template_kinds, &mut semantic_digests)?; + self.validate_contact_orbit_records(&catalogs, &mut template_kinds, &mut semantic_digests)?; + self.validate_color_contractions(&catalogs, &mut template_kinds, &mut semantic_digests)?; + self.validate_symmetry_proofs_basic(&catalogs, &mut template_kinds, &mut semantic_digests)?; + self.validate_transitions_basic(&catalogs, &mut template_kinds, &mut semantic_digests)?; + self.validate_propagators_basic(&catalogs, &mut template_kinds, &mut semantic_digests)?; + self.validate_closures_basic(&catalogs, &mut template_kinds, &mut semantic_digests)?; + let prepared_kernel_count = + self.validate_evaluators(&catalogs, &template_kinds, &mut semantic_digests)?; + self.validate_proof_subjects(&template_kinds)?; + self.validate_evaluator_state_contracts(&template_kinds)?; + + Ok(RecurrenceTemplateValidationSummary { + catalog_digest: self.catalog_digest, + compiled_model_digest: self.compiled_model_digest, + prepared_kernel_pack_digest: self.prepared_kernel_pack_digest, + parameter_count: checked_len(self.parameters.len(), "parameters")?, + current_state_count: checked_len(self.current_states.len(), "current states")?, + source_count: checked_len(self.sources.len(), "sources")?, + quantum_flow_count: checked_len(self.quantum_flows.len(), "quantum flows")?, + contact_orbit_certificate_count: checked_len( + self.contact_orbit_certificates.len(), + "contact-orbit certificates", + )?, + contact_orbit_step_count: checked_len( + self.contact_orbit_steps.len(), + "contact-orbit steps", + )?, + transition_count: checked_len(self.transitions.len(), "transitions")?, + propagator_count: checked_len(self.propagators.len(), "propagators")?, + closure_count: checked_len(self.closures.len(), "closures")?, + color_contraction_count: checked_len( + self.color_contractions.len(), + "color contractions", + )?, + lc_color_transition_witness_count: checked_len( + self.lc_color_transition_witnesses.len(), + "LC color transition witnesses", + )?, + symmetry_proof_count: checked_len(self.symmetry_proofs.len(), "symmetry proofs")?, + runtime_helicity_contract_count: checked_len( + self.runtime_helicity_contracts.len(), + "runtime-helicity contracts", + )?, + evaluator_binding_count: checked_len( + self.evaluator_bindings.len(), + "evaluator bindings", + )?, + prepared_kernel_count, + }) + } + + fn validate_catalogs(self) -> RusticolResult> { + validate_packed_ranges( + "string catalog", + self.string_ranges, + self.string_bytes.len(), + )?; + let mut strings = Vec::with_capacity(self.string_ranges.len()); + let mut previous = None; + for (index, range) in self.string_ranges.iter().copied().enumerate() { + let bytes = &self.string_bytes + [range.as_usize_range(self.string_bytes.len(), &format!("string {index}"))?]; + let value = std::str::from_utf8(bytes) + .map_err(|error| invalid(format!("string {index} is not UTF-8: {error}")))?; + if value.is_empty() { + return Err(invalid(format!("string catalog row {index} is empty"))); + } + if let Some(previous) = previous + && previous >= value + { + return Err(invalid(format!( + "string catalog is not in strict canonical order at row {index}" + ))); + } + previous = Some(value); + strings.push(value); + } + + validate_canonical_ids( + "digest catalog", + self.digest_catalog.iter().map(|row| row.id), + )?; + let mut digests = Vec::with_capacity(self.digest_catalog.len()); + let mut previous = None; + for (index, row) in self.digest_catalog.iter().enumerate() { + let value = SemanticDigest::new(row.value).map_err(|error| { + invalid(format!("digest catalog row {index} is invalid: {error}")) + })?; + if let Some(previous) = previous + && previous >= value + { + return Err(invalid(format!( + "digest catalog is not in strict canonical order at row {index}" + ))); + } + previous = Some(value); + digests.push(value); + } + + self.validate_sequence_catalogs(&strings)?; + let factors = self.validate_exact_factors(&strings)?; + Ok(ValidatedCatalogs { + strings, + digests, + factors, + }) + } + + fn validate_sequence_catalogs(self, strings: &[&str]) -> RusticolResult<()> { + validate_sequence_catalog( + "u32 sequence catalog", + self.u32_sequence_ranges, + self.u32_sequence_values, + false, + )?; + validate_sequence_catalog( + "i32 sequence catalog", + self.i32_sequence_ranges, + self.i32_sequence_values, + false, + )?; + validate_sequence_catalog( + "flavour-flow catalog", + self.flavour_flow_ranges, + self.flavour_flow_values, + true, + )?; + self.validate_coupling_order_catalog(strings)?; + self.validate_quantum_number_flow_catalog(strings)?; + Ok(()) + } + + fn validate_coupling_order_catalog(self, strings: &[&str]) -> RusticolResult<()> { + validate_indexed_ranges( + "coupling-order catalog", + self.coupling_order_ranges, + self.coupling_order_terms.len(), + )?; + let mut previous: Option> = None; + for (set_index, range) in self.coupling_order_ranges.iter().enumerate() { + let terms = &self.coupling_order_terms[range + .range + .as_usize_range(self.coupling_order_terms.len(), "coupling-order set")?]; + let mut current = Vec::with_capacity(terms.len()); + let mut previous_name = None; + for term in terms { + if term.set_id as usize != set_index { + return Err(invalid(format!( + "coupling-order term names set {}, expected {set_index}", + term.set_id + ))); + } + required_string(strings, term.name_string_id, "coupling-order name")?; + if let Some(previous_name) = previous_name + && previous_name >= term.name_string_id + { + return Err(invalid(format!( + "coupling-order set {set_index} is not strictly name ordered" + ))); + } + previous_name = Some(term.name_string_id); + current.push((term.name_string_id, term.power)); + } + if let Some(previous) = previous.as_ref() + && previous.as_slice() >= current.as_slice() + { + return Err(invalid(format!( + "coupling-order catalog is not in strict canonical order at set {set_index}" + ))); + } + previous = Some(current); + } + Ok(()) + } + + fn validate_quantum_number_flow_catalog(self, strings: &[&str]) -> RusticolResult<()> { + validate_indexed_ranges( + "quantum-number-flow catalog", + self.quantum_number_flow_ranges, + self.quantum_number_flow_terms.len(), + )?; + let mut previous: Option> = None; + for (flow_index, range) in self.quantum_number_flow_ranges.iter().enumerate() { + let terms = &self.quantum_number_flow_terms[range + .range + .as_usize_range(self.quantum_number_flow_terms.len(), "quantum-number flow")?]; + let mut current = Vec::with_capacity(terms.len()); + let mut previous_name = None; + for term in terms { + if term.flow_id as usize != flow_index { + return Err(invalid(format!( + "quantum-number-flow term names flow {}, expected {flow_index}", + term.flow_id + ))); + } + required_string(strings, term.name_string_id, "quantum-number name")?; + required_string( + strings, + term.expression_string_id, + "quantum-number expression", + )?; + if let Some(previous_name) = previous_name + && previous_name >= term.name_string_id + { + return Err(invalid(format!( + "quantum-number flow {flow_index} is not strictly name ordered" + ))); + } + previous_name = Some(term.name_string_id); + current.push((term.name_string_id, term.expression_string_id)); + } + if let Some(previous) = previous.as_ref() + && previous.as_slice() >= current.as_slice() + { + return Err(invalid(format!( + "quantum-number-flow catalog is not in strict canonical order at flow {flow_index}" + ))); + } + previous = Some(current); + } + Ok(()) + } + + fn validate_exact_factors(self, strings: &[&str]) -> RusticolResult> { + validate_canonical_ids( + "exact-factor catalog", + self.exact_factors.iter().map(|row| row.id), + )?; + let mut factors = Vec::with_capacity(self.exact_factors.len()); + let mut previous = None; + for (index, row) in self.exact_factors.iter().enumerate() { + let real_numerator = required_string( + strings, + row.real_numerator_string_id, + "exact real numerator", + )?; + let real_denominator = required_string( + strings, + row.real_denominator_string_id, + "exact real denominator", + )?; + let imag_numerator = required_string( + strings, + row.imag_numerator_string_id, + "exact imaginary numerator", + )?; + let imag_denominator = required_string( + strings, + row.imag_denominator_string_id, + "exact imaginary denominator", + )?; + let raw_key = ( + parse_canonical_i128(real_numerator, false, "exact real numerator")?, + parse_canonical_i128(real_denominator, true, "exact real denominator")?, + parse_canonical_i128(imag_numerator, false, "exact imaginary numerator")?, + parse_canonical_i128(imag_denominator, true, "exact imaginary denominator")?, + ); + let factor = ExactComplexRational::parse_parts( + real_numerator, + real_denominator, + imag_numerator, + imag_denominator, + ) + .map_err(|error| invalid(format!("exact factor {index} is invalid: {error}")))?; + if factor.real().numerator() != raw_key.0 + || factor.real().denominator() != raw_key.1 + || factor.imag().numerator() != raw_key.2 + || factor.imag().denominator() != raw_key.3 + { + return Err(invalid(format!( + "exact factor {index} is not reduced canonically" + ))); + } + if let Some(previous) = previous + && previous >= raw_key + { + return Err(invalid(format!( + "exact-factor catalog is not in strict canonical order at row {index}" + ))); + } + previous = Some(raw_key); + factors.push(factor); + } + Ok(factors) + } + + fn validate_header(self, catalogs: &ValidatedCatalogs<'_>) -> RusticolResult<()> { + if self.catalog_header.len() != 1 { + return Err(invalid(format!( + "catalog_header must contain one row, found {}", + self.catalog_header.len() + ))); + } + let header = self.catalog_header[0]; + if header.schema_version != RECURRENCE_TEMPLATE_INPUT_SCHEMA_VERSION { + return Err(RusticolError::compatibility(format!( + "unsupported recurrence template input schema {}; expected {}", + header.schema_version, RECURRENCE_TEMPLATE_INPUT_SCHEMA_VERSION + ))); + } + require_string_value( + &catalogs.strings, + header.abi_string_id, + RECURRENCE_TEMPLATE_ABI, + "recurrence template ABI", + )?; + require_string_value( + &catalogs.strings, + header.canonicalization_abi_string_id, + RECURRENCE_TEMPLATE_CANONICALIZATION_ABI, + "recurrence canonicalization ABI", + )?; + require_string_value( + &catalogs.strings, + header.exact_scalar_abi_string_id, + RECURRENCE_TEMPLATE_EXACT_SCALAR_ABI, + "recurrence exact-scalar ABI", + )?; + if required_digest( + &catalogs.digests, + header.compiled_model_digest_id, + "compiled-model digest", + )? != self.compiled_model_digest + { + return Err(invalid( + "compiled-model digest does not match the input envelope", + )); + } + if required_digest( + &catalogs.digests, + header.prepared_kernel_pack_digest_id, + "prepared-kernel-pack digest", + )? != self.prepared_kernel_pack_digest + { + return Err(invalid( + "prepared-kernel-pack digest does not match the input envelope", + )); + } + if required_digest( + &catalogs.digests, + header.catalog_digest_id, + "catalog digest", + )? != self.catalog_digest + { + return Err(invalid("catalog digest does not match the input envelope")); + } + + let expected = [ + ("parameters", header.parameter_count, self.parameters.len()), + ( + "current states", + header.current_state_count, + self.current_states.len(), + ), + ("sources", header.source_count, self.sources.len()), + ( + "quantum flows", + header.quantum_flow_count, + self.quantum_flows.len(), + ), + ( + "contact-orbit certificates", + header.contact_orbit_certificate_count, + self.contact_orbit_certificates.len(), + ), + ( + "contact-orbit steps", + header.contact_orbit_step_count, + self.contact_orbit_steps.len(), + ), + ( + "transitions", + header.transition_count, + self.transitions.len(), + ), + ( + "propagators", + header.propagator_count, + self.propagators.len(), + ), + ("closures", header.closure_count, self.closures.len()), + ( + "color contractions", + header.color_contraction_count, + self.color_contractions.len(), + ), + ( + "symmetry proofs", + header.symmetry_proof_count, + self.symmetry_proofs.len(), + ), + ( + "runtime-helicity contracts", + header.runtime_helicity_contract_count, + self.runtime_helicity_contracts.len(), + ), + ( + "evaluator bindings", + header.evaluator_binding_count, + self.evaluator_bindings.len(), + ), + ]; + for (label, declared, actual) in expected { + if usize::try_from(declared).ok() != Some(actual) { + return Err(invalid(format!( + "catalog header declares {declared} {label}, found {actual}" + ))); + } + } + Ok(()) + } + + fn validate_parameters( + self, + catalogs: &ValidatedCatalogs<'_>, + template_kinds: &mut BTreeMap, + semantic_digests: &mut BTreeSet, + ) -> RusticolResult<()> { + validate_record_ids( + "parameter", + self.parameters.iter().map(|row| row.id), + self.parameters.iter().map(|row| row.template_string_id), + )?; + for (index, row) in self.parameters.iter().enumerate() { + register_template( + template_kinds, + row.template_string_id, + TemplateKind::Parameter, + "parameter", + )?; + register_semantic_digest(semantic_digests, row.semantic_digest_id, "parameter")?; + required_string( + &catalogs.strings, + row.template_string_id, + "parameter template", + )?; + required_string(&catalogs.strings, row.name_string_id, "parameter name")?; + required_digest( + &catalogs.digests, + row.semantic_digest_id, + "parameter semantic digest", + )?; + let kind = ParameterKind::try_from(row.kind)?; + let value_type = ParameterValueType::try_from(row.value_type)?; + validate_bool(row.mutable, "parameter mutable")?; + let default = optional_factor( + &catalogs.factors, + row.default_factor_id, + "parameter default", + )?; + optional_digest( + &catalogs.digests, + row.exact_expression_digest_id, + "parameter exact expression", + )?; + let dependencies = + u32_sequence(self, row.dependency_sequence_id, "parameter dependencies")?; + validate_strict_u32(dependencies, "parameter dependencies")?; + validate_u32_references( + dependencies, + self.parameters.len(), + "parameter dependencies", + )?; + if dependencies.contains(&(index as u32)) { + return Err(invalid(format!( + "parameter {index} depends directly on itself" + ))); + } + match kind { + ParameterKind::Derived => { + if row.exact_expression_digest_id == MISSING_U32 { + return Err(invalid(format!( + "derived parameter {index} has no exact expression digest" + ))); + } + } + ParameterKind::External | ParameterKind::Constant => { + if default.is_none() { + return Err(invalid(format!( + "external/constant parameter {index} has no exact default" + ))); + } + } + } + if value_type == ParameterValueType::Real + && default.is_some_and(|value| !value.imag().is_zero()) + { + return Err(invalid(format!( + "real parameter {index} has a complex default" + ))); + } + } + self.validate_parameter_dependency_graph()?; + Ok(()) + } + + fn validate_parameter_dependency_graph(self) -> RusticolResult<()> { + let mut indegree = vec![0usize; self.parameters.len()]; + let mut dependents = vec![Vec::::new(); self.parameters.len()]; + for (index, row) in self.parameters.iter().enumerate() { + let dependencies = + u32_sequence(self, row.dependency_sequence_id, "parameter dependencies")?; + indegree[index] = dependencies.len(); + for dependency in dependencies { + dependents[*dependency as usize].push(index); + } + } + let mut ready = indegree + .iter() + .enumerate() + .filter_map(|(index, count)| (*count == 0).then_some(index)) + .collect::>(); + let mut visited = 0usize; + while let Some(index) = ready.pop() { + visited += 1; + for dependent in &dependents[index] { + indegree[*dependent] -= 1; + if indegree[*dependent] == 0 { + ready.push(*dependent); + } + } + } + if visited != self.parameters.len() { + let cyclic = indegree + .iter() + .enumerate() + .filter_map(|(index, count)| (*count > 0).then_some(index.to_string())) + .collect::>() + .join(", "); + return Err(invalid(format!( + "parameter dependency graph contains a cycle involving: {cyclic}" + ))); + } + Ok(()) + } + + fn validate_current_states( + self, + catalogs: &ValidatedCatalogs<'_>, + template_kinds: &mut BTreeMap, + semantic_digests: &mut BTreeSet, + ) -> RusticolResult<()> { + validate_record_ids( + "current state", + self.current_states.iter().map(|row| row.id), + self.current_states.iter().map(|row| row.template_string_id), + )?; + for (index, row) in self.current_states.iter().enumerate() { + register_template( + template_kinds, + row.template_string_id, + TemplateKind::CurrentState, + "current state", + )?; + register_semantic_digest(semantic_digests, row.semantic_digest_id, "current state")?; + for (id, label) in [ + (row.template_string_id, "current-state template"), + (row.species_string_id, "current-state species"), + (row.basis_string_id, "current-state basis"), + ] { + required_string(&catalogs.strings, id, label)?; + } + let color_shape = required_string( + &catalogs.strings, + row.lc_color_shape_string_id, + "current-state LC color shape", + )?; + validate_lc_color_shape( + color_shape, + row.color_representation, + &format!("current state {index}"), + )?; + optional_string( + &catalogs.strings, + row.auxiliary_kind_string_id, + "current-state auxiliary kind", + )?; + CurrentOrientation::try_from(row.orientation)?; + ParticleStatistics::try_from(row.statistics)?; + if row.dimension == 0 { + return Err(invalid(format!("current state {index} has zero dimension"))); + } + let tensor_ordering = u32_sequence( + self, + row.tensor_ordering_sequence_id, + "current tensor ordering", + )?; + if tensor_ordering.len() != row.dimension as usize { + return Err(invalid(format!( + "current state {index} dimension {} does not match tensor ordering length {}", + row.dimension, + tensor_ordering.len() + ))); + } + for string_id in tensor_ordering { + required_string(&catalogs.strings, *string_id, "current tensor label")?; + } + optional_reference( + row.mass_parameter_id, + self.parameters.len(), + "current mass parameter", + )?; + optional_reference( + row.width_parameter_id, + self.parameters.len(), + "current width parameter", + )?; + required_digest( + &catalogs.digests, + row.semantic_digest_id, + "current-state semantic digest", + )?; + } + Ok(()) + } + + fn validate_sources_basic( + self, + catalogs: &ValidatedCatalogs<'_>, + template_kinds: &mut BTreeMap, + semantic_digests: &mut BTreeSet, + ) -> RusticolResult<()> { + validate_record_ids( + "source", + self.sources.iter().map(|row| row.id), + self.sources.iter().map(|row| row.template_string_id), + )?; + for row in self.sources { + validate_concrete_spin_state(row.spin_state, "source template spin state")?; + register_template( + template_kinds, + row.template_string_id, + TemplateKind::Source, + "source", + )?; + register_semantic_digest(semantic_digests, row.semantic_digest_id, "source")?; + required_reference( + row.state_template_id, + self.current_states.len(), + "source state", + )?; + required_reference( + row.evaluator_binding_id, + self.evaluator_bindings.len(), + "source evaluator", + )?; + required_reference( + row.flavour_flow_id, + self.flavour_flow_ranges.len(), + "source flavour flow", + )?; + required_reference( + row.quantum_number_flow_id, + self.quantum_number_flow_ranges.len(), + "source quantum-number flow", + )?; + let state = self.current_states[row.state_template_id as usize]; + if row.lc_color_seed_shape_string_id != state.lc_color_shape_string_id { + return Err(invalid( + "source LC color seed shape does not match its current state", + )); + } + let seed_shape = required_string( + &catalogs.strings, + row.lc_color_seed_shape_string_id, + "source LC color seed shape", + )?; + validate_lc_color_shape( + seed_shape, + state.color_representation, + "source LC color seed", + )?; + let seed_operation = LCColorSourceSeedOperation::try_from(row.lc_color_seed_operation)?; + let seed_component_kind = if row.lc_color_seed_component_kind == u8::MAX { + None + } else { + Some(LCColorComponentKind::try_from( + row.lc_color_seed_component_kind, + )?) + }; + let seed_component_role = + LCColorComponentRole::try_from(row.lc_color_seed_component_role)?; + let expected_seed = match seed_shape { + "singlet-forest" => ( + LCColorSourceSeedOperation::Empty, + None, + LCColorComponentRole::None, + ), + "fundamental-open-string" | "antifundamental-open-string" => ( + LCColorSourceSeedOperation::Singleton, + Some(LCColorComponentKind::OpenString), + LCColorComponentRole::Active, + ), + "adjoint-segment" => ( + LCColorSourceSeedOperation::Singleton, + Some(LCColorComponentKind::AdjointSegment), + LCColorComponentRole::Active, + ), + _ => unreachable!("validated LC color shape"), + }; + if (seed_operation, seed_component_kind, seed_component_role) != expected_seed { + return Err(invalid( + "source LC color seed does not match its certified output shape", + )); + } + let seed_proof = required_digest( + &catalogs.digests, + row.lc_color_seed_proof_digest_id, + "source LC color seed proof", + )?; + LCColorSourceSeed::new( + seed_operation, + row.lc_color_seed_shape_string_id, + seed_component_kind, + seed_component_role, + seed_proof, + )?; + let seed_provenance = u32_sequence( + self, + row.lc_color_seed_provenance_sequence_id, + "source LC color seed provenance", + )?; + validate_string_pair_sequence( + &catalogs.strings, + seed_provenance, + "source LC color seed provenance", + )?; + optional_reference( + row.mass_parameter_id, + self.parameters.len(), + "source mass parameter", + )?; + optional_reference( + row.width_parameter_id, + self.parameters.len(), + "source width parameter", + )?; + required_string(&catalogs.strings, row.template_string_id, "source template")?; + required_string(&catalogs.strings, row.crossing_string_id, "source crossing")?; + let family = required_string( + &catalogs.strings, + row.wavefunction_family_string_id, + "source wavefunction family", + )?; + if !matches!( + family, + "scalar" | "fermion" | "vector" | "spin2" | "ghost" | "auxiliary" + ) { + return Err(invalid(format!( + "unsupported source wavefunction family {family:?}" + ))); + } + required_digest( + &catalogs.digests, + row.wavefunction_expression_digest_id, + "source wavefunction expression", + )?; + required_digest( + &catalogs.digests, + row.semantic_digest_id, + "source semantic digest", + )?; + } + Ok(()) + } + + fn validate_runtime_helicity_contracts( + self, + catalogs: &ValidatedCatalogs<'_>, + template_kinds: &mut BTreeMap, + semantic_digests: &mut BTreeSet, + ) -> RusticolResult<()> { + validate_record_ids( + "runtime-helicity contract", + self.runtime_helicity_contracts.iter().map(|row| row.id), + self.runtime_helicity_contracts + .iter() + .map(|row| row.template_string_id), + )?; + validate_canonical_ids( + "runtime-helicity variant", + self.runtime_helicity_variants.iter().map(|row| row.id), + )?; + let contract_ranges = self + .runtime_helicity_contracts + .iter() + .map(|row| row.variant_range) + .collect::>(); + validate_packed_ranges( + "runtime-helicity variants", + &contract_ranges, + self.runtime_helicity_variants.len(), + )?; + let embedding_ranges = self + .runtime_helicity_variants + .iter() + .map(|row| row.embedding_range) + .collect::>(); + validate_packed_ranges( + "runtime-helicity embeddings", + &embedding_ranges, + self.runtime_helicity_embeddings.len(), + )?; + let projection_ranges = self + .runtime_helicity_variants + .iter() + .map(|row| row.projection_range) + .collect::>(); + validate_packed_ranges( + "runtime-helicity projections", + &projection_ranges, + self.runtime_helicity_projections.len(), + )?; + + let mut owned_sources = BTreeSet::new(); + for contract in self.runtime_helicity_contracts { + register_template( + template_kinds, + contract.template_string_id, + TemplateKind::RuntimeHelicity, + "runtime-helicity contract", + )?; + register_semantic_digest( + semantic_digests, + contract.semantic_digest_id, + "runtime-helicity contract", + )?; + required_string( + &catalogs.strings, + contract.template_string_id, + "runtime-helicity template", + )?; + required_string( + &catalogs.strings, + contract.proof_algorithm_string_id, + "runtime-helicity proof algorithm", + )?; + required_digest( + &catalogs.digests, + contract.proof_digest_id, + "runtime-helicity proof", + )?; + required_digest( + &catalogs.digests, + contract.semantic_digest_id, + "runtime-helicity semantic digest", + )?; + let full_state_index = required_reference( + contract.full_state_template_id, + self.current_states.len(), + "runtime-helicity full state", + )?; + let full_dimension = self.current_states[full_state_index].dimension as usize; + let variants = &self.runtime_helicity_variants[contract.variant_range.as_usize_range( + self.runtime_helicity_variants.len(), + "runtime-helicity variants", + )?]; + if variants.is_empty() { + return Err(invalid(format!( + "runtime-helicity contract {} has no variants", + contract.id + ))); + } + for variant in variants { + if variant.contract_id != contract.id { + return Err(invalid(format!( + "runtime-helicity variant {} names contract {}, expected {}", + variant.id, variant.contract_id, contract.id + ))); + } + let source_index = required_reference( + variant.source_template_id, + self.sources.len(), + "runtime-helicity source", + )?; + if !owned_sources.insert(variant.source_template_id) { + return Err(invalid(format!( + "source template {} belongs to multiple runtime-helicity variants", + variant.source_template_id + ))); + } + let source_state_index = required_reference( + variant.source_state_template_id, + self.current_states.len(), + "runtime-helicity source state", + )?; + if self.sources[source_index].state_template_id != variant.source_state_template_id + { + return Err(invalid(format!( + "runtime-helicity variant {} source-state contract is inconsistent", + variant.id + ))); + } + required_digest( + &catalogs.digests, + variant.proof_digest_id, + "runtime-helicity variant proof", + )?; + let source_dimension = self.current_states[source_state_index].dimension as usize; + let embeddings = + &self.runtime_helicity_embeddings[variant.embedding_range.as_usize_range( + self.runtime_helicity_embeddings.len(), + "runtime-helicity embedding", + )?]; + let projections = + &self.runtime_helicity_projections[variant.projection_range.as_usize_range( + self.runtime_helicity_projections.len(), + "runtime-helicity projection", + )?]; + if embeddings.len() != full_dimension || projections.len() != source_dimension { + return Err(invalid(format!( + "runtime-helicity variant {} dimensions do not match its current states", + variant.id + ))); + } + for (component, embedding) in embeddings.iter().enumerate() { + if embedding.variant_id != variant.id + || embedding.full_component as usize != component + { + return Err(invalid(format!( + "runtime-helicity variant {} has noncanonical embedding rows", + variant.id + ))); + } + let factor = required_factor( + &catalogs.factors, + embedding.factor_id, + "runtime-helicity embedding factor", + )?; + if embedding.source_component == MISSING_U32 { + if !factor.is_zero() { + return Err(invalid( + "an absent runtime-helicity source component has nonzero factor", + )); + } + } else if embedding.source_component as usize >= source_dimension + || factor.is_zero() + { + return Err(invalid( + "a runtime-helicity source embedding is out of range or zero", + )); + } + } + for (component, projection) in projections.iter().enumerate() { + if projection.variant_id != variant.id + || projection.source_component as usize != component + || projection.full_component as usize >= full_dimension + { + return Err(invalid(format!( + "runtime-helicity variant {} has invalid projection rows", + variant.id + ))); + } + if embeddings[projection.full_component as usize].source_component + != projection.source_component + { + return Err(invalid(format!( + "runtime-helicity variant {} embedding and projection disagree", + variant.id + ))); + } + } + } + } + Ok(()) + } + + fn validate_quantum_flows( + self, + catalogs: &ValidatedCatalogs<'_>, + template_kinds: &mut BTreeMap, + semantic_digests: &mut BTreeSet, + ) -> RusticolResult<()> { + validate_record_ids( + "quantum flow", + self.quantum_flows.iter().map(|row| row.id), + self.quantum_flows.iter().map(|row| row.template_string_id), + )?; + let mut static_quantum_flows = BTreeMap::::new(); + for (index, row) in self.quantum_flows.iter().enumerate() { + register_template( + template_kinds, + row.template_string_id, + TemplateKind::QuantumFlow, + "quantum flow", + )?; + register_semantic_digest(semantic_digests, row.semantic_digest_id, "quantum flow")?; + required_string( + &catalogs.strings, + row.template_string_id, + "quantum-flow template", + )?; + let states = u32_sequence( + self, + row.input_state_sequence_id, + "quantum-flow input states", + )?; + if states.len() != 2 { + return Err(invalid(format!( + "quantum flow {index} must have binary input arity" + ))); + } + validate_u32_references( + states, + self.current_states.len(), + "quantum-flow input states", + )?; + let spins = i32_sequence(self, row.input_spin_sequence_id, "quantum-flow spins")?; + for spin in spins { + validate_concrete_spin_state(*spin, "quantum-flow input spin state")?; + } + validate_concrete_spin_state(row.result_spin_state, "quantum-flow result spin state")?; + let flavour_ids = u32_sequence( + self, + row.input_flavour_sequence_id, + "quantum-flow flavour flows", + )?; + let quantum_ids = u32_sequence( + self, + row.input_quantum_sequence_id, + "quantum-flow quantum-number flows", + )?; + if states.len() != spins.len() + || states.len() != flavour_ids.len() + || states.len() != quantum_ids.len() + { + return Err(invalid(format!( + "quantum flow {index} input contract columns have different arity" + ))); + } + validate_u32_references( + flavour_ids, + self.flavour_flow_ranges.len(), + "quantum-flow flavour flows", + )?; + validate_u32_references( + quantum_ids, + self.quantum_number_flow_ranges.len(), + "quantum-flow quantum-number flows", + )?; + let operation = required_string( + &catalogs.strings, + row.flavour_flow_operation_string_id, + "quantum-flow flavour operation", + )?; + if !matches!( + operation, + "constant-result" + | "append-left-result" + | "append-right-result" + | "concat-left-right-result" + ) { + return Err(invalid(format!( + "quantum flow {index} has unsupported flavour operation {operation:?}" + ))); + } + let quantum_operation = required_string( + &catalogs.strings, + row.quantum_number_flow_operation_string_id, + "quantum-flow quantum-number operation", + )?; + if quantum_operation != "particle-static-result" { + return Err(invalid(format!( + "quantum flow {index} has unsupported quantum-number operation {quantum_operation:?}" + ))); + } + required_reference( + row.result_state_template_id, + self.current_states.len(), + "quantum-flow result state", + )?; + required_reference( + row.result_flavour_flow_id, + self.flavour_flow_ranges.len(), + "quantum-flow result flavour flow", + )?; + required_reference( + row.result_quantum_number_flow_id, + self.quantum_number_flow_ranges.len(), + "quantum-flow result quantum-number flow", + )?; + let previous_quantum_flow = static_quantum_flows + .entry(row.result_state_template_id) + .or_insert(row.result_quantum_number_flow_id); + if *previous_quantum_flow != row.result_quantum_number_flow_id { + return Err(invalid(format!( + "quantum flow {index} violates particle-static quantum-number flow" + ))); + } + validate_flavour_flow_witness( + self, + index, + operation, + flavour_ids, + row.result_state_template_id, + row.result_flavour_flow_id, + )?; + required_reference( + row.coupling_order_set_id, + self.coupling_order_ranges.len(), + "quantum-flow coupling-order set", + )?; + required_factor( + &catalogs.factors, + row.exact_coupling_factor_id, + "quantum-flow exact coupling", + )?; + required_digest( + &catalogs.digests, + row.predicate_digest_id, + "quantum-flow predicate", + )?; + required_digest( + &catalogs.digests, + row.semantic_digest_id, + "quantum-flow semantic digest", + )?; + } + Ok(()) + } + + fn validate_contact_orbit_records( + self, + catalogs: &ValidatedCatalogs<'_>, + template_kinds: &mut BTreeMap, + semantic_digests: &mut BTreeSet, + ) -> RusticolResult<()> { + validate_record_ids( + "contact-orbit certificate", + self.contact_orbit_certificates.iter().map(|row| row.id), + self.contact_orbit_certificates + .iter() + .map(|row| row.template_string_id), + )?; + for (index, row) in self.contact_orbit_certificates.iter().enumerate() { + register_template( + template_kinds, + row.template_string_id, + TemplateKind::ContactOrbitCertificate, + "contact-orbit certificate", + )?; + register_semantic_digest( + semantic_digests, + row.semantic_digest_id, + "contact-orbit certificate", + )?; + required_string( + &catalogs.strings, + row.template_string_id, + "contact-orbit certificate template", + )?; + require_string_value( + &catalogs.strings, + row.algorithm_string_id, + CONTACT_ORBIT_ALGORITHM, + "contact-orbit certificate algorithm", + )?; + if row.algorithm_version != CONTACT_ORBIT_ALGORITHM_VERSION { + return Err(invalid(format!( + "contact-orbit certificate {index} has unsupported algorithm version {}", + row.algorithm_version + ))); + } + required_string( + &catalogs.strings, + row.vertex_string_id, + "contact-orbit certificate vertex", + )?; + let evaluator_class = required_string( + &catalogs.strings, + row.evaluator_class_string_id, + "contact-orbit evaluator class", + )?; + if !matches!( + evaluator_class, + CONTACT_ORBIT_EVALUATOR_CLASS | HEFT_CONTACT_ORBIT_EVALUATOR_CLASS + ) { + return Err(RusticolError::compatibility(format!( + "unsupported contact-orbit evaluator class {evaluator_class:?}" + ))); + } + let particles = u32_sequence( + self, + row.particle_string_sequence_id, + "contact-orbit certificate particles", + )?; + let equivalence_classes = u32_sequence( + self, + row.physical_leg_equivalence_sequence_id, + "contact-orbit certificate physical-leg equivalence classes", + )?; + if !matches!(particles.len(), 4 | 5) || equivalence_classes.len() != particles.len() { + return Err(invalid(format!( + "contact-orbit certificate {index} must describe four or five physical legs" + ))); + } + for particle in particles { + required_string( + &catalogs.strings, + *particle, + "contact-orbit certificate particle", + )?; + } + let mut class_by_particle = BTreeMap::::new(); + let mut next_class = 0u32; + let expected_classes = particles + .iter() + .map(|particle| { + *class_by_particle.entry(*particle).or_insert_with(|| { + let class = next_class; + next_class += 1; + class + }) + }) + .collect::>(); + if equivalence_classes != expected_classes.as_slice() { + return Err(invalid(format!( + "contact-orbit certificate {index} has noncanonical physical-leg equivalence classes" + ))); + } + let reconstruction = required_factor( + &catalogs.factors, + row.reconstruction_factor_id, + "contact-orbit certificate reconstruction factor", + )?; + if *reconstruction != ExactComplexRational::ONE { + return Err(invalid(format!( + "contact-orbit certificate {index} reconstruction factor is not one" + ))); + } + required_digest( + &catalogs.digests, + row.semantic_digest_id, + "contact-orbit certificate semantic digest", + )?; + } + + validate_record_ids( + "contact-orbit step", + self.contact_orbit_steps.iter().map(|row| row.id), + self.contact_orbit_steps + .iter() + .map(|row| row.template_string_id), + )?; + for (index, row) in self.contact_orbit_steps.iter().enumerate() { + register_template( + template_kinds, + row.template_string_id, + TemplateKind::ContactOrbitStep, + "contact-orbit step", + )?; + register_semantic_digest( + semantic_digests, + row.semantic_digest_id, + "contact-orbit step", + )?; + required_string( + &catalogs.strings, + row.template_string_id, + "contact-orbit step template", + )?; + required_reference( + row.certificate_id, + self.contact_orbit_certificates.len(), + "contact-orbit step certificate", + )?; + let certificate = self + .contact_orbit_certificates + .get(row.certificate_id as usize) + .ok_or_else(|| invalid("contact-orbit step certificate is absent"))?; + let arity = u32_sequence( + self, + certificate.physical_leg_equivalence_sequence_id, + "contact-orbit step certificate arity", + )? + .len(); + let stage = ContactOrbitStage::try_from(row.stage)?; + if usize::from(row.result_leg) >= arity { + return Err(invalid(format!( + "contact-orbit step {index} result leg is outside arity" + ))); + } + let left = u32_sequence( + self, + row.left_covered_leg_sequence_id, + "contact-orbit left covered legs", + )?; + let right = u32_sequence( + self, + row.right_covered_leg_sequence_id, + "contact-orbit right covered legs", + )?; + if left.is_empty() + || right.is_empty() + || left.iter().chain(right).any(|leg| *leg as usize >= arity) + { + return Err(invalid(format!( + "contact-orbit step {index} has invalid covered legs" + ))); + } + let covered = left.iter().chain(right).copied().collect::>(); + if covered.len() != left.len() + right.len() { + return Err(invalid(format!( + "contact-orbit step {index} repeats a covered leg" + ))); + } + let sources = i32_sequence( + self, + row.source_particle_leg_sequence_id, + "contact-orbit source-particle legs", + )?; + if sources.len() != 3 || sources.iter().any(|leg| *leg < -1 || *leg >= arity as i32) { + return Err(invalid(format!( + "contact-orbit step {index} has invalid source-particle legs" + ))); + } + let reconstruction = required_factor( + &catalogs.factors, + row.reconstruction_factor_id, + "contact-orbit step reconstruction factor", + )?; + if *reconstruction != ExactComplexRational::ONE { + return Err(invalid(format!( + "contact-orbit step {index} reconstruction factor is not one" + ))); + } + match stage { + ContactOrbitStage::Partial => { + if left.len() != 1 + || right.len() != 1 + || sources != [left[0] as i32, right[0] as i32, -1] + || covered.contains(&(row.result_leg as u32)) + { + return Err(invalid(format!( + "contact-orbit partial step {index} has inconsistent lineage" + ))); + } + } + ContactOrbitStage::Final => { + let expected_covered = (0u32..arity as u32) + .filter(|leg| *leg != row.result_leg as u32) + .collect::>(); + let expected_sources = [ + if left.len() > 1 { -1 } else { left[0] as i32 }, + if right.len() > 1 { -1 } else { right[0] as i32 }, + row.result_leg as i32, + ]; + if covered != expected_covered + || left.len() > 2 + || right.len() > 2 + || sources != expected_sources + { + return Err(invalid(format!( + "contact-orbit final step {index} has inconsistent lineage" + ))); + } + } + } + required_digest( + &catalogs.digests, + row.semantic_digest_id, + "contact-orbit step semantic digest", + )?; + } + Ok(()) + } + + fn validate_color_contractions( + self, + catalogs: &ValidatedCatalogs<'_>, + template_kinds: &mut BTreeMap, + semantic_digests: &mut BTreeSet, + ) -> RusticolResult<()> { + validate_record_ids( + "color contraction", + self.color_contractions.iter().map(|row| row.id), + self.color_contractions + .iter() + .map(|row| row.template_string_id), + )?; + let ranges: Vec<_> = self + .color_contractions + .iter() + .map(|row| CheckedTableRange::new(row.nc_term_start, row.nc_term_count)) + .collect(); + validate_packed_ranges("color Nc polynomial", &ranges, self.color_nc_terms.len())?; + let witness_ranges: Vec<_> = self + .color_contractions + .iter() + .map(|row| CheckedTableRange::new(row.witness_start, row.witness_count)) + .collect(); + validate_packed_ranges( + "LC color transition witnesses", + &witness_ranges, + self.lc_color_transition_witnesses.len(), + )?; + for (index, row) in self.color_contractions.iter().enumerate() { + register_template( + template_kinds, + row.template_string_id, + TemplateKind::ColorContraction, + "color contraction", + )?; + register_semantic_digest( + semantic_digests, + row.semantic_digest_id, + "color contraction", + )?; + required_string(&catalogs.strings, row.template_string_id, "color template")?; + required_string( + &catalogs.strings, + row.rule_kind_string_id, + "color rule kind", + )?; + let input_representations = i32_sequence( + self, + row.input_representation_sequence_id, + "color input representations", + )?; + validate_color_contract_shape( + index, + input_representations, + row.has_output_representation, + row.output_representation, + row.ordered_open_string_arity, + )?; + let witnesses = &self.lc_color_transition_witnesses[witness_ranges[index] + .as_usize_range( + self.lc_color_transition_witnesses.len(), + "LC color transition witnesses", + )?]; + if witnesses.is_empty() { + return Err(invalid(format!( + "color contraction {index} has no executable LC transition witness" + ))); + } + let has_output = row.has_output_representation == 1; + for (ordinal, witness) in witnesses.iter().enumerate() { + if witness.color_contraction_id as usize != index { + return Err(invalid(format!( + "LC color witness names contraction {}, expected {index}", + witness.color_contraction_id + ))); + } + if witness.ordinal as usize != ordinal { + return Err(invalid(format!( + "LC color witness for contraction {index} has ordinal {}, expected {ordinal}", + witness.ordinal + ))); + } + let left_shape = required_string( + &catalogs.strings, + witness.left_shape_string_id, + "LC color witness left shape", + )?; + let right_shape = required_string( + &catalogs.strings, + witness.right_shape_string_id, + "LC color witness right shape", + )?; + validate_lc_color_shape_name(left_shape, "LC color witness left shape")?; + validate_lc_color_shape_name(right_shape, "LC color witness right shape")?; + if input_representations.len() != 2 { + return Err(invalid(format!( + "LC color witness for contraction {index} requires exactly two input representations" + ))); + } + match witness.input_permutation { + 0 | 1 => {} + value => { + return Err(invalid(format!( + "LC color witness input permutation must be 0 or 1, found {value}" + ))); + } + } + for (shape, input) in [left_shape, right_shape].into_iter().zip([0usize, 1usize]) { + validate_lc_color_shape( + shape, + input_representations[input], + "LC color witness input", + )?; + } + if witness.reverse_parent_mask > 3 { + return Err(invalid(format!( + "LC color witness reverse-parent mask {} exceeds two inputs", + witness.reverse_parent_mask + ))); + } + let is_join = witness.component_operation == 0; + let is_close = witness.component_operation == 5; + if witness.component_operation > 5 { + return Err(invalid(format!( + "LC color witness operation {} is not supported", + witness.component_operation + ))); + } + let component_kind = if witness.result_component_kind == u8::MAX { + None + } else { + Some(LCColorComponentKind::try_from( + witness.result_component_kind, + )?) + }; + let component_role = LCColorComponentRole::try_from(witness.result_component_role)?; + if is_join { + if component_kind.is_none() || component_role == LCColorComponentRole::None { + return Err(invalid( + "LC color join witness requires component kind and role", + )); + } + let expected_role = if row.output_representation.abs() == 1 { + LCColorComponentRole::Passive + } else { + LCColorComponentRole::Active + }; + if component_role != expected_role { + return Err(invalid( + "LC color join role does not match its output representation", + )); + } + } else if is_close { + if component_role != LCColorComponentRole::None { + return Err(invalid("LC color closure result role must be none")); + } + let expected_kind = match input_representations { + [left, right] if left.abs() == 1 && right.abs() == 1 => None, + [left, right] + if (*left == 3 && *right == -3) || (*left == -3 && *right == 3) => + { + Some(LCColorComponentKind::OpenString) + } + [8, 8] => Some(LCColorComponentKind::Trace), + _ => { + return Err(invalid( + "LC color closure has unsupported input representations", + )); + } + }; + if component_kind != expected_kind { + return Err(invalid( + "LC color closure component kind does not match its inputs", + )); + } + } else { + if component_kind.is_some() { + return Err(invalid( + "only LC color join/closure witnesses may declare a component kind", + )); + } + let expected_role = if matches!(witness.component_operation, 2 | 3) { + LCColorComponentRole::Active + } else { + LCColorComponentRole::None + }; + if component_role != expected_role { + return Err(invalid( + "LC color witness result role does not match its operation", + )); + } + } + if is_close { + if has_output || witness.result_shape_string_id != MISSING_U32 { + return Err(invalid( + "LC color closure witness must have neither an output representation nor a result shape", + )); + } + } else { + if !has_output { + return Err(invalid( + "non-closure LC color witness requires an output representation", + )); + } + let result_shape = required_string( + &catalogs.strings, + witness.result_shape_string_id, + "LC color witness result shape", + )?; + validate_lc_color_shape( + result_shape, + row.output_representation, + "LC color witness result", + )?; + } + let factor = required_factor( + &catalogs.factors, + witness.exact_factor_id, + "LC color witness factor", + )?; + if *factor == ExactComplexRational::ZERO { + return Err(invalid("LC color witness factor must be nonzero")); + } + required_digest( + &catalogs.digests, + witness.proof_digest_id, + "LC color witness proof", + )?; + let input_port_pairings = u32_sequence( + self, + witness.input_port_pairing_sequence_id, + "LC color witness input-port pairings", + )?; + if input_port_pairings.len() % 4 != 0 { + return Err(invalid( + "LC color witness input-port pairings are not divisible by four", + )); + } + let result_port_bindings = u32_sequence( + self, + witness.result_port_binding_sequence_id, + "LC color witness result-port bindings", + )?; + if result_port_bindings.len() % 2 != 0 { + return Err(invalid( + "LC color witness result-port bindings are not divisible by two", + )); + } + let mut consumed_ports = BTreeSet::new(); + for port in input_port_pairings + .chunks_exact(2) + .chain(result_port_bindings.chunks_exact(2)) + { + let parent = port[0] as usize; + let local = port[1] as usize; + let representation = *input_representations.get(parent).ok_or_else(|| { + invalid("LC color witness port references an absent parent") + })?; + let port_count = match representation { + 1 => 0, + 3 | -3 => 1, + 8 => 2, + _ => { + return Err(invalid( + "LC color witness port uses an unsupported representation", + )); + } + }; + if local >= port_count { + return Err(invalid( + "LC color witness port references an absent local port", + )); + } + if !consumed_ports.insert((parent, local)) { + return Err(invalid( + "LC color witness consumes one parent port more than once", + )); + } + } + let expected_port_count = input_representations + .iter() + .map(|representation| match representation { + 1 => Ok(0_usize), + 3 | -3 => Ok(1), + 8 => Ok(2), + _ => Err(invalid( + "LC color witness uses an unsupported representation", + )), + }) + .sum::>()?; + if consumed_ports.len() != expected_port_count { + return Err(invalid( + "LC color witness does not consume every parent port exactly once", + )); + } + let provenance = u32_sequence( + self, + witness.provenance_sequence_id, + "LC color witness provenance", + )?; + validate_string_pair_sequence( + &catalogs.strings, + provenance, + "LC color witness provenance", + )?; + } + required_factor( + &catalogs.factors, + row.exact_coefficient_factor_id, + "color exact coefficient", + )?; + required_digest( + &catalogs.digests, + row.expression_digest_id, + "color expression", + )?; + required_digest( + &catalogs.digests, + row.semantic_digest_id, + "color semantic digest", + )?; + let terms = &self.color_nc_terms + [ranges[index].as_usize_range(self.color_nc_terms.len(), "color Nc terms")?]; + let mut previous_exponent = None; + for term in terms { + if term.color_contraction_id as usize != index { + return Err(invalid(format!( + "color Nc term names contraction {}, expected {index}", + term.color_contraction_id + ))); + } + if term.exponent < 0 { + return Err(invalid(format!( + "color contraction {index} has a negative Nc exponent" + ))); + } + if let Some(previous_exponent) = previous_exponent + && previous_exponent >= term.exponent + { + return Err(invalid(format!( + "color contraction {index} Nc powers are not strictly ordered" + ))); + } + let coefficient = + required_factor(&catalogs.factors, term.factor_id, "color Nc coefficient")?; + if *coefficient == ExactComplexRational::ZERO { + return Err(invalid(format!( + "color contraction {index} retains an exact-zero Nc term" + ))); + } + previous_exponent = Some(term.exponent); + } + } + Ok(()) + } + + fn validate_symmetry_proofs_basic( + self, + catalogs: &ValidatedCatalogs<'_>, + template_kinds: &mut BTreeMap, + semantic_digests: &mut BTreeSet, + ) -> RusticolResult<()> { + validate_record_ids( + "symmetry proof", + self.symmetry_proofs.iter().map(|row| row.id), + self.symmetry_proofs + .iter() + .map(|row| row.template_string_id), + )?; + for (index, row) in self.symmetry_proofs.iter().enumerate() { + register_template( + template_kinds, + row.template_string_id, + TemplateKind::SymmetryProof, + "symmetry proof", + )?; + register_semantic_digest(semantic_digests, row.semantic_digest_id, "symmetry proof")?; + required_string(&catalogs.strings, row.template_string_id, "proof template")?; + let algorithm = required_string( + &catalogs.strings, + row.proof_algorithm_string_id, + "proof algorithm", + )?; + if !is_supported_proof_algorithm(algorithm) { + return Err(invalid(format!( + "symmetry proof {index} uses unsupported algorithm {algorithm:?}" + ))); + } + let subjects = u32_sequence(self, row.subject_template_sequence_id, "proof subjects")?; + if subjects.is_empty() { + return Err(invalid(format!("symmetry proof {index} has no subjects"))); + } + for subject in subjects { + required_string(&catalogs.strings, *subject, "proof subject template")?; + } + validate_permutation( + u32_sequence( + self, + row.input_permutation_sequence_id, + "proof input permutation", + )?, + "proof input permutation", + )?; + let phase = required_factor( + &catalogs.factors, + row.exact_phase_factor_id, + "proof exact phase", + )?; + if *phase == ExactComplexRational::ZERO { + return Err(invalid(format!("symmetry proof {index} has zero phase"))); + } + let expressions = u32_sequence( + self, + row.expression_digest_sequence_id, + "proof expression digests", + )?; + if expressions.is_empty() { + return Err(invalid(format!( + "symmetry proof {index} has no expression digests" + ))); + } + validate_u32_references( + expressions, + catalogs.digests.len(), + "proof expression digests", + )?; + required_digest(&catalogs.digests, row.witness_digest_id, "proof witness")?; + required_digest( + &catalogs.digests, + row.semantic_digest_id, + "proof semantic digest", + )?; + } + Ok(()) + } + + fn validate_transitions_basic( + self, + catalogs: &ValidatedCatalogs<'_>, + template_kinds: &mut BTreeMap, + semantic_digests: &mut BTreeSet, + ) -> RusticolResult<()> { + validate_record_ids( + "transition", + self.transitions.iter().map(|row| row.id), + self.transitions.iter().map(|row| row.template_string_id), + )?; + for (index, row) in self.transitions.iter().enumerate() { + register_template( + template_kinds, + row.template_string_id, + TemplateKind::Transition, + "transition", + )?; + register_semantic_digest(semantic_digests, row.semantic_digest_id, "transition")?; + required_string( + &catalogs.strings, + row.template_string_id, + "transition template", + )?; + let inputs = + u32_sequence(self, row.input_state_sequence_id, "transition input states")?; + if inputs.is_empty() { + return Err(invalid(format!("transition {index} has no inputs"))); + } + validate_u32_references(inputs, self.current_states.len(), "transition input states")?; + required_reference( + row.result_state_template_id, + self.current_states.len(), + "transition result state", + )?; + required_reference( + row.quantum_flow_template_id, + self.quantum_flows.len(), + "transition quantum flow", + )?; + required_reference( + row.evaluator_binding_id, + self.evaluator_bindings.len(), + "transition evaluator", + )?; + let permutation = u32_sequence( + self, + row.canonical_input_order_sequence_id, + "transition canonical input order", + )?; + validate_permutation_of( + permutation, + inputs.len(), + "transition canonical input order", + )?; + let momentum = u32_sequence( + self, + row.momentum_convention_sequence_id, + "transition momentum convention", + )?; + if momentum.len() != inputs.len() { + return Err(invalid(format!( + "transition {index} momentum convention does not cover every input" + ))); + } + for string_id in momentum { + required_string( + &catalogs.strings, + *string_id, + "transition momentum convention", + )?; + } + let parameters = u32_sequence( + self, + row.coupling_parameter_sequence_id, + "transition coupling parameters", + )?; + validate_strict_u32(parameters, "transition coupling parameters")?; + validate_u32_references( + parameters, + self.parameters.len(), + "transition coupling parameters", + )?; + required_reference( + row.coupling_order_set_id, + self.coupling_order_ranges.len(), + "transition coupling-order set", + )?; + required_reference( + row.color_contraction_template_id, + self.color_contractions.len(), + "transition color contraction", + )?; + required_factor( + &catalogs.factors, + row.binding_coupling_factor_id, + "transition binding coupling", + )?; + required_factor( + &catalogs.factors, + row.exact_factor_id, + "transition exact factor", + )?; + OutputFactorSource::try_from(row.output_factor_source)?; + required_string( + &catalogs.strings, + row.equivalence_class_string_id, + "transition equivalence class", + )?; + if let Some(factor) = optional_factor( + &catalogs.factors, + row.input_exchange_factor_id, + "transition input-exchange factor", + )? && *factor == ExactComplexRational::ZERO + { + return Err(invalid(format!( + "transition {index} has a zero input-exchange factor" + ))); + } + required_string( + &catalogs.strings, + row.output_projection_string_id, + "transition output projection", + )?; + let contact_orbit_steps = u32_sequence( + self, + row.contact_orbit_step_sequence_id, + "transition contact-orbit steps", + )?; + let contact_orbit_step_digests = u32_sequence( + self, + row.contact_orbit_step_semantic_digest_sequence_id, + "transition contact-orbit step semantic digests", + )?; + if contact_orbit_steps.len() != contact_orbit_step_digests.len() { + return Err(invalid(format!( + "transition {index} has incomplete contact-orbit step bindings" + ))); + } + if contact_orbit_steps + .windows(2) + .any(|pair| pair[0] >= pair[1]) + { + return Err(invalid(format!( + "transition {index} contact-orbit steps are not canonical" + ))); + } + for (&step_id, &step_digest_id) in + contact_orbit_steps.iter().zip(contact_orbit_step_digests) + { + let step_index = required_reference( + step_id, + self.contact_orbit_steps.len(), + "transition contact-orbit step", + )?; + let expected_digest_id = self.contact_orbit_steps[step_index].semantic_digest_id; + if step_digest_id != expected_digest_id { + return Err(invalid(format!( + "transition {index} contact-orbit step digest does not match its record" + ))); + } + required_digest( + &catalogs.digests, + step_digest_id, + "transition contact-orbit step semantic digest", + )?; + } + required_digest( + &catalogs.digests, + row.semantic_digest_id, + "transition semantic digest", + )?; + + let flow = self.quantum_flows[row.quantum_flow_template_id as usize]; + let flow_inputs = u32_sequence( + self, + flow.input_state_sequence_id, + "quantum-flow input states", + )?; + if inputs != flow_inputs + || row.result_state_template_id != flow.result_state_template_id + || row.coupling_order_set_id != flow.coupling_order_set_id + { + return Err(invalid(format!( + "transition {index} and quantum-flow state/coupling contracts do not match" + ))); + } + } + Ok(()) + } + + fn validate_propagators_basic( + self, + catalogs: &ValidatedCatalogs<'_>, + template_kinds: &mut BTreeMap, + semantic_digests: &mut BTreeSet, + ) -> RusticolResult<()> { + validate_record_ids( + "propagator", + self.propagators.iter().map(|row| row.id), + self.propagators.iter().map(|row| row.template_string_id), + )?; + let mut state_owners = vec![None; self.current_states.len()]; + for (index, row) in self.propagators.iter().enumerate() { + register_template( + template_kinds, + row.template_string_id, + TemplateKind::Propagator, + "propagator", + )?; + register_semantic_digest(semantic_digests, row.semantic_digest_id, "propagator")?; + required_string( + &catalogs.strings, + row.template_string_id, + "propagator template", + )?; + let state = required_reference( + row.state_template_id, + self.current_states.len(), + "propagator state", + )?; + if let Some(previous) = state_owners[state].replace(index) { + return Err(invalid(format!( + "current state {state} has propagator rows {previous} and {index}" + ))); + } + let active = validate_bool(row.applies_propagator, "applies_propagator")?; + let evaluator = optional_reference( + row.evaluator_binding_id, + self.evaluator_bindings.len(), + "propagator evaluator", + )?; + let numerator = optional_digest( + &catalogs.digests, + row.numerator_expression_digest_id, + "propagator numerator", + )?; + let denominator = optional_digest( + &catalogs.digests, + row.denominator_expression_digest_id, + "propagator denominator", + )?; + if active { + if evaluator.is_none() || numerator.is_none() || denominator.is_none() { + return Err(invalid(format!( + "active propagator {index} requires evaluator, numerator, and denominator" + ))); + } + } else if evaluator.is_some() || numerator.is_some() || denominator.is_some() { + return Err(invalid(format!( + "identity propagator {index} carries evaluator or expression metadata" + ))); + } + optional_reference( + row.mass_parameter_id, + self.parameters.len(), + "propagator mass parameter", + )?; + optional_reference( + row.width_parameter_id, + self.parameters.len(), + "propagator width parameter", + )?; + optional_string(&catalogs.strings, row.gauge_string_id, "propagator gauge")?; + optional_reference( + row.linearity_proof_template_id, + self.symmetry_proofs.len(), + "propagator linearity proof", + )?; + required_digest( + &catalogs.digests, + row.semantic_digest_id, + "propagator semantic digest", + )?; + } + if let Some((state, _)) = state_owners + .iter() + .enumerate() + .find(|(_, owner)| owner.is_none()) + { + return Err(invalid(format!( + "current state {state} has no active or identity propagator" + ))); + } + Ok(()) + } + + fn validate_closures_basic( + self, + catalogs: &ValidatedCatalogs<'_>, + template_kinds: &mut BTreeMap, + semantic_digests: &mut BTreeSet, + ) -> RusticolResult<()> { + validate_record_ids( + "closure", + self.closures.iter().map(|row| row.id), + self.closures.iter().map(|row| row.template_string_id), + )?; + for (index, row) in self.closures.iter().enumerate() { + register_template( + template_kinds, + row.template_string_id, + TemplateKind::Closure, + "closure", + )?; + register_semantic_digest(semantic_digests, row.semantic_digest_id, "closure")?; + required_string( + &catalogs.strings, + row.template_string_id, + "closure template", + )?; + let inputs = u32_sequence(self, row.input_state_sequence_id, "closure input states")?; + if inputs.is_empty() { + return Err(invalid(format!("closure {index} has no inputs"))); + } + validate_u32_references(inputs, self.current_states.len(), "closure input states")?; + let result_state = optional_reference( + row.result_state_template_id, + self.current_states.len(), + "closure result state", + )?; + required_reference( + row.evaluator_binding_id, + self.evaluator_bindings.len(), + "closure evaluator", + )?; + validate_permutation_of( + u32_sequence( + self, + row.canonical_input_order_sequence_id, + "closure canonical input order", + )?, + inputs.len(), + "closure canonical input order", + )?; + let parameters = u32_sequence( + self, + row.coupling_parameter_sequence_id, + "closure coupling parameters", + )?; + validate_strict_u32(parameters, "closure coupling parameters")?; + validate_u32_references( + parameters, + self.parameters.len(), + "closure coupling parameters", + )?; + required_reference( + row.coupling_order_set_id, + self.coupling_order_ranges.len(), + "closure coupling-order set", + )?; + let eligible_flows = u32_sequence( + self, + row.eligible_quantum_flow_sequence_id, + "closure eligible quantum flows", + )?; + validate_strict_u32(eligible_flows, "closure eligible quantum flows")?; + validate_u32_references( + eligible_flows, + self.quantum_flows.len(), + "closure eligible quantum flows", + )?; + let evaluator = &self.evaluator_bindings[row.evaluator_binding_id as usize]; + match EvaluatorCallableKind::try_from(evaluator.callable_kind)? { + EvaluatorCallableKind::PreparedKernel if eligible_flows.is_empty() => { + return Err(invalid(format!( + "prepared closure {index} has no eligible quantum-flow witness" + ))); + } + EvaluatorCallableKind::RusticolTemplate if !eligible_flows.is_empty() => { + return Err(invalid(format!( + "direct Rusticol closure {index} carries prepared quantum-flow witnesses" + ))); + } + EvaluatorCallableKind::PreparedKernel if result_state.is_none() => { + return Err(invalid(format!( + "prepared closure {index} has no result-state contract" + ))); + } + EvaluatorCallableKind::RusticolTemplate if result_state.is_some() => { + return Err(invalid(format!( + "direct Rusticol closure {index} carries a result-state contract" + ))); + } + _ => {} + } + for flow_id in eligible_flows { + let flow = &self.quantum_flows[*flow_id as usize]; + let flow_inputs = u32_sequence( + self, + flow.input_state_sequence_id, + "closure quantum-flow input states", + )?; + if flow_inputs != inputs { + return Err(invalid(format!( + "closure {index} and eligible quantum flow {flow_id} have different input-state contracts" + ))); + } + if Some(flow.result_state_template_id as usize) != result_state { + return Err(invalid(format!( + "closure {index} and eligible quantum flow {flow_id} have different result-state contracts" + ))); + } + if flow.coupling_order_set_id != row.coupling_order_set_id { + return Err(invalid(format!( + "closure {index} and eligible quantum flow {flow_id} have different coupling-order contracts" + ))); + } + } + required_reference( + row.color_contraction_template_id, + self.color_contractions.len(), + "closure color contraction", + )?; + required_factor( + &catalogs.factors, + row.binding_coupling_factor_id, + "closure binding coupling", + )?; + required_factor( + &catalogs.factors, + row.exact_factor_id, + "closure exact factor", + )?; + OutputFactorSource::try_from(row.output_factor_source)?; + required_string( + &catalogs.strings, + row.equivalence_class_string_id, + "closure equivalence class", + )?; + if let Some(factor) = optional_factor( + &catalogs.factors, + row.input_exchange_factor_id, + "closure input-exchange factor", + )? && *factor == ExactComplexRational::ZERO + { + return Err(invalid(format!( + "closure {index} has a zero input-exchange factor" + ))); + } + required_string( + &catalogs.strings, + row.projection_string_id, + "closure projection", + )?; + let coefficients = u32_sequence( + self, + row.component_coefficient_sequence_id, + "closure component coefficients", + )?; + validate_u32_references( + coefficients, + catalogs.factors.len(), + "closure component coefficients", + )?; + let chirality = required_string( + &catalogs.strings, + row.chirality_relation_string_id, + "closure chirality relation", + )?; + if !matches!(chirality, "any" | "equal" | "opposite") { + return Err(invalid(format!( + "closure {index} has unsupported chirality relation {chirality:?}" + ))); + } + optional_string( + &catalogs.strings, + row.metric_signature_string_id, + "closure metric signature", + )?; + required_digest( + &catalogs.digests, + row.semantic_digest_id, + "closure semantic digest", + )?; + } + Ok(()) + } + + fn validate_evaluators( + self, + catalogs: &ValidatedCatalogs<'_>, + template_kinds: &BTreeMap, + semantic_digests: &mut BTreeSet, + ) -> RusticolResult { + validate_record_ids( + "evaluator binding", + self.evaluator_bindings.iter().map(|row| row.id), + self.evaluator_bindings + .iter() + .map(|row| row.resolver_key_string_id), + )?; + let mut prepared_contracts = BTreeMap::new(); + let mut runtime_contracts = BTreeMap::new(); + let mut semantic_owners = BTreeMap::new(); + let mut prepared_ids = BTreeSet::new(); + for (index, row) in self.evaluator_bindings.iter().enumerate() { + register_semantic_digest( + semantic_digests, + row.semantic_digest_id, + "evaluator binding", + )?; + required_string( + &catalogs.strings, + row.resolver_key_string_id, + "evaluator resolver key", + )?; + let contract_kind = EvaluatorContractKind::try_from(row.contract_kind)?; + let callable_kind = EvaluatorCallableKind::try_from(row.callable_kind)?; + required_digest( + &catalogs.digests, + row.callable_signature_digest_id, + "evaluator callable signature", + )?; + required_digest( + &catalogs.digests, + row.semantic_digest_id, + "evaluator semantic digest", + )?; + let inputs = u32_sequence(self, row.input_state_sequence_id, "evaluator input states")?; + validate_u32_references(inputs, self.current_states.len(), "evaluator input states")?; + let output = optional_reference( + row.output_state_template_id, + self.current_states.len(), + "evaluator output state", + )?; + let input_layout = + u32_sequence(self, row.input_layout_sequence_id, "evaluator input layout")?; + let output_layout = u32_sequence( + self, + row.output_layout_sequence_id, + "evaluator output layout", + )?; + let exact_expressions = u32_sequence( + self, + row.exact_expression_digest_sequence_id, + "evaluator exact expressions", + )?; + if input_layout.is_empty() || output_layout.is_empty() || exact_expressions.is_empty() { + return Err(invalid(format!( + "evaluator binding {index} has an empty layout or expression contract" + ))); + } + validate_u32_references( + input_layout, + catalogs.strings.len(), + "evaluator input layout", + )?; + validate_u32_references( + output_layout, + catalogs.strings.len(), + "evaluator output layout", + )?; + validate_u32_references( + exact_expressions, + catalogs.digests.len(), + "evaluator exact expressions", + )?; + if output_layout.len() != exact_expressions.len() { + return Err(invalid(format!( + "evaluator binding {index} output layout and exact expressions do not align" + ))); + } + if let Some(output) = output + && output_layout.len() != self.current_states[output].dimension as usize + { + return Err(invalid(format!( + "evaluator binding {index} output layout does not match state dimension" + ))); + } + match contract_kind { + EvaluatorContractKind::Source + | EvaluatorContractKind::Vertex + | EvaluatorContractKind::Propagator => { + if output.is_none() { + return Err(invalid(format!( + "evaluator binding {index} requires an output state" + ))); + } + } + EvaluatorContractKind::Closure | EvaluatorContractKind::ModelParameter => { + if output.is_some() { + return Err(invalid(format!( + "evaluator binding {index} cannot produce a current state" + ))); + } + } + } + match callable_kind { + EvaluatorCallableKind::PreparedKernel => { + if row.prepared_kernel_id == MISSING_U32 + || row.runtime_template_string_id != MISSING_U32 + { + return Err(invalid(format!( + "prepared-kernel evaluator binding {index} has invalid callable identity" + ))); + } + prepared_ids.insert(row.prepared_kernel_id); + } + EvaluatorCallableKind::RusticolTemplate => { + if row.prepared_kernel_id != MISSING_U32 + || row.runtime_template_string_id == MISSING_U32 + { + return Err(invalid(format!( + "Rusticol-template evaluator binding {index} has invalid callable identity" + ))); + } + required_string( + &catalogs.strings, + row.runtime_template_string_id, + "evaluator runtime template", + )?; + validate_rusticol_runtime_template(catalogs, row, contract_kind, index)?; + } + } + + let semantic_templates = u32_sequence( + self, + row.semantic_template_sequence_id, + "evaluator semantic templates", + )?; + if semantic_templates.is_empty() { + return Err(invalid(format!( + "evaluator binding {index} has no semantic templates" + ))); + } + validate_strict_u32(semantic_templates, "evaluator semantic templates")?; + for template in semantic_templates { + let template_kind = template_kinds.get(template).ok_or_else(|| { + invalid(format!( + "evaluator binding {index} references unknown semantic template string id {template}" + )) + })?; + if template_kind.evaluator_contract() != Some(contract_kind) { + return Err(invalid(format!( + "evaluator binding {index} contract kind does not match semantic template {template}" + ))); + } + if let Some(previous) = semantic_owners.insert(*template, index) + && previous != index + { + return Err(invalid(format!( + "semantic template {template} belongs to evaluator bindings {previous} and {index}" + ))); + } + } + + let callable_contract = CallableContractKey { + contract_kind, + callable_signature_digest_id: row.callable_signature_digest_id, + input_layout: input_layout.to_vec(), + output_layout: output_layout.to_vec(), + exact_expression_digests: exact_expressions.to_vec(), + }; + let (owner, previous) = match callable_kind { + EvaluatorCallableKind::PreparedKernel => ( + "prepared kernel ID", + prepared_contracts.insert(row.prepared_kernel_id, callable_contract.clone()), + ), + EvaluatorCallableKind::RusticolTemplate => ( + "Rusticol runtime template", + runtime_contracts + .insert(row.runtime_template_string_id, callable_contract.clone()), + ), + }; + if previous.is_some_and(|previous| previous != callable_contract) { + return Err(invalid(format!( + "{owner} has inconsistent evaluator callable contracts" + ))); + } + } + checked_len(prepared_ids.len(), "prepared kernels") + } + + fn validate_proof_subjects( + self, + template_kinds: &BTreeMap, + ) -> RusticolResult<()> { + for (index, proof) in self.symmetry_proofs.iter().enumerate() { + let subjects = + u32_sequence(self, proof.subject_template_sequence_id, "proof subjects")?; + for subject in subjects { + if !template_kinds.contains_key(subject) { + return Err(invalid(format!( + "symmetry proof {index} references unknown template string id {subject}" + ))); + } + } + } + Ok(()) + } + + fn validate_evaluator_state_contracts( + self, + _template_kinds: &BTreeMap, + ) -> RusticolResult<()> { + for source in self.sources { + self.validate_evaluator_contract( + source.evaluator_binding_id, + EvaluatorContractKind::Source, + &[], + Some(source.state_template_id), + source.template_string_id, + )?; + } + for transition in self.transitions { + self.validate_evaluator_contract( + transition.evaluator_binding_id, + EvaluatorContractKind::Vertex, + u32_sequence( + self, + transition.input_state_sequence_id, + "transition states", + )?, + Some(transition.result_state_template_id), + transition.template_string_id, + )?; + } + for propagator in self.propagators { + if propagator.evaluator_binding_id != MISSING_U32 { + self.validate_evaluator_contract( + propagator.evaluator_binding_id, + EvaluatorContractKind::Propagator, + &[propagator.state_template_id], + Some(propagator.state_template_id), + propagator.template_string_id, + )?; + } + } + for closure in self.closures { + self.validate_evaluator_contract( + closure.evaluator_binding_id, + EvaluatorContractKind::Closure, + u32_sequence(self, closure.input_state_sequence_id, "closure states")?, + None, + closure.template_string_id, + )?; + } + Ok(()) + } + + fn validate_evaluator_contract( + self, + evaluator_id: u32, + expected_kind: EvaluatorContractKind, + expected_inputs: &[u32], + expected_output: Option, + semantic_template_string_id: u32, + ) -> RusticolResult<()> { + let evaluator = self + .evaluator_bindings + .get(evaluator_id as usize) + .ok_or_else(|| invalid(format!("unknown evaluator binding {evaluator_id}")))?; + if EvaluatorContractKind::try_from(evaluator.contract_kind)? != expected_kind { + return Err(invalid(format!( + "semantic template string id {semantic_template_string_id} uses the wrong evaluator contract kind" + ))); + } + let inputs = u32_sequence(self, evaluator.input_state_sequence_id, "evaluator states")?; + if inputs != expected_inputs { + return Err(invalid(format!( + "semantic template string id {semantic_template_string_id} evaluator input states do not match" + ))); + } + if optional_id(evaluator.output_state_template_id) != expected_output { + return Err(invalid(format!( + "semantic template string id {semantic_template_string_id} evaluator output state does not match" + ))); + } + let templates = u32_sequence( + self, + evaluator.semantic_template_sequence_id, + "evaluator semantic templates", + )?; + if templates + .binary_search(&semantic_template_string_id) + .is_err() + { + return Err(invalid(format!( + "semantic template string id {semantic_template_string_id} is absent from its evaluator binding" + ))); + } + Ok(()) + } +} + +fn validate_rusticol_runtime_template( + catalogs: &ValidatedCatalogs<'_>, + row: &EvaluatorBindingRow, + contract_kind: EvaluatorContractKind, + index: usize, +) -> RusticolResult<()> { + let runtime_template = required_string( + &catalogs.strings, + row.runtime_template_string_id, + "evaluator runtime template", + )?; + let signature = required_digest( + &catalogs.digests, + row.callable_signature_digest_id, + "evaluator callable signature", + )? + .to_string(); + let signature_suffix = &signature[..24]; + match contract_kind { + EvaluatorContractKind::Source => { + const PREFIX: &str = "rusticol.source-fill."; + let suffix = format!(".v1:{signature_suffix}"); + let family = runtime_template + .strip_prefix(PREFIX) + .and_then(|value| value.strip_suffix(&suffix)) + .ok_or_else(|| { + invalid(format!( + "Rusticol source evaluator binding {index} has an unauthenticated runtime template" + )) + })?; + if !matches!( + family, + "scalar" | "fermion" | "vector" | "spin2" | "ghost" | "auxiliary" + ) { + return Err(invalid(format!( + "Rusticol source evaluator binding {index} has unsupported family {family:?}" + ))); + } + } + EvaluatorContractKind::Closure => { + let expected = format!("rusticol.closure-reduce.v1:{signature_suffix}"); + if runtime_template != expected { + return Err(invalid(format!( + "Rusticol closure evaluator binding {index} has an unauthenticated runtime template" + ))); + } + } + _ => { + return Err(invalid(format!( + "Rusticol runtime templates are not registered for evaluator binding {index} contract {contract_kind:?}" + ))); + } + } + Ok(()) +} + +fn validate_indexed_ranges( + label: &str, + ranges: &[IndexedRangeRow], + value_len: usize, +) -> RusticolResult<()> { + validate_canonical_ids(label, ranges.iter().map(|row| row.id))?; + let checked: Vec<_> = ranges.iter().map(|row| row.range).collect(); + validate_packed_ranges(label, &checked, value_len) +} + +fn validate_sequence_catalog( + label: &str, + ranges: &[IndexedRangeRow], + values: &[T], + require_nonempty: bool, +) -> RusticolResult<()> { + validate_indexed_ranges(label, ranges, values.len())?; + let mut previous: Option<&[T]> = None; + for (index, row) in ranges.iter().enumerate() { + let sequence = &values[row.range.as_usize_range(values.len(), label)?]; + if require_nonempty && sequence.is_empty() { + return Err(invalid(format!("{label} row {index} is empty"))); + } + if let Some(previous) = previous + && previous >= sequence + { + return Err(invalid(format!( + "{label} is not in strict canonical order at row {index}" + ))); + } + previous = Some(sequence); + } + Ok(()) +} + +fn validate_canonical_ids(label: &str, ids: impl IntoIterator) -> RusticolResult<()> { + for (index, id) in ids.into_iter().enumerate() { + if usize::try_from(id).ok() != Some(index) { + return Err(invalid(format!( + "{label} row {index} has noncanonical id {id}" + ))); + } + } + Ok(()) +} + +fn validate_record_ids( + label: &str, + ids: impl IntoIterator, + identity_string_ids: impl IntoIterator, +) -> RusticolResult<()> { + validate_canonical_ids(label, ids)?; + let mut previous = None; + for (index, id) in identity_string_ids.into_iter().enumerate() { + if let Some(previous) = previous + && previous >= id + { + return Err(invalid(format!( + "{label} records are not in strict semantic-identity order at row {index}" + ))); + } + previous = Some(id); + } + Ok(()) +} + +fn validate_bool(value: u8, label: &str) -> RusticolResult { + match value { + 0 => Ok(false), + 1 => Ok(true), + _ => Err(invalid(format!( + "{label} must be encoded as 0 or 1, found {value}" + ))), + } +} + +fn validate_color_contract_shape( + row: usize, + input_representations: &[i32], + has_output_representation: u8, + output_representation: i32, + ordered_open_string_arity: u32, +) -> RusticolResult<()> { + let has_output = validate_bool( + has_output_representation, + "color output-representation presence", + )?; + if !has_output && output_representation != 0 { + return Err(invalid(format!( + "color contraction {row} has no output representation but carries nonzero payload {output_representation}" + ))); + } + + // Every ordered open string has two endpoints. This slot-count bound is + // representation independent and therefore also applies to external + // models using an otherwise unknown representation encoding. + let endpoint_slots = input_representations + .len() + .checked_add(usize::from(has_output)) + .ok_or_else(|| invalid("color endpoint-slot count exceeds usize"))?; + let arity = usize::try_from(ordered_open_string_arity) + .map_err(|_| invalid("color open-string arity exceeds usize"))?; + if arity > endpoint_slots / 2 { + return Err(invalid(format!( + "color contraction {row} declares {arity} ordered open strings but has only {endpoint_slots} endpoint slots" + ))); + } + + let representations_are_standard = input_representations + .iter() + .copied() + .chain(has_output.then_some(output_representation)) + .all(|value| matches!(value, -3 | 1 | 3 | 8)); + if representations_are_standard { + let fundamental_endpoints = input_representations + .iter() + .copied() + .chain(has_output.then_some(output_representation)) + .filter(|value| matches!(value, -3 | 3)) + .count(); + if arity > fundamental_endpoints / 2 { + return Err(invalid(format!( + "color contraction {row} declares {arity} ordered open strings but its standard representations provide only {fundamental_endpoints} fundamental endpoints" + ))); + } + } + Ok(()) +} + +fn validate_lc_color_shape_name(value: &str, label: &str) -> RusticolResult<()> { + if matches!( + value, + "singlet-forest" + | "fundamental-open-string" + | "antifundamental-open-string" + | "adjoint-segment" + ) { + Ok(()) + } else { + Err(invalid(format!( + "{label} has unsupported LC color shape {value:?}" + ))) + } +} + +fn validate_lc_color_shape(value: &str, representation: i32, label: &str) -> RusticolResult<()> { + validate_lc_color_shape_name(value, label)?; + let expected = match representation { + 1 => "singlet-forest", + 3 => "fundamental-open-string", + -3 => "antifundamental-open-string", + 8 => "adjoint-segment", + other => { + return Err(invalid(format!( + "{label} uses unsupported LC representation {other}" + ))); + } + }; + if value != expected { + return Err(invalid(format!( + "{label} maps LC representation {representation} to {value:?}, expected {expected:?}" + ))); + } + Ok(()) +} + +fn checked_len(length: usize, label: &str) -> RusticolResult { + u32::try_from(length).map_err(|_| invalid(format!("{label} count {length} exceeds u32"))) +} + +fn required_reference(value: u32, target_len: usize, label: &str) -> RusticolResult { + let value = + usize::try_from(value).map_err(|_| invalid(format!("{label} id {value} exceeds usize")))?; + if value >= target_len { + return Err(invalid(format!( + "{label} id {value} exceeds target length {target_len}" + ))); + } + Ok(value) +} + +fn optional_reference(value: u32, target_len: usize, label: &str) -> RusticolResult> { + if value == MISSING_U32 { + Ok(None) + } else { + required_reference(value, target_len, label).map(Some) + } +} + +const fn optional_id(value: u32) -> Option { + if value == MISSING_U32 { + None + } else { + Some(value) + } +} + +fn required_string<'a>(strings: &[&'a str], id: u32, label: &str) -> RusticolResult<&'a str> { + strings + .get(required_reference(id, strings.len(), label)?) + .copied() + .ok_or_else(|| invalid(format!("{label} is absent"))) +} + +fn optional_string<'a>( + strings: &[&'a str], + id: u32, + label: &str, +) -> RusticolResult> { + optional_reference(id, strings.len(), label)? + .map(|index| { + strings + .get(index) + .copied() + .ok_or_else(|| invalid(format!("{label} is absent"))) + }) + .transpose() +} + +fn require_string_value( + strings: &[&str], + id: u32, + expected: &str, + label: &str, +) -> RusticolResult<()> { + let actual = required_string(strings, id, label)?; + if actual != expected { + return Err(RusticolError::compatibility(format!( + "unsupported {label} {actual:?}; expected {expected:?}" + ))); + } + Ok(()) +} + +fn required_digest( + digests: &[SemanticDigest], + id: u32, + label: &str, +) -> RusticolResult { + digests + .get(required_reference(id, digests.len(), label)?) + .copied() + .ok_or_else(|| invalid(format!("{label} is absent"))) +} + +fn validate_string_pair_sequence( + strings: &[&str], + values: &[u32], + label: &str, +) -> RusticolResult<()> { + if !values.len().is_multiple_of(2) { + return Err(invalid(format!("{label} must contain key/value pairs"))); + } + let mut previous_key = None; + for pair in values.chunks_exact(2) { + let key = required_string(strings, pair[0], &format!("{label} key"))?; + required_string(strings, pair[1], &format!("{label} value"))?; + if previous_key.is_some_and(|previous| previous >= key) { + return Err(invalid(format!("{label} keys must be sorted and unique"))); + } + previous_key = Some(key); + } + Ok(()) +} + +fn optional_digest( + digests: &[SemanticDigest], + id: u32, + label: &str, +) -> RusticolResult> { + optional_reference(id, digests.len(), label)? + .map(|index| { + digests + .get(index) + .copied() + .ok_or_else(|| invalid(format!("{label} is absent"))) + }) + .transpose() +} + +fn required_factor<'a>( + factors: &'a [ExactComplexRational], + id: u32, + label: &str, +) -> RusticolResult<&'a ExactComplexRational> { + factors + .get(required_reference(id, factors.len(), label)?) + .ok_or_else(|| invalid(format!("{label} is absent"))) +} + +fn optional_factor<'a>( + factors: &'a [ExactComplexRational], + id: u32, + label: &str, +) -> RusticolResult> { + optional_reference(id, factors.len(), label)? + .map(|index| { + factors + .get(index) + .ok_or_else(|| invalid(format!("{label} is absent"))) + }) + .transpose() +} + +fn sequence<'a, T>( + ranges: &[IndexedRangeRow], + values: &'a [T], + id: u32, + label: &str, +) -> RusticolResult<&'a [T]> { + let row = ranges + .get(required_reference(id, ranges.len(), label)?) + .ok_or_else(|| invalid(format!("{label} range is absent")))?; + let range = row.range.as_usize_range(values.len(), label)?; + Ok(&values[range]) +} + +fn u32_sequence<'a>( + input: RecurrenceTemplateInputView<'a>, + id: u32, + label: &str, +) -> RusticolResult<&'a [u32]> { + sequence( + input.u32_sequence_ranges, + input.u32_sequence_values, + id, + label, + ) +} + +fn i32_sequence<'a>( + input: RecurrenceTemplateInputView<'a>, + id: u32, + label: &str, +) -> RusticolResult<&'a [i32]> { + sequence( + input.i32_sequence_ranges, + input.i32_sequence_values, + id, + label, + ) +} + +fn validate_flavour_flow_witness( + input: RecurrenceTemplateInputView<'_>, + row: usize, + operation: &str, + input_flavour_ids: &[u32], + result_state_id: u32, + result_flavour_id: u32, +) -> RusticolResult<()> { + if input_flavour_ids.len() != 2 { + return Err(invalid(format!( + "quantum flow {row} must have two flavour-flow inputs" + ))); + } + let left = sequence( + input.flavour_flow_ranges, + input.flavour_flow_values, + input_flavour_ids[0], + "quantum-flow left flavour flow", + )?; + let right = sequence( + input.flavour_flow_ranges, + input.flavour_flow_values, + input_flavour_ids[1], + "quantum-flow right flavour flow", + )?; + let result = sequence( + input.flavour_flow_ranges, + input.flavour_flow_values, + result_flavour_id, + "quantum-flow result flavour flow", + )?; + let result_particle = input.current_states[result_state_id as usize].particle_id; + let mut expected = Vec::with_capacity(left.len() + right.len() + 1); + match operation { + "constant-result" => expected.push(result_particle), + "append-left-result" => { + expected.extend_from_slice(left); + if left.last().copied() != Some(result_particle) { + expected.push(result_particle); + } + } + "append-right-result" => { + expected.extend_from_slice(right); + if right.last().copied() != Some(result_particle) { + expected.push(result_particle); + } + } + "concat-left-right-result" => { + expected.extend_from_slice(left); + expected.extend_from_slice(right); + expected.push(result_particle); + } + _ => return Err(invalid("unsupported flavour-flow operation")), + } + if result != expected { + return Err(invalid(format!( + "quantum flow {row} flavour operation does not reproduce its stored result witness" + ))); + } + Ok(()) +} + +fn validate_strict_u32(values: &[u32], label: &str) -> RusticolResult<()> { + for (index, pair) in values.windows(2).enumerate() { + if pair[0] >= pair[1] { + return Err(invalid(format!( + "{label} is not strictly ordered at item {}", + index + 1 + ))); + } + } + Ok(()) +} + +fn validate_permutation(values: &[u32], label: &str) -> RusticolResult<()> { + validate_permutation_of(values, values.len(), label) +} + +fn validate_permutation_of(values: &[u32], arity: usize, label: &str) -> RusticolResult<()> { + if values.len() != arity { + return Err(invalid(format!( + "{label} length {} does not match arity {arity}", + values.len() + ))); + } + let mut seen = vec![false; arity]; + for value in values { + let index = usize::try_from(*value) + .map_err(|_| invalid(format!("{label} item {value} exceeds usize")))?; + if index >= arity || seen[index] { + return Err(invalid(format!( + "{label} is not a permutation of 0..{arity}" + ))); + } + seen[index] = true; + } + Ok(()) +} + +fn register_template( + templates: &mut BTreeMap, + string_id: u32, + kind: TemplateKind, + label: &str, +) -> RusticolResult<()> { + if let Some(previous) = templates.insert(string_id, kind) { + return Err(invalid(format!( + "{label} reuses global template string id {string_id} already owned by {previous:?}" + ))); + } + Ok(()) +} + +fn register_semantic_digest( + digests: &mut BTreeSet, + digest_id: u32, + label: &str, +) -> RusticolResult<()> { + if !digests.insert(digest_id) { + return Err(invalid(format!( + "{label} reuses semantic digest id {digest_id}" + ))); + } + Ok(()) +} + +fn parse_canonical_i128(value: &str, positive: bool, label: &str) -> RusticolResult { + let parsed = value + .parse::() + .map_err(|_| invalid(format!("{label} {value:?} is outside the i128 domain")))?; + if parsed.to_string() != value { + return Err(invalid(format!( + "{label} {value:?} is not a canonical decimal integer" + ))); + } + if parsed == i128::MIN { + return Err(invalid(format!( + "{label} cannot use i128::MIN in the symmetric exact-rational domain" + ))); + } + if positive && parsed <= 0 { + return Err(invalid(format!("{label} must be positive"))); + } + Ok(parsed) +} + +fn is_supported_proof_algorithm(value: &str) -> bool { + matches!( + value, + "canonical-crossing-bijection-v1" + | "canonical-current-word-reversal-v1" + | "canonical-kernel-input-exchange-v1" + | "canonical-model-contract-label-equivariance-v1" + | "canonical-recurrence-replay-witness-v1" + | "canonical-recurrence-union-witness-v1" + | "canonical-source-transition-dependency-shape-v1" + | "canonical-trace-amplitude-reversal-v1" + | "exact-expression-identity-v1" + | "prepared-kernel-homogeneous-complex-linear-current-v1" + | "prepared-kernel-independent-current-block-v1" + ) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn empty_canonical_input() -> OwnedRecurrenceTemplateInput { + let mut strings = vec![ + RECURRENCE_TEMPLATE_INPUT_ABI, + RECURRENCE_TEMPLATE_ABI, + RECURRENCE_TEMPLATE_CANONICALIZATION_ABI, + RECURRENCE_TEMPLATE_EXACT_SCALAR_ABI, + ]; + strings.sort_unstable(); + strings.dedup(); + let string_id = + |value: &str| u32::try_from(strings.binary_search(&value).unwrap()).unwrap(); + let mut string_ranges = Vec::new(); + let mut string_bytes = Vec::new(); + for value in &strings { + string_ranges.push(CheckedTableRange::new( + string_bytes.len() as u64, + value.len() as u64, + )); + string_bytes.extend_from_slice(value.as_bytes()); + } + + let compiled_model_digest = SemanticDigest::new([1; 32]).unwrap(); + let prepared_kernel_pack_digest = SemanticDigest::new([2; 32]).unwrap(); + let catalog_digest = SemanticDigest::new([3; 32]).unwrap(); + OwnedRecurrenceTemplateInput { + input_abi: RECURRENCE_TEMPLATE_INPUT_ABI.to_owned(), + catalog_digest, + compiled_model_digest, + prepared_kernel_pack_digest, + catalog_header: vec![CatalogHeaderRow { + schema_version: RECURRENCE_TEMPLATE_INPUT_SCHEMA_VERSION, + abi_string_id: string_id(RECURRENCE_TEMPLATE_ABI), + canonicalization_abi_string_id: string_id(RECURRENCE_TEMPLATE_CANONICALIZATION_ABI), + exact_scalar_abi_string_id: string_id(RECURRENCE_TEMPLATE_EXACT_SCALAR_ABI), + compiled_model_digest_id: 0, + prepared_kernel_pack_digest_id: 1, + catalog_digest_id: 2, + parameter_count: 0, + current_state_count: 0, + source_count: 0, + quantum_flow_count: 0, + contact_orbit_certificate_count: 0, + contact_orbit_step_count: 0, + transition_count: 0, + propagator_count: 0, + closure_count: 0, + color_contraction_count: 0, + symmetry_proof_count: 0, + runtime_helicity_contract_count: 0, + evaluator_binding_count: 0, + }], + coupling_order_ranges: vec![IndexedRangeRow { + id: 0, + range: CheckedTableRange::new(0, 0), + }], + coupling_order_terms: vec![], + contact_orbit_certificates: vec![], + contact_orbit_steps: vec![], + current_states: vec![], + digest_catalog: vec![ + DigestCatalogRow { + id: 0, + value: [1; 32], + }, + DigestCatalogRow { + id: 1, + value: [2; 32], + }, + DigestCatalogRow { + id: 2, + value: [3; 32], + }, + ], + evaluator_bindings: vec![], + exact_factors: vec![], + flavour_flow_ranges: vec![], + flavour_flow_values: vec![], + i32_sequence_ranges: vec![IndexedRangeRow { + id: 0, + range: CheckedTableRange::new(0, 0), + }], + i32_sequence_values: vec![], + parameters: vec![], + propagators: vec![], + quantum_flows: vec![], + quantum_number_flow_ranges: vec![], + quantum_number_flow_terms: vec![], + runtime_helicity_contracts: vec![], + runtime_helicity_variants: vec![], + runtime_helicity_embeddings: vec![], + runtime_helicity_projections: vec![], + sources: vec![], + string_ranges, + string_bytes, + symmetry_proofs: vec![], + transitions: vec![], + closures: vec![], + color_contractions: vec![], + lc_color_transition_witnesses: vec![], + color_nc_terms: vec![], + u32_sequence_ranges: vec![IndexedRangeRow { + id: 0, + range: CheckedTableRange::new(0, 0), + }], + u32_sequence_values: vec![], + } + } + + #[test] + fn output_factor_source_is_closed() { + assert_eq!( + OutputFactorSource::try_from(0).unwrap(), + OutputFactorSource::None + ); + assert!(OutputFactorSource::try_from(3).is_err()); + } + + #[test] + fn canonical_i128_rejects_aliases_and_asymmetric_minimum() { + assert_eq!(parse_canonical_i128("-17", false, "value").unwrap(), -17); + assert!(parse_canonical_i128("+17", false, "value").is_err()); + assert!(parse_canonical_i128("017", false, "value").is_err()); + assert!(parse_canonical_i128(&i128::MIN.to_string(), false, "value").is_err()); + } + + #[test] + fn permutations_are_checked_exactly() { + validate_permutation_of(&[2, 0, 1], 3, "test permutation").unwrap(); + assert!(validate_permutation_of(&[0, 0, 1], 3, "test permutation").is_err()); + assert!(validate_permutation_of(&[0, 1], 3, "test permutation").is_err()); + } + + #[test] + fn canonical_sequence_order_rejects_duplicates() { + let ranges = [ + IndexedRangeRow { + id: 0, + range: CheckedTableRange::new(0, 1), + }, + IndexedRangeRow { + id: 1, + range: CheckedTableRange::new(1, 1), + }, + ]; + assert!(validate_sequence_catalog("test", &ranges, &[4_u32, 4], false).is_err()); + } + + #[test] + fn absent_color_output_requires_zero_sentinel() { + validate_color_contract_shape(0, &[3, -3], 0, 0, 1).unwrap(); + assert!(validate_color_contract_shape(0, &[3, -3], 0, 8, 1).is_err()); + assert!(validate_color_contract_shape(0, &[3, -3], 2, 0, 1).is_err()); + } + + #[test] + fn color_open_string_arity_respects_available_endpoints() { + validate_color_contract_shape(0, &[3, 8], 1, 3, 1).unwrap(); + assert!(validate_color_contract_shape(0, &[3, 8], 1, 3, 2).is_err()); + assert!(validate_color_contract_shape(0, &[8, 8], 1, 8, 1).is_err()); + + // Unknown representation encodings retain the generic endpoint-slot + // bound without acquiring a model-specific fundamental-role rule. + validate_color_contract_shape(0, &[41, -41], 0, 0, 1).unwrap(); + } + + #[test] + fn empty_model_catalog_validates_end_to_end() { + let validated = empty_canonical_input().validate().unwrap(); + assert_eq!(validated.summary().current_state_count, 0); + assert_eq!(validated.summary().prepared_kernel_count, 0); + let semantic_index = validated.semantic_index().unwrap(); + assert!(semantic_index.is_empty()); + assert_eq!( + semantic_index.compiled_model_digest, + validated.summary().compiled_model_digest + ); + assert_eq!( + semantic_index.catalog_digest, + validated.summary().catalog_digest + ); + } + + #[test] + fn closure_component_coefficient_catalog_decodes_every_template() { + let mut input = crate::recurrence::validated_template_fixture().into_input(); + let unit = input.exact_factors[0]; + input.exact_factors.push(ExactFactorRow { + id: 1, + real_numerator_string_id: unit.real_numerator_string_id, + real_denominator_string_id: unit.real_denominator_string_id, + imag_numerator_string_id: unit.real_numerator_string_id, + imag_denominator_string_id: unit.imag_denominator_string_id, + }); + input.closures[1].component_coefficient_sequence_id = + input.closures[1].canonical_input_order_sequence_id; + + let catalog = input + .validate() + .unwrap() + .closure_component_coefficient_catalog() + .unwrap(); + let unit_plus_i = ExactComplexRational::parse_parts("1", "1", "1", "1").unwrap(); + assert_eq!(catalog.len(), 2); + assert_eq!(catalog[0].as_ref(), &[ExactComplexRational::ONE]); + assert_eq!( + catalog[1].as_ref(), + &[ExactComplexRational::ONE, unit_plus_i] + ); + } + + #[test] + fn closure_component_coefficient_catalog_rejects_corrupt_validated_state() { + let mut templates = crate::recurrence::validated_template_fixture(); + templates.input.closures[0].component_coefficient_sequence_id = + templates.input.closures[0].canonical_input_order_sequence_id; + let error = templates + .closure_component_coefficient_catalog() + .unwrap_err() + .to_string(); + assert!( + error.contains("closure component factor ID is out of range"), + "unexpected validation error: {error}" + ); + + let mut templates = crate::recurrence::validated_template_fixture(); + templates.input.closures[0].component_coefficient_sequence_id = MISSING_U32; + let error = templates + .closure_component_coefficient_catalog() + .unwrap_err() + .to_string(); + assert!( + error.contains("closure component coefficients id") + && error.contains("exceeds target length"), + "unexpected validation error: {error}" + ); + + let mut templates = crate::recurrence::validated_template_fixture(); + templates.input.closures[1].id = 7; + let error = templates + .closure_component_coefficient_catalog() + .unwrap_err() + .to_string(); + assert!( + error.contains("closure template 1 has noncanonical ID 7"), + "unexpected validation error: {error}" + ); + } + + #[test] + fn indirect_parameter_dependency_cycles_are_rejected() { + let mut input = empty_canonical_input(); + input.catalog_header[0].parameter_count = 2; + input.u32_sequence_ranges.extend([ + IndexedRangeRow { + id: 1, + range: CheckedTableRange::new(0, 1), + }, + IndexedRangeRow { + id: 2, + range: CheckedTableRange::new(1, 1), + }, + ]); + input.u32_sequence_values.extend([0, 1]); + let mut parameter_template_ids = [ + input.catalog_header[0].abi_string_id, + input.catalog_header[0].canonicalization_abi_string_id, + ]; + parameter_template_ids.sort_unstable(); + input.parameters = vec![ + ParameterRow { + id: 0, + template_string_id: parameter_template_ids[0], + name_string_id: input.catalog_header[0].canonicalization_abi_string_id, + kind: ParameterKind::Derived as u8, + value_type: ParameterValueType::Complex as u8, + mutable: 0, + default_factor_id: MISSING_U32, + exact_expression_digest_id: 0, + dependency_sequence_id: 2, + prepared_parameter_id: MISSING_U32, + semantic_digest_id: 0, + }, + ParameterRow { + id: 1, + template_string_id: parameter_template_ids[1], + name_string_id: input.catalog_header[0].exact_scalar_abi_string_id, + kind: ParameterKind::Derived as u8, + value_type: ParameterValueType::Complex as u8, + mutable: 0, + default_factor_id: MISSING_U32, + exact_expression_digest_id: 1, + dependency_sequence_id: 1, + prepared_parameter_id: MISSING_U32, + semantic_digest_id: 1, + }, + ]; + + let error = input.validate().unwrap_err().to_string(); + assert!( + error.contains("parameter dependency graph contains a cycle"), + "unexpected validation error: {error}" + ); + } +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/recurrence/template_json.rs b/pyamplicol/rust/crates/rusticol-core/src/recurrence/template_json.rs new file mode 100644 index 0000000000..f8e49c1fd4 --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/recurrence/template_json.rs @@ -0,0 +1,2956 @@ +// SPDX-License-Identifier: 0BSD + +//! Strict projection of the retained semantic recurrence-template JSON. +//! +//! Prepared kernel packs retain the model-wide recurrence template as the +//! canonical `RecurrenceTemplateCatalog.to_dict()` object. This module turns +//! that authenticated semantic object directly into the primitive owned core +//! columns. It deliberately has no Python, NumPy, process-builder, or direct +//! plan dependency. + +use std::collections::{BTreeMap, BTreeSet}; +use std::fmt::Write as _; + +use serde::{Deserialize, Deserializer}; +use serde_json::Value; +use sha2::{Digest, Sha256}; + +use super::template::{ + CatalogHeaderRow, ClosureRow, ColorContractionRow, ColorNcTermRow, ContactOrbitCertificateRow, + ContactOrbitStepRow, CouplingOrderTermRow, CurrentStateRow, DigestCatalogRow, + EvaluatorBindingRow, ExactFactorRow, IndexedRangeRow, LCColorTransitionWitnessRow, MISSING_U32, + OwnedRecurrenceTemplateInput, ParameterRow, PropagatorRow, QuantumFlowRow, + QuantumNumberFlowTermRow, RECURRENCE_TEMPLATE_CANONICALIZATION_ABI, + RECURRENCE_TEMPLATE_EXACT_SCALAR_ABI, RECURRENCE_TEMPLATE_INPUT_ABI, + RECURRENCE_TEMPLATE_INPUT_SCHEMA_VERSION, RuntimeHelicityContractRow, + RuntimeHelicityEmbeddingRow, RuntimeHelicityProjectionRow, RuntimeHelicityVariantRow, + SourceRow, SymmetryProofRow, TransitionRow, +}; +use super::{CheckedTableRange, ExactComplexRational, RECURRENCE_TEMPLATE_ABI, SemanticDigest}; +use crate::{RusticolError, RusticolResult}; + +const ROOT_SECTIONS: [(&str, &str, &str); 13] = [ + ("parameters", "parameter", "template_id"), + ("current_states", "current-state", "template_id"), + ("sources", "source", "template_id"), + ("quantum_flows", "quantum-flow", "template_id"), + ( + "contact_orbit_certificates", + "contact-orbit-certificate", + "template_id", + ), + ("contact_orbit_steps", "contact-orbit-step", "template_id"), + ("transitions", "transition", "template_id"), + ("propagators", "propagator", "template_id"), + ("closures", "closure", "template_id"), + ("color_contractions", "color-contraction", "template_id"), + ("symmetry_proofs", "symmetry-proof", "template_id"), + ( + "runtime_helicity_contracts", + "runtime-helicity-contract", + "template_id", + ), + ("evaluator_bindings", "evaluator-binding", "resolver_key"), +]; + +fn invalid(message: impl Into) -> RusticolError { + RusticolError::invalid_argument(message) +} + +#[derive(Clone, Debug)] +struct RequiredOption(Option); + +impl<'de, T> Deserialize<'de> for RequiredOption +where + T: Deserialize<'de>, +{ + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + Option::::deserialize(deserializer).map(Self) + } +} + +#[derive(Clone, Debug, Default)] +enum OptionalField { + #[default] + Absent, + Present(T), +} + +impl<'de, T> Deserialize<'de> for OptionalField +where + T: Deserialize<'de>, +{ + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + T::deserialize(deserializer).map(Self::Present) + } +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct SemanticCatalogJson { + header: CatalogHeaderJson, + parameters: Vec, + current_states: Vec, + sources: Vec, + quantum_flows: Vec, + contact_orbit_certificates: Vec, + contact_orbit_steps: Vec, + transitions: Vec, + propagators: Vec, + closures: Vec, + color_contractions: Vec, + symmetry_proofs: Vec, + runtime_helicity_contracts: Vec, + evaluator_bindings: Vec, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct CatalogHeaderJson { + abi: String, + canonicalization_abi: String, + compiled_model_digest: String, + exact_scalar_abi: String, + prepared_kernel_pack_digest: String, + catalog_digest: String, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct ExactFactorJson { + real_numerator: String, + real_denominator: String, + imag_numerator: String, + imag_denominator: String, +} + +#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] +struct FactorKey { + parts: [i128; 4], + strings: [String; 4], +} + +impl ExactFactorJson { + fn key(&self) -> RusticolResult { + let value = ExactComplexRational::parse_parts( + &self.real_numerator, + &self.real_denominator, + &self.imag_numerator, + &self.imag_denominator, + )?; + let real = value.real().to_string(); + let imag = value.imag().to_string(); + let expected_real = format!("{}/{}", self.real_numerator, self.real_denominator); + let expected_imag = format!("{}/{}", self.imag_numerator, self.imag_denominator); + if real != expected_real || imag != expected_imag { + return Err(invalid( + "exact complex rational components are not canonical reduced decimals", + )); + } + Ok(FactorKey { + parts: [ + parse_i128(&self.real_numerator, "real numerator")?, + parse_i128(&self.real_denominator, "real denominator")?, + parse_i128(&self.imag_numerator, "imaginary numerator")?, + parse_i128(&self.imag_denominator, "imaginary denominator")?, + ], + strings: [ + self.real_numerator.clone(), + self.real_denominator.clone(), + self.imag_numerator.clone(), + self.imag_denominator.clone(), + ], + }) + } +} + +fn parse_i128(value: &str, context: &str) -> RusticolResult { + value + .parse::() + .map_err(|_| invalid(format!("{context} is outside the exact i128 domain"))) +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct ParameterJson { + #[serde(rename = "record_kind")] + _record_kind: String, + semantic_digest: String, + template_id: String, + name: String, + parameter_kind: String, + value_type: String, + mutable: bool, + default_value: RequiredOption, + exact_expression_digest: RequiredOption, + dependency_parameter_ids: Vec, + prepared_parameter_id: RequiredOption, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct CurrentStateJson { + #[serde(rename = "record_kind")] + _record_kind: String, + semantic_digest: String, + template_id: String, + particle_id: i32, + anti_particle_id: i32, + species_id: String, + orientation: String, + statistics: String, + color_representation: i32, + basis: String, + tensor_ordering: Vec, + dimension: u32, + chirality: i32, + lc_color_shape_kind: String, + auxiliary_kind: RequiredOption, + mass_parameter_id: RequiredOption, + width_parameter_id: RequiredOption, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct LCColorSourceSeedJson { + operation: String, + output_shape_kind: String, + component_kind: RequiredOption, + component_role: String, + proof_digest: String, + provenance: Vec<[String; 2]>, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct SourceJson { + #[serde(rename = "record_kind")] + _record_kind: String, + semantic_digest: String, + template_id: String, + state_template_id: String, + crossing: String, + wavefunction_family: String, + helicity: i32, + spin_state: i32, + flavour_flow: Vec, + quantum_number_flow: Vec<[String; 2]>, + lc_color_seed: LCColorSourceSeedJson, + wavefunction_expression_digest: String, + evaluator_resolver_key: String, + mass_parameter_id: RequiredOption, + width_parameter_id: RequiredOption, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct QuantumFlowJson { + #[serde(rename = "record_kind")] + _record_kind: String, + semantic_digest: String, + template_id: String, + input_state_template_ids: Vec, + input_spin_states: Vec, + input_flavour_flows: Vec>, + input_quantum_number_flows: Vec>, + flavour_flow_operation: String, + quantum_number_flow_operation: String, + coupling_orders: Vec<(String, u32)>, + result_state_template_id: String, + result_spin_state: i32, + result_flavour_flow: Vec, + result_quantum_number_flow: Vec<[String; 2]>, + exact_coupling: ExactFactorJson, + predicate_digest: String, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct ContactOrbitCertificateJson { + #[serde(rename = "record_kind")] + _record_kind: String, + semantic_digest: String, + template_id: String, + algorithm: String, + algorithm_version: u32, + term_id: u32, + vertex: String, + particles: Vec, + evaluator_class: String, + physical_leg_equivalence_classes: Vec, + reconstruction_factor: ExactFactorJson, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct ContactOrbitStepJson { + #[serde(rename = "record_kind")] + _record_kind: String, + semantic_digest: String, + template_id: String, + certificate_template_id: String, + stage: String, + result_leg: u8, + left_covered_legs: Vec, + right_covered_legs: Vec, + source_particle_legs: [i32; 3], + reconstruction_factor: ExactFactorJson, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct TransitionJson { + #[serde(rename = "record_kind")] + _record_kind: String, + semantic_digest: String, + template_id: String, + input_state_template_ids: Vec, + result_state_template_id: String, + quantum_flow_template_id: String, + evaluator_resolver_key: String, + canonical_input_order: Vec, + momentum_convention: Vec, + coupling_parameter_ids: Vec, + coupling_orders: Vec<(String, u32)>, + color_contraction_template_id: String, + binding_coupling: ExactFactorJson, + exact_factor: ExactFactorJson, + output_factor_source: String, + equivalence_class: String, + input_exchange_factor: RequiredOption, + output_projection: String, + contact_orbit_step_template_ids: Vec, + contact_orbit_step_semantic_digests: Vec, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct PropagatorJson { + #[serde(rename = "record_kind")] + _record_kind: String, + semantic_digest: String, + template_id: String, + state_template_id: String, + applies_propagator: bool, + evaluator_resolver_key: RequiredOption, + numerator_expression_digest: RequiredOption, + denominator_expression_digest: RequiredOption, + mass_parameter_id: RequiredOption, + width_parameter_id: RequiredOption, + gauge: RequiredOption, + linearity_proof_template_id: RequiredOption, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct ClosureJson { + #[serde(rename = "record_kind")] + _record_kind: String, + semantic_digest: String, + template_id: String, + input_state_template_ids: Vec, + result_state_template_id: RequiredOption, + evaluator_resolver_key: String, + canonical_input_order: Vec, + coupling_parameter_ids: Vec, + coupling_orders: Vec<(String, u32)>, + eligible_quantum_flow_template_ids: Vec, + color_contraction_template_id: String, + binding_coupling: ExactFactorJson, + exact_factor: ExactFactorJson, + output_factor_source: String, + equivalence_class: String, + input_exchange_factor: RequiredOption, + projection: String, + component_coefficients: Vec, + chirality_relation: String, + metric_signature: RequiredOption, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct LCColorTransitionWitnessJson { + component_operation: String, + exact_factor: ExactFactorJson, + input_permutation: [u8; 2], + input_shape_kinds: [String; 2], + proof_digest: String, + provenance: Vec<[String; 2]>, + result_component_kind: RequiredOption, + result_component_role: String, + result_shape_kind: RequiredOption, + reverse_parent_mask: u8, + #[serde(default)] + input_port_pairings: OptionalField>, + #[serde(default)] + result_port_bindings: OptionalField>, +} + +type CanonicalPortSlices<'a> = (&'a [[[u32; 2]; 2]], &'a [[u32; 2]]); + +impl LCColorTransitionWitnessJson { + fn canonical_ports(&self) -> RusticolResult> { + match (&self.input_port_pairings, &self.result_port_bindings) { + (OptionalField::Absent, OptionalField::Absent) => Ok((&[], &[])), + (OptionalField::Present(pairings), OptionalField::Present(bindings)) + if !pairings.is_empty() || !bindings.is_empty() => + { + Ok((pairings, bindings)) + } + (OptionalField::Present(_), OptionalField::Present(_)) => Err(invalid( + "LC color witness must omit empty canonical port fields", + )), + _ => Err(invalid( + "LC color witness port-pairing and result-binding fields must be paired", + )), + } + } +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct ColorContractionJson { + #[serde(rename = "record_kind")] + _record_kind: String, + semantic_digest: String, + template_id: String, + rule_kind: String, + input_representations: Vec, + output_representation: RequiredOption, + ordered_open_string_arity: u32, + exact_coefficient: ExactFactorJson, + nc_polynomial: Vec<(i32, ExactFactorJson)>, + expression_digest: String, + transition_witnesses: Vec, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct SymmetryProofJson { + #[serde(rename = "record_kind")] + _record_kind: String, + semantic_digest: String, + template_id: String, + proof_algorithm: String, + subject_template_ids: Vec, + input_permutation: Vec, + exact_phase: ExactFactorJson, + expression_digests: Vec, + witness_digest: String, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct RuntimeHelicityVariantJson { + source_template_id: String, + source_state_template_id: String, + embedding_source_components: Vec>, + embedding_factors: Vec, + projection_full_components: Vec, + proof_digest: String, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct RuntimeHelicityContractJson { + #[serde(rename = "record_kind")] + _record_kind: String, + semantic_digest: String, + template_id: String, + full_state_template_id: String, + variants: Vec, + proof_algorithm: String, + proof_digest: String, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct EvaluatorBindingJson { + #[serde(rename = "record_kind")] + _record_kind: String, + semantic_digest: String, + resolver_key: String, + prepared_kernel_id: RequiredOption, + contract_kind: String, + callable_signature: String, + input_state_template_ids: Vec, + output_state_template_id: RequiredOption, + input_layout: Vec, + output_layout: Vec, + exact_expression_digests: Vec, + semantic_template_ids: Vec, + callable_kind: String, + runtime_template: RequiredOption, +} + +#[derive(Clone, Debug)] +struct Catalog { + values: Vec, + ids: BTreeMap, +} + +impl Catalog { + fn from_set(values: BTreeSet, context: &str) -> RusticolResult { + let values = values.into_iter().collect::>(); + let mut ids = BTreeMap::new(); + for (index, value) in values.iter().cloned().enumerate() { + ids.insert(value, checked_u32(index, context)?); + } + Ok(Self { values, ids }) + } + + fn id(&self, value: &T, context: &str) -> RusticolResult { + self.ids + .get(value) + .copied() + .ok_or_else(|| invalid(format!("{context} is absent from its canonical catalog"))) + } +} + +fn checked_u32(value: usize, context: &str) -> RusticolResult { + u32::try_from(value).map_err(|_| invalid(format!("{context} exceeds u32"))) +} + +fn checked_u64(value: usize, context: &str) -> RusticolResult { + u64::try_from(value).map_err(|_| invalid(format!("{context} exceeds u64"))) +} + +fn optional_id( + value: Option<&T>, + ids: &BTreeMap, + context: &str, +) -> RusticolResult { + match value { + None => Ok(MISSING_U32), + Some(value) => ids + .get(value) + .copied() + .ok_or_else(|| invalid(format!("unknown {context}"))), + } +} + +fn mapped_ids( + values: &[String], + ids: &BTreeMap, + context: &str, +) -> RusticolResult> { + values + .iter() + .map(|value| { + ids.get(value) + .copied() + .ok_or_else(|| invalid(format!("unknown {context} {value:?}"))) + }) + .collect() +} + +fn enum_id(value: &str, choices: &[(&str, u8)], context: &str) -> RusticolResult { + choices + .iter() + .find_map(|(name, id)| (*name == value).then_some(*id)) + .ok_or_else(|| invalid(format!("unsupported {context} {value:?}"))) +} + +/// Project one retained `RecurrenceTemplateCatalog.to_dict()` value into the +/// validated, owned core recurrence-template input. +/// +/// The caller must pass the semantic catalog object itself, not a surrounding +/// prepared-pack object. The function authenticates every record digest and +/// the catalog digest before deriving any primitive IDs. +#[cfg(any(feature = "python-generation-bridge", test))] +pub(crate) fn project_recurrence_template_catalog_json_v1( + value: &Value, +) -> RusticolResult { + authenticate_semantic_json(value)?; + project_authenticated_recurrence_template_catalog_json_v1(value.clone()) +} + +/// Project an owned semantic catalog without retaining its JSON representation. +pub(crate) fn project_owned_recurrence_template_catalog_json_v1( + value: Value, +) -> RusticolResult { + authenticate_semantic_json(&value)?; + project_authenticated_recurrence_template_catalog_json_v1(value) +} + +fn project_authenticated_recurrence_template_catalog_json_v1( + value: Value, +) -> RusticolResult { + let catalog: SemanticCatalogJson = serde_json::from_value(value).map_err(|error| { + invalid(format!( + "invalid recurrence-template semantic JSON shape: {error}" + )) + })?; + validate_header(&catalog.header)?; + validate_witness_port_shapes(&catalog)?; + project_catalog(&catalog) +} + +fn validate_header(header: &CatalogHeaderJson) -> RusticolResult<()> { + if header.abi != RECURRENCE_TEMPLATE_ABI { + return Err(invalid(format!( + "unsupported recurrence-template ABI {:?}", + header.abi + ))); + } + if header.canonicalization_abi != RECURRENCE_TEMPLATE_CANONICALIZATION_ABI { + return Err(invalid(format!( + "unsupported recurrence-template canonicalization ABI {:?}", + header.canonicalization_abi + ))); + } + if header.exact_scalar_abi != RECURRENCE_TEMPLATE_EXACT_SCALAR_ABI { + return Err(invalid(format!( + "unsupported recurrence-template exact-scalar ABI {:?}", + header.exact_scalar_abi + ))); + } + parse_digest(&header.compiled_model_digest, "compiled-model digest")?; + parse_digest( + &header.prepared_kernel_pack_digest, + "prepared-kernel-pack digest", + )?; + parse_digest(&header.catalog_digest, "catalog digest")?; + Ok(()) +} + +fn validate_witness_port_shapes(catalog: &SemanticCatalogJson) -> RusticolResult<()> { + for color in &catalog.color_contractions { + for witness in &color.transition_witnesses { + witness.canonical_ports()?; + } + } + for contract in &catalog.runtime_helicity_contracts { + let mut previous_source: Option<&str> = None; + for variant in &contract.variants { + if previous_source + .is_some_and(|previous| previous >= variant.source_template_id.as_str()) + { + return Err(invalid( + "runtime-helicity variants must be in strict source-template order", + )); + } + previous_source = Some(&variant.source_template_id); + if variant.embedding_source_components.len() != variant.embedding_factors.len() { + return Err(invalid( + "runtime-helicity embedding components and factors must have equal length", + )); + } + } + } + Ok(()) +} + +fn authenticate_semantic_json(value: &Value) -> RusticolResult<()> { + let root = value + .as_object() + .ok_or_else(|| invalid("recurrence-template catalog must be a JSON object"))?; + let header = root + .get("header") + .and_then(Value::as_object) + .ok_or_else(|| invalid("recurrence-template catalog header must be an object"))?; + let expected_catalog_digest = header + .get("catalog_digest") + .and_then(Value::as_str) + .ok_or_else(|| invalid("recurrence-template catalog digest must be a string"))?; + parse_digest(expected_catalog_digest, "catalog digest")?; + + for (section, expected_kind, identity_field) in ROOT_SECTIONS { + let records = root + .get(section) + .and_then(Value::as_array) + .ok_or_else(|| invalid(format!("recurrence-template {section} must be an array")))?; + let mut previous_identity: Option<&str> = None; + for (index, record) in records.iter().enumerate() { + let object = record.as_object().ok_or_else(|| { + invalid(format!( + "recurrence-template {section} row {index} must be an object" + )) + })?; + let kind = object + .get("record_kind") + .and_then(Value::as_str) + .ok_or_else(|| { + invalid(format!( + "recurrence-template {section} row {index} lacks record_kind" + )) + })?; + if kind != expected_kind { + return Err(invalid(format!( + "recurrence-template {section} row {index} has record kind {kind:?}, expected {expected_kind:?}" + ))); + } + let identity = object + .get(identity_field) + .and_then(Value::as_str) + .ok_or_else(|| { + invalid(format!( + "recurrence-template {section} row {index} lacks string {identity_field}" + )) + })?; + if identity.is_empty() || previous_identity.is_some_and(|previous| previous >= identity) + { + return Err(invalid(format!( + "recurrence-template {section} identities are not in strict canonical order at row {index}" + ))); + } + previous_identity = Some(identity); + + let expected = object + .get("semantic_digest") + .and_then(Value::as_str) + .ok_or_else(|| { + invalid(format!( + "recurrence-template {section} row {index} lacks semantic_digest" + )) + })?; + parse_digest(expected, "record semantic digest")?; + let mut payload = record.clone(); + payload + .as_object_mut() + .expect("record object was checked") + .remove("semantic_digest"); + let actual = canonical_json_digest(&payload)?; + if expected != actual { + return Err(invalid(format!( + "stale recurrence-template semantic digest in {section} row {index}" + ))); + } + } + } + + let mut catalog_payload = value.clone(); + catalog_payload + .as_object_mut() + .and_then(|root| root.get_mut("header")) + .and_then(Value::as_object_mut) + .expect("catalog/header objects were checked") + .remove("catalog_digest"); + let actual_catalog_digest = canonical_json_digest(&catalog_payload)?; + if expected_catalog_digest != actual_catalog_digest { + return Err(invalid("stale recurrence-template catalog digest")); + } + Ok(()) +} + +fn parse_digest(value: &str, context: &str) -> RusticolResult<[u8; 32]> { + if value.len() != 64 + || !value + .bytes() + .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte)) + { + return Err(invalid(format!( + "{context} must be a lowercase SHA-256 digest" + ))); + } + let mut bytes = [0_u8; 32]; + for (index, output) in bytes.iter_mut().enumerate() { + let offset = index * 2; + *output = u8::from_str_radix(&value[offset..offset + 2], 16) + .map_err(|_| invalid(format!("{context} contains invalid hexadecimal")))?; + } + if bytes == [0; 32] { + return Err(invalid(format!("{context} must not be all zero"))); + } + Ok(bytes) +} + +fn semantic_digest(value: &str, context: &str) -> RusticolResult { + SemanticDigest::new(parse_digest(value, context)?) +} + +fn canonical_json_digest(value: &Value) -> RusticolResult { + let mut canonical = String::new(); + write_canonical_json(value, &mut canonical)?; + Ok(format!("{:x}", Sha256::digest(canonical.as_bytes()))) +} + +fn write_canonical_json(value: &Value, output: &mut String) -> RusticolResult<()> { + match value { + Value::Null => output.push_str("null"), + Value::Bool(value) => output.push_str(if *value { "true" } else { "false" }), + Value::Number(value) => output.push_str(&python_json_number(value)), + Value::String(value) => write_ascii_json_string(value, output), + Value::Array(values) => { + output.push('['); + for (index, value) in values.iter().enumerate() { + if index != 0 { + output.push(','); + } + write_canonical_json(value, output)?; + } + output.push(']'); + } + Value::Object(values) => { + output.push('{'); + let mut keys = values.keys().collect::>(); + keys.sort_unstable(); + for (index, key) in keys.iter().enumerate() { + if index != 0 { + output.push(','); + } + write_ascii_json_string(key, output); + output.push(':'); + write_canonical_json(&values[*key], output)?; + } + output.push('}'); + } + } + Ok(()) +} + +fn python_json_number(number: &serde_json::Number) -> String { + let rendered = number.to_string(); + let Some((mantissa, exponent)) = rendered.split_once('e') else { + return rendered; + }; + let (sign, digits) = if let Some(digits) = exponent.strip_prefix('-') { + ('-', digits) + } else if let Some(digits) = exponent.strip_prefix('+') { + ('+', digits) + } else { + ('+', exponent) + }; + format!("{mantissa}e{sign}{digits:0>2}") +} + +fn write_ascii_json_string(value: &str, output: &mut String) { + output.push('"'); + for character in value.chars() { + match character { + '"' => output.push_str("\\\""), + '\\' => output.push_str("\\\\"), + '\u{0008}' => output.push_str("\\b"), + '\u{000c}' => output.push_str("\\f"), + '\n' => output.push_str("\\n"), + '\r' => output.push_str("\\r"), + '\t' => output.push_str("\\t"), + '\u{0020}'..='\u{007e}' => output.push(character), + character if u32::from(character) <= 0xffff => { + let _ = write!(output, "\\u{:04x}", u32::from(character)); + } + character => { + let scalar = u32::from(character) - 0x1_0000; + let high = 0xd800 + (scalar >> 10); + let low = 0xdc00 + (scalar & 0x3ff); + let _ = write!(output, "\\u{high:04x}\\u{low:04x}"); + } + } + } + output.push('"'); +} + +#[derive(Debug)] +struct SectionIds { + parameters: BTreeMap, + current_states: BTreeMap, + sources: BTreeMap, + quantum_flows: BTreeMap, + contact_orbit_certificates: BTreeMap, + contact_orbit_steps: BTreeMap, + color_contractions: BTreeMap, + symmetry_proofs: BTreeMap, + evaluator_bindings: BTreeMap, +} + +fn identity_ids<'a>( + values: impl IntoIterator, + context: &str, +) -> RusticolResult> { + let mut result = BTreeMap::new(); + for (index, value) in values.into_iter().enumerate() { + if result + .insert(value.clone(), checked_u32(index, context)?) + .is_some() + { + return Err(invalid(format!("duplicate {context} identity {value:?}"))); + } + } + Ok(result) +} + +impl SectionIds { + fn build(catalog: &SemanticCatalogJson) -> RusticolResult { + Ok(Self { + parameters: identity_ids( + catalog.parameters.iter().map(|record| &record.template_id), + "parameter", + )?, + current_states: identity_ids( + catalog + .current_states + .iter() + .map(|record| &record.template_id), + "current-state", + )?, + sources: identity_ids( + catalog.sources.iter().map(|record| &record.template_id), + "source", + )?, + quantum_flows: identity_ids( + catalog + .quantum_flows + .iter() + .map(|record| &record.template_id), + "quantum-flow", + )?, + contact_orbit_certificates: identity_ids( + catalog + .contact_orbit_certificates + .iter() + .map(|record| &record.template_id), + "contact-orbit certificate", + )?, + contact_orbit_steps: identity_ids( + catalog + .contact_orbit_steps + .iter() + .map(|record| &record.template_id), + "contact-orbit step", + )?, + color_contractions: identity_ids( + catalog + .color_contractions + .iter() + .map(|record| &record.template_id), + "color contraction", + )?, + symmetry_proofs: identity_ids( + catalog + .symmetry_proofs + .iter() + .map(|record| &record.template_id), + "symmetry proof", + )?, + evaluator_bindings: identity_ids( + catalog + .evaluator_bindings + .iter() + .map(|record| &record.resolver_key), + "evaluator binding", + )?, + }) + } +} + +#[derive(Debug)] +struct PrimitiveCatalogs { + strings: Catalog, + digests: Catalog<[u8; 32]>, + factors: Catalog, + flavour_flows: Catalog>, + quantum_number_flows: Catalog>, + coupling_orders: Catalog>, + u32_sequences: Catalog>, + i32_sequences: Catalog>, +} + +fn qflow(values: &[[String; 2]]) -> Vec<(String, String)> { + values + .iter() + .map(|pair| (pair[0].clone(), pair[1].clone())) + .collect() +} + +fn add_optional_string(values: &mut BTreeSet, value: &RequiredOption) { + if let Some(value) = &value.0 { + values.insert(value.clone()); + } +} + +fn add_digest(values: &mut BTreeSet<[u8; 32]>, value: &str, context: &str) -> RusticolResult<()> { + values.insert(parse_digest(value, context)?); + Ok(()) +} + +fn add_optional_digest( + values: &mut BTreeSet<[u8; 32]>, + value: &RequiredOption, + context: &str, +) -> RusticolResult<()> { + if let Some(value) = &value.0 { + add_digest(values, value, context)?; + } + Ok(()) +} + +fn add_factor(values: &mut BTreeSet, value: &ExactFactorJson) -> RusticolResult<()> { + values.insert(value.key()?); + Ok(()) +} + +fn add_optional_factor( + values: &mut BTreeSet, + value: &RequiredOption, +) -> RusticolResult<()> { + if let Some(value) = &value.0 { + add_factor(values, value)?; + } + Ok(()) +} + +fn collect_strings(catalog: &SemanticCatalogJson) -> RusticolResult> { + let mut values = BTreeSet::from([ + RECURRENCE_TEMPLATE_INPUT_ABI.to_owned(), + catalog.header.abi.clone(), + catalog.header.canonicalization_abi.clone(), + catalog.header.exact_scalar_abi.clone(), + ]); + for record in &catalog.parameters { + values.extend([record.template_id.clone(), record.name.clone()]); + values.extend(record.dependency_parameter_ids.iter().cloned()); + } + for record in &catalog.current_states { + values.extend([ + record.template_id.clone(), + record.species_id.clone(), + record.basis.clone(), + record.lc_color_shape_kind.clone(), + ]); + values.extend(record.tensor_ordering.iter().cloned()); + add_optional_string(&mut values, &record.auxiliary_kind); + add_optional_string(&mut values, &record.mass_parameter_id); + add_optional_string(&mut values, &record.width_parameter_id); + } + for record in &catalog.sources { + values.extend([ + record.template_id.clone(), + record.crossing.clone(), + record.wavefunction_family.clone(), + record.evaluator_resolver_key.clone(), + record.lc_color_seed.output_shape_kind.clone(), + ]); + for pair in &record.lc_color_seed.provenance { + values.extend(pair.iter().cloned()); + } + for pair in &record.quantum_number_flow { + values.extend(pair.iter().cloned()); + } + } + for record in &catalog.quantum_flows { + values.extend([ + record.template_id.clone(), + record.flavour_flow_operation.clone(), + record.quantum_number_flow_operation.clone(), + ]); + for flow in record + .input_quantum_number_flows + .iter() + .chain(std::iter::once(&record.result_quantum_number_flow)) + { + for pair in flow { + values.extend(pair.iter().cloned()); + } + } + values.extend(record.coupling_orders.iter().map(|(name, _)| name.clone())); + } + for record in &catalog.contact_orbit_certificates { + values.extend([ + record.template_id.clone(), + record.algorithm.clone(), + record.vertex.clone(), + record.evaluator_class.clone(), + ]); + values.extend(record.particles.iter().cloned()); + } + for record in &catalog.contact_orbit_steps { + values.extend([ + record.template_id.clone(), + record.certificate_template_id.clone(), + record.stage.clone(), + ]); + } + for record in &catalog.transitions { + values.extend([ + record.template_id.clone(), + record.evaluator_resolver_key.clone(), + record.output_factor_source.clone(), + record.equivalence_class.clone(), + record.output_projection.clone(), + ]); + values.extend(record.momentum_convention.iter().cloned()); + values.extend(record.coupling_orders.iter().map(|(name, _)| name.clone())); + } + for record in &catalog.propagators { + values.insert(record.template_id.clone()); + add_optional_string(&mut values, &record.evaluator_resolver_key); + add_optional_string(&mut values, &record.gauge); + } + for record in &catalog.closures { + values.extend([ + record.template_id.clone(), + record.evaluator_resolver_key.clone(), + record.output_factor_source.clone(), + record.equivalence_class.clone(), + record.projection.clone(), + record.chirality_relation.clone(), + ]); + add_optional_string(&mut values, &record.metric_signature); + values.extend(record.coupling_orders.iter().map(|(name, _)| name.clone())); + } + for record in &catalog.color_contractions { + values.extend([record.template_id.clone(), record.rule_kind.clone()]); + for witness in &record.transition_witnesses { + values.extend(witness.input_shape_kinds.iter().cloned()); + add_optional_string(&mut values, &witness.result_shape_kind); + for pair in &witness.provenance { + values.extend(pair.iter().cloned()); + } + } + } + for record in &catalog.symmetry_proofs { + values.extend([record.template_id.clone(), record.proof_algorithm.clone()]); + values.extend(record.subject_template_ids.iter().cloned()); + } + for record in &catalog.runtime_helicity_contracts { + values.extend([ + record.template_id.clone(), + record.full_state_template_id.clone(), + record.proof_algorithm.clone(), + ]); + for variant in &record.variants { + values.extend([ + variant.source_template_id.clone(), + variant.source_state_template_id.clone(), + ]); + } + } + for record in &catalog.evaluator_bindings { + values.insert(record.resolver_key.clone()); + values.extend(record.input_layout.iter().cloned()); + values.extend(record.output_layout.iter().cloned()); + values.extend(record.semantic_template_ids.iter().cloned()); + add_optional_string(&mut values, &record.runtime_template); + } + + for factor in collect_factors(catalog)? { + values.extend(factor.strings); + } + Ok(values) +} + +fn collect_digests(catalog: &SemanticCatalogJson) -> RusticolResult> { + let mut values = BTreeSet::new(); + add_digest( + &mut values, + &catalog.header.catalog_digest, + "catalog digest", + )?; + add_digest( + &mut values, + &catalog.header.compiled_model_digest, + "compiled-model digest", + )?; + add_digest( + &mut values, + &catalog.header.prepared_kernel_pack_digest, + "prepared-kernel-pack digest", + )?; + macro_rules! record_digests { + ($records:expr) => { + for record in $records { + add_digest( + &mut values, + &record.semantic_digest, + "record semantic digest", + )?; + } + }; + } + record_digests!(&catalog.parameters); + record_digests!(&catalog.current_states); + record_digests!(&catalog.sources); + record_digests!(&catalog.quantum_flows); + record_digests!(&catalog.contact_orbit_certificates); + record_digests!(&catalog.contact_orbit_steps); + record_digests!(&catalog.transitions); + record_digests!(&catalog.propagators); + record_digests!(&catalog.closures); + record_digests!(&catalog.color_contractions); + record_digests!(&catalog.symmetry_proofs); + record_digests!(&catalog.runtime_helicity_contracts); + record_digests!(&catalog.evaluator_bindings); + + for record in &catalog.parameters { + add_optional_digest( + &mut values, + &record.exact_expression_digest, + "parameter expression digest", + )?; + } + for record in &catalog.sources { + add_digest( + &mut values, + &record.wavefunction_expression_digest, + "source expression digest", + )?; + add_digest( + &mut values, + &record.lc_color_seed.proof_digest, + "source color proof digest", + )?; + } + for record in &catalog.quantum_flows { + add_digest( + &mut values, + &record.predicate_digest, + "quantum predicate digest", + )?; + } + for record in &catalog.transitions { + for digest in &record.contact_orbit_step_semantic_digests { + add_digest(&mut values, digest, "contact-orbit step digest")?; + } + } + for record in &catalog.propagators { + add_optional_digest( + &mut values, + &record.numerator_expression_digest, + "propagator numerator digest", + )?; + add_optional_digest( + &mut values, + &record.denominator_expression_digest, + "propagator denominator digest", + )?; + } + for record in &catalog.color_contractions { + add_digest( + &mut values, + &record.expression_digest, + "color expression digest", + )?; + for witness in &record.transition_witnesses { + add_digest( + &mut values, + &witness.proof_digest, + "color witness proof digest", + )?; + } + } + for record in &catalog.symmetry_proofs { + add_digest( + &mut values, + &record.witness_digest, + "symmetry witness digest", + )?; + for digest in &record.expression_digests { + add_digest(&mut values, digest, "symmetry expression digest")?; + } + } + for record in &catalog.runtime_helicity_contracts { + add_digest(&mut values, &record.proof_digest, "helicity proof digest")?; + for variant in &record.variants { + add_digest( + &mut values, + &variant.proof_digest, + "helicity variant proof digest", + )?; + } + } + for record in &catalog.evaluator_bindings { + add_digest( + &mut values, + &record.callable_signature, + "callable signature digest", + )?; + for digest in &record.exact_expression_digests { + add_digest(&mut values, digest, "evaluator expression digest")?; + } + } + Ok(values) +} + +fn collect_factors(catalog: &SemanticCatalogJson) -> RusticolResult> { + let mut values = BTreeSet::new(); + for record in &catalog.parameters { + add_optional_factor(&mut values, &record.default_value)?; + } + for record in &catalog.transitions { + add_factor(&mut values, &record.binding_coupling)?; + add_factor(&mut values, &record.exact_factor)?; + add_optional_factor(&mut values, &record.input_exchange_factor)?; + } + for record in &catalog.contact_orbit_certificates { + add_factor(&mut values, &record.reconstruction_factor)?; + } + for record in &catalog.contact_orbit_steps { + add_factor(&mut values, &record.reconstruction_factor)?; + } + for record in &catalog.quantum_flows { + add_factor(&mut values, &record.exact_coupling)?; + } + for record in &catalog.closures { + add_factor(&mut values, &record.binding_coupling)?; + add_factor(&mut values, &record.exact_factor)?; + add_optional_factor(&mut values, &record.input_exchange_factor)?; + for factor in &record.component_coefficients { + add_factor(&mut values, factor)?; + } + } + for record in &catalog.color_contractions { + add_factor(&mut values, &record.exact_coefficient)?; + for (_, factor) in &record.nc_polynomial { + add_factor(&mut values, factor)?; + } + for witness in &record.transition_witnesses { + add_factor(&mut values, &witness.exact_factor)?; + } + } + for record in &catalog.symmetry_proofs { + add_factor(&mut values, &record.exact_phase)?; + } + for record in &catalog.runtime_helicity_contracts { + for variant in &record.variants { + for factor in &variant.embedding_factors { + add_factor(&mut values, factor)?; + } + } + } + Ok(values) +} + +impl PrimitiveCatalogs { + fn build(catalog: &SemanticCatalogJson, ids: &SectionIds) -> RusticolResult { + let strings = Catalog::from_set(collect_strings(catalog)?, "string catalog")?; + let digests = Catalog::from_set(collect_digests(catalog)?, "digest catalog")?; + let factors = Catalog::from_set(collect_factors(catalog)?, "factor catalog")?; + + let mut flavour_values = BTreeSet::new(); + let mut quantum_values = BTreeSet::new(); + for record in &catalog.sources { + flavour_values.insert(record.flavour_flow.clone()); + quantum_values.insert(qflow(&record.quantum_number_flow)); + } + for record in &catalog.quantum_flows { + flavour_values.extend(record.input_flavour_flows.iter().cloned()); + flavour_values.insert(record.result_flavour_flow.clone()); + quantum_values.extend( + record + .input_quantum_number_flows + .iter() + .map(|flow| qflow(flow)), + ); + quantum_values.insert(qflow(&record.result_quantum_number_flow)); + } + let flavour_flows = Catalog::from_set(flavour_values, "flavour-flow catalog")?; + let quantum_number_flows = + Catalog::from_set(quantum_values, "quantum-number-flow catalog")?; + + let mut coupling_values = BTreeSet::from([Vec::new()]); + coupling_values.extend( + catalog + .quantum_flows + .iter() + .map(|record| record.coupling_orders.clone()), + ); + coupling_values.extend( + catalog + .transitions + .iter() + .map(|record| record.coupling_orders.clone()), + ); + coupling_values.extend( + catalog + .closures + .iter() + .map(|record| record.coupling_orders.clone()), + ); + let coupling_orders = Catalog::from_set(coupling_values, "coupling-order catalog")?; + + let mut u32_values = BTreeSet::from([Vec::new()]); + let mut i32_values = BTreeSet::from([Vec::new()]); + for record in &catalog.parameters { + u32_values.insert(mapped_ids( + &record.dependency_parameter_ids, + &ids.parameters, + "parameter dependency", + )?); + } + for record in &catalog.current_states { + u32_values.insert( + record + .tensor_ordering + .iter() + .map(|value| string_id(&strings, value)) + .collect::>>()?, + ); + } + for record in &catalog.sources { + u32_values.insert( + record + .lc_color_seed + .provenance + .iter() + .flat_map(|pair| pair.iter()) + .map(|value| string_id(&strings, value)) + .collect::>>()?, + ); + } + for record in &catalog.quantum_flows { + u32_values.insert(mapped_ids( + &record.input_state_template_ids, + &ids.current_states, + "quantum-flow input state", + )?); + u32_values.insert( + record + .input_flavour_flows + .iter() + .map(|value| flavour_flows.id(value, "input flavour flow")) + .collect::>>()?, + ); + u32_values.insert( + record + .input_quantum_number_flows + .iter() + .map(|value| { + quantum_number_flows.id(&qflow(value), "input quantum-number flow") + }) + .collect::>>()?, + ); + i32_values.insert(record.input_spin_states.clone()); + } + for record in &catalog.transitions { + u32_values.insert(mapped_ids( + &record.input_state_template_ids, + &ids.current_states, + "transition input state", + )?); + u32_values.insert(record.canonical_input_order.clone()); + u32_values.insert( + record + .momentum_convention + .iter() + .map(|value| string_id(&strings, value)) + .collect::>>()?, + ); + u32_values.insert(mapped_ids( + &record.coupling_parameter_ids, + &ids.parameters, + "transition coupling parameter", + )?); + u32_values.insert(mapped_ids( + &record.contact_orbit_step_template_ids, + &ids.contact_orbit_steps, + "transition contact-orbit step", + )?); + u32_values.insert( + record + .contact_orbit_step_semantic_digests + .iter() + .map(|value| digest_id(&digests, value)) + .collect::>>()?, + ); + } + for record in &catalog.contact_orbit_certificates { + u32_values.insert( + record + .particles + .iter() + .map(|value| string_id(&strings, value)) + .collect::>>()?, + ); + u32_values.insert(record.physical_leg_equivalence_classes.to_vec()); + } + for record in &catalog.contact_orbit_steps { + u32_values.insert(record.left_covered_legs.clone()); + u32_values.insert(record.right_covered_legs.clone()); + i32_values.insert(record.source_particle_legs.to_vec()); + } + for record in &catalog.closures { + u32_values.insert(mapped_ids( + &record.input_state_template_ids, + &ids.current_states, + "closure input state", + )?); + u32_values.insert(record.canonical_input_order.clone()); + u32_values.insert(mapped_ids( + &record.coupling_parameter_ids, + &ids.parameters, + "closure coupling parameter", + )?); + u32_values.insert(mapped_ids( + &record.eligible_quantum_flow_template_ids, + &ids.quantum_flows, + "closure quantum flow", + )?); + u32_values.insert( + record + .component_coefficients + .iter() + .map(|value| factor_id(&factors, value)) + .collect::>>()?, + ); + } + for record in &catalog.color_contractions { + i32_values.insert(record.input_representations.clone()); + for witness in &record.transition_witnesses { + let (pairings, bindings) = witness.canonical_ports()?; + u32_values.insert( + pairings + .iter() + .flat_map(|pairing| pairing.iter()) + .flat_map(|port| port.iter().copied()) + .collect(), + ); + u32_values.insert( + bindings + .iter() + .flat_map(|port| port.iter().copied()) + .collect(), + ); + u32_values.insert( + witness + .provenance + .iter() + .flat_map(|pair| pair.iter()) + .map(|value| string_id(&strings, value)) + .collect::>>()?, + ); + } + } + for record in &catalog.symmetry_proofs { + u32_values.insert( + record + .subject_template_ids + .iter() + .map(|value| string_id(&strings, value)) + .collect::>>()?, + ); + u32_values.insert(record.input_permutation.clone()); + u32_values.insert( + record + .expression_digests + .iter() + .map(|value| digest_id(&digests, value)) + .collect::>>()?, + ); + } + for record in &catalog.evaluator_bindings { + u32_values.insert(mapped_ids( + &record.input_state_template_ids, + &ids.current_states, + "evaluator input state", + )?); + u32_values.insert( + record + .input_layout + .iter() + .map(|value| string_id(&strings, value)) + .collect::>>()?, + ); + u32_values.insert( + record + .output_layout + .iter() + .map(|value| string_id(&strings, value)) + .collect::>>()?, + ); + u32_values.insert( + record + .exact_expression_digests + .iter() + .map(|value| digest_id(&digests, value)) + .collect::>>()?, + ); + u32_values.insert( + record + .semantic_template_ids + .iter() + .map(|value| string_id(&strings, value)) + .collect::>>()?, + ); + } + + Ok(Self { + strings, + digests, + factors, + flavour_flows, + quantum_number_flows, + coupling_orders, + u32_sequences: Catalog::from_set(u32_values, "u32 sequence catalog")?, + i32_sequences: Catalog::from_set(i32_values, "i32 sequence catalog")?, + }) + } +} + +fn string_id(catalog: &Catalog, value: &str) -> RusticolResult { + catalog.ids.get(value).copied().ok_or_else(|| { + invalid(format!( + "string {value:?} is absent from the canonical catalog" + )) + }) +} + +fn digest_id(catalog: &Catalog<[u8; 32]>, value: &str) -> RusticolResult { + catalog.id(&parse_digest(value, "digest catalog lookup")?, "digest") +} + +fn factor_id(catalog: &Catalog, value: &ExactFactorJson) -> RusticolResult { + catalog.id(&value.key()?, "exact factor") +} + +fn optional_factor_id( + catalog: &Catalog, + value: &RequiredOption, +) -> RusticolResult { + match &value.0 { + Some(value) => factor_id(catalog, value), + None => Ok(MISSING_U32), + } +} + +fn optional_digest_id( + catalog: &Catalog<[u8; 32]>, + value: &RequiredOption, +) -> RusticolResult { + match &value.0 { + Some(value) => digest_id(catalog, value), + None => Ok(MISSING_U32), + } +} + +fn pack_sequences( + catalog: &Catalog>, + context: &str, +) -> RusticolResult<(Vec, Vec)> { + let mut ranges = Vec::with_capacity(catalog.values.len()); + let mut flat = Vec::new(); + for (index, values) in catalog.values.iter().enumerate() { + ranges.push(IndexedRangeRow { + id: checked_u32(index, context)?, + range: CheckedTableRange::new( + checked_u64(flat.len(), context)?, + checked_u64(values.len(), context)?, + ), + }); + flat.extend(values.iter().cloned()); + } + Ok((ranges, flat)) +} + +fn project_catalog(catalog: &SemanticCatalogJson) -> RusticolResult { + let ids = SectionIds::build(catalog)?; + let primitive = PrimitiveCatalogs::build(catalog, &ids)?; + + let mut string_ranges = Vec::with_capacity(primitive.strings.values.len()); + let mut string_bytes = Vec::new(); + for value in &primitive.strings.values { + let bytes = value.as_bytes(); + string_ranges.push(CheckedTableRange::new( + checked_u64(string_bytes.len(), "string byte offset")?, + checked_u64(bytes.len(), "string byte length")?, + )); + string_bytes.extend_from_slice(bytes); + } + + let digest_catalog = primitive + .digests + .values + .iter() + .copied() + .enumerate() + .map(|(index, value)| { + Ok(DigestCatalogRow { + id: checked_u32(index, "digest catalog row")?, + value, + }) + }) + .collect::>>()?; + let exact_factors = primitive + .factors + .values + .iter() + .enumerate() + .map(|(index, factor)| { + Ok(ExactFactorRow { + id: checked_u32(index, "exact-factor row")?, + real_numerator_string_id: string_id(&primitive.strings, &factor.strings[0])?, + real_denominator_string_id: string_id(&primitive.strings, &factor.strings[1])?, + imag_numerator_string_id: string_id(&primitive.strings, &factor.strings[2])?, + imag_denominator_string_id: string_id(&primitive.strings, &factor.strings[3])?, + }) + }) + .collect::>>()?; + + let (flavour_flow_ranges, flavour_flow_values) = + pack_sequences(&primitive.flavour_flows, "flavour-flow")?; + let (i32_sequence_ranges, i32_sequence_values) = + pack_sequences(&primitive.i32_sequences, "i32 sequence")?; + let (u32_sequence_ranges, u32_sequence_values) = + pack_sequences(&primitive.u32_sequences, "u32 sequence")?; + let (coupling_order_ranges, _) = pack_sequences(&primitive.coupling_orders, "coupling-order")?; + let coupling_order_terms = primitive + .coupling_orders + .values + .iter() + .enumerate() + .flat_map(|(set_index, values)| { + values + .iter() + .map(move |(name, power)| (set_index, name, power)) + }) + .map(|(set_index, name, power)| { + Ok(CouplingOrderTermRow { + set_id: checked_u32(set_index, "coupling-order set")?, + name_string_id: string_id(&primitive.strings, name)?, + power: *power, + }) + }) + .collect::>>()?; + + let (quantum_number_flow_ranges, _) = + pack_sequences(&primitive.quantum_number_flows, "quantum-number flow")?; + let quantum_number_flow_terms = primitive + .quantum_number_flows + .values + .iter() + .enumerate() + .flat_map(|(flow_index, values)| { + values + .iter() + .map(move |(name, expression)| (flow_index, name, expression)) + }) + .map(|(flow_index, name, expression)| { + Ok(QuantumNumberFlowTermRow { + flow_id: checked_u32(flow_index, "quantum-number flow")?, + name_string_id: string_id(&primitive.strings, name)?, + expression_string_id: string_id(&primitive.strings, expression)?, + }) + }) + .collect::>>()?; + + let catalog_header = vec![CatalogHeaderRow { + schema_version: RECURRENCE_TEMPLATE_INPUT_SCHEMA_VERSION, + abi_string_id: string_id(&primitive.strings, &catalog.header.abi)?, + canonicalization_abi_string_id: string_id( + &primitive.strings, + &catalog.header.canonicalization_abi, + )?, + exact_scalar_abi_string_id: string_id( + &primitive.strings, + &catalog.header.exact_scalar_abi, + )?, + compiled_model_digest_id: digest_id( + &primitive.digests, + &catalog.header.compiled_model_digest, + )?, + prepared_kernel_pack_digest_id: digest_id( + &primitive.digests, + &catalog.header.prepared_kernel_pack_digest, + )?, + catalog_digest_id: digest_id(&primitive.digests, &catalog.header.catalog_digest)?, + parameter_count: checked_u32(catalog.parameters.len(), "parameter count")?, + current_state_count: checked_u32(catalog.current_states.len(), "current-state count")?, + source_count: checked_u32(catalog.sources.len(), "source count")?, + quantum_flow_count: checked_u32(catalog.quantum_flows.len(), "quantum-flow count")?, + contact_orbit_certificate_count: checked_u32( + catalog.contact_orbit_certificates.len(), + "contact-orbit certificate count", + )?, + contact_orbit_step_count: checked_u32( + catalog.contact_orbit_steps.len(), + "contact-orbit step count", + )?, + transition_count: checked_u32(catalog.transitions.len(), "transition count")?, + propagator_count: checked_u32(catalog.propagators.len(), "propagator count")?, + closure_count: checked_u32(catalog.closures.len(), "closure count")?, + color_contraction_count: checked_u32( + catalog.color_contractions.len(), + "color-contraction count", + )?, + symmetry_proof_count: checked_u32(catalog.symmetry_proofs.len(), "symmetry-proof count")?, + runtime_helicity_contract_count: checked_u32( + catalog.runtime_helicity_contracts.len(), + "runtime-helicity contract count", + )?, + evaluator_binding_count: checked_u32( + catalog.evaluator_bindings.len(), + "evaluator-binding count", + )?, + }]; + + let parameters = catalog + .parameters + .iter() + .enumerate() + .map(|(index, record)| { + Ok(ParameterRow { + id: checked_u32(index, "parameter row")?, + template_string_id: string_id(&primitive.strings, &record.template_id)?, + name_string_id: string_id(&primitive.strings, &record.name)?, + kind: enum_id( + &record.parameter_kind, + &[("external", 0), ("derived", 1), ("constant", 2)], + "parameter kind", + )?, + value_type: enum_id( + &record.value_type, + &[("real", 0), ("complex", 1)], + "parameter value type", + )?, + mutable: u8::from(record.mutable), + default_factor_id: optional_factor_id(&primitive.factors, &record.default_value)?, + exact_expression_digest_id: optional_digest_id( + &primitive.digests, + &record.exact_expression_digest, + )?, + dependency_sequence_id: primitive.u32_sequences.id( + &mapped_ids( + &record.dependency_parameter_ids, + &ids.parameters, + "parameter dependency", + )?, + "parameter dependency sequence", + )?, + prepared_parameter_id: record.prepared_parameter_id.0.unwrap_or(MISSING_U32), + semantic_digest_id: digest_id(&primitive.digests, &record.semantic_digest)?, + }) + }) + .collect::>>()?; + + let current_states = catalog + .current_states + .iter() + .enumerate() + .map(|(index, record)| { + let tensor_ordering = record + .tensor_ordering + .iter() + .map(|value| string_id(&primitive.strings, value)) + .collect::>>()?; + Ok(CurrentStateRow { + id: checked_u32(index, "current-state row")?, + template_string_id: string_id(&primitive.strings, &record.template_id)?, + particle_id: record.particle_id, + anti_particle_id: record.anti_particle_id, + species_string_id: string_id(&primitive.strings, &record.species_id)?, + orientation: enum_id( + &record.orientation, + &[("particle", 0), ("antiparticle", 1), ("self-conjugate", 2)], + "current orientation", + )?, + statistics: enum_id( + &record.statistics, + &[("boson", 0), ("fermion", 1)], + "particle statistics", + )?, + color_representation: record.color_representation, + basis_string_id: string_id(&primitive.strings, &record.basis)?, + tensor_ordering_sequence_id: primitive + .u32_sequences + .id(&tensor_ordering, "tensor-ordering sequence")?, + dimension: record.dimension, + chirality: record.chirality, + lc_color_shape_string_id: string_id( + &primitive.strings, + &record.lc_color_shape_kind, + )?, + auxiliary_kind_string_id: optional_string_id( + &primitive.strings, + &record.auxiliary_kind, + )?, + mass_parameter_id: optional_id( + record.mass_parameter_id.0.as_ref(), + &ids.parameters, + "current mass parameter", + )?, + width_parameter_id: optional_id( + record.width_parameter_id.0.as_ref(), + &ids.parameters, + "current width parameter", + )?, + semantic_digest_id: digest_id(&primitive.digests, &record.semantic_digest)?, + }) + }) + .collect::>>()?; + + let sources = catalog + .sources + .iter() + .enumerate() + .map(|(index, record)| { + let provenance = record + .lc_color_seed + .provenance + .iter() + .flat_map(|pair| pair.iter()) + .map(|value| string_id(&primitive.strings, value)) + .collect::>>()?; + Ok(SourceRow { + id: checked_u32(index, "source row")?, + template_string_id: string_id(&primitive.strings, &record.template_id)?, + state_template_id: required_id( + &ids.current_states, + &record.state_template_id, + "source state", + )?, + crossing_string_id: string_id(&primitive.strings, &record.crossing)?, + wavefunction_family_string_id: string_id( + &primitive.strings, + &record.wavefunction_family, + )?, + helicity: record.helicity, + spin_state: record.spin_state, + flavour_flow_id: primitive + .flavour_flows + .id(&record.flavour_flow, "source flavour flow")?, + quantum_number_flow_id: primitive.quantum_number_flows.id( + &qflow(&record.quantum_number_flow), + "source quantum-number flow", + )?, + lc_color_seed_operation: enum_id( + &record.lc_color_seed.operation, + &[("empty", 0), ("singleton", 1)], + "LC source-seed operation", + )?, + lc_color_seed_shape_string_id: string_id( + &primitive.strings, + &record.lc_color_seed.output_shape_kind, + )?, + lc_color_seed_component_kind: optional_enum_id( + &record.lc_color_seed.component_kind, + &[("open-string", 0), ("adjoint-segment", 1), ("trace", 2)], + "LC source-seed component kind", + 255, + )?, + lc_color_seed_component_role: enum_id( + &record.lc_color_seed.component_role, + &[("active", 0), ("passive", 1), ("none", 2)], + "LC source-seed component role", + )?, + lc_color_seed_proof_digest_id: digest_id( + &primitive.digests, + &record.lc_color_seed.proof_digest, + )?, + lc_color_seed_provenance_sequence_id: primitive + .u32_sequences + .id(&provenance, "LC source provenance sequence")?, + wavefunction_expression_digest_id: digest_id( + &primitive.digests, + &record.wavefunction_expression_digest, + )?, + evaluator_binding_id: required_id( + &ids.evaluator_bindings, + &record.evaluator_resolver_key, + "source evaluator", + )?, + mass_parameter_id: optional_id( + record.mass_parameter_id.0.as_ref(), + &ids.parameters, + "source mass parameter", + )?, + width_parameter_id: optional_id( + record.width_parameter_id.0.as_ref(), + &ids.parameters, + "source width parameter", + )?, + semantic_digest_id: digest_id(&primitive.digests, &record.semantic_digest)?, + }) + }) + .collect::>>()?; + + let quantum_flows = catalog + .quantum_flows + .iter() + .enumerate() + .map(|(index, record)| { + let input_flavours = record + .input_flavour_flows + .iter() + .map(|value| primitive.flavour_flows.id(value, "input flavour flow")) + .collect::>>()?; + let input_quantum = record + .input_quantum_number_flows + .iter() + .map(|value| { + primitive + .quantum_number_flows + .id(&qflow(value), "input quantum-number flow") + }) + .collect::>>()?; + Ok(QuantumFlowRow { + id: checked_u32(index, "quantum-flow row")?, + template_string_id: string_id(&primitive.strings, &record.template_id)?, + input_state_sequence_id: sequence_id( + &primitive.u32_sequences, + mapped_ids( + &record.input_state_template_ids, + &ids.current_states, + "quantum-flow input state", + )?, + "quantum-flow input-state sequence", + )?, + input_spin_sequence_id: sequence_id( + &primitive.i32_sequences, + record.input_spin_states.clone(), + "quantum-flow spin sequence", + )?, + input_flavour_sequence_id: sequence_id( + &primitive.u32_sequences, + input_flavours, + "quantum-flow flavour sequence", + )?, + input_quantum_sequence_id: sequence_id( + &primitive.u32_sequences, + input_quantum, + "quantum-flow quantum-number sequence", + )?, + flavour_flow_operation_string_id: string_id( + &primitive.strings, + &record.flavour_flow_operation, + )?, + quantum_number_flow_operation_string_id: string_id( + &primitive.strings, + &record.quantum_number_flow_operation, + )?, + coupling_order_set_id: primitive + .coupling_orders + .id(&record.coupling_orders, "quantum-flow coupling orders")?, + result_state_template_id: required_id( + &ids.current_states, + &record.result_state_template_id, + "quantum-flow result state", + )?, + result_spin_state: record.result_spin_state, + result_flavour_flow_id: primitive + .flavour_flows + .id(&record.result_flavour_flow, "result flavour flow")?, + result_quantum_number_flow_id: primitive.quantum_number_flows.id( + &qflow(&record.result_quantum_number_flow), + "result quantum-number flow", + )?, + exact_coupling_factor_id: factor_id(&primitive.factors, &record.exact_coupling)?, + predicate_digest_id: digest_id(&primitive.digests, &record.predicate_digest)?, + semantic_digest_id: digest_id(&primitive.digests, &record.semantic_digest)?, + }) + }) + .collect::>>()?; + + let contact_orbit_certificates = catalog + .contact_orbit_certificates + .iter() + .enumerate() + .map(|(index, record)| { + let particles = record + .particles + .iter() + .map(|value| string_id(&primitive.strings, value)) + .collect::>>()?; + Ok(ContactOrbitCertificateRow { + id: checked_u32(index, "contact-orbit certificate row")?, + template_string_id: string_id(&primitive.strings, &record.template_id)?, + algorithm_string_id: string_id(&primitive.strings, &record.algorithm)?, + algorithm_version: record.algorithm_version, + term_id: record.term_id, + vertex_string_id: string_id(&primitive.strings, &record.vertex)?, + particle_string_sequence_id: sequence_id( + &primitive.u32_sequences, + particles, + "contact-orbit particle sequence", + )?, + evaluator_class_string_id: string_id(&primitive.strings, &record.evaluator_class)?, + physical_leg_equivalence_sequence_id: sequence_id( + &primitive.u32_sequences, + record.physical_leg_equivalence_classes.to_vec(), + "contact-orbit equivalence sequence", + )?, + reconstruction_factor_id: factor_id( + &primitive.factors, + &record.reconstruction_factor, + )?, + semantic_digest_id: digest_id(&primitive.digests, &record.semantic_digest)?, + }) + }) + .collect::>>()?; + + let contact_orbit_steps = catalog + .contact_orbit_steps + .iter() + .enumerate() + .map(|(index, record)| { + Ok(ContactOrbitStepRow { + id: checked_u32(index, "contact-orbit step row")?, + template_string_id: string_id(&primitive.strings, &record.template_id)?, + certificate_id: required_id( + &ids.contact_orbit_certificates, + &record.certificate_template_id, + "contact-orbit certificate", + )?, + stage: enum_id( + &record.stage, + &[("partial", 0), ("final", 1)], + "contact-orbit stage", + )?, + result_leg: record.result_leg, + left_covered_leg_sequence_id: sequence_id( + &primitive.u32_sequences, + record.left_covered_legs.clone(), + "contact-orbit left legs", + )?, + right_covered_leg_sequence_id: sequence_id( + &primitive.u32_sequences, + record.right_covered_legs.clone(), + "contact-orbit right legs", + )?, + source_particle_leg_sequence_id: sequence_id( + &primitive.i32_sequences, + record.source_particle_legs.to_vec(), + "contact-orbit source legs", + )?, + reconstruction_factor_id: factor_id( + &primitive.factors, + &record.reconstruction_factor, + )?, + semantic_digest_id: digest_id(&primitive.digests, &record.semantic_digest)?, + }) + }) + .collect::>>()?; + + let transitions = catalog + .transitions + .iter() + .enumerate() + .map(|(index, record)| { + let momentum = record + .momentum_convention + .iter() + .map(|value| string_id(&primitive.strings, value)) + .collect::>>()?; + let step_digests = record + .contact_orbit_step_semantic_digests + .iter() + .map(|value| digest_id(&primitive.digests, value)) + .collect::>>()?; + Ok(TransitionRow { + id: checked_u32(index, "transition row")?, + template_string_id: string_id(&primitive.strings, &record.template_id)?, + input_state_sequence_id: sequence_id( + &primitive.u32_sequences, + mapped_ids( + &record.input_state_template_ids, + &ids.current_states, + "transition input state", + )?, + "transition input-state sequence", + )?, + result_state_template_id: required_id( + &ids.current_states, + &record.result_state_template_id, + "transition result state", + )?, + quantum_flow_template_id: required_id( + &ids.quantum_flows, + &record.quantum_flow_template_id, + "transition quantum flow", + )?, + evaluator_binding_id: required_id( + &ids.evaluator_bindings, + &record.evaluator_resolver_key, + "transition evaluator", + )?, + canonical_input_order_sequence_id: sequence_id( + &primitive.u32_sequences, + record.canonical_input_order.clone(), + "transition canonical input order", + )?, + momentum_convention_sequence_id: sequence_id( + &primitive.u32_sequences, + momentum, + "transition momentum convention", + )?, + coupling_parameter_sequence_id: sequence_id( + &primitive.u32_sequences, + mapped_ids( + &record.coupling_parameter_ids, + &ids.parameters, + "transition coupling parameter", + )?, + "transition coupling parameters", + )?, + coupling_order_set_id: primitive + .coupling_orders + .id(&record.coupling_orders, "transition coupling orders")?, + color_contraction_template_id: required_id( + &ids.color_contractions, + &record.color_contraction_template_id, + "transition color contraction", + )?, + binding_coupling_factor_id: factor_id( + &primitive.factors, + &record.binding_coupling, + )?, + exact_factor_id: factor_id(&primitive.factors, &record.exact_factor)?, + output_factor_source: enum_id( + &record.output_factor_source, + &[("none", 0), ("coupling-real", 1), ("coupling-imag", 2)], + "transition output-factor source", + )?, + equivalence_class_string_id: string_id( + &primitive.strings, + &record.equivalence_class, + )?, + input_exchange_factor_id: optional_factor_id( + &primitive.factors, + &record.input_exchange_factor, + )?, + output_projection_string_id: string_id( + &primitive.strings, + &record.output_projection, + )?, + contact_orbit_step_sequence_id: sequence_id( + &primitive.u32_sequences, + mapped_ids( + &record.contact_orbit_step_template_ids, + &ids.contact_orbit_steps, + "transition contact-orbit step", + )?, + "transition contact-orbit steps", + )?, + contact_orbit_step_semantic_digest_sequence_id: sequence_id( + &primitive.u32_sequences, + step_digests, + "transition contact-orbit digests", + )?, + semantic_digest_id: digest_id(&primitive.digests, &record.semantic_digest)?, + }) + }) + .collect::>>()?; + + let propagators = catalog + .propagators + .iter() + .enumerate() + .map(|(index, record)| { + Ok(PropagatorRow { + id: checked_u32(index, "propagator row")?, + template_string_id: string_id(&primitive.strings, &record.template_id)?, + state_template_id: required_id( + &ids.current_states, + &record.state_template_id, + "propagator state", + )?, + applies_propagator: u8::from(record.applies_propagator), + evaluator_binding_id: optional_id( + record.evaluator_resolver_key.0.as_ref(), + &ids.evaluator_bindings, + "propagator evaluator", + )?, + numerator_expression_digest_id: optional_digest_id( + &primitive.digests, + &record.numerator_expression_digest, + )?, + denominator_expression_digest_id: optional_digest_id( + &primitive.digests, + &record.denominator_expression_digest, + )?, + mass_parameter_id: optional_id( + record.mass_parameter_id.0.as_ref(), + &ids.parameters, + "propagator mass parameter", + )?, + width_parameter_id: optional_id( + record.width_parameter_id.0.as_ref(), + &ids.parameters, + "propagator width parameter", + )?, + gauge_string_id: optional_string_id(&primitive.strings, &record.gauge)?, + linearity_proof_template_id: optional_id( + record.linearity_proof_template_id.0.as_ref(), + &ids.symmetry_proofs, + "propagator linearity proof", + )?, + semantic_digest_id: digest_id(&primitive.digests, &record.semantic_digest)?, + }) + }) + .collect::>>()?; + + let closures = catalog + .closures + .iter() + .enumerate() + .map(|(index, record)| { + let coefficients = record + .component_coefficients + .iter() + .map(|value| factor_id(&primitive.factors, value)) + .collect::>>()?; + Ok(ClosureRow { + id: checked_u32(index, "closure row")?, + template_string_id: string_id(&primitive.strings, &record.template_id)?, + input_state_sequence_id: sequence_id( + &primitive.u32_sequences, + mapped_ids( + &record.input_state_template_ids, + &ids.current_states, + "closure input state", + )?, + "closure input states", + )?, + result_state_template_id: optional_id( + record.result_state_template_id.0.as_ref(), + &ids.current_states, + "closure result state", + )?, + evaluator_binding_id: required_id( + &ids.evaluator_bindings, + &record.evaluator_resolver_key, + "closure evaluator", + )?, + canonical_input_order_sequence_id: sequence_id( + &primitive.u32_sequences, + record.canonical_input_order.clone(), + "closure canonical input order", + )?, + coupling_parameter_sequence_id: sequence_id( + &primitive.u32_sequences, + mapped_ids( + &record.coupling_parameter_ids, + &ids.parameters, + "closure coupling parameter", + )?, + "closure coupling parameters", + )?, + coupling_order_set_id: primitive + .coupling_orders + .id(&record.coupling_orders, "closure coupling orders")?, + eligible_quantum_flow_sequence_id: sequence_id( + &primitive.u32_sequences, + mapped_ids( + &record.eligible_quantum_flow_template_ids, + &ids.quantum_flows, + "closure quantum flow", + )?, + "closure eligible quantum flows", + )?, + color_contraction_template_id: required_id( + &ids.color_contractions, + &record.color_contraction_template_id, + "closure color contraction", + )?, + binding_coupling_factor_id: factor_id( + &primitive.factors, + &record.binding_coupling, + )?, + exact_factor_id: factor_id(&primitive.factors, &record.exact_factor)?, + output_factor_source: enum_id( + &record.output_factor_source, + &[("none", 0), ("coupling-real", 1), ("coupling-imag", 2)], + "closure output-factor source", + )?, + equivalence_class_string_id: string_id( + &primitive.strings, + &record.equivalence_class, + )?, + input_exchange_factor_id: optional_factor_id( + &primitive.factors, + &record.input_exchange_factor, + )?, + projection_string_id: string_id(&primitive.strings, &record.projection)?, + component_coefficient_sequence_id: sequence_id( + &primitive.u32_sequences, + coefficients, + "closure component coefficients", + )?, + chirality_relation_string_id: string_id( + &primitive.strings, + &record.chirality_relation, + )?, + metric_signature_string_id: optional_string_id( + &primitive.strings, + &record.metric_signature, + )?, + semantic_digest_id: digest_id(&primitive.digests, &record.semantic_digest)?, + }) + }) + .collect::>>()?; + + let mut color_contractions = Vec::with_capacity(catalog.color_contractions.len()); + let mut lc_color_transition_witnesses = Vec::new(); + let mut color_nc_terms = Vec::new(); + for (color_index, record) in catalog.color_contractions.iter().enumerate() { + let color_id = checked_u32(color_index, "color-contraction row")?; + let witness_start = + checked_u64(lc_color_transition_witnesses.len(), "color witness offset")?; + let nc_term_start = checked_u64(color_nc_terms.len(), "color Nc offset")?; + for (ordinal, witness) in record.transition_witnesses.iter().enumerate() { + let (pairings, bindings) = witness.canonical_ports()?; + let flattened_pairings = pairings + .iter() + .flat_map(|pairing| pairing.iter()) + .flat_map(|port| port.iter().copied()) + .collect::>(); + let flattened_bindings = bindings + .iter() + .flat_map(|port| port.iter().copied()) + .collect::>(); + let provenance = witness + .provenance + .iter() + .flat_map(|pair| pair.iter()) + .map(|value| string_id(&primitive.strings, value)) + .collect::>>()?; + let input_permutation = match witness.input_permutation { + [0, 1] => 0, + [1, 0] => 1, + value => { + return Err(invalid(format!( + "unsupported LC witness input permutation {value:?}" + ))); + } + }; + lc_color_transition_witnesses.push(LCColorTransitionWitnessRow { + color_contraction_id: color_id, + ordinal: checked_u32(ordinal, "color witness ordinal")?, + left_shape_string_id: string_id(&primitive.strings, &witness.input_shape_kinds[0])?, + right_shape_string_id: string_id( + &primitive.strings, + &witness.input_shape_kinds[1], + )?, + input_permutation, + reverse_parent_mask: witness.reverse_parent_mask, + component_operation: enum_id( + &witness.component_operation, + &[ + ("concatenate-join", 0), + ("concatenate-keep", 1), + ("inherit-left", 2), + ("inherit-right", 3), + ("empty", 4), + ("close", 5), + ], + "LC witness component operation", + )?, + result_component_kind: optional_enum_id( + &witness.result_component_kind, + &[("open-string", 0), ("adjoint-segment", 1), ("trace", 2)], + "LC witness component kind", + 255, + )?, + result_component_role: enum_id( + &witness.result_component_role, + &[("active", 0), ("passive", 1), ("none", 2)], + "LC witness component role", + )?, + result_shape_string_id: optional_string_id( + &primitive.strings, + &witness.result_shape_kind, + )?, + exact_factor_id: factor_id(&primitive.factors, &witness.exact_factor)?, + proof_digest_id: digest_id(&primitive.digests, &witness.proof_digest)?, + input_port_pairing_sequence_id: sequence_id( + &primitive.u32_sequences, + flattened_pairings, + "LC witness port pairings", + )?, + result_port_binding_sequence_id: sequence_id( + &primitive.u32_sequences, + flattened_bindings, + "LC witness result bindings", + )?, + provenance_sequence_id: sequence_id( + &primitive.u32_sequences, + provenance, + "LC witness provenance", + )?, + }); + } + for (exponent, factor) in &record.nc_polynomial { + color_nc_terms.push(ColorNcTermRow { + color_contraction_id: color_id, + exponent: *exponent, + factor_id: factor_id(&primitive.factors, factor)?, + }); + } + color_contractions.push(ColorContractionRow { + id: color_id, + template_string_id: string_id(&primitive.strings, &record.template_id)?, + rule_kind_string_id: string_id(&primitive.strings, &record.rule_kind)?, + input_representation_sequence_id: sequence_id( + &primitive.i32_sequences, + record.input_representations.clone(), + "color input representations", + )?, + has_output_representation: u8::from(record.output_representation.0.is_some()), + output_representation: record.output_representation.0.unwrap_or(0), + ordered_open_string_arity: record.ordered_open_string_arity, + exact_coefficient_factor_id: factor_id(&primitive.factors, &record.exact_coefficient)?, + witness_start, + witness_count: checked_u64(record.transition_witnesses.len(), "color witness count")?, + nc_term_start, + nc_term_count: checked_u64(record.nc_polynomial.len(), "color Nc count")?, + expression_digest_id: digest_id(&primitive.digests, &record.expression_digest)?, + semantic_digest_id: digest_id(&primitive.digests, &record.semantic_digest)?, + }); + } + + let symmetry_proofs = catalog + .symmetry_proofs + .iter() + .enumerate() + .map(|(index, record)| { + let subjects = record + .subject_template_ids + .iter() + .map(|value| string_id(&primitive.strings, value)) + .collect::>>()?; + let expressions = record + .expression_digests + .iter() + .map(|value| digest_id(&primitive.digests, value)) + .collect::>>()?; + Ok(SymmetryProofRow { + id: checked_u32(index, "symmetry-proof row")?, + template_string_id: string_id(&primitive.strings, &record.template_id)?, + proof_algorithm_string_id: string_id(&primitive.strings, &record.proof_algorithm)?, + subject_template_sequence_id: sequence_id( + &primitive.u32_sequences, + subjects, + "symmetry-proof subjects", + )?, + input_permutation_sequence_id: sequence_id( + &primitive.u32_sequences, + record.input_permutation.clone(), + "symmetry-proof input permutation", + )?, + exact_phase_factor_id: factor_id(&primitive.factors, &record.exact_phase)?, + expression_digest_sequence_id: sequence_id( + &primitive.u32_sequences, + expressions, + "symmetry-proof expressions", + )?, + witness_digest_id: digest_id(&primitive.digests, &record.witness_digest)?, + semantic_digest_id: digest_id(&primitive.digests, &record.semantic_digest)?, + }) + }) + .collect::>>()?; + + let mut runtime_helicity_contracts = Vec::new(); + let mut runtime_helicity_variants = Vec::new(); + let mut runtime_helicity_embeddings = Vec::new(); + let mut runtime_helicity_projections = Vec::new(); + for (contract_index, record) in catalog.runtime_helicity_contracts.iter().enumerate() { + let contract_id = checked_u32(contract_index, "runtime-helicity contract")?; + let variant_start = checked_u64( + runtime_helicity_variants.len(), + "runtime-helicity variant offset", + )?; + for variant in &record.variants { + let variant_id = + checked_u32(runtime_helicity_variants.len(), "runtime-helicity variant")?; + let embedding_start = checked_u64( + runtime_helicity_embeddings.len(), + "runtime-helicity embedding offset", + )?; + for (full_component, (source_component, factor)) in variant + .embedding_source_components + .iter() + .zip(&variant.embedding_factors) + .enumerate() + { + runtime_helicity_embeddings.push(RuntimeHelicityEmbeddingRow { + variant_id, + full_component: checked_u32(full_component, "full helicity component")?, + source_component: source_component.unwrap_or(MISSING_U32), + factor_id: factor_id(&primitive.factors, factor)?, + }); + } + let projection_start = checked_u64( + runtime_helicity_projections.len(), + "runtime-helicity projection offset", + )?; + for (source_component, full_component) in variant + .projection_full_components + .iter() + .copied() + .enumerate() + { + runtime_helicity_projections.push(RuntimeHelicityProjectionRow { + variant_id, + source_component: checked_u32(source_component, "source helicity component")?, + full_component, + }); + } + runtime_helicity_variants.push(RuntimeHelicityVariantRow { + id: variant_id, + contract_id, + source_template_id: required_id( + &ids.sources, + &variant.source_template_id, + "runtime-helicity source", + )?, + source_state_template_id: required_id( + &ids.current_states, + &variant.source_state_template_id, + "runtime-helicity source state", + )?, + embedding_range: CheckedTableRange::new( + embedding_start, + checked_u64( + variant.embedding_source_components.len(), + "runtime-helicity embedding count", + )?, + ), + projection_range: CheckedTableRange::new( + projection_start, + checked_u64( + variant.projection_full_components.len(), + "runtime-helicity projection count", + )?, + ), + proof_digest_id: digest_id(&primitive.digests, &variant.proof_digest)?, + }); + } + runtime_helicity_contracts.push(RuntimeHelicityContractRow { + id: contract_id, + template_string_id: string_id(&primitive.strings, &record.template_id)?, + full_state_template_id: required_id( + &ids.current_states, + &record.full_state_template_id, + "runtime-helicity full state", + )?, + variant_range: CheckedTableRange::new( + variant_start, + checked_u64(record.variants.len(), "runtime-helicity variant count")?, + ), + proof_algorithm_string_id: string_id(&primitive.strings, &record.proof_algorithm)?, + proof_digest_id: digest_id(&primitive.digests, &record.proof_digest)?, + semantic_digest_id: digest_id(&primitive.digests, &record.semantic_digest)?, + }); + } + + let evaluator_bindings = catalog + .evaluator_bindings + .iter() + .enumerate() + .map(|(index, record)| { + let input_layout = record + .input_layout + .iter() + .map(|value| string_id(&primitive.strings, value)) + .collect::>>()?; + let output_layout = record + .output_layout + .iter() + .map(|value| string_id(&primitive.strings, value)) + .collect::>>()?; + let expressions = record + .exact_expression_digests + .iter() + .map(|value| digest_id(&primitive.digests, value)) + .collect::>>()?; + let semantic_templates = record + .semantic_template_ids + .iter() + .map(|value| string_id(&primitive.strings, value)) + .collect::>>()?; + Ok(EvaluatorBindingRow { + id: checked_u32(index, "evaluator-binding row")?, + resolver_key_string_id: string_id(&primitive.strings, &record.resolver_key)?, + prepared_kernel_id: record.prepared_kernel_id.0.unwrap_or(MISSING_U32), + contract_kind: enum_id( + &record.contract_kind, + &[ + ("source", 0), + ("vertex", 1), + ("propagator", 2), + ("closure", 3), + ("model-parameter", 4), + ], + "evaluator contract kind", + )?, + callable_signature_digest_id: digest_id( + &primitive.digests, + &record.callable_signature, + )?, + input_state_sequence_id: sequence_id( + &primitive.u32_sequences, + mapped_ids( + &record.input_state_template_ids, + &ids.current_states, + "evaluator input state", + )?, + "evaluator input states", + )?, + output_state_template_id: optional_id( + record.output_state_template_id.0.as_ref(), + &ids.current_states, + "evaluator output state", + )?, + input_layout_sequence_id: sequence_id( + &primitive.u32_sequences, + input_layout, + "evaluator input layout", + )?, + output_layout_sequence_id: sequence_id( + &primitive.u32_sequences, + output_layout, + "evaluator output layout", + )?, + exact_expression_digest_sequence_id: sequence_id( + &primitive.u32_sequences, + expressions, + "evaluator expressions", + )?, + semantic_template_sequence_id: sequence_id( + &primitive.u32_sequences, + semantic_templates, + "evaluator semantic templates", + )?, + callable_kind: enum_id( + &record.callable_kind, + &[("prepared-kernel", 0), ("rusticol-template", 1)], + "evaluator callable kind", + )?, + runtime_template_string_id: optional_string_id( + &primitive.strings, + &record.runtime_template, + )?, + semantic_digest_id: digest_id(&primitive.digests, &record.semantic_digest)?, + }) + }) + .collect::>>()?; + + let input = OwnedRecurrenceTemplateInput { + input_abi: RECURRENCE_TEMPLATE_INPUT_ABI.to_owned(), + catalog_digest: semantic_digest(&catalog.header.catalog_digest, "catalog digest")?, + compiled_model_digest: semantic_digest( + &catalog.header.compiled_model_digest, + "compiled-model digest", + )?, + prepared_kernel_pack_digest: semantic_digest( + &catalog.header.prepared_kernel_pack_digest, + "prepared-kernel-pack digest", + )?, + catalog_header, + coupling_order_ranges, + coupling_order_terms, + contact_orbit_certificates, + contact_orbit_steps, + current_states, + digest_catalog, + evaluator_bindings, + exact_factors, + flavour_flow_ranges, + flavour_flow_values, + i32_sequence_ranges, + i32_sequence_values, + parameters, + propagators, + quantum_flows, + quantum_number_flow_ranges, + quantum_number_flow_terms, + runtime_helicity_contracts, + runtime_helicity_variants, + runtime_helicity_embeddings, + runtime_helicity_projections, + sources, + string_ranges, + string_bytes, + symmetry_proofs, + transitions, + closures, + color_contractions, + lc_color_transition_witnesses, + color_nc_terms, + u32_sequence_ranges, + u32_sequence_values, + }; + input.as_view().validate()?; + Ok(input) +} + +fn required_id(ids: &BTreeMap, value: &str, context: &str) -> RusticolResult { + ids.get(value) + .copied() + .ok_or_else(|| invalid(format!("unknown {context} {value:?}"))) +} + +fn optional_string_id( + catalog: &Catalog, + value: &RequiredOption, +) -> RusticolResult { + match &value.0 { + Some(value) => string_id(catalog, value), + None => Ok(MISSING_U32), + } +} + +fn optional_enum_id( + value: &RequiredOption, + choices: &[(&str, u8)], + context: &str, + missing: u8, +) -> RusticolResult { + match &value.0 { + Some(value) => enum_id(value, choices, context), + None => Ok(missing), + } +} + +fn sequence_id( + catalog: &Catalog>, + values: Vec, + context: &str, +) -> RusticolResult { + catalog.id(&values, context) +} + +#[cfg(test)] +mod tests { + use serde_json::{Value, json}; + + use super::{ + ROOT_SECTIONS, canonical_json_digest, project_owned_recurrence_template_catalog_json_v1, + project_recurrence_template_catalog_json_v1, write_canonical_json, + }; + + const COMPILED_DIGEST: &str = + "1111111111111111111111111111111111111111111111111111111111111111"; + const PACK_DIGEST: &str = "2222222222222222222222222222222222222222222222222222222222222222"; + const EMPTY_CATALOG_DIGEST: &str = + "93c542973b80e35e8300b4c25182cb0900040f9893dc6671b4af628062b9fbec"; + + fn empty_catalog() -> Value { + json!({ + "header": { + "abi": "pyamplicol-recurrence-template-v1", + "canonicalization_abi": "pyamplicol-canonical-json-v1", + "compiled_model_digest": COMPILED_DIGEST, + "exact_scalar_abi": "pyamplicol-exact-complex-rational-v1", + "prepared_kernel_pack_digest": PACK_DIGEST, + "catalog_digest": EMPTY_CATALOG_DIGEST, + }, + "parameters": [], + "current_states": [], + "sources": [], + "quantum_flows": [], + "contact_orbit_certificates": [], + "contact_orbit_steps": [], + "transitions": [], + "propagators": [], + "closures": [], + "color_contractions": [], + "symmetry_proofs": [], + "runtime_helicity_contracts": [], + "evaluator_bindings": [], + }) + } + + fn refresh_catalog_digest(value: &mut Value) { + let mut payload = value.clone(); + payload["header"] + .as_object_mut() + .expect("test header") + .remove("catalog_digest"); + let digest = canonical_json_digest(&payload).expect("canonical test digest"); + value["header"]["catalog_digest"] = Value::String(digest); + } + + #[test] + fn project_empty_catalog_matches_known_python_contract() { + let input = project_recurrence_template_catalog_json_v1(&empty_catalog()) + .expect("known Python empty catalog must project"); + assert_eq!(input.input_abi, "pyamplicol-recurrence-template-input-v1"); + assert_eq!(input.catalog_header.len(), 1); + assert_eq!(input.catalog_header[0].parameter_count, 0); + assert_eq!(input.catalog_header[0].evaluator_binding_count, 0); + assert_eq!(input.string_ranges.len(), 4); + assert_eq!(input.digest_catalog.len(), 3); + assert_eq!(input.coupling_order_ranges.len(), 1); + assert_eq!(input.u32_sequence_ranges.len(), 1); + assert_eq!(input.i32_sequence_ranges.len(), 1); + assert!(input.exact_factors.is_empty()); + input + .clone() + .validate() + .expect("projected empty catalog must satisfy core validation"); + } + + #[test] + fn owned_projection_preserves_the_authenticated_contract() { + let catalog = empty_catalog(); + let retained = project_recurrence_template_catalog_json_v1(&catalog) + .expect("retained catalog must project") + .validate() + .expect("retained projection must validate"); + let owned = project_owned_recurrence_template_catalog_json_v1(catalog) + .expect("owned catalog must project") + .validate() + .expect("owned projection must validate"); + assert_eq!(owned.summary(), retained.summary()); + + let mut stale = empty_catalog(); + stale["header"]["catalog_digest"] = Value::String(COMPILED_DIGEST.to_owned()); + let error = project_owned_recurrence_template_catalog_json_v1(stale) + .expect_err("owned stale catalog must fail closed"); + assert!( + error + .to_string() + .contains("stale recurrence-template catalog digest"), + "unexpected validation error: {error}" + ); + } + + #[test] + fn root_inventory_is_exact_and_all_thirteen_sections_are_required() { + assert_eq!(ROOT_SECTIONS.len(), 13); + + let mut unknown = empty_catalog(); + unknown["opaque"] = json!([]); + refresh_catalog_digest(&mut unknown); + let error = project_recurrence_template_catalog_json_v1(&unknown) + .expect_err("unknown root field must fail closed"); + assert!( + error.to_string().contains("unknown field `opaque`"), + "unexpected validation error: {error}" + ); + + for (section, _, _) in ROOT_SECTIONS { + let mut missing = empty_catalog(); + missing + .as_object_mut() + .expect("test catalog") + .remove(section); + let error = project_recurrence_template_catalog_json_v1(&missing) + .expect_err("missing semantic section must fail closed"); + assert!( + error.to_string().contains(section), + "missing {section} produced unexpected validation error: {error}" + ); + } + } + + #[test] + fn stale_catalog_digest_fails_before_projection() { + let mut catalog = empty_catalog(); + catalog["header"]["catalog_digest"] = Value::String(COMPILED_DIGEST.to_owned()); + let error = project_recurrence_template_catalog_json_v1(&catalog) + .expect_err("stale catalog digest must fail closed"); + assert!( + error + .to_string() + .contains("stale recurrence-template catalog digest"), + "unexpected validation error: {error}" + ); + } + + #[test] + fn canonical_json_matches_python_ascii_contract() { + let value = json!({"z": "é\n", "a": [true, null, "λ", -2]}); + let mut output = String::new(); + write_canonical_json(&value, &mut output).expect("canonical writer"); + assert_eq!( + output, + "{\"a\":[true,null,\"\\u03bb\",-2],\"z\":\"\\u00e9\\n\"}" + ); + } +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/recurrence/tests.rs b/pyamplicol/rust/crates/rusticol-core/src/recurrence/tests.rs new file mode 100644 index 0000000000..7e3b1511fe --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/recurrence/tests.rs @@ -0,0 +1,707 @@ +// SPDX-License-Identifier: 0BSD + +use std::cmp::Ordering; + +use super::*; + +fn digest(byte: u8) -> SemanticDigest { + SemanticDigest::new([byte; 32]).expect("test digest must be nonzero") +} + +fn momentum(slot: u32) -> CanonicalMomentumLinearForm { + CanonicalMomentumLinearForm::new(vec![MomentumTerm { + source_slot: slot, + coefficient: 1, + }]) + .expect("test momentum is canonical") +} + +#[test] +fn abi_constants_are_frozen() { + assert_eq!(RECURRENCE_TEMPLATE_ABI, "pyamplicol-recurrence-template-v1"); + assert_eq!( + RECURRENCE_BUILDER_INPUT_ABI, + "pyamplicol-recurrence-builder-input-v2" + ); + assert_eq!( + RECURRENCE_BUILDER_RESULT_ABI, + "pyamplicol-recurrence-builder-result-v2" + ); + assert_eq!(RECURRENCE_PLAN_ABI, "pyamplicol-recurrence-plan-v2"); + assert_eq!( + RECURRENCE_RUNTIME_LAYOUT_ABI, + "pyamplicol-recurrence-runtime-layout-v2" + ); + assert_eq!( + RECURRENCE_RUNTIME_KIND, + "pyamplicol-runtime-recurrence-execution" + ); + assert_eq!( + RECURRENCE_RUNTIME_CAPABILITY, + "rusticol.recurrence-direct-arena.complex-f64.v1" + ); + assert_eq!( + RECURRENCE_LC_COLOR_CAPABILITY, + "rusticol.recurrence-color.lc.v1" + ); +} + +#[test] +fn dynamic_union_spin_sentinel_is_rejected_as_concrete_input() { + for context in [ + "process source spin", + "template source spin", + "quantum input spin", + "quantum result spin", + ] { + assert!(validate_concrete_spin_state(-1, context).is_ok()); + let error = validate_concrete_spin_state(DYNAMIC_UNION_SOURCE_SPIN_STATE_CLASS, context) + .unwrap_err() + .to_string(); + assert!(error.contains("reserved dynamic-union source-spin sentinel")); + } +} + +#[test] +fn exact_rationals_parse_reduce_and_canonicalize_zero() { + let value = ExactRational::parse_parts("-42", "56").unwrap(); + assert_eq!(value.numerator(), -3); + assert_eq!(value.denominator(), 4); + assert_eq!(value.to_string(), "-3/4"); + assert_eq!( + "000/009".parse::().unwrap(), + ExactRational::ZERO + ); + assert!(ExactRational::parse_parts("1", "0").is_err()); + assert!(ExactRational::parse_parts("1", "-2").is_err()); + assert!("1/2/3".parse::().is_err()); +} + +#[test] +fn exact_aggregation_distinguishes_binary64_collision() { + let one = ExactRational::ONE; + let epsilon = ExactRational::new(1, 1_i128 << 54).unwrap(); + let sum = one.checked_add(epsilon).unwrap(); + assert_ne!(sum, one); + assert_eq!(sum.checked_sub(one).unwrap(), epsilon); + assert_eq!(1.0_f64 + 2.0_f64.powi(-54), 1.0_f64); +} + +#[test] +fn exact_ordering_avoids_cross_multiplication_overflow() { + let maximum = i128::MAX; + let left = ExactRational::new(maximum - 1, maximum).unwrap(); + let right = ExactRational::new(maximum - 2, maximum - 1).unwrap(); + assert_eq!(left.cmp(&right), Ordering::Greater); +} + +#[test] +fn exact_arithmetic_fails_closed_on_overflow() { + let large = ExactRational::new(i128::MAX, 1).unwrap(); + assert!(large.checked_add(ExactRational::ONE).is_err()); + assert!( + large + .checked_mul(ExactRational::new(2, 1).unwrap()) + .is_err() + ); + assert!(ExactRational::ONE.checked_div(ExactRational::ZERO).is_err()); +} + +#[test] +fn binary64_conversion_is_exact_or_rejected() { + assert_eq!( + ExactRational::from_f64_exact(0.1).unwrap(), + ExactRational::new(3_602_879_701_896_397, 36_028_797_018_963_968).unwrap() + ); + assert_eq!( + ExactRational::from_f64_exact(-0.0).unwrap(), + ExactRational::ZERO + ); + assert!(ExactRational::from_f64_exact(f64::INFINITY).is_err()); + assert!(ExactRational::from_f64_exact(f64::from_bits(1)).is_err()); +} + +#[test] +fn exact_complex_arithmetic_is_canonical() { + let left = ExactComplexRational::parse_parts("1", "2", "1", "3").unwrap(); + let right = ExactComplexRational::parse_parts("2", "5", "-1", "7").unwrap(); + let product = left.checked_mul(right).unwrap(); + assert_eq!(product.real(), ExactRational::new(26, 105).unwrap()); + assert_eq!(product.imag(), ExactRational::new(13, 210).unwrap()); + assert_eq!(product.checked_div(right).unwrap(), left); +} + +#[test] +fn checked_ranges_reject_overflow_gaps_and_bounds() { + assert!(CheckedTableRange::new(u64::MAX, 1).end("test").is_err()); + assert!(validate_ranges_within("test", &[CheckedTableRange::new(2, 2)], 3).is_err()); + assert!( + validate_packed_ranges( + "test", + &[CheckedTableRange::new(0, 1), CheckedTableRange::new(2, 1)], + 3 + ) + .is_err() + ); + validate_packed_ranges( + "test", + &[CheckedTableRange::new(0, 1), CheckedTableRange::new(1, 2)], + 3, + ) + .unwrap(); +} + +#[test] +fn table_validators_reject_mismatched_columns_and_references() { + assert!(validate_equal_column_lengths("table", &[("a", 2), ("b", 1)]).is_err()); + assert!(validate_u32_references(&[0, 2], 2, "reference").is_err()); + validate_equal_column_lengths("table", &[("a", 2), ("b", 2)]).unwrap(); + validate_u32_references(&[0, 1], 2, "reference").unwrap(); +} + +#[test] +fn multiword_masks_retain_high_bits_and_validate_canonical_form() { + let ranges = [CheckedTableRange::new(0, 0), CheckedTableRange::new(0, 2)]; + let populations = [0, 2]; + let words = [1, 1_u64 << 63]; + let catalog = MultiwordMaskCatalogView { + ranges: &ranges, + populations: &populations, + words: &words, + }; + catalog.validate(true).unwrap(); + assert!(catalog.contains(1, 0).unwrap()); + assert!(catalog.contains(1, 127).unwrap()); + assert!(!catalog.contains(1, 126).unwrap()); + + let invalid_words = [1, 0]; + let invalid = MultiwordMaskCatalogView { + ranges: &[CheckedTableRange::new(0, 2)], + populations: &[1], + words: &invalid_words, + }; + assert!(invalid.validate(false).is_err()); +} + +fn header_for( + strategy: RecurrenceStrategy, + sections: &[CanonicalInputSection<'_>], +) -> RecurrenceBuilderInputHeader { + let placeholder = digest(9); + let mut header = RecurrenceBuilderInputHeader::canonical( + strategy, + sections.len() as u32, + digest(1), + digest(2), + digest(3), + placeholder, + ); + header.input_digest = canonical_input_digest(&header, sections).unwrap(); + header +} + +#[test] +fn input_header_digest_is_deterministic_and_authenticated() { + let sections = [ + CanonicalInputSection { + name: "catalogs/states", + row_width: 2, + row_count: 2, + bytes: &[1, 2, 3, 4], + }, + CanonicalInputSection { + name: "process/legs", + row_width: 1, + row_count: 2, + bytes: &[5, 6], + }, + ]; + let header = header_for(RecurrenceStrategy::TopologyReplay, §ions); + validate_header_and_sections(&header, §ions).unwrap(); + assert_eq!( + canonical_input_digest(&header, §ions).unwrap(), + header.input_digest + ); + + let modified = [ + sections[0], + CanonicalInputSection { + bytes: &[5, 7], + ..sections[1] + }, + ]; + assert!(validate_header_and_sections(&header, &modified).is_err()); +} + +#[test] +fn input_header_rejects_noncanonical_sections_and_abi_drift() { + let sections = [ + CanonicalInputSection { + name: "z", + row_width: 1, + row_count: 1, + bytes: &[0], + }, + CanonicalInputSection { + name: "a", + row_width: 1, + row_count: 1, + bytes: &[0], + }, + ]; + let mut header = RecurrenceBuilderInputHeader::canonical( + RecurrenceStrategy::AllFlowUnion, + 2, + digest(1), + digest(2), + digest(3), + digest(4), + ); + assert!(validate_header_and_sections(&header, §ions).is_err()); + let canonical_section = [CanonicalInputSection { + name: "a", + row_width: 1, + row_count: 1, + bytes: &[0], + }]; + let wrong_count_header = RecurrenceBuilderInputHeader::canonical( + RecurrenceStrategy::AllFlowUnion, + 2, + digest(1), + digest(2), + digest(3), + digest(4), + ); + assert!(canonical_input_digest(&wrong_count_header, &canonical_section).is_err()); + header.builder_input_abi = "future-abi".to_owned(); + assert!(header.validate_identity().is_err()); +} + +#[test] +fn strategy_and_digest_validation_fail_closed() { + assert_eq!( + RecurrenceStrategy::try_from(0).unwrap(), + RecurrenceStrategy::TopologyReplay + ); + assert_eq!( + RecurrenceStrategy::try_from(1).unwrap(), + RecurrenceStrategy::AllFlowUnion + ); + assert_eq!( + RecurrenceStrategy::try_from(2).unwrap(), + RecurrenceStrategy::ContractedColorUnion + ); + assert!(RecurrenceStrategy::try_from(3).is_err()); + assert!(SemanticDigest::new([0; 32]).is_err()); +} + +#[test] +fn current_core_key_preserves_every_model_visible_axis() { + let ancestry = vec![ + SourceStateAssignment::new(0, 1), + SourceStateAssignment::new(2, 0), + ]; + let key = CurrentCoreKey::new( + digest(1), + RecurrenceNodeKind::Current, + 7, + DynamicLCColorStateId::from_interner(13), + vec![0, 2], + CanonicalMomentumLinearForm::new(vec![ + MomentumTerm { + source_slot: 0, + coefficient: 1, + }, + MomentumTerm { + source_slot: 2, + coefficient: -1, + }, + ]) + .unwrap(), + CurrentHelicityIdentity::topology_replay(-1, ancestry.clone()).unwrap(), + vec![2, -2], + 3, + vec![1, 2], + CurrentSourceBinding::None, + Some(9), + ) + .unwrap(); + let changed = CurrentCoreKey::new( + digest(1), + RecurrenceNodeKind::Current, + 7, + DynamicLCColorStateId::from_interner(13), + vec![0, 2], + key.momentum().clone(), + CurrentHelicityIdentity::topology_replay(1, ancestry.clone()).unwrap(), + key.flavour_flow().to_vec(), + key.quantum_number_flow_id(), + key.coupling_orders().to_vec(), + CurrentSourceBinding::None, + Some(9), + ) + .unwrap(); + assert_ne!(key, changed); + assert!( + CurrentCoreKey::new( + digest(1), + RecurrenceNodeKind::Current, + 7, + DynamicLCColorStateId::from_interner(13), + vec![2, 0], + momentum(0), + CurrentHelicityIdentity::topology_replay(0, ancestry).unwrap(), + vec![], + 0, + vec![], + CurrentSourceBinding::None, + None, + ) + .is_err() + ); +} + +#[test] +fn current_core_key_enforces_layout_specific_helicity_identity() { + let source_momentum = momentum(0); + let replay_source = CurrentCoreKey::new( + digest(1), + RecurrenceNodeKind::Source, + 7, + DynamicLCColorStateId::from_interner(13), + vec![0], + source_momentum.clone(), + CurrentHelicityIdentity::topology_replay(-1, vec![SourceStateAssignment::new(0, 2)]) + .unwrap(), + vec![1], + 3, + vec![], + CurrentSourceBinding::FixedTemplate(11), + None, + ) + .unwrap(); + assert_eq!(replay_source.spin_state_class(), -1); + assert_eq!( + replay_source.helicity_identity().local_source_states()[0].state_index(), + 2 + ); + + let union_source = CurrentCoreKey::new( + digest(1), + RecurrenceNodeKind::Source, + 7, + DynamicLCColorStateId::from_interner(13), + vec![0], + source_momentum.clone(), + CurrentHelicityIdentity::all_flow_union(0), + vec![1], + 3, + vec![], + CurrentSourceBinding::runtime_dispatch(5, vec![11]).unwrap(), + None, + ) + .unwrap(); + assert_eq!( + union_source.source_binding(), + &CurrentSourceBinding::runtime_dispatch(5, vec![11]).unwrap() + ); + assert!( + union_source + .helicity_identity() + .local_source_states() + .is_empty() + ); + + assert!( + CurrentCoreKey::new( + digest(1), + RecurrenceNodeKind::Current, + 7, + DynamicLCColorStateId::from_interner(13), + vec![0, 2], + source_momentum.clone(), + CurrentHelicityIdentity::topology_replay(0, vec![SourceStateAssignment::new(0, 1)],) + .unwrap(), + vec![], + 3, + vec![], + CurrentSourceBinding::None, + None, + ) + .is_err() + ); + assert!( + CurrentCoreKey::new( + digest(1), + RecurrenceNodeKind::Source, + 7, + DynamicLCColorStateId::from_interner(13), + vec![0], + source_momentum, + CurrentHelicityIdentity::all_flow_union(0), + vec![], + 3, + vec![], + CurrentSourceBinding::FixedTemplate(11), + None, + ) + .is_err() + ); +} + +#[test] +fn dynamic_lc_color_states_preserve_order_and_only_fold_trace_rotations() { + let trace = LCColorComponent::new(LCColorComponentKind::Trace, vec![4, 7, 2]).unwrap(); + assert_eq!(trace.source_slots(), &[2, 4, 7]); + let reversed = LCColorComponent::new(LCColorComponentKind::Trace, vec![2, 7, 4]).unwrap(); + assert_ne!(trace, reversed); + + let open = LCColorComponent::new(LCColorComponentKind::OpenString, vec![7, 2]).unwrap(); + assert_eq!(open.source_slots(), &[7, 2]); + assert!(LCColorComponent::new(LCColorComponentKind::OpenString, vec![2, 2]).is_err()); + assert!(DynamicLCColorState::new(1, None, vec![trace, open]).is_err()); +} + +#[test] +fn dynamic_lc_color_state_ids_are_issued_only_by_the_interner() { + let state = DynamicLCColorState::new( + 1, + Some(0), + vec![LCColorComponent::new(LCColorComponentKind::AdjointSegment, vec![0]).unwrap()], + ) + .unwrap(); + let mut interner = DynamicLCColorStateInterner::default(); + let first = interner.intern(state.clone()).unwrap(); + let second = interner.intern(state).unwrap(); + assert_eq!(first, second); + assert_eq!(first.get(), 0); + assert_eq!(interner.len(), 1); + assert_eq!(interner.get(first).unwrap().output_color_shape_id(), 1); +} + +#[test] +fn compiler_certified_lc_color_witnesses_apply_exact_ordered_operations() { + let left = DynamicLCColorState::new( + 1, + Some(0), + vec![LCColorComponent::new(LCColorComponentKind::AdjointSegment, vec![0]).unwrap()], + ) + .unwrap(); + let right = DynamicLCColorState::new( + 1, + Some(0), + vec![LCColorComponent::new(LCColorComponentKind::AdjointSegment, vec![1, 2]).unwrap()], + ) + .unwrap(); + let witness = LCColorTransitionWitness::new( + [1, 0], + 0b10, + LCColorComponentOperation::ConcatenateJoin, + Some(LCColorComponentKind::AdjointSegment), + LCColorComponentRole::Active, + Some(2), + LCColorPortWiring::new( + [1, 0], + vec![], + vec![ + LCColorParentPort::new(1, 0).unwrap(), + LCColorParentPort::new(0, 0).unwrap(), + ], + ) + .unwrap(), + ExactComplexRational::new(ExactRational::ONE, ExactRational::ZERO), + digest(9), + ) + .unwrap(); + let result = witness.apply(&left, &right).unwrap().unwrap(); + assert_eq!(result.output_color_shape_id(), 2); + assert_eq!(result.components().len(), 2); + assert_eq!(result.components()[0].source_slots(), &[2, 1]); + assert_eq!(result.components()[1].source_slots(), &[0]); + assert_eq!(witness.proof_digest(), digest(9)); + + assert!( + LCColorTransitionWitness::new( + [0, 0], + 0, + LCColorComponentOperation::ConcatenateKeep, + None, + LCColorComponentRole::None, + Some(2), + LCColorPortWiring::new([0, 1], vec![], vec![]).unwrap(), + ExactComplexRational::new(ExactRational::ONE, ExactRational::ZERO), + digest(9), + ) + .is_err() + ); +} + +#[test] +fn lc_color_join_uses_declared_active_components_in_multi_line_forests() { + let left = DynamicLCColorState::new( + 1, + Some(1), + vec![ + LCColorComponent::new(LCColorComponentKind::OpenString, vec![7, 8]).unwrap(), + LCColorComponent::new(LCColorComponentKind::OpenString, vec![0, 1]).unwrap(), + ], + ) + .unwrap(); + let right = DynamicLCColorState::new_port_wired( + 1, + vec![ + LCColorPortBinding::new(0, LCColorEndpoint::Back), + LCColorPortBinding::new(0, LCColorEndpoint::Front), + ], + vec![ + LCColorComponent::new(LCColorComponentKind::AdjointSegment, vec![2]).unwrap(), + LCColorComponent::new(LCColorComponentKind::OpenString, vec![5, 6]).unwrap(), + ], + ) + .unwrap(); + let witness = LCColorTransitionWitness::new( + [0, 1], + 0, + LCColorComponentOperation::ConcatenateJoin, + Some(LCColorComponentKind::OpenString), + LCColorComponentRole::Active, + Some(2), + LCColorPortWiring::new( + [0, 1], + vec![[ + LCColorParentPort::new(0, 0).unwrap(), + LCColorParentPort::new(1, 1).unwrap(), + ]], + vec![LCColorParentPort::new(1, 0).unwrap()], + ) + .unwrap(), + ExactComplexRational::new(ExactRational::ONE, ExactRational::ZERO), + digest(11), + ) + .unwrap(); + let result = witness.apply(&left, &right).unwrap().unwrap(); + assert_eq!(result.active_component_index(), Some(1)); + assert_eq!(result.components()[0].source_slots(), &[7, 8]); + assert_eq!(result.components()[1].source_slots(), &[0, 1, 2]); + assert_eq!(result.components()[2].source_slots(), &[5, 6]); +} + +#[test] +fn port_wiring_preserves_crossed_two_line_connectivity() { + let fundamental = |slot| { + DynamicLCColorState::new_port_wired( + 1, + vec![LCColorPortBinding::new(0, LCColorEndpoint::Back)], + vec![LCColorComponent::new(LCColorComponentKind::OpenString, vec![slot]).unwrap()], + ) + .unwrap() + }; + let antifundamental = |slot| { + DynamicLCColorState::new_port_wired( + 2, + vec![LCColorPortBinding::new(0, LCColorEndpoint::Front)], + vec![LCColorComponent::new(LCColorComponentKind::OpenString, vec![slot]).unwrap()], + ) + .unwrap() + }; + + // u(2) + ubar(3) -> g*: the two gluon result ports retain the two + // independent quark-line endpoints rather than collapsing to one port. + let gluon_wiring = LCColorPortWiring::new( + [0, 1], + vec![], + vec![ + LCColorParentPort::new(0, 0).unwrap(), + LCColorParentPort::new(1, 0).unwrap(), + ], + ) + .unwrap(); + let gluon = gluon_wiring + .apply(&fundamental(2), &antifundamental(3), 3) + .unwrap(); + assert_eq!(gluon.result_port_bindings().len(), 2); + assert_eq!(gluon.result_port_lineage_source_slots().unwrap(), [2, 3]); + + // d(1) consumes the ubar-side gluon port; the u-side port becomes the + // result current port. This creates passive [1,3] plus active [2]. + let quark_wiring = LCColorPortWiring::new( + [0, 1], + vec![[ + LCColorParentPort::new(0, 0).unwrap(), + LCColorParentPort::new(1, 1).unwrap(), + ]], + vec![LCColorParentPort::new(1, 0).unwrap()], + ) + .unwrap(); + let complement = quark_wiring.apply(&fundamental(1), &gluon, 1).unwrap(); + assert_eq!( + complement + .components() + .iter() + .map(LCColorComponent::source_slots) + .collect::>(), + vec![&[1, 3][..], &[2][..]], + ); + assert_eq!(complement.result_port_lineage_source_slots().unwrap(), [2]); + + // The remaining result port closes against dbar(0), yielding exactly the + // crossed physical forest [1,3] + [2,0]. + let closure_wiring = LCColorPortWiring::new( + [0, 1], + vec![[ + LCColorParentPort::new(0, 0).unwrap(), + LCColorParentPort::new(1, 0).unwrap(), + ]], + vec![], + ) + .unwrap(); + let closed = closure_wiring + .apply(&complement, &antifundamental(0), 0) + .unwrap(); + assert!(closed.result_port_bindings().is_empty()); + assert!( + closed + .result_port_lineage_source_slots() + .unwrap() + .is_empty() + ); + assert_eq!( + closed + .components() + .iter() + .map(LCColorComponent::source_slots) + .collect::>(), + vec![&[1, 3][..], &[2, 0][..]], + ); +} + +#[test] +fn contribution_key_validates_aligned_parent_contracts() { + let key = ContributionKey::new( + 1, + vec![2, 3], + vec![4, 5], + vec![momentum(0), momentum(1)], + 6, + 7, + LCColorWitnessTermId::new(8, 0), + digest(9), + 10, + ) + .unwrap(); + assert_eq!(key.parent_value_class_ids(), &[2, 3]); + assert!( + ContributionKey::new( + 1, + vec![2, 3], + vec![4], + vec![momentum(0), momentum(1)], + 6, + 7, + LCColorWitnessTermId::new(8, 0), + digest(9), + 10, + ) + .is_err() + ); +} diff --git a/pyamplicol/rust/crates/rusticol-core/src/wavefunctions.rs b/pyamplicol/rust/crates/rusticol-core/src/wavefunctions.rs new file mode 100644 index 0000000000..3db5fed3ac --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/src/wavefunctions.rs @@ -0,0 +1,963 @@ +// SPDX-License-Identifier: 0BSD + +use super::*; + +pub(super) fn c64(re: f64, im: f64) -> Complex { + Complex::new(re, im) +} + +pub(super) fn negate(momentum: [f64; 4]) -> [f64; 4] { + [-momentum[0], -momentum[1], -momentum[2], -momentum[3]] +} + +pub(super) fn fortran_sign(value: f64, sign_source: f64) -> f64 { + value.abs().copysign(sign_source) +} + +#[cfg(feature = "symbolica-runtime")] +pub(super) fn complex_zero() -> Complex +where + T: Real + Clone, +{ + Complex::new(T::new_zero(), T::new_zero()) +} + +#[cfg(feature = "symbolica-runtime")] +pub(super) fn c_generic(re: T, im: T) -> Complex { + Complex::new(re, im) +} + +#[cfg(feature = "symbolica-runtime")] +pub(super) fn negate_generic(momentum: &[T; 4]) -> [T; 4] +where + T: Real + Clone, +{ + [ + -momentum[0].clone(), + -momentum[1].clone(), + -momentum[2].clone(), + -momentum[3].clone(), + ] +} + +#[cfg(feature = "symbolica-runtime")] +pub(super) fn is_zero(value: &T) -> bool { + value.to_f64() == 0.0 +} + +#[cfg(feature = "symbolica-runtime")] +pub(super) fn t_min(left: &T, right: &T) -> T +where + T: RealLike + PartialOrd + Clone, +{ + if left <= right { + left.clone() + } else { + right.clone() + } +} + +#[cfg(feature = "symbolica-runtime")] +pub(super) fn t_max(left: &T, right: &T) -> T +where + T: RealLike + PartialOrd + Clone, +{ + if left >= right { + left.clone() + } else { + right.clone() + } +} + +#[cfg(feature = "symbolica-runtime")] +pub(super) fn fortran_sign_generic(value: &T, sign_source: &T) -> T +where + T: Real + RealLike + Clone, +{ + let magnitude = value.norm(); + if sign_source.to_f64().is_sign_negative() { + -magnitude + } else { + magnitude + } +} + +pub(super) fn ext_quark_dirac(momentum: [f64; 4], helicity: i32) -> [Complex; 4] { + let [energy, px, py, pz] = momentum; + if energy > 0.0 { + let sqp0p3 = if px == 0.0 && py == 0.0 && pz < 0.0 { + 0.0 + } else { + (energy + pz).max(0.0).sqrt() + }; + let chi1 = c64(sqp0p3, 0.0); + let chi2 = if sqp0p3 == 0.0 { + c64(-(helicity as f64) * (2.0 * energy).sqrt(), 0.0) + } else { + c64(helicity as f64 * px / sqp0p3, -py / sqp0p3) + }; + if helicity == 1 { + return [chi1, chi2, c64(0.0, 0.0), c64(0.0, 0.0)]; + } + return [c64(0.0, 0.0), c64(0.0, 0.0), chi2, chi1]; + } + let sqp0p3 = if px == 0.0 && py == 0.0 && pz > 0.0 { + 0.0 + } else { + -(-(energy + pz)).max(0.0).sqrt() + }; + let chi1 = c64(sqp0p3, 0.0); + let chi2 = if sqp0p3 == 0.0 { + c64(-(helicity as f64) * (2.0 * energy.abs()).sqrt(), 0.0) + } else { + c64(-(helicity as f64) * (-px) / sqp0p3, py / sqp0p3) + }; + if -helicity == 1 { + [chi1, chi2, c64(0.0, 0.0), c64(0.0, 0.0)] + } else { + [c64(0.0, 0.0), c64(0.0, 0.0), chi2, chi1] + } +} + +pub(super) fn ext_antiquark_dirac(momentum: [f64; 4], helicity: i32) -> [Complex; 4] { + let [energy, px, py, pz] = momentum; + if energy > 0.0 { + let sqp0p3 = if px == 0.0 && py == 0.0 && pz < 0.0 { + 0.0 + } else { + -(energy + pz).max(0.0).sqrt() + }; + let chi1 = c64(sqp0p3, 0.0); + let chi2 = if sqp0p3 == 0.0 { + c64(-(helicity as f64) * (2.0 * energy).sqrt(), 0.0) + } else { + c64(-(helicity as f64) * px / sqp0p3, py / sqp0p3) + }; + if -helicity == 1 { + return [c64(0.0, 0.0), c64(0.0, 0.0), chi1, chi2]; + } + return [chi2, chi1, c64(0.0, 0.0), c64(0.0, 0.0)]; + } + let sqp0p3 = if px == 0.0 && py == 0.0 && pz > 0.0 { + 0.0 + } else { + (-(energy + pz)).max(0.0).sqrt() + }; + let chi1 = c64(sqp0p3, 0.0); + let chi2 = if sqp0p3 == 0.0 { + c64(-(helicity as f64) * (2.0 * energy.abs()).sqrt(), 0.0) + } else { + c64(helicity as f64 * (-px) / sqp0p3, -py / sqp0p3) + }; + if helicity == 1 { + [c64(0.0, 0.0), c64(0.0, 0.0), chi1, chi2] + } else { + [chi2, chi1, c64(0.0, 0.0), c64(0.0, 0.0)] + } +} + +pub(super) fn ext_quark_dirac_massive( + momentum: [f64; 4], + helicity: i32, + mass: f64, +) -> [Complex; 4] { + if mass.abs() < 1.0e-8 { + return ext_quark_dirac(momentum, helicity); + } + let [energy, px, py, pz] = momentum; + let nsf = if energy > 0.0 { 1 } else { -1 }; + let nh = nsf * helicity; + let pp = (px * px + py * py + pz * pz).sqrt().abs(); + let omega1 = (energy.abs() + pp).sqrt(); + let omega2 = mass / omega1; + let omega = [omega1, omega2]; + let sf1 = (1 + nsf + (1 - nsf) * nh) as f64 * 0.5; + let sf2 = (1 + nsf - (1 - nsf) * nh) as f64 * 0.5; + let ip = ((3 + nh) / 2 - 1) as usize; + let im = ((3 - nh) / 2 - 1) as usize; + let sfomeg = [sf1 * omega[ip], sf2 * omega[im]]; + let (signed_px, signed_py, signed_pz) = if energy > 0.0 { + (px, py, pz) + } else { + (-px, -py, -pz) + }; + let pp3 = (pp + signed_pz).max(0.0); + let chi1 = if pp == 0.0 { + c64(1.0, 0.0) + } else { + c64((pp3 * 0.5 / pp).sqrt(), 0.0) + }; + let chi2 = if pp3 == 0.0 || pp == 0.0 { + c64(-(nh as f64), 0.0) + } else { + let denom = (2.0 * pp * pp3).sqrt(); + c64((nh as f64) * signed_px / denom, -signed_py / denom) + }; + let chi = [chi1, chi2]; + [ + chi[im] * sfomeg[1], + chi[ip] * sfomeg[1], + chi[im] * sfomeg[0], + chi[ip] * sfomeg[0], + ] +} + +pub(super) fn ext_antiquark_dirac_massive( + momentum: [f64; 4], + helicity: i32, + mass: f64, +) -> [Complex; 4] { + if mass.abs() < 1.0e-8 { + return ext_antiquark_dirac(momentum, helicity); + } + let [energy, px, py, pz] = momentum; + let nsf = if energy > 0.0 { -1 } else { 1 }; + let nh = nsf * helicity; + let pp = (px * px + py * py + pz * pz).sqrt().abs(); + let omega1 = (energy.abs() + pp).sqrt(); + let omega2 = mass / omega1; + let omega = [omega1, omega2]; + let sf1 = (1 + nsf + (1 - nsf) * nh) as f64 * 0.5; + let sf2 = (1 + nsf - (1 - nsf) * nh) as f64 * 0.5; + let ip = ((3 + nh) / 2 - 1) as usize; + let im = ((3 - nh) / 2 - 1) as usize; + let sfomeg = [sf1 * omega[ip], sf2 * omega[im]]; + let (signed_px, signed_py, signed_pz) = if energy > 0.0 { + (px, py, pz) + } else { + (-px, -py, -pz) + }; + let pp3 = (pp + signed_pz).max(0.0); + let chi1 = if pp == 0.0 { + c64(1.0, 0.0) + } else { + c64((pp3 * 0.5 / pp).sqrt(), 0.0) + }; + let chi2 = if pp3 == 0.0 || pp == 0.0 { + c64(-(nh as f64), 0.0) + } else { + let denom = (2.0 * pp * pp3).sqrt(); + c64((nh as f64) * signed_px / denom, signed_py / denom) + }; + let chi = [chi1, chi2]; + [ + chi[im] * sfomeg[0], + chi[ip] * sfomeg[0], + chi[im] * sfomeg[1], + chi[ip] * sfomeg[1], + ] +} + +#[cfg(feature = "symbolica-runtime")] +pub(super) fn ext_quark_dirac_generic(momentum: &[T; 4], helicity: i32) -> [Complex; 4] +where + T: Real + RealLike + From + PartialOrd + Clone, +{ + let energy = momentum[0].clone(); + let px = momentum[1].clone(); + let py = momentum[2].clone(); + let pz = momentum[3].clone(); + if energy.to_f64() > 0.0 { + let sqp0p3 = if is_zero(&px) && is_zero(&py) && pz.to_f64() < 0.0 { + T::new_zero() + } else { + t_max(&(energy.clone() + pz.clone()), &T::new_zero()).sqrt() + }; + let chi1 = c_generic(sqp0p3.clone(), T::new_zero()); + let chi2 = if is_zero(&sqp0p3) { + c_generic( + -energy.from_i64(helicity as i64) * (energy.from_i64(2) * energy.clone()).sqrt(), + T::new_zero(), + ) + } else { + c_generic( + energy.from_i64(helicity as i64) * px.clone() / sqp0p3.clone(), + -py.clone() / sqp0p3.clone(), + ) + }; + if helicity == 1 { + return [chi1, chi2, complex_zero(), complex_zero()]; + } + return [complex_zero(), complex_zero(), chi2, chi1]; + } + let sqp0p3 = if is_zero(&px) && is_zero(&py) && pz.to_f64() > 0.0 { + T::new_zero() + } else { + -t_max(&(-(energy.clone() + pz.clone())), &T::new_zero()).sqrt() + }; + let chi1 = c_generic(sqp0p3.clone(), T::new_zero()); + let chi2 = if is_zero(&sqp0p3) { + c_generic( + -energy.from_i64(helicity as i64) * (energy.from_i64(2) * energy.norm()).sqrt(), + T::new_zero(), + ) + } else { + c_generic( + -energy.from_i64(helicity as i64) * (-px.clone()) / sqp0p3.clone(), + py.clone() / sqp0p3.clone(), + ) + }; + if -helicity == 1 { + [chi1, chi2, complex_zero(), complex_zero()] + } else { + [complex_zero(), complex_zero(), chi2, chi1] + } +} + +#[cfg(feature = "symbolica-runtime")] +pub(super) fn ext_antiquark_dirac_generic(momentum: &[T; 4], helicity: i32) -> [Complex; 4] +where + T: Real + RealLike + From + PartialOrd + Clone, +{ + let energy = momentum[0].clone(); + let px = momentum[1].clone(); + let py = momentum[2].clone(); + let pz = momentum[3].clone(); + if energy.to_f64() > 0.0 { + let sqp0p3 = if is_zero(&px) && is_zero(&py) && pz.to_f64() < 0.0 { + T::new_zero() + } else { + -t_max(&(energy.clone() + pz.clone()), &T::new_zero()).sqrt() + }; + let chi1 = c_generic(sqp0p3.clone(), T::new_zero()); + let chi2 = if is_zero(&sqp0p3) { + c_generic( + -energy.from_i64(helicity as i64) * (energy.from_i64(2) * energy.clone()).sqrt(), + T::new_zero(), + ) + } else { + c_generic( + -energy.from_i64(helicity as i64) * px.clone() / sqp0p3.clone(), + py.clone() / sqp0p3.clone(), + ) + }; + if -helicity == 1 { + return [complex_zero(), complex_zero(), chi1, chi2]; + } + return [chi2, chi1, complex_zero(), complex_zero()]; + } + let sqp0p3 = if is_zero(&px) && is_zero(&py) && pz.to_f64() > 0.0 { + T::new_zero() + } else { + t_max(&(-(energy.clone() + pz.clone())), &T::new_zero()).sqrt() + }; + let chi1 = c_generic(sqp0p3.clone(), T::new_zero()); + let chi2 = if is_zero(&sqp0p3) { + c_generic( + -energy.from_i64(helicity as i64) * (energy.from_i64(2) * energy.norm()).sqrt(), + T::new_zero(), + ) + } else { + c_generic( + energy.from_i64(helicity as i64) * (-px.clone()) / sqp0p3.clone(), + -py.clone() / sqp0p3.clone(), + ) + }; + if helicity == 1 { + [complex_zero(), complex_zero(), chi1, chi2] + } else { + [chi2, chi1, complex_zero(), complex_zero()] + } +} + +pub(super) fn ext_massive_vector( + momentum: [f64; 4], + helicity: i32, + mass: f64, +) -> [Complex; 4] { + let [energy, px, py, pz] = momentum; + if energy < 0.0 { + return ext_massive_vector(negate(momentum), -helicity, mass); + } + let sqh = 0.5f64.sqrt(); + let hel = helicity as f64; + let nsvahl = helicity.abs() as f64; + let pt2 = px * px + py * py; + let pp = energy.min((pt2 + pz * pz).sqrt()); + let pt = pp.min(pt2.sqrt()); + let hel0 = 1.0 - hel.abs(); + if pp == 0.0 { + return [ + c64(0.0, 0.0), + c64(-hel * sqh, 0.0), + c64(0.0, nsvahl * sqh), + c64(hel0, 0.0), + ]; + } + let emp = energy / (mass * pp); + let wf0 = c64(hel0 * pp / mass, 0.0); + let wf3 = c64(hel0 * pz * emp + hel * pt / pp * sqh, 0.0); + let (wf1, wf2) = if pt != 0.0 { + let pzpt = pz / (pp * pt) * sqh * hel; + ( + c64(hel0 * px * emp - px * pzpt, -nsvahl * py / pt * sqh), + c64(hel0 * py * emp - py * pzpt, nsvahl * px / pt * sqh), + ) + } else { + ( + c64(-hel * sqh, 0.0), + c64(0.0, nsvahl * fortran_sign(sqh, pz)), + ) + }; + [wf0, wf1, wf2, wf3] +} + +#[cfg(feature = "symbolica-runtime")] +pub(super) fn ext_massive_vector_generic( + momentum: &[T; 4], + helicity: i32, + mass: T, +) -> [Complex; 4] +where + T: Real + RealLike + From + PartialOrd + Clone, +{ + let energy = momentum[0].clone(); + if energy.to_f64() < 0.0 { + return ext_massive_vector_generic(&negate_generic(momentum), -helicity, mass); + } + let px = momentum[1].clone(); + let py = momentum[2].clone(); + let pz = momentum[3].clone(); + let sqh = (energy.one() / energy.from_i64(2)).sqrt(); + let hel = energy.from_i64(helicity as i64); + let nsvahl = energy.from_i64(helicity.abs() as i64); + let pt2 = px.clone() * px.clone() + py.clone() * py.clone(); + let pp = t_min(&energy, &(pt2.clone() + pz.clone() * pz.clone()).sqrt()); + let pt = t_min(&pp, &pt2.sqrt()); + let hel0 = if helicity == 0 { + energy.one() + } else { + T::new_zero() + }; + if is_zero(&pp) { + return [ + complex_zero(), + c_generic(-hel.clone() * sqh.clone(), T::new_zero()), + c_generic(T::new_zero(), nsvahl.clone() * sqh.clone()), + c_generic(hel0, T::new_zero()), + ]; + } + let emp = energy.clone() / (mass.clone() * pp.clone()); + let wf0 = c_generic(hel0.clone() * pp.clone() / mass, T::new_zero()); + let wf3 = c_generic( + hel0.clone() * pz.clone() * emp.clone() + + hel.clone() * pt.clone() / pp.clone() * sqh.clone(), + T::new_zero(), + ); + let (wf1, wf2) = if !is_zero(&pt) { + let pzpt = pz.clone() / (pp.clone() * pt.clone()) * sqh.clone() * hel.clone(); + ( + c_generic( + hel0.clone() * px.clone() * emp.clone() - px.clone() * pzpt.clone(), + -nsvahl.clone() * py.clone() / pt.clone() * sqh.clone(), + ), + c_generic( + hel0 * py.clone() * emp - py.clone() * pzpt, + nsvahl * px.clone() / pt * sqh, + ), + ) + } else { + ( + c_generic(-hel * sqh.clone(), T::new_zero()), + c_generic(T::new_zero(), nsvahl * fortran_sign_generic(&sqh, &pz)), + ) + }; + [wf0, wf1, wf2, wf3] +} + +pub(super) fn spin2_outer( + left: &[Complex; 4], + right: &[Complex; 4], +) -> [Complex; 16] { + std::array::from_fn(|index| left[index / 4] * right[index % 4]) +} + +pub(super) fn ext_spin2( + momentum: [f64; 4], + helicity: i32, + mass: f64, +) -> RusticolResult<[Complex; 16]> { + if mass == 0.0 { + if ![-2, 2].contains(&helicity) { + return Err(RusticolError::invalid_argument( + "massless spin-2 sources only support helicities -2 and 2", + )); + } + let vector = ext_gluon(momentum, helicity / 2); + return Ok(spin2_outer(&vector, &vector)); + } + if ![-2, -1, 0, 1, 2].contains(&helicity) { + return Err(RusticolError::invalid_argument(format!( + "unsupported massive spin-2 helicity {helicity}" + ))); + } + let plus = ext_massive_vector(momentum, 1, mass); + let minus = ext_massive_vector(momentum, -1, mass); + let longitudinal = ext_massive_vector(momentum, 0, mass); + let plus_plus = spin2_outer(&plus, &plus); + let minus_minus = spin2_outer(&minus, &minus); + if helicity == 2 { + return Ok(plus_plus); + } + if helicity == -2 { + return Ok(minus_minus); + } + let inverse_sqrt_two = 1.0 / 2.0f64.sqrt(); + if helicity == 1 { + let first = spin2_outer(&plus, &longitudinal); + let second = spin2_outer(&longitudinal, &plus); + return Ok(std::array::from_fn(|index| { + (first[index] + second[index]) * inverse_sqrt_two + })); + } + if helicity == -1 { + let first = spin2_outer(&minus, &longitudinal); + let second = spin2_outer(&longitudinal, &minus); + return Ok(std::array::from_fn(|index| { + (first[index] + second[index]) * inverse_sqrt_two + })); + } + let plus_minus = spin2_outer(&plus, &minus); + let minus_plus = spin2_outer(&minus, &plus); + let zero_zero = spin2_outer(&longitudinal, &longitudinal); + let inverse_sqrt_six = 1.0 / 6.0f64.sqrt(); + Ok(std::array::from_fn(|index| { + (plus_minus[index] + minus_plus[index] + c64(2.0, 0.0) * zero_zero[index]) + * inverse_sqrt_six + })) +} + +#[cfg(feature = "symbolica-runtime")] +pub(super) fn spin2_outer_generic( + left: &[Complex; 4], + right: &[Complex; 4], +) -> [Complex; 16] +where + T: Real + RealLike + From + Clone, +{ + std::array::from_fn(|index| left[index / 4].clone() * right[index % 4].clone()) +} + +#[cfg(feature = "symbolica-runtime")] +pub(super) fn ext_spin2_generic( + momentum: &[T; 4], + helicity: i32, + mass: T, +) -> RusticolResult<[Complex; 16]> +where + T: Real + RealLike + From + PartialOrd + Clone, +{ + if is_zero(&mass) { + if ![-2, 2].contains(&helicity) { + return Err(RusticolError::invalid_argument( + "massless spin-2 sources only support helicities -2 and 2", + )); + } + let vector = ext_gluon_generic(momentum, helicity / 2); + return Ok(spin2_outer_generic(&vector, &vector)); + } + if ![-2, -1, 0, 1, 2].contains(&helicity) { + return Err(RusticolError::invalid_argument(format!( + "unsupported massive spin-2 helicity {helicity}" + ))); + } + let plus = ext_massive_vector_generic(momentum, 1, mass.clone()); + let minus = ext_massive_vector_generic(momentum, -1, mass.clone()); + let longitudinal = ext_massive_vector_generic(momentum, 0, mass.clone()); + if helicity == 2 { + return Ok(spin2_outer_generic(&plus, &plus)); + } + if helicity == -2 { + return Ok(spin2_outer_generic(&minus, &minus)); + } + let inverse_sqrt_two = mass.one() / mass.from_i64(2).sqrt(); + let weight_two = c_generic(inverse_sqrt_two, T::new_zero()); + if helicity == 1 { + let first = spin2_outer_generic(&plus, &longitudinal); + let second = spin2_outer_generic(&longitudinal, &plus); + return Ok(std::array::from_fn(|index| { + (first[index].clone() + second[index].clone()) * weight_two.clone() + })); + } + if helicity == -1 { + let first = spin2_outer_generic(&minus, &longitudinal); + let second = spin2_outer_generic(&longitudinal, &minus); + return Ok(std::array::from_fn(|index| { + (first[index].clone() + second[index].clone()) * weight_two.clone() + })); + } + let plus_minus = spin2_outer_generic(&plus, &minus); + let minus_plus = spin2_outer_generic(&minus, &plus); + let zero_zero = spin2_outer_generic(&longitudinal, &longitudinal); + let two = c_generic(mass.from_i64(2), T::new_zero()); + let inverse_sqrt_six = c_generic(mass.one() / mass.from_i64(6).sqrt(), T::new_zero()); + Ok(std::array::from_fn(|index| { + (plus_minus[index].clone() + + minus_plus[index].clone() + + two.clone() * zero_zero[index].clone()) + * inverse_sqrt_six.clone() + })) +} + +pub(super) fn ext_gluon(momentum: [f64; 4], helicity: i32) -> [Complex; 4] { + let [energy, px, py, pz] = momentum; + let sqh = 0.5f64.sqrt(); + if energy > 0.0 { + let hel = helicity as f64; + let pp = energy; + let pt = (px * px + py * py).sqrt(); + let wf3 = c64(hel * pt / pp * sqh, 0.0); + let (wf1, wf2) = if pt != 0.0 { + let pzpt = pz / (pp * pt) * sqh * hel; + ( + c64(-px * pzpt, -py / pt * sqh), + c64(-py * pzpt, px / pt * sqh), + ) + } else { + (c64(-hel * sqh, 0.0), c64(0.0, fortran_sign(sqh, pz))) + }; + return [c64(0.0, 0.0), wf1, wf2, wf3]; + } + let hel = -helicity as f64; + let pp = -energy; + let pt = (px * px + py * py).sqrt(); + let wf3 = c64(hel * pt / pp * sqh, 0.0); + let (wf1, wf2) = if pt != 0.0 { + let pzpt = -pz / (pp * pt) * sqh * hel; + ( + c64(px * pzpt, py / pt * sqh), + c64(py * pzpt, -px / pt * sqh), + ) + } else { + (c64(-hel * sqh, 0.0), c64(0.0, -fortran_sign(sqh, pz))) + }; + [c64(0.0, 0.0), wf1, wf2, wf3] +} + +#[cfg(feature = "symbolica-runtime")] +pub(super) fn ext_gluon_generic(momentum: &[T; 4], helicity: i32) -> [Complex; 4] +where + T: Real + RealLike + From + Clone, +{ + let energy = momentum[0].clone(); + let px = momentum[1].clone(); + let py = momentum[2].clone(); + let pz = momentum[3].clone(); + let sqh = (energy.one() / energy.from_i64(2)).sqrt(); + if energy.to_f64() > 0.0 { + let hel = energy.from_i64(helicity as i64); + let pp = energy; + let pt = (px.clone() * px.clone() + py.clone() * py.clone()).sqrt(); + let wf3 = c_generic( + hel.clone() * pt.clone() / pp.clone() * sqh.clone(), + T::new_zero(), + ); + let (wf1, wf2) = if !is_zero(&pt) { + let pzpt = pz.clone() / (pp.clone() * pt.clone()) * sqh.clone() * hel.clone(); + ( + c_generic( + -px.clone() * pzpt.clone(), + -py.clone() / pt.clone() * sqh.clone(), + ), + c_generic(-py.clone() * pzpt, px.clone() / pt * sqh), + ) + } else { + ( + c_generic(-hel * sqh.clone(), T::new_zero()), + c_generic(T::new_zero(), fortran_sign_generic(&sqh, &pz)), + ) + }; + return [complex_zero(), wf1, wf2, wf3]; + } + let hel = energy.from_i64(-(helicity as i64)); + let pp = -energy; + let pt = (px.clone() * px.clone() + py.clone() * py.clone()).sqrt(); + let wf3 = c_generic( + hel.clone() * pt.clone() / pp.clone() * sqh.clone(), + T::new_zero(), + ); + let (wf1, wf2) = if !is_zero(&pt) { + let pzpt = -pz.clone() / (pp.clone() * pt.clone()) * sqh.clone() * hel.clone(); + ( + c_generic( + px.clone() * pzpt.clone(), + py.clone() / pt.clone() * sqh.clone(), + ), + c_generic(py.clone() * pzpt, -px.clone() / pt * sqh), + ) + } else { + ( + c_generic(-hel * sqh.clone(), T::new_zero()), + c_generic(T::new_zero(), -fortran_sign_generic(&sqh, &pz)), + ) + }; + [complex_zero(), wf1, wf2, wf3] +} + +pub(super) fn ext_quark_weyl_array( + momentum: [f64; 4], + helicity: i32, + chirality: i32, +) -> [Complex; 2] { + let [energy, px, py, pz] = momentum; + if energy > 0.0 { + let sqp0p3 = if px == 0.0 && py == 0.0 && pz < 0.0 { + 0.0 + } else { + (energy + pz).max(0.0).sqrt() + }; + let chi1 = c64(sqp0p3, 0.0); + let chi2 = if sqp0p3 == 0.0 { + c64(-(helicity as f64) * (2.0 * energy).sqrt(), 0.0) + } else { + c64(helicity as f64 * px / sqp0p3, -py / sqp0p3) + }; + if helicity == 1 && chirality == 1 { + return [chi1, chi2]; + } + if helicity == -1 && chirality == -1 { + return [chi2, chi1]; + } + return [c64(0.0, 0.0), c64(0.0, 0.0)]; + } + let sqp0p3 = if px == 0.0 && py == 0.0 && pz > 0.0 { + 0.0 + } else { + -(-(energy + pz)).max(0.0).sqrt() + }; + let chi1 = c64(sqp0p3, 0.0); + let chi2 = if sqp0p3 == 0.0 { + c64(-(helicity as f64) * (2.0 * energy.abs()).sqrt(), 0.0) + } else { + c64(-(helicity as f64) * (-px) / sqp0p3, py / sqp0p3) + }; + if helicity == -1 && chirality == 1 { + [chi1, chi2] + } else if helicity == 1 && chirality == -1 { + [chi2, chi1] + } else { + [c64(0.0, 0.0), c64(0.0, 0.0)] + } +} + +#[cfg(feature = "symbolica-runtime")] +pub(super) fn ext_quark_weyl_generic( + momentum: &[T; 4], + helicity: i32, + chirality: i32, +) -> Vec> +where + T: Real + RealLike + From + PartialOrd + Clone, +{ + let energy = momentum[0].clone(); + let px = momentum[1].clone(); + let py = momentum[2].clone(); + let pz = momentum[3].clone(); + if energy.to_f64() > 0.0 { + let sqp0p3 = if is_zero(&px) && is_zero(&py) && pz.to_f64() < 0.0 { + T::new_zero() + } else { + t_max(&(energy.clone() + pz.clone()), &T::new_zero()).sqrt() + }; + let chi1 = c_generic(sqp0p3.clone(), T::new_zero()); + let chi2 = if is_zero(&sqp0p3) { + c_generic( + -energy.from_i64(helicity as i64) * (energy.from_i64(2) * energy.clone()).sqrt(), + T::new_zero(), + ) + } else { + c_generic( + energy.from_i64(helicity as i64) * px.clone() / sqp0p3.clone(), + -py.clone() / sqp0p3.clone(), + ) + }; + if helicity == 1 && chirality == 1 { + return vec![chi1, chi2]; + } + if helicity == -1 && chirality == -1 { + return vec![chi2, chi1]; + } + return vec![complex_zero(), complex_zero()]; + } + let sqp0p3 = if is_zero(&px) && is_zero(&py) && pz.to_f64() > 0.0 { + T::new_zero() + } else { + -t_max(&(-(energy.clone() + pz.clone())), &T::new_zero()).sqrt() + }; + let chi1 = c_generic(sqp0p3.clone(), T::new_zero()); + let chi2 = if is_zero(&sqp0p3) { + c_generic( + -energy.from_i64(helicity as i64) * (energy.from_i64(2) * energy.norm()).sqrt(), + T::new_zero(), + ) + } else { + c_generic( + -energy.from_i64(helicity as i64) * (-px.clone()) / sqp0p3.clone(), + py.clone() / sqp0p3.clone(), + ) + }; + if helicity == -1 && chirality == 1 { + vec![chi1, chi2] + } else if helicity == 1 && chirality == -1 { + vec![chi2, chi1] + } else { + vec![complex_zero(), complex_zero()] + } +} + +pub(super) fn ext_antiquark_weyl_array( + momentum: [f64; 4], + helicity: i32, + chirality: i32, +) -> [Complex; 2] { + let [energy, px, py, pz] = momentum; + if energy > 0.0 { + let sqp0p3 = if px == 0.0 && py == 0.0 && pz < 0.0 { + 0.0 + } else { + -(energy + pz).max(0.0).sqrt() + }; + let chi1 = c64(sqp0p3, 0.0); + let chi2 = if sqp0p3 == 0.0 { + c64(-(helicity as f64) * (2.0 * energy).sqrt(), 0.0) + } else { + c64(-(helicity as f64) * px / sqp0p3, py / sqp0p3) + }; + if helicity == 1 && chirality == 1 { + return [chi2, chi1]; + } + if helicity == -1 && chirality == -1 { + return [chi1, chi2]; + } + return [c64(0.0, 0.0), c64(0.0, 0.0)]; + } + let sqp0p3 = if px == 0.0 && py == 0.0 && pz > 0.0 { + 0.0 + } else { + (-(energy + pz)).max(0.0).sqrt() + }; + let chi1 = c64(sqp0p3, 0.0); + let chi2 = if sqp0p3 == 0.0 { + c64(-(helicity as f64) * (2.0 * energy.abs()).sqrt(), 0.0) + } else { + c64(helicity as f64 * (-px) / sqp0p3, -py / sqp0p3) + }; + if helicity == -1 && chirality == 1 { + [chi2, chi1] + } else if helicity == 1 && chirality == -1 { + [chi1, chi2] + } else { + [c64(0.0, 0.0), c64(0.0, 0.0)] + } +} + +#[cfg(feature = "symbolica-runtime")] +pub(super) fn ext_antiquark_weyl_generic( + momentum: &[T; 4], + helicity: i32, + chirality: i32, +) -> Vec> +where + T: Real + RealLike + From + PartialOrd + Clone, +{ + let energy = momentum[0].clone(); + let px = momentum[1].clone(); + let py = momentum[2].clone(); + let pz = momentum[3].clone(); + if energy.to_f64() > 0.0 { + let sqp0p3 = if is_zero(&px) && is_zero(&py) && pz.to_f64() < 0.0 { + T::new_zero() + } else { + -t_max(&(energy.clone() + pz.clone()), &T::new_zero()).sqrt() + }; + let chi1 = c_generic(sqp0p3.clone(), T::new_zero()); + let chi2 = if is_zero(&sqp0p3) { + c_generic( + -energy.from_i64(helicity as i64) * (energy.from_i64(2) * energy.clone()).sqrt(), + T::new_zero(), + ) + } else { + c_generic( + -energy.from_i64(helicity as i64) * px.clone() / sqp0p3.clone(), + py.clone() / sqp0p3.clone(), + ) + }; + if helicity == 1 && chirality == 1 { + return vec![chi2, chi1]; + } + if helicity == -1 && chirality == -1 { + return vec![chi1, chi2]; + } + return vec![complex_zero(), complex_zero()]; + } + let sqp0p3 = if is_zero(&px) && is_zero(&py) && pz.to_f64() > 0.0 { + T::new_zero() + } else { + t_max(&(-(energy.clone() + pz.clone())), &T::new_zero()).sqrt() + }; + let chi1 = c_generic(sqp0p3.clone(), T::new_zero()); + let chi2 = if is_zero(&sqp0p3) { + c_generic( + -energy.from_i64(helicity as i64) * (energy.from_i64(2) * energy.norm()).sqrt(), + T::new_zero(), + ) + } else { + c_generic( + energy.from_i64(helicity as i64) * (-px.clone()) / sqp0p3.clone(), + -py.clone() / sqp0p3.clone(), + ) + }; + if helicity == -1 && chirality == 1 { + vec![chi2, chi1] + } else if helicity == 1 && chirality == -1 { + vec![chi1, chi2] + } else { + vec![complex_zero(), complex_zero()] + } +} + +#[cfg(test)] +mod tests { + use super::{Complex, ext_massive_vector, negate}; + + fn assert_complex_close(left: Complex, right: Complex) { + assert!((left.re - right.re).abs() < 1.0e-13); + assert!((left.im - right.im).abs() < 1.0e-13); + } + + #[test] + fn massive_vector_negative_energy_obeys_crossing_convention() { + let momentum = [13.0, 4.0, 8.0, 8.0]; + for helicity in [-1, 0, 1] { + let outgoing = ext_massive_vector(momentum, helicity, 5.0); + let incoming = ext_massive_vector(negate(momentum), -helicity, 5.0); + for (left, right) in incoming.into_iter().zip(outgoing) { + assert_complex_close(left, right); + } + } + } + + #[test] + fn massive_vector_is_transverse_for_both_energy_signs() { + let momentum = [13.0, 4.0, 8.0, 8.0]; + for helicity in [-1, 0, 1] { + for (candidate, source_helicity) in + [(momentum, helicity), (negate(momentum), -helicity)] + { + let wave = ext_massive_vector(candidate, source_helicity, 5.0); + let contraction_re = candidate[0] * wave[0].re + - candidate[1] * wave[1].re + - candidate[2] * wave[2].re + - candidate[3] * wave[3].re; + let contraction_im = candidate[0] * wave[0].im + - candidate[1] * wave[1].im + - candidate[2] * wave[2].im + - candidate[3] * wave[3].im; + assert!( + contraction_re.hypot(contraction_im) < 1.0e-12, + "({contraction_re}, {contraction_im})" + ); + } + } + } +} diff --git a/pyamplicol/rust/crates/rusticol-core/tests/direct_arena_workspace_allocations.rs b/pyamplicol/rust/crates/rusticol-core/tests/direct_arena_workspace_allocations.rs new file mode 100644 index 0000000000..8a8eef79d6 --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/tests/direct_arena_workspace_allocations.rs @@ -0,0 +1,277 @@ +// SPDX-License-Identifier: 0BSD + +use std::alloc::{GlobalAlloc, Layout, System}; +use std::cell::Cell; +use std::hint::black_box; + +use rusticol_core::direct_arena::{ + DirectArenaTrafficCounters, DirectArenaView, DirectArenaWorkspace, DirectPlaneShape, + DirectPointTile, +}; + +thread_local! { + static TRACK_ALLOCATIONS: Cell = const { Cell::new(false) }; + static ALLOCATION_COUNT: Cell = const { Cell::new(0) }; + static ALLOCATED_BYTES: Cell = const { Cell::new(0) }; +} + +struct CountingAllocator; + +#[global_allocator] +static GLOBAL_ALLOCATOR: CountingAllocator = CountingAllocator; + +unsafe impl GlobalAlloc for CountingAllocator { + unsafe fn alloc(&self, layout: Layout) -> *mut u8 { + count_allocation(layout.size()); + unsafe { System.alloc(layout) } + } + + unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 { + count_allocation(layout.size()); + unsafe { System.alloc_zeroed(layout) } + } + + unsafe fn realloc(&self, pointer: *mut u8, layout: Layout, new_size: usize) -> *mut u8 { + count_allocation(new_size); + unsafe { System.realloc(pointer, layout, new_size) } + } + + unsafe fn dealloc(&self, pointer: *mut u8, layout: Layout) { + unsafe { System.dealloc(pointer, layout) } + } +} + +fn count_allocation(bytes: usize) { + if TRACK_ALLOCATIONS.try_with(Cell::get).unwrap_or(false) { + let _ = ALLOCATION_COUNT.try_with(|count| count.set(count.get() + 1)); + let _ = ALLOCATED_BYTES.try_with(|total| total.set(total.get().saturating_add(bytes))); + } +} + +fn count_allocations(operation: impl FnOnce() -> T) -> (T, usize, usize) { + ALLOCATION_COUNT.with(|count| count.set(0)); + ALLOCATED_BYTES.with(|total| total.set(0)); + TRACK_ALLOCATIONS.with(|tracking| tracking.set(true)); + let result = operation(); + TRACK_ALLOCATIONS.with(|tracking| tracking.set(false)); + ( + result, + ALLOCATION_COUNT.with(Cell::get), + ALLOCATED_BYTES.with(Cell::get), + ) +} + +fn write_split( + values_re: *mut f64, + values_im: *mut f64, + shape: DirectPlaneShape, + component: u32, + point: u32, + value_re: f64, + value_im: f64, +) { + let index = shape + .checked_scalar_index(component, point, "test split plane") + .unwrap(); + unsafe { + values_re.add(index).write(value_re); + values_im.add(index).write(value_im); + } +} + +fn read_split( + values_re: *const f64, + values_im: *const f64, + shape: DirectPlaneShape, + component: u32, + point: u32, +) -> (f64, f64) { + let index = shape + .checked_scalar_index(component, point, "test split plane") + .unwrap(); + unsafe { (values_re.add(index).read(), values_im.add(index).read()) } +} + +fn write_current(view: DirectArenaView, component: u32, point: u32, re: f64, im: f64) { + write_split( + view.current_re, + view.current_im, + view.current_shape().unwrap(), + component, + point, + re, + im, + ); +} + +fn read_current(view: DirectArenaView, component: u32, point: u32) -> (f64, f64) { + read_split( + view.current_re, + view.current_im, + view.current_shape().unwrap(), + component, + point, + ) +} + +fn write_amplitude(view: DirectArenaView, component: u32, point: u32, re: f64, im: f64) { + write_split( + view.amplitude_re, + view.amplitude_im, + view.amplitude_shape().unwrap(), + component, + point, + re, + im, + ); +} + +fn read_amplitude(view: DirectArenaView, component: u32, point: u32) -> (f64, f64) { + read_split( + view.amplitude_re, + view.amplitude_im, + view.amplitude_shape().unwrap(), + component, + point, + ) +} + +#[test] +fn every_physical_plane_base_is_cache_aligned_for_awkward_capacities() { + for (tile_capacity, expected_stride) in [(1, 8), (7, 8), (127, 128), (129, 136)] { + let mut workspace = DirectArenaWorkspace::new(3, 2, tile_capacity).unwrap(); + assert_eq!(workspace.tile_capacity(), tile_capacity); + assert_eq!(workspace.point_stride(), expected_stride); + workspace.begin_tile(tile_capacity).unwrap(); + let view = workspace.view().unwrap(); + for component in 0..3_usize { + let offset = component * expected_stride as usize; + assert_eq!(unsafe { view.current_re.add(offset) } as usize % 64, 0); + assert_eq!(unsafe { view.current_im.add(offset) } as usize % 64, 0); + } + for component in 0..2_usize { + let offset = component * expected_stride as usize; + assert_eq!(unsafe { view.amplitude_re.add(offset) } as usize % 64, 0); + assert_eq!(unsafe { view.amplitude_im.add(offset) } as usize % 64, 0); + } + } +} + +#[test] +fn begin_tile_preserves_storage_and_selective_clear_skips_inactive_tails() { + let mut workspace = DirectArenaWorkspace::new(3, 2, 129).unwrap(); + workspace.begin_tile(129).unwrap(); + let initial = workspace.view().unwrap(); + assert_eq!(initial.point_stride, 136); + for component in 0..3 { + for point in 0..initial.point_stride { + let value = 10_000.0 + f64::from(component * 1000 + point); + write_current(initial, component, point, value, -value); + } + } + for component in 0..2 { + for point in 0..initial.point_stride { + let value = 20_000.0 + f64::from(component * 1000 + point); + write_amplitude(initial, component, point, value, -value); + } + } + + workspace.begin_tile(127).unwrap(); + workspace.clear_current_active(1, 1).unwrap(); + workspace.clear_amplitude_active(0, 1).unwrap(); + let view = workspace.view().unwrap(); + assert_eq!(view.current_re, initial.current_re); + assert_eq!(view.amplitude_re, initial.amplitude_re); + assert_eq!(view.point_stride, initial.point_stride); + assert_eq!(read_current(view, 1, 126), (0.0, 0.0)); + assert_eq!(read_current(view, 1, 127), (11_127.0, -11_127.0)); + assert_eq!(read_current(view, 0, 0), (10_000.0, -10_000.0)); + assert_eq!(read_amplitude(view, 0, 126), (0.0, 0.0)); + assert_eq!(read_amplitude(view, 0, 127), (20_127.0, -20_127.0)); + assert_eq!(read_amplitude(view, 1, 0), (21_000.0, -21_000.0)); +} + +#[test] +fn adapter_uses_global_tile_offsets_but_local_workspace_indices() { + const POINT_COUNT: u32 = 1025; + const SENTINEL: f64 = -9_876_543.25; + let input_a = (0..POINT_COUNT) + .map(|point| f64::from(point) + 0.25) + .collect::>(); + let input_b = (0..POINT_COUNT) + .map(|point| f64::from(point) * -0.5 - 3.0) + .collect::>(); + let mut output = vec![SENTINEL; POINT_COUNT as usize + 2]; + let mut workspace = DirectArenaWorkspace::new(2, 1, 129).unwrap(); + let tiles = workspace.point_tiles(POINT_COUNT, 128).unwrap(); + let mut final_tile = None; + + for tile in tiles { + workspace.begin_tile(tile.point_count).unwrap(); + let view = workspace.view().unwrap(); + for local in 0..tile.point_count { + let global = tile.point_start + local; + write_current(view, 0, local, input_a[global as usize], 0.0); + write_current(view, 1, local, input_b[global as usize], 0.0); + } + for local in 0..tile.point_count { + let left = read_current(view, 0, local).0; + let right = read_current(view, 1, local).0; + write_amplitude(view, 0, local, left + 2.0 * right, 0.0); + output[(tile.point_start + local) as usize + 1] = read_amplitude(view, 0, local).0; + } + final_tile = Some(tile); + } + assert_eq!( + final_tile, + Some(DirectPointTile { + point_start: 1024, + point_count: 1, + }) + ); + assert_eq!(output[0], SENTINEL); + assert_eq!(output[POINT_COUNT as usize + 1], SENTINEL); + for point in 0..POINT_COUNT as usize { + assert_eq!(output[point + 1], input_a[point] + 2.0 * input_b[point]); + } +} + +#[test] +fn warmed_begin_view_selective_clear_and_tile_reuse_allocate_zero_bytes() { + let mut workspace = DirectArenaWorkspace::new(97, 13, 129).unwrap(); + workspace.begin_tile(129).unwrap(); + workspace.clear_current_active(0, 97).unwrap(); + workspace.clear_amplitude_active(0, 13).unwrap(); + black_box(workspace.view().unwrap()); + let allocation_counters = workspace.allocation_counters(); + let traffic_counters = DirectArenaTrafficCounters::default(); + + let (tail, allocation_count, allocated_bytes) = count_allocations(|| { + let mut tail = DirectPointTile { + point_start: 0, + point_count: 0, + }; + for points in [127, 129, 128, 1].into_iter().cycle().take(64) { + workspace.begin_tile(points).unwrap(); + workspace.clear_current_active(3, 71).unwrap(); + workspace.clear_amplitude_active(2, 7).unwrap(); + black_box(workspace.view().unwrap()); + for tile in workspace.point_tiles(1025, points).unwrap() { + tail = black_box(tile); + } + } + tail + }); + + assert_eq!( + tail, + DirectPointTile { + point_start: 1024, + point_count: 1, + } + ); + assert_eq!(allocation_count, 0, "warmed workspace path allocated"); + assert_eq!(allocated_bytes, 0, "warmed workspace path allocated bytes"); + assert_eq!(workspace.allocation_counters(), allocation_counters); + assert_eq!(traffic_counters, DirectArenaTrafficCounters::default()); +} diff --git a/pyamplicol/rust/crates/rusticol-core/tests/eager_runtime.rs b/pyamplicol/rust/crates/rusticol-core/tests/eager_runtime.rs new file mode 100644 index 0000000000..5662358a4b --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/tests/eager_runtime.rs @@ -0,0 +1,1364 @@ +// SPDX-License-Identifier: 0BSD + +use rusticol_core::{ + EagerAttachmentRow, EagerClosureRow, EagerComplex64, EagerCouplingRow, EagerDirectClosureSpec, + EagerExecutionPlan, EagerExecutionRuntime, EagerFinalizationRow, EagerInvocationRow, + EagerKernelBackend, EagerKernelCall, EagerKernelInput, EagerKernelRole, EagerKernelSpec, + EagerPlanDefinition, EagerPlanDimensions, EagerPlanPayloads, EagerReductionEntry, + EagerReductionGroup, EagerRuntimeOptions, EagerSelectorDomainIdRow, EagerSelectorDomainRow, + EagerSelectorGroupRow, EagerSelectorPayloads, EagerSelectorStagePayload, EagerStagePayload, + MISSING_U32, RusticolError, RusticolErrorKind, RusticolResult, +}; +use std::alloc::{GlobalAlloc, Layout, System}; +use std::cell::Cell; + +thread_local! { + static TRACK_ALLOCATIONS: Cell = const { Cell::new(false) }; + static ALLOCATION_COUNT: Cell = const { Cell::new(0) }; +} + +struct CountingAllocator; + +#[global_allocator] +static GLOBAL_ALLOCATOR: CountingAllocator = CountingAllocator; + +unsafe impl GlobalAlloc for CountingAllocator { + unsafe fn alloc(&self, layout: Layout) -> *mut u8 { + count_allocation(); + unsafe { System.alloc(layout) } + } + + unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 { + count_allocation(); + unsafe { System.alloc_zeroed(layout) } + } + + unsafe fn realloc(&self, pointer: *mut u8, layout: Layout, new_size: usize) -> *mut u8 { + count_allocation(); + unsafe { System.realloc(pointer, layout, new_size) } + } + + unsafe fn dealloc(&self, pointer: *mut u8, layout: Layout) { + unsafe { System.dealloc(pointer, layout) } + } +} + +fn count_allocation() { + let tracking = TRACK_ALLOCATIONS.try_with(Cell::get).unwrap_or(false); + if tracking { + let _ = ALLOCATION_COUNT.try_with(|count| count.set(count.get() + 1)); + } +} + +fn count_allocations(function: impl FnOnce() -> T) -> (T, usize) { + ALLOCATION_COUNT.with(|count| count.set(0)); + TRACK_ALLOCATIONS.with(|tracking| tracking.set(true)); + let result = function(); + TRACK_ALLOCATIONS.with(|tracking| tracking.set(false)); + let count = ALLOCATION_COUNT.with(Cell::get); + (result, count) +} + +#[derive(Default)] +struct MockBackend { + calls: [usize; 3], + max_lanes: [usize; 3], +} + +impl EagerKernelBackend for MockBackend { + fn evaluate_batch(&mut self, call: EagerKernelCall<'_>) -> RusticolResult<()> { + let kernel = usize::try_from(call.kernel_id) + .map_err(|_| RusticolError::evaluation("mock kernel id does not fit usize"))?; + if kernel >= self.calls.len() { + return Err(RusticolError::evaluation("unknown mock eager kernel")); + } + self.calls[kernel] += 1; + self.max_lanes[kernel] = self.max_lanes[kernel].max(call.lane_count); + match call.kernel_id { + 0 => { + assert_eq!(call.input_component_count, 7); + assert_eq!(call.output_component_count, 1); + for lane in 0..call.lane_count { + let row = lane * call.input_component_count; + let right = call.inputs[row]; + let left = call.inputs[row + 1]; + let left_momentum = call.inputs[row + 2]; + let right_momentum = call.inputs[row + 3]; + let coupling_real = call.inputs[row + 4]; + let coupling_imag = call.inputs[row + 5]; + let parameter = call.inputs[row + 6]; + call.outputs[lane] = (left + right + left_momentum + right_momentum) + * coupling_real + + coupling_imag + + parameter; + } + } + 1 => { + assert_eq!(call.input_component_count, 3); + assert_eq!(call.output_component_count, 1); + for lane in 0..call.lane_count { + let row = lane * call.input_component_count; + let momentum = call.inputs[row]; + let current = call.inputs[row + 1]; + let parameter = call.inputs[row + 2]; + call.outputs[lane] = current * momentum + parameter; + } + } + 2 => { + assert_eq!(call.input_component_count, 5); + assert_eq!(call.output_component_count, 1); + for lane in 0..call.lane_count { + let row = lane * call.input_component_count; + let coupling_imag = call.inputs[row]; + let right = call.inputs[row + 1]; + let coupling_real = call.inputs[row + 2]; + let left = call.inputs[row + 3]; + let parameter = call.inputs[row + 4]; + call.outputs[lane] = left * right * coupling_real + coupling_imag + parameter; + } + } + _ => unreachable!(), + } + Ok(()) + } +} + +fn c64(real: f64) -> EagerComplex64 { + EagerComplex64::new(real, 0.0) +} + +fn definition() -> EagerPlanDefinition { + EagerPlanDefinition { + dimensions: EagerPlanDimensions { + value_slot_component_counts: vec![1, 1, 1, 1, 1, 1], + momentum_slot_component_counts: vec![1], + current_component_counts: vec![1, 1], + parameter_count: 1, + amplitude_count: 2, + }, + kernels: vec![ + EagerKernelSpec { + kernel_id: 0, + role: EagerKernelRole::Vertex, + inputs: vec![ + EagerKernelInput::SecondCurrentComponent(0), + EagerKernelInput::FirstCurrentComponent(0), + EagerKernelInput::FirstMomentumComponent(0), + EagerKernelInput::SecondMomentumComponent(0), + EagerKernelInput::CouplingReal, + EagerKernelInput::CouplingImag, + EagerKernelInput::ModelParameter(0), + ], + output_component_count: 1, + homogeneous_linear_first_current: false, + independent_block_size: 1, + }, + EagerKernelSpec { + kernel_id: 1, + role: EagerKernelRole::Finalization, + inputs: vec![ + EagerKernelInput::FirstMomentumComponent(0), + EagerKernelInput::FirstCurrentComponent(0), + EagerKernelInput::ModelParameter(0), + ], + output_component_count: 1, + homogeneous_linear_first_current: false, + independent_block_size: 1, + }, + EagerKernelSpec { + kernel_id: 2, + role: EagerKernelRole::Closure, + inputs: vec![ + EagerKernelInput::CouplingImag, + EagerKernelInput::SecondCurrentComponent(0), + EagerKernelInput::CouplingReal, + EagerKernelInput::FirstCurrentComponent(0), + EagerKernelInput::ModelParameter(0), + ], + output_component_count: 1, + homogeneous_linear_first_current: false, + independent_block_size: 1, + }, + ], + direct_closures: vec![EagerDirectClosureSpec { + closure_index: 1, + coefficients: vec![c64(3.0)], + }], + reduction_groups: vec![ + EagerReductionGroup { + coherent_group_id: 10, + amplitude_indices: vec![0], + }, + EagerReductionGroup { + coherent_group_id: 20, + amplitude_indices: vec![1], + }, + ], + reduction_entries: vec![ + EagerReductionEntry { + left_group_index: 0, + right_group_index: 0, + coefficient: c64(1.0), + }, + EagerReductionEntry { + left_group_index: 1, + right_group_index: 1, + coefficient: c64(0.5), + }, + ], + } +} + +fn invocation_rows() -> Vec { + vec![ + EagerInvocationRow { + kernel_id: 0, + left_value_slot_id: 0, + right_value_slot_id: 1, + left_momentum_slot_id: 0, + right_momentum_slot_id: 0, + coupling_slot_id: 0, + output_factor_source: rusticol_core::EAGER_OUTPUT_FACTOR_NONE, + attachment_start: 0, + attachment_count: 1, + }, + EagerInvocationRow { + kernel_id: 0, + left_value_slot_id: 1, + right_value_slot_id: 0, + left_momentum_slot_id: 0, + right_momentum_slot_id: 0, + coupling_slot_id: 0, + output_factor_source: rusticol_core::EAGER_OUTPUT_FACTOR_NONE, + attachment_start: 1, + attachment_count: 1, + }, + ] +} + +fn attachment_rows() -> Vec { + vec![ + EagerAttachmentRow { + result_current_id: 0, + factor_real: 1.0, + factor_imag: 0.0, + }, + EagerAttachmentRow { + result_current_id: 1, + factor_real: -1.0, + factor_imag: 0.0, + }, + ] +} + +fn finalization_rows() -> Vec { + vec![ + EagerFinalizationRow { + kernel_id: 1, + current_id: 0, + unpropagated_value_slot_id: 2, + propagated_value_slot_id: 3, + momentum_slot_id: 0, + }, + EagerFinalizationRow { + kernel_id: 1, + current_id: 1, + unpropagated_value_slot_id: 4, + propagated_value_slot_id: 5, + momentum_slot_id: 0, + }, + ] +} + +fn closure_rows() -> Vec { + vec![ + EagerClosureRow { + kernel_id: 2, + left_value_slot_id: 3, + right_value_slot_id: 0, + amplitude_index: 0, + coupling_slot_id: 0, + output_factor_source: rusticol_core::EAGER_OUTPUT_FACTOR_NONE, + factor_real: 1.0, + factor_imag: 0.0, + }, + EagerClosureRow { + kernel_id: MISSING_U32, + left_value_slot_id: 4, + right_value_slot_id: 1, + amplitude_index: 1, + coupling_slot_id: MISSING_U32, + output_factor_source: rusticol_core::EAGER_OUTPUT_FACTOR_NONE, + factor_real: 2.0, + factor_imag: 0.0, + }, + ] +} + +fn build_plan_with( + definition: EagerPlanDefinition, + invocations: &[EagerInvocationRow], + attachments: &[EagerAttachmentRow], + finalizations: &[EagerFinalizationRow], + closures: &[EagerClosureRow], +) -> RusticolResult { + let coupling_bytes = EagerCouplingRow::encode_table(&[EagerCouplingRow { + real_parameter_id: 0, + imag_parameter_id: MISSING_U32, + constant_real: 99.0, + constant_imag: 0.5, + }])?; + let invocation_bytes = EagerInvocationRow::encode_table(invocations)?; + let attachment_bytes = EagerAttachmentRow::encode_table(attachments)?; + let finalization_bytes = EagerFinalizationRow::encode_table(finalizations)?; + let closure_bytes = EagerClosureRow::encode_table(closures)?; + let stage = EagerStagePayload { + stage_index: 1, + invocations: &invocation_bytes, + attachments: &attachment_bytes, + finalizations: &finalization_bytes, + }; + EagerExecutionPlan::from_payloads( + definition, + EagerPlanPayloads { + couplings: &coupling_bytes, + stages: &[stage], + closures: &closure_bytes, + selector_domains: None, + }, + ) +} + +fn build_runtime(options: EagerRuntimeOptions) -> EagerExecutionRuntime { + let plan = build_plan_with( + definition(), + &invocation_rows(), + &attachment_rows(), + &finalization_rows(), + &closure_rows(), + ) + .unwrap(); + EagerExecutionRuntime::new(plan, options).unwrap() +} + +#[derive(Default)] +struct BlockBackend { + calls_by_block_size: [usize; 5], +} + +impl EagerKernelBackend for BlockBackend { + fn evaluate_batch(&mut self, call: EagerKernelCall<'_>) -> RusticolResult<()> { + let block_size = call.independent_block_size as usize; + self.calls_by_block_size[block_size] += 1; + assert_eq!(call.input_component_count, 2 * block_size); + assert_eq!(call.output_component_count, block_size); + for row in 0..call.lane_count { + for lane in 0..block_size { + let input = row * call.input_component_count + lane * 2; + let output = row * call.output_component_count + lane; + call.outputs[output] = call.inputs[input] + call.inputs[input + 1]; + } + } + Ok(()) + } +} + +fn build_block_runtime(point_count: usize) -> EagerExecutionRuntime { + const INVOCATION_COUNT: usize = 5; + let definition = EagerPlanDefinition { + dimensions: EagerPlanDimensions { + value_slot_component_counts: vec![1; 2 + INVOCATION_COUNT], + momentum_slot_component_counts: vec![1], + current_component_counts: vec![1; INVOCATION_COUNT], + parameter_count: 0, + amplitude_count: INVOCATION_COUNT as u32, + }, + kernels: vec![EagerKernelSpec { + kernel_id: 0, + role: EagerKernelRole::Vertex, + inputs: vec![ + EagerKernelInput::FirstCurrentComponent(0), + EagerKernelInput::SecondCurrentComponent(0), + ], + output_component_count: 1, + homogeneous_linear_first_current: false, + independent_block_size: 4, + }], + direct_closures: (0..INVOCATION_COUNT) + .map(|index| EagerDirectClosureSpec { + closure_index: index as u32, + coefficients: vec![c64(1.0)], + }) + .collect(), + reduction_groups: (0..INVOCATION_COUNT) + .map(|index| EagerReductionGroup { + coherent_group_id: index as u32, + amplitude_indices: vec![index as u32], + }) + .collect(), + reduction_entries: (0..INVOCATION_COUNT) + .map(|index| EagerReductionEntry { + left_group_index: index as u32, + right_group_index: index as u32, + coefficient: c64(1.0), + }) + .collect(), + }; + let invocations = (0..INVOCATION_COUNT) + .map(|index| EagerInvocationRow { + kernel_id: 0, + left_value_slot_id: 0, + right_value_slot_id: 1, + left_momentum_slot_id: 0, + right_momentum_slot_id: 0, + coupling_slot_id: 0, + output_factor_source: rusticol_core::EAGER_OUTPUT_FACTOR_NONE, + attachment_start: index as u64, + attachment_count: 1, + }) + .collect::>(); + let attachments = (0..INVOCATION_COUNT) + .map(|index| EagerAttachmentRow { + result_current_id: index as u32, + factor_real: index as f64 + 1.0, + factor_imag: 0.0, + }) + .collect::>(); + let finalizations = (0..INVOCATION_COUNT) + .map(|index| EagerFinalizationRow { + kernel_id: MISSING_U32, + current_id: index as u32, + unpropagated_value_slot_id: (2 + index) as u32, + propagated_value_slot_id: MISSING_U32, + momentum_slot_id: 0, + }) + .collect::>(); + let closures = (0..INVOCATION_COUNT) + .map(|index| EagerClosureRow { + kernel_id: MISSING_U32, + left_value_slot_id: (2 + index) as u32, + right_value_slot_id: 0, + amplitude_index: index as u32, + coupling_slot_id: MISSING_U32, + output_factor_source: rusticol_core::EAGER_OUTPUT_FACTOR_NONE, + factor_real: 1.0, + factor_imag: 0.0, + }) + .collect::>(); + let coupling_bytes = EagerCouplingRow::encode_table(&[EagerCouplingRow { + real_parameter_id: MISSING_U32, + imag_parameter_id: MISSING_U32, + constant_real: 1.0, + constant_imag: 0.0, + }]) + .unwrap(); + let invocation_bytes = EagerInvocationRow::encode_table(&invocations).unwrap(); + let attachment_bytes = EagerAttachmentRow::encode_table(&attachments).unwrap(); + let finalization_bytes = EagerFinalizationRow::encode_table(&finalizations).unwrap(); + let closure_bytes = EagerClosureRow::encode_table(&closures).unwrap(); + let stage = EagerStagePayload { + stage_index: 1, + invocations: &invocation_bytes, + attachments: &attachment_bytes, + finalizations: &finalization_bytes, + }; + let plan = EagerExecutionPlan::from_payloads( + definition, + EagerPlanPayloads { + couplings: &coupling_bytes, + stages: &[stage], + closures: &closure_bytes, + selector_domains: None, + }, + ) + .unwrap(); + EagerExecutionRuntime::new( + plan, + EagerRuntimeOptions { + point_tile_size: point_count, + workspace_bytes: 64 * 1024, + }, + ) + .unwrap() +} + +fn selector_domain_id_bytes(values: &[u32]) -> Vec { + EagerSelectorDomainIdRow::encode_table( + &values + .iter() + .map(|domain_id| EagerSelectorDomainIdRow { + domain_id: *domain_id, + }) + .collect::>(), + ) + .unwrap() +} + +#[allow(clippy::too_many_arguments)] +fn build_selector_plan_with( + finalizations: &[EagerFinalizationRow], + domain_rows: &[EagerSelectorDomainRow], + group_rows: &[EagerSelectorGroupRow], + invocation_domain_ids: &[u32], + attachment_domain_ids: &[u32], + unpropagated_domain_ids: &[u32], + propagated_domain_ids: &[u32], + closure_domain_ids: &[u32], +) -> RusticolResult { + let coupling_bytes = EagerCouplingRow::encode_table(&[EagerCouplingRow { + real_parameter_id: 0, + imag_parameter_id: MISSING_U32, + constant_real: 99.0, + constant_imag: 0.5, + }])?; + let invocation_bytes = EagerInvocationRow::encode_table(&invocation_rows())?; + let attachment_bytes = EagerAttachmentRow::encode_table(&attachment_rows())?; + let finalization_bytes = EagerFinalizationRow::encode_table(finalizations)?; + let closure_bytes = EagerClosureRow::encode_table(&closure_rows())?; + let stage = EagerStagePayload { + stage_index: 1, + invocations: &invocation_bytes, + attachments: &attachment_bytes, + finalizations: &finalization_bytes, + }; + let domains = EagerSelectorDomainRow::encode_table(domain_rows)?; + let groups = EagerSelectorGroupRow::encode_table(group_rows)?; + let invocation_domains = selector_domain_id_bytes(invocation_domain_ids); + let attachment_domains = selector_domain_id_bytes(attachment_domain_ids); + let unpropagated_domains = selector_domain_id_bytes(unpropagated_domain_ids); + let propagated_domains = selector_domain_id_bytes(propagated_domain_ids); + let closure_domains = selector_domain_id_bytes(closure_domain_ids); + let selector_stage = EagerSelectorStagePayload { + stage_index: 1, + invocation_domains: &invocation_domains, + attachment_domains: &attachment_domains, + unpropagated_finalization_domains: &unpropagated_domains, + propagated_finalization_domains: &propagated_domains, + }; + let selector = EagerSelectorPayloads { + domains: &domains, + domain_group_ids: &groups, + stages: &[selector_stage], + closure_domains: &closure_domains, + }; + EagerExecutionPlan::from_payloads( + definition(), + EagerPlanPayloads { + couplings: &coupling_bytes, + stages: &[stage], + closures: &closure_bytes, + selector_domains: Some(selector), + }, + ) +} + +fn build_selector_runtime(options: EagerRuntimeOptions) -> EagerExecutionRuntime { + let domains = [ + EagerSelectorDomainRow { + member_start: 0, + member_count: 0, + }, + EagerSelectorDomainRow { + member_start: 0, + member_count: 1, + }, + EagerSelectorDomainRow { + member_start: 1, + member_count: 1, + }, + ]; + let groups = [ + EagerSelectorGroupRow { + coherent_group_id: 10, + }, + EagerSelectorGroupRow { + coherent_group_id: 20, + }, + ]; + let plan = build_selector_plan_with( + &finalization_rows(), + &domains, + &groups, + &[1, 2], + &[1, 2], + &[0, 2], + &[1, 0], + &[1, 2], + ) + .unwrap(); + EagerExecutionRuntime::new(plan, options).unwrap() +} + +fn inputs(point_count: usize) -> (Vec, Vec) { + let mut values = vec![c64(0.0); 6 * point_count]; + let mut momenta = vec![0.0; point_count]; + for point in 0..point_count { + values[point] = c64(point as f64 + 1.0); + values[point_count + point] = c64(10.0 + point as f64); + momenta[point] = 0.5 + point as f64 * 0.25; + } + (values, momenta) +} + +fn expected(point: usize, momenta: &[f64]) -> (EagerComplex64, EagerComplex64, f64) { + let left = point as f64 + 1.0; + let right = 10.0 + point as f64; + let momentum = momenta[point]; + let current = (left + right + 2.0 * momentum) * 2.0 + 0.5 + 2.0; + let propagated = current * momentum + 2.0; + let amplitude_0 = propagated * left * 2.0 + 0.5 + 2.0; + let amplitude_1 = -6.0 * current * right; + let reduced = amplitude_0 * amplitude_0 + 0.5 * amplitude_1 * amplitude_1; + (c64(amplitude_0), c64(amplitude_1), reduced) +} + +#[test] +fn executes_packetized_stages_finalization_closures_and_reduction() { + let point_count = 5; + let mut runtime = build_runtime(EagerRuntimeOptions { + point_tile_size: 2, + workspace_bytes: 4096, + }); + let (values, momenta) = inputs(point_count); + let mut amplitudes = vec![c64(0.0); 2 * point_count]; + let mut reduced = vec![0.0; point_count]; + let mut backend = MockBackend::default(); + + runtime + .evaluate_into( + &mut backend, + point_count, + &values, + &momenta, + &[c64(2.0)], + &mut amplitudes, + &mut reduced, + ) + .unwrap(); + + for point in 0..point_count { + let (amplitude_0, amplitude_1, expected_reduced) = expected(point, &momenta); + assert_eq!(amplitudes[point], amplitude_0); + assert_eq!(amplitudes[point_count + point], amplitude_1); + assert_eq!(reduced[point], expected_reduced); + } + assert_eq!(backend.calls, [3, 3, 3]); + assert_eq!(backend.max_lanes, [4, 4, 2]); + assert_eq!(runtime.effective_point_tile_size(), 2); + assert_eq!(runtime.plan().invocation_count(), 2); + assert_eq!(runtime.plan().attachment_count(), 2); + assert_eq!(runtime.plan().closure_count(), 2); + assert_eq!(runtime.plan().reduction_group_count(), 2); + assert_eq!(runtime.plan().reduction_entry_count(), 2); + assert_eq!(runtime.plan().stage_indices().collect::>(), [1]); +} + +#[test] +fn independent_block_variant_preserves_scalar_tail_and_allocates_nothing() { + const POINT_COUNT: usize = 3; + const INVOCATION_COUNT: usize = 5; + let mut runtime = build_block_runtime(POINT_COUNT); + let mut values = vec![c64(0.0); (2 + INVOCATION_COUNT) * POINT_COUNT]; + for point in 0..POINT_COUNT { + values[point] = c64(point as f64 + 1.0); + values[POINT_COUNT + point] = c64(10.0 + point as f64); + } + let momenta = vec![0.0; POINT_COUNT]; + let mut amplitudes = vec![c64(0.0); INVOCATION_COUNT * POINT_COUNT]; + let mut reduced = vec![0.0; POINT_COUNT]; + let mut backend = BlockBackend::default(); + + runtime + .evaluate_into( + &mut backend, + POINT_COUNT, + &values, + &momenta, + &[], + &mut amplitudes, + &mut reduced, + ) + .unwrap(); + assert_eq!(backend.calls_by_block_size[4], 1); + assert_eq!(backend.calls_by_block_size[1], 1); + for point in 0..POINT_COUNT { + let left = point as f64 + 1.0; + let right = 10.0 + point as f64; + let mut expected_reduced = 0.0; + for invocation in 0..INVOCATION_COUNT { + let expected = (left + right) * (invocation as f64 + 1.0) * left; + assert_eq!(amplitudes[invocation * POINT_COUNT + point], c64(expected)); + expected_reduced += expected * expected; + } + assert_eq!(reduced[point], expected_reduced); + } + + let (_, allocation_count) = count_allocations(|| { + runtime.evaluate_into( + &mut backend, + POINT_COUNT, + &values, + &momenta, + &[], + &mut amplitudes, + &mut reduced, + ) + }); + assert_eq!(allocation_count, 0); +} + +#[test] +fn reduces_coherent_amplitudes_through_compact_groups() { + let point_count = 3; + let mut definition = definition(); + definition.reduction_groups = vec![EagerReductionGroup { + coherent_group_id: 10, + amplitude_indices: vec![0, 1], + }]; + definition.reduction_entries = vec![EagerReductionEntry { + left_group_index: 0, + right_group_index: 0, + coefficient: c64(0.25), + }]; + let plan = build_plan_with( + definition, + &invocation_rows(), + &attachment_rows(), + &finalization_rows(), + &closure_rows(), + ) + .unwrap(); + let mut runtime = EagerExecutionRuntime::new( + plan, + EagerRuntimeOptions { + point_tile_size: 2, + workspace_bytes: 4096, + }, + ) + .unwrap(); + let (values, momenta) = inputs(point_count); + let mut amplitudes = vec![c64(0.0); 2 * point_count]; + let mut reduced = vec![0.0; point_count]; + runtime + .evaluate_into( + &mut MockBackend::default(), + point_count, + &values, + &momenta, + &[c64(2.0)], + &mut amplitudes, + &mut reduced, + ) + .unwrap(); + + for (point, actual) in reduced.iter().copied().enumerate() { + let (left, right, _) = expected(point, &momenta); + assert_eq!(actual, 0.25 * (left + right).norm_sqr()); + } +} + +#[test] +fn workspace_budget_reduces_tile_and_splits_packets() { + let runtime = build_runtime(EagerRuntimeOptions { + point_tile_size: 8, + workspace_bytes: 400, + }); + assert_eq!(runtime.effective_point_tile_size(), 1); + assert_eq!(runtime.packet_count(), 4); + assert!(runtime.workspace_bytes() <= 400); + + let plan = build_plan_with( + definition(), + &invocation_rows(), + &attachment_rows(), + &finalization_rows(), + &closure_rows(), + ) + .unwrap(); + let runtime = EagerExecutionRuntime::new( + plan, + EagerRuntimeOptions { + point_tile_size: 8, + workspace_bytes: 250, + }, + ) + .unwrap(); + assert_eq!(runtime.effective_point_tile_size(), 1); + assert!(runtime.workspace_bytes() > 250); +} + +#[test] +fn warmed_evaluation_performs_no_allocations() { + let point_count = 4; + let mut runtime = build_runtime(EagerRuntimeOptions { + point_tile_size: point_count, + workspace_bytes: 4096, + }); + let (values, momenta) = inputs(point_count); + let parameters = [c64(2.0)]; + let mut amplitudes = vec![c64(0.0); 2 * point_count]; + let mut reduced = vec![0.0; point_count]; + let mut backend = MockBackend::default(); + runtime + .evaluate_into( + &mut backend, + point_count, + &values, + &momenta, + ¶meters, + &mut amplitudes, + &mut reduced, + ) + .unwrap(); + + let (result, allocation_count) = count_allocations(|| { + runtime.evaluate_into( + &mut backend, + point_count, + &values, + &momenta, + ¶meters, + &mut amplitudes, + &mut reduced, + ) + }); + result.unwrap(); + assert_eq!(allocation_count, 0); +} + +#[test] +fn selector_domains_skip_unrelated_kernels_and_structural_zeros() { + let point_count = 5; + let mut runtime = build_selector_runtime(EagerRuntimeOptions { + point_tile_size: 2, + workspace_bytes: 4096, + }); + assert_eq!(runtime.selector_group_ids(), Some(vec![10, 20])); + let (values, momenta) = inputs(point_count); + let parameters = [c64(2.0)]; + let mut amplitudes = vec![c64(99.0); 2 * point_count]; + + let mut first_backend = MockBackend::default(); + runtime + .evaluate_selected_amplitudes_into( + &mut first_backend, + &[10], + point_count, + &values, + &momenta, + ¶meters, + &mut amplitudes, + ) + .unwrap(); + for point in 0..point_count { + let (left, _right, _) = expected(point, &momenta); + assert_eq!(amplitudes[point], left); + assert_eq!(amplitudes[point_count + point], c64(0.0)); + } + assert_eq!(first_backend.calls, [3, 3, 3]); + + let mut second_backend = MockBackend::default(); + runtime + .evaluate_selected_amplitudes_into( + &mut second_backend, + &[20], + point_count, + &values, + &momenta, + ¶meters, + &mut amplitudes, + ) + .unwrap(); + for point in 0..point_count { + let (_left, right, _) = expected(point, &momenta); + assert_eq!(amplitudes[point], c64(0.0)); + assert_eq!(amplitudes[point_count + point], right); + } + assert_eq!(second_backend.calls, [3, 0, 0]); + + let mut zero_backend = MockBackend::default(); + runtime + .evaluate_selected_amplitudes_into( + &mut zero_backend, + &[], + point_count, + &values, + &momenta, + ¶meters, + &mut amplitudes, + ) + .unwrap(); + assert!(amplitudes.iter().all(|value| *value == c64(0.0))); + assert_eq!(zero_backend.calls, [0, 0, 0]); +} + +#[test] +fn unknown_selected_coherent_groups_are_invalid_arguments() { + let mut runtime = build_selector_runtime(EagerRuntimeOptions { + point_tile_size: 2, + workspace_bytes: 4096, + }); + let (values, momenta) = inputs(1); + let mut amplitudes = vec![c64(0.0); 2]; + let error = runtime + .evaluate_selected_amplitudes_into( + &mut MockBackend::default(), + &[99], + 1, + &values, + &momenta, + &[c64(2.0)], + &mut amplitudes, + ) + .unwrap_err(); + assert_eq!(error.kind(), RusticolErrorKind::InvalidArgument); + assert!(error.to_string().contains("unknown coherent group 99")); +} + +#[test] +fn warmed_selected_evaluation_performs_no_allocations() { + let point_count = 4; + let mut runtime = build_selector_runtime(EagerRuntimeOptions { + point_tile_size: point_count, + workspace_bytes: 4096, + }); + let (values, momenta) = inputs(point_count); + let parameters = [c64(2.0)]; + let mut amplitudes = vec![c64(0.0); 2 * point_count]; + let mut backend = MockBackend::default(); + runtime + .evaluate_selected_amplitudes_into( + &mut backend, + &[10], + point_count, + &values, + &momenta, + ¶meters, + &mut amplitudes, + ) + .unwrap(); + + let (result, allocation_count) = count_allocations(|| { + runtime.evaluate_selected_amplitudes_into( + &mut backend, + &[10], + point_count, + &values, + &momenta, + ¶meters, + &mut amplitudes, + ) + }); + result.unwrap(); + assert_eq!(allocation_count, 0); +} + +fn selector_domain_fixture() -> ([EagerSelectorDomainRow; 3], [EagerSelectorGroupRow; 2]) { + ( + [ + EagerSelectorDomainRow { + member_start: 0, + member_count: 0, + }, + EagerSelectorDomainRow { + member_start: 0, + member_count: 1, + }, + EagerSelectorDomainRow { + member_start: 1, + member_count: 1, + }, + ], + [ + EagerSelectorGroupRow { + coherent_group_id: 10, + }, + EagerSelectorGroupRow { + coherent_group_id: 20, + }, + ], + ) +} + +#[test] +fn unknown_selector_domain_ids_are_artifact_errors() { + let (domains, groups) = selector_domain_fixture(); + let error = build_selector_plan_with( + &finalization_rows(), + &domains, + &groups, + &[3, 2], + &[1, 2], + &[0, 2], + &[1, 0], + &[1, 2], + ) + .unwrap_err(); + assert_eq!(error.kind(), RusticolErrorKind::Artifact); + assert!(error.to_string().contains("unknown domain 3")); +} + +#[test] +fn invocation_selector_domains_must_equal_attachment_unions() { + let (domains, groups) = selector_domain_fixture(); + let error = build_selector_plan_with( + &finalization_rows(), + &domains, + &groups, + &[2, 2], + &[1, 2], + &[0, 2], + &[1, 0], + &[1, 2], + ) + .unwrap_err(); + assert_eq!(error.kind(), RusticolErrorKind::Artifact); + assert!(error.to_string().contains("attachment union")); +} + +#[test] +fn selector_domain_members_must_be_sorted_and_unique() { + let domains = [ + EagerSelectorDomainRow { + member_start: 0, + member_count: 0, + }, + EagerSelectorDomainRow { + member_start: 0, + member_count: 2, + }, + ]; + let groups = [ + EagerSelectorGroupRow { + coherent_group_id: 20, + }, + EagerSelectorGroupRow { + coherent_group_id: 10, + }, + ]; + let error = build_selector_plan_with( + &finalization_rows(), + &domains, + &groups, + &[1, 1], + &[1, 1], + &[0, 1], + &[1, 0], + &[1, 1], + ) + .unwrap_err(); + assert_eq!(error.kind(), RusticolErrorKind::Artifact); + assert!(error.to_string().contains("sorted and unique")); +} + +#[test] +fn absent_finalization_outputs_require_empty_selector_domains() { + let (domains, groups) = selector_domain_fixture(); + let mut finalizations = finalization_rows(); + finalizations[0].unpropagated_value_slot_id = MISSING_U32; + let error = build_selector_plan_with( + &finalizations, + &domains, + &groups, + &[1, 2], + &[1, 2], + &[1, 2], + &[1, 0], + &[1, 2], + ) + .unwrap_err(); + assert_eq!(error.kind(), RusticolErrorKind::Artifact); + assert!(error.to_string().contains("nonempty selector domain")); +} + +#[test] +fn closure_selector_domains_must_match_amplitude_owners() { + let (domains, groups) = selector_domain_fixture(); + let error = build_selector_plan_with( + &finalization_rows(), + &domains, + &groups, + &[1, 2], + &[1, 2], + &[0, 2], + &[1, 0], + &[2, 1], + ) + .unwrap_err(); + assert_eq!(error.kind(), RusticolErrorKind::Artifact); + assert!(error.to_string().contains("eager closure 0")); + assert!(error.to_string().contains("proven dependency closure")); +} + +#[test] +fn attachment_domains_must_match_downstream_finalization_dependencies() { + let (domains, groups) = selector_domain_fixture(); + let error = build_selector_plan_with( + &finalization_rows(), + &domains, + &groups, + &[2, 2], + &[2, 2], + &[0, 2], + &[1, 0], + &[1, 2], + ) + .unwrap_err(); + assert_eq!(error.kind(), RusticolErrorKind::Artifact); + assert!(error.to_string().contains("attachment 0")); + assert!(error.to_string().contains("proven dependency closure")); +} + +#[test] +fn finalization_domains_must_follow_downstream_value_consumers() { + let (domains, groups) = selector_domain_fixture(); + let error = build_selector_plan_with( + &finalization_rows(), + &domains, + &groups, + &[1, 2], + &[1, 2], + &[0, 2], + &[0, 0], + &[1, 2], + ) + .unwrap_err(); + assert_eq!(error.kind(), RusticolErrorKind::Artifact); + assert!(error.to_string().contains("propagated finalization 0")); + assert!(error.to_string().contains("proven dependency closure")); +} + +#[test] +fn selector_domains_reject_unknown_coherent_groups() { + let (domains, mut groups) = selector_domain_fixture(); + groups[0].coherent_group_id = 99; + let error = build_selector_plan_with( + &finalization_rows(), + &domains, + &groups, + &[1, 2], + &[1, 2], + &[0, 2], + &[1, 0], + &[1, 2], + ) + .unwrap_err(); + assert_eq!(error.kind(), RusticolErrorKind::Artifact); + assert!(error.to_string().contains("unknown coherent group 99")); +} + +#[test] +fn malformed_attachment_ranges_are_artifact_errors() { + let mut invocations = invocation_rows(); + invocations[0].attachment_start = u64::MAX; + let error = build_plan_with( + definition(), + &invocations, + &attachment_rows(), + &finalization_rows(), + &closure_rows(), + ) + .unwrap_err(); + assert_eq!(error.kind(), RusticolErrorKind::Artifact); + assert!(error.to_string().contains("attachment")); +} + +#[test] +fn unknown_attachment_targets_are_artifact_errors() { + let mut attachments = attachment_rows(); + attachments[1].result_current_id = 99; + let error = build_plan_with( + definition(), + &invocation_rows(), + &attachments, + &finalization_rows(), + &closure_rows(), + ) + .unwrap_err(); + assert_eq!(error.kind(), RusticolErrorKind::Artifact); + assert!(error.to_string().contains("unknown slot")); +} + +#[test] +fn duplicate_finalization_is_an_artifact_error() { + let mut finalizations = finalization_rows(); + finalizations[1].current_id = 0; + let error = build_plan_with( + definition(), + &invocation_rows(), + &attachment_rows(), + &finalizations, + &closure_rows(), + ) + .unwrap_err(); + assert_eq!(error.kind(), RusticolErrorKind::Artifact); + assert!(error.to_string().contains("more than once")); +} + +#[test] +fn finalization_of_an_unwritten_current_is_an_artifact_error() { + let mut attachments = attachment_rows(); + attachments[1].result_current_id = 0; + let error = build_plan_with( + definition(), + &invocation_rows(), + &attachments, + &finalization_rows(), + &closure_rows(), + ) + .unwrap_err(); + assert_eq!(error.kind(), RusticolErrorKind::Artifact); + assert!(error.to_string().contains("finalizes unwritten current 1")); +} + +#[test] +fn kernel_roles_and_finalization_outputs_are_validated() { + let mut wrong_role = definition(); + wrong_role.kernels[0].role = EagerKernelRole::Closure; + let error = build_plan_with( + wrong_role, + &invocation_rows(), + &attachment_rows(), + &finalization_rows(), + &closure_rows(), + ) + .unwrap_err(); + assert_eq!(error.kind(), RusticolErrorKind::Artifact); + assert!(error.to_string().contains("expected Vertex")); + + let mut aliased = finalization_rows(); + aliased[0].propagated_value_slot_id = aliased[0].unpropagated_value_slot_id; + let error = build_plan_with( + definition(), + &invocation_rows(), + &attachment_rows(), + &aliased, + &closure_rows(), + ) + .unwrap_err(); + assert_eq!(error.kind(), RusticolErrorKind::Artifact); + assert!(error.to_string().contains("aliases")); +} + +#[test] +fn direct_closure_requires_exact_coefficients() { + let mut missing = definition(); + missing.direct_closures.clear(); + let error = build_plan_with( + missing, + &invocation_rows(), + &attachment_rows(), + &finalization_rows(), + &closure_rows(), + ) + .unwrap_err(); + assert_eq!(error.kind(), RusticolErrorKind::Artifact); + assert!(error.to_string().contains("lacks contraction coefficients")); + + let mut wrong_width = definition(); + wrong_width.direct_closures[0].coefficients.push(c64(1.0)); + let error = build_plan_with( + wrong_width, + &invocation_rows(), + &attachment_rows(), + &finalization_rows(), + &closure_rows(), + ) + .unwrap_err(); + assert_eq!(error.kind(), RusticolErrorKind::Artifact); + assert!(error.to_string().contains("component widths")); +} + +#[test] +fn kernel_input_components_are_validated_against_invocation_slots() { + let mut definition = definition(); + definition.kernels[0].inputs[0] = EagerKernelInput::SecondCurrentComponent(1); + let error = build_plan_with( + definition, + &invocation_rows(), + &attachment_rows(), + &finalization_rows(), + &closure_rows(), + ) + .unwrap_err(); + assert_eq!(error.kind(), RusticolErrorKind::Artifact); + assert!(error.to_string().contains("SecondCurrentComponent(1)")); + assert!(error.to_string().contains("outside 0..1")); +} + +#[test] +fn kernel_input_roles_reject_unavailable_sources() { + let mut definition = definition(); + definition.kernels[1] + .inputs + .push(EagerKernelInput::CouplingReal); + let error = build_plan_with( + definition, + &invocation_rows(), + &attachment_rows(), + &finalization_rows(), + &closure_rows(), + ) + .unwrap_err(); + assert_eq!(error.kind(), RusticolErrorKind::Artifact); + assert!(error.to_string().contains("Finalization")); + assert!(error.to_string().contains("CouplingReal")); +} + +#[test] +fn kernel_input_descriptors_are_unique_and_parameter_bounded() { + let mut duplicate = definition(); + duplicate.kernels[2] + .inputs + .push(EagerKernelInput::CouplingImag); + let error = build_plan_with( + duplicate, + &invocation_rows(), + &attachment_rows(), + &finalization_rows(), + &closure_rows(), + ) + .unwrap_err(); + assert_eq!(error.kind(), RusticolErrorKind::Artifact); + assert!(error.to_string().contains("repeats input descriptor")); + + let mut out_of_bounds = definition(); + out_of_bounds.kernels[0] + .inputs + .push(EagerKernelInput::ModelParameter(1)); + let error = build_plan_with( + out_of_bounds, + &invocation_rows(), + &attachment_rows(), + &finalization_rows(), + &closure_rows(), + ) + .unwrap_err(); + assert_eq!(error.kind(), RusticolErrorKind::Artifact); + assert!(error.to_string().contains("model parameter")); + assert!(error.to_string().contains("outside 0..1")); +} + +#[test] +fn invalid_execution_buffers_return_errors_without_panicking() { + let mut runtime = build_runtime(EagerRuntimeOptions { + point_tile_size: 2, + workspace_bytes: 4096, + }); + let mut backend = MockBackend::default(); + let mut amplitudes = vec![c64(0.0); 2]; + let mut reduced = vec![0.0]; + let error = runtime + .evaluate_into( + &mut backend, + 1, + &[], + &[1.0], + &[c64(2.0)], + &mut amplitudes, + &mut reduced, + ) + .unwrap_err(); + assert_eq!(error.kind(), RusticolErrorKind::InvalidArgument); + assert!(error.to_string().contains("initial value buffer")); +} diff --git a/pyamplicol/rust/crates/rusticol-core/tests/fixtures/on_the_fly_query_parity_v1.json b/pyamplicol/rust/crates/rusticol-core/tests/fixtures/on_the_fly_query_parity_v1.json new file mode 100644 index 0000000000..42f131c7a8 --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/tests/fixtures/on_the_fly_query_parity_v1.json @@ -0,0 +1,304 @@ +{ + "schema": "pyamplicol-on-the-fly-query-parity-fixture-v1", + "provenance": { + "architecture_base_commit": "f986acf61e6807b74453b5ad7e7793b73082ee30", + "selected_generic_dag_oracle_commit": "2312b1137c7e2f3d6bdf9064a902592c16bac58c", + "ownership_oracle_commit": "cf8de3820152be262bafc6f93e5af9a6f423b271", + "scaling_oracle_commit": "dc171ac357c2817465f20baf3ff0efa91086b53a", + "numerical_authority": "retained authenticated recurrence runtime plus selected-GenericDAG p32 interpreter", + "digest_contract": "sha256 of the sorted canonical-JSON selected-GenericDAG semantic row multiset" + }, + "massless_2_to_3_point": { + "sha256": "3be3d02aa9d9932332e461a7dfd874cd97e0305f7a148119dc5a5acc760aa84b", + "momenta": [ + [500.0, 0.0, 0.0, 500.0], + [500.0, 0.0, 0.0, -500.0], + [333.3333333333333, 333.3333333333333, 0.0, 0.0], + [333.3333333333333, -166.66666666666666, 288.67513459481285, 0.0], + [333.3333333333333, -166.66666666666666, -288.67513459481285, 0.0] + ] + }, + "selected_queries": [ + { + "name": "scalar_contact", + "process": "h h > h h", + "coupling_limits": {"QCD": 0, "QED": 2}, + "flow_rank": "0", + "flow_id": "flow:singlet", + "helicity_rank": "0", + "helicity_id": "h:+0,+0,+0,+0", + "current_count": 11, + "contribution_count": 12, + "closure_count": 1, + "contribution_multiset_sha256": "871b91602e6883c887a89a979578d41ee05f9d7ff06c643eb57084bffbc84213", + "closure_multiset_sha256": "b440504ec358c7c49df1c0b782dc716d0d4af40d7a7394beaeb850f5634c81a4" + }, + { + "name": "pure_gluon_n5", + "process": "g g > g g g", + "coupling_limits": {"QCD": 3, "QED": 0}, + "flow_rank": "0", + "flow_id": "flow:1,2,3,4,5", + "helicity_rank": "0", + "helicity_id": "h:-1,-1,-1,-1,-1", + "current_count": 16, + "contribution_count": 25, + "closure_count": 1, + "contribution_multiset_sha256": "34926d54a299c6610de4c76384b6f8c4b7cbc8f076063d5a484f5a6822168102", + "closure_multiset_sha256": "345db39a6f134b4870c5207573630e299d61208ecd2ac9c9aef766a2044b06a3", + "selected_generic_dag_p32": "0.00004178069796309911254363311176", + "retained_recurrence_p32": "0.000041780697963098513001361030334541", + "retained_recurrence_f64_bits": "0x3f05e7b5d9d0f228", + "maximum_relative_residual": "2e-13" + }, + { + "name": "one_open_line_n5", + "process": "u u~ > g g g", + "coupling_limits": {"QCD": 3, "QED": 0}, + "flow_rank": "0", + "flow_id": "flow:2,3,4,5,1", + "helicity_rank": "9", + "helicity_id": "h:-1,+1,-1,-1,+1", + "current_count": 13, + "contribution_count": 14, + "closure_count": 1, + "contribution_multiset_sha256": "55fda24e1598a7125fdbda55b1e826f41ddcc0d991c25f844bc18159fcdbf5f5", + "closure_multiset_sha256": "2d29f0645b17d6bd55bfb2d0e44bbb503b2190f3bfb02e93f45fb2e1aa8e4024", + "selected_generic_dag_p32": "0.000001222663314878072887698934878", + "retained_recurrence_p32": "0.0000012226633148780577574738566420466", + "retained_recurrence_f64_bits": "0x3eb4834c88148d10", + "maximum_relative_residual": "2e-13" + }, + { + "name": "three_distinct_open_lines_n6", + "process": "d d~ > u u~ s s~", + "coupling_limits": {"QCD": 4, "QED": 0}, + "flow_rank": "0", + "flow_id": "flow:2,1,3,4,5,6", + "helicity_rank": "21", + "helicity_id": "h:-1,+1,-1,+1,-1,+1", + "current_count": 18, + "contribution_count": 16, + "closure_count": 2, + "contribution_multiset_sha256": "effc6a982369541b23bd804b3f293b914b26e3fa6e7396675452249a43f6162b", + "closure_multiset_sha256": "415aab2f7875fab2760cd1b959696c708dd69ea864a88de1f660dcedee2957e0", + "owner_multiset_sha256": "1b8a2d2fbdbd3b10303f0d7684c85db815fd91bc47c1dfa8ffb8ef18169234a5" + }, + { + "name": "three_identical_open_lines_n6", + "process": "d d~ > u u~ u u~", + "coupling_limits": {"QCD": 4, "QED": 0}, + "flow_rank": "0", + "flow_id": "flow:2,1,3,4,5,6", + "helicity_rank": "19", + "helicity_id": "h:-1,+1,-1,-1,+1,+1", + "current_count": 15, + "contribution_count": 11, + "closure_count": 2, + "contribution_multiset_sha256": "ea86aa72b8d08672672cfccecb8ddcb7fa7057338a344378a91eec47837c7cab", + "closure_multiset_sha256": "02bb77b2b2fd5884ab9e5db033e02c5bc8a38b7d30964094cad4993bb839b13a", + "owner_multiset_sha256": "e6cc180248ad8a3f39196a726aba76018b1ffb3dc01bb18e55b9d9eefcad58ab" + }, + { + "name": "three_identical_open_lines_exchange_n6", + "process": "d d~ > u u~ u u~", + "coupling_limits": { + "QCD": 4, + "QED": 0 + }, + "flow_rank": "2", + "flow_id": "flow:2,4,3,1,5,6", + "helicity_rank": "19", + "helicity_id": "h:-1,+1,-1,-1,+1,+1", + "current_count": 13, + "contribution_count": 10, + "closure_count": 1, + "contribution_multiset_sha256": "30082ef8c2d305d1ddc2370b28ff041e2d7cf22b2ab7419d41f667f3b4762c63", + "closure_multiset_sha256": "3be9bd544b4aa157bc08c597bb2f9fbcd43e0e3c649d75921bcdc01b38071ebe", + "owner_multiset_sha256": "6ccdfc73ac4efdf43f0c6d7556b6fe7e882be02567bc328b3f1d0d30865f0fb5", + "negative_evaluation_factor_count": 1, + "negative_evaluation_factor_multiset_sha256": "427fbcf7b8fc983357aa0106fef6f01f98493ba24354f4dde20403d6df928739" + } + ], + "reduction_contracts": [ + { + "process": "h h > h h", + "color_factor": 1, + "average_factor": 4, + "identical_factor": 2, + "qcd_coupling_power": 0, + "electroweak_coupling_power": 4, + "global_coupling_factor": "0.0012942082163584319" + }, + { + "process": "g g > g g g", + "color_factor": 243, + "average_factor": 256, + "identical_factor": 6, + "qcd_coupling_power": 3, + "electroweak_coupling_power": 0, + "global_coupling_factor": "3.260435506341527" + }, + { + "process": "u u~ > g g g", + "color_factor": 81, + "average_factor": 36, + "identical_factor": 6, + "qcd_coupling_power": 3, + "electroweak_coupling_power": 0, + "global_coupling_factor": "3.260435506341527" + }, + { + "process": "d d~ > u u~ s s~", + "color_factor": 27, + "average_factor": 36, + "identical_factor": 1, + "qcd_coupling_power": 4, + "electroweak_coupling_power": 0, + "global_coupling_factor": "4.834677230554605" + }, + { + "process": "d d~ > u u~ u u~", + "color_factor": 27, + "average_factor": 36, + "identical_factor": 4, + "qcd_coupling_power": 4, + "electroweak_coupling_power": 0, + "global_coupling_factor": "4.834677230554605" + } + ], + "small_domain_exhaustive_oracles": [ + { + "process": "g g > g g", + "query_count": 96, + "structural_zero_count": 0, + "maximum_current_count": 9, + "maximum_contribution_count": 8, + "maximum_closure_count": 1, + "aggregate_sha256": "6281e91a1ddf5913bd4e8658d56b6cbb8bbb668dab4d5d727786bbab53034376" + }, + { + "process": "u u~ > g g", + "query_count": 32, + "structural_zero_count": 16, + "maximum_current_count": 7, + "maximum_contribution_count": 4, + "maximum_closure_count": 1, + "aggregate_sha256": "c2f1c25205461cb52409e51e42ef31597bbc354ef432955245b7ed4386d297b1" + }, + { + "process": "d d~ > u u~ s s~", + "query_count": 384, + "structural_zero_count": 336, + "maximum_current_count": 18, + "maximum_contribution_count": 16, + "maximum_closure_count": 2, + "aggregate_sha256": "93ff051e1380d329572f459e48b3d13ac691d32da0532fcb8c65651d57c978a9" + }, + { + "process": "d d~ > u u~ u u~", + "query_count": 384, + "structural_zero_count": 312, + "maximum_current_count": 20, + "maximum_contribution_count": 22, + "maximum_closure_count": 2, + "aggregate_sha256": "80ae8c05d44d0922f03b43e74c9fa065fe1380b4160ef40619365fd16cd0a258" + } + ], + "structural_scale_queries": [ + { + "name": "pure_gluon_n7_first", + "process": "g g > g g g g g", + "flow_cardinality": "720", + "helicity_cardinality": "128", + "flow_rank": "0", + "helicity_rank": "0", + "current_count": 36, + "contribution_count": 105, + "closure_count": 1, + "peak_query_rows": 141, + "contribution_multiset_sha256": "db65f1f25b63d9748295aaa4f756a49ea46c60aad64fcba18cb306193637185b", + "closure_multiset_sha256": "f4ef144f5450601956c0fcc51afce5f698918bb6746251d12e830834eb7eda11" + }, + { + "name": "pure_gluon_n8_first", + "process": "g g > g g g g g g", + "flow_cardinality": "5040", + "helicity_cardinality": "256", + "flow_rank": "0", + "helicity_rank": "0", + "current_count": 49, + "contribution_count": 176, + "closure_count": 1, + "peak_query_rows": 225, + "contribution_multiset_sha256": "79c27773475fa4543c10df44e3371f7abbd2a27f340196e4275a403fc13aba4a", + "closure_multiset_sha256": "e68bb34e7da958e3e45e9194c6691eef4a36d399a11d77da8c303411f5f54c85" + }, + { + "name": "pure_gluon_n10_first", + "process": "g g > g g g g g g g g", + "flow_cardinality": "362880", + "helicity_cardinality": "1024", + "flow_rank": "0", + "helicity_rank": "0", + "current_count": 81, + "contribution_count": 400, + "closure_count": 1, + "peak_query_rows": 481, + "contribution_multiset_sha256": "d5fb390f78bb4050a03383b0198273ff56f10810d513cb28aa20153d4ff0d356", + "closure_multiset_sha256": "8a26d3b1e89796fe640a064d4d62aa6653149ed121b0ef380001dc6526197f18" + }, + { + "name": "one_open_line_n7_first_nonzero", + "process": "u u~ > g g g g g", + "flow_cardinality": "120", + "helicity_cardinality": "128", + "flow_rank": "0", + "helicity_rank": "33", + "current_count": 31, + "contribution_count": 71, + "closure_count": 1, + "peak_query_rows": 102, + "contribution_multiset_sha256": "49644bb28ced5658e80a2a5262d8ce404d4b0fd3ccac4aaf8fef2c870b4036f6", + "closure_multiset_sha256": "4665272ed07d193d107400aaf17812f7c6ed861017468360b5d5d12b49c23d7a" + }, + { + "name": "one_open_line_n8_first_nonzero", + "process": "u u~ > g g g g g g", + "flow_cardinality": "720", + "helicity_cardinality": "256", + "flow_rank": "0", + "helicity_rank": "65", + "current_count": 43, + "contribution_count": 126, + "closure_count": 1, + "peak_query_rows": 169, + "contribution_multiset_sha256": "b51c1dc874e9fea6feaffdfe941c0aef9b982e53dc337726776201481986b8c0", + "closure_multiset_sha256": "8b996826605c4f10387d838e0256bb03455f13fa65d0fc97966bdd0dbb5b5dc3" + }, + { + "name": "one_open_line_n10_first_nonzero", + "process": "u u~ > g g g g g g g g", + "flow_cardinality": "40320", + "helicity_cardinality": "1024", + "flow_rank": "0", + "helicity_rank": "257", + "current_count": 73, + "contribution_count": 309, + "closure_count": 1, + "peak_query_rows": 382, + "contribution_multiset_sha256": "adf783e0ddfabbbd3e59befd84a938c5f79dff7aff8d68fb615338909d525202", + "closure_multiset_sha256": "ff80c07b33dd9675d2c6d5606e52a23db367403cc04da387e8fcf3ea6ffc47ad" + } + ], + "forbidden_global_boundaries": [ + "build_color_plan", + "Generator._prepare_process_construction", + "project_recurrence_process_v1", + "build_recurrence_builder_input_v1", + "build_recurrence_physics", + "build_replay_targets", + "finish_program", + "DirectRecurrencePlan" + ] +} diff --git a/pyamplicol/rust/crates/rusticol-core/tests/fixtures/recurrence_execution_hzz_full_v2.json b/pyamplicol/rust/crates/rusticol-core/tests/fixtures/recurrence_execution_hzz_full_v2.json new file mode 100644 index 0000000000..17cb075afc --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/tests/fixtures/recurrence_execution_hzz_full_v2.json @@ -0,0 +1 @@ +{"builder_input_abi":"pyamplicol-recurrence-builder-input-v2","color_accuracy":"full","direct_backend_abi":"rusticol.recurrence-direct-backend.v1","direct_template_abi":"pyamplicol-recurrence-direct-template-v1","direct_template_catalog_digest":"1b402620f9d8c1fadc6be4bc43d650483d46455b2ec14e6579c00296fab99809","external_pdg_order":[1,-1,23,23,23],"kernel_pack":{"manifest_path":"model/eager-kernel-pack.json","payload_root":"model/eager-kernels"},"key":"d_dbar_to_z_z_z","kind":"pyamplicol-runtime-recurrence-execution","plan":{"builder_input_abi":"pyamplicol-recurrence-builder-input-v2","builder_input_sha256":"65048ae46f9172f870675aa1eb87675724ab18430889af33b7074c14af888c10","direct_backend_abi":"rusticol.recurrence-direct-backend.v1","direct_template_abi":"pyamplicol-recurrence-direct-template-v1","direct_template_catalog_digest":"1b402620f9d8c1fadc6be4bc43d650483d46455b2ec14e6579c00296fab99809","inspection_summary":{"construction":{"candidate_parent_pair_count":2706,"color_target_prune_count":0,"peak_contribution_count":450,"peak_current_count":193,"peak_dynamic_color_state_count":3,"peak_to_final_contribution_ratio":1.0,"peak_to_final_current_ratio":1.0},"direct_arena":{"arena_component_reuse_count":62,"cache_footprint_policy":"persisted-arena-v1","current_arena_components":369,"momentum_form_count":16,"packed_input_bytes":0,"packed_output_bytes":0,"row_group_count":23,"scatter_bytes":0,"selector_domain_count":1,"semantic_component_count":431},"direct_executor_count":633,"direct_template_abi":"pyamplicol-recurrence-direct-template-v1","execution_mode":"recurrence","generation_timings_seconds":{"catalog_authentication":0.043677792,"direct_lowering":0.073076834,"native_total":0.163294333,"python_extraction":0.00234625,"semantic_construction":0.005414292,"serialization":0.018141},"lc_flow_layout":"contracted-color-union","parameter_count":10,"prepared_kernel_count":49,"process_id":"d_dbar_to_z_z_z","recurrence_plan_abi":"pyamplicol-recurrence-plan-v2","runtime_container_member":{"container_size_bytes":70408,"path":"schedule/recurrence-direct-schedule-v2.bin","sha256":"f44237c7a9cb769845e09e57e7e328378a406405ff10119accbbc69f689e03bd","size_bytes":70120},"runtime_layout_abi":"pyamplicol-recurrence-runtime-layout-v2","runtime_layout_digest":"3e1c95714566f6b8f3b5075985e63421acef8aadcbd0e1201b988cd744eba442","schedule":{"amplitude_destination_count":54,"closure_term_count":54,"contribution_count":450,"current_count":193,"exact_factor_count":6,"finalization_count":126,"replay_target_count":1,"resolved_helicity_count":54,"retained_helicity_count":108,"source_row_count":13},"schedule_digest":"2474ae81e1e9b2027db747967d1322398b02a79e230a366376b5b5a552c366d7","sector_count":1,"selector_work_certificate":{"binding":"runtime_layout_digest","persisted_union":{"closure_count":54,"contribution_count":450,"current_count":193,"finalization_count":126,"row_count":643,"semantic_component_count":431,"source_row_count":13},"representatives":[],"schema_version":1},"semantic_digest":"65048ae46f9172f870675aa1eb87675724ab18430889af33b7074c14af888c10"},"kind":"pyamplicol-runtime-recurrence-execution","prepared_kernel_pack_digest":"ddaa7c31f2361bda246b1310c8dca2e800a7237afd270249f86d5b549b557868","process_binding":{"abi":"pyamplicol-recurrence-process-binding-v2","path":"recurrence-binding.bin","process_id":"d_dbar_to_z_z_z","process_semantic_digest":"65048ae46f9172f870675aa1eb87675724ab18430889af33b7074c14af888c10","process_support_words":[1],"remap":{"bijection_digest":"7077a7b59abb4f7bdb3b99d5618068255325c57dde6c3489e82086c795234688","direct_executors":{"changes":[],"count":633},"parameter_slots":{"changes":[],"count":10},"physical_sector_ids":[0],"public_flow_ids":[0],"source_helicity_signs":[1,1,1,1,1],"source_momentum_signs":[1,1,1,1,1],"source_slots":[0,1,2,3,4],"source_state_indices":[0,1,0,1,0,1,2,0,1,2,0,1,2],"source_state_offsets":[0,2,4,7,10,13],"source_templates":{"changes":[],"count":62},"state_templates":{"changes":[],"count":82}},"schedule_digest":"2474ae81e1e9b2027db747967d1322398b02a79e230a366376b5b5a552c366d7","sha256":"9c25bcb1d2f12f12d0a4b877fa568dd16da47386f9bf5e630e9d4fee4e49448a","size_bytes":327},"recurrence_plan_abi":"pyamplicol-recurrence-plan-v2","required_runtime_capabilities":["rusticol.recurrence-color.contracted.v1","rusticol.recurrence-direct-arena.complex-f64.v1"],"runtime_layout_abi":"pyamplicol-recurrence-runtime-layout-v2","runtime_schedule":{"index_sha256":"ae108ebefad0a37d8886f336068b23a0d1999082ba94500e9f6d5f26c95e0889","kind":"pyamplicol-recurrence-runtime-container","member_count":1,"path":"recurrence/schedules/2474ae81e1e9b2027db747967d1322398b02a79e230a366376b5b5a552c366d7/recurrence-runtime.pacbin","plan_member_path":"schedule/recurrence-direct-schedule-v2.bin","schema_version":1,"sha256":"90fc08f26e9f3a936fa4b8bd51f72c393844b726e99d0377c97940cacc04a8c1","size_bytes":70408,"storage_abi":"pacbin-v1","unpacked_size_bytes":70120}},"prepared_kernel_pack_digest":"ddaa7c31f2361bda246b1310c8dca2e800a7237afd270249f86d5b549b557868","process":"d d~ > z z z","recurrence_plan_abi":"pyamplicol-recurrence-plan-v2","recurrence_summary":{"builder_input_abi":"pyamplicol-recurrence-builder-input-v2","closure_term_count":54,"contribution_count":450,"current_count":193,"lc_flow_layout":"contracted-color-union","template_input_abi":"pyamplicol-recurrence-template-input-v1"},"required_runtime_capabilities":["rusticol.recurrence-color.contracted.v1","rusticol.recurrence-direct-arena.complex-f64.v1"],"runtime_layout_abi":"pyamplicol-recurrence-runtime-layout-v2","runtime_metadata":{"color_contraction":{"abi":"pyamplicol-recurrence-color-contraction-v3","active_sector_count":1,"color_accuracy":"full","component_count":54,"destination_count":54,"entry_count":1,"factorization":null,"group_count":54,"includes_color_factor":true,"logical_entry_count":54,"materialized_destination_count":54,"path":"recurrence-color.bin","sector_count":1,"semantic_digest":"2ba9e3b1eef24758b866592eddf98589809b0bcf9ac641e94be49e6695b0dc1c","sha256":"2ba9e3b1eef24758b866592eddf98589809b0bcf9ac641e94be49e6695b0dc1c","size_bytes":1088,"storage":"repeated"},"external_legs":[{"is_initial":true,"outgoing_pdg":-1,"physical_pdg":1,"public_label":1,"source_slot":0},{"is_initial":true,"outgoing_pdg":1,"physical_pdg":-1,"public_label":2,"source_slot":1},{"is_initial":false,"outgoing_pdg":23,"physical_pdg":23,"public_label":3,"source_slot":2},{"is_initial":false,"outgoing_pdg":23,"physical_pdg":23,"public_label":4,"source_slot":3},{"is_initial":false,"outgoing_pdg":23,"physical_pdg":23,"public_label":5,"source_slot":4}],"normalization":{"average_factor":36,"color_accuracy":"full","color_factor":3,"coupling_policy":"stage evaluators include local vertex couplings","couplings_in_stage_evaluators":true,"electroweak_coupling_power":3,"global_coupling_factor":0.006823435874517697,"identical_factor":6,"qcd_coupling_power":0},"parameter_projection":[{"component":0,"parameter_template_id":0,"prepared_parameter_id":null,"runtime_name":"normalization.alpha_ew","runtime_slot":0},{"component":0,"parameter_template_id":7,"prepared_parameter_id":null,"runtime_name":"normalization.alpha_s_me_check","runtime_slot":1},{"component":0,"parameter_template_id":2,"prepared_parameter_id":0,"runtime_name":"particle.23.mass","runtime_slot":2},{"component":0,"parameter_template_id":1,"prepared_parameter_id":1,"runtime_name":"particle.23.width","runtime_slot":3},{"component":0,"parameter_template_id":9,"prepared_parameter_id":2,"runtime_name":"particle.24.mass","runtime_slot":4},{"component":0,"parameter_template_id":3,"prepared_parameter_id":3,"runtime_name":"particle.24.width","runtime_slot":5},{"component":0,"parameter_template_id":5,"prepared_parameter_id":4,"runtime_name":"particle.25.mass","runtime_slot":6},{"component":0,"parameter_template_id":6,"prepared_parameter_id":5,"runtime_name":"particle.25.width","runtime_slot":7},{"component":0,"parameter_template_id":8,"prepared_parameter_id":6,"runtime_name":"particle.6.mass","runtime_slot":8},{"component":0,"parameter_template_id":4,"prepared_parameter_id":7,"runtime_name":"particle.6.width","runtime_slot":9}],"particle_masses":[{"mass":0.0,"outgoing_pdg":-1},{"mass":0.0,"outgoing_pdg":1},{"mass":91.188,"outgoing_pdg":23}],"prepared_parameter_defaults":[[91.188,0.0],[2.441404,0.0],[80.41900244575616,0.0],[2.0476,0.0],[125.0,0.0],[0.006382339,0.0],[173.0,0.0],[1.4915,0.0],[0.0,0.0],[0.0,0.0]],"public_color_flows":[],"runtime_parameters":[{"complex_component":null,"default":0.0075467711139788835,"kind":"external","name":"normalization.alpha_ew","parameter_index":0,"runtime_name":null},{"complex_component":null,"default":0.118,"kind":"external","name":"normalization.alpha_s_me_check","parameter_index":1,"runtime_name":null},{"complex_component":null,"default":91.188,"kind":"external","name":"particle.23.mass","parameter_index":2,"runtime_name":null},{"complex_component":null,"default":2.441404,"kind":"external","name":"particle.23.width","parameter_index":3,"runtime_name":null},{"complex_component":null,"default":80.41900244575616,"kind":"external","name":"particle.24.mass","parameter_index":4,"runtime_name":null},{"complex_component":null,"default":2.0476,"kind":"external","name":"particle.24.width","parameter_index":5,"runtime_name":null},{"complex_component":null,"default":125.0,"kind":"external","name":"particle.25.mass","parameter_index":6,"runtime_name":null},{"complex_component":null,"default":0.006382339,"kind":"external","name":"particle.25.width","parameter_index":7,"runtime_name":null},{"complex_component":null,"default":173.0,"kind":"external","name":"particle.6.mass","parameter_index":8,"runtime_name":null},{"complex_component":null,"default":1.4915,"kind":"external","name":"particle.6.width","parameter_index":9,"runtime_name":null}],"source_templates":[{"chirality":0,"crossing":{"chirality_factor":1,"helicity_factor":-1,"momentum_transform":"negate-four-momentum","phase":[1.0,0.0],"spin_state_factor":-1},"current_state_template_id":57,"dimension":4,"helicity":-1,"source_ir":{"basis":"lorentz-vector","component_dimension":4,"crossing":{"chirality_factor":1,"helicity_factor":-1,"momentum_transform":"negate-four-momentum","phase":[1.0,0.0],"spin_state_factor":-1},"identity":{"anti_canonical_id":"model:built-in-sm-leading-color:state:z","anti_display_name":"z","anti_pdg_label":23,"canonical_id":"model:built-in-sm-leading-color:state:z","display_name":"z","orientation":"self-conjugate","pdg_label":23,"self_conjugate":true,"species_id":"model:built-in-sm-leading-color:species:z"},"mass_parameter":"particle.23.mass","states":[{"chirality":0,"helicity":-1,"spin_state":-1},{"chirality":0,"helicity":0,"spin_state":0},{"chirality":0,"helicity":1,"spin_state":1}],"statistics":"boson","wavefunction_family":"vector","width_parameter":"particle.23.width"},"source_template_id":18,"spin_state":-1},{"chirality":1,"crossing":{"chirality_factor":-1,"helicity_factor":1,"momentum_transform":"negate-four-momentum","phase":[1.0,0.0],"spin_state_factor":-1},"current_state_template_id":16,"dimension":2,"helicity":1,"source_ir":{"basis":"weyl-chiral","component_dimension":2,"crossing":{"chirality_factor":-1,"helicity_factor":1,"momentum_transform":"negate-four-momentum","phase":[1.0,0.0],"spin_state_factor":-1},"identity":{"anti_canonical_id":"model:built-in-sm-leading-color:state:d","anti_display_name":"d","anti_pdg_label":1,"canonical_id":"model:built-in-sm-leading-color:state:d~","display_name":"d~","orientation":"antiparticle","pdg_label":-1,"self_conjugate":false,"species_id":"model:built-in-sm-leading-color:species:d"},"mass_parameter":null,"states":[{"chirality":-1,"helicity":-1,"spin_state":-1},{"chirality":1,"helicity":1,"spin_state":1}],"statistics":"fermion","wavefunction_family":"fermion","width_parameter":null},"source_template_id":23,"spin_state":1},{"chirality":0,"crossing":{"chirality_factor":1,"helicity_factor":-1,"momentum_transform":"negate-four-momentum","phase":[1.0,0.0],"spin_state_factor":-1},"current_state_template_id":57,"dimension":4,"helicity":0,"source_ir":{"basis":"lorentz-vector","component_dimension":4,"crossing":{"chirality_factor":1,"helicity_factor":-1,"momentum_transform":"negate-four-momentum","phase":[1.0,0.0],"spin_state_factor":-1},"identity":{"anti_canonical_id":"model:built-in-sm-leading-color:state:z","anti_display_name":"z","anti_pdg_label":23,"canonical_id":"model:built-in-sm-leading-color:state:z","display_name":"z","orientation":"self-conjugate","pdg_label":23,"self_conjugate":true,"species_id":"model:built-in-sm-leading-color:species:z"},"mass_parameter":"particle.23.mass","states":[{"chirality":0,"helicity":-1,"spin_state":-1},{"chirality":0,"helicity":0,"spin_state":0},{"chirality":0,"helicity":1,"spin_state":1}],"statistics":"boson","wavefunction_family":"vector","width_parameter":"particle.23.width"},"source_template_id":30,"spin_state":0},{"chirality":0,"crossing":{"chirality_factor":1,"helicity_factor":-1,"momentum_transform":"negate-four-momentum","phase":[1.0,0.0],"spin_state_factor":-1},"current_state_template_id":57,"dimension":4,"helicity":1,"source_ir":{"basis":"lorentz-vector","component_dimension":4,"crossing":{"chirality_factor":1,"helicity_factor":-1,"momentum_transform":"negate-four-momentum","phase":[1.0,0.0],"spin_state_factor":-1},"identity":{"anti_canonical_id":"model:built-in-sm-leading-color:state:z","anti_display_name":"z","anti_pdg_label":23,"canonical_id":"model:built-in-sm-leading-color:state:z","display_name":"z","orientation":"self-conjugate","pdg_label":23,"self_conjugate":true,"species_id":"model:built-in-sm-leading-color:species:z"},"mass_parameter":"particle.23.mass","states":[{"chirality":0,"helicity":-1,"spin_state":-1},{"chirality":0,"helicity":0,"spin_state":0},{"chirality":0,"helicity":1,"spin_state":1}],"statistics":"boson","wavefunction_family":"vector","width_parameter":"particle.23.width"},"source_template_id":34,"spin_state":1},{"chirality":-1,"crossing":{"chirality_factor":-1,"helicity_factor":1,"momentum_transform":"negate-four-momentum","phase":[1.0,0.0],"spin_state_factor":-1},"current_state_template_id":21,"dimension":2,"helicity":-1,"source_ir":{"basis":"weyl-chiral","component_dimension":2,"crossing":{"chirality_factor":-1,"helicity_factor":1,"momentum_transform":"negate-four-momentum","phase":[1.0,0.0],"spin_state_factor":-1},"identity":{"anti_canonical_id":"model:built-in-sm-leading-color:state:d~","anti_display_name":"d~","anti_pdg_label":-1,"canonical_id":"model:built-in-sm-leading-color:state:d","display_name":"d","orientation":"particle","pdg_label":1,"self_conjugate":false,"species_id":"model:built-in-sm-leading-color:species:d"},"mass_parameter":null,"states":[{"chirality":-1,"helicity":-1,"spin_state":-1},{"chirality":1,"helicity":1,"spin_state":1}],"statistics":"fermion","wavefunction_family":"fermion","width_parameter":null},"source_template_id":37,"spin_state":-1},{"chirality":1,"crossing":{"chirality_factor":-1,"helicity_factor":1,"momentum_transform":"negate-four-momentum","phase":[1.0,0.0],"spin_state_factor":-1},"current_state_template_id":39,"dimension":2,"helicity":1,"source_ir":{"basis":"weyl-chiral","component_dimension":2,"crossing":{"chirality_factor":-1,"helicity_factor":1,"momentum_transform":"negate-four-momentum","phase":[1.0,0.0],"spin_state_factor":-1},"identity":{"anti_canonical_id":"model:built-in-sm-leading-color:state:d~","anti_display_name":"d~","anti_pdg_label":-1,"canonical_id":"model:built-in-sm-leading-color:state:d","display_name":"d","orientation":"particle","pdg_label":1,"self_conjugate":false,"species_id":"model:built-in-sm-leading-color:species:d"},"mass_parameter":null,"states":[{"chirality":-1,"helicity":-1,"spin_state":-1},{"chirality":1,"helicity":1,"spin_state":1}],"statistics":"fermion","wavefunction_family":"fermion","width_parameter":null},"source_template_id":40,"spin_state":1},{"chirality":-1,"crossing":{"chirality_factor":-1,"helicity_factor":1,"momentum_transform":"negate-four-momentum","phase":[1.0,0.0],"spin_state_factor":-1},"current_state_template_id":76,"dimension":2,"helicity":-1,"source_ir":{"basis":"weyl-chiral","component_dimension":2,"crossing":{"chirality_factor":-1,"helicity_factor":1,"momentum_transform":"negate-four-momentum","phase":[1.0,0.0],"spin_state_factor":-1},"identity":{"anti_canonical_id":"model:built-in-sm-leading-color:state:d","anti_display_name":"d","anti_pdg_label":1,"canonical_id":"model:built-in-sm-leading-color:state:d~","display_name":"d~","orientation":"antiparticle","pdg_label":-1,"self_conjugate":false,"species_id":"model:built-in-sm-leading-color:species:d"},"mass_parameter":null,"states":[{"chirality":-1,"helicity":-1,"spin_state":-1},{"chirality":1,"helicity":1,"spin_state":1}],"statistics":"fermion","wavefunction_family":"fermion","width_parameter":null},"source_template_id":57,"spin_state":-1}]},"runtime_options":{"point_tile_size":1024,"workspace_mib":256},"schema_version":3} diff --git a/pyamplicol/rust/crates/rusticol-core/tests/fixtures/recurrence_execution_hzz_lc.json b/pyamplicol/rust/crates/rusticol-core/tests/fixtures/recurrence_execution_hzz_lc.json new file mode 100644 index 0000000000..09c0ea3b64 --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/tests/fixtures/recurrence_execution_hzz_lc.json @@ -0,0 +1 @@ +{"builder_input_abi":"pyamplicol-recurrence-builder-input-v2","color_accuracy":"lc","direct_backend_abi":"rusticol.recurrence-direct-backend.v1","direct_template_abi":"pyamplicol-recurrence-direct-template-v1","direct_template_catalog_digest":"c831e44d43cd88251e9a65233812063838cbcf749963e41abbe4e3cf667b5d16","external_pdg_order":[1,-1,23,23,23],"kernel_pack":{"manifest_path":"model/eager-kernel-pack.json","payload_root":"model/eager-kernels"},"key":"d_dbar_to_z_z_z","kind":"pyamplicol-runtime-recurrence-execution","plan":{"builder_input_abi":"pyamplicol-recurrence-builder-input-v2","builder_input_sha256":"9e5d6022436c86ce17f847d56a755b32854d2fe1234ceb8ee7868a9a90888d97","direct_backend_abi":"rusticol.recurrence-direct-backend.v1","direct_template_abi":"pyamplicol-recurrence-direct-template-v1","direct_template_catalog_digest":"c831e44d43cd88251e9a65233812063838cbcf749963e41abbe4e3cf667b5d16","inspection_summary":{"construction":{"candidate_parent_pair_count":2706,"color_target_prune_count":0,"peak_contribution_count":450,"peak_current_count":193,"peak_dynamic_color_state_count":3,"peak_to_final_contribution_ratio":1.0,"peak_to_final_current_ratio":1.0},"direct_arena":{"arena_component_reuse_count":62,"cache_footprint_policy":"selector-active-max-v1","current_arena_components":369,"momentum_form_count":16,"packed_input_bytes":0,"packed_output_bytes":0,"row_group_count":23,"scatter_bytes":0,"selector_domain_count":2,"semantic_component_count":431},"direct_executor_count":633,"direct_template_abi":"pyamplicol-recurrence-direct-template-v1","execution_mode":"recurrence","generation_timings_seconds":{"catalog_authentication":0.041804625,"direct_lowering":0.068728875,"native_total":0.153668125,"python_extraction":0.002089,"semantic_construction":0.005300625,"serialization":0.0152825},"lc_flow_layout":"topology-replay","parameter_count":10,"prepared_kernel_count":49,"process_id":"d_dbar_to_z_z_z","recurrence_plan_abi":"pyamplicol-recurrence-plan-v2","runtime_container_member":{"container_size_bytes":70408,"path":"schedule/recurrence-direct-schedule-v2.bin","sha256":"3451a0492abe65f5b8918fe816973891331b58f0f358191175c5dba2c8ad9629","size_bytes":70144},"runtime_layout_abi":"pyamplicol-recurrence-runtime-layout-v2","runtime_layout_digest":"f5ca63bfc81cc8d92b88f4df156798662bc562fb889a1005130cb180eaa4facc","schedule":{"amplitude_destination_count":54,"closure_term_count":54,"contribution_count":450,"current_count":193,"exact_factor_count":6,"finalization_count":126,"replay_target_count":1,"resolved_helicity_count":54,"retained_helicity_count":108,"source_row_count":13},"schedule_digest":"16aa32bc1d8c78321240b6fa2227fb2a8ee0832936d05aa6cdddfd0a067a6450","sector_count":1,"selector_work_certificate":{"binding":"runtime_layout_digest","persisted_union":{"closure_count":54,"contribution_count":450,"current_count":193,"finalization_count":126,"row_count":643,"semantic_component_count":431,"source_row_count":13},"representatives":[{"amplitude_destination_count":54,"closure_count":54,"contribution_count":450,"current_count":193,"finalization_count":126,"public_flow_count":1,"representative_sector_id":0,"row_count":643,"semantic_component_count":431,"source_row_count":13}],"schema_version":1},"semantic_digest":"9e5d6022436c86ce17f847d56a755b32854d2fe1234ceb8ee7868a9a90888d97"},"kind":"pyamplicol-runtime-recurrence-execution","prepared_kernel_pack_digest":"00f6903f11582717e1ddfab8a9a7d91de76acab0a7d47224a0c7ae362877b258","process_binding":{"abi":"pyamplicol-recurrence-process-binding-v2","path":"recurrence-binding.bin","process_id":"d_dbar_to_z_z_z","process_semantic_digest":"9e5d6022436c86ce17f847d56a755b32854d2fe1234ceb8ee7868a9a90888d97","process_support_words":[1],"remap":{"bijection_digest":"7077a7b59abb4f7bdb3b99d5618068255325c57dde6c3489e82086c795234688","direct_executors":{"changes":[],"count":633},"parameter_slots":{"changes":[],"count":10},"physical_sector_ids":[0],"public_flow_ids":[0],"source_helicity_signs":[1,1,1,1,1],"source_momentum_signs":[1,1,1,1,1],"source_slots":[0,1,2,3,4],"source_state_indices":[0,1,0,1,0,1,2,0,1,2,0,1,2],"source_state_offsets":[0,2,4,7,10,13],"source_templates":{"changes":[],"count":62},"state_templates":{"changes":[],"count":82}},"schedule_digest":"16aa32bc1d8c78321240b6fa2227fb2a8ee0832936d05aa6cdddfd0a067a6450","sha256":"d1e6e0877bee8f1310f641879ae65fd723db2cc399227aeb2f2d4a2c7cbfbde6","size_bytes":327},"recurrence_plan_abi":"pyamplicol-recurrence-plan-v2","required_runtime_capabilities":["rusticol.recurrence-color.lc.v1","rusticol.recurrence-direct-arena.complex-f64.v1"],"runtime_layout_abi":"pyamplicol-recurrence-runtime-layout-v2","runtime_schedule":{"index_sha256":"32f2fd79bf7abb2eba9779e53bac170de4bc57eda09f3bdcf7e16a3028b12ecb","kind":"pyamplicol-recurrence-runtime-container","member_count":1,"path":"recurrence/schedules/16aa32bc1d8c78321240b6fa2227fb2a8ee0832936d05aa6cdddfd0a067a6450/recurrence-runtime.pacbin","plan_member_path":"schedule/recurrence-direct-schedule-v2.bin","schema_version":1,"sha256":"55e2d17c9a91efd76b636835c93f750a2b7f33d803a244a5f27b30c7df7b1bf7","size_bytes":70408,"storage_abi":"pacbin-v1","unpacked_size_bytes":70144}},"prepared_kernel_pack_digest":"00f6903f11582717e1ddfab8a9a7d91de76acab0a7d47224a0c7ae362877b258","process":"d d~ > z z z","recurrence_plan_abi":"pyamplicol-recurrence-plan-v2","recurrence_summary":{"builder_input_abi":"pyamplicol-recurrence-builder-input-v2","closure_term_count":54,"contribution_count":450,"current_count":193,"lc_flow_layout":"topology-replay","template_input_abi":"pyamplicol-recurrence-template-input-v1"},"required_runtime_capabilities":["rusticol.recurrence-color.lc.v1","rusticol.recurrence-direct-arena.complex-f64.v1"],"runtime_layout_abi":"pyamplicol-recurrence-runtime-layout-v2","runtime_metadata":{"color_contraction":null,"external_legs":[{"is_initial":true,"outgoing_pdg":-1,"physical_pdg":1,"public_label":1,"source_slot":0},{"is_initial":true,"outgoing_pdg":1,"physical_pdg":-1,"public_label":2,"source_slot":1},{"is_initial":false,"outgoing_pdg":23,"physical_pdg":23,"public_label":3,"source_slot":2},{"is_initial":false,"outgoing_pdg":23,"physical_pdg":23,"public_label":4,"source_slot":3},{"is_initial":false,"outgoing_pdg":23,"physical_pdg":23,"public_label":5,"source_slot":4}],"normalization":{"average_factor":36,"color_accuracy":"lc","color_factor":3,"coupling_policy":"stage evaluators include local vertex couplings","couplings_in_stage_evaluators":true,"electroweak_coupling_power":3,"global_coupling_factor":0.006823435874517697,"identical_factor":6,"qcd_coupling_power":0},"parameter_projection":[{"component":0,"parameter_template_id":0,"prepared_parameter_id":null,"runtime_name":"normalization.alpha_ew","runtime_slot":0},{"component":0,"parameter_template_id":7,"prepared_parameter_id":null,"runtime_name":"normalization.alpha_s_me_check","runtime_slot":1},{"component":0,"parameter_template_id":2,"prepared_parameter_id":0,"runtime_name":"particle.23.mass","runtime_slot":2},{"component":0,"parameter_template_id":1,"prepared_parameter_id":1,"runtime_name":"particle.23.width","runtime_slot":3},{"component":0,"parameter_template_id":9,"prepared_parameter_id":2,"runtime_name":"particle.24.mass","runtime_slot":4},{"component":0,"parameter_template_id":3,"prepared_parameter_id":3,"runtime_name":"particle.24.width","runtime_slot":5},{"component":0,"parameter_template_id":5,"prepared_parameter_id":4,"runtime_name":"particle.25.mass","runtime_slot":6},{"component":0,"parameter_template_id":6,"prepared_parameter_id":5,"runtime_name":"particle.25.width","runtime_slot":7},{"component":0,"parameter_template_id":8,"prepared_parameter_id":6,"runtime_name":"particle.6.mass","runtime_slot":8},{"component":0,"parameter_template_id":4,"prepared_parameter_id":7,"runtime_name":"particle.6.width","runtime_slot":9}],"particle_masses":[{"mass":0.0,"outgoing_pdg":-1},{"mass":0.0,"outgoing_pdg":1},{"mass":91.188,"outgoing_pdg":23}],"prepared_parameter_defaults":[[91.188,0.0],[2.441404,0.0],[80.41900244575616,0.0],[2.0476,0.0],[125.0,0.0],[0.006382339,0.0],[173.0,0.0],[1.4915,0.0],[0.0,0.0],[0.0,0.0]],"public_color_flows":[{"construction_sector_id":0,"public_id":"flow:2,1","target_sector_id":0}],"runtime_parameters":[{"complex_component":null,"default":0.0075467711139788835,"kind":"external","name":"normalization.alpha_ew","parameter_index":0,"runtime_name":null},{"complex_component":null,"default":0.118,"kind":"external","name":"normalization.alpha_s_me_check","parameter_index":1,"runtime_name":null},{"complex_component":null,"default":91.188,"kind":"external","name":"particle.23.mass","parameter_index":2,"runtime_name":null},{"complex_component":null,"default":2.441404,"kind":"external","name":"particle.23.width","parameter_index":3,"runtime_name":null},{"complex_component":null,"default":80.41900244575616,"kind":"external","name":"particle.24.mass","parameter_index":4,"runtime_name":null},{"complex_component":null,"default":2.0476,"kind":"external","name":"particle.24.width","parameter_index":5,"runtime_name":null},{"complex_component":null,"default":125.0,"kind":"external","name":"particle.25.mass","parameter_index":6,"runtime_name":null},{"complex_component":null,"default":0.006382339,"kind":"external","name":"particle.25.width","parameter_index":7,"runtime_name":null},{"complex_component":null,"default":173.0,"kind":"external","name":"particle.6.mass","parameter_index":8,"runtime_name":null},{"complex_component":null,"default":1.4915,"kind":"external","name":"particle.6.width","parameter_index":9,"runtime_name":null}],"source_templates":[{"chirality":0,"crossing":{"chirality_factor":1,"helicity_factor":-1,"momentum_transform":"negate-four-momentum","phase":[1.0,0.0],"spin_state_factor":-1},"current_state_template_id":57,"dimension":4,"helicity":-1,"source_ir":{"basis":"lorentz-vector","component_dimension":4,"crossing":{"chirality_factor":1,"helicity_factor":-1,"momentum_transform":"negate-four-momentum","phase":[1.0,0.0],"spin_state_factor":-1},"identity":{"anti_canonical_id":"model:built-in-sm-leading-color:state:z","anti_display_name":"z","anti_pdg_label":23,"canonical_id":"model:built-in-sm-leading-color:state:z","display_name":"z","orientation":"self-conjugate","pdg_label":23,"self_conjugate":true,"species_id":"model:built-in-sm-leading-color:species:z"},"mass_parameter":"particle.23.mass","states":[{"chirality":0,"helicity":-1,"spin_state":-1},{"chirality":0,"helicity":0,"spin_state":0},{"chirality":0,"helicity":1,"spin_state":1}],"statistics":"boson","wavefunction_family":"vector","width_parameter":"particle.23.width"},"source_template_id":18,"spin_state":-1},{"chirality":1,"crossing":{"chirality_factor":-1,"helicity_factor":1,"momentum_transform":"negate-four-momentum","phase":[1.0,0.0],"spin_state_factor":-1},"current_state_template_id":16,"dimension":2,"helicity":1,"source_ir":{"basis":"weyl-chiral","component_dimension":2,"crossing":{"chirality_factor":-1,"helicity_factor":1,"momentum_transform":"negate-four-momentum","phase":[1.0,0.0],"spin_state_factor":-1},"identity":{"anti_canonical_id":"model:built-in-sm-leading-color:state:d","anti_display_name":"d","anti_pdg_label":1,"canonical_id":"model:built-in-sm-leading-color:state:d~","display_name":"d~","orientation":"antiparticle","pdg_label":-1,"self_conjugate":false,"species_id":"model:built-in-sm-leading-color:species:d"},"mass_parameter":null,"states":[{"chirality":-1,"helicity":-1,"spin_state":-1},{"chirality":1,"helicity":1,"spin_state":1}],"statistics":"fermion","wavefunction_family":"fermion","width_parameter":null},"source_template_id":23,"spin_state":1},{"chirality":0,"crossing":{"chirality_factor":1,"helicity_factor":-1,"momentum_transform":"negate-four-momentum","phase":[1.0,0.0],"spin_state_factor":-1},"current_state_template_id":57,"dimension":4,"helicity":0,"source_ir":{"basis":"lorentz-vector","component_dimension":4,"crossing":{"chirality_factor":1,"helicity_factor":-1,"momentum_transform":"negate-four-momentum","phase":[1.0,0.0],"spin_state_factor":-1},"identity":{"anti_canonical_id":"model:built-in-sm-leading-color:state:z","anti_display_name":"z","anti_pdg_label":23,"canonical_id":"model:built-in-sm-leading-color:state:z","display_name":"z","orientation":"self-conjugate","pdg_label":23,"self_conjugate":true,"species_id":"model:built-in-sm-leading-color:species:z"},"mass_parameter":"particle.23.mass","states":[{"chirality":0,"helicity":-1,"spin_state":-1},{"chirality":0,"helicity":0,"spin_state":0},{"chirality":0,"helicity":1,"spin_state":1}],"statistics":"boson","wavefunction_family":"vector","width_parameter":"particle.23.width"},"source_template_id":30,"spin_state":0},{"chirality":0,"crossing":{"chirality_factor":1,"helicity_factor":-1,"momentum_transform":"negate-four-momentum","phase":[1.0,0.0],"spin_state_factor":-1},"current_state_template_id":57,"dimension":4,"helicity":1,"source_ir":{"basis":"lorentz-vector","component_dimension":4,"crossing":{"chirality_factor":1,"helicity_factor":-1,"momentum_transform":"negate-four-momentum","phase":[1.0,0.0],"spin_state_factor":-1},"identity":{"anti_canonical_id":"model:built-in-sm-leading-color:state:z","anti_display_name":"z","anti_pdg_label":23,"canonical_id":"model:built-in-sm-leading-color:state:z","display_name":"z","orientation":"self-conjugate","pdg_label":23,"self_conjugate":true,"species_id":"model:built-in-sm-leading-color:species:z"},"mass_parameter":"particle.23.mass","states":[{"chirality":0,"helicity":-1,"spin_state":-1},{"chirality":0,"helicity":0,"spin_state":0},{"chirality":0,"helicity":1,"spin_state":1}],"statistics":"boson","wavefunction_family":"vector","width_parameter":"particle.23.width"},"source_template_id":34,"spin_state":1},{"chirality":-1,"crossing":{"chirality_factor":-1,"helicity_factor":1,"momentum_transform":"negate-four-momentum","phase":[1.0,0.0],"spin_state_factor":-1},"current_state_template_id":21,"dimension":2,"helicity":-1,"source_ir":{"basis":"weyl-chiral","component_dimension":2,"crossing":{"chirality_factor":-1,"helicity_factor":1,"momentum_transform":"negate-four-momentum","phase":[1.0,0.0],"spin_state_factor":-1},"identity":{"anti_canonical_id":"model:built-in-sm-leading-color:state:d~","anti_display_name":"d~","anti_pdg_label":-1,"canonical_id":"model:built-in-sm-leading-color:state:d","display_name":"d","orientation":"particle","pdg_label":1,"self_conjugate":false,"species_id":"model:built-in-sm-leading-color:species:d"},"mass_parameter":null,"states":[{"chirality":-1,"helicity":-1,"spin_state":-1},{"chirality":1,"helicity":1,"spin_state":1}],"statistics":"fermion","wavefunction_family":"fermion","width_parameter":null},"source_template_id":37,"spin_state":-1},{"chirality":1,"crossing":{"chirality_factor":-1,"helicity_factor":1,"momentum_transform":"negate-four-momentum","phase":[1.0,0.0],"spin_state_factor":-1},"current_state_template_id":39,"dimension":2,"helicity":1,"source_ir":{"basis":"weyl-chiral","component_dimension":2,"crossing":{"chirality_factor":-1,"helicity_factor":1,"momentum_transform":"negate-four-momentum","phase":[1.0,0.0],"spin_state_factor":-1},"identity":{"anti_canonical_id":"model:built-in-sm-leading-color:state:d~","anti_display_name":"d~","anti_pdg_label":-1,"canonical_id":"model:built-in-sm-leading-color:state:d","display_name":"d","orientation":"particle","pdg_label":1,"self_conjugate":false,"species_id":"model:built-in-sm-leading-color:species:d"},"mass_parameter":null,"states":[{"chirality":-1,"helicity":-1,"spin_state":-1},{"chirality":1,"helicity":1,"spin_state":1}],"statistics":"fermion","wavefunction_family":"fermion","width_parameter":null},"source_template_id":40,"spin_state":1},{"chirality":-1,"crossing":{"chirality_factor":-1,"helicity_factor":1,"momentum_transform":"negate-four-momentum","phase":[1.0,0.0],"spin_state_factor":-1},"current_state_template_id":76,"dimension":2,"helicity":-1,"source_ir":{"basis":"weyl-chiral","component_dimension":2,"crossing":{"chirality_factor":-1,"helicity_factor":1,"momentum_transform":"negate-four-momentum","phase":[1.0,0.0],"spin_state_factor":-1},"identity":{"anti_canonical_id":"model:built-in-sm-leading-color:state:d","anti_display_name":"d","anti_pdg_label":1,"canonical_id":"model:built-in-sm-leading-color:state:d~","display_name":"d~","orientation":"antiparticle","pdg_label":-1,"self_conjugate":false,"species_id":"model:built-in-sm-leading-color:species:d"},"mass_parameter":null,"states":[{"chirality":-1,"helicity":-1,"spin_state":-1},{"chirality":1,"helicity":1,"spin_state":1}],"statistics":"fermion","wavefunction_family":"fermion","width_parameter":null},"source_template_id":57,"spin_state":-1}]},"runtime_options":{"point_tile_size":1024,"workspace_mib":256},"schema_version":3} diff --git a/pyamplicol/rust/crates/rusticol-core/tests/fixtures/recurrence_execution_hzz_nlc.json b/pyamplicol/rust/crates/rusticol-core/tests/fixtures/recurrence_execution_hzz_nlc.json new file mode 100644 index 0000000000..85f912cb98 --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/tests/fixtures/recurrence_execution_hzz_nlc.json @@ -0,0 +1 @@ +{"builder_input_abi":"pyamplicol-recurrence-builder-input-v2","color_accuracy":"nlc","direct_backend_abi":"rusticol.recurrence-direct-backend.v1","direct_template_abi":"pyamplicol-recurrence-direct-template-v1","direct_template_catalog_digest":"c831e44d43cd88251e9a65233812063838cbcf749963e41abbe4e3cf667b5d16","external_pdg_order":[1,-1,23,23,23],"kernel_pack":{"manifest_path":"model/eager-kernel-pack.json","payload_root":"model/eager-kernels"},"key":"d_dbar_to_z_z_z","kind":"pyamplicol-runtime-recurrence-execution","plan":{"builder_input_abi":"pyamplicol-recurrence-builder-input-v2","builder_input_sha256":"6277cdfb8b170b27c6a6b28779fa607911610db924bf8a8b03d0b51060bb3633","direct_backend_abi":"rusticol.recurrence-direct-backend.v1","direct_template_abi":"pyamplicol-recurrence-direct-template-v1","direct_template_catalog_digest":"c831e44d43cd88251e9a65233812063838cbcf749963e41abbe4e3cf667b5d16","inspection_summary":{"construction":{"candidate_parent_pair_count":2706,"color_target_prune_count":0,"peak_contribution_count":450,"peak_current_count":193,"peak_dynamic_color_state_count":3,"peak_to_final_contribution_ratio":1.0,"peak_to_final_current_ratio":1.0},"direct_arena":{"arena_component_reuse_count":62,"cache_footprint_policy":"persisted-arena-v1","current_arena_components":369,"momentum_form_count":16,"packed_input_bytes":0,"packed_output_bytes":0,"row_group_count":23,"scatter_bytes":0,"selector_domain_count":1,"semantic_component_count":431},"direct_executor_count":633,"direct_template_abi":"pyamplicol-recurrence-direct-template-v1","execution_mode":"recurrence","generation_timings_seconds":{"catalog_authentication":0.075438833,"direct_lowering":0.074224625,"native_total":0.191393333,"python_extraction":0.012225667,"semantic_construction":0.005605333,"serialization":0.011755792},"lc_flow_layout":"contracted-color-union","parameter_count":10,"prepared_kernel_count":49,"process_id":"d_dbar_to_z_z_z","recurrence_plan_abi":"pyamplicol-recurrence-plan-v2","runtime_container_member":{"container_size_bytes":70408,"path":"schedule/recurrence-direct-schedule-v2.bin","sha256":"358acb674cefc1570146eb361bcaf42516c3045f91575327fa8e1b344fd3b6b0","size_bytes":70120},"runtime_layout_abi":"pyamplicol-recurrence-runtime-layout-v2","runtime_layout_digest":"a2fc04dc7b94e806af1fa14bdd26e0b61db13ef3d3e349e81b2c64faa5344abb","schedule":{"amplitude_destination_count":54,"closure_term_count":54,"contribution_count":450,"current_count":193,"exact_factor_count":6,"finalization_count":126,"replay_target_count":1,"resolved_helicity_count":54,"retained_helicity_count":108,"source_row_count":13},"schedule_digest":"32acf60fc25819dee96f7c64c5857baa63a18b9e1dd52ebe4ff674696bc158c7","sector_count":1,"selector_work_certificate":{"binding":"runtime_layout_digest","persisted_union":{"closure_count":54,"contribution_count":450,"current_count":193,"finalization_count":126,"row_count":643,"semantic_component_count":431,"source_row_count":13},"representatives":[],"schema_version":1},"semantic_digest":"6277cdfb8b170b27c6a6b28779fa607911610db924bf8a8b03d0b51060bb3633"},"kind":"pyamplicol-runtime-recurrence-execution","prepared_kernel_pack_digest":"00f6903f11582717e1ddfab8a9a7d91de76acab0a7d47224a0c7ae362877b258","process_binding":{"abi":"pyamplicol-recurrence-process-binding-v2","path":"recurrence-binding.bin","process_id":"d_dbar_to_z_z_z","process_semantic_digest":"6277cdfb8b170b27c6a6b28779fa607911610db924bf8a8b03d0b51060bb3633","process_support_words":[1],"remap":{"bijection_digest":"7077a7b59abb4f7bdb3b99d5618068255325c57dde6c3489e82086c795234688","direct_executors":{"changes":[],"count":633},"parameter_slots":{"changes":[],"count":10},"physical_sector_ids":[0],"public_flow_ids":[0],"source_helicity_signs":[1,1,1,1,1],"source_momentum_signs":[1,1,1,1,1],"source_slots":[0,1,2,3,4],"source_state_indices":[0,1,0,1,0,1,2,0,1,2,0,1,2],"source_state_offsets":[0,2,4,7,10,13],"source_templates":{"changes":[],"count":62},"state_templates":{"changes":[],"count":82}},"schedule_digest":"32acf60fc25819dee96f7c64c5857baa63a18b9e1dd52ebe4ff674696bc158c7","sha256":"bf1e043fd2cf9ec7565d4eb9a271c92fc07a94c8c9cba445484ac7778d81d276","size_bytes":327},"recurrence_plan_abi":"pyamplicol-recurrence-plan-v2","required_runtime_capabilities":["rusticol.recurrence-color.contracted.v1","rusticol.recurrence-direct-arena.complex-f64.v1"],"runtime_layout_abi":"pyamplicol-recurrence-runtime-layout-v2","runtime_schedule":{"index_sha256":"dab93f5cebe469bdb92fa6d218c045d1aca114e1f4bf08430f0aa52f1b0b2084","kind":"pyamplicol-recurrence-runtime-container","member_count":1,"path":"recurrence/schedules/32acf60fc25819dee96f7c64c5857baa63a18b9e1dd52ebe4ff674696bc158c7/recurrence-runtime.pacbin","plan_member_path":"schedule/recurrence-direct-schedule-v2.bin","schema_version":1,"sha256":"a19e79e209ad12c355639f6abed502b87cab311c0151627437b1cbe1ecf5d58c","size_bytes":70408,"storage_abi":"pacbin-v1","unpacked_size_bytes":70120}},"prepared_kernel_pack_digest":"00f6903f11582717e1ddfab8a9a7d91de76acab0a7d47224a0c7ae362877b258","process":"d d~ > z z z","recurrence_plan_abi":"pyamplicol-recurrence-plan-v2","recurrence_summary":{"builder_input_abi":"pyamplicol-recurrence-builder-input-v2","closure_term_count":54,"contribution_count":450,"current_count":193,"lc_flow_layout":"contracted-color-union","template_input_abi":"pyamplicol-recurrence-template-input-v1"},"required_runtime_capabilities":["rusticol.recurrence-color.contracted.v1","rusticol.recurrence-direct-arena.complex-f64.v1"],"runtime_layout_abi":"pyamplicol-recurrence-runtime-layout-v2","runtime_metadata":{"color_contraction":{"abi":"pyamplicol-recurrence-color-contraction-v3","active_sector_count":1,"color_accuracy":"nlc","component_count":54,"destination_count":54,"entry_count":1,"factorization":null,"group_count":54,"includes_color_factor":true,"logical_entry_count":54,"materialized_destination_count":54,"path":"recurrence-color.bin","sector_count":1,"semantic_digest":"fdf22dd8291d038eec5c0a115c6b18bfaca6bf77eb80eeb301425960a98ed027","sha256":"fdf22dd8291d038eec5c0a115c6b18bfaca6bf77eb80eeb301425960a98ed027","size_bytes":1088,"storage":"repeated"},"external_legs":[{"is_initial":true,"outgoing_pdg":-1,"physical_pdg":1,"public_label":1,"source_slot":0},{"is_initial":true,"outgoing_pdg":1,"physical_pdg":-1,"public_label":2,"source_slot":1},{"is_initial":false,"outgoing_pdg":23,"physical_pdg":23,"public_label":3,"source_slot":2},{"is_initial":false,"outgoing_pdg":23,"physical_pdg":23,"public_label":4,"source_slot":3},{"is_initial":false,"outgoing_pdg":23,"physical_pdg":23,"public_label":5,"source_slot":4}],"normalization":{"average_factor":36,"color_accuracy":"nlc","color_factor":3,"coupling_policy":"stage evaluators include local vertex couplings","couplings_in_stage_evaluators":true,"electroweak_coupling_power":3,"global_coupling_factor":0.006823435874517697,"identical_factor":6,"qcd_coupling_power":0},"parameter_projection":[{"component":0,"parameter_template_id":0,"prepared_parameter_id":null,"runtime_name":"normalization.alpha_ew","runtime_slot":0},{"component":0,"parameter_template_id":7,"prepared_parameter_id":null,"runtime_name":"normalization.alpha_s_me_check","runtime_slot":1},{"component":0,"parameter_template_id":2,"prepared_parameter_id":0,"runtime_name":"particle.23.mass","runtime_slot":2},{"component":0,"parameter_template_id":1,"prepared_parameter_id":1,"runtime_name":"particle.23.width","runtime_slot":3},{"component":0,"parameter_template_id":9,"prepared_parameter_id":2,"runtime_name":"particle.24.mass","runtime_slot":4},{"component":0,"parameter_template_id":3,"prepared_parameter_id":3,"runtime_name":"particle.24.width","runtime_slot":5},{"component":0,"parameter_template_id":5,"prepared_parameter_id":4,"runtime_name":"particle.25.mass","runtime_slot":6},{"component":0,"parameter_template_id":6,"prepared_parameter_id":5,"runtime_name":"particle.25.width","runtime_slot":7},{"component":0,"parameter_template_id":8,"prepared_parameter_id":6,"runtime_name":"particle.6.mass","runtime_slot":8},{"component":0,"parameter_template_id":4,"prepared_parameter_id":7,"runtime_name":"particle.6.width","runtime_slot":9}],"particle_masses":[{"mass":0.0,"outgoing_pdg":-1},{"mass":0.0,"outgoing_pdg":1},{"mass":91.188,"outgoing_pdg":23}],"prepared_parameter_defaults":[[91.188,0.0],[2.441404,0.0],[80.41900244575616,0.0],[2.0476,0.0],[125.0,0.0],[0.006382339,0.0],[173.0,0.0],[1.4915,0.0],[0.0,0.0],[0.0,0.0]],"public_color_flows":[],"runtime_parameters":[{"complex_component":null,"default":0.0075467711139788835,"kind":"external","name":"normalization.alpha_ew","parameter_index":0,"runtime_name":null},{"complex_component":null,"default":0.118,"kind":"external","name":"normalization.alpha_s_me_check","parameter_index":1,"runtime_name":null},{"complex_component":null,"default":91.188,"kind":"external","name":"particle.23.mass","parameter_index":2,"runtime_name":null},{"complex_component":null,"default":2.441404,"kind":"external","name":"particle.23.width","parameter_index":3,"runtime_name":null},{"complex_component":null,"default":80.41900244575616,"kind":"external","name":"particle.24.mass","parameter_index":4,"runtime_name":null},{"complex_component":null,"default":2.0476,"kind":"external","name":"particle.24.width","parameter_index":5,"runtime_name":null},{"complex_component":null,"default":125.0,"kind":"external","name":"particle.25.mass","parameter_index":6,"runtime_name":null},{"complex_component":null,"default":0.006382339,"kind":"external","name":"particle.25.width","parameter_index":7,"runtime_name":null},{"complex_component":null,"default":173.0,"kind":"external","name":"particle.6.mass","parameter_index":8,"runtime_name":null},{"complex_component":null,"default":1.4915,"kind":"external","name":"particle.6.width","parameter_index":9,"runtime_name":null}],"source_templates":[{"chirality":0,"crossing":{"chirality_factor":1,"helicity_factor":-1,"momentum_transform":"negate-four-momentum","phase":[1.0,0.0],"spin_state_factor":-1},"current_state_template_id":57,"dimension":4,"helicity":-1,"source_ir":{"basis":"lorentz-vector","component_dimension":4,"crossing":{"chirality_factor":1,"helicity_factor":-1,"momentum_transform":"negate-four-momentum","phase":[1.0,0.0],"spin_state_factor":-1},"identity":{"anti_canonical_id":"model:built-in-sm-leading-color:state:z","anti_display_name":"z","anti_pdg_label":23,"canonical_id":"model:built-in-sm-leading-color:state:z","display_name":"z","orientation":"self-conjugate","pdg_label":23,"self_conjugate":true,"species_id":"model:built-in-sm-leading-color:species:z"},"mass_parameter":"particle.23.mass","states":[{"chirality":0,"helicity":-1,"spin_state":-1},{"chirality":0,"helicity":0,"spin_state":0},{"chirality":0,"helicity":1,"spin_state":1}],"statistics":"boson","wavefunction_family":"vector","width_parameter":"particle.23.width"},"source_template_id":18,"spin_state":-1},{"chirality":1,"crossing":{"chirality_factor":-1,"helicity_factor":1,"momentum_transform":"negate-four-momentum","phase":[1.0,0.0],"spin_state_factor":-1},"current_state_template_id":16,"dimension":2,"helicity":1,"source_ir":{"basis":"weyl-chiral","component_dimension":2,"crossing":{"chirality_factor":-1,"helicity_factor":1,"momentum_transform":"negate-four-momentum","phase":[1.0,0.0],"spin_state_factor":-1},"identity":{"anti_canonical_id":"model:built-in-sm-leading-color:state:d","anti_display_name":"d","anti_pdg_label":1,"canonical_id":"model:built-in-sm-leading-color:state:d~","display_name":"d~","orientation":"antiparticle","pdg_label":-1,"self_conjugate":false,"species_id":"model:built-in-sm-leading-color:species:d"},"mass_parameter":null,"states":[{"chirality":-1,"helicity":-1,"spin_state":-1},{"chirality":1,"helicity":1,"spin_state":1}],"statistics":"fermion","wavefunction_family":"fermion","width_parameter":null},"source_template_id":23,"spin_state":1},{"chirality":0,"crossing":{"chirality_factor":1,"helicity_factor":-1,"momentum_transform":"negate-four-momentum","phase":[1.0,0.0],"spin_state_factor":-1},"current_state_template_id":57,"dimension":4,"helicity":0,"source_ir":{"basis":"lorentz-vector","component_dimension":4,"crossing":{"chirality_factor":1,"helicity_factor":-1,"momentum_transform":"negate-four-momentum","phase":[1.0,0.0],"spin_state_factor":-1},"identity":{"anti_canonical_id":"model:built-in-sm-leading-color:state:z","anti_display_name":"z","anti_pdg_label":23,"canonical_id":"model:built-in-sm-leading-color:state:z","display_name":"z","orientation":"self-conjugate","pdg_label":23,"self_conjugate":true,"species_id":"model:built-in-sm-leading-color:species:z"},"mass_parameter":"particle.23.mass","states":[{"chirality":0,"helicity":-1,"spin_state":-1},{"chirality":0,"helicity":0,"spin_state":0},{"chirality":0,"helicity":1,"spin_state":1}],"statistics":"boson","wavefunction_family":"vector","width_parameter":"particle.23.width"},"source_template_id":30,"spin_state":0},{"chirality":0,"crossing":{"chirality_factor":1,"helicity_factor":-1,"momentum_transform":"negate-four-momentum","phase":[1.0,0.0],"spin_state_factor":-1},"current_state_template_id":57,"dimension":4,"helicity":1,"source_ir":{"basis":"lorentz-vector","component_dimension":4,"crossing":{"chirality_factor":1,"helicity_factor":-1,"momentum_transform":"negate-four-momentum","phase":[1.0,0.0],"spin_state_factor":-1},"identity":{"anti_canonical_id":"model:built-in-sm-leading-color:state:z","anti_display_name":"z","anti_pdg_label":23,"canonical_id":"model:built-in-sm-leading-color:state:z","display_name":"z","orientation":"self-conjugate","pdg_label":23,"self_conjugate":true,"species_id":"model:built-in-sm-leading-color:species:z"},"mass_parameter":"particle.23.mass","states":[{"chirality":0,"helicity":-1,"spin_state":-1},{"chirality":0,"helicity":0,"spin_state":0},{"chirality":0,"helicity":1,"spin_state":1}],"statistics":"boson","wavefunction_family":"vector","width_parameter":"particle.23.width"},"source_template_id":34,"spin_state":1},{"chirality":-1,"crossing":{"chirality_factor":-1,"helicity_factor":1,"momentum_transform":"negate-four-momentum","phase":[1.0,0.0],"spin_state_factor":-1},"current_state_template_id":21,"dimension":2,"helicity":-1,"source_ir":{"basis":"weyl-chiral","component_dimension":2,"crossing":{"chirality_factor":-1,"helicity_factor":1,"momentum_transform":"negate-four-momentum","phase":[1.0,0.0],"spin_state_factor":-1},"identity":{"anti_canonical_id":"model:built-in-sm-leading-color:state:d~","anti_display_name":"d~","anti_pdg_label":-1,"canonical_id":"model:built-in-sm-leading-color:state:d","display_name":"d","orientation":"particle","pdg_label":1,"self_conjugate":false,"species_id":"model:built-in-sm-leading-color:species:d"},"mass_parameter":null,"states":[{"chirality":-1,"helicity":-1,"spin_state":-1},{"chirality":1,"helicity":1,"spin_state":1}],"statistics":"fermion","wavefunction_family":"fermion","width_parameter":null},"source_template_id":37,"spin_state":-1},{"chirality":1,"crossing":{"chirality_factor":-1,"helicity_factor":1,"momentum_transform":"negate-four-momentum","phase":[1.0,0.0],"spin_state_factor":-1},"current_state_template_id":39,"dimension":2,"helicity":1,"source_ir":{"basis":"weyl-chiral","component_dimension":2,"crossing":{"chirality_factor":-1,"helicity_factor":1,"momentum_transform":"negate-four-momentum","phase":[1.0,0.0],"spin_state_factor":-1},"identity":{"anti_canonical_id":"model:built-in-sm-leading-color:state:d~","anti_display_name":"d~","anti_pdg_label":-1,"canonical_id":"model:built-in-sm-leading-color:state:d","display_name":"d","orientation":"particle","pdg_label":1,"self_conjugate":false,"species_id":"model:built-in-sm-leading-color:species:d"},"mass_parameter":null,"states":[{"chirality":-1,"helicity":-1,"spin_state":-1},{"chirality":1,"helicity":1,"spin_state":1}],"statistics":"fermion","wavefunction_family":"fermion","width_parameter":null},"source_template_id":40,"spin_state":1},{"chirality":-1,"crossing":{"chirality_factor":-1,"helicity_factor":1,"momentum_transform":"negate-four-momentum","phase":[1.0,0.0],"spin_state_factor":-1},"current_state_template_id":76,"dimension":2,"helicity":-1,"source_ir":{"basis":"weyl-chiral","component_dimension":2,"crossing":{"chirality_factor":-1,"helicity_factor":1,"momentum_transform":"negate-four-momentum","phase":[1.0,0.0],"spin_state_factor":-1},"identity":{"anti_canonical_id":"model:built-in-sm-leading-color:state:d","anti_display_name":"d","anti_pdg_label":1,"canonical_id":"model:built-in-sm-leading-color:state:d~","display_name":"d~","orientation":"antiparticle","pdg_label":-1,"self_conjugate":false,"species_id":"model:built-in-sm-leading-color:species:d"},"mass_parameter":null,"states":[{"chirality":-1,"helicity":-1,"spin_state":-1},{"chirality":1,"helicity":1,"spin_state":1}],"statistics":"fermion","wavefunction_family":"fermion","width_parameter":null},"source_template_id":57,"spin_state":-1}]},"runtime_options":{"point_tile_size":1024,"workspace_mib":256},"schema_version":3} diff --git a/pyamplicol/rust/crates/rusticol-core/tests/generated_artifact_odd_tails.rs b/pyamplicol/rust/crates/rusticol-core/tests/generated_artifact_odd_tails.rs new file mode 100644 index 0000000000..fbfbcfa4bb --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/tests/generated_artifact_odd_tails.rs @@ -0,0 +1,860 @@ +// SPDX-License-Identifier: 0BSD + +#![cfg(all(feature = "f64-symjit", any(target_os = "linux", target_os = "macos")))] + +use rusticol_core::{NativeResolvedEvaluation, NativeRuntime, NativeRuntimeProfile, RusticolError}; +use std::alloc::{GlobalAlloc, Layout, System}; +use std::cell::Cell; +use std::fs; +use std::path::PathBuf; + +const ODD_TAIL_POINT_COUNTS: [usize; 4] = [127, 129, 1023, 1025]; +const PROFILE_REPETITIONS: usize = 2; +const EXPECTED_PROCESS: &str = "d d~ > z g g g g g g"; +const EXPECTED_EXTERNAL_PDGS: [i32; 9] = [1, -1, 23, 21, 21, 21, 21, 21, 21]; +const INPUT_PREFIX_CANARY_BITS: u64 = 0x3fd1_2345_6789_abcd; +const INPUT_SUFFIX_CANARY_BITS: u64 = 0xbfc9_8765_4321_0fed; +const OUTPUT_PREFIX_CANARY_BITS: u64 = 0x4023_4567_89ab_cdef; +const OUTPUT_SUFFIX_CANARY_BITS: u64 = 0xc013_579b_dfb7_5311; + +thread_local! { + static TRACK_ALLOCATIONS: Cell = const { Cell::new(false) }; + static ALLOCATION_COUNT: Cell = const { Cell::new(0) }; + static ALLOCATED_BYTES: Cell = const { Cell::new(0) }; +} + +struct CountingAllocator; + +#[global_allocator] +static GLOBAL_ALLOCATOR: CountingAllocator = CountingAllocator; + +unsafe impl GlobalAlloc for CountingAllocator { + unsafe fn alloc(&self, layout: Layout) -> *mut u8 { + count_allocation(layout.size()); + unsafe { System.alloc(layout) } + } + + unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 { + count_allocation(layout.size()); + unsafe { System.alloc_zeroed(layout) } + } + + unsafe fn realloc(&self, pointer: *mut u8, layout: Layout, new_size: usize) -> *mut u8 { + count_allocation(new_size); + unsafe { System.realloc(pointer, layout, new_size) } + } + + unsafe fn dealloc(&self, pointer: *mut u8, layout: Layout) { + unsafe { System.dealloc(pointer, layout) } + } +} + +fn count_allocation(bytes: usize) { + if TRACK_ALLOCATIONS.try_with(Cell::get).unwrap_or(false) { + let _ = ALLOCATION_COUNT.try_with(|count| count.set(count.get() + 1)); + let _ = ALLOCATED_BYTES.try_with(|total| total.set(total.get().saturating_add(bytes))); + } +} + +fn count_allocations(function: impl FnOnce() -> T) -> (T, usize, usize) { + ALLOCATION_COUNT.with(|count| count.set(0)); + ALLOCATED_BYTES.with(|total| total.set(0)); + TRACK_ALLOCATIONS.with(|tracking| tracking.set(true)); + let result = function(); + TRACK_ALLOCATIONS.with(|tracking| tracking.set(false)); + let count = ALLOCATION_COUNT.with(Cell::get); + let bytes = ALLOCATED_BYTES.with(Cell::get); + (result, count, bytes) +} + +#[derive(Clone, Copy)] +enum ProfileLane { + Compiled, + Eager, + RecurrenceTopology, + RecurrenceUnion, +} + +fn guarded_values(values: &[f64], prefix_bits: u64, suffix_bits: u64) -> Vec { + let mut guarded = Vec::with_capacity(values.len() + 2); + guarded.push(f64::from_bits(prefix_bits)); + guarded.extend_from_slice(values); + guarded.push(f64::from_bits(suffix_bits)); + guarded +} + +fn assert_guard_canaries( + guarded: &[f64], + logical_len: usize, + prefix_bits: u64, + suffix_bits: u64, + context: &str, +) { + assert_eq!( + guarded.len(), + logical_len + 2, + "{context}: malformed guarded buffer" + ); + assert_eq!( + guarded[0].to_bits(), + prefix_bits, + "{context}: prefix canary changed" + ); + assert_eq!( + guarded[logical_len + 1].to_bits(), + suffix_bits, + "{context}: suffix canary changed" + ); +} + +fn fixture_path(environment_name: &str) -> Option { + let Some(path) = std::env::var_os(environment_name) else { + eprintln!("skipping genuine odd-tail gate: {environment_name} is not set"); + return None; + }; + let path = PathBuf::from(path); + assert!( + path.is_dir(), + "{environment_name} does not name an artifact directory: {}", + path.display() + ); + Some(path) +} + +fn validation_momenta(runtime: &NativeRuntime) -> Vec { + let metadata = runtime.metadata(); + let path = runtime + .root() + .join("processes") + .join(&metadata.representative_process_key) + .join("validation-momenta.json"); + let payload: serde_json::Value = + serde_json::from_slice(&fs::read(&path).unwrap_or_else(|error| { + panic!( + "could not read genuine odd-tail validation momenta {}: {error}", + path.display() + ) + })) + .unwrap_or_else(|error| { + panic!( + "could not parse genuine odd-tail validation momenta {}: {error}", + path.display() + ) + }); + let point = payload["points"][0] + .as_array() + .expect("genuine odd-tail fixture must contain one validation point"); + assert_eq!( + point.len(), + metadata.external_count, + "genuine odd-tail validation point has the wrong external multiplicity" + ); + point + .iter() + .flat_map(|leg| { + leg["momentum"] + .as_array() + .expect("validation leg must contain four momentum components") + .iter() + .map(|component| { + component + .as_str() + .map(str::parse::) + .transpose() + .expect("validation momentum string must be binary64") + .or_else(|| component.as_f64()) + .expect("validation momentum component must be numeric") + }) + }) + .collect() +} + +fn assert_close(actual: f64, expected: f64, context: &str) { + let tolerance = 1.0e-15 + 1.0e-12 * expected.abs(); + assert!( + (actual - expected).abs() <= tolerance, + "{context}: {actual:.17e} != {expected:.17e} (tolerance {tolerance:.3e})" + ); +} + +fn alternating_helicity_references( + runtime: &mut NativeRuntime, + point: &[f64], + resolved: &NativeResolvedEvaluation, +) -> [(u32, f64); 2] { + let helicities = runtime + .helicities() + .expect("load genuine odd-tail helicity metadata"); + let color_count = resolved.color_ids.len(); + assert!( + color_count > 0, + "genuine odd-tail resolved reference has no color components" + ); + assert_eq!( + resolved.values.len(), + resolved.helicity_ids.len() * color_count, + "genuine odd-tail one-point resolved shape" + ); + + let mut candidates = Vec::new(); + for (resolved_index, helicity_id) in resolved.helicity_ids.iter().enumerate() { + let Some(helicity) = helicities.iter().find(|item| &item.id == helicity_id) else { + panic!("resolved helicity {helicity_id:?} is absent from runtime metadata"); + }; + if helicity.structural_zero { + continue; + } + let start = resolved_index * color_count; + let expected = resolved.values[start..start + color_count] + .iter() + .copied() + .sum::(); + if !expected.is_finite() || expected == 0.0 { + continue; + } + candidates.push(( + u32::try_from(helicity.index) + .expect("genuine odd-tail helicity index does not fit u32"), + expected, + helicity.id.clone(), + )); + } + + let mut selected_pair = None; + 'pairs: for (left_index, left) in candidates.iter().enumerate() { + for right in candidates.iter().skip(left_index + 1) { + let scale = left.1.abs().max(right.1.abs()); + let separation = (left.1 - right.1).abs(); + if separation > 64.0 * (1.0e-15 + 1.0e-12 * scale) { + selected_pair = Some([left.clone(), right.clone()]); + break 'pairs; + } + } + } + let selected_pair = selected_pair.unwrap_or_else(|| { + panic!(concat!( + "genuine odd-tail fixture has no two finite nonzero, numerically ", + "distinguishable helicity references" + )) + }); + + let mut verified = Vec::with_capacity(2); + for (index, expected, helicity_id) in selected_pair { + let selector = [helicity_id.clone()]; + let selected = runtime + .evaluate_f64_with_selectors(point, 1, Some(&selector), None, None, None) + .unwrap_or_else(|error| { + panic!( + "evaluate genuine odd-tail scalar helicity reference {:?}: {error}", + helicity_id + ) + })[0]; + assert_close( + selected, + expected, + &format!( + "genuine odd-tail selected/resolved helicity parity {:?}", + helicity_id + ), + ); + verified.push((index, selected)); + } + verified + .try_into() + .expect("genuine odd-tail helicity reference pair") +} + +fn assert_recurrence_layout(runtime: &NativeRuntime, expected_layout: &str) { + let metadata = runtime.metadata(); + let path = runtime + .root() + .join("processes") + .join(&metadata.representative_process_key) + .join("execution.json"); + let execution: serde_json::Value = + serde_json::from_slice(&fs::read(&path).unwrap_or_else(|error| { + panic!( + "could not read genuine odd-tail execution manifest {}: {error}", + path.display() + ) + })) + .unwrap_or_else(|error| { + panic!( + "could not parse genuine odd-tail execution manifest {}: {error}", + path.display() + ) + }); + assert_eq!( + execution["recurrence_summary"]["lc_flow_layout"].as_str(), + Some(expected_layout), + "genuine odd-tail recurrence fixture has the wrong LC layout" + ); +} + +fn assert_jit_optimization_level(runtime: &NativeRuntime, expected_level: u32) { + let path = runtime.root().join("config").join("effective.toml"); + let contents = fs::read_to_string(&path).unwrap_or_else(|error| { + panic!( + "could not read genuine odd-tail effective configuration {}: {error}", + path.display() + ) + }); + let mut section = ""; + let mut observed_backend = None; + let mut observed_level = None; + for line in contents.lines().map(str::trim) { + if line.starts_with('[') { + section = line; + continue; + } + let Some((name, value)) = line.split_once('=') else { + continue; + }; + match (section, name.trim()) { + ("[evaluator]", "backend") => { + observed_backend = Some(value.trim().trim_matches('"').to_string()); + } + ("[evaluator.jit]", "optimization_level") => { + observed_level = Some( + value + .trim() + .parse::() + .expect("JIT optimization level must be an unsigned integer"), + ); + } + _ => {} + } + } + assert_eq!( + observed_backend.as_deref(), + Some("jit"), + "genuine odd-tail fixture does not use the JIT evaluator backend" + ); + assert_eq!( + observed_level, + Some(expected_level), + "genuine odd-tail fixture has the wrong JIT optimization level" + ); +} + +fn assert_zero_legacy_boundary_counters(profile: &NativeRuntimeProfile, context: &str) { + // These are the legacy stage/amplitude and selector boundary adapters. + // Eager DirectTable gather/fanout and P-kernel scratch traffic are internal + // arena traffic and deliberately have separate attribution. + for (label, observed) in [ + ( + "stage input copies", + profile.stage_input_copy_component_count, + ), + ( + "stage leaf input copies", + profile.stage_leaf_input_copy_component_count, + ), + ( + "stage evaluator output gathers", + profile.stage_evaluator_output_gather_component_count, + ), + ( + "stage output assignments", + profile.stage_output_assign_component_count, + ), + ( + "amplitude input copies", + profile.amplitude_input_copy_component_count, + ), + ( + "amplitude leaf input copies", + profile.amplitude_leaf_input_copy_component_count, + ), + ( + "amplitude evaluator output gathers", + profile.amplitude_evaluator_output_gather_component_count, + ), + ( + "amplitude output remaps", + profile.amplitude_output_remap_component_count, + ), + ( + "selector gather points", + profile.selector_gather_point_count, + ), + ("selector gather bytes", profile.selector_gather_bytes), + ( + "selector scatter values", + profile.selector_scatter_value_count, + ), + ] { + assert_eq!(observed, 0, "{context}: {label}"); + } +} + +fn assert_zero_exposed_boundary_traffic( + profile: &NativeRuntimeProfile, + lane: ProfileLane, + measured_input_components: u64, + context: &str, +) { + assert_eq!( + profile.native_input_component_count, measured_input_components, + "{context}: profiled input component count" + ); + assert_eq!( + profile.native_input_pack_bytes, 0, + "{context}: native input packing traffic" + ); + assert_eq!( + profile.native_input_crossing_bytes, 0, + "{context}: native input crossing traffic" + ); + assert_eq!( + profile.native_input_container_allocation_count, 0, + "{context}: native input container allocations" + ); + assert_eq!( + profile.native_output_allocation_count, 0, + "{context}: native output allocations" + ); + assert_eq!( + profile.observed_scratch_reallocation_count, 0, + "{context}: warmed scratch reallocations" + ); + assert_zero_legacy_boundary_counters(profile, context); + + match lane { + ProfileLane::Compiled => { + assert!( + profile.compiled_direct_arena_engine_count > 0, + "{context}: compiled profile did not authenticate an Arena engine" + ); + assert!( + profile.compiled_direct_arena_call_count > 0, + "{context}: compiled profile observed no Arena calls" + ); + assert_eq!( + profile.compiled_direct_arena_boundary_input_bytes, 0, + "{context}: compiled arena input boundary traffic" + ); + assert_eq!( + profile.compiled_direct_arena_boundary_current_output_bytes, 0, + "{context}: compiled arena current-output boundary traffic" + ); + assert_eq!( + profile.compiled_direct_arena_boundary_amplitude_output_bytes, 0, + "{context}: compiled arena amplitude-output boundary traffic" + ); + } + ProfileLane::Eager => { + assert_eq!( + profile.compiled_direct_arena_engine_count, 0, + "{context}: eager profile reported a compiled Arena engine" + ); + } + ProfileLane::RecurrenceTopology | ProfileLane::RecurrenceUnion => { + panic!("{context}: recurrence must not request the eager/compiled Arena profile") + } + } +} + +fn assert_lane_activity_profile(runtime: &mut NativeRuntime, point: &[f64], lane: ProfileLane) { + if matches!(lane, ProfileLane::Compiled) { + return; + } + let point_count = *ODD_TAIL_POINT_COUNTS + .last() + .expect("genuine odd-tail point-count inventory"); + let momenta = point.repeat(point_count); + let profiled = runtime + .evaluate_f64_profile_repeated(&momenta, point_count, 1, None, None) + .expect("profile genuine odd-tail lane activity"); + let profile = &profiled.profile; + assert_eq!( + profile.observed_scratch_reallocation_count, 0, + "genuine odd-tail lane activity profile reallocated warmed scratch" + ); + assert_zero_legacy_boundary_counters(profile, "genuine odd-tail lane activity"); + + match lane { + ProfileLane::Compiled => unreachable!("compiled lane returned above"), + ProfileLane::Eager => { + assert!( + profile.evaluator_backend_call_count > 0, + "genuine eager odd-tail profile observed no JIT backend calls" + ); + } + ProfileLane::RecurrenceTopology => { + assert!( + profile.recurrence_schedule_execution_count > 0, + "genuine topology odd-tail profile observed no recurrence schedule" + ); + assert!( + profile.recurrence_replay_schedule_execution_count > 0, + "genuine topology odd-tail profile observed no replay schedule" + ); + assert_eq!( + profile.recurrence_union_schedule_execution_count, 0, + "genuine topology odd-tail profile unexpectedly used a union schedule" + ); + assert!( + profile.recurrence_replay_output_value_count > 0, + "genuine topology odd-tail profile observed no replay outputs" + ); + assert!( + profile.recurrence_source_call_count > 0 + && profile.recurrence_contribution_call_count > 0, + "genuine topology odd-tail profile did not execute source and contribution kernels" + ); + } + ProfileLane::RecurrenceUnion => { + assert!( + profile.recurrence_schedule_execution_count > 0, + "genuine union odd-tail profile observed no recurrence schedule" + ); + assert!( + profile.recurrence_union_schedule_execution_count > 0, + "genuine union odd-tail profile observed no union schedule" + ); + assert_eq!( + profile.recurrence_replay_schedule_execution_count, 0, + "genuine union odd-tail profile unexpectedly used a replay schedule" + ); + assert!( + profile.recurrence_union_source_row_count > 0, + "genuine union odd-tail profile observed no union source rows" + ); + assert!( + profile.recurrence_contribution_call_count > 0, + "genuine union odd-tail profile did not execute contribution kernels" + ); + } + } +} + +fn prove_genuine_odd_tails( + artifact: PathBuf, + expected_execution_mode: &str, + expected_jit_optimization_level: u32, + recurrence_layout: Option<&str>, + profile_lane: ProfileLane, +) { + let mut runtime = + NativeRuntime::load(&artifact, None, None).expect("load genuine odd-tail artifact"); + let metadata = runtime.metadata(); + assert_eq!( + metadata.execution_mode, expected_execution_mode, + "genuine odd-tail fixture has the wrong execution mode" + ); + assert_eq!( + metadata.process, EXPECTED_PROCESS, + "genuine odd-tail fixture has the wrong process" + ); + assert_eq!( + metadata.external_pdg_order, EXPECTED_EXTERNAL_PDGS, + "genuine odd-tail fixture has the wrong external PDG ordering" + ); + assert!( + metadata.current_count > 0 && metadata.interaction_count > 0, + "genuine odd-tail fixture does not contain a generated current DAG" + ); + match profile_lane { + ProfileLane::Compiled => assert_eq!( + metadata.prepared_backend, None, + "genuine compiled odd-tail fixture unexpectedly reports a prepared backend" + ), + ProfileLane::Eager | ProfileLane::RecurrenceTopology | ProfileLane::RecurrenceUnion => { + assert_eq!( + metadata.prepared_backend.as_deref(), + Some("jit"), + "genuine prepared odd-tail fixture does not use the JIT backend" + ) + } + } + assert_jit_optimization_level(&runtime, expected_jit_optimization_level); + if let Some(expected_layout) = recurrence_layout { + assert_recurrence_layout(&runtime, expected_layout); + } + + let point = validation_momenta(&runtime); + let owned_total = runtime + .evaluate_f64(&point, 1) + .expect("evaluate genuine odd-tail scalar reference")[0]; + let resolved = runtime + .evaluate_resolved_f64(&point, 1, None, None) + .expect("resolve genuine odd-tail scalar reference"); + let resolved_total = resolved.totals()[0]; + assert_close( + owned_total, + resolved_total, + "genuine odd-tail scalar total/resolved parity", + ); + assert!( + owned_total.is_finite(), + "genuine odd-tail scalar reference is non-finite" + ); + let alternating_helicities = alternating_helicity_references(&mut runtime, &point, &resolved); + + for point_count in ODD_TAIL_POINT_COUNTS { + let context = format!("{expected_execution_mode} point_count={point_count}"); + let logical_momenta = point.repeat(point_count); + let guarded_momenta = guarded_values( + &logical_momenta, + INPUT_PREFIX_CANARY_BITS, + INPUT_SUFFIX_CANARY_BITS, + ); + let momenta = &guarded_momenta[1..logical_momenta.len() + 1]; + let logical_output = vec![f64::NAN; point_count]; + let mut guarded_output = guarded_values( + &logical_output, + OUTPUT_PREFIX_CANARY_BITS, + OUTPUT_SUFFIX_CANARY_BITS, + ); + + for warmup_name in ["first", "descriptor"] { + guarded_output[1..point_count + 1].fill(f64::NAN); + runtime + .evaluate_f64_into( + momenta, + point_count, + &mut guarded_output[1..point_count + 1], + ) + .unwrap_or_else(|error| panic!("{context}: {warmup_name} warmup failed: {error}")); + assert_guard_canaries( + &guarded_momenta, + logical_momenta.len(), + INPUT_PREFIX_CANARY_BITS, + INPUT_SUFFIX_CANARY_BITS, + &format!("{context} {warmup_name} input"), + ); + assert_guard_canaries( + &guarded_output, + point_count, + OUTPUT_PREFIX_CANARY_BITS, + OUTPUT_SUFFIX_CANARY_BITS, + &format!("{context} {warmup_name} output"), + ); + for (point_index, observed) in guarded_output[1..point_count + 1] + .iter() + .copied() + .enumerate() + { + assert_close( + observed, + resolved_total, + &format!("{context} {warmup_name} numerical parity point={point_index}"), + ); + } + } + + guarded_output[1..point_count + 1].fill(f64::NAN); + let (result, allocation_count, allocated_bytes) = count_allocations(|| { + runtime.evaluate_f64_into( + momenta, + point_count, + &mut guarded_output[1..point_count + 1], + ) + }); + result.unwrap_or_else(|error: RusticolError| { + panic!("{context}: counted caller-output evaluation failed: {error}") + }); + assert_eq!( + (allocation_count, allocated_bytes), + (0, 0), + "{context}: warmed caller-output evaluation allocated" + ); + assert_guard_canaries( + &guarded_momenta, + logical_momenta.len(), + INPUT_PREFIX_CANARY_BITS, + INPUT_SUFFIX_CANARY_BITS, + &format!("{context} counted input"), + ); + assert_guard_canaries( + &guarded_output, + point_count, + OUTPUT_PREFIX_CANARY_BITS, + OUTPUT_SUFFIX_CANARY_BITS, + &format!("{context} counted output"), + ); + for (point_index, observed) in guarded_output[1..point_count + 1] + .iter() + .copied() + .enumerate() + { + assert_close( + observed, + resolved_total, + &format!("{context} counted numerical parity point={point_index}"), + ); + } + + let helicity_by_point = (0..point_count) + .map(|point_index| alternating_helicities[point_index % 2].0) + .collect::>(); + for warmup_name in ["first selector", "descriptor selector"] { + guarded_output[1..point_count + 1].fill(f64::NAN); + runtime + .evaluate_f64_into_with_selectors( + momenta, + point_count, + None, + None, + Some(&helicity_by_point), + None, + &mut guarded_output[1..point_count + 1], + ) + .unwrap_or_else(|error| panic!("{context}: {warmup_name} warmup failed: {error}")); + assert_guard_canaries( + &guarded_momenta, + logical_momenta.len(), + INPUT_PREFIX_CANARY_BITS, + INPUT_SUFFIX_CANARY_BITS, + &format!("{context} {warmup_name} input"), + ); + assert_guard_canaries( + &guarded_output, + point_count, + OUTPUT_PREFIX_CANARY_BITS, + OUTPUT_SUFFIX_CANARY_BITS, + &format!("{context} {warmup_name} output"), + ); + for (point_index, observed) in guarded_output[1..point_count + 1] + .iter() + .copied() + .enumerate() + { + assert_close( + observed, + alternating_helicities[point_index % 2].1, + &format!("{context} {warmup_name} numerical parity point={point_index}"), + ); + } + } + + guarded_output[1..point_count + 1].fill(f64::NAN); + let (selector_result, selector_allocations, selector_bytes) = count_allocations(|| { + runtime.evaluate_f64_into_with_selectors( + momenta, + point_count, + None, + None, + Some(&helicity_by_point), + None, + &mut guarded_output[1..point_count + 1], + ) + }); + selector_result.unwrap_or_else(|error: RusticolError| { + panic!("{context}: counted selector evaluation failed: {error}") + }); + assert_eq!( + (selector_allocations, selector_bytes), + (0, 0), + "{context}: warmed caller-output selector evaluation allocated" + ); + assert_guard_canaries( + &guarded_momenta, + logical_momenta.len(), + INPUT_PREFIX_CANARY_BITS, + INPUT_SUFFIX_CANARY_BITS, + &format!("{context} counted selector input"), + ); + assert_guard_canaries( + &guarded_output, + point_count, + OUTPUT_PREFIX_CANARY_BITS, + OUTPUT_SUFFIX_CANARY_BITS, + &format!("{context} counted selector output"), + ); + for (point_index, observed) in guarded_output[1..point_count + 1] + .iter() + .copied() + .enumerate() + { + assert_close( + observed, + alternating_helicities[point_index % 2].1, + &format!("{context} counted selector parity point={point_index}"), + ); + } + + if matches!(profile_lane, ProfileLane::Compiled | ProfileLane::Eager) { + let profiled = runtime + .evaluate_f64_arena_profile_repeated( + momenta, + point_count, + PROFILE_REPETITIONS, + None, + None, + ) + .unwrap_or_else(|error| panic!("{context}: warmed profile failed: {error}")); + assert_eq!( + profiled.values.len(), + point_count, + "{context}: profiled output length" + ); + for (point_index, observed) in profiled.values.iter().copied().enumerate() { + assert_close( + observed, + resolved_total, + &format!("{context} profiled numerical parity point={point_index}"), + ); + } + let measured_input_components = u64::try_from( + momenta + .len() + .checked_mul(PROFILE_REPETITIONS) + .expect("profiled input component count overflowed"), + ) + .expect("profiled input component count does not fit u64"); + assert_zero_exposed_boundary_traffic( + &profiled.profile, + profile_lane, + measured_input_components, + &context, + ); + } + } + assert_lane_activity_profile(&mut runtime, &point, profile_lane); +} + +#[test] +fn genuine_compiled_o3_odd_tails_preserve_numerics_allocations_and_boundary_traffic() { + let Some(artifact) = fixture_path("RUSTICOL_COMPILED_DIRECT_ARTIFACT") else { + return; + }; + prove_genuine_odd_tails(artifact, "compiled", 3, None, ProfileLane::Compiled); +} + +#[test] +fn genuine_eager_o2_odd_tails_preserve_numerics_allocations_and_boundary_traffic() { + let Some(artifact) = fixture_path("RUSTICOL_EAGER_ARTIFACT") else { + return; + }; + prove_genuine_odd_tails(artifact, "eager", 2, None, ProfileLane::Eager); +} + +#[test] +fn genuine_topology_recurrence_o2_odd_tails_preserve_numerics_and_allocations() { + let Some(artifact) = fixture_path("RUSTICOL_RECURRENCE_TOPOLOGY_ALLOCATION_ARTIFACT") else { + return; + }; + prove_genuine_odd_tails( + artifact, + "recurrence", + 2, + Some("topology-replay"), + ProfileLane::RecurrenceTopology, + ); +} + +#[test] +fn genuine_all_flow_union_recurrence_o2_odd_tails_preserve_numerics_and_allocations() { + let Some(artifact) = fixture_path("RUSTICOL_RECURRENCE_UNION_ALLOCATION_ARTIFACT") else { + return; + }; + prove_genuine_odd_tails( + artifact, + "recurrence", + 2, + Some("all-flow-union"), + ProfileLane::RecurrenceUnion, + ); +} diff --git a/pyamplicol/rust/crates/rusticol-core/tests/recurrence_direct_arena_allocations.rs b/pyamplicol/rust/crates/rusticol-core/tests/recurrence_direct_arena_allocations.rs new file mode 100644 index 0000000000..dfa995d94b --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/tests/recurrence_direct_arena_allocations.rs @@ -0,0 +1,972 @@ +// SPDX-License-Identifier: 0BSD + +use rusticol_core::recurrence::direct_backend::{ + DIRECT_STATUS_OK, DirectArenaView, DirectExecutorCatalog, DirectExecutorHandle, + DirectFactorView, DirectMomentumView, DirectParameterView, DirectUnionSourceDispatchHandle, +}; +use rusticol_core::recurrence::direct_runtime::DirectRecurrenceExecutionRuntime; +use rusticol_core::recurrence::{ + CheckedTableRange, ClosureExecutionProofGroupV2, ClosureProofContributionV2, + ClosureProofMetadataV2, DIRECT_NONE_U32, DirectAmplitudeDestinationDescriptor, + DirectClosureRow, DirectContributionRow, DirectCurrentDescriptor, DirectDestinationOperation, + DirectExecutorRole, DirectFinalizationRow, DirectMomentumFormDescriptor, DirectMomentumTerm, + DirectNodeKind, DirectRecurrencePlan, DirectRecurrencePlanParts, DirectReplayTargetDescriptor, + DirectResolvedHelicityDescriptor, DirectResolvedSourceSelection, DirectRowGroupDescriptor, + DirectSelectorDomainDescriptor, DirectSourceDispatchVariantDescriptor, + DirectSourceEmbeddingRow, DirectSourceProjectionRow, DirectSourceRow, + DirectSourceStateAssignment, ExactComplexRational, RecurrenceStrategy, SemanticDigest, + closure_component_factor_digest_v2, closure_selector_domain_digest_v2, +}; +use rusticol_core::{NativeRuntime, RusticolError}; +use std::alloc::{GlobalAlloc, Layout, System}; +use std::cell::Cell; +use std::ffi::c_void; +use std::fs; +use std::path::{Path, PathBuf}; + +thread_local! { + static TRACK_ALLOCATIONS: Cell = const { Cell::new(false) }; + static ALLOCATION_COUNT: Cell = const { Cell::new(0) }; + static ALLOCATED_BYTES: Cell = const { Cell::new(0) }; +} + +struct CountingAllocator; + +#[global_allocator] +static GLOBAL_ALLOCATOR: CountingAllocator = CountingAllocator; + +unsafe impl GlobalAlloc for CountingAllocator { + unsafe fn alloc(&self, layout: Layout) -> *mut u8 { + count_allocation(layout.size()); + unsafe { System.alloc(layout) } + } + + unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 { + count_allocation(layout.size()); + unsafe { System.alloc_zeroed(layout) } + } + + unsafe fn realloc(&self, pointer: *mut u8, layout: Layout, new_size: usize) -> *mut u8 { + count_allocation(new_size); + unsafe { System.realloc(pointer, layout, new_size) } + } + + unsafe fn dealloc(&self, pointer: *mut u8, layout: Layout) { + unsafe { System.dealloc(pointer, layout) } + } +} + +fn count_allocation(bytes: usize) { + let tracking = TRACK_ALLOCATIONS.try_with(Cell::get).unwrap_or(false); + if tracking { + let _ = ALLOCATION_COUNT.try_with(|count| count.set(count.get() + 1)); + let _ = ALLOCATED_BYTES.try_with(|total| total.set(total.get().saturating_add(bytes))); + } +} + +fn count_allocations(function: impl FnOnce() -> T) -> (T, usize, usize) { + ALLOCATION_COUNT.with(|count| count.set(0)); + ALLOCATED_BYTES.with(|total| total.set(0)); + TRACK_ALLOCATIONS.with(|tracking| tracking.set(true)); + let result = function(); + TRACK_ALLOCATIONS.with(|tracking| tracking.set(false)); + let count = ALLOCATION_COUNT.with(Cell::get); + let bytes = ALLOCATED_BYTES.with(Cell::get); + (result, count, bytes) +} + +const STATUS_BOUNDS: i32 = 2; +const POINT_COUNT: u32 = 4; +const EXPECTED_RE: [f64; 4] = [3.0, 6.0, 9.0, 12.0]; +const EXPECTED_IM: [f64; 4] = [1.0, 2.0, 3.0, 4.0]; +const REAL_ARTIFACT_REPETITIONS: usize = 8; +const TOPOLOGY_ARTIFACT_ENV: &str = "RUSTICOL_RECURRENCE_TOPOLOGY_ALLOCATION_ARTIFACT"; +const UNION_ARTIFACT_ENV: &str = "RUSTICOL_RECURRENCE_UNION_ALLOCATION_ARTIFACT"; +const CONTRACTED_ARTIFACT_ENV: &str = "RUSTICOL_RECURRENCE_CONTRACTED_ALLOCATION_ARTIFACT"; + +unsafe extern "C" fn fill_sources( + _context: *const c_void, + arena: DirectArenaView, + momenta: DirectMomentumView, + _parameters: DirectParameterView, + _factors: DirectFactorView, + rows: *const DirectSourceRow, + row_count: u32, + point_count: u32, +) -> i32 { + let rows = unsafe { std::slice::from_raw_parts(rows, row_count as usize) }; + for row in rows { + for point in 0..point_count as usize { + let source = row.momentum_form_id as usize + * momenta.lorentz_component_count as usize + * momenta.point_stride as usize + + point; + let destination = + row.destination_component_base as usize * arena.point_stride as usize + point; + if source >= momenta.scalar_len as usize + || destination >= arena.current_scalar_len as usize + { + return STATUS_BOUNDS; + } + unsafe { + *arena.current_re.add(destination) = *momenta.values.add(source); + *arena.current_im.add(destination) = 0.0; + } + } + } + DIRECT_STATUS_OK +} + +unsafe extern "C" fn fill_union_sources( + _context: *const c_void, + arena: DirectArenaView, + momenta: DirectMomentumView, + _parameters: DirectParameterView, + factors: DirectFactorView, + rows: *const DirectSourceRow, + row_count: u32, + variants: *const DirectSourceDispatchVariantDescriptor, + variant_count: u32, + embeddings: *const DirectSourceEmbeddingRow, + embedding_count: u32, + selections: *const DirectResolvedSourceSelection, + selection_count: u32, + point_count: u32, +) -> i32 { + let rows = unsafe { std::slice::from_raw_parts(rows, row_count as usize) }; + let variants = unsafe { std::slice::from_raw_parts(variants, variant_count as usize) }; + let embeddings = unsafe { std::slice::from_raw_parts(embeddings, embedding_count as usize) }; + let selections = unsafe { std::slice::from_raw_parts(selections, selection_count as usize) }; + for selection in selections { + let Some(variant) = variants.get(selection.dispatch_variant_id as usize) else { + return STATUS_BOUNDS; + }; + let Some(row) = rows.get(variant.source_row_id as usize) else { + return STATUS_BOUNDS; + }; + if row.source_slot != selection.source_slot + || variant.crossing_exact_factor_id >= factors.value_count + { + return STATUS_BOUNDS; + } + let embedding_start = variant.embedding_start as usize; + let Some(variant_embeddings) = + embeddings.get(embedding_start..embedding_start + variant.embedding_count as usize) + else { + return STATUS_BOUNDS; + }; + let crossing = unsafe { + *factors + .values_re + .add(variant.crossing_exact_factor_id as usize) + }; + for embedding in variant_embeddings { + if embedding.source_component_or_sentinel == DIRECT_NONE_U32 + || embedding.exact_factor_id >= factors.value_count + { + return STATUS_BOUNDS; + } + let embedding_factor = + unsafe { *factors.values_re.add(embedding.exact_factor_id as usize) }; + for point in 0..point_count as usize { + let source = row.momentum_form_id as usize + * momenta.lorentz_component_count as usize + * momenta.point_stride as usize + + point; + let destination = (row.destination_component_base as usize + + embedding.full_component as usize) + * arena.point_stride as usize + + point; + if source >= momenta.scalar_len as usize + || destination >= arena.current_scalar_len as usize + { + return STATUS_BOUNDS; + } + unsafe { + *arena.current_re.add(destination) = + *momenta.values.add(source) * crossing * embedding_factor; + *arena.current_im.add(destination) = 0.0; + } + } + } + } + DIRECT_STATUS_OK +} + +unsafe extern "C" fn accumulate_contributions( + _context: *const c_void, + arena: DirectArenaView, + _momenta: DirectMomentumView, + parameters: DirectParameterView, + factors: DirectFactorView, + rows: *const DirectContributionRow, + row_count: u32, + point_count: u32, +) -> i32 { + let rows = unsafe { std::slice::from_raw_parts(rows, row_count as usize) }; + for row in rows { + if row.exact_factor_id >= factors.value_count || parameters.value_count == 0 { + return STATUS_BOUNDS; + } + let factor_re = unsafe { *factors.values_re.add(row.exact_factor_id as usize) }; + let factor_im = unsafe { *factors.values_im.add(row.exact_factor_id as usize) }; + let parameter_re = unsafe { *parameters.values_re }; + let parameter_im = unsafe { *parameters.values_im }; + let scale_re = factor_re * parameter_re - factor_im * parameter_im; + let scale_im = factor_re * parameter_im + factor_im * parameter_re; + for point in 0..point_count as usize { + let source = row.parent0_component_base as usize * arena.point_stride as usize + point; + let destination = + row.destination_component_base as usize * arena.point_stride as usize + point; + if source >= arena.current_scalar_len as usize + || destination >= arena.current_scalar_len as usize + { + return STATUS_BOUNDS; + } + let source_re = unsafe { *arena.current_re.add(source) }; + let source_im = unsafe { *arena.current_im.add(source) }; + unsafe { + *arena.current_re.add(destination) += source_re * scale_re - source_im * scale_im; + *arena.current_im.add(destination) += source_re * scale_im + source_im * scale_re; + } + } + } + DIRECT_STATUS_OK +} + +unsafe extern "C" fn finalize_currents( + _context: *const c_void, + arena: DirectArenaView, + _momenta: DirectMomentumView, + _parameters: DirectParameterView, + factors: DirectFactorView, + rows: *const DirectFinalizationRow, + row_count: u32, + point_count: u32, +) -> i32 { + let rows = unsafe { std::slice::from_raw_parts(rows, row_count as usize) }; + for row in rows { + if row.exact_factor_id >= factors.value_count { + return STATUS_BOUNDS; + } + let factor = unsafe { *factors.values_re.add(row.exact_factor_id as usize) }; + for component in 0..usize::from(row.component_count) { + for point in 0..point_count as usize { + let destination = + (row.component_base as usize + component) * arena.point_stride as usize + point; + if destination >= arena.current_scalar_len as usize { + return STATUS_BOUNDS; + } + unsafe { + *arena.current_re.add(destination) *= factor; + *arena.current_im.add(destination) *= factor; + } + } + } + } + DIRECT_STATUS_OK +} + +unsafe extern "C" fn accumulate_closures( + _context: *const c_void, + arena: DirectArenaView, + _momenta: DirectMomentumView, + _parameters: DirectParameterView, + factors: DirectFactorView, + rows: *const DirectClosureRow, + row_count: u32, + point_count: u32, +) -> i32 { + let rows = unsafe { std::slice::from_raw_parts(rows, row_count as usize) }; + for row in rows { + if row.exact_factor_id >= factors.value_count { + return STATUS_BOUNDS; + } + let factor = unsafe { *factors.values_re.add(row.exact_factor_id as usize) }; + for point in 0..point_count as usize { + let source = row.parent0_component_base as usize * arena.point_stride as usize + point; + let destination = + row.amplitude_destination_id as usize * arena.point_stride as usize + point; + if source >= arena.current_scalar_len as usize + || destination >= arena.amplitude_scalar_len as usize + { + return STATUS_BOUNDS; + } + unsafe { + *arena.amplitude_re.add(destination) += *arena.current_re.add(source) * factor; + *arena.amplitude_im.add(destination) += *arena.current_im.add(source) * factor; + } + } + } + DIRECT_STATUS_OK +} + +fn direct_runtime(strategy: RecurrenceStrategy) -> DirectRecurrenceExecutionRuntime { + let is_union = strategy == RecurrenceStrategy::AllFlowUnion; + let catalog_digest = SemanticDigest::new([0x33; 32]).unwrap(); + let plan = DirectRecurrencePlan::new(DirectRecurrencePlanParts { + strategy, + semantic_digest: SemanticDigest::new([0x11; 32]).unwrap(), + prepared_pack_digest: SemanticDigest::new([0x22; 32]).unwrap(), + direct_template_catalog_digest: catalog_digest, + point_tile_size: POINT_COUNT, + workspace_mib: 1, + current_arena_components: 2, + physical_sector_count: 1, + retained_helicity_count: 1, + amplitude_destination_count: 1, + parameter_value_count: 1, + external_source_count: 1, + state_template_count: 2, + source_template_count: 1, + source_template_or_dispatch_count: 1, + runtime_helicity_contract_count: u32::from(is_union), + runtime_helicity_variant_count: u32::from(is_union), + direct_executor_count: 4, + currents: vec![ + DirectCurrentDescriptor { + semantic_current_id: 0, + node_kind: DirectNodeKind::Source, + state_template_id: 0, + component_base: 0, + component_count: 1, + momentum_form_id: 0, + stage: 0, + selector_domain_id: 0, + first_use: 0, + last_use: 1, + source_row_or_sentinel: 0, + finalization_row_or_sentinel: DIRECT_NONE_U32, + }, + DirectCurrentDescriptor { + semantic_current_id: 1, + node_kind: DirectNodeKind::Current, + state_template_id: 1, + component_base: 1, + component_count: 1, + momentum_form_id: 0, + stage: 1, + selector_domain_id: 0, + first_use: 1, + last_use: 2, + source_row_or_sentinel: DIRECT_NONE_U32, + finalization_row_or_sentinel: 0, + }, + ], + sources: vec![DirectSourceRow { + source_slot: 0, + destination_component_base: 0, + momentum_form_id: 0, + source_template_or_dispatch_domain: 0, + spin_state_class: -1, + exact_factor_id: 0, + selector_domain_id: 0, + }], + contributions: vec![DirectContributionRow { + parent0_component_base: 0, + parent1_component_base_or_sentinel: DIRECT_NONE_U32, + parent0_momentum_form_id: 0, + parent1_momentum_form_id_or_sentinel: DIRECT_NONE_U32, + destination_component_base: 1, + exact_factor_id: 0, + selector_domain_id: 0, + flags: 0, + }], + finalizations: vec![DirectFinalizationRow { + component_base: 1, + component_count: 1, + momentum_form_id: 0, + exact_factor_id: 0, + selector_domain_id: 0, + flags: 0, + }], + closures: vec![DirectClosureRow { + parent0_component_base: 1, + parent1_component_base_or_sentinel: DIRECT_NONE_U32, + parent0_momentum_form_id: 0, + parent1_momentum_form_id_or_sentinel: DIRECT_NONE_U32, + amplitude_destination_id: 0, + exact_factor_id: 0, + component_factor_start: 0, + component_count: 1, + selector_domain_id: 0, + flags: 0, + }], + row_groups: vec![ + DirectRowGroupDescriptor { + stage: 0, + role: DirectExecutorRole::Source, + destination_operation: DirectDestinationOperation::Initialize, + direct_executor_id: if is_union { DIRECT_NONE_U32 } else { 0 }, + row_start: 0, + row_count: 1, + }, + DirectRowGroupDescriptor { + stage: 1, + role: DirectExecutorRole::Contribution, + destination_operation: DirectDestinationOperation::Add, + direct_executor_id: 1, + row_start: 0, + row_count: 1, + }, + DirectRowGroupDescriptor { + stage: 1, + role: DirectExecutorRole::Finalization, + destination_operation: DirectDestinationOperation::FinalizeInPlace, + direct_executor_id: 2, + row_start: 0, + row_count: 1, + }, + DirectRowGroupDescriptor { + stage: 2, + role: DirectExecutorRole::Closure, + destination_operation: DirectDestinationOperation::ClosureAdd, + direct_executor_id: 3, + row_start: 0, + row_count: 1, + }, + ], + momentum_forms: vec![DirectMomentumFormDescriptor { + term_start: 0, + term_count: 1, + }], + momentum_terms: vec![DirectMomentumTerm { + source_slot: 0, + coefficient: 1, + }], + selector_domains: vec![DirectSelectorDomainDescriptor { + word_start: 0, + word_count: 1, + }], + selector_words: vec![1], + replay_targets: if is_union { + vec![] + } else { + vec![DirectReplayTargetDescriptor { + public_flow_id: 0, + representative_id: 0, + source_permutation_start: 0, + source_permutation_count: 1, + helicity_map_start: 0, + helicity_map_count: 1, + phase_exact_factor_id: 0, + multiplicity: 1, + selector_domain_id: 0, + }] + }, + source_permutations: if is_union { vec![] } else { vec![0] }, + replay_momentum_signs: if is_union { vec![] } else { vec![1] }, + replay_helicity_map: if is_union { vec![] } else { vec![0] }, + amplitude_destinations: vec![DirectAmplitudeDestinationDescriptor { + closure_row_start: 0, + id: 0, + target_sector_id: 0, + target_helicity_id_or_sentinel: if is_union { DIRECT_NONE_U32 } else { 0 }, + closure_row_count: 1, + selector_domain_id: 0, + }], + resolved_helicities: vec![DirectResolvedHelicityDescriptor { + source_state_start: 0, + source_selection_start: 0, + public_helicity_start: 0, + id: 0, + source_state_count: 1, + source_selection_count: u32::from(is_union), + public_helicity_count: 1, + selector_domain_id: 0, + }], + source_state_assignments: vec![DirectSourceStateAssignment { + source_slot: 0, + state_index: 0, + }], + source_dispatch_variants: if is_union { + vec![DirectSourceDispatchVariantDescriptor { + embedding_start: 0, + projection_start: 0, + source_row_id: 0, + dispatch_domain_id: 0, + runtime_variant_id: 0, + source_state_index: 0, + source_template_id: 0, + source_state_template_id: 0, + crossed_state_template_id: 0, + crossed_spin_state_class: -1, + direct_executor_id: 0, + crossing_exact_factor_id: 0, + embedding_count: 1, + projection_count: 1, + }] + } else { + vec![] + }, + source_embeddings: if is_union { + vec![DirectSourceEmbeddingRow { + full_component: 0, + source_component_or_sentinel: 0, + exact_factor_id: 0, + }] + } else { + vec![] + }, + source_projections: if is_union { + vec![DirectSourceProjectionRow { + source_component: 0, + full_component: 0, + }] + } else { + vec![] + }, + resolved_source_selections: if is_union { + vec![DirectResolvedSourceSelection { + source_slot: 0, + dispatch_variant_id: 0, + }] + } else { + vec![] + }, + public_helicities: vec![-1], + exact_factors: vec![ExactComplexRational::ONE], + closure_proofs: ClosureProofMetadataV2::new( + vec![ + ClosureProofContributionV2::new( + 0, + 0, + Some(0), + (!is_union).then_some(0), + 0, + SemanticDigest::new([0x41; 32]).unwrap(), + None, + vec![1], + vec![Some(1)], + vec![SemanticDigest::new([0x42; 32]).unwrap()], + vec![SemanticDigest::new([0x43; 32]).unwrap()], + vec![0], + vec![0], + vec![0], + 0, + SemanticDigest::new([0x44; 32]).unwrap(), + None, + vec![], + None, + ExactComplexRational::ONE, + 1, + ) + .unwrap(), + ], + vec![ + ClosureExecutionProofGroupV2::new( + 0, + Some(0), + Some(0), + CheckedTableRange::new(0, 1), + ExactComplexRational::ONE, + closure_component_factor_digest_v2(&[ExactComplexRational::ONE]).unwrap(), + closure_selector_domain_digest_v2(&[1]).unwrap(), + ) + .unwrap(), + ], + vec![], + ) + .unwrap(), + }) + .unwrap(); + + let executors = DirectExecutorCatalog::new( + &plan, + catalog_digest, + vec![ + DirectExecutorHandle::Source { + call: fill_sources, + context: std::ptr::null(), + }, + DirectExecutorHandle::Contribution { + call: accumulate_contributions, + context: std::ptr::null(), + }, + DirectExecutorHandle::Finalization { + call: finalize_currents, + context: std::ptr::null(), + }, + DirectExecutorHandle::Closure { + call: accumulate_closures, + context: std::ptr::null(), + }, + ], + ) + .unwrap(); + let mut runtime = if is_union { + DirectRecurrenceExecutionRuntime::new_with_union_source_dispatch( + plan, + executors, + 4, + DirectUnionSourceDispatchHandle { + call: fill_union_sources, + context: std::ptr::null(), + }, + ) + .unwrap() + } else { + DirectRecurrenceExecutionRuntime::new(plan, executors, 4).unwrap() + }; + runtime.set_parameters(&[3.0], &[1.0]).unwrap(); + runtime +} + +fn topology_replay_runtime() -> DirectRecurrenceExecutionRuntime { + direct_runtime(RecurrenceStrategy::TopologyReplay) +} + +fn all_flow_union_runtime() -> DirectRecurrenceExecutionRuntime { + direct_runtime(RecurrenceStrategy::AllFlowUnion) +} + +fn external_momenta() -> [f64; 16] { + [ + 1.0, 10.0, 100.0, 1000.0, 2.0, 20.0, 200.0, 2000.0, 3.0, 30.0, 300.0, 3000.0, 4.0, 40.0, + 400.0, 4000.0, + ] +} + +#[test] +fn warmed_topology_replay_tiles_allocate_zero_heap_bytes_and_remain_correct() { + let mut runtime = topology_replay_runtime(); + let selector = runtime.prepare_replay_selector(0).unwrap(); + let momenta = external_momenta(); + + let warm_output = runtime + .execute_replay_tile_from_external(&selector, POINT_COUNT, &momenta) + .unwrap(); + assert_eq!(warm_output.destination_re(0).unwrap(), EXPECTED_RE); + assert_eq!(warm_output.destination_im(0).unwrap(), EXPECTED_IM); + let persistent_allocations = runtime.allocation_counters(); + runtime.reset_counters(); + + let (result, allocation_count, allocated_bytes) = count_allocations(|| { + let mut observed_re = [0.0; POINT_COUNT as usize]; + let mut observed_im = [0.0; POINT_COUNT as usize]; + for _ in 0..32 { + let output = runtime.execute_replay_tile_from_external_unprofiled( + &selector, + POINT_COUNT, + &momenta, + )?; + observed_re.copy_from_slice(output.destination_re(0).ok_or_else(|| { + RusticolError::internal("missing direct recurrence real destination") + })?); + observed_im.copy_from_slice(output.destination_im(0).ok_or_else(|| { + RusticolError::internal("missing direct recurrence imaginary destination") + })?); + } + Ok::<_, RusticolError>((observed_re, observed_im)) + }); + + let (observed_re, observed_im) = result.unwrap(); + assert_eq!(observed_re, EXPECTED_RE); + assert_eq!(observed_im, EXPECTED_IM); + assert_eq!(allocation_count, 0, "warmed recurrence loop allocated"); + assert_eq!(allocated_bytes, 0, "warmed recurrence loop allocated bytes"); + assert_eq!(runtime.counters(), Default::default()); + assert_eq!(runtime.role_timings(), Default::default()); + assert_eq!(runtime.activity_counters(), Default::default()); + assert_eq!(runtime.phase_timings(), Default::default()); + assert_eq!(runtime.traffic_counters(), Default::default()); + assert_eq!(runtime.allocation_counters(), persistent_allocations); +} + +#[test] +fn warmed_all_flow_union_tiles_allocate_zero_heap_bytes_and_remain_correct() { + let mut runtime = all_flow_union_runtime(); + let selector = runtime.prepare_union_helicity_selector(0).unwrap(); + let momenta = external_momenta(); + + let warm_output = runtime + .execute_union_tile_from_external(&selector, POINT_COUNT, &momenta) + .unwrap(); + assert_eq!(warm_output.destination_re(0).unwrap(), EXPECTED_RE); + assert_eq!(warm_output.destination_im(0).unwrap(), EXPECTED_IM); + let persistent_allocations = runtime.allocation_counters(); + runtime.reset_counters(); + + let (result, allocation_count, allocated_bytes) = count_allocations(|| { + let mut observed_re = [0.0; POINT_COUNT as usize]; + let mut observed_im = [0.0; POINT_COUNT as usize]; + for _ in 0..32 { + let output = runtime.execute_union_tile_from_external_unprofiled( + &selector, + POINT_COUNT, + &momenta, + )?; + observed_re.copy_from_slice(output.destination_re(0).ok_or_else(|| { + RusticolError::internal("missing direct recurrence real destination") + })?); + observed_im.copy_from_slice(output.destination_im(0).ok_or_else(|| { + RusticolError::internal("missing direct recurrence imaginary destination") + })?); + } + Ok::<_, RusticolError>((observed_re, observed_im)) + }); + + let (observed_re, observed_im) = result.unwrap(); + assert_eq!(observed_re, EXPECTED_RE); + assert_eq!(observed_im, EXPECTED_IM); + assert_eq!( + allocation_count, 0, + "warmed union recurrence loop allocated" + ); + assert_eq!( + allocated_bytes, 0, + "warmed union recurrence loop allocated bytes" + ); + assert_eq!(runtime.counters(), Default::default()); + assert_eq!(runtime.role_timings(), Default::default()); + assert_eq!(runtime.activity_counters(), Default::default()); + assert_eq!(runtime.phase_timings(), Default::default()); + assert_eq!(runtime.traffic_counters(), Default::default()); + assert_eq!(runtime.allocation_counters(), persistent_allocations); +} + +fn genuine_artifact_path(environment_name: &str) -> Option { + let Some(path) = std::env::var_os(environment_name) else { + eprintln!("skipping genuine recurrence allocation proof: {environment_name} is not set"); + return None; + }; + let path = PathBuf::from(path); + assert!( + path.is_dir(), + "{environment_name} does not name an artifact directory: {}", + path.display() + ); + Some(path) +} + +fn validation_momenta(runtime: &NativeRuntime) -> Vec { + let metadata = runtime.metadata(); + let path = runtime + .root() + .join("processes") + .join(&metadata.representative_process_key) + .join("validation-momenta.json"); + let payload: serde_json::Value = + serde_json::from_slice(&fs::read(&path).unwrap_or_else(|error| { + panic!( + "could not read recurrence validation momenta {}: {error}", + path.display() + ) + })) + .unwrap_or_else(|error| { + panic!( + "could not parse recurrence validation momenta {}: {error}", + path.display() + ) + }); + let point = payload["points"][0] + .as_array() + .expect("recurrence validation payload must contain one point"); + assert_eq!( + point.len(), + metadata.external_count, + "recurrence validation point has the wrong external multiplicity" + ); + point + .iter() + .flat_map(|leg| { + leg["momentum"] + .as_array() + .expect("validation leg must contain four momentum components") + .iter() + .map(|component| { + component + .as_str() + .map(str::parse::) + .transpose() + .expect("validation momentum string must be binary64") + .or_else(|| component.as_f64()) + .expect("validation momentum component must be numeric") + }) + }) + .collect() +} + +fn assert_recurrence_layout(artifact: &Path, process_id: &str, expected_layout: &str) { + let path = artifact + .join("processes") + .join(process_id) + .join("execution.json"); + let execution: serde_json::Value = + serde_json::from_slice(&fs::read(&path).unwrap_or_else(|error| { + panic!( + "could not read recurrence execution manifest {}: {error}", + path.display() + ) + })) + .unwrap_or_else(|error| { + panic!( + "could not parse recurrence execution manifest {}: {error}", + path.display() + ) + }); + assert_eq!( + execution["recurrence_summary"]["lc_flow_layout"].as_str(), + Some(expected_layout), + "genuine allocation fixture has the wrong LC layout" + ); + assert_eq!( + execution["required_runtime_capabilities"] + .as_array() + .expect("recurrence capabilities must be an array") + .iter() + .filter_map(serde_json::Value::as_str) + .filter(|capability| { + *capability == "rusticol.recurrence-direct-arena.complex-f64.v1" + }) + .count(), + 1, + "genuine allocation fixture must require Direct-Arena" + ); +} + +fn prove_genuine_artifact_warmed_loop_allocates_nothing(artifact: PathBuf, expected_layout: &str) { + let mut runtime = + NativeRuntime::load(&artifact, None, None).expect("load genuine recurrence artifact"); + let metadata = runtime.metadata(); + assert_eq!(metadata.execution_mode, "recurrence"); + assert_recurrence_layout( + &artifact, + &metadata.representative_process_key, + expected_layout, + ); + + // Loading prepares every replay-flow or union-helicity selector. Momentum + // parsing, output ownership, and SymJIT descriptor creation also stay + // outside the counted region. + let one_point_momenta = validation_momenta(&runtime); + for point_count in [1, 3] { + let momenta = one_point_momenta.repeat(point_count); + let mut output = vec![f64::NAN; point_count]; + runtime + .evaluate_f64_into(&momenta, point_count, &mut output) + .expect("warm genuine recurrence evaluation"); + runtime + .evaluate_f64_into(&momenta, point_count, &mut output) + .expect("warm genuine recurrence descriptor caches"); + let expected = output.clone(); + assert!( + expected.iter().all(|value| value.is_finite()), + "genuine recurrence warmup returned a non-finite total" + ); + + let (result, allocation_count, allocated_bytes) = count_allocations(|| { + for _ in 0..REAL_ARTIFACT_REPETITIONS { + runtime.evaluate_f64_into(&momenta, point_count, &mut output)?; + std::hint::black_box(output.as_slice()); + } + Ok::<(), RusticolError>(()) + }); + result.expect("repeat genuine warmed recurrence evaluation"); + + assert_eq!(output, expected, "genuine recurrence output changed"); + assert_eq!( + allocation_count, 0, + "genuine warmed recurrence loop allocated at {point_count} points" + ); + assert_eq!( + allocated_bytes, 0, + "genuine warmed recurrence loop allocated bytes at {point_count} points" + ); + } +} + +fn prove_genuine_contracted_selector_plan_allocates_nothing(artifact: PathBuf) { + let mut runtime = + NativeRuntime::load(&artifact, None, None).expect("load genuine recurrence artifact"); + let metadata = runtime.metadata(); + assert_eq!(metadata.execution_mode, "recurrence"); + assert_recurrence_layout( + &artifact, + &metadata.representative_process_key, + "contracted-color-union", + ); + let helicity_ids = runtime + .helicity_ids() + .expect("load recurrence helicity IDs"); + assert_eq!( + helicity_ids.len(), + 1, + "contracted allocation artifact must retain exactly one helicity" + ); + let helicity_id = helicity_ids[0].clone(); + let selected_helicities = [helicity_id]; + let selector_plan = runtime + .prepare_recurrence_selector_plan(Some(&selected_helicities), None) + .expect("prepare recurrence selector plan"); + let momenta = validation_momenta(&runtime); + let mut output = [f64::NAN; 1]; + runtime + .evaluate_f64_into_with_recurrence_selector_plan(&selector_plan, &momenta, 1, &mut output) + .expect("warm genuine selected recurrence evaluation"); + let expected = output; + let mut total = [f64::NAN; 1]; + runtime + .evaluate_f64_into(&momenta, 1, &mut total) + .expect("evaluate genuine recurrence through the no-selector total fast path"); + assert_eq!( + total, expected, + "no-selector total must equal the sole retained-helicity selector" + ); + + let (result, allocation_count, allocated_bytes) = count_allocations(|| { + for _ in 0..REAL_ARTIFACT_REPETITIONS { + runtime.evaluate_f64_into_with_recurrence_selector_plan( + &selector_plan, + &momenta, + 1, + &mut output, + )?; + std::hint::black_box(output.as_slice()); + } + Ok::<(), RusticolError>(()) + }); + result.expect("repeat genuine selected recurrence evaluation"); + + assert_eq!(output, expected, "selected recurrence output changed"); + assert_eq!( + allocation_count, 0, + "prepared recurrence selector loop allocated" + ); + assert_eq!( + allocated_bytes, 0, + "prepared recurrence selector loop allocated bytes" + ); +} + +#[test] +fn genuine_topology_replay_artifact_warmed_loop_allocates_zero_heap_bytes() { + let Some(artifact) = genuine_artifact_path(TOPOLOGY_ARTIFACT_ENV) else { + return; + }; + prove_genuine_artifact_warmed_loop_allocates_nothing(artifact, "topology-replay"); +} + +#[test] +fn genuine_all_flow_union_artifact_warmed_loop_allocates_zero_heap_bytes() { + let Some(artifact) = genuine_artifact_path(UNION_ARTIFACT_ENV) else { + return; + }; + prove_genuine_artifact_warmed_loop_allocates_nothing(artifact, "all-flow-union"); +} + +#[test] +fn genuine_contracted_color_artifact_warmed_loop_allocates_zero_heap_bytes() { + let Some(artifact) = genuine_artifact_path(CONTRACTED_ARTIFACT_ENV) else { + return; + }; + prove_genuine_artifact_warmed_loop_allocates_nothing(artifact, "contracted-color-union"); +} + +#[test] +fn genuine_contracted_color_prepared_selector_allocates_zero_heap_bytes() { + let Some(artifact) = genuine_artifact_path(CONTRACTED_ARTIFACT_ENV) else { + return; + }; + prove_genuine_contracted_selector_plan_allocates_nothing(artifact); +} diff --git a/pyamplicol/rust/crates/rusticol-core/tests/recurrence_selected_helicity.rs b/pyamplicol/rust/crates/rusticol-core/tests/recurrence_selected_helicity.rs new file mode 100644 index 0000000000..4ab09847f3 --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-core/tests/recurrence_selected_helicity.rs @@ -0,0 +1,330 @@ +// SPDX-License-Identifier: 0BSD + +#![cfg(all(feature = "f64-symjit", any(target_os = "linux", target_os = "macos")))] + +//! Reuse the genuine topology-replay fixture accepted by the allocation +//! tests. No generation is performed here; the ordinary all-helicity route +//! supplies the independent resolved-component reference. + +use rusticol_core::NativeRuntime; +use std::fs; +use std::path::PathBuf; + +fn assert_close(actual: &[f64], expected: &[f64]) { + assert_eq!(actual.len(), expected.len()); + for (actual, expected) in actual.iter().zip(expected) { + let scale = actual.abs().max(expected.abs()).max(1.0e-200); + assert!( + (actual - expected).abs() <= 5.0e-12 * scale, + "selected recurrence differs: {actual:.17e} versus {expected:.17e}" + ); + } +} + +fn momenta(runtime: &NativeRuntime) -> Vec { + let path = runtime + .root() + .join("processes") + .join(&runtime.metadata().representative_process_key) + .join("validation-momenta.json"); + let payload: serde_json::Value = serde_json::from_slice(&fs::read(path).unwrap()).unwrap(); + let point = payload["points"][0].as_array().unwrap(); + let one_point = point + .iter() + .flat_map(|leg| { + leg["momentum"].as_array().unwrap().iter().map(|value| { + value + .as_str() + .map(|value| value.parse::().unwrap()) + .or_else(|| value.as_f64()) + .unwrap() + }) + }) + .collect::>(); + let mut batch = Vec::new(); + // Include a partial SIMD tile and distinct, on-shell, rotated points. + for angle in [0.0_f64, 0.21, 0.43, 0.76, 1.07] { + for leg in one_point.chunks_exact(4) { + batch.extend([ + leg[0], + angle.cos() * leg[1] + angle.sin() * leg[3], + leg[2], + -angle.sin() * leg[1] + angle.cos() * leg[3], + ]); + } + } + batch +} + +#[test] +fn genuine_selected_recurrence_matches_unrestricted_resolved_components() { + let Some(path) = std::env::var_os("RUSTICOL_RECURRENCE_TOPOLOGY_ALLOCATION_ARTIFACT") else { + return; + }; + let path = PathBuf::from(path); + assert!(path.is_dir(), "missing fixture {}", path.display()); + let mut runtime = NativeRuntime::load(&path, None, None).unwrap(); + assert_eq!(runtime.metadata().execution_mode, "recurrence"); + let helicities = runtime.process_physics().unwrap().helicities.clone(); + let color_ids = runtime.color_ids().unwrap(); + assert!( + helicities.len() > 2, + "fixture must retain generic helicities" + ); + assert!( + color_ids.len() > 1, + "fixture must exercise flow relabelling" + ); + let points = momenta(&runtime); + let point_count = 5; + let mut chosen = helicities + .iter() + .enumerate() + .filter(|(_, helicity)| helicity.computed && !helicity.structural_zero) + .take(2) + .map(|(index, _)| index) + .collect::>(); + if let Some(index) = helicities + .iter() + .position(|helicity| !helicity.computed && !helicity.structural_zero) + { + chosen.push(index); + } + if let Some(index) = helicities + .iter() + .position(|helicity| helicity.structural_zero) + { + chosen.push(index); + } + assert!(chosen.len() >= 2); + + for color_index in [0, color_ids.len() - 1] { + let colors = [color_ids[color_index].clone()]; + let baseline = runtime + .evaluate_resolved_f64(&points, point_count, None, Some(&colors)) + .unwrap(); + let total = runtime + .evaluate_f64_profile_with_selectors( + &points, + point_count, + None, + Some(&colors), + None, + None, + ) + .unwrap(); + let complete_helicities = helicities + .iter() + .map(|helicity| helicity.id.clone()) + .collect::>(); + let explicit_sum = runtime + .evaluate_f64_profile_with_selectors( + &points, + point_count, + Some(&complete_helicities), + Some(&colors), + None, + None, + ) + .unwrap(); + assert_close(&explicit_sum.values, &total.values); + assert_eq!( + explicit_sum.profile.recurrence_closure_row_count, + total.profile.recurrence_closure_row_count, + ); + for &index in &chosen { + let selected = [helicities[index].id.clone()]; + let expected = (0..point_count) + .map(|point| baseline.values[point * helicities.len() + index]) + .collect::>(); + let actual = runtime + .evaluate_f64_with_selectors( + &points, + point_count, + Some(&selected), + Some(&colors), + None, + None, + ) + .unwrap(); + assert_close(&actual, &expected); + let profiled = runtime + .evaluate_f64_profile_with_selectors( + &points, + point_count, + Some(&selected), + Some(&colors), + None, + None, + ) + .unwrap(); + assert_close(&profiled.values, &expected); + if expected.iter().any(|value| *value != 0.0) { + assert!( + profiled.profile.recurrence_closure_row_count + < total.profile.recurrence_closure_row_count, + "selected evaluator still executes all helicity closures" + ); + } + let resolved = runtime + .evaluate_resolved_f64(&points, point_count, Some(&selected), Some(&colors)) + .unwrap(); + assert_close(&resolved.values, &expected); + + // Restore the unrestricted route, then select the same cached + // route again. Reused arenas must not leak stale amplitudes. + let sum_again = runtime + .evaluate_f64_with_selectors(&points, point_count, None, Some(&colors), None, None) + .unwrap(); + assert_close(&sum_again, &total.values); + let selected_again = runtime + .evaluate_f64_with_selectors( + &points, + point_count, + Some(&selected), + Some(&colors), + None, + None, + ) + .unwrap(); + assert_close(&selected_again, &expected); + } + let mut indices = chosen.clone(); + indices.sort_unstable(); + indices.dedup(); + let selected = indices + .iter() + .map(|index| helicities[*index].id.clone()) + .collect::>(); + let expected = (0..point_count) + .map(|point| { + indices + .iter() + .map(|index| baseline.values[point * helicities.len() + index]) + .sum() + }) + .collect::>(); + let subset = runtime + .evaluate_f64_with_selectors( + &points, + point_count, + Some(&selected), + Some(&colors), + None, + None, + ) + .unwrap(); + assert_close(&subset, &expected); + + let by_point = (0..point_count) + .map(|point| chosen[point % chosen.len()] as u32) + .collect::>(); + let expected = by_point + .iter() + .enumerate() + .map(|(point, index)| baseline.values[point * helicities.len() + *index as usize]) + .collect::>(); + let point_selected = runtime + .evaluate_f64_with_selectors( + &points, + point_count, + None, + Some(&colors), + Some(&by_point), + None, + ) + .unwrap(); + assert_close(&point_selected, &expected); + assert!( + runtime + .evaluate_f64_with_selectors( + &points, + point_count, + Some(&[]), + Some(&colors), + None, + None + ) + .is_err() + ); + assert!( + runtime + .evaluate_f64_with_selectors( + &points, + point_count, + Some(&["missing-helicity".to_string()]), + Some(&colors), + None, + None, + ) + .is_err() + ); + } +} + +#[cfg(feature = "symbolica-runtime")] +#[test] +fn selected_compiled_replay_preserves_double_double_and_arb_components() { + let Some(path) = std::env::var_os("RUSTICOL_COMPILED_SELECTED_HELICITY_ARTIFACT") else { + return; + }; + let mut runtime = NativeRuntime::load(&PathBuf::from(path), None, None).unwrap(); + assert_eq!(runtime.metadata().execution_mode, "compiled"); + let helicities = runtime.process_physics().unwrap().helicities.clone(); + let colors = runtime.color_ids().unwrap(); + assert!(helicities.len() > 2 && colors.len() > 1); + let colors = [colors[colors.len() - 1].clone()]; + let mut chosen = helicities + .iter() + .enumerate() + .filter(|(_, helicity)| helicity.computed && !helicity.structural_zero) + .take(2) + .map(|(index, _)| index) + .collect::>(); + if let Some(index) = helicities + .iter() + .position(|helicity| !helicity.computed && !helicity.structural_zero) + { + chosen.push(index); + } + let points = momenta(&runtime) + .into_iter() + .map(|value| format!("{value:.17e}")) + .collect::>(); + for digits in [32, 64] { + let baseline = runtime + .evaluate_resolved_with_precision(&points, 5, digits, None, Some(&colors)) + .unwrap(); + for &index in &chosen { + let selected = [helicities[index].id.clone()]; + let expected = (0..5) + .map(|point| { + baseline.values[point * helicities.len() + index] + .parse() + .unwrap() + }) + .collect::>(); + let actual = runtime + .evaluate_resolved_with_precision( + &points, + 5, + digits, + Some(&selected), + Some(&colors), + ) + .unwrap(); + assert_close( + &actual + .values + .iter() + .map(|value| value.parse().unwrap()) + .collect::>(), + &expected, + ); + let sum_again = runtime + .evaluate_resolved_with_precision(&points, 5, digits, None, Some(&colors)) + .unwrap(); + assert_eq!(sum_again.values, baseline.values); + } + } +} diff --git a/pyamplicol/rust/crates/rusticol-python/Cargo.toml b/pyamplicol/rust/crates/rusticol-python/Cargo.toml new file mode 100644 index 0000000000..866058789b --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-python/Cargo.toml @@ -0,0 +1,35 @@ +# SPDX-License-Identifier: 0BSD + +[package] +name = "rusticol-python" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +authors.workspace = true +repository.workspace = true +build = "build.rs" + +[lib] +name = "_rusticol" +crate-type = ["cdylib", "rlib"] + +[features] +default = [] +extension-module = ["pyo3/extension-module"] +numpy = ["dep:numpy"] +on-the-fly-test-support = ["rusticol-core/on-the-fly-test-support"] + +[dependencies] +flate2.workspace = true +numpy = { workspace = true, optional = true } +num-bigint.workspace = true +num-rational.workspace = true +num-traits.workspace = true +pyo3.workspace = true +rusticol-core = { path = "../rusticol-core", default-features = false, features = ["f64-compiled", "f64-symjit", "python-generation-bridge"] } +serde_json.workspace = true +sha2.workspace = true + +[build-dependencies] +pyo3-build-config = "0.29" diff --git a/pyamplicol/rust/crates/rusticol-python/build.rs b/pyamplicol/rust/crates/rusticol-python/build.rs new file mode 100644 index 0000000000..057c9fb516 --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-python/build.rs @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: 0BSD + +use std::env; +use std::fs; +use std::io; +use std::path::{Path, PathBuf}; + +fn copy_tree(source: &Path, destination: &Path) -> io::Result<()> { + fs::create_dir_all(destination)?; + for entry in fs::read_dir(source)? { + let entry = entry?; + let file_type = entry.file_type()?; + let target = destination.join(entry.file_name()); + if file_type.is_symlink() { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "SDK staging may not contain symlinks", + )); + } + if file_type.is_dir() { + copy_tree(&entry.path(), &target)?; + } else if file_type.is_file() { + fs::copy(entry.path(), target)?; + } else { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "SDK staging must contain regular files only", + )); + } + } + Ok(()) +} + +fn main() { + pyo3_build_config::add_extension_module_link_args(); + println!("cargo:rerun-if-env-changed=PYAMPLICOL_SDK_STAGING"); + println!("cargo:rerun-if-env-changed=PYAMPLICOL_NATIVE_BUILD_INPUTS_SHA256"); + let output = PathBuf::from(env::var_os("OUT_DIR").expect("OUT_DIR")); + let sdk_output = output.join("_sdk"); + match fs::remove_dir_all(&sdk_output) { + Ok(()) => {} + Err(error) if error.kind() == io::ErrorKind::NotFound => {} + Err(error) => panic!("remove previous Rusticol SDK from OUT_DIR: {error}"), + } + let Some(staging) = env::var_os("PYAMPLICOL_SDK_STAGING") else { + return; + }; + let source = PathBuf::from(staging); + copy_tree(&source, &sdk_output).expect("copy validated Rusticol SDK into OUT_DIR"); +} diff --git a/pyamplicol/rust/crates/rusticol-python/src/eager_lowering.rs b/pyamplicol/rust/crates/rusticol-python/src/eager_lowering.rs new file mode 100644 index 0000000000..b568cf2f7c --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-python/src/eager_lowering.rs @@ -0,0 +1,1110 @@ +// SPDX-License-Identifier: 0BSD + +//! Private Python boundary for compact eager plan-v3 lowering. + +use numpy::{PyReadonlyArrayDyn, PyUntypedArrayMethods}; +use pyo3::exceptions::{PyTypeError, PyValueError}; +use pyo3::prelude::*; +use pyo3::types::{PyAny, PyDict, PyList}; +use rusticol_core::eager_layout::{ + EAGER_DIRECT_ARENA_RUNTIME_CAPABILITY, EAGER_LOWERING_INPUT_ABI, EAGER_PLAN_ABI, + EAGER_RUNTIME_CAPABILITY, EAGER_RUNTIME_CONTAINER_KIND, EAGER_RUNTIME_CONTAINER_SCHEMA, + EAGER_RUNTIME_LAYOUT_ABI, +}; +use rusticol_core::{ + EAGER_LOWERING_V1_TABLE_NAMES, EagerBitsetColumns, EagerCoherentGroupColumns, + EagerColorContractionEntryColumns, EagerColorContractionMetadata, EagerColorSelectorColumns, + EagerContractionCoefficientColumns, EagerCouplingColumns, EagerCurrentColumns, + EagerExactFactorColumns, EagerHelicitySelectorColumns, EagerI32SequenceColumns, + EagerInteractionColumns, EagerInteractionGroupColumns, EagerLoweringInputV1, + EagerLoweringInputV1View, EagerModelParameterColumns, EagerMomentumMaskColumns, + EagerPrimitiveColumnView, EagerReductionMemberColumns, EagerRetainedColumnView, + EagerRetainedTableView, EagerRootColumns, EagerSourceColumns, EagerU32SequenceColumns, + RusticolError, RusticolResult, lower_eager_plan_v3, write_eager_plan_v3_pacbin, +}; +use sha2::{Digest, Sha256}; +use std::collections::{BTreeMap, BTreeSet}; +use std::path::PathBuf; + +use crate::python_error; + +const INPUT_SCHEMA_SHA256: &str = + "59d5a3193ca25af6224c296297425782c394a34e352be622f47045e90d2bce29"; +const RESULT_KIND: &str = "pyamplicol-eager-runtime-lowering-result"; +const RESULT_SCHEMA_VERSION: u32 = 1; +const STORAGE_ABI: &str = "pacbin-v1"; +const SELECTOR_WORK_ABI: &str = "pyamplicol-eager-selector-work-v1"; + +struct NativeLoweringResult { + process_key: String, + model_name: String, + member_count: u64, + unpacked_size_bytes: u64, + index_sha256: String, + current_count: usize, + value_count: usize, + momentum_count: usize, + source_count: usize, + parameter_count: usize, + stage_count: usize, + coupling_count: usize, + invocation_count: usize, + attachment_count: usize, + finalization_count: usize, + closure_count: usize, + selector_domain_count: usize, + reduction_group_count: usize, + reduction_entry_count: usize, + retained_table_count: usize, + current_component_count: u64, + value_component_count: u64, + momentum_component_count: u64, + selected_flow_selector_count: u64, + selected_flow_current_count: u64, + selected_flow_evaluation_count: u64, + selected_flow_attachment_count: u64, + all_flow_selector_count: u64, + all_flow_current_count: u64, + all_flow_evaluation_count: u64, + all_flow_attachment_count: u64, + contracted_current_count: u64, + contracted_evaluation_count: u64, + contracted_attachment_count: u64, +} + +enum BorrowedValues<'py> { + U8(PyReadonlyArrayDyn<'py, u8>), + U32(PyReadonlyArrayDyn<'py, u32>), + U64(PyReadonlyArrayDyn<'py, u64>), + I32(PyReadonlyArrayDyn<'py, i32>), + F64(PyReadonlyArrayDyn<'py, f64>), +} + +impl BorrowedValues<'_> { + fn raw_bytes(&self) -> PyResult<&[u8]> { + fn bytes(values: &[T]) -> &[u8] { + // All accepted dtypes are explicit little-endian primitives. + unsafe { + std::slice::from_raw_parts( + values.as_ptr().cast::(), + std::mem::size_of_val(values), + ) + } + } + match self { + Self::U8(values) => values.as_slice().map_err(not_contiguous), + Self::U32(values) => values.as_slice().map(bytes).map_err(not_contiguous), + Self::U64(values) => values.as_slice().map(bytes).map_err(not_contiguous), + Self::I32(values) => values.as_slice().map(bytes).map_err(not_contiguous), + Self::F64(values) => values.as_slice().map(bytes).map_err(not_contiguous), + } + } + + fn primitive_view(&self) -> RusticolResult> { + match self { + Self::U8(values) => Ok(EagerPrimitiveColumnView::U8( + values.as_slice().map_err(core_not_contiguous)?, + )), + Self::U32(values) => Ok(EagerPrimitiveColumnView::U32( + values.as_slice().map_err(core_not_contiguous)?, + )), + Self::U64(values) => Ok(EagerPrimitiveColumnView::U64( + values.as_slice().map_err(core_not_contiguous)?, + )), + Self::I32(values) => Ok(EagerPrimitiveColumnView::I32( + values.as_slice().map_err(core_not_contiguous)?, + )), + Self::F64(values) => Ok(EagerPrimitiveColumnView::F64( + values.as_slice().map_err(core_not_contiguous)?, + )), + } + } + + fn as_u8(&self, context: &str) -> RusticolResult<&[u8]> { + match self.primitive_view()? { + EagerPrimitiveColumnView::U8(values) => Ok(values), + _ => Err(wrong_type(context, "u8")), + } + } + + fn as_u32(&self, context: &str) -> RusticolResult<&[u32]> { + match self.primitive_view()? { + EagerPrimitiveColumnView::U32(values) => Ok(values), + _ => Err(wrong_type(context, "u32")), + } + } + + fn as_u64(&self, context: &str) -> RusticolResult<&[u64]> { + match self.primitive_view()? { + EagerPrimitiveColumnView::U64(values) => Ok(values), + _ => Err(wrong_type(context, "u64")), + } + } + + fn as_i32(&self, context: &str) -> RusticolResult<&[i32]> { + match self.primitive_view()? { + EagerPrimitiveColumnView::I32(values) => Ok(values), + _ => Err(wrong_type(context, "i32")), + } + } + + fn as_f64(&self, context: &str) -> RusticolResult<&[f64]> { + match self.primitive_view()? { + EagerPrimitiveColumnView::F64(values) => Ok(values), + _ => Err(wrong_type(context, "f64")), + } + } +} + +struct BorrowedColumn<'py> { + name: String, + shape: Vec, + values: BorrowedValues<'py>, +} + +struct BorrowedTable<'py> { + name: String, + row_count: u64, + columns: Vec>, + column_by_name: BTreeMap, +} + +impl BorrowedTable<'_> { + fn column(&self, name: &str) -> RusticolResult<&BorrowedValues<'_>> { + let index = self.column_by_name.get(name).ok_or_else(|| { + RusticolError::invalid_argument(format!( + "eager lowering table {:?} has no column {name:?}", + self.name + )) + })?; + Ok(&self.columns[*index].values) + } +} + +struct BorrowedInput<'py> { + abi: String, + process_key: String, + model_name: String, + string_catalog: Vec, + canonical_ir_catalog: Vec, + semantic_limitations: Vec, + tables: Vec>, + table_by_name: BTreeMap, + digest: String, +} + +impl BorrowedInput<'_> { + fn table(&self, name: &str) -> RusticolResult<&BorrowedTable<'_>> { + let index = self.table_by_name.get(name).ok_or_else(|| { + RusticolError::invalid_argument(format!("eager lowering input has no table {name:?}")) + })?; + Ok(&self.tables[*index]) + } + + fn column(&self, table: &str, column: &str) -> RusticolResult<&BorrowedValues<'_>> { + self.table(table)?.column(column) + } + + fn into_core_input(&self) -> RusticolResult { + let strings = self + .string_catalog + .iter() + .map(String::as_str) + .collect::>(); + let canonical_ir = self + .canonical_ir_catalog + .iter() + .map(String::as_str) + .collect::>(); + let limitations = self + .semantic_limitations + .iter() + .map(String::as_str) + .collect::>(); + let retained_columns = + self.tables + .iter() + .map(|table| { + table + .columns + .iter() + .filter(|column| !is_consumed_column(&table.name, &column.name)) + .map(|column| { + let elements_per_row = column.shape.iter().skip(1).try_fold( + 1_u32, + |product, dimension| { + let dimension = u32::try_from(*dimension).map_err(|_| { + RusticolError::invalid_argument(format!( + "{}.{} trailing shape exceeds u32", + table.name, column.name + )) + })?; + product.checked_mul(dimension).ok_or_else(|| { + RusticolError::invalid_argument(format!( + "{}.{} elements per row exceed u32", + table.name, column.name + )) + }) + }, + )?; + Ok(EagerRetainedColumnView { + name: &column.name, + elements_per_row, + values: column.values.primitive_view()?, + }) + }) + .collect::>>() + }) + .collect::>>()?; + let retained_tables = self + .tables + .iter() + .zip(&retained_columns) + .map(|(table, columns)| EagerRetainedTableView { + name: &table.name, + row_count: table.row_count, + columns, + }) + .collect::>(); + let u8c = |table, column| { + self.column(table, column)? + .as_u8(&format!("{table}.{column}")) + }; + let u8x2 = |table, column| -> RusticolResult<&[[u8; 2]]> { + let values = u8c(table, column)?; + if values.len() % 2 != 0 { + return Err(RusticolError::invalid_argument(format!( + "{table}.{column} has an invalid flattened shape" + ))); + } + Ok(unsafe { + std::slice::from_raw_parts(values.as_ptr().cast::<[u8; 2]>(), values.len() / 2) + }) + }; + let u32c = |table, column| { + self.column(table, column)? + .as_u32(&format!("{table}.{column}")) + }; + let u64c = |table, column| { + self.column(table, column)? + .as_u64(&format!("{table}.{column}")) + }; + let i32c = |table, column| { + self.column(table, column)? + .as_i32(&format!("{table}.{column}")) + }; + let f64c = |table, column| { + self.column(table, column)? + .as_f64(&format!("{table}.{column}")) + }; + let contraction = self.table("color_contraction_metadata")?; + if contraction.row_count != 1 { + return Err(RusticolError::invalid_argument( + "color_contraction_metadata must contain exactly one row", + )); + } + EagerLoweringInputV1::try_from_view(EagerLoweringInputV1View { + abi: &self.abi, + process_key: &self.process_key, + model_name: &self.model_name, + string_catalog: &strings, + canonical_ir_catalog: &canonical_ir, + semantic_limitations: &limitations, + retained_tables: &retained_tables, + unsupported_tables: &[], + currents: EagerCurrentColumns { + id: u32c("currents", "id")?, + dimension: u32c("currents", "dimension")?, + is_source: u8c("currents", "is_source")?, + momentum_mask_bitset_id: u32c("currents", "momentum_mask_bitset_id")?, + propagator_ir_id: u32c("currents", "propagator_ir_id")?, + propagator_kernel_id: u32c("currents", "propagator_kernel_id")?, + }, + sources: EagerSourceColumns { + source_id: u32c("sources", "source_id")?, + current_id: u32c("sources", "current_id")?, + external_label: u32c("sources", "external_label")?, + input_momentum_slot: u32c("sources", "input_momentum_slot")?, + source_ir_id: u32c("sources", "source_ir_id")?, + crossing_ir_id: u32c("sources", "crossing_ir_id")?, + crossing_factor_id: u32c("sources", "crossing_factor_id")?, + declared_state_index: u32c("sources", "declared_state_index")?, + }, + interactions: EagerInteractionColumns { + id: u32c("interactions", "id")?, + stage_subset_size: u32c("interactions", "stage_subset_size")?, + left_current_id: u32c("interactions", "left_current_id")?, + right_current_id: u32c("interactions", "right_current_id")?, + result_current_id: u32c("interactions", "result_current_id")?, + coupling_id: u32c("interactions", "coupling_id")?, + coupling_factor_id: u32c("interactions", "coupling_factor_id")?, + color_factor_id: u32c("interactions", "color_factor_id")?, + evaluation_factor_id: u32c("interactions", "evaluation_factor_id")?, + evaluation_group_id: u32c("interactions", "evaluation_group_id")?, + full_tensor_network_ready: u8c("interactions", "full_tensor_network_ready")?, + kernel_id: u32c("interactions", "kernel_id")?, + canonical_input_order: u8x2("interactions", "canonical_input_order")?, + kernel_normalization_factor_id: u32c( + "interactions", + "kernel_normalization_factor_id", + )?, + output_factor_source: u8c("interactions", "output_factor_source")?, + }, + roots: EagerRootColumns { + id: u32c("roots", "id")?, + left_current_id: u32c("roots", "left_current_id")?, + right_current_id: u32c("roots", "right_current_id")?, + color_factor_id: u32c("roots", "color_factor_id")?, + contraction_ir_id: u32c("roots", "contraction_ir_id")?, + coupling_id: u32c("roots", "coupling_id")?, + coupling_factor_id: u32c("roots", "coupling_factor_id")?, + helicity_weight_factor_id: u32c("roots", "helicity_weight_factor_id")?, + coherent_group_id: u32c("roots", "coherent_group_id")?, + kernel_id: u32c("roots", "kernel_id")?, + canonical_input_order: u8x2("roots", "canonical_input_order")?, + kernel_normalization_factor_id: u32c("roots", "kernel_normalization_factor_id")?, + output_factor_source: u8c("roots", "output_factor_source")?, + }, + interaction_groups: EagerInteractionGroupColumns { + representative_interaction_id: u32c( + "interaction_groups", + "representative_interaction_id", + )?, + member_start: u64c("interaction_groups", "member_start")?, + member_count: u64c("interaction_groups", "member_count")?, + member_interaction_id: u32c("interaction_group_members", "interaction_id")?, + }, + momentum_masks: EagerMomentumMaskColumns { + slot_id: u32c("momentum_masks", "slot_id")?, + bitset_id: u32c("momentum_masks", "bitset_id")?, + }, + bitsets: EagerBitsetColumns { + start: u64c("bitset_ranges", "start")?, + count: u64c("bitset_ranges", "count")?, + bit_count: u64c("bitset_ranges", "bit_count")?, + words: u64c("bitset_words", "value")?, + }, + u32_sequences: EagerU32SequenceColumns { + start: u64c("u32_sequence_ranges", "start")?, + count: u64c("u32_sequence_ranges", "count")?, + values: u32c("u32_sequence_values", "value")?, + }, + i32_sequences: EagerI32SequenceColumns { + start: u64c("i32_sequence_ranges", "start")?, + count: u64c("i32_sequence_ranges", "count")?, + values: i32c("i32_sequence_values", "value")?, + }, + exact_factors: EagerExactFactorColumns { + real: f64c("exact_factors", "real")?, + imaginary: f64c("exact_factors", "imaginary")?, + canonical_string_id: u32c("exact_factors", "canonical_string_id")?, + exact_source: u8c("exact_factors", "exact_source")?, + exact_ir_id: u32c("exact_factors", "exact_ir_id")?, + source_ir_id: u32c("exact_factors", "source_ir_id")?, + }, + couplings: EagerCouplingColumns { + constant_factor_id: u32c("couplings", "constant_factor_id")?, + parameter_name_ids_sequence_id: u32c( + "couplings", + "parameter_name_ids_sequence_id", + )?, + }, + model_parameters: EagerModelParameterColumns { + name_string_id: u32c("model_parameters", "name_string_id")?, + kind_string_id: u32c("model_parameters", "kind_string_id")?, + default_value: f64c("model_parameters", "default_value")?, + default_factor_id: u32c("model_parameters", "default_factor_id")?, + runtime_name_string_id: u32c("model_parameters", "runtime_name_string_id")?, + complex_component: i32c("model_parameters", "complex_component")?, + derived: u8c("model_parameters", "derived")?, + }, + contraction_coefficients: EagerContractionCoefficientColumns { + contraction_ir_id: u32c("contraction_coefficients", "contraction_ir_id")?, + component_index: u32c("contraction_coefficients", "component_index")?, + factor_id: u32c("contraction_coefficients", "factor_id")?, + }, + coherent_groups: EagerCoherentGroupColumns { + id: u32c("coherent_groups", "id")?, + helicity_weight_factor_id: u32c("coherent_groups", "helicity_weight_factor_id")?, + all_sector_weight_factor_id: u32c( + "coherent_groups", + "all_sector_weight_factor_id", + )?, + }, + helicity_selectors: EagerHelicitySelectorColumns { + values_sequence_id: u32c("helicity_selectors", "values_sequence_id")?, + representative_sequence_id: u32c( + "helicity_selectors", + "representative_sequence_id", + )?, + coefficient_factor_id: u32c("helicity_selectors", "coefficient_factor_id")?, + computed: u8c("helicity_selectors", "computed")?, + structural_zero: u8c("helicity_selectors", "structural_zero")?, + }, + color_selectors: EagerColorSelectorColumns { + word_sequence_id: u32c("color_selectors", "word_sequence_id")?, + representative_word_sequence_id: u32c( + "color_selectors", + "representative_word_sequence_id", + )?, + coefficient_factor_id: u32c("color_selectors", "coefficient_factor_id")?, + computed: u8c("color_selectors", "computed")?, + }, + reduction_members: EagerReductionMemberColumns { + coherent_group_id: u32c("reduction_members", "coherent_group_id")?, + helicity_selector_id: u32c("reduction_members", "helicity_selector_id")?, + color_selector_id: u32c("reduction_members", "color_selector_id")?, + }, + color_contraction: EagerColorContractionMetadata { + present: u8c("color_contraction_metadata", "present")?[0], + supported: u8c("color_contraction_metadata", "supported")?[0], + group_count: u64c("color_contraction_metadata", "group_count")?[0], + includes_color_factor: u8c("color_contraction_metadata", "includes_color_factor")? + [0], + }, + color_contraction_entries: EagerColorContractionEntryColumns { + left_group_id: u32c("color_contraction_entries", "left_group_id")?, + right_group_id: u32c("color_contraction_entries", "right_group_id")?, + weight_factor_id: u32c("color_contraction_entries", "weight_factor_id")?, + symmetry_factor_id: u32c("color_contraction_entries", "symmetry_factor_id")?, + }, + }) + } +} + +#[pyfunction] +pub(crate) fn _lower_eager_runtime_v1( + py: Python<'_>, + lowering_input: &Bound<'_, PyAny>, + destination: PathBuf, +) -> PyResult> { + let borrowed = parse_input(lowering_input)?; + let owned = borrowed.into_core_input().map_err(python_error)?; + let input_digest = borrowed.digest.clone(); + let native = py + .detach(move || { + let plan = lower_eager_plan_v3(owned)?; + let selector_work = plan.selector_work_summary()?; + let result = NativeLoweringResult { + process_key: plan.process_key().to_owned(), + model_name: plan.model_name().to_owned(), + current_count: plan.currents().len(), + value_count: plan.values().len(), + momentum_count: plan.momenta().len(), + source_count: plan.sources().len(), + parameter_count: plan.parameters().len(), + stage_count: plan.stages().len(), + coupling_count: plan.couplings().len(), + invocation_count: plan.invocations().len(), + attachment_count: plan.attachments().len(), + finalization_count: plan.finalizations().len(), + closure_count: plan.closures().len(), + selector_domain_count: plan.selector_domains().len(), + reduction_group_count: plan.reduction_groups().len(), + reduction_entry_count: plan.reduction_entries().len(), + retained_table_count: plan.retained_tables().len(), + current_component_count: plan.current_component_count(), + value_component_count: plan.value_component_count(), + momentum_component_count: plan.momentum_component_count(), + selected_flow_selector_count: selector_work.selected_flow_selector_count, + selected_flow_current_count: selector_work.selected_flow_worst.current_count, + selected_flow_evaluation_count: selector_work.selected_flow_worst.evaluation_count, + selected_flow_attachment_count: selector_work.selected_flow_worst.attachment_count, + all_flow_selector_count: selector_work.all_flow_selector_count, + all_flow_current_count: selector_work.all_flow_worst.current_count, + all_flow_evaluation_count: selector_work.all_flow_worst.evaluation_count, + all_flow_attachment_count: selector_work.all_flow_worst.attachment_count, + contracted_current_count: selector_work.contracted.current_count, + contracted_evaluation_count: selector_work.contracted.evaluation_count, + contracted_attachment_count: selector_work.contracted.attachment_count, + member_count: 0, + unpacked_size_bytes: 0, + index_sha256: String::new(), + }; + let metadata = write_eager_plan_v3_pacbin(&plan, &destination)?; + Ok::<_, RusticolError>(NativeLoweringResult { + member_count: metadata.member_count, + unpacked_size_bytes: metadata.unpacked_bytes, + index_sha256: hex_digest(metadata.index_sha256), + ..result + }) + }) + .map_err(python_error)?; + result_mapping(py, &input_digest, native) +} + +fn result_mapping( + py: Python<'_>, + input_digest: &str, + native: NativeLoweringResult, +) -> PyResult> { + let result = PyDict::new(py); + result.set_item("kind", RESULT_KIND)?; + result.set_item("schema_version", RESULT_SCHEMA_VERSION)?; + result.set_item("lowering_input_abi", EAGER_LOWERING_INPUT_ABI)?; + result.set_item("lowering_input_sha256", input_digest)?; + result.set_item("eager_plan_abi", EAGER_PLAN_ABI)?; + result.set_item("runtime_layout_abi", EAGER_RUNTIME_LAYOUT_ABI)?; + result.set_item( + "required_runtime_capabilities", + PyList::new( + py, + [ + EAGER_DIRECT_ARENA_RUNTIME_CAPABILITY, + EAGER_RUNTIME_CAPABILITY, + ], + )?, + )?; + let container = PyDict::new(py); + container.set_item("kind", EAGER_RUNTIME_CONTAINER_KIND)?; + container.set_item("schema_version", EAGER_RUNTIME_CONTAINER_SCHEMA)?; + container.set_item("storage_abi", STORAGE_ABI)?; + container.set_item("member_count", native.member_count)?; + container.set_item("unpacked_size_bytes", native.unpacked_size_bytes)?; + container.set_item("index_sha256", native.index_sha256)?; + result.set_item("runtime_container", container)?; + + let inspection = PyDict::new(py); + inspection.set_item("execution_mode", "eager")?; + inspection.set_item("eager_plan_abi", EAGER_PLAN_ABI)?; + inspection.set_item("runtime_layout_abi", EAGER_RUNTIME_LAYOUT_ABI)?; + inspection.set_item("process_id", native.process_key)?; + inspection.set_item("model_name", native.model_name)?; + inspection.set_item("current_count", native.current_count)?; + inspection.set_item("value_count", native.value_count)?; + inspection.set_item("momentum_count", native.momentum_count)?; + inspection.set_item("source_count", native.source_count)?; + inspection.set_item("parameter_count", native.parameter_count)?; + inspection.set_item("stage_count", native.stage_count)?; + inspection.set_item("coupling_count", native.coupling_count)?; + inspection.set_item("invocation_count", native.invocation_count)?; + inspection.set_item("attachment_count", native.attachment_count)?; + inspection.set_item("finalization_count", native.finalization_count)?; + inspection.set_item("closure_count", native.closure_count)?; + inspection.set_item("selector_domain_count", native.selector_domain_count)?; + inspection.set_item("reduction_group_count", native.reduction_group_count)?; + inspection.set_item("reduction_entry_count", native.reduction_entry_count)?; + inspection.set_item("retained_table_count", native.retained_table_count)?; + inspection.set_item("current_component_count", native.current_component_count)?; + inspection.set_item("value_component_count", native.value_component_count)?; + inspection.set_item("momentum_component_count", native.momentum_component_count)?; + let selector_work = PyDict::new(py); + selector_work.set_item("abi", SELECTOR_WORK_ABI)?; + selector_work.set_item( + "selected_flow_selector_count", + native.selected_flow_selector_count, + )?; + selector_work.set_item( + "selected_flow_current_count", + native.selected_flow_current_count, + )?; + selector_work.set_item( + "selected_flow_evaluation_count", + native.selected_flow_evaluation_count, + )?; + selector_work.set_item( + "selected_flow_attachment_count", + native.selected_flow_attachment_count, + )?; + selector_work.set_item("all_flow_selector_count", native.all_flow_selector_count)?; + selector_work.set_item("all_flow_current_count", native.all_flow_current_count)?; + selector_work.set_item( + "all_flow_evaluation_count", + native.all_flow_evaluation_count, + )?; + selector_work.set_item( + "all_flow_attachment_count", + native.all_flow_attachment_count, + )?; + selector_work.set_item("contracted_current_count", native.contracted_current_count)?; + selector_work.set_item( + "contracted_evaluation_count", + native.contracted_evaluation_count, + )?; + selector_work.set_item( + "contracted_attachment_count", + native.contracted_attachment_count, + )?; + inspection.set_item("selector_work", selector_work)?; + result.set_item("inspection_summary", inspection)?; + Ok(result.into_any().unbind()) +} + +fn parse_input<'py>(input: &Bound<'py, PyAny>) -> PyResult> { + if cfg!(not(target_endian = "little")) { + return Err(PyValueError::new_err( + "eager lowering input v1 requires a little-endian target", + )); + } + let abi = required_string(input, "abi")?; + if abi != EAGER_LOWERING_INPUT_ABI { + return Err(PyValueError::new_err(format!( + "unsupported eager lowering input ABI {abi:?}" + ))); + } + let process_key = nonempty_string(input, "process_key")?; + let model_name = nonempty_string(input, "model_name")?; + let string_catalog = unique_strings(input, "string_catalog", false)?; + let canonical_ir_catalog = unique_strings(input, "canonical_ir_catalog", false)?; + let semantic_limitations = unique_strings(input, "semantic_limitations", true)?; + + let mut content_digest = Sha256::new(); + for value in [&abi, &process_key, &model_name] { + hash_text(&mut content_digest, value); + } + for catalog in [&string_catalog, &canonical_ir_catalog] { + hash_u64(&mut content_digest, catalog.len())?; + for value in catalog { + hash_text(&mut content_digest, value); + } + } + + let tables_object = input.getattr("tables")?; + let table_objects = tables_object + .try_iter() + .map_err(|_| PyTypeError::new_err("eager lowering tables must be iterable"))? + .collect::>>()?; + hash_u64(&mut content_digest, table_objects.len())?; + let mut schema_digest = Sha256::new(); + hash_u64(&mut schema_digest, table_objects.len())?; + let mut tables = Vec::with_capacity(table_objects.len()); + let mut table_by_name = BTreeMap::new(); + let mut previous_table_name: Option = None; + for table_object in table_objects { + let table_name = nonempty_string(&table_object, "name")?; + if previous_table_name + .as_deref() + .is_some_and(|name| name >= table_name.as_str()) + { + return Err(PyValueError::new_err( + "eager lowering table names must be unique and sorted", + )); + } + previous_table_name = Some(table_name.clone()); + let row_count = table_object.getattr("row_count")?.extract::()?; + hash_text(&mut content_digest, &table_name); + content_digest.update(row_count.to_le_bytes()); + hash_text(&mut schema_digest, &table_name); + + let columns_object = table_object.getattr("columns")?; + let column_objects = columns_object + .try_iter() + .map_err(|_| PyTypeError::new_err("eager lowering columns must be iterable"))? + .collect::>>()?; + hash_u32(&mut content_digest, column_objects.len())?; + hash_u32(&mut schema_digest, column_objects.len())?; + let mut columns = Vec::with_capacity(column_objects.len()); + let mut column_by_name = BTreeMap::new(); + for column_object in column_objects { + let column_name = nonempty_string(&column_object, "name")?; + if column_by_name.contains_key(&column_name) { + return Err(PyValueError::new_err(format!( + "table {table_name:?} contains duplicate column {column_name:?}" + ))); + } + let values_object = column_object.getattr("values")?; + let dtype = values_object + .getattr("dtype")? + .getattr("str")? + .extract::()?; + let writeable = values_object + .getattr("flags")? + .getattr("writeable")? + .extract::()?; + if writeable { + return Err(PyValueError::new_err(format!( + "{table_name}.{column_name} must be read-only" + ))); + } + let values = extract_values(&values_object, &dtype, &table_name, &column_name)?; + let shape = shape(&values); + if shape.is_empty() || u64::try_from(shape[0]).ok() != Some(row_count) { + return Err(PyValueError::new_err(format!( + "{table_name}.{column_name} shape does not match row_count" + ))); + } + hash_text(&mut content_digest, &column_name); + hash_text(&mut content_digest, &dtype); + hash_u8(&mut content_digest, shape.len())?; + for dimension in &shape { + hash_u64(&mut content_digest, *dimension)?; + } + content_digest.update(values.raw_bytes()?); + hash_text(&mut schema_digest, &column_name); + hash_text(&mut schema_digest, &dtype); + hash_u8(&mut schema_digest, shape.len())?; + for dimension in shape.iter().skip(1) { + hash_u64(&mut schema_digest, *dimension)?; + } + column_by_name.insert(column_name.clone(), columns.len()); + columns.push(BorrowedColumn { + name: column_name, + shape, + values, + }); + } + table_by_name.insert(table_name.clone(), tables.len()); + tables.push(BorrowedTable { + name: table_name, + row_count, + columns, + column_by_name, + }); + } + let names = tables + .iter() + .map(|table| table.name.as_str()) + .collect::>(); + if names != EAGER_LOWERING_V1_TABLE_NAMES { + return Err(PyValueError::new_err( + "eager lowering table inventory does not match input ABI v1", + )); + } + let schema_digest = hex_digest(schema_digest.finalize()); + if schema_digest != INPUT_SCHEMA_SHA256 { + return Err(PyValueError::new_err(format!( + "eager lowering table/column schema does not match input ABI v1: {schema_digest}" + ))); + } + for limitation in &semantic_limitations { + hash_text(&mut content_digest, limitation); + } + let digest = hex_digest(content_digest.finalize()); + let declared_digest = required_string(input, "digest")?; + if declared_digest != digest { + return Err(PyValueError::new_err( + "eager lowering input digest does not match its primitive columns", + )); + } + Ok(BorrowedInput { + abi, + process_key, + model_name, + string_catalog, + canonical_ir_catalog, + semantic_limitations, + tables, + table_by_name, + digest, + }) +} + +fn extract_values<'py>( + value: &Bound<'py, PyAny>, + dtype: &str, + table: &str, + column: &str, +) -> PyResult> { + let context = format!("{table}.{column}"); + macro_rules! extract { + ($ty:ty, $variant:ident) => {{ + let array = value + .extract::>() + .map_err(|_| { + PyTypeError::new_err(format!( + "{context} is not a NumPy array with dtype {dtype}" + )) + })?; + if !array.is_c_contiguous() { + return Err(PyValueError::new_err(format!( + "{context} must be C-contiguous" + ))); + } + BorrowedValues::$variant(array) + }}; + } + match dtype { + "|u1" => Ok(extract!(u8, U8)), + " Ok(extract!(u32, U32)), + " Ok(extract!(u64, U64)), + " Ok(extract!(i32, I32)), + " Ok(extract!(f64, F64)), + _ => Err(PyValueError::new_err(format!( + "{context} has unsupported dtype {dtype:?}" + ))), + } +} + +fn shape(values: &BorrowedValues<'_>) -> Vec { + match values { + BorrowedValues::U8(array) => array.shape().to_vec(), + BorrowedValues::U32(array) => array.shape().to_vec(), + BorrowedValues::U64(array) => array.shape().to_vec(), + BorrowedValues::I32(array) => array.shape().to_vec(), + BorrowedValues::F64(array) => array.shape().to_vec(), + } +} + +fn required_string(value: &Bound<'_, PyAny>, attribute: &str) -> PyResult { + value + .getattr(attribute)? + .extract::() + .map_err(|_| PyTypeError::new_err(format!("eager lowering {attribute} must be a string"))) +} + +fn nonempty_string(value: &Bound<'_, PyAny>, attribute: &str) -> PyResult { + let result = required_string(value, attribute)?; + if result.is_empty() { + return Err(PyValueError::new_err(format!( + "eager lowering {attribute} must not be empty" + ))); + } + Ok(result) +} + +fn unique_strings( + value: &Bound<'_, PyAny>, + attribute: &str, + require_nonempty: bool, +) -> PyResult> { + let object = value.getattr(attribute)?; + let result = object + .try_iter() + .map_err(|_| PyTypeError::new_err(format!("{attribute} must be iterable")))? + .map(|item| item?.extract::()) + .collect::>>()?; + if require_nonempty && result.is_empty() { + return Err(PyValueError::new_err(format!( + "{attribute} must not be empty" + ))); + } + if result.iter().any(String::is_empty) { + return Err(PyValueError::new_err(format!( + "{attribute} entries must not be empty" + ))); + } + let unique = result.iter().collect::>(); + if unique.len() != result.len() { + return Err(PyValueError::new_err(format!( + "{attribute} contains duplicate strings" + ))); + } + Ok(result) +} + +fn is_consumed_column(table: &str, column: &str) -> bool { + matches!( + (table, column), + ( + "currents", + "id" | "dimension" + | "is_source" + | "momentum_mask_bitset_id" + | "propagator_ir_id" + | "propagator_kernel_id" + ) | ( + "sources", + "source_id" + | "current_id" + | "external_label" + | "input_momentum_slot" + | "source_ir_id" + | "crossing_ir_id" + | "crossing_factor_id" + | "declared_state_index" + ) | ( + "interactions", + "id" | "stage_subset_size" + | "left_current_id" + | "right_current_id" + | "result_current_id" + | "coupling_id" + | "coupling_factor_id" + | "color_factor_id" + | "evaluation_factor_id" + | "evaluation_group_id" + | "full_tensor_network_ready" + | "kernel_id" + | "canonical_input_order" + | "kernel_normalization_factor_id" + | "output_factor_source" + ) | ( + "roots", + "id" | "left_current_id" + | "right_current_id" + | "color_factor_id" + | "contraction_ir_id" + | "coupling_id" + | "coupling_factor_id" + | "helicity_weight_factor_id" + | "coherent_group_id" + | "kernel_id" + | "canonical_input_order" + | "kernel_normalization_factor_id" + | "output_factor_source" + ) | ( + "interaction_groups", + "representative_interaction_id" | "member_start" | "member_count" + ) | ("interaction_group_members", "interaction_id") + | ("momentum_masks", "slot_id" | "bitset_id") + | ("bitset_ranges", "start" | "count" | "bit_count") + | ("bitset_words", "value") + | ("u32_sequence_ranges", "start" | "count") + | ("u32_sequence_values", "value") + | ("i32_sequence_ranges", "start" | "count") + | ("i32_sequence_values", "value") + | ( + "exact_factors", + "real" + | "imaginary" + | "canonical_string_id" + | "exact_source" + | "exact_ir_id" + | "source_ir_id" + ) + | ( + "couplings", + "constant_factor_id" | "parameter_name_ids_sequence_id" + ) + | ( + "model_parameters", + "name_string_id" + | "kind_string_id" + | "default_value" + | "default_factor_id" + | "runtime_name_string_id" + | "complex_component" + | "derived" + ) + | ( + "contraction_coefficients", + "contraction_ir_id" | "component_index" | "factor_id" + ) + | ( + "coherent_groups", + "id" | "helicity_weight_factor_id" | "all_sector_weight_factor_id" + ) + | ( + "helicity_selectors", + "values_sequence_id" + | "representative_sequence_id" + | "coefficient_factor_id" + | "computed" + | "structural_zero" + ) + | ( + "color_selectors", + "word_sequence_id" + | "representative_word_sequence_id" + | "coefficient_factor_id" + | "computed" + ) + | ( + "reduction_members", + "coherent_group_id" | "helicity_selector_id" | "color_selector_id" + ) + | ( + "color_contraction_metadata", + "present" | "supported" | "group_count" | "includes_color_factor" + ) + | ( + "color_contraction_entries", + "left_group_id" | "right_group_id" | "weight_factor_id" | "symmetry_factor_id" + ) + ) +} + +fn hash_text(digest: &mut Sha256, value: &str) { + digest.update((value.len() as u64).to_le_bytes()); + digest.update(value.as_bytes()); +} + +fn hash_u8(digest: &mut Sha256, value: usize) -> PyResult<()> { + digest + .update([u8::try_from(value) + .map_err(|_| PyValueError::new_err("eager lowering rank exceeds u8"))?]); + Ok(()) +} + +fn hash_u32(digest: &mut Sha256, value: usize) -> PyResult<()> { + digest.update( + u32::try_from(value) + .map_err(|_| PyValueError::new_err("eager lowering count exceeds u32"))? + .to_le_bytes(), + ); + Ok(()) +} + +fn hash_u64(digest: &mut Sha256, value: usize) -> PyResult<()> { + digest.update( + u64::try_from(value) + .map_err(|_| PyValueError::new_err("eager lowering count exceeds u64"))? + .to_le_bytes(), + ); + Ok(()) +} + +fn hex_digest(value: impl AsRef<[u8]>) -> String { + value + .as_ref() + .iter() + .map(|byte| format!("{byte:02x}")) + .collect() +} + +fn not_contiguous(error: impl std::fmt::Display) -> PyErr { + PyValueError::new_err(format!("NumPy column is not contiguous: {error}")) +} + +fn core_not_contiguous(error: impl std::fmt::Display) -> RusticolError { + RusticolError::invalid_argument(format!("NumPy column is not contiguous: {error}")) +} + +fn wrong_type(context: &str, dtype: &str) -> RusticolError { + RusticolError::invalid_argument(format!("{context} is not a {dtype} column")) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn every_v1_table_is_classified_for_retention() { + assert_eq!(EAGER_LOWERING_V1_TABLE_NAMES.len(), 64); + assert!( + EAGER_LOWERING_V1_TABLE_NAMES + .windows(2) + .all(|pair| pair[0] < pair[1]) + ); + for table in [ + "color_replay_contraction_component_groups", + "color_replay_contraction_entries", + "color_replay_contraction_metadata", + "color_replay_mappings", + "color_replay_metadata", + "color_replay_permutations", + "color_replay_physical_groups", + "color_replay_routes", + ] { + assert!(EAGER_LOWERING_V1_TABLE_NAMES.contains(&table)); + } + } + + #[test] + fn consumed_columns_are_narrow_and_explicit() { + assert!(is_consumed_column("currents", "id")); + assert!(is_consumed_column("interactions", "canonical_input_order")); + assert!(!is_consumed_column("currents", "particle_id")); + assert!(!is_consumed_column("metadata", "process_ir_id")); + assert!(!is_consumed_column( + "helicity_source_mappings", + "source_contract_digest_string_id" + )); + assert!(!is_consumed_column( + "helicity_materialized_schedules", + "active_current_sequence_id" + )); + assert!(!is_consumed_column( + "lc_replay_partitions", + "proof_digest_string_id" + )); + assert!(!is_consumed_column( + "color_replay_metadata", + "physical_group_count" + )); + assert!(!is_consumed_column( + "color_replay_routes", + "source_group_id" + )); + } +} diff --git a/pyamplicol/rust/crates/rusticol-python/src/lib.rs b/pyamplicol/rust/crates/rusticol-python/src/lib.rs new file mode 100644 index 0000000000..46ee8ec18a --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-python/src/lib.rs @@ -0,0 +1,2511 @@ +// SPDX-License-Identifier: 0BSD + +#[cfg(feature = "numpy")] +mod eager_lowering; +#[cfg(feature = "numpy")] +mod recurrence; +#[cfg(feature = "numpy")] +mod recurrence_numerical_evidence; +#[cfg(feature = "numpy")] +mod recurrence_template; + +#[cfg(feature = "numpy")] +use numpy::{PyReadonlyArray3, PyUntypedArrayMethods}; +use pyo3::IntoPyObjectExt; +use pyo3::create_exception; +use pyo3::exceptions::{PyException, PyTypeError, PyValueError}; +use pyo3::prelude::*; +use pyo3::types::{PyAny, PyBool, PyBytes, PyDict, PyInt, PyList, PyTuple}; +use rusticol_core::__private::{ + build_recurrence_bootstrap_image_v1, compile_symbolica_program_to_plane_application_bytes, +}; +use rusticol_core::{ + ColorAccuracy, ColorComponent as CoreColorComponent, ModelParameter as CoreModelParameter, + NativeOnTheFlyWarmUpEvent, NativeOnTheFlyWarmUpEventKind, NativeOnTheFlyWarmUpObserver, + NativeOnTheFlyWarmUpStage, NativeResolvedEvaluation, NativeRuntime, NativeRuntimeProfile, + ParameterKind, ParticleRole, ProcessPhysics as CoreProcessPhysics, ReductionKind, + RusticolError as CoreError, RusticolErrorKind, + eager_direct_descriptor_for_source_application_bytes, preflight_prepared_kernel_pack, + runtime_target_info, +}; +use serde_json::Value; +use std::collections::BTreeMap; +use std::marker::PhantomData; +use std::path::PathBuf; +use std::ptr::NonNull; +use std::sync::{Arc, Mutex}; +use std::thread::{self, ThreadId}; + +create_exception!(_rusticol, RusticolError, PyException); +create_exception!(_rusticol, ArtifactError, RusticolError); +create_exception!(_rusticol, CompatibilityError, RusticolError); +create_exception!(_rusticol, EvaluationError, RusticolError); +create_exception!(_rusticol, SelectorError, EvaluationError); +create_exception!(_rusticol, ModelParameterError, EvaluationError); + +#[pyclass(module = "pyamplicol._rusticol", frozen, get_all, skip_from_py_object)] +#[derive(Clone)] +struct TargetInfo { + triple: String, + cpu_features: Vec, +} + +fn python_error(error: CoreError) -> PyErr { + let message = error.to_string(); + match error.kind() { + RusticolErrorKind::InvalidArgument => PyValueError::new_err(message), + RusticolErrorKind::Artifact + | RusticolErrorKind::Security + | RusticolErrorKind::Integrity + | RusticolErrorKind::Serialization => ArtifactError::new_err(message), + RusticolErrorKind::Compatibility + | RusticolErrorKind::UnsupportedPrecision + | RusticolErrorKind::UnsupportedRuntimeCapability => CompatibilityError::new_err(message), + RusticolErrorKind::Selector => SelectorError::new_err(message), + RusticolErrorKind::ModelParameter => ModelParameterError::new_err(message), + RusticolErrorKind::Evaluation | RusticolErrorKind::Internal => { + EvaluationError::new_err(message) + } + _ => EvaluationError::new_err(message), + } +} + +fn require_raw_f64_precision(precision: u32) -> Result<(), CoreError> { + if precision == 16 { + return Ok(()); + } + Err(CoreError::compatibility(format!( + "raw pyamplicol._rusticol supports only precision=16 for Symbolica-free direct SymJIT evaluation; precision={precision} must be routed through the public Python Symbolica executor" + ))) +} + +fn report_on_the_fly_warm_up_progress( + callback: &Py, + callback_error: &Mutex>, + event: &NativeOnTheFlyWarmUpEvent, +) -> Result { + let payload = serde_json::to_string(event).map_err(|error| { + CoreError::serialization(format!( + "could not serialize on-the-fly warm-up progress event: {error}" + )) + })?; + let terminal_notification = event.kind == NativeOnTheFlyWarmUpEventKind::End + && event.stage == NativeOnTheFlyWarmUpStage::FirstEvaluation; + Python::attach(|py| { + let decision = callback.bind(py).call1((payload,)); + let decision = match decision { + Ok(decision) => decision, + Err(error) if terminal_notification => { + error.write_unraisable(py, Some(callback.bind(py))); + return Ok(true); + } + Err(error) => { + let message = error.to_string(); + let mut slot = callback_error.lock().map_err(|_| { + CoreError::internal("on-the-fly warm-up callback error lock was poisoned") + })?; + *slot = Some(error); + return Err(CoreError::evaluation(format!( + "on-the-fly warm-up progress callback failed: {message}" + ))); + } + }; + if decision.is_none() { + return Ok(true); + } + if !decision.is_instance_of::() { + let error = PyTypeError::new_err( + "on-the-fly warm-up progress callback must return bool or None", + ); + if terminal_notification { + error.write_unraisable(py, Some(callback.bind(py))); + return Ok(true); + } + let mut slot = callback_error.lock().map_err(|_| { + CoreError::internal("on-the-fly warm-up callback error lock was poisoned") + })?; + *slot = Some(error); + return Err(CoreError::invalid_argument( + "on-the-fly warm-up progress callback must return bool or None", + )); + } + decision.extract::().map_err(|error| { + CoreError::invalid_argument(format!( + "could not read on-the-fly warm-up progress decision: {error}" + )) + }) + }) +} + +#[pyclass(module = "pyamplicol._rusticol", frozen, get_all, skip_from_py_object)] +#[derive(Clone)] +struct ExternalParticle { + index: usize, + label: usize, + name: String, + pdg_id: i32, + state: String, + momentum_slot: usize, +} + +#[pyclass(module = "pyamplicol._rusticol", frozen, get_all, skip_from_py_object)] +#[derive(Clone)] +struct HelicityConfiguration { + id: String, + index: usize, + values: Vec, + computed: bool, + structural_zero: bool, + representative_id: String, + coefficient: f64, +} + +#[pyclass(module = "pyamplicol._rusticol", frozen, get_all, skip_from_py_object)] +#[derive(Clone)] +struct ColorFlow { + id: String, + index: usize, + word: Vec, + computed: bool, + representative_id: String, + coefficient: f64, +} + +#[pyclass(module = "pyamplicol._rusticol", frozen, get_all, skip_from_py_object)] +#[derive(Clone)] +struct ContractedColorComponent { + id: String, + index: usize, + description: String, +} + +#[pyclass(module = "pyamplicol._rusticol", frozen, get_all, skip_from_py_object)] +#[derive(Clone)] +struct ModelParameter { + name: String, + kind: String, + default_real: f64, + default_imaginary: f64, + mutable: bool, +} + +#[pyclass(module = "pyamplicol._rusticol", frozen, get_all, skip_from_py_object)] +#[derive(Clone)] +struct ReductionGroup { + id: String, + representative_helicity_id: String, + representative_color_id: String, + physical_helicity_ids: Vec, + physical_color_ids: Vec, +} + +#[pyclass(module = "pyamplicol._rusticol", frozen, get_all, skip_from_py_object)] +#[derive(Clone)] +struct PhysicsReduction { + kind: String, + groups: Vec, +} + +impl From<&CoreModelParameter> for ModelParameter { + fn from(parameter: &CoreModelParameter) -> Self { + Self { + name: parameter.name.clone(), + kind: parameter_kind_name(parameter.kind).to_string(), + default_real: parameter.default_real, + default_imaginary: parameter.default_imaginary, + mutable: parameter.mutable, + } + } +} + +#[pyclass(module = "pyamplicol._rusticol", frozen, skip_from_py_object)] +#[derive(Clone)] +struct ProcessPhysics { + process_id: String, + process: String, + color_accuracy: String, + helicity_coverage: String, + color_coverage: String, + color_kind: String, + structural_zero_helicity_count: usize, + external_particles: Vec, + helicities: Vec, + color_flows: Vec, + contracted_color_components: Vec, + reduction: PhysicsReduction, + model_parameters: Vec, + selector_capabilities: Vec, +} + +impl ProcessPhysics { + fn from_core(physics: &CoreProcessPhysics) -> Self { + let external_particles = physics + .external_particles + .iter() + .map(|particle| ExternalParticle { + index: particle.index, + label: particle.label, + name: particle.particle.clone(), + pdg_id: particle.pdg, + state: match particle.role { + ParticleRole::Initial => "incoming", + ParticleRole::Final => "outgoing", + } + .to_string(), + momentum_slot: particle.momentum_slot, + }) + .collect(); + let helicities = physics + .helicities + .iter() + .map(|helicity| HelicityConfiguration { + id: helicity.id.clone(), + index: helicity.index, + values: helicity.values.clone(), + computed: helicity.computed, + structural_zero: helicity.structural_zero, + representative_id: helicity.representative_id.clone(), + coefficient: helicity.coefficient, + }) + .collect(); + let color_flows = physics + .color_components + .iter() + .filter_map(|component| match component { + CoreColorComponent::LcFlow(flow) => Some(ColorFlow { + id: flow.id.clone(), + index: flow.index, + word: flow.word.clone(), + computed: flow.computed, + representative_id: flow.representative_id.clone(), + coefficient: flow.coefficient, + }), + CoreColorComponent::ContractedColor(_) => None, + }) + .collect(); + let contracted_color_components = physics + .color_components + .iter() + .filter_map(|component| match component { + CoreColorComponent::ContractedColor(color) => Some(ContractedColorComponent { + id: color.id.clone(), + index: color.index, + description: color.description.clone(), + }), + CoreColorComponent::LcFlow(_) => None, + }) + .collect(); + let mut selector_capabilities = Vec::new(); + if physics.selectors.helicity { + selector_capabilities.push("helicity".to_string()); + } + if physics.selectors.color_flow { + selector_capabilities.push("color_flow".to_string()); + } + if physics.selectors.contracted_color { + selector_capabilities.push("contracted_color".to_string()); + } + let reduction = PhysicsReduction { + kind: reduction_kind_name(physics.reduction.kind).to_string(), + groups: physics + .reduction + .groups + .iter() + .map(|group| ReductionGroup { + id: group.id.clone(), + representative_helicity_id: group.representative_helicity_id.clone(), + representative_color_id: group.representative_color_id.clone(), + physical_helicity_ids: group.physical_helicity_ids.clone(), + physical_color_ids: group.physical_color_ids.clone(), + }) + .collect(), + }; + Self { + process_id: physics.process_id.clone(), + process: physics.process.clone(), + color_accuracy: color_accuracy_name(physics.color_accuracy).to_string(), + helicity_coverage: physics.coverage.helicities.clone(), + color_coverage: physics.coverage.color.clone(), + color_kind: physics.coverage.color_kind.clone(), + structural_zero_helicity_count: physics.coverage.structural_zero_helicity_count, + external_particles, + helicities, + color_flows, + contracted_color_components, + reduction, + model_parameters: physics.model_parameters.iter().map(Into::into).collect(), + selector_capabilities, + } + } +} + +#[pymethods] +impl ProcessPhysics { + #[getter] + fn process_id(&self) -> &str { + &self.process_id + } + + #[getter] + fn process(&self) -> &str { + &self.process + } + + #[getter] + fn color_accuracy(&self) -> &str { + &self.color_accuracy + } + + #[getter] + fn helicity_coverage(&self) -> &str { + &self.helicity_coverage + } + + #[getter] + fn color_coverage(&self) -> &str { + &self.color_coverage + } + + #[getter] + fn color_kind(&self) -> &str { + &self.color_kind + } + + #[getter] + fn structural_zero_helicity_count(&self) -> usize { + self.structural_zero_helicity_count + } + + #[getter] + fn external_particles(&self) -> Vec { + self.external_particles.clone() + } + + #[getter] + fn helicities(&self) -> Vec { + self.helicities.clone() + } + + #[getter] + fn color_flows(&self) -> Vec { + self.color_flows.clone() + } + + #[getter] + fn contracted_color_components(&self) -> Vec { + self.contracted_color_components.clone() + } + + #[getter] + fn reduction(&self) -> PhysicsReduction { + self.reduction.clone() + } + + #[getter] + fn model_parameters(&self) -> Vec { + self.model_parameters.clone() + } + + #[getter] + fn selector_capabilities(&self) -> Vec { + self.selector_capabilities.clone() + } +} + +#[pyclass(module = "pyamplicol._rusticol", unsendable)] +struct ResolvedEvaluation { + values: Py, + totals: Py, + shape: (usize, usize, usize), + helicity_ids: Vec, + color_ids: Vec, + color_accuracy: String, + precision: u32, +} + +#[pymethods] +impl ResolvedEvaluation { + #[getter] + fn values(&self, py: Python<'_>) -> Py { + self.values.clone_ref(py) + } + + #[getter] + fn shape(&self) -> (usize, usize, usize) { + self.shape + } + + #[getter] + fn helicity_ids(&self) -> Vec { + self.helicity_ids.clone() + } + + #[getter] + fn color_ids(&self) -> Vec { + self.color_ids.clone() + } + + #[getter] + fn color_flow_ids(&self) -> Vec { + if self.color_accuracy == "lc" { + self.color_ids.clone() + } else { + Vec::new() + } + } + + #[getter] + fn color_accuracy(&self) -> &str { + &self.color_accuracy + } + + #[getter] + fn precision(&self) -> u32 { + self.precision + } + + fn total(&self, py: Python<'_>) -> Py { + self.totals.clone_ref(py) + } +} + +#[pyclass(module = "pyamplicol._rusticol", unsendable)] +struct Runtime { + runtime: NativeRuntime, +} + +/// An exclusive `NativeRuntime` borrow which stable PyO3 can carry through +/// [`Python::detach`]. +/// +/// Stable PyO3 conservatively uses `Send` as its `Ungil` bound even though +/// `detach` invokes its closure synchronously on the calling thread. The +/// native runtime is deliberately `!Send`, so this adapter retains its Rust +/// borrow for the whole detached call and permits access only on the thread +/// which created it. +struct SameThreadNativeRuntimeBorrow<'a> { + runtime: NonNull, + origin: ThreadId, + _exclusive: PhantomData<&'a mut NativeRuntime>, +} + +fn require_origin_thread(origin: ThreadId) -> Result<(), CoreError> { + if thread::current().id() == origin { + return Ok(()); + } + Err(CoreError::internal( + "detached NativeRuntime operation moved away from its owning thread", + )) +} + +impl<'a> SameThreadNativeRuntimeBorrow<'a> { + fn new(runtime: &'a mut NativeRuntime) -> Self { + Self { + runtime: NonNull::from(runtime), + origin: thread::current().id(), + _exclusive: PhantomData, + } + } + + fn run(mut self, operation: impl FnOnce(&mut NativeRuntime) -> R) -> Result { + require_origin_thread(self.origin)?; + // SAFETY: `new` receives an exclusive borrow whose lifetime is retained + // by `_exclusive`. This consuming method can create only one mutable + // reborrow, and the thread check above prevents dereferencing the + // deliberately thread-affine runtime from any other thread. + Ok(operation(unsafe { self.runtime.as_mut() })) + } +} + +// SAFETY: transferring the adapter cannot transfer, move, drop, or access the +// borrowed runtime. `run` is its only accessor and checks the creating thread +// before dereferencing; dropping the adapter never touches the pointee. The +// lifetime marker continues to enforce exclusive access on the creating stack. +unsafe impl Send for SameThreadNativeRuntimeBorrow<'_> {} + +// These signatures intentionally mirror the keyword-rich public Python ABI. +#[allow(clippy::too_many_arguments)] +#[pymethods] +impl Runtime { + #[new] + #[pyo3(signature=(artifact, *, process=None, model_parameters=None, mute_warnings=false))] + fn new( + artifact: &Bound<'_, PyAny>, + process: Option<&str>, + model_parameters: Option<&Bound<'_, PyAny>>, + mute_warnings: bool, + ) -> PyResult { + let path = path_from_python(artifact)?; + let mut runtime = NativeRuntime::load(path, process, None).map_err(python_error)?; + if let Some(mapping) = model_parameters { + let values = parse_model_parameters(mapping)?; + runtime + .set_model_parameters(&values) + .map_err(python_error)?; + } + if mute_warnings { + runtime.mute_warnings(); + } + Ok(Self { runtime }) + } + + #[staticmethod] + #[pyo3(signature=(artifact, *, process=None, model_parameters=None, mute_warnings=false))] + fn load( + artifact: &Bound<'_, PyAny>, + process: Option<&str>, + model_parameters: Option<&Bound<'_, PyAny>>, + mute_warnings: bool, + ) -> PyResult { + Self::new(artifact, process, model_parameters, mute_warnings) + } + + #[getter] + fn process(&self) -> String { + self.runtime.metadata().process + } + + #[getter] + fn artifact_id(&self) -> &str { + self.runtime.artifact_id() + } + + #[getter] + fn process_id(&self) -> String { + self.runtime.metadata().process_key + } + + #[getter] + fn color_accuracy(&self) -> String { + self.runtime.metadata().color_accuracy + } + + #[getter] + fn physics(&self) -> PyResult { + Ok(ProcessPhysics::from_core( + self.runtime.process_physics().map_err(python_error)?, + )) + } + + fn metadata_json(&self) -> PyResult { + self.runtime.metadata_json().map_err(python_error) + } + + fn save(&mut self, py: Python<'_>, path: &Bound<'_, PyAny>) -> PyResult<()> { + let path = path_from_python(path)?; + let runtime = SameThreadNativeRuntimeBorrow::new(&mut self.runtime); + py.detach(move || runtime.run(|runtime| runtime.save(path))) + .and_then(|result| result) + .map_err(python_error) + } + + fn load_cache(&mut self, py: Python<'_>, path: &Bound<'_, PyAny>) -> PyResult<()> { + let path = path_from_python(path)?; + let runtime = SameThreadNativeRuntimeBorrow::new(&mut self.runtime); + py.detach(move || runtime.run(|runtime| runtime.load_cache(path))) + .and_then(|result| result) + .map_err(python_error) + } + + #[pyo3(signature=(color_flow_ids=None))] + fn _on_the_fly_benchmark_context_json( + &self, + color_flow_ids: Option>, + ) -> PyResult> { + self.runtime + .on_the_fly_benchmark_context_json(color_flow_ids.as_deref()) + .map_err(python_error) + } + + #[pyo3(signature=(helicity_ids=None, color_flow_ids=None))] + fn _on_the_fly_selector_ordinals_json( + &self, + helicity_ids: Option>, + color_flow_ids: Option>, + ) -> PyResult> { + self.runtime + .on_the_fly_selector_ordinals_json(helicity_ids.as_deref(), color_flow_ids.as_deref()) + .map_err(python_error) + } + + /// Private read-only production introspection of retained compact OTF state. + fn _on_the_fly_runtime_state_census_json(&self) -> PyResult> { + self.runtime + .on_the_fly_runtime_state_census_json() + .map_err(python_error) + } + + #[pyo3(signature=(momenta, helicity_ids=None, color_flow_ids=None, progress_callback=None, *, n_cores=None))] + fn _on_the_fly_warm_up_f64_json( + &mut self, + py: Python<'_>, + momenta: &Bound<'_, PyAny>, + helicity_ids: Option>, + color_flow_ids: Option>, + progress_callback: Option>, + n_cores: Option<&Bound<'_, PyAny>>, + ) -> PyResult { + let n_cores = n_cores + .map(|value| { + if value.is_instance_of::() || !value.is_instance_of::() { + return Err(PyTypeError::new_err( + "n_cores must be a positive integer or None", + )); + } + match value.extract::() { + Ok(count) if count > 0 => Ok(count), + _ => Err(PyValueError::new_err( + "n_cores must be a positive integer fitting the platform's usize", + )), + } + }) + .transpose()?; + let momenta = parse_f64_momenta(momenta, self.runtime.external_count())?; + if momenta.point_count() != 1 { + return Err(PyValueError::new_err(format!( + "on-the-fly warm_up requires exactly one phase-space point, received {}", + momenta.point_count() + ))); + } + let momenta = momenta.as_slice().to_vec(); + if let Some(callback) = progress_callback.as_ref() + && !callback.bind(py).is_callable() + { + return Err(PyTypeError::new_err( + "on-the-fly warm_up progress_callback must be callable or None", + )); + } + let callback_error = Arc::new(Mutex::new(None)); + let detached_callback_error = Arc::clone(&callback_error); + let detached_runtime = SameThreadNativeRuntimeBorrow::new(&mut self.runtime); + let result = py + .detach(move || { + detached_runtime.run(|runtime| { + if let Some(callback) = progress_callback.as_ref() { + let mut observer = |event: &NativeOnTheFlyWarmUpEvent| { + report_on_the_fly_warm_up_progress( + callback, + detached_callback_error.as_ref(), + event, + ) + }; + let observer: &mut NativeOnTheFlyWarmUpObserver<'_> = &mut observer; + runtime.warm_up_on_the_fly_f64_with_selectors( + &momenta, + helicity_ids.as_deref(), + color_flow_ids.as_deref(), + n_cores, + Some(observer), + ) + } else { + runtime.warm_up_on_the_fly_f64_with_selectors( + &momenta, + helicity_ids.as_deref(), + color_flow_ids.as_deref(), + n_cores, + None, + ) + } + }) + }) + .and_then(|result| result); + if let Some(error) = callback_error + .lock() + .map_err(|_| { + PyException::new_err("on-the-fly warm-up callback error lock was poisoned") + })? + .take() + { + return Err(error); + } + let result = result.map_err(python_error)?; + serde_json::to_string(&result).map_err(|error| { + python_error(CoreError::serialization(format!( + "could not serialize on-the-fly warm-up result: {error}" + ))) + }) + } + + fn physics_json(&self) -> PyResult { + self.runtime.physics_json().map_err(python_error) + } + + fn _exact_runtime_state_json(&self) -> PyResult { + self.runtime + .exact_runtime_state_json() + .map_err(python_error) + } + + #[pyo3(signature=(momenta, *, helicities=None, color_flows=None, helicity_by_point=None, color_flow_by_point=None, precision=16))] + fn evaluate( + &mut self, + py: Python<'_>, + momenta: &Bound<'_, PyAny>, + helicities: Option>, + color_flows: Option>, + helicity_by_point: Option>, + color_flow_by_point: Option>, + precision: u32, + ) -> PyResult> { + require_raw_f64_precision(precision).map_err(python_error)?; + let momenta = parse_f64_momenta(momenta, self.runtime.external_count())?; + let point_count = momenta.point_count(); + let mut values = vec![0.0; point_count]; + self.runtime + .evaluate_f64_into_with_selectors( + momenta.as_slice(), + point_count, + helicities.as_deref(), + color_flows.as_deref(), + helicity_by_point.as_deref(), + color_flow_by_point.as_deref(), + &mut values, + ) + .map_err(python_error)?; + let result = PyList::new(py, values)?.into_any().unbind(); + self.emit_warnings(py)?; + Ok(result) + } + + #[pyo3(signature=(momenta, repetitions, *, helicities=None, color_flows=None, helicity_by_point=None, color_flow_by_point=None, precision=16))] + fn _benchmark_f64_wall_time( + &mut self, + momenta: &Bound<'_, PyAny>, + repetitions: usize, + helicities: Option>, + color_flows: Option>, + helicity_by_point: Option>, + color_flow_by_point: Option>, + precision: u32, + ) -> PyResult { + require_raw_f64_precision(precision).map_err(python_error)?; + let momenta = parse_f64_momenta(momenta, self.runtime.external_count())?; + let point_count = momenta.point_count(); + self.runtime + .benchmark_f64_wall_time_with_selectors( + momenta.as_slice(), + point_count, + repetitions, + helicities.as_deref(), + color_flows.as_deref(), + helicity_by_point.as_deref(), + color_flow_by_point.as_deref(), + ) + .map_err(python_error) + } + + #[pyo3(signature=(momenta, repetitions, *, helicities=None, color_flows=None, precision=16, include_values=false))] + fn _profile_arena_repeated( + &mut self, + py: Python<'_>, + momenta: &Bound<'_, PyAny>, + repetitions: usize, + helicities: Option>, + color_flows: Option>, + precision: u32, + include_values: bool, + ) -> PyResult> { + require_raw_f64_precision(precision).map_err(python_error)?; + let momenta = parse_f64_momenta(momenta, self.runtime.external_count())?; + let point_count = momenta.point_count(); + let profiled = self + .runtime + .evaluate_f64_arena_profile_repeated( + momenta.as_slice(), + point_count, + repetitions, + helicities.as_deref(), + color_flows.as_deref(), + ) + .map_err(python_error)?; + let measured_points = point_count + .checked_mul(repetitions) + .ok_or_else(|| PyValueError::new_err("arena profile point count overflowed"))?; + let result = runtime_profile_to_python(py, &profiled.profile, measured_points)?; + result.set_item("execution_mode", self.runtime.metadata().execution_mode)?; + result.set_item( + "profile_boundary", + "warmed-direct-arena-borrowed-input-preallocated-output-v1", + )?; + result.set_item("borrowed_flat_input", true)?; + result.set_item("preallocated_output", true)?; + result.set_item("phase_timing_scope", "coarse-arena-boundary-only-v1")?; + result.set_item("evaluator_timing_available", false)?; + if include_values { + result.set_item("values", PyList::new(py, profiled.values)?)?; + } + self.emit_warnings(py)?; + Ok(result.into_any().unbind()) + } + + #[pyo3(signature=(momenta, *, helicities=None, color_flows=None, helicity_by_point=None, color_flow_by_point=None, precision=16, include_values=false))] + fn profile( + &mut self, + py: Python<'_>, + momenta: &Bound<'_, PyAny>, + helicities: Option>, + color_flows: Option>, + helicity_by_point: Option>, + color_flow_by_point: Option>, + precision: u32, + include_values: bool, + ) -> PyResult> { + require_raw_f64_precision(precision).map_err(python_error)?; + let momenta = parse_f64_momenta(momenta, self.runtime.external_count())?; + let point_count = momenta.point_count(); + let profiled = self + .runtime + .evaluate_f64_profile_with_selectors( + momenta.as_slice(), + point_count, + helicities.as_deref(), + color_flows.as_deref(), + helicity_by_point.as_deref(), + color_flow_by_point.as_deref(), + ) + .map_err(python_error)?; + let result = runtime_profile_to_python(py, &profiled.profile, point_count)?; + result.set_item("execution_mode", self.runtime.metadata().execution_mode)?; + if include_values { + result.set_item("values", PyList::new(py, profiled.values)?)?; + } + self.emit_warnings(py)?; + Ok(result.into_any().unbind()) + } + + #[pyo3(signature=(momenta, repetitions, *, helicities=None, color_flows=None, helicity_by_point=None, color_flow_by_point=None, precision=16, include_values=false))] + fn profile_repeated( + &mut self, + py: Python<'_>, + momenta: &Bound<'_, PyAny>, + repetitions: usize, + helicities: Option>, + color_flows: Option>, + helicity_by_point: Option>, + color_flow_by_point: Option>, + precision: u32, + include_values: bool, + ) -> PyResult> { + require_raw_f64_precision(precision).map_err(python_error)?; + let momenta = parse_f64_momenta(momenta, self.runtime.external_count())?; + let point_count = momenta.point_count(); + let profiled = self + .runtime + .evaluate_f64_profile_repeated_with_selectors( + momenta.as_slice(), + point_count, + repetitions, + helicities.as_deref(), + color_flows.as_deref(), + helicity_by_point.as_deref(), + color_flow_by_point.as_deref(), + ) + .map_err(python_error)?; + let measured_points = point_count + .checked_mul(repetitions) + .ok_or_else(|| PyValueError::new_err("profile point count overflowed"))?; + let result = runtime_profile_to_python(py, &profiled.profile, measured_points)?; + result.set_item("execution_mode", self.runtime.metadata().execution_mode)?; + if include_values { + result.set_item("values", PyList::new(py, profiled.values)?)?; + } + self.emit_warnings(py)?; + Ok(result.into_any().unbind()) + } + + #[pyo3(signature=(momenta, *, helicities=None, color_flows=None, helicity_by_point=None, color_flow_by_point=None, precision=16, include_values=false))] + fn evaluate_profile( + &mut self, + py: Python<'_>, + momenta: &Bound<'_, PyAny>, + helicities: Option>, + color_flows: Option>, + helicity_by_point: Option>, + color_flow_by_point: Option>, + precision: u32, + include_values: bool, + ) -> PyResult> { + self.profile( + py, + momenta, + helicities, + color_flows, + helicity_by_point, + color_flow_by_point, + precision, + include_values, + ) + } + + #[pyo3(signature=(momenta, *, helicities=None, color_flows=None, precision=16))] + fn evaluate_resolved( + &mut self, + py: Python<'_>, + momenta: &Bound<'_, PyAny>, + helicities: Option>, + color_flows: Option>, + precision: u32, + ) -> PyResult { + require_raw_f64_precision(precision).map_err(python_error)?; + let accuracy = self.runtime.metadata().color_accuracy; + let momenta = parse_f64_momenta(momenta, self.runtime.external_count())?; + let point_count = momenta.point_count(); + let resolved = self + .runtime + .evaluate_resolved_f64( + momenta.as_slice(), + point_count, + helicities.as_deref(), + color_flows.as_deref(), + ) + .map_err(python_error)?; + let result = resolved_f64_to_python(py, resolved, accuracy, precision)?; + self.emit_warnings(py)?; + Ok(result) + } + + fn evaluate_with_prec( + &mut self, + py: Python<'_>, + momenta: &Bound<'_, PyAny>, + decimal_digit_precision: u32, + ) -> PyResult> { + self.evaluate(py, momenta, None, None, None, None, decimal_digit_precision) + } + + #[pyo3(signature=(momenta, decimal_digit_precision, helicities=None, color_flows=None))] + fn evaluate_resolved_with_prec( + &mut self, + py: Python<'_>, + momenta: &Bound<'_, PyAny>, + decimal_digit_precision: u32, + helicities: Option>, + color_flows: Option>, + ) -> PyResult { + self.evaluate_resolved( + py, + momenta, + helicities, + color_flows, + decimal_digit_precision, + ) + } + + fn set_model_parameters(&mut self, mapping: &Bound<'_, PyAny>) -> PyResult<()> { + let values = parse_model_parameters(mapping)?; + self.runtime + .set_model_parameters(&values) + .map_err(python_error) + } + + fn clear(&mut self) -> PyResult<()> { + self.runtime.clear().map_err(python_error) + } + + fn set_model_parameter(&mut self, name: &str, value: &Bound<'_, PyAny>) -> PyResult<()> { + let value = parse_complex_value(value, name)?; + self.runtime + .set_model_parameter(name, value.0, value.1) + .map_err(python_error) + } + + fn mute_warnings(&mut self) { + self.runtime.mute_warnings(); + } + + fn unmute_warnings(&mut self) { + self.runtime.unmute_warnings(); + } + + fn take_warnings(&mut self) -> Vec { + self.runtime.take_warnings() + } +} + +impl Runtime { + fn emit_warnings(&mut self, py: Python<'_>) -> PyResult<()> { + let pending = self.runtime.take_warnings(); + if pending.is_empty() { + return Ok(()); + } + let warnings = py.import("warnings")?; + for message in pending { + warnings.call_method1("warn", (message,))?; + } + Ok(()) + } +} + +fn color_accuracy_name(value: ColorAccuracy) -> &'static str { + value.as_str() +} + +fn reduction_kind_name(value: ReductionKind) -> &'static str { + match value { + ReductionKind::LcDiagonal => "lc-diagonal", + ReductionKind::ContractedColor => "contracted-color", + } +} + +fn parameter_kind_name(value: ParameterKind) -> &'static str { + match value { + ParameterKind::Normalization => "normalization", + ParameterKind::Mass => "mass", + ParameterKind::Width => "width", + ParameterKind::Coupling => "coupling", + ParameterKind::External => "external", + ParameterKind::Derived => "derived", + } +} + +fn path_from_python(value: &Bound<'_, PyAny>) -> PyResult { + value.extract::().map_err(|error| { + PyTypeError::new_err(format!( + "artifact must be a string or path-like object: {error}" + )) + }) +} + +fn parse_model_parameters(mapping: &Bound<'_, PyAny>) -> PyResult> { + let items = mapping.call_method0("items").map_err(|_| { + PyTypeError::new_err("model_parameters must be a mapping from names to numbers") + })?; + let mut result = BTreeMap::new(); + for item in items.try_iter()? { + let item = item?; + let name = item.get_item(0)?.extract::()?; + if name.is_empty() { + return Err(PyValueError::new_err( + "model parameter names must not be empty", + )); + } + let value = item.get_item(1)?; + if result + .insert(name.clone(), parse_complex_value(&value, &name)?) + .is_some() + { + return Err(PyValueError::new_err(format!( + "duplicate model parameter {name:?}" + ))); + } + } + Ok(result) +} + +fn parse_complex_value(value: &Bound<'_, PyAny>, name: &str) -> PyResult<(f64, f64)> { + if value.is_instance_of::() { + return Err(PyTypeError::new_err(format!( + "model parameter {name:?} must be numeric, not bool" + ))); + } + if let Ok(real) = value.extract::() { + return Ok((real, 0.0)); + } + if value.is_instance_of::() || value.is_instance_of::() { + if value.len()? != 2 { + return Err(PyValueError::new_err(format!( + "model parameter {name:?} array must contain [real, imaginary]" + ))); + } + let component = |index: usize, label: &str| -> PyResult { + let item = value.get_item(index)?; + if item.is_instance_of::() { + return Err(PyTypeError::new_err(format!( + "model parameter {name:?} {label} component must be numeric, not bool" + ))); + } + item.extract::().map_err(|_| { + PyTypeError::new_err(format!( + "model parameter {name:?} {label} component must be numeric" + )) + }) + }; + return Ok((component(0, "real")?, component(1, "imaginary")?)); + } + let real = value.getattr("real")?.extract::().map_err(|_| { + PyTypeError::new_err(format!( + "model parameter {name:?} must be a number or [real, imaginary]" + )) + })?; + let imaginary = value.getattr("imag")?.extract::().map_err(|_| { + PyTypeError::new_err(format!( + "model parameter {name:?} must be a number or [real, imaginary]" + )) + })?; + Ok((real, imaginary)) +} + +enum ParsedF64Momenta<'py> { + #[cfg(feature = "numpy")] + Borrowed { + array: PyReadonlyArray3<'py, f64>, + point_count: usize, + }, + Owned { + values: Vec, + point_count: usize, + _python: PhantomData<&'py ()>, + }, +} + +impl ParsedF64Momenta<'_> { + fn as_slice(&self) -> &[f64] { + match self { + #[cfg(feature = "numpy")] + Self::Borrowed { array, .. } => array + .as_slice() + .expect("contiguous NumPy momenta were validated while parsing"), + Self::Owned { values, .. } => values, + } + } + + fn point_count(&self) -> usize { + match self { + #[cfg(feature = "numpy")] + Self::Borrowed { point_count, .. } => *point_count, + Self::Owned { point_count, .. } => *point_count, + } + } +} + +fn parse_f64_momenta<'py>( + momenta: &Bound<'py, PyAny>, + expected_legs: usize, +) -> PyResult> { + #[cfg(feature = "numpy")] + if let Ok(array) = momenta.extract::>() { + let shape = array.shape(); + let point_count = shape[0]; + if shape[1] != expected_legs || shape[2] != 4 { + return Err(PyValueError::new_err(format!( + "momenta array has shape ({}, {}, {}), expected (points, {expected_legs}, 4)", + point_count, shape[1], shape[2] + ))); + } + if point_count == 0 { + return Err(PyValueError::new_err( + "momenta must contain at least one point", + )); + } + if array.as_slice().is_ok() { + return Ok(ParsedF64Momenta::Borrowed { point_count, array }); + } + } + + let mut values = Vec::new(); + let mut point_count = 0; + for (point_index, point) in momenta.try_iter()?.enumerate() { + let point = point?; + let mut leg_count = 0; + for (leg_index, leg) in point.try_iter()?.enumerate() { + let leg = leg?; + let components = leg + .try_iter()? + .map(|component| component?.extract::()) + .collect::>>()?; + if components.len() != 4 { + return Err(PyValueError::new_err(format!( + "momenta point {point_index} leg {leg_index} has {} components, expected 4", + components.len() + ))); + } + values.extend(components); + leg_count += 1; + } + if leg_count != expected_legs { + return Err(PyValueError::new_err(format!( + "momenta point {point_index} has {leg_count} external legs, expected {expected_legs}" + ))); + } + point_count += 1; + } + if point_count == 0 { + return Err(PyValueError::new_err( + "momenta must contain at least one point", + )); + } + Ok(ParsedF64Momenta::Owned { + values, + point_count, + _python: PhantomData, + }) +} + +fn resolved_f64_to_python( + py: Python<'_>, + resolved: NativeResolvedEvaluation, + color_accuracy: String, + precision: u32, +) -> PyResult { + let shape = resolved.shape(); + let values = nested_f64_values(py, &resolved.values, shape.1, shape.2)?; + let totals = PyList::new(py, resolved.totals())?.into_any().unbind(); + Ok(ResolvedEvaluation { + values, + totals, + shape, + helicity_ids: resolved.helicity_ids, + color_ids: resolved.color_ids, + color_accuracy, + precision, + }) +} + +fn nested_f64_values( + py: Python<'_>, + values: &[f64], + helicity_count: usize, + color_count: usize, +) -> PyResult> { + let point_width = helicity_count + .checked_mul(color_count) + .ok_or_else(|| EvaluationError::new_err("resolved shape overflow"))?; + if point_width == 0 || !values.len().is_multiple_of(point_width) { + return Err(EvaluationError::new_err( + "resolved value buffer does not match its shape", + )); + } + let mut points = Vec::with_capacity(values.len() / point_width); + for point in values.chunks(point_width) { + let mut helicities = Vec::with_capacity(helicity_count); + for colors in point.chunks(color_count) { + helicities.push(PyList::new(py, colors.iter().copied())?.into_any().unbind()); + } + points.push(PyList::new(py, helicities)?.into_any().unbind()); + } + Ok(PyList::new(py, points)?.into_any().unbind()) +} + +fn runtime_profile_to_python<'py>( + py: Python<'py>, + profile: &NativeRuntimeProfile, + point_count: usize, +) -> PyResult> { + let payload = PyDict::new(py); + payload.set_item("points", point_count)?; + payload.set_item("wall_time_s", profile.total_s)?; + payload.set_item("native_input_pack_time_s", profile.native_input_pack_s)?; + payload.set_item( + "native_input_crossing_time_s", + profile.native_input_crossing_s, + )?; + payload.set_item("orchestration_time_s", profile.orchestration_s)?; + payload.set_item("state_prepare_time_s", profile.state_prepare_s)?; + payload.set_item("state_clear_time_s", profile.state_clear_s)?; + payload.set_item("source_fill_time_s", profile.source_fill_s)?; + payload.set_item( + "momentum_input_setup_time_s", + profile.momentum_input_setup_s, + )?; + payload.set_item("momentum_setup_time_s", profile.momentum_setup_s)?; + payload.set_item( + "model_parameter_setup_time_s", + profile.model_parameter_setup_s, + )?; + payload.set_item("stage_input_pack_time_s", profile.stage_input_pack_s)?; + payload.set_item( + "stage_leaf_input_pack_time_s", + profile.stage_leaf_input_pack_s, + )?; + payload.set_item( + "stage_evaluator_call_time_s", + profile.stage_evaluator_call_s, + )?; + payload.set_item("stage_evaluator_time_s", profile.stage_evaluator_s)?; + payload.set_item("stage_backend_call_time_s", profile.stage_backend_call_s)?; + payload.set_item( + "stage_evaluator_output_gather_time_s", + profile.stage_evaluator_output_gather_s, + )?; + payload.set_item("output_assign_time_s", profile.output_assign_s)?; + payload.set_item( + "amplitude_input_pack_time_s", + profile.amplitude_input_pack_s, + )?; + payload.set_item( + "amplitude_evaluator_call_time_s", + profile.amplitude_evaluator_call_s, + )?; + payload.set_item( + "amplitude_leaf_input_pack_time_s", + profile.amplitude_leaf_input_pack_s, + )?; + payload.set_item( + "amplitude_backend_call_time_s", + profile.amplitude_backend_call_s, + )?; + payload.set_item( + "amplitude_evaluator_output_gather_time_s", + profile.amplitude_evaluator_output_gather_s, + )?; + payload.set_item( + "amplitude_output_remap_time_s", + profile.amplitude_output_remap_s, + )?; + payload.set_item("amplitude_evaluator_time_s", profile.amplitude_evaluator_s)?; + payload.set_item("reduction_time_s", profile.reduction_s)?; + payload.set_item( + "resolved_reduction_materialization_inclusive_time_s", + profile.resolved_reduction_materialization_s, + )?; + payload.set_item( + "total_materialization_time_s", + profile.total_materialization_s, + )?; + payload.set_item("final_output_copy_time_s", profile.final_output_copy_s)?; + payload.set_item( + "stage_input_pack_by_stage_time_s", + profile.stage_input_pack_by_stage_s.clone(), + )?; + payload.set_item( + "stage_evaluator_call_by_stage_time_s", + profile.stage_evaluator_call_by_stage_s.clone(), + )?; + payload.set_item( + "stage_leaf_input_pack_by_stage_time_s", + profile.stage_leaf_input_pack_by_stage_s.clone(), + )?; + payload.set_item( + "stage_backend_call_by_stage_time_s", + profile.stage_backend_call_by_stage_s.clone(), + )?; + payload.set_item( + "stage_evaluator_output_gather_by_stage_time_s", + profile.stage_evaluator_output_gather_by_stage_s.clone(), + )?; + payload.set_item( + "stage_output_assign_by_stage_time_s", + profile.stage_output_assign_by_stage_s.clone(), + )?; + payload.set_item("eager_initialize_time_s", profile.eager_initialize_s)?; + payload.set_item("eager_gather_time_s", profile.eager_gather_s)?; + payload.set_item("eager_kernel_call_time_s", profile.eager_kernel_call_s)?; + payload.set_item( + "eager_invocation_scatter_time_s", + profile.eager_invocation_scatter_s, + )?; + payload.set_item("eager_finalization_time_s", profile.eager_finalization_s)?; + payload.set_item( + "eager_scatter_finalization_time_s", + profile.eager_scatter_finalization_s, + )?; + payload.set_item("eager_closure_time_s", profile.eager_closure_s)?; + payload.set_item("eager_reduction_time_s", profile.eager_reduction_s)?; + payload.set_item("eager_copy_out_time_s", profile.eager_copy_out_s)?; + payload.set_item( + "recurrence_momentum_fill_time_s", + profile.recurrence_momentum_fill_s, + )?; + payload.set_item( + "recurrence_union_source_fill_time_s", + profile.recurrence_union_source_fill_s, + )?; + payload.set_item("recurrence_schedule_time_s", profile.recurrence_schedule_s)?; + payload.set_item( + "recurrence_source_kernel_time_s", + profile.recurrence_source_kernel_s, + )?; + payload.set_item( + "recurrence_contribution_kernel_time_s", + profile.recurrence_contribution_kernel_s, + )?; + payload.set_item( + "recurrence_finalization_time_s", + profile.recurrence_finalization_s, + )?; + payload.set_item("recurrence_closure_time_s", profile.recurrence_closure_s)?; + payload.set_item( + "recurrence_replay_output_mapping_time_s", + profile.recurrence_replay_output_mapping_s, + )?; + payload.set_item( + "recurrence_momentum_scalar_value_count", + profile.recurrence_momentum_scalar_value_count, + )?; + payload.set_item( + "recurrence_schedule_execution_count", + profile.recurrence_schedule_execution_count, + )?; + payload.set_item( + "recurrence_replay_schedule_execution_count", + profile.recurrence_replay_schedule_execution_count, + )?; + payload.set_item( + "recurrence_union_schedule_execution_count", + profile.recurrence_union_schedule_execution_count, + )?; + payload.set_item( + "recurrence_union_source_row_count", + profile.recurrence_union_source_row_count, + )?; + payload.set_item( + "recurrence_replay_output_value_count", + profile.recurrence_replay_output_value_count, + )?; + payload.set_item( + "recurrence_source_call_count", + profile.recurrence_source_call_count, + )?; + payload.set_item( + "recurrence_source_row_count", + profile.recurrence_source_row_count, + )?; + payload.set_item( + "recurrence_contribution_call_count", + profile.recurrence_contribution_call_count, + )?; + payload.set_item( + "recurrence_contribution_row_count", + profile.recurrence_contribution_row_count, + )?; + payload.set_item( + "recurrence_finalization_call_count", + profile.recurrence_finalization_call_count, + )?; + payload.set_item( + "recurrence_finalization_row_count", + profile.recurrence_finalization_row_count, + )?; + payload.set_item( + "recurrence_closure_call_count", + profile.recurrence_closure_call_count, + )?; + payload.set_item( + "recurrence_closure_row_count", + profile.recurrence_closure_row_count, + )?; + payload.set_item("selector_planner_time_s", profile.selector_planner_s)?; + payload.set_item("selector_gather_time_s", profile.selector_gather_s)?; + payload.set_item("selector_scatter_time_s", profile.selector_scatter_s)?; + payload.set_item("selector_plan_kind", &profile.selector_plan_kind)?; + payload.set_item("selector_group_sizes", profile.selector_group_sizes.clone())?; + payload.set_item( + "selector_reordered_point_count", + profile.selector_reordered_point_count, + )?; + payload.set_item("selector_simd_lane_width", profile.selector_simd_lane_width)?; + payload.set_item("selector_simd_occupancy", profile.selector_simd_occupancy)?; + payload.set_item( + "native_input_component_count", + profile.native_input_component_count, + )?; + payload.set_item("native_input_pack_bytes", profile.native_input_pack_bytes)?; + payload.set_item( + "native_input_crossing_bytes", + profile.native_input_crossing_bytes, + )?; + payload.set_item( + "native_input_container_allocation_count", + profile.native_input_container_allocation_count, + )?; + payload.set_item("state_component_count", profile.state_component_count)?; + payload.set_item( + "state_clear_component_count", + profile.state_clear_component_count, + )?; + payload.set_item("source_component_count", profile.source_component_count)?; + payload.set_item("momentum_component_count", profile.momentum_component_count)?; + payload.set_item( + "model_parameter_component_count", + profile.model_parameter_component_count, + )?; + payload.set_item( + "stage_input_copy_component_count", + profile.stage_input_copy_component_count, + )?; + payload.set_item( + "stage_leaf_input_copy_component_count", + profile.stage_leaf_input_copy_component_count, + )?; + payload.set_item( + "stage_evaluator_output_gather_component_count", + profile.stage_evaluator_output_gather_component_count, + )?; + payload.set_item( + "stage_output_assign_component_count", + profile.stage_output_assign_component_count, + )?; + payload.set_item( + "amplitude_input_copy_component_count", + profile.amplitude_input_copy_component_count, + )?; + payload.set_item( + "amplitude_leaf_input_copy_component_count", + profile.amplitude_leaf_input_copy_component_count, + )?; + payload.set_item( + "amplitude_evaluator_output_gather_component_count", + profile.amplitude_evaluator_output_gather_component_count, + )?; + payload.set_item( + "amplitude_output_remap_component_count", + profile.amplitude_output_remap_component_count, + )?; + payload.set_item( + "evaluator_backend_call_count", + profile.evaluator_backend_call_count, + )?; + payload.set_item( + "compiled_direct_arena_engine_count", + profile.compiled_direct_arena_engine_count, + )?; + payload.set_item( + "compiled_direct_arena_call_count", + profile.compiled_direct_arena_call_count, + )?; + payload.set_item( + "compiled_direct_arena_minimum_effective_tile_capacity", + profile.compiled_direct_arena_minimum_effective_tile_capacity, + )?; + payload.set_item( + "compiled_direct_arena_maximum_physical_scalar_values_per_point", + profile.compiled_direct_arena_maximum_physical_scalar_values_per_point, + )?; + payload.set_item( + "compiled_direct_arena_maximum_hot_scalar_values_per_point", + profile.compiled_direct_arena_maximum_hot_scalar_values_per_point, + )?; + payload.set_item( + "compiled_direct_arena_maximum_source_scalar_values_per_point", + profile.compiled_direct_arena_maximum_source_scalar_values_per_point, + )?; + payload.set_item( + "compiled_direct_arena_maximum_reduction_scalar_values_per_point", + profile.compiled_direct_arena_maximum_reduction_scalar_values_per_point, + )?; + payload.set_item( + "compiled_direct_arena_boundary_input_bytes", + profile.compiled_direct_arena_boundary_input_bytes, + )?; + payload.set_item( + "compiled_direct_arena_boundary_current_output_bytes", + profile.compiled_direct_arena_boundary_current_output_bytes, + )?; + payload.set_item( + "compiled_direct_arena_boundary_amplitude_output_bytes", + profile.compiled_direct_arena_boundary_amplitude_output_bytes, + )?; + payload.set_item( + "reduction_input_component_count", + profile.reduction_input_component_count, + )?; + payload.set_item( + "selector_gather_point_count", + profile.selector_gather_point_count, + )?; + payload.set_item("selector_gather_bytes", profile.selector_gather_bytes)?; + payload.set_item( + "selector_scatter_value_count", + profile.selector_scatter_value_count, + )?; + payload.set_item( + "resolved_materialized_component_count", + profile.resolved_materialized_component_count, + )?; + payload.set_item( + "total_materialized_value_count", + profile.total_materialized_value_count, + )?; + payload.set_item( + "final_output_copy_value_count", + profile.final_output_copy_value_count, + )?; + payload.set_item( + "observed_scratch_reallocation_count", + profile.observed_scratch_reallocation_count, + )?; + payload.set_item( + "native_output_allocation_count", + profile.native_output_allocation_count, + )?; + Ok(payload) +} + +#[pyfunction] +fn abi_version() -> u32 { + NativeRuntime::ABI_VERSION +} + +#[pyfunction] +fn package_version() -> &'static str { + option_env!("PYAMPLICOL_PACKAGE_VERSION").unwrap_or(env!("CARGO_PKG_VERSION")) +} + +#[pyfunction] +fn native_build_inputs_sha256() -> &'static str { + option_env!("PYAMPLICOL_NATIVE_BUILD_INPUTS_SHA256").unwrap_or("") +} + +#[pyfunction] +fn target_info() -> TargetInfo { + let target = runtime_target_info(); + TargetInfo { + triple: target.triple, + cpu_features: target.cpu_features, + } +} + +#[pyfunction] +fn _preflight_eager_kernel_pack(manifest_path: PathBuf, payload_root: PathBuf) -> PyResult { + preflight_prepared_kernel_pack(&manifest_path, &payload_root).map_err(python_error) +} + +#[pyfunction] +fn _eager_direct_descriptor_v1( + py: Python<'_>, + source_application: &[u8], + input_complex_count: u32, + output_complex_count: u32, +) -> PyResult> { + let descriptor = eager_direct_descriptor_for_source_application_bytes( + source_application, + input_complex_count, + output_complex_count, + std::path::Path::new(""), + ) + .map_err(python_error)?; + Ok(PyBytes::new(py, &descriptor).unbind()) +} + +#[pyfunction] +fn _compile_symjit_plane_application_v2( + py: Python<'_>, + program_repr: &str, + input_complex_count: usize, + output_complex_count: usize, + optimization_level: u8, + compress: bool, +) -> PyResult> { + let application = compile_symbolica_program_to_plane_application_bytes( + program_repr, + input_complex_count, + output_complex_count, + optimization_level, + compress, + ) + .map_err(python_error)?; + Ok(PyBytes::new(py, &application).unbind()) +} + +#[pyfunction] +fn _build_recurrence_bootstrap_v1( + py: Python<'_>, + context_json: &[u8], + execution_json: &[u8], + physics_json: &[u8], +) -> PyResult> { + let image = build_recurrence_bootstrap_image_v1(context_json, execution_json, physics_json) + .map_err(python_error)?; + Ok(PyBytes::new(py, &image).unbind()) +} + +#[pyfunction] +fn _load_eager_reduction_groups_v1( + py: Python<'_>, + artifact_root: PathBuf, + process_id: String, +) -> PyResult> { + let result_process_id = process_id.clone(); + let reduction_groups = py + .detach(move || NativeRuntime::load_eager_reduction_groups(artifact_root, &process_id)) + .map_err(python_error)?; + let result = PyDict::new(py); + result.set_item("abi", "pyamplicol-eager-reduction-groups-v1")?; + result.set_item("runtime_layout_abi", "pyamplicol-eager-runtime-layout-v1")?; + result.set_item("process_id", result_process_id)?; + result.set_item( + "reduction_groups", + json_value_to_python(py, &reduction_groups)?, + )?; + Ok(result.into_any().unbind()) +} + +#[pyfunction] +fn _load_eager_exact_sections_v1( + py: Python<'_>, + artifact_root: PathBuf, + process_id: String, +) -> PyResult> { + let sections = py + .detach(move || NativeRuntime::load_eager_exact_sections(artifact_root, &process_id)) + .map_err(python_error)?; + eager_exact_sections_to_python(py, sections) +} + +#[pyfunction] +fn _load_recurrence_exact_sections_v1( + py: Python<'_>, + artifact_root: PathBuf, + process_id: String, +) -> PyResult> { + let sections = py + .detach(move || NativeRuntime::load_recurrence_exact_sections(artifact_root, &process_id)) + .map_err(python_error)?; + recurrence_exact_sections_to_python(py, sections) +} + +fn eager_exact_sections_to_python( + py: Python<'_>, + sections: rusticol_core::NativeEagerExactSections, +) -> PyResult> { + let result = PyDict::new(py); + result.set_item("abi", "pyamplicol-eager-exact-sections-v1")?; + result.set_item("runtime_layout_abi", "pyamplicol-eager-runtime-layout-v1")?; + result.set_item("process_id", sections.process_id)?; + result.set_item( + "exact_schema", + json_value_to_python(py, §ions.exact_schema)?, + )?; + result.set_item( + "reduction_groups", + json_value_to_python(py, §ions.reduction_groups)?, + )?; + result.set_item("selector_group_ids", sections.selector_group_ids)?; + result.set_item("selector_domains", sections.selector_domains)?; + result.set_item( + "couplings", + sections + .couplings + .into_iter() + .map(|row| { + ( + row.real_parameter_id, + row.imaginary_parameter_id, + row.constant_real, + row.constant_imaginary, + ) + }) + .collect::>(), + )?; + result.set_item( + "stages", + sections + .stages + .into_iter() + .map(|row| { + ( + row.stage_index, + row.invocation_start, + row.invocation_count, + row.attachment_start, + row.attachment_count, + row.finalization_start, + row.finalization_count, + ) + }) + .collect::>(), + )?; + result.set_item( + "invocations", + sections + .invocations + .into_iter() + .map(|row| { + ( + row.kernel_id, + row.left_value_slot_id, + row.right_value_slot_id, + row.left_momentum_slot_id, + row.right_momentum_slot_id, + row.coupling_slot_id, + row.output_factor_source, + row.attachment_start, + row.attachment_count, + row.selector_domain_id, + ) + }) + .collect::>(), + )?; + result.set_item( + "attachments", + sections + .attachments + .into_iter() + .map(|row| { + ( + row.result_current_id, + row.factor_numerators + .into_iter() + .map(|[real, imaginary]| (real, imaginary)) + .collect::>(), + row.factor_denominator + .map(|[real, imaginary]| (real, imaginary)), + row.selector_domain_id, + ) + }) + .collect::>(), + )?; + result.set_item( + "finalizations", + sections + .finalizations + .into_iter() + .map(|row| { + ( + row.kernel_id, + row.current_id, + row.unpropagated_value_slot_id, + row.propagated_value_slot_id, + row.momentum_slot_id, + row.unpropagated_selector_domain_id, + row.propagated_selector_domain_id, + ) + }) + .collect::>(), + )?; + result.set_item( + "closures", + sections + .closures + .into_iter() + .map(|row| { + ( + row.kernel_id, + row.left_value_slot_id, + row.right_value_slot_id, + row.amplitude_index, + row.coupling_slot_id, + row.output_factor_source, + row.factor_numerators + .into_iter() + .map(|[real, imaginary]| (real, imaginary)) + .collect::>(), + row.factor_denominator + .map(|[real, imaginary]| (real, imaginary)), + row.direct_coefficients.map(|values| { + values + .into_iter() + .map(|[real, imaginary]| (real, imaginary)) + .collect::>() + }), + row.coherent_group_id, + row.selector_domain_id, + ) + }) + .collect::>(), + )?; + Ok(result.into_any().unbind()) +} + +fn recurrence_exact_sections_to_python( + py: Python<'_>, + sections: rusticol_core::NativeRecurrenceExactSections, +) -> PyResult> { + let result = PyDict::new(py); + result.set_item("abi", "pyamplicol-recurrence-exact-sections-v1")?; + result.set_item( + "runtime_layout_abi", + "pyamplicol-recurrence-runtime-layout-v2", + )?; + result.set_item("process_id", sections.process_id)?; + result.set_item("strategy", sections.strategy)?; + result.set_item("semantic_digest", sections.semantic_digest)?; + result.set_item("runtime_layout_digest", sections.runtime_layout_digest)?; + result.set_item( + "counts", + ( + sections.current_arena_components, + sections.amplitude_destination_count, + sections.parameter_value_count, + sections.external_source_count, + ), + )?; + result.set_item( + "currents", + sections + .currents + .into_iter() + .map(|row| { + ( + row.semantic_current_id, + row.node_kind as u16, + row.state_template_id, + row.component_base, + row.component_count, + row.momentum_form_id, + row.stage, + row.selector_domain_id, + row.first_use, + row.last_use, + row.source_row_or_sentinel, + row.finalization_row_or_sentinel, + ) + }) + .collect::>(), + )?; + result.set_item( + "sources", + sections + .sources + .into_iter() + .map(|row| { + ( + row.source_slot, + row.destination_component_base, + row.momentum_form_id, + row.source_template_or_dispatch_domain, + row.spin_state_class, + row.exact_factor_id, + row.selector_domain_id, + ) + }) + .collect::>(), + )?; + result.set_item( + "contributions", + sections + .contributions + .into_iter() + .map(|row| { + ( + row.parent0_component_base, + row.parent1_component_base_or_sentinel, + row.parent0_momentum_form_id, + row.parent1_momentum_form_id_or_sentinel, + row.destination_component_base, + row.exact_factor_id, + row.selector_domain_id, + row.flags, + ) + }) + .collect::>(), + )?; + result.set_item( + "finalizations", + sections + .finalizations + .into_iter() + .map(|row| { + ( + row.component_base, + row.component_count, + row.momentum_form_id, + row.exact_factor_id, + row.selector_domain_id, + row.flags, + ) + }) + .collect::>(), + )?; + result.set_item( + "closures", + sections + .closures + .into_iter() + .map(|row| { + ( + row.parent0_component_base, + row.parent1_component_base_or_sentinel, + row.parent0_momentum_form_id, + row.parent1_momentum_form_id_or_sentinel, + row.amplitude_destination_id, + row.exact_factor_id, + row.component_factor_start, + row.component_count, + row.selector_domain_id, + row.flags, + ) + }) + .collect::>(), + )?; + result.set_item( + "row_groups", + sections + .row_groups + .into_iter() + .map(|row| { + ( + row.stage, + row.role as u16, + row.destination_operation as u16, + row.direct_executor_id, + row.row_start, + row.row_count, + ) + }) + .collect::>(), + )?; + result.set_item( + "momentum_forms", + sections + .momentum_forms + .into_iter() + .map(|row| (row.term_start, row.term_count)) + .collect::>(), + )?; + result.set_item( + "momentum_terms", + sections + .momentum_terms + .into_iter() + .map(|row| (row.source_slot, row.coefficient)) + .collect::>(), + )?; + result.set_item( + "replay_targets", + sections + .replay_targets + .into_iter() + .map(|row| { + ( + row.public_flow_id, + row.representative_id, + row.source_permutation_start, + row.source_permutation_count, + row.helicity_map_start, + row.helicity_map_count, + row.phase_exact_factor_id, + row.multiplicity, + row.selector_domain_id, + ) + }) + .collect::>(), + )?; + result.set_item("source_permutations", sections.source_permutations)?; + result.set_item("replay_momentum_signs", sections.replay_momentum_signs)?; + result.set_item("replay_helicity_map", sections.replay_helicity_map)?; + result.set_item( + "source_state_assignments", + sections + .source_state_assignments + .into_iter() + .map(|row| (row.source_slot, row.state_index)) + .collect::>(), + )?; + let source_dispatch_variants = sections + .source_dispatch_variants + .into_iter() + .map(|row| { + Ok(vec![ + i64::try_from(row.embedding_start).map_err(|_| { + PyValueError::new_err("recurrence source embedding offset exceeds i64") + })?, + i64::try_from(row.projection_start).map_err(|_| { + PyValueError::new_err("recurrence source projection offset exceeds i64") + })?, + i64::from(row.source_row_id), + i64::from(row.dispatch_domain_id), + i64::from(row.runtime_variant_id), + i64::from(row.source_state_index), + i64::from(row.source_template_id), + i64::from(row.source_state_template_id), + i64::from(row.crossed_state_template_id), + i64::from(row.crossed_spin_state_class), + i64::from(row.direct_executor_id), + i64::from(row.crossing_exact_factor_id), + i64::from(row.embedding_count), + i64::from(row.projection_count), + ]) + }) + .collect::>>()?; + result.set_item("source_dispatch_variants", source_dispatch_variants)?; + result.set_item( + "source_embeddings", + sections + .source_embeddings + .into_iter() + .map(|row| { + ( + row.full_component, + row.source_component_or_sentinel, + row.exact_factor_id, + ) + }) + .collect::>(), + )?; + result.set_item( + "source_projections", + sections + .source_projections + .into_iter() + .map(|row| (row.source_component, row.full_component)) + .collect::>(), + )?; + result.set_item( + "resolved_source_selections", + sections + .resolved_source_selections + .into_iter() + .map(|row| (row.source_slot, row.dispatch_variant_id)) + .collect::>(), + )?; + result.set_item( + "amplitude_destinations", + sections + .amplitude_destinations + .into_iter() + .map(|row| { + ( + row.closure_row_start, + row.id, + row.target_sector_id, + row.target_helicity_id_or_sentinel, + row.closure_row_count, + row.selector_domain_id, + ) + }) + .collect::>(), + )?; + result.set_item( + "resolved_helicities", + sections + .resolved_helicities + .into_iter() + .map(|row| { + ( + row.source_state_start, + row.source_selection_start, + row.public_helicity_start, + row.id, + row.source_state_count, + row.source_selection_count, + row.public_helicity_count, + row.selector_domain_id, + ) + }) + .collect::>(), + )?; + result.set_item("public_helicities", sections.public_helicities)?; + result.set_item( + "exact_factors", + sections + .exact_factors + .into_iter() + .map(|factor| { + ( + factor.real_numerator, + factor.real_denominator, + factor.imaginary_numerator, + factor.imaginary_denominator, + ) + }) + .collect::>(), + )?; + result.set_item("public_flow_ids", sections.public_flow_ids)?; + result.set_item( + "executors", + sections + .executors + .into_iter() + .map(|executor| { + ( + executor.direct_executor_id, + executor.role, + executor.destination_operation, + executor.parent_component_counts, + executor.destination_component_count, + executor.momentum_operand_count, + executor.prepared_kernel_id, + executor.runtime_template, + ) + }) + .collect::>(), + )?; + Ok(result.into_any().unbind()) +} + +fn json_value_to_python(py: Python<'_>, value: &Value) -> PyResult> { + match value { + Value::Null => Ok(py.None()), + Value::Bool(value) => value.into_py_any(py), + Value::Number(value) => { + if let Some(value) = value.as_i64() { + value.into_py_any(py) + } else if let Some(value) = value.as_u64() { + value.into_py_any(py) + } else if let Some(value) = value.as_f64() { + value.into_py_any(py) + } else { + Err(ArtifactError::new_err( + "compact eager exact schema contains an invalid number", + )) + } + } + Value::String(value) => value.as_str().into_py_any(py), + Value::Array(values) => { + let values = values + .iter() + .map(|value| json_value_to_python(py, value)) + .collect::>>()?; + Ok(PyList::new(py, values)?.into_any().unbind()) + } + Value::Object(values) => { + let result = PyDict::new(py); + for (key, value) in values { + result.set_item(key, json_value_to_python(py, value)?)?; + } + Ok(result.into_any().unbind()) + } + } +} + +#[pymodule] +fn _rusticol(module: &Bound<'_, PyModule>) -> PyResult<()> { + module.add("RusticolError", module.py().get_type::())?; + module.add("ArtifactError", module.py().get_type::())?; + module.add( + "CompatibilityError", + module.py().get_type::(), + )?; + module.add("EvaluationError", module.py().get_type::())?; + module.add("SelectorError", module.py().get_type::())?; + module.add( + "ModelParameterError", + module.py().get_type::(), + )?; + module.add_class::()?; + module.add_class::()?; + module.add_class::()?; + module.add_class::()?; + module.add_class::()?; + module.add_class::()?; + module.add_class::()?; + module.add_class::()?; + module.add_class::()?; + module.add_class::()?; + module.add_class::()?; + module.add_function(wrap_pyfunction!(abi_version, module)?)?; + module.add_function(wrap_pyfunction!(package_version, module)?)?; + module.add_function(wrap_pyfunction!(native_build_inputs_sha256, module)?)?; + module.add_function(wrap_pyfunction!(target_info, module)?)?; + module.add_function(wrap_pyfunction!(_preflight_eager_kernel_pack, module)?)?; + module.add_function(wrap_pyfunction!( + _compile_symjit_plane_application_v2, + module + )?)?; + module.add_function(wrap_pyfunction!(_build_recurrence_bootstrap_v1, module)?)?; + module.add_function(wrap_pyfunction!(_eager_direct_descriptor_v1, module)?)?; + module.add_function(wrap_pyfunction!(_load_eager_reduction_groups_v1, module)?)?; + module.add_function(wrap_pyfunction!(_load_eager_exact_sections_v1, module)?)?; + module.add_function(wrap_pyfunction!( + _load_recurrence_exact_sections_v1, + module + )?)?; + #[cfg(feature = "numpy")] + module.add_function(wrap_pyfunction!( + eager_lowering::_lower_eager_runtime_v1, + module + )?)?; + #[cfg(feature = "numpy")] + module.add_function(wrap_pyfunction!( + recurrence::_lower_recurrence_direct_v2, + module + )?)?; + #[cfg(all(feature = "numpy", feature = "on-the-fly-test-support"))] + module.add_function(wrap_pyfunction!( + recurrence::_on_the_fly_test_support_probe_v1, + module + )?)?; + #[cfg(all(feature = "numpy", feature = "on-the-fly-test-support"))] + module.add_function(wrap_pyfunction!( + recurrence::_on_the_fly_query_family_census_v1, + module + )?)?; + #[cfg(all(feature = "numpy", feature = "on-the-fly-test-support"))] + module.add_function(wrap_pyfunction!( + recurrence::_on_the_fly_artifact_probe_v1, + module + )?)?; + #[cfg(all(feature = "numpy", feature = "on-the-fly-test-support"))] + module.add_function(wrap_pyfunction!( + recurrence::_on_the_fly_execution_diagnostic_v1, + module + )?)?; + #[cfg(feature = "numpy")] + module.add_function(wrap_pyfunction!( + recurrence_template::_validate_recurrence_template_input_v1, + module + )?)?; + #[cfg(feature = "numpy")] + module.add_function(wrap_pyfunction!( + recurrence::_build_on_the_fly_process_seeds_v1, + module + )?)?; + #[cfg(feature = "numpy")] + module.add_function(wrap_pyfunction!( + recurrence::_build_on_the_fly_process_seed_v1, + module + )?)?; + #[cfg(feature = "numpy")] + module.add_function(wrap_pyfunction!( + recurrence::_inspect_on_the_fly_process_seed_v1, + module + )?)?; + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use rusticol_core::{ + ContractedColor as CoreContractedColor, Coverage, ExternalParticle as CoreExternalParticle, + Helicity, LcColorFlow, Reduction, ReductionGroup as CoreReductionGroup, ReductionKind, + SelectorCapabilities, + }; + + #[cfg(not(feature = "on-the-fly-test-support"))] + #[test] + fn default_build_exposes_loaded_on_the_fly_state_census_binding() { + let binding: fn(&Runtime) -> PyResult> = + Runtime::_on_the_fly_runtime_state_census_json; + let _ = binding; + } + + #[test] + fn raw_runtime_accepts_only_f64_precision() { + assert!(require_raw_f64_precision(16).is_ok()); + let error = require_raw_f64_precision(32).unwrap_err(); + assert_eq!(error.kind(), RusticolErrorKind::Compatibility); + assert!(error.to_string().contains("only precision=16")); + assert!( + error + .to_string() + .contains("public Python Symbolica executor") + ); + } + + #[test] + fn detached_runtime_guard_allows_only_its_origin_thread() { + let origin = thread::current().id(); + assert!(require_origin_thread(origin).is_ok()); + + let error = thread::spawn(move || require_origin_thread(origin).unwrap_err()) + .join() + .expect("thread guard check must not panic"); + assert_eq!(error.kind(), RusticolErrorKind::Internal); + assert!(error.to_string().contains("owning thread")); + } + + #[test] + fn process_physics_maps_the_canonical_python_metadata_fields() { + let mut physics = CoreProcessPhysics { + schema_version: 1, + kind: "pyamplicol-resolved-physics".to_string(), + process_id: "p0".to_string(), + process: "d d~ > z".to_string(), + color_accuracy: ColorAccuracy::Lc, + coverage: Coverage { + helicities: "complete".to_string(), + color: "complete".to_string(), + color_kind: "physical-lc-flows".to_string(), + structural_zero_helicity_count: 0, + }, + external_particles: vec![ + core_particle(0, "d", 1, ParticleRole::Initial), + core_particle(1, "d~", -1, ParticleRole::Initial), + core_particle(2, "z", 23, ParticleRole::Final), + ], + helicities: vec![Helicity { + id: "h0".to_string(), + index: 0, + values: vec![-1, 1, 0], + computed: true, + structural_zero: false, + representative_id: "h0".to_string(), + coefficient: 2.0, + }], + color_components: vec![CoreColorComponent::LcFlow(LcColorFlow { + id: "c0".to_string(), + index: 0, + word: vec![1, 2], + computed: true, + representative_id: "c0".to_string(), + coefficient: 3.0, + })], + reduction: Reduction { + kind: ReductionKind::LcDiagonal, + groups: vec![CoreReductionGroup { + id: "group:0".to_string(), + representative_helicity_id: "h0".to_string(), + representative_color_id: "c0".to_string(), + physical_helicity_ids: vec!["h0".to_string()], + physical_color_ids: vec!["c0".to_string()], + }], + }, + model_parameters: vec![CoreModelParameter { + name: "alpha_s".to_string(), + kind: ParameterKind::Coupling, + default_real: 0.118, + default_imaginary: 0.0, + mutable: true, + }], + selectors: SelectorCapabilities { + helicity: true, + color_flow: true, + contracted_color: false, + }, + extensions: BTreeMap::new(), + }; + + physics.validate().unwrap(); + let mapped = ProcessPhysics::from_core(&physics); + + assert_eq!(mapped.process_id, "p0"); + assert_eq!(mapped.process, "d d~ > z"); + assert_eq!(mapped.color_accuracy, "lc"); + assert_eq!(mapped.helicity_coverage, "complete"); + assert_eq!(mapped.color_coverage, "complete"); + assert_eq!(mapped.color_kind, "physical-lc-flows"); + assert_eq!(mapped.structural_zero_helicity_count, 0); + assert_eq!(mapped.external_particles[0].name, "d"); + assert_eq!(mapped.external_particles[0].pdg_id, 1); + assert_eq!(mapped.external_particles[0].state, "incoming"); + assert_eq!(mapped.external_particles[2].state, "outgoing"); + assert_eq!(mapped.helicities[0].representative_id, "h0"); + assert_eq!(mapped.helicities[0].coefficient, 2.0); + assert_eq!(mapped.color_flows[0].word, vec![1, 2]); + assert_eq!(mapped.color_flows[0].coefficient, 3.0); + assert!(mapped.contracted_color_components.is_empty()); + assert_eq!(mapped.reduction.kind, "lc-diagonal"); + assert_eq!(mapped.reduction.groups[0].id, "group:0"); + assert_eq!(mapped.reduction.groups[0].physical_helicity_ids, vec!["h0"]); + assert_eq!(mapped.model_parameters[0].kind, "coupling"); + assert_eq!(mapped.model_parameters[0].default_real, 0.118); + assert!(mapped.model_parameters[0].mutable); + assert_eq!(mapped.selector_capabilities, vec!["helicity", "color_flow"]); + + physics.color_accuracy = ColorAccuracy::Full; + physics.coverage.color = "contracted".to_string(); + physics.coverage.color_kind = "contracted-color".to_string(); + physics.color_components = vec![CoreColorComponent::ContractedColor(CoreContractedColor { + id: "contracted".to_string(), + index: 0, + description: "fully contracted color".to_string(), + })]; + physics.reduction.kind = ReductionKind::ContractedColor; + physics.reduction.groups[0].representative_color_id = "contracted".to_string(); + physics.reduction.groups[0].physical_color_ids = vec!["contracted".to_string()]; + physics.selectors.color_flow = false; + physics.validate().unwrap(); + + let mapped = ProcessPhysics::from_core(&physics); + assert!(mapped.color_flows.is_empty()); + assert_eq!(mapped.contracted_color_components[0].id, "contracted"); + assert_eq!(mapped.selector_capabilities, vec!["helicity"]); + } + + #[test] + fn target_info_is_canonical_and_matches_the_core() { + let mapped = target_info(); + let core = runtime_target_info(); + assert_eq!(mapped.triple, core.triple); + assert_eq!(mapped.cpu_features, core.cpu_features); + assert!(mapped.cpu_features.windows(2).all(|pair| pair[0] < pair[1])); + } + + fn core_particle( + index: usize, + name: &str, + pdg: i32, + role: ParticleRole, + ) -> CoreExternalParticle { + CoreExternalParticle { + index, + label: index + 1, + particle: name.to_string(), + pdg, + role, + momentum_slot: index, + momentum_components: ["E", "px", "py", "pz"].map(str::to_string), + } + } +} diff --git a/pyamplicol/rust/crates/rusticol-python/src/recurrence.rs b/pyamplicol/rust/crates/rusticol-python/src/recurrence.rs new file mode 100644 index 0000000000..bafa3e8045 --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-python/src/recurrence.rs @@ -0,0 +1,5866 @@ +// SPDX-License-Identifier: 0BSD + +//! Private Python boundary for Direct-Arena recurrence lowering. +//! +//! Python supplies authenticated fixed-width builder and prepared-template +//! columns. Rust constructs the compact recurrence, lowers its direct arena +//! plan, and publishes the plan-v2 PACBIN without exposing an intermediate +//! packet schedule. + +use numpy::{PyReadonlyArrayDyn, PyUntypedArrayMethods}; +use pyo3::exceptions::{PyTypeError, PyValueError}; +use pyo3::prelude::*; +use pyo3::types::{PyAny, PyBytes, PyDict, PyList}; +use rusticol_core::recurrence::process; +use rusticol_core::recurrence::template; +use rusticol_core::recurrence::{ + AuthenticatedRecurrenceBuilderInput, CLOSURE_TARGET_DOMAIN_PROOF_KIND, CheckedTableRange, + DIRECT_NONE_U32, DirectExecutorRole, DirectRecurrencePlan, DirectRecurrenceRuntimeOptions, + DirectSelectorWorkSummary, PreparedDirectExecutorBinding, PreparedDirectExecutorCatalog, + RECURRENCE_BUILDER_INPUT_ABI, RECURRENCE_CONTRACTED_COLOR_CAPABILITY, + RECURRENCE_DIRECT_PLAN_ABI, RECURRENCE_DIRECT_RUNTIME_CAPABILITY, + RECURRENCE_DIRECT_RUNTIME_LAYOUT_ABI, RECURRENCE_DIRECT_SCHEDULE_MEMBER, + RECURRENCE_DIRECT_TEMPLATE_ABI, RECURRENCE_LC_COLOR_CAPABILITY, RecurrenceBuildProgress, + RecurrenceGenerationTelemetry, RecurrenceRelationDiscoveryMode, + RecurrenceRelationDiscoveryOptions, RecurrenceRelationDiscoveryReport, RecurrenceStrategy, + SemanticDigest, authenticate_recurrence_numerical_relation_provenance, checked_usize, + lower_recurrence_direct_plan_v2, lower_recurrence_direct_plan_v2_with_helicity_dispatch, + lower_recurrence_direct_plan_v2_with_relation_discovery, + write_recurrence_direct_plan_pacbin_with_projection_certificate, + write_recurrence_helicity_dispatch_v1_atomic, +}; +#[cfg(feature = "on-the-fly-test-support")] +use rusticol_core::recurrence::{ + ConstructionTransitionDiagnosticRowV1, ExactComplexRational, OnTheFlyQueryFamilyCensusV1, + OnTheFlyTestSupportReportV1, on_the_fly_query_family_census_v1, + on_the_fly_test_support_probe_v1, +}; +#[cfg(feature = "on-the-fly-test-support")] +use rusticol_core::{ + NativeOnTheFlyArtifactProbeV1, NativeOnTheFlyExecutionDiagnosticV1, NativeRuntime, +}; +use rusticol_core::{ + NativeRecurrenceExactExecutor, NativeRecurrenceExactFactor, NativeRecurrenceExactSections, + RusticolError, RusticolResult, +}; +use serde_json::{Map as JsonMap, Value as JsonValue}; +use sha2::{Digest, Sha256}; +use std::collections::{BTreeMap, BTreeSet}; +use std::path::PathBuf; +use std::time::Instant; + +use crate::python_error; +use crate::recurrence_numerical_evidence::{ + authenticated_runtime_parameter_contract, parse_numerical_relation_evidence, +}; + +const RUNTIME_CONTAINER_KIND: &str = "pyamplicol-recurrence-runtime-container"; +const RUNTIME_CONTAINER_SCHEMA_VERSION: u32 = 1; +const STORAGE_ABI: &str = "pacbin-v1"; +const DIRECT_BUILDER_INPUT_ABI: &str = "pyamplicol-recurrence-builder-input-v2"; +const DIRECT_LOWERING_RESULT_KIND: &str = "pyamplicol-recurrence-direct-lowering-result"; +const DIRECT_LOWERING_RESULT_SCHEMA_VERSION: u32 = 4; +const RECURRENCE_HELICITY_DISPATCH_ABI: &str = "pyamplicol-recurrence-helicity-dispatch-v1"; +const DIRECT_CANONICALIZATION_ABI: &str = "pyamplicol-canonical-json-v1"; +const DIRECT_BACKEND_ABI: &str = "rusticol.recurrence-direct-backend.v1"; +const DIRECT_PAYLOAD_BINDING_ABI: &str = "pyamplicol-recurrence-plane-binding-v2"; +const DIRECT_IDENTITY_FINALIZER: &str = "rusticol.identity-finalize-in-place.v1"; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum PrimitiveKind { + U8, + U32, + U64, + I32, +} + +impl PrimitiveKind { + const fn dtype(self) -> &'static str { + match self { + Self::U8 => "|u1", + Self::U32 => " " " Option { + match value { + "|u1" => Some(Self::U8), + " Some(Self::U32), + " Some(Self::U64), + " Some(Self::I32), + _ => None, + } + } +} + +#[derive(Clone, Copy)] +struct ColumnSpec { + name: &'static str, + kind: PrimitiveKind, + tail_shape: &'static [usize], +} + +#[derive(Clone, Copy)] +struct TableSpec { + name: &'static str, + columns: &'static [ColumnSpec], +} + +const fn column(name: &'static str, kind: PrimitiveKind) -> ColumnSpec { + ColumnSpec { + name, + kind, + tail_shape: &[], + } +} + +const fn shaped_column( + name: &'static str, + kind: PrimitiveKind, + tail_shape: &'static [usize], +) -> ColumnSpec { + ColumnSpec { + name, + kind, + tail_shape, + } +} + +const TABLE_SPECS: &[TableSpec] = &[ + TableSpec { + name: "bitset_ranges", + columns: &[ + column("id", PrimitiveKind::U32), + column("start", PrimitiveKind::U64), + column("count", PrimitiveKind::U64), + column("bit_count", PrimitiveKind::U64), + ], + }, + TableSpec { + name: "bitset_words", + columns: &[column("value", PrimitiveKind::U64)], + }, + TableSpec { + name: "coupling_limits", + columns: &[ + column("name_string_id", PrimitiveKind::U32), + column("minimum", PrimitiveKind::U32), + column("maximum", PrimitiveKind::U32), + ], + }, + TableSpec { + name: "digest_catalog", + columns: &[ + column("id", PrimitiveKind::U32), + shaped_column("value", PrimitiveKind::U8, &[32]), + ], + }, + TableSpec { + name: "exact_factors", + columns: &[ + column("id", PrimitiveKind::U32), + column("real_numerator_string_id", PrimitiveKind::U32), + column("real_denominator_string_id", PrimitiveKind::U32), + column("imag_numerator_string_id", PrimitiveKind::U32), + column("imag_denominator_string_id", PrimitiveKind::U32), + ], + }, + TableSpec { + name: "external_legs", + columns: &[ + column("source_slot", PrimitiveKind::U32), + column("public_label", PrimitiveKind::U32), + column("physical_pdg", PrimitiveKind::I32), + column("outgoing_pdg", PrimitiveKind::I32), + column("is_initial", PrimitiveKind::U8), + column("is_fermionic", PrimitiveKind::U8), + column("source_state_start", PrimitiveKind::U64), + column("source_state_count", PrimitiveKind::U64), + column("momentum_mask_id", PrimitiveKind::U32), + column("support_mask_id", PrimitiveKind::U32), + ], + }, + TableSpec { + name: "header", + columns: &[ + column("schema_version", PrimitiveKind::U32), + column("abi_string_id", PrimitiveKind::U32), + column("process_id_string_id", PrimitiveKind::U32), + column("layout", PrimitiveKind::U8), + column("selected_flow_mode", PrimitiveKind::U8), + column("selected_source_mode", PrimitiveKind::U8), + column("external_leg_count", PrimitiveKind::U32), + column("physical_sector_count", PrimitiveKind::U32), + column("public_flow_count", PrimitiveKind::U32), + column("replay_partition_count", PrimitiveKind::U32), + column("coupling_limit_count", PrimitiveKind::U32), + column("parameter_projection_count", PrimitiveKind::U32), + column("process_support_mask_id", PrimitiveKind::U32), + ], + }, + TableSpec { + name: "header_digests", + columns: &[ + column("role_string_id", PrimitiveKind::U32), + column("digest_id", PrimitiveKind::U32), + ], + }, + TableSpec { + name: "lc_open_strings", + columns: &[ + column("sector_id", PrimitiveKind::U32), + column("ordinal", PrimitiveKind::U32), + column("fundamental_source_slot", PrimitiveKind::U32), + column("antifundamental_source_slot", PrimitiveKind::U32), + column("adjoint_sequence_id", PrimitiveKind::U32), + column("singlet_sequence_id", PrimitiveKind::U32), + ], + }, + TableSpec { + name: "normalization", + columns: &[ + column("factor_id", PrimitiveKind::U32), + column("convention_string_id", PrimitiveKind::U32), + column("semantic_digest_id", PrimitiveKind::U32), + ], + }, + TableSpec { + name: "parameter_projection", + columns: &[ + column("runtime_slot", PrimitiveKind::U32), + column("runtime_name_string_id", PrimitiveKind::U32), + column("parameter_template_id", PrimitiveKind::U32), + column("prepared_parameter_id", PrimitiveKind::U32), + column("component", PrimitiveKind::U32), + ], + }, + TableSpec { + name: "physical_lc_sectors", + columns: &[ + column("sector_id", PrimitiveKind::U32), + column("public_id_string_id", PrimitiveKind::U32), + column("kind", PrimitiveKind::U8), + column("closure_source_slot", PrimitiveKind::U32), + column("closure_proof_algorithm_string_id", PrimitiveKind::U32), + column("closure_proof_digest_id", PrimitiveKind::U32), + column("open_string_start", PrimitiveKind::U64), + column("open_string_count", PrimitiveKind::U64), + column("trace_sequence_id", PrimitiveKind::U32), + column("singlet_sequence_id", PrimitiveKind::U32), + column("word_sequence_id", PrimitiveKind::U32), + column("support_mask_id", PrimitiveKind::U32), + ], + }, + TableSpec { + name: "public_lc_flows", + columns: &[ + column("flow_id", PrimitiveKind::U32), + column("public_id_string_id", PrimitiveKind::U32), + column("construction_sector_id", PrimitiveKind::U32), + column("word_sequence_id", PrimitiveKind::U32), + column("source_slot_permutation_sequence_id", PrimitiveKind::U32), + column("reduction_weight_factor_id", PrimitiveKind::U32), + ], + }, + TableSpec { + name: "replay_partitions", + columns: &[ + column("partition_id", PrimitiveKind::U32), + column("representative_sector_id", PrimitiveKind::U32), + column("materialized_sector_id", PrimitiveKind::U32), + column("target_start", PrimitiveKind::U64), + column("target_count", PrimitiveKind::U64), + column("proof_algorithm_string_id", PrimitiveKind::U32), + column("proof_digest_id", PrimitiveKind::U32), + ], + }, + TableSpec { + name: "replay_targets", + columns: &[ + column("partition_id", PrimitiveKind::U32), + column("sector_id", PrimitiveKind::U32), + column("external_permutation_sequence_id", PrimitiveKind::U32), + column("source_slot_permutation_sequence_id", PrimitiveKind::U32), + column("amplitude_phase_factor_id", PrimitiveKind::U32), + column("fermion_sign", PrimitiveKind::I32), + ], + }, + TableSpec { + name: "selected_public_flow_coverage", + columns: &[column("flow_id", PrimitiveKind::U32)], + }, + TableSpec { + name: "selected_source_coverage", + columns: &[ + column("source_slot", PrimitiveKind::U32), + column("source_state_index", PrimitiveKind::U32), + ], + }, + TableSpec { + name: "semantic_template_references", + columns: &[ + column("kind_string_id", PrimitiveKind::U32), + column("template_id", PrimitiveKind::U32), + column("semantic_digest_id", PrimitiveKind::U32), + column("prepared_kernel_id", PrimitiveKind::U32), + ], + }, + TableSpec { + name: "source_states", + columns: &[ + column("source_slot", PrimitiveKind::U32), + column("state_index", PrimitiveKind::U32), + column("public_helicity", PrimitiveKind::I32), + column("chirality", PrimitiveKind::I32), + column("spin_state", PrimitiveKind::I32), + column("current_state_template_id", PrimitiveKind::U32), + column("source_template_id", PrimitiveKind::U32), + column("momentum_sign", PrimitiveKind::I32), + column("crossing_phase_factor_id", PrimitiveKind::U32), + ], + }, + TableSpec { + name: "string_bytes", + columns: &[column("value", PrimitiveKind::U8)], + }, + TableSpec { + name: "string_ranges", + columns: &[ + column("start", PrimitiveKind::U64), + column("count", PrimitiveKind::U64), + ], + }, + TableSpec { + name: "u32_sequence_ranges", + columns: &[ + column("start", PrimitiveKind::U64), + column("count", PrimitiveKind::U64), + ], + }, + TableSpec { + name: "u32_sequence_values", + columns: &[column("value", PrimitiveKind::U32)], + }, +]; + +const FERMION_PAIRING_TABLE_SPECS: &[TableSpec] = &[ + TableSpec { + name: "header", + columns: &[ + column("schema_version", PrimitiveKind::U32), + column("abi_string_id", PrimitiveKind::U32), + column("process_key_string_id", PrimitiveKind::U32), + column("proof_algorithm_string_id", PrimitiveKind::U32), + column("source_count", PrimitiveKind::U32), + column("endpoint_count", PrimitiveKind::U32), + column("pairing_class_count", PrimitiveKind::U32), + column("rule_count", PrimitiveKind::U32), + column("endpoint_state_template_count", PrimitiveKind::U64), + column("endpoint_anti_state_template_count", PrimitiveKind::U64), + column("endpoint_basis_count", PrimitiveKind::U64), + column("endpoint_color_representation_count", PrimitiveKind::U64), + column("class_fundamental_slot_count", PrimitiveKind::U64), + column("class_antifundamental_slot_count", PrimitiveKind::U64), + column("class_reference_pairing_count", PrimitiveKind::U64), + column("rule_class_pairing_index_count", PrimitiveKind::U64), + column("rule_endpoint_pairing_count", PrimitiveKind::U64), + column("rule_source_permutation_count", PrimitiveKind::U64), + column("rule_lineage_count", PrimitiveKind::U64), + column("exact_integer_count", PrimitiveKind::U32), + column("exact_integer_limb_count", PrimitiveKind::U64), + column("string_count", PrimitiveKind::U32), + column("string_byte_count", PrimitiveKind::U64), + column("no_fermion_line", PrimitiveKind::U32), + shaped_column("topology_digest", PrimitiveKind::U8, &[32]), + shaped_column("semantic_digest", PrimitiveKind::U8, &[32]), + ], + }, + TableSpec { + name: "endpoints", + columns: &[ + column("endpoint_id", PrimitiveKind::U32), + column("source_slot", PrimitiveKind::U32), + column("public_label", PrimitiveKind::U32), + column("species_class_id", PrimitiveKind::U32), + column("species_string_id", PrimitiveKind::U32), + column("particle_orientation", PrimitiveKind::U8), + column("color_orientation", PrimitiveKind::U8), + column("state_template_start", PrimitiveKind::U64), + column("state_template_count", PrimitiveKind::U64), + column("anti_state_template_start", PrimitiveKind::U64), + column("anti_state_template_count", PrimitiveKind::U64), + column("basis_start", PrimitiveKind::U64), + column("basis_count", PrimitiveKind::U64), + column("color_representation_start", PrimitiveKind::U64), + column("color_representation_count", PrimitiveKind::U64), + shaped_column("contract_digest", PrimitiveKind::U8, &[32]), + ], + }, + TableSpec { + name: "endpoint_state_template_ids", + columns: &[column("string_id", PrimitiveKind::U32)], + }, + TableSpec { + name: "endpoint_anti_state_template_ids", + columns: &[column("string_id", PrimitiveKind::U32)], + }, + TableSpec { + name: "endpoint_basis_ids", + columns: &[column("string_id", PrimitiveKind::U32)], + }, + TableSpec { + name: "endpoint_color_representations", + columns: &[column("value", PrimitiveKind::I32)], + }, + TableSpec { + name: "pairing_classes", + columns: &[ + column("class_id", PrimitiveKind::U32), + column("species_class_id", PrimitiveKind::U32), + column("species_string_id", PrimitiveKind::U32), + column("fundamental_slot_start", PrimitiveKind::U64), + column("fundamental_slot_count", PrimitiveKind::U64), + column("antifundamental_slot_start", PrimitiveKind::U64), + column("antifundamental_slot_count", PrimitiveKind::U64), + column("reference_pairing_start", PrimitiveKind::U64), + column("reference_pairing_count", PrimitiveKind::U64), + column("pairing_count", PrimitiveKind::U64), + shaped_column("proof_digest", PrimitiveKind::U8, &[32]), + ], + }, + TableSpec { + name: "class_fundamental_slots", + columns: &[column("source_slot", PrimitiveKind::U32)], + }, + TableSpec { + name: "class_antifundamental_slots", + columns: &[column("source_slot", PrimitiveKind::U32)], + }, + TableSpec { + name: "class_reference_pairings", + columns: &[ + column("fundamental_source_slot", PrimitiveKind::U32), + column("antifundamental_source_slot", PrimitiveKind::U32), + ], + }, + TableSpec { + name: "rules", + columns: &[ + column("rule_id", PrimitiveKind::U32), + column("class_pairing_index_start", PrimitiveKind::U64), + column("class_pairing_index_count", PrimitiveKind::U64), + column("endpoint_pairing_start", PrimitiveKind::U64), + column("endpoint_pairing_count", PrimitiveKind::U64), + column("source_permutation_start", PrimitiveKind::U64), + column("source_permutation_count", PrimitiveKind::U64), + column("lineage_start", PrimitiveKind::U64), + column("lineage_count", PrimitiveKind::U64), + column("fermion_parity", PrimitiveKind::I32), + column("real_numerator_integer_id", PrimitiveKind::U32), + column("real_denominator_integer_id", PrimitiveKind::U32), + column("imag_numerator_integer_id", PrimitiveKind::U32), + column("imag_denominator_integer_id", PrimitiveKind::U32), + column("multiplicity", PrimitiveKind::U64), + column("proof_algorithm_string_id", PrimitiveKind::U32), + shaped_column("proof_digest", PrimitiveKind::U8, &[32]), + ], + }, + TableSpec { + name: "rule_class_pairing_indices", + columns: &[ + column("class_id", PrimitiveKind::U32), + column("pairing_index", PrimitiveKind::U64), + ], + }, + TableSpec { + name: "rule_endpoint_pairings", + columns: &[ + column("fundamental_source_slot", PrimitiveKind::U32), + column("antifundamental_source_slot", PrimitiveKind::U32), + ], + }, + TableSpec { + name: "rule_source_slot_permutations", + columns: &[column("source_slot", PrimitiveKind::U32)], + }, + TableSpec { + name: "rule_lineages", + columns: &[column("line_id", PrimitiveKind::U32)], + }, + TableSpec { + name: "exact_integers", + columns: &[ + column("integer_id", PrimitiveKind::U32), + column("sign", PrimitiveKind::I32), + column("limb_start", PrimitiveKind::U64), + column("limb_count", PrimitiveKind::U64), + ], + }, + TableSpec { + name: "exact_integer_limbs", + columns: &[column("value", PrimitiveKind::U64)], + }, + TableSpec { + name: "string_ranges", + columns: &[ + column("string_id", PrimitiveKind::U32), + column("start", PrimitiveKind::U64), + column("count", PrimitiveKind::U64), + ], + }, + TableSpec { + name: "string_bytes", + columns: &[column("value", PrimitiveKind::U8)], + }, +]; + +#[derive(Clone)] +enum OwnedValues { + U8(Vec), + U32(Vec), + U64(Vec), + I32(Vec), +} + +impl OwnedValues { + fn raw_bytes(&self) -> &[u8] { + fn bytes(values: &[T]) -> &[u8] { + // Accepted multi-byte arrays are explicitly little-endian and this + // module rejects big-endian hosts before extraction. + unsafe { + std::slice::from_raw_parts( + values.as_ptr().cast::(), + std::mem::size_of_val(values), + ) + } + } + match self { + Self::U8(values) => values, + Self::U32(values) => bytes(values), + Self::U64(values) => bytes(values), + Self::I32(values) => bytes(values), + } + } + + fn as_u8(&self, context: &str) -> RusticolResult<&[u8]> { + match self { + Self::U8(values) => Ok(values), + _ => Err(wrong_type(context, "u8")), + } + } + + fn as_u32(&self, context: &str) -> RusticolResult<&[u32]> { + match self { + Self::U32(values) => Ok(values), + _ => Err(wrong_type(context, "u32")), + } + } + + fn as_u64(&self, context: &str) -> RusticolResult<&[u64]> { + match self { + Self::U64(values) => Ok(values), + _ => Err(wrong_type(context, "u64")), + } + } + + fn as_i32(&self, context: &str) -> RusticolResult<&[i32]> { + match self { + Self::I32(values) => Ok(values), + _ => Err(wrong_type(context, "i32")), + } + } +} + +#[derive(Clone)] +struct OwnedColumn { + name: String, + dtype: &'static str, + shape: Vec, + values: OwnedValues, +} + +#[derive(Clone)] +struct OwnedTable { + name: String, + row_count: u64, + columns: Vec, + column_by_name: BTreeMap, +} + +impl OwnedTable { + fn row_count(&self) -> RusticolResult { + checked_usize(self.row_count, &format!("{} row count", self.name)) + } + + fn column(&self, name: &str) -> RusticolResult<&OwnedColumn> { + let index = self.column_by_name.get(name).ok_or_else(|| { + invalid(format!( + "recurrence builder table {:?} has no column {name:?}", + self.name + )) + })?; + Ok(&self.columns[*index]) + } + + fn u8(&self, column: &str) -> RusticolResult<&[u8]> { + self.column(column)? + .values + .as_u8(&format!("{}.{column}", self.name)) + } + + fn u32(&self, column: &str) -> RusticolResult<&[u32]> { + self.column(column)? + .values + .as_u32(&format!("{}.{column}", self.name)) + } + + fn u64(&self, column: &str) -> RusticolResult<&[u64]> { + self.column(column)? + .values + .as_u64(&format!("{}.{column}", self.name)) + } + + fn i32(&self, column: &str) -> RusticolResult<&[i32]> { + self.column(column)? + .values + .as_i32(&format!("{}.{column}", self.name)) + } +} + +#[derive(Clone)] +struct OwnedInput { + abi: String, + declared_digest: String, + tables: Vec, + table_by_name: BTreeMap, + declared_fermion_pairing_digest: Option, + fermion_pairing_tables: Vec, + fermion_pairing_table_by_name: BTreeMap, +} + +impl OwnedInput { + fn table(&self, name: &str) -> RusticolResult<&OwnedTable> { + let index = self + .table_by_name + .get(name) + .ok_or_else(|| invalid(format!("recurrence builder input has no table {name:?}")))?; + Ok(&self.tables[*index]) + } + + fn column(&self, table: &str, column: &str) -> RusticolResult<&OwnedColumn> { + self.table(table)?.column(column) + } + + fn u8(&self, table: &str, column: &str) -> RusticolResult<&[u8]> { + self.column(table, column)? + .values + .as_u8(&format!("{table}.{column}")) + } + + fn u32(&self, table: &str, column: &str) -> RusticolResult<&[u32]> { + self.column(table, column)? + .values + .as_u32(&format!("{table}.{column}")) + } + + fn u64(&self, table: &str, column: &str) -> RusticolResult<&[u64]> { + self.column(table, column)? + .values + .as_u64(&format!("{table}.{column}")) + } + + fn i32(&self, table: &str, column: &str) -> RusticolResult<&[i32]> { + self.column(table, column)? + .values + .as_i32(&format!("{table}.{column}")) + } + + fn fermion_pairing_table(&self, name: &str) -> RusticolResult<&OwnedTable> { + let index = self + .fermion_pairing_table_by_name + .get(name) + .ok_or_else(|| invalid(format!("recurrence fermion pairing has no table {name:?}")))?; + Ok(&self.fermion_pairing_tables[*index]) + } + + fn pairing_u8(&self, table: &str, column: &str) -> RusticolResult<&[u8]> { + self.fermion_pairing_table(table)?.u8(column) + } + + fn pairing_u32(&self, table: &str, column: &str) -> RusticolResult<&[u32]> { + self.fermion_pairing_table(table)?.u32(column) + } + + fn pairing_u64(&self, table: &str, column: &str) -> RusticolResult<&[u64]> { + self.fermion_pairing_table(table)?.u64(column) + } + + fn pairing_i32(&self, table: &str, column: &str) -> RusticolResult<&[i32]> { + self.fermion_pairing_table(table)?.i32(column) + } +} + +const TEMPLATE_TABLE_INVENTORY: &[(&str, usize)] = &[ + ("catalog_header", 20), + ("closures", 20), + ("color_contractions", 14), + ("color_nc_terms", 3), + ("contact_orbit_certificates", 11), + ("contact_orbit_steps", 10), + ("coupling_order_ranges", 3), + ("coupling_order_terms", 3), + ("current_states", 17), + ("digest_catalog", 2), + ("evaluator_bindings", 14), + ("exact_factors", 5), + ("flavour_flow_ranges", 3), + ("flavour_flow_values", 1), + ("i32_sequence_ranges", 3), + ("i32_sequence_values", 1), + ("lc_color_transition_witnesses", 15), + ("parameters", 11), + ("propagators", 12), + ("quantum_flows", 16), + ("quantum_number_flow_ranges", 3), + ("quantum_number_flow_terms", 3), + ("runtime_helicity_contracts", 8), + ("runtime_helicity_embeddings", 4), + ("runtime_helicity_projections", 3), + ("runtime_helicity_variants", 9), + ("sources", 20), + ("string_bytes", 1), + ("string_ranges", 2), + ("symmetry_proofs", 9), + ("transitions", 20), + ("u32_sequence_ranges", 3), + ("u32_sequence_values", 1), +]; + +struct PreparedTemplateInput { + input: OwnedInput, + canonical_digest_property: String, + catalog_digest: SemanticDigest, + compiled_model_digest: SemanticDigest, + prepared_kernel_pack_digest: SemanticDigest, +} + +struct AuthenticatedBuilderInputs { + builder_input_digest: String, + template_input_digest: String, + authenticated: AuthenticatedRecurrenceBuilderInput, +} + +fn authenticate_builder_inputs( + input: OwnedInput, + prepared_template: PreparedTemplateInput, + expected_pack_digest: SemanticDigest, +) -> RusticolResult { + validate_inventory(&input)?; + let builder_input_digest = canonical_digest(&input)?; + if builder_input_digest != input.declared_digest { + return Err(invalid(format!( + "recurrence builder input digest mismatch: declared {}, found {builder_input_digest}", + input.declared_digest + ))); + } + let template_input_digest = prepared_template.canonical_digest()?; + if prepared_template.prepared_kernel_pack_digest != expected_pack_digest { + return Err(RusticolError::integrity( + "recurrence prepared-kernel pack digest does not match the authenticated template", + )); + } + let process = decode_process_input(&input)?.validate()?; + let template = prepared_template.into_core()?.validate()?; + let authenticated = AuthenticatedRecurrenceBuilderInput::new(process, template)?; + Ok(AuthenticatedBuilderInputs { + builder_input_digest, + template_input_digest, + authenticated, + }) +} + +impl PreparedTemplateInput { + fn canonical_digest(&self) -> RusticolResult { + let mut digest = Sha256::new(); + hash_text(&mut digest, &self.input.abi)?; + digest.update(self.catalog_digest.as_bytes()); + digest.update(self.compiled_model_digest.as_bytes()); + digest.update(self.prepared_kernel_pack_digest.as_bytes()); + hash_tables(&mut digest, &self.input.tables)?; + Ok(hex_digest(digest.finalize())) + } + + fn into_core(self) -> RusticolResult { + let actual_digest = self.canonical_digest()?; + if actual_digest != self.input.declared_digest + || actual_digest != self.canonical_digest_property + { + return Err(invalid(format!( + "recurrence template input digest mismatch: declared {}, found {actual_digest}", + self.input.declared_digest + ))); + } + decode_template_input( + &self.input, + self.catalog_digest, + self.compiled_model_digest, + self.prepared_kernel_pack_digest, + ) + } +} + +#[derive(Clone, Copy, Debug, Default)] +struct DirectLoweringTimings { + python_extraction_seconds: f64, + catalog_authentication_seconds: f64, + semantic_construction_seconds: f64, + direct_lowering_seconds: f64, + serialization_seconds: f64, + native_total_seconds: f64, +} + +#[derive(Debug)] +struct NativeDirectLoweringResult { + builder_input_digest: String, + template_input_digest: String, + prepared_kernel_pack_digest: String, + direct_template_catalog_digest: String, + process_id: String, + strategy: RecurrenceStrategy, + semantic_digest: String, + runtime_layout_digest: String, + member_count: u64, + unpacked_size_bytes: u64, + index_sha256: String, + container_size: u64, + plan_payload_size: u64, + plan_sha256: String, + projection_certificate_payload_size: Option, + projection_certificate_sha256: Option, + current_count: usize, + source_row_count: usize, + contribution_count: usize, + finalization_count: usize, + closure_count: usize, + row_group_count: usize, + momentum_form_count: usize, + selector_domain_count: usize, + replay_target_count: usize, + amplitude_destination_count: usize, + resolved_helicity_count: usize, + retained_helicity_count: u64, + exact_factor_count: usize, + semantic_component_count: u64, + current_arena_components: u32, + parameter_value_count: u32, + physical_sector_count: u32, + direct_executor_count: u32, + prepared_kernel_count: usize, + resolved_helicities: Vec>, + amplitude_destinations: Vec<(u32, Option)>, + closure_target_domain_certificate: Option, + selector_work: Vec<(DirectSelectorWorkSummary, u64)>, + relation_discovery: Option, + exact_sections: NativeRecurrenceExactSections, + construction: RecurrenceConstructionMetrics, + generation_profile: RecurrenceGenerationTelemetry, + timings: DirectLoweringTimings, + helicity_dispatch: Option, +} + +#[derive(Debug)] +struct NativeClosureTargetDomainCertificate { + physical_sector_count: u32, + resolved_helicity_count: u32, + certified_structural_zero_physical_sector_ids: Vec, + semantic_digest: String, +} + +#[derive(Debug)] +struct NativeHelicityDispatchMetadata { + size_bytes: u64, + sha256: String, + base_runtime_layout_digest: String, + resolved_helicity_count: usize, +} + +#[derive(Clone, Debug, Default)] +struct RecurrenceConstructionMetrics { + peak_current_count: usize, + peak_contribution_count: usize, + peak_dynamic_color_state_count: usize, + color_target_prune_count: usize, + candidate_parent_pair_count_by_stage: BTreeMap, +} + +impl RecurrenceConstructionMetrics { + fn include(&mut self, progress: &RecurrenceBuildProgress) { + self.peak_current_count = self.peak_current_count.max(progress.current_count); + self.peak_contribution_count = self + .peak_contribution_count + .max(progress.contribution_count); + self.peak_dynamic_color_state_count = self + .peak_dynamic_color_state_count + .max(progress.dynamic_color_state_count); + self.color_target_prune_count = self + .color_target_prune_count + .max(progress.color_target_prune_count); + if let Some(stage_index) = progress.stage_index { + self.candidate_parent_pair_count_by_stage + .entry(stage_index) + .and_modify(|count| { + *count = (*count).max(progress.candidate_parent_pair_count); + }) + .or_insert(progress.candidate_parent_pair_count); + } + } + + fn candidate_parent_pair_count(&self) -> usize { + self.candidate_parent_pair_count_by_stage + .values() + .copied() + .sum() + } +} + +struct AuthenticatedDirectTemplateCatalog { + catalog: PreparedDirectExecutorCatalog, + catalog_digest: SemanticDigest, + prepared_kernel_pack_digest: SemanticDigest, + prepared_kernel_count: usize, + exact_executors: Vec, +} + +#[pyfunction(signature = ( + builder_input, + prepared_template_input, + direct_template_catalog_json, + prepared_kernel_pack_digest, + schedule_semantic_digest, + destination, + *, + point_tile_size, + workspace_mib, + relation_discovery_mode, + relation_discovery_precision_digits, + relation_discovery_probe_count, + relation_discovery_verification_probe_count, + relation_discovery_relative_tolerance, + relation_discovery_absolute_tolerance, + relation_discovery_seed, + color_accuracy, + relation_discovery_evidence_json=None, + helicity_dispatch_destination=None, + progress_callback=None +))] +#[allow(clippy::too_many_arguments)] +pub(crate) fn _lower_recurrence_direct_v2( + py: Python<'_>, + builder_input: &Bound<'_, PyAny>, + prepared_template_input: &Bound<'_, PyAny>, + direct_template_catalog_json: &Bound<'_, PyBytes>, + prepared_kernel_pack_digest: String, + schedule_semantic_digest: String, + destination: PathBuf, + point_tile_size: u32, + workspace_mib: u32, + relation_discovery_mode: String, + relation_discovery_precision_digits: u32, + relation_discovery_probe_count: u32, + relation_discovery_verification_probe_count: u32, + relation_discovery_relative_tolerance: f64, + relation_discovery_absolute_tolerance: f64, + relation_discovery_seed: u64, + color_accuracy: String, + relation_discovery_evidence_json: Option<&Bound<'_, PyBytes>>, + helicity_dispatch_destination: Option, + progress_callback: Option>, +) -> PyResult> { + let extraction_started = Instant::now(); + let owned = parse_input(builder_input)?; + let prepared_template = parse_prepared_template_input(prepared_template_input)?; + let direct_template_catalog_json = direct_template_catalog_json.as_bytes().to_vec(); + validate_sha256_text(&prepared_kernel_pack_digest, "prepared kernel pack digest")?; + validate_sha256_text(&schedule_semantic_digest, "recurrence schedule digest")?; + let relation_discovery = RecurrenceRelationDiscoveryOptions::new( + RecurrenceRelationDiscoveryMode::parse(&relation_discovery_mode).map_err(python_error)?, + relation_discovery_precision_digits, + relation_discovery_probe_count, + relation_discovery_verification_probe_count, + relation_discovery_relative_tolerance, + relation_discovery_absolute_tolerance, + relation_discovery_seed, + color_accuracy, + ) + .map_err(python_error)?; + let relation_discovery_evidence_json = + relation_discovery_evidence_json.map(|raw| raw.as_bytes().to_vec()); + let python_extraction_seconds = extraction_started.elapsed().as_secs_f64(); + + let native = py + .detach(move || { + let mut report = + |progress| report_recurrence_build_progress(progress_callback.as_ref(), progress); + lower_recurrence_direct( + owned, + prepared_template, + &direct_template_catalog_json, + &prepared_kernel_pack_digest, + &schedule_semantic_digest, + &destination, + point_tile_size, + workspace_mib, + relation_discovery, + relation_discovery_evidence_json, + helicity_dispatch_destination.as_deref(), + python_extraction_seconds, + &mut report, + ) + }) + .map_err(python_error)?; + direct_lowering_mapping(py, native) +} + +#[cfg(feature = "on-the-fly-test-support")] +#[pyfunction(signature = ( + builder_input, + prepared_template_input, + direct_template_catalog_json, + prepared_kernel_pack_digest, + selected_public_flow_id, + public_helicities +))] +pub(crate) fn _on_the_fly_test_support_probe_v1( + py: Python<'_>, + builder_input: &Bound<'_, PyAny>, + prepared_template_input: &Bound<'_, PyAny>, + direct_template_catalog_json: &Bound<'_, PyBytes>, + prepared_kernel_pack_digest: String, + selected_public_flow_id: u32, + public_helicities: Vec, +) -> PyResult> { + let input = parse_input(builder_input)?; + let prepared_template = parse_prepared_template_input(prepared_template_input)?; + let direct_template_catalog_json = direct_template_catalog_json.as_bytes().to_vec(); + validate_sha256_text(&prepared_kernel_pack_digest, "prepared kernel pack digest")?; + let native = py + .detach(move || { + let expected_pack_digest = semantic_digest_from_hex( + &prepared_kernel_pack_digest, + "prepared kernel pack digest", + )?; + let authenticated = + authenticate_builder_inputs(input, prepared_template, expected_pack_digest)? + .authenticated; + let direct_catalog = parse_direct_template_catalog( + &direct_template_catalog_json, + expected_pack_digest, + authenticated.template().summary().catalog_digest, + authenticated.template().summary().compiled_model_digest, + )?; + on_the_fly_test_support_probe_v1( + &authenticated, + &direct_catalog.catalog, + selected_public_flow_id, + &public_helicities, + ) + }) + .map_err(python_error)?; + on_the_fly_test_support_mapping(py, native) +} + +#[cfg(feature = "on-the-fly-test-support")] +#[pyfunction(signature = ( + builder_input, + prepared_template_input, + direct_template_catalog_json, + prepared_kernel_pack_digest, + selected_public_flow_ids, + public_helicities, + *, + enable_color_projection=true +))] +#[allow(clippy::too_many_arguments)] +pub(crate) fn _on_the_fly_query_family_census_v1( + py: Python<'_>, + builder_input: &Bound<'_, PyAny>, + prepared_template_input: &Bound<'_, PyAny>, + direct_template_catalog_json: &Bound<'_, PyBytes>, + prepared_kernel_pack_digest: String, + selected_public_flow_ids: Vec, + public_helicities: Vec>, + enable_color_projection: bool, +) -> PyResult> { + if selected_public_flow_ids.len() != public_helicities.len() { + return Err(PyValueError::new_err( + "on-the-fly query-family flow and helicity columns have different lengths", + )); + } + let input = parse_input(builder_input)?; + let prepared_template = parse_prepared_template_input(prepared_template_input)?; + let direct_template_catalog_json = direct_template_catalog_json.as_bytes().to_vec(); + validate_sha256_text(&prepared_kernel_pack_digest, "prepared kernel pack digest")?; + let queries = selected_public_flow_ids + .into_iter() + .zip(public_helicities) + .collect::>(); + let native = py + .detach(move || { + let expected_pack_digest = semantic_digest_from_hex( + &prepared_kernel_pack_digest, + "prepared kernel pack digest", + )?; + let authenticated = + authenticate_builder_inputs(input, prepared_template, expected_pack_digest)? + .authenticated; + let direct_catalog = parse_direct_template_catalog( + &direct_template_catalog_json, + expected_pack_digest, + authenticated.template().summary().catalog_digest, + authenticated.template().summary().compiled_model_digest, + )?; + on_the_fly_query_family_census_v1( + &authenticated, + &direct_catalog.catalog, + &queries, + enable_color_projection, + ) + }) + .map_err(python_error)?; + on_the_fly_query_family_census_mapping(py, native) +} + +#[cfg(feature = "on-the-fly-test-support")] +#[pyfunction(signature = ( + artifact_path, + process_id, + builder_input, + prepared_template_input, + direct_template_catalog_json, + prepared_kernel_pack_digest, + selected_public_flow_id, + public_helicities, + point_major_external_momenta, + point_count, + *, + parameter_overrides=None, + tamper_executor_key=false, + benchmark=false, + benchmark_warmup_repetitions=0, + benchmark_repetitions=0, + collect_current_diagnostics=true, + enable_color_projection=true, + query_family=None +))] +#[allow(clippy::too_many_arguments)] +pub(crate) fn _on_the_fly_artifact_probe_v1( + py: Python<'_>, + artifact_path: PathBuf, + process_id: String, + builder_input: &Bound<'_, PyAny>, + prepared_template_input: &Bound<'_, PyAny>, + direct_template_catalog_json: &Bound<'_, PyBytes>, + prepared_kernel_pack_digest: String, + selected_public_flow_id: u32, + public_helicities: Vec, + point_major_external_momenta: Vec, + point_count: u32, + parameter_overrides: Option>>, + tamper_executor_key: bool, + benchmark: bool, + benchmark_warmup_repetitions: u32, + benchmark_repetitions: u32, + collect_current_diagnostics: bool, + enable_color_projection: bool, + query_family: Option)>>, +) -> PyResult> { + let input = parse_input(builder_input)?; + let prepared_template = parse_prepared_template_input(prepared_template_input)?; + let direct_template_catalog_json = direct_template_catalog_json.as_bytes().to_vec(); + validate_sha256_text(&prepared_kernel_pack_digest, "prepared kernel pack digest")?; + let parameter_overrides = parameter_overrides + .unwrap_or_default() + .into_iter() + .map(|(name, value)| { + let value: [f64; 2] = value.try_into().map_err(|value: Vec| { + PyValueError::new_err(format!( + "on-the-fly parameter override {name:?} has {} components, expected 2", + value.len() + )) + })?; + Ok((name, value)) + }) + .collect::>>()?; + let native = py + .detach(move || { + let expected_pack_digest = semantic_digest_from_hex( + &prepared_kernel_pack_digest, + "prepared kernel pack digest", + )?; + let authenticated = + authenticate_builder_inputs(input, prepared_template, expected_pack_digest)? + .authenticated; + let direct_catalog = parse_direct_template_catalog( + &direct_template_catalog_json, + expected_pack_digest, + authenticated.template().summary().catalog_digest, + authenticated.template().summary().compiled_model_digest, + )?; + NativeRuntime::on_the_fly_artifact_probe_v1( + artifact_path, + &process_id, + &authenticated, + &direct_catalog.catalog, + selected_public_flow_id, + &public_helicities, + &point_major_external_momenta, + point_count, + ¶meter_overrides, + tamper_executor_key, + benchmark, + benchmark_warmup_repetitions, + benchmark_repetitions, + collect_current_diagnostics, + enable_color_projection, + query_family.as_deref(), + ) + }) + .map_err(python_error)?; + on_the_fly_artifact_probe_mapping(py, native) +} + +#[cfg(feature = "on-the-fly-test-support")] +#[pyfunction(signature = ( + on_the_fly_artifact_path, + on_the_fly_process_id, + recurrence_artifact_path, + recurrence_process_id, + builder_input, + prepared_template_input, + direct_template_catalog_json, + prepared_kernel_pack_digest, + selected_public_flow_id, + public_helicities, + point_major_external_momenta, + *, + parameter_overrides=None +))] +#[allow(clippy::too_many_arguments)] +pub(crate) fn _on_the_fly_execution_diagnostic_v1( + py: Python<'_>, + on_the_fly_artifact_path: PathBuf, + on_the_fly_process_id: String, + recurrence_artifact_path: PathBuf, + recurrence_process_id: String, + builder_input: &Bound<'_, PyAny>, + prepared_template_input: &Bound<'_, PyAny>, + direct_template_catalog_json: &Bound<'_, PyBytes>, + prepared_kernel_pack_digest: String, + selected_public_flow_id: u32, + public_helicities: Vec, + point_major_external_momenta: Vec, + parameter_overrides: Option>>, +) -> PyResult> { + let input = parse_input(builder_input)?; + let prepared_template = parse_prepared_template_input(prepared_template_input)?; + let direct_template_catalog_json = direct_template_catalog_json.as_bytes().to_vec(); + validate_sha256_text(&prepared_kernel_pack_digest, "prepared kernel pack digest")?; + let parameter_overrides = parameter_overrides + .unwrap_or_default() + .into_iter() + .map(|(name, value)| { + let value: [f64; 2] = value.try_into().map_err(|value: Vec| { + PyValueError::new_err(format!( + "execution diagnostic parameter override {name:?} has {} components, expected 2", + value.len() + )) + })?; + Ok((name, value)) + }) + .collect::>>()?; + let native = py + .detach(move || { + let expected_pack_digest = semantic_digest_from_hex( + &prepared_kernel_pack_digest, + "prepared kernel pack digest", + )?; + let authenticated = + authenticate_builder_inputs(input, prepared_template, expected_pack_digest)? + .authenticated; + let direct_catalog = parse_direct_template_catalog( + &direct_template_catalog_json, + expected_pack_digest, + authenticated.template().summary().catalog_digest, + authenticated.template().summary().compiled_model_digest, + )?; + NativeRuntime::on_the_fly_execution_diagnostic_v1( + on_the_fly_artifact_path, + &on_the_fly_process_id, + recurrence_artifact_path, + &recurrence_process_id, + &authenticated, + &direct_catalog.catalog, + selected_public_flow_id, + &public_helicities, + &point_major_external_momenta, + ¶meter_overrides, + ) + }) + .map_err(python_error)?; + on_the_fly_execution_diagnostic_mapping(py, native) +} + +fn build_on_the_fly_process_seeds_v1( + ordered_source_projection_jsons: Vec>, + recurrence_template_catalog_json: Vec, + direct_template_catalog_json: Vec, + prepared_kernel_pack_digest: String, +) -> RusticolResult>> { + let expected_pack_digest = + semantic_digest_from_hex(&prepared_kernel_pack_digest, "prepared kernel pack digest")?; + let template_value: JsonValue = serde_json::from_slice(&recurrence_template_catalog_json) + .map_err(|error| { + invalid(format!( + "recurrence-template catalog is not valid JSON: {error}" + )) + })?; + let templates = + rusticol_core::__private::project_recurrence_template_catalog_json_v1(&template_value)? + .validate()?; + let summary = templates.summary(); + if summary.prepared_kernel_pack_digest != expected_pack_digest { + return Err(RusticolError::integrity( + "recurrence-template catalog prepared-kernel pack differs from the requested pack", + )); + } + let direct = parse_direct_template_catalog( + &direct_template_catalog_json, + expected_pack_digest, + summary.catalog_digest, + summary.compiled_model_digest, + )?; + rusticol_core::__private::build_on_the_fly_process_seed_bytes_batch_v1( + &ordered_source_projection_jsons, + &templates, + &direct.catalog, + expected_pack_digest, + ) +} + +fn owned_on_the_fly_source_projection_jsons(values: &Bound<'_, PyAny>) -> PyResult>> { + let iterator = values.try_iter().map_err(|_| { + PyTypeError::new_err("ordered on-the-fly source projections must be iterable") + })?; + let mut result = Vec::new(); + for (index, item) in iterator.enumerate() { + let item = item?; + let value = item.cast::().map_err(|_| { + PyTypeError::new_err(format!( + "on-the-fly source projection at index {index} must be bytes" + )) + })?; + result.push(value.as_bytes().to_vec()); + } + Ok(result) +} + +/// Private ordered cold-path bridge used by the on-the-fly artifact writer. +/// +/// The four byte/string inputs are the complete boundary: in particular this +/// function accepts no recurrence builder input, physical color plan, DAG, or +/// direct-plan payload. +#[pyfunction] +pub(crate) fn _build_on_the_fly_process_seeds_v1( + py: Python<'_>, + ordered_source_projection_jsons: &Bound<'_, PyAny>, + recurrence_template_catalog_json: &Bound<'_, PyBytes>, + direct_template_catalog_json: &Bound<'_, PyBytes>, + prepared_kernel_pack_digest: &str, +) -> PyResult> { + let ordered_source_projection_jsons = + owned_on_the_fly_source_projection_jsons(ordered_source_projection_jsons)?; + let recurrence_template_catalog_json = recurrence_template_catalog_json.as_bytes().to_vec(); + let direct_template_catalog_json = direct_template_catalog_json.as_bytes().to_vec(); + let prepared_kernel_pack_digest = prepared_kernel_pack_digest.to_owned(); + let encoded = py + .detach(move || { + build_on_the_fly_process_seeds_v1( + ordered_source_projection_jsons, + recurrence_template_catalog_json, + direct_template_catalog_json, + prepared_kernel_pack_digest, + ) + }) + .map_err(python_error)?; + let result = PyList::empty(py); + for payload in encoded { + result.append(PyBytes::new(py, &payload))?; + } + Ok(result.unbind()) +} + +/// Private singleton wrapper retained for callers of the original bridge. +#[pyfunction] +pub(crate) fn _build_on_the_fly_process_seed_v1( + py: Python<'_>, + source_projection_json: &Bound<'_, PyBytes>, + recurrence_template_catalog_json: &Bound<'_, PyBytes>, + direct_template_catalog_json: &Bound<'_, PyBytes>, + prepared_kernel_pack_digest: &str, +) -> PyResult> { + let source_projection_json = source_projection_json.as_bytes().to_vec(); + let recurrence_template_catalog_json = recurrence_template_catalog_json.as_bytes().to_vec(); + let direct_template_catalog_json = direct_template_catalog_json.as_bytes().to_vec(); + let prepared_kernel_pack_digest = prepared_kernel_pack_digest.to_owned(); + let mut encoded = py + .detach(move || { + build_on_the_fly_process_seeds_v1( + vec![source_projection_json], + recurrence_template_catalog_json, + direct_template_catalog_json, + prepared_kernel_pack_digest, + ) + }) + .map_err(python_error)?; + let payload = encoded.pop().ok_or_else(|| { + PyValueError::new_err("singleton on-the-fly process-seed batch returned no payload") + })?; + Ok(PyBytes::new(py, &payload).unbind()) +} + +/// Private authoritative decoder used to bind a compact seed to the JSON +/// execution manifest and publication audit. +#[pyfunction] +pub(crate) fn _inspect_on_the_fly_process_seed_v1( + py: Python<'_>, + payload: &Bound<'_, PyBytes>, +) -> PyResult { + let payload = payload.as_bytes().to_vec(); + py.detach(move || { + rusticol_core::__private::inspect_on_the_fly_process_seed_identity_json_v1(&payload) + }) + .map_err(python_error) +} + +#[cfg(feature = "on-the-fly-test-support")] +fn on_the_fly_artifact_probe_mapping( + py: Python<'_>, + native: NativeOnTheFlyArtifactProbeV1, +) -> PyResult> { + let result = PyDict::new(py); + result.set_item("artifact_id", native.artifact_id)?; + result.set_item("process_id", native.process_id)?; + result.set_item("seed_digest", native.seed_digest)?; + result.set_item("query_digest", native.query_digest)?; + result.set_item("trace_digest", native.trace_digest)?; + result.set_item("point_count", native.point_count)?; + result.set_item("raw_amplitudes", native.raw_amplitudes)?; + result.set_item("normalized_values", native.normalized_values)?; + result.set_item("normalization_factor", native.normalization_factor)?; + if let Some(family) = native.query_family { + let family_result = PyDict::new(py); + let queries = PyList::empty(py); + for query in family.queries { + let row = PyDict::new(py); + row.set_item("selected_public_flow_id", query.selected_public_flow_id)?; + row.set_item("public_helicities", query.public_helicities)?; + row.set_item("query_digest", query.query_digest)?; + row.set_item("raw_amplitudes", query.raw_amplitudes)?; + row.set_item("normalized_values", query.normalized_values)?; + queries.append(row)?; + } + family_result.set_item("queries", queries)?; + family_result.set_item( + "census", + on_the_fly_query_family_census_mapping(py, family.census)?, + )?; + family_result.set_item("execution_cache_hit", family.execution_cache_hit)?; + family_result.set_item("execution_source_calls", family.execution_source_calls)?; + family_result.set_item("execution_source_rows", family.execution_source_rows)?; + family_result.set_item( + "execution_contribution_calls", + family.execution_contribution_calls, + )?; + family_result.set_item( + "execution_contribution_rows", + family.execution_contribution_rows, + )?; + family_result.set_item( + "execution_finalization_calls", + family.execution_finalization_calls, + )?; + family_result.set_item( + "execution_finalization_rows", + family.execution_finalization_rows, + )?; + family_result.set_item("execution_closure_calls", family.execution_closure_calls)?; + family_result.set_item("execution_closure_rows", family.execution_closure_rows)?; + family_result.set_item("cold_prepare_seconds", family.cold_prepare_seconds)?; + family_result.set_item( + "benchmark_warmup_repetitions", + family.benchmark_warmup_repetitions, + )?; + family_result.set_item("benchmark_repetitions", family.benchmark_repetitions)?; + family_result.set_item( + "private_warmed_elapsed_seconds", + family.benchmark_elapsed_seconds, + )?; + family_result.set_item( + "private_warmed_seconds_per_point", + family.benchmark_seconds_per_point, + )?; + family_result.set_item("private_timing_excludes_source_crossing", true)?; + result.set_item("query_family", family_result)?; + } else { + result.set_item("query_family", py.None())?; + } + result.set_item("projection_enabled", native.projection_enabled)?; + result.set_item("projection_applied", native.projection_applied)?; + for (prefix, counts) in [ + ("pre", native.projection_counts[0]), + ("post", native.projection_counts[1]), + ] { + for (name, value) in ["current", "contribution", "closure"] + .into_iter() + .zip(counts) + { + result.set_item(format!("{prefix}_projection_{name}_count"), value)?; + } + } + result.set_item("work_census_basis", native.work_census_basis)?; + result.set_item("logical_current_count", native.logical_current_count)?; + result.set_item("resident_current_count", native.resident_current_count)?; + result.set_item( + "resident_current_component_count", + native.resident_current_component_count, + )?; + result.set_item("source_operation_count", native.source_operation_count)?; + result.set_item( + "contribution_operation_count", + native.contribution_operation_count, + )?; + result.set_item( + "finalization_operation_count", + native.finalization_operation_count, + )?; + result.set_item("closure_operation_count", native.closure_operation_count)?; + result.set_item( + "total_kernel_application_count", + native.total_kernel_application_count, + )?; + result.set_item( + "semantic_executor_binding_count", + native.semantic_executor_binding_count, + )?; + result.set_item( + "distinct_prepared_executor_count", + native.distinct_prepared_executor_count, + )?; + result.set_item("trace_build_count", native.trace_build_count)?; + result.set_item("trace_cache_hit_count", native.trace_cache_hit_count)?; + result.set_item("momentum_fill_count", native.momentum_fill_count)?; + result.set_item( + "benchmark_warmup_repetitions", + native.benchmark_warmup_repetitions, + )?; + result.set_item("benchmark_repetitions", native.benchmark_repetitions)?; + result.set_item( + "benchmark_elapsed_seconds", + native.benchmark_elapsed_seconds, + )?; + result.set_item( + "benchmark_seconds_per_point", + native.benchmark_seconds_per_point, + )?; + result.set_item( + "direct_plan_load_attempts", + native.direct_plan_load_attempts, + )?; + result.set_item( + "direct_plan_decode_attempts", + native.direct_plan_decode_attempts, + )?; + result.set_item( + "direct_plan_materialization_attempts", + native.direct_plan_materialization_attempts, + )?; + result.set_item( + "established_builder_attempts", + native.established_builder_attempts, + )?; + let currents = PyList::empty(py); + for current in native.currents { + let row = PyDict::new(py); + row.set_item("semantic_digest", current.semantic_digest)?; + row.set_item("component_count", current.component_count)?; + row.set_item("values", current.values)?; + currents.append(row)?; + } + result.set_item("currents", currents)?; + Ok(result.into_any().unbind()) +} + +#[cfg(feature = "on-the-fly-test-support")] +fn on_the_fly_execution_diagnostic_mapping( + py: Python<'_>, + native: NativeOnTheFlyExecutionDiagnosticV1, +) -> PyResult> { + let result = PyDict::new(py); + result.set_item("schema_version", 1)?; + result.set_item("on_the_fly_artifact_id", native.on_the_fly_artifact_id)?; + result.set_item("on_the_fly_process_id", native.on_the_fly_process_id)?; + result.set_item("recurrence_artifact_id", native.recurrence_artifact_id)?; + result.set_item("recurrence_process_id", native.recurrence_process_id)?; + result.set_item("seed_digest", native.seed_digest)?; + result.set_item("query_digest", native.query_digest)?; + result.set_item("trace_digest", native.trace_digest)?; + result.set_item( + "direct_plan_semantic_digest", + native.direct_plan_semantic_digest, + )?; + result.set_item( + "direct_runtime_layout_digest", + native.direct_runtime_layout_digest, + )?; + result.set_item("public_flow_id", native.public_flow_id)?; + result.set_item("representative_flow_id", native.representative_flow_id)?; + result.set_item("public_helicities", native.public_helicities)?; + result.set_item( + "public_direct_helicity_id", + native.public_direct_helicity_id, + )?; + result.set_item( + "representative_direct_helicity_id", + native.representative_direct_helicity_id, + )?; + result.set_item("replay_phase", native.replay_phase.to_vec())?; + result.set_item("replay_multiplicity", native.replay_multiplicity)?; + result.set_item("replay_scale", native.replay_scale.to_vec())?; + result.set_item( + "on_the_fly_public_amplitude", + native.on_the_fly_public_amplitude.to_vec(), + )?; + result.set_item( + "recurrence_representative_amplitude", + native.recurrence_representative_amplitude.to_vec(), + )?; + result.set_item( + "recurrence_public_amplitude", + native.recurrence_public_amplitude.to_vec(), + )?; + result.set_item( + "amplitude_absolute_delta", + native.amplitude_absolute_delta.to_vec(), + )?; + result.set_item("raw_bit_difference_count", native.raw_bit_difference_count)?; + result.set_item("compared_current_count", native.compared_current_count)?; + result.set_item( + "excluded_direct_current_count", + native.excluded_direct_current_count, + )?; + + let first_raw_bit_difference = native.first_raw_bit_difference; + let components = PyList::empty(py); + for component in native.current_components { + let row = PyDict::new(py); + row.set_item("dependency_depth", component.dependency_depth)?; + row.set_item("semantic_digest", component.semantic_digest)?; + row.set_item("component", component.component)?; + row.set_item("on_the_fly", component.on_the_fly.to_vec())?; + row.set_item("recurrence", component.recurrence.to_vec())?; + row.set_item("on_the_fly_bits", component.on_the_fly_bits.to_vec())?; + row.set_item("recurrence_bits", component.recurrence_bits.to_vec())?; + row.set_item("absolute_delta", component.absolute_delta.to_vec())?; + components.append(row)?; + } + result.set_item("current_components", &components)?; + if let Some(index) = first_raw_bit_difference { + result.set_item("first_raw_bit_difference_index", index)?; + result.set_item("first_raw_bit_difference", components.get_item(index)?)?; + } else { + result.set_item("first_raw_bit_difference_index", py.None())?; + result.set_item("first_raw_bit_difference", py.None())?; + } + Ok(result.into_any().unbind()) +} + +#[cfg(feature = "on-the-fly-test-support")] +fn on_the_fly_query_family_census_mapping( + py: Python<'_>, + native: OnTheFlyQueryFamilyCensusV1, +) -> PyResult> { + let result = PyDict::new(py); + result.set_item("query_count", native.query_count)?; + result.set_item( + "source_frame_partition_count", + native.source_frame_partition_count, + )?; + result.set_item( + "projection_applied_query_count", + native.projection_applied_query_count, + )?; + result.set_item( + "projection_pre_current_count", + native.projection_pre_current_count, + )?; + result.set_item( + "projection_pre_contribution_count", + native.projection_pre_contribution_count, + )?; + result.set_item( + "projection_pre_closure_count", + native.projection_pre_closure_count, + )?; + result.set_item( + "projection_post_current_count", + native.projection_post_current_count, + )?; + result.set_item( + "projection_post_contribution_count", + native.projection_post_contribution_count, + )?; + result.set_item( + "projection_post_closure_count", + native.projection_post_closure_count, + )?; + result.set_item( + "dynamic_current_occurrence_count", + native.dynamic_current_occurrence_count, + )?; + result.set_item( + "dynamic_current_component_occurrence_count", + native.dynamic_current_component_occurrence_count, + )?; + result.set_item("dynamic_source_rows", native.dynamic_source_rows)?; + result.set_item( + "dynamic_contribution_rows", + native.dynamic_contribution_rows, + )?; + result.set_item( + "dynamic_finalization_rows", + native.dynamic_finalization_rows, + )?; + result.set_item("dynamic_closure_rows", native.dynamic_closure_rows)?; + result.set_item("dynamic_source_calls", native.dynamic_source_calls)?; + result.set_item( + "dynamic_contribution_calls", + native.dynamic_contribution_calls, + )?; + result.set_item( + "dynamic_finalization_calls", + native.dynamic_finalization_calls, + )?; + result.set_item("dynamic_closure_calls", native.dynamic_closure_calls)?; + result.set_item( + "union_unique_current_count", + native.union_unique_current_count, + )?; + result.set_item( + "union_unique_current_component_count", + native.union_unique_current_component_count, + )?; + result.set_item("union_source_rows", native.union_source_rows)?; + result.set_item("union_contribution_rows", native.union_contribution_rows)?; + result.set_item("union_finalization_rows", native.union_finalization_rows)?; + result.set_item("union_closure_rows", native.union_closure_rows)?; + result.set_item( + "union_amplitude_destination_count", + native.union_amplitude_destination_count, + )?; + result.set_item( + "union_source_executor_call_groups", + native.union_source_executor_call_groups, + )?; + result.set_item( + "union_contribution_executor_call_groups", + native.union_contribution_executor_call_groups, + )?; + result.set_item( + "union_finalization_executor_call_groups", + native.union_finalization_executor_call_groups, + )?; + result.set_item( + "union_closure_executor_call_groups", + native.union_closure_executor_call_groups, + )?; + Ok(result.into_any().unbind()) +} + +#[cfg(feature = "on-the-fly-test-support")] +fn on_the_fly_exact_factor_mapping( + py: Python<'_>, + factor: ExactComplexRational, +) -> PyResult> { + let result = PyDict::new(py); + for (name, value) in [("real", factor.real()), ("imag", factor.imag())] { + let part = PyDict::new(py); + part.set_item("numerator", value.numerator().to_string())?; + part.set_item("denominator", value.denominator().to_string())?; + result.set_item(name, part)?; + } + Ok(result.into_any().unbind()) +} + +#[cfg(feature = "on-the-fly-test-support")] +fn on_the_fly_optional_exact_factor_mapping( + py: Python<'_>, + factor: Option, +) -> PyResult> { + match factor { + Some(factor) => on_the_fly_exact_factor_mapping(py, factor), + None => Ok(py.None()), + } +} + +#[cfg(feature = "on-the-fly-test-support")] +fn on_the_fly_transition_candidate_mapping( + py: Python<'_>, + row: ConstructionTransitionDiagnosticRowV1, +) -> PyResult> { + let result = PyDict::new(py); + result.set_item("materialized_sector_id", row.materialized_sector_id)?; + result.set_item( + "output_current_digest", + row.output_current_digest.to_string(), + )?; + result.set_item( + "ordered_parent_digests", + row.ordered_parent_digests + .into_iter() + .map(|digest| digest.to_string()) + .collect::>(), + )?; + result.set_item("transition_template_id", row.transition_template_id)?; + result.set_item( + "transition_semantic_digest", + row.transition_semantic_digest.to_string(), + )?; + result.set_item( + "evaluator_binding_semantic_digest", + row.evaluator_binding_semantic_digest.to_string(), + )?; + result.set_item("result_state_template_id", row.result_state_template_id)?; + result.set_item("quantum_flow_witness_id", row.quantum_flow_witness_id)?; + result.set_item( + "quantum_semantic_digest", + row.quantum_semantic_digest.to_string(), + )?; + result.set_item( + "color_contraction_template_id", + row.color_contraction_template_id, + )?; + result.set_item("color_witness_ordinal", row.color_witness_ordinal)?; + result.set_item( + "color_witness_proof_digest", + row.color_witness_proof_digest.to_string(), + )?; + result.set_item("output_projection_id", row.output_projection_id)?; + let factors = PyDict::new(py); + for (name, factor) in [ + ("transition", row.transition_factor), + ("contraction", row.contraction_factor), + ("output", row.output_factor), + ("exchange", row.exchange_factor), + ("witness", row.witness_factor), + ("reversal", row.reversal_factor), + ("candidate_product", row.candidate_factor), + ("aggregate_after", row.aggregate_factor_after), + ] { + factors.set_item(name, on_the_fly_exact_factor_mapping(py, factor)?)?; + } + result.set_item("factor_components", factors)?; + result.set_item("reversal_mask", row.reversal_mask)?; + let reflections = PyDict::new(py); + reflections.set_item( + "parent_proof_digests", + row.parent_reflection_proof_digests + .into_iter() + .map(|digest| digest.map(|value| value.to_string())) + .collect::>(), + )?; + let parent_phases = PyList::empty(py); + for phase in row.parent_reflection_phases { + parent_phases.append(on_the_fly_optional_exact_factor_mapping(py, phase)?)?; + } + reflections.set_item("parent_phases", parent_phases)?; + reflections.set_item( + "local_proof_digest", + row.local_reflection_proof_digest + .map(|digest| digest.to_string()), + )?; + reflections.set_item( + "local_phase", + on_the_fly_optional_exact_factor_mapping(py, row.local_reflection_phase)?, + )?; + reflections.set_item( + "result_proof_digest", + row.result_reflection_proof_digest + .map(|digest| digest.to_string()), + )?; + reflections.set_item( + "result_phase", + on_the_fly_optional_exact_factor_mapping(py, row.result_reflection_phase)?, + )?; + result.set_item("reflection", reflections)?; + result.set_item("output_color_orientation", row.output_color_orientation)?; + result.set_item("post_row_complex_value", py.None())?; + Ok(result.into_any().unbind()) +} + +#[cfg(feature = "on-the-fly-test-support")] +fn on_the_fly_transition_candidates_mapping( + py: Python<'_>, + mut rows: Vec, +) -> PyResult> { + rows.sort_by_cached_key(|row| format!("{row:?}")); + let result = PyList::empty(py); + for row in rows { + result.append(on_the_fly_transition_candidate_mapping(py, row)?)?; + } + Ok(result.unbind()) +} + +#[cfg(feature = "on-the-fly-test-support")] +fn on_the_fly_test_support_mapping( + py: Python<'_>, + native: OnTheFlyTestSupportReportV1, +) -> PyResult> { + let result = PyDict::new(py); + let structural_parity = native.structural_parity(); + result.set_item("seed_digest", native.seed_digest.to_string())?; + result.set_item("query_digest", native.query_digest.to_string())?; + result.set_item("selector_digest", native.selector_digest.to_string())?; + result.set_item("trace_digest", native.trace_digest.to_string())?; + result.set_item("current_count", native.current_count)?; + result.set_item("contribution_count", native.contribution_count)?; + result.set_item("closure_count", native.closure_count)?; + result.set_item( + "established_current_count", + native.established_current_count, + )?; + result.set_item( + "established_contribution_count", + native.established_contribution_count, + )?; + result.set_item( + "established_closure_count", + native.established_closure_count, + )?; + result.set_item( + "current_multiset_digest", + native.current_multiset_digest.to_string(), + )?; + result.set_item( + "established_current_multiset_digest", + native.established_current_multiset_digest.to_string(), + )?; + result.set_item( + "contribution_multiset_digest", + native.contribution_multiset_digest.to_string(), + )?; + result.set_item( + "established_contribution_multiset_digest", + native.established_contribution_multiset_digest.to_string(), + )?; + result.set_item( + "closure_multiset_digest", + native.closure_multiset_digest.to_string(), + )?; + result.set_item( + "closure_parity_multiset_digest", + native.closure_parity_multiset_digest.to_string(), + )?; + result.set_item( + "established_closure_parity_multiset_digest", + native + .established_closure_parity_multiset_digest + .to_string(), + )?; + result.set_item( + "negative_contribution_factor_count", + native.negative_contribution_factor_count, + )?; + result.set_item( + "established_negative_contribution_factor_count", + native.established_negative_contribution_factor_count, + )?; + result.set_item("source_domain_equal", native.source_domain_equal)?; + result.set_item("pairing_oracle_equal", native.pairing_oracle_equal)?; + result.set_item("pairing_fermion_parities", native.pairing_fermion_parities)?; + result.set_item( + "established_pairing_fermion_parities", + native.established_pairing_fermion_parities, + )?; + result.set_item( + "workspace_capacity_independent", + native.workspace_capacity_independent, + )?; + result.set_item("structural_parity", structural_parity)?; + result.set_item( + "compact_transition_candidates", + on_the_fly_transition_candidates_mapping(py, native.compact_transition_candidates)?, + )?; + result.set_item( + "established_transition_candidates", + on_the_fly_transition_candidates_mapping(py, native.established_transition_candidates)?, + )?; + Ok(result.into_any().unbind()) +} + +#[allow(clippy::too_many_arguments)] +fn lower_recurrence_direct( + input: OwnedInput, + prepared_template: PreparedTemplateInput, + direct_template_catalog_json: &[u8], + prepared_kernel_pack_digest: &str, + schedule_semantic_digest: &str, + destination: &std::path::Path, + point_tile_size: u32, + workspace_mib: u32, + mut relation_discovery: RecurrenceRelationDiscoveryOptions, + relation_discovery_evidence_json: Option>, + helicity_dispatch_destination: Option<&std::path::Path>, + python_extraction_seconds: f64, + progress: &mut dyn FnMut(RecurrenceBuildProgress) -> RusticolResult<()>, +) -> RusticolResult { + let native_started = Instant::now(); + if std::fs::symlink_metadata(destination).is_ok() { + return Err(invalid(format!( + "recurrence direct-plan destination already exists: {}", + destination.display() + ))); + } + if let Some(dispatch_destination) = helicity_dispatch_destination { + if std::fs::symlink_metadata(dispatch_destination).is_ok() { + return Err(invalid(format!( + "recurrence helicity-dispatch destination already exists: {}", + dispatch_destination.display() + ))); + } + if relation_discovery.mode != RecurrenceRelationDiscoveryMode::Off + || relation_discovery_evidence_json.is_some() + { + return Err(invalid( + "recurrence helicity-dispatch lowering requires relation discovery off", + )); + } + } + + let expected_pack_digest = + semantic_digest_from_hex(prepared_kernel_pack_digest, "prepared kernel pack digest")?; + let authenticated_inputs = + authenticate_builder_inputs(input, prepared_template, expected_pack_digest)?; + let builder_input_digest = authenticated_inputs.builder_input_digest; + let template_input_digest = authenticated_inputs.template_input_digest; + let authenticated = authenticated_inputs.authenticated; + + let catalog_started = Instant::now(); + let direct_catalog = parse_direct_template_catalog( + direct_template_catalog_json, + expected_pack_digest, + authenticated.template().summary().catalog_digest, + authenticated.template().summary().compiled_model_digest, + )?; + let catalog_authentication_seconds = catalog_started.elapsed().as_secs_f64(); + + let process_id = authenticated.process().summary().process_id().to_owned(); + let strategy = authenticated.process().summary().strategy(); + let semantic_digest = + semantic_digest_from_hex(schedule_semantic_digest, "recurrence schedule digest")?; + if let Some(raw_evidence) = relation_discovery_evidence_json { + let parameter_contract = authenticated_runtime_parameter_contract( + authenticated.process(), + authenticated.template(), + )?; + relation_discovery = relation_discovery.with_numerical_evidence( + parse_numerical_relation_evidence(&raw_evidence, ¶meter_contract)?, + )?; + } + + let semantic_started = Instant::now(); + let mut construction = RecurrenceConstructionMetrics::default(); + let mut tracked_progress = |snapshot: RecurrenceBuildProgress| { + construction.include(&snapshot); + progress(snapshot) + }; + let (program, generation_profile) = + authenticated.build_with_progress_and_telemetry(&mut tracked_progress)?; + let closure_target_domain_certificate = + program + .closure_target_domain_certificate() + .map(|certificate| NativeClosureTargetDomainCertificate { + physical_sector_count: certificate.physical_sector_count(), + resolved_helicity_count: certificate.resolved_helicity_count(), + certified_structural_zero_physical_sector_ids: certificate + .certified_structural_zero_physical_sector_ids() + .to_vec(), + semantic_digest: certificate.semantic_digest().to_string(), + }); + let projection_certificate = program + .color_projection_certificate_body() + .map(|body| body.to_vec()); + let semantic_construction_seconds = semantic_started.elapsed().as_secs_f64(); + + let direct_lowering_started = Instant::now(); + let runtime_options = DirectRecurrenceRuntimeOptions::new(point_tile_size, workspace_mib)?; + if let Some(evidence) = relation_discovery.numerical_evidence.as_ref() { + let baseline_plan = lower_recurrence_direct_plan_v2( + &program, + authenticated.template(), + &direct_catalog.catalog, + semantic_digest, + expected_pack_digest, + direct_catalog.catalog_digest, + runtime_options, + )?; + authenticate_recurrence_numerical_relation_provenance( + evidence, + &baseline_plan, + &process_id, + )?; + } + let (plan, relation_discovery_report, helicity_dispatch) = + if helicity_dispatch_destination.is_some() { + let (plan, dispatch) = lower_recurrence_direct_plan_v2_with_helicity_dispatch( + &program, + authenticated.template(), + &direct_catalog.catalog, + semantic_digest, + expected_pack_digest, + direct_catalog.catalog_digest, + runtime_options, + )?; + (plan, None, Some(dispatch)) + } else { + let (plan, report) = lower_recurrence_direct_plan_v2_with_relation_discovery( + &program, + authenticated.template(), + &direct_catalog.catalog, + semantic_digest, + expected_pack_digest, + direct_catalog.catalog_digest, + runtime_options, + &relation_discovery, + )?; + (plan, report, None) + }; + let direct_lowering_seconds = direct_lowering_started.elapsed().as_secs_f64(); + + let resolved_helicities = resolved_helicities_from_direct_plan(&plan)?; + let amplitude_destinations = plan + .amplitude_destinations() + .iter() + .map(|destination| { + ( + destination.target_sector_id, + (destination.target_helicity_id_or_sentinel != DIRECT_NONE_U32) + .then_some(destination.target_helicity_id_or_sentinel), + ) + }) + .collect(); + let semantic_component_count = plan.currents().iter().try_fold(0_u64, |total, current| { + total + .checked_add(u64::from(current.component_count)) + .ok_or_else(|| invalid("recurrence semantic component count exceeds u64")) + })?; + let selector_work = + if strategy.uses_topology_replay_targets() && !plan.replay_targets().is_empty() { + let mut public_flow_counts = BTreeMap::::new(); + for target in plan.replay_targets() { + let count = public_flow_counts + .entry(target.representative_id) + .or_default(); + *count = count + .checked_add(1) + .ok_or_else(|| invalid("recurrence public-flow count exceeds u64"))?; + } + public_flow_counts + .into_iter() + .map(|(representative_sector_id, public_flow_count)| { + Ok(( + plan.selector_work_summary(representative_sector_id)?, + public_flow_count, + )) + }) + .collect::>>()? + } else { + vec![] + }; + let exact_sections = exact_sections_from_direct_plan( + &plan, + process_id.clone(), + direct_catalog.exact_executors.clone(), + ); + + let serialization_started = Instant::now(); + let metadata = write_recurrence_direct_plan_pacbin_with_projection_certificate( + destination, + &plan, + projection_certificate.as_deref(), + )?; + let helicity_dispatch = match (helicity_dispatch_destination, helicity_dispatch.as_ref()) { + (Some(dispatch_destination), Some(dispatch)) => { + let dispatch_metadata = match write_recurrence_helicity_dispatch_v1_atomic( + dispatch_destination, + dispatch, + ) { + Ok(metadata) => metadata, + Err(error) => { + let _ = std::fs::remove_file(destination); + return Err(error); + } + }; + Some(NativeHelicityDispatchMetadata { + size_bytes: dispatch_metadata.size_bytes, + sha256: hex_digest(dispatch_metadata.sha256), + base_runtime_layout_digest: dispatch.runtime_layout_digest().to_string(), + resolved_helicity_count: dispatch.resolved_helicity_count() as usize, + }) + } + (None, None) => None, + _ => { + let _ = std::fs::remove_file(destination); + return Err(invalid( + "recurrence helicity-dispatch destination and lowering result disagree", + )); + } + }; + let serialization_seconds = serialization_started.elapsed().as_secs_f64(); + let native_total_seconds = native_started.elapsed().as_secs_f64(); + + Ok(NativeDirectLoweringResult { + builder_input_digest, + template_input_digest, + prepared_kernel_pack_digest: direct_catalog.prepared_kernel_pack_digest.to_string(), + direct_template_catalog_digest: direct_catalog.catalog_digest.to_string(), + process_id, + strategy, + semantic_digest: plan.semantic_digest().to_string(), + runtime_layout_digest: plan.runtime_layout_digest().to_string(), + member_count: metadata.member_count, + unpacked_size_bytes: metadata.unpacked_size_bytes, + index_sha256: hex_digest(metadata.index_sha256), + container_size: metadata.container_size, + plan_payload_size: metadata.plan_payload_size, + plan_sha256: hex_digest(metadata.plan_sha256), + projection_certificate_payload_size: metadata.projection_certificate_payload_size, + projection_certificate_sha256: metadata.projection_certificate_sha256.map(hex_digest), + current_count: plan.currents().len(), + source_row_count: plan.sources().len(), + contribution_count: plan.contributions().len(), + finalization_count: plan.finalizations().len(), + closure_count: plan.closures().len(), + row_group_count: plan.row_groups().len(), + momentum_form_count: plan.momentum_forms().len(), + selector_domain_count: plan.selector_domains().len(), + replay_target_count: plan.replay_targets().len(), + amplitude_destination_count: plan.amplitude_destinations().len(), + resolved_helicity_count: plan.resolved_helicities().len(), + retained_helicity_count: plan.retained_helicity_count(), + exact_factor_count: plan.exact_factors().len(), + semantic_component_count, + current_arena_components: plan.current_arena_components(), + parameter_value_count: plan.parameter_value_count(), + physical_sector_count: plan.physical_sector_count(), + direct_executor_count: plan.direct_executor_count(), + prepared_kernel_count: direct_catalog.prepared_kernel_count, + resolved_helicities, + amplitude_destinations, + closure_target_domain_certificate, + selector_work, + relation_discovery: relation_discovery_report, + exact_sections, + construction, + generation_profile, + timings: DirectLoweringTimings { + python_extraction_seconds, + catalog_authentication_seconds, + semantic_construction_seconds, + direct_lowering_seconds, + serialization_seconds, + native_total_seconds, + }, + helicity_dispatch, + }) +} + +fn resolved_helicities_from_direct_plan( + plan: &DirectRecurrencePlan, +) -> RusticolResult>> { + let public = plan.public_helicities(); + plan.resolved_helicities() + .iter() + .enumerate() + .map(|(index, row)| { + if row.id != index as u32 { + return Err(invalid(format!( + "direct resolved helicity {index} has non-canonical ID {}", + row.id + ))); + } + let start = usize::try_from(row.public_helicity_start) + .map_err(|_| invalid("direct resolved-helicity offset exceeds usize"))?; + let count = usize::try_from(row.public_helicity_count) + .map_err(|_| invalid("direct resolved-helicity count exceeds usize"))?; + let end = start + .checked_add(count) + .ok_or_else(|| invalid("direct resolved-helicity range exceeds usize"))?; + public + .get(start..end) + .map(<[i32]>::to_vec) + .ok_or_else(|| invalid("direct resolved-helicity range is out of bounds")) + }) + .collect() +} + +pub(super) fn canonical_json_sha256(value: &JsonValue, context: &str) -> RusticolResult { + Ok(hex_digest(Sha256::digest(canonical_json_bytes( + value, context, + )?))) +} + +fn parse_direct_template_catalog( + bytes: &[u8], + expected_pack_digest: SemanticDigest, + expected_template_catalog_digest: SemanticDigest, + expected_compiled_model_digest: SemanticDigest, +) -> RusticolResult { + if bytes.is_empty() { + return Err(invalid("direct-template catalog JSON must not be empty")); + } + let value: JsonValue = serde_json::from_slice(bytes).map_err(|error| { + invalid(format!( + "direct-template catalog is not valid JSON: {error}" + )) + })?; + let canonical = canonical_json_bytes(&value, "direct-template catalog")?; + if canonical != bytes { + return Err(invalid( + "direct-template catalog JSON is not canonical ASCII JSON", + )); + } + let object = json_object(&value, "direct-template catalog")?; + require_json_fields( + object, + &[ + "abi", + "backend", + "backend_abi", + "canonicalization_abi", + "catalog_digest", + "compiled_model_digest", + "optimization_level", + "optimization_settings_digest", + "portable", + "prepared_kernel_contract_digest", + "prepared_kernel_pack_digest", + "prepared_kernel_payload_digest", + "recurrence_template_catalog_digest", + "target_triple", + "templates", + ], + "direct-template catalog", + )?; + require_json_string_value( + object, + "abi", + RECURRENCE_DIRECT_TEMPLATE_ABI, + "direct-template catalog", + )?; + require_json_string_value( + object, + "backend_abi", + DIRECT_BACKEND_ABI, + "direct-template catalog", + )?; + require_json_string_value( + object, + "canonicalization_abi", + DIRECT_CANONICALIZATION_ABI, + "direct-template catalog", + )?; + + let catalog_digest = json_sha256(object, "catalog_digest", "direct-template catalog digest")?; + let actual_catalog_digest = digest_json_without_field( + &value, + "catalog_digest", + "direct-template catalog semantic payload", + )?; + if catalog_digest != actual_catalog_digest { + return Err(RusticolError::integrity( + "direct-template catalog digest does not match its canonical payload", + )); + } + let prepared_kernel_pack_digest = json_sha256( + object, + "prepared_kernel_pack_digest", + "direct-template prepared-kernel pack digest", + )?; + if prepared_kernel_pack_digest != expected_pack_digest { + return Err(RusticolError::integrity( + "direct-template catalog prepared-kernel pack digest does not match the requested pack", + )); + } + let recurrence_template_catalog_digest = json_sha256( + object, + "recurrence_template_catalog_digest", + "direct-template semantic catalog digest", + )?; + if recurrence_template_catalog_digest != expected_template_catalog_digest { + return Err(RusticolError::integrity( + "direct-template catalog does not match the authenticated recurrence template", + )); + } + let compiled_model_digest = json_sha256( + object, + "compiled_model_digest", + "direct-template compiled-model digest", + )?; + if compiled_model_digest != expected_compiled_model_digest { + return Err(RusticolError::integrity( + "direct-template catalog does not match the authenticated compiled model", + )); + } + for (field, context) in [ + ( + "prepared_kernel_contract_digest", + "direct-template prepared-kernel contract digest", + ), + ( + "prepared_kernel_payload_digest", + "direct-template prepared-kernel payload digest", + ), + ( + "optimization_settings_digest", + "direct-template optimization-settings digest", + ), + ] { + json_sha256(object, field, context)?; + } + + let backend = json_string(object, "backend", "direct-template backend")?; + if !matches!(backend, "jit" | "cpp" | "asm") { + return Err(invalid(format!( + "direct-template catalog has unsupported backend {backend:?}" + ))); + } + let target_triple = + json_nonempty_string(object, "target_triple", "direct-template target triple")?; + let portable = json_bool(object, "portable", "direct-template portable flag")?; + let optimization_level = json_u32( + object, + "optimization_level", + "direct-template optimization level", + )?; + match backend { + "jit" if !portable || optimization_level != 2 => { + return Err(invalid( + "prepared direct JIT catalogs must use portable SymJIT O2", + )); + } + "cpp" | "asm" if portable => { + return Err(invalid( + "prepared direct C++/ASM catalogs must be target-native", + )); + } + _ => {} + } + + let templates = json_array(object, "templates", "direct-template templates")?; + if templates.is_empty() { + return Err(invalid( + "direct-template executor catalog must not be empty", + )); + } + let mut bindings = Vec::with_capacity(templates.len()); + let mut exact_executors = Vec::with_capacity(templates.len()); + let mut prepared_kernel_ids = BTreeSet::new(); + let mut identity_finalizer_seen = false; + for (expected_executor_id, template_value) in templates.iter().enumerate() { + let context = format!("direct template {expected_executor_id}"); + let template = json_object(template_value, &context)?; + require_json_fields( + template, + &[ + "abi", + "alignment_bytes", + "backend", + "coupling_slot_count", + "destination_aliasing", + "destination_component_count", + "destination_operation", + "direct_executor_id", + "evaluator_binding_id", + "evaluator_resolver_key", + "exact_expression_digest", + "momentum_operand_count", + "optimization_level", + "parameter_slot_count", + "parent_arity", + "parent_component_counts", + "payload_binding", + "portable", + "role", + "semantic_digest", + "semantic_template_ids", + "simd_axis", + "target_triple", + "template_id", + ], + &context, + )?; + require_json_string_value(template, "abi", RECURRENCE_DIRECT_TEMPLATE_ABI, &context)?; + let actual_template_digest = + digest_json_without_field(template_value, "semantic_digest", &context)?; + let template_digest = json_sha256( + template, + "semantic_digest", + &format!("{context} semantic digest"), + )?; + if template_digest != actual_template_digest { + return Err(RusticolError::integrity(format!( + "{context} semantic digest does not match its canonical payload" + ))); + } + + let direct_executor_id = json_u32( + template, + "direct_executor_id", + &format!("{context} executor ID"), + )?; + let expected_executor_id = u32::try_from(expected_executor_id) + .map_err(|_| invalid("direct-template executor count exceeds u32"))?; + if direct_executor_id != expected_executor_id { + return Err(invalid(format!( + "{context} has executor ID {direct_executor_id}, expected dense ID {expected_executor_id}" + ))); + } + let evaluator_binding_id = json_u32( + template, + "evaluator_binding_id", + &format!("{context} evaluator-binding ID"), + )?; + let role_text = json_string(template, "role", &format!("{context} role"))?; + let role = direct_executor_role(role_text, &context)?; + let expected_operation = match role { + DirectExecutorRole::Source => "initialize", + DirectExecutorRole::Contribution => "add", + DirectExecutorRole::Finalization => "finalize-in-place", + DirectExecutorRole::Closure => "closure-add", + }; + require_json_string_value( + template, + "destination_operation", + expected_operation, + &context, + )?; + require_json_string_value(template, "backend", backend, &context)?; + require_json_string_value(template, "target_triple", target_triple, &context)?; + if json_bool(template, "portable", &format!("{context} portable flag"))? != portable + || json_u32( + template, + "optimization_level", + &format!("{context} optimization level"), + )? != optimization_level + { + return Err(invalid(format!( + "{context} backend policy does not match its catalog" + ))); + } + require_json_string_value(template, "simd_axis", "points-contiguous", &context)?; + let destination_aliasing = json_bool( + template, + "destination_aliasing", + &format!("{context} destination aliasing"), + )?; + if destination_aliasing != (role == DirectExecutorRole::Finalization) { + return Err(invalid(format!( + "{context} has an invalid destination-aliasing contract" + ))); + } + json_nonempty_string(template, "template_id", &format!("{context} template ID"))?; + json_nonempty_string( + template, + "evaluator_resolver_key", + &format!("{context} evaluator resolver key"), + )?; + json_sha256( + template, + "exact_expression_digest", + &format!("{context} exact-expression digest"), + )?; + validate_direct_template_shapes(template, &context)?; + let parent_component_counts = json_array( + template, + "parent_component_counts", + &format!("{context} parent component counts"), + )? + .iter() + .enumerate() + .map(|(index, value)| json_value_u32(value, &format!("{context} parent component {index}"))) + .collect::>>()?; + let destination_component_count = json_u32( + template, + "destination_component_count", + &format!("{context} destination component count"), + )?; + let momentum_operand_count = json_u32( + template, + "momentum_operand_count", + &format!("{context} momentum operand count"), + )?; + + let payload = json_object( + json_field(template, "payload_binding", &context)?, + &format!("{context} payload binding"), + )?; + require_json_fields_with_optional( + payload, + &[ + "abi", + "destination_operation", + "direct_application_abi", + "exact_factor_scalar_slots", + "input_plane_count", + "input_plane_projections", + "intrinsic_contract_digest", + "kind", + "output_alias_inputs", + "contribution_parent_permutation", + "parameter_bindings", + "payload_digest", + "payload_paths", + "prepared_kernel_id", + "prepared_template_semantic_digest", + "role", + "runtime_template", + "scalar_input_count", + "scalar_projections", + "source_application_abi", + "source_application_path", + "source_application_sha256", + "state_plane_indices", + ], + &["native_entry_point"], + &format!("{context} payload binding"), + )?; + require_json_string_value( + payload, + "abi", + DIRECT_PAYLOAD_BINDING_ABI, + &format!("{context} payload binding"), + )?; + json_sha256( + payload, + "payload_digest", + &format!("{context} payload digest"), + )?; + let payload_kind = json_string(payload, "kind", &format!("{context} payload kind"))?; + if !matches!( + payload_kind, + "rusticol-intrinsic" | "prepared-direct-call" | "pending-direct-call-abi" + ) { + return Err(invalid(format!( + "{context} has unsupported payload kind {payload_kind:?}" + ))); + } + if payload_kind == "pending-direct-call-abi" { + return Err(RusticolError::compatibility(format!( + "{context} has no executable Direct-Arena payload; rebuild the prepared model" + ))); + } + let parent_permutation_values = json_array( + payload, + "contribution_parent_permutation", + &format!("{context} parent permutation"), + )?; + if parent_permutation_values.len() != 2 { + return Err(invalid(format!( + "{context} parent permutation must contain two entries" + ))); + } + let mut parent_permutation = [0_u8; 2]; + for (index, value) in parent_permutation_values.iter().enumerate() { + parent_permutation[index] = u8::try_from(json_value_u32( + value, + &format!("{context} parent permutation entry {index}"), + )?) + .map_err(|_| invalid(format!("{context} parent permutation exceeds u8")))?; + } + if !matches!(parent_permutation, [0, 1] | [1, 0]) + || (parent_permutation != [0, 1] + && !(payload_kind == "rusticol-intrinsic" + && role == DirectExecutorRole::Contribution)) + { + return Err(invalid(format!( + "{context} has an invalid parent permutation" + ))); + } + let runtime_template = json_optional_string( + payload, + "runtime_template", + &format!("{context} runtime template"), + )?; + let prepared_kernel_id = json_optional_u32( + payload, + "prepared_kernel_id", + &format!("{context} prepared-kernel ID"), + )?; + exact_executors.push(NativeRecurrenceExactExecutor { + direct_executor_id, + role: role_text.to_owned(), + destination_operation: expected_operation.to_owned(), + parent_component_counts, + destination_component_count, + momentum_operand_count, + prepared_kernel_id, + runtime_template: runtime_template.map(str::to_owned), + }); + let native_entry_point = match payload.get("native_entry_point") { + None | Some(JsonValue::Null) => None, + Some(JsonValue::String(value)) if !value.is_empty() => Some(value.as_str()), + Some(_) => { + return Err(invalid(format!( + "{context} native entry point must be a nonempty string or null" + ))); + } + }; + match (payload_kind, backend) { + ("prepared-direct-call", "cpp" | "asm") => { + let kernel_id = prepared_kernel_id.ok_or_else(|| { + invalid(format!( + "{context} native prepared call has no prepared-kernel ID" + )) + })?; + let expected = + format!("pyamplicol_recurrence_direct_{role_text}_k{kernel_id:08x}_v1"); + if native_entry_point != Some(expected.as_str()) { + return Err(invalid(format!( + "{context} native prepared call has a noncanonical entry point" + ))); + } + } + _ if native_entry_point.is_some() => { + return Err(invalid(format!( + "{context} non-native payload carries a native entry point" + ))); + } + _ => {} + } + let intrinsic_contract_digest = json_optional_string( + payload, + "intrinsic_contract_digest", + &format!("{context} intrinsic contract digest"), + )?; + if let Some(digest) = intrinsic_contract_digest { + semantic_digest_from_hex(digest, &format!("{context} intrinsic contract digest"))?; + } + if payload_kind == "rusticol-intrinsic" + && role == DirectExecutorRole::Contribution + && intrinsic_contract_digest.is_none() + { + return Err(invalid(format!( + "{context} contribution intrinsic has no certified contract digest" + ))); + } + if payload_kind != "rusticol-intrinsic" && intrinsic_contract_digest.is_some() { + return Err(invalid(format!( + "{context} non-intrinsic payload carries an intrinsic contract digest" + ))); + } + validate_string_array( + json_array( + payload, + "payload_paths", + &format!("{context} payload paths"), + )?, + &format!("{context} payload paths"), + )?; + + let identity_finalizer = + runtime_template.is_some_and(|name| name == DIRECT_IDENTITY_FINALIZER); + if identity_finalizer { + if identity_finalizer_seen { + return Err(invalid( + "direct-template catalog contains more than one generic identity finalizer", + )); + } + if role != DirectExecutorRole::Finalization + || payload_kind != "rusticol-intrinsic" + || prepared_kernel_id.is_some() + { + return Err(invalid(format!( + "{context} has an invalid identity-finalizer contract" + ))); + } + identity_finalizer_seen = true; + bindings.push(PreparedDirectExecutorBinding::identity_finalizer( + direct_executor_id, + )); + } else { + if let Some(kernel_id) = prepared_kernel_id { + prepared_kernel_ids.insert(kernel_id); + } + bindings.push( + PreparedDirectExecutorBinding::evaluator_with_parent_permutation( + role, + evaluator_binding_id, + direct_executor_id, + parent_permutation, + ), + ); + } + } + + let catalog = PreparedDirectExecutorCatalog::new(catalog_digest, bindings)?; + Ok(AuthenticatedDirectTemplateCatalog { + catalog, + catalog_digest, + prepared_kernel_pack_digest, + prepared_kernel_count: prepared_kernel_ids.len(), + exact_executors, + }) +} + +fn exact_sections_from_direct_plan( + plan: &DirectRecurrencePlan, + process_id: String, + executors: Vec, +) -> NativeRecurrenceExactSections { + let public_flow_ids = match plan.strategy() { + RecurrenceStrategy::TopologyReplay => plan + .replay_targets() + .iter() + .map(|target| target.public_flow_id) + .collect(), + RecurrenceStrategy::AllFlowUnion => plan + .amplitude_destinations() + .iter() + .map(|destination| destination.target_sector_id) + .collect(), + RecurrenceStrategy::ContractedColorUnion => Vec::new(), + }; + let exact_factors = plan + .exact_factors() + .iter() + .map(|factor| NativeRecurrenceExactFactor { + real_numerator: factor.real().numerator().to_string(), + real_denominator: factor.real().denominator().to_string(), + imaginary_numerator: factor.imag().numerator().to_string(), + imaginary_denominator: factor.imag().denominator().to_string(), + }) + .collect(); + NativeRecurrenceExactSections { + process_id, + strategy: plan.strategy().as_str().to_owned(), + semantic_digest: plan.semantic_digest().to_string(), + runtime_layout_digest: plan.runtime_layout_digest().to_string(), + current_arena_components: plan.current_arena_components(), + amplitude_destination_count: plan.amplitude_destination_count(), + parameter_value_count: plan.parameter_value_count(), + external_source_count: plan.external_source_count(), + currents: plan.currents().to_vec(), + sources: plan.sources().to_vec(), + contributions: plan.contributions().to_vec(), + finalizations: plan.finalizations().to_vec(), + closures: plan.closures().to_vec(), + row_groups: plan.row_groups().to_vec(), + momentum_forms: plan.momentum_forms().to_vec(), + momentum_terms: plan.momentum_terms().to_vec(), + replay_targets: plan.replay_targets().to_vec(), + source_permutations: plan.source_permutations().to_vec(), + replay_momentum_signs: plan.replay_momentum_signs().to_vec(), + replay_helicity_map: plan.replay_helicity_map().to_vec(), + amplitude_destinations: plan.amplitude_destinations().to_vec(), + resolved_helicities: plan.resolved_helicities().to_vec(), + public_helicities: plan.public_helicities().to_vec(), + source_state_assignments: plan.source_state_assignments().to_vec(), + source_dispatch_variants: plan.source_dispatch_variants().to_vec(), + source_embeddings: plan.source_embeddings().to_vec(), + source_projections: plan.source_projections().to_vec(), + resolved_source_selections: plan.resolved_source_selections().to_vec(), + exact_factors, + public_flow_ids, + executors, + } +} + +fn validate_direct_template_shapes( + template: &JsonMap, + context: &str, +) -> RusticolResult<()> { + let parent_arity = json_u32(template, "parent_arity", &format!("{context} parent arity"))?; + let parent_counts = json_array( + template, + "parent_component_counts", + &format!("{context} parent component counts"), + )?; + if parent_counts.len() != parent_arity as usize { + return Err(invalid(format!( + "{context} parent component counts do not match parent arity" + ))); + } + for (index, value) in parent_counts.iter().enumerate() { + let count = json_value_u32(value, &format!("{context} parent component {index}"))?; + if count == 0 { + return Err(invalid(format!( + "{context} parent component {index} is empty" + ))); + } + } + for field in [ + "coupling_slot_count", + "momentum_operand_count", + "parameter_slot_count", + ] { + json_u32(template, field, &format!("{context} {field}"))?; + } + if json_u32( + template, + "destination_component_count", + &format!("{context} destination component count"), + )? == 0 + { + return Err(invalid(format!( + "{context} destination component count must be positive" + ))); + } + let alignment = json_u32(template, "alignment_bytes", &format!("{context} alignment"))?; + if alignment == 0 || !alignment.is_power_of_two() { + return Err(invalid(format!( + "{context} alignment must be a positive power of two" + ))); + } + validate_string_array( + json_array( + template, + "semantic_template_ids", + &format!("{context} semantic template IDs"), + )?, + &format!("{context} semantic template IDs"), + ) +} + +fn direct_executor_role(value: &str, context: &str) -> RusticolResult { + match value { + "source" => Ok(DirectExecutorRole::Source), + "contribution" => Ok(DirectExecutorRole::Contribution), + "finalization" => Ok(DirectExecutorRole::Finalization), + "closure" => Ok(DirectExecutorRole::Closure), + _ => Err(invalid(format!( + "{context} has unsupported direct role {value:?}" + ))), + } +} + +pub(super) fn canonical_json_bytes(value: &JsonValue, context: &str) -> RusticolResult> { + serde_json::to_vec(value) + .map_err(|error| invalid(format!("could not canonicalize {context}: {error}"))) +} + +fn digest_json_without_field( + value: &JsonValue, + field: &str, + context: &str, +) -> RusticolResult { + let mut semantic = value.clone(); + let object = semantic + .as_object_mut() + .ok_or_else(|| invalid(format!("{context} must be a JSON object")))?; + if object.remove(field).is_none() { + return Err(invalid(format!("{context} has no {field:?} field"))); + } + let digest: [u8; 32] = Sha256::digest(canonical_json_bytes(&semantic, context)?).into(); + SemanticDigest::new(digest) +} + +pub(super) fn json_object<'a>( + value: &'a JsonValue, + context: &str, +) -> RusticolResult<&'a JsonMap> { + value + .as_object() + .ok_or_else(|| invalid(format!("{context} must be a JSON object"))) +} + +pub(super) fn require_json_fields( + object: &JsonMap, + expected: &[&str], + context: &str, +) -> RusticolResult<()> { + require_json_fields_with_optional(object, expected, &[], context) +} + +fn require_json_fields_with_optional( + object: &JsonMap, + expected: &[&str], + optional: &[&str], + context: &str, +) -> RusticolResult<()> { + let expected = expected.iter().copied().collect::>(); + let optional = optional.iter().copied().collect::>(); + let allowed = expected.union(&optional).copied().collect::>(); + let actual = object.keys().map(String::as_str).collect::>(); + if expected.is_subset(&actual) && actual.is_subset(&allowed) { + return Ok(()); + } + let missing = expected.difference(&actual).copied().collect::>(); + let unexpected = actual.difference(&allowed).copied().collect::>(); + Err(invalid(format!( + "{context} fields do not match direct-template-v1; missing={missing:?}, unexpected={unexpected:?}" + ))) +} + +pub(super) fn json_field<'a>( + object: &'a JsonMap, + field: &str, + context: &str, +) -> RusticolResult<&'a JsonValue> { + object + .get(field) + .ok_or_else(|| invalid(format!("{context} has no {field:?} field"))) +} + +pub(super) fn json_string<'a>( + object: &'a JsonMap, + field: &str, + context: &str, +) -> RusticolResult<&'a str> { + json_field(object, field, context)? + .as_str() + .ok_or_else(|| invalid(format!("{context} must be a string"))) +} + +pub(super) fn json_nonempty_string<'a>( + object: &'a JsonMap, + field: &str, + context: &str, +) -> RusticolResult<&'a str> { + let value = json_string(object, field, context)?; + if value.is_empty() { + return Err(invalid(format!("{context} must not be empty"))); + } + Ok(value) +} + +pub(super) fn require_json_string_value( + object: &JsonMap, + field: &str, + expected: &str, + context: &str, +) -> RusticolResult<()> { + let actual = json_string(object, field, context)?; + if actual != expected { + return Err(invalid(format!( + "{context} {field:?} is {actual:?}, expected {expected:?}" + ))); + } + Ok(()) +} + +pub(super) fn json_bool( + object: &JsonMap, + field: &str, + context: &str, +) -> RusticolResult { + json_field(object, field, context)? + .as_bool() + .ok_or_else(|| invalid(format!("{context} must be a boolean"))) +} + +pub(super) fn json_u32( + object: &JsonMap, + field: &str, + context: &str, +) -> RusticolResult { + json_value_u32(json_field(object, field, context)?, context) +} + +fn json_value_u32(value: &JsonValue, context: &str) -> RusticolResult { + value + .as_u64() + .and_then(|value| u32::try_from(value).ok()) + .ok_or_else(|| invalid(format!("{context} must be a nonnegative u32"))) +} + +pub(super) fn json_optional_u32( + object: &JsonMap, + field: &str, + context: &str, +) -> RusticolResult> { + let value = json_field(object, field, context)?; + if value.is_null() { + return Ok(None); + } + json_value_u32(value, context).map(Some) +} + +fn json_optional_string<'a>( + object: &'a JsonMap, + field: &str, + context: &str, +) -> RusticolResult> { + let value = json_field(object, field, context)?; + if value.is_null() { + return Ok(None); + } + value + .as_str() + .filter(|value| !value.is_empty()) + .map(Some) + .ok_or_else(|| invalid(format!("{context} must be null or a nonempty string"))) +} + +pub(super) fn json_array<'a>( + object: &'a JsonMap, + field: &str, + context: &str, +) -> RusticolResult<&'a [JsonValue]> { + json_field(object, field, context)? + .as_array() + .map(Vec::as_slice) + .ok_or_else(|| invalid(format!("{context} must be a JSON array"))) +} + +fn validate_string_array(values: &[JsonValue], context: &str) -> RusticolResult<()> { + if values + .iter() + .all(|value| value.as_str().is_some_and(|value| !value.is_empty())) + { + return Ok(()); + } + Err(invalid(format!( + "{context} must contain only nonempty strings" + ))) +} + +fn json_sha256( + object: &JsonMap, + field: &str, + context: &str, +) -> RusticolResult { + semantic_digest_from_hex(json_string(object, field, context)?, context) +} + +fn report_recurrence_build_progress( + callback: Option<&Py>, + progress: RecurrenceBuildProgress, +) -> RusticolResult<()> { + let Some(callback) = callback else { + return Ok(()); + }; + Python::attach(|py| -> PyResult<()> { + let payload = PyDict::new(py); + payload.set_item("step", progress.phase)?; + payload.set_item("phase_index", progress.phase_index)?; + payload.set_item("phase_total", progress.phase_total)?; + payload.set_item("stage_total", progress.stage_total)?; + payload.set_item( + "candidate_parent_pair_count", + progress.candidate_parent_pair_count, + )?; + payload.set_item("current_count", progress.current_count)?; + payload.set_item("contribution_count", progress.contribution_count)?; + payload.set_item( + "dynamic_color_state_count", + progress.dynamic_color_state_count, + )?; + payload.set_item( + "color_target_prune_count", + progress.color_target_prune_count, + )?; + if let Some(stage_index) = progress.stage_index { + payload.set_item("stage_index", stage_index)?; + } + if let Some(subset_size) = progress.subset_size { + payload.set_item("subset_size", subset_size)?; + } + if let Some(total) = progress.candidate_parent_pair_total { + payload.set_item("candidate_parent_pair_total", total)?; + } + callback.bind(py).call1((payload,))?; + Ok(()) + }) + .map_err(|error| invalid(format!("recurrence progress callback failed: {error}"))) +} + +fn recurrence_generation_profile_mapping<'py>( + py: Python<'py>, + native: &NativeDirectLoweringResult, +) -> PyResult> { + let profile = PyDict::new(py); + profile.set_item("schema_version", 1)?; + profile.set_item("scope", "generation-only")?; + + let telemetry = &native.generation_profile; + let timings = PyDict::new(py); + for (name, nanoseconds) in [ + ( + "transition-catalog", + telemetry.transition_catalog_nanoseconds, + ), + ( + "structural-feasibility", + telemetry.structural_feasibility_nanoseconds, + ), + ( + "color-target-index", + telemetry.color_target_index_nanoseconds, + ), + ("structural-demand", telemetry.structural_demand_nanoseconds), + ("support-indexing", telemetry.support_indexing_nanoseconds), + ( + "candidate-processing", + telemetry.candidate_processing_nanoseconds, + ), + ( + "closure-processing", + telemetry.closure_processing_nanoseconds, + ), + ( + "canonical-emission", + telemetry.canonical_emission_nanoseconds, + ), + ] { + timings.set_item(name, nanoseconds as f64 / 1_000_000_000.0)?; + } + for (name, seconds) in [ + ( + "python-extraction", + native.timings.python_extraction_seconds, + ), + ( + "catalog-authentication", + native.timings.catalog_authentication_seconds, + ), + ( + "semantic-construction-total", + native.timings.semantic_construction_seconds, + ), + ("direct-lowering", native.timings.direct_lowering_seconds), + ("serialization", native.timings.serialization_seconds), + ("native-total", native.timings.native_total_seconds), + ] { + timings.set_item(name, seconds)?; + } + profile.set_item("timings_seconds", timings)?; + + let counters = PyDict::new(py); + for (name, value) in [ + ("support_bucket_count", telemetry.support_bucket_count), + ( + "support_bucket_probe_count", + telemetry.support_bucket_probe_count, + ), + ( + "support_bucket_cache_hit_count", + telemetry.support_bucket_cache_hit_count, + ), + ( + "support_bucket_cache_miss_count", + telemetry.support_bucket_cache_miss_count, + ), + ( + "candidate_parent_pair_theoretical_count", + telemetry.candidate_parent_pair_theoretical_count, + ), + ( + "candidate_parent_pair_visited_count", + telemetry.candidate_parent_pair_visited_count, + ), + ( + "structural_feasible_support_count", + telemetry.structural_feasible_support_count, + ), + ( + "structural_decomposition_count", + telemetry.structural_decomposition_count, + ), + ( + "structural_forward_transition_probe_count", + telemetry.structural_forward_transition_probe_count, + ), + ( + "structural_demand_support_count", + telemetry.structural_demand_support_count, + ), + ( + "structural_demand_state_count", + telemetry.structural_demand_state_count, + ), + ("structural_reject_count", telemetry.structural_reject_count), + ( + "transition_index_hit_count", + telemetry.transition_index_hit_count, + ), + ( + "transition_index_miss_count", + telemetry.transition_index_miss_count, + ), + ( + "transition_candidate_count", + telemetry.transition_candidate_count, + ), + ("quantum_match_count", telemetry.quantum_match_count), + ("coupling_match_count", telemetry.coupling_match_count), + ("transition_accept_count", telemetry.transition_accept_count), + ("color_shape_match_count", telemetry.color_shape_match_count), + ("color_result_count", telemetry.color_result_count), + ( + "color_target_accept_count", + telemetry.color_target_accept_count, + ), + ( + "color_target_reject_count", + telemetry.color_target_reject_count, + ), + ( + "color_acceptance_cache_hit_count", + telemetry.color_acceptance_cache_hit_count, + ), + ( + "color_acceptance_cache_miss_count", + telemetry.color_acceptance_cache_miss_count, + ), + ( + "color_fragment_bucket_count", + telemetry.color_fragment_bucket_count, + ), + ( + "color_fragment_hash_lookup_count", + telemetry.color_fragment_hash_lookup_count, + ), + ( + "color_posting_incidence_count", + telemetry.color_posting_incidence_count, + ), + ( + "color_sparse_posting_bucket_count", + telemetry.color_sparse_posting_bucket_count, + ), + ( + "color_dense_posting_bucket_count", + telemetry.color_dense_posting_bucket_count, + ), + ( + "color_sparse_posting_bytes", + telemetry.color_sparse_posting_bytes, + ), + ( + "color_dense_posting_bytes", + telemetry.color_dense_posting_bytes, + ), + ( + "accepted_parent_key_clone_count", + telemetry.accepted_parent_key_clone_count, + ), + ( + "current_key_lookup_count", + telemetry.current_key_lookup_count, + ), + ("current_key_hit_count", telemetry.current_key_hit_count), + ("current_insert_count", telemetry.current_insert_count), + ("current_key_clone_count", telemetry.current_key_clone_count), + ( + "indexed_hash_lookup_count", + telemetry.indexed_hash_lookup_count, + ), + ( + "contribution_attempt_count", + telemetry.contribution_attempt_count, + ), + ( + "contribution_insert_count", + telemetry.contribution_insert_count, + ), + ( + "contribution_merge_count", + telemetry.contribution_merge_count, + ), + ( + "closure_candidate_theoretical_count", + telemetry.closure_candidate_theoretical_count, + ), + ("closure_candidate_count", telemetry.closure_candidate_count), + ( + "closure_support_lookup_count", + telemetry.closure_support_lookup_count, + ), + ( + "closure_state_match_count", + telemetry.closure_state_match_count, + ), + ( + "closure_color_attempt_count", + telemetry.closure_color_attempt_count, + ), + ("closure_group_count", telemetry.closure_group_count), + ( + "closure_proof_contribution_count", + telemetry.closure_proof_contribution_count, + ), + ( + "constructed_current_count", + telemetry.constructed_current_count, + ), + ( + "constructed_contribution_count", + telemetry.constructed_contribution_count, + ), + ( + "constructed_interaction_count", + telemetry.constructed_interaction_count, + ), + ( + "constructed_dynamic_color_state_count", + telemetry.constructed_dynamic_color_state_count, + ), + ("emitted_current_count", telemetry.emitted_current_count), + ( + "emitted_contribution_count", + telemetry.emitted_contribution_count, + ), + ( + "emitted_interaction_count", + telemetry.emitted_interaction_count, + ), + ( + "emitted_finalization_count", + telemetry.emitted_finalization_count, + ), + ("emitted_closure_count", telemetry.emitted_closure_count), + ] { + counters.set_item(name, value)?; + } + profile.set_item("operation_counters", counters)?; + + let serialized_bytes = PyDict::new(py); + serialized_bytes.set_item("plan_payload", native.plan_payload_size)?; + serialized_bytes.set_item("container", native.container_size)?; + serialized_bytes.set_item("unpacked_container", native.unpacked_size_bytes)?; + profile.set_item("serialized_bytes", serialized_bytes)?; + Ok(profile) +} + +fn direct_lowering_mapping( + py: Python<'_>, + native: NativeDirectLoweringResult, +) -> PyResult> { + let generation_profile = recurrence_generation_profile_mapping(py, &native)?; + let result = PyDict::new(py); + result.set_item("kind", DIRECT_LOWERING_RESULT_KIND)?; + result.set_item("schema_version", DIRECT_LOWERING_RESULT_SCHEMA_VERSION)?; + result.set_item("builder_input_abi", DIRECT_BUILDER_INPUT_ABI)?; + result.set_item("builder_input_sha256", native.builder_input_digest)?; + result.set_item( + "template_input_abi", + template::RECURRENCE_TEMPLATE_INPUT_ABI, + )?; + result.set_item("template_input_sha256", native.template_input_digest)?; + result.set_item( + "prepared_kernel_pack_digest", + native.prepared_kernel_pack_digest, + )?; + result.set_item("direct_template_abi", RECURRENCE_DIRECT_TEMPLATE_ABI)?; + result.set_item( + "direct_template_catalog_digest", + native.direct_template_catalog_digest, + )?; + result.set_item("recurrence_plan_abi", RECURRENCE_DIRECT_PLAN_ABI)?; + result.set_item("runtime_layout_abi", RECURRENCE_DIRECT_RUNTIME_LAYOUT_ABI)?; + result.set_item( + "required_runtime_capabilities", + PyList::new( + py, + [ + match native.strategy { + RecurrenceStrategy::ContractedColorUnion => { + RECURRENCE_CONTRACTED_COLOR_CAPABILITY + } + RecurrenceStrategy::TopologyReplay | RecurrenceStrategy::AllFlowUnion => { + RECURRENCE_LC_COLOR_CAPABILITY + } + }, + RECURRENCE_DIRECT_RUNTIME_CAPABILITY, + ], + )?, + )?; + + let container = PyDict::new(py); + container.set_item("kind", RUNTIME_CONTAINER_KIND)?; + container.set_item("schema_version", RUNTIME_CONTAINER_SCHEMA_VERSION)?; + container.set_item("storage_abi", STORAGE_ABI)?; + container.set_item("member_count", native.member_count)?; + container.set_item("unpacked_size_bytes", native.unpacked_size_bytes)?; + container.set_item("index_sha256", native.index_sha256)?; + result.set_item("runtime_container", container)?; + + let inspection = PyDict::new(py); + inspection.set_item("execution_mode", "recurrence")?; + inspection.set_item("recurrence_plan_abi", RECURRENCE_DIRECT_PLAN_ABI)?; + inspection.set_item("runtime_layout_abi", RECURRENCE_DIRECT_RUNTIME_LAYOUT_ABI)?; + inspection.set_item("direct_template_abi", RECURRENCE_DIRECT_TEMPLATE_ABI)?; + inspection.set_item("process_id", native.process_id)?; + inspection.set_item("lc_flow_layout", native.strategy.as_str())?; + inspection.set_item("prepared_kernel_count", native.prepared_kernel_count)?; + inspection.set_item("parameter_count", native.parameter_value_count)?; + inspection.set_item("sector_count", native.physical_sector_count)?; + inspection.set_item("direct_executor_count", native.direct_executor_count)?; + inspection.set_item("semantic_digest", native.semantic_digest)?; + inspection.set_item("runtime_layout_digest", native.runtime_layout_digest)?; + + let schedule = PyDict::new(py); + schedule.set_item("current_count", native.current_count)?; + schedule.set_item("source_row_count", native.source_row_count)?; + schedule.set_item("contribution_count", native.contribution_count)?; + schedule.set_item("finalization_count", native.finalization_count)?; + schedule.set_item("closure_term_count", native.closure_count)?; + schedule.set_item( + "amplitude_destination_count", + native.amplitude_destination_count, + )?; + schedule.set_item("replay_target_count", native.replay_target_count)?; + schedule.set_item("resolved_helicity_count", native.resolved_helicity_count)?; + schedule.set_item("retained_helicity_count", native.retained_helicity_count)?; + schedule.set_item("exact_factor_count", native.exact_factor_count)?; + inspection.set_item("schedule", schedule)?; + + let selector_work = PyDict::new(py); + selector_work.set_item("schema_version", 1)?; + selector_work.set_item("binding", "runtime_layout_digest")?; + let persisted = PyDict::new(py); + persisted.set_item("current_count", native.current_count)?; + persisted.set_item("semantic_component_count", native.semantic_component_count)?; + persisted.set_item("source_row_count", native.source_row_count)?; + persisted.set_item("contribution_count", native.contribution_count)?; + persisted.set_item("finalization_count", native.finalization_count)?; + persisted.set_item("closure_count", native.closure_count)?; + persisted.set_item( + "row_count", + native.source_row_count + + native.contribution_count + + native.finalization_count + + native.closure_count, + )?; + selector_work.set_item("persisted_union", persisted)?; + let representatives = PyList::empty(py); + for (summary, public_flow_count) in native.selector_work { + let entry = PyDict::new(py); + entry.set_item("representative_sector_id", summary.physical_sector_id)?; + entry.set_item("public_flow_count", public_flow_count)?; + entry.set_item("current_count", summary.current_count)?; + entry.set_item("semantic_component_count", summary.semantic_component_count)?; + entry.set_item("source_row_count", summary.source_row_count)?; + entry.set_item("contribution_count", summary.contribution_count)?; + entry.set_item("finalization_count", summary.finalization_count)?; + entry.set_item("closure_count", summary.closure_count)?; + entry.set_item( + "amplitude_destination_count", + summary.amplitude_destination_count, + )?; + entry.set_item("row_count", summary.row_count())?; + representatives.append(entry)?; + } + selector_work.set_item("representatives", representatives)?; + inspection.set_item("selector_work_certificate", selector_work)?; + + let construction = PyDict::new(py); + construction.set_item("peak_current_count", native.construction.peak_current_count)?; + construction.set_item( + "peak_contribution_count", + native.construction.peak_contribution_count, + )?; + construction.set_item( + "peak_contribution_count_semantics", + "resident-pending-contributions-v1", + )?; + construction.set_item( + "peak_dynamic_color_state_count", + native.construction.peak_dynamic_color_state_count, + )?; + construction.set_item( + "color_target_prune_count", + native.construction.color_target_prune_count, + )?; + construction.set_item( + "candidate_parent_pair_count", + native.construction.candidate_parent_pair_count(), + )?; + construction.set_item( + "peak_to_final_current_ratio", + native.construction.peak_current_count as f64 / native.current_count.max(1) as f64, + )?; + construction.set_item( + "peak_to_final_contribution_ratio", + native.construction.peak_contribution_count as f64 + / native.contribution_count.max(1) as f64, + )?; + inspection.set_item("construction", construction)?; + + let direct_arena = PyDict::new(py); + direct_arena.set_item("semantic_component_count", native.semantic_component_count)?; + direct_arena.set_item("current_arena_components", native.current_arena_components)?; + direct_arena.set_item( + "arena_component_reuse_count", + native + .semantic_component_count + .saturating_sub(u64::from(native.current_arena_components)), + )?; + direct_arena.set_item("momentum_form_count", native.momentum_form_count)?; + direct_arena.set_item("selector_domain_count", native.selector_domain_count)?; + direct_arena.set_item( + "cache_footprint_policy", + if native.strategy == RecurrenceStrategy::TopologyReplay { + "selector-active-max-v1" + } else { + "persisted-arena-v1" + }, + )?; + direct_arena.set_item("row_group_count", native.row_group_count)?; + direct_arena.set_item("packed_input_bytes", 0)?; + direct_arena.set_item("packed_output_bytes", 0)?; + direct_arena.set_item("scatter_bytes", 0)?; + inspection.set_item("direct_arena", direct_arena)?; + if let Some(report) = native.relation_discovery.as_ref() { + inspection.set_item( + "relation_discovery", + relation_discovery_mapping(py, report)?, + )?; + } + + let member = PyDict::new(py); + member.set_item("path", RECURRENCE_DIRECT_SCHEDULE_MEMBER)?; + member.set_item("size_bytes", native.plan_payload_size)?; + member.set_item("sha256", native.plan_sha256)?; + member.set_item("container_size_bytes", native.container_size)?; + inspection.set_item("runtime_container_member", member)?; + if let (Some(size_bytes), Some(sha256)) = ( + native.projection_certificate_payload_size, + native.projection_certificate_sha256, + ) { + let certificate = PyDict::new(py); + certificate.set_item( + "path", + rusticol_core::recurrence::RECURRENCE_COLOR_PROJECTION_CERTIFICATE_MEMBER, + )?; + certificate.set_item("schema_version", 1)?; + certificate.set_item("proof_kind", "exact-rectangular-sum-projection")?; + certificate.set_item("publishable", true)?; + certificate.set_item("size_bytes", size_bytes)?; + certificate.set_item("sha256", sha256)?; + inspection.set_item("color_projection_certificate", certificate)?; + } + if let Some(certificate) = native.closure_target_domain_certificate.as_ref() { + let provenance = PyDict::new(py); + provenance.set_item("proof_kind", CLOSURE_TARGET_DOMAIN_PROOF_KIND)?; + provenance.set_item("semantic_digest", &certificate.semantic_digest)?; + provenance.set_item("sector_count", certificate.physical_sector_count)?; + provenance.set_item( + "resolved_helicity_count", + certificate.resolved_helicity_count, + )?; + provenance.set_item( + "certified_structural_zero_sector_count", + certificate + .certified_structural_zero_physical_sector_ids + .len(), + )?; + inspection.set_item("structural_zero_physical_sector_certificate", provenance)?; + } + + let timings = PyDict::new(py); + timings.set_item( + "python_extraction", + native.timings.python_extraction_seconds, + )?; + timings.set_item( + "catalog_authentication", + native.timings.catalog_authentication_seconds, + )?; + timings.set_item( + "semantic_construction", + native.timings.semantic_construction_seconds, + )?; + timings.set_item("direct_lowering", native.timings.direct_lowering_seconds)?; + timings.set_item("serialization", native.timings.serialization_seconds)?; + timings.set_item("native_total", native.timings.native_total_seconds)?; + inspection.set_item("generation_timings_seconds", timings)?; + + result.set_item("inspection_summary", inspection)?; + result.set_item("resolved_helicities", native.resolved_helicities)?; + result.set_item("amplitude_destinations", native.amplitude_destinations)?; + if let Some(certificate) = native.closure_target_domain_certificate.as_ref() { + let metadata = PyDict::new(py); + metadata.set_item("proof_kind", CLOSURE_TARGET_DOMAIN_PROOF_KIND)?; + metadata.set_item( + "sector_ids", + &certificate.certified_structural_zero_physical_sector_ids, + )?; + metadata.set_item("semantic_digest", &certificate.semantic_digest)?; + metadata.set_item("sector_count", certificate.physical_sector_count)?; + metadata.set_item( + "resolved_helicity_count", + certificate.resolved_helicity_count, + )?; + result.set_item("structural_zero_physical_sector_certificate", metadata)?; + } else { + result.set_item("structural_zero_physical_sector_certificate", py.None())?; + } + if let Some(dispatch) = native.helicity_dispatch.as_ref() { + let metadata = PyDict::new(py); + metadata.set_item("abi", RECURRENCE_HELICITY_DISPATCH_ABI)?; + metadata.set_item("size_bytes", dispatch.size_bytes)?; + metadata.set_item("sha256", &dispatch.sha256)?; + metadata.set_item( + "base_runtime_layout_digest", + &dispatch.base_runtime_layout_digest, + )?; + metadata.set_item("resolved_helicity_count", dispatch.resolved_helicity_count)?; + result.set_item("helicity_dispatch", metadata)?; + } else { + result.set_item("helicity_dispatch", py.None())?; + } + result.set_item( + "exact_sections", + crate::recurrence_exact_sections_to_python(py, native.exact_sections)?, + )?; + result.set_item("generation_profile", generation_profile)?; + Ok(result.into_any().unbind()) +} + +fn relation_discovery_mapping<'py>( + py: Python<'py>, + report: &RecurrenceRelationDiscoveryReport, +) -> PyResult> { + let payload = PyDict::new(py); + payload.set_item("schema_version", 1)?; + payload.set_item("requested_mode", report.requested_mode.as_str())?; + payload.set_item("state", report.state)?; + + let scope = PyDict::new(py); + scope.set_item("execution_mode", "recurrence")?; + scope.set_item("color_accuracy", &report.color_accuracy)?; + scope.set_item("representation", "recurrence-direct-plan-v2")?; + scope.set_item("lc_flow_layout", report.strategy.as_str())?; + payload.set_item("scope", scope)?; + + let probe = PyDict::new(py); + probe.set_item("status", "completed")?; + probe.set_item("precision_digits", report.precision_digits)?; + probe.set_item("probe_count", report.probe_count)?; + probe.set_item("verification_probe_count", report.verification_probe_count)?; + probe.set_item( + "effective_projection_count", + report.effective_projection_count, + )?; + probe.set_item("seed", report.seed)?; + probe.set_item("deterministic", true)?; + probe.set_item( + "candidate_only", + report.certificate_algorithm + != rusticol_core::recurrence::NUMERICAL_RELATION_CERTIFICATE_ALGORITHM, + )?; + probe.set_item("tested_hypothesis_count", report.tested_hypothesis_count)?; + probe.set_item( + "verification_rejected_count", + report.verification_rejected_count, + )?; + probe.set_item( + "runtime_parameter_schema_sha256", + report + .authenticated_runtime_parameter_schema_sha256 + .as_deref(), + )?; + probe.set_item( + "candidate_observation_batch_sha256", + report + .authenticated_candidate_observation_batch_sha256 + .as_deref(), + )?; + probe.set_item( + "verification_observation_batch_sha256", + report + .authenticated_verification_observation_batch_sha256 + .as_deref(), + )?; + probe.set_item( + "decision_sha256", + report.authenticated_decision_sha256.as_deref(), + )?; + probe.set_item( + "rejection_decision_sha256", + &report.rejected_decision_sha256, + )?; + payload.set_item("probe", probe)?; + + let replay = PyDict::new(py); + replay.set_item("algorithm", &report.certificate_algorithm)?; + replay.set_item( + "status", + if report.certificates.is_empty() { + "no-certified-relations" + } else { + "verified" + }, + )?; + replay.set_item( + "certificate_set_sha256", + relation_certificate_set_sha256(report), + )?; + payload.set_item("certificate_replay", replay)?; + payload.set_item( + "numerical_candidate_count", + report.numerical_candidate_count, + )?; + payload.set_item( + "uncertified_candidate_count", + report.uncertified_candidate_count, + )?; + payload.set_item( + "rejected_hypothesis_count", + report.rejected_hypothesis_count, + )?; + payload.set_item( + "exact_certified_relation_count", + report.exact_certified_relation_count, + )?; + payload.set_item("applied_relation_count", report.applied_relation_count)?; + payload.set_item( + "interaction_evaluation_count_before", + report.interaction_evaluation_count_before, + )?; + payload.set_item( + "interaction_evaluation_count_after", + report.interaction_evaluation_count_after, + )?; + payload.set_item( + "interaction_evaluation_savings", + report + .interaction_evaluation_count_before + .saturating_sub(report.interaction_evaluation_count_after), + )?; + payload.set_item("current_count_before", report.current_count_before)?; + payload.set_item("current_count_after", report.current_count_after)?; + payload.set_item( + "contribution_count_before", + report.contribution_count_before, + )?; + payload.set_item("contribution_count_after", report.contribution_count_after)?; + payload.set_item("scale_copy_row_count", report.scale_copy_row_count)?; + + let certificates = PyList::empty(py); + // Recurrence schedules can contain many exact aliases. Keep generation + // metadata bounded while the aggregate digest authenticates the complete + // replayed certificate set and the plan binds every applied scale row. + const CERTIFICATE_SAMPLE_LIMIT: usize = 16; + for certificate in report.certificates.iter().take(CERTIFICATE_SAMPLE_LIMIT) { + let entry = PyDict::new(py); + entry.set_item("algorithm", &report.certificate_algorithm)?; + entry.set_item("current_id", certificate.current_id)?; + entry.set_item("representative_id", certificate.representative_id)?; + let factor = PyDict::new(py); + factor.set_item( + "real_numerator", + certificate.factor.real().numerator().to_string(), + )?; + factor.set_item( + "real_denominator", + certificate.factor.real().denominator().to_string(), + )?; + factor.set_item( + "imag_numerator", + certificate.factor.imag().numerator().to_string(), + )?; + factor.set_item( + "imag_denominator", + certificate.factor.imag().denominator().to_string(), + )?; + entry.set_item("factor_exact_rational", factor)?; + entry.set_item( + "current_term_vector_sha256", + &certificate.current_expression_sha256, + )?; + entry.set_item( + "representative_term_vector_sha256", + &certificate.representative_expression_sha256, + )?; + entry.set_item("proof_sha256", &certificate.proof_sha256)?; + certificates.append(entry)?; + } + payload.set_item("certificates", certificates)?; + payload.set_item("certificate_count", report.certificates.len())?; + payload.set_item( + "certificates_truncated", + report.certificates.len() > CERTIFICATE_SAMPLE_LIMIT, + )?; + + let rejected = PyList::empty(py); + for reason in &report.rejected_candidates { + let entry = PyDict::new(py); + entry.set_item("reason", reason)?; + rejected.append(entry)?; + } + payload.set_item("rejected_candidates", rejected)?; + let rejected_diagnostics = PyDict::new(py); + rejected_diagnostics.set_item( + "total_rejected_hypothesis_count", + report.rejected_hypothesis_count, + )?; + rejected_diagnostics.set_item("retained_count", report.rejected_candidates.len())?; + rejected_diagnostics.set_item( + "truncated", + report.rejected_hypothesis_count > report.rejected_candidates.len(), + )?; + let authenticated_raw_decisions = report.authenticated_decision_sha256.is_some(); + if authenticated_raw_decisions && !report.rejected_candidates.is_empty() { + return Err(PyValueError::new_err( + "authenticated raw relation diagnostics must use zero retained rows", + )); + } + rejected_diagnostics.set_item( + "truncation_policy", + if authenticated_raw_decisions { + "none-authenticated-full-rejection-digest-v1" + } else { + "first-16-in-canonical-discovery-order-v1" + }, + )?; + let retained_rows = report + .rejected_candidates + .iter() + .map(|reason| serde_json::json!({"reason": reason})) + .collect::>(); + let retained_payload = serde_json::json!({ + "abi": "pyamplicol-rejected-relation-diagnostics-v1", + "rows": retained_rows, + }); + let retained_bytes = + canonical_json_bytes(&retained_payload, "native recurrence rejected diagnostics") + .map_err(|error| PyValueError::new_err(error.to_string()))?; + rejected_diagnostics.set_item( + "retained_sha256", + hex_digest(Sha256::digest(retained_bytes)), + )?; + rejected_diagnostics.set_item( + "full_census_sha256", + hex_digest(Sha256::digest( + canonical_json_bytes( + &serde_json::json!({ + "abi": "pyamplicol-relation-discovery-full-census-v1", + "tested_hypothesis_count": report.tested_hypothesis_count, + "numerical_candidate_count": report.numerical_candidate_count, + "verification_rejected_count": report.verification_rejected_count, + "uncertified_candidate_count": + report.uncertified_candidate_count, + "certified_relation_count": report.exact_certified_relation_count, + "rejected_hypothesis_count": + report.rejected_hypothesis_count, + "applied_relation_count": report.applied_relation_count, + "runtime_parameter_schema_sha256": + report.authenticated_runtime_parameter_schema_sha256, + "candidate_observation_batch_sha256": + report.authenticated_candidate_observation_batch_sha256, + "verification_observation_batch_sha256": + report.authenticated_verification_observation_batch_sha256, + "decision_sha256": report.authenticated_decision_sha256, + "rejection_decision_sha256": + report.rejected_decision_sha256, + }), + "native recurrence relation census", + ) + .map_err(|error| PyValueError::new_err(error.to_string()))?, + )), + )?; + rejected_diagnostics.set_item("full_rejection_sha256", &report.rejected_decision_sha256)?; + payload.set_item("rejected_candidate_diagnostics", rejected_diagnostics)?; + payload.set_item( + "follow_up_boundary", + "Direct-plan-v2 retains dense semantic current descriptors and exact closure bindings; exact certified reuse removes interaction rows and emits one scale-copy row per relation. Direct-Arena lifetime allocation continues to own physical component recycling.", + )?; + Ok(payload) +} + +fn relation_certificate_set_sha256(report: &RecurrenceRelationDiscoveryReport) -> String { + if let Some(digest) = report.authenticated_certificate_set_sha256.as_ref() { + return digest.clone(); + } + let mut hash = Sha256::new(); + hash.update(b"pyamplicol-recurrence-relation-certificate-set-v1"); + hash.update((report.certificates.len() as u64).to_le_bytes()); + for certificate in &report.certificates { + hash.update(certificate.current_id.to_le_bytes()); + hash.update(certificate.representative_id.to_le_bytes()); + for value in [ + certificate.factor.real().numerator(), + certificate.factor.real().denominator(), + certificate.factor.imag().numerator(), + certificate.factor.imag().denominator(), + ] { + let text = value.to_string(); + hash.update((text.len() as u64).to_le_bytes()); + hash.update(text.as_bytes()); + } + hash.update(certificate.current_expression_sha256.as_bytes()); + hash.update(certificate.representative_expression_sha256.as_bytes()); + hash.update(certificate.proof_sha256.as_bytes()); + } + format!("{:x}", hash.finalize()) +} + +fn parse_input(input: &Bound<'_, PyAny>) -> PyResult { + if cfg!(not(target_endian = "little")) { + return Err(PyValueError::new_err( + "recurrence builder input v2 requires a little-endian target", + )); + } + let abi = required_string(input, "abi")?; + if abi != RECURRENCE_BUILDER_INPUT_ABI { + return Err(PyValueError::new_err(format!( + "unsupported recurrence builder input ABI {abi:?}" + ))); + } + let declared_digest = required_string(input, "digest")?; + validate_sha256_text(&declared_digest, "recurrence builder input digest")?; + + let table_objects = iterable_attribute(input, "tables", "recurrence builder tables")?; + if table_objects.len() != TABLE_SPECS.len() { + return Err(PyValueError::new_err(format!( + "recurrence builder table inventory has {} tables, expected {}", + table_objects.len(), + TABLE_SPECS.len() + ))); + } + + let mut tables = Vec::with_capacity(TABLE_SPECS.len()); + let mut table_by_name = BTreeMap::new(); + for (table_object, spec) in table_objects.into_iter().zip(TABLE_SPECS) { + let table_name = required_nonempty_string(&table_object, "name", "table name")?; + if table_name != spec.name { + return Err(PyValueError::new_err(format!( + "recurrence builder table inventory mismatch: found {table_name:?}, expected {:?}", + spec.name + ))); + } + let row_count = table_object + .getattr("row_count")? + .extract::() + .map_err(|_| { + PyTypeError::new_err(format!( + "recurrence builder table {table_name:?} row_count must be u64" + )) + })?; + let column_objects = iterable_attribute( + &table_object, + "columns", + &format!("recurrence builder table {table_name:?} columns"), + )?; + if column_objects.len() != spec.columns.len() { + return Err(PyValueError::new_err(format!( + "recurrence builder table {table_name:?} has {} columns, expected {}", + column_objects.len(), + spec.columns.len() + ))); + } + + let mut columns = Vec::with_capacity(spec.columns.len()); + let mut column_by_name = BTreeMap::new(); + for (column_object, column_spec) in column_objects.into_iter().zip(spec.columns) { + let column_name = required_nonempty_string(&column_object, "name", "column name")?; + if column_name != column_spec.name { + return Err(PyValueError::new_err(format!( + "recurrence builder table {table_name:?} column mismatch: found {column_name:?}, expected {:?}", + column_spec.name + ))); + } + let context = format!("{table_name}.{column_name}"); + let values_object = column_object.getattr("values")?; + let dtype = values_object + .getattr("dtype")? + .getattr("str")? + .extract::()?; + if dtype != column_spec.kind.dtype() { + return Err(PyValueError::new_err(format!( + "{context} has dtype {dtype:?}, expected {:?}", + column_spec.kind.dtype() + ))); + } + let flags = values_object.getattr("flags")?; + if flags.getattr("writeable")?.extract::()? { + return Err(PyValueError::new_err(format!( + "{context} must be read-only" + ))); + } + if !flags.getattr("owndata")?.extract::()? { + return Err(PyValueError::new_err(format!( + "{context} must own its storage" + ))); + } + let (values, shape) = extract_owned_values(&values_object, column_spec.kind, &context)?; + if shape.first().copied() != Some(row_count) { + return Err(PyValueError::new_err(format!( + "{context} first dimension does not match row_count {row_count}" + ))); + } + let actual_tail = shape + .iter() + .skip(1) + .map(|value| usize::try_from(*value)) + .collect::, _>>() + .map_err(|_| PyValueError::new_err(format!("{context} shape exceeds usize")))?; + if actual_tail != column_spec.tail_shape { + return Err(PyValueError::new_err(format!( + "{context} has tail shape {actual_tail:?}, expected {:?}", + column_spec.tail_shape + ))); + } + column_by_name.insert(column_name.clone(), columns.len()); + columns.push(OwnedColumn { + name: column_name, + dtype: column_spec.kind.dtype(), + shape, + values, + }); + } + table_by_name.insert(table_name.clone(), tables.len()); + tables.push(OwnedTable { + name: table_name, + row_count, + columns, + column_by_name, + }); + } + + let pairing_table_objects = if input.hasattr("fermion_pairing_tables")? { + iterable_attribute( + input, + "fermion_pairing_tables", + "recurrence fermion-pairing tables", + )? + } else { + Vec::new() + }; + if !pairing_table_objects.is_empty() + && pairing_table_objects.len() != FERMION_PAIRING_TABLE_SPECS.len() + { + return Err(PyValueError::new_err(format!( + "recurrence fermion-pairing table inventory has {} tables, expected {}", + pairing_table_objects.len(), + FERMION_PAIRING_TABLE_SPECS.len() + ))); + } + let mut fermion_pairing_tables = Vec::with_capacity(pairing_table_objects.len()); + let mut fermion_pairing_table_by_name = BTreeMap::new(); + for (table_object, spec) in pairing_table_objects + .into_iter() + .zip(FERMION_PAIRING_TABLE_SPECS) + { + let table_name = required_nonempty_string(&table_object, "name", "table name")?; + if table_name != spec.name { + return Err(PyValueError::new_err(format!( + "recurrence fermion-pairing table inventory mismatch: found {table_name:?}, expected {:?}", + spec.name + ))); + } + let row_count = table_object + .getattr("row_count")? + .extract::() + .map_err(|_| { + PyTypeError::new_err(format!( + "recurrence fermion-pairing table {table_name:?} row_count must be u64" + )) + })?; + let column_objects = iterable_attribute( + &table_object, + "columns", + &format!("recurrence fermion-pairing table {table_name:?} columns"), + )?; + if column_objects.len() != spec.columns.len() { + return Err(PyValueError::new_err(format!( + "recurrence fermion-pairing table {table_name:?} has {} columns, expected {}", + column_objects.len(), + spec.columns.len() + ))); + } + + let mut columns = Vec::with_capacity(spec.columns.len()); + let mut column_by_name = BTreeMap::new(); + for (column_object, column_spec) in column_objects.into_iter().zip(spec.columns) { + let column_name = required_nonempty_string(&column_object, "name", "column name")?; + if column_name != column_spec.name { + return Err(PyValueError::new_err(format!( + "recurrence fermion-pairing table {table_name:?} column mismatch: found {column_name:?}, expected {:?}", + column_spec.name + ))); + } + let context = format!("fermion_pairing.{table_name}.{column_name}"); + let values_object = column_object.getattr("values")?; + let dtype = values_object + .getattr("dtype")? + .getattr("str")? + .extract::()?; + if dtype != column_spec.kind.dtype() { + return Err(PyValueError::new_err(format!( + "{context} has dtype {dtype:?}, expected {:?}", + column_spec.kind.dtype() + ))); + } + let flags = values_object.getattr("flags")?; + if flags.getattr("writeable")?.extract::()? { + return Err(PyValueError::new_err(format!( + "{context} must be read-only" + ))); + } + if !flags.getattr("owndata")?.extract::()? { + return Err(PyValueError::new_err(format!( + "{context} must own its storage" + ))); + } + let (values, shape) = extract_owned_values(&values_object, column_spec.kind, &context)?; + if shape.first().copied() != Some(row_count) { + return Err(PyValueError::new_err(format!( + "{context} first dimension does not match row_count {row_count}" + ))); + } + let actual_tail = shape + .iter() + .skip(1) + .map(|value| usize::try_from(*value)) + .collect::, _>>() + .map_err(|_| PyValueError::new_err(format!("{context} shape exceeds usize")))?; + if actual_tail != column_spec.tail_shape { + return Err(PyValueError::new_err(format!( + "{context} has tail shape {actual_tail:?}, expected {:?}", + column_spec.tail_shape + ))); + } + column_by_name.insert(column_name.clone(), columns.len()); + columns.push(OwnedColumn { + name: column_name, + dtype: column_spec.kind.dtype(), + shape, + values, + }); + } + fermion_pairing_table_by_name.insert(table_name.clone(), fermion_pairing_tables.len()); + fermion_pairing_tables.push(OwnedTable { + name: table_name, + row_count, + columns, + column_by_name, + }); + } + let declared_fermion_pairing_digest = if fermion_pairing_tables.is_empty() { + None + } else { + if !input.hasattr("fermion_pairing_digest")? { + return Err(PyValueError::new_err( + "recurrence fermion-pairing tables require a canonical digest", + )); + } + let value = input.getattr("fermion_pairing_digest")?; + if value.is_none() { + return Err(PyValueError::new_err( + "recurrence fermion-pairing tables require a canonical digest", + )); + } + let digest = value.extract::().map_err(|_| { + PyTypeError::new_err("recurrence fermion-pairing digest must be a string") + })?; + validate_sha256_text(&digest, "recurrence fermion-pairing digest")?; + Some(digest) + }; + + Ok(OwnedInput { + abi, + declared_digest, + tables, + table_by_name, + declared_fermion_pairing_digest, + fermion_pairing_tables, + fermion_pairing_table_by_name, + }) +} + +fn parse_prepared_template_input(input: &Bound<'_, PyAny>) -> PyResult { + if cfg!(not(target_endian = "little")) { + return Err(PyValueError::new_err( + "recurrence template input v1 requires a little-endian target", + )); + } + let abi = required_string(input, "abi")?; + if abi != template::RECURRENCE_TEMPLATE_INPUT_ABI { + return Err(PyValueError::new_err(format!( + "unsupported recurrence template input ABI {abi:?}" + ))); + } + let declared_digest = required_string(input, "digest")?; + validate_sha256_text(&declared_digest, "recurrence template input digest")?; + let canonical_digest_property = required_string(input, "canonical_digest")?; + validate_sha256_text( + &canonical_digest_property, + "recurrence template canonical digest", + )?; + let catalog_digest = semantic_digest_python_attribute(input, "catalog_digest")?; + let compiled_model_digest = semantic_digest_python_attribute(input, "compiled_model_digest")?; + let prepared_kernel_pack_digest = + semantic_digest_python_attribute(input, "prepared_kernel_pack_digest")?; + + let table_objects = iterable_attribute(input, "tables", "recurrence template tables")?; + if table_objects.len() != TEMPLATE_TABLE_INVENTORY.len() { + return Err(PyValueError::new_err(format!( + "recurrence template table inventory has {} tables, expected {}", + table_objects.len(), + TEMPLATE_TABLE_INVENTORY.len() + ))); + } + + let mut tables = Vec::with_capacity(TEMPLATE_TABLE_INVENTORY.len()); + let mut table_by_name = BTreeMap::new(); + for (table_object, (expected_name, expected_column_count)) in + table_objects.into_iter().zip(TEMPLATE_TABLE_INVENTORY) + { + let table_name = required_nonempty_string(&table_object, "name", "table name")?; + if table_name != *expected_name { + return Err(PyValueError::new_err(format!( + "recurrence template table inventory mismatch: found {table_name:?}, expected {expected_name:?}" + ))); + } + let row_count = table_object + .getattr("row_count")? + .extract::() + .map_err(|_| { + PyTypeError::new_err(format!( + "recurrence template table {table_name:?} row_count must be u64" + )) + })?; + let column_objects = iterable_attribute( + &table_object, + "columns", + &format!("recurrence template table {table_name:?} columns"), + )?; + if column_objects.len() != *expected_column_count { + return Err(PyValueError::new_err(format!( + "recurrence template table {table_name:?} has {} columns, expected {expected_column_count}", + column_objects.len() + ))); + } + + let mut columns = Vec::with_capacity(*expected_column_count); + let mut column_by_name = BTreeMap::new(); + for column_object in column_objects { + let column_name = required_nonempty_string(&column_object, "name", "column name")?; + if column_by_name.contains_key(&column_name) { + return Err(PyValueError::new_err(format!( + "recurrence template table {table_name:?} repeats column {column_name:?}" + ))); + } + let context = format!("{table_name}.{column_name}"); + let values_object = column_object.getattr("values")?; + let dtype = values_object + .getattr("dtype")? + .getattr("str")? + .extract::()?; + let kind = PrimitiveKind::from_dtype(&dtype).ok_or_else(|| { + PyValueError::new_err(format!( + "{context} has unsupported recurrence-template dtype {dtype:?}" + )) + })?; + let flags = values_object.getattr("flags")?; + if flags.getattr("writeable")?.extract::()? { + return Err(PyValueError::new_err(format!( + "{context} must be read-only" + ))); + } + if !flags.getattr("owndata")?.extract::()? { + return Err(PyValueError::new_err(format!( + "{context} must own its storage" + ))); + } + let (values, shape) = extract_owned_values(&values_object, kind, &context)?; + if shape.first().copied() != Some(row_count) { + return Err(PyValueError::new_err(format!( + "{context} first dimension does not match row_count {row_count}" + ))); + } + column_by_name.insert(column_name.clone(), columns.len()); + columns.push(OwnedColumn { + name: column_name, + dtype: kind.dtype(), + shape, + values, + }); + } + table_by_name.insert(table_name.clone(), tables.len()); + tables.push(OwnedTable { + name: table_name, + row_count, + columns, + column_by_name, + }); + } + + Ok(PreparedTemplateInput { + input: OwnedInput { + abi, + declared_digest, + tables, + table_by_name, + declared_fermion_pairing_digest: None, + fermion_pairing_tables: Vec::new(), + fermion_pairing_table_by_name: BTreeMap::new(), + }, + canonical_digest_property, + catalog_digest, + compiled_model_digest, + prepared_kernel_pack_digest, + }) +} + +fn extract_owned_values( + value: &Bound<'_, PyAny>, + kind: PrimitiveKind, + context: &str, +) -> PyResult<(OwnedValues, Vec)> { + macro_rules! extract { + ($ty:ty, $variant:ident) => {{ + let array = value + .extract::>() + .map_err(|_| { + PyTypeError::new_err(format!( + "{context} is not a NumPy array with dtype {:?}", + kind.dtype() + )) + })?; + if !array.is_c_contiguous() { + return Err(PyValueError::new_err(format!( + "{context} must be C-contiguous" + ))); + } + let shape = array + .shape() + .iter() + .map(|value| { + u64::try_from(*value) + .map_err(|_| PyValueError::new_err(format!("{context} shape exceeds u64"))) + }) + .collect::>>()?; + let values = array + .as_slice() + .map_err(|error| { + PyValueError::new_err(format!("{context} must be contiguous: {error}")) + })? + .to_vec(); + (OwnedValues::$variant(values), shape) + }}; + } + Ok(match kind { + PrimitiveKind::U8 => extract!(u8, U8), + PrimitiveKind::U32 => extract!(u32, U32), + PrimitiveKind::U64 => extract!(u64, U64), + PrimitiveKind::I32 => extract!(i32, I32), + }) +} + +fn decode_process_input( + input: &OwnedInput, +) -> RusticolResult { + let fermion_pairing = decode_fermion_pairing_input(input)?; + let bitset_ranges = decode_process_rows(input, "bitset_ranges", |row| { + Ok(process::ProcessBitsetRangeRow { + id: input.u32("bitset_ranges", "id")?[row], + range: CheckedTableRange { + start: input.u64("bitset_ranges", "start")?[row], + count: input.u64("bitset_ranges", "count")?[row], + }, + bit_count: input.u64("bitset_ranges", "bit_count")?[row], + }) + })?; + let coupling_limits = decode_process_rows(input, "coupling_limits", |row| { + Ok(process::ProcessCouplingLimitRow { + name_string_id: input.u32("coupling_limits", "name_string_id")?[row], + minimum: input.u32("coupling_limits", "minimum")?[row], + maximum: input.u32("coupling_limits", "maximum")?[row], + }) + })?; + let digest_values = input.u8("digest_catalog", "value")?; + let digest_catalog = decode_process_rows(input, "digest_catalog", |row| { + let start = row + .checked_mul(32) + .ok_or_else(|| invalid("process digest catalog offset exceeds usize"))?; + let value = digest_values + .get(start..start + 32) + .ok_or_else(|| invalid("process digest catalog row is truncated"))? + .try_into() + .map_err(|_| invalid("process digest catalog row must contain 32 bytes"))?; + Ok(process::ProcessDigestCatalogRow { + id: input.u32("digest_catalog", "id")?[row], + value, + }) + })?; + let exact_factors = decode_process_rows(input, "exact_factors", |row| { + Ok(process::ProcessExactFactorRow { + id: input.u32("exact_factors", "id")?[row], + real_numerator_string_id: input.u32("exact_factors", "real_numerator_string_id")?[row], + real_denominator_string_id: input.u32("exact_factors", "real_denominator_string_id")? + [row], + imag_numerator_string_id: input.u32("exact_factors", "imag_numerator_string_id")?[row], + imag_denominator_string_id: input.u32("exact_factors", "imag_denominator_string_id")? + [row], + }) + })?; + let external_legs = decode_process_rows(input, "external_legs", |row| { + Ok(process::ProcessExternalLegRow { + source_slot: input.u32("external_legs", "source_slot")?[row], + public_label: input.u32("external_legs", "public_label")?[row], + physical_pdg: input.i32("external_legs", "physical_pdg")?[row], + outgoing_pdg: input.i32("external_legs", "outgoing_pdg")?[row], + is_initial: input.u8("external_legs", "is_initial")?[row], + is_fermionic: input.u8("external_legs", "is_fermionic")?[row], + source_state_range: CheckedTableRange { + start: input.u64("external_legs", "source_state_start")?[row], + count: input.u64("external_legs", "source_state_count")?[row], + }, + momentum_mask_id: input.u32("external_legs", "momentum_mask_id")?[row], + support_mask_id: input.u32("external_legs", "support_mask_id")?[row], + }) + })?; + let header = decode_process_rows(input, "header", |row| { + Ok(process::ProcessHeaderRow { + schema_version: input.u32("header", "schema_version")?[row], + abi_string_id: input.u32("header", "abi_string_id")?[row], + process_id_string_id: input.u32("header", "process_id_string_id")?[row], + layout: input.u8("header", "layout")?[row], + selected_flow_mode: input.u8("header", "selected_flow_mode")?[row], + selected_source_mode: input.u8("header", "selected_source_mode")?[row], + external_leg_count: input.u32("header", "external_leg_count")?[row], + physical_sector_count: input.u32("header", "physical_sector_count")?[row], + public_flow_count: input.u32("header", "public_flow_count")?[row], + replay_partition_count: input.u32("header", "replay_partition_count")?[row], + coupling_limit_count: input.u32("header", "coupling_limit_count")?[row], + parameter_projection_count: input.u32("header", "parameter_projection_count")?[row], + process_support_mask_id: input.u32("header", "process_support_mask_id")?[row], + }) + })?; + let header_digests = decode_process_rows(input, "header_digests", |row| { + Ok(process::ProcessHeaderDigestRow { + role_string_id: input.u32("header_digests", "role_string_id")?[row], + digest_id: input.u32("header_digests", "digest_id")?[row], + }) + })?; + let lc_open_strings = decode_process_rows(input, "lc_open_strings", |row| { + Ok(process::ProcessLCOpenStringRow { + sector_id: input.u32("lc_open_strings", "sector_id")?[row], + ordinal: input.u32("lc_open_strings", "ordinal")?[row], + fundamental_source_slot: input.u32("lc_open_strings", "fundamental_source_slot")?[row], + antifundamental_source_slot: input + .u32("lc_open_strings", "antifundamental_source_slot")?[row], + adjoint_sequence_id: input.u32("lc_open_strings", "adjoint_sequence_id")?[row], + singlet_sequence_id: input.u32("lc_open_strings", "singlet_sequence_id")?[row], + }) + })?; + let normalization = decode_process_rows(input, "normalization", |row| { + Ok(process::ProcessNormalizationRow { + factor_id: input.u32("normalization", "factor_id")?[row], + convention_string_id: input.u32("normalization", "convention_string_id")?[row], + semantic_digest_id: input.u32("normalization", "semantic_digest_id")?[row], + }) + })?; + let parameter_projection = decode_process_rows(input, "parameter_projection", |row| { + Ok(process::ProcessParameterProjectionRow { + runtime_slot: input.u32("parameter_projection", "runtime_slot")?[row], + runtime_name_string_id: input.u32("parameter_projection", "runtime_name_string_id")? + [row], + parameter_template_id: input.u32("parameter_projection", "parameter_template_id")?[row], + prepared_parameter_id: input.u32("parameter_projection", "prepared_parameter_id")?[row], + component: input.u32("parameter_projection", "component")?[row], + }) + })?; + let physical_lc_sectors = decode_process_rows(input, "physical_lc_sectors", |row| { + Ok(process::ProcessPhysicalLCSectorRow { + sector_id: input.u32("physical_lc_sectors", "sector_id")?[row], + public_id_string_id: input.u32("physical_lc_sectors", "public_id_string_id")?[row], + kind: input.u8("physical_lc_sectors", "kind")?[row], + closure_source_slot: input.u32("physical_lc_sectors", "closure_source_slot")?[row], + closure_proof_algorithm_string_id: input + .u32("physical_lc_sectors", "closure_proof_algorithm_string_id")?[row], + closure_proof_digest_id: input.u32("physical_lc_sectors", "closure_proof_digest_id")? + [row], + open_string_range: CheckedTableRange { + start: input.u64("physical_lc_sectors", "open_string_start")?[row], + count: input.u64("physical_lc_sectors", "open_string_count")?[row], + }, + trace_sequence_id: input.u32("physical_lc_sectors", "trace_sequence_id")?[row], + singlet_sequence_id: input.u32("physical_lc_sectors", "singlet_sequence_id")?[row], + word_sequence_id: input.u32("physical_lc_sectors", "word_sequence_id")?[row], + support_mask_id: input.u32("physical_lc_sectors", "support_mask_id")?[row], + }) + })?; + let public_lc_flows = decode_process_rows(input, "public_lc_flows", |row| { + Ok(process::ProcessPublicLCFlowRow { + flow_id: input.u32("public_lc_flows", "flow_id")?[row], + public_id_string_id: input.u32("public_lc_flows", "public_id_string_id")?[row], + construction_sector_id: input.u32("public_lc_flows", "construction_sector_id")?[row], + word_sequence_id: input.u32("public_lc_flows", "word_sequence_id")?[row], + source_slot_permutation_sequence_id: input + .u32("public_lc_flows", "source_slot_permutation_sequence_id")?[row], + reduction_weight_factor_id: input + .u32("public_lc_flows", "reduction_weight_factor_id")?[row], + }) + })?; + let replay_partitions = decode_process_rows(input, "replay_partitions", |row| { + Ok(process::ProcessReplayPartitionRow { + partition_id: input.u32("replay_partitions", "partition_id")?[row], + representative_sector_id: input.u32("replay_partitions", "representative_sector_id")? + [row], + materialized_sector_id: input.u32("replay_partitions", "materialized_sector_id")?[row], + target_range: CheckedTableRange { + start: input.u64("replay_partitions", "target_start")?[row], + count: input.u64("replay_partitions", "target_count")?[row], + }, + proof_algorithm_string_id: input + .u32("replay_partitions", "proof_algorithm_string_id")?[row], + proof_digest_id: input.u32("replay_partitions", "proof_digest_id")?[row], + }) + })?; + let replay_targets = decode_process_rows(input, "replay_targets", |row| { + Ok(process::ProcessReplayTargetRow { + partition_id: input.u32("replay_targets", "partition_id")?[row], + sector_id: input.u32("replay_targets", "sector_id")?[row], + external_permutation_sequence_id: input + .u32("replay_targets", "external_permutation_sequence_id")?[row], + source_slot_permutation_sequence_id: input + .u32("replay_targets", "source_slot_permutation_sequence_id")?[row], + amplitude_phase_factor_id: input.u32("replay_targets", "amplitude_phase_factor_id")? + [row], + fermion_sign: input.i32("replay_targets", "fermion_sign")?[row], + }) + })?; + let selected_public_flow_coverage = + decode_process_rows(input, "selected_public_flow_coverage", |row| { + Ok(process::ProcessSelectedPublicFlowRow { + flow_id: input.u32("selected_public_flow_coverage", "flow_id")?[row], + }) + })?; + let selected_source_coverage = decode_process_rows(input, "selected_source_coverage", |row| { + Ok(process::ProcessSelectedSourceStateRow { + source_slot: input.u32("selected_source_coverage", "source_slot")?[row], + source_state_index: input.u32("selected_source_coverage", "source_state_index")?[row], + }) + })?; + let semantic_template_references = + decode_process_rows(input, "semantic_template_references", |row| { + Ok(process::ProcessSemanticTemplateReferenceRow { + kind_string_id: input.u32("semantic_template_references", "kind_string_id")?[row], + template_id: input.u32("semantic_template_references", "template_id")?[row], + semantic_digest_id: input + .u32("semantic_template_references", "semantic_digest_id")?[row], + prepared_kernel_id: input + .u32("semantic_template_references", "prepared_kernel_id")?[row], + }) + })?; + let source_states = decode_process_rows(input, "source_states", |row| { + Ok(process::ProcessSourceStateRow { + source_slot: input.u32("source_states", "source_slot")?[row], + state_index: input.u32("source_states", "state_index")?[row], + public_helicity: input.i32("source_states", "public_helicity")?[row], + chirality: input.i32("source_states", "chirality")?[row], + spin_state: input.i32("source_states", "spin_state")?[row], + current_state_template_id: input.u32("source_states", "current_state_template_id")? + [row], + source_template_id: input.u32("source_states", "source_template_id")?[row], + momentum_sign: input.i32("source_states", "momentum_sign")?[row], + crossing_phase_factor_id: input.u32("source_states", "crossing_phase_factor_id")?[row], + }) + })?; + + Ok(process::OwnedRecurrenceProcessInput { + input_abi: input.abi.clone(), + declared_input_digest: semantic_digest_from_hex( + &input.declared_digest, + "recurrence process input digest", + )?, + fermion_pairing, + bitset_ranges, + bitset_words: input.u64("bitset_words", "value")?.to_vec(), + coupling_limits, + digest_catalog, + exact_factors, + external_legs, + header, + header_digests, + lc_open_strings, + normalization, + parameter_projection, + physical_lc_sectors, + public_lc_flows, + replay_partitions, + replay_targets, + selected_public_flow_coverage, + selected_source_coverage, + semantic_template_references, + source_states, + string_ranges: plain_ranges(input, "string_ranges")?, + string_bytes: input.u8("string_bytes", "value")?.to_vec(), + u32_sequence_ranges: plain_ranges(input, "u32_sequence_ranges")?, + u32_sequence_values: input.u32("u32_sequence_values", "value")?.to_vec(), + }) +} + +fn decode_fermion_pairing_input( + input: &OwnedInput, +) -> RusticolResult> { + let Some(declared_digest) = input.declared_fermion_pairing_digest.as_deref() else { + if !input.fermion_pairing_tables.is_empty() { + return Err(invalid( + "recurrence fermion-pairing tables have no declared digest", + )); + } + return Ok(None); + }; + if input.fermion_pairing_tables.is_empty() { + return Err(invalid( + "recurrence fermion-pairing digest has no fixed-width tables", + )); + } + + let header = decode_pairing_rows(input, "header", |row| { + Ok(process::FermionPairingHeaderRow { + schema_version: input.pairing_u32("header", "schema_version")?[row], + abi_string_id: input.pairing_u32("header", "abi_string_id")?[row], + process_key_string_id: input.pairing_u32("header", "process_key_string_id")?[row], + proof_algorithm_string_id: input.pairing_u32("header", "proof_algorithm_string_id")? + [row], + source_count: input.pairing_u32("header", "source_count")?[row], + endpoint_count: input.pairing_u32("header", "endpoint_count")?[row], + pairing_class_count: input.pairing_u32("header", "pairing_class_count")?[row], + rule_count: input.pairing_u32("header", "rule_count")?[row], + endpoint_state_template_count: input + .pairing_u64("header", "endpoint_state_template_count")?[row], + endpoint_anti_state_template_count: input + .pairing_u64("header", "endpoint_anti_state_template_count")?[row], + endpoint_basis_count: input.pairing_u64("header", "endpoint_basis_count")?[row], + endpoint_color_representation_count: input + .pairing_u64("header", "endpoint_color_representation_count")?[row], + class_fundamental_slot_count: input + .pairing_u64("header", "class_fundamental_slot_count")?[row], + class_antifundamental_slot_count: input + .pairing_u64("header", "class_antifundamental_slot_count")?[row], + class_reference_pairing_count: input + .pairing_u64("header", "class_reference_pairing_count")?[row], + rule_class_pairing_index_count: input + .pairing_u64("header", "rule_class_pairing_index_count")?[row], + rule_endpoint_pairing_count: input + .pairing_u64("header", "rule_endpoint_pairing_count")?[row], + rule_source_permutation_count: input + .pairing_u64("header", "rule_source_permutation_count")?[row], + rule_lineage_count: input.pairing_u64("header", "rule_lineage_count")?[row], + exact_integer_count: input.pairing_u32("header", "exact_integer_count")?[row], + exact_integer_limb_count: input.pairing_u64("header", "exact_integer_limb_count")?[row], + string_count: input.pairing_u32("header", "string_count")?[row], + string_byte_count: input.pairing_u64("header", "string_byte_count")?[row], + no_fermion_line: input.pairing_u32("header", "no_fermion_line")?[row], + topology_digest: pairing_digest_row(input, "header", "topology_digest", row)?, + semantic_digest: pairing_digest_row(input, "header", "semantic_digest", row)?, + }) + })?; + let endpoints = decode_pairing_rows(input, "endpoints", |row| { + Ok(process::FermionPairingEndpointRow { + endpoint_id: input.pairing_u32("endpoints", "endpoint_id")?[row], + source_slot: input.pairing_u32("endpoints", "source_slot")?[row], + public_label: input.pairing_u32("endpoints", "public_label")?[row], + species_class_id: input.pairing_u32("endpoints", "species_class_id")?[row], + species_string_id: input.pairing_u32("endpoints", "species_string_id")?[row], + particle_orientation: input.pairing_u8("endpoints", "particle_orientation")?[row], + color_orientation: input.pairing_u8("endpoints", "color_orientation")?[row], + state_template_range: pairing_range(input, "endpoints", "state_template", row)?, + anti_state_template_range: pairing_range( + input, + "endpoints", + "anti_state_template", + row, + )?, + basis_range: pairing_range(input, "endpoints", "basis", row)?, + color_representation_range: pairing_range( + input, + "endpoints", + "color_representation", + row, + )?, + contract_digest: pairing_digest_row(input, "endpoints", "contract_digest", row)?, + }) + })?; + let pairing_classes = decode_pairing_rows(input, "pairing_classes", |row| { + Ok(process::FermionPairingClassRow { + class_id: input.pairing_u32("pairing_classes", "class_id")?[row], + species_class_id: input.pairing_u32("pairing_classes", "species_class_id")?[row], + species_string_id: input.pairing_u32("pairing_classes", "species_string_id")?[row], + fundamental_slot_range: pairing_range( + input, + "pairing_classes", + "fundamental_slot", + row, + )?, + antifundamental_slot_range: pairing_range( + input, + "pairing_classes", + "antifundamental_slot", + row, + )?, + reference_pairing_range: pairing_range( + input, + "pairing_classes", + "reference_pairing", + row, + )?, + pairing_count: input.pairing_u64("pairing_classes", "pairing_count")?[row], + proof_digest: pairing_digest_row(input, "pairing_classes", "proof_digest", row)?, + }) + })?; + let rules = decode_pairing_rows(input, "rules", |row| { + Ok(process::FermionPairingRuleRow { + rule_id: input.pairing_u32("rules", "rule_id")?[row], + class_pairing_index_range: pairing_range(input, "rules", "class_pairing_index", row)?, + endpoint_pairing_range: pairing_range(input, "rules", "endpoint_pairing", row)?, + source_permutation_range: pairing_range(input, "rules", "source_permutation", row)?, + lineage_range: pairing_range(input, "rules", "lineage", row)?, + fermion_parity: input.pairing_i32("rules", "fermion_parity")?[row], + real_numerator_integer_id: input.pairing_u32("rules", "real_numerator_integer_id")? + [row], + real_denominator_integer_id: input + .pairing_u32("rules", "real_denominator_integer_id")?[row], + imag_numerator_integer_id: input.pairing_u32("rules", "imag_numerator_integer_id")? + [row], + imag_denominator_integer_id: input + .pairing_u32("rules", "imag_denominator_integer_id")?[row], + multiplicity: input.pairing_u64("rules", "multiplicity")?[row], + proof_algorithm_string_id: input.pairing_u32("rules", "proof_algorithm_string_id")? + [row], + proof_digest: pairing_digest_row(input, "rules", "proof_digest", row)?, + }) + })?; + let rule_class_pairing_indices = + decode_pairing_rows(input, "rule_class_pairing_indices", |row| { + Ok(process::FermionPairingClassPairingIndexRow { + class_id: input.pairing_u32("rule_class_pairing_indices", "class_id")?[row], + pairing_index: input.pairing_u64("rule_class_pairing_indices", "pairing_index")? + [row], + }) + })?; + let decode_pairs = |table_name: &str| { + decode_pairing_rows(input, table_name, |row| { + Ok(process::FermionPairingEndpointPairRow { + fundamental_source_slot: input + .pairing_u32(table_name, "fundamental_source_slot")?[row], + antifundamental_source_slot: input + .pairing_u32(table_name, "antifundamental_source_slot")?[row], + }) + }) + }; + let exact_integers = decode_pairing_rows(input, "exact_integers", |row| { + Ok(process::FermionPairingExactIntegerRow { + integer_id: input.pairing_u32("exact_integers", "integer_id")?[row], + sign: input.pairing_i32("exact_integers", "sign")?[row], + limb_range: pairing_range(input, "exact_integers", "limb", row)?, + }) + })?; + + Ok(Some(process::OwnedFermionPairingInput { + input_abi: process::RECURRENCE_FERMION_PAIRING_COLUMNAR_ABI.to_owned(), + declared_columnar_digest: semantic_digest_from_hex( + declared_digest, + "recurrence fermion-pairing digest", + )?, + header, + endpoints, + endpoint_state_template_ids: input + .pairing_u32("endpoint_state_template_ids", "string_id")? + .to_vec(), + endpoint_anti_state_template_ids: input + .pairing_u32("endpoint_anti_state_template_ids", "string_id")? + .to_vec(), + endpoint_basis_ids: input + .pairing_u32("endpoint_basis_ids", "string_id")? + .to_vec(), + endpoint_color_representations: input + .pairing_i32("endpoint_color_representations", "value")? + .to_vec(), + pairing_classes, + class_fundamental_slots: input + .pairing_u32("class_fundamental_slots", "source_slot")? + .to_vec(), + class_antifundamental_slots: input + .pairing_u32("class_antifundamental_slots", "source_slot")? + .to_vec(), + class_reference_pairings: decode_pairs("class_reference_pairings")?, + rules, + rule_class_pairing_indices, + rule_endpoint_pairings: decode_pairs("rule_endpoint_pairings")?, + rule_source_slot_permutations: input + .pairing_u32("rule_source_slot_permutations", "source_slot")? + .to_vec(), + rule_lineages: input.pairing_u32("rule_lineages", "line_id")?.to_vec(), + exact_integers, + exact_integer_limbs: input.pairing_u64("exact_integer_limbs", "value")?.to_vec(), + string_ranges: pairing_plain_ranges(input, "string_ranges")?, + string_bytes: input.pairing_u8("string_bytes", "value")?.to_vec(), + })) +} + +fn decode_pairing_rows( + input: &OwnedInput, + table_name: &str, + mut decode: impl FnMut(usize) -> RusticolResult, +) -> RusticolResult> { + let row_count = checked_usize( + input.fermion_pairing_table(table_name)?.row_count, + &format!("fermion-pairing {table_name} row count"), + )?; + (0..row_count).map(&mut decode).collect() +} + +fn pairing_range( + input: &OwnedInput, + table_name: &str, + prefix: &str, + row: usize, +) -> RusticolResult { + Ok(CheckedTableRange { + start: input.pairing_u64(table_name, &format!("{prefix}_start"))?[row], + count: input.pairing_u64(table_name, &format!("{prefix}_count"))?[row], + }) +} + +fn pairing_plain_ranges( + input: &OwnedInput, + table_name: &str, +) -> RusticolResult> { + decode_pairing_rows(input, table_name, |row| { + Ok(CheckedTableRange { + start: input.pairing_u64(table_name, "start")?[row], + count: input.pairing_u64(table_name, "count")?[row], + }) + }) +} + +fn pairing_digest_row( + input: &OwnedInput, + table_name: &str, + column_name: &str, + row: usize, +) -> RusticolResult<[u8; 32]> { + let values = input.pairing_u8(table_name, column_name)?; + let start = row + .checked_mul(32) + .ok_or_else(|| invalid("fermion-pairing digest offset exceeds usize"))?; + values + .get(start..start + 32) + .ok_or_else(|| invalid("fermion-pairing digest row is truncated"))? + .try_into() + .map_err(|_| invalid("fermion-pairing digest row must contain 32 bytes")) +} + +fn decode_process_rows( + input: &OwnedInput, + table_name: &str, + mut decode: impl FnMut(usize) -> RusticolResult, +) -> RusticolResult> { + let row_count = checked_usize( + input.table(table_name)?.row_count, + &format!("{table_name} row count"), + )?; + (0..row_count).map(&mut decode).collect() +} + +fn plain_ranges(input: &OwnedInput, table_name: &str) -> RusticolResult> { + decode_process_rows(input, table_name, |row| { + Ok(CheckedTableRange { + start: input.u64(table_name, "start")?[row], + count: input.u64(table_name, "count")?[row], + }) + }) +} + +fn decode_template_input( + input: &OwnedInput, + catalog_digest: SemanticDigest, + compiled_model_digest: SemanticDigest, + prepared_kernel_pack_digest: SemanticDigest, +) -> RusticolResult { + use template::{ + CatalogHeaderRow, ClosureRow, ColorContractionRow, ColorNcTermRow, + ContactOrbitCertificateRow, ContactOrbitStepRow, CouplingOrderTermRow, CurrentStateRow, + EvaluatorBindingRow, ExactFactorRow, LCColorTransitionWitnessRow, + OwnedRecurrenceTemplateInput, ParameterRow, PropagatorRow, QuantumFlowRow, + QuantumNumberFlowTermRow, RuntimeHelicityContractRow, RuntimeHelicityEmbeddingRow, + RuntimeHelicityProjectionRow, RuntimeHelicityVariantRow, SourceRow, SymmetryProofRow, + TransitionRow, + }; + let catalog_header = decode_template_rows(input.table("catalog_header")?, |table, row| { + Ok(CatalogHeaderRow { + schema_version: table.u32("schema_version")?[row], + abi_string_id: table.u32("abi_string_id")?[row], + canonicalization_abi_string_id: table.u32("canonicalization_abi_string_id")?[row], + exact_scalar_abi_string_id: table.u32("exact_scalar_abi_string_id")?[row], + compiled_model_digest_id: table.u32("compiled_model_digest_id")?[row], + prepared_kernel_pack_digest_id: table.u32("prepared_kernel_pack_digest_id")?[row], + catalog_digest_id: table.u32("catalog_digest_id")?[row], + parameter_count: table.u32("parameter_count")?[row], + current_state_count: table.u32("current_state_count")?[row], + source_count: table.u32("source_count")?[row], + quantum_flow_count: table.u32("quantum_flow_count")?[row], + contact_orbit_certificate_count: table.u32("contact_orbit_certificate_count")?[row], + contact_orbit_step_count: table.u32("contact_orbit_step_count")?[row], + transition_count: table.u32("transition_count")?[row], + propagator_count: table.u32("propagator_count")?[row], + closure_count: table.u32("closure_count")?[row], + color_contraction_count: table.u32("color_contraction_count")?[row], + symmetry_proof_count: table.u32("symmetry_proof_count")?[row], + runtime_helicity_contract_count: table.u32("runtime_helicity_contract_count")?[row], + evaluator_binding_count: table.u32("evaluator_binding_count")?[row], + }) + })?; + let coupling_order_ranges = template_indexed_ranges(input.table("coupling_order_ranges")?)?; + let coupling_order_terms = + decode_template_rows(input.table("coupling_order_terms")?, |table, row| { + Ok(CouplingOrderTermRow { + set_id: table.u32("set_id")?[row], + name_string_id: table.u32("name_string_id")?[row], + power: table.u32("power")?[row], + }) + })?; + let contact_orbit_certificates = + decode_template_rows(input.table("contact_orbit_certificates")?, |table, row| { + Ok(ContactOrbitCertificateRow { + id: table.u32("id")?[row], + template_string_id: table.u32("template_string_id")?[row], + algorithm_string_id: table.u32("algorithm_string_id")?[row], + algorithm_version: table.u32("algorithm_version")?[row], + term_id: table.u32("term_id")?[row], + vertex_string_id: table.u32("vertex_string_id")?[row], + particle_string_sequence_id: table.u32("particle_string_sequence_id")?[row], + evaluator_class_string_id: table.u32("evaluator_class_string_id")?[row], + physical_leg_equivalence_sequence_id: table + .u32("physical_leg_equivalence_sequence_id")?[row], + reconstruction_factor_id: table.u32("reconstruction_factor_id")?[row], + semantic_digest_id: table.u32("semantic_digest_id")?[row], + }) + })?; + let contact_orbit_steps = + decode_template_rows(input.table("contact_orbit_steps")?, |table, row| { + Ok(ContactOrbitStepRow { + id: table.u32("id")?[row], + template_string_id: table.u32("template_string_id")?[row], + certificate_id: table.u32("certificate_id")?[row], + stage: table.u8("stage")?[row], + result_leg: table.u8("result_leg")?[row], + left_covered_leg_sequence_id: table.u32("left_covered_leg_sequence_id")?[row], + right_covered_leg_sequence_id: table.u32("right_covered_leg_sequence_id")?[row], + source_particle_leg_sequence_id: table.u32("source_particle_leg_sequence_id")?[row], + reconstruction_factor_id: table.u32("reconstruction_factor_id")?[row], + semantic_digest_id: table.u32("semantic_digest_id")?[row], + }) + })?; + let current_states = decode_template_rows(input.table("current_states")?, |table, row| { + Ok(CurrentStateRow { + id: table.u32("id")?[row], + template_string_id: table.u32("template_string_id")?[row], + particle_id: table.i32("particle_id")?[row], + anti_particle_id: table.i32("anti_particle_id")?[row], + species_string_id: table.u32("species_string_id")?[row], + orientation: table.u8("orientation")?[row], + statistics: table.u8("statistics")?[row], + color_representation: table.i32("color_representation")?[row], + basis_string_id: table.u32("basis_string_id")?[row], + tensor_ordering_sequence_id: table.u32("tensor_ordering_sequence_id")?[row], + dimension: table.u32("dimension")?[row], + chirality: table.i32("chirality")?[row], + lc_color_shape_string_id: table.u32("lc_color_shape_string_id")?[row], + auxiliary_kind_string_id: table.u32("auxiliary_kind_string_id")?[row], + mass_parameter_id: table.u32("mass_parameter_id")?[row], + width_parameter_id: table.u32("width_parameter_id")?[row], + semantic_digest_id: table.u32("semantic_digest_id")?[row], + }) + })?; + let digest_catalog = decode_template_digest_catalog(input.table("digest_catalog")?)?; + let evaluator_bindings = + decode_template_rows(input.table("evaluator_bindings")?, |table, row| { + Ok(EvaluatorBindingRow { + id: table.u32("id")?[row], + resolver_key_string_id: table.u32("resolver_key_string_id")?[row], + prepared_kernel_id: table.u32("prepared_kernel_id")?[row], + contract_kind: table.u8("contract_kind")?[row], + callable_signature_digest_id: table.u32("callable_signature_digest_id")?[row], + input_state_sequence_id: table.u32("input_state_sequence_id")?[row], + output_state_template_id: table.u32("output_state_template_id")?[row], + input_layout_sequence_id: table.u32("input_layout_sequence_id")?[row], + output_layout_sequence_id: table.u32("output_layout_sequence_id")?[row], + exact_expression_digest_sequence_id: table + .u32("exact_expression_digest_sequence_id")?[row], + semantic_template_sequence_id: table.u32("semantic_template_sequence_id")?[row], + callable_kind: table.u8("callable_kind")?[row], + runtime_template_string_id: table.u32("runtime_template_string_id")?[row], + semantic_digest_id: table.u32("semantic_digest_id")?[row], + }) + })?; + let exact_factors = decode_template_rows(input.table("exact_factors")?, |table, row| { + Ok(ExactFactorRow { + id: table.u32("id")?[row], + real_numerator_string_id: table.u32("real_numerator_string_id")?[row], + real_denominator_string_id: table.u32("real_denominator_string_id")?[row], + imag_numerator_string_id: table.u32("imag_numerator_string_id")?[row], + imag_denominator_string_id: table.u32("imag_denominator_string_id")?[row], + }) + })?; + let flavour_flow_ranges = template_indexed_ranges(input.table("flavour_flow_ranges")?)?; + let flavour_flow_values = input.table("flavour_flow_values")?.i32("value")?.to_vec(); + let i32_sequence_ranges = template_indexed_ranges(input.table("i32_sequence_ranges")?)?; + let i32_sequence_values = input.table("i32_sequence_values")?.i32("value")?.to_vec(); + let parameters = decode_template_rows(input.table("parameters")?, |table, row| { + Ok(ParameterRow { + id: table.u32("id")?[row], + template_string_id: table.u32("template_string_id")?[row], + name_string_id: table.u32("name_string_id")?[row], + kind: table.u8("kind")?[row], + value_type: table.u8("value_type")?[row], + mutable: table.u8("mutable")?[row], + default_factor_id: table.u32("default_factor_id")?[row], + exact_expression_digest_id: table.u32("exact_expression_digest_id")?[row], + dependency_sequence_id: table.u32("dependency_sequence_id")?[row], + prepared_parameter_id: table.u32("prepared_parameter_id")?[row], + semantic_digest_id: table.u32("semantic_digest_id")?[row], + }) + })?; + let propagators = decode_template_rows(input.table("propagators")?, |table, row| { + Ok(PropagatorRow { + id: table.u32("id")?[row], + template_string_id: table.u32("template_string_id")?[row], + state_template_id: table.u32("state_template_id")?[row], + applies_propagator: table.u8("applies_propagator")?[row], + evaluator_binding_id: table.u32("evaluator_binding_id")?[row], + numerator_expression_digest_id: table.u32("numerator_expression_digest_id")?[row], + denominator_expression_digest_id: table.u32("denominator_expression_digest_id")?[row], + mass_parameter_id: table.u32("mass_parameter_id")?[row], + width_parameter_id: table.u32("width_parameter_id")?[row], + gauge_string_id: table.u32("gauge_string_id")?[row], + linearity_proof_template_id: table.u32("linearity_proof_template_id")?[row], + semantic_digest_id: table.u32("semantic_digest_id")?[row], + }) + })?; + let quantum_flows = decode_template_rows(input.table("quantum_flows")?, |table, row| { + Ok(QuantumFlowRow { + id: table.u32("id")?[row], + template_string_id: table.u32("template_string_id")?[row], + input_state_sequence_id: table.u32("input_state_sequence_id")?[row], + input_spin_sequence_id: table.u32("input_spin_sequence_id")?[row], + input_flavour_sequence_id: table.u32("input_flavour_sequence_id")?[row], + input_quantum_sequence_id: table.u32("input_quantum_sequence_id")?[row], + flavour_flow_operation_string_id: table.u32("flavour_flow_operation_string_id")?[row], + quantum_number_flow_operation_string_id: table + .u32("quantum_number_flow_operation_string_id")?[row], + coupling_order_set_id: table.u32("coupling_order_set_id")?[row], + result_state_template_id: table.u32("result_state_template_id")?[row], + result_spin_state: table.i32("result_spin_state")?[row], + result_flavour_flow_id: table.u32("result_flavour_flow_id")?[row], + result_quantum_number_flow_id: table.u32("result_quantum_number_flow_id")?[row], + exact_coupling_factor_id: table.u32("exact_coupling_factor_id")?[row], + predicate_digest_id: table.u32("predicate_digest_id")?[row], + semantic_digest_id: table.u32("semantic_digest_id")?[row], + }) + })?; + let quantum_number_flow_ranges = + template_indexed_ranges(input.table("quantum_number_flow_ranges")?)?; + let quantum_number_flow_terms = + decode_template_rows(input.table("quantum_number_flow_terms")?, |table, row| { + Ok(QuantumNumberFlowTermRow { + flow_id: table.u32("flow_id")?[row], + name_string_id: table.u32("name_string_id")?[row], + expression_string_id: table.u32("expression_string_id")?[row], + }) + })?; + let runtime_helicity_contracts = + decode_template_rows(input.table("runtime_helicity_contracts")?, |table, row| { + Ok(RuntimeHelicityContractRow { + id: table.u32("id")?[row], + template_string_id: table.u32("template_string_id")?[row], + full_state_template_id: table.u32("full_state_template_id")?[row], + variant_range: CheckedTableRange::new( + table.u32("variant_offset")?[row].into(), + table.u32("variant_count")?[row].into(), + ), + proof_algorithm_string_id: table.u32("proof_algorithm_string_id")?[row], + proof_digest_id: table.u32("proof_digest_id")?[row], + semantic_digest_id: table.u32("semantic_digest_id")?[row], + }) + })?; + let runtime_helicity_embeddings = + decode_template_rows(input.table("runtime_helicity_embeddings")?, |table, row| { + Ok(RuntimeHelicityEmbeddingRow { + variant_id: table.u32("variant_id")?[row], + full_component: table.u32("full_component")?[row], + source_component: table.u32("source_component")?[row], + factor_id: table.u32("factor_id")?[row], + }) + })?; + let runtime_helicity_projections = decode_template_rows( + input.table("runtime_helicity_projections")?, + |table, row| { + Ok(RuntimeHelicityProjectionRow { + variant_id: table.u32("variant_id")?[row], + source_component: table.u32("source_component")?[row], + full_component: table.u32("full_component")?[row], + }) + }, + )?; + let runtime_helicity_variants = + decode_template_rows(input.table("runtime_helicity_variants")?, |table, row| { + Ok(RuntimeHelicityVariantRow { + id: table.u32("id")?[row], + contract_id: table.u32("contract_id")?[row], + source_template_id: table.u32("source_template_id")?[row], + source_state_template_id: table.u32("source_state_template_id")?[row], + embedding_range: CheckedTableRange::new( + table.u32("embedding_offset")?[row].into(), + table.u32("embedding_count")?[row].into(), + ), + projection_range: CheckedTableRange::new( + table.u32("projection_offset")?[row].into(), + table.u32("projection_count")?[row].into(), + ), + proof_digest_id: table.u32("proof_digest_id")?[row], + }) + })?; + let sources = decode_template_rows(input.table("sources")?, |table, row| { + Ok(SourceRow { + id: table.u32("id")?[row], + template_string_id: table.u32("template_string_id")?[row], + state_template_id: table.u32("state_template_id")?[row], + crossing_string_id: table.u32("crossing_string_id")?[row], + wavefunction_family_string_id: table.u32("wavefunction_family_string_id")?[row], + helicity: table.i32("helicity")?[row], + spin_state: table.i32("spin_state")?[row], + flavour_flow_id: table.u32("flavour_flow_id")?[row], + quantum_number_flow_id: table.u32("quantum_number_flow_id")?[row], + lc_color_seed_operation: table.u8("lc_color_seed_operation")?[row], + lc_color_seed_shape_string_id: table.u32("lc_color_seed_shape_string_id")?[row], + lc_color_seed_component_kind: table.u8("lc_color_seed_component_kind")?[row], + lc_color_seed_component_role: table.u8("lc_color_seed_component_role")?[row], + lc_color_seed_proof_digest_id: table.u32("lc_color_seed_proof_digest_id")?[row], + lc_color_seed_provenance_sequence_id: table + .u32("lc_color_seed_provenance_sequence_id")?[row], + wavefunction_expression_digest_id: table.u32("wavefunction_expression_digest_id")?[row], + evaluator_binding_id: table.u32("evaluator_binding_id")?[row], + mass_parameter_id: table.u32("mass_parameter_id")?[row], + width_parameter_id: table.u32("width_parameter_id")?[row], + semantic_digest_id: table.u32("semantic_digest_id")?[row], + }) + })?; + let string_ranges = template_plain_ranges(input.table("string_ranges")?)?; + let string_bytes = input.table("string_bytes")?.u8("value")?.to_vec(); + let symmetry_proofs = decode_template_rows(input.table("symmetry_proofs")?, |table, row| { + Ok(SymmetryProofRow { + id: table.u32("id")?[row], + template_string_id: table.u32("template_string_id")?[row], + proof_algorithm_string_id: table.u32("proof_algorithm_string_id")?[row], + subject_template_sequence_id: table.u32("subject_template_sequence_id")?[row], + input_permutation_sequence_id: table.u32("input_permutation_sequence_id")?[row], + exact_phase_factor_id: table.u32("exact_phase_factor_id")?[row], + expression_digest_sequence_id: table.u32("expression_digest_sequence_id")?[row], + witness_digest_id: table.u32("witness_digest_id")?[row], + semantic_digest_id: table.u32("semantic_digest_id")?[row], + }) + })?; + let transitions = decode_template_rows(input.table("transitions")?, |table, row| { + Ok(TransitionRow { + id: table.u32("id")?[row], + template_string_id: table.u32("template_string_id")?[row], + input_state_sequence_id: table.u32("input_state_sequence_id")?[row], + result_state_template_id: table.u32("result_state_template_id")?[row], + quantum_flow_template_id: table.u32("quantum_flow_template_id")?[row], + evaluator_binding_id: table.u32("evaluator_binding_id")?[row], + canonical_input_order_sequence_id: table.u32("canonical_input_order_sequence_id")?[row], + momentum_convention_sequence_id: table.u32("momentum_convention_sequence_id")?[row], + coupling_parameter_sequence_id: table.u32("coupling_parameter_sequence_id")?[row], + coupling_order_set_id: table.u32("coupling_order_set_id")?[row], + color_contraction_template_id: table.u32("color_contraction_template_id")?[row], + binding_coupling_factor_id: table.u32("binding_coupling_factor_id")?[row], + exact_factor_id: table.u32("exact_factor_id")?[row], + output_factor_source: table.u8("output_factor_source")?[row], + equivalence_class_string_id: table.u32("equivalence_class_string_id")?[row], + input_exchange_factor_id: table.u32("input_exchange_factor_id")?[row], + output_projection_string_id: table.u32("output_projection_string_id")?[row], + contact_orbit_step_sequence_id: table.u32("contact_orbit_step_sequence_id")?[row], + contact_orbit_step_semantic_digest_sequence_id: table + .u32("contact_orbit_step_semantic_digest_sequence_id")?[row], + semantic_digest_id: table.u32("semantic_digest_id")?[row], + }) + })?; + let closures = decode_template_rows(input.table("closures")?, |table, row| { + Ok(ClosureRow { + id: table.u32("id")?[row], + template_string_id: table.u32("template_string_id")?[row], + input_state_sequence_id: table.u32("input_state_sequence_id")?[row], + result_state_template_id: table.u32("result_state_template_id")?[row], + evaluator_binding_id: table.u32("evaluator_binding_id")?[row], + canonical_input_order_sequence_id: table.u32("canonical_input_order_sequence_id")?[row], + coupling_parameter_sequence_id: table.u32("coupling_parameter_sequence_id")?[row], + coupling_order_set_id: table.u32("coupling_order_set_id")?[row], + eligible_quantum_flow_sequence_id: table.u32("eligible_quantum_flow_sequence_id")?[row], + color_contraction_template_id: table.u32("color_contraction_template_id")?[row], + binding_coupling_factor_id: table.u32("binding_coupling_factor_id")?[row], + exact_factor_id: table.u32("exact_factor_id")?[row], + output_factor_source: table.u8("output_factor_source")?[row], + equivalence_class_string_id: table.u32("equivalence_class_string_id")?[row], + input_exchange_factor_id: table.u32("input_exchange_factor_id")?[row], + projection_string_id: table.u32("projection_string_id")?[row], + component_coefficient_sequence_id: table.u32("component_coefficient_sequence_id")?[row], + chirality_relation_string_id: table.u32("chirality_relation_string_id")?[row], + metric_signature_string_id: table.u32("metric_signature_string_id")?[row], + semantic_digest_id: table.u32("semantic_digest_id")?[row], + }) + })?; + let color_contractions = + decode_template_rows(input.table("color_contractions")?, |table, row| { + Ok(ColorContractionRow { + id: table.u32("id")?[row], + template_string_id: table.u32("template_string_id")?[row], + rule_kind_string_id: table.u32("rule_kind_string_id")?[row], + input_representation_sequence_id: table.u32("input_representation_sequence_id")? + [row], + has_output_representation: table.u8("has_output_representation")?[row], + output_representation: table.i32("output_representation")?[row], + ordered_open_string_arity: table.u32("ordered_open_string_arity")?[row], + exact_coefficient_factor_id: table.u32("exact_coefficient_factor_id")?[row], + witness_start: table.u64("witness_start")?[row], + witness_count: table.u64("witness_count")?[row], + nc_term_start: table.u64("nc_term_start")?[row], + nc_term_count: table.u64("nc_term_count")?[row], + expression_digest_id: table.u32("expression_digest_id")?[row], + semantic_digest_id: table.u32("semantic_digest_id")?[row], + }) + })?; + let lc_color_transition_witnesses = decode_template_rows( + input.table("lc_color_transition_witnesses")?, + |table, row| { + Ok(LCColorTransitionWitnessRow { + color_contraction_id: table.u32("color_contraction_id")?[row], + ordinal: table.u32("ordinal")?[row], + left_shape_string_id: table.u32("left_shape_string_id")?[row], + right_shape_string_id: table.u32("right_shape_string_id")?[row], + input_permutation: table.u8("input_permutation")?[row], + reverse_parent_mask: table.u8("reverse_parent_mask")?[row], + component_operation: table.u8("component_operation")?[row], + result_component_kind: table.u8("result_component_kind")?[row], + result_component_role: table.u8("result_component_role")?[row], + result_shape_string_id: table.u32("result_shape_string_id")?[row], + exact_factor_id: table.u32("exact_factor_id")?[row], + proof_digest_id: table.u32("proof_digest_id")?[row], + input_port_pairing_sequence_id: table.u32("input_port_pairing_sequence_id")?[row], + result_port_binding_sequence_id: table.u32("result_port_binding_sequence_id")?[row], + provenance_sequence_id: table.u32("provenance_sequence_id")?[row], + }) + }, + )?; + let color_nc_terms = decode_template_rows(input.table("color_nc_terms")?, |table, row| { + Ok(ColorNcTermRow { + color_contraction_id: table.u32("color_contraction_id")?[row], + exponent: table.i32("exponent")?[row], + factor_id: table.u32("factor_id")?[row], + }) + })?; + let u32_sequence_ranges = template_indexed_ranges(input.table("u32_sequence_ranges")?)?; + let u32_sequence_values = input.table("u32_sequence_values")?.u32("value")?.to_vec(); + + Ok(OwnedRecurrenceTemplateInput { + input_abi: input.abi.clone(), + catalog_digest: catalog_digest, + compiled_model_digest: compiled_model_digest, + prepared_kernel_pack_digest: prepared_kernel_pack_digest, + catalog_header, + coupling_order_ranges, + coupling_order_terms, + contact_orbit_certificates, + contact_orbit_steps, + current_states, + digest_catalog, + evaluator_bindings, + exact_factors, + flavour_flow_ranges, + flavour_flow_values, + i32_sequence_ranges, + i32_sequence_values, + parameters, + propagators, + quantum_flows, + quantum_number_flow_ranges, + quantum_number_flow_terms, + runtime_helicity_contracts, + runtime_helicity_variants, + runtime_helicity_embeddings, + runtime_helicity_projections, + sources, + string_ranges, + string_bytes, + symmetry_proofs, + transitions, + closures, + color_contractions, + lc_color_transition_witnesses, + color_nc_terms, + u32_sequence_ranges, + u32_sequence_values, + }) +} + +fn decode_template_rows( + table: &OwnedTable, + mut decode: impl FnMut(&OwnedTable, usize) -> RusticolResult, +) -> RusticolResult> { + let row_count = table.row_count()?; + (0..row_count).map(|row| decode(table, row)).collect() +} + +fn template_indexed_ranges(table: &OwnedTable) -> RusticolResult> { + decode_template_rows(table, |table, row| { + Ok(template::IndexedRangeRow { + id: table.u32("id")?[row], + range: CheckedTableRange::new(table.u64("start")?[row], table.u64("count")?[row]), + }) + }) +} + +fn template_plain_ranges(table: &OwnedTable) -> RusticolResult> { + decode_template_rows(table, |table, row| { + Ok(CheckedTableRange::new( + table.u64("start")?[row], + table.u64("count")?[row], + )) + }) +} + +fn decode_template_digest_catalog( + table: &OwnedTable, +) -> RusticolResult> { + let ids = table.u32("id")?; + let values = table.u8("value")?; + let row_count = table.row_count()?; + let expected = row_count + .checked_mul(32) + .ok_or_else(|| invalid("digest catalog byte count exceeds usize"))?; + if values.len() != expected { + return Err(invalid(format!( + "digest_catalog.value has {} bytes, expected {expected}", + values.len() + ))); + } + (0..row_count) + .map(|row| { + let start = row * 32; + let mut value = [0_u8; 32]; + value.copy_from_slice(&values[start..start + 32]); + Ok(template::DigestCatalogRow { + id: ids[row], + value, + }) + }) + .collect() +} + +fn validate_inventory(input: &OwnedInput) -> RusticolResult<()> { + if input.abi != RECURRENCE_BUILDER_INPUT_ABI { + return Err(RusticolError::compatibility(format!( + "unsupported recurrence builder input ABI {:?}; expected {:?}", + input.abi, RECURRENCE_BUILDER_INPUT_ABI + ))); + } + if input.tables.len() != TABLE_SPECS.len() { + return Err(invalid( + "recurrence builder table inventory changed after extraction", + )); + } + for (table, spec) in input.tables.iter().zip(TABLE_SPECS) { + if table.name != spec.name || table.columns.len() != spec.columns.len() { + return Err(invalid(format!( + "recurrence builder table {:?} schema changed after extraction", + table.name + ))); + } + for (column, column_spec) in table.columns.iter().zip(spec.columns) { + if column.name != column_spec.name || column.dtype != column_spec.kind.dtype() { + return Err(invalid(format!( + "recurrence builder column {}.{} schema changed after extraction", + table.name, column.name + ))); + } + if column.shape.first().copied() != Some(table.row_count) { + return Err(invalid(format!( + "recurrence builder column {}.{} row count changed after extraction", + table.name, column.name + ))); + } + } + } + Ok(()) +} + +fn canonical_digest(input: &OwnedInput) -> RusticolResult { + let mut digest = Sha256::new(); + hash_text(&mut digest, &input.abi)?; + hash_tables(&mut digest, &input.tables)?; + Ok(hex_digest(digest.finalize())) +} + +fn hash_tables(digest: &mut Sha256, tables: &[OwnedTable]) -> RusticolResult<()> { + digest.update( + u64::try_from(tables.len()) + .map_err(|_| invalid("recurrence table count exceeds u64"))? + .to_le_bytes(), + ); + for table in tables { + hash_text(digest, &table.name)?; + digest.update(table.row_count.to_le_bytes()); + digest.update( + u32::try_from(table.columns.len()) + .map_err(|_| invalid("recurrence table column count exceeds u32"))? + .to_le_bytes(), + ); + for column in &table.columns { + hash_text(digest, &column.name)?; + hash_text(digest, column.dtype)?; + digest.update( + u8::try_from(column.shape.len()) + .map_err(|_| invalid("recurrence column rank exceeds u8"))? + .to_le_bytes(), + ); + for dimension in &column.shape { + digest.update(dimension.to_le_bytes()); + } + digest.update(column.values.raw_bytes()); + } + } + Ok(()) +} + +fn iterable_attribute<'py>( + value: &Bound<'py, PyAny>, + attribute: &str, + context: &str, +) -> PyResult>> { + value + .getattr(attribute)? + .try_iter() + .map_err(|_| PyTypeError::new_err(format!("{context} must be iterable")))? + .collect() +} + +fn required_string(value: &Bound<'_, PyAny>, attribute: &str) -> PyResult { + value.getattr(attribute)?.extract::().map_err(|_| { + PyTypeError::new_err(format!( + "recurrence builder input {attribute} must be a string" + )) + }) +} + +fn required_nonempty_string( + value: &Bound<'_, PyAny>, + attribute: &str, + context: &str, +) -> PyResult { + let result = required_string(value, attribute)?; + if result.is_empty() { + return Err(PyValueError::new_err(format!( + "recurrence builder {context} must not be empty" + ))); + } + Ok(result) +} + +fn validate_sha256_text(value: &str, context: &str) -> PyResult<()> { + if value.len() != 64 + || !value + .bytes() + .all(|byte| byte.is_ascii_digit() || matches!(byte, b'a'..=b'f')) + { + return Err(PyValueError::new_err(format!( + "{context} must be a lowercase SHA-256 digest" + ))); + } + Ok(()) +} + +fn semantic_digest_python_attribute( + value: &Bound<'_, PyAny>, + attribute: &str, +) -> PyResult { + let digest = required_string(value, attribute)?; + semantic_digest_from_hex(&digest, attribute).map_err(python_error) +} + +pub(super) fn semantic_digest_from_hex( + value: &str, + context: &str, +) -> RusticolResult { + if value.len() != 64 + || !value + .bytes() + .all(|byte| byte.is_ascii_digit() || matches!(byte, b'a'..=b'f')) + { + return Err(invalid(format!( + "{context} must be a lowercase SHA-256 digest" + ))); + } + let mut result = [0_u8; 32]; + for (index, byte) in result.iter_mut().enumerate() { + let offset = index * 2; + *byte = u8::from_str_radix(&value[offset..offset + 2], 16) + .map_err(|_| invalid(format!("{context} is not hexadecimal")))?; + } + SemanticDigest::new(result) +} + +fn hash_text(digest: &mut Sha256, value: &str) -> RusticolResult<()> { + digest.update( + u64::try_from(value.len()) + .map_err(|_| invalid("recurrence digest text length exceeds u64"))? + .to_le_bytes(), + ); + digest.update(value.as_bytes()); + Ok(()) +} + +pub(super) fn hex_digest(value: impl AsRef<[u8]>) -> String { + let mut result = String::with_capacity(value.as_ref().len() * 2); + for byte in value.as_ref() { + use std::fmt::Write; + write!(&mut result, "{byte:02x}").expect("writing to String cannot fail"); + } + result +} + +pub(super) fn invalid(message: impl Into) -> RusticolError { + RusticolError::invalid_argument(message) +} + +fn wrong_type(context: &str, expected: &str) -> RusticolError { + invalid(format!("{context} is not a {expected} recurrence column")) +} + +#[cfg(test)] +mod direct_binding_tests { + use super::*; + use serde_json::json; + + fn construction_progress( + stage_index: usize, + candidate_parent_pair_count: usize, + ) -> RecurrenceBuildProgress { + RecurrenceBuildProgress { + phase: "recurrence stage", + phase_index: stage_index + 2, + phase_total: 33, + stage_index: Some(stage_index), + stage_total: 30, + subset_size: Some(stage_index % 5 + 2), + candidate_parent_pair_count, + candidate_parent_pair_total: Some(candidate_parent_pair_count), + current_count: 0, + contribution_count: 0, + dynamic_color_state_count: 0, + color_target_prune_count: 0, + } + } + + fn digest(seed: u8) -> String { + format!("{seed:02x}").repeat(32) + } + + fn refresh_digest(value: &mut JsonValue, field: &str, context: &str) { + value.as_object_mut().unwrap().remove(field); + let digest: [u8; 32] = Sha256::digest(canonical_json_bytes(value, context).unwrap()).into(); + value + .as_object_mut() + .unwrap() + .insert(field.to_owned(), json!(hex_digest(digest))); + } + + fn canonical_direct_catalog() -> (Vec, SemanticDigest, SemanticDigest, SemanticDigest) { + let prepared_pack = semantic_digest_from_hex(&digest(1), "test pack").unwrap(); + let semantic_catalog = + semantic_digest_from_hex(&digest(2), "test semantic catalog").unwrap(); + let compiled_model = semantic_digest_from_hex(&digest(3), "test compiled model").unwrap(); + let mut template = json!({ + "abi": RECURRENCE_DIRECT_TEMPLATE_ABI, + "alignment_bytes": 64, + "backend": "jit", + "coupling_slot_count": 0, + "destination_aliasing": false, + "destination_component_count": 4, + "destination_operation": "initialize", + "direct_executor_id": 0, + "evaluator_binding_id": 7, + "evaluator_resolver_key": "source:7", + "exact_expression_digest": digest(4), + "momentum_operand_count": 1, + "optimization_level": 2, + "parameter_slot_count": 0, + "parent_arity": 0, + "parent_component_counts": [], + "payload_binding": { + "abi": DIRECT_PAYLOAD_BINDING_ABI, + "destination_operation": "initialize", + "direct_application_abi": null, + "exact_factor_scalar_slots": [], + "input_plane_count": 0, + "input_plane_projections": [], + "intrinsic_contract_digest": null, + "kind": "rusticol-intrinsic", + "native_entry_point": null, + "output_alias_inputs": [], + "contribution_parent_permutation": [0, 1], + "parameter_bindings": [], + "payload_digest": digest(5), + "payload_paths": [], + "prepared_kernel_id": null, + "prepared_template_semantic_digest": null, + "role": "source", + "runtime_template": "rusticol.source-fill.v1", + "scalar_input_count": 0, + "scalar_projections": [], + "source_application_abi": null, + "source_application_path": null, + "source_application_sha256": null, + "state_plane_indices": [] + }, + "portable": true, + "role": "source", + "semantic_template_ids": ["source:test"], + "simd_axis": "points-contiguous", + "target_triple": "symjit-storage-v3-portable", + "template_id": "direct:source:test" + }); + let template_digest: [u8; 32] = + Sha256::digest(canonical_json_bytes(&template, "test template").unwrap()).into(); + template.as_object_mut().unwrap().insert( + "semantic_digest".to_owned(), + json!(hex_digest(template_digest)), + ); + + let mut catalog = json!({ + "abi": RECURRENCE_DIRECT_TEMPLATE_ABI, + "backend": "jit", + "backend_abi": DIRECT_BACKEND_ABI, + "canonicalization_abi": DIRECT_CANONICALIZATION_ABI, + "compiled_model_digest": compiled_model.to_string(), + "optimization_level": 2, + "optimization_settings_digest": digest(6), + "portable": true, + "prepared_kernel_contract_digest": digest(7), + "prepared_kernel_pack_digest": prepared_pack.to_string(), + "prepared_kernel_payload_digest": digest(8), + "recurrence_template_catalog_digest": semantic_catalog.to_string(), + "target_triple": "symjit-storage-v3-portable", + "templates": [template] + }); + let catalog_digest: [u8; 32] = + Sha256::digest(canonical_json_bytes(&catalog, "test catalog").unwrap()).into(); + catalog.as_object_mut().unwrap().insert( + "catalog_digest".to_owned(), + json!(hex_digest(catalog_digest)), + ); + ( + canonical_json_bytes(&catalog, "test catalog").unwrap(), + prepared_pack, + semantic_catalog, + compiled_model, + ) + } + + #[test] + fn canonical_catalog_authenticates_dense_role_binding() { + let (bytes, pack, semantic, model) = canonical_direct_catalog(); + let parsed = parse_direct_template_catalog(&bytes, pack, semantic, model).unwrap(); + + assert_eq!(parsed.catalog.direct_executor_count(), 1); + assert_eq!( + parsed + .catalog + .resolve_evaluator(DirectExecutorRole::Source, 7) + .unwrap(), + 0 + ); + assert_eq!(parsed.prepared_kernel_count, 0); + } + + #[test] + fn construction_metrics_sum_lane_unique_stage_counts() { + let mut metrics = RecurrenceConstructionMetrics::default(); + for lane_index in 0..6 { + for stage_index_within_lane in 0..5 { + let stage_index = lane_index * 5 + stage_index_within_lane; + let final_count = (lane_index + 1) * (stage_index_within_lane + 2); + metrics.include(&construction_progress(stage_index, final_count / 2)); + metrics.include(&construction_progress(stage_index, final_count)); + } + } + + let expected = (0..6) + .flat_map(|lane_index| { + (0..5).map(move |stage_index_within_lane| { + (lane_index + 1) * (stage_index_within_lane + 2) + }) + }) + .sum::(); + assert_eq!(metrics.candidate_parent_pair_count_by_stage.len(), 30); + assert_eq!(metrics.candidate_parent_pair_count(), expected); + } + + #[test] + fn noncanonical_catalog_bytes_are_rejected() { + let (mut bytes, pack, semantic, model) = canonical_direct_catalog(); + bytes.push(b'\n'); + + let error = match parse_direct_template_catalog(&bytes, pack, semantic, model) { + Ok(_) => panic!("noncanonical direct catalog was accepted"), + Err(error) => error, + }; + assert!(error.to_string().contains("not canonical")); + } + + #[test] + fn catalog_digest_authenticates_executor_mapping() { + let (bytes, pack, semantic, model) = canonical_direct_catalog(); + let mut value: JsonValue = serde_json::from_slice(&bytes).unwrap(); + value["templates"][0]["evaluator_binding_id"] = json!(8); + let tampered = canonical_json_bytes(&value, "tampered catalog").unwrap(); + + let error = match parse_direct_template_catalog(&tampered, pack, semantic, model) { + Ok(_) => panic!("tampered direct catalog was accepted"), + Err(error) => error, + }; + assert!(error.to_string().contains("catalog digest")); + } + + #[test] + fn pending_direct_payload_is_rejected_before_lowering() { + let (bytes, pack, semantic, model) = canonical_direct_catalog(); + let mut value: JsonValue = serde_json::from_slice(&bytes).unwrap(); + let template = &mut value["templates"][0]; + template["payload_binding"]["kind"] = json!("pending-direct-call-abi"); + template["payload_binding"]["runtime_template"] = JsonValue::Null; + template["payload_binding"]["prepared_kernel_id"] = json!(7); + refresh_digest(template, "semantic_digest", "pending direct template"); + refresh_digest(&mut value, "catalog_digest", "pending direct catalog"); + let pending = canonical_json_bytes(&value, "pending direct catalog").unwrap(); + + let error = match parse_direct_template_catalog(&pending, pack, semantic, model) { + Ok(_) => panic!("pending direct payload was accepted"), + Err(error) => error, + }; + assert!( + error + .to_string() + .contains("no executable Direct-Arena payload") + ); + } +} diff --git a/pyamplicol/rust/crates/rusticol-python/src/recurrence_numerical_evidence.rs b/pyamplicol/rust/crates/rusticol-python/src/recurrence_numerical_evidence.rs new file mode 100644 index 0000000000..20ec80243b --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-python/src/recurrence_numerical_evidence.rs @@ -0,0 +1,4783 @@ +// SPDX-License-Identifier: 0BSD + +//! Fail-closed authentication of recurrence numerical-current evidence. +//! +//! Python supplies complete raw candidate and independent-verification captures. +//! This module independently reconstructs their schema, probe contexts, exact +//! rational residuals, decision census, certificates, and current mappings +//! before the native Direct-Arena lowering is allowed to apply any reuse. + +use flate2::read::ZlibDecoder; +use num_bigint::BigInt; +use num_rational::BigRational; +use num_traits::{One, Signed, ToPrimitive, Zero}; +use rusticol_core::recurrence::process::ValidatedRecurrenceProcessInput; +use rusticol_core::recurrence::template::{ParameterKind, ValidatedRecurrenceTemplateInput}; +use rusticol_core::recurrence::{ + RecurrenceNumericalCurrentMapping, RecurrenceNumericalRelationEvidence, + RecurrenceRelationDiscoveryMode, +}; +use rusticol_core::{RusticolError, RusticolResult}; +use serde_json::{Map as JsonMap, Value as JsonValue, value::RawValue as JsonRawValue}; +use sha2::{Digest, Sha256}; +use std::collections::{BTreeMap, BTreeSet}; +use std::io::{self, Read, Write}; +use std::ops::Range; +use std::str::FromStr; +use std::time::Instant; + +use crate::recurrence::{ + canonical_json_bytes, canonical_json_sha256, hex_digest, invalid, json_array, json_bool, + json_field, json_nonempty_string, json_object, json_string, json_u32, require_json_fields, + require_json_string_value, semantic_digest_from_hex, +}; + +type ExactProbeComplex = (BigRational, BigRational); + +const COMPRESSED_EVIDENCE_MAGIC: &[u8; 8] = b"PACNCEZ1"; +const COMPRESSED_EVIDENCE_HEADER_BYTES: usize = 8 + 8 + 32; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +struct RawByteRange { + start: usize, + end: usize, +} + +impl RawByteRange { + fn from_usize(start: usize, end: usize, limit: usize, context: &str) -> RusticolResult { + if start > end || end > limit { + return Err(invalid(format!("{context} is outside the raw evidence"))); + } + Ok(Self { start, end }) + } + + fn as_usize(self, limit: usize, context: &str) -> RusticolResult> { + let range = self.start..self.end; + if range.start > range.end || range.end > limit { + return Err(invalid(format!("{context} is outside the raw evidence"))); + } + Ok(range) + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +struct RawObservationRow { + row: RawByteRange, + values: RawByteRange, +} + +struct LocatedObservationArrays { + metadata_bytes: Vec, + candidate: RawByteRange, + verification: RawByteRange, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +struct LocatedObservationRanges { + candidate: RawByteRange, + verification: RawByteRange, +} + +fn borrowed_raw_value_range( + bytes: &[u8], + value: &JsonRawValue, + context: &str, +) -> RusticolResult { + let raw = value.get().as_bytes(); + let bytes_start = bytes.as_ptr() as usize; + let raw_start = raw.as_ptr() as usize; + let start = raw_start + .checked_sub(bytes_start) + .ok_or_else(|| invalid(format!("{context} does not borrow the raw evidence")))?; + let end = start + .checked_add(raw.len()) + .ok_or_else(|| invalid(format!("{context} byte range overflows usize")))?; + RawByteRange::from_usize(start, end, bytes.len(), context) +} + +fn locate_raw_observation_ranges(bytes: &[u8]) -> RusticolResult { + let root = serde_json::from_slice::>(bytes) + .map_err(|error| invalid(format!("numerical relation evidence is not JSON: {error}")))?; + let locate = |field: &str| -> RusticolResult { + let capture = root + .get(field) + .ok_or_else(|| invalid(format!("numerical relation evidence has no {field}")))?; + let capture_fields = serde_json::from_str::>(capture.get()) + .map_err(|error| { + invalid(format!( + "{field} raw numerical capture is not JSON: {error}" + )) + })?; + let observations = capture_fields + .get("observations") + .ok_or_else(|| invalid(format!("{field} raw numerical capture has no observations")))?; + borrowed_raw_value_range(bytes, observations, &format!("{field} observations")) + }; + let candidate = locate("candidate_capture")?; + let verification = locate("verification_capture")?; + let candidate_range = candidate.as_usize(bytes.len(), "candidate observation-array range")?; + let verification_range = + verification.as_usize(bytes.len(), "verification observation-array range")?; + if candidate_range.end > verification_range.start { + return Err(invalid( + "raw numerical observation arrays overlap or are not canonical", + )); + } + Ok(LocatedObservationRanges { + candidate, + verification, + }) +} + +fn raw_observation_metadata_byte_count( + bytes: &[u8], + ranges: LocatedObservationRanges, +) -> RusticolResult { + let candidate_range = ranges + .candidate + .as_usize(bytes.len(), "candidate observation-array range")?; + let verification_range = ranges + .verification + .as_usize(bytes.len(), "verification observation-array range")?; + let removed = candidate_range + .len() + .checked_add(verification_range.len()) + .ok_or_else(|| invalid("raw observation-array byte count overflows usize"))?; + bytes + .len() + .checked_sub(removed) + .and_then(|size| size.checked_add(4)) + .ok_or_else(|| invalid("raw numerical metadata size overflows usize")) +} + +fn materialize_raw_observation_metadata( + bytes: &[u8], + ranges: LocatedObservationRanges, +) -> RusticolResult> { + let candidate_range = ranges + .candidate + .as_usize(bytes.len(), "candidate observation-array range")?; + let verification_range = ranges + .verification + .as_usize(bytes.len(), "verification observation-array range")?; + let capacity = raw_observation_metadata_byte_count(bytes, ranges)?; + let mut metadata_bytes = Vec::with_capacity(capacity); + metadata_bytes.extend_from_slice(&bytes[..candidate_range.start]); + metadata_bytes.extend_from_slice(b"[]"); + metadata_bytes.extend_from_slice(&bytes[candidate_range.end..verification_range.start]); + metadata_bytes.extend_from_slice(b"[]"); + metadata_bytes.extend_from_slice(&bytes[verification_range.end..]); + if metadata_bytes.len() != capacity { + return Err(invalid( + "raw numerical metadata materialization length drifted", + )); + } + Ok(metadata_bytes) +} + +#[cfg(test)] +fn locate_raw_observation_arrays(bytes: &[u8]) -> RusticolResult { + let ranges = locate_raw_observation_ranges(bytes)?; + let metadata_bytes = materialize_raw_observation_metadata(bytes, ranges)?; + Ok(LocatedObservationArrays { + metadata_bytes, + candidate: ranges.candidate, + verification: ranges.verification, + }) +} + +struct CanonicalObservationCursor<'a> { + bytes: &'a [u8], + position: usize, + absolute_start: usize, +} + +impl<'a> CanonicalObservationCursor<'a> { + fn expect(&mut self, expected: &[u8], context: &str) -> RusticolResult<()> { + let end = self + .position + .checked_add(expected.len()) + .ok_or_else(|| invalid(format!("{context} offset overflows usize")))?; + if self.bytes.get(self.position..end) != Some(expected) { + return Err(invalid(format!("{context} is malformed or not canonical"))); + } + self.position = end; + Ok(()) + } + + fn absolute_position(&self, context: &str) -> RusticolResult { + self.absolute_start + .checked_add(self.position) + .ok_or_else(|| invalid(format!("{context} offset overflows usize"))) + } + + fn decimal_string(&mut self, context: &str) -> RusticolResult<&'a str> { + self.expect(b"\"", context)?; + let remaining = self + .bytes + .get(self.position..) + .ok_or_else(|| invalid(format!("{context} is truncated")))?; + let width = remaining + .iter() + .position(|byte| *byte == b'"') + .ok_or_else(|| invalid(format!("{context} is unterminated")))?; + let end = self + .position + .checked_add(width) + .ok_or_else(|| invalid(format!("{context} width overflows usize")))?; + let text = std::str::from_utf8( + self.bytes + .get(self.position..end) + .ok_or_else(|| invalid(format!("{context} is truncated")))?, + ) + .map_err(|_| invalid(format!("{context} is not ASCII")))?; + validate_canonical_decimal_text(text, context)?; + self.position = end; + self.expect(b"\"", context)?; + Ok(text) + } +} + +fn scan_raw_observation_array( + bytes: &[u8], + array: RawByteRange, + source: &RawSourceSemantics, + point_count: usize, + label: &str, +) -> RusticolResult> { + let array_range = array.as_usize(bytes.len(), &format!("{label} observation array"))?; + let raw_array = bytes + .get(array_range.clone()) + .ok_or_else(|| invalid(format!("{label} observation array is absent")))?; + let mut cursor = CanonicalObservationCursor { + bytes: raw_array, + position: 0, + absolute_start: array_range.start, + }; + cursor.expect(b"[", &format!("{label} observation array"))?; + let mut rows = Vec::with_capacity(source.currents.len()); + for (index, current) in source.currents.iter().enumerate() { + if index != 0 { + cursor.expect(b",", &format!("{label} observation row {index}"))?; + } + let row_start = cursor.absolute_position(&format!("{label} observation row {index}"))?; + cursor.expect( + b"{\"current_id\":", + &format!("{label} observation row {index}"), + )?; + cursor.expect( + current.current_id.to_string().as_bytes(), + &format!("{label} observation row {index} current ID"), + )?; + cursor.expect( + b",\"dimension\":", + &format!("{label} observation row {index}"), + )?; + cursor.expect( + current.dimension.to_string().as_bytes(), + &format!("{label} observation row {index} dimension"), + )?; + cursor.expect(b",\"values\":", &format!("{label} observation row {index}"))?; + let values_start = + cursor.absolute_position(&format!("{label} observation row {index} values"))?; + cursor.expect(b"[", &format!("{label} observation row {index} values"))?; + let expected_width = point_count + .checked_mul(usize::from(current.dimension)) + .ok_or_else(|| { + invalid(format!( + "{label} observation row {index} width overflows usize" + )) + })?; + for component in 0..expected_width { + let context = format!("{label} observation row {index} component {component}"); + if component != 0 { + cursor.expect(b",", &context)?; + } + cursor.expect(b"[", &context)?; + cursor.decimal_string(&format!("{context} real part"))?; + cursor.expect(b",", &context)?; + cursor.decimal_string(&format!("{context} imaginary part"))?; + cursor.expect(b"]", &context)?; + } + cursor.expect(b"]", &format!("{label} observation row {index} values"))?; + let values_end = + cursor.absolute_position(&format!("{label} observation row {index} values"))?; + cursor.expect(b"}", &format!("{label} observation row {index}"))?; + let row_end = cursor.absolute_position(&format!("{label} observation row {index}"))?; + rows.push(RawObservationRow { + row: RawByteRange::from_usize( + row_start, + row_end, + bytes.len(), + &format!("{label} observation row {index}"), + )?, + values: RawByteRange::from_usize( + values_start, + values_end, + bytes.len(), + &format!("{label} observation row {index} values"), + )?, + }); + } + cursor.expect(b"]", &format!("{label} observation array"))?; + if cursor.position != cursor.bytes.len() { + return Err(invalid(format!( + "{label} observation array has trailing or extra rows" + ))); + } + Ok(rows) +} + +struct CanonicalJsonMatcher<'a> { + expected: &'a [u8], + offset: usize, + matches: bool, +} + +impl Write for CanonicalJsonMatcher<'_> { + fn write(&mut self, buffer: &[u8]) -> io::Result { + let end = self.offset.saturating_add(buffer.len()); + if end > self.expected.len() || self.expected[self.offset..end] != *buffer { + self.matches = false; + } + self.offset = end; + Ok(buffer.len()) + } + + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } +} + +fn validate_canonical_json_bytes( + value: &JsonValue, + expected: &[u8], + context: &str, +) -> RusticolResult<()> { + let mut matcher = CanonicalJsonMatcher { + expected, + offset: 0, + matches: true, + }; + serde_json::to_writer(&mut matcher, value) + .map_err(|error| invalid(format!("could not canonicalize {context}: {error}")))?; + if !matcher.matches || matcher.offset != expected.len() { + return Err(invalid(format!("{context} is not canonical ASCII JSON"))); + } + Ok(()) +} + +fn validate_raw_json_lexical_structure(bytes: &[u8]) -> RusticolResult<()> { + let mut in_string = false; + let mut escaped = false; + let mut depth = 0_usize; + for &byte in bytes { + if !byte.is_ascii() { + return Err(invalid( + "numerical relation evidence must be canonical ASCII JSON", + )); + } + if in_string { + if escaped { + escaped = false; + } else if byte == b'\\' { + escaped = true; + } else if byte == b'"' { + in_string = false; + } + continue; + } + match byte { + b'"' => { + in_string = true; + } + b'{' | b'[' => { + depth += 1; + } + b'}' | b']' => { + depth = depth.saturating_sub(1); + } + _ => {} + } + } + if in_string || depth != 0 { + return Err(invalid( + "numerical relation evidence JSON lexical structure is incomplete", + )); + } + Ok(()) +} + +fn decode_compressed_evidence(bytes: &[u8]) -> RusticolResult> { + if bytes.len() < COMPRESSED_EVIDENCE_HEADER_BYTES { + return Err(invalid( + "compressed numerical relation evidence header is truncated", + )); + } + if bytes.get(..COMPRESSED_EVIDENCE_MAGIC.len()) != Some(COMPRESSED_EVIDENCE_MAGIC) { + return Err(invalid( + "compressed numerical relation evidence has invalid magic", + )); + } + let declared_bytes = u64::from_be_bytes( + bytes[8..16] + .try_into() + .map_err(|_| invalid("compressed numerical evidence length header is malformed"))?, + ); + let declared_bytes = usize::try_from(declared_bytes) + .map_err(|_| invalid("compressed numerical evidence length exceeds usize"))?; + if declared_bytes == 0 { + return Err(invalid( + "compressed numerical relation evidence declares an empty payload", + )); + } + + let expected_digest = bytes + .get(16..COMPRESSED_EVIDENCE_HEADER_BYTES) + .ok_or_else(|| invalid("compressed numerical evidence digest header is malformed"))?; + let compressed = bytes + .get(COMPRESSED_EVIDENCE_HEADER_BYTES..) + .ok_or_else(|| invalid("compressed numerical evidence payload is truncated"))?; + if compressed.is_empty() { + return Err(invalid( + "compressed numerical relation evidence payload is empty", + )); + } + + let mut decoded = Vec::new(); + decoded + .try_reserve_exact(declared_bytes) + .map_err(|_| invalid("could not reserve decompressed numerical evidence"))?; + let mut decoder = ZlibDecoder::new(compressed); + { + let maximum_read = u64::try_from(declared_bytes) + .ok() + .and_then(|count| count.checked_add(1)) + .ok_or_else(|| invalid("compressed numerical decompression limit overflows u64"))?; + let mut limited = decoder.by_ref().take(maximum_read); + limited.read_to_end(&mut decoded).map_err(|error| { + invalid(format!( + "compressed numerical relation evidence could not be decompressed: {error}" + )) + })?; + } + if decoded.len() != declared_bytes { + return Err(invalid(format!( + "compressed numerical relation evidence decompressed length disagrees with its declaration (declared={declared_bytes}, actual={})", + decoded.len() + ))); + } + if usize::try_from(decoder.total_in()).ok() != Some(compressed.len()) { + return Err(invalid( + "compressed numerical relation evidence has trailing or unconsumed transport bytes", + )); + } + if Sha256::digest(&decoded).as_slice() != expected_digest { + return Err(RusticolError::integrity( + "compressed numerical relation evidence digest mismatch", + )); + } + Ok(decoded) +} + +#[derive(Clone, Debug)] +struct RawNumericalCapture<'a> { + raw_bytes: &'a [u8], + observation_rows: Vec, + point_count: usize, + point_sha256s: Vec, + kinematic_sha256s: Vec, + parameter_context_sha256s: Vec, + parameter_contexts: Vec>, + dimensions: Vec, + observation_batch_sha256: String, + capture_contract_sha256: String, +} + +#[derive(Clone, Debug)] +struct RawSourceCurrent { + current_id: u32, + is_source: bool, + contract_key: Vec, + dimension: u16, + selector_domain_id: u32, +} + +#[derive(Clone, Debug)] +struct RawSourceSemantics { + value: JsonValue, + process_id: String, + physical_pdgs: Vec, + strategy: String, + selector_schedule: JsonValue, + currents: Vec, +} + +#[derive(Clone, Debug)] +struct DerivedNumericalRelation { + mapping: RecurrenceNumericalCurrentMapping, + certificate: JsonValue, + mapping_json: JsonValue, +} + +#[derive(Clone, Debug)] +struct RawNumericalCandidateIndex { + #[cfg(test)] + observation_index: usize, + scalar_component: usize, + entries: Vec<(BigRational, u32)>, + selected_values: Vec<(u32, ExactProbeComplex)>, +} + +impl RawNumericalCandidateIndex { + fn selected_value(&self, current_id: u32, context: &str) -> RusticolResult<&ExactProbeComplex> { + let index = self + .selected_values + .binary_search_by_key(¤t_id, |(candidate_id, _value)| *candidate_id) + .map_err(|_| invalid(format!("{context} selected observation is absent")))?; + Ok(&self.selected_values[index].1) + } +} + +#[derive(Clone, Debug)] +struct RawNumericalDerivation { + relations: Vec, + numerical_candidate_count: usize, + verification_rejected_count: usize, + rejected_hypothesis_count: usize, + tested_hypothesis_count: usize, + theoretical_pair_hypothesis_count: usize, + screened_pair_hypothesis_count: usize, + zero_hypothesis_count: usize, + candidate_index_contract_count: usize, + exhaustive_fallback_contract_count: usize, + decision_sha256: String, + rejection_decision_sha256: String, +} + +/// Internal, generation-only measurements for numerical-evidence replay. +/// +/// This record never enters a Python result, recurrence artifact, runtime +/// payload, semantic digest, or public ABI. +#[doc(hidden)] +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub(super) struct RecurrenceEvidenceAuthenticationTelemetry { + pub transport_decode_nanoseconds: u64, + pub capture_authentication_nanoseconds: u64, + pub candidate_index_nanoseconds: u64, + pub hypothesis_replay_nanoseconds: u64, + pub derivation_finalization_nanoseconds: u64, + pub evidence_finalization_nanoseconds: u64, + pub total_nanoseconds: u64, + pub candidate_index_observation_row_scan_count: usize, + pub retained_selected_observation_count: usize, + pub selected_observation_reuse_count: usize, + pub zero_residual_streamed_value_count: usize, + pub materialized_observation_vector_count: usize, + pub rational_string_materialization_count: usize, + pub rational_string_use_count: usize, + pub rational_string_reuse_count: usize, +} + +impl RecurrenceEvidenceAuthenticationTelemetry { + fn checked_add(target: &mut usize, value: usize, description: &str) -> RusticolResult<()> { + *target = target + .checked_add(value) + .ok_or_else(|| invalid(format!("{description} exceeds usize")))?; + Ok(()) + } + + fn record_rational_string_uses(&mut self, count: usize) -> RusticolResult<()> { + Self::checked_add( + &mut self.rational_string_use_count, + count, + "evidence-authentication rational-string use count", + ) + } + + fn finish_rational_string_census(&mut self) -> RusticolResult<()> { + self.rational_string_reuse_count = self + .rational_string_use_count + .checked_sub(self.rational_string_materialization_count) + .ok_or_else(|| { + invalid( + "evidence-authentication rational-string use count is below materialization count", + ) + })?; + Ok(()) + } +} + +fn elapsed_nanoseconds(started: Instant) -> u64 { + u64::try_from(started.elapsed().as_nanos()).unwrap_or(u64::MAX) +} + +#[derive(Clone, Debug)] +struct AuthenticatedRuntimeParameterSlot { + default: BigRational, + probe_policy: &'static str, +} + +#[derive(Clone, Debug)] +pub(super) struct AuthenticatedRuntimeParameterContract { + schema: JsonValue, + schema_sha256: String, + slots: Vec, + process_id: String, + physical_pdgs: Vec, +} + +#[derive(Clone, Debug)] +struct RelationResiduals { + maximum_ratio: BigRational, + strings: RelationResidualStrings, +} + +#[derive(Clone, Debug)] +struct RelationResidualStrings { + maximum_absolute: String, + maximum_relative: String, + maximum_ratio: String, +} + +pub(super) fn authenticated_runtime_parameter_contract( + process: &ValidatedRecurrenceProcessInput, + template: &ValidatedRecurrenceTemplateInput, +) -> RusticolResult { + const ABI: &str = "pyamplicol-recurrence-runtime-parameter-schema-v2"; + const MATERIAL_POLICY: &str = "native-template-default-perturbed-v1"; + const DERIVED_POLICY: &str = "derived-overwritten-fixed-zero-v1"; + let input = process.input(); + let parameter_contracts = template.parameter_kind_and_default_factors()?; + let mut parameters = Vec::with_capacity(input.parameter_projection.len()); + let mut slots = Vec::with_capacity(input.parameter_projection.len()); + for (index, row) in input.parameter_projection.iter().enumerate() { + if row.runtime_slot as usize != index { + return Err(invalid( + "authenticated runtime parameter projection is not dense", + )); + } + let range = input + .string_ranges + .get(row.runtime_name_string_id as usize) + .copied() + .ok_or_else(|| invalid("runtime-parameter name string range is absent"))? + .as_usize_range( + input.string_bytes.len(), + "runtime-parameter name string range", + )?; + let runtime_name = std::str::from_utf8(&input.string_bytes[range]) + .map_err(|_| invalid("runtime-parameter name is not UTF-8"))?; + let (kind, default_factor) = parameter_contracts + .get(row.parameter_template_id as usize) + .copied() + .ok_or_else(|| invalid("runtime-parameter projection references an absent template"))?; + let (default, default_binary64, probe_policy) = if kind == ParameterKind::Derived { + (BigRational::zero(), 0.0_f64, DERIVED_POLICY) + } else { + let factor = default_factor.ok_or_else(|| { + invalid("material runtime parameter has no authenticated template default") + })?; + let exact = match row.component { + 0 => factor.real(), + 1 => factor.imag(), + _ => { + return Err(invalid( + "runtime-parameter component is not real or imaginary", + )); + } + }; + let rational = BigRational::new( + BigInt::from(exact.numerator()), + BigInt::from(exact.denominator()), + ); + let binary64 = rational.to_f64().ok_or_else(|| { + invalid("authenticated runtime-parameter default is outside binary64") + })?; + if !binary64.is_finite() { + return Err(invalid( + "authenticated runtime-parameter default is not finite binary64", + )); + } + ( + exact_binary64_rational(binary64)?, + binary64, + MATERIAL_POLICY, + ) + }; + parameters.push(serde_json::json!({ + "runtime_slot": row.runtime_slot, + "runtime_name": runtime_name, + "parameter_template_id": row.parameter_template_id, + "prepared_parameter_id": row.prepared_parameter_id(), + "component": row.component, + "default_binary64": python_f64_hex_signed(default_binary64), + "probe_policy": probe_policy, + })); + slots.push(AuthenticatedRuntimeParameterSlot { + default, + probe_policy, + }); + } + let schema = serde_json::json!({ + "abi": ABI, + "parameters": parameters, + }); + let schema_sha256 = + canonical_json_sha256(&schema, "native recurrence runtime-parameter schema")?; + Ok(AuthenticatedRuntimeParameterContract { + schema, + schema_sha256, + slots, + process_id: process.summary().process_id().to_owned(), + physical_pdgs: input + .external_legs + .iter() + .map(|leg| leg.physical_pdg) + .collect(), + }) +} + +pub(super) fn parse_numerical_relation_evidence( + bytes: &[u8], + runtime_parameters: &AuthenticatedRuntimeParameterContract, +) -> RusticolResult { + parse_numerical_relation_evidence_with_telemetry(bytes, runtime_parameters) + .map(|(evidence, _telemetry)| evidence) +} + +#[doc(hidden)] +pub(super) fn parse_numerical_relation_evidence_with_telemetry( + bytes: &[u8], + runtime_parameters: &AuthenticatedRuntimeParameterContract, +) -> RusticolResult<( + RecurrenceNumericalRelationEvidence, + RecurrenceEvidenceAuthenticationTelemetry, +)> { + let total_started = Instant::now(); + let mut telemetry = RecurrenceEvidenceAuthenticationTelemetry::default(); + if bytes.is_empty() { + return Err(invalid("numerical relation evidence must not be empty")); + } + let evidence = if bytes.starts_with(COMPRESSED_EVIDENCE_MAGIC) { + let decode_started = Instant::now(); + let decoded = decode_compressed_evidence(bytes)?; + telemetry.transport_decode_nanoseconds = elapsed_nanoseconds(decode_started); + parse_numerical_relation_evidence_v3(&decoded, runtime_parameters, &mut telemetry)? + } else { + parse_numerical_relation_evidence_v3(bytes, runtime_parameters, &mut telemetry)? + }; + telemetry.total_nanoseconds = elapsed_nanoseconds(total_started); + Ok((evidence, telemetry)) +} + +fn parse_numerical_relation_evidence_v3( + bytes: &[u8], + runtime_parameters: &AuthenticatedRuntimeParameterContract, + telemetry: &mut RecurrenceEvidenceAuthenticationTelemetry, +) -> RusticolResult { + const ABI: &str = "pyamplicol-recurrence-numerical-current-evidence-v3"; + const RELATION_SET_ABI: &str = "pyamplicol-authenticated-numerical-current-relation-set-v2"; + validate_raw_json_lexical_structure(bytes)?; + let observation_ranges = locate_raw_observation_ranges(bytes)?; + let metadata_bytes = materialize_raw_observation_metadata(bytes, observation_ranges)?; + let located_observations = LocatedObservationArrays { + metadata_bytes, + candidate: observation_ranges.candidate, + verification: observation_ranges.verification, + }; + let value: JsonValue = serde_json::from_slice(&located_observations.metadata_bytes) + .map_err(|error| invalid(format!("numerical relation evidence is not JSON: {error}")))?; + validate_canonical_json_bytes( + &value, + &located_observations.metadata_bytes, + "numerical relation evidence metadata", + )?; + let object = json_object(&value, "numerical relation evidence")?; + require_json_fields( + object, + &[ + "abi", + "requested_mode", + "schedule_semantic_digest", + "baseline_runtime_layout_digest", + "source_semantics", + "source_semantics_sha256", + "runtime_parameter_schema", + "runtime_parameter_schema_sha256", + "candidate_capture", + "verification_capture", + "certificate_algorithm", + "certificate_set_sha256", + "precision_digits", + "probe_count", + "verification_probe_count", + "relative_tolerance_binary64", + "absolute_tolerance_binary64", + "seed", + "candidate_index", + "numerical_candidate_count", + "verification_rejected_count", + "rejected_hypothesis_count", + "tested_hypothesis_count", + "decision_sha256", + "rejection_decision_sha256", + "certificates", + "mappings", + ], + "numerical relation evidence", + )?; + require_json_string_value(object, "abi", ABI, "numerical relation evidence")?; + let requested_mode = RecurrenceRelationDiscoveryMode::parse(json_string( + object, + "requested_mode", + "numerical relation evidence mode", + )?)?; + if requested_mode == RecurrenceRelationDiscoveryMode::Off { + return Err(invalid("off mode cannot carry numerical relation evidence")); + } + let schedule_semantic_digest = evidence_sha256( + object, + "schedule_semantic_digest", + "numerical relation schedule digest", + )?; + let baseline_runtime_layout_digest = evidence_sha256( + object, + "baseline_runtime_layout_digest", + "numerical relation baseline layout digest", + )?; + let source_semantics_sha256 = evidence_sha256( + object, + "source_semantics_sha256", + "numerical relation source semantics", + )?; + let source = parse_raw_source_semantics( + json_field(object, "source_semantics", "numerical relation evidence")?, + &source_semantics_sha256, + &schedule_semantic_digest, + &baseline_runtime_layout_digest, + runtime_parameters, + )?; + let runtime_parameter_schema_sha256 = evidence_sha256( + object, + "runtime_parameter_schema_sha256", + "recurrence runtime parameter schema", + )?; + let runtime_parameter_count = validate_raw_runtime_parameter_schema( + json_field( + object, + "runtime_parameter_schema", + "numerical relation evidence", + )?, + &runtime_parameter_schema_sha256, + runtime_parameters, + )?; + let certificate_algorithm = json_nonempty_string( + object, + "certificate_algorithm", + "numerical relation certificate algorithm", + )? + .to_owned(); + if certificate_algorithm != rusticol_core::recurrence::NUMERICAL_RELATION_CERTIFICATE_ALGORITHM + { + return Err(invalid( + "numerical relation evidence uses an unsupported certificate algorithm", + )); + } + let precision_digits = json_u32(object, "precision_digits", "numerical relation precision")?; + let probe_count = json_u32( + object, + "probe_count", + "numerical relation candidate probe count", + )?; + let verification_probe_count = json_u32( + object, + "verification_probe_count", + "numerical relation verification probe count", + )?; + raw_evidence_geometry_counts( + &source, + probe_count, + verification_probe_count, + runtime_parameter_count, + )?; + let relative_tolerance_hex = json_string( + object, + "relative_tolerance_binary64", + "numerical relation relative tolerance encoding", + )?; + let absolute_tolerance_hex = json_string( + object, + "absolute_tolerance_binary64", + "numerical relation absolute tolerance encoding", + )?; + let (relative_tolerance, relative_exact) = parse_nonnegative_python_f64_hex( + relative_tolerance_hex, + "numerical relation relative tolerance encoding", + )?; + let (absolute_tolerance, absolute_exact) = parse_nonnegative_python_f64_hex( + absolute_tolerance_hex, + "numerical relation absolute tolerance encoding", + )?; + if relative_tolerance == 0.0 && absolute_tolerance == 0.0 { + return Err(RusticolError::integrity( + "numerical relation tolerances cannot both be zero", + )); + } + if json_string( + object, + "relative_tolerance_binary64", + "numerical relation relative tolerance encoding", + )? != python_f64_hex(relative_tolerance) + || json_string( + object, + "absolute_tolerance_binary64", + "numerical relation absolute tolerance encoding", + )? != python_f64_hex(absolute_tolerance) + { + return Err(RusticolError::integrity( + "numerical relation tolerance encoding disagrees with its binary64 value", + )); + } + let seed = json_u64(object, "seed", "numerical relation seed")?; + let capture_authentication_started = Instant::now(); + let candidate = parse_raw_numerical_capture( + bytes, + located_observations.candidate, + json_field(object, "candidate_capture", "numerical relation evidence")?, + "candidate", + "candidate-current-probes-v1", + precision_digits, + probe_count, + &source, + &source_semantics_sha256, + &runtime_parameter_schema_sha256, + runtime_parameter_count, + seed, + )?; + let verification = parse_raw_numerical_capture( + bytes, + located_observations.verification, + json_field( + object, + "verification_capture", + "numerical relation evidence", + )?, + "verification", + "independent-verification-current-probes-v1", + precision_digits, + verification_probe_count, + &source, + &source_semantics_sha256, + &runtime_parameter_schema_sha256, + runtime_parameter_count, + seed, + )?; + validate_independent_raw_captures(&candidate, &verification, runtime_parameters, seed)?; + telemetry.capture_authentication_nanoseconds = + elapsed_nanoseconds(capture_authentication_started); + + let derivation = derive_raw_numerical_relations( + &source, + &candidate, + &verification, + precision_digits, + seed, + &relative_exact, + &absolute_exact, + json_string( + object, + "relative_tolerance_binary64", + "numerical relation relative tolerance encoding", + )?, + json_string( + object, + "absolute_tolerance_binary64", + "numerical relation absolute tolerance encoding", + )?, + &runtime_parameter_schema_sha256, + &certificate_algorithm, + telemetry, + )?; + let evidence_finalization_started = Instant::now(); + let supplied_decision_sha256 = evidence_sha256( + object, + "decision_sha256", + "numerical relation decision chain", + )?; + if supplied_decision_sha256 != derivation.decision_sha256 { + return Err(RusticolError::integrity(format!( + "numerical relation decision digest was not derived from every raw hypothesis: \ + supplied {supplied_decision_sha256}, derived {}", + derivation.decision_sha256 + ))); + } + let supplied_rejection_sha256 = evidence_sha256( + object, + "rejection_decision_sha256", + "numerical relation rejection decision chain", + )?; + if supplied_rejection_sha256 != derivation.rejection_decision_sha256 { + return Err(RusticolError::integrity( + "numerical relation rejection digest was not derived from every rejected raw hypothesis", + )); + } + for (field, expected) in [ + ( + "numerical_candidate_count", + derivation.numerical_candidate_count, + ), + ( + "verification_rejected_count", + derivation.verification_rejected_count, + ), + ( + "tested_hypothesis_count", + derivation.tested_hypothesis_count, + ), + ( + "rejected_hypothesis_count", + derivation.rejected_hypothesis_count, + ), + ] { + if evidence_usize(object, field, "numerical relation evidence counter")? != expected { + return Err(RusticolError::integrity(format!( + "numerical relation evidence {field} was not derived from raw observations" + ))); + } + } + validate_candidate_index_claim( + json_field(object, "candidate_index", "numerical relation evidence")?, + &derivation, + )?; + let supplied_certificates = + json_array(object, "certificates", "numerical relation certificates")?; + let supplied_mappings = json_array(object, "mappings", "numerical relation mappings")?; + let derived_certificates = derivation + .relations + .iter() + .map(|relation| relation.certificate.clone()) + .collect::>(); + let derived_mappings = derivation + .relations + .iter() + .map(|relation| relation.mapping_json.clone()) + .collect::>(); + if supplied_certificates != derived_certificates || supplied_mappings != derived_mappings { + return Err(RusticolError::integrity( + "numerical relation certificates or mappings were not derived from raw observations", + )); + } + let relation_set = serde_json::json!({ + "abi": RELATION_SET_ABI, + "certificates": derived_certificates, + "mappings": derived_mappings, + }); + let actual_certificate_set_sha256 = + canonical_json_sha256(&relation_set, "numerical relation certificate set")?; + let certificate_set_sha256 = evidence_sha256( + object, + "certificate_set_sha256", + "numerical relation certificate set", + )?; + if certificate_set_sha256 != actual_certificate_set_sha256 { + return Err(RusticolError::integrity( + "numerical relation certificate-set digest does not match native raw-evidence replay", + )); + } + let evidence = RecurrenceNumericalRelationEvidence { + requested_mode, + schedule_semantic_digest, + baseline_runtime_layout_digest, + source_semantics_sha256, + runtime_parameter_schema_sha256, + candidate_observation_batch_sha256: candidate.observation_batch_sha256, + verification_observation_batch_sha256: verification.observation_batch_sha256, + decision_sha256: derivation.decision_sha256, + rejection_decision_sha256: derivation.rejection_decision_sha256, + certificate_algorithm, + certificate_set_sha256, + precision_digits, + probe_count, + verification_probe_count, + relative_tolerance, + absolute_tolerance, + seed, + numerical_candidate_count: derivation.numerical_candidate_count, + verification_rejected_count: derivation.verification_rejected_count, + rejected_hypothesis_count: derivation.rejected_hypothesis_count, + tested_hypothesis_count: derivation.tested_hypothesis_count, + mappings: derivation + .relations + .into_iter() + .map(|relation| relation.mapping) + .collect(), + }; + telemetry.evidence_finalization_nanoseconds = + elapsed_nanoseconds(evidence_finalization_started); + Ok(evidence) +} + +fn parse_raw_source_semantics( + value: &JsonValue, + claimed_sha256: &str, + schedule_semantic_digest: &str, + baseline_runtime_layout_digest: &str, + authenticated_process: &AuthenticatedRuntimeParameterContract, +) -> RusticolResult { + const ABI: &str = "pyamplicol-recurrence-numerical-current-source-v3"; + let object = json_object(value, "recurrence numerical source semantics")?; + require_json_fields( + object, + &[ + "abi", + "process_id", + "strategy", + "schedule_semantic_digest", + "baseline_runtime_layout_digest", + "selector_schedule", + "currents", + ], + "recurrence numerical source semantics", + )?; + require_json_string_value(object, "abi", ABI, "recurrence numerical source semantics")?; + if canonical_json_sha256(value, "recurrence numerical source semantics")? != claimed_sha256 { + return Err(RusticolError::integrity( + "recurrence numerical source-semantics digest does not match its raw payload", + )); + } + require_json_string_value( + object, + "schedule_semantic_digest", + schedule_semantic_digest, + "recurrence numerical source semantics", + )?; + require_json_string_value( + object, + "baseline_runtime_layout_digest", + baseline_runtime_layout_digest, + "recurrence numerical source semantics", + )?; + require_json_string_value( + object, + "process_id", + &authenticated_process.process_id, + "recurrence numerical source semantics", + )?; + if authenticated_process.physical_pdgs.is_empty() { + return Err(invalid( + "authenticated recurrence process has no physical external particles", + )); + } + let strategy = + json_nonempty_string(object, "strategy", "recurrence numerical source semantics")? + .to_owned(); + if !matches!( + strategy.as_str(), + "topology-replay" | "all-flow-union" | "contracted-color-union" + ) { + return Err(invalid("raw recurrence source has an unsupported strategy")); + } + let current_values = json_array(object, "currents", "recurrence numerical source currents")?; + let mut currents = Vec::with_capacity(current_values.len()); + for (index, current_value) in current_values.iter().enumerate() { + let context = format!("recurrence numerical source current {index}"); + let current = json_object(current_value, &context)?; + require_json_fields(current, &["current_id", "is_source", "contract"], &context)?; + let current_id = json_u32(current, "current_id", &context)?; + if current_id as usize != index { + return Err(invalid(format!( + "{context} has non-canonical current ID {current_id}" + ))); + } + let is_source = json_field(current, "is_source", &context)? + .as_bool() + .ok_or_else(|| invalid(format!("{context} source flag is not boolean")))?; + let contract = json_field(current, "contract", &context)?.clone(); + let contract_values = contract + .as_array() + .ok_or_else(|| invalid(format!("{context} contract is not an array")))?; + if contract_values.len() != 9 { + return Err(invalid(format!("{context} contract width is not nine"))); + } + let dimension_u64 = contract_values[3] + .as_u64() + .ok_or_else(|| invalid(format!("{context} dimension is not unsigned")))?; + let dimension = u16::try_from(dimension_u64) + .map_err(|_| invalid(format!("{context} dimension exceeds u16")))?; + if dimension == 0 { + return Err(invalid(format!("{context} dimension is zero"))); + } + let selector_domain_id = contract_values[5] + .as_u64() + .and_then(|value| u32::try_from(value).ok()) + .ok_or_else(|| invalid(format!("{context} selector domain is not u32")))?; + currents.push(RawSourceCurrent { + current_id, + is_source, + contract_key: canonical_json_bytes(&contract, &context)?, + dimension, + selector_domain_id, + }); + } + Ok(RawSourceSemantics { + value: value.clone(), + process_id: authenticated_process.process_id.clone(), + physical_pdgs: authenticated_process.physical_pdgs.clone(), + strategy, + selector_schedule: json_field( + object, + "selector_schedule", + "recurrence numerical source semantics", + )? + .clone(), + currents, + }) +} + +fn validate_candidate_index_claim( + value: &JsonValue, + derivation: &RawNumericalDerivation, +) -> RusticolResult<()> { + const ALGORITHM: &str = "complete-contract-anchor-tolerance-window-v1"; + let object = json_object(value, "raw numerical candidate index")?; + require_json_fields( + object, + &[ + "algorithm", + "completeness", + "contract_count", + "exhaustive_fallback_contract_count", + "theoretical_pair_hypothesis_count", + "screened_pair_hypothesis_count", + "zero_hypothesis_count", + "nearest_rejected_scope", + ], + "raw numerical candidate index", + )?; + require_json_string_value( + object, + "algorithm", + ALGORITHM, + "raw numerical candidate index", + )?; + require_json_string_value( + object, + "completeness", + "complete-within-configured-tolerance", + "raw numerical candidate index", + )?; + require_json_string_value( + object, + "nearest_rejected_scope", + "zero-and-tolerance-window-screened-hypotheses", + "raw numerical candidate index", + )?; + for (field, expected) in [ + ("contract_count", derivation.candidate_index_contract_count), + ( + "exhaustive_fallback_contract_count", + derivation.exhaustive_fallback_contract_count, + ), + ( + "theoretical_pair_hypothesis_count", + derivation.theoretical_pair_hypothesis_count, + ), + ( + "screened_pair_hypothesis_count", + derivation.screened_pair_hypothesis_count, + ), + ("zero_hypothesis_count", derivation.zero_hypothesis_count), + ] { + if evidence_usize(object, field, "raw numerical candidate index")? != expected { + return Err(RusticolError::integrity(format!( + "raw numerical candidate index {field} was not independently derived" + ))); + } + } + Ok(()) +} + +fn raw_evidence_geometry_counts( + source: &RawSourceSemantics, + probe_count: u32, + verification_probe_count: u32, + runtime_parameter_count: usize, +) -> RusticolResult<(usize, usize, usize, usize)> { + let current_count = source.currents.len(); + let row_count = source + .currents + .len() + .checked_mul(2) + .and_then(|count| count.checked_add(runtime_parameter_count)) + .ok_or_else(|| invalid("raw numerical row count overflows usize"))?; + let component_count = source.currents.iter().try_fold(0_usize, |total, current| { + total + .checked_add(usize::from(current.dimension)) + .ok_or_else(|| invalid("raw numerical component count overflows usize")) + })?; + let point_count = usize::try_from(probe_count) + .ok() + .and_then(|candidate| { + usize::try_from(verification_probe_count) + .ok() + .and_then(|verification| candidate.checked_add(verification)) + }) + .ok_or_else(|| invalid("raw numerical probe count overflows usize"))?; + let observation_scalar_count = component_count + .checked_mul(point_count) + .and_then(|count| count.checked_mul(2)) + .ok_or_else(|| invalid("raw numerical scalar count overflows usize"))?; + let parameter_scalar_count = runtime_parameter_count + .checked_mul(point_count) + .ok_or_else(|| invalid("raw parameter scalar count overflows usize"))?; + let scalar_count = observation_scalar_count + .checked_add(parameter_scalar_count) + .ok_or_else(|| invalid("raw total scalar count overflows usize"))?; + Ok((current_count, component_count, scalar_count, row_count)) +} + +fn validate_raw_runtime_parameter_schema( + value: &JsonValue, + claimed_sha256: &str, + expected: &AuthenticatedRuntimeParameterContract, +) -> RusticolResult { + if value != &expected.schema || claimed_sha256 != expected.schema_sha256 { + return Err(RusticolError::integrity( + "recurrence runtime parameter schema/defaults do not match the authenticated native template input", + )); + } + Ok(expected.slots.len()) +} + +fn canonical_json_sha256_with_raw_arrays( + value: &JsonValue, + raw_arrays: &[(&str, &[u8])], + context: &str, +) -> RusticolResult { + let canonical = canonical_json_bytes(value, context)?; + let mut replacements = Vec::with_capacity(raw_arrays.len()); + let mut names = BTreeSet::new(); + for (placeholder, raw_array) in raw_arrays { + if !names.insert(*placeholder) { + return Err(invalid(format!( + "{context} raw-array placeholder is duplicated" + ))); + } + let encoded_placeholder = canonical_json_bytes( + &serde_json::json!([placeholder]), + &format!("{context} raw-array placeholder"), + )?; + let mut matches = canonical + .windows(encoded_placeholder.len()) + .enumerate() + .filter_map(|(index, window)| (window == encoded_placeholder).then_some(index)); + let start = matches + .next() + .ok_or_else(|| invalid(format!("{context} has no raw-array placeholder")))?; + if matches.next().is_some() { + return Err(invalid(format!( + "{context} raw-array placeholder is ambiguous" + ))); + } + let end = start + .checked_add(encoded_placeholder.len()) + .ok_or_else(|| invalid(format!("{context} placeholder range overflows usize")))?; + replacements.push((start, end, *raw_array)); + } + replacements.sort_by_key(|replacement| replacement.0); + let mut digest = Sha256::new(); + let mut cursor = 0; + for (start, end, raw_array) in replacements { + if start < cursor { + return Err(invalid(format!("{context} raw-array placeholders overlap"))); + } + digest.update(&canonical[cursor..start]); + digest.update(raw_array); + cursor = end; + } + digest.update(&canonical[cursor..]); + Ok(hex_digest(digest.finalize())) +} + +#[allow(clippy::too_many_arguments)] +fn observation_batch_sha256_from_raw( + raw_bytes: &[u8], + observation_array: RawByteRange, + source_semantics_sha256: &str, + runtime_parameter_schema_sha256: &str, + point_sha256s: &[String], + selector_context_sha256s: &[String], + parameter_context_sha256s: &[String], + batch_abi: &str, + context: &str, +) -> RusticolResult { + const PLACEHOLDER: &str = "__pyamplicol_authenticated_raw_array_placeholder__"; + let range = observation_array.as_usize(raw_bytes.len(), context)?; + let raw_array = raw_bytes + .get(range) + .ok_or_else(|| invalid(format!("{context} raw observation array is absent")))?; + canonical_json_sha256_with_raw_arrays( + &serde_json::json!({ + "abi": batch_abi, + "source_semantics_sha256": source_semantics_sha256, + "runtime_parameter_schema_sha256": runtime_parameter_schema_sha256, + "point_sha256s": point_sha256s, + "selector_context_sha256s": selector_context_sha256s, + "parameter_context_sha256s": parameter_context_sha256s, + "currents": [PLACEHOLDER], + }), + &[(PLACEHOLDER, raw_array)], + context, + ) +} + +#[allow(clippy::too_many_arguments)] +fn parse_raw_numerical_capture<'a>( + raw_bytes: &'a [u8], + observation_array: RawByteRange, + value: &JsonValue, + label: &str, + domain: &str, + precision_digits: u32, + expected_point_count: u32, + source: &RawSourceSemantics, + source_semantics_sha256: &str, + runtime_parameter_schema_sha256: &str, + runtime_parameter_count: usize, + seed: u64, +) -> RusticolResult> { + const ABI: &str = "pyamplicol-recurrence-current-observation-capture-v2"; + const BATCH_ABI: &str = "pyamplicol-recurrence-current-observation-batch-v2"; + const PARAMETER_CONTEXT_ABI: &str = "pyamplicol-recurrence-parameter-context-v1"; + let context = format!("{label} raw numerical capture"); + let object = json_object(value, &context)?; + require_json_fields( + object, + &[ + "abi", + "precision_digits", + "point_count", + "point_sha256s", + "kinematic_sha256s", + "parameter_contexts", + "parameter_context_sha256s", + "context_sha256s", + "points", + "current_count", + "runtime_parameter_schema_sha256", + "source_semantics_sha256", + "observation_batch_sha256", + "capture_contract_sha256", + "context_policy", + "complete_current_components", + "point_major", + "current_dimensions_sha256", + "evaluator", + "kinematic_binary64", + "selector_contexts", + "current_dimensions", + "observations", + ], + &context, + )?; + require_json_string_value(object, "abi", ABI, &context)?; + if json_u32(object, "precision_digits", &context)? != precision_digits + || json_u32(object, "point_count", &context)? != expected_point_count + || expected_point_count < 2 + { + return Err(invalid(format!("{context} probe geometry is inconsistent"))); + } + require_json_string_value( + object, + "runtime_parameter_schema_sha256", + runtime_parameter_schema_sha256, + &context, + )?; + require_json_string_value( + object, + "source_semantics_sha256", + source_semantics_sha256, + &context, + )?; + require_json_string_value( + object, + "evaluator", + "recurrence-direct-plan-decimal-symbolica-exact", + &context, + )?; + if !json_bool(object, "complete_current_components", &context)? + || !json_bool(object, "point_major", &context)? + { + return Err(invalid(format!( + "{context} does not contain complete point-major current components" + ))); + } + let expected_policy = match source.strategy.as_str() { + "topology-replay" => "seeded-replay-target-per-physical-point-v1", + "all-flow-union" => "seeded-helicity-per-selector-domain-and-physical-point-v1", + "contracted-color-union" => "fixed-contracted-source-schedule-v1", + _ => return Err(invalid("raw recurrence source strategy is unsupported")), + }; + require_json_string_value(object, "context_policy", expected_policy, &context)?; + + let point_values = json_array(object, "points", &context)?; + let point_sha256s = parse_sha256_array(object, "point_sha256s", &context)?; + let kinematic_values = json_array(object, "kinematic_binary64", &context)?; + let kinematic_sha256s = parse_sha256_array(object, "kinematic_sha256s", &context)?; + let selector_contexts = json_array(object, "selector_contexts", &context)?; + let selector_context_sha256s = parse_sha256_array(object, "context_sha256s", &context)?; + let parameter_values = json_array(object, "parameter_contexts", &context)?; + let parameter_context_sha256s = + parse_sha256_array(object, "parameter_context_sha256s", &context)?; + let point_count = expected_point_count as usize; + if [ + point_values.len(), + point_sha256s.len(), + kinematic_values.len(), + kinematic_sha256s.len(), + selector_contexts.len(), + selector_context_sha256s.len(), + parameter_values.len(), + parameter_context_sha256s.len(), + ] + .into_iter() + .any(|count| count != point_count) + { + return Err(invalid(format!( + "{context} does not cover every ordered probe slot" + ))); + } + if point_sha256s.iter().collect::>().len() != point_count + || kinematic_sha256s.iter().collect::>().len() != point_count + { + return Err(invalid(format!( + "{context} physical probes are not distinct" + ))); + } + + let mut parameter_contexts = Vec::with_capacity(point_count); + for point_index in 0..point_count { + let point_context = format!("{context} point {point_index}"); + let actual_point_sha256 = canonical_json_sha256( + &point_values[point_index], + &format!("{point_context} record"), + )?; + if point_sha256s[point_index] != actual_point_sha256 { + return Err(RusticolError::integrity(format!( + "{point_context} digest does not match its raw point" + ))); + } + validate_raw_kinematic( + &point_values[point_index], + &kinematic_values[point_index], + source, + domain_seed(seed, domain, point_index), + &point_context, + )?; + let actual_kinematic_sha256 = canonical_json_sha256( + &kinematic_values[point_index], + &format!("{point_context} binary64 kinematics"), + )?; + if kinematic_sha256s[point_index] != actual_kinematic_sha256 { + return Err(RusticolError::integrity(format!( + "{point_context} kinematic digest does not match its raw binary64 values" + ))); + } + let expected_selector = expected_selector_context(source, seed, domain, point_index)?; + if selector_contexts[point_index] != expected_selector + || selector_context_sha256s[point_index] + != canonical_json_sha256(&expected_selector, &format!("{point_context} selector"))? + { + return Err(RusticolError::integrity(format!( + "{point_context} selector context is stale or non-deterministic" + ))); + } + let raw_parameters = parameter_values[point_index] + .as_array() + .ok_or_else(|| invalid(format!("{point_context} parameters are not an array")))?; + if raw_parameters.len() != runtime_parameter_count { + return Err(invalid(format!( + "{point_context} parameters do not cover every runtime slot" + ))); + } + let parsed_parameters = raw_parameters + .iter() + .enumerate() + .map(|(slot, value)| { + let text = value.as_str().ok_or_else(|| { + invalid(format!("{point_context} parameter {slot} is not a string")) + })?; + parse_canonical_decimal(text, &format!("{point_context} parameter {slot}")) + }) + .collect::>>()?; + let parameter_payload = serde_json::json!({ + "abi": PARAMETER_CONTEXT_ABI, + "domain": domain, + "point_index": point_index, + "point_sha256": point_sha256s[point_index], + "runtime_parameter_schema_sha256": runtime_parameter_schema_sha256, + "values": raw_parameters, + }); + if parameter_context_sha256s[point_index] + != canonical_json_sha256( + ¶meter_payload, + &format!("{point_context} parameter context"), + )? + { + return Err(RusticolError::integrity(format!( + "{point_context} parameter-context digest does not match its raw values" + ))); + } + parameter_contexts.push(parsed_parameters); + } + + let dimension_values = json_array(object, "current_dimensions", &context)?; + if dimension_values.len() != source.currents.len() + || json_u32(object, "current_count", &context)? as usize != source.currents.len() + { + return Err(invalid(format!( + "{context} current census does not match source semantics" + ))); + } + let observation_rows = + scan_raw_observation_array(raw_bytes, observation_array, source, point_count, label)?; + let mut dimensions = Vec::with_capacity(source.currents.len()); + for (index, source_current) in source.currents.iter().enumerate() { + let row_context = format!("{context} current {index}"); + let dimension_row = json_object(&dimension_values[index], &row_context)?; + require_json_fields(dimension_row, &["current_id", "dimension"], &row_context)?; + let current_id = json_u32(dimension_row, "current_id", &row_context)?; + let dimension = json_u32(dimension_row, "dimension", &row_context)?; + if current_id != source_current.current_id + || dimension != u32::from(source_current.dimension) + { + return Err(invalid(format!( + "{row_context} dimension or ID disagrees with source semantics" + ))); + } + dimensions.push(source_current.dimension); + } + let dimensions_object = dimensions + .iter() + .enumerate() + .map(|(current_id, dimension)| { + ( + current_id.to_string(), + JsonValue::from(u64::from(*dimension)), + ) + }) + .collect::>(); + if evidence_sha256(object, "current_dimensions_sha256", &context)? + != canonical_json_sha256( + &JsonValue::Object(dimensions_object), + &format!("{context} dimensions"), + )? + { + return Err(RusticolError::integrity(format!( + "{context} dimension digest does not match the raw census" + ))); + } + let observation_batch_sha256 = evidence_sha256(object, "observation_batch_sha256", &context)?; + if observation_batch_sha256 + != observation_batch_sha256_from_raw( + raw_bytes, + observation_array, + source_semantics_sha256, + runtime_parameter_schema_sha256, + &point_sha256s, + &selector_context_sha256s, + ¶meter_context_sha256s, + BATCH_ABI, + &format!("{context} observation batch"), + )? + { + return Err(RusticolError::integrity(format!( + "{context} observation-batch digest does not match raw observations" + ))); + } + let capture_contract_sha256 = evidence_sha256(object, "capture_contract_sha256", &context)?; + let capture_payload = serde_json::json!({ + "abi": ABI, + "precision_digits": precision_digits, + "source_semantics_sha256": source_semantics_sha256, + "runtime_parameter_schema_sha256": runtime_parameter_schema_sha256, + "point_sha256s": point_sha256s, + "kinematic_sha256s": kinematic_sha256s, + "selector_context_sha256s": selector_context_sha256s, + "parameter_context_sha256s": parameter_context_sha256s, + "current_dimensions": dimension_values, + "observation_batch_sha256": observation_batch_sha256, + "context_policy": expected_policy, + }); + if capture_contract_sha256 + != canonical_json_sha256(&capture_payload, &format!("{context} contract"))? + { + return Err(RusticolError::integrity(format!( + "{context} contract digest does not match raw capture" + ))); + } + Ok(RawNumericalCapture { + raw_bytes, + observation_rows, + point_count, + point_sha256s, + kinematic_sha256s, + parameter_context_sha256s, + parameter_contexts, + dimensions, + observation_batch_sha256, + capture_contract_sha256, + }) +} + +fn parse_sha256_array( + object: &JsonMap, + field: &str, + context: &str, +) -> RusticolResult> { + json_array(object, field, context)? + .iter() + .enumerate() + .map(|(index, value)| { + let text = value + .as_str() + .ok_or_else(|| invalid(format!("{context} {field}[{index}] is not a string")))?; + semantic_digest_from_hex(text, &format!("{context} {field}[{index}]"))?; + Ok(text.to_owned()) + }) + .collect() +} + +fn validate_raw_kinematic( + point: &JsonValue, + kinematic: &JsonValue, + source: &RawSourceSemantics, + expected_seed: u64, + context: &str, +) -> RusticolResult<()> { + let point_object = json_object(point, &format!("{context} point record"))?; + require_json_fields( + point_object, + &[ + "schema_version", + "kind", + "process_id", + "process", + "seed", + "available", + "error", + "points", + ], + &format!("{context} point record"), + )?; + if json_u32(point_object, "schema_version", context)? != 1 + || json_string(point_object, "kind", context)? != "pyamplicol-rusticol-validation-momenta" + || !json_bool(point_object, "available", context)? + || !json_field(point_object, "error", context)?.is_null() + { + return Err(invalid(format!( + "{context} validation point is unavailable" + ))); + } + require_json_string_value(point_object, "process_id", &source.process_id, context)?; + json_nonempty_string(point_object, "process", context)?; + if json_u64(point_object, "seed", context)? != expected_seed { + return Err(RusticolError::integrity(format!( + "{context} validation-point seed is stale" + ))); + } + let point_sets = json_array(point_object, "points", context)?; + if point_sets.len() != 1 { + return Err(invalid(format!( + "{context} validation point must contain one momentum set" + ))); + } + let particles = point_sets[0] + .as_array() + .ok_or_else(|| invalid(format!("{context} particle set is not an array")))?; + let kinematic_rows = kinematic + .as_array() + .ok_or_else(|| invalid(format!("{context} kinematics are not an array")))?; + if particles.len() != kinematic_rows.len() + || particles.len() != source.physical_pdgs.len() + || particles.is_empty() + { + return Err(invalid(format!( + "{context} point, process, and binary64 kinematic widths disagree" + ))); + } + for (particle_index, ((particle, raw_row), expected_pdg)) in particles + .iter() + .zip(kinematic_rows) + .zip(&source.physical_pdgs) + .enumerate() + { + let particle_context = format!("{context} particle {particle_index}"); + let particle = json_object(particle, &particle_context)?; + require_json_fields(particle, &["pdg", "momentum"], &particle_context)?; + let actual_pdg = particle + .get("pdg") + .and_then(JsonValue::as_i64) + .and_then(|value| i32::try_from(value).ok()) + .ok_or_else(|| invalid(format!("{particle_context} PDG is not an i32")))?; + if actual_pdg != *expected_pdg { + return Err(RusticolError::integrity(format!( + "{particle_context} PDG does not match the authenticated recurrence process" + ))); + } + let momentum = json_array(particle, "momentum", &particle_context)?; + let binary = raw_row + .as_array() + .filter(|row| row.len() == 4) + .ok_or_else(|| { + invalid(format!( + "{particle_context} binary64 momentum is not four-wide" + )) + })?; + if momentum.len() != 4 { + return Err(invalid(format!( + "{particle_context} decimal momentum is not four-wide" + ))); + } + for component in 0..4 { + let decimal = momentum[component].as_str().ok_or_else(|| { + invalid(format!( + "{particle_context} momentum component {component} is not a string" + )) + })?; + parse_canonical_decimal( + decimal, + &format!("{particle_context} momentum component {component}"), + )?; + let binary_value = decimal.parse::().map_err(|_| { + invalid(format!( + "{particle_context} momentum component {component} is not binary64" + )) + })?; + if !binary_value.is_finite() + || binary[component].as_str() != Some(python_f64_hex_signed(binary_value).as_str()) + { + return Err(RusticolError::integrity(format!( + "{particle_context} decimal and binary64 momentum disagree at component {component}" + ))); + } + } + } + Ok(()) +} + +fn all_flow_selector_domain_helicities( + source: &RawSourceSemantics, +) -> RusticolResult>> { + let schedule = json_object( + &source.selector_schedule, + "recurrence numerical selector schedule", + )?; + require_json_fields( + schedule, + &["policy", "resolved_helicities"], + "recurrence union selector schedule", + )?; + require_json_string_value( + schedule, + "policy", + "seeded-helicity-per-selector-domain-and-physical-point-v1", + "recurrence union selector schedule", + )?; + let helicities = json_array( + schedule, + "resolved_helicities", + "recurrence union selector schedule", + )?; + let mut by_domain = BTreeMap::>::new(); + for (index, value) in helicities.iter().enumerate() { + let helicity = json_object(value, "recurrence resolved helicity")?; + require_json_fields( + helicity, + &["helicity_index", "helicity_id", "selector_domain_id"], + "recurrence resolved helicity", + )?; + if json_u32(helicity, "helicity_index", "recurrence resolved helicity")? as usize != index { + return Err(invalid( + "recurrence resolved-helicity index is not canonical", + )); + } + by_domain + .entry(json_u32( + helicity, + "selector_domain_id", + "recurrence resolved helicity", + )?) + .or_default() + .push(( + index, + json_u32(helicity, "helicity_id", "recurrence resolved helicity")?, + )); + } + if by_domain.is_empty() { + return Err(invalid("recurrence union selector schedule is empty")); + } + Ok(by_domain) +} + +fn expected_selector_context( + source: &RawSourceSemantics, + seed: u64, + domain: &str, + point_index: usize, +) -> RusticolResult { + let selector_seed = domain_seed(seed, domain, point_index); + match source.strategy.as_str() { + "topology-replay" => { + let schedule = json_object( + &source.selector_schedule, + "recurrence numerical selector schedule", + )?; + require_json_fields( + schedule, + &["policy", "replay_targets"], + "recurrence topology selector schedule", + )?; + require_json_string_value( + schedule, + "policy", + "seeded-replay-target-per-physical-point-v1", + "recurrence topology selector schedule", + )?; + let targets = json_array( + schedule, + "replay_targets", + "recurrence topology selector schedule", + )?; + if targets.is_empty() { + return Err(invalid("recurrence topology selector schedule is empty")); + } + let target_index = selector_seed as usize % targets.len(); + let target = json_object(&targets[target_index], "recurrence replay target")?; + require_json_fields( + target, + &[ + "target_index", + "public_flow_id", + "representative_id", + "selector_domain_id", + ], + "recurrence replay target", + )?; + if json_u32(target, "target_index", "recurrence replay target")? as usize + != target_index + { + return Err(invalid("recurrence replay target index is not canonical")); + } + Ok(serde_json::json!({ + "strategy": source.strategy, + "target_index": target_index, + "public_flow_id": json_u32(target, "public_flow_id", "recurrence replay target")?, + "representative_id": json_u32(target, "representative_id", "recurrence replay target")?, + "selector_domain_id": json_u32(target, "selector_domain_id", "recurrence replay target")?, + })) + } + "all-flow-union" => { + let selected = all_flow_selector_domain_helicities(source)? + .into_iter() + .map(|(selector_domain_id, rows)| { + let (helicity_index, helicity_id) = rows[selector_seed as usize % rows.len()]; + serde_json::json!({ + "selector_domain_id": selector_domain_id, + "helicity_index": helicity_index, + "helicity_id": helicity_id, + }) + }) + .collect::>(); + Ok(serde_json::json!({ + "strategy": source.strategy, + "selector_domain_helicities": selected, + })) + } + "contracted-color-union" => { + let schedule = json_object( + &source.selector_schedule, + "recurrence numerical selector schedule", + )?; + require_json_fields( + schedule, + &["policy", "fixed_source_schedule"], + "recurrence contracted selector schedule", + )?; + require_json_string_value( + schedule, + "policy", + "fixed-contracted-source-schedule-v1", + "recurrence contracted selector schedule", + )?; + if !json_bool( + schedule, + "fixed_source_schedule", + "recurrence contracted selector schedule", + )? { + return Err(invalid( + "recurrence contracted source schedule is not fixed", + )); + } + Ok(serde_json::json!({ + "strategy": source.strategy, + "fixed_source_schedule": true, + })) + } + _ => Err(invalid("recurrence selector strategy is unsupported")), + } +} + +fn validate_independent_raw_captures( + candidate: &RawNumericalCapture<'_>, + verification: &RawNumericalCapture<'_>, + runtime_parameters: &AuthenticatedRuntimeParameterContract, + seed: u64, +) -> RusticolResult<()> { + if candidate.dimensions != verification.dimensions + || candidate.observation_rows.len() != verification.observation_rows.len() + || !sets_are_disjoint(&candidate.point_sha256s, &verification.point_sha256s) + || !sets_are_disjoint( + &candidate.kinematic_sha256s, + &verification.kinematic_sha256s, + ) + || !sets_are_disjoint( + &candidate.parameter_context_sha256s, + &verification.parameter_context_sha256s, + ) + { + return Err(RusticolError::integrity( + "candidate and verification raw captures are not independent", + )); + } + let expected_candidate = deterministic_parameter_contexts( + &runtime_parameters.slots, + candidate.parameter_contexts.len(), + seed, + "candidate-current-parameter-probes-v1", + true, + )?; + let expected_verification = deterministic_parameter_contexts( + &runtime_parameters.slots, + verification.parameter_contexts.len(), + seed, + "independent-verification-current-parameter-probes-v1", + false, + )?; + if candidate.parameter_contexts != expected_candidate + || verification.parameter_contexts != expected_verification + { + return Err(RusticolError::integrity( + "raw recurrence parameter contexts are stale or non-deterministic", + )); + } + Ok(()) +} + +fn sets_are_disjoint(left: &[String], right: &[String]) -> bool { + let left = left.iter().collect::>(); + right.iter().all(|value| !left.contains(value)) +} + +fn deterministic_parameter_contexts( + slots: &[AuthenticatedRuntimeParameterSlot], + count: usize, + seed: u64, + domain: &str, + include_defaults: bool, +) -> RusticolResult>> { + (0..count) + .map(|probe_index| { + if include_defaults && probe_index == 0 { + return Ok(slots.iter().map(|slot| slot.default.clone()).collect()); + } + slots + .iter() + .enumerate() + .map(|(parameter_index, slot)| { + if slot.probe_policy == "derived-overwritten-fixed-zero-v1" { + return Ok(slot.default.clone()); + } + let digest = Sha256::digest( + format!("{seed}:{domain}:{probe_index}:{parameter_index}").as_bytes(), + ); + let unsigned = u64::from_be_bytes( + digest[..8] + .try_into() + .map_err(|_| invalid("parameter digest width is not eight"))?, + ); + let signed = i128::from(unsigned) - (1_i128 << 63); + let signed = if signed == 0 { 1 } else { signed }; + let scale = slot.default.abs().max(BigRational::one()); + Ok(&slot.default + + scale * BigRational::new(BigInt::from(signed), BigInt::one() << 67_usize)) + }) + .collect() + }) + .collect() +} + +fn domain_seed(seed: u64, domain: &str, index: usize) -> u64 { + let digest = Sha256::digest(format!("{seed}:{domain}:{index}").as_bytes()); + u64::from_be_bytes( + digest[..8] + .try_into() + .expect("SHA-256 prefix is eight bytes"), + ) +} + +fn validate_canonical_decimal_text<'a>( + value: &'a str, + context: &str, +) -> RusticolResult<(&'a str, &'a str, bool)> { + if value.is_empty() || value.starts_with('+') || value == "-0" || value.contains(['e', 'E']) { + return Err(invalid(format!( + "{context} is not a canonical finite decimal" + ))); + } + let (negative, unsigned) = value + .strip_prefix('-') + .map_or((false, value), |rest| (true, rest)); + let (integer, fraction) = unsigned + .split_once('.') + .map_or((unsigned, None), |(integer, fraction)| { + (integer, Some(fraction)) + }); + if integer.is_empty() + || !integer.bytes().all(|byte| byte.is_ascii_digit()) + || integer.len() > 1 && integer.starts_with('0') + || fraction.is_some_and(|fraction| { + fraction.is_empty() + || !fraction.bytes().all(|byte| byte.is_ascii_digit()) + || fraction.ends_with('0') + }) + { + return Err(invalid(format!( + "{context} is not a canonical finite decimal" + ))); + } + let fraction = fraction.unwrap_or(""); + if negative && integer == "0" && fraction.bytes().all(|byte| byte == b'0') { + return Err(invalid(format!("{context} is negative zero"))); + } + Ok((integer, fraction, negative)) +} + +fn parse_canonical_decimal(value: &str, context: &str) -> RusticolResult { + let (integer, fraction, negative) = validate_canonical_decimal_text(value, context)?; + let digits = format!("{integer}{fraction}"); + let mut numerator = BigInt::from_str(&digits) + .map_err(|_| invalid(format!("{context} decimal numerator is invalid")))?; + if negative { + numerator = -numerator; + } + let denominator = BigInt::from(10_u8).pow( + u32::try_from(fraction.len()) + .map_err(|_| invalid(format!("{context} decimal scale exceeds u32")))?, + ); + Ok(BigRational::new(numerator, denominator)) +} + +fn exact_binary64_rational(value: f64) -> RusticolResult { + if !value.is_finite() { + return Err(invalid("binary64 value is not finite")); + } + if value == 0.0 { + return Ok(BigRational::zero()); + } + let bits = value.to_bits(); + let negative = bits >> 63 != 0; + let exponent_bits = ((bits >> 52) & 0x7ff) as i32; + let fraction = bits & ((1_u64 << 52) - 1); + let (significand, exponent) = if exponent_bits == 0 { + (fraction, -1074) + } else { + ((1_u64 << 52) | fraction, exponent_bits - 1023 - 52) + }; + let mut numerator = BigInt::from(significand); + let mut denominator = BigInt::one(); + if exponent >= 0 { + numerator <<= exponent as usize; + } else { + denominator <<= (-exponent) as usize; + } + if negative { + numerator = -numerator; + } + Ok(BigRational::new(numerator, denominator)) +} + +fn parse_nonnegative_python_f64_hex( + text: &str, + context: &str, +) -> RusticolResult<(f64, BigRational)> { + if text.starts_with('-') || text.len() > 24 { + return Err(invalid(format!( + "{context} is not a canonical nonnegative finite binary64 encoding" + ))); + } + let (mantissa, exponent_text) = text.split_once('p').ok_or_else(|| { + invalid(format!( + "{context} is not a canonical nonnegative finite binary64 encoding" + )) + })?; + let (leading, fraction_text) = mantissa + .strip_prefix("0x") + .and_then(|value| value.split_once('.')) + .ok_or_else(|| { + invalid(format!( + "{context} is not a canonical nonnegative finite binary64 encoding" + )) + })?; + if fraction_text.is_empty() + || fraction_text.len() > 13 + || !fraction_text.bytes().all(|byte| byte.is_ascii_hexdigit()) + || !matches!(leading, "0" | "1") + { + return Err(invalid(format!( + "{context} is not a canonical nonnegative finite binary64 encoding" + ))); + } + let exponent = i32::from_str(exponent_text).map_err(|_| { + invalid(format!( + "{context} is not a canonical nonnegative finite binary64 encoding" + )) + })?; + let parsed_fraction = u64::from_str_radix(fraction_text, 16).map_err(|_| { + invalid(format!( + "{context} is not a canonical nonnegative finite binary64 encoding" + )) + })?; + let padding = 4 * (13 - fraction_text.len()); + let fraction = parsed_fraction + .checked_shl( + u32::try_from(padding) + .map_err(|_| invalid(format!("{context} fraction padding is invalid")))?, + ) + .ok_or_else(|| invalid(format!("{context} fraction is too wide")))?; + let bits = match leading { + "1" if (-1022..=1023).contains(&exponent) => { + let exponent_bits = u64::try_from(exponent + 1023) + .map_err(|_| invalid(format!("{context} exponent is invalid")))?; + (exponent_bits << 52) | fraction + } + "0" if exponent == -1022 && fraction != 0 => fraction, + "0" if exponent == 0 && fraction == 0 => 0, + _ => { + return Err(invalid(format!( + "{context} is not a canonical nonnegative finite binary64 encoding" + ))); + } + }; + let value = f64::from_bits(bits); + if !value.is_finite() || python_f64_hex(value) != text { + return Err(invalid(format!( + "{context} is not a canonical nonnegative finite binary64 encoding" + ))); + } + let exact = exact_binary64_rational(value)?; + Ok((value, exact)) +} + +fn python_f64_hex_signed(value: f64) -> String { + let sign = if value.is_sign_negative() { "-" } else { "" }; + format!("{sign}{}", python_f64_hex(value.abs())) +} + +fn raw_observation_row<'a>( + capture: &'a RawNumericalCapture<'_>, + current_id: u32, + context: &str, +) -> RusticolResult<&'a RawObservationRow> { + capture + .observation_rows + .get(current_id as usize) + .ok_or_else(|| invalid(format!("{context} row is absent"))) +} + +fn visit_raw_observation_text<'a, F>( + capture: &RawNumericalCapture<'a>, + current_id: u32, + context: &str, + mut visit: F, +) -> RusticolResult<()> +where + F: FnMut(usize, &'a str, &'a str) -> RusticolResult<()>, +{ + let row = raw_observation_row(capture, current_id, context)?; + let values_range = row.values.as_usize(capture.raw_bytes.len(), context)?; + let values = capture + .raw_bytes + .get(values_range.clone()) + .ok_or_else(|| invalid(format!("{context} values are absent")))?; + let dimension = capture + .dimensions + .get(current_id as usize) + .copied() + .ok_or_else(|| invalid(format!("{context} dimension is absent")))?; + let expected_width = capture + .point_count + .checked_mul(usize::from(dimension)) + .ok_or_else(|| invalid(format!("{context} width overflows usize")))?; + let mut cursor = CanonicalObservationCursor { + bytes: values, + position: 0, + absolute_start: values_range.start, + }; + cursor.expect(b"[", context)?; + for index in 0..expected_width { + if index != 0 { + cursor.expect(b",", context)?; + } + cursor.expect(b"[", context)?; + let real = cursor.decimal_string(&format!("{context} component {index} real part"))?; + cursor.expect(b",", context)?; + let imaginary = + cursor.decimal_string(&format!("{context} component {index} imaginary part"))?; + cursor.expect(b"]", context)?; + visit(index, real, imaginary)?; + } + cursor.expect(b"]", context)?; + if cursor.position != cursor.bytes.len() { + return Err(invalid(format!( + "{context} has trailing or extra observation values" + ))); + } + Ok(()) +} + +fn parse_raw_observation_values( + capture: &RawNumericalCapture<'_>, + current_id: u32, + context: &str, +) -> RusticolResult> { + let dimension = capture + .dimensions + .get(current_id as usize) + .copied() + .ok_or_else(|| invalid(format!("{context} dimension is absent")))?; + let width = capture + .point_count + .checked_mul(usize::from(dimension)) + .ok_or_else(|| invalid(format!("{context} width overflows usize")))?; + let mut values = Vec::with_capacity(width); + visit_raw_observation_text(capture, current_id, context, |index, real, imaginary| { + values.push(( + parse_canonical_decimal(real, &format!("{context} component {index} real part"))?, + parse_canonical_decimal( + imaginary, + &format!("{context} component {index} imaginary part"), + )?, + )); + Ok(()) + })?; + Ok(values) +} + +#[cfg(test)] +fn raw_observation_value( + capture: &RawNumericalCapture<'_>, + current_id: u32, + observation_index: usize, + context: &str, +) -> RusticolResult { + let mut selected = None; + visit_raw_observation_text(capture, current_id, context, |index, real, imaginary| { + if index == observation_index { + selected = Some(( + parse_canonical_decimal(real, &format!("{context} selected real part"))?, + parse_canonical_decimal(imaginary, &format!("{context} selected imaginary part"))?, + )); + } + Ok(()) + })?; + selected.ok_or_else(|| invalid(format!("{context} selected observation is absent"))) +} + +fn raw_observation_values_bytes<'a>( + capture: &RawNumericalCapture<'a>, + current_id: u32, + context: &str, +) -> RusticolResult<&'a [u8]> { + let row = raw_observation_row(capture, current_id, context)?; + let range = row.values.as_usize(capture.raw_bytes.len(), context)?; + capture + .raw_bytes + .get(range) + .ok_or_else(|| invalid(format!("{context} values are absent"))) +} + +fn exact_probe_scalar(value: &ExactProbeComplex, scalar_component: usize) -> &BigRational { + if scalar_component == 0 { + &value.0 + } else { + &value.1 + } +} + +fn build_raw_numerical_candidate_indexes( + source: &RawSourceSemantics, + candidate: &RawNumericalCapture<'_>, + telemetry: &mut RecurrenceEvidenceAuthenticationTelemetry, +) -> RusticolResult, RawNumericalCandidateIndex>> { + let mut members_by_contract = BTreeMap::, Vec>::new(); + for current in &source.currents { + members_by_contract + .entry(current.contract_key.clone()) + .or_default() + .push(current.current_id); + } + members_by_contract + .into_iter() + .map(|(contract, members)| { + let first_id = members + .first() + .copied() + .ok_or_else(|| invalid("raw numerical candidate index has no members"))?; + let dimension = candidate + .dimensions + .get(first_id as usize) + .copied() + .ok_or_else(|| invalid("raw numerical candidate index dimension is absent"))?; + let width = candidate + .point_count + .checked_mul(usize::from(dimension)) + .ok_or_else(|| invalid("raw numerical candidate index width overflows usize"))?; + let scalar_width = width + .checked_mul(2) + .ok_or_else(|| invalid("raw numerical candidate scalar width overflows usize"))?; + let mut scalar_columns = (0..scalar_width) + .map(|_| Vec::<&str>::with_capacity(members.len())) + .collect::>(); + for current_id in &members { + if candidate.dimensions.get(*current_id as usize) != Some(&dimension) { + return Err(invalid( + "raw numerical candidate index has inconsistent widths", + )); + } + visit_raw_observation_text( + candidate, + *current_id, + "raw numerical candidate index", + |index, real, imaginary| { + scalar_columns[index * 2].push(real); + scalar_columns[index * 2 + 1].push(imaginary); + Ok(()) + }, + )?; + RecurrenceEvidenceAuthenticationTelemetry::checked_add( + &mut telemetry.candidate_index_observation_row_scan_count, + 1, + "evidence-authentication candidate-index row-scan count", + )?; + } + let mut best_choice = (0_usize, 0_usize); + let mut best_score = (0_usize, 0_usize); + let mut sorted_column = Vec::<&str>::with_capacity(members.len()); + for observation_index in 0..width { + for scalar_component in 0..2 { + let column = &scalar_columns[observation_index * 2 + scalar_component]; + sorted_column.clear(); + sorted_column.extend_from_slice(column); + sorted_column.sort_unstable(); + let unique = usize::from(!sorted_column.is_empty()) + + sorted_column + .windows(2) + .filter(|pair| pair[0] != pair[1]) + .count(); + let nonzero = sorted_column.iter().filter(|value| **value != "0").count(); + let score = (unique, nonzero); + if score > best_score + || score == best_score + && (observation_index, scalar_component) < best_choice + { + best_score = score; + best_choice = (observation_index, scalar_component); + } + } + } + let selected_column_start = best_choice + .0 + .checked_mul(2) + .ok_or_else(|| invalid("raw numerical selected column overflows usize"))?; + let real_column = &scalar_columns[selected_column_start]; + let imaginary_column = &scalar_columns[selected_column_start + 1]; + if real_column.len() != members.len() || imaginary_column.len() != members.len() { + return Err(invalid( + "raw numerical selected observation column is incomplete", + )); + } + let selected_values = members + .iter() + .zip(real_column.iter().zip(imaginary_column.iter())) + .map(|(current_id, (real, imaginary))| { + Ok(( + *current_id, + ( + parse_canonical_decimal( + real, + "raw numerical candidate index selected real part", + )?, + parse_canonical_decimal( + imaginary, + "raw numerical candidate index selected imaginary part", + )?, + ), + )) + }) + .collect::>>()?; + RecurrenceEvidenceAuthenticationTelemetry::checked_add( + &mut telemetry.retained_selected_observation_count, + selected_values.len(), + "evidence-authentication retained selected-observation count", + )?; + let mut entries = selected_values + .iter() + .map(|(current_id, value)| { + ( + exact_probe_scalar(value, best_choice.1).clone(), + *current_id, + ) + }) + .collect::>(); + RecurrenceEvidenceAuthenticationTelemetry::checked_add( + &mut telemetry.selected_observation_reuse_count, + selected_values.len(), + "evidence-authentication selected-observation reuse count", + )?; + entries.sort(); + Ok(( + contract, + RawNumericalCandidateIndex { + #[cfg(test)] + observation_index: best_choice.0, + scalar_component: best_choice.1, + entries, + selected_values, + }, + )) + }) + .collect() +} + +fn raw_numerical_tolerance_window_ids( + index: &RawNumericalCandidateIndex, + selected: &ExactProbeComplex, + current_id: u32, + relation_kind: &str, + relative_tolerance: &BigRational, + absolute_tolerance: &BigRational, +) -> RusticolResult> { + if !matches!(relation_kind, "equal" | "opposite") { + return Err(invalid( + "raw numerical candidate index relation is unsupported", + )); + } + if relative_tolerance >= &BigRational::one() { + return Ok(index + .entries + .iter() + .filter_map(|(_value, representative_id)| { + (*representative_id < current_id).then_some(*representative_id) + }) + .collect()); + } + let selected_scalar = exact_probe_scalar(selected, index.scalar_component); + let target = if relation_kind == "equal" { + selected_scalar.clone() + } else { + -selected_scalar + }; + // For complex-pair infinity norms X (current), Y (representative), and D + // (residual), passing gives D <= a+r*max(X,Y) and reverse triangle gives + // Y <= X+D. Hence r<1 implies D <= (a+rX)/(1-r); the indexed scalar + // residual is <= D, so this window cannot discard a passing relation. + let current_scale = selected.0.abs().max(selected.1.abs()); + let radius = (absolute_tolerance + relative_tolerance * current_scale) + / (BigRational::one() - relative_tolerance); + let lower_value = &target - &radius; + let upper_value = target + radius; + let lower = index + .entries + .partition_point(|(value, _current_id)| value < &lower_value); + let upper = index + .entries + .partition_point(|(value, _current_id)| value <= &upper_value); + Ok(index.entries[lower..upper] + .iter() + .filter_map(|(_value, representative_id)| { + (*representative_id < current_id).then_some(*representative_id) + }) + .collect()) +} + +#[allow(clippy::too_many_arguments)] +fn derive_raw_numerical_relations( + source: &RawSourceSemantics, + candidate: &RawNumericalCapture<'_>, + verification: &RawNumericalCapture<'_>, + precision_digits: u32, + seed: u64, + relative_tolerance: &BigRational, + absolute_tolerance: &BigRational, + relative_tolerance_hex: &str, + absolute_tolerance_hex: &str, + runtime_parameter_schema_sha256: &str, + certificate_algorithm: &str, + telemetry: &mut RecurrenceEvidenceAuthenticationTelemetry, +) -> RusticolResult { + const DECISION_CHAIN_ABI: &str = "pyamplicol-recurrence-numerical-decision-chain-v1"; + const REJECTION_CHAIN_ABI: &str = "pyamplicol-recurrence-rejected-numerical-decision-chain-v1"; + let candidate_index_started = Instant::now(); + let candidate_indexes = build_raw_numerical_candidate_indexes(source, candidate, telemetry)?; + telemetry.candidate_index_nanoseconds = elapsed_nanoseconds(candidate_index_started); + let mut prior_by_contract = BTreeMap::, Vec>::new(); + let mut derived = Vec::new(); + let mut numerical_candidate_count = 0_usize; + let mut verification_rejected_count = 0_usize; + let mut tested_hypothesis_count = 0_usize; + let mut theoretical_pair_hypothesis_count = 0_usize; + let mut screened_pair_hypothesis_count = 0_usize; + let mut zero_hypothesis_count = 0_usize; + let suppressed_selector_domains = if source.strategy == "all-flow-union" { + all_flow_selector_domain_helicities(source)? + .into_iter() + .filter_map(|(domain_id, helicities)| (helicities.len() > 1).then_some(domain_id)) + .collect::>() + } else { + BTreeSet::new() + }; + let source_semantics_sha256 = + canonical_json_sha256(&source.value, "raw numerical source semantics")?; + let mut decision_chain = canonical_json_sha256( + &serde_json::json!({ + "abi": DECISION_CHAIN_ABI, + "source_semantics_sha256": source_semantics_sha256, + "candidate_capture_sha256": candidate.capture_contract_sha256, + "verification_capture_sha256": verification.capture_contract_sha256, + }), + "raw numerical decision-chain root", + )?; + let mut rejection_chain = canonical_json_sha256( + &serde_json::json!({ + "abi": REJECTION_CHAIN_ABI, + "source_semantics_sha256": source_semantics_sha256, + "candidate_capture_sha256": candidate.capture_contract_sha256, + "verification_capture_sha256": verification.capture_contract_sha256, + }), + "raw numerical rejection-chain root", + )?; + let hypothesis_replay_started = Instant::now(); + for current in &source.currents { + let prior = prior_by_contract + .entry(current.contract_key.clone()) + .or_default(); + if current.is_source { + prior.push(current.current_id); + continue; + } + if suppressed_selector_domains.contains(¤t.selector_domain_id) { + continue; + } + let candidate_index = candidate_indexes + .get(¤t.contract_key) + .ok_or_else(|| invalid("raw numerical candidate index contract is absent"))?; + let selected = candidate_index + .selected_value(current.current_id, "raw numerical candidate index current")?; + RecurrenceEvidenceAuthenticationTelemetry::checked_add( + &mut telemetry.selected_observation_reuse_count, + 1, + "evidence-authentication selected-observation reuse count", + )?; + let equal_representatives = raw_numerical_tolerance_window_ids( + candidate_index, + selected, + current.current_id, + "equal", + relative_tolerance, + absolute_tolerance, + )?; + let opposite_representatives = raw_numerical_tolerance_window_ids( + candidate_index, + selected, + current.current_id, + "opposite", + relative_tolerance, + absolute_tolerance, + )?; + theoretical_pair_hypothesis_count = theoretical_pair_hypothesis_count + .checked_add( + prior + .len() + .checked_mul(2) + .ok_or_else(|| invalid("theoretical pair count overflows usize"))?, + ) + .ok_or_else(|| invalid("theoretical pair count overflows usize"))?; + screened_pair_hypothesis_count = screened_pair_hypothesis_count + .checked_add(equal_representatives.len()) + .and_then(|count| count.checked_add(opposite_representatives.len())) + .ok_or_else(|| invalid("screened pair count overflows usize"))?; + zero_hypothesis_count = zero_hypothesis_count + .checked_add(1) + .ok_or_else(|| invalid("zero hypothesis count overflows usize"))?; + screened_pair_hypothesis_count + .checked_add(zero_hypothesis_count) + .ok_or_else(|| invalid("total screened hypothesis count overflows usize"))?; + let mut hypotheses = vec![("zero", None)]; + for representative_id in equal_representatives + .union(&opposite_representatives) + .copied() + { + if equal_representatives.contains(&representative_id) { + hypotheses.push(("equal", Some(representative_id))); + } + if opposite_representatives.contains(&representative_id) { + hypotheses.push(("opposite", Some(representative_id))); + } + } + for (relation_kind, representative_id) in hypotheses { + tested_hypothesis_count = tested_hypothesis_count + .checked_add(1) + .ok_or_else(|| invalid("numerical hypothesis count overflows usize"))?; + let candidate_residuals = relation_residuals( + relation_kind, + current.current_id, + representative_id, + candidate, + relative_tolerance, + absolute_tolerance, + telemetry, + )?; + if candidate_residuals.maximum_ratio > BigRational::one() { + decision_chain = advance_decision_chain( + &decision_chain, + current.current_id, + representative_id, + relation_kind, + &candidate_residuals, + None, + false, + telemetry, + )?; + rejection_chain = advance_rejection_chain( + &rejection_chain, + current.current_id, + representative_id, + relation_kind, + &candidate_residuals, + "candidate-observations-not-equal", + telemetry, + )?; + continue; + } + numerical_candidate_count = numerical_candidate_count + .checked_add(1) + .ok_or_else(|| invalid("numerical candidate count overflows usize"))?; + let verification_residuals = relation_residuals( + relation_kind, + current.current_id, + representative_id, + verification, + relative_tolerance, + absolute_tolerance, + telemetry, + )?; + if verification_residuals.maximum_ratio > BigRational::one() { + verification_rejected_count = verification_rejected_count + .checked_add(1) + .ok_or_else(|| invalid("verification rejection count overflows usize"))?; + decision_chain = advance_decision_chain( + &decision_chain, + current.current_id, + representative_id, + relation_kind, + &candidate_residuals, + Some(&verification_residuals), + false, + telemetry, + )?; + rejection_chain = advance_rejection_chain( + &rejection_chain, + current.current_id, + representative_id, + relation_kind, + &verification_residuals, + "independent-verification-rejected-candidate", + telemetry, + )?; + continue; + } + let execution_representative_id = if relation_kind == "zero" { + prior.first().copied().unwrap_or(current.current_id) + } else { + representative_id + .ok_or_else(|| invalid("non-zero numerical relation has no representative"))? + }; + derived.push(build_derived_numerical_relation( + source, + current, + representative_id, + execution_representative_id, + relation_kind, + candidate, + verification, + &candidate_residuals, + &verification_residuals, + precision_digits, + seed, + relative_tolerance_hex, + absolute_tolerance_hex, + runtime_parameter_schema_sha256, + certificate_algorithm, + telemetry, + )?); + decision_chain = advance_decision_chain( + &decision_chain, + current.current_id, + representative_id, + relation_kind, + &candidate_residuals, + Some(&verification_residuals), + true, + telemetry, + )?; + break; + } + prior.push(current.current_id); + } + telemetry.hypothesis_replay_nanoseconds = elapsed_nanoseconds(hypothesis_replay_started); + let derivation_finalization_started = Instant::now(); + let rejected_hypothesis_count = tested_hypothesis_count + .checked_sub(derived.len()) + .ok_or_else(|| invalid("raw numerical rejected-hypothesis count underflows"))?; + let decision_sha256 = canonical_json_sha256( + &serde_json::json!({ + "abi": DECISION_CHAIN_ABI, + "chain_tail_sha256": decision_chain, + "tested_hypothesis_count": tested_hypothesis_count, + "theoretical_pair_hypothesis_count": theoretical_pair_hypothesis_count, + "screened_pair_hypothesis_count": screened_pair_hypothesis_count, + "zero_hypothesis_count": zero_hypothesis_count, + "numerical_candidate_count": numerical_candidate_count, + "verification_rejected_count": verification_rejected_count, + "rejected_hypothesis_count": rejected_hypothesis_count, + "certified_relation_count": derived.len(), + }), + "raw numerical decision-chain census", + )?; + let rejection_decision_sha256 = rejection_chain; + telemetry.finish_rational_string_census()?; + telemetry.derivation_finalization_nanoseconds = + elapsed_nanoseconds(derivation_finalization_started); + Ok(RawNumericalDerivation { + relations: derived, + numerical_candidate_count, + verification_rejected_count, + rejected_hypothesis_count, + tested_hypothesis_count, + theoretical_pair_hypothesis_count, + screened_pair_hypothesis_count, + zero_hypothesis_count, + candidate_index_contract_count: candidate_indexes.len(), + exhaustive_fallback_contract_count: if relative_tolerance >= &BigRational::one() { + candidate_indexes.len() + } else { + 0 + }, + decision_sha256, + rejection_decision_sha256, + }) +} + +#[allow(clippy::too_many_arguments)] +fn advance_decision_chain( + previous: &str, + current_id: u32, + representative_id: Option, + relation_kind: &str, + candidate: &RelationResiduals, + verification: Option<&RelationResiduals>, + selected: bool, + telemetry: &mut RecurrenceEvidenceAuthenticationTelemetry, +) -> RusticolResult { + telemetry.record_rational_string_uses(if verification.is_some() { 6 } else { 3 })?; + let row = serde_json::json!({ + "current_id": current_id, + "representative_id": representative_id, + "relation_kind": relation_kind, + "candidate_maximum_absolute_residual": candidate.strings.maximum_absolute.as_str(), + "candidate_maximum_relative_residual": candidate.strings.maximum_relative.as_str(), + "candidate_maximum_tolerance_ratio": candidate.strings.maximum_ratio.as_str(), + "candidate_accepted": candidate.maximum_ratio <= BigRational::one(), + "verification_maximum_absolute_residual": verification.map(|value| value.strings.maximum_absolute.as_str()), + "verification_maximum_relative_residual": verification.map(|value| value.strings.maximum_relative.as_str()), + "verification_maximum_tolerance_ratio": verification.map(|value| value.strings.maximum_ratio.as_str()), + "verification_accepted": verification.map(|value| value.maximum_ratio <= BigRational::one()), + "selected": selected, + }); + let previous = semantic_digest_from_hex(previous, "raw numerical decision-chain tail")?; + let mut digest = Sha256::new(); + digest.update(previous.as_bytes()); + digest.update(canonical_json_bytes(&row, "raw numerical decision row")?); + Ok(hex_digest(digest.finalize())) +} + +fn advance_rejection_chain( + previous: &str, + current_id: u32, + representative_id: Option, + relation_kind: &str, + residuals: &RelationResiduals, + reason: &str, + telemetry: &mut RecurrenceEvidenceAuthenticationTelemetry, +) -> RusticolResult { + telemetry.record_rational_string_uses(3)?; + let row = serde_json::json!({ + "current_id": current_id, + "representative_id": representative_id, + "relation_kind": relation_kind, + "reason": reason, + "maximum_absolute_residual": residuals.strings.maximum_absolute.as_str(), + "maximum_relative_residual": residuals.strings.maximum_relative.as_str(), + "maximum_tolerance_ratio": residuals.strings.maximum_ratio.as_str(), + }); + let previous = semantic_digest_from_hex(previous, "raw numerical rejection-chain predecessor")?; + let mut digest = Sha256::new(); + digest.update(previous.as_bytes()); + digest.update(canonical_json_bytes( + &row, + "raw numerical rejected hypothesis", + )?); + Ok(hex_digest(digest.finalize())) +} + +fn relation_residuals( + relation_kind: &str, + current_id: u32, + representative_id: Option, + capture: &RawNumericalCapture<'_>, + relative_tolerance: &BigRational, + absolute_tolerance: &BigRational, + telemetry: &mut RecurrenceEvidenceAuthenticationTelemetry, +) -> RusticolResult { + let mut maximum_absolute = BigRational::zero(); + let mut maximum_relative = BigRational::zero(); + let mut maximum_ratio = BigRational::zero(); + if relation_kind == "zero" { + if representative_id.is_some() { + return Err(invalid("raw numerical zero relation has a representative")); + } + visit_raw_observation_text( + capture, + current_id, + "raw numerical current observations", + |index, real, imaginary| { + let current_value = ( + parse_canonical_decimal( + real, + &format!("raw numerical current observations component {index} real part"), + )?, + parse_canonical_decimal( + imaginary, + &format!( + "raw numerical current observations component {index} imaginary part" + ), + )?, + ); + accumulate_relation_residual( + relation_kind, + ¤t_value, + None, + relative_tolerance, + absolute_tolerance, + &mut maximum_absolute, + &mut maximum_relative, + &mut maximum_ratio, + ) + }, + )?; + let streamed_value_count = capture + .dimensions + .get(current_id as usize) + .copied() + .and_then(|dimension| capture.point_count.checked_mul(usize::from(dimension))) + .ok_or_else(|| invalid("raw numerical zero-residual width overflows usize"))?; + RecurrenceEvidenceAuthenticationTelemetry::checked_add( + &mut telemetry.zero_residual_streamed_value_count, + streamed_value_count, + "evidence-authentication zero-residual streamed-value count", + )?; + } else { + let current = parse_raw_observation_values( + capture, + current_id, + "raw numerical current observations", + )?; + let representative_id = representative_id + .ok_or_else(|| invalid("raw numerical non-zero relation has no representative"))?; + let representative = parse_raw_observation_values( + capture, + representative_id, + "raw numerical representative observations", + )?; + RecurrenceEvidenceAuthenticationTelemetry::checked_add( + &mut telemetry.materialized_observation_vector_count, + 2, + "evidence-authentication materialized observation-vector count", + )?; + if representative.len() != current.len() { + return Err(invalid("raw numerical relation width is invalid")); + } + for (current_value, representative_value) in current.iter().zip(&representative) { + accumulate_relation_residual( + relation_kind, + current_value, + Some(representative_value), + relative_tolerance, + absolute_tolerance, + &mut maximum_absolute, + &mut maximum_relative, + &mut maximum_ratio, + )?; + } + } + let strings = RelationResidualStrings { + maximum_absolute: rational_string(&maximum_absolute), + maximum_relative: rational_string(&maximum_relative), + maximum_ratio: rational_string(&maximum_ratio), + }; + RecurrenceEvidenceAuthenticationTelemetry::checked_add( + &mut telemetry.rational_string_materialization_count, + 3, + "evidence-authentication rational-string materialization count", + )?; + Ok(RelationResiduals { + maximum_ratio, + strings, + }) +} + +#[allow(clippy::too_many_arguments)] +fn accumulate_relation_residual( + relation_kind: &str, + current_value: &ExactProbeComplex, + representative_value: Option<&ExactProbeComplex>, + relative_tolerance: &BigRational, + absolute_tolerance: &BigRational, + maximum_absolute: &mut BigRational, + maximum_relative: &mut BigRational, + maximum_ratio: &mut BigRational, +) -> RusticolResult<()> { + let current_scale = current_value.0.abs().max(current_value.1.abs()); + let (difference, scale) = match relation_kind { + "zero" => { + if representative_value.is_some() { + return Err(invalid("raw numerical zero relation has a representative")); + } + (current_scale.clone(), current_scale) + } + "equal" | "opposite" => { + let representative_value = representative_value + .ok_or_else(|| invalid("raw numerical non-zero relation has no representative"))?; + let (right_real, right_imaginary) = if relation_kind == "equal" { + ( + representative_value.0.clone(), + representative_value.1.clone(), + ) + } else { + ( + -representative_value.0.clone(), + -representative_value.1.clone(), + ) + }; + ( + (¤t_value.0 - right_real) + .abs() + .max((¤t_value.1 - right_imaginary).abs()), + current_scale + .max(representative_value.0.abs()) + .max(representative_value.1.abs()), + ) + } + _ => return Err(invalid("raw numerical relation kind is unsupported")), + }; + let allowed = absolute_tolerance + relative_tolerance * &scale; + let (relative, ratio) = if difference.is_zero() { + (BigRational::zero(), BigRational::zero()) + } else { + if allowed <= BigRational::zero() { + return Err(invalid("raw numerical tolerance is not positive")); + } + ( + &difference / scale.max(absolute_tolerance.clone()), + &difference / allowed, + ) + }; + *maximum_absolute = std::mem::take(maximum_absolute).max(difference); + *maximum_relative = std::mem::take(maximum_relative).max(relative); + *maximum_ratio = std::mem::take(maximum_ratio).max(ratio); + Ok(()) +} + +#[allow(clippy::too_many_arguments)] +fn build_derived_numerical_relation( + source: &RawSourceSemantics, + current: &RawSourceCurrent, + representative_id: Option, + execution_representative_id: u32, + relation_kind: &str, + candidate: &RawNumericalCapture<'_>, + verification: &RawNumericalCapture<'_>, + candidate_residuals: &RelationResiduals, + verification_residuals: &RelationResiduals, + precision_digits: u32, + seed: u64, + relative_tolerance_hex: &str, + absolute_tolerance_hex: &str, + runtime_parameter_schema_sha256: &str, + certificate_algorithm: &str, + telemetry: &mut RecurrenceEvidenceAuthenticationTelemetry, +) -> RusticolResult { + let factor = match relation_kind { + "equal" => rusticol_core::recurrence::ExactComplexRational::ONE, + "opposite" => rusticol_core::recurrence::ExactComplexRational::ONE.checked_neg()?, + "zero" => rusticol_core::recurrence::ExactComplexRational::ZERO, + _ => return Err(invalid("raw numerical relation kind is unsupported")), + }; + let factor_integer = match relation_kind { + "equal" => serde_json::json!([1, 0]), + "opposite" => serde_json::json!([-1, 0]), + "zero" => serde_json::json!([0, 0]), + _ => unreachable!(), + }; + let candidate_observations_sha256 = relation_observation_sha256( + current.current_id, + representative_id, + relation_kind, + candidate, + )?; + let verification_observations_sha256 = relation_observation_sha256( + current.current_id, + representative_id, + relation_kind, + verification, + )?; + let probe_contract = serde_json::json!({ + "algorithm": certificate_algorithm, + "source_semantics_sha256": canonical_json_sha256( + &source.value, + "raw numerical source semantics", + )?, + "current_id": current.current_id, + "representative_id": representative_id, + "execution_representative_id": execution_representative_id, + "relation_kind": relation_kind, + "precision_digits": precision_digits, + "seed": seed, + "candidate_domain": "candidate-current-probes-v1", + "verification_domain": "independent-verification-current-probes-v1", + "relative_tolerance_binary64": relative_tolerance_hex, + "absolute_tolerance_binary64": absolute_tolerance_hex, + "candidate_probe_count": candidate.point_sha256s.len(), + "verification_probe_count": verification.point_sha256s.len(), + "current_dimension": current.dimension, + "runtime_parameter_schema_sha256": runtime_parameter_schema_sha256, + "candidate_capture_sha256": candidate.capture_contract_sha256, + "verification_capture_sha256": verification.capture_contract_sha256, + "candidate_observations_sha256": candidate_observations_sha256, + "verification_observations_sha256": verification_observations_sha256, + }); + let probe_contract_sha256 = + canonical_json_sha256(&probe_contract, "raw numerical probe contract")?; + telemetry.record_rational_string_uses(12)?; + let proof = serde_json::json!({ + "algorithm": certificate_algorithm, + "source_semantics_sha256": probe_contract["source_semantics_sha256"], + "current_id": current.current_id, + "representative_id": representative_id, + "execution_representative_id": execution_representative_id, + "relation_kind": relation_kind, + "precision_digits": precision_digits, + "seed": seed, + "candidate_domain": "candidate-current-probes-v1", + "verification_domain": "independent-verification-current-probes-v1", + "relative_tolerance_binary64": relative_tolerance_hex, + "absolute_tolerance_binary64": absolute_tolerance_hex, + "candidate_probe_count": candidate.point_sha256s.len(), + "verification_probe_count": verification.point_sha256s.len(), + "current_dimension": current.dimension, + "runtime_parameter_schema_sha256": runtime_parameter_schema_sha256, + "candidate_capture_sha256": candidate.capture_contract_sha256, + "verification_capture_sha256": verification.capture_contract_sha256, + "candidate_observations_sha256": candidate_observations_sha256, + "verification_observations_sha256": verification_observations_sha256, + "proof_kind": "authenticated-numerical", + "factor_integer": factor_integer, + "candidate_maximum_absolute_residual": candidate_residuals.strings.maximum_absolute.as_str(), + "candidate_maximum_relative_residual": candidate_residuals.strings.maximum_relative.as_str(), + "candidate_maximum_tolerance_ratio": candidate_residuals.strings.maximum_ratio.as_str(), + "verification_maximum_absolute_residual": verification_residuals.strings.maximum_absolute.as_str(), + "verification_maximum_relative_residual": verification_residuals.strings.maximum_relative.as_str(), + "verification_maximum_tolerance_ratio": verification_residuals.strings.maximum_ratio.as_str(), + "probe_contract_sha256": probe_contract_sha256, + }); + let proof_sha256 = canonical_json_sha256(&proof, "raw numerical relation proof")?; + let certificate = serde_json::json!({ + "algorithm": certificate_algorithm, + "proof_kind": "authenticated-numerical", + "relation_kind": relation_kind, + "current_id": current.current_id, + "representative_id": representative_id, + "execution_representative_id": execution_representative_id, + "factor_integer": factor_integer, + "source_semantics_sha256": probe_contract["source_semantics_sha256"], + "precision_digits": precision_digits, + "seed": seed, + "relative_tolerance_binary64": relative_tolerance_hex, + "absolute_tolerance_binary64": absolute_tolerance_hex, + "candidate_probe_count": candidate.point_sha256s.len(), + "verification_probe_count": verification.point_sha256s.len(), + "current_dimension": current.dimension, + "runtime_parameter_schema_sha256": runtime_parameter_schema_sha256, + "candidate_capture_sha256": candidate.capture_contract_sha256, + "verification_capture_sha256": verification.capture_contract_sha256, + "candidate_maximum_absolute_residual": candidate_residuals.strings.maximum_absolute.as_str(), + "candidate_maximum_relative_residual": candidate_residuals.strings.maximum_relative.as_str(), + "candidate_maximum_tolerance_ratio": candidate_residuals.strings.maximum_ratio.as_str(), + "verification_maximum_absolute_residual": verification_residuals.strings.maximum_absolute.as_str(), + "verification_maximum_relative_residual": verification_residuals.strings.maximum_relative.as_str(), + "verification_maximum_tolerance_ratio": verification_residuals.strings.maximum_ratio.as_str(), + "candidate_observations_sha256": candidate_observations_sha256, + "verification_observations_sha256": verification_observations_sha256, + "probe_contract_sha256": probe_contract_sha256, + "proof_sha256": proof_sha256, + }); + let mapping_json = serde_json::json!({ + "current_id": current.current_id, + "representative_id": representative_id, + "execution_representative_id": execution_representative_id, + "relation_kind": relation_kind, + "factor_integer": factor_integer, + "current_dimension": current.dimension, + "certificate_proof_sha256": proof_sha256, + "candidate_observations_sha256": candidate_observations_sha256, + "verification_observations_sha256": verification_observations_sha256, + }); + Ok(DerivedNumericalRelation { + mapping: RecurrenceNumericalCurrentMapping { + current_id: current.current_id, + representative_id, + execution_representative_id, + relation_kind: relation_kind.to_owned(), + factor, + current_dimension: current.dimension, + certificate_proof_sha256: proof_sha256, + candidate_observations_sha256, + verification_observations_sha256, + }, + certificate, + mapping_json, + }) +} + +fn relation_observation_sha256( + current_id: u32, + representative_id: Option, + relation_kind: &str, + capture: &RawNumericalCapture<'_>, +) -> RusticolResult { + const CURRENT_PLACEHOLDER: &str = "__pyamplicol_authenticated_current_values_placeholder__"; + const REPRESENTATIVE_PLACEHOLDER: &str = + "__pyamplicol_authenticated_representative_values_placeholder__"; + let current_values = + raw_observation_values_bytes(capture, current_id, "raw relation current observations")?; + let current_dimension = capture + .dimensions + .get(current_id as usize) + .copied() + .ok_or_else(|| invalid("raw relation current dimension is absent"))?; + let (representative_values, representative_raw) = + if let Some(representative_id) = representative_id { + ( + serde_json::json!([REPRESENTATIVE_PLACEHOLDER]), + Some(raw_observation_values_bytes( + capture, + representative_id, + "raw relation representative observations", + )?), + ) + } else { + (JsonValue::Null, None) + }; + let payload = serde_json::json!({ + "abi": "pyamplicol-recurrence-relation-observation-v2", + "capture_contract_sha256": capture.capture_contract_sha256, + "current_id": current_id, + "representative_id": representative_id, + "relation_kind": relation_kind, + "current_dimension": current_dimension, + "current_values": [CURRENT_PLACEHOLDER], + "representative_values": representative_values, + }); + let mut replacements = vec![(CURRENT_PLACEHOLDER, current_values)]; + if let Some(representative_raw) = representative_raw { + replacements.push((REPRESENTATIVE_PLACEHOLDER, representative_raw)); + } + canonical_json_sha256_with_raw_arrays( + &payload, + &replacements, + "raw numerical relation observations", + ) +} + +fn rational_string(value: &BigRational) -> String { + if value.denom().is_one() { + value.numer().to_string() + } else { + format!("{}/{}", value.numer(), value.denom()) + } +} + +fn evidence_sha256( + object: &JsonMap, + field: &str, + context: &str, +) -> RusticolResult { + let value = json_string(object, field, context)?; + semantic_digest_from_hex(value, context)?; + Ok(value.to_owned()) +} + +fn evidence_usize( + object: &JsonMap, + field: &str, + context: &str, +) -> RusticolResult { + json_field(object, field, context)? + .as_u64() + .and_then(|value| usize::try_from(value).ok()) + .ok_or_else(|| invalid(format!("{context} must be a nonnegative usize"))) +} + +fn json_u64( + object: &JsonMap, + field: &str, + context: &str, +) -> RusticolResult { + json_field(object, field, context)? + .as_u64() + .ok_or_else(|| invalid(format!("{context} must be a nonnegative u64"))) +} + +fn python_f64_hex(value: f64) -> String { + let bits = value.to_bits(); + if bits & 0x7fff_ffff_ffff_ffff == 0 { + return "0x0.0p+0".to_owned(); + } + let exponent_bits = ((bits >> 52) & 0x7ff) as i32; + let fraction = bits & ((1_u64 << 52) - 1); + let (leading, exponent) = if exponent_bits == 0 { + (0, -1022) + } else { + (1, exponent_bits - 1023) + }; + format!("0x{leading}.{fraction:013x}p{exponent:+}") +} + +#[cfg(test)] +mod tests { + use super::*; + use flate2::{Compression, write::ZlibEncoder}; + use rusticol_core::recurrence::RecurrenceRelationDiscoveryOptions; + use serde_json::json; + + fn digest(seed: u8) -> String { + format!("{seed:02x}").repeat(32) + } + + fn empty_runtime_parameter_contract() -> AuthenticatedRuntimeParameterContract { + let schema = json!({ + "abi": "pyamplicol-recurrence-runtime-parameter-schema-v2", + "parameters": [], + }); + AuthenticatedRuntimeParameterContract { + schema_sha256: canonical_json_sha256(&schema, "empty runtime parameter schema") + .unwrap(), + schema, + slots: Vec::new(), + process_id: "raw-v3-test".to_owned(), + physical_pdgs: vec![1], + } + } + + fn parse_fixture(bytes: &[u8]) -> RusticolResult { + parse_numerical_relation_evidence(bytes, &empty_runtime_parameter_contract()) + } + + fn scanner_source(metadata_bytes: &[u8]) -> (JsonValue, RawSourceSemantics) { + let metadata: JsonValue = serde_json::from_slice(metadata_bytes).unwrap(); + let object = metadata.as_object().unwrap(); + let source_semantics_sha256 = object["source_semantics_sha256"].as_str().unwrap(); + let source = parse_raw_source_semantics( + &object["source_semantics"], + source_semantics_sha256, + object["schedule_semantic_digest"].as_str().unwrap(), + object["baseline_runtime_layout_digest"].as_str().unwrap(), + &empty_runtime_parameter_contract(), + ) + .unwrap(); + (metadata, source) + } + + fn capture_from_observation_array<'a>( + encoded: &'a [u8], + source: &RawSourceSemantics, + point_count: usize, + ) -> RawNumericalCapture<'a> { + let observation_array = + RawByteRange::from_usize(0, encoded.len(), encoded.len(), "test observation array") + .unwrap(); + let observation_rows = + scan_raw_observation_array(encoded, observation_array, source, point_count, "test") + .unwrap(); + RawNumericalCapture { + raw_bytes: encoded, + observation_rows, + point_count, + point_sha256s: Vec::new(), + kinematic_sha256s: Vec::new(), + parameter_context_sha256s: Vec::new(), + parameter_contexts: Vec::new(), + dimensions: source + .currents + .iter() + .map(|current| current.dimension) + .collect(), + observation_batch_sha256: digest(80), + capture_contract_sha256: digest(81), + } + } + + fn refresh_numerical_certificate_set_digest(value: &mut JsonValue) { + let object = value.as_object().unwrap(); + let relation_set = json!({ + "abi": "pyamplicol-authenticated-numerical-current-relation-set-v2", + "certificates": object["certificates"].clone(), + "mappings": object["mappings"].clone(), + }); + let digest = + canonical_json_sha256(&relation_set, "test numerical relation certificate set") + .unwrap(); + value + .as_object_mut() + .unwrap() + .insert("certificate_set_sha256".to_owned(), json!(digest)); + } + + fn raw_capture_fixture( + domain: &str, + point_base: u32, + source_semantics_sha256: &str, + runtime_parameter_schema_sha256: &str, + seed: u64, + ) -> JsonValue { + let points = (0..4_u32) + .map(|index| { + let value = point_base + index; + json!({ + "schema_version": 1, + "kind": "pyamplicol-rusticol-validation-momenta", + "process_id": "raw-v3-test", + "process": "1 -> 1", + "seed": domain_seed(seed, domain, index as usize), + "available": true, + "error": null, + "points": [[{ + "pdg": 1, + "momentum": [value.to_string(), "0", "0", "0"], + }]], + }) + }) + .collect::>(); + let point_sha256s = points + .iter() + .map(|point| canonical_json_sha256(point, "fixture point").unwrap()) + .collect::>(); + let kinematic_binary64 = (0..4_u32) + .map(|index| { + json!([[ + python_f64_hex(f64::from(point_base + index)), + "0x0.0p+0", + "0x0.0p+0", + "0x0.0p+0", + ]]) + }) + .collect::>(); + let kinematic_sha256s = kinematic_binary64 + .iter() + .map(|row| canonical_json_sha256(row, "fixture kinematics").unwrap()) + .collect::>(); + let selector_contexts = (0..4) + .map(|_| { + json!({ + "strategy": "contracted-color-union", + "fixed_source_schedule": true, + }) + }) + .collect::>(); + let selector_context_sha256s = selector_contexts + .iter() + .map(|row| canonical_json_sha256(row, "fixture selector").unwrap()) + .collect::>(); + let parameter_contexts = vec![json!([]); 4]; + let parameter_context_sha256s = (0..4) + .map(|point_index| { + canonical_json_sha256( + &json!({ + "abi": "pyamplicol-recurrence-parameter-context-v1", + "domain": domain, + "point_index": point_index, + "point_sha256": point_sha256s[point_index], + "runtime_parameter_schema_sha256": runtime_parameter_schema_sha256, + "values": [], + }), + "fixture parameter context", + ) + .unwrap() + }) + .collect::>(); + let observation_scalars = [ + [1_i64, 1, 1, 1], + [2, 3, 4, 5], + [2, 3, 4, 5], + [-2, -3, -4, -5], + [0, 0, 0, 0], + [7, 8, 9, 11], + ]; + let observations = observation_scalars + .iter() + .enumerate() + .map(|(current_id, values)| { + json!({ + "current_id": current_id, + "dimension": 1, + "values": values + .iter() + .map(|value| json!([value.to_string(), "0"])) + .collect::>(), + }) + }) + .collect::>(); + let dimensions = (0..observation_scalars.len()) + .map(|current_id| json!({"current_id": current_id, "dimension": 1})) + .collect::>(); + let observation_batch_sha256 = canonical_json_sha256( + &json!({ + "abi": "pyamplicol-recurrence-current-observation-batch-v2", + "source_semantics_sha256": source_semantics_sha256, + "runtime_parameter_schema_sha256": runtime_parameter_schema_sha256, + "point_sha256s": point_sha256s, + "selector_context_sha256s": selector_context_sha256s, + "parameter_context_sha256s": parameter_context_sha256s, + "currents": observations, + }), + "fixture observation batch", + ) + .unwrap(); + let capture_contract_sha256 = canonical_json_sha256( + &json!({ + "abi": "pyamplicol-recurrence-current-observation-capture-v2", + "precision_digits": 96, + "source_semantics_sha256": source_semantics_sha256, + "runtime_parameter_schema_sha256": runtime_parameter_schema_sha256, + "point_sha256s": point_sha256s, + "kinematic_sha256s": kinematic_sha256s, + "selector_context_sha256s": selector_context_sha256s, + "parameter_context_sha256s": parameter_context_sha256s, + "current_dimensions": dimensions, + "observation_batch_sha256": observation_batch_sha256, + "context_policy": "fixed-contracted-source-schedule-v1", + }), + "fixture capture contract", + ) + .unwrap(); + let dimensions_object = (0..observation_scalars.len()) + .map(|current_id| (current_id.to_string(), json!(1))) + .collect::>(); + let value = json!({ + "abi": "pyamplicol-recurrence-current-observation-capture-v2", + "precision_digits": 96, + "point_count": 4, + "point_sha256s": point_sha256s, + "kinematic_sha256s": kinematic_sha256s, + "parameter_contexts": parameter_contexts, + "parameter_context_sha256s": parameter_context_sha256s, + "context_sha256s": selector_context_sha256s, + "points": points, + "current_count": observation_scalars.len(), + "runtime_parameter_schema_sha256": runtime_parameter_schema_sha256, + "source_semantics_sha256": source_semantics_sha256, + "observation_batch_sha256": observation_batch_sha256, + "capture_contract_sha256": capture_contract_sha256, + "context_policy": "fixed-contracted-source-schedule-v1", + "complete_current_components": true, + "point_major": true, + "current_dimensions_sha256": canonical_json_sha256( + &JsonValue::Object(dimensions_object), + "fixture dimensions", + ).unwrap(), + "evaluator": "recurrence-direct-plan-decimal-symbolica-exact", + "kinematic_binary64": kinematic_binary64, + "selector_contexts": selector_contexts, + "current_dimensions": dimensions, + "observations": observations, + }); + value + } + + fn canonical_numerical_relation_evidence() -> JsonValue { + canonical_numerical_relation_evidence_with_tolerances(1.0e-70, 1.0e-80) + } + + fn canonical_numerical_relation_evidence_with_tolerances( + relative_tolerance: f64, + absolute_tolerance: f64, + ) -> JsonValue { + let seed = 0x5059_414d_u64; + let source_semantics = json!({ + "abi": "pyamplicol-recurrence-numerical-current-source-v3", + "process_id": "raw-v3-test", + "strategy": "contracted-color-union", + "schedule_semantic_digest": digest(40), + "baseline_runtime_layout_digest": digest(41), + "selector_schedule": { + "policy": "fixed-contracted-source-schedule-v1", + "fixed_source_schedule": true, + }, + "currents": (0..6_u32).map(|current_id| json!({ + "current_id": current_id, + "is_source": current_id == 0, + "contract": [ + if current_id == 0 { 0 } else { 1 }, + 1, 7, 1, 0, 0, u32::MAX, u32::MAX, ["1", "1", "0", "1"], + ], + })).collect::>(), + }); + let source_semantics_sha256 = + canonical_json_sha256(&source_semantics, "fixture source semantics").unwrap(); + let source = parse_raw_source_semantics( + &source_semantics, + &source_semantics_sha256, + &digest(40), + &digest(41), + &empty_runtime_parameter_contract(), + ) + .unwrap(); + let runtime_parameter_schema = json!({ + "abi": "pyamplicol-recurrence-runtime-parameter-schema-v2", + "parameters": [], + }); + let runtime_parameter_schema_sha256 = canonical_json_sha256( + &runtime_parameter_schema, + "fixture runtime parameter schema", + ) + .unwrap(); + let candidate_value = raw_capture_fixture( + "candidate-current-probes-v1", + 1, + &source_semantics_sha256, + &runtime_parameter_schema_sha256, + seed, + ); + let verification_value = raw_capture_fixture( + "independent-verification-current-probes-v1", + 101, + &source_semantics_sha256, + &runtime_parameter_schema_sha256, + seed, + ); + let fixture_wire = canonical_json_bytes( + &json!({ + "candidate_capture": candidate_value.clone(), + "verification_capture": verification_value.clone(), + }), + "raw capture fixture wire", + ) + .unwrap(); + let located = locate_raw_observation_arrays(&fixture_wire).unwrap(); + let metadata: JsonValue = serde_json::from_slice(&located.metadata_bytes).unwrap(); + let candidate = parse_raw_numerical_capture( + &fixture_wire, + located.candidate, + &metadata["candidate_capture"], + "candidate", + "candidate-current-probes-v1", + 96, + 4, + &source, + &source_semantics_sha256, + &runtime_parameter_schema_sha256, + 0, + seed, + ) + .unwrap(); + let verification = parse_raw_numerical_capture( + &fixture_wire, + located.verification, + &metadata["verification_capture"], + "verification", + "independent-verification-current-probes-v1", + 96, + 4, + &source, + &source_semantics_sha256, + &runtime_parameter_schema_sha256, + 0, + seed, + ) + .unwrap(); + validate_independent_raw_captures( + &candidate, + &verification, + &empty_runtime_parameter_contract(), + seed, + ) + .unwrap(); + let mut derivation_telemetry = RecurrenceEvidenceAuthenticationTelemetry::default(); + let derivation = derive_raw_numerical_relations( + &source, + &candidate, + &verification, + 96, + seed, + &exact_binary64_rational(relative_tolerance).unwrap(), + &exact_binary64_rational(absolute_tolerance).unwrap(), + &python_f64_hex(relative_tolerance), + &python_f64_hex(absolute_tolerance), + &runtime_parameter_schema_sha256, + rusticol_core::recurrence::NUMERICAL_RELATION_CERTIFICATE_ALGORITHM, + &mut derivation_telemetry, + ) + .unwrap(); + assert_eq!( + derivation + .relations + .iter() + .map(|relation| relation.mapping.relation_kind.as_str()) + .collect::>(), + ["equal", "opposite", "zero"], + ); + let certificates = derivation + .relations + .iter() + .map(|relation| relation.certificate.clone()) + .collect::>(); + let mappings = derivation + .relations + .iter() + .map(|relation| relation.mapping_json.clone()) + .collect::>(); + let mut evidence = json!({ + "abi": "pyamplicol-recurrence-numerical-current-evidence-v3", + "requested_mode": "certified-reuse", + "schedule_semantic_digest": digest(40), + "baseline_runtime_layout_digest": digest(41), + "source_semantics": source_semantics, + "source_semantics_sha256": source_semantics_sha256, + "runtime_parameter_schema": runtime_parameter_schema, + "runtime_parameter_schema_sha256": runtime_parameter_schema_sha256, + "candidate_capture": candidate_value, + "verification_capture": verification_value, + "certificate_algorithm": + rusticol_core::recurrence::NUMERICAL_RELATION_CERTIFICATE_ALGORITHM, + "certificate_set_sha256": digest(0), + "precision_digits": 96, + "probe_count": 4, + "verification_probe_count": 4, + "relative_tolerance_binary64": python_f64_hex(relative_tolerance), + "absolute_tolerance_binary64": python_f64_hex(absolute_tolerance), + "seed": seed, + "candidate_index": { + "algorithm": "complete-contract-anchor-tolerance-window-v1", + "completeness": "complete-within-configured-tolerance", + "contract_count": derivation.candidate_index_contract_count, + "exhaustive_fallback_contract_count": + derivation.exhaustive_fallback_contract_count, + "theoretical_pair_hypothesis_count": + derivation.theoretical_pair_hypothesis_count, + "screened_pair_hypothesis_count": + derivation.screened_pair_hypothesis_count, + "zero_hypothesis_count": derivation.zero_hypothesis_count, + "nearest_rejected_scope": + "zero-and-tolerance-window-screened-hypotheses", + }, + "numerical_candidate_count": derivation.numerical_candidate_count, + "verification_rejected_count": derivation.verification_rejected_count, + "rejected_hypothesis_count": derivation.rejected_hypothesis_count, + "tested_hypothesis_count": derivation.tested_hypothesis_count, + "decision_sha256": derivation.decision_sha256, + "rejection_decision_sha256": + derivation.rejection_decision_sha256, + "certificates": certificates, + "mappings": mappings, + }); + refresh_numerical_certificate_set_digest(&mut evidence); + evidence + } + + #[test] + fn numerical_relation_evidence_recomputes_every_digest_layer() { + let evidence = canonical_numerical_relation_evidence(); + let encoded = canonical_json_bytes(&evidence, "test numerical evidence").unwrap(); + assert_eq!(encoded.len(), 16_778); + assert_eq!( + hex_digest(Sha256::digest(&encoded)), + "aceda1cf45eddf400159f8ce7f60c3bc0de399273fd7a035b5343544d7b19c96", + ); + assert_eq!( + evidence["decision_sha256"], + "e1899c8791f374e406550435e7ff3e3c57044be2a9161b3f39a023df283cc403", + ); + assert_eq!( + evidence["rejection_decision_sha256"], + "ff294e07e4c84a5c6645c71ad1b9c55aff8dc5f7bfa302b3f20c367ffcfa01be", + ); + assert_eq!( + evidence["certificate_set_sha256"], + "5c500444d772f455df8650acaa1d38ca75638452b84c8e9bc0a5037756a04c97", + ); + let parsed = parse_fixture(&encoded).unwrap(); + assert_eq!(parsed.mappings.len(), 3); + assert_eq!( + parsed.rejected_hypothesis_count, + parsed.tested_hypothesis_count - parsed.mappings.len(), + ); + assert!(parsed.rejected_hypothesis_count > parsed.verification_rejected_count); + + let mut stale_set = evidence.clone(); + stale_set["certificates"][0]["current_dimension"] = json!(3); + let stale_bytes = + canonical_json_bytes(&stale_set, "stale numerical certificate set").unwrap(); + assert!( + parse_fixture(&stale_bytes) + .unwrap_err() + .to_string() + .contains("not derived from raw observations") + ); + + let mut stale_proof = evidence.clone(); + stale_proof["certificates"][0]["candidate_maximum_absolute_residual"] = json!("1e-90"); + refresh_numerical_certificate_set_digest(&mut stale_proof); + let stale_bytes = canonical_json_bytes(&stale_proof, "stale numerical proof").unwrap(); + assert!( + parse_fixture(&stale_bytes) + .unwrap_err() + .to_string() + .contains("not derived from raw observations") + ); + + let mut stale_mapping = evidence.clone(); + stale_mapping["mappings"][0]["current_dimension"] = json!(3); + refresh_numerical_certificate_set_digest(&mut stale_mapping); + let stale_bytes = canonical_json_bytes(&stale_mapping, "stale numerical mapping").unwrap(); + assert!( + parse_fixture(&stale_bytes) + .unwrap_err() + .to_string() + .contains("not derived from raw observations") + ); + + let mut stale_tolerance = evidence; + stale_tolerance["relative_tolerance_binary64"] = json!("0x1.0p+0"); + let stale_bytes = + canonical_json_bytes(&stale_tolerance, "stale numerical tolerance").unwrap(); + assert!( + parse_fixture(&stale_bytes) + .unwrap_err() + .to_string() + .contains("canonical nonnegative finite binary64") + ); + + let mut stale_index = canonical_numerical_relation_evidence(); + stale_index["candidate_index"]["screened_pair_hypothesis_count"] = json!(99); + let stale_bytes = + canonical_json_bytes(&stale_index, "stale numerical candidate index").unwrap(); + assert!( + parse_fixture(&stale_bytes) + .unwrap_err() + .to_string() + .contains("was not independently derived") + ); + } + + #[test] + fn profiled_evidence_authentication_is_semantically_identical() { + let evidence = canonical_numerical_relation_evidence(); + let encoded = canonical_json_bytes(&evidence, "profiled numerical evidence").unwrap(); + let expected = parse_fixture(&encoded).unwrap(); + let (profiled, telemetry) = parse_numerical_relation_evidence_with_telemetry( + &encoded, + &empty_runtime_parameter_contract(), + ) + .unwrap(); + + assert_eq!(profiled, expected); + assert_eq!(telemetry.transport_decode_nanoseconds, 0); + assert!(telemetry.total_nanoseconds >= telemetry.candidate_index_nanoseconds); + assert!(telemetry.total_nanoseconds >= telemetry.hypothesis_replay_nanoseconds); + assert!(telemetry.total_nanoseconds >= telemetry.evidence_finalization_nanoseconds); + assert_eq!( + ( + telemetry.candidate_index_observation_row_scan_count, + telemetry.retained_selected_observation_count, + telemetry.selected_observation_reuse_count, + telemetry.zero_residual_streamed_value_count, + telemetry.materialized_observation_vector_count, + telemetry.rational_string_materialization_count, + telemetry.rational_string_use_count, + telemetry.rational_string_reuse_count, + ), + (6, 6, 11, 24, 8, 30, 78, 48), + ); + + let compressed = compressed_envelope(&encoded); + let (compressed_profiled, compressed_telemetry) = + parse_numerical_relation_evidence_with_telemetry( + &compressed, + &empty_runtime_parameter_contract(), + ) + .unwrap(); + assert_eq!(compressed_profiled, expected); + assert!( + compressed_telemetry.total_nanoseconds + >= compressed_telemetry.transport_decode_nanoseconds + ); + assert_eq!( + compressed_telemetry.rational_string_reuse_count, + telemetry.rational_string_reuse_count, + ); + } + + #[test] + fn candidate_index_retains_the_exact_selected_observations() { + let source = RawSourceSemantics { + value: json!({}), + process_id: "selected-observation-test".to_owned(), + physical_pdgs: vec![1], + strategy: "contracted-color-union".to_owned(), + selector_schedule: json!({}), + currents: (0..3) + .map(|current_id| RawSourceCurrent { + current_id, + is_source: false, + contract_key: vec![7], + dimension: 1, + selector_domain_id: 0, + }) + .collect(), + }; + let bytes = canonical_json_bytes( + &json!([ + {"current_id": 0, "dimension": 1, "values": [["1", "10"], ["4", "0"]]}, + {"current_id": 1, "dimension": 1, "values": [["2", "10"], ["4", "0"]]}, + {"current_id": 2, "dimension": 1, "values": [["3", "10"], ["4", "0"]]}, + ]), + "selected observation fixture", + ) + .unwrap(); + let capture = capture_from_observation_array(&bytes, &source, 2); + let mut telemetry = RecurrenceEvidenceAuthenticationTelemetry::default(); + let indexes = + build_raw_numerical_candidate_indexes(&source, &capture, &mut telemetry).unwrap(); + let index = indexes.get(&vec![7]).unwrap(); + + assert_eq!((index.observation_index, index.scalar_component), (0, 0)); + for current_id in 0..3 { + assert_eq!( + index + .selected_value(current_id, "retained selected observation") + .unwrap(), + &raw_observation_value( + &capture, + current_id, + index.observation_index, + "retained selected observation", + ) + .unwrap(), + ); + } + assert_eq!(telemetry.candidate_index_observation_row_scan_count, 3); + assert_eq!(telemetry.retained_selected_observation_count, 3); + assert_eq!(telemetry.selected_observation_reuse_count, 3); + assert!( + index + .selected_value(u32::MAX, "retained selected observation") + .unwrap_err() + .to_string() + .contains("selected observation is absent") + ); + } + + #[test] + fn multi_helicity_all_flow_domains_contribute_no_raw_hypotheses() { + let source_value = json!({"synthetic": "multi-helicity-all-flow"}); + let source = RawSourceSemantics { + value: source_value.clone(), + process_id: "unsafe-selector-domain-test".to_owned(), + physical_pdgs: vec![1], + strategy: "all-flow-union".to_owned(), + selector_schedule: json!({ + "policy": "seeded-helicity-per-selector-domain-and-physical-point-v1", + "resolved_helicities": [ + {"helicity_index": 0, "helicity_id": 0, "selector_domain_id": 0}, + {"helicity_index": 1, "helicity_id": 2, "selector_domain_id": 0}, + ], + }), + currents: (0..3) + .map(|current_id| RawSourceCurrent { + current_id, + is_source: current_id == 0, + contract_key: vec![7], + dimension: 1, + selector_domain_id: 0, + }) + .collect(), + }; + let bytes = canonical_json_bytes( + &json!([ + {"current_id": 0, "dimension": 1, "values": [["1", "0"], ["1", "0"]]}, + {"current_id": 1, "dimension": 1, "values": [["1", "0"], ["1", "0"]]}, + {"current_id": 2, "dimension": 1, "values": [["1", "0"], ["1", "0"]]}, + ]), + "unsafe selector-domain observations", + ) + .unwrap(); + let candidate = capture_from_observation_array(&bytes, &source, 2); + let verification = capture_from_observation_array(&bytes, &source, 2); + let runtime_parameter_schema_sha256 = digest(82); + let mut telemetry = RecurrenceEvidenceAuthenticationTelemetry::default(); + let derivation = derive_raw_numerical_relations( + &source, + &candidate, + &verification, + 96, + 67, + &exact_binary64_rational(1.0e-70).unwrap(), + &exact_binary64_rational(1.0e-80).unwrap(), + &python_f64_hex(1.0e-70), + &python_f64_hex(1.0e-80), + &runtime_parameter_schema_sha256, + rusticol_core::recurrence::NUMERICAL_RELATION_CERTIFICATE_ALGORITHM, + &mut telemetry, + ) + .unwrap(); + + assert!(derivation.relations.is_empty()); + assert_eq!(derivation.tested_hypothesis_count, 0); + assert_eq!(derivation.theoretical_pair_hypothesis_count, 0); + assert_eq!(derivation.screened_pair_hypothesis_count, 0); + assert_eq!(derivation.zero_hypothesis_count, 0); + let chain_root = canonical_json_sha256( + &json!({ + "abi": "pyamplicol-recurrence-numerical-decision-chain-v1", + "source_semantics_sha256": canonical_json_sha256( + &source_value, + "unsafe selector-domain source", + ).unwrap(), + "candidate_capture_sha256": candidate.capture_contract_sha256, + "verification_capture_sha256": verification.capture_contract_sha256, + }), + "unsafe selector-domain decision root", + ) + .unwrap(); + let expected = canonical_json_sha256( + &json!({ + "abi": "pyamplicol-recurrence-numerical-decision-chain-v1", + "chain_tail_sha256": chain_root, + "tested_hypothesis_count": 0, + "theoretical_pair_hypothesis_count": 0, + "screened_pair_hypothesis_count": 0, + "zero_hypothesis_count": 0, + "numerical_candidate_count": 0, + "verification_rejected_count": 0, + "rejected_hypothesis_count": 0, + "certified_relation_count": 0, + }), + "unsafe selector-domain decision census", + ) + .unwrap(); + assert_eq!(derivation.decision_sha256, expected); + } + + #[test] + fn zero_residuals_stream_without_materialized_comparison_vectors() { + let source = RawSourceSemantics { + value: json!({}), + process_id: "zero-residual-test".to_owned(), + physical_pdgs: vec![1], + strategy: "contracted-color-union".to_owned(), + selector_schedule: json!({}), + currents: vec![RawSourceCurrent { + current_id: 0, + is_source: false, + contract_key: vec![1], + dimension: 1, + selector_domain_id: 0, + }], + }; + let bytes = canonical_json_bytes( + &json!([ + {"current_id": 0, "dimension": 1, "values": [["3", "4"], ["0", "0"]]}, + ]), + "zero residual fixture", + ) + .unwrap(); + let capture = capture_from_observation_array(&bytes, &source, 2); + let mut telemetry = RecurrenceEvidenceAuthenticationTelemetry::default(); + let residuals = relation_residuals( + "zero", + 0, + None, + &capture, + &BigRational::zero(), + &BigRational::one(), + &mut telemetry, + ) + .unwrap(); + + assert_eq!(residuals.maximum_ratio, BigRational::from_integer(4.into())); + assert_eq!(residuals.strings.maximum_absolute, "4"); + assert_eq!(residuals.strings.maximum_relative, "1"); + assert_eq!(residuals.strings.maximum_ratio, "4"); + assert_eq!(telemetry.zero_residual_streamed_value_count, 2); + assert_eq!(telemetry.materialized_observation_vector_count, 0); + assert_eq!(telemetry.rational_string_materialization_count, 3); + } + + #[test] + #[ignore = "manual recurrence evidence-authentication microbenchmark"] + fn benchmark_canonical_evidence_authentication() { + let evidence = canonical_numerical_relation_evidence(); + let encoded = canonical_json_bytes(&evidence, "benchmark numerical evidence").unwrap(); + let started = std::time::Instant::now(); + for _ in 0..50 { + let parsed = parse_fixture(&encoded).unwrap(); + assert_eq!(parsed.mappings.len(), 3); + } + eprintln!( + "evidence-authentication-benchmark iterations=50 elapsed_ns={}", + started.elapsed().as_nanos() + ); + } + + #[test] + fn raw_probe_identity_is_bound_to_the_authenticated_process_and_seed_domain() { + fn mutate_first_candidate_point( + mut evidence: JsonValue, + field: &str, + value: JsonValue, + ) -> Vec { + evidence["candidate_capture"]["points"][0][field] = value; + let digest = canonical_json_sha256( + &evidence["candidate_capture"]["points"][0], + "tampered candidate point", + ) + .unwrap(); + evidence["candidate_capture"]["point_sha256s"][0] = json!(digest); + canonical_json_bytes(&evidence, "tampered probe identity").unwrap() + } + + let wrong_id = mutate_first_candidate_point( + canonical_numerical_relation_evidence(), + "process_id", + json!("unrelated-process"), + ); + assert!( + parse_fixture(&wrong_id) + .unwrap_err() + .to_string() + .contains("process_id") + ); + + let wrong_pdg = mutate_first_candidate_point( + canonical_numerical_relation_evidence(), + "points", + json!([[{ + "pdg": -1, + "momentum": ["1", "0", "0", "0"], + }]]), + ); + assert!( + parse_fixture(&wrong_pdg) + .unwrap_err() + .to_string() + .contains("authenticated recurrence process") + ); + + let wrong_seed = + mutate_first_candidate_point(canonical_numerical_relation_evidence(), "seed", json!(0)); + assert!( + parse_fixture(&wrong_seed) + .unwrap_err() + .to_string() + .contains("validation-point seed is stale") + ); + } + + #[test] + fn numerical_relation_evidence_accepts_canonical_binary64_tolerance_boundaries() { + for (relative, absolute) in [ + (1.0e-8, 1.0e-9), + (f64::from_bits(1), 0.0), + (f64::MIN_POSITIVE, f64::from_bits(1)), + (0.25, 0.0), + ] { + let evidence = + canonical_numerical_relation_evidence_with_tolerances(relative, absolute); + let encoded = canonical_json_bytes(&evidence, "boundary tolerance evidence").unwrap(); + let parsed = parse_fixture(&encoded).unwrap(); + assert_eq!(parsed.relative_tolerance.to_bits(), relative.to_bits()); + assert_eq!(parsed.absolute_tolerance.to_bits(), absolute.to_bits()); + } + let (maximum, exact) = + parse_nonnegative_python_f64_hex(&python_f64_hex(f64::MAX), "maximum").unwrap(); + assert_eq!(maximum, f64::MAX); + assert_eq!(exact, exact_binary64_rational(f64::MAX).unwrap()); + } + + #[test] + fn numerical_relation_evidence_rejects_nonfinite_negative_zero_and_noncanonical_tolerances() { + for invalid_tolerance in [ + "nan", + "inf", + "-0x0.0p+0", + "0x1.0p-3", + "0X1.0000000000000P-3", + ] { + let mut evidence = canonical_numerical_relation_evidence(); + evidence["relative_tolerance_binary64"] = json!(invalid_tolerance); + let encoded = canonical_json_bytes(&evidence, "invalid tolerance evidence").unwrap(); + assert!( + parse_fixture(&encoded) + .unwrap_err() + .to_string() + .contains("canonical nonnegative finite binary64") + ); + } + let mut zero = canonical_numerical_relation_evidence(); + zero["relative_tolerance_binary64"] = json!("0x0.0p+0"); + zero["absolute_tolerance_binary64"] = json!("0x0.0p+0"); + let encoded = canonical_json_bytes(&zero, "zero tolerance evidence").unwrap(); + assert!( + parse_fixture(&encoded) + .unwrap_err() + .to_string() + .contains("cannot both be zero") + ); + } + + #[test] + fn observation_locator_builds_canonical_metadata_and_authenticated_offsets() { + let evidence = canonical_numerical_relation_evidence(); + let encoded = canonical_json_bytes(&evidence, "observation locator fixture").unwrap(); + let located = locate_raw_observation_arrays(&encoded).unwrap(); + let (metadata, source) = scanner_source(&located.metadata_bytes); + validate_canonical_json_bytes( + &metadata, + &located.metadata_bytes, + "observation locator metadata", + ) + .unwrap(); + assert_eq!(metadata["candidate_capture"]["observations"], json!([])); + assert_eq!(metadata["verification_capture"]["observations"], json!([])); + let candidate = + scan_raw_observation_array(&encoded, located.candidate, &source, 4, "candidate") + .unwrap(); + let verification = + scan_raw_observation_array(&encoded, located.verification, &source, 4, "verification") + .unwrap(); + assert_eq!(candidate.len(), source.currents.len()); + assert_eq!(verification.len(), source.currents.len()); + assert!(candidate.iter().all(|row| row.row.start < row.row.end)); + assert!( + candidate + .iter() + .all(|row| row.row.start <= row.values.start && row.values.end <= row.row.end) + ); + } + + #[test] + fn observation_scanner_rejects_truncated_reordered_extra_and_wrong_width_rows() { + let evidence = canonical_numerical_relation_evidence(); + let encoded = canonical_json_bytes(&evidence, "observation scanner fixture").unwrap(); + let located = locate_raw_observation_arrays(&encoded).unwrap(); + let (_metadata, source) = scanner_source(&located.metadata_bytes); + let candidate_range = located + .candidate + .as_usize(encoded.len(), "candidate fixture") + .unwrap(); + let candidate = &encoded[candidate_range]; + + let truncated = &candidate[..candidate.len() - 1]; + let truncated_range = + RawByteRange::from_usize(0, truncated.len(), truncated.len(), "truncated").unwrap(); + assert!( + scan_raw_observation_array(truncated, truncated_range, &source, 4, "truncated",) + .unwrap_err() + .to_string() + .contains("malformed or not canonical") + ); + + let text = std::str::from_utf8(candidate).unwrap(); + let reordered = text.replacen( + "{\"current_id\":0,\"dimension\":1,", + "{\"dimension\":1,\"current_id\":0,", + 1, + ); + assert_ne!(reordered, text); + let reordered_range = + RawByteRange::from_usize(0, reordered.len(), reordered.len(), "reordered").unwrap(); + assert!( + scan_raw_observation_array( + reordered.as_bytes(), + reordered_range, + &source, + 4, + "reordered", + ) + .unwrap_err() + .to_string() + .contains("malformed or not canonical") + ); + + let mut extra = candidate[..candidate.len() - 1].to_vec(); + extra.extend_from_slice(b",{}]"); + let extra_range = RawByteRange::from_usize(0, extra.len(), extra.len(), "extra").unwrap(); + assert!( + scan_raw_observation_array(&extra, extra_range, &source, 4, "extra") + .unwrap_err() + .to_string() + .contains("malformed or not canonical") + ); + + let mut wrong_width = evidence["candidate_capture"]["observations"].clone(); + wrong_width[0]["values"].as_array_mut().unwrap().pop(); + let wrong_width = + canonical_json_bytes(&wrong_width, "wrong-width observation fixture").unwrap(); + let wrong_width_range = + RawByteRange::from_usize(0, wrong_width.len(), wrong_width.len(), "wrong width") + .unwrap(); + assert!( + scan_raw_observation_array(&wrong_width, wrong_width_range, &source, 4, "wrong width",) + .unwrap_err() + .to_string() + .contains("malformed or not canonical") + ); + + let mut wide_scalar = evidence["candidate_capture"]["observations"].clone(); + wide_scalar[0]["values"][0][0] = json!("1".repeat(16_385)); + let wide_scalar = + canonical_json_bytes(&wide_scalar, "wide-scalar observation fixture").unwrap(); + let wide_scalar_range = + RawByteRange::from_usize(0, wide_scalar.len(), wide_scalar.len(), "wide scalar") + .unwrap(); + scan_raw_observation_array(&wide_scalar, wide_scalar_range, &source, 4, "wide scalar") + .unwrap(); + + if usize::BITS > 32 { + let offset = u32::MAX as usize + 1; + assert_eq!( + RawByteRange::from_usize(offset, offset, offset, "large offset") + .unwrap() + .as_usize(offset, "large offset") + .unwrap(), + offset..offset, + ); + } + } + + #[test] + fn raw_candidate_index_is_complete_at_equal_and_opposite_boundaries() { + fn rational(numerator: i128, denominator: i128) -> BigRational { + BigRational::new(BigInt::from(numerator), BigInt::from(denominator)) + } + let mut entries = vec![ + (rational(100, 9), 0), + (rational(-100, 9), 1), + (BigRational::from_integer(BigInt::from(1_000)), 2), + (BigRational::from_integer(BigInt::from(10)), 4), + ]; + entries.sort(); + let index = RawNumericalCandidateIndex { + observation_index: 0, + scalar_component: 0, + entries, + selected_values: Vec::new(), + }; + let current = ( + BigRational::from_integer(BigInt::from(10)), + BigRational::zero(), + ); + let relative = rational(1, 10); + let absolute = BigRational::zero(); + let boundary_residual = rational(10, 9); + assert_eq!((¤t.0 - rational(100, 9)).abs(), boundary_residual,); + assert_eq!(&relative * rational(100, 9).abs(), boundary_residual,); + assert_eq!( + raw_numerical_tolerance_window_ids(&index, ¤t, 4, "equal", &relative, &absolute,) + .unwrap(), + BTreeSet::from([0]), + ); + assert_eq!( + raw_numerical_tolerance_window_ids( + &index, ¤t, 4, "opposite", &relative, &absolute, + ) + .unwrap(), + BTreeSet::from([1]), + ); + } + + #[test] + fn numerical_relation_options_reject_stale_probe_and_counter_contracts() { + let evidence = canonical_numerical_relation_evidence(); + let encoded = canonical_json_bytes(&evidence, "test numerical evidence").unwrap(); + let parsed = parse_fixture(&encoded).unwrap(); + let options = RecurrenceRelationDiscoveryOptions::new( + RecurrenceRelationDiscoveryMode::CertifiedReuse, + 96, + 4, + 4, + 1.0e-70, + 1.0e-80, + 0x5059_414d, + "lc", + ) + .unwrap(); + options + .clone() + .with_numerical_evidence(parsed.clone()) + .unwrap(); + + let mut stale_probe = parsed.clone(); + stale_probe.probe_count = 5; + assert!( + options + .clone() + .with_numerical_evidence(stale_probe) + .unwrap_err() + .to_string() + .contains("probe contract") + ); + + let mut stale_counters = parsed; + stale_counters.verification_rejected_count = 1; + assert!( + options + .with_numerical_evidence(stale_counters) + .unwrap_err() + .to_string() + .contains("candidate counters") + ); + } + + #[test] + fn raw_numerical_replay_rejects_context_nonfinite_and_decision_tampering() { + let evidence = canonical_numerical_relation_evidence(); + + let mut selector_tamper = evidence.clone(); + selector_tamper["candidate_capture"]["selector_contexts"][0]["fixed_source_schedule"] = + json!(false); + let error = + parse_fixture(&canonical_json_bytes(&selector_tamper, "selector tamper").unwrap()) + .unwrap_err() + .to_string(); + assert!(error.contains("selector context")); + + let mut nonfinite = evidence.clone(); + nonfinite["candidate_capture"]["observations"][1]["values"][0][0] = json!("NaN"); + let error = + parse_fixture(&canonical_json_bytes(&nonfinite, "nonfinite raw observation").unwrap()) + .unwrap_err() + .to_string(); + assert!(error.contains("canonical finite decimal")); + + let mut stale_decision = evidence; + stale_decision["decision_sha256"] = json!(digest(99)); + let error = + parse_fixture(&canonical_json_bytes(&stale_decision, "stale decision digest").unwrap()) + .unwrap_err() + .to_string(); + assert!(error.contains("decision digest")); + + let mut stale_rejection = canonical_numerical_relation_evidence(); + stale_rejection["rejection_decision_sha256"] = json!(digest(98)); + let error = parse_fixture( + &canonical_json_bytes(&stale_rejection, "stale rejection digest").unwrap(), + ) + .unwrap_err() + .to_string(); + assert!(error.contains("rejection digest")); + } + + #[test] + fn raw_numerical_tolerance_boundary_and_parameter_domains_are_exact() { + let defaults = vec![AuthenticatedRuntimeParameterSlot { + default: BigRational::new(BigInt::from(3), BigInt::from(2)), + probe_policy: "native-template-default-perturbed-v1", + }]; + let candidate = deterministic_parameter_contexts( + &defaults, + 4, + 17, + "candidate-current-parameter-probes-v1", + true, + ) + .unwrap(); + let verification = deterministic_parameter_contexts( + &defaults, + 4, + 17, + "independent-verification-current-parameter-probes-v1", + false, + ) + .unwrap(); + assert_eq!(candidate[0], vec![defaults[0].default.clone()]); + assert!( + candidate + .iter() + .all(|row| !verification.iter().any(|other| row == other)) + ); + + let source = RawSourceSemantics { + value: json!({}), + process_id: "residual-test".to_owned(), + physical_pdgs: vec![1], + strategy: "contracted-color-union".to_owned(), + selector_schedule: json!({}), + currents: (0..2) + .map(|current_id| RawSourceCurrent { + current_id, + is_source: false, + contract_key: vec![1], + dimension: 1, + selector_domain_id: 0, + }) + .collect(), + }; + let boundary_bytes = canonical_json_bytes( + &json!([ + {"current_id": 0, "dimension": 1, "values": [["1", "0"]]}, + {"current_id": 1, "dimension": 1, "values": [["2", "0"]]}, + ]), + "boundary observation capture", + ) + .unwrap(); + let boundary_capture = capture_from_observation_array(&boundary_bytes, &source, 1); + let mut telemetry = RecurrenceEvidenceAuthenticationTelemetry::default(); + let boundary = relation_residuals( + "equal", + 1, + Some(0), + &boundary_capture, + &BigRational::zero(), + &BigRational::one(), + &mut telemetry, + ) + .unwrap(); + assert_eq!(boundary.maximum_ratio, BigRational::one()); + let outside_bytes = canonical_json_bytes( + &json!([ + {"current_id": 0, "dimension": 1, "values": [["1", "0"]]}, + {"current_id": 1, "dimension": 1, "values": [["2.000001", "0"]]}, + ]), + "outside observation capture", + ) + .unwrap(); + let outside_capture = capture_from_observation_array(&outside_bytes, &source, 1); + let outside = relation_residuals( + "equal", + 1, + Some(0), + &outside_capture, + &BigRational::zero(), + &BigRational::one(), + &mut telemetry, + ) + .unwrap(); + assert!(outside.maximum_ratio > BigRational::one()); + } + + #[test] + fn authenticated_parameter_defaults_reject_shifted_neighborhoods() { + fn contract(default: i64) -> AuthenticatedRuntimeParameterContract { + let default = BigRational::from_integer(BigInt::from(default)); + let schema = json!({ + "abi": "pyamplicol-recurrence-runtime-parameter-schema-v2", + "parameters": [{ + "runtime_slot": 0, + "runtime_name": "mass", + "parameter_template_id": 0, + "prepared_parameter_id": 0, + "component": 0, + "default_binary64": python_f64_hex_signed(default.to_f64().unwrap()), + "probe_policy": "native-template-default-perturbed-v1", + }], + }); + AuthenticatedRuntimeParameterContract { + schema_sha256: canonical_json_sha256(&schema, "test runtime parameter schema") + .unwrap(), + schema, + slots: vec![AuthenticatedRuntimeParameterSlot { + default, + probe_policy: "native-template-default-perturbed-v1", + }], + process_id: "parameter-test".to_owned(), + physical_pdgs: vec![1], + } + } + + let authenticated = contract(3); + let shifted = contract(5); + assert!( + validate_raw_runtime_parameter_schema( + &shifted.schema, + &shifted.schema_sha256, + &authenticated, + ) + .unwrap_err() + .to_string() + .contains("authenticated native template input") + ); + } + + #[test] + fn native_parameter_default_hex_preserves_negative_binary64_signs() { + assert_eq!(python_f64_hex_signed(-1.5), "-0x1.8000000000000p+0"); + assert_eq!( + python_f64_hex_signed(f64::from_bits(0x8000_0000_0000_0001)), + "-0x0.0000000000001p-1022" + ); + assert_eq!(python_f64_hex_signed(0.0), "0x0.0p+0"); + } + + #[test] + fn derived_parameter_probe_slots_are_fixed_and_tampering_fails() { + let contract = AuthenticatedRuntimeParameterContract { + schema: json!({}), + schema_sha256: digest(90), + slots: vec![AuthenticatedRuntimeParameterSlot { + default: BigRational::zero(), + probe_policy: "derived-overwritten-fixed-zero-v1", + }], + process_id: "parameter-test".to_owned(), + physical_pdgs: vec![1], + }; + let candidate_contexts = deterministic_parameter_contexts( + &contract.slots, + 4, + 17, + "candidate-current-parameter-probes-v1", + true, + ) + .unwrap(); + let verification_contexts = deterministic_parameter_contexts( + &contract.slots, + 4, + 17, + "independent-verification-current-parameter-probes-v1", + false, + ) + .unwrap(); + assert!( + candidate_contexts + .iter() + .chain(&verification_contexts) + .all(|row| row == &[BigRational::zero()]) + ); + let capture = |parameter_contexts, base: u8| RawNumericalCapture { + raw_bytes: b"[]", + observation_rows: Vec::new(), + point_count: 4, + point_sha256s: (0..4).map(|index| digest(base + index)).collect(), + kinematic_sha256s: (0..4).map(|index| digest(base + 10 + index)).collect(), + parameter_context_sha256s: (0..4).map(|index| digest(base + 20 + index)).collect(), + parameter_contexts, + dimensions: Vec::new(), + observation_batch_sha256: digest(base + 30), + capture_contract_sha256: digest(base + 31), + }; + let mut candidate = capture(candidate_contexts, 1); + let verification = capture(verification_contexts, 101); + validate_independent_raw_captures(&candidate, &verification, &contract, 17).unwrap(); + candidate.parameter_contexts[1][0] = BigRational::one(); + assert!( + validate_independent_raw_captures(&candidate, &verification, &contract, 17) + .unwrap_err() + .to_string() + .contains("stale or non-deterministic") + ); + } + + #[test] + fn parameter_rows_are_included_in_raw_geometry() { + let source = RawSourceSemantics { + value: json!({}), + process_id: "memory-preflight-test".to_owned(), + physical_pdgs: vec![1], + strategy: "contracted-color-union".to_owned(), + selector_schedule: json!({}), + currents: Vec::new(), + }; + assert_eq!( + raw_evidence_geometry_counts(&source, 2, 2, 400_000).unwrap(), + (0, 0, 1_600_000, 400_000) + ); + } + + fn compressed_envelope(raw: &[u8]) -> Vec { + let mut encoder = ZlibEncoder::new(Vec::new(), Compression::fast()); + encoder.write_all(raw).unwrap(); + let compressed = encoder.finish().unwrap(); + let mut envelope = Vec::with_capacity(COMPRESSED_EVIDENCE_HEADER_BYTES + compressed.len()); + envelope.extend_from_slice(COMPRESSED_EVIDENCE_MAGIC); + envelope.extend_from_slice(&(raw.len() as u64).to_be_bytes()); + envelope.extend_from_slice(&Sha256::digest(raw)); + envelope.extend_from_slice(&compressed); + envelope + } + + #[test] + fn compressed_transport_checks_declared_length_and_digest_before_json_parsing() { + let raw = br#"{"candidate_capture":{"observations":[]},"verification_capture":{"observations":[]}}"#; + let encoded = compressed_envelope(raw); + assert_eq!(decode_compressed_evidence(&encoded).unwrap(), raw); + + let mut digest_tamper = encoded.clone(); + digest_tamper[16] ^= 1; + assert!( + decode_compressed_evidence(&digest_tamper) + .unwrap_err() + .to_string() + .contains("digest mismatch") + ); + + let mut length_tamper = encoded.clone(); + length_tamper[8..16].copy_from_slice(&((raw.len() - 1) as u64).to_be_bytes()); + assert!( + decode_compressed_evidence(&length_tamper) + .unwrap_err() + .to_string() + .contains("decompressed length") + ); + + let mut trailing = encoded; + trailing.push(0); + assert!( + decode_compressed_evidence(&trailing) + .unwrap_err() + .to_string() + .contains("trailing or unconsumed") + ); + } +} diff --git a/pyamplicol/rust/crates/rusticol-python/src/recurrence_template.rs b/pyamplicol/rust/crates/rusticol-python/src/recurrence_template.rs new file mode 100644 index 0000000000..f8cf07841f --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-python/src/recurrence_template.rs @@ -0,0 +1,1693 @@ +// SPDX-License-Identifier: 0BSD + +//! Private Python boundary for model-wide recurrence-template input v1. +//! +//! The Python producer owns and freezes canonical NumPy columns. This module +//! checks that envelope before copying it into the strongly typed core input. +//! Prepared-kernel IDs can additionally be checked against an inventory from +//! an already authenticated prepared pack. Central registration must provide +//! that inventory before the validated template input is used for lowering. + +use std::collections::{BTreeMap, BTreeSet}; + +use numpy::{PyReadonlyArrayDyn, PyUntypedArrayMethods}; +use pyo3::exceptions::{PyTypeError, PyValueError}; +use pyo3::prelude::*; +use pyo3::types::{PyAny, PyDict}; +use rusticol_core::recurrence::template::{ + CatalogHeaderRow, ClosureRow, ColorContractionRow, ColorNcTermRow, ContactOrbitCertificateRow, + ContactOrbitStepRow, CouplingOrderTermRow, CurrentStateRow, DigestCatalogRow, + EvaluatorBindingRow, ExactFactorRow, IndexedRangeRow, LCColorTransitionWitnessRow, MISSING_U32, + OwnedRecurrenceTemplateInput, ParameterRow, PropagatorRow, QuantumFlowRow, + QuantumNumberFlowTermRow, RECURRENCE_TEMPLATE_INPUT_ABI, + RECURRENCE_TEMPLATE_INPUT_SCHEMA_VERSION, RuntimeHelicityContractRow, + RuntimeHelicityEmbeddingRow, RuntimeHelicityProjectionRow, RuntimeHelicityVariantRow, + SourceRow, SymmetryProofRow, TransitionRow, +}; +use rusticol_core::recurrence::{CheckedTableRange, SemanticDigest, checked_usize}; +use rusticol_core::{RusticolError, RusticolResult}; +use sha2::{Digest, Sha256}; + +use crate::python_error; + +const RESULT_KIND: &str = "pyamplicol-recurrence-template-validation-result"; +const RESULT_SCHEMA_VERSION: u32 = 1; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum PrimitiveKind { + U8, + U32, + U64, + I32, +} + +impl PrimitiveKind { + const fn dtype(self) -> &'static str { + match self { + Self::U8 => "|u1", + Self::U32 => " " " ColumnSpec { + ColumnSpec { + name, + kind, + tail_shape: &[], + } +} + +const fn shaped_column( + name: &'static str, + kind: PrimitiveKind, + tail_shape: &'static [usize], +) -> ColumnSpec { + ColumnSpec { + name, + kind, + tail_shape, + } +} + +const TABLE_SPECS: &[TableSpec] = &[ + TableSpec { + name: "catalog_header", + columns: &[ + column("schema_version", PrimitiveKind::U32), + column("abi_string_id", PrimitiveKind::U32), + column("canonicalization_abi_string_id", PrimitiveKind::U32), + column("exact_scalar_abi_string_id", PrimitiveKind::U32), + column("compiled_model_digest_id", PrimitiveKind::U32), + column("prepared_kernel_pack_digest_id", PrimitiveKind::U32), + column("catalog_digest_id", PrimitiveKind::U32), + column("parameter_count", PrimitiveKind::U32), + column("current_state_count", PrimitiveKind::U32), + column("source_count", PrimitiveKind::U32), + column("quantum_flow_count", PrimitiveKind::U32), + column("contact_orbit_certificate_count", PrimitiveKind::U32), + column("contact_orbit_step_count", PrimitiveKind::U32), + column("transition_count", PrimitiveKind::U32), + column("propagator_count", PrimitiveKind::U32), + column("closure_count", PrimitiveKind::U32), + column("color_contraction_count", PrimitiveKind::U32), + column("symmetry_proof_count", PrimitiveKind::U32), + column("runtime_helicity_contract_count", PrimitiveKind::U32), + column("evaluator_binding_count", PrimitiveKind::U32), + ], + }, + TableSpec { + name: "closures", + columns: &[ + column("id", PrimitiveKind::U32), + column("template_string_id", PrimitiveKind::U32), + column("input_state_sequence_id", PrimitiveKind::U32), + column("result_state_template_id", PrimitiveKind::U32), + column("evaluator_binding_id", PrimitiveKind::U32), + column("canonical_input_order_sequence_id", PrimitiveKind::U32), + column("coupling_parameter_sequence_id", PrimitiveKind::U32), + column("coupling_order_set_id", PrimitiveKind::U32), + column("eligible_quantum_flow_sequence_id", PrimitiveKind::U32), + column("color_contraction_template_id", PrimitiveKind::U32), + column("binding_coupling_factor_id", PrimitiveKind::U32), + column("exact_factor_id", PrimitiveKind::U32), + column("output_factor_source", PrimitiveKind::U8), + column("equivalence_class_string_id", PrimitiveKind::U32), + column("input_exchange_factor_id", PrimitiveKind::U32), + column("projection_string_id", PrimitiveKind::U32), + column("component_coefficient_sequence_id", PrimitiveKind::U32), + column("chirality_relation_string_id", PrimitiveKind::U32), + column("metric_signature_string_id", PrimitiveKind::U32), + column("semantic_digest_id", PrimitiveKind::U32), + ], + }, + TableSpec { + name: "color_contractions", + columns: &[ + column("id", PrimitiveKind::U32), + column("template_string_id", PrimitiveKind::U32), + column("rule_kind_string_id", PrimitiveKind::U32), + column("input_representation_sequence_id", PrimitiveKind::U32), + column("has_output_representation", PrimitiveKind::U8), + column("output_representation", PrimitiveKind::I32), + column("ordered_open_string_arity", PrimitiveKind::U32), + column("exact_coefficient_factor_id", PrimitiveKind::U32), + column("witness_start", PrimitiveKind::U64), + column("witness_count", PrimitiveKind::U64), + column("nc_term_start", PrimitiveKind::U64), + column("nc_term_count", PrimitiveKind::U64), + column("expression_digest_id", PrimitiveKind::U32), + column("semantic_digest_id", PrimitiveKind::U32), + ], + }, + TableSpec { + name: "color_nc_terms", + columns: &[ + column("color_contraction_id", PrimitiveKind::U32), + column("exponent", PrimitiveKind::I32), + column("factor_id", PrimitiveKind::U32), + ], + }, + TableSpec { + name: "contact_orbit_certificates", + columns: &[ + column("id", PrimitiveKind::U32), + column("template_string_id", PrimitiveKind::U32), + column("algorithm_string_id", PrimitiveKind::U32), + column("algorithm_version", PrimitiveKind::U32), + column("term_id", PrimitiveKind::U32), + column("vertex_string_id", PrimitiveKind::U32), + column("particle_string_sequence_id", PrimitiveKind::U32), + column("evaluator_class_string_id", PrimitiveKind::U32), + column("physical_leg_equivalence_sequence_id", PrimitiveKind::U32), + column("reconstruction_factor_id", PrimitiveKind::U32), + column("semantic_digest_id", PrimitiveKind::U32), + ], + }, + TableSpec { + name: "contact_orbit_steps", + columns: &[ + column("id", PrimitiveKind::U32), + column("template_string_id", PrimitiveKind::U32), + column("certificate_id", PrimitiveKind::U32), + column("stage", PrimitiveKind::U8), + column("result_leg", PrimitiveKind::U8), + column("left_covered_leg_sequence_id", PrimitiveKind::U32), + column("right_covered_leg_sequence_id", PrimitiveKind::U32), + column("source_particle_leg_sequence_id", PrimitiveKind::U32), + column("reconstruction_factor_id", PrimitiveKind::U32), + column("semantic_digest_id", PrimitiveKind::U32), + ], + }, + TableSpec { + name: "coupling_order_ranges", + columns: &[ + column("id", PrimitiveKind::U32), + column("start", PrimitiveKind::U64), + column("count", PrimitiveKind::U64), + ], + }, + TableSpec { + name: "coupling_order_terms", + columns: &[ + column("set_id", PrimitiveKind::U32), + column("name_string_id", PrimitiveKind::U32), + column("power", PrimitiveKind::U32), + ], + }, + TableSpec { + name: "current_states", + columns: &[ + column("id", PrimitiveKind::U32), + column("template_string_id", PrimitiveKind::U32), + column("particle_id", PrimitiveKind::I32), + column("anti_particle_id", PrimitiveKind::I32), + column("species_string_id", PrimitiveKind::U32), + column("orientation", PrimitiveKind::U8), + column("statistics", PrimitiveKind::U8), + column("color_representation", PrimitiveKind::I32), + column("basis_string_id", PrimitiveKind::U32), + column("tensor_ordering_sequence_id", PrimitiveKind::U32), + column("dimension", PrimitiveKind::U32), + column("chirality", PrimitiveKind::I32), + column("lc_color_shape_string_id", PrimitiveKind::U32), + column("auxiliary_kind_string_id", PrimitiveKind::U32), + column("mass_parameter_id", PrimitiveKind::U32), + column("width_parameter_id", PrimitiveKind::U32), + column("semantic_digest_id", PrimitiveKind::U32), + ], + }, + TableSpec { + name: "digest_catalog", + columns: &[ + column("id", PrimitiveKind::U32), + shaped_column("value", PrimitiveKind::U8, &[32]), + ], + }, + TableSpec { + name: "evaluator_bindings", + columns: &[ + column("id", PrimitiveKind::U32), + column("resolver_key_string_id", PrimitiveKind::U32), + column("prepared_kernel_id", PrimitiveKind::U32), + column("contract_kind", PrimitiveKind::U8), + column("callable_signature_digest_id", PrimitiveKind::U32), + column("input_state_sequence_id", PrimitiveKind::U32), + column("output_state_template_id", PrimitiveKind::U32), + column("input_layout_sequence_id", PrimitiveKind::U32), + column("output_layout_sequence_id", PrimitiveKind::U32), + column("exact_expression_digest_sequence_id", PrimitiveKind::U32), + column("semantic_template_sequence_id", PrimitiveKind::U32), + column("callable_kind", PrimitiveKind::U8), + column("runtime_template_string_id", PrimitiveKind::U32), + column("semantic_digest_id", PrimitiveKind::U32), + ], + }, + TableSpec { + name: "exact_factors", + columns: &[ + column("id", PrimitiveKind::U32), + column("real_numerator_string_id", PrimitiveKind::U32), + column("real_denominator_string_id", PrimitiveKind::U32), + column("imag_numerator_string_id", PrimitiveKind::U32), + column("imag_denominator_string_id", PrimitiveKind::U32), + ], + }, + TableSpec { + name: "flavour_flow_ranges", + columns: &[ + column("id", PrimitiveKind::U32), + column("start", PrimitiveKind::U64), + column("count", PrimitiveKind::U64), + ], + }, + TableSpec { + name: "flavour_flow_values", + columns: &[column("value", PrimitiveKind::I32)], + }, + TableSpec { + name: "i32_sequence_ranges", + columns: &[ + column("id", PrimitiveKind::U32), + column("start", PrimitiveKind::U64), + column("count", PrimitiveKind::U64), + ], + }, + TableSpec { + name: "i32_sequence_values", + columns: &[column("value", PrimitiveKind::I32)], + }, + TableSpec { + name: "lc_color_transition_witnesses", + columns: &[ + column("color_contraction_id", PrimitiveKind::U32), + column("ordinal", PrimitiveKind::U32), + column("left_shape_string_id", PrimitiveKind::U32), + column("right_shape_string_id", PrimitiveKind::U32), + column("input_permutation", PrimitiveKind::U8), + column("reverse_parent_mask", PrimitiveKind::U8), + column("component_operation", PrimitiveKind::U8), + column("result_component_kind", PrimitiveKind::U8), + column("result_component_role", PrimitiveKind::U8), + column("result_shape_string_id", PrimitiveKind::U32), + column("exact_factor_id", PrimitiveKind::U32), + column("proof_digest_id", PrimitiveKind::U32), + column("input_port_pairing_sequence_id", PrimitiveKind::U32), + column("result_port_binding_sequence_id", PrimitiveKind::U32), + column("provenance_sequence_id", PrimitiveKind::U32), + ], + }, + TableSpec { + name: "parameters", + columns: &[ + column("id", PrimitiveKind::U32), + column("template_string_id", PrimitiveKind::U32), + column("name_string_id", PrimitiveKind::U32), + column("kind", PrimitiveKind::U8), + column("value_type", PrimitiveKind::U8), + column("mutable", PrimitiveKind::U8), + column("default_factor_id", PrimitiveKind::U32), + column("exact_expression_digest_id", PrimitiveKind::U32), + column("dependency_sequence_id", PrimitiveKind::U32), + column("prepared_parameter_id", PrimitiveKind::U32), + column("semantic_digest_id", PrimitiveKind::U32), + ], + }, + TableSpec { + name: "propagators", + columns: &[ + column("id", PrimitiveKind::U32), + column("template_string_id", PrimitiveKind::U32), + column("state_template_id", PrimitiveKind::U32), + column("applies_propagator", PrimitiveKind::U8), + column("evaluator_binding_id", PrimitiveKind::U32), + column("numerator_expression_digest_id", PrimitiveKind::U32), + column("denominator_expression_digest_id", PrimitiveKind::U32), + column("mass_parameter_id", PrimitiveKind::U32), + column("width_parameter_id", PrimitiveKind::U32), + column("gauge_string_id", PrimitiveKind::U32), + column("linearity_proof_template_id", PrimitiveKind::U32), + column("semantic_digest_id", PrimitiveKind::U32), + ], + }, + TableSpec { + name: "quantum_flows", + columns: &[ + column("id", PrimitiveKind::U32), + column("template_string_id", PrimitiveKind::U32), + column("input_state_sequence_id", PrimitiveKind::U32), + column("input_spin_sequence_id", PrimitiveKind::U32), + column("input_flavour_sequence_id", PrimitiveKind::U32), + column("input_quantum_sequence_id", PrimitiveKind::U32), + column("flavour_flow_operation_string_id", PrimitiveKind::U32), + column( + "quantum_number_flow_operation_string_id", + PrimitiveKind::U32, + ), + column("coupling_order_set_id", PrimitiveKind::U32), + column("result_state_template_id", PrimitiveKind::U32), + column("result_spin_state", PrimitiveKind::I32), + column("result_flavour_flow_id", PrimitiveKind::U32), + column("result_quantum_number_flow_id", PrimitiveKind::U32), + column("exact_coupling_factor_id", PrimitiveKind::U32), + column("predicate_digest_id", PrimitiveKind::U32), + column("semantic_digest_id", PrimitiveKind::U32), + ], + }, + TableSpec { + name: "quantum_number_flow_ranges", + columns: &[ + column("id", PrimitiveKind::U32), + column("start", PrimitiveKind::U64), + column("count", PrimitiveKind::U64), + ], + }, + TableSpec { + name: "quantum_number_flow_terms", + columns: &[ + column("flow_id", PrimitiveKind::U32), + column("name_string_id", PrimitiveKind::U32), + column("expression_string_id", PrimitiveKind::U32), + ], + }, + TableSpec { + name: "runtime_helicity_contracts", + columns: &[ + column("id", PrimitiveKind::U32), + column("template_string_id", PrimitiveKind::U32), + column("full_state_template_id", PrimitiveKind::U32), + column("variant_offset", PrimitiveKind::U32), + column("variant_count", PrimitiveKind::U32), + column("proof_algorithm_string_id", PrimitiveKind::U32), + column("proof_digest_id", PrimitiveKind::U32), + column("semantic_digest_id", PrimitiveKind::U32), + ], + }, + TableSpec { + name: "runtime_helicity_embeddings", + columns: &[ + column("variant_id", PrimitiveKind::U32), + column("full_component", PrimitiveKind::U32), + column("source_component", PrimitiveKind::U32), + column("factor_id", PrimitiveKind::U32), + ], + }, + TableSpec { + name: "runtime_helicity_projections", + columns: &[ + column("variant_id", PrimitiveKind::U32), + column("source_component", PrimitiveKind::U32), + column("full_component", PrimitiveKind::U32), + ], + }, + TableSpec { + name: "runtime_helicity_variants", + columns: &[ + column("id", PrimitiveKind::U32), + column("contract_id", PrimitiveKind::U32), + column("source_template_id", PrimitiveKind::U32), + column("source_state_template_id", PrimitiveKind::U32), + column("embedding_offset", PrimitiveKind::U32), + column("embedding_count", PrimitiveKind::U32), + column("projection_offset", PrimitiveKind::U32), + column("projection_count", PrimitiveKind::U32), + column("proof_digest_id", PrimitiveKind::U32), + ], + }, + TableSpec { + name: "sources", + columns: &[ + column("id", PrimitiveKind::U32), + column("template_string_id", PrimitiveKind::U32), + column("state_template_id", PrimitiveKind::U32), + column("crossing_string_id", PrimitiveKind::U32), + column("wavefunction_family_string_id", PrimitiveKind::U32), + column("helicity", PrimitiveKind::I32), + column("spin_state", PrimitiveKind::I32), + column("flavour_flow_id", PrimitiveKind::U32), + column("quantum_number_flow_id", PrimitiveKind::U32), + column("lc_color_seed_operation", PrimitiveKind::U8), + column("lc_color_seed_shape_string_id", PrimitiveKind::U32), + column("lc_color_seed_component_kind", PrimitiveKind::U8), + column("lc_color_seed_component_role", PrimitiveKind::U8), + column("lc_color_seed_proof_digest_id", PrimitiveKind::U32), + column("lc_color_seed_provenance_sequence_id", PrimitiveKind::U32), + column("wavefunction_expression_digest_id", PrimitiveKind::U32), + column("evaluator_binding_id", PrimitiveKind::U32), + column("mass_parameter_id", PrimitiveKind::U32), + column("width_parameter_id", PrimitiveKind::U32), + column("semantic_digest_id", PrimitiveKind::U32), + ], + }, + TableSpec { + name: "string_bytes", + columns: &[column("value", PrimitiveKind::U8)], + }, + TableSpec { + name: "string_ranges", + columns: &[ + column("start", PrimitiveKind::U64), + column("count", PrimitiveKind::U64), + ], + }, + TableSpec { + name: "symmetry_proofs", + columns: &[ + column("id", PrimitiveKind::U32), + column("template_string_id", PrimitiveKind::U32), + column("proof_algorithm_string_id", PrimitiveKind::U32), + column("subject_template_sequence_id", PrimitiveKind::U32), + column("input_permutation_sequence_id", PrimitiveKind::U32), + column("exact_phase_factor_id", PrimitiveKind::U32), + column("expression_digest_sequence_id", PrimitiveKind::U32), + column("witness_digest_id", PrimitiveKind::U32), + column("semantic_digest_id", PrimitiveKind::U32), + ], + }, + TableSpec { + name: "transitions", + columns: &[ + column("id", PrimitiveKind::U32), + column("template_string_id", PrimitiveKind::U32), + column("input_state_sequence_id", PrimitiveKind::U32), + column("result_state_template_id", PrimitiveKind::U32), + column("quantum_flow_template_id", PrimitiveKind::U32), + column("evaluator_binding_id", PrimitiveKind::U32), + column("canonical_input_order_sequence_id", PrimitiveKind::U32), + column("momentum_convention_sequence_id", PrimitiveKind::U32), + column("coupling_parameter_sequence_id", PrimitiveKind::U32), + column("coupling_order_set_id", PrimitiveKind::U32), + column("color_contraction_template_id", PrimitiveKind::U32), + column("binding_coupling_factor_id", PrimitiveKind::U32), + column("exact_factor_id", PrimitiveKind::U32), + column("output_factor_source", PrimitiveKind::U8), + column("equivalence_class_string_id", PrimitiveKind::U32), + column("input_exchange_factor_id", PrimitiveKind::U32), + column("output_projection_string_id", PrimitiveKind::U32), + column("contact_orbit_step_sequence_id", PrimitiveKind::U32), + column( + "contact_orbit_step_semantic_digest_sequence_id", + PrimitiveKind::U32, + ), + column("semantic_digest_id", PrimitiveKind::U32), + ], + }, + TableSpec { + name: "u32_sequence_ranges", + columns: &[ + column("id", PrimitiveKind::U32), + column("start", PrimitiveKind::U64), + column("count", PrimitiveKind::U64), + ], + }, + TableSpec { + name: "u32_sequence_values", + columns: &[column("value", PrimitiveKind::U32)], + }, +]; + +#[derive(Clone)] +enum OwnedValues { + U8(Vec), + U32(Vec), + U64(Vec), + I32(Vec), +} + +impl OwnedValues { + fn raw_bytes(&self) -> &[u8] { + fn bytes(values: &[T]) -> &[u8] { + // Multi-byte inputs are explicitly little-endian and big-endian + // hosts are rejected before NumPy extraction. + unsafe { + std::slice::from_raw_parts( + values.as_ptr().cast::(), + std::mem::size_of_val(values), + ) + } + } + match self { + Self::U8(values) => values, + Self::U32(values) => bytes(values), + Self::U64(values) => bytes(values), + Self::I32(values) => bytes(values), + } + } + + fn as_u8(&self, context: &str) -> RusticolResult<&[u8]> { + match self { + Self::U8(values) => Ok(values), + _ => Err(wrong_type(context, "u8")), + } + } + + fn as_u32(&self, context: &str) -> RusticolResult<&[u32]> { + match self { + Self::U32(values) => Ok(values), + _ => Err(wrong_type(context, "u32")), + } + } + + fn as_u64(&self, context: &str) -> RusticolResult<&[u64]> { + match self { + Self::U64(values) => Ok(values), + _ => Err(wrong_type(context, "u64")), + } + } + + fn as_i32(&self, context: &str) -> RusticolResult<&[i32]> { + match self { + Self::I32(values) => Ok(values), + _ => Err(wrong_type(context, "i32")), + } + } +} + +#[derive(Clone)] +struct OwnedColumn { + name: String, + dtype: &'static str, + shape: Vec, + values: OwnedValues, +} + +#[derive(Clone)] +struct OwnedTable { + name: String, + row_count: u64, + columns: Vec, + column_by_name: BTreeMap, +} + +impl OwnedTable { + fn row_count(&self) -> RusticolResult { + checked_usize(self.row_count, &format!("{} row count", self.name)) + } + + fn column(&self, name: &str) -> RusticolResult<&OwnedColumn> { + let index = self.column_by_name.get(name).ok_or_else(|| { + invalid(format!( + "recurrence template table {:?} has no column {name:?}", + self.name + )) + })?; + Ok(&self.columns[*index]) + } + + fn u8(&self, column: &str) -> RusticolResult<&[u8]> { + self.column(column)? + .values + .as_u8(&format!("{}.{column}", self.name)) + } + + fn u32(&self, column: &str) -> RusticolResult<&[u32]> { + self.column(column)? + .values + .as_u32(&format!("{}.{column}", self.name)) + } + + fn u64(&self, column: &str) -> RusticolResult<&[u64]> { + self.column(column)? + .values + .as_u64(&format!("{}.{column}", self.name)) + } + + fn i32(&self, column: &str) -> RusticolResult<&[i32]> { + self.column(column)? + .values + .as_i32(&format!("{}.{column}", self.name)) + } +} + +struct DecodedInput { + abi: String, + declared_digest: String, + canonical_digest_property: String, + catalog_digest: SemanticDigest, + compiled_model_digest: SemanticDigest, + prepared_kernel_pack_digest: SemanticDigest, + tables: Vec, + table_by_name: BTreeMap, +} + +impl DecodedInput { + fn table(&self, name: &str) -> RusticolResult<&OwnedTable> { + let index = self + .table_by_name + .get(name) + .ok_or_else(|| invalid(format!("recurrence template input has no table {name:?}")))?; + Ok(&self.tables[*index]) + } + + fn canonical_digest(&self) -> RusticolResult { + let mut digest = Sha256::new(); + hash_text(&mut digest, &self.abi)?; + digest.update(self.catalog_digest.as_bytes()); + digest.update(self.compiled_model_digest.as_bytes()); + digest.update(self.prepared_kernel_pack_digest.as_bytes()); + digest.update( + u64::try_from(self.tables.len()) + .map_err(|_| invalid("recurrence template table count exceeds u64"))? + .to_le_bytes(), + ); + for table in &self.tables { + hash_text(&mut digest, &table.name)?; + digest.update(table.row_count.to_le_bytes()); + digest.update( + u32::try_from(table.columns.len()) + .map_err(|_| invalid("recurrence template column count exceeds u32"))? + .to_le_bytes(), + ); + for column in &table.columns { + hash_text(&mut digest, &column.name)?; + hash_text(&mut digest, column.dtype)?; + digest.update( + u8::try_from(column.shape.len()) + .map_err(|_| invalid("recurrence template column rank exceeds u8"))? + .to_le_bytes(), + ); + for dimension in &column.shape { + digest.update(dimension.to_le_bytes()); + } + digest.update(column.values.raw_bytes()); + } + } + Ok(hex_digest(digest.finalize())) + } + + fn into_core(self) -> RusticolResult { + let catalog_header = decode_rows(self.table("catalog_header")?, |table, row| { + Ok(CatalogHeaderRow { + schema_version: table.u32("schema_version")?[row], + abi_string_id: table.u32("abi_string_id")?[row], + canonicalization_abi_string_id: table.u32("canonicalization_abi_string_id")?[row], + exact_scalar_abi_string_id: table.u32("exact_scalar_abi_string_id")?[row], + compiled_model_digest_id: table.u32("compiled_model_digest_id")?[row], + prepared_kernel_pack_digest_id: table.u32("prepared_kernel_pack_digest_id")?[row], + catalog_digest_id: table.u32("catalog_digest_id")?[row], + parameter_count: table.u32("parameter_count")?[row], + current_state_count: table.u32("current_state_count")?[row], + source_count: table.u32("source_count")?[row], + quantum_flow_count: table.u32("quantum_flow_count")?[row], + contact_orbit_certificate_count: table.u32("contact_orbit_certificate_count")?[row], + contact_orbit_step_count: table.u32("contact_orbit_step_count")?[row], + transition_count: table.u32("transition_count")?[row], + propagator_count: table.u32("propagator_count")?[row], + closure_count: table.u32("closure_count")?[row], + color_contraction_count: table.u32("color_contraction_count")?[row], + symmetry_proof_count: table.u32("symmetry_proof_count")?[row], + runtime_helicity_contract_count: table.u32("runtime_helicity_contract_count")?[row], + evaluator_binding_count: table.u32("evaluator_binding_count")?[row], + }) + })?; + let coupling_order_ranges = indexed_ranges(self.table("coupling_order_ranges")?)?; + let coupling_order_terms = + decode_rows(self.table("coupling_order_terms")?, |table, row| { + Ok(CouplingOrderTermRow { + set_id: table.u32("set_id")?[row], + name_string_id: table.u32("name_string_id")?[row], + power: table.u32("power")?[row], + }) + })?; + let contact_orbit_certificates = + decode_rows(self.table("contact_orbit_certificates")?, |table, row| { + Ok(ContactOrbitCertificateRow { + id: table.u32("id")?[row], + template_string_id: table.u32("template_string_id")?[row], + algorithm_string_id: table.u32("algorithm_string_id")?[row], + algorithm_version: table.u32("algorithm_version")?[row], + term_id: table.u32("term_id")?[row], + vertex_string_id: table.u32("vertex_string_id")?[row], + particle_string_sequence_id: table.u32("particle_string_sequence_id")?[row], + evaluator_class_string_id: table.u32("evaluator_class_string_id")?[row], + physical_leg_equivalence_sequence_id: table + .u32("physical_leg_equivalence_sequence_id")?[row], + reconstruction_factor_id: table.u32("reconstruction_factor_id")?[row], + semantic_digest_id: table.u32("semantic_digest_id")?[row], + }) + })?; + let contact_orbit_steps = decode_rows(self.table("contact_orbit_steps")?, |table, row| { + Ok(ContactOrbitStepRow { + id: table.u32("id")?[row], + template_string_id: table.u32("template_string_id")?[row], + certificate_id: table.u32("certificate_id")?[row], + stage: table.u8("stage")?[row], + result_leg: table.u8("result_leg")?[row], + left_covered_leg_sequence_id: table.u32("left_covered_leg_sequence_id")?[row], + right_covered_leg_sequence_id: table.u32("right_covered_leg_sequence_id")?[row], + source_particle_leg_sequence_id: table.u32("source_particle_leg_sequence_id")?[row], + reconstruction_factor_id: table.u32("reconstruction_factor_id")?[row], + semantic_digest_id: table.u32("semantic_digest_id")?[row], + }) + })?; + let current_states = decode_rows(self.table("current_states")?, |table, row| { + Ok(CurrentStateRow { + id: table.u32("id")?[row], + template_string_id: table.u32("template_string_id")?[row], + particle_id: table.i32("particle_id")?[row], + anti_particle_id: table.i32("anti_particle_id")?[row], + species_string_id: table.u32("species_string_id")?[row], + orientation: table.u8("orientation")?[row], + statistics: table.u8("statistics")?[row], + color_representation: table.i32("color_representation")?[row], + basis_string_id: table.u32("basis_string_id")?[row], + tensor_ordering_sequence_id: table.u32("tensor_ordering_sequence_id")?[row], + dimension: table.u32("dimension")?[row], + chirality: table.i32("chirality")?[row], + lc_color_shape_string_id: table.u32("lc_color_shape_string_id")?[row], + auxiliary_kind_string_id: table.u32("auxiliary_kind_string_id")?[row], + mass_parameter_id: table.u32("mass_parameter_id")?[row], + width_parameter_id: table.u32("width_parameter_id")?[row], + semantic_digest_id: table.u32("semantic_digest_id")?[row], + }) + })?; + let digest_catalog = decode_digest_catalog(self.table("digest_catalog")?)?; + let evaluator_bindings = decode_rows(self.table("evaluator_bindings")?, |table, row| { + Ok(EvaluatorBindingRow { + id: table.u32("id")?[row], + resolver_key_string_id: table.u32("resolver_key_string_id")?[row], + prepared_kernel_id: table.u32("prepared_kernel_id")?[row], + contract_kind: table.u8("contract_kind")?[row], + callable_signature_digest_id: table.u32("callable_signature_digest_id")?[row], + input_state_sequence_id: table.u32("input_state_sequence_id")?[row], + output_state_template_id: table.u32("output_state_template_id")?[row], + input_layout_sequence_id: table.u32("input_layout_sequence_id")?[row], + output_layout_sequence_id: table.u32("output_layout_sequence_id")?[row], + exact_expression_digest_sequence_id: table + .u32("exact_expression_digest_sequence_id")?[row], + semantic_template_sequence_id: table.u32("semantic_template_sequence_id")?[row], + callable_kind: table.u8("callable_kind")?[row], + runtime_template_string_id: table.u32("runtime_template_string_id")?[row], + semantic_digest_id: table.u32("semantic_digest_id")?[row], + }) + })?; + let exact_factors = decode_rows(self.table("exact_factors")?, |table, row| { + Ok(ExactFactorRow { + id: table.u32("id")?[row], + real_numerator_string_id: table.u32("real_numerator_string_id")?[row], + real_denominator_string_id: table.u32("real_denominator_string_id")?[row], + imag_numerator_string_id: table.u32("imag_numerator_string_id")?[row], + imag_denominator_string_id: table.u32("imag_denominator_string_id")?[row], + }) + })?; + let flavour_flow_ranges = indexed_ranges(self.table("flavour_flow_ranges")?)?; + let flavour_flow_values = self.table("flavour_flow_values")?.i32("value")?.to_vec(); + let i32_sequence_ranges = indexed_ranges(self.table("i32_sequence_ranges")?)?; + let i32_sequence_values = self.table("i32_sequence_values")?.i32("value")?.to_vec(); + let parameters = decode_rows(self.table("parameters")?, |table, row| { + Ok(ParameterRow { + id: table.u32("id")?[row], + template_string_id: table.u32("template_string_id")?[row], + name_string_id: table.u32("name_string_id")?[row], + kind: table.u8("kind")?[row], + value_type: table.u8("value_type")?[row], + mutable: table.u8("mutable")?[row], + default_factor_id: table.u32("default_factor_id")?[row], + exact_expression_digest_id: table.u32("exact_expression_digest_id")?[row], + dependency_sequence_id: table.u32("dependency_sequence_id")?[row], + prepared_parameter_id: table.u32("prepared_parameter_id")?[row], + semantic_digest_id: table.u32("semantic_digest_id")?[row], + }) + })?; + let propagators = decode_rows(self.table("propagators")?, |table, row| { + Ok(PropagatorRow { + id: table.u32("id")?[row], + template_string_id: table.u32("template_string_id")?[row], + state_template_id: table.u32("state_template_id")?[row], + applies_propagator: table.u8("applies_propagator")?[row], + evaluator_binding_id: table.u32("evaluator_binding_id")?[row], + numerator_expression_digest_id: table.u32("numerator_expression_digest_id")?[row], + denominator_expression_digest_id: table.u32("denominator_expression_digest_id")? + [row], + mass_parameter_id: table.u32("mass_parameter_id")?[row], + width_parameter_id: table.u32("width_parameter_id")?[row], + gauge_string_id: table.u32("gauge_string_id")?[row], + linearity_proof_template_id: table.u32("linearity_proof_template_id")?[row], + semantic_digest_id: table.u32("semantic_digest_id")?[row], + }) + })?; + let quantum_flows = decode_rows(self.table("quantum_flows")?, |table, row| { + Ok(QuantumFlowRow { + id: table.u32("id")?[row], + template_string_id: table.u32("template_string_id")?[row], + input_state_sequence_id: table.u32("input_state_sequence_id")?[row], + input_spin_sequence_id: table.u32("input_spin_sequence_id")?[row], + input_flavour_sequence_id: table.u32("input_flavour_sequence_id")?[row], + input_quantum_sequence_id: table.u32("input_quantum_sequence_id")?[row], + flavour_flow_operation_string_id: table.u32("flavour_flow_operation_string_id")? + [row], + quantum_number_flow_operation_string_id: table + .u32("quantum_number_flow_operation_string_id")?[row], + coupling_order_set_id: table.u32("coupling_order_set_id")?[row], + result_state_template_id: table.u32("result_state_template_id")?[row], + result_spin_state: table.i32("result_spin_state")?[row], + result_flavour_flow_id: table.u32("result_flavour_flow_id")?[row], + result_quantum_number_flow_id: table.u32("result_quantum_number_flow_id")?[row], + exact_coupling_factor_id: table.u32("exact_coupling_factor_id")?[row], + predicate_digest_id: table.u32("predicate_digest_id")?[row], + semantic_digest_id: table.u32("semantic_digest_id")?[row], + }) + })?; + let quantum_number_flow_ranges = indexed_ranges(self.table("quantum_number_flow_ranges")?)?; + let quantum_number_flow_terms = + decode_rows(self.table("quantum_number_flow_terms")?, |table, row| { + Ok(QuantumNumberFlowTermRow { + flow_id: table.u32("flow_id")?[row], + name_string_id: table.u32("name_string_id")?[row], + expression_string_id: table.u32("expression_string_id")?[row], + }) + })?; + let runtime_helicity_contracts = + decode_rows(self.table("runtime_helicity_contracts")?, |table, row| { + Ok(RuntimeHelicityContractRow { + id: table.u32("id")?[row], + template_string_id: table.u32("template_string_id")?[row], + full_state_template_id: table.u32("full_state_template_id")?[row], + variant_range: CheckedTableRange::new( + table.u32("variant_offset")?[row].into(), + table.u32("variant_count")?[row].into(), + ), + proof_algorithm_string_id: table.u32("proof_algorithm_string_id")?[row], + proof_digest_id: table.u32("proof_digest_id")?[row], + semantic_digest_id: table.u32("semantic_digest_id")?[row], + }) + })?; + let runtime_helicity_embeddings = + decode_rows(self.table("runtime_helicity_embeddings")?, |table, row| { + Ok(RuntimeHelicityEmbeddingRow { + variant_id: table.u32("variant_id")?[row], + full_component: table.u32("full_component")?[row], + source_component: table.u32("source_component")?[row], + factor_id: table.u32("factor_id")?[row], + }) + })?; + let runtime_helicity_projections = + decode_rows(self.table("runtime_helicity_projections")?, |table, row| { + Ok(RuntimeHelicityProjectionRow { + variant_id: table.u32("variant_id")?[row], + source_component: table.u32("source_component")?[row], + full_component: table.u32("full_component")?[row], + }) + })?; + let runtime_helicity_variants = + decode_rows(self.table("runtime_helicity_variants")?, |table, row| { + Ok(RuntimeHelicityVariantRow { + id: table.u32("id")?[row], + contract_id: table.u32("contract_id")?[row], + source_template_id: table.u32("source_template_id")?[row], + source_state_template_id: table.u32("source_state_template_id")?[row], + embedding_range: CheckedTableRange::new( + table.u32("embedding_offset")?[row].into(), + table.u32("embedding_count")?[row].into(), + ), + projection_range: CheckedTableRange::new( + table.u32("projection_offset")?[row].into(), + table.u32("projection_count")?[row].into(), + ), + proof_digest_id: table.u32("proof_digest_id")?[row], + }) + })?; + let sources = decode_rows(self.table("sources")?, |table, row| { + Ok(SourceRow { + id: table.u32("id")?[row], + template_string_id: table.u32("template_string_id")?[row], + state_template_id: table.u32("state_template_id")?[row], + crossing_string_id: table.u32("crossing_string_id")?[row], + wavefunction_family_string_id: table.u32("wavefunction_family_string_id")?[row], + helicity: table.i32("helicity")?[row], + spin_state: table.i32("spin_state")?[row], + flavour_flow_id: table.u32("flavour_flow_id")?[row], + quantum_number_flow_id: table.u32("quantum_number_flow_id")?[row], + lc_color_seed_operation: table.u8("lc_color_seed_operation")?[row], + lc_color_seed_shape_string_id: table.u32("lc_color_seed_shape_string_id")?[row], + lc_color_seed_component_kind: table.u8("lc_color_seed_component_kind")?[row], + lc_color_seed_component_role: table.u8("lc_color_seed_component_role")?[row], + lc_color_seed_proof_digest_id: table.u32("lc_color_seed_proof_digest_id")?[row], + lc_color_seed_provenance_sequence_id: table + .u32("lc_color_seed_provenance_sequence_id")?[row], + wavefunction_expression_digest_id: table + .u32("wavefunction_expression_digest_id")?[row], + evaluator_binding_id: table.u32("evaluator_binding_id")?[row], + mass_parameter_id: table.u32("mass_parameter_id")?[row], + width_parameter_id: table.u32("width_parameter_id")?[row], + semantic_digest_id: table.u32("semantic_digest_id")?[row], + }) + })?; + let string_ranges = plain_ranges(self.table("string_ranges")?)?; + let string_bytes = self.table("string_bytes")?.u8("value")?.to_vec(); + let symmetry_proofs = decode_rows(self.table("symmetry_proofs")?, |table, row| { + Ok(SymmetryProofRow { + id: table.u32("id")?[row], + template_string_id: table.u32("template_string_id")?[row], + proof_algorithm_string_id: table.u32("proof_algorithm_string_id")?[row], + subject_template_sequence_id: table.u32("subject_template_sequence_id")?[row], + input_permutation_sequence_id: table.u32("input_permutation_sequence_id")?[row], + exact_phase_factor_id: table.u32("exact_phase_factor_id")?[row], + expression_digest_sequence_id: table.u32("expression_digest_sequence_id")?[row], + witness_digest_id: table.u32("witness_digest_id")?[row], + semantic_digest_id: table.u32("semantic_digest_id")?[row], + }) + })?; + let transitions = decode_rows(self.table("transitions")?, |table, row| { + Ok(TransitionRow { + id: table.u32("id")?[row], + template_string_id: table.u32("template_string_id")?[row], + input_state_sequence_id: table.u32("input_state_sequence_id")?[row], + result_state_template_id: table.u32("result_state_template_id")?[row], + quantum_flow_template_id: table.u32("quantum_flow_template_id")?[row], + evaluator_binding_id: table.u32("evaluator_binding_id")?[row], + canonical_input_order_sequence_id: table + .u32("canonical_input_order_sequence_id")?[row], + momentum_convention_sequence_id: table.u32("momentum_convention_sequence_id")?[row], + coupling_parameter_sequence_id: table.u32("coupling_parameter_sequence_id")?[row], + coupling_order_set_id: table.u32("coupling_order_set_id")?[row], + color_contraction_template_id: table.u32("color_contraction_template_id")?[row], + binding_coupling_factor_id: table.u32("binding_coupling_factor_id")?[row], + exact_factor_id: table.u32("exact_factor_id")?[row], + output_factor_source: table.u8("output_factor_source")?[row], + equivalence_class_string_id: table.u32("equivalence_class_string_id")?[row], + input_exchange_factor_id: table.u32("input_exchange_factor_id")?[row], + output_projection_string_id: table.u32("output_projection_string_id")?[row], + contact_orbit_step_sequence_id: table.u32("contact_orbit_step_sequence_id")?[row], + contact_orbit_step_semantic_digest_sequence_id: table + .u32("contact_orbit_step_semantic_digest_sequence_id")?[row], + semantic_digest_id: table.u32("semantic_digest_id")?[row], + }) + })?; + let closures = decode_rows(self.table("closures")?, |table, row| { + Ok(ClosureRow { + id: table.u32("id")?[row], + template_string_id: table.u32("template_string_id")?[row], + input_state_sequence_id: table.u32("input_state_sequence_id")?[row], + result_state_template_id: table.u32("result_state_template_id")?[row], + evaluator_binding_id: table.u32("evaluator_binding_id")?[row], + canonical_input_order_sequence_id: table + .u32("canonical_input_order_sequence_id")?[row], + coupling_parameter_sequence_id: table.u32("coupling_parameter_sequence_id")?[row], + coupling_order_set_id: table.u32("coupling_order_set_id")?[row], + eligible_quantum_flow_sequence_id: table + .u32("eligible_quantum_flow_sequence_id")?[row], + color_contraction_template_id: table.u32("color_contraction_template_id")?[row], + binding_coupling_factor_id: table.u32("binding_coupling_factor_id")?[row], + exact_factor_id: table.u32("exact_factor_id")?[row], + output_factor_source: table.u8("output_factor_source")?[row], + equivalence_class_string_id: table.u32("equivalence_class_string_id")?[row], + input_exchange_factor_id: table.u32("input_exchange_factor_id")?[row], + projection_string_id: table.u32("projection_string_id")?[row], + component_coefficient_sequence_id: table + .u32("component_coefficient_sequence_id")?[row], + chirality_relation_string_id: table.u32("chirality_relation_string_id")?[row], + metric_signature_string_id: table.u32("metric_signature_string_id")?[row], + semantic_digest_id: table.u32("semantic_digest_id")?[row], + }) + })?; + let color_contractions = decode_rows(self.table("color_contractions")?, |table, row| { + Ok(ColorContractionRow { + id: table.u32("id")?[row], + template_string_id: table.u32("template_string_id")?[row], + rule_kind_string_id: table.u32("rule_kind_string_id")?[row], + input_representation_sequence_id: table.u32("input_representation_sequence_id")? + [row], + has_output_representation: table.u8("has_output_representation")?[row], + output_representation: table.i32("output_representation")?[row], + ordered_open_string_arity: table.u32("ordered_open_string_arity")?[row], + exact_coefficient_factor_id: table.u32("exact_coefficient_factor_id")?[row], + witness_start: table.u64("witness_start")?[row], + witness_count: table.u64("witness_count")?[row], + nc_term_start: table.u64("nc_term_start")?[row], + nc_term_count: table.u64("nc_term_count")?[row], + expression_digest_id: table.u32("expression_digest_id")?[row], + semantic_digest_id: table.u32("semantic_digest_id")?[row], + }) + })?; + let lc_color_transition_witnesses = decode_rows( + self.table("lc_color_transition_witnesses")?, + |table, row| { + Ok(LCColorTransitionWitnessRow { + color_contraction_id: table.u32("color_contraction_id")?[row], + ordinal: table.u32("ordinal")?[row], + left_shape_string_id: table.u32("left_shape_string_id")?[row], + right_shape_string_id: table.u32("right_shape_string_id")?[row], + input_permutation: table.u8("input_permutation")?[row], + reverse_parent_mask: table.u8("reverse_parent_mask")?[row], + component_operation: table.u8("component_operation")?[row], + result_component_kind: table.u8("result_component_kind")?[row], + result_component_role: table.u8("result_component_role")?[row], + result_shape_string_id: table.u32("result_shape_string_id")?[row], + exact_factor_id: table.u32("exact_factor_id")?[row], + proof_digest_id: table.u32("proof_digest_id")?[row], + input_port_pairing_sequence_id: table.u32("input_port_pairing_sequence_id")? + [row], + result_port_binding_sequence_id: table + .u32("result_port_binding_sequence_id")?[row], + provenance_sequence_id: table.u32("provenance_sequence_id")?[row], + }) + }, + )?; + let color_nc_terms = decode_rows(self.table("color_nc_terms")?, |table, row| { + Ok(ColorNcTermRow { + color_contraction_id: table.u32("color_contraction_id")?[row], + exponent: table.i32("exponent")?[row], + factor_id: table.u32("factor_id")?[row], + }) + })?; + let u32_sequence_ranges = indexed_ranges(self.table("u32_sequence_ranges")?)?; + let u32_sequence_values = self.table("u32_sequence_values")?.u32("value")?.to_vec(); + + Ok(OwnedRecurrenceTemplateInput { + input_abi: self.abi, + catalog_digest: self.catalog_digest, + compiled_model_digest: self.compiled_model_digest, + prepared_kernel_pack_digest: self.prepared_kernel_pack_digest, + catalog_header, + coupling_order_ranges, + coupling_order_terms, + contact_orbit_certificates, + contact_orbit_steps, + current_states, + digest_catalog, + evaluator_bindings, + exact_factors, + flavour_flow_ranges, + flavour_flow_values, + i32_sequence_ranges, + i32_sequence_values, + parameters, + propagators, + quantum_flows, + quantum_number_flow_ranges, + quantum_number_flow_terms, + runtime_helicity_contracts, + runtime_helicity_variants, + runtime_helicity_embeddings, + runtime_helicity_projections, + sources, + string_ranges, + string_bytes, + symmetry_proofs, + transitions, + closures, + color_contractions, + lc_color_transition_witnesses, + color_nc_terms, + u32_sequence_ranges, + u32_sequence_values, + }) + } +} + +struct InventoryValidation { + referenced_count: usize, + authenticated_count: Option, + verified: bool, +} + +struct NativeValidationResult { + input_digest: String, + catalog_digest: SemanticDigest, + compiled_model_digest: SemanticDigest, + prepared_kernel_pack_digest: SemanticDigest, + parameter_count: u32, + current_state_count: u32, + source_count: u32, + quantum_flow_count: u32, + contact_orbit_certificate_count: u32, + contact_orbit_step_count: u32, + transition_count: u32, + propagator_count: u32, + closure_count: u32, + color_contraction_count: u32, + lc_color_transition_witness_count: u32, + symmetry_proof_count: u32, + runtime_helicity_contract_count: u32, + evaluator_binding_count: u32, + prepared_kernel_count: u32, + inventory: InventoryValidation, +} + +#[pyfunction(signature = (template_input, authenticated_prepared_kernel_ids=None))] +pub(crate) fn _validate_recurrence_template_input_v1( + py: Python<'_>, + template_input: &Bound<'_, PyAny>, + authenticated_prepared_kernel_ids: Option>, +) -> PyResult> { + let decoded = parse_input(template_input)?; + let native = py + .detach(move || validate_input(decoded, authenticated_prepared_kernel_ids)) + .map_err(python_error)?; + result_mapping(py, native) +} + +fn parse_input(input: &Bound<'_, PyAny>) -> PyResult { + if cfg!(not(target_endian = "little")) { + return Err(PyValueError::new_err( + "recurrence template input v1 requires a little-endian target", + )); + } + let abi = required_string(input, "abi")?; + if abi != RECURRENCE_TEMPLATE_INPUT_ABI { + return Err(PyValueError::new_err(format!( + "unsupported recurrence template input ABI {abi:?}" + ))); + } + let declared_digest = required_sha256(input, "digest", "template input digest")?; + let canonical_digest_property = + required_sha256(input, "canonical_digest", "template input canonical digest")?; + if declared_digest != canonical_digest_property { + return Err(PyValueError::new_err( + "recurrence template input digest and canonical_digest disagree", + )); + } + let catalog_digest = semantic_digest_attribute(input, "catalog_digest")?; + let compiled_model_digest = semantic_digest_attribute(input, "compiled_model_digest")?; + let prepared_kernel_pack_digest = + semantic_digest_attribute(input, "prepared_kernel_pack_digest")?; + + let table_objects = iterable_attribute(input, "tables", "recurrence template tables")?; + if table_objects.len() != TABLE_SPECS.len() { + return Err(PyValueError::new_err(format!( + "recurrence template table inventory has {} tables, expected {}", + table_objects.len(), + TABLE_SPECS.len() + ))); + } + + let mut tables = Vec::with_capacity(TABLE_SPECS.len()); + let mut table_by_name = BTreeMap::new(); + for (table_object, spec) in table_objects.into_iter().zip(TABLE_SPECS) { + let table_name = required_nonempty_string(&table_object, "name", "table name")?; + if table_name != spec.name { + return Err(PyValueError::new_err(format!( + "recurrence template table inventory mismatch: found {table_name:?}, expected {:?}", + spec.name + ))); + } + let row_count = table_object + .getattr("row_count")? + .extract::() + .map_err(|_| { + PyTypeError::new_err(format!( + "recurrence template table {table_name:?} row_count must be u64" + )) + })?; + let column_objects = iterable_attribute( + &table_object, + "columns", + &format!("recurrence template table {table_name:?} columns"), + )?; + if column_objects.len() != spec.columns.len() { + return Err(PyValueError::new_err(format!( + "recurrence template table {table_name:?} has {} columns, expected {}", + column_objects.len(), + spec.columns.len() + ))); + } + + let mut columns = Vec::with_capacity(spec.columns.len()); + let mut column_by_name = BTreeMap::new(); + for (column_object, column_spec) in column_objects.into_iter().zip(spec.columns) { + let column_name = required_nonempty_string(&column_object, "name", "column name")?; + if column_name != column_spec.name { + return Err(PyValueError::new_err(format!( + "recurrence template table {table_name:?} column mismatch: found {column_name:?}, expected {:?}", + column_spec.name + ))); + } + let context = format!("{table_name}.{column_name}"); + let values_object = column_object.getattr("values")?; + let dtype = values_object + .getattr("dtype")? + .getattr("str")? + .extract::()?; + if dtype != column_spec.kind.dtype() { + return Err(PyValueError::new_err(format!( + "{context} has dtype {dtype:?}, expected {:?}", + column_spec.kind.dtype() + ))); + } + let flags = values_object.getattr("flags")?; + if flags.getattr("writeable")?.extract::()? { + return Err(PyValueError::new_err(format!( + "{context} must be read-only" + ))); + } + if !flags.getattr("owndata")?.extract::()? { + return Err(PyValueError::new_err(format!( + "{context} must own its storage" + ))); + } + let (values, shape) = extract_owned_values(&values_object, column_spec.kind, &context)?; + if shape.first().copied() != Some(row_count) { + return Err(PyValueError::new_err(format!( + "{context} first dimension does not match row_count {row_count}" + ))); + } + let actual_tail = shape + .iter() + .skip(1) + .map(|value| usize::try_from(*value)) + .collect::, _>>() + .map_err(|_| PyValueError::new_err(format!("{context} shape exceeds usize")))?; + if actual_tail != column_spec.tail_shape { + return Err(PyValueError::new_err(format!( + "{context} has tail shape {actual_tail:?}, expected {:?}", + column_spec.tail_shape + ))); + } + column_by_name.insert(column_name.clone(), columns.len()); + columns.push(OwnedColumn { + name: column_name, + dtype: column_spec.kind.dtype(), + shape, + values, + }); + } + table_by_name.insert(table_name.clone(), tables.len()); + tables.push(OwnedTable { + name: table_name, + row_count, + columns, + column_by_name, + }); + } + + Ok(DecodedInput { + abi, + declared_digest, + canonical_digest_property, + catalog_digest, + compiled_model_digest, + prepared_kernel_pack_digest, + tables, + table_by_name, + }) +} + +fn extract_owned_values( + value: &Bound<'_, PyAny>, + kind: PrimitiveKind, + context: &str, +) -> PyResult<(OwnedValues, Vec)> { + macro_rules! extract { + ($ty:ty, $variant:ident) => {{ + let array = value + .extract::>() + .map_err(|_| { + PyTypeError::new_err(format!( + "{context} is not a NumPy array with dtype {:?}", + kind.dtype() + )) + })?; + if !array.is_c_contiguous() { + return Err(PyValueError::new_err(format!( + "{context} must be C-contiguous" + ))); + } + let shape = array + .shape() + .iter() + .map(|value| { + u64::try_from(*value) + .map_err(|_| PyValueError::new_err(format!("{context} shape exceeds u64"))) + }) + .collect::>>()?; + let values = array + .as_slice() + .map_err(|error| { + PyValueError::new_err(format!("{context} must be contiguous: {error}")) + })? + .to_vec(); + (OwnedValues::$variant(values), shape) + }}; + } + Ok(match kind { + PrimitiveKind::U8 => extract!(u8, U8), + PrimitiveKind::U32 => extract!(u32, U32), + PrimitiveKind::U64 => extract!(u64, U64), + PrimitiveKind::I32 => extract!(i32, I32), + }) +} + +fn validate_input( + decoded: DecodedInput, + authenticated_prepared_kernel_ids: Option>, +) -> RusticolResult { + let actual_digest = decoded.canonical_digest()?; + if actual_digest != decoded.declared_digest + || actual_digest != decoded.canonical_digest_property + { + return Err(invalid(format!( + "recurrence template input digest mismatch: declared {}, found {actual_digest}", + decoded.declared_digest + ))); + } + let core = decoded.into_core()?; + let inventory = + validate_prepared_kernel_inventory(&core, authenticated_prepared_kernel_ids.as_deref())?; + let validated = core.validate()?; + let summary = validated.summary(); + if summary.prepared_kernel_count as usize != inventory.referenced_count { + return Err(invalid(format!( + "core prepared-kernel count {} disagrees with decoded inventory count {}", + summary.prepared_kernel_count, inventory.referenced_count + ))); + } + Ok(NativeValidationResult { + input_digest: actual_digest, + catalog_digest: summary.catalog_digest, + compiled_model_digest: summary.compiled_model_digest, + prepared_kernel_pack_digest: summary.prepared_kernel_pack_digest, + parameter_count: summary.parameter_count, + current_state_count: summary.current_state_count, + source_count: summary.source_count, + quantum_flow_count: summary.quantum_flow_count, + contact_orbit_certificate_count: summary.contact_orbit_certificate_count, + contact_orbit_step_count: summary.contact_orbit_step_count, + transition_count: summary.transition_count, + propagator_count: summary.propagator_count, + closure_count: summary.closure_count, + color_contraction_count: summary.color_contraction_count, + lc_color_transition_witness_count: summary.lc_color_transition_witness_count, + symmetry_proof_count: summary.symmetry_proof_count, + runtime_helicity_contract_count: summary.runtime_helicity_contract_count, + evaluator_binding_count: summary.evaluator_binding_count, + prepared_kernel_count: summary.prepared_kernel_count, + inventory, + }) +} + +fn validate_prepared_kernel_inventory( + input: &OwnedRecurrenceTemplateInput, + authenticated_ids: Option<&[u32]>, +) -> RusticolResult { + let referenced = input + .evaluator_bindings + .iter() + .filter_map(|binding| { + (binding.prepared_kernel_id != MISSING_U32).then_some(binding.prepared_kernel_id) + }) + .collect::>(); + let Some(authenticated_ids) = authenticated_ids else { + return Ok(InventoryValidation { + referenced_count: referenced.len(), + authenticated_count: None, + verified: false, + }); + }; + let authenticated = authenticated_ids.iter().copied().collect::>(); + if authenticated.len() != authenticated_ids.len() { + return Err(invalid( + "authenticated prepared-kernel ID inventory contains duplicates", + )); + } + if authenticated.contains(&MISSING_U32) { + return Err(invalid( + "authenticated prepared-kernel ID inventory contains the missing-ID sentinel", + )); + } + let missing = referenced + .difference(&authenticated) + .copied() + .take(16) + .collect::>(); + if !missing.is_empty() { + return Err(invalid(format!( + "recurrence template references prepared-kernel IDs absent from the authenticated pack inventory: {missing:?}" + ))); + } + Ok(InventoryValidation { + referenced_count: referenced.len(), + authenticated_count: Some(authenticated.len()), + verified: true, + }) +} + +fn result_mapping(py: Python<'_>, native: NativeValidationResult) -> PyResult> { + let result = PyDict::new(py); + result.set_item("kind", RESULT_KIND)?; + result.set_item("schema_version", RESULT_SCHEMA_VERSION)?; + result.set_item("validation_status", "validated")?; + result.set_item("template_input_abi", RECURRENCE_TEMPLATE_INPUT_ABI)?; + result.set_item( + "template_input_schema_version", + RECURRENCE_TEMPLATE_INPUT_SCHEMA_VERSION, + )?; + result.set_item("template_input_sha256", native.input_digest)?; + result.set_item("catalog_digest", native.catalog_digest.to_string())?; + result.set_item( + "compiled_model_digest", + native.compiled_model_digest.to_string(), + )?; + result.set_item( + "prepared_kernel_pack_digest", + native.prepared_kernel_pack_digest.to_string(), + )?; + result.set_item( + "prepared_kernel_inventory_verified", + native.inventory.verified, + )?; + result.set_item( + "prepared_kernel_inventory_count", + native.inventory.authenticated_count, + )?; + if !native.inventory.verified { + result.set_item( + "prepared_kernel_inventory_requirement", + "central registration must compare referenced IDs with the authenticated prepared pack", + )?; + } + + let counts = PyDict::new(py); + counts.set_item("parameters", native.parameter_count)?; + counts.set_item("current_states", native.current_state_count)?; + counts.set_item("sources", native.source_count)?; + counts.set_item("quantum_flows", native.quantum_flow_count)?; + counts.set_item( + "contact_orbit_certificates", + native.contact_orbit_certificate_count, + )?; + counts.set_item("contact_orbit_steps", native.contact_orbit_step_count)?; + counts.set_item("transitions", native.transition_count)?; + counts.set_item("propagators", native.propagator_count)?; + counts.set_item("closures", native.closure_count)?; + counts.set_item("color_contractions", native.color_contraction_count)?; + counts.set_item( + "lc_color_transition_witnesses", + native.lc_color_transition_witness_count, + )?; + counts.set_item("symmetry_proofs", native.symmetry_proof_count)?; + counts.set_item( + "runtime_helicity_contracts", + native.runtime_helicity_contract_count, + )?; + counts.set_item("evaluator_bindings", native.evaluator_binding_count)?; + counts.set_item("prepared_kernels", native.prepared_kernel_count)?; + counts.set_item( + "referenced_prepared_kernels", + native.inventory.referenced_count, + )?; + result.set_item("counts", counts)?; + Ok(result.into_any().unbind()) +} + +fn decode_rows( + table: &OwnedTable, + mut decode: impl FnMut(&OwnedTable, usize) -> RusticolResult, +) -> RusticolResult> { + let row_count = table.row_count()?; + (0..row_count).map(|row| decode(table, row)).collect() +} + +fn indexed_ranges(table: &OwnedTable) -> RusticolResult> { + decode_rows(table, |table, row| { + Ok(IndexedRangeRow { + id: table.u32("id")?[row], + range: CheckedTableRange::new(table.u64("start")?[row], table.u64("count")?[row]), + }) + }) +} + +fn plain_ranges(table: &OwnedTable) -> RusticolResult> { + decode_rows(table, |table, row| { + Ok(CheckedTableRange::new( + table.u64("start")?[row], + table.u64("count")?[row], + )) + }) +} + +fn decode_digest_catalog(table: &OwnedTable) -> RusticolResult> { + let ids = table.u32("id")?; + let values = table.u8("value")?; + let row_count = table.row_count()?; + let expected = row_count + .checked_mul(32) + .ok_or_else(|| invalid("digest catalog byte count exceeds usize"))?; + if values.len() != expected { + return Err(invalid(format!( + "digest_catalog.value has {} bytes, expected {expected}", + values.len() + ))); + } + (0..row_count) + .map(|row| { + let start = row * 32; + let mut value = [0_u8; 32]; + value.copy_from_slice(&values[start..start + 32]); + Ok(DigestCatalogRow { + id: ids[row], + value, + }) + }) + .collect() +} + +fn semantic_digest_attribute( + input: &Bound<'_, PyAny>, + attribute: &str, +) -> PyResult { + let value = required_sha256(input, attribute, attribute)?; + let bytes = decode_sha256(&value, attribute)?; + SemanticDigest::new(bytes).map_err(python_error) +} + +fn iterable_attribute<'py>( + value: &Bound<'py, PyAny>, + attribute: &str, + context: &str, +) -> PyResult>> { + value + .getattr(attribute)? + .try_iter() + .map_err(|_| PyTypeError::new_err(format!("{context} must be iterable")))? + .collect() +} + +fn required_string(value: &Bound<'_, PyAny>, attribute: &str) -> PyResult { + value.getattr(attribute)?.extract::().map_err(|_| { + PyTypeError::new_err(format!( + "recurrence template input {attribute} must be a string" + )) + }) +} + +fn required_nonempty_string( + value: &Bound<'_, PyAny>, + attribute: &str, + context: &str, +) -> PyResult { + let result = required_string(value, attribute)?; + if result.is_empty() { + return Err(PyValueError::new_err(format!( + "recurrence template {context} must not be empty" + ))); + } + Ok(result) +} + +fn required_sha256(value: &Bound<'_, PyAny>, attribute: &str, context: &str) -> PyResult { + let result = required_string(value, attribute)?; + validate_sha256_text(&result, context)?; + Ok(result) +} + +fn validate_sha256_text(value: &str, context: &str) -> PyResult<()> { + if value.len() != 64 + || !value + .bytes() + .all(|byte| byte.is_ascii_digit() || matches!(byte, b'a'..=b'f')) + { + return Err(PyValueError::new_err(format!( + "{context} must be a lowercase SHA-256 digest" + ))); + } + Ok(()) +} + +fn decode_sha256(value: &str, context: &str) -> PyResult<[u8; 32]> { + validate_sha256_text(value, context)?; + let mut result = [0_u8; 32]; + for (index, byte) in result.iter_mut().enumerate() { + let offset = index * 2; + *byte = u8::from_str_radix(&value[offset..offset + 2], 16) + .map_err(|_| PyValueError::new_err(format!("{context} is not hexadecimal")))?; + } + Ok(result) +} + +fn hash_text(digest: &mut Sha256, value: &str) -> RusticolResult<()> { + digest.update( + u64::try_from(value.len()) + .map_err(|_| invalid("recurrence template digest text length exceeds u64"))? + .to_le_bytes(), + ); + digest.update(value.as_bytes()); + Ok(()) +} + +fn hex_digest(value: impl AsRef<[u8]>) -> String { + let mut result = String::with_capacity(value.as_ref().len() * 2); + for byte in value.as_ref() { + use std::fmt::Write; + write!(&mut result, "{byte:02x}").expect("writing to String cannot fail"); + } + result +} + +fn invalid(message: impl Into) -> RusticolError { + RusticolError::invalid_argument(message) +} + +fn wrong_type(context: &str, expected: &str) -> RusticolError { + invalid(format!( + "{context} is not a {expected} recurrence template column" + )) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn table_inventory_is_canonical() { + assert!( + TABLE_SPECS + .windows(2) + .all(|pair| pair[0].name < pair[1].name) + ); + for table in TABLE_SPECS { + assert!( + table + .columns + .iter() + .map(|column| column.name) + .collect::>() + .len() + == table.columns.len() + ); + } + } + + #[test] + fn sha256_decoder_is_strict() { + let value = "01".repeat(32); + assert_eq!(decode_sha256(&value, "test").unwrap(), [1; 32]); + assert!(decode_sha256(&"A1".repeat(32), "test").is_err()); + assert!(decode_sha256("00", "test").is_err()); + } +} diff --git a/pyamplicol/rust/crates/rusticol-python/stubs/pyamplicol/__init__.pyi b/pyamplicol/rust/crates/rusticol-python/stubs/pyamplicol/__init__.pyi new file mode 100644 index 0000000000..8a98ab015e --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-python/stubs/pyamplicol/__init__.pyi @@ -0,0 +1 @@ +# SPDX-License-Identifier: 0BSD diff --git a/pyamplicol/rust/crates/rusticol-python/stubs/pyamplicol/_rusticol.pyi b/pyamplicol/rust/crates/rusticol-python/stubs/pyamplicol/_rusticol.pyi new file mode 100644 index 0000000000..20a9c9d343 --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-python/stubs/pyamplicol/_rusticol.pyi @@ -0,0 +1,312 @@ +# SPDX-License-Identifier: 0BSD + +from collections.abc import Callable, Mapping, Sequence +from os import PathLike +from typing import Literal, TypeAlias + +ScalarInput: TypeAlias = float | int +Momenta: TypeAlias = Sequence[Sequence[Sequence[ScalarInput]]] +ModelParameterValue: TypeAlias = complex | float | int +ResolvedScalar: TypeAlias = float +RuntimeProfile: TypeAlias = Mapping[str, object] + +class TargetInfo: + """Native target triple and CPU features supported by this runtime.""" + + @property + def triple(self) -> str: ... + @property + def cpu_features(self) -> list[str]: ... + +class RusticolError(Exception): ... +class ArtifactError(RusticolError): ... +class CompatibilityError(RusticolError): ... +class EvaluationError(RusticolError): ... +class SelectorError(EvaluationError): ... +class ModelParameterError(EvaluationError): ... + +class ExternalParticle: + """One external particle in the public process momentum order.""" + + @property + def index(self) -> int: ... + @property + def label(self) -> int: ... + @property + def name(self) -> str: ... + @property + def pdg_id(self) -> int: ... + @property + def state(self) -> Literal["incoming", "outgoing"]: ... + @property + def momentum_slot(self) -> int: ... + +class HelicityConfiguration: + """A physical helicity vector and its computed representative metadata.""" + + @property + def id(self) -> str: ... + @property + def index(self) -> int: ... + @property + def values(self) -> list[int]: ... + @property + def computed(self) -> bool: ... + @property + def structural_zero(self) -> bool: ... + @property + def representative_id(self) -> str: ... + @property + def coefficient(self) -> float: ... + +class ColorFlow: + """A stable physical LC flow selector and ordered colored word.""" + + @property + def id(self) -> str: ... + @property + def index(self) -> int: ... + @property + def word(self) -> list[int]: ... + @property + def computed(self) -> bool: ... + @property + def representative_id(self) -> str: ... + @property + def coefficient(self) -> float: ... + +class ContractedColorComponent: + """The singleton contracted-color axis used by NLC and full color.""" + + @property + def id(self) -> str: ... + @property + def index(self) -> int: ... + @property + def description(self) -> str: ... + +class ModelParameter: + """A runtime model parameter and its default complex value.""" + + @property + def name(self) -> str: ... + @property + def kind(self) -> str: ... + @property + def default_real(self) -> float: ... + @property + def default_imaginary(self) -> float: ... + @property + def mutable(self) -> bool: ... + +class ReductionGroup: + @property + def id(self) -> str: ... + @property + def representative_helicity_id(self) -> str: ... + @property + def representative_color_id(self) -> str: ... + @property + def physical_helicity_ids(self) -> list[str]: ... + @property + def physical_color_ids(self) -> list[str]: ... + +class PhysicsReduction: + @property + def kind(self) -> Literal["lc-diagonal", "contracted-color"]: ... + @property + def groups(self) -> list[ReductionGroup]: ... + +class ProcessPhysics: + """Physical axes, selector capabilities, and reductions for one process.""" + + @property + def process_id(self) -> str: ... + @property + def process(self) -> str: ... + @property + def color_accuracy(self) -> Literal["lc", "nlc", "full"]: ... + @property + def helicity_coverage(self) -> str: ... + @property + def color_coverage(self) -> str: ... + @property + def color_kind(self) -> str: ... + @property + def structural_zero_helicity_count(self) -> int: ... + @property + def external_particles(self) -> list[ExternalParticle]: ... + @property + def helicities(self) -> list[HelicityConfiguration]: ... + @property + def color_flows(self) -> list[ColorFlow]: ... + @property + def contracted_color_components(self) -> list[ContractedColorComponent]: ... + @property + def reduction(self) -> PhysicsReduction: ... + @property + def model_parameters(self) -> list[ModelParameter]: ... + @property + def selector_capabilities(self) -> list[str]: ... + +class ResolvedEvaluation: + """Values shaped ``(point, helicity, color)``. + + ``total()`` sums the non-point axes. LC uses physical color flows; NLC and + full color use one contracted color component. + """ + + @property + def values(self) -> list[list[list[ResolvedScalar]]]: ... + @property + def shape(self) -> tuple[int, int, int]: ... + @property + def helicity_ids(self) -> list[str]: ... + @property + def color_ids(self) -> list[str]: ... + @property + def color_flow_ids(self) -> list[str]: ... + @property + def color_accuracy(self) -> Literal["lc", "nlc", "full"]: ... + @property + def precision(self) -> int: ... + def total(self) -> list[ResolvedScalar]: ... + +class Runtime: + """Native f64 runtime for one process in a generated artifact.""" + + def __init__( + self, + artifact: str | PathLike[str], + *, + process: str | None = None, + model_parameters: Mapping[str, ModelParameterValue] | None = None, + mute_warnings: bool = False, + ) -> None: ... + @staticmethod + def load( + artifact: str | PathLike[str], + *, + process: str | None = None, + model_parameters: Mapping[str, ModelParameterValue] | None = None, + mute_warnings: bool = False, + ) -> Runtime: + """Load by stable process ID, alias ID, or exact process expression.""" + ... + @property + def process(self) -> str: ... + @property + def artifact_id(self) -> str: ... + @property + def process_id(self) -> str: ... + @property + def color_accuracy(self) -> Literal["lc", "nlc", "full"]: ... + @property + def physics(self) -> ProcessPhysics: ... + def metadata_json(self) -> str: ... + def physics_json(self) -> str: ... + def save(self, path: str | PathLike[str]) -> None: + """Save the retained on-the-fly recursion, without model parameters.""" + ... + def load_cache(self, path: str | PathLike[str]) -> None: + """Restore a saved recursion into the same loaded on-the-fly process.""" + ... + def _on_the_fly_warm_up_f64_json( + self, + momenta: Momenta, + helicity_ids: Sequence[str] | None = None, + color_flow_ids: Sequence[str] | None = None, + progress_callback: Callable[[str], bool | None] | None = None, + *, + n_cores: int | None = None, + ) -> str: ... + def evaluate( + self, + momenta: Momenta, + *, + helicities: Sequence[str] | None = None, + color_flows: Sequence[str] | None = None, + helicity_by_point: Sequence[int] | None = None, + color_flow_by_point: Sequence[int] | None = None, + precision: Literal[16] = 16, + ) -> list[float]: + """Return one fully summed matrix element for every input point.""" + ... + def _benchmark_f64_wall_time( + self, + momenta: Momenta, + repetitions: int, + *, + helicities: Sequence[str] | None = None, + color_flows: Sequence[str] | None = None, + helicity_by_point: Sequence[int] | None = None, + color_flow_by_point: Sequence[int] | None = None, + precision: Literal[16] = 16, + ) -> float: ... + def profile( + self, + momenta: Momenta, + *, + helicities: Sequence[str] | None = None, + color_flows: Sequence[str] | None = None, + helicity_by_point: Sequence[int] | None = None, + color_flow_by_point: Sequence[int] | None = None, + precision: Literal[16] = 16, + include_values: bool = False, + ) -> RuntimeProfile: ... + def profile_repeated( + self, + momenta: Momenta, + repetitions: int, + *, + helicities: Sequence[str] | None = None, + color_flows: Sequence[str] | None = None, + helicity_by_point: Sequence[int] | None = None, + color_flow_by_point: Sequence[int] | None = None, + precision: Literal[16] = 16, + include_values: bool = False, + ) -> RuntimeProfile: ... + def evaluate_profile( + self, + momenta: Momenta, + *, + helicities: Sequence[str] | None = None, + color_flows: Sequence[str] | None = None, + helicity_by_point: Sequence[int] | None = None, + color_flow_by_point: Sequence[int] | None = None, + precision: Literal[16] = 16, + include_values: bool = False, + ) -> RuntimeProfile: ... + def evaluate_resolved( + self, + momenta: Momenta, + *, + helicities: Sequence[str] | None = None, + color_flows: Sequence[str] | None = None, + precision: Literal[16] = 16, + ) -> ResolvedEvaluation: + """Return individual physical helicity and color components.""" + ... + def evaluate_with_prec( + self, momenta: Momenta, decimal_digit_precision: Literal[16] + ) -> list[float]: ... + def evaluate_resolved_with_prec( + self, + momenta: Momenta, + decimal_digit_precision: Literal[16], + helicities: Sequence[str] | None = None, + color_flows: Sequence[str] | None = None, + ) -> ResolvedEvaluation: ... + def set_model_parameters(self, mapping: Mapping[str, ModelParameterValue]) -> None: + """Validate and atomically apply a parameter-update batch.""" + ... + def clear(self) -> None: ... + def set_model_parameter(self, name: str, value: ModelParameterValue) -> None: ... + def mute_warnings(self) -> None: ... + def unmute_warnings(self) -> None: ... + def take_warnings(self) -> list[str]: ... + +def abi_version() -> int: ... +def package_version() -> str: ... +def native_build_inputs_sha256() -> str: ... +def target_info() -> TargetInfo: ... diff --git a/pyamplicol/rust/crates/rusticol-python/tests/pyrightconfig.json b/pyamplicol/rust/crates/rusticol-python/tests/pyrightconfig.json new file mode 100644 index 0000000000..88f6c84f0b --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-python/tests/pyrightconfig.json @@ -0,0 +1,7 @@ +{ + "include": ["typing_consumer.py"], + "extraPaths": ["../stubs"], + "pythonVersion": "3.11", + "typeCheckingMode": "strict", + "reportMissingModuleSource": "none" +} diff --git a/pyamplicol/rust/crates/rusticol-python/tests/stub_contract.rs b/pyamplicol/rust/crates/rusticol-python/tests/stub_contract.rs new file mode 100644 index 0000000000..8f7b8bcaf8 --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-python/tests/stub_contract.rs @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: 0BSD + +const BINDING: &str = include_str!("../src/lib.rs"); +const STUB: &str = include_str!("../stubs/pyamplicol/_rusticol.pyi"); +const CONSUMER: &str = include_str!("typing_consumer.py"); + +#[test] +fn stub_covers_the_exported_binding_surface() { + for name in [ + "Runtime", + "ProcessPhysics", + "ExternalParticle", + "HelicityConfiguration", + "ColorFlow", + "ContractedColorComponent", + "ModelParameter", + "ResolvedEvaluation", + "RusticolError", + "ArtifactError", + "CompatibilityError", + "EvaluationError", + "SelectorError", + "ModelParameterError", + ] { + assert!(BINDING.contains(name), "binding is missing {name}"); + assert!( + STUB.contains(&format!("class {name}")), + "stub is missing {name}" + ); + } + for function in ["abi_version", "package_version"] { + assert!(BINDING.contains(&format!("fn {function}"))); + assert!(STUB.contains(&format!("def {function}"))); + } +} + +#[test] +fn f64_precision_and_typed_metadata_are_exercised_by_a_consumer() { + assert!(BINDING.contains("fn artifact_id(&self) -> &str")); + assert!(STUB.contains("def artifact_id(self) -> str")); + assert!(CONSUMER.contains("assert_type(runtime.artifact_id, str)")); + assert!(BINDING.contains("color_flows=None, precision=16")); + assert!(STUB.contains("precision: Literal[16] = 16")); + assert!(BINDING.contains("only precision=16")); + assert!(CONSUMER.contains("evaluate(momenta, precision=16)")); + assert!(CONSUMER.contains("evaluate_resolved(momenta, precision=16)")); + assert!(!CONSUMER.contains("Decimal")); + for field in [ + "external_particles", + "helicities", + "color_flows", + "contracted_color_components", + "model_parameters", + ] { + assert!(STUB.contains(&format!("def {field}"))); + assert!(CONSUMER.contains(field)); + } +} diff --git a/pyamplicol/rust/crates/rusticol-python/tests/typing_consumer.py b/pyamplicol/rust/crates/rusticol-python/tests/typing_consumer.py new file mode 100644 index 0000000000..244fe75b71 --- /dev/null +++ b/pyamplicol/rust/crates/rusticol-python/tests/typing_consumer.py @@ -0,0 +1,42 @@ +# SPDX-License-Identifier: 0BSD + +from collections.abc import Mapping +from typing import assert_type + +from pyamplicol._rusticol import ( + ColorFlow, + ContractedColorComponent, + ExternalParticle, + HelicityConfiguration, + ModelParameter, + ProcessPhysics, + ResolvedEvaluation, + Runtime, +) + + +def consume_runtime(runtime: Runtime) -> None: + momenta = [[[10.0, 0.0, 0.0, 10.0]] * 3] + + assert_type(runtime.artifact_id, str) + assert_type(runtime.evaluate(momenta), list[float]) + assert_type(runtime.evaluate(momenta, precision=16), list[float]) + assert_type(runtime.profile(momenta, precision=16), Mapping[str, object]) + assert_type(runtime.evaluate_profile(momenta, precision=16), Mapping[str, object]) + assert_type(runtime.evaluate_resolved(momenta, precision=16), ResolvedEvaluation) + + physics = assert_type(runtime.physics, ProcessPhysics) + assert_type(physics.external_particles, list[ExternalParticle]) + assert_type(physics.helicities, list[HelicityConfiguration]) + assert_type(physics.color_flows, list[ColorFlow]) + assert_type(physics.contracted_color_components, list[ContractedColorComponent]) + assert_type(physics.model_parameters, list[ModelParameter]) + + resolved = runtime.evaluate_resolved( + momenta, + helicities=["h0"], + color_flows=["c0"], + precision=16, + ) + assert_type(resolved.values, list[list[list[float]]]) + assert_type(resolved.total(), list[float]) diff --git a/pyamplicol/schemas/README.md b/pyamplicol/schemas/README.md new file mode 100644 index 0000000000..ccf70aa16e --- /dev/null +++ b/pyamplicol/schemas/README.md @@ -0,0 +1,47 @@ +# Shared Schemas + +These JSON Schemas are the normative wire contracts shared by the Python +generator, Rusticol, the native SDK, and installed-package self-tests. + +- `artifact-manifest-v3.schema.json` describes the only top-level process + artifact accepted by pyAmpliCol 0.1. +- `runtime-physics-v1.schema.json` describes either resolved public helicity + and color axes or compact on-the-fly public particle/parameter metadata; + resolved artifacts also carry reductions and selectors. +- `reference-physics-v2.schema.json` is the repository/sdist-only contract for + exact decimal physics baselines, complete resolved axes, structural zeros, + model-derived external spin/color/mass metadata, topology, canonical + reduction-group mappings, normalization, payload hashes, and capture + provenance. +- `reference-oracle-evidence-v2.schema.json` records independent analytic or + pinned-Fortran evidence. Evidence can certify resolved cells, per-helicity + aggregates, or totals according to the oracle's actual semantics; it must + not promote diagnostic contraction partitions into physical color flows. + This is a developer validation format, not a runtime artifact format. +- `reference-fixture-bundle-v1.schema.json` is the final commit marker for a + captured fixture and its evidence documents. It names and hashes every JSON + member; readers reject data files until this manifest is present and valid. + +Evaluator plans remain private payloads. The top-level manifest nevertheless +records their canonical required runtime capabilities so a loader can reject +an unsupported backend before opening evaluator bytes. Direct SymJIT f64 +applications, optional Symbolica evaluator state, compiled libraries, and +their checksums, producer ABI, and target compatibility are covered by the +manifest; their internal representation is not part of the public API. + +Loaders must perform checks that JSON Schema cannot express before opening a +payload: + +1. Resolve every payload beneath the artifact root without following a + symlink outside it. +2. Reject absolute paths, `..` traversal, duplicate normalized paths, + missing or unexpected executable payloads, and non-regular files. +3. Verify every byte size and SHA-256 digest before deserializing or loading + executable state. +4. Check package, process-artifact, runtime-physics, compiled-model, + evaluator runtime capabilities, relevant evaluator serialization ABI, + C-ABI, target-triple, and CPU-feature compatibility. + +Unknown top-level fields are rejected. Deliberate extension data belongs in +the explicit `extensions` objects and must not affect physics or executable +loading. diff --git a/pyamplicol/schemas/artifact-manifest-v3.schema.json b/pyamplicol/schemas/artifact-manifest-v3.schema.json new file mode 100644 index 0000000000..dee9c7c2b1 --- /dev/null +++ b/pyamplicol/schemas/artifact-manifest-v3.schema.json @@ -0,0 +1,574 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://pyamplicol.org/schemas/artifact-manifest-v3.schema.json", + "title": "pyAmpliCol process artifact manifest v3", + "type": "object", + "additionalProperties": false, + "required": [ + "schema_version", + "kind", + "artifact_id", + "created_utc", + "producer", + "model", + "configuration", + "processes", + "runtime", + "payloads", + "dependencies" + ], + "properties": { + "schema_version": { + "const": 3 + }, + "kind": { + "enum": [ + "pyamplicol-process", + "pyamplicol-process-set" + ] + }, + "artifact_id": { + "$ref": "#/$defs/sha256" + }, + "created_utc": { + "type": "string", + "format": "date-time" + }, + "producer": { + "$ref": "#/$defs/producer" + }, + "model": { + "$ref": "#/$defs/model" + }, + "configuration": { + "$ref": "#/$defs/configuration" + }, + "processes": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/$defs/process" + } + }, + "default_process_id": { + "oneOf": [ + { + "$ref": "#/$defs/public_id" + }, + { + "type": "null" + } + ], + "default": null + }, + "runtime": { + "$ref": "#/$defs/runtime" + }, + "payloads": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/$defs/payload" + } + }, + "dependencies": { + "type": "array", + "items": { + "$ref": "#/$defs/dependency" + } + }, + "extensions": { + "type": "object", + "default": {} + } + }, + "$defs": { + "sha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "git_revision": { + "type": "string", + "pattern": "^[a-f0-9]{40}$" + }, + "public_id": { + "type": "string", + "minLength": 1, + "maxLength": 255, + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:+,~-]*$" + }, + "runtime_capability": { + "enum": [ + "symjit.application.complex-f64.v1", + "symbolica.legacy-jit-container.complex-f64.v1", + "symbolica.compiled-cpp.complex-f64.v1", + "symbolica.compiled-asm.complex-f64.v1" + ] + }, + "relative_path": { + "type": "string", + "minLength": 1, + "pattern": "^(?!/)(?!.*(?:^|/)\\.\\.(?:/|$))(?!.*\\\\).+$" + }, + "version_set": { + "type": "object", + "additionalProperties": false, + "required": [ + "python_api", + "toml", + "compiled_model", + "process_artifact", + "runtime_physics", + "symbolica_serialization", + "c_abi" + ], + "properties": { + "python_api": { + "type": "integer", + "minimum": 1 + }, + "toml": { + "type": "integer", + "minimum": 1 + }, + "compiled_model": { + "type": "integer", + "minimum": 1 + }, + "process_artifact": { + "const": 3 + }, + "runtime_physics": { + "const": 1 + }, + "symbolica_serialization": { + "type": "string", + "minLength": 1 + }, + "c_abi": { + "type": "integer", + "minimum": 1 + } + } + }, + "target": { + "type": "object", + "additionalProperties": false, + "required": [ + "triple", + "cpu_features" + ], + "properties": { + "triple": { + "type": "string", + "minLength": 1 + }, + "cpu_features": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + } + } + }, + "producer": { + "type": "object", + "additionalProperties": false, + "required": [ + "distribution", + "version", + "versions", + "target" + ], + "properties": { + "distribution": { + "const": "pyamplicol" + }, + "version": { + "type": "string", + "minLength": 1 + }, + "versions": { + "$ref": "#/$defs/version_set" + }, + "target": { + "$ref": "#/$defs/target" + }, + "git_revision": { + "oneOf": [ + { + "$ref": "#/$defs/git_revision" + }, + { + "type": "null" + } + ] + }, + "native_build_inputs_sha256": { + "oneOf": [ + { + "$ref": "#/$defs/sha256" + }, + { + "type": "null" + } + ] + } + } + }, + "model": { + "type": "object", + "additionalProperties": false, + "required": [ + "name", + "source_kind", + "content_sha256", + "compiled_schema_version" + ], + "properties": { + "name": { + "type": "string", + "minLength": 1 + }, + "source_kind": { + "enum": [ + "built-in-sm", + "built-in-sm-heft", + "ufo", + "ufo-json", + "compiled-model" + ] + }, + "content_sha256": { + "$ref": "#/$defs/sha256" + }, + "compiled_schema_version": { + "type": "integer", + "minimum": 1 + }, + "restriction": { + "oneOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "null" + } + ], + "default": null + } + } + }, + "configuration": { + "type": "object", + "additionalProperties": false, + "required": [ + "toml_schema_version", + "requested_path", + "effective_path", + "adjustments" + ], + "properties": { + "toml_schema_version": { + "const": 1 + }, + "requested_path": { + "$ref": "#/$defs/relative_path" + }, + "effective_path": { + "$ref": "#/$defs/relative_path" + }, + "adjustments": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "path", + "reason" + ], + "properties": { + "path": { + "type": "string", + "minLength": 1 + }, + "reason": { + "type": "string", + "minLength": 1 + } + } + } + } + } + }, + "process": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "expression", + "color_accuracy", + "external_pdgs", + "physics_path", + "required_runtime_capabilities", + "aliases" + ], + "properties": { + "id": { + "$ref": "#/$defs/public_id" + }, + "expression": { + "type": "string", + "minLength": 1 + }, + "color_accuracy": { + "enum": [ + "lc", + "nlc", + "full" + ] + }, + "external_pdgs": { + "type": "array", + "minItems": 3, + "items": { + "type": "integer" + } + }, + "physics_path": { + "$ref": "#/$defs/relative_path" + }, + "required_runtime_capabilities": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { + "$ref": "#/$defs/runtime_capability" + } + }, + "aliases": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "expression", + "external_pdgs", + "external_permutation" + ], + "properties": { + "id": { + "$ref": "#/$defs/public_id" + }, + "expression": { + "type": "string", + "minLength": 1 + }, + "external_pdgs": { + "type": "array", + "minItems": 3, + "items": { + "type": "integer" + } + }, + "external_permutation": { + "type": "array", + "minItems": 3, + "uniqueItems": true, + "prefixItems": [ + { + "const": 0 + }, + { + "const": 1 + } + ], + "items": { + "type": "integer", + "minimum": 0 + } + } + } + } + } + } + }, + "runtime": { + "type": "object", + "additionalProperties": false, + "required": [ + "engine", + "engine_version", + "evaluator_manifest_path", + "api_bundle_path", + "required_runtime_capabilities" + ], + "properties": { + "engine": { + "const": "rusticol" + }, + "engine_version": { + "type": "string", + "minLength": 1 + }, + "evaluator_manifest_path": { + "$ref": "#/$defs/relative_path" + }, + "api_bundle_path": { + "oneOf": [ + { + "$ref": "#/$defs/relative_path" + }, + { + "type": "null" + } + ] + }, + "required_runtime_capabilities": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { + "$ref": "#/$defs/runtime_capability" + } + } + } + }, + "payload": { + "type": "object", + "additionalProperties": false, + "allOf": [ + { + "if": { + "properties": { + "executable": { + "const": true + } + }, + "required": [ + "executable" + ] + }, + "then": { + "properties": { + "role": { + "enum": [ + "evaluator-state", + "api-source", + "api-build-file" + ] + } + } + } + }, + { + "if": { + "properties": { + "role": { + "const": "evaluator-state" + } + }, + "required": [ + "role" + ] + }, + "then": { + "required": [ + "target" + ] + } + } + ], + "required": [ + "path", + "role", + "media_type", + "size_bytes", + "sha256", + "executable" + ], + "properties": { + "path": { + "$ref": "#/$defs/relative_path" + }, + "role": { + "enum": [ + "configuration-requested", + "configuration-effective", + "compiled-model", + "runtime-physics", + "evaluator-manifest", + "evaluator-state", + "model-parameters", + "validation-momenta", + "api-source", + "api-build-file", + "sdk-metadata", + "process-groups" + ] + }, + "media_type": { + "type": "string", + "minLength": 1 + }, + "size_bytes": { + "type": "integer", + "minimum": 0 + }, + "sha256": { + "$ref": "#/$defs/sha256" + }, + "executable": { + "type": "boolean" + }, + "target": { + "$ref": "#/$defs/target" + }, + "process_id": { + "$ref": "#/$defs/public_id" + } + } + }, + "dependency": { + "type": "object", + "additionalProperties": false, + "required": [ + "name", + "version", + "source", + "license" + ], + "properties": { + "name": { + "type": "string", + "minLength": 1 + }, + "version": { + "type": "string", + "minLength": 1 + }, + "source": { + "type": "string", + "minLength": 1 + }, + "license": { + "type": "string", + "minLength": 1 + }, + "content_sha256": { + "$ref": "#/$defs/sha256" + }, + "revision": { + "type": "string", + "minLength": 1 + }, + "patch_sha256": { + "$ref": "#/$defs/sha256" + } + } + } + } +} diff --git a/pyamplicol/schemas/reference-fixture-bundle-v1.schema.json b/pyamplicol/schemas/reference-fixture-bundle-v1.schema.json new file mode 100644 index 0000000000..03b5667888 --- /dev/null +++ b/pyamplicol/schemas/reference-fixture-bundle-v1.schema.json @@ -0,0 +1,48 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://pyamplicol.org/schemas/reference-fixture-bundle-v1.schema.json", + "title": "pyAmpliCol reference fixture bundle manifest v1", + "type": "object", + "additionalProperties": false, + "required": [ + "schema_version", + "kind", + "files", + "bundle_sha256" + ], + "properties": { + "schema_version": { + "const": 1 + }, + "kind": { + "const": "pyamplicol-reference-fixture-bundle" + }, + "files": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "path", + "sha256" + ], + "properties": { + "path": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._-]*\\.json$" + }, + "sha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + } + } + } + }, + "bundle_sha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + } + } +} diff --git a/pyamplicol/schemas/reference-oracle-evidence-v2.schema.json b/pyamplicol/schemas/reference-oracle-evidence-v2.schema.json new file mode 100644 index 0000000000..42f485c580 --- /dev/null +++ b/pyamplicol/schemas/reference-oracle-evidence-v2.schema.json @@ -0,0 +1,436 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://pyamplicol.org/schemas/reference-oracle-evidence-v2.schema.json", + "title": "pyAmpliCol independent oracle evidence v2", + "type": "object", + "additionalProperties": false, + "required": [ + "evidence_schema_version", + "kind", + "evidence_set_id", + "captured_at", + "oracle", + "dependency_ids", + "records" + ], + "properties": { + "evidence_schema_version": { + "const": 2 + }, + "kind": { + "const": "pyamplicol-reference-oracle-evidence" + }, + "evidence_set_id": { + "$ref": "#/$defs/public_id" + }, + "captured_at": { + "type": "string", + "format": "date-time" + }, + "oracle": { + "$ref": "#/$defs/oracle" + }, + "dependency_ids": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { + "$ref": "#/$defs/public_id" + } + }, + "records": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/$defs/record" + } + } + }, + "$defs": { + "public_id": { + "type": "string", + "minLength": 1, + "maxLength": 255, + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:+,~-]*$" + }, + "sha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "decimal": { + "type": "string", + "pattern": "^(?:0|-?(?:(?:[1-9][0-9]*)(?:\\.[0-9]*[1-9])?|0\\.[0-9]*[1-9]))$" + }, + "nonnegative_decimal": { + "type": "string", + "pattern": "^(?:0|[1-9][0-9]*)(?:\\.[0-9]*[1-9])?$" + }, + "oracle": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "name", + "implementation", + "revision", + "content_sha256", + "independence_statement", + "validation_profile", + "tolerance_ceiling" + ], + "properties": { + "id": { + "$ref": "#/$defs/public_id" + }, + "name": { + "type": "string", + "minLength": 1 + }, + "implementation": { + "type": "string", + "minLength": 1 + }, + "revision": { + "type": "string", + "minLength": 1 + }, + "content_sha256": { + "$ref": "#/$defs/sha256" + }, + "independence_statement": { + "type": "string", + "minLength": 1 + }, + "validation_profile": { + "enum": [ + "exact", + "binary64", + "high-precision" + ] + }, + "tolerance_ceiling": { + "$ref": "#/$defs/tolerances" + } + } + }, + "process_identity": { + "type": "object", + "additionalProperties": false, + "required": [ + "expression", + "ordered_external_pdgs", + "ordered_external_leg_ids", + "source_to_row_permutation", + "row_id", + "color_order_count", + "ordered_color_legs" + ], + "properties": { + "expression": { + "type": "string", + "minLength": 1 + }, + "ordered_external_pdgs": { + "type": "array", + "minItems": 3, + "items": { + "type": "integer" + } + }, + "ordered_external_leg_ids": { + "type": "array", + "minItems": 3, + "uniqueItems": true, + "items": { + "$ref": "#/$defs/public_id" + } + }, + "source_to_row_permutation": { + "type": "array", + "minItems": 3, + "uniqueItems": true, + "items": { + "type": "integer", + "minimum": 0 + } + }, + "row_id": { + "oneOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "null" + } + ] + }, + "color_order_count": { + "type": "integer", + "minimum": 1 + }, + "ordered_color_legs": { + "type": "array", + "uniqueItems": true, + "items": { + "$ref": "#/$defs/public_id" + } + } + } + }, + "tolerances": { + "type": "object", + "additionalProperties": false, + "required": [ + "relative", + "absolute" + ], + "properties": { + "relative": { + "$ref": "#/$defs/nonnegative_decimal" + }, + "absolute": { + "$ref": "#/$defs/nonnegative_decimal" + } + } + }, + "decimal_vector": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/$defs/decimal" + } + }, + "decimal_matrix": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/$defs/decimal_vector" + } + }, + "record": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "case_id", + "point_id", + "independent_of_pyamplicol", + "arithmetic_precision_bits", + "round_trip_decimal_digits", + "certified_decimal_digits", + "arithmetic", + "coverage", + "helicity_ids", + "color_ids", + "observed_total", + "observed_helicity_totals", + "observed_values", + "process_identity", + "input_sha256", + "physics_case_sha256", + "oracle_output_sha256", + "evidence_record_sha256", + "command", + "tolerances" + ], + "properties": { + "id": { + "$ref": "#/$defs/public_id" + }, + "case_id": { + "$ref": "#/$defs/public_id" + }, + "point_id": { + "$ref": "#/$defs/public_id" + }, + "independent_of_pyamplicol": { + "const": true + }, + "arithmetic_precision_bits": { + "type": "integer", + "minimum": 2 + }, + "round_trip_decimal_digits": { + "type": "integer", + "minimum": 1 + }, + "certified_decimal_digits": { + "type": "integer", + "minimum": 1 + }, + "arithmetic": { + "enum": [ + "binary64", + "decimal", + "rational", + "analytic" + ] + }, + "coverage": { + "enum": [ + "total", + "helicity-aggregate", + "resolved" + ] + }, + "helicity_ids": { + "type": "array", + "uniqueItems": true, + "items": { + "$ref": "#/$defs/public_id" + } + }, + "color_ids": { + "type": "array", + "uniqueItems": true, + "items": { + "$ref": "#/$defs/public_id" + } + }, + "observed_total": { + "$ref": "#/$defs/decimal" + }, + "observed_helicity_totals": { + "oneOf": [ + { + "$ref": "#/$defs/decimal_vector" + }, + { + "type": "null" + } + ] + }, + "observed_values": { + "oneOf": [ + { + "$ref": "#/$defs/decimal_matrix" + }, + { + "type": "null" + } + ] + }, + "process_identity": { + "$ref": "#/$defs/process_identity" + }, + "input_sha256": { + "$ref": "#/$defs/sha256" + }, + "physics_case_sha256": { + "$ref": "#/$defs/sha256" + }, + "oracle_output_sha256": { + "$ref": "#/$defs/sha256" + }, + "evidence_record_sha256": { + "$ref": "#/$defs/sha256" + }, + "command": { + "type": "array", + "minItems": 1, + "items": { + "type": "string", + "minLength": 1 + } + }, + "tolerances": { + "$ref": "#/$defs/tolerances" + } + }, + "allOf": [ + { + "if": { + "properties": { + "arithmetic": { + "const": "binary64" + } + }, + "required": [ + "arithmetic" + ] + }, + "then": { + "properties": { + "arithmetic_precision_bits": { + "const": 53 + }, + "round_trip_decimal_digits": { + "const": 17 + }, + "certified_decimal_digits": { + "maximum": 15 + } + } + } + }, + { + "if": { + "properties": { + "coverage": { + "const": "total" + } + }, + "required": [ + "coverage" + ] + }, + "then": { + "properties": { + "observed_helicity_totals": { + "type": "null" + }, + "observed_values": { + "type": "null" + } + } + } + }, + { + "if": { + "properties": { + "coverage": { + "const": "helicity-aggregate" + } + }, + "required": [ + "coverage" + ] + }, + "then": { + "properties": { + "observed_helicity_totals": { + "$ref": "#/$defs/decimal_vector" + }, + "observed_values": { + "type": "null" + } + } + } + }, + { + "if": { + "properties": { + "coverage": { + "const": "resolved" + } + }, + "required": [ + "coverage" + ] + }, + "then": { + "properties": { + "observed_helicity_totals": { + "type": "null" + }, + "observed_values": { + "$ref": "#/$defs/decimal_matrix" + } + } + } + } + ] + } + } +} diff --git a/pyamplicol/schemas/reference-physics-v2.schema.json b/pyamplicol/schemas/reference-physics-v2.schema.json new file mode 100644 index 0000000000..b875160166 --- /dev/null +++ b/pyamplicol/schemas/reference-physics-v2.schema.json @@ -0,0 +1,1131 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://pyamplicol.org/schemas/reference-physics-v2.schema.json", + "title": "pyAmpliCol test reference physics fixture v2", + "type": "object", + "additionalProperties": false, + "required": [ + "fixture_schema_version", + "kind", + "fixture_id", + "provenance", + "dependencies", + "models", + "processes", + "points", + "cases", + "evidence_sets" + ], + "properties": { + "fixture_schema_version": { + "const": 2 + }, + "kind": { + "const": "pyamplicol-reference-physics" + }, + "fixture_id": { + "$ref": "#/$defs/public_id" + }, + "provenance": { + "$ref": "#/$defs/fixture_provenance" + }, + "dependencies": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/$defs/dependency" + } + }, + "models": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/$defs/model" + } + }, + "processes": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/$defs/process" + } + }, + "points": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/$defs/point" + } + }, + "cases": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/$defs/case" + } + }, + "evidence_sets": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { + "$ref": "#/$defs/public_id" + } + } + }, + "$defs": { + "public_id": { + "type": "string", + "minLength": 1, + "maxLength": 255, + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:+,~-]*$" + }, + "sha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "git_revision": { + "type": "string", + "pattern": "^[a-f0-9]{40}$" + }, + "decimal": { + "type": "string", + "pattern": "^(?:0|-?(?:(?:[1-9][0-9]*)(?:\\.[0-9]*[1-9])?|0\\.[0-9]*[1-9]))$" + }, + "nonnegative_decimal": { + "type": "string", + "pattern": "^(?:0|[1-9][0-9]*)(?:\\.[0-9]*[1-9])?$" + }, + "positive_decimal": { + "type": "string", + "pattern": "^(?:(?:[1-9][0-9]*)(?:\\.[0-9]*[1-9])?|0\\.[0-9]*[1-9])$" + }, + "complex_decimal": { + "type": "object", + "additionalProperties": false, + "required": [ + "real", + "imag" + ], + "properties": { + "real": { + "$ref": "#/$defs/decimal" + }, + "imag": { + "$ref": "#/$defs/decimal" + } + } + }, + "fixture_provenance": { + "type": "object", + "additionalProperties": false, + "required": [ + "source_repository", + "source_revision", + "source_tree_sha256", + "captured_at", + "capture_command", + "working_tree_clean", + "memory_watchdog_gb" + ], + "properties": { + "source_repository": { + "type": "string", + "format": "uri" + }, + "source_revision": { + "$ref": "#/$defs/git_revision" + }, + "source_tree_sha256": { + "$ref": "#/$defs/sha256" + }, + "captured_at": { + "type": "string", + "format": "date-time" + }, + "capture_command": { + "type": "array", + "minItems": 1, + "items": { + "type": "string", + "minLength": 1 + } + }, + "working_tree_clean": { + "type": "boolean" + }, + "memory_watchdog_gb": { + "type": "integer", + "minimum": 1 + } + } + }, + "dependency": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "name", + "version", + "revision", + "content_sha256", + "serialization_abi", + "license" + ], + "properties": { + "id": { + "$ref": "#/$defs/public_id" + }, + "name": { + "type": "string", + "minLength": 1 + }, + "version": { + "type": "string", + "minLength": 1 + }, + "revision": { + "oneOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "null" + } + ] + }, + "content_sha256": { + "$ref": "#/$defs/sha256" + }, + "patch_sha256": { + "$ref": "#/$defs/sha256" + }, + "serialization_abi": { + "oneOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "null" + } + ] + }, + "license": { + "type": "string", + "minLength": 1 + } + } + }, + "model": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "name", + "source_kind", + "content_sha256", + "compiled_model_sha256", + "compiled_schema_version", + "restriction", + "dependency_ids", + "parameter_defaults" + ], + "properties": { + "id": { + "$ref": "#/$defs/public_id" + }, + "name": { + "type": "string", + "minLength": 1 + }, + "source_kind": { + "enum": [ + "built-in-sm", + "built-in-sm-heft", + "ufo", + "ufo-json", + "compiled-model" + ] + }, + "content_sha256": { + "$ref": "#/$defs/sha256" + }, + "compiled_model_sha256": { + "$ref": "#/$defs/sha256", + "description": "Canonical hash of the compiled model contract excluding conversion and phase timing diagnostics." + }, + "compiled_schema_version": { + "type": "integer", + "minimum": 1 + }, + "restriction": { + "oneOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "null" + } + ] + }, + "dependency_ids": { + "type": "array", + "uniqueItems": true, + "items": { + "$ref": "#/$defs/public_id" + } + }, + "parameter_defaults": { + "type": "object", + "propertyNames": { + "type": "string", + "minLength": 1 + }, + "additionalProperties": { + "$ref": "#/$defs/complex_decimal" + } + } + } + }, + "process": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "expression", + "external_pdgs", + "external_labels", + "external_leg_ids", + "external_spins", + "external_colors", + "external_masses", + "external_helicity_domains", + "initial_state_count", + "alias_of", + "final_state_permutation" + ], + "properties": { + "id": { + "$ref": "#/$defs/public_id" + }, + "expression": { + "type": "string", + "minLength": 1 + }, + "external_pdgs": { + "type": "array", + "minItems": 3, + "items": { + "type": "integer" + } + }, + "external_labels": { + "type": "array", + "minItems": 3, + "uniqueItems": true, + "items": { + "type": "integer", + "minimum": 1 + } + }, + "external_leg_ids": { + "type": "array", + "minItems": 3, + "uniqueItems": true, + "items": { + "$ref": "#/$defs/public_id" + } + }, + "external_spins": { + "type": "array", + "minItems": 3, + "items": { + "type": "integer", + "minimum": 1 + } + }, + "external_colors": { + "type": "array", + "minItems": 3, + "items": { + "enum": [ + -3, + 1, + 3, + 8 + ] + } + }, + "external_masses": { + "type": "array", + "minItems": 3, + "items": { + "$ref": "#/$defs/nonnegative_decimal" + } + }, + "external_helicity_domains": { + "type": "array", + "minItems": 3, + "items": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { + "type": "integer" + } + } + }, + "initial_state_count": { + "type": "integer", + "minimum": 1 + }, + "alias_of": { + "oneOf": [ + { + "$ref": "#/$defs/public_id" + }, + { + "type": "null" + } + ] + }, + "final_state_permutation": { + "oneOf": [ + { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { + "type": "integer", + "minimum": 0 + } + }, + { + "type": "null" + } + ] + } + }, + "allOf": [ + { + "if": { + "properties": { + "alias_of": { + "type": "null" + } + }, + "required": [ + "alias_of" + ] + }, + "then": { + "properties": { + "final_state_permutation": { + "type": "null" + } + } + }, + "else": { + "properties": { + "final_state_permutation": { + "type": "array", + "minItems": 1 + } + } + } + } + ] + }, + "point_algorithm": { + "type": "object", + "additionalProperties": false, + "required": [ + "name", + "version", + "rng", + "seed" + ], + "properties": { + "name": { + "type": "string", + "minLength": 1 + }, + "version": { + "type": "string", + "minLength": 1 + }, + "rng": { + "oneOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "null" + } + ] + }, + "seed": { + "oneOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ] + } + } + }, + "point": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "process_id", + "class", + "algorithm", + "sqrt_s", + "momenta", + "masses", + "arithmetic_precision_bits", + "round_trip_decimal_digits", + "certified_decimal_digits", + "stress_metric" + ], + "properties": { + "id": { + "$ref": "#/$defs/public_id" + }, + "process_id": { + "$ref": "#/$defs/public_id" + }, + "class": { + "enum": [ + "canonical", + "generic", + "stress" + ] + }, + "algorithm": { + "$ref": "#/$defs/point_algorithm" + }, + "sqrt_s": { + "$ref": "#/$defs/decimal" + }, + "momenta": { + "type": "array", + "minItems": 3, + "items": { + "type": "array", + "minItems": 4, + "maxItems": 4, + "items": { + "$ref": "#/$defs/decimal" + } + } + }, + "masses": { + "type": "array", + "minItems": 3, + "items": { + "$ref": "#/$defs/nonnegative_decimal" + } + }, + "arithmetic_precision_bits": { + "type": "integer", + "minimum": 2 + }, + "round_trip_decimal_digits": { + "type": "integer", + "minimum": 1 + }, + "certified_decimal_digits": { + "type": "integer", + "minimum": 1 + }, + "stress_metric": { + "oneOf": [ + { + "$ref": "#/$defs/stress_metric" + }, + { + "type": "null" + } + ] + } + }, + "allOf": [ + { + "if": { + "properties": { + "class": { + "const": "stress" + } + }, + "required": [ + "class" + ] + }, + "then": { + "properties": { + "stress_metric": { + "$ref": "#/$defs/stress_metric" + } + } + }, + "else": { + "properties": { + "stress_metric": { + "type": "null" + } + } + } + } + ] + }, + "stress_metric": { + "type": "object", + "additionalProperties": false, + "required": [ + "kind", + "value" + ], + "properties": { + "kind": { + "enum": [ + "minimum-final-energy-fraction", + "minimum-final-transverse-momentum-squared-fraction", + "relative-excess-energy" + ] + }, + "value": { + "$ref": "#/$defs/positive_decimal" + } + } + }, + "coverage": { + "type": "object", + "additionalProperties": false, + "required": [ + "helicities", + "color", + "color_kind", + "helicity_count", + "color_component_count", + "structural_zero_helicity_count" + ], + "properties": { + "helicities": { + "enum": [ + "complete", + "selected" + ] + }, + "color": { + "enum": [ + "complete", + "selected", + "contracted" + ] + }, + "color_kind": { + "enum": [ + "physical-lc-flows", + "contracted-color" + ] + }, + "helicity_count": { + "type": "integer", + "minimum": 1 + }, + "color_component_count": { + "type": "integer", + "minimum": 1 + }, + "structural_zero_helicity_count": { + "type": "integer", + "minimum": 0 + } + } + }, + "selectors": { + "type": "object", + "additionalProperties": false, + "required": [ + "helicity", + "color_flow", + "omitted_helicity", + "omitted_color" + ], + "properties": { + "helicity": { + "type": "boolean" + }, + "color_flow": { + "type": "boolean" + }, + "omitted_helicity": { + "const": "all-components" + }, + "omitted_color": { + "enum": [ + "all-components", + "contracted-component" + ] + } + } + }, + "normalization": { + "type": "object", + "additionalProperties": false, + "required": [ + "average_factor", + "color_factor", + "identical_factor", + "global_coupling_factor", + "quark_line_partner_factor", + "couplings_in_stage_evaluators" + ], + "properties": { + "average_factor": { + "$ref": "#/$defs/decimal" + }, + "color_factor": { + "$ref": "#/$defs/decimal" + }, + "identical_factor": { + "$ref": "#/$defs/decimal" + }, + "global_coupling_factor": { + "$ref": "#/$defs/decimal" + }, + "quark_line_partner_factor": { + "$ref": "#/$defs/decimal" + }, + "couplings_in_stage_evaluators": { + "type": "boolean" + } + } + }, + "topology": { + "type": "object", + "additionalProperties": false, + "required": [ + "currents", + "interactions", + "roots", + "reduction_groups" + ], + "properties": { + "currents": { + "type": "integer", + "minimum": 0 + }, + "interactions": { + "type": "integer", + "minimum": 0 + }, + "roots": { + "type": "integer", + "minimum": 1 + }, + "reduction_groups": { + "type": "integer", + "minimum": 1 + } + } + }, + "helicity": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "index", + "values", + "computed", + "structural_zero", + "representative_id", + "coefficient" + ], + "properties": { + "id": { + "$ref": "#/$defs/public_id" + }, + "index": { + "type": "integer", + "minimum": 0 + }, + "values": { + "type": "array", + "minItems": 3, + "items": { + "type": "integer" + } + }, + "computed": { + "type": "boolean" + }, + "structural_zero": { + "type": "boolean" + }, + "representative_id": { + "$ref": "#/$defs/public_id" + }, + "coefficient": { + "$ref": "#/$defs/decimal" + } + } + }, + "lc_color_flow": { + "type": "object", + "additionalProperties": false, + "required": [ + "kind", + "id", + "index", + "word", + "computed", + "representative_id", + "coefficient" + ], + "properties": { + "kind": { + "const": "lc-flow" + }, + "id": { + "$ref": "#/$defs/public_id" + }, + "index": { + "type": "integer", + "minimum": 0 + }, + "word": { + "type": "array", + "items": { + "type": "integer", + "minimum": 1 + } + }, + "computed": { + "type": "boolean" + }, + "representative_id": { + "$ref": "#/$defs/public_id" + }, + "coefficient": { + "$ref": "#/$defs/decimal" + } + } + }, + "contracted_color": { + "type": "object", + "additionalProperties": false, + "required": [ + "kind", + "id", + "index", + "description" + ], + "properties": { + "kind": { + "const": "contracted-color" + }, + "id": { + "$ref": "#/$defs/public_id" + }, + "index": { + "const": 0 + }, + "description": { + "type": "string", + "minLength": 1 + } + } + }, + "axes": { + "type": "object", + "additionalProperties": false, + "required": [ + "helicities", + "colors" + ], + "properties": { + "helicities": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/$defs/helicity" + } + }, + "colors": { + "type": "array", + "minItems": 1, + "items": { + "oneOf": [ + { + "$ref": "#/$defs/lc_color_flow" + }, + { + "$ref": "#/$defs/contracted_color" + } + ] + } + } + } + }, + "reduction_group": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "representative_helicity_id", + "representative_color_id", + "physical_helicity_ids", + "physical_color_ids" + ], + "properties": { + "id": { + "$ref": "#/$defs/public_id" + }, + "representative_helicity_id": { + "$ref": "#/$defs/public_id" + }, + "representative_color_id": { + "$ref": "#/$defs/public_id" + }, + "physical_helicity_ids": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { + "$ref": "#/$defs/public_id" + } + }, + "physical_color_ids": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { + "$ref": "#/$defs/public_id" + } + } + } + }, + "reduction": { + "type": "object", + "additionalProperties": false, + "required": [ + "kind", + "cell_semantics", + "groups", + "plan_sha256" + ], + "properties": { + "kind": { + "enum": [ + "lc-diagonal", + "contracted-color" + ] + }, + "cell_semantics": { + "enum": [ + "sum-all-contributing-groups", + "fully-contracted-color" + ] + }, + "groups": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/$defs/reduction_group" + } + }, + "plan_sha256": { + "$ref": "#/$defs/sha256" + } + } + }, + "observation": { + "type": "object", + "additionalProperties": false, + "required": [ + "point_id", + "arithmetic_precision_bits", + "round_trip_decimal_digits", + "certified_decimal_digits", + "values", + "total", + "evidence_refs" + ], + "properties": { + "point_id": { + "$ref": "#/$defs/public_id" + }, + "arithmetic_precision_bits": { + "type": "integer", + "minimum": 2 + }, + "round_trip_decimal_digits": { + "type": "integer", + "minimum": 1 + }, + "certified_decimal_digits": { + "type": "integer", + "minimum": 1 + }, + "values": { + "type": "array", + "minItems": 1, + "items": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/$defs/decimal" + } + } + }, + "total": { + "$ref": "#/$defs/decimal" + }, + "evidence_refs": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { + "$ref": "#/$defs/public_id" + } + } + } + }, + "case": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "case_kind", + "model_id", + "process_id", + "color_accuracy", + "point_policy", + "point_ids", + "coverage", + "selectors", + "normalization", + "topology", + "artifact_physics_sha256", + "artifact_execution_sha256", + "physics_case_sha256", + "axes", + "reduction", + "observations" + ], + "properties": { + "id": { + "$ref": "#/$defs/public_id" + }, + "case_kind": { + "enum": [ + "substantive", + "diagnostic" + ] + }, + "model_id": { + "$ref": "#/$defs/public_id" + }, + "process_id": { + "$ref": "#/$defs/public_id" + }, + "color_accuracy": { + "enum": [ + "lc", + "nlc", + "full" + ] + }, + "point_policy": { + "enum": [ + "standard", + "degenerate-2to1" + ] + }, + "point_ids": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { + "$ref": "#/$defs/public_id" + } + }, + "coverage": { + "$ref": "#/$defs/coverage" + }, + "selectors": { + "$ref": "#/$defs/selectors" + }, + "normalization": { + "$ref": "#/$defs/normalization" + }, + "topology": { + "$ref": "#/$defs/topology" + }, + "artifact_physics_sha256": { + "$ref": "#/$defs/sha256" + }, + "artifact_execution_sha256": { + "$ref": "#/$defs/sha256" + }, + "physics_case_sha256": { + "$ref": "#/$defs/sha256" + }, + "axes": { + "$ref": "#/$defs/axes" + }, + "reduction": { + "$ref": "#/$defs/reduction" + }, + "observations": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/$defs/observation" + } + } + }, + "allOf": [ + { + "if": { + "properties": { + "case_kind": { + "const": "substantive" + } + }, + "required": [ + "case_kind" + ] + }, + "then": { + "properties": { + "coverage": { + "properties": { + "helicities": { + "const": "complete" + }, + "color": { + "enum": [ + "complete", + "contracted" + ] + } + } + } + } + } + } + ] + } + } +} diff --git a/pyamplicol/schemas/runtime-physics-v1.schema.json b/pyamplicol/schemas/runtime-physics-v1.schema.json new file mode 100644 index 0000000000..b79912f1ff --- /dev/null +++ b/pyamplicol/schemas/runtime-physics-v1.schema.json @@ -0,0 +1,456 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://pyamplicol.org/schemas/runtime-physics-v1.schema.json", + "title": "pyAmpliCol runtime physics metadata v1", + "oneOf": [ + { + "$ref": "#/$defs/resolved_physics" + }, + { + "$ref": "#/$defs/on_the_fly_public_metadata" + } + ], + "$defs": { + "resolved_physics": { + "type": "object", + "additionalProperties": false, + "required": [ + "schema_version", + "kind", + "process_id", + "process", + "color_accuracy", + "coverage", + "external_particles", + "helicities", + "color_components", + "reduction", + "model_parameters", + "selectors" + ], + "properties": { + "schema_version": { + "const": 1 + }, + "kind": { + "const": "pyamplicol-resolved-physics" + }, + "process_id": { + "$ref": "#/$defs/public_id" + }, + "process": { + "type": "string", + "minLength": 1 + }, + "color_accuracy": { + "enum": [ + "lc", + "nlc", + "full" + ] + }, + "coverage": { + "$ref": "#/$defs/coverage" + }, + "external_particles": { + "type": "array", + "minItems": 3, + "items": { + "$ref": "#/$defs/external_particle" + } + }, + "helicities": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/$defs/helicity" + } + }, + "color_components": { + "type": "array", + "minItems": 1, + "items": { + "oneOf": [ + { + "$ref": "#/$defs/color_flow" + }, + { + "$ref": "#/$defs/contracted_color" + } + ] + } + }, + "reduction": { + "$ref": "#/$defs/reduction" + }, + "model_parameters": { + "type": "array", + "items": { + "$ref": "#/$defs/model_parameter" + } + }, + "selectors": { + "$ref": "#/$defs/selectors" + }, + "extensions": { + "type": "object", + "default": {} + } + } + }, + "on_the_fly_public_metadata": { + "type": "object", + "additionalProperties": false, + "required": [ + "schema_version", + "kind", + "process_id", + "process", + "color_accuracy", + "external_particles", + "model_parameters" + ], + "properties": { + "schema_version": { + "const": 1 + }, + "kind": { + "const": "pyamplicol-on-the-fly-public-metadata" + }, + "process_id": { + "$ref": "#/$defs/public_id" + }, + "process": { + "type": "string", + "minLength": 1 + }, + "color_accuracy": { + "const": "lc" + }, + "external_particles": { + "type": "array", + "minItems": 3, + "items": { + "$ref": "#/$defs/external_particle" + } + }, + "model_parameters": { + "type": "array", + "items": { + "$ref": "#/$defs/model_parameter" + } + } + } + }, + "public_id": { + "type": "string", + "minLength": 1, + "maxLength": 255, + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:+,~-]*$" + }, + "coverage": { + "type": "object", + "additionalProperties": false, + "required": [ + "helicities", + "color", + "color_kind", + "structural_zero_helicity_count" + ], + "properties": { + "helicities": { + "enum": [ + "complete", + "selected" + ] + }, + "color": { + "enum": [ + "complete", + "selected", + "contracted" + ] + }, + "color_kind": { + "enum": [ + "physical-lc-flows", + "contracted-color" + ] + }, + "structural_zero_helicity_count": { + "type": "integer", + "minimum": 0 + } + } + }, + "external_particle": { + "type": "object", + "additionalProperties": false, + "required": [ + "index", + "label", + "particle", + "pdg", + "role", + "momentum_slot", + "momentum_components" + ], + "properties": { + "index": { + "type": "integer", + "minimum": 0 + }, + "label": { + "type": "integer", + "minimum": 1 + }, + "particle": { + "type": "string", + "minLength": 1 + }, + "pdg": { + "type": "integer" + }, + "role": { + "enum": [ + "initial", + "final" + ] + }, + "momentum_slot": { + "type": "integer", + "minimum": 0 + }, + "momentum_components": { + "const": [ + "E", + "px", + "py", + "pz" + ] + } + } + }, + "helicity": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "index", + "values", + "computed", + "structural_zero", + "representative_id", + "coefficient" + ], + "properties": { + "id": { + "$ref": "#/$defs/public_id" + }, + "index": { + "type": "integer", + "minimum": 0 + }, + "values": { + "type": "array", + "minItems": 3, + "items": { + "type": "integer" + } + }, + "computed": { + "type": "boolean" + }, + "structural_zero": { + "type": "boolean" + }, + "representative_id": { + "$ref": "#/$defs/public_id" + }, + "coefficient": { + "type": "number" + } + } + }, + "color_flow": { + "type": "object", + "additionalProperties": false, + "required": [ + "kind", + "id", + "index", + "word", + "computed", + "representative_id", + "coefficient" + ], + "properties": { + "kind": { + "const": "lc-flow" + }, + "id": { + "$ref": "#/$defs/public_id" + }, + "index": { + "type": "integer", + "minimum": 0 + }, + "word": { + "type": "array", + "items": { + "type": "integer", + "minimum": 1 + } + }, + "computed": { + "type": "boolean" + }, + "representative_id": { + "$ref": "#/$defs/public_id" + }, + "coefficient": { + "type": "number" + } + } + }, + "contracted_color": { + "type": "object", + "additionalProperties": false, + "required": [ + "kind", + "id", + "index", + "description" + ], + "properties": { + "kind": { + "const": "contracted-color" + }, + "id": { + "$ref": "#/$defs/public_id" + }, + "index": { + "const": 0 + }, + "description": { + "type": "string", + "minLength": 1 + } + } + }, + "reduction": { + "type": "object", + "additionalProperties": false, + "required": [ + "kind", + "groups" + ], + "properties": { + "kind": { + "enum": [ + "lc-diagonal", + "contracted-color" + ] + }, + "groups": { + "type": "array", + "items": { + "$ref": "#/$defs/reduction_group" + } + } + } + }, + "reduction_group": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "representative_helicity_id", + "representative_color_id", + "physical_helicity_ids", + "physical_color_ids" + ], + "properties": { + "id": { + "$ref": "#/$defs/public_id" + }, + "representative_helicity_id": { + "$ref": "#/$defs/public_id" + }, + "representative_color_id": { + "$ref": "#/$defs/public_id" + }, + "physical_helicity_ids": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/$defs/public_id" + } + }, + "physical_color_ids": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/$defs/public_id" + } + } + } + }, + "model_parameter": { + "type": "object", + "additionalProperties": false, + "required": [ + "name", + "kind", + "default_real", + "default_imaginary", + "mutable" + ], + "properties": { + "name": { + "type": "string", + "minLength": 1 + }, + "kind": { + "enum": [ + "normalization", + "mass", + "width", + "coupling", + "external", + "derived" + ] + }, + "default_real": { + "type": "number" + }, + "default_imaginary": { + "type": "number" + }, + "mutable": { + "type": "boolean" + } + } + }, + "selectors": { + "type": "object", + "additionalProperties": false, + "required": [ + "helicity", + "color_flow", + "contracted_color" + ], + "properties": { + "helicity": { + "type": "boolean" + }, + "color_flow": { + "type": "boolean" + }, + "contracted_color": { + "const": false + } + } + } + } +} diff --git a/pyamplicol/source_hash.py b/pyamplicol/source_hash.py new file mode 100644 index 0000000000..9c43423d94 --- /dev/null +++ b/pyamplicol/source_hash.py @@ -0,0 +1,79 @@ +# SPDX-License-Identifier: 0BSD +"""Hash of the pyAmpliCol sources that end up in an installed package. + +The build stores it as ``pyamplicol._source_hash.SOURCE_HASH``, so MadGraph7 +can warn when the installed package no longer matches the source tree (as for +madspace). Generated files (native libraries, staged SDK, prepared-model packs +from the release archive, build provenance) are left out. +""" + +import hashlib +from pathlib import Path, PurePosixPath + +_ROOT = Path(__file__).resolve().parent + +_SOURCE_DIRS = ("src", "rust") +_SOURCE_FILES = ("Cargo.toml", "Cargo.lock", "pyproject.toml", "rust-toolchain.toml") + +_IGNORED_DIR_NAMES = {"__pycache__", "target"} +_IGNORED_FILE_NAMES = {".DS_Store", "_build_info.json", "_source_hash.py"} +_IGNORED_SUFFIXES = {".pyc", ".pyo", ".so", ".dylib", ".pyd", ".pyamplicol-model"} +_IGNORED_TREES = tuple( + PurePosixPath(path) + for path in ( + "src/pyamplicol/_sdk/lib", + "src/pyamplicol/_sdk/include", + "src/pyamplicol/_sdk/fortran", + "src/pyamplicol/_sdk/sboms", + "src/pyamplicol/_sdk/link.json", + "src/pyamplicol/_sdk/metadata.json", + "src/pyamplicol/_examples", + ) +) + + +def _ignored(relative: PurePosixPath) -> bool: + if _IGNORED_DIR_NAMES & set(relative.parts): + return True + if relative.name in _IGNORED_FILE_NAMES or relative.suffix in _IGNORED_SUFFIXES: + return True + if ( + relative.name.endswith(".metadata.json") + and relative.parent.name == "prepared_models" + ): + return True + # Self-test fixtures other than the tracked portable one are generated. + parts = relative.parts + selftest = parts[:4] == ("src", "pyamplicol", "assets", "selftest") + if selftest and len(parts) > 5 and parts[4] != "portable-64le": + return True + return any(relative == tree or tree in relative.parents for tree in _IGNORED_TREES) + + +def _iter_source_files(root: Path): + for dir_name in _SOURCE_DIRS: + for path in (root / dir_name).rglob("*"): + relative = PurePosixPath(path.relative_to(root).as_posix()) + if path.is_file() and not _ignored(relative): + yield path + for file_name in _SOURCE_FILES: + yield root / file_name + + +def compute_source_hash(root: Path = _ROOT) -> str: + root = Path(root).resolve() + paths = sorted( + _iter_source_files(root), key=lambda p: p.relative_to(root).as_posix() + ) + digest = hashlib.sha256() + for path in paths: + digest.update(path.relative_to(root).as_posix().encode("utf-8")) + digest.update(b"\0") + with open(path, "rb") as f: + digest.update(hashlib.file_digest(f, "sha256").digest()) + digest.update(b"\0") + return digest.hexdigest() + + +if __name__ == "__main__": + print(compute_source_hash()) diff --git a/pyamplicol/src/pyamplicol/__init__.py b/pyamplicol/src/pyamplicol/__init__.py new file mode 100644 index 0000000000..535575e367 --- /dev/null +++ b/pyamplicol/src/pyamplicol/__init__.py @@ -0,0 +1,275 @@ +# SPDX-License-Identifier: 0BSD +"""Public Python interface for pyAmpliCol. + +The package's Symbolica library key is tried before any symbolic work. +Personal licenses and restricted mode remain available if registration fails. +Model tooling is loaded only when generation or runtime services are first used. +""" + +from __future__ import annotations + +import os as _os +from contextlib import suppress as _suppress +from importlib import import_module +from typing import TYPE_CHECKING, Any + +from ._internal.versions import package_version + +if TYPE_CHECKING: + from types import ModuleType as _ModuleType + +# Registration precedes symbolic work, including in spawned Python workers. +# Keep startup quiet before CLI output/banner policy can be resolved, without +# changing the caller's banner or personal-license environment. +_previous_banner = _os.environ.get("SYMBOLICA_HIDE_BANNER") +_os.environ["SYMBOLICA_HIDE_BANNER"] = "1" +_symbolica: _ModuleType | None +try: + try: + import symbolica as _symbolica + except ModuleNotFoundError as _error: + if _error.name != "symbolica": + raise + # Keep configuration/source-only tooling usable without native deps. + _symbolica = None + + _set_library_key = getattr(_symbolica, "set_library_key", None) + if callable(_set_library_key): + # Symbolica's public is_licensed() remains the authority: a personal + # key can still license this process, otherwise use restricted mode. + with _suppress(ValueError, PermissionError): + _set_library_key( + "SU-419-pyamplicol-2028.01.01-" + "KJBO3RLZJ3H5FKU5CL756QLKZCURABBCKRGHZRK5OVIUQXZBCLJTOUQPHXYVTANIX" + "SCKI6YTZDWGH2NWHQIBCPDUO5GVW2STVMP3MCQ" + ) +finally: + if _previous_banner is None: + _os.environ.pop("SYMBOLICA_HIDE_BANNER", None) + else: + _os.environ["SYMBOLICA_HIDE_BANNER"] = _previous_banner +del _os, _suppress, _previous_banner, _symbolica, _set_library_key + +if TYPE_CHECKING: + from .api import ( + ArtifactError, + BenchmarkComponentTiming, + BenchmarkProfileCounters, + BenchmarkResult, + BenchmarkRunner, + BenchmarkStageTiming, + BenchmarkStatistics, + BenchmarkTimingBreakdown, + ColorComponent, + ColorFlow, + CompatibilityError, + CompiledModel, + CompiledModelCapabilities, + CompiledModelInfo, + CompiledModelSource, + ConfigurationError, + ContractedColorComponent, + DependencyError, + EvaluationError, + ExternalParticle, + GenerationError, + GenerationPlan, + GenerationResult, + Generator, + HelicityConfiguration, + ModelCompilationIssue, + ModelCompilationPhase, + ModelError, + ModelParameter, + ModelSource, + PhysicsReduction, + ProcessAlias, + ProcessPhysics, + ProcessRequest, + ProcessSet, + PyAmpliColError, + ReductionGroup, + ResolvedEvaluation, + Runtime, + WarmUpResult, + benchmark, + generate, + group_processes, + load, + ) + from .config import ( + BenchmarkConfig, + ColorConfig, + ColorFFTBasis, + EvaluationConfig, + GenerationConfig, + RunConfig, + ) + from .correlators import ( + ColorCorrelator, + CorrelatedRequest, + CorrelatedValue, + CorrelatorConfig, + EmitGluon, + SplitGluon, + ) + +__version__ = package_version() + +__all__ = [ + "ArtifactError", + "BenchmarkComponentTiming", + "BenchmarkConfig", + "BenchmarkProfileCounters", + "BenchmarkResult", + "BenchmarkRunner", + "BenchmarkStageTiming", + "BenchmarkStatistics", + "BenchmarkTimingBreakdown", + "ColorComponent", + "ColorConfig", + "ColorCorrelator", + "ColorFFTBasis", + "ColorFlow", + "CompatibilityError", + "CompiledModel", + "CompiledModelCapabilities", + "CompiledModelInfo", + "CompiledModelSource", + "ConfigurationError", + "ContractedColorComponent", + "CorrelatedRequest", + "CorrelatedValue", + "CorrelatorConfig", + "DependencyError", + "EmitGluon", + "EvaluationConfig", + "EvaluationError", + "ExternalParticle", + "GenerationConfig", + "GenerationError", + "GenerationPlan", + "GenerationResult", + "Generator", + "HelicityConfiguration", + "ModelCompilationIssue", + "ModelCompilationPhase", + "ModelError", + "ModelParameter", + "ModelSource", + "PhysicsReduction", + "ProcessAlias", + "ProcessPhysics", + "ProcessRequest", + "ProcessSet", + "PyAmpliColError", + "ReductionGroup", + "ResolvedEvaluation", + "RunConfig", + "Runtime", + "SplitGluon", + "WarmUpResult", + "__version__", + "benchmark", + "generate", + "group_processes", + "load", +] + +_ERROR_EXPORTS = ( + "ArtifactError", + "CompatibilityError", + "ConfigurationError", + "DependencyError", + "EvaluationError", + "GenerationError", + "ModelError", + "PyAmpliColError", +) +_REQUEST_EXPORTS = ( + "ModelSource", + "ProcessAlias", + "ProcessRequest", + "ProcessSet", +) +_MODEL_EXPORTS = ( + "CompiledModel", + "CompiledModelCapabilities", + "CompiledModelInfo", + "CompiledModelSource", + "ModelCompilationIssue", + "ModelCompilationPhase", +) +_RESULT_EXPORTS = ( + "BenchmarkComponentTiming", + "BenchmarkProfileCounters", + "BenchmarkResult", + "BenchmarkStageTiming", + "BenchmarkStatistics", + "BenchmarkTimingBreakdown", + "ColorComponent", + "ColorFlow", + "ContractedColorComponent", + "ExternalParticle", + "GenerationPlan", + "GenerationResult", + "HelicityConfiguration", + "ModelParameter", + "PhysicsReduction", + "ProcessPhysics", + "ReductionGroup", + "ResolvedEvaluation", + "WarmUpResult", +) +_SERVICE_EXPORTS = ( + "BenchmarkRunner", + "Generator", + "Runtime", + "benchmark", + "generate", + "load", +) +_CONFIG_EXPORTS = ( + "BenchmarkConfig", + "ColorConfig", + "ColorFFTBasis", + "EvaluationConfig", + "GenerationConfig", + "RunConfig", +) +_PUBLIC_EXPORTS = { + **{ + name: (".correlators", name) + for name in ( + "ColorCorrelator", + "CorrelatedRequest", + "CorrelatedValue", + "CorrelatorConfig", + "EmitGluon", + "SplitGluon", + ) + }, + **{name: (".api.errors", name) for name in _ERROR_EXPORTS}, + **{name: (".api.models", name) for name in _MODEL_EXPORTS}, + **{name: (".api.requests", name) for name in _REQUEST_EXPORTS}, + **{name: (".api.results", name) for name in _RESULT_EXPORTS}, + **{name: (".api.services", name) for name in _SERVICE_EXPORTS}, + **{name: (".config", name) for name in _CONFIG_EXPORTS}, + "group_processes": (".api.process_groups", "group_processes"), +} + + +def __getattr__(name: str) -> Any: + """Load a public object only when that object is requested.""" + + export = _PUBLIC_EXPORTS.get(name) + if export is None: + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + module_name, attribute = export + value = getattr(import_module(module_name, __name__), attribute) + globals()[name] = value + return value + + +def __dir__() -> list[str]: + return sorted(set(globals()).union(__all__)) diff --git a/pyamplicol/src/pyamplicol/__main__.py b/pyamplicol/src/pyamplicol/__main__.py new file mode 100644 index 0000000000..db791b7c48 --- /dev/null +++ b/pyamplicol/src/pyamplicol/__main__.py @@ -0,0 +1,6 @@ +# SPDX-License-Identifier: 0BSD +from __future__ import annotations + +from .cli import main + +raise SystemExit(main()) diff --git a/pyamplicol/src/pyamplicol/_internal/physics/__init__.py b/pyamplicol/src/pyamplicol/_internal/physics/__init__.py new file mode 100644 index 0000000000..c4c49a35c1 --- /dev/null +++ b/pyamplicol/src/pyamplicol/_internal/physics/__init__.py @@ -0,0 +1,18 @@ +# SPDX-License-Identifier: 0BSD +"""Neutral physics contracts shared across generation and evaluation.""" + +from .types import ( + ExternalMomentum, + FourMomentum, + HelicityContribution, + MatrixElementEvaluation, + NativeEvaluationError, +) + +__all__ = [ + "ExternalMomentum", + "FourMomentum", + "HelicityContribution", + "MatrixElementEvaluation", + "NativeEvaluationError", +] diff --git a/pyamplicol/src/pyamplicol/_internal/physics/parameters.py b/pyamplicol/src/pyamplicol/_internal/physics/parameters.py new file mode 100644 index 0000000000..c617d70fa1 --- /dev/null +++ b/pyamplicol/src/pyamplicol/_internal/physics/parameters.py @@ -0,0 +1,323 @@ +# SPDX-License-Identifier: 0BSD +from __future__ import annotations + +import base64 +from collections.abc import Sequence +from dataclasses import dataclass +from typing import Any + +from .symbols import symbols + +ParameterHead = tuple[str, ...] + + +@dataclass(frozen=True) +class ParameterRange: + head: ParameterHead + start: int + stop: int + role: str = "scalar" + tensor_name: str | None = None + + def to_json_dict(self) -> dict[str, object]: + return { + "head": list(self.head), + "range": [self.start, self.stop], + "role": self.role, + "tensor_name": self.tensor_name, + } + + @classmethod + def from_json_dict(cls, data: dict[str, object]) -> ParameterRange: + head = data.get("head") + span = data.get("range") + if not isinstance(head, list) or not all( + isinstance(item, str) for item in head + ): + raise ValueError("parameter range is missing a string head") + if not isinstance(span, list) or len(span) != 2: + raise ValueError("parameter range is missing a two-entry range") + role = data.get("role", "scalar") + tensor_name = data.get("tensor_name") + return cls( + head=tuple(head), + start=int(span[0]), + stop=int(span[1]), + role=role if isinstance(role, str) else "scalar", + tensor_name=tensor_name if isinstance(tensor_name, str) else None, + ) + + +class ParamBuilder: + """Ordered runtime input builder for pyamplicol Symbolica evaluators.""" + + def __init__(self) -> None: + self.positions: dict[ParameterHead, tuple[int, int]] = {} + self.order: list[ParameterHead] = [] + self.ranges: dict[ParameterHead, ParameterRange] = {} + self.values: list[complex] = [] + self._symbols: list[Any] = [] + self.real_valued_inputs: list[int] = [] + self.purely_imaginary_valued_inputs: list[int] = [] + self.forced_complex_valued_inputs: list[int] = [] + + def add_parameter_list( + self, + head: ParameterHead, + length: int, + *, + role: str = "scalar", + tensor_name: str | None = None, + real_valued: bool = False, + ) -> tuple[Any, ...]: + if head in self.positions: + raise ValueError(f"parameter head already registered: {head}") + if length <= 0: + raise ValueError("parameter length must be positive") + start = len(self.values) + stop = start + length + self.positions[head] = (start, stop) + self.order.append(head) + self.ranges[head] = ParameterRange( + head=head, + start=start, + stop=stop, + role=role, + tensor_name=tensor_name, + ) + self.values.extend(0j for _ in range(length)) + if real_valued: + self.real_valued_inputs.extend(range(start, stop)) + parameter_symbols = tuple( + symbols.parameter_component(head, index) for index in range(length) + ) + self._symbols.extend(parameter_symbols) + return parameter_symbols + + def register_rank1_tensor( + self, + library: Any, + *, + tensor_name: str, + representation: Any, + head: ParameterHead, + length: int, + role: str, + real_valued: bool = False, + ) -> tuple[Any, ...]: + from symbolica.community.tensor import Tensor, TensorName + + parameter_symbols = self.add_parameter_list( + head, + length, + role=role, + tensor_name=tensor_name, + real_valued=real_valued, + ) + library.register( + Tensor.dense( + TensorName(tensor_name)(representation), + parameter_symbols, + ) + ) + return parameter_symbols + + def set_parameter_values( + self, + head: ParameterHead, + values: Sequence[complex | float], + *, + check_phase_flags: bool = True, + ) -> None: + if head not in self.positions: + raise ValueError(f"unknown parameter head: {head}") + start, stop = self.positions[head] + if stop - start != len(values): + raise ValueError( + f"parameter {head} expects {stop - start} values, got {len(values)}" + ) + complex_values = [complex(value) for value in values] + if check_phase_flags: + self._check_values_against_phase_flags(start, complex_values) + self.values[start:stop] = complex_values + + def parameter_symbols(self) -> list[Any]: + return list(self._symbols) + + def get_values(self, *, complexified: bool = False) -> list[complex] | list[float]: + if not complexified: + return list(self.values) + flat: list[float] = [] + for value in self.values: + flat.extend([float(value.real), float(value.imag)]) + return flat + + def get_complex_values(self) -> list[complex]: + return list(self.values) + + def get_real_values(self) -> list[float]: + """Return evaluator inputs for Symbolica real-valued evaluators.""" + + real_values: list[float] = [] + for index, value in enumerate(self.values): + if value.imag != 0.0: + raise ValueError( + f"parameter index {index} has imaginary part {value.imag}; " + "complex inputs must be represented with an explicit real/imag " + "parameter packing" + ) + real_values.append(float(value.real)) + return real_values + + def to_json_dict(self) -> dict[str, object]: + return { + "parameters": [self.ranges[head].to_json_dict() for head in self.order], + "values": [[value.real, value.imag] for value in self.values], + "real_valued_inputs": list(self.real_valued_inputs), + "purely_imaginary_valued_inputs": list(self.purely_imaginary_valued_inputs), + "forced_complex_valued_inputs": list(self.forced_complex_valued_inputs), + } + + @classmethod + def from_json_dict(cls, data: dict[str, object]) -> ParamBuilder: + builder = cls() + parameters = data.get("parameters") + values = data.get("values") + if not isinstance(parameters, list): + raise ValueError("param builder payload is missing parameters") + if not isinstance(values, list): + raise ValueError("param builder payload is missing values") + for item in parameters: + if not isinstance(item, dict): + raise ValueError("malformed parameter entry") + parameter_range = ParameterRange.from_json_dict(item) + length = parameter_range.stop - parameter_range.start + symbols_for_range = builder.add_parameter_list( + parameter_range.head, + length, + role=parameter_range.role, + tensor_name=parameter_range.tensor_name, + ) + if len(symbols_for_range) != length: + raise ValueError("internal parameter reconstruction mismatch") + decoded_values = [] + for item in values: + if not isinstance(item, list) or len(item) != 2: + raise ValueError("malformed parameter value entry") + decoded_values.append(complex(float(item[0]), float(item[1]))) + if len(decoded_values) != len(builder.values): + raise ValueError("parameter value count does not match ranges") + builder.values = decoded_values + builder.real_valued_inputs = _int_list(data.get("real_valued_inputs", [])) + builder.purely_imaginary_valued_inputs = _int_list( + data.get("purely_imaginary_valued_inputs", []) + ) + builder.forced_complex_valued_inputs = _int_list( + data.get("forced_complex_valued_inputs", []) + ) + return builder + + def _check_values_against_phase_flags( + self, + start: int, + values: Sequence[complex], + ) -> None: + for offset, value in enumerate(values): + index = start + offset + if index in self.real_valued_inputs and value.imag != 0.0: + raise ValueError(f"parameter index {index} is marked real") + if index in self.purely_imaginary_valued_inputs and value.real != 0.0: + raise ValueError(f"parameter index {index} is marked imaginary") + + +@dataclass +class SymbolicaEvaluatorBundle: + name: str + evaluator: Any + param_builder: ParamBuilder + output_length: int = 1 + complex_inputs: bool = False + metadata: dict[str, object] | None = None + + def evaluate(self) -> tuple[complex, ...]: + if self.complex_inputs: + result = self.evaluator.evaluate_complex( + [self.param_builder.get_complex_values()] + ) + else: + result = self.evaluator.evaluate([self.param_builder.get_real_values()]) + row = result[0] + return tuple(complex(value) for value in row[: self.output_length]) + + def to_artifact_payload(self) -> dict[str, object]: + return { + "kind": "pyamplicol-symbolica-evaluator-bundle", + "name": self.name, + "output_length": self.output_length, + "complex_inputs": self.complex_inputs, + "param_builder": self.param_builder.to_json_dict(), + "evaluator_state_b64": base64.b64encode(self.evaluator.save()).decode( + "ascii" + ), + "metadata": self.metadata or {}, + } + + @classmethod + def from_artifact_payload( + cls, + payload: dict[str, object], + ) -> SymbolicaEvaluatorBundle: + from symbolica import Evaluator + + encoded = payload.get("evaluator_state_b64") + param_builder_payload = payload.get("param_builder") + name = payload.get("name") + output_length = payload.get("output_length", 1) + complex_inputs = payload.get("complex_inputs", False) + metadata = payload.get("metadata") + if not isinstance(encoded, str): + raise ValueError("bundle payload is missing evaluator_state_b64") + if not isinstance(param_builder_payload, dict): + raise ValueError("bundle payload is missing param_builder") + if not isinstance(name, str): + raise ValueError("bundle payload is missing name") + param_builder = ParamBuilder.from_json_dict(param_builder_payload) + evaluator = Evaluator.load(base64.b64decode(encoded)) + if param_builder.real_valued_inputs: + evaluator.set_real_params(param_builder.real_valued_inputs) + return cls( + name=name, + evaluator=evaluator, + param_builder=param_builder, + output_length=_int_value(output_length, "output_length"), + complex_inputs=_bool_value(complex_inputs, "complex_inputs"), + metadata=metadata if isinstance(metadata, dict) else {}, + ) + + +def _int_list(value: object) -> list[int]: + if not isinstance(value, list): + return [] + return [int(item) for item in value] + + +def _int_value(value: object, name: str) -> int: + if isinstance(value, bool): + raise ValueError(f"{name} must be an integer") + if isinstance(value, int | str | bytes | bytearray): + return int(value) + raise ValueError(f"{name} must be an integer") + + +def _bool_value(value: object, name: str) -> bool: + if isinstance(value, bool): + return value + raise ValueError(f"{name} must be a boolean") + + +__all__ = [ + "ParamBuilder", + "ParameterHead", + "ParameterRange", + "SymbolicaEvaluatorBundle", +] diff --git a/pyamplicol/src/pyamplicol/_internal/physics/symbols.py b/pyamplicol/src/pyamplicol/_internal/physics/symbols.py new file mode 100644 index 0000000000..80e4069f36 --- /dev/null +++ b/pyamplicol/src/pyamplicol/_internal/physics/symbols.py @@ -0,0 +1,261 @@ +# SPDX-License-Identifier: 0BSD +from __future__ import annotations + +import re +from dataclasses import dataclass +from functools import cached_property +from hashlib import sha256 +from threading import Lock +from typing import Any + +_UFO_CANONICAL_NAMESPACE = "UFO::{}::" +# Symbolica's process-wide symbol table cannot handle concurrent S(...) calls. +_SYMBOL_CONSTRUCTION_LOCK = Lock() +_SYMBOL_CACHE: dict[tuple[str, bool], Any] = {} + + +def _construct_symbol(qualified_name: str, *, is_real: bool) -> Any: + from symbolica import S + + return S(qualified_name, is_real=True) if is_real else S(qualified_name) + + +def _symbol(qualified_name: str, *, is_real: bool = False) -> Any: + with _SYMBOL_CONSTRUCTION_LOCK: + key = (qualified_name, is_real) + cached = _SYMBOL_CACHE.get(key) + if cached is not None: + return cached + value = _construct_symbol(qualified_name, is_real=is_real) + _SYMBOL_CACHE[key] = value + return value + + +def _safe_namespace_part(value: object) -> str: + text = str(value).strip() + if re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", text): + return text + readable = re.sub(r"[^A-Za-z0-9_]+", "_", text).strip("_") or "unnamed" + if readable[0].isdigit(): + readable = f"n_{readable}" + digest = sha256(text.encode("utf-8")).hexdigest()[:12] + return f"{readable}_{digest}" + + +def _safe_symbol_path(value: object) -> str: + return "::".join( + _safe_namespace_part(part) for part in str(value).split("::") if part + ) + + +@dataclass(frozen=True) +class ModelSymbolRegistry: + """Symbolica names owned by one externally supplied model.""" + + model_name: str + + @cached_property + def namespace(self) -> str: + return f"model_{_safe_namespace_part(self.model_name)}" + + def qualified_name(self, name: str) -> str: + return f"{self.namespace}::{_safe_symbol_path(name)}" + + def symbol(self, name: str) -> Any: + return _symbol(self.qualified_name(name)) + + def expression(self, source: str) -> Any: + """Parse a UFO expression after moving every UFO head into this model.""" + + from symbolica import E + + namespaced = str(source).replace( + _UFO_CANONICAL_NAMESPACE, + f"{self.namespace}::", + ) + namespaced = namespaced.replace("UFO::", f"{self.namespace}::") + return E(namespaced) + + def expression_string(self, source: str) -> str: + return self.expression(source).to_canonical_string() + + def kernel_tensor_name(self, kind: int, side: str) -> str: + return self.qualified_name(f"compiler::kernel_{int(kind)}_{side}") + + def kernel_component(self, kind: int, side: str, index: int) -> Any: + return self.symbol(f"compiler::kernel_{int(kind)}_{side}_{int(index)}") + + def kernel_momentum(self, kind: int, side: str, index: int) -> Any: + return self.symbol( + f"compiler::kernel_{int(kind)}_{side}_momentum_{int(index)}" + ) + + def contact_leg_tensor_name(self, kind: int, leg: int) -> str: + return self.qualified_name( + f"compiler::contact_{int(kind)}_leg_{int(leg)}" + ) + + def ufo_momentum_tensor_name(self, leg: int) -> str: + return self.qualified_name(f"compiler::ufo_momentum_{int(leg)}") + + +@dataclass(frozen=True) +class SymbolRegistry: + """Central registry for reusable Symbolica heads used by pyamplicol.""" + + namespace: str = "pyamplicol" + + def qualified_name(self, name: str) -> str: + return f"{self.namespace}::{name}" + + def display_name(self, name: str) -> str: + return self.qualified_name(name) + + def symbol(self, name: str) -> Any: + return _symbol(self.qualified_name(name)) + + def real_symbol(self, name: str) -> Any: + return _symbol(self.qualified_name(name), is_real=True) + + def parameter(self, name: str) -> Any: + return self.symbol(f"param::{name}") + + def parameter_component( + self, + head: tuple[str, ...], + index: int, + ) -> Any: + safe_head = "::".join(_safe_symbol_path(part) for part in head) + return self.parameter(f"{safe_head}::c{int(index)}") + + def model(self, model_name: str) -> ModelSymbolRegistry: + return ModelSymbolRegistry(str(model_name)) + + def model_owned_name(self, model_name: str, name: str) -> str: + model_tag = _safe_namespace_part(model_name) + return self.qualified_name(f"model::{model_tag}::{name}") + + def model_owned(self, model_name: str, name: str) -> Any: + return self.symbol(f"model::{_safe_namespace_part(model_name)}::{name}") + + def derived_coupling(self, model_name: str, term_id: int) -> Any: + return self.model_owned(model_name, f"derived_coupling_{int(term_id)}") + + def runtime_model_parameter(self, model_name: str, name: str) -> Any: + """Return the canonical head for one external-model runtime input.""" + + match = re.fullmatch(r"derived_coupling_([0-9]+)", str(name)) + if match is not None: + return self.derived_coupling(model_name, int(match.group(1))) + return self.model(model_name).symbol(str(name)) + + def kernel_component(self, kind: int, side: str, index: int) -> Any: + return self.symbol(f"kernel_{int(kind)}_{side}_{int(index)}") + + def kernel_momentum(self, kind: int, side: str, index: int) -> Any: + return self.symbol(f"kernel_{int(kind)}_{side}_momentum_{int(index)}") + + def kernel_tensor_name(self, kind: int, side: str) -> str: + return self.qualified_name(f"kernel_{int(kind)}_{side}") + + def kernel_function_argument( + self, + model_name: str, + key: str, + role: str, + index: int, + ) -> Any: + return self.model_owned( + model_name, + f"kernel_function::{key}::{role}::c{int(index)}", + ) + + def kernel_function_component( + self, + model_name: str, + key: str, + index: int, + ) -> Any: + return self.model_owned( + model_name, + f"kernel_function::{key}::component::c{int(index)}", + ) + + def weyl_probe( + self, + model_name: str, + kind: int, + side: str, + role: str, + index: int, + ) -> Any: + return self.model_owned( + model_name, + f"weyl_probe::kind_{int(kind)}::{side}::{role}::c{int(index)}", + ) + + def runtime_domain( + self, + model_name: str, + parameter_name: str, + domain: str, + ) -> Any: + name = f"runtime_domain::{domain}::{_safe_symbol_path(parameter_name)}" + if domain == "complex": + return self.model_owned(model_name, name) + real_symbol = self.real_symbol( + f"model::{_safe_namespace_part(model_name)}::{name}" + ) + return 1j * real_symbol if domain == "imaginary" else real_symbol + + def custom_propagator_component( + self, + model_name: str, + role: str, + index: int, + ) -> Any: + return self.model_owned( + model_name, + f"custom_propagator::{role}::c{int(index)}", + ) + + def custom_propagator_tensor_name(self, model_name: str) -> str: + return self.model_owned_name(model_name, "custom_propagator::input") + + def ufo_momentum_tensor_name(self, leg: int) -> str: + return self.qualified_name(f"ufo_momentum_{int(leg)}") + + def contact_leg_tensor_name(self, kind: int, leg: int) -> str: + return self.qualified_name(f"contact_{int(kind)}_leg_{int(leg)}") + + def canonical_tensor_index( + self, + family: str, + representation: str, + index: int, + ) -> Any: + """Return a reserved dummy-index candidate for tensor canonization.""" + + return self.symbol( + "canonical_tensor_index::" + f"{_safe_symbol_path(family)}::" + f"{_safe_symbol_path(representation)}::i{int(index)}" + ) + + @cached_property + def color_projection_probe_name(self) -> str: + return self.qualified_name("color_projection_probe") + + def inline_function_wildcard( + self, + definition_index: int, + argument_index: int, + ) -> Any: + return self.symbol( + f"inline_function_{int(definition_index)}_argument_{int(argument_index)}_" + ) + +symbols = SymbolRegistry() + + +__all__ = ["ModelSymbolRegistry", "SymbolRegistry", "symbols"] diff --git a/pyamplicol/src/pyamplicol/_internal/physics/types.py b/pyamplicol/src/pyamplicol/_internal/physics/types.py new file mode 100644 index 0000000000..43dc97d642 --- /dev/null +++ b/pyamplicol/src/pyamplicol/_internal/physics/types.py @@ -0,0 +1,51 @@ +# SPDX-License-Identifier: 0BSD +from __future__ import annotations + +from dataclasses import dataclass + +FourMomentum = tuple[float, float, float, float] +WaveFunction = tuple[complex, complex, complex, complex] +WeylWaveFunction = tuple[complex, complex] +TensorWaveFunction = tuple[complex, complex, complex, complex, complex, complex] + + +@dataclass(frozen=True) +class ExternalMomentum: + pdg: int + momentum: FourMomentum + + +@dataclass(frozen=True) +class HelicityContribution: + helicities: tuple[int, ...] + amplitude: complex + squared: float + + +@dataclass(frozen=True) +class MatrixElementEvaluation: + process: str + particles: tuple[ExternalMomentum, ...] + matrix_element: float + raw_helicity_sum: float + color_factor: int + average_factor: int + coupling_factor: float + helicity_contributions: tuple[HelicityContribution, ...] + identical_factor: int = 1 + + +class NativeEvaluationError(ValueError): + pass + + +__all__ = [ + "ExternalMomentum", + "FourMomentum", + "HelicityContribution", + "MatrixElementEvaluation", + "NativeEvaluationError", + "TensorWaveFunction", + "WaveFunction", + "WeylWaveFunction", +] diff --git a/pyamplicol/src/pyamplicol/_internal/sm_heft.py b/pyamplicol/src/pyamplicol/_internal/sm_heft.py new file mode 100644 index 0000000000..47fb203707 --- /dev/null +++ b/pyamplicol/src/pyamplicol/_internal/sm_heft.py @@ -0,0 +1,373 @@ +# SPDX-License-Identifier: 0BSD +"""Packaged scalar-HEFT extension of the Standard Model source. + +The extension is deliberately stored as a small set of canonical model records +on top of the already packaged SM JSON model. It adds the scalar Hgg, Hggg, +and Hgggg interactions from the FeynRules Higgs Effective Theory model without +duplicating the Standard Model payload or retaining a runtime dependency on an +external UFO checkout. +""" + +from __future__ import annotations + +import hashlib +import json +from collections.abc import Iterator, Mapping, Sequence +from contextlib import contextmanager +from copy import deepcopy +from dataclasses import replace +from pathlib import Path +from tempfile import TemporaryDirectory +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from pyamplicol.models.loading import CompiledModel + +CANONICAL_SM_HEFT_SOURCE = "built-in-sm-heft" +SM_HEFT_ALIASES = frozenset( + { + CANONICAL_SM_HEFT_SOURCE, + "builtin_sm_heft", + "sm-heft", + "sm_heft", + } +) + +_HEFT_ORDER = { + "name": "HIG", + "expansion_order": 1, + "hierarchy": 1, +} + +_HEFT_PARAMETER = { + "name": "GH", + "nature": "internal", + "parameter_type": "real", + "value": None, + "expression": ( + "(1+1/168*UFO::{}::MH^4*UFO::{}::MT^(-4)" + "+13/16800*UFO::{}::MH^6*UFO::{}::MT^(-6)" + "+7/120*UFO::{}::MH^2*UFO::{}::MT^(-2))" + "*-1/12*UFO::{}::G^2*UFO::{}::vev^(-1)*𝜋^(-2)" + ), + "lhablock": None, + "lhacode": None, +} + +_HEFT_COUPLINGS = ( + { + "name": "GC_HEFT_HGG", + "expression": "-1𝑖*UFO::{}::GH", # noqa: RUF001 + "orders": [["HIG", 1]], + "value": None, + }, + { + "name": "GC_HEFT_HGGG", + "expression": "-1*UFO::{}::G*UFO::{}::GH", + "orders": [["HIG", 1], ["QCD", 1]], + "value": None, + }, + { + "name": "GC_HEFT_HGGGG", + "expression": "1𝑖*UFO::{}::GH*UFO::{}::G^2", # noqa: RUF001 + "orders": [["HIG", 1], ["QCD", 2]], + "value": None, + }, +) + +_HEFT_LORENTZ_STRUCTURES = ( + { + "name": "HEFT_VVS", + "spins": [3, 3, 1], + "structure": ( + "-1*UFO::{}::Metric(UFO::{}::idx(1,1),UFO::{}::idx(1,2))" + "*UFO::{}::P(UFO::{}::dummy(1),UFO::{}::idx(1,1))" + "*UFO::{}::P(UFO::{}::dummy(1),UFO::{}::idx(1,2))" + "+UFO::{}::P(UFO::{}::idx(1,1),UFO::{}::idx(1,2))" + "*UFO::{}::P(UFO::{}::idx(1,2),UFO::{}::idx(1,1))" + ), + }, + { + "name": "HEFT_VVVS", + "spins": [3, 3, 3, 1], + "structure": ( + "-1*UFO::{}::Metric(UFO::{}::idx(1,1),UFO::{}::idx(1,2))" + "*UFO::{}::P(UFO::{}::idx(1,3),UFO::{}::idx(1,2))" + "+-1*UFO::{}::Metric(UFO::{}::idx(1,1),UFO::{}::idx(1,3))" + "*UFO::{}::P(UFO::{}::idx(1,2),UFO::{}::idx(1,1))" + "+-1*UFO::{}::Metric(UFO::{}::idx(1,2),UFO::{}::idx(1,3))" + "*UFO::{}::P(UFO::{}::idx(1,1),UFO::{}::idx(1,3))" + "+UFO::{}::Metric(UFO::{}::idx(1,1),UFO::{}::idx(1,2))" + "*UFO::{}::P(UFO::{}::idx(1,3),UFO::{}::idx(1,1))" + "+UFO::{}::Metric(UFO::{}::idx(1,1),UFO::{}::idx(1,3))" + "*UFO::{}::P(UFO::{}::idx(1,2),UFO::{}::idx(1,3))" + "+UFO::{}::Metric(UFO::{}::idx(1,2),UFO::{}::idx(1,3))" + "*UFO::{}::P(UFO::{}::idx(1,1),UFO::{}::idx(1,2))" + ), + }, + { + "name": "HEFT_VVVVS1", + "spins": [3, 3, 3, 3, 1], + "structure": ( + "-1*UFO::{}::Metric(UFO::{}::idx(1,1),UFO::{}::idx(1,3))" + "*UFO::{}::Metric(UFO::{}::idx(1,2),UFO::{}::idx(1,4))" + "+UFO::{}::Metric(UFO::{}::idx(1,1),UFO::{}::idx(1,4))" + "*UFO::{}::Metric(UFO::{}::idx(1,2),UFO::{}::idx(1,3))" + ), + }, + { + "name": "HEFT_VVVVS2", + "spins": [3, 3, 3, 3, 1], + "structure": ( + "-1*UFO::{}::Metric(UFO::{}::idx(1,1),UFO::{}::idx(1,2))" + "*UFO::{}::Metric(UFO::{}::idx(1,3),UFO::{}::idx(1,4))" + "+UFO::{}::Metric(UFO::{}::idx(1,1),UFO::{}::idx(1,4))" + "*UFO::{}::Metric(UFO::{}::idx(1,2),UFO::{}::idx(1,3))" + ), + }, + { + "name": "HEFT_VVVVS3", + "spins": [3, 3, 3, 3, 1], + "structure": ( + "-1*UFO::{}::Metric(UFO::{}::idx(1,1),UFO::{}::idx(1,2))" + "*UFO::{}::Metric(UFO::{}::idx(1,3),UFO::{}::idx(1,4))" + "+UFO::{}::Metric(UFO::{}::idx(1,1),UFO::{}::idx(1,3))" + "*UFO::{}::Metric(UFO::{}::idx(1,2),UFO::{}::idx(1,4))" + ), + }, +) + +_HEFT_VERTICES = ( + { + "name": "V_HEFT_HGG", + "particles": ["g", "g", "H"], + "color_structures": ["UFO::{}::Identity(1,2)"], + "lorentz_structures": ["HEFT_VVS"], + "couplings": [["GC_HEFT_HGG"]], + }, + { + "name": "V_HEFT_HGGG", + "particles": ["g", "g", "g", "H"], + "color_structures": ["UFO::{}::f(1,2,3)"], + "lorentz_structures": ["HEFT_VVVS"], + "couplings": [["GC_HEFT_HGGG"]], + }, + { + "name": "V_HEFT_HGGGG", + "particles": ["g", "g", "g", "g", "H"], + "color_structures": [ + "UFO::{}::f(-1,1,2)*UFO::{}::f(3,4,-1)", + "UFO::{}::f(-1,1,3)*UFO::{}::f(2,4,-1)", + "UFO::{}::f(-1,1,4)*UFO::{}::f(2,3,-1)", + ], + "lorentz_structures": [ + "HEFT_VVVVS1", + "HEFT_VVVVS2", + "HEFT_VVVVS3", + ], + "couplings": [ + ["GC_HEFT_HGGGG", None, None], + [None, "GC_HEFT_HGGGG", None], + [None, None, "GC_HEFT_HGGGG"], + ], + }, +) + + +def is_sm_heft_alias(value: object) -> bool: + return str(value).lower() in SM_HEFT_ALIASES + + +def packaged_sm_source_path() -> Path: + """Return the installed package's canonical SM JSON source.""" + + path = ( + Path(__file__).resolve().parents[1] + / "assets" + / "models" + / "json" + / "sm" + / "sm.json" + ) + if not path.is_file(): + raise RuntimeError(f"packaged Standard Model source is missing: {path}") + return path + + +def extend_sm_model_payload(model: Mapping[str, object]) -> dict[str, object]: + """Return an independent SM payload with the scalar HEFT records appended.""" + + payload = deepcopy(dict(model)) + _require_base_particles(payload) + additions: tuple[tuple[str, Sequence[Mapping[str, object]]], ...] = ( + ("orders", (_HEFT_ORDER,)), + ("parameters", (_HEFT_PARAMETER,)), + ("couplings", _HEFT_COUPLINGS), + ("lorentz_structures", _HEFT_LORENTZ_STRUCTURES), + ("vertex_rules", _HEFT_VERTICES), + ) + for field, records in additions: + raw_values = payload.get(field) + if not isinstance(raw_values, list) or not all( + isinstance(record, dict) for record in raw_values + ): + raise ValueError(f"packaged Standard Model {field} records are invalid") + existing_names = {str(record.get("name")) for record in raw_values} + added_names = {str(record["name"]) for record in records} + conflicts = sorted(existing_names.intersection(added_names)) + if conflicts: + raise ValueError( + f"packaged Standard Model already defines HEFT {field}: {conflicts!r}" + ) + raw_values.extend(deepcopy(list(records))) + payload["name"] = CANONICAL_SM_HEFT_SOURCE + return payload + + +def source_digest() -> str: + """Identify only the packaged SM inputs and canonical HEFT physics records.""" + + digest = hashlib.sha256() + base = packaged_sm_source_path() + restriction = base.with_name("restrict_default.json") + for path in (base, restriction): + if not path.is_file(): + raise RuntimeError(f"packaged Standard Model input is missing: {path}") + digest.update(path.name.encode("utf-8") + b"\0") + digest.update(path.read_bytes()) + digest.update(b"\0") + digest.update( + json.dumps( + { + "order": _HEFT_ORDER, + "parameter": _HEFT_PARAMETER, + "couplings": _HEFT_COUPLINGS, + "lorentz_structures": _HEFT_LORENTZ_STRUCTURES, + "vertices": _HEFT_VERTICES, + }, + ensure_ascii=True, + separators=(",", ":"), + sort_keys=True, + ).encode("ascii") + ) + return digest.hexdigest() + + +@contextmanager +def _materialized_source() -> Iterator[tuple[Path, Path]]: + """Materialize the canonical overlay for the existing JSON compiler.""" + + base = packaged_sm_source_path() + packaged_restriction = base.with_name("restrict_default.json") + with TemporaryDirectory(prefix="pyamplicol-sm-heft-") as temporary: + source = Path(temporary) / "sm-heft.json" + restriction = source.with_name("restrict_default.json") + source.write_text( + json.dumps( + extend_sm_model_payload(json.loads(base.read_text(encoding="utf-8"))), + ensure_ascii=False, + separators=(",", ":"), + sort_keys=True, + ) + + "\n", + encoding="utf-8", + ) + restriction.write_bytes(packaged_restriction.read_bytes()) + yield source, restriction + + +def _with_packaged_identity(compiled: CompiledModel) -> CompiledModel: + return replace( + compiled, + source={ + "kind": "built-in-sm-heft", + "source_name": None, + "digest": source_digest(), + "options": { + "restriction": {"kind": "name", "value": "default"}, + "simplify": True, + }, + }, + _serialized_path=None, + ) + + +def compile_sm_heft_source( + *, + cache_dir: Path | None = None, + use_cache: bool = True, + require_supported: bool = True, +) -> CompiledModel: + """Compile the packaged overlay through the canonical JSON model path.""" + + from pyamplicol.models.loading import compile_model_source + + with _materialized_source() as (source, restriction): + compiled = compile_model_source( + source, + restriction=str(restriction), + simplify=True, + cache_dir=cache_dir, + use_cache=use_cache, + require_supported=require_supported, + ) + return _with_packaged_identity(compiled) + + +def load_cached_sm_heft_source( + *, + cache_dir: Path | None = None, + require_supported: bool = True, +) -> CompiledModel | None: + """Load the packaged overlay only when its canonical cache already exists.""" + + from pyamplicol.models.loading import load_cached_model_source + + with _materialized_source() as (source, restriction): + compiled = load_cached_model_source( + source, + restriction=str(restriction), + simplify=True, + cache_dir=cache_dir, + require_supported=require_supported, + ) + return None if compiled is None else _with_packaged_identity(compiled) + + +def _require_base_particles(payload: Mapping[str, object]) -> None: + raw_particles = payload.get("particles") + if not isinstance(raw_particles, list): + raise ValueError("packaged Standard Model particle records are invalid") + particles = { + str(record.get("name")): record + for record in raw_particles + if isinstance(record, dict) + } + expected = {"g": (21, 3, 8), "H": (25, 1, 1)} + for name, identity in expected.items(): + record = particles.get(name) + observed = ( + None + if record is None + else ( + record.get("pdg_code"), + record.get("spin"), + record.get("color"), + ) + ) + if observed != identity: + raise ValueError( + f"packaged Standard Model particle {name!r} changed: {observed!r}" + ) + + +__all__ = [ + "CANONICAL_SM_HEFT_SOURCE", + "SM_HEFT_ALIASES", + "compile_sm_heft_source", + "extend_sm_model_payload", + "is_sm_heft_alias", + "load_cached_sm_heft_source", + "packaged_sm_source_path", + "source_digest", +] diff --git a/pyamplicol/src/pyamplicol/_internal/versions.py b/pyamplicol/src/pyamplicol/_internal/versions.py new file mode 100644 index 0000000000..78fa1ce649 --- /dev/null +++ b/pyamplicol/src/pyamplicol/_internal/versions.py @@ -0,0 +1,1088 @@ +# SPDX-License-Identifier: 0BSD +"""Independent public, serialization, and evaluator runtime contracts.""" + +from __future__ import annotations + +import hashlib +import importlib +import json +import re +import tomllib +from collections.abc import Mapping +from importlib import metadata +from pathlib import Path +from typing import Any + +PYTHON_API_VERSION = 1 +TOML_SCHEMA_VERSION = 1 +COMPILED_MODEL_SCHEMA_VERSION = 9 +PROCESS_ARTIFACT_SCHEMA_VERSION = 3 +RUNTIME_PHYSICS_SCHEMA_VERSION = 1 +C_ABI_VERSION = 1 + +# These are project-owned wire-format identifiers. Dependency source revisions +# live in the release and contributor locks, outside serialized artifacts. +SYMBOLICA_SERIALIZATION_ABI = "symbolica-bincode2-v1" +SYMJIT_APPLICATION_ABI = "symjit-application-storage-v3" +SYMJIT_PLANE_APPLICATION_ABI = "pyamplicol-symjit-plane-application-v2" +NATIVE_COMPILED_DIRECT_APPLICATION_ABI = ( + "pyamplicol-native-compiled-direct-application-v1" +) +NATIVE_EAGER_DIRECT_TABLE_APPLICATION_ABI = "pyamplicol-eager-native-direct-table-v1" +COMPILED_PLANE_DIRECT_APPLICATION_ABI = "pyamplicol-compiled-plane-kernel-v2" +EAGER_DIRECT_TABLE_DESCRIPTOR_ABI = "pyamplicol-eager-plane-table-descriptor-v1" +EAGER_DIRECT_TABLE_BINDING_ABI = "pyamplicol-eager-plane-table-binding-v2" +RECURRENCE_DIRECT_BINDING_ABI = "pyamplicol-recurrence-plane-binding-v2" + +SYMJIT_F64_RUNTIME_CAPABILITY = "symjit.application.complex-f64.v1" +SYMBOLICA_LEGACY_JIT_RUNTIME_CAPABILITY = ( + "symbolica.legacy-jit-container.complex-f64.v1" +) +SYMBOLICA_CPP_RUNTIME_CAPABILITY = "symbolica.compiled-cpp.complex-f64.v1" +SYMBOLICA_ASM_RUNTIME_CAPABILITY = "symbolica.compiled-asm.complex-f64.v1" +EAGER_DAG_F64_RUNTIME_CAPABILITY = "rusticol.eager-dag.complex-f64.v1" +EAGER_RUNTIME_LAYOUT_F64_CAPABILITY = "rusticol.eager-runtime-layout.complex-f64.v1" +EAGER_DIRECT_ARENA_RUNTIME_CAPABILITY = "eager-direct-arena-v1" +EAGER_LC_TOPOLOGY_REPLAY_RUNTIME_CAPABILITY = "rusticol.eager-dag.lc-topology-replay.v1" +RECURRENCE_BUILDER_INPUT_ABI = "pyamplicol-recurrence-builder-input-v2" +RECURRENCE_PLAN_ABI = "pyamplicol-recurrence-plan-v2" +RECURRENCE_RUNTIME_LAYOUT_ABI = "pyamplicol-recurrence-runtime-layout-v2" +RECURRENCE_DIRECT_TEMPLATE_ABI = "pyamplicol-recurrence-direct-template-v1" +RECURRENCE_DIRECT_BACKEND_ABI = "rusticol.recurrence-direct-backend.v1" +RECURRENCE_DIRECT_ARENA_RUNTIME_CAPABILITY = ( + "rusticol.recurrence-direct-arena.complex-f64.v1" +) +RECURRENCE_COLOR_RUNTIME_CAPABILITY = "rusticol.recurrence-color.lc.v1" +RECURRENCE_CONTRACTED_COLOR_RUNTIME_CAPABILITY = ( + "rusticol.recurrence-color.contracted.v1" +) +RECURRENCE_HELICITY_SELECTOR_COMPANION_RUNTIME_CAPABILITY = ( + "rusticol.recurrence-helicity-selector-companion.v2" +) +ON_THE_FLY_RUNTIME_CAPABILITY = "rusticol.on-the-fly.complex-f64.v1" +ON_THE_FLY_LC_COLOR_RUNTIME_CAPABILITY = "rusticol.on-the-fly.lc-color.v1" +ON_THE_FLY_CONTRACTED_COLOR_RUNTIME_CAPABILITY = ( + "rusticol.on-the-fly.contracted-color.v1" +) +SYMMETRIC_GROUP_FFT_COLOR_RUNTIME_CAPABILITY = ( + "rusticol.color-contraction.symmetric-group-fft.v1" +) +COMPILED_RUNTIME_SELECTORS_CAPABILITY = "rusticol.compiled.runtime-selectors.v1" +COMPILED_PLANE_ARENA_RUNTIME_CAPABILITY = "compiled-plane-arena-v1" +COMPILED_HELICITY_DUAL_LANE_CAPABILITY = "rusticol.compiled.helicity-dual-lane.v1" +COMPILED_HELICITY_SELECTOR_UNION_CAPABILITY = ( + "rusticol.compiled.helicity-selector-union.v1" +) +COMPILED_HELICITY_PRIMARY_RECURRENCE_CAPABILITY = ( + "rusticol.compiled.helicity-primary-recurrence.v1" +) +COMPILED_COLOR_TOPOLOGY_LANES_CAPABILITY = "rusticol.compiled.color-topology-lanes.v1" +COMPILED_COLOR_CONTRACTION_WALSH_CAPABILITY = ( + "rusticol.compiled.color-contraction-walsh.v1" +) +COMPILED_COLOR_CONTRACTION_WALSH_C2K_CAPABILITY = ( + "rusticol.compiled.color-contraction-walsh-c2k.v1" +) +EVALUATOR_RUNTIME_CAPABILITIES = frozenset( + { + COMPILED_COLOR_CONTRACTION_WALSH_C2K_CAPABILITY, + COMPILED_COLOR_CONTRACTION_WALSH_CAPABILITY, + COMPILED_COLOR_TOPOLOGY_LANES_CAPABILITY, + COMPILED_HELICITY_DUAL_LANE_CAPABILITY, + COMPILED_HELICITY_PRIMARY_RECURRENCE_CAPABILITY, + COMPILED_HELICITY_SELECTOR_UNION_CAPABILITY, + COMPILED_PLANE_ARENA_RUNTIME_CAPABILITY, + COMPILED_RUNTIME_SELECTORS_CAPABILITY, + EAGER_DIRECT_ARENA_RUNTIME_CAPABILITY, + EAGER_RUNTIME_LAYOUT_F64_CAPABILITY, + RECURRENCE_COLOR_RUNTIME_CAPABILITY, + RECURRENCE_CONTRACTED_COLOR_RUNTIME_CAPABILITY, + RECURRENCE_HELICITY_SELECTOR_COMPANION_RUNTIME_CAPABILITY, + RECURRENCE_DIRECT_ARENA_RUNTIME_CAPABILITY, + ON_THE_FLY_LC_COLOR_RUNTIME_CAPABILITY, + ON_THE_FLY_CONTRACTED_COLOR_RUNTIME_CAPABILITY, + SYMMETRIC_GROUP_FFT_COLOR_RUNTIME_CAPABILITY, + ON_THE_FLY_RUNTIME_CAPABILITY, + SYMJIT_F64_RUNTIME_CAPABILITY, + SYMBOLICA_LEGACY_JIT_RUNTIME_CAPABILITY, + SYMBOLICA_CPP_RUNTIME_CAPABILITY, + SYMBOLICA_ASM_RUNTIME_CAPABILITY, + } +) +KNOWN_EVALUATOR_RUNTIME_CAPABILITIES = EVALUATOR_RUNTIME_CAPABILITIES | frozenset( + { + # Historical eager plan-v2 artifacts remain parseable for inspection + # and actionable migration errors, but are not executable capabilities. + EAGER_DAG_F64_RUNTIME_CAPABILITY, + EAGER_LC_TOPOLOGY_REPLAY_RUNTIME_CAPABILITY, + } +) + +_SOURCE_PACKAGE_ROOT = Path(__file__).resolve().parents[1] +_SOURCE_ROOT = Path(__file__).resolve().parents[3] +_PACKAGE_BUILD_INFO_PATH = _SOURCE_PACKAGE_ROOT / "_build_info.json" +_SOURCE_RUNTIME_ROOT = _SOURCE_ROOT / ".artifacts" / "source-runtime" +_SOURCE_BUILD_INFO_PATH = _SOURCE_RUNTIME_ROOT / "_build_info.json" +_SOURCE_RUNTIME_STAGING_PATH = _SOURCE_RUNTIME_ROOT / ".staging" + +_NATIVE_BUILD_IDENTITY_DOMAIN = b"pyamplicol-native-build-inputs-v2\0" +_NATIVE_BUILD_INPUT_FILES = ( + Path("Cargo.lock"), + Path("Cargo.toml"), + Path("pyproject.toml"), + Path("rust-toolchain.toml"), + Path("dependencies/candidate-Cargo.lock"), + Path("dependencies/candidate-cargo-config.toml"), + Path("dependencies/install-state.json"), +) +_NATIVE_BUILD_INPUT_TREES = (Path("rust"),) +# Keep this projection identical to build_backend/native_build_identity.py +# without importing build-only modules. Release sdists omit candidate inputs. +_RELEASE_OMITTED_NATIVE_BUILD_INPUTS = frozenset( + { + Path("dependencies/candidate-Cargo.lock"), + Path("dependencies/candidate-cargo-config.toml"), + Path("dependencies/install-state.json"), + } +) +_NATIVE_MATURIN_CONFIG_KEYS = frozenset( + { + "all-features", + "bindings", + "config", + "features", + "include-debuginfo", + "manifest-path", + "module-name", + "no-default-features", + "profile", + "rustc-args", + "strip", + "target", + "targets", + "unstable-flags", + "zig", + } +) +_NON_OUTPUT_MATURIN_CONFIG_KEYS = frozenset( + { + "editable-profile", + "frozen", + "locked", + "pgo-command", + "target-dir", + "use-base-python", + } +) +_PACKAGING_MATURIN_CONFIG_KEYS = frozenset( + { + "auditwheel", + "compatibility", + "data", + "exclude", + "generate-ci", + "generate-stubs", + "include", + "include-import-lib", + "manylinux", + "python-packages", + "python-source", + "sbom", + "sdist-generator", + "skip-auditwheel", + "compression-enable-large-file-support", + "compression-level", + "compression-method", + "sbom-include", + } +) +_NATIVE_BUILD_IGNORED_TREE_PARTS = frozenset({".artifacts", "__pycache__", "target"}) +_NON_NATIVE_RUST_PATHS = frozenset( + { + Path("rust/crates/rusticol-capi/tests/eager_artifact.rs"), + Path("rust/crates/rusticol-capi/tests/runtime_selectors.rs"), + Path("rust/crates/rusticol-core/src/artifact_tests.rs"), + Path("rust/crates/rusticol-core/src/eager_layout_tests.rs"), + Path("rust/crates/rusticol-core/src/eager_lowering_v3_tests.rs"), + Path("rust/crates/rusticol-core/src/eager_plan_v3_pacbin_tests.rs"), + Path("rust/crates/rusticol-core/src/eager_runtime/plan_v3_tests.rs"), + Path("rust/crates/rusticol-core/src/engine/contraction_metadata_tests.rs"), + Path("rust/crates/rusticol-core/src/engine/eager_integration_tests.rs"), + Path("rust/crates/rusticol-core/src/engine/eager_v3_manifest_tests.rs"), + Path("rust/crates/rusticol-core/src/engine/quantum_number_flow_tests.rs"), + Path("rust/crates/rusticol-core/src/engine/recurrence_integration_tests.rs"), + Path("rust/crates/rusticol-core/src/engine/source_metadata_tests.rs"), + Path("rust/crates/rusticol-core/src/engine_tests.rs"), + Path("rust/crates/rusticol-core/src/metadata_tests.rs"), + Path("rust/crates/rusticol-core/src/pacbin_tests.rs"), + Path("rust/crates/rusticol-core/src/recurrence/direct_backend_tests.rs"), + Path("rust/crates/rusticol-core/src/recurrence/direct_codec_tests.rs"), + Path("rust/crates/rusticol-core/src/recurrence/direct_lowering_tests.rs"), + Path("rust/crates/rusticol-core/src/recurrence/direct_pacbin_tests.rs"), + Path("rust/crates/rusticol-core/src/recurrence/direct_plan_tests.rs"), + Path("rust/crates/rusticol-core/src/recurrence/direct_runtime_tests.rs"), + Path("rust/crates/rusticol-core/src/recurrence/tests.rs"), + Path("rust/crates/rusticol-core/tests/direct_arena_workspace_allocations.rs"), + Path("rust/crates/rusticol-core/tests/eager_runtime.rs"), + Path( + "rust/crates/rusticol-core/tests/fixtures/on_the_fly_query_parity_v1.json" + ), + Path( + "rust/crates/rusticol-core/tests/fixtures/recurrence_execution_hzz_full_v2.json" + ), + Path( + "rust/crates/rusticol-core/tests/fixtures/recurrence_execution_hzz_lc.json" + ), + Path( + "rust/crates/rusticol-core/tests/fixtures/recurrence_execution_hzz_nlc.json" + ), + Path("rust/crates/rusticol-core/tests/generated_artifact_odd_tails.rs"), + Path("rust/crates/rusticol-core/tests/recurrence_direct_arena_allocations.rs"), + Path("rust/crates/rusticol-python/stubs/pyamplicol/__init__.pyi"), + Path("rust/crates/rusticol-python/stubs/pyamplicol/_rusticol.pyi"), + Path("rust/crates/rusticol-python/tests/pyrightconfig.json"), + Path("rust/crates/rusticol-python/tests/stub_contract.rs"), + Path("rust/crates/rusticol-python/tests/typing_consumer.py"), + } +) +_NATIVE_BUILD_CONTRACT_KEYS = frozenset( + {"macos", "rust-path-remapping", "schema-version", "sdk"} +) +_RUST_PATH_REMAP_KEYS = frozenset( + {"build", "candidate-checkouts", "checkout", "source", "sysroot"} +) +_MACOS_NATIVE_BUILD_KEYS = frozenset({"cc", "cxx", "deployment-target"}) +_SDK_NATIVE_BUILD_KEYS = frozenset({"package", "profile", "rustc-codegen-arguments"}) +_CANDIDATE_CARGO_CONFIG = Path("dependencies/candidate-cargo-config.toml") +_CANDIDATE_INSTALL_STATE = Path("dependencies/install-state.json") +_CANDIDATE_PATCH_TARGETS = { + "graphica": "dependencies/checkouts/symbolica/lib/graphica", + "numerica": "dependencies/checkouts/symbolica/lib/numerica", + "symbolica": "dependencies/checkouts/symbolica", + "symjit": "dependencies/checkouts/symjit", +} +_NATIVE_CANDIDATE_SOURCE_NAMES = frozenset({"symbolica", "symjit"}) +_NATIVE_EXTENSION_SUFFIXES = (".dylib", ".pyd", ".so") + + +def _canonical_json_bytes(payload: Any) -> bytes: + try: + return ( + json.dumps( + payload, + sort_keys=True, + separators=(",", ":"), + allow_nan=False, + ) + + "\n" + ).encode("utf-8") + except (TypeError, ValueError) as error: + raise RuntimeError( + f"candidate native build identity is not canonical JSON: {error}" + ) from error + + +def _native_build_contract(payload: Mapping[str, Any]) -> dict[str, Any]: + tool = payload.get("tool") + pyamplicol = tool.get("pyamplicol") if isinstance(tool, Mapping) else None + contract = ( + pyamplicol.get("native-build") if isinstance(pyamplicol, Mapping) else None + ) + if not isinstance(contract, Mapping) or set(contract) != set( + _NATIVE_BUILD_CONTRACT_KEYS + ): + raise RuntimeError( + "native pyproject contract must contain exactly schema-version, " + "rust-path-remapping, macos, and sdk" + ) + schema_version = contract.get("schema-version") + if ( + not isinstance(schema_version, int) + or isinstance(schema_version, bool) + or schema_version != 1 + ): + raise RuntimeError("native pyproject contract must use schema-version 1") + remapping = contract.get("rust-path-remapping") + if not isinstance(remapping, Mapping) or set(remapping) != set( + _RUST_PATH_REMAP_KEYS + ): + raise RuntimeError("native Rust path-remapping contract is incomplete") + canonical_remapping: dict[str, str] = {} + for key in sorted(_RUST_PATH_REMAP_KEYS): + value = remapping[key] + if not isinstance(value, str) or not value.startswith("/"): + raise RuntimeError( + f"native Rust path-remapping destination {key} must be absolute" + ) + canonical_remapping[key] = value + if len(set(canonical_remapping.values())) != len(canonical_remapping): + raise RuntimeError("native Rust path-remapping destinations must be unique") + macos = contract.get("macos") + if not isinstance(macos, Mapping) or set(macos) != set(_MACOS_NATIVE_BUILD_KEYS): + raise RuntimeError("native macOS build contract is incomplete") + canonical_macos: dict[str, str] = {} + for key in sorted(_MACOS_NATIVE_BUILD_KEYS): + value = macos[key] + if not isinstance(value, str) or not value: + raise RuntimeError(f"native macOS build setting {key} is invalid") + canonical_macos[key] = value + if not canonical_macos["cc"].startswith("/") or not canonical_macos[ + "cxx" + ].startswith("/"): + raise RuntimeError("native macOS compilers must be absolute paths") + sdk = contract.get("sdk") + if not isinstance(sdk, Mapping) or set(sdk) != set(_SDK_NATIVE_BUILD_KEYS): + raise RuntimeError("native SDK build contract is incomplete") + package = sdk.get("package") + profile = sdk.get("profile") + codegen = sdk.get("rustc-codegen-arguments") + if not isinstance(package, str) or not package: + raise RuntimeError("native SDK package must be a non-empty string") + if not isinstance(profile, str) or not profile: + raise RuntimeError("native SDK profile must be a non-empty string") + if ( + not isinstance(codegen, list) + or not codegen + or any(not isinstance(value, str) or not value for value in codegen) + ): + raise RuntimeError("native SDK rustc codegen arguments must be strings") + return { + "macos": canonical_macos, + "rust-path-remapping": canonical_remapping, + "schema-version": 1, + "sdk": { + "package": package, + "profile": profile, + "rustc-codegen-arguments": list(codegen), + }, + } + + +def _native_pyproject_bytes(data: bytes) -> bytes: + try: + payload = tomllib.loads(data.decode("utf-8")) + except (UnicodeDecodeError, tomllib.TOMLDecodeError) as error: + raise RuntimeError( + f"invalid native pyproject configuration: {error}" + ) from error + build_system = payload.get("build-system") + requirements = ( + build_system.get("requires") if isinstance(build_system, Mapping) else None + ) + if not isinstance(requirements, list) or any( + not isinstance(requirement, str) for requirement in requirements + ): + raise RuntimeError("native pyproject build-system requires must be a list") + maturin_pins: list[str] = [] + for requirement in requirements: + match = re.fullmatch( + r"\s*maturin\s*==\s*([0-9][A-Za-z0-9._+-]*)\s*", + requirement, + flags=re.IGNORECASE, + ) + if match is not None: + maturin_pins.append(f"maturin=={match.group(1)}") + elif re.match(r"\s*maturin(?:\W|$)", requirement, flags=re.IGNORECASE): + raise RuntimeError( + "native Maturin build requirement must be exactly pinned" + ) + if len(maturin_pins) != 1: + raise RuntimeError( + "native pyproject must contain exactly one pinned Maturin requirement" + ) + tool = payload.get("tool") + maturin = tool.get("maturin") if isinstance(tool, Mapping) else None + if not isinstance(maturin, Mapping): + raise RuntimeError("native pyproject Maturin configuration must be a table") + classified = ( + _NATIVE_MATURIN_CONFIG_KEYS + | _NON_OUTPUT_MATURIN_CONFIG_KEYS + | _PACKAGING_MATURIN_CONFIG_KEYS + ) + unknown = set(maturin) - classified + if unknown: + rendered = ", ".join(sorted(str(key) for key in unknown)) + raise RuntimeError(f"unclassified Maturin configuration: {rendered}") + return _canonical_json_bytes( + { + "build-system": {"requires": maturin_pins}, + "tool": { + "maturin": { + key: maturin[key] + for key in sorted(_NATIVE_MATURIN_CONFIG_KEYS & set(maturin)) + }, + "pyamplicol": {"native-build": _native_build_contract(payload)}, + }, + } + ) + + +def _canonical_checkout_path(raw: str) -> str: + normalized = raw.replace("\\", "/").rstrip("/") + if normalized in {"TMP_FIXED_SYMJIT", "./TMP_FIXED_SYMJIT"} or normalized.endswith( + "/TMP_FIXED_SYMJIT" + ): + return "TMP_FIXED_SYMJIT" + marker = "/dependencies/checkouts/" + if marker in normalized: + suffix = normalized.rsplit(marker, maxsplit=1)[1] + if suffix: + return f"dependencies/checkouts/{suffix}" + prefix = "dependencies/checkouts/" + if normalized.startswith(prefix) and len(normalized) > len(prefix): + return normalized + raise RuntimeError( + "candidate Cargo patch paths must resolve below dependencies/checkouts" + ) + + +def _candidate_cargo_config_bytes(data: bytes) -> bytes: + """Return the exact path-independent candidate Cargo patch contract.""" + + try: + payload = tomllib.loads(data.decode("utf-8")) + patch = payload["patch"] + crates_io = patch["crates-io"] + except ( + KeyError, + TypeError, + UnicodeDecodeError, + tomllib.TOMLDecodeError, + ) as error: + raise RuntimeError(f"invalid candidate Cargo config: {error}") from error + if ( + not isinstance(payload, dict) + or set(payload) != {"patch"} + or not isinstance(patch, dict) + or set(patch) != {"crates-io"} + or not isinstance(crates_io, dict) + or not set(_CANDIDATE_PATCH_TARGETS).issubset(crates_io) + ): + raise RuntimeError( + "candidate Cargo config must contain the locked native crates.io " + "patch table" + ) + canonical: dict[str, Any] = {"patch": {"crates-io": {}}} + canonical_patches = canonical["patch"]["crates-io"] + for name, expected in _CANDIDATE_PATCH_TARGETS.items(): + entry = crates_io[name] + if ( + not isinstance(entry, Mapping) + or set(entry) != {"path"} + or not isinstance(entry["path"], str) + ): + raise RuntimeError( + f"candidate Cargo patch {name} must contain exactly one path" + ) + observed = _canonical_checkout_path(entry["path"]) + if observed != expected and not ( + name == "symjit" and observed == "TMP_FIXED_SYMJIT" + ): + raise RuntimeError( + f"candidate Cargo patch {name} must resolve to {expected}" + ) + canonical_patches[name] = {"path": observed} + return _canonical_json_bytes(canonical) + + +def _required_string( + payload: Mapping[str, Any], + key: str, + *, + description: str, +) -> str: + value = payload.get(key) + if not isinstance(value, str) or not value: + raise RuntimeError(f"{description} {key} must be a non-empty string") + return value + + +def _required_hex( + payload: Mapping[str, Any], + key: str, + *, + length: int, + description: str, +) -> str: + value = _required_string(payload, key, description=description) + if len(value) != length or any( + character not in "0123456789abcdef" for character in value + ): + raise RuntimeError( + f"{description} {key} must be a lowercase {length}-digit hex value" + ) + return value + + +def _canonical_candidate_sources(raw_sources: Any) -> dict[str, dict[str, str]]: + if not isinstance(raw_sources, Mapping): + raise RuntimeError("candidate installer state sources must be an object") + source_names = set(raw_sources) + if any(not isinstance(name, str) or not name for name in source_names): + raise RuntimeError("candidate installer source names must be non-empty strings") + if not _NATIVE_CANDIDATE_SOURCE_NAMES.issubset(source_names): + raise RuntimeError( + "candidate installer state has an incomplete native source map" + ) + sources: dict[str, dict[str, str]] = {} + for name in sorted(_NATIVE_CANDIDATE_SOURCE_NAMES): + raw_source = raw_sources[name] + description = f"candidate installer source {name}" + if not isinstance(raw_source, Mapping): + raise RuntimeError(f"{description} must be an object") + allowed_fields = {"url", "revision", "branch"} + required_fields = {"url", "revision"} + if not required_fields.issubset(raw_source) or not set(raw_source).issubset( + allowed_fields + ): + raise RuntimeError( + f"{description} must contain url/revision and optional branch only" + ) + source = { + "url": _required_string(raw_source, "url", description=description), + "revision": _required_hex( + raw_source, + "revision", + length=40, + description=description, + ), + } + if "branch" in raw_source: + source["branch"] = _required_string( + raw_source, + "branch", + description=description, + ) + sources[name] = source + return sources + + +def _candidate_install_state_bytes( + root: Path, + data: bytes, + *, + cargo_config_data: bytes, +) -> bytes: + """Validate and canonicalize the minimal immutable-source install state.""" + + del root, cargo_config_data + + try: + payload = json.loads(data) + except (UnicodeDecodeError, json.JSONDecodeError) as error: + raise RuntimeError(f"invalid candidate installer state: {error}") from error + schema_version = ( + payload.get("schema_version") if isinstance(payload, dict) else None + ) + if not isinstance(payload, dict) or set(payload) != { + "publishable", + "schema_version", + "sources", + }: + raise RuntimeError( + "candidate installer state must contain only schema_version, " + "publishable, and sources" + ) + if ( + not isinstance(schema_version, int) + or isinstance(schema_version, bool) + or schema_version != 1 + or payload["publishable"] is not False + ): + raise RuntimeError( + "candidate installer state must be a non-publishable schema-1 object" + ) + sources = _canonical_candidate_sources(payload["sources"]) + return _canonical_json_bytes( + { + "publishable": False, + "schema_version": 1, + "sources": sources, + } + ) + + +def _release_cargo_lock_bytes(root: Path, data: bytes) -> bytes: + """Return the ordinary immutable-Git Cargo lock without projection.""" + + del root + return data + + +def _is_native_build_tree_input(root: Path, tree: Path, path: Path) -> bool: + if not path.is_file(): + return False + tree_relative = path.relative_to(tree) + root_relative = path.relative_to(root) + return ( + not _NATIVE_BUILD_IGNORED_TREE_PARTS.intersection(tree_relative.parts) + and root_relative not in _NON_NATIVE_RUST_PATHS + ) + + +def _native_build_inputs_digest( + root: Path, + *, + normalize_release_cargo_lock: bool = False, +) -> str: + """Hash the small set of checkout inputs that determines the native build.""" + + candidate_config = root / _CANDIDATE_CARGO_CONFIG + candidate_state = root / _CANDIDATE_INSTALL_STATE + canonical_candidate_inputs: dict[Path, bytes] = {} + if not normalize_release_cargo_lock: + if candidate_config.is_file() != candidate_state.is_file(): + raise RuntimeError( + "candidate native build identity requires both " + "candidate-cargo-config.toml and install-state.json" + ) + if candidate_config.is_file(): + cargo_config_data = candidate_config.read_bytes() + canonical_candidate_inputs[candidate_config] = ( + _candidate_cargo_config_bytes(cargo_config_data) + ) + canonical_candidate_inputs[candidate_state] = ( + _candidate_install_state_bytes( + root, + candidate_state.read_bytes(), + cargo_config_data=cargo_config_data, + ) + ) + + paths = [ + root / relative + for relative in _NATIVE_BUILD_INPUT_FILES + if not ( + normalize_release_cargo_lock + and relative in _RELEASE_OMITTED_NATIVE_BUILD_INPUTS + ) + and not (canonical_candidate_inputs and relative == Path("Cargo.lock")) + ] + # Local upstream fixes have no new Git revision. Include their compiler + # inputs in the existing cache key, but not their tests or documentation. + manifest = root / "Cargo.toml" + if manifest.is_file(): + cargo = tomllib.loads(manifest.read_text(encoding="utf-8")) + local_symjit = cargo.get("patch", {}).get("crates-io", {}).get("symjit", {}) + local_path = local_symjit.get("path") + if isinstance(local_path, str): + local_root = (root / local_path).resolve() + if local_root == (root / "TMP_FIXED_SYMJIT").resolve(): + paths.extend(local_root / name for name in ("Cargo.toml", "build.rs")) + paths.extend( + path for path in (local_root / "src").rglob("*") if path.is_file() + ) + for relative in _NATIVE_BUILD_INPUT_TREES: + tree = root / relative + if not tree.is_dir(): + continue + paths.extend( + path + for path in tree.rglob("*") + if _is_native_build_tree_input(root, tree, path) + and not ( + normalize_release_cargo_lock + and path.relative_to(root) in _RELEASE_OMITTED_NATIVE_BUILD_INPUTS + ) + ) + digest = hashlib.sha256(_NATIVE_BUILD_IDENTITY_DOMAIN) + for path in sorted(set(paths)): + if not path.is_file(): + continue + relative = path.relative_to(root).as_posix().encode("utf-8") + data = canonical_candidate_inputs.get(path, path.read_bytes()) + if path == root / "pyproject.toml": + data = _native_pyproject_bytes(data) + if normalize_release_cargo_lock and path == root / "Cargo.lock": + data = _release_cargo_lock_bytes(root, data) + digest.update(len(relative).to_bytes(8, "little")) + digest.update(relative) + digest.update(len(data).to_bytes(8, "little")) + digest.update(data) + return digest.hexdigest() + + +def _native_extensions(package_root: Path) -> tuple[Path, ...]: + return tuple( + sorted( + ( + path + for path in package_root.glob("_rusticol.*") + if path.is_file() and path.name.endswith(_NATIVE_EXTENSION_SUFFIXES) + ), + key=lambda path: path.name, + ) + ) + + +def _read_build_info(path: Path, description: str) -> dict[str, Any]: + try: + payload = json.loads(path.read_text(encoding="utf-8")) + except (OSError, TypeError, ValueError) as error: + raise RuntimeError( + f"{description} is unreadable; rerun `just dev-install`" + ) from error + if not isinstance(payload, dict): + raise RuntimeError(f"{description} is invalid; rerun `just dev-install`") + return payload + + +def _is_source_checkout(package_root: Path, source_root: Path) -> bool: + return ( + package_root == source_root / "src" / "pyamplicol" + and (source_root / "pyproject.toml").is_file() + ) + + +def _verify_source_runtime( + payload: dict[str, Any], + *, + package_root: Path | None = None, + source_root: Path | None = None, +) -> None: + contract = payload.get("source_runtime") + if not isinstance(contract, dict): + raise RuntimeError( + "source runtime provenance is missing; rerun `just dev-install`" + ) + extension_name = contract.get("extension_name") + extension_sha256 = contract.get("extension_sha256") + mode = contract.get("mode") + native_digest = contract.get("native_build_inputs_sha256") + if not all( + isinstance(value, str) and value + for value in (extension_name, extension_sha256, native_digest) + ) or mode not in {"candidate", "release"}: + raise RuntimeError( + "source runtime provenance is incomplete; rerun `just dev-install`" + ) + valid_build_mode = ( + mode == "candidate" and payload.get("publishable") is False + ) or ( + mode == "release" + and ( + payload.get("publishable") is True + or ( + payload.get("publishable") is False + and payload.get("release_prepared_model_bootstrap") is True + ) + ) + ) + if not valid_build_mode: + raise RuntimeError( + "source runtime provenance has an inconsistent build mode; " + "rerun `just dev-install`" + ) + if ( + len(extension_sha256) != 64 + or any(character not in "0123456789abcdef" for character in extension_sha256) + or len(native_digest) != 64 + or any(character not in "0123456789abcdef" for character in native_digest) + or payload.get("native_build_inputs_sha256") != native_digest + ): + raise RuntimeError( + "source runtime provenance is incomplete; rerun `just dev-install`" + ) + package_root = package_root or _SOURCE_PACKAGE_ROOT + extensions = _native_extensions(package_root) + if len(extensions) != 1 or extensions[0].name != extension_name: + raise RuntimeError( + "source runtime extension inventory is ambiguous or stale; " + "rerun `just dev-install`" + ) + if hashlib.sha256(extensions[0].read_bytes()).hexdigest() != extension_sha256: + raise RuntimeError( + "source runtime extension is stale or was replaced; " + "rerun `just dev-install`" + ) + source_root = source_root or _SOURCE_ROOT + if ( + _native_build_inputs_digest( + source_root, + normalize_release_cargo_lock=mode == "release", + ) + != native_digest + ): + raise RuntimeError( + "native build inputs changed after the source runtime was staged; " + "rerun `just dev-install`" + ) + + +def _verify_candidate_install(payload: dict[str, Any]) -> None: + raw_root = payload.get("source_checkout") + native_digest = payload.get("native_build_inputs_sha256") + if ( + not isinstance(raw_root, str) + or not raw_root + or not isinstance(native_digest, str) + or len(native_digest) != 64 + ): + raise RuntimeError( + "candidate wheel provenance is incomplete; rerun `just dev-install`" + ) + source_root = Path(raw_root) + if not source_root.is_absolute() or not (source_root / "pyproject.toml").is_file(): + raise RuntimeError( + "candidate wheel source checkout is unavailable; rerun `just dev-install`" + ) + release_prepared_model_bootstrap = ( + payload.get("release_prepared_model_bootstrap") is True + ) + if ( + _native_build_inputs_digest( + source_root, + normalize_release_cargo_lock=release_prepared_model_bootstrap, + ) + != native_digest + ): + install_kind = ( + "release prepared-model bootstrap" + if release_prepared_model_bootstrap + else "candidate wheel" + ) + raise RuntimeError( + f"installed {install_kind} is stale for this checkout; " + "rerun `just dev-install`" + ) + + +def _active_build_info() -> dict[str, Any] | None: + if ( + _is_source_checkout(_SOURCE_PACKAGE_ROOT, _SOURCE_ROOT) + and _SOURCE_BUILD_INFO_PATH.exists() + ): + return _read_build_info( + _SOURCE_BUILD_INFO_PATH, + "source runtime provenance", + ) + if _PACKAGE_BUILD_INFO_PATH.exists(): + return _read_build_info(_PACKAGE_BUILD_INFO_PATH, "wheel build provenance") + return None + + +def _native_build_inputs_from_build_info(build_info: Mapping[str, Any]) -> str: + native_digest = build_info.get("native_build_inputs_sha256") + if native_digest is None and build_info.get("publishable") is True: + try: + native = importlib.import_module("pyamplicol._rusticol") + except ImportError as error: + raise RuntimeError( + "active native build-input identity is unavailable; reinstall " + "pyAmpliCol" + ) from error + operation = getattr(native, "native_build_inputs_sha256", None) + native_digest = operation() if callable(operation) else None + if ( + not isinstance(native_digest, str) + or len(native_digest) != 64 + or any(character not in "0123456789abcdef" for character in native_digest) + ): + raise RuntimeError( + "active native build-input identity is incomplete; reinstall pyAmpliCol" + ) + return native_digest + + +def active_native_build_inputs_sha256() -> str: + """Return the authenticated native build-input digest, if installed.""" + + build_info = _active_build_info() + if build_info is None: + raise RuntimeError( + "active native build provenance is unavailable; reinstall pyAmpliCol" + ) + return _native_build_inputs_from_build_info(build_info) + + +def active_native_source_identity() -> tuple[str, str]: + """Return the authenticated source revision and native-input digest.""" + + build_info = _active_build_info() + if build_info is None: + raise RuntimeError( + "active native build provenance is unavailable; reinstall pyAmpliCol" + ) + source_revision = build_info.get("source_revision") + if ( + not isinstance(source_revision, str) + or len(source_revision) != 40 + or any(character not in "0123456789abcdef" for character in source_revision) + ): + raise RuntimeError( + "active native source identity is incomplete; reinstall pyAmpliCol" + ) + return source_revision, _native_build_inputs_from_build_info(build_info) + + +def active_source_revision() -> str | None: + """Return the revision bound to the active wheel/source runtime, if any.""" + + build_info = _active_build_info() + if build_info is None: + return None + revision = build_info.get("source_revision") + if ( + not isinstance(revision, str) + or len(revision) != 40 + or any(character not in "0123456789abcdef" for character in revision) + ): + return None + return revision + + +def active_source_checkout() -> Path | None: + """Return the checkout backing a non-publishable local build, if available.""" + + build_info = _active_build_info() + if build_info is None or build_info.get("publishable") is not False: + return None + raw_root = build_info.get("source_checkout") + if not isinstance(raw_root, str) or not raw_root: + return None + root = Path(raw_root) + return root if root.is_absolute() else None + + +def verify_native_module(module: Any, *, expected_version: str | None = None) -> None: + """Reject a stale native extension in contributor builds. + + Published wheels retain the normal package-manager import path; the extra + build-ID check is limited to non-publishable candidate wheels and staged + source runtimes. + """ + + # Unit tests and external adapters may provide a duck-typed stand-in. Real + # extension modules always expose an on-disk module path. + if not getattr(module, "__file__", None): + return + operation = getattr(module, "package_version", None) + if not callable(operation): + raise RuntimeError( + "native runtime has no package-version contract; reinstall pyAmpliCol" + ) + observed = operation() + expected = expected_version or package_version() + if not isinstance(observed, str) or observed.replace("-dev.", ".dev") != expected: + raise RuntimeError( + "native runtime version does not match the Python package " + f"({observed!r} != {expected!r}); rerun `just dev-install`" + ) + build_info = _active_build_info() + if build_info is None: + return + source_runtime = build_info.get("source_runtime") + if build_info.get("publishable") is not False and not isinstance( + source_runtime, dict + ): + return + native_digest = build_info.get("native_build_inputs_sha256") + native_operation = getattr(module, "native_build_inputs_sha256", None) + if not isinstance(native_digest, str) or not callable(native_operation): + raise RuntimeError( + "native runtime build-ID contract is incomplete; rerun `just dev-install`" + ) + if native_operation() != native_digest: + raise RuntimeError( + "native runtime was built from different source inputs; " + "rerun `just dev-install`" + ) + + +def package_version(default: str = "0.2.1") -> str: + """Return the wheel/source-runtime version without importing heavy modules.""" + + if _is_source_checkout(_SOURCE_PACKAGE_ROOT, _SOURCE_ROOT): + source_runtime_present = ( + _SOURCE_BUILD_INFO_PATH.exists() + or _SOURCE_RUNTIME_STAGING_PATH.exists() + or bool(_native_extensions(_SOURCE_PACKAGE_ROOT)) + ) + if source_runtime_present: + if _SOURCE_RUNTIME_STAGING_PATH.exists(): + raise RuntimeError( + "source runtime staging is incomplete; rerun `just dev-install`" + ) + payload = _read_build_info( + _SOURCE_BUILD_INFO_PATH, + "source runtime provenance", + ) + _verify_source_runtime(payload) + value = payload.get("version") + if not isinstance(value, str) or not value: + raise RuntimeError( + "source runtime version provenance is invalid; " + "rerun `just dev-install`" + ) + return value + + if _PACKAGE_BUILD_INFO_PATH.exists(): + payload = _read_build_info( + _PACKAGE_BUILD_INFO_PATH, + "wheel build provenance", + ) + value = payload.get("version") + if not isinstance(value, str) or not value: + raise RuntimeError( + "wheel build version provenance is invalid; reinstall pyAmpliCol" + ) + if payload.get("publishable") is False: + _verify_candidate_install(payload) + return value + try: + return metadata.version("pyamplicol") + except metadata.PackageNotFoundError: + return default + + +__all__ = [ + "COMPILED_COLOR_CONTRACTION_WALSH_C2K_CAPABILITY", + "COMPILED_COLOR_CONTRACTION_WALSH_CAPABILITY", + "COMPILED_COLOR_TOPOLOGY_LANES_CAPABILITY", + "COMPILED_HELICITY_DUAL_LANE_CAPABILITY", + "COMPILED_HELICITY_PRIMARY_RECURRENCE_CAPABILITY", + "COMPILED_HELICITY_SELECTOR_UNION_CAPABILITY", + "COMPILED_MODEL_SCHEMA_VERSION", + "COMPILED_PLANE_ARENA_RUNTIME_CAPABILITY", + "COMPILED_PLANE_DIRECT_APPLICATION_ABI", + "COMPILED_RUNTIME_SELECTORS_CAPABILITY", + "C_ABI_VERSION", + "EAGER_DAG_F64_RUNTIME_CAPABILITY", + "EAGER_DIRECT_ARENA_RUNTIME_CAPABILITY", + "EAGER_DIRECT_TABLE_BINDING_ABI", + "EAGER_DIRECT_TABLE_DESCRIPTOR_ABI", + "EAGER_LC_TOPOLOGY_REPLAY_RUNTIME_CAPABILITY", + "EAGER_RUNTIME_LAYOUT_F64_CAPABILITY", + "EVALUATOR_RUNTIME_CAPABILITIES", + "KNOWN_EVALUATOR_RUNTIME_CAPABILITIES", + "ON_THE_FLY_CONTRACTED_COLOR_RUNTIME_CAPABILITY", + "ON_THE_FLY_LC_COLOR_RUNTIME_CAPABILITY", + "ON_THE_FLY_RUNTIME_CAPABILITY", + "PROCESS_ARTIFACT_SCHEMA_VERSION", + "PYTHON_API_VERSION", + "RECURRENCE_BUILDER_INPUT_ABI", + "RECURRENCE_COLOR_RUNTIME_CAPABILITY", + "RECURRENCE_DIRECT_ARENA_RUNTIME_CAPABILITY", + "RECURRENCE_DIRECT_BACKEND_ABI", + "RECURRENCE_DIRECT_BINDING_ABI", + "RECURRENCE_DIRECT_TEMPLATE_ABI", + "RECURRENCE_HELICITY_SELECTOR_COMPANION_RUNTIME_CAPABILITY", + "RECURRENCE_PLAN_ABI", + "RECURRENCE_RUNTIME_LAYOUT_ABI", + "RUNTIME_PHYSICS_SCHEMA_VERSION", + "SYMBOLICA_ASM_RUNTIME_CAPABILITY", + "SYMBOLICA_CPP_RUNTIME_CAPABILITY", + "SYMBOLICA_LEGACY_JIT_RUNTIME_CAPABILITY", + "SYMBOLICA_SERIALIZATION_ABI", + "SYMJIT_APPLICATION_ABI", + "SYMJIT_F64_RUNTIME_CAPABILITY", + "SYMJIT_PLANE_APPLICATION_ABI", + "SYMMETRIC_GROUP_FFT_COLOR_RUNTIME_CAPABILITY", + "TOML_SCHEMA_VERSION", + "active_native_build_inputs_sha256", + "active_native_source_identity", + "active_source_checkout", + "active_source_revision", + "package_version", + "verify_native_module", +] diff --git a/pyamplicol/src/pyamplicol/_profiling_campaign/README.md b/pyamplicol/src/pyamplicol/_profiling_campaign/README.md new file mode 100644 index 0000000000..63557f2937 --- /dev/null +++ b/pyamplicol/src/pyamplicol/_profiling_campaign/README.md @@ -0,0 +1,190 @@ + +# pyAmpliCol profiling campaign template + +This packaged directory is the authoritative reset template for a +self-contained profiling campaign. Create it in a destination whose name +identifies its measurement environment: + +```console +pyamplicol profiling-campaign copy ./my-profiling-campaign \ + --local-madgraph /path/to/MG5_aMC +cd ./my-profiling-campaign +``` + +The copy contains the report's LaTeX sources, reset raw JSON measurements, and +generated table TeX. Its visible `campaign_artifacts/` directory contains all +evaluator attempts, prepared artifacts, worker logs, locks, leases, and other +runtime state. Moving or renaming the whole campaign therefore moves its state; +no repository-level or legacy `.artifacts` directory is consulted. + +Use the executable manual controller described in `TABLE_FILLING.md`. Its +`--help` output is the authoritative selector, resource-limit, reuse, and +keyboard-control reference: + +```console +./steer_performance_campaign.py --help +``` + +An installed pyAmpliCol wheel can create the portable reset copy without a +source checkout: + +```console +pyamplicol profiling-campaign copy ./pyamplicol-profiling-campaign --force +./pyamplicol-profiling-campaign/steer_performance_campaign.py --help +./pyamplicol-profiling-campaign/steer_performance_campaign.py run \ + --workers 1 --table matrix --process-id 1 --multiplicity 1 \ + --color-approximation lc --generation-mode non-union-flow \ + --generation-engine recurrence --model built_in \ + --no-dependencies-added --no-dashboard +``` + +The final command is a real, deliberately small installation smoke test. It +measures only `d d~ > Z`; broader multiplicities belong on a dedicated +profiling host. + +`--force` overwrites the managed template files and resets only this copy's +`campaign_artifacts/`, `pyAmpliCol.pdf`, `campaign_summary_ids/`, and known +measurement-lineage/LaTeX build byproducts. It keeps unrelated files and a +previously recorded `.pyamplicol-original-amplicol` default unless a new +`--local-amplicol PATH` is supplied. It likewise preserves the recorded +`.pyamplicol-madgraph` default unless `--local-madgraph PATH` is supplied. +Stop an active campaign before resetting its destination. + +Release wheels do not include the optional `ratatui` and `ratatui_py` +dashboard bindings. Installed campaign runs therefore continue headlessly as +if `--no-dashboard` were supplied. `dashboard-snapshot` is available when +those bindings are present, including in a contributor checkout prepared with +`just dev-install`; otherwise it exits with the corresponding instruction. + +By default, a run adds the available numerical-authority closure for every +selected cell at the active source revision. MadGraph standalone is the +authoritative full-colour boundary for the four UFO-SM comparison views; +recurrence is the independent authority for compiled/eager comparisons on the +other surfaces. Original AmpliCol is retained only as a legacy performance and +numerical diagnostic and never certifies or rejects a candidate. Independent +processes remain parallel, while a terminal required authority releases its +candidate to run unverified. Use +`--no-dependencies-added` to suppress optional authority expansion; hard +construction and selector/provider dependencies are always retained. A +selection that directly or automatically includes `amplicol` requires a clean, +complete checkout exposing the color-probe sources and Make targets from PR +#12. Supply it with `run --original-amplicol PATH`, or add +`--local-amplicol PATH` to the copy command to record that checkout as this +campaign's default; the run option overrides the saved default. A source +checkout can prepare the default path with +`just dev-install --with-legacy-amplicol`. The `amplicol_with_patches` branch +works now; a compatible upstream revision will work unchanged after the PR is +merged. + +The full-colour MadGraph views require an installation with executable +`bin/mg5_aMC` and its standard `models/sm` UFO model. Supply it once with +`profiling-campaign copy --local-madgraph PATH`, or override the saved value +with `run --madgraph PATH`. Each authority cell streams `generate`, `output +standalone`, and `launch -f` through a MadGraph command card, counts the +standalone compilation in generation time, then compiles a small campaign-owned +Fortran driver to evaluate and benchmark the generated matrix element. The +same deterministic point is evaluated by pyAmpliCol at precision 200 for +recurrence, compiled, and eager, and at the supported precision 16 for OTF; all +four candidates must agree within relative tolerance `1e-10`. + +For on-the-fly LC cells in the n<=4 matrix, the matching recurrence cell is the +single numerical correctness authority. No compiled artifact is generated or +loaded for that gate. Original AmpliCol remains the legacy performance +baseline used by the OTF-versus-AmpliCol tables. OTF NLC and full colour use a +selector-free contracted workload and require the authenticated contracted- +colour runtime capability; the UFO-SM full-colour cells compare directly with +MadGraph. This contract is report-cache schema v5. + +With `run --fail-fast`, dispatch proceeds in multiplicity waves (`n=1`, then +`n=2`, and so on) and stops at the first required mismatch. A failed legacy +AmpliCol comparison is deliberately non-terminal: the existing table keeps all +other entries unchanged and renders only the AmpliCol generation time in red. + +Rebuild every table and the PDF from one stable current-result snapshot with: + +```console +./steer_performance_campaign.py refresh-pdf +``` + +The refresh shows a coloured progress bar while it reads and confirms the +campaign's current records. List the stable top-level PDF section IDs, or omit +selected sections from one generated PDF, with: + +```console +./steer_performance_campaign.py refresh-pdf --list-sections +./steer_performance_campaign.py refresh-pdf \ + --remove-sections worked-zgg shared-current-dag +``` + +Section removal changes only that staged PDF build. It does not remove +measurements, result JSON, generated table TeX, or source material; the next +plain `refresh-pdf` restores the complete report. `--quiet` suppresses both the +scan progress display and live LaTeX output. + +Review the deterministic dashboard fixture, or capture the newest running +campaign's read-only informational lease, with: + +```console +./steer_performance_campaign.py dashboard-snapshot +./steer_performance_campaign.py dashboard-snapshot --live +``` + +The live form reads only compact lease JSON in the ignored coordination +directory. Use `--instance ID_OR_PREFIX` to choose among concurrent runs; see +`dashboard-snapshot --help` for staleness and styled-cell capture controls. + +Every completed or orderly interrupted `run` atomically replaces +`campaign_summary_ids/` with one text file per non-success status. Each file +contains exact canonical cell IDs and can be fed straight back to `run` or +`inspect`: + +```console +./steer_performance_campaign.py run \ + --cell-id-file campaign_summary_ids/error.txt \ + campaign_summary_ids/validation_failed.txt \ + --force-refresh +``` + +`campaign_summary_ids/unverified.txt` is directly replayable without +`--force-refresh`: those timing diagnostics are not successful currents, and +they are rerun and validated automatically once the required recurrence or +MadGraph authority is available. + +For state-based retries, keep the ordinary table/process/engine selectors and +add `--rerun-failed`, `--rerun-capped`, or both. The flags filter only that +direct selection: failed means its latest terminal non-success is not an +authenticated policy cap, while capped means an authenticated policy cap. The +combined flags take the union; successful, static, and never-attempted cells +are excluded. Unverified results remain automatically retryable and can be +selected as failed. Normal dependency closure and reuse of successful currents +still apply, and `--continue-across-revisions` controls whether historical +terminal state participates. The retry flags conflict with `--force-refresh`. +A retry filter with no matches exits successfully without replacing the +existing campaign summary. + +The default lifecycle is compact. Each completed attempt retains its sealed +result and current pointer, provenance, commands, progress/timeline evidence, +bounded log tails, and reusable outputs needed by currents or dependencies. +Disposable copied AmpliCol, MadGraph, and build workspaces are removed after +publication; cross-revision reuse relies on the sealed results, not those +workspaces. Pass `run --retain-workspaces` only when full debug workspaces are +needed. `--cleanup-artifacts` remains a compatibility spelling for the default +compact policy. + +Two storage guards apply independently of process-tree memory supervision: +`--attempt-output-limit` defaults to 67,108,864 bytes (64 MiB) per watched +output/log file, and `--minimum-free-disk` defaults to 5,368,709,120 bytes +(5 GiB) on the campaign artifact volume. Both options accept byte counts. + +Create independent campaigns with separate `profiling-campaign copy` commands. +Every result artifact, worker lease, lock, and reproduction file remains below +that copy's `campaign_artifacts/`, even when two destinations share a basename. +A copy made from an installed wheel uses that wheel's recorded source revision +and installed runtime. Never commit `campaign_artifacts/`, evaluator artifacts, +candidate wheels, prepared models, attempts, logs, locks, coordination state, +page images, or LaTeX auxiliary files. + +When the launcher runs from a source checkout, it checks the checkout index +and worktree before measuring. It reads ordinary Git metadata directly and, +if HEAD is packed, uses one read-only `git rev-parse` query for the committed +tree. Installed-wheel campaigns do not need a Git checkout. diff --git a/pyamplicol/src/pyamplicol/_profiling_campaign/TABLE_FILLING.md b/pyamplicol/src/pyamplicol/_profiling_campaign/TABLE_FILLING.md new file mode 100644 index 0000000000..e165188269 --- /dev/null +++ b/pyamplicol/src/pyamplicol/_profiling_campaign/TABLE_FILLING.md @@ -0,0 +1,224 @@ + +# Profiling campaign table filling + +First create and enter a reset campaign. The executable controller in that +copy is the authoritative campaign interface: + +```console +pyamplicol profiling-campaign copy ./my-profiling-campaign \ + --local-madgraph /path/to/MG5_aMC +cd ./my-profiling-campaign +./steer_performance_campaign.py --help +``` + +For an installed release wheel, first copy the reset campaign into a new or +empty destination and invoke the controller there: + +```console +pyamplicol profiling-campaign copy ./pyamplicol-profiling-campaign --force +./pyamplicol-profiling-campaign/steer_performance_campaign.py \ + run --workers 1 --table matrix --process-id 1 --multiplicity 1 \ + --color-approximation lc --generation-mode non-union-flow \ + --generation-engine recurrence --model built_in \ + --no-dependencies-added --no-dashboard +``` + +This real smoke campaign selects only the final-state-multiplicity-one +`d d~ > Z` recurrence cell and is suitable for checking an installation. + +Release wheels omit the optional `ratatui` and `ratatui_py` dashboard +bindings. Installed runs automatically continue headlessly as though +`--no-dashboard` had been supplied. `dashboard-snapshot` remains available +when those bindings are present, notably in a contributor checkout prepared +with `just dev-install`; without them it exits with an actionable instruction. + +By default, selected cells gain their available active-source numerical +authority closure. MadGraph standalone is authoritative for the full-colour +UFO-SM views, and recurrence is authoritative for the remaining compiled/eager +cross-mode comparisons. Original AmpliCol is an optional legacy diagnostic and +performance denominator only. Added cells are dependency-only work; +independent processes remain parallel, and a terminal required authority +releases its candidate to run unverified. Pass `--no-dependencies-added` to +suppress optional +authority expansion while retaining every hard construction and +selector/provider dependency. A selection that directly or automatically +includes `amplicol` requires a clean, complete checkout exposing the PR #12 +probe sources and Make targets. Supply it with +`run --original-amplicol PATH`, or pass `--local-amplicol PATH` to the initial +`profiling-campaign copy` command to save it as the campaign default; the run +option overrides the saved default. A source checkout can prepare the default +path with `just dev-install --with-legacy-amplicol`. The +`amplicol_with_patches` branch works now, and a compatible upstream revision +will work unchanged after the PR is merged. + +The MadGraph full-colour cells require an installation containing executable +`bin/mg5_aMC` and the standard `models/sm` UFO model. Record it with the copy +command's `--local-madgraph PATH`, or supply/override it for one invocation via +`run --madgraph PATH`. The campaign times the streamed `generate`, `output +standalone`, `launch -f` sequence, including standalone compilation, and uses a +custom Fortran driver for the reference value and repeated-evaluation timing. +pyAmpliCol evaluates the shared comparison point at precision 200 for +recurrence, compiled, and eager, and at the supported precision 16 for OTF; +the strict relative tolerance is `1e-10` for every candidate. + +Use `run --fail-fast` for multiplicity-wave validation: all selected `n=1` +work completes before `n=2` is released, and the campaign stops at the first +required mismatch. A disagreement with legacy AmpliCol is non-terminal; only +its generation-time token is rendered red, while the rest of the existing +table remains unchanged. OTF NLC and full colour are genuine contracted +workloads with no flow selector and require their accuracy-specific runtime +capability; OTF LC retains the selected-flow and all-flow workloads. + +Inside a contributor checkout it re-executes with the repository `.venv`; an +installed copy uses the wheel's Python runtime. Both modes reuse compatible +same-source currents by default, supervise each process tree, and store campaign +state plus compact retained evidence in this campaign's visible +`campaign_artifacts/` directory. Full workspaces and other heavy debugging +payloads are retained only with `--retain-workspaces`. That state moves with the +entire campaign and never falls back to an old repository-level `.artifacts` +directory. Use separate copied directories for independent campaigns, even when +their basenames happen to match. + +Preview selections before launching workers: + +```console +./steer_performance_campaign.py run \ + --dry-run --table z_table --multiplicity 3 4 \ + --model built_in sm_ufo +``` + +The preview lists direct cells and dependency work separately and prints a +wrapped public-CLI recipe for every selected runnable cell, even for broad +selections. Each recipe uses the repository `.venv/bin/pyamplicol` and states +whether it is exact, a template, or a diagnostic protocol exception. UFO +recurrence/eager recipes include their public `model compile` prerequisite. + +The same small installation campaign, when run from inside its directory, is: + +```console +./steer_performance_campaign.py run \ + --workers 1 --table matrix --process-id 1 --multiplicity 1 \ + --color-approximation lc --generation-mode non-union-flow \ + --generation-engine recurrence --model built_in \ + --no-dependencies-added --no-dashboard +``` + +Broader selections may raise `--workers` and `--cores-per-worker` on a dedicated +profiling host. Original AmpliCol builds use the independent +`--amplicol-build-jobs` setting, which remains 1 by default because the +maintained legacy generator target is not parallel-safe. Defaults are one +worker, one pyAmpliCol core, one AmpliCol build job, a one-hour +generation/preparation limit, and a decimal 30 GB process-tree RAM limit. +`--ram-limit` applies to each worker tree. Add `--campaign-ram-limit` to place +an aggregate ceiling across all workers; it is conservatively divided by the +requested worker count and combined with the per-worker limit. Thus +`--workers 10 --ram-limit 30000000000 --campaign-ram-limit 30000000000` +limits each tree to 3 GB rather than permitting ten independent 30 GB claims. +Press `Ctrl-C` or `Esc` to stop dispatch, terminate supervised process trees, +preserve completed currents, and restore the terminal. + +After normal completion or an orderly interrupt, the controller prints the +absolute path of `campaign_summary_ids/`. It replaces that directory with one +sorted text file per non-success status, for example `error.txt`, +`blocked_dependency.txt`, `validation_failed.txt`, or `worker_timeout.txt`. +Replay any union of those exact IDs while retaining the normal dependency +closure and other selector intersections: + +```console +./steer_performance_campaign.py run \ + --cell-id-file campaign_summary_ids/error.txt \ + campaign_summary_ids/validation_failed.txt \ + --force-refresh +``` + +`--cell-id-file` is also accepted by `inspect`. Blank lines and full-line `#` +comments are ignored. For a structured retry within the ordinary direct +selection, add `--rerun-failed`, `--rerun-capped`, or both: + +```console +./steer_performance_campaign.py run \ + --table z_table --generation-engine recurrence compiled \ + --rerun-failed --rerun-capped +``` + +`--rerun-failed` keeps cells whose latest terminal outcome is unsuccessful but +is not an authenticated policy cap; `--rerun-capped` keeps authenticated policy +caps, and combining them takes the union. Successful, static, and unseen cells +are not direct retry targets. Unverified results are automatically retryable +and may be selected as failed. Dependency closure is still planned normally, +including reuse of successful prerequisite currents. Historical outcomes are +recognized only with `--continue-across-revisions`. These retry flags cannot be +combined with `--force-refresh`; use that option only for an unconditional +refresh. If no selected cell matches the requested retry state, the command is +a successful no-op and preserves the existing campaign summary. + +Replaying `blocked_dependency.txt` automatically includes required +prerequisites. +`unverified.txt` needs no `--force-refresh`: an unverified timing diagnostic is +not a successful current and is automatically rerun against a later available +recurrence or MadGraph authority. + +The default lifecycle is compact. Each completed attempt retains its sealed +result and current pointer, provenance, commands, progress/timeline evidence, +bounded log tails, and reusable outputs needed by currents or dependencies. +Disposable copied AmpliCol, MadGraph, and build workspaces are removed after +publication; cross-revision reuse relies on the sealed results, not those +workspaces. Use `--retain-workspaces` only when full debug workspaces are +needed. `--cleanup-artifacts` remains a compatibility spelling for the default +compact policy. + +Storage safety is separate from the process-tree RAM cap. +`--attempt-output-limit` defaults to 67,108,864 bytes (64 MiB) per watched +output/log file, while `--minimum-free-disk` reserves 5,368,709,120 bytes +(5 GiB) on the campaign artifact volume. Both options accept byte counts. + +Running `pyamplicol profiling-campaign copy DEST --force` again resets only +`DEST/campaign_artifacts`, the managed PDF, the campaign summary-ID directory, +known lineage/LaTeX build byproducts, and the packaged template files. It +preserves unrelated files and the recorded original-AmpliCol and MadGraph +paths unless new `--local-amplicol` or `--local-madgraph` values are supplied. +Stop active campaign processes before resetting their destination. + +Capture a running dashboard from another terminal without attaching to or +changing the campaign: + +```console +./steer_performance_campaign.py \ + dashboard-snapshot --live --width 160 --height 48 +``` + +This reads only the compact non-stale coordination lease. The newest active +invocation is the default; pass `--instance ID_OR_PREFIX` to choose one +concurrent run. Omit `--live` for the deterministic synthetic layout fixture. + +Inspect coverage and AmpliCol-relative statistics: + +```console +./steer_performance_campaign.py inspect \ + --color-approximation lc --generation-engine recurrence compiled +``` + +Rebuild all current JSON/TeX tables and atomically replace the PDF: + +```console +./steer_performance_campaign.py refresh-pdf +``` + +Interactive refreshes show a coloured progress bar while the controller reads +and confirms the complete current-record snapshot. To discover stable +top-level section IDs or build a shorter PDF without changing campaign data: + +```console +./steer_performance_campaign.py refresh-pdf --list-sections +./steer_performance_campaign.py refresh-pdf \ + --remove-sections worked-zgg shared-current-dag +``` + +The omission applies only to that PDF build. Measurements, JSON caches, table +TeX, and the canonical source remain present; a later plain refresh restores +the full report. Use `--quiet` to suppress the scan progress display and live +LaTeX output. + +Human output is coloured by default. Use `--no-color` or `NO_COLOR`; JSON is +always uncoloured. Consult each subcommand’s `--help` for all selectors, +aliases, resource controls, profiling parameters, reuse rules, and examples. diff --git a/pyamplicol/src/pyamplicol/_profiling_campaign/build_pdf.py b/pyamplicol/src/pyamplicol/_profiling_campaign/build_pdf.py new file mode 100644 index 0000000000..47b616fc1c --- /dev/null +++ b/pyamplicol/src/pyamplicol/_profiling_campaign/build_pdf.py @@ -0,0 +1,112 @@ +# SPDX-License-Identifier: 0BSD +"""Compile and verify an exported pyAmpliCol report using only its TeX folder.""" + +from __future__ import annotations + +import argparse +import re +import subprocess +from collections.abc import Sequence +from pathlib import Path +from shutil import which + + +class StandaloneBuildError(RuntimeError): + """The standalone publication could not be compiled or verified.""" + + +_UNRESOLVED_PATTERNS = ( + re.compile(r"LaTeX Warning: Citation .* undefined"), + re.compile(r"LaTeX Warning: Reference .* undefined"), + re.compile(r"LaTeX Warning: There were undefined references"), + re.compile(r"Package rerunfilecheck Warning:"), +) + + +def validate_latex_log(log: str) -> None: + """Reject layout overflow and unresolved cross-reference diagnostics.""" + + if "Overfull \\hbox" in log or "Overfull \\vbox" in log: + raise StandaloneBuildError("LaTeX output contains an overfull box") + for pattern in _UNRESOLVED_PATTERNS: + if pattern.search(log): + raise StandaloneBuildError( + f"LaTeX output is unresolved: {pattern.pattern}" + ) + + +def compile_report( + report_dir: Path, + *, + engine: str = "pdflatex", + passes: int = 3, +) -> Path: + """Compile ``pyAmpliCol.tex`` and reject unresolved or overflowing output.""" + + root = report_dir.expanduser().resolve(strict=True) + source = root / "pyAmpliCol.tex" + if not source.is_file(): + raise StandaloneBuildError(f"missing report source: {source}") + if passes < 2: + raise StandaloneBuildError("at least two LaTeX passes are required") + executable = which(engine) + if executable is None: + raise StandaloneBuildError(f"LaTeX engine is not available: {engine}") + + command = ( + executable, + "-interaction=nonstopmode", + "-halt-on-error", + "-file-line-error", + source.name, + ) + for _ in range(passes): + completed = subprocess.run( + command, + cwd=root, + check=False, + capture_output=True, + text=True, + ) + if completed.returncode != 0: + detail = (completed.stdout + completed.stderr)[-4000:] + raise StandaloneBuildError( + f"LaTeX compilation failed with exit {completed.returncode}:\n" + f"{detail}" + ) + + log_path = root / "pyAmpliCol.log" + try: + log = log_path.read_text(encoding="utf-8", errors="replace") + except OSError as error: + raise StandaloneBuildError(f"cannot read LaTeX log: {error}") from error + validate_latex_log(log) + + output = root / "pyAmpliCol.pdf" + if not output.is_file() or output.stat().st_size == 0: + raise StandaloneBuildError(f"LaTeX did not produce {output}") + return output + + +def _parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + description="Compile and verify this standalone pyAmpliCol report", + ) + parser.add_argument("--engine", default="pdflatex") + parser.add_argument("--passes", type=int, default=3) + return parser + + +def main(argv: Sequence[str] | None = None) -> int: + arguments = _parser().parse_args(argv) + output = compile_report( + Path(__file__).resolve().parent, + engine=arguments.engine, + passes=arguments.passes, + ) + print(output) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/pyamplicol/src/pyamplicol/_profiling_campaign/pyAmpliCol.tex b/pyamplicol/src/pyamplicol/_profiling_campaign/pyAmpliCol.tex new file mode 100644 index 0000000000..885d2d8362 --- /dev/null +++ b/pyamplicol/src/pyamplicol/_profiling_campaign/pyAmpliCol.tex @@ -0,0 +1,537 @@ +% SPDX-License-Identifier: 0BSD +\documentclass[10pt,letterpaper]{article} + +\usepackage[T1]{fontenc} +\usepackage[utf8]{inputenc} +\usepackage[letterpaper,margin=0.75in,headheight=21pt]{geometry} +\usepackage{amsmath,amssymb} +\usepackage{array} +\usepackage{booktabs} +\usepackage{eso-pic} +\usepackage{fancyhdr} +\usepackage{hyperref} +\usepackage{listings} +\usepackage{longtable} +\usepackage{pdflscape} +\usepackage{siunitx} +\usepackage[table]{xcolor} + +\DeclareMathSizes{5}{5}{5}{5} +\DeclareMathSizes{6.8}{6.8}{5}{5} + +\definecolor{ReportInk}{HTML}{17212B} +\definecolor{ReportBlue}{HTML}{145A86} +\definecolor{ReportGreen}{HTML}{287A58} +\definecolor{ReportOrange}{HTML}{B76E00} +\definecolor{ReportRed}{HTML}{B33A3A} +\definecolor{ReportRule}{HTML}{CBD3DA} +\definecolor{ReportMuted}{HTML}{66717C} +\definecolor{refblue}{HTML}{DCEBFF} +\definecolor{bestgreen}{HTML}{DFF5E4} +\definecolor{speedgreen}{HTML}{1A7F37} +\definecolor{speedorange}{HTML}{BF8700} +\definecolor{speedred}{HTML}{CF222E} + +\hypersetup{ + colorlinks=true, + linkcolor=ReportBlue, + urlcolor=ReportBlue, + pdftitle={pyAmpliCol 1.0.1 Methodology and Performance Report}, + pdfauthor={Rikkert Frederix, Valentin Hirschi, and Timea Vitos} +} +\sisetup{detect-all,scientific-notation=true,round-mode=figures,round-precision=3} +\setlength{\parindent}{0pt} +\setlength{\parskip}{0.55em} +\setcounter{secnumdepth}{2} +\setcounter{tocdepth}{1} + +\pagestyle{fancy} +\fancyhf{} +\fancyhead[L]{\textcolor{ReportInk}{pyAmpliCol 1.0.1}} +\fancyhead[R]{\textcolor{ReportMuted}{Methodology and performance}} +\fancyfoot[C]{\thepage} +\renewcommand{\headrulewidth}{0.4pt} +\renewcommand{\headrule}{\hbox to\headwidth{\color{ReportRule}\leaders\hrule height \headrulewidth\hfill}} + +\newif\ifReportLandscapePage +\ReportLandscapePagefalse +\AddToShipoutPictureFG{% + \ifReportLandscapePage + \AtPageLowerLeft{% + \put(\LenToUnit{\dimexpr\paperwidth-0.35in\relax}, + \LenToUnit{0.5\paperheight}){% + \rotatebox{90}{\makebox[0pt][c]{% + \textcolor{ReportMuted}{\thepage}}}}}% + \fi +} + +\newcolumntype{L}[1]{>{\raggedright\arraybackslash}p{#1}} +\newcolumntype{R}[1]{>{\raggedleft\arraybackslash}p{#1}} +\newcommand{\PAC}{\texttt{pyAmpliCol}} +\newcommand{\AC}{\texttt{AmpliCol}} +\newcommand{\ReportNA}{\textcolor{ReportMuted}{\textsc{n/a}}} +\newcommand{\ReportStatus}[1]{\textcolor{ReportMuted}{\textsc{#1}}} +\newcommand{\ReportMetricCell}[2]{\shortstack{\textcolor{ReportGreen}{G #1}\\\textcolor{ReportBlue}{R #2}}} +\newcommand{\ReportTableNote}[1]{\par\smallskip\noindent\footnotesize\textcolor{ReportMuted}{#1}\par} +\newcommand{\ReportMatrixLegend}{\ReportTableNote{The table heading names the baseline and candidate. LC reports the selected-flow, summed-helicity workload and the all-flow, selected-helicity workload separately. NLC and full colour report one contracted workload. Parenthesized values are candidate-to-baseline ratios; an execution attribution is shown only when both measurements expose it.}} +\newcommand{\ReportLadderLegend}{\ReportTableNote{Each LC workload reports process-generation time in seconds and native wall time in microseconds per phase-space point, together with a separately measured execution attribution when exposed. Compiled rows use the labelled JIT, ASM, or C++ backend; eager and recurrence rows use portable JIT O2 prepared models. Parenthesized wall-time and selected-flow generation values compare each pyAmpliCol row with the corresponding AmpliCol workload. For compiled and eager rows, wall time is the authenticated warmed total-evaluator boundary; not exposed refers only to a narrower execution attribution. All-flow generation is shown absolutely without a ratio because its setup boundary differs from the reference.}} +\newcommand{\ReportLandscapeFooter}{} +\newcommand{\ReportProfileName}{unpopulated} +\newcommand{\ReportPlatformSummary}{specified by the architecture profile} +\newcommand{\ReportToolchainSummary}{specified by the architecture profile} +\newcommand{\ReportEditionStatement}{This source is the unpopulated + documentation template. Architecture-specific copies contain their recorded + environment and measured results.} +\InputIfFileExists{report_environment.tex}{}{} + +\lstset{ + basicstyle=\ttfamily\small, + columns=fullflexible, + frame=single, + rulecolor=\color{ReportRule}, + breaklines=true, + showstringspaces=false, + xleftmargin=0.5em, + xrightmargin=0.5em +} + +\title{\vspace{-1.4cm}\textbf{pyAmpliCol 1.0.1}\\[0.35em] + \Large Methodology and Performance Report} +\author{Rikkert Frederix \and Valentin Hirschi \and Timea Vitos} +\date{Release 1.0.1\\[0.25em]\small Measurement profile: + \texttt{\ReportProfileName}} + +\begin{document} +\maketitle + +\begin{abstract} +This report presents the physics coverage, numerical validation, and +performance of \PAC{} 1.0.1. We compare compiled, eager, recurrence, and +on-the-fly evaluation across the report's stated built-in Standard Model and +UFO or serialized-JSON scopes, together with scalar-contact and scalar-gravity +benchmarks. Every reported +timing is associated with a documented software revision, hardware profile, +numerical configuration, and validation reference appropriate to the +corresponding data set. +\end{abstract} + +\begingroup +\small +\setlength{\parskip}{0pt} +\tableofcontents +\endgroup +\clearpage + +% pyamplicol-report-section-begin: scope +\section{Scope} + +\PAC{} generates and evaluates tree-level scattering amplitudes. Compiled mode +specializes a shared-current directed acyclic graph (DAG) to a process. Eager +mode evaluates the same graph with reusable model kernels. Recurrence mode +constructs a compact current schedule directly. On-the-fly mode stores a +compact source-oriented process seed and constructs selector-family schedules +lazily; this report covers its built-in-SM leading-colour and selector-free +contracted NLC/full lanes through four final-state particles. Within their +declared scopes, the modes implement the +same physics normalization, colour treatment, helicity sums, and resolved +observables. The original \AC{} program and its use in event generation are +described in Ref.~\cite{FrederixVitos2026}. + +The report has three purposes: +\begin{enumerate} + \item define the observables and conditions required for a reproducible + benchmark; + \item preserve a stable matrix of representative process families and colour + approximations; and + \item present performance only after numerical agreement has been verified. +\end{enumerate} + +In each architecture-specific report, an \ReportNA{} entry denotes a +process/multiplicity combination outside the declared scope or an observable +that does not apply. Every applicable cell across the complete declared +multiplicity range must instead contain a validated measurement or an +documented resource-limit status. A measurement that does not pass +numerical validation is labelled explicitly and never replaced by a timing +value. No value is copied, interpolated, or estimated from a neighbouring +cell. +% pyamplicol-report-section-end: scope + +% pyamplicol-report-section-begin: recursion +\section{Current Recursion and Model Lowering} + +\subsection{Shared-current DAG} + +For an external subset $I$, particle state $t$, and local spin or flow label +$\chi$, an off-shell current has the schematic recursion +\begin{equation} + J^{\alpha}_{t,\chi}(I) + = P^{\alpha}{}_{\beta}(t,p_I) + \sum_{I=A\mathbin{\dot\cup}B} + V^{\beta}{}_{\gamma\delta}(t_A,t_B\!\to t) + J^{\gamma}_{t_A,\chi_A}(A) + J^{\delta}_{t_B,\chi_B}(B), + \qquad p_I=\sum_{i\in I}p_i . + \label{eq:recursion} +\end{equation} +Current identity includes the particle, external labels, helicity ancestry, +chirality or spin state, flavour and charge flow, colour state, momentum mask, +and any auxiliary-current kind. Contributions with the same complete identity +share one node. A topological sweep therefore evaluates each retained current +once per phase-space point and reuses it in all dependent amplitudes. +This is a Berends--Giele-type construction~\cite{BerendsGiele1988}. + +The Universal Feynman Output format (UFO; originally Universal FeynRules +Output) describes particles, parameters, interactions, Lorentz tensors, +colour tensors, and propagators in a model-independent +form~\cite{UFO2012,UFO2023}. \PAC{} normalizes a trusted UFO Python module, or +its own serialized JSON representation of that model, to the same canonical +representation used by process generation. The UFO module is executable +Python; the corresponding serialized JSON form is data-only. + +Optimizations of imported models are based on exact colour, Lorentz, +propagator, and coupling identities rather than particle names or numerical +codes. For Standard-Model validation processes, the built-in and imported +routes must produce equivalent current, interaction, amplitude, and colour +content. Algebraic reuse is allowed only when the relevant identity has been +established exactly. + +\subsection{Contact terms and spin two} + +A supported vertex with valence above three is lowered to a deterministic tree +of non-propagating auxiliary currents. The construction inserts the original +coupling once and introduces no pole. Direct contraction of the original +contact tensor is the validation reference for the scalar-contact ladder. + +The scalar-gravity ladder exercises rank-two external states and +momentum-dependent interactions. For a massless graviton, the helicity-two +polarization tensor is represented as +\begin{equation} + \epsilon^{\mu\nu}_{\pm 2} + = \epsilon^{\mu}_{\pm}\epsilon^{\nu}_{\pm} . +\end{equation} +Validation checks symmetry, transversality, tracelessness, normalization, and +agreement between ordinary and higher-precision evaluation. +% pyamplicol-report-section-end: recursion + +% pyamplicol-report-section-begin: colour-runtime +\section{Colour and Runtime Semantics} + +The process matrices use three colour contracts: +\begin{center} +\begin{tabular}{@{}L{0.45in}L{5.85in}@{}} +\textbf{LC} & Leading-colour ordered amplitudes with physical colour-flow selectors~\cite{Maltoni2002}. \\ +\textbf{NLC} & Every colour-metric entry whose leading power is at most two below the leading power of the complete basis. \\ +\textbf{Full} & The complete supported sparse colour metric for the generated amplitude basis. \\ +\end{tabular} +\end{center} + +For NLC, ``retained'' applies to a colour-matrix \emph{entry}, not to a +truncated numerical coefficient. Write the exact overlap of two colour-flow +tensors as +\begin{equation} + C_{ab}(N_c)=\sum_p c^{(ab)}_p N_c^p, + \qquad + p_{ab}=\max\{p:c^{(ab)}_p\ne0\}, + \label{eq:nlc-entry-gate} +\end{equation} +and let \(P\) be the leading power of the complete colour basis. The sparse +NLC metric is +\begin{equation} + C^{\mathrm{NLC}}_{ab}= + \begin{cases} + C_{ab}(3), & p_{ab}\ge P-2,\\ + 0, & p_{ab}\linewidth\resizebox{\linewidth}{!}{\usebox0}\else\makebox[\linewidth][c]{\usebox0}\fi\endgroup} +\providecommand{\matrixpunct}[1]{\textcolor{black}{\texttt{#1}}} +\providecommand{\matrixratio}[2]{\matrixpunct{(}\textcolor{#1}{\texttt{x#2}}\matrixpunct{)}} +\providecommand{\matrixna}[1]{\textcolor{#1}{\texttt{N/A}}} +\providecommand{\matrixnotapplicable}[1]{\textcolor{#1}{\textsc{not applicable}}} +\providecommand{\matrixnotexposed}[1]{\textcolor{#1}{\textsc{not exposed}}} +\providecommand{\matrixstaticna}[1]{\textcolor{#1}{\textsc{static N/A}}} +\providecommand{\matrixtotalevaluator}[1]{\textcolor{ReportBlue}{\texttt{T }}#1} +\providecommand{\matrixrecurrencecore}[1]{\textcolor{ReportOrange}{\texttt{C }}#1} +\providecommand{\matrixzrecurrenceclocks}[2]{\shortstack[r]{#1\\#2}} +\providecommand{\matrixwallclock}[2]{\textcolor{#1}{\texttt{W #2}}} +\providecommand{\matrixwallabsolute}[1]{\textcolor{ReportBlue}{\texttt{W }}#1} +\providecommand{\matrixruntimepair}[2]{\shortstack[l]{#1\\#2}} +\providecommand{\matrixruntimetriple}[3]{\shortstack[l]{#1\\#2\matrixpunct{ | }#3}} +\providecommand{\matrixbelow}[1]{\textcolor{ReportMuted}{\texttt{<#1}}} +\providecommand{\matrixnaratio}[1]{\matrixpunct{(}\matrixna{#1}\matrixpunct{)}} +\providecommand{\matrixstatus}[2]{\textcolor{#1}{\textsc{#2}}} +\providecommand{\matrixncabsolute}[1]{#1} +\providecommand{\matrixsummaryratio}[2]{\textcolor{#1}{\texttt{x#2}}} +\providecommand{\matrixsummaryratiohighlight}[2]{\begingroup\setlength{\fboxsep}{0.9pt}\setlength{\fboxrule}{0.35pt}\fbox{\textcolor{#1}{{\usefont{T1}{lmtt}{b}{n}x#2}}}\endgroup} +\providecommand{\matrixsummarystats}[5]{\begingroup\matrixsummaryfont\begin{tabular}[t]{@{}l@{\hspace{0.014in}\matrixpunct{|}\hspace{0.014in}}l@{\hspace{0.014in}\matrixpunct{|}\hspace{0.014in}}l@{\hspace{0.014in}\matrixpunct{|}\hspace{0.014in}}l@{\hspace{0.014in}\matrixpunct{|}\hspace{0.014in}}l@{}}\makebox[3.6em][l]{#1}&\makebox[4.6em][l]{#2}&\makebox[3.6em][l]{#3}&\makebox[3.6em][l]{#4}&\makebox[4.2em][l]{#5}\end{tabular}\endgroup} +\providecommand{\matrixsummaryworkloads}[2]{\begin{tabular}[t]{@{}l@{}}#1\\[-0.10em]#2\end{tabular}} +\providecommand{\matrixpair}[2]{\begin{tabular}[t]{@{}r@{\hspace{0.025in}\matrixpunct{|}\hspace{0.025in}}r@{}}#1\end{tabular}} +\providecommand{\bestmoderatio}[2]{\textcolor{#1}{\texttt{x#2}}} +\providecommand{\bestmodecompactratio}[3]{\matrixpunct{(}\textcolor{ReportMuted}{\texttt{[x#1]}}\hspace{0.04in}\textcolor{#2}{\texttt{x#3}}\matrixpunct{)}} +\providecommand{\bestmodewallratio}[2]{\matrixpunct{(}\textcolor{#1}{\texttt{x#2}}\matrixpunct{)}} +\providecommand{\bestmodecompactprefix}[1]{\matrixpunct{(}\textcolor{ReportMuted}{\texttt{[x#1]}}\hspace{0.04in}} +\providecommand{\bestmodeopenprefix}{\matrixpunct{(}\hspace{0.04in}} +\providecommand{\bestmodeprimaryratio}[2]{\textcolor{#1}{\texttt{x#2}}\matrixpunct{)}} +\providecommand{\bestmodeabsoluteprefix}[1]{#1\hspace{0.04in}} +\providecommand{\bestmodecompactabsolute}[2]{\textcolor{ReportMuted}{\texttt{[#1]}}\hspace{0.04in}\textcolor{black}{\texttt{#2}}} +\providecommand{\bestmodecompactabsoluteprefix}[1]{\textcolor{ReportMuted}{\texttt{[#1]}}\hspace{0.04in}} +\providecommand{\bestmodeabsoluteprimary}[1]{\textcolor{black}{\texttt{#1}}} +\providecommand{\bestmodeunverifiedclockprefix}[2]{\textcolor{ReportMuted}{\texttt{[}#1\texttt{]}}\hspace{0.04in}#2\hspace{0.04in}} +\providecommand{\bestmodecode}[1]{\textcolor{ReportMuted}{\texttt{(#1)}}} +\providecommand{\bestmodecodeprefix}[1]{\bestmodecode{#1}\hspace{0.025in}} +\providecommand{\bestmodeabsolutechoice}[2]{#1\hspace{0.025in}\bestmodecode{#2}} +\providecommand{\bestmodemix}[1]{\textcolor{ReportBlue}{\texttt{[#1]}}} +\clearpage +\subsection{Built-in SM best measured mode versus AmpliCol: full-colour} +\noindent\begin{minipage}{\linewidth} +\subsubsection*{Built-in SM best measured mode versus AmpliCol full-colour (block 1 of 2)} +\begingroup +\centering +\footnotesize +\setlength{\tabcolsep}{2.1pt} +\renewcommand{\arraystretch}{1.00} +\matrixfitwidth{% +\begin{tabular}{@{}rL{1.45in}l>{\matrixentryfont}l>{\matrixentryfont}r@{}>{\matrixentryfont}l>{\matrixentryfont}l>{\matrixentryfont}r@{}>{\matrixentryfont}l>{\matrixentryfont}l>{\matrixentryfont}r@{}>{\matrixentryfont}l@{}} +\toprule +\textbf{ID} & \textbf{base process} & \textbf{metric} & \multicolumn{3}{c}{\textbf{n=1}} & \multicolumn{3}{c}{\textbf{n=2}} & \multicolumn{3}{c}{\textbf{n=3}} \\ +\specialrule{0.85pt}{0pt}{0pt} +\rowcolor{refblue} +\texttt{1} & $d\bar d\to Z+(n-1)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{2} & $u\bar d\to W^++(n-1)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{3} & $d\bar d\to e^+e^-+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{4} & $u\bar d\to e^+\nu_e+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{5} & $d\bar d\to ZZ+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{6} & $gg\to t\bar t+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{7} & $d\bar d\to t\bar t+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{8} & $gg\to gg+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{9} & $d\bar d\to ZZZ+(n-3)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{10} & $d\bar d\to e^+e^-ZH+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{11} & $d\bar d\to t\bar t ZH+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{12} & $d\bar d\to e^+e^-e^+e^-+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{13} & $d\bar d\to u\bar u\,s\bar s+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{14} & $d\bar d\to u\bar u\,s\bar s\,c\bar c+(n-6)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{15} & $d\bar d\to u\bar u\,u\bar u+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\specialrule{1.05pt}{0.22em}{0.18em} +\multicolumn{3}{@{}l}{} & \multicolumn{3}{c}{\cellcolor{refblue}\textbf{n=1}} & \multicolumn{3}{c}{\textbf{n=2}} & \multicolumn{3}{c}{\cellcolor{refblue}\textbf{n=3}} \\ +\multicolumn{3}{@{}l}{\textbf{summary: generation}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} & \multicolumn{3}{c}{\matrixna{ReportMuted}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} \\[0.08em] +\multicolumn{3}{@{}l}{\textbf{summary: wall}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} & \multicolumn{3}{c}{\matrixna{ReportMuted}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} \\ +\bottomrule +\end{tabular} +} +\endgroup +\ReportTableNote{{\scriptsize The candidate is selected independently in each cell and workload by the smallest validated wall time. The upper row is generation in seconds and the lower row is runtime in \(\mu\mathrm{s}/\mathrm{pt}\). In LC cells, each pair is non-union-flow \texttt{|} union-flow. Generation multipliers are coloured directly. Each runtime workload uses \texttt{([xS]xW)}. The muted supplementary xS first compares compatible exposed execution attributions; otherwise it uses authenticated evaluator totals. Because original AmpliCol exposes no evaluator-total clock, a candidate total then uses the AmpliCol direct execution clock, or its wall clock when direct execution is absent, as denominator. An evaluator total is never divided by a recurrence core. This evaluator-total fallback is diagnostic only; execution/execution remains the compatible supplementary attribution. The coloured xW is always the candidate/AmpliCol wall-time multiplier used for selection and as the primary figure of merit. If no compatible supplementary clock exists, only \texttt{(xW)} is shown. No clock is fabricated from another. Every displayed number uses exactly three significant digits. Original-AmpliCol rows beyond its three-open-quark-line scope are catalog static N/A entries; their candidates are shown absolutely and excluded from ratio summaries. Unavailable baselines keep their status; candidates remain absolute. Unverified compiled, eager, or on-the-fly diagnostics are never eligible for best-mode selection or summaries and remain visibly marked unverified. OTF \texttt{([xG]x(G+W))} follows the enclosing contracted cold-start contract. Only exact stored same-point links enter ratios. Summary rows contain multipliers only in min, max, median, average, and weighted-average order; the weighted average is the ratio of timing sums and is the framed bold entry. LC generation is non-union only; LC runtime retains separate non-union and union wall-only lines. Muted generation-row codes (r)|(c)|(e)|(o) identify recurrence JIT O2, compiled JIT O3, eager-DAG JIT O2, and on-the-fly JIT O2 wall-time winners; runtime rows omit them. Summary mode counts use r|c|e|o in the same order.}} +\end{minipage} +\clearpage +\noindent\begin{minipage}{\linewidth} +\subsubsection*{Built-in SM best measured mode versus AmpliCol full-colour (block 2 of 2)} +\begingroup +\centering +\footnotesize +\setlength{\tabcolsep}{2.1pt} +\renewcommand{\arraystretch}{1.00} +\matrixfitwidth{% +\begin{tabular}{@{}rL{1.45in}l>{\matrixentryfont}l>{\matrixentryfont}r@{}>{\matrixentryfont}l>{\matrixentryfont}l>{\matrixentryfont}r@{}>{\matrixentryfont}l>{\matrixentryfont}l>{\matrixentryfont}r@{}>{\matrixentryfont}l@{}} +\toprule +\textbf{ID} & \textbf{base process} & \textbf{metric} & \multicolumn{3}{c}{\textbf{n=4}} & \multicolumn{3}{c}{\textbf{n=5}} & \multicolumn{3}{c}{\textbf{n=6}} \\ +\specialrule{0.85pt}{0pt}{0pt} +\rowcolor{refblue} +\texttt{1} & $d\bar d\to Z+(n-1)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{2} & $u\bar d\to W^++(n-1)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{3} & $d\bar d\to e^+e^-+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{4} & $u\bar d\to e^+\nu_e+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{5} & $d\bar d\to ZZ+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{6} & $gg\to t\bar t+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{7} & $d\bar d\to t\bar t+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{8} & $gg\to gg+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{9} & $d\bar d\to ZZZ+(n-3)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{10} & $d\bar d\to e^+e^-ZH+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{11} & $d\bar d\to t\bar t ZH+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{12} & $d\bar d\to e^+e^-e^+e^-+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{13} & $d\bar d\to u\bar u\,s\bar s+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{14} & $d\bar d\to u\bar u\,s\bar s\,c\bar c+(n-6)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstaticna{ReportMuted} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \matrixstaticna{ReportMuted} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{15} & $d\bar d\to u\bar u\,u\bar u+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\specialrule{1.05pt}{0.22em}{0.18em} +\multicolumn{3}{@{}l}{} & \multicolumn{3}{c}{\cellcolor{refblue}\textbf{n=4}} & \multicolumn{3}{c}{\textbf{n=5}} & \multicolumn{3}{c}{\cellcolor{refblue}\textbf{n=6}} \\ +\multicolumn{3}{@{}l}{\textbf{summary: generation}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} & \multicolumn{3}{c}{\matrixna{ReportMuted}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} \\[0.08em] +\multicolumn{3}{@{}l}{\textbf{summary: wall}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} & \multicolumn{3}{c}{\matrixna{ReportMuted}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} \\ +\bottomrule +\end{tabular} +} +\endgroup +\ReportTableNote{{\scriptsize The candidate is selected independently in each cell and workload by the smallest validated wall time. The upper row is generation in seconds and the lower row is runtime in \(\mu\mathrm{s}/\mathrm{pt}\). In LC cells, each pair is non-union-flow \texttt{|} union-flow. Generation multipliers are coloured directly. Each runtime workload uses \texttt{([xS]xW)}. The muted supplementary xS first compares compatible exposed execution attributions; otherwise it uses authenticated evaluator totals. Because original AmpliCol exposes no evaluator-total clock, a candidate total then uses the AmpliCol direct execution clock, or its wall clock when direct execution is absent, as denominator. An evaluator total is never divided by a recurrence core. This evaluator-total fallback is diagnostic only; execution/execution remains the compatible supplementary attribution. The coloured xW is always the candidate/AmpliCol wall-time multiplier used for selection and as the primary figure of merit. If no compatible supplementary clock exists, only \texttt{(xW)} is shown. No clock is fabricated from another. Every displayed number uses exactly three significant digits. Original-AmpliCol rows beyond its three-open-quark-line scope are catalog static N/A entries; their candidates are shown absolutely and excluded from ratio summaries. Unavailable baselines keep their status; candidates remain absolute. Unverified compiled, eager, or on-the-fly diagnostics are never eligible for best-mode selection or summaries and remain visibly marked unverified. OTF \texttt{([xG]x(G+W))} follows the enclosing contracted cold-start contract. Only exact stored same-point links enter ratios. Summary rows contain multipliers only in min, max, median, average, and weighted-average order; the weighted average is the ratio of timing sums and is the framed bold entry. LC generation is non-union only; LC runtime retains separate non-union and union wall-only lines. Muted generation-row codes (r)|(c)|(e)|(o) identify recurrence JIT O2, compiled JIT O3, eager-DAG JIT O2, and on-the-fly JIT O2 wall-time winners; runtime rows omit them. Summary mode counts use r|c|e|o in the same order.}} +\end{minipage} diff --git a/pyamplicol/src/pyamplicol/_profiling_campaign/result_matrix_best_builtin_sm_lc_table.tex b/pyamplicol/src/pyamplicol/_profiling_campaign/result_matrix_best_builtin_sm_lc_table.tex new file mode 100644 index 0000000000..da9fedbcb0 --- /dev/null +++ b/pyamplicol/src/pyamplicol/_profiling_campaign/result_matrix_best_builtin_sm_lc_table.tex @@ -0,0 +1,296 @@ +% SPDX-License-Identifier: 0BSD +% Generated by tools/performance_report/render.py; do not edit. +\providecommand{\matrixentryfont}{\fontsize{6.8pt}{7.8pt}\selectfont} +\providecommand{\matrixentryfontlc}{\fontsize{6.5pt}{7.5pt}\selectfont} +\providecommand{\matrixsummaryfont}{\fontsize{6.2pt}{7.4pt}\selectfont} +\providecommand{\matrixfitwidth}[1]{\begingroup\sbox0{#1}\ifdim\wd0>\linewidth\resizebox{\linewidth}{!}{\usebox0}\else\makebox[\linewidth][c]{\usebox0}\fi\endgroup} +\providecommand{\matrixpunct}[1]{\textcolor{black}{\texttt{#1}}} +\providecommand{\matrixratio}[2]{\matrixpunct{(}\textcolor{#1}{\texttt{x#2}}\matrixpunct{)}} +\providecommand{\matrixna}[1]{\textcolor{#1}{\texttt{N/A}}} +\providecommand{\matrixnotapplicable}[1]{\textcolor{#1}{\textsc{not applicable}}} +\providecommand{\matrixnotexposed}[1]{\textcolor{#1}{\textsc{not exposed}}} +\providecommand{\matrixstaticna}[1]{\textcolor{#1}{\textsc{static N/A}}} +\providecommand{\matrixtotalevaluator}[1]{\textcolor{ReportBlue}{\texttt{T }}#1} +\providecommand{\matrixrecurrencecore}[1]{\textcolor{ReportOrange}{\texttt{C }}#1} +\providecommand{\matrixzrecurrenceclocks}[2]{\shortstack[r]{#1\\#2}} +\providecommand{\matrixwallclock}[2]{\textcolor{#1}{\texttt{W #2}}} +\providecommand{\matrixwallabsolute}[1]{\textcolor{ReportBlue}{\texttt{W }}#1} +\providecommand{\matrixruntimepair}[2]{\shortstack[l]{#1\\#2}} +\providecommand{\matrixruntimetriple}[3]{\shortstack[l]{#1\\#2\matrixpunct{ | }#3}} +\providecommand{\matrixbelow}[1]{\textcolor{ReportMuted}{\texttt{<#1}}} +\providecommand{\matrixnaratio}[1]{\matrixpunct{(}\matrixna{#1}\matrixpunct{)}} +\providecommand{\matrixstatus}[2]{\textcolor{#1}{\textsc{#2}}} +\providecommand{\matrixncabsolute}[1]{#1} +\providecommand{\matrixsummaryratio}[2]{\textcolor{#1}{\texttt{x#2}}} +\providecommand{\matrixsummaryratiohighlight}[2]{\begingroup\setlength{\fboxsep}{0.9pt}\setlength{\fboxrule}{0.35pt}\fbox{\textcolor{#1}{{\usefont{T1}{lmtt}{b}{n}x#2}}}\endgroup} +\providecommand{\matrixsummarystats}[5]{\begingroup\matrixsummaryfont\begin{tabular}[t]{@{}l@{\hspace{0.014in}\matrixpunct{|}\hspace{0.014in}}l@{\hspace{0.014in}\matrixpunct{|}\hspace{0.014in}}l@{\hspace{0.014in}\matrixpunct{|}\hspace{0.014in}}l@{\hspace{0.014in}\matrixpunct{|}\hspace{0.014in}}l@{}}\makebox[3.6em][l]{#1}&\makebox[4.6em][l]{#2}&\makebox[3.6em][l]{#3}&\makebox[3.6em][l]{#4}&\makebox[4.2em][l]{#5}\end{tabular}\endgroup} +\providecommand{\matrixsummaryworkloads}[2]{\begin{tabular}[t]{@{}l@{}}#1\\[-0.10em]#2\end{tabular}} +\providecommand{\matrixpair}[2]{\begin{tabular}[t]{@{}r@{\hspace{0.025in}\matrixpunct{|}\hspace{0.025in}}r@{}}#1\end{tabular}} +\providecommand{\bestmoderatio}[2]{\textcolor{#1}{\texttt{x#2}}} +\providecommand{\bestmodecompactratio}[3]{\matrixpunct{(}\textcolor{ReportMuted}{\texttt{[x#1]}}\hspace{0.04in}\textcolor{#2}{\texttt{x#3}}\matrixpunct{)}} +\providecommand{\bestmodewallratio}[2]{\matrixpunct{(}\textcolor{#1}{\texttt{x#2}}\matrixpunct{)}} +\providecommand{\bestmodecompactprefix}[1]{\matrixpunct{(}\textcolor{ReportMuted}{\texttt{[x#1]}}\hspace{0.04in}} +\providecommand{\bestmodeopenprefix}{\matrixpunct{(}\hspace{0.04in}} +\providecommand{\bestmodeprimaryratio}[2]{\textcolor{#1}{\texttt{x#2}}\matrixpunct{)}} +\providecommand{\bestmodeabsoluteprefix}[1]{#1\hspace{0.04in}} +\providecommand{\bestmodecompactabsolute}[2]{\textcolor{ReportMuted}{\texttt{[#1]}}\hspace{0.04in}\textcolor{black}{\texttt{#2}}} +\providecommand{\bestmodecompactabsoluteprefix}[1]{\textcolor{ReportMuted}{\texttt{[#1]}}\hspace{0.04in}} +\providecommand{\bestmodeabsoluteprimary}[1]{\textcolor{black}{\texttt{#1}}} +\providecommand{\bestmodeunverifiedclockprefix}[2]{\textcolor{ReportMuted}{\texttt{[}#1\texttt{]}}\hspace{0.04in}#2\hspace{0.04in}} +\providecommand{\bestmodecode}[1]{\textcolor{ReportMuted}{\texttt{(#1)}}} +\providecommand{\bestmodecodeprefix}[1]{\bestmodecode{#1}\hspace{0.025in}} +\providecommand{\bestmodeabsolutechoice}[2]{#1\hspace{0.025in}\bestmodecode{#2}} +\providecommand{\bestmodemix}[1]{\textcolor{ReportBlue}{\texttt{[#1]}}} +\clearpage +\subsection{Built-in SM best measured mode versus AmpliCol: LC} +\noindent\begin{minipage}{\linewidth} +\subsubsection*{Built-in SM best measured mode versus AmpliCol LC (block 1 of 3)} +\begingroup +\centering +\footnotesize +\setlength{\tabcolsep}{2.1pt} +\renewcommand{\arraystretch}{1.00} +\matrixfitwidth{% +\begin{tabular}{@{}rL{1.45in}l>{\matrixentryfontlc}l>{\matrixentryfontlc}c>{\matrixentryfontlc}l>{\matrixentryfontlc}r@{}>{\matrixentryfontlc}l>{\matrixentryfontlc}c>{\matrixentryfontlc}r@{}>{\matrixentryfontlc}l>{\matrixentryfontlc}l>{\matrixentryfontlc}c>{\matrixentryfontlc}l>{\matrixentryfontlc}r@{}>{\matrixentryfontlc}l>{\matrixentryfontlc}c>{\matrixentryfontlc}r@{}>{\matrixentryfontlc}l>{\matrixentryfontlc}l>{\matrixentryfontlc}c>{\matrixentryfontlc}l>{\matrixentryfontlc}r@{}>{\matrixentryfontlc}l>{\matrixentryfontlc}c>{\matrixentryfontlc}r@{}>{\matrixentryfontlc}l@{}} +\toprule +\textbf{ID} & \textbf{base process} & \textbf{metric} & \multicolumn{8}{c}{\textbf{n=1}} & \multicolumn{8}{c}{\textbf{n=2}} & \multicolumn{8}{c}{\textbf{n=3}} \\ +\specialrule{0.85pt}{0pt}{0pt} +\rowcolor{refblue} +\texttt{1} & $d\bar d\to Z+(n-1)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{2} & $u\bar d\to W^++(n-1)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{3} & $d\bar d\to e^+e^-+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{8}{c}{} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{4} & $u\bar d\to e^+\nu_e+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{8}{c}{} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{5} & $d\bar d\to ZZ+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{8}{c}{} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{6} & $gg\to t\bar t+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{8}{c}{} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{7} & $d\bar d\to t\bar t+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{8}{c}{} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{8} & $gg\to gg+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{8}{c}{} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{9} & $d\bar d\to ZZZ+(n-3)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{8}{c}{} & \multicolumn{8}{c}{} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{10} & $d\bar d\to e^+e^-ZH+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{8}{c}{} & \multicolumn{8}{c}{} & \multicolumn{8}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{11} & $d\bar d\to t\bar t ZH+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{8}{c}{} & \multicolumn{8}{c}{} & \multicolumn{8}{c}{} \\ +\addlinespace[0.05em] +\texttt{12} & $d\bar d\to e^+e^-e^+e^-+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{8}{c}{} & \multicolumn{8}{c}{} & \multicolumn{8}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{13} & $d\bar d\to u\bar u\,s\bar s+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{8}{c}{} & \multicolumn{8}{c}{} & \multicolumn{8}{c}{} \\ +\addlinespace[0.05em] +\texttt{14} & $d\bar d\to u\bar u\,s\bar s\,c\bar c+(n-6)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{8}{c}{} & \multicolumn{8}{c}{} & \multicolumn{8}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{15} & $d\bar d\to u\bar u\,u\bar u+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{8}{c}{} & \multicolumn{8}{c}{} & \multicolumn{8}{c}{} \\ +\addlinespace[0.05em] +\specialrule{1.05pt}{0.22em}{0.18em} +\multicolumn{3}{@{}l}{} & \multicolumn{8}{c}{\cellcolor{refblue}\textbf{n=1}} & \multicolumn{8}{c}{\textbf{n=2}} & \multicolumn{8}{c}{\cellcolor{refblue}\textbf{n=3}} \\ +\multicolumn{3}{@{}l}{\textbf{summary: generation}} & \multicolumn{8}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} & \multicolumn{8}{c}{\matrixna{ReportMuted}} & \multicolumn{8}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} \\[0.08em] +\multicolumn{3}{@{}l}{\matrixsummaryworkloads{\textbf{summary: run single-flow, hel. sum}}{\textbf{summary: run all-flows, single hel.}}} & \multicolumn{8}{c}{\cellcolor{refblue}\matrixsummaryworkloads{\matrixna{ReportMuted}}{\matrixna{ReportMuted}}} & \multicolumn{8}{c}{\matrixsummaryworkloads{\matrixna{ReportMuted}}{\matrixna{ReportMuted}}} & \multicolumn{8}{c}{\cellcolor{refblue}\matrixsummaryworkloads{\matrixna{ReportMuted}}{\matrixna{ReportMuted}}} \\ +\bottomrule +\end{tabular} +} +\endgroup +\ReportTableNote{{\scriptsize The candidate is selected independently in each cell and workload by the smallest validated wall time. The upper row is generation in seconds and the lower row is runtime in \(\mu\mathrm{s}/\mathrm{pt}\). In LC cells, each pair is non-union-flow \texttt{|} union-flow. Generation multipliers are coloured directly. Each runtime workload uses \texttt{([xS]xW)}. The muted supplementary xS first compares compatible exposed execution attributions; otherwise it uses authenticated evaluator totals. Because original AmpliCol exposes no evaluator-total clock, a candidate total then uses the AmpliCol direct execution clock, or its wall clock when direct execution is absent, as denominator. An evaluator total is never divided by a recurrence core. This evaluator-total fallback is diagnostic only; execution/execution remains the compatible supplementary attribution. The coloured xW is always the candidate/AmpliCol wall-time multiplier used for selection and as the primary figure of merit. If no compatible supplementary clock exists, only \texttt{(xW)} is shown. No clock is fabricated from another. Every displayed number uses exactly three significant digits. Original-AmpliCol rows beyond its three-open-quark-line scope are catalog static N/A entries; their candidates are shown absolutely and excluded from ratio summaries. Unavailable baselines keep their status; candidates remain absolute. Unverified compiled, eager, or on-the-fly diagnostics are never eligible for best-mode selection or summaries and remain visibly marked unverified. For the LC all-flow workload, AmpliCol direct-evaluation setup and pyAmpliCol process generation have different boundaries, so the candidate generation value is shown absolutely; no misleading multiplier is formed. OTF \texttt{([xG]x(G+W)) | [G]G+W} follows the enclosing cold-start contract. Only exact stored same-point links enter ratios. Summary rows contain multipliers only in min, max, median, average, and weighted-average order; the weighted average is the ratio of timing sums and is the framed bold entry. LC generation is non-union only; LC runtime retains separate non-union and union wall-only lines. Muted generation-row codes (r)|(c)|(e)|(o) identify recurrence JIT O2, compiled JIT O3, eager-DAG JIT O2, and on-the-fly JIT O2 wall-time winners; runtime rows omit them. Summary mode counts use r|c|e|o in the same order.}} +\end{minipage} +\clearpage +\noindent\begin{minipage}{\linewidth} +\subsubsection*{Built-in SM best measured mode versus AmpliCol LC (block 2 of 3)} +\begingroup +\centering +\footnotesize +\setlength{\tabcolsep}{2.1pt} +\renewcommand{\arraystretch}{1.00} +\matrixfitwidth{% +\begin{tabular}{@{}rL{1.45in}l>{\matrixentryfontlc}l>{\matrixentryfontlc}c>{\matrixentryfontlc}l>{\matrixentryfontlc}r@{}>{\matrixentryfontlc}l>{\matrixentryfontlc}c>{\matrixentryfontlc}r@{}>{\matrixentryfontlc}l>{\matrixentryfontlc}l>{\matrixentryfontlc}c>{\matrixentryfontlc}l>{\matrixentryfontlc}r@{}>{\matrixentryfontlc}l>{\matrixentryfontlc}c>{\matrixentryfontlc}r@{}>{\matrixentryfontlc}l>{\matrixentryfontlc}l>{\matrixentryfontlc}c>{\matrixentryfontlc}l>{\matrixentryfontlc}r@{}>{\matrixentryfontlc}l>{\matrixentryfontlc}c>{\matrixentryfontlc}r@{}>{\matrixentryfontlc}l@{}} +\toprule +\textbf{ID} & \textbf{base process} & \textbf{metric} & \multicolumn{8}{c}{\textbf{n=4}} & \multicolumn{8}{c}{\textbf{n=5}} & \multicolumn{8}{c}{\textbf{n=6}} \\ +\specialrule{0.85pt}{0pt}{0pt} +\rowcolor{refblue} +\texttt{1} & $d\bar d\to Z+(n-1)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{2} & $u\bar d\to W^++(n-1)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{3} & $d\bar d\to e^+e^-+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{4} & $u\bar d\to e^+\nu_e+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{5} & $d\bar d\to ZZ+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{6} & $gg\to t\bar t+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{7} & $d\bar d\to t\bar t+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{8} & $gg\to gg+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{9} & $d\bar d\to ZZZ+(n-3)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{10} & $d\bar d\to e^+e^-ZH+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{11} & $d\bar d\to t\bar t ZH+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{12} & $d\bar d\to e^+e^-e^+e^-+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{13} & $d\bar d\to u\bar u\,s\bar s+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{14} & $d\bar d\to u\bar u\,s\bar s\,c\bar c+(n-6)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstaticna{ReportMuted} & \matrixpunct{|} & \matrixstaticna{ReportMuted} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{8}{c}{} & \multicolumn{8}{c}{} & \matrixstaticna{ReportMuted} & \matrixpunct{|} & \matrixstaticna{ReportMuted} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{15} & $d\bar d\to u\bar u\,u\bar u+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\specialrule{1.05pt}{0.22em}{0.18em} +\multicolumn{3}{@{}l}{} & \multicolumn{8}{c}{\cellcolor{refblue}\textbf{n=4}} & \multicolumn{8}{c}{\textbf{n=5}} & \multicolumn{8}{c}{\cellcolor{refblue}\textbf{n=6}} \\ +\multicolumn{3}{@{}l}{\textbf{summary: generation}} & \multicolumn{8}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} & \multicolumn{8}{c}{\matrixna{ReportMuted}} & \multicolumn{8}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} \\[0.08em] +\multicolumn{3}{@{}l}{\matrixsummaryworkloads{\textbf{summary: run single-flow, hel. sum}}{\textbf{summary: run all-flows, single hel.}}} & \multicolumn{8}{c}{\cellcolor{refblue}\matrixsummaryworkloads{\matrixna{ReportMuted}}{\matrixna{ReportMuted}}} & \multicolumn{8}{c}{\matrixsummaryworkloads{\matrixna{ReportMuted}}{\matrixna{ReportMuted}}} & \multicolumn{8}{c}{\cellcolor{refblue}\matrixsummaryworkloads{\matrixna{ReportMuted}}{\matrixna{ReportMuted}}} \\ +\bottomrule +\end{tabular} +} +\endgroup +\ReportTableNote{{\scriptsize The candidate is selected independently in each cell and workload by the smallest validated wall time. The upper row is generation in seconds and the lower row is runtime in \(\mu\mathrm{s}/\mathrm{pt}\). In LC cells, each pair is non-union-flow \texttt{|} union-flow. Generation multipliers are coloured directly. Each runtime workload uses \texttt{([xS]xW)}. The muted supplementary xS first compares compatible exposed execution attributions; otherwise it uses authenticated evaluator totals. Because original AmpliCol exposes no evaluator-total clock, a candidate total then uses the AmpliCol direct execution clock, or its wall clock when direct execution is absent, as denominator. An evaluator total is never divided by a recurrence core. This evaluator-total fallback is diagnostic only; execution/execution remains the compatible supplementary attribution. The coloured xW is always the candidate/AmpliCol wall-time multiplier used for selection and as the primary figure of merit. If no compatible supplementary clock exists, only \texttt{(xW)} is shown. No clock is fabricated from another. Every displayed number uses exactly three significant digits. Original-AmpliCol rows beyond its three-open-quark-line scope are catalog static N/A entries; their candidates are shown absolutely and excluded from ratio summaries. Unavailable baselines keep their status; candidates remain absolute. Unverified compiled, eager, or on-the-fly diagnostics are never eligible for best-mode selection or summaries and remain visibly marked unverified. For the LC all-flow workload, AmpliCol direct-evaluation setup and pyAmpliCol process generation have different boundaries, so the candidate generation value is shown absolutely; no misleading multiplier is formed. OTF \texttt{([xG]x(G+W)) | [G]G+W} follows the enclosing cold-start contract. Only exact stored same-point links enter ratios. Summary rows contain multipliers only in min, max, median, average, and weighted-average order; the weighted average is the ratio of timing sums and is the framed bold entry. LC generation is non-union only; LC runtime retains separate non-union and union wall-only lines. Muted generation-row codes (r)|(c)|(e)|(o) identify recurrence JIT O2, compiled JIT O3, eager-DAG JIT O2, and on-the-fly JIT O2 wall-time winners; runtime rows omit them. Summary mode counts use r|c|e|o in the same order.}} +\end{minipage} +\clearpage +\noindent\begin{minipage}{\linewidth} +\subsubsection*{Built-in SM best measured mode versus AmpliCol LC (block 3 of 3)} +\begingroup +\centering +\footnotesize +\setlength{\tabcolsep}{2.1pt} +\renewcommand{\arraystretch}{1.00} +\matrixfitwidth{% +\begin{tabular}{@{}rL{1.45in}l>{\matrixentryfontlc}l>{\matrixentryfontlc}c>{\matrixentryfontlc}l>{\matrixentryfontlc}r@{}>{\matrixentryfontlc}l>{\matrixentryfontlc}c>{\matrixentryfontlc}r@{}>{\matrixentryfontlc}l>{\matrixentryfontlc}l>{\matrixentryfontlc}c>{\matrixentryfontlc}l>{\matrixentryfontlc}r@{}>{\matrixentryfontlc}l>{\matrixentryfontlc}c>{\matrixentryfontlc}r@{}>{\matrixentryfontlc}l>{\matrixentryfontlc}l>{\matrixentryfontlc}c>{\matrixentryfontlc}l>{\matrixentryfontlc}r@{}>{\matrixentryfontlc}l>{\matrixentryfontlc}c>{\matrixentryfontlc}r@{}>{\matrixentryfontlc}l@{}} +\toprule +\textbf{ID} & \textbf{base process} & \textbf{metric} & \multicolumn{8}{c}{\textbf{n=7}} & \multicolumn{8}{c}{\textbf{n=8}} & \multicolumn{8}{c}{\textbf{n=9}} \\ +\specialrule{0.85pt}{0pt}{0pt} +\rowcolor{refblue} +\texttt{1} & $d\bar d\to Z+(n-1)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{2} & $u\bar d\to W^++(n-1)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{3} & $d\bar d\to e^+e^-+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{4} & $u\bar d\to e^+\nu_e+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{5} & $d\bar d\to ZZ+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{6} & $gg\to t\bar t+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{7} & $d\bar d\to t\bar t+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{8} & $gg\to gg+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{9} & $d\bar d\to ZZZ+(n-3)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{10} & $d\bar d\to e^+e^-ZH+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{11} & $d\bar d\to t\bar t ZH+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{12} & $d\bar d\to e^+e^-e^+e^-+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{13} & $d\bar d\to u\bar u\,s\bar s+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{14} & $d\bar d\to u\bar u\,s\bar s\,c\bar c+(n-6)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstaticna{ReportMuted} & \matrixpunct{|} & \matrixstaticna{ReportMuted} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstaticna{ReportMuted} & \matrixpunct{|} & \matrixstaticna{ReportMuted} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstaticna{ReportMuted} & \matrixpunct{|} & \matrixstaticna{ReportMuted} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstaticna{ReportMuted} & \matrixpunct{|} & \matrixstaticna{ReportMuted} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstaticna{ReportMuted} & \matrixpunct{|} & \matrixstaticna{ReportMuted} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstaticna{ReportMuted} & \matrixpunct{|} & \matrixstaticna{ReportMuted} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{15} & $d\bar d\to u\bar u\,u\bar u+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\specialrule{1.05pt}{0.22em}{0.18em} +\multicolumn{3}{@{}l}{} & \multicolumn{8}{c}{\cellcolor{refblue}\textbf{n=7}} & \multicolumn{8}{c}{\textbf{n=8}} & \multicolumn{8}{c}{\cellcolor{refblue}\textbf{n=9}} \\ +\multicolumn{3}{@{}l}{\textbf{summary: generation}} & \multicolumn{8}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} & \multicolumn{8}{c}{\matrixna{ReportMuted}} & \multicolumn{8}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} \\[0.08em] +\multicolumn{3}{@{}l}{\matrixsummaryworkloads{\textbf{summary: run single-flow, hel. sum}}{\textbf{summary: run all-flows, single hel.}}} & \multicolumn{8}{c}{\cellcolor{refblue}\matrixsummaryworkloads{\matrixna{ReportMuted}}{\matrixna{ReportMuted}}} & \multicolumn{8}{c}{\matrixsummaryworkloads{\matrixna{ReportMuted}}{\matrixna{ReportMuted}}} & \multicolumn{8}{c}{\cellcolor{refblue}\matrixsummaryworkloads{\matrixna{ReportMuted}}{\matrixna{ReportMuted}}} \\ +\bottomrule +\end{tabular} +} +\endgroup +\ReportTableNote{{\scriptsize The candidate is selected independently in each cell and workload by the smallest validated wall time. The upper row is generation in seconds and the lower row is runtime in \(\mu\mathrm{s}/\mathrm{pt}\). In LC cells, each pair is non-union-flow \texttt{|} union-flow. Generation multipliers are coloured directly. Each runtime workload uses \texttt{([xS]xW)}. The muted supplementary xS first compares compatible exposed execution attributions; otherwise it uses authenticated evaluator totals. Because original AmpliCol exposes no evaluator-total clock, a candidate total then uses the AmpliCol direct execution clock, or its wall clock when direct execution is absent, as denominator. An evaluator total is never divided by a recurrence core. This evaluator-total fallback is diagnostic only; execution/execution remains the compatible supplementary attribution. The coloured xW is always the candidate/AmpliCol wall-time multiplier used for selection and as the primary figure of merit. If no compatible supplementary clock exists, only \texttt{(xW)} is shown. No clock is fabricated from another. Every displayed number uses exactly three significant digits. Original-AmpliCol rows beyond its three-open-quark-line scope are catalog static N/A entries; their candidates are shown absolutely and excluded from ratio summaries. Unavailable baselines keep their status; candidates remain absolute. Unverified compiled, eager, or on-the-fly diagnostics are never eligible for best-mode selection or summaries and remain visibly marked unverified. For the LC all-flow workload, AmpliCol direct-evaluation setup and pyAmpliCol process generation have different boundaries, so the candidate generation value is shown absolutely; no misleading multiplier is formed. OTF \texttt{([xG]x(G+W)) | [G]G+W} follows the enclosing cold-start contract. Only exact stored same-point links enter ratios. Summary rows contain multipliers only in min, max, median, average, and weighted-average order; the weighted average is the ratio of timing sums and is the framed bold entry. LC generation is non-union only; LC runtime retains separate non-union and union wall-only lines. Muted generation-row codes (r)|(c)|(e)|(o) identify recurrence JIT O2, compiled JIT O3, eager-DAG JIT O2, and on-the-fly JIT O2 wall-time winners; runtime rows omit them. Summary mode counts use r|c|e|o in the same order.}} +\end{minipage} diff --git a/pyamplicol/src/pyamplicol/_profiling_campaign/result_matrix_best_builtin_sm_nlc_table.tex b/pyamplicol/src/pyamplicol/_profiling_campaign/result_matrix_best_builtin_sm_nlc_table.tex new file mode 100644 index 0000000000..083155fcda --- /dev/null +++ b/pyamplicol/src/pyamplicol/_profiling_campaign/result_matrix_best_builtin_sm_nlc_table.tex @@ -0,0 +1,212 @@ +% SPDX-License-Identifier: 0BSD +% Generated by tools/performance_report/render.py; do not edit. +\providecommand{\matrixentryfont}{\fontsize{6.8pt}{7.8pt}\selectfont} +\providecommand{\matrixentryfontlc}{\fontsize{6.5pt}{7.5pt}\selectfont} +\providecommand{\matrixsummaryfont}{\fontsize{6.2pt}{7.4pt}\selectfont} +\providecommand{\matrixfitwidth}[1]{\begingroup\sbox0{#1}\ifdim\wd0>\linewidth\resizebox{\linewidth}{!}{\usebox0}\else\makebox[\linewidth][c]{\usebox0}\fi\endgroup} +\providecommand{\matrixpunct}[1]{\textcolor{black}{\texttt{#1}}} +\providecommand{\matrixratio}[2]{\matrixpunct{(}\textcolor{#1}{\texttt{x#2}}\matrixpunct{)}} +\providecommand{\matrixna}[1]{\textcolor{#1}{\texttt{N/A}}} +\providecommand{\matrixnotapplicable}[1]{\textcolor{#1}{\textsc{not applicable}}} +\providecommand{\matrixnotexposed}[1]{\textcolor{#1}{\textsc{not exposed}}} +\providecommand{\matrixstaticna}[1]{\textcolor{#1}{\textsc{static N/A}}} +\providecommand{\matrixtotalevaluator}[1]{\textcolor{ReportBlue}{\texttt{T }}#1} +\providecommand{\matrixrecurrencecore}[1]{\textcolor{ReportOrange}{\texttt{C }}#1} +\providecommand{\matrixzrecurrenceclocks}[2]{\shortstack[r]{#1\\#2}} +\providecommand{\matrixwallclock}[2]{\textcolor{#1}{\texttt{W #2}}} +\providecommand{\matrixwallabsolute}[1]{\textcolor{ReportBlue}{\texttt{W }}#1} +\providecommand{\matrixruntimepair}[2]{\shortstack[l]{#1\\#2}} +\providecommand{\matrixruntimetriple}[3]{\shortstack[l]{#1\\#2\matrixpunct{ | }#3}} +\providecommand{\matrixbelow}[1]{\textcolor{ReportMuted}{\texttt{<#1}}} +\providecommand{\matrixnaratio}[1]{\matrixpunct{(}\matrixna{#1}\matrixpunct{)}} +\providecommand{\matrixstatus}[2]{\textcolor{#1}{\textsc{#2}}} +\providecommand{\matrixncabsolute}[1]{#1} +\providecommand{\matrixsummaryratio}[2]{\textcolor{#1}{\texttt{x#2}}} +\providecommand{\matrixsummaryratiohighlight}[2]{\begingroup\setlength{\fboxsep}{0.9pt}\setlength{\fboxrule}{0.35pt}\fbox{\textcolor{#1}{{\usefont{T1}{lmtt}{b}{n}x#2}}}\endgroup} +\providecommand{\matrixsummarystats}[5]{\begingroup\matrixsummaryfont\begin{tabular}[t]{@{}l@{\hspace{0.014in}\matrixpunct{|}\hspace{0.014in}}l@{\hspace{0.014in}\matrixpunct{|}\hspace{0.014in}}l@{\hspace{0.014in}\matrixpunct{|}\hspace{0.014in}}l@{\hspace{0.014in}\matrixpunct{|}\hspace{0.014in}}l@{}}\makebox[3.6em][l]{#1}&\makebox[4.6em][l]{#2}&\makebox[3.6em][l]{#3}&\makebox[3.6em][l]{#4}&\makebox[4.2em][l]{#5}\end{tabular}\endgroup} +\providecommand{\matrixsummaryworkloads}[2]{\begin{tabular}[t]{@{}l@{}}#1\\[-0.10em]#2\end{tabular}} +\providecommand{\matrixpair}[2]{\begin{tabular}[t]{@{}r@{\hspace{0.025in}\matrixpunct{|}\hspace{0.025in}}r@{}}#1\end{tabular}} +\providecommand{\bestmoderatio}[2]{\textcolor{#1}{\texttt{x#2}}} +\providecommand{\bestmodecompactratio}[3]{\matrixpunct{(}\textcolor{ReportMuted}{\texttt{[x#1]}}\hspace{0.04in}\textcolor{#2}{\texttt{x#3}}\matrixpunct{)}} +\providecommand{\bestmodewallratio}[2]{\matrixpunct{(}\textcolor{#1}{\texttt{x#2}}\matrixpunct{)}} +\providecommand{\bestmodecompactprefix}[1]{\matrixpunct{(}\textcolor{ReportMuted}{\texttt{[x#1]}}\hspace{0.04in}} +\providecommand{\bestmodeopenprefix}{\matrixpunct{(}\hspace{0.04in}} +\providecommand{\bestmodeprimaryratio}[2]{\textcolor{#1}{\texttt{x#2}}\matrixpunct{)}} +\providecommand{\bestmodeabsoluteprefix}[1]{#1\hspace{0.04in}} +\providecommand{\bestmodecompactabsolute}[2]{\textcolor{ReportMuted}{\texttt{[#1]}}\hspace{0.04in}\textcolor{black}{\texttt{#2}}} +\providecommand{\bestmodecompactabsoluteprefix}[1]{\textcolor{ReportMuted}{\texttt{[#1]}}\hspace{0.04in}} +\providecommand{\bestmodeabsoluteprimary}[1]{\textcolor{black}{\texttt{#1}}} +\providecommand{\bestmodeunverifiedclockprefix}[2]{\textcolor{ReportMuted}{\texttt{[}#1\texttt{]}}\hspace{0.04in}#2\hspace{0.04in}} +\providecommand{\bestmodecode}[1]{\textcolor{ReportMuted}{\texttt{(#1)}}} +\providecommand{\bestmodecodeprefix}[1]{\bestmodecode{#1}\hspace{0.025in}} +\providecommand{\bestmodeabsolutechoice}[2]{#1\hspace{0.025in}\bestmodecode{#2}} +\providecommand{\bestmodemix}[1]{\textcolor{ReportBlue}{\texttt{[#1]}}} +\clearpage +\subsection{Built-in SM best measured mode versus AmpliCol: NLC} +\noindent\begin{minipage}{\linewidth} +\subsubsection*{Built-in SM best measured mode versus AmpliCol NLC (block 1 of 2)} +\begingroup +\centering +\footnotesize +\setlength{\tabcolsep}{2.1pt} +\renewcommand{\arraystretch}{1.00} +\matrixfitwidth{% +\begin{tabular}{@{}rL{1.45in}l>{\matrixentryfont}l>{\matrixentryfont}r@{}>{\matrixentryfont}l>{\matrixentryfont}l>{\matrixentryfont}r@{}>{\matrixentryfont}l>{\matrixentryfont}l>{\matrixentryfont}r@{}>{\matrixentryfont}l@{}} +\toprule +\textbf{ID} & \textbf{base process} & \textbf{metric} & \multicolumn{3}{c}{\textbf{n=1}} & \multicolumn{3}{c}{\textbf{n=2}} & \multicolumn{3}{c}{\textbf{n=3}} \\ +\specialrule{0.85pt}{0pt}{0pt} +\rowcolor{refblue} +\texttt{1} & $d\bar d\to Z+(n-1)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{2} & $u\bar d\to W^++(n-1)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{3} & $d\bar d\to e^+e^-+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{4} & $u\bar d\to e^+\nu_e+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{5} & $d\bar d\to ZZ+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{6} & $gg\to t\bar t+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{7} & $d\bar d\to t\bar t+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{8} & $gg\to gg+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{9} & $d\bar d\to ZZZ+(n-3)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{10} & $d\bar d\to e^+e^-ZH+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{11} & $d\bar d\to t\bar t ZH+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{12} & $d\bar d\to e^+e^-e^+e^-+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{13} & $d\bar d\to u\bar u\,s\bar s+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{14} & $d\bar d\to u\bar u\,s\bar s\,c\bar c+(n-6)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{15} & $d\bar d\to u\bar u\,u\bar u+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\specialrule{1.05pt}{0.22em}{0.18em} +\multicolumn{3}{@{}l}{} & \multicolumn{3}{c}{\cellcolor{refblue}\textbf{n=1}} & \multicolumn{3}{c}{\textbf{n=2}} & \multicolumn{3}{c}{\cellcolor{refblue}\textbf{n=3}} \\ +\multicolumn{3}{@{}l}{\textbf{summary: generation}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} & \multicolumn{3}{c}{\matrixna{ReportMuted}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} \\[0.08em] +\multicolumn{3}{@{}l}{\textbf{summary: wall}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} & \multicolumn{3}{c}{\matrixna{ReportMuted}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} \\ +\bottomrule +\end{tabular} +} +\endgroup +\ReportTableNote{{\scriptsize The candidate is selected independently in each cell and workload by the smallest validated wall time. The upper row is generation in seconds and the lower row is runtime in \(\mu\mathrm{s}/\mathrm{pt}\). In LC cells, each pair is non-union-flow \texttt{|} union-flow. Generation multipliers are coloured directly. Each runtime workload uses \texttt{([xS]xW)}. The muted supplementary xS first compares compatible exposed execution attributions; otherwise it uses authenticated evaluator totals. Because original AmpliCol exposes no evaluator-total clock, a candidate total then uses the AmpliCol direct execution clock, or its wall clock when direct execution is absent, as denominator. An evaluator total is never divided by a recurrence core. This evaluator-total fallback is diagnostic only; execution/execution remains the compatible supplementary attribution. The coloured xW is always the candidate/AmpliCol wall-time multiplier used for selection and as the primary figure of merit. If no compatible supplementary clock exists, only \texttt{(xW)} is shown. No clock is fabricated from another. Every displayed number uses exactly three significant digits. Original-AmpliCol rows beyond its three-open-quark-line scope are catalog static N/A entries; their candidates are shown absolutely and excluded from ratio summaries. Unavailable baselines keep their status; candidates remain absolute. Unverified compiled, eager, or on-the-fly diagnostics are never eligible for best-mode selection or summaries and remain visibly marked unverified. OTF \texttt{([xG]x(G+W))} follows the enclosing contracted cold-start contract. Only exact stored same-point links enter ratios. Summary rows contain multipliers only in min, max, median, average, and weighted-average order; the weighted average is the ratio of timing sums and is the framed bold entry. LC generation is non-union only; LC runtime retains separate non-union and union wall-only lines. Muted generation-row codes (r)|(c)|(e)|(o) identify recurrence JIT O2, compiled JIT O3, eager-DAG JIT O2, and on-the-fly JIT O2 wall-time winners; runtime rows omit them. Summary mode counts use r|c|e|o in the same order.}} +\end{minipage} +\clearpage +\noindent\begin{minipage}{\linewidth} +\subsubsection*{Built-in SM best measured mode versus AmpliCol NLC (block 2 of 2)} +\begingroup +\centering +\footnotesize +\setlength{\tabcolsep}{2.1pt} +\renewcommand{\arraystretch}{1.00} +\matrixfitwidth{% +\begin{tabular}{@{}rL{1.45in}l>{\matrixentryfont}l>{\matrixentryfont}r@{}>{\matrixentryfont}l>{\matrixentryfont}l>{\matrixentryfont}r@{}>{\matrixentryfont}l>{\matrixentryfont}l>{\matrixentryfont}r@{}>{\matrixentryfont}l@{}} +\toprule +\textbf{ID} & \textbf{base process} & \textbf{metric} & \multicolumn{3}{c}{\textbf{n=4}} & \multicolumn{3}{c}{\textbf{n=5}} & \multicolumn{3}{c}{\textbf{n=6}} \\ +\specialrule{0.85pt}{0pt}{0pt} +\rowcolor{refblue} +\texttt{1} & $d\bar d\to Z+(n-1)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{2} & $u\bar d\to W^++(n-1)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{3} & $d\bar d\to e^+e^-+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{4} & $u\bar d\to e^+\nu_e+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{5} & $d\bar d\to ZZ+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{6} & $gg\to t\bar t+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{7} & $d\bar d\to t\bar t+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{8} & $gg\to gg+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{9} & $d\bar d\to ZZZ+(n-3)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{10} & $d\bar d\to e^+e^-ZH+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{11} & $d\bar d\to t\bar t ZH+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{12} & $d\bar d\to e^+e^-e^+e^-+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{13} & $d\bar d\to u\bar u\,s\bar s+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{14} & $d\bar d\to u\bar u\,s\bar s\,c\bar c+(n-6)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstaticna{ReportMuted} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \matrixstaticna{ReportMuted} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{15} & $d\bar d\to u\bar u\,u\bar u+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\specialrule{1.05pt}{0.22em}{0.18em} +\multicolumn{3}{@{}l}{} & \multicolumn{3}{c}{\cellcolor{refblue}\textbf{n=4}} & \multicolumn{3}{c}{\textbf{n=5}} & \multicolumn{3}{c}{\cellcolor{refblue}\textbf{n=6}} \\ +\multicolumn{3}{@{}l}{\textbf{summary: generation}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} & \multicolumn{3}{c}{\matrixna{ReportMuted}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} \\[0.08em] +\multicolumn{3}{@{}l}{\textbf{summary: wall}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} & \multicolumn{3}{c}{\matrixna{ReportMuted}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} \\ +\bottomrule +\end{tabular} +} +\endgroup +\ReportTableNote{{\scriptsize The candidate is selected independently in each cell and workload by the smallest validated wall time. The upper row is generation in seconds and the lower row is runtime in \(\mu\mathrm{s}/\mathrm{pt}\). In LC cells, each pair is non-union-flow \texttt{|} union-flow. Generation multipliers are coloured directly. Each runtime workload uses \texttt{([xS]xW)}. The muted supplementary xS first compares compatible exposed execution attributions; otherwise it uses authenticated evaluator totals. Because original AmpliCol exposes no evaluator-total clock, a candidate total then uses the AmpliCol direct execution clock, or its wall clock when direct execution is absent, as denominator. An evaluator total is never divided by a recurrence core. This evaluator-total fallback is diagnostic only; execution/execution remains the compatible supplementary attribution. The coloured xW is always the candidate/AmpliCol wall-time multiplier used for selection and as the primary figure of merit. If no compatible supplementary clock exists, only \texttt{(xW)} is shown. No clock is fabricated from another. Every displayed number uses exactly three significant digits. Original-AmpliCol rows beyond its three-open-quark-line scope are catalog static N/A entries; their candidates are shown absolutely and excluded from ratio summaries. Unavailable baselines keep their status; candidates remain absolute. Unverified compiled, eager, or on-the-fly diagnostics are never eligible for best-mode selection or summaries and remain visibly marked unverified. OTF \texttt{([xG]x(G+W))} follows the enclosing contracted cold-start contract. Only exact stored same-point links enter ratios. Summary rows contain multipliers only in min, max, median, average, and weighted-average order; the weighted average is the ratio of timing sums and is the framed bold entry. LC generation is non-union only; LC runtime retains separate non-union and union wall-only lines. Muted generation-row codes (r)|(c)|(e)|(o) identify recurrence JIT O2, compiled JIT O3, eager-DAG JIT O2, and on-the-fly JIT O2 wall-time winners; runtime rows omit them. Summary mode counts use r|c|e|o in the same order.}} +\end{minipage} diff --git a/pyamplicol/src/pyamplicol/_profiling_campaign/result_matrix_compiled_builtin_sm_full_table.tex b/pyamplicol/src/pyamplicol/_profiling_campaign/result_matrix_compiled_builtin_sm_full_table.tex new file mode 100644 index 0000000000..d16f732d53 --- /dev/null +++ b/pyamplicol/src/pyamplicol/_profiling_campaign/result_matrix_compiled_builtin_sm_full_table.tex @@ -0,0 +1,208 @@ +% SPDX-License-Identifier: 0BSD +% Generated by tools/performance_report/render.py; do not edit. +\providecommand{\matrixentryfont}{\fontsize{6.8pt}{7.8pt}\selectfont} +\providecommand{\matrixentryfontlc}{\fontsize{6.5pt}{7.5pt}\selectfont} +\providecommand{\matrixsummaryfont}{\fontsize{6.2pt}{7.4pt}\selectfont} +\providecommand{\matrixfitwidth}[1]{\begingroup\sbox0{#1}\ifdim\wd0>\linewidth\resizebox{\linewidth}{!}{\usebox0}\else\makebox[\linewidth][c]{\usebox0}\fi\endgroup} +\providecommand{\matrixpunct}[1]{\textcolor{black}{\texttt{#1}}} +\providecommand{\matrixratio}[2]{\matrixpunct{(}\textcolor{#1}{\texttt{x#2}}\matrixpunct{)}} +\providecommand{\matrixna}[1]{\textcolor{#1}{\texttt{N/A}}} +\providecommand{\matrixnotapplicable}[1]{\textcolor{#1}{\textsc{not applicable}}} +\providecommand{\matrixnotexposed}[1]{\textcolor{#1}{\textsc{not exposed}}} +\providecommand{\matrixstaticna}[1]{\textcolor{#1}{\textsc{static N/A}}} +\providecommand{\matrixtotalevaluator}[1]{\textcolor{ReportBlue}{\texttt{T }}#1} +\providecommand{\matrixrecurrencecore}[1]{\textcolor{ReportOrange}{\texttt{C }}#1} +\providecommand{\matrixzrecurrenceclocks}[2]{\shortstack[r]{#1\\#2}} +\providecommand{\matrixwallclock}[2]{\textcolor{#1}{\texttt{W #2}}} +\providecommand{\matrixwallabsolute}[1]{\textcolor{ReportBlue}{\texttt{W }}#1} +\providecommand{\matrixruntimepair}[2]{\shortstack[l]{#1\\#2}} +\providecommand{\matrixruntimetriple}[3]{\shortstack[l]{#1\\#2\matrixpunct{ | }#3}} +\providecommand{\matrixbelow}[1]{\textcolor{ReportMuted}{\texttt{<#1}}} +\providecommand{\matrixnaratio}[1]{\matrixpunct{(}\matrixna{#1}\matrixpunct{)}} +\providecommand{\matrixstatus}[2]{\textcolor{#1}{\textsc{#2}}} +\providecommand{\matrixncabsolute}[1]{#1} +\providecommand{\matrixsummaryratio}[2]{\textcolor{#1}{\texttt{x#2}}} +\providecommand{\matrixsummaryratiohighlight}[2]{\begingroup\setlength{\fboxsep}{0.9pt}\setlength{\fboxrule}{0.35pt}\fbox{\textcolor{#1}{{\usefont{T1}{lmtt}{b}{n}x#2}}}\endgroup} +\providecommand{\matrixsummarystats}[5]{\begingroup\matrixsummaryfont\begin{tabular}[t]{@{}l@{\hspace{0.014in}\matrixpunct{|}\hspace{0.014in}}l@{\hspace{0.014in}\matrixpunct{|}\hspace{0.014in}}l@{\hspace{0.014in}\matrixpunct{|}\hspace{0.014in}}l@{\hspace{0.014in}\matrixpunct{|}\hspace{0.014in}}l@{}}\makebox[3.6em][l]{#1}&\makebox[4.6em][l]{#2}&\makebox[3.6em][l]{#3}&\makebox[3.6em][l]{#4}&\makebox[4.2em][l]{#5}\end{tabular}\endgroup} +\providecommand{\matrixsummaryworkloads}[2]{\begin{tabular}[t]{@{}l@{}}#1\\[-0.10em]#2\end{tabular}} +\providecommand{\matrixpair}[2]{\begin{tabular}[t]{@{}r@{\hspace{0.025in}\matrixpunct{|}\hspace{0.025in}}r@{}}#1\end{tabular}} +\providecommand{\bestmoderatio}[2]{\textcolor{#1}{\texttt{x#2}}} +\providecommand{\bestmodecompactratio}[3]{\matrixpunct{(}\textcolor{ReportMuted}{\texttt{[x#1]}}\hspace{0.04in}\textcolor{#2}{\texttt{x#3}}\matrixpunct{)}} +\providecommand{\bestmodewallratio}[2]{\matrixpunct{(}\textcolor{#1}{\texttt{x#2}}\matrixpunct{)}} +\providecommand{\bestmodecompactprefix}[1]{\matrixpunct{(}\textcolor{ReportMuted}{\texttt{[x#1]}}\hspace{0.04in}} +\providecommand{\bestmodeopenprefix}{\matrixpunct{(}\hspace{0.04in}} +\providecommand{\bestmodeprimaryratio}[2]{\textcolor{#1}{\texttt{x#2}}\matrixpunct{)}} +\providecommand{\bestmodeabsoluteprefix}[1]{#1\hspace{0.04in}} +\providecommand{\bestmodecompactabsolute}[2]{\textcolor{ReportMuted}{\texttt{[#1]}}\hspace{0.04in}\textcolor{black}{\texttt{#2}}} +\providecommand{\bestmodecompactabsoluteprefix}[1]{\textcolor{ReportMuted}{\texttt{[#1]}}\hspace{0.04in}} +\providecommand{\bestmodeabsoluteprimary}[1]{\textcolor{black}{\texttt{#1}}} +\providecommand{\bestmodeunverifiedclockprefix}[2]{\textcolor{ReportMuted}{\texttt{[}#1\texttt{]}}\hspace{0.04in}#2\hspace{0.04in}} +\clearpage +\subsection{Built-in SM compiled JIT O3 versus recurrence JIT O2 full-colour} +\noindent\begin{minipage}{\linewidth} +\subsubsection*{Built-in SM compiled JIT O3 versus recurrence JIT O2 full-colour (block 1 of 2)} +\begingroup +\centering +\footnotesize +\setlength{\tabcolsep}{2.1pt} +\renewcommand{\arraystretch}{1.04} +\matrixfitwidth{% +\begin{tabular}{@{}rL{1.45in}l>{\matrixentryfont}l>{\matrixentryfont}r@{}>{\matrixentryfont}l>{\matrixentryfont}l>{\matrixentryfont}r@{}>{\matrixentryfont}l>{\matrixentryfont}l>{\matrixentryfont}r@{}>{\matrixentryfont}l@{}} +\toprule +\textbf{ID} & \textbf{base process} & \textbf{metric} & \multicolumn{3}{c}{\textbf{n=1}} & \multicolumn{3}{c}{\textbf{n=2}} & \multicolumn{3}{c}{\textbf{n=3}} \\ +\specialrule{0.85pt}{0pt}{0pt} +\rowcolor{refblue} +\texttt{1} & $d\bar d\to Z+(n-1)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{2} & $u\bar d\to W^++(n-1)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{3} & $d\bar d\to e^+e^-+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{4} & $u\bar d\to e^+\nu_e+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{5} & $d\bar d\to ZZ+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{6} & $gg\to t\bar t+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{7} & $d\bar d\to t\bar t+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{8} & $gg\to gg+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{9} & $d\bar d\to ZZZ+(n-3)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{10} & $d\bar d\to e^+e^-ZH+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{11} & $d\bar d\to t\bar t ZH+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{12} & $d\bar d\to e^+e^-e^+e^-+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{13} & $d\bar d\to u\bar u\,s\bar s+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{14} & $d\bar d\to u\bar u\,s\bar s\,c\bar c+(n-6)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{15} & $d\bar d\to u\bar u\,u\bar u+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\specialrule{1.05pt}{0.22em}{0.18em} +\multicolumn{3}{@{}l}{} & \multicolumn{3}{c}{\cellcolor{refblue}\textbf{n=1}} & \multicolumn{3}{c}{\textbf{n=2}} & \multicolumn{3}{c}{\cellcolor{refblue}\textbf{n=3}} \\ +\multicolumn{3}{@{}l}{\textbf{summary: generation}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} & \multicolumn{3}{c}{\matrixna{ReportMuted}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} \\[0.08em] +\multicolumn{3}{@{}l}{\textbf{summary: wall}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} & \multicolumn{3}{c}{\matrixna{ReportMuted}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} \\ +\bottomrule +\end{tabular} +} +\endgroup +\ReportTableNote{{\scriptsize Baseline: recurrence JIT O2; candidate: compiled JIT O3. Each cell shows the baseline generation and wall time, followed by candidate/baseline generation and wall-time ratios. Runtime comparisons use ([xS]xW). The muted supplementary xS first compares compatible exposed execution attributions; otherwise it compares authenticated evaluator totals. Only when original AmpliCol has no evaluator-total clock may a candidate evaluator total use the legacy direct execution clock (or its wall clock when direct execution is absent) as denominator. An evaluator total is never divided by a recurrence core. This evaluator-total fallback is diagnostic only; the execution/execution ratio remains the compatible supplementary attribution. The coloured xW is always the candidate/baseline wall multiplier and remains the primary figure of merit. When no compatible supplementary clock exists, only (xW) is shown. No clock is fabricated from another. Every displayed number uses exactly three significant digits. Unverified compiled or eager diagnostics show absolute generation and runtime values with a yellow unverified marker, but never enter ratios or summaries. In their runtime token the muted bracketed value is evaluator-total and the following value is outer wall time. Ratios and summaries require exact stored linkage to the current denominator. Summary rows contain multipliers only in min, max, median, average, and weighted-average order; the weighted average is the ratio of timing sums, is the framed bold entry, and runtime statistics use wall time only. Not applicable marks a multiplicity for which the process family does not exist. Not run marks an otherwise defined process that was intentionally excluded from the declared measurement scope. Fixed-engine tables intentionally omit mode letters because the selected engine is named in the table heading.}} +\end{minipage} +\clearpage +\noindent\begin{minipage}{\linewidth} +\subsubsection*{Built-in SM compiled JIT O3 versus recurrence JIT O2 full-colour (block 2 of 2)} +\begingroup +\centering +\footnotesize +\setlength{\tabcolsep}{2.1pt} +\renewcommand{\arraystretch}{1.04} +\matrixfitwidth{% +\begin{tabular}{@{}rL{1.45in}l>{\matrixentryfont}l>{\matrixentryfont}r@{}>{\matrixentryfont}l>{\matrixentryfont}l>{\matrixentryfont}r@{}>{\matrixentryfont}l>{\matrixentryfont}l>{\matrixentryfont}r@{}>{\matrixentryfont}l@{}} +\toprule +\textbf{ID} & \textbf{base process} & \textbf{metric} & \multicolumn{3}{c}{\textbf{n=4}} & \multicolumn{3}{c}{\textbf{n=5}} & \multicolumn{3}{c}{\textbf{n=6}} \\ +\specialrule{0.85pt}{0pt}{0pt} +\rowcolor{refblue} +\texttt{1} & $d\bar d\to Z+(n-1)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{2} & $u\bar d\to W^++(n-1)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{3} & $d\bar d\to e^+e^-+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{4} & $u\bar d\to e^+\nu_e+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{5} & $d\bar d\to ZZ+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{6} & $gg\to t\bar t+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{7} & $d\bar d\to t\bar t+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{8} & $gg\to gg+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{9} & $d\bar d\to ZZZ+(n-3)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{10} & $d\bar d\to e^+e^-ZH+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{11} & $d\bar d\to t\bar t ZH+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{12} & $d\bar d\to e^+e^-e^+e^-+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{13} & $d\bar d\to u\bar u\,s\bar s+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{14} & $d\bar d\to u\bar u\,s\bar s\,c\bar c+(n-6)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{15} & $d\bar d\to u\bar u\,u\bar u+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\specialrule{1.05pt}{0.22em}{0.18em} +\multicolumn{3}{@{}l}{} & \multicolumn{3}{c}{\cellcolor{refblue}\textbf{n=4}} & \multicolumn{3}{c}{\textbf{n=5}} & \multicolumn{3}{c}{\cellcolor{refblue}\textbf{n=6}} \\ +\multicolumn{3}{@{}l}{\textbf{summary: generation}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} & \multicolumn{3}{c}{\matrixna{ReportMuted}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} \\[0.08em] +\multicolumn{3}{@{}l}{\textbf{summary: wall}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} & \multicolumn{3}{c}{\matrixna{ReportMuted}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} \\ +\bottomrule +\end{tabular} +} +\endgroup +\ReportTableNote{{\scriptsize Baseline: recurrence JIT O2; candidate: compiled JIT O3. Each cell shows the baseline generation and wall time, followed by candidate/baseline generation and wall-time ratios. Runtime comparisons use ([xS]xW). The muted supplementary xS first compares compatible exposed execution attributions; otherwise it compares authenticated evaluator totals. Only when original AmpliCol has no evaluator-total clock may a candidate evaluator total use the legacy direct execution clock (or its wall clock when direct execution is absent) as denominator. An evaluator total is never divided by a recurrence core. This evaluator-total fallback is diagnostic only; the execution/execution ratio remains the compatible supplementary attribution. The coloured xW is always the candidate/baseline wall multiplier and remains the primary figure of merit. When no compatible supplementary clock exists, only (xW) is shown. No clock is fabricated from another. Every displayed number uses exactly three significant digits. Unverified compiled or eager diagnostics show absolute generation and runtime values with a yellow unverified marker, but never enter ratios or summaries. In their runtime token the muted bracketed value is evaluator-total and the following value is outer wall time. Ratios and summaries require exact stored linkage to the current denominator. Summary rows contain multipliers only in min, max, median, average, and weighted-average order; the weighted average is the ratio of timing sums, is the framed bold entry, and runtime statistics use wall time only. Not applicable marks a multiplicity for which the process family does not exist. Not run marks an otherwise defined process that was intentionally excluded from the declared measurement scope. Fixed-engine tables intentionally omit mode letters because the selected engine is named in the table heading.}} +\end{minipage} diff --git a/pyamplicol/src/pyamplicol/_profiling_campaign/result_matrix_compiled_builtin_sm_lc_table.tex b/pyamplicol/src/pyamplicol/_profiling_campaign/result_matrix_compiled_builtin_sm_lc_table.tex new file mode 100644 index 0000000000..732e4545c7 --- /dev/null +++ b/pyamplicol/src/pyamplicol/_profiling_campaign/result_matrix_compiled_builtin_sm_lc_table.tex @@ -0,0 +1,292 @@ +% SPDX-License-Identifier: 0BSD +% Generated by tools/performance_report/render.py; do not edit. +\providecommand{\matrixentryfont}{\fontsize{6.8pt}{7.8pt}\selectfont} +\providecommand{\matrixentryfontlc}{\fontsize{6.5pt}{7.5pt}\selectfont} +\providecommand{\matrixsummaryfont}{\fontsize{6.2pt}{7.4pt}\selectfont} +\providecommand{\matrixfitwidth}[1]{\begingroup\sbox0{#1}\ifdim\wd0>\linewidth\resizebox{\linewidth}{!}{\usebox0}\else\makebox[\linewidth][c]{\usebox0}\fi\endgroup} +\providecommand{\matrixpunct}[1]{\textcolor{black}{\texttt{#1}}} +\providecommand{\matrixratio}[2]{\matrixpunct{(}\textcolor{#1}{\texttt{x#2}}\matrixpunct{)}} +\providecommand{\matrixna}[1]{\textcolor{#1}{\texttt{N/A}}} +\providecommand{\matrixnotapplicable}[1]{\textcolor{#1}{\textsc{not applicable}}} +\providecommand{\matrixnotexposed}[1]{\textcolor{#1}{\textsc{not exposed}}} +\providecommand{\matrixstaticna}[1]{\textcolor{#1}{\textsc{static N/A}}} +\providecommand{\matrixtotalevaluator}[1]{\textcolor{ReportBlue}{\texttt{T }}#1} +\providecommand{\matrixrecurrencecore}[1]{\textcolor{ReportOrange}{\texttt{C }}#1} +\providecommand{\matrixzrecurrenceclocks}[2]{\shortstack[r]{#1\\#2}} +\providecommand{\matrixwallclock}[2]{\textcolor{#1}{\texttt{W #2}}} +\providecommand{\matrixwallabsolute}[1]{\textcolor{ReportBlue}{\texttt{W }}#1} +\providecommand{\matrixruntimepair}[2]{\shortstack[l]{#1\\#2}} +\providecommand{\matrixruntimetriple}[3]{\shortstack[l]{#1\\#2\matrixpunct{ | }#3}} +\providecommand{\matrixbelow}[1]{\textcolor{ReportMuted}{\texttt{<#1}}} +\providecommand{\matrixnaratio}[1]{\matrixpunct{(}\matrixna{#1}\matrixpunct{)}} +\providecommand{\matrixstatus}[2]{\textcolor{#1}{\textsc{#2}}} +\providecommand{\matrixncabsolute}[1]{#1} +\providecommand{\matrixsummaryratio}[2]{\textcolor{#1}{\texttt{x#2}}} +\providecommand{\matrixsummaryratiohighlight}[2]{\begingroup\setlength{\fboxsep}{0.9pt}\setlength{\fboxrule}{0.35pt}\fbox{\textcolor{#1}{{\usefont{T1}{lmtt}{b}{n}x#2}}}\endgroup} +\providecommand{\matrixsummarystats}[5]{\begingroup\matrixsummaryfont\begin{tabular}[t]{@{}l@{\hspace{0.014in}\matrixpunct{|}\hspace{0.014in}}l@{\hspace{0.014in}\matrixpunct{|}\hspace{0.014in}}l@{\hspace{0.014in}\matrixpunct{|}\hspace{0.014in}}l@{\hspace{0.014in}\matrixpunct{|}\hspace{0.014in}}l@{}}\makebox[3.6em][l]{#1}&\makebox[4.6em][l]{#2}&\makebox[3.6em][l]{#3}&\makebox[3.6em][l]{#4}&\makebox[4.2em][l]{#5}\end{tabular}\endgroup} +\providecommand{\matrixsummaryworkloads}[2]{\begin{tabular}[t]{@{}l@{}}#1\\[-0.10em]#2\end{tabular}} +\providecommand{\matrixpair}[2]{\begin{tabular}[t]{@{}r@{\hspace{0.025in}\matrixpunct{|}\hspace{0.025in}}r@{}}#1\end{tabular}} +\providecommand{\bestmoderatio}[2]{\textcolor{#1}{\texttt{x#2}}} +\providecommand{\bestmodecompactratio}[3]{\matrixpunct{(}\textcolor{ReportMuted}{\texttt{[x#1]}}\hspace{0.04in}\textcolor{#2}{\texttt{x#3}}\matrixpunct{)}} +\providecommand{\bestmodewallratio}[2]{\matrixpunct{(}\textcolor{#1}{\texttt{x#2}}\matrixpunct{)}} +\providecommand{\bestmodecompactprefix}[1]{\matrixpunct{(}\textcolor{ReportMuted}{\texttt{[x#1]}}\hspace{0.04in}} +\providecommand{\bestmodeopenprefix}{\matrixpunct{(}\hspace{0.04in}} +\providecommand{\bestmodeprimaryratio}[2]{\textcolor{#1}{\texttt{x#2}}\matrixpunct{)}} +\providecommand{\bestmodeabsoluteprefix}[1]{#1\hspace{0.04in}} +\providecommand{\bestmodecompactabsolute}[2]{\textcolor{ReportMuted}{\texttt{[#1]}}\hspace{0.04in}\textcolor{black}{\texttt{#2}}} +\providecommand{\bestmodecompactabsoluteprefix}[1]{\textcolor{ReportMuted}{\texttt{[#1]}}\hspace{0.04in}} +\providecommand{\bestmodeabsoluteprimary}[1]{\textcolor{black}{\texttt{#1}}} +\providecommand{\bestmodeunverifiedclockprefix}[2]{\textcolor{ReportMuted}{\texttt{[}#1\texttt{]}}\hspace{0.04in}#2\hspace{0.04in}} +\clearpage +\subsection{Built-in SM compiled JIT O3 versus recurrence JIT O2 LC} +\noindent\begin{minipage}{\linewidth} +\subsubsection*{Built-in SM compiled JIT O3 versus recurrence JIT O2 LC (block 1 of 3)} +\begingroup +\centering +\footnotesize +\setlength{\tabcolsep}{2.1pt} +\renewcommand{\arraystretch}{1.04} +\matrixfitwidth{% +\begin{tabular}{@{}rL{1.45in}l>{\matrixentryfontlc}l>{\matrixentryfontlc}c>{\matrixentryfontlc}l>{\matrixentryfontlc}r@{}>{\matrixentryfontlc}l>{\matrixentryfontlc}c>{\matrixentryfontlc}r@{}>{\matrixentryfontlc}l>{\matrixentryfontlc}l>{\matrixentryfontlc}c>{\matrixentryfontlc}l>{\matrixentryfontlc}r@{}>{\matrixentryfontlc}l>{\matrixentryfontlc}c>{\matrixentryfontlc}r@{}>{\matrixentryfontlc}l>{\matrixentryfontlc}l>{\matrixentryfontlc}c>{\matrixentryfontlc}l>{\matrixentryfontlc}r@{}>{\matrixentryfontlc}l>{\matrixentryfontlc}c>{\matrixentryfontlc}r@{}>{\matrixentryfontlc}l@{}} +\toprule +\textbf{ID} & \textbf{base process} & \textbf{metric} & \multicolumn{8}{c}{\textbf{n=1}} & \multicolumn{8}{c}{\textbf{n=2}} & \multicolumn{8}{c}{\textbf{n=3}} \\ +\specialrule{0.85pt}{0pt}{0pt} +\rowcolor{refblue} +\texttt{1} & $d\bar d\to Z+(n-1)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{2} & $u\bar d\to W^++(n-1)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{3} & $d\bar d\to e^+e^-+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{8}{c}{} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{4} & $u\bar d\to e^+\nu_e+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{8}{c}{} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{5} & $d\bar d\to ZZ+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{8}{c}{} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{6} & $gg\to t\bar t+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{8}{c}{} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{7} & $d\bar d\to t\bar t+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{8}{c}{} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{8} & $gg\to gg+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{8}{c}{} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{9} & $d\bar d\to ZZZ+(n-3)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{8}{c}{} & \multicolumn{8}{c}{} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{10} & $d\bar d\to e^+e^-ZH+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{8}{c}{} & \multicolumn{8}{c}{} & \multicolumn{8}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{11} & $d\bar d\to t\bar t ZH+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{8}{c}{} & \multicolumn{8}{c}{} & \multicolumn{8}{c}{} \\ +\addlinespace[0.05em] +\texttt{12} & $d\bar d\to e^+e^-e^+e^-+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{8}{c}{} & \multicolumn{8}{c}{} & \multicolumn{8}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{13} & $d\bar d\to u\bar u\,s\bar s+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{8}{c}{} & \multicolumn{8}{c}{} & \multicolumn{8}{c}{} \\ +\addlinespace[0.05em] +\texttt{14} & $d\bar d\to u\bar u\,s\bar s\,c\bar c+(n-6)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{8}{c}{} & \multicolumn{8}{c}{} & \multicolumn{8}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{15} & $d\bar d\to u\bar u\,u\bar u+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{8}{c}{} & \multicolumn{8}{c}{} & \multicolumn{8}{c}{} \\ +\addlinespace[0.05em] +\specialrule{1.05pt}{0.22em}{0.18em} +\multicolumn{3}{@{}l}{} & \multicolumn{8}{c}{\cellcolor{refblue}\textbf{n=1}} & \multicolumn{8}{c}{\textbf{n=2}} & \multicolumn{8}{c}{\cellcolor{refblue}\textbf{n=3}} \\ +\multicolumn{3}{@{}l}{\textbf{summary: generation}} & \multicolumn{8}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} & \multicolumn{8}{c}{\matrixna{ReportMuted}} & \multicolumn{8}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} \\[0.08em] +\multicolumn{3}{@{}l}{\matrixsummaryworkloads{\textbf{summary: run single-flow, hel. sum}}{\textbf{summary: run all-flows, single hel.}}} & \multicolumn{8}{c}{\cellcolor{refblue}\matrixsummaryworkloads{\matrixna{ReportMuted}}{\matrixna{ReportMuted}}} & \multicolumn{8}{c}{\matrixsummaryworkloads{\matrixna{ReportMuted}}{\matrixna{ReportMuted}}} & \multicolumn{8}{c}{\cellcolor{refblue}\matrixsummaryworkloads{\matrixna{ReportMuted}}{\matrixna{ReportMuted}}} \\ +\bottomrule +\end{tabular} +} +\endgroup +\ReportTableNote{{\scriptsize Baseline: recurrence JIT O2; candidate: compiled JIT O3. Each cell shows the topology-replay/all-flow-union baseline generation times and wall times, followed by layout-matched candidate/baseline generation and wall-time ratios. Runtime comparisons use ([xS]xW). The muted supplementary xS first compares compatible exposed execution attributions; otherwise it compares authenticated evaluator totals. Only when original AmpliCol has no evaluator-total clock may a candidate evaluator total use the legacy direct execution clock (or its wall clock when direct execution is absent) as denominator. An evaluator total is never divided by a recurrence core. This evaluator-total fallback is diagnostic only; the execution/execution ratio remains the compatible supplementary attribution. The coloured xW is always the candidate/baseline wall multiplier and remains the primary figure of merit. When no compatible supplementary clock exists, only (xW) is shown. No clock is fabricated from another. Every displayed number uses exactly three significant digits. Unverified compiled or eager diagnostics show absolute generation and runtime values with a yellow unverified marker, but never enter ratios or summaries. In their runtime token the muted bracketed value is evaluator-total and the following value is outer wall time. Ratios and summaries require exact stored linkage to the current denominator. Summary rows contain multipliers only in min, max, median, average, and weighted-average order; the weighted average is the ratio of timing sums and is the framed bold entry. LC generation uses selected flow only, while LC runtime shows separate selected-flow and all-flow wall-only lines. Not applicable marks a multiplicity for which the process family does not exist. Not run marks an otherwise defined process that was intentionally excluded from the declared measurement scope. Fixed-engine tables intentionally omit mode letters because the selected engine is named in the table heading.}} +\end{minipage} +\clearpage +\noindent\begin{minipage}{\linewidth} +\subsubsection*{Built-in SM compiled JIT O3 versus recurrence JIT O2 LC (block 2 of 3)} +\begingroup +\centering +\footnotesize +\setlength{\tabcolsep}{2.1pt} +\renewcommand{\arraystretch}{1.04} +\matrixfitwidth{% +\begin{tabular}{@{}rL{1.45in}l>{\matrixentryfontlc}l>{\matrixentryfontlc}c>{\matrixentryfontlc}l>{\matrixentryfontlc}r@{}>{\matrixentryfontlc}l>{\matrixentryfontlc}c>{\matrixentryfontlc}r@{}>{\matrixentryfontlc}l>{\matrixentryfontlc}l>{\matrixentryfontlc}c>{\matrixentryfontlc}l>{\matrixentryfontlc}r@{}>{\matrixentryfontlc}l>{\matrixentryfontlc}c>{\matrixentryfontlc}r@{}>{\matrixentryfontlc}l>{\matrixentryfontlc}l>{\matrixentryfontlc}c>{\matrixentryfontlc}l>{\matrixentryfontlc}r@{}>{\matrixentryfontlc}l>{\matrixentryfontlc}c>{\matrixentryfontlc}r@{}>{\matrixentryfontlc}l@{}} +\toprule +\textbf{ID} & \textbf{base process} & \textbf{metric} & \multicolumn{8}{c}{\textbf{n=4}} & \multicolumn{8}{c}{\textbf{n=5}} & \multicolumn{8}{c}{\textbf{n=6}} \\ +\specialrule{0.85pt}{0pt}{0pt} +\rowcolor{refblue} +\texttt{1} & $d\bar d\to Z+(n-1)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{2} & $u\bar d\to W^++(n-1)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{3} & $d\bar d\to e^+e^-+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{4} & $u\bar d\to e^+\nu_e+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{5} & $d\bar d\to ZZ+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{6} & $gg\to t\bar t+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{7} & $d\bar d\to t\bar t+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{8} & $gg\to gg+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{9} & $d\bar d\to ZZZ+(n-3)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{10} & $d\bar d\to e^+e^-ZH+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{11} & $d\bar d\to t\bar t ZH+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{12} & $d\bar d\to e^+e^-e^+e^-+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{13} & $d\bar d\to u\bar u\,s\bar s+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{14} & $d\bar d\to u\bar u\,s\bar s\,c\bar c+(n-6)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{8}{c}{} & \multicolumn{8}{c}{} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{15} & $d\bar d\to u\bar u\,u\bar u+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\specialrule{1.05pt}{0.22em}{0.18em} +\multicolumn{3}{@{}l}{} & \multicolumn{8}{c}{\cellcolor{refblue}\textbf{n=4}} & \multicolumn{8}{c}{\textbf{n=5}} & \multicolumn{8}{c}{\cellcolor{refblue}\textbf{n=6}} \\ +\multicolumn{3}{@{}l}{\textbf{summary: generation}} & \multicolumn{8}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} & \multicolumn{8}{c}{\matrixna{ReportMuted}} & \multicolumn{8}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} \\[0.08em] +\multicolumn{3}{@{}l}{\matrixsummaryworkloads{\textbf{summary: run single-flow, hel. sum}}{\textbf{summary: run all-flows, single hel.}}} & \multicolumn{8}{c}{\cellcolor{refblue}\matrixsummaryworkloads{\matrixna{ReportMuted}}{\matrixna{ReportMuted}}} & \multicolumn{8}{c}{\matrixsummaryworkloads{\matrixna{ReportMuted}}{\matrixna{ReportMuted}}} & \multicolumn{8}{c}{\cellcolor{refblue}\matrixsummaryworkloads{\matrixna{ReportMuted}}{\matrixna{ReportMuted}}} \\ +\bottomrule +\end{tabular} +} +\endgroup +\ReportTableNote{{\scriptsize Baseline: recurrence JIT O2; candidate: compiled JIT O3. Each cell shows the topology-replay/all-flow-union baseline generation times and wall times, followed by layout-matched candidate/baseline generation and wall-time ratios. Runtime comparisons use ([xS]xW). The muted supplementary xS first compares compatible exposed execution attributions; otherwise it compares authenticated evaluator totals. Only when original AmpliCol has no evaluator-total clock may a candidate evaluator total use the legacy direct execution clock (or its wall clock when direct execution is absent) as denominator. An evaluator total is never divided by a recurrence core. This evaluator-total fallback is diagnostic only; the execution/execution ratio remains the compatible supplementary attribution. The coloured xW is always the candidate/baseline wall multiplier and remains the primary figure of merit. When no compatible supplementary clock exists, only (xW) is shown. No clock is fabricated from another. Every displayed number uses exactly three significant digits. Unverified compiled or eager diagnostics show absolute generation and runtime values with a yellow unverified marker, but never enter ratios or summaries. In their runtime token the muted bracketed value is evaluator-total and the following value is outer wall time. Ratios and summaries require exact stored linkage to the current denominator. Summary rows contain multipliers only in min, max, median, average, and weighted-average order; the weighted average is the ratio of timing sums and is the framed bold entry. LC generation uses selected flow only, while LC runtime shows separate selected-flow and all-flow wall-only lines. Not applicable marks a multiplicity for which the process family does not exist. Not run marks an otherwise defined process that was intentionally excluded from the declared measurement scope. Fixed-engine tables intentionally omit mode letters because the selected engine is named in the table heading.}} +\end{minipage} +\clearpage +\noindent\begin{minipage}{\linewidth} +\subsubsection*{Built-in SM compiled JIT O3 versus recurrence JIT O2 LC (block 3 of 3)} +\begingroup +\centering +\footnotesize +\setlength{\tabcolsep}{2.1pt} +\renewcommand{\arraystretch}{1.04} +\matrixfitwidth{% +\begin{tabular}{@{}rL{1.45in}l>{\matrixentryfontlc}l>{\matrixentryfontlc}c>{\matrixentryfontlc}l>{\matrixentryfontlc}r@{}>{\matrixentryfontlc}l>{\matrixentryfontlc}c>{\matrixentryfontlc}r@{}>{\matrixentryfontlc}l>{\matrixentryfontlc}l>{\matrixentryfontlc}c>{\matrixentryfontlc}l>{\matrixentryfontlc}r@{}>{\matrixentryfontlc}l>{\matrixentryfontlc}c>{\matrixentryfontlc}r@{}>{\matrixentryfontlc}l>{\matrixentryfontlc}l>{\matrixentryfontlc}c>{\matrixentryfontlc}l>{\matrixentryfontlc}r@{}>{\matrixentryfontlc}l>{\matrixentryfontlc}c>{\matrixentryfontlc}r@{}>{\matrixentryfontlc}l@{}} +\toprule +\textbf{ID} & \textbf{base process} & \textbf{metric} & \multicolumn{8}{c}{\textbf{n=7}} & \multicolumn{8}{c}{\textbf{n=8}} & \multicolumn{8}{c}{\textbf{n=9}} \\ +\specialrule{0.85pt}{0pt}{0pt} +\rowcolor{refblue} +\texttt{1} & $d\bar d\to Z+(n-1)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{2} & $u\bar d\to W^++(n-1)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{3} & $d\bar d\to e^+e^-+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{4} & $u\bar d\to e^+\nu_e+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{5} & $d\bar d\to ZZ+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{6} & $gg\to t\bar t+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{7} & $d\bar d\to t\bar t+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{8} & $gg\to gg+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{9} & $d\bar d\to ZZZ+(n-3)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{10} & $d\bar d\to e^+e^-ZH+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{11} & $d\bar d\to t\bar t ZH+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{12} & $d\bar d\to e^+e^-e^+e^-+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{13} & $d\bar d\to u\bar u\,s\bar s+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{14} & $d\bar d\to u\bar u\,s\bar s\,c\bar c+(n-6)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{15} & $d\bar d\to u\bar u\,u\bar u+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\specialrule{1.05pt}{0.22em}{0.18em} +\multicolumn{3}{@{}l}{} & \multicolumn{8}{c}{\cellcolor{refblue}\textbf{n=7}} & \multicolumn{8}{c}{\textbf{n=8}} & \multicolumn{8}{c}{\cellcolor{refblue}\textbf{n=9}} \\ +\multicolumn{3}{@{}l}{\textbf{summary: generation}} & \multicolumn{8}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} & \multicolumn{8}{c}{\matrixna{ReportMuted}} & \multicolumn{8}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} \\[0.08em] +\multicolumn{3}{@{}l}{\matrixsummaryworkloads{\textbf{summary: run single-flow, hel. sum}}{\textbf{summary: run all-flows, single hel.}}} & \multicolumn{8}{c}{\cellcolor{refblue}\matrixsummaryworkloads{\matrixna{ReportMuted}}{\matrixna{ReportMuted}}} & \multicolumn{8}{c}{\matrixsummaryworkloads{\matrixna{ReportMuted}}{\matrixna{ReportMuted}}} & \multicolumn{8}{c}{\cellcolor{refblue}\matrixsummaryworkloads{\matrixna{ReportMuted}}{\matrixna{ReportMuted}}} \\ +\bottomrule +\end{tabular} +} +\endgroup +\ReportTableNote{{\scriptsize Baseline: recurrence JIT O2; candidate: compiled JIT O3. Each cell shows the topology-replay/all-flow-union baseline generation times and wall times, followed by layout-matched candidate/baseline generation and wall-time ratios. Runtime comparisons use ([xS]xW). The muted supplementary xS first compares compatible exposed execution attributions; otherwise it compares authenticated evaluator totals. Only when original AmpliCol has no evaluator-total clock may a candidate evaluator total use the legacy direct execution clock (or its wall clock when direct execution is absent) as denominator. An evaluator total is never divided by a recurrence core. This evaluator-total fallback is diagnostic only; the execution/execution ratio remains the compatible supplementary attribution. The coloured xW is always the candidate/baseline wall multiplier and remains the primary figure of merit. When no compatible supplementary clock exists, only (xW) is shown. No clock is fabricated from another. Every displayed number uses exactly three significant digits. Unverified compiled or eager diagnostics show absolute generation and runtime values with a yellow unverified marker, but never enter ratios or summaries. In their runtime token the muted bracketed value is evaluator-total and the following value is outer wall time. Ratios and summaries require exact stored linkage to the current denominator. Summary rows contain multipliers only in min, max, median, average, and weighted-average order; the weighted average is the ratio of timing sums and is the framed bold entry. LC generation uses selected flow only, while LC runtime shows separate selected-flow and all-flow wall-only lines. Not applicable marks a multiplicity for which the process family does not exist. Not run marks an otherwise defined process that was intentionally excluded from the declared measurement scope. Fixed-engine tables intentionally omit mode letters because the selected engine is named in the table heading.}} +\end{minipage} diff --git a/pyamplicol/src/pyamplicol/_profiling_campaign/result_matrix_compiled_builtin_sm_nlc_table.tex b/pyamplicol/src/pyamplicol/_profiling_campaign/result_matrix_compiled_builtin_sm_nlc_table.tex new file mode 100644 index 0000000000..c08b9b2b41 --- /dev/null +++ b/pyamplicol/src/pyamplicol/_profiling_campaign/result_matrix_compiled_builtin_sm_nlc_table.tex @@ -0,0 +1,208 @@ +% SPDX-License-Identifier: 0BSD +% Generated by tools/performance_report/render.py; do not edit. +\providecommand{\matrixentryfont}{\fontsize{6.8pt}{7.8pt}\selectfont} +\providecommand{\matrixentryfontlc}{\fontsize{6.5pt}{7.5pt}\selectfont} +\providecommand{\matrixsummaryfont}{\fontsize{6.2pt}{7.4pt}\selectfont} +\providecommand{\matrixfitwidth}[1]{\begingroup\sbox0{#1}\ifdim\wd0>\linewidth\resizebox{\linewidth}{!}{\usebox0}\else\makebox[\linewidth][c]{\usebox0}\fi\endgroup} +\providecommand{\matrixpunct}[1]{\textcolor{black}{\texttt{#1}}} +\providecommand{\matrixratio}[2]{\matrixpunct{(}\textcolor{#1}{\texttt{x#2}}\matrixpunct{)}} +\providecommand{\matrixna}[1]{\textcolor{#1}{\texttt{N/A}}} +\providecommand{\matrixnotapplicable}[1]{\textcolor{#1}{\textsc{not applicable}}} +\providecommand{\matrixnotexposed}[1]{\textcolor{#1}{\textsc{not exposed}}} +\providecommand{\matrixstaticna}[1]{\textcolor{#1}{\textsc{static N/A}}} +\providecommand{\matrixtotalevaluator}[1]{\textcolor{ReportBlue}{\texttt{T }}#1} +\providecommand{\matrixrecurrencecore}[1]{\textcolor{ReportOrange}{\texttt{C }}#1} +\providecommand{\matrixzrecurrenceclocks}[2]{\shortstack[r]{#1\\#2}} +\providecommand{\matrixwallclock}[2]{\textcolor{#1}{\texttt{W #2}}} +\providecommand{\matrixwallabsolute}[1]{\textcolor{ReportBlue}{\texttt{W }}#1} +\providecommand{\matrixruntimepair}[2]{\shortstack[l]{#1\\#2}} +\providecommand{\matrixruntimetriple}[3]{\shortstack[l]{#1\\#2\matrixpunct{ | }#3}} +\providecommand{\matrixbelow}[1]{\textcolor{ReportMuted}{\texttt{<#1}}} +\providecommand{\matrixnaratio}[1]{\matrixpunct{(}\matrixna{#1}\matrixpunct{)}} +\providecommand{\matrixstatus}[2]{\textcolor{#1}{\textsc{#2}}} +\providecommand{\matrixncabsolute}[1]{#1} +\providecommand{\matrixsummaryratio}[2]{\textcolor{#1}{\texttt{x#2}}} +\providecommand{\matrixsummaryratiohighlight}[2]{\begingroup\setlength{\fboxsep}{0.9pt}\setlength{\fboxrule}{0.35pt}\fbox{\textcolor{#1}{{\usefont{T1}{lmtt}{b}{n}x#2}}}\endgroup} +\providecommand{\matrixsummarystats}[5]{\begingroup\matrixsummaryfont\begin{tabular}[t]{@{}l@{\hspace{0.014in}\matrixpunct{|}\hspace{0.014in}}l@{\hspace{0.014in}\matrixpunct{|}\hspace{0.014in}}l@{\hspace{0.014in}\matrixpunct{|}\hspace{0.014in}}l@{\hspace{0.014in}\matrixpunct{|}\hspace{0.014in}}l@{}}\makebox[3.6em][l]{#1}&\makebox[4.6em][l]{#2}&\makebox[3.6em][l]{#3}&\makebox[3.6em][l]{#4}&\makebox[4.2em][l]{#5}\end{tabular}\endgroup} +\providecommand{\matrixsummaryworkloads}[2]{\begin{tabular}[t]{@{}l@{}}#1\\[-0.10em]#2\end{tabular}} +\providecommand{\matrixpair}[2]{\begin{tabular}[t]{@{}r@{\hspace{0.025in}\matrixpunct{|}\hspace{0.025in}}r@{}}#1\end{tabular}} +\providecommand{\bestmoderatio}[2]{\textcolor{#1}{\texttt{x#2}}} +\providecommand{\bestmodecompactratio}[3]{\matrixpunct{(}\textcolor{ReportMuted}{\texttt{[x#1]}}\hspace{0.04in}\textcolor{#2}{\texttt{x#3}}\matrixpunct{)}} +\providecommand{\bestmodewallratio}[2]{\matrixpunct{(}\textcolor{#1}{\texttt{x#2}}\matrixpunct{)}} +\providecommand{\bestmodecompactprefix}[1]{\matrixpunct{(}\textcolor{ReportMuted}{\texttt{[x#1]}}\hspace{0.04in}} +\providecommand{\bestmodeopenprefix}{\matrixpunct{(}\hspace{0.04in}} +\providecommand{\bestmodeprimaryratio}[2]{\textcolor{#1}{\texttt{x#2}}\matrixpunct{)}} +\providecommand{\bestmodeabsoluteprefix}[1]{#1\hspace{0.04in}} +\providecommand{\bestmodecompactabsolute}[2]{\textcolor{ReportMuted}{\texttt{[#1]}}\hspace{0.04in}\textcolor{black}{\texttt{#2}}} +\providecommand{\bestmodecompactabsoluteprefix}[1]{\textcolor{ReportMuted}{\texttt{[#1]}}\hspace{0.04in}} +\providecommand{\bestmodeabsoluteprimary}[1]{\textcolor{black}{\texttt{#1}}} +\providecommand{\bestmodeunverifiedclockprefix}[2]{\textcolor{ReportMuted}{\texttt{[}#1\texttt{]}}\hspace{0.04in}#2\hspace{0.04in}} +\clearpage +\subsection{Built-in SM compiled JIT O3 versus recurrence JIT O2 NLC} +\noindent\begin{minipage}{\linewidth} +\subsubsection*{Built-in SM compiled JIT O3 versus recurrence JIT O2 NLC (block 1 of 2)} +\begingroup +\centering +\footnotesize +\setlength{\tabcolsep}{2.1pt} +\renewcommand{\arraystretch}{1.04} +\matrixfitwidth{% +\begin{tabular}{@{}rL{1.45in}l>{\matrixentryfont}l>{\matrixentryfont}r@{}>{\matrixentryfont}l>{\matrixentryfont}l>{\matrixentryfont}r@{}>{\matrixentryfont}l>{\matrixentryfont}l>{\matrixentryfont}r@{}>{\matrixentryfont}l@{}} +\toprule +\textbf{ID} & \textbf{base process} & \textbf{metric} & \multicolumn{3}{c}{\textbf{n=1}} & \multicolumn{3}{c}{\textbf{n=2}} & \multicolumn{3}{c}{\textbf{n=3}} \\ +\specialrule{0.85pt}{0pt}{0pt} +\rowcolor{refblue} +\texttt{1} & $d\bar d\to Z+(n-1)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{2} & $u\bar d\to W^++(n-1)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{3} & $d\bar d\to e^+e^-+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{4} & $u\bar d\to e^+\nu_e+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{5} & $d\bar d\to ZZ+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{6} & $gg\to t\bar t+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{7} & $d\bar d\to t\bar t+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{8} & $gg\to gg+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{9} & $d\bar d\to ZZZ+(n-3)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{10} & $d\bar d\to e^+e^-ZH+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{11} & $d\bar d\to t\bar t ZH+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{12} & $d\bar d\to e^+e^-e^+e^-+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{13} & $d\bar d\to u\bar u\,s\bar s+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{14} & $d\bar d\to u\bar u\,s\bar s\,c\bar c+(n-6)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{15} & $d\bar d\to u\bar u\,u\bar u+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\specialrule{1.05pt}{0.22em}{0.18em} +\multicolumn{3}{@{}l}{} & \multicolumn{3}{c}{\cellcolor{refblue}\textbf{n=1}} & \multicolumn{3}{c}{\textbf{n=2}} & \multicolumn{3}{c}{\cellcolor{refblue}\textbf{n=3}} \\ +\multicolumn{3}{@{}l}{\textbf{summary: generation}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} & \multicolumn{3}{c}{\matrixna{ReportMuted}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} \\[0.08em] +\multicolumn{3}{@{}l}{\textbf{summary: wall}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} & \multicolumn{3}{c}{\matrixna{ReportMuted}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} \\ +\bottomrule +\end{tabular} +} +\endgroup +\ReportTableNote{{\scriptsize Baseline: recurrence JIT O2; candidate: compiled JIT O3. Each cell shows the baseline generation and wall time, followed by candidate/baseline generation and wall-time ratios. Runtime comparisons use ([xS]xW). The muted supplementary xS first compares compatible exposed execution attributions; otherwise it compares authenticated evaluator totals. Only when original AmpliCol has no evaluator-total clock may a candidate evaluator total use the legacy direct execution clock (or its wall clock when direct execution is absent) as denominator. An evaluator total is never divided by a recurrence core. This evaluator-total fallback is diagnostic only; the execution/execution ratio remains the compatible supplementary attribution. The coloured xW is always the candidate/baseline wall multiplier and remains the primary figure of merit. When no compatible supplementary clock exists, only (xW) is shown. No clock is fabricated from another. Every displayed number uses exactly three significant digits. Unverified compiled or eager diagnostics show absolute generation and runtime values with a yellow unverified marker, but never enter ratios or summaries. In their runtime token the muted bracketed value is evaluator-total and the following value is outer wall time. Ratios and summaries require exact stored linkage to the current denominator. Summary rows contain multipliers only in min, max, median, average, and weighted-average order; the weighted average is the ratio of timing sums, is the framed bold entry, and runtime statistics use wall time only. Not applicable marks a multiplicity for which the process family does not exist. Not run marks an otherwise defined process that was intentionally excluded from the declared measurement scope. Fixed-engine tables intentionally omit mode letters because the selected engine is named in the table heading.}} +\end{minipage} +\clearpage +\noindent\begin{minipage}{\linewidth} +\subsubsection*{Built-in SM compiled JIT O3 versus recurrence JIT O2 NLC (block 2 of 2)} +\begingroup +\centering +\footnotesize +\setlength{\tabcolsep}{2.1pt} +\renewcommand{\arraystretch}{1.04} +\matrixfitwidth{% +\begin{tabular}{@{}rL{1.45in}l>{\matrixentryfont}l>{\matrixentryfont}r@{}>{\matrixentryfont}l>{\matrixentryfont}l>{\matrixentryfont}r@{}>{\matrixentryfont}l>{\matrixentryfont}l>{\matrixentryfont}r@{}>{\matrixentryfont}l@{}} +\toprule +\textbf{ID} & \textbf{base process} & \textbf{metric} & \multicolumn{3}{c}{\textbf{n=4}} & \multicolumn{3}{c}{\textbf{n=5}} & \multicolumn{3}{c}{\textbf{n=6}} \\ +\specialrule{0.85pt}{0pt}{0pt} +\rowcolor{refblue} +\texttt{1} & $d\bar d\to Z+(n-1)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{2} & $u\bar d\to W^++(n-1)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{3} & $d\bar d\to e^+e^-+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{4} & $u\bar d\to e^+\nu_e+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{5} & $d\bar d\to ZZ+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{6} & $gg\to t\bar t+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{7} & $d\bar d\to t\bar t+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{8} & $gg\to gg+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{9} & $d\bar d\to ZZZ+(n-3)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{10} & $d\bar d\to e^+e^-ZH+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{11} & $d\bar d\to t\bar t ZH+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{12} & $d\bar d\to e^+e^-e^+e^-+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{13} & $d\bar d\to u\bar u\,s\bar s+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{14} & $d\bar d\to u\bar u\,s\bar s\,c\bar c+(n-6)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{15} & $d\bar d\to u\bar u\,u\bar u+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\specialrule{1.05pt}{0.22em}{0.18em} +\multicolumn{3}{@{}l}{} & \multicolumn{3}{c}{\cellcolor{refblue}\textbf{n=4}} & \multicolumn{3}{c}{\textbf{n=5}} & \multicolumn{3}{c}{\cellcolor{refblue}\textbf{n=6}} \\ +\multicolumn{3}{@{}l}{\textbf{summary: generation}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} & \multicolumn{3}{c}{\matrixna{ReportMuted}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} \\[0.08em] +\multicolumn{3}{@{}l}{\textbf{summary: wall}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} & \multicolumn{3}{c}{\matrixna{ReportMuted}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} \\ +\bottomrule +\end{tabular} +} +\endgroup +\ReportTableNote{{\scriptsize Baseline: recurrence JIT O2; candidate: compiled JIT O3. Each cell shows the baseline generation and wall time, followed by candidate/baseline generation and wall-time ratios. Runtime comparisons use ([xS]xW). The muted supplementary xS first compares compatible exposed execution attributions; otherwise it compares authenticated evaluator totals. Only when original AmpliCol has no evaluator-total clock may a candidate evaluator total use the legacy direct execution clock (or its wall clock when direct execution is absent) as denominator. An evaluator total is never divided by a recurrence core. This evaluator-total fallback is diagnostic only; the execution/execution ratio remains the compatible supplementary attribution. The coloured xW is always the candidate/baseline wall multiplier and remains the primary figure of merit. When no compatible supplementary clock exists, only (xW) is shown. No clock is fabricated from another. Every displayed number uses exactly three significant digits. Unverified compiled or eager diagnostics show absolute generation and runtime values with a yellow unverified marker, but never enter ratios or summaries. In their runtime token the muted bracketed value is evaluator-total and the following value is outer wall time. Ratios and summaries require exact stored linkage to the current denominator. Summary rows contain multipliers only in min, max, median, average, and weighted-average order; the weighted average is the ratio of timing sums, is the framed bold entry, and runtime statistics use wall time only. Not applicable marks a multiplicity for which the process family does not exist. Not run marks an otherwise defined process that was intentionally excluded from the declared measurement scope. Fixed-engine tables intentionally omit mode letters because the selected engine is named in the table heading.}} +\end{minipage} diff --git a/pyamplicol/src/pyamplicol/_profiling_campaign/result_matrix_compiled_ufo_sm_full_vs_madgraph_table.tex b/pyamplicol/src/pyamplicol/_profiling_campaign/result_matrix_compiled_ufo_sm_full_vs_madgraph_table.tex new file mode 100644 index 0000000000..fd504db4b3 --- /dev/null +++ b/pyamplicol/src/pyamplicol/_profiling_campaign/result_matrix_compiled_ufo_sm_full_vs_madgraph_table.tex @@ -0,0 +1,208 @@ +% SPDX-License-Identifier: 0BSD +% Generated by tools/performance_report/render.py; do not edit. +\providecommand{\matrixentryfont}{\fontsize{6.8pt}{7.8pt}\selectfont} +\providecommand{\matrixentryfontlc}{\fontsize{6.5pt}{7.5pt}\selectfont} +\providecommand{\matrixsummaryfont}{\fontsize{6.2pt}{7.4pt}\selectfont} +\providecommand{\matrixfitwidth}[1]{\begingroup\sbox0{#1}\ifdim\wd0>\linewidth\resizebox{\linewidth}{!}{\usebox0}\else\makebox[\linewidth][c]{\usebox0}\fi\endgroup} +\providecommand{\matrixpunct}[1]{\textcolor{black}{\texttt{#1}}} +\providecommand{\matrixratio}[2]{\matrixpunct{(}\textcolor{#1}{\texttt{x#2}}\matrixpunct{)}} +\providecommand{\matrixna}[1]{\textcolor{#1}{\texttt{N/A}}} +\providecommand{\matrixnotapplicable}[1]{\textcolor{#1}{\textsc{not applicable}}} +\providecommand{\matrixnotexposed}[1]{\textcolor{#1}{\textsc{not exposed}}} +\providecommand{\matrixstaticna}[1]{\textcolor{#1}{\textsc{static N/A}}} +\providecommand{\matrixtotalevaluator}[1]{\textcolor{ReportBlue}{\texttt{T }}#1} +\providecommand{\matrixrecurrencecore}[1]{\textcolor{ReportOrange}{\texttt{C }}#1} +\providecommand{\matrixzrecurrenceclocks}[2]{\shortstack[r]{#1\\#2}} +\providecommand{\matrixwallclock}[2]{\textcolor{#1}{\texttt{W #2}}} +\providecommand{\matrixwallabsolute}[1]{\textcolor{ReportBlue}{\texttt{W }}#1} +\providecommand{\matrixruntimepair}[2]{\shortstack[l]{#1\\#2}} +\providecommand{\matrixruntimetriple}[3]{\shortstack[l]{#1\\#2\matrixpunct{ | }#3}} +\providecommand{\matrixbelow}[1]{\textcolor{ReportMuted}{\texttt{<#1}}} +\providecommand{\matrixnaratio}[1]{\matrixpunct{(}\matrixna{#1}\matrixpunct{)}} +\providecommand{\matrixstatus}[2]{\textcolor{#1}{\textsc{#2}}} +\providecommand{\matrixncabsolute}[1]{#1} +\providecommand{\matrixsummaryratio}[2]{\textcolor{#1}{\texttt{x#2}}} +\providecommand{\matrixsummaryratiohighlight}[2]{\begingroup\setlength{\fboxsep}{0.9pt}\setlength{\fboxrule}{0.35pt}\fbox{\textcolor{#1}{{\usefont{T1}{lmtt}{b}{n}x#2}}}\endgroup} +\providecommand{\matrixsummarystats}[5]{\begingroup\matrixsummaryfont\begin{tabular}[t]{@{}l@{\hspace{0.014in}\matrixpunct{|}\hspace{0.014in}}l@{\hspace{0.014in}\matrixpunct{|}\hspace{0.014in}}l@{\hspace{0.014in}\matrixpunct{|}\hspace{0.014in}}l@{\hspace{0.014in}\matrixpunct{|}\hspace{0.014in}}l@{}}\makebox[3.6em][l]{#1}&\makebox[4.6em][l]{#2}&\makebox[3.6em][l]{#3}&\makebox[3.6em][l]{#4}&\makebox[4.2em][l]{#5}\end{tabular}\endgroup} +\providecommand{\matrixsummaryworkloads}[2]{\begin{tabular}[t]{@{}l@{}}#1\\[-0.10em]#2\end{tabular}} +\providecommand{\matrixpair}[2]{\begin{tabular}[t]{@{}r@{\hspace{0.025in}\matrixpunct{|}\hspace{0.025in}}r@{}}#1\end{tabular}} +\providecommand{\bestmoderatio}[2]{\textcolor{#1}{\texttt{x#2}}} +\providecommand{\bestmodecompactratio}[3]{\matrixpunct{(}\textcolor{ReportMuted}{\texttt{[x#1]}}\hspace{0.04in}\textcolor{#2}{\texttt{x#3}}\matrixpunct{)}} +\providecommand{\bestmodewallratio}[2]{\matrixpunct{(}\textcolor{#1}{\texttt{x#2}}\matrixpunct{)}} +\providecommand{\bestmodecompactprefix}[1]{\matrixpunct{(}\textcolor{ReportMuted}{\texttt{[x#1]}}\hspace{0.04in}} +\providecommand{\bestmodeopenprefix}{\matrixpunct{(}\hspace{0.04in}} +\providecommand{\bestmodeprimaryratio}[2]{\textcolor{#1}{\texttt{x#2}}\matrixpunct{)}} +\providecommand{\bestmodeabsoluteprefix}[1]{#1\hspace{0.04in}} +\providecommand{\bestmodecompactabsolute}[2]{\textcolor{ReportMuted}{\texttt{[#1]}}\hspace{0.04in}\textcolor{black}{\texttt{#2}}} +\providecommand{\bestmodecompactabsoluteprefix}[1]{\textcolor{ReportMuted}{\texttt{[#1]}}\hspace{0.04in}} +\providecommand{\bestmodeabsoluteprimary}[1]{\textcolor{black}{\texttt{#1}}} +\providecommand{\bestmodeunverifiedclockprefix}[2]{\textcolor{ReportMuted}{\texttt{[}#1\texttt{]}}\hspace{0.04in}#2\hspace{0.04in}} +\clearpage +\subsection{UFO-SM compiled JIT O3 versus MadGraph full-colour} +\noindent\begin{minipage}{\linewidth} +\subsubsection*{UFO-SM compiled JIT O3 versus MadGraph full-colour (block 1 of 2)} +\begingroup +\centering +\footnotesize +\setlength{\tabcolsep}{2.1pt} +\renewcommand{\arraystretch}{1.04} +\matrixfitwidth{% +\begin{tabular}{@{}rL{1.45in}l>{\matrixentryfont}l>{\matrixentryfont}r@{}>{\matrixentryfont}l>{\matrixentryfont}l>{\matrixentryfont}r@{}>{\matrixentryfont}l>{\matrixentryfont}l>{\matrixentryfont}r@{}>{\matrixentryfont}l@{}} +\toprule +\textbf{ID} & \textbf{base process} & \textbf{metric} & \multicolumn{3}{c}{\textbf{n=1}} & \multicolumn{3}{c}{\textbf{n=2}} & \multicolumn{3}{c}{\textbf{n=3}} \\ +\specialrule{0.85pt}{0pt}{0pt} +\rowcolor{refblue} +\texttt{1} & $d\bar d\to Z+(n-1)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{2} & $u\bar d\to W^++(n-1)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{3} & $d\bar d\to e^+e^-+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{4} & $u\bar d\to e^+\nu_e+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{5} & $d\bar d\to ZZ+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{6} & $gg\to t\bar t+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{7} & $d\bar d\to t\bar t+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{8} & $gg\to gg+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{9} & $d\bar d\to ZZZ+(n-3)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{10} & $d\bar d\to e^+e^-ZH+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{11} & $d\bar d\to t\bar t ZH+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{12} & $d\bar d\to e^+e^-e^+e^-+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{13} & $d\bar d\to u\bar u\,s\bar s+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{14} & $d\bar d\to u\bar u\,s\bar s\,c\bar c+(n-6)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{15} & $d\bar d\to u\bar u\,u\bar u+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\specialrule{1.05pt}{0.22em}{0.18em} +\multicolumn{3}{@{}l}{} & \multicolumn{3}{c}{\cellcolor{refblue}\textbf{n=1}} & \multicolumn{3}{c}{\textbf{n=2}} & \multicolumn{3}{c}{\cellcolor{refblue}\textbf{n=3}} \\ +\multicolumn{3}{@{}l}{\textbf{summary: generation}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} & \multicolumn{3}{c}{\matrixna{ReportMuted}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} \\[0.08em] +\multicolumn{3}{@{}l}{\textbf{summary: wall}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} & \multicolumn{3}{c}{\matrixna{ReportMuted}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} \\ +\bottomrule +\end{tabular} +} +\endgroup +\ReportTableNote{{\scriptsize Baseline: MadGraph standalone; candidate: compiled JIT O3. Each cell shows the baseline generation and wall time, followed by candidate/baseline generation and wall-time ratios. Runtime comparisons use ([xS]xW). The muted supplementary xS first compares compatible exposed execution attributions; otherwise it compares authenticated evaluator totals. Only when original AmpliCol has no evaluator-total clock may a candidate evaluator total use the legacy direct execution clock (or its wall clock when direct execution is absent) as denominator. An evaluator total is never divided by a recurrence core. This evaluator-total fallback is diagnostic only; the execution/execution ratio remains the compatible supplementary attribution. The coloured xW is always the candidate/baseline wall multiplier and remains the primary figure of merit. When no compatible supplementary clock exists, only (xW) is shown. No clock is fabricated from another. Every displayed number uses exactly three significant digits. MadGraph standalone is the authoritative full-colour numerical reference; its generation and repeated-evaluation measurements are shared by all four UFO-SM comparison views. Recurrence, compiled, and eager candidates use p200 at the comparison point; the on-the-fly candidate uses its supported p16 lane. Unverified compiled or eager diagnostics show absolute generation and runtime values with a yellow unverified marker, but never enter ratios or summaries. In their runtime token the muted bracketed value is evaluator-total and the following value is outer wall time. Ratios and summaries require exact stored linkage to the current denominator. Summary rows contain multipliers only in min, max, median, average, and weighted-average order; the weighted average is the ratio of timing sums, is the framed bold entry, and runtime statistics use wall time only. Not applicable marks a multiplicity for which the process family does not exist. Not run marks an otherwise defined process that was intentionally excluded from the declared measurement scope. Fixed-engine tables intentionally omit mode letters because the selected engine is named in the table heading.}} +\end{minipage} +\clearpage +\noindent\begin{minipage}{\linewidth} +\subsubsection*{UFO-SM compiled JIT O3 versus MadGraph full-colour (block 2 of 2)} +\begingroup +\centering +\footnotesize +\setlength{\tabcolsep}{2.1pt} +\renewcommand{\arraystretch}{1.04} +\matrixfitwidth{% +\begin{tabular}{@{}rL{1.45in}l>{\matrixentryfont}l>{\matrixentryfont}r@{}>{\matrixentryfont}l>{\matrixentryfont}l>{\matrixentryfont}r@{}>{\matrixentryfont}l>{\matrixentryfont}l>{\matrixentryfont}r@{}>{\matrixentryfont}l@{}} +\toprule +\textbf{ID} & \textbf{base process} & \textbf{metric} & \multicolumn{3}{c}{\textbf{n=4}} & \multicolumn{3}{c}{\textbf{n=5}} & \multicolumn{3}{c}{\textbf{n=6}} \\ +\specialrule{0.85pt}{0pt}{0pt} +\rowcolor{refblue} +\texttt{1} & $d\bar d\to Z+(n-1)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{2} & $u\bar d\to W^++(n-1)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{3} & $d\bar d\to e^+e^-+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{4} & $u\bar d\to e^+\nu_e+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{5} & $d\bar d\to ZZ+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{6} & $gg\to t\bar t+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{7} & $d\bar d\to t\bar t+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{8} & $gg\to gg+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{9} & $d\bar d\to ZZZ+(n-3)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{10} & $d\bar d\to e^+e^-ZH+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{11} & $d\bar d\to t\bar t ZH+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{12} & $d\bar d\to e^+e^-e^+e^-+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{13} & $d\bar d\to u\bar u\,s\bar s+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{14} & $d\bar d\to u\bar u\,s\bar s\,c\bar c+(n-6)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{15} & $d\bar d\to u\bar u\,u\bar u+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\specialrule{1.05pt}{0.22em}{0.18em} +\multicolumn{3}{@{}l}{} & \multicolumn{3}{c}{\cellcolor{refblue}\textbf{n=4}} & \multicolumn{3}{c}{\textbf{n=5}} & \multicolumn{3}{c}{\cellcolor{refblue}\textbf{n=6}} \\ +\multicolumn{3}{@{}l}{\textbf{summary: generation}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} & \multicolumn{3}{c}{\matrixna{ReportMuted}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} \\[0.08em] +\multicolumn{3}{@{}l}{\textbf{summary: wall}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} & \multicolumn{3}{c}{\matrixna{ReportMuted}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} \\ +\bottomrule +\end{tabular} +} +\endgroup +\ReportTableNote{{\scriptsize Baseline: MadGraph standalone; candidate: compiled JIT O3. Each cell shows the baseline generation and wall time, followed by candidate/baseline generation and wall-time ratios. Runtime comparisons use ([xS]xW). The muted supplementary xS first compares compatible exposed execution attributions; otherwise it compares authenticated evaluator totals. Only when original AmpliCol has no evaluator-total clock may a candidate evaluator total use the legacy direct execution clock (or its wall clock when direct execution is absent) as denominator. An evaluator total is never divided by a recurrence core. This evaluator-total fallback is diagnostic only; the execution/execution ratio remains the compatible supplementary attribution. The coloured xW is always the candidate/baseline wall multiplier and remains the primary figure of merit. When no compatible supplementary clock exists, only (xW) is shown. No clock is fabricated from another. Every displayed number uses exactly three significant digits. MadGraph standalone is the authoritative full-colour numerical reference; its generation and repeated-evaluation measurements are shared by all four UFO-SM comparison views. Recurrence, compiled, and eager candidates use p200 at the comparison point; the on-the-fly candidate uses its supported p16 lane. Unverified compiled or eager diagnostics show absolute generation and runtime values with a yellow unverified marker, but never enter ratios or summaries. In their runtime token the muted bracketed value is evaluator-total and the following value is outer wall time. Ratios and summaries require exact stored linkage to the current denominator. Summary rows contain multipliers only in min, max, median, average, and weighted-average order; the weighted average is the ratio of timing sums, is the framed bold entry, and runtime statistics use wall time only. Not applicable marks a multiplicity for which the process family does not exist. Not run marks an otherwise defined process that was intentionally excluded from the declared measurement scope. Fixed-engine tables intentionally omit mode letters because the selected engine is named in the table heading.}} +\end{minipage} diff --git a/pyamplicol/src/pyamplicol/_profiling_campaign/result_matrix_eager_builtin_sm_full_table.tex b/pyamplicol/src/pyamplicol/_profiling_campaign/result_matrix_eager_builtin_sm_full_table.tex new file mode 100644 index 0000000000..a8494ee036 --- /dev/null +++ b/pyamplicol/src/pyamplicol/_profiling_campaign/result_matrix_eager_builtin_sm_full_table.tex @@ -0,0 +1,208 @@ +% SPDX-License-Identifier: 0BSD +% Generated by tools/performance_report/render.py; do not edit. +\providecommand{\matrixentryfont}{\fontsize{6.8pt}{7.8pt}\selectfont} +\providecommand{\matrixentryfontlc}{\fontsize{6.5pt}{7.5pt}\selectfont} +\providecommand{\matrixsummaryfont}{\fontsize{6.2pt}{7.4pt}\selectfont} +\providecommand{\matrixfitwidth}[1]{\begingroup\sbox0{#1}\ifdim\wd0>\linewidth\resizebox{\linewidth}{!}{\usebox0}\else\makebox[\linewidth][c]{\usebox0}\fi\endgroup} +\providecommand{\matrixpunct}[1]{\textcolor{black}{\texttt{#1}}} +\providecommand{\matrixratio}[2]{\matrixpunct{(}\textcolor{#1}{\texttt{x#2}}\matrixpunct{)}} +\providecommand{\matrixna}[1]{\textcolor{#1}{\texttt{N/A}}} +\providecommand{\matrixnotapplicable}[1]{\textcolor{#1}{\textsc{not applicable}}} +\providecommand{\matrixnotexposed}[1]{\textcolor{#1}{\textsc{not exposed}}} +\providecommand{\matrixstaticna}[1]{\textcolor{#1}{\textsc{static N/A}}} +\providecommand{\matrixtotalevaluator}[1]{\textcolor{ReportBlue}{\texttt{T }}#1} +\providecommand{\matrixrecurrencecore}[1]{\textcolor{ReportOrange}{\texttt{C }}#1} +\providecommand{\matrixzrecurrenceclocks}[2]{\shortstack[r]{#1\\#2}} +\providecommand{\matrixwallclock}[2]{\textcolor{#1}{\texttt{W #2}}} +\providecommand{\matrixwallabsolute}[1]{\textcolor{ReportBlue}{\texttt{W }}#1} +\providecommand{\matrixruntimepair}[2]{\shortstack[l]{#1\\#2}} +\providecommand{\matrixruntimetriple}[3]{\shortstack[l]{#1\\#2\matrixpunct{ | }#3}} +\providecommand{\matrixbelow}[1]{\textcolor{ReportMuted}{\texttt{<#1}}} +\providecommand{\matrixnaratio}[1]{\matrixpunct{(}\matrixna{#1}\matrixpunct{)}} +\providecommand{\matrixstatus}[2]{\textcolor{#1}{\textsc{#2}}} +\providecommand{\matrixncabsolute}[1]{#1} +\providecommand{\matrixsummaryratio}[2]{\textcolor{#1}{\texttt{x#2}}} +\providecommand{\matrixsummaryratiohighlight}[2]{\begingroup\setlength{\fboxsep}{0.9pt}\setlength{\fboxrule}{0.35pt}\fbox{\textcolor{#1}{{\usefont{T1}{lmtt}{b}{n}x#2}}}\endgroup} +\providecommand{\matrixsummarystats}[5]{\begingroup\matrixsummaryfont\begin{tabular}[t]{@{}l@{\hspace{0.014in}\matrixpunct{|}\hspace{0.014in}}l@{\hspace{0.014in}\matrixpunct{|}\hspace{0.014in}}l@{\hspace{0.014in}\matrixpunct{|}\hspace{0.014in}}l@{\hspace{0.014in}\matrixpunct{|}\hspace{0.014in}}l@{}}\makebox[3.6em][l]{#1}&\makebox[4.6em][l]{#2}&\makebox[3.6em][l]{#3}&\makebox[3.6em][l]{#4}&\makebox[4.2em][l]{#5}\end{tabular}\endgroup} +\providecommand{\matrixsummaryworkloads}[2]{\begin{tabular}[t]{@{}l@{}}#1\\[-0.10em]#2\end{tabular}} +\providecommand{\matrixpair}[2]{\begin{tabular}[t]{@{}r@{\hspace{0.025in}\matrixpunct{|}\hspace{0.025in}}r@{}}#1\end{tabular}} +\providecommand{\bestmoderatio}[2]{\textcolor{#1}{\texttt{x#2}}} +\providecommand{\bestmodecompactratio}[3]{\matrixpunct{(}\textcolor{ReportMuted}{\texttt{[x#1]}}\hspace{0.04in}\textcolor{#2}{\texttt{x#3}}\matrixpunct{)}} +\providecommand{\bestmodewallratio}[2]{\matrixpunct{(}\textcolor{#1}{\texttt{x#2}}\matrixpunct{)}} +\providecommand{\bestmodecompactprefix}[1]{\matrixpunct{(}\textcolor{ReportMuted}{\texttt{[x#1]}}\hspace{0.04in}} +\providecommand{\bestmodeopenprefix}{\matrixpunct{(}\hspace{0.04in}} +\providecommand{\bestmodeprimaryratio}[2]{\textcolor{#1}{\texttt{x#2}}\matrixpunct{)}} +\providecommand{\bestmodeabsoluteprefix}[1]{#1\hspace{0.04in}} +\providecommand{\bestmodecompactabsolute}[2]{\textcolor{ReportMuted}{\texttt{[#1]}}\hspace{0.04in}\textcolor{black}{\texttt{#2}}} +\providecommand{\bestmodecompactabsoluteprefix}[1]{\textcolor{ReportMuted}{\texttt{[#1]}}\hspace{0.04in}} +\providecommand{\bestmodeabsoluteprimary}[1]{\textcolor{black}{\texttt{#1}}} +\providecommand{\bestmodeunverifiedclockprefix}[2]{\textcolor{ReportMuted}{\texttt{[}#1\texttt{]}}\hspace{0.04in}#2\hspace{0.04in}} +\clearpage +\subsection{Built-in SM eager-DAG JIT O2 versus recurrence JIT O2 full-colour} +\noindent\begin{minipage}{\linewidth} +\subsubsection*{Built-in SM eager-DAG JIT O2 versus recurrence JIT O2 full-colour (block 1 of 2)} +\begingroup +\centering +\footnotesize +\setlength{\tabcolsep}{2.1pt} +\renewcommand{\arraystretch}{1.04} +\matrixfitwidth{% +\begin{tabular}{@{}rL{1.45in}l>{\matrixentryfont}l>{\matrixentryfont}r@{}>{\matrixentryfont}l>{\matrixentryfont}l>{\matrixentryfont}r@{}>{\matrixentryfont}l>{\matrixentryfont}l>{\matrixentryfont}r@{}>{\matrixentryfont}l@{}} +\toprule +\textbf{ID} & \textbf{base process} & \textbf{metric} & \multicolumn{3}{c}{\textbf{n=1}} & \multicolumn{3}{c}{\textbf{n=2}} & \multicolumn{3}{c}{\textbf{n=3}} \\ +\specialrule{0.85pt}{0pt}{0pt} +\rowcolor{refblue} +\texttt{1} & $d\bar d\to Z+(n-1)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{2} & $u\bar d\to W^++(n-1)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{3} & $d\bar d\to e^+e^-+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{4} & $u\bar d\to e^+\nu_e+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{5} & $d\bar d\to ZZ+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{6} & $gg\to t\bar t+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{7} & $d\bar d\to t\bar t+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{8} & $gg\to gg+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{9} & $d\bar d\to ZZZ+(n-3)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{10} & $d\bar d\to e^+e^-ZH+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{11} & $d\bar d\to t\bar t ZH+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{12} & $d\bar d\to e^+e^-e^+e^-+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{13} & $d\bar d\to u\bar u\,s\bar s+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{14} & $d\bar d\to u\bar u\,s\bar s\,c\bar c+(n-6)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{15} & $d\bar d\to u\bar u\,u\bar u+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\specialrule{1.05pt}{0.22em}{0.18em} +\multicolumn{3}{@{}l}{} & \multicolumn{3}{c}{\cellcolor{refblue}\textbf{n=1}} & \multicolumn{3}{c}{\textbf{n=2}} & \multicolumn{3}{c}{\cellcolor{refblue}\textbf{n=3}} \\ +\multicolumn{3}{@{}l}{\textbf{summary: generation}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} & \multicolumn{3}{c}{\matrixna{ReportMuted}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} \\[0.08em] +\multicolumn{3}{@{}l}{\textbf{summary: wall}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} & \multicolumn{3}{c}{\matrixna{ReportMuted}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} \\ +\bottomrule +\end{tabular} +} +\endgroup +\ReportTableNote{{\scriptsize Baseline: recurrence JIT O2; candidate: eager-DAG JIT O2. Each cell shows the baseline generation and wall time, followed by candidate/baseline generation and wall-time ratios. Runtime comparisons use ([xS]xW). The muted supplementary xS first compares compatible exposed execution attributions; otherwise it compares authenticated evaluator totals. Only when original AmpliCol has no evaluator-total clock may a candidate evaluator total use the legacy direct execution clock (or its wall clock when direct execution is absent) as denominator. An evaluator total is never divided by a recurrence core. This evaluator-total fallback is diagnostic only; the execution/execution ratio remains the compatible supplementary attribution. The coloured xW is always the candidate/baseline wall multiplier and remains the primary figure of merit. When no compatible supplementary clock exists, only (xW) is shown. No clock is fabricated from another. Every displayed number uses exactly three significant digits. Unverified compiled or eager diagnostics show absolute generation and runtime values with a yellow unverified marker, but never enter ratios or summaries. In their runtime token the muted bracketed value is evaluator-total and the following value is outer wall time. Ratios and summaries require exact stored linkage to the current denominator. Summary rows contain multipliers only in min, max, median, average, and weighted-average order; the weighted average is the ratio of timing sums, is the framed bold entry, and runtime statistics use wall time only. Not applicable marks a multiplicity for which the process family does not exist. Not run marks an otherwise defined process that was intentionally excluded from the declared measurement scope. Fixed-engine tables intentionally omit mode letters because the selected engine is named in the table heading.}} +\end{minipage} +\clearpage +\noindent\begin{minipage}{\linewidth} +\subsubsection*{Built-in SM eager-DAG JIT O2 versus recurrence JIT O2 full-colour (block 2 of 2)} +\begingroup +\centering +\footnotesize +\setlength{\tabcolsep}{2.1pt} +\renewcommand{\arraystretch}{1.04} +\matrixfitwidth{% +\begin{tabular}{@{}rL{1.45in}l>{\matrixentryfont}l>{\matrixentryfont}r@{}>{\matrixentryfont}l>{\matrixentryfont}l>{\matrixentryfont}r@{}>{\matrixentryfont}l>{\matrixentryfont}l>{\matrixentryfont}r@{}>{\matrixentryfont}l@{}} +\toprule +\textbf{ID} & \textbf{base process} & \textbf{metric} & \multicolumn{3}{c}{\textbf{n=4}} & \multicolumn{3}{c}{\textbf{n=5}} & \multicolumn{3}{c}{\textbf{n=6}} \\ +\specialrule{0.85pt}{0pt}{0pt} +\rowcolor{refblue} +\texttt{1} & $d\bar d\to Z+(n-1)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{2} & $u\bar d\to W^++(n-1)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{3} & $d\bar d\to e^+e^-+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{4} & $u\bar d\to e^+\nu_e+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{5} & $d\bar d\to ZZ+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{6} & $gg\to t\bar t+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{7} & $d\bar d\to t\bar t+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{8} & $gg\to gg+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{9} & $d\bar d\to ZZZ+(n-3)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{10} & $d\bar d\to e^+e^-ZH+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{11} & $d\bar d\to t\bar t ZH+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{12} & $d\bar d\to e^+e^-e^+e^-+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{13} & $d\bar d\to u\bar u\,s\bar s+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{14} & $d\bar d\to u\bar u\,s\bar s\,c\bar c+(n-6)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{15} & $d\bar d\to u\bar u\,u\bar u+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\specialrule{1.05pt}{0.22em}{0.18em} +\multicolumn{3}{@{}l}{} & \multicolumn{3}{c}{\cellcolor{refblue}\textbf{n=4}} & \multicolumn{3}{c}{\textbf{n=5}} & \multicolumn{3}{c}{\cellcolor{refblue}\textbf{n=6}} \\ +\multicolumn{3}{@{}l}{\textbf{summary: generation}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} & \multicolumn{3}{c}{\matrixna{ReportMuted}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} \\[0.08em] +\multicolumn{3}{@{}l}{\textbf{summary: wall}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} & \multicolumn{3}{c}{\matrixna{ReportMuted}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} \\ +\bottomrule +\end{tabular} +} +\endgroup +\ReportTableNote{{\scriptsize Baseline: recurrence JIT O2; candidate: eager-DAG JIT O2. Each cell shows the baseline generation and wall time, followed by candidate/baseline generation and wall-time ratios. Runtime comparisons use ([xS]xW). The muted supplementary xS first compares compatible exposed execution attributions; otherwise it compares authenticated evaluator totals. Only when original AmpliCol has no evaluator-total clock may a candidate evaluator total use the legacy direct execution clock (or its wall clock when direct execution is absent) as denominator. An evaluator total is never divided by a recurrence core. This evaluator-total fallback is diagnostic only; the execution/execution ratio remains the compatible supplementary attribution. The coloured xW is always the candidate/baseline wall multiplier and remains the primary figure of merit. When no compatible supplementary clock exists, only (xW) is shown. No clock is fabricated from another. Every displayed number uses exactly three significant digits. Unverified compiled or eager diagnostics show absolute generation and runtime values with a yellow unverified marker, but never enter ratios or summaries. In their runtime token the muted bracketed value is evaluator-total and the following value is outer wall time. Ratios and summaries require exact stored linkage to the current denominator. Summary rows contain multipliers only in min, max, median, average, and weighted-average order; the weighted average is the ratio of timing sums, is the framed bold entry, and runtime statistics use wall time only. Not applicable marks a multiplicity for which the process family does not exist. Not run marks an otherwise defined process that was intentionally excluded from the declared measurement scope. Fixed-engine tables intentionally omit mode letters because the selected engine is named in the table heading.}} +\end{minipage} diff --git a/pyamplicol/src/pyamplicol/_profiling_campaign/result_matrix_eager_builtin_sm_lc_table.tex b/pyamplicol/src/pyamplicol/_profiling_campaign/result_matrix_eager_builtin_sm_lc_table.tex new file mode 100644 index 0000000000..2002ea40d2 --- /dev/null +++ b/pyamplicol/src/pyamplicol/_profiling_campaign/result_matrix_eager_builtin_sm_lc_table.tex @@ -0,0 +1,292 @@ +% SPDX-License-Identifier: 0BSD +% Generated by tools/performance_report/render.py; do not edit. +\providecommand{\matrixentryfont}{\fontsize{6.8pt}{7.8pt}\selectfont} +\providecommand{\matrixentryfontlc}{\fontsize{6.5pt}{7.5pt}\selectfont} +\providecommand{\matrixsummaryfont}{\fontsize{6.2pt}{7.4pt}\selectfont} +\providecommand{\matrixfitwidth}[1]{\begingroup\sbox0{#1}\ifdim\wd0>\linewidth\resizebox{\linewidth}{!}{\usebox0}\else\makebox[\linewidth][c]{\usebox0}\fi\endgroup} +\providecommand{\matrixpunct}[1]{\textcolor{black}{\texttt{#1}}} +\providecommand{\matrixratio}[2]{\matrixpunct{(}\textcolor{#1}{\texttt{x#2}}\matrixpunct{)}} +\providecommand{\matrixna}[1]{\textcolor{#1}{\texttt{N/A}}} +\providecommand{\matrixnotapplicable}[1]{\textcolor{#1}{\textsc{not applicable}}} +\providecommand{\matrixnotexposed}[1]{\textcolor{#1}{\textsc{not exposed}}} +\providecommand{\matrixstaticna}[1]{\textcolor{#1}{\textsc{static N/A}}} +\providecommand{\matrixtotalevaluator}[1]{\textcolor{ReportBlue}{\texttt{T }}#1} +\providecommand{\matrixrecurrencecore}[1]{\textcolor{ReportOrange}{\texttt{C }}#1} +\providecommand{\matrixzrecurrenceclocks}[2]{\shortstack[r]{#1\\#2}} +\providecommand{\matrixwallclock}[2]{\textcolor{#1}{\texttt{W #2}}} +\providecommand{\matrixwallabsolute}[1]{\textcolor{ReportBlue}{\texttt{W }}#1} +\providecommand{\matrixruntimepair}[2]{\shortstack[l]{#1\\#2}} +\providecommand{\matrixruntimetriple}[3]{\shortstack[l]{#1\\#2\matrixpunct{ | }#3}} +\providecommand{\matrixbelow}[1]{\textcolor{ReportMuted}{\texttt{<#1}}} +\providecommand{\matrixnaratio}[1]{\matrixpunct{(}\matrixna{#1}\matrixpunct{)}} +\providecommand{\matrixstatus}[2]{\textcolor{#1}{\textsc{#2}}} +\providecommand{\matrixncabsolute}[1]{#1} +\providecommand{\matrixsummaryratio}[2]{\textcolor{#1}{\texttt{x#2}}} +\providecommand{\matrixsummaryratiohighlight}[2]{\begingroup\setlength{\fboxsep}{0.9pt}\setlength{\fboxrule}{0.35pt}\fbox{\textcolor{#1}{{\usefont{T1}{lmtt}{b}{n}x#2}}}\endgroup} +\providecommand{\matrixsummarystats}[5]{\begingroup\matrixsummaryfont\begin{tabular}[t]{@{}l@{\hspace{0.014in}\matrixpunct{|}\hspace{0.014in}}l@{\hspace{0.014in}\matrixpunct{|}\hspace{0.014in}}l@{\hspace{0.014in}\matrixpunct{|}\hspace{0.014in}}l@{\hspace{0.014in}\matrixpunct{|}\hspace{0.014in}}l@{}}\makebox[3.6em][l]{#1}&\makebox[4.6em][l]{#2}&\makebox[3.6em][l]{#3}&\makebox[3.6em][l]{#4}&\makebox[4.2em][l]{#5}\end{tabular}\endgroup} +\providecommand{\matrixsummaryworkloads}[2]{\begin{tabular}[t]{@{}l@{}}#1\\[-0.10em]#2\end{tabular}} +\providecommand{\matrixpair}[2]{\begin{tabular}[t]{@{}r@{\hspace{0.025in}\matrixpunct{|}\hspace{0.025in}}r@{}}#1\end{tabular}} +\providecommand{\bestmoderatio}[2]{\textcolor{#1}{\texttt{x#2}}} +\providecommand{\bestmodecompactratio}[3]{\matrixpunct{(}\textcolor{ReportMuted}{\texttt{[x#1]}}\hspace{0.04in}\textcolor{#2}{\texttt{x#3}}\matrixpunct{)}} +\providecommand{\bestmodewallratio}[2]{\matrixpunct{(}\textcolor{#1}{\texttt{x#2}}\matrixpunct{)}} +\providecommand{\bestmodecompactprefix}[1]{\matrixpunct{(}\textcolor{ReportMuted}{\texttt{[x#1]}}\hspace{0.04in}} +\providecommand{\bestmodeopenprefix}{\matrixpunct{(}\hspace{0.04in}} +\providecommand{\bestmodeprimaryratio}[2]{\textcolor{#1}{\texttt{x#2}}\matrixpunct{)}} +\providecommand{\bestmodeabsoluteprefix}[1]{#1\hspace{0.04in}} +\providecommand{\bestmodecompactabsolute}[2]{\textcolor{ReportMuted}{\texttt{[#1]}}\hspace{0.04in}\textcolor{black}{\texttt{#2}}} +\providecommand{\bestmodecompactabsoluteprefix}[1]{\textcolor{ReportMuted}{\texttt{[#1]}}\hspace{0.04in}} +\providecommand{\bestmodeabsoluteprimary}[1]{\textcolor{black}{\texttt{#1}}} +\providecommand{\bestmodeunverifiedclockprefix}[2]{\textcolor{ReportMuted}{\texttt{[}#1\texttt{]}}\hspace{0.04in}#2\hspace{0.04in}} +\clearpage +\subsection{Built-in SM eager-DAG JIT O2 versus recurrence JIT O2 LC} +\noindent\begin{minipage}{\linewidth} +\subsubsection*{Built-in SM eager-DAG JIT O2 versus recurrence JIT O2 LC (block 1 of 3)} +\begingroup +\centering +\footnotesize +\setlength{\tabcolsep}{2.1pt} +\renewcommand{\arraystretch}{1.04} +\matrixfitwidth{% +\begin{tabular}{@{}rL{1.45in}l>{\matrixentryfontlc}l>{\matrixentryfontlc}c>{\matrixentryfontlc}l>{\matrixentryfontlc}r@{}>{\matrixentryfontlc}l>{\matrixentryfontlc}c>{\matrixentryfontlc}r@{}>{\matrixentryfontlc}l>{\matrixentryfontlc}l>{\matrixentryfontlc}c>{\matrixentryfontlc}l>{\matrixentryfontlc}r@{}>{\matrixentryfontlc}l>{\matrixentryfontlc}c>{\matrixentryfontlc}r@{}>{\matrixentryfontlc}l>{\matrixentryfontlc}l>{\matrixentryfontlc}c>{\matrixentryfontlc}l>{\matrixentryfontlc}r@{}>{\matrixentryfontlc}l>{\matrixentryfontlc}c>{\matrixentryfontlc}r@{}>{\matrixentryfontlc}l@{}} +\toprule +\textbf{ID} & \textbf{base process} & \textbf{metric} & \multicolumn{8}{c}{\textbf{n=1}} & \multicolumn{8}{c}{\textbf{n=2}} & \multicolumn{8}{c}{\textbf{n=3}} \\ +\specialrule{0.85pt}{0pt}{0pt} +\rowcolor{refblue} +\texttt{1} & $d\bar d\to Z+(n-1)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{2} & $u\bar d\to W^++(n-1)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{3} & $d\bar d\to e^+e^-+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{8}{c}{} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{4} & $u\bar d\to e^+\nu_e+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{8}{c}{} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{5} & $d\bar d\to ZZ+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{8}{c}{} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{6} & $gg\to t\bar t+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{8}{c}{} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{7} & $d\bar d\to t\bar t+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{8}{c}{} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{8} & $gg\to gg+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{8}{c}{} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{9} & $d\bar d\to ZZZ+(n-3)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{8}{c}{} & \multicolumn{8}{c}{} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{10} & $d\bar d\to e^+e^-ZH+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{8}{c}{} & \multicolumn{8}{c}{} & \multicolumn{8}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{11} & $d\bar d\to t\bar t ZH+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{8}{c}{} & \multicolumn{8}{c}{} & \multicolumn{8}{c}{} \\ +\addlinespace[0.05em] +\texttt{12} & $d\bar d\to e^+e^-e^+e^-+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{8}{c}{} & \multicolumn{8}{c}{} & \multicolumn{8}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{13} & $d\bar d\to u\bar u\,s\bar s+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{8}{c}{} & \multicolumn{8}{c}{} & \multicolumn{8}{c}{} \\ +\addlinespace[0.05em] +\texttt{14} & $d\bar d\to u\bar u\,s\bar s\,c\bar c+(n-6)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{8}{c}{} & \multicolumn{8}{c}{} & \multicolumn{8}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{15} & $d\bar d\to u\bar u\,u\bar u+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{8}{c}{} & \multicolumn{8}{c}{} & \multicolumn{8}{c}{} \\ +\addlinespace[0.05em] +\specialrule{1.05pt}{0.22em}{0.18em} +\multicolumn{3}{@{}l}{} & \multicolumn{8}{c}{\cellcolor{refblue}\textbf{n=1}} & \multicolumn{8}{c}{\textbf{n=2}} & \multicolumn{8}{c}{\cellcolor{refblue}\textbf{n=3}} \\ +\multicolumn{3}{@{}l}{\textbf{summary: generation}} & \multicolumn{8}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} & \multicolumn{8}{c}{\matrixna{ReportMuted}} & \multicolumn{8}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} \\[0.08em] +\multicolumn{3}{@{}l}{\matrixsummaryworkloads{\textbf{summary: run single-flow, hel. sum}}{\textbf{summary: run all-flows, single hel.}}} & \multicolumn{8}{c}{\cellcolor{refblue}\matrixsummaryworkloads{\matrixna{ReportMuted}}{\matrixna{ReportMuted}}} & \multicolumn{8}{c}{\matrixsummaryworkloads{\matrixna{ReportMuted}}{\matrixna{ReportMuted}}} & \multicolumn{8}{c}{\cellcolor{refblue}\matrixsummaryworkloads{\matrixna{ReportMuted}}{\matrixna{ReportMuted}}} \\ +\bottomrule +\end{tabular} +} +\endgroup +\ReportTableNote{{\scriptsize Baseline: recurrence JIT O2; candidate: eager-DAG JIT O2. Each cell shows the topology-replay/all-flow-union baseline generation times and wall times, followed by layout-matched candidate/baseline generation and wall-time ratios. Runtime comparisons use ([xS]xW). The muted supplementary xS first compares compatible exposed execution attributions; otherwise it compares authenticated evaluator totals. Only when original AmpliCol has no evaluator-total clock may a candidate evaluator total use the legacy direct execution clock (or its wall clock when direct execution is absent) as denominator. An evaluator total is never divided by a recurrence core. This evaluator-total fallback is diagnostic only; the execution/execution ratio remains the compatible supplementary attribution. The coloured xW is always the candidate/baseline wall multiplier and remains the primary figure of merit. When no compatible supplementary clock exists, only (xW) is shown. No clock is fabricated from another. Every displayed number uses exactly three significant digits. Unverified compiled or eager diagnostics show absolute generation and runtime values with a yellow unverified marker, but never enter ratios or summaries. In their runtime token the muted bracketed value is evaluator-total and the following value is outer wall time. Ratios and summaries require exact stored linkage to the current denominator. Summary rows contain multipliers only in min, max, median, average, and weighted-average order; the weighted average is the ratio of timing sums and is the framed bold entry. LC generation uses selected flow only, while LC runtime shows separate selected-flow and all-flow wall-only lines. Not applicable marks a multiplicity for which the process family does not exist. Not run marks an otherwise defined process that was intentionally excluded from the declared measurement scope. Fixed-engine tables intentionally omit mode letters because the selected engine is named in the table heading.}} +\end{minipage} +\clearpage +\noindent\begin{minipage}{\linewidth} +\subsubsection*{Built-in SM eager-DAG JIT O2 versus recurrence JIT O2 LC (block 2 of 3)} +\begingroup +\centering +\footnotesize +\setlength{\tabcolsep}{2.1pt} +\renewcommand{\arraystretch}{1.04} +\matrixfitwidth{% +\begin{tabular}{@{}rL{1.45in}l>{\matrixentryfontlc}l>{\matrixentryfontlc}c>{\matrixentryfontlc}l>{\matrixentryfontlc}r@{}>{\matrixentryfontlc}l>{\matrixentryfontlc}c>{\matrixentryfontlc}r@{}>{\matrixentryfontlc}l>{\matrixentryfontlc}l>{\matrixentryfontlc}c>{\matrixentryfontlc}l>{\matrixentryfontlc}r@{}>{\matrixentryfontlc}l>{\matrixentryfontlc}c>{\matrixentryfontlc}r@{}>{\matrixentryfontlc}l>{\matrixentryfontlc}l>{\matrixentryfontlc}c>{\matrixentryfontlc}l>{\matrixentryfontlc}r@{}>{\matrixentryfontlc}l>{\matrixentryfontlc}c>{\matrixentryfontlc}r@{}>{\matrixentryfontlc}l@{}} +\toprule +\textbf{ID} & \textbf{base process} & \textbf{metric} & \multicolumn{8}{c}{\textbf{n=4}} & \multicolumn{8}{c}{\textbf{n=5}} & \multicolumn{8}{c}{\textbf{n=6}} \\ +\specialrule{0.85pt}{0pt}{0pt} +\rowcolor{refblue} +\texttt{1} & $d\bar d\to Z+(n-1)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{2} & $u\bar d\to W^++(n-1)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{3} & $d\bar d\to e^+e^-+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{4} & $u\bar d\to e^+\nu_e+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{5} & $d\bar d\to ZZ+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{6} & $gg\to t\bar t+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{7} & $d\bar d\to t\bar t+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{8} & $gg\to gg+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{9} & $d\bar d\to ZZZ+(n-3)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{10} & $d\bar d\to e^+e^-ZH+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{11} & $d\bar d\to t\bar t ZH+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{12} & $d\bar d\to e^+e^-e^+e^-+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{13} & $d\bar d\to u\bar u\,s\bar s+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{14} & $d\bar d\to u\bar u\,s\bar s\,c\bar c+(n-6)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{8}{c}{} & \multicolumn{8}{c}{} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{15} & $d\bar d\to u\bar u\,u\bar u+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\specialrule{1.05pt}{0.22em}{0.18em} +\multicolumn{3}{@{}l}{} & \multicolumn{8}{c}{\cellcolor{refblue}\textbf{n=4}} & \multicolumn{8}{c}{\textbf{n=5}} & \multicolumn{8}{c}{\cellcolor{refblue}\textbf{n=6}} \\ +\multicolumn{3}{@{}l}{\textbf{summary: generation}} & \multicolumn{8}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} & \multicolumn{8}{c}{\matrixna{ReportMuted}} & \multicolumn{8}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} \\[0.08em] +\multicolumn{3}{@{}l}{\matrixsummaryworkloads{\textbf{summary: run single-flow, hel. sum}}{\textbf{summary: run all-flows, single hel.}}} & \multicolumn{8}{c}{\cellcolor{refblue}\matrixsummaryworkloads{\matrixna{ReportMuted}}{\matrixna{ReportMuted}}} & \multicolumn{8}{c}{\matrixsummaryworkloads{\matrixna{ReportMuted}}{\matrixna{ReportMuted}}} & \multicolumn{8}{c}{\cellcolor{refblue}\matrixsummaryworkloads{\matrixna{ReportMuted}}{\matrixna{ReportMuted}}} \\ +\bottomrule +\end{tabular} +} +\endgroup +\ReportTableNote{{\scriptsize Baseline: recurrence JIT O2; candidate: eager-DAG JIT O2. Each cell shows the topology-replay/all-flow-union baseline generation times and wall times, followed by layout-matched candidate/baseline generation and wall-time ratios. Runtime comparisons use ([xS]xW). The muted supplementary xS first compares compatible exposed execution attributions; otherwise it compares authenticated evaluator totals. Only when original AmpliCol has no evaluator-total clock may a candidate evaluator total use the legacy direct execution clock (or its wall clock when direct execution is absent) as denominator. An evaluator total is never divided by a recurrence core. This evaluator-total fallback is diagnostic only; the execution/execution ratio remains the compatible supplementary attribution. The coloured xW is always the candidate/baseline wall multiplier and remains the primary figure of merit. When no compatible supplementary clock exists, only (xW) is shown. No clock is fabricated from another. Every displayed number uses exactly three significant digits. Unverified compiled or eager diagnostics show absolute generation and runtime values with a yellow unverified marker, but never enter ratios or summaries. In their runtime token the muted bracketed value is evaluator-total and the following value is outer wall time. Ratios and summaries require exact stored linkage to the current denominator. Summary rows contain multipliers only in min, max, median, average, and weighted-average order; the weighted average is the ratio of timing sums and is the framed bold entry. LC generation uses selected flow only, while LC runtime shows separate selected-flow and all-flow wall-only lines. Not applicable marks a multiplicity for which the process family does not exist. Not run marks an otherwise defined process that was intentionally excluded from the declared measurement scope. Fixed-engine tables intentionally omit mode letters because the selected engine is named in the table heading.}} +\end{minipage} +\clearpage +\noindent\begin{minipage}{\linewidth} +\subsubsection*{Built-in SM eager-DAG JIT O2 versus recurrence JIT O2 LC (block 3 of 3)} +\begingroup +\centering +\footnotesize +\setlength{\tabcolsep}{2.1pt} +\renewcommand{\arraystretch}{1.04} +\matrixfitwidth{% +\begin{tabular}{@{}rL{1.45in}l>{\matrixentryfontlc}l>{\matrixentryfontlc}c>{\matrixentryfontlc}l>{\matrixentryfontlc}r@{}>{\matrixentryfontlc}l>{\matrixentryfontlc}c>{\matrixentryfontlc}r@{}>{\matrixentryfontlc}l>{\matrixentryfontlc}l>{\matrixentryfontlc}c>{\matrixentryfontlc}l>{\matrixentryfontlc}r@{}>{\matrixentryfontlc}l>{\matrixentryfontlc}c>{\matrixentryfontlc}r@{}>{\matrixentryfontlc}l>{\matrixentryfontlc}l>{\matrixentryfontlc}c>{\matrixentryfontlc}l>{\matrixentryfontlc}r@{}>{\matrixentryfontlc}l>{\matrixentryfontlc}c>{\matrixentryfontlc}r@{}>{\matrixentryfontlc}l@{}} +\toprule +\textbf{ID} & \textbf{base process} & \textbf{metric} & \multicolumn{8}{c}{\textbf{n=7}} & \multicolumn{8}{c}{\textbf{n=8}} & \multicolumn{8}{c}{\textbf{n=9}} \\ +\specialrule{0.85pt}{0pt}{0pt} +\rowcolor{refblue} +\texttt{1} & $d\bar d\to Z+(n-1)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{2} & $u\bar d\to W^++(n-1)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{3} & $d\bar d\to e^+e^-+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{4} & $u\bar d\to e^+\nu_e+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{5} & $d\bar d\to ZZ+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{6} & $gg\to t\bar t+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{7} & $d\bar d\to t\bar t+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{8} & $gg\to gg+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{9} & $d\bar d\to ZZZ+(n-3)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{10} & $d\bar d\to e^+e^-ZH+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{11} & $d\bar d\to t\bar t ZH+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{12} & $d\bar d\to e^+e^-e^+e^-+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{13} & $d\bar d\to u\bar u\,s\bar s+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{14} & $d\bar d\to u\bar u\,s\bar s\,c\bar c+(n-6)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{15} & $d\bar d\to u\bar u\,u\bar u+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\specialrule{1.05pt}{0.22em}{0.18em} +\multicolumn{3}{@{}l}{} & \multicolumn{8}{c}{\cellcolor{refblue}\textbf{n=7}} & \multicolumn{8}{c}{\textbf{n=8}} & \multicolumn{8}{c}{\cellcolor{refblue}\textbf{n=9}} \\ +\multicolumn{3}{@{}l}{\textbf{summary: generation}} & \multicolumn{8}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} & \multicolumn{8}{c}{\matrixna{ReportMuted}} & \multicolumn{8}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} \\[0.08em] +\multicolumn{3}{@{}l}{\matrixsummaryworkloads{\textbf{summary: run single-flow, hel. sum}}{\textbf{summary: run all-flows, single hel.}}} & \multicolumn{8}{c}{\cellcolor{refblue}\matrixsummaryworkloads{\matrixna{ReportMuted}}{\matrixna{ReportMuted}}} & \multicolumn{8}{c}{\matrixsummaryworkloads{\matrixna{ReportMuted}}{\matrixna{ReportMuted}}} & \multicolumn{8}{c}{\cellcolor{refblue}\matrixsummaryworkloads{\matrixna{ReportMuted}}{\matrixna{ReportMuted}}} \\ +\bottomrule +\end{tabular} +} +\endgroup +\ReportTableNote{{\scriptsize Baseline: recurrence JIT O2; candidate: eager-DAG JIT O2. Each cell shows the topology-replay/all-flow-union baseline generation times and wall times, followed by layout-matched candidate/baseline generation and wall-time ratios. Runtime comparisons use ([xS]xW). The muted supplementary xS first compares compatible exposed execution attributions; otherwise it compares authenticated evaluator totals. Only when original AmpliCol has no evaluator-total clock may a candidate evaluator total use the legacy direct execution clock (or its wall clock when direct execution is absent) as denominator. An evaluator total is never divided by a recurrence core. This evaluator-total fallback is diagnostic only; the execution/execution ratio remains the compatible supplementary attribution. The coloured xW is always the candidate/baseline wall multiplier and remains the primary figure of merit. When no compatible supplementary clock exists, only (xW) is shown. No clock is fabricated from another. Every displayed number uses exactly three significant digits. Unverified compiled or eager diagnostics show absolute generation and runtime values with a yellow unverified marker, but never enter ratios or summaries. In their runtime token the muted bracketed value is evaluator-total and the following value is outer wall time. Ratios and summaries require exact stored linkage to the current denominator. Summary rows contain multipliers only in min, max, median, average, and weighted-average order; the weighted average is the ratio of timing sums and is the framed bold entry. LC generation uses selected flow only, while LC runtime shows separate selected-flow and all-flow wall-only lines. Not applicable marks a multiplicity for which the process family does not exist. Not run marks an otherwise defined process that was intentionally excluded from the declared measurement scope. Fixed-engine tables intentionally omit mode letters because the selected engine is named in the table heading.}} +\end{minipage} diff --git a/pyamplicol/src/pyamplicol/_profiling_campaign/result_matrix_eager_builtin_sm_nlc_table.tex b/pyamplicol/src/pyamplicol/_profiling_campaign/result_matrix_eager_builtin_sm_nlc_table.tex new file mode 100644 index 0000000000..1e0fd55100 --- /dev/null +++ b/pyamplicol/src/pyamplicol/_profiling_campaign/result_matrix_eager_builtin_sm_nlc_table.tex @@ -0,0 +1,208 @@ +% SPDX-License-Identifier: 0BSD +% Generated by tools/performance_report/render.py; do not edit. +\providecommand{\matrixentryfont}{\fontsize{6.8pt}{7.8pt}\selectfont} +\providecommand{\matrixentryfontlc}{\fontsize{6.5pt}{7.5pt}\selectfont} +\providecommand{\matrixsummaryfont}{\fontsize{6.2pt}{7.4pt}\selectfont} +\providecommand{\matrixfitwidth}[1]{\begingroup\sbox0{#1}\ifdim\wd0>\linewidth\resizebox{\linewidth}{!}{\usebox0}\else\makebox[\linewidth][c]{\usebox0}\fi\endgroup} +\providecommand{\matrixpunct}[1]{\textcolor{black}{\texttt{#1}}} +\providecommand{\matrixratio}[2]{\matrixpunct{(}\textcolor{#1}{\texttt{x#2}}\matrixpunct{)}} +\providecommand{\matrixna}[1]{\textcolor{#1}{\texttt{N/A}}} +\providecommand{\matrixnotapplicable}[1]{\textcolor{#1}{\textsc{not applicable}}} +\providecommand{\matrixnotexposed}[1]{\textcolor{#1}{\textsc{not exposed}}} +\providecommand{\matrixstaticna}[1]{\textcolor{#1}{\textsc{static N/A}}} +\providecommand{\matrixtotalevaluator}[1]{\textcolor{ReportBlue}{\texttt{T }}#1} +\providecommand{\matrixrecurrencecore}[1]{\textcolor{ReportOrange}{\texttt{C }}#1} +\providecommand{\matrixzrecurrenceclocks}[2]{\shortstack[r]{#1\\#2}} +\providecommand{\matrixwallclock}[2]{\textcolor{#1}{\texttt{W #2}}} +\providecommand{\matrixwallabsolute}[1]{\textcolor{ReportBlue}{\texttt{W }}#1} +\providecommand{\matrixruntimepair}[2]{\shortstack[l]{#1\\#2}} +\providecommand{\matrixruntimetriple}[3]{\shortstack[l]{#1\\#2\matrixpunct{ | }#3}} +\providecommand{\matrixbelow}[1]{\textcolor{ReportMuted}{\texttt{<#1}}} +\providecommand{\matrixnaratio}[1]{\matrixpunct{(}\matrixna{#1}\matrixpunct{)}} +\providecommand{\matrixstatus}[2]{\textcolor{#1}{\textsc{#2}}} +\providecommand{\matrixncabsolute}[1]{#1} +\providecommand{\matrixsummaryratio}[2]{\textcolor{#1}{\texttt{x#2}}} +\providecommand{\matrixsummaryratiohighlight}[2]{\begingroup\setlength{\fboxsep}{0.9pt}\setlength{\fboxrule}{0.35pt}\fbox{\textcolor{#1}{{\usefont{T1}{lmtt}{b}{n}x#2}}}\endgroup} +\providecommand{\matrixsummarystats}[5]{\begingroup\matrixsummaryfont\begin{tabular}[t]{@{}l@{\hspace{0.014in}\matrixpunct{|}\hspace{0.014in}}l@{\hspace{0.014in}\matrixpunct{|}\hspace{0.014in}}l@{\hspace{0.014in}\matrixpunct{|}\hspace{0.014in}}l@{\hspace{0.014in}\matrixpunct{|}\hspace{0.014in}}l@{}}\makebox[3.6em][l]{#1}&\makebox[4.6em][l]{#2}&\makebox[3.6em][l]{#3}&\makebox[3.6em][l]{#4}&\makebox[4.2em][l]{#5}\end{tabular}\endgroup} +\providecommand{\matrixsummaryworkloads}[2]{\begin{tabular}[t]{@{}l@{}}#1\\[-0.10em]#2\end{tabular}} +\providecommand{\matrixpair}[2]{\begin{tabular}[t]{@{}r@{\hspace{0.025in}\matrixpunct{|}\hspace{0.025in}}r@{}}#1\end{tabular}} +\providecommand{\bestmoderatio}[2]{\textcolor{#1}{\texttt{x#2}}} +\providecommand{\bestmodecompactratio}[3]{\matrixpunct{(}\textcolor{ReportMuted}{\texttt{[x#1]}}\hspace{0.04in}\textcolor{#2}{\texttt{x#3}}\matrixpunct{)}} +\providecommand{\bestmodewallratio}[2]{\matrixpunct{(}\textcolor{#1}{\texttt{x#2}}\matrixpunct{)}} +\providecommand{\bestmodecompactprefix}[1]{\matrixpunct{(}\textcolor{ReportMuted}{\texttt{[x#1]}}\hspace{0.04in}} +\providecommand{\bestmodeopenprefix}{\matrixpunct{(}\hspace{0.04in}} +\providecommand{\bestmodeprimaryratio}[2]{\textcolor{#1}{\texttt{x#2}}\matrixpunct{)}} +\providecommand{\bestmodeabsoluteprefix}[1]{#1\hspace{0.04in}} +\providecommand{\bestmodecompactabsolute}[2]{\textcolor{ReportMuted}{\texttt{[#1]}}\hspace{0.04in}\textcolor{black}{\texttt{#2}}} +\providecommand{\bestmodecompactabsoluteprefix}[1]{\textcolor{ReportMuted}{\texttt{[#1]}}\hspace{0.04in}} +\providecommand{\bestmodeabsoluteprimary}[1]{\textcolor{black}{\texttt{#1}}} +\providecommand{\bestmodeunverifiedclockprefix}[2]{\textcolor{ReportMuted}{\texttt{[}#1\texttt{]}}\hspace{0.04in}#2\hspace{0.04in}} +\clearpage +\subsection{Built-in SM eager-DAG JIT O2 versus recurrence JIT O2 NLC} +\noindent\begin{minipage}{\linewidth} +\subsubsection*{Built-in SM eager-DAG JIT O2 versus recurrence JIT O2 NLC (block 1 of 2)} +\begingroup +\centering +\footnotesize +\setlength{\tabcolsep}{2.1pt} +\renewcommand{\arraystretch}{1.04} +\matrixfitwidth{% +\begin{tabular}{@{}rL{1.45in}l>{\matrixentryfont}l>{\matrixentryfont}r@{}>{\matrixentryfont}l>{\matrixentryfont}l>{\matrixentryfont}r@{}>{\matrixentryfont}l>{\matrixentryfont}l>{\matrixentryfont}r@{}>{\matrixentryfont}l@{}} +\toprule +\textbf{ID} & \textbf{base process} & \textbf{metric} & \multicolumn{3}{c}{\textbf{n=1}} & \multicolumn{3}{c}{\textbf{n=2}} & \multicolumn{3}{c}{\textbf{n=3}} \\ +\specialrule{0.85pt}{0pt}{0pt} +\rowcolor{refblue} +\texttt{1} & $d\bar d\to Z+(n-1)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{2} & $u\bar d\to W^++(n-1)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{3} & $d\bar d\to e^+e^-+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{4} & $u\bar d\to e^+\nu_e+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{5} & $d\bar d\to ZZ+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{6} & $gg\to t\bar t+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{7} & $d\bar d\to t\bar t+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{8} & $gg\to gg+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{9} & $d\bar d\to ZZZ+(n-3)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{10} & $d\bar d\to e^+e^-ZH+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{11} & $d\bar d\to t\bar t ZH+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{12} & $d\bar d\to e^+e^-e^+e^-+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{13} & $d\bar d\to u\bar u\,s\bar s+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{14} & $d\bar d\to u\bar u\,s\bar s\,c\bar c+(n-6)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{15} & $d\bar d\to u\bar u\,u\bar u+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\specialrule{1.05pt}{0.22em}{0.18em} +\multicolumn{3}{@{}l}{} & \multicolumn{3}{c}{\cellcolor{refblue}\textbf{n=1}} & \multicolumn{3}{c}{\textbf{n=2}} & \multicolumn{3}{c}{\cellcolor{refblue}\textbf{n=3}} \\ +\multicolumn{3}{@{}l}{\textbf{summary: generation}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} & \multicolumn{3}{c}{\matrixna{ReportMuted}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} \\[0.08em] +\multicolumn{3}{@{}l}{\textbf{summary: wall}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} & \multicolumn{3}{c}{\matrixna{ReportMuted}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} \\ +\bottomrule +\end{tabular} +} +\endgroup +\ReportTableNote{{\scriptsize Baseline: recurrence JIT O2; candidate: eager-DAG JIT O2. Each cell shows the baseline generation and wall time, followed by candidate/baseline generation and wall-time ratios. Runtime comparisons use ([xS]xW). The muted supplementary xS first compares compatible exposed execution attributions; otherwise it compares authenticated evaluator totals. Only when original AmpliCol has no evaluator-total clock may a candidate evaluator total use the legacy direct execution clock (or its wall clock when direct execution is absent) as denominator. An evaluator total is never divided by a recurrence core. This evaluator-total fallback is diagnostic only; the execution/execution ratio remains the compatible supplementary attribution. The coloured xW is always the candidate/baseline wall multiplier and remains the primary figure of merit. When no compatible supplementary clock exists, only (xW) is shown. No clock is fabricated from another. Every displayed number uses exactly three significant digits. Unverified compiled or eager diagnostics show absolute generation and runtime values with a yellow unverified marker, but never enter ratios or summaries. In their runtime token the muted bracketed value is evaluator-total and the following value is outer wall time. Ratios and summaries require exact stored linkage to the current denominator. Summary rows contain multipliers only in min, max, median, average, and weighted-average order; the weighted average is the ratio of timing sums, is the framed bold entry, and runtime statistics use wall time only. Not applicable marks a multiplicity for which the process family does not exist. Not run marks an otherwise defined process that was intentionally excluded from the declared measurement scope. Fixed-engine tables intentionally omit mode letters because the selected engine is named in the table heading.}} +\end{minipage} +\clearpage +\noindent\begin{minipage}{\linewidth} +\subsubsection*{Built-in SM eager-DAG JIT O2 versus recurrence JIT O2 NLC (block 2 of 2)} +\begingroup +\centering +\footnotesize +\setlength{\tabcolsep}{2.1pt} +\renewcommand{\arraystretch}{1.04} +\matrixfitwidth{% +\begin{tabular}{@{}rL{1.45in}l>{\matrixentryfont}l>{\matrixentryfont}r@{}>{\matrixentryfont}l>{\matrixentryfont}l>{\matrixentryfont}r@{}>{\matrixentryfont}l>{\matrixentryfont}l>{\matrixentryfont}r@{}>{\matrixentryfont}l@{}} +\toprule +\textbf{ID} & \textbf{base process} & \textbf{metric} & \multicolumn{3}{c}{\textbf{n=4}} & \multicolumn{3}{c}{\textbf{n=5}} & \multicolumn{3}{c}{\textbf{n=6}} \\ +\specialrule{0.85pt}{0pt}{0pt} +\rowcolor{refblue} +\texttt{1} & $d\bar d\to Z+(n-1)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{2} & $u\bar d\to W^++(n-1)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{3} & $d\bar d\to e^+e^-+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{4} & $u\bar d\to e^+\nu_e+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{5} & $d\bar d\to ZZ+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{6} & $gg\to t\bar t+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{7} & $d\bar d\to t\bar t+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{8} & $gg\to gg+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{9} & $d\bar d\to ZZZ+(n-3)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{10} & $d\bar d\to e^+e^-ZH+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{11} & $d\bar d\to t\bar t ZH+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{12} & $d\bar d\to e^+e^-e^+e^-+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{13} & $d\bar d\to u\bar u\,s\bar s+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{14} & $d\bar d\to u\bar u\,s\bar s\,c\bar c+(n-6)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{15} & $d\bar d\to u\bar u\,u\bar u+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\specialrule{1.05pt}{0.22em}{0.18em} +\multicolumn{3}{@{}l}{} & \multicolumn{3}{c}{\cellcolor{refblue}\textbf{n=4}} & \multicolumn{3}{c}{\textbf{n=5}} & \multicolumn{3}{c}{\cellcolor{refblue}\textbf{n=6}} \\ +\multicolumn{3}{@{}l}{\textbf{summary: generation}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} & \multicolumn{3}{c}{\matrixna{ReportMuted}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} \\[0.08em] +\multicolumn{3}{@{}l}{\textbf{summary: wall}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} & \multicolumn{3}{c}{\matrixna{ReportMuted}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} \\ +\bottomrule +\end{tabular} +} +\endgroup +\ReportTableNote{{\scriptsize Baseline: recurrence JIT O2; candidate: eager-DAG JIT O2. Each cell shows the baseline generation and wall time, followed by candidate/baseline generation and wall-time ratios. Runtime comparisons use ([xS]xW). The muted supplementary xS first compares compatible exposed execution attributions; otherwise it compares authenticated evaluator totals. Only when original AmpliCol has no evaluator-total clock may a candidate evaluator total use the legacy direct execution clock (or its wall clock when direct execution is absent) as denominator. An evaluator total is never divided by a recurrence core. This evaluator-total fallback is diagnostic only; the execution/execution ratio remains the compatible supplementary attribution. The coloured xW is always the candidate/baseline wall multiplier and remains the primary figure of merit. When no compatible supplementary clock exists, only (xW) is shown. No clock is fabricated from another. Every displayed number uses exactly three significant digits. Unverified compiled or eager diagnostics show absolute generation and runtime values with a yellow unverified marker, but never enter ratios or summaries. In their runtime token the muted bracketed value is evaluator-total and the following value is outer wall time. Ratios and summaries require exact stored linkage to the current denominator. Summary rows contain multipliers only in min, max, median, average, and weighted-average order; the weighted average is the ratio of timing sums, is the framed bold entry, and runtime statistics use wall time only. Not applicable marks a multiplicity for which the process family does not exist. Not run marks an otherwise defined process that was intentionally excluded from the declared measurement scope. Fixed-engine tables intentionally omit mode letters because the selected engine is named in the table heading.}} +\end{minipage} diff --git a/pyamplicol/src/pyamplicol/_profiling_campaign/result_matrix_eager_ufo_sm_full_vs_madgraph_table.tex b/pyamplicol/src/pyamplicol/_profiling_campaign/result_matrix_eager_ufo_sm_full_vs_madgraph_table.tex new file mode 100644 index 0000000000..3fcb7c1aca --- /dev/null +++ b/pyamplicol/src/pyamplicol/_profiling_campaign/result_matrix_eager_ufo_sm_full_vs_madgraph_table.tex @@ -0,0 +1,208 @@ +% SPDX-License-Identifier: 0BSD +% Generated by tools/performance_report/render.py; do not edit. +\providecommand{\matrixentryfont}{\fontsize{6.8pt}{7.8pt}\selectfont} +\providecommand{\matrixentryfontlc}{\fontsize{6.5pt}{7.5pt}\selectfont} +\providecommand{\matrixsummaryfont}{\fontsize{6.2pt}{7.4pt}\selectfont} +\providecommand{\matrixfitwidth}[1]{\begingroup\sbox0{#1}\ifdim\wd0>\linewidth\resizebox{\linewidth}{!}{\usebox0}\else\makebox[\linewidth][c]{\usebox0}\fi\endgroup} +\providecommand{\matrixpunct}[1]{\textcolor{black}{\texttt{#1}}} +\providecommand{\matrixratio}[2]{\matrixpunct{(}\textcolor{#1}{\texttt{x#2}}\matrixpunct{)}} +\providecommand{\matrixna}[1]{\textcolor{#1}{\texttt{N/A}}} +\providecommand{\matrixnotapplicable}[1]{\textcolor{#1}{\textsc{not applicable}}} +\providecommand{\matrixnotexposed}[1]{\textcolor{#1}{\textsc{not exposed}}} +\providecommand{\matrixstaticna}[1]{\textcolor{#1}{\textsc{static N/A}}} +\providecommand{\matrixtotalevaluator}[1]{\textcolor{ReportBlue}{\texttt{T }}#1} +\providecommand{\matrixrecurrencecore}[1]{\textcolor{ReportOrange}{\texttt{C }}#1} +\providecommand{\matrixzrecurrenceclocks}[2]{\shortstack[r]{#1\\#2}} +\providecommand{\matrixwallclock}[2]{\textcolor{#1}{\texttt{W #2}}} +\providecommand{\matrixwallabsolute}[1]{\textcolor{ReportBlue}{\texttt{W }}#1} +\providecommand{\matrixruntimepair}[2]{\shortstack[l]{#1\\#2}} +\providecommand{\matrixruntimetriple}[3]{\shortstack[l]{#1\\#2\matrixpunct{ | }#3}} +\providecommand{\matrixbelow}[1]{\textcolor{ReportMuted}{\texttt{<#1}}} +\providecommand{\matrixnaratio}[1]{\matrixpunct{(}\matrixna{#1}\matrixpunct{)}} +\providecommand{\matrixstatus}[2]{\textcolor{#1}{\textsc{#2}}} +\providecommand{\matrixncabsolute}[1]{#1} +\providecommand{\matrixsummaryratio}[2]{\textcolor{#1}{\texttt{x#2}}} +\providecommand{\matrixsummaryratiohighlight}[2]{\begingroup\setlength{\fboxsep}{0.9pt}\setlength{\fboxrule}{0.35pt}\fbox{\textcolor{#1}{{\usefont{T1}{lmtt}{b}{n}x#2}}}\endgroup} +\providecommand{\matrixsummarystats}[5]{\begingroup\matrixsummaryfont\begin{tabular}[t]{@{}l@{\hspace{0.014in}\matrixpunct{|}\hspace{0.014in}}l@{\hspace{0.014in}\matrixpunct{|}\hspace{0.014in}}l@{\hspace{0.014in}\matrixpunct{|}\hspace{0.014in}}l@{\hspace{0.014in}\matrixpunct{|}\hspace{0.014in}}l@{}}\makebox[3.6em][l]{#1}&\makebox[4.6em][l]{#2}&\makebox[3.6em][l]{#3}&\makebox[3.6em][l]{#4}&\makebox[4.2em][l]{#5}\end{tabular}\endgroup} +\providecommand{\matrixsummaryworkloads}[2]{\begin{tabular}[t]{@{}l@{}}#1\\[-0.10em]#2\end{tabular}} +\providecommand{\matrixpair}[2]{\begin{tabular}[t]{@{}r@{\hspace{0.025in}\matrixpunct{|}\hspace{0.025in}}r@{}}#1\end{tabular}} +\providecommand{\bestmoderatio}[2]{\textcolor{#1}{\texttt{x#2}}} +\providecommand{\bestmodecompactratio}[3]{\matrixpunct{(}\textcolor{ReportMuted}{\texttt{[x#1]}}\hspace{0.04in}\textcolor{#2}{\texttt{x#3}}\matrixpunct{)}} +\providecommand{\bestmodewallratio}[2]{\matrixpunct{(}\textcolor{#1}{\texttt{x#2}}\matrixpunct{)}} +\providecommand{\bestmodecompactprefix}[1]{\matrixpunct{(}\textcolor{ReportMuted}{\texttt{[x#1]}}\hspace{0.04in}} +\providecommand{\bestmodeopenprefix}{\matrixpunct{(}\hspace{0.04in}} +\providecommand{\bestmodeprimaryratio}[2]{\textcolor{#1}{\texttt{x#2}}\matrixpunct{)}} +\providecommand{\bestmodeabsoluteprefix}[1]{#1\hspace{0.04in}} +\providecommand{\bestmodecompactabsolute}[2]{\textcolor{ReportMuted}{\texttt{[#1]}}\hspace{0.04in}\textcolor{black}{\texttt{#2}}} +\providecommand{\bestmodecompactabsoluteprefix}[1]{\textcolor{ReportMuted}{\texttt{[#1]}}\hspace{0.04in}} +\providecommand{\bestmodeabsoluteprimary}[1]{\textcolor{black}{\texttt{#1}}} +\providecommand{\bestmodeunverifiedclockprefix}[2]{\textcolor{ReportMuted}{\texttt{[}#1\texttt{]}}\hspace{0.04in}#2\hspace{0.04in}} +\clearpage +\subsection{UFO-SM eager-DAG JIT O2 versus MadGraph full-colour} +\noindent\begin{minipage}{\linewidth} +\subsubsection*{UFO-SM eager-DAG JIT O2 versus MadGraph full-colour (block 1 of 2)} +\begingroup +\centering +\footnotesize +\setlength{\tabcolsep}{2.1pt} +\renewcommand{\arraystretch}{1.04} +\matrixfitwidth{% +\begin{tabular}{@{}rL{1.45in}l>{\matrixentryfont}l>{\matrixentryfont}r@{}>{\matrixentryfont}l>{\matrixentryfont}l>{\matrixentryfont}r@{}>{\matrixentryfont}l>{\matrixentryfont}l>{\matrixentryfont}r@{}>{\matrixentryfont}l@{}} +\toprule +\textbf{ID} & \textbf{base process} & \textbf{metric} & \multicolumn{3}{c}{\textbf{n=1}} & \multicolumn{3}{c}{\textbf{n=2}} & \multicolumn{3}{c}{\textbf{n=3}} \\ +\specialrule{0.85pt}{0pt}{0pt} +\rowcolor{refblue} +\texttt{1} & $d\bar d\to Z+(n-1)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{2} & $u\bar d\to W^++(n-1)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{3} & $d\bar d\to e^+e^-+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{4} & $u\bar d\to e^+\nu_e+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{5} & $d\bar d\to ZZ+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{6} & $gg\to t\bar t+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{7} & $d\bar d\to t\bar t+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{8} & $gg\to gg+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{9} & $d\bar d\to ZZZ+(n-3)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{10} & $d\bar d\to e^+e^-ZH+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{11} & $d\bar d\to t\bar t ZH+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{12} & $d\bar d\to e^+e^-e^+e^-+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{13} & $d\bar d\to u\bar u\,s\bar s+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{14} & $d\bar d\to u\bar u\,s\bar s\,c\bar c+(n-6)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{15} & $d\bar d\to u\bar u\,u\bar u+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\specialrule{1.05pt}{0.22em}{0.18em} +\multicolumn{3}{@{}l}{} & \multicolumn{3}{c}{\cellcolor{refblue}\textbf{n=1}} & \multicolumn{3}{c}{\textbf{n=2}} & \multicolumn{3}{c}{\cellcolor{refblue}\textbf{n=3}} \\ +\multicolumn{3}{@{}l}{\textbf{summary: generation}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} & \multicolumn{3}{c}{\matrixna{ReportMuted}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} \\[0.08em] +\multicolumn{3}{@{}l}{\textbf{summary: wall}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} & \multicolumn{3}{c}{\matrixna{ReportMuted}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} \\ +\bottomrule +\end{tabular} +} +\endgroup +\ReportTableNote{{\scriptsize Baseline: MadGraph standalone; candidate: eager-DAG JIT O2. Each cell shows the baseline generation and wall time, followed by candidate/baseline generation and wall-time ratios. Runtime comparisons use ([xS]xW). The muted supplementary xS first compares compatible exposed execution attributions; otherwise it compares authenticated evaluator totals. Only when original AmpliCol has no evaluator-total clock may a candidate evaluator total use the legacy direct execution clock (or its wall clock when direct execution is absent) as denominator. An evaluator total is never divided by a recurrence core. This evaluator-total fallback is diagnostic only; the execution/execution ratio remains the compatible supplementary attribution. The coloured xW is always the candidate/baseline wall multiplier and remains the primary figure of merit. When no compatible supplementary clock exists, only (xW) is shown. No clock is fabricated from another. Every displayed number uses exactly three significant digits. MadGraph standalone is the authoritative full-colour numerical reference; its generation and repeated-evaluation measurements are shared by all four UFO-SM comparison views. Recurrence, compiled, and eager candidates use p200 at the comparison point; the on-the-fly candidate uses its supported p16 lane. Unverified compiled or eager diagnostics show absolute generation and runtime values with a yellow unverified marker, but never enter ratios or summaries. In their runtime token the muted bracketed value is evaluator-total and the following value is outer wall time. Ratios and summaries require exact stored linkage to the current denominator. Summary rows contain multipliers only in min, max, median, average, and weighted-average order; the weighted average is the ratio of timing sums, is the framed bold entry, and runtime statistics use wall time only. Not applicable marks a multiplicity for which the process family does not exist. Not run marks an otherwise defined process that was intentionally excluded from the declared measurement scope. Fixed-engine tables intentionally omit mode letters because the selected engine is named in the table heading.}} +\end{minipage} +\clearpage +\noindent\begin{minipage}{\linewidth} +\subsubsection*{UFO-SM eager-DAG JIT O2 versus MadGraph full-colour (block 2 of 2)} +\begingroup +\centering +\footnotesize +\setlength{\tabcolsep}{2.1pt} +\renewcommand{\arraystretch}{1.04} +\matrixfitwidth{% +\begin{tabular}{@{}rL{1.45in}l>{\matrixentryfont}l>{\matrixentryfont}r@{}>{\matrixentryfont}l>{\matrixentryfont}l>{\matrixentryfont}r@{}>{\matrixentryfont}l>{\matrixentryfont}l>{\matrixentryfont}r@{}>{\matrixentryfont}l@{}} +\toprule +\textbf{ID} & \textbf{base process} & \textbf{metric} & \multicolumn{3}{c}{\textbf{n=4}} & \multicolumn{3}{c}{\textbf{n=5}} & \multicolumn{3}{c}{\textbf{n=6}} \\ +\specialrule{0.85pt}{0pt}{0pt} +\rowcolor{refblue} +\texttt{1} & $d\bar d\to Z+(n-1)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{2} & $u\bar d\to W^++(n-1)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{3} & $d\bar d\to e^+e^-+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{4} & $u\bar d\to e^+\nu_e+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{5} & $d\bar d\to ZZ+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{6} & $gg\to t\bar t+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{7} & $d\bar d\to t\bar t+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{8} & $gg\to gg+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{9} & $d\bar d\to ZZZ+(n-3)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{10} & $d\bar d\to e^+e^-ZH+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{11} & $d\bar d\to t\bar t ZH+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{12} & $d\bar d\to e^+e^-e^+e^-+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{13} & $d\bar d\to u\bar u\,s\bar s+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{14} & $d\bar d\to u\bar u\,s\bar s\,c\bar c+(n-6)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{15} & $d\bar d\to u\bar u\,u\bar u+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\specialrule{1.05pt}{0.22em}{0.18em} +\multicolumn{3}{@{}l}{} & \multicolumn{3}{c}{\cellcolor{refblue}\textbf{n=4}} & \multicolumn{3}{c}{\textbf{n=5}} & \multicolumn{3}{c}{\cellcolor{refblue}\textbf{n=6}} \\ +\multicolumn{3}{@{}l}{\textbf{summary: generation}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} & \multicolumn{3}{c}{\matrixna{ReportMuted}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} \\[0.08em] +\multicolumn{3}{@{}l}{\textbf{summary: wall}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} & \multicolumn{3}{c}{\matrixna{ReportMuted}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} \\ +\bottomrule +\end{tabular} +} +\endgroup +\ReportTableNote{{\scriptsize Baseline: MadGraph standalone; candidate: eager-DAG JIT O2. Each cell shows the baseline generation and wall time, followed by candidate/baseline generation and wall-time ratios. Runtime comparisons use ([xS]xW). The muted supplementary xS first compares compatible exposed execution attributions; otherwise it compares authenticated evaluator totals. Only when original AmpliCol has no evaluator-total clock may a candidate evaluator total use the legacy direct execution clock (or its wall clock when direct execution is absent) as denominator. An evaluator total is never divided by a recurrence core. This evaluator-total fallback is diagnostic only; the execution/execution ratio remains the compatible supplementary attribution. The coloured xW is always the candidate/baseline wall multiplier and remains the primary figure of merit. When no compatible supplementary clock exists, only (xW) is shown. No clock is fabricated from another. Every displayed number uses exactly three significant digits. MadGraph standalone is the authoritative full-colour numerical reference; its generation and repeated-evaluation measurements are shared by all four UFO-SM comparison views. Recurrence, compiled, and eager candidates use p200 at the comparison point; the on-the-fly candidate uses its supported p16 lane. Unverified compiled or eager diagnostics show absolute generation and runtime values with a yellow unverified marker, but never enter ratios or summaries. In their runtime token the muted bracketed value is evaluator-total and the following value is outer wall time. Ratios and summaries require exact stored linkage to the current denominator. Summary rows contain multipliers only in min, max, median, average, and weighted-average order; the weighted average is the ratio of timing sums, is the framed bold entry, and runtime statistics use wall time only. Not applicable marks a multiplicity for which the process family does not exist. Not run marks an otherwise defined process that was intentionally excluded from the declared measurement scope. Fixed-engine tables intentionally omit mode letters because the selected engine is named in the table heading.}} +\end{minipage} diff --git a/pyamplicol/src/pyamplicol/_profiling_campaign/result_matrix_on_the_fly_builtin_sm_full_table.tex b/pyamplicol/src/pyamplicol/_profiling_campaign/result_matrix_on_the_fly_builtin_sm_full_table.tex new file mode 100644 index 0000000000..d7752ffdf6 --- /dev/null +++ b/pyamplicol/src/pyamplicol/_profiling_campaign/result_matrix_on_the_fly_builtin_sm_full_table.tex @@ -0,0 +1,208 @@ +% SPDX-License-Identifier: 0BSD +% Generated by tools/performance_report/render.py; do not edit. +\providecommand{\matrixentryfont}{\fontsize{6.8pt}{7.8pt}\selectfont} +\providecommand{\matrixentryfontlc}{\fontsize{6.5pt}{7.5pt}\selectfont} +\providecommand{\matrixsummaryfont}{\fontsize{6.2pt}{7.4pt}\selectfont} +\providecommand{\matrixfitwidth}[1]{\begingroup\sbox0{#1}\ifdim\wd0>\linewidth\resizebox{\linewidth}{!}{\usebox0}\else\makebox[\linewidth][c]{\usebox0}\fi\endgroup} +\providecommand{\matrixpunct}[1]{\textcolor{black}{\texttt{#1}}} +\providecommand{\matrixratio}[2]{\matrixpunct{(}\textcolor{#1}{\texttt{x#2}}\matrixpunct{)}} +\providecommand{\matrixna}[1]{\textcolor{#1}{\texttt{N/A}}} +\providecommand{\matrixnotapplicable}[1]{\textcolor{#1}{\textsc{not applicable}}} +\providecommand{\matrixnotexposed}[1]{\textcolor{#1}{\textsc{not exposed}}} +\providecommand{\matrixstaticna}[1]{\textcolor{#1}{\textsc{static N/A}}} +\providecommand{\matrixtotalevaluator}[1]{\textcolor{ReportBlue}{\texttt{T }}#1} +\providecommand{\matrixrecurrencecore}[1]{\textcolor{ReportOrange}{\texttt{C }}#1} +\providecommand{\matrixzrecurrenceclocks}[2]{\shortstack[r]{#1\\#2}} +\providecommand{\matrixwallclock}[2]{\textcolor{#1}{\texttt{W #2}}} +\providecommand{\matrixwallabsolute}[1]{\textcolor{ReportBlue}{\texttt{W }}#1} +\providecommand{\matrixruntimepair}[2]{\shortstack[l]{#1\\#2}} +\providecommand{\matrixruntimetriple}[3]{\shortstack[l]{#1\\#2\matrixpunct{ | }#3}} +\providecommand{\matrixbelow}[1]{\textcolor{ReportMuted}{\texttt{<#1}}} +\providecommand{\matrixnaratio}[1]{\matrixpunct{(}\matrixna{#1}\matrixpunct{)}} +\providecommand{\matrixstatus}[2]{\textcolor{#1}{\textsc{#2}}} +\providecommand{\matrixncabsolute}[1]{#1} +\providecommand{\matrixsummaryratio}[2]{\textcolor{#1}{\texttt{x#2}}} +\providecommand{\matrixsummaryratiohighlight}[2]{\begingroup\setlength{\fboxsep}{0.9pt}\setlength{\fboxrule}{0.35pt}\fbox{\textcolor{#1}{{\usefont{T1}{lmtt}{b}{n}x#2}}}\endgroup} +\providecommand{\matrixsummarystats}[5]{\begingroup\matrixsummaryfont\begin{tabular}[t]{@{}l@{\hspace{0.014in}\matrixpunct{|}\hspace{0.014in}}l@{\hspace{0.014in}\matrixpunct{|}\hspace{0.014in}}l@{\hspace{0.014in}\matrixpunct{|}\hspace{0.014in}}l@{\hspace{0.014in}\matrixpunct{|}\hspace{0.014in}}l@{}}\makebox[3.6em][l]{#1}&\makebox[4.6em][l]{#2}&\makebox[3.6em][l]{#3}&\makebox[3.6em][l]{#4}&\makebox[4.2em][l]{#5}\end{tabular}\endgroup} +\providecommand{\matrixsummaryworkloads}[2]{\begin{tabular}[t]{@{}l@{}}#1\\[-0.10em]#2\end{tabular}} +\providecommand{\matrixpair}[2]{\begin{tabular}[t]{@{}r@{\hspace{0.025in}\matrixpunct{|}\hspace{0.025in}}r@{}}#1\end{tabular}} +\providecommand{\bestmoderatio}[2]{\textcolor{#1}{\texttt{x#2}}} +\providecommand{\bestmodecompactratio}[3]{\matrixpunct{(}\textcolor{ReportMuted}{\texttt{[x#1]}}\hspace{0.04in}\textcolor{#2}{\texttt{x#3}}\matrixpunct{)}} +\providecommand{\bestmodewallratio}[2]{\matrixpunct{(}\textcolor{#1}{\texttt{x#2}}\matrixpunct{)}} +\providecommand{\bestmodecompactprefix}[1]{\matrixpunct{(}\textcolor{ReportMuted}{\texttt{[x#1]}}\hspace{0.04in}} +\providecommand{\bestmodeopenprefix}{\matrixpunct{(}\hspace{0.04in}} +\providecommand{\bestmodeprimaryratio}[2]{\textcolor{#1}{\texttt{x#2}}\matrixpunct{)}} +\providecommand{\bestmodeabsoluteprefix}[1]{#1\hspace{0.04in}} +\providecommand{\bestmodecompactabsolute}[2]{\textcolor{ReportMuted}{\texttt{[#1]}}\hspace{0.04in}\textcolor{black}{\texttt{#2}}} +\providecommand{\bestmodecompactabsoluteprefix}[1]{\textcolor{ReportMuted}{\texttt{[#1]}}\hspace{0.04in}} +\providecommand{\bestmodeabsoluteprimary}[1]{\textcolor{black}{\texttt{#1}}} +\providecommand{\bestmodeunverifiedclockprefix}[2]{\textcolor{ReportMuted}{\texttt{[}#1\texttt{]}}\hspace{0.04in}#2\hspace{0.04in}} +\clearpage +\subsection{Built-in SM on-the-fly JIT O2 versus AmpliCol full-colour} +\noindent\begin{minipage}{\linewidth} +\subsubsection*{Built-in SM on-the-fly JIT O2 versus AmpliCol full-colour (block 1 of 2)} +\begingroup +\centering +\footnotesize +\setlength{\tabcolsep}{2.1pt} +\renewcommand{\arraystretch}{1.04} +\matrixfitwidth{% +\begin{tabular}{@{}rL{1.45in}l>{\matrixentryfont}l>{\matrixentryfont}r@{}>{\matrixentryfont}l>{\matrixentryfont}l>{\matrixentryfont}r@{}>{\matrixentryfont}l>{\matrixentryfont}l>{\matrixentryfont}r@{}>{\matrixentryfont}l@{}} +\toprule +\textbf{ID} & \textbf{base process} & \textbf{metric} & \multicolumn{3}{c}{\textbf{n=1}} & \multicolumn{3}{c}{\textbf{n=2}} & \multicolumn{3}{c}{\textbf{n=3}} \\ +\specialrule{0.85pt}{0pt}{0pt} +\rowcolor{refblue} +\texttt{1} & $d\bar d\to Z+(n-1)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{2} & $u\bar d\to W^++(n-1)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{3} & $d\bar d\to e^+e^-+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{4} & $u\bar d\to e^+\nu_e+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{5} & $d\bar d\to ZZ+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{6} & $gg\to t\bar t+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{7} & $d\bar d\to t\bar t+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{8} & $gg\to gg+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{9} & $d\bar d\to ZZZ+(n-3)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{10} & $d\bar d\to e^+e^-ZH+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{11} & $d\bar d\to t\bar t ZH+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{12} & $d\bar d\to e^+e^-e^+e^-+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{13} & $d\bar d\to u\bar u\,s\bar s+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{14} & $d\bar d\to u\bar u\,s\bar s\,c\bar c+(n-6)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{15} & $d\bar d\to u\bar u\,u\bar u+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\specialrule{1.05pt}{0.22em}{0.18em} +\multicolumn{3}{@{}l}{} & \multicolumn{3}{c}{\cellcolor{refblue}\textbf{n=1}} & \multicolumn{3}{c}{\textbf{n=2}} & \multicolumn{3}{c}{\cellcolor{refblue}\textbf{n=3}} \\ +\multicolumn{3}{@{}l}{\textbf{summary: generation}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} & \multicolumn{3}{c}{\matrixna{ReportMuted}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} \\[0.08em] +\multicolumn{3}{@{}l}{\textbf{summary: wall}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} & \multicolumn{3}{c}{\matrixna{ReportMuted}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} \\ +\bottomrule +\end{tabular} +} +\endgroup +\ReportTableNote{{\scriptsize Baseline: original AmpliCol; candidate: on-the-fly JIT O2. Each cell shows the baseline generation and wall time, followed by candidate/baseline generation and wall-time ratios. Runtime comparisons use ([xS]xW). The muted supplementary xS first compares compatible exposed execution attributions; otherwise it compares authenticated evaluator totals. Only when original AmpliCol has no evaluator-total clock may a candidate evaluator total use the legacy direct execution clock (or its wall clock when direct execution is absent) as denominator. An evaluator total is never divided by a recurrence core. This evaluator-total fallback is diagnostic only; the execution/execution ratio remains the compatible supplementary attribution. The coloured xW is always the candidate/baseline wall multiplier and remains the primary figure of merit. When no compatible supplementary clock exists, only (xW) is shown. No clock is fabricated from another. Every displayed number uses exactly three significant digits. Original AmpliCol is static N/A beyond three open quark lines; unavailable, terminal, or unlinked baselines keep their status while successful candidate timings remain absolute and leave summaries. Summary rows contain multipliers only in min, max, median, average, and weighted-average order; the weighted average is the ratio of timing sums, is the framed bold entry, and runtime statistics use wall time only. Not applicable marks a multiplicity for which the process family does not exist. Not run marks an otherwise defined process that was intentionally excluded from the declared measurement scope. Fixed-engine tables intentionally omit mode letters because the selected engine is named in the table heading.}} +\end{minipage} +\clearpage +\noindent\begin{minipage}{\linewidth} +\subsubsection*{Built-in SM on-the-fly JIT O2 versus AmpliCol full-colour (block 2 of 2)} +\begingroup +\centering +\footnotesize +\setlength{\tabcolsep}{2.1pt} +\renewcommand{\arraystretch}{1.04} +\matrixfitwidth{% +\begin{tabular}{@{}rL{1.45in}l>{\matrixentryfont}l>{\matrixentryfont}r@{}>{\matrixentryfont}l>{\matrixentryfont}l>{\matrixentryfont}r@{}>{\matrixentryfont}l>{\matrixentryfont}l>{\matrixentryfont}r@{}>{\matrixentryfont}l@{}} +\toprule +\textbf{ID} & \textbf{base process} & \textbf{metric} & \multicolumn{3}{c}{\textbf{n=4}} & \multicolumn{3}{c}{\textbf{n=5}} & \multicolumn{3}{c}{\textbf{n=6}} \\ +\specialrule{0.85pt}{0pt}{0pt} +\rowcolor{refblue} +\texttt{1} & $d\bar d\to Z+(n-1)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{2} & $u\bar d\to W^++(n-1)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{3} & $d\bar d\to e^+e^-+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{4} & $u\bar d\to e^+\nu_e+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{5} & $d\bar d\to ZZ+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{6} & $gg\to t\bar t+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{7} & $d\bar d\to t\bar t+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{8} & $gg\to gg+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{9} & $d\bar d\to ZZZ+(n-3)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{10} & $d\bar d\to e^+e^-ZH+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{11} & $d\bar d\to t\bar t ZH+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{12} & $d\bar d\to e^+e^-e^+e^-+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{13} & $d\bar d\to u\bar u\,s\bar s+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{14} & $d\bar d\to u\bar u\,s\bar s\,c\bar c+(n-6)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstaticna{ReportMuted} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \matrixstaticna{ReportMuted} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{15} & $d\bar d\to u\bar u\,u\bar u+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\specialrule{1.05pt}{0.22em}{0.18em} +\multicolumn{3}{@{}l}{} & \multicolumn{3}{c}{\cellcolor{refblue}\textbf{n=4}} & \multicolumn{3}{c}{\textbf{n=5}} & \multicolumn{3}{c}{\cellcolor{refblue}\textbf{n=6}} \\ +\multicolumn{3}{@{}l}{\textbf{summary: generation}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} & \multicolumn{3}{c}{\matrixna{ReportMuted}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} \\[0.08em] +\multicolumn{3}{@{}l}{\textbf{summary: wall}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} & \multicolumn{3}{c}{\matrixna{ReportMuted}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} \\ +\bottomrule +\end{tabular} +} +\endgroup +\ReportTableNote{{\scriptsize Baseline: original AmpliCol; candidate: on-the-fly JIT O2. Each cell shows the baseline generation and wall time, followed by candidate/baseline generation and wall-time ratios. Runtime comparisons use ([xS]xW). The muted supplementary xS first compares compatible exposed execution attributions; otherwise it compares authenticated evaluator totals. Only when original AmpliCol has no evaluator-total clock may a candidate evaluator total use the legacy direct execution clock (or its wall clock when direct execution is absent) as denominator. An evaluator total is never divided by a recurrence core. This evaluator-total fallback is diagnostic only; the execution/execution ratio remains the compatible supplementary attribution. The coloured xW is always the candidate/baseline wall multiplier and remains the primary figure of merit. When no compatible supplementary clock exists, only (xW) is shown. No clock is fabricated from another. Every displayed number uses exactly three significant digits. Original AmpliCol is static N/A beyond three open quark lines; unavailable, terminal, or unlinked baselines keep their status while successful candidate timings remain absolute and leave summaries. Summary rows contain multipliers only in min, max, median, average, and weighted-average order; the weighted average is the ratio of timing sums, is the framed bold entry, and runtime statistics use wall time only. Not applicable marks a multiplicity for which the process family does not exist. Not run marks an otherwise defined process that was intentionally excluded from the declared measurement scope. Fixed-engine tables intentionally omit mode letters because the selected engine is named in the table heading.}} +\end{minipage} diff --git a/pyamplicol/src/pyamplicol/_profiling_campaign/result_matrix_on_the_fly_builtin_sm_lc_table.tex b/pyamplicol/src/pyamplicol/_profiling_campaign/result_matrix_on_the_fly_builtin_sm_lc_table.tex new file mode 100644 index 0000000000..5c2aa0a1bd --- /dev/null +++ b/pyamplicol/src/pyamplicol/_profiling_campaign/result_matrix_on_the_fly_builtin_sm_lc_table.tex @@ -0,0 +1,208 @@ +% SPDX-License-Identifier: 0BSD +% Generated by tools/performance_report/render.py; do not edit. +\providecommand{\matrixentryfont}{\fontsize{6.8pt}{7.8pt}\selectfont} +\providecommand{\matrixentryfontlc}{\fontsize{6.5pt}{7.5pt}\selectfont} +\providecommand{\matrixsummaryfont}{\fontsize{6.2pt}{7.4pt}\selectfont} +\providecommand{\matrixfitwidth}[1]{\begingroup\sbox0{#1}\ifdim\wd0>\linewidth\resizebox{\linewidth}{!}{\usebox0}\else\makebox[\linewidth][c]{\usebox0}\fi\endgroup} +\providecommand{\matrixpunct}[1]{\textcolor{black}{\texttt{#1}}} +\providecommand{\matrixratio}[2]{\matrixpunct{(}\textcolor{#1}{\texttt{x#2}}\matrixpunct{)}} +\providecommand{\matrixna}[1]{\textcolor{#1}{\texttt{N/A}}} +\providecommand{\matrixnotapplicable}[1]{\textcolor{#1}{\textsc{not applicable}}} +\providecommand{\matrixnotexposed}[1]{\textcolor{#1}{\textsc{not exposed}}} +\providecommand{\matrixstaticna}[1]{\textcolor{#1}{\textsc{static N/A}}} +\providecommand{\matrixtotalevaluator}[1]{\textcolor{ReportBlue}{\texttt{T }}#1} +\providecommand{\matrixrecurrencecore}[1]{\textcolor{ReportOrange}{\texttt{C }}#1} +\providecommand{\matrixzrecurrenceclocks}[2]{\shortstack[r]{#1\\#2}} +\providecommand{\matrixwallclock}[2]{\textcolor{#1}{\texttt{W #2}}} +\providecommand{\matrixwallabsolute}[1]{\textcolor{ReportBlue}{\texttt{W }}#1} +\providecommand{\matrixruntimepair}[2]{\shortstack[l]{#1\\#2}} +\providecommand{\matrixruntimetriple}[3]{\shortstack[l]{#1\\#2\matrixpunct{ | }#3}} +\providecommand{\matrixbelow}[1]{\textcolor{ReportMuted}{\texttt{<#1}}} +\providecommand{\matrixnaratio}[1]{\matrixpunct{(}\matrixna{#1}\matrixpunct{)}} +\providecommand{\matrixstatus}[2]{\textcolor{#1}{\textsc{#2}}} +\providecommand{\matrixncabsolute}[1]{#1} +\providecommand{\matrixsummaryratio}[2]{\textcolor{#1}{\texttt{x#2}}} +\providecommand{\matrixsummaryratiohighlight}[2]{\begingroup\setlength{\fboxsep}{0.9pt}\setlength{\fboxrule}{0.35pt}\fbox{\textcolor{#1}{{\usefont{T1}{lmtt}{b}{n}x#2}}}\endgroup} +\providecommand{\matrixsummarystats}[5]{\begingroup\matrixsummaryfont\begin{tabular}[t]{@{}l@{\hspace{0.014in}\matrixpunct{|}\hspace{0.014in}}l@{\hspace{0.014in}\matrixpunct{|}\hspace{0.014in}}l@{\hspace{0.014in}\matrixpunct{|}\hspace{0.014in}}l@{\hspace{0.014in}\matrixpunct{|}\hspace{0.014in}}l@{}}\makebox[3.6em][l]{#1}&\makebox[4.6em][l]{#2}&\makebox[3.6em][l]{#3}&\makebox[3.6em][l]{#4}&\makebox[4.2em][l]{#5}\end{tabular}\endgroup} +\providecommand{\matrixsummaryworkloads}[2]{\begin{tabular}[t]{@{}l@{}}#1\\[-0.10em]#2\end{tabular}} +\providecommand{\matrixpair}[2]{\begin{tabular}[t]{@{}r@{\hspace{0.025in}\matrixpunct{|}\hspace{0.025in}}r@{}}#1\end{tabular}} +\providecommand{\bestmoderatio}[2]{\textcolor{#1}{\texttt{x#2}}} +\providecommand{\bestmodecompactratio}[3]{\matrixpunct{(}\textcolor{ReportMuted}{\texttt{[x#1]}}\hspace{0.04in}\textcolor{#2}{\texttt{x#3}}\matrixpunct{)}} +\providecommand{\bestmodewallratio}[2]{\matrixpunct{(}\textcolor{#1}{\texttt{x#2}}\matrixpunct{)}} +\providecommand{\bestmodecompactprefix}[1]{\matrixpunct{(}\textcolor{ReportMuted}{\texttt{[x#1]}}\hspace{0.04in}} +\providecommand{\bestmodeopenprefix}{\matrixpunct{(}\hspace{0.04in}} +\providecommand{\bestmodeprimaryratio}[2]{\textcolor{#1}{\texttt{x#2}}\matrixpunct{)}} +\providecommand{\bestmodeabsoluteprefix}[1]{#1\hspace{0.04in}} +\providecommand{\bestmodecompactabsolute}[2]{\textcolor{ReportMuted}{\texttt{[#1]}}\hspace{0.04in}\textcolor{black}{\texttt{#2}}} +\providecommand{\bestmodecompactabsoluteprefix}[1]{\textcolor{ReportMuted}{\texttt{[#1]}}\hspace{0.04in}} +\providecommand{\bestmodeabsoluteprimary}[1]{\textcolor{black}{\texttt{#1}}} +\providecommand{\bestmodeunverifiedclockprefix}[2]{\textcolor{ReportMuted}{\texttt{[}#1\texttt{]}}\hspace{0.04in}#2\hspace{0.04in}} +\clearpage +\subsection{Built-in SM on-the-fly JIT O2 versus AmpliCol LC} +\noindent\begin{minipage}{\linewidth} +\subsubsection*{Built-in SM on-the-fly JIT O2 versus AmpliCol LC (block 1 of 2)} +\begingroup +\centering +\footnotesize +\setlength{\tabcolsep}{2.1pt} +\renewcommand{\arraystretch}{1.00} +\matrixfitwidth{% +\begin{tabular}{@{}rL{1.45in}l>{\matrixentryfontlc}l>{\matrixentryfontlc}c>{\matrixentryfontlc}l>{\matrixentryfontlc}r@{}>{\matrixentryfontlc}l>{\matrixentryfontlc}c>{\matrixentryfontlc}r@{}>{\matrixentryfontlc}l>{\matrixentryfontlc}l>{\matrixentryfontlc}c>{\matrixentryfontlc}l>{\matrixentryfontlc}r@{}>{\matrixentryfontlc}l>{\matrixentryfontlc}c>{\matrixentryfontlc}r@{}>{\matrixentryfontlc}l@{}} +\toprule +\textbf{ID} & \textbf{base process} & \textbf{metric} & \multicolumn{8}{c}{\textbf{n=1}} & \multicolumn{8}{c}{\textbf{n=2}} \\ +\specialrule{0.85pt}{0pt}{0pt} +\rowcolor{refblue} +\texttt{1} & $d\bar d\to Z+(n-1)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{2} & $u\bar d\to W^++(n-1)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{3} & $d\bar d\to e^+e^-+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{8}{c}{} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{4} & $u\bar d\to e^+\nu_e+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{8}{c}{} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{5} & $d\bar d\to ZZ+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{8}{c}{} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{6} & $gg\to t\bar t+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{8}{c}{} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{7} & $d\bar d\to t\bar t+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{8}{c}{} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{8} & $gg\to gg+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{8}{c}{} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{9} & $d\bar d\to ZZZ+(n-3)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{8}{c}{} & \multicolumn{8}{c}{} \\ +\addlinespace[0.05em] +\texttt{10} & $d\bar d\to e^+e^-ZH+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{8}{c}{} & \multicolumn{8}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{11} & $d\bar d\to t\bar t ZH+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{8}{c}{} & \multicolumn{8}{c}{} \\ +\addlinespace[0.05em] +\texttt{12} & $d\bar d\to e^+e^-e^+e^-+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{8}{c}{} & \multicolumn{8}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{13} & $d\bar d\to u\bar u\,s\bar s+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{8}{c}{} & \multicolumn{8}{c}{} \\ +\addlinespace[0.05em] +\texttt{14} & $d\bar d\to u\bar u\,s\bar s\,c\bar c+(n-6)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{8}{c}{} & \multicolumn{8}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{15} & $d\bar d\to u\bar u\,u\bar u+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{8}{c}{} & \multicolumn{8}{c}{} \\ +\addlinespace[0.05em] +\specialrule{1.05pt}{0.22em}{0.18em} +\multicolumn{3}{@{}l}{} & \multicolumn{8}{c}{\cellcolor{refblue}\textbf{n=1}} & \multicolumn{8}{c}{\textbf{n=2}} \\ +\multicolumn{3}{@{}l}{\textbf{summary: generation}} & \multicolumn{8}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} & \multicolumn{8}{c}{\matrixna{ReportMuted}} \\[0.08em] +\multicolumn{3}{@{}l}{\matrixsummaryworkloads{\textbf{summary: run single-flow, hel. sum}}{\textbf{summary: run all-flows, single hel.}}} & \multicolumn{8}{c}{\cellcolor{refblue}\matrixsummaryworkloads{\matrixna{ReportMuted}}{\matrixna{ReportMuted}}} & \multicolumn{8}{c}{\matrixsummaryworkloads{\matrixna{ReportMuted}}{\matrixna{ReportMuted}}} \\ +\bottomrule +\end{tabular} +} +\endgroup +\ReportTableNote{{\scriptsize Baseline: original AmpliCol; candidate: on-the-fly JIT O2. Each cell shows non-union-flow and union-flow quantities. For non-union flow, generation is ([xG] x(G+W)): the muted inner multiplier is artifact generation G alone and the normally coloured outer multiplier adds exactly the first cold on-the-fly evaluation W, both relative to AmpliCol selected-flow generation. For union flow, generation is the absolute [G] G+W pair, with G muted and G+W in normal black. W excludes artifact loading and conventional benchmark warm-ups. Runtime comparisons use ([xS]xW). The muted supplementary xS first compares compatible exposed execution attributions; otherwise it compares authenticated evaluator totals. Only when original AmpliCol has no evaluator-total clock may a candidate evaluator total use the legacy direct execution clock (or its wall clock when direct execution is absent) as denominator. An evaluator total is never divided by a recurrence core. This evaluator-total fallback is diagnostic only; the execution/execution ratio remains the compatible supplementary attribution. The coloured xW is always the candidate/baseline wall multiplier and remains the primary figure of merit. When no compatible supplementary clock exists, only (xW) is shown. No clock is fabricated from another. Every displayed number uses exactly three significant digits. Original AmpliCol is static N/A beyond three open quark lines; unavailable, terminal, or unlinked baselines keep their status while successful candidate timings remain absolute and leave summaries. Summary rows contain multipliers only in min, max, median, average, and weighted-average order; the weighted average is the ratio of timing sums and is the framed bold entry. LC generation uses selected flow only, while LC runtime shows separate selected-flow and all-flow wall-only lines. Not applicable marks a multiplicity for which the process family does not exist. Not run marks an otherwise defined process that was intentionally excluded from the declared measurement scope. Fixed-engine tables intentionally omit mode letters because the selected engine is named in the table heading.}} +\end{minipage} +\clearpage +\noindent\begin{minipage}{\linewidth} +\subsubsection*{Built-in SM on-the-fly JIT O2 versus AmpliCol LC (block 2 of 2)} +\begingroup +\centering +\footnotesize +\setlength{\tabcolsep}{2.1pt} +\renewcommand{\arraystretch}{1.00} +\matrixfitwidth{% +\begin{tabular}{@{}rL{1.45in}l>{\matrixentryfontlc}l>{\matrixentryfontlc}c>{\matrixentryfontlc}l>{\matrixentryfontlc}r@{}>{\matrixentryfontlc}l>{\matrixentryfontlc}c>{\matrixentryfontlc}r@{}>{\matrixentryfontlc}l>{\matrixentryfontlc}l>{\matrixentryfontlc}c>{\matrixentryfontlc}l>{\matrixentryfontlc}r@{}>{\matrixentryfontlc}l>{\matrixentryfontlc}c>{\matrixentryfontlc}r@{}>{\matrixentryfontlc}l@{}} +\toprule +\textbf{ID} & \textbf{base process} & \textbf{metric} & \multicolumn{8}{c}{\textbf{n=3}} & \multicolumn{8}{c}{\textbf{n=4}} \\ +\specialrule{0.85pt}{0pt}{0pt} +\rowcolor{refblue} +\texttt{1} & $d\bar d\to Z+(n-1)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{2} & $u\bar d\to W^++(n-1)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{3} & $d\bar d\to e^+e^-+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{4} & $u\bar d\to e^+\nu_e+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{5} & $d\bar d\to ZZ+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{6} & $gg\to t\bar t+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{7} & $d\bar d\to t\bar t+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{8} & $gg\to gg+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{9} & $d\bar d\to ZZZ+(n-3)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{10} & $d\bar d\to e^+e^-ZH+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{8}{c}{} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{11} & $d\bar d\to t\bar t ZH+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{8}{c}{} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{12} & $d\bar d\to e^+e^-e^+e^-+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{8}{c}{} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{13} & $d\bar d\to u\bar u\,s\bar s+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{8}{c}{} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{14} & $d\bar d\to u\bar u\,s\bar s\,c\bar c+(n-6)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{8}{c}{} & \multicolumn{8}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{15} & $d\bar d\to u\bar u\,u\bar u+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{8}{c}{} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\specialrule{1.05pt}{0.22em}{0.18em} +\multicolumn{3}{@{}l}{} & \multicolumn{8}{c}{\cellcolor{refblue}\textbf{n=3}} & \multicolumn{8}{c}{\textbf{n=4}} \\ +\multicolumn{3}{@{}l}{\textbf{summary: generation}} & \multicolumn{8}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} & \multicolumn{8}{c}{\matrixna{ReportMuted}} \\[0.08em] +\multicolumn{3}{@{}l}{\matrixsummaryworkloads{\textbf{summary: run single-flow, hel. sum}}{\textbf{summary: run all-flows, single hel.}}} & \multicolumn{8}{c}{\cellcolor{refblue}\matrixsummaryworkloads{\matrixna{ReportMuted}}{\matrixna{ReportMuted}}} & \multicolumn{8}{c}{\matrixsummaryworkloads{\matrixna{ReportMuted}}{\matrixna{ReportMuted}}} \\ +\bottomrule +\end{tabular} +} +\endgroup +\ReportTableNote{{\scriptsize Baseline: original AmpliCol; candidate: on-the-fly JIT O2. Each cell shows non-union-flow and union-flow quantities. For non-union flow, generation is ([xG] x(G+W)): the muted inner multiplier is artifact generation G alone and the normally coloured outer multiplier adds exactly the first cold on-the-fly evaluation W, both relative to AmpliCol selected-flow generation. For union flow, generation is the absolute [G] G+W pair, with G muted and G+W in normal black. W excludes artifact loading and conventional benchmark warm-ups. Runtime comparisons use ([xS]xW). The muted supplementary xS first compares compatible exposed execution attributions; otherwise it compares authenticated evaluator totals. Only when original AmpliCol has no evaluator-total clock may a candidate evaluator total use the legacy direct execution clock (or its wall clock when direct execution is absent) as denominator. An evaluator total is never divided by a recurrence core. This evaluator-total fallback is diagnostic only; the execution/execution ratio remains the compatible supplementary attribution. The coloured xW is always the candidate/baseline wall multiplier and remains the primary figure of merit. When no compatible supplementary clock exists, only (xW) is shown. No clock is fabricated from another. Every displayed number uses exactly three significant digits. Original AmpliCol is static N/A beyond three open quark lines; unavailable, terminal, or unlinked baselines keep their status while successful candidate timings remain absolute and leave summaries. Summary rows contain multipliers only in min, max, median, average, and weighted-average order; the weighted average is the ratio of timing sums and is the framed bold entry. LC generation uses selected flow only, while LC runtime shows separate selected-flow and all-flow wall-only lines. Not applicable marks a multiplicity for which the process family does not exist. Not run marks an otherwise defined process that was intentionally excluded from the declared measurement scope. Fixed-engine tables intentionally omit mode letters because the selected engine is named in the table heading.}} +\end{minipage} diff --git a/pyamplicol/src/pyamplicol/_profiling_campaign/result_matrix_on_the_fly_builtin_sm_nlc_table.tex b/pyamplicol/src/pyamplicol/_profiling_campaign/result_matrix_on_the_fly_builtin_sm_nlc_table.tex new file mode 100644 index 0000000000..8daf768bf4 --- /dev/null +++ b/pyamplicol/src/pyamplicol/_profiling_campaign/result_matrix_on_the_fly_builtin_sm_nlc_table.tex @@ -0,0 +1,208 @@ +% SPDX-License-Identifier: 0BSD +% Generated by tools/performance_report/render.py; do not edit. +\providecommand{\matrixentryfont}{\fontsize{6.8pt}{7.8pt}\selectfont} +\providecommand{\matrixentryfontlc}{\fontsize{6.5pt}{7.5pt}\selectfont} +\providecommand{\matrixsummaryfont}{\fontsize{6.2pt}{7.4pt}\selectfont} +\providecommand{\matrixfitwidth}[1]{\begingroup\sbox0{#1}\ifdim\wd0>\linewidth\resizebox{\linewidth}{!}{\usebox0}\else\makebox[\linewidth][c]{\usebox0}\fi\endgroup} +\providecommand{\matrixpunct}[1]{\textcolor{black}{\texttt{#1}}} +\providecommand{\matrixratio}[2]{\matrixpunct{(}\textcolor{#1}{\texttt{x#2}}\matrixpunct{)}} +\providecommand{\matrixna}[1]{\textcolor{#1}{\texttt{N/A}}} +\providecommand{\matrixnotapplicable}[1]{\textcolor{#1}{\textsc{not applicable}}} +\providecommand{\matrixnotexposed}[1]{\textcolor{#1}{\textsc{not exposed}}} +\providecommand{\matrixstaticna}[1]{\textcolor{#1}{\textsc{static N/A}}} +\providecommand{\matrixtotalevaluator}[1]{\textcolor{ReportBlue}{\texttt{T }}#1} +\providecommand{\matrixrecurrencecore}[1]{\textcolor{ReportOrange}{\texttt{C }}#1} +\providecommand{\matrixzrecurrenceclocks}[2]{\shortstack[r]{#1\\#2}} +\providecommand{\matrixwallclock}[2]{\textcolor{#1}{\texttt{W #2}}} +\providecommand{\matrixwallabsolute}[1]{\textcolor{ReportBlue}{\texttt{W }}#1} +\providecommand{\matrixruntimepair}[2]{\shortstack[l]{#1\\#2}} +\providecommand{\matrixruntimetriple}[3]{\shortstack[l]{#1\\#2\matrixpunct{ | }#3}} +\providecommand{\matrixbelow}[1]{\textcolor{ReportMuted}{\texttt{<#1}}} +\providecommand{\matrixnaratio}[1]{\matrixpunct{(}\matrixna{#1}\matrixpunct{)}} +\providecommand{\matrixstatus}[2]{\textcolor{#1}{\textsc{#2}}} +\providecommand{\matrixncabsolute}[1]{#1} +\providecommand{\matrixsummaryratio}[2]{\textcolor{#1}{\texttt{x#2}}} +\providecommand{\matrixsummaryratiohighlight}[2]{\begingroup\setlength{\fboxsep}{0.9pt}\setlength{\fboxrule}{0.35pt}\fbox{\textcolor{#1}{{\usefont{T1}{lmtt}{b}{n}x#2}}}\endgroup} +\providecommand{\matrixsummarystats}[5]{\begingroup\matrixsummaryfont\begin{tabular}[t]{@{}l@{\hspace{0.014in}\matrixpunct{|}\hspace{0.014in}}l@{\hspace{0.014in}\matrixpunct{|}\hspace{0.014in}}l@{\hspace{0.014in}\matrixpunct{|}\hspace{0.014in}}l@{\hspace{0.014in}\matrixpunct{|}\hspace{0.014in}}l@{}}\makebox[3.6em][l]{#1}&\makebox[4.6em][l]{#2}&\makebox[3.6em][l]{#3}&\makebox[3.6em][l]{#4}&\makebox[4.2em][l]{#5}\end{tabular}\endgroup} +\providecommand{\matrixsummaryworkloads}[2]{\begin{tabular}[t]{@{}l@{}}#1\\[-0.10em]#2\end{tabular}} +\providecommand{\matrixpair}[2]{\begin{tabular}[t]{@{}r@{\hspace{0.025in}\matrixpunct{|}\hspace{0.025in}}r@{}}#1\end{tabular}} +\providecommand{\bestmoderatio}[2]{\textcolor{#1}{\texttt{x#2}}} +\providecommand{\bestmodecompactratio}[3]{\matrixpunct{(}\textcolor{ReportMuted}{\texttt{[x#1]}}\hspace{0.04in}\textcolor{#2}{\texttt{x#3}}\matrixpunct{)}} +\providecommand{\bestmodewallratio}[2]{\matrixpunct{(}\textcolor{#1}{\texttt{x#2}}\matrixpunct{)}} +\providecommand{\bestmodecompactprefix}[1]{\matrixpunct{(}\textcolor{ReportMuted}{\texttt{[x#1]}}\hspace{0.04in}} +\providecommand{\bestmodeopenprefix}{\matrixpunct{(}\hspace{0.04in}} +\providecommand{\bestmodeprimaryratio}[2]{\textcolor{#1}{\texttt{x#2}}\matrixpunct{)}} +\providecommand{\bestmodeabsoluteprefix}[1]{#1\hspace{0.04in}} +\providecommand{\bestmodecompactabsolute}[2]{\textcolor{ReportMuted}{\texttt{[#1]}}\hspace{0.04in}\textcolor{black}{\texttt{#2}}} +\providecommand{\bestmodecompactabsoluteprefix}[1]{\textcolor{ReportMuted}{\texttt{[#1]}}\hspace{0.04in}} +\providecommand{\bestmodeabsoluteprimary}[1]{\textcolor{black}{\texttt{#1}}} +\providecommand{\bestmodeunverifiedclockprefix}[2]{\textcolor{ReportMuted}{\texttt{[}#1\texttt{]}}\hspace{0.04in}#2\hspace{0.04in}} +\clearpage +\subsection{Built-in SM on-the-fly JIT O2 versus AmpliCol NLC} +\noindent\begin{minipage}{\linewidth} +\subsubsection*{Built-in SM on-the-fly JIT O2 versus AmpliCol NLC (block 1 of 2)} +\begingroup +\centering +\footnotesize +\setlength{\tabcolsep}{2.1pt} +\renewcommand{\arraystretch}{1.04} +\matrixfitwidth{% +\begin{tabular}{@{}rL{1.45in}l>{\matrixentryfont}l>{\matrixentryfont}r@{}>{\matrixentryfont}l>{\matrixentryfont}l>{\matrixentryfont}r@{}>{\matrixentryfont}l>{\matrixentryfont}l>{\matrixentryfont}r@{}>{\matrixentryfont}l@{}} +\toprule +\textbf{ID} & \textbf{base process} & \textbf{metric} & \multicolumn{3}{c}{\textbf{n=1}} & \multicolumn{3}{c}{\textbf{n=2}} & \multicolumn{3}{c}{\textbf{n=3}} \\ +\specialrule{0.85pt}{0pt}{0pt} +\rowcolor{refblue} +\texttt{1} & $d\bar d\to Z+(n-1)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{2} & $u\bar d\to W^++(n-1)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{3} & $d\bar d\to e^+e^-+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{4} & $u\bar d\to e^+\nu_e+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{5} & $d\bar d\to ZZ+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{6} & $gg\to t\bar t+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{7} & $d\bar d\to t\bar t+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{8} & $gg\to gg+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{9} & $d\bar d\to ZZZ+(n-3)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{10} & $d\bar d\to e^+e^-ZH+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{11} & $d\bar d\to t\bar t ZH+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{12} & $d\bar d\to e^+e^-e^+e^-+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{13} & $d\bar d\to u\bar u\,s\bar s+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{14} & $d\bar d\to u\bar u\,s\bar s\,c\bar c+(n-6)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{15} & $d\bar d\to u\bar u\,u\bar u+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\specialrule{1.05pt}{0.22em}{0.18em} +\multicolumn{3}{@{}l}{} & \multicolumn{3}{c}{\cellcolor{refblue}\textbf{n=1}} & \multicolumn{3}{c}{\textbf{n=2}} & \multicolumn{3}{c}{\cellcolor{refblue}\textbf{n=3}} \\ +\multicolumn{3}{@{}l}{\textbf{summary: generation}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} & \multicolumn{3}{c}{\matrixna{ReportMuted}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} \\[0.08em] +\multicolumn{3}{@{}l}{\textbf{summary: wall}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} & \multicolumn{3}{c}{\matrixna{ReportMuted}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} \\ +\bottomrule +\end{tabular} +} +\endgroup +\ReportTableNote{{\scriptsize Baseline: original AmpliCol; candidate: on-the-fly JIT O2. Each cell shows the baseline generation and wall time, followed by candidate/baseline generation and wall-time ratios. Runtime comparisons use ([xS]xW). The muted supplementary xS first compares compatible exposed execution attributions; otherwise it compares authenticated evaluator totals. Only when original AmpliCol has no evaluator-total clock may a candidate evaluator total use the legacy direct execution clock (or its wall clock when direct execution is absent) as denominator. An evaluator total is never divided by a recurrence core. This evaluator-total fallback is diagnostic only; the execution/execution ratio remains the compatible supplementary attribution. The coloured xW is always the candidate/baseline wall multiplier and remains the primary figure of merit. When no compatible supplementary clock exists, only (xW) is shown. No clock is fabricated from another. Every displayed number uses exactly three significant digits. Original AmpliCol is static N/A beyond three open quark lines; unavailable, terminal, or unlinked baselines keep their status while successful candidate timings remain absolute and leave summaries. Summary rows contain multipliers only in min, max, median, average, and weighted-average order; the weighted average is the ratio of timing sums, is the framed bold entry, and runtime statistics use wall time only. Not applicable marks a multiplicity for which the process family does not exist. Not run marks an otherwise defined process that was intentionally excluded from the declared measurement scope. Fixed-engine tables intentionally omit mode letters because the selected engine is named in the table heading.}} +\end{minipage} +\clearpage +\noindent\begin{minipage}{\linewidth} +\subsubsection*{Built-in SM on-the-fly JIT O2 versus AmpliCol NLC (block 2 of 2)} +\begingroup +\centering +\footnotesize +\setlength{\tabcolsep}{2.1pt} +\renewcommand{\arraystretch}{1.04} +\matrixfitwidth{% +\begin{tabular}{@{}rL{1.45in}l>{\matrixentryfont}l>{\matrixentryfont}r@{}>{\matrixentryfont}l>{\matrixentryfont}l>{\matrixentryfont}r@{}>{\matrixentryfont}l>{\matrixentryfont}l>{\matrixentryfont}r@{}>{\matrixentryfont}l@{}} +\toprule +\textbf{ID} & \textbf{base process} & \textbf{metric} & \multicolumn{3}{c}{\textbf{n=4}} & \multicolumn{3}{c}{\textbf{n=5}} & \multicolumn{3}{c}{\textbf{n=6}} \\ +\specialrule{0.85pt}{0pt}{0pt} +\rowcolor{refblue} +\texttt{1} & $d\bar d\to Z+(n-1)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{2} & $u\bar d\to W^++(n-1)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{3} & $d\bar d\to e^+e^-+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{4} & $u\bar d\to e^+\nu_e+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{5} & $d\bar d\to ZZ+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{6} & $gg\to t\bar t+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{7} & $d\bar d\to t\bar t+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{8} & $gg\to gg+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{9} & $d\bar d\to ZZZ+(n-3)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{10} & $d\bar d\to e^+e^-ZH+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{11} & $d\bar d\to t\bar t ZH+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{12} & $d\bar d\to e^+e^-e^+e^-+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{13} & $d\bar d\to u\bar u\,s\bar s+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{14} & $d\bar d\to u\bar u\,s\bar s\,c\bar c+(n-6)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstaticna{ReportMuted} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \matrixstaticna{ReportMuted} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{15} & $d\bar d\to u\bar u\,u\bar u+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\specialrule{1.05pt}{0.22em}{0.18em} +\multicolumn{3}{@{}l}{} & \multicolumn{3}{c}{\cellcolor{refblue}\textbf{n=4}} & \multicolumn{3}{c}{\textbf{n=5}} & \multicolumn{3}{c}{\cellcolor{refblue}\textbf{n=6}} \\ +\multicolumn{3}{@{}l}{\textbf{summary: generation}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} & \multicolumn{3}{c}{\matrixna{ReportMuted}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} \\[0.08em] +\multicolumn{3}{@{}l}{\textbf{summary: wall}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} & \multicolumn{3}{c}{\matrixna{ReportMuted}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} \\ +\bottomrule +\end{tabular} +} +\endgroup +\ReportTableNote{{\scriptsize Baseline: original AmpliCol; candidate: on-the-fly JIT O2. Each cell shows the baseline generation and wall time, followed by candidate/baseline generation and wall-time ratios. Runtime comparisons use ([xS]xW). The muted supplementary xS first compares compatible exposed execution attributions; otherwise it compares authenticated evaluator totals. Only when original AmpliCol has no evaluator-total clock may a candidate evaluator total use the legacy direct execution clock (or its wall clock when direct execution is absent) as denominator. An evaluator total is never divided by a recurrence core. This evaluator-total fallback is diagnostic only; the execution/execution ratio remains the compatible supplementary attribution. The coloured xW is always the candidate/baseline wall multiplier and remains the primary figure of merit. When no compatible supplementary clock exists, only (xW) is shown. No clock is fabricated from another. Every displayed number uses exactly three significant digits. Original AmpliCol is static N/A beyond three open quark lines; unavailable, terminal, or unlinked baselines keep their status while successful candidate timings remain absolute and leave summaries. Summary rows contain multipliers only in min, max, median, average, and weighted-average order; the weighted average is the ratio of timing sums, is the framed bold entry, and runtime statistics use wall time only. Not applicable marks a multiplicity for which the process family does not exist. Not run marks an otherwise defined process that was intentionally excluded from the declared measurement scope. Fixed-engine tables intentionally omit mode letters because the selected engine is named in the table heading.}} +\end{minipage} diff --git a/pyamplicol/src/pyamplicol/_profiling_campaign/result_matrix_on_the_fly_ufo_sm_full_vs_madgraph_table.tex b/pyamplicol/src/pyamplicol/_profiling_campaign/result_matrix_on_the_fly_ufo_sm_full_vs_madgraph_table.tex new file mode 100644 index 0000000000..ba6f691dd1 --- /dev/null +++ b/pyamplicol/src/pyamplicol/_profiling_campaign/result_matrix_on_the_fly_ufo_sm_full_vs_madgraph_table.tex @@ -0,0 +1,208 @@ +% SPDX-License-Identifier: 0BSD +% Generated by tools/performance_report/render.py; do not edit. +\providecommand{\matrixentryfont}{\fontsize{6.8pt}{7.8pt}\selectfont} +\providecommand{\matrixentryfontlc}{\fontsize{6.5pt}{7.5pt}\selectfont} +\providecommand{\matrixsummaryfont}{\fontsize{6.2pt}{7.4pt}\selectfont} +\providecommand{\matrixfitwidth}[1]{\begingroup\sbox0{#1}\ifdim\wd0>\linewidth\resizebox{\linewidth}{!}{\usebox0}\else\makebox[\linewidth][c]{\usebox0}\fi\endgroup} +\providecommand{\matrixpunct}[1]{\textcolor{black}{\texttt{#1}}} +\providecommand{\matrixratio}[2]{\matrixpunct{(}\textcolor{#1}{\texttt{x#2}}\matrixpunct{)}} +\providecommand{\matrixna}[1]{\textcolor{#1}{\texttt{N/A}}} +\providecommand{\matrixnotapplicable}[1]{\textcolor{#1}{\textsc{not applicable}}} +\providecommand{\matrixnotexposed}[1]{\textcolor{#1}{\textsc{not exposed}}} +\providecommand{\matrixstaticna}[1]{\textcolor{#1}{\textsc{static N/A}}} +\providecommand{\matrixtotalevaluator}[1]{\textcolor{ReportBlue}{\texttt{T }}#1} +\providecommand{\matrixrecurrencecore}[1]{\textcolor{ReportOrange}{\texttt{C }}#1} +\providecommand{\matrixzrecurrenceclocks}[2]{\shortstack[r]{#1\\#2}} +\providecommand{\matrixwallclock}[2]{\textcolor{#1}{\texttt{W #2}}} +\providecommand{\matrixwallabsolute}[1]{\textcolor{ReportBlue}{\texttt{W }}#1} +\providecommand{\matrixruntimepair}[2]{\shortstack[l]{#1\\#2}} +\providecommand{\matrixruntimetriple}[3]{\shortstack[l]{#1\\#2\matrixpunct{ | }#3}} +\providecommand{\matrixbelow}[1]{\textcolor{ReportMuted}{\texttt{<#1}}} +\providecommand{\matrixnaratio}[1]{\matrixpunct{(}\matrixna{#1}\matrixpunct{)}} +\providecommand{\matrixstatus}[2]{\textcolor{#1}{\textsc{#2}}} +\providecommand{\matrixncabsolute}[1]{#1} +\providecommand{\matrixsummaryratio}[2]{\textcolor{#1}{\texttt{x#2}}} +\providecommand{\matrixsummaryratiohighlight}[2]{\begingroup\setlength{\fboxsep}{0.9pt}\setlength{\fboxrule}{0.35pt}\fbox{\textcolor{#1}{{\usefont{T1}{lmtt}{b}{n}x#2}}}\endgroup} +\providecommand{\matrixsummarystats}[5]{\begingroup\matrixsummaryfont\begin{tabular}[t]{@{}l@{\hspace{0.014in}\matrixpunct{|}\hspace{0.014in}}l@{\hspace{0.014in}\matrixpunct{|}\hspace{0.014in}}l@{\hspace{0.014in}\matrixpunct{|}\hspace{0.014in}}l@{\hspace{0.014in}\matrixpunct{|}\hspace{0.014in}}l@{}}\makebox[3.6em][l]{#1}&\makebox[4.6em][l]{#2}&\makebox[3.6em][l]{#3}&\makebox[3.6em][l]{#4}&\makebox[4.2em][l]{#5}\end{tabular}\endgroup} +\providecommand{\matrixsummaryworkloads}[2]{\begin{tabular}[t]{@{}l@{}}#1\\[-0.10em]#2\end{tabular}} +\providecommand{\matrixpair}[2]{\begin{tabular}[t]{@{}r@{\hspace{0.025in}\matrixpunct{|}\hspace{0.025in}}r@{}}#1\end{tabular}} +\providecommand{\bestmoderatio}[2]{\textcolor{#1}{\texttt{x#2}}} +\providecommand{\bestmodecompactratio}[3]{\matrixpunct{(}\textcolor{ReportMuted}{\texttt{[x#1]}}\hspace{0.04in}\textcolor{#2}{\texttt{x#3}}\matrixpunct{)}} +\providecommand{\bestmodewallratio}[2]{\matrixpunct{(}\textcolor{#1}{\texttt{x#2}}\matrixpunct{)}} +\providecommand{\bestmodecompactprefix}[1]{\matrixpunct{(}\textcolor{ReportMuted}{\texttt{[x#1]}}\hspace{0.04in}} +\providecommand{\bestmodeopenprefix}{\matrixpunct{(}\hspace{0.04in}} +\providecommand{\bestmodeprimaryratio}[2]{\textcolor{#1}{\texttt{x#2}}\matrixpunct{)}} +\providecommand{\bestmodeabsoluteprefix}[1]{#1\hspace{0.04in}} +\providecommand{\bestmodecompactabsolute}[2]{\textcolor{ReportMuted}{\texttt{[#1]}}\hspace{0.04in}\textcolor{black}{\texttt{#2}}} +\providecommand{\bestmodecompactabsoluteprefix}[1]{\textcolor{ReportMuted}{\texttt{[#1]}}\hspace{0.04in}} +\providecommand{\bestmodeabsoluteprimary}[1]{\textcolor{black}{\texttt{#1}}} +\providecommand{\bestmodeunverifiedclockprefix}[2]{\textcolor{ReportMuted}{\texttt{[}#1\texttt{]}}\hspace{0.04in}#2\hspace{0.04in}} +\clearpage +\subsection{UFO-SM on-the-fly JIT O2 versus MadGraph full-colour} +\noindent\begin{minipage}{\linewidth} +\subsubsection*{UFO-SM on-the-fly JIT O2 versus MadGraph full-colour (block 1 of 2)} +\begingroup +\centering +\footnotesize +\setlength{\tabcolsep}{2.1pt} +\renewcommand{\arraystretch}{1.04} +\matrixfitwidth{% +\begin{tabular}{@{}rL{1.45in}l>{\matrixentryfont}l>{\matrixentryfont}r@{}>{\matrixentryfont}l>{\matrixentryfont}l>{\matrixentryfont}r@{}>{\matrixentryfont}l>{\matrixentryfont}l>{\matrixentryfont}r@{}>{\matrixentryfont}l@{}} +\toprule +\textbf{ID} & \textbf{base process} & \textbf{metric} & \multicolumn{3}{c}{\textbf{n=1}} & \multicolumn{3}{c}{\textbf{n=2}} & \multicolumn{3}{c}{\textbf{n=3}} \\ +\specialrule{0.85pt}{0pt}{0pt} +\rowcolor{refblue} +\texttt{1} & $d\bar d\to Z+(n-1)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{2} & $u\bar d\to W^++(n-1)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{3} & $d\bar d\to e^+e^-+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{4} & $u\bar d\to e^+\nu_e+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{5} & $d\bar d\to ZZ+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{6} & $gg\to t\bar t+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{7} & $d\bar d\to t\bar t+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{8} & $gg\to gg+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{9} & $d\bar d\to ZZZ+(n-3)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{10} & $d\bar d\to e^+e^-ZH+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{11} & $d\bar d\to t\bar t ZH+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{12} & $d\bar d\to e^+e^-e^+e^-+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{13} & $d\bar d\to u\bar u\,s\bar s+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{14} & $d\bar d\to u\bar u\,s\bar s\,c\bar c+(n-6)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{15} & $d\bar d\to u\bar u\,u\bar u+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\specialrule{1.05pt}{0.22em}{0.18em} +\multicolumn{3}{@{}l}{} & \multicolumn{3}{c}{\cellcolor{refblue}\textbf{n=1}} & \multicolumn{3}{c}{\textbf{n=2}} & \multicolumn{3}{c}{\cellcolor{refblue}\textbf{n=3}} \\ +\multicolumn{3}{@{}l}{\textbf{summary: generation}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} & \multicolumn{3}{c}{\matrixna{ReportMuted}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} \\[0.08em] +\multicolumn{3}{@{}l}{\textbf{summary: wall}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} & \multicolumn{3}{c}{\matrixna{ReportMuted}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} \\ +\bottomrule +\end{tabular} +} +\endgroup +\ReportTableNote{{\scriptsize Baseline: MadGraph standalone; candidate: on-the-fly JIT O2. Each cell shows the baseline generation and wall time, followed by candidate/baseline generation and wall-time ratios. Runtime comparisons use ([xS]xW). The muted supplementary xS first compares compatible exposed execution attributions; otherwise it compares authenticated evaluator totals. Only when original AmpliCol has no evaluator-total clock may a candidate evaluator total use the legacy direct execution clock (or its wall clock when direct execution is absent) as denominator. An evaluator total is never divided by a recurrence core. This evaluator-total fallback is diagnostic only; the execution/execution ratio remains the compatible supplementary attribution. The coloured xW is always the candidate/baseline wall multiplier and remains the primary figure of merit. When no compatible supplementary clock exists, only (xW) is shown. No clock is fabricated from another. Every displayed number uses exactly three significant digits. MadGraph standalone is the authoritative full-colour numerical reference; its generation and repeated-evaluation measurements are shared by all four UFO-SM comparison views. Recurrence, compiled, and eager candidates use p200 at the comparison point; the on-the-fly candidate uses its supported p16 lane. Ratios and summaries require exact stored linkage to the current denominator. Summary rows contain multipliers only in min, max, median, average, and weighted-average order; the weighted average is the ratio of timing sums, is the framed bold entry, and runtime statistics use wall time only. Not applicable marks a multiplicity for which the process family does not exist. Not run marks an otherwise defined process that was intentionally excluded from the declared measurement scope. Fixed-engine tables intentionally omit mode letters because the selected engine is named in the table heading.}} +\end{minipage} +\clearpage +\noindent\begin{minipage}{\linewidth} +\subsubsection*{UFO-SM on-the-fly JIT O2 versus MadGraph full-colour (block 2 of 2)} +\begingroup +\centering +\footnotesize +\setlength{\tabcolsep}{2.1pt} +\renewcommand{\arraystretch}{1.04} +\matrixfitwidth{% +\begin{tabular}{@{}rL{1.45in}l>{\matrixentryfont}l>{\matrixentryfont}r@{}>{\matrixentryfont}l>{\matrixentryfont}l>{\matrixentryfont}r@{}>{\matrixentryfont}l>{\matrixentryfont}l>{\matrixentryfont}r@{}>{\matrixentryfont}l@{}} +\toprule +\textbf{ID} & \textbf{base process} & \textbf{metric} & \multicolumn{3}{c}{\textbf{n=4}} & \multicolumn{3}{c}{\textbf{n=5}} & \multicolumn{3}{c}{\textbf{n=6}} \\ +\specialrule{0.85pt}{0pt}{0pt} +\rowcolor{refblue} +\texttt{1} & $d\bar d\to Z+(n-1)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{2} & $u\bar d\to W^++(n-1)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{3} & $d\bar d\to e^+e^-+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{4} & $u\bar d\to e^+\nu_e+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{5} & $d\bar d\to ZZ+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{6} & $gg\to t\bar t+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{7} & $d\bar d\to t\bar t+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{8} & $gg\to gg+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{9} & $d\bar d\to ZZZ+(n-3)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{10} & $d\bar d\to e^+e^-ZH+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{11} & $d\bar d\to t\bar t ZH+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{12} & $d\bar d\to e^+e^-e^+e^-+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{13} & $d\bar d\to u\bar u\,s\bar s+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{14} & $d\bar d\to u\bar u\,s\bar s\,c\bar c+(n-6)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{15} & $d\bar d\to u\bar u\,u\bar u+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\specialrule{1.05pt}{0.22em}{0.18em} +\multicolumn{3}{@{}l}{} & \multicolumn{3}{c}{\cellcolor{refblue}\textbf{n=4}} & \multicolumn{3}{c}{\textbf{n=5}} & \multicolumn{3}{c}{\cellcolor{refblue}\textbf{n=6}} \\ +\multicolumn{3}{@{}l}{\textbf{summary: generation}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} & \multicolumn{3}{c}{\matrixna{ReportMuted}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} \\[0.08em] +\multicolumn{3}{@{}l}{\textbf{summary: wall}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} & \multicolumn{3}{c}{\matrixna{ReportMuted}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} \\ +\bottomrule +\end{tabular} +} +\endgroup +\ReportTableNote{{\scriptsize Baseline: MadGraph standalone; candidate: on-the-fly JIT O2. Each cell shows the baseline generation and wall time, followed by candidate/baseline generation and wall-time ratios. Runtime comparisons use ([xS]xW). The muted supplementary xS first compares compatible exposed execution attributions; otherwise it compares authenticated evaluator totals. Only when original AmpliCol has no evaluator-total clock may a candidate evaluator total use the legacy direct execution clock (or its wall clock when direct execution is absent) as denominator. An evaluator total is never divided by a recurrence core. This evaluator-total fallback is diagnostic only; the execution/execution ratio remains the compatible supplementary attribution. The coloured xW is always the candidate/baseline wall multiplier and remains the primary figure of merit. When no compatible supplementary clock exists, only (xW) is shown. No clock is fabricated from another. Every displayed number uses exactly three significant digits. MadGraph standalone is the authoritative full-colour numerical reference; its generation and repeated-evaluation measurements are shared by all four UFO-SM comparison views. Recurrence, compiled, and eager candidates use p200 at the comparison point; the on-the-fly candidate uses its supported p16 lane. Ratios and summaries require exact stored linkage to the current denominator. Summary rows contain multipliers only in min, max, median, average, and weighted-average order; the weighted average is the ratio of timing sums, is the framed bold entry, and runtime statistics use wall time only. Not applicable marks a multiplicity for which the process family does not exist. Not run marks an otherwise defined process that was intentionally excluded from the declared measurement scope. Fixed-engine tables intentionally omit mode letters because the selected engine is named in the table heading.}} +\end{minipage} diff --git a/pyamplicol/src/pyamplicol/_profiling_campaign/result_matrix_recurrence_builtin_sm_full_table.tex b/pyamplicol/src/pyamplicol/_profiling_campaign/result_matrix_recurrence_builtin_sm_full_table.tex new file mode 100644 index 0000000000..14e6163be3 --- /dev/null +++ b/pyamplicol/src/pyamplicol/_profiling_campaign/result_matrix_recurrence_builtin_sm_full_table.tex @@ -0,0 +1,208 @@ +% SPDX-License-Identifier: 0BSD +% Generated by tools/performance_report/render.py; do not edit. +\providecommand{\matrixentryfont}{\fontsize{6.8pt}{7.8pt}\selectfont} +\providecommand{\matrixentryfontlc}{\fontsize{6.5pt}{7.5pt}\selectfont} +\providecommand{\matrixsummaryfont}{\fontsize{6.2pt}{7.4pt}\selectfont} +\providecommand{\matrixfitwidth}[1]{\begingroup\sbox0{#1}\ifdim\wd0>\linewidth\resizebox{\linewidth}{!}{\usebox0}\else\makebox[\linewidth][c]{\usebox0}\fi\endgroup} +\providecommand{\matrixpunct}[1]{\textcolor{black}{\texttt{#1}}} +\providecommand{\matrixratio}[2]{\matrixpunct{(}\textcolor{#1}{\texttt{x#2}}\matrixpunct{)}} +\providecommand{\matrixna}[1]{\textcolor{#1}{\texttt{N/A}}} +\providecommand{\matrixnotapplicable}[1]{\textcolor{#1}{\textsc{not applicable}}} +\providecommand{\matrixnotexposed}[1]{\textcolor{#1}{\textsc{not exposed}}} +\providecommand{\matrixstaticna}[1]{\textcolor{#1}{\textsc{static N/A}}} +\providecommand{\matrixtotalevaluator}[1]{\textcolor{ReportBlue}{\texttt{T }}#1} +\providecommand{\matrixrecurrencecore}[1]{\textcolor{ReportOrange}{\texttt{C }}#1} +\providecommand{\matrixzrecurrenceclocks}[2]{\shortstack[r]{#1\\#2}} +\providecommand{\matrixwallclock}[2]{\textcolor{#1}{\texttt{W #2}}} +\providecommand{\matrixwallabsolute}[1]{\textcolor{ReportBlue}{\texttt{W }}#1} +\providecommand{\matrixruntimepair}[2]{\shortstack[l]{#1\\#2}} +\providecommand{\matrixruntimetriple}[3]{\shortstack[l]{#1\\#2\matrixpunct{ | }#3}} +\providecommand{\matrixbelow}[1]{\textcolor{ReportMuted}{\texttt{<#1}}} +\providecommand{\matrixnaratio}[1]{\matrixpunct{(}\matrixna{#1}\matrixpunct{)}} +\providecommand{\matrixstatus}[2]{\textcolor{#1}{\textsc{#2}}} +\providecommand{\matrixncabsolute}[1]{#1} +\providecommand{\matrixsummaryratio}[2]{\textcolor{#1}{\texttt{x#2}}} +\providecommand{\matrixsummaryratiohighlight}[2]{\begingroup\setlength{\fboxsep}{0.9pt}\setlength{\fboxrule}{0.35pt}\fbox{\textcolor{#1}{{\usefont{T1}{lmtt}{b}{n}x#2}}}\endgroup} +\providecommand{\matrixsummarystats}[5]{\begingroup\matrixsummaryfont\begin{tabular}[t]{@{}l@{\hspace{0.014in}\matrixpunct{|}\hspace{0.014in}}l@{\hspace{0.014in}\matrixpunct{|}\hspace{0.014in}}l@{\hspace{0.014in}\matrixpunct{|}\hspace{0.014in}}l@{\hspace{0.014in}\matrixpunct{|}\hspace{0.014in}}l@{}}\makebox[3.6em][l]{#1}&\makebox[4.6em][l]{#2}&\makebox[3.6em][l]{#3}&\makebox[3.6em][l]{#4}&\makebox[4.2em][l]{#5}\end{tabular}\endgroup} +\providecommand{\matrixsummaryworkloads}[2]{\begin{tabular}[t]{@{}l@{}}#1\\[-0.10em]#2\end{tabular}} +\providecommand{\matrixpair}[2]{\begin{tabular}[t]{@{}r@{\hspace{0.025in}\matrixpunct{|}\hspace{0.025in}}r@{}}#1\end{tabular}} +\providecommand{\bestmoderatio}[2]{\textcolor{#1}{\texttt{x#2}}} +\providecommand{\bestmodecompactratio}[3]{\matrixpunct{(}\textcolor{ReportMuted}{\texttt{[x#1]}}\hspace{0.04in}\textcolor{#2}{\texttt{x#3}}\matrixpunct{)}} +\providecommand{\bestmodewallratio}[2]{\matrixpunct{(}\textcolor{#1}{\texttt{x#2}}\matrixpunct{)}} +\providecommand{\bestmodecompactprefix}[1]{\matrixpunct{(}\textcolor{ReportMuted}{\texttt{[x#1]}}\hspace{0.04in}} +\providecommand{\bestmodeopenprefix}{\matrixpunct{(}\hspace{0.04in}} +\providecommand{\bestmodeprimaryratio}[2]{\textcolor{#1}{\texttt{x#2}}\matrixpunct{)}} +\providecommand{\bestmodeabsoluteprefix}[1]{#1\hspace{0.04in}} +\providecommand{\bestmodecompactabsolute}[2]{\textcolor{ReportMuted}{\texttt{[#1]}}\hspace{0.04in}\textcolor{black}{\texttt{#2}}} +\providecommand{\bestmodecompactabsoluteprefix}[1]{\textcolor{ReportMuted}{\texttt{[#1]}}\hspace{0.04in}} +\providecommand{\bestmodeabsoluteprimary}[1]{\textcolor{black}{\texttt{#1}}} +\providecommand{\bestmodeunverifiedclockprefix}[2]{\textcolor{ReportMuted}{\texttt{[}#1\texttt{]}}\hspace{0.04in}#2\hspace{0.04in}} +\clearpage +\subsection{Built-in SM recurrence versus AmpliCol full-colour} +\noindent\begin{minipage}{\linewidth} +\subsubsection*{Built-in SM recurrence versus AmpliCol full-colour (block 1 of 2)} +\begingroup +\centering +\footnotesize +\setlength{\tabcolsep}{2.1pt} +\renewcommand{\arraystretch}{1.04} +\matrixfitwidth{% +\begin{tabular}{@{}rL{1.45in}l>{\matrixentryfont}l>{\matrixentryfont}r@{}>{\matrixentryfont}l>{\matrixentryfont}l>{\matrixentryfont}r@{}>{\matrixentryfont}l>{\matrixentryfont}l>{\matrixentryfont}r@{}>{\matrixentryfont}l@{}} +\toprule +\textbf{ID} & \textbf{base process} & \textbf{metric} & \multicolumn{3}{c}{\textbf{n=1}} & \multicolumn{3}{c}{\textbf{n=2}} & \multicolumn{3}{c}{\textbf{n=3}} \\ +\specialrule{0.85pt}{0pt}{0pt} +\rowcolor{refblue} +\texttt{1} & $d\bar d\to Z+(n-1)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{2} & $u\bar d\to W^++(n-1)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{3} & $d\bar d\to e^+e^-+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{4} & $u\bar d\to e^+\nu_e+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{5} & $d\bar d\to ZZ+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{6} & $gg\to t\bar t+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{7} & $d\bar d\to t\bar t+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{8} & $gg\to gg+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{9} & $d\bar d\to ZZZ+(n-3)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{10} & $d\bar d\to e^+e^-ZH+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{11} & $d\bar d\to t\bar t ZH+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{12} & $d\bar d\to e^+e^-e^+e^-+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{13} & $d\bar d\to u\bar u\,s\bar s+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{14} & $d\bar d\to u\bar u\,s\bar s\,c\bar c+(n-6)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{15} & $d\bar d\to u\bar u\,u\bar u+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\specialrule{1.05pt}{0.22em}{0.18em} +\multicolumn{3}{@{}l}{} & \multicolumn{3}{c}{\cellcolor{refblue}\textbf{n=1}} & \multicolumn{3}{c}{\textbf{n=2}} & \multicolumn{3}{c}{\cellcolor{refblue}\textbf{n=3}} \\ +\multicolumn{3}{@{}l}{\textbf{summary: generation}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} & \multicolumn{3}{c}{\matrixna{ReportMuted}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} \\[0.08em] +\multicolumn{3}{@{}l}{\textbf{summary: wall}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} & \multicolumn{3}{c}{\matrixna{ReportMuted}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} \\ +\bottomrule +\end{tabular} +} +\endgroup +\ReportTableNote{{\scriptsize Baseline: original AmpliCol; candidate: recurrence JIT O2. Each cell shows the baseline generation and wall time, followed by candidate/baseline generation and wall-time ratios. Runtime comparisons use ([xS]xW). The muted supplementary xS first compares compatible exposed execution attributions; otherwise it compares authenticated evaluator totals. Only when original AmpliCol has no evaluator-total clock may a candidate evaluator total use the legacy direct execution clock (or its wall clock when direct execution is absent) as denominator. An evaluator total is never divided by a recurrence core. This evaluator-total fallback is diagnostic only; the execution/execution ratio remains the compatible supplementary attribution. The coloured xW is always the candidate/baseline wall multiplier and remains the primary figure of merit. When no compatible supplementary clock exists, only (xW) is shown. No clock is fabricated from another. Every displayed number uses exactly three significant digits. Original AmpliCol is static N/A beyond three open quark lines; unavailable, terminal, or unlinked baselines keep their status while successful candidate timings remain absolute and leave summaries. Summary rows contain multipliers only in min, max, median, average, and weighted-average order; the weighted average is the ratio of timing sums, is the framed bold entry, and runtime statistics use wall time only. Not applicable marks a multiplicity for which the process family does not exist. Not run marks an otherwise defined process that was intentionally excluded from the declared measurement scope. Fixed-engine tables intentionally omit mode letters because the selected engine is named in the table heading.}} +\end{minipage} +\clearpage +\noindent\begin{minipage}{\linewidth} +\subsubsection*{Built-in SM recurrence versus AmpliCol full-colour (block 2 of 2)} +\begingroup +\centering +\footnotesize +\setlength{\tabcolsep}{2.1pt} +\renewcommand{\arraystretch}{1.04} +\matrixfitwidth{% +\begin{tabular}{@{}rL{1.45in}l>{\matrixentryfont}l>{\matrixentryfont}r@{}>{\matrixentryfont}l>{\matrixentryfont}l>{\matrixentryfont}r@{}>{\matrixentryfont}l>{\matrixentryfont}l>{\matrixentryfont}r@{}>{\matrixentryfont}l@{}} +\toprule +\textbf{ID} & \textbf{base process} & \textbf{metric} & \multicolumn{3}{c}{\textbf{n=4}} & \multicolumn{3}{c}{\textbf{n=5}} & \multicolumn{3}{c}{\textbf{n=6}} \\ +\specialrule{0.85pt}{0pt}{0pt} +\rowcolor{refblue} +\texttt{1} & $d\bar d\to Z+(n-1)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{2} & $u\bar d\to W^++(n-1)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{3} & $d\bar d\to e^+e^-+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{4} & $u\bar d\to e^+\nu_e+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{5} & $d\bar d\to ZZ+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{6} & $gg\to t\bar t+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{7} & $d\bar d\to t\bar t+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{8} & $gg\to gg+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{9} & $d\bar d\to ZZZ+(n-3)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{10} & $d\bar d\to e^+e^-ZH+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{11} & $d\bar d\to t\bar t ZH+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{12} & $d\bar d\to e^+e^-e^+e^-+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{13} & $d\bar d\to u\bar u\,s\bar s+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{14} & $d\bar d\to u\bar u\,s\bar s\,c\bar c+(n-6)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstaticna{ReportMuted} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \matrixstaticna{ReportMuted} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{15} & $d\bar d\to u\bar u\,u\bar u+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\specialrule{1.05pt}{0.22em}{0.18em} +\multicolumn{3}{@{}l}{} & \multicolumn{3}{c}{\cellcolor{refblue}\textbf{n=4}} & \multicolumn{3}{c}{\textbf{n=5}} & \multicolumn{3}{c}{\cellcolor{refblue}\textbf{n=6}} \\ +\multicolumn{3}{@{}l}{\textbf{summary: generation}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} & \multicolumn{3}{c}{\matrixna{ReportMuted}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} \\[0.08em] +\multicolumn{3}{@{}l}{\textbf{summary: wall}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} & \multicolumn{3}{c}{\matrixna{ReportMuted}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} \\ +\bottomrule +\end{tabular} +} +\endgroup +\ReportTableNote{{\scriptsize Baseline: original AmpliCol; candidate: recurrence JIT O2. Each cell shows the baseline generation and wall time, followed by candidate/baseline generation and wall-time ratios. Runtime comparisons use ([xS]xW). The muted supplementary xS first compares compatible exposed execution attributions; otherwise it compares authenticated evaluator totals. Only when original AmpliCol has no evaluator-total clock may a candidate evaluator total use the legacy direct execution clock (or its wall clock when direct execution is absent) as denominator. An evaluator total is never divided by a recurrence core. This evaluator-total fallback is diagnostic only; the execution/execution ratio remains the compatible supplementary attribution. The coloured xW is always the candidate/baseline wall multiplier and remains the primary figure of merit. When no compatible supplementary clock exists, only (xW) is shown. No clock is fabricated from another. Every displayed number uses exactly three significant digits. Original AmpliCol is static N/A beyond three open quark lines; unavailable, terminal, or unlinked baselines keep their status while successful candidate timings remain absolute and leave summaries. Summary rows contain multipliers only in min, max, median, average, and weighted-average order; the weighted average is the ratio of timing sums, is the framed bold entry, and runtime statistics use wall time only. Not applicable marks a multiplicity for which the process family does not exist. Not run marks an otherwise defined process that was intentionally excluded from the declared measurement scope. Fixed-engine tables intentionally omit mode letters because the selected engine is named in the table heading.}} +\end{minipage} diff --git a/pyamplicol/src/pyamplicol/_profiling_campaign/result_matrix_recurrence_builtin_sm_lc_table.tex b/pyamplicol/src/pyamplicol/_profiling_campaign/result_matrix_recurrence_builtin_sm_lc_table.tex new file mode 100644 index 0000000000..5be1fb8d19 --- /dev/null +++ b/pyamplicol/src/pyamplicol/_profiling_campaign/result_matrix_recurrence_builtin_sm_lc_table.tex @@ -0,0 +1,292 @@ +% SPDX-License-Identifier: 0BSD +% Generated by tools/performance_report/render.py; do not edit. +\providecommand{\matrixentryfont}{\fontsize{6.8pt}{7.8pt}\selectfont} +\providecommand{\matrixentryfontlc}{\fontsize{6.5pt}{7.5pt}\selectfont} +\providecommand{\matrixsummaryfont}{\fontsize{6.2pt}{7.4pt}\selectfont} +\providecommand{\matrixfitwidth}[1]{\begingroup\sbox0{#1}\ifdim\wd0>\linewidth\resizebox{\linewidth}{!}{\usebox0}\else\makebox[\linewidth][c]{\usebox0}\fi\endgroup} +\providecommand{\matrixpunct}[1]{\textcolor{black}{\texttt{#1}}} +\providecommand{\matrixratio}[2]{\matrixpunct{(}\textcolor{#1}{\texttt{x#2}}\matrixpunct{)}} +\providecommand{\matrixna}[1]{\textcolor{#1}{\texttt{N/A}}} +\providecommand{\matrixnotapplicable}[1]{\textcolor{#1}{\textsc{not applicable}}} +\providecommand{\matrixnotexposed}[1]{\textcolor{#1}{\textsc{not exposed}}} +\providecommand{\matrixstaticna}[1]{\textcolor{#1}{\textsc{static N/A}}} +\providecommand{\matrixtotalevaluator}[1]{\textcolor{ReportBlue}{\texttt{T }}#1} +\providecommand{\matrixrecurrencecore}[1]{\textcolor{ReportOrange}{\texttt{C }}#1} +\providecommand{\matrixzrecurrenceclocks}[2]{\shortstack[r]{#1\\#2}} +\providecommand{\matrixwallclock}[2]{\textcolor{#1}{\texttt{W #2}}} +\providecommand{\matrixwallabsolute}[1]{\textcolor{ReportBlue}{\texttt{W }}#1} +\providecommand{\matrixruntimepair}[2]{\shortstack[l]{#1\\#2}} +\providecommand{\matrixruntimetriple}[3]{\shortstack[l]{#1\\#2\matrixpunct{ | }#3}} +\providecommand{\matrixbelow}[1]{\textcolor{ReportMuted}{\texttt{<#1}}} +\providecommand{\matrixnaratio}[1]{\matrixpunct{(}\matrixna{#1}\matrixpunct{)}} +\providecommand{\matrixstatus}[2]{\textcolor{#1}{\textsc{#2}}} +\providecommand{\matrixncabsolute}[1]{#1} +\providecommand{\matrixsummaryratio}[2]{\textcolor{#1}{\texttt{x#2}}} +\providecommand{\matrixsummaryratiohighlight}[2]{\begingroup\setlength{\fboxsep}{0.9pt}\setlength{\fboxrule}{0.35pt}\fbox{\textcolor{#1}{{\usefont{T1}{lmtt}{b}{n}x#2}}}\endgroup} +\providecommand{\matrixsummarystats}[5]{\begingroup\matrixsummaryfont\begin{tabular}[t]{@{}l@{\hspace{0.014in}\matrixpunct{|}\hspace{0.014in}}l@{\hspace{0.014in}\matrixpunct{|}\hspace{0.014in}}l@{\hspace{0.014in}\matrixpunct{|}\hspace{0.014in}}l@{\hspace{0.014in}\matrixpunct{|}\hspace{0.014in}}l@{}}\makebox[3.6em][l]{#1}&\makebox[4.6em][l]{#2}&\makebox[3.6em][l]{#3}&\makebox[3.6em][l]{#4}&\makebox[4.2em][l]{#5}\end{tabular}\endgroup} +\providecommand{\matrixsummaryworkloads}[2]{\begin{tabular}[t]{@{}l@{}}#1\\[-0.10em]#2\end{tabular}} +\providecommand{\matrixpair}[2]{\begin{tabular}[t]{@{}r@{\hspace{0.025in}\matrixpunct{|}\hspace{0.025in}}r@{}}#1\end{tabular}} +\providecommand{\bestmoderatio}[2]{\textcolor{#1}{\texttt{x#2}}} +\providecommand{\bestmodecompactratio}[3]{\matrixpunct{(}\textcolor{ReportMuted}{\texttt{[x#1]}}\hspace{0.04in}\textcolor{#2}{\texttt{x#3}}\matrixpunct{)}} +\providecommand{\bestmodewallratio}[2]{\matrixpunct{(}\textcolor{#1}{\texttt{x#2}}\matrixpunct{)}} +\providecommand{\bestmodecompactprefix}[1]{\matrixpunct{(}\textcolor{ReportMuted}{\texttt{[x#1]}}\hspace{0.04in}} +\providecommand{\bestmodeopenprefix}{\matrixpunct{(}\hspace{0.04in}} +\providecommand{\bestmodeprimaryratio}[2]{\textcolor{#1}{\texttt{x#2}}\matrixpunct{)}} +\providecommand{\bestmodeabsoluteprefix}[1]{#1\hspace{0.04in}} +\providecommand{\bestmodecompactabsolute}[2]{\textcolor{ReportMuted}{\texttt{[#1]}}\hspace{0.04in}\textcolor{black}{\texttt{#2}}} +\providecommand{\bestmodecompactabsoluteprefix}[1]{\textcolor{ReportMuted}{\texttt{[#1]}}\hspace{0.04in}} +\providecommand{\bestmodeabsoluteprimary}[1]{\textcolor{black}{\texttt{#1}}} +\providecommand{\bestmodeunverifiedclockprefix}[2]{\textcolor{ReportMuted}{\texttt{[}#1\texttt{]}}\hspace{0.04in}#2\hspace{0.04in}} +\clearpage +\subsection{Built-in SM recurrence versus AmpliCol LC} +\noindent\begin{minipage}{\linewidth} +\subsubsection*{Built-in SM recurrence versus AmpliCol LC (block 1 of 3)} +\begingroup +\centering +\footnotesize +\setlength{\tabcolsep}{2.1pt} +\renewcommand{\arraystretch}{1.04} +\matrixfitwidth{% +\begin{tabular}{@{}rL{1.45in}l>{\matrixentryfontlc}l>{\matrixentryfontlc}c>{\matrixentryfontlc}l>{\matrixentryfontlc}r@{}>{\matrixentryfontlc}l>{\matrixentryfontlc}c>{\matrixentryfontlc}r@{}>{\matrixentryfontlc}l>{\matrixentryfontlc}l>{\matrixentryfontlc}c>{\matrixentryfontlc}l>{\matrixentryfontlc}r@{}>{\matrixentryfontlc}l>{\matrixentryfontlc}c>{\matrixentryfontlc}r@{}>{\matrixentryfontlc}l>{\matrixentryfontlc}l>{\matrixentryfontlc}c>{\matrixentryfontlc}l>{\matrixentryfontlc}r@{}>{\matrixentryfontlc}l>{\matrixentryfontlc}c>{\matrixentryfontlc}r@{}>{\matrixentryfontlc}l@{}} +\toprule +\textbf{ID} & \textbf{base process} & \textbf{metric} & \multicolumn{8}{c}{\textbf{n=1}} & \multicolumn{8}{c}{\textbf{n=2}} & \multicolumn{8}{c}{\textbf{n=3}} \\ +\specialrule{0.85pt}{0pt}{0pt} +\rowcolor{refblue} +\texttt{1} & $d\bar d\to Z+(n-1)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{2} & $u\bar d\to W^++(n-1)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{3} & $d\bar d\to e^+e^-+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{8}{c}{} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{4} & $u\bar d\to e^+\nu_e+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{8}{c}{} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{5} & $d\bar d\to ZZ+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{8}{c}{} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{6} & $gg\to t\bar t+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{8}{c}{} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{7} & $d\bar d\to t\bar t+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{8}{c}{} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{8} & $gg\to gg+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{8}{c}{} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{9} & $d\bar d\to ZZZ+(n-3)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{8}{c}{} & \multicolumn{8}{c}{} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{10} & $d\bar d\to e^+e^-ZH+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{8}{c}{} & \multicolumn{8}{c}{} & \multicolumn{8}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{11} & $d\bar d\to t\bar t ZH+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{8}{c}{} & \multicolumn{8}{c}{} & \multicolumn{8}{c}{} \\ +\addlinespace[0.05em] +\texttt{12} & $d\bar d\to e^+e^-e^+e^-+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{8}{c}{} & \multicolumn{8}{c}{} & \multicolumn{8}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{13} & $d\bar d\to u\bar u\,s\bar s+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{8}{c}{} & \multicolumn{8}{c}{} & \multicolumn{8}{c}{} \\ +\addlinespace[0.05em] +\texttt{14} & $d\bar d\to u\bar u\,s\bar s\,c\bar c+(n-6)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{8}{c}{} & \multicolumn{8}{c}{} & \multicolumn{8}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{15} & $d\bar d\to u\bar u\,u\bar u+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{8}{c}{} & \multicolumn{8}{c}{} & \multicolumn{8}{c}{} \\ +\addlinespace[0.05em] +\specialrule{1.05pt}{0.22em}{0.18em} +\multicolumn{3}{@{}l}{} & \multicolumn{8}{c}{\cellcolor{refblue}\textbf{n=1}} & \multicolumn{8}{c}{\textbf{n=2}} & \multicolumn{8}{c}{\cellcolor{refblue}\textbf{n=3}} \\ +\multicolumn{3}{@{}l}{\textbf{summary: generation}} & \multicolumn{8}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} & \multicolumn{8}{c}{\matrixna{ReportMuted}} & \multicolumn{8}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} \\[0.08em] +\multicolumn{3}{@{}l}{\matrixsummaryworkloads{\textbf{summary: run single-flow, hel. sum}}{\textbf{summary: run all-flows, single hel.}}} & \multicolumn{8}{c}{\cellcolor{refblue}\matrixsummaryworkloads{\matrixna{ReportMuted}}{\matrixna{ReportMuted}}} & \multicolumn{8}{c}{\matrixsummaryworkloads{\matrixna{ReportMuted}}{\matrixna{ReportMuted}}} & \multicolumn{8}{c}{\cellcolor{refblue}\matrixsummaryworkloads{\matrixna{ReportMuted}}{\matrixna{ReportMuted}}} \\ +\bottomrule +\end{tabular} +} +\endgroup +\ReportTableNote{{\scriptsize Baseline: original AmpliCol; candidate: recurrence JIT O2. Each cell shows the selected-flow library-generation and all-flow direct-setup baseline quantities. The selected-flow entry carries a generation ratio; the all-flow entry carries the absolute pyAmpliCol process-generation time. Its setup boundary differs from the reference, so no generation ratio is formed. Runtime comparisons use ([xS]xW). The muted supplementary xS first compares compatible exposed execution attributions; otherwise it compares authenticated evaluator totals. Only when original AmpliCol has no evaluator-total clock may a candidate evaluator total use the legacy direct execution clock (or its wall clock when direct execution is absent) as denominator. An evaluator total is never divided by a recurrence core. This evaluator-total fallback is diagnostic only; the execution/execution ratio remains the compatible supplementary attribution. The coloured xW is always the candidate/baseline wall multiplier and remains the primary figure of merit. When no compatible supplementary clock exists, only (xW) is shown. No clock is fabricated from another. Every displayed number uses exactly three significant digits. Original AmpliCol is static N/A beyond three open quark lines; unavailable, terminal, or unlinked baselines keep their status while successful candidate timings remain absolute and leave summaries. Summary rows contain multipliers only in min, max, median, average, and weighted-average order; the weighted average is the ratio of timing sums and is the framed bold entry. LC generation uses selected flow only, while LC runtime shows separate selected-flow and all-flow wall-only lines. Not applicable marks a multiplicity for which the process family does not exist. Not run marks an otherwise defined process that was intentionally excluded from the declared measurement scope. Fixed-engine tables intentionally omit mode letters because the selected engine is named in the table heading.}} +\end{minipage} +\clearpage +\noindent\begin{minipage}{\linewidth} +\subsubsection*{Built-in SM recurrence versus AmpliCol LC (block 2 of 3)} +\begingroup +\centering +\footnotesize +\setlength{\tabcolsep}{2.1pt} +\renewcommand{\arraystretch}{1.04} +\matrixfitwidth{% +\begin{tabular}{@{}rL{1.45in}l>{\matrixentryfontlc}l>{\matrixentryfontlc}c>{\matrixentryfontlc}l>{\matrixentryfontlc}r@{}>{\matrixentryfontlc}l>{\matrixentryfontlc}c>{\matrixentryfontlc}r@{}>{\matrixentryfontlc}l>{\matrixentryfontlc}l>{\matrixentryfontlc}c>{\matrixentryfontlc}l>{\matrixentryfontlc}r@{}>{\matrixentryfontlc}l>{\matrixentryfontlc}c>{\matrixentryfontlc}r@{}>{\matrixentryfontlc}l>{\matrixentryfontlc}l>{\matrixentryfontlc}c>{\matrixentryfontlc}l>{\matrixentryfontlc}r@{}>{\matrixentryfontlc}l>{\matrixentryfontlc}c>{\matrixentryfontlc}r@{}>{\matrixentryfontlc}l@{}} +\toprule +\textbf{ID} & \textbf{base process} & \textbf{metric} & \multicolumn{8}{c}{\textbf{n=4}} & \multicolumn{8}{c}{\textbf{n=5}} & \multicolumn{8}{c}{\textbf{n=6}} \\ +\specialrule{0.85pt}{0pt}{0pt} +\rowcolor{refblue} +\texttt{1} & $d\bar d\to Z+(n-1)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{2} & $u\bar d\to W^++(n-1)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{3} & $d\bar d\to e^+e^-+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{4} & $u\bar d\to e^+\nu_e+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{5} & $d\bar d\to ZZ+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{6} & $gg\to t\bar t+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{7} & $d\bar d\to t\bar t+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{8} & $gg\to gg+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{9} & $d\bar d\to ZZZ+(n-3)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{10} & $d\bar d\to e^+e^-ZH+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{11} & $d\bar d\to t\bar t ZH+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{12} & $d\bar d\to e^+e^-e^+e^-+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{13} & $d\bar d\to u\bar u\,s\bar s+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{14} & $d\bar d\to u\bar u\,s\bar s\,c\bar c+(n-6)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstaticna{ReportMuted} & \matrixpunct{|} & \matrixstaticna{ReportMuted} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{8}{c}{} & \multicolumn{8}{c}{} & \matrixstaticna{ReportMuted} & \matrixpunct{|} & \matrixstaticna{ReportMuted} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{15} & $d\bar d\to u\bar u\,u\bar u+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\specialrule{1.05pt}{0.22em}{0.18em} +\multicolumn{3}{@{}l}{} & \multicolumn{8}{c}{\cellcolor{refblue}\textbf{n=4}} & \multicolumn{8}{c}{\textbf{n=5}} & \multicolumn{8}{c}{\cellcolor{refblue}\textbf{n=6}} \\ +\multicolumn{3}{@{}l}{\textbf{summary: generation}} & \multicolumn{8}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} & \multicolumn{8}{c}{\matrixna{ReportMuted}} & \multicolumn{8}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} \\[0.08em] +\multicolumn{3}{@{}l}{\matrixsummaryworkloads{\textbf{summary: run single-flow, hel. sum}}{\textbf{summary: run all-flows, single hel.}}} & \multicolumn{8}{c}{\cellcolor{refblue}\matrixsummaryworkloads{\matrixna{ReportMuted}}{\matrixna{ReportMuted}}} & \multicolumn{8}{c}{\matrixsummaryworkloads{\matrixna{ReportMuted}}{\matrixna{ReportMuted}}} & \multicolumn{8}{c}{\cellcolor{refblue}\matrixsummaryworkloads{\matrixna{ReportMuted}}{\matrixna{ReportMuted}}} \\ +\bottomrule +\end{tabular} +} +\endgroup +\ReportTableNote{{\scriptsize Baseline: original AmpliCol; candidate: recurrence JIT O2. Each cell shows the selected-flow library-generation and all-flow direct-setup baseline quantities. The selected-flow entry carries a generation ratio; the all-flow entry carries the absolute pyAmpliCol process-generation time. Its setup boundary differs from the reference, so no generation ratio is formed. Runtime comparisons use ([xS]xW). The muted supplementary xS first compares compatible exposed execution attributions; otherwise it compares authenticated evaluator totals. Only when original AmpliCol has no evaluator-total clock may a candidate evaluator total use the legacy direct execution clock (or its wall clock when direct execution is absent) as denominator. An evaluator total is never divided by a recurrence core. This evaluator-total fallback is diagnostic only; the execution/execution ratio remains the compatible supplementary attribution. The coloured xW is always the candidate/baseline wall multiplier and remains the primary figure of merit. When no compatible supplementary clock exists, only (xW) is shown. No clock is fabricated from another. Every displayed number uses exactly three significant digits. Original AmpliCol is static N/A beyond three open quark lines; unavailable, terminal, or unlinked baselines keep their status while successful candidate timings remain absolute and leave summaries. Summary rows contain multipliers only in min, max, median, average, and weighted-average order; the weighted average is the ratio of timing sums and is the framed bold entry. LC generation uses selected flow only, while LC runtime shows separate selected-flow and all-flow wall-only lines. Not applicable marks a multiplicity for which the process family does not exist. Not run marks an otherwise defined process that was intentionally excluded from the declared measurement scope. Fixed-engine tables intentionally omit mode letters because the selected engine is named in the table heading.}} +\end{minipage} +\clearpage +\noindent\begin{minipage}{\linewidth} +\subsubsection*{Built-in SM recurrence versus AmpliCol LC (block 3 of 3)} +\begingroup +\centering +\footnotesize +\setlength{\tabcolsep}{2.1pt} +\renewcommand{\arraystretch}{1.04} +\matrixfitwidth{% +\begin{tabular}{@{}rL{1.45in}l>{\matrixentryfontlc}l>{\matrixentryfontlc}c>{\matrixentryfontlc}l>{\matrixentryfontlc}r@{}>{\matrixentryfontlc}l>{\matrixentryfontlc}c>{\matrixentryfontlc}r@{}>{\matrixentryfontlc}l>{\matrixentryfontlc}l>{\matrixentryfontlc}c>{\matrixentryfontlc}l>{\matrixentryfontlc}r@{}>{\matrixentryfontlc}l>{\matrixentryfontlc}c>{\matrixentryfontlc}r@{}>{\matrixentryfontlc}l>{\matrixentryfontlc}l>{\matrixentryfontlc}c>{\matrixentryfontlc}l>{\matrixentryfontlc}r@{}>{\matrixentryfontlc}l>{\matrixentryfontlc}c>{\matrixentryfontlc}r@{}>{\matrixentryfontlc}l@{}} +\toprule +\textbf{ID} & \textbf{base process} & \textbf{metric} & \multicolumn{8}{c}{\textbf{n=7}} & \multicolumn{8}{c}{\textbf{n=8}} & \multicolumn{8}{c}{\textbf{n=9}} \\ +\specialrule{0.85pt}{0pt}{0pt} +\rowcolor{refblue} +\texttt{1} & $d\bar d\to Z+(n-1)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{2} & $u\bar d\to W^++(n-1)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{3} & $d\bar d\to e^+e^-+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{4} & $u\bar d\to e^+\nu_e+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{5} & $d\bar d\to ZZ+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{6} & $gg\to t\bar t+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{7} & $d\bar d\to t\bar t+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{8} & $gg\to gg+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{9} & $d\bar d\to ZZZ+(n-3)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{10} & $d\bar d\to e^+e^-ZH+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{11} & $d\bar d\to t\bar t ZH+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{12} & $d\bar d\to e^+e^-e^+e^-+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{13} & $d\bar d\to u\bar u\,s\bar s+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{14} & $d\bar d\to u\bar u\,s\bar s\,c\bar c+(n-6)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstaticna{ReportMuted} & \matrixpunct{|} & \matrixstaticna{ReportMuted} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstaticna{ReportMuted} & \matrixpunct{|} & \matrixstaticna{ReportMuted} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstaticna{ReportMuted} & \matrixpunct{|} & \matrixstaticna{ReportMuted} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstaticna{ReportMuted} & \matrixpunct{|} & \matrixstaticna{ReportMuted} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstaticna{ReportMuted} & \matrixpunct{|} & \matrixstaticna{ReportMuted} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstaticna{ReportMuted} & \matrixpunct{|} & \matrixstaticna{ReportMuted} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{15} & $d\bar d\to u\bar u\,u\bar u+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\specialrule{1.05pt}{0.22em}{0.18em} +\multicolumn{3}{@{}l}{} & \multicolumn{8}{c}{\cellcolor{refblue}\textbf{n=7}} & \multicolumn{8}{c}{\textbf{n=8}} & \multicolumn{8}{c}{\cellcolor{refblue}\textbf{n=9}} \\ +\multicolumn{3}{@{}l}{\textbf{summary: generation}} & \multicolumn{8}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} & \multicolumn{8}{c}{\matrixna{ReportMuted}} & \multicolumn{8}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} \\[0.08em] +\multicolumn{3}{@{}l}{\matrixsummaryworkloads{\textbf{summary: run single-flow, hel. sum}}{\textbf{summary: run all-flows, single hel.}}} & \multicolumn{8}{c}{\cellcolor{refblue}\matrixsummaryworkloads{\matrixna{ReportMuted}}{\matrixna{ReportMuted}}} & \multicolumn{8}{c}{\matrixsummaryworkloads{\matrixna{ReportMuted}}{\matrixna{ReportMuted}}} & \multicolumn{8}{c}{\cellcolor{refblue}\matrixsummaryworkloads{\matrixna{ReportMuted}}{\matrixna{ReportMuted}}} \\ +\bottomrule +\end{tabular} +} +\endgroup +\ReportTableNote{{\scriptsize Baseline: original AmpliCol; candidate: recurrence JIT O2. Each cell shows the selected-flow library-generation and all-flow direct-setup baseline quantities. The selected-flow entry carries a generation ratio; the all-flow entry carries the absolute pyAmpliCol process-generation time. Its setup boundary differs from the reference, so no generation ratio is formed. Runtime comparisons use ([xS]xW). The muted supplementary xS first compares compatible exposed execution attributions; otherwise it compares authenticated evaluator totals. Only when original AmpliCol has no evaluator-total clock may a candidate evaluator total use the legacy direct execution clock (or its wall clock when direct execution is absent) as denominator. An evaluator total is never divided by a recurrence core. This evaluator-total fallback is diagnostic only; the execution/execution ratio remains the compatible supplementary attribution. The coloured xW is always the candidate/baseline wall multiplier and remains the primary figure of merit. When no compatible supplementary clock exists, only (xW) is shown. No clock is fabricated from another. Every displayed number uses exactly three significant digits. Original AmpliCol is static N/A beyond three open quark lines; unavailable, terminal, or unlinked baselines keep their status while successful candidate timings remain absolute and leave summaries. Summary rows contain multipliers only in min, max, median, average, and weighted-average order; the weighted average is the ratio of timing sums and is the framed bold entry. LC generation uses selected flow only, while LC runtime shows separate selected-flow and all-flow wall-only lines. Not applicable marks a multiplicity for which the process family does not exist. Not run marks an otherwise defined process that was intentionally excluded from the declared measurement scope. Fixed-engine tables intentionally omit mode letters because the selected engine is named in the table heading.}} +\end{minipage} diff --git a/pyamplicol/src/pyamplicol/_profiling_campaign/result_matrix_recurrence_builtin_sm_nlc_table.tex b/pyamplicol/src/pyamplicol/_profiling_campaign/result_matrix_recurrence_builtin_sm_nlc_table.tex new file mode 100644 index 0000000000..edd2ff8e12 --- /dev/null +++ b/pyamplicol/src/pyamplicol/_profiling_campaign/result_matrix_recurrence_builtin_sm_nlc_table.tex @@ -0,0 +1,208 @@ +% SPDX-License-Identifier: 0BSD +% Generated by tools/performance_report/render.py; do not edit. +\providecommand{\matrixentryfont}{\fontsize{6.8pt}{7.8pt}\selectfont} +\providecommand{\matrixentryfontlc}{\fontsize{6.5pt}{7.5pt}\selectfont} +\providecommand{\matrixsummaryfont}{\fontsize{6.2pt}{7.4pt}\selectfont} +\providecommand{\matrixfitwidth}[1]{\begingroup\sbox0{#1}\ifdim\wd0>\linewidth\resizebox{\linewidth}{!}{\usebox0}\else\makebox[\linewidth][c]{\usebox0}\fi\endgroup} +\providecommand{\matrixpunct}[1]{\textcolor{black}{\texttt{#1}}} +\providecommand{\matrixratio}[2]{\matrixpunct{(}\textcolor{#1}{\texttt{x#2}}\matrixpunct{)}} +\providecommand{\matrixna}[1]{\textcolor{#1}{\texttt{N/A}}} +\providecommand{\matrixnotapplicable}[1]{\textcolor{#1}{\textsc{not applicable}}} +\providecommand{\matrixnotexposed}[1]{\textcolor{#1}{\textsc{not exposed}}} +\providecommand{\matrixstaticna}[1]{\textcolor{#1}{\textsc{static N/A}}} +\providecommand{\matrixtotalevaluator}[1]{\textcolor{ReportBlue}{\texttt{T }}#1} +\providecommand{\matrixrecurrencecore}[1]{\textcolor{ReportOrange}{\texttt{C }}#1} +\providecommand{\matrixzrecurrenceclocks}[2]{\shortstack[r]{#1\\#2}} +\providecommand{\matrixwallclock}[2]{\textcolor{#1}{\texttt{W #2}}} +\providecommand{\matrixwallabsolute}[1]{\textcolor{ReportBlue}{\texttt{W }}#1} +\providecommand{\matrixruntimepair}[2]{\shortstack[l]{#1\\#2}} +\providecommand{\matrixruntimetriple}[3]{\shortstack[l]{#1\\#2\matrixpunct{ | }#3}} +\providecommand{\matrixbelow}[1]{\textcolor{ReportMuted}{\texttt{<#1}}} +\providecommand{\matrixnaratio}[1]{\matrixpunct{(}\matrixna{#1}\matrixpunct{)}} +\providecommand{\matrixstatus}[2]{\textcolor{#1}{\textsc{#2}}} +\providecommand{\matrixncabsolute}[1]{#1} +\providecommand{\matrixsummaryratio}[2]{\textcolor{#1}{\texttt{x#2}}} +\providecommand{\matrixsummaryratiohighlight}[2]{\begingroup\setlength{\fboxsep}{0.9pt}\setlength{\fboxrule}{0.35pt}\fbox{\textcolor{#1}{{\usefont{T1}{lmtt}{b}{n}x#2}}}\endgroup} +\providecommand{\matrixsummarystats}[5]{\begingroup\matrixsummaryfont\begin{tabular}[t]{@{}l@{\hspace{0.014in}\matrixpunct{|}\hspace{0.014in}}l@{\hspace{0.014in}\matrixpunct{|}\hspace{0.014in}}l@{\hspace{0.014in}\matrixpunct{|}\hspace{0.014in}}l@{\hspace{0.014in}\matrixpunct{|}\hspace{0.014in}}l@{}}\makebox[3.6em][l]{#1}&\makebox[4.6em][l]{#2}&\makebox[3.6em][l]{#3}&\makebox[3.6em][l]{#4}&\makebox[4.2em][l]{#5}\end{tabular}\endgroup} +\providecommand{\matrixsummaryworkloads}[2]{\begin{tabular}[t]{@{}l@{}}#1\\[-0.10em]#2\end{tabular}} +\providecommand{\matrixpair}[2]{\begin{tabular}[t]{@{}r@{\hspace{0.025in}\matrixpunct{|}\hspace{0.025in}}r@{}}#1\end{tabular}} +\providecommand{\bestmoderatio}[2]{\textcolor{#1}{\texttt{x#2}}} +\providecommand{\bestmodecompactratio}[3]{\matrixpunct{(}\textcolor{ReportMuted}{\texttt{[x#1]}}\hspace{0.04in}\textcolor{#2}{\texttt{x#3}}\matrixpunct{)}} +\providecommand{\bestmodewallratio}[2]{\matrixpunct{(}\textcolor{#1}{\texttt{x#2}}\matrixpunct{)}} +\providecommand{\bestmodecompactprefix}[1]{\matrixpunct{(}\textcolor{ReportMuted}{\texttt{[x#1]}}\hspace{0.04in}} +\providecommand{\bestmodeopenprefix}{\matrixpunct{(}\hspace{0.04in}} +\providecommand{\bestmodeprimaryratio}[2]{\textcolor{#1}{\texttt{x#2}}\matrixpunct{)}} +\providecommand{\bestmodeabsoluteprefix}[1]{#1\hspace{0.04in}} +\providecommand{\bestmodecompactabsolute}[2]{\textcolor{ReportMuted}{\texttt{[#1]}}\hspace{0.04in}\textcolor{black}{\texttt{#2}}} +\providecommand{\bestmodecompactabsoluteprefix}[1]{\textcolor{ReportMuted}{\texttt{[#1]}}\hspace{0.04in}} +\providecommand{\bestmodeabsoluteprimary}[1]{\textcolor{black}{\texttt{#1}}} +\providecommand{\bestmodeunverifiedclockprefix}[2]{\textcolor{ReportMuted}{\texttt{[}#1\texttt{]}}\hspace{0.04in}#2\hspace{0.04in}} +\clearpage +\subsection{Built-in SM recurrence versus AmpliCol NLC} +\noindent\begin{minipage}{\linewidth} +\subsubsection*{Built-in SM recurrence versus AmpliCol NLC (block 1 of 2)} +\begingroup +\centering +\footnotesize +\setlength{\tabcolsep}{2.1pt} +\renewcommand{\arraystretch}{1.04} +\matrixfitwidth{% +\begin{tabular}{@{}rL{1.45in}l>{\matrixentryfont}l>{\matrixentryfont}r@{}>{\matrixentryfont}l>{\matrixentryfont}l>{\matrixentryfont}r@{}>{\matrixentryfont}l>{\matrixentryfont}l>{\matrixentryfont}r@{}>{\matrixentryfont}l@{}} +\toprule +\textbf{ID} & \textbf{base process} & \textbf{metric} & \multicolumn{3}{c}{\textbf{n=1}} & \multicolumn{3}{c}{\textbf{n=2}} & \multicolumn{3}{c}{\textbf{n=3}} \\ +\specialrule{0.85pt}{0pt}{0pt} +\rowcolor{refblue} +\texttt{1} & $d\bar d\to Z+(n-1)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{2} & $u\bar d\to W^++(n-1)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{3} & $d\bar d\to e^+e^-+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{4} & $u\bar d\to e^+\nu_e+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{5} & $d\bar d\to ZZ+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{6} & $gg\to t\bar t+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{7} & $d\bar d\to t\bar t+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{8} & $gg\to gg+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{9} & $d\bar d\to ZZZ+(n-3)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{10} & $d\bar d\to e^+e^-ZH+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{11} & $d\bar d\to t\bar t ZH+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{12} & $d\bar d\to e^+e^-e^+e^-+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{13} & $d\bar d\to u\bar u\,s\bar s+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{14} & $d\bar d\to u\bar u\,s\bar s\,c\bar c+(n-6)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{15} & $d\bar d\to u\bar u\,u\bar u+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\specialrule{1.05pt}{0.22em}{0.18em} +\multicolumn{3}{@{}l}{} & \multicolumn{3}{c}{\cellcolor{refblue}\textbf{n=1}} & \multicolumn{3}{c}{\textbf{n=2}} & \multicolumn{3}{c}{\cellcolor{refblue}\textbf{n=3}} \\ +\multicolumn{3}{@{}l}{\textbf{summary: generation}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} & \multicolumn{3}{c}{\matrixna{ReportMuted}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} \\[0.08em] +\multicolumn{3}{@{}l}{\textbf{summary: wall}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} & \multicolumn{3}{c}{\matrixna{ReportMuted}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} \\ +\bottomrule +\end{tabular} +} +\endgroup +\ReportTableNote{{\scriptsize Baseline: original AmpliCol; candidate: recurrence JIT O2. Each cell shows the baseline generation and wall time, followed by candidate/baseline generation and wall-time ratios. Runtime comparisons use ([xS]xW). The muted supplementary xS first compares compatible exposed execution attributions; otherwise it compares authenticated evaluator totals. Only when original AmpliCol has no evaluator-total clock may a candidate evaluator total use the legacy direct execution clock (or its wall clock when direct execution is absent) as denominator. An evaluator total is never divided by a recurrence core. This evaluator-total fallback is diagnostic only; the execution/execution ratio remains the compatible supplementary attribution. The coloured xW is always the candidate/baseline wall multiplier and remains the primary figure of merit. When no compatible supplementary clock exists, only (xW) is shown. No clock is fabricated from another. Every displayed number uses exactly three significant digits. Original AmpliCol is static N/A beyond three open quark lines; unavailable, terminal, or unlinked baselines keep their status while successful candidate timings remain absolute and leave summaries. Summary rows contain multipliers only in min, max, median, average, and weighted-average order; the weighted average is the ratio of timing sums, is the framed bold entry, and runtime statistics use wall time only. Not applicable marks a multiplicity for which the process family does not exist. Not run marks an otherwise defined process that was intentionally excluded from the declared measurement scope. Fixed-engine tables intentionally omit mode letters because the selected engine is named in the table heading.}} +\end{minipage} +\clearpage +\noindent\begin{minipage}{\linewidth} +\subsubsection*{Built-in SM recurrence versus AmpliCol NLC (block 2 of 2)} +\begingroup +\centering +\footnotesize +\setlength{\tabcolsep}{2.1pt} +\renewcommand{\arraystretch}{1.04} +\matrixfitwidth{% +\begin{tabular}{@{}rL{1.45in}l>{\matrixentryfont}l>{\matrixentryfont}r@{}>{\matrixentryfont}l>{\matrixentryfont}l>{\matrixentryfont}r@{}>{\matrixentryfont}l>{\matrixentryfont}l>{\matrixentryfont}r@{}>{\matrixentryfont}l@{}} +\toprule +\textbf{ID} & \textbf{base process} & \textbf{metric} & \multicolumn{3}{c}{\textbf{n=4}} & \multicolumn{3}{c}{\textbf{n=5}} & \multicolumn{3}{c}{\textbf{n=6}} \\ +\specialrule{0.85pt}{0pt}{0pt} +\rowcolor{refblue} +\texttt{1} & $d\bar d\to Z+(n-1)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{2} & $u\bar d\to W^++(n-1)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{3} & $d\bar d\to e^+e^-+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{4} & $u\bar d\to e^+\nu_e+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{5} & $d\bar d\to ZZ+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{6} & $gg\to t\bar t+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{7} & $d\bar d\to t\bar t+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{8} & $gg\to gg+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{9} & $d\bar d\to ZZZ+(n-3)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{10} & $d\bar d\to e^+e^-ZH+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{11} & $d\bar d\to t\bar t ZH+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{12} & $d\bar d\to e^+e^-e^+e^-+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{13} & $d\bar d\to u\bar u\,s\bar s+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{14} & $d\bar d\to u\bar u\,s\bar s\,c\bar c+(n-6)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstaticna{ReportMuted} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \matrixstaticna{ReportMuted} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{15} & $d\bar d\to u\bar u\,u\bar u+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\specialrule{1.05pt}{0.22em}{0.18em} +\multicolumn{3}{@{}l}{} & \multicolumn{3}{c}{\cellcolor{refblue}\textbf{n=4}} & \multicolumn{3}{c}{\textbf{n=5}} & \multicolumn{3}{c}{\cellcolor{refblue}\textbf{n=6}} \\ +\multicolumn{3}{@{}l}{\textbf{summary: generation}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} & \multicolumn{3}{c}{\matrixna{ReportMuted}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} \\[0.08em] +\multicolumn{3}{@{}l}{\textbf{summary: wall}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} & \multicolumn{3}{c}{\matrixna{ReportMuted}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} \\ +\bottomrule +\end{tabular} +} +\endgroup +\ReportTableNote{{\scriptsize Baseline: original AmpliCol; candidate: recurrence JIT O2. Each cell shows the baseline generation and wall time, followed by candidate/baseline generation and wall-time ratios. Runtime comparisons use ([xS]xW). The muted supplementary xS first compares compatible exposed execution attributions; otherwise it compares authenticated evaluator totals. Only when original AmpliCol has no evaluator-total clock may a candidate evaluator total use the legacy direct execution clock (or its wall clock when direct execution is absent) as denominator. An evaluator total is never divided by a recurrence core. This evaluator-total fallback is diagnostic only; the execution/execution ratio remains the compatible supplementary attribution. The coloured xW is always the candidate/baseline wall multiplier and remains the primary figure of merit. When no compatible supplementary clock exists, only (xW) is shown. No clock is fabricated from another. Every displayed number uses exactly three significant digits. Original AmpliCol is static N/A beyond three open quark lines; unavailable, terminal, or unlinked baselines keep their status while successful candidate timings remain absolute and leave summaries. Summary rows contain multipliers only in min, max, median, average, and weighted-average order; the weighted average is the ratio of timing sums, is the framed bold entry, and runtime statistics use wall time only. Not applicable marks a multiplicity for which the process family does not exist. Not run marks an otherwise defined process that was intentionally excluded from the declared measurement scope. Fixed-engine tables intentionally omit mode letters because the selected engine is named in the table heading.}} +\end{minipage} diff --git a/pyamplicol/src/pyamplicol/_profiling_campaign/result_matrix_recurrence_ufo_sm_full_table.tex b/pyamplicol/src/pyamplicol/_profiling_campaign/result_matrix_recurrence_ufo_sm_full_table.tex new file mode 100644 index 0000000000..2a5ddbcb6a --- /dev/null +++ b/pyamplicol/src/pyamplicol/_profiling_campaign/result_matrix_recurrence_ufo_sm_full_table.tex @@ -0,0 +1,208 @@ +% SPDX-License-Identifier: 0BSD +% Generated by tools/performance_report/render.py; do not edit. +\providecommand{\matrixentryfont}{\fontsize{6.8pt}{7.8pt}\selectfont} +\providecommand{\matrixentryfontlc}{\fontsize{6.5pt}{7.5pt}\selectfont} +\providecommand{\matrixsummaryfont}{\fontsize{6.2pt}{7.4pt}\selectfont} +\providecommand{\matrixfitwidth}[1]{\begingroup\sbox0{#1}\ifdim\wd0>\linewidth\resizebox{\linewidth}{!}{\usebox0}\else\makebox[\linewidth][c]{\usebox0}\fi\endgroup} +\providecommand{\matrixpunct}[1]{\textcolor{black}{\texttt{#1}}} +\providecommand{\matrixratio}[2]{\matrixpunct{(}\textcolor{#1}{\texttt{x#2}}\matrixpunct{)}} +\providecommand{\matrixna}[1]{\textcolor{#1}{\texttt{N/A}}} +\providecommand{\matrixnotapplicable}[1]{\textcolor{#1}{\textsc{not applicable}}} +\providecommand{\matrixnotexposed}[1]{\textcolor{#1}{\textsc{not exposed}}} +\providecommand{\matrixstaticna}[1]{\textcolor{#1}{\textsc{static N/A}}} +\providecommand{\matrixtotalevaluator}[1]{\textcolor{ReportBlue}{\texttt{T }}#1} +\providecommand{\matrixrecurrencecore}[1]{\textcolor{ReportOrange}{\texttt{C }}#1} +\providecommand{\matrixzrecurrenceclocks}[2]{\shortstack[r]{#1\\#2}} +\providecommand{\matrixwallclock}[2]{\textcolor{#1}{\texttt{W #2}}} +\providecommand{\matrixwallabsolute}[1]{\textcolor{ReportBlue}{\texttt{W }}#1} +\providecommand{\matrixruntimepair}[2]{\shortstack[l]{#1\\#2}} +\providecommand{\matrixruntimetriple}[3]{\shortstack[l]{#1\\#2\matrixpunct{ | }#3}} +\providecommand{\matrixbelow}[1]{\textcolor{ReportMuted}{\texttt{<#1}}} +\providecommand{\matrixnaratio}[1]{\matrixpunct{(}\matrixna{#1}\matrixpunct{)}} +\providecommand{\matrixstatus}[2]{\textcolor{#1}{\textsc{#2}}} +\providecommand{\matrixncabsolute}[1]{#1} +\providecommand{\matrixsummaryratio}[2]{\textcolor{#1}{\texttt{x#2}}} +\providecommand{\matrixsummaryratiohighlight}[2]{\begingroup\setlength{\fboxsep}{0.9pt}\setlength{\fboxrule}{0.35pt}\fbox{\textcolor{#1}{{\usefont{T1}{lmtt}{b}{n}x#2}}}\endgroup} +\providecommand{\matrixsummarystats}[5]{\begingroup\matrixsummaryfont\begin{tabular}[t]{@{}l@{\hspace{0.014in}\matrixpunct{|}\hspace{0.014in}}l@{\hspace{0.014in}\matrixpunct{|}\hspace{0.014in}}l@{\hspace{0.014in}\matrixpunct{|}\hspace{0.014in}}l@{\hspace{0.014in}\matrixpunct{|}\hspace{0.014in}}l@{}}\makebox[3.6em][l]{#1}&\makebox[4.6em][l]{#2}&\makebox[3.6em][l]{#3}&\makebox[3.6em][l]{#4}&\makebox[4.2em][l]{#5}\end{tabular}\endgroup} +\providecommand{\matrixsummaryworkloads}[2]{\begin{tabular}[t]{@{}l@{}}#1\\[-0.10em]#2\end{tabular}} +\providecommand{\matrixpair}[2]{\begin{tabular}[t]{@{}r@{\hspace{0.025in}\matrixpunct{|}\hspace{0.025in}}r@{}}#1\end{tabular}} +\providecommand{\bestmoderatio}[2]{\textcolor{#1}{\texttt{x#2}}} +\providecommand{\bestmodecompactratio}[3]{\matrixpunct{(}\textcolor{ReportMuted}{\texttt{[x#1]}}\hspace{0.04in}\textcolor{#2}{\texttt{x#3}}\matrixpunct{)}} +\providecommand{\bestmodewallratio}[2]{\matrixpunct{(}\textcolor{#1}{\texttt{x#2}}\matrixpunct{)}} +\providecommand{\bestmodecompactprefix}[1]{\matrixpunct{(}\textcolor{ReportMuted}{\texttt{[x#1]}}\hspace{0.04in}} +\providecommand{\bestmodeopenprefix}{\matrixpunct{(}\hspace{0.04in}} +\providecommand{\bestmodeprimaryratio}[2]{\textcolor{#1}{\texttt{x#2}}\matrixpunct{)}} +\providecommand{\bestmodeabsoluteprefix}[1]{#1\hspace{0.04in}} +\providecommand{\bestmodecompactabsolute}[2]{\textcolor{ReportMuted}{\texttt{[#1]}}\hspace{0.04in}\textcolor{black}{\texttt{#2}}} +\providecommand{\bestmodecompactabsoluteprefix}[1]{\textcolor{ReportMuted}{\texttt{[#1]}}\hspace{0.04in}} +\providecommand{\bestmodeabsoluteprimary}[1]{\textcolor{black}{\texttt{#1}}} +\providecommand{\bestmodeunverifiedclockprefix}[2]{\textcolor{ReportMuted}{\texttt{[}#1\texttt{]}}\hspace{0.04in}#2\hspace{0.04in}} +\clearpage +\subsection{UFO-SM recurrence versus AmpliCol full-colour} +\noindent\begin{minipage}{\linewidth} +\subsubsection*{UFO-SM recurrence versus AmpliCol full-colour (block 1 of 2)} +\begingroup +\centering +\footnotesize +\setlength{\tabcolsep}{2.1pt} +\renewcommand{\arraystretch}{1.04} +\matrixfitwidth{% +\begin{tabular}{@{}rL{1.45in}l>{\matrixentryfont}l>{\matrixentryfont}r@{}>{\matrixentryfont}l>{\matrixentryfont}l>{\matrixentryfont}r@{}>{\matrixentryfont}l>{\matrixentryfont}l>{\matrixentryfont}r@{}>{\matrixentryfont}l@{}} +\toprule +\textbf{ID} & \textbf{base process} & \textbf{metric} & \multicolumn{3}{c}{\textbf{n=1}} & \multicolumn{3}{c}{\textbf{n=2}} & \multicolumn{3}{c}{\textbf{n=3}} \\ +\specialrule{0.85pt}{0pt}{0pt} +\rowcolor{refblue} +\texttt{1} & $d\bar d\to Z+(n-1)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{2} & $u\bar d\to W^++(n-1)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{3} & $d\bar d\to e^+e^-+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{4} & $u\bar d\to e^+\nu_e+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{5} & $d\bar d\to ZZ+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{6} & $gg\to t\bar t+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{7} & $d\bar d\to t\bar t+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{8} & $gg\to gg+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{9} & $d\bar d\to ZZZ+(n-3)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{10} & $d\bar d\to e^+e^-ZH+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{11} & $d\bar d\to t\bar t ZH+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{12} & $d\bar d\to e^+e^-e^+e^-+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{13} & $d\bar d\to u\bar u\,s\bar s+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{14} & $d\bar d\to u\bar u\,s\bar s\,c\bar c+(n-6)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{15} & $d\bar d\to u\bar u\,u\bar u+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\specialrule{1.05pt}{0.22em}{0.18em} +\multicolumn{3}{@{}l}{} & \multicolumn{3}{c}{\cellcolor{refblue}\textbf{n=1}} & \multicolumn{3}{c}{\textbf{n=2}} & \multicolumn{3}{c}{\cellcolor{refblue}\textbf{n=3}} \\ +\multicolumn{3}{@{}l}{\textbf{summary: generation}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} & \multicolumn{3}{c}{\matrixna{ReportMuted}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} \\[0.08em] +\multicolumn{3}{@{}l}{\textbf{summary: wall}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} & \multicolumn{3}{c}{\matrixna{ReportMuted}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} \\ +\bottomrule +\end{tabular} +} +\endgroup +\ReportTableNote{{\scriptsize Baseline: original AmpliCol; candidate: recurrence JIT O2. Each cell shows the baseline generation and wall time, followed by candidate/baseline generation and wall-time ratios. Runtime comparisons use ([xS]xW). The muted supplementary xS first compares compatible exposed execution attributions; otherwise it compares authenticated evaluator totals. Only when original AmpliCol has no evaluator-total clock may a candidate evaluator total use the legacy direct execution clock (or its wall clock when direct execution is absent) as denominator. An evaluator total is never divided by a recurrence core. This evaluator-total fallback is diagnostic only; the execution/execution ratio remains the compatible supplementary attribution. The coloured xW is always the candidate/baseline wall multiplier and remains the primary figure of merit. When no compatible supplementary clock exists, only (xW) is shown. No clock is fabricated from another. Every displayed number uses exactly three significant digits. Original AmpliCol is static N/A beyond three open quark lines; unavailable, terminal, or unlinked baselines keep their status while successful candidate timings remain absolute and leave summaries. Summary rows contain multipliers only in min, max, median, average, and weighted-average order; the weighted average is the ratio of timing sums, is the framed bold entry, and runtime statistics use wall time only. Not applicable marks a multiplicity for which the process family does not exist. Not run marks an otherwise defined process that was intentionally excluded from the declared measurement scope. Fixed-engine tables intentionally omit mode letters because the selected engine is named in the table heading.}} +\end{minipage} +\clearpage +\noindent\begin{minipage}{\linewidth} +\subsubsection*{UFO-SM recurrence versus AmpliCol full-colour (block 2 of 2)} +\begingroup +\centering +\footnotesize +\setlength{\tabcolsep}{2.1pt} +\renewcommand{\arraystretch}{1.04} +\matrixfitwidth{% +\begin{tabular}{@{}rL{1.45in}l>{\matrixentryfont}l>{\matrixentryfont}r@{}>{\matrixentryfont}l>{\matrixentryfont}l>{\matrixentryfont}r@{}>{\matrixentryfont}l>{\matrixentryfont}l>{\matrixentryfont}r@{}>{\matrixentryfont}l@{}} +\toprule +\textbf{ID} & \textbf{base process} & \textbf{metric} & \multicolumn{3}{c}{\textbf{n=4}} & \multicolumn{3}{c}{\textbf{n=5}} & \multicolumn{3}{c}{\textbf{n=6}} \\ +\specialrule{0.85pt}{0pt}{0pt} +\rowcolor{refblue} +\texttt{1} & $d\bar d\to Z+(n-1)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{2} & $u\bar d\to W^++(n-1)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{3} & $d\bar d\to e^+e^-+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{4} & $u\bar d\to e^+\nu_e+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{5} & $d\bar d\to ZZ+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{6} & $gg\to t\bar t+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{7} & $d\bar d\to t\bar t+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{8} & $gg\to gg+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{9} & $d\bar d\to ZZZ+(n-3)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{10} & $d\bar d\to e^+e^-ZH+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{11} & $d\bar d\to t\bar t ZH+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{12} & $d\bar d\to e^+e^-e^+e^-+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{13} & $d\bar d\to u\bar u\,s\bar s+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{14} & $d\bar d\to u\bar u\,s\bar s\,c\bar c+(n-6)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstaticna{ReportMuted} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \matrixstaticna{ReportMuted} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{15} & $d\bar d\to u\bar u\,u\bar u+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\specialrule{1.05pt}{0.22em}{0.18em} +\multicolumn{3}{@{}l}{} & \multicolumn{3}{c}{\cellcolor{refblue}\textbf{n=4}} & \multicolumn{3}{c}{\textbf{n=5}} & \multicolumn{3}{c}{\cellcolor{refblue}\textbf{n=6}} \\ +\multicolumn{3}{@{}l}{\textbf{summary: generation}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} & \multicolumn{3}{c}{\matrixna{ReportMuted}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} \\[0.08em] +\multicolumn{3}{@{}l}{\textbf{summary: wall}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} & \multicolumn{3}{c}{\matrixna{ReportMuted}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} \\ +\bottomrule +\end{tabular} +} +\endgroup +\ReportTableNote{{\scriptsize Baseline: original AmpliCol; candidate: recurrence JIT O2. Each cell shows the baseline generation and wall time, followed by candidate/baseline generation and wall-time ratios. Runtime comparisons use ([xS]xW). The muted supplementary xS first compares compatible exposed execution attributions; otherwise it compares authenticated evaluator totals. Only when original AmpliCol has no evaluator-total clock may a candidate evaluator total use the legacy direct execution clock (or its wall clock when direct execution is absent) as denominator. An evaluator total is never divided by a recurrence core. This evaluator-total fallback is diagnostic only; the execution/execution ratio remains the compatible supplementary attribution. The coloured xW is always the candidate/baseline wall multiplier and remains the primary figure of merit. When no compatible supplementary clock exists, only (xW) is shown. No clock is fabricated from another. Every displayed number uses exactly three significant digits. Original AmpliCol is static N/A beyond three open quark lines; unavailable, terminal, or unlinked baselines keep their status while successful candidate timings remain absolute and leave summaries. Summary rows contain multipliers only in min, max, median, average, and weighted-average order; the weighted average is the ratio of timing sums, is the framed bold entry, and runtime statistics use wall time only. Not applicable marks a multiplicity for which the process family does not exist. Not run marks an otherwise defined process that was intentionally excluded from the declared measurement scope. Fixed-engine tables intentionally omit mode letters because the selected engine is named in the table heading.}} +\end{minipage} diff --git a/pyamplicol/src/pyamplicol/_profiling_campaign/result_matrix_recurrence_ufo_sm_full_vs_madgraph_table.tex b/pyamplicol/src/pyamplicol/_profiling_campaign/result_matrix_recurrence_ufo_sm_full_vs_madgraph_table.tex new file mode 100644 index 0000000000..af6e00faf9 --- /dev/null +++ b/pyamplicol/src/pyamplicol/_profiling_campaign/result_matrix_recurrence_ufo_sm_full_vs_madgraph_table.tex @@ -0,0 +1,208 @@ +% SPDX-License-Identifier: 0BSD +% Generated by tools/performance_report/render.py; do not edit. +\providecommand{\matrixentryfont}{\fontsize{6.8pt}{7.8pt}\selectfont} +\providecommand{\matrixentryfontlc}{\fontsize{6.5pt}{7.5pt}\selectfont} +\providecommand{\matrixsummaryfont}{\fontsize{6.2pt}{7.4pt}\selectfont} +\providecommand{\matrixfitwidth}[1]{\begingroup\sbox0{#1}\ifdim\wd0>\linewidth\resizebox{\linewidth}{!}{\usebox0}\else\makebox[\linewidth][c]{\usebox0}\fi\endgroup} +\providecommand{\matrixpunct}[1]{\textcolor{black}{\texttt{#1}}} +\providecommand{\matrixratio}[2]{\matrixpunct{(}\textcolor{#1}{\texttt{x#2}}\matrixpunct{)}} +\providecommand{\matrixna}[1]{\textcolor{#1}{\texttt{N/A}}} +\providecommand{\matrixnotapplicable}[1]{\textcolor{#1}{\textsc{not applicable}}} +\providecommand{\matrixnotexposed}[1]{\textcolor{#1}{\textsc{not exposed}}} +\providecommand{\matrixstaticna}[1]{\textcolor{#1}{\textsc{static N/A}}} +\providecommand{\matrixtotalevaluator}[1]{\textcolor{ReportBlue}{\texttt{T }}#1} +\providecommand{\matrixrecurrencecore}[1]{\textcolor{ReportOrange}{\texttt{C }}#1} +\providecommand{\matrixzrecurrenceclocks}[2]{\shortstack[r]{#1\\#2}} +\providecommand{\matrixwallclock}[2]{\textcolor{#1}{\texttt{W #2}}} +\providecommand{\matrixwallabsolute}[1]{\textcolor{ReportBlue}{\texttt{W }}#1} +\providecommand{\matrixruntimepair}[2]{\shortstack[l]{#1\\#2}} +\providecommand{\matrixruntimetriple}[3]{\shortstack[l]{#1\\#2\matrixpunct{ | }#3}} +\providecommand{\matrixbelow}[1]{\textcolor{ReportMuted}{\texttt{<#1}}} +\providecommand{\matrixnaratio}[1]{\matrixpunct{(}\matrixna{#1}\matrixpunct{)}} +\providecommand{\matrixstatus}[2]{\textcolor{#1}{\textsc{#2}}} +\providecommand{\matrixncabsolute}[1]{#1} +\providecommand{\matrixsummaryratio}[2]{\textcolor{#1}{\texttt{x#2}}} +\providecommand{\matrixsummaryratiohighlight}[2]{\begingroup\setlength{\fboxsep}{0.9pt}\setlength{\fboxrule}{0.35pt}\fbox{\textcolor{#1}{{\usefont{T1}{lmtt}{b}{n}x#2}}}\endgroup} +\providecommand{\matrixsummarystats}[5]{\begingroup\matrixsummaryfont\begin{tabular}[t]{@{}l@{\hspace{0.014in}\matrixpunct{|}\hspace{0.014in}}l@{\hspace{0.014in}\matrixpunct{|}\hspace{0.014in}}l@{\hspace{0.014in}\matrixpunct{|}\hspace{0.014in}}l@{\hspace{0.014in}\matrixpunct{|}\hspace{0.014in}}l@{}}\makebox[3.6em][l]{#1}&\makebox[4.6em][l]{#2}&\makebox[3.6em][l]{#3}&\makebox[3.6em][l]{#4}&\makebox[4.2em][l]{#5}\end{tabular}\endgroup} +\providecommand{\matrixsummaryworkloads}[2]{\begin{tabular}[t]{@{}l@{}}#1\\[-0.10em]#2\end{tabular}} +\providecommand{\matrixpair}[2]{\begin{tabular}[t]{@{}r@{\hspace{0.025in}\matrixpunct{|}\hspace{0.025in}}r@{}}#1\end{tabular}} +\providecommand{\bestmoderatio}[2]{\textcolor{#1}{\texttt{x#2}}} +\providecommand{\bestmodecompactratio}[3]{\matrixpunct{(}\textcolor{ReportMuted}{\texttt{[x#1]}}\hspace{0.04in}\textcolor{#2}{\texttt{x#3}}\matrixpunct{)}} +\providecommand{\bestmodewallratio}[2]{\matrixpunct{(}\textcolor{#1}{\texttt{x#2}}\matrixpunct{)}} +\providecommand{\bestmodecompactprefix}[1]{\matrixpunct{(}\textcolor{ReportMuted}{\texttt{[x#1]}}\hspace{0.04in}} +\providecommand{\bestmodeopenprefix}{\matrixpunct{(}\hspace{0.04in}} +\providecommand{\bestmodeprimaryratio}[2]{\textcolor{#1}{\texttt{x#2}}\matrixpunct{)}} +\providecommand{\bestmodeabsoluteprefix}[1]{#1\hspace{0.04in}} +\providecommand{\bestmodecompactabsolute}[2]{\textcolor{ReportMuted}{\texttt{[#1]}}\hspace{0.04in}\textcolor{black}{\texttt{#2}}} +\providecommand{\bestmodecompactabsoluteprefix}[1]{\textcolor{ReportMuted}{\texttt{[#1]}}\hspace{0.04in}} +\providecommand{\bestmodeabsoluteprimary}[1]{\textcolor{black}{\texttt{#1}}} +\providecommand{\bestmodeunverifiedclockprefix}[2]{\textcolor{ReportMuted}{\texttt{[}#1\texttt{]}}\hspace{0.04in}#2\hspace{0.04in}} +\clearpage +\subsection{UFO-SM recurrence JIT O2 versus MadGraph full-colour} +\noindent\begin{minipage}{\linewidth} +\subsubsection*{UFO-SM recurrence JIT O2 versus MadGraph full-colour (block 1 of 2)} +\begingroup +\centering +\footnotesize +\setlength{\tabcolsep}{2.1pt} +\renewcommand{\arraystretch}{1.04} +\matrixfitwidth{% +\begin{tabular}{@{}rL{1.45in}l>{\matrixentryfont}l>{\matrixentryfont}r@{}>{\matrixentryfont}l>{\matrixentryfont}l>{\matrixentryfont}r@{}>{\matrixentryfont}l>{\matrixentryfont}l>{\matrixentryfont}r@{}>{\matrixentryfont}l@{}} +\toprule +\textbf{ID} & \textbf{base process} & \textbf{metric} & \multicolumn{3}{c}{\textbf{n=1}} & \multicolumn{3}{c}{\textbf{n=2}} & \multicolumn{3}{c}{\textbf{n=3}} \\ +\specialrule{0.85pt}{0pt}{0pt} +\rowcolor{refblue} +\texttt{1} & $d\bar d\to Z+(n-1)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{2} & $u\bar d\to W^++(n-1)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{3} & $d\bar d\to e^+e^-+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{4} & $u\bar d\to e^+\nu_e+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{5} & $d\bar d\to ZZ+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{6} & $gg\to t\bar t+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{7} & $d\bar d\to t\bar t+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{8} & $gg\to gg+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{9} & $d\bar d\to ZZZ+(n-3)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{10} & $d\bar d\to e^+e^-ZH+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{11} & $d\bar d\to t\bar t ZH+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{12} & $d\bar d\to e^+e^-e^+e^-+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{13} & $d\bar d\to u\bar u\,s\bar s+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{14} & $d\bar d\to u\bar u\,s\bar s\,c\bar c+(n-6)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{15} & $d\bar d\to u\bar u\,u\bar u+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\specialrule{1.05pt}{0.22em}{0.18em} +\multicolumn{3}{@{}l}{} & \multicolumn{3}{c}{\cellcolor{refblue}\textbf{n=1}} & \multicolumn{3}{c}{\textbf{n=2}} & \multicolumn{3}{c}{\cellcolor{refblue}\textbf{n=3}} \\ +\multicolumn{3}{@{}l}{\textbf{summary: generation}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} & \multicolumn{3}{c}{\matrixna{ReportMuted}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} \\[0.08em] +\multicolumn{3}{@{}l}{\textbf{summary: wall}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} & \multicolumn{3}{c}{\matrixna{ReportMuted}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} \\ +\bottomrule +\end{tabular} +} +\endgroup +\ReportTableNote{{\scriptsize Baseline: MadGraph standalone; candidate: recurrence JIT O2. Each cell shows the baseline generation and wall time, followed by candidate/baseline generation and wall-time ratios. Runtime comparisons use ([xS]xW). The muted supplementary xS first compares compatible exposed execution attributions; otherwise it compares authenticated evaluator totals. Only when original AmpliCol has no evaluator-total clock may a candidate evaluator total use the legacy direct execution clock (or its wall clock when direct execution is absent) as denominator. An evaluator total is never divided by a recurrence core. This evaluator-total fallback is diagnostic only; the execution/execution ratio remains the compatible supplementary attribution. The coloured xW is always the candidate/baseline wall multiplier and remains the primary figure of merit. When no compatible supplementary clock exists, only (xW) is shown. No clock is fabricated from another. Every displayed number uses exactly three significant digits. MadGraph standalone is the authoritative full-colour numerical reference; its generation and repeated-evaluation measurements are shared by all four UFO-SM comparison views. Recurrence, compiled, and eager candidates use p200 at the comparison point; the on-the-fly candidate uses its supported p16 lane. Ratios and summaries require exact stored linkage to the current denominator. Summary rows contain multipliers only in min, max, median, average, and weighted-average order; the weighted average is the ratio of timing sums, is the framed bold entry, and runtime statistics use wall time only. Not applicable marks a multiplicity for which the process family does not exist. Not run marks an otherwise defined process that was intentionally excluded from the declared measurement scope. Fixed-engine tables intentionally omit mode letters because the selected engine is named in the table heading.}} +\end{minipage} +\clearpage +\noindent\begin{minipage}{\linewidth} +\subsubsection*{UFO-SM recurrence JIT O2 versus MadGraph full-colour (block 2 of 2)} +\begingroup +\centering +\footnotesize +\setlength{\tabcolsep}{2.1pt} +\renewcommand{\arraystretch}{1.04} +\matrixfitwidth{% +\begin{tabular}{@{}rL{1.45in}l>{\matrixentryfont}l>{\matrixentryfont}r@{}>{\matrixentryfont}l>{\matrixentryfont}l>{\matrixentryfont}r@{}>{\matrixentryfont}l>{\matrixentryfont}l>{\matrixentryfont}r@{}>{\matrixentryfont}l@{}} +\toprule +\textbf{ID} & \textbf{base process} & \textbf{metric} & \multicolumn{3}{c}{\textbf{n=4}} & \multicolumn{3}{c}{\textbf{n=5}} & \multicolumn{3}{c}{\textbf{n=6}} \\ +\specialrule{0.85pt}{0pt}{0pt} +\rowcolor{refblue} +\texttt{1} & $d\bar d\to Z+(n-1)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{2} & $u\bar d\to W^++(n-1)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{3} & $d\bar d\to e^+e^-+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{4} & $u\bar d\to e^+\nu_e+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{5} & $d\bar d\to ZZ+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{6} & $gg\to t\bar t+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{7} & $d\bar d\to t\bar t+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{8} & $gg\to gg+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{9} & $d\bar d\to ZZZ+(n-3)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{10} & $d\bar d\to e^+e^-ZH+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{11} & $d\bar d\to t\bar t ZH+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{12} & $d\bar d\to e^+e^-e^+e^-+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{13} & $d\bar d\to u\bar u\,s\bar s+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{14} & $d\bar d\to u\bar u\,s\bar s\,c\bar c+(n-6)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{15} & $d\bar d\to u\bar u\,u\bar u+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\specialrule{1.05pt}{0.22em}{0.18em} +\multicolumn{3}{@{}l}{} & \multicolumn{3}{c}{\cellcolor{refblue}\textbf{n=4}} & \multicolumn{3}{c}{\textbf{n=5}} & \multicolumn{3}{c}{\cellcolor{refblue}\textbf{n=6}} \\ +\multicolumn{3}{@{}l}{\textbf{summary: generation}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} & \multicolumn{3}{c}{\matrixna{ReportMuted}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} \\[0.08em] +\multicolumn{3}{@{}l}{\textbf{summary: wall}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} & \multicolumn{3}{c}{\matrixna{ReportMuted}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} \\ +\bottomrule +\end{tabular} +} +\endgroup +\ReportTableNote{{\scriptsize Baseline: MadGraph standalone; candidate: recurrence JIT O2. Each cell shows the baseline generation and wall time, followed by candidate/baseline generation and wall-time ratios. Runtime comparisons use ([xS]xW). The muted supplementary xS first compares compatible exposed execution attributions; otherwise it compares authenticated evaluator totals. Only when original AmpliCol has no evaluator-total clock may a candidate evaluator total use the legacy direct execution clock (or its wall clock when direct execution is absent) as denominator. An evaluator total is never divided by a recurrence core. This evaluator-total fallback is diagnostic only; the execution/execution ratio remains the compatible supplementary attribution. The coloured xW is always the candidate/baseline wall multiplier and remains the primary figure of merit. When no compatible supplementary clock exists, only (xW) is shown. No clock is fabricated from another. Every displayed number uses exactly three significant digits. MadGraph standalone is the authoritative full-colour numerical reference; its generation and repeated-evaluation measurements are shared by all four UFO-SM comparison views. Recurrence, compiled, and eager candidates use p200 at the comparison point; the on-the-fly candidate uses its supported p16 lane. Ratios and summaries require exact stored linkage to the current denominator. Summary rows contain multipliers only in min, max, median, average, and weighted-average order; the weighted average is the ratio of timing sums, is the framed bold entry, and runtime statistics use wall time only. Not applicable marks a multiplicity for which the process family does not exist. Not run marks an otherwise defined process that was intentionally excluded from the declared measurement scope. Fixed-engine tables intentionally omit mode letters because the selected engine is named in the table heading.}} +\end{minipage} diff --git a/pyamplicol/src/pyamplicol/_profiling_campaign/result_matrix_recurrence_ufo_sm_lc_table.tex b/pyamplicol/src/pyamplicol/_profiling_campaign/result_matrix_recurrence_ufo_sm_lc_table.tex new file mode 100644 index 0000000000..13ea78895d --- /dev/null +++ b/pyamplicol/src/pyamplicol/_profiling_campaign/result_matrix_recurrence_ufo_sm_lc_table.tex @@ -0,0 +1,292 @@ +% SPDX-License-Identifier: 0BSD +% Generated by tools/performance_report/render.py; do not edit. +\providecommand{\matrixentryfont}{\fontsize{6.8pt}{7.8pt}\selectfont} +\providecommand{\matrixentryfontlc}{\fontsize{6.5pt}{7.5pt}\selectfont} +\providecommand{\matrixsummaryfont}{\fontsize{6.2pt}{7.4pt}\selectfont} +\providecommand{\matrixfitwidth}[1]{\begingroup\sbox0{#1}\ifdim\wd0>\linewidth\resizebox{\linewidth}{!}{\usebox0}\else\makebox[\linewidth][c]{\usebox0}\fi\endgroup} +\providecommand{\matrixpunct}[1]{\textcolor{black}{\texttt{#1}}} +\providecommand{\matrixratio}[2]{\matrixpunct{(}\textcolor{#1}{\texttt{x#2}}\matrixpunct{)}} +\providecommand{\matrixna}[1]{\textcolor{#1}{\texttt{N/A}}} +\providecommand{\matrixnotapplicable}[1]{\textcolor{#1}{\textsc{not applicable}}} +\providecommand{\matrixnotexposed}[1]{\textcolor{#1}{\textsc{not exposed}}} +\providecommand{\matrixstaticna}[1]{\textcolor{#1}{\textsc{static N/A}}} +\providecommand{\matrixtotalevaluator}[1]{\textcolor{ReportBlue}{\texttt{T }}#1} +\providecommand{\matrixrecurrencecore}[1]{\textcolor{ReportOrange}{\texttt{C }}#1} +\providecommand{\matrixzrecurrenceclocks}[2]{\shortstack[r]{#1\\#2}} +\providecommand{\matrixwallclock}[2]{\textcolor{#1}{\texttt{W #2}}} +\providecommand{\matrixwallabsolute}[1]{\textcolor{ReportBlue}{\texttt{W }}#1} +\providecommand{\matrixruntimepair}[2]{\shortstack[l]{#1\\#2}} +\providecommand{\matrixruntimetriple}[3]{\shortstack[l]{#1\\#2\matrixpunct{ | }#3}} +\providecommand{\matrixbelow}[1]{\textcolor{ReportMuted}{\texttt{<#1}}} +\providecommand{\matrixnaratio}[1]{\matrixpunct{(}\matrixna{#1}\matrixpunct{)}} +\providecommand{\matrixstatus}[2]{\textcolor{#1}{\textsc{#2}}} +\providecommand{\matrixncabsolute}[1]{#1} +\providecommand{\matrixsummaryratio}[2]{\textcolor{#1}{\texttt{x#2}}} +\providecommand{\matrixsummaryratiohighlight}[2]{\begingroup\setlength{\fboxsep}{0.9pt}\setlength{\fboxrule}{0.35pt}\fbox{\textcolor{#1}{{\usefont{T1}{lmtt}{b}{n}x#2}}}\endgroup} +\providecommand{\matrixsummarystats}[5]{\begingroup\matrixsummaryfont\begin{tabular}[t]{@{}l@{\hspace{0.014in}\matrixpunct{|}\hspace{0.014in}}l@{\hspace{0.014in}\matrixpunct{|}\hspace{0.014in}}l@{\hspace{0.014in}\matrixpunct{|}\hspace{0.014in}}l@{\hspace{0.014in}\matrixpunct{|}\hspace{0.014in}}l@{}}\makebox[3.6em][l]{#1}&\makebox[4.6em][l]{#2}&\makebox[3.6em][l]{#3}&\makebox[3.6em][l]{#4}&\makebox[4.2em][l]{#5}\end{tabular}\endgroup} +\providecommand{\matrixsummaryworkloads}[2]{\begin{tabular}[t]{@{}l@{}}#1\\[-0.10em]#2\end{tabular}} +\providecommand{\matrixpair}[2]{\begin{tabular}[t]{@{}r@{\hspace{0.025in}\matrixpunct{|}\hspace{0.025in}}r@{}}#1\end{tabular}} +\providecommand{\bestmoderatio}[2]{\textcolor{#1}{\texttt{x#2}}} +\providecommand{\bestmodecompactratio}[3]{\matrixpunct{(}\textcolor{ReportMuted}{\texttt{[x#1]}}\hspace{0.04in}\textcolor{#2}{\texttt{x#3}}\matrixpunct{)}} +\providecommand{\bestmodewallratio}[2]{\matrixpunct{(}\textcolor{#1}{\texttt{x#2}}\matrixpunct{)}} +\providecommand{\bestmodecompactprefix}[1]{\matrixpunct{(}\textcolor{ReportMuted}{\texttt{[x#1]}}\hspace{0.04in}} +\providecommand{\bestmodeopenprefix}{\matrixpunct{(}\hspace{0.04in}} +\providecommand{\bestmodeprimaryratio}[2]{\textcolor{#1}{\texttt{x#2}}\matrixpunct{)}} +\providecommand{\bestmodeabsoluteprefix}[1]{#1\hspace{0.04in}} +\providecommand{\bestmodecompactabsolute}[2]{\textcolor{ReportMuted}{\texttt{[#1]}}\hspace{0.04in}\textcolor{black}{\texttt{#2}}} +\providecommand{\bestmodecompactabsoluteprefix}[1]{\textcolor{ReportMuted}{\texttt{[#1]}}\hspace{0.04in}} +\providecommand{\bestmodeabsoluteprimary}[1]{\textcolor{black}{\texttt{#1}}} +\providecommand{\bestmodeunverifiedclockprefix}[2]{\textcolor{ReportMuted}{\texttt{[}#1\texttt{]}}\hspace{0.04in}#2\hspace{0.04in}} +\clearpage +\subsection{UFO-SM recurrence versus AmpliCol LC} +\noindent\begin{minipage}{\linewidth} +\subsubsection*{UFO-SM recurrence versus AmpliCol LC (block 1 of 3)} +\begingroup +\centering +\footnotesize +\setlength{\tabcolsep}{2.1pt} +\renewcommand{\arraystretch}{1.04} +\matrixfitwidth{% +\begin{tabular}{@{}rL{1.45in}l>{\matrixentryfontlc}l>{\matrixentryfontlc}c>{\matrixentryfontlc}l>{\matrixentryfontlc}r@{}>{\matrixentryfontlc}l>{\matrixentryfontlc}c>{\matrixentryfontlc}r@{}>{\matrixentryfontlc}l>{\matrixentryfontlc}l>{\matrixentryfontlc}c>{\matrixentryfontlc}l>{\matrixentryfontlc}r@{}>{\matrixentryfontlc}l>{\matrixentryfontlc}c>{\matrixentryfontlc}r@{}>{\matrixentryfontlc}l>{\matrixentryfontlc}l>{\matrixentryfontlc}c>{\matrixentryfontlc}l>{\matrixentryfontlc}r@{}>{\matrixentryfontlc}l>{\matrixentryfontlc}c>{\matrixentryfontlc}r@{}>{\matrixentryfontlc}l@{}} +\toprule +\textbf{ID} & \textbf{base process} & \textbf{metric} & \multicolumn{8}{c}{\textbf{n=1}} & \multicolumn{8}{c}{\textbf{n=2}} & \multicolumn{8}{c}{\textbf{n=3}} \\ +\specialrule{0.85pt}{0pt}{0pt} +\rowcolor{refblue} +\texttt{1} & $d\bar d\to Z+(n-1)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{2} & $u\bar d\to W^++(n-1)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{3} & $d\bar d\to e^+e^-+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{8}{c}{} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{4} & $u\bar d\to e^+\nu_e+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{8}{c}{} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{5} & $d\bar d\to ZZ+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{8}{c}{} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{6} & $gg\to t\bar t+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{8}{c}{} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{7} & $d\bar d\to t\bar t+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{8}{c}{} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{8} & $gg\to gg+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{8}{c}{} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{9} & $d\bar d\to ZZZ+(n-3)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{8}{c}{} & \multicolumn{8}{c}{} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{10} & $d\bar d\to e^+e^-ZH+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{8}{c}{} & \multicolumn{8}{c}{} & \multicolumn{8}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{11} & $d\bar d\to t\bar t ZH+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{8}{c}{} & \multicolumn{8}{c}{} & \multicolumn{8}{c}{} \\ +\addlinespace[0.05em] +\texttt{12} & $d\bar d\to e^+e^-e^+e^-+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{8}{c}{} & \multicolumn{8}{c}{} & \multicolumn{8}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{13} & $d\bar d\to u\bar u\,s\bar s+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{8}{c}{} & \multicolumn{8}{c}{} & \multicolumn{8}{c}{} \\ +\addlinespace[0.05em] +\texttt{14} & $d\bar d\to u\bar u\,s\bar s\,c\bar c+(n-6)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{8}{c}{} & \multicolumn{8}{c}{} & \multicolumn{8}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{15} & $d\bar d\to u\bar u\,u\bar u+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{8}{c}{} & \multicolumn{8}{c}{} & \multicolumn{8}{c}{} \\ +\addlinespace[0.05em] +\specialrule{1.05pt}{0.22em}{0.18em} +\multicolumn{3}{@{}l}{} & \multicolumn{8}{c}{\cellcolor{refblue}\textbf{n=1}} & \multicolumn{8}{c}{\textbf{n=2}} & \multicolumn{8}{c}{\cellcolor{refblue}\textbf{n=3}} \\ +\multicolumn{3}{@{}l}{\textbf{summary: generation}} & \multicolumn{8}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} & \multicolumn{8}{c}{\matrixna{ReportMuted}} & \multicolumn{8}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} \\[0.08em] +\multicolumn{3}{@{}l}{\matrixsummaryworkloads{\textbf{summary: run single-flow, hel. sum}}{\textbf{summary: run all-flows, single hel.}}} & \multicolumn{8}{c}{\cellcolor{refblue}\matrixsummaryworkloads{\matrixna{ReportMuted}}{\matrixna{ReportMuted}}} & \multicolumn{8}{c}{\matrixsummaryworkloads{\matrixna{ReportMuted}}{\matrixna{ReportMuted}}} & \multicolumn{8}{c}{\cellcolor{refblue}\matrixsummaryworkloads{\matrixna{ReportMuted}}{\matrixna{ReportMuted}}} \\ +\bottomrule +\end{tabular} +} +\endgroup +\ReportTableNote{{\scriptsize Baseline: original AmpliCol; candidate: recurrence JIT O2. Each cell shows the selected-flow library-generation and all-flow direct-setup baseline quantities. The selected-flow entry carries a generation ratio; the all-flow entry carries the absolute pyAmpliCol process-generation time. Its setup boundary differs from the reference, so no generation ratio is formed. Runtime comparisons use ([xS]xW). The muted supplementary xS first compares compatible exposed execution attributions; otherwise it compares authenticated evaluator totals. Only when original AmpliCol has no evaluator-total clock may a candidate evaluator total use the legacy direct execution clock (or its wall clock when direct execution is absent) as denominator. An evaluator total is never divided by a recurrence core. This evaluator-total fallback is diagnostic only; the execution/execution ratio remains the compatible supplementary attribution. The coloured xW is always the candidate/baseline wall multiplier and remains the primary figure of merit. When no compatible supplementary clock exists, only (xW) is shown. No clock is fabricated from another. Every displayed number uses exactly three significant digits. Original AmpliCol is static N/A beyond three open quark lines; unavailable, terminal, or unlinked baselines keep their status while successful candidate timings remain absolute and leave summaries. Summary rows contain multipliers only in min, max, median, average, and weighted-average order; the weighted average is the ratio of timing sums and is the framed bold entry. LC generation uses selected flow only, while LC runtime shows separate selected-flow and all-flow wall-only lines. Not applicable marks a multiplicity for which the process family does not exist. Not run marks an otherwise defined process that was intentionally excluded from the declared measurement scope. Fixed-engine tables intentionally omit mode letters because the selected engine is named in the table heading.}} +\end{minipage} +\clearpage +\noindent\begin{minipage}{\linewidth} +\subsubsection*{UFO-SM recurrence versus AmpliCol LC (block 2 of 3)} +\begingroup +\centering +\footnotesize +\setlength{\tabcolsep}{2.1pt} +\renewcommand{\arraystretch}{1.04} +\matrixfitwidth{% +\begin{tabular}{@{}rL{1.45in}l>{\matrixentryfontlc}l>{\matrixentryfontlc}c>{\matrixentryfontlc}l>{\matrixentryfontlc}r@{}>{\matrixentryfontlc}l>{\matrixentryfontlc}c>{\matrixentryfontlc}r@{}>{\matrixentryfontlc}l>{\matrixentryfontlc}l>{\matrixentryfontlc}c>{\matrixentryfontlc}l>{\matrixentryfontlc}r@{}>{\matrixentryfontlc}l>{\matrixentryfontlc}c>{\matrixentryfontlc}r@{}>{\matrixentryfontlc}l>{\matrixentryfontlc}l>{\matrixentryfontlc}c>{\matrixentryfontlc}l>{\matrixentryfontlc}r@{}>{\matrixentryfontlc}l>{\matrixentryfontlc}c>{\matrixentryfontlc}r@{}>{\matrixentryfontlc}l@{}} +\toprule +\textbf{ID} & \textbf{base process} & \textbf{metric} & \multicolumn{8}{c}{\textbf{n=4}} & \multicolumn{8}{c}{\textbf{n=5}} & \multicolumn{8}{c}{\textbf{n=6}} \\ +\specialrule{0.85pt}{0pt}{0pt} +\rowcolor{refblue} +\texttt{1} & $d\bar d\to Z+(n-1)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{2} & $u\bar d\to W^++(n-1)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{3} & $d\bar d\to e^+e^-+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{4} & $u\bar d\to e^+\nu_e+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{5} & $d\bar d\to ZZ+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{6} & $gg\to t\bar t+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{7} & $d\bar d\to t\bar t+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{8} & $gg\to gg+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{9} & $d\bar d\to ZZZ+(n-3)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{10} & $d\bar d\to e^+e^-ZH+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{11} & $d\bar d\to t\bar t ZH+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{12} & $d\bar d\to e^+e^-e^+e^-+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{13} & $d\bar d\to u\bar u\,s\bar s+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{14} & $d\bar d\to u\bar u\,s\bar s\,c\bar c+(n-6)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{8}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstaticna{ReportMuted} & \matrixpunct{|} & \matrixstaticna{ReportMuted} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{8}{c}{} & \multicolumn{8}{c}{} & \matrixstaticna{ReportMuted} & \matrixpunct{|} & \matrixstaticna{ReportMuted} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{15} & $d\bar d\to u\bar u\,u\bar u+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\specialrule{1.05pt}{0.22em}{0.18em} +\multicolumn{3}{@{}l}{} & \multicolumn{8}{c}{\cellcolor{refblue}\textbf{n=4}} & \multicolumn{8}{c}{\textbf{n=5}} & \multicolumn{8}{c}{\cellcolor{refblue}\textbf{n=6}} \\ +\multicolumn{3}{@{}l}{\textbf{summary: generation}} & \multicolumn{8}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} & \multicolumn{8}{c}{\matrixna{ReportMuted}} & \multicolumn{8}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} \\[0.08em] +\multicolumn{3}{@{}l}{\matrixsummaryworkloads{\textbf{summary: run single-flow, hel. sum}}{\textbf{summary: run all-flows, single hel.}}} & \multicolumn{8}{c}{\cellcolor{refblue}\matrixsummaryworkloads{\matrixna{ReportMuted}}{\matrixna{ReportMuted}}} & \multicolumn{8}{c}{\matrixsummaryworkloads{\matrixna{ReportMuted}}{\matrixna{ReportMuted}}} & \multicolumn{8}{c}{\cellcolor{refblue}\matrixsummaryworkloads{\matrixna{ReportMuted}}{\matrixna{ReportMuted}}} \\ +\bottomrule +\end{tabular} +} +\endgroup +\ReportTableNote{{\scriptsize Baseline: original AmpliCol; candidate: recurrence JIT O2. Each cell shows the selected-flow library-generation and all-flow direct-setup baseline quantities. The selected-flow entry carries a generation ratio; the all-flow entry carries the absolute pyAmpliCol process-generation time. Its setup boundary differs from the reference, so no generation ratio is formed. Runtime comparisons use ([xS]xW). The muted supplementary xS first compares compatible exposed execution attributions; otherwise it compares authenticated evaluator totals. Only when original AmpliCol has no evaluator-total clock may a candidate evaluator total use the legacy direct execution clock (or its wall clock when direct execution is absent) as denominator. An evaluator total is never divided by a recurrence core. This evaluator-total fallback is diagnostic only; the execution/execution ratio remains the compatible supplementary attribution. The coloured xW is always the candidate/baseline wall multiplier and remains the primary figure of merit. When no compatible supplementary clock exists, only (xW) is shown. No clock is fabricated from another. Every displayed number uses exactly three significant digits. Original AmpliCol is static N/A beyond three open quark lines; unavailable, terminal, or unlinked baselines keep their status while successful candidate timings remain absolute and leave summaries. Summary rows contain multipliers only in min, max, median, average, and weighted-average order; the weighted average is the ratio of timing sums and is the framed bold entry. LC generation uses selected flow only, while LC runtime shows separate selected-flow and all-flow wall-only lines. Not applicable marks a multiplicity for which the process family does not exist. Not run marks an otherwise defined process that was intentionally excluded from the declared measurement scope. Fixed-engine tables intentionally omit mode letters because the selected engine is named in the table heading.}} +\end{minipage} +\clearpage +\noindent\begin{minipage}{\linewidth} +\subsubsection*{UFO-SM recurrence versus AmpliCol LC (block 3 of 3)} +\begingroup +\centering +\footnotesize +\setlength{\tabcolsep}{2.1pt} +\renewcommand{\arraystretch}{1.04} +\matrixfitwidth{% +\begin{tabular}{@{}rL{1.45in}l>{\matrixentryfontlc}l>{\matrixentryfontlc}c>{\matrixentryfontlc}l>{\matrixentryfontlc}r@{}>{\matrixentryfontlc}l>{\matrixentryfontlc}c>{\matrixentryfontlc}r@{}>{\matrixentryfontlc}l>{\matrixentryfontlc}l>{\matrixentryfontlc}c>{\matrixentryfontlc}l>{\matrixentryfontlc}r@{}>{\matrixentryfontlc}l>{\matrixentryfontlc}c>{\matrixentryfontlc}r@{}>{\matrixentryfontlc}l>{\matrixentryfontlc}l>{\matrixentryfontlc}c>{\matrixentryfontlc}l>{\matrixentryfontlc}r@{}>{\matrixentryfontlc}l>{\matrixentryfontlc}c>{\matrixentryfontlc}r@{}>{\matrixentryfontlc}l@{}} +\toprule +\textbf{ID} & \textbf{base process} & \textbf{metric} & \multicolumn{8}{c}{\textbf{n=7}} & \multicolumn{8}{c}{\textbf{n=8}} & \multicolumn{8}{c}{\textbf{n=9}} \\ +\specialrule{0.85pt}{0pt}{0pt} +\rowcolor{refblue} +\texttt{1} & $d\bar d\to Z+(n-1)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{2} & $u\bar d\to W^++(n-1)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{3} & $d\bar d\to e^+e^-+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{4} & $u\bar d\to e^+\nu_e+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{5} & $d\bar d\to ZZ+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{6} & $gg\to t\bar t+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{7} & $d\bar d\to t\bar t+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{8} & $gg\to gg+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{9} & $d\bar d\to ZZZ+(n-3)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{10} & $d\bar d\to e^+e^-ZH+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{11} & $d\bar d\to t\bar t ZH+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{12} & $d\bar d\to e^+e^-e^+e^-+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{13} & $d\bar d\to u\bar u\,s\bar s+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{14} & $d\bar d\to u\bar u\,s\bar s\,c\bar c+(n-6)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstaticna{ReportMuted} & \matrixpunct{|} & \matrixstaticna{ReportMuted} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstaticna{ReportMuted} & \matrixpunct{|} & \matrixstaticna{ReportMuted} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstaticna{ReportMuted} & \matrixpunct{|} & \matrixstaticna{ReportMuted} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstaticna{ReportMuted} & \matrixpunct{|} & \matrixstaticna{ReportMuted} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstaticna{ReportMuted} & \matrixpunct{|} & \matrixstaticna{ReportMuted} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstaticna{ReportMuted} & \matrixpunct{|} & \matrixstaticna{ReportMuted} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{15} & $d\bar d\to u\bar u\,u\bar u+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixpunct{|} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\specialrule{1.05pt}{0.22em}{0.18em} +\multicolumn{3}{@{}l}{} & \multicolumn{8}{c}{\cellcolor{refblue}\textbf{n=7}} & \multicolumn{8}{c}{\textbf{n=8}} & \multicolumn{8}{c}{\cellcolor{refblue}\textbf{n=9}} \\ +\multicolumn{3}{@{}l}{\textbf{summary: generation}} & \multicolumn{8}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} & \multicolumn{8}{c}{\matrixna{ReportMuted}} & \multicolumn{8}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} \\[0.08em] +\multicolumn{3}{@{}l}{\matrixsummaryworkloads{\textbf{summary: run single-flow, hel. sum}}{\textbf{summary: run all-flows, single hel.}}} & \multicolumn{8}{c}{\cellcolor{refblue}\matrixsummaryworkloads{\matrixna{ReportMuted}}{\matrixna{ReportMuted}}} & \multicolumn{8}{c}{\matrixsummaryworkloads{\matrixna{ReportMuted}}{\matrixna{ReportMuted}}} & \multicolumn{8}{c}{\cellcolor{refblue}\matrixsummaryworkloads{\matrixna{ReportMuted}}{\matrixna{ReportMuted}}} \\ +\bottomrule +\end{tabular} +} +\endgroup +\ReportTableNote{{\scriptsize Baseline: original AmpliCol; candidate: recurrence JIT O2. Each cell shows the selected-flow library-generation and all-flow direct-setup baseline quantities. The selected-flow entry carries a generation ratio; the all-flow entry carries the absolute pyAmpliCol process-generation time. Its setup boundary differs from the reference, so no generation ratio is formed. Runtime comparisons use ([xS]xW). The muted supplementary xS first compares compatible exposed execution attributions; otherwise it compares authenticated evaluator totals. Only when original AmpliCol has no evaluator-total clock may a candidate evaluator total use the legacy direct execution clock (or its wall clock when direct execution is absent) as denominator. An evaluator total is never divided by a recurrence core. This evaluator-total fallback is diagnostic only; the execution/execution ratio remains the compatible supplementary attribution. The coloured xW is always the candidate/baseline wall multiplier and remains the primary figure of merit. When no compatible supplementary clock exists, only (xW) is shown. No clock is fabricated from another. Every displayed number uses exactly three significant digits. Original AmpliCol is static N/A beyond three open quark lines; unavailable, terminal, or unlinked baselines keep their status while successful candidate timings remain absolute and leave summaries. Summary rows contain multipliers only in min, max, median, average, and weighted-average order; the weighted average is the ratio of timing sums and is the framed bold entry. LC generation uses selected flow only, while LC runtime shows separate selected-flow and all-flow wall-only lines. Not applicable marks a multiplicity for which the process family does not exist. Not run marks an otherwise defined process that was intentionally excluded from the declared measurement scope. Fixed-engine tables intentionally omit mode letters because the selected engine is named in the table heading.}} +\end{minipage} diff --git a/pyamplicol/src/pyamplicol/_profiling_campaign/result_matrix_recurrence_ufo_sm_nlc_table.tex b/pyamplicol/src/pyamplicol/_profiling_campaign/result_matrix_recurrence_ufo_sm_nlc_table.tex new file mode 100644 index 0000000000..59add0555e --- /dev/null +++ b/pyamplicol/src/pyamplicol/_profiling_campaign/result_matrix_recurrence_ufo_sm_nlc_table.tex @@ -0,0 +1,208 @@ +% SPDX-License-Identifier: 0BSD +% Generated by tools/performance_report/render.py; do not edit. +\providecommand{\matrixentryfont}{\fontsize{6.8pt}{7.8pt}\selectfont} +\providecommand{\matrixentryfontlc}{\fontsize{6.5pt}{7.5pt}\selectfont} +\providecommand{\matrixsummaryfont}{\fontsize{6.2pt}{7.4pt}\selectfont} +\providecommand{\matrixfitwidth}[1]{\begingroup\sbox0{#1}\ifdim\wd0>\linewidth\resizebox{\linewidth}{!}{\usebox0}\else\makebox[\linewidth][c]{\usebox0}\fi\endgroup} +\providecommand{\matrixpunct}[1]{\textcolor{black}{\texttt{#1}}} +\providecommand{\matrixratio}[2]{\matrixpunct{(}\textcolor{#1}{\texttt{x#2}}\matrixpunct{)}} +\providecommand{\matrixna}[1]{\textcolor{#1}{\texttt{N/A}}} +\providecommand{\matrixnotapplicable}[1]{\textcolor{#1}{\textsc{not applicable}}} +\providecommand{\matrixnotexposed}[1]{\textcolor{#1}{\textsc{not exposed}}} +\providecommand{\matrixstaticna}[1]{\textcolor{#1}{\textsc{static N/A}}} +\providecommand{\matrixtotalevaluator}[1]{\textcolor{ReportBlue}{\texttt{T }}#1} +\providecommand{\matrixrecurrencecore}[1]{\textcolor{ReportOrange}{\texttt{C }}#1} +\providecommand{\matrixzrecurrenceclocks}[2]{\shortstack[r]{#1\\#2}} +\providecommand{\matrixwallclock}[2]{\textcolor{#1}{\texttt{W #2}}} +\providecommand{\matrixwallabsolute}[1]{\textcolor{ReportBlue}{\texttt{W }}#1} +\providecommand{\matrixruntimepair}[2]{\shortstack[l]{#1\\#2}} +\providecommand{\matrixruntimetriple}[3]{\shortstack[l]{#1\\#2\matrixpunct{ | }#3}} +\providecommand{\matrixbelow}[1]{\textcolor{ReportMuted}{\texttt{<#1}}} +\providecommand{\matrixnaratio}[1]{\matrixpunct{(}\matrixna{#1}\matrixpunct{)}} +\providecommand{\matrixstatus}[2]{\textcolor{#1}{\textsc{#2}}} +\providecommand{\matrixncabsolute}[1]{#1} +\providecommand{\matrixsummaryratio}[2]{\textcolor{#1}{\texttt{x#2}}} +\providecommand{\matrixsummaryratiohighlight}[2]{\begingroup\setlength{\fboxsep}{0.9pt}\setlength{\fboxrule}{0.35pt}\fbox{\textcolor{#1}{{\usefont{T1}{lmtt}{b}{n}x#2}}}\endgroup} +\providecommand{\matrixsummarystats}[5]{\begingroup\matrixsummaryfont\begin{tabular}[t]{@{}l@{\hspace{0.014in}\matrixpunct{|}\hspace{0.014in}}l@{\hspace{0.014in}\matrixpunct{|}\hspace{0.014in}}l@{\hspace{0.014in}\matrixpunct{|}\hspace{0.014in}}l@{\hspace{0.014in}\matrixpunct{|}\hspace{0.014in}}l@{}}\makebox[3.6em][l]{#1}&\makebox[4.6em][l]{#2}&\makebox[3.6em][l]{#3}&\makebox[3.6em][l]{#4}&\makebox[4.2em][l]{#5}\end{tabular}\endgroup} +\providecommand{\matrixsummaryworkloads}[2]{\begin{tabular}[t]{@{}l@{}}#1\\[-0.10em]#2\end{tabular}} +\providecommand{\matrixpair}[2]{\begin{tabular}[t]{@{}r@{\hspace{0.025in}\matrixpunct{|}\hspace{0.025in}}r@{}}#1\end{tabular}} +\providecommand{\bestmoderatio}[2]{\textcolor{#1}{\texttt{x#2}}} +\providecommand{\bestmodecompactratio}[3]{\matrixpunct{(}\textcolor{ReportMuted}{\texttt{[x#1]}}\hspace{0.04in}\textcolor{#2}{\texttt{x#3}}\matrixpunct{)}} +\providecommand{\bestmodewallratio}[2]{\matrixpunct{(}\textcolor{#1}{\texttt{x#2}}\matrixpunct{)}} +\providecommand{\bestmodecompactprefix}[1]{\matrixpunct{(}\textcolor{ReportMuted}{\texttt{[x#1]}}\hspace{0.04in}} +\providecommand{\bestmodeopenprefix}{\matrixpunct{(}\hspace{0.04in}} +\providecommand{\bestmodeprimaryratio}[2]{\textcolor{#1}{\texttt{x#2}}\matrixpunct{)}} +\providecommand{\bestmodeabsoluteprefix}[1]{#1\hspace{0.04in}} +\providecommand{\bestmodecompactabsolute}[2]{\textcolor{ReportMuted}{\texttt{[#1]}}\hspace{0.04in}\textcolor{black}{\texttt{#2}}} +\providecommand{\bestmodecompactabsoluteprefix}[1]{\textcolor{ReportMuted}{\texttt{[#1]}}\hspace{0.04in}} +\providecommand{\bestmodeabsoluteprimary}[1]{\textcolor{black}{\texttt{#1}}} +\providecommand{\bestmodeunverifiedclockprefix}[2]{\textcolor{ReportMuted}{\texttt{[}#1\texttt{]}}\hspace{0.04in}#2\hspace{0.04in}} +\clearpage +\subsection{UFO-SM recurrence versus AmpliCol NLC} +\noindent\begin{minipage}{\linewidth} +\subsubsection*{UFO-SM recurrence versus AmpliCol NLC (block 1 of 2)} +\begingroup +\centering +\footnotesize +\setlength{\tabcolsep}{2.1pt} +\renewcommand{\arraystretch}{1.04} +\matrixfitwidth{% +\begin{tabular}{@{}rL{1.45in}l>{\matrixentryfont}l>{\matrixentryfont}r@{}>{\matrixentryfont}l>{\matrixentryfont}l>{\matrixentryfont}r@{}>{\matrixentryfont}l>{\matrixentryfont}l>{\matrixentryfont}r@{}>{\matrixentryfont}l@{}} +\toprule +\textbf{ID} & \textbf{base process} & \textbf{metric} & \multicolumn{3}{c}{\textbf{n=1}} & \multicolumn{3}{c}{\textbf{n=2}} & \multicolumn{3}{c}{\textbf{n=3}} \\ +\specialrule{0.85pt}{0pt}{0pt} +\rowcolor{refblue} +\texttt{1} & $d\bar d\to Z+(n-1)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{2} & $u\bar d\to W^++(n-1)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{3} & $d\bar d\to e^+e^-+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{4} & $u\bar d\to e^+\nu_e+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{5} & $d\bar d\to ZZ+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{6} & $gg\to t\bar t+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{7} & $d\bar d\to t\bar t+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{8} & $gg\to gg+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{9} & $d\bar d\to ZZZ+(n-3)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{10} & $d\bar d\to e^+e^-ZH+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{11} & $d\bar d\to t\bar t ZH+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{12} & $d\bar d\to e^+e^-e^+e^-+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{13} & $d\bar d\to u\bar u\,s\bar s+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{14} & $d\bar d\to u\bar u\,s\bar s\,c\bar c+(n-6)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{15} & $d\bar d\to u\bar u\,u\bar u+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\specialrule{1.05pt}{0.22em}{0.18em} +\multicolumn{3}{@{}l}{} & \multicolumn{3}{c}{\cellcolor{refblue}\textbf{n=1}} & \multicolumn{3}{c}{\textbf{n=2}} & \multicolumn{3}{c}{\cellcolor{refblue}\textbf{n=3}} \\ +\multicolumn{3}{@{}l}{\textbf{summary: generation}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} & \multicolumn{3}{c}{\matrixna{ReportMuted}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} \\[0.08em] +\multicolumn{3}{@{}l}{\textbf{summary: wall}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} & \multicolumn{3}{c}{\matrixna{ReportMuted}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} \\ +\bottomrule +\end{tabular} +} +\endgroup +\ReportTableNote{{\scriptsize Baseline: original AmpliCol; candidate: recurrence JIT O2. Each cell shows the baseline generation and wall time, followed by candidate/baseline generation and wall-time ratios. Runtime comparisons use ([xS]xW). The muted supplementary xS first compares compatible exposed execution attributions; otherwise it compares authenticated evaluator totals. Only when original AmpliCol has no evaluator-total clock may a candidate evaluator total use the legacy direct execution clock (or its wall clock when direct execution is absent) as denominator. An evaluator total is never divided by a recurrence core. This evaluator-total fallback is diagnostic only; the execution/execution ratio remains the compatible supplementary attribution. The coloured xW is always the candidate/baseline wall multiplier and remains the primary figure of merit. When no compatible supplementary clock exists, only (xW) is shown. No clock is fabricated from another. Every displayed number uses exactly three significant digits. Original AmpliCol is static N/A beyond three open quark lines; unavailable, terminal, or unlinked baselines keep their status while successful candidate timings remain absolute and leave summaries. Summary rows contain multipliers only in min, max, median, average, and weighted-average order; the weighted average is the ratio of timing sums, is the framed bold entry, and runtime statistics use wall time only. Not applicable marks a multiplicity for which the process family does not exist. Not run marks an otherwise defined process that was intentionally excluded from the declared measurement scope. Fixed-engine tables intentionally omit mode letters because the selected engine is named in the table heading.}} +\end{minipage} +\clearpage +\noindent\begin{minipage}{\linewidth} +\subsubsection*{UFO-SM recurrence versus AmpliCol NLC (block 2 of 2)} +\begingroup +\centering +\footnotesize +\setlength{\tabcolsep}{2.1pt} +\renewcommand{\arraystretch}{1.04} +\matrixfitwidth{% +\begin{tabular}{@{}rL{1.45in}l>{\matrixentryfont}l>{\matrixentryfont}r@{}>{\matrixentryfont}l>{\matrixentryfont}l>{\matrixentryfont}r@{}>{\matrixentryfont}l>{\matrixentryfont}l>{\matrixentryfont}r@{}>{\matrixentryfont}l@{}} +\toprule +\textbf{ID} & \textbf{base process} & \textbf{metric} & \multicolumn{3}{c}{\textbf{n=4}} & \multicolumn{3}{c}{\textbf{n=5}} & \multicolumn{3}{c}{\textbf{n=6}} \\ +\specialrule{0.85pt}{0pt}{0pt} +\rowcolor{refblue} +\texttt{1} & $d\bar d\to Z+(n-1)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{2} & $u\bar d\to W^++(n-1)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{3} & $d\bar d\to e^+e^-+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{4} & $u\bar d\to e^+\nu_e+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{5} & $d\bar d\to ZZ+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{6} & $gg\to t\bar t+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{7} & $d\bar d\to t\bar t+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{8} & $gg\to gg+(n-2)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{9} & $d\bar d\to ZZZ+(n-3)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{10} & $d\bar d\to e^+e^-ZH+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{11} & $d\bar d\to t\bar t ZH+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\texttt{12} & $d\bar d\to e^+e^-e^+e^-+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{\matrixstatus{ReportMuted}{not run}} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \multicolumn{3}{c}{} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{13} & $d\bar d\to u\bar u\,s\bar s+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\texttt{14} & $d\bar d\to u\bar u\,s\bar s\,c\bar c+(n-6)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \multicolumn{3}{c}{\matrixnotapplicable{ReportMuted}} & \matrixstaticna{ReportMuted} & & \matrixstatus{ReportMuted}{N/A} \\ + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \multicolumn{3}{c}{} & \multicolumn{3}{c}{} & \matrixstaticna{ReportMuted} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\rowcolor{refblue} +\texttt{15} & $d\bar d\to u\bar u\,u\bar u+(n-4)g$ & \textcolor{ReportMuted}{\scriptsize gen. [s]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} + & & \textcolor{ReportMuted}{\scriptsize run [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & & \matrixstatus{ReportMuted}{N/A} \\ +\addlinespace[0.05em] +\specialrule{1.05pt}{0.22em}{0.18em} +\multicolumn{3}{@{}l}{} & \multicolumn{3}{c}{\cellcolor{refblue}\textbf{n=4}} & \multicolumn{3}{c}{\textbf{n=5}} & \multicolumn{3}{c}{\cellcolor{refblue}\textbf{n=6}} \\ +\multicolumn{3}{@{}l}{\textbf{summary: generation}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} & \multicolumn{3}{c}{\matrixna{ReportMuted}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} \\[0.08em] +\multicolumn{3}{@{}l}{\textbf{summary: wall}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} & \multicolumn{3}{c}{\matrixna{ReportMuted}} & \multicolumn{3}{c}{\cellcolor{refblue}\matrixna{ReportMuted}} \\ +\bottomrule +\end{tabular} +} +\endgroup +\ReportTableNote{{\scriptsize Baseline: original AmpliCol; candidate: recurrence JIT O2. Each cell shows the baseline generation and wall time, followed by candidate/baseline generation and wall-time ratios. Runtime comparisons use ([xS]xW). The muted supplementary xS first compares compatible exposed execution attributions; otherwise it compares authenticated evaluator totals. Only when original AmpliCol has no evaluator-total clock may a candidate evaluator total use the legacy direct execution clock (or its wall clock when direct execution is absent) as denominator. An evaluator total is never divided by a recurrence core. This evaluator-total fallback is diagnostic only; the execution/execution ratio remains the compatible supplementary attribution. The coloured xW is always the candidate/baseline wall multiplier and remains the primary figure of merit. When no compatible supplementary clock exists, only (xW) is shown. No clock is fabricated from another. Every displayed number uses exactly three significant digits. Original AmpliCol is static N/A beyond three open quark lines; unavailable, terminal, or unlinked baselines keep their status while successful candidate timings remain absolute and leave summaries. Summary rows contain multipliers only in min, max, median, average, and weighted-average order; the weighted average is the ratio of timing sums, is the framed bold entry, and runtime statistics use wall time only. Not applicable marks a multiplicity for which the process family does not exist. Not run marks an otherwise defined process that was intentionally excluded from the declared measurement scope. Fixed-engine tables intentionally omit mode letters because the selected engine is named in the table heading.}} +\end{minipage} diff --git a/pyamplicol/src/pyamplicol/_profiling_campaign/result_scalar_contact_table.tex b/pyamplicol/src/pyamplicol/_profiling_campaign/result_scalar_contact_table.tex new file mode 100644 index 0000000000..f50d770db7 --- /dev/null +++ b/pyamplicol/src/pyamplicol/_profiling_campaign/result_scalar_contact_table.tex @@ -0,0 +1,51 @@ +% SPDX-License-Identifier: 0BSD +% Generated by tools/performance_report/render.py; do not edit. +\providecommand{\matrixentryfont}{\fontsize{6.8pt}{7.8pt}\selectfont} +\providecommand{\matrixentryfontlc}{\fontsize{6.5pt}{7.5pt}\selectfont} +\providecommand{\matrixsummaryfont}{\fontsize{6.2pt}{7.4pt}\selectfont} +\providecommand{\matrixfitwidth}[1]{\begingroup\sbox0{#1}\ifdim\wd0>\linewidth\resizebox{\linewidth}{!}{\usebox0}\else\makebox[\linewidth][c]{\usebox0}\fi\endgroup} +\providecommand{\matrixpunct}[1]{\textcolor{black}{\texttt{#1}}} +\providecommand{\matrixratio}[2]{\matrixpunct{(}\textcolor{#1}{\texttt{x#2}}\matrixpunct{)}} +\providecommand{\matrixna}[1]{\textcolor{#1}{\texttt{N/A}}} +\providecommand{\matrixnotapplicable}[1]{\textcolor{#1}{\textsc{not applicable}}} +\providecommand{\matrixnotexposed}[1]{\textcolor{#1}{\textsc{not exposed}}} +\providecommand{\matrixstaticna}[1]{\textcolor{#1}{\textsc{static N/A}}} +\providecommand{\matrixtotalevaluator}[1]{\textcolor{ReportBlue}{\texttt{T }}#1} +\providecommand{\matrixrecurrencecore}[1]{\textcolor{ReportOrange}{\texttt{C }}#1} +\providecommand{\matrixzrecurrenceclocks}[2]{\shortstack[r]{#1\\#2}} +\providecommand{\matrixwallclock}[2]{\textcolor{#1}{\texttt{W #2}}} +\providecommand{\matrixwallabsolute}[1]{\textcolor{ReportBlue}{\texttt{W }}#1} +\providecommand{\matrixruntimepair}[2]{\shortstack[l]{#1\\#2}} +\providecommand{\matrixruntimetriple}[3]{\shortstack[l]{#1\\#2\matrixpunct{ | }#3}} +\providecommand{\matrixbelow}[1]{\textcolor{ReportMuted}{\texttt{<#1}}} +\providecommand{\matrixnaratio}[1]{\matrixpunct{(}\matrixna{#1}\matrixpunct{)}} +\providecommand{\matrixstatus}[2]{\textcolor{#1}{\textsc{#2}}} +\providecommand{\matrixncabsolute}[1]{#1} +\providecommand{\matrixsummaryratio}[2]{\textcolor{#1}{\texttt{x#2}}} +\providecommand{\matrixsummaryratiohighlight}[2]{\begingroup\setlength{\fboxsep}{0.9pt}\setlength{\fboxrule}{0.35pt}\fbox{\textcolor{#1}{{\usefont{T1}{lmtt}{b}{n}x#2}}}\endgroup} +\providecommand{\matrixsummarystats}[5]{\begingroup\matrixsummaryfont\begin{tabular}[t]{@{}l@{\hspace{0.014in}\matrixpunct{|}\hspace{0.014in}}l@{\hspace{0.014in}\matrixpunct{|}\hspace{0.014in}}l@{\hspace{0.014in}\matrixpunct{|}\hspace{0.014in}}l@{\hspace{0.014in}\matrixpunct{|}\hspace{0.014in}}l@{}}\makebox[3.6em][l]{#1}&\makebox[4.6em][l]{#2}&\makebox[3.6em][l]{#3}&\makebox[3.6em][l]{#4}&\makebox[4.2em][l]{#5}\end{tabular}\endgroup} +\providecommand{\matrixsummaryworkloads}[2]{\begin{tabular}[t]{@{}l@{}}#1\\[-0.10em]#2\end{tabular}} +\subsection{Scalar-contact ladder} +\begingroup +\scriptsize +\setlength{\tabcolsep}{2.2pt} +\renewcommand{\arraystretch}{1.10} +\begin{center} +\begin{tabular}{@{}L{1.00in}L{0.72in}L{0.72in}L{0.72in}L{0.72in}L{0.72in}L{0.72in}L{0.72in}@{}} +\toprule +\multicolumn{8}{c}{\texttt{scalar\_0 scalar\_0 > n*scalar\_0}} \\ +\textbf{metric} & \textbf{\texttt{n=2}} & \textbf{\texttt{n=3}} & \textbf{\texttt{n=4}} & \textbf{\texttt{n=5}} & \textbf{\texttt{n=6}} & \textbf{\texttt{n=7}} & \textbf{\texttt{n=8}} \\ +\midrule +\rowcolor{refblue} +generation [s] & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +wall [\(\mu\mathrm{s}/\mathrm{pt}\)] & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} +{\fontsize{6.4pt}{7.4pt}\selectfont\mbox{evaluator total [\(\mu\mathrm{s}/\mathrm{pt}\)]}} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +matrix element & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{ReportOrange!8} +relative diff. vs hp & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +\bottomrule +\end{tabular} +\end{center} +\endgroup +\ReportTableNote{Wall time is the common runtime observable. Evaluator total is the independently measured accumulated warmed evaluator clock from its dedicated authenticated record; it is never copied from or derived from wall time. Unverified diagnostics retain absolute generation, wall, and evaluator clocks with a yellow marker; they are not successes. \textsc{not exposed} denotes a successful wall measurement, and older entries without dedicated evaluator-total evidence remain valid; it does not denote a missing result.} diff --git a/pyamplicol/src/pyamplicol/_profiling_campaign/result_scalar_gravity_table.tex b/pyamplicol/src/pyamplicol/_profiling_campaign/result_scalar_gravity_table.tex new file mode 100644 index 0000000000..a47239c5af --- /dev/null +++ b/pyamplicol/src/pyamplicol/_profiling_campaign/result_scalar_gravity_table.tex @@ -0,0 +1,51 @@ +% SPDX-License-Identifier: 0BSD +% Generated by tools/performance_report/render.py; do not edit. +\providecommand{\matrixentryfont}{\fontsize{6.8pt}{7.8pt}\selectfont} +\providecommand{\matrixentryfontlc}{\fontsize{6.5pt}{7.5pt}\selectfont} +\providecommand{\matrixsummaryfont}{\fontsize{6.2pt}{7.4pt}\selectfont} +\providecommand{\matrixfitwidth}[1]{\begingroup\sbox0{#1}\ifdim\wd0>\linewidth\resizebox{\linewidth}{!}{\usebox0}\else\makebox[\linewidth][c]{\usebox0}\fi\endgroup} +\providecommand{\matrixpunct}[1]{\textcolor{black}{\texttt{#1}}} +\providecommand{\matrixratio}[2]{\matrixpunct{(}\textcolor{#1}{\texttt{x#2}}\matrixpunct{)}} +\providecommand{\matrixna}[1]{\textcolor{#1}{\texttt{N/A}}} +\providecommand{\matrixnotapplicable}[1]{\textcolor{#1}{\textsc{not applicable}}} +\providecommand{\matrixnotexposed}[1]{\textcolor{#1}{\textsc{not exposed}}} +\providecommand{\matrixstaticna}[1]{\textcolor{#1}{\textsc{static N/A}}} +\providecommand{\matrixtotalevaluator}[1]{\textcolor{ReportBlue}{\texttt{T }}#1} +\providecommand{\matrixrecurrencecore}[1]{\textcolor{ReportOrange}{\texttt{C }}#1} +\providecommand{\matrixzrecurrenceclocks}[2]{\shortstack[r]{#1\\#2}} +\providecommand{\matrixwallclock}[2]{\textcolor{#1}{\texttt{W #2}}} +\providecommand{\matrixwallabsolute}[1]{\textcolor{ReportBlue}{\texttt{W }}#1} +\providecommand{\matrixruntimepair}[2]{\shortstack[l]{#1\\#2}} +\providecommand{\matrixruntimetriple}[3]{\shortstack[l]{#1\\#2\matrixpunct{ | }#3}} +\providecommand{\matrixbelow}[1]{\textcolor{ReportMuted}{\texttt{<#1}}} +\providecommand{\matrixnaratio}[1]{\matrixpunct{(}\matrixna{#1}\matrixpunct{)}} +\providecommand{\matrixstatus}[2]{\textcolor{#1}{\textsc{#2}}} +\providecommand{\matrixncabsolute}[1]{#1} +\providecommand{\matrixsummaryratio}[2]{\textcolor{#1}{\texttt{x#2}}} +\providecommand{\matrixsummaryratiohighlight}[2]{\begingroup\setlength{\fboxsep}{0.9pt}\setlength{\fboxrule}{0.35pt}\fbox{\textcolor{#1}{{\usefont{T1}{lmtt}{b}{n}x#2}}}\endgroup} +\providecommand{\matrixsummarystats}[5]{\begingroup\matrixsummaryfont\begin{tabular}[t]{@{}l@{\hspace{0.014in}\matrixpunct{|}\hspace{0.014in}}l@{\hspace{0.014in}\matrixpunct{|}\hspace{0.014in}}l@{\hspace{0.014in}\matrixpunct{|}\hspace{0.014in}}l@{\hspace{0.014in}\matrixpunct{|}\hspace{0.014in}}l@{}}\makebox[3.6em][l]{#1}&\makebox[4.6em][l]{#2}&\makebox[3.6em][l]{#3}&\makebox[3.6em][l]{#4}&\makebox[4.2em][l]{#5}\end{tabular}\endgroup} +\providecommand{\matrixsummaryworkloads}[2]{\begin{tabular}[t]{@{}l@{}}#1\\[-0.10em]#2\end{tabular}} +\subsection{Scalar-gravity ladder} +\begingroup +\scriptsize +\setlength{\tabcolsep}{3.2pt} +\renewcommand{\arraystretch}{1.10} +\begin{center} +\begin{tabular}{@{}L{1.08in}L{0.82in}L{0.82in}L{0.82in}@{}} +\toprule +\multicolumn{4}{c}{\texttt{scalar\_0 scalar\_0 > n*graviton}} \\ +\textbf{metric} & \textbf{\texttt{n=2}} & \textbf{\texttt{n=3}} & \textbf{\texttt{n=4}} \\ +\midrule +\rowcolor{refblue} +generation [s] & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +wall [\(\mu\mathrm{s}/\mathrm{pt}\)] & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{refblue} +{\fontsize{6.4pt}{7.4pt}\selectfont\mbox{evaluator total [\(\mu\mathrm{s}/\mathrm{pt}\)]}} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +matrix element & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{ReportOrange!8} +relative diff. vs hp & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +\bottomrule +\end{tabular} +\end{center} +\endgroup +\ReportTableNote{Wall time is the common runtime observable. Evaluator total is the independently measured accumulated warmed evaluator clock from its dedicated authenticated record; it is never copied from or derived from wall time. Unverified diagnostics retain absolute generation, wall, and evaluator clocks with a yellow marker; they are not successes. \textsc{not exposed} denotes a successful wall measurement, and older entries without dedicated evaluator-total evidence remain valid; it does not denote a missing result.} diff --git a/pyamplicol/src/pyamplicol/_profiling_campaign/result_tables.py b/pyamplicol/src/pyamplicol/_profiling_campaign/result_tables.py new file mode 100644 index 0000000000..b8e6c811a0 --- /dev/null +++ b/pyamplicol/src/pyamplicol/_profiling_campaign/result_tables.py @@ -0,0 +1,711 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: 0BSD +"""Compatibility entry point for the performance-report service.""" + +from __future__ import annotations + +import importlib.machinery +import json +import os +import subprocess +import sys +import sysconfig +import tempfile +import uuid +from pathlib import Path + +_EXACT_REEXEC_MARKER = "PYAMPLICOL_EXACT_PYTHON_REEXEC" +_EXACT_IMPORT_PATHS = "PYAMPLICOL_EXACT_IMPORT_PATHS" +_PUBLICATION_ONLY_COMMANDS = { + "init-profile", + "export-profile", + "validate", + "audit", + "audit-source-bridge", + "reset", + "render", + "recover", + "publish-snapshot", + "validate-snapshot", + "snapshot-cell-boundary", + "accept-cell-boundary", +} +_GLOBAL_OPTIONS_WITH_VALUE = { + "--repo-root", + "--report-profile", + "--docs-dir", + "--artifact-root", + "--coordination-root", + "--class-c-ancestor-runtime-root", + "--measurement-source-root", + "--expected-measurement-source-revision", + "--expected-measurement-source-tree", + "--expected-policy-wrapper-revision", + "--expected-policy-wrapper-tree", + "--expected-policy-entrypoint-sha256", + "--expected-legacy-adapter-sha256", + "--study-contract-sha256", +} + + +def _repository_root(entrypoint: Path) -> Path: + for candidate in entrypoint.parents: + if ( + (candidate / "tools/performance_report").is_dir() + and (candidate / "src/pyamplicol").is_dir() + ): + return candidate + raise RuntimeError( + "result_tables.py must run from a pyAmpliCol source checkout" + ) + + +def _embedded_profile(entrypoint: Path, repo_root: Path) -> str | None: + profile_parent = repo_root / "docs/performance_reports" + try: + relative = entrypoint.parent.relative_to(profile_parent) + except ValueError: + return None + return relative.parts[0] if len(relative.parts) == 1 else None + + +def _has_option(arguments: list[str], option: str) -> bool: + return any( + argument == option or argument.startswith(f"{option}=") + for argument in arguments + ) + + +def _subcommand(arguments: list[str]) -> str | None: + offset = 0 + while offset < len(arguments): + argument = arguments[offset] + if argument in _GLOBAL_OPTIONS_WITH_VALUE: + offset += 2 + continue + if any( + argument.startswith(f"{option}=") + for option in _GLOBAL_OPTIONS_WITH_VALUE + ): + offset += 1 + continue + if argument.startswith("-"): + offset += 1 + continue + return argument + return None + + +def _option_path(arguments: list[str], option: str) -> Path | None: + """Read one early bootstrap path option without importing the CLI.""" + + values: list[str] = [] + offset = 0 + while offset < len(arguments): + argument = arguments[offset] + if argument == option: + if offset + 1 >= len(arguments): + raise RuntimeError(f"{option} requires one absolute path") + values.append(arguments[offset + 1]) + offset += 2 + continue + prefix = f"{option}=" + if argument.startswith(prefix): + values.append(argument[len(prefix) :]) + offset += 1 + if not values: + return None + if len(values) != 1: + raise RuntimeError(f"{option} must be specified exactly once") + path = Path(values[0]) + if not path.is_absolute(): + raise RuntimeError(f"{option} must be an absolute path") + try: + root = path.resolve(strict=True) + except OSError as error: + raise RuntimeError(f"{option} is unavailable") from error + if not (root / "src/pyamplicol").is_dir(): + raise RuntimeError(f"{option} has no pyamplicol source package") + return root + + +def _option_value(arguments: list[str], option: str) -> str | None: + """Read one exact early-bootstrap value without importing the CLI.""" + + values: list[str] = [] + offset = 0 + while offset < len(arguments): + argument = arguments[offset] + if argument == option: + if offset + 1 >= len(arguments): + raise RuntimeError(f"{option} requires one value") + values.append(arguments[offset + 1]) + offset += 2 + continue + prefix = f"{option}=" + if argument.startswith(prefix): + values.append(argument[len(prefix) :]) + offset += 1 + if not values: + return None + if len(values) != 1 or not values[0]: + raise RuntimeError(f"{option} must be specified exactly once") + return values[0] + + +def _git_value(root: Path, expression: str) -> str: + completed = subprocess.run( + ("git", "-C", str(root), "rev-parse", expression), + check=False, + capture_output=True, + text=True, + ) + value = completed.stdout.strip() + if ( + completed.returncode != 0 + or len(value) != 40 + or any(character not in "0123456789abcdef" for character in value) + ): + raise RuntimeError( + f"cannot authenticate worker checkout identity {expression!r}" + ) + return value + + +def _require_git_identity( + root: Path, + *, + revision: str, + tree: str, + require_clean: bool, + require_tracked_clean: bool = False, + untracked_source_paths: tuple[str, ...] = (), +) -> None: + if ( + _git_value(root, "HEAD^{commit}") != revision + or _git_value(root, "HEAD^{tree}") != tree + ): + raise RuntimeError("worker checkout Git identity differs") + if require_clean: + completed = subprocess.run( + ( + "git", + "-C", + str(root), + "status", + "--porcelain=v1", + "--untracked-files=all", + ), + check=False, + capture_output=True, + text=True, + ) + if completed.returncode != 0 or completed.stdout: + raise RuntimeError("policy-wrapper checkout is not clean") + if require_tracked_clean: + completed = subprocess.run( + ( + "git", + "-C", + str(root), + "diff", + "--quiet", + "--no-ext-diff", + "HEAD", + "--", + ), + check=False, + capture_output=True, + text=True, + ) + if completed.returncode == 1: + raise RuntimeError( + "measured-source checkout has tracked changes" + ) + if completed.returncode != 0: + raise RuntimeError( + "cannot inspect measured-source tracked cleanliness" + ) + if untracked_source_paths: + completed = subprocess.run( + ( + "git", + "-C", + str(root), + "ls-files", + "--others", + "--exclude-standard", + "-z", + "--", + *untracked_source_paths, + ), + check=False, + capture_output=True, + ) + if completed.returncode != 0: + raise RuntimeError( + "cannot inspect measured-source untracked files" + ) + if completed.stdout: + raise RuntimeError( + "measured-source checkout has untracked files in " + "imported source roots" + ) + + +def _sha256_file(path: Path) -> str: + import hashlib + + digest = hashlib.sha256() + with path.open("rb") as stream: + for block in iter(lambda: stream.read(1024 * 1024), b""): + digest.update(block) + return digest.hexdigest() + + +def _measured_venv_site_paths(measured_root: Path) -> tuple[Path, ...]: + """Recover the measured checkout's venv even under ``-I -S``.""" + + expected_venv = measured_root / ".venv" + if expected_venv.is_symlink() or not expected_venv.is_dir(): + raise RuntimeError( + "split worker requires a regular measured .venv directory" + ) + venv_root = expected_venv.resolve(strict=True) + executable = Path(os.path.abspath(sys.executable)) + if executable.parent.parent.resolve(strict=True) != venv_root: + raise RuntimeError( + "split worker interpreter is not the measured .venv interpreter" + ) + configuration = expected_venv / "pyvenv.cfg" + if configuration.is_symlink() or not configuration.is_file(): + raise RuntimeError("measured .venv has no regular pyvenv.cfg") + fields: dict[str, str] = {} + for line in configuration.read_text( + encoding="utf-8", + errors="strict", + ).splitlines(): + key, separator, value = line.partition("=") + if separator: + fields[key.strip().lower()] = value.strip() + if fields.get("include-system-site-packages", "").lower() != "false": + raise RuntimeError( + "measured .venv must exclude system site-packages" + ) + + variables = dict(sysconfig.get_config_vars()) + variables.update( + { + "base": str(venv_root), + "platbase": str(venv_root), + "installed_base": str(venv_root), + "installed_platbase": str(venv_root), + } + ) + paths: list[Path] = [] + for name in ("purelib", "platlib"): + value = sysconfig.get_path(name, vars=variables) + if not isinstance(value, str): + raise RuntimeError(f"measured .venv has no {name} path") + candidate = Path(value) + try: + resolved = candidate.resolve(strict=True) + resolved.relative_to(venv_root) + except (OSError, ValueError) as error: + raise RuntimeError( + f"measured .venv {name} path is unavailable" + ) from error + if resolved not in paths: + paths.append(resolved) + if not paths: + raise RuntimeError("measured .venv has no site-packages path") + return tuple(paths) + + +def _native_extensions(package_dir: Path) -> tuple[Path, ...]: + try: + candidates = tuple( + sorted( + ( + path + for path in package_dir.iterdir() + if path.name.startswith("_rusticol") + and path.name.endswith( + tuple(importlib.machinery.EXTENSION_SUFFIXES) + ) + ), + key=lambda path: path.name, + ) + ) + except OSError: + return () + if any(path.is_symlink() or not path.is_file() for path in candidates): + raise RuntimeError( + "measured native extension must be a regular file" + ) + return candidates + + +def _measured_split_runtime( + measured_root: Path, +) -> tuple[tuple[Path, ...], Path]: + """Return exact venv import roots and one canonical measured native root.""" + + site_paths = _measured_venv_site_paths(measured_root) + source_package = (measured_root / "src/pyamplicol").resolve(strict=True) + package_dirs = ( + source_package, + *( + site_path / "pyamplicol" + for site_path in site_paths + if (site_path / "pyamplicol").is_dir() + ), + ) + candidates: list[tuple[Path, Path, str]] = [] + for package_dir in package_dirs: + extensions = _native_extensions(package_dir) + if len(extensions) > 1: + raise RuntimeError( + "measured package has multiple native extension candidates" + ) + if extensions: + extension = extensions[0] + candidates.append( + (package_dir, extension, _sha256_file(extension)) + ) + if not candidates: + raise RuntimeError( + "split worker found no native extension in measured source/venv" + ) + if len({digest for _package, _path, digest in candidates}) != 1: + raise RuntimeError( + "measured source and venv native extensions differ" + ) + canonical_package = next( + ( + package + for package, _path, _digest in candidates + if package == source_package + ), + candidates[0][0], + ) + return site_paths, canonical_package + + +ENTRYPOINT = Path(__file__).resolve() +REPOSITORY_ROOT = _repository_root(ENTRYPOINT) +EMBEDDED_PROFILE = _embedded_profile(ENTRYPOINT, REPOSITORY_ROOT) +ARGUMENTS = list(sys.argv[1:]) +if ( + EMBEDDED_PROFILE is not None + and not _has_option(ARGUMENTS, "--report-profile") + and not _has_option(ARGUMENTS, "--docs-dir") +): + ARGUMENTS[:0] = ("--report-profile", EMBEDDED_PROFILE) +COMMAND = _subcommand(ARGUMENTS) +CLASS_C_ANCESTOR_RUNTIME_ROOT = _option_path( + ARGUMENTS, + "--class-c-ancestor-runtime-root", +) +MEASUREMENT_SOURCE_ROOT = _option_path( + ARGUMENTS, + "--measurement-source-root", +) +_SPLIT_WORKER_OPTIONS = { + "measured_revision": _option_value( + ARGUMENTS, + "--expected-measurement-source-revision", + ), + "measured_tree": _option_value( + ARGUMENTS, + "--expected-measurement-source-tree", + ), + "wrapper_revision": _option_value( + ARGUMENTS, + "--expected-policy-wrapper-revision", + ), + "wrapper_tree": _option_value( + ARGUMENTS, + "--expected-policy-wrapper-tree", + ), + "entrypoint_sha256": _option_value( + ARGUMENTS, + "--expected-policy-entrypoint-sha256", + ), + "legacy_adapter_sha256": _option_value( + ARGUMENTS, + "--expected-legacy-adapter-sha256", + ), + "study_contract_sha256": _option_value( + ARGUMENTS, + "--study-contract-sha256", + ), +} +_SPLIT_WORKER_SPECIFIED = ( + MEASUREMENT_SOURCE_ROOT is not None + or any(value is not None for value in _SPLIT_WORKER_OPTIONS.values()) +) +if _SPLIT_WORKER_SPECIFIED: + if MEASUREMENT_SOURCE_ROOT is None or any( + value is None for value in _SPLIT_WORKER_OPTIONS.values() + ): + raise RuntimeError( + "split worker wrapper/source options must be specified together" + ) + if COMMAND not in {"_prepare", "_worker"}: + raise RuntimeError( + "split worker wrapper/source options are restricted to workers" + ) + requested_repo_root = _option_path(ARGUMENTS, "--repo-root") + if requested_repo_root != MEASUREMENT_SOURCE_ROOT: + raise RuntimeError( + "worker --repo-root must equal --measurement-source-root" + ) + measured_revision = str(_SPLIT_WORKER_OPTIONS["measured_revision"]) + measured_tree = str(_SPLIT_WORKER_OPTIONS["measured_tree"]) + wrapper_revision = str(_SPLIT_WORKER_OPTIONS["wrapper_revision"]) + wrapper_tree = str(_SPLIT_WORKER_OPTIONS["wrapper_tree"]) + _require_git_identity( + MEASUREMENT_SOURCE_ROOT, + revision=measured_revision, + tree=measured_tree, + require_clean=False, + require_tracked_clean=True, + # ``src`` is the measured checkout's only Python import root. + untracked_source_paths=("src",), + ) + _require_git_identity( + REPOSITORY_ROOT, + revision=wrapper_revision, + tree=wrapper_tree, + require_clean=True, + ) + if _sha256_file(ENTRYPOINT) != _SPLIT_WORKER_OPTIONS[ + "entrypoint_sha256" + ]: + raise RuntimeError("policy-wrapper entrypoint digest differs") + if _sha256_file( + REPOSITORY_ROOT / "tools/performance_report/legacy.py" + ) != _SPLIT_WORKER_OPTIONS["legacy_adapter_sha256"]: + raise RuntimeError("policy-wrapper legacy adapter digest differs") +if ( + CLASS_C_ANCESTOR_RUNTIME_ROOT is not None + and COMMAND != "prepare-class-c-bridge" +): + raise RuntimeError( + "--class-c-ancestor-runtime-root is restricted to " + "prepare-class-c-bridge" + ) +RUNTIME_REQUIRED = COMMAND not in _PUBLICATION_ONLY_COMMANDS + + +def _native_package_dir(search_path: object = sys.path) -> Path | None: + if not isinstance(search_path, (list, tuple)): + raise RuntimeError("Python import path is invalid") + for entry in search_path: + if not isinstance(entry, str): + continue + package_dir = Path(entry) / "pyamplicol" + try: + native = any( + path.is_file() + and path.name.startswith("_rusticol") + and path.name.endswith(tuple(importlib.machinery.EXTENSION_SUFFIXES)) + for path in package_dir.iterdir() + ) + except OSError: + continue + if native: + return package_dir + return None + + +def _bootstrap_exact_python(arguments: list[str]) -> None: + """Re-exec before importing any repository or candidate module.""" + + if os.environ.get(_EXACT_REEXEC_MARKER) == "1": + if ( + sys.flags.isolated != 1 + or sys.flags.no_site != 1 + or sys.flags.ignore_environment != 1 + or sys.flags.dont_write_bytecode != 1 + or not sys.dont_write_bytecode + ): + raise RuntimeError("exact Python re-exec flags are incomplete") + raw_paths = json.loads(os.environ[_EXACT_IMPORT_PATHS]) + if not isinstance(raw_paths, list) or not all( + isinstance(path, str) and Path(path).is_absolute() for path in raw_paths + ): + raise RuntimeError("exact Python import paths are invalid") + if MEASUREMENT_SOURCE_ROOT is not None: + measured_paths, _native_package = _measured_split_runtime( + MEASUREMENT_SOURCE_ROOT + ) + if raw_paths != [str(path) for path in measured_paths]: + raise RuntimeError( + "exact Python import paths differ from measured .venv" + ) + cache_prefix = os.environ.get("PYTHONPYCACHEPREFIX") + if ( + not isinstance(cache_prefix, str) + or not Path(cache_prefix).is_absolute() + or Path(cache_prefix).exists() + ): + raise RuntimeError("exact Python cache prefix is not absent") + for import_path in reversed(raw_paths): + if import_path not in sys.path: + sys.path.insert(0, import_path) + # ``-I`` ignores PYTHON* variables. Install the authenticated absent + # namespace explicitly before any repository or candidate import. + sys.pycache_prefix = cache_prefix + return + + prefix = Path(tempfile.gettempdir()) / ( + f".pyamplicol-no-bytecode-{os.getpid()}-{uuid.uuid4().hex}" + ) + if prefix.exists(): + raise RuntimeError( + f"isolated Python cache prefix unexpectedly exists: {prefix}" + ) + if MEASUREMENT_SOURCE_ROOT is not None: + measured_paths, _native_package = _measured_split_runtime( + MEASUREMENT_SOURCE_ROOT + ) + import_paths = [str(path) for path in measured_paths] + elif COMMAND in {"_prepare", "_worker"}: + # The scheduler deliberately starts workers with ``-I -S``. On + # Python 3.12, suppressing ``site`` also prevents pyvenv.cfg from + # changing ``sys.prefix``, so generic sysconfig discovery below would + # resolve the base interpreter's site-packages instead of this + # checkout's venv. Workers always use the repository venv; recover + # and authenticate its paths explicitly just as split workers do. + import_paths = [ + str(path) for path in _measured_venv_site_paths(REPOSITORY_ROOT) + ] + else: + import_paths = [] + native_package = _native_package_dir() + if native_package is not None: + import_paths.append( + str(native_package.parent.resolve(strict=True)) + ) + for scheme_name in ("purelib", "platlib"): + scheme_path = sysconfig.get_path(scheme_name) + if not isinstance(scheme_path, str): + continue + try: + resolved = str(Path(scheme_path).resolve(strict=True)) + except OSError: + continue + if resolved not in import_paths: + import_paths.append(resolved) + environment = os.environ.copy() + environment["PYTHONDONTWRITEBYTECODE"] = "1" + environment["PYTHONPYCACHEPREFIX"] = str(prefix) + environment[_EXACT_REEXEC_MARKER] = "1" + environment[_EXACT_IMPORT_PATHS] = json.dumps(import_paths) + os.execve( + sys.executable, + ( + sys.executable, + "-I", + "-S", + "-B", + str(ENTRYPOINT.resolve(strict=True)), + *arguments, + ), + environment, + ) + raise RuntimeError("exact Python re-exec returned unexpectedly") + + +if __name__ == "__main__": + _bootstrap_exact_python(ARGUMENTS) + +_PACKAGE_SOURCE_ROOT = ( + CLASS_C_ANCESTOR_RUNTIME_ROOT + if CLASS_C_ANCESTOR_RUNTIME_ROOT is not None + else ( + REPOSITORY_ROOT + if MEASUREMENT_SOURCE_ROOT is None + else MEASUREMENT_SOURCE_ROOT + ) +) +_IMPORT_ROOTS = ( + REPOSITORY_ROOT, + _PACKAGE_SOURCE_ROOT / "src", +) +for source_root in reversed(_IMPORT_ROOTS): + if str(source_root) not in sys.path: + sys.path.insert(0, str(source_root)) +NATIVE_PACKAGE_DIR = ( + _native_package_dir() + if MEASUREMENT_SOURCE_ROOT is None + else _measured_split_runtime(MEASUREMENT_SOURCE_ROOT)[1] +) + +if __name__ == "__main__": + from tools.performance_report.runtime_evidence import ( + RuntimeEvidenceError, + source_only_bytecode_policy, + ) + + try: + source_only_bytecode_policy() + except RuntimeEvidenceError as error: + raise RuntimeError( + "could not establish source-only Python bytecode isolation" + ) from error + +if __name__ == "__main__" and RUNTIME_REQUIRED: + from tools.performance_report.runtime_evidence import ( + native_extension_in_package, + preimport_python_runtime_identity, + ) + + if NATIVE_PACKAGE_DIR is not None: + _EXACT_SOURCE_PACKAGE = _PACKAGE_SOURCE_ROOT / "src" / "pyamplicol" + _EXACT_PACKAGE_ROOTS = ( + (_EXACT_SOURCE_PACKAGE,) + if NATIVE_PACKAGE_DIR == _EXACT_SOURCE_PACKAGE + else (_EXACT_SOURCE_PACKAGE, NATIVE_PACKAGE_DIR) + ) + _EXACT_NATIVE_EXTENSION = native_extension_in_package(NATIVE_PACKAGE_DIR) + preimport_python_runtime_identity( + _EXACT_PACKAGE_ROOTS, + native_extension=_EXACT_NATIVE_EXTENSION, + ) + + import pyamplicol + + if ( + NATIVE_PACKAGE_DIR is not None + and str(NATIVE_PACKAGE_DIR) not in pyamplicol.__path__ + ): + # Match normal Python import precedence exactly. Additional native-bearing + # installations remain ineligible rather than expanding this namespace. + pyamplicol.__path__.append(str(NATIVE_PACKAGE_DIR)) + if MEASUREMENT_SOURCE_ROOT is not None: + expected_package_roots = { + path.resolve(strict=True) + for path in _EXACT_PACKAGE_ROOTS + } + observed_package_roots = { + Path(path).resolve(strict=True) + for path in pyamplicol.__path__ + } + if ( + observed_package_roots + - expected_package_roots + or _EXACT_SOURCE_PACKAGE.resolve(strict=True) + not in observed_package_roots + ): + raise RuntimeError( + "pyamplicol namespace escaped measured source/venv roots" + ) + +from tools.performance_report.cli import main # noqa: E402, I001 + + +if __name__ == "__main__": + raise SystemExit(main(ARGUMENTS)) diff --git a/pyamplicol/src/pyamplicol/_profiling_campaign/result_validation_summary.tex b/pyamplicol/src/pyamplicol/_profiling_campaign/result_validation_summary.tex new file mode 100644 index 0000000000..445174f8f5 --- /dev/null +++ b/pyamplicol/src/pyamplicol/_profiling_campaign/result_validation_summary.tex @@ -0,0 +1,44 @@ +% SPDX-License-Identifier: 0BSD +% Generated by tools/performance_report/validation_summary.py; do not edit. +\begin{samepage} +\subsection*{Numerical validation summary} +Only measurements whose numerical checks pass are counted below. The scope comprises every applicable report cell across the complete declared multiplicity range. +\begin{center} +\small +\begin{tabular}{@{}l r r r l@{}} +\toprule +\textbf{final-state multiplicity} & \textbf{declared} & \textbf{static N/A} & \textbf{verified} & \textbf{measurable coverage} \\ +\midrule +1 & 84 & 0 & 0 & \textcolor{ReportOrange}{incomplete (0 censored, 84 open)} \\ +2 & 254 & 0 & 0 & \textcolor{ReportOrange}{incomplete (0 censored, 254 open)} \\ +3 & 282 & 0 & 0 & \textcolor{ReportOrange}{incomplete (0 censored, 282 open)} \\ +4 & 422 & 0 & 0 & \textcolor{ReportOrange}{incomplete (0 censored, 422 open)} \\ +5 & 393 & 0 & 0 & \textcolor{ReportOrange}{incomplete (0 censored, 393 open)} \\ +6 & 227 & 4 & 0 & \textcolor{ReportOrange}{incomplete (0 censored, 223 open)} \\ +7 & 179 & 10 & 0 & \textcolor{ReportOrange}{incomplete (0 censored, 169 open)} \\ +8 & 179 & 10 & 0 & \textcolor{ReportOrange}{incomplete (0 censored, 169 open)} \\ +9 & 178 & 10 & 0 & \textcolor{ReportOrange}{incomplete (0 censored, 168 open)} \\ +\midrule +\textbf{total} & 2198 & 34 & 0 & \textcolor{ReportOrange}{incomplete (0 censored, 2164 open)} \\ +\bottomrule +\end{tabular} +\end{center} +\end{samepage} +\ReportTableNote{Full-catalog display accounting through \(n=9\): 2198 declared cells, comprising 2164 measurable cells and 34 catalog-authenticated static N/A rows; 659 matrix process/multiplicity positions marked either \textsc{not applicable} or \textsc{not run}; and 36 reference execution fields marked \textsc{not exposed}. The latter two categories are intentional table structure, not unfilled measurements.} +\begin{center} +\small +\begin{tabular}{@{}l r r@{}} +\toprule +\textbf{validation evidence} & \textbf{records} & \textbf{max. relative difference} \\ +\midrule +MadGraph standalone tree-level reference records & 0 & \textcolor{ReportMuted}{reference} \\ +\PAC{} versus MadGraph binary64 (p200; OTF p16) & 0 & \textcolor{ReportMuted}{---} \\ +original-\AC{} legacy diagnostic records & 0 & \textcolor{ReportMuted}{diagnostic} \\ +\PAC{} versus original-\AC{} legacy diagnostic & 0 pass, 0 mismatch & \textcolor{ReportMuted}{---} \\ +compiled/eager/OTF versus recurrence & 0 & \textcolor{ReportMuted}{---} \\ +optimized result versus resolved sum & 0 & \textcolor{ReportMuted}{---} \\ +binary64 result versus p32 & 0 & \textcolor{ReportMuted}{---} \\ +\bottomrule +\end{tabular} +\end{center} +\ReportTableNote{Profile: \texttt{\ReportProfileName}. Measured source: \textcolor{ReportOrange}{source identity is incomplete or mixed}. The authoritative \PAC{} versus MadGraph binary64 full-colour comparison uses p200 for recurrence, compiled, and eager candidates and the supported p16 lane for OTF; all use a relative tolerance of \(10^{-10}\). Original-\AC{} comparisons are retained non-authoritative legacy diagnostics at \(10^{-8}\); a legacy mismatch does not invalidate a candidate; cross-mode (including OTF/recurrence), resolved-sum, and p32 comparisons use \(10^{-12}\).} diff --git a/pyamplicol/src/pyamplicol/_profiling_campaign/result_z_builtin_sm_table.tex b/pyamplicol/src/pyamplicol/_profiling_campaign/result_z_builtin_sm_table.tex new file mode 100644 index 0000000000..37c64a6a68 --- /dev/null +++ b/pyamplicol/src/pyamplicol/_profiling_campaign/result_z_builtin_sm_table.tex @@ -0,0 +1,205 @@ +% SPDX-License-Identifier: 0BSD +% Generated by tools/performance_report/render.py; do not edit. +\providecommand{\matrixentryfont}{\fontsize{6.8pt}{7.8pt}\selectfont} +\providecommand{\matrixentryfontlc}{\fontsize{6.5pt}{7.5pt}\selectfont} +\providecommand{\matrixsummaryfont}{\fontsize{6.2pt}{7.4pt}\selectfont} +\providecommand{\matrixfitwidth}[1]{\begingroup\sbox0{#1}\ifdim\wd0>\linewidth\resizebox{\linewidth}{!}{\usebox0}\else\makebox[\linewidth][c]{\usebox0}\fi\endgroup} +\providecommand{\matrixpunct}[1]{\textcolor{black}{\texttt{#1}}} +\providecommand{\matrixratio}[2]{\matrixpunct{(}\textcolor{#1}{\texttt{x#2}}\matrixpunct{)}} +\providecommand{\matrixna}[1]{\textcolor{#1}{\texttt{N/A}}} +\providecommand{\matrixnotapplicable}[1]{\textcolor{#1}{\textsc{not applicable}}} +\providecommand{\matrixnotexposed}[1]{\textcolor{#1}{\textsc{not exposed}}} +\providecommand{\matrixstaticna}[1]{\textcolor{#1}{\textsc{static N/A}}} +\providecommand{\matrixtotalevaluator}[1]{\textcolor{ReportBlue}{\texttt{T }}#1} +\providecommand{\matrixrecurrencecore}[1]{\textcolor{ReportOrange}{\texttt{C }}#1} +\providecommand{\matrixzrecurrenceclocks}[2]{\shortstack[r]{#1\\#2}} +\providecommand{\matrixwallclock}[2]{\textcolor{#1}{\texttt{W #2}}} +\providecommand{\matrixwallabsolute}[1]{\textcolor{ReportBlue}{\texttt{W }}#1} +\providecommand{\matrixruntimepair}[2]{\shortstack[l]{#1\\#2}} +\providecommand{\matrixruntimetriple}[3]{\shortstack[l]{#1\\#2\matrixpunct{ | }#3}} +\providecommand{\matrixbelow}[1]{\textcolor{ReportMuted}{\texttt{<#1}}} +\providecommand{\matrixnaratio}[1]{\matrixpunct{(}\matrixna{#1}\matrixpunct{)}} +\providecommand{\matrixstatus}[2]{\textcolor{#1}{\textsc{#2}}} +\providecommand{\matrixncabsolute}[1]{#1} +\providecommand{\matrixsummaryratio}[2]{\textcolor{#1}{\texttt{x#2}}} +\providecommand{\matrixsummaryratiohighlight}[2]{\begingroup\setlength{\fboxsep}{0.9pt}\setlength{\fboxrule}{0.35pt}\fbox{\textcolor{#1}{{\usefont{T1}{lmtt}{b}{n}x#2}}}\endgroup} +\providecommand{\matrixsummarystats}[5]{\begingroup\matrixsummaryfont\begin{tabular}[t]{@{}l@{\hspace{0.014in}\matrixpunct{|}\hspace{0.014in}}l@{\hspace{0.014in}\matrixpunct{|}\hspace{0.014in}}l@{\hspace{0.014in}\matrixpunct{|}\hspace{0.014in}}l@{\hspace{0.014in}\matrixpunct{|}\hspace{0.014in}}l@{}}\makebox[3.6em][l]{#1}&\makebox[4.6em][l]{#2}&\makebox[3.6em][l]{#3}&\makebox[3.6em][l]{#4}&\makebox[4.2em][l]{#5}\end{tabular}\endgroup} +\providecommand{\matrixsummaryworkloads}[2]{\begin{tabular}[t]{@{}l@{}}#1\\[-0.10em]#2\end{tabular}} +\clearpage +\subsection{Built-in SM dedicated \(d\bar d\to Z+\) gluon performance} +\noindent\begin{minipage}{\linewidth} +\subsubsection*{Built-in SM \(d\bar d\to Z+\) gluon ladder (block 1 of 3)} +\begingroup +\centering +\scriptsize +\setlength{\tabcolsep}{1.4pt} +\renewcommand{\arraystretch}{1.05} +\begin{tabular}{@{}r L{1.50in} R{1.26in} R{1.26in} R{1.26in} @{\hspace{0.08in}}R{1.26in} R{1.26in} R{1.26in}@{}} +\toprule +\textbf{n} & \textbf{setup} & \multicolumn{3}{c}{\textbf{selected flow, helicity sum}} & \multicolumn{3}{c}{\textbf{all flows, single helicity}} \\ +& & \textbf{gen [s]} & \textbf{wall [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \textbf{eval [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \textbf{gen [s]} & \textbf{wall [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \textbf{eval [\(\mu\mathrm{s}/\mathrm{pt}\)]} \\ +\midrule +\rowcolor{refblue} +1 & \AC{} reference & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixnotexposed{ReportMuted} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixnotexposed{ReportMuted} \\ +1 & compiled JIT O1 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +1 & compiled ASM O3 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +1 & compiled C++ O3 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{ReportGreen!12} +1 & compiled JIT O3 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{ReportGreen!12} +1 & eager-DAG JIT O2 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{ReportGreen!12} +1 & recurrence JIT O2 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{ReportGreen!12} +1 & on-the-fly JIT O2 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +\midrule[0.45pt] +\rowcolor{refblue} +2 & \AC{} reference & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixnotexposed{ReportMuted} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixnotexposed{ReportMuted} \\ +2 & compiled JIT O1 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +2 & compiled ASM O3 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +2 & compiled C++ O3 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{ReportGreen!12} +2 & compiled JIT O3 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{ReportGreen!12} +2 & eager-DAG JIT O2 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{ReportGreen!12} +2 & recurrence JIT O2 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{ReportGreen!12} +2 & on-the-fly JIT O2 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +\midrule[0.45pt] +\rowcolor{refblue} +3 & \AC{} reference & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixnotexposed{ReportMuted} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixnotexposed{ReportMuted} \\ +3 & compiled JIT O1 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +3 & compiled ASM O3 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +3 & compiled C++ O3 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{ReportGreen!12} +3 & compiled JIT O3 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{ReportGreen!12} +3 & eager-DAG JIT O2 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{ReportGreen!12} +3 & recurrence JIT O2 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{ReportGreen!12} +3 & on-the-fly JIT O2 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +\bottomrule +\end{tabular} +\endgroup +\ReportTableNote{Original AmpliCol is the denominator only when successful and exactly linked by stored same-point evidence, directly or through current recurrence. Otherwise its status remains visible and successful pyAmpliCol values are absolute. Unverified diagnostics retain absolute clocks with a yellow marker, but never enter ratios or summaries. Compiled, eager, and recurrence rows report separate topology-replay and all-flow-union generation and runtime measurements. On-the-fly uses one compact query-local process seed for both physical workloads. Its generation entry is the absolute \texttt{[G] G+W} pair: muted G is process-seed generation and G+W adds the first cold on-the-fly evaluation. Parenthesized values are candidate/reference ratios. All-flow generation shows the absolute pyAmpliCol value without a ratio because its setup boundary differs from the reference. The wall time is the common runtime observable. Not exposed means that a successful wall measurement has no dedicated authenticated evaluator-total timing. Authenticated accumulated warmed evaluator totals appear in the eval column with enough precision to remain distinct from wall time. Its parenthesized multiplier uses the original-AmpliCol wall measurement as denominator because the legacy reference has no separate evaluator-total clock. Recurrence rows retain the independently measured narrower recurrence core in detailed evidence; it is not relabeled as evaluator total. Neither evaluator total nor recurrence core is derived from wall time or from the other. Older entries without authenticated total evidence remain marked not exposed; it is not a missing measurement. user cap: native C++/ASM generation is not attempted above n=6 (native-backend-generation-cap-n6-v1).} +\end{minipage} +\clearpage +\noindent\begin{minipage}{\linewidth} +\subsubsection*{Built-in SM \(d\bar d\to Z+\) gluon ladder (block 2 of 3)} +\begingroup +\centering +\scriptsize +\setlength{\tabcolsep}{1.4pt} +\renewcommand{\arraystretch}{1.05} +\begin{tabular}{@{}r L{1.50in} R{1.26in} R{1.26in} R{1.26in} @{\hspace{0.08in}}R{1.26in} R{1.26in} R{1.26in}@{}} +\toprule +\textbf{n} & \textbf{setup} & \multicolumn{3}{c}{\textbf{selected flow, helicity sum}} & \multicolumn{3}{c}{\textbf{all flows, single helicity}} \\ +& & \textbf{gen [s]} & \textbf{wall [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \textbf{eval [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \textbf{gen [s]} & \textbf{wall [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \textbf{eval [\(\mu\mathrm{s}/\mathrm{pt}\)]} \\ +\midrule +\rowcolor{refblue} +4 & \AC{} reference & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixnotexposed{ReportMuted} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixnotexposed{ReportMuted} \\ +4 & compiled JIT O1 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +4 & compiled ASM O3 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +4 & compiled C++ O3 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{ReportGreen!12} +4 & compiled JIT O3 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{ReportGreen!12} +4 & eager-DAG JIT O2 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{ReportGreen!12} +4 & recurrence JIT O2 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{ReportGreen!12} +4 & on-the-fly JIT O2 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +\midrule[0.45pt] +\rowcolor{refblue} +5 & \AC{} reference & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixnotexposed{ReportMuted} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixnotexposed{ReportMuted} \\ +5 & compiled JIT O1 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +5 & compiled ASM O3 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +5 & compiled C++ O3 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{ReportGreen!12} +5 & compiled JIT O3 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{ReportGreen!12} +5 & eager-DAG JIT O2 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{ReportGreen!12} +5 & recurrence JIT O2 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{ReportGreen!12} +5 & on-the-fly JIT O2 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +\midrule[0.45pt] +\rowcolor{refblue} +6 & \AC{} reference & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixnotexposed{ReportMuted} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixnotexposed{ReportMuted} \\ +6 & compiled JIT O1 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +6 & compiled ASM O3 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +6 & compiled C++ O3 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{ReportGreen!12} +6 & compiled JIT O3 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{ReportGreen!12} +6 & eager-DAG JIT O2 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{ReportGreen!12} +6 & recurrence JIT O2 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{ReportGreen!12} +6 & on-the-fly JIT O2 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +\bottomrule +\end{tabular} +\endgroup +\ReportTableNote{Original AmpliCol is the denominator only when successful and exactly linked by stored same-point evidence, directly or through current recurrence. Otherwise its status remains visible and successful pyAmpliCol values are absolute. Unverified diagnostics retain absolute clocks with a yellow marker, but never enter ratios or summaries. Compiled, eager, and recurrence rows report separate topology-replay and all-flow-union generation and runtime measurements. On-the-fly uses one compact query-local process seed for both physical workloads. Its generation entry is the absolute \texttt{[G] G+W} pair: muted G is process-seed generation and G+W adds the first cold on-the-fly evaluation. Parenthesized values are candidate/reference ratios. All-flow generation shows the absolute pyAmpliCol value without a ratio because its setup boundary differs from the reference. The wall time is the common runtime observable. Not exposed means that a successful wall measurement has no dedicated authenticated evaluator-total timing. Authenticated accumulated warmed evaluator totals appear in the eval column with enough precision to remain distinct from wall time. Its parenthesized multiplier uses the original-AmpliCol wall measurement as denominator because the legacy reference has no separate evaluator-total clock. Recurrence rows retain the independently measured narrower recurrence core in detailed evidence; it is not relabeled as evaluator total. Neither evaluator total nor recurrence core is derived from wall time or from the other. Older entries without authenticated total evidence remain marked not exposed; it is not a missing measurement. user cap: native C++/ASM generation is not attempted above n=6 (native-backend-generation-cap-n6-v1).} +\end{minipage} +\clearpage +\noindent\begin{minipage}{\linewidth} +\subsubsection*{Built-in SM \(d\bar d\to Z+\) gluon ladder (block 3 of 3)} +\begingroup +\centering +\scriptsize +\setlength{\tabcolsep}{1.4pt} +\renewcommand{\arraystretch}{1.05} +\begin{tabular}{@{}r L{1.50in} R{1.26in} R{1.26in} R{1.26in} @{\hspace{0.08in}}R{1.26in} R{1.26in} R{1.26in}@{}} +\toprule +\textbf{n} & \textbf{setup} & \multicolumn{3}{c}{\textbf{selected flow, helicity sum}} & \multicolumn{3}{c}{\textbf{all flows, single helicity}} \\ +& & \textbf{gen [s]} & \textbf{wall [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \textbf{eval [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \textbf{gen [s]} & \textbf{wall [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \textbf{eval [\(\mu\mathrm{s}/\mathrm{pt}\)]} \\ +\midrule +\rowcolor{refblue} +7 & \AC{} reference & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixnotexposed{ReportMuted} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixnotexposed{ReportMuted} \\ +7 & compiled JIT O1 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +7 & compiled ASM O3 & \matrixstaticna{ReportMuted} & \matrixstaticna{ReportMuted} & \matrixstaticna{ReportMuted} & \matrixstaticna{ReportMuted} & \matrixstaticna{ReportMuted} & \matrixstaticna{ReportMuted} \\ +7 & compiled C++ O3 & \matrixstaticna{ReportMuted} & \matrixstaticna{ReportMuted} & \matrixstaticna{ReportMuted} & \matrixstaticna{ReportMuted} & \matrixstaticna{ReportMuted} & \matrixstaticna{ReportMuted} \\ +\rowcolor{ReportGreen!12} +7 & compiled JIT O3 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{ReportGreen!12} +7 & eager-DAG JIT O2 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{ReportGreen!12} +7 & recurrence JIT O2 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{ReportGreen!12} +7 & on-the-fly JIT O2 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +\midrule[0.45pt] +\rowcolor{refblue} +8 & \AC{} reference & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixnotexposed{ReportMuted} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixnotexposed{ReportMuted} \\ +8 & compiled JIT O1 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +8 & compiled ASM O3 & \matrixstaticna{ReportMuted} & \matrixstaticna{ReportMuted} & \matrixstaticna{ReportMuted} & \matrixstaticna{ReportMuted} & \matrixstaticna{ReportMuted} & \matrixstaticna{ReportMuted} \\ +8 & compiled C++ O3 & \matrixstaticna{ReportMuted} & \matrixstaticna{ReportMuted} & \matrixstaticna{ReportMuted} & \matrixstaticna{ReportMuted} & \matrixstaticna{ReportMuted} & \matrixstaticna{ReportMuted} \\ +\rowcolor{ReportGreen!12} +8 & compiled JIT O3 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{ReportGreen!12} +8 & eager-DAG JIT O2 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{ReportGreen!12} +8 & recurrence JIT O2 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{ReportGreen!12} +8 & on-the-fly JIT O2 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +\midrule[0.45pt] +\rowcolor{refblue} +9 & \AC{} reference & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixnotexposed{ReportMuted} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixnotexposed{ReportMuted} \\ +9 & compiled JIT O1 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +9 & compiled ASM O3 & \matrixstaticna{ReportMuted} & \matrixstaticna{ReportMuted} & \matrixstaticna{ReportMuted} & \matrixstaticna{ReportMuted} & \matrixstaticna{ReportMuted} & \matrixstaticna{ReportMuted} \\ +9 & compiled C++ O3 & \matrixstaticna{ReportMuted} & \matrixstaticna{ReportMuted} & \matrixstaticna{ReportMuted} & \matrixstaticna{ReportMuted} & \matrixstaticna{ReportMuted} & \matrixstaticna{ReportMuted} \\ +\rowcolor{ReportGreen!12} +9 & compiled JIT O3 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{ReportGreen!12} +9 & eager-DAG JIT O2 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{ReportGreen!12} +9 & recurrence JIT O2 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{ReportGreen!12} +9 & on-the-fly JIT O2 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +\bottomrule +\end{tabular} +\endgroup +\ReportTableNote{Original AmpliCol is the denominator only when successful and exactly linked by stored same-point evidence, directly or through current recurrence. Otherwise its status remains visible and successful pyAmpliCol values are absolute. Unverified diagnostics retain absolute clocks with a yellow marker, but never enter ratios or summaries. Compiled, eager, and recurrence rows report separate topology-replay and all-flow-union generation and runtime measurements. On-the-fly uses one compact query-local process seed for both physical workloads. Its generation entry is the absolute \texttt{[G] G+W} pair: muted G is process-seed generation and G+W adds the first cold on-the-fly evaluation. Parenthesized values are candidate/reference ratios. All-flow generation shows the absolute pyAmpliCol value without a ratio because its setup boundary differs from the reference. The wall time is the common runtime observable. Not exposed means that a successful wall measurement has no dedicated authenticated evaluator-total timing. Authenticated accumulated warmed evaluator totals appear in the eval column with enough precision to remain distinct from wall time. Its parenthesized multiplier uses the original-AmpliCol wall measurement as denominator because the legacy reference has no separate evaluator-total clock. Recurrence rows retain the independently measured narrower recurrence core in detailed evidence; it is not relabeled as evaluator total. Neither evaluator total nor recurrence core is derived from wall time or from the other. Older entries without authenticated total evidence remain marked not exposed; it is not a missing measurement. user cap: native C++/ASM generation is not attempted above n=6 (native-backend-generation-cap-n6-v1).} +\end{minipage} diff --git a/pyamplicol/src/pyamplicol/_profiling_campaign/result_z_external_sm_table.tex b/pyamplicol/src/pyamplicol/_profiling_campaign/result_z_external_sm_table.tex new file mode 100644 index 0000000000..949cde29e6 --- /dev/null +++ b/pyamplicol/src/pyamplicol/_profiling_campaign/result_z_external_sm_table.tex @@ -0,0 +1,205 @@ +% SPDX-License-Identifier: 0BSD +% Generated by tools/performance_report/render.py; do not edit. +\providecommand{\matrixentryfont}{\fontsize{6.8pt}{7.8pt}\selectfont} +\providecommand{\matrixentryfontlc}{\fontsize{6.5pt}{7.5pt}\selectfont} +\providecommand{\matrixsummaryfont}{\fontsize{6.2pt}{7.4pt}\selectfont} +\providecommand{\matrixfitwidth}[1]{\begingroup\sbox0{#1}\ifdim\wd0>\linewidth\resizebox{\linewidth}{!}{\usebox0}\else\makebox[\linewidth][c]{\usebox0}\fi\endgroup} +\providecommand{\matrixpunct}[1]{\textcolor{black}{\texttt{#1}}} +\providecommand{\matrixratio}[2]{\matrixpunct{(}\textcolor{#1}{\texttt{x#2}}\matrixpunct{)}} +\providecommand{\matrixna}[1]{\textcolor{#1}{\texttt{N/A}}} +\providecommand{\matrixnotapplicable}[1]{\textcolor{#1}{\textsc{not applicable}}} +\providecommand{\matrixnotexposed}[1]{\textcolor{#1}{\textsc{not exposed}}} +\providecommand{\matrixstaticna}[1]{\textcolor{#1}{\textsc{static N/A}}} +\providecommand{\matrixtotalevaluator}[1]{\textcolor{ReportBlue}{\texttt{T }}#1} +\providecommand{\matrixrecurrencecore}[1]{\textcolor{ReportOrange}{\texttt{C }}#1} +\providecommand{\matrixzrecurrenceclocks}[2]{\shortstack[r]{#1\\#2}} +\providecommand{\matrixwallclock}[2]{\textcolor{#1}{\texttt{W #2}}} +\providecommand{\matrixwallabsolute}[1]{\textcolor{ReportBlue}{\texttt{W }}#1} +\providecommand{\matrixruntimepair}[2]{\shortstack[l]{#1\\#2}} +\providecommand{\matrixruntimetriple}[3]{\shortstack[l]{#1\\#2\matrixpunct{ | }#3}} +\providecommand{\matrixbelow}[1]{\textcolor{ReportMuted}{\texttt{<#1}}} +\providecommand{\matrixnaratio}[1]{\matrixpunct{(}\matrixna{#1}\matrixpunct{)}} +\providecommand{\matrixstatus}[2]{\textcolor{#1}{\textsc{#2}}} +\providecommand{\matrixncabsolute}[1]{#1} +\providecommand{\matrixsummaryratio}[2]{\textcolor{#1}{\texttt{x#2}}} +\providecommand{\matrixsummaryratiohighlight}[2]{\begingroup\setlength{\fboxsep}{0.9pt}\setlength{\fboxrule}{0.35pt}\fbox{\textcolor{#1}{{\usefont{T1}{lmtt}{b}{n}x#2}}}\endgroup} +\providecommand{\matrixsummarystats}[5]{\begingroup\matrixsummaryfont\begin{tabular}[t]{@{}l@{\hspace{0.014in}\matrixpunct{|}\hspace{0.014in}}l@{\hspace{0.014in}\matrixpunct{|}\hspace{0.014in}}l@{\hspace{0.014in}\matrixpunct{|}\hspace{0.014in}}l@{\hspace{0.014in}\matrixpunct{|}\hspace{0.014in}}l@{}}\makebox[3.6em][l]{#1}&\makebox[4.6em][l]{#2}&\makebox[3.6em][l]{#3}&\makebox[3.6em][l]{#4}&\makebox[4.2em][l]{#5}\end{tabular}\endgroup} +\providecommand{\matrixsummaryworkloads}[2]{\begin{tabular}[t]{@{}l@{}}#1\\[-0.10em]#2\end{tabular}} +\clearpage +\subsection{UFO-SM dedicated \(d\bar d\to Z+\) gluon performance} +\noindent\begin{minipage}{\linewidth} +\subsubsection*{UFO-SM \(d\bar d\to Z+\) gluon ladder (block 1 of 3)} +\begingroup +\centering +\scriptsize +\setlength{\tabcolsep}{1.4pt} +\renewcommand{\arraystretch}{1.05} +\begin{tabular}{@{}r L{1.50in} R{1.26in} R{1.26in} R{1.26in} @{\hspace{0.08in}}R{1.26in} R{1.26in} R{1.26in}@{}} +\toprule +\textbf{n} & \textbf{setup} & \multicolumn{3}{c}{\textbf{selected flow, helicity sum}} & \multicolumn{3}{c}{\textbf{all flows, single helicity}} \\ +& & \textbf{gen [s]} & \textbf{wall [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \textbf{eval [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \textbf{gen [s]} & \textbf{wall [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \textbf{eval [\(\mu\mathrm{s}/\mathrm{pt}\)]} \\ +\midrule +\rowcolor{refblue} +1 & \AC{} reference & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixnotexposed{ReportMuted} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixnotexposed{ReportMuted} \\ +1 & compiled JIT O1 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +1 & compiled ASM O3 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +1 & compiled C++ O3 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{ReportGreen!12} +1 & compiled JIT O3 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{ReportGreen!12} +1 & eager-DAG JIT O2 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{ReportGreen!12} +1 & recurrence JIT O2 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{ReportGreen!12} +1 & on-the-fly JIT O2 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +\midrule[0.45pt] +\rowcolor{refblue} +2 & \AC{} reference & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixnotexposed{ReportMuted} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixnotexposed{ReportMuted} \\ +2 & compiled JIT O1 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +2 & compiled ASM O3 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +2 & compiled C++ O3 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{ReportGreen!12} +2 & compiled JIT O3 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{ReportGreen!12} +2 & eager-DAG JIT O2 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{ReportGreen!12} +2 & recurrence JIT O2 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{ReportGreen!12} +2 & on-the-fly JIT O2 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +\midrule[0.45pt] +\rowcolor{refblue} +3 & \AC{} reference & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixnotexposed{ReportMuted} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixnotexposed{ReportMuted} \\ +3 & compiled JIT O1 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +3 & compiled ASM O3 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +3 & compiled C++ O3 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{ReportGreen!12} +3 & compiled JIT O3 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{ReportGreen!12} +3 & eager-DAG JIT O2 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{ReportGreen!12} +3 & recurrence JIT O2 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{ReportGreen!12} +3 & on-the-fly JIT O2 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +\bottomrule +\end{tabular} +\endgroup +\ReportTableNote{Original AmpliCol is the denominator only when successful and exactly linked by stored same-point evidence, directly or through current recurrence. Otherwise its status remains visible and successful pyAmpliCol values are absolute. Unverified diagnostics retain absolute clocks with a yellow marker, but never enter ratios or summaries. Compiled, eager, and recurrence rows report separate topology-replay and all-flow-union generation and runtime measurements. On-the-fly uses one compact query-local process seed for both physical workloads. Its generation entry is the absolute \texttt{[G] G+W} pair: muted G is process-seed generation and G+W adds the first cold on-the-fly evaluation. Parenthesized values are candidate/reference ratios. All-flow generation shows the absolute pyAmpliCol value without a ratio because its setup boundary differs from the reference. The wall time is the common runtime observable. Not exposed means that a successful wall measurement has no dedicated authenticated evaluator-total timing. Authenticated accumulated warmed evaluator totals appear in the eval column with enough precision to remain distinct from wall time. Its parenthesized multiplier uses the original-AmpliCol wall measurement as denominator because the legacy reference has no separate evaluator-total clock. Recurrence rows retain the independently measured narrower recurrence core in detailed evidence; it is not relabeled as evaluator total. Neither evaluator total nor recurrence core is derived from wall time or from the other. Older entries without authenticated total evidence remain marked not exposed; it is not a missing measurement. user cap: native C++/ASM generation is not attempted above n=6 (native-backend-generation-cap-n6-v1).} +\end{minipage} +\clearpage +\noindent\begin{minipage}{\linewidth} +\subsubsection*{UFO-SM \(d\bar d\to Z+\) gluon ladder (block 2 of 3)} +\begingroup +\centering +\scriptsize +\setlength{\tabcolsep}{1.4pt} +\renewcommand{\arraystretch}{1.05} +\begin{tabular}{@{}r L{1.50in} R{1.26in} R{1.26in} R{1.26in} @{\hspace{0.08in}}R{1.26in} R{1.26in} R{1.26in}@{}} +\toprule +\textbf{n} & \textbf{setup} & \multicolumn{3}{c}{\textbf{selected flow, helicity sum}} & \multicolumn{3}{c}{\textbf{all flows, single helicity}} \\ +& & \textbf{gen [s]} & \textbf{wall [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \textbf{eval [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \textbf{gen [s]} & \textbf{wall [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \textbf{eval [\(\mu\mathrm{s}/\mathrm{pt}\)]} \\ +\midrule +\rowcolor{refblue} +4 & \AC{} reference & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixnotexposed{ReportMuted} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixnotexposed{ReportMuted} \\ +4 & compiled JIT O1 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +4 & compiled ASM O3 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +4 & compiled C++ O3 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{ReportGreen!12} +4 & compiled JIT O3 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{ReportGreen!12} +4 & eager-DAG JIT O2 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{ReportGreen!12} +4 & recurrence JIT O2 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{ReportGreen!12} +4 & on-the-fly JIT O2 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +\midrule[0.45pt] +\rowcolor{refblue} +5 & \AC{} reference & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixnotexposed{ReportMuted} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixnotexposed{ReportMuted} \\ +5 & compiled JIT O1 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +5 & compiled ASM O3 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +5 & compiled C++ O3 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{ReportGreen!12} +5 & compiled JIT O3 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{ReportGreen!12} +5 & eager-DAG JIT O2 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{ReportGreen!12} +5 & recurrence JIT O2 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{ReportGreen!12} +5 & on-the-fly JIT O2 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +\midrule[0.45pt] +\rowcolor{refblue} +6 & \AC{} reference & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixnotexposed{ReportMuted} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixnotexposed{ReportMuted} \\ +6 & compiled JIT O1 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +6 & compiled ASM O3 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +6 & compiled C++ O3 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{ReportGreen!12} +6 & compiled JIT O3 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{ReportGreen!12} +6 & eager-DAG JIT O2 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{ReportGreen!12} +6 & recurrence JIT O2 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{ReportGreen!12} +6 & on-the-fly JIT O2 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +\bottomrule +\end{tabular} +\endgroup +\ReportTableNote{Original AmpliCol is the denominator only when successful and exactly linked by stored same-point evidence, directly or through current recurrence. Otherwise its status remains visible and successful pyAmpliCol values are absolute. Unverified diagnostics retain absolute clocks with a yellow marker, but never enter ratios or summaries. Compiled, eager, and recurrence rows report separate topology-replay and all-flow-union generation and runtime measurements. On-the-fly uses one compact query-local process seed for both physical workloads. Its generation entry is the absolute \texttt{[G] G+W} pair: muted G is process-seed generation and G+W adds the first cold on-the-fly evaluation. Parenthesized values are candidate/reference ratios. All-flow generation shows the absolute pyAmpliCol value without a ratio because its setup boundary differs from the reference. The wall time is the common runtime observable. Not exposed means that a successful wall measurement has no dedicated authenticated evaluator-total timing. Authenticated accumulated warmed evaluator totals appear in the eval column with enough precision to remain distinct from wall time. Its parenthesized multiplier uses the original-AmpliCol wall measurement as denominator because the legacy reference has no separate evaluator-total clock. Recurrence rows retain the independently measured narrower recurrence core in detailed evidence; it is not relabeled as evaluator total. Neither evaluator total nor recurrence core is derived from wall time or from the other. Older entries without authenticated total evidence remain marked not exposed; it is not a missing measurement. user cap: native C++/ASM generation is not attempted above n=6 (native-backend-generation-cap-n6-v1).} +\end{minipage} +\clearpage +\noindent\begin{minipage}{\linewidth} +\subsubsection*{UFO-SM \(d\bar d\to Z+\) gluon ladder (block 3 of 3)} +\begingroup +\centering +\scriptsize +\setlength{\tabcolsep}{1.4pt} +\renewcommand{\arraystretch}{1.05} +\begin{tabular}{@{}r L{1.50in} R{1.26in} R{1.26in} R{1.26in} @{\hspace{0.08in}}R{1.26in} R{1.26in} R{1.26in}@{}} +\toprule +\textbf{n} & \textbf{setup} & \multicolumn{3}{c}{\textbf{selected flow, helicity sum}} & \multicolumn{3}{c}{\textbf{all flows, single helicity}} \\ +& & \textbf{gen [s]} & \textbf{wall [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \textbf{eval [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \textbf{gen [s]} & \textbf{wall [\(\mu\mathrm{s}/\mathrm{pt}\)]} & \textbf{eval [\(\mu\mathrm{s}/\mathrm{pt}\)]} \\ +\midrule +\rowcolor{refblue} +7 & \AC{} reference & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixnotexposed{ReportMuted} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixnotexposed{ReportMuted} \\ +7 & compiled JIT O1 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +7 & compiled ASM O3 & \matrixstaticna{ReportMuted} & \matrixstaticna{ReportMuted} & \matrixstaticna{ReportMuted} & \matrixstaticna{ReportMuted} & \matrixstaticna{ReportMuted} & \matrixstaticna{ReportMuted} \\ +7 & compiled C++ O3 & \matrixstaticna{ReportMuted} & \matrixstaticna{ReportMuted} & \matrixstaticna{ReportMuted} & \matrixstaticna{ReportMuted} & \matrixstaticna{ReportMuted} & \matrixstaticna{ReportMuted} \\ +\rowcolor{ReportGreen!12} +7 & compiled JIT O3 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{ReportGreen!12} +7 & eager-DAG JIT O2 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{ReportGreen!12} +7 & recurrence JIT O2 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{ReportGreen!12} +7 & on-the-fly JIT O2 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +\midrule[0.45pt] +\rowcolor{refblue} +8 & \AC{} reference & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixnotexposed{ReportMuted} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixnotexposed{ReportMuted} \\ +8 & compiled JIT O1 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +8 & compiled ASM O3 & \matrixstaticna{ReportMuted} & \matrixstaticna{ReportMuted} & \matrixstaticna{ReportMuted} & \matrixstaticna{ReportMuted} & \matrixstaticna{ReportMuted} & \matrixstaticna{ReportMuted} \\ +8 & compiled C++ O3 & \matrixstaticna{ReportMuted} & \matrixstaticna{ReportMuted} & \matrixstaticna{ReportMuted} & \matrixstaticna{ReportMuted} & \matrixstaticna{ReportMuted} & \matrixstaticna{ReportMuted} \\ +\rowcolor{ReportGreen!12} +8 & compiled JIT O3 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{ReportGreen!12} +8 & eager-DAG JIT O2 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{ReportGreen!12} +8 & recurrence JIT O2 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{ReportGreen!12} +8 & on-the-fly JIT O2 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +\midrule[0.45pt] +\rowcolor{refblue} +9 & \AC{} reference & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixnotexposed{ReportMuted} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixnotexposed{ReportMuted} \\ +9 & compiled JIT O1 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +9 & compiled ASM O3 & \matrixstaticna{ReportMuted} & \matrixstaticna{ReportMuted} & \matrixstaticna{ReportMuted} & \matrixstaticna{ReportMuted} & \matrixstaticna{ReportMuted} & \matrixstaticna{ReportMuted} \\ +9 & compiled C++ O3 & \matrixstaticna{ReportMuted} & \matrixstaticna{ReportMuted} & \matrixstaticna{ReportMuted} & \matrixstaticna{ReportMuted} & \matrixstaticna{ReportMuted} & \matrixstaticna{ReportMuted} \\ +\rowcolor{ReportGreen!12} +9 & compiled JIT O3 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{ReportGreen!12} +9 & eager-DAG JIT O2 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{ReportGreen!12} +9 & recurrence JIT O2 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +\rowcolor{ReportGreen!12} +9 & on-the-fly JIT O2 & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} & \matrixstatus{ReportMuted}{N/A} \\ +\bottomrule +\end{tabular} +\endgroup +\ReportTableNote{Original AmpliCol is the denominator only when successful and exactly linked by stored same-point evidence, directly or through current recurrence. Otherwise its status remains visible and successful pyAmpliCol values are absolute. Unverified diagnostics retain absolute clocks with a yellow marker, but never enter ratios or summaries. Compiled, eager, and recurrence rows report separate topology-replay and all-flow-union generation and runtime measurements. On-the-fly uses one compact query-local process seed for both physical workloads. Its generation entry is the absolute \texttt{[G] G+W} pair: muted G is process-seed generation and G+W adds the first cold on-the-fly evaluation. Parenthesized values are candidate/reference ratios. All-flow generation shows the absolute pyAmpliCol value without a ratio because its setup boundary differs from the reference. The wall time is the common runtime observable. Not exposed means that a successful wall measurement has no dedicated authenticated evaluator-total timing. Authenticated accumulated warmed evaluator totals appear in the eval column with enough precision to remain distinct from wall time. Its parenthesized multiplier uses the original-AmpliCol wall measurement as denominator because the legacy reference has no separate evaluator-total clock. Recurrence rows retain the independently measured narrower recurrence core in detailed evidence; it is not relabeled as evaluator total. Neither evaluator total nor recurrence core is derived from wall time or from the other. Older entries without authenticated total evidence remain marked not exposed; it is not a missing measurement. user cap: native C++/ASM generation is not attempted above n=6 (native-backend-generation-cap-n6-v1).} +\end{minipage} diff --git a/pyamplicol/src/pyamplicol/_profiling_campaign/results/matrix_compiled_builtin_sm_full.json b/pyamplicol/src/pyamplicol/_profiling_campaign/results/matrix_compiled_builtin_sm_full.json new file mode 100644 index 0000000000..6ef9d5f8d1 --- /dev/null +++ b/pyamplicol/src/pyamplicol/_profiling_campaign/results/matrix_compiled_builtin_sm_full.json @@ -0,0 +1 @@ +{"$schema":"./report-cache.schema.json","dataset_id":"matrix_compiled_builtin_sm_full","entries":[{"cell_id":"matrix-compiled-builtin-sm-full-n1-dd-z-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":1,"process":"d d~ > z","process_key":"dd_z_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-builtin-sm-full-n1-ud-w-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":1,"process":"u d~ > w+","process_key":"ud_w_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-builtin-sm-full-n2-dd-epem-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":2,"process":"d d~ > e+ e-","process_key":"dd_epem_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-builtin-sm-full-n2-dd-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":2,"process":"d d~ > t t~","process_key":"dd_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-builtin-sm-full-n2-dd-z-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":2,"process":"d d~ > z g","process_key":"dd_z_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-builtin-sm-full-n2-dd-zz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":2,"process":"d d~ > z z","process_key":"dd_zz_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-builtin-sm-full-n2-gg-gluons-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":2,"process":"g g > g g","process_key":"gg_gluons","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-builtin-sm-full-n2-gg-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":2,"process":"g g > t t~","process_key":"gg_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-builtin-sm-full-n2-ud-epve-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":2,"process":"u d~ > e+ ve","process_key":"ud_epve_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-builtin-sm-full-n2-ud-w-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":2,"process":"u d~ > w+ g","process_key":"ud_w_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-builtin-sm-full-n3-dd-epem-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":3,"process":"d d~ > e+ e- g","process_key":"dd_epem_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-builtin-sm-full-n3-dd-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":3,"process":"d d~ > t t~ g","process_key":"dd_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-builtin-sm-full-n3-dd-z-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":3,"process":"d d~ > z g g","process_key":"dd_z_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-builtin-sm-full-n3-dd-zz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":3,"process":"d d~ > z z g","process_key":"dd_zz_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-builtin-sm-full-n3-dd-zzz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":3,"process":"d d~ > z z z","process_key":"dd_zzz_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-builtin-sm-full-n3-gg-gluons-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":3,"process":"g g > g g g","process_key":"gg_gluons","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-builtin-sm-full-n3-gg-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":3,"process":"g g > t t~ g","process_key":"gg_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-builtin-sm-full-n3-ud-epve-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":3,"process":"u d~ > e+ ve g","process_key":"ud_epve_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-builtin-sm-full-n3-ud-w-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":3,"process":"u d~ > w+ g g","process_key":"ud_w_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-builtin-sm-full-n4-dd-3q-identical-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":4,"process":"d d~ > u u~ u u~","process_key":"dd_3q_identical_lines","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-builtin-sm-full-n4-dd-3q-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":4,"process":"d d~ > u u~ s s~","process_key":"dd_3q_lines","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-builtin-sm-full-n4-dd-4l-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":4,"process":"d d~ > e+ e- e+ e-","process_key":"dd_4l_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-builtin-sm-full-n4-dd-epem-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":4,"process":"d d~ > e+ e- g g","process_key":"dd_epem_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-builtin-sm-full-n4-dd-epemzh-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":4,"process":"d d~ > e+ e- z h","process_key":"dd_epemzh_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-builtin-sm-full-n4-dd-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":4,"process":"d d~ > t t~ g g","process_key":"dd_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-builtin-sm-full-n4-dd-ttzh-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":4,"process":"d d~ > t t~ z h","process_key":"dd_ttzh_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-builtin-sm-full-n4-dd-z-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":4,"process":"d d~ > z g g g","process_key":"dd_z_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-builtin-sm-full-n4-dd-zz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":4,"process":"d d~ > z z g g","process_key":"dd_zz_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-builtin-sm-full-n4-dd-zzz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":4,"process":"d d~ > z z z g","process_key":"dd_zzz_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-builtin-sm-full-n4-gg-gluons-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":4,"process":"g g > g g g g","process_key":"gg_gluons","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-builtin-sm-full-n4-gg-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":4,"process":"g g > t t~ g g","process_key":"gg_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-builtin-sm-full-n4-ud-epve-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":4,"process":"u d~ > e+ ve g g","process_key":"ud_epve_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-builtin-sm-full-n4-ud-w-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":4,"process":"u d~ > w+ g g g","process_key":"ud_w_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-builtin-sm-full-n5-dd-3q-identical-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":5,"process":"d d~ > u u~ u u~ g","process_key":"dd_3q_identical_lines","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-builtin-sm-full-n5-dd-3q-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":5,"process":"d d~ > u u~ s s~ g","process_key":"dd_3q_lines","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-builtin-sm-full-n5-dd-4l-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":5,"process":"d d~ > e+ e- e+ e- g","process_key":"dd_4l_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-builtin-sm-full-n5-dd-epem-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":5,"process":"d d~ > e+ e- g g g","process_key":"dd_epem_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-builtin-sm-full-n5-dd-epemzh-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":5,"process":"d d~ > e+ e- z h g","process_key":"dd_epemzh_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-builtin-sm-full-n5-dd-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":5,"process":"d d~ > t t~ g g g","process_key":"dd_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-builtin-sm-full-n5-dd-ttzh-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":5,"process":"d d~ > t t~ z h g","process_key":"dd_ttzh_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-builtin-sm-full-n5-dd-z-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":5,"process":"d d~ > z g g g g","process_key":"dd_z_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-builtin-sm-full-n5-dd-zz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":5,"process":"d d~ > z z g g g","process_key":"dd_zz_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-builtin-sm-full-n5-dd-zzz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":5,"process":"d d~ > z z z g g","process_key":"dd_zzz_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-builtin-sm-full-n5-gg-gluons-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":5,"process":"g g > g g g g g","process_key":"gg_gluons","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-builtin-sm-full-n5-gg-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":5,"process":"g g > t t~ g g g","process_key":"gg_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-builtin-sm-full-n5-ud-epve-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":5,"process":"u d~ > e+ ve g g g","process_key":"ud_epve_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-builtin-sm-full-n5-ud-w-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":5,"process":"u d~ > w+ g g g g","process_key":"ud_w_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-builtin-sm-full-n6-dd-3q-identical-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":6,"process":"d d~ > u u~ u u~ g g","process_key":"dd_3q_identical_lines","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-builtin-sm-full-n6-dd-3q-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":6,"process":"d d~ > u u~ s s~ g g","process_key":"dd_3q_lines","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-builtin-sm-full-n6-dd-4q-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":6,"process":"d d~ > u u~ s s~ c c~","process_key":"dd_4q_lines","variant":null,"workload":"contracted"}],"kind":"performance_measurements","measurement_specs":[{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"}],"report_version":"0.3.0","schema_version":5} diff --git a/pyamplicol/src/pyamplicol/_profiling_campaign/results/matrix_compiled_builtin_sm_lc.json b/pyamplicol/src/pyamplicol/_profiling_campaign/results/matrix_compiled_builtin_sm_lc.json new file mode 100644 index 0000000000..e2aa032bfc --- /dev/null +++ b/pyamplicol/src/pyamplicol/_profiling_campaign/results/matrix_compiled_builtin_sm_lc.json @@ -0,0 +1 @@ +{"$schema":"./report-cache.schema.json","dataset_id":"matrix_compiled_builtin_sm_lc","entries":[{"cell_id":"matrix-compiled-builtin-sm-lc-n1-dd-z-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":1,"process":"d d~ > z","process_key":"dd_z_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n1-dd-z-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":1,"process":"d d~ > z","process_key":"dd_z_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n1-ud-w-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":1,"process":"u d~ > w+","process_key":"ud_w_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n1-ud-w-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":1,"process":"u d~ > w+","process_key":"ud_w_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n2-dd-epem-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":2,"process":"d d~ > e+ e-","process_key":"dd_epem_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n2-dd-epem-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":2,"process":"d d~ > e+ e-","process_key":"dd_epem_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n2-dd-tt-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":2,"process":"d d~ > t t~","process_key":"dd_tt_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n2-dd-tt-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":2,"process":"d d~ > t t~","process_key":"dd_tt_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n2-dd-z-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":2,"process":"d d~ > z g","process_key":"dd_z_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n2-dd-z-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":2,"process":"d d~ > z g","process_key":"dd_z_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n2-dd-zz-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":2,"process":"d d~ > z z","process_key":"dd_zz_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n2-dd-zz-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":2,"process":"d d~ > z z","process_key":"dd_zz_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n2-gg-gluons-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":2,"process":"g g > g g","process_key":"gg_gluons","variant":null,"workload":"all-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n2-gg-gluons-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":2,"process":"g g > g g","process_key":"gg_gluons","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n2-gg-tt-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":2,"process":"g g > t t~","process_key":"gg_tt_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n2-gg-tt-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":2,"process":"g g > t t~","process_key":"gg_tt_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n2-ud-epve-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":2,"process":"u d~ > e+ ve","process_key":"ud_epve_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n2-ud-epve-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":2,"process":"u d~ > e+ ve","process_key":"ud_epve_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n2-ud-w-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":2,"process":"u d~ > w+ g","process_key":"ud_w_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n2-ud-w-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":2,"process":"u d~ > w+ g","process_key":"ud_w_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n3-dd-epem-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":3,"process":"d d~ > e+ e- g","process_key":"dd_epem_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n3-dd-epem-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":3,"process":"d d~ > e+ e- g","process_key":"dd_epem_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n3-dd-tt-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":3,"process":"d d~ > t t~ g","process_key":"dd_tt_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n3-dd-tt-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":3,"process":"d d~ > t t~ g","process_key":"dd_tt_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n3-dd-z-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":3,"process":"d d~ > z g g","process_key":"dd_z_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n3-dd-z-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":3,"process":"d d~ > z g g","process_key":"dd_z_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n3-dd-zz-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":3,"process":"d d~ > z z g","process_key":"dd_zz_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n3-dd-zz-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":3,"process":"d d~ > z z g","process_key":"dd_zz_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n3-dd-zzz-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":3,"process":"d d~ > z z z","process_key":"dd_zzz_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n3-dd-zzz-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":3,"process":"d d~ > z z z","process_key":"dd_zzz_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n3-gg-gluons-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":3,"process":"g g > g g g","process_key":"gg_gluons","variant":null,"workload":"all-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n3-gg-gluons-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":3,"process":"g g > g g g","process_key":"gg_gluons","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n3-gg-tt-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":3,"process":"g g > t t~ g","process_key":"gg_tt_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n3-gg-tt-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":3,"process":"g g > t t~ g","process_key":"gg_tt_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n3-ud-epve-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":3,"process":"u d~ > e+ ve g","process_key":"ud_epve_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n3-ud-epve-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":3,"process":"u d~ > e+ ve g","process_key":"ud_epve_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n3-ud-w-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":3,"process":"u d~ > w+ g g","process_key":"ud_w_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n3-ud-w-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":3,"process":"u d~ > w+ g g","process_key":"ud_w_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n4-dd-3q-identical-lines-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":4,"process":"d d~ > u u~ u u~","process_key":"dd_3q_identical_lines","variant":null,"workload":"all-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n4-dd-3q-identical-lines-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":4,"process":"d d~ > u u~ u u~","process_key":"dd_3q_identical_lines","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n4-dd-3q-lines-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":4,"process":"d d~ > u u~ s s~","process_key":"dd_3q_lines","variant":null,"workload":"all-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n4-dd-3q-lines-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":4,"process":"d d~ > u u~ s s~","process_key":"dd_3q_lines","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n4-dd-4l-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":4,"process":"d d~ > e+ e- e+ e-","process_key":"dd_4l_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n4-dd-4l-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":4,"process":"d d~ > e+ e- e+ e-","process_key":"dd_4l_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n4-dd-epem-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":4,"process":"d d~ > e+ e- g g","process_key":"dd_epem_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n4-dd-epem-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":4,"process":"d d~ > e+ e- g g","process_key":"dd_epem_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n4-dd-epemzh-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":4,"process":"d d~ > e+ e- z h","process_key":"dd_epemzh_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n4-dd-epemzh-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":4,"process":"d d~ > e+ e- z h","process_key":"dd_epemzh_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n4-dd-tt-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":4,"process":"d d~ > t t~ g g","process_key":"dd_tt_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n4-dd-tt-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":4,"process":"d d~ > t t~ g g","process_key":"dd_tt_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n4-dd-ttzh-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":4,"process":"d d~ > t t~ z h","process_key":"dd_ttzh_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n4-dd-ttzh-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":4,"process":"d d~ > t t~ z h","process_key":"dd_ttzh_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n4-dd-z-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":4,"process":"d d~ > z g g g","process_key":"dd_z_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n4-dd-z-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":4,"process":"d d~ > z g g g","process_key":"dd_z_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n4-dd-zz-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":4,"process":"d d~ > z z g g","process_key":"dd_zz_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n4-dd-zz-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":4,"process":"d d~ > z z g g","process_key":"dd_zz_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n4-dd-zzz-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":4,"process":"d d~ > z z z g","process_key":"dd_zzz_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n4-dd-zzz-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":4,"process":"d d~ > z z z g","process_key":"dd_zzz_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n4-gg-gluons-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":4,"process":"g g > g g g g","process_key":"gg_gluons","variant":null,"workload":"all-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n4-gg-gluons-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":4,"process":"g g > g g g g","process_key":"gg_gluons","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n4-gg-tt-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":4,"process":"g g > t t~ g g","process_key":"gg_tt_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n4-gg-tt-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":4,"process":"g g > t t~ g g","process_key":"gg_tt_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n4-ud-epve-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":4,"process":"u d~ > e+ ve g g","process_key":"ud_epve_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n4-ud-epve-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":4,"process":"u d~ > e+ ve g g","process_key":"ud_epve_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n4-ud-w-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":4,"process":"u d~ > w+ g g g","process_key":"ud_w_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n4-ud-w-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":4,"process":"u d~ > w+ g g g","process_key":"ud_w_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n5-dd-3q-identical-lines-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":5,"process":"d d~ > u u~ u u~ g","process_key":"dd_3q_identical_lines","variant":null,"workload":"all-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n5-dd-3q-identical-lines-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":5,"process":"d d~ > u u~ u u~ g","process_key":"dd_3q_identical_lines","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n5-dd-3q-lines-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":5,"process":"d d~ > u u~ s s~ g","process_key":"dd_3q_lines","variant":null,"workload":"all-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n5-dd-3q-lines-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":5,"process":"d d~ > u u~ s s~ g","process_key":"dd_3q_lines","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n5-dd-4l-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":5,"process":"d d~ > e+ e- e+ e- g","process_key":"dd_4l_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n5-dd-4l-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":5,"process":"d d~ > e+ e- e+ e- g","process_key":"dd_4l_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n5-dd-epem-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":5,"process":"d d~ > e+ e- g g g","process_key":"dd_epem_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n5-dd-epem-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":5,"process":"d d~ > e+ e- g g g","process_key":"dd_epem_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n5-dd-epemzh-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":5,"process":"d d~ > e+ e- z h g","process_key":"dd_epemzh_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n5-dd-epemzh-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":5,"process":"d d~ > e+ e- z h g","process_key":"dd_epemzh_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n5-dd-tt-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":5,"process":"d d~ > t t~ g g g","process_key":"dd_tt_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n5-dd-tt-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":5,"process":"d d~ > t t~ g g g","process_key":"dd_tt_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n5-dd-ttzh-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":5,"process":"d d~ > t t~ z h g","process_key":"dd_ttzh_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n5-dd-ttzh-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":5,"process":"d d~ > t t~ z h g","process_key":"dd_ttzh_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n5-dd-z-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":5,"process":"d d~ > z g g g g","process_key":"dd_z_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n5-dd-z-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":5,"process":"d d~ > z g g g g","process_key":"dd_z_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n5-dd-zz-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":5,"process":"d d~ > z z g g g","process_key":"dd_zz_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n5-dd-zz-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":5,"process":"d d~ > z z g g g","process_key":"dd_zz_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n5-dd-zzz-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":5,"process":"d d~ > z z z g g","process_key":"dd_zzz_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n5-dd-zzz-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":5,"process":"d d~ > z z z g g","process_key":"dd_zzz_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n5-gg-gluons-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":5,"process":"g g > g g g g g","process_key":"gg_gluons","variant":null,"workload":"all-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n5-gg-gluons-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":5,"process":"g g > g g g g g","process_key":"gg_gluons","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n5-gg-tt-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":5,"process":"g g > t t~ g g g","process_key":"gg_tt_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n5-gg-tt-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":5,"process":"g g > t t~ g g g","process_key":"gg_tt_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n5-ud-epve-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":5,"process":"u d~ > e+ ve g g g","process_key":"ud_epve_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n5-ud-epve-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":5,"process":"u d~ > e+ ve g g g","process_key":"ud_epve_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n5-ud-w-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":5,"process":"u d~ > w+ g g g g","process_key":"ud_w_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n5-ud-w-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":5,"process":"u d~ > w+ g g g g","process_key":"ud_w_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n6-dd-3q-identical-lines-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":6,"process":"d d~ > u u~ u u~ g g","process_key":"dd_3q_identical_lines","variant":null,"workload":"all-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n6-dd-3q-identical-lines-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":6,"process":"d d~ > u u~ u u~ g g","process_key":"dd_3q_identical_lines","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n6-dd-3q-lines-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":6,"process":"d d~ > u u~ s s~ g g","process_key":"dd_3q_lines","variant":null,"workload":"all-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n6-dd-3q-lines-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":6,"process":"d d~ > u u~ s s~ g g","process_key":"dd_3q_lines","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n6-dd-4l-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":6,"process":"d d~ > e+ e- e+ e- g g","process_key":"dd_4l_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n6-dd-4l-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":6,"process":"d d~ > e+ e- e+ e- g g","process_key":"dd_4l_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n6-dd-4q-lines-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":6,"process":"d d~ > u u~ s s~ c c~","process_key":"dd_4q_lines","variant":null,"workload":"all-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n6-dd-4q-lines-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":6,"process":"d d~ > u u~ s s~ c c~","process_key":"dd_4q_lines","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n6-dd-epem-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":6,"process":"d d~ > e+ e- g g g g","process_key":"dd_epem_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n6-dd-epem-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":6,"process":"d d~ > e+ e- g g g g","process_key":"dd_epem_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n6-dd-epemzh-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":6,"process":"d d~ > e+ e- z h g g","process_key":"dd_epemzh_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n6-dd-epemzh-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":6,"process":"d d~ > e+ e- z h g g","process_key":"dd_epemzh_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n6-dd-tt-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":6,"process":"d d~ > t t~ g g g g","process_key":"dd_tt_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n6-dd-tt-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":6,"process":"d d~ > t t~ g g g g","process_key":"dd_tt_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n6-dd-ttzh-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":6,"process":"d d~ > t t~ z h g g","process_key":"dd_ttzh_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n6-dd-ttzh-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":6,"process":"d d~ > t t~ z h g g","process_key":"dd_ttzh_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n6-dd-z-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":6,"process":"d d~ > z g g g g g","process_key":"dd_z_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n6-dd-z-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":6,"process":"d d~ > z g g g g g","process_key":"dd_z_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n6-dd-zz-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":6,"process":"d d~ > z z g g g g","process_key":"dd_zz_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n6-dd-zz-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":6,"process":"d d~ > z z g g g g","process_key":"dd_zz_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n6-dd-zzz-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":6,"process":"d d~ > z z z g g g","process_key":"dd_zzz_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n6-dd-zzz-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":6,"process":"d d~ > z z z g g g","process_key":"dd_zzz_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n6-gg-gluons-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":6,"process":"g g > g g g g g g","process_key":"gg_gluons","variant":null,"workload":"all-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n6-gg-gluons-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":6,"process":"g g > g g g g g g","process_key":"gg_gluons","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n6-gg-tt-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":6,"process":"g g > t t~ g g g g","process_key":"gg_tt_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n6-gg-tt-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":6,"process":"g g > t t~ g g g g","process_key":"gg_tt_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n6-ud-epve-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":6,"process":"u d~ > e+ ve g g g g","process_key":"ud_epve_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n6-ud-epve-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":6,"process":"u d~ > e+ ve g g g g","process_key":"ud_epve_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n6-ud-w-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":6,"process":"u d~ > w+ g g g g g","process_key":"ud_w_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n6-ud-w-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":6,"process":"u d~ > w+ g g g g g","process_key":"ud_w_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n7-dd-3q-identical-lines-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":7,"process":"d d~ > u u~ u u~ g g g","process_key":"dd_3q_identical_lines","variant":null,"workload":"all-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n7-dd-3q-identical-lines-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":7,"process":"d d~ > u u~ u u~ g g g","process_key":"dd_3q_identical_lines","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n7-dd-3q-lines-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":7,"process":"d d~ > u u~ s s~ g g g","process_key":"dd_3q_lines","variant":null,"workload":"all-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n7-dd-3q-lines-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":7,"process":"d d~ > u u~ s s~ g g g","process_key":"dd_3q_lines","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n7-dd-4l-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":7,"process":"d d~ > e+ e- e+ e- g g g","process_key":"dd_4l_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n7-dd-4l-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":7,"process":"d d~ > e+ e- e+ e- g g g","process_key":"dd_4l_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n7-dd-4q-lines-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":7,"process":"d d~ > u u~ s s~ c c~ g","process_key":"dd_4q_lines","variant":null,"workload":"all-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n7-dd-4q-lines-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":7,"process":"d d~ > u u~ s s~ c c~ g","process_key":"dd_4q_lines","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n7-dd-epem-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":7,"process":"d d~ > e+ e- g g g g g","process_key":"dd_epem_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n7-dd-epem-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":7,"process":"d d~ > e+ e- g g g g g","process_key":"dd_epem_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n7-dd-epemzh-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":7,"process":"d d~ > e+ e- z h g g g","process_key":"dd_epemzh_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n7-dd-epemzh-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":7,"process":"d d~ > e+ e- z h g g g","process_key":"dd_epemzh_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n7-dd-tt-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":7,"process":"d d~ > t t~ g g g g g","process_key":"dd_tt_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n7-dd-tt-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":7,"process":"d d~ > t t~ g g g g g","process_key":"dd_tt_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n7-dd-ttzh-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":7,"process":"d d~ > t t~ z h g g g","process_key":"dd_ttzh_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n7-dd-ttzh-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":7,"process":"d d~ > t t~ z h g g g","process_key":"dd_ttzh_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n7-dd-z-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":7,"process":"d d~ > z g g g g g g","process_key":"dd_z_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n7-dd-z-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":7,"process":"d d~ > z g g g g g g","process_key":"dd_z_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n7-dd-zz-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":7,"process":"d d~ > z z g g g g g","process_key":"dd_zz_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n7-dd-zz-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":7,"process":"d d~ > z z g g g g g","process_key":"dd_zz_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n7-dd-zzz-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":7,"process":"d d~ > z z z g g g g","process_key":"dd_zzz_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n7-dd-zzz-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":7,"process":"d d~ > z z z g g g g","process_key":"dd_zzz_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n7-gg-gluons-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":7,"process":"g g > g g g g g g g","process_key":"gg_gluons","variant":null,"workload":"all-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n7-gg-gluons-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":7,"process":"g g > g g g g g g g","process_key":"gg_gluons","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n7-gg-tt-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":7,"process":"g g > t t~ g g g g g","process_key":"gg_tt_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n7-gg-tt-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":7,"process":"g g > t t~ g g g g g","process_key":"gg_tt_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n7-ud-epve-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":7,"process":"u d~ > e+ ve g g g g g","process_key":"ud_epve_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n7-ud-epve-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":7,"process":"u d~ > e+ ve g g g g g","process_key":"ud_epve_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n7-ud-w-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":7,"process":"u d~ > w+ g g g g g g","process_key":"ud_w_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n7-ud-w-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":7,"process":"u d~ > w+ g g g g g g","process_key":"ud_w_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n8-dd-3q-identical-lines-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":8,"process":"d d~ > u u~ u u~ g g g g","process_key":"dd_3q_identical_lines","variant":null,"workload":"all-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n8-dd-3q-identical-lines-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":8,"process":"d d~ > u u~ u u~ g g g g","process_key":"dd_3q_identical_lines","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n8-dd-3q-lines-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":8,"process":"d d~ > u u~ s s~ g g g g","process_key":"dd_3q_lines","variant":null,"workload":"all-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n8-dd-3q-lines-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":8,"process":"d d~ > u u~ s s~ g g g g","process_key":"dd_3q_lines","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n8-dd-4l-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":8,"process":"d d~ > e+ e- e+ e- g g g g","process_key":"dd_4l_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n8-dd-4l-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":8,"process":"d d~ > e+ e- e+ e- g g g g","process_key":"dd_4l_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n8-dd-4q-lines-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":8,"process":"d d~ > u u~ s s~ c c~ g g","process_key":"dd_4q_lines","variant":null,"workload":"all-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n8-dd-4q-lines-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":8,"process":"d d~ > u u~ s s~ c c~ g g","process_key":"dd_4q_lines","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n8-dd-epem-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":8,"process":"d d~ > e+ e- g g g g g g","process_key":"dd_epem_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n8-dd-epem-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":8,"process":"d d~ > e+ e- g g g g g g","process_key":"dd_epem_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n8-dd-epemzh-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":8,"process":"d d~ > e+ e- z h g g g g","process_key":"dd_epemzh_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n8-dd-epemzh-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":8,"process":"d d~ > e+ e- z h g g g g","process_key":"dd_epemzh_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n8-dd-tt-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":8,"process":"d d~ > t t~ g g g g g g","process_key":"dd_tt_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n8-dd-tt-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":8,"process":"d d~ > t t~ g g g g g g","process_key":"dd_tt_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n8-dd-ttzh-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":8,"process":"d d~ > t t~ z h g g g g","process_key":"dd_ttzh_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n8-dd-ttzh-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":8,"process":"d d~ > t t~ z h g g g g","process_key":"dd_ttzh_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n8-dd-z-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":8,"process":"d d~ > z g g g g g g g","process_key":"dd_z_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n8-dd-z-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":8,"process":"d d~ > z g g g g g g g","process_key":"dd_z_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n8-dd-zz-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":8,"process":"d d~ > z z g g g g g g","process_key":"dd_zz_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n8-dd-zz-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":8,"process":"d d~ > z z g g g g g g","process_key":"dd_zz_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n8-dd-zzz-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":8,"process":"d d~ > z z z g g g g g","process_key":"dd_zzz_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n8-dd-zzz-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":8,"process":"d d~ > z z z g g g g g","process_key":"dd_zzz_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n8-gg-gluons-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":8,"process":"g g > g g g g g g g g","process_key":"gg_gluons","variant":null,"workload":"all-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n8-gg-gluons-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":8,"process":"g g > g g g g g g g g","process_key":"gg_gluons","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n8-gg-tt-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":8,"process":"g g > t t~ g g g g g g","process_key":"gg_tt_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n8-gg-tt-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":8,"process":"g g > t t~ g g g g g g","process_key":"gg_tt_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n8-ud-epve-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":8,"process":"u d~ > e+ ve g g g g g g","process_key":"ud_epve_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n8-ud-epve-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":8,"process":"u d~ > e+ ve g g g g g g","process_key":"ud_epve_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n8-ud-w-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":8,"process":"u d~ > w+ g g g g g g g","process_key":"ud_w_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n8-ud-w-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":8,"process":"u d~ > w+ g g g g g g g","process_key":"ud_w_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n9-dd-3q-identical-lines-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":9,"process":"d d~ > u u~ u u~ g g g g g","process_key":"dd_3q_identical_lines","variant":null,"workload":"all-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n9-dd-3q-identical-lines-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":9,"process":"d d~ > u u~ u u~ g g g g g","process_key":"dd_3q_identical_lines","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n9-dd-3q-lines-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":9,"process":"d d~ > u u~ s s~ g g g g g","process_key":"dd_3q_lines","variant":null,"workload":"all-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n9-dd-3q-lines-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":9,"process":"d d~ > u u~ s s~ g g g g g","process_key":"dd_3q_lines","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n9-dd-4l-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":9,"process":"d d~ > e+ e- e+ e- g g g g g","process_key":"dd_4l_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n9-dd-4l-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":9,"process":"d d~ > e+ e- e+ e- g g g g g","process_key":"dd_4l_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n9-dd-4q-lines-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":9,"process":"d d~ > u u~ s s~ c c~ g g g","process_key":"dd_4q_lines","variant":null,"workload":"all-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n9-dd-4q-lines-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":9,"process":"d d~ > u u~ s s~ c c~ g g g","process_key":"dd_4q_lines","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n9-dd-epem-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":9,"process":"d d~ > e+ e- g g g g g g g","process_key":"dd_epem_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n9-dd-epem-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":9,"process":"d d~ > e+ e- g g g g g g g","process_key":"dd_epem_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n9-dd-epemzh-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":9,"process":"d d~ > e+ e- z h g g g g g","process_key":"dd_epemzh_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n9-dd-epemzh-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":9,"process":"d d~ > e+ e- z h g g g g g","process_key":"dd_epemzh_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n9-dd-tt-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":9,"process":"d d~ > t t~ g g g g g g g","process_key":"dd_tt_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n9-dd-tt-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":9,"process":"d d~ > t t~ g g g g g g g","process_key":"dd_tt_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n9-dd-ttzh-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":9,"process":"d d~ > t t~ z h g g g g g","process_key":"dd_ttzh_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n9-dd-ttzh-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":9,"process":"d d~ > t t~ z h g g g g g","process_key":"dd_ttzh_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n9-dd-z-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":9,"process":"d d~ > z g g g g g g g g","process_key":"dd_z_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n9-dd-z-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":9,"process":"d d~ > z g g g g g g g g","process_key":"dd_z_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n9-dd-zz-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":9,"process":"d d~ > z z g g g g g g g","process_key":"dd_zz_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n9-dd-zz-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":9,"process":"d d~ > z z g g g g g g g","process_key":"dd_zz_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n9-dd-zzz-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":9,"process":"d d~ > z z z g g g g g g","process_key":"dd_zzz_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n9-dd-zzz-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":9,"process":"d d~ > z z z g g g g g g","process_key":"dd_zzz_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n9-gg-gluons-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":9,"process":"g g > g g g g g g g g g","process_key":"gg_gluons","variant":null,"workload":"all-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n9-gg-gluons-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":9,"process":"g g > g g g g g g g g g","process_key":"gg_gluons","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n9-gg-tt-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":9,"process":"g g > t t~ g g g g g g g","process_key":"gg_tt_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n9-gg-tt-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":9,"process":"g g > t t~ g g g g g g g","process_key":"gg_tt_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n9-ud-epve-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":9,"process":"u d~ > e+ ve g g g g g g g","process_key":"ud_epve_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n9-ud-epve-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":9,"process":"u d~ > e+ ve g g g g g g g","process_key":"ud_epve_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n9-ud-w-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":9,"process":"u d~ > w+ g g g g g g g g","process_key":"ud_w_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-compiled-builtin-sm-lc-n9-ud-w-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":9,"process":"u d~ > w+ g g g g g g g g","process_key":"ud_w_jets","variant":null,"workload":"selected-flow"}],"kind":"performance_measurements","measurement_specs":[{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"}],"report_version":"0.3.0","schema_version":5} diff --git a/pyamplicol/src/pyamplicol/_profiling_campaign/results/matrix_compiled_builtin_sm_nlc.json b/pyamplicol/src/pyamplicol/_profiling_campaign/results/matrix_compiled_builtin_sm_nlc.json new file mode 100644 index 0000000000..a7d6505199 --- /dev/null +++ b/pyamplicol/src/pyamplicol/_profiling_campaign/results/matrix_compiled_builtin_sm_nlc.json @@ -0,0 +1 @@ +{"$schema":"./report-cache.schema.json","dataset_id":"matrix_compiled_builtin_sm_nlc","entries":[{"cell_id":"matrix-compiled-builtin-sm-nlc-n1-dd-z-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":1,"process":"d d~ > z","process_key":"dd_z_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-builtin-sm-nlc-n1-ud-w-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":1,"process":"u d~ > w+","process_key":"ud_w_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-builtin-sm-nlc-n2-dd-epem-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":2,"process":"d d~ > e+ e-","process_key":"dd_epem_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-builtin-sm-nlc-n2-dd-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":2,"process":"d d~ > t t~","process_key":"dd_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-builtin-sm-nlc-n2-dd-z-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":2,"process":"d d~ > z g","process_key":"dd_z_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-builtin-sm-nlc-n2-dd-zz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":2,"process":"d d~ > z z","process_key":"dd_zz_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-builtin-sm-nlc-n2-gg-gluons-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":2,"process":"g g > g g","process_key":"gg_gluons","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-builtin-sm-nlc-n2-gg-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":2,"process":"g g > t t~","process_key":"gg_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-builtin-sm-nlc-n2-ud-epve-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":2,"process":"u d~ > e+ ve","process_key":"ud_epve_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-builtin-sm-nlc-n2-ud-w-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":2,"process":"u d~ > w+ g","process_key":"ud_w_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-builtin-sm-nlc-n3-dd-epem-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":3,"process":"d d~ > e+ e- g","process_key":"dd_epem_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-builtin-sm-nlc-n3-dd-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":3,"process":"d d~ > t t~ g","process_key":"dd_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-builtin-sm-nlc-n3-dd-z-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":3,"process":"d d~ > z g g","process_key":"dd_z_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-builtin-sm-nlc-n3-dd-zz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":3,"process":"d d~ > z z g","process_key":"dd_zz_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-builtin-sm-nlc-n3-dd-zzz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":3,"process":"d d~ > z z z","process_key":"dd_zzz_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-builtin-sm-nlc-n3-gg-gluons-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":3,"process":"g g > g g g","process_key":"gg_gluons","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-builtin-sm-nlc-n3-gg-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":3,"process":"g g > t t~ g","process_key":"gg_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-builtin-sm-nlc-n3-ud-epve-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":3,"process":"u d~ > e+ ve g","process_key":"ud_epve_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-builtin-sm-nlc-n3-ud-w-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":3,"process":"u d~ > w+ g g","process_key":"ud_w_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-builtin-sm-nlc-n4-dd-3q-identical-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":4,"process":"d d~ > u u~ u u~","process_key":"dd_3q_identical_lines","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-builtin-sm-nlc-n4-dd-3q-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":4,"process":"d d~ > u u~ s s~","process_key":"dd_3q_lines","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-builtin-sm-nlc-n4-dd-4l-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":4,"process":"d d~ > e+ e- e+ e-","process_key":"dd_4l_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-builtin-sm-nlc-n4-dd-epem-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":4,"process":"d d~ > e+ e- g g","process_key":"dd_epem_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-builtin-sm-nlc-n4-dd-epemzh-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":4,"process":"d d~ > e+ e- z h","process_key":"dd_epemzh_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-builtin-sm-nlc-n4-dd-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":4,"process":"d d~ > t t~ g g","process_key":"dd_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-builtin-sm-nlc-n4-dd-ttzh-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":4,"process":"d d~ > t t~ z h","process_key":"dd_ttzh_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-builtin-sm-nlc-n4-dd-z-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":4,"process":"d d~ > z g g g","process_key":"dd_z_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-builtin-sm-nlc-n4-dd-zz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":4,"process":"d d~ > z z g g","process_key":"dd_zz_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-builtin-sm-nlc-n4-dd-zzz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":4,"process":"d d~ > z z z g","process_key":"dd_zzz_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-builtin-sm-nlc-n4-gg-gluons-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":4,"process":"g g > g g g g","process_key":"gg_gluons","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-builtin-sm-nlc-n4-gg-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":4,"process":"g g > t t~ g g","process_key":"gg_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-builtin-sm-nlc-n4-ud-epve-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":4,"process":"u d~ > e+ ve g g","process_key":"ud_epve_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-builtin-sm-nlc-n4-ud-w-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":4,"process":"u d~ > w+ g g g","process_key":"ud_w_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-builtin-sm-nlc-n5-dd-3q-identical-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":5,"process":"d d~ > u u~ u u~ g","process_key":"dd_3q_identical_lines","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-builtin-sm-nlc-n5-dd-3q-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":5,"process":"d d~ > u u~ s s~ g","process_key":"dd_3q_lines","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-builtin-sm-nlc-n5-dd-4l-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":5,"process":"d d~ > e+ e- e+ e- g","process_key":"dd_4l_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-builtin-sm-nlc-n5-dd-epem-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":5,"process":"d d~ > e+ e- g g g","process_key":"dd_epem_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-builtin-sm-nlc-n5-dd-epemzh-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":5,"process":"d d~ > e+ e- z h g","process_key":"dd_epemzh_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-builtin-sm-nlc-n5-dd-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":5,"process":"d d~ > t t~ g g g","process_key":"dd_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-builtin-sm-nlc-n5-dd-ttzh-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":5,"process":"d d~ > t t~ z h g","process_key":"dd_ttzh_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-builtin-sm-nlc-n5-dd-z-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":5,"process":"d d~ > z g g g g","process_key":"dd_z_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-builtin-sm-nlc-n5-dd-zz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":5,"process":"d d~ > z z g g g","process_key":"dd_zz_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-builtin-sm-nlc-n5-dd-zzz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":5,"process":"d d~ > z z z g g","process_key":"dd_zzz_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-builtin-sm-nlc-n5-gg-gluons-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":5,"process":"g g > g g g g g","process_key":"gg_gluons","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-builtin-sm-nlc-n5-gg-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":5,"process":"g g > t t~ g g g","process_key":"gg_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-builtin-sm-nlc-n5-ud-epve-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":5,"process":"u d~ > e+ ve g g g","process_key":"ud_epve_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-builtin-sm-nlc-n5-ud-w-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":5,"process":"u d~ > w+ g g g g","process_key":"ud_w_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-builtin-sm-nlc-n6-dd-3q-identical-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":6,"process":"d d~ > u u~ u u~ g g","process_key":"dd_3q_identical_lines","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-builtin-sm-nlc-n6-dd-3q-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":6,"process":"d d~ > u u~ s s~ g g","process_key":"dd_3q_lines","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-builtin-sm-nlc-n6-dd-4q-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":6,"process":"d d~ > u u~ s s~ c c~","process_key":"dd_4q_lines","variant":null,"workload":"contracted"}],"kind":"performance_measurements","measurement_specs":[{"accuracy":"nlc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"}],"report_version":"0.3.0","schema_version":5} diff --git a/pyamplicol/src/pyamplicol/_profiling_campaign/results/matrix_compiled_ufo_sm_full.json b/pyamplicol/src/pyamplicol/_profiling_campaign/results/matrix_compiled_ufo_sm_full.json new file mode 100644 index 0000000000..28ac310734 --- /dev/null +++ b/pyamplicol/src/pyamplicol/_profiling_campaign/results/matrix_compiled_ufo_sm_full.json @@ -0,0 +1 @@ +{"$schema":"./report-cache.schema.json","dataset_id":"matrix_compiled_ufo_sm_full","entries":[{"cell_id":"matrix-compiled-ufo-sm-full-n1-dd-z-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"ufo_sm"},"n_final":1,"process":"d d~ > z","process_key":"dd_z_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-ufo-sm-full-n1-ud-w-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"ufo_sm"},"n_final":1,"process":"u d~ > w+","process_key":"ud_w_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-ufo-sm-full-n2-dd-epem-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"ufo_sm"},"n_final":2,"process":"d d~ > e+ e-","process_key":"dd_epem_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-ufo-sm-full-n2-dd-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"ufo_sm"},"n_final":2,"process":"d d~ > t t~","process_key":"dd_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-ufo-sm-full-n2-dd-z-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"ufo_sm"},"n_final":2,"process":"d d~ > z g","process_key":"dd_z_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-ufo-sm-full-n2-dd-zz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"ufo_sm"},"n_final":2,"process":"d d~ > z z","process_key":"dd_zz_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-ufo-sm-full-n2-gg-gluons-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"ufo_sm"},"n_final":2,"process":"g g > g g","process_key":"gg_gluons","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-ufo-sm-full-n2-gg-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"ufo_sm"},"n_final":2,"process":"g g > t t~","process_key":"gg_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-ufo-sm-full-n2-ud-epve-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"ufo_sm"},"n_final":2,"process":"u d~ > e+ ve","process_key":"ud_epve_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-ufo-sm-full-n2-ud-w-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"ufo_sm"},"n_final":2,"process":"u d~ > w+ g","process_key":"ud_w_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-ufo-sm-full-n3-dd-epem-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"ufo_sm"},"n_final":3,"process":"d d~ > e+ e- g","process_key":"dd_epem_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-ufo-sm-full-n3-dd-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"ufo_sm"},"n_final":3,"process":"d d~ > t t~ g","process_key":"dd_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-ufo-sm-full-n3-dd-z-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"ufo_sm"},"n_final":3,"process":"d d~ > z g g","process_key":"dd_z_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-ufo-sm-full-n3-dd-zz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"ufo_sm"},"n_final":3,"process":"d d~ > z z g","process_key":"dd_zz_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-ufo-sm-full-n3-dd-zzz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"ufo_sm"},"n_final":3,"process":"d d~ > z z z","process_key":"dd_zzz_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-ufo-sm-full-n3-gg-gluons-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"ufo_sm"},"n_final":3,"process":"g g > g g g","process_key":"gg_gluons","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-ufo-sm-full-n3-gg-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"ufo_sm"},"n_final":3,"process":"g g > t t~ g","process_key":"gg_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-ufo-sm-full-n3-ud-epve-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"ufo_sm"},"n_final":3,"process":"u d~ > e+ ve g","process_key":"ud_epve_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-ufo-sm-full-n3-ud-w-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"ufo_sm"},"n_final":3,"process":"u d~ > w+ g g","process_key":"ud_w_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-ufo-sm-full-n4-dd-3q-identical-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"ufo_sm"},"n_final":4,"process":"d d~ > u u~ u u~","process_key":"dd_3q_identical_lines","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-ufo-sm-full-n4-dd-3q-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"ufo_sm"},"n_final":4,"process":"d d~ > u u~ s s~","process_key":"dd_3q_lines","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-ufo-sm-full-n4-dd-4l-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"ufo_sm"},"n_final":4,"process":"d d~ > e+ e- e+ e-","process_key":"dd_4l_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-ufo-sm-full-n4-dd-epem-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"ufo_sm"},"n_final":4,"process":"d d~ > e+ e- g g","process_key":"dd_epem_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-ufo-sm-full-n4-dd-epemzh-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"ufo_sm"},"n_final":4,"process":"d d~ > e+ e- z h","process_key":"dd_epemzh_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-ufo-sm-full-n4-dd-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"ufo_sm"},"n_final":4,"process":"d d~ > t t~ g g","process_key":"dd_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-ufo-sm-full-n4-dd-ttzh-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"ufo_sm"},"n_final":4,"process":"d d~ > t t~ z h","process_key":"dd_ttzh_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-ufo-sm-full-n4-dd-z-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"ufo_sm"},"n_final":4,"process":"d d~ > z g g g","process_key":"dd_z_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-ufo-sm-full-n4-dd-zz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"ufo_sm"},"n_final":4,"process":"d d~ > z z g g","process_key":"dd_zz_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-ufo-sm-full-n4-dd-zzz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"ufo_sm"},"n_final":4,"process":"d d~ > z z z g","process_key":"dd_zzz_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-ufo-sm-full-n4-gg-gluons-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"ufo_sm"},"n_final":4,"process":"g g > g g g g","process_key":"gg_gluons","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-ufo-sm-full-n4-gg-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"ufo_sm"},"n_final":4,"process":"g g > t t~ g g","process_key":"gg_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-ufo-sm-full-n4-ud-epve-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"ufo_sm"},"n_final":4,"process":"u d~ > e+ ve g g","process_key":"ud_epve_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-ufo-sm-full-n4-ud-w-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"ufo_sm"},"n_final":4,"process":"u d~ > w+ g g g","process_key":"ud_w_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-ufo-sm-full-n5-dd-3q-identical-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"ufo_sm"},"n_final":5,"process":"d d~ > u u~ u u~ g","process_key":"dd_3q_identical_lines","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-ufo-sm-full-n5-dd-3q-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"ufo_sm"},"n_final":5,"process":"d d~ > u u~ s s~ g","process_key":"dd_3q_lines","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-ufo-sm-full-n5-dd-4l-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"ufo_sm"},"n_final":5,"process":"d d~ > e+ e- e+ e- g","process_key":"dd_4l_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-ufo-sm-full-n5-dd-epem-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"ufo_sm"},"n_final":5,"process":"d d~ > e+ e- g g g","process_key":"dd_epem_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-ufo-sm-full-n5-dd-epemzh-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"ufo_sm"},"n_final":5,"process":"d d~ > e+ e- z h g","process_key":"dd_epemzh_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-ufo-sm-full-n5-dd-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"ufo_sm"},"n_final":5,"process":"d d~ > t t~ g g g","process_key":"dd_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-ufo-sm-full-n5-dd-ttzh-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"ufo_sm"},"n_final":5,"process":"d d~ > t t~ z h g","process_key":"dd_ttzh_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-ufo-sm-full-n5-dd-z-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"ufo_sm"},"n_final":5,"process":"d d~ > z g g g g","process_key":"dd_z_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-ufo-sm-full-n5-dd-zz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"ufo_sm"},"n_final":5,"process":"d d~ > z z g g g","process_key":"dd_zz_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-ufo-sm-full-n5-dd-zzz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"ufo_sm"},"n_final":5,"process":"d d~ > z z z g g","process_key":"dd_zzz_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-ufo-sm-full-n5-gg-gluons-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"ufo_sm"},"n_final":5,"process":"g g > g g g g g","process_key":"gg_gluons","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-ufo-sm-full-n5-gg-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"ufo_sm"},"n_final":5,"process":"g g > t t~ g g g","process_key":"gg_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-ufo-sm-full-n5-ud-epve-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"ufo_sm"},"n_final":5,"process":"u d~ > e+ ve g g g","process_key":"ud_epve_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-ufo-sm-full-n5-ud-w-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"ufo_sm"},"n_final":5,"process":"u d~ > w+ g g g g","process_key":"ud_w_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-ufo-sm-full-n6-dd-3q-identical-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"ufo_sm"},"n_final":6,"process":"d d~ > u u~ u u~ g g","process_key":"dd_3q_identical_lines","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-ufo-sm-full-n6-dd-3q-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"ufo_sm"},"n_final":6,"process":"d d~ > u u~ s s~ g g","process_key":"dd_3q_lines","variant":null,"workload":"contracted"},{"cell_id":"matrix-compiled-ufo-sm-full-n6-dd-4q-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"ufo_sm"},"n_final":6,"process":"d d~ > u u~ s s~ c c~","process_key":"dd_4q_lines","variant":null,"workload":"contracted"}],"kind":"performance_measurements","measurement_specs":[{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"ufo_sm"}],"report_version":"0.3.0","schema_version":5} diff --git a/pyamplicol/src/pyamplicol/_profiling_campaign/results/matrix_eager_builtin_sm_full.json b/pyamplicol/src/pyamplicol/_profiling_campaign/results/matrix_eager_builtin_sm_full.json new file mode 100644 index 0000000000..c0482dc1b3 --- /dev/null +++ b/pyamplicol/src/pyamplicol/_profiling_campaign/results/matrix_eager_builtin_sm_full.json @@ -0,0 +1 @@ +{"$schema":"./report-cache.schema.json","dataset_id":"matrix_eager_builtin_sm_full","entries":[{"cell_id":"matrix-eager-builtin-sm-full-n1-dd-z-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":1,"process":"d d~ > z","process_key":"dd_z_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-builtin-sm-full-n1-ud-w-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":1,"process":"u d~ > w+","process_key":"ud_w_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-builtin-sm-full-n2-dd-epem-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":2,"process":"d d~ > e+ e-","process_key":"dd_epem_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-builtin-sm-full-n2-dd-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":2,"process":"d d~ > t t~","process_key":"dd_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-builtin-sm-full-n2-dd-z-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":2,"process":"d d~ > z g","process_key":"dd_z_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-builtin-sm-full-n2-dd-zz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":2,"process":"d d~ > z z","process_key":"dd_zz_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-builtin-sm-full-n2-gg-gluons-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":2,"process":"g g > g g","process_key":"gg_gluons","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-builtin-sm-full-n2-gg-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":2,"process":"g g > t t~","process_key":"gg_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-builtin-sm-full-n2-ud-epve-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":2,"process":"u d~ > e+ ve","process_key":"ud_epve_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-builtin-sm-full-n2-ud-w-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":2,"process":"u d~ > w+ g","process_key":"ud_w_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-builtin-sm-full-n3-dd-epem-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"d d~ > e+ e- g","process_key":"dd_epem_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-builtin-sm-full-n3-dd-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"d d~ > t t~ g","process_key":"dd_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-builtin-sm-full-n3-dd-z-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"d d~ > z g g","process_key":"dd_z_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-builtin-sm-full-n3-dd-zz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"d d~ > z z g","process_key":"dd_zz_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-builtin-sm-full-n3-dd-zzz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"d d~ > z z z","process_key":"dd_zzz_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-builtin-sm-full-n3-gg-gluons-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"g g > g g g","process_key":"gg_gluons","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-builtin-sm-full-n3-gg-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"g g > t t~ g","process_key":"gg_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-builtin-sm-full-n3-ud-epve-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"u d~ > e+ ve g","process_key":"ud_epve_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-builtin-sm-full-n3-ud-w-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"u d~ > w+ g g","process_key":"ud_w_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-builtin-sm-full-n4-dd-3q-identical-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > u u~ u u~","process_key":"dd_3q_identical_lines","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-builtin-sm-full-n4-dd-3q-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > u u~ s s~","process_key":"dd_3q_lines","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-builtin-sm-full-n4-dd-4l-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > e+ e- e+ e-","process_key":"dd_4l_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-builtin-sm-full-n4-dd-epem-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > e+ e- g g","process_key":"dd_epem_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-builtin-sm-full-n4-dd-epemzh-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > e+ e- z h","process_key":"dd_epemzh_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-builtin-sm-full-n4-dd-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > t t~ g g","process_key":"dd_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-builtin-sm-full-n4-dd-ttzh-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > t t~ z h","process_key":"dd_ttzh_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-builtin-sm-full-n4-dd-z-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > z g g g","process_key":"dd_z_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-builtin-sm-full-n4-dd-zz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > z z g g","process_key":"dd_zz_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-builtin-sm-full-n4-dd-zzz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > z z z g","process_key":"dd_zzz_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-builtin-sm-full-n4-gg-gluons-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"g g > g g g g","process_key":"gg_gluons","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-builtin-sm-full-n4-gg-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"g g > t t~ g g","process_key":"gg_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-builtin-sm-full-n4-ud-epve-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"u d~ > e+ ve g g","process_key":"ud_epve_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-builtin-sm-full-n4-ud-w-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"u d~ > w+ g g g","process_key":"ud_w_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-builtin-sm-full-n5-dd-3q-identical-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"d d~ > u u~ u u~ g","process_key":"dd_3q_identical_lines","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-builtin-sm-full-n5-dd-3q-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"d d~ > u u~ s s~ g","process_key":"dd_3q_lines","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-builtin-sm-full-n5-dd-4l-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"d d~ > e+ e- e+ e- g","process_key":"dd_4l_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-builtin-sm-full-n5-dd-epem-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"d d~ > e+ e- g g g","process_key":"dd_epem_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-builtin-sm-full-n5-dd-epemzh-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"d d~ > e+ e- z h g","process_key":"dd_epemzh_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-builtin-sm-full-n5-dd-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"d d~ > t t~ g g g","process_key":"dd_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-builtin-sm-full-n5-dd-ttzh-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"d d~ > t t~ z h g","process_key":"dd_ttzh_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-builtin-sm-full-n5-dd-z-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"d d~ > z g g g g","process_key":"dd_z_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-builtin-sm-full-n5-dd-zz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"d d~ > z z g g g","process_key":"dd_zz_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-builtin-sm-full-n5-dd-zzz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"d d~ > z z z g g","process_key":"dd_zzz_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-builtin-sm-full-n5-gg-gluons-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"g g > g g g g g","process_key":"gg_gluons","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-builtin-sm-full-n5-gg-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"g g > t t~ g g g","process_key":"gg_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-builtin-sm-full-n5-ud-epve-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"u d~ > e+ ve g g g","process_key":"ud_epve_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-builtin-sm-full-n5-ud-w-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"u d~ > w+ g g g g","process_key":"ud_w_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-builtin-sm-full-n6-dd-3q-identical-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":6,"process":"d d~ > u u~ u u~ g g","process_key":"dd_3q_identical_lines","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-builtin-sm-full-n6-dd-3q-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":6,"process":"d d~ > u u~ s s~ g g","process_key":"dd_3q_lines","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-builtin-sm-full-n6-dd-4q-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":6,"process":"d d~ > u u~ s s~ c c~","process_key":"dd_4q_lines","variant":null,"workload":"contracted"}],"kind":"performance_measurements","measurement_specs":[{"accuracy":"full","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"}],"report_version":"0.3.0","schema_version":5} diff --git a/pyamplicol/src/pyamplicol/_profiling_campaign/results/matrix_eager_builtin_sm_lc.json b/pyamplicol/src/pyamplicol/_profiling_campaign/results/matrix_eager_builtin_sm_lc.json new file mode 100644 index 0000000000..300584c62f --- /dev/null +++ b/pyamplicol/src/pyamplicol/_profiling_campaign/results/matrix_eager_builtin_sm_lc.json @@ -0,0 +1 @@ +{"$schema":"./report-cache.schema.json","dataset_id":"matrix_eager_builtin_sm_lc","entries":[{"cell_id":"matrix-eager-builtin-sm-lc-n1-dd-z-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":1,"process":"d d~ > z","process_key":"dd_z_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n1-dd-z-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":1,"process":"d d~ > z","process_key":"dd_z_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n1-ud-w-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":1,"process":"u d~ > w+","process_key":"ud_w_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n1-ud-w-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":1,"process":"u d~ > w+","process_key":"ud_w_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n2-dd-epem-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":2,"process":"d d~ > e+ e-","process_key":"dd_epem_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n2-dd-epem-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":2,"process":"d d~ > e+ e-","process_key":"dd_epem_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n2-dd-tt-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":2,"process":"d d~ > t t~","process_key":"dd_tt_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n2-dd-tt-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":2,"process":"d d~ > t t~","process_key":"dd_tt_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n2-dd-z-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":2,"process":"d d~ > z g","process_key":"dd_z_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n2-dd-z-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":2,"process":"d d~ > z g","process_key":"dd_z_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n2-dd-zz-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":2,"process":"d d~ > z z","process_key":"dd_zz_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n2-dd-zz-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":2,"process":"d d~ > z z","process_key":"dd_zz_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n2-gg-gluons-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":2,"process":"g g > g g","process_key":"gg_gluons","variant":null,"workload":"all-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n2-gg-gluons-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":2,"process":"g g > g g","process_key":"gg_gluons","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n2-gg-tt-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":2,"process":"g g > t t~","process_key":"gg_tt_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n2-gg-tt-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":2,"process":"g g > t t~","process_key":"gg_tt_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n2-ud-epve-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":2,"process":"u d~ > e+ ve","process_key":"ud_epve_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n2-ud-epve-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":2,"process":"u d~ > e+ ve","process_key":"ud_epve_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n2-ud-w-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":2,"process":"u d~ > w+ g","process_key":"ud_w_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n2-ud-w-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":2,"process":"u d~ > w+ g","process_key":"ud_w_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n3-dd-epem-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"d d~ > e+ e- g","process_key":"dd_epem_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n3-dd-epem-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"d d~ > e+ e- g","process_key":"dd_epem_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n3-dd-tt-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"d d~ > t t~ g","process_key":"dd_tt_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n3-dd-tt-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"d d~ > t t~ g","process_key":"dd_tt_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n3-dd-z-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"d d~ > z g g","process_key":"dd_z_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n3-dd-z-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"d d~ > z g g","process_key":"dd_z_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n3-dd-zz-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"d d~ > z z g","process_key":"dd_zz_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n3-dd-zz-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"d d~ > z z g","process_key":"dd_zz_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n3-dd-zzz-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"d d~ > z z z","process_key":"dd_zzz_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n3-dd-zzz-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"d d~ > z z z","process_key":"dd_zzz_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n3-gg-gluons-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"g g > g g g","process_key":"gg_gluons","variant":null,"workload":"all-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n3-gg-gluons-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"g g > g g g","process_key":"gg_gluons","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n3-gg-tt-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"g g > t t~ g","process_key":"gg_tt_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n3-gg-tt-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"g g > t t~ g","process_key":"gg_tt_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n3-ud-epve-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"u d~ > e+ ve g","process_key":"ud_epve_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n3-ud-epve-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"u d~ > e+ ve g","process_key":"ud_epve_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n3-ud-w-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"u d~ > w+ g g","process_key":"ud_w_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n3-ud-w-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"u d~ > w+ g g","process_key":"ud_w_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n4-dd-3q-identical-lines-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > u u~ u u~","process_key":"dd_3q_identical_lines","variant":null,"workload":"all-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n4-dd-3q-identical-lines-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > u u~ u u~","process_key":"dd_3q_identical_lines","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n4-dd-3q-lines-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > u u~ s s~","process_key":"dd_3q_lines","variant":null,"workload":"all-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n4-dd-3q-lines-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > u u~ s s~","process_key":"dd_3q_lines","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n4-dd-4l-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > e+ e- e+ e-","process_key":"dd_4l_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n4-dd-4l-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > e+ e- e+ e-","process_key":"dd_4l_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n4-dd-epem-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > e+ e- g g","process_key":"dd_epem_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n4-dd-epem-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > e+ e- g g","process_key":"dd_epem_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n4-dd-epemzh-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > e+ e- z h","process_key":"dd_epemzh_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n4-dd-epemzh-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > e+ e- z h","process_key":"dd_epemzh_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n4-dd-tt-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > t t~ g g","process_key":"dd_tt_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n4-dd-tt-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > t t~ g g","process_key":"dd_tt_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n4-dd-ttzh-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > t t~ z h","process_key":"dd_ttzh_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n4-dd-ttzh-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > t t~ z h","process_key":"dd_ttzh_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n4-dd-z-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > z g g g","process_key":"dd_z_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n4-dd-z-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > z g g g","process_key":"dd_z_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n4-dd-zz-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > z z g g","process_key":"dd_zz_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n4-dd-zz-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > z z g g","process_key":"dd_zz_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n4-dd-zzz-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > z z z g","process_key":"dd_zzz_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n4-dd-zzz-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > z z z g","process_key":"dd_zzz_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n4-gg-gluons-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"g g > g g g g","process_key":"gg_gluons","variant":null,"workload":"all-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n4-gg-gluons-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"g g > g g g g","process_key":"gg_gluons","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n4-gg-tt-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"g g > t t~ g g","process_key":"gg_tt_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n4-gg-tt-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"g g > t t~ g g","process_key":"gg_tt_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n4-ud-epve-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"u d~ > e+ ve g g","process_key":"ud_epve_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n4-ud-epve-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"u d~ > e+ ve g g","process_key":"ud_epve_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n4-ud-w-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"u d~ > w+ g g g","process_key":"ud_w_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n4-ud-w-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"u d~ > w+ g g g","process_key":"ud_w_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n5-dd-3q-identical-lines-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"d d~ > u u~ u u~ g","process_key":"dd_3q_identical_lines","variant":null,"workload":"all-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n5-dd-3q-identical-lines-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"d d~ > u u~ u u~ g","process_key":"dd_3q_identical_lines","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n5-dd-3q-lines-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"d d~ > u u~ s s~ g","process_key":"dd_3q_lines","variant":null,"workload":"all-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n5-dd-3q-lines-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"d d~ > u u~ s s~ g","process_key":"dd_3q_lines","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n5-dd-4l-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"d d~ > e+ e- e+ e- g","process_key":"dd_4l_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n5-dd-4l-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"d d~ > e+ e- e+ e- g","process_key":"dd_4l_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n5-dd-epem-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"d d~ > e+ e- g g g","process_key":"dd_epem_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n5-dd-epem-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"d d~ > e+ e- g g g","process_key":"dd_epem_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n5-dd-epemzh-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"d d~ > e+ e- z h g","process_key":"dd_epemzh_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n5-dd-epemzh-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"d d~ > e+ e- z h g","process_key":"dd_epemzh_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n5-dd-tt-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"d d~ > t t~ g g g","process_key":"dd_tt_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n5-dd-tt-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"d d~ > t t~ g g g","process_key":"dd_tt_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n5-dd-ttzh-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"d d~ > t t~ z h g","process_key":"dd_ttzh_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n5-dd-ttzh-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"d d~ > t t~ z h g","process_key":"dd_ttzh_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n5-dd-z-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"d d~ > z g g g g","process_key":"dd_z_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n5-dd-z-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"d d~ > z g g g g","process_key":"dd_z_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n5-dd-zz-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"d d~ > z z g g g","process_key":"dd_zz_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n5-dd-zz-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"d d~ > z z g g g","process_key":"dd_zz_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n5-dd-zzz-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"d d~ > z z z g g","process_key":"dd_zzz_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n5-dd-zzz-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"d d~ > z z z g g","process_key":"dd_zzz_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n5-gg-gluons-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"g g > g g g g g","process_key":"gg_gluons","variant":null,"workload":"all-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n5-gg-gluons-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"g g > g g g g g","process_key":"gg_gluons","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n5-gg-tt-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"g g > t t~ g g g","process_key":"gg_tt_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n5-gg-tt-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"g g > t t~ g g g","process_key":"gg_tt_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n5-ud-epve-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"u d~ > e+ ve g g g","process_key":"ud_epve_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n5-ud-epve-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"u d~ > e+ ve g g g","process_key":"ud_epve_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n5-ud-w-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"u d~ > w+ g g g g","process_key":"ud_w_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n5-ud-w-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"u d~ > w+ g g g g","process_key":"ud_w_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n6-dd-3q-identical-lines-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":6,"process":"d d~ > u u~ u u~ g g","process_key":"dd_3q_identical_lines","variant":null,"workload":"all-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n6-dd-3q-identical-lines-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":6,"process":"d d~ > u u~ u u~ g g","process_key":"dd_3q_identical_lines","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n6-dd-3q-lines-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":6,"process":"d d~ > u u~ s s~ g g","process_key":"dd_3q_lines","variant":null,"workload":"all-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n6-dd-3q-lines-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":6,"process":"d d~ > u u~ s s~ g g","process_key":"dd_3q_lines","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n6-dd-4l-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":6,"process":"d d~ > e+ e- e+ e- g g","process_key":"dd_4l_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n6-dd-4l-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":6,"process":"d d~ > e+ e- e+ e- g g","process_key":"dd_4l_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n6-dd-4q-lines-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":6,"process":"d d~ > u u~ s s~ c c~","process_key":"dd_4q_lines","variant":null,"workload":"all-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n6-dd-4q-lines-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":6,"process":"d d~ > u u~ s s~ c c~","process_key":"dd_4q_lines","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n6-dd-epem-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":6,"process":"d d~ > e+ e- g g g g","process_key":"dd_epem_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n6-dd-epem-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":6,"process":"d d~ > e+ e- g g g g","process_key":"dd_epem_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n6-dd-epemzh-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":6,"process":"d d~ > e+ e- z h g g","process_key":"dd_epemzh_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n6-dd-epemzh-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":6,"process":"d d~ > e+ e- z h g g","process_key":"dd_epemzh_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n6-dd-tt-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":6,"process":"d d~ > t t~ g g g g","process_key":"dd_tt_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n6-dd-tt-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":6,"process":"d d~ > t t~ g g g g","process_key":"dd_tt_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n6-dd-ttzh-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":6,"process":"d d~ > t t~ z h g g","process_key":"dd_ttzh_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n6-dd-ttzh-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":6,"process":"d d~ > t t~ z h g g","process_key":"dd_ttzh_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n6-dd-z-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":6,"process":"d d~ > z g g g g g","process_key":"dd_z_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n6-dd-z-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":6,"process":"d d~ > z g g g g g","process_key":"dd_z_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n6-dd-zz-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":6,"process":"d d~ > z z g g g g","process_key":"dd_zz_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n6-dd-zz-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":6,"process":"d d~ > z z g g g g","process_key":"dd_zz_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n6-dd-zzz-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":6,"process":"d d~ > z z z g g g","process_key":"dd_zzz_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n6-dd-zzz-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":6,"process":"d d~ > z z z g g g","process_key":"dd_zzz_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n6-gg-gluons-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":6,"process":"g g > g g g g g g","process_key":"gg_gluons","variant":null,"workload":"all-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n6-gg-gluons-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":6,"process":"g g > g g g g g g","process_key":"gg_gluons","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n6-gg-tt-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":6,"process":"g g > t t~ g g g g","process_key":"gg_tt_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n6-gg-tt-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":6,"process":"g g > t t~ g g g g","process_key":"gg_tt_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n6-ud-epve-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":6,"process":"u d~ > e+ ve g g g g","process_key":"ud_epve_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n6-ud-epve-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":6,"process":"u d~ > e+ ve g g g g","process_key":"ud_epve_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n6-ud-w-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":6,"process":"u d~ > w+ g g g g g","process_key":"ud_w_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n6-ud-w-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":6,"process":"u d~ > w+ g g g g g","process_key":"ud_w_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n7-dd-3q-identical-lines-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":7,"process":"d d~ > u u~ u u~ g g g","process_key":"dd_3q_identical_lines","variant":null,"workload":"all-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n7-dd-3q-identical-lines-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":7,"process":"d d~ > u u~ u u~ g g g","process_key":"dd_3q_identical_lines","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n7-dd-3q-lines-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":7,"process":"d d~ > u u~ s s~ g g g","process_key":"dd_3q_lines","variant":null,"workload":"all-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n7-dd-3q-lines-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":7,"process":"d d~ > u u~ s s~ g g g","process_key":"dd_3q_lines","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n7-dd-4l-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":7,"process":"d d~ > e+ e- e+ e- g g g","process_key":"dd_4l_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n7-dd-4l-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":7,"process":"d d~ > e+ e- e+ e- g g g","process_key":"dd_4l_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n7-dd-4q-lines-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":7,"process":"d d~ > u u~ s s~ c c~ g","process_key":"dd_4q_lines","variant":null,"workload":"all-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n7-dd-4q-lines-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":7,"process":"d d~ > u u~ s s~ c c~ g","process_key":"dd_4q_lines","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n7-dd-epem-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":7,"process":"d d~ > e+ e- g g g g g","process_key":"dd_epem_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n7-dd-epem-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":7,"process":"d d~ > e+ e- g g g g g","process_key":"dd_epem_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n7-dd-epemzh-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":7,"process":"d d~ > e+ e- z h g g g","process_key":"dd_epemzh_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n7-dd-epemzh-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":7,"process":"d d~ > e+ e- z h g g g","process_key":"dd_epemzh_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n7-dd-tt-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":7,"process":"d d~ > t t~ g g g g g","process_key":"dd_tt_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n7-dd-tt-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":7,"process":"d d~ > t t~ g g g g g","process_key":"dd_tt_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n7-dd-ttzh-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":7,"process":"d d~ > t t~ z h g g g","process_key":"dd_ttzh_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n7-dd-ttzh-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":7,"process":"d d~ > t t~ z h g g g","process_key":"dd_ttzh_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n7-dd-z-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":7,"process":"d d~ > z g g g g g g","process_key":"dd_z_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n7-dd-z-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":7,"process":"d d~ > z g g g g g g","process_key":"dd_z_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n7-dd-zz-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":7,"process":"d d~ > z z g g g g g","process_key":"dd_zz_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n7-dd-zz-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":7,"process":"d d~ > z z g g g g g","process_key":"dd_zz_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n7-dd-zzz-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":7,"process":"d d~ > z z z g g g g","process_key":"dd_zzz_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n7-dd-zzz-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":7,"process":"d d~ > z z z g g g g","process_key":"dd_zzz_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n7-gg-gluons-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":7,"process":"g g > g g g g g g g","process_key":"gg_gluons","variant":null,"workload":"all-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n7-gg-gluons-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":7,"process":"g g > g g g g g g g","process_key":"gg_gluons","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n7-gg-tt-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":7,"process":"g g > t t~ g g g g g","process_key":"gg_tt_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n7-gg-tt-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":7,"process":"g g > t t~ g g g g g","process_key":"gg_tt_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n7-ud-epve-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":7,"process":"u d~ > e+ ve g g g g g","process_key":"ud_epve_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n7-ud-epve-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":7,"process":"u d~ > e+ ve g g g g g","process_key":"ud_epve_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n7-ud-w-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":7,"process":"u d~ > w+ g g g g g g","process_key":"ud_w_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n7-ud-w-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":7,"process":"u d~ > w+ g g g g g g","process_key":"ud_w_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n8-dd-3q-identical-lines-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":8,"process":"d d~ > u u~ u u~ g g g g","process_key":"dd_3q_identical_lines","variant":null,"workload":"all-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n8-dd-3q-identical-lines-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":8,"process":"d d~ > u u~ u u~ g g g g","process_key":"dd_3q_identical_lines","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n8-dd-3q-lines-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":8,"process":"d d~ > u u~ s s~ g g g g","process_key":"dd_3q_lines","variant":null,"workload":"all-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n8-dd-3q-lines-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":8,"process":"d d~ > u u~ s s~ g g g g","process_key":"dd_3q_lines","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n8-dd-4l-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":8,"process":"d d~ > e+ e- e+ e- g g g g","process_key":"dd_4l_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n8-dd-4l-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":8,"process":"d d~ > e+ e- e+ e- g g g g","process_key":"dd_4l_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n8-dd-4q-lines-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":8,"process":"d d~ > u u~ s s~ c c~ g g","process_key":"dd_4q_lines","variant":null,"workload":"all-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n8-dd-4q-lines-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":8,"process":"d d~ > u u~ s s~ c c~ g g","process_key":"dd_4q_lines","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n8-dd-epem-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":8,"process":"d d~ > e+ e- g g g g g g","process_key":"dd_epem_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n8-dd-epem-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":8,"process":"d d~ > e+ e- g g g g g g","process_key":"dd_epem_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n8-dd-epemzh-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":8,"process":"d d~ > e+ e- z h g g g g","process_key":"dd_epemzh_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n8-dd-epemzh-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":8,"process":"d d~ > e+ e- z h g g g g","process_key":"dd_epemzh_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n8-dd-tt-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":8,"process":"d d~ > t t~ g g g g g g","process_key":"dd_tt_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n8-dd-tt-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":8,"process":"d d~ > t t~ g g g g g g","process_key":"dd_tt_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n8-dd-ttzh-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":8,"process":"d d~ > t t~ z h g g g g","process_key":"dd_ttzh_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n8-dd-ttzh-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":8,"process":"d d~ > t t~ z h g g g g","process_key":"dd_ttzh_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n8-dd-z-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":8,"process":"d d~ > z g g g g g g g","process_key":"dd_z_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n8-dd-z-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":8,"process":"d d~ > z g g g g g g g","process_key":"dd_z_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n8-dd-zz-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":8,"process":"d d~ > z z g g g g g g","process_key":"dd_zz_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n8-dd-zz-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":8,"process":"d d~ > z z g g g g g g","process_key":"dd_zz_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n8-dd-zzz-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":8,"process":"d d~ > z z z g g g g g","process_key":"dd_zzz_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n8-dd-zzz-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":8,"process":"d d~ > z z z g g g g g","process_key":"dd_zzz_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n8-gg-gluons-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":8,"process":"g g > g g g g g g g g","process_key":"gg_gluons","variant":null,"workload":"all-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n8-gg-gluons-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":8,"process":"g g > g g g g g g g g","process_key":"gg_gluons","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n8-gg-tt-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":8,"process":"g g > t t~ g g g g g g","process_key":"gg_tt_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n8-gg-tt-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":8,"process":"g g > t t~ g g g g g g","process_key":"gg_tt_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n8-ud-epve-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":8,"process":"u d~ > e+ ve g g g g g g","process_key":"ud_epve_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n8-ud-epve-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":8,"process":"u d~ > e+ ve g g g g g g","process_key":"ud_epve_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n8-ud-w-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":8,"process":"u d~ > w+ g g g g g g g","process_key":"ud_w_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n8-ud-w-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":8,"process":"u d~ > w+ g g g g g g g","process_key":"ud_w_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n9-dd-3q-identical-lines-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":9,"process":"d d~ > u u~ u u~ g g g g g","process_key":"dd_3q_identical_lines","variant":null,"workload":"all-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n9-dd-3q-identical-lines-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":9,"process":"d d~ > u u~ u u~ g g g g g","process_key":"dd_3q_identical_lines","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n9-dd-3q-lines-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":9,"process":"d d~ > u u~ s s~ g g g g g","process_key":"dd_3q_lines","variant":null,"workload":"all-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n9-dd-3q-lines-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":9,"process":"d d~ > u u~ s s~ g g g g g","process_key":"dd_3q_lines","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n9-dd-4l-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":9,"process":"d d~ > e+ e- e+ e- g g g g g","process_key":"dd_4l_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n9-dd-4l-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":9,"process":"d d~ > e+ e- e+ e- g g g g g","process_key":"dd_4l_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n9-dd-4q-lines-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":9,"process":"d d~ > u u~ s s~ c c~ g g g","process_key":"dd_4q_lines","variant":null,"workload":"all-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n9-dd-4q-lines-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":9,"process":"d d~ > u u~ s s~ c c~ g g g","process_key":"dd_4q_lines","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n9-dd-epem-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":9,"process":"d d~ > e+ e- g g g g g g g","process_key":"dd_epem_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n9-dd-epem-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":9,"process":"d d~ > e+ e- g g g g g g g","process_key":"dd_epem_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n9-dd-epemzh-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":9,"process":"d d~ > e+ e- z h g g g g g","process_key":"dd_epemzh_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n9-dd-epemzh-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":9,"process":"d d~ > e+ e- z h g g g g g","process_key":"dd_epemzh_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n9-dd-tt-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":9,"process":"d d~ > t t~ g g g g g g g","process_key":"dd_tt_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n9-dd-tt-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":9,"process":"d d~ > t t~ g g g g g g g","process_key":"dd_tt_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n9-dd-ttzh-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":9,"process":"d d~ > t t~ z h g g g g g","process_key":"dd_ttzh_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n9-dd-ttzh-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":9,"process":"d d~ > t t~ z h g g g g g","process_key":"dd_ttzh_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n9-dd-z-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":9,"process":"d d~ > z g g g g g g g g","process_key":"dd_z_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n9-dd-z-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":9,"process":"d d~ > z g g g g g g g g","process_key":"dd_z_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n9-dd-zz-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":9,"process":"d d~ > z z g g g g g g g","process_key":"dd_zz_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n9-dd-zz-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":9,"process":"d d~ > z z g g g g g g g","process_key":"dd_zz_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n9-dd-zzz-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":9,"process":"d d~ > z z z g g g g g g","process_key":"dd_zzz_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n9-dd-zzz-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":9,"process":"d d~ > z z z g g g g g g","process_key":"dd_zzz_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n9-gg-gluons-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":9,"process":"g g > g g g g g g g g g","process_key":"gg_gluons","variant":null,"workload":"all-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n9-gg-gluons-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":9,"process":"g g > g g g g g g g g g","process_key":"gg_gluons","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n9-gg-tt-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":9,"process":"g g > t t~ g g g g g g g","process_key":"gg_tt_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n9-gg-tt-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":9,"process":"g g > t t~ g g g g g g g","process_key":"gg_tt_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n9-ud-epve-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":9,"process":"u d~ > e+ ve g g g g g g g","process_key":"ud_epve_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n9-ud-epve-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":9,"process":"u d~ > e+ ve g g g g g g g","process_key":"ud_epve_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n9-ud-w-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":9,"process":"u d~ > w+ g g g g g g g g","process_key":"ud_w_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-eager-builtin-sm-lc-n9-ud-w-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":9,"process":"u d~ > w+ g g g g g g g g","process_key":"ud_w_jets","variant":null,"workload":"selected-flow"}],"kind":"performance_measurements","measurement_specs":[{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"}],"report_version":"0.3.0","schema_version":5} diff --git a/pyamplicol/src/pyamplicol/_profiling_campaign/results/matrix_eager_builtin_sm_nlc.json b/pyamplicol/src/pyamplicol/_profiling_campaign/results/matrix_eager_builtin_sm_nlc.json new file mode 100644 index 0000000000..6669da05d5 --- /dev/null +++ b/pyamplicol/src/pyamplicol/_profiling_campaign/results/matrix_eager_builtin_sm_nlc.json @@ -0,0 +1 @@ +{"$schema":"./report-cache.schema.json","dataset_id":"matrix_eager_builtin_sm_nlc","entries":[{"cell_id":"matrix-eager-builtin-sm-nlc-n1-dd-z-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":1,"process":"d d~ > z","process_key":"dd_z_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-builtin-sm-nlc-n1-ud-w-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":1,"process":"u d~ > w+","process_key":"ud_w_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-builtin-sm-nlc-n2-dd-epem-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":2,"process":"d d~ > e+ e-","process_key":"dd_epem_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-builtin-sm-nlc-n2-dd-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":2,"process":"d d~ > t t~","process_key":"dd_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-builtin-sm-nlc-n2-dd-z-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":2,"process":"d d~ > z g","process_key":"dd_z_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-builtin-sm-nlc-n2-dd-zz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":2,"process":"d d~ > z z","process_key":"dd_zz_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-builtin-sm-nlc-n2-gg-gluons-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":2,"process":"g g > g g","process_key":"gg_gluons","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-builtin-sm-nlc-n2-gg-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":2,"process":"g g > t t~","process_key":"gg_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-builtin-sm-nlc-n2-ud-epve-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":2,"process":"u d~ > e+ ve","process_key":"ud_epve_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-builtin-sm-nlc-n2-ud-w-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":2,"process":"u d~ > w+ g","process_key":"ud_w_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-builtin-sm-nlc-n3-dd-epem-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"d d~ > e+ e- g","process_key":"dd_epem_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-builtin-sm-nlc-n3-dd-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"d d~ > t t~ g","process_key":"dd_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-builtin-sm-nlc-n3-dd-z-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"d d~ > z g g","process_key":"dd_z_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-builtin-sm-nlc-n3-dd-zz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"d d~ > z z g","process_key":"dd_zz_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-builtin-sm-nlc-n3-dd-zzz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"d d~ > z z z","process_key":"dd_zzz_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-builtin-sm-nlc-n3-gg-gluons-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"g g > g g g","process_key":"gg_gluons","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-builtin-sm-nlc-n3-gg-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"g g > t t~ g","process_key":"gg_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-builtin-sm-nlc-n3-ud-epve-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"u d~ > e+ ve g","process_key":"ud_epve_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-builtin-sm-nlc-n3-ud-w-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"u d~ > w+ g g","process_key":"ud_w_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-builtin-sm-nlc-n4-dd-3q-identical-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > u u~ u u~","process_key":"dd_3q_identical_lines","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-builtin-sm-nlc-n4-dd-3q-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > u u~ s s~","process_key":"dd_3q_lines","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-builtin-sm-nlc-n4-dd-4l-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > e+ e- e+ e-","process_key":"dd_4l_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-builtin-sm-nlc-n4-dd-epem-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > e+ e- g g","process_key":"dd_epem_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-builtin-sm-nlc-n4-dd-epemzh-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > e+ e- z h","process_key":"dd_epemzh_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-builtin-sm-nlc-n4-dd-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > t t~ g g","process_key":"dd_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-builtin-sm-nlc-n4-dd-ttzh-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > t t~ z h","process_key":"dd_ttzh_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-builtin-sm-nlc-n4-dd-z-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > z g g g","process_key":"dd_z_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-builtin-sm-nlc-n4-dd-zz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > z z g g","process_key":"dd_zz_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-builtin-sm-nlc-n4-dd-zzz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > z z z g","process_key":"dd_zzz_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-builtin-sm-nlc-n4-gg-gluons-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"g g > g g g g","process_key":"gg_gluons","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-builtin-sm-nlc-n4-gg-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"g g > t t~ g g","process_key":"gg_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-builtin-sm-nlc-n4-ud-epve-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"u d~ > e+ ve g g","process_key":"ud_epve_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-builtin-sm-nlc-n4-ud-w-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"u d~ > w+ g g g","process_key":"ud_w_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-builtin-sm-nlc-n5-dd-3q-identical-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"d d~ > u u~ u u~ g","process_key":"dd_3q_identical_lines","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-builtin-sm-nlc-n5-dd-3q-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"d d~ > u u~ s s~ g","process_key":"dd_3q_lines","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-builtin-sm-nlc-n5-dd-4l-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"d d~ > e+ e- e+ e- g","process_key":"dd_4l_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-builtin-sm-nlc-n5-dd-epem-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"d d~ > e+ e- g g g","process_key":"dd_epem_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-builtin-sm-nlc-n5-dd-epemzh-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"d d~ > e+ e- z h g","process_key":"dd_epemzh_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-builtin-sm-nlc-n5-dd-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"d d~ > t t~ g g g","process_key":"dd_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-builtin-sm-nlc-n5-dd-ttzh-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"d d~ > t t~ z h g","process_key":"dd_ttzh_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-builtin-sm-nlc-n5-dd-z-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"d d~ > z g g g g","process_key":"dd_z_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-builtin-sm-nlc-n5-dd-zz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"d d~ > z z g g g","process_key":"dd_zz_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-builtin-sm-nlc-n5-dd-zzz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"d d~ > z z z g g","process_key":"dd_zzz_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-builtin-sm-nlc-n5-gg-gluons-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"g g > g g g g g","process_key":"gg_gluons","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-builtin-sm-nlc-n5-gg-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"g g > t t~ g g g","process_key":"gg_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-builtin-sm-nlc-n5-ud-epve-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"u d~ > e+ ve g g g","process_key":"ud_epve_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-builtin-sm-nlc-n5-ud-w-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"u d~ > w+ g g g g","process_key":"ud_w_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-builtin-sm-nlc-n6-dd-3q-identical-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":6,"process":"d d~ > u u~ u u~ g g","process_key":"dd_3q_identical_lines","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-builtin-sm-nlc-n6-dd-3q-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":6,"process":"d d~ > u u~ s s~ g g","process_key":"dd_3q_lines","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-builtin-sm-nlc-n6-dd-4q-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":6,"process":"d d~ > u u~ s s~ c c~","process_key":"dd_4q_lines","variant":null,"workload":"contracted"}],"kind":"performance_measurements","measurement_specs":[{"accuracy":"nlc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"}],"report_version":"0.3.0","schema_version":5} diff --git a/pyamplicol/src/pyamplicol/_profiling_campaign/results/matrix_eager_ufo_sm_full.json b/pyamplicol/src/pyamplicol/_profiling_campaign/results/matrix_eager_ufo_sm_full.json new file mode 100644 index 0000000000..9398809b07 --- /dev/null +++ b/pyamplicol/src/pyamplicol/_profiling_campaign/results/matrix_eager_ufo_sm_full.json @@ -0,0 +1 @@ +{"$schema":"./report-cache.schema.json","dataset_id":"matrix_eager_ufo_sm_full","entries":[{"cell_id":"matrix-eager-ufo-sm-full-n1-dd-z-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"ufo_sm"},"n_final":1,"process":"d d~ > z","process_key":"dd_z_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-ufo-sm-full-n1-ud-w-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"ufo_sm"},"n_final":1,"process":"u d~ > w+","process_key":"ud_w_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-ufo-sm-full-n2-dd-epem-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"ufo_sm"},"n_final":2,"process":"d d~ > e+ e-","process_key":"dd_epem_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-ufo-sm-full-n2-dd-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"ufo_sm"},"n_final":2,"process":"d d~ > t t~","process_key":"dd_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-ufo-sm-full-n2-dd-z-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"ufo_sm"},"n_final":2,"process":"d d~ > z g","process_key":"dd_z_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-ufo-sm-full-n2-dd-zz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"ufo_sm"},"n_final":2,"process":"d d~ > z z","process_key":"dd_zz_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-ufo-sm-full-n2-gg-gluons-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"ufo_sm"},"n_final":2,"process":"g g > g g","process_key":"gg_gluons","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-ufo-sm-full-n2-gg-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"ufo_sm"},"n_final":2,"process":"g g > t t~","process_key":"gg_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-ufo-sm-full-n2-ud-epve-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"ufo_sm"},"n_final":2,"process":"u d~ > e+ ve","process_key":"ud_epve_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-ufo-sm-full-n2-ud-w-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"ufo_sm"},"n_final":2,"process":"u d~ > w+ g","process_key":"ud_w_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-ufo-sm-full-n3-dd-epem-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"ufo_sm"},"n_final":3,"process":"d d~ > e+ e- g","process_key":"dd_epem_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-ufo-sm-full-n3-dd-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"ufo_sm"},"n_final":3,"process":"d d~ > t t~ g","process_key":"dd_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-ufo-sm-full-n3-dd-z-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"ufo_sm"},"n_final":3,"process":"d d~ > z g g","process_key":"dd_z_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-ufo-sm-full-n3-dd-zz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"ufo_sm"},"n_final":3,"process":"d d~ > z z g","process_key":"dd_zz_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-ufo-sm-full-n3-dd-zzz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"ufo_sm"},"n_final":3,"process":"d d~ > z z z","process_key":"dd_zzz_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-ufo-sm-full-n3-gg-gluons-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"ufo_sm"},"n_final":3,"process":"g g > g g g","process_key":"gg_gluons","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-ufo-sm-full-n3-gg-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"ufo_sm"},"n_final":3,"process":"g g > t t~ g","process_key":"gg_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-ufo-sm-full-n3-ud-epve-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"ufo_sm"},"n_final":3,"process":"u d~ > e+ ve g","process_key":"ud_epve_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-ufo-sm-full-n3-ud-w-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"ufo_sm"},"n_final":3,"process":"u d~ > w+ g g","process_key":"ud_w_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-ufo-sm-full-n4-dd-3q-identical-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"ufo_sm"},"n_final":4,"process":"d d~ > u u~ u u~","process_key":"dd_3q_identical_lines","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-ufo-sm-full-n4-dd-3q-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"ufo_sm"},"n_final":4,"process":"d d~ > u u~ s s~","process_key":"dd_3q_lines","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-ufo-sm-full-n4-dd-4l-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"ufo_sm"},"n_final":4,"process":"d d~ > e+ e- e+ e-","process_key":"dd_4l_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-ufo-sm-full-n4-dd-epem-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"ufo_sm"},"n_final":4,"process":"d d~ > e+ e- g g","process_key":"dd_epem_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-ufo-sm-full-n4-dd-epemzh-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"ufo_sm"},"n_final":4,"process":"d d~ > e+ e- z h","process_key":"dd_epemzh_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-ufo-sm-full-n4-dd-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"ufo_sm"},"n_final":4,"process":"d d~ > t t~ g g","process_key":"dd_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-ufo-sm-full-n4-dd-ttzh-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"ufo_sm"},"n_final":4,"process":"d d~ > t t~ z h","process_key":"dd_ttzh_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-ufo-sm-full-n4-dd-z-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"ufo_sm"},"n_final":4,"process":"d d~ > z g g g","process_key":"dd_z_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-ufo-sm-full-n4-dd-zz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"ufo_sm"},"n_final":4,"process":"d d~ > z z g g","process_key":"dd_zz_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-ufo-sm-full-n4-dd-zzz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"ufo_sm"},"n_final":4,"process":"d d~ > z z z g","process_key":"dd_zzz_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-ufo-sm-full-n4-gg-gluons-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"ufo_sm"},"n_final":4,"process":"g g > g g g g","process_key":"gg_gluons","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-ufo-sm-full-n4-gg-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"ufo_sm"},"n_final":4,"process":"g g > t t~ g g","process_key":"gg_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-ufo-sm-full-n4-ud-epve-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"ufo_sm"},"n_final":4,"process":"u d~ > e+ ve g g","process_key":"ud_epve_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-ufo-sm-full-n4-ud-w-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"ufo_sm"},"n_final":4,"process":"u d~ > w+ g g g","process_key":"ud_w_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-ufo-sm-full-n5-dd-3q-identical-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"ufo_sm"},"n_final":5,"process":"d d~ > u u~ u u~ g","process_key":"dd_3q_identical_lines","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-ufo-sm-full-n5-dd-3q-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"ufo_sm"},"n_final":5,"process":"d d~ > u u~ s s~ g","process_key":"dd_3q_lines","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-ufo-sm-full-n5-dd-4l-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"ufo_sm"},"n_final":5,"process":"d d~ > e+ e- e+ e- g","process_key":"dd_4l_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-ufo-sm-full-n5-dd-epem-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"ufo_sm"},"n_final":5,"process":"d d~ > e+ e- g g g","process_key":"dd_epem_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-ufo-sm-full-n5-dd-epemzh-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"ufo_sm"},"n_final":5,"process":"d d~ > e+ e- z h g","process_key":"dd_epemzh_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-ufo-sm-full-n5-dd-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"ufo_sm"},"n_final":5,"process":"d d~ > t t~ g g g","process_key":"dd_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-ufo-sm-full-n5-dd-ttzh-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"ufo_sm"},"n_final":5,"process":"d d~ > t t~ z h g","process_key":"dd_ttzh_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-ufo-sm-full-n5-dd-z-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"ufo_sm"},"n_final":5,"process":"d d~ > z g g g g","process_key":"dd_z_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-ufo-sm-full-n5-dd-zz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"ufo_sm"},"n_final":5,"process":"d d~ > z z g g g","process_key":"dd_zz_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-ufo-sm-full-n5-dd-zzz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"ufo_sm"},"n_final":5,"process":"d d~ > z z z g g","process_key":"dd_zzz_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-ufo-sm-full-n5-gg-gluons-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"ufo_sm"},"n_final":5,"process":"g g > g g g g g","process_key":"gg_gluons","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-ufo-sm-full-n5-gg-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"ufo_sm"},"n_final":5,"process":"g g > t t~ g g g","process_key":"gg_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-ufo-sm-full-n5-ud-epve-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"ufo_sm"},"n_final":5,"process":"u d~ > e+ ve g g g","process_key":"ud_epve_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-ufo-sm-full-n5-ud-w-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"ufo_sm"},"n_final":5,"process":"u d~ > w+ g g g g","process_key":"ud_w_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-ufo-sm-full-n6-dd-3q-identical-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"ufo_sm"},"n_final":6,"process":"d d~ > u u~ u u~ g g","process_key":"dd_3q_identical_lines","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-ufo-sm-full-n6-dd-3q-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"ufo_sm"},"n_final":6,"process":"d d~ > u u~ s s~ g g","process_key":"dd_3q_lines","variant":null,"workload":"contracted"},{"cell_id":"matrix-eager-ufo-sm-full-n6-dd-4q-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"ufo_sm"},"n_final":6,"process":"d d~ > u u~ s s~ c c~","process_key":"dd_4q_lines","variant":null,"workload":"contracted"}],"kind":"performance_measurements","measurement_specs":[{"accuracy":"full","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"ufo_sm"}],"report_version":"0.3.0","schema_version":5} diff --git a/pyamplicol/src/pyamplicol/_profiling_campaign/results/matrix_on_the_fly_builtin_sm_full.json b/pyamplicol/src/pyamplicol/_profiling_campaign/results/matrix_on_the_fly_builtin_sm_full.json new file mode 100644 index 0000000000..eff1e7a3b2 --- /dev/null +++ b/pyamplicol/src/pyamplicol/_profiling_campaign/results/matrix_on_the_fly_builtin_sm_full.json @@ -0,0 +1 @@ +{"$schema":"./report-cache.schema.json","dataset_id":"matrix_on_the_fly_builtin_sm_full","entries":[{"cell_id":"matrix-on-the-fly-builtin-sm-full-n1-dd-z-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":1,"process":"d d~ > z","process_key":"dd_z_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-builtin-sm-full-n1-ud-w-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":1,"process":"u d~ > w+","process_key":"ud_w_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-builtin-sm-full-n2-dd-epem-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":2,"process":"d d~ > e+ e-","process_key":"dd_epem_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-builtin-sm-full-n2-dd-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":2,"process":"d d~ > t t~","process_key":"dd_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-builtin-sm-full-n2-dd-z-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":2,"process":"d d~ > z g","process_key":"dd_z_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-builtin-sm-full-n2-dd-zz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":2,"process":"d d~ > z z","process_key":"dd_zz_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-builtin-sm-full-n2-gg-gluons-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":2,"process":"g g > g g","process_key":"gg_gluons","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-builtin-sm-full-n2-gg-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":2,"process":"g g > t t~","process_key":"gg_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-builtin-sm-full-n2-ud-epve-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":2,"process":"u d~ > e+ ve","process_key":"ud_epve_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-builtin-sm-full-n2-ud-w-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":2,"process":"u d~ > w+ g","process_key":"ud_w_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-builtin-sm-full-n3-dd-epem-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"d d~ > e+ e- g","process_key":"dd_epem_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-builtin-sm-full-n3-dd-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"d d~ > t t~ g","process_key":"dd_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-builtin-sm-full-n3-dd-z-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"d d~ > z g g","process_key":"dd_z_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-builtin-sm-full-n3-dd-zz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"d d~ > z z g","process_key":"dd_zz_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-builtin-sm-full-n3-dd-zzz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"d d~ > z z z","process_key":"dd_zzz_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-builtin-sm-full-n3-gg-gluons-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"g g > g g g","process_key":"gg_gluons","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-builtin-sm-full-n3-gg-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"g g > t t~ g","process_key":"gg_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-builtin-sm-full-n3-ud-epve-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"u d~ > e+ ve g","process_key":"ud_epve_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-builtin-sm-full-n3-ud-w-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"u d~ > w+ g g","process_key":"ud_w_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-builtin-sm-full-n4-dd-3q-identical-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > u u~ u u~","process_key":"dd_3q_identical_lines","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-builtin-sm-full-n4-dd-3q-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > u u~ s s~","process_key":"dd_3q_lines","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-builtin-sm-full-n4-dd-4l-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > e+ e- e+ e-","process_key":"dd_4l_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-builtin-sm-full-n4-dd-epem-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > e+ e- g g","process_key":"dd_epem_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-builtin-sm-full-n4-dd-epemzh-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > e+ e- z h","process_key":"dd_epemzh_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-builtin-sm-full-n4-dd-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > t t~ g g","process_key":"dd_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-builtin-sm-full-n4-dd-ttzh-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > t t~ z h","process_key":"dd_ttzh_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-builtin-sm-full-n4-dd-z-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > z g g g","process_key":"dd_z_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-builtin-sm-full-n4-dd-zz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > z z g g","process_key":"dd_zz_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-builtin-sm-full-n4-dd-zzz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > z z z g","process_key":"dd_zzz_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-builtin-sm-full-n4-gg-gluons-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"g g > g g g g","process_key":"gg_gluons","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-builtin-sm-full-n4-gg-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"g g > t t~ g g","process_key":"gg_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-builtin-sm-full-n4-ud-epve-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"u d~ > e+ ve g g","process_key":"ud_epve_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-builtin-sm-full-n4-ud-w-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"u d~ > w+ g g g","process_key":"ud_w_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-builtin-sm-full-n5-dd-3q-identical-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"d d~ > u u~ u u~ g","process_key":"dd_3q_identical_lines","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-builtin-sm-full-n5-dd-3q-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"d d~ > u u~ s s~ g","process_key":"dd_3q_lines","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-builtin-sm-full-n5-dd-4l-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"d d~ > e+ e- e+ e- g","process_key":"dd_4l_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-builtin-sm-full-n5-dd-epem-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"d d~ > e+ e- g g g","process_key":"dd_epem_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-builtin-sm-full-n5-dd-epemzh-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"d d~ > e+ e- z h g","process_key":"dd_epemzh_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-builtin-sm-full-n5-dd-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"d d~ > t t~ g g g","process_key":"dd_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-builtin-sm-full-n5-dd-ttzh-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"d d~ > t t~ z h g","process_key":"dd_ttzh_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-builtin-sm-full-n5-dd-z-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"d d~ > z g g g g","process_key":"dd_z_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-builtin-sm-full-n5-dd-zz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"d d~ > z z g g g","process_key":"dd_zz_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-builtin-sm-full-n5-dd-zzz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"d d~ > z z z g g","process_key":"dd_zzz_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-builtin-sm-full-n5-gg-gluons-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"g g > g g g g g","process_key":"gg_gluons","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-builtin-sm-full-n5-gg-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"g g > t t~ g g g","process_key":"gg_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-builtin-sm-full-n5-ud-epve-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"u d~ > e+ ve g g g","process_key":"ud_epve_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-builtin-sm-full-n5-ud-w-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"u d~ > w+ g g g g","process_key":"ud_w_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-builtin-sm-full-n6-dd-3q-identical-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":6,"process":"d d~ > u u~ u u~ g g","process_key":"dd_3q_identical_lines","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-builtin-sm-full-n6-dd-3q-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":6,"process":"d d~ > u u~ s s~ g g","process_key":"dd_3q_lines","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-builtin-sm-full-n6-dd-4q-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":6,"process":"d d~ > u u~ s s~ c c~","process_key":"dd_4q_lines","variant":null,"workload":"contracted"}],"kind":"performance_measurements","measurement_specs":[{"accuracy":"full","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"}],"report_version":"0.3.0","schema_version":5} diff --git a/pyamplicol/src/pyamplicol/_profiling_campaign/results/matrix_on_the_fly_builtin_sm_lc.json b/pyamplicol/src/pyamplicol/_profiling_campaign/results/matrix_on_the_fly_builtin_sm_lc.json new file mode 100644 index 0000000000..10352c3c4d --- /dev/null +++ b/pyamplicol/src/pyamplicol/_profiling_campaign/results/matrix_on_the_fly_builtin_sm_lc.json @@ -0,0 +1 @@ +{"$schema":"./report-cache.schema.json","dataset_id":"matrix_on_the_fly_builtin_sm_lc","entries":[{"cell_id":"matrix-on-the-fly-builtin-sm-lc-n1-dd-z-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":1,"process":"d d~ > z","process_key":"dd_z_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-on-the-fly-builtin-sm-lc-n1-dd-z-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":1,"process":"d d~ > z","process_key":"dd_z_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-on-the-fly-builtin-sm-lc-n1-ud-w-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":1,"process":"u d~ > w+","process_key":"ud_w_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-on-the-fly-builtin-sm-lc-n1-ud-w-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":1,"process":"u d~ > w+","process_key":"ud_w_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-on-the-fly-builtin-sm-lc-n2-dd-epem-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":2,"process":"d d~ > e+ e-","process_key":"dd_epem_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-on-the-fly-builtin-sm-lc-n2-dd-epem-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":2,"process":"d d~ > e+ e-","process_key":"dd_epem_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-on-the-fly-builtin-sm-lc-n2-dd-tt-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":2,"process":"d d~ > t t~","process_key":"dd_tt_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-on-the-fly-builtin-sm-lc-n2-dd-tt-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":2,"process":"d d~ > t t~","process_key":"dd_tt_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-on-the-fly-builtin-sm-lc-n2-dd-z-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":2,"process":"d d~ > z g","process_key":"dd_z_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-on-the-fly-builtin-sm-lc-n2-dd-z-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":2,"process":"d d~ > z g","process_key":"dd_z_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-on-the-fly-builtin-sm-lc-n2-dd-zz-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":2,"process":"d d~ > z z","process_key":"dd_zz_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-on-the-fly-builtin-sm-lc-n2-dd-zz-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":2,"process":"d d~ > z z","process_key":"dd_zz_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-on-the-fly-builtin-sm-lc-n2-gg-gluons-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":2,"process":"g g > g g","process_key":"gg_gluons","variant":null,"workload":"all-flow"},{"cell_id":"matrix-on-the-fly-builtin-sm-lc-n2-gg-gluons-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":2,"process":"g g > g g","process_key":"gg_gluons","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-on-the-fly-builtin-sm-lc-n2-gg-tt-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":2,"process":"g g > t t~","process_key":"gg_tt_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-on-the-fly-builtin-sm-lc-n2-gg-tt-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":2,"process":"g g > t t~","process_key":"gg_tt_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-on-the-fly-builtin-sm-lc-n2-ud-epve-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":2,"process":"u d~ > e+ ve","process_key":"ud_epve_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-on-the-fly-builtin-sm-lc-n2-ud-epve-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":2,"process":"u d~ > e+ ve","process_key":"ud_epve_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-on-the-fly-builtin-sm-lc-n2-ud-w-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":2,"process":"u d~ > w+ g","process_key":"ud_w_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-on-the-fly-builtin-sm-lc-n2-ud-w-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":2,"process":"u d~ > w+ g","process_key":"ud_w_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-on-the-fly-builtin-sm-lc-n3-dd-epem-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"d d~ > e+ e- g","process_key":"dd_epem_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-on-the-fly-builtin-sm-lc-n3-dd-epem-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"d d~ > e+ e- g","process_key":"dd_epem_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-on-the-fly-builtin-sm-lc-n3-dd-tt-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"d d~ > t t~ g","process_key":"dd_tt_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-on-the-fly-builtin-sm-lc-n3-dd-tt-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"d d~ > t t~ g","process_key":"dd_tt_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-on-the-fly-builtin-sm-lc-n3-dd-z-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"d d~ > z g g","process_key":"dd_z_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-on-the-fly-builtin-sm-lc-n3-dd-z-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"d d~ > z g g","process_key":"dd_z_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-on-the-fly-builtin-sm-lc-n3-dd-zz-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"d d~ > z z g","process_key":"dd_zz_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-on-the-fly-builtin-sm-lc-n3-dd-zz-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"d d~ > z z g","process_key":"dd_zz_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-on-the-fly-builtin-sm-lc-n3-dd-zzz-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"d d~ > z z z","process_key":"dd_zzz_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-on-the-fly-builtin-sm-lc-n3-dd-zzz-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"d d~ > z z z","process_key":"dd_zzz_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-on-the-fly-builtin-sm-lc-n3-gg-gluons-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"g g > g g g","process_key":"gg_gluons","variant":null,"workload":"all-flow"},{"cell_id":"matrix-on-the-fly-builtin-sm-lc-n3-gg-gluons-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"g g > g g g","process_key":"gg_gluons","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-on-the-fly-builtin-sm-lc-n3-gg-tt-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"g g > t t~ g","process_key":"gg_tt_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-on-the-fly-builtin-sm-lc-n3-gg-tt-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"g g > t t~ g","process_key":"gg_tt_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-on-the-fly-builtin-sm-lc-n3-ud-epve-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"u d~ > e+ ve g","process_key":"ud_epve_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-on-the-fly-builtin-sm-lc-n3-ud-epve-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"u d~ > e+ ve g","process_key":"ud_epve_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-on-the-fly-builtin-sm-lc-n3-ud-w-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"u d~ > w+ g g","process_key":"ud_w_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-on-the-fly-builtin-sm-lc-n3-ud-w-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"u d~ > w+ g g","process_key":"ud_w_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-on-the-fly-builtin-sm-lc-n4-dd-3q-identical-lines-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > u u~ u u~","process_key":"dd_3q_identical_lines","variant":null,"workload":"all-flow"},{"cell_id":"matrix-on-the-fly-builtin-sm-lc-n4-dd-3q-identical-lines-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > u u~ u u~","process_key":"dd_3q_identical_lines","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-on-the-fly-builtin-sm-lc-n4-dd-3q-lines-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > u u~ s s~","process_key":"dd_3q_lines","variant":null,"workload":"all-flow"},{"cell_id":"matrix-on-the-fly-builtin-sm-lc-n4-dd-3q-lines-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > u u~ s s~","process_key":"dd_3q_lines","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-on-the-fly-builtin-sm-lc-n4-dd-4l-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > e+ e- e+ e-","process_key":"dd_4l_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-on-the-fly-builtin-sm-lc-n4-dd-4l-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > e+ e- e+ e-","process_key":"dd_4l_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-on-the-fly-builtin-sm-lc-n4-dd-epem-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > e+ e- g g","process_key":"dd_epem_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-on-the-fly-builtin-sm-lc-n4-dd-epem-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > e+ e- g g","process_key":"dd_epem_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-on-the-fly-builtin-sm-lc-n4-dd-epemzh-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > e+ e- z h","process_key":"dd_epemzh_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-on-the-fly-builtin-sm-lc-n4-dd-epemzh-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > e+ e- z h","process_key":"dd_epemzh_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-on-the-fly-builtin-sm-lc-n4-dd-tt-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > t t~ g g","process_key":"dd_tt_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-on-the-fly-builtin-sm-lc-n4-dd-tt-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > t t~ g g","process_key":"dd_tt_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-on-the-fly-builtin-sm-lc-n4-dd-ttzh-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > t t~ z h","process_key":"dd_ttzh_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-on-the-fly-builtin-sm-lc-n4-dd-ttzh-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > t t~ z h","process_key":"dd_ttzh_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-on-the-fly-builtin-sm-lc-n4-dd-z-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > z g g g","process_key":"dd_z_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-on-the-fly-builtin-sm-lc-n4-dd-z-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > z g g g","process_key":"dd_z_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-on-the-fly-builtin-sm-lc-n4-dd-zz-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > z z g g","process_key":"dd_zz_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-on-the-fly-builtin-sm-lc-n4-dd-zz-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > z z g g","process_key":"dd_zz_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-on-the-fly-builtin-sm-lc-n4-dd-zzz-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > z z z g","process_key":"dd_zzz_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-on-the-fly-builtin-sm-lc-n4-dd-zzz-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > z z z g","process_key":"dd_zzz_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-on-the-fly-builtin-sm-lc-n4-gg-gluons-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"g g > g g g g","process_key":"gg_gluons","variant":null,"workload":"all-flow"},{"cell_id":"matrix-on-the-fly-builtin-sm-lc-n4-gg-gluons-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"g g > g g g g","process_key":"gg_gluons","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-on-the-fly-builtin-sm-lc-n4-gg-tt-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"g g > t t~ g g","process_key":"gg_tt_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-on-the-fly-builtin-sm-lc-n4-gg-tt-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"g g > t t~ g g","process_key":"gg_tt_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-on-the-fly-builtin-sm-lc-n4-ud-epve-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"u d~ > e+ ve g g","process_key":"ud_epve_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-on-the-fly-builtin-sm-lc-n4-ud-epve-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"u d~ > e+ ve g g","process_key":"ud_epve_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-on-the-fly-builtin-sm-lc-n4-ud-w-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"u d~ > w+ g g g","process_key":"ud_w_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-on-the-fly-builtin-sm-lc-n4-ud-w-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"u d~ > w+ g g g","process_key":"ud_w_jets","variant":null,"workload":"selected-flow"}],"kind":"performance_measurements","measurement_specs":[{"accuracy":"lc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"}],"report_version":"0.3.0","schema_version":5} diff --git a/pyamplicol/src/pyamplicol/_profiling_campaign/results/matrix_on_the_fly_builtin_sm_nlc.json b/pyamplicol/src/pyamplicol/_profiling_campaign/results/matrix_on_the_fly_builtin_sm_nlc.json new file mode 100644 index 0000000000..c1d6d121b2 --- /dev/null +++ b/pyamplicol/src/pyamplicol/_profiling_campaign/results/matrix_on_the_fly_builtin_sm_nlc.json @@ -0,0 +1 @@ +{"$schema":"./report-cache.schema.json","dataset_id":"matrix_on_the_fly_builtin_sm_nlc","entries":[{"cell_id":"matrix-on-the-fly-builtin-sm-nlc-n1-dd-z-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":1,"process":"d d~ > z","process_key":"dd_z_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-builtin-sm-nlc-n1-ud-w-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":1,"process":"u d~ > w+","process_key":"ud_w_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-builtin-sm-nlc-n2-dd-epem-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":2,"process":"d d~ > e+ e-","process_key":"dd_epem_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-builtin-sm-nlc-n2-dd-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":2,"process":"d d~ > t t~","process_key":"dd_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-builtin-sm-nlc-n2-dd-z-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":2,"process":"d d~ > z g","process_key":"dd_z_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-builtin-sm-nlc-n2-dd-zz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":2,"process":"d d~ > z z","process_key":"dd_zz_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-builtin-sm-nlc-n2-gg-gluons-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":2,"process":"g g > g g","process_key":"gg_gluons","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-builtin-sm-nlc-n2-gg-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":2,"process":"g g > t t~","process_key":"gg_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-builtin-sm-nlc-n2-ud-epve-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":2,"process":"u d~ > e+ ve","process_key":"ud_epve_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-builtin-sm-nlc-n2-ud-w-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":2,"process":"u d~ > w+ g","process_key":"ud_w_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-builtin-sm-nlc-n3-dd-epem-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"d d~ > e+ e- g","process_key":"dd_epem_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-builtin-sm-nlc-n3-dd-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"d d~ > t t~ g","process_key":"dd_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-builtin-sm-nlc-n3-dd-z-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"d d~ > z g g","process_key":"dd_z_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-builtin-sm-nlc-n3-dd-zz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"d d~ > z z g","process_key":"dd_zz_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-builtin-sm-nlc-n3-dd-zzz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"d d~ > z z z","process_key":"dd_zzz_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-builtin-sm-nlc-n3-gg-gluons-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"g g > g g g","process_key":"gg_gluons","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-builtin-sm-nlc-n3-gg-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"g g > t t~ g","process_key":"gg_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-builtin-sm-nlc-n3-ud-epve-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"u d~ > e+ ve g","process_key":"ud_epve_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-builtin-sm-nlc-n3-ud-w-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"u d~ > w+ g g","process_key":"ud_w_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-builtin-sm-nlc-n4-dd-3q-identical-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > u u~ u u~","process_key":"dd_3q_identical_lines","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-builtin-sm-nlc-n4-dd-3q-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > u u~ s s~","process_key":"dd_3q_lines","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-builtin-sm-nlc-n4-dd-4l-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > e+ e- e+ e-","process_key":"dd_4l_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-builtin-sm-nlc-n4-dd-epem-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > e+ e- g g","process_key":"dd_epem_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-builtin-sm-nlc-n4-dd-epemzh-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > e+ e- z h","process_key":"dd_epemzh_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-builtin-sm-nlc-n4-dd-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > t t~ g g","process_key":"dd_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-builtin-sm-nlc-n4-dd-ttzh-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > t t~ z h","process_key":"dd_ttzh_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-builtin-sm-nlc-n4-dd-z-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > z g g g","process_key":"dd_z_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-builtin-sm-nlc-n4-dd-zz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > z z g g","process_key":"dd_zz_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-builtin-sm-nlc-n4-dd-zzz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > z z z g","process_key":"dd_zzz_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-builtin-sm-nlc-n4-gg-gluons-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"g g > g g g g","process_key":"gg_gluons","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-builtin-sm-nlc-n4-gg-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"g g > t t~ g g","process_key":"gg_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-builtin-sm-nlc-n4-ud-epve-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"u d~ > e+ ve g g","process_key":"ud_epve_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-builtin-sm-nlc-n4-ud-w-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"u d~ > w+ g g g","process_key":"ud_w_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-builtin-sm-nlc-n5-dd-3q-identical-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"d d~ > u u~ u u~ g","process_key":"dd_3q_identical_lines","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-builtin-sm-nlc-n5-dd-3q-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"d d~ > u u~ s s~ g","process_key":"dd_3q_lines","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-builtin-sm-nlc-n5-dd-4l-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"d d~ > e+ e- e+ e- g","process_key":"dd_4l_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-builtin-sm-nlc-n5-dd-epem-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"d d~ > e+ e- g g g","process_key":"dd_epem_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-builtin-sm-nlc-n5-dd-epemzh-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"d d~ > e+ e- z h g","process_key":"dd_epemzh_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-builtin-sm-nlc-n5-dd-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"d d~ > t t~ g g g","process_key":"dd_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-builtin-sm-nlc-n5-dd-ttzh-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"d d~ > t t~ z h g","process_key":"dd_ttzh_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-builtin-sm-nlc-n5-dd-z-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"d d~ > z g g g g","process_key":"dd_z_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-builtin-sm-nlc-n5-dd-zz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"d d~ > z z g g g","process_key":"dd_zz_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-builtin-sm-nlc-n5-dd-zzz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"d d~ > z z z g g","process_key":"dd_zzz_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-builtin-sm-nlc-n5-gg-gluons-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"g g > g g g g g","process_key":"gg_gluons","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-builtin-sm-nlc-n5-gg-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"g g > t t~ g g g","process_key":"gg_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-builtin-sm-nlc-n5-ud-epve-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"u d~ > e+ ve g g g","process_key":"ud_epve_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-builtin-sm-nlc-n5-ud-w-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"u d~ > w+ g g g g","process_key":"ud_w_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-builtin-sm-nlc-n6-dd-3q-identical-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":6,"process":"d d~ > u u~ u u~ g g","process_key":"dd_3q_identical_lines","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-builtin-sm-nlc-n6-dd-3q-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":6,"process":"d d~ > u u~ s s~ g g","process_key":"dd_3q_lines","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-builtin-sm-nlc-n6-dd-4q-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":6,"process":"d d~ > u u~ s s~ c c~","process_key":"dd_4q_lines","variant":null,"workload":"contracted"}],"kind":"performance_measurements","measurement_specs":[{"accuracy":"nlc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"}],"report_version":"0.3.0","schema_version":5} diff --git a/pyamplicol/src/pyamplicol/_profiling_campaign/results/matrix_on_the_fly_ufo_sm_full.json b/pyamplicol/src/pyamplicol/_profiling_campaign/results/matrix_on_the_fly_ufo_sm_full.json new file mode 100644 index 0000000000..48e2c99a45 --- /dev/null +++ b/pyamplicol/src/pyamplicol/_profiling_campaign/results/matrix_on_the_fly_ufo_sm_full.json @@ -0,0 +1 @@ +{"$schema":"./report-cache.schema.json","dataset_id":"matrix_on_the_fly_ufo_sm_full","entries":[{"cell_id":"matrix-on-the-fly-ufo-sm-full-n1-dd-z-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"ufo_sm"},"n_final":1,"process":"d d~ > z","process_key":"dd_z_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-ufo-sm-full-n1-ud-w-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"ufo_sm"},"n_final":1,"process":"u d~ > w+","process_key":"ud_w_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-ufo-sm-full-n2-dd-epem-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"ufo_sm"},"n_final":2,"process":"d d~ > e+ e-","process_key":"dd_epem_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-ufo-sm-full-n2-dd-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"ufo_sm"},"n_final":2,"process":"d d~ > t t~","process_key":"dd_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-ufo-sm-full-n2-dd-z-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"ufo_sm"},"n_final":2,"process":"d d~ > z g","process_key":"dd_z_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-ufo-sm-full-n2-dd-zz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"ufo_sm"},"n_final":2,"process":"d d~ > z z","process_key":"dd_zz_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-ufo-sm-full-n2-gg-gluons-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"ufo_sm"},"n_final":2,"process":"g g > g g","process_key":"gg_gluons","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-ufo-sm-full-n2-gg-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"ufo_sm"},"n_final":2,"process":"g g > t t~","process_key":"gg_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-ufo-sm-full-n2-ud-epve-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"ufo_sm"},"n_final":2,"process":"u d~ > e+ ve","process_key":"ud_epve_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-ufo-sm-full-n2-ud-w-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"ufo_sm"},"n_final":2,"process":"u d~ > w+ g","process_key":"ud_w_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-ufo-sm-full-n3-dd-epem-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"ufo_sm"},"n_final":3,"process":"d d~ > e+ e- g","process_key":"dd_epem_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-ufo-sm-full-n3-dd-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"ufo_sm"},"n_final":3,"process":"d d~ > t t~ g","process_key":"dd_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-ufo-sm-full-n3-dd-z-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"ufo_sm"},"n_final":3,"process":"d d~ > z g g","process_key":"dd_z_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-ufo-sm-full-n3-dd-zz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"ufo_sm"},"n_final":3,"process":"d d~ > z z g","process_key":"dd_zz_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-ufo-sm-full-n3-dd-zzz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"ufo_sm"},"n_final":3,"process":"d d~ > z z z","process_key":"dd_zzz_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-ufo-sm-full-n3-gg-gluons-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"ufo_sm"},"n_final":3,"process":"g g > g g g","process_key":"gg_gluons","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-ufo-sm-full-n3-gg-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"ufo_sm"},"n_final":3,"process":"g g > t t~ g","process_key":"gg_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-ufo-sm-full-n3-ud-epve-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"ufo_sm"},"n_final":3,"process":"u d~ > e+ ve g","process_key":"ud_epve_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-ufo-sm-full-n3-ud-w-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"ufo_sm"},"n_final":3,"process":"u d~ > w+ g g","process_key":"ud_w_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-ufo-sm-full-n4-dd-3q-identical-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"ufo_sm"},"n_final":4,"process":"d d~ > u u~ u u~","process_key":"dd_3q_identical_lines","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-ufo-sm-full-n4-dd-3q-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"ufo_sm"},"n_final":4,"process":"d d~ > u u~ s s~","process_key":"dd_3q_lines","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-ufo-sm-full-n4-dd-4l-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"ufo_sm"},"n_final":4,"process":"d d~ > e+ e- e+ e-","process_key":"dd_4l_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-ufo-sm-full-n4-dd-epem-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"ufo_sm"},"n_final":4,"process":"d d~ > e+ e- g g","process_key":"dd_epem_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-ufo-sm-full-n4-dd-epemzh-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"ufo_sm"},"n_final":4,"process":"d d~ > e+ e- z h","process_key":"dd_epemzh_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-ufo-sm-full-n4-dd-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"ufo_sm"},"n_final":4,"process":"d d~ > t t~ g g","process_key":"dd_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-ufo-sm-full-n4-dd-ttzh-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"ufo_sm"},"n_final":4,"process":"d d~ > t t~ z h","process_key":"dd_ttzh_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-ufo-sm-full-n4-dd-z-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"ufo_sm"},"n_final":4,"process":"d d~ > z g g g","process_key":"dd_z_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-ufo-sm-full-n4-dd-zz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"ufo_sm"},"n_final":4,"process":"d d~ > z z g g","process_key":"dd_zz_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-ufo-sm-full-n4-dd-zzz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"ufo_sm"},"n_final":4,"process":"d d~ > z z z g","process_key":"dd_zzz_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-ufo-sm-full-n4-gg-gluons-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"ufo_sm"},"n_final":4,"process":"g g > g g g g","process_key":"gg_gluons","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-ufo-sm-full-n4-gg-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"ufo_sm"},"n_final":4,"process":"g g > t t~ g g","process_key":"gg_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-ufo-sm-full-n4-ud-epve-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"ufo_sm"},"n_final":4,"process":"u d~ > e+ ve g g","process_key":"ud_epve_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-ufo-sm-full-n4-ud-w-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"ufo_sm"},"n_final":4,"process":"u d~ > w+ g g g","process_key":"ud_w_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-ufo-sm-full-n5-dd-3q-identical-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"ufo_sm"},"n_final":5,"process":"d d~ > u u~ u u~ g","process_key":"dd_3q_identical_lines","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-ufo-sm-full-n5-dd-3q-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"ufo_sm"},"n_final":5,"process":"d d~ > u u~ s s~ g","process_key":"dd_3q_lines","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-ufo-sm-full-n5-dd-4l-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"ufo_sm"},"n_final":5,"process":"d d~ > e+ e- e+ e- g","process_key":"dd_4l_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-ufo-sm-full-n5-dd-epem-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"ufo_sm"},"n_final":5,"process":"d d~ > e+ e- g g g","process_key":"dd_epem_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-ufo-sm-full-n5-dd-epemzh-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"ufo_sm"},"n_final":5,"process":"d d~ > e+ e- z h g","process_key":"dd_epemzh_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-ufo-sm-full-n5-dd-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"ufo_sm"},"n_final":5,"process":"d d~ > t t~ g g g","process_key":"dd_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-ufo-sm-full-n5-dd-ttzh-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"ufo_sm"},"n_final":5,"process":"d d~ > t t~ z h g","process_key":"dd_ttzh_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-ufo-sm-full-n5-dd-z-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"ufo_sm"},"n_final":5,"process":"d d~ > z g g g g","process_key":"dd_z_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-ufo-sm-full-n5-dd-zz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"ufo_sm"},"n_final":5,"process":"d d~ > z z g g g","process_key":"dd_zz_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-ufo-sm-full-n5-dd-zzz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"ufo_sm"},"n_final":5,"process":"d d~ > z z z g g","process_key":"dd_zzz_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-ufo-sm-full-n5-gg-gluons-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"ufo_sm"},"n_final":5,"process":"g g > g g g g g","process_key":"gg_gluons","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-ufo-sm-full-n5-gg-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"ufo_sm"},"n_final":5,"process":"g g > t t~ g g g","process_key":"gg_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-ufo-sm-full-n5-ud-epve-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"ufo_sm"},"n_final":5,"process":"u d~ > e+ ve g g g","process_key":"ud_epve_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-ufo-sm-full-n5-ud-w-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"ufo_sm"},"n_final":5,"process":"u d~ > w+ g g g g","process_key":"ud_w_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-ufo-sm-full-n6-dd-3q-identical-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"ufo_sm"},"n_final":6,"process":"d d~ > u u~ u u~ g g","process_key":"dd_3q_identical_lines","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-ufo-sm-full-n6-dd-3q-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"ufo_sm"},"n_final":6,"process":"d d~ > u u~ s s~ g g","process_key":"dd_3q_lines","variant":null,"workload":"contracted"},{"cell_id":"matrix-on-the-fly-ufo-sm-full-n6-dd-4q-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"ufo_sm"},"n_final":6,"process":"d d~ > u u~ s s~ c c~","process_key":"dd_4q_lines","variant":null,"workload":"contracted"}],"kind":"performance_measurements","measurement_specs":[{"accuracy":"full","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"ufo_sm"}],"report_version":"0.3.0","schema_version":5} diff --git a/pyamplicol/src/pyamplicol/_profiling_campaign/results/matrix_recurrence_builtin_sm_full.json b/pyamplicol/src/pyamplicol/_profiling_campaign/results/matrix_recurrence_builtin_sm_full.json new file mode 100644 index 0000000000..bc5aceec31 --- /dev/null +++ b/pyamplicol/src/pyamplicol/_profiling_campaign/results/matrix_recurrence_builtin_sm_full.json @@ -0,0 +1 @@ +{"$schema":"./report-cache.schema.json","dataset_id":"matrix_recurrence_builtin_sm_full","entries":[{"cell_id":"matrix-recurrence-builtin-sm-full-n1-dd-z-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":1,"process":"d d~ > z","process_key":"dd_z_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-builtin-sm-full-n1-ud-w-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":1,"process":"u d~ > w+","process_key":"ud_w_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-builtin-sm-full-n2-dd-epem-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":2,"process":"d d~ > e+ e-","process_key":"dd_epem_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-builtin-sm-full-n2-dd-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":2,"process":"d d~ > t t~","process_key":"dd_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-builtin-sm-full-n2-dd-z-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":2,"process":"d d~ > z g","process_key":"dd_z_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-builtin-sm-full-n2-dd-zz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":2,"process":"d d~ > z z","process_key":"dd_zz_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-builtin-sm-full-n2-gg-gluons-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":2,"process":"g g > g g","process_key":"gg_gluons","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-builtin-sm-full-n2-gg-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":2,"process":"g g > t t~","process_key":"gg_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-builtin-sm-full-n2-ud-epve-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":2,"process":"u d~ > e+ ve","process_key":"ud_epve_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-builtin-sm-full-n2-ud-w-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":2,"process":"u d~ > w+ g","process_key":"ud_w_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-builtin-sm-full-n3-dd-epem-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"d d~ > e+ e- g","process_key":"dd_epem_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-builtin-sm-full-n3-dd-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"d d~ > t t~ g","process_key":"dd_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-builtin-sm-full-n3-dd-z-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"d d~ > z g g","process_key":"dd_z_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-builtin-sm-full-n3-dd-zz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"d d~ > z z g","process_key":"dd_zz_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-builtin-sm-full-n3-dd-zzz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"d d~ > z z z","process_key":"dd_zzz_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-builtin-sm-full-n3-gg-gluons-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"g g > g g g","process_key":"gg_gluons","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-builtin-sm-full-n3-gg-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"g g > t t~ g","process_key":"gg_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-builtin-sm-full-n3-ud-epve-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"u d~ > e+ ve g","process_key":"ud_epve_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-builtin-sm-full-n3-ud-w-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"u d~ > w+ g g","process_key":"ud_w_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-builtin-sm-full-n4-dd-3q-identical-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > u u~ u u~","process_key":"dd_3q_identical_lines","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-builtin-sm-full-n4-dd-3q-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > u u~ s s~","process_key":"dd_3q_lines","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-builtin-sm-full-n4-dd-4l-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > e+ e- e+ e-","process_key":"dd_4l_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-builtin-sm-full-n4-dd-epem-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > e+ e- g g","process_key":"dd_epem_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-builtin-sm-full-n4-dd-epemzh-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > e+ e- z h","process_key":"dd_epemzh_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-builtin-sm-full-n4-dd-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > t t~ g g","process_key":"dd_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-builtin-sm-full-n4-dd-ttzh-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > t t~ z h","process_key":"dd_ttzh_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-builtin-sm-full-n4-dd-z-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > z g g g","process_key":"dd_z_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-builtin-sm-full-n4-dd-zz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > z z g g","process_key":"dd_zz_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-builtin-sm-full-n4-dd-zzz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > z z z g","process_key":"dd_zzz_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-builtin-sm-full-n4-gg-gluons-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"g g > g g g g","process_key":"gg_gluons","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-builtin-sm-full-n4-gg-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"g g > t t~ g g","process_key":"gg_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-builtin-sm-full-n4-ud-epve-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"u d~ > e+ ve g g","process_key":"ud_epve_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-builtin-sm-full-n4-ud-w-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"u d~ > w+ g g g","process_key":"ud_w_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-builtin-sm-full-n5-dd-3q-identical-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"d d~ > u u~ u u~ g","process_key":"dd_3q_identical_lines","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-builtin-sm-full-n5-dd-3q-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"d d~ > u u~ s s~ g","process_key":"dd_3q_lines","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-builtin-sm-full-n5-dd-4l-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"d d~ > e+ e- e+ e- g","process_key":"dd_4l_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-builtin-sm-full-n5-dd-epem-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"d d~ > e+ e- g g g","process_key":"dd_epem_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-builtin-sm-full-n5-dd-epemzh-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"d d~ > e+ e- z h g","process_key":"dd_epemzh_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-builtin-sm-full-n5-dd-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"d d~ > t t~ g g g","process_key":"dd_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-builtin-sm-full-n5-dd-ttzh-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"d d~ > t t~ z h g","process_key":"dd_ttzh_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-builtin-sm-full-n5-dd-z-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"d d~ > z g g g g","process_key":"dd_z_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-builtin-sm-full-n5-dd-zz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"d d~ > z z g g g","process_key":"dd_zz_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-builtin-sm-full-n5-dd-zzz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"d d~ > z z z g g","process_key":"dd_zzz_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-builtin-sm-full-n5-gg-gluons-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"g g > g g g g g","process_key":"gg_gluons","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-builtin-sm-full-n5-gg-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"g g > t t~ g g g","process_key":"gg_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-builtin-sm-full-n5-ud-epve-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"u d~ > e+ ve g g g","process_key":"ud_epve_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-builtin-sm-full-n5-ud-w-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"u d~ > w+ g g g g","process_key":"ud_w_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-builtin-sm-full-n6-dd-3q-identical-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":6,"process":"d d~ > u u~ u u~ g g","process_key":"dd_3q_identical_lines","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-builtin-sm-full-n6-dd-3q-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":6,"process":"d d~ > u u~ s s~ g g","process_key":"dd_3q_lines","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-builtin-sm-full-n6-dd-4q-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":6,"process":"d d~ > u u~ s s~ c c~","process_key":"dd_4q_lines","variant":null,"workload":"contracted"}],"kind":"performance_measurements","measurement_specs":[{"accuracy":"full","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"}],"report_version":"0.3.0","schema_version":5} diff --git a/pyamplicol/src/pyamplicol/_profiling_campaign/results/matrix_recurrence_builtin_sm_lc.json b/pyamplicol/src/pyamplicol/_profiling_campaign/results/matrix_recurrence_builtin_sm_lc.json new file mode 100644 index 0000000000..7fc6491610 --- /dev/null +++ b/pyamplicol/src/pyamplicol/_profiling_campaign/results/matrix_recurrence_builtin_sm_lc.json @@ -0,0 +1 @@ +{"$schema":"./report-cache.schema.json","dataset_id":"matrix_recurrence_builtin_sm_lc","entries":[{"cell_id":"matrix-recurrence-builtin-sm-lc-n1-dd-z-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":1,"process":"d d~ > z","process_key":"dd_z_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n1-dd-z-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":1,"process":"d d~ > z","process_key":"dd_z_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n1-ud-w-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":1,"process":"u d~ > w+","process_key":"ud_w_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n1-ud-w-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":1,"process":"u d~ > w+","process_key":"ud_w_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n2-dd-epem-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":2,"process":"d d~ > e+ e-","process_key":"dd_epem_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n2-dd-epem-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":2,"process":"d d~ > e+ e-","process_key":"dd_epem_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n2-dd-tt-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":2,"process":"d d~ > t t~","process_key":"dd_tt_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n2-dd-tt-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":2,"process":"d d~ > t t~","process_key":"dd_tt_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n2-dd-z-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":2,"process":"d d~ > z g","process_key":"dd_z_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n2-dd-z-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":2,"process":"d d~ > z g","process_key":"dd_z_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n2-dd-zz-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":2,"process":"d d~ > z z","process_key":"dd_zz_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n2-dd-zz-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":2,"process":"d d~ > z z","process_key":"dd_zz_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n2-gg-gluons-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":2,"process":"g g > g g","process_key":"gg_gluons","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n2-gg-gluons-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":2,"process":"g g > g g","process_key":"gg_gluons","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n2-gg-tt-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":2,"process":"g g > t t~","process_key":"gg_tt_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n2-gg-tt-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":2,"process":"g g > t t~","process_key":"gg_tt_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n2-ud-epve-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":2,"process":"u d~ > e+ ve","process_key":"ud_epve_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n2-ud-epve-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":2,"process":"u d~ > e+ ve","process_key":"ud_epve_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n2-ud-w-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":2,"process":"u d~ > w+ g","process_key":"ud_w_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n2-ud-w-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":2,"process":"u d~ > w+ g","process_key":"ud_w_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n3-dd-epem-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"d d~ > e+ e- g","process_key":"dd_epem_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n3-dd-epem-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"d d~ > e+ e- g","process_key":"dd_epem_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n3-dd-tt-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"d d~ > t t~ g","process_key":"dd_tt_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n3-dd-tt-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"d d~ > t t~ g","process_key":"dd_tt_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n3-dd-z-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"d d~ > z g g","process_key":"dd_z_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n3-dd-z-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"d d~ > z g g","process_key":"dd_z_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n3-dd-zz-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"d d~ > z z g","process_key":"dd_zz_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n3-dd-zz-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"d d~ > z z g","process_key":"dd_zz_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n3-dd-zzz-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"d d~ > z z z","process_key":"dd_zzz_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n3-dd-zzz-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"d d~ > z z z","process_key":"dd_zzz_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n3-gg-gluons-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"g g > g g g","process_key":"gg_gluons","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n3-gg-gluons-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"g g > g g g","process_key":"gg_gluons","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n3-gg-tt-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"g g > t t~ g","process_key":"gg_tt_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n3-gg-tt-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"g g > t t~ g","process_key":"gg_tt_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n3-ud-epve-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"u d~ > e+ ve g","process_key":"ud_epve_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n3-ud-epve-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"u d~ > e+ ve g","process_key":"ud_epve_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n3-ud-w-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"u d~ > w+ g g","process_key":"ud_w_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n3-ud-w-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"u d~ > w+ g g","process_key":"ud_w_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n4-dd-3q-identical-lines-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > u u~ u u~","process_key":"dd_3q_identical_lines","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n4-dd-3q-identical-lines-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > u u~ u u~","process_key":"dd_3q_identical_lines","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n4-dd-3q-lines-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > u u~ s s~","process_key":"dd_3q_lines","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n4-dd-3q-lines-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > u u~ s s~","process_key":"dd_3q_lines","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n4-dd-4l-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > e+ e- e+ e-","process_key":"dd_4l_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n4-dd-4l-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > e+ e- e+ e-","process_key":"dd_4l_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n4-dd-epem-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > e+ e- g g","process_key":"dd_epem_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n4-dd-epem-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > e+ e- g g","process_key":"dd_epem_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n4-dd-epemzh-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > e+ e- z h","process_key":"dd_epemzh_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n4-dd-epemzh-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > e+ e- z h","process_key":"dd_epemzh_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n4-dd-tt-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > t t~ g g","process_key":"dd_tt_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n4-dd-tt-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > t t~ g g","process_key":"dd_tt_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n4-dd-ttzh-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > t t~ z h","process_key":"dd_ttzh_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n4-dd-ttzh-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > t t~ z h","process_key":"dd_ttzh_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n4-dd-z-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > z g g g","process_key":"dd_z_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n4-dd-z-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > z g g g","process_key":"dd_z_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n4-dd-zz-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > z z g g","process_key":"dd_zz_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n4-dd-zz-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > z z g g","process_key":"dd_zz_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n4-dd-zzz-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > z z z g","process_key":"dd_zzz_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n4-dd-zzz-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > z z z g","process_key":"dd_zzz_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n4-gg-gluons-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"g g > g g g g","process_key":"gg_gluons","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n4-gg-gluons-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"g g > g g g g","process_key":"gg_gluons","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n4-gg-tt-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"g g > t t~ g g","process_key":"gg_tt_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n4-gg-tt-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"g g > t t~ g g","process_key":"gg_tt_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n4-ud-epve-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"u d~ > e+ ve g g","process_key":"ud_epve_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n4-ud-epve-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"u d~ > e+ ve g g","process_key":"ud_epve_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n4-ud-w-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"u d~ > w+ g g g","process_key":"ud_w_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n4-ud-w-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"u d~ > w+ g g g","process_key":"ud_w_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n5-dd-3q-identical-lines-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"d d~ > u u~ u u~ g","process_key":"dd_3q_identical_lines","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n5-dd-3q-identical-lines-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"d d~ > u u~ u u~ g","process_key":"dd_3q_identical_lines","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n5-dd-3q-lines-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"d d~ > u u~ s s~ g","process_key":"dd_3q_lines","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n5-dd-3q-lines-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"d d~ > u u~ s s~ g","process_key":"dd_3q_lines","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n5-dd-4l-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"d d~ > e+ e- e+ e- g","process_key":"dd_4l_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n5-dd-4l-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"d d~ > e+ e- e+ e- g","process_key":"dd_4l_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n5-dd-epem-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"d d~ > e+ e- g g g","process_key":"dd_epem_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n5-dd-epem-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"d d~ > e+ e- g g g","process_key":"dd_epem_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n5-dd-epemzh-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"d d~ > e+ e- z h g","process_key":"dd_epemzh_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n5-dd-epemzh-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"d d~ > e+ e- z h g","process_key":"dd_epemzh_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n5-dd-tt-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"d d~ > t t~ g g g","process_key":"dd_tt_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n5-dd-tt-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"d d~ > t t~ g g g","process_key":"dd_tt_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n5-dd-ttzh-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"d d~ > t t~ z h g","process_key":"dd_ttzh_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n5-dd-ttzh-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"d d~ > t t~ z h g","process_key":"dd_ttzh_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n5-dd-z-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"d d~ > z g g g g","process_key":"dd_z_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n5-dd-z-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"d d~ > z g g g g","process_key":"dd_z_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n5-dd-zz-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"d d~ > z z g g g","process_key":"dd_zz_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n5-dd-zz-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"d d~ > z z g g g","process_key":"dd_zz_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n5-dd-zzz-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"d d~ > z z z g g","process_key":"dd_zzz_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n5-dd-zzz-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"d d~ > z z z g g","process_key":"dd_zzz_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n5-gg-gluons-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"g g > g g g g g","process_key":"gg_gluons","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n5-gg-gluons-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"g g > g g g g g","process_key":"gg_gluons","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n5-gg-tt-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"g g > t t~ g g g","process_key":"gg_tt_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n5-gg-tt-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"g g > t t~ g g g","process_key":"gg_tt_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n5-ud-epve-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"u d~ > e+ ve g g g","process_key":"ud_epve_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n5-ud-epve-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"u d~ > e+ ve g g g","process_key":"ud_epve_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n5-ud-w-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"u d~ > w+ g g g g","process_key":"ud_w_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n5-ud-w-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"u d~ > w+ g g g g","process_key":"ud_w_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n6-dd-3q-identical-lines-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":6,"process":"d d~ > u u~ u u~ g g","process_key":"dd_3q_identical_lines","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n6-dd-3q-identical-lines-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":6,"process":"d d~ > u u~ u u~ g g","process_key":"dd_3q_identical_lines","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n6-dd-3q-lines-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":6,"process":"d d~ > u u~ s s~ g g","process_key":"dd_3q_lines","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n6-dd-3q-lines-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":6,"process":"d d~ > u u~ s s~ g g","process_key":"dd_3q_lines","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n6-dd-4l-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":6,"process":"d d~ > e+ e- e+ e- g g","process_key":"dd_4l_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n6-dd-4l-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":6,"process":"d d~ > e+ e- e+ e- g g","process_key":"dd_4l_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n6-dd-4q-lines-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":6,"process":"d d~ > u u~ s s~ c c~","process_key":"dd_4q_lines","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n6-dd-4q-lines-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":6,"process":"d d~ > u u~ s s~ c c~","process_key":"dd_4q_lines","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n6-dd-epem-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":6,"process":"d d~ > e+ e- g g g g","process_key":"dd_epem_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n6-dd-epem-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":6,"process":"d d~ > e+ e- g g g g","process_key":"dd_epem_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n6-dd-epemzh-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":6,"process":"d d~ > e+ e- z h g g","process_key":"dd_epemzh_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n6-dd-epemzh-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":6,"process":"d d~ > e+ e- z h g g","process_key":"dd_epemzh_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n6-dd-tt-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":6,"process":"d d~ > t t~ g g g g","process_key":"dd_tt_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n6-dd-tt-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":6,"process":"d d~ > t t~ g g g g","process_key":"dd_tt_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n6-dd-ttzh-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":6,"process":"d d~ > t t~ z h g g","process_key":"dd_ttzh_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n6-dd-ttzh-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":6,"process":"d d~ > t t~ z h g g","process_key":"dd_ttzh_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n6-dd-z-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":6,"process":"d d~ > z g g g g g","process_key":"dd_z_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n6-dd-z-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":6,"process":"d d~ > z g g g g g","process_key":"dd_z_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n6-dd-zz-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":6,"process":"d d~ > z z g g g g","process_key":"dd_zz_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n6-dd-zz-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":6,"process":"d d~ > z z g g g g","process_key":"dd_zz_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n6-dd-zzz-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":6,"process":"d d~ > z z z g g g","process_key":"dd_zzz_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n6-dd-zzz-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":6,"process":"d d~ > z z z g g g","process_key":"dd_zzz_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n6-gg-gluons-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":6,"process":"g g > g g g g g g","process_key":"gg_gluons","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n6-gg-gluons-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":6,"process":"g g > g g g g g g","process_key":"gg_gluons","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n6-gg-tt-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":6,"process":"g g > t t~ g g g g","process_key":"gg_tt_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n6-gg-tt-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":6,"process":"g g > t t~ g g g g","process_key":"gg_tt_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n6-ud-epve-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":6,"process":"u d~ > e+ ve g g g g","process_key":"ud_epve_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n6-ud-epve-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":6,"process":"u d~ > e+ ve g g g g","process_key":"ud_epve_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n6-ud-w-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":6,"process":"u d~ > w+ g g g g g","process_key":"ud_w_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n6-ud-w-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":6,"process":"u d~ > w+ g g g g g","process_key":"ud_w_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n7-dd-3q-identical-lines-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":7,"process":"d d~ > u u~ u u~ g g g","process_key":"dd_3q_identical_lines","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n7-dd-3q-identical-lines-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":7,"process":"d d~ > u u~ u u~ g g g","process_key":"dd_3q_identical_lines","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n7-dd-3q-lines-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":7,"process":"d d~ > u u~ s s~ g g g","process_key":"dd_3q_lines","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n7-dd-3q-lines-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":7,"process":"d d~ > u u~ s s~ g g g","process_key":"dd_3q_lines","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n7-dd-4l-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":7,"process":"d d~ > e+ e- e+ e- g g g","process_key":"dd_4l_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n7-dd-4l-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":7,"process":"d d~ > e+ e- e+ e- g g g","process_key":"dd_4l_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n7-dd-4q-lines-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":7,"process":"d d~ > u u~ s s~ c c~ g","process_key":"dd_4q_lines","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n7-dd-4q-lines-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":7,"process":"d d~ > u u~ s s~ c c~ g","process_key":"dd_4q_lines","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n7-dd-epem-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":7,"process":"d d~ > e+ e- g g g g g","process_key":"dd_epem_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n7-dd-epem-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":7,"process":"d d~ > e+ e- g g g g g","process_key":"dd_epem_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n7-dd-epemzh-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":7,"process":"d d~ > e+ e- z h g g g","process_key":"dd_epemzh_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n7-dd-epemzh-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":7,"process":"d d~ > e+ e- z h g g g","process_key":"dd_epemzh_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n7-dd-tt-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":7,"process":"d d~ > t t~ g g g g g","process_key":"dd_tt_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n7-dd-tt-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":7,"process":"d d~ > t t~ g g g g g","process_key":"dd_tt_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n7-dd-ttzh-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":7,"process":"d d~ > t t~ z h g g g","process_key":"dd_ttzh_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n7-dd-ttzh-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":7,"process":"d d~ > t t~ z h g g g","process_key":"dd_ttzh_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n7-dd-z-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":7,"process":"d d~ > z g g g g g g","process_key":"dd_z_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n7-dd-z-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":7,"process":"d d~ > z g g g g g g","process_key":"dd_z_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n7-dd-zz-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":7,"process":"d d~ > z z g g g g g","process_key":"dd_zz_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n7-dd-zz-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":7,"process":"d d~ > z z g g g g g","process_key":"dd_zz_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n7-dd-zzz-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":7,"process":"d d~ > z z z g g g g","process_key":"dd_zzz_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n7-dd-zzz-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":7,"process":"d d~ > z z z g g g g","process_key":"dd_zzz_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n7-gg-gluons-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":7,"process":"g g > g g g g g g g","process_key":"gg_gluons","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n7-gg-gluons-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":7,"process":"g g > g g g g g g g","process_key":"gg_gluons","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n7-gg-tt-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":7,"process":"g g > t t~ g g g g g","process_key":"gg_tt_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n7-gg-tt-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":7,"process":"g g > t t~ g g g g g","process_key":"gg_tt_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n7-ud-epve-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":7,"process":"u d~ > e+ ve g g g g g","process_key":"ud_epve_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n7-ud-epve-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":7,"process":"u d~ > e+ ve g g g g g","process_key":"ud_epve_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n7-ud-w-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":7,"process":"u d~ > w+ g g g g g g","process_key":"ud_w_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n7-ud-w-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":7,"process":"u d~ > w+ g g g g g g","process_key":"ud_w_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n8-dd-3q-identical-lines-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":8,"process":"d d~ > u u~ u u~ g g g g","process_key":"dd_3q_identical_lines","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n8-dd-3q-identical-lines-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":8,"process":"d d~ > u u~ u u~ g g g g","process_key":"dd_3q_identical_lines","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n8-dd-3q-lines-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":8,"process":"d d~ > u u~ s s~ g g g g","process_key":"dd_3q_lines","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n8-dd-3q-lines-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":8,"process":"d d~ > u u~ s s~ g g g g","process_key":"dd_3q_lines","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n8-dd-4l-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":8,"process":"d d~ > e+ e- e+ e- g g g g","process_key":"dd_4l_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n8-dd-4l-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":8,"process":"d d~ > e+ e- e+ e- g g g g","process_key":"dd_4l_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n8-dd-4q-lines-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":8,"process":"d d~ > u u~ s s~ c c~ g g","process_key":"dd_4q_lines","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n8-dd-4q-lines-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":8,"process":"d d~ > u u~ s s~ c c~ g g","process_key":"dd_4q_lines","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n8-dd-epem-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":8,"process":"d d~ > e+ e- g g g g g g","process_key":"dd_epem_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n8-dd-epem-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":8,"process":"d d~ > e+ e- g g g g g g","process_key":"dd_epem_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n8-dd-epemzh-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":8,"process":"d d~ > e+ e- z h g g g g","process_key":"dd_epemzh_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n8-dd-epemzh-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":8,"process":"d d~ > e+ e- z h g g g g","process_key":"dd_epemzh_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n8-dd-tt-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":8,"process":"d d~ > t t~ g g g g g g","process_key":"dd_tt_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n8-dd-tt-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":8,"process":"d d~ > t t~ g g g g g g","process_key":"dd_tt_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n8-dd-ttzh-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":8,"process":"d d~ > t t~ z h g g g g","process_key":"dd_ttzh_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n8-dd-ttzh-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":8,"process":"d d~ > t t~ z h g g g g","process_key":"dd_ttzh_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n8-dd-z-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":8,"process":"d d~ > z g g g g g g g","process_key":"dd_z_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n8-dd-z-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":8,"process":"d d~ > z g g g g g g g","process_key":"dd_z_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n8-dd-zz-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":8,"process":"d d~ > z z g g g g g g","process_key":"dd_zz_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n8-dd-zz-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":8,"process":"d d~ > z z g g g g g g","process_key":"dd_zz_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n8-dd-zzz-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":8,"process":"d d~ > z z z g g g g g","process_key":"dd_zzz_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n8-dd-zzz-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":8,"process":"d d~ > z z z g g g g g","process_key":"dd_zzz_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n8-gg-gluons-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":8,"process":"g g > g g g g g g g g","process_key":"gg_gluons","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n8-gg-gluons-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":8,"process":"g g > g g g g g g g g","process_key":"gg_gluons","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n8-gg-tt-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":8,"process":"g g > t t~ g g g g g g","process_key":"gg_tt_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n8-gg-tt-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":8,"process":"g g > t t~ g g g g g g","process_key":"gg_tt_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n8-ud-epve-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":8,"process":"u d~ > e+ ve g g g g g g","process_key":"ud_epve_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n8-ud-epve-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":8,"process":"u d~ > e+ ve g g g g g g","process_key":"ud_epve_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n8-ud-w-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":8,"process":"u d~ > w+ g g g g g g g","process_key":"ud_w_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n8-ud-w-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":8,"process":"u d~ > w+ g g g g g g g","process_key":"ud_w_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n9-dd-3q-identical-lines-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":9,"process":"d d~ > u u~ u u~ g g g g g","process_key":"dd_3q_identical_lines","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n9-dd-3q-identical-lines-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":9,"process":"d d~ > u u~ u u~ g g g g g","process_key":"dd_3q_identical_lines","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n9-dd-3q-lines-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":9,"process":"d d~ > u u~ s s~ g g g g g","process_key":"dd_3q_lines","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n9-dd-3q-lines-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":9,"process":"d d~ > u u~ s s~ g g g g g","process_key":"dd_3q_lines","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n9-dd-4l-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":9,"process":"d d~ > e+ e- e+ e- g g g g g","process_key":"dd_4l_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n9-dd-4l-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":9,"process":"d d~ > e+ e- e+ e- g g g g g","process_key":"dd_4l_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n9-dd-4q-lines-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":9,"process":"d d~ > u u~ s s~ c c~ g g g","process_key":"dd_4q_lines","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n9-dd-4q-lines-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":9,"process":"d d~ > u u~ s s~ c c~ g g g","process_key":"dd_4q_lines","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n9-dd-epem-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":9,"process":"d d~ > e+ e- g g g g g g g","process_key":"dd_epem_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n9-dd-epem-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":9,"process":"d d~ > e+ e- g g g g g g g","process_key":"dd_epem_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n9-dd-epemzh-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":9,"process":"d d~ > e+ e- z h g g g g g","process_key":"dd_epemzh_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n9-dd-epemzh-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":9,"process":"d d~ > e+ e- z h g g g g g","process_key":"dd_epemzh_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n9-dd-tt-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":9,"process":"d d~ > t t~ g g g g g g g","process_key":"dd_tt_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n9-dd-tt-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":9,"process":"d d~ > t t~ g g g g g g g","process_key":"dd_tt_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n9-dd-ttzh-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":9,"process":"d d~ > t t~ z h g g g g g","process_key":"dd_ttzh_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n9-dd-ttzh-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":9,"process":"d d~ > t t~ z h g g g g g","process_key":"dd_ttzh_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n9-dd-z-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":9,"process":"d d~ > z g g g g g g g g","process_key":"dd_z_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n9-dd-z-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":9,"process":"d d~ > z g g g g g g g g","process_key":"dd_z_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n9-dd-zz-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":9,"process":"d d~ > z z g g g g g g g","process_key":"dd_zz_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n9-dd-zz-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":9,"process":"d d~ > z z g g g g g g g","process_key":"dd_zz_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n9-dd-zzz-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":9,"process":"d d~ > z z z g g g g g g","process_key":"dd_zzz_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n9-dd-zzz-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":9,"process":"d d~ > z z z g g g g g g","process_key":"dd_zzz_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n9-gg-gluons-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":9,"process":"g g > g g g g g g g g g","process_key":"gg_gluons","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n9-gg-gluons-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":9,"process":"g g > g g g g g g g g g","process_key":"gg_gluons","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n9-gg-tt-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":9,"process":"g g > t t~ g g g g g g g","process_key":"gg_tt_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n9-gg-tt-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":9,"process":"g g > t t~ g g g g g g g","process_key":"gg_tt_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n9-ud-epve-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":9,"process":"u d~ > e+ ve g g g g g g g","process_key":"ud_epve_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n9-ud-epve-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":9,"process":"u d~ > e+ ve g g g g g g g","process_key":"ud_epve_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n9-ud-w-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":9,"process":"u d~ > w+ g g g g g g g g","process_key":"ud_w_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-builtin-sm-lc-n9-ud-w-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":9,"process":"u d~ > w+ g g g g g g g g","process_key":"ud_w_jets","variant":null,"workload":"selected-flow"}],"kind":"performance_measurements","measurement_specs":[{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"}],"report_version":"0.3.0","schema_version":5} diff --git a/pyamplicol/src/pyamplicol/_profiling_campaign/results/matrix_recurrence_builtin_sm_nlc.json b/pyamplicol/src/pyamplicol/_profiling_campaign/results/matrix_recurrence_builtin_sm_nlc.json new file mode 100644 index 0000000000..c89f92b4d3 --- /dev/null +++ b/pyamplicol/src/pyamplicol/_profiling_campaign/results/matrix_recurrence_builtin_sm_nlc.json @@ -0,0 +1 @@ +{"$schema":"./report-cache.schema.json","dataset_id":"matrix_recurrence_builtin_sm_nlc","entries":[{"cell_id":"matrix-recurrence-builtin-sm-nlc-n1-dd-z-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":1,"process":"d d~ > z","process_key":"dd_z_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-builtin-sm-nlc-n1-ud-w-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":1,"process":"u d~ > w+","process_key":"ud_w_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-builtin-sm-nlc-n2-dd-epem-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":2,"process":"d d~ > e+ e-","process_key":"dd_epem_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-builtin-sm-nlc-n2-dd-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":2,"process":"d d~ > t t~","process_key":"dd_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-builtin-sm-nlc-n2-dd-z-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":2,"process":"d d~ > z g","process_key":"dd_z_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-builtin-sm-nlc-n2-dd-zz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":2,"process":"d d~ > z z","process_key":"dd_zz_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-builtin-sm-nlc-n2-gg-gluons-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":2,"process":"g g > g g","process_key":"gg_gluons","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-builtin-sm-nlc-n2-gg-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":2,"process":"g g > t t~","process_key":"gg_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-builtin-sm-nlc-n2-ud-epve-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":2,"process":"u d~ > e+ ve","process_key":"ud_epve_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-builtin-sm-nlc-n2-ud-w-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":2,"process":"u d~ > w+ g","process_key":"ud_w_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-builtin-sm-nlc-n3-dd-epem-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"d d~ > e+ e- g","process_key":"dd_epem_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-builtin-sm-nlc-n3-dd-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"d d~ > t t~ g","process_key":"dd_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-builtin-sm-nlc-n3-dd-z-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"d d~ > z g g","process_key":"dd_z_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-builtin-sm-nlc-n3-dd-zz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"d d~ > z z g","process_key":"dd_zz_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-builtin-sm-nlc-n3-dd-zzz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"d d~ > z z z","process_key":"dd_zzz_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-builtin-sm-nlc-n3-gg-gluons-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"g g > g g g","process_key":"gg_gluons","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-builtin-sm-nlc-n3-gg-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"g g > t t~ g","process_key":"gg_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-builtin-sm-nlc-n3-ud-epve-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"u d~ > e+ ve g","process_key":"ud_epve_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-builtin-sm-nlc-n3-ud-w-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"u d~ > w+ g g","process_key":"ud_w_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-builtin-sm-nlc-n4-dd-3q-identical-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > u u~ u u~","process_key":"dd_3q_identical_lines","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-builtin-sm-nlc-n4-dd-3q-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > u u~ s s~","process_key":"dd_3q_lines","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-builtin-sm-nlc-n4-dd-4l-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > e+ e- e+ e-","process_key":"dd_4l_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-builtin-sm-nlc-n4-dd-epem-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > e+ e- g g","process_key":"dd_epem_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-builtin-sm-nlc-n4-dd-epemzh-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > e+ e- z h","process_key":"dd_epemzh_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-builtin-sm-nlc-n4-dd-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > t t~ g g","process_key":"dd_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-builtin-sm-nlc-n4-dd-ttzh-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > t t~ z h","process_key":"dd_ttzh_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-builtin-sm-nlc-n4-dd-z-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > z g g g","process_key":"dd_z_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-builtin-sm-nlc-n4-dd-zz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > z z g g","process_key":"dd_zz_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-builtin-sm-nlc-n4-dd-zzz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > z z z g","process_key":"dd_zzz_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-builtin-sm-nlc-n4-gg-gluons-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"g g > g g g g","process_key":"gg_gluons","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-builtin-sm-nlc-n4-gg-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"g g > t t~ g g","process_key":"gg_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-builtin-sm-nlc-n4-ud-epve-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"u d~ > e+ ve g g","process_key":"ud_epve_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-builtin-sm-nlc-n4-ud-w-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"u d~ > w+ g g g","process_key":"ud_w_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-builtin-sm-nlc-n5-dd-3q-identical-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"d d~ > u u~ u u~ g","process_key":"dd_3q_identical_lines","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-builtin-sm-nlc-n5-dd-3q-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"d d~ > u u~ s s~ g","process_key":"dd_3q_lines","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-builtin-sm-nlc-n5-dd-4l-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"d d~ > e+ e- e+ e- g","process_key":"dd_4l_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-builtin-sm-nlc-n5-dd-epem-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"d d~ > e+ e- g g g","process_key":"dd_epem_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-builtin-sm-nlc-n5-dd-epemzh-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"d d~ > e+ e- z h g","process_key":"dd_epemzh_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-builtin-sm-nlc-n5-dd-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"d d~ > t t~ g g g","process_key":"dd_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-builtin-sm-nlc-n5-dd-ttzh-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"d d~ > t t~ z h g","process_key":"dd_ttzh_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-builtin-sm-nlc-n5-dd-z-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"d d~ > z g g g g","process_key":"dd_z_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-builtin-sm-nlc-n5-dd-zz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"d d~ > z z g g g","process_key":"dd_zz_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-builtin-sm-nlc-n5-dd-zzz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"d d~ > z z z g g","process_key":"dd_zzz_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-builtin-sm-nlc-n5-gg-gluons-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"g g > g g g g g","process_key":"gg_gluons","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-builtin-sm-nlc-n5-gg-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"g g > t t~ g g g","process_key":"gg_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-builtin-sm-nlc-n5-ud-epve-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"u d~ > e+ ve g g g","process_key":"ud_epve_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-builtin-sm-nlc-n5-ud-w-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"u d~ > w+ g g g g","process_key":"ud_w_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-builtin-sm-nlc-n6-dd-3q-identical-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":6,"process":"d d~ > u u~ u u~ g g","process_key":"dd_3q_identical_lines","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-builtin-sm-nlc-n6-dd-3q-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":6,"process":"d d~ > u u~ s s~ g g","process_key":"dd_3q_lines","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-builtin-sm-nlc-n6-dd-4q-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":6,"process":"d d~ > u u~ s s~ c c~","process_key":"dd_4q_lines","variant":null,"workload":"contracted"}],"kind":"performance_measurements","measurement_specs":[{"accuracy":"nlc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"}],"report_version":"0.3.0","schema_version":5} diff --git a/pyamplicol/src/pyamplicol/_profiling_campaign/results/matrix_recurrence_ufo_sm_full.json b/pyamplicol/src/pyamplicol/_profiling_campaign/results/matrix_recurrence_ufo_sm_full.json new file mode 100644 index 0000000000..2c9182fb76 --- /dev/null +++ b/pyamplicol/src/pyamplicol/_profiling_campaign/results/matrix_recurrence_ufo_sm_full.json @@ -0,0 +1 @@ +{"$schema":"./report-cache.schema.json","dataset_id":"matrix_recurrence_ufo_sm_full","entries":[{"cell_id":"matrix-recurrence-ufo-sm-full-n1-dd-z-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":1,"process":"d d~ > z","process_key":"dd_z_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-ufo-sm-full-n1-ud-w-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":1,"process":"u d~ > w+","process_key":"ud_w_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-ufo-sm-full-n2-dd-epem-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":2,"process":"d d~ > e+ e-","process_key":"dd_epem_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-ufo-sm-full-n2-dd-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":2,"process":"d d~ > t t~","process_key":"dd_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-ufo-sm-full-n2-dd-z-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":2,"process":"d d~ > z g","process_key":"dd_z_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-ufo-sm-full-n2-dd-zz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":2,"process":"d d~ > z z","process_key":"dd_zz_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-ufo-sm-full-n2-gg-gluons-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":2,"process":"g g > g g","process_key":"gg_gluons","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-ufo-sm-full-n2-gg-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":2,"process":"g g > t t~","process_key":"gg_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-ufo-sm-full-n2-ud-epve-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":2,"process":"u d~ > e+ ve","process_key":"ud_epve_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-ufo-sm-full-n2-ud-w-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":2,"process":"u d~ > w+ g","process_key":"ud_w_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-ufo-sm-full-n3-dd-epem-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":3,"process":"d d~ > e+ e- g","process_key":"dd_epem_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-ufo-sm-full-n3-dd-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":3,"process":"d d~ > t t~ g","process_key":"dd_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-ufo-sm-full-n3-dd-z-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":3,"process":"d d~ > z g g","process_key":"dd_z_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-ufo-sm-full-n3-dd-zz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":3,"process":"d d~ > z z g","process_key":"dd_zz_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-ufo-sm-full-n3-dd-zzz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":3,"process":"d d~ > z z z","process_key":"dd_zzz_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-ufo-sm-full-n3-gg-gluons-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":3,"process":"g g > g g g","process_key":"gg_gluons","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-ufo-sm-full-n3-gg-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":3,"process":"g g > t t~ g","process_key":"gg_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-ufo-sm-full-n3-ud-epve-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":3,"process":"u d~ > e+ ve g","process_key":"ud_epve_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-ufo-sm-full-n3-ud-w-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":3,"process":"u d~ > w+ g g","process_key":"ud_w_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-ufo-sm-full-n4-dd-3q-identical-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":4,"process":"d d~ > u u~ u u~","process_key":"dd_3q_identical_lines","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-ufo-sm-full-n4-dd-3q-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":4,"process":"d d~ > u u~ s s~","process_key":"dd_3q_lines","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-ufo-sm-full-n4-dd-4l-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":4,"process":"d d~ > e+ e- e+ e-","process_key":"dd_4l_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-ufo-sm-full-n4-dd-epem-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":4,"process":"d d~ > e+ e- g g","process_key":"dd_epem_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-ufo-sm-full-n4-dd-epemzh-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":4,"process":"d d~ > e+ e- z h","process_key":"dd_epemzh_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-ufo-sm-full-n4-dd-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":4,"process":"d d~ > t t~ g g","process_key":"dd_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-ufo-sm-full-n4-dd-ttzh-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":4,"process":"d d~ > t t~ z h","process_key":"dd_ttzh_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-ufo-sm-full-n4-dd-z-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":4,"process":"d d~ > z g g g","process_key":"dd_z_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-ufo-sm-full-n4-dd-zz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":4,"process":"d d~ > z z g g","process_key":"dd_zz_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-ufo-sm-full-n4-dd-zzz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":4,"process":"d d~ > z z z g","process_key":"dd_zzz_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-ufo-sm-full-n4-gg-gluons-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":4,"process":"g g > g g g g","process_key":"gg_gluons","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-ufo-sm-full-n4-gg-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":4,"process":"g g > t t~ g g","process_key":"gg_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-ufo-sm-full-n4-ud-epve-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":4,"process":"u d~ > e+ ve g g","process_key":"ud_epve_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-ufo-sm-full-n4-ud-w-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":4,"process":"u d~ > w+ g g g","process_key":"ud_w_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-ufo-sm-full-n5-dd-3q-identical-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":5,"process":"d d~ > u u~ u u~ g","process_key":"dd_3q_identical_lines","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-ufo-sm-full-n5-dd-3q-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":5,"process":"d d~ > u u~ s s~ g","process_key":"dd_3q_lines","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-ufo-sm-full-n5-dd-4l-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":5,"process":"d d~ > e+ e- e+ e- g","process_key":"dd_4l_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-ufo-sm-full-n5-dd-epem-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":5,"process":"d d~ > e+ e- g g g","process_key":"dd_epem_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-ufo-sm-full-n5-dd-epemzh-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":5,"process":"d d~ > e+ e- z h g","process_key":"dd_epemzh_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-ufo-sm-full-n5-dd-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":5,"process":"d d~ > t t~ g g g","process_key":"dd_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-ufo-sm-full-n5-dd-ttzh-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":5,"process":"d d~ > t t~ z h g","process_key":"dd_ttzh_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-ufo-sm-full-n5-dd-z-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":5,"process":"d d~ > z g g g g","process_key":"dd_z_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-ufo-sm-full-n5-dd-zz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":5,"process":"d d~ > z z g g g","process_key":"dd_zz_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-ufo-sm-full-n5-dd-zzz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":5,"process":"d d~ > z z z g g","process_key":"dd_zzz_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-ufo-sm-full-n5-gg-gluons-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":5,"process":"g g > g g g g g","process_key":"gg_gluons","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-ufo-sm-full-n5-gg-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":5,"process":"g g > t t~ g g g","process_key":"gg_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-ufo-sm-full-n5-ud-epve-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":5,"process":"u d~ > e+ ve g g g","process_key":"ud_epve_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-ufo-sm-full-n5-ud-w-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":5,"process":"u d~ > w+ g g g g","process_key":"ud_w_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-ufo-sm-full-n6-dd-3q-identical-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":6,"process":"d d~ > u u~ u u~ g g","process_key":"dd_3q_identical_lines","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-ufo-sm-full-n6-dd-3q-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":6,"process":"d d~ > u u~ s s~ g g","process_key":"dd_3q_lines","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-ufo-sm-full-n6-dd-4q-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":6,"process":"d d~ > u u~ s s~ c c~","process_key":"dd_4q_lines","variant":null,"workload":"contracted"}],"kind":"performance_measurements","measurement_specs":[{"accuracy":"full","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"}],"report_version":"0.3.0","schema_version":5} diff --git a/pyamplicol/src/pyamplicol/_profiling_campaign/results/matrix_recurrence_ufo_sm_lc.json b/pyamplicol/src/pyamplicol/_profiling_campaign/results/matrix_recurrence_ufo_sm_lc.json new file mode 100644 index 0000000000..de72771e12 --- /dev/null +++ b/pyamplicol/src/pyamplicol/_profiling_campaign/results/matrix_recurrence_ufo_sm_lc.json @@ -0,0 +1 @@ +{"$schema":"./report-cache.schema.json","dataset_id":"matrix_recurrence_ufo_sm_lc","entries":[{"cell_id":"matrix-recurrence-ufo-sm-lc-n1-dd-z-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":1,"process":"d d~ > z","process_key":"dd_z_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n1-dd-z-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":1,"process":"d d~ > z","process_key":"dd_z_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n1-ud-w-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":1,"process":"u d~ > w+","process_key":"ud_w_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n1-ud-w-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":1,"process":"u d~ > w+","process_key":"ud_w_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n2-dd-epem-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":2,"process":"d d~ > e+ e-","process_key":"dd_epem_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n2-dd-epem-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":2,"process":"d d~ > e+ e-","process_key":"dd_epem_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n2-dd-tt-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":2,"process":"d d~ > t t~","process_key":"dd_tt_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n2-dd-tt-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":2,"process":"d d~ > t t~","process_key":"dd_tt_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n2-dd-z-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":2,"process":"d d~ > z g","process_key":"dd_z_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n2-dd-z-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":2,"process":"d d~ > z g","process_key":"dd_z_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n2-dd-zz-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":2,"process":"d d~ > z z","process_key":"dd_zz_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n2-dd-zz-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":2,"process":"d d~ > z z","process_key":"dd_zz_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n2-gg-gluons-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":2,"process":"g g > g g","process_key":"gg_gluons","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n2-gg-gluons-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":2,"process":"g g > g g","process_key":"gg_gluons","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n2-gg-tt-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":2,"process":"g g > t t~","process_key":"gg_tt_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n2-gg-tt-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":2,"process":"g g > t t~","process_key":"gg_tt_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n2-ud-epve-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":2,"process":"u d~ > e+ ve","process_key":"ud_epve_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n2-ud-epve-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":2,"process":"u d~ > e+ ve","process_key":"ud_epve_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n2-ud-w-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":2,"process":"u d~ > w+ g","process_key":"ud_w_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n2-ud-w-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":2,"process":"u d~ > w+ g","process_key":"ud_w_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n3-dd-epem-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":3,"process":"d d~ > e+ e- g","process_key":"dd_epem_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n3-dd-epem-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":3,"process":"d d~ > e+ e- g","process_key":"dd_epem_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n3-dd-tt-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":3,"process":"d d~ > t t~ g","process_key":"dd_tt_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n3-dd-tt-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":3,"process":"d d~ > t t~ g","process_key":"dd_tt_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n3-dd-z-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":3,"process":"d d~ > z g g","process_key":"dd_z_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n3-dd-z-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":3,"process":"d d~ > z g g","process_key":"dd_z_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n3-dd-zz-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":3,"process":"d d~ > z z g","process_key":"dd_zz_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n3-dd-zz-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":3,"process":"d d~ > z z g","process_key":"dd_zz_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n3-dd-zzz-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":3,"process":"d d~ > z z z","process_key":"dd_zzz_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n3-dd-zzz-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":3,"process":"d d~ > z z z","process_key":"dd_zzz_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n3-gg-gluons-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":3,"process":"g g > g g g","process_key":"gg_gluons","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n3-gg-gluons-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":3,"process":"g g > g g g","process_key":"gg_gluons","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n3-gg-tt-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":3,"process":"g g > t t~ g","process_key":"gg_tt_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n3-gg-tt-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":3,"process":"g g > t t~ g","process_key":"gg_tt_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n3-ud-epve-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":3,"process":"u d~ > e+ ve g","process_key":"ud_epve_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n3-ud-epve-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":3,"process":"u d~ > e+ ve g","process_key":"ud_epve_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n3-ud-w-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":3,"process":"u d~ > w+ g g","process_key":"ud_w_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n3-ud-w-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":3,"process":"u d~ > w+ g g","process_key":"ud_w_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n4-dd-3q-identical-lines-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":4,"process":"d d~ > u u~ u u~","process_key":"dd_3q_identical_lines","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n4-dd-3q-identical-lines-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":4,"process":"d d~ > u u~ u u~","process_key":"dd_3q_identical_lines","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n4-dd-3q-lines-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":4,"process":"d d~ > u u~ s s~","process_key":"dd_3q_lines","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n4-dd-3q-lines-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":4,"process":"d d~ > u u~ s s~","process_key":"dd_3q_lines","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n4-dd-4l-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":4,"process":"d d~ > e+ e- e+ e-","process_key":"dd_4l_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n4-dd-4l-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":4,"process":"d d~ > e+ e- e+ e-","process_key":"dd_4l_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n4-dd-epem-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":4,"process":"d d~ > e+ e- g g","process_key":"dd_epem_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n4-dd-epem-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":4,"process":"d d~ > e+ e- g g","process_key":"dd_epem_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n4-dd-epemzh-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":4,"process":"d d~ > e+ e- z h","process_key":"dd_epemzh_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n4-dd-epemzh-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":4,"process":"d d~ > e+ e- z h","process_key":"dd_epemzh_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n4-dd-tt-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":4,"process":"d d~ > t t~ g g","process_key":"dd_tt_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n4-dd-tt-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":4,"process":"d d~ > t t~ g g","process_key":"dd_tt_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n4-dd-ttzh-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":4,"process":"d d~ > t t~ z h","process_key":"dd_ttzh_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n4-dd-ttzh-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":4,"process":"d d~ > t t~ z h","process_key":"dd_ttzh_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n4-dd-z-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":4,"process":"d d~ > z g g g","process_key":"dd_z_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n4-dd-z-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":4,"process":"d d~ > z g g g","process_key":"dd_z_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n4-dd-zz-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":4,"process":"d d~ > z z g g","process_key":"dd_zz_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n4-dd-zz-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":4,"process":"d d~ > z z g g","process_key":"dd_zz_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n4-dd-zzz-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":4,"process":"d d~ > z z z g","process_key":"dd_zzz_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n4-dd-zzz-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":4,"process":"d d~ > z z z g","process_key":"dd_zzz_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n4-gg-gluons-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":4,"process":"g g > g g g g","process_key":"gg_gluons","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n4-gg-gluons-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":4,"process":"g g > g g g g","process_key":"gg_gluons","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n4-gg-tt-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":4,"process":"g g > t t~ g g","process_key":"gg_tt_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n4-gg-tt-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":4,"process":"g g > t t~ g g","process_key":"gg_tt_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n4-ud-epve-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":4,"process":"u d~ > e+ ve g g","process_key":"ud_epve_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n4-ud-epve-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":4,"process":"u d~ > e+ ve g g","process_key":"ud_epve_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n4-ud-w-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":4,"process":"u d~ > w+ g g g","process_key":"ud_w_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n4-ud-w-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":4,"process":"u d~ > w+ g g g","process_key":"ud_w_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n5-dd-3q-identical-lines-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":5,"process":"d d~ > u u~ u u~ g","process_key":"dd_3q_identical_lines","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n5-dd-3q-identical-lines-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":5,"process":"d d~ > u u~ u u~ g","process_key":"dd_3q_identical_lines","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n5-dd-3q-lines-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":5,"process":"d d~ > u u~ s s~ g","process_key":"dd_3q_lines","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n5-dd-3q-lines-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":5,"process":"d d~ > u u~ s s~ g","process_key":"dd_3q_lines","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n5-dd-4l-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":5,"process":"d d~ > e+ e- e+ e- g","process_key":"dd_4l_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n5-dd-4l-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":5,"process":"d d~ > e+ e- e+ e- g","process_key":"dd_4l_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n5-dd-epem-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":5,"process":"d d~ > e+ e- g g g","process_key":"dd_epem_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n5-dd-epem-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":5,"process":"d d~ > e+ e- g g g","process_key":"dd_epem_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n5-dd-epemzh-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":5,"process":"d d~ > e+ e- z h g","process_key":"dd_epemzh_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n5-dd-epemzh-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":5,"process":"d d~ > e+ e- z h g","process_key":"dd_epemzh_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n5-dd-tt-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":5,"process":"d d~ > t t~ g g g","process_key":"dd_tt_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n5-dd-tt-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":5,"process":"d d~ > t t~ g g g","process_key":"dd_tt_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n5-dd-ttzh-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":5,"process":"d d~ > t t~ z h g","process_key":"dd_ttzh_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n5-dd-ttzh-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":5,"process":"d d~ > t t~ z h g","process_key":"dd_ttzh_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n5-dd-z-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":5,"process":"d d~ > z g g g g","process_key":"dd_z_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n5-dd-z-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":5,"process":"d d~ > z g g g g","process_key":"dd_z_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n5-dd-zz-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":5,"process":"d d~ > z z g g g","process_key":"dd_zz_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n5-dd-zz-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":5,"process":"d d~ > z z g g g","process_key":"dd_zz_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n5-dd-zzz-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":5,"process":"d d~ > z z z g g","process_key":"dd_zzz_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n5-dd-zzz-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":5,"process":"d d~ > z z z g g","process_key":"dd_zzz_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n5-gg-gluons-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":5,"process":"g g > g g g g g","process_key":"gg_gluons","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n5-gg-gluons-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":5,"process":"g g > g g g g g","process_key":"gg_gluons","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n5-gg-tt-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":5,"process":"g g > t t~ g g g","process_key":"gg_tt_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n5-gg-tt-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":5,"process":"g g > t t~ g g g","process_key":"gg_tt_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n5-ud-epve-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":5,"process":"u d~ > e+ ve g g g","process_key":"ud_epve_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n5-ud-epve-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":5,"process":"u d~ > e+ ve g g g","process_key":"ud_epve_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n5-ud-w-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":5,"process":"u d~ > w+ g g g g","process_key":"ud_w_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n5-ud-w-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":5,"process":"u d~ > w+ g g g g","process_key":"ud_w_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n6-dd-3q-identical-lines-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":6,"process":"d d~ > u u~ u u~ g g","process_key":"dd_3q_identical_lines","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n6-dd-3q-identical-lines-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":6,"process":"d d~ > u u~ u u~ g g","process_key":"dd_3q_identical_lines","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n6-dd-3q-lines-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":6,"process":"d d~ > u u~ s s~ g g","process_key":"dd_3q_lines","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n6-dd-3q-lines-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":6,"process":"d d~ > u u~ s s~ g g","process_key":"dd_3q_lines","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n6-dd-4l-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":6,"process":"d d~ > e+ e- e+ e- g g","process_key":"dd_4l_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n6-dd-4l-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":6,"process":"d d~ > e+ e- e+ e- g g","process_key":"dd_4l_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n6-dd-4q-lines-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":6,"process":"d d~ > u u~ s s~ c c~","process_key":"dd_4q_lines","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n6-dd-4q-lines-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":6,"process":"d d~ > u u~ s s~ c c~","process_key":"dd_4q_lines","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n6-dd-epem-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":6,"process":"d d~ > e+ e- g g g g","process_key":"dd_epem_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n6-dd-epem-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":6,"process":"d d~ > e+ e- g g g g","process_key":"dd_epem_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n6-dd-epemzh-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":6,"process":"d d~ > e+ e- z h g g","process_key":"dd_epemzh_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n6-dd-epemzh-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":6,"process":"d d~ > e+ e- z h g g","process_key":"dd_epemzh_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n6-dd-tt-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":6,"process":"d d~ > t t~ g g g g","process_key":"dd_tt_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n6-dd-tt-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":6,"process":"d d~ > t t~ g g g g","process_key":"dd_tt_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n6-dd-ttzh-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":6,"process":"d d~ > t t~ z h g g","process_key":"dd_ttzh_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n6-dd-ttzh-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":6,"process":"d d~ > t t~ z h g g","process_key":"dd_ttzh_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n6-dd-z-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":6,"process":"d d~ > z g g g g g","process_key":"dd_z_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n6-dd-z-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":6,"process":"d d~ > z g g g g g","process_key":"dd_z_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n6-dd-zz-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":6,"process":"d d~ > z z g g g g","process_key":"dd_zz_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n6-dd-zz-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":6,"process":"d d~ > z z g g g g","process_key":"dd_zz_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n6-dd-zzz-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":6,"process":"d d~ > z z z g g g","process_key":"dd_zzz_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n6-dd-zzz-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":6,"process":"d d~ > z z z g g g","process_key":"dd_zzz_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n6-gg-gluons-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":6,"process":"g g > g g g g g g","process_key":"gg_gluons","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n6-gg-gluons-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":6,"process":"g g > g g g g g g","process_key":"gg_gluons","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n6-gg-tt-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":6,"process":"g g > t t~ g g g g","process_key":"gg_tt_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n6-gg-tt-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":6,"process":"g g > t t~ g g g g","process_key":"gg_tt_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n6-ud-epve-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":6,"process":"u d~ > e+ ve g g g g","process_key":"ud_epve_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n6-ud-epve-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":6,"process":"u d~ > e+ ve g g g g","process_key":"ud_epve_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n6-ud-w-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":6,"process":"u d~ > w+ g g g g g","process_key":"ud_w_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n6-ud-w-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":6,"process":"u d~ > w+ g g g g g","process_key":"ud_w_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n7-dd-3q-identical-lines-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":7,"process":"d d~ > u u~ u u~ g g g","process_key":"dd_3q_identical_lines","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n7-dd-3q-identical-lines-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":7,"process":"d d~ > u u~ u u~ g g g","process_key":"dd_3q_identical_lines","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n7-dd-3q-lines-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":7,"process":"d d~ > u u~ s s~ g g g","process_key":"dd_3q_lines","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n7-dd-3q-lines-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":7,"process":"d d~ > u u~ s s~ g g g","process_key":"dd_3q_lines","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n7-dd-4l-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":7,"process":"d d~ > e+ e- e+ e- g g g","process_key":"dd_4l_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n7-dd-4l-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":7,"process":"d d~ > e+ e- e+ e- g g g","process_key":"dd_4l_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n7-dd-4q-lines-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":7,"process":"d d~ > u u~ s s~ c c~ g","process_key":"dd_4q_lines","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n7-dd-4q-lines-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":7,"process":"d d~ > u u~ s s~ c c~ g","process_key":"dd_4q_lines","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n7-dd-epem-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":7,"process":"d d~ > e+ e- g g g g g","process_key":"dd_epem_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n7-dd-epem-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":7,"process":"d d~ > e+ e- g g g g g","process_key":"dd_epem_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n7-dd-epemzh-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":7,"process":"d d~ > e+ e- z h g g g","process_key":"dd_epemzh_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n7-dd-epemzh-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":7,"process":"d d~ > e+ e- z h g g g","process_key":"dd_epemzh_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n7-dd-tt-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":7,"process":"d d~ > t t~ g g g g g","process_key":"dd_tt_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n7-dd-tt-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":7,"process":"d d~ > t t~ g g g g g","process_key":"dd_tt_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n7-dd-ttzh-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":7,"process":"d d~ > t t~ z h g g g","process_key":"dd_ttzh_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n7-dd-ttzh-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":7,"process":"d d~ > t t~ z h g g g","process_key":"dd_ttzh_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n7-dd-z-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":7,"process":"d d~ > z g g g g g g","process_key":"dd_z_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n7-dd-z-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":7,"process":"d d~ > z g g g g g g","process_key":"dd_z_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n7-dd-zz-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":7,"process":"d d~ > z z g g g g g","process_key":"dd_zz_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n7-dd-zz-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":7,"process":"d d~ > z z g g g g g","process_key":"dd_zz_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n7-dd-zzz-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":7,"process":"d d~ > z z z g g g g","process_key":"dd_zzz_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n7-dd-zzz-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":7,"process":"d d~ > z z z g g g g","process_key":"dd_zzz_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n7-gg-gluons-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":7,"process":"g g > g g g g g g g","process_key":"gg_gluons","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n7-gg-gluons-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":7,"process":"g g > g g g g g g g","process_key":"gg_gluons","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n7-gg-tt-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":7,"process":"g g > t t~ g g g g g","process_key":"gg_tt_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n7-gg-tt-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":7,"process":"g g > t t~ g g g g g","process_key":"gg_tt_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n7-ud-epve-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":7,"process":"u d~ > e+ ve g g g g g","process_key":"ud_epve_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n7-ud-epve-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":7,"process":"u d~ > e+ ve g g g g g","process_key":"ud_epve_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n7-ud-w-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":7,"process":"u d~ > w+ g g g g g g","process_key":"ud_w_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n7-ud-w-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":7,"process":"u d~ > w+ g g g g g g","process_key":"ud_w_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n8-dd-3q-identical-lines-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":8,"process":"d d~ > u u~ u u~ g g g g","process_key":"dd_3q_identical_lines","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n8-dd-3q-identical-lines-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":8,"process":"d d~ > u u~ u u~ g g g g","process_key":"dd_3q_identical_lines","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n8-dd-3q-lines-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":8,"process":"d d~ > u u~ s s~ g g g g","process_key":"dd_3q_lines","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n8-dd-3q-lines-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":8,"process":"d d~ > u u~ s s~ g g g g","process_key":"dd_3q_lines","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n8-dd-4l-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":8,"process":"d d~ > e+ e- e+ e- g g g g","process_key":"dd_4l_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n8-dd-4l-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":8,"process":"d d~ > e+ e- e+ e- g g g g","process_key":"dd_4l_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n8-dd-4q-lines-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":8,"process":"d d~ > u u~ s s~ c c~ g g","process_key":"dd_4q_lines","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n8-dd-4q-lines-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":8,"process":"d d~ > u u~ s s~ c c~ g g","process_key":"dd_4q_lines","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n8-dd-epem-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":8,"process":"d d~ > e+ e- g g g g g g","process_key":"dd_epem_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n8-dd-epem-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":8,"process":"d d~ > e+ e- g g g g g g","process_key":"dd_epem_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n8-dd-epemzh-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":8,"process":"d d~ > e+ e- z h g g g g","process_key":"dd_epemzh_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n8-dd-epemzh-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":8,"process":"d d~ > e+ e- z h g g g g","process_key":"dd_epemzh_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n8-dd-tt-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":8,"process":"d d~ > t t~ g g g g g g","process_key":"dd_tt_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n8-dd-tt-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":8,"process":"d d~ > t t~ g g g g g g","process_key":"dd_tt_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n8-dd-ttzh-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":8,"process":"d d~ > t t~ z h g g g g","process_key":"dd_ttzh_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n8-dd-ttzh-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":8,"process":"d d~ > t t~ z h g g g g","process_key":"dd_ttzh_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n8-dd-z-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":8,"process":"d d~ > z g g g g g g g","process_key":"dd_z_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n8-dd-z-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":8,"process":"d d~ > z g g g g g g g","process_key":"dd_z_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n8-dd-zz-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":8,"process":"d d~ > z z g g g g g g","process_key":"dd_zz_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n8-dd-zz-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":8,"process":"d d~ > z z g g g g g g","process_key":"dd_zz_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n8-dd-zzz-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":8,"process":"d d~ > z z z g g g g g","process_key":"dd_zzz_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n8-dd-zzz-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":8,"process":"d d~ > z z z g g g g g","process_key":"dd_zzz_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n8-gg-gluons-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":8,"process":"g g > g g g g g g g g","process_key":"gg_gluons","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n8-gg-gluons-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":8,"process":"g g > g g g g g g g g","process_key":"gg_gluons","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n8-gg-tt-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":8,"process":"g g > t t~ g g g g g g","process_key":"gg_tt_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n8-gg-tt-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":8,"process":"g g > t t~ g g g g g g","process_key":"gg_tt_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n8-ud-epve-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":8,"process":"u d~ > e+ ve g g g g g g","process_key":"ud_epve_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n8-ud-epve-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":8,"process":"u d~ > e+ ve g g g g g g","process_key":"ud_epve_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n8-ud-w-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":8,"process":"u d~ > w+ g g g g g g g","process_key":"ud_w_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n8-ud-w-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":8,"process":"u d~ > w+ g g g g g g g","process_key":"ud_w_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n9-dd-3q-identical-lines-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":9,"process":"d d~ > u u~ u u~ g g g g g","process_key":"dd_3q_identical_lines","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n9-dd-3q-identical-lines-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":9,"process":"d d~ > u u~ u u~ g g g g g","process_key":"dd_3q_identical_lines","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n9-dd-3q-lines-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":9,"process":"d d~ > u u~ s s~ g g g g g","process_key":"dd_3q_lines","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n9-dd-3q-lines-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":9,"process":"d d~ > u u~ s s~ g g g g g","process_key":"dd_3q_lines","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n9-dd-4l-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":9,"process":"d d~ > e+ e- e+ e- g g g g g","process_key":"dd_4l_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n9-dd-4l-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":9,"process":"d d~ > e+ e- e+ e- g g g g g","process_key":"dd_4l_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n9-dd-4q-lines-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":9,"process":"d d~ > u u~ s s~ c c~ g g g","process_key":"dd_4q_lines","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n9-dd-4q-lines-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":9,"process":"d d~ > u u~ s s~ c c~ g g g","process_key":"dd_4q_lines","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n9-dd-epem-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":9,"process":"d d~ > e+ e- g g g g g g g","process_key":"dd_epem_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n9-dd-epem-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":9,"process":"d d~ > e+ e- g g g g g g g","process_key":"dd_epem_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n9-dd-epemzh-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":9,"process":"d d~ > e+ e- z h g g g g g","process_key":"dd_epemzh_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n9-dd-epemzh-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":9,"process":"d d~ > e+ e- z h g g g g g","process_key":"dd_epemzh_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n9-dd-tt-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":9,"process":"d d~ > t t~ g g g g g g g","process_key":"dd_tt_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n9-dd-tt-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":9,"process":"d d~ > t t~ g g g g g g g","process_key":"dd_tt_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n9-dd-ttzh-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":9,"process":"d d~ > t t~ z h g g g g g","process_key":"dd_ttzh_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n9-dd-ttzh-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":9,"process":"d d~ > t t~ z h g g g g g","process_key":"dd_ttzh_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n9-dd-z-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":9,"process":"d d~ > z g g g g g g g g","process_key":"dd_z_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n9-dd-z-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":9,"process":"d d~ > z g g g g g g g g","process_key":"dd_z_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n9-dd-zz-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":9,"process":"d d~ > z z g g g g g g g","process_key":"dd_zz_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n9-dd-zz-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":9,"process":"d d~ > z z g g g g g g g","process_key":"dd_zz_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n9-dd-zzz-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":9,"process":"d d~ > z z z g g g g g g","process_key":"dd_zzz_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n9-dd-zzz-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":9,"process":"d d~ > z z z g g g g g g","process_key":"dd_zzz_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n9-gg-gluons-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":9,"process":"g g > g g g g g g g g g","process_key":"gg_gluons","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n9-gg-gluons-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":9,"process":"g g > g g g g g g g g g","process_key":"gg_gluons","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n9-gg-tt-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":9,"process":"g g > t t~ g g g g g g g","process_key":"gg_tt_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n9-gg-tt-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":9,"process":"g g > t t~ g g g g g g g","process_key":"gg_tt_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n9-ud-epve-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":9,"process":"u d~ > e+ ve g g g g g g g","process_key":"ud_epve_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n9-ud-epve-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":9,"process":"u d~ > e+ ve g g g g g g g","process_key":"ud_epve_jets","variant":null,"workload":"selected-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n9-ud-w-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":9,"process":"u d~ > w+ g g g g g g g g","process_key":"ud_w_jets","variant":null,"workload":"all-flow"},{"cell_id":"matrix-recurrence-ufo-sm-lc-n9-ud-w-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":9,"process":"u d~ > w+ g g g g g g g g","process_key":"ud_w_jets","variant":null,"workload":"selected-flow"}],"kind":"performance_measurements","measurement_specs":[{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"}],"report_version":"0.3.0","schema_version":5} diff --git a/pyamplicol/src/pyamplicol/_profiling_campaign/results/matrix_recurrence_ufo_sm_nlc.json b/pyamplicol/src/pyamplicol/_profiling_campaign/results/matrix_recurrence_ufo_sm_nlc.json new file mode 100644 index 0000000000..b7faa8ce38 --- /dev/null +++ b/pyamplicol/src/pyamplicol/_profiling_campaign/results/matrix_recurrence_ufo_sm_nlc.json @@ -0,0 +1 @@ +{"$schema":"./report-cache.schema.json","dataset_id":"matrix_recurrence_ufo_sm_nlc","entries":[{"cell_id":"matrix-recurrence-ufo-sm-nlc-n1-dd-z-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":1,"process":"d d~ > z","process_key":"dd_z_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-ufo-sm-nlc-n1-ud-w-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":1,"process":"u d~ > w+","process_key":"ud_w_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-ufo-sm-nlc-n2-dd-epem-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":2,"process":"d d~ > e+ e-","process_key":"dd_epem_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-ufo-sm-nlc-n2-dd-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":2,"process":"d d~ > t t~","process_key":"dd_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-ufo-sm-nlc-n2-dd-z-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":2,"process":"d d~ > z g","process_key":"dd_z_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-ufo-sm-nlc-n2-dd-zz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":2,"process":"d d~ > z z","process_key":"dd_zz_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-ufo-sm-nlc-n2-gg-gluons-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":2,"process":"g g > g g","process_key":"gg_gluons","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-ufo-sm-nlc-n2-gg-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":2,"process":"g g > t t~","process_key":"gg_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-ufo-sm-nlc-n2-ud-epve-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":2,"process":"u d~ > e+ ve","process_key":"ud_epve_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-ufo-sm-nlc-n2-ud-w-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":2,"process":"u d~ > w+ g","process_key":"ud_w_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-ufo-sm-nlc-n3-dd-epem-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":3,"process":"d d~ > e+ e- g","process_key":"dd_epem_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-ufo-sm-nlc-n3-dd-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":3,"process":"d d~ > t t~ g","process_key":"dd_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-ufo-sm-nlc-n3-dd-z-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":3,"process":"d d~ > z g g","process_key":"dd_z_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-ufo-sm-nlc-n3-dd-zz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":3,"process":"d d~ > z z g","process_key":"dd_zz_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-ufo-sm-nlc-n3-dd-zzz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":3,"process":"d d~ > z z z","process_key":"dd_zzz_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-ufo-sm-nlc-n3-gg-gluons-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":3,"process":"g g > g g g","process_key":"gg_gluons","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-ufo-sm-nlc-n3-gg-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":3,"process":"g g > t t~ g","process_key":"gg_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-ufo-sm-nlc-n3-ud-epve-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":3,"process":"u d~ > e+ ve g","process_key":"ud_epve_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-ufo-sm-nlc-n3-ud-w-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":3,"process":"u d~ > w+ g g","process_key":"ud_w_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-ufo-sm-nlc-n4-dd-3q-identical-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":4,"process":"d d~ > u u~ u u~","process_key":"dd_3q_identical_lines","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-ufo-sm-nlc-n4-dd-3q-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":4,"process":"d d~ > u u~ s s~","process_key":"dd_3q_lines","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-ufo-sm-nlc-n4-dd-4l-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":4,"process":"d d~ > e+ e- e+ e-","process_key":"dd_4l_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-ufo-sm-nlc-n4-dd-epem-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":4,"process":"d d~ > e+ e- g g","process_key":"dd_epem_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-ufo-sm-nlc-n4-dd-epemzh-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":4,"process":"d d~ > e+ e- z h","process_key":"dd_epemzh_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-ufo-sm-nlc-n4-dd-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":4,"process":"d d~ > t t~ g g","process_key":"dd_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-ufo-sm-nlc-n4-dd-ttzh-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":4,"process":"d d~ > t t~ z h","process_key":"dd_ttzh_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-ufo-sm-nlc-n4-dd-z-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":4,"process":"d d~ > z g g g","process_key":"dd_z_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-ufo-sm-nlc-n4-dd-zz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":4,"process":"d d~ > z z g g","process_key":"dd_zz_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-ufo-sm-nlc-n4-dd-zzz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":4,"process":"d d~ > z z z g","process_key":"dd_zzz_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-ufo-sm-nlc-n4-gg-gluons-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":4,"process":"g g > g g g g","process_key":"gg_gluons","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-ufo-sm-nlc-n4-gg-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":4,"process":"g g > t t~ g g","process_key":"gg_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-ufo-sm-nlc-n4-ud-epve-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":4,"process":"u d~ > e+ ve g g","process_key":"ud_epve_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-ufo-sm-nlc-n4-ud-w-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":4,"process":"u d~ > w+ g g g","process_key":"ud_w_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-ufo-sm-nlc-n5-dd-3q-identical-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":5,"process":"d d~ > u u~ u u~ g","process_key":"dd_3q_identical_lines","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-ufo-sm-nlc-n5-dd-3q-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":5,"process":"d d~ > u u~ s s~ g","process_key":"dd_3q_lines","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-ufo-sm-nlc-n5-dd-4l-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":5,"process":"d d~ > e+ e- e+ e- g","process_key":"dd_4l_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-ufo-sm-nlc-n5-dd-epem-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":5,"process":"d d~ > e+ e- g g g","process_key":"dd_epem_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-ufo-sm-nlc-n5-dd-epemzh-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":5,"process":"d d~ > e+ e- z h g","process_key":"dd_epemzh_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-ufo-sm-nlc-n5-dd-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":5,"process":"d d~ > t t~ g g g","process_key":"dd_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-ufo-sm-nlc-n5-dd-ttzh-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":5,"process":"d d~ > t t~ z h g","process_key":"dd_ttzh_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-ufo-sm-nlc-n5-dd-z-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":5,"process":"d d~ > z g g g g","process_key":"dd_z_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-ufo-sm-nlc-n5-dd-zz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":5,"process":"d d~ > z z g g g","process_key":"dd_zz_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-ufo-sm-nlc-n5-dd-zzz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":5,"process":"d d~ > z z z g g","process_key":"dd_zzz_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-ufo-sm-nlc-n5-gg-gluons-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":5,"process":"g g > g g g g g","process_key":"gg_gluons","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-ufo-sm-nlc-n5-gg-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":5,"process":"g g > t t~ g g g","process_key":"gg_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-ufo-sm-nlc-n5-ud-epve-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":5,"process":"u d~ > e+ ve g g g","process_key":"ud_epve_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-ufo-sm-nlc-n5-ud-w-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":5,"process":"u d~ > w+ g g g g","process_key":"ud_w_jets","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-ufo-sm-nlc-n6-dd-3q-identical-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":6,"process":"d d~ > u u~ u u~ g g","process_key":"dd_3q_identical_lines","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-ufo-sm-nlc-n6-dd-3q-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":6,"process":"d d~ > u u~ s s~ g g","process_key":"dd_3q_lines","variant":null,"workload":"contracted"},{"cell_id":"matrix-recurrence-ufo-sm-nlc-n6-dd-4q-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":6,"process":"d d~ > u u~ s s~ c c~","process_key":"dd_4q_lines","variant":null,"workload":"contracted"}],"kind":"performance_measurements","measurement_specs":[{"accuracy":"nlc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"}],"report_version":"0.3.0","schema_version":5} diff --git a/pyamplicol/src/pyamplicol/_profiling_campaign/results/reference_amplicol_full.json b/pyamplicol/src/pyamplicol/_profiling_campaign/results/reference_amplicol_full.json new file mode 100644 index 0000000000..bec9e43703 --- /dev/null +++ b/pyamplicol/src/pyamplicol/_profiling_campaign/results/reference_amplicol_full.json @@ -0,0 +1 @@ +{"$schema":"./report-cache.schema.json","dataset_id":"reference_amplicol_full","entries":[{"cell_id":"reference-amplicol-full-n1-dd-z-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":1,"process":"d d~ > z","process_key":"dd_z_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-amplicol-full-n1-ud-w-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":1,"process":"u d~ > w+","process_key":"ud_w_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-amplicol-full-n2-dd-epem-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":2,"process":"d d~ > e+ e-","process_key":"dd_epem_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-amplicol-full-n2-dd-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":2,"process":"d d~ > t t~","process_key":"dd_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-amplicol-full-n2-dd-z-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":2,"process":"d d~ > z g","process_key":"dd_z_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-amplicol-full-n2-dd-zz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":2,"process":"d d~ > z z","process_key":"dd_zz_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-amplicol-full-n2-gg-gluons-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":2,"process":"g g > g g","process_key":"gg_gluons","variant":null,"workload":"contracted"},{"cell_id":"reference-amplicol-full-n2-gg-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":2,"process":"g g > t t~","process_key":"gg_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-amplicol-full-n2-ud-epve-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":2,"process":"u d~ > e+ ve","process_key":"ud_epve_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-amplicol-full-n2-ud-w-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":2,"process":"u d~ > w+ g","process_key":"ud_w_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-amplicol-full-n3-dd-epem-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":3,"process":"d d~ > e+ e- g","process_key":"dd_epem_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-amplicol-full-n3-dd-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":3,"process":"d d~ > t t~ g","process_key":"dd_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-amplicol-full-n3-dd-z-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":3,"process":"d d~ > z g g","process_key":"dd_z_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-amplicol-full-n3-dd-zz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":3,"process":"d d~ > z z g","process_key":"dd_zz_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-amplicol-full-n3-dd-zzz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":3,"process":"d d~ > z z z","process_key":"dd_zzz_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-amplicol-full-n3-gg-gluons-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":3,"process":"g g > g g g","process_key":"gg_gluons","variant":null,"workload":"contracted"},{"cell_id":"reference-amplicol-full-n3-gg-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":3,"process":"g g > t t~ g","process_key":"gg_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-amplicol-full-n3-ud-epve-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":3,"process":"u d~ > e+ ve g","process_key":"ud_epve_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-amplicol-full-n3-ud-w-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":3,"process":"u d~ > w+ g g","process_key":"ud_w_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-amplicol-full-n4-dd-3q-identical-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":4,"process":"d d~ > u u~ u u~","process_key":"dd_3q_identical_lines","variant":null,"workload":"contracted"},{"cell_id":"reference-amplicol-full-n4-dd-3q-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":4,"process":"d d~ > u u~ s s~","process_key":"dd_3q_lines","variant":null,"workload":"contracted"},{"cell_id":"reference-amplicol-full-n4-dd-4l-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":4,"process":"d d~ > e+ e- e+ e-","process_key":"dd_4l_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-amplicol-full-n4-dd-epem-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":4,"process":"d d~ > e+ e- g g","process_key":"dd_epem_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-amplicol-full-n4-dd-epemzh-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":4,"process":"d d~ > e+ e- z h","process_key":"dd_epemzh_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-amplicol-full-n4-dd-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":4,"process":"d d~ > t t~ g g","process_key":"dd_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-amplicol-full-n4-dd-ttzh-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":4,"process":"d d~ > t t~ z h","process_key":"dd_ttzh_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-amplicol-full-n4-dd-z-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":4,"process":"d d~ > z g g g","process_key":"dd_z_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-amplicol-full-n4-dd-zz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":4,"process":"d d~ > z z g g","process_key":"dd_zz_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-amplicol-full-n4-dd-zzz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":4,"process":"d d~ > z z z g","process_key":"dd_zzz_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-amplicol-full-n4-gg-gluons-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":4,"process":"g g > g g g g","process_key":"gg_gluons","variant":null,"workload":"contracted"},{"cell_id":"reference-amplicol-full-n4-gg-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":4,"process":"g g > t t~ g g","process_key":"gg_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-amplicol-full-n4-ud-epve-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":4,"process":"u d~ > e+ ve g g","process_key":"ud_epve_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-amplicol-full-n4-ud-w-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":4,"process":"u d~ > w+ g g g","process_key":"ud_w_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-amplicol-full-n5-dd-3q-identical-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":5,"process":"d d~ > u u~ u u~ g","process_key":"dd_3q_identical_lines","variant":null,"workload":"contracted"},{"cell_id":"reference-amplicol-full-n5-dd-3q-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":5,"process":"d d~ > u u~ s s~ g","process_key":"dd_3q_lines","variant":null,"workload":"contracted"},{"cell_id":"reference-amplicol-full-n5-dd-4l-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":5,"process":"d d~ > e+ e- e+ e- g","process_key":"dd_4l_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-amplicol-full-n5-dd-epem-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":5,"process":"d d~ > e+ e- g g g","process_key":"dd_epem_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-amplicol-full-n5-dd-epemzh-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":5,"process":"d d~ > e+ e- z h g","process_key":"dd_epemzh_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-amplicol-full-n5-dd-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":5,"process":"d d~ > t t~ g g g","process_key":"dd_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-amplicol-full-n5-dd-ttzh-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":5,"process":"d d~ > t t~ z h g","process_key":"dd_ttzh_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-amplicol-full-n5-dd-z-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":5,"process":"d d~ > z g g g g","process_key":"dd_z_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-amplicol-full-n5-dd-zz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":5,"process":"d d~ > z z g g g","process_key":"dd_zz_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-amplicol-full-n5-dd-zzz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":5,"process":"d d~ > z z z g g","process_key":"dd_zzz_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-amplicol-full-n5-gg-gluons-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":5,"process":"g g > g g g g g","process_key":"gg_gluons","variant":null,"workload":"contracted"},{"cell_id":"reference-amplicol-full-n5-gg-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":5,"process":"g g > t t~ g g g","process_key":"gg_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-amplicol-full-n5-ud-epve-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":5,"process":"u d~ > e+ ve g g g","process_key":"ud_epve_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-amplicol-full-n5-ud-w-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":5,"process":"u d~ > w+ g g g g","process_key":"ud_w_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-amplicol-full-n6-dd-3q-identical-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":6,"process":"d d~ > u u~ u u~ g g","process_key":"dd_3q_identical_lines","variant":null,"workload":"contracted"},{"cell_id":"reference-amplicol-full-n6-dd-3q-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":6,"process":"d d~ > u u~ s s~ g g","process_key":"dd_3q_lines","variant":null,"workload":"contracted"},{"cell_id":"reference-amplicol-full-n6-dd-4q-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":6,"process":"d d~ > u u~ s s~ c c~","process_key":"dd_4q_lines","variant":null,"workload":"contracted"}],"kind":"performance_measurements","measurement_specs":[{"accuracy":"full","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null}],"report_version":"0.3.0","schema_version":5} diff --git a/pyamplicol/src/pyamplicol/_profiling_campaign/results/reference_amplicol_lc.json b/pyamplicol/src/pyamplicol/_profiling_campaign/results/reference_amplicol_lc.json new file mode 100644 index 0000000000..e281c6abd1 --- /dev/null +++ b/pyamplicol/src/pyamplicol/_profiling_campaign/results/reference_amplicol_lc.json @@ -0,0 +1 @@ +{"$schema":"./report-cache.schema.json","dataset_id":"reference_amplicol_lc","entries":[{"cell_id":"reference-amplicol-lc-n1-dd-z-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":1,"process":"d d~ > z","process_key":"dd_z_jets","variant":null,"workload":"all-flow"},{"cell_id":"reference-amplicol-lc-n1-dd-z-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":1,"process":"d d~ > z","process_key":"dd_z_jets","variant":null,"workload":"selected-flow"},{"cell_id":"reference-amplicol-lc-n1-ud-w-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":1,"process":"u d~ > w+","process_key":"ud_w_jets","variant":null,"workload":"all-flow"},{"cell_id":"reference-amplicol-lc-n1-ud-w-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":1,"process":"u d~ > w+","process_key":"ud_w_jets","variant":null,"workload":"selected-flow"},{"cell_id":"reference-amplicol-lc-n2-dd-epem-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":2,"process":"d d~ > e+ e-","process_key":"dd_epem_jets","variant":null,"workload":"all-flow"},{"cell_id":"reference-amplicol-lc-n2-dd-epem-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":2,"process":"d d~ > e+ e-","process_key":"dd_epem_jets","variant":null,"workload":"selected-flow"},{"cell_id":"reference-amplicol-lc-n2-dd-tt-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":2,"process":"d d~ > t t~","process_key":"dd_tt_jets","variant":null,"workload":"all-flow"},{"cell_id":"reference-amplicol-lc-n2-dd-tt-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":2,"process":"d d~ > t t~","process_key":"dd_tt_jets","variant":null,"workload":"selected-flow"},{"cell_id":"reference-amplicol-lc-n2-dd-z-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":2,"process":"d d~ > z g","process_key":"dd_z_jets","variant":null,"workload":"all-flow"},{"cell_id":"reference-amplicol-lc-n2-dd-z-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":2,"process":"d d~ > z g","process_key":"dd_z_jets","variant":null,"workload":"selected-flow"},{"cell_id":"reference-amplicol-lc-n2-dd-zz-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":2,"process":"d d~ > z z","process_key":"dd_zz_jets","variant":null,"workload":"all-flow"},{"cell_id":"reference-amplicol-lc-n2-dd-zz-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":2,"process":"d d~ > z z","process_key":"dd_zz_jets","variant":null,"workload":"selected-flow"},{"cell_id":"reference-amplicol-lc-n2-gg-gluons-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":2,"process":"g g > g g","process_key":"gg_gluons","variant":null,"workload":"all-flow"},{"cell_id":"reference-amplicol-lc-n2-gg-gluons-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":2,"process":"g g > g g","process_key":"gg_gluons","variant":null,"workload":"selected-flow"},{"cell_id":"reference-amplicol-lc-n2-gg-tt-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":2,"process":"g g > t t~","process_key":"gg_tt_jets","variant":null,"workload":"all-flow"},{"cell_id":"reference-amplicol-lc-n2-gg-tt-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":2,"process":"g g > t t~","process_key":"gg_tt_jets","variant":null,"workload":"selected-flow"},{"cell_id":"reference-amplicol-lc-n2-ud-epve-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":2,"process":"u d~ > e+ ve","process_key":"ud_epve_jets","variant":null,"workload":"all-flow"},{"cell_id":"reference-amplicol-lc-n2-ud-epve-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":2,"process":"u d~ > e+ ve","process_key":"ud_epve_jets","variant":null,"workload":"selected-flow"},{"cell_id":"reference-amplicol-lc-n2-ud-w-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":2,"process":"u d~ > w+ g","process_key":"ud_w_jets","variant":null,"workload":"all-flow"},{"cell_id":"reference-amplicol-lc-n2-ud-w-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":2,"process":"u d~ > w+ g","process_key":"ud_w_jets","variant":null,"workload":"selected-flow"},{"cell_id":"reference-amplicol-lc-n3-dd-epem-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":3,"process":"d d~ > e+ e- g","process_key":"dd_epem_jets","variant":null,"workload":"all-flow"},{"cell_id":"reference-amplicol-lc-n3-dd-epem-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":3,"process":"d d~ > e+ e- g","process_key":"dd_epem_jets","variant":null,"workload":"selected-flow"},{"cell_id":"reference-amplicol-lc-n3-dd-tt-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":3,"process":"d d~ > t t~ g","process_key":"dd_tt_jets","variant":null,"workload":"all-flow"},{"cell_id":"reference-amplicol-lc-n3-dd-tt-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":3,"process":"d d~ > t t~ g","process_key":"dd_tt_jets","variant":null,"workload":"selected-flow"},{"cell_id":"reference-amplicol-lc-n3-dd-z-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":3,"process":"d d~ > z g g","process_key":"dd_z_jets","variant":null,"workload":"all-flow"},{"cell_id":"reference-amplicol-lc-n3-dd-z-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":3,"process":"d d~ > z g g","process_key":"dd_z_jets","variant":null,"workload":"selected-flow"},{"cell_id":"reference-amplicol-lc-n3-dd-zz-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":3,"process":"d d~ > z z g","process_key":"dd_zz_jets","variant":null,"workload":"all-flow"},{"cell_id":"reference-amplicol-lc-n3-dd-zz-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":3,"process":"d d~ > z z g","process_key":"dd_zz_jets","variant":null,"workload":"selected-flow"},{"cell_id":"reference-amplicol-lc-n3-dd-zzz-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":3,"process":"d d~ > z z z","process_key":"dd_zzz_jets","variant":null,"workload":"all-flow"},{"cell_id":"reference-amplicol-lc-n3-dd-zzz-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":3,"process":"d d~ > z z z","process_key":"dd_zzz_jets","variant":null,"workload":"selected-flow"},{"cell_id":"reference-amplicol-lc-n3-gg-gluons-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":3,"process":"g g > g g g","process_key":"gg_gluons","variant":null,"workload":"all-flow"},{"cell_id":"reference-amplicol-lc-n3-gg-gluons-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":3,"process":"g g > g g g","process_key":"gg_gluons","variant":null,"workload":"selected-flow"},{"cell_id":"reference-amplicol-lc-n3-gg-tt-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":3,"process":"g g > t t~ g","process_key":"gg_tt_jets","variant":null,"workload":"all-flow"},{"cell_id":"reference-amplicol-lc-n3-gg-tt-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":3,"process":"g g > t t~ g","process_key":"gg_tt_jets","variant":null,"workload":"selected-flow"},{"cell_id":"reference-amplicol-lc-n3-ud-epve-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":3,"process":"u d~ > e+ ve g","process_key":"ud_epve_jets","variant":null,"workload":"all-flow"},{"cell_id":"reference-amplicol-lc-n3-ud-epve-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":3,"process":"u d~ > e+ ve g","process_key":"ud_epve_jets","variant":null,"workload":"selected-flow"},{"cell_id":"reference-amplicol-lc-n3-ud-w-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":3,"process":"u d~ > w+ g g","process_key":"ud_w_jets","variant":null,"workload":"all-flow"},{"cell_id":"reference-amplicol-lc-n3-ud-w-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":3,"process":"u d~ > w+ g g","process_key":"ud_w_jets","variant":null,"workload":"selected-flow"},{"cell_id":"reference-amplicol-lc-n4-dd-3q-identical-lines-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":4,"process":"d d~ > u u~ u u~","process_key":"dd_3q_identical_lines","variant":null,"workload":"all-flow"},{"cell_id":"reference-amplicol-lc-n4-dd-3q-identical-lines-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":4,"process":"d d~ > u u~ u u~","process_key":"dd_3q_identical_lines","variant":null,"workload":"selected-flow"},{"cell_id":"reference-amplicol-lc-n4-dd-3q-lines-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":4,"process":"d d~ > u u~ s s~","process_key":"dd_3q_lines","variant":null,"workload":"all-flow"},{"cell_id":"reference-amplicol-lc-n4-dd-3q-lines-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":4,"process":"d d~ > u u~ s s~","process_key":"dd_3q_lines","variant":null,"workload":"selected-flow"},{"cell_id":"reference-amplicol-lc-n4-dd-4l-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":4,"process":"d d~ > e+ e- e+ e-","process_key":"dd_4l_jets","variant":null,"workload":"all-flow"},{"cell_id":"reference-amplicol-lc-n4-dd-4l-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":4,"process":"d d~ > e+ e- e+ e-","process_key":"dd_4l_jets","variant":null,"workload":"selected-flow"},{"cell_id":"reference-amplicol-lc-n4-dd-epem-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":4,"process":"d d~ > e+ e- g g","process_key":"dd_epem_jets","variant":null,"workload":"all-flow"},{"cell_id":"reference-amplicol-lc-n4-dd-epem-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":4,"process":"d d~ > e+ e- g g","process_key":"dd_epem_jets","variant":null,"workload":"selected-flow"},{"cell_id":"reference-amplicol-lc-n4-dd-epemzh-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":4,"process":"d d~ > e+ e- z h","process_key":"dd_epemzh_jets","variant":null,"workload":"all-flow"},{"cell_id":"reference-amplicol-lc-n4-dd-epemzh-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":4,"process":"d d~ > e+ e- z h","process_key":"dd_epemzh_jets","variant":null,"workload":"selected-flow"},{"cell_id":"reference-amplicol-lc-n4-dd-tt-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":4,"process":"d d~ > t t~ g g","process_key":"dd_tt_jets","variant":null,"workload":"all-flow"},{"cell_id":"reference-amplicol-lc-n4-dd-tt-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":4,"process":"d d~ > t t~ g g","process_key":"dd_tt_jets","variant":null,"workload":"selected-flow"},{"cell_id":"reference-amplicol-lc-n4-dd-ttzh-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":4,"process":"d d~ > t t~ z h","process_key":"dd_ttzh_jets","variant":null,"workload":"all-flow"},{"cell_id":"reference-amplicol-lc-n4-dd-ttzh-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":4,"process":"d d~ > t t~ z h","process_key":"dd_ttzh_jets","variant":null,"workload":"selected-flow"},{"cell_id":"reference-amplicol-lc-n4-dd-z-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":4,"process":"d d~ > z g g g","process_key":"dd_z_jets","variant":null,"workload":"all-flow"},{"cell_id":"reference-amplicol-lc-n4-dd-z-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":4,"process":"d d~ > z g g g","process_key":"dd_z_jets","variant":null,"workload":"selected-flow"},{"cell_id":"reference-amplicol-lc-n4-dd-zz-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":4,"process":"d d~ > z z g g","process_key":"dd_zz_jets","variant":null,"workload":"all-flow"},{"cell_id":"reference-amplicol-lc-n4-dd-zz-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":4,"process":"d d~ > z z g g","process_key":"dd_zz_jets","variant":null,"workload":"selected-flow"},{"cell_id":"reference-amplicol-lc-n4-dd-zzz-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":4,"process":"d d~ > z z z g","process_key":"dd_zzz_jets","variant":null,"workload":"all-flow"},{"cell_id":"reference-amplicol-lc-n4-dd-zzz-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":4,"process":"d d~ > z z z g","process_key":"dd_zzz_jets","variant":null,"workload":"selected-flow"},{"cell_id":"reference-amplicol-lc-n4-gg-gluons-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":4,"process":"g g > g g g g","process_key":"gg_gluons","variant":null,"workload":"all-flow"},{"cell_id":"reference-amplicol-lc-n4-gg-gluons-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":4,"process":"g g > g g g g","process_key":"gg_gluons","variant":null,"workload":"selected-flow"},{"cell_id":"reference-amplicol-lc-n4-gg-tt-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":4,"process":"g g > t t~ g g","process_key":"gg_tt_jets","variant":null,"workload":"all-flow"},{"cell_id":"reference-amplicol-lc-n4-gg-tt-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":4,"process":"g g > t t~ g g","process_key":"gg_tt_jets","variant":null,"workload":"selected-flow"},{"cell_id":"reference-amplicol-lc-n4-ud-epve-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":4,"process":"u d~ > e+ ve g g","process_key":"ud_epve_jets","variant":null,"workload":"all-flow"},{"cell_id":"reference-amplicol-lc-n4-ud-epve-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":4,"process":"u d~ > e+ ve g g","process_key":"ud_epve_jets","variant":null,"workload":"selected-flow"},{"cell_id":"reference-amplicol-lc-n4-ud-w-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":4,"process":"u d~ > w+ g g g","process_key":"ud_w_jets","variant":null,"workload":"all-flow"},{"cell_id":"reference-amplicol-lc-n4-ud-w-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":4,"process":"u d~ > w+ g g g","process_key":"ud_w_jets","variant":null,"workload":"selected-flow"},{"cell_id":"reference-amplicol-lc-n5-dd-3q-identical-lines-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":5,"process":"d d~ > u u~ u u~ g","process_key":"dd_3q_identical_lines","variant":null,"workload":"all-flow"},{"cell_id":"reference-amplicol-lc-n5-dd-3q-identical-lines-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":5,"process":"d d~ > u u~ u u~ g","process_key":"dd_3q_identical_lines","variant":null,"workload":"selected-flow"},{"cell_id":"reference-amplicol-lc-n5-dd-3q-lines-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":5,"process":"d d~ > u u~ s s~ g","process_key":"dd_3q_lines","variant":null,"workload":"all-flow"},{"cell_id":"reference-amplicol-lc-n5-dd-3q-lines-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":5,"process":"d d~ > u u~ s s~ g","process_key":"dd_3q_lines","variant":null,"workload":"selected-flow"},{"cell_id":"reference-amplicol-lc-n5-dd-4l-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":5,"process":"d d~ > e+ e- e+ e- g","process_key":"dd_4l_jets","variant":null,"workload":"all-flow"},{"cell_id":"reference-amplicol-lc-n5-dd-4l-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":5,"process":"d d~ > e+ e- e+ e- g","process_key":"dd_4l_jets","variant":null,"workload":"selected-flow"},{"cell_id":"reference-amplicol-lc-n5-dd-epem-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":5,"process":"d d~ > e+ e- g g g","process_key":"dd_epem_jets","variant":null,"workload":"all-flow"},{"cell_id":"reference-amplicol-lc-n5-dd-epem-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":5,"process":"d d~ > e+ e- g g g","process_key":"dd_epem_jets","variant":null,"workload":"selected-flow"},{"cell_id":"reference-amplicol-lc-n5-dd-epemzh-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":5,"process":"d d~ > e+ e- z h g","process_key":"dd_epemzh_jets","variant":null,"workload":"all-flow"},{"cell_id":"reference-amplicol-lc-n5-dd-epemzh-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":5,"process":"d d~ > e+ e- z h g","process_key":"dd_epemzh_jets","variant":null,"workload":"selected-flow"},{"cell_id":"reference-amplicol-lc-n5-dd-tt-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":5,"process":"d d~ > t t~ g g g","process_key":"dd_tt_jets","variant":null,"workload":"all-flow"},{"cell_id":"reference-amplicol-lc-n5-dd-tt-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":5,"process":"d d~ > t t~ g g g","process_key":"dd_tt_jets","variant":null,"workload":"selected-flow"},{"cell_id":"reference-amplicol-lc-n5-dd-ttzh-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":5,"process":"d d~ > t t~ z h g","process_key":"dd_ttzh_jets","variant":null,"workload":"all-flow"},{"cell_id":"reference-amplicol-lc-n5-dd-ttzh-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":5,"process":"d d~ > t t~ z h g","process_key":"dd_ttzh_jets","variant":null,"workload":"selected-flow"},{"cell_id":"reference-amplicol-lc-n5-dd-z-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":5,"process":"d d~ > z g g g g","process_key":"dd_z_jets","variant":null,"workload":"all-flow"},{"cell_id":"reference-amplicol-lc-n5-dd-z-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":5,"process":"d d~ > z g g g g","process_key":"dd_z_jets","variant":null,"workload":"selected-flow"},{"cell_id":"reference-amplicol-lc-n5-dd-zz-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":5,"process":"d d~ > z z g g g","process_key":"dd_zz_jets","variant":null,"workload":"all-flow"},{"cell_id":"reference-amplicol-lc-n5-dd-zz-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":5,"process":"d d~ > z z g g g","process_key":"dd_zz_jets","variant":null,"workload":"selected-flow"},{"cell_id":"reference-amplicol-lc-n5-dd-zzz-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":5,"process":"d d~ > z z z g g","process_key":"dd_zzz_jets","variant":null,"workload":"all-flow"},{"cell_id":"reference-amplicol-lc-n5-dd-zzz-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":5,"process":"d d~ > z z z g g","process_key":"dd_zzz_jets","variant":null,"workload":"selected-flow"},{"cell_id":"reference-amplicol-lc-n5-gg-gluons-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":5,"process":"g g > g g g g g","process_key":"gg_gluons","variant":null,"workload":"all-flow"},{"cell_id":"reference-amplicol-lc-n5-gg-gluons-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":5,"process":"g g > g g g g g","process_key":"gg_gluons","variant":null,"workload":"selected-flow"},{"cell_id":"reference-amplicol-lc-n5-gg-tt-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":5,"process":"g g > t t~ g g g","process_key":"gg_tt_jets","variant":null,"workload":"all-flow"},{"cell_id":"reference-amplicol-lc-n5-gg-tt-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":5,"process":"g g > t t~ g g g","process_key":"gg_tt_jets","variant":null,"workload":"selected-flow"},{"cell_id":"reference-amplicol-lc-n5-ud-epve-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":5,"process":"u d~ > e+ ve g g g","process_key":"ud_epve_jets","variant":null,"workload":"all-flow"},{"cell_id":"reference-amplicol-lc-n5-ud-epve-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":5,"process":"u d~ > e+ ve g g g","process_key":"ud_epve_jets","variant":null,"workload":"selected-flow"},{"cell_id":"reference-amplicol-lc-n5-ud-w-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":5,"process":"u d~ > w+ g g g g","process_key":"ud_w_jets","variant":null,"workload":"all-flow"},{"cell_id":"reference-amplicol-lc-n5-ud-w-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":5,"process":"u d~ > w+ g g g g","process_key":"ud_w_jets","variant":null,"workload":"selected-flow"},{"cell_id":"reference-amplicol-lc-n6-dd-3q-identical-lines-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":6,"process":"d d~ > u u~ u u~ g g","process_key":"dd_3q_identical_lines","variant":null,"workload":"all-flow"},{"cell_id":"reference-amplicol-lc-n6-dd-3q-identical-lines-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":6,"process":"d d~ > u u~ u u~ g g","process_key":"dd_3q_identical_lines","variant":null,"workload":"selected-flow"},{"cell_id":"reference-amplicol-lc-n6-dd-3q-lines-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":6,"process":"d d~ > u u~ s s~ g g","process_key":"dd_3q_lines","variant":null,"workload":"all-flow"},{"cell_id":"reference-amplicol-lc-n6-dd-3q-lines-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":6,"process":"d d~ > u u~ s s~ g g","process_key":"dd_3q_lines","variant":null,"workload":"selected-flow"},{"cell_id":"reference-amplicol-lc-n6-dd-4l-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":6,"process":"d d~ > e+ e- e+ e- g g","process_key":"dd_4l_jets","variant":null,"workload":"all-flow"},{"cell_id":"reference-amplicol-lc-n6-dd-4l-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":6,"process":"d d~ > e+ e- e+ e- g g","process_key":"dd_4l_jets","variant":null,"workload":"selected-flow"},{"cell_id":"reference-amplicol-lc-n6-dd-4q-lines-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":6,"process":"d d~ > u u~ s s~ c c~","process_key":"dd_4q_lines","variant":null,"workload":"all-flow"},{"cell_id":"reference-amplicol-lc-n6-dd-4q-lines-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":6,"process":"d d~ > u u~ s s~ c c~","process_key":"dd_4q_lines","variant":null,"workload":"selected-flow"},{"cell_id":"reference-amplicol-lc-n6-dd-epem-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":6,"process":"d d~ > e+ e- g g g g","process_key":"dd_epem_jets","variant":null,"workload":"all-flow"},{"cell_id":"reference-amplicol-lc-n6-dd-epem-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":6,"process":"d d~ > e+ e- g g g g","process_key":"dd_epem_jets","variant":null,"workload":"selected-flow"},{"cell_id":"reference-amplicol-lc-n6-dd-epemzh-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":6,"process":"d d~ > e+ e- z h g g","process_key":"dd_epemzh_jets","variant":null,"workload":"all-flow"},{"cell_id":"reference-amplicol-lc-n6-dd-epemzh-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":6,"process":"d d~ > e+ e- z h g g","process_key":"dd_epemzh_jets","variant":null,"workload":"selected-flow"},{"cell_id":"reference-amplicol-lc-n6-dd-tt-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":6,"process":"d d~ > t t~ g g g g","process_key":"dd_tt_jets","variant":null,"workload":"all-flow"},{"cell_id":"reference-amplicol-lc-n6-dd-tt-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":6,"process":"d d~ > t t~ g g g g","process_key":"dd_tt_jets","variant":null,"workload":"selected-flow"},{"cell_id":"reference-amplicol-lc-n6-dd-ttzh-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":6,"process":"d d~ > t t~ z h g g","process_key":"dd_ttzh_jets","variant":null,"workload":"all-flow"},{"cell_id":"reference-amplicol-lc-n6-dd-ttzh-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":6,"process":"d d~ > t t~ z h g g","process_key":"dd_ttzh_jets","variant":null,"workload":"selected-flow"},{"cell_id":"reference-amplicol-lc-n6-dd-z-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":6,"process":"d d~ > z g g g g g","process_key":"dd_z_jets","variant":null,"workload":"all-flow"},{"cell_id":"reference-amplicol-lc-n6-dd-z-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":6,"process":"d d~ > z g g g g g","process_key":"dd_z_jets","variant":null,"workload":"selected-flow"},{"cell_id":"reference-amplicol-lc-n6-dd-zz-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":6,"process":"d d~ > z z g g g g","process_key":"dd_zz_jets","variant":null,"workload":"all-flow"},{"cell_id":"reference-amplicol-lc-n6-dd-zz-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":6,"process":"d d~ > z z g g g g","process_key":"dd_zz_jets","variant":null,"workload":"selected-flow"},{"cell_id":"reference-amplicol-lc-n6-dd-zzz-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":6,"process":"d d~ > z z z g g g","process_key":"dd_zzz_jets","variant":null,"workload":"all-flow"},{"cell_id":"reference-amplicol-lc-n6-dd-zzz-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":6,"process":"d d~ > z z z g g g","process_key":"dd_zzz_jets","variant":null,"workload":"selected-flow"},{"cell_id":"reference-amplicol-lc-n6-gg-gluons-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":6,"process":"g g > g g g g g g","process_key":"gg_gluons","variant":null,"workload":"all-flow"},{"cell_id":"reference-amplicol-lc-n6-gg-gluons-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":6,"process":"g g > g g g g g g","process_key":"gg_gluons","variant":null,"workload":"selected-flow"},{"cell_id":"reference-amplicol-lc-n6-gg-tt-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":6,"process":"g g > t t~ g g g g","process_key":"gg_tt_jets","variant":null,"workload":"all-flow"},{"cell_id":"reference-amplicol-lc-n6-gg-tt-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":6,"process":"g g > t t~ g g g g","process_key":"gg_tt_jets","variant":null,"workload":"selected-flow"},{"cell_id":"reference-amplicol-lc-n6-ud-epve-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":6,"process":"u d~ > e+ ve g g g g","process_key":"ud_epve_jets","variant":null,"workload":"all-flow"},{"cell_id":"reference-amplicol-lc-n6-ud-epve-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":6,"process":"u d~ > e+ ve g g g g","process_key":"ud_epve_jets","variant":null,"workload":"selected-flow"},{"cell_id":"reference-amplicol-lc-n6-ud-w-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":6,"process":"u d~ > w+ g g g g g","process_key":"ud_w_jets","variant":null,"workload":"all-flow"},{"cell_id":"reference-amplicol-lc-n6-ud-w-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":6,"process":"u d~ > w+ g g g g g","process_key":"ud_w_jets","variant":null,"workload":"selected-flow"},{"cell_id":"reference-amplicol-lc-n7-dd-3q-identical-lines-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":7,"process":"d d~ > u u~ u u~ g g g","process_key":"dd_3q_identical_lines","variant":null,"workload":"all-flow"},{"cell_id":"reference-amplicol-lc-n7-dd-3q-identical-lines-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":7,"process":"d d~ > u u~ u u~ g g g","process_key":"dd_3q_identical_lines","variant":null,"workload":"selected-flow"},{"cell_id":"reference-amplicol-lc-n7-dd-3q-lines-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":7,"process":"d d~ > u u~ s s~ g g g","process_key":"dd_3q_lines","variant":null,"workload":"all-flow"},{"cell_id":"reference-amplicol-lc-n7-dd-3q-lines-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":7,"process":"d d~ > u u~ s s~ g g g","process_key":"dd_3q_lines","variant":null,"workload":"selected-flow"},{"cell_id":"reference-amplicol-lc-n7-dd-4l-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":7,"process":"d d~ > e+ e- e+ e- g g g","process_key":"dd_4l_jets","variant":null,"workload":"all-flow"},{"cell_id":"reference-amplicol-lc-n7-dd-4l-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":7,"process":"d d~ > e+ e- e+ e- g g g","process_key":"dd_4l_jets","variant":null,"workload":"selected-flow"},{"cell_id":"reference-amplicol-lc-n7-dd-4q-lines-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":7,"process":"d d~ > u u~ s s~ c c~ g","process_key":"dd_4q_lines","variant":null,"workload":"all-flow"},{"cell_id":"reference-amplicol-lc-n7-dd-4q-lines-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":7,"process":"d d~ > u u~ s s~ c c~ g","process_key":"dd_4q_lines","variant":null,"workload":"selected-flow"},{"cell_id":"reference-amplicol-lc-n7-dd-epem-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":7,"process":"d d~ > e+ e- g g g g g","process_key":"dd_epem_jets","variant":null,"workload":"all-flow"},{"cell_id":"reference-amplicol-lc-n7-dd-epem-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":7,"process":"d d~ > e+ e- g g g g g","process_key":"dd_epem_jets","variant":null,"workload":"selected-flow"},{"cell_id":"reference-amplicol-lc-n7-dd-epemzh-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":7,"process":"d d~ > e+ e- z h g g g","process_key":"dd_epemzh_jets","variant":null,"workload":"all-flow"},{"cell_id":"reference-amplicol-lc-n7-dd-epemzh-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":7,"process":"d d~ > e+ e- z h g g g","process_key":"dd_epemzh_jets","variant":null,"workload":"selected-flow"},{"cell_id":"reference-amplicol-lc-n7-dd-tt-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":7,"process":"d d~ > t t~ g g g g g","process_key":"dd_tt_jets","variant":null,"workload":"all-flow"},{"cell_id":"reference-amplicol-lc-n7-dd-tt-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":7,"process":"d d~ > t t~ g g g g g","process_key":"dd_tt_jets","variant":null,"workload":"selected-flow"},{"cell_id":"reference-amplicol-lc-n7-dd-ttzh-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":7,"process":"d d~ > t t~ z h g g g","process_key":"dd_ttzh_jets","variant":null,"workload":"all-flow"},{"cell_id":"reference-amplicol-lc-n7-dd-ttzh-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":7,"process":"d d~ > t t~ z h g g g","process_key":"dd_ttzh_jets","variant":null,"workload":"selected-flow"},{"cell_id":"reference-amplicol-lc-n7-dd-z-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":7,"process":"d d~ > z g g g g g g","process_key":"dd_z_jets","variant":null,"workload":"all-flow"},{"cell_id":"reference-amplicol-lc-n7-dd-z-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":7,"process":"d d~ > z g g g g g g","process_key":"dd_z_jets","variant":null,"workload":"selected-flow"},{"cell_id":"reference-amplicol-lc-n7-dd-zz-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":7,"process":"d d~ > z z g g g g g","process_key":"dd_zz_jets","variant":null,"workload":"all-flow"},{"cell_id":"reference-amplicol-lc-n7-dd-zz-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":7,"process":"d d~ > z z g g g g g","process_key":"dd_zz_jets","variant":null,"workload":"selected-flow"},{"cell_id":"reference-amplicol-lc-n7-dd-zzz-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":7,"process":"d d~ > z z z g g g g","process_key":"dd_zzz_jets","variant":null,"workload":"all-flow"},{"cell_id":"reference-amplicol-lc-n7-dd-zzz-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":7,"process":"d d~ > z z z g g g g","process_key":"dd_zzz_jets","variant":null,"workload":"selected-flow"},{"cell_id":"reference-amplicol-lc-n7-gg-gluons-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":7,"process":"g g > g g g g g g g","process_key":"gg_gluons","variant":null,"workload":"all-flow"},{"cell_id":"reference-amplicol-lc-n7-gg-gluons-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":7,"process":"g g > g g g g g g g","process_key":"gg_gluons","variant":null,"workload":"selected-flow"},{"cell_id":"reference-amplicol-lc-n7-gg-tt-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":7,"process":"g g > t t~ g g g g g","process_key":"gg_tt_jets","variant":null,"workload":"all-flow"},{"cell_id":"reference-amplicol-lc-n7-gg-tt-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":7,"process":"g g > t t~ g g g g g","process_key":"gg_tt_jets","variant":null,"workload":"selected-flow"},{"cell_id":"reference-amplicol-lc-n7-ud-epve-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":7,"process":"u d~ > e+ ve g g g g g","process_key":"ud_epve_jets","variant":null,"workload":"all-flow"},{"cell_id":"reference-amplicol-lc-n7-ud-epve-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":7,"process":"u d~ > e+ ve g g g g g","process_key":"ud_epve_jets","variant":null,"workload":"selected-flow"},{"cell_id":"reference-amplicol-lc-n7-ud-w-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":7,"process":"u d~ > w+ g g g g g g","process_key":"ud_w_jets","variant":null,"workload":"all-flow"},{"cell_id":"reference-amplicol-lc-n7-ud-w-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":7,"process":"u d~ > w+ g g g g g g","process_key":"ud_w_jets","variant":null,"workload":"selected-flow"},{"cell_id":"reference-amplicol-lc-n8-dd-3q-identical-lines-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":8,"process":"d d~ > u u~ u u~ g g g g","process_key":"dd_3q_identical_lines","variant":null,"workload":"all-flow"},{"cell_id":"reference-amplicol-lc-n8-dd-3q-identical-lines-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":8,"process":"d d~ > u u~ u u~ g g g g","process_key":"dd_3q_identical_lines","variant":null,"workload":"selected-flow"},{"cell_id":"reference-amplicol-lc-n8-dd-3q-lines-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":8,"process":"d d~ > u u~ s s~ g g g g","process_key":"dd_3q_lines","variant":null,"workload":"all-flow"},{"cell_id":"reference-amplicol-lc-n8-dd-3q-lines-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":8,"process":"d d~ > u u~ s s~ g g g g","process_key":"dd_3q_lines","variant":null,"workload":"selected-flow"},{"cell_id":"reference-amplicol-lc-n8-dd-4l-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":8,"process":"d d~ > e+ e- e+ e- g g g g","process_key":"dd_4l_jets","variant":null,"workload":"all-flow"},{"cell_id":"reference-amplicol-lc-n8-dd-4l-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":8,"process":"d d~ > e+ e- e+ e- g g g g","process_key":"dd_4l_jets","variant":null,"workload":"selected-flow"},{"cell_id":"reference-amplicol-lc-n8-dd-4q-lines-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":8,"process":"d d~ > u u~ s s~ c c~ g g","process_key":"dd_4q_lines","variant":null,"workload":"all-flow"},{"cell_id":"reference-amplicol-lc-n8-dd-4q-lines-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":8,"process":"d d~ > u u~ s s~ c c~ g g","process_key":"dd_4q_lines","variant":null,"workload":"selected-flow"},{"cell_id":"reference-amplicol-lc-n8-dd-epem-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":8,"process":"d d~ > e+ e- g g g g g g","process_key":"dd_epem_jets","variant":null,"workload":"all-flow"},{"cell_id":"reference-amplicol-lc-n8-dd-epem-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":8,"process":"d d~ > e+ e- g g g g g g","process_key":"dd_epem_jets","variant":null,"workload":"selected-flow"},{"cell_id":"reference-amplicol-lc-n8-dd-epemzh-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":8,"process":"d d~ > e+ e- z h g g g g","process_key":"dd_epemzh_jets","variant":null,"workload":"all-flow"},{"cell_id":"reference-amplicol-lc-n8-dd-epemzh-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":8,"process":"d d~ > e+ e- z h g g g g","process_key":"dd_epemzh_jets","variant":null,"workload":"selected-flow"},{"cell_id":"reference-amplicol-lc-n8-dd-tt-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":8,"process":"d d~ > t t~ g g g g g g","process_key":"dd_tt_jets","variant":null,"workload":"all-flow"},{"cell_id":"reference-amplicol-lc-n8-dd-tt-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":8,"process":"d d~ > t t~ g g g g g g","process_key":"dd_tt_jets","variant":null,"workload":"selected-flow"},{"cell_id":"reference-amplicol-lc-n8-dd-ttzh-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":8,"process":"d d~ > t t~ z h g g g g","process_key":"dd_ttzh_jets","variant":null,"workload":"all-flow"},{"cell_id":"reference-amplicol-lc-n8-dd-ttzh-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":8,"process":"d d~ > t t~ z h g g g g","process_key":"dd_ttzh_jets","variant":null,"workload":"selected-flow"},{"cell_id":"reference-amplicol-lc-n8-dd-z-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":8,"process":"d d~ > z g g g g g g g","process_key":"dd_z_jets","variant":null,"workload":"all-flow"},{"cell_id":"reference-amplicol-lc-n8-dd-z-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":8,"process":"d d~ > z g g g g g g g","process_key":"dd_z_jets","variant":null,"workload":"selected-flow"},{"cell_id":"reference-amplicol-lc-n8-dd-zz-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":8,"process":"d d~ > z z g g g g g g","process_key":"dd_zz_jets","variant":null,"workload":"all-flow"},{"cell_id":"reference-amplicol-lc-n8-dd-zz-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":8,"process":"d d~ > z z g g g g g g","process_key":"dd_zz_jets","variant":null,"workload":"selected-flow"},{"cell_id":"reference-amplicol-lc-n8-dd-zzz-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":8,"process":"d d~ > z z z g g g g g","process_key":"dd_zzz_jets","variant":null,"workload":"all-flow"},{"cell_id":"reference-amplicol-lc-n8-dd-zzz-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":8,"process":"d d~ > z z z g g g g g","process_key":"dd_zzz_jets","variant":null,"workload":"selected-flow"},{"cell_id":"reference-amplicol-lc-n8-gg-gluons-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":8,"process":"g g > g g g g g g g g","process_key":"gg_gluons","variant":null,"workload":"all-flow"},{"cell_id":"reference-amplicol-lc-n8-gg-gluons-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":8,"process":"g g > g g g g g g g g","process_key":"gg_gluons","variant":null,"workload":"selected-flow"},{"cell_id":"reference-amplicol-lc-n8-gg-tt-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":8,"process":"g g > t t~ g g g g g g","process_key":"gg_tt_jets","variant":null,"workload":"all-flow"},{"cell_id":"reference-amplicol-lc-n8-gg-tt-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":8,"process":"g g > t t~ g g g g g g","process_key":"gg_tt_jets","variant":null,"workload":"selected-flow"},{"cell_id":"reference-amplicol-lc-n8-ud-epve-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":8,"process":"u d~ > e+ ve g g g g g g","process_key":"ud_epve_jets","variant":null,"workload":"all-flow"},{"cell_id":"reference-amplicol-lc-n8-ud-epve-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":8,"process":"u d~ > e+ ve g g g g g g","process_key":"ud_epve_jets","variant":null,"workload":"selected-flow"},{"cell_id":"reference-amplicol-lc-n8-ud-w-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":8,"process":"u d~ > w+ g g g g g g g","process_key":"ud_w_jets","variant":null,"workload":"all-flow"},{"cell_id":"reference-amplicol-lc-n8-ud-w-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":8,"process":"u d~ > w+ g g g g g g g","process_key":"ud_w_jets","variant":null,"workload":"selected-flow"},{"cell_id":"reference-amplicol-lc-n9-dd-3q-identical-lines-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":9,"process":"d d~ > u u~ u u~ g g g g g","process_key":"dd_3q_identical_lines","variant":null,"workload":"all-flow"},{"cell_id":"reference-amplicol-lc-n9-dd-3q-identical-lines-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":9,"process":"d d~ > u u~ u u~ g g g g g","process_key":"dd_3q_identical_lines","variant":null,"workload":"selected-flow"},{"cell_id":"reference-amplicol-lc-n9-dd-3q-lines-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":9,"process":"d d~ > u u~ s s~ g g g g g","process_key":"dd_3q_lines","variant":null,"workload":"all-flow"},{"cell_id":"reference-amplicol-lc-n9-dd-3q-lines-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":9,"process":"d d~ > u u~ s s~ g g g g g","process_key":"dd_3q_lines","variant":null,"workload":"selected-flow"},{"cell_id":"reference-amplicol-lc-n9-dd-4l-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":9,"process":"d d~ > e+ e- e+ e- g g g g g","process_key":"dd_4l_jets","variant":null,"workload":"all-flow"},{"cell_id":"reference-amplicol-lc-n9-dd-4l-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":9,"process":"d d~ > e+ e- e+ e- g g g g g","process_key":"dd_4l_jets","variant":null,"workload":"selected-flow"},{"cell_id":"reference-amplicol-lc-n9-dd-4q-lines-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":9,"process":"d d~ > u u~ s s~ c c~ g g g","process_key":"dd_4q_lines","variant":null,"workload":"all-flow"},{"cell_id":"reference-amplicol-lc-n9-dd-4q-lines-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":9,"process":"d d~ > u u~ s s~ c c~ g g g","process_key":"dd_4q_lines","variant":null,"workload":"selected-flow"},{"cell_id":"reference-amplicol-lc-n9-dd-epem-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":9,"process":"d d~ > e+ e- g g g g g g g","process_key":"dd_epem_jets","variant":null,"workload":"all-flow"},{"cell_id":"reference-amplicol-lc-n9-dd-epem-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":9,"process":"d d~ > e+ e- g g g g g g g","process_key":"dd_epem_jets","variant":null,"workload":"selected-flow"},{"cell_id":"reference-amplicol-lc-n9-dd-epemzh-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":9,"process":"d d~ > e+ e- z h g g g g g","process_key":"dd_epemzh_jets","variant":null,"workload":"all-flow"},{"cell_id":"reference-amplicol-lc-n9-dd-epemzh-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":9,"process":"d d~ > e+ e- z h g g g g g","process_key":"dd_epemzh_jets","variant":null,"workload":"selected-flow"},{"cell_id":"reference-amplicol-lc-n9-dd-tt-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":9,"process":"d d~ > t t~ g g g g g g g","process_key":"dd_tt_jets","variant":null,"workload":"all-flow"},{"cell_id":"reference-amplicol-lc-n9-dd-tt-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":9,"process":"d d~ > t t~ g g g g g g g","process_key":"dd_tt_jets","variant":null,"workload":"selected-flow"},{"cell_id":"reference-amplicol-lc-n9-dd-ttzh-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":9,"process":"d d~ > t t~ z h g g g g g","process_key":"dd_ttzh_jets","variant":null,"workload":"all-flow"},{"cell_id":"reference-amplicol-lc-n9-dd-ttzh-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":9,"process":"d d~ > t t~ z h g g g g g","process_key":"dd_ttzh_jets","variant":null,"workload":"selected-flow"},{"cell_id":"reference-amplicol-lc-n9-dd-z-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":9,"process":"d d~ > z g g g g g g g g","process_key":"dd_z_jets","variant":null,"workload":"all-flow"},{"cell_id":"reference-amplicol-lc-n9-dd-z-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":9,"process":"d d~ > z g g g g g g g g","process_key":"dd_z_jets","variant":null,"workload":"selected-flow"},{"cell_id":"reference-amplicol-lc-n9-dd-zz-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":9,"process":"d d~ > z z g g g g g g g","process_key":"dd_zz_jets","variant":null,"workload":"all-flow"},{"cell_id":"reference-amplicol-lc-n9-dd-zz-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":9,"process":"d d~ > z z g g g g g g g","process_key":"dd_zz_jets","variant":null,"workload":"selected-flow"},{"cell_id":"reference-amplicol-lc-n9-dd-zzz-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":9,"process":"d d~ > z z z g g g g g g","process_key":"dd_zzz_jets","variant":null,"workload":"all-flow"},{"cell_id":"reference-amplicol-lc-n9-dd-zzz-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":9,"process":"d d~ > z z z g g g g g g","process_key":"dd_zzz_jets","variant":null,"workload":"selected-flow"},{"cell_id":"reference-amplicol-lc-n9-gg-gluons-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":9,"process":"g g > g g g g g g g g g","process_key":"gg_gluons","variant":null,"workload":"all-flow"},{"cell_id":"reference-amplicol-lc-n9-gg-gluons-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":9,"process":"g g > g g g g g g g g g","process_key":"gg_gluons","variant":null,"workload":"selected-flow"},{"cell_id":"reference-amplicol-lc-n9-gg-tt-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":9,"process":"g g > t t~ g g g g g g g","process_key":"gg_tt_jets","variant":null,"workload":"all-flow"},{"cell_id":"reference-amplicol-lc-n9-gg-tt-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":9,"process":"g g > t t~ g g g g g g g","process_key":"gg_tt_jets","variant":null,"workload":"selected-flow"},{"cell_id":"reference-amplicol-lc-n9-ud-epve-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":9,"process":"u d~ > e+ ve g g g g g g g","process_key":"ud_epve_jets","variant":null,"workload":"all-flow"},{"cell_id":"reference-amplicol-lc-n9-ud-epve-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":9,"process":"u d~ > e+ ve g g g g g g g","process_key":"ud_epve_jets","variant":null,"workload":"selected-flow"},{"cell_id":"reference-amplicol-lc-n9-ud-w-jets-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":9,"process":"u d~ > w+ g g g g g g g g","process_key":"ud_w_jets","variant":null,"workload":"all-flow"},{"cell_id":"reference-amplicol-lc-n9-ud-w-jets-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":9,"process":"u d~ > w+ g g g g g g g g","process_key":"ud_w_jets","variant":null,"workload":"selected-flow"}],"kind":"performance_measurements","measurement_specs":[{"accuracy":"lc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null}],"report_version":"0.3.0","schema_version":5} diff --git a/pyamplicol/src/pyamplicol/_profiling_campaign/results/reference_amplicol_nlc.json b/pyamplicol/src/pyamplicol/_profiling_campaign/results/reference_amplicol_nlc.json new file mode 100644 index 0000000000..82c28e8fcf --- /dev/null +++ b/pyamplicol/src/pyamplicol/_profiling_campaign/results/reference_amplicol_nlc.json @@ -0,0 +1 @@ +{"$schema":"./report-cache.schema.json","dataset_id":"reference_amplicol_nlc","entries":[{"cell_id":"reference-amplicol-nlc-n1-dd-z-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":1,"process":"d d~ > z","process_key":"dd_z_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-amplicol-nlc-n1-ud-w-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":1,"process":"u d~ > w+","process_key":"ud_w_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-amplicol-nlc-n2-dd-epem-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":2,"process":"d d~ > e+ e-","process_key":"dd_epem_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-amplicol-nlc-n2-dd-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":2,"process":"d d~ > t t~","process_key":"dd_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-amplicol-nlc-n2-dd-z-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":2,"process":"d d~ > z g","process_key":"dd_z_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-amplicol-nlc-n2-dd-zz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":2,"process":"d d~ > z z","process_key":"dd_zz_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-amplicol-nlc-n2-gg-gluons-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":2,"process":"g g > g g","process_key":"gg_gluons","variant":null,"workload":"contracted"},{"cell_id":"reference-amplicol-nlc-n2-gg-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":2,"process":"g g > t t~","process_key":"gg_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-amplicol-nlc-n2-ud-epve-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":2,"process":"u d~ > e+ ve","process_key":"ud_epve_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-amplicol-nlc-n2-ud-w-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":2,"process":"u d~ > w+ g","process_key":"ud_w_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-amplicol-nlc-n3-dd-epem-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":3,"process":"d d~ > e+ e- g","process_key":"dd_epem_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-amplicol-nlc-n3-dd-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":3,"process":"d d~ > t t~ g","process_key":"dd_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-amplicol-nlc-n3-dd-z-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":3,"process":"d d~ > z g g","process_key":"dd_z_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-amplicol-nlc-n3-dd-zz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":3,"process":"d d~ > z z g","process_key":"dd_zz_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-amplicol-nlc-n3-dd-zzz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":3,"process":"d d~ > z z z","process_key":"dd_zzz_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-amplicol-nlc-n3-gg-gluons-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":3,"process":"g g > g g g","process_key":"gg_gluons","variant":null,"workload":"contracted"},{"cell_id":"reference-amplicol-nlc-n3-gg-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":3,"process":"g g > t t~ g","process_key":"gg_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-amplicol-nlc-n3-ud-epve-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":3,"process":"u d~ > e+ ve g","process_key":"ud_epve_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-amplicol-nlc-n3-ud-w-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":3,"process":"u d~ > w+ g g","process_key":"ud_w_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-amplicol-nlc-n4-dd-3q-identical-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":4,"process":"d d~ > u u~ u u~","process_key":"dd_3q_identical_lines","variant":null,"workload":"contracted"},{"cell_id":"reference-amplicol-nlc-n4-dd-3q-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":4,"process":"d d~ > u u~ s s~","process_key":"dd_3q_lines","variant":null,"workload":"contracted"},{"cell_id":"reference-amplicol-nlc-n4-dd-4l-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":4,"process":"d d~ > e+ e- e+ e-","process_key":"dd_4l_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-amplicol-nlc-n4-dd-epem-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":4,"process":"d d~ > e+ e- g g","process_key":"dd_epem_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-amplicol-nlc-n4-dd-epemzh-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":4,"process":"d d~ > e+ e- z h","process_key":"dd_epemzh_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-amplicol-nlc-n4-dd-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":4,"process":"d d~ > t t~ g g","process_key":"dd_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-amplicol-nlc-n4-dd-ttzh-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":4,"process":"d d~ > t t~ z h","process_key":"dd_ttzh_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-amplicol-nlc-n4-dd-z-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":4,"process":"d d~ > z g g g","process_key":"dd_z_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-amplicol-nlc-n4-dd-zz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":4,"process":"d d~ > z z g g","process_key":"dd_zz_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-amplicol-nlc-n4-dd-zzz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":4,"process":"d d~ > z z z g","process_key":"dd_zzz_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-amplicol-nlc-n4-gg-gluons-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":4,"process":"g g > g g g g","process_key":"gg_gluons","variant":null,"workload":"contracted"},{"cell_id":"reference-amplicol-nlc-n4-gg-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":4,"process":"g g > t t~ g g","process_key":"gg_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-amplicol-nlc-n4-ud-epve-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":4,"process":"u d~ > e+ ve g g","process_key":"ud_epve_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-amplicol-nlc-n4-ud-w-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":4,"process":"u d~ > w+ g g g","process_key":"ud_w_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-amplicol-nlc-n5-dd-3q-identical-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":5,"process":"d d~ > u u~ u u~ g","process_key":"dd_3q_identical_lines","variant":null,"workload":"contracted"},{"cell_id":"reference-amplicol-nlc-n5-dd-3q-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":5,"process":"d d~ > u u~ s s~ g","process_key":"dd_3q_lines","variant":null,"workload":"contracted"},{"cell_id":"reference-amplicol-nlc-n5-dd-4l-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":5,"process":"d d~ > e+ e- e+ e- g","process_key":"dd_4l_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-amplicol-nlc-n5-dd-epem-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":5,"process":"d d~ > e+ e- g g g","process_key":"dd_epem_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-amplicol-nlc-n5-dd-epemzh-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":5,"process":"d d~ > e+ e- z h g","process_key":"dd_epemzh_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-amplicol-nlc-n5-dd-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":5,"process":"d d~ > t t~ g g g","process_key":"dd_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-amplicol-nlc-n5-dd-ttzh-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":5,"process":"d d~ > t t~ z h g","process_key":"dd_ttzh_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-amplicol-nlc-n5-dd-z-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":5,"process":"d d~ > z g g g g","process_key":"dd_z_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-amplicol-nlc-n5-dd-zz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":5,"process":"d d~ > z z g g g","process_key":"dd_zz_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-amplicol-nlc-n5-dd-zzz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":5,"process":"d d~ > z z z g g","process_key":"dd_zzz_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-amplicol-nlc-n5-gg-gluons-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":5,"process":"g g > g g g g g","process_key":"gg_gluons","variant":null,"workload":"contracted"},{"cell_id":"reference-amplicol-nlc-n5-gg-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":5,"process":"g g > t t~ g g g","process_key":"gg_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-amplicol-nlc-n5-ud-epve-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":5,"process":"u d~ > e+ ve g g g","process_key":"ud_epve_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-amplicol-nlc-n5-ud-w-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":5,"process":"u d~ > w+ g g g g","process_key":"ud_w_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-amplicol-nlc-n6-dd-3q-identical-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":6,"process":"d d~ > u u~ u u~ g g","process_key":"dd_3q_identical_lines","variant":null,"workload":"contracted"},{"cell_id":"reference-amplicol-nlc-n6-dd-3q-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":6,"process":"d d~ > u u~ s s~ g g","process_key":"dd_3q_lines","variant":null,"workload":"contracted"},{"cell_id":"reference-amplicol-nlc-n6-dd-4q-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"nlc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null},"n_final":6,"process":"d d~ > u u~ s s~ c c~","process_key":"dd_4q_lines","variant":null,"workload":"contracted"}],"kind":"performance_measurements","measurement_specs":[{"accuracy":"nlc","backend":"fortran","execution_mode":"amplicol","jit_optimization_level":null,"model":null}],"report_version":"0.3.0","schema_version":5} diff --git a/pyamplicol/src/pyamplicol/_profiling_campaign/results/reference_madgraph_full.json b/pyamplicol/src/pyamplicol/_profiling_campaign/results/reference_madgraph_full.json new file mode 100644 index 0000000000..b5b1a59584 --- /dev/null +++ b/pyamplicol/src/pyamplicol/_profiling_campaign/results/reference_madgraph_full.json @@ -0,0 +1 @@ +{"$schema":"./report-cache.schema.json","dataset_id":"reference_madgraph_full","entries":[{"cell_id":"reference-madgraph-full-n1-dd-z-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"fortran","execution_mode":"madgraph","jit_optimization_level":null,"model":"ufo_sm"},"n_final":1,"process":"d d~ > z","process_key":"dd_z_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-madgraph-full-n1-ud-w-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"fortran","execution_mode":"madgraph","jit_optimization_level":null,"model":"ufo_sm"},"n_final":1,"process":"u d~ > w+","process_key":"ud_w_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-madgraph-full-n2-dd-epem-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"fortran","execution_mode":"madgraph","jit_optimization_level":null,"model":"ufo_sm"},"n_final":2,"process":"d d~ > e+ e-","process_key":"dd_epem_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-madgraph-full-n2-dd-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"fortran","execution_mode":"madgraph","jit_optimization_level":null,"model":"ufo_sm"},"n_final":2,"process":"d d~ > t t~","process_key":"dd_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-madgraph-full-n2-dd-z-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"fortran","execution_mode":"madgraph","jit_optimization_level":null,"model":"ufo_sm"},"n_final":2,"process":"d d~ > z g","process_key":"dd_z_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-madgraph-full-n2-dd-zz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"fortran","execution_mode":"madgraph","jit_optimization_level":null,"model":"ufo_sm"},"n_final":2,"process":"d d~ > z z","process_key":"dd_zz_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-madgraph-full-n2-gg-gluons-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"fortran","execution_mode":"madgraph","jit_optimization_level":null,"model":"ufo_sm"},"n_final":2,"process":"g g > g g","process_key":"gg_gluons","variant":null,"workload":"contracted"},{"cell_id":"reference-madgraph-full-n2-gg-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"fortran","execution_mode":"madgraph","jit_optimization_level":null,"model":"ufo_sm"},"n_final":2,"process":"g g > t t~","process_key":"gg_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-madgraph-full-n2-ud-epve-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"fortran","execution_mode":"madgraph","jit_optimization_level":null,"model":"ufo_sm"},"n_final":2,"process":"u d~ > e+ ve","process_key":"ud_epve_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-madgraph-full-n2-ud-w-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"fortran","execution_mode":"madgraph","jit_optimization_level":null,"model":"ufo_sm"},"n_final":2,"process":"u d~ > w+ g","process_key":"ud_w_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-madgraph-full-n3-dd-epem-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"fortran","execution_mode":"madgraph","jit_optimization_level":null,"model":"ufo_sm"},"n_final":3,"process":"d d~ > e+ e- g","process_key":"dd_epem_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-madgraph-full-n3-dd-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"fortran","execution_mode":"madgraph","jit_optimization_level":null,"model":"ufo_sm"},"n_final":3,"process":"d d~ > t t~ g","process_key":"dd_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-madgraph-full-n3-dd-z-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"fortran","execution_mode":"madgraph","jit_optimization_level":null,"model":"ufo_sm"},"n_final":3,"process":"d d~ > z g g","process_key":"dd_z_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-madgraph-full-n3-dd-zz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"fortran","execution_mode":"madgraph","jit_optimization_level":null,"model":"ufo_sm"},"n_final":3,"process":"d d~ > z z g","process_key":"dd_zz_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-madgraph-full-n3-dd-zzz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"fortran","execution_mode":"madgraph","jit_optimization_level":null,"model":"ufo_sm"},"n_final":3,"process":"d d~ > z z z","process_key":"dd_zzz_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-madgraph-full-n3-gg-gluons-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"fortran","execution_mode":"madgraph","jit_optimization_level":null,"model":"ufo_sm"},"n_final":3,"process":"g g > g g g","process_key":"gg_gluons","variant":null,"workload":"contracted"},{"cell_id":"reference-madgraph-full-n3-gg-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"fortran","execution_mode":"madgraph","jit_optimization_level":null,"model":"ufo_sm"},"n_final":3,"process":"g g > t t~ g","process_key":"gg_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-madgraph-full-n3-ud-epve-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"fortran","execution_mode":"madgraph","jit_optimization_level":null,"model":"ufo_sm"},"n_final":3,"process":"u d~ > e+ ve g","process_key":"ud_epve_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-madgraph-full-n3-ud-w-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"fortran","execution_mode":"madgraph","jit_optimization_level":null,"model":"ufo_sm"},"n_final":3,"process":"u d~ > w+ g g","process_key":"ud_w_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-madgraph-full-n4-dd-3q-identical-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"fortran","execution_mode":"madgraph","jit_optimization_level":null,"model":"ufo_sm"},"n_final":4,"process":"d d~ > u u~ u u~","process_key":"dd_3q_identical_lines","variant":null,"workload":"contracted"},{"cell_id":"reference-madgraph-full-n4-dd-3q-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"fortran","execution_mode":"madgraph","jit_optimization_level":null,"model":"ufo_sm"},"n_final":4,"process":"d d~ > u u~ s s~","process_key":"dd_3q_lines","variant":null,"workload":"contracted"},{"cell_id":"reference-madgraph-full-n4-dd-4l-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"fortran","execution_mode":"madgraph","jit_optimization_level":null,"model":"ufo_sm"},"n_final":4,"process":"d d~ > e+ e- e+ e-","process_key":"dd_4l_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-madgraph-full-n4-dd-epem-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"fortran","execution_mode":"madgraph","jit_optimization_level":null,"model":"ufo_sm"},"n_final":4,"process":"d d~ > e+ e- g g","process_key":"dd_epem_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-madgraph-full-n4-dd-epemzh-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"fortran","execution_mode":"madgraph","jit_optimization_level":null,"model":"ufo_sm"},"n_final":4,"process":"d d~ > e+ e- z h","process_key":"dd_epemzh_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-madgraph-full-n4-dd-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"fortran","execution_mode":"madgraph","jit_optimization_level":null,"model":"ufo_sm"},"n_final":4,"process":"d d~ > t t~ g g","process_key":"dd_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-madgraph-full-n4-dd-ttzh-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"fortran","execution_mode":"madgraph","jit_optimization_level":null,"model":"ufo_sm"},"n_final":4,"process":"d d~ > t t~ z h","process_key":"dd_ttzh_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-madgraph-full-n4-dd-z-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"fortran","execution_mode":"madgraph","jit_optimization_level":null,"model":"ufo_sm"},"n_final":4,"process":"d d~ > z g g g","process_key":"dd_z_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-madgraph-full-n4-dd-zz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"fortran","execution_mode":"madgraph","jit_optimization_level":null,"model":"ufo_sm"},"n_final":4,"process":"d d~ > z z g g","process_key":"dd_zz_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-madgraph-full-n4-dd-zzz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"fortran","execution_mode":"madgraph","jit_optimization_level":null,"model":"ufo_sm"},"n_final":4,"process":"d d~ > z z z g","process_key":"dd_zzz_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-madgraph-full-n4-gg-gluons-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"fortran","execution_mode":"madgraph","jit_optimization_level":null,"model":"ufo_sm"},"n_final":4,"process":"g g > g g g g","process_key":"gg_gluons","variant":null,"workload":"contracted"},{"cell_id":"reference-madgraph-full-n4-gg-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"fortran","execution_mode":"madgraph","jit_optimization_level":null,"model":"ufo_sm"},"n_final":4,"process":"g g > t t~ g g","process_key":"gg_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-madgraph-full-n4-ud-epve-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"fortran","execution_mode":"madgraph","jit_optimization_level":null,"model":"ufo_sm"},"n_final":4,"process":"u d~ > e+ ve g g","process_key":"ud_epve_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-madgraph-full-n4-ud-w-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"fortran","execution_mode":"madgraph","jit_optimization_level":null,"model":"ufo_sm"},"n_final":4,"process":"u d~ > w+ g g g","process_key":"ud_w_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-madgraph-full-n5-dd-3q-identical-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"fortran","execution_mode":"madgraph","jit_optimization_level":null,"model":"ufo_sm"},"n_final":5,"process":"d d~ > u u~ u u~ g","process_key":"dd_3q_identical_lines","variant":null,"workload":"contracted"},{"cell_id":"reference-madgraph-full-n5-dd-3q-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"fortran","execution_mode":"madgraph","jit_optimization_level":null,"model":"ufo_sm"},"n_final":5,"process":"d d~ > u u~ s s~ g","process_key":"dd_3q_lines","variant":null,"workload":"contracted"},{"cell_id":"reference-madgraph-full-n5-dd-4l-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"fortran","execution_mode":"madgraph","jit_optimization_level":null,"model":"ufo_sm"},"n_final":5,"process":"d d~ > e+ e- e+ e- g","process_key":"dd_4l_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-madgraph-full-n5-dd-epem-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"fortran","execution_mode":"madgraph","jit_optimization_level":null,"model":"ufo_sm"},"n_final":5,"process":"d d~ > e+ e- g g g","process_key":"dd_epem_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-madgraph-full-n5-dd-epemzh-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"fortran","execution_mode":"madgraph","jit_optimization_level":null,"model":"ufo_sm"},"n_final":5,"process":"d d~ > e+ e- z h g","process_key":"dd_epemzh_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-madgraph-full-n5-dd-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"fortran","execution_mode":"madgraph","jit_optimization_level":null,"model":"ufo_sm"},"n_final":5,"process":"d d~ > t t~ g g g","process_key":"dd_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-madgraph-full-n5-dd-ttzh-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"fortran","execution_mode":"madgraph","jit_optimization_level":null,"model":"ufo_sm"},"n_final":5,"process":"d d~ > t t~ z h g","process_key":"dd_ttzh_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-madgraph-full-n5-dd-z-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"fortran","execution_mode":"madgraph","jit_optimization_level":null,"model":"ufo_sm"},"n_final":5,"process":"d d~ > z g g g g","process_key":"dd_z_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-madgraph-full-n5-dd-zz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"fortran","execution_mode":"madgraph","jit_optimization_level":null,"model":"ufo_sm"},"n_final":5,"process":"d d~ > z z g g g","process_key":"dd_zz_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-madgraph-full-n5-dd-zzz-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"fortran","execution_mode":"madgraph","jit_optimization_level":null,"model":"ufo_sm"},"n_final":5,"process":"d d~ > z z z g g","process_key":"dd_zzz_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-madgraph-full-n5-gg-gluons-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"fortran","execution_mode":"madgraph","jit_optimization_level":null,"model":"ufo_sm"},"n_final":5,"process":"g g > g g g g g","process_key":"gg_gluons","variant":null,"workload":"contracted"},{"cell_id":"reference-madgraph-full-n5-gg-tt-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"fortran","execution_mode":"madgraph","jit_optimization_level":null,"model":"ufo_sm"},"n_final":5,"process":"g g > t t~ g g g","process_key":"gg_tt_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-madgraph-full-n5-ud-epve-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"fortran","execution_mode":"madgraph","jit_optimization_level":null,"model":"ufo_sm"},"n_final":5,"process":"u d~ > e+ ve g g g","process_key":"ud_epve_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-madgraph-full-n5-ud-w-jets-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"fortran","execution_mode":"madgraph","jit_optimization_level":null,"model":"ufo_sm"},"n_final":5,"process":"u d~ > w+ g g g g","process_key":"ud_w_jets","variant":null,"workload":"contracted"},{"cell_id":"reference-madgraph-full-n6-dd-3q-identical-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"fortran","execution_mode":"madgraph","jit_optimization_level":null,"model":"ufo_sm"},"n_final":6,"process":"d d~ > u u~ u u~ g g","process_key":"dd_3q_identical_lines","variant":null,"workload":"contracted"},{"cell_id":"reference-madgraph-full-n6-dd-3q-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"fortran","execution_mode":"madgraph","jit_optimization_level":null,"model":"ufo_sm"},"n_final":6,"process":"d d~ > u u~ s s~ g g","process_key":"dd_3q_lines","variant":null,"workload":"contracted"},{"cell_id":"reference-madgraph-full-n6-dd-4q-lines-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"fortran","execution_mode":"madgraph","jit_optimization_level":null,"model":"ufo_sm"},"n_final":6,"process":"d d~ > u u~ s s~ c c~","process_key":"dd_4q_lines","variant":null,"workload":"contracted"}],"kind":"performance_measurements","measurement_specs":[{"accuracy":"full","backend":"fortran","execution_mode":"madgraph","jit_optimization_level":null,"model":"ufo_sm"}],"report_version":"0.3.0","schema_version":5} diff --git a/pyamplicol/src/pyamplicol/_profiling_campaign/results/report-cache.schema.json b/pyamplicol/src/pyamplicol/_profiling_campaign/results/report-cache.schema.json new file mode 100644 index 0000000000..8d0b133880 --- /dev/null +++ b/pyamplicol/src/pyamplicol/_profiling_campaign/results/report-cache.schema.json @@ -0,0 +1 @@ +{"$id":"https://pyamplicol.dev/schema/report-cache-v5.json","$schema":"https://json-schema.org/draft/2020-12/schema","additionalProperties":false,"properties":{"$schema":{"const":"./report-cache.schema.json"},"dataset_id":{"minLength":1,"type":"string"},"entries":{"items":{"additionalProperties":false,"properties":{"cell_id":{"minLength":1,"type":"string"},"measurement":{"additionalProperties":false,"allOf":[{"if":{"properties":{"status":{"const":"ok"}},"required":["status"]},"then":{"properties":{"validation":{"properties":{"direct_agreements":{"items":{"oneOf":[{"additionalProperties":false,"properties":{"abi":{"const":"pyamplicol-report-direct-agreement-v1"},"absolute_difference":{"minimum":0,"type":"number"},"absolute_tolerance":{"minimum":0,"type":"number"},"baseline":{"type":"number"},"baseline_cell_id":{"minLength":1,"type":"string"},"candidate":{"type":"number"},"candidate_cell_id":{"minLength":1,"type":"string"},"edge_kind":{"enum":["builtin-ufo-recurrence","z-recurrence-cross-mode","lc-cross-layout-component","lc-legacy-pyamplicol-component","madgraph-full-colour"]},"relative_difference":{"minimum":0,"type":"number"},"relative_tolerance":{"minimum":0,"type":"number"},"status":{"enum":["not_available","ok","failed","timeout","memory_limit","skip","validation_failed","unverified","error","unsupported"]},"value_kind":{"enum":["matrix_element","lc_common_component"]}},"required":["abi","edge_kind","value_kind","baseline_cell_id","candidate_cell_id","status","candidate","baseline","absolute_difference","relative_difference","relative_tolerance","absolute_tolerance"],"type":"object"},{"additionalProperties":false,"properties":{"abi":{"const":"pyamplicol-report-direct-agreement-v2"},"absolute_difference":{"minimum":0,"type":"number"},"baseline":{"type":"number"},"baseline_cell_id":{"minLength":1,"type":"string"},"baseline_scale":{"minimum":0,"type":"number"},"baseline_scale_source":{"minLength":1,"type":"string"},"candidate":{"type":"number"},"candidate_cell_id":{"minLength":1,"type":"string"},"candidate_scale":{"minimum":0,"type":"number"},"candidate_scale_source":{"minLength":1,"type":"string"},"comparison_binding":{"minProperties":1,"type":"object"},"comparison_scale":{"minimum":0,"type":"number"},"conditioned_residual":{"minimum":0,"type":"number"},"edge_kind":{"enum":["builtin-ufo-recurrence","z-recurrence-cross-mode","lc-cross-layout-component","lc-legacy-pyamplicol-component","madgraph-full-colour"]},"error_bound":{"minimum":0,"type":"number"},"relative_difference":{"minimum":0,"type":"number"},"relative_tolerance":{"minimum":0,"type":"number"},"status":{"enum":["not_available","ok","failed","timeout","memory_limit","skip","validation_failed","unverified","error","unsupported"]},"value_kind":{"enum":["matrix_element","lc_common_component"]}},"required":["abi","edge_kind","value_kind","baseline_cell_id","candidate_cell_id","status","candidate","baseline","candidate_scale","baseline_scale","candidate_scale_source","baseline_scale_source","comparison_scale","absolute_difference","relative_difference","conditioned_residual","error_bound","relative_tolerance","comparison_binding"],"type":"object"}]},"type":"array"},"independent_authority":{"additionalProperties":false,"properties":{"abi":{"const":"pyamplicol-report-independent-authority-v1"},"expected_cell_ids":{"items":{"minLength":1,"type":"string"},"type":"array","uniqueItems":true},"reason":{"minLength":1,"type":"string"},"same_artifact_diagnostics_are_authority":{"const":false},"selected_cell_id":{"type":["string","null"]},"status":{"enum":["unavailable","verified","mismatch","incompatible"]}},"required":["abi","expected_cell_ids","selected_cell_id","status","reason","same_artifact_diagnostics_are_authority"],"type":"object"},"lc_common_component":{"additionalProperties":false,"properties":{"abi":{"const":"pyamplicol-report-lc-common-component-v1"},"cell_id":{"minLength":1,"type":"string"},"color_flow_ids":{"items":{"minLength":1,"type":"string"},"minItems":1,"type":"array","uniqueItems":true},"helicity_ids":{"items":{"minLength":1,"type":"string"},"minItems":1,"type":"array","uniqueItems":true},"point_digest":{"pattern":"^[0-9a-f]{64}$","type":"string"},"value":{"type":"number"}},"required":["abi","cell_id","value","point_digest","helicity_ids","color_flow_ids"],"type":"object"},"madgraph_comparison":{"type":"object"},"on_the_fly_recurrence_authority":{"additionalProperties":false,"properties":{"abi":{"const":"pyamplicol-report-on-the-fly-recurrence-authority-validation-v1"},"after_clear":{"additionalProperties":false,"properties":{"candidate_resolved_ordering_sha256":{"pattern":"^[0-9a-f]{64}$","type":"string"},"candidate_resolved_source_sha256":{"pattern":"^[0-9a-f]{64}$","type":"string"},"candidate_resolved_sum":{"additionalProperties":false,"properties":{"check_count":{"minimum":1,"type":"integer"},"maximum_absolute_delta":{"minimum":0,"type":"number"},"maximum_conditioned_residual":{"minimum":0,"type":"number"}},"required":["check_count","maximum_conditioned_residual","maximum_absolute_delta"],"type":"object"},"candidate_resolved_total_source_sha256":{"pattern":"^[0-9a-f]{64}$","type":"string"},"candidate_total_source_sha256":{"pattern":"^[0-9a-f]{64}$","type":"string"},"comparison":{"additionalProperties":false,"properties":{"authority_cell_id":{"minLength":1,"type":"string"},"resolved_components":{"additionalProperties":false,"properties":{"check_count":{"minimum":1,"type":"integer"},"component_count":{"minimum":1,"type":"integer"},"maximum_absolute_delta":{"minimum":0,"type":"number"},"maximum_conditioned_residual":{"minimum":0,"type":"number"}},"required":["check_count","component_count","maximum_conditioned_residual","maximum_absolute_delta"],"type":"object"},"total":{"additionalProperties":false,"properties":{"check_count":{"minimum":1,"type":"integer"},"maximum_absolute_delta":{"minimum":0,"type":"number"},"maximum_conditioned_residual":{"minimum":0,"type":"number"}},"required":["check_count","maximum_conditioned_residual","maximum_absolute_delta"],"type":"object"}},"required":["authority_cell_id","total","resolved_components"],"type":"object"}},"required":["candidate_total_source_sha256","candidate_resolved_total_source_sha256","candidate_resolved_ordering_sha256","candidate_resolved_source_sha256","candidate_resolved_sum","comparison"],"type":"object"},"authority":{"additionalProperties":false,"properties":{"artifact_id":{"pattern":"^[0-9a-f]{64}$","type":"string"},"cell_id":{"minLength":1,"type":"string"},"resolved_ordering_sha256":{"pattern":"^[0-9a-f]{64}$","type":"string"},"resolved_source_sha256":{"pattern":"^[0-9a-f]{64}$","type":"string"},"resolved_sum":{"additionalProperties":false,"properties":{"check_count":{"minimum":1,"type":"integer"},"maximum_absolute_delta":{"minimum":0,"type":"number"},"maximum_conditioned_residual":{"minimum":0,"type":"number"}},"required":["check_count","maximum_conditioned_residual","maximum_absolute_delta"],"type":"object"},"resolved_total_source_sha256":{"pattern":"^[0-9a-f]{64}$","type":"string"},"total_source_sha256":{"pattern":"^[0-9a-f]{64}$","type":"string"}},"required":["cell_id","artifact_id","total_source_sha256","resolved_total_source_sha256","resolved_ordering_sha256","resolved_source_sha256","resolved_sum"],"type":"object"},"before_clear":{"additionalProperties":false,"properties":{"candidate_resolved_ordering_sha256":{"pattern":"^[0-9a-f]{64}$","type":"string"},"candidate_resolved_source_sha256":{"pattern":"^[0-9a-f]{64}$","type":"string"},"candidate_resolved_sum":{"additionalProperties":false,"properties":{"check_count":{"minimum":1,"type":"integer"},"maximum_absolute_delta":{"minimum":0,"type":"number"},"maximum_conditioned_residual":{"minimum":0,"type":"number"}},"required":["check_count","maximum_conditioned_residual","maximum_absolute_delta"],"type":"object"},"candidate_resolved_total_source_sha256":{"pattern":"^[0-9a-f]{64}$","type":"string"},"candidate_total_source_sha256":{"pattern":"^[0-9a-f]{64}$","type":"string"},"comparison":{"additionalProperties":false,"properties":{"authority_cell_id":{"minLength":1,"type":"string"},"resolved_components":{"additionalProperties":false,"properties":{"check_count":{"minimum":1,"type":"integer"},"component_count":{"minimum":1,"type":"integer"},"maximum_absolute_delta":{"minimum":0,"type":"number"},"maximum_conditioned_residual":{"minimum":0,"type":"number"}},"required":["check_count","component_count","maximum_conditioned_residual","maximum_absolute_delta"],"type":"object"},"total":{"additionalProperties":false,"properties":{"check_count":{"minimum":1,"type":"integer"},"maximum_absolute_delta":{"minimum":0,"type":"number"},"maximum_conditioned_residual":{"minimum":0,"type":"number"}},"required":["check_count","maximum_conditioned_residual","maximum_absolute_delta"],"type":"object"}},"required":["authority_cell_id","total","resolved_components"],"type":"object"}},"required":["candidate_total_source_sha256","candidate_resolved_total_source_sha256","candidate_resolved_ordering_sha256","candidate_resolved_source_sha256","candidate_resolved_sum","comparison"],"type":"object"},"candidate_artifact_id":{"pattern":"^[0-9a-f]{64}$","type":"string"},"candidate_cell_id":{"minLength":1,"type":"string"},"lifecycle":{"additionalProperties":false,"properties":{"authority_artifact_loaded_only":{"const":true},"candidate_loaded_before_validation":{"const":true},"clear_call_count":{"const":2},"final_clear_before_profile":{"const":true},"validated_after_clear":{"const":true},"validated_before_clear":{"const":true}},"required":["authority_artifact_loaded_only","candidate_loaded_before_validation","validated_before_clear","validated_after_clear","clear_call_count","final_clear_before_profile"],"type":"object"},"point_count":{"minimum":1,"type":"integer"},"point_digest":{"pattern":"^[0-9a-f]{64}$","type":"string"},"precision_digits":{"const":16},"relative_tolerance":{"const":1e-12},"resolved_check_count":{"minimum":1,"type":"integer"},"resolved_component_count":{"minimum":1,"type":"integer"},"selector_sha256":{"pattern":"^[0-9a-f]{64}$","type":"string"},"status":{"const":"ok"},"workload":{"enum":["selected-flow","all-flow"]}},"required":["abi","status","candidate_cell_id","candidate_artifact_id","workload","precision_digits","relative_tolerance","point_digest","selector_sha256","point_count","resolved_component_count","resolved_check_count","authority","before_clear","after_clear","lifecycle"],"type":"object"},"status":{"enum":["not_available","ok","failed","timeout","memory_limit","skip","validation_failed","unverified","error","unsupported"]}},"required":["direct_agreements"],"type":"object"}}}},{"if":{"properties":{"status":{"const":"unverified"}},"required":["status"]},"then":{"properties":{"failure":{"type":"object"},"validation":{"properties":{"direct_agreements":{"items":{"oneOf":[{"additionalProperties":false,"properties":{"abi":{"const":"pyamplicol-report-direct-agreement-v1"},"absolute_difference":{"minimum":0,"type":"number"},"absolute_tolerance":{"minimum":0,"type":"number"},"baseline":{"type":"number"},"baseline_cell_id":{"minLength":1,"type":"string"},"candidate":{"type":"number"},"candidate_cell_id":{"minLength":1,"type":"string"},"edge_kind":{"enum":["builtin-ufo-recurrence","z-recurrence-cross-mode","lc-cross-layout-component","lc-legacy-pyamplicol-component","madgraph-full-colour"]},"relative_difference":{"minimum":0,"type":"number"},"relative_tolerance":{"minimum":0,"type":"number"},"status":{"enum":["not_available","ok","failed","timeout","memory_limit","skip","validation_failed","unverified","error","unsupported"]},"value_kind":{"enum":["matrix_element","lc_common_component"]}},"required":["abi","edge_kind","value_kind","baseline_cell_id","candidate_cell_id","status","candidate","baseline","absolute_difference","relative_difference","relative_tolerance","absolute_tolerance"],"type":"object"},{"additionalProperties":false,"properties":{"abi":{"const":"pyamplicol-report-direct-agreement-v2"},"absolute_difference":{"minimum":0,"type":"number"},"baseline":{"type":"number"},"baseline_cell_id":{"minLength":1,"type":"string"},"baseline_scale":{"minimum":0,"type":"number"},"baseline_scale_source":{"minLength":1,"type":"string"},"candidate":{"type":"number"},"candidate_cell_id":{"minLength":1,"type":"string"},"candidate_scale":{"minimum":0,"type":"number"},"candidate_scale_source":{"minLength":1,"type":"string"},"comparison_binding":{"minProperties":1,"type":"object"},"comparison_scale":{"minimum":0,"type":"number"},"conditioned_residual":{"minimum":0,"type":"number"},"edge_kind":{"enum":["builtin-ufo-recurrence","z-recurrence-cross-mode","lc-cross-layout-component","lc-legacy-pyamplicol-component","madgraph-full-colour"]},"error_bound":{"minimum":0,"type":"number"},"relative_difference":{"minimum":0,"type":"number"},"relative_tolerance":{"minimum":0,"type":"number"},"status":{"enum":["not_available","ok","failed","timeout","memory_limit","skip","validation_failed","unverified","error","unsupported"]},"value_kind":{"enum":["matrix_element","lc_common_component"]}},"required":["abi","edge_kind","value_kind","baseline_cell_id","candidate_cell_id","status","candidate","baseline","candidate_scale","baseline_scale","candidate_scale_source","baseline_scale_source","comparison_scale","absolute_difference","relative_difference","conditioned_residual","error_bound","relative_tolerance","comparison_binding"],"type":"object"}]},"type":"array"},"independent_authority":{"additionalProperties":false,"properties":{"abi":{"const":"pyamplicol-report-independent-authority-v1"},"expected_cell_ids":{"items":{"minLength":1,"type":"string"},"type":"array","uniqueItems":true},"reason":{"minLength":1,"type":"string"},"same_artifact_diagnostics_are_authority":{"const":false},"selected_cell_id":{"type":["string","null"]},"status":{"enum":["unavailable","verified","mismatch","incompatible"]}},"required":["abi","expected_cell_ids","selected_cell_id","status","reason","same_artifact_diagnostics_are_authority"],"type":"object"},"lc_common_component":{"additionalProperties":false,"properties":{"abi":{"const":"pyamplicol-report-lc-common-component-v1"},"cell_id":{"minLength":1,"type":"string"},"color_flow_ids":{"items":{"minLength":1,"type":"string"},"minItems":1,"type":"array","uniqueItems":true},"helicity_ids":{"items":{"minLength":1,"type":"string"},"minItems":1,"type":"array","uniqueItems":true},"point_digest":{"pattern":"^[0-9a-f]{64}$","type":"string"},"value":{"type":"number"}},"required":["abi","cell_id","value","point_digest","helicity_ids","color_flow_ids"],"type":"object"},"madgraph_comparison":{"type":"object"},"on_the_fly_recurrence_authority":{"additionalProperties":false,"properties":{"abi":{"const":"pyamplicol-report-on-the-fly-recurrence-authority-validation-v1"},"after_clear":{"additionalProperties":false,"properties":{"candidate_resolved_ordering_sha256":{"pattern":"^[0-9a-f]{64}$","type":"string"},"candidate_resolved_source_sha256":{"pattern":"^[0-9a-f]{64}$","type":"string"},"candidate_resolved_sum":{"additionalProperties":false,"properties":{"check_count":{"minimum":1,"type":"integer"},"maximum_absolute_delta":{"minimum":0,"type":"number"},"maximum_conditioned_residual":{"minimum":0,"type":"number"}},"required":["check_count","maximum_conditioned_residual","maximum_absolute_delta"],"type":"object"},"candidate_resolved_total_source_sha256":{"pattern":"^[0-9a-f]{64}$","type":"string"},"candidate_total_source_sha256":{"pattern":"^[0-9a-f]{64}$","type":"string"},"comparison":{"additionalProperties":false,"properties":{"authority_cell_id":{"minLength":1,"type":"string"},"resolved_components":{"additionalProperties":false,"properties":{"check_count":{"minimum":1,"type":"integer"},"component_count":{"minimum":1,"type":"integer"},"maximum_absolute_delta":{"minimum":0,"type":"number"},"maximum_conditioned_residual":{"minimum":0,"type":"number"}},"required":["check_count","component_count","maximum_conditioned_residual","maximum_absolute_delta"],"type":"object"},"total":{"additionalProperties":false,"properties":{"check_count":{"minimum":1,"type":"integer"},"maximum_absolute_delta":{"minimum":0,"type":"number"},"maximum_conditioned_residual":{"minimum":0,"type":"number"}},"required":["check_count","maximum_conditioned_residual","maximum_absolute_delta"],"type":"object"}},"required":["authority_cell_id","total","resolved_components"],"type":"object"}},"required":["candidate_total_source_sha256","candidate_resolved_total_source_sha256","candidate_resolved_ordering_sha256","candidate_resolved_source_sha256","candidate_resolved_sum","comparison"],"type":"object"},"authority":{"additionalProperties":false,"properties":{"artifact_id":{"pattern":"^[0-9a-f]{64}$","type":"string"},"cell_id":{"minLength":1,"type":"string"},"resolved_ordering_sha256":{"pattern":"^[0-9a-f]{64}$","type":"string"},"resolved_source_sha256":{"pattern":"^[0-9a-f]{64}$","type":"string"},"resolved_sum":{"additionalProperties":false,"properties":{"check_count":{"minimum":1,"type":"integer"},"maximum_absolute_delta":{"minimum":0,"type":"number"},"maximum_conditioned_residual":{"minimum":0,"type":"number"}},"required":["check_count","maximum_conditioned_residual","maximum_absolute_delta"],"type":"object"},"resolved_total_source_sha256":{"pattern":"^[0-9a-f]{64}$","type":"string"},"total_source_sha256":{"pattern":"^[0-9a-f]{64}$","type":"string"}},"required":["cell_id","artifact_id","total_source_sha256","resolved_total_source_sha256","resolved_ordering_sha256","resolved_source_sha256","resolved_sum"],"type":"object"},"before_clear":{"additionalProperties":false,"properties":{"candidate_resolved_ordering_sha256":{"pattern":"^[0-9a-f]{64}$","type":"string"},"candidate_resolved_source_sha256":{"pattern":"^[0-9a-f]{64}$","type":"string"},"candidate_resolved_sum":{"additionalProperties":false,"properties":{"check_count":{"minimum":1,"type":"integer"},"maximum_absolute_delta":{"minimum":0,"type":"number"},"maximum_conditioned_residual":{"minimum":0,"type":"number"}},"required":["check_count","maximum_conditioned_residual","maximum_absolute_delta"],"type":"object"},"candidate_resolved_total_source_sha256":{"pattern":"^[0-9a-f]{64}$","type":"string"},"candidate_total_source_sha256":{"pattern":"^[0-9a-f]{64}$","type":"string"},"comparison":{"additionalProperties":false,"properties":{"authority_cell_id":{"minLength":1,"type":"string"},"resolved_components":{"additionalProperties":false,"properties":{"check_count":{"minimum":1,"type":"integer"},"component_count":{"minimum":1,"type":"integer"},"maximum_absolute_delta":{"minimum":0,"type":"number"},"maximum_conditioned_residual":{"minimum":0,"type":"number"}},"required":["check_count","component_count","maximum_conditioned_residual","maximum_absolute_delta"],"type":"object"},"total":{"additionalProperties":false,"properties":{"check_count":{"minimum":1,"type":"integer"},"maximum_absolute_delta":{"minimum":0,"type":"number"},"maximum_conditioned_residual":{"minimum":0,"type":"number"}},"required":["check_count","maximum_conditioned_residual","maximum_absolute_delta"],"type":"object"}},"required":["authority_cell_id","total","resolved_components"],"type":"object"}},"required":["candidate_total_source_sha256","candidate_resolved_total_source_sha256","candidate_resolved_ordering_sha256","candidate_resolved_source_sha256","candidate_resolved_sum","comparison"],"type":"object"},"candidate_artifact_id":{"pattern":"^[0-9a-f]{64}$","type":"string"},"candidate_cell_id":{"minLength":1,"type":"string"},"lifecycle":{"additionalProperties":false,"properties":{"authority_artifact_loaded_only":{"const":true},"candidate_loaded_before_validation":{"const":true},"clear_call_count":{"const":2},"final_clear_before_profile":{"const":true},"validated_after_clear":{"const":true},"validated_before_clear":{"const":true}},"required":["authority_artifact_loaded_only","candidate_loaded_before_validation","validated_before_clear","validated_after_clear","clear_call_count","final_clear_before_profile"],"type":"object"},"point_count":{"minimum":1,"type":"integer"},"point_digest":{"pattern":"^[0-9a-f]{64}$","type":"string"},"precision_digits":{"const":16},"relative_tolerance":{"const":1e-12},"resolved_check_count":{"minimum":1,"type":"integer"},"resolved_component_count":{"minimum":1,"type":"integer"},"selector_sha256":{"pattern":"^[0-9a-f]{64}$","type":"string"},"status":{"const":"ok"},"workload":{"enum":["selected-flow","all-flow"]}},"required":["abi","status","candidate_cell_id","candidate_artifact_id","workload","precision_digits","relative_tolerance","point_digest","selector_sha256","point_count","resolved_component_count","resolved_check_count","authority","before_clear","after_clear","lifecycle"],"type":"object"},"status":{"enum":["not_available","ok","failed","timeout","memory_limit","skip","validation_failed","unverified","error","unsupported"]}},"required":["status","direct_agreements","independent_authority","resolved_sum","high_precision_resolved_sum","high_precision","precision_diagnostic"],"type":"object"}}}}],"properties":{"artifact":{"type":["object","null"]},"execution_seconds_per_point":{"minimum":0,"type":["number","null"]},"failure":{"type":["object","null"]},"generation_seconds":{"minimum":0,"type":["number","null"]},"matrix_element":{"type":["number","string","null"]},"provenance":{"oneOf":[{"type":"null"},{"properties":{"generation_timer_excludes_model_preparation":{"const":true},"runtime_load_included_in_cold_warmup":{"const":false},"runtime_profile":{"dependentRequired":{"cold_warmup_acceptance_eligible":["cold_warmup_acceptance_eligible","cold_warmup_batch_size","cold_warmup_elapsed_seconds","cold_warmup_point_count","cold_warmup_ratio_eligible","cold_warmup_run_count","cold_warmup_runtime_cold_before_first_evaluation","cold_warmup_runtime_freshness","cold_warmup_runtime_retained_after_first_evaluation","cold_warmup_runtime_retained_before_first_evaluation","cold_warmup_runtime_state_after","cold_warmup_runtime_state_before","cold_warmup_runtime_state_evidence","cold_warmup_timer_source","cold_warmup_timing_scope","first_warmup_run_outer_wall_seconds","warmup_batch_size","warmup_configured_run_count","warmup_elapsed_seconds","warmup_point_count","warmup_run_outer_wall_seconds","warmup_timer_source","warmup_timing_scope"],"cold_warmup_batch_size":["cold_warmup_acceptance_eligible","cold_warmup_batch_size","cold_warmup_elapsed_seconds","cold_warmup_point_count","cold_warmup_ratio_eligible","cold_warmup_run_count","cold_warmup_runtime_cold_before_first_evaluation","cold_warmup_runtime_freshness","cold_warmup_runtime_retained_after_first_evaluation","cold_warmup_runtime_retained_before_first_evaluation","cold_warmup_runtime_state_after","cold_warmup_runtime_state_before","cold_warmup_runtime_state_evidence","cold_warmup_timer_source","cold_warmup_timing_scope","first_warmup_run_outer_wall_seconds","warmup_batch_size","warmup_configured_run_count","warmup_elapsed_seconds","warmup_point_count","warmup_run_outer_wall_seconds","warmup_timer_source","warmup_timing_scope"],"cold_warmup_elapsed_seconds":["cold_warmup_acceptance_eligible","cold_warmup_batch_size","cold_warmup_elapsed_seconds","cold_warmup_point_count","cold_warmup_ratio_eligible","cold_warmup_run_count","cold_warmup_runtime_cold_before_first_evaluation","cold_warmup_runtime_freshness","cold_warmup_runtime_retained_after_first_evaluation","cold_warmup_runtime_retained_before_first_evaluation","cold_warmup_runtime_state_after","cold_warmup_runtime_state_before","cold_warmup_runtime_state_evidence","cold_warmup_timer_source","cold_warmup_timing_scope","first_warmup_run_outer_wall_seconds","warmup_batch_size","warmup_configured_run_count","warmup_elapsed_seconds","warmup_point_count","warmup_run_outer_wall_seconds","warmup_timer_source","warmup_timing_scope"],"cold_warmup_point_count":["cold_warmup_acceptance_eligible","cold_warmup_batch_size","cold_warmup_elapsed_seconds","cold_warmup_point_count","cold_warmup_ratio_eligible","cold_warmup_run_count","cold_warmup_runtime_cold_before_first_evaluation","cold_warmup_runtime_freshness","cold_warmup_runtime_retained_after_first_evaluation","cold_warmup_runtime_retained_before_first_evaluation","cold_warmup_runtime_state_after","cold_warmup_runtime_state_before","cold_warmup_runtime_state_evidence","cold_warmup_timer_source","cold_warmup_timing_scope","first_warmup_run_outer_wall_seconds","warmup_batch_size","warmup_configured_run_count","warmup_elapsed_seconds","warmup_point_count","warmup_run_outer_wall_seconds","warmup_timer_source","warmup_timing_scope"],"cold_warmup_ratio_eligible":["cold_warmup_acceptance_eligible","cold_warmup_batch_size","cold_warmup_elapsed_seconds","cold_warmup_point_count","cold_warmup_ratio_eligible","cold_warmup_run_count","cold_warmup_runtime_cold_before_first_evaluation","cold_warmup_runtime_freshness","cold_warmup_runtime_retained_after_first_evaluation","cold_warmup_runtime_retained_before_first_evaluation","cold_warmup_runtime_state_after","cold_warmup_runtime_state_before","cold_warmup_runtime_state_evidence","cold_warmup_timer_source","cold_warmup_timing_scope","first_warmup_run_outer_wall_seconds","warmup_batch_size","warmup_configured_run_count","warmup_elapsed_seconds","warmup_point_count","warmup_run_outer_wall_seconds","warmup_timer_source","warmup_timing_scope"],"cold_warmup_run_count":["cold_warmup_acceptance_eligible","cold_warmup_batch_size","cold_warmup_elapsed_seconds","cold_warmup_point_count","cold_warmup_ratio_eligible","cold_warmup_run_count","cold_warmup_runtime_cold_before_first_evaluation","cold_warmup_runtime_freshness","cold_warmup_runtime_retained_after_first_evaluation","cold_warmup_runtime_retained_before_first_evaluation","cold_warmup_runtime_state_after","cold_warmup_runtime_state_before","cold_warmup_runtime_state_evidence","cold_warmup_timer_source","cold_warmup_timing_scope","first_warmup_run_outer_wall_seconds","warmup_batch_size","warmup_configured_run_count","warmup_elapsed_seconds","warmup_point_count","warmup_run_outer_wall_seconds","warmup_timer_source","warmup_timing_scope"],"cold_warmup_runtime_cold_before_first_evaluation":["cold_warmup_acceptance_eligible","cold_warmup_batch_size","cold_warmup_elapsed_seconds","cold_warmup_point_count","cold_warmup_ratio_eligible","cold_warmup_run_count","cold_warmup_runtime_cold_before_first_evaluation","cold_warmup_runtime_freshness","cold_warmup_runtime_retained_after_first_evaluation","cold_warmup_runtime_retained_before_first_evaluation","cold_warmup_runtime_state_after","cold_warmup_runtime_state_before","cold_warmup_runtime_state_evidence","cold_warmup_timer_source","cold_warmup_timing_scope","first_warmup_run_outer_wall_seconds","warmup_batch_size","warmup_configured_run_count","warmup_elapsed_seconds","warmup_point_count","warmup_run_outer_wall_seconds","warmup_timer_source","warmup_timing_scope"],"cold_warmup_runtime_freshness":["cold_warmup_acceptance_eligible","cold_warmup_batch_size","cold_warmup_elapsed_seconds","cold_warmup_point_count","cold_warmup_ratio_eligible","cold_warmup_run_count","cold_warmup_runtime_cold_before_first_evaluation","cold_warmup_runtime_freshness","cold_warmup_runtime_retained_after_first_evaluation","cold_warmup_runtime_retained_before_first_evaluation","cold_warmup_runtime_state_after","cold_warmup_runtime_state_before","cold_warmup_runtime_state_evidence","cold_warmup_timer_source","cold_warmup_timing_scope","first_warmup_run_outer_wall_seconds","warmup_batch_size","warmup_configured_run_count","warmup_elapsed_seconds","warmup_point_count","warmup_run_outer_wall_seconds","warmup_timer_source","warmup_timing_scope"],"cold_warmup_runtime_retained_after_first_evaluation":["cold_warmup_acceptance_eligible","cold_warmup_batch_size","cold_warmup_elapsed_seconds","cold_warmup_point_count","cold_warmup_ratio_eligible","cold_warmup_run_count","cold_warmup_runtime_cold_before_first_evaluation","cold_warmup_runtime_freshness","cold_warmup_runtime_retained_after_first_evaluation","cold_warmup_runtime_retained_before_first_evaluation","cold_warmup_runtime_state_after","cold_warmup_runtime_state_before","cold_warmup_runtime_state_evidence","cold_warmup_timer_source","cold_warmup_timing_scope","first_warmup_run_outer_wall_seconds","warmup_batch_size","warmup_configured_run_count","warmup_elapsed_seconds","warmup_point_count","warmup_run_outer_wall_seconds","warmup_timer_source","warmup_timing_scope"],"cold_warmup_runtime_retained_before_first_evaluation":["cold_warmup_acceptance_eligible","cold_warmup_batch_size","cold_warmup_elapsed_seconds","cold_warmup_point_count","cold_warmup_ratio_eligible","cold_warmup_run_count","cold_warmup_runtime_cold_before_first_evaluation","cold_warmup_runtime_freshness","cold_warmup_runtime_retained_after_first_evaluation","cold_warmup_runtime_retained_before_first_evaluation","cold_warmup_runtime_state_after","cold_warmup_runtime_state_before","cold_warmup_runtime_state_evidence","cold_warmup_timer_source","cold_warmup_timing_scope","first_warmup_run_outer_wall_seconds","warmup_batch_size","warmup_configured_run_count","warmup_elapsed_seconds","warmup_point_count","warmup_run_outer_wall_seconds","warmup_timer_source","warmup_timing_scope"],"cold_warmup_runtime_state_after":["cold_warmup_acceptance_eligible","cold_warmup_batch_size","cold_warmup_elapsed_seconds","cold_warmup_point_count","cold_warmup_ratio_eligible","cold_warmup_run_count","cold_warmup_runtime_cold_before_first_evaluation","cold_warmup_runtime_freshness","cold_warmup_runtime_retained_after_first_evaluation","cold_warmup_runtime_retained_before_first_evaluation","cold_warmup_runtime_state_after","cold_warmup_runtime_state_before","cold_warmup_runtime_state_evidence","cold_warmup_timer_source","cold_warmup_timing_scope","first_warmup_run_outer_wall_seconds","warmup_batch_size","warmup_configured_run_count","warmup_elapsed_seconds","warmup_point_count","warmup_run_outer_wall_seconds","warmup_timer_source","warmup_timing_scope"],"cold_warmup_runtime_state_before":["cold_warmup_acceptance_eligible","cold_warmup_batch_size","cold_warmup_elapsed_seconds","cold_warmup_point_count","cold_warmup_ratio_eligible","cold_warmup_run_count","cold_warmup_runtime_cold_before_first_evaluation","cold_warmup_runtime_freshness","cold_warmup_runtime_retained_after_first_evaluation","cold_warmup_runtime_retained_before_first_evaluation","cold_warmup_runtime_state_after","cold_warmup_runtime_state_before","cold_warmup_runtime_state_evidence","cold_warmup_timer_source","cold_warmup_timing_scope","first_warmup_run_outer_wall_seconds","warmup_batch_size","warmup_configured_run_count","warmup_elapsed_seconds","warmup_point_count","warmup_run_outer_wall_seconds","warmup_timer_source","warmup_timing_scope"],"cold_warmup_runtime_state_evidence":["cold_warmup_acceptance_eligible","cold_warmup_batch_size","cold_warmup_elapsed_seconds","cold_warmup_point_count","cold_warmup_ratio_eligible","cold_warmup_run_count","cold_warmup_runtime_cold_before_first_evaluation","cold_warmup_runtime_freshness","cold_warmup_runtime_retained_after_first_evaluation","cold_warmup_runtime_retained_before_first_evaluation","cold_warmup_runtime_state_after","cold_warmup_runtime_state_before","cold_warmup_runtime_state_evidence","cold_warmup_timer_source","cold_warmup_timing_scope","first_warmup_run_outer_wall_seconds","warmup_batch_size","warmup_configured_run_count","warmup_elapsed_seconds","warmup_point_count","warmup_run_outer_wall_seconds","warmup_timer_source","warmup_timing_scope"],"cold_warmup_timer_source":["cold_warmup_acceptance_eligible","cold_warmup_batch_size","cold_warmup_elapsed_seconds","cold_warmup_point_count","cold_warmup_ratio_eligible","cold_warmup_run_count","cold_warmup_runtime_cold_before_first_evaluation","cold_warmup_runtime_freshness","cold_warmup_runtime_retained_after_first_evaluation","cold_warmup_runtime_retained_before_first_evaluation","cold_warmup_runtime_state_after","cold_warmup_runtime_state_before","cold_warmup_runtime_state_evidence","cold_warmup_timer_source","cold_warmup_timing_scope","first_warmup_run_outer_wall_seconds","warmup_batch_size","warmup_configured_run_count","warmup_elapsed_seconds","warmup_point_count","warmup_run_outer_wall_seconds","warmup_timer_source","warmup_timing_scope"],"cold_warmup_timing_scope":["cold_warmup_acceptance_eligible","cold_warmup_batch_size","cold_warmup_elapsed_seconds","cold_warmup_point_count","cold_warmup_ratio_eligible","cold_warmup_run_count","cold_warmup_runtime_cold_before_first_evaluation","cold_warmup_runtime_freshness","cold_warmup_runtime_retained_after_first_evaluation","cold_warmup_runtime_retained_before_first_evaluation","cold_warmup_runtime_state_after","cold_warmup_runtime_state_before","cold_warmup_runtime_state_evidence","cold_warmup_timer_source","cold_warmup_timing_scope","first_warmup_run_outer_wall_seconds","warmup_batch_size","warmup_configured_run_count","warmup_elapsed_seconds","warmup_point_count","warmup_run_outer_wall_seconds","warmup_timer_source","warmup_timing_scope"]},"properties":{"cold_warmup_acceptance_eligible":{"const":false},"cold_warmup_batch_size":{"minimum":1,"type":"integer"},"cold_warmup_elapsed_seconds":{"exclusiveMinimum":0,"type":"number"},"cold_warmup_point_count":{"minimum":1,"type":"integer"},"cold_warmup_ratio_eligible":{"const":false},"cold_warmup_run_count":{"const":1},"cold_warmup_runtime_cold_before_first_evaluation":{"type":"boolean"},"cold_warmup_runtime_freshness":{"enum":["authenticated-already-retained","authenticated-cold"]},"cold_warmup_runtime_retained_after_first_evaluation":{"const":true},"cold_warmup_runtime_retained_before_first_evaluation":{"type":"boolean"},"cold_warmup_runtime_state_after":{"additionalProperties":false,"properties":{"active_family_union_census":{"oneOf":[{"type":"null"},{"additionalProperties":false,"properties":{"basis":{"const":"shared-query-family-union-v1"},"query_count":{"minimum":0,"type":"integer"},"scope":{"const":"active-family-union"},"union_amplitude_destination_count":{"minimum":0,"type":"integer"},"union_closure_executor_call_groups":{"minimum":0,"type":"integer"},"union_closure_rows":{"minimum":0,"type":"integer"},"union_contribution_executor_call_groups":{"minimum":0,"type":"integer"},"union_contribution_rows":{"minimum":0,"type":"integer"},"union_finalization_executor_call_groups":{"minimum":0,"type":"integer"},"union_finalization_rows":{"minimum":0,"type":"integer"},"union_source_executor_call_groups":{"minimum":0,"type":"integer"},"union_source_rows":{"minimum":0,"type":"integer"},"union_unique_current_component_count":{"minimum":0,"type":"integer"},"union_unique_current_count":{"minimum":0,"type":"integer"}},"required":["basis","query_count","scope","union_amplitude_destination_count","union_closure_executor_call_groups","union_closure_rows","union_contribution_executor_call_groups","union_contribution_rows","union_finalization_executor_call_groups","union_finalization_rows","union_source_executor_call_groups","union_source_rows","union_unique_current_component_count","union_unique_current_count"],"type":"object"}]},"family_cache_limit":{"const":1},"family_cache_policy":{"const":"last-family-only"},"kind":{"const":"rusticol-on-the-fly-runtime-state-census-v1"},"pending_family_count":{"minimum":0,"type":"integer"},"process_id":{"minLength":1,"type":"string"},"process_preparation_count":{"minimum":0,"type":"integer"},"retained_amplitude_destination_count":{"minimum":0,"type":"integer"},"retained_embedded_lookup_key_count":{"minimum":0,"type":"integer"},"retained_executor_handle_count":{"minimum":0,"type":"integer"},"retained_family_count":{"maximum":1,"minimum":0,"type":"integer"},"retained_query_local_trace_count":{"minimum":0,"type":"integer"},"retained_request_count":{"minimum":0,"type":"integer"},"retained_selection_count":{"minimum":0,"type":"integer"},"semantic_executor_binding_count":{"minimum":0,"type":"integer"}},"required":["active_family_union_census","family_cache_limit","family_cache_policy","kind","pending_family_count","process_id","process_preparation_count","retained_amplitude_destination_count","retained_embedded_lookup_key_count","retained_executor_handle_count","retained_family_count","retained_query_local_trace_count","retained_request_count","retained_selection_count","semantic_executor_binding_count"],"type":"object"},"cold_warmup_runtime_state_before":{"additionalProperties":false,"properties":{"active_family_union_census":{"oneOf":[{"type":"null"},{"additionalProperties":false,"properties":{"basis":{"const":"shared-query-family-union-v1"},"query_count":{"minimum":0,"type":"integer"},"scope":{"const":"active-family-union"},"union_amplitude_destination_count":{"minimum":0,"type":"integer"},"union_closure_executor_call_groups":{"minimum":0,"type":"integer"},"union_closure_rows":{"minimum":0,"type":"integer"},"union_contribution_executor_call_groups":{"minimum":0,"type":"integer"},"union_contribution_rows":{"minimum":0,"type":"integer"},"union_finalization_executor_call_groups":{"minimum":0,"type":"integer"},"union_finalization_rows":{"minimum":0,"type":"integer"},"union_source_executor_call_groups":{"minimum":0,"type":"integer"},"union_source_rows":{"minimum":0,"type":"integer"},"union_unique_current_component_count":{"minimum":0,"type":"integer"},"union_unique_current_count":{"minimum":0,"type":"integer"}},"required":["basis","query_count","scope","union_amplitude_destination_count","union_closure_executor_call_groups","union_closure_rows","union_contribution_executor_call_groups","union_contribution_rows","union_finalization_executor_call_groups","union_finalization_rows","union_source_executor_call_groups","union_source_rows","union_unique_current_component_count","union_unique_current_count"],"type":"object"}]},"family_cache_limit":{"const":1},"family_cache_policy":{"const":"last-family-only"},"kind":{"const":"rusticol-on-the-fly-runtime-state-census-v1"},"pending_family_count":{"minimum":0,"type":"integer"},"process_id":{"minLength":1,"type":"string"},"process_preparation_count":{"minimum":0,"type":"integer"},"retained_amplitude_destination_count":{"minimum":0,"type":"integer"},"retained_embedded_lookup_key_count":{"minimum":0,"type":"integer"},"retained_executor_handle_count":{"minimum":0,"type":"integer"},"retained_family_count":{"maximum":1,"minimum":0,"type":"integer"},"retained_query_local_trace_count":{"minimum":0,"type":"integer"},"retained_request_count":{"minimum":0,"type":"integer"},"retained_selection_count":{"minimum":0,"type":"integer"},"semantic_executor_binding_count":{"minimum":0,"type":"integer"}},"required":["active_family_union_census","family_cache_limit","family_cache_policy","kind","pending_family_count","process_id","process_preparation_count","retained_amplitude_destination_count","retained_embedded_lookup_key_count","retained_executor_handle_count","retained_family_count","retained_query_local_trace_count","retained_request_count","retained_selection_count","semantic_executor_binding_count"],"type":"object"},"cold_warmup_runtime_state_evidence":{"const":"authenticated-native-otf-census-v1"},"cold_warmup_timer_source":{"const":"python_outer_time.perf_counter"},"cold_warmup_timing_scope":{"const":"one initial requested-selector Runtime evaluation on the full benchmark batch; artifact generation and Runtime/artifact load are excluded"},"first_warmup_run_outer_wall_seconds":{"minimum":0,"type":["number","null"]},"warmup_batch_size":{"minimum":1,"type":"integer"},"warmup_configured_run_count":{"minimum":0,"type":"integer"},"warmup_elapsed_seconds":{"minimum":0,"type":"number"},"warmup_point_count":{"minimum":0,"type":"integer"},"warmup_run_outer_wall_seconds":{"items":{"minimum":0,"type":"number"},"type":"array"},"warmup_timer_source":{"const":"python_outer_time.perf_counter"},"warmup_timing_scope":{"const":"configured benchmark warm-up iteration outer wall; includes the headline evaluation and optional native-profile warm-up; artifact generation and Runtime/artifact load are excluded"}},"type":"object"}},"type":"object"}]},"relative_standard_error":{"minimum":0,"type":["number","null"]},"resources":{"type":["object","null"]},"sample_count":{"minimum":1,"type":["integer","null"]},"selector_contract":{"type":["object","null"]},"standard_error_seconds_per_point":{"minimum":0,"type":["number","null"]},"status":{"enum":["not_available","ok","failed","timeout","memory_limit","skip","validation_failed","unverified","error","unsupported"]},"validation":{"oneOf":[{"type":"null"},{"properties":{"direct_agreements":{"items":{"oneOf":[{"additionalProperties":false,"properties":{"abi":{"const":"pyamplicol-report-direct-agreement-v1"},"absolute_difference":{"minimum":0,"type":"number"},"absolute_tolerance":{"minimum":0,"type":"number"},"baseline":{"type":"number"},"baseline_cell_id":{"minLength":1,"type":"string"},"candidate":{"type":"number"},"candidate_cell_id":{"minLength":1,"type":"string"},"edge_kind":{"enum":["builtin-ufo-recurrence","z-recurrence-cross-mode","lc-cross-layout-component","lc-legacy-pyamplicol-component","madgraph-full-colour"]},"relative_difference":{"minimum":0,"type":"number"},"relative_tolerance":{"minimum":0,"type":"number"},"status":{"enum":["not_available","ok","failed","timeout","memory_limit","skip","validation_failed","unverified","error","unsupported"]},"value_kind":{"enum":["matrix_element","lc_common_component"]}},"required":["abi","edge_kind","value_kind","baseline_cell_id","candidate_cell_id","status","candidate","baseline","absolute_difference","relative_difference","relative_tolerance","absolute_tolerance"],"type":"object"},{"additionalProperties":false,"properties":{"abi":{"const":"pyamplicol-report-direct-agreement-v2"},"absolute_difference":{"minimum":0,"type":"number"},"baseline":{"type":"number"},"baseline_cell_id":{"minLength":1,"type":"string"},"baseline_scale":{"minimum":0,"type":"number"},"baseline_scale_source":{"minLength":1,"type":"string"},"candidate":{"type":"number"},"candidate_cell_id":{"minLength":1,"type":"string"},"candidate_scale":{"minimum":0,"type":"number"},"candidate_scale_source":{"minLength":1,"type":"string"},"comparison_binding":{"minProperties":1,"type":"object"},"comparison_scale":{"minimum":0,"type":"number"},"conditioned_residual":{"minimum":0,"type":"number"},"edge_kind":{"enum":["builtin-ufo-recurrence","z-recurrence-cross-mode","lc-cross-layout-component","lc-legacy-pyamplicol-component","madgraph-full-colour"]},"error_bound":{"minimum":0,"type":"number"},"relative_difference":{"minimum":0,"type":"number"},"relative_tolerance":{"minimum":0,"type":"number"},"status":{"enum":["not_available","ok","failed","timeout","memory_limit","skip","validation_failed","unverified","error","unsupported"]},"value_kind":{"enum":["matrix_element","lc_common_component"]}},"required":["abi","edge_kind","value_kind","baseline_cell_id","candidate_cell_id","status","candidate","baseline","candidate_scale","baseline_scale","candidate_scale_source","baseline_scale_source","comparison_scale","absolute_difference","relative_difference","conditioned_residual","error_bound","relative_tolerance","comparison_binding"],"type":"object"}]},"type":"array"},"independent_authority":{"additionalProperties":false,"properties":{"abi":{"const":"pyamplicol-report-independent-authority-v1"},"expected_cell_ids":{"items":{"minLength":1,"type":"string"},"type":"array","uniqueItems":true},"reason":{"minLength":1,"type":"string"},"same_artifact_diagnostics_are_authority":{"const":false},"selected_cell_id":{"type":["string","null"]},"status":{"enum":["unavailable","verified","mismatch","incompatible"]}},"required":["abi","expected_cell_ids","selected_cell_id","status","reason","same_artifact_diagnostics_are_authority"],"type":"object"},"lc_common_component":{"additionalProperties":false,"properties":{"abi":{"const":"pyamplicol-report-lc-common-component-v1"},"cell_id":{"minLength":1,"type":"string"},"color_flow_ids":{"items":{"minLength":1,"type":"string"},"minItems":1,"type":"array","uniqueItems":true},"helicity_ids":{"items":{"minLength":1,"type":"string"},"minItems":1,"type":"array","uniqueItems":true},"point_digest":{"pattern":"^[0-9a-f]{64}$","type":"string"},"value":{"type":"number"}},"required":["abi","cell_id","value","point_digest","helicity_ids","color_flow_ids"],"type":"object"},"madgraph_comparison":{"type":"object"},"on_the_fly_recurrence_authority":{"additionalProperties":false,"properties":{"abi":{"const":"pyamplicol-report-on-the-fly-recurrence-authority-validation-v1"},"after_clear":{"additionalProperties":false,"properties":{"candidate_resolved_ordering_sha256":{"pattern":"^[0-9a-f]{64}$","type":"string"},"candidate_resolved_source_sha256":{"pattern":"^[0-9a-f]{64}$","type":"string"},"candidate_resolved_sum":{"additionalProperties":false,"properties":{"check_count":{"minimum":1,"type":"integer"},"maximum_absolute_delta":{"minimum":0,"type":"number"},"maximum_conditioned_residual":{"minimum":0,"type":"number"}},"required":["check_count","maximum_conditioned_residual","maximum_absolute_delta"],"type":"object"},"candidate_resolved_total_source_sha256":{"pattern":"^[0-9a-f]{64}$","type":"string"},"candidate_total_source_sha256":{"pattern":"^[0-9a-f]{64}$","type":"string"},"comparison":{"additionalProperties":false,"properties":{"authority_cell_id":{"minLength":1,"type":"string"},"resolved_components":{"additionalProperties":false,"properties":{"check_count":{"minimum":1,"type":"integer"},"component_count":{"minimum":1,"type":"integer"},"maximum_absolute_delta":{"minimum":0,"type":"number"},"maximum_conditioned_residual":{"minimum":0,"type":"number"}},"required":["check_count","component_count","maximum_conditioned_residual","maximum_absolute_delta"],"type":"object"},"total":{"additionalProperties":false,"properties":{"check_count":{"minimum":1,"type":"integer"},"maximum_absolute_delta":{"minimum":0,"type":"number"},"maximum_conditioned_residual":{"minimum":0,"type":"number"}},"required":["check_count","maximum_conditioned_residual","maximum_absolute_delta"],"type":"object"}},"required":["authority_cell_id","total","resolved_components"],"type":"object"}},"required":["candidate_total_source_sha256","candidate_resolved_total_source_sha256","candidate_resolved_ordering_sha256","candidate_resolved_source_sha256","candidate_resolved_sum","comparison"],"type":"object"},"authority":{"additionalProperties":false,"properties":{"artifact_id":{"pattern":"^[0-9a-f]{64}$","type":"string"},"cell_id":{"minLength":1,"type":"string"},"resolved_ordering_sha256":{"pattern":"^[0-9a-f]{64}$","type":"string"},"resolved_source_sha256":{"pattern":"^[0-9a-f]{64}$","type":"string"},"resolved_sum":{"additionalProperties":false,"properties":{"check_count":{"minimum":1,"type":"integer"},"maximum_absolute_delta":{"minimum":0,"type":"number"},"maximum_conditioned_residual":{"minimum":0,"type":"number"}},"required":["check_count","maximum_conditioned_residual","maximum_absolute_delta"],"type":"object"},"resolved_total_source_sha256":{"pattern":"^[0-9a-f]{64}$","type":"string"},"total_source_sha256":{"pattern":"^[0-9a-f]{64}$","type":"string"}},"required":["cell_id","artifact_id","total_source_sha256","resolved_total_source_sha256","resolved_ordering_sha256","resolved_source_sha256","resolved_sum"],"type":"object"},"before_clear":{"additionalProperties":false,"properties":{"candidate_resolved_ordering_sha256":{"pattern":"^[0-9a-f]{64}$","type":"string"},"candidate_resolved_source_sha256":{"pattern":"^[0-9a-f]{64}$","type":"string"},"candidate_resolved_sum":{"additionalProperties":false,"properties":{"check_count":{"minimum":1,"type":"integer"},"maximum_absolute_delta":{"minimum":0,"type":"number"},"maximum_conditioned_residual":{"minimum":0,"type":"number"}},"required":["check_count","maximum_conditioned_residual","maximum_absolute_delta"],"type":"object"},"candidate_resolved_total_source_sha256":{"pattern":"^[0-9a-f]{64}$","type":"string"},"candidate_total_source_sha256":{"pattern":"^[0-9a-f]{64}$","type":"string"},"comparison":{"additionalProperties":false,"properties":{"authority_cell_id":{"minLength":1,"type":"string"},"resolved_components":{"additionalProperties":false,"properties":{"check_count":{"minimum":1,"type":"integer"},"component_count":{"minimum":1,"type":"integer"},"maximum_absolute_delta":{"minimum":0,"type":"number"},"maximum_conditioned_residual":{"minimum":0,"type":"number"}},"required":["check_count","component_count","maximum_conditioned_residual","maximum_absolute_delta"],"type":"object"},"total":{"additionalProperties":false,"properties":{"check_count":{"minimum":1,"type":"integer"},"maximum_absolute_delta":{"minimum":0,"type":"number"},"maximum_conditioned_residual":{"minimum":0,"type":"number"}},"required":["check_count","maximum_conditioned_residual","maximum_absolute_delta"],"type":"object"}},"required":["authority_cell_id","total","resolved_components"],"type":"object"}},"required":["candidate_total_source_sha256","candidate_resolved_total_source_sha256","candidate_resolved_ordering_sha256","candidate_resolved_source_sha256","candidate_resolved_sum","comparison"],"type":"object"},"candidate_artifact_id":{"pattern":"^[0-9a-f]{64}$","type":"string"},"candidate_cell_id":{"minLength":1,"type":"string"},"lifecycle":{"additionalProperties":false,"properties":{"authority_artifact_loaded_only":{"const":true},"candidate_loaded_before_validation":{"const":true},"clear_call_count":{"const":2},"final_clear_before_profile":{"const":true},"validated_after_clear":{"const":true},"validated_before_clear":{"const":true}},"required":["authority_artifact_loaded_only","candidate_loaded_before_validation","validated_before_clear","validated_after_clear","clear_call_count","final_clear_before_profile"],"type":"object"},"point_count":{"minimum":1,"type":"integer"},"point_digest":{"pattern":"^[0-9a-f]{64}$","type":"string"},"precision_digits":{"const":16},"relative_tolerance":{"const":1e-12},"resolved_check_count":{"minimum":1,"type":"integer"},"resolved_component_count":{"minimum":1,"type":"integer"},"selector_sha256":{"pattern":"^[0-9a-f]{64}$","type":"string"},"status":{"const":"ok"},"workload":{"enum":["selected-flow","all-flow"]}},"required":["abi","status","candidate_cell_id","candidate_artifact_id","workload","precision_digits","relative_tolerance","point_digest","selector_sha256","point_count","resolved_component_count","resolved_check_count","authority","before_clear","after_clear","lifecycle"],"type":"object"},"status":{"enum":["not_available","ok","failed","timeout","memory_limit","skip","validation_failed","unverified","error","unsupported"]}},"type":"object"}]},"wall_seconds_per_point":{"minimum":0,"type":["number","null"]}},"required":["status","generation_seconds","wall_seconds_per_point","execution_seconds_per_point","matrix_element","sample_count","standard_error_seconds_per_point","relative_standard_error","artifact","selector_contract","validation","resources","provenance","failure"],"type":"object"},"measurement_spec":{"type":"object"},"n_final":{"minimum":1,"type":"integer"},"process":{"minLength":1,"type":"string"},"process_key":{"type":["string","null"]},"variant":{"type":["string","null"]},"workload":{"enum":["selected-flow","all-flow","contracted"]}},"required":["cell_id","process_key","process","n_final","variant","workload","measurement_spec","measurement"],"type":"object"},"type":"array"},"kind":{"const":"performance_measurements"},"measurement_specs":{"items":{"type":"object"},"type":"array"},"report_version":{"const":"0.3.0"},"schema_version":{"const":5}},"required":["$schema","schema_version","report_version","kind","dataset_id","measurement_specs","entries"],"title":"pyAmpliCol performance cache","type":"object"} diff --git a/pyamplicol/src/pyamplicol/_profiling_campaign/results/scalar_contact.json b/pyamplicol/src/pyamplicol/_profiling_campaign/results/scalar_contact.json new file mode 100644 index 0000000000..4fccc05d98 --- /dev/null +++ b/pyamplicol/src/pyamplicol/_profiling_campaign/results/scalar_contact.json @@ -0,0 +1 @@ +{"$schema":"./report-cache.schema.json","dataset_id":"scalar_contact","entries":[{"cell_id":"scalar-contact-n2-scalar-contact-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"scalar_contact"},"n_final":2,"process":"scalar_0 scalar_0 > scalar_0 scalar_0","process_key":"scalar_contact","variant":null,"workload":"contracted"},{"cell_id":"scalar-contact-n3-scalar-contact-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"scalar_contact"},"n_final":3,"process":"scalar_0 scalar_0 > scalar_0 scalar_0 scalar_0","process_key":"scalar_contact","variant":null,"workload":"contracted"},{"cell_id":"scalar-contact-n4-scalar-contact-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"scalar_contact"},"n_final":4,"process":"scalar_0 scalar_0 > scalar_0 scalar_0 scalar_0 scalar_0","process_key":"scalar_contact","variant":null,"workload":"contracted"},{"cell_id":"scalar-contact-n5-scalar-contact-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"scalar_contact"},"n_final":5,"process":"scalar_0 scalar_0 > scalar_0 scalar_0 scalar_0 scalar_0 scalar_0","process_key":"scalar_contact","variant":null,"workload":"contracted"},{"cell_id":"scalar-contact-n6-scalar-contact-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"scalar_contact"},"n_final":6,"process":"scalar_0 scalar_0 > scalar_0 scalar_0 scalar_0 scalar_0 scalar_0 scalar_0","process_key":"scalar_contact","variant":null,"workload":"contracted"},{"cell_id":"scalar-contact-n7-scalar-contact-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"scalar_contact"},"n_final":7,"process":"scalar_0 scalar_0 > scalar_0 scalar_0 scalar_0 scalar_0 scalar_0 scalar_0 scalar_0","process_key":"scalar_contact","variant":null,"workload":"contracted"},{"cell_id":"scalar-contact-n8-scalar-contact-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"scalar_contact"},"n_final":8,"process":"scalar_0 scalar_0 > scalar_0 scalar_0 scalar_0 scalar_0 scalar_0 scalar_0 scalar_0 scalar_0","process_key":"scalar_contact","variant":null,"workload":"contracted"}],"kind":"performance_measurements","measurement_specs":[{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"scalar_contact"}],"report_version":"0.3.0","schema_version":5} diff --git a/pyamplicol/src/pyamplicol/_profiling_campaign/results/scalar_gravity.json b/pyamplicol/src/pyamplicol/_profiling_campaign/results/scalar_gravity.json new file mode 100644 index 0000000000..f4ea7c5afd --- /dev/null +++ b/pyamplicol/src/pyamplicol/_profiling_campaign/results/scalar_gravity.json @@ -0,0 +1 @@ +{"$schema":"./report-cache.schema.json","dataset_id":"scalar_gravity","entries":[{"cell_id":"scalar-gravity-n2-scalar-gravity-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"scalar_gravity"},"n_final":2,"process":"scalar_0 scalar_0 > graviton graviton","process_key":"scalar_gravity","variant":null,"workload":"contracted"},{"cell_id":"scalar-gravity-n3-scalar-gravity-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"scalar_gravity"},"n_final":3,"process":"scalar_0 scalar_0 > graviton graviton graviton","process_key":"scalar_gravity","variant":null,"workload":"contracted"},{"cell_id":"scalar-gravity-n4-scalar-gravity-contracted","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"scalar_gravity"},"n_final":4,"process":"scalar_0 scalar_0 > graviton graviton graviton graviton","process_key":"scalar_gravity","variant":null,"workload":"contracted"}],"kind":"performance_measurements","measurement_specs":[{"accuracy":"full","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"scalar_gravity"}],"report_version":"0.3.0","schema_version":5} diff --git a/pyamplicol/src/pyamplicol/_profiling_campaign/results/z_builtin_sm.json b/pyamplicol/src/pyamplicol/_profiling_campaign/results/z_builtin_sm.json new file mode 100644 index 0000000000..12340896f4 --- /dev/null +++ b/pyamplicol/src/pyamplicol/_profiling_campaign/results/z_builtin_sm.json @@ -0,0 +1 @@ +{"$schema":"./report-cache.schema.json","dataset_id":"z_builtin_sm","entries":[{"cell_id":"z-builtin-sm-n1-dd-z-jets-asm-o3-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"asm","execution_mode":"compiled","jit_optimization_level":null,"model":"builtin_sm"},"n_final":1,"process":"d d~ > z","process_key":"dd_z_jets","variant":"asm_o3","workload":"all-flow"},{"cell_id":"z-builtin-sm-n1-dd-z-jets-asm-o3-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"asm","execution_mode":"compiled","jit_optimization_level":null,"model":"builtin_sm"},"n_final":1,"process":"d d~ > z","process_key":"dd_z_jets","variant":"asm_o3","workload":"selected-flow"},{"cell_id":"z-builtin-sm-n1-dd-z-jets-cpp-o3-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"cpp","execution_mode":"compiled","jit_optimization_level":null,"model":"builtin_sm"},"n_final":1,"process":"d d~ > z","process_key":"dd_z_jets","variant":"cpp_o3","workload":"all-flow"},{"cell_id":"z-builtin-sm-n1-dd-z-jets-cpp-o3-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"cpp","execution_mode":"compiled","jit_optimization_level":null,"model":"builtin_sm"},"n_final":1,"process":"d d~ > z","process_key":"dd_z_jets","variant":"cpp_o3","workload":"selected-flow"},{"cell_id":"z-builtin-sm-n1-dd-z-jets-eager-jit-o2-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":1,"process":"d d~ > z","process_key":"dd_z_jets","variant":"eager_jit_o2","workload":"all-flow"},{"cell_id":"z-builtin-sm-n1-dd-z-jets-eager-jit-o2-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":1,"process":"d d~ > z","process_key":"dd_z_jets","variant":"eager_jit_o2","workload":"selected-flow"},{"cell_id":"z-builtin-sm-n1-dd-z-jets-jit-o1-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":1,"model":"builtin_sm"},"n_final":1,"process":"d d~ > z","process_key":"dd_z_jets","variant":"jit_o1","workload":"all-flow"},{"cell_id":"z-builtin-sm-n1-dd-z-jets-jit-o1-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":1,"model":"builtin_sm"},"n_final":1,"process":"d d~ > z","process_key":"dd_z_jets","variant":"jit_o1","workload":"selected-flow"},{"cell_id":"z-builtin-sm-n1-dd-z-jets-jit-o3-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":1,"process":"d d~ > z","process_key":"dd_z_jets","variant":"jit_o3","workload":"all-flow"},{"cell_id":"z-builtin-sm-n1-dd-z-jets-jit-o3-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":1,"process":"d d~ > z","process_key":"dd_z_jets","variant":"jit_o3","workload":"selected-flow"},{"cell_id":"z-builtin-sm-n1-dd-z-jets-on-the-fly-jit-o2-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":1,"process":"d d~ > z","process_key":"dd_z_jets","variant":"on_the_fly_jit_o2","workload":"all-flow"},{"cell_id":"z-builtin-sm-n1-dd-z-jets-on-the-fly-jit-o2-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":1,"process":"d d~ > z","process_key":"dd_z_jets","variant":"on_the_fly_jit_o2","workload":"selected-flow"},{"cell_id":"z-builtin-sm-n1-dd-z-jets-recurrence-jit-o2-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":1,"process":"d d~ > z","process_key":"dd_z_jets","variant":"recurrence_jit_o2","workload":"all-flow"},{"cell_id":"z-builtin-sm-n1-dd-z-jets-recurrence-jit-o2-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":1,"process":"d d~ > z","process_key":"dd_z_jets","variant":"recurrence_jit_o2","workload":"selected-flow"},{"cell_id":"z-builtin-sm-n2-dd-z-jets-asm-o3-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"asm","execution_mode":"compiled","jit_optimization_level":null,"model":"builtin_sm"},"n_final":2,"process":"d d~ > z g","process_key":"dd_z_jets","variant":"asm_o3","workload":"all-flow"},{"cell_id":"z-builtin-sm-n2-dd-z-jets-asm-o3-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"asm","execution_mode":"compiled","jit_optimization_level":null,"model":"builtin_sm"},"n_final":2,"process":"d d~ > z g","process_key":"dd_z_jets","variant":"asm_o3","workload":"selected-flow"},{"cell_id":"z-builtin-sm-n2-dd-z-jets-cpp-o3-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"cpp","execution_mode":"compiled","jit_optimization_level":null,"model":"builtin_sm"},"n_final":2,"process":"d d~ > z g","process_key":"dd_z_jets","variant":"cpp_o3","workload":"all-flow"},{"cell_id":"z-builtin-sm-n2-dd-z-jets-cpp-o3-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"cpp","execution_mode":"compiled","jit_optimization_level":null,"model":"builtin_sm"},"n_final":2,"process":"d d~ > z g","process_key":"dd_z_jets","variant":"cpp_o3","workload":"selected-flow"},{"cell_id":"z-builtin-sm-n2-dd-z-jets-eager-jit-o2-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":2,"process":"d d~ > z g","process_key":"dd_z_jets","variant":"eager_jit_o2","workload":"all-flow"},{"cell_id":"z-builtin-sm-n2-dd-z-jets-eager-jit-o2-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":2,"process":"d d~ > z g","process_key":"dd_z_jets","variant":"eager_jit_o2","workload":"selected-flow"},{"cell_id":"z-builtin-sm-n2-dd-z-jets-jit-o1-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":1,"model":"builtin_sm"},"n_final":2,"process":"d d~ > z g","process_key":"dd_z_jets","variant":"jit_o1","workload":"all-flow"},{"cell_id":"z-builtin-sm-n2-dd-z-jets-jit-o1-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":1,"model":"builtin_sm"},"n_final":2,"process":"d d~ > z g","process_key":"dd_z_jets","variant":"jit_o1","workload":"selected-flow"},{"cell_id":"z-builtin-sm-n2-dd-z-jets-jit-o3-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":2,"process":"d d~ > z g","process_key":"dd_z_jets","variant":"jit_o3","workload":"all-flow"},{"cell_id":"z-builtin-sm-n2-dd-z-jets-jit-o3-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":2,"process":"d d~ > z g","process_key":"dd_z_jets","variant":"jit_o3","workload":"selected-flow"},{"cell_id":"z-builtin-sm-n2-dd-z-jets-on-the-fly-jit-o2-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":2,"process":"d d~ > z g","process_key":"dd_z_jets","variant":"on_the_fly_jit_o2","workload":"all-flow"},{"cell_id":"z-builtin-sm-n2-dd-z-jets-on-the-fly-jit-o2-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":2,"process":"d d~ > z g","process_key":"dd_z_jets","variant":"on_the_fly_jit_o2","workload":"selected-flow"},{"cell_id":"z-builtin-sm-n2-dd-z-jets-recurrence-jit-o2-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":2,"process":"d d~ > z g","process_key":"dd_z_jets","variant":"recurrence_jit_o2","workload":"all-flow"},{"cell_id":"z-builtin-sm-n2-dd-z-jets-recurrence-jit-o2-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":2,"process":"d d~ > z g","process_key":"dd_z_jets","variant":"recurrence_jit_o2","workload":"selected-flow"},{"cell_id":"z-builtin-sm-n3-dd-z-jets-asm-o3-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"asm","execution_mode":"compiled","jit_optimization_level":null,"model":"builtin_sm"},"n_final":3,"process":"d d~ > z g g","process_key":"dd_z_jets","variant":"asm_o3","workload":"all-flow"},{"cell_id":"z-builtin-sm-n3-dd-z-jets-asm-o3-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"asm","execution_mode":"compiled","jit_optimization_level":null,"model":"builtin_sm"},"n_final":3,"process":"d d~ > z g g","process_key":"dd_z_jets","variant":"asm_o3","workload":"selected-flow"},{"cell_id":"z-builtin-sm-n3-dd-z-jets-cpp-o3-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"cpp","execution_mode":"compiled","jit_optimization_level":null,"model":"builtin_sm"},"n_final":3,"process":"d d~ > z g g","process_key":"dd_z_jets","variant":"cpp_o3","workload":"all-flow"},{"cell_id":"z-builtin-sm-n3-dd-z-jets-cpp-o3-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"cpp","execution_mode":"compiled","jit_optimization_level":null,"model":"builtin_sm"},"n_final":3,"process":"d d~ > z g g","process_key":"dd_z_jets","variant":"cpp_o3","workload":"selected-flow"},{"cell_id":"z-builtin-sm-n3-dd-z-jets-eager-jit-o2-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"d d~ > z g g","process_key":"dd_z_jets","variant":"eager_jit_o2","workload":"all-flow"},{"cell_id":"z-builtin-sm-n3-dd-z-jets-eager-jit-o2-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"d d~ > z g g","process_key":"dd_z_jets","variant":"eager_jit_o2","workload":"selected-flow"},{"cell_id":"z-builtin-sm-n3-dd-z-jets-jit-o1-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":1,"model":"builtin_sm"},"n_final":3,"process":"d d~ > z g g","process_key":"dd_z_jets","variant":"jit_o1","workload":"all-flow"},{"cell_id":"z-builtin-sm-n3-dd-z-jets-jit-o1-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":1,"model":"builtin_sm"},"n_final":3,"process":"d d~ > z g g","process_key":"dd_z_jets","variant":"jit_o1","workload":"selected-flow"},{"cell_id":"z-builtin-sm-n3-dd-z-jets-jit-o3-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":3,"process":"d d~ > z g g","process_key":"dd_z_jets","variant":"jit_o3","workload":"all-flow"},{"cell_id":"z-builtin-sm-n3-dd-z-jets-jit-o3-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":3,"process":"d d~ > z g g","process_key":"dd_z_jets","variant":"jit_o3","workload":"selected-flow"},{"cell_id":"z-builtin-sm-n3-dd-z-jets-on-the-fly-jit-o2-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"d d~ > z g g","process_key":"dd_z_jets","variant":"on_the_fly_jit_o2","workload":"all-flow"},{"cell_id":"z-builtin-sm-n3-dd-z-jets-on-the-fly-jit-o2-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"d d~ > z g g","process_key":"dd_z_jets","variant":"on_the_fly_jit_o2","workload":"selected-flow"},{"cell_id":"z-builtin-sm-n3-dd-z-jets-recurrence-jit-o2-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"d d~ > z g g","process_key":"dd_z_jets","variant":"recurrence_jit_o2","workload":"all-flow"},{"cell_id":"z-builtin-sm-n3-dd-z-jets-recurrence-jit-o2-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":3,"process":"d d~ > z g g","process_key":"dd_z_jets","variant":"recurrence_jit_o2","workload":"selected-flow"},{"cell_id":"z-builtin-sm-n4-dd-z-jets-asm-o3-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"asm","execution_mode":"compiled","jit_optimization_level":null,"model":"builtin_sm"},"n_final":4,"process":"d d~ > z g g g","process_key":"dd_z_jets","variant":"asm_o3","workload":"all-flow"},{"cell_id":"z-builtin-sm-n4-dd-z-jets-asm-o3-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"asm","execution_mode":"compiled","jit_optimization_level":null,"model":"builtin_sm"},"n_final":4,"process":"d d~ > z g g g","process_key":"dd_z_jets","variant":"asm_o3","workload":"selected-flow"},{"cell_id":"z-builtin-sm-n4-dd-z-jets-cpp-o3-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"cpp","execution_mode":"compiled","jit_optimization_level":null,"model":"builtin_sm"},"n_final":4,"process":"d d~ > z g g g","process_key":"dd_z_jets","variant":"cpp_o3","workload":"all-flow"},{"cell_id":"z-builtin-sm-n4-dd-z-jets-cpp-o3-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"cpp","execution_mode":"compiled","jit_optimization_level":null,"model":"builtin_sm"},"n_final":4,"process":"d d~ > z g g g","process_key":"dd_z_jets","variant":"cpp_o3","workload":"selected-flow"},{"cell_id":"z-builtin-sm-n4-dd-z-jets-eager-jit-o2-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > z g g g","process_key":"dd_z_jets","variant":"eager_jit_o2","workload":"all-flow"},{"cell_id":"z-builtin-sm-n4-dd-z-jets-eager-jit-o2-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > z g g g","process_key":"dd_z_jets","variant":"eager_jit_o2","workload":"selected-flow"},{"cell_id":"z-builtin-sm-n4-dd-z-jets-jit-o1-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":1,"model":"builtin_sm"},"n_final":4,"process":"d d~ > z g g g","process_key":"dd_z_jets","variant":"jit_o1","workload":"all-flow"},{"cell_id":"z-builtin-sm-n4-dd-z-jets-jit-o1-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":1,"model":"builtin_sm"},"n_final":4,"process":"d d~ > z g g g","process_key":"dd_z_jets","variant":"jit_o1","workload":"selected-flow"},{"cell_id":"z-builtin-sm-n4-dd-z-jets-jit-o3-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":4,"process":"d d~ > z g g g","process_key":"dd_z_jets","variant":"jit_o3","workload":"all-flow"},{"cell_id":"z-builtin-sm-n4-dd-z-jets-jit-o3-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":4,"process":"d d~ > z g g g","process_key":"dd_z_jets","variant":"jit_o3","workload":"selected-flow"},{"cell_id":"z-builtin-sm-n4-dd-z-jets-on-the-fly-jit-o2-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > z g g g","process_key":"dd_z_jets","variant":"on_the_fly_jit_o2","workload":"all-flow"},{"cell_id":"z-builtin-sm-n4-dd-z-jets-on-the-fly-jit-o2-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > z g g g","process_key":"dd_z_jets","variant":"on_the_fly_jit_o2","workload":"selected-flow"},{"cell_id":"z-builtin-sm-n4-dd-z-jets-recurrence-jit-o2-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > z g g g","process_key":"dd_z_jets","variant":"recurrence_jit_o2","workload":"all-flow"},{"cell_id":"z-builtin-sm-n4-dd-z-jets-recurrence-jit-o2-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":4,"process":"d d~ > z g g g","process_key":"dd_z_jets","variant":"recurrence_jit_o2","workload":"selected-flow"},{"cell_id":"z-builtin-sm-n5-dd-z-jets-asm-o3-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"asm","execution_mode":"compiled","jit_optimization_level":null,"model":"builtin_sm"},"n_final":5,"process":"d d~ > z g g g g","process_key":"dd_z_jets","variant":"asm_o3","workload":"all-flow"},{"cell_id":"z-builtin-sm-n5-dd-z-jets-asm-o3-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"asm","execution_mode":"compiled","jit_optimization_level":null,"model":"builtin_sm"},"n_final":5,"process":"d d~ > z g g g g","process_key":"dd_z_jets","variant":"asm_o3","workload":"selected-flow"},{"cell_id":"z-builtin-sm-n5-dd-z-jets-cpp-o3-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"cpp","execution_mode":"compiled","jit_optimization_level":null,"model":"builtin_sm"},"n_final":5,"process":"d d~ > z g g g g","process_key":"dd_z_jets","variant":"cpp_o3","workload":"all-flow"},{"cell_id":"z-builtin-sm-n5-dd-z-jets-cpp-o3-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"cpp","execution_mode":"compiled","jit_optimization_level":null,"model":"builtin_sm"},"n_final":5,"process":"d d~ > z g g g g","process_key":"dd_z_jets","variant":"cpp_o3","workload":"selected-flow"},{"cell_id":"z-builtin-sm-n5-dd-z-jets-eager-jit-o2-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"d d~ > z g g g g","process_key":"dd_z_jets","variant":"eager_jit_o2","workload":"all-flow"},{"cell_id":"z-builtin-sm-n5-dd-z-jets-eager-jit-o2-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"d d~ > z g g g g","process_key":"dd_z_jets","variant":"eager_jit_o2","workload":"selected-flow"},{"cell_id":"z-builtin-sm-n5-dd-z-jets-jit-o1-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":1,"model":"builtin_sm"},"n_final":5,"process":"d d~ > z g g g g","process_key":"dd_z_jets","variant":"jit_o1","workload":"all-flow"},{"cell_id":"z-builtin-sm-n5-dd-z-jets-jit-o1-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":1,"model":"builtin_sm"},"n_final":5,"process":"d d~ > z g g g g","process_key":"dd_z_jets","variant":"jit_o1","workload":"selected-flow"},{"cell_id":"z-builtin-sm-n5-dd-z-jets-jit-o3-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":5,"process":"d d~ > z g g g g","process_key":"dd_z_jets","variant":"jit_o3","workload":"all-flow"},{"cell_id":"z-builtin-sm-n5-dd-z-jets-jit-o3-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":5,"process":"d d~ > z g g g g","process_key":"dd_z_jets","variant":"jit_o3","workload":"selected-flow"},{"cell_id":"z-builtin-sm-n5-dd-z-jets-on-the-fly-jit-o2-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"d d~ > z g g g g","process_key":"dd_z_jets","variant":"on_the_fly_jit_o2","workload":"all-flow"},{"cell_id":"z-builtin-sm-n5-dd-z-jets-on-the-fly-jit-o2-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"d d~ > z g g g g","process_key":"dd_z_jets","variant":"on_the_fly_jit_o2","workload":"selected-flow"},{"cell_id":"z-builtin-sm-n5-dd-z-jets-recurrence-jit-o2-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"d d~ > z g g g g","process_key":"dd_z_jets","variant":"recurrence_jit_o2","workload":"all-flow"},{"cell_id":"z-builtin-sm-n5-dd-z-jets-recurrence-jit-o2-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":5,"process":"d d~ > z g g g g","process_key":"dd_z_jets","variant":"recurrence_jit_o2","workload":"selected-flow"},{"cell_id":"z-builtin-sm-n6-dd-z-jets-asm-o3-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"asm","execution_mode":"compiled","jit_optimization_level":null,"model":"builtin_sm"},"n_final":6,"process":"d d~ > z g g g g g","process_key":"dd_z_jets","variant":"asm_o3","workload":"all-flow"},{"cell_id":"z-builtin-sm-n6-dd-z-jets-asm-o3-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"asm","execution_mode":"compiled","jit_optimization_level":null,"model":"builtin_sm"},"n_final":6,"process":"d d~ > z g g g g g","process_key":"dd_z_jets","variant":"asm_o3","workload":"selected-flow"},{"cell_id":"z-builtin-sm-n6-dd-z-jets-cpp-o3-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"cpp","execution_mode":"compiled","jit_optimization_level":null,"model":"builtin_sm"},"n_final":6,"process":"d d~ > z g g g g g","process_key":"dd_z_jets","variant":"cpp_o3","workload":"all-flow"},{"cell_id":"z-builtin-sm-n6-dd-z-jets-cpp-o3-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"cpp","execution_mode":"compiled","jit_optimization_level":null,"model":"builtin_sm"},"n_final":6,"process":"d d~ > z g g g g g","process_key":"dd_z_jets","variant":"cpp_o3","workload":"selected-flow"},{"cell_id":"z-builtin-sm-n6-dd-z-jets-eager-jit-o2-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":6,"process":"d d~ > z g g g g g","process_key":"dd_z_jets","variant":"eager_jit_o2","workload":"all-flow"},{"cell_id":"z-builtin-sm-n6-dd-z-jets-eager-jit-o2-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":6,"process":"d d~ > z g g g g g","process_key":"dd_z_jets","variant":"eager_jit_o2","workload":"selected-flow"},{"cell_id":"z-builtin-sm-n6-dd-z-jets-jit-o1-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":1,"model":"builtin_sm"},"n_final":6,"process":"d d~ > z g g g g g","process_key":"dd_z_jets","variant":"jit_o1","workload":"all-flow"},{"cell_id":"z-builtin-sm-n6-dd-z-jets-jit-o1-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":1,"model":"builtin_sm"},"n_final":6,"process":"d d~ > z g g g g g","process_key":"dd_z_jets","variant":"jit_o1","workload":"selected-flow"},{"cell_id":"z-builtin-sm-n6-dd-z-jets-jit-o3-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":6,"process":"d d~ > z g g g g g","process_key":"dd_z_jets","variant":"jit_o3","workload":"all-flow"},{"cell_id":"z-builtin-sm-n6-dd-z-jets-jit-o3-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":6,"process":"d d~ > z g g g g g","process_key":"dd_z_jets","variant":"jit_o3","workload":"selected-flow"},{"cell_id":"z-builtin-sm-n6-dd-z-jets-on-the-fly-jit-o2-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":6,"process":"d d~ > z g g g g g","process_key":"dd_z_jets","variant":"on_the_fly_jit_o2","workload":"all-flow"},{"cell_id":"z-builtin-sm-n6-dd-z-jets-on-the-fly-jit-o2-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":6,"process":"d d~ > z g g g g g","process_key":"dd_z_jets","variant":"on_the_fly_jit_o2","workload":"selected-flow"},{"cell_id":"z-builtin-sm-n6-dd-z-jets-recurrence-jit-o2-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":6,"process":"d d~ > z g g g g g","process_key":"dd_z_jets","variant":"recurrence_jit_o2","workload":"all-flow"},{"cell_id":"z-builtin-sm-n6-dd-z-jets-recurrence-jit-o2-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":6,"process":"d d~ > z g g g g g","process_key":"dd_z_jets","variant":"recurrence_jit_o2","workload":"selected-flow"},{"cell_id":"z-builtin-sm-n7-dd-z-jets-asm-o3-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"asm","execution_mode":"compiled","jit_optimization_level":null,"model":"builtin_sm"},"n_final":7,"process":"d d~ > z g g g g g g","process_key":"dd_z_jets","variant":"asm_o3","workload":"all-flow"},{"cell_id":"z-builtin-sm-n7-dd-z-jets-asm-o3-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"asm","execution_mode":"compiled","jit_optimization_level":null,"model":"builtin_sm"},"n_final":7,"process":"d d~ > z g g g g g g","process_key":"dd_z_jets","variant":"asm_o3","workload":"selected-flow"},{"cell_id":"z-builtin-sm-n7-dd-z-jets-cpp-o3-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"cpp","execution_mode":"compiled","jit_optimization_level":null,"model":"builtin_sm"},"n_final":7,"process":"d d~ > z g g g g g g","process_key":"dd_z_jets","variant":"cpp_o3","workload":"all-flow"},{"cell_id":"z-builtin-sm-n7-dd-z-jets-cpp-o3-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"cpp","execution_mode":"compiled","jit_optimization_level":null,"model":"builtin_sm"},"n_final":7,"process":"d d~ > z g g g g g g","process_key":"dd_z_jets","variant":"cpp_o3","workload":"selected-flow"},{"cell_id":"z-builtin-sm-n7-dd-z-jets-eager-jit-o2-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":7,"process":"d d~ > z g g g g g g","process_key":"dd_z_jets","variant":"eager_jit_o2","workload":"all-flow"},{"cell_id":"z-builtin-sm-n7-dd-z-jets-eager-jit-o2-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":7,"process":"d d~ > z g g g g g g","process_key":"dd_z_jets","variant":"eager_jit_o2","workload":"selected-flow"},{"cell_id":"z-builtin-sm-n7-dd-z-jets-jit-o1-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":1,"model":"builtin_sm"},"n_final":7,"process":"d d~ > z g g g g g g","process_key":"dd_z_jets","variant":"jit_o1","workload":"all-flow"},{"cell_id":"z-builtin-sm-n7-dd-z-jets-jit-o1-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":1,"model":"builtin_sm"},"n_final":7,"process":"d d~ > z g g g g g g","process_key":"dd_z_jets","variant":"jit_o1","workload":"selected-flow"},{"cell_id":"z-builtin-sm-n7-dd-z-jets-jit-o3-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":7,"process":"d d~ > z g g g g g g","process_key":"dd_z_jets","variant":"jit_o3","workload":"all-flow"},{"cell_id":"z-builtin-sm-n7-dd-z-jets-jit-o3-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":7,"process":"d d~ > z g g g g g g","process_key":"dd_z_jets","variant":"jit_o3","workload":"selected-flow"},{"cell_id":"z-builtin-sm-n7-dd-z-jets-on-the-fly-jit-o2-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":7,"process":"d d~ > z g g g g g g","process_key":"dd_z_jets","variant":"on_the_fly_jit_o2","workload":"all-flow"},{"cell_id":"z-builtin-sm-n7-dd-z-jets-on-the-fly-jit-o2-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":7,"process":"d d~ > z g g g g g g","process_key":"dd_z_jets","variant":"on_the_fly_jit_o2","workload":"selected-flow"},{"cell_id":"z-builtin-sm-n7-dd-z-jets-recurrence-jit-o2-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":7,"process":"d d~ > z g g g g g g","process_key":"dd_z_jets","variant":"recurrence_jit_o2","workload":"all-flow"},{"cell_id":"z-builtin-sm-n7-dd-z-jets-recurrence-jit-o2-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":7,"process":"d d~ > z g g g g g g","process_key":"dd_z_jets","variant":"recurrence_jit_o2","workload":"selected-flow"},{"cell_id":"z-builtin-sm-n8-dd-z-jets-asm-o3-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"asm","execution_mode":"compiled","jit_optimization_level":null,"model":"builtin_sm"},"n_final":8,"process":"d d~ > z g g g g g g g","process_key":"dd_z_jets","variant":"asm_o3","workload":"all-flow"},{"cell_id":"z-builtin-sm-n8-dd-z-jets-asm-o3-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"asm","execution_mode":"compiled","jit_optimization_level":null,"model":"builtin_sm"},"n_final":8,"process":"d d~ > z g g g g g g g","process_key":"dd_z_jets","variant":"asm_o3","workload":"selected-flow"},{"cell_id":"z-builtin-sm-n8-dd-z-jets-cpp-o3-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"cpp","execution_mode":"compiled","jit_optimization_level":null,"model":"builtin_sm"},"n_final":8,"process":"d d~ > z g g g g g g g","process_key":"dd_z_jets","variant":"cpp_o3","workload":"all-flow"},{"cell_id":"z-builtin-sm-n8-dd-z-jets-cpp-o3-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"cpp","execution_mode":"compiled","jit_optimization_level":null,"model":"builtin_sm"},"n_final":8,"process":"d d~ > z g g g g g g g","process_key":"dd_z_jets","variant":"cpp_o3","workload":"selected-flow"},{"cell_id":"z-builtin-sm-n8-dd-z-jets-eager-jit-o2-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":8,"process":"d d~ > z g g g g g g g","process_key":"dd_z_jets","variant":"eager_jit_o2","workload":"all-flow"},{"cell_id":"z-builtin-sm-n8-dd-z-jets-eager-jit-o2-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":8,"process":"d d~ > z g g g g g g g","process_key":"dd_z_jets","variant":"eager_jit_o2","workload":"selected-flow"},{"cell_id":"z-builtin-sm-n8-dd-z-jets-jit-o1-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":1,"model":"builtin_sm"},"n_final":8,"process":"d d~ > z g g g g g g g","process_key":"dd_z_jets","variant":"jit_o1","workload":"all-flow"},{"cell_id":"z-builtin-sm-n8-dd-z-jets-jit-o1-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":1,"model":"builtin_sm"},"n_final":8,"process":"d d~ > z g g g g g g g","process_key":"dd_z_jets","variant":"jit_o1","workload":"selected-flow"},{"cell_id":"z-builtin-sm-n8-dd-z-jets-jit-o3-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":8,"process":"d d~ > z g g g g g g g","process_key":"dd_z_jets","variant":"jit_o3","workload":"all-flow"},{"cell_id":"z-builtin-sm-n8-dd-z-jets-jit-o3-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":8,"process":"d d~ > z g g g g g g g","process_key":"dd_z_jets","variant":"jit_o3","workload":"selected-flow"},{"cell_id":"z-builtin-sm-n8-dd-z-jets-on-the-fly-jit-o2-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":8,"process":"d d~ > z g g g g g g g","process_key":"dd_z_jets","variant":"on_the_fly_jit_o2","workload":"all-flow"},{"cell_id":"z-builtin-sm-n8-dd-z-jets-on-the-fly-jit-o2-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":8,"process":"d d~ > z g g g g g g g","process_key":"dd_z_jets","variant":"on_the_fly_jit_o2","workload":"selected-flow"},{"cell_id":"z-builtin-sm-n8-dd-z-jets-recurrence-jit-o2-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":8,"process":"d d~ > z g g g g g g g","process_key":"dd_z_jets","variant":"recurrence_jit_o2","workload":"all-flow"},{"cell_id":"z-builtin-sm-n8-dd-z-jets-recurrence-jit-o2-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":8,"process":"d d~ > z g g g g g g g","process_key":"dd_z_jets","variant":"recurrence_jit_o2","workload":"selected-flow"},{"cell_id":"z-builtin-sm-n9-dd-z-jets-asm-o3-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"asm","execution_mode":"compiled","jit_optimization_level":null,"model":"builtin_sm"},"n_final":9,"process":"d d~ > z g g g g g g g g","process_key":"dd_z_jets","variant":"asm_o3","workload":"all-flow"},{"cell_id":"z-builtin-sm-n9-dd-z-jets-asm-o3-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"asm","execution_mode":"compiled","jit_optimization_level":null,"model":"builtin_sm"},"n_final":9,"process":"d d~ > z g g g g g g g g","process_key":"dd_z_jets","variant":"asm_o3","workload":"selected-flow"},{"cell_id":"z-builtin-sm-n9-dd-z-jets-cpp-o3-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"cpp","execution_mode":"compiled","jit_optimization_level":null,"model":"builtin_sm"},"n_final":9,"process":"d d~ > z g g g g g g g g","process_key":"dd_z_jets","variant":"cpp_o3","workload":"all-flow"},{"cell_id":"z-builtin-sm-n9-dd-z-jets-cpp-o3-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"cpp","execution_mode":"compiled","jit_optimization_level":null,"model":"builtin_sm"},"n_final":9,"process":"d d~ > z g g g g g g g g","process_key":"dd_z_jets","variant":"cpp_o3","workload":"selected-flow"},{"cell_id":"z-builtin-sm-n9-dd-z-jets-eager-jit-o2-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":9,"process":"d d~ > z g g g g g g g g","process_key":"dd_z_jets","variant":"eager_jit_o2","workload":"all-flow"},{"cell_id":"z-builtin-sm-n9-dd-z-jets-eager-jit-o2-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},"n_final":9,"process":"d d~ > z g g g g g g g g","process_key":"dd_z_jets","variant":"eager_jit_o2","workload":"selected-flow"},{"cell_id":"z-builtin-sm-n9-dd-z-jets-jit-o1-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":1,"model":"builtin_sm"},"n_final":9,"process":"d d~ > z g g g g g g g g","process_key":"dd_z_jets","variant":"jit_o1","workload":"all-flow"},{"cell_id":"z-builtin-sm-n9-dd-z-jets-jit-o1-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":1,"model":"builtin_sm"},"n_final":9,"process":"d d~ > z g g g g g g g g","process_key":"dd_z_jets","variant":"jit_o1","workload":"selected-flow"},{"cell_id":"z-builtin-sm-n9-dd-z-jets-jit-o3-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":9,"process":"d d~ > z g g g g g g g g","process_key":"dd_z_jets","variant":"jit_o3","workload":"all-flow"},{"cell_id":"z-builtin-sm-n9-dd-z-jets-jit-o3-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},"n_final":9,"process":"d d~ > z g g g g g g g g","process_key":"dd_z_jets","variant":"jit_o3","workload":"selected-flow"},{"cell_id":"z-builtin-sm-n9-dd-z-jets-on-the-fly-jit-o2-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":9,"process":"d d~ > z g g g g g g g g","process_key":"dd_z_jets","variant":"on_the_fly_jit_o2","workload":"all-flow"},{"cell_id":"z-builtin-sm-n9-dd-z-jets-on-the-fly-jit-o2-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},"n_final":9,"process":"d d~ > z g g g g g g g g","process_key":"dd_z_jets","variant":"on_the_fly_jit_o2","workload":"selected-flow"},{"cell_id":"z-builtin-sm-n9-dd-z-jets-recurrence-jit-o2-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":9,"process":"d d~ > z g g g g g g g g","process_key":"dd_z_jets","variant":"recurrence_jit_o2","workload":"all-flow"},{"cell_id":"z-builtin-sm-n9-dd-z-jets-recurrence-jit-o2-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"},"n_final":9,"process":"d d~ > z g g g g g g g g","process_key":"dd_z_jets","variant":"recurrence_jit_o2","workload":"selected-flow"}],"kind":"performance_measurements","measurement_specs":[{"accuracy":"lc","backend":"asm","execution_mode":"compiled","jit_optimization_level":null,"model":"builtin_sm"},{"accuracy":"lc","backend":"cpp","execution_mode":"compiled","jit_optimization_level":null,"model":"builtin_sm"},{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":1,"model":"builtin_sm"},{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"builtin_sm"},{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"builtin_sm"},{"accuracy":"lc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"builtin_sm"},{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"builtin_sm"}],"report_version":"0.3.0","schema_version":5} diff --git a/pyamplicol/src/pyamplicol/_profiling_campaign/results/z_external_sm.json b/pyamplicol/src/pyamplicol/_profiling_campaign/results/z_external_sm.json new file mode 100644 index 0000000000..d55bbd3db6 --- /dev/null +++ b/pyamplicol/src/pyamplicol/_profiling_campaign/results/z_external_sm.json @@ -0,0 +1 @@ +{"$schema":"./report-cache.schema.json","dataset_id":"z_external_sm","entries":[{"cell_id":"z-external-sm-n1-dd-z-jets-asm-o3-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"asm","execution_mode":"compiled","jit_optimization_level":null,"model":"ufo_sm"},"n_final":1,"process":"d d~ > z","process_key":"dd_z_jets","variant":"asm_o3","workload":"all-flow"},{"cell_id":"z-external-sm-n1-dd-z-jets-asm-o3-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"asm","execution_mode":"compiled","jit_optimization_level":null,"model":"ufo_sm"},"n_final":1,"process":"d d~ > z","process_key":"dd_z_jets","variant":"asm_o3","workload":"selected-flow"},{"cell_id":"z-external-sm-n1-dd-z-jets-cpp-o3-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"cpp","execution_mode":"compiled","jit_optimization_level":null,"model":"ufo_sm"},"n_final":1,"process":"d d~ > z","process_key":"dd_z_jets","variant":"cpp_o3","workload":"all-flow"},{"cell_id":"z-external-sm-n1-dd-z-jets-cpp-o3-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"cpp","execution_mode":"compiled","jit_optimization_level":null,"model":"ufo_sm"},"n_final":1,"process":"d d~ > z","process_key":"dd_z_jets","variant":"cpp_o3","workload":"selected-flow"},{"cell_id":"z-external-sm-n1-dd-z-jets-eager-jit-o2-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"ufo_sm"},"n_final":1,"process":"d d~ > z","process_key":"dd_z_jets","variant":"eager_jit_o2","workload":"all-flow"},{"cell_id":"z-external-sm-n1-dd-z-jets-eager-jit-o2-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"ufo_sm"},"n_final":1,"process":"d d~ > z","process_key":"dd_z_jets","variant":"eager_jit_o2","workload":"selected-flow"},{"cell_id":"z-external-sm-n1-dd-z-jets-jit-o1-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":1,"model":"ufo_sm"},"n_final":1,"process":"d d~ > z","process_key":"dd_z_jets","variant":"jit_o1","workload":"all-flow"},{"cell_id":"z-external-sm-n1-dd-z-jets-jit-o1-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":1,"model":"ufo_sm"},"n_final":1,"process":"d d~ > z","process_key":"dd_z_jets","variant":"jit_o1","workload":"selected-flow"},{"cell_id":"z-external-sm-n1-dd-z-jets-jit-o3-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"ufo_sm"},"n_final":1,"process":"d d~ > z","process_key":"dd_z_jets","variant":"jit_o3","workload":"all-flow"},{"cell_id":"z-external-sm-n1-dd-z-jets-jit-o3-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"ufo_sm"},"n_final":1,"process":"d d~ > z","process_key":"dd_z_jets","variant":"jit_o3","workload":"selected-flow"},{"cell_id":"z-external-sm-n1-dd-z-jets-on-the-fly-jit-o2-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"ufo_sm"},"n_final":1,"process":"d d~ > z","process_key":"dd_z_jets","variant":"on_the_fly_jit_o2","workload":"all-flow"},{"cell_id":"z-external-sm-n1-dd-z-jets-on-the-fly-jit-o2-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"ufo_sm"},"n_final":1,"process":"d d~ > z","process_key":"dd_z_jets","variant":"on_the_fly_jit_o2","workload":"selected-flow"},{"cell_id":"z-external-sm-n1-dd-z-jets-recurrence-jit-o2-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":1,"process":"d d~ > z","process_key":"dd_z_jets","variant":"recurrence_jit_o2","workload":"all-flow"},{"cell_id":"z-external-sm-n1-dd-z-jets-recurrence-jit-o2-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":1,"process":"d d~ > z","process_key":"dd_z_jets","variant":"recurrence_jit_o2","workload":"selected-flow"},{"cell_id":"z-external-sm-n2-dd-z-jets-asm-o3-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"asm","execution_mode":"compiled","jit_optimization_level":null,"model":"ufo_sm"},"n_final":2,"process":"d d~ > z g","process_key":"dd_z_jets","variant":"asm_o3","workload":"all-flow"},{"cell_id":"z-external-sm-n2-dd-z-jets-asm-o3-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"asm","execution_mode":"compiled","jit_optimization_level":null,"model":"ufo_sm"},"n_final":2,"process":"d d~ > z g","process_key":"dd_z_jets","variant":"asm_o3","workload":"selected-flow"},{"cell_id":"z-external-sm-n2-dd-z-jets-cpp-o3-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"cpp","execution_mode":"compiled","jit_optimization_level":null,"model":"ufo_sm"},"n_final":2,"process":"d d~ > z g","process_key":"dd_z_jets","variant":"cpp_o3","workload":"all-flow"},{"cell_id":"z-external-sm-n2-dd-z-jets-cpp-o3-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"cpp","execution_mode":"compiled","jit_optimization_level":null,"model":"ufo_sm"},"n_final":2,"process":"d d~ > z g","process_key":"dd_z_jets","variant":"cpp_o3","workload":"selected-flow"},{"cell_id":"z-external-sm-n2-dd-z-jets-eager-jit-o2-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"ufo_sm"},"n_final":2,"process":"d d~ > z g","process_key":"dd_z_jets","variant":"eager_jit_o2","workload":"all-flow"},{"cell_id":"z-external-sm-n2-dd-z-jets-eager-jit-o2-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"ufo_sm"},"n_final":2,"process":"d d~ > z g","process_key":"dd_z_jets","variant":"eager_jit_o2","workload":"selected-flow"},{"cell_id":"z-external-sm-n2-dd-z-jets-jit-o1-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":1,"model":"ufo_sm"},"n_final":2,"process":"d d~ > z g","process_key":"dd_z_jets","variant":"jit_o1","workload":"all-flow"},{"cell_id":"z-external-sm-n2-dd-z-jets-jit-o1-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":1,"model":"ufo_sm"},"n_final":2,"process":"d d~ > z g","process_key":"dd_z_jets","variant":"jit_o1","workload":"selected-flow"},{"cell_id":"z-external-sm-n2-dd-z-jets-jit-o3-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"ufo_sm"},"n_final":2,"process":"d d~ > z g","process_key":"dd_z_jets","variant":"jit_o3","workload":"all-flow"},{"cell_id":"z-external-sm-n2-dd-z-jets-jit-o3-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"ufo_sm"},"n_final":2,"process":"d d~ > z g","process_key":"dd_z_jets","variant":"jit_o3","workload":"selected-flow"},{"cell_id":"z-external-sm-n2-dd-z-jets-on-the-fly-jit-o2-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"ufo_sm"},"n_final":2,"process":"d d~ > z g","process_key":"dd_z_jets","variant":"on_the_fly_jit_o2","workload":"all-flow"},{"cell_id":"z-external-sm-n2-dd-z-jets-on-the-fly-jit-o2-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"ufo_sm"},"n_final":2,"process":"d d~ > z g","process_key":"dd_z_jets","variant":"on_the_fly_jit_o2","workload":"selected-flow"},{"cell_id":"z-external-sm-n2-dd-z-jets-recurrence-jit-o2-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":2,"process":"d d~ > z g","process_key":"dd_z_jets","variant":"recurrence_jit_o2","workload":"all-flow"},{"cell_id":"z-external-sm-n2-dd-z-jets-recurrence-jit-o2-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":2,"process":"d d~ > z g","process_key":"dd_z_jets","variant":"recurrence_jit_o2","workload":"selected-flow"},{"cell_id":"z-external-sm-n3-dd-z-jets-asm-o3-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"asm","execution_mode":"compiled","jit_optimization_level":null,"model":"ufo_sm"},"n_final":3,"process":"d d~ > z g g","process_key":"dd_z_jets","variant":"asm_o3","workload":"all-flow"},{"cell_id":"z-external-sm-n3-dd-z-jets-asm-o3-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"asm","execution_mode":"compiled","jit_optimization_level":null,"model":"ufo_sm"},"n_final":3,"process":"d d~ > z g g","process_key":"dd_z_jets","variant":"asm_o3","workload":"selected-flow"},{"cell_id":"z-external-sm-n3-dd-z-jets-cpp-o3-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"cpp","execution_mode":"compiled","jit_optimization_level":null,"model":"ufo_sm"},"n_final":3,"process":"d d~ > z g g","process_key":"dd_z_jets","variant":"cpp_o3","workload":"all-flow"},{"cell_id":"z-external-sm-n3-dd-z-jets-cpp-o3-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"cpp","execution_mode":"compiled","jit_optimization_level":null,"model":"ufo_sm"},"n_final":3,"process":"d d~ > z g g","process_key":"dd_z_jets","variant":"cpp_o3","workload":"selected-flow"},{"cell_id":"z-external-sm-n3-dd-z-jets-eager-jit-o2-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"ufo_sm"},"n_final":3,"process":"d d~ > z g g","process_key":"dd_z_jets","variant":"eager_jit_o2","workload":"all-flow"},{"cell_id":"z-external-sm-n3-dd-z-jets-eager-jit-o2-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"ufo_sm"},"n_final":3,"process":"d d~ > z g g","process_key":"dd_z_jets","variant":"eager_jit_o2","workload":"selected-flow"},{"cell_id":"z-external-sm-n3-dd-z-jets-jit-o1-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":1,"model":"ufo_sm"},"n_final":3,"process":"d d~ > z g g","process_key":"dd_z_jets","variant":"jit_o1","workload":"all-flow"},{"cell_id":"z-external-sm-n3-dd-z-jets-jit-o1-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":1,"model":"ufo_sm"},"n_final":3,"process":"d d~ > z g g","process_key":"dd_z_jets","variant":"jit_o1","workload":"selected-flow"},{"cell_id":"z-external-sm-n3-dd-z-jets-jit-o3-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"ufo_sm"},"n_final":3,"process":"d d~ > z g g","process_key":"dd_z_jets","variant":"jit_o3","workload":"all-flow"},{"cell_id":"z-external-sm-n3-dd-z-jets-jit-o3-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"ufo_sm"},"n_final":3,"process":"d d~ > z g g","process_key":"dd_z_jets","variant":"jit_o3","workload":"selected-flow"},{"cell_id":"z-external-sm-n3-dd-z-jets-on-the-fly-jit-o2-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"ufo_sm"},"n_final":3,"process":"d d~ > z g g","process_key":"dd_z_jets","variant":"on_the_fly_jit_o2","workload":"all-flow"},{"cell_id":"z-external-sm-n3-dd-z-jets-on-the-fly-jit-o2-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"ufo_sm"},"n_final":3,"process":"d d~ > z g g","process_key":"dd_z_jets","variant":"on_the_fly_jit_o2","workload":"selected-flow"},{"cell_id":"z-external-sm-n3-dd-z-jets-recurrence-jit-o2-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":3,"process":"d d~ > z g g","process_key":"dd_z_jets","variant":"recurrence_jit_o2","workload":"all-flow"},{"cell_id":"z-external-sm-n3-dd-z-jets-recurrence-jit-o2-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":3,"process":"d d~ > z g g","process_key":"dd_z_jets","variant":"recurrence_jit_o2","workload":"selected-flow"},{"cell_id":"z-external-sm-n4-dd-z-jets-asm-o3-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"asm","execution_mode":"compiled","jit_optimization_level":null,"model":"ufo_sm"},"n_final":4,"process":"d d~ > z g g g","process_key":"dd_z_jets","variant":"asm_o3","workload":"all-flow"},{"cell_id":"z-external-sm-n4-dd-z-jets-asm-o3-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"asm","execution_mode":"compiled","jit_optimization_level":null,"model":"ufo_sm"},"n_final":4,"process":"d d~ > z g g g","process_key":"dd_z_jets","variant":"asm_o3","workload":"selected-flow"},{"cell_id":"z-external-sm-n4-dd-z-jets-cpp-o3-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"cpp","execution_mode":"compiled","jit_optimization_level":null,"model":"ufo_sm"},"n_final":4,"process":"d d~ > z g g g","process_key":"dd_z_jets","variant":"cpp_o3","workload":"all-flow"},{"cell_id":"z-external-sm-n4-dd-z-jets-cpp-o3-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"cpp","execution_mode":"compiled","jit_optimization_level":null,"model":"ufo_sm"},"n_final":4,"process":"d d~ > z g g g","process_key":"dd_z_jets","variant":"cpp_o3","workload":"selected-flow"},{"cell_id":"z-external-sm-n4-dd-z-jets-eager-jit-o2-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"ufo_sm"},"n_final":4,"process":"d d~ > z g g g","process_key":"dd_z_jets","variant":"eager_jit_o2","workload":"all-flow"},{"cell_id":"z-external-sm-n4-dd-z-jets-eager-jit-o2-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"ufo_sm"},"n_final":4,"process":"d d~ > z g g g","process_key":"dd_z_jets","variant":"eager_jit_o2","workload":"selected-flow"},{"cell_id":"z-external-sm-n4-dd-z-jets-jit-o1-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":1,"model":"ufo_sm"},"n_final":4,"process":"d d~ > z g g g","process_key":"dd_z_jets","variant":"jit_o1","workload":"all-flow"},{"cell_id":"z-external-sm-n4-dd-z-jets-jit-o1-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":1,"model":"ufo_sm"},"n_final":4,"process":"d d~ > z g g g","process_key":"dd_z_jets","variant":"jit_o1","workload":"selected-flow"},{"cell_id":"z-external-sm-n4-dd-z-jets-jit-o3-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"ufo_sm"},"n_final":4,"process":"d d~ > z g g g","process_key":"dd_z_jets","variant":"jit_o3","workload":"all-flow"},{"cell_id":"z-external-sm-n4-dd-z-jets-jit-o3-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"ufo_sm"},"n_final":4,"process":"d d~ > z g g g","process_key":"dd_z_jets","variant":"jit_o3","workload":"selected-flow"},{"cell_id":"z-external-sm-n4-dd-z-jets-on-the-fly-jit-o2-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"ufo_sm"},"n_final":4,"process":"d d~ > z g g g","process_key":"dd_z_jets","variant":"on_the_fly_jit_o2","workload":"all-flow"},{"cell_id":"z-external-sm-n4-dd-z-jets-on-the-fly-jit-o2-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"ufo_sm"},"n_final":4,"process":"d d~ > z g g g","process_key":"dd_z_jets","variant":"on_the_fly_jit_o2","workload":"selected-flow"},{"cell_id":"z-external-sm-n4-dd-z-jets-recurrence-jit-o2-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":4,"process":"d d~ > z g g g","process_key":"dd_z_jets","variant":"recurrence_jit_o2","workload":"all-flow"},{"cell_id":"z-external-sm-n4-dd-z-jets-recurrence-jit-o2-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":4,"process":"d d~ > z g g g","process_key":"dd_z_jets","variant":"recurrence_jit_o2","workload":"selected-flow"},{"cell_id":"z-external-sm-n5-dd-z-jets-asm-o3-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"asm","execution_mode":"compiled","jit_optimization_level":null,"model":"ufo_sm"},"n_final":5,"process":"d d~ > z g g g g","process_key":"dd_z_jets","variant":"asm_o3","workload":"all-flow"},{"cell_id":"z-external-sm-n5-dd-z-jets-asm-o3-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"asm","execution_mode":"compiled","jit_optimization_level":null,"model":"ufo_sm"},"n_final":5,"process":"d d~ > z g g g g","process_key":"dd_z_jets","variant":"asm_o3","workload":"selected-flow"},{"cell_id":"z-external-sm-n5-dd-z-jets-cpp-o3-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"cpp","execution_mode":"compiled","jit_optimization_level":null,"model":"ufo_sm"},"n_final":5,"process":"d d~ > z g g g g","process_key":"dd_z_jets","variant":"cpp_o3","workload":"all-flow"},{"cell_id":"z-external-sm-n5-dd-z-jets-cpp-o3-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"cpp","execution_mode":"compiled","jit_optimization_level":null,"model":"ufo_sm"},"n_final":5,"process":"d d~ > z g g g g","process_key":"dd_z_jets","variant":"cpp_o3","workload":"selected-flow"},{"cell_id":"z-external-sm-n5-dd-z-jets-eager-jit-o2-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"ufo_sm"},"n_final":5,"process":"d d~ > z g g g g","process_key":"dd_z_jets","variant":"eager_jit_o2","workload":"all-flow"},{"cell_id":"z-external-sm-n5-dd-z-jets-eager-jit-o2-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"ufo_sm"},"n_final":5,"process":"d d~ > z g g g g","process_key":"dd_z_jets","variant":"eager_jit_o2","workload":"selected-flow"},{"cell_id":"z-external-sm-n5-dd-z-jets-jit-o1-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":1,"model":"ufo_sm"},"n_final":5,"process":"d d~ > z g g g g","process_key":"dd_z_jets","variant":"jit_o1","workload":"all-flow"},{"cell_id":"z-external-sm-n5-dd-z-jets-jit-o1-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":1,"model":"ufo_sm"},"n_final":5,"process":"d d~ > z g g g g","process_key":"dd_z_jets","variant":"jit_o1","workload":"selected-flow"},{"cell_id":"z-external-sm-n5-dd-z-jets-jit-o3-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"ufo_sm"},"n_final":5,"process":"d d~ > z g g g g","process_key":"dd_z_jets","variant":"jit_o3","workload":"all-flow"},{"cell_id":"z-external-sm-n5-dd-z-jets-jit-o3-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"ufo_sm"},"n_final":5,"process":"d d~ > z g g g g","process_key":"dd_z_jets","variant":"jit_o3","workload":"selected-flow"},{"cell_id":"z-external-sm-n5-dd-z-jets-on-the-fly-jit-o2-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"ufo_sm"},"n_final":5,"process":"d d~ > z g g g g","process_key":"dd_z_jets","variant":"on_the_fly_jit_o2","workload":"all-flow"},{"cell_id":"z-external-sm-n5-dd-z-jets-on-the-fly-jit-o2-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"ufo_sm"},"n_final":5,"process":"d d~ > z g g g g","process_key":"dd_z_jets","variant":"on_the_fly_jit_o2","workload":"selected-flow"},{"cell_id":"z-external-sm-n5-dd-z-jets-recurrence-jit-o2-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":5,"process":"d d~ > z g g g g","process_key":"dd_z_jets","variant":"recurrence_jit_o2","workload":"all-flow"},{"cell_id":"z-external-sm-n5-dd-z-jets-recurrence-jit-o2-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":5,"process":"d d~ > z g g g g","process_key":"dd_z_jets","variant":"recurrence_jit_o2","workload":"selected-flow"},{"cell_id":"z-external-sm-n6-dd-z-jets-asm-o3-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"asm","execution_mode":"compiled","jit_optimization_level":null,"model":"ufo_sm"},"n_final":6,"process":"d d~ > z g g g g g","process_key":"dd_z_jets","variant":"asm_o3","workload":"all-flow"},{"cell_id":"z-external-sm-n6-dd-z-jets-asm-o3-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"asm","execution_mode":"compiled","jit_optimization_level":null,"model":"ufo_sm"},"n_final":6,"process":"d d~ > z g g g g g","process_key":"dd_z_jets","variant":"asm_o3","workload":"selected-flow"},{"cell_id":"z-external-sm-n6-dd-z-jets-cpp-o3-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"cpp","execution_mode":"compiled","jit_optimization_level":null,"model":"ufo_sm"},"n_final":6,"process":"d d~ > z g g g g g","process_key":"dd_z_jets","variant":"cpp_o3","workload":"all-flow"},{"cell_id":"z-external-sm-n6-dd-z-jets-cpp-o3-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"cpp","execution_mode":"compiled","jit_optimization_level":null,"model":"ufo_sm"},"n_final":6,"process":"d d~ > z g g g g g","process_key":"dd_z_jets","variant":"cpp_o3","workload":"selected-flow"},{"cell_id":"z-external-sm-n6-dd-z-jets-eager-jit-o2-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"ufo_sm"},"n_final":6,"process":"d d~ > z g g g g g","process_key":"dd_z_jets","variant":"eager_jit_o2","workload":"all-flow"},{"cell_id":"z-external-sm-n6-dd-z-jets-eager-jit-o2-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"ufo_sm"},"n_final":6,"process":"d d~ > z g g g g g","process_key":"dd_z_jets","variant":"eager_jit_o2","workload":"selected-flow"},{"cell_id":"z-external-sm-n6-dd-z-jets-jit-o1-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":1,"model":"ufo_sm"},"n_final":6,"process":"d d~ > z g g g g g","process_key":"dd_z_jets","variant":"jit_o1","workload":"all-flow"},{"cell_id":"z-external-sm-n6-dd-z-jets-jit-o1-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":1,"model":"ufo_sm"},"n_final":6,"process":"d d~ > z g g g g g","process_key":"dd_z_jets","variant":"jit_o1","workload":"selected-flow"},{"cell_id":"z-external-sm-n6-dd-z-jets-jit-o3-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"ufo_sm"},"n_final":6,"process":"d d~ > z g g g g g","process_key":"dd_z_jets","variant":"jit_o3","workload":"all-flow"},{"cell_id":"z-external-sm-n6-dd-z-jets-jit-o3-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"ufo_sm"},"n_final":6,"process":"d d~ > z g g g g g","process_key":"dd_z_jets","variant":"jit_o3","workload":"selected-flow"},{"cell_id":"z-external-sm-n6-dd-z-jets-on-the-fly-jit-o2-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"ufo_sm"},"n_final":6,"process":"d d~ > z g g g g g","process_key":"dd_z_jets","variant":"on_the_fly_jit_o2","workload":"all-flow"},{"cell_id":"z-external-sm-n6-dd-z-jets-on-the-fly-jit-o2-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"ufo_sm"},"n_final":6,"process":"d d~ > z g g g g g","process_key":"dd_z_jets","variant":"on_the_fly_jit_o2","workload":"selected-flow"},{"cell_id":"z-external-sm-n6-dd-z-jets-recurrence-jit-o2-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":6,"process":"d d~ > z g g g g g","process_key":"dd_z_jets","variant":"recurrence_jit_o2","workload":"all-flow"},{"cell_id":"z-external-sm-n6-dd-z-jets-recurrence-jit-o2-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":6,"process":"d d~ > z g g g g g","process_key":"dd_z_jets","variant":"recurrence_jit_o2","workload":"selected-flow"},{"cell_id":"z-external-sm-n7-dd-z-jets-asm-o3-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"asm","execution_mode":"compiled","jit_optimization_level":null,"model":"ufo_sm"},"n_final":7,"process":"d d~ > z g g g g g g","process_key":"dd_z_jets","variant":"asm_o3","workload":"all-flow"},{"cell_id":"z-external-sm-n7-dd-z-jets-asm-o3-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"asm","execution_mode":"compiled","jit_optimization_level":null,"model":"ufo_sm"},"n_final":7,"process":"d d~ > z g g g g g g","process_key":"dd_z_jets","variant":"asm_o3","workload":"selected-flow"},{"cell_id":"z-external-sm-n7-dd-z-jets-cpp-o3-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"cpp","execution_mode":"compiled","jit_optimization_level":null,"model":"ufo_sm"},"n_final":7,"process":"d d~ > z g g g g g g","process_key":"dd_z_jets","variant":"cpp_o3","workload":"all-flow"},{"cell_id":"z-external-sm-n7-dd-z-jets-cpp-o3-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"cpp","execution_mode":"compiled","jit_optimization_level":null,"model":"ufo_sm"},"n_final":7,"process":"d d~ > z g g g g g g","process_key":"dd_z_jets","variant":"cpp_o3","workload":"selected-flow"},{"cell_id":"z-external-sm-n7-dd-z-jets-eager-jit-o2-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"ufo_sm"},"n_final":7,"process":"d d~ > z g g g g g g","process_key":"dd_z_jets","variant":"eager_jit_o2","workload":"all-flow"},{"cell_id":"z-external-sm-n7-dd-z-jets-eager-jit-o2-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"ufo_sm"},"n_final":7,"process":"d d~ > z g g g g g g","process_key":"dd_z_jets","variant":"eager_jit_o2","workload":"selected-flow"},{"cell_id":"z-external-sm-n7-dd-z-jets-jit-o1-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":1,"model":"ufo_sm"},"n_final":7,"process":"d d~ > z g g g g g g","process_key":"dd_z_jets","variant":"jit_o1","workload":"all-flow"},{"cell_id":"z-external-sm-n7-dd-z-jets-jit-o1-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":1,"model":"ufo_sm"},"n_final":7,"process":"d d~ > z g g g g g g","process_key":"dd_z_jets","variant":"jit_o1","workload":"selected-flow"},{"cell_id":"z-external-sm-n7-dd-z-jets-jit-o3-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"ufo_sm"},"n_final":7,"process":"d d~ > z g g g g g g","process_key":"dd_z_jets","variant":"jit_o3","workload":"all-flow"},{"cell_id":"z-external-sm-n7-dd-z-jets-jit-o3-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"ufo_sm"},"n_final":7,"process":"d d~ > z g g g g g g","process_key":"dd_z_jets","variant":"jit_o3","workload":"selected-flow"},{"cell_id":"z-external-sm-n7-dd-z-jets-on-the-fly-jit-o2-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"ufo_sm"},"n_final":7,"process":"d d~ > z g g g g g g","process_key":"dd_z_jets","variant":"on_the_fly_jit_o2","workload":"all-flow"},{"cell_id":"z-external-sm-n7-dd-z-jets-on-the-fly-jit-o2-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"ufo_sm"},"n_final":7,"process":"d d~ > z g g g g g g","process_key":"dd_z_jets","variant":"on_the_fly_jit_o2","workload":"selected-flow"},{"cell_id":"z-external-sm-n7-dd-z-jets-recurrence-jit-o2-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":7,"process":"d d~ > z g g g g g g","process_key":"dd_z_jets","variant":"recurrence_jit_o2","workload":"all-flow"},{"cell_id":"z-external-sm-n7-dd-z-jets-recurrence-jit-o2-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":7,"process":"d d~ > z g g g g g g","process_key":"dd_z_jets","variant":"recurrence_jit_o2","workload":"selected-flow"},{"cell_id":"z-external-sm-n8-dd-z-jets-asm-o3-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"asm","execution_mode":"compiled","jit_optimization_level":null,"model":"ufo_sm"},"n_final":8,"process":"d d~ > z g g g g g g g","process_key":"dd_z_jets","variant":"asm_o3","workload":"all-flow"},{"cell_id":"z-external-sm-n8-dd-z-jets-asm-o3-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"asm","execution_mode":"compiled","jit_optimization_level":null,"model":"ufo_sm"},"n_final":8,"process":"d d~ > z g g g g g g g","process_key":"dd_z_jets","variant":"asm_o3","workload":"selected-flow"},{"cell_id":"z-external-sm-n8-dd-z-jets-cpp-o3-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"cpp","execution_mode":"compiled","jit_optimization_level":null,"model":"ufo_sm"},"n_final":8,"process":"d d~ > z g g g g g g g","process_key":"dd_z_jets","variant":"cpp_o3","workload":"all-flow"},{"cell_id":"z-external-sm-n8-dd-z-jets-cpp-o3-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"cpp","execution_mode":"compiled","jit_optimization_level":null,"model":"ufo_sm"},"n_final":8,"process":"d d~ > z g g g g g g g","process_key":"dd_z_jets","variant":"cpp_o3","workload":"selected-flow"},{"cell_id":"z-external-sm-n8-dd-z-jets-eager-jit-o2-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"ufo_sm"},"n_final":8,"process":"d d~ > z g g g g g g g","process_key":"dd_z_jets","variant":"eager_jit_o2","workload":"all-flow"},{"cell_id":"z-external-sm-n8-dd-z-jets-eager-jit-o2-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"ufo_sm"},"n_final":8,"process":"d d~ > z g g g g g g g","process_key":"dd_z_jets","variant":"eager_jit_o2","workload":"selected-flow"},{"cell_id":"z-external-sm-n8-dd-z-jets-jit-o1-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":1,"model":"ufo_sm"},"n_final":8,"process":"d d~ > z g g g g g g g","process_key":"dd_z_jets","variant":"jit_o1","workload":"all-flow"},{"cell_id":"z-external-sm-n8-dd-z-jets-jit-o1-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":1,"model":"ufo_sm"},"n_final":8,"process":"d d~ > z g g g g g g g","process_key":"dd_z_jets","variant":"jit_o1","workload":"selected-flow"},{"cell_id":"z-external-sm-n8-dd-z-jets-jit-o3-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"ufo_sm"},"n_final":8,"process":"d d~ > z g g g g g g g","process_key":"dd_z_jets","variant":"jit_o3","workload":"all-flow"},{"cell_id":"z-external-sm-n8-dd-z-jets-jit-o3-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"ufo_sm"},"n_final":8,"process":"d d~ > z g g g g g g g","process_key":"dd_z_jets","variant":"jit_o3","workload":"selected-flow"},{"cell_id":"z-external-sm-n8-dd-z-jets-on-the-fly-jit-o2-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"ufo_sm"},"n_final":8,"process":"d d~ > z g g g g g g g","process_key":"dd_z_jets","variant":"on_the_fly_jit_o2","workload":"all-flow"},{"cell_id":"z-external-sm-n8-dd-z-jets-on-the-fly-jit-o2-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"ufo_sm"},"n_final":8,"process":"d d~ > z g g g g g g g","process_key":"dd_z_jets","variant":"on_the_fly_jit_o2","workload":"selected-flow"},{"cell_id":"z-external-sm-n8-dd-z-jets-recurrence-jit-o2-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":8,"process":"d d~ > z g g g g g g g","process_key":"dd_z_jets","variant":"recurrence_jit_o2","workload":"all-flow"},{"cell_id":"z-external-sm-n8-dd-z-jets-recurrence-jit-o2-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":8,"process":"d d~ > z g g g g g g g","process_key":"dd_z_jets","variant":"recurrence_jit_o2","workload":"selected-flow"},{"cell_id":"z-external-sm-n9-dd-z-jets-asm-o3-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"asm","execution_mode":"compiled","jit_optimization_level":null,"model":"ufo_sm"},"n_final":9,"process":"d d~ > z g g g g g g g g","process_key":"dd_z_jets","variant":"asm_o3","workload":"all-flow"},{"cell_id":"z-external-sm-n9-dd-z-jets-asm-o3-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"asm","execution_mode":"compiled","jit_optimization_level":null,"model":"ufo_sm"},"n_final":9,"process":"d d~ > z g g g g g g g g","process_key":"dd_z_jets","variant":"asm_o3","workload":"selected-flow"},{"cell_id":"z-external-sm-n9-dd-z-jets-cpp-o3-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"cpp","execution_mode":"compiled","jit_optimization_level":null,"model":"ufo_sm"},"n_final":9,"process":"d d~ > z g g g g g g g g","process_key":"dd_z_jets","variant":"cpp_o3","workload":"all-flow"},{"cell_id":"z-external-sm-n9-dd-z-jets-cpp-o3-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"cpp","execution_mode":"compiled","jit_optimization_level":null,"model":"ufo_sm"},"n_final":9,"process":"d d~ > z g g g g g g g g","process_key":"dd_z_jets","variant":"cpp_o3","workload":"selected-flow"},{"cell_id":"z-external-sm-n9-dd-z-jets-eager-jit-o2-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"ufo_sm"},"n_final":9,"process":"d d~ > z g g g g g g g g","process_key":"dd_z_jets","variant":"eager_jit_o2","workload":"all-flow"},{"cell_id":"z-external-sm-n9-dd-z-jets-eager-jit-o2-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"ufo_sm"},"n_final":9,"process":"d d~ > z g g g g g g g g","process_key":"dd_z_jets","variant":"eager_jit_o2","workload":"selected-flow"},{"cell_id":"z-external-sm-n9-dd-z-jets-jit-o1-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":1,"model":"ufo_sm"},"n_final":9,"process":"d d~ > z g g g g g g g g","process_key":"dd_z_jets","variant":"jit_o1","workload":"all-flow"},{"cell_id":"z-external-sm-n9-dd-z-jets-jit-o1-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":1,"model":"ufo_sm"},"n_final":9,"process":"d d~ > z g g g g g g g g","process_key":"dd_z_jets","variant":"jit_o1","workload":"selected-flow"},{"cell_id":"z-external-sm-n9-dd-z-jets-jit-o3-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"ufo_sm"},"n_final":9,"process":"d d~ > z g g g g g g g g","process_key":"dd_z_jets","variant":"jit_o3","workload":"all-flow"},{"cell_id":"z-external-sm-n9-dd-z-jets-jit-o3-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"ufo_sm"},"n_final":9,"process":"d d~ > z g g g g g g g g","process_key":"dd_z_jets","variant":"jit_o3","workload":"selected-flow"},{"cell_id":"z-external-sm-n9-dd-z-jets-on-the-fly-jit-o2-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"ufo_sm"},"n_final":9,"process":"d d~ > z g g g g g g g g","process_key":"dd_z_jets","variant":"on_the_fly_jit_o2","workload":"all-flow"},{"cell_id":"z-external-sm-n9-dd-z-jets-on-the-fly-jit-o2-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"ufo_sm"},"n_final":9,"process":"d d~ > z g g g g g g g g","process_key":"dd_z_jets","variant":"on_the_fly_jit_o2","workload":"selected-flow"},{"cell_id":"z-external-sm-n9-dd-z-jets-recurrence-jit-o2-all-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":9,"process":"d d~ > z g g g g g g g g","process_key":"dd_z_jets","variant":"recurrence_jit_o2","workload":"all-flow"},{"cell_id":"z-external-sm-n9-dd-z-jets-recurrence-jit-o2-selected-flow","measurement":{"artifact":null,"execution_seconds_per_point":null,"failure":null,"generation_seconds":null,"matrix_element":null,"provenance":null,"relative_standard_error":null,"resources":null,"sample_count":null,"selector_contract":null,"standard_error_seconds_per_point":null,"status":"not_available","validation":null,"wall_seconds_per_point":null},"measurement_spec":{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"},"n_final":9,"process":"d d~ > z g g g g g g g g","process_key":"dd_z_jets","variant":"recurrence_jit_o2","workload":"selected-flow"}],"kind":"performance_measurements","measurement_specs":[{"accuracy":"lc","backend":"asm","execution_mode":"compiled","jit_optimization_level":null,"model":"ufo_sm"},{"accuracy":"lc","backend":"cpp","execution_mode":"compiled","jit_optimization_level":null,"model":"ufo_sm"},{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":1,"model":"ufo_sm"},{"accuracy":"lc","backend":"jit","execution_mode":"compiled","jit_optimization_level":3,"model":"ufo_sm"},{"accuracy":"lc","backend":"jit","execution_mode":"eager","jit_optimization_level":2,"model":"ufo_sm"},{"accuracy":"lc","backend":"jit","execution_mode":"on-the-fly","jit_optimization_level":2,"model":"ufo_sm"},{"accuracy":"lc","backend":"jit","execution_mode":"recurrence","jit_optimization_level":2,"model":"ufo_sm"}],"report_version":"0.3.0","schema_version":5} diff --git a/pyamplicol/src/pyamplicol/_profiling_campaign/section_eager_execution.tex b/pyamplicol/src/pyamplicol/_profiling_campaign/section_eager_execution.tex new file mode 100644 index 0000000000..cb9ca80b1e --- /dev/null +++ b/pyamplicol/src/pyamplicol/_profiling_campaign/section_eager_execution.tex @@ -0,0 +1,97 @@ +% SPDX-License-Identifier: 0BSD +\section{Compiled, Eager, and Recurrence Execution} +\label{sec:eager-execution} + +The three execution modes differ in how they turn the same current recursion +into numerical work. Compiled mode specializes groups of current relations to +one process. Eager mode applies reusable model kernels according to a +process-specific schedule. Recurrence mode constructs a compact schedule of +currents, contributions, and closures directly. These choices change the +balance between process-generation cost and runtime work, but not the +observable being evaluated. + +Recurrence JIT O2 is the default execution mode. + +\begin{center} +\small +\begin{tabular}{@{}L{0.72in}L{1.85in}L{2.00in}L{1.40in}@{}} +\toprule +\textbf{mode} & \textbf{process preparation} & \textbf{evaluation} & + \textbf{characteristic balance} \\ +\midrule +Compiled + & Builds process-specific stage evaluators + & Evaluates a small number of process-wide, multi-output stages + & More preparation; greater fusion across a process \\ +Eager + & Maps the process graph to reusable prepared-model kernels + & Applies those kernels according to the process dependencies + & Reuses model work across processes \\ +Recurrence + & Builds a compact schedule directly from the current recursion + & Evaluates scheduled currents, contributions, and closures + & Avoids construction of a generic process DAG \\ +\bottomrule +\end{tabular} +\end{center} + +\subsection{Common physics contract} + +All three modes retain the same external states, perturbative orders, +helicities, colour components, coherent amplitude groups, symmetry factors, +and initial-state averages. In a contracted colour basis their common +observable has the schematic form +\begin{equation} + \mathcal M_p + =\mathcal N\sum_h\sum_{a,b} + A_{ha}(p)^{*}\,C_{ab}\,A_{hb}(p), + \label{eq:three-mode-common-observable} +\end{equation} +with the corresponding resolved form retained when helicity or leading-colour +flow components are requested. Each mode must reproduce both the directly +evaluated total and the sum of those resolved components. + +Current contributions are accumulated coherently before a propagator or other +current transformation is applied. Final closures then form the amplitude +components entering Eq.~\eqref{eq:three-mode-common-observable}. This ordering +is common to the three modes and prevents execution strategy from changing the +normalization. + +\subsection{Prepared-model boundary} + +A prepared model contains reusable numerical information derived from the +model's particles, parameters, propagators, and interaction tensors. Eager and +recurrence JIT O2 measurements reuse this model-level preparation and add only +the process-dependent schedule. Compiled JIT O3 measurements instead include +construction and compilation of their process-specific evaluators. + +Prepared-model creation occurs outside the process-generation timer. The +reported eager and recurrence generation times therefore describe adding a +process to an already prepared model, not importing and compiling the model +itself. + +\subsection{Batches, selectors, and portability} + +Evaluation is batched over phase-space points, allowing the JIT backend to use +the point dimension for vectorization. Partitioning a large input batch changes +neither the process representation nor the numerical result. + +Helicity and colour-flow selectors refer to physical resolved components. A +complete leading-colour calculation retains every declared helicity and flow, +so selecting one flow with summed helicities or one helicity with all flows +does not alter the underlying physics coverage. The selected-flow and all-flow +layouts used for these workloads retain that same physical coverage. + +Portable JIT state is compiled for the receiving processor when loaded. +Target-native ASM and C++ payloads remain architecture-specific, and the exact +symbolic representation is retained where higher-precision evaluation is +supported. + +\subsection{Interpretation} + +Compiled mode may benefit most when process-wide fusion outweighs its +additional preparation cost. Eager mode can reduce repeated preparation when +many processes share a model, while recurrence provides a compact direct +representation of the current construction. The tables compare these +trade-offs under identical physics, selector, precision, and validation +conditions; no mode is assumed to be universally fastest. diff --git a/pyamplicol/src/pyamplicol/_profiling_campaign/section_lc_flow_layouts.tex b/pyamplicol/src/pyamplicol/_profiling_campaign/section_lc_flow_layouts.tex new file mode 100644 index 0000000000..9a5e688c83 --- /dev/null +++ b/pyamplicol/src/pyamplicol/_profiling_campaign/section_lc_flow_layouts.tex @@ -0,0 +1,148 @@ +% SPDX-License-Identifier: 0BSD +\section{Reusable Leading-Colour Execution Layouts} +\label{sec:lc-flow-layouts} + +Leading-colour coverage and its numerical layout are separate concepts. Let +\(\mathcal H\) be the retained helicities and \(\mathcal F\) the retained +physical colour flows. Complete leading-colour coverage represents +\begin{equation} + \mathcal C_{\mathrm{LC}}=\mathcal H\times\mathcal F + \label{eq:lc-complete-coverage} +\end{equation} +and can expose every resolved amplitude \(\mathcal A_{hf}\). For selected +subsets \(S_H\subseteq\mathcal H\) and \(S_F\subseteq\mathcal F\), the +observable is +\begin{equation} + \mathcal W(S_H,S_F) + =\mathcal N\sum_{h\in S_H}\sum_{f\in S_F}w_{hf} + \left|\sum_{r\in G(h,f)}\mathcal A^{(r)}_{hf}\right|^2 . + \label{eq:lc-runtime-selection} +\end{equation} +Here \(G(h,f)\) is a coherent contribution group, \(w_{hf}\) contains exact +symmetry and leading-colour weights, and \(\mathcal N\) is the process +normalization. The layout changes how this sum is scheduled, not its +components or normalization. + +\subsection{Two complete-coverage layouts} + +The report measures two layouts because selected-flow and all-flow workloads +offer different opportunities for sharing: + +\begin{center} +\small +\begin{tabular}{@{}L{1.32in}L{2.40in}L{2.40in}@{}} +\toprule +\textbf{property} & \textbf{topology replay} & + \textbf{all-flow union} \\ +\midrule +Measured workload & one selected flow, summed helicities & + all flows, one selected helicity \\ +Retained physics & every flow and helicity & + every flow and helicity \\ +Main reuse & equivalent flow topologies related by exact permutations and + factors & currents and closures shared directly across all flows \\ +Availability & compiled, eager, recurrence, and on-the-fly to + \(n_{\mathrm{final}}=4\) & + compiled, eager, and recurrence \\ +\bottomrule +\end{tabular} +\end{center} + +On-the-fly execution does not materialize a separate all-flow-union artifact. +Within its LC scope, one compact process-seed artifact serves both measured +workloads: one selected flow with helicities summed, and all physical flows +with one helicity selected. Thus the all-flow observable remains the same, but +its on-the-fly execution is reconstructed lazily from the shared compact +artifact rather than from \(D_{\mathrm{union}}\). + +In topology replay, exact relations partition physical flows into equivalence +classes. If \(\rho(f)\) represents the class containing \(f\), the calculation +records a permutation \(\pi_f\), an exact factor \(s_f\), and the corresponding +weight: +\begin{equation} + \mathcal A_{h f}(p) + =s_f\,\mathcal A_{\pi_f(h),\rho(f)}\!\left(\pi_f(p)\right). + \label{eq:lc-topology-replay} +\end{equation} +This avoids constructing an independent current graph for every physical +ordering and is suited to selected-flow, helicity-summed evaluation. + +The all-flow union instead merges the dependency graphs of all resolved +components: +\begin{equation} + D_{\mathrm{union}} + =\left(\bigcup_{h\in\mathcal H,\,f\in\mathcal F}D_{hf}\right) + \big/\!\sim , + \label{eq:lc-all-flow-union} +\end{equation} +where \(\sim\) identifies currents, kernel values, and closures that are +algebraically equal up to retained exact factors. For a selected helicity, +one graph traversal then reuses intermediate currents across all flows. + +Compiled mode specializes the chosen layout to process-wide stage evaluators. +Eager mode applies prepared-model kernels to the same dependency structure, +recurrence mode constructs a compact current schedule directly, and on-the-fly +mode reconstructs query-local traces from its compact process seed. These +execution choices do not change \(\mathcal H\), \(\mathcal F\), or +Eq.~\eqref{eq:lc-runtime-selection}. + +\subsection{Runtime selection} + +Flow and helicity selections refer to physical components. A selection may be +applied to a complete batch or specified independently for each point; omitting +an axis sums all retained entries on that axis. Rows with equal selections can +be grouped into contiguous numerical batches. This grouping changes neither +the generated coverage nor the resolved result. + +\subsection{Generation and reference boundaries} + +The two layouts build different graphs and numerical schedules, so their +process-generation times are distinct observables: +\begin{equation} + T_{\mathrm{gen}}^{\mathrm{replay}} + \quad\hbox{and}\quad + T_{\mathrm{gen}}^{\mathrm{union}}. + \label{eq:lc-two-generation-times} +\end{equation} +Cross-mode comparisons use the same layout on both sides. Preparation of +reusable model-level kernels occurs before the process-generation timer and is +not included in these table values. + +The on-the-fly exception has one process-generation quantity \(G\), because +the selected-flow and all-flow workloads share one compact artifact. Its first +full-batch evaluation \(W\) constructs the initially requested selector-family +state. Artifact loading and the conventional benchmark warmup calls are timed +separately and are excluded from \(W\). Calling \texttt{Runtime.clear()} drops +the lazy on-the-fly state without replacing the process artifact. + +The original-\AC{} reference uses two different routes. For the selected-flow +workload, its generation value measures creation of the process library. For +the all-flow workload, the reported setup value measures preparation of direct +all-flow evaluation. The corresponding runtime measurements implement the +same physical workloads as \PAC{}, but the all-flow setup quantities do not +define a like-for-like process-generation speedup. The table therefore reports +both quantities absolutely and marks their generation comparison as not +comparable. + +Before timing an on-the-fly cell, the complete resolved components for both +workloads are compared with a separately generated recurrence authority. The +same componentwise comparison is repeated after +\texttt{Runtime.clear()} to check the cold reconstruction path. + +\subsection{NLC and full colour} + +The alternative layouts apply only at leading colour. NLC and full-colour +evaluation construct an amplitude basis and contract it with the appropriate +colour metric: +\begin{equation} + \mathcal W_{\mathrm{contracted}} + =\mathcal N\sum_h\sum_{a,b} + \mathcal A_{ha}^{*}\,C_{ab}\,\mathcal A_{hb}. + \label{eq:contracted-colour-layout} +\end{equation} +These treatments do not expose the physical leading-colour flow axis, so there +is no topology-replay or all-flow-union distinction. On-the-fly NLC and +full-colour entries are selector-free contracted evaluations: their cold stage +builds the requested helicity family and evaluates the authenticated sparse-NLC +or complete-colour contraction payload. They expose helicity selection but no +leading-colour flow selector. diff --git a/pyamplicol/src/pyamplicol/_profiling_campaign/section_three_mode_performance_tables.tex b/pyamplicol/src/pyamplicol/_profiling_campaign/section_three_mode_performance_tables.tex new file mode 100644 index 0000000000..0d5369dd35 --- /dev/null +++ b/pyamplicol/src/pyamplicol/_profiling_campaign/section_three_mode_performance_tables.tex @@ -0,0 +1,230 @@ +% SPDX-License-Identifier: 0BSD +\section{Standard-Model Process Matrices} +\label{sec:three-mode-performance-tables} + +The process matrices use recurrence as the primary \PAC{} execution mode. +Built-in-SM and UFO-SM recurrence JIT O2 retain comparisons with legacy +original-\AC{} results. Separate built-in-SM matrices compare compiled JIT O3 +and eager JIT O2 with recurrence JIT O2 for the same process, colour treatment, +and observable. A built-in-SM on-the-fly JIT O2 matrix compares directly with +original-\AC{} for LC at \(n_{\mathrm{final}}\leq4\). Four additional +full-colour UFO-SM views compare recurrence, compiled, eager, and on-the-fly +with one shared authoritative MadGraph standalone reference. + +The canonical matrix spans electroweak vector production, charged-current and +leptonic channels, heavy-quark production, pure gluons, multi-boson final +states, Higgs-associated final states, and three- and four-quark-line cases. +Here \(n\) is the number of final-state particles. LC coverage retains every +declared family through \(n=9\), except that on-the-fly LC coverage currently +ends at \(n_{\mathrm{final}}=4\). NLC and full-colour grids display +\(n=1,\ldots,6\), including genuine on-the-fly contracted cells. A multiplicity +for which a process family does not yet exist +is marked \textsc{not applicable}; an otherwise defined process intentionally +excluded from the declared measurement scope is marked \textsc{not run}. +These are table structure, not unfilled measurements. A complete edition +accounts for every scheduled cell across the full declared range. +Resource-limited cells carry a documented status; every successful cell is +numerical and validated. An in-scope result that does not pass required +numerical validation is identified by a status and does not carry a timing +ratio. Original-\AC{} disagreement is the deliberate exception: it is a legacy +diagnostic, not a required validation failure. +Best-mode cells select the fastest successful candidate, including on-the-fly +where it is in scope and marking that winner with \texttt{(o)}. If none +succeeds, the recurrence attempt is shown with \texttt{(r)} and its terminal +status. + +The UFO-SM recurrence matrices use the same process definitions, colour +contracts, numerical parameters, selectors, and validation points as the +built-in-SM recurrence matrices. Their source is the packaged Standard Model +loaded through the serialized-JSON interface. This comparison verifies that the +imported and built-in model routes implement the same requested physics. +Outside the four MadGraph-reference full-colour views, compiled, eager, and +on-the-fly process matrices remain built-in-only, isolating differences between +execution modes from differences in model loading. + +\input{result_validation_summary.tex} + +\subsection*{Workloads and denominators} + +For a reported timing quantity \(T\), each multiplier is +\begin{equation} + r_{\mathrm{candidate/baseline}} + = \frac{T_{\mathrm{candidate}}}{T_{\mathrm{baseline}}}. + \label{eq:three-mode-matrix-time-ratio} +\end{equation} +Green, orange, and red denote respectively \(r<1\), \(1\leq r<2\), and +\(r\geq2\). Green therefore means that the candidate is faster than the +baseline named in the table heading. The recurrence matrices use original +\AC{} as the baseline, as does the detailed on-the-fly LC matrix. The compiled +and eager matrices use built-in-SM recurrence JIT O2 as the baseline. + +Every LC comparison contains two complete-coverage layouts: +\begin{itemize} + \item topology replay, measured with one physical colour flow selected and + all retained helicities summed; and + \item the all-flow union, measured with one physical helicity selected and + all retained colour flows summed. +\end{itemize} +Neither selection is fixed during generation. Each LC cell reports the two +baseline generation quantities and wall times, followed by candidate +generation entries and runtime ratios. The muted supplementary ratio first +compares separately measured execution attributions when both sides expose +compatible boundaries; otherwise it compares authenticated evaluator totals. +Only against original \AC{}, which has no evaluator-total clock, may a candidate +evaluator total use the legacy direct execution clock (or wall when direct +execution is absent) as denominator. An evaluator total is never divided by a +recurrence core. This evaluator-total fallback is diagnostic only; +execution/execution remains the compatible supplementary attribution. If no +compatible supplementary clock exists, only the wall ratio is shown. Compiled +and eager generation are compared with recurrence using the same layout. + +On-the-fly is the scoped LC exception to the two-artifact layout. One compact +process-seed artifact serves both the selected-flow, helicity-summed +workload and the all-flow, single-helicity workload. Its process-generation +time is \(G\). For selected flow, \texttt{([xG] x(G+W))} shows the muted +generation-only multiplier and the normally coloured multiplier after adding +the first full-batch evaluation \(W\), both relative to the matching \AC{} +library-generation time. For all flow, \texttt{[G] G+W} shows the two absolute +times, with \(G\) muted and \(G+W\) in normal black. In this generation-only +notation, \(W\) excludes artifact loading and the conventional benchmark +warmup calls. + +The two original-\AC{} reference workloads have different setup boundaries. +For the selected-flow workload, the reported generation quantity is creation +of the process library. For the all-flow workload, it is preparation of direct +all-flow evaluation. Runtime ratios compare the same physical observable. The +all-flow setup quantities are not like-for-like, so the table shows the +candidate process-generation time as an absolute value and forms no ratio. +For on-the-fly this is the absolute \texttt{[G] G+W} pair above. + +NLC and full colour use one helicity-summed, colour-contracted calculation per +mode. Their cells report the absolute baseline generation and wall time, +followed by the candidate generation and runtime ratios. No per-flow scalar or +flow selector is defined for these contracted colour accuracies. On-the-fly +uses the same public contracted result shape and authenticates its expanded +colour-contraction payload. + +The wall quantity is the repeated native evaluation time after caller-language +input conversion and is the primary runtime observable. Independent accumulated +evaluator-total and narrower recurrence-core clocks remain in the detailed +measurement evidence; neither is copied from wall time or from the other. The +compact runtime notation \texttt{([xS]xW)} shows a compatible supplementary +clock multiplier \texttt{xS} in muted brackets and the primary wall multiplier +\texttt{xW} in its performance colour. The hierarchy above determines whether +\texttt{xS} is execution attribution or a diagnostic evaluator-total fallback. +If no supplementary ratio is eligible, the cell shows only \texttt{(xW)}. + +Preparation of reusable model-level kernels is excluded from +process-generation time. Compiled generation still includes process-specific +evaluator construction and backend compilation. On-the-fly \(W\) starts only +after the compact artifact has been loaded and stops after its first complete +batch evaluation; later benchmark warmups are separate. + +A timing is shown only after the candidate passes its required internal and +cross-mode checks. Compiled and eager results agree with recurrence at the +strict cross-mode tolerance, and built-in and UFO-SM recurrence results are +compared directly. Each result must reproduce its resolved sum. The four new +full-colour UFO-SM views instead use the shared MadGraph standalone value as +the authoritative physics boundary: \PAC{} is evaluated at precision 200 at +the same point and must agree within relative tolerance \(10^{-10}\). For LC, +the common flow--helicity component is compared directly between topology +replay and all-flow union. On-the-fly adds a recurrence-authority gate: for +both workloads, its complete resolved-component array must agree with the +separately generated recurrence result before timing. After +\texttt{Runtime.clear()}, the full component comparison and the resolved-sum +identity are repeated before the cell is accepted. + +Original \AC{} remains visible as a performance baseline and numerical +diagnostic, but is not authoritative. A mismatch with it does not suppress the +candidate timing, ratio, or summary contribution; only the corresponding +\AC{} generation-time token is coloured red. This rule applies equally to LC, +NLC, and full-colour legacy comparisons. + +\subsection*{Cell and summary layout} + +\begin{center} +\small +\begin{tabular}{@{}L{0.95in}L{2.45in}L{5.75in}@{}} +\toprule +\textbf{contract} & \textbf{baseline entry} & \textbf{candidate entries} \\ +\midrule +LC, upper + & topology replay \texttt{|} all-flow generation quantities in seconds + & layout-matched ratios, except that the all-flow candidate against original + \AC{} is shown absolutely without a ratio; on-the-fly uses selected + \texttt{([xG] x(G+W))} \texttt{|} all-flow \texttt{[G] G+W} \\ +LC, lower + & selected-flow \texttt{|} all-flow wall times in + \(\mu\mathrm{s}/\mathrm{pt}\) + & compact \texttt{([xS]xW)} comparisons, or \texttt{(xW)} when only wall is + comparable \\ +NLC and full-colour, upper + & contracted generation time in seconds + & candidate generation ratio \\ +NLC and full-colour, lower + & contracted wall time in \(\mu\mathrm{s}/\mathrm{pt}\) + & compact \texttt{([xS]xW)} comparison, or \texttt{(xW)} when only wall is + comparable \\ +\bottomrule +\end{tabular} +\end{center} + +Summary rows contain multipliers only, ordered as minimum, maximum, median, +arithmetic mean, and weighted mean. The arithmetic mean averages the +individual candidate/baseline multipliers; the weighted mean is the ratio of +sums +\(\sum_i T_{\mathrm{candidate},i}/\sum_i T_{\mathrm{baseline},i}\). +Generation summaries use topology replay for LC and the contracted workload +for NLC and full colour; the non-comparable LC all-flow generation quantity is +omitted. In the best-mode overview, an on-the-fly winner contributes its +displayed primary \(G+W\) quantity, not the muted generation-only \(G\) +diagnostic. Fixed-engine on-the-fly summaries retain their generation-only +comparison. Runtime summaries use wall time only, with +separate aligned LC topology-replay and all-flow lines. Structurally inapplicable, +resource-limited, unsupported, and validation-failed cells do not contribute. + +The first three matrices provide a built-in-SM overview against original +\AC{}. For each process, multiplicity, colour treatment, and LC workload, +they select the validated pyAmpliCol mode with the smallest wall time. +Selection is independent in every cell: the overview records the measured +winner rather than prescribing one mode for all processes. Its upper row +reports generation and its lower row reports runtime. LC pairs use +non-union-flow \texttt{|} union-flow ordering. Since original-\AC{} all-flow +setup and \PAC{} process generation have different boundaries, the latter is +shown absolutely rather than as a misleading ratio. When on-the-fly is the +winner, the same \texttt{([xG] x(G+W))} and \texttt{[G] G+W} generation +notation applies beside its \texttt{(o)} marker. + +\input{result_matrix_best_builtin_sm_lc_table.tex} +\input{result_matrix_best_builtin_sm_nlc_table.tex} +\input{result_matrix_best_builtin_sm_full_table.tex} + +The detailed tables are grouped by execution mode, model source, and colour +treatment. + +\input{result_matrix_recurrence_builtin_sm_lc_table.tex} +\input{result_matrix_recurrence_builtin_sm_nlc_table.tex} +\input{result_matrix_recurrence_builtin_sm_full_table.tex} +\input{result_matrix_recurrence_ufo_sm_lc_table.tex} +\input{result_matrix_recurrence_ufo_sm_nlc_table.tex} +\input{result_matrix_recurrence_ufo_sm_full_table.tex} +\input{result_matrix_compiled_builtin_sm_lc_table.tex} +\input{result_matrix_compiled_builtin_sm_nlc_table.tex} +\input{result_matrix_compiled_builtin_sm_full_table.tex} +\input{result_matrix_eager_builtin_sm_lc_table.tex} +\input{result_matrix_eager_builtin_sm_nlc_table.tex} +\input{result_matrix_eager_builtin_sm_full_table.tex} +\input{result_matrix_on_the_fly_builtin_sm_lc_table.tex} +\input{result_matrix_on_the_fly_builtin_sm_nlc_table.tex} +\input{result_matrix_on_the_fly_builtin_sm_full_table.tex} + +The following four views reuse the candidate measurements above and one shared +MadGraph full-colour reference cache; they do not create duplicate campaign +measurements. The recurrence, compiled, and eager comparisons use p200 +candidate values, while the genuine on-the-fly contracted comparison uses its +supported p16 lane. + +\input{result_matrix_recurrence_ufo_sm_full_vs_madgraph_table.tex} +\input{result_matrix_compiled_ufo_sm_full_vs_madgraph_table.tex} +\input{result_matrix_eager_ufo_sm_full_vs_madgraph_table.tex} +\input{result_matrix_on_the_fly_ufo_sm_full_vs_madgraph_table.tex} diff --git a/pyamplicol/src/pyamplicol/_profiling_campaign/section_ufo_support.tex b/pyamplicol/src/pyamplicol/_profiling_campaign/section_ufo_support.tex new file mode 100644 index 0000000000..3f01e3d9d2 --- /dev/null +++ b/pyamplicol/src/pyamplicol/_profiling_campaign/section_ufo_support.tex @@ -0,0 +1,163 @@ +% SPDX-License-Identifier: 0BSD +\section{Supported UFO and Serialized-JSON Models} +\label{sec:ufo-support} + +\PAC{} accepts a documented subset of the Universal FeynRules Output (UFO) +format and an equivalent serialized-JSON representation. Both sources are +normalized before process generation and then use the same current recursion, +colour treatment, and numerical interfaces as the built-in models. Unsupported +structures are rejected explicitly rather than interpreted approximately. + +\subsection{Sources and normalization} + +The two input routes are +\begin{equation} + \{\text{trusted UFO module},\text{ serialized JSON}\} + \longrightarrow + \text{normalized model} + \longrightarrow + \text{numerical kernels}. + \label{eq:ufo-loading-route} +\end{equation} +A UFO module is executable Python and must therefore come from a trusted +source. The serialized JSON form is data-only and is preferable for portable +exchange and archival. After loading, both forms describe the same particles, +parameters, propagators, interactions, coupling orders, Lorentz tensors, and +colour tensors. + +Model symbols remain in a model-local namespace, so equally named parameters +from different models are not identified accidentally. Normalization also +makes component orderings and particle conjugation explicit. A model is +accepted for ordinary process generation only when the normalized model +satisfies the support boundary below; an unrepresentable requested interaction +is rejected explicitly. + +\subsection{Interactions and exact verification} + +A UFO vertex is a sum over colour and Lorentz structures, +\begin{equation} + \mathcal V(1,\ldots,n) + =\sum_{a,b}g_{ab}\, + C_a(c_1,\ldots,c_n)\, + L_b(s_1,p_1;\ldots;s_n,p_n). + \label{eq:ufo-vertex-matrix} +\end{equation} +Each term retains its coupling-order assignment. The supported Lorentz basis +includes scalars, momenta, metrics, Dirac identity and gamma matrices, +\(\gamma_5\), chiral projectors, slashed momenta, and sigma tensors. The +supported SU(3) representations are the singlet, fundamental, +antifundamental, and adjoint. + +Colour projection and symmetry reuse are based on exact normalized +expressions. Numerical agreement at a sample point is not used to establish an +algebraic identity. When an identity needed only for reuse cannot be proved, +the corresponding contributions are evaluated separately. When an identity +is required to represent an interaction at all, failure to establish it causes +the model or process to be rejected. + +Leading-colour singlet subtraction, for example, follows from the fundamental +Fierz identity +\begin{equation} + (T^a)_i{}^{\bar\jmath}(T^a)_k{}^{\bar l} + =T_R\left( + \delta_i{}^{\bar l}\delta_k{}^{\bar\jmath} + -\frac{1}{N_c} + \delta_i{}^{\bar\jmath}\delta_k{}^{\bar l} + \right). + \label{eq:fierz} +\end{equation} +It is used only when the colour representation, Lorentz interaction, and +vector propagator satisfy the corresponding exact conditions. The same +principle applies to vectorlike parity, Yang--Mills relations, and current +reflection. + +\subsection{Contact interactions and propagators} + +Supported contact interactions are rewritten with non-propagating auxiliary +currents only when the original tensor can be reconstructed exactly. The +physical coupling appears once, and the rewriting introduces no artificial +pole. Colour-singlet scalar contacts provide the validation ladder for this +construction. Higher-valence singlet interactions are accepted when their +normalized tensor admits the same exact reconstruction. + +A coloured four-vector gauge contact is accepted only for the two-\(f\) +structure +\begin{equation} + C^{a_1a_2a_3a_4} + =c\,f^{a_i a_j b}f^{b a_k a_r}, + \label{eq:two-f-color} +\end{equation} +up to exact permutations, with one shared adjoint index and a matching Lorentz +factorization. Other coloured contact tensors require a separately supported +intermediate colour channel. + +Standard scalar, Dirac, vector, and spin-two propagators use the mass, width, +gauge, and component ordering declared by the normalized model. A custom UFO +propagator is accepted only when its numerator and denominator can be lowered +exactly; optimizations that assume a standard propagator are then disabled. +Massless spin two is validated by the scalar-gravity model. Massive spin-two +support remains experimental because it has not yet been tested across a +comparably broad set of models. + +\subsection{Support boundary} + +In the table, \emph{conditional} means that the stated exact algebraic +condition must be established for the particular model expression. It does not +imply acceptance of every algebraically equivalent UFO spelling. + +\begingroup +\small +\setlength{\LTpre}{0.3em} +\setlength{\LTpost}{0.3em} +\begin{longtable}{@{}L{0.83in}L{1.47in}L{0.93in}L{3.15in}@{}} +\toprule +\textbf{area} & \textbf{class} & \textbf{status} & \textbf{boundary} \\ +\midrule +\endfirsthead +\toprule +\textbf{area} & \textbf{class} & \textbf{status} & \textbf{boundary} \\ +\midrule +\endhead +source & UFO directory / JSON & supported & UFO Python must be trusted; JSON + is data-only and both use the same normalized model \\ +spin & scalar, Dirac, vector & supported & standard wavefunctions, + propagators, component orderings, and closures \\ +spin & massless spin two & supported & tensor path validated by the + scalar-gravity model \\ +spin & massive spin two & experimental & Fierz--Pauli path is available, but + broad model validation is incomplete \\ +spin & UFO ghosts & partial & propagating ghosts require an exactly + representable custom propagator and are not broadly qualified \\ +spin & Majorana/FNV, spin \(3/2\) & unsupported & no + fermion-number-violating-flow or Rarita--Schwinger current prescription \\ +colour & \(1,3,\bar3,8\) of SU(3) & supported & LC, NLC, and full-colour + treatments described in this report \\ +colour & \(6,\bar6\), extra groups & unsupported & no sextet or + multiple-group flow and contraction representation \\ +arity 3 & \(1,\delta,T,f\) colour & supported & exact colour projection and + representable Lorentz components \\ +arity 3 & \(d^{abc}\), \(\epsilon\), \(K_6\), \(T_6\) & unsupported & no + supported colour-flow and contraction prescription \\ +arity 4 & colour singlet & conditional & exact tensor reconstruction by + non-propagating auxiliary currents \\ +arity 4 & two-\(f\) gauge colour & conditional & one shared adjoint index, + trivalent topology, and exact Lorentz reconstruction \\ +arity 4 & other coloured tensors & unsupported & no supported auxiliary colour + channel \\ +arity \(>4\) & colour-singlet contact & conditional & exact no-pole + reconstruction with the original coupling inserted once \\ +arity \(>4\) & coloured contact & unsupported & no general + irreducible-channel factorization \\ +Lorentz & normalized tensor basis & supported & every tensor function must + normalize with explicit component axes \\ +Lorentz & form factors / unknown functions & unsupported & no portable + numerical prescription \\ +propagator & standard \(0,\frac12,1,2\) & supported & model mass and width, + with explicit component ordering and closure \\ +propagator & custom UFO tensor & conditional & exact component lowering; + standard-propagator optimizations are disabled \\ +symmetry & vectorlike/Yang--Mills/reflection & conditional & exact normalized + identities over the complete interaction sector used by the process \\ +\bottomrule +\end{longtable} +\endgroup diff --git a/pyamplicol/src/pyamplicol/_profiling_campaign/section_zgg_dag.tex b/pyamplicol/src/pyamplicol/_profiling_campaign/section_zgg_dag.tex new file mode 100644 index 0000000000..2a78286be4 --- /dev/null +++ b/pyamplicol/src/pyamplicol/_profiling_campaign/section_zgg_dag.tex @@ -0,0 +1,83 @@ +% SPDX-License-Identifier: 0BSD +\section{\texorpdfstring{Shared-Current DAG and Numerical Execution}{Shared-Current DAG and Numerical Execution}} +\label{sec:zgg-dag} + +The current recursion becomes a directed acyclic graph (DAG) when physically +identical intermediate currents are shared. This section summarizes the +identity and ordering needed for that reuse and relates them to the three +execution modes. + +\subsection{Current identity and reuse} + +Two contributions share a current only when every physics-relevant label +agrees. Schematically, +\begin{equation} + \Omega(J)= + \bigl(t,I,H,s,F,Q,C,O,a,\pi_I\bigr), + \label{eq:zgg-current-key} +\end{equation} +where the entries identify the particle species, external subset, helicity and +spin ancestry, flavour and additive charges, colour state, perturbative order, +auxiliary-current type, and colour-sensitive ordering. The subset determines +the current momentum \(q_I=\sum_{i\in I}q_i\), while the ordered labels prevent +distinct colour traversals from being merged. + +Exact identity permits one stored current to serve every amplitude contribution +that needs it. A second kind of reuse applies when different currents require +the same numerical kernel value up to a known exact factor. Neither form of +reuse merges distinct physical helicity or colour-flow components. + +\subsection{Topological ordering} + +The graph is evaluated in increasing external-subset size. Every input is +therefore available before the current that consumes it. For the representative +\(d\bar d\to Zgg\) process, the ordering is schematically +\begin{equation} + \boxed{\text{sources}} + \longrightarrow + \boxed{|I|=2} + \longrightarrow + \boxed{|I|=3} + \longrightarrow + \boxed{|I|=4} + \longrightarrow + \boxed{\text{amplitude closures}} . + \label{eq:zgg-stage-chain} +\end{equation} +At each stage, all admissible partitions contributing to the same current are +summed coherently before propagation. The final closures join the largest +retained currents to the remaining external states. This ordering makes the +dependency structure explicit without assigning physical meaning to a +particular storage layout. + +\subsection{Numerical realization} + +Compiled mode combines many relations in a stage into process-specific +multi-output evaluators. Eager mode follows the same process graph using +reusable prepared-model kernels. Recurrence mode represents the current and +closure sequence directly. All three preserve the same coherent sums, colour +contraction, helicity treatment, symmetry weights, and normalization. + +For resolved leading-colour evaluation, the public result has the form +\begin{equation} + R_{p,h,f}=\left|\mathcal M_{h,f}(p)\right|^2, + \qquad + \operatorname{shape}(R)= + (N_{\rm point},N_{\rm helicity},N_{\rm flow}) . + \label{eq:zgg-resolved-shape} +\end{equation} +Structural zeros remain explicit, and summing the non-point axes must reproduce +the directly evaluated total within the stated numerical tolerance. + +\subsection{Timing boundary} + +The primary runtime quantity is the complete native wall time after +caller-language array conversion. It includes source and momentum preparation, +the mode-specific numerical evaluation, selectors, closures, and final +reduction. Packing performed by the caller is outside this boundary. + +Some backends additionally expose a narrower execution attribution. Because +its precise boundary is mode-specific, it is reported only as a supplementary +quantity and only when measured directly. The complete wall time remains the +common observable used for comparisons; an unavailable attribution is marked +explicitly rather than inferred. diff --git a/pyamplicol/src/pyamplicol/_profiling_campaign/section_zgg_example.tex b/pyamplicol/src/pyamplicol/_profiling_campaign/section_zgg_example.tex new file mode 100644 index 0000000000..86f7892480 --- /dev/null +++ b/pyamplicol/src/pyamplicol/_profiling_campaign/section_zgg_example.tex @@ -0,0 +1,143 @@ +% SPDX-License-Identifier: 0BSD +\section{\texorpdfstring{Worked \(d\bar d\to Zgg\) Example}{Worked d dbar -> Z g g Example}} +\label{sec:zgg-example} + +This example illustrates how a familiar tree-level process is represented by +reusable off-shell currents. It also fixes the momentum, helicity, and colour +conventions used in the performance tables. + +\subsection{External legs and conventions} + +The physical process is +\begin{equation} + d(p_1)\,\bar d(p_2) \longrightarrow Z(p_3)\,g(p_4)\,g(p_5), + \qquad p_1+p_2=p_3+p_4+p_5 . + \label{eq:zgg-physical-process} +\end{equation} +The current recursion uses the all-outgoing convention +\begin{equation} + q_1=-p_1,\quad q_2=-p_2,\quad q_i=p_i\ (i=3,4,5), + \qquad \sum_{i=1}^{5}q_i=0 . + \label{eq:zgg-all-outgoing} +\end{equation} +External momenta are supplied in the physical convention of +Eq.~\eqref{eq:zgg-physical-process} and crossed to the all-outgoing convention +before the recursion is evaluated. + +\begin{center} +\small +\begin{tabular}{@{}cL{0.90in}L{1.05in}L{1.00in}cL{1.35in}@{}} +\toprule +\textbf{label} & \textbf{physical leg} & \textbf{outgoing state} & +\textbf{momentum} & \textbf{states} & \textbf{colour role} \\ +\midrule +$1$ & incoming $d$ & outgoing $\bar d$ & $q_1=-p_1$ & $2$ & + $\overline{\mathbf 3}$, line end \\ +$2$ & incoming $\bar d$ & outgoing $d$ & $q_2=-p_2$ & $2$ & + $\mathbf 3$, line start \\ +$3$ & outgoing $Z$ & outgoing $Z$ & $q_3=p_3$ & $3$ & singlet \\ +$4$ & outgoing $g$ & outgoing $g$ & $q_4=p_4$ & $2$ & adjoint insertion \\ +$5$ & outgoing $g$ & outgoing $g$ & $q_5=p_5$ & $2$ & adjoint insertion \\ +\bottomrule +\end{tabular} +\end{center} + +There are \(2\times2\times3\times2\times2=48\) physical helicity +configurations. Twenty-four vanish identically because the massless quark line +permits only chirality-compatible initial-state helicities; they remain +explicit zero entries in resolved results. + +At leading colour the \(Z\) is absent from the colour word. Complete coverage +contains the two orderings +\begin{equation} + c_0=(2,4,5,1),\qquad c_1=(2,5,4,1). + \label{eq:zgg-physical-flows} +\end{equation} +Both are retained during generation, so either flow can be selected at +evaluation time. + +\subsection{Reusable current recursion} + +For a nonempty source subset \(I\), define its momentum by +\begin{equation} + q_I=\sum_{i\in I}q_i . + \label{eq:zgg-subset-mask} +\end{equation} +A one-leg current is the appropriate external spinor, polarization vector, or +scalar wavefunction. Composite currents obey the binary recursion +\begin{equation} + J_{t,\Omega}^{a}(I) + =P_t{}^{a}{}_{b}(q_I) + \sum_{\substack{I=A\mathbin{\dot\cup}B\\ + A,B\ne\varnothing}} + \sum_{v\,:\,t_A t_B\to t} + \kappa_v\, + V_v{}^{b}{}_{cd}(q_A,q_B) + J_{t_A,\Omega_A}^{c}(A) + J_{t_B,\Omega_B}^{d}(B). + \label{eq:zgg-current-recursion} +\end{equation} +Here \(t\) is the particle state, \(P_t\) is its propagator, \(V_v\) is an +allowed interaction, and \(\Omega\) collects the quantum numbers required to +identify the current. Only combinations consistent with the model, +perturbative order, and colour ordering enter the sum. + +Representative terms along the quark line are +\begin{align} + J_d(2,5) &={P}_d\,V_{dgd}\bigl[J_d(2),J_g(5)\bigr], + \label{eq:zgg-j25}\\ + J_g(4,5) &={P}_g\,V_{ggg}\bigl[J_g(4),J_g(5)\bigr], + \label{eq:zgg-j45}\\ + J_d(2,4,5) &={P}_d\Bigl( + V_{dgd}\bigl[J_d(2,5),J_g(4)\bigr] + +V_{dgd}\bigl[J_d(2,4),J_g(5)\bigr] + +V_{dgd}\bigl[J_d(2),J_g(4,5)\bigr] + \Bigr), + \label{eq:zgg-j245}\\ + J_d(2,3,4,5) &={P}_d\!\sum_{A\dot\cup B=\{2,3,4,5\}} + V\bigl[J(A),J(B)\bigr]. + \label{eq:zgg-j2345} +\end{align} +Equation~\eqref{eq:zgg-j2345} includes every admissible placement of the +\(Z\) emission and both gluon insertions. Ordered currents such as +\(J_d(2,5,4)\) and \(J_d(2,4,5)\) remain distinct even though their momentum +subset is the same. + +The final closure joins the endpoint current to source label \(1\): +\begin{equation} + {\cal A}_{h,c}^{(r)} + =C^{(r)}_{\dot a a}\, + J_{\bar d}^{\dot a}(1)\, + J_d^a(2,3,4,5), + \qquad + {\cal A}_{h,c}=\sum_{r\in G(h,c)}{\cal A}_{h,c}^{(r)} . + \label{eq:zgg-amplitude-root} +\end{equation} +The set \(G(h,c)\) contains contributions that must be summed coherently. +Identical subcurrents are evaluated once and reused wherever their complete +quantum-number and ordering labels agree. This sharing is the essential +difference between the current DAG and an independent evaluation of every +Feynman diagram. + +\subsection{Resolved observable and normalization} + +The two leading-colour execution layouts evaluate the same complete set of +helicity and flow components. One is optimized for a selected flow with summed helicities; +the other shares work across all flows for a selected helicity. Their resolved +components and their summed matrix element are numerically identical. + +For the built-in Standard Model, the normalization used in this example is +\begin{equation} + |{\cal M}|^2 + ={\cal N}\sum_{h,c}w_{h,c}\, + \left|\sum_{r\in G(h,c)}{\cal A}_{h,c}^{(r)}\right|^2, + \qquad + {\cal N}=\frac{27\,(4\pi\alpha_s)^2(8\pi\alpha_{\rm EW})} + {36\times2}. + \label{eq:zgg-normalization} +\end{equation} +The denominator averages over the incoming colour and spin states, while the +factor \(2=2!\) accounts for the identical final-state gluons. Coupling +powers, colour weights, coherent groups, and symmetry factors are retained +separately so that changing numerical model parameters does not alter the +process definition. diff --git a/pyamplicol/src/pyamplicol/_profiling_campaign/steer_performance_campaign.py b/pyamplicol/src/pyamplicol/_profiling_campaign/steer_performance_campaign.py new file mode 100755 index 0000000000..071a694a63 --- /dev/null +++ b/pyamplicol/src/pyamplicol/_profiling_campaign/steer_performance_campaign.py @@ -0,0 +1,140 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: 0BSD +"""Steer a copied pyAmpliCol profiling campaign.""" + +from __future__ import annotations + +import os +import sys +from importlib import import_module +from pathlib import Path + + +def _entrypoint_from_invocation(raw_entrypoint: str) -> Path: + """Preserve a trustworthy lexical campaign path for relative launchers.""" + + entrypoint = Path(raw_entrypoint) + if entrypoint.is_absolute(): + return Path(os.path.abspath(os.fspath(entrypoint))) + + logical_text = os.environ.get("PWD") + logical_cwd = Path(logical_text) if logical_text else None + try: + physical_cwd = Path.cwd() + same_directory = ( + logical_cwd is not None + and logical_cwd.is_absolute() + and logical_cwd.samefile(physical_cwd) + ) + except OSError: + same_directory = False + if not same_directory: + raise RuntimeError( + "campaign launcher cannot identify its working directory: " + "the shell PWD is missing or no longer names the physical directory. " + "Run `cd ..` and re-enter the intended campaign directory, then retry " + "(or invoke its absolute path from another working directory)." + ) + return Path(os.path.abspath(os.fspath(logical_cwd / entrypoint))) + + +def _repository_root(entrypoint: Path) -> Path: + for candidate in entrypoint.parents: + if (candidate / "tools/performance_report").is_dir() and ( + candidate / "src/pyamplicol" + ).is_dir(): + return candidate + raise RuntimeError("campaign entrypoint is not inside a pyAmpliCol checkout") + + +def _embedded_profile(entrypoint: Path, repo_root: Path) -> str: + """Derive the campaign identity from its directory under report profiles.""" + + profile_parent = repo_root / "docs/performance_reports" + try: + relative = entrypoint.parent.relative_to(profile_parent) + except ValueError as error: + raise RuntimeError( + "campaign entrypoint must be directly inside " + "docs/performance_reports/" + ) from error + if len(relative.parts) != 1: + raise RuntimeError( + "campaign entrypoint must be directly inside " + "docs/performance_reports/" + ) + return relative.parts[0] + + +def _reexecute_with_repository_python(repo_root: Path, entrypoint: Path) -> None: + expected = repo_root / ".venv/bin/python" + if not expected.is_file(): + raise RuntimeError( + f"repository Python is unavailable at {expected}; run `just dev-install`" + ) + try: + current_environment = Path(sys.prefix).resolve(strict=True) + expected_environment = (repo_root / ".venv").resolve(strict=True) + except OSError as error: + raise RuntimeError(f"cannot resolve repository Python: {error}") from error + if current_environment == expected_environment: + return + environment = dict(os.environ) + # macOS framework Python leaves this launcher hint behind when a shebang + # process execs a virtual-environment interpreter. Keeping it can make + # the new interpreter ignore the repository venv's site-packages. + environment.pop("__PYVENV_LAUNCHER__", None) + environment.pop("PYTHONHOME", None) + os.execve( + os.fspath(expected), + (os.fspath(expected), os.fspath(entrypoint), *sys.argv[1:]), + environment, + ) + + +def main() -> int: + # Keep the lexical path until the controller has rejected symlinked + # campaign directories and ancestors. Resolving here would erase the + # exact path that must be validated before campaign state is opened. + try: + entrypoint = _entrypoint_from_invocation(sys.argv[0]) + except RuntimeError as error: + sys.stderr.write(f"error: {error}\n") + return 2 + profile = entrypoint.parent.name + try: + repo_root = _repository_root(entrypoint) + source_profile = _embedded_profile(entrypoint, repo_root) + except RuntimeError: + installed = import_module( + "pyamplicol._performance_report.manual_campaign" + ) + return installed.main( + sys.argv[1:], + repo_root=entrypoint.parent, + profile=profile, + docs_dir=entrypoint.parent, + installed=True, + launcher_path_checked=True, + ) + + _reexecute_with_repository_python(repo_root, entrypoint) + sys.path.insert(0, os.fspath(repo_root)) + sys.path.insert(0, os.fspath(repo_root / "src")) + from tools.performance_report.manual_campaign import main as campaign_main + + return campaign_main( + sys.argv[1:], + repo_root=repo_root, + profile=source_profile, + docs_dir=entrypoint.parent, + launcher_path_checked=True, + ) + + +if __name__ == "__main__": + try: + raise SystemExit(main()) + except KeyboardInterrupt: + sys.stderr.write("Interrupted; workers were asked to stop cleanly.\n") + raise SystemExit(130) from None diff --git a/pyamplicol/src/pyamplicol/_sdk/__init__.py b/pyamplicol/src/pyamplicol/_sdk/__init__.py new file mode 100644 index 0000000000..d5ab7c89cc --- /dev/null +++ b/pyamplicol/src/pyamplicol/_sdk/__init__.py @@ -0,0 +1,6 @@ +# SPDX-License-Identifier: 0BSD +"""Installed C, C++, and Fortran Rusticol SDK resources.""" + +from .config import SdkInfo, load_sdk_info + +__all__ = ["SdkInfo", "load_sdk_info"] diff --git a/pyamplicol/src/pyamplicol/_sdk/config.py b/pyamplicol/src/pyamplicol/_sdk/config.py new file mode 100644 index 0000000000..9e1b3450a0 --- /dev/null +++ b/pyamplicol/src/pyamplicol/_sdk/config.py @@ -0,0 +1,9 @@ +# SPDX-License-Identifier: 0BSD +"""Compatibility import for the standalone, metadata-only SDK query utility.""" + +from rusticol_config import SdkInfo, SdkUnavailableError, load_sdk_info, main + +__all__ = ["SdkInfo", "SdkUnavailableError", "load_sdk_info", "main"] + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/pyamplicol/src/pyamplicol/_sdk/rust/rusticol.rs b/pyamplicol/src/pyamplicol/_sdk/rust/rusticol.rs new file mode 100644 index 0000000000..0ae7ee6e49 --- /dev/null +++ b/pyamplicol/src/pyamplicol/_sdk/rust/rusticol.rs @@ -0,0 +1,1836 @@ +// SPDX-License-Identifier: 0BSD +// Dependency-free safe Rust 2021 bindings for Rusticol C ABI v1. + +use std::any::Any; +use std::error; +use std::ffi::{c_char, c_int, c_void, CStr, CString}; +use std::fmt; +use std::marker::PhantomData; +use std::panic::{catch_unwind, resume_unwind, AssertUnwindSafe}; +use std::path::Path; +use std::ptr::{self, NonNull}; +use std::rc::Rc; + +/// C ABI version implemented by this source wrapper. +pub const ABI_VERSION: u32 = 1; + +/// Dependency-free complex value; no C complex memory layout is assumed. +#[derive(Clone, Copy, Debug, Default, PartialEq)] +pub struct Complex64 { + pub re: f64, + pub im: f64, +} + +impl Complex64 { + pub const fn new(re: f64, im: f64) -> Self { + Self { re, im } + } +} + +/// A literal vector on a one-based public leg. One entry broadcasts over points. +#[derive(Clone, Debug)] +pub struct SpinCorrelationVector { + pub leg: usize, + pub components: Vec<[Complex64; 4]>, +} + +#[derive(Clone, Debug)] +pub struct CorrelatedRequest { + pub color_correlation: String, + /// None inherits the setter; Some(vec![]) selects physical helicities. + pub spin_vectors: Option>, +} + +impl CorrelatedRequest { + pub fn new(color_correlation: impl Into) -> Self { + Self { + color_correlation: color_correlation.into(), + spin_vectors: None, + } + } +} + +#[derive(Clone, Debug)] +pub struct CorrelatedEvaluation { + pub values: Vec, + pub request_count: usize, + pub point_count: usize, +} + +impl CorrelatedEvaluation { + /// Access request-major output by its zero-based request and point indices. + pub fn get(&self, request: usize, point: usize) -> Option { + if request >= self.request_count || point >= self.point_count { + None + } else { + self.values + .get(request.checked_mul(self.point_count)?.checked_add(point)?) + .copied() + } + } +} + +/// The category of a Rusticol SDK error. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum ErrorKind { + InvalidArgument, + BufferTooSmall, + Runtime, + Panic, + AbiMismatch, + InvalidInput, + InvalidUtf8, + InvalidResponse, + SizeOverflow, + UnknownStatus(c_int), +} + +/// A typed error returned by the safe Rusticol wrapper. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Error { + kind: ErrorKind, + message: String, +} + +impl Error { + fn new(kind: ErrorKind, message: impl Into) -> Self { + Self { + kind, + message: message.into(), + } + } + + fn from_status(status: c_int) -> Self { + let kind = match status { + ffi::STATUS_INVALID_ARGUMENT => ErrorKind::InvalidArgument, + ffi::STATUS_BUFFER_TOO_SMALL => ErrorKind::BufferTooSmall, + ffi::STATUS_RUNTIME_ERROR => ErrorKind::Runtime, + ffi::STATUS_PANIC => ErrorKind::Panic, + other => ErrorKind::UnknownStatus(other), + }; + Self::new(kind, last_error_message(status)) + } + + pub fn kind(&self) -> ErrorKind { + self.kind + } + + pub fn message(&self) -> &str { + &self.message + } +} + +impl fmt::Display for Error { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str(&self.message) + } +} + +impl error::Error for Error {} + +/// Result type used by the safe Rusticol wrapper. +pub type Result = std::result::Result; + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ExternalParticle { + pub index: usize, + pub pdg: i32, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct HelicityConfiguration { + pub id: String, + pub helicities: Vec, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ColorComponent { + pub id: String, + pub kind: String, + pub word: Vec, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ModelParameter { + pub name: String, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum WarmUpEventKind { + Start, + Update, + End, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum WarmUpStage { + ProcessPreparation, + QueryFamily, + FamilyFinalization, + FirstEvaluation, +} + +/// One throttled progress snapshot from an explicit OTF warm-up. +#[derive(Clone, Debug, PartialEq)] +pub struct WarmUpProgress { + pub schema_version: u32, + pub kind: WarmUpEventKind, + pub stage: WarmUpStage, + pub completed: u64, + pub total: u64, + pub elapsed_seconds: f64, + pub current_rss_bytes: Option, + pub peak_rss_bytes: Option, + pub workers: u64, +} + +impl WarmUpProgress { + fn from_ffi(event: &ffi::WarmUpProgressEvent) -> Result { + if event.schema_version != 1 { + return Err(Error::new( + ErrorKind::InvalidResponse, + format!( + "Rusticol returned unsupported warm-up progress schema {}", + event.schema_version + ), + )); + } + let kind = match event.kind { + ffi::WARM_UP_EVENT_START => WarmUpEventKind::Start, + ffi::WARM_UP_EVENT_UPDATE => WarmUpEventKind::Update, + ffi::WARM_UP_EVENT_END => WarmUpEventKind::End, + value => { + return Err(Error::new( + ErrorKind::InvalidResponse, + format!("Rusticol returned unknown warm-up event kind {value}"), + )); + } + }; + let stage = match event.stage { + ffi::WARM_UP_STAGE_PROCESS_PREPARATION => WarmUpStage::ProcessPreparation, + ffi::WARM_UP_STAGE_QUERY_FAMILY => WarmUpStage::QueryFamily, + ffi::WARM_UP_STAGE_FAMILY_FINALIZATION => WarmUpStage::FamilyFinalization, + ffi::WARM_UP_STAGE_FIRST_EVALUATION => WarmUpStage::FirstEvaluation, + value => { + return Err(Error::new( + ErrorKind::InvalidResponse, + format!("Rusticol returned unknown warm-up stage {value}"), + )); + } + }; + Ok(Self { + schema_version: event.schema_version, + kind, + stage, + completed: event.completed, + total: event.total, + elapsed_seconds: event.elapsed_seconds, + current_rss_bytes: (event.current_rss_available != 0) + .then_some(event.current_rss_bytes), + peak_rss_bytes: (event.peak_rss_available != 0).then_some(event.peak_rss_bytes), + workers: event.workers, + }) + } +} + +/// Summary of exactly one binary64 point used to warm an OTF selection. +#[derive(Clone, Debug, PartialEq)] +pub struct WarmUpResult { + pub schema_version: u32, + pub elapsed_seconds: f64, + pub query_count: u64, + pub warmed_query_count: u64, + pub current_rss_bytes: Option, + pub peak_rss_bytes: Option, + pub already_warm: bool, + pub first_evaluation_completed: bool, +} + +impl WarmUpResult { + fn from_ffi(result: ffi::WarmUpResult) -> Result { + if result.schema_version != 1 { + return Err(Error::new( + ErrorKind::InvalidResponse, + format!( + "Rusticol returned unsupported warm-up result schema {}", + result.schema_version + ), + )); + } + if result.first_evaluation_completed == 0 { + return Err(Error::new( + ErrorKind::InvalidResponse, + "Rusticol reported a successful warm-up without its first evaluation", + )); + } + Ok(Self { + schema_version: result.schema_version, + elapsed_seconds: result.elapsed_seconds, + query_count: result.query_count, + warmed_query_count: result.warmed_query_count, + current_rss_bytes: (result.current_rss_available != 0) + .then_some(result.current_rss_bytes), + peak_rss_bytes: (result.peak_rss_available != 0).then_some(result.peak_rss_bytes), + already_warm: result.already_warm != 0, + first_evaluation_completed: true, + }) + } +} + +struct WarmUpCallbackState<'a> { + callback: &'a mut dyn FnMut(&WarmUpProgress) -> bool, + conversion_error: Option, + panic_payload: Option>, +} + +unsafe extern "C" fn warm_up_progress_trampoline( + event: *const ffi::WarmUpProgressEvent, + user_data: *mut c_void, +) -> c_int { + if event.is_null() || user_data.is_null() { + return 0; + } + // SAFETY: `Runtime::warm_up_f64` passes this stack-owned state and the C + // ABI invokes the callback synchronously on the coordinating thread only. + let state = unsafe { &mut *user_data.cast::>() }; + // SAFETY: The C ABI promises a borrowed event for this callback invocation. + let event = match WarmUpProgress::from_ffi(unsafe { &*event }) { + Ok(event) => event, + Err(error) => { + state.conversion_error = Some(error); + return 0; + } + }; + let terminal = + event.kind == WarmUpEventKind::End && event.stage == WarmUpStage::FirstEvaluation; + match catch_unwind(AssertUnwindSafe(|| (state.callback)(&event))) { + Ok(keep_going) => c_int::from(keep_going), + Err(_) if terminal => 1, + Err(payload) => { + state.panic_payload = Some(payload); + 0 + } + } +} + +/// Typed physics metadata used to interpret resolved output axes. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct PhysicsMetadata { + pub process: String, + pub process_key: String, + pub color_accuracy: String, + pub external_particles: Vec, + pub helicities: Vec, + pub colors: Vec, +} + +/// Physical component selectors for resolved evaluation. +/// +/// An empty axis selects every component on that axis. Selected output remains +/// in the artifact's canonical metadata order. +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub struct Selectors { + helicity_ids: Vec, + color_ids: Vec, +} + +impl Selectors { + pub fn all() -> Self { + Self::default() + } + + pub fn with_helicities(mut self, ids: I) -> Self + where + I: IntoIterator, + S: Into, + { + self.helicity_ids = ids.into_iter().map(Into::into).collect(); + self + } + + pub fn with_colors(mut self, ids: I) -> Self + where + I: IntoIterator, + S: Into, + { + self.color_ids = ids.into_iter().map(Into::into).collect(); + self + } + + pub fn helicity_ids(&self) -> &[String] { + &self.helicity_ids + } + + pub fn color_ids(&self) -> &[String] { + &self.color_ids + } +} + +/// One atomic model-parameter update in a batch. +#[derive(Clone, Debug, PartialEq)] +pub struct ParameterUpdate { + pub name: String, + pub real: f64, + pub imaginary: f64, +} + +impl ParameterUpdate { + pub fn new(name: impl Into, real: f64, imaginary: f64) -> Self { + Self { + name: name.into(), + real, + imaginary, + } + } +} + +/// Owned resolved f64 matrix elements with typed physical axes. +#[derive(Clone, Debug, PartialEq)] +pub struct ResolvedEvaluation { + values: Vec, + point_count: usize, + helicities: Vec, + colors: Vec, +} + +impl ResolvedEvaluation { + pub fn values(&self) -> &[f64] { + &self.values + } + + pub fn point_count(&self) -> usize { + self.point_count + } + + pub fn helicities(&self) -> &[HelicityConfiguration] { + &self.helicities + } + + pub fn colors(&self) -> &[ColorComponent] { + &self.colors + } + + pub fn shape(&self) -> (usize, usize, usize) { + (self.point_count, self.helicities.len(), self.colors.len()) + } + + pub fn get(&self, point: usize, helicity: usize, color: usize) -> Option { + if point >= self.point_count + || helicity >= self.helicities.len() + || color >= self.colors.len() + { + return None; + } + let offset = (point * self.helicities.len() + helicity) * self.colors.len() + color; + self.values.get(offset).copied() + } + + /// Explicitly sum every resolved component for each phase-space point. + pub fn totals(&self) -> Vec { + let stride = self.helicities.len() * self.colors.len(); + self.values + .chunks_exact(stride) + .map(|point| point.iter().sum()) + .collect() + } +} + +/// Owning Rusticol runtime handle. +/// +/// Methods requiring mutable runtime state take `&mut self`. The marker keeps +/// a handle on its creating thread because C ABI v1 does not promise that an +/// individual handle may be transferred between threads. +pub struct Runtime { + handle: NonNull, + _thread_bound: PhantomData>, +} + +impl Runtime { + /// Load an artifact, optionally selecting a process and applying a JSON + /// model-parameter card before the runtime becomes visible to the caller. + pub fn load( + artifact: impl AsRef, + process_key: Option<&str>, + model_parameters_path: Option<&Path>, + ) -> Result { + ensure_abi_version()?; + let artifact = path_cstring(artifact.as_ref(), "artifact path")?; + let process_key = optional_cstring(process_key, "process selector")?; + let model_parameters = model_parameters_path + .map(|path| path_cstring(path, "model-parameter path")) + .transpose()?; + let mut handle = ptr::null_mut(); + // SAFETY: CStrings remain live for the call and output points to writable storage. + check(unsafe { + ffi::runtime_load( + artifact.as_ptr(), + optional_pointer(&process_key), + optional_pointer(&model_parameters), + &mut handle, + ) + })?; + let handle = NonNull::new(handle).ok_or_else(|| { + Error::new( + ErrorKind::InvalidResponse, + "Rusticol returned a null runtime handle after a successful load", + ) + })?; + Ok(Self { + handle, + _thread_bound: PhantomData, + }) + } + + /// Save the completed, retained OTF recursion cache without warming new selections. + /// The cache file complements, rather than replaces, the generated process output. + pub fn save(&self, path: impl AsRef) -> Result<()> { + let path = path_cstring(path.as_ref(), "cache path")?; + // SAFETY: The owning handle and NUL-terminated path remain live for the call. + check(unsafe { ffi::runtime_save(self.handle.as_ptr(), path.as_ptr()) }) + } + + /// Restore a saved OTF cache into this already-loaded matching process. + pub fn load_cache(&mut self, path: impl AsRef) -> Result<()> { + let path = path_cstring(path.as_ref(), "cache path")?; + // SAFETY: The uniquely borrowed handle and path remain live for the call. + check(unsafe { ffi::runtime_load_cache(self.handle.as_ptr(), path.as_ptr()) }) + } + + pub fn process(&self) -> Result { + self.get_string(ffi::runtime_process) + } + + pub fn process_key(&self) -> Result { + self.get_string(ffi::runtime_process_key) + } + + pub fn representative_process_key(&self) -> Result { + self.get_string(ffi::runtime_representative_process_key) + } + + pub fn color_accuracy(&self) -> Result { + self.get_string(ffi::runtime_color_accuracy) + } + + pub fn metadata_json(&self) -> Result { + self.get_string(ffi::runtime_metadata_json) + } + + pub fn execution_mode(&self) -> Result { + self.get_string(ffi::runtime_execution_mode) + } + + pub fn physics_json(&self) -> Result { + self.get_string(ffi::runtime_physics_json) + } + + pub fn external_particles(&self) -> Result> { + let mut count = 0; + // SAFETY: The RAII handle is live and count is writable. + check(unsafe { ffi::runtime_external_count(self.handle.as_ptr(), &mut count) })?; + let mut particles = Vec::with_capacity(count); + for index in 0..count { + let mut pdg = 0; + // SAFETY: The RAII handle is live and pdg is writable. + check(unsafe { ffi::runtime_external_pdg(self.handle.as_ptr(), index, &mut pdg) })?; + particles.push(ExternalParticle { index, pdg }); + } + Ok(particles) + } + + /// Representative-index to public/requested-index external-leg permutation. + pub fn external_permutation(&self) -> Result> { + read_usize_vector(|output, capacity, required| { + // SAFETY: The helper owns a correctly sized output buffer for the live handle. + unsafe { + ffi::runtime_external_permutation(self.handle.as_ptr(), output, capacity, required) + } + }) + } + + /// Load one public-order JSON kinematic point as flattened f64 components. + pub fn load_kinematics_json(&self, path: impl AsRef) -> Result> { + let path = path_cstring(path.as_ref(), "kinematics path")?; + read_f64_vector(|output, capacity, required| { + // SAFETY: The path and output storage remain live for the call. + unsafe { + ffi::runtime_load_kinematics_json( + self.handle.as_ptr(), + path.as_ptr(), + output, + capacity, + required, + ) + } + }) + } + + pub fn helicities(&self) -> Result> { + let mut count = 0; + // SAFETY: The RAII handle is live and count is writable. + check(unsafe { ffi::runtime_helicity_count(self.handle.as_ptr(), &mut count) })?; + let mut result = Vec::with_capacity(count); + for index in 0..count { + let id = self.get_indexed_string(ffi::runtime_helicity_id, index)?; + let helicities = read_i32_vector(|output, capacity, required| { + // SAFETY: The helper owns a correctly sized output buffer for the live handle. + unsafe { + ffi::runtime_helicity_vector( + self.handle.as_ptr(), + index, + output, + capacity, + required, + ) + } + })?; + result.push(HelicityConfiguration { id, helicities }); + } + Ok(result) + } + + pub fn colors(&self) -> Result> { + let mut count = 0; + // SAFETY: The RAII handle is live and count is writable. + check(unsafe { ffi::runtime_color_count(self.handle.as_ptr(), &mut count) })?; + let mut result = Vec::with_capacity(count); + for index in 0..count { + let id = self.get_indexed_string(ffi::runtime_color_id, index)?; + let kind = self.get_indexed_string(ffi::runtime_color_kind, index)?; + let word = read_usize_vector(|output, capacity, required| { + // SAFETY: The helper owns a correctly sized output buffer for the live handle. + unsafe { + ffi::runtime_color_word(self.handle.as_ptr(), index, output, capacity, required) + } + })?; + result.push(ColorComponent { id, kind, word }); + } + Ok(result) + } + + pub fn model_parameters(&self) -> Result> { + let mut count = 0; + // SAFETY: The RAII handle is live and count is writable. + check(unsafe { ffi::runtime_model_parameter_count(self.handle.as_ptr(), &mut count) })?; + (0..count) + .map(|index| { + self.get_indexed_string(ffi::runtime_model_parameter_name, index) + .map(|name| ModelParameter { name }) + }) + .collect() + } + + pub fn physics(&self) -> Result { + Ok(PhysicsMetadata { + process: self.process()?, + process_key: self.process_key()?, + color_accuracy: self.color_accuracy()?, + external_particles: self.external_particles()?, + helicities: self.helicities()?, + colors: self.colors()?, + }) + } + + /// Evaluate compatibility totals for one or more flattened momentum points. + pub fn evaluate_f64(&mut self, momenta: &[f64], point_count: usize) -> Result> { + self.validate_momenta(momenta, point_count)?; + let mut values = vec![0.0; point_count]; + // SAFETY: Input and output slices are disjoint, sized, and live for the call. + check(unsafe { + ffi::runtime_evaluate_f64( + self.handle.as_ptr(), + momenta.as_ptr(), + momenta.len(), + point_count, + values.as_mut_ptr(), + values.len(), + ) + })?; + Ok(values) + } + + pub fn color_correlation_catalogue_json(&mut self) -> Result { + read_string(|buffer, capacity, required| { + // SAFETY: read_string owns the output buffer; this handle is exclusive. + unsafe { + ffi::rusticol_runtime_color_correlation_catalogue_json( + self.handle.as_ptr(), + buffer, + capacity, + required, + ) + } + }) + } + + pub fn color_correlation_ids(&mut self) -> Result> { + let mut count = 0; + // SAFETY: The output and exclusively borrowed handle are live. + check(unsafe { + ffi::rusticol_runtime_color_correlation_count(self.handle.as_ptr(), &mut count) + })?; + (0..count) + .map(|index| { + read_string(|buffer, capacity, required| { + // SAFETY: read_string owns the output buffer. + unsafe { + ffi::rusticol_runtime_color_correlation_id( + self.handle.as_ptr(), + index, + buffer, + capacity, + required, + ) + } + }) + }) + .collect() + } + + /// Replace defaults atomically; an empty slice clears them. Ordinary calls are unaffected. + pub fn set_spin_correlation_vectors( + &mut self, + vectors: &[SpinCorrelationVector], + ) -> Result<()> { + let storage = SpinCorrelationStorage::new(vectors); + // SAFETY: storage owns every component and descriptor until the call returns. + check(unsafe { + ffi::rusticol_runtime_set_spin_correlation_vectors_f64( + self.handle.as_ptr(), + storage.vectors.as_ptr(), + storage.vectors.len(), + ) + }) + } + + pub fn evaluate_correlated_many_f64( + &mut self, + momenta: &[f64], + point_count: usize, + requests: &[CorrelatedRequest], + helicity_ids: &[String], + ) -> Result { + self.validate_momenta(momenta, point_count)?; + if requests.is_empty() { + return Err(Error::new( + ErrorKind::InvalidInput, + "a correlated batch requires requests", + )); + } + let output_count = requests + .len() + .checked_mul(point_count) + .and_then(|n| n.checked_mul(2)) + .ok_or_else(|| { + Error::new( + ErrorKind::SizeOverflow, + "correlated output dimensions overflow", + ) + })?; + let ids = requests + .iter() + .map(|request| checked_cstring(&request.color_correlation, "colour correlation ID")) + .collect::>>()?; + let storage = requests + .iter() + .map(|request| { + SpinCorrelationStorage::new(request.spin_vectors.as_deref().unwrap_or(&[])) + }) + .collect::>(); + let raw_requests = requests + .iter() + .zip(&ids) + .zip(&storage) + .map(|((request, id), vectors)| ffi::CorrelatedRequest { + color_correlation: id.as_ptr(), + spin_vectors: vectors.vectors.as_ptr(), + spin_vector_count: vectors.vectors.len(), + use_default_spin_vectors: u32::from(request.spin_vectors.is_none()), + }) + .collect::>(); + let helicities = cstring_list(helicity_ids, "helicity ID")?; + let helicity_pointers = cstring_pointers(&helicities); + let mut raw = vec![0.0; output_count]; + // SAFETY: Owned buffers retain all nested pointers; output is disjoint and correctly sized. + check(unsafe { + ffi::rusticol_runtime_evaluate_correlated_many_f64( + self.handle.as_ptr(), + momenta.as_ptr(), + momenta.len(), + point_count, + raw_requests.as_ptr(), + raw_requests.len(), + helicity_pointers.as_ptr(), + helicity_pointers.len(), + raw.as_mut_ptr(), + raw.len(), + ) + })?; + Ok(CorrelatedEvaluation { + values: raw + .chunks_exact(2) + .map(|value| Complex64::new(value[0], value[1])) + .collect(), + request_count: requests.len(), + point_count, + }) + } + + pub fn evaluate_correlated_f64( + &mut self, + momenta: &[f64], + point_count: usize, + request: &CorrelatedRequest, + helicity_ids: &[String], + ) -> Result> { + self.evaluate_correlated_many_f64( + momenta, + point_count, + std::slice::from_ref(request), + helicity_ids, + ) + .map(|result| result.values) + } + + /// Construct and retain one selected OTF family, then evaluate exactly one + /// binary64 point. The optional callback receives throttled progress and + /// may return `false` to cancel at the next cold-path boundary. + /// `n_cores=None` uses the process-output construction default; `Some(n)` + /// requires `n > 0` and overrides construction workers for this call only. + /// Evaluation defaults and structural cache identity are unchanged. + pub fn warm_up( + &mut self, + point: &[f64], + selectors: &Selectors, + n_cores: Option, + progress: Option<&mut dyn FnMut(&WarmUpProgress) -> bool>, + ) -> Result { + self.warm_up_f64(point, selectors, n_cores, progress) + } + + /// Binary64-explicit alias for [`Runtime::warm_up`]. + /// + /// Callback panics are contained at the C boundary and resumed after the + /// native call returns. The terminal post-commit notification is + /// non-cancellable, so its callback result or panic is ignored. + pub fn warm_up_f64( + &mut self, + point: &[f64], + selectors: &Selectors, + n_cores: Option, + progress: Option<&mut dyn FnMut(&WarmUpProgress) -> bool>, + ) -> Result { + if n_cores == Some(0) { + return Err(Error::new( + ErrorKind::InvalidArgument, + "warm-up n_cores must be positive", + )); + } + let n_cores = n_cores.unwrap_or(0); + self.validate_momenta(point, 1)?; + let helicity_strings = cstring_list(&selectors.helicity_ids, "helicity selector")?; + let color_strings = cstring_list(&selectors.color_ids, "color selector")?; + let helicity_pointers = cstring_pointers(&helicity_strings); + let color_pointers = cstring_pointers(&color_strings); + let (helicity_pointer, helicity_count) = selector_parts(&helicity_pointers); + let (color_pointer, color_count) = selector_parts(&color_pointers); + let mut result = ffi::WarmUpResult::default(); + + let status = if let Some(callback) = progress { + let mut state = WarmUpCallbackState { + callback, + conversion_error: None, + panic_payload: None, + }; + // SAFETY: Every input and selector allocation remains live. The + // callback state is stack-owned for this synchronous call. + let status = unsafe { + ffi::runtime_warm_up_f64_with_cores( + self.handle.as_ptr(), + point.as_ptr(), + point.len(), + helicity_pointer, + helicity_count, + color_pointer, + color_count, + n_cores, + Some(warm_up_progress_trampoline), + (&mut state as *mut WarmUpCallbackState<'_>).cast(), + &mut result, + ) + }; + if let Some(payload) = state.panic_payload { + resume_unwind(payload); + } + if let Some(error) = state.conversion_error { + return Err(error); + } + status + } else { + // SAFETY: Every input and selector allocation remains live; both + // callback pointers are null together. + unsafe { + ffi::runtime_warm_up_f64_with_cores( + self.handle.as_ptr(), + point.as_ptr(), + point.len(), + helicity_pointer, + helicity_count, + color_pointer, + color_count, + n_cores, + None, + ptr::null_mut(), + &mut result, + ) + } + }; + check(status)?; + WarmUpResult::from_ffi(result) + } + + /// Evaluate one selected total per point. + /// + /// Global string selectors retain subset/sum semantics. Per-point selector + /// arrays contain zero-based physical-axis indices and must contain exactly + /// one entry per momentum point. The global and per-point forms are mutually + /// exclusive on the same axis. + pub fn evaluate_selected_f64( + &mut self, + momenta: &[f64], + point_count: usize, + selectors: &Selectors, + helicity_by_point: Option<&[u32]>, + color_flow_by_point: Option<&[u32]>, + ) -> Result> { + self.validate_momenta(momenta, point_count)?; + if !selectors.helicity_ids.is_empty() && helicity_by_point.is_some() { + return Err(Error::new( + ErrorKind::InvalidArgument, + "global helicity selectors and helicity_by_point are mutually exclusive", + )); + } + if !selectors.color_ids.is_empty() && color_flow_by_point.is_some() { + return Err(Error::new( + ErrorKind::InvalidArgument, + "global color selectors and color_flow_by_point are mutually exclusive", + )); + } + for (name, values) in [ + ("helicity_by_point", helicity_by_point), + ("color_flow_by_point", color_flow_by_point), + ] { + if let Some(values) = values { + if values.len() != point_count { + return Err(Error::new( + ErrorKind::InvalidArgument, + format!( + "{name} contains {} entries, expected {point_count}", + values.len() + ), + )); + } + } + } + + let helicity_strings = cstring_list(&selectors.helicity_ids, "helicity selector")?; + let color_strings = cstring_list(&selectors.color_ids, "color selector")?; + let helicity_pointers = cstring_pointers(&helicity_strings); + let color_pointers = cstring_pointers(&color_strings); + let (helicity_pointer, helicity_count) = selector_parts(&helicity_pointers); + let (color_pointer, color_count) = selector_parts(&color_pointers); + let (helicity_by_point_pointer, helicity_by_point_count) = + u32_selector_parts(helicity_by_point); + let (color_flow_by_point_pointer, color_flow_by_point_count) = + u32_selector_parts(color_flow_by_point); + let mut values = vec![0.0; point_count]; + // SAFETY: All selector and momentum buffers remain live and the output + // has exactly one writable entry per point. + check(unsafe { + ffi::runtime_evaluate_selected_f64( + self.handle.as_ptr(), + momenta.as_ptr(), + momenta.len(), + point_count, + helicity_pointer, + helicity_count, + color_pointer, + color_count, + helicity_by_point_pointer, + helicity_by_point_count, + color_flow_by_point_pointer, + color_flow_by_point_count, + values.as_mut_ptr(), + values.len(), + ) + })?; + Ok(values) + } + + /// Evaluate resolved f64 components for one or more momentum points. + pub fn evaluate_resolved_f64( + &mut self, + momenta: &[f64], + point_count: usize, + selectors: &Selectors, + ) -> Result { + self.validate_momenta(momenta, point_count)?; + let helicity_strings = cstring_list(&selectors.helicity_ids, "helicity selector")?; + let color_strings = cstring_list(&selectors.color_ids, "color selector")?; + let helicity_pointers = cstring_pointers(&helicity_strings); + let color_pointers = cstring_pointers(&color_strings); + let (helicity_pointer, helicity_count) = selector_parts(&helicity_pointers); + let (color_pointer, color_count) = selector_parts(&color_pointers); + + let mut resolved_helicity_count = 0; + let mut resolved_color_count = 0; + // SAFETY: Selector CStrings and output dimensions remain live for the call. + check(unsafe { + ffi::runtime_resolved_shape( + self.handle.as_ptr(), + helicity_pointer, + helicity_count, + color_pointer, + color_count, + &mut resolved_helicity_count, + &mut resolved_color_count, + ) + })?; + if resolved_helicity_count == 0 || resolved_color_count == 0 { + return Err(Error::new( + ErrorKind::InvalidResponse, + "Rusticol returned an empty resolved axis", + )); + } + let value_count = point_count + .checked_mul(resolved_helicity_count) + .and_then(|count| count.checked_mul(resolved_color_count)) + .ok_or_else(|| Error::new(ErrorKind::SizeOverflow, "resolved output shape overflow"))?; + let mut values = vec![0.0; value_count]; + let mut returned_helicity_count = resolved_helicity_count; + let mut returned_color_count = resolved_color_count; + // SAFETY: Every pointer refers to a live, disjoint, correctly sized allocation. + check(unsafe { + ffi::runtime_evaluate_resolved_f64( + self.handle.as_ptr(), + momenta.as_ptr(), + momenta.len(), + point_count, + helicity_pointer, + helicity_count, + color_pointer, + color_count, + values.as_mut_ptr(), + values.len(), + &mut returned_helicity_count, + &mut returned_color_count, + ) + })?; + if returned_helicity_count != resolved_helicity_count + || returned_color_count != resolved_color_count + { + return Err(Error::new( + ErrorKind::InvalidResponse, + "Rusticol changed the resolved shape during evaluation", + )); + } + + let metadata = self.physics()?; + let helicities = select_helicities(metadata.helicities, &selectors.helicity_ids); + let colors = select_colors(metadata.colors, &selectors.color_ids); + if helicities.len() != resolved_helicity_count || colors.len() != resolved_color_count { + return Err(Error::new( + ErrorKind::InvalidResponse, + "Rusticol resolved output does not match its physics metadata", + )); + } + Ok(ResolvedEvaluation { + values, + point_count, + helicities, + colors, + }) + } + + pub fn set_model_parameter(&mut self, name: &str, real: f64, imaginary: f64) -> Result<()> { + let name = checked_cstring(name, "model-parameter name")?; + // SAFETY: The name CString and owning handle remain live for the call. + check(unsafe { + ffi::runtime_set_model_parameter(self.handle.as_ptr(), name.as_ptr(), real, imaginary) + }) + } + + /// Apply a batch as one atomic update. A failing entry leaves all runtime + /// model parameters unchanged. + pub fn set_model_parameters(&mut self, updates: &[ParameterUpdate]) -> Result<()> { + if updates.is_empty() { + return Ok(()); + } + let names = updates + .iter() + .map(|update| checked_cstring(&update.name, "model-parameter name")) + .collect::>>()?; + let name_pointers = cstring_pointers(&names); + let real = updates.iter().map(|update| update.real).collect::>(); + let imaginary = updates + .iter() + .map(|update| update.imaginary) + .collect::>(); + // SAFETY: All arrays have identical nonzero lengths and remain live for the call. + check(unsafe { + ffi::runtime_set_model_parameters( + self.handle.as_ptr(), + name_pointers.as_ptr(), + real.as_ptr(), + imaginary.as_ptr(), + updates.len(), + ) + }) + } + + pub fn set_model_parameters_json(&mut self, path: impl AsRef) -> Result<()> { + let path = path_cstring(path.as_ref(), "model-parameter path")?; + // SAFETY: The path CString and owning handle remain live for the call. + check(unsafe { + ffi::runtime_set_model_parameters_json(self.handle.as_ptr(), path.as_ptr()) + }) + } + + pub fn set_warnings_muted(&mut self, muted: bool) -> Result<()> { + // SAFETY: The owning handle remains live and exclusively borrowed. + check(unsafe { ffi::runtime_mute_warnings(self.handle.as_ptr(), c_int::from(muted)) }) + } + + pub fn mute_warnings(&mut self) -> Result<()> { + self.set_warnings_muted(true) + } + + pub fn unmute_warnings(&mut self) -> Result<()> { + self.set_warnings_muted(false) + } + + /// Return pending warnings as a JSON array and consume them after copying. + pub fn take_warnings_json(&mut self) -> Result { + read_string(|buffer, capacity, required| { + // SAFETY: The helper owns the output buffer and the handle is exclusively borrowed. + unsafe { + ffi::runtime_take_warnings_json(self.handle.as_ptr(), buffer, capacity, required) + } + }) + } + + fn validate_momenta(&self, momenta: &[f64], point_count: usize) -> Result<()> { + if point_count == 0 { + return Err(Error::new( + ErrorKind::InvalidInput, + "point_count must be positive", + )); + } + let external_count = self.external_particles()?.len(); + let expected = point_count + .checked_mul(external_count) + .and_then(|count| count.checked_mul(4)) + .ok_or_else(|| Error::new(ErrorKind::SizeOverflow, "momentum shape overflow"))?; + if momenta.len() != expected { + return Err(Error::new( + ErrorKind::InvalidInput, + format!( + "momenta contain {} values, expected {expected} for {point_count} point(s)", + momenta.len() + ), + )); + } + if momenta.iter().any(|value| !value.is_finite()) { + return Err(Error::new( + ErrorKind::InvalidInput, + "momenta must contain only finite f64 values", + )); + } + Ok(()) + } + + fn get_string(&self, getter: ffi::StringGetter) -> Result { + read_string(|buffer, capacity, required| { + // SAFETY: The helper owns the output buffer and the RAII handle is live. + unsafe { getter(self.handle.as_ptr(), buffer, capacity, required) } + }) + } + + fn get_indexed_string(&self, getter: ffi::IndexedStringGetter, index: usize) -> Result { + read_string(|buffer, capacity, required| { + // SAFETY: The helper owns the output buffer and the RAII handle is live. + unsafe { getter(self.handle.as_ptr(), index, buffer, capacity, required) } + }) + } +} + +impl Drop for Runtime { + fn drop(&mut self) { + // SAFETY: Runtime uniquely owns this live handle and frees it exactly once. + let _ = unsafe { ffi::runtime_free(self.handle.as_ptr()) }; + } +} + +pub fn abi_version() -> u32 { + // SAFETY: This function has no pointer arguments or mutable state contract. + unsafe { ffi::abi_version() } +} + +pub fn ensure_abi_version() -> Result<()> { + let observed = abi_version(); + if observed == ABI_VERSION { + Ok(()) + } else { + Err(Error::new( + ErrorKind::AbiMismatch, + format!( + "Rusticol ABI version {observed} does not match required version {ABI_VERSION}" + ), + )) + } +} + +pub fn supported_runtime_capabilities_json() -> Result { + read_string(|buffer, capacity, required| { + // SAFETY: The helper owns the correctly sized output buffer. + unsafe { ffi::supported_runtime_capabilities_json(buffer, capacity, required) } + }) +} + +fn check(status: c_int) -> Result<()> { + if status == ffi::STATUS_OK { + Ok(()) + } else { + Err(Error::from_status(status)) + } +} + +fn last_error_message(original_status: c_int) -> String { + let mut required = 0; + // SAFETY: A null zero-capacity buffer is the documented size query. + let query_status = unsafe { ffi::last_error_message(ptr::null_mut(), 0, &mut required) }; + if query_status != ffi::STATUS_OK || required == 0 { + return format!("Rusticol C ABI call failed with status {original_status}"); + } + let mut buffer = vec![0_u8; required]; + // SAFETY: The vector provides required writable bytes and remains live for the call. + let copy_status = + unsafe { ffi::last_error_message(buffer.as_mut_ptr().cast(), buffer.len(), &mut required) }; + if copy_status != ffi::STATUS_OK { + return format!("Rusticol C ABI call failed with status {original_status}"); + } + CStr::from_bytes_until_nul(&buffer) + .ok() + .and_then(|value| value.to_str().ok()) + .map(str::to_owned) + .unwrap_or_else(|| format!("Rusticol C ABI call failed with status {original_status}")) +} + +fn read_string(mut getter: impl FnMut(*mut c_char, usize, *mut usize) -> c_int) -> Result { + let mut required = 0; + check(getter(ptr::null_mut(), 0, &mut required))?; + if required == 0 { + return Err(Error::new( + ErrorKind::InvalidResponse, + "Rusticol returned a zero-length string buffer", + )); + } + let mut buffer = vec![0_u8; required]; + let mut copied_required = required; + check(getter( + buffer.as_mut_ptr().cast(), + buffer.len(), + &mut copied_required, + ))?; + if copied_required == 0 || copied_required > buffer.len() { + return Err(Error::new( + ErrorKind::InvalidResponse, + "Rusticol returned an inconsistent string size", + )); + } + let value = CStr::from_bytes_until_nul(&buffer).map_err(|_| { + Error::new( + ErrorKind::InvalidResponse, + "Rusticol returned a string without a trailing NUL", + ) + })?; + value.to_str().map(str::to_owned).map_err(|_| { + Error::new( + ErrorKind::InvalidUtf8, + "Rusticol returned a string that is not valid UTF-8", + ) + }) +} + +fn read_i32_vector( + mut getter: impl FnMut(*mut i32, usize, *mut usize) -> c_int, +) -> Result> { + let mut required = 0; + check(getter(ptr::null_mut(), 0, &mut required))?; + if required == 0 { + return Ok(Vec::new()); + } + let mut values = vec![0_i32; required]; + let mut copied_required = required; + check(getter( + values.as_mut_ptr(), + values.len(), + &mut copied_required, + ))?; + if copied_required != values.len() { + return Err(Error::new( + ErrorKind::InvalidResponse, + "Rusticol returned an inconsistent i32 vector size", + )); + } + Ok(values) +} + +fn read_usize_vector( + mut getter: impl FnMut(*mut usize, usize, *mut usize) -> c_int, +) -> Result> { + let mut required = 0; + check(getter(ptr::null_mut(), 0, &mut required))?; + if required == 0 { + return Ok(Vec::new()); + } + let mut values = vec![0_usize; required]; + let mut copied_required = required; + check(getter( + values.as_mut_ptr(), + values.len(), + &mut copied_required, + ))?; + if copied_required != values.len() { + return Err(Error::new( + ErrorKind::InvalidResponse, + "Rusticol returned an inconsistent usize vector size", + )); + } + Ok(values) +} + +fn read_f64_vector( + mut getter: impl FnMut(*mut f64, usize, *mut usize) -> c_int, +) -> Result> { + let mut required = 0; + check(getter(ptr::null_mut(), 0, &mut required))?; + if required == 0 { + return Ok(Vec::new()); + } + let mut values = vec![0.0_f64; required]; + let mut copied_required = required; + check(getter( + values.as_mut_ptr(), + values.len(), + &mut copied_required, + ))?; + if copied_required != values.len() { + return Err(Error::new( + ErrorKind::InvalidResponse, + "Rusticol returned an inconsistent f64 vector size", + )); + } + Ok(values) +} + +fn checked_cstring(value: &str, description: &str) -> Result { + CString::new(value).map_err(|_| { + Error::new( + ErrorKind::InvalidInput, + format!("{description} contains a NUL byte"), + ) + }) +} + +fn path_cstring(path: &Path, description: &str) -> Result { + let value = path.to_str().ok_or_else(|| { + Error::new( + ErrorKind::InvalidUtf8, + format!("{description} is not valid UTF-8: {}", path.display()), + ) + })?; + checked_cstring(value, description) +} + +fn optional_cstring(value: Option<&str>, description: &str) -> Result> { + value + .map(|item| checked_cstring(item, description)) + .transpose() +} + +fn optional_pointer(value: &Option) -> *const c_char { + value.as_ref().map_or(ptr::null(), |item| item.as_ptr()) +} + +fn cstring_list(values: &[String], description: &str) -> Result> { + values + .iter() + .map(|value| checked_cstring(value, description)) + .collect() +} + +fn cstring_pointers(values: &[CString]) -> Vec<*const c_char> { + values.iter().map(|value| value.as_ptr()).collect() +} + +fn selector_parts(values: &[*const c_char]) -> (*const *const c_char, usize) { + if values.is_empty() { + (ptr::null(), 0) + } else { + (values.as_ptr(), values.len()) + } +} + +fn u32_selector_parts(values: Option<&[u32]>) -> (*const u32, usize) { + match values { + Some(values) if !values.is_empty() => (values.as_ptr(), values.len()), + _ => (ptr::null(), 0), + } +} + +fn select_helicities( + available: Vec, + selected: &[String], +) -> Vec { + if selected.is_empty() { + available + } else { + available + .into_iter() + .filter(|item| selected.iter().any(|id| id == &item.id)) + .collect() + } +} + +fn select_colors(available: Vec, selected: &[String]) -> Vec { + if selected.is_empty() { + available + } else { + available + .into_iter() + .filter(|item| selected.iter().any(|id| id == &item.id)) + .collect() + } +} + +struct SpinCorrelationStorage { + // Buffers own the memory referenced by vectors even after moving this struct. + _components: Vec>, + vectors: Vec, +} + +impl SpinCorrelationStorage { + fn new(input: &[SpinCorrelationVector]) -> Self { + let components = input + .iter() + .map(|vector| { + vector + .components + .iter() + .flatten() + .flat_map(|value| [value.re, value.im]) + .collect::>() + }) + .collect::>(); + let vectors = input + .iter() + .zip(&components) + .map(|(vector, flat)| ffi::SpinCorrelationVector { + leg: vector.leg, + point_count: vector.components.len(), + components: flat.as_ptr(), + component_count: flat.len(), + }) + .collect(); + Self { + _components: components, + vectors, + } + } +} + +mod ffi { + use super::{c_char, c_int, c_void}; + + pub(super) const STATUS_OK: c_int = 0; + pub(super) const STATUS_INVALID_ARGUMENT: c_int = 1; + pub(super) const STATUS_BUFFER_TOO_SMALL: c_int = 2; + pub(super) const STATUS_RUNTIME_ERROR: c_int = 3; + pub(super) const STATUS_PANIC: c_int = 4; + + pub(super) const WARM_UP_EVENT_START: u32 = 0; + pub(super) const WARM_UP_EVENT_UPDATE: u32 = 1; + pub(super) const WARM_UP_EVENT_END: u32 = 2; + pub(super) const WARM_UP_STAGE_PROCESS_PREPARATION: u32 = 0; + pub(super) const WARM_UP_STAGE_QUERY_FAMILY: u32 = 1; + pub(super) const WARM_UP_STAGE_FAMILY_FINALIZATION: u32 = 2; + pub(super) const WARM_UP_STAGE_FIRST_EVALUATION: u32 = 3; + + #[repr(C)] + pub(super) struct RuntimeHandle { + _private: [u8; 0], + } + + #[repr(C)] + pub(super) struct SpinCorrelationVector { + pub(super) leg: usize, + pub(super) point_count: usize, + pub(super) components: *const f64, + pub(super) component_count: usize, + } + + #[repr(C)] + pub(super) struct CorrelatedRequest { + pub(super) color_correlation: *const c_char, + pub(super) spin_vectors: *const SpinCorrelationVector, + pub(super) spin_vector_count: usize, + pub(super) use_default_spin_vectors: u32, + } + + #[repr(C)] + pub(super) struct WarmUpProgressEvent { + pub(super) schema_version: u32, + pub(super) kind: u32, + pub(super) stage: u32, + pub(super) reserved: u32, + pub(super) completed: u64, + pub(super) total: u64, + pub(super) elapsed_seconds: f64, + pub(super) current_rss_bytes: u64, + pub(super) peak_rss_bytes: u64, + pub(super) workers: u64, + pub(super) current_rss_available: u32, + pub(super) peak_rss_available: u32, + } + + #[repr(C)] + #[derive(Default)] + pub(super) struct WarmUpResult { + pub(super) schema_version: u32, + pub(super) reserved: u32, + pub(super) elapsed_seconds: f64, + pub(super) query_count: u64, + pub(super) warmed_query_count: u64, + pub(super) current_rss_bytes: u64, + pub(super) peak_rss_bytes: u64, + pub(super) current_rss_available: u32, + pub(super) peak_rss_available: u32, + pub(super) already_warm: u32, + pub(super) first_evaluation_completed: u32, + } + + pub(super) type WarmUpProgressCallback = + Option c_int>; + + pub(super) type StringGetter = + unsafe extern "C" fn(*const RuntimeHandle, *mut c_char, usize, *mut usize) -> c_int; + pub(super) type IndexedStringGetter = + unsafe extern "C" fn(*const RuntimeHandle, usize, *mut c_char, usize, *mut usize) -> c_int; + + unsafe extern "C" { + pub(super) fn rusticol_runtime_color_correlation_count( + handle: *mut RuntimeHandle, + output: *mut usize, + ) -> c_int; + pub(super) fn rusticol_runtime_color_correlation_id( + handle: *mut RuntimeHandle, + index: usize, + buffer: *mut c_char, + capacity: usize, + required: *mut usize, + ) -> c_int; + pub(super) fn rusticol_runtime_color_correlation_catalogue_json( + handle: *mut RuntimeHandle, + buffer: *mut c_char, + capacity: usize, + required: *mut usize, + ) -> c_int; + pub(super) fn rusticol_runtime_set_spin_correlation_vectors_f64( + handle: *mut RuntimeHandle, + vectors: *const SpinCorrelationVector, + count: usize, + ) -> c_int; + pub(super) fn rusticol_runtime_evaluate_correlated_many_f64( + handle: *mut RuntimeHandle, + momenta: *const f64, + momentum_count: usize, + point_count: usize, + requests: *const CorrelatedRequest, + request_count: usize, + helicity_ids: *const *const c_char, + helicity_count: usize, + output: *mut f64, + output_capacity: usize, + ) -> c_int; + pub(super) fn rusticol_abi_version() -> u32; + pub(super) fn rusticol_supported_runtime_capabilities_json( + buffer: *mut c_char, + capacity: usize, + required: *mut usize, + ) -> c_int; + pub(super) fn rusticol_last_error_message( + buffer: *mut c_char, + capacity: usize, + required: *mut usize, + ) -> c_int; + pub(super) fn rusticol_runtime_load( + process_dir: *const c_char, + process_key: *const c_char, + model_parameters_path: *const c_char, + output: *mut *mut RuntimeHandle, + ) -> c_int; + pub(super) fn rusticol_runtime_free(handle: *mut RuntimeHandle) -> c_int; + pub(super) fn rusticol_runtime_save( + handle: *const RuntimeHandle, + path: *const c_char, + ) -> c_int; + pub(super) fn rusticol_runtime_load_cache( + handle: *mut RuntimeHandle, + path: *const c_char, + ) -> c_int; + pub(super) fn rusticol_runtime_metadata_json( + handle: *const RuntimeHandle, + buffer: *mut c_char, + capacity: usize, + required: *mut usize, + ) -> c_int; + pub(super) fn rusticol_runtime_execution_mode( + handle: *const RuntimeHandle, + buffer: *mut c_char, + capacity: usize, + required: *mut usize, + ) -> c_int; + pub(super) fn rusticol_runtime_physics_json( + handle: *const RuntimeHandle, + buffer: *mut c_char, + capacity: usize, + required: *mut usize, + ) -> c_int; + pub(super) fn rusticol_runtime_process( + handle: *const RuntimeHandle, + buffer: *mut c_char, + capacity: usize, + required: *mut usize, + ) -> c_int; + pub(super) fn rusticol_runtime_process_key( + handle: *const RuntimeHandle, + buffer: *mut c_char, + capacity: usize, + required: *mut usize, + ) -> c_int; + pub(super) fn rusticol_runtime_representative_process_key( + handle: *const RuntimeHandle, + buffer: *mut c_char, + capacity: usize, + required: *mut usize, + ) -> c_int; + pub(super) fn rusticol_runtime_color_accuracy( + handle: *const RuntimeHandle, + buffer: *mut c_char, + capacity: usize, + required: *mut usize, + ) -> c_int; + pub(super) fn rusticol_runtime_external_count( + handle: *const RuntimeHandle, + output: *mut usize, + ) -> c_int; + pub(super) fn rusticol_runtime_external_pdg( + handle: *const RuntimeHandle, + index: usize, + output: *mut i32, + ) -> c_int; + pub(super) fn rusticol_runtime_external_permutation( + handle: *const RuntimeHandle, + output: *mut usize, + capacity: usize, + required: *mut usize, + ) -> c_int; + pub(super) fn rusticol_runtime_load_kinematics_json( + handle: *const RuntimeHandle, + path: *const c_char, + output: *mut f64, + capacity: usize, + required: *mut usize, + ) -> c_int; + pub(super) fn rusticol_runtime_helicity_count( + handle: *const RuntimeHandle, + output: *mut usize, + ) -> c_int; + pub(super) fn rusticol_runtime_helicity_id( + handle: *const RuntimeHandle, + index: usize, + buffer: *mut c_char, + capacity: usize, + required: *mut usize, + ) -> c_int; + pub(super) fn rusticol_runtime_helicity_vector( + handle: *const RuntimeHandle, + index: usize, + output: *mut i32, + capacity: usize, + required: *mut usize, + ) -> c_int; + pub(super) fn rusticol_runtime_color_count( + handle: *const RuntimeHandle, + output: *mut usize, + ) -> c_int; + pub(super) fn rusticol_runtime_color_id( + handle: *const RuntimeHandle, + index: usize, + buffer: *mut c_char, + capacity: usize, + required: *mut usize, + ) -> c_int; + pub(super) fn rusticol_runtime_color_kind( + handle: *const RuntimeHandle, + index: usize, + buffer: *mut c_char, + capacity: usize, + required: *mut usize, + ) -> c_int; + pub(super) fn rusticol_runtime_color_word( + handle: *const RuntimeHandle, + index: usize, + output: *mut usize, + capacity: usize, + required: *mut usize, + ) -> c_int; + pub(super) fn rusticol_runtime_model_parameter_count( + handle: *const RuntimeHandle, + output: *mut usize, + ) -> c_int; + pub(super) fn rusticol_runtime_model_parameter_name( + handle: *const RuntimeHandle, + index: usize, + buffer: *mut c_char, + capacity: usize, + required: *mut usize, + ) -> c_int; + pub(super) fn rusticol_runtime_resolved_shape( + handle: *const RuntimeHandle, + helicity_ids: *const *const c_char, + helicity_count: usize, + color_ids: *const *const c_char, + color_count: usize, + output_helicity_count: *mut usize, + output_color_count: *mut usize, + ) -> c_int; + pub(super) fn rusticol_runtime_warm_up_f64_with_cores( + handle: *mut RuntimeHandle, + momenta: *const f64, + momentum_count: usize, + helicity_ids: *const *const c_char, + helicity_count: usize, + color_ids: *const *const c_char, + color_count: usize, + n_cores: usize, + progress_callback: WarmUpProgressCallback, + progress_user_data: *mut c_void, + output: *mut WarmUpResult, + ) -> c_int; + pub(super) fn rusticol_runtime_evaluate_f64( + handle: *mut RuntimeHandle, + momenta: *const f64, + momentum_count: usize, + point_count: usize, + output: *mut f64, + output_capacity: usize, + ) -> c_int; + pub(super) fn rusticol_runtime_evaluate_selected_f64( + handle: *mut RuntimeHandle, + momenta: *const f64, + momentum_count: usize, + point_count: usize, + helicity_ids: *const *const c_char, + helicity_count: usize, + color_ids: *const *const c_char, + color_count: usize, + helicity_by_point: *const u32, + helicity_by_point_count: usize, + color_flow_by_point: *const u32, + color_flow_by_point_count: usize, + output: *mut f64, + output_capacity: usize, + ) -> c_int; + pub(super) fn rusticol_runtime_evaluate_resolved_f64( + handle: *mut RuntimeHandle, + momenta: *const f64, + momentum_count: usize, + point_count: usize, + helicity_ids: *const *const c_char, + helicity_count: usize, + color_ids: *const *const c_char, + color_count: usize, + output: *mut f64, + output_capacity: usize, + output_helicity_count: *mut usize, + output_color_count: *mut usize, + ) -> c_int; + pub(super) fn rusticol_runtime_set_model_parameters( + handle: *mut RuntimeHandle, + names: *const *const c_char, + real: *const f64, + imaginary: *const f64, + count: usize, + ) -> c_int; + pub(super) fn rusticol_runtime_set_model_parameter( + handle: *mut RuntimeHandle, + name: *const c_char, + real: f64, + imaginary: f64, + ) -> c_int; + pub(super) fn rusticol_runtime_set_model_parameters_json( + handle: *mut RuntimeHandle, + path: *const c_char, + ) -> c_int; + pub(super) fn rusticol_runtime_mute_warnings( + handle: *mut RuntimeHandle, + muted: c_int, + ) -> c_int; + pub(super) fn rusticol_runtime_take_warnings_json( + handle: *mut RuntimeHandle, + buffer: *mut c_char, + capacity: usize, + required: *mut usize, + ) -> c_int; + } + + pub(super) use rusticol_abi_version as abi_version; + pub(super) use rusticol_last_error_message as last_error_message; + pub(super) use rusticol_runtime_color_accuracy as runtime_color_accuracy; + pub(super) use rusticol_runtime_color_count as runtime_color_count; + pub(super) use rusticol_runtime_color_id as runtime_color_id; + pub(super) use rusticol_runtime_color_kind as runtime_color_kind; + pub(super) use rusticol_runtime_color_word as runtime_color_word; + pub(super) use rusticol_runtime_evaluate_f64 as runtime_evaluate_f64; + pub(super) use rusticol_runtime_evaluate_resolved_f64 as runtime_evaluate_resolved_f64; + pub(super) use rusticol_runtime_evaluate_selected_f64 as runtime_evaluate_selected_f64; + pub(super) use rusticol_runtime_execution_mode as runtime_execution_mode; + pub(super) use rusticol_runtime_external_count as runtime_external_count; + pub(super) use rusticol_runtime_external_pdg as runtime_external_pdg; + pub(super) use rusticol_runtime_external_permutation as runtime_external_permutation; + pub(super) use rusticol_runtime_free as runtime_free; + pub(super) use rusticol_runtime_helicity_count as runtime_helicity_count; + pub(super) use rusticol_runtime_helicity_id as runtime_helicity_id; + pub(super) use rusticol_runtime_helicity_vector as runtime_helicity_vector; + pub(super) use rusticol_runtime_load as runtime_load; + pub(super) use rusticol_runtime_load_cache as runtime_load_cache; + pub(super) use rusticol_runtime_load_kinematics_json as runtime_load_kinematics_json; + pub(super) use rusticol_runtime_metadata_json as runtime_metadata_json; + pub(super) use rusticol_runtime_model_parameter_count as runtime_model_parameter_count; + pub(super) use rusticol_runtime_model_parameter_name as runtime_model_parameter_name; + pub(super) use rusticol_runtime_mute_warnings as runtime_mute_warnings; + pub(super) use rusticol_runtime_physics_json as runtime_physics_json; + pub(super) use rusticol_runtime_process as runtime_process; + pub(super) use rusticol_runtime_process_key as runtime_process_key; + pub(super) use rusticol_runtime_representative_process_key as runtime_representative_process_key; + pub(super) use rusticol_runtime_resolved_shape as runtime_resolved_shape; + pub(super) use rusticol_runtime_save as runtime_save; + pub(super) use rusticol_runtime_set_model_parameter as runtime_set_model_parameter; + pub(super) use rusticol_runtime_set_model_parameters as runtime_set_model_parameters; + pub(super) use rusticol_runtime_set_model_parameters_json as runtime_set_model_parameters_json; + pub(super) use rusticol_runtime_take_warnings_json as runtime_take_warnings_json; + pub(super) use rusticol_runtime_warm_up_f64_with_cores as runtime_warm_up_f64_with_cores; + pub(super) use rusticol_supported_runtime_capabilities_json as supported_runtime_capabilities_json; +} diff --git a/pyamplicol/src/pyamplicol/api/__init__.py b/pyamplicol/src/pyamplicol/api/__init__.py new file mode 100644 index 0000000000..898b51ff5d --- /dev/null +++ b/pyamplicol/src/pyamplicol/api/__init__.py @@ -0,0 +1,222 @@ +# SPDX-License-Identifier: 0BSD +from __future__ import annotations + +from importlib import import_module +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from .errors import ( + ArtifactError, + CompatibilityError, + ConfigurationError, + DependencyError, + EvaluationError, + GenerationError, + ModelError, + PyAmpliColError, + ) + from .models import ( + CompiledModel, + CompiledModelCapabilities, + CompiledModelInfo, + CompiledModelSource, + ModelCompilationIssue, + ModelCompilationPhase, + ) + from .process_groups import group_processes + from .protocols import ( + BenchmarkBackend, + BenchmarkFactory, + GeneratorBackend, + GeneratorFactory, + ModelParameters, + Momenta, + RuntimeBackend, + RuntimeLoader, + ) + from .requests import ( + ModelSource, + ModelSourceKind, + ProcessAlias, + ProcessRequest, + ProcessSet, + ) + from .results import ( + BenchmarkComponentTiming, + BenchmarkProfileCounters, + BenchmarkResult, + BenchmarkStageTiming, + BenchmarkStatistics, + BenchmarkTimingBreakdown, + ColorComponent, + ColorFlow, + ContractedColorComponent, + ExternalParticle, + GenerationPlan, + GenerationResult, + HelicityConfiguration, + ModelParameter, + PhysicsReduction, + ProcessPhysics, + ReductionGroup, + ResolvedEvaluation, + WarmUpResult, + ) + from .services import ( + BenchmarkRunner, + Generator, + Runtime, + benchmark, + generate, + install_backend_factories, + load, + ) + +__all__ = [ + "ArtifactError", + "BenchmarkBackend", + "BenchmarkComponentTiming", + "BenchmarkFactory", + "BenchmarkProfileCounters", + "BenchmarkResult", + "BenchmarkRunner", + "BenchmarkStageTiming", + "BenchmarkStatistics", + "BenchmarkTimingBreakdown", + "ColorComponent", + "ColorFlow", + "CompatibilityError", + "CompiledModel", + "CompiledModelCapabilities", + "CompiledModelInfo", + "CompiledModelSource", + "ConfigurationError", + "ContractedColorComponent", + "DependencyError", + "EvaluationError", + "ExternalParticle", + "GenerationError", + "GenerationPlan", + "GenerationResult", + "Generator", + "GeneratorBackend", + "GeneratorFactory", + "HelicityConfiguration", + "ModelCompilationIssue", + "ModelCompilationPhase", + "ModelError", + "ModelParameter", + "ModelParameters", + "ModelSource", + "ModelSourceKind", + "Momenta", + "PhysicsReduction", + "ProcessAlias", + "ProcessPhysics", + "ProcessRequest", + "ProcessSet", + "PyAmpliColError", + "ReductionGroup", + "ResolvedEvaluation", + "Runtime", + "RuntimeBackend", + "RuntimeLoader", + "WarmUpResult", + "benchmark", + "generate", + "group_processes", + "install_backend_factories", + "load", +] + +_ERROR_EXPORTS = ( + "ArtifactError", + "CompatibilityError", + "ConfigurationError", + "DependencyError", + "EvaluationError", + "GenerationError", + "ModelError", + "PyAmpliColError", +) +_PROTOCOL_EXPORTS = ( + "BenchmarkBackend", + "BenchmarkFactory", + "GeneratorBackend", + "GeneratorFactory", + "ModelParameters", + "Momenta", + "RuntimeBackend", + "RuntimeLoader", +) +_REQUEST_EXPORTS = ( + "ModelSource", + "ModelSourceKind", + "ProcessAlias", + "ProcessRequest", + "ProcessSet", +) +_MODEL_EXPORTS = ( + "CompiledModel", + "CompiledModelCapabilities", + "CompiledModelInfo", + "CompiledModelSource", + "ModelCompilationIssue", + "ModelCompilationPhase", +) +_RESULT_EXPORTS = ( + "BenchmarkComponentTiming", + "BenchmarkProfileCounters", + "BenchmarkResult", + "BenchmarkStageTiming", + "BenchmarkStatistics", + "BenchmarkTimingBreakdown", + "ColorComponent", + "ColorFlow", + "ContractedColorComponent", + "ExternalParticle", + "GenerationPlan", + "GenerationResult", + "HelicityConfiguration", + "ModelParameter", + "PhysicsReduction", + "ProcessPhysics", + "ReductionGroup", + "ResolvedEvaluation", + "WarmUpResult", +) +_SERVICE_EXPORTS = ( + "BenchmarkRunner", + "Generator", + "Runtime", + "benchmark", + "generate", + "install_backend_factories", + "load", +) +_PUBLIC_EXPORTS = { + **{name: (".errors", name) for name in _ERROR_EXPORTS}, + **{name: (".protocols", name) for name in _PROTOCOL_EXPORTS}, + **{name: (".models", name) for name in _MODEL_EXPORTS}, + **{name: (".requests", name) for name in _REQUEST_EXPORTS}, + **{name: (".results", name) for name in _RESULT_EXPORTS}, + **{name: (".services", name) for name in _SERVICE_EXPORTS}, + "group_processes": (".process_groups", "group_processes"), +} + + +def __getattr__(name: str) -> Any: + """Load a public object only when that object is requested.""" + + export = _PUBLIC_EXPORTS.get(name) + if export is None: + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + module_name, attribute = export + package = __name__ if module_name.startswith(".") else None + value = getattr(import_module(module_name, package), attribute) + globals()[name] = value + return value + + +def __dir__() -> list[str]: + return sorted(set(globals()).union(__all__)) diff --git a/pyamplicol/src/pyamplicol/api/errors.py b/pyamplicol/src/pyamplicol/api/errors.py new file mode 100644 index 0000000000..fe120bc965 --- /dev/null +++ b/pyamplicol/src/pyamplicol/api/errors.py @@ -0,0 +1,40 @@ +# SPDX-License-Identifier: 0BSD +from __future__ import annotations + +from pyamplicol.config.errors import ConfigurationError, PyAmpliColError + + +class ModelError(PyAmpliColError): + """A model source or compiled model is invalid.""" + + +class GenerationError(PyAmpliColError): + """Planning or artifact generation failed.""" + + +class ArtifactError(PyAmpliColError): + """An artifact is malformed, incomplete, or unavailable.""" + + +class CompatibilityError(ArtifactError): + """An artifact is incompatible with the active target or ABI.""" + + +class EvaluationError(PyAmpliColError): + """Runtime loading, selection, or evaluation failed.""" + + +class DependencyError(PyAmpliColError): + """A required optional or native dependency is unavailable.""" + + +__all__ = [ + "ArtifactError", + "CompatibilityError", + "ConfigurationError", + "DependencyError", + "EvaluationError", + "GenerationError", + "ModelError", + "PyAmpliColError", +] diff --git a/pyamplicol/src/pyamplicol/api/models.py b/pyamplicol/src/pyamplicol/api/models.py new file mode 100644 index 0000000000..e1b4ceeef1 --- /dev/null +++ b/pyamplicol/src/pyamplicol/api/models.py @@ -0,0 +1,698 @@ +# SPDX-License-Identifier: 0BSD +from __future__ import annotations + +import math +import os +from collections.abc import Iterable, Mapping, Sequence +from dataclasses import dataclass +from pathlib import Path +from typing import Literal, Protocol, TypeAlias, cast + +from .results import ModelParameter + +CompiledModelSourceKind: TypeAlias = Literal[ + "built-in-sm", + "built-in-sm-heft", + "ufo", + "json", + "compiled", +] +ModelCompilationSeverity: TypeAlias = Literal["warning", "error"] +SupportedColorAccuracy: TypeAlias = Literal["lc", "nlc", "full"] + + +class _CompiledModelPayloadView(Protocol): + name: str + schema_version: int + model_compiler_version: int + source: Mapping[str, object] + capabilities: Mapping[str, object] + parameter_defaults: Mapping[str, object] + issues: Iterable[object] + phase_timings: Mapping[str, object] + conversion_seconds: float + prepared_backend: str | None + + +@dataclass(frozen=True, slots=True) +class CompiledModelSource: + """Stable provenance for the source consumed by the model compiler.""" + + kind: CompiledModelSourceKind + name: str | None = None + digest: str | None = None + restriction: str | None = None + restriction_digest: str | None = None + simplify: bool = True + + def __post_init__(self) -> None: + if self.kind not in ( + "built-in-sm", + "built-in-sm-heft", + "ufo", + "json", + "compiled", + ): + raise ValueError(f"unsupported compiled model source kind {self.kind!r}") + for field_name in ("name", "digest", "restriction", "restriction_digest"): + value = getattr(self, field_name) + if value is not None and (not isinstance(value, str) or not value.strip()): + raise ValueError( + f"compiled model source {field_name} must be non-empty or null" + ) + if self.restriction is not None and self.restriction_digest is not None: + raise ValueError( + "compiled model source cannot have both a restriction name and digest" + ) + if not isinstance(self.simplify, bool): + raise TypeError("compiled model source simplify must be a boolean") + + +@dataclass(frozen=True, slots=True) +class CompiledModelCapabilities: + """User-relevant capabilities of a compiled physics model.""" + + particle_count: int = 0 + parameter_count: int = 0 + vertex_count: int = 0 + propagator_count: int = 0 + form_factor_count: int = 0 + maximum_valence: int = 0 + spins: tuple[int, ...] = () + color_representations: tuple[int, ...] = () + supported_color_accuracies: tuple[SupportedColorAccuracy, ...] = () + has_custom_propagators: bool = False + + def __post_init__(self) -> None: + for field_name in ( + "particle_count", + "parameter_count", + "vertex_count", + "propagator_count", + "form_factor_count", + "maximum_valence", + ): + _validate_non_negative_integer(getattr(self, field_name), field_name) + + spins = tuple(self.spins) + colors = tuple(self.color_representations) + accuracies = tuple(self.supported_color_accuracies) + _validate_integer_tuple(spins, "spins") + _validate_integer_tuple(colors, "color_representations") + if len(set(spins)) != len(spins): + raise ValueError("compiled model spins must be unique") + if len(set(colors)) != len(colors): + raise ValueError("compiled model color representations must be unique") + if any(accuracy not in ("lc", "nlc", "full") for accuracy in accuracies): + raise ValueError("compiled model has an unsupported color accuracy") + if len(set(accuracies)) != len(accuracies): + raise ValueError("compiled model color accuracies must be unique") + if not isinstance(self.has_custom_propagators, bool): + raise TypeError("has_custom_propagators must be a boolean") + + object.__setattr__(self, "spins", spins) + object.__setattr__(self, "color_representations", colors) + object.__setattr__(self, "supported_color_accuracies", accuracies) + + @property + def max_vertex_valence(self) -> int: + """Compatibility spelling for serialized model capability records.""" + + return self.maximum_valence + + @property + def color_accuracy_modes(self) -> tuple[SupportedColorAccuracy, ...]: + """Compatibility spelling for serialized model capability records.""" + + return self.supported_color_accuracies + + +@dataclass(frozen=True, slots=True) +class ModelCompilationIssue: + """A stable diagnostic emitted while compiling a model.""" + + severity: ModelCompilationSeverity + code: str + message: str + context: str = "" + + def __post_init__(self) -> None: + if self.severity not in ("warning", "error"): + raise ValueError(f"unsupported model issue severity {self.severity!r}") + if not isinstance(self.code, str) or not self.code.strip(): + raise ValueError("model compilation issue code must not be empty") + if not isinstance(self.message, str) or not self.message.strip(): + raise ValueError("model compilation issue message must not be empty") + if not isinstance(self.context, str): + raise TypeError("model compilation issue context must be text") + + +@dataclass(frozen=True, slots=True) +class ModelCompilationPhase: + """Wall-clock duration of one model-compilation phase.""" + + name: str + seconds: float + + def __post_init__(self) -> None: + if not isinstance(self.name, str) or not self.name.strip(): + raise ValueError("model compilation phase name must not be empty") + object.__setattr__( + self, + "seconds", + _non_negative_finite(self.seconds, "model compilation phase seconds"), + ) + + +@dataclass(frozen=True, slots=True) +class CompiledModelInfo: + """Deeply immutable public metadata for one compiled model.""" + + name: str + schema_version: int + model_compiler_version: int + source: CompiledModelSource + capabilities: CompiledModelCapabilities + parameters: tuple[ModelParameter, ...] + issues: tuple[ModelCompilationIssue, ...] + compilation_phases: tuple[ModelCompilationPhase, ...] + conversion_seconds: float + + def __post_init__(self) -> None: + if not isinstance(self.name, str) or not self.name.strip(): + raise ValueError("compiled model name must not be empty") + _validate_non_negative_integer(self.schema_version, "schema_version") + _validate_non_negative_integer( + self.model_compiler_version, + "model_compiler_version", + ) + if not isinstance(self.source, CompiledModelSource): + raise TypeError("compiled model source must be CompiledModelSource") + if not isinstance(self.capabilities, CompiledModelCapabilities): + raise TypeError( + "compiled model capabilities must be CompiledModelCapabilities" + ) + + parameters = tuple(self.parameters) + issues = tuple(self.issues) + phases = tuple(self.compilation_phases) + _validate_tuple_members(parameters, ModelParameter, "parameters") + _validate_tuple_members(issues, ModelCompilationIssue, "issues") + _validate_tuple_members(phases, ModelCompilationPhase, "compilation_phases") + if len({parameter.name for parameter in parameters}) != len(parameters): + raise ValueError("compiled model parameter names must be unique") + if len({phase.name for phase in phases}) != len(phases): + raise ValueError("model compilation phase names must be unique") + object.__setattr__( + self, + "conversion_seconds", + _non_negative_finite( + self.conversion_seconds, + "compiled model conversion_seconds", + ), + ) + + object.__setattr__(self, "parameters", parameters) + object.__setattr__(self, "issues", issues) + object.__setattr__(self, "compilation_phases", phases) + + @property + def supported(self) -> bool: + return not any(issue.severity == "error" for issue in self.issues) + + @property + def phases(self) -> tuple[ModelCompilationPhase, ...]: + """Short alias for :attr:`compilation_phases`.""" + + return self.compilation_phases + + +class CompiledModel: + """Opaque compiled-model handle accepted by :class:`~pyamplicol.Generator`. + + Construct handles with :meth:`pyamplicol.ModelSource.compile`. The compiler's + mutable payload and expression IR intentionally remain private; stable model + metadata is available through :attr:`info` and the typed convenience + properties below. + """ + + __slots__ = ("_info", "_payload") + + _info: CompiledModelInfo + _payload: object + + def __init__(self) -> None: + raise TypeError("construct compiled models with ModelSource.compile()") + + def __setattr__(self, _name: str, _value: object) -> None: + raise AttributeError("CompiledModel handles are immutable") + + def __delattr__(self, _name: str) -> None: + raise AttributeError("CompiledModel handles are immutable") + + @property + def info(self) -> CompiledModelInfo: + """Stable, deeply immutable model metadata.""" + + return self._info + + @property + def name(self) -> str: + return self._info.name + + @property + def schema_version(self) -> int: + return self._info.schema_version + + @property + def model_compiler_version(self) -> int: + return self._info.model_compiler_version + + @property + def source(self) -> CompiledModelSource: + return self._info.source + + @property + def capabilities(self) -> CompiledModelCapabilities: + return self._info.capabilities + + @property + def parameters(self) -> tuple[ModelParameter, ...]: + return self._info.parameters + + @property + def issues(self) -> tuple[ModelCompilationIssue, ...]: + return self._info.issues + + @property + def compilation_phases(self) -> tuple[ModelCompilationPhase, ...]: + return self._info.compilation_phases + + @property + def conversion_seconds(self) -> float: + return self._info.conversion_seconds + + @property + def supported(self) -> bool: + return self._info.supported + + @property + def is_prepared(self) -> bool: + """Whether this handle owns a validated prepared-mode kernel pack. + + The pack is shared by eager, recurrence, and on-the-fly execution. + """ + + return self.prepared_backend is not None + + @property + def prepared_backend(self) -> Literal["jit", "asm", "cpp"] | None: + """Backend embedded in the prepared model, if any.""" + + backend = getattr(self._payload, "prepared_backend", None) + if backend is None: + return None + if backend not in ("jit", "asm", "cpp"): + raise RuntimeError(f"invalid prepared-model backend {backend!r}") + return cast(Literal["jit", "asm", "cpp"], backend) + + def write(self, path: os.PathLike[str] | str) -> Path: + """Serialize the compiled model and return its absolute output path.""" + + writer = getattr(self._payload, "write", None) + if not callable(writer): + raise RuntimeError("compiled model payload cannot be serialized") + target = Path(os.fspath(path)).expanduser().resolve(strict=False) + return Path(os.fspath(writer(target))).resolve(strict=False) + + def write_parameter_card(self, path: os.PathLike[str] | str) -> Path: + """Write mutable external-parameter defaults as a JSON model card.""" + + writer = getattr(self._payload, "write_parameter_card", None) + if not callable(writer): + raise RuntimeError("compiled model payload cannot write parameter cards") + target = Path(os.fspath(path)).expanduser().resolve(strict=False) + return Path(os.fspath(writer(target))).resolve(strict=False) + + def __repr__(self) -> str: + return ( + f"CompiledModel(name={self.name!r}, supported={self.supported!r}, " + f"schema_version={self.schema_version})" + ) + + +def _compiled_model_from_payload(payload: object) -> CompiledModel: + """Create an opaque public handle from the private compiler payload.""" + + view = cast(_CompiledModelPayloadView, payload) + try: + info = _compiled_model_info( + name=str(view.name), + schema_version=int(view.schema_version), + model_compiler_version=int(view.model_compiler_version), + source=view.source, + capabilities=view.capabilities, + parameter_defaults=view.parameter_defaults, + issues=view.issues, + phase_timings=view.phase_timings, + conversion_seconds=float(view.conversion_seconds), + ) + except (AttributeError, TypeError, ValueError) as exc: + raise TypeError("invalid private compiled-model payload") from exc + model = object.__new__(CompiledModel) + object.__setattr__(model, "_payload", payload) + object.__setattr__(model, "_info", info) + return model + + +def _compiled_model_payload(model: CompiledModel) -> object: + """Return the private payload at an internal generation boundary.""" + + if not isinstance(model, CompiledModel): + raise TypeError("expected a CompiledModel handle") + return model._payload + + +def _compiled_model_info( + *, + name: str, + schema_version: int, + model_compiler_version: int, + source: Mapping[str, object], + capabilities: Mapping[str, object], + parameter_defaults: Mapping[str, object], + issues: Iterable[object], + phase_timings: Mapping[str, object], + conversion_seconds: float, +) -> CompiledModelInfo: + """Convert compiler-owned records without importing the compiler module.""" + + return CompiledModelInfo( + name=name, + schema_version=schema_version, + model_compiler_version=model_compiler_version, + source=_compiled_model_source(source), + capabilities=_compiled_model_capabilities(capabilities), + parameters=_compiled_model_parameters(parameter_defaults), + issues=tuple(_model_compilation_issue(issue) for issue in issues), + compilation_phases=_model_compilation_phases(phase_timings), + conversion_seconds=conversion_seconds, + ) + + +def _compiled_model_source(source: Mapping[str, object]) -> CompiledModelSource: + raw_kind = source.get("kind") + if not isinstance(raw_kind, str) or raw_kind not in ( + "built-in-sm", + "built-in-sm-heft", + "ufo", + "json", + "compiled", + ): + raise ValueError(f"unsupported compiled model source kind {raw_kind!r}") + + raw_name = source.get("source_name", source.get("name")) + name = _optional_non_empty_string(raw_name, "compiled model source name") + digest = _optional_non_empty_string( + source.get("digest"), + "compiled model source digest", + ) + + options = _optional_mapping(source.get("options"), "compiled model source options") + simplify = _boolean( + source.get("simplify", options.get("simplify", True)), + "compiled model source simplify", + ) + restriction: str | None = None + restriction_digest: str | None = None + raw_restriction = source.get("restriction", options.get("restriction")) + if isinstance(raw_restriction, str): + restriction = _optional_non_empty_string( + raw_restriction, + "compiled model source restriction", + ) + elif isinstance(raw_restriction, Mapping): + restriction_kind = raw_restriction.get("kind") + if restriction_kind == "name": + restriction = _optional_non_empty_string( + raw_restriction.get("value"), + "compiled model source restriction", + ) + elif restriction_kind == "file": + restriction_digest = _optional_non_empty_string( + raw_restriction.get("sha256"), + "compiled model restriction digest", + ) + + return CompiledModelSource( + kind=cast(CompiledModelSourceKind, raw_kind), + name=name, + digest=digest, + restriction=restriction, + restriction_digest=restriction_digest, + simplify=simplify, + ) + + +def _compiled_model_capabilities( + capabilities: Mapping[str, object], +) -> CompiledModelCapabilities: + return CompiledModelCapabilities( + particle_count=_mapping_non_negative_integer( + capabilities, + "particle_count", + ), + parameter_count=_mapping_non_negative_integer( + capabilities, + "parameter_count", + ), + vertex_count=_mapping_non_negative_integer(capabilities, "vertex_count"), + propagator_count=_mapping_non_negative_integer( + capabilities, + "propagator_count", + fallback="compiled_propagator_count", + ), + form_factor_count=_mapping_non_negative_integer( + capabilities, + "form_factor_count", + ), + maximum_valence=_mapping_non_negative_integer( + capabilities, + "maximum_valence", + fallback="max_vertex_valence", + ), + spins=_integer_values(capabilities.get("spins", ()), "spins"), + color_representations=_integer_values( + capabilities.get("color_representations", ()), + "color_representations", + ), + supported_color_accuracies=_color_accuracies( + capabilities.get( + "supported_color_accuracies", + capabilities.get("color_accuracy_modes", ()), + ) + ), + has_custom_propagators=_boolean( + capabilities.get("has_custom_propagators", False), + "has_custom_propagators", + ), + ) + + +def _compiled_model_parameters( + parameter_defaults: Mapping[str, object], +) -> tuple[ModelParameter, ...]: + parameters = [] + for raw_name, value in parameter_defaults.items(): + name = str(raw_name) + real, imaginary = _complex_pair(value, f"model parameter {name!r}") + parameters.append( + ModelParameter( + name=name, + kind="external", + default_real=real, + default_imaginary=imaginary, + mutable=True, + ) + ) + return tuple(sorted(parameters, key=lambda parameter: parameter.name)) + + +def _model_compilation_issue(issue: object) -> ModelCompilationIssue: + if isinstance(issue, ModelCompilationIssue): + return issue + raw_severity = _record_value(issue, "severity") + if not isinstance(raw_severity, str) or raw_severity not in ("warning", "error"): + raise ValueError(f"unsupported model issue severity {raw_severity!r}") + code = _record_text(issue, "code") + message = _record_text(issue, "message") + raw_context = _record_value(issue, "context", default="") + context = "" if raw_context is None else raw_context + if not isinstance(context, str): + raise TypeError("model compilation issue context must be text") + return ModelCompilationIssue( + severity=cast(ModelCompilationSeverity, raw_severity), + code=code, + message=message, + context=context, + ) + + +def _model_compilation_phases( + phase_timings: Mapping[str, object], +) -> tuple[ModelCompilationPhase, ...]: + return tuple( + ModelCompilationPhase( + name=str(name), + seconds=_finite_float(value, f"model compilation phase {name!r}"), + ) + for name, value in phase_timings.items() + ) + + +_MISSING = object() + + +def _record_value( + record: object, + field_name: str, + *, + default: object = _MISSING, +) -> object: + if isinstance(record, Mapping): + if field_name in record: + return record[field_name] + elif hasattr(record, field_name): + return getattr(record, field_name) + if default is not _MISSING: + return default + raise TypeError(f"model compilation issue has no {field_name!r} field") + + +def _record_text(record: object, field_name: str) -> str: + value = _record_value(record, field_name) + if not isinstance(value, str) or not value.strip(): + raise ValueError(f"model compilation issue {field_name} must not be empty") + return value + + +def _optional_mapping(value: object, context: str) -> Mapping[str, object]: + if value is None: + return {} + if not isinstance(value, Mapping): + raise TypeError(f"{context} must be a mapping") + return value + + +def _optional_non_empty_string(value: object, context: str) -> str | None: + if value is None: + return None + if not isinstance(value, str) or not value.strip(): + raise ValueError(f"{context} must be non-empty or null") + return value + + +def _mapping_non_negative_integer( + mapping: Mapping[str, object], + field_name: str, + *, + fallback: str | None = None, +) -> int: + value = mapping.get(field_name, _MISSING) + if value is _MISSING and fallback is not None: + value = mapping.get(fallback, _MISSING) + if value is _MISSING: + value = 0 + _validate_non_negative_integer(value, field_name) + return cast(int, value) + + +def _validate_non_negative_integer(value: object, context: str) -> None: + if isinstance(value, bool) or not isinstance(value, int) or value < 0: + raise ValueError(f"{context} must be a non-negative integer") + + +def _validate_integer_tuple(values: tuple[object, ...], context: str) -> None: + if not all( + isinstance(value, int) and not isinstance(value, bool) for value in values + ): + raise ValueError(f"{context} must contain integers") + + +def _integer_values(value: object, context: str) -> tuple[int, ...]: + values = _sequence(value, context) + _validate_integer_tuple(values, context) + return cast(tuple[int, ...], values) + + +def _color_accuracies(value: object) -> tuple[SupportedColorAccuracy, ...]: + values = _sequence(value, "supported color accuracies") + if not all( + isinstance(accuracy, str) and accuracy in ("lc", "nlc", "full") + for accuracy in values + ): + raise ValueError("compiled model has an unsupported color accuracy") + return cast(tuple[SupportedColorAccuracy, ...], values) + + +def _sequence(value: object, context: str) -> tuple[object, ...]: + if isinstance(value, (str, bytes, bytearray)) or not isinstance(value, Sequence): + raise TypeError(f"{context} must be a sequence") + return tuple(value) + + +def _complex_pair(value: object, context: str) -> tuple[float, float]: + if isinstance(value, complex): + return float(value.real), float(value.imag) + if isinstance(value, Sequence) and not isinstance(value, (str, bytes, bytearray)): + if len(value) != 2: + raise ValueError(f"{context} must contain a real and imaginary value") + return ( + _finite_float(value[0], f"{context} real component"), + _finite_float(value[1], f"{context} imaginary component"), + ) + return _finite_float(value, context), 0.0 + + +def _boolean(value: object, context: str) -> bool: + if not isinstance(value, bool): + raise TypeError(f"{context} must be a boolean") + return value + + +def _finite_float(value: object, context: str) -> float: + if isinstance(value, (bool, str, bytes, bytearray)): + raise TypeError(f"{context} must be a finite number") + try: + result = float(value) # type: ignore[arg-type] + except (TypeError, ValueError, OverflowError) as exc: + raise TypeError(f"{context} must be a finite number") from exc + if not math.isfinite(result): + raise ValueError(f"{context} must be finite") + return result + + +def _non_negative_finite(value: object, context: str) -> float: + result = _finite_float(value, context) + if result < 0.0: + raise ValueError(f"{context} must be non-negative") + return result + + +def _validate_tuple_members( + values: tuple[object, ...], + member_type: type[object], + context: str, +) -> None: + if not all(isinstance(value, member_type) for value in values): + raise TypeError(f"compiled model {context} contain invalid records") + + +__all__ = [ + "CompiledModel", + "CompiledModelCapabilities", + "CompiledModelInfo", + "CompiledModelSource", + "ModelCompilationIssue", + "ModelCompilationPhase", +] diff --git a/pyamplicol/src/pyamplicol/api/process_groups.py b/pyamplicol/src/pyamplicol/api/process_groups.py new file mode 100644 index 0000000000..573d61188b --- /dev/null +++ b/pyamplicol/src/pyamplicol/api/process_groups.py @@ -0,0 +1,53 @@ +# SPDX-License-Identifier: 0BSD +"""Process-group analysis of a generated artifact.""" + +from __future__ import annotations + +import os +from pathlib import Path + +from pyamplicol.generation.process_grouping import GroupingAnalysis +from pyamplicol.generation.process_grouping_numerical import ( + update_artifact_process_groups, +) + + +def group_processes( + artifact: os.PathLike[str] | str, + *, + analysis: str = "numerical", + only_unit_multiplier: bool = False, + consider_charge_conjugation: bool = True, +) -> dict[str, object]: + """Group the artifact's subprocesses with equal matrix elements. + + The stored representatives are compared numerically and every new + relation (a rational factor and a leg permutation, optionally with charge + conjugation) is merged into ``process-groups.json``, which is rewritten in + place. UMAMI files and the artifact ID are left unchanged. ``"none"`` only + writes the trivial map when the artifact has none. Returns a summary. + """ + + if analysis not in tuple(GroupingAnalysis): + raise ValueError( + "analysis must be one of " + + ", ".join(repr(str(value)) for value in GroupingAnalysis) + ) + for name, value in ( + ("only_unit_multiplier", only_unit_multiplier), + ("consider_charge_conjugation", consider_charge_conjugation), + ): + if not isinstance(value, bool): + raise TypeError(f"{name} must be a boolean") + path = Path(os.fspath(artifact)).expanduser() + if not path.is_dir(): + raise FileNotFoundError(f"artifact does not exist: {path}") + return update_artifact_process_groups( + path, + analysis=GroupingAnalysis(analysis), + only_unit_multiplier=only_unit_multiplier, + consider_charge_conjugation=consider_charge_conjugation, + ) + + +__all__ = ["group_processes"] diff --git a/pyamplicol/src/pyamplicol/api/protocols.py b/pyamplicol/src/pyamplicol/api/protocols.py new file mode 100644 index 0000000000..7685c2575c --- /dev/null +++ b/pyamplicol/src/pyamplicol/api/protocols.py @@ -0,0 +1,144 @@ +# SPDX-License-Identifier: 0BSD +from __future__ import annotations + +import os +from collections.abc import Mapping, Sequence +from decimal import Decimal +from typing import Protocol, runtime_checkable + +import pyamplicol as _pyamplicol +from pyamplicol.config import ( + BenchmarkConfig, + ConfigResolution, + GenerationConfig, + RunConfig, +) +from pyamplicol.reporting import ProgressSink + +from .requests import ModelSource, ProcessSet +from .results import ( + BenchmarkResult, + GenerationPlan, + GenerationResult, + ProcessPhysics, + ResolvedEvaluation, +) + +ScalarInput = float | int | str | Decimal +ScalarValue = complex | Decimal +Momenta = Sequence[Sequence[Sequence[ScalarInput]]] +ModelParameters = Mapping[ + str, + complex | float | int | list[float | int] | tuple[float | int, float | int], +] + + +@runtime_checkable +class GeneratorBackend(Protocol): + def plan( + self, + processes: ProcessSet, + *, + model: ModelSource | _pyamplicol.CompiledModel | None = None, + ) -> GenerationPlan: ... + + def generate( + self, + processes: ProcessSet, + output: os.PathLike[str] | str, + *, + model: ModelSource | _pyamplicol.CompiledModel | None = None, + mode: str = "error", + ) -> GenerationResult: ... + + +@runtime_checkable +class RuntimeBackend(Protocol): + """Minimum structural runtime contract. + + Authenticated ``artifact_id`` and ``execution_mode`` are facade capabilities + rather than protocol requirements so existing injected third-party backends + remain structurally compatible. Built-in backends expose both, and + :class:`pyamplicol.Runtime` fails closed when either identity is requested + but unavailable. + """ + + @property + def physics(self) -> ProcessPhysics: ... + + def evaluate( + self, + momenta: Momenta, + *, + helicities: Sequence[str] | None = None, + color_flows: Sequence[str] | None = None, + precision: int = 16, + ) -> Sequence[ScalarValue]: ... + + def evaluate_resolved( + self, + momenta: Momenta, + *, + helicities: Sequence[str] | None = None, + color_flows: Sequence[str] | None = None, + precision: int = 16, + ) -> ResolvedEvaluation: ... + + def set_model_parameters(self, mapping: ModelParameters) -> None: ... + + def clear(self) -> None: ... + + def mute_warnings(self) -> None: ... + + def unmute_warnings(self) -> None: ... + + +@runtime_checkable +class BenchmarkBackend(Protocol): + def run( + self, + target: RuntimeBackend | os.PathLike[str] | str, + *, + points: Momenta | None = None, + ) -> BenchmarkResult: ... + + +class GeneratorFactory(Protocol): + def __call__( + self, + config: GenerationConfig | RunConfig | ConfigResolution | None, + progress: ProgressSink | None, + ) -> GeneratorBackend: ... + + +class RuntimeLoader(Protocol): + def __call__( + self, + artifact: os.PathLike[str] | str, + *, + process: str | None, + model_parameters: ModelParameters | None, + mute_warnings: bool, + ) -> RuntimeBackend: ... + + +class BenchmarkFactory(Protocol): + def __call__( + self, + config: BenchmarkConfig | RunConfig | None, + progress: ProgressSink | None, + ) -> BenchmarkBackend: ... + + +__all__ = [ + "BenchmarkBackend", + "BenchmarkFactory", + "GeneratorBackend", + "GeneratorFactory", + "ModelParameters", + "Momenta", + "RuntimeBackend", + "RuntimeLoader", + "ScalarInput", + "ScalarValue", +] diff --git a/pyamplicol/src/pyamplicol/api/requests.py b/pyamplicol/src/pyamplicol/api/requests.py new file mode 100644 index 0000000000..87ea91f1ac --- /dev/null +++ b/pyamplicol/src/pyamplicol/api/requests.py @@ -0,0 +1,495 @@ +# SPDX-License-Identifier: 0BSD +from __future__ import annotations + +import hashlib +import os +import re +from collections.abc import Iterable, Sequence +from dataclasses import dataclass +from pathlib import Path +from typing import TYPE_CHECKING, Literal, TypeAlias + +import pyamplicol as _pyamplicol +from pyamplicol.config import EvaluatorConfig + +from .errors import ModelError + +if TYPE_CHECKING: + from pyamplicol.models.loading import CompiledModel + +ModelSourceKind: TypeAlias = Literal[ + "built-in-sm", + "built-in-sm-heft", + "ufo", + "json", + "compiled", + "prepared", +] +_PROCESS_NAME = re.compile(r"^[A-Za-z][A-Za-z0-9_.-]*$") + + +@dataclass(frozen=True, slots=True) +class ModelSource: + """A built-in, packaged HEFT, UFO, JSON, or precompiled model input. + + Use :meth:`from_config` for a resolved run card or :meth:`from_path` for an + external model. Calling :meth:`compile` produces the canonical serialized + model consumed by process generation. + """ + + kind: ModelSourceKind + path: Path | None = None + restriction: Path | str | None = None + simplify: bool = True + + def __post_init__(self) -> None: + if self.kind not in ( + "built-in-sm", + "built-in-sm-heft", + "ufo", + "json", + "compiled", + "prepared", + ): + raise ModelError(f"unsupported model source kind {self.kind!r}") + if not isinstance(self.simplify, bool): + raise ModelError("model simplify must be a boolean") + if self.path is not None: + try: + resolved_path = ( + Path(os.fspath(self.path)).expanduser().resolve(strict=False) + ) + except TypeError as exc: + raise ModelError("model path must be path-like or null") from exc + object.__setattr__(self, "path", resolved_path) + if self.restriction is not None: + if isinstance(self.restriction, str): + if not self.restriction or any( + character.isspace() for character in self.restriction + ): + raise ModelError( + "model restriction names must be non-empty and contain no " + "whitespace" + ) + else: + try: + resolved_restriction = ( + Path(os.fspath(self.restriction)) + .expanduser() + .resolve(strict=False) + ) + except TypeError as exc: + raise ModelError( + "model restriction must be a name, path-like, or null" + ) from exc + object.__setattr__(self, "restriction", resolved_restriction) + if self.kind in {"built-in-sm", "built-in-sm-heft"}: + if self.path is not None: + raise ModelError("built-in models have no public source path") + elif self.kind in ("ufo", "json") and ( + self.path is None or not self.path.is_absolute() + ): + raise ModelError("external model paths must be absolute") + elif self.path is not None and not self.path.is_absolute(): + raise ModelError("compiled model paths must be absolute") + if isinstance(self.restriction, Path) and not self.restriction.is_absolute(): + raise ModelError("model restriction paths must be absolute") + + @classmethod + def built_in_sm(cls) -> ModelSource: + return cls(kind="built-in-sm") + + @classmethod + def built_in_sm_heft(cls) -> ModelSource: + """Select the packaged Standard Model with scalar HEFT interactions.""" + + return cls(kind="built-in-sm-heft") + + @classmethod + def from_config(cls, config: object) -> ModelSource: + """Create a source from the typed ``[model]`` run-card section. + + Restriction selectors such as ``default``, ``none``, or a loader-defined + name remain names. Explicit restriction files are resolved relative to + the selected UFO/JSON model by :meth:`from_path`. + """ + + from pyamplicol.config import ModelConfig + + if not isinstance(config, ModelConfig): + raise TypeError("model source config must be a ModelConfig") + if config.source == "built-in-sm": + if config.restriction not in (None, "default"): + raise ModelError( + "model restrictions can only be applied to external models" + ) + if not config.simplify: + raise ModelError( + "simplification cannot be disabled for the built-in Standard Model" + ) + return cls.built_in_sm() + from pyamplicol._internal.sm_heft import is_sm_heft_alias + + if is_sm_heft_alias(config.source): + if config.restriction not in (None, "default"): + raise ModelError( + "model restrictions cannot be applied to built-in-sm-heft" + ) + if not config.simplify: + raise ModelError( + "simplification cannot be disabled for built-in-sm-heft" + ) + return cls.built_in_sm_heft() + return cls.from_path( + config.source, + restriction=config.restriction, + simplify=config.simplify, + ) + + @classmethod + def from_path( + cls, + path: os.PathLike[str] | str, + restriction: os.PathLike[str] | str | None = None, + simplify: bool = True, + ) -> ModelSource: + """Resolve an external source kind without importing Symbolica.""" + + source = Path(os.fspath(path)).expanduser().resolve(strict=False) + if not source.exists(): + raise ModelError(f"model source does not exist: {source}") + if source.is_dir(): + kind: ModelSourceKind = "ufo" + elif source.is_file() and source.name.lower().endswith(".pyamplicol-model"): + kind = "prepared" + elif source.is_file() and source.suffix.lower() == ".json": + lowered_name = source.name.lower() + kind = ( + "compiled" + if lowered_name.endswith( + (".pyamplicol-model.json", ".compiled-model.json") + ) + else "json" + ) + elif source.is_file(): + kind = "compiled" + else: + raise ModelError(f"model source is not a file or directory: {source}") + + restriction_value: Path | str | None = None + if restriction is not None: + raw_restriction = os.fspath(restriction) + if not isinstance(raw_restriction, str): + raise ModelError( + "model restriction must resolve to a text path or name" + ) + restriction_path = Path(raw_restriction).expanduser() + if not restriction_path.is_absolute(): + base = source if source.is_dir() else source.parent + restriction_path = base / restriction_path + restriction_path = restriction_path.resolve(strict=False) + if restriction_path.is_file(): + restriction_value = restriction_path + elif not isinstance(restriction, str) or _restriction_looks_like_path( + raw_restriction + ): + raise ModelError( + f"model restriction file does not exist: {restriction_path}" + ) + else: + restriction_value = raw_restriction + return cls( + kind=kind, + path=source, + restriction=restriction_value, + simplify=simplify, + ) + + def compile( + self, + *, + cache_dir: os.PathLike[str] | str | None = None, + use_cache: bool = True, + require_supported: bool = True, + prepared_output: os.PathLike[str] | str | None = None, + evaluator: EvaluatorConfig | None = None, + ) -> _pyamplicol.CompiledModel: + """Compile or load this source and return its compiled-model handle. + + By default this only creates the canonical, portable model IR in memory. + Supplying both ``prepared_output`` and ``evaluator`` additionally writes + one self-contained ``.pyamplicol-model`` bundle containing exactly the + requested evaluator backend pack. The returned handle is loaded from that + bundle and can be passed directly to eager, recurrence, or on-the-fly + generation. + """ + + if (prepared_output is None) != (evaluator is None): + raise ModelError( + "prepared model compilation requires both prepared_output and " + "an EvaluatorConfig" + ) + if evaluator is not None and not isinstance(evaluator, EvaluatorConfig): + raise ModelError("evaluator must be an EvaluatorConfig or null") + + resolved_cache: Path | None = None + if cache_dir is not None: + try: + resolved_cache = ( + Path(os.fspath(cache_dir)).expanduser().resolve(strict=False) + ) + except TypeError as exc: + raise ModelError( + "model cache directory must be path-like or null" + ) from exc + resolved_prepared_output: Path | None = None + if prepared_output is not None: + try: + resolved_prepared_output = ( + Path(os.fspath(prepared_output)).expanduser().resolve(strict=False) + ) + except TypeError as exc: + raise ModelError( + "prepared model output must be path-like or null" + ) from exc + if not resolved_prepared_output.name.lower().endswith(".pyamplicol-model"): + raise ModelError( + "prepared model output must end with '.pyamplicol-model'" + ) + try: + payload = _compile_model_source_payload( + self, + cache_dir=resolved_cache, + use_cache=use_cache, + require_supported=require_supported, + ) + if resolved_prepared_output is not None: + assert evaluator is not None + from pyamplicol.models.prepared_compile import prepare_model_bundle + + prepared = prepare_model_bundle( + payload, + resolved_prepared_output, + evaluator=evaluator, + ) + from pyamplicol.models.loading import load_compiled_model + + payload = load_compiled_model(prepared.output) + from .models import _compiled_model_from_payload + + return _compiled_model_from_payload(payload) + except (OSError, RuntimeError, TypeError, ValueError) as exc: + raise ModelError(str(exc)) from exc + + +def _compile_model_source_payload( + source: ModelSource, + *, + cache_dir: Path | None, + use_cache: bool, + require_supported: bool, +) -> CompiledModel: + """Compile one normalized source through the shared private boundary.""" + + if source.kind == "built-in-sm-heft": + from pyamplicol._internal.sm_heft import compile_sm_heft_source + + return compile_sm_heft_source( + cache_dir=cache_dir, + use_cache=use_cache, + require_supported=require_supported, + ) + if source.kind == "built-in-sm": + source_input: str | Path = source.kind + elif source.path is not None: + source_input = source.path + else: + raise ModelError("an in-memory compiled model source cannot be reloaded") + from pyamplicol.models.loading import compile_model_source + + return compile_model_source( + source_input, + restriction=( + "default" if source.restriction is None else os.fspath(source.restriction) + ), + simplify=source.simplify, + cache_dir=cache_dir, + use_cache=use_cache, + require_supported=require_supported, + ) + + +def _restriction_looks_like_path(value: str) -> bool: + path = Path(value) + return ( + path.is_absolute() + or path.parent != Path(".") + or path.name.startswith("restrict_") + or path.suffix.lower() in {".dat", ".json"} + ) + + +def _validate_delimiters(expression: str) -> None: + pairs = {")": "(", "]": "[", "}": "{"} + stack: list[str] = [] + for character in expression: + if character in "([{": + stack.append(character) + elif character in pairs and (not stack or stack.pop() != pairs[character]): + raise ValueError("process expression has unbalanced delimiters") + if stack: + raise ValueError("process expression has unbalanced delimiters") + + +def _default_process_name(expression: str) -> str: + readable = expression.replace("~", "bar") + readable = readable.replace(">", "_to_") + readable = re.sub(r"[^A-Za-z0-9]+", "_", readable).strip("_").lower() + if not readable or not readable[0].isalpha(): + readable = f"process_{readable}" if readable else "process" + if len(readable) <= 72: + return readable + digest = hashlib.sha256(expression.encode("utf-8")).hexdigest()[:10] + return f"{readable[:61]}_{digest}" + + +@dataclass(frozen=True, slots=True) +class ProcessRequest: + """One concrete or multiparticle process expression and its artifact name.""" + + expression: str + name: str + + def __post_init__(self) -> None: + if not isinstance(self.expression, str) or not self.expression: + raise ValueError("process expression must be a non-empty string") + if not isinstance(self.name, str) or not _PROCESS_NAME.fullmatch(self.name): + raise ValueError( + "process name must start with a letter and contain only " + "letters, digits, '.', '_', or '-'" + ) + + @classmethod + def parse(cls, expression: str, *, name: str | None = None) -> ProcessRequest: + """Normalize an expression and derive a stable readable name if omitted.""" + + if not isinstance(expression, str): + raise TypeError("process expression must be a string") + if "\n" in expression or "\r" in expression: + raise ValueError("one ProcessRequest may contain only one line") + normalized = " ".join(expression.split()) + if not normalized: + raise ValueError("process expression must not be empty") + _validate_delimiters(normalized) + if ">" not in normalized: + raise ValueError("process expression must contain '>'") + for segment in normalized.split(","): + if ">" not in segment: + continue + if segment.count(">") != 1: + raise ValueError("each process clause must contain exactly one arrow") + sides = segment.split(">") + if any(not side.strip() for side in sides): + raise ValueError("each process arrow must have particles on both sides") + selected_name = _default_process_name(normalized) if name is None else name + return cls(expression=normalized, name=selected_name) + + +@dataclass(frozen=True, slots=True) +class ProcessAlias: + """A public process alias backed by a generated concrete process. + + ``particle_permutation`` maps each stored representative external index to + its public alias index. Incoming and outgoing legs may each be reordered, + but a leg may not cross the process arrow. Runtime selectors and metadata + are remapped to the alias automatically. + """ + + name: str + process_name: str + particle_permutation: tuple[int, ...] = () + + def __post_init__(self) -> None: + if not _PROCESS_NAME.fullmatch(self.name): + raise ValueError(f"invalid process alias name {self.name!r}") + if not _PROCESS_NAME.fullmatch(self.process_name): + raise ValueError(f"invalid aliased process name {self.process_name!r}") + permutation = tuple(self.particle_permutation) + if any( + isinstance(item, bool) or not isinstance(item, int) or item < 0 + for item in permutation + ): + raise ValueError("particle_permutation must contain non-negative integers") + if permutation and sorted(permutation) != list(range(len(permutation))): + raise ValueError("particle_permutation must be a complete permutation") + object.__setattr__(self, "particle_permutation", permutation) + + +@dataclass(frozen=True, slots=True) +class ProcessSet: + """A named collection of generation requests and optional crossing aliases.""" + + requests: tuple[ProcessRequest, ...] + aliases: tuple[ProcessAlias, ...] = () + + def __post_init__(self) -> None: + requests = tuple(self.requests) + aliases = tuple(self.aliases) + if not requests: + raise ValueError("a ProcessSet must contain at least one request") + if not all(isinstance(request, ProcessRequest) for request in requests): + raise TypeError("ProcessSet.requests must contain ProcessRequest objects") + if not all(isinstance(alias, ProcessAlias) for alias in aliases): + raise TypeError("ProcessSet.aliases must contain ProcessAlias objects") + request_names = tuple(request.name for request in requests) + if len(set(request_names)) != len(request_names): + raise ValueError("ProcessSet request names must be unique") + alias_names = tuple(alias.name for alias in aliases) + if len(set(alias_names)) != len(alias_names): + raise ValueError("ProcessSet alias names must be unique") + if set(alias_names).intersection(request_names): + raise ValueError("process alias names may not shadow request names") + unknown_targets = { + alias.process_name + for alias in aliases + if alias.process_name not in request_names + } + if unknown_targets: + raise ValueError( + "process aliases refer to unknown requests: " + + ", ".join(sorted(unknown_targets)) + ) + object.__setattr__(self, "requests", requests) + object.__setattr__(self, "aliases", aliases) + + @classmethod + def from_expressions( + cls, + expressions: Iterable[str], + *, + names: Sequence[str] = (), + aliases: Iterable[ProcessAlias] = (), + ) -> ProcessSet: + """Build a process set from expressions using optional aligned names.""" + + expression_tuple = tuple(expressions) + name_tuple = tuple(names) + if name_tuple and len(name_tuple) != len(expression_tuple): + raise ValueError("process names must be empty or aligned with expressions") + requests = tuple( + ProcessRequest.parse( + expression, + name=name_tuple[index] if name_tuple else None, + ) + for index, expression in enumerate(expression_tuple) + ) + return cls(requests=requests, aliases=tuple(aliases)) + + +__all__ = [ + "ModelSource", + "ModelSourceKind", + "ProcessAlias", + "ProcessRequest", + "ProcessSet", +] diff --git a/pyamplicol/src/pyamplicol/api/results.py b/pyamplicol/src/pyamplicol/api/results.py new file mode 100644 index 0000000000..f540298d32 --- /dev/null +++ b/pyamplicol/src/pyamplicol/api/results.py @@ -0,0 +1,1006 @@ +# SPDX-License-Identifier: 0BSD +from __future__ import annotations + +import math +import os +from collections.abc import Mapping +from dataclasses import dataclass +from decimal import Decimal +from pathlib import Path +from types import MappingProxyType +from typing import Literal, TypeAlias, cast + +from pyamplicol.config import ( + BenchmarkConfig, + ConfigClamp, + GenerationConfig, + RunConfig, +) + +from .requests import ProcessRequest, ProcessSet + + +def _freeze(value: object) -> object: + if isinstance(value, Mapping): + return MappingProxyType( + {str(key): _freeze(entry) for key, entry in value.items()} + ) + if isinstance(value, (list, tuple)): + return tuple(_freeze(entry) for entry in value) + return value + + +def _freeze_mapping(value: Mapping[str, object]) -> Mapping[str, object]: + frozen = _freeze(value) + assert isinstance(frozen, Mapping) + return frozen + + +def _exact_decimal_sum(values: tuple[Decimal, ...]) -> Decimal: + if not values: + return Decimal(0) + if not all(value.is_finite() for value in values): + return sum(values, start=Decimal(0)) + + def finite_exponent(value: Decimal) -> int: + exponent = value.as_tuple().exponent + if not isinstance(exponent, int): + raise ValueError("finite Decimal has a non-integral exponent") + return exponent + + common_exponent = min(finite_exponent(value) for value in values) + total = 0 + for value in values: + sign, digits, _ = value.as_tuple() + exponent = finite_exponent(value) + coefficient = int("".join(str(digit) for digit in digits) or "0") + if sign: + coefficient = -coefficient + total += coefficient * 10 ** (exponent - common_exponent) + if total == 0: + return Decimal(0) + digits = tuple(int(character) for character in str(abs(total))) + return Decimal((int(total < 0), digits, common_exponent)) + + +@dataclass(frozen=True, slots=True) +class ExternalParticle: + index: int + label: int + name: str + pdg_id: int + state: Literal["incoming", "outgoing"] + momentum_slot: int + + def __post_init__(self) -> None: + for name in ("index", "label", "momentum_slot"): + value = getattr(self, name) + if isinstance(value, bool) or not isinstance(value, int) or value < 0: + raise ValueError(f"external particle {name} must be non-negative") + if not self.name: + raise ValueError("external particle name must not be empty") + if isinstance(self.pdg_id, bool) or not isinstance(self.pdg_id, int): + raise ValueError("external particle pdg_id must be an integer") + if self.state not in ("incoming", "outgoing"): + raise ValueError("external particle state must be 'incoming' or 'outgoing'") + + +@dataclass(frozen=True, slots=True) +class HelicityConfiguration: + id: str + index: int + values: tuple[int, ...] + computed: bool + structural_zero: bool + representative_id: str + coefficient: float + + def __post_init__(self) -> None: + if not self.id or not self.representative_id: + raise ValueError("helicity IDs must not be empty") + if isinstance(self.index, bool) or self.index < 0: + raise ValueError("helicity index must be non-negative") + values = tuple(self.values) + if not all( + isinstance(value, int) and not isinstance(value, bool) for value in values + ): + raise ValueError("helicity values must be integers") + if not math.isfinite(self.coefficient): + raise ValueError("helicity coefficient must be finite") + object.__setattr__(self, "values", values) + + +@dataclass(frozen=True, slots=True) +class ColorFlow: + id: str + index: int + word: tuple[int, ...] + computed: bool + representative_id: str + coefficient: float + + def __post_init__(self) -> None: + if not self.id or not self.representative_id: + raise ValueError("color-flow IDs must not be empty") + if isinstance(self.index, bool) or self.index < 0: + raise ValueError("color-flow index must be non-negative") + word = tuple(self.word) + if not all( + isinstance(value, int) and not isinstance(value, bool) and value >= 0 + for value in word + ): + raise ValueError("color-flow words must contain non-negative integers") + if not math.isfinite(self.coefficient): + raise ValueError("color-flow coefficient must be finite") + object.__setattr__(self, "word", word) + + +@dataclass(frozen=True, slots=True) +class ContractedColorComponent: + id: str + index: int + description: str + + def __post_init__(self) -> None: + if not self.id or not self.description: + raise ValueError("contracted-color metadata must not be empty") + if isinstance(self.index, bool) or self.index < 0: + raise ValueError("contracted-color index must be non-negative") + + +@dataclass(frozen=True, slots=True) +class ModelParameter: + name: str + kind: str + default_real: float + default_imaginary: float + mutable: bool + + def __post_init__(self) -> None: + if not self.name or not self.kind: + raise ValueError("model parameter name and kind must not be empty") + if not math.isfinite(self.default_real) or not math.isfinite( + self.default_imaginary + ): + raise ValueError("model parameter defaults must be finite") + + +ColorComponent: TypeAlias = ColorFlow | ContractedColorComponent + + +@dataclass(frozen=True, slots=True) +class ReductionGroup: + id: str + representative_helicity_id: str + representative_color_id: str + physical_helicity_ids: tuple[str, ...] + physical_color_ids: tuple[str, ...] + + def __post_init__(self) -> None: + if not self.id or not self.representative_helicity_id: + raise ValueError("reduction group and representative IDs must not be empty") + if not self.representative_color_id: + raise ValueError("reduction color representative ID must not be empty") + helicities = tuple(self.physical_helicity_ids) + colors = tuple(self.physical_color_ids) + if not helicities or not colors: + raise ValueError("reduction groups require physical helicity and color IDs") + if len(set(helicities)) != len(helicities) or len(set(colors)) != len(colors): + raise ValueError("reduction group member IDs must be unique") + if self.representative_helicity_id not in helicities: + raise ValueError("helicity representative must be a physical group member") + if self.representative_color_id not in colors: + raise ValueError("color representative must be a physical group member") + object.__setattr__(self, "physical_helicity_ids", helicities) + object.__setattr__(self, "physical_color_ids", colors) + + +@dataclass(frozen=True, slots=True) +class PhysicsReduction: + kind: Literal["lc-diagonal", "contracted-color"] + groups: tuple[ReductionGroup, ...] + + def __post_init__(self) -> None: + if self.kind not in ("lc-diagonal", "contracted-color"): + raise ValueError("unsupported physics reduction kind") + groups = tuple(self.groups) + if not all(isinstance(group, ReductionGroup) for group in groups): + raise TypeError("physics reduction contains invalid groups") + if len({group.id for group in groups}) != len(groups): + raise ValueError("physics reduction group IDs must be unique") + object.__setattr__(self, "groups", groups) + + +@dataclass(frozen=True, slots=True) +class GenerationPlan: + """Dry-run result with concrete requests, coverage, and effective settings.""" + + concrete_processes: tuple[ProcessRequest, ...] + estimated_coverage: Mapping[str, object] + requested_settings: RunConfig | GenerationConfig + effective_settings: RunConfig | GenerationConfig + adjustments: tuple[ConfigClamp, ...] = () + unsupported_features: tuple[str, ...] = () + + def __post_init__(self) -> None: + concrete_processes = tuple(self.concrete_processes) + if not all( + isinstance(process, ProcessRequest) for process in concrete_processes + ): + raise TypeError("concrete_processes must contain ProcessRequest objects") + for name in ("requested_settings", "effective_settings"): + if not isinstance(getattr(self, name), (RunConfig, GenerationConfig)): + raise TypeError(f"{name} must be RunConfig or GenerationConfig") + adjustments = tuple(self.adjustments) + if not all(isinstance(adjustment, ConfigClamp) for adjustment in adjustments): + raise TypeError("adjustments must contain ConfigClamp objects") + unsupported_features = tuple(self.unsupported_features) + if not all( + isinstance(feature, str) and feature for feature in unsupported_features + ): + raise ValueError("unsupported_features must contain non-empty strings") + object.__setattr__(self, "concrete_processes", concrete_processes) + object.__setattr__( + self, "estimated_coverage", _freeze_mapping(self.estimated_coverage) + ) + object.__setattr__(self, "adjustments", adjustments) + object.__setattr__(self, "unsupported_features", unsupported_features) + + +@dataclass(frozen=True, slots=True) +class GenerationResult: + """Location and process inventory of a successfully written artifact.""" + + output: Path + processes: ProcessSet + mode: Literal["error", "append", "replace"] + schema_version: int = 3 + files: tuple[Path, ...] = () + + def __post_init__(self) -> None: + try: + output = Path(os.fspath(self.output)).expanduser().resolve(strict=False) + files = tuple( + Path(os.fspath(path)).expanduser().resolve(strict=False) + for path in self.files + ) + except TypeError as exc: + raise ValueError("generation result paths must be path-like") from exc + object.__setattr__(self, "output", output) + if self.mode not in ("error", "append", "replace"): + raise ValueError(f"invalid generation mode {self.mode!r}") + if self.schema_version != 3: + raise ValueError("generated artifacts must use schema version 3") + object.__setattr__(self, "files", files) + + +@dataclass(frozen=True, slots=True) +class WarmUpResult: + """Metrics from warming one on-the-fly selector family at one f64 point.""" + + elapsed_seconds: float + query_count: int + warmed_query_count: int + current_rss_bytes: int | None + peak_rss_bytes: int | None + already_warm: bool + + def __post_init__(self) -> None: + if ( + isinstance(self.elapsed_seconds, bool) + or not isinstance(self.elapsed_seconds, (int, float)) + or not math.isfinite(float(self.elapsed_seconds)) + or self.elapsed_seconds < 0.0 + ): + raise ValueError("warm-up elapsed_seconds must be finite and non-negative") + object.__setattr__(self, "elapsed_seconds", float(self.elapsed_seconds)) + for name in ("query_count", "warmed_query_count"): + value = getattr(self, name) + if isinstance(value, bool) or not isinstance(value, int) or value < 0: + raise ValueError(f"warm-up {name} must be a non-negative integer") + if self.warmed_query_count > self.query_count: + raise ValueError("warm-up warmed_query_count may not exceed query_count") + for name in ("current_rss_bytes", "peak_rss_bytes"): + value = getattr(self, name) + if value is not None and ( + isinstance(value, bool) or not isinstance(value, int) or value < 0 + ): + raise ValueError( + f"warm-up {name} must be a non-negative integer or null" + ) + if ( + self.current_rss_bytes is not None + and self.peak_rss_bytes is not None + and self.current_rss_bytes > self.peak_rss_bytes + ): + raise ValueError("warm-up current RSS may not exceed peak RSS") + if not isinstance(self.already_warm, bool): + raise TypeError("warm-up already_warm must be a boolean") + if self.already_warm and self.warmed_query_count != 0: + raise ValueError("an already-warm selector family cannot warm new queries") + + +@dataclass(frozen=True, slots=True) +class ProcessPhysics: + """Physical axes, reduction metadata, and selectors for one process. + + LC processes expose physical color flows; NLC/full processes expose one + contracted color component. ``selector_capabilities`` states which axes may + be restricted without changing the generated artifact. + """ + + process_id: str + process: str + color_accuracy: Literal["lc", "nlc", "full"] + helicity_coverage: str + color_coverage: str + color_kind: str + structural_zero_helicity_count: int + external_particles: tuple[ExternalParticle, ...] + helicities: tuple[HelicityConfiguration, ...] + color_flows: tuple[ColorFlow, ...] + contracted_color_components: tuple[ContractedColorComponent, ...] + reduction: PhysicsReduction + model_parameters: tuple[ModelParameter, ...] + selector_capabilities: tuple[str, ...] + + def __post_init__(self) -> None: + if not self.process_id or not self.process: + raise ValueError("process ID and expression must not be empty") + if self.color_accuracy not in ("lc", "nlc", "full"): + raise ValueError("color accuracy must be 'lc', 'nlc', or 'full'") + for name in ("helicity_coverage", "color_coverage", "color_kind"): + if not getattr(self, name): + raise ValueError(f"{name} must not be empty") + if ( + isinstance(self.structural_zero_helicity_count, bool) + or self.structural_zero_helicity_count < 0 + ): + raise ValueError("structural-zero helicity count must be non-negative") + particles = tuple(self.external_particles) + helicities = tuple(self.helicities) + color_flows = tuple(self.color_flows) + contracted = tuple(self.contracted_color_components) + reduction = self.reduction + parameters = tuple(self.model_parameters) + selector_capabilities = tuple(self.selector_capabilities) + expected = ( + ("external_particles", particles, ExternalParticle), + ("helicities", helicities, HelicityConfiguration), + ("color_flows", color_flows, ColorFlow), + ("contracted_color_components", contracted, ContractedColorComponent), + ("model_parameters", parameters, ModelParameter), + ) + for name, values, expected_type in expected: + if not all(isinstance(value, expected_type) for value in values): + raise TypeError(f"{name} contains invalid metadata") + if not helicities: + raise ValueError("physics metadata requires physical helicities") + particle_indices = tuple(particle.index for particle in particles) + if len(set(particle_indices)) != len(particles): + raise ValueError("external particle indices must be unique") + for name, identifiers in ( + ("helicities", tuple(value.id for value in helicities)), + ("color_flows", tuple(value.id for value in color_flows)), + ( + "contracted_color_components", + tuple(value.id for value in contracted), + ), + ): + if len(set(identifiers)) != len(identifiers): + raise ValueError(f"{name} IDs must be unique") + parameter_names = tuple(parameter.name for parameter in parameters) + if len(set(parameter_names)) != len(parameter_names): + raise ValueError("model parameter names must be unique") + if self.color_accuracy == "lc" and not color_flows: + raise ValueError("LC physics metadata requires physical color flows") + if self.color_accuracy != "lc" and len(contracted) != 1: + raise ValueError("NLC/full physics metadata requires one color component") + if not isinstance(reduction, PhysicsReduction): + raise TypeError("reduction must be PhysicsReduction metadata") + expected_reduction = ( + "lc-diagonal" if self.color_accuracy == "lc" else "contracted-color" + ) + if reduction.kind != expected_reduction: + raise ValueError("reduction kind does not match color accuracy") + physical_helicity_ids = {value.id for value in helicities} + physical_color_ids = {value.id for value in color_flows} | { + value.id for value in contracted + } + for group in reduction.groups: + if not set(group.physical_helicity_ids) <= physical_helicity_ids: + raise ValueError("reduction references unknown physical helicities") + if not set(group.physical_color_ids) <= physical_color_ids: + raise ValueError("reduction references unknown physical colors") + if not all(isinstance(value, str) and value for value in selector_capabilities): + raise ValueError("selector_capabilities must contain non-empty strings") + if len(set(selector_capabilities)) != len(selector_capabilities): + raise ValueError("selector_capabilities must be unique") + object.__setattr__(self, "external_particles", particles) + object.__setattr__(self, "helicities", helicities) + object.__setattr__(self, "color_flows", color_flows) + object.__setattr__(self, "contracted_color_components", contracted) + object.__setattr__(self, "model_parameters", parameters) + object.__setattr__(self, "selector_capabilities", selector_capabilities) + + @property + def helicity_ids(self) -> tuple[str, ...]: + return tuple(item.id for item in self.helicities) + + @property + def color_flow_ids(self) -> tuple[str, ...]: + return tuple(item.id for item in self.color_flows) + + @property + def color_ids(self) -> tuple[str, ...]: + components: tuple[ColorComponent, ...] = ( + self.color_flows + if self.color_accuracy == "lc" + else self.contracted_color_components + ) + return tuple(item.id for item in components) + + +@dataclass(frozen=True, slots=True) +class ResolvedEvaluation: + """Physical values shaped ``(point, helicity, color)``. + + :meth:`total` explicitly sums the helicity and color axes for each point and + reproduces the compatibility summed evaluation. + """ + + values: tuple[tuple[tuple[complex | Decimal, ...], ...], ...] + helicity_ids: tuple[str, ...] + color_ids: tuple[str, ...] + color_accuracy: Literal["lc", "nlc", "full"] = "lc" + + def __post_init__(self) -> None: + values = tuple( + tuple( + tuple( + entry if isinstance(entry, Decimal) else complex(entry) + for entry in colors + ) + for colors in helicities + ) + for helicities in self.values + ) + helicity_ids = tuple(self.helicity_ids) + color_ids = tuple(self.color_ids) + expected_colors = len(color_ids) + if self.color_accuracy != "lc" and expected_colors != 1: + raise ValueError("NLC/full resolved output has one contracted color axis") + for point in values: + if len(point) != len(helicity_ids): + raise ValueError( + "resolved values do not match the physical helicity dimension" + ) + if any(len(colors) != expected_colors for colors in point): + raise ValueError( + "resolved values do not match the contracted color dimension" + ) + if self.color_accuracy not in ("lc", "nlc", "full"): + raise ValueError("color_accuracy must be 'lc', 'nlc', or 'full'") + object.__setattr__(self, "values", values) + object.__setattr__(self, "helicity_ids", helicity_ids) + object.__setattr__(self, "color_ids", color_ids) + + @property + def color_flow_ids(self) -> tuple[str, ...]: + return self.color_ids if self.color_accuracy == "lc" else () + + @property + def accuracy(self) -> Literal["lc", "nlc", "full"]: + """Compatibility alias for :attr:`color_accuracy`.""" + + return self.color_accuracy + + @property + def shape(self) -> tuple[int, int, int]: + return (len(self.values), len(self.helicity_ids), len(self.color_ids)) + + def total(self) -> tuple[complex | Decimal, ...]: + """Sum all non-point axes while preserving decimal precision.""" + + totals: list[complex | Decimal] = [] + for point in self.values: + entries = tuple(entry for helicity in point for entry in helicity) + if entries and isinstance(entries[0], Decimal): + if not all(isinstance(entry, Decimal) for entry in entries): + raise TypeError("resolved values must not mix Decimal and complex") + totals.append( + _exact_decimal_sum(tuple(cast(Decimal, entry) for entry in entries)) + ) + else: + totals.append(sum((complex(entry) for entry in entries), start=0j)) + return tuple(totals) + + +@dataclass(frozen=True, slots=True) +class BenchmarkStatistics: + """Distribution summary for repeated benchmark measurements.""" + + standard_deviation: float + standard_error: float + relative_standard_error: float + + def __post_init__(self) -> None: + for name in ( + "standard_deviation", + "standard_error", + "relative_standard_error", + ): + value = getattr(self, name) + if not math.isfinite(value) or value < 0: + raise ValueError(f"benchmark {name} must be finite and non-negative") + + +@dataclass(frozen=True, slots=True) +class BenchmarkComponentTiming: + """Mean per-point time and uncertainty for one runtime-profile component.""" + + mean_seconds_per_point: float + uncertainty: BenchmarkStatistics + sample_count: int + + def __post_init__(self) -> None: + if ( + not math.isfinite(self.mean_seconds_per_point) + or self.mean_seconds_per_point < 0 + ): + raise ValueError( + "benchmark component timing must be finite and non-negative" + ) + if not isinstance(self.uncertainty, BenchmarkStatistics): + raise TypeError( + "benchmark component uncertainty must be BenchmarkStatistics" + ) + if ( + isinstance(self.sample_count, bool) + or not isinstance(self.sample_count, int) + or self.sample_count < 1 + ): + raise ValueError("benchmark component sample_count must be positive") + + +@dataclass(frozen=True, slots=True) +class BenchmarkStageTiming: + """Per-point timings for one native evaluator stage. + + The leaf/backend/output-gather fields are internal attribution, not + additional top-level phases. A full-stage leaf gather is owned by the + evaluator-call envelope; a composed selected-chunk leaf gather is owned by + the input-pack envelope. + """ + + stage_index: int + input_pack_time: BenchmarkComponentTiming | None = None + evaluator_call_time: BenchmarkComponentTiming | None = None + output_assign_time: BenchmarkComponentTiming | None = None + leaf_input_pack_time: BenchmarkComponentTiming | None = None + backend_call_time: BenchmarkComponentTiming | None = None + evaluator_output_gather_time: BenchmarkComponentTiming | None = None + + def __post_init__(self) -> None: + if ( + isinstance(self.stage_index, bool) + or not isinstance(self.stage_index, int) + or self.stage_index < 1 + ): + raise ValueError("benchmark stage_index must be positive") + timings = ( + self.input_pack_time, + self.evaluator_call_time, + self.output_assign_time, + self.leaf_input_pack_time, + self.backend_call_time, + self.evaluator_output_gather_time, + ) + if not any(timing is not None for timing in timings): + raise ValueError("benchmark stage timing must contain at least one value") + if any( + timing is not None and not isinstance(timing, BenchmarkComponentTiming) + for timing in timings + ): + raise TypeError("benchmark stage timings must be BenchmarkComponentTiming") + + +@dataclass(frozen=True, slots=True) +class BenchmarkProfileCounters: + """Mean native profile work counts with an explicit normalization basis. + + Movement and materialization fields are means per profiled phase-space + point. Backend-call, allocation, and compiled Direct-Arena fields are means + per profiled runtime call. The native repeated profiler reports integer + totals; normalizing before aggregating samples keeps these values comparable + across batch sizes and repetition counts. + """ + + sample_count: int + normalization: Literal["mean_per_profiled_point_or_runtime_call_v1"] = ( + "mean_per_profiled_point_or_runtime_call_v1" + ) + native_input_components_per_point: float | None = None + native_input_pack_bytes_per_point: float | None = None + native_input_crossing_bytes_per_point: float | None = None + state_components_per_point: float | None = None + state_clear_components_per_point: float | None = None + source_components_per_point: float | None = None + momentum_components_per_point: float | None = None + model_parameter_components_per_point: float | None = None + stage_input_copy_components_per_point: float | None = None + stage_leaf_input_copy_components_per_point: float | None = None + stage_evaluator_output_gather_components_per_point: float | None = None + stage_output_assign_components_per_point: float | None = None + amplitude_input_copy_components_per_point: float | None = None + amplitude_leaf_input_copy_components_per_point: float | None = None + amplitude_evaluator_output_gather_components_per_point: float | None = None + amplitude_output_remap_components_per_point: float | None = None + reduction_input_components_per_point: float | None = None + selector_gather_points_per_point: float | None = None + selector_gather_bytes_per_point: float | None = None + selector_scatter_values_per_point: float | None = None + resolved_materialized_components_per_point: float | None = None + total_materialized_values_per_point: float | None = None + final_output_copy_values_per_point: float | None = None + native_input_container_allocations_per_call: float | None = None + evaluator_backend_calls_per_call: float | None = None + compiled_direct_arena_engines_per_call: float | None = None + compiled_direct_arena_calls_per_call: float | None = None + compiled_direct_arena_boundary_input_bytes_per_call: float | None = None + compiled_direct_arena_boundary_current_output_bytes_per_call: float | None = None + compiled_direct_arena_boundary_amplitude_output_bytes_per_call: float | None = None + recurrence_momentum_scalar_values_per_point: float | None = None + recurrence_schedule_executions_per_call: float | None = None + recurrence_replay_schedule_executions_per_call: float | None = None + recurrence_union_schedule_executions_per_call: float | None = None + recurrence_union_source_rows_per_call: float | None = None + recurrence_replay_output_values_per_point: float | None = None + recurrence_source_calls_per_call: float | None = None + recurrence_source_rows_per_call: float | None = None + recurrence_contribution_calls_per_call: float | None = None + recurrence_contribution_rows_per_call: float | None = None + recurrence_finalization_calls_per_call: float | None = None + recurrence_finalization_rows_per_call: float | None = None + recurrence_closure_calls_per_call: float | None = None + recurrence_closure_rows_per_call: float | None = None + observed_scratch_reallocations_per_call: float | None = None + native_output_allocations_per_call: float | None = None + + def __post_init__(self) -> None: + if ( + isinstance(self.sample_count, bool) + or not isinstance(self.sample_count, int) + or self.sample_count < 1 + ): + raise ValueError("benchmark profile counter sample_count must be positive") + if self.normalization != "mean_per_profiled_point_or_runtime_call_v1": + raise ValueError("benchmark profile counter normalization is invalid") + for name in ( + "native_input_components_per_point", + "native_input_pack_bytes_per_point", + "native_input_crossing_bytes_per_point", + "state_components_per_point", + "state_clear_components_per_point", + "source_components_per_point", + "momentum_components_per_point", + "model_parameter_components_per_point", + "stage_input_copy_components_per_point", + "stage_leaf_input_copy_components_per_point", + "stage_evaluator_output_gather_components_per_point", + "stage_output_assign_components_per_point", + "amplitude_input_copy_components_per_point", + "amplitude_leaf_input_copy_components_per_point", + "amplitude_evaluator_output_gather_components_per_point", + "amplitude_output_remap_components_per_point", + "reduction_input_components_per_point", + "selector_gather_points_per_point", + "selector_gather_bytes_per_point", + "selector_scatter_values_per_point", + "resolved_materialized_components_per_point", + "total_materialized_values_per_point", + "final_output_copy_values_per_point", + "native_input_container_allocations_per_call", + "evaluator_backend_calls_per_call", + "compiled_direct_arena_engines_per_call", + "compiled_direct_arena_calls_per_call", + "compiled_direct_arena_boundary_input_bytes_per_call", + "compiled_direct_arena_boundary_current_output_bytes_per_call", + "compiled_direct_arena_boundary_amplitude_output_bytes_per_call", + "recurrence_momentum_scalar_values_per_point", + "recurrence_schedule_executions_per_call", + "recurrence_replay_schedule_executions_per_call", + "recurrence_union_schedule_executions_per_call", + "recurrence_union_source_rows_per_call", + "recurrence_replay_output_values_per_point", + "recurrence_source_calls_per_call", + "recurrence_source_rows_per_call", + "recurrence_contribution_calls_per_call", + "recurrence_contribution_rows_per_call", + "recurrence_finalization_calls_per_call", + "recurrence_finalization_rows_per_call", + "recurrence_closure_calls_per_call", + "recurrence_closure_rows_per_call", + "observed_scratch_reallocations_per_call", + "native_output_allocations_per_call", + ): + value = getattr(self, name) + if value is not None and ( + not isinstance(value, (float, int)) + or isinstance(value, bool) + or not math.isfinite(float(value)) + or value < 0 + ): + raise ValueError( + f"benchmark profile counter {name} must be finite and non-negative" + ) + + +@dataclass(frozen=True, slots=True) +class BenchmarkTimingBreakdown: + """Typed aggregate of bounded native Rusticol profile samples. + + Internal leaf/backend/output-gather/remap fields explain their enclosing + work but are not additive top-level phases. Leaf gathering is owned by the + evaluator envelope for full stages and by the input-pack envelope for + composed selected-chunk paths. The recurrence schedule is an inclusive + top-level phase; its source-kernel, contribution-kernel, finalization, and + closure fields are sub-attribution and must not be added to it. + """ + + sample_count: int + execution_mode: Literal["compiled", "eager", "recurrence", "on-the-fly"] = ( + "compiled" + ) + wall_time: BenchmarkComponentTiming | None = None + source_fill_time: BenchmarkComponentTiming | None = None + momentum_setup_time: BenchmarkComponentTiming | None = None + stage_input_pack_time: BenchmarkComponentTiming | None = None + stage_evaluator_call_time: BenchmarkComponentTiming | None = None + output_assign_time: BenchmarkComponentTiming | None = None + amplitude_input_pack_time: BenchmarkComponentTiming | None = None + amplitude_evaluator_call_time: BenchmarkComponentTiming | None = None + reduction_time: BenchmarkComponentTiming | None = None + other_core_time: BenchmarkComponentTiming | None = None + eager_execution_time: BenchmarkComponentTiming | None = None + eager_initialize_time: BenchmarkComponentTiming | None = None + eager_gather_time: BenchmarkComponentTiming | None = None + eager_kernel_call_time: BenchmarkComponentTiming | None = None + eager_invocation_scatter_time: BenchmarkComponentTiming | None = None + eager_finalization_time: BenchmarkComponentTiming | None = None + eager_scatter_finalization_time: BenchmarkComponentTiming | None = None + eager_closure_time: BenchmarkComponentTiming | None = None + eager_copy_out_time: BenchmarkComponentTiming | None = None + stages: tuple[BenchmarkStageTiming, ...] = () + native_input_pack_time: BenchmarkComponentTiming | None = None + native_input_crossing_time: BenchmarkComponentTiming | None = None + orchestration_time: BenchmarkComponentTiming | None = None + state_prepare_time: BenchmarkComponentTiming | None = None + state_clear_time: BenchmarkComponentTiming | None = None + momentum_input_setup_time: BenchmarkComponentTiming | None = None + model_parameter_setup_time: BenchmarkComponentTiming | None = None + total_materialization_time: BenchmarkComponentTiming | None = None + final_output_copy_time: BenchmarkComponentTiming | None = None + selector_planner_time: BenchmarkComponentTiming | None = None + selector_gather_time: BenchmarkComponentTiming | None = None + selector_scatter_time: BenchmarkComponentTiming | None = None + recurrence_momentum_fill_time: BenchmarkComponentTiming | None = None + recurrence_union_source_fill_time: BenchmarkComponentTiming | None = None + recurrence_schedule_time: BenchmarkComponentTiming | None = None + recurrence_source_kernel_time: BenchmarkComponentTiming | None = None + recurrence_contribution_kernel_time: BenchmarkComponentTiming | None = None + recurrence_finalization_time: BenchmarkComponentTiming | None = None + recurrence_closure_time: BenchmarkComponentTiming | None = None + recurrence_replay_output_mapping_time: BenchmarkComponentTiming | None = None + counters: BenchmarkProfileCounters | None = None + stage_leaf_input_pack_time: BenchmarkComponentTiming | None = None + stage_backend_call_time: BenchmarkComponentTiming | None = None + stage_evaluator_output_gather_time: BenchmarkComponentTiming | None = None + amplitude_leaf_input_pack_time: BenchmarkComponentTiming | None = None + amplitude_backend_call_time: BenchmarkComponentTiming | None = None + amplitude_evaluator_output_gather_time: BenchmarkComponentTiming | None = None + amplitude_output_remap_time: BenchmarkComponentTiming | None = None + + def __post_init__(self) -> None: + if ( + isinstance(self.sample_count, bool) + or not isinstance(self.sample_count, int) + or self.sample_count < 1 + ): + raise ValueError("benchmark timing breakdown sample_count must be positive") + if self.execution_mode not in { + "compiled", + "eager", + "recurrence", + "on-the-fly", + }: + raise ValueError( + "benchmark timing breakdown execution_mode must be compiled, eager, " + "recurrence, or on-the-fly" + ) + for name in ( + "wall_time", + "native_input_pack_time", + "native_input_crossing_time", + "orchestration_time", + "state_prepare_time", + "state_clear_time", + "source_fill_time", + "momentum_input_setup_time", + "momentum_setup_time", + "model_parameter_setup_time", + "stage_input_pack_time", + "stage_evaluator_call_time", + "stage_leaf_input_pack_time", + "stage_backend_call_time", + "stage_evaluator_output_gather_time", + "output_assign_time", + "amplitude_input_pack_time", + "amplitude_evaluator_call_time", + "amplitude_leaf_input_pack_time", + "amplitude_backend_call_time", + "amplitude_evaluator_output_gather_time", + "amplitude_output_remap_time", + "reduction_time", + "total_materialization_time", + "final_output_copy_time", + "selector_planner_time", + "selector_gather_time", + "selector_scatter_time", + "other_core_time", + "eager_execution_time", + "eager_initialize_time", + "eager_gather_time", + "eager_kernel_call_time", + "eager_invocation_scatter_time", + "eager_finalization_time", + "eager_scatter_finalization_time", + "eager_closure_time", + "eager_copy_out_time", + "recurrence_momentum_fill_time", + "recurrence_union_source_fill_time", + "recurrence_schedule_time", + "recurrence_source_kernel_time", + "recurrence_contribution_kernel_time", + "recurrence_finalization_time", + "recurrence_closure_time", + "recurrence_replay_output_mapping_time", + ): + value = getattr(self, name) + if value is not None and not isinstance(value, BenchmarkComponentTiming): + raise TypeError( + f"benchmark timing breakdown {name} must be " + "BenchmarkComponentTiming or null" + ) + stages = tuple(self.stages) + if not all(isinstance(stage, BenchmarkStageTiming) for stage in stages): + raise TypeError("benchmark timing breakdown stages are invalid") + indices = tuple(stage.stage_index for stage in stages) + if len(indices) != len(set(indices)): + raise ValueError("benchmark timing breakdown stage indices must be unique") + if self.counters is not None and not isinstance( + self.counters, BenchmarkProfileCounters + ): + raise TypeError( + "benchmark timing breakdown counters must be " + "BenchmarkProfileCounters or null" + ) + object.__setattr__(self, "stages", stages) + + +@dataclass(frozen=True, slots=True) +class BenchmarkResult: + """Per-point timings and uncertainty across independent measured blocks. + + ``sample_count`` is the number of timed blocks. Each block averages + ``repetitions_per_sample`` runtime calls of ``effective_config.batch_size`` + points. For native f64 evaluation, ``wall_time_per_point`` is measured by + Rusticol around repeated core evaluations of an already packed momentum + buffer; caller-language conversion and adapter overhead are excluded. + For compiled/eager execution, ``evaluator_time_per_point`` is the relevant + evaluator envelope measured by the bounded native profiler. For recurrence + and on-the-fly execution, it is the inclusive recurrence schedule measured + by the paired profiled pass. ``evaluator_total_time_per_point`` is the + accumulated warmed headline evaluator duration divided by its authenticated + measured-point count; it is distinct from both wall-sample statistics and + execution attribution. + ``interrupted`` marks a valid partial result computed only from timing blocks + that finished before sampling was interrupted. + """ + + requested_config: BenchmarkConfig + effective_config: BenchmarkConfig + sample_count: int + wall_time_per_point: float + evaluator_time_per_point: float | None + uncertainty: BenchmarkStatistics + environment: Mapping[str, object] + repetitions_per_sample: int = 1 + evaluator_uncertainty: BenchmarkStatistics | None = None + process_id: str | None = None + process_expression: str | None = None + timing_breakdown: BenchmarkTimingBreakdown | None = None + interrupted: bool = False + evaluator_total_time_per_point: float | None = None + + def __post_init__(self) -> None: + if ( + isinstance(self.sample_count, bool) + or not isinstance(self.sample_count, int) + or self.sample_count < 1 + ): + raise ValueError("benchmark sample_count must be positive") + if not isinstance(self.interrupted, bool): + raise TypeError("benchmark interrupted flag must be a boolean") + if not math.isfinite(self.wall_time_per_point) or self.wall_time_per_point < 0: + raise ValueError("benchmark wall_time_per_point must be non-negative") + if self.evaluator_time_per_point is not None and ( + not math.isfinite(self.evaluator_time_per_point) + or self.evaluator_time_per_point < 0 + ): + raise ValueError("benchmark evaluator_time_per_point must be non-negative") + if self.evaluator_total_time_per_point is not None and ( + not math.isfinite(self.evaluator_total_time_per_point) + or self.evaluator_total_time_per_point <= 0 + ): + raise ValueError( + "benchmark evaluator_total_time_per_point must be positive" + ) + if ( + isinstance(self.repetitions_per_sample, bool) + or not isinstance(self.repetitions_per_sample, int) + or self.repetitions_per_sample < 1 + ): + raise ValueError("benchmark repetitions_per_sample must be positive") + if self.evaluator_uncertainty is not None and not isinstance( + self.evaluator_uncertainty, BenchmarkStatistics + ): + raise TypeError( + "benchmark evaluator_uncertainty must be BenchmarkStatistics or null" + ) + for name in ("process_id", "process_expression"): + value = getattr(self, name) + if value is not None and (not isinstance(value, str) or not value): + raise ValueError(f"benchmark {name} must be a non-empty string or null") + if self.timing_breakdown is not None and not isinstance( + self.timing_breakdown, BenchmarkTimingBreakdown + ): + raise TypeError( + "benchmark timing_breakdown must be BenchmarkTimingBreakdown or null" + ) + object.__setattr__(self, "environment", _freeze_mapping(self.environment)) + + @property + def evaluation_count(self) -> int: + """Return the number of timed runtime evaluations.""" + + return self.sample_count * self.repetitions_per_sample + + @property + def evaluated_point_count(self) -> int: + """Return the number of phase-space point evaluations timed.""" + + return self.evaluation_count * self.effective_config.batch_size + + +__all__ = [ + "BenchmarkComponentTiming", + "BenchmarkProfileCounters", + "BenchmarkResult", + "BenchmarkStageTiming", + "BenchmarkStatistics", + "BenchmarkTimingBreakdown", + "ColorComponent", + "ColorFlow", + "ContractedColorComponent", + "ExternalParticle", + "GenerationPlan", + "GenerationResult", + "HelicityConfiguration", + "ModelParameter", + "PhysicsReduction", + "ProcessPhysics", + "ReductionGroup", + "ResolvedEvaluation", + "WarmUpResult", +] diff --git a/pyamplicol/src/pyamplicol/api/services.py b/pyamplicol/src/pyamplicol/api/services.py new file mode 100644 index 0000000000..87df2a81c9 --- /dev/null +++ b/pyamplicol/src/pyamplicol/api/services.py @@ -0,0 +1,1016 @@ +# SPDX-License-Identifier: 0BSD +from __future__ import annotations + +import importlib +import inspect +import os +from collections.abc import Callable, Iterable, Mapping, Sequence +from decimal import Decimal +from fractions import Fraction +from pathlib import Path +from typing import TYPE_CHECKING, Any, Literal, cast + +import pyamplicol as _pyamplicol +from pyamplicol.config import ( + Action, + BenchmarkConfig, + ConfigResolution, + GenerationConfig, + RunConfig, +) +from pyamplicol.reporting import ProgressSink + +from .errors import ( + ArtifactError, + CompatibilityError, + DependencyError, + EvaluationError, + GenerationError, +) +from .protocols import ( + BenchmarkBackend, + BenchmarkFactory, + GeneratorBackend, + GeneratorFactory, + ModelParameters, + Momenta, + RuntimeBackend, + RuntimeLoader, +) +from .requests import ModelSource, ProcessRequest, ProcessSet +from .results import ( + BenchmarkResult, + ColorFlow, + GenerationPlan, + GenerationResult, + HelicityConfiguration, + ProcessPhysics, + ResolvedEvaluation, + WarmUpResult, +) + +_generator_factory: GeneratorFactory | None = None +_runtime_loader: RuntimeLoader | None = None +_benchmark_factory: BenchmarkFactory | None = None + +if TYPE_CHECKING: + from pyamplicol.runtime.correlations import CorrelatorCatalogue, CorrelatorEvaluator + + +def install_backend_factories( + *, + generator: GeneratorFactory | None = None, + runtime: RuntimeLoader | None = None, + benchmark: BenchmarkFactory | None = None, +) -> None: + """Install backend adapters without importing domain dependencies eagerly.""" + + global _generator_factory, _runtime_loader, _benchmark_factory + if generator is not None: + _generator_factory = generator + if runtime is not None: + _runtime_loader = runtime + if benchmark is not None: + _benchmark_factory = benchmark + + +def _discover(attribute: str, modules: Sequence[str]) -> Any: + failures: list[str] = [] + for module_name in modules: + try: + module = importlib.import_module(module_name) + except ImportError as exc: + failures.append(f"{module_name}: {exc}") + continue + factory = getattr(module, attribute, None) + if factory is not None: + return factory + failures.append(f"{module_name}: missing {attribute}") + detail = "; ".join(failures) + raise DependencyError( + f"no pyAmpliCol backend provides {attribute}; backend discovery tried {detail}" + ) + + +def _get_generator_factory() -> GeneratorFactory: + if _generator_factory is not None: + return _generator_factory + return cast( + GeneratorFactory, + _discover( + "create_generator_backend", + ("pyamplicol.generation", "pyamplicol.generation.service"), + ), + ) + + +def _generation_resource_resolution( + config: GenerationConfig | RunConfig | ConfigResolution | None, +) -> ConfigResolution: + from pyamplicol import licensing + + if isinstance(config, ConfigResolution): + effective = config.effective + else: + effective = ( + RunConfig(action=Action.GENERATE) + if config is None + else RunConfig(action=Action.GENERATE, generation=config) + if isinstance(config, GenerationConfig) + else config + ) + state = licensing.detect_symbolica_license( + suggest=effective.symbolica.suggest_license, + json_mode=str(effective.output.format) == "json", + ) + return licensing.resolve_symbolica_resource_config( + config, + state, + ) + + +def _get_runtime_loader() -> RuntimeLoader: + if _runtime_loader is not None: + return _runtime_loader + return cast( + RuntimeLoader, + _discover( + "load_runtime_backend", + ("pyamplicol.artifact", "pyamplicol.runtime"), + ), + ) + + +def _get_benchmark_factory() -> BenchmarkFactory: + if _benchmark_factory is not None: + return _benchmark_factory + return cast( + BenchmarkFactory, + _discover( + "create_benchmark_backend", + ("pyamplicol.benchmarking", "pyamplicol.benchmark"), + ), + ) + + +def _process_set( + processes: ProcessSet | ProcessRequest | str | Iterable[ProcessRequest | str], +) -> ProcessSet: + if isinstance(processes, ProcessSet): + return processes + if isinstance(processes, ProcessRequest): + return ProcessSet((processes,)) + if isinstance(processes, str): + return ProcessSet((ProcessRequest.parse(processes),)) + requests = tuple( + entry if isinstance(entry, ProcessRequest) else ProcessRequest.parse(entry) + for entry in processes + ) + return ProcessSet(requests) + + +def _validate_progress(progress: ProgressSink | None) -> None: + if progress is not None and not isinstance(progress, ProgressSink): + raise TypeError("progress must implement ProgressSink.emit(event)") + + +def _validate_precision(precision: int) -> int: + if isinstance(precision, bool) or not isinstance(precision, int): + raise TypeError("precision must be a positive integer number of decimal digits") + if precision < 1: + raise ValueError( + "precision must be a positive integer number of decimal digits" + ) + return precision + + +def _selector_ids( + values: Sequence[str | HelicityConfiguration | ColorFlow] | None, + *, + expected_type: type[HelicityConfiguration] | type[ColorFlow], + name: str, +) -> tuple[str, ...] | None: + if values is None: + return None + identifiers: list[str] = [] + for value in values: + if isinstance(value, str) and value: + identifiers.append(value) + elif isinstance(value, expected_type): + identifiers.append(value.id) + else: + raise TypeError(f"{name} selectors must be IDs or {expected_type.__name__}") + if len(set(identifiers)) != len(identifiers): + raise ValueError(f"{name} selectors must be unique") + return tuple(identifiers) or None + + +def _point_selector_ids( + values: Sequence[str | HelicityConfiguration | ColorFlow] | None, + *, + expected_type: type[HelicityConfiguration] | type[ColorFlow], + name: str, +) -> tuple[str, ...] | None: + if values is None: + return None + identifiers: list[str] = [] + for value in values: + if isinstance(value, str) and value: + identifiers.append(value) + elif isinstance(value, expected_type): + identifiers.append(value.id) + else: + raise TypeError(f"{name} selectors must be IDs or {expected_type.__name__}") + return tuple(identifiers) or None + + +def _accepts_keyword_arguments(operation: object, *names: str) -> bool: + if not callable(operation): + return False + try: + parameters = inspect.signature(operation).parameters.values() + except (TypeError, ValueError): + return False + declared = {parameter.name for parameter in parameters} + accepts_arbitrary = any( + parameter.kind is inspect.Parameter.VAR_KEYWORD for parameter in parameters + ) + return accepts_arbitrary or all(name in declared for name in names) + + +class Generator: + """Plan and generate process artifacts with typed configuration. + + A :class:`~pyamplicol.config.ConfigResolution` preserves both requested and + effective settings, including license/resource clamps, in the output. + """ + + def __init__( + self, + config: GenerationConfig | RunConfig | ConfigResolution | None = None, + progress: ProgressSink | None = None, + ) -> None: + if config is not None and not isinstance( + config, (GenerationConfig, RunConfig, ConfigResolution) + ): + raise TypeError( + "Generator config must be GenerationConfig, RunConfig, " + "ConfigResolution, or null" + ) + _validate_progress(progress) + self._config = config + self._progress = progress + self._backend: GeneratorBackend | None = None + + def _implementation(self) -> GeneratorBackend: + if self._backend is None: + self._backend = _get_generator_factory()(self._config, self._progress) + return self._backend + + def _resolve_generation_resources(self) -> None: + resource_config = _generation_resource_resolution(self._config) + if resource_config != self._config: + self._config = resource_config + self._backend = None + + def plan( + self, + processes: ProcessSet | ProcessRequest | str | Iterable[ProcessRequest | str], + *, + model: ModelSource | _pyamplicol.CompiledModel | None = None, + ) -> GenerationPlan: + """Resolve concrete processes and coverage without writing an artifact.""" + + process_set = _process_set(processes) + self._resolve_generation_resources() + result = self._implementation().plan(process_set, model=model) + if not isinstance(result, GenerationPlan): + raise GenerationError( + "generator backend returned an invalid GenerationPlan" + ) + return result + + def generate( + self, + processes: ProcessSet | ProcessRequest | str | Iterable[ProcessRequest | str], + output: os.PathLike[str] | str, + *, + model: ModelSource | _pyamplicol.CompiledModel | None = None, + mode: Literal["error", "append", "replace"] = "error", + correlators: _pyamplicol.CorrelatorConfig | None = None, + ) -> GenerationResult: + """Generate an artifact in ``error``, ``append``, or ``replace`` mode.""" + + if mode not in ("error", "append", "replace"): + raise ValueError("generation mode must be 'error', 'append', or 'replace'") + process_set = _process_set(processes) + destination = Path(os.fspath(output)).expanduser().resolve(strict=False) + if destination.exists() and not destination.is_dir(): + raise GenerationError( + f"artifact destination is not a directory: {destination}" + ) + if mode == "error" and destination.exists(): + raise FileExistsError(f"artifact already exists: {destination}") + if mode == "append" and not destination.is_dir(): + raise FileNotFoundError(f"cannot append to missing artifact: {destination}") + self._resolve_generation_resources() + if correlators is not None: + from pyamplicol.generation.correlated import generate_correlated + + return generate_correlated( + process_set, + destination, + model=model, + mode=mode, + config=self._config, + progress=self._progress, + declarations=correlators, + ) + result = self._implementation().generate( + process_set, destination, model=model, mode=mode + ) + if not isinstance(result, GenerationResult): + raise GenerationError( + "generator backend returned an invalid GenerationResult" + ) + return result + + +class Runtime: + """Typed Python facade for one process in a generated Rusticol artifact.""" + + def __init__(self, backend: RuntimeBackend) -> None: + if not isinstance(backend, RuntimeBackend): + raise TypeError("Runtime backend does not implement RuntimeBackend") + self._backend = backend + + @classmethod + def load( + cls, + artifact: os.PathLike[str] | str, + *, + process: str | None = None, + model_parameters: ModelParameters | None = None, + mute_warnings: bool = False, + ) -> Runtime: + """Load one process by stable ID, alias ID, or exact expression. + + ``model_parameters`` is applied atomically before the runtime is + returned. Omit ``process`` only for a single-process artifact or to use + the artifact's declared default. + """ + + path = Path(os.fspath(artifact)).expanduser().resolve(strict=False) + parameters = dict(model_parameters) if model_parameters is not None else None + backend = _get_runtime_loader()( + path, + process=process, + model_parameters=parameters, + mute_warnings=mute_warnings, + ) + return backend if isinstance(backend, cls) else cls(backend) + + @property + def physics(self) -> ProcessPhysics: + """Return the complete public process metadata. + + On-the-fly runtimes materialize their compact helicity and color-flow + axes on first access and retain that compatibility view for the life of + the runtime. Use :meth:`inspect` for compact high-multiplicity runtime + metadata and cache-state observation. + """ + + result = self._backend.physics + if not isinstance(result, ProcessPhysics): + raise EvaluationError("runtime backend returned invalid process physics") + return result + + @property + def artifact_id(self) -> str: + """Identity of the artifact manifest authenticated by the native loader.""" + + try: + value = getattr(self._backend, "artifact_id", None) + except ArtifactError as error: + raise EvaluationError( + "runtime backend does not expose an authenticated artifact identity" + ) from error + if ( + not isinstance(value, str) + or len(value) != 64 + or value != value.lower() + or any(character not in "0123456789abcdef" for character in value) + ): + raise EvaluationError( + "runtime backend does not expose an authenticated artifact identity" + ) + return value + + @property + def execution_mode( + self, + ) -> Literal["compiled", "eager", "recurrence", "on-the-fly"]: + """Native execution lane authenticated while loading the artifact.""" + + value = getattr(self._backend, "execution_mode", None) + if value not in {"compiled", "eager", "recurrence", "on-the-fly"}: + raise EvaluationError( + "runtime backend does not expose a valid execution mode" + ) + return cast(Literal["compiled", "eager", "recurrence", "on-the-fly"], value) + + def inspect(self) -> Mapping[str, object]: + """Return compact authenticated runtime metadata and live cache state. + + This optional facade capability does not form part of the minimum + :class:`RuntimeBackend` protocol. The built-in backend implements it + without opening dense process physics. + """ + + operation = getattr(self._backend, "inspect", None) + if not callable(operation): + raise CompatibilityError( + "runtime backend does not expose compact runtime inspection" + ) + result = operation() + if not isinstance(result, Mapping): + raise EvaluationError("runtime backend returned invalid runtime inspection") + return dict(result) + + def save(self, path: os.PathLike[str] | str) -> None: + """Save the retained on-the-fly recursion after completed evaluations. + + This saves structural warm-up work, not momenta, evaluated matrix + elements, or model-parameter values. Load the same process normally, + then use :meth:`load_cache` to restore it. Only the currently retained + selector family is saved; other selectors keep their usual cold path. + """ + + operation = getattr(self._backend, "save", None) + if not callable(operation): + raise CompatibilityError( + "runtime backend does not support saving on-the-fly caches" + ) + operation(Path(os.fspath(path)).expanduser().resolve(strict=False)) + + def load_cache(self, path: os.PathLike[str] | str) -> None: + """Restore a saved on-the-fly recursion into the same loaded process. + + The current model parameters remain unchanged. A failed restore leaves + the existing cache intact. Restoring does not specialize the process: + new momenta, batch sizes and selector families remain supported. + """ + + operation = getattr(self._backend, "load_cache", None) + if not callable(operation): + raise CompatibilityError( + "runtime backend does not support restoring on-the-fly caches" + ) + operation(Path(os.fspath(path)).expanduser().resolve(strict=False)) + + def evaluate( + self, + momenta: Momenta, + *, + helicities: Sequence[str | HelicityConfiguration] | None = None, + color_flows: Sequence[str | ColorFlow] | None = None, + helicity_by_point: Sequence[str | HelicityConfiguration] | None = None, + color_flow_by_point: Sequence[str | ColorFlow] | None = None, + precision: int = 16, + ) -> tuple[complex | Decimal, ...]: + """Return one fully summed matrix element for every input point. + + Selectors accept stable string IDs or the typed objects exposed by + :attr:`physics`. Precision 16 uses the native f64 path; larger values + use the retained high-precision evaluator state when available. + """ + + helicity_ids = _selector_ids( + helicities, + expected_type=HelicityConfiguration, + name="helicity", + ) + color_ids = _selector_ids( + color_flows, + expected_type=ColorFlow, + name="color-flow", + ) + point_helicity_ids = _point_selector_ids( + helicity_by_point, + expected_type=HelicityConfiguration, + name="per-point helicity", + ) + point_color_ids = _point_selector_ids( + color_flow_by_point, + expected_type=ColorFlow, + name="per-point color-flow", + ) + if helicity_ids is not None and point_helicity_ids is not None: + raise ValueError("helicities and helicity_by_point are mutually exclusive") + if color_ids is not None and point_color_ids is not None: + raise ValueError( + "color_flows and color_flow_by_point are mutually exclusive" + ) + precision = _validate_precision(precision) + if point_helicity_ids is None and point_color_ids is None: + values = self._backend.evaluate( + momenta, + helicities=helicity_ids, + color_flows=color_ids, + precision=precision, + ) + else: + operation = self._backend.evaluate + if not _accepts_keyword_arguments( + operation, + "helicity_by_point", + "color_flow_by_point", + ): + raise EvaluationError( + "runtime backend does not support per-point helicity/color-flow " + "selectors; use batch-global selectors or install a " + "selector-capable backend" + ) + values = cast(Any, operation)( + momenta, + helicities=helicity_ids, + color_flows=color_ids, + helicity_by_point=point_helicity_ids, + color_flow_by_point=point_color_ids, + precision=precision, + ) + return tuple( + value if isinstance(value, Decimal) else complex(value) for value in values + ) + + def warm_up( + self, + momenta: Momenta, + *, + precision: int = 16, + helicities: Sequence[str | HelicityConfiguration] | None = None, + color_flows: Sequence[str | ColorFlow] | None = None, + progress: ProgressSink | None = None, + n_cores: int | None = None, + ) -> WarmUpResult: + """Warm one on-the-fly selector family using exactly one f64 point. + + This is an explicit structural warm-up, not a throughput evaluation: + batches and high-precision execution are intentionally rejected. LC + accepts the same helicity and color-flow selectors as :meth:`evaluate`; + contracted NLC/full execution accepts only helicity selectors. + ``n_cores`` overrides the process's query-construction worker count for + this call only; omitted, it uses the generation-time setting. Changing + the count neither discards a warm family nor changes evaluation threads. + """ + + if n_cores is not None: + if isinstance(n_cores, bool) or not isinstance(n_cores, int): + raise TypeError("n_cores must be a positive integer or None") + if n_cores < 1: + raise ValueError("n_cores must be a positive integer or None") + try: + point_count = len(momenta) + except TypeError as exc: + raise TypeError("warm_up momenta must be a sized sequence") from exc + if point_count != 1: + raise ValueError( + "warm_up requires exactly one phase-space point; " + f"received {point_count}" + ) + precision = _validate_precision(precision) + if precision != 16: + raise CompatibilityError( + "on-the-fly warm_up supports only precision=16 (native f64); " + f"received precision={precision}" + ) + _validate_progress(progress) + if self.execution_mode != "on-the-fly": + raise CompatibilityError( + "warm_up is available only for on-the-fly runtimes" + ) + operation = getattr(self._backend, "warm_up", None) + if not callable(operation): + raise CompatibilityError( + "installed on-the-fly runtime does not expose explicit warm-up; " + "reinstall pyAmpliCol from the current source revision" + ) + result = operation( + momenta, + helicities=_selector_ids( + helicities, + expected_type=HelicityConfiguration, + name="helicity", + ), + color_flows=_selector_ids( + color_flows, + expected_type=ColorFlow, + name="color-flow", + ), + precision=precision, + progress=progress, + n_cores=n_cores, + ) + if not isinstance(result, WarmUpResult): + raise EvaluationError("runtime backend returned an invalid WarmUpResult") + return result + + def evaluate_with_prec( + self, + momenta: Momenta, + precision: int, + *, + helicities: Sequence[str | HelicityConfiguration] | None = None, + color_flows: Sequence[str | ColorFlow] | None = None, + ) -> tuple[complex | Decimal, ...]: + return self.evaluate( + momenta, + helicities=helicities, + color_flows=color_flows, + precision=precision, + ) + + def evaluate_resolved( + self, + momenta: Momenta, + *, + helicities: Sequence[str | HelicityConfiguration] | None = None, + color_flows: Sequence[str | ColorFlow] | None = None, + precision: int = 16, + ) -> ResolvedEvaluation: + """Return physical values resolved by helicity and color component. + + LC output has shape ``(point, helicity, color_flow)``. NLC/full output + has shape ``(point, helicity, 1)`` because color is already contracted. + Summing the non-point axes reproduces :meth:`evaluate`. + """ + + precision = _validate_precision(precision) + result = self._backend.evaluate_resolved( + momenta, + helicities=_selector_ids( + helicities, + expected_type=HelicityConfiguration, + name="helicity", + ), + color_flows=_selector_ids( + color_flows, + expected_type=ColorFlow, + name="color-flow", + ), + precision=precision, + ) + if not isinstance(result, ResolvedEvaluation): + raise EvaluationError( + "runtime backend returned an invalid ResolvedEvaluation" + ) + return result + + def evaluate_resolved_with_prec( + self, + momenta: Momenta, + precision: int, + *, + helicities: Sequence[str | HelicityConfiguration] | None = None, + color_flows: Sequence[str | ColorFlow] | None = None, + ) -> ResolvedEvaluation: + return self.evaluate_resolved( + momenta, + helicities=helicities, + color_flows=color_flows, + precision=precision, + ) + + def _diagnostic_project_onshell( + self, + momenta: Momenta, + *, + precision: int, + ) -> tuple[object, Mapping[str, object]]: + """Forward the private report-only kinematic projection capability.""" + + precision = _validate_precision(precision) + operation = getattr(self._backend, "_diagnostic_project_onshell", None) + if not callable(operation): + raise EvaluationError( + "runtime backend does not support diagnostic on-shell projection" + ) + result = operation(momenta, precision=precision) + if ( + not isinstance(result, tuple) + or len(result) != 2 + or not isinstance(result[1], Mapping) + ): + raise EvaluationError( + "runtime backend returned an invalid diagnostic on-shell projection" + ) + return result[0], dict(result[1]) + + def set_model_parameters(self, mapping: ModelParameters) -> None: + """Validate and atomically apply a batch of runtime model parameters.""" + + self._backend.set_model_parameters(dict(mapping)) + + def _correlator_catalogue(self) -> CorrelatorCatalogue: + catalogue: CorrelatorCatalogue | None = getattr( + self, "_correlated_catalogue", None + ) + if catalogue is None: + from pyamplicol.runtime.correlations import load_correlator_catalogue + + catalogue = load_correlator_catalogue(self._backend) + self._correlated_catalogue = catalogue + return catalogue + + def available_color_correlations(self) -> tuple[_pyamplicol.ColorCorrelator, ...]: + """List this process's resolved operators, including ``"born"``. + + Reads the declared catalogue without loading an amplitude executor. + Each returned record contains its runtime ID and ordered bra/ket steps. + """ + return self._correlator_catalogue().declarations.color_requests + + def _correlator_evaluator(self) -> CorrelatorEvaluator: + evaluator: CorrelatorEvaluator | None = getattr( + self, "_correlated_evaluator", None + ) + if evaluator is None: + from pyamplicol.runtime.correlations import CorrelatorEvaluator + + evaluator = CorrelatorEvaluator( + self._backend, catalogue=self._correlator_catalogue() + ) + self._correlated_evaluator = evaluator + return evaluator + + def set_spin_correlation_vectors( + self, vectors: Mapping[int, object] | None + ) -> None: + """Replace declared spin-1 sources by literal complex four-vectors. + + Keys are public one-based leg labels. A value is either ``(E,x,y,z)`` + or one such vector per phase-space point. The set of keys must match a + spin class declared at generation. ``None`` or ``{}`` restores ordinary + helicity states. Vectors are neither normalized nor projected. + Real components accept Decimal; a ``(real, imaginary)`` Decimal pair + represents a complex component without a binary64 conversion. + This affects the correlated evaluation methods, not :meth:`evaluate`. + """ + self._correlator_evaluator().set_spin_correlation_vectors(vectors) + + def evaluate_correlated( + self, + momenta: Momenta, + *, + color_correlation: str = "born", + helicities: Sequence[str | HelicityConfiguration] | None = None, + precision: int = 16, + arithmetic: str = "arbitrary", + ) -> tuple[_pyamplicol.CorrelatedValue, ...]: + """Evaluate a generation-time colour ID with the current spin vectors. + + Returns one complex decimal value per point. ``"born"`` selects the + Born metric at the declared colour accuracy. All ordinary normalization + factors remain in place; a replaced spin leg is counted once in the + helicity sum. + + ``arithmetic="double-double"`` selects genuine DoubleFloat arithmetic + (at most 31 output digits); the default uses arbitrary precision. + Decimal spin inputs (including complex-component pairs) retain their + digits until conversion into the requested arithmetic. + """ + return self._correlator_evaluator().evaluate( + momenta, + color_correlation=color_correlation, + helicities=_selector_ids( + helicities, expected_type=HelicityConfiguration, name="helicity" + ), + precision=_validate_precision(precision), + arithmetic=arithmetic, + ) + + def evaluate_correlated_many( + self, + momenta: Momenta, + requests: Mapping[str, _pyamplicol.CorrelatedRequest], + *, + helicities: Sequence[str | HelicityConfiguration] | None = None, + precision: int = 16, + arithmetic: str = "arbitrary", + ) -> dict[str, tuple[_pyamplicol.CorrelatedValue, ...]]: + """Evaluate labelled colour/spin combinations over a point batch. + + Equal spin assignments share amplitudes across colour IDs. Different + assignments may reuse stages with identical exact inputs. Numerical + reuse is local to this call; requests never change the spin setter. + ``None`` vectors inherit its initial state and ``{}`` selects physical + helicities. Each result tuple follows the input point order. + Arithmetic and Decimal spin inputs follow :meth:`evaluate_correlated`. + """ + return self._correlator_evaluator().evaluate_many( + momenta, + requests, + helicities=_selector_ids( + helicities, expected_type=HelicityConfiguration, name="helicity" + ), + precision=_validate_precision(precision), + arithmetic=arithmetic, + ) + + def clear(self) -> None: + """Drop warmed execution state while keeping this artifact loaded. + + An already materialized :attr:`physics` compatibility view remains + cached until the runtime itself is released. + """ + + self._backend.clear() + correlated = getattr(self, "_correlated_evaluator", None) + if correlated is not None: + correlated.clear() + + @property + def representative_process_key(self) -> str: + """Stable artifact process implementing the selected public ordering.""" + + value = getattr(self._backend, "representative_process_key", None) + return value if isinstance(value, str) and value else self.physics.process_id + + @property + def external_permutation(self) -> tuple[int, ...]: + """Representative-index to public-index external-leg permutation.""" + + value = getattr(self._backend, "external_permutation", None) + count_value = getattr(self._backend, "external_count", None) + count: int | None + if count_value is None: + count = None + elif ( + isinstance(count_value, bool) + or not isinstance(count_value, int) + or count_value < 1 + ): + raise CompatibilityError("runtime external process count is invalid") + else: + count = count_value + if value is None: + if count is None: + # Preserve the original minimum RuntimeBackend contract. The + # built-in backend always supplies authenticated compact + # metadata, while older injected backends may expose only + # ProcessPhysics. + return tuple(range(len(self.physics.external_particles))) + return tuple(range(count)) + try: + permutation = tuple(value) + except TypeError as exc: + raise CompatibilityError( + "runtime external permutation must be an iterable of integers" + ) from exc + if any( + isinstance(index, bool) or not isinstance(index, int) + for index in permutation + ): + raise CompatibilityError( + "runtime external permutation must contain integers" + ) + expected_count = ( + len(self.physics.external_particles) if count is None else count + ) + if len(permutation) != expected_count or sorted(permutation) != list( + range(expected_count) + ): + raise CompatibilityError("runtime external permutation is invalid") + return permutation + + @property + def process_group_factor(self) -> Fraction: + """Exact factor relating the selected process to its representative. + + A process selected through the artifact's process-group map evaluates + ``factor * ME_representative`` at the permuted momenta; every other + selection reports one. + """ + + value = getattr(self._backend, "process_group_factor", None) + if value is None: + return Fraction(1) + if not isinstance(value, Fraction) or value <= 0: + raise CompatibilityError("runtime process-group factor is invalid") + return value + + @property + def charge_conjugated(self) -> bool: + """Whether the selection is the charge conjugate of its representative. + + Its LC color-flow words are then the reversed representative words. + """ + + return getattr(self._backend, "charge_conjugated", False) is True + + def set_model_parameter(self, name: str, value: complex | float | int) -> None: + self.set_model_parameters({name: value}) + + def mute_warnings(self) -> None: + self._backend.mute_warnings() + + def unmute_warnings(self) -> None: + self._backend.unmute_warnings() + + +class BenchmarkRunner: + """Profile summed process evaluation with a typed benchmark configuration.""" + + def __init__( + self, + config: BenchmarkConfig | RunConfig | None = None, + progress: ProgressSink | None = None, + *, + _chunk_guard: Callable[[float | None, str], None] | None = None, + ) -> None: + if config is not None and not isinstance(config, (BenchmarkConfig, RunConfig)): + raise TypeError( + "BenchmarkRunner config must be BenchmarkConfig, RunConfig, or null" + ) + _validate_progress(progress) + if _chunk_guard is not None and not callable(_chunk_guard): + raise TypeError("BenchmarkRunner chunk guard must be callable") + self._config = config + self._progress = progress + self._chunk_guard = _chunk_guard + self._backend: BenchmarkBackend | None = None + + def _implementation(self) -> BenchmarkBackend: + if self._backend is None: + self._backend = _get_benchmark_factory()(self._config, self._progress) + if self._chunk_guard is not None: + setter = getattr(self._backend, "set_chunk_guard", None) + if not callable(setter): + raise EvaluationError( + "benchmark backend does not support a profiling chunk guard" + ) + setter(self._chunk_guard) + return self._backend + + def run( + self, + target: Runtime | os.PathLike[str] | str, + *, + points: Momenta | None = None, + ) -> BenchmarkResult: + """Profile an artifact path or an already loaded :class:`Runtime`.""" + + backend_target: RuntimeBackend | os.PathLike[str] | str + if isinstance(target, Runtime): + backend_target = target._backend + else: + backend_target = Path(os.fspath(target)).expanduser().resolve(strict=False) + result = self._implementation().run(backend_target, points=points) + if not isinstance(result, BenchmarkResult): + raise EvaluationError( + "benchmark backend returned an invalid BenchmarkResult" + ) + return result + + +def generate( + processes: ProcessSet | ProcessRequest | str | Iterable[ProcessRequest | str], + output: os.PathLike[str] | str, + *, + model: ModelSource | _pyamplicol.CompiledModel | None = None, + mode: Literal["error", "append", "replace"] = "error", + config: GenerationConfig | RunConfig | ConfigResolution | None = None, + progress: ProgressSink | None = None, + correlators: _pyamplicol.CorrelatorConfig | None = None, +) -> GenerationResult: + """Generate a process artifact using a one-shot convenience function.""" + + return Generator(config=config, progress=progress).generate( + processes, output, model=model, mode=mode, correlators=correlators + ) + + +load = Runtime.load + + +def benchmark( + target: Runtime | os.PathLike[str] | str, + *, + points: Momenta | None = None, + config: BenchmarkConfig | RunConfig | None = None, + progress: ProgressSink | None = None, +) -> BenchmarkResult: + """Profile a generated artifact using a one-shot convenience function.""" + + return BenchmarkRunner(config=config, progress=progress).run(target, points=points) + + +__all__ = [ + "BenchmarkRunner", + "Generator", + "Runtime", + "benchmark", + "generate", + "install_backend_factories", + "load", +] diff --git a/pyamplicol/src/pyamplicol/artifacts/__init__.py b/pyamplicol/src/pyamplicol/artifacts/__init__.py new file mode 100644 index 0000000000..ccb104888a --- /dev/null +++ b/pyamplicol/src/pyamplicol/artifacts/__init__.py @@ -0,0 +1,43 @@ +# SPDX-License-Identifier: 0BSD +"""Schema-v3 process artifact services.""" + +from .api_bundle import emit_api_bundle +from .inspection import ( + ArtifactAliasInspection, + ArtifactDependencyInspection, + ArtifactInspection, + ArtifactProcessInspection, + inspect_artifact, +) +from .manifest import ( + MANIFEST_NAME, + ArtifactManifest, + PayloadRecord, + compute_artifact_id, + load_manifest, + validate_payloads, +) +from .security import confined_path, normalize_relative_path, sha256_file +from .transaction import ArtifactTransaction, ArtifactWriteMode +from .writer import ArtifactBuilder + +__all__ = [ + "MANIFEST_NAME", + "ArtifactAliasInspection", + "ArtifactBuilder", + "ArtifactDependencyInspection", + "ArtifactInspection", + "ArtifactManifest", + "ArtifactProcessInspection", + "ArtifactTransaction", + "ArtifactWriteMode", + "PayloadRecord", + "compute_artifact_id", + "confined_path", + "emit_api_bundle", + "inspect_artifact", + "load_manifest", + "normalize_relative_path", + "sha256_file", + "validate_payloads", +] diff --git a/pyamplicol/src/pyamplicol/artifacts/api_bundle.py b/pyamplicol/src/pyamplicol/artifacts/api_bundle.py new file mode 100644 index 0000000000..f70b997b95 --- /dev/null +++ b/pyamplicol/src/pyamplicol/artifacts/api_bundle.py @@ -0,0 +1,171 @@ +# SPDX-License-Identifier: 0BSD +"""Emit the wheel-independent API examples bundled with process artifacts.""" + +from __future__ import annotations + +import importlib.resources +import math +from collections.abc import Mapping, Sequence +from dataclasses import dataclass +from decimal import Decimal + +from .manifest import PayloadRecord +from .writer import ArtifactBuilder + +Scalar = float | int | str | Decimal +ValidationPoint = Sequence[Sequence[Scalar]] + +_TEMPLATES: tuple[tuple[str, str, str, bool], ...] = ( + ( + "python/check_standalone.py", + "text/x-python", + "api-source", + True, + ), + ( + "c/check_standalone.c", + "text/x-csrc", + "api-source", + False, + ), + ("c/Makefile", "text/x-makefile", "api-build-file", False), + ( + "cpp/check_standalone.cpp", + "text/x-c++src", + "api-source", + False, + ), + ("cpp/Makefile", "text/x-makefile", "api-build-file", False), + ( + "fortran/check_standalone.f90", + "text/x-fortran", + "api-source", + False, + ), + ("fortran/Makefile", "text/x-makefile", "api-build-file", False), + ( + "rust/check_standalone.rs", + "text/x-rust", + "api-source", + False, + ), + ("rust/Makefile", "text/x-makefile", "api-build-file", False), + ("umami/Makefile", "text/x-makefile", "api-build-file", False), + ("umami/umami.h", "text/x-chdr", "api-source", False), + ("umami/umami_provider.h", "text/x-chdr", "api-source", False), + ("umami/umami.c", "text/x-csrc", "api-source", False), + ("umami/umami_driver.c", "text/x-csrc", "api-source", False), + ("umami/umami.exports", "text/plain", "api-build-file", False), + ("umami/umami.exports.macos", "text/plain", "api-build-file", False), + ("umami/UMAMI_LICENSE", "text/plain", "sdk-metadata", False), + ("umami/README.md", "text/markdown", "sdk-metadata", False), +) + + +@dataclass(frozen=True, slots=True) +class ApiBundlePayload: + path: str + content: bytes + role: str + media_type: str + executable: bool = False + + +def _template_bytes(relative: str) -> bytes: + root = importlib.resources.files("pyamplicol.assets.api_templates") + resource = root.joinpath(*relative.split("/")) + if not resource.is_file(): + raise RuntimeError(f"packaged API template is missing: {relative}") + return resource.read_bytes() + + +def _scalar_text(value: Scalar) -> str: + if isinstance(value, bool): + raise TypeError("validation momenta must not contain booleans") + if isinstance(value, Decimal): + if not value.is_finite(): + raise ValueError("validation momenta must be finite") + return str(value) + if isinstance(value, str): + parsed = Decimal(value) + if not parsed.is_finite(): + raise ValueError("validation momenta must be finite") + return value + numeric = float(value) + if not math.isfinite(numeric): + raise ValueError("validation momenta must be finite") + return format(numeric, ".17g") + + +def format_validation_points( + points: Mapping[str, ValidationPoint], +) -> bytes: + """Serialize deterministic f64-compatible points in a language-neutral format.""" + + lines = ["RUSTICOL_VALIDATION_POINTS_V1"] + for process_id in sorted(points): + if not process_id or any(character.isspace() for character in process_id): + raise ValueError("validation-point process IDs must be non-empty tokens") + momenta = tuple(tuple(momentum) for momentum in points[process_id]) + if not momenta: + raise ValueError(f"validation point for {process_id!r} is empty") + if any(len(momentum) != 4 for momentum in momenta): + raise ValueError( + f"validation point for {process_id!r} must use four-vectors" + ) + components = [ + _scalar_text(component) for momentum in momenta for component in momentum + ] + lines.append("\t".join((process_id, str(len(momenta)), *components))) + return ("\n".join(lines) + "\n").encode("ascii") + + +def api_bundle_payloads( + validation_points: Mapping[str, ValidationPoint] | None = None, +) -> tuple[ApiBundlePayload, ...]: + payloads = [ + ApiBundlePayload( + path=f"API/{relative}", + content=_template_bytes(relative), + role=role, + media_type=media_type, + executable=executable, + ) + for relative, media_type, role, executable in _TEMPLATES + ] + payloads.append( + ApiBundlePayload( + path="API/validation_points.dat", + content=format_validation_points(validation_points or {}), + role="validation-momenta", + media_type="text/tab-separated-values", + ) + ) + return tuple(payloads) + + +def emit_api_bundle( + builder: ArtifactBuilder, + validation_points: Mapping[str, ValidationPoint] | None = None, +) -> tuple[PayloadRecord, ...]: + """Write exactly one root API bundle through an active artifact builder.""" + + return tuple( + builder.add_bytes( + payload.path, + payload.content, + role=payload.role, + media_type=payload.media_type, + executable=payload.executable, + ) + for payload in api_bundle_payloads(validation_points) + ) + + +__all__ = [ + "ApiBundlePayload", + "ValidationPoint", + "api_bundle_payloads", + "emit_api_bundle", + "format_validation_points", +] diff --git a/pyamplicol/src/pyamplicol/artifacts/inspection.py b/pyamplicol/src/pyamplicol/artifacts/inspection.py new file mode 100644 index 0000000000..242348fb64 --- /dev/null +++ b/pyamplicol/src/pyamplicol/artifacts/inspection.py @@ -0,0 +1,3252 @@ +# SPDX-License-Identifier: 0BSD +"""Read-only summaries of generated process artifacts.""" + +from __future__ import annotations + +import json +import math +import struct +from collections.abc import Mapping, Sequence +from dataclasses import dataclass +from pathlib import Path +from typing import Any, cast + +from pyamplicol._internal.versions import ( + EAGER_DIRECT_ARENA_RUNTIME_CAPABILITY, + ON_THE_FLY_CONTRACTED_COLOR_RUNTIME_CAPABILITY, + ON_THE_FLY_LC_COLOR_RUNTIME_CAPABILITY, + ON_THE_FLY_RUNTIME_CAPABILITY, + RECURRENCE_BUILDER_INPUT_ABI, + RECURRENCE_COLOR_RUNTIME_CAPABILITY, + RECURRENCE_CONTRACTED_COLOR_RUNTIME_CAPABILITY, + RECURRENCE_DIRECT_ARENA_RUNTIME_CAPABILITY, + RECURRENCE_DIRECT_BACKEND_ABI, + RECURRENCE_DIRECT_TEMPLATE_ABI, + RECURRENCE_HELICITY_SELECTOR_COMPANION_RUNTIME_CAPABILITY, + RECURRENCE_PLAN_ABI, + RECURRENCE_RUNTIME_LAYOUT_ABI, + SYMMETRIC_GROUP_FFT_COLOR_RUNTIME_CAPABILITY, +) +from pyamplicol.api.errors import ArtifactError +from pyamplicol.generation.process_grouping import GroupMember, ProcessGroupMap + +from .manifest import MANIFEST_NAME, ArtifactManifest, load_manifest +from .process_groups import initial_process_groups, read_process_groups +from .security import confined_path, normalize_relative_path + +_EAGER_RUNTIME_KIND = "pyamplicol-runtime-eager-execution" +_RECURRENCE_RUNTIME_KIND = "pyamplicol-runtime-recurrence-execution" +_ON_THE_FLY_RUNTIME_KIND = "pyamplicol-runtime-on-the-fly-execution" +_ON_THE_FLY_PUBLIC_METADATA_KIND = "pyamplicol-on-the-fly-public-metadata" +_RETIRED_RECURRENCE_RUNTIME_CAPABILITY = "rusticol.recurrence-runtime.complex-f64.v1" +_RECURRENCE_DIRECT_SCHEDULE_MEMBER_PATH = "schedule/recurrence-direct-schedule-v2.bin" +_RECURRENCE_COLOR_CONTRACTION_CODEC_ABI = "pyamplicol-recurrence-color-contraction-v3" +_EAGER_PLAN_V3_ABI = "pyamplicol-eager-plan-v3" +_EAGER_RUNTIME_CAPABILITIES = frozenset( + { + EAGER_DIRECT_ARENA_RUNTIME_CAPABILITY, + "rusticol.eager-dag.complex-f64.v1", + "rusticol.eager-runtime-layout.complex-f64.v1", + } +) +_MISSING_U32 = (1 << 32) - 1 +_EAGER_INVOCATION = struct.Struct(" Mapping[str, object]: + if not isinstance(value, Mapping): + raise ArtifactError(f"{context} must be an object") + return value + + +def _sequence(value: object, context: str) -> Sequence[object]: + if isinstance(value, (str, bytes)) or not isinstance(value, Sequence): + raise ArtifactError(f"{context} must be an array") + return value + + +def _computed(value: object, context: str) -> bool: + record = _mapping(value, context) + if record.get("kind") == "contracted-color": + return True + computed = record.get("computed") + if not isinstance(computed, bool): + raise ArtifactError(f"{context}.computed must be a boolean") + return computed + + +def _integer(value: object, context: str, *, minimum: int = 0) -> int: + if isinstance(value, bool) or not isinstance(value, int) or value < minimum: + raise ArtifactError(f"{context} must be an integer >= {minimum}") + return value + + +def _string(value: object, context: str) -> str: + if not isinstance(value, str) or not value: + raise ArtifactError(f"{context} must be a non-empty string") + return value + + +def _json_mapping(path: Path, context: str) -> Mapping[str, object]: + try: + value = json.loads(path.read_text(encoding="utf-8")) + except OSError as exc: + raise ArtifactError(f"cannot read {context}: {exc}") from exc + except json.JSONDecodeError as exc: + raise ArtifactError(f"invalid {context}: {exc}") from exc + return _mapping(value, context) + + +def _artifact_path( + manifest: ArtifactManifest, + relative: str | Path, + context: str, +) -> Path: + try: + normalized = normalize_relative_path(Path(relative).as_posix()) + except ArtifactError: + raise + except (TypeError, ValueError) as exc: + raise ArtifactError(f"{context} is not a valid artifact path") from exc + return confined_path(manifest.root, normalized) + + +def _execution_paths(manifest: ArtifactManifest) -> Mapping[str, Path]: + evaluator_relative = _string( + manifest.runtime.get("evaluator_manifest_path"), + "runtime.evaluator_manifest_path", + ) + evaluator_path = _artifact_path( + manifest, + evaluator_relative, + "runtime.evaluator_manifest_path", + ) + evaluator_set = _json_mapping(evaluator_path, "runtime evaluator manifest") + records = _sequence( + evaluator_set.get("processes"), + "runtime evaluator manifest.processes", + ) + base = Path(evaluator_relative).parent + result: dict[str, Path] = {} + for index, raw in enumerate(records): + record = _mapping(raw, f"runtime evaluator manifest.processes[{index}]") + process_id = _string( + record.get("process_id"), + f"runtime evaluator manifest.processes[{index}].process_id", + ) + manifest_path = _string( + record.get("manifest_path"), + f"runtime evaluator manifest.processes[{index}].manifest_path", + ) + if process_id in result: + raise ArtifactError( + f"runtime evaluator manifest repeats process {process_id!r}" + ) + result[process_id] = _artifact_path( + manifest, + base / manifest_path, + f"runtime evaluator manifest for {process_id!r}", + ) + expected = {str(process["id"]) for process in manifest.processes} + if set(result) != expected: + raise ArtifactError( + "runtime evaluator manifest process IDs do not match artifact processes" + ) + return result + + +@dataclass(frozen=True, slots=True) +class _ExecutionInspection: + execution_mode: str + physical_helicity_count: int | None = None + physical_color_flow_count: int | None = None + prepared_backend: str | None = None + prepared_kernel_count: int | None = None + referenced_kernel_count: int | None = None + invocation_count: int | None = None + attachment_count: int | None = None + evaluation_alias_count: int | None = None + maximum_fanout: int | None = None + finalization_count: int | None = None + closure_count: int | None = None + selector_closure_available: bool = False + selector_domain_count: int | None = None + selector_domain_membership_count: int | None = None + requested_point_tile_size: int | None = None + effective_point_tile_size: int | None = None + requested_query_construction_threads: int | None = None + # Historical field name: this is a soft batching target, not an upper limit. + workspace_limit_bytes: int | None = None + workspace_bytes: int | None = None + arena_semantic_component_count: int | None = None + arena_component_count: int | None = None + arena_component_reuse_count: int | None = None + momentum_form_count: int | None = None + direct_source_row_count: int | None = None + direct_contribution_row_count: int | None = None + direct_finalization_row_count: int | None = None + direct_closure_row_count: int | None = None + direct_row_group_count: int | None = None + recurrence_color_accuracy: str | None = None + recurrence_color_storage: str | None = None + recurrence_color_sector_count: int | None = None + recurrence_color_active_sector_count: int | None = None + recurrence_color_component_count: int | None = None + recurrence_color_group_count: int | None = None + recurrence_color_destination_count: int | None = None + recurrence_color_entry_count: int | None = None + recurrence_color_logical_entry_count: int | None = None + recurrence_color_factorization_kind: str | None = None + recurrence_color_factorization_rank: int | None = None + recurrence_color_factorization_coset_count: int | None = None + recurrence_color_contraction_method: str | None = None + recurrence_color_fft_degree: int | None = None + recurrence_color_fft_channel_count: int | None = None + recurrence_color_fft_covered_local_group_count: int | None = None + recurrence_color_direct_residual_group_count: int | None = None + recurrence_color_direct_residual_entry_count: int | None = None + recurrence_color_raw_kernel_bytes: int | None = None + recurrence_color_transformed_kernel_bytes: int | None = None + recurrence_color_fft_capability: str | None = None + packed_input_bytes: int | None = None + packed_output_bytes: int | None = None + scatter_bytes: int | None = None + native_profile_phases: tuple[str, ...] = () + + +@dataclass(frozen=True, slots=True) +class _PhysicsInspection: + physical_helicities: int + computed_helicities: int + physical_color_components: int + computed_color_components: int + helicity_coverage: str + color_coverage: str + selector_provenance: str | None = None + helicity_runtime_contract: str | None = None + color_flow_runtime_contract: str | None = None + generation_specialized_axes: tuple[str, ...] = () + selected_source_helicities: tuple[tuple[int, int], ...] = () + selected_color_sector_ids: tuple[int, ...] = () + helicity_recurrence_status: str | None = None + helicity_optimized_class_count: int | None = None + helicity_optimized_current_count: int | None = None + helicity_residual_current_count: int | None = None + helicity_optimized_amplitude_class_count: int | None = None + helicity_residual_amplitude_count: int | None = None + helicity_materialized_current_count: int | None = None + helicity_materialized_amplitude_count: int | None = None + lc_physical_sector_count: int | None = None + lc_materialized_sector_count: int | None = None + lc_replayed_sector_count: int | None = None + lc_residual_sector_count: int | None = None + + +def _table_record( + value: object, + context: str, +) -> tuple[str, int, int]: + record = _mapping(value, context) + return ( + _string(record.get("path"), f"{context}.path"), + _integer(record.get("count"), f"{context}.count"), + _integer(record.get("row_size"), f"{context}.row_size", minimum=1), + ) + + +def _table_rows( + manifest: ArtifactManifest, + execution_root: Path, + table: object, + layout: struct.Struct, + context: str, +) -> tuple[tuple[int | float, ...], ...]: + relative, count, row_size = _table_record(table, context) + if row_size != layout.size: + raise ArtifactError(f"{context}.row_size is {row_size}, expected {layout.size}") + path = _artifact_path( + manifest, + execution_root.relative_to(manifest.root) / relative, + f"{context}.path", + ) + try: + payload = path.read_bytes() + except OSError as exc: + raise ArtifactError(f"cannot read {context} payload: {exc}") from exc + expected_size = count * row_size + if len(payload) != expected_size: + raise ArtifactError( + f"{context} declares {count} rows but has {len(payload)} bytes" + ) + return cast( + tuple[tuple[int | float, ...], ...], + tuple(tuple(row) for row in layout.iter_unpack(payload)), + ) + + +def _optional_positive_integer( + value: object, + context: str, +) -> int | None: + if value is None: + return None + return _integer(value, context, minimum=1) + + +def _profile_phases(execution: Mapping[str, object]) -> tuple[str, ...]: + raw = execution.get("native_profile_phases") + if raw is None: + return _EAGER_PROFILE_PHASES + phases = tuple( + _string(value, f"execution.native_profile_phases[{index}]") + for index, value in enumerate(_sequence(raw, "execution.native_profile_phases")) + ) + if len(phases) != len(set(phases)): + raise ArtifactError("execution.native_profile_phases contains duplicates") + return phases + + +def _selector_domain_ids( + manifest: ArtifactManifest, + execution_root: Path, + table: object, + *, + expected_count: int, + domain_count: int, + context: str, +) -> tuple[int, ...]: + rows = _table_rows( + manifest, + execution_root, + table, + _EAGER_SELECTOR_DOMAIN_ID, + context, + ) + if len(rows) != expected_count: + raise ArtifactError( + f"{context} has {len(rows)} rows, expected {expected_count}" + ) + result = tuple(int(row[0]) for row in rows) + invalid = next((value for value in result if value >= domain_count), None) + if invalid is not None: + raise ArtifactError(f"{context} references unknown domain {invalid}") + return result + + +def _selector_domain_inspection( + manifest: ArtifactManifest, + execution: Mapping[str, object], + execution_root: Path, + selector_closures: object, + *, + stages: Sequence[ + tuple[ + Mapping[str, object], + tuple[tuple[int | float, ...], ...], + int, + tuple[tuple[int | float, ...], ...], + ] + ], + closures: tuple[tuple[int | float, ...], ...], +) -> tuple[int, int]: + selector = _mapping( + selector_closures, + "eager execution.plan.selector_closures", + ) + abi = _string( + selector.get("abi"), + "eager execution.plan.selector_closures.abi", + ) + if abi != _EAGER_SELECTOR_DOMAINS_ABI: + raise ArtifactError(f"unsupported eager selector-domain ABI {abi!r}") + + domain_rows = _table_rows( + manifest, + execution_root, + selector.get("domains"), + _EAGER_SELECTOR_DOMAIN, + "eager execution.plan.selector_closures.domains", + ) + group_rows = _table_rows( + manifest, + execution_root, + selector.get("domain_group_ids"), + _EAGER_SELECTOR_GROUP, + "eager execution.plan.selector_closures.domain_group_ids", + ) + memberships: list[frozenset[int]] = [] + cursor = 0 + for domain_id, row in enumerate(domain_rows): + start, count = int(row[0]), int(row[1]) + if start != cursor: + raise ArtifactError( + f"eager selector domain {domain_id} starts at {start}, " + f"expected {cursor}" + ) + stop = start + count + if stop > len(group_rows): + raise ArtifactError( + f"eager selector domain {domain_id} exceeds its membership table" + ) + members = tuple(int(member[0]) for member in group_rows[start:stop]) + if members != tuple(sorted(set(members))): + raise ArtifactError( + f"eager selector domain {domain_id} members are not unique and sorted" + ) + memberships.append(frozenset(members)) + cursor = stop + if cursor != len(group_rows): + raise ArtifactError( + "eager selector domains do not cover their membership table" + ) + if len(memberships) != len(set(memberships)): + raise ArtifactError("eager selector domains contain duplicate memberships") + + runtime_schema = _mapping( + execution.get("runtime_schema"), "eager execution.runtime_schema" + ) + amplitude_stage = _mapping( + runtime_schema.get("amplitude_stage"), + "eager execution.runtime_schema.amplitude_stage", + ) + roots = _sequence( + amplitude_stage.get("roots"), + "eager execution.runtime_schema.amplitude_stage.roots", + ) + groups_by_output: dict[int, int] = {} + for index, raw_root in enumerate(roots): + root = _mapping( + raw_root, + f"eager execution.runtime_schema.amplitude_stage.roots[{index}]", + ) + output_index = _integer( + root.get("output_index"), + f"eager execution.runtime_schema.amplitude_stage.roots[{index}]" + ".output_index", + ) + group_id = _integer( + root.get("coherent_group_id"), + f"eager execution.runtime_schema.amplitude_stage.roots[{index}]" + ".coherent_group_id", + ) + if output_index in groups_by_output: + raise ArtifactError( + f"eager amplitude roots repeat output index {output_index}" + ) + groups_by_output[output_index] = group_id + + declared_groups = set(groups_by_output.values()) + unknown_groups = ( + set().union(*memberships) - declared_groups if memberships else set() + ) + if unknown_groups: + rendered = ", ".join(str(value) for value in sorted(unknown_groups)) + raise ArtifactError( + f"eager selector domains reference undeclared coherent groups: {rendered}" + ) + + selector_stages = _sequence( + selector.get("stages"), + "eager execution.plan.selector_closures.stages", + ) + if len(selector_stages) != len(stages): + raise ArtifactError("eager selector domains do not cover every stage") + domain_count = len(memberships) + for index, (raw_selector_stage, execution_stage) in enumerate( + zip(selector_stages, stages, strict=True) + ): + selector_stage = _mapping( + raw_selector_stage, + f"eager execution.plan.selector_closures.stages[{index}]", + ) + stage, invocations, attachment_count, finalizations = execution_stage + stage_index = _integer( + stage.get("stage_index"), + f"eager execution.plan.stages[{index}].stage_index", + ) + selector_stage_index = _integer( + selector_stage.get("stage_index"), + f"eager execution.plan.selector_closures.stages[{index}].stage_index", + ) + if selector_stage_index != stage_index: + raise ArtifactError("eager selector-domain stage index mismatch") + invocation_domains = _selector_domain_ids( + manifest, + execution_root, + selector_stage.get("invocation_domains"), + expected_count=len(invocations), + domain_count=domain_count, + context=( + f"eager execution.plan.selector_closures.stages[{index}]" + ".invocation_domains" + ), + ) + attachment_domains = _selector_domain_ids( + manifest, + execution_root, + selector_stage.get("attachment_domains"), + expected_count=attachment_count, + domain_count=domain_count, + context=( + f"eager execution.plan.selector_closures.stages[{index}]" + ".attachment_domains" + ), + ) + unpropagated_domains = _selector_domain_ids( + manifest, + execution_root, + selector_stage.get("unpropagated_finalization_domains"), + expected_count=len(finalizations), + domain_count=domain_count, + context=( + f"eager execution.plan.selector_closures.stages[{index}]" + ".unpropagated_finalization_domains" + ), + ) + propagated_domains = _selector_domain_ids( + manifest, + execution_root, + selector_stage.get("propagated_finalization_domains"), + expected_count=len(finalizations), + domain_count=domain_count, + context=( + f"eager execution.plan.selector_closures.stages[{index}]" + ".propagated_finalization_domains" + ), + ) + + for invocation, domain_id in zip(invocations, invocation_domains, strict=True): + start, count = int(invocation[7]), int(invocation[8]) + stop = start + count + if stop > len(attachment_domains): + raise ArtifactError( + "eager invocation selector range exceeds attachment domains" + ) + expected_domain = frozenset( + group_id + for attachment_domain_id in attachment_domains[start:stop] + for group_id in memberships[attachment_domain_id] + ) + if memberships[domain_id] != expected_domain: + raise ArtifactError( + "eager invocation selector domain does not equal its " + "attachment-domain union" + ) + for finalization, unpropagated, propagated in zip( + finalizations, + unpropagated_domains, + propagated_domains, + strict=True, + ): + if int(finalization[2]) == _MISSING_U32 and memberships[unpropagated]: + raise ArtifactError( + "missing eager unpropagated output has a nonempty selector domain" + ) + if int(finalization[3]) == _MISSING_U32 and memberships[propagated]: + raise ArtifactError( + "missing eager propagated output has a nonempty selector domain" + ) + + closure_domains = _selector_domain_ids( + manifest, + execution_root, + selector.get("closure_domains"), + expected_count=len(closures), + domain_count=domain_count, + context="eager execution.plan.selector_closures.closure_domains", + ) + for closure, domain_id in zip(closures, closure_domains, strict=True): + output_index = int(closure[3]) + try: + expected = frozenset((groups_by_output[output_index],)) + except KeyError as exc: + raise ArtifactError( + f"eager closure references unknown amplitude output {output_index}" + ) from exc + if memberships[domain_id] != expected: + raise ArtifactError( + "eager closure selector domain does not match its coherent group" + ) + + return domain_count, len(group_rows) + + +def _eager_execution_inspection( + manifest: ArtifactManifest, + execution: Mapping[str, object], + execution_path: Path, +) -> _ExecutionInspection: + if execution.get("eager_plan_abi") == _EAGER_PLAN_V3_ABI: + return _eager_v3_execution_inspection(manifest, execution) + runtime_options = _mapping( + execution.get("runtime_options"), "eager execution.runtime_options" + ) + requested_tile = _integer( + runtime_options.get("point_tile_size"), + "eager execution.runtime_options.point_tile_size", + minimum=1, + ) + workspace_mib = _integer( + runtime_options.get("workspace_mib"), + "eager execution.runtime_options.workspace_mib", + minimum=1, + ) + effective_tile = _optional_positive_integer( + runtime_options.get("effective_point_tile_size"), + "eager execution.runtime_options.effective_point_tile_size", + ) + workspace_bytes = _optional_positive_integer( + runtime_options.get("workspace_bytes"), + "eager execution.runtime_options.workspace_bytes", + ) + plan = _mapping(execution.get("plan"), "eager execution.plan") + stages = _sequence(plan.get("stages"), "eager execution.plan.stages") + execution_root = execution_path.parent + + invocation_count = 0 + attachment_count = 0 + finalization_count = 0 + maximum_fanout = 0 + referenced_kernel_ids: set[int] = set() + inspected_stages: list[ + tuple[ + Mapping[str, object], + tuple[tuple[int | float, ...], ...], + int, + tuple[tuple[int | float, ...], ...], + ] + ] = [] + for index, raw_stage in enumerate(stages): + stage = _mapping(raw_stage, f"eager execution.plan.stages[{index}]") + invocations = _table_rows( + manifest, + execution_root, + stage.get("invocations"), + _EAGER_INVOCATION, + f"eager execution.plan.stages[{index}].invocations", + ) + _attachment_path, stage_attachments, _attachment_size = _table_record( + stage.get("attachments"), + f"eager execution.plan.stages[{index}].attachments", + ) + finalizations = _table_rows( + manifest, + execution_root, + stage.get("finalizations"), + _EAGER_FINALIZATION, + f"eager execution.plan.stages[{index}].finalizations", + ) + inspected_stages.append((stage, invocations, stage_attachments, finalizations)) + invocation_count += len(invocations) + attachment_count += stage_attachments + finalization_count += len(finalizations) + for row in invocations: + referenced_kernel_ids.add(int(row[0])) + maximum_fanout = max(maximum_fanout, int(row[-1])) + referenced_kernel_ids.update( + int(row[0]) for row in finalizations if int(row[0]) != _MISSING_U32 + ) + + closures = _table_rows( + manifest, + execution_root, + plan.get("closures"), + _EAGER_CLOSURE, + "eager execution.plan.closures", + ) + referenced_kernel_ids.update( + int(row[0]) for row in closures if int(row[0]) != _MISSING_U32 + ) + if attachment_count < invocation_count: + raise ArtifactError( + "eager execution has fewer attachments than canonical invocations" + ) + + kernel_pack = _mapping(execution.get("kernel_pack"), "eager execution.kernel_pack") + pack_path = _artifact_path( + manifest, + _string( + kernel_pack.get("manifest_path"), + "eager execution.kernel_pack.manifest_path", + ), + "eager execution.kernel_pack.manifest_path", + ) + pack = _json_mapping(pack_path, "prepared eager kernel pack") + kernels = _sequence(pack.get("kernels"), "prepared eager kernel pack.kernels") + kernel_ids = { + _integer( + _mapping(raw, f"prepared eager kernel pack.kernels[{index}]").get( + "kernel_id" + ), + f"prepared eager kernel pack.kernels[{index}].kernel_id", + ) + for index, raw in enumerate(kernels) + } + if not referenced_kernel_ids <= kernel_ids: + missing = ", ".join( + str(value) for value in sorted(referenced_kernel_ids - kernel_ids) + ) + raise ArtifactError( + "eager execution references kernels absent from its prepared pack: " + f"{missing}" + ) + + selector_closures = plan.get("selector_closures") + selector_closure_available = False + selector_domain_count = None + selector_domain_membership_count = None + if selector_closures is not None: + ( + selector_domain_count, + selector_domain_membership_count, + ) = _selector_domain_inspection( + manifest, + execution, + execution_root, + selector_closures, + stages=tuple(inspected_stages), + closures=closures, + ) + selector_closure_available = True + + return _ExecutionInspection( + execution_mode="eager", + prepared_backend=_string( + pack.get("backend"), "prepared eager kernel pack.backend" + ), + prepared_kernel_count=len(kernels), + referenced_kernel_count=len(referenced_kernel_ids), + invocation_count=invocation_count, + attachment_count=attachment_count, + evaluation_alias_count=attachment_count - invocation_count, + maximum_fanout=maximum_fanout, + finalization_count=finalization_count, + closure_count=len(closures), + selector_closure_available=selector_closure_available, + selector_domain_count=selector_domain_count, + selector_domain_membership_count=selector_domain_membership_count, + requested_point_tile_size=requested_tile, + effective_point_tile_size=effective_tile, + workspace_limit_bytes=workspace_mib * 1024 * 1024, + workspace_bytes=workspace_bytes, + native_profile_phases=_profile_phases(execution), + ) + + +def _eager_v3_execution_inspection( + manifest: ArtifactManifest, + execution: Mapping[str, object], +) -> _ExecutionInspection: + runtime_options = _mapping( + execution.get("runtime_options"), "eager execution.runtime_options" + ) + requested_tile = _integer( + runtime_options.get("point_tile_size"), + "eager execution.runtime_options.point_tile_size", + minimum=1, + ) + workspace_mib = _integer( + runtime_options.get("workspace_mib"), + "eager execution.runtime_options.workspace_mib", + minimum=1, + ) + plan = _mapping(execution.get("plan"), "eager execution.plan") + summary = _mapping( + plan.get("inspection_summary"), + "eager execution.plan.inspection_summary", + ) + pack_reference = _mapping( + execution.get("kernel_pack"), "eager execution.kernel_pack" + ) + pack_path = _artifact_path( + manifest, + _string( + pack_reference.get("manifest_path"), + "eager execution.kernel_pack.manifest_path", + ), + "eager execution.kernel_pack.manifest_path", + ) + pack = _json_mapping(pack_path, "prepared eager kernel pack") + kernels = _sequence(pack.get("kernels"), "prepared eager kernel pack.kernels") + invocation_count = _integer( + summary.get("invocation_count"), + "eager inspection invocation_count", + ) + attachment_count = _integer( + summary.get("attachment_count"), + "eager inspection attachment_count", + ) + if attachment_count < invocation_count: + raise ArtifactError( + "eager execution has fewer attachments than canonical invocations" + ) + selector_domain_count = _integer( + summary.get("selector_domain_count"), + "eager inspection selector_domain_count", + ) + return _ExecutionInspection( + execution_mode="eager", + prepared_backend=_string( + pack.get("backend"), "prepared eager kernel pack.backend" + ), + prepared_kernel_count=len(kernels), + invocation_count=invocation_count, + attachment_count=attachment_count, + evaluation_alias_count=attachment_count - invocation_count, + finalization_count=_integer( + summary.get("finalization_count"), + "eager inspection finalization_count", + ), + closure_count=_integer( + summary.get("closure_count"), "eager inspection closure_count" + ), + selector_closure_available=selector_domain_count > 0, + selector_domain_count=selector_domain_count, + requested_point_tile_size=requested_tile, + workspace_limit_bytes=workspace_mib * 1024 * 1024, + native_profile_phases=_profile_phases(execution), + ) + + +def _recurrence_execution_inspection( + manifest: ArtifactManifest, + execution: Mapping[str, object], +) -> _ExecutionInspection: + declared_capabilities = frozenset( + str(value) + for value in _sequence( + execution.get("required_runtime_capabilities"), + "recurrence execution.required_runtime_capabilities", + ) + ) + _require_contract( + execution, + "builder_input_abi", + RECURRENCE_BUILDER_INPUT_ABI, + "recurrence execution", + ) + _require_contract( + execution, + "recurrence_plan_abi", + RECURRENCE_PLAN_ABI, + "recurrence execution", + ) + _require_contract( + execution, + "runtime_layout_abi", + RECURRENCE_RUNTIME_LAYOUT_ABI, + "recurrence execution", + ) + _require_contract( + execution, + "direct_template_abi", + RECURRENCE_DIRECT_TEMPLATE_ABI, + "recurrence execution", + ) + _require_contract( + execution, + "direct_backend_abi", + RECURRENCE_DIRECT_BACKEND_ABI, + "recurrence execution", + ) + runtime_options = _mapping( + execution.get("runtime_options"), + "recurrence execution.runtime_options", + ) + requested_tile = _integer( + runtime_options.get("point_tile_size"), + "recurrence execution.runtime_options.point_tile_size", + minimum=1, + ) + workspace_mib = _integer( + runtime_options.get("workspace_mib"), + "recurrence execution.runtime_options.workspace_mib", + minimum=1, + ) + plan = _mapping(execution.get("plan"), "recurrence execution.plan") + _require_contract( + plan, + "builder_input_abi", + RECURRENCE_BUILDER_INPUT_ABI, + "recurrence execution.plan", + ) + _require_contract( + plan, + "recurrence_plan_abi", + RECURRENCE_PLAN_ABI, + "recurrence execution.plan", + ) + _require_contract( + plan, + "runtime_layout_abi", + RECURRENCE_RUNTIME_LAYOUT_ABI, + "recurrence execution.plan", + ) + _require_contract( + plan, + "direct_template_abi", + RECURRENCE_DIRECT_TEMPLATE_ABI, + "recurrence execution.plan", + ) + _require_contract( + plan, + "direct_backend_abi", + RECURRENCE_DIRECT_BACKEND_ABI, + "recurrence execution.plan", + ) + runtime_schedule = _mapping( + plan.get("runtime_schedule"), + "recurrence execution.plan.runtime_schedule", + ) + _require_contract( + runtime_schedule, + "plan_member_path", + _RECURRENCE_DIRECT_SCHEDULE_MEMBER_PATH, + "recurrence execution.plan.runtime_schedule", + ) + binding = _mapping( + plan.get("process_binding"), + "recurrence execution.plan.process_binding", + ) + _require_contract( + binding, + "abi", + "pyamplicol-recurrence-process-binding-v4", + "recurrence execution.plan.process_binding", + ) + primary_process_digest = _string( + binding.get("process_digest"), + "recurrence execution.plan.process_binding.process_digest", + ) + if len(primary_process_digest) != 64 or any( + character not in "0123456789abcdef" for character in primary_process_digest + ): + raise ArtifactError( + "recurrence primary process binding has an invalid process digest" + ) + companion_process_digest = execution.get("process_digest") + if ( + companion_process_digest is not None + and companion_process_digest != primary_process_digest + ): + raise ArtifactError( + "recurrence companion process digest disagrees with its primary binding" + ) + summary = _mapping( + plan.get("inspection_summary"), + "recurrence execution.plan.inspection_summary", + ) + if ( + _string( + summary.get("execution_mode"), + "recurrence inspection execution_mode", + ) + != "recurrence" + ): + raise ArtifactError( + "recurrence inspection summary has the wrong execution mode" + ) + schedule = _mapping( + summary.get("schedule"), + "recurrence inspection schedule", + ) + direct_arena_raw = summary.get("direct_arena") + direct_arena = ( + {} + if direct_arena_raw is None + else _mapping(direct_arena_raw, "recurrence inspection direct_arena") + ) + pack_reference = _mapping( + execution.get("kernel_pack"), + "recurrence execution.kernel_pack", + ) + pack_path = _artifact_path( + manifest, + _string( + pack_reference.get("manifest_path"), + "recurrence execution.kernel_pack.manifest_path", + ), + "recurrence execution.kernel_pack.manifest_path", + ) + pack = _json_mapping(pack_path, "prepared recurrence kernel pack") + kernels = _sequence( + pack.get("kernels"), + "prepared recurrence kernel pack.kernels", + ) + prepared_kernel_count = _integer( + summary.get("prepared_kernel_count"), + "recurrence inspection prepared_kernel_count", + ) + kernel_ids = { + _integer( + _mapping( + value, + f"prepared recurrence kernel pack.kernels[{index}]", + ).get("kernel_id"), + f"prepared recurrence kernel pack.kernels[{index}].kernel_id", + ) + for index, value in enumerate(kernels) + } + direct_catalog = _mapping( + pack.get("recurrence_direct_template"), + "prepared recurrence kernel pack.recurrence_direct_template", + ) + direct_templates = _sequence( + direct_catalog.get("templates"), + "prepared recurrence direct templates", + ) + referenced_kernel_ids: set[int] = set() + for index, value in enumerate(direct_templates): + template = _mapping( + value, + f"prepared recurrence direct templates[{index}]", + ) + binding = _mapping( + template.get("payload_binding"), + f"prepared recurrence direct templates[{index}].payload_binding", + ) + kernel_id = binding.get("prepared_kernel_id") + if kernel_id is None: + continue + parsed_kernel_id = _integer( + kernel_id, + f"prepared recurrence direct templates[{index}].prepared_kernel_id", + ) + if parsed_kernel_id not in kernel_ids: + raise ArtifactError( + "recurrence direct template references an absent prepared kernel" + ) + referenced_kernel_ids.add(parsed_kernel_id) + complete_companion_catalog = ( + RECURRENCE_HELICITY_SELECTOR_COMPANION_RUNTIME_CAPABILITY + in declared_capabilities + ) + if ( + not complete_companion_catalog + and prepared_kernel_count != len(referenced_kernel_ids) + ) or ( + complete_companion_catalog + and not 0 < prepared_kernel_count <= len(referenced_kernel_ids) + ): + raise ArtifactError( + "recurrence inspection prepared-kernel count does not match its " + "direct template catalog" + ) + semantic_components = _optional_counter( + direct_arena, + "semantic_component_count", + "recurrence inspection direct_arena.semantic_component_count", + ) + arena_components = _optional_counter( + direct_arena, + "current_arena_components", + "recurrence inspection direct_arena.current_arena_components", + ) + arena_reuse = _optional_counter( + direct_arena, + "arena_component_reuse_count", + "recurrence inspection direct_arena.arena_component_reuse_count", + ) + if ( + semantic_components is not None + and arena_components is not None + and arena_reuse is not None + and semantic_components - arena_components != arena_reuse + ): + raise ArtifactError( + "recurrence direct-arena component reuse count is inconsistent" + ) + packed_input_bytes = _optional_counter( + direct_arena, + "packed_input_bytes", + "recurrence inspection direct_arena.packed_input_bytes", + ) + packed_output_bytes = _optional_counter( + direct_arena, + "packed_output_bytes", + "recurrence inspection direct_arena.packed_output_bytes", + ) + scatter_bytes = _optional_counter( + direct_arena, + "scatter_bytes", + "recurrence inspection direct_arena.scatter_bytes", + ) + for label, value in ( + ("packed-input bytes", packed_input_bytes), + ("packed-output bytes", packed_output_bytes), + ("scatter bytes", scatter_bytes), + ): + if value not in {None, 0}: + raise ArtifactError(f"recurrence direct-arena {label} must be zero") + source_rows = _optional_counter( + schedule, + "source_row_count", + "recurrence inspection source row count", + ) + contribution_rows = _optional_counter( + schedule, + "contribution_count", + "recurrence inspection contribution count", + ) + finalization_rows = _optional_counter( + schedule, + "finalization_count", + "recurrence inspection finalization count", + ) + closure_rows = _optional_counter( + schedule, + "closure_term_count", + "recurrence inspection closure row count", + ) + runtime_metadata = _mapping( + execution.get("runtime_metadata"), + "recurrence execution.runtime_metadata", + ) + color_reference_raw = runtime_metadata.get("color_contraction") + color_accuracy = None + color_storage = None + color_sector_count = None + color_active_sector_count = None + color_component_count = None + color_group_count = None + color_destination_count = None + color_entry_count = None + color_logical_entry_count = None + color_factorization_kind = None + color_factorization_rank = None + color_factorization_coset_count = None + color_fft_provenance = None + if color_reference_raw is not None: + color_reference = _mapping( + color_reference_raw, + "recurrence execution.runtime_metadata.color_contraction", + ) + _require_contract( + color_reference, + "abi", + _RECURRENCE_COLOR_CONTRACTION_CODEC_ABI, + "recurrence execution.runtime_metadata.color_contraction", + ) + color_accuracy = _string( + color_reference.get("color_accuracy"), + "recurrence color-contraction accuracy", + ) + if color_accuracy not in {"nlc", "full"}: + raise ArtifactError( + "recurrence color-contraction accuracy must be nlc or full" + ) + color_storage = _string( + color_reference.get("storage"), + "recurrence color-contraction storage", + ) + if color_storage not in {"expanded", "repeated", "convolution-kernels"}: + raise ArtifactError( + "recurrence color-contraction storage must be expanded, repeated, " + "or convolution-kernels" + ) + color_sector_count = _integer( + color_reference.get("sector_count"), + "recurrence color-contraction sector count", + minimum=1, + ) + color_active_sector_count = _integer( + color_reference.get("active_sector_count"), + "recurrence color-contraction active sector count", + minimum=1, + ) + color_component_count = _integer( + color_reference.get("component_count"), + "recurrence color-contraction component count", + minimum=1, + ) + color_group_count = _integer( + color_reference.get("group_count"), + "recurrence color-contraction group count", + minimum=1, + ) + color_destination_count = _integer( + color_reference.get("destination_count"), + "recurrence color-contraction destination count", + minimum=1, + ) + color_entry_count = _integer( + color_reference.get("entry_count"), + "recurrence color-contraction entry count", + minimum=1, + ) + color_logical_entry_count = _integer( + color_reference.get("logical_entry_count"), + "recurrence color-contraction logical entry count", + minimum=1, + ) + if color_active_sector_count > color_sector_count: + raise ArtifactError( + "recurrence active color sectors exceed the physical sector count" + ) + if color_destination_count < color_group_count: + raise ArtifactError( + "recurrence color destinations do not cover contraction groups" + ) + if color_group_count > color_active_sector_count * color_component_count: + raise ArtifactError( + "recurrence color groups exceed the active sector-helicity domain" + ) + factorization_raw = color_reference.get("factorization") + if factorization_raw is not None: + factorization = _mapping( + factorization_raw, + "recurrence color-contraction factorization", + ) + color_factorization_kind = _string( + factorization.get("kind"), + "recurrence color-contraction factorization kind", + ) + color_factorization_rank = _integer( + factorization.get("rank"), + "recurrence color-contraction factorization rank", + minimum=2, + ) + color_factorization_coset_count = _integer( + factorization.get("coset_count"), + "recurrence color-contraction factorization coset count", + minimum=1, + ) + valid_factorization = ( + ( + color_storage == "repeated" + and color_factorization_kind == "klein-four-walsh" + and color_factorization_rank == 2 + ) + or ( + color_storage == "repeated" + and color_factorization_kind == "elementary-abelian-walsh" + and 3 <= color_factorization_rank <= 31 + ) + or ( + color_storage == "convolution-kernels" + and color_factorization_kind == "symmetric-group-fourier" + and 2 <= color_factorization_rank <= 10 + ) + ) + if not valid_factorization: + raise ArtifactError( + "recurrence color factorization kind, rank, and storage disagree" + ) + symmetric_group_fft = color_factorization_kind == "symmetric-group-fourier" + if (color_storage == "convolution-kernels") != symmetric_group_fft: + raise ArtifactError( + "recurrence convolution-kernel storage requires symmetric-group " + "Fourier factorization" + ) + if ( + SYMMETRIC_GROUP_FFT_COLOR_RUNTIME_CAPABILITY in declared_capabilities + ) != symmetric_group_fft: + raise ArtifactError( + "recurrence symmetric-group Fourier metadata and runtime capability " + "disagree" + ) + color_fft_provenance = _symmetric_group_fft_provenance( + color_reference, + factorization_kind=color_factorization_kind, + degree=color_factorization_rank, + channel_count=color_factorization_coset_count, + group_count=color_group_count, + component_count=color_component_count, + entry_count=color_entry_count, + logical_entry_count=color_logical_entry_count, + context="recurrence color-contraction", + ) + elif SYMMETRIC_GROUP_FFT_COLOR_RUNTIME_CAPABILITY in declared_capabilities: + raise ArtifactError( + "recurrence symmetric-group Fourier capability has no color payload" + ) + return _ExecutionInspection( + execution_mode="recurrence", + prepared_backend=_string( + pack.get("backend"), + "prepared recurrence kernel pack.backend", + ), + prepared_kernel_count=prepared_kernel_count, + invocation_count=contribution_rows, + attachment_count=0, + finalization_count=finalization_rows, + closure_count=closure_rows, + requested_point_tile_size=requested_tile, + effective_point_tile_size=requested_tile, + workspace_limit_bytes=workspace_mib * 1024 * 1024, + arena_semantic_component_count=semantic_components, + arena_component_count=arena_components, + arena_component_reuse_count=arena_reuse, + momentum_form_count=_optional_counter( + direct_arena, + "momentum_form_count", + "recurrence inspection direct_arena.momentum_form_count", + ), + direct_source_row_count=source_rows, + direct_contribution_row_count=contribution_rows, + direct_finalization_row_count=finalization_rows, + direct_closure_row_count=closure_rows, + direct_row_group_count=_optional_counter( + direct_arena, + "row_group_count", + "recurrence inspection direct_arena.row_group_count", + ), + recurrence_color_accuracy=color_accuracy, + recurrence_color_storage=color_storage, + recurrence_color_sector_count=color_sector_count, + recurrence_color_active_sector_count=color_active_sector_count, + recurrence_color_component_count=color_component_count, + recurrence_color_group_count=color_group_count, + recurrence_color_destination_count=color_destination_count, + recurrence_color_entry_count=color_entry_count, + recurrence_color_logical_entry_count=color_logical_entry_count, + recurrence_color_factorization_kind=color_factorization_kind, + recurrence_color_factorization_rank=color_factorization_rank, + recurrence_color_factorization_coset_count=(color_factorization_coset_count), + recurrence_color_contraction_method=( + None + if color_fft_provenance is None + else cast(str, color_fft_provenance["method"]) + ), + recurrence_color_fft_degree=( + None + if color_fft_provenance is None + else cast(int, color_fft_provenance["degree"]) + ), + recurrence_color_fft_channel_count=( + None + if color_fft_provenance is None + else cast(int, color_fft_provenance["channel_count"]) + ), + recurrence_color_fft_covered_local_group_count=( + None + if color_fft_provenance is None + else cast(int, color_fft_provenance["covered_local_group_count"]) + ), + recurrence_color_direct_residual_group_count=( + None + if color_fft_provenance is None + else cast(int, color_fft_provenance["residual_group_count"]) + ), + recurrence_color_direct_residual_entry_count=( + None + if color_fft_provenance is None + else cast(int, color_fft_provenance["residual_entry_count"]) + ), + recurrence_color_raw_kernel_bytes=( + None + if color_fft_provenance is None + else cast(int, color_fft_provenance["raw_kernel_bytes"]) + ), + recurrence_color_transformed_kernel_bytes=( + None + if color_fft_provenance is None + else cast(int, color_fft_provenance["transformed_kernel_bytes"]) + ), + recurrence_color_fft_capability=( + None + if color_fft_provenance is None + else cast(str, color_fft_provenance["capability"]) + ), + packed_input_bytes=0, + packed_output_bytes=0, + scatter_bytes=0, + native_profile_phases=_RECURRENCE_PROFILE_PHASES, + ) + + +def _symmetric_group_fft_provenance( + reference: Mapping[str, object], + *, + factorization_kind: str | None, + degree: int | None, + channel_count: int | None, + group_count: int, + component_count: int, + entry_count: int, + logical_entry_count: int, + context: str, +) -> dict[str, int | str] | None: + raw = reference.get("fft_provenance") + if factorization_kind != "symmetric-group-fourier": + if raw is not None or "fft_provenance" in reference: + raise ArtifactError(f"{context} non-FFT summary carries FFT provenance") + return None + if degree is None or channel_count is None: + raise ArtifactError(f"{context} symmetric-group factorization is incomplete") + provenance = _mapping(raw, f"{context} FFT provenance") + expected_fields = { + "method", + "degree", + "channel_count", + "covered_local_group_count", + "residual_group_count", + "residual_entry_count", + "raw_kernel_bytes", + "transformed_kernel_bytes", + "capability", + } + if set(provenance) != expected_fields: + raise ArtifactError(f"{context} FFT provenance has unsupported fields") + if group_count % component_count: + raise ArtifactError(f"{context} FFT component map is not rectangular") + local_group_count = group_count // component_count + group_order = math.factorial(degree) + covered_local_group_count = channel_count * group_order + if covered_local_group_count > local_group_count: + raise ArtifactError(f"{context} FFT channels exceed the local group domain") + residual_group_count = local_group_count - covered_local_group_count + residual_entry_count = ( + local_group_count * (local_group_count + 1) // 2 + - covered_local_group_count * (covered_local_group_count + 1) // 2 + ) + kernel_entry_count = channel_count * (channel_count + 1) // 2 * group_order + raw_kernel_bytes = kernel_entry_count * 16 + transformed_kernel_bytes = kernel_entry_count * 8 + values: dict[str, int | str] = { + "method": _string(provenance.get("method"), f"{context} FFT method"), + "degree": _integer(provenance.get("degree"), f"{context} FFT degree"), + "channel_count": _integer( + provenance.get("channel_count"), f"{context} FFT channel count" + ), + "covered_local_group_count": _integer( + provenance.get("covered_local_group_count"), + f"{context} FFT-covered local group count", + ), + "residual_group_count": _integer( + provenance.get("residual_group_count"), + f"{context} direct residual group count", + ), + "residual_entry_count": _integer( + provenance.get("residual_entry_count"), + f"{context} direct residual entry count", + ), + "raw_kernel_bytes": _integer( + provenance.get("raw_kernel_bytes"), f"{context} raw kernel bytes" + ), + "transformed_kernel_bytes": _integer( + provenance.get("transformed_kernel_bytes"), + f"{context} transformed kernel bytes", + ), + "capability": _string( + provenance.get("capability"), f"{context} FFT capability" + ), + } + expected: dict[str, int | str] = { + "method": "symmetric-group-fourier", + "degree": degree, + "channel_count": channel_count, + "covered_local_group_count": covered_local_group_count, + "residual_group_count": residual_group_count, + "residual_entry_count": residual_entry_count, + "raw_kernel_bytes": raw_kernel_bytes, + "transformed_kernel_bytes": transformed_kernel_bytes, + "capability": SYMMETRIC_GROUP_FFT_COLOR_RUNTIME_CAPABILITY, + } + if ( + values != expected + or entry_count != kernel_entry_count + residual_entry_count + or logical_entry_count != entry_count * component_count + ): + raise ArtifactError(f"{context} symmetric-group FFT provenance is inconsistent") + return values + + +def _on_the_fly_color_reference( + runtime_metadata: Mapping[str, object], + *, + color_accuracy: str, + contracted: bool, +) -> dict[str, Any] | None: + raw = runtime_metadata.get("color_contraction") + if not contracted: + if raw is not None or "color_contraction" in runtime_metadata: + raise ArtifactError( + "LC on-the-fly execution unexpectedly carries color contraction" + ) + return None + reference = _mapping( + raw, + "on-the-fly execution.runtime_metadata.color_contraction", + ) + expected_fields = { + "abi", + "color_accuracy", + "storage", + "includes_color_factor", + "group_count", + "sector_count", + "active_sector_count", + "component_count", + "destination_count", + "entry_count", + "logical_entry_count", + "semantic_digest", + "factorization", + "path", + "size_bytes", + "sha256", + } + if reference.get("factorization") is not None: + expected_fields.add("fft_provenance") + if set(reference) != expected_fields: + raise ArtifactError( + "on-the-fly color-contraction reference has unsupported fields" + ) + _require_contract( + reference, + "abi", + _RECURRENCE_COLOR_CONTRACTION_CODEC_ABI, + "on-the-fly execution.runtime_metadata.color_contraction", + ) + accuracy = _string( + reference.get("color_accuracy"), + "on-the-fly color-contraction accuracy", + ) + storage = _string( + reference.get("storage"), + "on-the-fly color-contraction storage", + ) + sector_count = _integer( + reference.get("sector_count"), + "on-the-fly color-contraction sector count", + minimum=1, + ) + active_sector_count = _integer( + reference.get("active_sector_count"), + "on-the-fly color-contraction active sector count", + minimum=1, + ) + component_count = _integer( + reference.get("component_count"), + "on-the-fly color-contraction component count", + minimum=1, + ) + group_count = _integer( + reference.get("group_count"), + "on-the-fly color-contraction group count", + minimum=1, + ) + destination_count = _integer( + reference.get("destination_count"), + "on-the-fly color-contraction destination count", + minimum=1, + ) + entry_count = _integer( + reference.get("entry_count"), + "on-the-fly color-contraction entry count", + minimum=1, + ) + logical_entry_count = _integer( + reference.get("logical_entry_count"), + "on-the-fly color-contraction logical entry count", + minimum=1, + ) + semantic_digest = _string( + reference.get("semantic_digest"), + "on-the-fly color-contraction semantic digest", + ) + payload_sha256 = _string( + reference.get("sha256"), + "on-the-fly color-contraction SHA-256", + ) + factorization_raw = reference.get("factorization") + symmetric_group_fft = False + factorization_rank = None + factorization_coset_count = None + if factorization_raw is not None: + factorization = _mapping( + factorization_raw, + "on-the-fly color-contraction factorization", + ) + if set(factorization) != {"kind", "rank", "coset_count"}: + raise ArtifactError( + "on-the-fly color-contraction factorization has unsupported fields" + ) + factorization_kind = _string( + factorization.get("kind"), + "on-the-fly color-contraction factorization kind", + ) + factorization_rank = _integer( + factorization.get("rank"), + "on-the-fly color-contraction factorization degree", + minimum=2, + ) + factorization_coset_count = _integer( + factorization.get("coset_count"), + "on-the-fly color-contraction channel count", + minimum=1, + ) + symmetric_group_fft = ( + factorization_kind == "symmetric-group-fourier" and factorization_rank <= 10 + ) + if not symmetric_group_fft: + raise ArtifactError( + "on-the-fly color factorization must be symmetric-group Fourier" + ) + if ( + accuracy != color_accuracy + or accuracy not in {"nlc", "full"} + or storage != ("convolution-kernels" if symmetric_group_fft else "expanded") + or reference.get("includes_color_factor") is not True + or component_count != 1 + or active_sector_count != group_count + or destination_count != group_count + or group_count > sector_count + or logical_entry_count != entry_count + or reference.get("path") != "on-the-fly-color.bin" + or semantic_digest != payload_sha256 + or len(payload_sha256) != 64 + or any(character not in "0123456789abcdef" for character in payload_sha256) + ): + raise ArtifactError("on-the-fly color-contraction reference is noncanonical") + fft_provenance = _symmetric_group_fft_provenance( + reference, + factorization_kind=("symmetric-group-fourier" if symmetric_group_fft else None), + degree=factorization_rank, + channel_count=factorization_coset_count, + group_count=group_count, + component_count=component_count, + entry_count=entry_count, + logical_entry_count=logical_entry_count, + context="on-the-fly color-contraction", + ) + _integer( + reference.get("size_bytes"), + "on-the-fly color-contraction payload size", + minimum=1, + ) + return { + "color_accuracy": accuracy, + "storage": storage, + "sector_count": sector_count, + "active_sector_count": active_sector_count, + "component_count": component_count, + "group_count": group_count, + "destination_count": destination_count, + "entry_count": entry_count, + "logical_entry_count": logical_entry_count, + "factorization_kind": ( + "symmetric-group-fourier" if symmetric_group_fft else None + ), + "factorization_rank": factorization_rank, + "factorization_coset_count": factorization_coset_count, + "contraction_method": ( + None if fft_provenance is None else fft_provenance["method"] + ), + "fft_degree": (None if fft_provenance is None else fft_provenance["degree"]), + "fft_channel_count": ( + None if fft_provenance is None else fft_provenance["channel_count"] + ), + "fft_covered_local_group_count": ( + None + if fft_provenance is None + else fft_provenance["covered_local_group_count"] + ), + "direct_residual_group_count": ( + None if fft_provenance is None else fft_provenance["residual_group_count"] + ), + "direct_residual_entry_count": ( + None if fft_provenance is None else fft_provenance["residual_entry_count"] + ), + "raw_kernel_bytes": ( + None if fft_provenance is None else fft_provenance["raw_kernel_bytes"] + ), + "transformed_kernel_bytes": ( + None + if fft_provenance is None + else fft_provenance["transformed_kernel_bytes"] + ), + "fft_capability": ( + None if fft_provenance is None else fft_provenance["capability"] + ), + } + + +def _execution_inspection( + manifest: ArtifactManifest, + execution_path: Path, + required_runtime_capabilities: Sequence[object] | None = None, + *, + process: Mapping[str, object] | None = None, +) -> _ExecutionInspection: + capabilities = ( + required_runtime_capabilities + if required_runtime_capabilities is not None + else _sequence( + manifest.runtime.get("required_runtime_capabilities"), + "runtime.required_runtime_capabilities", + ) + ) + capability_values = frozenset(str(value) for value in capabilities) + on_the_fly_color_capabilities = { + ON_THE_FLY_LC_COLOR_RUNTIME_CAPABILITY, + ON_THE_FLY_CONTRACTED_COLOR_RUNTIME_CAPABILITY, + } + if ( + ON_THE_FLY_RUNTIME_CAPABILITY in capability_values + or not on_the_fly_color_capabilities.isdisjoint(capability_values) + ): + selected_color_capabilities = capability_values.intersection( + on_the_fly_color_capabilities + ) + if len(selected_color_capabilities) != 1: + raise ArtifactError( + "on-the-fly execution must declare exactly one color capability" + ) + selected_color_capability = next(iter(selected_color_capabilities)) + on_the_fly_capabilities = { + ON_THE_FLY_RUNTIME_CAPABILITY, + selected_color_capability, + } + symmetric_group_fft_capability = ( + SYMMETRIC_GROUP_FFT_COLOR_RUNTIME_CAPABILITY in capability_values + ) + if symmetric_group_fft_capability: + on_the_fly_capabilities.add(SYMMETRIC_GROUP_FFT_COLOR_RUNTIME_CAPABILITY) + if capability_values != on_the_fly_capabilities or len(capabilities) != len( + on_the_fly_capabilities + ): + raise ArtifactError( + "on-the-fly execution must declare exactly its required runtime " + "capabilities" + ) + contracted_color = ( + selected_color_capability == ON_THE_FLY_CONTRACTED_COLOR_RUNTIME_CAPABILITY + ) + expected_color_accuracies = {"nlc", "full"} if contracted_color else {"lc"} + execution = _json_mapping(execution_path, "process execution manifest") + expected_root_fields = { + "schema_version", + "kind", + "required_runtime_capabilities", + "process", + "key", + "color_accuracy", + "external_pdg_order", + "kernel_pack", + "runtime_options", + "selector_policy", + "runtime_metadata", + "runtime_container", + } + if process is None or set(execution) != expected_root_fields: + raise ArtifactError( + "on-the-fly process execution manifest has unsupported root fields" + ) + kind = _string(execution.get("kind"), "process execution manifest.kind") + declared_capabilities = _sequence( + execution.get("required_runtime_capabilities"), + "on-the-fly process execution manifest.required_runtime_capabilities", + ) + declared_capability_values = frozenset( + str(value) for value in declared_capabilities + ) + external_pdgs = tuple( + _sequence( + execution.get("external_pdg_order"), + "on-the-fly process execution manifest.external_pdg_order", + ) + ) + expected_pdgs = tuple( + _sequence( + process.get("external_pdgs"), + f"process {process.get('id')} external_pdgs", + ) + ) + if ( + any( + isinstance(value, bool) or not isinstance(value, int) + for value in (*external_pdgs, *expected_pdgs) + ) + or execution.get("schema_version") != 3 + or kind != _ON_THE_FLY_RUNTIME_KIND + or len(declared_capabilities) != len(on_the_fly_capabilities) + or declared_capability_values != on_the_fly_capabilities + or execution.get("process") != process.get("expression") + or execution.get("key") != process.get("id") + or execution.get("color_accuracy") not in expected_color_accuracies + or process.get("color_accuracy") != execution.get("color_accuracy") + or external_pdgs != expected_pdgs + ): + raise ArtifactError( + "on-the-fly process execution manifest disagrees with its outer process" + ) + kernel_pack = _mapping( + execution.get("kernel_pack"), + "on-the-fly execution.kernel_pack", + ) + if kernel_pack != { + "manifest_path": "model/eager-kernel-pack.json", + "payload_root": "model/eager-kernels", + }: + raise ArtifactError("on-the-fly prepared-kernel paths are not canonical") + runtime_container = _mapping( + execution.get("runtime_container"), + "on-the-fly execution.runtime_container", + ) + if runtime_container != { + "kind": "pyamplicol-on-the-fly-runtime-container", + "schema_version": 1, + "storage_abi": "pacbin-v1", + "path": "on-the-fly-runtime.pacbin", + "seed_member_path": "on-the-fly/process-seed-v1.bin", + }: + raise ArtifactError("on-the-fly runtime-container contract is noncanonical") + runtime_metadata = _mapping( + execution.get("runtime_metadata"), + "on-the-fly execution.runtime_metadata", + ) + color_reference = _on_the_fly_color_reference( + runtime_metadata, + color_accuracy=str(execution.get("color_accuracy")), + contracted=contracted_color, + ) + reference_uses_symmetric_group_fft = ( + color_reference is not None + and color_reference["factorization_kind"] == "symmetric-group-fourier" + ) + if reference_uses_symmetric_group_fft != symmetric_group_fft_capability: + raise ArtifactError( + "on-the-fly symmetric-group Fourier metadata and runtime " + "capability disagree" + ) + selector_policy = _mapping( + execution.get("selector_policy"), + "on-the-fly execution.selector_policy", + ) + if set(selector_policy) - {"color_basis"} != { + "color_coverage", + "reference_color_word", + "trace_reflections_folded", + "selector_census", + }: + raise ArtifactError( + "on-the-fly execution.selector_policy has unsupported fields" + ) + color_basis = selector_policy.get("color_basis", "trace") + if color_basis not in ("trace", "adjoint"): + raise ArtifactError( + "on-the-fly execution.selector_policy.color_basis " + "must be trace or adjoint" + ) + if color_basis == "adjoint" and not contracted_color: + raise ArtifactError("adjoint on-the-fly color requires contracted NLC/full") + expected_color_coverage = "contracted" if contracted_color else "complete" + if selector_policy.get("color_coverage") != expected_color_coverage: + raise ArtifactError( + "on-the-fly execution.selector_policy.color_coverage disagrees " + "with its color capability" + ) + trace_reflections_folded = selector_policy.get("trace_reflections_folded") + if not isinstance(trace_reflections_folded, bool): + raise ArtifactError( + "on-the-fly execution.selector_policy.trace_reflections_folded " + "must be a boolean" + ) + if contracted_color and trace_reflections_folded: + raise ArtifactError( + "contracted on-the-fly color cannot fold trace reflections" + ) + reference = selector_policy.get("reference_color_word") + if reference is not None: + word = tuple( + _integer( + value, + "on-the-fly execution.selector_policy.reference_color_word" + f"[{index}]", + minimum=1, + ) + for index, value in enumerate( + _sequence( + reference, + "on-the-fly execution.selector_policy.reference_color_word", + ) + ) + ) + if not word or len(set(word)) != len(word): + raise ArtifactError( + "on-the-fly execution selector reference must contain unique " + "positive labels" + ) + census = _mapping( + selector_policy.get("selector_census"), + "on-the-fly execution.selector_policy.selector_census", + ) + if set(census) != { + "physical_helicity_count", + "physical_color_flow_count", + }: + raise ArtifactError( + "on-the-fly execution selector census has unsupported fields" + ) + helicity_count = _integer( + census.get("physical_helicity_count"), + "on-the-fly execution selector census physical_helicity_count", + minimum=1, + ) + color_flow_count = _integer( + census.get("physical_color_flow_count"), + "on-the-fly execution selector census physical_color_flow_count", + minimum=1, + ) + if helicity_count > (1 << 64) - 1 or color_flow_count > (1 << 64) - 1: + raise ArtifactError( + "on-the-fly execution selector census exceeds the u64 domain" + ) + if contracted_color and color_flow_count != 1: + raise ArtifactError( + "contracted on-the-fly execution must expose one physical color result" + ) + runtime_options = _mapping( + execution.get("runtime_options"), + "on-the-fly execution.runtime_options", + ) + if set(runtime_options) != { + "point_tile_size", + "query_construction_threads", + }: + raise ArtifactError("on-the-fly execution.runtime_options is invalid") + point_tile_size = _integer( + runtime_options.get("point_tile_size"), + "on-the-fly execution.runtime_options.point_tile_size", + minimum=1, + ) + query_construction_threads = _integer( + runtime_options.get("query_construction_threads"), + "on-the-fly execution.runtime_options.query_construction_threads", + minimum=1, + ) + return _ExecutionInspection( + execution_mode="on-the-fly", + physical_helicity_count=helicity_count, + physical_color_flow_count=color_flow_count, + requested_point_tile_size=point_tile_size, + requested_query_construction_threads=query_construction_threads, + recurrence_color_accuracy=( + None if color_reference is None else color_reference["color_accuracy"] + ), + recurrence_color_storage=( + None if color_reference is None else color_reference["storage"] + ), + recurrence_color_sector_count=( + None if color_reference is None else color_reference["sector_count"] + ), + recurrence_color_active_sector_count=( + None + if color_reference is None + else color_reference["active_sector_count"] + ), + recurrence_color_component_count=( + None if color_reference is None else color_reference["component_count"] + ), + recurrence_color_group_count=( + None if color_reference is None else color_reference["group_count"] + ), + recurrence_color_destination_count=( + None + if color_reference is None + else color_reference["destination_count"] + ), + recurrence_color_entry_count=( + None if color_reference is None else color_reference["entry_count"] + ), + recurrence_color_logical_entry_count=( + None + if color_reference is None + else color_reference["logical_entry_count"] + ), + recurrence_color_factorization_kind=( + None + if color_reference is None + else color_reference["factorization_kind"] + ), + recurrence_color_factorization_rank=( + None + if color_reference is None + else color_reference["factorization_rank"] + ), + recurrence_color_factorization_coset_count=( + None + if color_reference is None + else color_reference["factorization_coset_count"] + ), + recurrence_color_contraction_method=( + None + if color_reference is None + else color_reference["contraction_method"] + ), + recurrence_color_fft_degree=( + None if color_reference is None else color_reference["fft_degree"] + ), + recurrence_color_fft_channel_count=( + None + if color_reference is None + else color_reference["fft_channel_count"] + ), + recurrence_color_fft_covered_local_group_count=( + None + if color_reference is None + else color_reference["fft_covered_local_group_count"] + ), + recurrence_color_direct_residual_group_count=( + None + if color_reference is None + else color_reference["direct_residual_group_count"] + ), + recurrence_color_direct_residual_entry_count=( + None + if color_reference is None + else color_reference["direct_residual_entry_count"] + ), + recurrence_color_raw_kernel_bytes=( + None if color_reference is None else color_reference["raw_kernel_bytes"] + ), + recurrence_color_transformed_kernel_bytes=( + None + if color_reference is None + else color_reference["transformed_kernel_bytes"] + ), + recurrence_color_fft_capability=( + None if color_reference is None else color_reference["fft_capability"] + ), + native_profile_phases=_ON_THE_FLY_PROFILE_PHASES, + ) + if RECURRENCE_DIRECT_ARENA_RUNTIME_CAPABILITY in capability_values: + color_capabilities = capability_values.intersection( + { + RECURRENCE_COLOR_RUNTIME_CAPABILITY, + RECURRENCE_CONTRACTED_COLOR_RUNTIME_CAPABILITY, + } + ) + if len(color_capabilities) != 1: + raise ArtifactError( + "recurrence direct-arena execution must declare exactly one " + "recurrence color capability" + ) + required = { + *color_capabilities, + RECURRENCE_DIRECT_ARENA_RUNTIME_CAPABILITY, + } + if not required.issubset(capability_values): + missing = ", ".join(sorted(required - capability_values)) + raise ArtifactError( + "recurrence direct-arena execution is missing required " + f"capabilities: {missing}" + ) + execution = _json_mapping(execution_path, "process execution manifest") + kind = _string(execution.get("kind"), "process execution manifest.kind") + if kind != _RECURRENCE_RUNTIME_KIND: + raise ArtifactError( + "recurrence capability is paired with an unsupported process " + f"execution kind {kind!r}" + ) + return _recurrence_execution_inspection(manifest, execution) + if _EAGER_RUNTIME_CAPABILITIES.isdisjoint(capability_values): + # Compiled execution manifests can be several GiB at high multiplicity. + # Their capability contract already identifies the execution lane, and + # the manifest hash validation above has verified the payload bytes. + return _ExecutionInspection( + execution_mode="compiled", + native_profile_phases=_COMPILED_PROFILE_PHASES, + ) + execution = _json_mapping(execution_path, "process execution manifest") + kind = _string(execution.get("kind"), "process execution manifest.kind") + if kind == _EAGER_RUNTIME_KIND: + return _eager_execution_inspection(manifest, execution, execution_path) + raise ArtifactError(f"unsupported process execution kind {kind!r}") + + +def _optional_nonnegative_integer( + value: object, + context: str, +) -> int | None: + if value is None: + return None + return _integer(value, context, minimum=0) + + +def _optional_counter( + record: Mapping[str, object], + name: str, + context: str, +) -> int | None: + if name not in record: + return None + return _integer(record[name], context, minimum=0) + + +def _require_contract( + record: Mapping[str, object], + name: str, + expected: str, + context: str, +) -> None: + actual = record.get(name) + if actual != expected: + raise ArtifactError( + f"{context}.{name} must be {expected!r}; regenerate the recurrence " + "artifact with the current pyAmpliCol" + ) + + +def _source_helicity_selection( + value: object, + context: str, +) -> tuple[tuple[int, int], ...]: + selection = _mapping(value, context) + result: list[tuple[int, int]] = [] + for raw_label, raw_helicity in selection.items(): + try: + label = int(raw_label) + except (TypeError, ValueError) as exc: + raise ArtifactError(f"{context} has a non-integer source label") from exc + if str(label) != str(raw_label) or label < 1: + raise ArtifactError(f"{context} has an invalid source label {raw_label!r}") + if isinstance(raw_helicity, bool) or not isinstance(raw_helicity, int): + raise ArtifactError(f"{context}[{raw_label!r}] must be an integer") + result.append((label, raw_helicity)) + result.sort() + if len({label for label, _helicity in result}) != len(result): + raise ArtifactError(f"{context} repeats a source label") + return tuple(result) + + +def _color_sector_selection( + value: object, + context: str, +) -> tuple[int, ...]: + result = tuple( + _integer(item, f"{context}[{index}]") + for index, item in enumerate(_sequence(value, context)) + ) + if result != tuple(sorted(set(result))): + raise ArtifactError(f"{context} must contain unique sorted sector IDs") + return result + + +def _physics_inspection( + manifest: ArtifactManifest, + process: Mapping[str, object], + execution: _ExecutionInspection, +) -> _PhysicsInspection: + relative = str(process["physics_path"]) + path = manifest.root / relative + try: + payload = json.loads(path.read_text(encoding="utf-8")) + except OSError as exc: + raise ArtifactError( + f"cannot read runtime physics metadata {relative}: {exc}" + ) from exc + except json.JSONDecodeError as exc: + raise ArtifactError( + f"invalid runtime physics metadata {relative}: {exc}" + ) from exc + + physics = _mapping(payload, f"runtime physics metadata {relative}") + if execution.execution_mode == "on-the-fly": + if set(physics) != { + "schema_version", + "kind", + "process_id", + "process", + "color_accuracy", + "external_particles", + "model_parameters", + }: + raise ArtifactError( + f"{relative} compact on-the-fly metadata has unsupported fields" + ) + if ( + physics.get("schema_version") != 1 + or physics.get("kind") != _ON_THE_FLY_PUBLIC_METADATA_KIND + or physics.get("process_id") != process.get("id") + or physics.get("process") != process.get("expression") + or physics.get("color_accuracy") not in {"lc", "nlc", "full"} + or process.get("color_accuracy") != physics.get("color_accuracy") + ): + raise ArtifactError( + f"{relative} compact on-the-fly metadata disagrees with its process" + ) + external_pdgs = _sequence( + process.get("external_pdgs"), + f"process {process.get('id')} external_pdgs", + ) + externals = _sequence( + physics.get("external_particles"), + f"{relative}.external_particles", + ) + if len(externals) != len(external_pdgs): + raise ArtifactError( + f"{relative}.external_particles disagrees with the process arity" + ) + for index, (raw_external, pdg) in enumerate( + zip(externals, external_pdgs, strict=True) + ): + external = _mapping( + raw_external, + f"{relative}.external_particles[{index}]", + ) + if ( + set(external) + != { + "index", + "label", + "particle", + "pdg", + "role", + "momentum_slot", + "momentum_components", + } + or external.get("index") != index + or external.get("label") != index + 1 + or isinstance(external.get("pdg"), bool) + or not isinstance(external.get("pdg"), int) + or external.get("pdg") != pdg + or external.get("momentum_slot") != index + or external.get("role") != ("initial" if index < 2 else "final") + or external.get("momentum_components") != ["E", "px", "py", "pz"] + ): + raise ArtifactError( + f"{relative}.external_particles[{index}] is noncanonical" + ) + _string( + external.get("particle"), + f"{relative}.external_particles[{index}].particle", + ) + parameter_names: set[str] = set() + for index, raw_parameter in enumerate( + _sequence(physics.get("model_parameters"), f"{relative}.model_parameters") + ): + parameter = _mapping( + raw_parameter, + f"{relative}.model_parameters[{index}]", + ) + if set(parameter) != { + "name", + "kind", + "default_real", + "default_imaginary", + "mutable", + }: + raise ArtifactError( + f"{relative}.model_parameters[{index}] has unsupported fields" + ) + name = _string( + parameter.get("name"), + f"{relative}.model_parameters[{index}].name", + ) + kind = parameter.get("kind") + mutable = parameter.get("mutable") + defaults = ( + parameter.get("default_real"), + parameter.get("default_imaginary"), + ) + if ( + name in parameter_names + or kind + not in { + "normalization", + "mass", + "width", + "coupling", + "external", + "derived", + } + or not isinstance(mutable, bool) + or (kind == "derived" and mutable) + or any( + isinstance(value, bool) + or not isinstance(value, int | float) + or not math.isfinite(value) + for value in defaults + ) + ): + raise ArtifactError( + f"{relative}.model_parameters[{index}] is noncanonical" + ) + parameter_names.add(name) + helicity_count = execution.physical_helicity_count + color_flow_count = execution.physical_color_flow_count + if helicity_count is None or color_flow_count is None: # pragma: no cover + raise ArtifactError("on-the-fly execution has no selector census") + contracted_color = process.get("color_accuracy") in {"nlc", "full"} + return _PhysicsInspection( + physical_helicities=helicity_count, + computed_helicities=helicity_count, + physical_color_components=color_flow_count, + computed_color_components=color_flow_count, + helicity_coverage="complete", + color_coverage="contracted" if contracted_color else "complete", + selector_provenance="on-the-fly-compact-seed", + helicity_runtime_contract="complete-reusable", + color_flow_runtime_contract=( + None if contracted_color else "complete-reusable" + ), + lc_physical_sector_count=(None if contracted_color else color_flow_count), + ) + helicities = _sequence(physics.get("helicities"), f"{relative}.helicities") + colors = _sequence(physics.get("color_components"), f"{relative}.color_components") + coverage = _mapping(physics.get("coverage"), f"{relative}.coverage") + + computed_helicities = sum( + _computed(item, f"{relative}.helicities[{index}]") + for index, item in enumerate(helicities) + ) + computed_colors = sum( + _computed(item, f"{relative}.color_components[{index}]") + for index, item in enumerate(colors) + ) + selector_fields: dict[str, object] = {} + extensions_raw = physics.get("extensions") + extensions = ( + {} + if extensions_raw is None + else _mapping(extensions_raw, f"{relative}.extensions") + ) + selectors_raw = extensions.get("runtime_selectors") + if selectors_raw is not None: + selectors = _mapping(selectors_raw, f"{relative}.extensions.runtime_selectors") + if selectors.get("kind") != "pyamplicol-runtime-selectors": + raise ArtifactError( + f"{relative}.extensions.runtime_selectors.kind is unsupported" + ) + if selectors.get("contract_version") != 1: + raise ArtifactError( + f"{relative}.extensions.runtime_selectors.contract_version must be 1" + ) + selector_fields["selector_provenance"] = _string( + selectors.get("provenance"), + f"{relative}.extensions.runtime_selectors.provenance", + ) + axes = _mapping( + selectors.get("axes"), + f"{relative}.extensions.runtime_selectors.axes", + ) + helicity_axis = _mapping( + axes.get("helicity"), + f"{relative}.extensions.runtime_selectors.axes.helicity", + ) + color_axis = _mapping( + axes.get("color_flow"), + f"{relative}.extensions.runtime_selectors.axes.color_flow", + ) + helicity_generation_coverage = _string( + helicity_axis.get("generation_coverage"), + f"{relative}.extensions.runtime_selectors.axes.helicity." + "generation_coverage", + ) + color_generation_coverage = _string( + color_axis.get("generation_coverage"), + f"{relative}.extensions.runtime_selectors.axes.color_flow." + "generation_coverage", + ) + helicity_selection = _source_helicity_selection( + helicity_axis.get("generation_selection"), + f"{relative}.extensions.runtime_selectors.axes.helicity." + "generation_selection", + ) + color_selection = _color_sector_selection( + color_axis.get("generation_selection"), + f"{relative}.extensions.runtime_selectors.axes.color_flow." + "generation_selection", + ) + selector_fields["selected_source_helicities"] = helicity_selection + selector_fields["selected_color_sector_ids"] = color_selection + selector_fields["helicity_runtime_contract"] = _string( + helicity_axis.get("runtime_contract"), + f"{relative}.extensions.runtime_selectors.axes.helicity.runtime_contract", + ) + selector_fields["color_flow_runtime_contract"] = _string( + color_axis.get("runtime_contract"), + f"{relative}.extensions.runtime_selectors.axes.color_flow.runtime_contract", + ) + selector_fields["generation_specialized_axes"] = tuple( + _string( + value, + f"{relative}.extensions.runtime_selectors." + f"generation_specialized_axes[{index}]", + ) + for index, value in enumerate( + _sequence( + selectors.get("generation_specialized_axes"), + f"{relative}.extensions.runtime_selectors." + "generation_specialized_axes", + ) + ) + ) + helicity_contract = cast(str, selector_fields["helicity_runtime_contract"]) + color_contract = cast(str, selector_fields["color_flow_runtime_contract"]) + specialized_axes = cast( + tuple[str, ...], selector_fields["generation_specialized_axes"] + ) + expected_specialized_axes = ( + *(("helicity",) if helicity_contract == "generation-specialized" else ()), + *(("color_flow",) if color_contract == "generation-specialized" else ()), + ) + if helicity_contract not in { + "complete-reusable", + "generation-specialized", + }: + raise ArtifactError("unsupported helicity runtime-selector contract") + if color_contract not in { + "complete-reusable", + "generation-specialized", + "contracted-color", + }: + raise ArtifactError("unsupported color-flow runtime-selector contract") + if helicity_generation_coverage != str(coverage.get("helicities")): + raise ArtifactError( + "helicity selector generation coverage disagrees with physics coverage" + ) + if color_generation_coverage != str(coverage.get("color")): + raise ArtifactError( + "color-flow selector generation coverage disagrees with " + "physics coverage" + ) + if specialized_axes != expected_specialized_axes: + raise ArtifactError( + "runtime-selector specialized axes disagree with axis contracts" + ) + if (helicity_contract == "complete-reusable") != ( + helicity_generation_coverage == "complete" and not helicity_selection + ): + raise ArtifactError( + "helicity runtime contract disagrees with generation selection" + ) + if color_contract == "complete-reusable" and ( + color_generation_coverage != "complete" or color_selection + ): + raise ArtifactError( + "color-flow reusable contract carries a generation specialization" + ) + if color_contract == "generation-specialized" and ( + color_generation_coverage != "selected" or not color_selection + ): + raise ArtifactError( + "color-flow specialized contract lacks an exact sector selection" + ) + if color_contract == "contracted-color" and ( + color_generation_coverage != "contracted" or color_selection + ): + raise ArtifactError( + "contracted-color runtime contract carries LC selection metadata" + ) + legacy_helicity_selection = _source_helicity_selection( + extensions.get("selected_source_helicities", {}), + f"{relative}.extensions.selected_source_helicities", + ) + if legacy_helicity_selection != helicity_selection: + raise ArtifactError( + "runtime-selector helicity selection disagrees with physics provenance" + ) + recurrence_raw = selectors.get("helicity_recurrence") + if recurrence_raw is not None: + recurrence = _mapping( + recurrence_raw, + f"{relative}.extensions.runtime_selectors.helicity_recurrence", + ) + selector_fields["helicity_recurrence_status"] = _string( + recurrence.get("status"), + f"{relative}.extensions.runtime_selectors.helicity_recurrence.status", + ) + proof_counts = _mapping( + recurrence.get("proof_counts"), + f"{relative}.extensions.runtime_selectors." + "helicity_recurrence.proof_counts", + ) + count_fields = { + "helicity_optimized_class_count": ("optimized_recurrence_class_count"), + "helicity_optimized_current_count": "optimized_current_count", + "helicity_residual_current_count": "residual_current_count", + "helicity_optimized_amplitude_class_count": ( + "optimized_amplitude_class_count" + ), + "helicity_residual_amplitude_count": "residual_amplitude_count", + } + for field, key in count_fields.items(): + selector_fields[field] = _optional_nonnegative_integer( + proof_counts.get(key), + f"{relative}.extensions.runtime_selectors." + f"helicity_recurrence.proof_counts.{key}", + ) + selector_fields["helicity_materialized_current_count"] = ( + _optional_nonnegative_integer( + recurrence.get("materialized_current_count"), + f"{relative}.extensions.runtime_selectors." + "helicity_recurrence.materialized_current_count", + ) + ) + selector_fields["helicity_materialized_amplitude_count"] = ( + _optional_nonnegative_integer( + recurrence.get("materialized_amplitude_count"), + f"{relative}.extensions.runtime_selectors." + "helicity_recurrence.materialized_amplitude_count", + ) + ) + + replay_raw = extensions.get("lc_topology_replay") + if replay_raw is not None: + replay = _mapping(replay_raw, f"{relative}.extensions.lc_topology_replay") + selector_fields.update( + { + "lc_physical_sector_count": len( + _sequence( + replay.get("physical_sector_ids"), + f"{relative}.extensions.lc_topology_replay.physical_sector_ids", + ) + ), + "lc_materialized_sector_count": len( + _sequence( + replay.get("materialized_sector_ids"), + f"{relative}.extensions.lc_topology_replay." + "materialized_sector_ids", + ) + ), + "lc_replayed_sector_count": _integer( + replay.get("replayed_sector_count"), + f"{relative}.extensions.lc_topology_replay.replayed_sector_count", + minimum=0, + ), + "lc_residual_sector_count": len( + _sequence( + replay.get("residual_sector_ids"), + f"{relative}.extensions.lc_topology_replay.residual_sector_ids", + ) + ), + } + ) + + return _PhysicsInspection( + physical_helicities=len(helicities), + computed_helicities=computed_helicities, + physical_color_components=len(colors), + computed_color_components=computed_colors, + helicity_coverage=str(coverage.get("helicities", "unknown")), + color_coverage=str(coverage.get("color", "unknown")), + **selector_fields, + ) + + +def _process_inspection( + manifest: ArtifactManifest, + process: Mapping[str, object], + execution: _ExecutionInspection, +) -> ArtifactProcessInspection: + process_id = str(process["id"]) + lc_flow_layout: str | None = None + if str(process["color_accuracy"]) == "lc": + if execution.execution_mode == "on-the-fly": + lc_flow_layout = "compact/query-local" + else: + lc_flow_layout = "topology-replay" + generation = manifest.extensions.get("generation") + if isinstance(generation, Mapping): + concrete = generation.get("concrete_processes") + if isinstance(concrete, Sequence) and not isinstance( + concrete, (str, bytes) + ): + for record in concrete: + if ( + not isinstance(record, Mapping) + or record.get("id") != process_id + ): + continue + filters = record.get("filters") + if not isinstance(filters, Mapping): + continue + candidate = filters.get("lc_flow_layout") + if candidate is not None: + lc_flow_layout = str(candidate) + if lc_flow_layout not in {"topology-replay", "all-flow-union"}: + raise ArtifactError( + f"process {process_id}.filters.lc_flow_layout is " + "unsupported" + ) + aliases: list[ArtifactAliasInspection] = [] + for index, raw_alias in enumerate( + _sequence(process.get("aliases"), f"process {process_id}.aliases") + ): + alias = _mapping(raw_alias, f"process {process_id}.aliases[{index}]") + aliases.append( + ArtifactAliasInspection( + id=str(alias["id"]), + expression=str(alias["expression"]), + representative_id=process_id, + external_pdgs=tuple( + cast(int, value) + for value in _sequence( + alias.get("external_pdgs"), + f"process {process_id}.aliases[{index}].external_pdgs", + ) + ), + ) + ) + physics = _physics_inspection(manifest, process, execution) + union_sector_count = ( + physics.physical_color_components + if lc_flow_layout == "all-flow-union" + else None + ) + return ArtifactProcessInspection( + id=process_id, + expression=str(process["expression"]), + color_accuracy=str(process["color_accuracy"]), + external_pdgs=tuple( + cast(int, value) + for value in _sequence( + process.get("external_pdgs"), f"process {process_id}.external_pdgs" + ) + ), + default=process_id == manifest.default_process_id, + physical_helicities=physics.physical_helicities, + computed_helicities=physics.computed_helicities, + physical_color_components=physics.physical_color_components, + computed_color_components=physics.computed_color_components, + helicity_coverage=physics.helicity_coverage, + color_coverage=physics.color_coverage, + aliases=tuple(aliases), + selector_provenance=physics.selector_provenance, + helicity_runtime_contract=physics.helicity_runtime_contract, + color_flow_runtime_contract=physics.color_flow_runtime_contract, + generation_specialized_axes=physics.generation_specialized_axes, + selected_source_helicities=physics.selected_source_helicities, + selected_color_sector_ids=physics.selected_color_sector_ids, + helicity_recurrence_status=physics.helicity_recurrence_status, + helicity_optimized_class_count=physics.helicity_optimized_class_count, + helicity_optimized_current_count=physics.helicity_optimized_current_count, + helicity_residual_current_count=physics.helicity_residual_current_count, + helicity_optimized_amplitude_class_count=( + physics.helicity_optimized_amplitude_class_count + ), + helicity_residual_amplitude_count=(physics.helicity_residual_amplitude_count), + helicity_materialized_current_count=( + physics.helicity_materialized_current_count + ), + helicity_materialized_amplitude_count=( + physics.helicity_materialized_amplitude_count + ), + lc_physical_sector_count=physics.lc_physical_sector_count, + lc_materialized_sector_count=physics.lc_materialized_sector_count, + lc_replayed_sector_count=physics.lc_replayed_sector_count, + lc_residual_sector_count=physics.lc_residual_sector_count, + lc_flow_layout=lc_flow_layout, + lc_union_sector_count=union_sector_count, + execution_mode=execution.execution_mode, + prepared_backend=execution.prepared_backend, + prepared_kernel_count=execution.prepared_kernel_count, + referenced_kernel_count=execution.referenced_kernel_count, + invocation_count=execution.invocation_count, + attachment_count=execution.attachment_count, + evaluation_alias_count=execution.evaluation_alias_count, + maximum_fanout=execution.maximum_fanout, + finalization_count=execution.finalization_count, + closure_count=execution.closure_count, + selector_closure_available=execution.selector_closure_available, + selector_domain_count=execution.selector_domain_count, + selector_domain_membership_count=execution.selector_domain_membership_count, + requested_point_tile_size=execution.requested_point_tile_size, + effective_point_tile_size=execution.effective_point_tile_size, + requested_query_construction_threads=( + execution.requested_query_construction_threads + ), + workspace_limit_bytes=execution.workspace_limit_bytes, + workspace_bytes=execution.workspace_bytes, + arena_semantic_component_count=execution.arena_semantic_component_count, + arena_component_count=execution.arena_component_count, + arena_component_reuse_count=execution.arena_component_reuse_count, + momentum_form_count=execution.momentum_form_count, + direct_source_row_count=execution.direct_source_row_count, + direct_contribution_row_count=execution.direct_contribution_row_count, + direct_finalization_row_count=execution.direct_finalization_row_count, + direct_closure_row_count=execution.direct_closure_row_count, + direct_row_group_count=execution.direct_row_group_count, + recurrence_color_accuracy=execution.recurrence_color_accuracy, + recurrence_color_storage=execution.recurrence_color_storage, + recurrence_color_sector_count=execution.recurrence_color_sector_count, + recurrence_color_active_sector_count=( + execution.recurrence_color_active_sector_count + ), + recurrence_color_component_count=execution.recurrence_color_component_count, + recurrence_color_group_count=execution.recurrence_color_group_count, + recurrence_color_destination_count=( + execution.recurrence_color_destination_count + ), + recurrence_color_entry_count=execution.recurrence_color_entry_count, + recurrence_color_logical_entry_count=( + execution.recurrence_color_logical_entry_count + ), + recurrence_color_factorization_kind=( + execution.recurrence_color_factorization_kind + ), + recurrence_color_factorization_rank=( + execution.recurrence_color_factorization_rank + ), + recurrence_color_factorization_coset_count=( + execution.recurrence_color_factorization_coset_count + ), + recurrence_color_contraction_method=( + execution.recurrence_color_contraction_method + ), + recurrence_color_fft_degree=execution.recurrence_color_fft_degree, + recurrence_color_fft_channel_count=( + execution.recurrence_color_fft_channel_count + ), + recurrence_color_fft_covered_local_group_count=( + execution.recurrence_color_fft_covered_local_group_count + ), + recurrence_color_direct_residual_group_count=( + execution.recurrence_color_direct_residual_group_count + ), + recurrence_color_direct_residual_entry_count=( + execution.recurrence_color_direct_residual_entry_count + ), + recurrence_color_raw_kernel_bytes=(execution.recurrence_color_raw_kernel_bytes), + recurrence_color_transformed_kernel_bytes=( + execution.recurrence_color_transformed_kernel_bytes + ), + recurrence_color_fft_capability=(execution.recurrence_color_fft_capability), + packed_input_bytes=execution.packed_input_bytes, + packed_output_bytes=execution.packed_output_bytes, + scatter_bytes=execution.scatter_bytes, + native_profile_phases=execution.native_profile_phases, + ) + + +def _group_member_inspection( + member: GroupMember, +) -> ArtifactProcessGroupMemberInspection: + return ArtifactProcessGroupMemberInspection( + id=member.process_id, + process=member.expression, + pdgs=member.pdgs, + initial_count=member.initial_count, + factor=str(member.factor), + permutation=member.permutation, + conjugated=member.conjugated, + basis=str(member.basis), + generated=member.generated, + warning=member.warning, + ) + + +def _groups_inspection( + group_map: ProcessGroupMap, + *, + stored: bool, + process_ids: Sequence[str] = (), +) -> ArtifactProcessGroupsInspection: + """Return ``group_map`` with its groups in ``process_ids`` order.""" + + order = {process_id: index for index, process_id in enumerate(process_ids)} + groups = sorted( + group_map.groups.values(), + key=lambda group: ( + order.get(group.representative.process_id, len(order)), + group.representative.process_id, + ), + ) + return ArtifactProcessGroupsInspection( + stored=stored, + groups=tuple( + ArtifactProcessGroupInspection( + representative_id=group.representative.process_id, + representative=group.representative.expression, + members=tuple( + _group_member_inspection(member) for member in group.members + ), + ) + for group in groups + ), + analyses=tuple(dict(analysis) for analysis in group_map.analyses), + ) + + +def _process_groups_inspection( + manifest: ArtifactManifest, +) -> ArtifactProcessGroupsInspection: + """Read the stored process-group map; without one, each process is a group.""" + + group_map = read_process_groups(manifest.root) + stored = group_map is not None + if group_map is None: + group_map = initial_process_groups( + ( + str(process["id"]), + str(process["expression"]), + cast(Sequence[int], process["external_pdgs"]), + ) + for process in manifest.processes + ) + return _groups_inspection( + group_map, + stored=stored, + process_ids=[str(process["id"]) for process in manifest.processes], + ) + + +def stored_process_groups( + artifact: str | Path, +) -> ArtifactProcessGroupsInspection | None: + """Return the artifact's stored process-group map, or ``None`` without one. + + Unlike :func:`inspect_artifact`, this reads only ``artifact.json`` and + ``process-groups.json`` and verifies no payload. + """ + + group_map = read_process_groups(artifact) + return None if group_map is None else _groups_inspection(group_map, stored=True) + + +def process_group_selection( + artifact: str | Path, + process_id: str, + *, + permutation: Sequence[int] | None = None, +) -> ArtifactProcessGroupSelection | None: + """Return how ``process_id`` relates to its stored process group. + + Returns ``None`` when the artifact stores no map or the process is alone in + its group. ``permutation`` is the runtime's external permutation of the + requested leg order: from the representative's legs for a routed member, + whose runtime already composes the stored relation, and from the + process's own legs otherwise. The returned permutation relates the + representative to the requested leg order. + """ + + group_map = read_process_groups(artifact) + group = None if group_map is None else group_map.group_of(process_id) + if group is None or len(group.members) == 1: + return None + member = next(item for item in group.members if item.process_id == process_id) + representative = group.representative + relation_permutation = member.permutation + if permutation is not None: + requested = tuple(int(index) for index in permutation) + relation_permutation = ( + tuple(requested[index] for index in member.permutation) + if member.generated + else requested + ) + if member.process_id == representative.process_id: + role = "representative" + elif member.generated: + role = "generated member" + else: + role = "routed member" + return ArtifactProcessGroupSelection( + process_id=member.process_id, + role=role, + representative_id=representative.process_id, + representative=representative.expression, + factor=str(member.factor), + permutation=relation_permutation, + conjugated=member.conjugated, + basis=str(member.basis), + warning=member.warning, + group_size=len(group.members), + routed_count=sum(not item.generated for item in group.members), + conjugated_count=sum(item.conjugated for item in group.members), + ) + + +def _reject_retired_recurrence_metadata(artifact: str | Path) -> None: + """Reject pre-release recurrence artifacts before payload verification.""" + + root = Path(artifact).expanduser().resolve(strict=True) + manifest_path = root / MANIFEST_NAME + try: + raw = json.loads(manifest_path.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError): + # The strict manifest loader below owns ordinary format diagnostics. + return + if not isinstance(raw, Mapping): + return + capabilities: set[str] = set() + runtime = raw.get("runtime") + if isinstance(runtime, Mapping): + declared = runtime.get("required_runtime_capabilities") + if isinstance(declared, Sequence) and not isinstance(declared, (str, bytes)): + capabilities.update(str(value) for value in declared) + processes = raw.get("processes") + if isinstance(processes, Sequence) and not isinstance(processes, (str, bytes)): + for process in processes: + if not isinstance(process, Mapping): + continue + declared = process.get("required_runtime_capabilities") + if isinstance(declared, Sequence) and not isinstance( + declared, (str, bytes) + ): + capabilities.update(str(value) for value in declared) + if _RETIRED_RECURRENCE_RUNTIME_CAPABILITY in capabilities: + raise ArtifactError( + "this pre-release recurrence artifact uses the retired packet ABI; " + "regenerate it with the current pyAmpliCol direct-arena recurrence mode" + ) + + +def inspect_artifact(artifact: str | Path) -> ArtifactInspection: + """Validate and summarize one generated artifact without loading evaluators.""" + + _reject_retired_recurrence_metadata(artifact) + manifest = load_manifest(artifact) + target = _mapping(manifest.producer["target"], "producer.target") + model = manifest.model + runtime = manifest.runtime + execution_paths = _execution_paths(manifest) + processes = tuple( + _process_inspection( + manifest, + process, + _execution_inspection( + manifest, + execution_paths[str(process["id"])], + _sequence( + process.get("required_runtime_capabilities"), + f"process {process['id']} required runtime capabilities", + ), + process=process, + ), + ) + for process in manifest.processes + ) + dependencies = tuple( + ArtifactDependencyInspection( + name=str(dependency["name"]), + version=str(dependency["version"]), + license=str(dependency["license"]), + source=str(dependency["source"]), + ) + for dependency in manifest.dependencies + ) + restriction = model.get("restriction") + container_path: str | None = None + container_abi: str | None = None + container_member_count: int | None = None + container_unpacked_size: int | None = None + container_raw = manifest.extensions.get("evaluator_payload_container") + if container_raw is not None: + container = _mapping( + container_raw, + "extensions.evaluator_payload_container", + ) + if ( + container.get("kind") != "pyamplicol-evaluator-payload-container" + or container.get("schema_version") != 1 + or container.get("storage_abi") != "pacbin-v1" + ): + raise ArtifactError("unsupported evaluator payload container contract") + container_path = _string( + container.get("path"), + "extensions.evaluator_payload_container.path", + ) + container_abi = _string( + container.get("storage_abi"), + "extensions.evaluator_payload_container.storage_abi", + ) + container_member_count = _integer( + container.get("member_count"), + "extensions.evaluator_payload_container.member_count", + minimum=0, + ) + container_unpacked_size = _integer( + container.get("unpacked_size_bytes"), + "extensions.evaluator_payload_container.unpacked_size_bytes", + minimum=0, + ) + matching = tuple( + payload for payload in manifest.payloads if payload.path == container_path + ) + if len(matching) != 1 or matching[0].role != "evaluator-state": + raise ArtifactError( + "evaluator payload container is not one declared " + "evaluator-state payload" + ) + return ArtifactInspection( + kind="pyamplicol-artifact-inspection", + path=manifest.root, + artifact_kind=manifest.kind, + artifact_id=manifest.artifact_id, + created_utc=manifest.created_utc, + producer_version=str(manifest.producer["version"]), + target=str(target["triple"]), + cpu_features=tuple( + str(value) + for value in _sequence( + target.get("cpu_features"), "producer.target.cpu_features" + ) + ), + model_name=str(model["name"]), + model_source=str(model["source_kind"]), + model_restriction=None if restriction is None else str(restriction), + default_process_id=manifest.default_process_id, + runtime_engine=str(runtime["engine"]), + runtime_version=str(runtime["engine_version"]), + runtime_capabilities=tuple( + str(value) + for value in _sequence( + runtime.get("required_runtime_capabilities"), + "runtime.required_runtime_capabilities", + ) + ), + payload_count=len(manifest.payloads), + payload_size_bytes=sum(payload.size_bytes for payload in manifest.payloads), + integrity="verified", + processes=processes, + dependencies=dependencies, + physical_file_count=len(manifest.payloads) + 1, + evaluator_container_path=container_path, + evaluator_container_abi=container_abi, + evaluator_container_member_count=container_member_count, + evaluator_container_unpacked_size_bytes=container_unpacked_size, + process_groups=_process_groups_inspection(manifest), + ) + + +__all__ = [ + "ArtifactAliasInspection", + "ArtifactDependencyInspection", + "ArtifactInspection", + "ArtifactProcessGroupInspection", + "ArtifactProcessGroupMemberInspection", + "ArtifactProcessGroupSelection", + "ArtifactProcessGroupsInspection", + "ArtifactProcessInspection", + "inspect_artifact", + "process_group_selection", + "stored_process_groups", +] diff --git a/pyamplicol/src/pyamplicol/artifacts/manifest.py b/pyamplicol/src/pyamplicol/artifacts/manifest.py new file mode 100644 index 0000000000..6b430c636a --- /dev/null +++ b/pyamplicol/src/pyamplicol/artifacts/manifest.py @@ -0,0 +1,1011 @@ +# SPDX-License-Identifier: 0BSD +"""Typed schema-v3 manifest loading and payload validation.""" + +from __future__ import annotations + +import importlib +import json +import re +import struct +import sys +from collections.abc import Mapping, Sequence, Set +from dataclasses import dataclass +from datetime import datetime +from pathlib import Path +from types import MappingProxyType +from typing import Any, Literal, cast + +from pyamplicol._internal.versions import ( + KNOWN_EVALUATOR_RUNTIME_CAPABILITIES, + SYMBOLICA_ASM_RUNTIME_CAPABILITY, + SYMBOLICA_CPP_RUNTIME_CAPABILITY, + SYMBOLICA_LEGACY_JIT_RUNTIME_CAPABILITY, + verify_native_module, +) +from pyamplicol.api.errors import ArtifactError, CompatibilityError + +from .security import ( + confined_path, + executable_bit, + normalize_relative_path, + sha256_file, +) + +MANIFEST_NAME = "artifact.json" +SCHEMA_VERSION = 3 +ARTIFACT_IDENTITY_EXTENSION = "artifact_identity" +ARTIFACT_IDENTITY_KIND = "pyamplicol-runtime-payload-identity" +ARTIFACT_IDENTITY_SCHEMA_VERSION = 1 +PORTABLE_64LE_TARGET = "portable-64le" +_SHA256 = re.compile(r"^[a-f0-9]{64}$") +_GIT_REVISION = re.compile(r"^[a-f0-9]{40}$") +_PUBLIC_ID = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:+,~-]{0,254}$") +_CPU_FEATURE_ID = re.compile(r"^[a-z0-9][a-z0-9.-]*$") +_SUPPORTED_ARTIFACT_TARGETS = frozenset( + { + "aarch64-apple-darwin", + "x86_64-apple-darwin", + "x86_64-unknown-linux-gnu", + } +) +_TARGET_SPECIFIC_EVALUATOR_RUNTIME_CAPABILITIES = frozenset( + { + SYMBOLICA_ASM_RUNTIME_CAPABILITY, + SYMBOLICA_CPP_RUNTIME_CAPABILITY, + SYMBOLICA_LEGACY_JIT_RUNTIME_CAPABILITY, + } +) +_EXECUTABLE_ROLES = {"api-source", "api-build-file", "evaluator-state"} +_PAYLOAD_ROLES = { + "configuration-requested", + "configuration-effective", + "compiled-model", + "runtime-physics", + "evaluator-manifest", + "evaluator-state", + "model-parameters", + "validation-momenta", + "structural-source-proof", + "api-source", + "api-build-file", + "sdk-metadata", + "process-groups", +} +_RUNTIME_IDENTITY_PAYLOAD_ROLES = frozenset( + { + "compiled-model", + "evaluator-manifest", + "evaluator-state", + "model-parameters", + "runtime-physics", + } +) + + +def artifact_identity_extension() -> dict[str, object]: + """Return the sole supported artifact-ID interpretation contract.""" + + return { + "kind": ARTIFACT_IDENTITY_KIND, + "schema_version": ARTIFACT_IDENTITY_SCHEMA_VERSION, + } + + +def _validate_artifact_identity_extension( + extensions: Mapping[str, object], +) -> None: + raw = extensions.get(ARTIFACT_IDENTITY_EXTENSION) + if raw is None: + raise CompatibilityError( + "artifact predates the required runtime-payload identity contract; " + "regenerate it with the current pyAmpliCol" + ) + policy = _mapping(raw, f"extensions.{ARTIFACT_IDENTITY_EXTENSION}") + _keys( + policy, + f"extensions.{ARTIFACT_IDENTITY_EXTENSION}", + required={"kind", "schema_version"}, + ) + if ( + policy.get("kind") != ARTIFACT_IDENTITY_KIND + or policy.get("schema_version") != ARTIFACT_IDENTITY_SCHEMA_VERSION + ): + raise CompatibilityError( + "artifact uses an unsupported artifact identity contract; " + "regenerate it with the current pyAmpliCol" + ) + + +def _keys( + value: Mapping[str, object], + context: str, + *, + required: Set[str], + optional: Set[str] = frozenset(), +) -> None: + missing = required - value.keys() + unknown = value.keys() - required - optional + if missing: + raise ArtifactError( + f"{context} is missing fields: {', '.join(sorted(missing))}" + ) + if unknown: + raise ArtifactError( + f"{context} has unknown fields: {', '.join(sorted(unknown))}" + ) + + +def _mapping(value: object, context: str) -> Mapping[str, object]: + if not isinstance(value, Mapping): + raise ArtifactError(f"{context} must be an object") + if not all(isinstance(key, str) for key in value): + raise ArtifactError(f"{context} keys must be strings") + return value + + +def _sequence(value: object, context: str) -> Sequence[object]: + if isinstance(value, (str, bytes)) or not isinstance(value, Sequence): + raise ArtifactError(f"{context} must be an array") + return value + + +def _string(value: object, context: str) -> str: + if not isinstance(value, str) or not value: + raise ArtifactError(f"{context} must be a non-empty string") + return value + + +def _public_id(value: object, context: str) -> str: + identifier = _string(value, context) + if _PUBLIC_ID.fullmatch(identifier) is None: + raise ArtifactError(f"{context} is not a valid public identifier") + return identifier + + +def _string_array(value: object, context: str) -> tuple[str, ...]: + items = tuple(_sequence(value, context)) + result = tuple( + _string(item, f"{context}[{index}]") for index, item in enumerate(items) + ) + if len(set(result)) != len(result): + raise ArtifactError(f"{context} must not contain duplicates") + return result + + +def _runtime_capabilities(value: object, context: str) -> tuple[str, ...]: + result = _string_array(value, context) + if not result: + raise ArtifactError(f"{context} must contain at least one capability") + if result != tuple(sorted(result)): + raise ArtifactError(f"{context} must be sorted") + unknown = set(result) - KNOWN_EVALUATOR_RUNTIME_CAPABILITIES + if unknown: + raise ArtifactError( + f"{context} contains unsupported capabilities: " + + ", ".join(sorted(unknown)) + ) + return result + + +def _sha256(value: object, context: str) -> str: + digest = _string(value, context) + if _SHA256.fullmatch(digest) is None: + raise ArtifactError(f"{context} must be a lowercase SHA-256 digest") + return digest + + +def _integer(value: object, context: str, *, minimum: int = 0) -> int: + if isinstance(value, bool) or not isinstance(value, int) or value < minimum: + raise ArtifactError(f"{context} must be an integer >= {minimum}") + return value + + +def _target(value: object, context: str) -> Mapping[str, object]: + raw = _mapping(value, context) + _keys(raw, context, required={"triple", "cpu_features"}) + cpu_features = _string_array(raw.get("cpu_features"), f"{context}.cpu_features") + if cpu_features != tuple(sorted(cpu_features)): + raise ArtifactError(f"{context}.cpu_features must be sorted") + invalid = tuple( + feature + for feature in cpu_features + if _CPU_FEATURE_ID.fullmatch(feature) is None + ) + if invalid: + raise ArtifactError( + f"{context}.cpu_features contains non-canonical IDs: " + + ", ".join(repr(feature) for feature in invalid) + ) + return MappingProxyType( + { + "triple": _string(raw.get("triple"), f"{context}.triple"), + "cpu_features": cpu_features, + } + ) + + +def _producer(value: object) -> Mapping[str, object]: + raw = _mapping(value, "producer") + _keys( + raw, + "producer", + required={"distribution", "version", "versions", "target"}, + optional={"git_revision", "native_build_inputs_sha256"}, + ) + if raw.get("distribution") != "pyamplicol": + raise ArtifactError("producer.distribution must be 'pyamplicol'") + versions = _mapping(raw.get("versions"), "producer.versions") + version_fields = { + "python_api", + "toml", + "compiled_model", + "process_artifact", + "runtime_physics", + "symbolica_serialization", + "c_abi", + } + _keys(versions, "producer.versions", required=version_fields) + normalized_versions: dict[str, object] = { + name: _integer(versions.get(name), f"producer.versions.{name}", minimum=1) + for name in version_fields - {"symbolica_serialization"} + } + if normalized_versions["process_artifact"] != 3: + raise ArtifactError("producer.versions.process_artifact must be 3") + if normalized_versions["runtime_physics"] != 1: + raise ArtifactError("producer.versions.runtime_physics must be 1") + normalized_versions["symbolica_serialization"] = _string( + versions.get("symbolica_serialization"), + "producer.versions.symbolica_serialization", + ) + result: dict[str, object] = { + "distribution": "pyamplicol", + "version": _string(raw.get("version"), "producer.version"), + "versions": MappingProxyType(normalized_versions), + "target": _target(raw.get("target"), "producer.target"), + } + revision = raw.get("git_revision") + if revision is not None: + revision_value = _string(revision, "producer.git_revision") + if _GIT_REVISION.fullmatch(revision_value) is None: + raise ArtifactError("producer.git_revision must be a Git SHA-1") + result["git_revision"] = revision_value + native_inputs = raw.get("native_build_inputs_sha256") + if native_inputs is not None: + native_inputs_value = _string( + native_inputs, + "producer.native_build_inputs_sha256", + ) + if _SHA256.fullmatch(native_inputs_value) is None: + raise ArtifactError("producer.native_build_inputs_sha256 must be a SHA-256") + result["native_build_inputs_sha256"] = native_inputs_value + return MappingProxyType(result) + + +def _model(value: object) -> Mapping[str, object]: + raw = _mapping(value, "model") + _keys( + raw, + "model", + required={"name", "source_kind", "content_sha256", "compiled_schema_version"}, + optional={"restriction"}, + ) + source_kind = raw.get("source_kind") + if source_kind not in { + "built-in-sm", + "built-in-sm-heft", + "ufo", + "ufo-json", + "compiled-model", + }: + raise ArtifactError(f"unsupported model.source_kind {source_kind!r}") + result: dict[str, object] = { + "name": _string(raw.get("name"), "model.name"), + "source_kind": source_kind, + "content_sha256": _sha256(raw.get("content_sha256"), "model.content_sha256"), + "compiled_schema_version": _integer( + raw.get("compiled_schema_version"), + "model.compiled_schema_version", + minimum=1, + ), + } + if "restriction" in raw: + restriction = raw.get("restriction") + result["restriction"] = ( + None if restriction is None else _string(restriction, "model.restriction") + ) + return MappingProxyType(result) + + +def _configuration(value: object) -> Mapping[str, object]: + raw = _mapping(value, "configuration") + _keys( + raw, + "configuration", + required={ + "toml_schema_version", + "requested_path", + "effective_path", + "adjustments", + }, + ) + if raw.get("toml_schema_version") != 1: + raise ArtifactError("configuration.toml_schema_version must be 1") + adjustments: list[Mapping[str, object]] = [] + for index, item in enumerate( + _sequence(raw.get("adjustments"), "configuration.adjustments") + ): + adjustment = _mapping(item, f"configuration.adjustments[{index}]") + _keys( + adjustment, + f"configuration.adjustments[{index}]", + required={"path", "reason"}, + ) + adjustments.append( + MappingProxyType( + { + "path": _string( + adjustment.get("path"), + f"configuration.adjustments[{index}].path", + ), + "reason": _string( + adjustment.get("reason"), + f"configuration.adjustments[{index}].reason", + ), + } + ) + ) + return MappingProxyType( + { + "toml_schema_version": 1, + "requested_path": normalize_relative_path( + _string(raw.get("requested_path"), "configuration.requested_path") + ), + "effective_path": normalize_relative_path( + _string(raw.get("effective_path"), "configuration.effective_path") + ), + "adjustments": tuple(adjustments), + } + ) + + +def _process(value: object, index: int) -> Mapping[str, object]: + context = f"processes[{index}]" + raw = _mapping(value, context) + _keys( + raw, + context, + required={ + "id", + "expression", + "color_accuracy", + "external_pdgs", + "physics_path", + "required_runtime_capabilities", + "aliases", + }, + ) + accuracy = raw.get("color_accuracy") + if accuracy not in {"lc", "nlc", "full"}: + raise ArtifactError(f"{context}.color_accuracy is invalid") + pdgs = tuple(_sequence(raw.get("external_pdgs"), f"{context}.external_pdgs")) + if len(pdgs) < 3 or not all( + isinstance(value, int) and not isinstance(value, bool) for value in pdgs + ): + raise ArtifactError( + f"{context}.external_pdgs must contain at least three integers" + ) + process_initial_count = _process_initial_count( + _string(raw.get("expression"), f"{context}.expression"), + context, + len(pdgs), + ) + aliases: list[Mapping[str, object]] = [] + for alias_index, item in enumerate( + _sequence(raw.get("aliases"), f"{context}.aliases") + ): + alias_context = f"{context}.aliases[{alias_index}]" + alias = _mapping(item, alias_context) + _keys( + alias, + alias_context, + required={"id", "expression", "external_pdgs", "external_permutation"}, + ) + permutation = tuple( + _sequence( + alias.get("external_permutation"), + f"{alias_context}.external_permutation", + ) + ) + if not all( + isinstance(value, int) and not isinstance(value, bool) and value >= 0 + for value in permutation + ): + raise ArtifactError(f"{alias_context}.external_permutation is invalid") + if len(permutation) != len(pdgs) or sorted(permutation) != list( + range(len(pdgs)) + ): + raise ArtifactError( + f"{alias_context}.external_permutation must be a complete " + f"permutation of {len(pdgs)} external particles" + ) + alias_initial_count = _process_initial_count( + _string(alias.get("expression"), f"{alias_context}.expression"), + alias_context, + len(pdgs), + ) + if alias_initial_count != process_initial_count or any( + (representative_index < process_initial_count) + != (public_index < process_initial_count) + for representative_index, public_index in enumerate(permutation) + ): + raise ArtifactError( + f"{alias_context}.external_permutation may only permute " + "particles within the incoming and outgoing sides" + ) + alias_pdgs = tuple( + _sequence(alias.get("external_pdgs"), f"{alias_context}.external_pdgs") + ) + if len(alias_pdgs) != len(pdgs) or not all( + isinstance(value, int) and not isinstance(value, bool) + for value in alias_pdgs + ): + raise ArtifactError( + f"{alias_context}.external_pdgs must contain {len(pdgs)} integers" + ) + expected_pdgs: list[int | None] = [None] * len(pdgs) + for representative_index, alias_index in enumerate(permutation): + expected_pdgs[alias_index] = int(pdgs[representative_index]) + if tuple(expected_pdgs) != alias_pdgs: + raise ArtifactError( + f"{alias_context}.external_pdgs does not match external_permutation" + ) + aliases.append( + MappingProxyType( + { + "id": _public_id(alias.get("id"), f"{alias_context}.id"), + "expression": _string( + alias.get("expression"), f"{alias_context}.expression" + ), + "external_pdgs": alias_pdgs, + "external_permutation": permutation, + } + ) + ) + return MappingProxyType( + { + "id": _public_id(raw.get("id"), f"{context}.id"), + "expression": _string(raw.get("expression"), f"{context}.expression"), + "color_accuracy": accuracy, + "external_pdgs": pdgs, + "physics_path": normalize_relative_path( + _string(raw.get("physics_path"), f"{context}.physics_path") + ), + "required_runtime_capabilities": _runtime_capabilities( + raw.get("required_runtime_capabilities"), + f"{context}.required_runtime_capabilities", + ), + "aliases": tuple(aliases), + } + ) + + +def _process_initial_count(expression: str, context: str, particle_count: int) -> int: + sides = expression.split(">") + if len(sides) != 2: + raise ArtifactError( + f"{context}.expression must contain exactly one '>' separator" + ) + initial_count = len(sides[0].split()) + final_count = len(sides[1].split()) + if ( + initial_count < 1 + or final_count < 1 + or initial_count + final_count != particle_count + ): + raise ArtifactError( + f"{context}.expression particle count does not match external_pdgs" + ) + return initial_count + + +def _runtime(value: object) -> Mapping[str, object]: + raw = _mapping(value, "runtime") + _keys( + raw, + "runtime", + required={ + "engine", + "engine_version", + "evaluator_manifest_path", + "api_bundle_path", + "required_runtime_capabilities", + }, + ) + if raw.get("engine") != "rusticol": + raise ArtifactError("runtime.engine must be 'rusticol'") + api_path = raw.get("api_bundle_path") + return MappingProxyType( + { + "engine": "rusticol", + "engine_version": _string( + raw.get("engine_version"), "runtime.engine_version" + ), + "evaluator_manifest_path": normalize_relative_path( + _string( + raw.get("evaluator_manifest_path"), + "runtime.evaluator_manifest_path", + ) + ), + "api_bundle_path": None + if api_path is None + else normalize_relative_path(_string(api_path, "runtime.api_bundle_path")), + "required_runtime_capabilities": _runtime_capabilities( + raw.get("required_runtime_capabilities"), + "runtime.required_runtime_capabilities", + ), + } + ) + + +def _dependency(value: object, index: int) -> Mapping[str, object]: + context = f"dependencies[{index}]" + raw = _mapping(value, context) + _keys( + raw, + context, + required={"name", "version", "source", "license"}, + optional={"content_sha256", "revision", "patch_sha256"}, + ) + result: dict[str, object] = { + name: _string(raw.get(name), f"{context}.{name}") + for name in ("name", "version", "source", "license") + } + for name in ("content_sha256", "patch_sha256"): + if name in raw: + result[name] = _sha256(raw.get(name), f"{context}.{name}") + if "revision" in raw: + result["revision"] = _string(raw.get("revision"), f"{context}.revision") + return MappingProxyType(result) + + +@dataclass(frozen=True, slots=True) +class PayloadRecord: + path: str + role: str + media_type: str + size_bytes: int + sha256: str + executable: bool + target: Mapping[str, object] | None = None + process_id: str | None = None + + @classmethod + def from_mapping(cls, value: object, index: int) -> PayloadRecord: + raw = _mapping(value, f"payloads[{index}]") + _keys( + raw, + f"payloads[{index}]", + required={ + "path", + "role", + "media_type", + "size_bytes", + "sha256", + "executable", + }, + optional={"target", "process_id"}, + ) + executable = raw.get("executable") + if not isinstance(executable, bool): + raise ArtifactError(f"payloads[{index}].executable must be a boolean") + target = raw.get("target") + process_id = raw.get("process_id") + role = _string(raw.get("role"), f"payloads[{index}].role") + if role not in _PAYLOAD_ROLES: + raise ArtifactError(f"payloads[{index}].role is unsupported: {role!r}") + return cls( + path=normalize_relative_path( + _string(raw.get("path"), f"payloads[{index}].path") + ), + role=role, + media_type=_string(raw.get("media_type"), f"payloads[{index}].media_type"), + size_bytes=_integer(raw.get("size_bytes"), f"payloads[{index}].size_bytes"), + sha256=_sha256(raw.get("sha256"), f"payloads[{index}].sha256"), + executable=executable, + target=( + _target(target, f"payloads[{index}].target") + if target is not None + else None + ), + process_id=( + _public_id(process_id, f"payloads[{index}].process_id") + if process_id is not None + else None + ), + ) + + def as_dict(self) -> dict[str, object]: + result: dict[str, object] = { + "path": self.path, + "role": self.role, + "media_type": self.media_type, + "size_bytes": self.size_bytes, + "sha256": self.sha256, + "executable": self.executable, + } + if self.target is not None: + result["target"] = dict(self.target) + if self.process_id is not None: + result["process_id"] = self.process_id + return result + + +@dataclass(frozen=True, slots=True) +class ArtifactManifest: + root: Path + kind: Literal["pyamplicol-process", "pyamplicol-process-set"] + artifact_id: str + created_utc: str + producer: Mapping[str, object] + model: Mapping[str, object] + configuration: Mapping[str, object] + processes: tuple[Mapping[str, object], ...] + default_process_id: str | None + runtime: Mapping[str, object] + payloads: tuple[PayloadRecord, ...] + dependencies: tuple[Mapping[str, object], ...] + extensions: Mapping[str, object] + + @classmethod + def from_mapping(cls, root: Path, value: object) -> ArtifactManifest: + raw = _mapping(value, "artifact manifest") + schema = raw.get("schema_version") + if schema in (1, 2): + raise CompatibilityError( + f"process-artifact schema v{schema} is unsupported; regenerate " + "the process with pyAmpliCol 0.1.0 or newer" + ) + if schema != SCHEMA_VERSION: + raise CompatibilityError( + f"unsupported process-artifact schema {schema!r}; expected v3" + ) + if "extensions" not in raw: + raise CompatibilityError( + "artifact predates the required runtime-payload identity contract; " + "regenerate it with the current pyAmpliCol" + ) + _keys( + raw, + "artifact manifest", + required={ + "schema_version", + "kind", + "artifact_id", + "created_utc", + "producer", + "model", + "configuration", + "processes", + "runtime", + "payloads", + "dependencies", + "extensions", + }, + optional={"default_process_id"}, + ) + kind = raw.get("kind") + if kind not in ("pyamplicol-process", "pyamplicol-process-set"): + raise ArtifactError(f"unsupported artifact kind {kind!r}") + process_values = _sequence(raw.get("processes"), "processes") + if not process_values: + raise ArtifactError("processes must contain at least one process") + payload_values = _sequence(raw.get("payloads"), "payloads") + if not payload_values: + raise ArtifactError("payloads must contain at least one payload") + dependency_values = _sequence(raw.get("dependencies"), "dependencies") + default_process = raw.get("default_process_id") + extensions = _mapping(raw.get("extensions"), "extensions") + _validate_artifact_identity_extension(extensions) + created_utc = _string(raw.get("created_utc"), "created_utc") + try: + datetime.fromisoformat(created_utc.replace("Z", "+00:00")) + except ValueError as exc: + raise ArtifactError("created_utc must be an ISO-8601 date-time") from exc + processes = tuple( + _process(item, index) for index, item in enumerate(process_values) + ) + process_ids = tuple(str(item["id"]) for item in processes) + if len(set(process_ids)) != len(process_ids): + raise ArtifactError("process IDs must be unique") + default_process_id = ( + _public_id(default_process, "default_process_id") + if default_process is not None + else None + ) + if default_process_id is not None and default_process_id not in process_ids: + raise ArtifactError("default_process_id does not identify a process") + runtime = _runtime(raw.get("runtime")) + process_capabilities = { + str(capability) + for process in processes + for capability in _sequence( + process["required_runtime_capabilities"], + "process.required_runtime_capabilities", + ) + } + runtime_capabilities = set( + _sequence( + runtime["required_runtime_capabilities"], + "runtime.required_runtime_capabilities", + ) + ) + if runtime_capabilities != process_capabilities: + raise ArtifactError( + "runtime.required_runtime_capabilities must equal the union of " + "process capability declarations" + ) + return cls( + root=root.resolve(strict=True), + kind=kind, + artifact_id=_sha256(raw.get("artifact_id"), "artifact_id"), + created_utc=created_utc, + producer=_producer(raw.get("producer")), + model=_model(raw.get("model")), + configuration=_configuration(raw.get("configuration")), + processes=processes, + default_process_id=default_process_id, + runtime=runtime, + payloads=tuple( + PayloadRecord.from_mapping(item, index) + for index, item in enumerate(payload_values) + ), + dependencies=tuple( + _dependency(item, index) for index, item in enumerate(dependency_values) + ), + extensions=MappingProxyType(dict(extensions)), + ) + + def as_dict(self) -> dict[str, object]: + result: dict[str, object] = { + "schema_version": SCHEMA_VERSION, + "kind": self.kind, + "artifact_id": self.artifact_id, + "created_utc": self.created_utc, + "producer": _plain(self.producer), + "model": _plain(self.model), + "configuration": _plain(self.configuration), + "processes": _plain(self.processes), + "runtime": _plain(self.runtime), + "payloads": [payload.as_dict() for payload in self.payloads], + "dependencies": _plain(self.dependencies), + "extensions": _plain(self.extensions), + } + if self.default_process_id is not None: + result["default_process_id"] = self.default_process_id + return result + + +def _plain(value: object) -> Any: + if isinstance(value, Mapping): + return {str(key): _plain(item) for key, item in value.items()} + if isinstance(value, Sequence) and not isinstance(value, (str, bytes)): + return [_plain(item) for item in value] + return value + + +def canonical_manifest_bytes(value: Mapping[str, object]) -> bytes: + return ( + json.dumps( + _plain(value), + sort_keys=True, + separators=(",", ":"), + allow_nan=False, + ) + + "\n" + ).encode("utf-8") + + +def _artifact_identity_content(value: Mapping[str, object]) -> dict[str, object]: + payloads = _sequence(value.get("payloads"), "payloads") + runtime_records: list[dict[str, object]] = [] + for index, item in enumerate(payloads): + record = _mapping(item, f"payloads[{index}]") + role = _string(record.get("role"), f"payloads[{index}].role") + if role in _RUNTIME_IDENTITY_PAYLOAD_ROLES: + runtime_records.append(cast(dict[str, object], _plain(record))) + runtime_records.sort(key=lambda record: str(record.get("path", ""))) + return { + "kind": "pyamplicol-runtime-payload-identity", + "schema_version": 1, + "payloads": runtime_records, + } + + +def compute_artifact_id(value: Mapping[str, object]) -> str: + import hashlib + + content = _artifact_identity_content(value) + return hashlib.sha256(canonical_manifest_bytes(content)).hexdigest() + + +def _validate_target_compatibility( + manifest: ArtifactManifest, + *, + expected_target: str | None = None, +) -> None: + producer_target = _target(manifest.producer.get("target"), "producer.target") + target_triple = _string(producer_target.get("triple"), "producer.target.triple") + is_portable_64le = target_triple == PORTABLE_64LE_TARGET + if ( + expected_target is not None + and not is_portable_64le + and target_triple != expected_target + ): + raise CompatibilityError( + f"artifact target {target_triple!r} is incompatible with " + f"runtime target {expected_target!r}" + ) + seen: set[str] = set() + for record in manifest.payloads: + if record.path in seen: + raise ArtifactError(f"duplicate artifact payload path: {record.path}") + seen.add(record.path) + if record.role == "evaluator-state" and record.target is None: + raise ArtifactError( + f"evaluator-state payload has no target metadata: {record.path}" + ) + if record.target is not None and dict(record.target) != dict(producer_target): + raise CompatibilityError( + f"payload {record.path} target metadata differs from producer target" + ) + + required_features = cast(tuple[str, ...], producer_target["cpu_features"]) + if is_portable_64le: + if required_features: + raise CompatibilityError( + f"{PORTABLE_64LE_TARGET} artifacts may not require CPU features" + ) + runtime_capabilities = set( + cast(tuple[str, ...], manifest.runtime["required_runtime_capabilities"]) + ) + target_specific_capabilities = sorted( + runtime_capabilities & _TARGET_SPECIFIC_EVALUATOR_RUNTIME_CAPABILITIES + ) + if target_specific_capabilities: + raise CompatibilityError( + f"{PORTABLE_64LE_TARGET} artifact declares target-specific " + "evaluator capabilities: " + + ", ".join(target_specific_capabilities) + ) + if ( + expected_target is not None + and expected_target not in _SUPPORTED_ARTIFACT_TARGETS + ): + raise CompatibilityError( + f"artifact target {PORTABLE_64LE_TARGET!r} is incompatible with " + f"runtime target {expected_target!r}" + ) + if sys.byteorder != "little" or struct.calcsize("P") * 8 != 64: + raise CompatibilityError( + f"{PORTABLE_64LE_TARGET} artifacts require a 64-bit " + "little-endian runtime" + ) + return + if required_features: + runtime_triple, runtime_features = _runtime_target_metadata() + if target_triple != runtime_triple: + raise CompatibilityError( + f"artifact target {target_triple!r} is incompatible with " + f"runtime target {runtime_triple!r}" + ) + unavailable = sorted(set(required_features) - set(runtime_features)) + if unavailable: + raise CompatibilityError( + "artifact requires unavailable CPU features: " + ", ".join(unavailable) + ) + + +def validate_payloads( + manifest: ArtifactManifest, + *, + expected_target: str | None = None, +) -> None: + if compute_artifact_id(manifest.as_dict()) != manifest.artifact_id: + raise ArtifactError("artifact manifest identity digest mismatch") + _validate_target_compatibility(manifest, expected_target=expected_target) + seen = {record.path for record in manifest.payloads} + for record in manifest.payloads: + path = confined_path(manifest.root, record.path) + if path.stat().st_size != record.size_bytes: + raise ArtifactError(f"artifact payload size mismatch: {record.path}") + if sha256_file(path) != record.sha256: + raise ArtifactError(f"artifact payload digest mismatch: {record.path}") + actual_executable = executable_bit(path) + if actual_executable != record.executable: + raise ArtifactError( + f"artifact payload executable-bit mismatch: {record.path}" + ) + if record.executable and record.role not in _EXECUTABLE_ROLES: + raise ArtifactError( + f"artifact payload role {record.role!r} may not be executable" + ) + + expected_files = seen | {MANIFEST_NAME} + for path in manifest.root.rglob("*"): + relative = path.relative_to(manifest.root).as_posix() + if path.is_symlink(): + raise ArtifactError( + f"artifact tree contains an undeclared symlink: {relative}" + ) + if not path.is_file() or relative in expected_files: + continue + if executable_bit(path): + raise ArtifactError( + f"artifact tree contains an undeclared executable: {relative}" + ) + + +def _runtime_target_metadata() -> tuple[str, tuple[str, ...]]: + try: + rusticol = importlib.import_module("pyamplicol._rusticol") + verify_native_module(rusticol) + info = rusticol.target_info() + triple = str(info.triple) + features = tuple(str(feature) for feature in info.cpu_features) + except (AttributeError, ImportError, OSError, RuntimeError) as error: + raise CompatibilityError( + "artifact CPU-feature requirements cannot be checked because the " + "Rusticol native runtime is unavailable" + ) from error + if not triple: + raise CompatibilityError("Rusticol returned an empty runtime target triple") + if triple not in _SUPPORTED_ARTIFACT_TARGETS: + raise CompatibilityError( + f"Rusticol process artifacts are not supported on target {triple!r}" + ) + if features != tuple(sorted(set(features))) or any( + _CPU_FEATURE_ID.fullmatch(feature) is None for feature in features + ): + raise CompatibilityError( + "Rusticol returned invalid runtime CPU-feature metadata" + ) + return triple, features + + +def load_manifest( + artifact: str | Path, + *, + expected_target: str | None = None, + verify_payloads: bool = False, +) -> ArtifactManifest: + root = Path(artifact).expanduser().resolve(strict=True) + if not root.is_dir(): + raise ArtifactError(f"artifact root is not a directory: {root}") + manifest_path = confined_path(root, MANIFEST_NAME) + try: + raw = json.loads(manifest_path.read_text(encoding="utf-8")) + except json.JSONDecodeError as exc: + raise ArtifactError(f"invalid artifact manifest JSON: {exc}") from exc + manifest = ArtifactManifest.from_mapping(root, raw) + if verify_payloads: + validate_payloads(manifest, expected_target=expected_target) + else: + _validate_target_compatibility(manifest, expected_target=expected_target) + return manifest + + +__all__ = [ + "ARTIFACT_IDENTITY_EXTENSION", + "ARTIFACT_IDENTITY_KIND", + "ARTIFACT_IDENTITY_SCHEMA_VERSION", + "MANIFEST_NAME", + "PORTABLE_64LE_TARGET", + "SCHEMA_VERSION", + "ArtifactManifest", + "PayloadRecord", + "artifact_identity_extension", + "canonical_manifest_bytes", + "compute_artifact_id", + "load_manifest", + "validate_payloads", +] diff --git a/pyamplicol/src/pyamplicol/artifacts/process_groups.py b/pyamplicol/src/pyamplicol/artifacts/process_groups.py new file mode 100644 index 0000000000..7524c968a4 --- /dev/null +++ b/pyamplicol/src/pyamplicol/artifacts/process_groups.py @@ -0,0 +1,197 @@ +# SPDX-License-Identifier: 0BSD +"""Read and update the process-group map stored in an artifact. + +``process-groups.json`` is a declared payload whose role is not part of the +runtime-payload identity: rewriting it updates its payload record in +``artifact.json`` but never ``artifact_id``. +""" + +from __future__ import annotations + +import fcntl +import hashlib +import json +import os +import tempfile +from collections.abc import Collection, Iterable, Iterator, Mapping, Sequence +from contextlib import contextmanager, suppress +from pathlib import Path + +from pyamplicol.api.errors import ArtifactError +from pyamplicol.generation.process_grouping import ( + PROCESS_GROUPS_PATH, + PROCESS_GROUPS_ROLE, + GroupingProcess, + GroupMember, + ProcessGroupMap, +) + +from .manifest import ( + MANIFEST_NAME, + PayloadRecord, + canonical_manifest_bytes, + load_manifest, +) +from .security import confined_path, fsync_directory + +PROCESS_GROUPS_MEDIA_TYPE = "application/json" + + +def grouping_process( + process_id: str, + expression: str, + external_pdgs: Sequence[int], +) -> GroupingProcess: + """Return the grouping view of one generated artifact process.""" + + initial, _separator, _final = expression.partition(">") + return GroupingProcess( + process_id=process_id, + expression=expression, + pdgs=tuple(int(pdg) for pdg in external_pdgs), + initial_count=len(initial.split()), + ) + + +def initial_process_groups( + processes: Iterable[tuple[str, str, Sequence[int]]], + members: Mapping[str, Sequence[GroupMember]] | None = None, +) -> ProcessGroupMap: + """Group each generated ``(id, expression, pdgs)`` with its known members. + + ``members`` holds, per representative ID, the relations recorded before + generation (flavour-blind members); every other process maps to itself. + """ + + result = ProcessGroupMap() + for process_id, expression, external_pdgs in processes: + representative = GroupMember.identity( + grouping_process(process_id, expression, external_pdgs) + ) + result.add_group([representative, *(members or {}).get(process_id, ())]) + return result + + +def process_groups_payload(group_map: ProcessGroupMap) -> bytes: + return group_map.to_json().encode("utf-8") + + +def validate_process_groups( + group_map: ProcessGroupMap, + process_ids: Collection[str], +) -> None: + """Require exactly the artifact's processes to be generated members. + + Members that were not generated are evaluated through their representative, + so, as the runtime requires, their factor must be positive. + """ + + generated = [ + member.process_id for member in group_map.members() if member.generated + ] + if len(generated) != len(set(generated)) or set(generated) != set(process_ids): + missing = sorted(set(process_ids) - set(generated)) + unknown = sorted(set(generated) - set(process_ids)) + raise ArtifactError( + "process-group map does not list each artifact process exactly once " + f"as a generated member (missing: {missing}, not in artifact: {unknown})" + ) + for member in group_map.members(): + if not member.generated and member.factor <= 0: + raise ArtifactError( + f"process-group member {member.process_id!r} is not generated, so " + f"its factor {member.factor} must be positive" + ) + + +@contextmanager +def _artifact_lock(root: Path) -> Iterator[None]: + # The lock file of ArtifactTransaction, which serializes generation. + with root.with_name(f".{root.name}.lock").open("a+b") as stream: + fcntl.flock(stream.fileno(), fcntl.LOCK_EX) + try: + yield + finally: + fcntl.flock(stream.fileno(), fcntl.LOCK_UN) + + +def _replace_file(path: Path, content: bytes) -> None: + descriptor, temporary_name = tempfile.mkstemp( + prefix=f".{path.name}.", suffix=".tmp", dir=path.parent + ) + temporary = Path(temporary_name) + try: + with os.fdopen(descriptor, "wb") as stream: + stream.write(content) + stream.flush() + os.fsync(stream.fileno()) + temporary.chmod(0o644) + os.replace(temporary, path) + finally: + with suppress(FileNotFoundError): + temporary.unlink() + + +def read_process_groups(artifact: str | Path) -> ProcessGroupMap | None: + """Return the stored process-group map, or ``None`` when none is declared.""" + + manifest = load_manifest(artifact) + record = next( + (item for item in manifest.payloads if item.role == PROCESS_GROUPS_ROLE), + None, + ) + if record is None: + return None + path = confined_path(manifest.root, record.path) + try: + return ProcessGroupMap.from_json(path.read_text(encoding="utf-8")) + except (KeyError, TypeError, ValueError) as exc: + raise ArtifactError(f"invalid process-group map {record.path}: {exc}") from exc + + +def write_process_groups(artifact: str | Path, group_map: ProcessGroupMap) -> None: + """Replace ``process-groups.json`` and its payload record in place. + + The update runs under the artifact's generation lock. The payload file is + replaced first and the manifest second, each atomically, so an interrupted + update leaves a digest mismatch that the next update repairs. + ``artifact_id`` does not change because the role is not an identity role. + """ + + root = Path(artifact).expanduser().resolve(strict=True) + content = process_groups_payload(group_map) + with _artifact_lock(root): + manifest = load_manifest(root) + validate_process_groups( + group_map, [str(process["id"]) for process in manifest.processes] + ) + manifest_path = confined_path(root, MANIFEST_NAME) + raw = json.loads(manifest_path.read_text(encoding="utf-8")) + payload_path = confined_path(root, PROCESS_GROUPS_PATH, must_exist=False) + _replace_file(payload_path, content) + record = PayloadRecord( + path=PROCESS_GROUPS_PATH, + role=PROCESS_GROUPS_ROLE, + media_type=PROCESS_GROUPS_MEDIA_TYPE, + size_bytes=len(content), + sha256=hashlib.sha256(content).hexdigest(), + executable=False, + ) + payloads = [ + item for item in raw["payloads"] if item.get("path") != PROCESS_GROUPS_PATH + ] + payloads.append(record.as_dict()) + raw["payloads"] = sorted(payloads, key=lambda item: str(item["path"])) + _replace_file(manifest_path, canonical_manifest_bytes(raw)) + fsync_directory(root) + + +__all__ = [ + "PROCESS_GROUPS_MEDIA_TYPE", + "grouping_process", + "initial_process_groups", + "process_groups_payload", + "read_process_groups", + "validate_process_groups", + "write_process_groups", +] diff --git a/pyamplicol/src/pyamplicol/artifacts/security.py b/pyamplicol/src/pyamplicol/artifacts/security.py new file mode 100644 index 0000000000..b2d8a3763d --- /dev/null +++ b/pyamplicol/src/pyamplicol/artifacts/security.py @@ -0,0 +1,95 @@ +# SPDX-License-Identifier: 0BSD +"""Filesystem and digest checks for schema-v3 process artifacts.""" + +from __future__ import annotations + +import hashlib +import os +import stat +from pathlib import Path, PurePosixPath + +from pyamplicol.api.errors import ArtifactError + + +def normalize_relative_path(value: str) -> str: + """Return a normalized artifact path or reject unsafe input.""" + + if not isinstance(value, str) or not value: + raise ArtifactError("artifact payload paths must be non-empty strings") + if "\\" in value or "\x00" in value: + raise ArtifactError(f"artifact payload path is not portable: {value!r}") + path = PurePosixPath(value) + if path.is_absolute() or any(part in {"", ".", ".."} for part in path.parts): + raise ArtifactError(f"artifact payload path is not confined: {value!r}") + normalized = path.as_posix() + if normalized != value: + raise ArtifactError( + f"artifact payload path is not normalized: {value!r} != {normalized!r}" + ) + return normalized + + +def confined_path(root: Path, relative: str, *, must_exist: bool = True) -> Path: + """Resolve a payload while rejecting symlinks and root escapes.""" + + normalized = normalize_relative_path(relative) + root = root.resolve(strict=True) + candidate = root.joinpath(*PurePosixPath(normalized).parts) + current = root + for part in PurePosixPath(normalized).parts: + current = current / part + try: + metadata = current.lstat() + except FileNotFoundError: + if must_exist: + raise ArtifactError( + f"artifact payload is missing: {normalized}" + ) from None + break + if stat.S_ISLNK(metadata.st_mode): + raise ArtifactError( + f"artifact payload may not traverse a symlink: {normalized}" + ) + if must_exist and not candidate.is_file(): + raise ArtifactError(f"artifact payload is not a regular file: {normalized}") + try: + resolved = candidate.resolve(strict=must_exist) + resolved.relative_to(root) + except (OSError, ValueError) as exc: + raise ArtifactError(f"artifact payload escapes its root: {normalized}") from exc + return candidate + + +def sha256_file(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as stream: + while block := stream.read(1024 * 1024): + digest.update(block) + return digest.hexdigest() + + +def executable_bit(path: Path) -> bool: + return bool(path.stat().st_mode & (stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)) + + +def fsync_file(path: Path) -> None: + with path.open("rb") as stream: + os.fsync(stream.fileno()) + + +def fsync_directory(path: Path) -> None: + descriptor = os.open(path, os.O_RDONLY) + try: + os.fsync(descriptor) + finally: + os.close(descriptor) + + +__all__ = [ + "confined_path", + "executable_bit", + "fsync_directory", + "fsync_file", + "normalize_relative_path", + "sha256_file", +] diff --git a/pyamplicol/src/pyamplicol/artifacts/transaction.py b/pyamplicol/src/pyamplicol/artifacts/transaction.py new file mode 100644 index 0000000000..51d3d6d487 --- /dev/null +++ b/pyamplicol/src/pyamplicol/artifacts/transaction.py @@ -0,0 +1,205 @@ +# SPDX-License-Identifier: 0BSD +"""Locked transactional directory updates for generated artifacts.""" + +from __future__ import annotations + +import ctypes +import errno +import fcntl +import os +import shutil +import sys +import uuid +from pathlib import Path +from types import TracebackType +from typing import BinaryIO, Literal + +from pyamplicol.api.errors import ArtifactError + +from .security import fsync_directory + +ArtifactWriteMode = Literal["error", "append", "replace"] + +_AT_FDCWD = -100 +_RENAME_EXCHANGE = 0x2 +_LINUX_X86_64_RENAMEAT2 = 316 + + +def _raise_exchange_error(source: Path, destination: Path) -> None: + error_number = ctypes.get_errno() or errno.EIO + raise OSError( + error_number, + (f"atomic artifact directory exchange failed: {os.strerror(error_number)}"), + f"{source} <-> {destination}", + ) + + +def _exchange_directories(source: Path, destination: Path) -> None: + """Atomically exchange two existing directories on release platforms.""" + + source_bytes = os.fsencode(source) + destination_bytes = os.fsencode(destination) + libc = ctypes.CDLL(None, use_errno=True) + ctypes.set_errno(0) + + if sys.platform == "darwin": + try: + renameatx_np = libc.renameatx_np + except AttributeError as error: + raise OSError( + errno.ENOTSUP, + "this macOS runtime does not provide atomic directory exchange", + ) from error + renameatx_np.argtypes = ( + ctypes.c_int, + ctypes.c_char_p, + ctypes.c_int, + ctypes.c_char_p, + ctypes.c_uint, + ) + renameatx_np.restype = ctypes.c_int + result = renameatx_np( + _AT_FDCWD, + source_bytes, + _AT_FDCWD, + destination_bytes, + _RENAME_EXCHANGE, + ) + elif sys.platform.startswith("linux"): + try: + renameat2 = libc.renameat2 + except AttributeError: + if os.uname().machine != "x86_64": + raise OSError( + errno.ENOTSUP, + "atomic directory exchange is unsupported on this Linux target", + ) from None + syscall = libc.syscall + syscall.restype = ctypes.c_long + result = syscall( + _LINUX_X86_64_RENAMEAT2, + _AT_FDCWD, + source_bytes, + _AT_FDCWD, + destination_bytes, + _RENAME_EXCHANGE, + ) + else: + renameat2.argtypes = ( + ctypes.c_int, + ctypes.c_char_p, + ctypes.c_int, + ctypes.c_char_p, + ctypes.c_uint, + ) + renameat2.restype = ctypes.c_int + result = renameat2( + _AT_FDCWD, + source_bytes, + _AT_FDCWD, + destination_bytes, + _RENAME_EXCHANGE, + ) + else: + raise OSError( + errno.ENOTSUP, + "atomic artifact replacement is supported only on macOS and Linux", + ) + + if result != 0: + _raise_exchange_error(source, destination) + + +class ArtifactTransaction: + """Prepare a sibling directory and publish it only after successful work.""" + + def __init__( + self, + destination: str | Path, + *, + mode: ArtifactWriteMode = "error", + ) -> None: + if mode not in ("error", "append", "replace"): + raise ValueError("artifact mode must be 'error', 'append', or 'replace'") + self.destination = Path(destination).expanduser().resolve(strict=False) + self.mode = mode + self.staging = self.destination.with_name( + f".{self.destination.name}.staging-{uuid.uuid4().hex}" + ) + self._lock_path = self.destination.with_name(f".{self.destination.name}.lock") + self._lock_stream: BinaryIO | None = None + self._entered = False + self._committed = False + + def __enter__(self) -> Path: + if self._entered: + raise RuntimeError("artifact transaction cannot be entered twice") + self.destination.parent.mkdir(parents=True, exist_ok=True) + lock_stream = self._lock_path.open("a+b") + fcntl.flock(lock_stream.fileno(), fcntl.LOCK_EX) + self._lock_stream = lock_stream + self._entered = True + try: + if self.destination.exists() and not self.destination.is_dir(): + raise ArtifactError( + f"artifact destination is not a directory: {self.destination}" + ) + if self.mode == "error" and self.destination.exists(): + raise FileExistsError(f"artifact already exists: {self.destination}") + if self.mode == "append": + if not self.destination.is_dir(): + raise FileNotFoundError( + f"cannot append to missing artifact: {self.destination}" + ) + shutil.copytree(self.destination, self.staging) + else: + self.staging.mkdir() + return self.staging + except BaseException: + self._release() + raise + + def commit(self) -> None: + if not self._entered: + raise RuntimeError("artifact transaction has not been entered") + if self._committed: + raise RuntimeError("artifact transaction is already committed") + if not self.staging.is_dir(): + raise ArtifactError("artifact staging directory disappeared before commit") + fsync_directory(self.staging) + try: + if self.destination.exists(): + _exchange_directories(self.staging, self.destination) + else: + os.replace(self.staging, self.destination) + fsync_directory(self.destination.parent) + self._committed = True + except BaseException: + # Atomic exchange leaves both directory names untouched on failure. + raise + + def _release(self) -> None: + if self.staging.exists(): + shutil.rmtree(self.staging) + if self._lock_stream is not None: + stream = self._lock_stream + fcntl.flock(stream.fileno(), fcntl.LOCK_UN) + stream.close() + self._lock_stream = None + + def __exit__( + self, + exception_type: type[BaseException] | None, + exception: BaseException | None, + traceback: TracebackType | None, + ) -> Literal[False]: + del exception, traceback + try: + if exception_type is None and not self._committed: + self.commit() + finally: + self._release() + return False + + +__all__ = ["ArtifactTransaction", "ArtifactWriteMode"] diff --git a/pyamplicol/src/pyamplicol/artifacts/umami.py b/pyamplicol/src/pyamplicol/artifacts/umami.py new file mode 100644 index 0000000000..a687412204 --- /dev/null +++ b/pyamplicol/src/pyamplicol/artifacts/umami.py @@ -0,0 +1,939 @@ +# SPDX-License-Identifier: 0BSD +"""Export UMAMI lookup tables without changing numerical artifact contents. + +Identical final particles permit relabelling of physical colour contributions. +Other equivalences require generation certificates or exact structural keys; +equal masses or parameter defaults are not a proof. The exception is the +explicit ``--flavor-blind`` assumption: its ungenerated group members are +exported through their representative's runtime and recorded in +``grouping.assumptions``. Integration multiplicities live in the JSON, not in +the C provider. +""" + +from __future__ import annotations + +import json +import math +from collections.abc import Mapping, Sequence +from contextlib import suppress +from dataclasses import dataclass +from typing import TYPE_CHECKING, Any + +from .api_bundle import ApiBundlePayload + +if TYPE_CHECKING: + from ..generation.process_grouping import GroupMember + +FLAVOR_BLIND_ASSUMPTION = ( + "flavor-blind matrix elements: ungenerated process-group members are " + "evaluated through their relabelled representative (assumed, not proven)" +) + + +@dataclass(frozen=True) +class UmamiProcessInput: + process_id: str + expression: str + color_accuracy: str + external_pdgs: tuple[int, ...] + physics: Mapping[str, Any] + aliases: tuple[Mapping[str, Any], ...] = () + color_sectors: tuple[Mapping[str, Any], ...] = () + # Keys certify equality of the normalized, available-helicity-summed + # squared contribution, in this process's external momentum order. + structural_keys: Mapping[str, str] | None = None + # Used when re-exporting an appended compact OTF process: its previous SDK + # contains authoritative physical LHE tags, while physics.json stays compact. + color_flows: Mapping[str, Sequence[Sequence[int]]] | None = None + # Ungenerated group members evaluated through this process's runtime: + # ME_member(p) = factor * ME_this(q) with q[r] = p[permutation[r]]. + group_members: tuple[GroupMember, ...] = () + + +def _records(value: Any) -> list[Mapping[str, Any]]: + return [record for record in value or () if isinstance(record, Mapping)] + + +def _finite(value: Any, context: str) -> float: + result = float(value) + if not math.isfinite(result): + raise ValueError(f"UMAMI {context} must be finite") + return result + + +def _pair(value: Any) -> tuple[float, float]: + if isinstance(value, Sequence) and not isinstance(value, str): + return _finite(value[0], "parameter"), _finite(value[1], "parameter") + return _finite(value, "parameter"), 0.0 + + +def _model_tables(model: Mapping[str, Any]) -> tuple[dict, dict, dict]: + ir = model.get("ir", model.get("model", model)) + particles = {int(p["pdg_code"]): p for p in _records(ir.get("particles"))} + parameters = {str(p["name"]): p for p in _records(ir.get("parameters"))} + defaults = { + str(k): _pair(v) for k, v in model.get("parameter_defaults", {}).items() + } + for name, parameter in parameters.items(): + if name not in defaults and parameter.get("value") is not None: + defaults[name] = _pair(parameter["value"]) + if name not in defaults: + with suppress(TypeError, ValueError): + defaults[name] = _pair(parameter.get("resolved_expression")) + return particles, parameters, defaults + + +def _mass( + particle: Mapping, parameters: Mapping, defaults: Mapping +) -> tuple[str, float]: + name = str(particle["mass"]) + if name in defaults: + real, imaginary = defaults[name] + else: + try: + real, imaginary = float(name), 0.0 + except ValueError as exc: + # UFO's reserved ZERO is a literal, not an independent parameter. + if name.upper() != "ZERO": + raise ValueError(f"UMAMI cannot resolve default mass {name!r}") from exc + real, imaginary = 0.0, 0.0 + if imaginary or real < 0.0 or not math.isfinite(real): + raise ValueError(f"UMAMI external mass {name!r} must be nonnegative and real") + declaration = parameters.get(name, {}) + semantic = ( + name + if declaration.get("nature") == "external" + else str(declaration.get("resolved_expression", name)) + ) + if not declaration and real == 0.0: + semantic = "0" + return semantic, real + + +def _alpha_s_name(model: Mapping, physics: Mapping, parameters: Mapping) -> str | None: + public = {p["name"]: p for p in _records(physics.get("model_parameters"))} + explicit = model.get("extensions", {}).get("alpha_s_parameter") + candidates = ( + [str(explicit)] + if explicit + else [ + name + for name, p in parameters.items() + if str(p.get("lhablock", "")).upper() == "SMINPUTS" + and list(p.get("lhacode", ())) == [3] + and p.get("nature") == "external" + ] + ) + candidates = [name for name in candidates if public.get(name, {}).get("mutable")] + return candidates[0] if len(candidates) == 1 else None + + +def _slha_reference(declaration: Mapping) -> dict[str, Any]: + """The parameter card entry of an external UFO parameter, if it has one.""" + if declaration.get("nature") != "external" or not declaration.get("lhablock"): + return {} + return { + "lhablock": str(declaration["lhablock"]).upper(), + "lhacode": [int(code) for code in declaration.get("lhacode", ())], + } + + +def _flow_id(word: Sequence[int]) -> str: + return "flow:" + (",".join(str(label) for label in word) if word else "singlet") + + +def _word(sector: Mapping) -> tuple[int, ...]: + return tuple( + sector.get("word_labels") or next(iter(sector.get("color_words", ())), ()) + ) + + +def _permutation(values: Sequence[int], count: int) -> tuple[int, ...]: + result = tuple(int(i) for i in values) + if sorted(result) != list(range(count)): + raise ValueError("UMAMI external permutation is not a complete bijection") + return result + + +def _member_permutation( + member: GroupMember, count: int, incoming: int +) -> tuple[int, ...]: + """The ``representative leg -> member leg`` map; incoming swaps are allowed.""" + permutation = _permutation(member.permutation, count) + if len(member.pdgs) != count or any( + (leg < incoming) != (target < incoming) + for leg, target in enumerate(permutation) + ): + raise ValueError( + f"UMAMI group member {member.process_id!r} must keep initial legs initial" + ) + return permutation + + +def _color_flow( + word: Sequence[int], + external: Sequence[Mapping], + particles: Mapping, + sector: Mapping | None = None, +) -> list[list[int]]: + """Assign LHE tags from all-outgoing fundamental chains/adjoint cycles.""" + labels = {int(p["label"]): i for i, p in enumerate(external)} + roles = {} + for label, slot in labels.items(): + p = external[slot] + representation = int(particles[int(p["pdg"])]["color"]) + if p["role"] == "initial" and abs(representation) == 3: + representation = -representation + roles[label] = representation + colored = [int(label) for label in word if roles[int(label)] != 1] + if set(colored) != {label for label, rep in roles.items() if rep != 1}: + raise ValueError("UMAMI LC word must contain every coloured external leg once") + if len(colored) != len(set(colored)): + raise ValueError("UMAMI LC word repeats an external leg") + if any(rep not in (1, 3, -3, 8) for rep in roles.values()): + raise ValueError( + "UMAMI LHE tags require singlet, fundamental or adjoint colour" + ) + result = [[0, 0] for _ in external] + edge = 501 + if sector is not None and sector.get("kind") == "open-lines": + lines = _records(sector.get("open_color_lines")) + chains = [ + [ + int(line["fundamental_label"]), + *(int(label) for label in line.get("adjoint_labels", ())), + int(line["antifundamental_label"]), + ] + for line in lines + ] + flattened = [label for chain in chains for label in chain] + if sorted(flattened) != sorted(colored): + raise ValueError("UMAMI colour-sector lines do not cover the LC word") + for chain in chains: + if ( + roles[chain[0]] != 3 + or roles[chain[-1]] != -3 + or any(roles[label] != 8 for label in chain[1:-1]) + ): + raise ValueError( + "UMAMI colour-sector endpoints disagree with the model" + ) + result[labels[chain[0]]][0] = edge + for label in chain[1:-1]: + result[labels[label]] = [edge + 1, edge] + edge += 1 + result[labels[chain[-1]]][1] = edge + edge += 1 + elif colored and all(roles[label] == 8 for label in colored): + for index, label in enumerate(colored): + result[labels[label]] = [edge + index, edge + (index - 1) % len(colored)] + else: + current = None + for label in colored: + role = roles[label] + if role == 3: + if current is not None: + raise ValueError( + "UMAMI LC word has an unterminated fundamental chain" + ) + current = edge + result[labels[label]][0] = current + elif role == 8 and current is not None: + result[labels[label]] = [current + 1, current] + current += 1 + elif role == -3 and current is not None: + result[labels[label]][1] = current + edge = current + 1 + current = None + else: + raise ValueError( + "UMAMI LC word does not describe complete fundamental chains" + ) + if current is not None: + raise ValueError("UMAMI LC word has an unterminated fundamental chain") + for index, p in enumerate(external): + if p["role"] == "initial": + result[index].reverse() + return result + + +def _safe_replay(process: UmamiProcessInput) -> dict[str, tuple[str, tuple[int, ...]]]: + """Convert certified label maps; weights may fold reflection, not one flow.""" + physics = process.physics + external = _records(physics["external_particles"]) + count = len(external) + labels = {int(p["label"]): i for i, p in enumerate(external)} + identity = tuple(range(count)) + colors = {str(c["id"]): c for c in _records(physics.get("color_components"))} + result = {key: (key, identity) for key in colors} + certificate = physics.get("extensions", {}).get("lc_topology_replay") + if not certificate: + # These public nontrivial mappings are certified trace reflections. + for key, color in colors.items(): + representative = str(color.get("representative_id", key)) + if representative in colors and float(color.get("coefficient", 1.0)) == 1.0: + result[key] = (representative, identity) + return result + sectors = {int(s["id"]): s for s in process.color_sectors} + sector_flow_ids = {_flow_id(_word(s)) for s in sectors.values()} + helicities = {tuple(h["values"]) for h in _records(physics.get("helicities"))} + for partition in _records(certificate.get("partitions")): + if (partition.get("proof") or {}).get("status") != "proven": + continue + representative = sectors.get(int(partition["representative_sector_id"])) + if representative is None: + continue + rep_id = _flow_id(_word(representative)) + for sector_id, pairs in zip( + partition["active_sector_ids"], partition["label_permutations"], strict=True + ): + sector = sectors.get(int(sector_id)) + if sector is None: + continue + mapping = dict(pairs) + try: + perm = _permutation( + [labels[int(mapping[int(p["label"])])] for p in external], count + ) + except (KeyError, ValueError): + continue + # No beam/PDF symmetry is assumed. Restricted helicity coverage + # must also be invariant under the proposed external relabelling. + if any( + perm[i] != i for i, p in enumerate(external) if p["role"] == "initial" + ): + continue + if {tuple(h[i] for i in perm) for h in helicities} != helicities: + continue + word = _word(sector) + target_ids = [_flow_id(word)] + if sector.get("kind") == "single-trace" and len(word) > 2: + reflected = _flow_id((word[0], *reversed(word[1:]))) + # Only an additional public flow absent from the sector plan + # is a folded reflection. An explicit reversed sector has its + # own proof/permutation and must not be overwritten here. + if reflected not in sector_flow_ids: + target_ids.append(reflected) + for target in target_ids: + if target in colors and rep_id in colors: + result[target] = (rep_id, perm) + return result + + +def _channel_pattern(order, incoming, masses): + """The cyclic colour order from the first incoming leg, with the final-state + legs known only by their mass: orders with equal patterns share one + phase-space map up to relabelling final-state legs of equal mass.""" + start = order.index(0) + rotated = order[start:] + order[:start] + pattern = tuple( + ("initial", slot) if slot < incoming else ("final", masses[slot]) + for slot in rotated + ) + return pattern, rotated + + +def _relabelled(values, relabel): + result = [None] * len(values) + for slot, value in enumerate(values): + result[relabel[slot]] = value + return result + + +def _relabelled_runtime(runtime, relabel): + return { + **runtime, + "permutation": [relabel[slot] for slot in runtime["permutation"]], + } + + +def _intern(table: list, value: Any) -> int: + try: + return table.index(value) + except ValueError: + table.append(value) + return len(table) - 1 + + +def _identical_orbit(order, flow, pdgs, incoming, helicities, word_size): + """An oriented physical colour tensor modulo identical outgoing labels. + + Equal keys induce the explicit bijection between the two complete orders. + It fixes incoming slots, preserves particle identity, directed colour edges + and the available helicity domain. Exchanging identical fermions changes + the overall amplitude sign, not this squared physical contribution. This + argument is independent of runtime topology/reflection reuse certificates. + """ + + def key(candidate): + tags = {} + directed_edges = [] + for slot in candidate: + for tag in flow[slot] if flow is not None else (): + directed_edges.append(tags.setdefault(tag, len(tags) + 1) if tag else 0) + return ( + tuple( + ("incoming", slot) if slot < incoming else ("final", pdgs[slot]) + for slot in candidate + ), + tuple(directed_edges), + tuple( + sorted( + tuple(h["values"][slot] for slot in candidate) for h in helicities + ) + ), + ) + + starts = [0] + if flow is not None and word_size: + # Undo incoming crossing to identify oriented open-line starts from + # the actual colour tensor. A closed trace has no such endpoints. + pairs = [ + tuple(reversed(flow[s])) if s < incoming else tuple(flow[s]) + for s in order[:word_size] + ] + if all(any(pair) for pair in pairs): + starts = [i for i, (left, right) in enumerate(pairs) if left and not right] + if not starts: + starts = list(range(word_size)) if all(all(p) for p in pairs) else [0] + # Re-anchoring a trace, or cyclically rotating complete open-line blocks, + # changes no colour tensor. Never reverse it or arbitrarily sort blocks. + candidates = [order[i:word_size] + order[:i] + order[word_size:] for i in starts] + return min((key(candidate), candidate) for candidate in candidates) + + +def _add_terms(entry, terms): + for term in terms: + existing = next( + ( + value + for value in entry["matrix_elements"] + if value["pdg_ids"] == term["pdg_ids"] + ), + None, + ) + if existing is None: + entry["matrix_elements"].append(dict(term)) + else: + existing["factor"] += term["factor"] + + +def _merge_flavour_orbits(data): + """Merge different flavours with a common proven computation. + + Do not use runtime reflection reuse to further collapse same-flavour colour + orders: their integration partition was already constructed physically. + A merged flavour is evaluated by the kept selector, so the complete runtime + evaluation (permutation, factor and colour tags included) must agree. + """ + candidates = {} + incoming = data["provider"]["incoming_count"] + for channel in data["channels"]: + kept = [] + for entry in channel["processes"]: + runtime = entry["runtime"] + pdgs = {term["pdg_ids"] for term in entry["matrix_elements"]} + key = ( + runtime["process_index"], + runtime["color_id"], + runtime["color_index"], + tuple(runtime["permutation"]), + runtime["factor"], + tuple(data["pdg_ids"][next(iter(pdgs))][:incoming]), + ) + previous = next( + ( + other + for other in candidates.get(key, ()) + if pdgs.isdisjoint(t["pdg_ids"] for t in other["matrix_elements"]) + ), + None, + ) + if previous is None: + candidates.setdefault(key, []).append(entry) + kept.append(entry) + else: + _add_terms(previous, entry["matrix_elements"]) + previous["members"].extend(entry["members"]) + channel["processes"] = kept + data["channels"] = [c for c in data["channels"] if c["processes"]] + for index, channel in enumerate(data["channels"]): + for entry in channel["processes"]: + entry["multichannels"] = [index] + + +def _export_helicities(data, runtime_helicities): + """Map runtime helicities to exported ones, given in the input leg order.""" + entries = [e for c in data["channels"] for e in c["processes"]] + for runtime in [e["runtime"] for e in entries] + [ + m["runtime"] for e in entries for m in e["members"] + ]: + permutation = runtime["permutation"] + exported = [] + for values in runtime_helicities[runtime["process_index"]]: + ordered = [0] * len(values) + for runtime_leg, input_leg in enumerate(permutation): + ordered[input_leg] = values[runtime_leg] + exported.append(_intern(data["helicities"], ordered)) + runtime["helicity_export"] = exported + + +def build_umami_metadata( + *, + processes: Sequence[UmamiProcessInput], + compiled_model: Mapping[str, Any], + artifact_id: str | None = None, +) -> tuple[dict[str, Any], ...]: + """Return separate, directly consumable metadata documents per provider. + + Each provider exports sources ``(process, member)``: a generated process + (``member`` is None) or one of its ungenerated group members, evaluated + through the process's runtime without a runtime row of its own. + """ + particles, parameters, defaults = _model_tables(compiled_model) + groups: list[tuple[tuple, list[tuple], list[tuple[str, float]]]] = [] + + def physical(source, incoming: int) -> tuple: + process, member = source + pdgs = process.external_pdgs if member is None else tuple(member.pdgs) + return pdgs[:incoming], tuple(sorted(pdgs[incoming:])) + + def place(signature, source, masses, preferred=None): + # Alternative selections of the same external process are not additive. + key = physical(source, signature[1]) + compatible = next( + ( + g + for g in ([preferred] if preferred else []) + groups + if g[0] == signature + and all(physical(other, signature[1]) != key for other in g[1]) + ), + None, + ) + if compatible is None: + compatible = (signature, [], masses) + groups.append(compatible) + compatible[1].append(source) + return compatible + + for process in processes: + external = _records(process.physics.get("external_particles")) + if not external or len(external) != len(process.external_pdgs): + raise ValueError( + f"UMAMI process {process.process_id!r} lacks resolved external metadata" + ) + if tuple(int(p["pdg"]) for p in external) != process.external_pdgs: + raise ValueError("UMAMI external PDGs disagree with resolved physics") + incoming = sum(p["role"] == "initial" for p in external) + if [p["role"] for p in external] != ["initial"] * incoming + ["final"] * ( + len(external) - incoming + ): + raise ValueError("UMAMI requires initial legs before final legs") + # The public catalog has already evaluated internal/derived parameters + # for the artifact's defaults. Reuse those values instead of adding a + # second symbolic evaluator here (the parameter card can omit them). + process_defaults = { + **defaults, + **{ + str(p["name"]): ( + _finite(p["default_real"], "parameter"), + _finite(p.get("default_imaginary", 0.0), "parameter"), + ) + for p in _records(process.physics.get("model_parameters")) + }, + } + masses = [ + _mass(particles[pdg], parameters, process_defaults) + for pdg in process.external_pdgs + ] + # The available helicity set, not its enumeration order. + helicity_domain = tuple( + sorted( + tuple(h["values"]) for h in _records(process.physics.get("helicities")) + ) + ) + signature = ( + len(external), + incoming, + process.color_accuracy, + tuple(masses), + helicity_domain, + ) + home = place(signature, (process, None), masses) + for member in process.group_members: + # A member's legs carry its own masses; a member whose relabelling + # moves unequal masses needs a provider of its own. + relation = _member_permutation(member, len(external), incoming) + member_masses = [ + _mass(particles[int(pdg)], parameters, process_defaults) + for pdg in member.pdgs + ] + domain = tuple( + sorted(tuple(_relabelled(v, relation)) for v in helicity_domain) + ) + place( + (*signature[:3], tuple(member_masses), domain), + (process, member), + member_masses, + home, + ) + documents = [] + for index, (_, sources, masses) in enumerate(groups): + provider_id = f"p{index}" + documents.append( + _build_provider( + provider_id, + sources, + masses, + compiled_model, + particles, + parameters, + len(groups) == 1, + ) + ) + documents[-1]["provider"]["artifact_id"] = artifact_id + return tuple(documents) + + +def _build_provider( + provider_id, sources, masses, model, particles, parameters, single +): + first_external = _records(sources[0][0].physics["external_particles"]) + count = len(first_external) + incoming = sum(p["role"] == "initial" for p in first_external) + identity = list(range(count)) + assumptions = [ + "identical-final-particle permutation-invariant observables and cuts", + "final-state flavour-blind observables and cuts", + ] + if any(member is not None for _, member in sources): + assumptions.append(FLAVOR_BLIND_ASSUMPTION) + data: dict[str, Any] = { + "kind": "pyamplicol-umami-metadata", + "schema_version": 1, + "provider": { + "id": provider_id, + "library": "libumami.so" if single else f"libumami_{provider_id}.so", + "particle_count": count, + "incoming_count": incoming, + "masses": [m[1] for m in masses], + "mass_parameters": [m[0] for m in masses], + "color_accuracy": sources[0][0].color_accuracy, + }, + "grouping": {"assumptions": assumptions}, + "channels": [], + "pdg_ids": [], + "color_orders": [], + "color_flows": [], + "helicities": [], + "runtime_processes": [], + "aliases": [], + "xml_header": ( + 'UMAMI artifact provider' + ), + } + structural_representatives: dict[str, dict] = {} + orbit_entries: dict[tuple, tuple[dict, list[int]]] = {} + channel_lookup = {} + channel_patterns: dict[tuple, list[int]] = {} + physical_count = 0 + runtime_helicities: list[list[list[int]]] = [] + runtime_indices: dict[str, int] = {} + for process, group_member in sources: + physics = process.physics + external = _records(physics["external_particles"]) + labels = {int(p["label"]): i for i, p in enumerate(external)} + helicities = _records(physics.get("helicities")) + if process.process_id not in runtime_indices: + runtime_indices[process.process_id] = len(data["runtime_processes"]) + data["runtime_processes"].append( + { + "id": process.process_id, + "expression": process.expression, + "pdgs": list(process.external_pdgs), + "color_accuracy": process.color_accuracy, + "alpha_s_parameter": _alpha_s_name(model, physics, parameters), + "parameters": [ + { + **p, + **_slha_reference(parameters.get(p["name"], {})), + "is_complex": ( + parameters.get(p["name"], {}).get("parameter_type") + == "complex" + or p.get("kind") == "coupling" + or p.get("default_imaginary", 0.0) != 0.0 + ), + } + for p in _records(physics.get("model_parameters")) + ], + "helicity_ids": [h["id"] for h in helicities], + "coverage": dict(physics.get("coverage", {})), + } + ) + data["aliases"].extend( + {"process_id": process.process_id, **dict(a)} for a in process.aliases + ) + runtime_helicities.append([list(h["values"]) for h in helicities]) + process_index = runtime_indices[process.process_id] + source_id, pdgs = process.process_id, process.external_pdgs + relation = None + if group_member is not None: + # Representative leg r is the member's leg relation[r]: helicity + # values and colour tags move with it, momenta through the runtime. + relation = _member_permutation(group_member, count, incoming) + source_id, pdgs = group_member.process_id, tuple(group_member.pdgs) + helicities = [ + {**h, "values": _relabelled(h["values"], relation)} for h in helicities + ] + # Helicity values in the input leg order, interned per contribution + # once its final-state legs are relabelled to the channel. + helicity_groups: dict[str, list[list[int]]] = {} + helicity_mappings = [] + for h in helicities: + key = str(h.get("representative_id", h["id"])) + helicity_groups.setdefault(key, []).append(list(h["values"])) + helicity_mappings.append( + { + "helicity": list(h["values"]), + "representative_id": h.get("representative_id", h["id"]), + "coefficient": h.get("coefficient", 1.0), + "structural_zero": h.get("structural_zero", False), + } + ) + replay = _safe_replay(process) + sectors_by_word = {_word(sector): sector for sector in process.color_sectors} + colors = sorted( + _records(physics.get("color_components")), + key=lambda color: tuple(color.get("word", ())), + ) + for color in colors: + physical_count += 1 + color_id = str(color["id"]) + contracted = process.color_accuracy != "lc" + word = tuple(int(v) for v in color.get("word", ())) + order = [labels[label] for label in word] + order += [i for i in identity if i not in order] + order = list(_permutation(order, count)) + flow = ( + None + if contracted + else [list(pair) for pair in process.color_flows[color_id]] + if process.color_flows is not None + else _color_flow(word, external, particles, sectors_by_word.get(word)) + ) + physical_color = None if contracted else color_id + if relation is not None: + # Charge conjugation reverses the word and swaps the colour + # and anticolour tags of every leg. + word_slots = [relation[slot] for slot in order[: len(word)]] + if group_member.conjugated: + word_slots.reverse() + order = word_slots + [i for i in identity if i not in word_slots] + if flow is not None: + if group_member.conjugated: + flow = [pair[::-1] for pair in flow] + flow = _relabelled(flow, relation) + physical_color = _flow_id( + [int(external[slot]["label"]) for slot in word_slots] + ) + color_index = None if flow is None else _intern(data["color_flows"], flow) + rep_color, perm = replay.get(color_id, (color_id, tuple(identity))) + runtime = { + "process_index": process_index, + "process_id": process.process_id, + "color_id": None if contracted else rep_color, + "color_index": 0 if color_index is None else color_index, + "permutation": list(perm), + "factor": 1.0, + } + semantic_key = (process.structural_keys or {}).get(color_id) + if semantic_key: + if semantic_key in structural_representatives: + runtime = dict(structural_representatives[semantic_key]) + else: + # The proof key already includes normalization and colour + # contraction; exact cross-flavour equality needs no cuts. + structural_representatives[semantic_key] = dict(runtime) + if relation is not None: + runtime = { + **runtime, + "permutation": [relation[slot] for slot in runtime["permutation"]], + "factor": runtime["factor"] * float(group_member.factor), + } + member = { + "process_id": source_id, + "pdgs": list(pdgs), + "color_id": physical_color, + "color_flows": color_index, + "runtime": dict(runtime), + "factor": 1.0, + } + orbit_key, aligned_order = _identical_orbit( + order, + flow, + pdgs, + incoming, + helicities, + len(word), + ) + orbit = (source_id, orbit_key) + if orbit in orbit_entries: + entry, representative_order, representative_runtime, relabel = ( + orbit_entries[orbit] + ) + # Evaluate each physical member through the exported entry, + # independently of any additional internal runtime reuse. + slots = dict(zip(representative_order, aligned_order, strict=True)) + member["runtime"] = { + **representative_runtime, + "permutation": [ + slots[i] for i in representative_runtime["permutation"] + ], + } + pdg_index = _intern( + data["pdg_ids"], _relabelled(list(pdgs), relabel) + ) + _add_terms(entry, [{"pdg_ids": pdg_index, "factor": 1.0}]) + entry["members"].append(member) + continue + # The exported contribution relabels its final-state legs onto the + # first colour order of its channel; members keep physical labels. + pattern, rotated = _channel_pattern( + order, incoming, data["provider"]["masses"] + ) + relabel = list(identity) + representative = channel_patterns.setdefault(pattern, rotated) + for slot, target in zip(rotated, representative, strict=True): + relabel[slot] = target + channel_order = [relabel[slot] for slot in order] + entry_color_index = ( + None if flow is None else _intern(data["color_flows"], _relabelled(flow, relabel)) + ) + entry_runtime = _relabelled_runtime(runtime, relabel) + if entry_color_index is not None: + entry_runtime["color_index"] = entry_color_index + pdg_index = _intern( + data["pdg_ids"], _relabelled(list(pdgs), relabel) + ) + entry = { + "matrix_elements": [{"pdg_ids": pdg_index, "factor": 1.0}], + "color_order": _intern(data["color_orders"], channel_order), + "color_flows": entry_color_index, + "helicities": [ + [_intern(data["helicities"], _relabelled(v, relabel)) for v in group] + for group in helicity_groups.values() + ], + "helicity_mappings": [ + { + **mapping, + "helicity": _intern( + data["helicities"], _relabelled(mapping["helicity"], relabel) + ), + } + for mapping in helicity_mappings + ], + "runtime": entry_runtime, + "members": [member], + "multichannels": [], + } + # A single complete map per channel pattern. Maps are shared only if + # their fixed external masses agree (already a provider invariant). + if pattern not in channel_lookup: + channel_lookup[pattern] = len(data["channels"]) + data["channels"].append( + {"phasespace_order": channel_order, "processes": []} + ) + channel_index = channel_lookup[pattern] + entry["multichannels"] = [channel_index] + data["channels"][channel_index]["processes"].append(entry) + orbit_entries[orbit] = (entry, aligned_order, runtime, relabel) + _merge_flavour_orbits(data) + _export_helicities(data, runtime_helicities) + alpha_names = {p["alpha_s_parameter"] for p in data["runtime_processes"]} + data["provider"]["alpha_s_parameter"] = ( + next(iter(alpha_names)) if len(alpha_names) == 1 else None + ) + data["provider"]["supports_alpha_s"] = all(name is not None for name in alpha_names) + data["provider"]["helicity_count"] = max( + (len(p["helicity_ids"]) for p in data["runtime_processes"]), default=0 + ) + data["provider"]["color_count"] = max(1, len(data["color_flows"])) + data["grouping"]["physical_contributions"] = physical_count + data["grouping"]["exported_contributions"] = sum( + len(c["processes"]) for c in data["channels"] + ) + return data + + +def _json_payload(path: str, value: Any) -> ApiBundlePayload: + return ApiBundlePayload( + path, + (json.dumps(value, indent=2, sort_keys=True, allow_nan=False) + "\n").encode(), + "sdk-metadata", + "application/json", + ) + + +def umami_bundle_payloads( + *, + processes: Sequence[UmamiProcessInput], + compiled_model: Mapping[str, Any], + artifact_id: str | None = None, +) -> tuple[ApiBundlePayload, ...]: + """Create JSON, provider headers and Make variables from one representation.""" + from .umami_c import provider_header + + documents = build_umami_metadata( + processes=processes, + compiled_model=compiled_model, + artifact_id=artifact_id, + ) + payloads = [] + entries = [] + for data in documents: + provider = data["provider"] + identifier = provider["id"] + relative = ( + "metadata.json" + if len(documents) == 1 + else f"providers/{identifier}/metadata.json" + ) + payloads.append(_json_payload(f"API/umami/{relative}", data)) + entries.append( + {"id": identifier, "metadata": relative, "library": provider["library"]} + ) + payloads.append( + ApiBundlePayload( + f"API/umami/provider_{identifier}.h", + provider_header(data).encode(), + "api-source", + "text/x-chdr", + ) + ) + if len(documents) != 1: + payloads.append( + _json_payload( + "API/umami/metadata.json", + { + "kind": "pyamplicol-umami-provider-index", + "schema_version": 1, + "providers": entries, + }, + ) + ) + make = "UMAMI_PROVIDERS := " + " ".join(p["id"] for p in entries) + "\n" + make += f"UMAMI_SINGLE_PROVIDER := {int(len(documents) == 1)}\n" + payloads.append( + ApiBundlePayload( + "API/umami/providers.mk", make.encode(), "api-build-file", "text/x-makefile" + ) + ) + return tuple(payloads) + + +__all__ = [ + "FLAVOR_BLIND_ASSUMPTION", + "UmamiProcessInput", + "build_umami_metadata", + "umami_bundle_payloads", +] diff --git a/pyamplicol/src/pyamplicol/artifacts/umami_c.py b/pyamplicol/src/pyamplicol/artifacts/umami_c.py new file mode 100644 index 0000000000..fc9f94efc6 --- /dev/null +++ b/pyamplicol/src/pyamplicol/artifacts/umami_c.py @@ -0,0 +1,173 @@ +# SPDX-License-Identifier: 0BSD +"""Small C data emitter shared by generated UMAMI providers.""" + +from __future__ import annotations + +import json +from collections.abc import Mapping +from typing import Any + + +def _string(value: object) -> str: + return "NULL" if value is None else json.dumps(str(value), ensure_ascii=True) + + +def _number(value: object) -> str: + return format(float(value), ".17g") + + +def _boolean(value: object) -> str: + return "true" if value else "false" + + +def provider_header(data: Mapping[str, Any]) -> str: + """Render the private ABI defined by ``umami_provider.h``.""" + provider = data["provider"] + lines = [ + "/* SPDX-License-Identifier: 0BSD */", + "/* Generated from the same records as metadata.json. */", + '#include "umami_provider.h"', + f"#define UMAMI_HAS_ALPHA_S {int(provider['supports_alpha_s'])}", + f"#define UMAMI_HAS_COLOR_FLOW {int(provider['color_accuracy'] == 'lc')}", + f"#define UMAMI_ARTIFACT_ID {_string(provider.get('artifact_id'))}", + "static const double umami_masses[] = {" + + ", ".join(_number(v) for v in provider["masses"]) + + "};", + ] + process_rows = [] + for index, process in enumerate(data["runtime_processes"]): + prefix = f"umami_p{index}" + lines.append( + f"static const int {prefix}_pdgs[] = {{" + + ", ".join(str(v) for v in process["pdgs"]) + + "};" + ) + helicities = process["helicity_ids"] + parameters = process["parameters"] + if helicities: + lines.append( + f"static const char *const {prefix}_helicities[] = {{" + + ", ".join(_string(v) for v in helicities) + + "};" + ) + if parameters: + lines.append(f"static const UmamiParameter {prefix}_parameters[] = {{") + for p in parameters: + lines.append( + " {" + + ", ".join( + ( + _string(p["name"]), + _number(p["default_real"]), + _number(p.get("default_imaginary", 0.0)), + _boolean(p["mutable"]), + _boolean( + p.get( + "is_complex", p.get("default_imaginary", 0.0) != 0.0 + ) + ), + ) + ) + + "}," + ) + lines.append("};") + process_rows.append( + " {" + + ", ".join( + ( + _string(process["id"]), + f"{prefix}_pdgs", + _string(process["alpha_s_parameter"]), + _string(process["color_accuracy"]), + str(len(helicities)), + f"{prefix}_helicities" if helicities else "NULL", + str(len(parameters)), + f"{prefix}_parameters" if parameters else "NULL", + ) + ) + + "}," + ) + lines.extend( + ["static const UmamiProcess umami_processes[] = {", *process_rows, "};"] + ) + channel_rows = [] + for channel_index, channel in enumerate(data["channels"]): + flavour_rows = [] + for flavour_index, flavour in enumerate(channel["processes"]): + prefix = f"umami_c{channel_index}_f{flavour_index}" + runtime = flavour["runtime"] + lines.append( + f"static const size_t {prefix}_permutation[] = {{" + + ", ".join(str(v) for v in runtime["permutation"]) + + "};" + ) + exported = runtime["helicity_export"] + to_runtime = [-1] * len(data["helicities"]) + for runtime_index, export_index in enumerate(exported): + to_runtime[export_index] = runtime_index + lines.append( + f"static const int {prefix}_helicity_runtime[] = {{" + + ", ".join(str(v) for v in to_runtime or [-1]) + + "};" + ) + lines.append( + f"static const size_t {prefix}_helicity_export[] = {{" + + ", ".join(str(v) for v in exported or [0]) + + "};" + ) + lines.append( + f"static const UmamiContribution {prefix}_contributions[] = {{" + ) + lines.append( + " {" + + ", ".join( + ( + str(runtime["process_index"]), + _string(runtime["color_id"]), + str(runtime["color_index"]), + f"{prefix}_permutation", + _number(runtime["factor"]), + f"{prefix}_helicity_runtime", + f"{prefix}_helicity_export", + ) + ) + + "}," + ) + lines.append("};") + flavour_rows.append(f" {{1, {prefix}_contributions}},") + lines.extend( + [ + f"static const UmamiFlavour umami_c{channel_index}_flavours[] = {{", + *flavour_rows, + "};", + ] + ) + channel_rows.append( + f" {{{len(flavour_rows)}, umami_c{channel_index}_flavours}}," + ) + lines.extend( + ["static const UmamiChannel umami_channels[] = {", *channel_rows, "};"] + ) + lines.extend( + [ + "static const UmamiProvider umami_provider = {", + " " + + ", ".join( + ( + _string(provider["id"]), + str(provider["particle_count"]), + str(provider["incoming_count"]), + str(provider["helicity_count"]), + str(provider["color_count"]), + str(len(data["helicities"])), + ) + ) + + ",", + f" umami_masses, {len(process_rows)}, umami_processes,", + f" {len(channel_rows)}, umami_channels, " + + _boolean(provider["supports_alpha_s"]), + "};", + "", + ] + ) + return "\n".join(lines) diff --git a/pyamplicol/src/pyamplicol/artifacts/umami_otf.py b/pyamplicol/src/pyamplicol/artifacts/umami_otf.py new file mode 100644 index 0000000000..3282601e3a --- /dev/null +++ b/pyamplicol/src/pyamplicol/artifacts/umami_otf.py @@ -0,0 +1,191 @@ +# SPDX-License-Identifier: 0BSD +"""Explicit SDK selector tables from a compact on-the-fly source seed. + +This does not construct recurrence schedules or discover amplitude relations. +The expanded tables are SDK payloads only; the runtime seed remains compact. +""" + +from __future__ import annotations + +from collections.abc import Mapping, Sequence +from dataclasses import replace +from itertools import product +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from ..generation.on_the_fly_seed import OnTheFlyProcessSeedProjectionV1 + from ..processes.ir import CanonicalProcessIR + from .umami import UmamiProcessInput + + +def build_umami_on_the_fly_physics( + public_metadata: Mapping[str, Any], + process: CanonicalProcessIR, + seed: OnTheFlyProcessSeedProjectionV1, +) -> tuple[dict[str, Any], tuple[Mapping[str, Any], ...]]: + """Enumerate available axes, conservatively without additional proofs.""" + from ..color import build_color_plan + + sources = {source.public_label: source for source in seed.external_sources} + # Match the native selector's seed-domain order exactly. Its mixed-radix + # enumeration, like product(), varies the last public leg fastest. + domains = [ + tuple(state.public_helicity for state in sources[leg.label].source_states) + for leg in process.legs + ] + helicities = [] + for index, values in enumerate(product(*domains)): + identifier = "h:" + ",".join(f"{value:+d}" for value in values) + helicities.append( + { + "id": identifier, + "index": index, + "values": list(values), + "computed": True, + "representative_id": identifier, + "coefficient": 1.0, + "structural_zero": False, + } + ) + sectors: tuple[Mapping[str, Any], ...] = () + if process.color_accuracy == "lc": + plan = build_color_plan(process, color_accuracy="lc") + sectors = tuple(sector.to_json_dict() for sector in plan.sectors) + colors = [] + for index, sector in enumerate(plan.sectors): + word = tuple(sector.word_labels or sector.color_words[0]) + identifier = "flow:" + ( + ",".join(str(label) for label in word) if word else "singlet" + ) + colors.append( + { + "kind": "lc-flow", + "id": identifier, + "index": index, + "word": list(word), + "computed": True, + "representative_id": identifier, + "coefficient": 1.0, + } + ) + else: + colors = [{"kind": "contracted-color", "id": "color:contracted", "index": 0}] + return ( + { + **public_metadata, + "kind": "pyamplicol-resolved-physics", + "helicities": helicities, + "color_components": colors, + "coverage": { + "helicities": "complete", + "color": "complete" if process.color_accuracy == "lc" else "contracted", + }, + "extensions": {}, + }, + sectors, + ) + + +def restore_umami_on_the_fly_input( + process: UmamiProcessInput, documents: Sequence[Mapping[str, Any]] +) -> UmamiProcessInput: + """Recover an old compact process's explicit SDK axes during append. + + The SDK retains every physical selector and its colour tags in its member + records, even when integration orbits have been compressed. Recover those + records, not just the exported representatives or their multiplicities. + Another provider may also load this runtime for flavour-blind members only. + """ + matches = [ + (document, runtime) + for document in documents + for runtime in document["runtime_processes"] + if runtime["id"] == process.process_id + and any( + member["process_id"] == process.process_id + for channel in document["channels"] + for entry in channel["processes"] + for member in entry["members"] + ) + ] + if len(matches) != 1: + raise ValueError("UMAMI append requires the previous OTF process metadata") + document, runtime = matches[0] + if ( + tuple(runtime["pdgs"]) != process.external_pdgs + or runtime["color_accuracy"] != process.color_accuracy + ): + raise ValueError("previous UMAMI OTF process identity disagrees with append") + helicity_values = { + "h:" + ",".join(f"{value:+d}" for value in values): list(values) + for values in document["helicities"] + } + helicities = [ + { + "id": identifier, + "index": index, + "values": helicity_values[identifier], + "computed": True, + "representative_id": identifier, + "coefficient": 1.0, + "structural_zero": False, + } + for index, identifier in enumerate(runtime["helicity_ids"]) + ] + colors = [] + flows = {} + for channel in document["channels"]: + for entry in channel["processes"]: + for member in entry["members"]: + if member["process_id"] != process.process_id: + continue + identifier = member["color_id"] + if process.color_accuracy != "lc": + colors.append( + { + "kind": "contracted-color", + "id": "color:contracted", + "index": 0, + } + ) + continue + if not isinstance(identifier, str) or not identifier.startswith( + "flow:" + ): + raise ValueError("previous UMAMI OTF colour selector is invalid") + suffix = identifier.removeprefix("flow:") + word = ( + [] if suffix == "singlet" else [int(v) for v in suffix.split(",")] + ) + tags = document["color_flows"][member["color_flows"]] + if len(tags) != len(process.external_pdgs) or any( + len(pair) != 2 or any(not isinstance(v, int) or v < 0 for v in pair) + for pair in tags + ): + raise ValueError("previous UMAMI OTF colour tags are invalid") + flows[identifier] = tags + colors.append( + { + "kind": "lc-flow", + "id": identifier, + "index": len(colors), + "word": word, + "computed": True, + "representative_id": identifier, + "coefficient": 1.0, + } + ) + if not helicities or not colors or len({c["id"] for c in colors}) != len(colors): + raise ValueError("previous UMAMI OTF selectors are empty or repeated") + return replace( + process, + physics={ + **process.physics, + "kind": "pyamplicol-resolved-physics", + "helicities": helicities, + "color_components": colors, + "coverage": dict(runtime["coverage"]), + "extensions": {}, + }, + color_flows=flows, + ) diff --git a/pyamplicol/src/pyamplicol/artifacts/umami_reweight.py b/pyamplicol/src/pyamplicol/artifacts/umami_reweight.py new file mode 100644 index 0000000000..9855832618 --- /dev/null +++ b/pyamplicol/src/pyamplicol/artifacts/umami_reweight.py @@ -0,0 +1,235 @@ +# SPDX-License-Identifier: 0BSD +"""UMAMI providers of colour-accuracy ratios between two artifacts. + +The ratio is the target matrix element (e.g. full colour) over the leading-colour +reference summed over all colour flows, both at the helicity of the event. It +reweights events of a leading-colour UMAMI provider generated from the same +processes, with the channel, flavour and helicity indices of that provider. +""" + +from __future__ import annotations + +import importlib.resources +import json +import os +import re +from collections.abc import Mapping, Sequence +from pathlib import Path +from typing import Any + +from .umami_c import _boolean, _string + +_TEMPLATES = { + "umami_reweight.c": "umami_reweight.c", + "reweight_provider.h": "reweight_provider.h", + "umami.h": "umami.h", + "umami.exports": "umami.exports", + "umami.exports.macos": "umami.exports.macos", + "UMAMI_LICENSE": "UMAMI_LICENSE", + "Makefile.reweight": "Makefile", +} + + +_IDENTIFIER = re.compile(r"[A-Za-z0-9_]+") + + +def _identifier(value: object) -> str: + # Provider ids end up in file names and in the makefile. + if not isinstance(value, str) or not _IDENTIFIER.fullmatch(value): + raise ValueError(f"invalid UMAMI provider id {value!r}") + return value + + +def _integer(value: object) -> int: + # Metadata values are written into generated C sources. + if isinstance(value, bool) or not isinstance(value, int): + raise ValueError(f"expected an integer in the UMAMI metadata, got {value!r}") + return value + + +def load_umami_metadata(artifact: Path) -> list[dict[str, Any]]: + """The metadata documents of all UMAMI providers of an artifact.""" + root = (Path(artifact) / "API" / "umami").resolve() + index = json.loads((root / "metadata.json").read_text(encoding="utf-8")) + if index.get("kind") != "pyamplicol-umami-provider-index": + return [index] + documents = [] + for entry in index["providers"]: + path = (root / entry["metadata"]).resolve() + if not path.is_relative_to(root): + raise ValueError(f"UMAMI metadata path {entry['metadata']!r} leaves {root}") + documents.append(json.loads(path.read_text(encoding="utf-8"))) + return documents + + +def _process_row(process: Mapping[str, Any], prefix: str, lines: list[str]) -> str: + lines.append( + f"static const int {prefix}_pdgs[] = {{" + + ", ".join(str(_integer(v)) for v in process["pdgs"]) + + "};" + ) + return ( + " {" + + ", ".join( + ( + _string(process["id"]), + f"{prefix}_pdgs", + _string(process["alpha_s_parameter"]), + str(len(process["helicity_ids"])), + ) + ) + + "}," + ) + + +def _int_array(name: str, ctype: str, values: Sequence[int]) -> str: + return ( + f"static const {ctype} {name}[] = {{" + + ", ".join(str(_integer(v)) for v in values or [-1]) + + "};" + ) + + +def reweight_header( + reference: Mapping[str, Any], + targets: Sequence[Mapping[str, Any]], + reference_path: str, + target_path: str, +) -> str: + """Render the tables of one reweight provider, see ``reweight_provider.h``.""" + target_processes: list[Mapping[str, Any]] = [] + target_ids: list[str | None] = [] + target_lookup = {} + for document in targets: + for process in document["runtime_processes"]: + target_lookup.setdefault( + tuple(process["pdgs"]), (process, document["provider"]["artifact_id"]) + ) + reference_processes = reference["runtime_processes"] + lines = [ + "/* SPDX-License-Identifier: 0BSD */", + "/* Generated from the UMAMI metadata of the two artifacts. */", + '#include "reweight_provider.h"', + ] + rows = [ + _process_row(process, f"reweight_r{i}", lines) + for i, process in enumerate(reference_processes) + ] + lines.extend(["static const ReweightProcess reweight_references[] = {", *rows, "};"]) + + exported_count = len(reference["helicities"]) + channel_rows = [] + for c, channel in enumerate(reference["channels"]): + flavour_rows = [] + for f, entry in enumerate(channel["processes"]): + runtime = entry["runtime"] + if "helicity_export" not in runtime: + raise ValueError("the reference UMAMI metadata has no helicity export") + process_index = _integer(runtime["process_index"]) + process = reference_processes[process_index] + match = target_lookup.get(tuple(process["pdgs"])) + if match is None: + raise ValueError( + f"no process of the target artifact matches {process['id']!r}" + ) + target, target_id = match + if target not in target_processes: + target_processes.append(target) + target_ids.append(target_id) + target_index = target_processes.index(target) + reference_helicity = [-1] * exported_count + target_helicity = [-1] * exported_count + for runtime_index, exported in enumerate(runtime["helicity_export"]): + reference_helicity[exported] = runtime_index + identifier = process["helicity_ids"][runtime_index] + if identifier in target["helicity_ids"]: + target_helicity[exported] = target["helicity_ids"].index(identifier) + prefix = f"reweight_c{c}_f{f}" + lines.append( + _int_array(f"{prefix}_permutation", "size_t", runtime["permutation"]) + ) + lines.append(_int_array(f"{prefix}_reference", "int", reference_helicity)) + lines.append(_int_array(f"{prefix}_target", "int", target_helicity)) + flavour_rows.append( + f" {{{process_index}, {target_index}, " + f"{prefix}_permutation, {prefix}_reference, {prefix}_target}}," + ) + lines.extend( + [ + f"static const ReweightFlavour reweight_c{c}_flavours[] = {{", + *flavour_rows, + "};", + ] + ) + channel_rows.append(f" {{{len(flavour_rows)}, reweight_c{c}_flavours}},") + if len(set(target_ids)) > 1: + raise ValueError("the target processes come from different artifacts") + rows = [ + _process_row(process, f"reweight_t{i}", lines) + for i, process in enumerate(target_processes) + ] + lines.extend(["static const ReweightProcess reweight_targets[] = {", *rows, "};"]) + lines.extend( + ["static const ReweightChannel reweight_channels[] = {", *channel_rows, "};"] + ) + supports_alpha_s = all( + p["alpha_s_parameter"] is not None + for p in [*reference_processes, *target_processes] + ) + lines.extend( + [ + f"#define UMAMI_HAS_ALPHA_S {int(supports_alpha_s)}", + "static const ReweightProvider reweight_provider = {", + f" {_string(reference['provider']['id'])}, " + f"{_integer(reference['provider']['particle_count'])}, {exported_count},", + f" {_string(reference_path)}, {_string(target_path)},", + f" {_string(reference['provider'].get('artifact_id'))}, " + f"{_string(target_ids[0] if target_ids else None)},", + f" {len(reference_processes)}, reweight_references,", + f" {len(target_processes)}, reweight_targets,", + f" {len(channel_rows)}, reweight_channels, {_boolean(supports_alpha_s)}", + "};", + "", + ] + ) + return "\n".join(lines) + + +def write_reweight_provider( + reference_artifact: str | os.PathLike[str], + target_artifact: str | os.PathLike[str], + output: str | os.PathLike[str], +) -> list[str]: + """Write the sources of the reweight providers to ``output``. + + One provider is written per UMAMI provider of the leading-colour reference + artifact; ``make -C output`` builds ``build/libreweight_.so``. The + libraries are initialized with the absolute path of ``output``, the artifacts + are found relative to it. Returns the provider ids. + """ + output = Path(output) + output.mkdir(parents=True, exist_ok=True) + references = load_umami_metadata(Path(reference_artifact)) + targets = load_umami_metadata(Path(target_artifact)) + if any(d["provider"]["color_accuracy"] != "lc" for d in references): + raise ValueError("the reference artifact must have leading-colour accuracy") + reference_path = os.path.relpath(Path(reference_artifact).resolve(), output.resolve()) + target_path = os.path.relpath(Path(target_artifact).resolve(), output.resolve()) + templates = importlib.resources.files("pyamplicol.assets.api_templates") / "umami" + for source, destination in _TEMPLATES.items(): + (output / destination).write_bytes((templates / source).read_bytes()) + ids = [] + for reference in references: + identifier = _identifier(reference["provider"]["id"]) + (output / f"reweight_{identifier}.h").write_text( + reweight_header(reference, targets, reference_path, target_path), + encoding="utf-8", + ) + ids.append(identifier) + (output / "providers.mk").write_text( + "UMAMI_PROVIDERS := " + " ".join(ids) + "\n", encoding="utf-8" + ) + return ids + + +__all__ = ["load_umami_metadata", "reweight_header", "write_reweight_provider"] diff --git a/pyamplicol/src/pyamplicol/artifacts/writer.py b/pyamplicol/src/pyamplicol/artifacts/writer.py new file mode 100644 index 0000000000..e7dc464711 --- /dev/null +++ b/pyamplicol/src/pyamplicol/artifacts/writer.py @@ -0,0 +1,387 @@ +# SPDX-License-Identifier: 0BSD +"""High-level schema-v3 artifact builder.""" + +from __future__ import annotations + +import io +import json +import os +import tempfile +from collections.abc import Mapping, Sequence +from contextlib import suppress +from datetime import UTC, datetime +from pathlib import Path +from typing import BinaryIO, Literal + +from .manifest import ( + ARTIFACT_IDENTITY_EXTENSION, + MANIFEST_NAME, + PayloadRecord, + artifact_identity_extension, + canonical_manifest_bytes, + compute_artifact_id, + load_manifest, +) +from .security import ( + confined_path, + executable_bit, + fsync_file, + normalize_relative_path, + sha256_file, +) +from .transaction import ArtifactTransaction, ArtifactWriteMode + + +class ArtifactBuilder: + """Build one artifact in a private staging directory.""" + + def __init__( + self, + destination: str | Path, + *, + mode: ArtifactWriteMode = "error", + expected_artifact_id: str | None = None, + ) -> None: + if expected_artifact_id is not None and mode != "append": + raise ValueError("expected_artifact_id is valid only for append mode") + self._transaction = ArtifactTransaction(destination, mode=mode) + self._expected_artifact_id = expected_artifact_id + self.root: Path | None = None + self._payloads: dict[str, PayloadRecord] = {} + + def __enter__(self) -> ArtifactBuilder: + self.root = self._transaction.__enter__() + try: + if self._transaction.mode == "append": + existing = self.root / MANIFEST_NAME + if existing.is_file(): + manifest = load_manifest(self.root) + if ( + self._expected_artifact_id is not None + and manifest.artifact_id != self._expected_artifact_id + ): + raise ValueError( + "append artifact changed before the transaction lock was " + "acquired; retry the append" + ) + self._payloads.update( + (record.path, record) for record in manifest.payloads + ) + existing.unlink() + return self + except BaseException as error: + self._transaction.__exit__(type(error), error, error.__traceback__) + self.root = None + raise + + def _root(self) -> Path: + if self.root is None: + raise RuntimeError("artifact builder is not active") + return self.root + + def staged_path(self, relative: str, *, create_parent: bool = False) -> Path: + """Return a confined path in the private staging directory. + + This is intended for producers that already provide their own atomic, + streaming writer. Call :meth:`register_staged_file` after the file is + complete and independently validated. + """ + + path_value = normalize_relative_path(relative) + if path_value == MANIFEST_NAME: + raise ValueError(f"{MANIFEST_NAME} is reserved for the artifact manifest") + path = confined_path(self._root(), path_value, must_exist=False) + if create_parent: + path.parent.mkdir(parents=True, exist_ok=True) + return path + + def register_staged_file( + self, + relative: str, + *, + role: str, + media_type: str, + executable: bool | None = None, + target: Mapping[str, object] | None = None, + process_id: str | None = None, + ) -> PayloadRecord: + """Register a complete regular file already present in staging.""" + + path_value = normalize_relative_path(relative) + path = self.staged_path(path_value) + if path.is_symlink() or not path.is_file(): + raise ValueError(f"staged artifact payload must be a regular file: {path}") + if executable is not None: + path.chmod(0o755 if executable else 0o644) + fsync_file(path) + record = PayloadRecord( + path=path_value, + role=role, + media_type=media_type, + size_bytes=path.stat().st_size, + sha256=sha256_file(path), + executable=executable_bit(path), + target=dict(target) if target is not None else None, + process_id=process_id, + ) + self._payloads[path_value] = record + return record + + def add_bytes( + self, + relative: str, + content: bytes, + *, + role: str, + media_type: str, + executable: bool = False, + target: Mapping[str, object] | None = None, + process_id: str | None = None, + ) -> PayloadRecord: + return self.add_stream( + relative, + io.BytesIO(content), + role=role, + media_type=media_type, + executable=executable, + target=target, + process_id=process_id, + ) + + def add_stream( + self, + relative: str, + source: BinaryIO, + *, + role: str, + media_type: str, + executable: bool = False, + target: Mapping[str, object] | None = None, + process_id: str | None = None, + chunk_size: int = 1024 * 1024, + ) -> PayloadRecord: + """Copy a binary stream into staging with bounded memory.""" + + if ( + not isinstance(chunk_size, int) + or isinstance(chunk_size, bool) + or chunk_size <= 0 + ): + raise ValueError("artifact stream chunk size must be a positive integer") + read = getattr(source, "read", None) + if not callable(read): + raise TypeError("artifact stream source must provide read()") + path_value = normalize_relative_path(relative) + path = self.staged_path(path_value, create_parent=True) + descriptor, temporary_name = tempfile.mkstemp( + prefix=f".{path.name}.", suffix=".tmp", dir=path.parent + ) + temporary = Path(temporary_name) + descriptor_open = True + try: + with os.fdopen(descriptor, "wb") as stream: + descriptor_open = False + while True: + chunk = source.read(chunk_size) + if not isinstance(chunk, bytes | bytearray | memoryview): + raise TypeError("artifact stream source must return bytes") + if not chunk: + break + if len(chunk) > chunk_size: + raise ValueError( + "artifact stream source returned more bytes than requested" + ) + view = memoryview(chunk) + written = 0 + while written < len(view): + count = stream.write(view[written:]) + if not isinstance(count, int) or count <= 0: + raise OSError("short write while staging artifact payload") + written += count + stream.flush() + os.fsync(stream.fileno()) + temporary.chmod(0o755 if executable else 0o644) + os.replace(temporary, path) + finally: + if descriptor_open: + os.close(descriptor) + with suppress(FileNotFoundError): + temporary.unlink() + return self.register_staged_file( + path_value, + role=role, + media_type=media_type, + executable=executable, + target=target, + process_id=process_id, + ) + + def add_json( + self, + relative: str, + value: object, + *, + role: str, + process_id: str | None = None, + compact: bool = False, + ) -> PayloadRecord: + content = ( + json.dumps( + value, + indent=None if compact else 2, + separators=(",", ":") if compact else None, + sort_keys=True, + allow_nan=False, + ) + + "\n" + ).encode("utf-8") + return self.add_bytes( + relative, + content, + role=role, + media_type="application/json", + process_id=process_id, + ) + + def add_file( + self, + relative: str, + source: str | Path, + *, + role: str, + media_type: str, + executable: bool | None = None, + target: Mapping[str, object] | None = None, + process_id: str | None = None, + ) -> PayloadRecord: + requested_source = Path(source).expanduser() + if requested_source.is_symlink(): + raise ValueError( + f"artifact source must be a regular file: {requested_source}" + ) + source_path = requested_source.resolve(strict=True) + if not source_path.is_file(): + raise ValueError(f"artifact source must be a regular file: {source_path}") + with source_path.open("rb") as stream: + return self.add_stream( + relative, + stream, + role=role, + media_type=media_type, + executable=( + executable_bit(source_path) if executable is None else executable + ), + target=target, + process_id=process_id, + ) + + def discard_payloads(self, relative: str, *, recursive: bool = False) -> None: + """Remove an owned payload or payload subtree from the staging snapshot.""" + + path_value = normalize_relative_path(relative) + if path_value == MANIFEST_NAME: + raise ValueError(f"{MANIFEST_NAME} is reserved for the artifact manifest") + prefix = path_value.rstrip("/") + "/" + owned = tuple( + path + for path in self._payloads + if path == path_value or (recursive and path.startswith(prefix)) + ) + for owned_path in owned: + path = confined_path(self._root(), owned_path, must_exist=False) + if path.is_symlink() or path.is_dir(): + raise ValueError( + f"artifact payload must be a regular file: {owned_path}" + ) + if path.exists(): + path.unlink() + self._payloads.pop(owned_path, None) + if recursive: + root = confined_path(self._root(), path_value, must_exist=False) + if root.is_symlink(): + raise ValueError( + f"artifact payload path must not be a symlink: {path_value}" + ) + if root.is_dir(): + directories = sorted( + (path for path in root.rglob("*") if path.is_dir()), + key=lambda path: len(path.parts), + reverse=True, + ) + for directory in (*directories, root): + with suppress(OSError): + directory.rmdir() + + def payload_records(self) -> tuple[PayloadRecord, ...]: + """Return the immutable, path-ordered staged payload inventory. + + Derived runtime caches use this snapshot to bind their compact native + representation to the ordinary artifact payload declarations. The + cache is added only after the snapshot is encoded, so it cannot create + a self-hash cycle; normal finalization still includes the cache record + in the authoritative runtime artifact identity. + """ + + self._root() + return tuple(sorted(self._payloads.values(), key=lambda item: item.path)) + + def finalize( + self, + *, + kind: Literal["pyamplicol-process", "pyamplicol-process-set"], + producer: Mapping[str, object], + model: Mapping[str, object], + configuration: Mapping[str, object], + processes: Sequence[Mapping[str, object]], + runtime: Mapping[str, object], + dependencies: Sequence[Mapping[str, object]] = (), + default_process_id: str | None = None, + extensions: Mapping[str, object] | None = None, + ) -> Path: + if not self._payloads: + raise ValueError("an artifact must contain at least one payload") + manifest_extensions = dict(extensions or {}) + supplied_identity = manifest_extensions.get(ARTIFACT_IDENTITY_EXTENSION) + expected_identity = artifact_identity_extension() + if supplied_identity is not None and supplied_identity != expected_identity: + raise ValueError( + "extensions.artifact_identity is compiler-owned and must use the " + "current runtime-payload identity contract" + ) + manifest_extensions[ARTIFACT_IDENTITY_EXTENSION] = expected_identity + manifest: dict[str, object] = { + "schema_version": 3, + "kind": kind, + "artifact_id": "0" * 64, + "created_utc": datetime.now(UTC).isoformat().replace("+00:00", "Z"), + "producer": dict(producer), + "model": dict(model), + "configuration": dict(configuration), + "processes": [dict(process) for process in processes], + "runtime": dict(runtime), + "payloads": [ + record.as_dict() + for record in sorted( + self._payloads.values(), + key=lambda item: item.path, + ) + ], + "dependencies": [dict(dependency) for dependency in dependencies], + "extensions": manifest_extensions, + } + if default_process_id is not None: + manifest["default_process_id"] = default_process_id + manifest["artifact_id"] = compute_artifact_id(manifest) + path = self._root() / MANIFEST_NAME + path.write_bytes(canonical_manifest_bytes(manifest)) + path.chmod(0o644) + fsync_file(path) + return path + + def __exit__(self, *exception: object) -> bool: + if exception[0] is None and not (self._root() / MANIFEST_NAME).is_file(): + raise RuntimeError("artifact builder exited without finalize()") + return self._transaction.__exit__(*exception) # type: ignore[arg-type] + + +__all__ = ["ArtifactBuilder"] diff --git a/pyamplicol/src/pyamplicol/assets/__init__.py b/pyamplicol/src/pyamplicol/assets/__init__.py new file mode 100644 index 0000000000..be31ea44b9 --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/__init__.py @@ -0,0 +1,2 @@ +# SPDX-License-Identifier: 0BSD +"""Wheel-owned models and generated-output templates.""" diff --git a/pyamplicol/src/pyamplicol/assets/api_templates/__init__.py b/pyamplicol/src/pyamplicol/assets/api_templates/__init__.py new file mode 100644 index 0000000000..4267ca4dfb --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/api_templates/__init__.py @@ -0,0 +1,2 @@ +# SPDX-License-Identifier: 0BSD +"""Source templates copied into generated process artifacts.""" diff --git a/pyamplicol/src/pyamplicol/assets/api_templates/c/Makefile b/pyamplicol/src/pyamplicol/assets/api_templates/c/Makefile new file mode 100644 index 0000000000..4c0d18953f --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/api_templates/c/Makefile @@ -0,0 +1,42 @@ +# SPDX-License-Identifier: 0BSD +RUSTICOL_CONFIG ?= rusticol-config +RUSTICOL_CONFIG_PATH := $(shell \ + if command -v "$(RUSTICOL_CONFIG)" >/dev/null 2>&1; then \ + command -v "$(RUSTICOL_CONFIG)"; \ + elif [ "$(RUSTICOL_CONFIG)" = rusticol-config ]; then \ + directory="$(CURDIR)"; \ + while [ "$$directory" != / ]; do \ + candidate="$$directory/.venv/bin/rusticol-config"; \ + if [ -x "$$candidate" ]; then printf '%s\n' "$$candidate"; break; fi; \ + directory=$$(dirname "$$directory"); \ + done; \ + fi) +ifeq ($(strip $(RUSTICOL_CONFIG_PATH)),) +$(error rusticol-config is unavailable; activate the pyAmpliCol environment or set RUSTICOL_CONFIG=/absolute/path) +endif +CC ?= cc +ARTIFACT_DIR := $(abspath ../..) +BUILD_DIR ?= $(abspath ../../..)/.pyamplicol-api-build/$(notdir $(ARTIFACT_DIR))/c +TARGET := $(BUILD_DIR)/check_standalone +CPPFLAGS += $(shell "$(RUSTICOL_CONFIG_PATH)" --cflags) +CFLAGS ?= -O2 +CFLAGS += -std=c11 -Wall -Wextra -pedantic +LDLIBS += $(shell "$(RUSTICOL_CONFIG_PATH)" --libs) + +.PHONY: all check_standalone clean run + +all: $(TARGET) + +check_standalone: $(TARGET) + +$(BUILD_DIR): + mkdir -p "$@" + +$(TARGET): check_standalone.c | $(BUILD_DIR) + $(CC) $(CPPFLAGS) $(CFLAGS) "$<" -o "$@" $(LDLIBS) + +run: $(TARGET) + cd "$(ARTIFACT_DIR)" && "$(TARGET)" $(ARGS) + +clean: + $(RM) "$(TARGET)" diff --git a/pyamplicol/src/pyamplicol/assets/api_templates/c/check_standalone.c b/pyamplicol/src/pyamplicol/assets/api_templates/c/check_standalone.c new file mode 100644 index 0000000000..defccd3210 --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/api_templates/c/check_standalone.c @@ -0,0 +1,885 @@ +/* SPDX-License-Identifier: 0BSD */ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define MAX_OVERRIDES 128 + +typedef struct { + const char *process; + const char *model_parameters; + const char *kinematics; + const char *parameter_names[MAX_OVERRIDES]; + double parameter_real[MAX_OVERRIDES]; + double parameter_imaginary[MAX_OVERRIDES]; + size_t parameter_count; + int precision; + bool json; +} Options; + +typedef struct { + char *id; + int32_t *values; + size_t value_count; +} HelicityMetadata; + +typedef struct { + char *id; + char *kind; + size_t *word; + size_t word_count; +} ColorMetadata; + +typedef int (*RuntimeStringGetter)( + const RusticolRuntimeHandle *, char *, size_t, size_t * +); +typedef int (*IndexedStringGetter)( + const RusticolRuntimeHandle *, size_t, char *, size_t, size_t * +); + +static void fail(const char *message) { + fprintf(stderr, "check_standalone: %s\n", message); + exit(EXIT_FAILURE); +} + +static void fail_errno(const char *operation) { + fprintf(stderr, "check_standalone: %s: %s\n", operation, strerror(errno)); + exit(EXIT_FAILURE); +} + +static void *allocate(size_t count, size_t size) { + void *output; + if (count != 0 && size > SIZE_MAX / count) { + fail("allocation size overflow"); + } + output = calloc(count == 0 ? 1 : count, size == 0 ? 1 : size); + if (output == NULL) { + fail("out of memory"); + } + return output; +} + +static void *resize(void *pointer, size_t size) { + void *output = realloc(pointer, size == 0 ? 1 : size); + if (output == NULL) { + free(pointer); + fail("out of memory"); + } + return output; +} + +static void check_rusticol(int status, const char *operation) { + size_t required = 0; + char *message = NULL; + if (status == RUSTICOL_STATUS_OK) { + return; + } + if (rusticol_last_error_message(NULL, 0, &required) == RUSTICOL_STATUS_OK && + required > 0) { + message = allocate(required, sizeof(*message)); + if (rusticol_last_error_message(message, required, &required) != + RUSTICOL_STATUS_OK) { + free(message); + message = NULL; + } + } + fprintf( + stderr, + "check_standalone: %s failed (status %d)%s%s\n", + operation, + status, + message == NULL ? "" : ": ", + message == NULL ? "" : message + ); + free(message); + exit(EXIT_FAILURE); +} + +static double parse_double(const char *text, const char *description) { + char *end = NULL; + double value; + errno = 0; + value = strtod(text, &end); + if (text == end || end == NULL || *end != '\0' || errno == ERANGE || + !isfinite(value)) { + fprintf(stderr, "check_standalone: invalid %s: %s\n", description, text); + exit(EXIT_FAILURE); + } + return value; +} + +static size_t parse_size(const char *text, const char *description) { + char *end = NULL; + unsigned long long value; + errno = 0; + value = strtoull(text, &end, 10); + if (text == end || end == NULL || *end != '\0' || errno == ERANGE || + value > SIZE_MAX) { + fprintf(stderr, "check_standalone: invalid %s: %s\n", description, text); + exit(EXIT_FAILURE); + } + return (size_t)value; +} + +static Options parse_options(int argc, char **argv) { + Options options = {0}; + int index; + options.precision = 16; + for (index = 1; index < argc; ++index) { + const char *argument = argv[index]; + if (strcmp(argument, "--") == 0) { + continue; + } + if (strcmp(argument, "--process") == 0) { + if (++index >= argc) { + fail("missing value after --process"); + } + options.process = argv[index]; + } else if (strcmp(argument, "--model-parameters") == 0) { + if (++index >= argc) { + fail("missing value after --model-parameters"); + } + options.model_parameters = argv[index]; + } else if (strcmp(argument, "--kinematics") == 0) { + if (++index >= argc) { + fail("missing value after --kinematics"); + } + options.kinematics = argv[index]; + } else if (strcmp(argument, "--set-parameter") == 0) { + size_t slot = options.parameter_count; + if (index + 3 >= argc) { + fail("--set-parameter requires NAME REAL IMAG"); + } + if (slot >= MAX_OVERRIDES) { + fail("too many --set-parameter options"); + } + options.parameter_names[slot] = argv[++index]; + options.parameter_real[slot] = + parse_double(argv[++index], "real model-parameter component"); + options.parameter_imaginary[slot] = + parse_double(argv[++index], "imaginary model-parameter component"); + options.parameter_count += 1; + } else if (strcmp(argument, "--precision") == 0) { + size_t precision; + if (++index >= argc) { + fail("missing value after --precision"); + } + precision = parse_size(argv[index], "--precision value"); + if (precision > INT32_MAX) { + fail("invalid --precision value"); + } + options.precision = (int)precision; + } else if (strcmp(argument, "--json") == 0) { + options.json = true; + } else if (strcmp(argument, "--help") == 0 || strcmp(argument, "-h") == 0) { + puts( + "usage: check_standalone [--process ID|EXPRESSION] " + "[--model-parameters PATH] " + "[--kinematics PATH] " + "[--set-parameter NAME REAL IMAG] " + "[--precision 16] [--json]" + ); + exit(EXIT_SUCCESS); + } else { + fprintf(stderr, "check_standalone: unknown option: %s\n", argument); + exit(EXIT_FAILURE); + } + } + if (options.precision != 16) { + fail("the C Rusticol API supports only double precision (--precision 16)"); + } + return options; +} + +static char *runtime_string( + const RusticolRuntimeHandle *runtime, + RuntimeStringGetter getter, + const char *operation +) { + size_t required = 0; + char *output; + check_rusticol(getter(runtime, NULL, 0, &required), operation); + if (required == 0) { + fail("Rusticol returned an invalid string size"); + } + output = allocate(required, sizeof(*output)); + check_rusticol(getter(runtime, output, required, &required), operation); + return output; +} + +static char *indexed_string( + const RusticolRuntimeHandle *runtime, + size_t index, + IndexedStringGetter getter, + const char *operation +) { + size_t required = 0; + char *output; + check_rusticol(getter(runtime, index, NULL, 0, &required), operation); + if (required == 0) { + fail("Rusticol returned an invalid indexed string size"); + } + output = allocate(required, sizeof(*output)); + check_rusticol(getter(runtime, index, output, required, &required), operation); + return output; +} + +static char *read_line(FILE *stream) { + size_t capacity = 256; + size_t length = 0; + char *line = allocate(capacity, sizeof(*line)); + int character; + while ((character = fgetc(stream)) != EOF && character != '\n') { + if (length + 1 >= capacity) { + if (capacity > SIZE_MAX / 2) { + free(line); + fail("validation-point row is too large"); + } + capacity *= 2; + line = resize(line, capacity); + } + line[length++] = (char)character; + } + if (ferror(stream)) { + free(line); + fail_errno("read API/validation_points.dat"); + } + if (character == EOF && length == 0) { + free(line); + return NULL; + } + if (length > 0 && line[length - 1] == '\r') { + length -= 1; + } + line[length] = '\0'; + return line; +} + +static char *take_field(char **cursor) { + char *field; + char *separator; + if (*cursor == NULL) { + return NULL; + } + field = *cursor; + separator = strchr(field, '\t'); + if (separator == NULL) { + *cursor = NULL; + } else { + *separator = '\0'; + *cursor = separator + 1; + } + return field; +} + +static double *load_validation_point( + const char *path, + const char *process_key, + size_t external_count +) { + FILE *input = fopen(path, "r"); + char *line; + if (input == NULL) { + if (errno == ENOENT) { + return NULL; + } + fail_errno("open API/validation_points.dat"); + } + line = read_line(input); + if (line == NULL || strcmp(line, "RUSTICOL_VALIDATION_POINTS_V1") != 0) { + free(line); + fclose(input); + fail("unsupported validation_points.dat format"); + } + free(line); + + while ((line = read_line(input)) != NULL) { + char *cursor = line; + char *row_process; + char *row_count_text; + size_t row_count; + size_t momentum_count; + size_t index; + double *momenta; + if (line[0] == '\0' || line[0] == '#') { + free(line); + continue; + } + row_process = take_field(&cursor); + row_count_text = take_field(&cursor); + if (row_process == NULL || row_count_text == NULL) { + free(line); + fclose(input); + fail("invalid validation point row"); + } + if (strcmp(row_process, process_key) != 0) { + free(line); + continue; + } + row_count = parse_size(row_count_text, "validation external-particle count"); + if (row_count != external_count || external_count > SIZE_MAX / 4) { + free(line); + fclose(input); + fail("validation point has an incompatible external-particle count"); + } + momentum_count = 4 * external_count; + momenta = allocate(momentum_count, sizeof(*momenta)); + for (index = 0; index < momentum_count; ++index) { + char *component = take_field(&cursor); + if (component == NULL) { + free(momenta); + free(line); + fclose(input); + fail("validation point has too few momentum components"); + } + momenta[index] = + parse_double(component, "validation-point momentum component"); + } + if (cursor != NULL) { + free(momenta); + free(line); + fclose(input); + fail("validation point has too many momentum components"); + } + free(line); + if (fclose(input) != 0) { + free(momenta); + fail_errno("close API/validation_points.dat"); + } + return momenta; + } + if (fclose(input) != 0) { + fail_errno("close API/validation_points.dat"); + } + return NULL; +} + +static size_t *load_external_permutation( + const RusticolRuntimeHandle *runtime, + size_t expected_count +) { + size_t required = 0; + size_t *permutation; + check_rusticol( + rusticol_runtime_external_permutation(runtime, NULL, 0, &required), + "query external permutation size" + ); + if (required != expected_count) { + fail("external permutation has an incompatible size"); + } + permutation = allocate(required, sizeof(*permutation)); + check_rusticol( + rusticol_runtime_external_permutation( + runtime, permutation, required, &required + ), + "query external permutation" + ); + return permutation; +} + +static double *reorder_validation_point( + double *representative, + const size_t *permutation, + size_t external_count +) { + double *public_point = allocate(4 * external_count, sizeof(*public_point)); + bool *seen = allocate(external_count, sizeof(*seen)); + size_t representative_index; + for (representative_index = 0; representative_index < external_count; + ++representative_index) { + size_t public_index = permutation[representative_index]; + if (public_index >= external_count || seen[public_index]) { + free(seen); + free(public_point); + free(representative); + fail("runtime returned an invalid external permutation"); + } + seen[public_index] = true; + memcpy( + &public_point[4 * public_index], + &representative[4 * representative_index], + 4 * sizeof(*public_point) + ); + } + free(seen); + free(representative); + return public_point; +} + +static double *load_kinematics_json( + const RusticolRuntimeHandle *runtime, + const char *path, + size_t expected_count +) { + size_t required = 0; + double *momenta; + check_rusticol( + rusticol_runtime_load_kinematics_json( + runtime, path, NULL, 0, &required + ), + "query kinematics JSON size" + ); + if (required != 4 * expected_count) { + fail("kinematics JSON has an incompatible external-particle count"); + } + momenta = allocate(required, sizeof(*momenta)); + check_rusticol( + rusticol_runtime_load_kinematics_json( + runtime, path, momenta, required, &required + ), + "load kinematics JSON" + ); + return momenta; +} + +static int32_t *load_external_particles( + const RusticolRuntimeHandle *runtime, + size_t *count +) { + int32_t *particles; + size_t index; + check_rusticol( + rusticol_runtime_external_count(runtime, count), + "query external-particle count" + ); + particles = allocate(*count, sizeof(*particles)); + for (index = 0; index < *count; ++index) { + check_rusticol( + rusticol_runtime_external_pdg(runtime, index, &particles[index]), + "query external-particle PDG" + ); + } + return particles; +} + +static HelicityMetadata *load_helicities( + const RusticolRuntimeHandle *runtime, + size_t *count +) { + HelicityMetadata *items; + size_t index; + check_rusticol( + rusticol_runtime_helicity_count(runtime, count), + "query helicity count" + ); + items = allocate(*count, sizeof(*items)); + for (index = 0; index < *count; ++index) { + items[index].id = indexed_string( + runtime, + index, + rusticol_runtime_helicity_id, + "query helicity ID" + ); + check_rusticol( + rusticol_runtime_helicity_vector( + runtime, index, NULL, 0, &items[index].value_count + ), + "query helicity-vector size" + ); + items[index].values = + allocate(items[index].value_count, sizeof(*items[index].values)); + if (items[index].value_count > 0) { + check_rusticol( + rusticol_runtime_helicity_vector( + runtime, + index, + items[index].values, + items[index].value_count, + &items[index].value_count + ), + "query helicity vector" + ); + } + } + return items; +} + +static ColorMetadata *load_colors( + const RusticolRuntimeHandle *runtime, + size_t *count +) { + ColorMetadata *items; + size_t index; + check_rusticol(rusticol_runtime_color_count(runtime, count), "query color count"); + items = allocate(*count, sizeof(*items)); + for (index = 0; index < *count; ++index) { + items[index].id = indexed_string( + runtime, index, rusticol_runtime_color_id, "query color ID" + ); + items[index].kind = indexed_string( + runtime, index, rusticol_runtime_color_kind, "query color kind" + ); + check_rusticol( + rusticol_runtime_color_word( + runtime, index, NULL, 0, &items[index].word_count + ), + "query color-word size" + ); + items[index].word = + allocate(items[index].word_count, sizeof(*items[index].word)); + if (items[index].word_count > 0) { + check_rusticol( + rusticol_runtime_color_word( + runtime, + index, + items[index].word, + items[index].word_count, + &items[index].word_count + ), + "query color word" + ); + } + } + return items; +} + +static void write_json_string(const char *value) { + const unsigned char *cursor = (const unsigned char *)value; + putchar('"'); + while (*cursor != '\0') { + unsigned char character = *cursor++; + switch (character) { + case '"': fputs("\\\"", stdout); break; + case '\\': fputs("\\\\", stdout); break; + case '\b': fputs("\\b", stdout); break; + case '\f': fputs("\\f", stdout); break; + case '\n': fputs("\\n", stdout); break; + case '\r': fputs("\\r", stdout); break; + case '\t': fputs("\\t", stdout); break; + default: + if (character < 0x20) { + printf("\\u%04x", (unsigned int)character); + } else { + putchar((int)character); + } + } + } + putchar('"'); +} + +static void write_common_json( + const char *process, + const char *process_key, + const char *color_accuracy, + const int32_t *particles, + size_t particle_count, + const HelicityMetadata *helicities, + size_t helicity_count, + const ColorMetadata *colors, + size_t color_count +) { + size_t index; + fputs("\"process\":", stdout); + write_json_string(process); + fputs(",\"process_key\":", stdout); + write_json_string(process_key); + fputs(",\"color_accuracy\":", stdout); + write_json_string(color_accuracy); + fputs(",\"external_particles\":[", stdout); + for (index = 0; index < particle_count; ++index) { + if (index != 0) { + putchar(','); + } + printf("{\"index\":%zu,\"pdg\":%" PRId32 "}", index, particles[index]); + } + fputs("],\"helicities\":[", stdout); + for (index = 0; index < helicity_count; ++index) { + size_t component; + if (index != 0) { + putchar(','); + } + fputs("{\"id\":", stdout); + write_json_string(helicities[index].id); + fputs(",\"helicities\":[", stdout); + for (component = 0; component < helicities[index].value_count; ++component) { + if (component != 0) { + putchar(','); + } + printf("%" PRId32, helicities[index].values[component]); + } + fputs("]}", stdout); + } + fputs("],\"colors\":[", stdout); + for (index = 0; index < color_count; ++index) { + size_t component; + if (index != 0) { + putchar(','); + } + fputs("{\"id\":", stdout); + write_json_string(colors[index].id); + fputs(",\"kind\":", stdout); + write_json_string(colors[index].kind); + fputs(",\"word\":[", stdout); + for (component = 0; component < colors[index].word_count; ++component) { + if (component != 0) { + putchar(','); + } + printf("%zu", colors[index].word[component]); + } + fputs("]}", stdout); + } + putchar(']'); +} + +static void free_helicities(HelicityMetadata *items, size_t count) { + size_t index; + for (index = 0; index < count; ++index) { + free(items[index].id); + free(items[index].values); + } + free(items); +} + +static void free_colors(ColorMetadata *items, size_t count) { + size_t index; + for (index = 0; index < count; ++index) { + free(items[index].id); + free(items[index].kind); + free(items[index].word); + } + free(items); +} + +int main(int argc, char **argv) { + Options options = parse_options(argc, argv); + RusticolRuntimeHandle *runtime = NULL; + FILE *manifest; + char *process; + char *process_key; + char *representative_process_key; + char *color_accuracy; + int32_t *particles; + HelicityMetadata *helicities; + ColorMetadata *colors; + size_t particle_count = 0; + size_t helicity_count = 0; + size_t color_count = 0; + double *momenta; + size_t *external_permutation = NULL; + double total = NAN; + double *resolved = NULL; + size_t resolved_helicity_count = 0; + size_t resolved_color_count = 0; + size_t resolved_count; + double explicit_total = 0.0; + size_t index; + + manifest = fopen("artifact.json", "r"); + if (manifest == NULL) { + fail("run check_standalone from a generated artifact directory"); + } + if (fclose(manifest) != 0) { + fail_errno("close artifact.json"); + } + + check_rusticol( + rusticol_runtime_load( + ".", options.process, options.model_parameters, &runtime + ), + "load artifact" + ); + if (options.parameter_count > 0) { + check_rusticol( + rusticol_runtime_set_model_parameters( + runtime, + options.parameter_names, + options.parameter_real, + options.parameter_imaginary, + options.parameter_count + ), + "set model parameters" + ); + } + + process = runtime_string(runtime, rusticol_runtime_process, "query process"); + process_key = + runtime_string(runtime, rusticol_runtime_process_key, "query process key"); + representative_process_key = runtime_string( + runtime, + rusticol_runtime_representative_process_key, + "query representative process key" + ); + color_accuracy = runtime_string( + runtime, rusticol_runtime_color_accuracy, "query color accuracy" + ); + particles = load_external_particles(runtime, &particle_count); + helicities = load_helicities(runtime, &helicity_count); + colors = load_colors(runtime, &color_count); + if (options.kinematics != NULL) { + momenta = load_kinematics_json(runtime, options.kinematics, particle_count); + } else { + external_permutation = load_external_permutation(runtime, particle_count); + momenta = load_validation_point( + "API/validation_points.dat", representative_process_key, particle_count + ); + if (momenta != NULL) { + momenta = reorder_validation_point( + momenta, external_permutation, particle_count + ); + } + } + + if (momenta == NULL) { + if (options.json) { + fputs("{\"language\":\"c\",\"available\":false,", stdout); + write_common_json( + process, + process_key, + color_accuracy, + particles, + particle_count, + helicities, + helicity_count, + colors, + color_count + ); + fputs( + ",\"diagnostic\":\"no bundled validation point is available\"}\n", + stdout + ); + } else { + printf("process: %s\n", process); + puts("no bundled validation point is available; metadata load succeeded"); + } + } else { + check_rusticol( + rusticol_runtime_evaluate_f64( + runtime, momenta, 4 * particle_count, 1, &total, 1 + ), + "evaluate compatibility total" + ); + check_rusticol( + rusticol_runtime_resolved_shape( + runtime, + NULL, + 0, + NULL, + 0, + &resolved_helicity_count, + &resolved_color_count + ), + "query resolved shape" + ); + if (resolved_helicity_count != helicity_count || + resolved_color_count != color_count) { + fail("resolved shape does not match runtime metadata"); + } + if (resolved_helicity_count != 0 && + resolved_color_count > SIZE_MAX / resolved_helicity_count) { + fail("resolved output size overflow"); + } + resolved_count = resolved_helicity_count * resolved_color_count; + resolved = allocate(resolved_count, sizeof(*resolved)); + check_rusticol( + rusticol_runtime_evaluate_resolved_f64( + runtime, + momenta, + 4 * particle_count, + 1, + NULL, + 0, + NULL, + 0, + resolved, + resolved_count, + &resolved_helicity_count, + &resolved_color_count + ), + "evaluate resolved components" + ); + if (!isfinite(total)) { + fail("matrix-element total is not finite"); + } + for (index = 0; index < resolved_count; ++index) { + if (!isfinite(resolved[index])) { + fail("resolved matrix-element output is not finite"); + } + explicit_total += resolved[index]; + } + + if (options.json) { + fputs( + "{\"language\":\"c\",\"available\":true,\"precision\":16,", + stdout + ); + write_common_json( + process, + process_key, + color_accuracy, + particles, + particle_count, + helicities, + helicity_count, + colors, + color_count + ); + printf( + ",\"shape\":[1,%zu,%zu],\"values\":[", + resolved_helicity_count, + resolved_color_count + ); + for (index = 0; index < resolved_count; ++index) { + if (index != 0) { + putchar(','); + } + printf("%.17g", resolved[index]); + } + printf( + "],\"resolved_sum\":[%.17g],\"compatibility_total\":[%.17g]}\n", + explicit_total, + total + ); + } else { + size_t helicity_index; + size_t color_index; + printf("process: %s [%s]\n", process, process_key); + printf( + "resolved shape: (1, %zu, %zu)\n", + resolved_helicity_count, + resolved_color_count + ); + for (helicity_index = 0; + helicity_index < resolved_helicity_count; + ++helicity_index) { + for (color_index = 0; color_index < resolved_color_count; ++color_index) { + size_t offset = + helicity_index * resolved_color_count + color_index; + printf( + " %s %s %.17g\n", + helicities[helicity_index].id, + colors[color_index].id, + resolved[offset] + ); + } + } + printf("explicit resolved sum: %.17g\n", explicit_total); + printf("compatibility total: %.17g\n", total); + } + if (!isfinite(explicit_total) || + fabs(explicit_total - total) > 1.0e-12 * fmax(fabs(total), 1.0)) { + fail("resolved components do not reproduce the compatibility total"); + } + } + + check_rusticol(rusticol_runtime_free(runtime), "free runtime"); + free(momenta); + free(resolved); + free(process); + free(process_key); + free(representative_process_key); + free(external_permutation); + free(color_accuracy); + free(particles); + free_helicities(helicities, helicity_count); + free_colors(colors, color_count); + return EXIT_SUCCESS; +} diff --git a/pyamplicol/src/pyamplicol/assets/api_templates/cpp/Makefile b/pyamplicol/src/pyamplicol/assets/api_templates/cpp/Makefile new file mode 100644 index 0000000000..3edf40f252 --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/api_templates/cpp/Makefile @@ -0,0 +1,42 @@ +# SPDX-License-Identifier: 0BSD +RUSTICOL_CONFIG ?= rusticol-config +RUSTICOL_CONFIG_PATH := $(shell \ + if command -v "$(RUSTICOL_CONFIG)" >/dev/null 2>&1; then \ + command -v "$(RUSTICOL_CONFIG)"; \ + elif [ "$(RUSTICOL_CONFIG)" = rusticol-config ]; then \ + directory="$(CURDIR)"; \ + while [ "$$directory" != / ]; do \ + candidate="$$directory/.venv/bin/rusticol-config"; \ + if [ -x "$$candidate" ]; then printf '%s\n' "$$candidate"; break; fi; \ + directory=$$(dirname "$$directory"); \ + done; \ + fi) +ifeq ($(strip $(RUSTICOL_CONFIG_PATH)),) +$(error rusticol-config is unavailable; activate the pyAmpliCol environment or set RUSTICOL_CONFIG=/absolute/path) +endif +CXX ?= c++ +ARTIFACT_DIR := $(abspath ../..) +BUILD_DIR ?= $(abspath ../../..)/.pyamplicol-api-build/$(notdir $(ARTIFACT_DIR))/cpp +TARGET := $(BUILD_DIR)/check_standalone +CPPFLAGS += $(shell "$(RUSTICOL_CONFIG_PATH)" --cflags) +CXXFLAGS ?= -O2 +CXXFLAGS += -std=c++17 -Wall -Wextra -pedantic +LDLIBS += $(shell "$(RUSTICOL_CONFIG_PATH)" --libs) + +.PHONY: all check_standalone clean run + +all: $(TARGET) + +check_standalone: $(TARGET) + +$(BUILD_DIR): + mkdir -p "$@" + +$(TARGET): check_standalone.cpp | $(BUILD_DIR) + $(CXX) $(CPPFLAGS) $(CXXFLAGS) "$<" -o "$@" $(LDLIBS) + +run: $(TARGET) + cd "$(ARTIFACT_DIR)" && "$(TARGET)" $(ARGS) + +clean: + $(RM) "$(TARGET)" diff --git a/pyamplicol/src/pyamplicol/assets/api_templates/cpp/check_standalone.cpp b/pyamplicol/src/pyamplicol/assets/api_templates/cpp/check_standalone.cpp new file mode 100644 index 0000000000..8938ae2a3b --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/api_templates/cpp/check_standalone.cpp @@ -0,0 +1,293 @@ +// SPDX-License-Identifier: 0BSD + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace fs = std::filesystem; + +struct Options { + std::string process; + std::string model_parameters; + std::string kinematics; + std::vector parameter_names; + std::vector parameter_real; + std::vector parameter_imaginary; + int precision = 16; + bool json = false; +}; + +std::string json_string(const std::string &value) { + std::ostringstream output; + output << '"'; + for (const unsigned char character : value) { + switch (character) { + case '"': output << "\\\""; break; + case '\\': output << "\\\\"; break; + case '\n': output << "\\n"; break; + case '\r': output << "\\r"; break; + case '\t': output << "\\t"; break; + default: + if (character < 0x20) { + output << "\\u" << std::hex << std::setw(4) << std::setfill('0') + << static_cast(character) << std::dec << std::setfill(' '); + } else { + output << character; + } + } + } + output << '"'; + return output.str(); +} + +Options parse_options(int argc, char **argv) { + Options options; + for (int index = 1; index < argc; ++index) { + const std::string argument = argv[index]; + auto require = [&](int count) { + if (index + count >= argc) { + throw std::invalid_argument("missing value after " + argument); + } + }; + if (argument == "--process") { + require(1); + options.process = argv[++index]; + } else if (argument == "--model-parameters") { + require(1); + options.model_parameters = argv[++index]; + } else if (argument == "--kinematics") { + require(1); + options.kinematics = argv[++index]; + } else if (argument == "--set-parameter") { + require(3); + options.parameter_names.emplace_back(argv[++index]); + options.parameter_real.push_back(std::stod(argv[++index])); + options.parameter_imaginary.push_back(std::stod(argv[++index])); + } else if (argument == "--precision") { + require(1); + options.precision = std::stoi(argv[++index]); + } else if (argument == "--json") { + options.json = true; + } else if (argument == "--help" || argument == "-h") { + std::cout << "usage: check_standalone [--process ID|EXPRESSION] " + "[--model-parameters PATH] " + "[--kinematics PATH] " + "[--set-parameter NAME REAL IMAG] " + "[--precision 16] [--json]\n"; + std::exit(0); + } else { + throw std::invalid_argument("unknown option: " + argument); + } + } + if (options.precision != 16) { + throw std::invalid_argument( + "the C++ Rusticol API supports only double precision (--precision 16)"); + } + return options; +} + +std::vector split_tabs(const std::string &line) { + std::vector fields; + std::size_t start = 0; + while (true) { + const auto separator = line.find('\t', start); + fields.push_back(line.substr(start, separator - start)); + if (separator == std::string::npos) break; + start = separator + 1; + } + return fields; +} + +std::vector load_validation_point( + const fs::path &path, const std::string &process_id, std::size_t external_count) { + std::ifstream input(path); + if (!input) return {}; + std::string line; + std::getline(input, line); + if (line != "RUSTICOL_VALIDATION_POINTS_V1") { + throw std::runtime_error("unsupported validation_points.dat format"); + } + while (std::getline(input, line)) { + if (line.empty() || line[0] == '#') continue; + const auto fields = split_tabs(line); + if (fields.size() < 2 || fields[0] != process_id) continue; + const auto row_count = static_cast(std::stoull(fields[1])); + if (row_count != external_count || fields.size() != 2 + 4 * external_count) { + throw std::runtime_error( + "validation point has an incompatible external-particle count"); + } + std::vector values; + values.reserve(4 * external_count); + for (std::size_t index = 2; index < fields.size(); ++index) { + values.push_back(std::stod(fields[index])); + } + return values; + } + return {}; +} + +std::vector reorder_validation_point( + const std::vector &representative, + const std::vector &permutation) { + if (representative.empty()) return {}; + if (representative.size() != 4 * permutation.size()) { + throw std::runtime_error("validation point does not match external permutation"); + } + std::vector public_point(representative.size()); + std::vector seen(permutation.size(), false); + for (std::size_t representative_index = 0; + representative_index < permutation.size(); ++representative_index) { + const auto public_index = permutation[representative_index]; + if (public_index >= permutation.size() || seen[public_index]) { + throw std::runtime_error("runtime returned an invalid external permutation"); + } + seen[public_index] = true; + std::copy_n( + representative.begin() + 4 * representative_index, + 4, + public_point.begin() + 4 * public_index); + } + return public_point; +} + +template +void write_integer_array(std::ostream &output, const std::vector &values) { + output << '['; + for (std::size_t index = 0; index < values.size(); ++index) { + if (index) output << ','; + output << values[index]; + } + output << ']'; +} + +void write_common_json( + std::ostream &output, + const rusticol::Runtime &runtime, + const std::vector &particles, + const std::vector &helicities, + const std::vector &colors) { + output << "\"process\":" << json_string(runtime.process()) + << ",\"process_key\":" << json_string(runtime.process_key()) + << ",\"color_accuracy\":" << json_string(runtime.color_accuracy()); + output << ",\"external_particles\":["; + for (std::size_t index = 0; index < particles.size(); ++index) { + if (index) output << ','; + output << "{\"index\":" << particles[index].index + << ",\"pdg\":" << particles[index].pdg << '}'; + } + output << "],\"helicities\":["; + for (std::size_t index = 0; index < helicities.size(); ++index) { + if (index) output << ','; + output << "{\"id\":" << json_string(helicities[index].id) + << ",\"helicities\":"; + write_integer_array(output, helicities[index].helicities); + output << '}'; + } + output << "],\"colors\":["; + for (std::size_t index = 0; index < colors.size(); ++index) { + if (index) output << ','; + output << "{\"id\":" << json_string(colors[index].id) + << ",\"kind\":" << json_string(colors[index].kind) + << ",\"word\":"; + write_integer_array(output, colors[index].word); + output << '}'; + } + output << ']'; +} + +int main(int argc, char **argv) { + try { + const auto options = parse_options(argc, argv); + const fs::path executable = fs::weakly_canonical(fs::absolute(argv[0])); + fs::path root = fs::current_path(); + if (!fs::is_regular_file(root / "artifact.json")) { + root = executable.parent_path().parent_path().parent_path(); + } + rusticol::Runtime runtime(root.string(), options.process, options.model_parameters); + if (!options.parameter_names.empty()) { + runtime.set_model_parameters( + options.parameter_names, + options.parameter_real, + options.parameter_imaginary); + } + const auto particles = runtime.external_particles(); + const auto helicities = runtime.helicities(); + const auto colors = runtime.colors(); + const auto momenta = options.kinematics.empty() + ? reorder_validation_point( + load_validation_point( + root / "API" / "validation_points.dat", + runtime.representative_process_key(), + particles.size()), + runtime.external_permutation()) + : runtime.load_kinematics_json(options.kinematics); + if (momenta.empty()) { + if (options.json) { + std::cout << "{\"language\":\"cpp\",\"available\":false,"; + write_common_json(std::cout, runtime, particles, helicities, colors); + std::cout << ",\"diagnostic\":" + "\"no bundled validation point is available\"}\n"; + } else { + std::cout << "process: " << runtime.process() << "\n" + << "no bundled validation point is available; " + "metadata load succeeded\n"; + } + return 0; + } + const auto total = runtime.evaluate(momenta, 1); + const auto resolved = runtime.evaluate_resolved(momenta, 1); + const auto explicit_total = resolved.total(); + + if (options.json) { + std::cout << std::setprecision(17) + << "{\"language\":\"cpp\",\"available\":true," + "\"precision\":16,"; + write_common_json(std::cout, runtime, particles, helicities, colors); + std::cout << ",\"shape\":[1," << resolved.helicities.size() << ',' + << resolved.colors.size() << "],\"values\":["; + for (std::size_t index = 0; index < resolved.values.size(); ++index) { + if (index) std::cout << ','; + std::cout << resolved.values[index]; + } + std::cout << "],\"resolved_sum\":[" << explicit_total[0] + << "],\"compatibility_total\":[" << total[0] << "]}\n"; + } else { + std::cout << std::setprecision(17) + << "process: " << runtime.process() << " [" + << runtime.process_key() << "]\n" + << "resolved shape: (1, " << resolved.helicities.size() << ", " + << resolved.colors.size() << ")\n"; + for (std::size_t helicity = 0; + helicity < resolved.helicities.size(); + ++helicity) { + for (std::size_t color = 0; color < resolved.colors.size(); ++color) { + std::cout << " " << resolved.helicities[helicity].id << " " + << resolved.colors[color].id << " " + << resolved(0, helicity, color) << '\n'; + } + } + std::cout << "explicit resolved sum: " << explicit_total[0] << '\n' + << "compatibility total: " << total[0] << '\n'; + } + const double scale = std::max(std::abs(total[0]), 1.0); + if (std::abs(explicit_total[0] - total[0]) > 1.0e-12 * scale) { + throw std::runtime_error( + "resolved components do not reproduce the compatibility total"); + } + return 0; + } catch (const std::exception &error) { + std::cerr << "check_standalone: " << error.what() << '\n'; + return 1; + } +} diff --git a/pyamplicol/src/pyamplicol/assets/api_templates/fortran/Makefile b/pyamplicol/src/pyamplicol/assets/api_templates/fortran/Makefile new file mode 100644 index 0000000000..e626df0d15 --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/api_templates/fortran/Makefile @@ -0,0 +1,46 @@ +# SPDX-License-Identifier: 0BSD +RUSTICOL_CONFIG ?= rusticol-config +RUSTICOL_CONFIG_PATH := $(shell \ + if command -v "$(RUSTICOL_CONFIG)" >/dev/null 2>&1; then \ + command -v "$(RUSTICOL_CONFIG)"; \ + elif [ "$(RUSTICOL_CONFIG)" = rusticol-config ]; then \ + directory="$(CURDIR)"; \ + while [ "$$directory" != / ]; do \ + candidate="$$directory/.venv/bin/rusticol-config"; \ + if [ -x "$$candidate" ]; then printf '%s\n' "$$candidate"; break; fi; \ + directory=$$(dirname "$$directory"); \ + done; \ + fi) +ifeq ($(strip $(RUSTICOL_CONFIG_PATH)),) +$(error rusticol-config is unavailable; activate the pyAmpliCol environment or set RUSTICOL_CONFIG=/absolute/path) +endif +ifeq ($(origin FC), default) +FC := gfortran +endif +FC ?= gfortran +ARTIFACT_DIR := $(abspath ../..) +BUILD_DIR ?= $(abspath ../../..)/.pyamplicol-api-build/$(notdir $(ARTIFACT_DIR))/fortran +TARGET := $(BUILD_DIR)/check_standalone +RUSTICOL_FORTRAN := $(shell "$(RUSTICOL_CONFIG_PATH)" --fortran-source) +FFLAGS ?= -O2 +FFLAGS += -std=f2008 -ffree-line-length-none -Wall -Wextra +LDLIBS += $(shell "$(RUSTICOL_CONFIG_PATH)" --libs) + +.PHONY: all check_standalone clean run + +all: $(TARGET) + +check_standalone: $(TARGET) + +$(BUILD_DIR): + mkdir -p "$@" + +$(TARGET): check_standalone.f90 | $(BUILD_DIR) + test -f "$(RUSTICOL_FORTRAN)" + $(FC) $(FFLAGS) -J"$(BUILD_DIR)" "$(RUSTICOL_FORTRAN)" "$<" -o "$@" $(LDLIBS) + +run: $(TARGET) + cd "$(ARTIFACT_DIR)" && "$(TARGET)" $(ARGS) + +clean: + $(RM) "$(TARGET)" "$(BUILD_DIR)/rusticol.mod" diff --git a/pyamplicol/src/pyamplicol/assets/api_templates/fortran/check_standalone.f90 b/pyamplicol/src/pyamplicol/assets/api_templates/fortran/check_standalone.f90 new file mode 100644 index 0000000000..d52a39379a --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/api_templates/fortran/check_standalone.f90 @@ -0,0 +1,374 @@ +! SPDX-License-Identifier: 0BSD + +program check_standalone + use, intrinsic :: iso_c_binding + use, intrinsic :: iso_fortran_env, only: error_unit + use rusticol + implicit none + + integer, parameter :: MAX_OVERRIDES = 128 + type(rusticol_runtime) :: runtime + type(rusticol_external_particle), allocatable :: particles(:) + type(rusticol_helicity_configuration), allocatable :: helicities(:) + type(rusticol_color_component), allocatable :: colors(:) + character(len=4096) :: process_dir, process_key, representative_process_key + character(len=4096) :: model_parameters, kinematics + character(len=4096) :: executable + character(len=256) :: override_names_buffer(MAX_OVERRIDES) + character(len=256), allocatable :: override_names(:) + real(c_double) :: override_real_buffer(MAX_OVERRIDES) + real(c_double) :: override_imaginary_buffer(MAX_OVERRIDES) + real(c_double), allocatable :: override_real(:), override_imaginary(:) + real(c_double), allocatable, target :: momenta(:), totals(:), resolved(:, :, :) + integer(c_size_t), allocatable :: external_permutation(:) + logical :: json_output, point_available + integer :: precision, override_count + integer(c_int) :: status + integer :: color_index, helicity_index + real(c_double) :: explicit_total, scale + + call parse_options(process_key, model_parameters, kinematics, precision, json_output, & + override_names_buffer, override_real_buffer, override_imaginary_buffer, & + override_count) + if (precision /= 16) call fail( & + "the Fortran Rusticol API supports only double precision (--precision 16)") + call resolve_process_dir(executable, process_dir) + + if (len_trim(process_key) > 0 .and. len_trim(model_parameters) > 0) then + call runtime%load(trim(process_dir), trim(process_key), & + trim(model_parameters), status) + else if (len_trim(process_key) > 0) then + call runtime%load(trim(process_dir), trim(process_key), ierr=status) + else if (len_trim(model_parameters) > 0) then + call runtime%load(trim(process_dir), model_parameters=trim(model_parameters), & + ierr=status) + else + call runtime%load(trim(process_dir), ierr=status) + end if + if (status /= RUSTICOL_STATUS_OK) call fail(rusticol_last_error()) + + if (override_count > 0) then + allocate(override_names(override_count), override_real(override_count), & + override_imaginary(override_count)) + override_names = override_names_buffer(1:override_count) + override_real = override_real_buffer(1:override_count) + override_imaginary = override_imaginary_buffer(1:override_count) + call runtime%set_model_parameters( & + override_names, override_real, override_imaginary, ierr=status) + if (status /= RUSTICOL_STATUS_OK) call fail(rusticol_last_error()) + end if + + particles = runtime%external_particles(ierr=status) + if (status /= RUSTICOL_STATUS_OK) call fail(rusticol_last_error()) + helicities = runtime%helicities(ierr=status) + if (status /= RUSTICOL_STATUS_OK) call fail(rusticol_last_error()) + colors = runtime%colors(ierr=status) + if (status /= RUSTICOL_STATUS_OK) call fail(rusticol_last_error()) + process_key = runtime%process_key(ierr=status) + if (status /= RUSTICOL_STATUS_OK) call fail(rusticol_last_error()) + if (len_trim(kinematics) > 0) then + call runtime%load_kinematics_json(trim(kinematics), momenta, ierr=status) + if (status /= RUSTICOL_STATUS_OK) call fail(rusticol_last_error()) + point_available = .true. + else + representative_process_key = runtime%representative_process_key(ierr=status) + if (status /= RUSTICOL_STATUS_OK) call fail(rusticol_last_error()) + external_permutation = runtime%external_permutation(ierr=status) + if (status /= RUSTICOL_STATUS_OK) call fail(rusticol_last_error()) + call load_validation_point( & + trim(process_dir) // "/API/validation_points.dat", & + trim(representative_process_key), size(particles), momenta, point_available) + if (point_available) call reorder_validation_point(momenta, external_permutation) + end if + + if (.not. point_available) then + if (json_output) then + call emit_json_prefix(runtime, particles, helicities, colors, .false.) + write(*, '(A)') & + ',"diagnostic":"no bundled validation point is available"}' + else + write(*, '(A)') "process: " // runtime%process() + write(*, '(A)') & + "no bundled validation point is available; metadata load succeeded" + end if + stop 0 + end if + + call runtime%evaluate(momenta, 1_c_size_t, totals, ierr=status) + if (status /= RUSTICOL_STATUS_OK) call fail(rusticol_last_error()) + call runtime%evaluate_resolved(momenta, 1_c_size_t, resolved, ierr=status) + if (status /= RUSTICOL_STATUS_OK) call fail(rusticol_last_error()) + explicit_total = sum(resolved(:, :, 1)) + + if (json_output) then + call emit_json_prefix(runtime, particles, helicities, colors, .true.) + write(*, '(A,I0,A,I0,A)', advance='no') & + ',"shape":[1,', size(resolved, 2), ',', size(resolved, 1), & + '],"values":[' + do helicity_index = 1, size(resolved, 2) + do color_index = 1, size(resolved, 1) + if (helicity_index > 1 .or. color_index > 1) & + write(*, '(A)', advance='no') ',' + write(*, '(G0.17)', advance='no') & + resolved(color_index, helicity_index, 1) + end do + end do + write(*, '(A,G0.17,A,G0.17,A)') '],"resolved_sum":[', & + explicit_total, '],"compatibility_total":[', totals(1), ']}' + else + write(*, '(A)') "process: " // runtime%process() // & + " [" // trim(process_key) // "]" + write(*, '(A,I0,A,I0,A)') "resolved shape: (1, ", & + size(resolved, 2), ", ", size(resolved, 1), ")" + do helicity_index = 1, size(helicities) + do color_index = 1, size(colors) + write(*, '(2X,A,2X,A,2X,G0.17)') & + trim(helicities(helicity_index)%id), & + trim(colors(color_index)%id), & + resolved(color_index, helicity_index, 1) + end do + end do + write(*, '(A,G0.17)') "explicit resolved sum: ", explicit_total + write(*, '(A,G0.17)') "compatibility total: ", totals(1) + end if + scale = max(abs(totals(1)), 1.0_c_double) + if (abs(explicit_total - totals(1)) > 1.0e-12_c_double * scale) then + call fail("resolved components do not reproduce the compatibility total") + end if + +contains + + subroutine fail(message) + character(len=*), intent(in) :: message + write(error_unit, '(A)') "check_standalone: " // trim(message) + stop 1 + end subroutine fail + + subroutine parse_options(selected_process, model_card, kinematics_path, selected_precision, & + json, names, real_values, imaginary_values, count) + character(len=*), intent(out) :: selected_process, model_card, kinematics_path + integer, intent(out) :: selected_precision, count + logical, intent(out) :: json + character(len=*), intent(out) :: names(:) + real(c_double), intent(out) :: real_values(:), imaginary_values(:) + character(len=4096) :: argument, value + integer :: argc, index, parse_status + + selected_process = "" + model_card = "" + kinematics_path = "" + selected_precision = 16 + count = 0 + json = .false. + argc = command_argument_count() + index = 1 + do while (index <= argc) + call get_command_argument(index, argument) + select case (trim(argument)) + case ("--process") + if (index + 1 > argc) call fail("missing value after --process") + index = index + 1 + call get_command_argument(index, selected_process) + case ("--model-parameters") + if (index + 1 > argc) call fail( & + "missing value after --model-parameters") + index = index + 1 + call get_command_argument(index, model_card) + case ("--kinematics") + if (index + 1 > argc) call fail("missing value after --kinematics") + index = index + 1 + call get_command_argument(index, kinematics_path) + case ("--set-parameter") + if (index + 3 > argc) call fail( & + "--set-parameter requires NAME REAL IMAG") + if (count >= size(names)) call fail( & + "too many --set-parameter options") + count = count + 1 + index = index + 1 + call get_command_argument(index, names(count)) + index = index + 1 + call get_command_argument(index, value) + read(value, *, iostat=parse_status) real_values(count) + if (parse_status /= 0) call fail( & + "invalid real model-parameter component") + index = index + 1 + call get_command_argument(index, value) + read(value, *, iostat=parse_status) imaginary_values(count) + if (parse_status /= 0) call fail( & + "invalid imaginary model-parameter component") + case ("--precision") + if (index + 1 > argc) call fail("missing value after --precision") + index = index + 1 + call get_command_argument(index, value) + read(value, *, iostat=parse_status) selected_precision + if (parse_status /= 0) call fail("invalid --precision value") + case ("--json") + json = .true. + case ("--help", "-h") + write(*, '(A)') "usage: check_standalone [--process ID|EXPRESSION] " // & + "[--model-parameters PATH] [--kinematics PATH] " // & + "[--set-parameter NAME REAL IMAG] " // & + "[--precision 16] [--json]" + stop 0 + case default + call fail("unknown option: " // trim(argument)) + end select + index = index + 1 + end do + end subroutine parse_options + + subroutine resolve_process_dir(program_path, root) + character(len=*), intent(out) :: program_path, root + integer :: slash + logical :: artifact_in_working_directory + call get_command_argument(0, program_path) + inquire(file="artifact.json", exist=artifact_in_working_directory) + if (artifact_in_working_directory) then + root = "." + return + end if + slash = scan(trim(program_path), "/", back=.true.) + if (slash == 0) then + root = "../.." + else + root = program_path(:slash - 1) // "/../.." + end if + end subroutine resolve_process_dir + + subroutine load_validation_point(path, key, external_count, values, available) + character(len=*), intent(in) :: path, key + integer, intent(in) :: external_count + real(c_double), allocatable, target, intent(out) :: values(:) + logical, intent(out) :: available + character(len=65536) :: line + character(len=4096) :: row_key + integer :: unit, io_status, row_count + + available = .false. + allocate(values(4 * external_count)) + open(newunit=unit, file=path, status="old", action="read", & + iostat=io_status) + if (io_status /= 0) return + read(unit, '(A)', iostat=io_status) line + if (io_status /= 0 .or. & + trim(line) /= "RUSTICOL_VALIDATION_POINTS_V1") then + close(unit) + call fail("unsupported validation_points.dat format") + end if + do + read(unit, '(A)', iostat=io_status) line + if (io_status /= 0) exit + if (len_trim(line) == 0 .or. line(1:1) == "#") cycle + read(line, *, iostat=io_status) row_key, row_count + if (io_status /= 0 .or. trim(row_key) /= trim(key)) cycle + if (row_count /= external_count) then + close(unit) + call fail( & + "validation point has an incompatible external-particle count") + end if + read(line, *, iostat=io_status) row_key, row_count, values + if (io_status /= 0) then + close(unit) + call fail("invalid validation point row") + end if + available = .true. + exit + end do + close(unit) + end subroutine load_validation_point + + subroutine reorder_validation_point(values, permutation) + real(c_double), allocatable, intent(inout) :: values(:) + integer(c_size_t), intent(in) :: permutation(:) + real(c_double), allocatable :: reordered(:) + logical, allocatable :: seen(:) + integer :: representative_index, public_index + + if (size(values) /= 4 * size(permutation)) call fail( & + "validation point does not match external permutation") + allocate(reordered(size(values)), seen(size(permutation))) + seen = .false. + do representative_index = 1, size(permutation) + public_index = int(permutation(representative_index)) + 1 + if (public_index < 1 .or. public_index > size(permutation) .or. & + seen(public_index)) call fail( & + "runtime returned an invalid external permutation") + seen(public_index) = .true. + reordered(4 * public_index - 3:4 * public_index) = & + values(4 * representative_index - 3:4 * representative_index) + end do + call move_alloc(reordered, values) + end subroutine reorder_validation_point + + function json_escape(value) result(escaped) + character(len=*), intent(in) :: value + character(len=:), allocatable :: escaped + integer :: index + escaped = "" + do index = 1, len_trim(value) + select case (value(index:index)) + case ('"') + escaped = escaped // '\"' + case ('\') + escaped = escaped // '\\' + case default + escaped = escaped // value(index:index) + end select + end do + end function json_escape + + subroutine emit_json_prefix(active_runtime, external, helicity_items, & + color_items, available) + type(rusticol_runtime), intent(inout) :: active_runtime + type(rusticol_external_particle), intent(in) :: external(:) + type(rusticol_helicity_configuration), intent(in) :: helicity_items(:) + type(rusticol_color_component), intent(in) :: color_items(:) + logical, intent(in) :: available + integer :: item, component + + write(*, '(A)', advance='no') '{"language":"fortran","available":' + if (available) then + write(*, '(A)', advance='no') 'true,"precision":16' + else + write(*, '(A)', advance='no') 'false' + end if + write(*, '(A)', advance='no') ',"process":"' // & + json_escape(active_runtime%process()) // '","process_key":"' // & + json_escape(active_runtime%process_key()) // & + '","color_accuracy":"' // & + json_escape(active_runtime%color_accuracy()) // & + '","external_particles":[' + do item = 1, size(external) + if (item > 1) write(*, '(A)', advance='no') ',' + write(*, '(A,I0,A,I0,A)', advance='no') & + '{"index":', external(item)%index, ',"pdg":', & + external(item)%pdg, '}' + end do + write(*, '(A)', advance='no') '],"helicities":[' + do item = 1, size(helicity_items) + if (item > 1) write(*, '(A)', advance='no') ',' + write(*, '(A)', advance='no') '{"id":"' // & + json_escape(helicity_items(item)%id) // '","helicities":[' + do component = 1, size(helicity_items(item)%helicities) + if (component > 1) write(*, '(A)', advance='no') ',' + write(*, '(I0)', advance='no') & + helicity_items(item)%helicities(component) + end do + write(*, '(A)', advance='no') ']}' + end do + write(*, '(A)', advance='no') '],"colors":[' + do item = 1, size(color_items) + if (item > 1) write(*, '(A)', advance='no') ',' + write(*, '(A)', advance='no') '{"id":"' // & + json_escape(color_items(item)%id) // '","kind":"' // & + json_escape(color_items(item)%kind) // '","word":[' + do component = 1, size(color_items(item)%word) + if (component > 1) write(*, '(A)', advance='no') ',' + write(*, '(I0)', advance='no') color_items(item)%word(component) + end do + write(*, '(A)', advance='no') ']}' + end do + write(*, '(A)', advance='no') ']' + end subroutine emit_json_prefix + +end program check_standalone diff --git a/pyamplicol/src/pyamplicol/assets/api_templates/python/check_standalone.py b/pyamplicol/src/pyamplicol/assets/api_templates/python/check_standalone.py new file mode 100644 index 0000000000..3e22ac3d56 --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/api_templates/python/check_standalone.py @@ -0,0 +1,401 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: 0BSD +"""Evaluate a generated process through pyAmpliCol's installed Python API.""" + +from __future__ import annotations + +import argparse +import importlib +import json +import math +import os +import statistics +import sys +import time +from decimal import Decimal +from pathlib import Path +from typing import Any + + +def _load_pyamplicol() -> Any: + try: + return importlib.import_module("pyamplicol") + except ModuleNotFoundError as error: + if error.name != "pyamplicol": + raise + previous = os.environ.get("_PYAMPLICOL_API_BOOTSTRAP") + if previous is not None: + raise SystemExit( + "pyamplicol remained unavailable after retrying with " + previous + ) from error + script = Path(__file__).resolve() + layouts = ( + (Path("bin/python"), Path("bin/rusticol-config")), + (Path("Scripts/python.exe"), Path("Scripts/rusticol-config.exe")), + ) + for parent in script.parents: + for python_relative, config_relative in layouts: + python = parent / ".venv" / python_relative + config = parent / ".venv" / config_relative + if ( + not python.is_file() + or not os.access(python, os.X_OK) + or not config.is_file() + or not os.access(config, os.X_OK) + ): + continue + environment = os.environ.copy() + environment["_PYAMPLICOL_API_BOOTSTRAP"] = str(python) + os.execve( + python, + (str(python), str(script), *sys.argv[1:]), + environment, + ) + raise SystemExit( + "pyamplicol is not importable; activate its environment or place this " + "artifact below a checkout containing .venv from `just dev-install`" + ) from error + + +pyamplicol = _load_pyamplicol() + + +def _validation_point(path: Path, process_id: str, precision: int): + if not path.is_file(): + return None + lines = path.read_text(encoding="ascii").splitlines() + if not lines or lines[0] != "RUSTICOL_VALIDATION_POINTS_V1": + raise SystemExit("unsupported validation_points.dat format") + for line in lines[1:]: + if not line or line.startswith("#"): + continue + fields = line.split("\t") + if len(fields) < 2 or fields[0] != process_id: + continue + external_count = int(fields[1]) + components = fields[2:] + if len(components) != external_count * 4: + raise SystemExit(f"invalid validation row for {process_id!r}") + convert = float if precision == 16 else Decimal + return [ + [convert(value) for value in components[index : index + 4]] + for index in range(0, len(components), 4) + ] + return None + + +def _reorder_point( + point: list[list[Any]], permutation: tuple[int, ...] +) -> list[list[Any]]: + if len(point) != len(permutation) or sorted(permutation) != list(range(len(point))): + raise SystemExit("runtime returned an invalid external permutation") + reordered: list[list[Any] | None] = [None] * len(point) + for representative_index, public_index in enumerate(permutation): + reordered[public_index] = point[representative_index] + if any(vector is None for vector in reordered): + raise SystemExit("runtime returned an incomplete external permutation") + return [vector for vector in reordered if vector is not None] + + +def _kinematics(path: str, precision: int, external_count: int) -> list[list[Any]]: + source = Path(path).expanduser().resolve(strict=True) + + def invalid_constant(value: str) -> None: + raise ValueError(f"non-finite JSON constant {value!r}") + + try: + payload = json.loads( + source.read_text(encoding="utf-8"), + parse_float=Decimal if precision != 16 else float, + parse_int=Decimal if precision != 16 else int, + parse_constant=invalid_constant, + ) + except (json.JSONDecodeError, OSError, ValueError) as error: + raise SystemExit(f"could not read kinematics JSON {source}: {error}") from error + if ( + isinstance(payload, list) + and len(payload) == 1 + and isinstance(payload[0], list) + and payload[0] + and isinstance(payload[0][0], list) + ): + payload = payload[0] + if not isinstance(payload, list) or len(payload) != external_count: + raise SystemExit( + f"kinematics JSON must contain one [{external_count}][4] point" + ) + convert = float if precision == 16 else Decimal + point: list[list[Any]] = [] + for leg_index, raw_leg in enumerate(payload): + if not isinstance(raw_leg, list) or len(raw_leg) != 4: + raise SystemExit( + f"kinematics leg {leg_index} must contain [E, px, py, pz]" + ) + leg: list[Any] = [] + for component_index, raw in enumerate(raw_leg): + if isinstance(raw, bool) or not isinstance(raw, (int, float, Decimal, str)): + raise SystemExit( + f"kinematics leg {leg_index} component {component_index} " + "must be a number or decimal string" + ) + try: + value = convert(raw) + except (TypeError, ValueError) as error: + raise SystemExit( + f"kinematics leg {leg_index} component {component_index} " + "is not a decimal value" + ) from error + finite = ( + math.isfinite(value) + if isinstance(value, float) + else value.is_finite() + ) + if not finite: + raise SystemExit("kinematics components must be finite") + leg.append(value) + point.append(leg) + return point + + +def _repeat_point(point: list[list[Any]], count: int) -> list[list[list[Any]]]: + return [point for _ in range(count)] + + +def _flatten(values: Any) -> list[Any]: + if hasattr(values, "reshape"): + return values.reshape(-1).tolist() + output: list[Any] = [] + + def visit(value: Any) -> None: + if isinstance(value, (list, tuple)): + for entry in value: + visit(entry) + else: + output.append(value) + + visit(values) + return output + + +def _real(value: Any) -> Any: + if isinstance(value, Decimal): + return value + converted = complex(value) + if abs(converted.imag) > 1.0e-14 * max(1.0, abs(converted.real)): + raise SystemExit("matrix-element output unexpectedly has an imaginary part") + return converted.real + + +def _json_scalar(value: Any, precision: int) -> float | str: + real = _real(value) + return float(real) if precision == 16 else str(real) + + +def _parameter_component(value: Any) -> float: + if isinstance(value, bool) or not isinstance(value, (int, float)): + raise SystemExit("model-parameter components must be JSON numbers") + component = float(value) + if not math.isfinite(component): + raise SystemExit("model-parameter components must be finite") + return component + + +def _parameter_value(value: Any) -> complex | float: + if isinstance(value, list): + if len(value) != 2: + raise SystemExit("model-parameter arrays must contain [real, imaginary]") + return complex( + _parameter_component(value[0]), + _parameter_component(value[1]), + ) + return _parameter_component(value) + + +def _model_parameters(path: str | None) -> dict[str, complex | float] | None: + if path is None: + return None + source = Path(path).expanduser().resolve(strict=True) + payload = json.loads(source.read_text(encoding="utf-8")) + if not isinstance(payload, dict): + raise SystemExit("model-parameter JSON must be an object") + return {str(name): _parameter_value(value) for name, value in payload.items()} + + +def _physics_payload(physics: pyamplicol.ProcessPhysics) -> dict[str, Any]: + colors = [ + {"id": item.id, "kind": "lc-flow", "word": list(item.word)} + for item in physics.color_flows + ] + colors.extend( + {"id": item.id, "kind": "contracted-color", "word": []} + for item in physics.contracted_color_components + ) + return { + "process": physics.process, + "process_key": physics.process_id, + "color_accuracy": physics.color_accuracy, + "external_particles": [ + {"index": item.index, "pdg": item.pdg_id} + for item in physics.external_particles + ], + "helicities": [ + {"id": item.id, "helicities": list(item.values)} + for item in physics.helicities + ], + "colors": colors, + } + + +def _profile( + runtime: pyamplicol.Runtime, + point: list[list[Any]], + precision: int, + target_runtime: float, + batch_size: int, +) -> dict[str, float | int]: + samples: list[float] = [] + elapsed = 0.0 + while len(samples) < 8 or elapsed < target_runtime: + batch = _repeat_point(point, batch_size) + started = time.perf_counter() + runtime.evaluate(batch, precision=precision) + wall = time.perf_counter() - started + elapsed += wall + samples.append(wall / batch_size) + return { + "samples": len(samples) * batch_size, + "wall_us_per_point": statistics.mean(samples) * 1.0e6, + } + + +def _parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + description="Resolved pyAmpliCol Python API example" + ) + parser.add_argument( + "--process", + help="stable process/alias ID or exact concrete process expression", + ) + parser.add_argument("--model-parameters") + parser.add_argument( + "--kinematics", + help="JSON point in the external-particle order supplied by --process", + ) + parser.add_argument( + "--set-parameter", + nargs=3, + action="append", + default=[], + metavar=("NAME", "REAL", "IMAG"), + ) + parser.add_argument("--precision", type=int, default=16) + parser.add_argument("--profile", action="store_true") + parser.add_argument("--target-runtime", type=float, default=10.0) + parser.add_argument("--batch-size", type=int, default=128) + parser.add_argument("--json", action="store_true") + return parser + + +def main() -> int: + args = _parser().parse_args() + root = Path(__file__).resolve().parents[2] + runtime = pyamplicol.Runtime.load( + root, + process=args.process, + model_parameters=_model_parameters(args.model_parameters), + ) + if args.set_parameter: + runtime.set_model_parameters( + { + name: complex(float(real), float(imaginary)) + for name, real, imaginary in args.set_parameter + } + ) + physics = runtime.physics + metadata = _physics_payload(physics) + if args.kinematics is None: + point = _validation_point( + root / "API" / "validation_points.dat", + runtime.representative_process_key, + args.precision, + ) + if point is not None: + point = _reorder_point(point, runtime.external_permutation) + else: + point = _kinematics( + args.kinematics, + args.precision, + len(physics.external_particles), + ) + if point is None: + payload = { + "language": "python", + "available": False, + "diagnostic": "no bundled validation point is available", + **metadata, + } + if args.json: + print(json.dumps(payload, separators=(",", ":"), sort_keys=True)) + else: + print(f"process: {physics.process}") + print("no bundled validation point is available; metadata load succeeded") + return 0 + + batch = _repeat_point(point, 1) + resolved = runtime.evaluate_resolved(batch, precision=args.precision) + total = runtime.evaluate(batch, precision=args.precision) + values = _flatten(resolved.values) + explicit_total = _flatten(resolved.total()) + compatibility_total = _flatten(total) + payload: dict[str, Any] = { + "language": "python", + "available": True, + "precision": args.precision, + **metadata, + "shape": list(resolved.shape), + "values": [_json_scalar(value, args.precision) for value in values], + "resolved_sum": [ + _json_scalar(value, args.precision) for value in explicit_total + ], + "compatibility_total": [ + _json_scalar(value, args.precision) for value in compatibility_total + ], + } + if args.profile: + payload["profile"] = _profile( + runtime, + point, + args.precision, + max(args.target_runtime, 0.0), + max(args.batch_size, 1), + ) + if args.json: + print(json.dumps(payload, separators=(",", ":"), sort_keys=True)) + return 0 + + print(f"process: {physics.process} [{physics.process_id}]") + print(f"resolved shape: {resolved.shape}") + color_items = metadata["colors"] + color_count = len(color_items) + for helicity_index, helicity in enumerate(metadata["helicities"]): + for color_index, color in enumerate(color_items): + offset = helicity_index * color_count + color_index + print(f" {helicity['id']} {color['id']} {values[offset]}") + print(f"explicit resolved sum: {explicit_total[0]}") + print(f"compatibility total: {compatibility_total[0]}") + if not math.isclose( + float(_real(explicit_total[0])), + float(_real(compatibility_total[0])), + rel_tol=1.0e-12, + abs_tol=1.0e-15, + ): + raise SystemExit("resolved components do not reproduce the compatibility total") + if args.profile: + profile = payload["profile"] + assert isinstance(profile, dict) + print(f"timing: {profile['wall_us_per_point']:.6g} us/point") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/pyamplicol/src/pyamplicol/assets/api_templates/rust/Makefile b/pyamplicol/src/pyamplicol/assets/api_templates/rust/Makefile new file mode 100644 index 0000000000..3f8824f58a --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/api_templates/rust/Makefile @@ -0,0 +1,49 @@ +# SPDX-License-Identifier: 0BSD +RUSTICOL_CONFIG ?= rusticol-config +RUSTICOL_CONFIG_PATH := $(shell \ + if command -v "$(RUSTICOL_CONFIG)" >/dev/null 2>&1; then \ + command -v "$(RUSTICOL_CONFIG)"; \ + elif [ "$(RUSTICOL_CONFIG)" = rusticol-config ]; then \ + directory="$(CURDIR)"; \ + while [ "$$directory" != / ]; do \ + candidate="$$directory/.venv/bin/rusticol-config"; \ + if [ -x "$$candidate" ]; then printf '%s\n' "$$candidate"; break; fi; \ + directory=$$(dirname "$$directory"); \ + done; \ + fi) +ifeq ($(strip $(RUSTICOL_CONFIG_PATH)),) +$(error rusticol-config is unavailable; activate the pyAmpliCol environment or set RUSTICOL_CONFIG=/absolute/path) +endif +RUSTC ?= rustc +RUST_SCRIPT ?= rust-script +ARTIFACT_DIR := $(abspath ../..) +BUILD_DIR ?= $(abspath ../../..)/.pyamplicol-api-build/$(notdir $(ARTIFACT_DIR))/rust +TARGET := $(BUILD_DIR)/check_standalone +RUSTFLAGS ?= -C opt-level=2 +RUSTICOL_RUST_SOURCE := $(shell "$(RUSTICOL_CONFIG_PATH)" --rust-source) +RUSTICOL_RUSTFLAGS := $(shell "$(RUSTICOL_CONFIG_PATH)" --rustflags) + +.PHONY: all check_standalone clean run run-script + +all: $(TARGET) + +check_standalone: $(TARGET) + +$(BUILD_DIR): + mkdir -p "$@" + +$(TARGET): check_standalone.rs | $(BUILD_DIR) + RUSTICOL_RUST_SOURCE="$(RUSTICOL_RUST_SOURCE)" \ + $(RUSTC) --edition=2021 $(RUSTFLAGS) "$<" -o "$@" $(RUSTICOL_RUSTFLAGS) + +run: $(TARGET) + cd "$(ARTIFACT_DIR)" && "$(TARGET)" $(ARGS) + +run-script: + cd "$(ARTIFACT_DIR)" && \ + RUSTICOL_RUST_SOURCE="$(RUSTICOL_RUST_SOURCE)" \ + CARGO_ENCODED_RUSTFLAGS="$$("$(RUSTICOL_CONFIG_PATH)" --cargo-rustflags)" \ + $(RUST_SCRIPT) "API/rust/check_standalone.rs" -- $(ARGS) + +clean: + $(RM) "$(TARGET)" diff --git a/pyamplicol/src/pyamplicol/assets/api_templates/rust/check_standalone.rs b/pyamplicol/src/pyamplicol/assets/api_templates/rust/check_standalone.rs new file mode 100644 index 0000000000..5c91f5c862 --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/api_templates/rust/check_standalone.rs @@ -0,0 +1,454 @@ +#!/usr/bin/env rust-script +// SPDX-License-Identifier: 0BSD +//! ```cargo +//! [package] +//! edition = "2021" +//! ``` + +use std::env; +use std::fmt::Write as _; +use std::fs; +use std::path::{Path, PathBuf}; + +#[allow(dead_code)] +mod rusticol { + include!(env!("RUSTICOL_RUST_SOURCE")); +} + +use rusticol::{ParameterUpdate, PhysicsMetadata, ResolvedEvaluation, Runtime, Selectors}; + +#[derive(Default)] +struct Options { + process: Option, + model_parameters: Option, + kinematics: Option, + overrides: Vec, + precision: u32, + json: bool, + help: bool, +} + +fn sdk(result: rusticol::Result) -> Result { + result.map_err(|error| error.to_string()) +} + +fn usage() -> &'static str { + "usage: check_standalone [--process ID|EXPRESSION] [--model-parameters PATH] \ +[--kinematics PATH] [--set-parameter NAME REAL IMAG] [--precision 16] [--json]" +} + +fn parse_options() -> Result { + let arguments = env::args().skip(1).collect::>(); + let mut options = Options { + precision: 16, + ..Options::default() + }; + let mut index = 0; + while index < arguments.len() { + let argument = &arguments[index]; + match argument.as_str() { + "--" => { + index += 1; + } + "--process" => { + options.process = Some(required_argument(&arguments, index, 1)?.to_owned()); + index += 2; + } + "--model-parameters" => { + options.model_parameters = + Some(required_argument(&arguments, index, 1)?.to_owned()); + index += 2; + } + "--kinematics" => { + options.kinematics = + Some(required_argument(&arguments, index, 1)?.to_owned()); + index += 2; + } + "--set-parameter" => { + let name = required_argument(&arguments, index, 1)?.to_owned(); + let real = parse_f64( + required_argument(&arguments, index, 2)?, + "real model-parameter component", + )?; + let imaginary = parse_f64( + required_argument(&arguments, index, 3)?, + "imaginary model-parameter component", + )?; + options + .overrides + .push(ParameterUpdate::new(name, real, imaginary)); + index += 4; + } + "--precision" => { + options.precision = required_argument(&arguments, index, 1)? + .parse() + .map_err(|_| "invalid --precision value".to_owned())?; + index += 2; + } + "--json" => { + options.json = true; + index += 1; + } + "--help" | "-h" => { + options.help = true; + index += 1; + } + _ => return Err(format!("unknown option: {argument}")), + } + } + if options.precision != 16 { + return Err( + "the Rust Rusticol API supports only double precision (--precision 16)".to_owned(), + ); + } + Ok(options) +} + +fn required_argument<'a>( + arguments: &'a [String], + option_index: usize, + offset: usize, +) -> Result<&'a str, String> { + arguments + .get(option_index + offset) + .map(String::as_str) + .ok_or_else(|| format!("missing value after {}", arguments[option_index])) +} + +fn parse_f64(value: &str, description: &str) -> Result { + let parsed = value + .parse::() + .map_err(|_| format!("invalid {description}"))?; + if parsed.is_finite() { + Ok(parsed) + } else { + Err(format!("invalid {description}")) + } +} + +fn artifact_root() -> Result { + let current = + env::current_dir().map_err(|error| format!("cannot read current directory: {error}"))?; + if current.join("artifact.json").is_file() { + return Ok(current); + } + + let executable = env::current_exe() + .map_err(|error| format!("cannot locate check_standalone executable: {error}"))?; + for ancestor in executable.ancestors().skip(1) { + if ancestor.join("artifact.json").is_file() { + return Ok(ancestor.to_path_buf()); + } + } + + let language_dir = executable.parent(); + let build_artifact = language_dir.and_then(Path::parent); + let build_root = build_artifact.and_then(Path::parent); + if build_root.and_then(Path::file_name) == Some(".pyamplicol-api-build".as_ref()) { + if let (Some(parent), Some(name)) = ( + build_root.and_then(Path::parent), + build_artifact.and_then(Path::file_name), + ) { + let candidate = parent.join(name); + if candidate.join("artifact.json").is_file() { + return Ok(candidate); + } + } + } + + Err("run check_standalone from a generated artifact directory".to_owned()) +} + +fn load_validation_point( + path: &Path, + process_key: &str, + external_count: usize, +) -> Result>, String> { + let contents = match fs::read_to_string(path) { + Ok(contents) => contents, + Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None), + Err(error) => return Err(format!("cannot read {}: {error}", path.display())), + }; + let mut lines = contents.lines(); + if lines.next() != Some("RUSTICOL_VALIDATION_POINTS_V1") { + return Err("unsupported validation_points.dat format".to_owned()); + } + for line in lines { + if line.is_empty() || line.starts_with('#') { + continue; + } + let fields = line.split('\t').collect::>(); + if fields.len() < 2 || fields[0] != process_key { + continue; + } + let row_count = fields[1] + .parse::() + .map_err(|_| "invalid validation point row".to_owned())?; + if row_count != external_count || fields.len() != 2 + 4 * external_count { + return Err("validation point has an incompatible external-particle count".to_owned()); + } + let momenta = fields[2..] + .iter() + .map(|value| parse_f64(value, "validation-point component")) + .collect::, _>>()?; + return Ok(Some(momenta)); + } + Ok(None) +} + +fn reorder_validation_point( + representative: Vec, + permutation: &[usize], +) -> Result, String> { + if representative.len() != 4 * permutation.len() { + return Err("validation point does not match external permutation".to_owned()); + } + let mut public_point = vec![0.0; representative.len()]; + let mut seen = vec![false; permutation.len()]; + for (representative_index, &public_index) in permutation.iter().enumerate() { + if public_index >= permutation.len() || seen[public_index] { + return Err("runtime returned an invalid external permutation".to_owned()); + } + seen[public_index] = true; + public_point[4 * public_index..4 * public_index + 4].copy_from_slice( + &representative[4 * representative_index..4 * representative_index + 4], + ); + } + Ok(public_point) +} + +fn json_string(value: &str) -> String { + let mut output = String::with_capacity(value.len() + 2); + output.push('"'); + for character in value.chars() { + match character { + '"' => output.push_str("\\\""), + '\\' => output.push_str("\\\\"), + '\u{08}' => output.push_str("\\b"), + '\u{0c}' => output.push_str("\\f"), + '\n' => output.push_str("\\n"), + '\r' => output.push_str("\\r"), + '\t' => output.push_str("\\t"), + character if character <= '\u{1f}' => { + write!(&mut output, "\\u{:04x}", character as u32) + .expect("writing to a String cannot fail"); + } + character => output.push(character), + } + } + output.push('"'); + output +} + +fn write_i32_array(output: &mut String, values: &[i32]) { + output.push('['); + for (index, value) in values.iter().enumerate() { + if index != 0 { + output.push(','); + } + write!(output, "{value}").expect("writing to a String cannot fail"); + } + output.push(']'); +} + +fn write_usize_array(output: &mut String, values: &[usize]) { + output.push('['); + for (index, value) in values.iter().enumerate() { + if index != 0 { + output.push(','); + } + write!(output, "{value}").expect("writing to a String cannot fail"); + } + output.push(']'); +} + +fn write_f64_array(output: &mut String, values: &[f64]) -> Result<(), String> { + output.push('['); + for (index, value) in values.iter().enumerate() { + if !value.is_finite() { + return Err("matrix-element output is not finite".to_owned()); + } + if index != 0 { + output.push(','); + } + write!(output, "{value}").expect("writing to a String cannot fail"); + } + output.push(']'); + Ok(()) +} + +fn write_common_json(output: &mut String, metadata: &PhysicsMetadata) { + write!( + output, + "\"process\":{},\"process_key\":{},\"color_accuracy\":{}", + json_string(&metadata.process), + json_string(&metadata.process_key), + json_string(&metadata.color_accuracy), + ) + .expect("writing to a String cannot fail"); + + output.push_str(",\"external_particles\":["); + for (index, particle) in metadata.external_particles.iter().enumerate() { + if index != 0 { + output.push(','); + } + write!( + output, + "{{\"index\":{},\"pdg\":{}}}", + particle.index, particle.pdg + ) + .expect("writing to a String cannot fail"); + } + + output.push_str("],\"helicities\":["); + for (index, helicity) in metadata.helicities.iter().enumerate() { + if index != 0 { + output.push(','); + } + write!( + output, + "{{\"id\":{},\"helicities\":", + json_string(&helicity.id) + ) + .expect("writing to a String cannot fail"); + write_i32_array(output, &helicity.helicities); + output.push('}'); + } + + output.push_str("],\"colors\":["); + for (index, color) in metadata.colors.iter().enumerate() { + if index != 0 { + output.push(','); + } + write!( + output, + "{{\"id\":{},\"kind\":{},\"word\":", + json_string(&color.id), + json_string(&color.kind), + ) + .expect("writing to a String cannot fail"); + write_usize_array(output, &color.word); + output.push('}'); + } + output.push(']'); +} + +fn available_json( + metadata: &PhysicsMetadata, + resolved: &ResolvedEvaluation, + explicit_total: f64, + total: f64, +) -> Result { + let (_, helicity_count, color_count) = resolved.shape(); + let mut output = "{\"language\":\"rust\",\"available\":true,\"precision\":16,".to_owned(); + write_common_json(&mut output, metadata); + write!( + &mut output, + ",\"shape\":[1,{helicity_count},{color_count}],\"values\":" + ) + .expect("writing to a String cannot fail"); + write_f64_array(&mut output, resolved.values())?; + output.push_str(",\"resolved_sum\":"); + write_f64_array(&mut output, &[explicit_total])?; + output.push_str(",\"compatibility_total\":"); + write_f64_array(&mut output, &[total])?; + output.push_str("}\n"); + Ok(output) +} + +fn unavailable_json(metadata: &PhysicsMetadata) -> String { + let mut output = "{\"language\":\"rust\",\"available\":false,".to_owned(); + write_common_json(&mut output, metadata); + output.push_str(",\"diagnostic\":\"no bundled validation point is available\"}\n"); + output +} + +fn run() -> Result<(), String> { + let options = parse_options()?; + if options.help { + println!("{}", usage()); + return Ok(()); + } + let root = artifact_root()?; + let model_parameters = options.model_parameters.as_deref().map(Path::new); + let mut runtime = sdk(Runtime::load( + &root, + options.process.as_deref(), + model_parameters, + ))?; + sdk(runtime.set_model_parameters(&options.overrides))?; + let metadata = sdk(runtime.physics())?; + let point = if let Some(path) = options.kinematics.as_deref() { + Some(sdk(runtime.load_kinematics_json(path))?) + } else { + load_validation_point( + &root.join("API/validation_points.dat"), + &sdk(runtime.representative_process_key())?, + metadata.external_particles.len(), + )? + .map(|point| { + let permutation = sdk(runtime.external_permutation())?; + reorder_validation_point(point, &permutation) + }) + .transpose()? + }; + let Some(momenta) = point else { + if options.json { + print!("{}", unavailable_json(&metadata)); + } else { + println!("process: {}", metadata.process); + println!("no bundled validation point is available; metadata load succeeded"); + } + return Ok(()); + }; + + let totals = sdk(runtime.evaluate_f64(&momenta, 1))?; + let total = *totals + .first() + .ok_or_else(|| "Rusticol returned no compatibility total".to_owned())?; + let resolved = sdk(runtime.evaluate_resolved_f64(&momenta, 1, &Selectors::all()))?; + let (_, helicity_count, color_count) = resolved.shape(); + if helicity_count != metadata.helicities.len() || color_count != metadata.colors.len() { + return Err("resolved shape does not match runtime metadata".to_owned()); + } + let resolved_totals = resolved.totals(); + let explicit_total = *resolved_totals + .first() + .ok_or_else(|| "Rusticol returned no resolved total".to_owned())?; + if !explicit_total.is_finite() || !total.is_finite() { + return Err("matrix-element output is not finite".to_owned()); + } + let scale = total.abs().max(1.0); + if (explicit_total - total).abs() > 1.0e-12 * scale { + return Err("resolved components do not reproduce the compatibility total".to_owned()); + } + + if options.json { + print!( + "{}", + available_json(&metadata, &resolved, explicit_total, total)? + ); + return Ok(()); + } + + println!("process: {} [{}]", metadata.process, metadata.process_key); + println!("resolved shape: (1, {helicity_count}, {color_count})"); + for (helicity_index, helicity) in resolved.helicities().iter().enumerate() { + for (color_index, color) in resolved.colors().iter().enumerate() { + let value = resolved + .get(0, helicity_index, color_index) + .ok_or_else(|| "resolved result index is out of range".to_owned())?; + println!(" {} {} {}", helicity.id, color.id, value); + } + } + println!("explicit resolved sum: {explicit_total}"); + println!("compatibility total: {total}"); + Ok(()) +} + +fn main() { + if let Err(error) = run() { + eprintln!("check_standalone: {error}"); + std::process::exit(1); + } +} diff --git a/pyamplicol/src/pyamplicol/assets/api_templates/umami/Makefile b/pyamplicol/src/pyamplicol/assets/api_templates/umami/Makefile new file mode 100644 index 0000000000..6e37a40031 --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/api_templates/umami/Makefile @@ -0,0 +1,84 @@ +# SPDX-License-Identifier: 0BSD +RUSTICOL_CONFIG ?= rusticol-config +RUSTICOL_CONFIG_PATH := $(shell \ + if command -v "$(RUSTICOL_CONFIG)" >/dev/null 2>&1; then \ + command -v "$(RUSTICOL_CONFIG)"; \ + elif [ "$(RUSTICOL_CONFIG)" = rusticol-config ]; then \ + directory="$(CURDIR)"; \ + while [ "$$directory" != / ]; do \ + candidate="$$directory/.venv/bin/rusticol-config"; \ + if [ -x "$$candidate" ]; then printf '%s\n' "$$candidate"; break; fi; \ + directory=$$(dirname "$$directory"); \ + done; \ + fi) +ifeq ($(strip $(RUSTICOL_CONFIG_PATH)),) +$(error rusticol-config is unavailable; activate the pyAmpliCol environment or set RUSTICOL_CONFIG=/absolute/path) +endif +include providers.mk +CC ?= cc +# Make's abspath/notdir functions split on spaces; the shell preserves a path. +ARTIFACT_DIR := $(shell cd ../.. && pwd -P) +BUILD_DIR ?= $(shell dirname "$(ARTIFACT_DIR)")/.pyamplicol-api-build/$(shell basename "$(ARTIFACT_DIR)")/umami +empty := +space := $(empty) $(empty) +make_path = $(subst $(space),\$(space),$(1)) +BUILD_TARGET := $(call make_path,$(BUILD_DIR)) +CFLAGS ?= -O2 +CFLAGS += -std=c11 -Wall -Wextra -pedantic +SDK_LIBRARY := $(shell "$(RUSTICOL_CONFIG_PATH)" --library) +ifeq ($(strip $(SDK_LIBRARY)),) +$(error rusticol-config could not resolve the native library) +endif +SDK_CFLAGS := $(shell "$(RUSTICOL_CONFIG_PATH)" --cflags) +SDK_LIBS := $(shell "$(RUSTICOL_CONFIG_PATH)" --libs) +SDK_LIBRARY_TARGET := $(call make_path,$(SDK_LIBRARY)) +HOST_SYSTEM := $(shell uname -s) +ifeq ($(HOST_SYSTEM),Darwin) +SHARED_FLAGS = -dynamiclib -Wl,-install_name,@rpath/$(shell basename "$@") -Wl,-exported_symbols_list,umami.exports.macos +DRIVER_FLAGS = -Wl,-rpath,@loader_path +else ifeq ($(HOST_SYSTEM),Linux) +SHARED_FLAGS = -shared -Wl,-z,defs -Wl,-soname,$(shell basename "$@") -Wl,--version-script=umami.exports +DRIVER_FLAGS = -Wl,-rpath,'$$ORIGIN' +else +$(error UMAMI shared-library examples support Linux and macOS) +endif +ifeq ($(UMAMI_SINGLE_PROVIDER),1) +LIBRARY_NAMES := libumami.so +DRIVER_NAMES := umami_driver +RUN_DRIVER := $(BUILD_DIR)/umami_driver +else +LIBRARY_NAMES := $(addprefix libumami_,$(addsuffix .so,$(UMAMI_PROVIDERS))) +DRIVER_NAMES := $(addprefix umami_driver_,$(UMAMI_PROVIDERS)) +PROVIDER ?= $(firstword $(UMAMI_PROVIDERS)) +RUN_DRIVER := $(BUILD_DIR)/umami_driver_$(PROVIDER) +endif +LIBRARIES := $(addprefix $(BUILD_TARGET)/,$(LIBRARY_NAMES)) +DRIVERS := $(addprefix $(BUILD_TARGET)/,$(DRIVER_NAMES)) + +.PHONY: all shared run clean +all: $(LIBRARIES) $(DRIVERS) +shared: $(LIBRARIES) + +$(BUILD_TARGET): + mkdir -p "$@" + +# Explicit generated rules also work with spaces in directories on older Make +# versions, whose pattern-rule handling does not preserve escaped spaces. +define provider_rules +$(BUILD_TARGET)/libumami$(2).so: umami.c provider_$(1).h umami_provider.h umami.h umami.exports umami.exports.macos $(SDK_LIBRARY_TARGET) | $(BUILD_TARGET) + $$(CC) $$(CPPFLAGS) $$(SDK_CFLAGS) $$(CFLAGS) -fPIC -DUMAMI_PROVIDER_HEADER='"provider_$(1).h"' $$(SHARED_FLAGS) "$$<" -o "$$@" $$(LDFLAGS) $$(SDK_LIBS) + +$(BUILD_TARGET)/umami_driver$(2): umami_driver.c $(BUILD_TARGET)/libumami$(2).so provider_$(1).h umami_provider.h umami.h | $(BUILD_TARGET) + $$(CC) $$(CPPFLAGS) $$(CFLAGS) -DUMAMI_PROVIDER_HEADER='"provider_$(1).h"' "$$<" "$(BUILD_DIR)/libumami$(2).so" -o "$$@" $$(LDFLAGS) $$(DRIVER_FLAGS) +endef +ifeq ($(UMAMI_SINGLE_PROVIDER),1) +$(eval $(call provider_rules,p0,)) +else +$(foreach provider,$(UMAMI_PROVIDERS),$(eval $(call provider_rules,$(provider),_$(provider)))) +endif + +run: $(call make_path,$(RUN_DRIVER)) + "$(RUN_DRIVER)" "$(ARTIFACT_DIR)" $(ARGS) + +clean: + $(RM) $(foreach name,$(LIBRARY_NAMES) $(DRIVER_NAMES),"$(BUILD_DIR)/$(name)") diff --git a/pyamplicol/src/pyamplicol/assets/api_templates/umami/Makefile.reweight b/pyamplicol/src/pyamplicol/assets/api_templates/umami/Makefile.reweight new file mode 100644 index 0000000000..225709e043 --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/api_templates/umami/Makefile.reweight @@ -0,0 +1,35 @@ +# SPDX-License-Identifier: 0BSD +RUSTICOL_CONFIG ?= rusticol-config +RUSTICOL_CONFIG_PATH := $(shell command -v "$(RUSTICOL_CONFIG)" 2>/dev/null) +ifeq ($(strip $(RUSTICOL_CONFIG_PATH)),) +$(error rusticol-config is unavailable; activate the pyAmpliCol environment or set RUSTICOL_CONFIG=/absolute/path) +endif +include providers.mk +CC ?= cc +BUILD_DIR ?= build +CFLAGS ?= -O2 +CFLAGS += -std=c11 -Wall -Wextra -pedantic +SDK_LIBRARY := $(shell "$(RUSTICOL_CONFIG_PATH)" --library) +SDK_CFLAGS := $(shell "$(RUSTICOL_CONFIG_PATH)" --cflags) +SDK_LIBS := $(shell "$(RUSTICOL_CONFIG_PATH)" --libs) +HOST_SYSTEM := $(shell uname -s) +ifeq ($(HOST_SYSTEM),Darwin) +SHARED_FLAGS = -dynamiclib -Wl,-install_name,@rpath/$(notdir $@) -Wl,-exported_symbols_list,umami.exports.macos +else ifeq ($(HOST_SYSTEM),Linux) +SHARED_FLAGS = -shared -Wl,-z,defs -Wl,-soname,$(notdir $@) -Wl,--version-script=umami.exports +else +$(error UMAMI shared libraries support Linux and macOS) +endif +LIBRARIES := $(addprefix $(BUILD_DIR)/libreweight_,$(addsuffix .so,$(UMAMI_PROVIDERS))) + +.PHONY: all shared clean +all shared: $(LIBRARIES) + +$(BUILD_DIR): + mkdir -p "$@" + +$(BUILD_DIR)/libreweight_%.so: umami_reweight.c reweight_%.h reweight_provider.h umami.h $(SDK_LIBRARY) | $(BUILD_DIR) + $(CC) $(CPPFLAGS) $(SDK_CFLAGS) $(CFLAGS) -fPIC -DUMAMI_REWEIGHT_HEADER='"reweight_$*.h"' $(SHARED_FLAGS) "$<" -o "$@" $(LDFLAGS) $(SDK_LIBS) + +clean: + $(RM) $(LIBRARIES) diff --git a/pyamplicol/src/pyamplicol/assets/api_templates/umami/README.md b/pyamplicol/src/pyamplicol/assets/api_templates/umami/README.md new file mode 100644 index 0000000000..49c2f74bb4 --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/api_templates/umami/README.md @@ -0,0 +1,76 @@ +# UMAMI provider + +Build with the pyAmpliCol environment active: + +```sh +make +make run +``` + +`rusticol-config --cflags` and `--libs` locate the installed native SDK. Set +`RUSTICOL_CONFIG=/absolute/path/to/rusticol-config` if needed. Build products go +to the sibling `.pyamplicol-api-build//umami` directory, not into the +artifact payload; override `BUILD_DIR` to choose another location. + +A single provider produces `libumami.so` and `umami_driver`. Multiple providers +produce `libumami_p0.so`, `umami_driver_p0`, etc.; select `PROVIDER=p0` with +`make run`. `metadata.json` identifies the available providers and their +compatible external states. The driver takes `ABSOLUTE_ARTIFACT [CHANNEL [FLAVOUR]]` +and evaluates a saved validation point. + +The shared library statically includes Rusticol. The driver links only that +shared library, with a relative loader search path; keep the driver and library +together when moving them. Runtime does not require Python. Retain all artifact +files and their native evaluator dependencies. The matching SDK is needed only +when compiling the provider. + +Unlike the standard parameter-card convention, `umami_initialize` requires the +absolute artifact directory in `param_card_path`. Saved defaults initialize each +independent handle; `umami_set_parameter` updates mutable model parameters through +Rusticol. Optional per-event alpha_s is temporary and restored before returning. +The interface reports unsupported alpha_s when the model has no explicit mapping. +`UMAMI_META_MASSES` describes the saved default masses, not later handle-local +parameter changes. + +Momenta use `input[event + stride*(particle + particle_count*component)]`, with +components `(E,px,py,pz)`. Channel and flavour indices are zero-based. The returned +matrix element includes the native incoming averaging and identical-particle +normalization. It sums the artifact's available helicities. A random helicity +input produces only an informational label; it does not sample the matrix element. +Explicit helicity selection, random colour selection, diagrams and GPU streams +are not supported by this adapter. LC rows select physical additive colour flows; +NLC/full-colour rows use the complete contracted colour result, without a flow. +These contracted providers do not support colour-count metadata or colour-index +output; the corresponding capability query reports this explicitly. +Integration multiplicities in metadata are not silently included in the returned +matrix element. Consult metadata for restricted coverage and grouping assumptions. + +UMAMI automatically groups proven equivalent contributions; there is no grouping +setting. Compact integration requires permutation-invariant, flavour-blind cuts +and observables, recorded in `grouping.assumptions` alongside contribution +counters. Identical-particle integration orbits are derived from final-state PDGs, +oriented physical colour topology and permutations that preserve available +helicity coverage. They do not add a reflection quotient or +exchange incoming beams. With complete LC coverage, `g g > g g g` has four +representatives for 24 physical colour contributions, each with integration +multiplicity six. Full colour remains one contracted contribution with no extra +factorial multiplier; native identical-particle normalization is already included. + +Proven equivalent contributions with different physical final-state flavours +are also grouped. Equivalence uses model expressions and generated computation, +not equal default parameters or Standard Model-specific particle rules. +Unproved relations remain separate. The one exception is generation with +`--flavor-blind`: its ungenerated flavour-class members are evaluated through +their representative's runtime, and each provider exporting them records that +assumption in `grouping.assumptions`. + +Integration multiplicities require cuts invariant under the recorded particle +permutations; they are not factors for pointwise sums. Metadata retains physical +members and their momentum mappings. Arbitrary labelled-leg or species-sensitive +cuts require expanding those members and applying each cut to its physical +contribution. Grouping never assumes equal PDFs; physical incoming PDGs remain +oriented and retain their own PDF weights. + +The public header is copied from MadGraph7's UMAMI interface. Its copyright and +license terms are retained in `UMAMI_LICENSE`; distribute that notice alongside +compiled providers. The pyAmpliCol adapter and driver are distributed under 0BSD. diff --git a/pyamplicol/src/pyamplicol/assets/api_templates/umami/UMAMI_LICENSE b/pyamplicol/src/pyamplicol/assets/api_templates/umami/UMAMI_LICENSE new file mode 100644 index 0000000000..55c561ffc2 --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/api_templates/umami/UMAMI_LICENSE @@ -0,0 +1,41 @@ +The MadGraph7 software comes with the following Open Source License, +adapted from the University of Illinois/NCSA license. + +Copyright (c) 2009, 2013, the MadTeam. All rights reserved. + +Developed by the "MadTeam", i.e. + +Johan ALWALL +Rikkert FREDERIX +Stefano FRIXIONE +Michel HERQUET +Valentin HIRSCHI +Fabio MALTONI +Olivier MATTELAER +Tim STELZER +Paolo TORRIELLI +Marco ZARO + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal with the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimers. + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimers in the + documentation and/or other materials provided with the distribution. + 3. Neither the name "MadTeam", the names of the authors, their institution, + nor the names of its contributors may be used to endorse or promote + products derived from this Software without specific prior written + permission. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +CONTRIBUTORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +WITH THE SOFTWARE. diff --git a/pyamplicol/src/pyamplicol/assets/api_templates/umami/reweight_provider.h b/pyamplicol/src/pyamplicol/assets/api_templates/umami/reweight_provider.h new file mode 100644 index 0000000000..b9a34d4926 --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/api_templates/umami/reweight_provider.h @@ -0,0 +1,41 @@ +/* SPDX-License-Identifier: 0BSD */ +#ifndef PYAMPLICOL_UMAMI_REWEIGHT_PROVIDER_H +#define PYAMPLICOL_UMAMI_REWEIGHT_PROVIDER_H +#include +#include + +typedef struct { + const char *key; + const int *pdgs; + const char *alpha_s_name; + size_t helicity_count; +} ReweightProcess; + +typedef struct { + size_t reference_process, target_process; + /* runtime[j] = input[permutation[j]], including both incoming legs. */ + const size_t *permutation; + /* Exported helicity index -> runtime index of each process, -1 if absent. */ + const int *reference_helicity, *target_helicity; +} ReweightFlavour; + +typedef struct { + size_t flavour_count; + const ReweightFlavour *flavours; +} ReweightChannel; + +typedef struct { + const char *id; + size_t particle_count, exported_helicity_count; + /* Artifact directories relative to the directory passed to umami_initialize. */ + const char *reference_artifact, *target_artifact; + const char *reference_artifact_id, *target_artifact_id; + size_t reference_process_count; + const ReweightProcess *reference_processes; + size_t target_process_count; + const ReweightProcess *target_processes; + size_t channel_count; + const ReweightChannel *channels; + bool supports_alpha_s; +} ReweightProvider; +#endif diff --git a/pyamplicol/src/pyamplicol/assets/api_templates/umami/umami.c b/pyamplicol/src/pyamplicol/assets/api_templates/umami/umami.c new file mode 100644 index 0000000000..be6f8e7d27 --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/api_templates/umami/umami.c @@ -0,0 +1,437 @@ +/* SPDX-License-Identifier: 0BSD */ +#include "umami.h" +#include "umami_provider.h" +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef UMAMI_PROVIDER_HEADER +#define UMAMI_PROVIDER_HEADER "provider_p0.h" +#endif +#include UMAMI_PROVIDER_HEADER + +typedef struct { + char *name; + double real, imaginary; +} Override; + +typedef struct { + char *artifact; + RusticolRuntimeHandle **runtimes; + double *momenta; + double *helicity_values; + /* Parameters set so far, applied to the processes loaded later. */ + Override *overrides; + size_t override_count; +} Instance; + +static bool const supported_inputs[UMAMI_INPUT_KEY_COUNT] = { + [UMAMI_IN_MOMENTA] = true, + [UMAMI_IN_ALPHA_S] = UMAMI_HAS_ALPHA_S, + [UMAMI_IN_FLAVOR_INDEX] = true, + [UMAMI_IN_RANDOM_HELICITY] = true, + [UMAMI_IN_HELICITY_INDEX] = true, + [UMAMI_IN_CHANNEL_INDEX] = true, +}; +static bool const required_inputs[UMAMI_INPUT_KEY_COUNT] = { + [UMAMI_IN_MOMENTA] = true, +}; +static bool const supported_outputs[UMAMI_OUTPUT_KEY_COUNT] = { + [UMAMI_OUT_MATRIX_ELEMENT] = true, + [UMAMI_OUT_COLOR_INDEX] = UMAMI_HAS_COLOR_FLOW, + [UMAMI_OUT_HELICITY_INDEX] = true, +}; + +static UmamiStatus native_error(int status) { + if (status == RUSTICOL_STATUS_OK) return UMAMI_SUCCESS; + char message[1024]; + size_t required = 0; + if (rusticol_last_error_message(message, sizeof(message), &required) == + RUSTICOL_STATUS_OK) fprintf(stderr, "umami: %s\n", message); + return UMAMI_ERROR; +} + +static UmamiParameter const *parameter(size_t process, char const *name) { + UmamiProcess const *entry = &umami_provider.processes[process]; + for (size_t i = 0; i < entry->parameter_count; ++i) + if (strcmp(entry->parameters[i].name, name) == 0) return &entry->parameters[i]; + return NULL; +} + +static UmamiStatus load_process(Instance *instance, size_t process) { + if (instance->runtimes[process] != NULL) return UMAMI_SUCCESS; + UmamiProcess const *entry = &umami_provider.processes[process]; + RusticolRuntimeHandle *runtime = NULL; + int status = rusticol_runtime_load(instance->artifact, entry->key, NULL, &runtime); + if (status != RUSTICOL_STATUS_OK) return native_error(status); + size_t count = 0, required = 0; + char accuracy[16]; + status = rusticol_runtime_external_count(runtime, &count); + bool compatible = status == RUSTICOL_STATUS_OK && count == umami_provider.particle_count; + char const *expected_identity = UMAMI_ARTIFACT_ID; + if (compatible && expected_identity != NULL) { + size_t capacity = strlen(expected_identity) + 1; + char *identity = malloc(capacity); + if (identity == NULL) compatible = false; + else { + status = rusticol_runtime_artifact_id(runtime, identity, capacity, &required); + compatible = status == RUSTICOL_STATUS_OK && strcmp(identity, expected_identity) == 0; + free(identity); + } + } + for (size_t j = 0; compatible && j < count; ++j) { + int32_t pdg = 0; + status = rusticol_runtime_external_pdg(runtime, j, &pdg); + compatible = status == RUSTICOL_STATUS_OK && pdg == entry->pdgs[j]; + } + if (compatible) { + status = rusticol_runtime_color_accuracy(runtime, accuracy, sizeof(accuracy), &required); + compatible = status == RUSTICOL_STATUS_OK && strcmp(accuracy, entry->color_accuracy) == 0; + } + if (compatible) { + status = rusticol_runtime_helicity_count(runtime, &count); + compatible = status == RUSTICOL_STATUS_OK && count == entry->helicity_count; + } + for (size_t j = 0; compatible && j < entry->helicity_count; ++j) { + size_t capacity = strlen(entry->helicity_ids[j]) + 1; + char *identifier = malloc(capacity); + if (identifier == NULL) { compatible = false; break; } + status = rusticol_runtime_helicity_id(runtime, j, identifier, capacity, &required); + compatible = status == RUSTICOL_STATUS_OK && strcmp(identifier, entry->helicity_ids[j]) == 0; + free(identifier); + } + if (!compatible) { + (void)rusticol_runtime_free(runtime); + fprintf(stderr, "umami: artifact is incompatible with provider %s, process %s\n", + umami_provider.id, entry->key); + return UMAMI_ERROR; + } + for (size_t i = 0; i < instance->override_count; ++i) { + Override const *override = &instance->overrides[i]; + UmamiParameter const *known = parameter(process, override->name); + if (known == NULL) continue; + status = rusticol_runtime_set_model_parameter(runtime, override->name, + override->real, known->is_complex ? override->imaginary : 0.0); + if (status != RUSTICOL_STATUS_OK) { + (void)rusticol_runtime_free(runtime); + return native_error(status); + } + } + instance->runtimes[process] = runtime; + return UMAMI_SUCCESS; +} + +static UmamiStatus record_override(Instance *instance, char const *name, + double real, double imaginary) { + for (size_t i = 0; i < instance->override_count; ++i) + if (strcmp(instance->overrides[i].name, name) == 0) { + instance->overrides[i].real = real; + instance->overrides[i].imaginary = imaginary; + return UMAMI_SUCCESS; + } + Override *overrides = realloc(instance->overrides, + (instance->override_count + 1) * sizeof(*overrides)); + if (overrides == NULL) return UMAMI_ERROR; + instance->overrides = overrides; + char *copy = malloc(strlen(name) + 1); + if (copy == NULL) return UMAMI_ERROR; + strcpy(copy, name); + overrides[instance->override_count++] = (Override){copy, real, imaginary}; + return UMAMI_SUCCESS; +} + +UmamiStatus umami_get_meta(UmamiMetaKey key, void *result) { + if (result == NULL) return UMAMI_ERROR; + switch (key) { + case UMAMI_META_DEVICE: *(UmamiDevice *)result = UMAMI_DEVICE_CPU; break; + case UMAMI_META_PARTICLE_COUNT: *(int *)result = (int)umami_provider.particle_count; break; + case UMAMI_META_HELICITY_COUNT: *(int *)result = (int)umami_provider.helicity_count; break; + case UMAMI_META_COLOR_COUNT: + if (!UMAMI_HAS_COLOR_FLOW) return UMAMI_ERROR_UNSUPPORTED_META; + *(int *)result = (int)umami_provider.color_count; break; + case UMAMI_META_MASSES: + memcpy(result, umami_provider.masses, umami_provider.particle_count * sizeof(double)); + break; + default: return UMAMI_ERROR_UNSUPPORTED_META; + } + return UMAMI_SUCCESS; +} + +UmamiStatus umami_supported_inputs(bool const **supported, int *count) { + if (supported == NULL || count == NULL) return UMAMI_ERROR; + *supported = supported_inputs; *count = UMAMI_INPUT_KEY_COUNT; + return UMAMI_SUCCESS; +} +UmamiStatus umami_required_inputs(bool const **required, int *count) { + if (required == NULL || count == NULL) return UMAMI_ERROR; + *required = required_inputs; *count = UMAMI_INPUT_KEY_COUNT; + return UMAMI_SUCCESS; +} +UmamiStatus umami_supported_outputs(bool const **supported, int *count) { + if (supported == NULL || count == NULL) return UMAMI_ERROR; + *supported = supported_outputs; *count = UMAMI_OUTPUT_KEY_COUNT; + return UMAMI_SUCCESS; +} + +UmamiStatus umami_initialize(UmamiHandle *handle, char const *param_card_path) { + if (handle == NULL) return UMAMI_ERROR; + *handle = NULL; + struct stat info; + if (param_card_path == NULL || param_card_path[0] != '/' || + stat(param_card_path, &info) != 0 || !S_ISDIR(info.st_mode) || + umami_provider.process_count == 0 || umami_provider.particle_count > INT_MAX || + umami_provider.particle_count > SIZE_MAX / (4 * sizeof(double))) return UMAMI_ERROR; + Instance *instance = calloc(1, sizeof(*instance)); + if (instance == NULL) return UMAMI_ERROR; + size_t length = strlen(param_card_path) + 1; + instance->artifact = malloc(length); + instance->runtimes = calloc(umami_provider.process_count, sizeof(*instance->runtimes)); + instance->momenta = calloc(4 * umami_provider.particle_count, sizeof(double)); + instance->helicity_values = calloc(umami_provider.helicity_count + 1, sizeof(double)); + if (instance->artifact == NULL || instance->runtimes == NULL || instance->momenta == NULL || + instance->helicity_values == NULL) { + umami_free(instance); return UMAMI_ERROR; + } + memcpy(instance->artifact, param_card_path, length); + /* Validate the artifact now; other processes are loaded lazily on first use. */ + UmamiStatus status = load_process(instance, 0); + if (status != UMAMI_SUCCESS) { umami_free(instance); return status; } + *handle = instance; + return UMAMI_SUCCESS; +} + +UmamiStatus umami_get_parameter(UmamiHandle handle, char const *name, + double *real, double *imaginary) { + if (handle == NULL || name == NULL || real == NULL) return UMAMI_ERROR; + Instance *instance = handle; + for (size_t i = 0; i < umami_provider.process_count; ++i) { + UmamiParameter const *entry = parameter(i, name); + if (entry == NULL) continue; + if (entry->is_complex && imaginary == NULL) return UMAMI_ERROR; + UmamiStatus status = load_process(instance, i); + if (status != UMAMI_SUCCESS) return status; + double re, im; + status = native_error(rusticol_runtime_get_model_parameter(instance->runtimes[i], name, &re, &im)); + if (status != UMAMI_SUCCESS) return status; + *real = re; + if (imaginary != NULL) *imaginary = im; + return UMAMI_SUCCESS; + } + return UMAMI_ERROR; +} + +UmamiStatus umami_set_parameter(UmamiHandle handle, char const *name, + double real, double imaginary) { + if (handle == NULL || name == NULL || !isfinite(real)) return UMAMI_ERROR; + Instance *instance = handle; + size_t count = umami_provider.process_count; + double *previous = calloc(count * 2, sizeof(double)); + bool *changed = calloc(count, sizeof(bool)); + if (previous == NULL || changed == NULL) { free(previous); free(changed); return UMAMI_ERROR; } + UmamiStatus status = UMAMI_ERROR; + bool found = false; + for (size_t i = 0; i < count; ++i) { + UmamiParameter const *entry = parameter(i, name); + if (entry == NULL) continue; + /* Load the first process using it, so that the value is validated now. */ + if (!found && load_process(instance, i) != UMAMI_SUCCESS) goto rollback; + found = true; + if (!entry->mutable || (entry->is_complex && !isfinite(imaginary))) goto rollback; + if (instance->runtimes[i] == NULL) continue; + status = native_error(rusticol_runtime_get_model_parameter( + instance->runtimes[i], name, &previous[2*i], &previous[2*i+1])); + if (status != UMAMI_SUCCESS) goto rollback; + status = native_error(rusticol_runtime_set_model_parameter( + instance->runtimes[i], name, real, entry->is_complex ? imaginary : 0.0)); + if (status != UMAMI_SUCCESS) goto rollback; + changed[i] = true; + } + if (found && record_override(instance, name, real, imaginary) != UMAMI_SUCCESS) + goto rollback; + free(previous); free(changed); + return found ? UMAMI_SUCCESS : UMAMI_ERROR; +rollback: + for (size_t i = 0; i < count; ++i) + if (changed[i]) (void)rusticol_runtime_set_model_parameter( + instance->runtimes[i], name, previous[2*i], previous[2*i+1]); + free(previous); free(changed); + return UMAMI_ERROR; +} + +/* Total of one contribution, at a fixed runtime helicity if helicity is not NULL. With + * values, the values of all runtime helicities are written there and summed. */ +static UmamiStatus evaluate_contribution(Instance *instance, UmamiContribution const *entry, + double const *momenta, size_t event, size_t stride, + double const *alpha_s, uint32_t const *helicity, + double *values, double *result) { + size_t process = entry->process_index; + UmamiProcess const *description = &umami_provider.processes[process]; + UmamiStatus status = load_process(instance, process); + if (status != UMAMI_SUCCESS) return status; + RusticolRuntimeHandle *runtime = instance->runtimes[process]; + size_t n = umami_provider.particle_count; + for (size_t j = 0; j < n; ++j) + for (size_t k = 0; k < 4; ++k) { + double value = momenta[event + stride * (n*k + entry->permutation[j])]; + if (!isfinite(value)) return UMAMI_ERROR; + instance->momenta[4*j+k] = value; + } + double previous_real = 0.0, previous_imaginary = 0.0; + bool changed = false; + if (alpha_s != NULL) { + if (description->alpha_s_name == NULL) return UMAMI_ERROR_UNSUPPORTED_INPUT; + if (!isfinite(*alpha_s) || *alpha_s < 0.0) return UMAMI_ERROR; + status = native_error(rusticol_runtime_get_model_parameter( + runtime, description->alpha_s_name, &previous_real, &previous_imaginary)); + if (status != UMAMI_SUCCESS) return status; + changed = previous_real != *alpha_s || previous_imaginary != 0.0; + if (changed) { + status = native_error(rusticol_runtime_set_model_parameter( + runtime, description->alpha_s_name, *alpha_s, 0.0)); + if (status != UMAMI_SUCCESS) return status; + } + } + int native_status; + if (values != NULL) { + size_t helicity_count = 0, color_count = 0; + native_status = rusticol_runtime_evaluate_resolved_f64( + runtime, instance->momenta, 4*n, 1, NULL, 0, + entry->color_id == NULL ? NULL : &entry->color_id, + entry->color_id == NULL ? 0 : 1, + values, description->helicity_count, &helicity_count, &color_count); + if (native_status == RUSTICOL_STATUS_OK && + (helicity_count != description->helicity_count || color_count != 1)) { + fprintf(stderr, "umami: unexpected resolved shape of process %s\n", description->key); + native_status = RUSTICOL_STATUS_RUNTIME_ERROR; + } + *result = 0.0; + for (size_t h = 0; native_status == RUSTICOL_STATUS_OK && h < helicity_count; ++h) + *result += values[h]; + } else { + native_status = rusticol_runtime_evaluate_selected_f64( + runtime, instance->momenta, 4*n, 1, + NULL, 0, + entry->color_id == NULL ? NULL : &entry->color_id, + entry->color_id == NULL ? 0 : 1, + helicity, helicity == NULL ? 0 : 1, NULL, 0, result, 1); + } + status = native_error(native_status); + if (changed) { + UmamiStatus restored = native_error(rusticol_runtime_set_model_parameter( + runtime, description->alpha_s_name, previous_real, previous_imaginary)); + if (restored != UMAMI_SUCCESS) return restored; + } + return status; +} + +UmamiStatus umami_matrix_element(UmamiHandle handle, size_t count, size_t stride, size_t offset, + size_t input_count, UmamiInputKey const *input_keys, void const *const *inputs, + size_t output_count, UmamiOutputKey const *output_keys, void *const *outputs) { + if (handle == NULL || offset > stride || count > stride-offset || + (input_count && (input_keys == NULL || inputs == NULL)) || + (output_count && (output_keys == NULL || outputs == NULL)) || + (stride && umami_provider.particle_count > SIZE_MAX / stride / 4)) return UMAMI_ERROR; + void const *in[UMAMI_INPUT_KEY_COUNT] = {0}; + void *out[UMAMI_OUTPUT_KEY_COUNT] = {0}; + for (size_t i = 0; i < input_count; ++i) { + int key = input_keys[i]; + if (key < 0 || key >= UMAMI_INPUT_KEY_COUNT || !supported_inputs[key]) + return UMAMI_ERROR_UNSUPPORTED_INPUT; + if (inputs[i] == NULL || in[key] != NULL) return UMAMI_ERROR; + in[key] = inputs[i]; + } + for (size_t i = 0; i < output_count; ++i) { + int key = output_keys[i]; + if (key < 0 || key >= UMAMI_OUTPUT_KEY_COUNT || !supported_outputs[key]) + return UMAMI_ERROR_UNSUPPORTED_OUTPUT; + if (outputs[i] == NULL || out[key] != NULL) return UMAMI_ERROR; + out[key] = outputs[i]; + } + if (count == 0) return UMAMI_SUCCESS; + if (in[UMAMI_IN_MOMENTA] == NULL) return UMAMI_ERROR_MISSING_INPUT; + Instance *instance = handle; + for (size_t event = offset; event < offset+count; ++event) { + size_t channel = in[UMAMI_IN_CHANNEL_INDEX] ? + ((unsigned int const *)in[UMAMI_IN_CHANNEL_INDEX])[event] : 0; + int flavour = in[UMAMI_IN_FLAVOR_INDEX] ? + ((int const *)in[UMAMI_IN_FLAVOR_INDEX])[event] : 0; + if (channel >= umami_provider.channel_count || flavour < 0 || + (size_t)flavour >= umami_provider.channels[channel].flavour_count) return UMAMI_ERROR; + UmamiFlavour const *selection = &umami_provider.channels[channel].flavours[flavour]; + if (selection->contribution_count == 0) return UMAMI_ERROR; + double random = in[UMAMI_IN_RANDOM_HELICITY] ? + ((double const *)in[UMAMI_IN_RANDOM_HELICITY])[event] : 0.5; + if (!isfinite(random) || random < 0.0 || random >= 1.0) return UMAMI_ERROR; + double const *alpha_s = + in[UMAMI_IN_ALPHA_S] ? &((double const *)in[UMAMI_IN_ALPHA_S])[event] : NULL; + UmamiContribution const *first = &selection->contributions[0]; + int label = 0; + double total = 0.0; + if (in[UMAMI_IN_HELICITY_INDEX]) { + /* a fixed exported helicity, absent ones do not contribute */ + label = ((int const *)in[UMAMI_IN_HELICITY_INDEX])[event]; + if (label < 0 || (size_t)label >= umami_provider.exported_helicity_count) + return UMAMI_ERROR; + for (size_t j = 0; j < selection->contribution_count; ++j) { + UmamiContribution const *entry = &selection->contributions[j]; + if (entry->helicity_runtime[label] < 0) continue; + uint32_t helicity = (uint32_t)entry->helicity_runtime[label]; + double value = 0.0; + UmamiStatus status = evaluate_contribution(instance, entry, + in[UMAMI_IN_MOMENTA], event, stride, alpha_s, &helicity, NULL, &value); + if (status != UMAMI_SUCCESS) return status; + total += entry->factor * value; + } + } else if (out[UMAMI_OUT_HELICITY_INDEX] && selection->contribution_count == 1) { + /* sample the helicity in proportion to its contribution */ + double *values = instance->helicity_values; + UmamiStatus status = evaluate_contribution(instance, first, + in[UMAMI_IN_MOMENTA], event, stride, alpha_s, NULL, values, &total); + if (status != UMAMI_SUCCESS) return status; + size_t count = umami_provider.processes[first->process_index].helicity_count; + double target = random * total, cumulative = 0.0; + size_t chosen = 0; + for (size_t h = 0; h < count; ++h) { + if (values[h] <= 0.0) continue; + chosen = h; + cumulative += values[h]; + if (cumulative > target) break; + } + label = (int)first->helicity_export[chosen]; + total *= first->factor; + } else { + if (out[UMAMI_OUT_HELICITY_INDEX]) return UMAMI_ERROR_UNSUPPORTED_OUTPUT; + for (size_t j = 0; j < selection->contribution_count; ++j) { + double value = 0.0; + UmamiStatus status = evaluate_contribution(instance, &selection->contributions[j], + in[UMAMI_IN_MOMENTA], event, stride, alpha_s, NULL, NULL, &value); + if (status != UMAMI_SUCCESS) return status; + total += selection->contributions[j].factor * value; + } + } + if (!isfinite(total)) return UMAMI_ERROR; + if (out[UMAMI_OUT_MATRIX_ELEMENT]) ((double *)out[UMAMI_OUT_MATRIX_ELEMENT])[event] = total; + if (out[UMAMI_OUT_HELICITY_INDEX]) ((int *)out[UMAMI_OUT_HELICITY_INDEX])[event] = label; + if (out[UMAMI_OUT_COLOR_INDEX]) ((int *)out[UMAMI_OUT_COLOR_INDEX])[event] = + (int)first->color_index; + } + return UMAMI_SUCCESS; +} + +UmamiStatus umami_free(UmamiHandle handle) { + if (handle == NULL) return UMAMI_SUCCESS; + Instance *instance = handle; + if (instance->runtimes != NULL) + for (size_t i = 0; i < umami_provider.process_count; ++i) + if (instance->runtimes[i] != NULL) (void)rusticol_runtime_free(instance->runtimes[i]); + for (size_t i = 0; i < instance->override_count; ++i) free(instance->overrides[i].name); + free(instance->overrides); + free(instance->runtimes); free(instance->momenta); free(instance->helicity_values); + free(instance->artifact); free(instance); + return UMAMI_SUCCESS; +} diff --git a/pyamplicol/src/pyamplicol/assets/api_templates/umami/umami.exports b/pyamplicol/src/pyamplicol/assets/api_templates/umami/umami.exports new file mode 100644 index 0000000000..5fb36d6d48 --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/api_templates/umami/umami.exports @@ -0,0 +1,13 @@ +UMAMI_1.0 { + global: + umami_get_meta; + umami_supported_inputs; + umami_required_inputs; + umami_supported_outputs; + umami_initialize; + umami_set_parameter; + umami_get_parameter; + umami_matrix_element; + umami_free; + local: *; +}; diff --git a/pyamplicol/src/pyamplicol/assets/api_templates/umami/umami.exports.macos b/pyamplicol/src/pyamplicol/assets/api_templates/umami/umami.exports.macos new file mode 100644 index 0000000000..8fbe7e93fb --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/api_templates/umami/umami.exports.macos @@ -0,0 +1,9 @@ +_umami_get_meta +_umami_supported_inputs +_umami_required_inputs +_umami_supported_outputs +_umami_initialize +_umami_set_parameter +_umami_get_parameter +_umami_matrix_element +_umami_free diff --git a/pyamplicol/src/pyamplicol/assets/api_templates/umami/umami.h b/pyamplicol/src/pyamplicol/assets/api_templates/umami/umami.h new file mode 100644 index 0000000000..4eb2dba380 --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/api_templates/umami/umami.h @@ -0,0 +1,324 @@ +/* + * _ + * (_) + * _ _ _ __ ___ __ _ _ __ ___ _ + * | | | | '_ ` _ \ / _` | '_ ` _ \| | + * | |_| | | | | | | (_| | | | | | | | + * \__,_|_| |_| |_|\__,_|_| |_| |_|_| + * + * Unified MAtrix eleMent Interface + * + * + */ + +#ifndef UMAMI_HEADER +#define UMAMI_HEADER 1 + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Major version number of the UMAMI interface. If the major version is the same + * between caller and implementation, binary compatibility is ensured. + */ +#define UMAMI_MAJOR_VERSION 1 +/** + * Minor version number of the UMAMI interface. Between minor versions, new keys for + * errors, devices, metadata, inputs and outputs can be added. + */ +#define UMAMI_MINOR_VERSION 2 + +typedef enum { + /** operation was executed successfully */ + UMAMI_SUCCESS, + /** an unspecified error */ + UMAMI_ERROR, + /** operation not implemented */ + UMAMI_ERROR_NOT_IMPLEMENTED, + /** the provided input key is not supported by this matrix element implementation */ + UMAMI_ERROR_UNSUPPORTED_INPUT, + /** the provided output key is not supported by this matrix element implementation + */ + UMAMI_ERROR_UNSUPPORTED_OUTPUT, + /** the meta_key was not yet initialized */ + UMAMI_ERROR_UNINITIALIZED_META, + /** the provided metadata key is not supported by this matrix element implementation + */ + UMAMI_ERROR_UNSUPPORTED_META, + /** a mandatory matrix element input was not provided */ + UMAMI_ERROR_MISSING_INPUT, +} UmamiStatus; + +typedef enum { + UMAMI_DEVICE_CPU, + UMAMI_DEVICE_CUDA, + UMAMI_DEVICE_HIP, +} UmamiDevice; + +typedef enum { + /** `UmamiDevice` specifying the type of device */ + UMAMI_META_DEVICE, + /** `int` specifying the number of external particles */ + UMAMI_META_PARTICLE_COUNT, + /** `int` specifying the number of Feynman diagrams */ + UMAMI_META_DIAGRAM_COUNT, + /** `int` specifying the number of helicities */ + UMAMI_META_HELICITY_COUNT, + /** `int` specifying the number of colors */ + UMAMI_META_COLOR_COUNT, + /** `double` array of length particle count: the external-leg masses (GeV) */ + UMAMI_META_MASSES, +} UmamiMetaKey; + +typedef enum { + /** momenta of the external legs, type: `double`, shape: `(particle count, 4)` */ + UMAMI_IN_MOMENTA, + /** value for the strong coupling, type: `double`, shape: `()` */ + UMAMI_IN_ALPHA_S, + /** flavor index, type: `int`, shape: `()` */ + UMAMI_IN_FLAVOR_INDEX, + /** random number for color selection, type: `double`, shape: `()` */ + UMAMI_IN_RANDOM_COLOR, + /** random number for helicity selection, type: `double`, shape: `()` */ + UMAMI_IN_RANDOM_HELICITY, + /** random number for diagram selection, type: `double`, shape: `()` */ + UMAMI_IN_RANDOM_DIAGRAM, + /** externally selected helicity index, type: `int`, shape: `()` */ + UMAMI_IN_HELICITY_INDEX, + /** externally selected diagram index, type: `unsigned int`, shape: `()` */ + UMAMI_IN_DIAGRAM_INDEX, + /** externally selected channel index, type: `unsigned int`, shape: `()` */ + UMAMI_IN_CHANNEL_INDEX, + /** number of external legs, type: `int`, shape: `()` */ + UMAMI_IN_PARTICLE_COUNT, + /** PDG ids of the external legs, type: `int`, shape: `(particle count)` */ + UMAMI_IN_PDG_IDS, + /** momentum fraction of an incoming parton, type: `double`, shape: `()` */ + UMAMI_IN_MOMENTUM_FRACTION, + /** energy scale, e.g. the factorization scale, type: `double`, shape: `()` */ + UMAMI_IN_SCALE, + /** PDG id of a single parton, type: `int`, shape: `()` */ + UMAMI_IN_PDG_ID, + /** index of the beam (0 or 1), type: `int`, shape: `()` */ + UMAMI_IN_BEAM_INDEX, +} UmamiInputKey; + +/** Number of values in `UmamiInputKey` */ +#define UMAMI_INPUT_KEY_COUNT (UMAMI_IN_BEAM_INDEX + 1) + +typedef enum { + /** value of the matrix element, type: `double`, shape: `()` */ + UMAMI_OUT_MATRIX_ELEMENT, + /** squared amplitudes of the diagrams, type: `double`, shape: `(diagram count)` */ + UMAMI_OUT_DIAGRAM_AMP2, + /** selected color index, type: `int`, shape: `()` */ + UMAMI_OUT_COLOR_INDEX, + /** selected helicity index, type: `int`, shape: `()` */ + UMAMI_OUT_HELICITY_INDEX, + /** selected diagram index, type: `int`, shape: `()` */ + UMAMI_OUT_DIAGRAM_INDEX, + /** CUDA or HIP stream for asynchronous execution. Listed as an output as it is a + * mutable pointer */ + UMAMI_OUT_GPU_STREAM, + /** value of a custom cut, 0 rejects the event, type: `double`, shape: `()` */ + UMAMI_OUT_CUT, + /** value of a custom energy scale, type: `double`, shape: `()` */ + UMAMI_OUT_SCALE, + /** value x f(x, Q) of a custom parton density, type: `double`, shape: `()` */ + UMAMI_OUT_PDF, + /** value of a custom event bias, type: `double`, shape: `()` */ + UMAMI_OUT_BIAS, + /** ratio of the matrix element to a reference matrix element defined by the + * caller, type: `double`, shape: `()` */ + UMAMI_OUT_MATRIX_ELEMENT_RATIO, +} UmamiOutputKey; + +/** Number of values in `UmamiOutputKey` */ +#define UMAMI_OUTPUT_KEY_COUNT (UMAMI_OUT_MATRIX_ELEMENT_RATIO + 1) + +/** Implementation-defined pointer to a matrix element instance */ +typedef void* UmamiHandle; + +/** + * Retrieve metadata about the implemented matrix element + * + * @param meta_key + * key specifying the type of metadata to be retrieved + * @param result + * pointer to store the result. It's type depends on the metadata key + * @return + * UMAMI_SUCCESS on success, error code otherwise + */ +UmamiStatus umami_get_meta(UmamiMetaKey meta_key, void* result); + +/** + * Reports which input keys can be passed to this matrix element implementation, to + * be written by whoever implements the UMAMI interface. Implementing this function + * is optional; a caller that fails to resolve it should treat it as unavailable. + * + * @param supported + * pointer set on return to an implementation-owned array of booleans, where + * element `i` indicates whether the input key with numeric value `i` + * (see `UmamiInputKey`) is accepted by this implementation. + * @param count + * pointer set on return to the number of elements in `*supported`, reflecting + * the number of `UmamiInputKey` values known to this implementation. A caller + * built against a newer header should treat any key index beyond `*count` as + * not supported. + * @return + * UMAMI_SUCCESS on success, error code otherwise + */ +UmamiStatus umami_supported_inputs(bool const** supported, int* count); + +/** + * Reports which input keys must be passed to this matrix element implementation for + * `umami_matrix_element` to succeed, to be written by whoever implements the UMAMI + * interface. This is a subset of the keys reported by `umami_supported_inputs`. + * Implementing this function is optional; a caller that fails to resolve it should + * treat it as unavailable. + * + * @param required + * pointer set on return to an implementation-owned array of booleans, where + * element `i` indicates whether the input key with numeric value `i` + * (see `UmamiInputKey`) is mandatory for this implementation. + * @param count + * pointer set on return to the number of elements in `*required`. A caller + * built against a newer header should treat any key index beyond `*count` as + * not required. + * @return + * UMAMI_SUCCESS on success, error code otherwise + */ +UmamiStatus umami_required_inputs(bool const** required, int* count); + +/** + * Reports which output keys can be requested from this matrix element + * implementation, to be written by whoever implements the UMAMI interface. + * Implementing this function is optional; a caller that fails to resolve it should + * treat it as unavailable. + * + * @param supported + * pointer set on return to an implementation-owned array of booleans, where + * element `i` indicates whether the output key with numeric value `i` + * (see `UmamiOutputKey`) can be produced by this implementation. + * @param count + * pointer set on return to the number of elements in `*supported`. A caller + * built against a newer header should treat any key index beyond `*count` as + * not supported. + * @return + * UMAMI_SUCCESS on success, error code otherwise + */ +UmamiStatus umami_supported_outputs(bool const** supported, int* count); + +/** + * Creates an instance of the matrix element. Each instance is independent, so thread + * safety can be achieved by creating a separate one for every thread. + * + * @param handle + * pointer to an instance of the subprocess. Has to be cleaned up by + * the caller with `free_subprocess`. + * @param param_card_path + * path to the parameter file + * @return + * UMAMI_SUCCESS on success, error code otherwise + */ +UmamiStatus umami_initialize(UmamiHandle* handle, char const* param_card_path); + +/** + * Sets the value of a model parameter + * + * @param handle + * handle of a matrix element instance + * @param name + * name of the parameter + * @param parameter_real + * real part of the parameter value + * @param parameter_imag + * imaginary part of the parameter value. Ignored for real valued parameters. + * @return + * UMAMI_SUCCESS on success, error code otherwise + */ +UmamiStatus umami_set_parameter( + UmamiHandle handle, char const* name, double parameter_real, double parameter_imag +); + +/** + * Retrieves the value of a model parameter + * + * @param handle + * handle of a matrix element instance + * @param name + * name of the parameter + * @param parameter_real + * pointer to double to return real part of the parameter value + * @param parameter_imag + * pointer to double to return imaginary part of the parameter value. Ignored + * for real-valued parameters (i.e. you may pass a null pointer) + * @return + * UMAMI_SUCCESS on success, error code otherwise + */ +UmamiStatus umami_get_parameter( + UmamiHandle handle, char const* name, double* parameter_real, double* parameter_imag +); + +/** + * Evaluates the matrix element as a function of the given inputs, filling the + * requested outputs. Unless otherwise specified, all inputs and outputs have a + * column-major memory layout and have a batch dimension that is contiguous in memory. + * + * @param handle + * handle of a matrix element instance + * @param count + * number of events to evaluate the matrix element for + * @param stride + * stride of the batch dimension of the input and output arrays to simplify + * parallel execution on CPUs, see memory layout + * @param offset + * offset of the event index + * @param input_count + * number of inputs to the matrix element + * @param input_keys + * pointer to an array of input keys, length `input_count` + * @param inputs + * pointer to an array of void pointers to the inputs. The type of the inputs + * depends on the input key + * @param output_count + * number of outputs to the matrix element + * @param output_keys + * pointer to an array of output keys, length `output_count` + * @param outputs + * pointer to an array of void pointers to the outputs. The type of the outputs + * depends on the output key. The caller is responsible for allocating memory for + * the outputs. + * @return + * UMAMI_SUCCESS on success, error code otherwise + */ +UmamiStatus umami_matrix_element( + UmamiHandle handle, + size_t count, + size_t stride, + size_t offset, + size_t input_count, + UmamiInputKey const* input_keys, + void const* const* inputs, + size_t output_count, + UmamiOutputKey const* output_keys, + void* const* outputs +); + +/** + * Frees matrix element instance + * + * @param handle + * handle of a matrix element instance + */ +UmamiStatus umami_free(UmamiHandle handle); + +#ifdef __cplusplus +} +#endif + +#endif // UMAMI_HEADER diff --git a/pyamplicol/src/pyamplicol/assets/api_templates/umami/umami_driver.c b/pyamplicol/src/pyamplicol/assets/api_templates/umami/umami_driver.c new file mode 100644 index 0000000000..9a37dc217f --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/api_templates/umami/umami_driver.c @@ -0,0 +1,93 @@ +/* SPDX-License-Identifier: 0BSD */ +/* A normal linked executable: no dlopen, Python, or direct Rusticol linkage. */ +#include "umami.h" +#include "umami_provider.h" +#include +#include +#include +#include +#include +#include +#ifndef UMAMI_PROVIDER_HEADER +#define UMAMI_PROVIDER_HEADER "provider_p0.h" +#endif +#include UMAMI_PROVIDER_HEADER + +static void check(UmamiStatus status, char const *operation) { + if (status != UMAMI_SUCCESS) { + fprintf(stderr, "umami_driver: %s failed (status %d)\n", operation, status); + exit(EXIT_FAILURE); + } +} + +static unsigned int index_argument(char const *text) { + char *end = NULL; + errno = 0; + unsigned long value = strtoul(text, &end, 10); + if (errno || text == end || *end || text[0] == '-' || value > INT_MAX) { + fprintf(stderr, "invalid channel/flavour index: %s\n", text); + exit(EXIT_FAILURE); + } + return (unsigned int)value; +} + +int main(int argc, char **argv) { + if (argc < 2 || argc > 4) { + fprintf(stderr, "usage: %s ABSOLUTE_ARTIFACT [CHANNEL [FLAVOUR]]\n", argv[0]); + return EXIT_FAILURE; + } + unsigned int channel = argc > 2 ? index_argument(argv[2]) : 0; + int flavour = argc > 3 ? (int)index_argument(argv[3]) : 0; + if (channel >= umami_provider.channel_count || + (size_t)flavour >= umami_provider.channels[channel].flavour_count) { + fputs("channel/flavour out of range\n", stderr); return EXIT_FAILURE; + } + UmamiContribution const *entry = &umami_provider.channels[channel].flavours[flavour].contributions[0]; + char const *process = umami_provider.processes[entry->process_index].key; + int n = 0; + check(umami_get_meta(UMAMI_META_PARTICLE_COUNT, &n), "particle count"); + double *point = calloc((size_t)n * 4, sizeof(double)); + double *momenta = calloc((size_t)n * 4, sizeof(double)); + size_t path_length = strlen(argv[1]) + sizeof("/API/validation_points.dat"); + char *path = malloc(path_length); + if (point == NULL || momenta == NULL || path == NULL) return EXIT_FAILURE; + snprintf(path, path_length, "%s/API/validation_points.dat", argv[1]); + FILE *file = fopen(path, "r"); + if (file == NULL) { perror(path); return EXIT_FAILURE; } + char name[256]; + if (fscanf(file, "%255s", name) != 1 || strcmp(name, "RUSTICOL_VALIDATION_POINTS_V1")) { + fputs("invalid validation-point file\n", stderr); return EXIT_FAILURE; + } + bool found = false; + size_t particles; + while (fscanf(file, "%255s %zu", name, &particles) == 2) { + bool selected = strcmp(name, process) == 0 && particles == (size_t)n; + for (size_t j = 0; j < particles * 4; ++j) { + double value; + if (fscanf(file, "%lf", &value) != 1) return EXIT_FAILURE; + if (selected) point[j] = value; + } + if (selected) { found = true; break; } + } + fclose(file); free(path); + if (!found) { fprintf(stderr, "no saved point for %s\n", process); return EXIT_FAILURE; } + for (size_t j = 0; j < (size_t)n; ++j) + for (size_t k = 0; k < 4; ++k) + momenta[(size_t)n*k + entry->permutation[j]] = point[4*j+k]; + UmamiHandle handle = NULL; + check(umami_initialize(&handle, argv[1]), "initialize"); + double value = NAN, random = 0.5; + int helicity = -1; + UmamiInputKey input_keys[] = {UMAMI_IN_MOMENTA, UMAMI_IN_CHANNEL_INDEX, + UMAMI_IN_FLAVOR_INDEX, UMAMI_IN_RANDOM_HELICITY}; + void const *inputs[] = {momenta, &channel, &flavour, &random}; + UmamiOutputKey output_keys[] = {UMAMI_OUT_MATRIX_ELEMENT, UMAMI_OUT_HELICITY_INDEX}; + void *outputs[] = {&value, &helicity}; + check(umami_matrix_element(handle, 1, 1, 0, 4, input_keys, inputs, 2, output_keys, outputs), "evaluate"); + check(umami_free(handle), "free"); + free(point); free(momenta); + if (!isfinite(value)) return EXIT_FAILURE; + printf("{\"provider\":\"%s\",\"channel\":%u,\"flavour\":%d,\"matrix_element\":%.17g,\"helicity_label\":%d}\n", + umami_provider.id, channel, flavour, value, helicity); + return EXIT_SUCCESS; +} diff --git a/pyamplicol/src/pyamplicol/assets/api_templates/umami/umami_provider.h b/pyamplicol/src/pyamplicol/assets/api_templates/umami/umami_provider.h new file mode 100644 index 0000000000..bbffa09da4 --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/api_templates/umami/umami_provider.h @@ -0,0 +1,59 @@ +/* SPDX-License-Identifier: 0BSD */ +#ifndef PYAMPLICOL_UMAMI_PROVIDER_H +#define PYAMPLICOL_UMAMI_PROVIDER_H +#include +#include + +typedef struct { + const char *name; + double real, imaginary; + bool mutable, is_complex; +} UmamiParameter; + +typedef struct { + const char *key; + const int *pdgs; + const char *alpha_s_name; + const char *color_accuracy; + size_t helicity_count; + const char *const *helicity_ids; + size_t parameter_count; + const UmamiParameter *parameters; +} UmamiProcess; + +typedef struct { + size_t process_index; + const char *color_id; + size_t color_index; + /* runtime[j] = input[permutation[j]], including both incoming legs. */ + const size_t *permutation; + double factor; + /* Exported helicity index (input leg order) -> runtime index, -1 if absent. */ + const int *helicity_runtime; + /* Runtime helicity index -> exported helicity index. */ + const size_t *helicity_export; +} UmamiContribution; + +typedef struct { + size_t contribution_count; + const UmamiContribution *contributions; +} UmamiFlavour; + +typedef struct { + size_t flavour_count; + const UmamiFlavour *flavours; +} UmamiChannel; + +typedef struct { + const char *id; + size_t particle_count, incoming_count, helicity_count, color_count; + /* Number of exported helicities, the metadata "helicities" table. */ + size_t exported_helicity_count; + const double *masses; + size_t process_count; + const UmamiProcess *processes; + size_t channel_count; + const UmamiChannel *channels; + bool supports_alpha_s; +} UmamiProvider; +#endif diff --git a/pyamplicol/src/pyamplicol/assets/api_templates/umami/umami_reweight.c b/pyamplicol/src/pyamplicol/assets/api_templates/umami/umami_reweight.c new file mode 100644 index 0000000000..7719348f8d --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/api_templates/umami/umami_reweight.c @@ -0,0 +1,367 @@ +/* SPDX-License-Identifier: 0BSD */ +/* UMAMI provider of the ratio of a target-accuracy matrix element (e.g. full colour) + * to the leading-colour one summed over all colour flows, at a fixed helicity. */ +#include "umami.h" +#include "reweight_provider.h" +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef UMAMI_REWEIGHT_HEADER +#define UMAMI_REWEIGHT_HEADER "reweight_p0.h" +#endif +#include UMAMI_REWEIGHT_HEADER + +typedef struct { + char *name; + double real, imaginary; +} Override; + +typedef struct { + char *base; + RusticolRuntimeHandle **reference, **target; + double *momenta; + /* Parameters set so far, applied to the processes loaded later. */ + Override *overrides; + size_t override_count; +} Instance; + +static bool const supported_inputs[UMAMI_INPUT_KEY_COUNT] = { + [UMAMI_IN_MOMENTA] = true, + [UMAMI_IN_ALPHA_S] = UMAMI_HAS_ALPHA_S, + [UMAMI_IN_FLAVOR_INDEX] = true, + [UMAMI_IN_HELICITY_INDEX] = true, + [UMAMI_IN_CHANNEL_INDEX] = true, +}; +static bool const required_inputs[UMAMI_INPUT_KEY_COUNT] = { + [UMAMI_IN_MOMENTA] = true, + [UMAMI_IN_HELICITY_INDEX] = true, +}; +static bool const supported_outputs[UMAMI_OUTPUT_KEY_COUNT] = { + [UMAMI_OUT_MATRIX_ELEMENT_RATIO] = true, +}; + +static UmamiStatus native_error(int status) { + if (status == RUSTICOL_STATUS_OK) return UMAMI_SUCCESS; + char message[1024]; + size_t required = 0; + if (rusticol_last_error_message(message, sizeof(message), &required) == + RUSTICOL_STATUS_OK) fprintf(stderr, "umami: %s\n", message); + return UMAMI_ERROR; +} + +static UmamiStatus load(char const *base, char const *relative, char const *identity, + ReweightProcess const *process, RusticolRuntimeHandle **result) { + size_t length = strlen(base) + strlen(relative) + 2; + char *path = malloc(length); + if (path == NULL) return UMAMI_ERROR; + snprintf(path, length, "%s/%s", base, relative); + RusticolRuntimeHandle *runtime = NULL; + int status = rusticol_runtime_load(path, process->key, NULL, &runtime); + free(path); + if (status != RUSTICOL_STATUS_OK) return native_error(status); + size_t count = 0, required = 0; + status = rusticol_runtime_external_count(runtime, &count); + bool compatible = status == RUSTICOL_STATUS_OK && count == reweight_provider.particle_count; + for (size_t j = 0; compatible && j < count; ++j) { + int32_t pdg = 0; + status = rusticol_runtime_external_pdg(runtime, j, &pdg); + compatible = status == RUSTICOL_STATUS_OK && pdg == process->pdgs[j]; + } + if (compatible) { + status = rusticol_runtime_helicity_count(runtime, &count); + compatible = status == RUSTICOL_STATUS_OK && count == process->helicity_count; + } + if (compatible && identity != NULL) { + char *loaded = malloc(strlen(identity) + 1); + compatible = loaded != NULL && rusticol_runtime_artifact_id( + runtime, loaded, strlen(identity) + 1, &required) == RUSTICOL_STATUS_OK && + strcmp(loaded, identity) == 0; + free(loaded); + } + if (!compatible) { + (void)rusticol_runtime_free(runtime); + fprintf(stderr, "umami: artifact %s is incompatible with reweight provider %s\n", + relative, reweight_provider.id); + return UMAMI_ERROR; + } + *result = runtime; + return UMAMI_SUCCESS; +} + +/* Loads a reference (side 0) or target process on first use, with the parameters set. */ +static UmamiStatus ensure_loaded(Instance *instance, int side, size_t index) { + RusticolRuntimeHandle **slot = + side == 0 ? &instance->reference[index] : &instance->target[index]; + if (*slot != NULL) return UMAMI_SUCCESS; + RusticolRuntimeHandle *runtime = NULL; + UmamiStatus status = side == 0 + ? load(instance->base, reweight_provider.reference_artifact, + reweight_provider.reference_artifact_id, + &reweight_provider.reference_processes[index], &runtime) + : load(instance->base, reweight_provider.target_artifact, + reweight_provider.target_artifact_id, + &reweight_provider.target_processes[index], &runtime); + if (status != UMAMI_SUCCESS) return status; + for (size_t i = 0; i < instance->override_count; ++i) { + Override const *override = &instance->overrides[i]; + double re = 0.0, im = 0.0; + if (rusticol_runtime_get_model_parameter(runtime, override->name, &re, &im) != + RUSTICOL_STATUS_OK) continue; + status = native_error(rusticol_runtime_set_model_parameter( + runtime, override->name, override->real, override->imaginary)); + if (status != UMAMI_SUCCESS) { (void)rusticol_runtime_free(runtime); return status; } + } + *slot = runtime; + return UMAMI_SUCCESS; +} + +static UmamiStatus record_override(Instance *instance, char const *name, + double real, double imaginary) { + for (size_t i = 0; i < instance->override_count; ++i) + if (strcmp(instance->overrides[i].name, name) == 0) { + instance->overrides[i].real = real; + instance->overrides[i].imaginary = imaginary; + return UMAMI_SUCCESS; + } + Override *overrides = realloc(instance->overrides, + (instance->override_count + 1) * sizeof(*overrides)); + if (overrides == NULL) return UMAMI_ERROR; + instance->overrides = overrides; + char *copy = malloc(strlen(name) + 1); + if (copy == NULL) return UMAMI_ERROR; + strcpy(copy, name); + overrides[instance->override_count++] = (Override){copy, real, imaginary}; + return UMAMI_SUCCESS; +} + +UmamiStatus umami_get_meta(UmamiMetaKey key, void *result) { + if (result == NULL) return UMAMI_ERROR; + switch (key) { + case UMAMI_META_DEVICE: *(UmamiDevice *)result = UMAMI_DEVICE_CPU; break; + case UMAMI_META_PARTICLE_COUNT: *(int *)result = (int)reweight_provider.particle_count; break; + case UMAMI_META_HELICITY_COUNT: + *(int *)result = (int)reweight_provider.exported_helicity_count; break; + default: return UMAMI_ERROR_UNSUPPORTED_META; + } + return UMAMI_SUCCESS; +} + +UmamiStatus umami_supported_inputs(bool const **supported, int *count) { + if (supported == NULL || count == NULL) return UMAMI_ERROR; + *supported = supported_inputs; *count = UMAMI_INPUT_KEY_COUNT; + return UMAMI_SUCCESS; +} +UmamiStatus umami_required_inputs(bool const **required, int *count) { + if (required == NULL || count == NULL) return UMAMI_ERROR; + *required = required_inputs; *count = UMAMI_INPUT_KEY_COUNT; + return UMAMI_SUCCESS; +} +UmamiStatus umami_supported_outputs(bool const **supported, int *count) { + if (supported == NULL || count == NULL) return UMAMI_ERROR; + *supported = supported_outputs; *count = UMAMI_OUTPUT_KEY_COUNT; + return UMAMI_SUCCESS; +} + +UmamiStatus umami_initialize(UmamiHandle *handle, char const *param_card_path) { + if (handle == NULL) return UMAMI_ERROR; + *handle = NULL; + struct stat info; + if (param_card_path == NULL || param_card_path[0] != '/' || + stat(param_card_path, &info) != 0 || !S_ISDIR(info.st_mode) || + reweight_provider.particle_count > INT_MAX) return UMAMI_ERROR; + Instance *instance = calloc(1, sizeof(*instance)); + if (instance == NULL) return UMAMI_ERROR; + instance->reference = calloc(reweight_provider.reference_process_count + 1, + sizeof(*instance->reference)); + instance->target = calloc(reweight_provider.target_process_count + 1, + sizeof(*instance->target)); + instance->momenta = calloc(4 * reweight_provider.particle_count, sizeof(double)); + instance->base = malloc(strlen(param_card_path) + 1); + if (instance->reference == NULL || instance->target == NULL || instance->momenta == NULL || + instance->base == NULL || reweight_provider.reference_process_count == 0 || + reweight_provider.target_process_count == 0) { + umami_free(instance); return UMAMI_ERROR; + } + strcpy(instance->base, param_card_path); + /* Validate both artifacts now; other processes are loaded lazily on first use. */ + for (int side = 0; side < 2; ++side) { + UmamiStatus status = ensure_loaded(instance, side, 0); + if (status != UMAMI_SUCCESS) { umami_free(instance); return status; } + } + *handle = instance; + return UMAMI_SUCCESS; +} + +/* Runs body for every loaded runtime, reference ones first. */ +#define FOR_EACH_RUNTIME(instance, runtime, body) \ + for (size_t side = 0; side < 2; ++side) { \ + size_t count = side == 0 ? reweight_provider.reference_process_count \ + : reweight_provider.target_process_count; \ + for (size_t i = 0; i < count; ++i) { \ + RusticolRuntimeHandle *runtime = \ + side == 0 ? (instance)->reference[i] : (instance)->target[i]; \ + if (runtime == NULL) continue; \ + body \ + } \ + } + +UmamiStatus umami_get_parameter(UmamiHandle handle, char const *name, + double *real, double *imaginary) { + if (handle == NULL || name == NULL || real == NULL) return UMAMI_ERROR; + Instance *instance = handle; + double re = 0.0, im = 0.0; + FOR_EACH_RUNTIME(instance, runtime, { + if (rusticol_runtime_get_model_parameter(runtime, name, &re, &im) == + RUSTICOL_STATUS_OK) { + *real = re; + if (imaginary != NULL) *imaginary = im; + return UMAMI_SUCCESS; + } + }) + return UMAMI_ERROR; +} + +UmamiStatus umami_set_parameter(UmamiHandle handle, char const *name, + double real, double imaginary) { + if (handle == NULL || name == NULL || !isfinite(real)) return UMAMI_ERROR; + Instance *instance = handle; + bool found = false; + double re = 0.0, im = 0.0; + FOR_EACH_RUNTIME(instance, runtime, { + if (rusticol_runtime_get_model_parameter(runtime, name, &re, &im) != + RUSTICOL_STATUS_OK) continue; + found = true; + if (native_error(rusticol_runtime_set_model_parameter(runtime, name, real, imaginary)) != + UMAMI_SUCCESS) return UMAMI_ERROR; + }) + if (!found) return UMAMI_ERROR; + return record_override(instance, name, real, imaginary); +} + +/* Value of one process at a fixed runtime helicity, with an optional alpha_s. */ +static UmamiStatus evaluate(Instance *instance, RusticolRuntimeHandle *runtime, + ReweightProcess const *process, uint32_t helicity, + double const *alpha_s, double *result) { + size_t n = reweight_provider.particle_count; + double previous_real = 0.0, previous_imaginary = 0.0; + bool changed = false; + UmamiStatus status; + if (alpha_s != NULL) { + if (process->alpha_s_name == NULL) return UMAMI_ERROR_UNSUPPORTED_INPUT; + status = native_error(rusticol_runtime_get_model_parameter( + runtime, process->alpha_s_name, &previous_real, &previous_imaginary)); + if (status != UMAMI_SUCCESS) return status; + changed = previous_real != *alpha_s || previous_imaginary != 0.0; + if (changed) { + status = native_error(rusticol_runtime_set_model_parameter( + runtime, process->alpha_s_name, *alpha_s, 0.0)); + if (status != UMAMI_SUCCESS) return status; + } + } + status = native_error(rusticol_runtime_evaluate_selected_f64( + runtime, instance->momenta, 4*n, 1, NULL, 0, NULL, 0, + &helicity, 1, NULL, 0, result, 1)); + if (changed) { + UmamiStatus restored = native_error(rusticol_runtime_set_model_parameter( + runtime, process->alpha_s_name, previous_real, previous_imaginary)); + if (restored != UMAMI_SUCCESS) return restored; + } + return status; +} + +UmamiStatus umami_matrix_element(UmamiHandle handle, size_t count, size_t stride, size_t offset, + size_t input_count, UmamiInputKey const *input_keys, void const *const *inputs, + size_t output_count, UmamiOutputKey const *output_keys, void *const *outputs) { + if (handle == NULL || offset > stride || count > stride-offset || + (input_count && (input_keys == NULL || inputs == NULL)) || + (output_count && (output_keys == NULL || outputs == NULL)) || + (stride && reweight_provider.particle_count > SIZE_MAX / stride / 4)) return UMAMI_ERROR; + void const *in[UMAMI_INPUT_KEY_COUNT] = {0}; + void *out[UMAMI_OUTPUT_KEY_COUNT] = {0}; + for (size_t i = 0; i < input_count; ++i) { + int key = input_keys[i]; + if (key < 0 || key >= UMAMI_INPUT_KEY_COUNT || !supported_inputs[key]) + return UMAMI_ERROR_UNSUPPORTED_INPUT; + if (inputs[i] == NULL || in[key] != NULL) return UMAMI_ERROR; + in[key] = inputs[i]; + } + for (size_t i = 0; i < output_count; ++i) { + int key = output_keys[i]; + if (key < 0 || key >= UMAMI_OUTPUT_KEY_COUNT || !supported_outputs[key]) + return UMAMI_ERROR_UNSUPPORTED_OUTPUT; + if (outputs[i] == NULL || out[key] != NULL) return UMAMI_ERROR; + out[key] = outputs[i]; + } + if (count == 0) return UMAMI_SUCCESS; + if (in[UMAMI_IN_MOMENTA] == NULL || in[UMAMI_IN_HELICITY_INDEX] == NULL) + return UMAMI_ERROR_MISSING_INPUT; + Instance *instance = handle; + double const *momenta = in[UMAMI_IN_MOMENTA]; + size_t n = reweight_provider.particle_count; + for (size_t event = offset; event < offset+count; ++event) { + size_t channel = in[UMAMI_IN_CHANNEL_INDEX] ? + ((unsigned int const *)in[UMAMI_IN_CHANNEL_INDEX])[event] : 0; + int flavour = in[UMAMI_IN_FLAVOR_INDEX] ? + ((int const *)in[UMAMI_IN_FLAVOR_INDEX])[event] : 0; + int helicity = ((int const *)in[UMAMI_IN_HELICITY_INDEX])[event]; + if (channel >= reweight_provider.channel_count || flavour < 0 || + (size_t)flavour >= reweight_provider.channels[channel].flavour_count || + helicity < 0 || (size_t)helicity >= reweight_provider.exported_helicity_count) + return UMAMI_ERROR; + ReweightFlavour const *entry = &reweight_provider.channels[channel].flavours[flavour]; + for (size_t j = 0; j < n; ++j) + for (size_t k = 0; k < 4; ++k) { + double value = momenta[event + stride * (n*k + entry->permutation[j])]; + if (!isfinite(value)) return UMAMI_ERROR; + instance->momenta[4*j+k] = value; + } + double const *alpha_s = + in[UMAMI_IN_ALPHA_S] ? &((double const *)in[UMAMI_IN_ALPHA_S])[event] : NULL; + int reference_helicity = entry->reference_helicity[helicity]; + int target_helicity = entry->target_helicity[helicity]; + double reference = 0.0, target = 0.0; + if ((reference_helicity >= 0 && + ensure_loaded(instance, 0, entry->reference_process) != UMAMI_SUCCESS) || + (target_helicity >= 0 && + ensure_loaded(instance, 1, entry->target_process) != UMAMI_SUCCESS)) + return UMAMI_ERROR; + if (reference_helicity >= 0) { + UmamiStatus status = evaluate(instance, instance->reference[entry->reference_process], + &reweight_provider.reference_processes[entry->reference_process], + (uint32_t)reference_helicity, alpha_s, &reference); + if (status != UMAMI_SUCCESS) return status; + } + if (target_helicity >= 0) { + UmamiStatus status = evaluate(instance, instance->target[entry->target_process], + &reweight_provider.target_processes[entry->target_process], + (uint32_t)target_helicity, alpha_s, &target); + if (status != UMAMI_SUCCESS) return status; + } + double ratio = reference != 0.0 ? target / reference : 0.0; + if (!isfinite(ratio)) return UMAMI_ERROR; + if (out[UMAMI_OUT_MATRIX_ELEMENT_RATIO]) + ((double *)out[UMAMI_OUT_MATRIX_ELEMENT_RATIO])[event] = ratio; + } + return UMAMI_SUCCESS; +} + +UmamiStatus umami_free(UmamiHandle handle) { + if (handle == NULL) return UMAMI_SUCCESS; + Instance *instance = handle; + if (instance->reference != NULL) + for (size_t i = 0; i < reweight_provider.reference_process_count; ++i) + if (instance->reference[i] != NULL) (void)rusticol_runtime_free(instance->reference[i]); + if (instance->target != NULL) + for (size_t i = 0; i < reweight_provider.target_process_count; ++i) + if (instance->target[i] != NULL) (void)rusticol_runtime_free(instance->target[i]); + for (size_t i = 0; i < instance->override_count; ++i) free(instance->overrides[i].name); + free(instance->overrides); free(instance->base); + free(instance->reference); free(instance->target); free(instance->momenta); free(instance); + return UMAMI_SUCCESS; +} diff --git a/pyamplicol/src/pyamplicol/assets/models/MANIFEST.sha256 b/pyamplicol/src/pyamplicol/assets/models/MANIFEST.sha256 new file mode 100644 index 0000000000..7842e0453c --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/models/MANIFEST.sha256 @@ -0,0 +1,67 @@ +72b500a47f53fba22d7563263b27eb51906d035b6fd96f9130b327e70542ab88 PROVENANCE.toml +78e536276cfa98a854ea9b418a6de9437da1e21ea258fcc57ba2b20a6a691900 json/scalar_gravity/restrict_default.json +3bfc81548466ffc464fa72a3469a9c0c8930970bf048c64c43aee13c8e83bd72 json/scalar_gravity/scalar_gravity.json +ffe7759b8efcc86e6d12d3797c2e63a91c18c07de1bace6a51ee02144ecbf218 json/scalars/restrict_default.json +ca27b8cc228cd03db57556fc1c40298865a75269bf2744bee8c59c013eb28df7 json/scalars/scalars.json +0a268a9fc959036e166367155ab2eeca5c52f76927a89245f8964b9b33b2a8b5 json/scalars/scalars_2p_3p.json +014e93a779812d9af1d3886143cebc8fd911e92f5f970aee4dfffe01429651f5 json/sm/restrict_c_mass.json +be8bdca54507167d8ae4b33d0b0b77808052cd841d6fabd26a3d99926d915b99 json/sm/restrict_ckm.json +8fdd48a3624620c1a15bab602f43a41519038cf69d8fef6aeaaaf13a3f1846c1 json/sm/restrict_default.json +3d9fe3095907cef03f889bd95b9630a2f836d70244f1b450cfd5d2764913531b json/sm/restrict_lepton_masses.json +82d2fbc06f9d0b7daaed56673912d920ca459f435e9083deed2f4b716c2f4a30 json/sm/restrict_no_b_mass.json +f925921df1b66f010cd6f8cd348b5f4728ba37678e0733d8618cef3f2b4e610e json/sm/restrict_no_masses.json +a26603f4f3572309dc7ce2f23295d5a8aec61d2f3c42a017341245e459648478 json/sm/restrict_no_tau_mass.json +fdc03507f82a3d98571b97ba0ab34f712dc0eb02b101f8308b96c6ab8490c08c json/sm/restrict_no_widths.json +b8b4376c82f9486266283a383a0347ec42498296180d92249e2e7a772e1a7e1a json/sm/restrict_zeromass_ckm.json +c1222e3b70b71804e5212e994a3b76e2f098f7fc3ba1693c9f9c65578d7e3e00 json/sm/sm.json +e1ee0059f3fc534c462e98c358249acbce0a711171fcbfe41720a9a1f24b2933 json/sm/sm_wrapped_indices.json +baf7043f1586a6b70546d04019b11c3fc17f181f278d5d33a70b7217b5e0db71 ufo/scalar_gravity/__init__.py +63738137e1355e064968615a500ecd195395af6f8dea4fd4efee541b47dc7f27 ufo/scalar_gravity/coupling_orders.py +afd07d76eaefaf4828a243eb63c88196f11da1fcbea59f873c1e3832fbc276fb ufo/scalar_gravity/couplings.py +7ab870cbfebbca3858ab8d6780d138b4b2531f6e2be84ff2cf4e6362b6c601bd ufo/scalar_gravity/function_library.py +28e38a5a20aee6e02605fc2dc2d2e850b05edc1ade5f343fc2ef780ba9666e42 ufo/scalar_gravity/lorentz.py +5db44f44bdc8df1ada2c76bd02d5f9241298d0187b963c0d2ee39b88e3910cb3 ufo/scalar_gravity/object_library.py +ed708e1744af39117e364be9f8c2f9c7fa844781701a3fb419cff95a8b427dd7 ufo/scalar_gravity/param_card.dat +01b0477e0b0e352f968a6d1bfd1e9724c505adc6dd5c1d39dd08e9da97cd9f00 ufo/scalar_gravity/parameters.py +90b4efa5fd4c8eb443c43a3a8d7109add2fae94807670098195d6024acd91bd5 ufo/scalar_gravity/particles.py +ed708e1744af39117e364be9f8c2f9c7fa844781701a3fb419cff95a8b427dd7 ufo/scalar_gravity/restrict_default.dat +0005fc634dd25e095ad70ee49b1789736e6ed31a428794866fea279585a7fbd0 ufo/scalar_gravity/restrict_full.dat +d45878face2f212eaed32932e36ffcaf33e643db347c44d074657292805b47a0 ufo/scalar_gravity/run_write_param_card.py +4c7d261c81d15242c09cd12403fe8d32a5d41fefc4136e726d5e934d9358e9be ufo/scalar_gravity/vertices.py +3008b3269d0689370523a4a6dbcdd4100bb7d78599e2f3df79127ead6fe7fc75 ufo/scalar_gravity/write_param_card.py +baf7043f1586a6b70546d04019b11c3fc17f181f278d5d33a70b7217b5e0db71 ufo/scalars/__init__.py +2e691ad2042a4a7edde293fe69ba96f54ec08221061db0ed1dc30f588011504e ufo/scalars/coupling_orders.py +325cfc644fe3b86cffd8de916c3f285016d70fe673d50f3c5ca1c7dcdfaa4183 ufo/scalars/couplings.py +7ab870cbfebbca3858ab8d6780d138b4b2531f6e2be84ff2cf4e6362b6c601bd ufo/scalars/function_library.py +3daaf714d13328419366cc5c9b6a490d6cd9cff8ace604bf2ec0915061e5b736 ufo/scalars/lorentz.py +5db44f44bdc8df1ada2c76bd02d5f9241298d0187b963c0d2ee39b88e3910cb3 ufo/scalars/object_library.py +7b11a9b1d84126d31f58d5c4752ea0fe0921074bfcb338ecd6ef4d2ad7da89a7 ufo/scalars/parameters.py +1e346940ecf9746445db47219256cc4739444b111f3a2a96967bbb890b57e310 ufo/scalars/particles.py +020178e9df8fe242fd4d34077db4caff694959a7c6d7a5c829527f8de4e1c80f ufo/scalars/restrict_default.dat +0005fc634dd25e095ad70ee49b1789736e6ed31a428794866fea279585a7fbd0 ufo/scalars/restrict_full.dat +d45878face2f212eaed32932e36ffcaf33e643db347c44d074657292805b47a0 ufo/scalars/run_write_param_card.py +1341725ef6b4bf5eb7ff5fa09d633c1cb52e89b695c627ae42c2ca5664525d4f ufo/scalars/vertices.py +3008b3269d0689370523a4a6dbcdd4100bb7d78599e2f3df79127ead6fe7fc75 ufo/scalars/write_param_card.py +dcefb2fa6a4b1653d87944a11dfaedf1b5a2c0a5b27d2370685c2e21d157a568 ufo/sm/__init__.py +de21ae72ae23f8409d077599f0e4fabbab5287373e3930bc34a19adacd60781e ufo/sm/build_restrict.py +cb45f17e973165af72af7f5fabe368306919b00d8421745c79807b5fda1bdace ufo/sm/coupling_orders.py +a5d30cec4efec54b637b5a2ce8017b54fa78f16c546afe33e7410a0279bf979c ufo/sm/couplings.py +12b9bc8544b01e248087f197c2dc9137d274f980178bc72f67d130fb149007a2 ufo/sm/decays.py +a8821ed8a739e43b705ff7c7aa7d93ff545071b0fe020f5c0c8b0799c30ef2ae ufo/sm/function_library.py +1f13e8b63e2ecf428e2875e14bc6901ce49952e1cf2b18ea9934c8d4264e3559 ufo/sm/lorentz.py +6391978031caea837ecea426f2f0cfdac748457a00bac99e02c025c42e883cb5 ufo/sm/object_library.py +e8599a97d31004d7af8c589a5e734c9141b48d9becdbbde86a37e41635390e38 ufo/sm/parameters.py +9b542b5d49598ade4d9d6897d569e8feeaa05993e74aef67fb5dfafd5efa9916 ufo/sm/particles.py +817a7843a1435c1c7b1f0231ababc346e58f81d8038b9c0b2c75eae32960da9e ufo/sm/restrict_c_mass.dat +75fd361e4ce78100b7dd107cd42b1015d9303da44bec8971e401c2d20d7876cb ufo/sm/restrict_ckm.dat +988939a63e2d35ca991bac864b8f10f6ee19fd13063cb68e91af962767974889 ufo/sm/restrict_default.dat +f1e023d45808db115385e5707640a3a3e126217840067a14200e995181eba151 ufo/sm/restrict_full.dat +eb44e12f6f4ee591e76277322359e31f4a577a9200acd6c5c8dc6629a039e8c8 ufo/sm/restrict_lepton_masses.dat +32dc93b5584b7bdbf24e87ce12d55398cc2d865e0240ccf6e853c2ade1f90e9e ufo/sm/restrict_no_b_mass.dat +539664a3651839946cc995257bd9e108054c789f5352ecc0e562260798e66bf0 ufo/sm/restrict_no_masses.dat +52433bb4161af5a0f17c91964fdd695b155c0c9821eda37142f04a1b1c2e57f8 ufo/sm/restrict_no_tau_mass.dat +0ab8569ed8e747b0c0c0ee2c2c93db1e368489237baf8430c53eef605a1634e8 ufo/sm/restrict_no_widths.dat +2388cf490afcfb5530f49ae3a85fec235be236e33c4bb5e1515f3d4827b963c6 ufo/sm/restrict_zeromass_ckm.dat +d45878face2f212eaed32932e36ffcaf33e643db347c44d074657292805b47a0 ufo/sm/run_write_param_card.py +6f3353b483e6a3f7c6b9f5e79d3a818af8e933ec63f96c18ed271760f6492c68 ufo/sm/vertices.py +9a289a0f17edaaa0609ccd7d83db3b2d4b389e51f07a0f0c47ffce77afb18c03 ufo/sm/write_param_card.py diff --git a/pyamplicol/src/pyamplicol/assets/models/PROVENANCE.toml b/pyamplicol/src/pyamplicol/assets/models/PROVENANCE.toml new file mode 100644 index 0000000000..5ec6b003c0 --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/models/PROVENANCE.toml @@ -0,0 +1,1140 @@ +# SPDX-License-Identifier: 0BSD +schema_version = 1 +manifest = "MANIFEST.sha256" +canonical_root = "pyamplicol/assets/models" +release_status = "eligible" +selection_policy = "Git blobs under the baseline package asset tree only; the duplicate top-level tree and working-tree generated files are excluded" +baseline_tracked_file_count = 67 +baseline_payload_file_count = 66 +[[licenses]] +id = "0BSD" +path = "LICENSE" +applies_to = "pyAmpliCol-owned built-in and scalar model material" + +[[licenses]] +id = "GammaLoop-model-assets" +path = "licenses/GammaLoop-model-assets.txt" +applies_to = "UFO and derived JSON model assets distributed with GammaLoop" + +[[licenses]] +id = "MadGraph5-aMCatNLO" +path = "licenses/MadGraph5_aMCatNLO.txt" +applies_to = "ufo/sm/build_restrict.py" + +[[licenses]] +id = "MIT" +path = "licenses/ufo-model-loader.txt" +applies_to = "serialization tool; generated JSON retains source-model terms" + +[[models]] +id = "built-in-sm" +format = "python" +canonical_package_path = "pyamplicol/models/builtin/__init__.py" +source_repository = "git@github.com:rikkert-frederix/AmpliCol.git" +source_revision = "643bc6f99d7b2249af0a85204768df243e612411" +source_path = "pyAmpliCol/src/pyamplicol/model.py" +source_sha256 = "7b3bac9e53193260bf9c2829ef972f87d23246e93dc57f208bda4821195b7ad3" +companion_source_path = "pyAmpliCol/src/pyamplicol/symbols.py" +companion_source_sha256 = "265178a9b8960c8176decbc92000b47c778e9c993851cc76f067d4cb8e0c6d62" +license_ids = [ "0BSD",] +release_eligible = true +asset_manifest_member = false +note = "Code-backed model owned by the models/generation worker; no duplicate serialization is invented here" + +[[models]] +id = "sm" +formats = [ "ufo", "json",] +authors = [ "N. Christensen", "C. Duhr",] +canonical_ufo_path = "pyamplicol/assets/models/ufo/sm" +canonical_json_path = "pyamplicol/assets/models/json/sm/sm.json" +license_ids = [ "GammaLoop-model-assets",] +release_eligible = true + +[[models]] +id = "scalars" +formats = [ "ufo", "json",] +authors = [ "Valentin Hirschi",] +generator = "FeynRules 1.7.69" +canonical_ufo_path = "pyamplicol/assets/models/ufo/scalars" +canonical_json_path = "pyamplicol/assets/models/json/scalars/scalars.json" +license_ids = [ "0BSD", "GammaLoop-model-assets",] +release_eligible = true + +[[models]] +id = "scalar_gravity" +formats = [ "ufo", "json",] +authors = [ "Valentin Hirschi",] +generator = "FeynRules 1.7.69" +canonical_ufo_path = "pyamplicol/assets/models/ufo/scalar_gravity" +canonical_json_path = "pyamplicol/assets/models/json/scalar_gravity/scalar_gravity.json" +license_ids = [ "0BSD", "GammaLoop-model-assets",] +release_eligible = true + +[[transformations]] +id = "verbatim-git-blob-v1" +description = "Copy the selected baseline Git blob byte-for-byte" + +[[transformations]] +id = "scalar-static-defaults-v1" +description = "Replace UFO_SCALARS_MODEL_* environment reads with the historical defaults N_SCALARS=3 and N_POINT_INTERACTIONS=3..10" +authorized_by = "docs/development/PHYSICS_EXTRACTION_CONTRACT.md#model-determinism" + +[[transformations]] +id = "scalar-gravity-static-defaults-v1" +description = "Replace UFO_GRAVITY_MODEL_N_SCALARS with N_SCALARS=3 and remove the unused interaction-option branch containing undefined N_POINT_INTERACTIONS" +authorized_by = "docs/development/PHYSICS_EXTRACTION_CONTRACT.md#model-determinism" + +[[transformations]] +id = "loader-ufo-full-v1" +description = "Load sanitized UFO with restriction='full', simplify=True, no index wrapping; serialize model with ufo-model-loader 0.1.8 JSONLook.VERBOSE" +authorized_by = "docs/development/MODEL_ASSET_PROVENANCE.md#before-extraction" + +[[transformations]] +id = "loader-ufo-full-wrapped-v1" +description = "Load sanitized UFO with restriction='full', simplify=True, wrap Lorentz indices; serialize with ufo-model-loader 0.1.8 JSONLook.VERBOSE" +authorized_by = "docs/development/MODEL_ASSET_PROVENANCE.md#before-extraction" + +[[transformations]] +id = "loader-json-canonical-v1" +description = "Load the tracked frozen JSON variant without a restriction and reserialize with ufo-model-loader 0.1.8 JSONLook.VERBOSE" +authorized_by = "docs/development/MODEL_ASSET_PROVENANCE.md#before-extraction" + +[[transformations]] +id = "sm-standard-ufo-spinor-order-v1" +description = "Swap the fermion-pair leg labels in the spinor slots of the SM Lorentz structures to the standard UFO order of MadGraph and FeynRules, e.g. FFV1 = Gamma(3,2,1); the JSON models apply it to their loader-ufo-full-v1 and loader-ufo-full-wrapped-v1 serializations" +authorized_by = "docs/development/model-assets/TRANSFORMATIONS.md#standard-ufo-spinor-order" + +[[transformations]] +id = "loader-restriction-card-v1" +description = "Parse every external parameter from the matching sanitized UFO restriction card and serialize the complete card, including explicit zeros, with ufo-model-loader 0.1.8 JSONLook.VERBOSE" +authorized_by = "docs/development/MODEL_ASSET_PROVENANCE.md#before-extraction" + +[[files]] +canonical_package_path = "pyamplicol/assets/models/json/scalar_gravity/restrict_default.json" +package_sha256 = "78e536276cfa98a854ea9b418a6de9437da1e21ea258fcc57ba2b20a6a691900" +package_size = 357 +source_repository = "git@github.com:rikkert-frederix/AmpliCol.git" +source_revision = "643bc6f99d7b2249af0a85204768df243e612411" +source_path = "pyAmpliCol/src/pyamplicol/assets/models/json/scalar_gravity/restrict_default.json" +source_blob_oid = "f3e10838fadee3867cafa98712e40121b4c652b8" +source_git_mode = "100644" +source_sha256 = "78e536276cfa98a854ea9b418a6de9437da1e21ea258fcc57ba2b20a6a691900" +license_ids = [ "0BSD", "GammaLoop-model-assets",] +transformation_id = "loader-restriction-card-v1" +upstream_distribution_path = "assets/models/json/scalar_gravity/restrict_default.json" +upstream_exact_blob_match = false +generated_from = "ufo/scalar_gravity/restrict_default.dat" + +[[files]] +canonical_package_path = "pyamplicol/assets/models/json/scalar_gravity/scalar_gravity.json" +package_sha256 = "3bfc81548466ffc464fa72a3469a9c0c8930970bf048c64c43aee13c8e83bd72" +package_size = 224817 +source_repository = "git@github.com:rikkert-frederix/AmpliCol.git" +source_revision = "643bc6f99d7b2249af0a85204768df243e612411" +source_path = "pyAmpliCol/src/pyamplicol/assets/models/json/scalar_gravity/scalar_gravity.json" +source_blob_oid = "9dba5b8cf3e65a38166a2f852f0aaa7ea987b08b" +source_git_mode = "100644" +source_sha256 = "e303fd1c39eb39e7deca5b36099202074a9048da3dbf2a73baf1f43a31648173" +license_ids = [ "0BSD", "GammaLoop-model-assets",] +transformation_id = "loader-ufo-full-v1" +upstream_distribution_path = "assets/models/json/scalar_gravity/scalar_gravity.json" +upstream_exact_blob_match = true +generated_from = "ufo/scalar_gravity" + +[[files]] +canonical_package_path = "pyamplicol/assets/models/json/scalars/restrict_default.json" +package_sha256 = "ffe7759b8efcc86e6d12d3797c2e63a91c18c07de1bace6a51ee02144ecbf218" +package_size = 322 +source_repository = "git@github.com:rikkert-frederix/AmpliCol.git" +source_revision = "643bc6f99d7b2249af0a85204768df243e612411" +source_path = "pyAmpliCol/src/pyamplicol/assets/models/json/scalars/restrict_default.json" +source_blob_oid = "510bf5ae4c24bdf3bd092c46b5cf30d1a3ea012b" +source_git_mode = "100644" +source_sha256 = "31e30e8bdbdaf4753e361269fcd0294f7d7c82956c0afe66052dd527aa4b864a" +license_ids = [ "0BSD", "GammaLoop-model-assets",] +transformation_id = "loader-restriction-card-v1" +upstream_distribution_path = "assets/models/json/scalars/restrict_default.json" +upstream_exact_blob_match = false +generated_from = "ufo/scalars/restrict_default.dat" + +[[files]] +canonical_package_path = "pyamplicol/assets/models/json/scalars/scalars.json" +package_sha256 = "ca27b8cc228cd03db57556fc1c40298865a75269bf2744bee8c59c013eb28df7" +package_size = 127756 +source_repository = "git@github.com:rikkert-frederix/AmpliCol.git" +source_revision = "643bc6f99d7b2249af0a85204768df243e612411" +source_path = "pyAmpliCol/src/pyamplicol/assets/models/json/scalars/scalars.json" +source_blob_oid = "47b891712d2298fe3f617a554b4a88cb4a8f0f8b" +source_git_mode = "100644" +source_sha256 = "6085cb3b1307bfdf7f3bb709ac1df9354b0cef8b1a3f6316f673fa069c9d9c85" +license_ids = [ "0BSD", "GammaLoop-model-assets",] +transformation_id = "loader-ufo-full-v1" +upstream_distribution_path = "assets/models/json/scalars/scalars.json" +upstream_exact_blob_match = true +generated_from = "ufo/scalars" + +[[files]] +canonical_package_path = "pyamplicol/assets/models/json/scalars/scalars_2p_3p.json" +package_sha256 = "0a268a9fc959036e166367155ab2eeca5c52f76927a89245f8964b9b33b2a8b5" +package_size = 10990 +source_repository = "git@github.com:rikkert-frederix/AmpliCol.git" +source_revision = "643bc6f99d7b2249af0a85204768df243e612411" +source_path = "pyAmpliCol/src/pyamplicol/assets/models/json/scalars/scalars_2p_3p.json" +source_blob_oid = "f456fa7fb19c5c2964ac431c1d43ab6393814a0b" +source_git_mode = "100644" +source_sha256 = "ba4a928f4bf28975ca861c85106559a6d2747326572751eec3448bf67a5f763a" +license_ids = [ "0BSD", "GammaLoop-model-assets",] +transformation_id = "loader-json-canonical-v1" +upstream_distribution_path = "assets/models/json/scalars/scalars_2p_3p.json" +upstream_exact_blob_match = true +generated_from = "json/scalars/scalars_2p_3p.json" +frozen_options = "N_SCALARS=3; N_POINT_INTERACTIONS=[2,3]" + +[[files]] +canonical_package_path = "pyamplicol/assets/models/json/sm/restrict_c_mass.json" +package_sha256 = "014e93a779812d9af1d3886143cebc8fd911e92f5f970aee4dfffe01429651f5" +package_size = 908 +source_repository = "git@github.com:rikkert-frederix/AmpliCol.git" +source_revision = "643bc6f99d7b2249af0a85204768df243e612411" +source_path = "pyAmpliCol/src/pyamplicol/assets/models/json/sm/restrict_c_mass.json" +source_blob_oid = "6f2691574431b0efb59b4df5449bf8b035118f50" +source_git_mode = "100644" +source_sha256 = "6b191f6d0c45a6e91ea867079b1095e37fc4882df1c1380e70312890959dc7a6" +license_ids = [ "GammaLoop-model-assets",] +transformation_id = "loader-restriction-card-v1" +upstream_distribution_path = "assets/models/json/sm/restrict_c_mass.json" +upstream_exact_blob_match = false +generated_from = "ufo/sm/restrict_c_mass.dat" + +[[files]] +canonical_package_path = "pyamplicol/assets/models/json/sm/restrict_ckm.json" +package_sha256 = "be8bdca54507167d8ae4b33d0b0b77808052cd841d6fabd26a3d99926d915b99" +package_size = 915 +source_repository = "git@github.com:rikkert-frederix/AmpliCol.git" +source_revision = "643bc6f99d7b2249af0a85204768df243e612411" +source_path = "pyAmpliCol/src/pyamplicol/assets/models/json/sm/restrict_ckm.json" +source_blob_oid = "80a237d40629c2181b63311d79f104f2f865121d" +source_git_mode = "100644" +source_sha256 = "57f3ffbd0a0784d433bb9b01211299f29dde1ddd0182ee0b1e4a87dc3f2b5c73" +license_ids = [ "GammaLoop-model-assets",] +transformation_id = "loader-restriction-card-v1" +upstream_distribution_path = "assets/models/json/sm/restrict_ckm.json" +upstream_exact_blob_match = false +generated_from = "ufo/sm/restrict_ckm.dat" + +[[files]] +canonical_package_path = "pyamplicol/assets/models/json/sm/restrict_default.json" +package_sha256 = "8fdd48a3624620c1a15bab602f43a41519038cf69d8fef6aeaaaf13a3f1846c1" +package_size = 906 +source_repository = "git@github.com:rikkert-frederix/AmpliCol.git" +source_revision = "643bc6f99d7b2249af0a85204768df243e612411" +source_path = "pyAmpliCol/src/pyamplicol/assets/models/json/sm/restrict_default.json" +source_blob_oid = "3ad2ec48354f2eaa98873074ebab97c39b02c297" +source_git_mode = "100644" +source_sha256 = "9d357d17c254595dbf612b66d6b531c982eefb71432835360bbfd8b21dcf454b" +license_ids = [ "GammaLoop-model-assets",] +transformation_id = "loader-restriction-card-v1" +upstream_distribution_path = "assets/models/json/sm/restrict_default.json" +upstream_exact_blob_match = false +generated_from = "ufo/sm/restrict_default.dat" + +[[files]] +canonical_package_path = "pyamplicol/assets/models/json/sm/restrict_lepton_masses.json" +package_sha256 = "3d9fe3095907cef03f889bd95b9630a2f836d70244f1b450cfd5d2764913531b" +package_size = 929 +source_repository = "git@github.com:rikkert-frederix/AmpliCol.git" +source_revision = "643bc6f99d7b2249af0a85204768df243e612411" +source_path = "pyAmpliCol/src/pyamplicol/assets/models/json/sm/restrict_lepton_masses.json" +source_blob_oid = "b9a1d2fc10724269fee9b45235351039396594fe" +source_git_mode = "100644" +source_sha256 = "a22a9de56e536cd7c4e4bd4707f5f95d3255fb0dffd49d9567ec9a659576bb76" +license_ids = [ "GammaLoop-model-assets",] +transformation_id = "loader-restriction-card-v1" +upstream_distribution_path = "assets/models/json/sm/restrict_lepton_masses.json" +upstream_exact_blob_match = false +generated_from = "ufo/sm/restrict_lepton_masses.dat" + +[[files]] +canonical_package_path = "pyamplicol/assets/models/json/sm/restrict_no_b_mass.json" +package_sha256 = "82d2fbc06f9d0b7daaed56673912d920ca459f435e9083deed2f4b716c2f4a30" +package_size = 906 +source_repository = "git@github.com:rikkert-frederix/AmpliCol.git" +source_revision = "643bc6f99d7b2249af0a85204768df243e612411" +source_path = "pyAmpliCol/src/pyamplicol/assets/models/json/sm/restrict_no_b_mass.json" +source_blob_oid = "d9b4518f19c497b9b414fc5380286a1f3f1fe9dc" +source_git_mode = "100644" +source_sha256 = "2ca10cc1e02b34f226db983da9a787659257003e6d84001204389e8aef03dfa5" +license_ids = [ "GammaLoop-model-assets",] +transformation_id = "loader-restriction-card-v1" +upstream_distribution_path = "assets/models/json/sm/restrict_no_b_mass.json" +upstream_exact_blob_match = false +generated_from = "ufo/sm/restrict_no_b_mass.dat" + +[[files]] +canonical_package_path = "pyamplicol/assets/models/json/sm/restrict_no_masses.json" +package_sha256 = "f925921df1b66f010cd6f8cd348b5f4728ba37678e0733d8618cef3f2b4e610e" +package_size = 902 +source_repository = "git@github.com:rikkert-frederix/AmpliCol.git" +source_revision = "643bc6f99d7b2249af0a85204768df243e612411" +source_path = "pyAmpliCol/src/pyamplicol/assets/models/json/sm/restrict_no_masses.json" +source_blob_oid = "4532ce8adb6385909b113dd706e9382dbba9b3c3" +source_git_mode = "100644" +source_sha256 = "9bc6bba7b940da83484e5f43346ba202333669aaca578dfa110ed3fbc17514b8" +license_ids = [ "GammaLoop-model-assets",] +transformation_id = "loader-restriction-card-v1" +upstream_distribution_path = "assets/models/json/sm/restrict_no_masses.json" +upstream_exact_blob_match = false +generated_from = "ufo/sm/restrict_no_masses.dat" + +[[files]] +canonical_package_path = "pyamplicol/assets/models/json/sm/restrict_no_tau_mass.json" +package_sha256 = "a26603f4f3572309dc7ce2f23295d5a8aec61d2f3c42a017341245e459648478" +package_size = 902 +source_repository = "git@github.com:rikkert-frederix/AmpliCol.git" +source_revision = "643bc6f99d7b2249af0a85204768df243e612411" +source_path = "pyAmpliCol/src/pyamplicol/assets/models/json/sm/restrict_no_tau_mass.json" +source_blob_oid = "f6a10ad19ea5b9e812b654ef98bfeb0c6ea01667" +source_git_mode = "100644" +source_sha256 = "7560d6890d7cd26532f28865fb8f4092ffcbf49c54fa7a16e921e6b08c3f43e3" +license_ids = [ "GammaLoop-model-assets",] +transformation_id = "loader-restriction-card-v1" +upstream_distribution_path = "assets/models/json/sm/restrict_no_tau_mass.json" +upstream_exact_blob_match = false +generated_from = "ufo/sm/restrict_no_tau_mass.dat" + +[[files]] +canonical_package_path = "pyamplicol/assets/models/json/sm/restrict_no_widths.json" +package_sha256 = "fdc03507f82a3d98571b97ba0ab34f712dc0eb02b101f8308b96c6ab8490c08c" +package_size = 885 +source_repository = "git@github.com:rikkert-frederix/AmpliCol.git" +source_revision = "643bc6f99d7b2249af0a85204768df243e612411" +source_path = "pyAmpliCol/src/pyamplicol/assets/models/json/sm/restrict_no_widths.json" +source_blob_oid = "267fd018128278533f8b809ab4c58453ebb7b016" +source_git_mode = "100644" +source_sha256 = "9002d18be203c34418a3966f7cedc5d20844c317c55906efca32f51a38f1f183" +license_ids = [ "GammaLoop-model-assets",] +transformation_id = "loader-restriction-card-v1" +upstream_distribution_path = "assets/models/json/sm/restrict_no_widths.json" +upstream_exact_blob_match = false +generated_from = "ufo/sm/restrict_no_widths.dat" + +[[files]] +canonical_package_path = "pyamplicol/assets/models/json/sm/restrict_zeromass_ckm.json" +package_sha256 = "b8b4376c82f9486266283a383a0347ec42498296180d92249e2e7a772e1a7e1a" +package_size = 911 +source_repository = "git@github.com:rikkert-frederix/AmpliCol.git" +source_revision = "643bc6f99d7b2249af0a85204768df243e612411" +source_path = "pyAmpliCol/src/pyamplicol/assets/models/json/sm/restrict_zeromass_ckm.json" +source_blob_oid = "5a4dd6a33974cd47a32aa3a82625e6f0f38f803c" +source_git_mode = "100644" +source_sha256 = "2173adc123ef202816d56e20772747f8a513ce9294635e099fdbe4832329944d" +license_ids = [ "GammaLoop-model-assets",] +transformation_id = "loader-restriction-card-v1" +upstream_distribution_path = "assets/models/json/sm/restrict_zeromass_ckm.json" +upstream_exact_blob_match = false +generated_from = "ufo/sm/restrict_zeromass_ckm.dat" + +[[files]] +canonical_package_path = "pyamplicol/assets/models/json/sm/sm.json" +package_sha256 = "c1222e3b70b71804e5212e994a3b76e2f098f7fc3ba1693c9f9c65578d7e3e00" +package_size = 120661 +source_repository = "git@github.com:rikkert-frederix/AmpliCol.git" +source_revision = "643bc6f99d7b2249af0a85204768df243e612411" +source_path = "pyAmpliCol/src/pyamplicol/assets/models/json/sm/sm.json" +source_blob_oid = "41927df948a4c22e3295363be3b4c39b3fedc6c3" +source_git_mode = "100644" +source_sha256 = "684f75c88c7faf6832e9e8122970ec5fe98b9cd30bc989b7f8e1818c327fe165" +license_ids = [ "GammaLoop-model-assets",] +transformation_id = "sm-standard-ufo-spinor-order-v1" +upstream_distribution_path = "assets/models/json/sm/sm.json" +upstream_exact_blob_match = false +generated_from = "ufo/sm" + +[[files]] +canonical_package_path = "pyamplicol/assets/models/json/sm/sm_wrapped_indices.json" +package_sha256 = "e1ee0059f3fc534c462e98c358249acbce0a711171fcbfe41720a9a1f24b2933" +package_size = 124855 +source_repository = "git@github.com:rikkert-frederix/AmpliCol.git" +source_revision = "643bc6f99d7b2249af0a85204768df243e612411" +source_path = "pyAmpliCol/src/pyamplicol/assets/models/json/sm/sm_wrapped_indices.json" +source_blob_oid = "bd86527135715c8bdc9f21aef6f511543a5b7fe2" +source_git_mode = "100644" +source_sha256 = "b4221a0985ad5c6987c325a75012a3231995c604e44ad543d0b1ead22eb19404" +license_ids = [ "GammaLoop-model-assets",] +transformation_id = "sm-standard-ufo-spinor-order-v1" +upstream_distribution_path = "assets/models/json/sm/sm_wrapped_indices.json" +upstream_exact_blob_match = false +generated_from = "ufo/sm" + +[[files]] +canonical_package_path = "pyamplicol/assets/models/ufo/scalar_gravity/__init__.py" +package_sha256 = "baf7043f1586a6b70546d04019b11c3fc17f181f278d5d33a70b7217b5e0db71" +package_size = 652 +source_repository = "git@github.com:rikkert-frederix/AmpliCol.git" +source_revision = "643bc6f99d7b2249af0a85204768df243e612411" +source_path = "pyAmpliCol/src/pyamplicol/assets/models/ufo/scalar_gravity/__init__.py" +source_blob_oid = "9a3ad6aac9acfbe3f8e4b4423d5565bffd909fe1" +source_git_mode = "100644" +source_sha256 = "baf7043f1586a6b70546d04019b11c3fc17f181f278d5d33a70b7217b5e0db71" +license_ids = [ "0BSD", "GammaLoop-model-assets",] +transformation_id = "verbatim-git-blob-v1" +upstream_distribution_path = "assets/models/ufo/scalar_gravity/__init__.py" +upstream_exact_blob_match = true + +[[files]] +canonical_package_path = "pyamplicol/assets/models/ufo/scalar_gravity/coupling_orders.py" +package_sha256 = "63738137e1355e064968615a500ecd195395af6f8dea4fd4efee541b47dc7f27" +package_size = 524 +source_repository = "git@github.com:rikkert-frederix/AmpliCol.git" +source_revision = "643bc6f99d7b2249af0a85204768df243e612411" +source_path = "pyAmpliCol/src/pyamplicol/assets/models/ufo/scalar_gravity/coupling_orders.py" +source_blob_oid = "f6a1ec99fbba8b0dcf9abc3192831839f3df1fe5" +source_git_mode = "100644" +source_sha256 = "63738137e1355e064968615a500ecd195395af6f8dea4fd4efee541b47dc7f27" +license_ids = [ "0BSD", "GammaLoop-model-assets",] +transformation_id = "verbatim-git-blob-v1" +upstream_distribution_path = "assets/models/ufo/scalar_gravity/coupling_orders.py" +upstream_exact_blob_match = true + +[[files]] +canonical_package_path = "pyamplicol/assets/models/ufo/scalar_gravity/couplings.py" +package_sha256 = "afd07d76eaefaf4828a243eb63c88196f11da1fcbea59f873c1e3832fbc276fb" +package_size = 1865 +source_repository = "git@github.com:rikkert-frederix/AmpliCol.git" +source_revision = "643bc6f99d7b2249af0a85204768df243e612411" +source_path = "pyAmpliCol/src/pyamplicol/assets/models/ufo/scalar_gravity/couplings.py" +source_blob_oid = "643747e8f3ca22ce8189ffa5d3875028bc9d0424" +source_git_mode = "100644" +source_sha256 = "afd07d76eaefaf4828a243eb63c88196f11da1fcbea59f873c1e3832fbc276fb" +license_ids = [ "0BSD", "GammaLoop-model-assets",] +transformation_id = "verbatim-git-blob-v1" +upstream_distribution_path = "assets/models/ufo/scalar_gravity/couplings.py" +upstream_exact_blob_match = true + +[[files]] +canonical_package_path = "pyamplicol/assets/models/ufo/scalar_gravity/function_library.py" +package_sha256 = "7ab870cbfebbca3858ab8d6780d138b4b2531f6e2be84ff2cf4e6362b6c601bd" +package_size = 1332 +source_repository = "git@github.com:rikkert-frederix/AmpliCol.git" +source_revision = "643bc6f99d7b2249af0a85204768df243e612411" +source_path = "pyAmpliCol/src/pyamplicol/assets/models/ufo/scalar_gravity/function_library.py" +source_blob_oid = "b2843b6ca0834c14389aef93203a7eaeb8022b7c" +source_git_mode = "100644" +source_sha256 = "7ab870cbfebbca3858ab8d6780d138b4b2531f6e2be84ff2cf4e6362b6c601bd" +license_ids = [ "0BSD", "GammaLoop-model-assets",] +transformation_id = "verbatim-git-blob-v1" +upstream_distribution_path = "assets/models/ufo/scalar_gravity/function_library.py" +upstream_exact_blob_match = true + +[[files]] +canonical_package_path = "pyamplicol/assets/models/ufo/scalar_gravity/lorentz.py" +package_sha256 = "28e38a5a20aee6e02605fc2dc2d2e850b05edc1ade5f343fc2ef780ba9666e42" +package_size = 134243 +source_repository = "git@github.com:rikkert-frederix/AmpliCol.git" +source_revision = "643bc6f99d7b2249af0a85204768df243e612411" +source_path = "pyAmpliCol/src/pyamplicol/assets/models/ufo/scalar_gravity/lorentz.py" +source_blob_oid = "768a0b9363a70b28c980c4d2447d4c665b3a07ef" +source_git_mode = "100644" +source_sha256 = "28e38a5a20aee6e02605fc2dc2d2e850b05edc1ade5f343fc2ef780ba9666e42" +license_ids = [ "0BSD", "GammaLoop-model-assets",] +transformation_id = "verbatim-git-blob-v1" +upstream_distribution_path = "assets/models/ufo/scalar_gravity/lorentz.py" +upstream_exact_blob_match = false + +[[files]] +canonical_package_path = "pyamplicol/assets/models/ufo/scalar_gravity/object_library.py" +package_sha256 = "5db44f44bdc8df1ada2c76bd02d5f9241298d0187b963c0d2ee39b88e3910cb3" +package_size = 7079 +source_repository = "git@github.com:rikkert-frederix/AmpliCol.git" +source_revision = "643bc6f99d7b2249af0a85204768df243e612411" +source_path = "pyAmpliCol/src/pyamplicol/assets/models/ufo/scalar_gravity/object_library.py" +source_blob_oid = "85c41db185c460a8ad17d4220365056f04039658" +source_git_mode = "100644" +source_sha256 = "5db44f44bdc8df1ada2c76bd02d5f9241298d0187b963c0d2ee39b88e3910cb3" +license_ids = [ "0BSD", "GammaLoop-model-assets",] +transformation_id = "verbatim-git-blob-v1" +upstream_distribution_path = "assets/models/ufo/scalar_gravity/object_library.py" +upstream_exact_blob_match = true + +[[files]] +canonical_package_path = "pyamplicol/assets/models/ufo/scalar_gravity/param_card.dat" +package_sha256 = "ed708e1744af39117e364be9f8c2f9c7fa844781701a3fb419cff95a8b427dd7" +package_size = 1960 +source_repository = "git@github.com:rikkert-frederix/AmpliCol.git" +source_revision = "643bc6f99d7b2249af0a85204768df243e612411" +source_path = "pyAmpliCol/src/pyamplicol/assets/models/ufo/scalar_gravity/param_card.dat" +source_blob_oid = "84e32fdbbbe9c57cb64d47a00794ec13660ee70e" +source_git_mode = "100644" +source_sha256 = "ed708e1744af39117e364be9f8c2f9c7fa844781701a3fb419cff95a8b427dd7" +license_ids = [ "0BSD", "GammaLoop-model-assets",] +transformation_id = "verbatim-git-blob-v1" +upstream_distribution_path = "assets/models/ufo/scalar_gravity/param_card.dat" +upstream_exact_blob_match = false + +[[files]] +canonical_package_path = "pyamplicol/assets/models/ufo/scalar_gravity/parameters.py" +package_sha256 = "01b0477e0b0e352f968a6d1bfd1e9724c505adc6dd5c1d39dd08e9da97cd9f00" +package_size = 3645 +source_repository = "git@github.com:rikkert-frederix/AmpliCol.git" +source_revision = "643bc6f99d7b2249af0a85204768df243e612411" +source_path = "pyAmpliCol/src/pyamplicol/assets/models/ufo/scalar_gravity/parameters.py" +source_blob_oid = "1fbe07ca4c5f8d8ea72b541930d6293ee6870959" +source_git_mode = "100644" +source_sha256 = "68de6a8ab6ecb1f358a13954be99597bb30665d1e195b7395f8faf405bd8657e" +license_ids = [ "0BSD", "GammaLoop-model-assets",] +transformation_id = "scalar-gravity-static-defaults-v1" +upstream_distribution_path = "assets/models/ufo/scalar_gravity/parameters.py" +upstream_exact_blob_match = true +generated_from = "ufo/scalar_gravity/parameters.py" + +[[files]] +canonical_package_path = "pyamplicol/assets/models/ufo/scalar_gravity/particles.py" +package_sha256 = "90b4efa5fd4c8eb443c43a3a8d7109add2fae94807670098195d6024acd91bd5" +package_size = 1681 +source_repository = "git@github.com:rikkert-frederix/AmpliCol.git" +source_revision = "643bc6f99d7b2249af0a85204768df243e612411" +source_path = "pyAmpliCol/src/pyamplicol/assets/models/ufo/scalar_gravity/particles.py" +source_blob_oid = "bea3ca350ebb8cc66c5179ffb491c82f68d7a026" +source_git_mode = "100644" +source_sha256 = "90b4efa5fd4c8eb443c43a3a8d7109add2fae94807670098195d6024acd91bd5" +license_ids = [ "0BSD", "GammaLoop-model-assets",] +transformation_id = "verbatim-git-blob-v1" +upstream_distribution_path = "assets/models/ufo/scalar_gravity/particles.py" +upstream_exact_blob_match = true + +[[files]] +canonical_package_path = "pyamplicol/assets/models/ufo/scalar_gravity/restrict_default.dat" +package_sha256 = "ed708e1744af39117e364be9f8c2f9c7fa844781701a3fb419cff95a8b427dd7" +package_size = 1960 +source_repository = "git@github.com:rikkert-frederix/AmpliCol.git" +source_revision = "643bc6f99d7b2249af0a85204768df243e612411" +source_path = "pyAmpliCol/src/pyamplicol/assets/models/ufo/scalar_gravity/restrict_default.dat" +source_blob_oid = "84e32fdbbbe9c57cb64d47a00794ec13660ee70e" +source_git_mode = "100644" +source_sha256 = "ed708e1744af39117e364be9f8c2f9c7fa844781701a3fb419cff95a8b427dd7" +license_ids = [ "0BSD", "GammaLoop-model-assets",] +transformation_id = "verbatim-git-blob-v1" +upstream_distribution_path = "assets/models/ufo/scalar_gravity/restrict_default.dat" +upstream_exact_blob_match = false + +[[files]] +canonical_package_path = "pyamplicol/assets/models/ufo/scalar_gravity/restrict_full.dat" +package_sha256 = "0005fc634dd25e095ad70ee49b1789736e6ed31a428794866fea279585a7fbd0" +package_size = 1579 +source_repository = "git@github.com:rikkert-frederix/AmpliCol.git" +source_revision = "643bc6f99d7b2249af0a85204768df243e612411" +source_path = "pyAmpliCol/src/pyamplicol/assets/models/ufo/scalar_gravity/restrict_full.dat" +source_blob_oid = "b206643b681de5bf10fd96112a3a925d5a0e7234" +source_git_mode = "100644" +source_sha256 = "0005fc634dd25e095ad70ee49b1789736e6ed31a428794866fea279585a7fbd0" +license_ids = [ "0BSD", "GammaLoop-model-assets",] +transformation_id = "verbatim-git-blob-v1" +upstream_distribution_path = "assets/models/ufo/scalar_gravity/restrict_full.dat" +upstream_exact_blob_match = false + +[[files]] +canonical_package_path = "pyamplicol/assets/models/ufo/scalar_gravity/run_write_param_card.py" +package_sha256 = "d45878face2f212eaed32932e36ffcaf33e643db347c44d074657292805b47a0" +package_size = 543 +source_repository = "git@github.com:rikkert-frederix/AmpliCol.git" +source_revision = "643bc6f99d7b2249af0a85204768df243e612411" +source_path = "pyAmpliCol/src/pyamplicol/assets/models/ufo/scalar_gravity/run_write_param_card.py" +source_blob_oid = "d87ecc25b963bd42e7f886e06bc475c8f5333e5b" +source_git_mode = "100755" +source_sha256 = "d45878face2f212eaed32932e36ffcaf33e643db347c44d074657292805b47a0" +license_ids = [ "0BSD", "GammaLoop-model-assets",] +transformation_id = "verbatim-git-blob-v1" +upstream_distribution_path = "assets/models/ufo/scalar_gravity/run_write_param_card.py" +upstream_exact_blob_match = true + +[[files]] +canonical_package_path = "pyamplicol/assets/models/ufo/scalar_gravity/vertices.py" +package_sha256 = "4c7d261c81d15242c09cd12403fe8d32a5d41fefc4136e726d5e934d9358e9be" +package_size = 1893 +source_repository = "git@github.com:rikkert-frederix/AmpliCol.git" +source_revision = "643bc6f99d7b2249af0a85204768df243e612411" +source_path = "pyAmpliCol/src/pyamplicol/assets/models/ufo/scalar_gravity/vertices.py" +source_blob_oid = "ca314996c75d9f0750e452a7232ac9a4555b0345" +source_git_mode = "100644" +source_sha256 = "4c7d261c81d15242c09cd12403fe8d32a5d41fefc4136e726d5e934d9358e9be" +license_ids = [ "0BSD", "GammaLoop-model-assets",] +transformation_id = "verbatim-git-blob-v1" +upstream_distribution_path = "assets/models/ufo/scalar_gravity/vertices.py" +upstream_exact_blob_match = true + +[[files]] +canonical_package_path = "pyamplicol/assets/models/ufo/scalar_gravity/write_param_card.py" +package_sha256 = "3008b3269d0689370523a4a6dbcdd4100bb7d78599e2f3df79127ead6fe7fc75" +package_size = 6556 +source_repository = "git@github.com:rikkert-frederix/AmpliCol.git" +source_revision = "643bc6f99d7b2249af0a85204768df243e612411" +source_path = "pyAmpliCol/src/pyamplicol/assets/models/ufo/scalar_gravity/write_param_card.py" +source_blob_oid = "bcf80e99553c7631dc4a1ea92d4a28a87ecde78f" +source_git_mode = "100644" +source_sha256 = "3008b3269d0689370523a4a6dbcdd4100bb7d78599e2f3df79127ead6fe7fc75" +license_ids = [ "0BSD", "GammaLoop-model-assets",] +transformation_id = "verbatim-git-blob-v1" +upstream_distribution_path = "assets/models/ufo/scalar_gravity/write_param_card.py" +upstream_exact_blob_match = false + +[[files]] +canonical_package_path = "pyamplicol/assets/models/ufo/scalars/__init__.py" +package_sha256 = "baf7043f1586a6b70546d04019b11c3fc17f181f278d5d33a70b7217b5e0db71" +package_size = 652 +source_repository = "git@github.com:rikkert-frederix/AmpliCol.git" +source_revision = "643bc6f99d7b2249af0a85204768df243e612411" +source_path = "pyAmpliCol/src/pyamplicol/assets/models/ufo/scalars/__init__.py" +source_blob_oid = "9a3ad6aac9acfbe3f8e4b4423d5565bffd909fe1" +source_git_mode = "100644" +source_sha256 = "baf7043f1586a6b70546d04019b11c3fc17f181f278d5d33a70b7217b5e0db71" +license_ids = [ "0BSD", "GammaLoop-model-assets",] +transformation_id = "verbatim-git-blob-v1" +upstream_distribution_path = "assets/models/ufo/scalars/__init__.py" +upstream_exact_blob_match = true + +[[files]] +canonical_package_path = "pyamplicol/assets/models/ufo/scalars/coupling_orders.py" +package_sha256 = "2e691ad2042a4a7edde293fe69ba96f54ec08221061db0ed1dc30f588011504e" +package_size = 416 +source_repository = "git@github.com:rikkert-frederix/AmpliCol.git" +source_revision = "643bc6f99d7b2249af0a85204768df243e612411" +source_path = "pyAmpliCol/src/pyamplicol/assets/models/ufo/scalars/coupling_orders.py" +source_blob_oid = "cb073b19efe1954a432c2c375f73c363f6e6b53d" +source_git_mode = "100644" +source_sha256 = "2e691ad2042a4a7edde293fe69ba96f54ec08221061db0ed1dc30f588011504e" +license_ids = [ "0BSD", "GammaLoop-model-assets",] +transformation_id = "verbatim-git-blob-v1" +upstream_distribution_path = "assets/models/ufo/scalars/coupling_orders.py" +upstream_exact_blob_match = true + +[[files]] +canonical_package_path = "pyamplicol/assets/models/ufo/scalars/couplings.py" +package_sha256 = "325cfc644fe3b86cffd8de916c3f285016d70fe673d50f3c5ca1c7dcdfaa4183" +package_size = 257 +source_repository = "git@github.com:rikkert-frederix/AmpliCol.git" +source_revision = "643bc6f99d7b2249af0a85204768df243e612411" +source_path = "pyAmpliCol/src/pyamplicol/assets/models/ufo/scalars/couplings.py" +source_blob_oid = "9a3f7c38b09a3d9440e478fefe3b4c6457340bad" +source_git_mode = "100644" +source_sha256 = "325cfc644fe3b86cffd8de916c3f285016d70fe673d50f3c5ca1c7dcdfaa4183" +license_ids = [ "0BSD", "GammaLoop-model-assets",] +transformation_id = "verbatim-git-blob-v1" +upstream_distribution_path = "assets/models/ufo/scalars/couplings.py" +upstream_exact_blob_match = true + +[[files]] +canonical_package_path = "pyamplicol/assets/models/ufo/scalars/function_library.py" +package_sha256 = "7ab870cbfebbca3858ab8d6780d138b4b2531f6e2be84ff2cf4e6362b6c601bd" +package_size = 1332 +source_repository = "git@github.com:rikkert-frederix/AmpliCol.git" +source_revision = "643bc6f99d7b2249af0a85204768df243e612411" +source_path = "pyAmpliCol/src/pyamplicol/assets/models/ufo/scalars/function_library.py" +source_blob_oid = "b2843b6ca0834c14389aef93203a7eaeb8022b7c" +source_git_mode = "100644" +source_sha256 = "7ab870cbfebbca3858ab8d6780d138b4b2531f6e2be84ff2cf4e6362b6c601bd" +license_ids = [ "0BSD", "GammaLoop-model-assets",] +transformation_id = "verbatim-git-blob-v1" +upstream_distribution_path = "assets/models/ufo/scalars/function_library.py" +upstream_exact_blob_match = true + +[[files]] +canonical_package_path = "pyamplicol/assets/models/ufo/scalars/lorentz.py" +package_sha256 = "3daaf714d13328419366cc5c9b6a490d6cd9cff8ace604bf2ec0915061e5b736" +package_size = 726 +source_repository = "git@github.com:rikkert-frederix/AmpliCol.git" +source_revision = "643bc6f99d7b2249af0a85204768df243e612411" +source_path = "pyAmpliCol/src/pyamplicol/assets/models/ufo/scalars/lorentz.py" +source_blob_oid = "85a4a01cac7a4677d484ea5efbfa75c5fe3d661c" +source_git_mode = "100644" +source_sha256 = "3daaf714d13328419366cc5c9b6a490d6cd9cff8ace604bf2ec0915061e5b736" +license_ids = [ "0BSD", "GammaLoop-model-assets",] +transformation_id = "verbatim-git-blob-v1" +upstream_distribution_path = "assets/models/ufo/scalars/lorentz.py" +upstream_exact_blob_match = true + +[[files]] +canonical_package_path = "pyamplicol/assets/models/ufo/scalars/object_library.py" +package_sha256 = "5db44f44bdc8df1ada2c76bd02d5f9241298d0187b963c0d2ee39b88e3910cb3" +package_size = 7079 +source_repository = "git@github.com:rikkert-frederix/AmpliCol.git" +source_revision = "643bc6f99d7b2249af0a85204768df243e612411" +source_path = "pyAmpliCol/src/pyamplicol/assets/models/ufo/scalars/object_library.py" +source_blob_oid = "85c41db185c460a8ad17d4220365056f04039658" +source_git_mode = "100644" +source_sha256 = "5db44f44bdc8df1ada2c76bd02d5f9241298d0187b963c0d2ee39b88e3910cb3" +license_ids = [ "0BSD", "GammaLoop-model-assets",] +transformation_id = "verbatim-git-blob-v1" +upstream_distribution_path = "assets/models/ufo/scalars/object_library.py" +upstream_exact_blob_match = true + +[[files]] +canonical_package_path = "pyamplicol/assets/models/ufo/scalars/parameters.py" +package_sha256 = "7b11a9b1d84126d31f58d5c4752ea0fe0921074bfcb338ecd6ef4d2ad7da89a7" +package_size = 3452 +source_repository = "git@github.com:rikkert-frederix/AmpliCol.git" +source_revision = "643bc6f99d7b2249af0a85204768df243e612411" +source_path = "pyAmpliCol/src/pyamplicol/assets/models/ufo/scalars/parameters.py" +source_blob_oid = "884e4025f897608d8f2773300c8cb1fc71306904" +source_git_mode = "100644" +source_sha256 = "7620698c89ec9a87eb11177f682627e9a334222d42b378c18e111b866f7c3ad0" +license_ids = [ "0BSD", "GammaLoop-model-assets",] +transformation_id = "scalar-static-defaults-v1" +upstream_distribution_path = "assets/models/ufo/scalars/parameters.py" +upstream_exact_blob_match = true +generated_from = "ufo/scalars/parameters.py" + +[[files]] +canonical_package_path = "pyamplicol/assets/models/ufo/scalars/particles.py" +package_sha256 = "1e346940ecf9746445db47219256cc4739444b111f3a2a96967bbb890b57e310" +package_size = 1221 +source_repository = "git@github.com:rikkert-frederix/AmpliCol.git" +source_revision = "643bc6f99d7b2249af0a85204768df243e612411" +source_path = "pyAmpliCol/src/pyamplicol/assets/models/ufo/scalars/particles.py" +source_blob_oid = "20399e78bea20b015fdfd4703a896775f8916cb1" +source_git_mode = "100644" +source_sha256 = "1e346940ecf9746445db47219256cc4739444b111f3a2a96967bbb890b57e310" +license_ids = [ "0BSD", "GammaLoop-model-assets",] +transformation_id = "verbatim-git-blob-v1" +upstream_distribution_path = "assets/models/ufo/scalars/particles.py" +upstream_exact_blob_match = true + +[[files]] +canonical_package_path = "pyamplicol/assets/models/ufo/scalars/restrict_default.dat" +package_sha256 = "020178e9df8fe242fd4d34077db4caff694959a7c6d7a5c829527f8de4e1c80f" +package_size = 1748 +source_repository = "git@github.com:rikkert-frederix/AmpliCol.git" +source_revision = "643bc6f99d7b2249af0a85204768df243e612411" +source_path = "pyAmpliCol/src/pyamplicol/assets/models/ufo/scalars/restrict_default.dat" +source_blob_oid = "8d45909b657a69ab4a7e8f4f799aeb1cc2163959" +source_git_mode = "100644" +source_sha256 = "020178e9df8fe242fd4d34077db4caff694959a7c6d7a5c829527f8de4e1c80f" +license_ids = [ "0BSD", "GammaLoop-model-assets",] +transformation_id = "verbatim-git-blob-v1" +upstream_distribution_path = "assets/models/ufo/scalars/restrict_default.dat" +upstream_exact_blob_match = false + +[[files]] +canonical_package_path = "pyamplicol/assets/models/ufo/scalars/restrict_full.dat" +package_sha256 = "0005fc634dd25e095ad70ee49b1789736e6ed31a428794866fea279585a7fbd0" +package_size = 1579 +source_repository = "git@github.com:rikkert-frederix/AmpliCol.git" +source_revision = "643bc6f99d7b2249af0a85204768df243e612411" +source_path = "pyAmpliCol/src/pyamplicol/assets/models/ufo/scalars/restrict_full.dat" +source_blob_oid = "b206643b681de5bf10fd96112a3a925d5a0e7234" +source_git_mode = "100644" +source_sha256 = "0005fc634dd25e095ad70ee49b1789736e6ed31a428794866fea279585a7fbd0" +license_ids = [ "0BSD", "GammaLoop-model-assets",] +transformation_id = "verbatim-git-blob-v1" +upstream_distribution_path = "assets/models/ufo/scalars/restrict_full.dat" +upstream_exact_blob_match = false + +[[files]] +canonical_package_path = "pyamplicol/assets/models/ufo/scalars/run_write_param_card.py" +package_sha256 = "d45878face2f212eaed32932e36ffcaf33e643db347c44d074657292805b47a0" +package_size = 543 +source_repository = "git@github.com:rikkert-frederix/AmpliCol.git" +source_revision = "643bc6f99d7b2249af0a85204768df243e612411" +source_path = "pyAmpliCol/src/pyamplicol/assets/models/ufo/scalars/run_write_param_card.py" +source_blob_oid = "d87ecc25b963bd42e7f886e06bc475c8f5333e5b" +source_git_mode = "100755" +source_sha256 = "d45878face2f212eaed32932e36ffcaf33e643db347c44d074657292805b47a0" +license_ids = [ "0BSD", "GammaLoop-model-assets",] +transformation_id = "verbatim-git-blob-v1" +upstream_distribution_path = "assets/models/ufo/scalars/run_write_param_card.py" +upstream_exact_blob_match = true + +[[files]] +canonical_package_path = "pyamplicol/assets/models/ufo/scalars/vertices.py" +package_sha256 = "1341725ef6b4bf5eb7ff5fa09d633c1cb52e89b695c627ae42c2ca5664525d4f" +package_size = 1103 +source_repository = "git@github.com:rikkert-frederix/AmpliCol.git" +source_revision = "643bc6f99d7b2249af0a85204768df243e612411" +source_path = "pyAmpliCol/src/pyamplicol/assets/models/ufo/scalars/vertices.py" +source_blob_oid = "b58af0f4ce08f1be499c51dba34fba062a5e4191" +source_git_mode = "100644" +source_sha256 = "1341725ef6b4bf5eb7ff5fa09d633c1cb52e89b695c627ae42c2ca5664525d4f" +license_ids = [ "0BSD", "GammaLoop-model-assets",] +transformation_id = "verbatim-git-blob-v1" +upstream_distribution_path = "assets/models/ufo/scalars/vertices.py" +upstream_exact_blob_match = true + +[[files]] +canonical_package_path = "pyamplicol/assets/models/ufo/scalars/write_param_card.py" +package_sha256 = "3008b3269d0689370523a4a6dbcdd4100bb7d78599e2f3df79127ead6fe7fc75" +package_size = 6556 +source_repository = "git@github.com:rikkert-frederix/AmpliCol.git" +source_revision = "643bc6f99d7b2249af0a85204768df243e612411" +source_path = "pyAmpliCol/src/pyamplicol/assets/models/ufo/scalars/write_param_card.py" +source_blob_oid = "bcf80e99553c7631dc4a1ea92d4a28a87ecde78f" +source_git_mode = "100644" +source_sha256 = "3008b3269d0689370523a4a6dbcdd4100bb7d78599e2f3df79127ead6fe7fc75" +license_ids = [ "0BSD", "GammaLoop-model-assets",] +transformation_id = "verbatim-git-blob-v1" +upstream_distribution_path = "assets/models/ufo/scalars/write_param_card.py" +upstream_exact_blob_match = false + +[[files]] +canonical_package_path = "pyamplicol/assets/models/ufo/sm/__init__.py" +package_sha256 = "dcefb2fa6a4b1653d87944a11dfaedf1b5a2c0a5b27d2370685c2e21d157a568" +package_size = 886 +source_repository = "git@github.com:rikkert-frederix/AmpliCol.git" +source_revision = "643bc6f99d7b2249af0a85204768df243e612411" +source_path = "pyAmpliCol/src/pyamplicol/assets/models/ufo/sm/__init__.py" +source_blob_oid = "a4b9447bcac8c96261eaed4a3e4c8bdcaaae0e9a" +source_git_mode = "100644" +source_sha256 = "dcefb2fa6a4b1653d87944a11dfaedf1b5a2c0a5b27d2370685c2e21d157a568" +license_ids = [ "GammaLoop-model-assets",] +transformation_id = "verbatim-git-blob-v1" +upstream_distribution_path = "assets/models/ufo/sm/__init__.py" +upstream_exact_blob_match = true + +[[files]] +canonical_package_path = "pyamplicol/assets/models/ufo/sm/build_restrict.py" +package_sha256 = "de21ae72ae23f8409d077599f0e4fabbab5287373e3930bc34a19adacd60781e" +package_size = 2886 +source_repository = "git@github.com:rikkert-frederix/AmpliCol.git" +source_revision = "643bc6f99d7b2249af0a85204768df243e612411" +source_path = "pyAmpliCol/src/pyamplicol/assets/models/ufo/sm/build_restrict.py" +source_blob_oid = "838268f2087759d8315a27d3160c9f003c4849d3" +source_git_mode = "100644" +source_sha256 = "de21ae72ae23f8409d077599f0e4fabbab5287373e3930bc34a19adacd60781e" +license_ids = [ "GammaLoop-model-assets", "MadGraph5-aMCatNLO",] +transformation_id = "verbatim-git-blob-v1" +upstream_distribution_path = "assets/models/ufo/sm/build_restrict.py" +upstream_exact_blob_match = false + +[[files]] +canonical_package_path = "pyamplicol/assets/models/ufo/sm/coupling_orders.py" +package_sha256 = "cb45f17e973165af72af7f5fabe368306919b00d8421745c79807b5fda1bdace" +package_size = 485 +source_repository = "git@github.com:rikkert-frederix/AmpliCol.git" +source_revision = "643bc6f99d7b2249af0a85204768df243e612411" +source_path = "pyAmpliCol/src/pyamplicol/assets/models/ufo/sm/coupling_orders.py" +source_blob_oid = "3a0bd46b64bf60b7348433c3fa8b161fc2bcd4bf" +source_git_mode = "100644" +source_sha256 = "cb45f17e973165af72af7f5fabe368306919b00d8421745c79807b5fda1bdace" +license_ids = [ "GammaLoop-model-assets",] +transformation_id = "verbatim-git-blob-v1" +upstream_distribution_path = "assets/models/ufo/sm/coupling_orders.py" +upstream_exact_blob_match = true + +[[files]] +canonical_package_path = "pyamplicol/assets/models/ufo/sm/couplings.py" +package_sha256 = "a5d30cec4efec54b637b5a2ce8017b54fa78f16c546afe33e7410a0279bf979c" +package_size = 14157 +source_repository = "git@github.com:rikkert-frederix/AmpliCol.git" +source_revision = "643bc6f99d7b2249af0a85204768df243e612411" +source_path = "pyAmpliCol/src/pyamplicol/assets/models/ufo/sm/couplings.py" +source_blob_oid = "5e3e67ffd2ef718620644c76aaa687d6cd18a924" +source_git_mode = "100644" +source_sha256 = "a5d30cec4efec54b637b5a2ce8017b54fa78f16c546afe33e7410a0279bf979c" +license_ids = [ "GammaLoop-model-assets",] +transformation_id = "verbatim-git-blob-v1" +upstream_distribution_path = "assets/models/ufo/sm/couplings.py" +upstream_exact_blob_match = false + +[[files]] +canonical_package_path = "pyamplicol/assets/models/ufo/sm/decays.py" +package_sha256 = "12b9bc8544b01e248087f197c2dc9137d274f980178bc72f67d130fb149007a2" +package_size = 13625 +source_repository = "git@github.com:rikkert-frederix/AmpliCol.git" +source_revision = "643bc6f99d7b2249af0a85204768df243e612411" +source_path = "pyAmpliCol/src/pyamplicol/assets/models/ufo/sm/decays.py" +source_blob_oid = "23af667e4c1d5df7ea393bf098daa9982112b588" +source_git_mode = "100644" +source_sha256 = "12b9bc8544b01e248087f197c2dc9137d274f980178bc72f67d130fb149007a2" +license_ids = [ "GammaLoop-model-assets",] +transformation_id = "verbatim-git-blob-v1" +upstream_distribution_path = "assets/models/ufo/sm/decays.py" +upstream_exact_blob_match = false + +[[files]] +canonical_package_path = "pyamplicol/assets/models/ufo/sm/function_library.py" +package_sha256 = "a8821ed8a739e43b705ff7c7aa7d93ff545071b0fe020f5c0c8b0799c30ef2ae" +package_size = 1339 +source_repository = "git@github.com:rikkert-frederix/AmpliCol.git" +source_revision = "643bc6f99d7b2249af0a85204768df243e612411" +source_path = "pyAmpliCol/src/pyamplicol/assets/models/ufo/sm/function_library.py" +source_blob_oid = "33a8975a586a597cfb86f20d7ccb102d804b1787" +source_git_mode = "100644" +source_sha256 = "a8821ed8a739e43b705ff7c7aa7d93ff545071b0fe020f5c0c8b0799c30ef2ae" +license_ids = [ "GammaLoop-model-assets",] +transformation_id = "verbatim-git-blob-v1" +upstream_distribution_path = "assets/models/ufo/sm/function_library.py" +upstream_exact_blob_match = false + +[[files]] +canonical_package_path = "pyamplicol/assets/models/ufo/sm/lorentz.py" +package_sha256 = "1f13e8b63e2ecf428e2875e14bc6901ce49952e1cf2b18ea9934c8d4264e3559" +package_size = 2656 +source_repository = "git@github.com:rikkert-frederix/AmpliCol.git" +source_revision = "643bc6f99d7b2249af0a85204768df243e612411" +source_path = "pyAmpliCol/src/pyamplicol/assets/models/ufo/sm/lorentz.py" +source_blob_oid = "f413c46228114e00282f2d5ba99ffeb8eecd3d7e" +source_git_mode = "100644" +source_sha256 = "cd578c286b038dd3daae396ebd1515cf36d8089b63a1d20247d2b797054b162b" +license_ids = [ "GammaLoop-model-assets",] +transformation_id = "sm-standard-ufo-spinor-order-v1" +upstream_distribution_path = "assets/models/ufo/sm/lorentz.py" +upstream_exact_blob_match = false + +[[files]] +canonical_package_path = "pyamplicol/assets/models/ufo/sm/object_library.py" +package_sha256 = "6391978031caea837ecea426f2f0cfdac748457a00bac99e02c025c42e883cb5" +package_size = 6959 +source_repository = "git@github.com:rikkert-frederix/AmpliCol.git" +source_revision = "643bc6f99d7b2249af0a85204768df243e612411" +source_path = "pyAmpliCol/src/pyamplicol/assets/models/ufo/sm/object_library.py" +source_blob_oid = "d63a4532dd732b2cb471c57b2217113d8c9ccfc9" +source_git_mode = "100644" +source_sha256 = "6391978031caea837ecea426f2f0cfdac748457a00bac99e02c025c42e883cb5" +license_ids = [ "GammaLoop-model-assets",] +transformation_id = "verbatim-git-blob-v1" +upstream_distribution_path = "assets/models/ufo/sm/object_library.py" +upstream_exact_blob_match = false + +[[files]] +canonical_package_path = "pyamplicol/assets/models/ufo/sm/parameters.py" +package_sha256 = "e8599a97d31004d7af8c589a5e734c9141b48d9becdbbde86a37e41635390e38" +package_size = 14852 +source_repository = "git@github.com:rikkert-frederix/AmpliCol.git" +source_revision = "643bc6f99d7b2249af0a85204768df243e612411" +source_path = "pyAmpliCol/src/pyamplicol/assets/models/ufo/sm/parameters.py" +source_blob_oid = "8cf61680938dd9cf68f615ee47d85a281b9b9215" +source_git_mode = "100644" +source_sha256 = "e8599a97d31004d7af8c589a5e734c9141b48d9becdbbde86a37e41635390e38" +license_ids = [ "GammaLoop-model-assets",] +transformation_id = "verbatim-git-blob-v1" +upstream_distribution_path = "assets/models/ufo/sm/parameters.py" +upstream_exact_blob_match = true + +[[files]] +canonical_package_path = "pyamplicol/assets/models/ufo/sm/particles.py" +package_sha256 = "9b542b5d49598ade4d9d6897d569e8feeaa05993e74aef67fb5dfafd5efa9916" +package_size = 10248 +source_repository = "git@github.com:rikkert-frederix/AmpliCol.git" +source_revision = "643bc6f99d7b2249af0a85204768df243e612411" +source_path = "pyAmpliCol/src/pyamplicol/assets/models/ufo/sm/particles.py" +source_blob_oid = "161bd4d13e0e49d8c3ddac24ca4de488c4bcd329" +source_git_mode = "100644" +source_sha256 = "9b542b5d49598ade4d9d6897d569e8feeaa05993e74aef67fb5dfafd5efa9916" +license_ids = [ "GammaLoop-model-assets",] +transformation_id = "verbatim-git-blob-v1" +upstream_distribution_path = "assets/models/ufo/sm/particles.py" +upstream_exact_blob_match = true + +[[files]] +canonical_package_path = "pyamplicol/assets/models/ufo/sm/restrict_c_mass.dat" +package_sha256 = "817a7843a1435c1c7b1f0231ababc346e58f81d8038b9c0b2c75eae32960da9e" +package_size = 1412 +source_repository = "git@github.com:rikkert-frederix/AmpliCol.git" +source_revision = "643bc6f99d7b2249af0a85204768df243e612411" +source_path = "pyAmpliCol/src/pyamplicol/assets/models/ufo/sm/restrict_c_mass.dat" +source_blob_oid = "153d8c6509d5ecedfde606ef3a2bcf434b1960df" +source_git_mode = "100644" +source_sha256 = "817a7843a1435c1c7b1f0231ababc346e58f81d8038b9c0b2c75eae32960da9e" +license_ids = [ "GammaLoop-model-assets",] +transformation_id = "verbatim-git-blob-v1" +upstream_distribution_path = "assets/models/ufo/sm/restrict_c_mass.dat" +upstream_exact_blob_match = false + +[[files]] +canonical_package_path = "pyamplicol/assets/models/ufo/sm/restrict_ckm.dat" +package_sha256 = "75fd361e4ce78100b7dd107cd42b1015d9303da44bec8971e401c2d20d7876cb" +package_size = 1412 +source_repository = "git@github.com:rikkert-frederix/AmpliCol.git" +source_revision = "643bc6f99d7b2249af0a85204768df243e612411" +source_path = "pyAmpliCol/src/pyamplicol/assets/models/ufo/sm/restrict_ckm.dat" +source_blob_oid = "3b0e9f9db10eff5ceedb5ee13e2912014d42a367" +source_git_mode = "100644" +source_sha256 = "75fd361e4ce78100b7dd107cd42b1015d9303da44bec8971e401c2d20d7876cb" +license_ids = [ "GammaLoop-model-assets",] +transformation_id = "verbatim-git-blob-v1" +upstream_distribution_path = "assets/models/ufo/sm/restrict_ckm.dat" +upstream_exact_blob_match = false + +[[files]] +canonical_package_path = "pyamplicol/assets/models/ufo/sm/restrict_default.dat" +package_sha256 = "988939a63e2d35ca991bac864b8f10f6ee19fd13063cb68e91af962767974889" +package_size = 1439 +source_repository = "git@github.com:rikkert-frederix/AmpliCol.git" +source_revision = "643bc6f99d7b2249af0a85204768df243e612411" +source_path = "pyAmpliCol/src/pyamplicol/assets/models/ufo/sm/restrict_default.dat" +source_blob_oid = "2ae557d5ad70599a258322b6fb1d8c15bc4966e2" +source_git_mode = "100644" +source_sha256 = "988939a63e2d35ca991bac864b8f10f6ee19fd13063cb68e91af962767974889" +license_ids = [ "GammaLoop-model-assets",] +transformation_id = "verbatim-git-blob-v1" +upstream_distribution_path = "assets/models/ufo/sm/restrict_default.dat" +upstream_exact_blob_match = false + +[[files]] +canonical_package_path = "pyamplicol/assets/models/ufo/sm/restrict_full.dat" +package_sha256 = "f1e023d45808db115385e5707640a3a3e126217840067a14200e995181eba151" +package_size = 4670 +source_repository = "git@github.com:rikkert-frederix/AmpliCol.git" +source_revision = "643bc6f99d7b2249af0a85204768df243e612411" +source_path = "pyAmpliCol/src/pyamplicol/assets/models/ufo/sm/restrict_full.dat" +source_blob_oid = "38ec0ccc5299c453c792bb0851fd55467521bc54" +source_git_mode = "100644" +source_sha256 = "f1e023d45808db115385e5707640a3a3e126217840067a14200e995181eba151" +license_ids = [ "GammaLoop-model-assets",] +transformation_id = "verbatim-git-blob-v1" +upstream_distribution_path = "assets/models/ufo/sm/restrict_full.dat" +upstream_exact_blob_match = false + +[[files]] +canonical_package_path = "pyamplicol/assets/models/ufo/sm/restrict_lepton_masses.dat" +package_sha256 = "eb44e12f6f4ee591e76277322359e31f4a577a9200acd6c5c8dc6629a039e8c8" +package_size = 1439 +source_repository = "git@github.com:rikkert-frederix/AmpliCol.git" +source_revision = "643bc6f99d7b2249af0a85204768df243e612411" +source_path = "pyAmpliCol/src/pyamplicol/assets/models/ufo/sm/restrict_lepton_masses.dat" +source_blob_oid = "b8d90f3e35ac00e2700b06be399f43a9aeb0a904" +source_git_mode = "100644" +source_sha256 = "eb44e12f6f4ee591e76277322359e31f4a577a9200acd6c5c8dc6629a039e8c8" +license_ids = [ "GammaLoop-model-assets",] +transformation_id = "verbatim-git-blob-v1" +upstream_distribution_path = "assets/models/ufo/sm/restrict_lepton_masses.dat" +upstream_exact_blob_match = false + +[[files]] +canonical_package_path = "pyamplicol/assets/models/ufo/sm/restrict_no_b_mass.dat" +package_sha256 = "32dc93b5584b7bdbf24e87ce12d55398cc2d865e0240ccf6e853c2ade1f90e9e" +package_size = 1412 +source_repository = "git@github.com:rikkert-frederix/AmpliCol.git" +source_revision = "643bc6f99d7b2249af0a85204768df243e612411" +source_path = "pyAmpliCol/src/pyamplicol/assets/models/ufo/sm/restrict_no_b_mass.dat" +source_blob_oid = "f42e3375d13af52b900c0819b13db974a68bbedc" +source_git_mode = "100644" +source_sha256 = "32dc93b5584b7bdbf24e87ce12d55398cc2d865e0240ccf6e853c2ade1f90e9e" +license_ids = [ "GammaLoop-model-assets",] +transformation_id = "verbatim-git-blob-v1" +upstream_distribution_path = "assets/models/ufo/sm/restrict_no_b_mass.dat" +upstream_exact_blob_match = false + +[[files]] +canonical_package_path = "pyamplicol/assets/models/ufo/sm/restrict_no_masses.dat" +package_sha256 = "539664a3651839946cc995257bd9e108054c789f5352ecc0e562260798e66bf0" +package_size = 1412 +source_repository = "git@github.com:rikkert-frederix/AmpliCol.git" +source_revision = "643bc6f99d7b2249af0a85204768df243e612411" +source_path = "pyAmpliCol/src/pyamplicol/assets/models/ufo/sm/restrict_no_masses.dat" +source_blob_oid = "31a72cedc730903d603b00e0b998a76b9165edd2" +source_git_mode = "100644" +source_sha256 = "539664a3651839946cc995257bd9e108054c789f5352ecc0e562260798e66bf0" +license_ids = [ "GammaLoop-model-assets",] +transformation_id = "verbatim-git-blob-v1" +upstream_distribution_path = "assets/models/ufo/sm/restrict_no_masses.dat" +upstream_exact_blob_match = false + +[[files]] +canonical_package_path = "pyamplicol/assets/models/ufo/sm/restrict_no_tau_mass.dat" +package_sha256 = "52433bb4161af5a0f17c91964fdd695b155c0c9821eda37142f04a1b1c2e57f8" +package_size = 1412 +source_repository = "git@github.com:rikkert-frederix/AmpliCol.git" +source_revision = "643bc6f99d7b2249af0a85204768df243e612411" +source_path = "pyAmpliCol/src/pyamplicol/assets/models/ufo/sm/restrict_no_tau_mass.dat" +source_blob_oid = "6497e444d297fbeeafd34f0d50986e04c42884a8" +source_git_mode = "100644" +source_sha256 = "52433bb4161af5a0f17c91964fdd695b155c0c9821eda37142f04a1b1c2e57f8" +license_ids = [ "GammaLoop-model-assets",] +transformation_id = "verbatim-git-blob-v1" +upstream_distribution_path = "assets/models/ufo/sm/restrict_no_tau_mass.dat" +upstream_exact_blob_match = false + +[[files]] +canonical_package_path = "pyamplicol/assets/models/ufo/sm/restrict_no_widths.dat" +package_sha256 = "0ab8569ed8e747b0c0c0ee2c2c93db1e368489237baf8430c53eef605a1634e8" +package_size = 1410 +source_repository = "git@github.com:rikkert-frederix/AmpliCol.git" +source_revision = "643bc6f99d7b2249af0a85204768df243e612411" +source_path = "pyAmpliCol/src/pyamplicol/assets/models/ufo/sm/restrict_no_widths.dat" +source_blob_oid = "55e78884b93d2fcd1a6125d12937ea40974d594a" +source_git_mode = "100644" +source_sha256 = "0ab8569ed8e747b0c0c0ee2c2c93db1e368489237baf8430c53eef605a1634e8" +license_ids = [ "GammaLoop-model-assets",] +transformation_id = "verbatim-git-blob-v1" +upstream_distribution_path = "assets/models/ufo/sm/restrict_no_widths.dat" +upstream_exact_blob_match = false + +[[files]] +canonical_package_path = "pyamplicol/assets/models/ufo/sm/restrict_zeromass_ckm.dat" +package_sha256 = "2388cf490afcfb5530f49ae3a85fec235be236e33c4bb5e1515f3d4827b963c6" +package_size = 1412 +source_repository = "git@github.com:rikkert-frederix/AmpliCol.git" +source_revision = "643bc6f99d7b2249af0a85204768df243e612411" +source_path = "pyAmpliCol/src/pyamplicol/assets/models/ufo/sm/restrict_zeromass_ckm.dat" +source_blob_oid = "c82164bac69678563fad925c7e9fedc8d497c545" +source_git_mode = "100644" +source_sha256 = "2388cf490afcfb5530f49ae3a85fec235be236e33c4bb5e1515f3d4827b963c6" +license_ids = [ "GammaLoop-model-assets",] +transformation_id = "verbatim-git-blob-v1" +upstream_distribution_path = "assets/models/ufo/sm/restrict_zeromass_ckm.dat" +upstream_exact_blob_match = false + +[[files]] +canonical_package_path = "pyamplicol/assets/models/ufo/sm/run_write_param_card.py" +package_sha256 = "d45878face2f212eaed32932e36ffcaf33e643db347c44d074657292805b47a0" +package_size = 543 +source_repository = "git@github.com:rikkert-frederix/AmpliCol.git" +source_revision = "643bc6f99d7b2249af0a85204768df243e612411" +source_path = "pyAmpliCol/src/pyamplicol/assets/models/ufo/sm/run_write_param_card.py" +source_blob_oid = "d87ecc25b963bd42e7f886e06bc475c8f5333e5b" +source_git_mode = "100755" +source_sha256 = "d45878face2f212eaed32932e36ffcaf33e643db347c44d074657292805b47a0" +license_ids = [ "GammaLoop-model-assets",] +transformation_id = "verbatim-git-blob-v1" +upstream_distribution_path = "assets/models/ufo/sm/run_write_param_card.py" +upstream_exact_blob_match = true + +[[files]] +canonical_package_path = "pyamplicol/assets/models/ufo/sm/vertices.py" +package_sha256 = "6f3353b483e6a3f7c6b9f5e79d3a818af8e933ec63f96c18ed271760f6492c68" +package_size = 32556 +source_repository = "git@github.com:rikkert-frederix/AmpliCol.git" +source_revision = "643bc6f99d7b2249af0a85204768df243e612411" +source_path = "pyAmpliCol/src/pyamplicol/assets/models/ufo/sm/vertices.py" +source_blob_oid = "d8c6ceb815df1d152dc651585848bbd273300bed" +source_git_mode = "100644" +source_sha256 = "6f3353b483e6a3f7c6b9f5e79d3a818af8e933ec63f96c18ed271760f6492c68" +license_ids = [ "GammaLoop-model-assets",] +transformation_id = "verbatim-git-blob-v1" +upstream_distribution_path = "assets/models/ufo/sm/vertices.py" +upstream_exact_blob_match = false + +[[files]] +canonical_package_path = "pyamplicol/assets/models/ufo/sm/write_param_card.py" +package_sha256 = "9a289a0f17edaaa0609ccd7d83db3b2d4b389e51f07a0f0c47ffce77afb18c03" +package_size = 6619 +source_repository = "git@github.com:rikkert-frederix/AmpliCol.git" +source_revision = "643bc6f99d7b2249af0a85204768df243e612411" +source_path = "pyAmpliCol/src/pyamplicol/assets/models/ufo/sm/write_param_card.py" +source_blob_oid = "3abf4cff0cb9dca8baba042a0b7c373b4e8b8b64" +source_git_mode = "100644" +source_sha256 = "9a289a0f17edaaa0609ccd7d83db3b2d4b389e51f07a0f0c47ffce77afb18c03" +license_ids = [ "GammaLoop-model-assets",] +transformation_id = "verbatim-git-blob-v1" +upstream_distribution_path = "assets/models/ufo/sm/write_param_card.py" +upstream_exact_blob_match = false + +[source] +repository = "git@github.com:rikkert-frederix/AmpliCol.git" +revision = "643bc6f99d7b2249af0a85204768df243e612411" +root = "pyAmpliCol/src/pyamplicol/assets/models" + +[generator] +distribution = "ufo-model-loader" +version = "0.1.8" +repository = "https://github.com/alphal00p/ufo_model_loader.git" +revision = "70ddee6b416f8c8b340e0d087646d77095c5d24b" +license_id = "MIT" +serialization = "JSONLook.VERBOSE" + +[upstream_distribution] +repository = "https://github.com/alphal00p/gammaloop.git" +revision = "db79edc84f6a1580decbcc4ede7ea0b1c79d9a08" +root = "assets/models" +baseline_exact_blob_matches = 28 +baseline_nonmatching_blobs = 38 +interpretation = "Exactness is asserted per file below; all primary source identities remain the AmpliCol baseline Git objects" diff --git a/pyamplicol/src/pyamplicol/assets/models/json/scalar_gravity/restrict_default.json b/pyamplicol/src/pyamplicol/assets/models/json/scalar_gravity/restrict_default.json new file mode 100644 index 0000000000..f3e10838fa --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/models/json/scalar_gravity/restrict_default.json @@ -0,0 +1,38 @@ +{ + "Gf": [ + 1.16639e-05, + 0.0 + ], + "aEWM1": [ + 132.507, + 0.0 + ], + "aS": [ + 0.118, + 0.0 + ], + "dim": [ + 4.0, + 0.0 + ], + "kappa": [ + 1.0, + 0.0 + ], + "mass_scalar_1": [ + 1.0, + 0.0 + ], + "mass_scalar_2": [ + 2.0, + 0.0 + ], + "width_scalar_1": [ + 0.0, + 0.0 + ], + "width_scalar_2": [ + 0.0, + 0.0 + ] +} \ No newline at end of file diff --git a/pyamplicol/src/pyamplicol/assets/models/json/scalar_gravity/scalar_gravity.json b/pyamplicol/src/pyamplicol/assets/models/json/scalar_gravity/scalar_gravity.json new file mode 100644 index 0000000000..2df4210543 --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/models/json/scalar_gravity/scalar_gravity.json @@ -0,0 +1,867 @@ +{ + "couplings": [ + { + "expression": "UFO::{}::ZERO", + "name": "SSTmpart0", + "orders": [ + [ + "GRAV", + 1 + ] + ], + "value": [ + 0.0, + 0.0 + ] + }, + { + "expression": "UFO::{}::ZERO", + "name": "SSTTTmpart0", + "orders": [ + [ + "GRAV", + 3 + ] + ], + "value": [ + 0.0, + 0.0 + ] + }, + { + "expression": "UFO::{}::ZERO", + "name": "SSTTmpart0", + "orders": [ + [ + "GRAV", + 2 + ] + ], + "value": [ + 0.0, + 0.0 + ] + }, + { + "expression": "1𝑖/2*UFO::{}::kappa*UFO::{}::mass_scalar_1^2", + "name": "SSTmpart1", + "orders": [ + [ + "GRAV", + 1 + ] + ], + "value": [ + 0.0, + 0.5 + ] + }, + { + "expression": "1𝑖/32*UFO::{}::kappa^3*UFO::{}::mass_scalar_1^2", + "name": "SSTTTmpart1", + "orders": [ + [ + "GRAV", + 3 + ] + ], + "value": [ + 0.0, + 0.03125 + ] + }, + { + "expression": "1𝑖/8*UFO::{}::kappa^2*UFO::{}::mass_scalar_1^2", + "name": "SSTTmpart1", + "orders": [ + [ + "GRAV", + 2 + ] + ], + "value": [ + 0.0, + 0.125 + ] + }, + { + "expression": "1𝑖/2*UFO::{}::kappa*UFO::{}::mass_scalar_2^2", + "name": "SSTmpart2", + "orders": [ + [ + "GRAV", + 1 + ] + ], + "value": [ + 0.0, + 2.0 + ] + }, + { + "expression": "1𝑖/32*UFO::{}::kappa^3*UFO::{}::mass_scalar_2^2", + "name": "SSTTTmpart2", + "orders": [ + [ + "GRAV", + 3 + ] + ], + "value": [ + 0.0, + 0.125 + ] + }, + { + "expression": "1𝑖/8*UFO::{}::kappa^2*UFO::{}::mass_scalar_2^2", + "name": "SSTTmpart2", + "orders": [ + [ + "GRAV", + 2 + ] + ], + "value": [ + 0.0, + 0.5 + ] + }, + { + "expression": "1𝑖/2*UFO::{}::kappa", + "name": "SST", + "orders": [ + [ + "GRAV", + 1 + ] + ], + "value": [ + 0.0, + 0.5 + ] + }, + { + "expression": "1𝑖/8*UFO::{}::kappa^2", + "name": "SSTT", + "orders": [ + [ + "GRAV", + 2 + ] + ], + "value": [ + 0.0, + 0.125 + ] + }, + { + "expression": "1𝑖/32*UFO::{}::kappa^3", + "name": "SSTTT", + "orders": [ + [ + "GRAV", + 3 + ] + ], + "value": [ + 0.0, + 0.03125 + ] + }, + { + "expression": "1𝑖/8*UFO::{}::kappa", + "name": "TTT", + "orders": [ + [ + "GRAV", + 1 + ] + ], + "value": [ + 0.0, + 0.125 + ] + }, + { + "expression": "1𝑖/32*UFO::{}::kappa^2", + "name": "TTTT", + "orders": [ + [ + "GRAV", + 2 + ] + ], + "value": [ + 0.0, + 0.03125 + ] + } + ], + "form_factors": [], + "functions": [ + { + "arguments": [ + "z" + ], + "expression": null, + "name": "complexconjugate" + }, + { + "arguments": [ + "z" + ], + "expression": null, + "name": "re" + }, + { + "arguments": [ + "z" + ], + "expression": null, + "name": "im" + }, + { + "arguments": [ + "z" + ], + "expression": null, + "name": "sec" + }, + { + "arguments": [ + "z" + ], + "expression": null, + "name": "asec" + }, + { + "arguments": [ + "z" + ], + "expression": null, + "name": "csc" + }, + { + "arguments": [ + "z" + ], + "expression": null, + "name": "acsc" + } + ], + "lorentz_structures": [ + { + "name": "SSTmpart", + "spins": [ + 1, + 1, + 5 + ], + "structure": "-1*UFO::{}::Metric(1003,2003)" + }, + { + "name": "SST", + "spins": [ + 1, + 1, + 5 + ], + "structure": "-1*UFO::{}::Metric(1003,2003)*UFO::{}::P(-1,1)*UFO::{}::P(-1,2)+UFO::{}::P(1003,1)*UFO::{}::P(2003,2)+UFO::{}::P(1003,2)*UFO::{}::P(2003,1)" + }, + { + "name": "SSTTmpart", + "spins": [ + 1, + 1, + 5, + 5 + ], + "structure": "-2*UFO::{}::Metric(2003,1003)*UFO::{}::Metric(2004,1004)+2*UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2003,2004)+2*UFO::{}::Metric(2003,1004)*UFO::{}::Metric(2004,1003)" + }, + { + "name": "SSTT", + "spins": [ + 1, + 1, + 5, + 5 + ], + "structure": "(-1*UFO::{}::P(1003,1)*UFO::{}::P(2004,2)+-1*UFO::{}::P(1003,2)*UFO::{}::P(2004,1)+UFO::{}::Metric(2004,1003)*UFO::{}::P(-1,1)*UFO::{}::P(-1,2))*UFO::{}::Metric(2003,1004)+(-1*UFO::{}::P(2003,1)*UFO::{}::P(2004,2)+-1*UFO::{}::P(2003,2)*UFO::{}::P(2004,1)+UFO::{}::Metric(2003,2004)*UFO::{}::P(-1,1)*UFO::{}::P(-1,2))*UFO::{}::Metric(1003,1004)+-1*UFO::{}::Metric(2003,1003)*UFO::{}::Metric(2004,1004)*UFO::{}::P(-1,1)*UFO::{}::P(-1,2)+-1*UFO::{}::Metric(2003,2004)*UFO::{}::P(1003,1)*UFO::{}::P(1004,2)+-1*UFO::{}::Metric(2003,2004)*UFO::{}::P(1003,2)*UFO::{}::P(1004,1)+-1*UFO::{}::Metric(2004,1003)*UFO::{}::P(1004,1)*UFO::{}::P(2003,2)+-1*UFO::{}::Metric(2004,1003)*UFO::{}::P(1004,2)*UFO::{}::P(2003,1)+UFO::{}::Metric(2003,1003)*UFO::{}::P(1004,1)*UFO::{}::P(2004,2)+UFO::{}::Metric(2003,1003)*UFO::{}::P(1004,2)*UFO::{}::P(2004,1)+UFO::{}::Metric(2004,1004)*UFO::{}::P(1003,1)*UFO::{}::P(2003,2)+UFO::{}::Metric(2004,1004)*UFO::{}::P(1003,2)*UFO::{}::P(2003,1)" + }, + { + "name": "SSTTTmpart", + "spins": [ + 1, + 1, + 5, + 5, + 5 + ], + "structure": "-1*UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2003,2004)*UFO::{}::Metric(2005,1005)+-1*UFO::{}::Metric(1003,1005)*UFO::{}::Metric(2003,2005)*UFO::{}::Metric(2004,1004)+-1*UFO::{}::Metric(1004,1005)*UFO::{}::Metric(2003,1003)*UFO::{}::Metric(2004,2005)+-1*UFO::{}::Metric(2003,1003)*UFO::{}::Metric(2004,1005)*UFO::{}::Metric(2005,1004)+-1*UFO::{}::Metric(2003,1004)*UFO::{}::Metric(2004,1003)*UFO::{}::Metric(2005,1005)+-1*UFO::{}::Metric(2003,1005)*UFO::{}::Metric(2004,1004)*UFO::{}::Metric(2005,1003)+UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2003,1005)*UFO::{}::Metric(2004,2005)+UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2003,2005)*UFO::{}::Metric(2004,1005)+UFO::{}::Metric(1003,1005)*UFO::{}::Metric(2003,1004)*UFO::{}::Metric(2004,2005)+UFO::{}::Metric(1003,1005)*UFO::{}::Metric(2003,2004)*UFO::{}::Metric(2005,1004)+UFO::{}::Metric(1004,1005)*UFO::{}::Metric(2003,2004)*UFO::{}::Metric(2005,1003)+UFO::{}::Metric(1004,1005)*UFO::{}::Metric(2003,2005)*UFO::{}::Metric(2004,1003)+UFO::{}::Metric(2003,1004)*UFO::{}::Metric(2004,1005)*UFO::{}::Metric(2005,1003)+UFO::{}::Metric(2003,1005)*UFO::{}::Metric(2004,1003)*UFO::{}::Metric(2005,1004)" + }, + { + "name": "SSTTT", + "spins": [ + 1, + 1, + 5, + 5, + 5 + ], + "structure": "((UFO::{}::Metric(2004,1004)*UFO::{}::P(-1,1)*UFO::{}::P(-1,2)+UFO::{}::P(1004,1)*UFO::{}::P(2004,2)+UFO::{}::P(1004,2)*UFO::{}::P(2004,1))*-1*UFO::{}::Metric(2003,2005)+(UFO::{}::P(1004,1)*UFO::{}::P(2003,2)+UFO::{}::P(1004,2)*UFO::{}::P(2003,1))*UFO::{}::Metric(2004,2005)+UFO::{}::Metric(2003,1004)*UFO::{}::P(2004,1)*UFO::{}::P(2005,2)+UFO::{}::Metric(2003,1004)*UFO::{}::P(2004,2)*UFO::{}::P(2005,1)+UFO::{}::Metric(2003,2004)*UFO::{}::P(1004,1)*UFO::{}::P(2005,2)+UFO::{}::Metric(2003,2004)*UFO::{}::P(1004,2)*UFO::{}::P(2005,1)+UFO::{}::Metric(2005,1004)*UFO::{}::P(2003,1)*UFO::{}::P(2004,2)+UFO::{}::Metric(2005,1004)*UFO::{}::P(2003,2)*UFO::{}::P(2004,1))*UFO::{}::Metric(1003,1005)+((UFO::{}::P(1003,1)*UFO::{}::P(2003,2)+UFO::{}::P(1003,2)*UFO::{}::P(2003,1))*-1*UFO::{}::Metric(2004,2005)+-1*UFO::{}::Metric(2003,1003)*UFO::{}::Metric(2004,2005)*UFO::{}::P(-1,1)*UFO::{}::P(-1,2)+UFO::{}::Metric(2003,2004)*UFO::{}::P(1003,1)*UFO::{}::P(2005,2)+UFO::{}::Metric(2003,2004)*UFO::{}::P(1003,2)*UFO::{}::P(2005,1)+UFO::{}::Metric(2003,2005)*UFO::{}::P(1003,1)*UFO::{}::P(2004,2)+UFO::{}::Metric(2003,2005)*UFO::{}::P(1003,2)*UFO::{}::P(2004,1)+UFO::{}::Metric(2004,1003)*UFO::{}::P(2003,1)*UFO::{}::P(2005,2)+UFO::{}::Metric(2004,1003)*UFO::{}::P(2003,2)*UFO::{}::P(2005,1)+UFO::{}::Metric(2005,1003)*UFO::{}::P(2003,1)*UFO::{}::P(2004,2)+UFO::{}::Metric(2005,1003)*UFO::{}::P(2003,2)*UFO::{}::P(2004,1))*UFO::{}::Metric(1004,1005)+-1*UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2003,2004)*UFO::{}::Metric(2005,1005)*UFO::{}::P(-1,1)*UFO::{}::P(-1,2)+-1*UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1005,1)*UFO::{}::P(2005,2)+-1*UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1005,2)*UFO::{}::P(2005,1)+-1*UFO::{}::Metric(2003,1003)*UFO::{}::Metric(2004,1005)*UFO::{}::Metric(2005,1004)*UFO::{}::P(-1,1)*UFO::{}::P(-1,2)+-1*UFO::{}::Metric(2003,1004)*UFO::{}::Metric(2004,1003)*UFO::{}::Metric(2005,1005)*UFO::{}::P(-1,1)*UFO::{}::P(-1,2)+-1*UFO::{}::Metric(2003,1004)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1005,1)*UFO::{}::P(2005,2)+-1*UFO::{}::Metric(2003,1004)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1005,2)*UFO::{}::P(2005,1)+-1*UFO::{}::Metric(2003,1005)*UFO::{}::Metric(2004,1004)*UFO::{}::Metric(2005,1003)*UFO::{}::P(-1,1)*UFO::{}::P(-1,2)+-1*UFO::{}::Metric(2003,1005)*UFO::{}::Metric(2005,1003)*UFO::{}::P(1004,1)*UFO::{}::P(2004,2)+-1*UFO::{}::Metric(2003,1005)*UFO::{}::Metric(2005,1003)*UFO::{}::P(1004,2)*UFO::{}::P(2004,1)+-1*UFO::{}::Metric(2004,1005)*UFO::{}::Metric(2005,1004)*UFO::{}::P(1003,1)*UFO::{}::P(2003,2)+-1*UFO::{}::Metric(2004,1005)*UFO::{}::Metric(2005,1004)*UFO::{}::P(1003,2)*UFO::{}::P(2003,1)+UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2003,1005)*UFO::{}::P(2004,1)*UFO::{}::P(2005,2)+UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2003,1005)*UFO::{}::P(2004,2)*UFO::{}::P(2005,1)+UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2003,2005)*UFO::{}::P(1005,1)*UFO::{}::P(2004,2)+UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2003,2005)*UFO::{}::P(1005,2)*UFO::{}::P(2004,1)+UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2004,1005)*UFO::{}::P(2003,1)*UFO::{}::P(2005,2)+UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2004,1005)*UFO::{}::P(2003,2)*UFO::{}::P(2005,1)+UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2004,2005)*UFO::{}::P(1005,1)*UFO::{}::P(2003,2)+UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2004,2005)*UFO::{}::P(1005,2)*UFO::{}::P(2003,1)+UFO::{}::Metric(2003,1003)*UFO::{}::Metric(2004,1004)*UFO::{}::Metric(2005,1005)*UFO::{}::P(-1,1)*UFO::{}::P(-1,2)+UFO::{}::Metric(2003,1004)*UFO::{}::Metric(2004,1005)*UFO::{}::P(1003,1)*UFO::{}::P(2005,2)+UFO::{}::Metric(2003,1004)*UFO::{}::Metric(2004,1005)*UFO::{}::P(1003,2)*UFO::{}::P(2005,1)+UFO::{}::Metric(2003,1004)*UFO::{}::Metric(2004,2005)*UFO::{}::P(1003,1)*UFO::{}::P(1005,2)+UFO::{}::Metric(2003,1004)*UFO::{}::Metric(2004,2005)*UFO::{}::P(1003,2)*UFO::{}::P(1005,1)+UFO::{}::Metric(2003,1004)*UFO::{}::Metric(2005,1003)*UFO::{}::P(1005,1)*UFO::{}::P(2004,2)+UFO::{}::Metric(2003,1004)*UFO::{}::Metric(2005,1003)*UFO::{}::P(1005,2)*UFO::{}::P(2004,1)+UFO::{}::Metric(2003,1005)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1004,1)*UFO::{}::P(2005,2)+UFO::{}::Metric(2003,1005)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1004,2)*UFO::{}::P(2005,1)+UFO::{}::Metric(2003,1005)*UFO::{}::Metric(2004,2005)*UFO::{}::P(1003,1)*UFO::{}::P(1004,2)+UFO::{}::Metric(2003,1005)*UFO::{}::Metric(2004,2005)*UFO::{}::P(1003,2)*UFO::{}::P(1004,1)+UFO::{}::Metric(2003,1005)*UFO::{}::Metric(2005,1004)*UFO::{}::P(1003,1)*UFO::{}::P(2004,2)+UFO::{}::Metric(2003,1005)*UFO::{}::Metric(2005,1004)*UFO::{}::P(1003,2)*UFO::{}::P(2004,1)+UFO::{}::Metric(2003,2004)*UFO::{}::Metric(2005,1003)*UFO::{}::P(1004,1)*UFO::{}::P(1005,2)+UFO::{}::Metric(2003,2004)*UFO::{}::Metric(2005,1003)*UFO::{}::P(1004,2)*UFO::{}::P(1005,1)+UFO::{}::Metric(2003,2004)*UFO::{}::Metric(2005,1004)*UFO::{}::P(1003,1)*UFO::{}::P(1005,2)+UFO::{}::Metric(2003,2004)*UFO::{}::Metric(2005,1004)*UFO::{}::P(1003,2)*UFO::{}::P(1005,1)+UFO::{}::Metric(2003,2005)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1004,1)*UFO::{}::P(1005,2)+UFO::{}::Metric(2003,2005)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1004,2)*UFO::{}::P(1005,1)+UFO::{}::Metric(2003,2005)*UFO::{}::Metric(2004,1005)*UFO::{}::P(1003,1)*UFO::{}::P(1004,2)+UFO::{}::Metric(2003,2005)*UFO::{}::Metric(2004,1005)*UFO::{}::P(1003,2)*UFO::{}::P(1004,1)+UFO::{}::Metric(2004,1003)*UFO::{}::Metric(2005,1004)*UFO::{}::P(1005,1)*UFO::{}::P(2003,2)+UFO::{}::Metric(2004,1003)*UFO::{}::Metric(2005,1004)*UFO::{}::P(1005,2)*UFO::{}::P(2003,1)+UFO::{}::Metric(2004,1005)*UFO::{}::Metric(2005,1003)*UFO::{}::P(1004,1)*UFO::{}::P(2003,2)+UFO::{}::Metric(2004,1005)*UFO::{}::Metric(2005,1003)*UFO::{}::P(1004,2)*UFO::{}::P(2003,1)" + }, + { + "name": "TTT", + "spins": [ + 5, + 5, + 5 + ], + "structure": "((UFO::{}::P(1001,2)*UFO::{}::P(2003,1)+UFO::{}::P(1001,3)*UFO::{}::P(2003,2))*UFO::{}::Metric(2001,1002)+(UFO::{}::P(2001,2)*UFO::{}::P(2003,1)+UFO::{}::P(2001,3)*UFO::{}::P(2003,2))*UFO::{}::Metric(1001,1002)+-2*UFO::{}::Metric(2003,1002)*UFO::{}::P(1001,2)*UFO::{}::P(2001,3)+-2*UFO::{}::Metric(2003,1002)*UFO::{}::P(1001,3)*UFO::{}::P(2001,2)+UFO::{}::Metric(2001,2003)*UFO::{}::P(1001,2)*UFO::{}::P(1002,3)+UFO::{}::Metric(2001,2003)*UFO::{}::P(1001,3)*UFO::{}::P(1002,1)+UFO::{}::Metric(2003,1001)*UFO::{}::P(1002,1)*UFO::{}::P(2001,3)+UFO::{}::Metric(2003,1001)*UFO::{}::P(1002,3)*UFO::{}::P(2001,2))*-1*UFO::{}::Metric(2002,1003)+((UFO::{}::P(1003,1)*UFO::{}::P(2001,2)+UFO::{}::P(1003,2)*UFO::{}::P(2001,3))*UFO::{}::Metric(2003,1002)+(UFO::{}::P(2001,2)*UFO::{}::P(2003,1)+UFO::{}::P(2001,3)*UFO::{}::P(2003,2))*UFO::{}::Metric(1002,1003)+-2*UFO::{}::Metric(2001,1002)*UFO::{}::P(1003,1)*UFO::{}::P(2003,2)+-2*UFO::{}::Metric(2001,1002)*UFO::{}::P(1003,2)*UFO::{}::P(2003,1)+UFO::{}::Metric(2001,1003)*UFO::{}::P(1002,1)*UFO::{}::P(2003,2)+UFO::{}::Metric(2001,1003)*UFO::{}::P(1002,3)*UFO::{}::P(2003,1)+UFO::{}::Metric(2001,2003)*UFO::{}::P(1002,1)*UFO::{}::P(1003,2)+UFO::{}::Metric(2001,2003)*UFO::{}::P(1002,3)*UFO::{}::P(1003,1))*-1*UFO::{}::Metric(2002,1001)+-1*UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,1003)*UFO::{}::P(2002,1)*UFO::{}::P(2003,2)+-1*UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,1003)*UFO::{}::P(2002,3)*UFO::{}::P(2003,1)+-1*UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,2003)*UFO::{}::P(1003,1)*UFO::{}::P(2002,3)+-1*UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,2003)*UFO::{}::P(1003,2)*UFO::{}::P(2002,1)+-1*UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2002,2003)*UFO::{}::P(1003,1)*UFO::{}::P(2001,2)+-1*UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2002,2003)*UFO::{}::P(1003,2)*UFO::{}::P(2001,3)+-1*UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,1002)*UFO::{}::P(2002,1)*UFO::{}::P(2003,2)+-1*UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,1002)*UFO::{}::P(2002,3)*UFO::{}::P(2003,1)+-1*UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,2002)*UFO::{}::P(1002,1)*UFO::{}::P(2003,2)+-1*UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,2002)*UFO::{}::P(1002,3)*UFO::{}::P(2003,1)+-1*UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2002,2003)*UFO::{}::P(1002,1)*UFO::{}::P(2001,3)+-1*UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2002,2003)*UFO::{}::P(1002,3)*UFO::{}::P(2001,2)+-1*UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2003,1002)*UFO::{}::P(2001,2)*UFO::{}::P(2002,3)+-1*UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2003,1002)*UFO::{}::P(2001,3)*UFO::{}::P(2002,1)+-1*UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,2002)*UFO::{}::P(1001,2)*UFO::{}::P(2003,1)+-1*UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,2002)*UFO::{}::P(1001,3)*UFO::{}::P(2003,2)+-1*UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,2003)*UFO::{}::P(1001,2)*UFO::{}::P(2002,3)+-1*UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,2003)*UFO::{}::P(1001,3)*UFO::{}::P(2002,1)+-1*UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2003,1001)*UFO::{}::P(2001,2)*UFO::{}::P(2002,3)+-1*UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2003,1001)*UFO::{}::P(2001,3)*UFO::{}::P(2002,1)+-1*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,2003)*UFO::{}::P(1001,2)*UFO::{}::P(1003,1)+-1*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,2003)*UFO::{}::P(1001,3)*UFO::{}::P(1003,2)+-1*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2003,1001)*UFO::{}::P(1003,1)*UFO::{}::P(2002,3)+-1*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2003,1001)*UFO::{}::P(1003,2)*UFO::{}::P(2002,1)+-1*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,2003)*UFO::{}::P(1001,2)*UFO::{}::P(1002,3)+-1*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,2003)*UFO::{}::P(1001,3)*UFO::{}::P(1002,1)+-1*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2003,1002)*UFO::{}::P(1001,2)*UFO::{}::P(2002,3)+-1*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2003,1002)*UFO::{}::P(1001,3)*UFO::{}::P(2002,1)+-1*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,1001)*UFO::{}::P(1002,1)*UFO::{}::P(1003,2)+-1*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,1001)*UFO::{}::P(1002,3)*UFO::{}::P(1003,1)+-1*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,1002)*UFO::{}::P(1001,2)*UFO::{}::P(1003,1)+-1*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,1002)*UFO::{}::P(1001,3)*UFO::{}::P(1003,2)+2*UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,2002)*UFO::{}::P(1003,1)*UFO::{}::P(2003,2)+2*UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,2002)*UFO::{}::P(1003,2)*UFO::{}::P(2003,1)+2*UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,2003)*UFO::{}::P(1002,1)*UFO::{}::P(2002,3)+2*UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,2003)*UFO::{}::P(1002,3)*UFO::{}::P(2002,1)+2*UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2002,2003)*UFO::{}::P(1001,2)*UFO::{}::P(2001,3)+2*UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2002,2003)*UFO::{}::P(1001,3)*UFO::{}::P(2001,2)+2*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2003,1001)*UFO::{}::P(1002,1)*UFO::{}::P(2002,3)+2*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2003,1001)*UFO::{}::P(1002,3)*UFO::{}::P(2002,1)" + }, + { + "name": "TTTT", + "spins": [ + 5, + 5, + 5, + 5 + ], + "structure": "-1*UFO::{}::Metric(1001,1002)*UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,2004)*UFO::{}::P(-1,1)*UFO::{}::P(-1,2)+-1*UFO::{}::Metric(1001,1002)*UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,2004)*UFO::{}::P(-1,1)*UFO::{}::P(-1,3)+-1*UFO::{}::Metric(1001,1002)*UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,2004)*UFO::{}::P(-1,2)*UFO::{}::P(-1,4)+-1*UFO::{}::Metric(1001,1002)*UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,2004)*UFO::{}::P(-1,3)*UFO::{}::P(-1,4)+-1*UFO::{}::Metric(1001,1002)*UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,2003)*UFO::{}::P(-1,1)*UFO::{}::P(-1,2)+-1*UFO::{}::Metric(1001,1002)*UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,2003)*UFO::{}::P(-1,1)*UFO::{}::P(-1,4)+-1*UFO::{}::Metric(1001,1002)*UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,2003)*UFO::{}::P(-1,2)*UFO::{}::P(-1,3)+-1*UFO::{}::Metric(1001,1002)*UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,2003)*UFO::{}::P(-1,3)*UFO::{}::P(-1,4)+-1*UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2003,2004)*UFO::{}::P(-1,1)*UFO::{}::P(-1,2)+-1*UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2003,2004)*UFO::{}::P(-1,1)*UFO::{}::P(-1,3)+-1*UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2003,2004)*UFO::{}::P(-1,2)*UFO::{}::P(-1,4)+-1*UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2003,2004)*UFO::{}::P(-1,3)*UFO::{}::P(-1,4)+-1*UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,2004)*UFO::{}::Metric(2003,1004)*UFO::{}::P(-1,1)*UFO::{}::P(-1,2)+-1*UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,2004)*UFO::{}::Metric(2003,1004)*UFO::{}::P(-1,1)*UFO::{}::P(-1,3)+-1*UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,2004)*UFO::{}::Metric(2003,1004)*UFO::{}::P(-1,2)*UFO::{}::P(-1,4)+-1*UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,2004)*UFO::{}::Metric(2003,1004)*UFO::{}::P(-1,3)*UFO::{}::P(-1,4)+-1*UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,2004)*UFO::{}::P(-1,1)*UFO::{}::P(-1,2)+-1*UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,2004)*UFO::{}::P(-1,1)*UFO::{}::P(-1,4)+-1*UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,2004)*UFO::{}::P(-1,2)*UFO::{}::P(-1,3)+-1*UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,2004)*UFO::{}::P(-1,3)*UFO::{}::P(-1,4)+-1*UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,2003)*UFO::{}::Metric(2004,1003)*UFO::{}::P(-1,1)*UFO::{}::P(-1,2)+-1*UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,2003)*UFO::{}::Metric(2004,1003)*UFO::{}::P(-1,1)*UFO::{}::P(-1,4)+-1*UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,2003)*UFO::{}::Metric(2004,1003)*UFO::{}::P(-1,2)*UFO::{}::P(-1,3)+-1*UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,2003)*UFO::{}::Metric(2004,1003)*UFO::{}::P(-1,3)*UFO::{}::P(-1,4)+-1*UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2004,1003)*UFO::{}::P(-1,1)*UFO::{}::P(-1,2)+-1*UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2004,1003)*UFO::{}::P(-1,1)*UFO::{}::P(-1,3)+-1*UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2004,1003)*UFO::{}::P(-1,2)*UFO::{}::P(-1,4)+-1*UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2004,1003)*UFO::{}::P(-1,3)*UFO::{}::P(-1,4)+-1*UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,1004)*UFO::{}::P(-1,1)*UFO::{}::P(-1,2)+-1*UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,1004)*UFO::{}::P(-1,1)*UFO::{}::P(-1,4)+-1*UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,1004)*UFO::{}::P(-1,2)*UFO::{}::P(-1,3)+-1*UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,1004)*UFO::{}::P(-1,3)*UFO::{}::P(-1,4)+-1*UFO::{}::Metric(1001,1003)*UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,2004)*UFO::{}::P(-1,1)*UFO::{}::P(-1,2)+-1*UFO::{}::Metric(1001,1003)*UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,2004)*UFO::{}::P(-1,1)*UFO::{}::P(-1,3)+-1*UFO::{}::Metric(1001,1003)*UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,2004)*UFO::{}::P(-1,2)*UFO::{}::P(-1,4)+-1*UFO::{}::Metric(1001,1003)*UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,2004)*UFO::{}::P(-1,3)*UFO::{}::P(-1,4)+-1*UFO::{}::Metric(1001,1003)*UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,2003)*UFO::{}::P(-1,1)*UFO::{}::P(-1,3)+-1*UFO::{}::Metric(1001,1003)*UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,2003)*UFO::{}::P(-1,1)*UFO::{}::P(-1,4)+-1*UFO::{}::Metric(1001,1003)*UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,2003)*UFO::{}::P(-1,2)*UFO::{}::P(-1,3)+-1*UFO::{}::Metric(1001,1003)*UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,2003)*UFO::{}::P(-1,2)*UFO::{}::P(-1,4)+-1*UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2003,2004)*UFO::{}::P(-1,1)*UFO::{}::P(-1,2)+-1*UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2003,2004)*UFO::{}::P(-1,1)*UFO::{}::P(-1,3)+-1*UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2003,2004)*UFO::{}::P(-1,2)*UFO::{}::P(-1,4)+-1*UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2003,2004)*UFO::{}::P(-1,3)*UFO::{}::P(-1,4)+-1*UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,2004)*UFO::{}::Metric(2003,1004)*UFO::{}::P(-1,1)*UFO::{}::P(-1,2)+-1*UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,2004)*UFO::{}::Metric(2003,1004)*UFO::{}::P(-1,1)*UFO::{}::P(-1,3)+-1*UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,2004)*UFO::{}::Metric(2003,1004)*UFO::{}::P(-1,2)*UFO::{}::P(-1,4)+-1*UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,2004)*UFO::{}::Metric(2003,1004)*UFO::{}::P(-1,3)*UFO::{}::P(-1,4)+-1*UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,2003)*UFO::{}::Metric(2004,1002)*UFO::{}::P(-1,1)*UFO::{}::P(-1,3)+-1*UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,2003)*UFO::{}::Metric(2004,1002)*UFO::{}::P(-1,1)*UFO::{}::P(-1,4)+-1*UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,2003)*UFO::{}::Metric(2004,1002)*UFO::{}::P(-1,2)*UFO::{}::P(-1,3)+-1*UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,2003)*UFO::{}::Metric(2004,1002)*UFO::{}::P(-1,2)*UFO::{}::P(-1,4)+-1*UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,2004)*UFO::{}::Metric(2003,1002)*UFO::{}::P(-1,1)*UFO::{}::P(-1,3)+-1*UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,2004)*UFO::{}::Metric(2003,1002)*UFO::{}::P(-1,1)*UFO::{}::P(-1,4)+-1*UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,2004)*UFO::{}::Metric(2003,1002)*UFO::{}::P(-1,2)*UFO::{}::P(-1,3)+-1*UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,2004)*UFO::{}::Metric(2003,1002)*UFO::{}::P(-1,2)*UFO::{}::P(-1,4)+-1*UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,1004)*UFO::{}::Metric(2004,1002)*UFO::{}::P(-1,1)*UFO::{}::P(-1,2)+-1*UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,1004)*UFO::{}::Metric(2004,1002)*UFO::{}::P(-1,1)*UFO::{}::P(-1,3)+-1*UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,1004)*UFO::{}::Metric(2004,1002)*UFO::{}::P(-1,2)*UFO::{}::P(-1,4)+-1*UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,1004)*UFO::{}::Metric(2004,1002)*UFO::{}::P(-1,3)*UFO::{}::P(-1,4)+-1*UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2003,1002)*UFO::{}::P(-1,1)*UFO::{}::P(-1,3)+-1*UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2003,1002)*UFO::{}::P(-1,1)*UFO::{}::P(-1,4)+-1*UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2003,1002)*UFO::{}::P(-1,2)*UFO::{}::P(-1,3)+-1*UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2003,1002)*UFO::{}::P(-1,2)*UFO::{}::P(-1,4)+-1*UFO::{}::Metric(1001,1004)*UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,2004)*UFO::{}::P(-1,1)*UFO::{}::P(-1,2)+-1*UFO::{}::Metric(1001,1004)*UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,2004)*UFO::{}::P(-1,1)*UFO::{}::P(-1,4)+-1*UFO::{}::Metric(1001,1004)*UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,2004)*UFO::{}::P(-1,2)*UFO::{}::P(-1,3)+-1*UFO::{}::Metric(1001,1004)*UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,2004)*UFO::{}::P(-1,3)*UFO::{}::P(-1,4)+-1*UFO::{}::Metric(1001,1004)*UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,2004)*UFO::{}::P(-1,1)*UFO::{}::P(-1,3)+-1*UFO::{}::Metric(1001,1004)*UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,2004)*UFO::{}::P(-1,1)*UFO::{}::P(-1,4)+-1*UFO::{}::Metric(1001,1004)*UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,2004)*UFO::{}::P(-1,2)*UFO::{}::P(-1,3)+-1*UFO::{}::Metric(1001,1004)*UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,2004)*UFO::{}::P(-1,2)*UFO::{}::P(-1,4)+-1*UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,2004)*UFO::{}::P(-1,1)*UFO::{}::P(-1,2)+-1*UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,2004)*UFO::{}::P(-1,1)*UFO::{}::P(-1,4)+-1*UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,2004)*UFO::{}::P(-1,2)*UFO::{}::P(-1,3)+-1*UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,2004)*UFO::{}::P(-1,3)*UFO::{}::P(-1,4)+-1*UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,2003)*UFO::{}::Metric(2004,1003)*UFO::{}::P(-1,1)*UFO::{}::P(-1,2)+-1*UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,2003)*UFO::{}::Metric(2004,1003)*UFO::{}::P(-1,1)*UFO::{}::P(-1,4)+-1*UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,2003)*UFO::{}::Metric(2004,1003)*UFO::{}::P(-1,2)*UFO::{}::P(-1,3)+-1*UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,2003)*UFO::{}::Metric(2004,1003)*UFO::{}::P(-1,3)*UFO::{}::P(-1,4)+-1*UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,2003)*UFO::{}::Metric(2004,1002)*UFO::{}::P(-1,1)*UFO::{}::P(-1,3)+-1*UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,2003)*UFO::{}::Metric(2004,1002)*UFO::{}::P(-1,1)*UFO::{}::P(-1,4)+-1*UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,2003)*UFO::{}::Metric(2004,1002)*UFO::{}::P(-1,2)*UFO::{}::P(-1,3)+-1*UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,2003)*UFO::{}::Metric(2004,1002)*UFO::{}::P(-1,2)*UFO::{}::P(-1,4)+-1*UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,2004)*UFO::{}::Metric(2003,1002)*UFO::{}::P(-1,1)*UFO::{}::P(-1,3)+-1*UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,2004)*UFO::{}::Metric(2003,1002)*UFO::{}::P(-1,1)*UFO::{}::P(-1,4)+-1*UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,2004)*UFO::{}::Metric(2003,1002)*UFO::{}::P(-1,2)*UFO::{}::P(-1,3)+-1*UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,2004)*UFO::{}::Metric(2003,1002)*UFO::{}::P(-1,2)*UFO::{}::P(-1,4)+-1*UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,1002)*UFO::{}::Metric(2004,1003)*UFO::{}::P(-1,1)*UFO::{}::P(-1,2)+-1*UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,1002)*UFO::{}::Metric(2004,1003)*UFO::{}::P(-1,1)*UFO::{}::P(-1,4)+-1*UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,1002)*UFO::{}::Metric(2004,1003)*UFO::{}::P(-1,2)*UFO::{}::P(-1,3)+-1*UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,1002)*UFO::{}::Metric(2004,1003)*UFO::{}::P(-1,3)*UFO::{}::P(-1,4)+-1*UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2004,1002)*UFO::{}::P(-1,1)*UFO::{}::P(-1,3)+-1*UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2004,1002)*UFO::{}::P(-1,1)*UFO::{}::P(-1,4)+-1*UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2004,1002)*UFO::{}::P(-1,2)*UFO::{}::P(-1,3)+-1*UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2004,1002)*UFO::{}::P(-1,2)*UFO::{}::P(-1,4)+-1*UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,2004)*UFO::{}::P(-1,1)*UFO::{}::P(-1,2)+-1*UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,2004)*UFO::{}::P(-1,1)*UFO::{}::P(-1,4)+-1*UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,2004)*UFO::{}::P(-1,2)*UFO::{}::P(-1,3)+-1*UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,2004)*UFO::{}::P(-1,3)*UFO::{}::P(-1,4)+-1*UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,2004)*UFO::{}::Metric(2003,1001)*UFO::{}::P(-1,1)*UFO::{}::P(-1,3)+-1*UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,2004)*UFO::{}::Metric(2003,1001)*UFO::{}::P(-1,1)*UFO::{}::P(-1,4)+-1*UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,2004)*UFO::{}::Metric(2003,1001)*UFO::{}::P(-1,2)*UFO::{}::P(-1,3)+-1*UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,2004)*UFO::{}::Metric(2003,1001)*UFO::{}::P(-1,2)*UFO::{}::P(-1,4)+-1*UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,1004)*UFO::{}::Metric(2004,1001)*UFO::{}::P(-1,1)*UFO::{}::P(-1,2)+-1*UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,1004)*UFO::{}::Metric(2004,1001)*UFO::{}::P(-1,1)*UFO::{}::P(-1,4)+-1*UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,1004)*UFO::{}::Metric(2004,1001)*UFO::{}::P(-1,2)*UFO::{}::P(-1,3)+-1*UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,1004)*UFO::{}::Metric(2004,1001)*UFO::{}::P(-1,3)*UFO::{}::P(-1,4)+-1*UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2004,1001)*UFO::{}::P(-1,1)*UFO::{}::P(-1,3)+-1*UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2004,1001)*UFO::{}::P(-1,1)*UFO::{}::P(-1,4)+-1*UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2004,1001)*UFO::{}::P(-1,2)*UFO::{}::P(-1,3)+-1*UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2004,1001)*UFO::{}::P(-1,2)*UFO::{}::P(-1,4)+-1*UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,1004)*UFO::{}::P(-1,1)*UFO::{}::P(-1,2)+-1*UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,1004)*UFO::{}::P(-1,1)*UFO::{}::P(-1,4)+-1*UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,1004)*UFO::{}::P(-1,2)*UFO::{}::P(-1,3)+-1*UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,1004)*UFO::{}::P(-1,3)*UFO::{}::P(-1,4)+-1*UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2003,1001)*UFO::{}::P(-1,1)*UFO::{}::P(-1,3)+-1*UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2003,1001)*UFO::{}::P(-1,1)*UFO::{}::P(-1,4)+-1*UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2003,1001)*UFO::{}::P(-1,2)*UFO::{}::P(-1,3)+-1*UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2003,1001)*UFO::{}::P(-1,2)*UFO::{}::P(-1,4)+-1*UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,2004)*UFO::{}::P(-1,1)*UFO::{}::P(-1,2)+-1*UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,2004)*UFO::{}::P(-1,1)*UFO::{}::P(-1,3)+-1*UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,2004)*UFO::{}::P(-1,2)*UFO::{}::P(-1,4)+-1*UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,2004)*UFO::{}::P(-1,3)*UFO::{}::P(-1,4)+-1*UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,2003)*UFO::{}::Metric(2004,1001)*UFO::{}::P(-1,1)*UFO::{}::P(-1,3)+-1*UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,2003)*UFO::{}::Metric(2004,1001)*UFO::{}::P(-1,1)*UFO::{}::P(-1,4)+-1*UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,2003)*UFO::{}::Metric(2004,1001)*UFO::{}::P(-1,2)*UFO::{}::P(-1,3)+-1*UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,2003)*UFO::{}::Metric(2004,1001)*UFO::{}::P(-1,2)*UFO::{}::P(-1,4)+-1*UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,1001)*UFO::{}::Metric(2004,1003)*UFO::{}::P(-1,1)*UFO::{}::P(-1,2)+-1*UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,1001)*UFO::{}::Metric(2004,1003)*UFO::{}::P(-1,1)*UFO::{}::P(-1,3)+-1*UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,1001)*UFO::{}::Metric(2004,1003)*UFO::{}::P(-1,2)*UFO::{}::P(-1,4)+-1*UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,1001)*UFO::{}::Metric(2004,1003)*UFO::{}::P(-1,3)*UFO::{}::P(-1,4)+-1*UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2004,1003)*UFO::{}::P(-1,1)*UFO::{}::P(-1,2)+-1*UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2004,1003)*UFO::{}::P(-1,1)*UFO::{}::P(-1,3)+-1*UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2004,1003)*UFO::{}::P(-1,2)*UFO::{}::P(-1,4)+-1*UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2004,1003)*UFO::{}::P(-1,3)*UFO::{}::P(-1,4)+-1*UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2004,1001)*UFO::{}::P(-1,1)*UFO::{}::P(-1,3)+-1*UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2004,1001)*UFO::{}::P(-1,1)*UFO::{}::P(-1,4)+-1*UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2004,1001)*UFO::{}::P(-1,2)*UFO::{}::P(-1,3)+-1*UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2004,1001)*UFO::{}::P(-1,2)*UFO::{}::P(-1,4)+-1*UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,1001)*UFO::{}::P(-1,1)*UFO::{}::P(-1,3)+-1*UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,1001)*UFO::{}::P(-1,1)*UFO::{}::P(-1,4)+-1*UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,1001)*UFO::{}::P(-1,2)*UFO::{}::P(-1,3)+-1*UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,1001)*UFO::{}::P(-1,2)*UFO::{}::P(-1,4)+-1*UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,2003)*UFO::{}::Metric(2004,1001)*UFO::{}::P(-1,1)*UFO::{}::P(-1,2)+-1*UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,2003)*UFO::{}::Metric(2004,1001)*UFO::{}::P(-1,1)*UFO::{}::P(-1,4)+-1*UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,2003)*UFO::{}::Metric(2004,1001)*UFO::{}::P(-1,2)*UFO::{}::P(-1,3)+-1*UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,2003)*UFO::{}::Metric(2004,1001)*UFO::{}::P(-1,3)*UFO::{}::P(-1,4)+-1*UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,2004)*UFO::{}::Metric(2003,1001)*UFO::{}::P(-1,1)*UFO::{}::P(-1,2)+-1*UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,2004)*UFO::{}::Metric(2003,1001)*UFO::{}::P(-1,1)*UFO::{}::P(-1,3)+-1*UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,2004)*UFO::{}::Metric(2003,1001)*UFO::{}::P(-1,2)*UFO::{}::P(-1,4)+-1*UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,2004)*UFO::{}::Metric(2003,1001)*UFO::{}::P(-1,3)*UFO::{}::P(-1,4)+-1*UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,1001)*UFO::{}::Metric(2004,1002)*UFO::{}::P(-1,1)*UFO::{}::P(-1,2)+-1*UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,1001)*UFO::{}::Metric(2004,1002)*UFO::{}::P(-1,1)*UFO::{}::P(-1,3)+-1*UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,1001)*UFO::{}::Metric(2004,1002)*UFO::{}::P(-1,2)*UFO::{}::P(-1,4)+-1*UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,1001)*UFO::{}::Metric(2004,1002)*UFO::{}::P(-1,3)*UFO::{}::P(-1,4)+-1*UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,1002)*UFO::{}::Metric(2004,1001)*UFO::{}::P(-1,1)*UFO::{}::P(-1,2)+-1*UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,1002)*UFO::{}::Metric(2004,1001)*UFO::{}::P(-1,1)*UFO::{}::P(-1,4)+-1*UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,1002)*UFO::{}::Metric(2004,1001)*UFO::{}::P(-1,2)*UFO::{}::P(-1,3)+-1*UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,1002)*UFO::{}::Metric(2004,1001)*UFO::{}::P(-1,3)*UFO::{}::P(-1,4)+-1*UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2004,1002)*UFO::{}::P(-1,1)*UFO::{}::P(-1,2)+-1*UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2004,1002)*UFO::{}::P(-1,1)*UFO::{}::P(-1,3)+-1*UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2004,1002)*UFO::{}::P(-1,2)*UFO::{}::P(-1,4)+-1*UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2004,1002)*UFO::{}::P(-1,3)*UFO::{}::P(-1,4)+-1*UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,1002)*UFO::{}::P(-1,1)*UFO::{}::P(-1,2)+-1*UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,1002)*UFO::{}::P(-1,1)*UFO::{}::P(-1,4)+-1*UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,1002)*UFO::{}::P(-1,2)*UFO::{}::P(-1,3)+-1*UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,1002)*UFO::{}::P(-1,3)*UFO::{}::P(-1,4)+-1*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,1004)*UFO::{}::Metric(2004,1001)*UFO::{}::P(-1,1)*UFO::{}::P(-1,2)+-1*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,1004)*UFO::{}::Metric(2004,1001)*UFO::{}::P(-1,1)*UFO::{}::P(-1,4)+-1*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,1004)*UFO::{}::Metric(2004,1001)*UFO::{}::P(-1,2)*UFO::{}::P(-1,3)+-1*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,1004)*UFO::{}::Metric(2004,1001)*UFO::{}::P(-1,3)*UFO::{}::P(-1,4)+-1*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2003,1001)*UFO::{}::Metric(2004,1003)*UFO::{}::P(-1,1)*UFO::{}::P(-1,2)+-1*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2003,1001)*UFO::{}::Metric(2004,1003)*UFO::{}::P(-1,1)*UFO::{}::P(-1,3)+-1*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2003,1001)*UFO::{}::Metric(2004,1003)*UFO::{}::P(-1,2)*UFO::{}::P(-1,4)+-1*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2003,1001)*UFO::{}::Metric(2004,1003)*UFO::{}::P(-1,3)*UFO::{}::P(-1,4)+-1*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,1004)*UFO::{}::Metric(2004,1002)*UFO::{}::P(-1,1)*UFO::{}::P(-1,2)+-1*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,1004)*UFO::{}::Metric(2004,1002)*UFO::{}::P(-1,1)*UFO::{}::P(-1,3)+-1*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,1004)*UFO::{}::Metric(2004,1002)*UFO::{}::P(-1,2)*UFO::{}::P(-1,4)+-1*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,1004)*UFO::{}::Metric(2004,1002)*UFO::{}::P(-1,3)*UFO::{}::P(-1,4)+-1*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2003,1002)*UFO::{}::Metric(2004,1001)*UFO::{}::P(-1,1)*UFO::{}::P(-1,3)+-1*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2003,1002)*UFO::{}::Metric(2004,1001)*UFO::{}::P(-1,1)*UFO::{}::P(-1,4)+-1*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2003,1002)*UFO::{}::Metric(2004,1001)*UFO::{}::P(-1,2)*UFO::{}::P(-1,3)+-1*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2003,1002)*UFO::{}::Metric(2004,1001)*UFO::{}::P(-1,2)*UFO::{}::P(-1,4)+-1*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,1002)*UFO::{}::Metric(2004,1003)*UFO::{}::P(-1,1)*UFO::{}::P(-1,2)+-1*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,1002)*UFO::{}::Metric(2004,1003)*UFO::{}::P(-1,1)*UFO::{}::P(-1,4)+-1*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,1002)*UFO::{}::Metric(2004,1003)*UFO::{}::P(-1,2)*UFO::{}::P(-1,3)+-1*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,1002)*UFO::{}::Metric(2004,1003)*UFO::{}::P(-1,3)*UFO::{}::P(-1,4)+-1*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,1001)*UFO::{}::Metric(2004,1002)*UFO::{}::P(-1,1)*UFO::{}::P(-1,3)+-1*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,1001)*UFO::{}::Metric(2004,1002)*UFO::{}::P(-1,1)*UFO::{}::P(-1,4)+-1*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,1001)*UFO::{}::Metric(2004,1002)*UFO::{}::P(-1,2)*UFO::{}::P(-1,3)+-1*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,1001)*UFO::{}::Metric(2004,1002)*UFO::{}::P(-1,2)*UFO::{}::P(-1,4)+-2*UFO::{}::Metric(1001,1002)*UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,2002)*UFO::{}::P(2003,1)*UFO::{}::P(2004,2)+-2*UFO::{}::Metric(1001,1002)*UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,2002)*UFO::{}::P(2003,1)*UFO::{}::P(2004,3)+-2*UFO::{}::Metric(1001,1002)*UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,2002)*UFO::{}::P(2003,2)*UFO::{}::P(2004,1)+-2*UFO::{}::Metric(1001,1002)*UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,2002)*UFO::{}::P(2003,2)*UFO::{}::P(2004,3)+-2*UFO::{}::Metric(1001,1002)*UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,2002)*UFO::{}::P(2003,4)*UFO::{}::P(2004,1)+-2*UFO::{}::Metric(1001,1002)*UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,2002)*UFO::{}::P(2003,4)*UFO::{}::P(2004,2)+-2*UFO::{}::Metric(1001,1002)*UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2003,2004)*UFO::{}::P(2001,2)*UFO::{}::P(2002,3)+-2*UFO::{}::Metric(1001,1002)*UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2003,2004)*UFO::{}::P(2001,2)*UFO::{}::P(2002,4)+-2*UFO::{}::Metric(1001,1002)*UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2003,2004)*UFO::{}::P(2001,3)*UFO::{}::P(2002,1)+-2*UFO::{}::Metric(1001,1002)*UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2003,2004)*UFO::{}::P(2001,3)*UFO::{}::P(2002,4)+-2*UFO::{}::Metric(1001,1002)*UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2003,2004)*UFO::{}::P(2001,4)*UFO::{}::P(2002,1)+-2*UFO::{}::Metric(1001,1002)*UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2003,2004)*UFO::{}::P(2001,4)*UFO::{}::P(2002,3)+-2*UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,1003)*UFO::{}::P(1004,1)*UFO::{}::P(2004,2)+-2*UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,1003)*UFO::{}::P(1004,2)*UFO::{}::P(2004,1)+-2*UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,1004)*UFO::{}::P(1003,1)*UFO::{}::P(2004,2)+-2*UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,1004)*UFO::{}::P(1003,1)*UFO::{}::P(2004,3)+-2*UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,1004)*UFO::{}::P(1003,2)*UFO::{}::P(2004,1)+-2*UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,1004)*UFO::{}::P(1003,2)*UFO::{}::P(2004,3)+-2*UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,1004)*UFO::{}::P(1003,4)*UFO::{}::P(2004,1)+-2*UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,1004)*UFO::{}::P(1003,4)*UFO::{}::P(2004,2)+-2*UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1003,1)*UFO::{}::P(1004,2)+-2*UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1003,1)*UFO::{}::P(1004,3)+-2*UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1003,2)*UFO::{}::P(1004,1)+-2*UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1003,2)*UFO::{}::P(1004,3)+-2*UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1003,4)*UFO::{}::P(1004,1)+-2*UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1003,4)*UFO::{}::P(1004,2)+-2*UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1004,1)*UFO::{}::P(2003,2)+-2*UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1004,1)*UFO::{}::P(2003,4)+-2*UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1004,2)*UFO::{}::P(2003,1)+-2*UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1004,2)*UFO::{}::P(2003,4)+-2*UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1004,3)*UFO::{}::P(2003,1)+-2*UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1004,3)*UFO::{}::P(2003,2)+-2*UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2004,1004)*UFO::{}::P(1003,1)*UFO::{}::P(2003,2)+-2*UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2004,1004)*UFO::{}::P(1003,2)*UFO::{}::P(2003,1)+-2*UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2003,1003)*UFO::{}::Metric(2004,1004)*UFO::{}::P(2001,3)*UFO::{}::P(2002,4)+-2*UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2003,1003)*UFO::{}::Metric(2004,1004)*UFO::{}::P(2001,4)*UFO::{}::P(2002,3)+-2*UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2003,1004)*UFO::{}::Metric(2004,1003)*UFO::{}::P(2001,2)*UFO::{}::P(2002,3)+-2*UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2003,1004)*UFO::{}::Metric(2004,1003)*UFO::{}::P(2001,2)*UFO::{}::P(2002,4)+-2*UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2003,1004)*UFO::{}::Metric(2004,1003)*UFO::{}::P(2001,3)*UFO::{}::P(2002,1)+-2*UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2003,1004)*UFO::{}::Metric(2004,1003)*UFO::{}::P(2001,3)*UFO::{}::P(2002,4)+-2*UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2003,1004)*UFO::{}::Metric(2004,1003)*UFO::{}::P(2001,4)*UFO::{}::P(2002,1)+-2*UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2003,1004)*UFO::{}::Metric(2004,1003)*UFO::{}::P(2001,4)*UFO::{}::P(2002,3)+-2*UFO::{}::Metric(1001,1003)*UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,2003)*UFO::{}::P(2002,1)*UFO::{}::P(2004,2)+-2*UFO::{}::Metric(1001,1003)*UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,2003)*UFO::{}::P(2002,1)*UFO::{}::P(2004,3)+-2*UFO::{}::Metric(1001,1003)*UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,2003)*UFO::{}::P(2002,3)*UFO::{}::P(2004,1)+-2*UFO::{}::Metric(1001,1003)*UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,2003)*UFO::{}::P(2002,3)*UFO::{}::P(2004,2)+-2*UFO::{}::Metric(1001,1003)*UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,2003)*UFO::{}::P(2002,4)*UFO::{}::P(2004,1)+-2*UFO::{}::Metric(1001,1003)*UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,2003)*UFO::{}::P(2002,4)*UFO::{}::P(2004,3)+-2*UFO::{}::Metric(1001,1003)*UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2002,2004)*UFO::{}::P(2001,2)*UFO::{}::P(2003,1)+-2*UFO::{}::Metric(1001,1003)*UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2002,2004)*UFO::{}::P(2001,2)*UFO::{}::P(2003,4)+-2*UFO::{}::Metric(1001,1003)*UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2002,2004)*UFO::{}::P(2001,3)*UFO::{}::P(2003,2)+-2*UFO::{}::Metric(1001,1003)*UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2002,2004)*UFO::{}::P(2001,3)*UFO::{}::P(2003,4)+-2*UFO::{}::Metric(1001,1003)*UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2002,2004)*UFO::{}::P(2001,4)*UFO::{}::P(2003,1)+-2*UFO::{}::Metric(1001,1003)*UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2002,2004)*UFO::{}::P(2001,4)*UFO::{}::P(2003,2)+-2*UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,1002)*UFO::{}::P(1004,1)*UFO::{}::P(2004,3)+-2*UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,1002)*UFO::{}::P(1004,3)*UFO::{}::P(2004,1)+-2*UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,1004)*UFO::{}::P(1002,1)*UFO::{}::P(2004,2)+-2*UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,1004)*UFO::{}::P(1002,1)*UFO::{}::P(2004,3)+-2*UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,1004)*UFO::{}::P(1002,3)*UFO::{}::P(2004,1)+-2*UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,1004)*UFO::{}::P(1002,3)*UFO::{}::P(2004,2)+-2*UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,1004)*UFO::{}::P(1002,4)*UFO::{}::P(2004,1)+-2*UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,1004)*UFO::{}::P(1002,4)*UFO::{}::P(2004,3)+-2*UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,2004)*UFO::{}::P(1002,1)*UFO::{}::P(1004,2)+-2*UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,2004)*UFO::{}::P(1002,1)*UFO::{}::P(1004,3)+-2*UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,2004)*UFO::{}::P(1002,3)*UFO::{}::P(1004,1)+-2*UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,2004)*UFO::{}::P(1002,3)*UFO::{}::P(1004,2)+-2*UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,2004)*UFO::{}::P(1002,4)*UFO::{}::P(1004,1)+-2*UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,2004)*UFO::{}::P(1002,4)*UFO::{}::P(1004,3)+-2*UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1004,1)*UFO::{}::P(2002,3)+-2*UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1004,1)*UFO::{}::P(2002,4)+-2*UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1004,2)*UFO::{}::P(2002,1)+-2*UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1004,2)*UFO::{}::P(2002,3)+-2*UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1004,3)*UFO::{}::P(2002,1)+-2*UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1004,3)*UFO::{}::P(2002,4)+-2*UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2004,1004)*UFO::{}::P(1002,1)*UFO::{}::P(2002,3)+-2*UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2004,1004)*UFO::{}::P(1002,3)*UFO::{}::P(2002,1)+-2*UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2002,1002)*UFO::{}::Metric(2004,1004)*UFO::{}::P(2001,2)*UFO::{}::P(2003,4)+-2*UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2002,1002)*UFO::{}::Metric(2004,1004)*UFO::{}::P(2001,4)*UFO::{}::P(2003,2)+-2*UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2004,1002)*UFO::{}::P(2001,2)*UFO::{}::P(2003,1)+-2*UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2004,1002)*UFO::{}::P(2001,2)*UFO::{}::P(2003,4)+-2*UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2004,1002)*UFO::{}::P(2001,3)*UFO::{}::P(2003,2)+-2*UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2004,1002)*UFO::{}::P(2001,3)*UFO::{}::P(2003,4)+-2*UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2004,1002)*UFO::{}::P(2001,4)*UFO::{}::P(2003,1)+-2*UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2004,1002)*UFO::{}::P(2001,4)*UFO::{}::P(2003,2)+-2*UFO::{}::Metric(1001,1004)*UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,2004)*UFO::{}::P(2002,1)*UFO::{}::P(2003,2)+-2*UFO::{}::Metric(1001,1004)*UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,2004)*UFO::{}::P(2002,1)*UFO::{}::P(2003,4)+-2*UFO::{}::Metric(1001,1004)*UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,2004)*UFO::{}::P(2002,3)*UFO::{}::P(2003,1)+-2*UFO::{}::Metric(1001,1004)*UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,2004)*UFO::{}::P(2002,3)*UFO::{}::P(2003,4)+-2*UFO::{}::Metric(1001,1004)*UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,2004)*UFO::{}::P(2002,4)*UFO::{}::P(2003,1)+-2*UFO::{}::Metric(1001,1004)*UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,2004)*UFO::{}::P(2002,4)*UFO::{}::P(2003,2)+-2*UFO::{}::Metric(1001,1004)*UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2002,2003)*UFO::{}::P(2001,2)*UFO::{}::P(2004,1)+-2*UFO::{}::Metric(1001,1004)*UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2002,2003)*UFO::{}::P(2001,2)*UFO::{}::P(2004,3)+-2*UFO::{}::Metric(1001,1004)*UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2002,2003)*UFO::{}::P(2001,3)*UFO::{}::P(2004,1)+-2*UFO::{}::Metric(1001,1004)*UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2002,2003)*UFO::{}::P(2001,3)*UFO::{}::P(2004,2)+-2*UFO::{}::Metric(1001,1004)*UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2002,2003)*UFO::{}::P(2001,4)*UFO::{}::P(2004,2)+-2*UFO::{}::Metric(1001,1004)*UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2002,2003)*UFO::{}::P(2001,4)*UFO::{}::P(2004,3)+-2*UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,1002)*UFO::{}::P(1003,1)*UFO::{}::P(2003,4)+-2*UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,1002)*UFO::{}::P(1003,4)*UFO::{}::P(2003,1)+-2*UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,1003)*UFO::{}::P(1002,1)*UFO::{}::P(2003,2)+-2*UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,1003)*UFO::{}::P(1002,1)*UFO::{}::P(2003,4)+-2*UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,1003)*UFO::{}::P(1002,3)*UFO::{}::P(2003,1)+-2*UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,1003)*UFO::{}::P(1002,3)*UFO::{}::P(2003,4)+-2*UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,1003)*UFO::{}::P(1002,4)*UFO::{}::P(2003,1)+-2*UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,1003)*UFO::{}::P(1002,4)*UFO::{}::P(2003,2)+-2*UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,2003)*UFO::{}::P(1002,1)*UFO::{}::P(1003,2)+-2*UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,2003)*UFO::{}::P(1002,1)*UFO::{}::P(1003,4)+-2*UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,2003)*UFO::{}::P(1002,3)*UFO::{}::P(1003,1)+-2*UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,2003)*UFO::{}::P(1002,3)*UFO::{}::P(1003,4)+-2*UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,2003)*UFO::{}::P(1002,4)*UFO::{}::P(1003,1)+-2*UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,2003)*UFO::{}::P(1002,4)*UFO::{}::P(1003,2)+-2*UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2003,1002)*UFO::{}::P(1003,1)*UFO::{}::P(2002,3)+-2*UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2003,1002)*UFO::{}::P(1003,1)*UFO::{}::P(2002,4)+-2*UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2003,1002)*UFO::{}::P(1003,2)*UFO::{}::P(2002,1)+-2*UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2003,1002)*UFO::{}::P(1003,2)*UFO::{}::P(2002,4)+-2*UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2003,1002)*UFO::{}::P(1003,4)*UFO::{}::P(2002,1)+-2*UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2003,1002)*UFO::{}::P(1003,4)*UFO::{}::P(2002,3)+-2*UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2003,1003)*UFO::{}::P(1002,1)*UFO::{}::P(2002,4)+-2*UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2003,1003)*UFO::{}::P(1002,4)*UFO::{}::P(2002,1)+-2*UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2002,1002)*UFO::{}::Metric(2003,1003)*UFO::{}::P(2001,2)*UFO::{}::P(2004,3)+-2*UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2002,1002)*UFO::{}::Metric(2003,1003)*UFO::{}::P(2001,3)*UFO::{}::P(2004,2)+-2*UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,1002)*UFO::{}::P(2001,2)*UFO::{}::P(2004,1)+-2*UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,1002)*UFO::{}::P(2001,2)*UFO::{}::P(2004,3)+-2*UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,1002)*UFO::{}::P(2001,3)*UFO::{}::P(2004,1)+-2*UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,1002)*UFO::{}::P(2001,3)*UFO::{}::P(2004,2)+-2*UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,1002)*UFO::{}::P(2001,4)*UFO::{}::P(2004,2)+-2*UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,1002)*UFO::{}::P(2001,4)*UFO::{}::P(2004,3)+-2*UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,1001)*UFO::{}::Metric(2002,2003)*UFO::{}::P(1004,2)*UFO::{}::P(2004,3)+-2*UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,1001)*UFO::{}::Metric(2002,2003)*UFO::{}::P(1004,3)*UFO::{}::P(2004,2)+-2*UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,1001)*UFO::{}::Metric(2004,1004)*UFO::{}::P(2002,1)*UFO::{}::P(2003,4)+-2*UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,1001)*UFO::{}::Metric(2004,1004)*UFO::{}::P(2002,4)*UFO::{}::P(2003,1)+-2*UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,2003)*UFO::{}::P(1001,2)*UFO::{}::P(2004,1)+-2*UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,2003)*UFO::{}::P(1001,2)*UFO::{}::P(2004,3)+-2*UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,2003)*UFO::{}::P(1001,3)*UFO::{}::P(2004,1)+-2*UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,2003)*UFO::{}::P(1001,3)*UFO::{}::P(2004,2)+-2*UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,2003)*UFO::{}::P(1001,4)*UFO::{}::P(2004,2)+-2*UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,2003)*UFO::{}::P(1001,4)*UFO::{}::P(2004,3)+-2*UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2004,1001)*UFO::{}::P(2002,1)*UFO::{}::P(2003,2)+-2*UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2004,1001)*UFO::{}::P(2002,1)*UFO::{}::P(2003,4)+-2*UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2004,1001)*UFO::{}::P(2002,3)*UFO::{}::P(2003,1)+-2*UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2004,1001)*UFO::{}::P(2002,3)*UFO::{}::P(2003,4)+-2*UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2004,1001)*UFO::{}::P(2002,4)*UFO::{}::P(2003,1)+-2*UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2004,1001)*UFO::{}::P(2002,4)*UFO::{}::P(2003,2)+-2*UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,2003)*UFO::{}::P(1001,2)*UFO::{}::P(1004,1)+-2*UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,2003)*UFO::{}::P(1001,2)*UFO::{}::P(1004,3)+-2*UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,2003)*UFO::{}::P(1001,3)*UFO::{}::P(1004,1)+-2*UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,2003)*UFO::{}::P(1001,3)*UFO::{}::P(1004,2)+-2*UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,2003)*UFO::{}::P(1001,4)*UFO::{}::P(1004,2)+-2*UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,2003)*UFO::{}::P(1001,4)*UFO::{}::P(1004,3)+-2*UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2002,2003)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1004,1)*UFO::{}::P(2001,2)+-2*UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2002,2003)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1004,1)*UFO::{}::P(2001,3)+-2*UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2002,2003)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1004,2)*UFO::{}::P(2001,3)+-2*UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2002,2003)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1004,2)*UFO::{}::P(2001,4)+-2*UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2002,2003)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1004,3)*UFO::{}::P(2001,2)+-2*UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2002,2003)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1004,3)*UFO::{}::P(2001,4)+-2*UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2002,2003)*UFO::{}::Metric(2004,1004)*UFO::{}::P(1001,2)*UFO::{}::P(2001,3)+-2*UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2002,2003)*UFO::{}::Metric(2004,1004)*UFO::{}::P(1001,3)*UFO::{}::P(2001,2)+-2*UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,1001)*UFO::{}::Metric(2002,2004)*UFO::{}::P(1003,2)*UFO::{}::P(2003,4)+-2*UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,1001)*UFO::{}::Metric(2002,2004)*UFO::{}::P(1003,4)*UFO::{}::P(2003,2)+-2*UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,1001)*UFO::{}::Metric(2003,1003)*UFO::{}::P(2002,1)*UFO::{}::P(2004,3)+-2*UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,1001)*UFO::{}::Metric(2003,1003)*UFO::{}::P(2002,3)*UFO::{}::P(2004,1)+-2*UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,2004)*UFO::{}::P(1001,2)*UFO::{}::P(2003,1)+-2*UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,2004)*UFO::{}::P(1001,2)*UFO::{}::P(2003,4)+-2*UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,2004)*UFO::{}::P(1001,3)*UFO::{}::P(2003,2)+-2*UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,2004)*UFO::{}::P(1001,3)*UFO::{}::P(2003,4)+-2*UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,2004)*UFO::{}::P(1001,4)*UFO::{}::P(2003,1)+-2*UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,2004)*UFO::{}::P(1001,4)*UFO::{}::P(2003,2)+-2*UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2003,1001)*UFO::{}::P(2002,1)*UFO::{}::P(2004,2)+-2*UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2003,1001)*UFO::{}::P(2002,1)*UFO::{}::P(2004,3)+-2*UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2003,1001)*UFO::{}::P(2002,3)*UFO::{}::P(2004,1)+-2*UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2003,1001)*UFO::{}::P(2002,3)*UFO::{}::P(2004,2)+-2*UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2003,1001)*UFO::{}::P(2002,4)*UFO::{}::P(2004,1)+-2*UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2003,1001)*UFO::{}::P(2002,4)*UFO::{}::P(2004,3)+-2*UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,2004)*UFO::{}::P(1001,2)*UFO::{}::P(1003,1)+-2*UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,2004)*UFO::{}::P(1001,2)*UFO::{}::P(1003,4)+-2*UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,2004)*UFO::{}::P(1001,3)*UFO::{}::P(1003,2)+-2*UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,2004)*UFO::{}::P(1001,3)*UFO::{}::P(1003,4)+-2*UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,2004)*UFO::{}::P(1001,4)*UFO::{}::P(1003,1)+-2*UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,2004)*UFO::{}::P(1001,4)*UFO::{}::P(1003,2)+-2*UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2002,2004)*UFO::{}::Metric(2003,1001)*UFO::{}::P(1003,1)*UFO::{}::P(2001,2)+-2*UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2002,2004)*UFO::{}::Metric(2003,1001)*UFO::{}::P(1003,1)*UFO::{}::P(2001,4)+-2*UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2002,2004)*UFO::{}::Metric(2003,1001)*UFO::{}::P(1003,2)*UFO::{}::P(2001,3)+-2*UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2002,2004)*UFO::{}::Metric(2003,1001)*UFO::{}::P(1003,2)*UFO::{}::P(2001,4)+-2*UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2002,2004)*UFO::{}::Metric(2003,1001)*UFO::{}::P(1003,4)*UFO::{}::P(2001,2)+-2*UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2002,2004)*UFO::{}::Metric(2003,1001)*UFO::{}::P(1003,4)*UFO::{}::P(2001,3)+-2*UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2002,2004)*UFO::{}::Metric(2003,1003)*UFO::{}::P(1001,2)*UFO::{}::P(2001,4)+-2*UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2002,2004)*UFO::{}::Metric(2003,1003)*UFO::{}::P(1001,4)*UFO::{}::P(2001,2)+-2*UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,1001)*UFO::{}::Metric(2002,1002)*UFO::{}::P(2003,1)*UFO::{}::P(2004,2)+-2*UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,1001)*UFO::{}::Metric(2002,1002)*UFO::{}::P(2003,2)*UFO::{}::P(2004,1)+-2*UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,1001)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1002,3)*UFO::{}::P(2002,4)+-2*UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,1001)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1002,4)*UFO::{}::P(2002,3)+-2*UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,1001)*UFO::{}::P(2003,1)*UFO::{}::P(2004,2)+-2*UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,1001)*UFO::{}::P(2003,1)*UFO::{}::P(2004,3)+-2*UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,1001)*UFO::{}::P(2003,2)*UFO::{}::P(2004,1)+-2*UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,1001)*UFO::{}::P(2003,2)*UFO::{}::P(2004,3)+-2*UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,1001)*UFO::{}::P(2003,4)*UFO::{}::P(2004,1)+-2*UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,1001)*UFO::{}::P(2003,4)*UFO::{}::P(2004,2)+-2*UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1001,2)*UFO::{}::P(2002,3)+-2*UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1001,2)*UFO::{}::P(2002,4)+-2*UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1001,3)*UFO::{}::P(2002,1)+-2*UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1001,3)*UFO::{}::P(2002,4)+-2*UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1001,4)*UFO::{}::P(2002,1)+-2*UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1001,4)*UFO::{}::P(2002,3)+-2*UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1001,2)*UFO::{}::P(1002,3)+-2*UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1001,2)*UFO::{}::P(1002,4)+-2*UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1001,3)*UFO::{}::P(1002,1)+-2*UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1001,3)*UFO::{}::P(1002,4)+-2*UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1001,4)*UFO::{}::P(1002,1)+-2*UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1001,4)*UFO::{}::P(1002,3)+-2*UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1002,1)*UFO::{}::P(2001,3)+-2*UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1002,1)*UFO::{}::P(2001,4)+-2*UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1002,3)*UFO::{}::P(2001,2)+-2*UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1002,3)*UFO::{}::P(2001,4)+-2*UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1002,4)*UFO::{}::P(2001,2)+-2*UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1002,4)*UFO::{}::P(2001,3)+-2*UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2002,1002)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1001,3)*UFO::{}::P(2001,4)+-2*UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2002,1002)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1001,4)*UFO::{}::P(2001,3)+-2*UFO::{}::Metric(2001,1001)*UFO::{}::Metric(2002,1002)*UFO::{}::Metric(2003,1004)*UFO::{}::P(1003,1)*UFO::{}::P(2004,2)+-2*UFO::{}::Metric(2001,1001)*UFO::{}::Metric(2002,1002)*UFO::{}::Metric(2003,1004)*UFO::{}::P(1003,2)*UFO::{}::P(2004,1)+-2*UFO::{}::Metric(2001,1001)*UFO::{}::Metric(2002,1002)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1003,1)*UFO::{}::P(1004,2)+-2*UFO::{}::Metric(2001,1001)*UFO::{}::Metric(2002,1002)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1003,2)*UFO::{}::P(1004,1)+-2*UFO::{}::Metric(2001,1001)*UFO::{}::Metric(2002,1002)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1004,1)*UFO::{}::P(2003,2)+-2*UFO::{}::Metric(2001,1001)*UFO::{}::Metric(2002,1002)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1004,2)*UFO::{}::P(2003,1)+-2*UFO::{}::Metric(2001,1001)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,1002)*UFO::{}::P(1004,2)*UFO::{}::P(2004,3)+-2*UFO::{}::Metric(2001,1001)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,1002)*UFO::{}::P(1004,3)*UFO::{}::P(2004,2)+-2*UFO::{}::Metric(2001,1001)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2004,1004)*UFO::{}::P(1002,1)*UFO::{}::P(2003,4)+-2*UFO::{}::Metric(2001,1001)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2004,1004)*UFO::{}::P(1002,4)*UFO::{}::P(2003,1)+-2*UFO::{}::Metric(2001,1001)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2003,1003)*UFO::{}::P(1002,1)*UFO::{}::P(2004,3)+-2*UFO::{}::Metric(2001,1001)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2003,1003)*UFO::{}::P(1002,3)*UFO::{}::P(2004,1)+-2*UFO::{}::Metric(2001,1001)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1003,2)*UFO::{}::P(2003,4)+-2*UFO::{}::Metric(2001,1001)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1003,4)*UFO::{}::P(2003,2)+-2*UFO::{}::Metric(2001,1001)*UFO::{}::Metric(2002,2003)*UFO::{}::Metric(2004,1004)*UFO::{}::P(1002,1)*UFO::{}::P(1003,4)+-2*UFO::{}::Metric(2001,1001)*UFO::{}::Metric(2002,2003)*UFO::{}::Metric(2004,1004)*UFO::{}::P(1002,4)*UFO::{}::P(1003,1)+-2*UFO::{}::Metric(2001,1001)*UFO::{}::Metric(2002,2004)*UFO::{}::Metric(2003,1003)*UFO::{}::P(1002,1)*UFO::{}::P(1004,3)+-2*UFO::{}::Metric(2001,1001)*UFO::{}::Metric(2002,2004)*UFO::{}::Metric(2003,1003)*UFO::{}::P(1002,3)*UFO::{}::P(1004,1)+-2*UFO::{}::Metric(2001,1001)*UFO::{}::Metric(2003,1002)*UFO::{}::Metric(2004,1004)*UFO::{}::P(1003,1)*UFO::{}::P(2002,4)+-2*UFO::{}::Metric(2001,1001)*UFO::{}::Metric(2003,1002)*UFO::{}::Metric(2004,1004)*UFO::{}::P(1003,4)*UFO::{}::P(2002,1)+-2*UFO::{}::Metric(2001,1001)*UFO::{}::Metric(2003,1003)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1004,1)*UFO::{}::P(2002,3)+-2*UFO::{}::Metric(2001,1001)*UFO::{}::Metric(2003,1003)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1004,3)*UFO::{}::P(2002,1)+-2*UFO::{}::Metric(2001,1001)*UFO::{}::Metric(2003,1004)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1002,3)*UFO::{}::P(2002,4)+-2*UFO::{}::Metric(2001,1001)*UFO::{}::Metric(2003,1004)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1002,4)*UFO::{}::P(2002,3)+-2*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,1003)*UFO::{}::P(1004,1)*UFO::{}::P(2004,2)+-2*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,1003)*UFO::{}::P(1004,2)*UFO::{}::P(2004,1)+-2*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,1004)*UFO::{}::P(1003,1)*UFO::{}::P(2004,2)+-2*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,1004)*UFO::{}::P(1003,1)*UFO::{}::P(2004,3)+-2*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,1004)*UFO::{}::P(1003,2)*UFO::{}::P(2004,1)+-2*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,1004)*UFO::{}::P(1003,2)*UFO::{}::P(2004,3)+-2*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,1004)*UFO::{}::P(1003,4)*UFO::{}::P(2004,1)+-2*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,1004)*UFO::{}::P(1003,4)*UFO::{}::P(2004,2)+-2*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1003,1)*UFO::{}::P(1004,2)+-2*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1003,1)*UFO::{}::P(1004,3)+-2*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1003,2)*UFO::{}::P(1004,1)+-2*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1003,2)*UFO::{}::P(1004,3)+-2*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1003,4)*UFO::{}::P(1004,1)+-2*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1003,4)*UFO::{}::P(1004,2)+-2*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1004,1)*UFO::{}::P(2003,2)+-2*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1004,1)*UFO::{}::P(2003,4)+-2*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1004,2)*UFO::{}::P(2003,1)+-2*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1004,2)*UFO::{}::P(2003,4)+-2*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1004,3)*UFO::{}::P(2003,1)+-2*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1004,3)*UFO::{}::P(2003,2)+-2*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2004,1004)*UFO::{}::P(1003,1)*UFO::{}::P(2003,2)+-2*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2004,1004)*UFO::{}::P(1003,2)*UFO::{}::P(2003,1)+-2*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2003,1003)*UFO::{}::Metric(2004,1004)*UFO::{}::P(1001,3)*UFO::{}::P(2002,4)+-2*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2003,1003)*UFO::{}::Metric(2004,1004)*UFO::{}::P(1001,4)*UFO::{}::P(2002,3)+-2*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2003,1004)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1001,2)*UFO::{}::P(2002,3)+-2*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2003,1004)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1001,2)*UFO::{}::P(2002,4)+-2*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2003,1004)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1001,3)*UFO::{}::P(2002,1)+-2*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2003,1004)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1001,3)*UFO::{}::P(2002,4)+-2*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2003,1004)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1001,4)*UFO::{}::P(2002,1)+-2*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2003,1004)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1001,4)*UFO::{}::P(2002,3)+-2*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,1002)*UFO::{}::Metric(2003,1001)*UFO::{}::P(1004,1)*UFO::{}::P(2004,3)+-2*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,1002)*UFO::{}::Metric(2003,1001)*UFO::{}::P(1004,3)*UFO::{}::P(2004,1)+-2*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,1002)*UFO::{}::Metric(2004,1004)*UFO::{}::P(1001,2)*UFO::{}::P(2003,4)+-2*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,1002)*UFO::{}::Metric(2004,1004)*UFO::{}::P(1001,4)*UFO::{}::P(2003,2)+-2*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2003,1001)*UFO::{}::P(1002,1)*UFO::{}::P(2004,2)+-2*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2003,1001)*UFO::{}::P(1002,1)*UFO::{}::P(2004,3)+-2*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2003,1001)*UFO::{}::P(1002,3)*UFO::{}::P(2004,1)+-2*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2003,1001)*UFO::{}::P(1002,3)*UFO::{}::P(2004,2)+-2*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2003,1001)*UFO::{}::P(1002,4)*UFO::{}::P(2004,1)+-2*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2003,1001)*UFO::{}::P(1002,4)*UFO::{}::P(2004,3)+-2*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1001,2)*UFO::{}::P(2003,1)+-2*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1001,2)*UFO::{}::P(2003,4)+-2*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1001,3)*UFO::{}::P(2003,2)+-2*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1001,3)*UFO::{}::P(2003,4)+-2*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1001,4)*UFO::{}::P(2003,1)+-2*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1001,4)*UFO::{}::P(2003,2)+-2*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,2004)*UFO::{}::Metric(2003,1001)*UFO::{}::P(1002,1)*UFO::{}::P(1004,2)+-2*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,2004)*UFO::{}::Metric(2003,1001)*UFO::{}::P(1002,1)*UFO::{}::P(1004,3)+-2*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,2004)*UFO::{}::Metric(2003,1001)*UFO::{}::P(1002,3)*UFO::{}::P(1004,1)+-2*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,2004)*UFO::{}::Metric(2003,1001)*UFO::{}::P(1002,3)*UFO::{}::P(1004,2)+-2*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,2004)*UFO::{}::Metric(2003,1001)*UFO::{}::P(1002,4)*UFO::{}::P(1004,1)+-2*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,2004)*UFO::{}::Metric(2003,1001)*UFO::{}::P(1002,4)*UFO::{}::P(1004,3)+-2*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2003,1001)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1004,1)*UFO::{}::P(2002,3)+-2*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2003,1001)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1004,1)*UFO::{}::P(2002,4)+-2*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2003,1001)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1004,2)*UFO::{}::P(2002,1)+-2*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2003,1001)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1004,2)*UFO::{}::P(2002,3)+-2*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2003,1001)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1004,3)*UFO::{}::P(2002,1)+-2*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2003,1001)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1004,3)*UFO::{}::P(2002,4)+-2*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2003,1001)*UFO::{}::Metric(2004,1004)*UFO::{}::P(1002,1)*UFO::{}::P(2002,3)+-2*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2003,1001)*UFO::{}::Metric(2004,1004)*UFO::{}::P(1002,3)*UFO::{}::P(2002,1)+-2*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,1002)*UFO::{}::Metric(2003,1003)*UFO::{}::P(1001,2)*UFO::{}::P(2004,3)+-2*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,1002)*UFO::{}::Metric(2003,1003)*UFO::{}::P(1001,3)*UFO::{}::P(2004,2)+-2*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,1002)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1003,1)*UFO::{}::P(2003,4)+-2*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,1002)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1003,4)*UFO::{}::P(2003,1)+-2*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,1002)*UFO::{}::P(1001,2)*UFO::{}::P(2004,1)+-2*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,1002)*UFO::{}::P(1001,2)*UFO::{}::P(2004,3)+-2*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,1002)*UFO::{}::P(1001,3)*UFO::{}::P(2004,1)+-2*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,1002)*UFO::{}::P(1001,3)*UFO::{}::P(2004,2)+-2*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,1002)*UFO::{}::P(1001,4)*UFO::{}::P(2004,2)+-2*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,1002)*UFO::{}::P(1001,4)*UFO::{}::P(2004,3)+-2*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1002,1)*UFO::{}::P(2003,2)+-2*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1002,1)*UFO::{}::P(2003,4)+-2*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1002,3)*UFO::{}::P(2003,1)+-2*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1002,3)*UFO::{}::P(2003,4)+-2*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1002,4)*UFO::{}::P(2003,1)+-2*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1002,4)*UFO::{}::P(2003,2)+-2*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,2003)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1002,1)*UFO::{}::P(1003,2)+-2*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,2003)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1002,1)*UFO::{}::P(1003,4)+-2*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,2003)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1002,3)*UFO::{}::P(1003,1)+-2*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,2003)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1002,3)*UFO::{}::P(1003,4)+-2*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,2003)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1002,4)*UFO::{}::P(1003,1)+-2*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,2003)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1002,4)*UFO::{}::P(1003,2)+-2*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2003,1002)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1003,1)*UFO::{}::P(2002,3)+-2*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2003,1002)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1003,1)*UFO::{}::P(2002,4)+-2*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2003,1002)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1003,2)*UFO::{}::P(2002,1)+-2*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2003,1002)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1003,2)*UFO::{}::P(2002,4)+-2*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2003,1002)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1003,4)*UFO::{}::P(2002,1)+-2*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2003,1002)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1003,4)*UFO::{}::P(2002,3)+-2*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2003,1003)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1002,1)*UFO::{}::P(2002,4)+-2*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2003,1003)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1002,4)*UFO::{}::P(2002,1)+-2*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,1003)*UFO::{}::Metric(2004,1004)*UFO::{}::P(1001,3)*UFO::{}::P(1002,4)+-2*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,1003)*UFO::{}::Metric(2004,1004)*UFO::{}::P(1001,4)*UFO::{}::P(1002,3)+-2*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,1004)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1001,2)*UFO::{}::P(1002,3)+-2*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,1004)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1001,2)*UFO::{}::P(1002,4)+-2*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,1004)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1001,3)*UFO::{}::P(1002,1)+-2*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,1004)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1001,3)*UFO::{}::P(1002,4)+-2*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,1004)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1001,4)*UFO::{}::P(1002,1)+-2*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,1004)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1001,4)*UFO::{}::P(1002,3)+-2*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,1002)*UFO::{}::Metric(2004,1004)*UFO::{}::P(1001,2)*UFO::{}::P(1003,4)+-2*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,1002)*UFO::{}::Metric(2004,1004)*UFO::{}::P(1001,4)*UFO::{}::P(1003,2)+-2*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1001,2)*UFO::{}::P(1003,1)+-2*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1001,2)*UFO::{}::P(1003,4)+-2*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1001,3)*UFO::{}::P(1003,2)+-2*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1001,3)*UFO::{}::P(1003,4)+-2*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1001,4)*UFO::{}::P(1003,1)+-2*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1001,4)*UFO::{}::P(1003,2)+-2*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,1002)*UFO::{}::Metric(2003,1003)*UFO::{}::P(1001,2)*UFO::{}::P(1004,3)+-2*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,1002)*UFO::{}::Metric(2003,1003)*UFO::{}::P(1001,3)*UFO::{}::P(1004,2)+-2*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,1002)*UFO::{}::P(1001,2)*UFO::{}::P(1004,1)+-2*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,1002)*UFO::{}::P(1001,2)*UFO::{}::P(1004,3)+-2*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,1002)*UFO::{}::P(1001,3)*UFO::{}::P(1004,1)+-2*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,1002)*UFO::{}::P(1001,3)*UFO::{}::P(1004,2)+-2*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,1002)*UFO::{}::P(1001,4)*UFO::{}::P(1004,2)+-2*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,1002)*UFO::{}::P(1001,4)*UFO::{}::P(1004,3)+-2*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,1003)*UFO::{}::Metric(2004,1004)*UFO::{}::P(1002,3)*UFO::{}::P(2001,4)+-2*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,1003)*UFO::{}::Metric(2004,1004)*UFO::{}::P(1002,4)*UFO::{}::P(2001,3)+-2*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,1004)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1002,1)*UFO::{}::P(2001,3)+-2*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,1004)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1002,1)*UFO::{}::P(2001,4)+-2*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,1004)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1002,3)*UFO::{}::P(2001,2)+-2*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,1004)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1002,3)*UFO::{}::P(2001,4)+-2*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,1004)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1002,4)*UFO::{}::P(2001,2)+-2*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,1004)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1002,4)*UFO::{}::P(2001,3)+-2*UFO::{}::Metric(2002,1002)*UFO::{}::Metric(2003,1001)*UFO::{}::Metric(2004,1004)*UFO::{}::P(1003,2)*UFO::{}::P(2001,4)+-2*UFO::{}::Metric(2002,1002)*UFO::{}::Metric(2003,1001)*UFO::{}::Metric(2004,1004)*UFO::{}::P(1003,4)*UFO::{}::P(2001,2)+-2*UFO::{}::Metric(2002,1002)*UFO::{}::Metric(2003,1003)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1004,2)*UFO::{}::P(2001,3)+-2*UFO::{}::Metric(2002,1002)*UFO::{}::Metric(2003,1003)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1004,3)*UFO::{}::P(2001,2)+-2*UFO::{}::Metric(2002,1002)*UFO::{}::Metric(2003,1004)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1001,3)*UFO::{}::P(2001,4)+-2*UFO::{}::Metric(2002,1002)*UFO::{}::Metric(2003,1004)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1001,4)*UFO::{}::P(2001,3)+-2*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,1002)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1004,1)*UFO::{}::P(2001,2)+-2*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,1002)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1004,1)*UFO::{}::P(2001,3)+-2*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,1002)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1004,2)*UFO::{}::P(2001,3)+-2*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,1002)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1004,2)*UFO::{}::P(2001,4)+-2*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,1002)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1004,3)*UFO::{}::P(2001,2)+-2*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,1002)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1004,3)*UFO::{}::P(2001,4)+-2*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,1002)*UFO::{}::Metric(2004,1004)*UFO::{}::P(1001,2)*UFO::{}::P(2001,3)+-2*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,1002)*UFO::{}::Metric(2004,1004)*UFO::{}::P(1001,3)*UFO::{}::P(2001,2)+-2*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2003,1001)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1003,1)*UFO::{}::P(2001,2)+-2*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2003,1001)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1003,1)*UFO::{}::P(2001,4)+-2*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2003,1001)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1003,2)*UFO::{}::P(2001,3)+-2*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2003,1001)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1003,2)*UFO::{}::P(2001,4)+-2*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2003,1001)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1003,4)*UFO::{}::P(2001,2)+-2*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2003,1001)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1003,4)*UFO::{}::P(2001,3)+-2*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2003,1003)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1001,2)*UFO::{}::P(2001,4)+-2*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2003,1003)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1001,4)*UFO::{}::P(2001,2)+-4*UFO::{}::Metric(1001,1002)*UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,2002)*UFO::{}::P(2003,4)*UFO::{}::P(2004,3)+-4*UFO::{}::Metric(1001,1002)*UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2003,2004)*UFO::{}::P(2001,2)*UFO::{}::P(2002,1)+-4*UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,1004)*UFO::{}::P(1003,4)*UFO::{}::P(2004,3)+-4*UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1003,4)*UFO::{}::P(1004,3)+-4*UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1004,3)*UFO::{}::P(2003,4)+-4*UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2003,1004)*UFO::{}::Metric(2004,1003)*UFO::{}::P(2001,2)*UFO::{}::P(2002,1)+-4*UFO::{}::Metric(1001,1003)*UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,2003)*UFO::{}::P(2002,4)*UFO::{}::P(2004,2)+-4*UFO::{}::Metric(1001,1003)*UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2002,2004)*UFO::{}::P(2001,3)*UFO::{}::P(2003,1)+-4*UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,1004)*UFO::{}::P(1002,4)*UFO::{}::P(2004,2)+-4*UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,2004)*UFO::{}::P(1002,4)*UFO::{}::P(1004,2)+-4*UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1004,2)*UFO::{}::P(2002,4)+-4*UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2004,1002)*UFO::{}::P(2001,3)*UFO::{}::P(2003,1)+-4*UFO::{}::Metric(1001,1004)*UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,2004)*UFO::{}::P(2002,3)*UFO::{}::P(2003,2)+-4*UFO::{}::Metric(1001,1004)*UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2002,2003)*UFO::{}::P(2001,4)*UFO::{}::P(2004,1)+-4*UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,1003)*UFO::{}::P(1002,3)*UFO::{}::P(2003,2)+-4*UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,2003)*UFO::{}::P(1002,3)*UFO::{}::P(1003,2)+-4*UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2003,1002)*UFO::{}::P(1003,2)*UFO::{}::P(2002,3)+-4*UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,1002)*UFO::{}::P(2001,4)*UFO::{}::P(2004,1)+-4*UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,2003)*UFO::{}::P(1001,4)*UFO::{}::P(2004,1)+-4*UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2004,1001)*UFO::{}::P(2002,3)*UFO::{}::P(2003,2)+-4*UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,2003)*UFO::{}::P(1001,4)*UFO::{}::P(1004,1)+-4*UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2002,2003)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1004,1)*UFO::{}::P(2001,4)+-4*UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,2004)*UFO::{}::P(1001,3)*UFO::{}::P(2003,1)+-4*UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2003,1001)*UFO::{}::P(2002,4)*UFO::{}::P(2004,2)+-4*UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,2004)*UFO::{}::P(1001,3)*UFO::{}::P(1003,1)+-4*UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2002,2004)*UFO::{}::Metric(2003,1001)*UFO::{}::P(1003,1)*UFO::{}::P(2001,3)+-4*UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,1001)*UFO::{}::P(2003,4)*UFO::{}::P(2004,3)+-4*UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1001,2)*UFO::{}::P(2002,1)+-4*UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1001,2)*UFO::{}::P(1002,1)+-4*UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1002,1)*UFO::{}::P(2001,2)+-4*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,1004)*UFO::{}::P(1003,4)*UFO::{}::P(2004,3)+-4*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1003,4)*UFO::{}::P(1004,3)+-4*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1004,3)*UFO::{}::P(2003,4)+-4*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2003,1004)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1001,2)*UFO::{}::P(2002,1)+-4*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2003,1001)*UFO::{}::P(1002,4)*UFO::{}::P(2004,2)+-4*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1001,3)*UFO::{}::P(2003,1)+-4*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,2004)*UFO::{}::Metric(2003,1001)*UFO::{}::P(1002,4)*UFO::{}::P(1004,2)+-4*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2003,1001)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1004,2)*UFO::{}::P(2002,4)+-4*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,1002)*UFO::{}::P(1001,4)*UFO::{}::P(2004,1)+-4*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1002,3)*UFO::{}::P(2003,2)+-4*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,2003)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1002,3)*UFO::{}::P(1003,2)+-4*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2003,1002)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1003,2)*UFO::{}::P(2002,3)+-4*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,1004)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1001,2)*UFO::{}::P(1002,1)+-4*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1001,3)*UFO::{}::P(1003,1)+-4*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,1002)*UFO::{}::P(1001,4)*UFO::{}::P(1004,1)+-4*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,1004)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1002,1)*UFO::{}::P(2001,2)+-4*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,1002)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1004,1)*UFO::{}::P(2001,4)+-4*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2003,1001)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1003,1)*UFO::{}::P(2001,3)+6*UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,1003)*UFO::{}::Metric(2004,1004)*UFO::{}::P(-1,3)*UFO::{}::P(-1,4)+6*UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,1002)*UFO::{}::Metric(2004,1004)*UFO::{}::P(-1,2)*UFO::{}::P(-1,4)+6*UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,1002)*UFO::{}::Metric(2003,1003)*UFO::{}::P(-1,2)*UFO::{}::P(-1,3)+6*UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,1001)*UFO::{}::Metric(2002,2003)*UFO::{}::Metric(2004,1004)*UFO::{}::P(-1,1)*UFO::{}::P(-1,4)+6*UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,1001)*UFO::{}::Metric(2002,2004)*UFO::{}::Metric(2003,1003)*UFO::{}::P(-1,1)*UFO::{}::P(-1,3)+6*UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,1001)*UFO::{}::Metric(2002,1002)*UFO::{}::Metric(2003,2004)*UFO::{}::P(-1,1)*UFO::{}::P(-1,2)+6*UFO::{}::Metric(2001,1001)*UFO::{}::Metric(2002,1002)*UFO::{}::Metric(2003,1004)*UFO::{}::Metric(2004,1003)*UFO::{}::P(-1,1)*UFO::{}::P(-1,2)+6*UFO::{}::Metric(2001,1001)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,1002)*UFO::{}::Metric(2004,1004)*UFO::{}::P(-1,1)*UFO::{}::P(-1,4)+6*UFO::{}::Metric(2001,1001)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2003,1003)*UFO::{}::Metric(2004,1002)*UFO::{}::P(-1,1)*UFO::{}::P(-1,3)+6*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,1003)*UFO::{}::Metric(2004,1004)*UFO::{}::P(-1,3)*UFO::{}::P(-1,4)+6*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,1002)*UFO::{}::Metric(2003,1001)*UFO::{}::Metric(2004,1004)*UFO::{}::P(-1,2)*UFO::{}::P(-1,4)+6*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,1002)*UFO::{}::Metric(2003,1003)*UFO::{}::Metric(2004,1001)*UFO::{}::P(-1,2)*UFO::{}::P(-1,3)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,2003)*UFO::{}::P(2002,1)*UFO::{}::P(2004,2)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,2003)*UFO::{}::P(2002,3)*UFO::{}::P(2004,1)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,2003)*UFO::{}::P(2002,4)*UFO::{}::P(2004,2)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,2003)*UFO::{}::P(2002,4)*UFO::{}::P(2004,3)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,2004)*UFO::{}::P(2002,1)*UFO::{}::P(2003,2)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,2004)*UFO::{}::P(2002,3)*UFO::{}::P(2003,2)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,2004)*UFO::{}::P(2002,3)*UFO::{}::P(2003,4)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,2004)*UFO::{}::P(2002,4)*UFO::{}::P(2003,1)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2002,2003)*UFO::{}::P(2001,2)*UFO::{}::P(2004,1)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2002,2003)*UFO::{}::P(2001,3)*UFO::{}::P(2004,2)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2002,2003)*UFO::{}::P(2001,4)*UFO::{}::P(2004,1)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2002,2003)*UFO::{}::P(2001,4)*UFO::{}::P(2004,3)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2002,2004)*UFO::{}::P(2001,2)*UFO::{}::P(2003,1)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2002,2004)*UFO::{}::P(2001,3)*UFO::{}::P(2003,1)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2002,2004)*UFO::{}::P(2001,3)*UFO::{}::P(2003,4)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2002,2004)*UFO::{}::P(2001,4)*UFO::{}::P(2003,2)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,1004)*UFO::{}::P(2003,1)*UFO::{}::P(2004,3)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,1004)*UFO::{}::P(2003,2)*UFO::{}::P(2004,1)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,1004)*UFO::{}::P(2003,4)*UFO::{}::P(2004,2)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,1004)*UFO::{}::P(2003,4)*UFO::{}::P(2004,3)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,2004)*UFO::{}::P(1004,1)*UFO::{}::P(2003,2)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,2004)*UFO::{}::P(1004,2)*UFO::{}::P(2003,4)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,2004)*UFO::{}::P(1004,3)*UFO::{}::P(2003,1)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,2004)*UFO::{}::P(1004,3)*UFO::{}::P(2003,4)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2003,1004)*UFO::{}::P(2002,1)*UFO::{}::P(2004,2)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2003,1004)*UFO::{}::P(2002,3)*UFO::{}::P(2004,1)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2003,1004)*UFO::{}::P(2002,4)*UFO::{}::P(2004,2)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2003,1004)*UFO::{}::P(2002,4)*UFO::{}::P(2004,3)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1004,1)*UFO::{}::P(2002,3)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1004,2)*UFO::{}::P(2002,1)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1004,2)*UFO::{}::P(2002,4)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1004,3)*UFO::{}::P(2002,4)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2004,1004)*UFO::{}::P(2002,1)*UFO::{}::P(2003,2)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2004,1004)*UFO::{}::P(2002,3)*UFO::{}::P(2003,1)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,1003)*UFO::{}::P(2003,1)*UFO::{}::P(2004,2)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,1003)*UFO::{}::P(2003,2)*UFO::{}::P(2004,3)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,1003)*UFO::{}::P(2003,4)*UFO::{}::P(2004,1)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,1003)*UFO::{}::P(2003,4)*UFO::{}::P(2004,3)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,2003)*UFO::{}::P(1003,1)*UFO::{}::P(2004,2)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,2003)*UFO::{}::P(1003,2)*UFO::{}::P(2004,3)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,2003)*UFO::{}::P(1003,4)*UFO::{}::P(2004,1)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,2003)*UFO::{}::P(1003,4)*UFO::{}::P(2004,3)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2003,1003)*UFO::{}::P(2002,1)*UFO::{}::P(2004,2)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2003,1003)*UFO::{}::P(2002,4)*UFO::{}::P(2004,1)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1003,1)*UFO::{}::P(2002,4)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1003,2)*UFO::{}::P(2002,1)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1003,2)*UFO::{}::P(2002,3)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1003,4)*UFO::{}::P(2002,3)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2004,1003)*UFO::{}::P(2002,1)*UFO::{}::P(2003,2)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2004,1003)*UFO::{}::P(2002,3)*UFO::{}::P(2003,2)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2004,1003)*UFO::{}::P(2002,3)*UFO::{}::P(2003,4)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2004,1003)*UFO::{}::P(2002,4)*UFO::{}::P(2003,1)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,1004)*UFO::{}::P(1003,1)*UFO::{}::P(2004,3)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,1004)*UFO::{}::P(1003,2)*UFO::{}::P(2004,1)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,1004)*UFO::{}::P(1003,4)*UFO::{}::P(2004,2)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,1004)*UFO::{}::P(1003,4)*UFO::{}::P(2004,3)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,2004)*UFO::{}::P(1003,1)*UFO::{}::P(1004,3)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,2004)*UFO::{}::P(1003,2)*UFO::{}::P(1004,1)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,2004)*UFO::{}::P(1003,4)*UFO::{}::P(1004,2)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,2004)*UFO::{}::P(1003,4)*UFO::{}::P(1004,3)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1004,1)*UFO::{}::P(2002,3)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1004,2)*UFO::{}::P(2002,1)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1004,2)*UFO::{}::P(2002,4)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1004,3)*UFO::{}::P(2002,4)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2004,1004)*UFO::{}::P(1003,1)*UFO::{}::P(2002,3)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2004,1004)*UFO::{}::P(1003,2)*UFO::{}::P(2002,1)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,1003)*UFO::{}::P(1004,1)*UFO::{}::P(2003,4)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,1003)*UFO::{}::P(1004,2)*UFO::{}::P(2003,1)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,1003)*UFO::{}::P(1004,3)*UFO::{}::P(2003,2)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,1003)*UFO::{}::P(1004,3)*UFO::{}::P(2003,4)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,2003)*UFO::{}::P(1003,1)*UFO::{}::P(1004,2)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,2003)*UFO::{}::P(1003,2)*UFO::{}::P(1004,3)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,2003)*UFO::{}::P(1003,4)*UFO::{}::P(1004,1)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,2003)*UFO::{}::P(1003,4)*UFO::{}::P(1004,3)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2003,1003)*UFO::{}::P(1004,1)*UFO::{}::P(2002,4)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2003,1003)*UFO::{}::P(1004,2)*UFO::{}::P(2002,1)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2003,1004)*UFO::{}::P(1003,1)*UFO::{}::P(2002,4)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2003,1004)*UFO::{}::P(1003,2)*UFO::{}::P(2002,1)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2003,1004)*UFO::{}::P(1003,2)*UFO::{}::P(2002,3)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2003,1004)*UFO::{}::P(1003,4)*UFO::{}::P(2002,3)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,1004)*UFO::{}::P(2001,2)*UFO::{}::P(2004,1)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,1004)*UFO::{}::P(2001,3)*UFO::{}::P(2004,2)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,1004)*UFO::{}::P(2001,4)*UFO::{}::P(2004,1)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,1004)*UFO::{}::P(2001,4)*UFO::{}::P(2004,3)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1004,1)*UFO::{}::P(2001,2)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1004,1)*UFO::{}::P(2001,4)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1004,2)*UFO::{}::P(2001,3)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1004,3)*UFO::{}::P(2001,4)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2004,1004)*UFO::{}::P(2001,2)*UFO::{}::P(2003,1)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2004,1004)*UFO::{}::P(2001,3)*UFO::{}::P(2003,2)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2003,1003)*UFO::{}::P(2001,2)*UFO::{}::P(2004,1)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2003,1003)*UFO::{}::P(2001,4)*UFO::{}::P(2004,2)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1003,1)*UFO::{}::P(2001,2)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1003,1)*UFO::{}::P(2001,3)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1003,2)*UFO::{}::P(2001,4)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1003,4)*UFO::{}::P(2001,3)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2004,1003)*UFO::{}::P(2001,2)*UFO::{}::P(2003,1)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2004,1003)*UFO::{}::P(2001,3)*UFO::{}::P(2003,1)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2004,1003)*UFO::{}::P(2001,3)*UFO::{}::P(2003,4)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2004,1003)*UFO::{}::P(2001,4)*UFO::{}::P(2003,2)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2002,2003)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1004,1)*UFO::{}::P(2001,2)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2002,2003)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1004,1)*UFO::{}::P(2001,4)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2002,2003)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1004,2)*UFO::{}::P(2001,3)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2002,2003)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1004,3)*UFO::{}::P(2001,4)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2002,2003)*UFO::{}::Metric(2004,1004)*UFO::{}::P(1003,1)*UFO::{}::P(2001,2)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2002,2003)*UFO::{}::Metric(2004,1004)*UFO::{}::P(1003,2)*UFO::{}::P(2001,3)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2002,2004)*UFO::{}::Metric(2003,1003)*UFO::{}::P(1004,1)*UFO::{}::P(2001,2)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2002,2004)*UFO::{}::Metric(2003,1003)*UFO::{}::P(1004,2)*UFO::{}::P(2001,4)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2002,2004)*UFO::{}::Metric(2003,1004)*UFO::{}::P(1003,1)*UFO::{}::P(2001,2)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2002,2004)*UFO::{}::Metric(2003,1004)*UFO::{}::P(1003,1)*UFO::{}::P(2001,3)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2002,2004)*UFO::{}::Metric(2003,1004)*UFO::{}::P(1003,2)*UFO::{}::P(2001,4)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2002,2004)*UFO::{}::Metric(2003,1004)*UFO::{}::P(1003,4)*UFO::{}::P(2001,3)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,2002)*UFO::{}::P(2003,1)*UFO::{}::P(2004,3)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,2002)*UFO::{}::P(2003,2)*UFO::{}::P(2004,1)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,2002)*UFO::{}::P(2003,4)*UFO::{}::P(2004,2)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,2002)*UFO::{}::P(2003,4)*UFO::{}::P(2004,3)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,2004)*UFO::{}::P(2002,1)*UFO::{}::P(2003,4)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,2004)*UFO::{}::P(2002,3)*UFO::{}::P(2003,1)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,2004)*UFO::{}::P(2002,3)*UFO::{}::P(2003,2)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,2004)*UFO::{}::P(2002,4)*UFO::{}::P(2003,2)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2002,2003)*UFO::{}::P(2001,2)*UFO::{}::P(2004,3)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2002,2003)*UFO::{}::P(2001,3)*UFO::{}::P(2004,1)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2002,2003)*UFO::{}::P(2001,4)*UFO::{}::P(2004,1)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2002,2003)*UFO::{}::P(2001,4)*UFO::{}::P(2004,2)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2003,2004)*UFO::{}::P(2001,2)*UFO::{}::P(2002,1)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2003,2004)*UFO::{}::P(2001,2)*UFO::{}::P(2002,4)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2003,2004)*UFO::{}::P(2001,3)*UFO::{}::P(2002,1)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2003,2004)*UFO::{}::P(2001,4)*UFO::{}::P(2002,3)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,1004)*UFO::{}::P(2003,1)*UFO::{}::P(2004,3)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,1004)*UFO::{}::P(2003,2)*UFO::{}::P(2004,1)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,1004)*UFO::{}::P(2003,4)*UFO::{}::P(2004,2)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,1004)*UFO::{}::P(2003,4)*UFO::{}::P(2004,3)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,2004)*UFO::{}::P(1004,1)*UFO::{}::P(2003,2)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,2004)*UFO::{}::P(1004,2)*UFO::{}::P(2003,4)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,2004)*UFO::{}::P(1004,3)*UFO::{}::P(2003,1)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,2004)*UFO::{}::P(1004,3)*UFO::{}::P(2003,4)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2003,1004)*UFO::{}::P(2002,1)*UFO::{}::P(2004,2)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2003,1004)*UFO::{}::P(2002,3)*UFO::{}::P(2004,1)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2003,1004)*UFO::{}::P(2002,4)*UFO::{}::P(2004,2)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2003,1004)*UFO::{}::P(2002,4)*UFO::{}::P(2004,3)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1004,1)*UFO::{}::P(2002,3)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1004,2)*UFO::{}::P(2002,1)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1004,2)*UFO::{}::P(2002,4)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1004,3)*UFO::{}::P(2002,4)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2004,1004)*UFO::{}::P(2002,1)*UFO::{}::P(2003,2)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2004,1004)*UFO::{}::P(2002,3)*UFO::{}::P(2003,1)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,1002)*UFO::{}::P(2003,1)*UFO::{}::P(2004,3)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,1002)*UFO::{}::P(2003,4)*UFO::{}::P(2004,1)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,2003)*UFO::{}::P(1002,1)*UFO::{}::P(2004,3)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,2003)*UFO::{}::P(1002,3)*UFO::{}::P(2004,2)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,2003)*UFO::{}::P(1002,4)*UFO::{}::P(2004,1)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,2003)*UFO::{}::P(1002,4)*UFO::{}::P(2004,2)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,2004)*UFO::{}::P(1002,1)*UFO::{}::P(2003,4)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,2004)*UFO::{}::P(1002,3)*UFO::{}::P(2003,1)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,2004)*UFO::{}::P(1002,3)*UFO::{}::P(2003,2)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,2004)*UFO::{}::P(1002,4)*UFO::{}::P(2003,2)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2003,1002)*UFO::{}::P(2002,1)*UFO::{}::P(2004,3)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2003,1002)*UFO::{}::P(2002,3)*UFO::{}::P(2004,2)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2003,1002)*UFO::{}::P(2002,4)*UFO::{}::P(2004,1)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2003,1002)*UFO::{}::P(2002,4)*UFO::{}::P(2004,2)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2004,1002)*UFO::{}::P(2002,1)*UFO::{}::P(2003,4)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2004,1002)*UFO::{}::P(2002,3)*UFO::{}::P(2003,1)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2004,1002)*UFO::{}::P(2002,3)*UFO::{}::P(2003,2)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2004,1002)*UFO::{}::P(2002,4)*UFO::{}::P(2003,2)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,1004)*UFO::{}::P(1002,1)*UFO::{}::P(2004,2)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,1004)*UFO::{}::P(1002,3)*UFO::{}::P(2004,1)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,1004)*UFO::{}::P(1002,4)*UFO::{}::P(2004,2)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,1004)*UFO::{}::P(1002,4)*UFO::{}::P(2004,3)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1002,1)*UFO::{}::P(1004,2)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1002,3)*UFO::{}::P(1004,1)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1002,4)*UFO::{}::P(1004,2)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1002,4)*UFO::{}::P(1004,3)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1004,1)*UFO::{}::P(2003,2)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1004,2)*UFO::{}::P(2003,4)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1004,3)*UFO::{}::P(2003,1)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1004,3)*UFO::{}::P(2003,4)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2004,1004)*UFO::{}::P(1002,1)*UFO::{}::P(2003,2)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2004,1004)*UFO::{}::P(1002,3)*UFO::{}::P(2003,1)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,1002)*UFO::{}::P(1004,1)*UFO::{}::P(2003,4)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,1002)*UFO::{}::P(1004,3)*UFO::{}::P(2003,1)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,1004)*UFO::{}::P(1002,1)*UFO::{}::P(2003,4)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,1004)*UFO::{}::P(1002,3)*UFO::{}::P(2003,1)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,1004)*UFO::{}::P(1002,3)*UFO::{}::P(2003,2)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,1004)*UFO::{}::P(1002,4)*UFO::{}::P(2003,2)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,2003)*UFO::{}::P(1002,1)*UFO::{}::P(1004,3)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,2003)*UFO::{}::P(1002,3)*UFO::{}::P(1004,2)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,2003)*UFO::{}::P(1002,4)*UFO::{}::P(1004,1)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,2003)*UFO::{}::P(1002,4)*UFO::{}::P(1004,2)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2003,1002)*UFO::{}::P(1004,1)*UFO::{}::P(2002,4)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2003,1002)*UFO::{}::P(1004,2)*UFO::{}::P(2002,3)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2003,1002)*UFO::{}::P(1004,2)*UFO::{}::P(2002,4)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2003,1002)*UFO::{}::P(1004,3)*UFO::{}::P(2002,1)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2002,1002)*UFO::{}::Metric(2003,1004)*UFO::{}::P(2001,3)*UFO::{}::P(2004,1)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2002,1002)*UFO::{}::Metric(2003,1004)*UFO::{}::P(2001,4)*UFO::{}::P(2004,3)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2002,1002)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1004,1)*UFO::{}::P(2001,3)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2002,1002)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1004,3)*UFO::{}::P(2001,4)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2003,1002)*UFO::{}::P(2001,2)*UFO::{}::P(2004,3)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2003,1002)*UFO::{}::P(2001,3)*UFO::{}::P(2004,1)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2003,1002)*UFO::{}::P(2001,4)*UFO::{}::P(2004,1)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2003,1002)*UFO::{}::P(2001,4)*UFO::{}::P(2004,2)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1002,1)*UFO::{}::P(2001,2)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1002,1)*UFO::{}::P(2001,3)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1002,3)*UFO::{}::P(2001,4)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1002,4)*UFO::{}::P(2001,2)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2002,2003)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1004,1)*UFO::{}::P(2001,3)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2002,2003)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1004,1)*UFO::{}::P(2001,4)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2002,2003)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1004,2)*UFO::{}::P(2001,4)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2002,2003)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1004,3)*UFO::{}::P(2001,2)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2002,2003)*UFO::{}::Metric(2004,1004)*UFO::{}::P(1002,1)*UFO::{}::P(2001,3)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2002,2003)*UFO::{}::Metric(2004,1004)*UFO::{}::P(1002,3)*UFO::{}::P(2001,2)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2002,2004)*UFO::{}::Metric(2003,1002)*UFO::{}::P(1004,1)*UFO::{}::P(2001,3)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2002,2004)*UFO::{}::Metric(2003,1002)*UFO::{}::P(1004,1)*UFO::{}::P(2001,4)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2002,2004)*UFO::{}::Metric(2003,1002)*UFO::{}::P(1004,2)*UFO::{}::P(2001,4)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2002,2004)*UFO::{}::Metric(2003,1002)*UFO::{}::P(1004,3)*UFO::{}::P(2001,2)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2002,2004)*UFO::{}::Metric(2003,1004)*UFO::{}::P(1002,1)*UFO::{}::P(2001,2)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2002,2004)*UFO::{}::Metric(2003,1004)*UFO::{}::P(1002,1)*UFO::{}::P(2001,3)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2002,2004)*UFO::{}::Metric(2003,1004)*UFO::{}::P(1002,3)*UFO::{}::P(2001,4)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2002,2004)*UFO::{}::Metric(2003,1004)*UFO::{}::P(1002,4)*UFO::{}::P(2001,2)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2003,1002)*UFO::{}::Metric(2004,1004)*UFO::{}::P(2001,2)*UFO::{}::P(2002,3)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2003,1002)*UFO::{}::Metric(2004,1004)*UFO::{}::P(2001,3)*UFO::{}::P(2002,1)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2003,1004)*UFO::{}::Metric(2004,1002)*UFO::{}::P(2001,2)*UFO::{}::P(2002,1)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2003,1004)*UFO::{}::Metric(2004,1002)*UFO::{}::P(2001,2)*UFO::{}::P(2002,4)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2003,1004)*UFO::{}::Metric(2004,1002)*UFO::{}::P(2001,3)*UFO::{}::P(2002,1)+UFO::{}::Metric(1001,1003)*UFO::{}::Metric(2003,1004)*UFO::{}::Metric(2004,1002)*UFO::{}::P(2001,4)*UFO::{}::P(2002,3)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,2002)*UFO::{}::P(2003,1)*UFO::{}::P(2004,2)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,2002)*UFO::{}::P(2003,2)*UFO::{}::P(2004,3)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,2002)*UFO::{}::P(2003,4)*UFO::{}::P(2004,1)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,2002)*UFO::{}::P(2003,4)*UFO::{}::P(2004,3)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,2003)*UFO::{}::P(2002,1)*UFO::{}::P(2004,3)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,2003)*UFO::{}::P(2002,3)*UFO::{}::P(2004,2)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,2003)*UFO::{}::P(2002,4)*UFO::{}::P(2004,1)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,2003)*UFO::{}::P(2002,4)*UFO::{}::P(2004,2)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2002,2004)*UFO::{}::P(2001,2)*UFO::{}::P(2003,4)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2002,2004)*UFO::{}::P(2001,3)*UFO::{}::P(2003,1)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2002,2004)*UFO::{}::P(2001,3)*UFO::{}::P(2003,2)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2002,2004)*UFO::{}::P(2001,4)*UFO::{}::P(2003,1)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2003,2004)*UFO::{}::P(2001,2)*UFO::{}::P(2002,1)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2003,2004)*UFO::{}::P(2001,2)*UFO::{}::P(2002,3)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2003,2004)*UFO::{}::P(2001,3)*UFO::{}::P(2002,4)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2003,2004)*UFO::{}::P(2001,4)*UFO::{}::P(2002,1)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,1003)*UFO::{}::P(2003,1)*UFO::{}::P(2004,2)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,1003)*UFO::{}::P(2003,2)*UFO::{}::P(2004,3)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,1003)*UFO::{}::P(2003,4)*UFO::{}::P(2004,1)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,1003)*UFO::{}::P(2003,4)*UFO::{}::P(2004,3)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,2003)*UFO::{}::P(1003,1)*UFO::{}::P(2004,2)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,2003)*UFO::{}::P(1003,2)*UFO::{}::P(2004,3)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,2003)*UFO::{}::P(1003,4)*UFO::{}::P(2004,1)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,2003)*UFO::{}::P(1003,4)*UFO::{}::P(2004,3)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2003,1003)*UFO::{}::P(2002,1)*UFO::{}::P(2004,2)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2003,1003)*UFO::{}::P(2002,4)*UFO::{}::P(2004,1)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1003,1)*UFO::{}::P(2002,4)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1003,2)*UFO::{}::P(2002,1)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1003,2)*UFO::{}::P(2002,3)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1003,4)*UFO::{}::P(2002,3)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2004,1003)*UFO::{}::P(2002,1)*UFO::{}::P(2003,2)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2004,1003)*UFO::{}::P(2002,3)*UFO::{}::P(2003,2)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2004,1003)*UFO::{}::P(2002,3)*UFO::{}::P(2003,4)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2004,1003)*UFO::{}::P(2002,4)*UFO::{}::P(2003,1)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,1002)*UFO::{}::P(2003,1)*UFO::{}::P(2004,3)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,1002)*UFO::{}::P(2003,4)*UFO::{}::P(2004,1)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,2003)*UFO::{}::P(1002,1)*UFO::{}::P(2004,3)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,2003)*UFO::{}::P(1002,3)*UFO::{}::P(2004,2)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,2003)*UFO::{}::P(1002,4)*UFO::{}::P(2004,1)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,2003)*UFO::{}::P(1002,4)*UFO::{}::P(2004,2)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,2004)*UFO::{}::P(1002,1)*UFO::{}::P(2003,4)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,2004)*UFO::{}::P(1002,3)*UFO::{}::P(2003,1)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,2004)*UFO::{}::P(1002,3)*UFO::{}::P(2003,2)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,2004)*UFO::{}::P(1002,4)*UFO::{}::P(2003,2)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2003,1002)*UFO::{}::P(2002,1)*UFO::{}::P(2004,3)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2003,1002)*UFO::{}::P(2002,3)*UFO::{}::P(2004,2)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2003,1002)*UFO::{}::P(2002,4)*UFO::{}::P(2004,1)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2003,1002)*UFO::{}::P(2002,4)*UFO::{}::P(2004,2)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2004,1002)*UFO::{}::P(2002,1)*UFO::{}::P(2003,4)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2004,1002)*UFO::{}::P(2002,3)*UFO::{}::P(2003,1)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2004,1002)*UFO::{}::P(2002,3)*UFO::{}::P(2003,2)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2004,1002)*UFO::{}::P(2002,4)*UFO::{}::P(2003,2)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,1002)*UFO::{}::P(1003,1)*UFO::{}::P(2004,2)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,1002)*UFO::{}::P(1003,2)*UFO::{}::P(2004,3)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,1002)*UFO::{}::P(1003,4)*UFO::{}::P(2004,1)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,1002)*UFO::{}::P(1003,4)*UFO::{}::P(2004,3)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,1003)*UFO::{}::P(1002,1)*UFO::{}::P(2004,2)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,1003)*UFO::{}::P(1002,4)*UFO::{}::P(2004,1)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1002,1)*UFO::{}::P(1003,2)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1002,3)*UFO::{}::P(1003,2)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1002,3)*UFO::{}::P(1003,4)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1002,4)*UFO::{}::P(1003,1)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1002,1)*UFO::{}::P(2003,2)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1002,3)*UFO::{}::P(2003,2)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1002,3)*UFO::{}::P(2003,4)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1002,4)*UFO::{}::P(2003,1)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,1002)*UFO::{}::P(1003,1)*UFO::{}::P(2004,3)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,1002)*UFO::{}::P(1003,4)*UFO::{}::P(2004,1)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,1003)*UFO::{}::P(1002,1)*UFO::{}::P(2004,3)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,1003)*UFO::{}::P(1002,3)*UFO::{}::P(2004,2)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,1003)*UFO::{}::P(1002,4)*UFO::{}::P(2004,1)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,1003)*UFO::{}::P(1002,4)*UFO::{}::P(2004,2)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,2004)*UFO::{}::P(1002,1)*UFO::{}::P(1003,4)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,2004)*UFO::{}::P(1002,3)*UFO::{}::P(1003,1)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,2004)*UFO::{}::P(1002,3)*UFO::{}::P(1003,2)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,2004)*UFO::{}::P(1002,4)*UFO::{}::P(1003,2)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1003,1)*UFO::{}::P(2002,3)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1003,2)*UFO::{}::P(2002,3)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1003,2)*UFO::{}::P(2002,4)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1003,4)*UFO::{}::P(2002,1)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2002,1002)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1003,1)*UFO::{}::P(2001,4)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2002,1002)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1003,4)*UFO::{}::P(2001,3)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2002,1002)*UFO::{}::Metric(2004,1003)*UFO::{}::P(2001,3)*UFO::{}::P(2003,4)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2002,1002)*UFO::{}::Metric(2004,1003)*UFO::{}::P(2001,4)*UFO::{}::P(2003,1)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1002,1)*UFO::{}::P(2001,2)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1002,1)*UFO::{}::P(2001,4)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1002,3)*UFO::{}::P(2001,2)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1002,4)*UFO::{}::P(2001,3)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2004,1002)*UFO::{}::P(2001,2)*UFO::{}::P(2003,4)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2004,1002)*UFO::{}::P(2001,3)*UFO::{}::P(2003,1)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2004,1002)*UFO::{}::P(2001,3)*UFO::{}::P(2003,2)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2004,1002)*UFO::{}::P(2001,4)*UFO::{}::P(2003,1)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2002,2003)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1003,1)*UFO::{}::P(2001,3)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2002,2003)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1003,1)*UFO::{}::P(2001,4)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2002,2003)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1003,2)*UFO::{}::P(2001,3)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2002,2003)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1003,4)*UFO::{}::P(2001,2)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2002,2003)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1002,1)*UFO::{}::P(2001,2)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2002,2003)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1002,1)*UFO::{}::P(2001,4)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2002,2003)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1002,3)*UFO::{}::P(2001,2)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2002,2003)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1002,4)*UFO::{}::P(2001,3)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2002,2004)*UFO::{}::Metric(2003,1002)*UFO::{}::P(1003,1)*UFO::{}::P(2001,3)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2002,2004)*UFO::{}::Metric(2003,1002)*UFO::{}::P(1003,1)*UFO::{}::P(2001,4)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2002,2004)*UFO::{}::Metric(2003,1002)*UFO::{}::P(1003,2)*UFO::{}::P(2001,3)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2002,2004)*UFO::{}::Metric(2003,1002)*UFO::{}::P(1003,4)*UFO::{}::P(2001,2)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2002,2004)*UFO::{}::Metric(2003,1003)*UFO::{}::P(1002,1)*UFO::{}::P(2001,4)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2002,2004)*UFO::{}::Metric(2003,1003)*UFO::{}::P(1002,4)*UFO::{}::P(2001,2)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2003,1002)*UFO::{}::Metric(2004,1003)*UFO::{}::P(2001,2)*UFO::{}::P(2002,1)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2003,1002)*UFO::{}::Metric(2004,1003)*UFO::{}::P(2001,2)*UFO::{}::P(2002,3)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2003,1002)*UFO::{}::Metric(2004,1003)*UFO::{}::P(2001,3)*UFO::{}::P(2002,4)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2003,1002)*UFO::{}::Metric(2004,1003)*UFO::{}::P(2001,4)*UFO::{}::P(2002,1)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2003,1003)*UFO::{}::Metric(2004,1002)*UFO::{}::P(2001,2)*UFO::{}::P(2002,4)+UFO::{}::Metric(1001,1004)*UFO::{}::Metric(2003,1003)*UFO::{}::Metric(2004,1002)*UFO::{}::P(2001,4)*UFO::{}::P(2002,1)+UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,1001)*UFO::{}::Metric(2002,1004)*UFO::{}::P(2003,2)*UFO::{}::P(2004,3)+UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,1001)*UFO::{}::Metric(2002,1004)*UFO::{}::P(2003,4)*UFO::{}::P(2004,2)+UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,1001)*UFO::{}::Metric(2002,2004)*UFO::{}::P(1004,2)*UFO::{}::P(2003,4)+UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,1001)*UFO::{}::Metric(2002,2004)*UFO::{}::P(1004,3)*UFO::{}::P(2003,2)+UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,1001)*UFO::{}::Metric(2003,1004)*UFO::{}::P(2002,3)*UFO::{}::P(2004,2)+UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,1001)*UFO::{}::Metric(2003,1004)*UFO::{}::P(2002,4)*UFO::{}::P(2004,3)+UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,1001)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1004,2)*UFO::{}::P(2002,3)+UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,1001)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1004,3)*UFO::{}::P(2002,4)+UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,1001)*UFO::{}::P(2003,1)*UFO::{}::P(2004,2)+UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,1001)*UFO::{}::P(2003,2)*UFO::{}::P(2004,3)+UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,1001)*UFO::{}::P(2003,4)*UFO::{}::P(2004,1)+UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,1001)*UFO::{}::P(2003,4)*UFO::{}::P(2004,3)+UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,2004)*UFO::{}::P(1001,2)*UFO::{}::P(2003,4)+UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,2004)*UFO::{}::P(1001,3)*UFO::{}::P(2003,1)+UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,2004)*UFO::{}::P(1001,3)*UFO::{}::P(2003,2)+UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,2004)*UFO::{}::P(1001,4)*UFO::{}::P(2003,1)+UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2003,1001)*UFO::{}::P(2002,1)*UFO::{}::P(2004,3)+UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2003,1001)*UFO::{}::P(2002,3)*UFO::{}::P(2004,2)+UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2003,1001)*UFO::{}::P(2002,4)*UFO::{}::P(2004,1)+UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2003,1001)*UFO::{}::P(2002,4)*UFO::{}::P(2004,2)+UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1001,2)*UFO::{}::P(2002,1)+UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1001,2)*UFO::{}::P(2002,3)+UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1001,3)*UFO::{}::P(2002,4)+UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1001,4)*UFO::{}::P(2002,1)+UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,1004)*UFO::{}::P(1001,2)*UFO::{}::P(2004,1)+UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,1004)*UFO::{}::P(1001,3)*UFO::{}::P(2004,2)+UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,1004)*UFO::{}::P(1001,4)*UFO::{}::P(2004,1)+UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,1004)*UFO::{}::P(1001,4)*UFO::{}::P(2004,3)+UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1001,2)*UFO::{}::P(1004,1)+UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1001,3)*UFO::{}::P(1004,2)+UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1001,4)*UFO::{}::P(1004,1)+UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1001,4)*UFO::{}::P(1004,3)+UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1004,1)*UFO::{}::P(2003,4)+UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1004,2)*UFO::{}::P(2003,1)+UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1004,3)*UFO::{}::P(2003,2)+UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1004,3)*UFO::{}::P(2003,4)+UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2004,1004)*UFO::{}::P(1001,2)*UFO::{}::P(2003,1)+UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2004,1004)*UFO::{}::P(1001,3)*UFO::{}::P(2003,2)+UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,1004)*UFO::{}::P(1001,2)*UFO::{}::P(2004,3)+UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,1004)*UFO::{}::P(1001,3)*UFO::{}::P(2004,1)+UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,1004)*UFO::{}::P(1001,4)*UFO::{}::P(2004,1)+UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,1004)*UFO::{}::P(1001,4)*UFO::{}::P(2004,2)+UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,2004)*UFO::{}::P(1001,2)*UFO::{}::P(1004,3)+UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,2004)*UFO::{}::P(1001,3)*UFO::{}::P(1004,1)+UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,2004)*UFO::{}::P(1001,4)*UFO::{}::P(1004,1)+UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,2004)*UFO::{}::P(1001,4)*UFO::{}::P(1004,2)+UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1004,1)*UFO::{}::P(2002,4)+UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1004,2)*UFO::{}::P(2002,3)+UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1004,2)*UFO::{}::P(2002,4)+UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1004,3)*UFO::{}::P(2002,1)+UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2004,1004)*UFO::{}::P(1001,2)*UFO::{}::P(2002,3)+UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2004,1004)*UFO::{}::P(1001,3)*UFO::{}::P(2002,1)+UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,1001)*UFO::{}::P(1004,1)*UFO::{}::P(2003,4)+UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,1001)*UFO::{}::P(1004,2)*UFO::{}::P(2003,1)+UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,1001)*UFO::{}::P(1004,3)*UFO::{}::P(2003,2)+UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,1001)*UFO::{}::P(1004,3)*UFO::{}::P(2003,4)+UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,1004)*UFO::{}::P(1001,2)*UFO::{}::P(2003,4)+UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,1004)*UFO::{}::P(1001,3)*UFO::{}::P(2003,1)+UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,1004)*UFO::{}::P(1001,3)*UFO::{}::P(2003,2)+UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,1004)*UFO::{}::P(1001,4)*UFO::{}::P(2003,1)+UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2003,1001)*UFO::{}::P(1004,1)*UFO::{}::P(2002,4)+UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2003,1001)*UFO::{}::P(1004,2)*UFO::{}::P(2002,3)+UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2003,1001)*UFO::{}::P(1004,2)*UFO::{}::P(2002,4)+UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2003,1001)*UFO::{}::P(1004,3)*UFO::{}::P(2002,1)+UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2003,1004)*UFO::{}::P(1001,2)*UFO::{}::P(2002,1)+UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2003,1004)*UFO::{}::P(1001,2)*UFO::{}::P(2002,3)+UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2003,1004)*UFO::{}::P(1001,3)*UFO::{}::P(2002,4)+UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2003,1004)*UFO::{}::P(1001,4)*UFO::{}::P(2002,1)+UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,1004)*UFO::{}::P(2001,2)*UFO::{}::P(2004,1)+UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,1004)*UFO::{}::P(2001,3)*UFO::{}::P(2004,2)+UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,1004)*UFO::{}::P(2001,4)*UFO::{}::P(2004,1)+UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,1004)*UFO::{}::P(2001,4)*UFO::{}::P(2004,3)+UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1004,1)*UFO::{}::P(2001,2)+UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1004,1)*UFO::{}::P(2001,4)+UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1004,2)*UFO::{}::P(2001,3)+UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1004,3)*UFO::{}::P(2001,4)+UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2004,1004)*UFO::{}::P(2001,2)*UFO::{}::P(2003,1)+UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2004,1004)*UFO::{}::P(2001,3)*UFO::{}::P(2003,2)+UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2003,1001)*UFO::{}::P(2001,2)*UFO::{}::P(2004,3)+UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2003,1001)*UFO::{}::P(2001,3)*UFO::{}::P(2004,1)+UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2003,1001)*UFO::{}::P(2001,4)*UFO::{}::P(2004,1)+UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2003,1001)*UFO::{}::P(2001,4)*UFO::{}::P(2004,2)+UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2004,1001)*UFO::{}::P(2001,2)*UFO::{}::P(2003,4)+UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2004,1001)*UFO::{}::P(2001,3)*UFO::{}::P(2003,1)+UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2004,1001)*UFO::{}::P(2001,3)*UFO::{}::P(2003,2)+UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2004,1001)*UFO::{}::P(2001,4)*UFO::{}::P(2003,1)+UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2002,2004)*UFO::{}::Metric(2003,1001)*UFO::{}::P(1004,1)*UFO::{}::P(2001,3)+UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2002,2004)*UFO::{}::Metric(2003,1001)*UFO::{}::P(1004,1)*UFO::{}::P(2001,4)+UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2002,2004)*UFO::{}::Metric(2003,1001)*UFO::{}::P(1004,2)*UFO::{}::P(2001,4)+UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2002,2004)*UFO::{}::Metric(2003,1001)*UFO::{}::P(1004,3)*UFO::{}::P(2001,2)+UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2003,1001)*UFO::{}::Metric(2004,1004)*UFO::{}::P(2001,2)*UFO::{}::P(2002,3)+UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2003,1001)*UFO::{}::Metric(2004,1004)*UFO::{}::P(2001,3)*UFO::{}::P(2002,1)+UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2003,1004)*UFO::{}::Metric(2004,1001)*UFO::{}::P(2001,2)*UFO::{}::P(2002,1)+UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2003,1004)*UFO::{}::Metric(2004,1001)*UFO::{}::P(2001,2)*UFO::{}::P(2002,3)+UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2003,1004)*UFO::{}::Metric(2004,1001)*UFO::{}::P(2001,3)*UFO::{}::P(2002,4)+UFO::{}::Metric(1002,1003)*UFO::{}::Metric(2003,1004)*UFO::{}::Metric(2004,1001)*UFO::{}::P(2001,4)*UFO::{}::P(2002,1)+UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,1001)*UFO::{}::Metric(2002,1003)*UFO::{}::P(2003,2)*UFO::{}::P(2004,3)+UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,1001)*UFO::{}::Metric(2002,1003)*UFO::{}::P(2003,4)*UFO::{}::P(2004,2)+UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,1001)*UFO::{}::Metric(2002,2003)*UFO::{}::P(1003,2)*UFO::{}::P(2004,3)+UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,1001)*UFO::{}::Metric(2002,2003)*UFO::{}::P(1003,4)*UFO::{}::P(2004,2)+UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,1001)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1003,2)*UFO::{}::P(2002,4)+UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,1001)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1003,4)*UFO::{}::P(2002,3)+UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,1001)*UFO::{}::Metric(2004,1003)*UFO::{}::P(2002,3)*UFO::{}::P(2003,4)+UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,1001)*UFO::{}::Metric(2004,1003)*UFO::{}::P(2002,4)*UFO::{}::P(2003,2)+UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,1001)*UFO::{}::P(2003,1)*UFO::{}::P(2004,3)+UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,1001)*UFO::{}::P(2003,2)*UFO::{}::P(2004,1)+UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,1001)*UFO::{}::P(2003,4)*UFO::{}::P(2004,2)+UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,1001)*UFO::{}::P(2003,4)*UFO::{}::P(2004,3)+UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,2003)*UFO::{}::P(1001,2)*UFO::{}::P(2004,3)+UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,2003)*UFO::{}::P(1001,3)*UFO::{}::P(2004,1)+UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,2003)*UFO::{}::P(1001,4)*UFO::{}::P(2004,1)+UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,2003)*UFO::{}::P(1001,4)*UFO::{}::P(2004,2)+UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1001,2)*UFO::{}::P(2002,1)+UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1001,2)*UFO::{}::P(2002,4)+UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1001,3)*UFO::{}::P(2002,1)+UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1001,4)*UFO::{}::P(2002,3)+UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2004,1001)*UFO::{}::P(2002,1)*UFO::{}::P(2003,4)+UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2004,1001)*UFO::{}::P(2002,3)*UFO::{}::P(2003,1)+UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2004,1001)*UFO::{}::P(2002,3)*UFO::{}::P(2003,2)+UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2004,1001)*UFO::{}::P(2002,4)*UFO::{}::P(2003,2)+UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,1001)*UFO::{}::P(1003,1)*UFO::{}::P(2004,3)+UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,1001)*UFO::{}::P(1003,2)*UFO::{}::P(2004,1)+UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,1001)*UFO::{}::P(1003,4)*UFO::{}::P(2004,2)+UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,1001)*UFO::{}::P(1003,4)*UFO::{}::P(2004,3)+UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,1003)*UFO::{}::P(1001,2)*UFO::{}::P(2004,1)+UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,1003)*UFO::{}::P(1001,4)*UFO::{}::P(2004,2)+UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1001,2)*UFO::{}::P(1003,1)+UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1001,3)*UFO::{}::P(1003,1)+UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1001,3)*UFO::{}::P(1003,4)+UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1001,4)*UFO::{}::P(1003,2)+UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1001,2)*UFO::{}::P(2003,1)+UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1001,3)*UFO::{}::P(2003,1)+UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1001,3)*UFO::{}::P(2003,4)+UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1001,4)*UFO::{}::P(2003,2)+UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,1001)*UFO::{}::P(1003,1)*UFO::{}::P(2004,3)+UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,1001)*UFO::{}::P(1003,2)*UFO::{}::P(2004,1)+UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,1001)*UFO::{}::P(1003,4)*UFO::{}::P(2004,2)+UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,1001)*UFO::{}::P(1003,4)*UFO::{}::P(2004,3)+UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,1003)*UFO::{}::P(1001,2)*UFO::{}::P(2004,3)+UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,1003)*UFO::{}::P(1001,3)*UFO::{}::P(2004,1)+UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,1003)*UFO::{}::P(1001,4)*UFO::{}::P(2004,1)+UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,1003)*UFO::{}::P(1001,4)*UFO::{}::P(2004,2)+UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1003,1)*UFO::{}::P(2002,3)+UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1003,2)*UFO::{}::P(2002,3)+UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1003,2)*UFO::{}::P(2002,4)+UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1003,4)*UFO::{}::P(2002,1)+UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1001,2)*UFO::{}::P(2002,1)+UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1001,2)*UFO::{}::P(2002,4)+UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1001,3)*UFO::{}::P(2002,1)+UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1001,4)*UFO::{}::P(2002,3)+UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,1003)*UFO::{}::P(1001,2)*UFO::{}::P(2003,4)+UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,1003)*UFO::{}::P(1001,3)*UFO::{}::P(2003,1)+UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,1003)*UFO::{}::P(1001,3)*UFO::{}::P(2003,2)+UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,1003)*UFO::{}::P(1001,4)*UFO::{}::P(2003,1)+UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,2003)*UFO::{}::P(1001,2)*UFO::{}::P(1003,4)+UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,2003)*UFO::{}::P(1001,3)*UFO::{}::P(1003,1)+UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,2003)*UFO::{}::P(1001,3)*UFO::{}::P(1003,2)+UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,2003)*UFO::{}::P(1001,4)*UFO::{}::P(1003,1)+UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2003,1001)*UFO::{}::P(1003,1)*UFO::{}::P(2002,3)+UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2003,1001)*UFO::{}::P(1003,2)*UFO::{}::P(2002,3)+UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2003,1001)*UFO::{}::P(1003,2)*UFO::{}::P(2002,4)+UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2003,1001)*UFO::{}::P(1003,4)*UFO::{}::P(2002,1)+UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2003,1003)*UFO::{}::P(1001,2)*UFO::{}::P(2002,4)+UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2003,1003)*UFO::{}::P(1001,4)*UFO::{}::P(2002,1)+UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,1003)*UFO::{}::P(2001,2)*UFO::{}::P(2004,1)+UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,1003)*UFO::{}::P(2001,4)*UFO::{}::P(2004,2)+UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1003,1)*UFO::{}::P(2001,2)+UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1003,1)*UFO::{}::P(2001,3)+UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1003,2)*UFO::{}::P(2001,4)+UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1003,4)*UFO::{}::P(2001,3)+UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2004,1003)*UFO::{}::P(2001,2)*UFO::{}::P(2003,1)+UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2004,1003)*UFO::{}::P(2001,3)*UFO::{}::P(2003,1)+UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2004,1003)*UFO::{}::P(2001,3)*UFO::{}::P(2003,4)+UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2004,1003)*UFO::{}::P(2001,4)*UFO::{}::P(2003,2)+UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,1001)*UFO::{}::P(2001,2)*UFO::{}::P(2004,3)+UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,1001)*UFO::{}::P(2001,3)*UFO::{}::P(2004,1)+UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,1001)*UFO::{}::P(2001,4)*UFO::{}::P(2004,1)+UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,1001)*UFO::{}::P(2001,4)*UFO::{}::P(2004,2)+UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2004,1001)*UFO::{}::P(2001,2)*UFO::{}::P(2003,4)+UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2004,1001)*UFO::{}::P(2001,3)*UFO::{}::P(2003,1)+UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2004,1001)*UFO::{}::P(2001,3)*UFO::{}::P(2003,2)+UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2004,1001)*UFO::{}::P(2001,4)*UFO::{}::P(2003,1)+UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2002,2003)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1003,1)*UFO::{}::P(2001,3)+UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2002,2003)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1003,1)*UFO::{}::P(2001,4)+UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2002,2003)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1003,2)*UFO::{}::P(2001,3)+UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2002,2003)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1003,4)*UFO::{}::P(2001,2)+UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2003,1001)*UFO::{}::Metric(2004,1003)*UFO::{}::P(2001,2)*UFO::{}::P(2002,1)+UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2003,1001)*UFO::{}::Metric(2004,1003)*UFO::{}::P(2001,2)*UFO::{}::P(2002,4)+UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2003,1001)*UFO::{}::Metric(2004,1003)*UFO::{}::P(2001,3)*UFO::{}::P(2002,1)+UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2003,1001)*UFO::{}::Metric(2004,1003)*UFO::{}::P(2001,4)*UFO::{}::P(2002,3)+UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2003,1003)*UFO::{}::Metric(2004,1001)*UFO::{}::P(2001,2)*UFO::{}::P(2002,4)+UFO::{}::Metric(1002,1004)*UFO::{}::Metric(2003,1003)*UFO::{}::Metric(2004,1001)*UFO::{}::P(2001,4)*UFO::{}::P(2002,1)+UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,1001)*UFO::{}::Metric(2002,2003)*UFO::{}::P(1002,3)*UFO::{}::P(2004,2)+UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,1001)*UFO::{}::Metric(2002,2003)*UFO::{}::P(1002,4)*UFO::{}::P(2004,3)+UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,1001)*UFO::{}::Metric(2002,2004)*UFO::{}::P(1002,3)*UFO::{}::P(2003,4)+UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,1001)*UFO::{}::Metric(2002,2004)*UFO::{}::P(1002,4)*UFO::{}::P(2003,2)+UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,1001)*UFO::{}::Metric(2003,1002)*UFO::{}::P(2002,3)*UFO::{}::P(2004,2)+UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,1001)*UFO::{}::Metric(2003,1002)*UFO::{}::P(2002,4)*UFO::{}::P(2004,3)+UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,1001)*UFO::{}::Metric(2004,1002)*UFO::{}::P(2002,3)*UFO::{}::P(2003,4)+UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,1001)*UFO::{}::Metric(2004,1002)*UFO::{}::P(2002,4)*UFO::{}::P(2003,2)+UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,2003)*UFO::{}::P(1001,2)*UFO::{}::P(2004,1)+UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,2003)*UFO::{}::P(1001,3)*UFO::{}::P(2004,2)+UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,2003)*UFO::{}::P(1001,4)*UFO::{}::P(2004,1)+UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,2003)*UFO::{}::P(1001,4)*UFO::{}::P(2004,3)+UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,2004)*UFO::{}::P(1001,2)*UFO::{}::P(2003,1)+UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,2004)*UFO::{}::P(1001,3)*UFO::{}::P(2003,1)+UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,2004)*UFO::{}::P(1001,3)*UFO::{}::P(2003,4)+UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,2004)*UFO::{}::P(1001,4)*UFO::{}::P(2003,2)+UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2003,1001)*UFO::{}::P(2002,1)*UFO::{}::P(2004,2)+UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2003,1001)*UFO::{}::P(2002,3)*UFO::{}::P(2004,1)+UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2003,1001)*UFO::{}::P(2002,4)*UFO::{}::P(2004,2)+UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2003,1001)*UFO::{}::P(2002,4)*UFO::{}::P(2004,3)+UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2004,1001)*UFO::{}::P(2002,1)*UFO::{}::P(2003,2)+UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2004,1001)*UFO::{}::P(2002,3)*UFO::{}::P(2003,2)+UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2004,1001)*UFO::{}::P(2002,3)*UFO::{}::P(2003,4)+UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2004,1001)*UFO::{}::P(2002,4)*UFO::{}::P(2003,1)+UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,1001)*UFO::{}::P(1002,1)*UFO::{}::P(2004,2)+UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,1001)*UFO::{}::P(1002,3)*UFO::{}::P(2004,1)+UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,1001)*UFO::{}::P(1002,4)*UFO::{}::P(2004,2)+UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,1001)*UFO::{}::P(1002,4)*UFO::{}::P(2004,3)+UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,1002)*UFO::{}::P(1001,2)*UFO::{}::P(2004,1)+UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,1002)*UFO::{}::P(1001,3)*UFO::{}::P(2004,2)+UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,1002)*UFO::{}::P(1001,4)*UFO::{}::P(2004,1)+UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,1002)*UFO::{}::P(1001,4)*UFO::{}::P(2004,3)+UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1002,1)*UFO::{}::P(2003,2)+UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1002,3)*UFO::{}::P(2003,2)+UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1002,3)*UFO::{}::P(2003,4)+UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1002,4)*UFO::{}::P(2003,1)+UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1001,2)*UFO::{}::P(2003,1)+UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1001,3)*UFO::{}::P(2003,1)+UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1001,3)*UFO::{}::P(2003,4)+UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1001,4)*UFO::{}::P(2003,2)+UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,1001)*UFO::{}::P(1002,1)*UFO::{}::P(2004,2)+UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,1001)*UFO::{}::P(1002,3)*UFO::{}::P(2004,1)+UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,1001)*UFO::{}::P(1002,4)*UFO::{}::P(2004,2)+UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,1001)*UFO::{}::P(1002,4)*UFO::{}::P(2004,3)+UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,1002)*UFO::{}::P(1001,3)*UFO::{}::P(2004,1)+UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,1002)*UFO::{}::P(1001,4)*UFO::{}::P(2004,3)+UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,2004)*UFO::{}::P(1001,2)*UFO::{}::P(1002,1)+UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,2004)*UFO::{}::P(1001,2)*UFO::{}::P(1002,4)+UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,2004)*UFO::{}::P(1001,3)*UFO::{}::P(1002,1)+UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,2004)*UFO::{}::P(1001,4)*UFO::{}::P(1002,3)+UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1001,2)*UFO::{}::P(2002,1)+UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1001,2)*UFO::{}::P(2002,4)+UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1001,3)*UFO::{}::P(2002,1)+UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1001,4)*UFO::{}::P(2002,3)+UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,1001)*UFO::{}::P(1002,1)*UFO::{}::P(2003,2)+UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,1001)*UFO::{}::P(1002,3)*UFO::{}::P(2003,2)+UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,1001)*UFO::{}::P(1002,3)*UFO::{}::P(2003,4)+UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,1001)*UFO::{}::P(1002,4)*UFO::{}::P(2003,1)+UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,1002)*UFO::{}::P(1001,3)*UFO::{}::P(2003,4)+UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,1002)*UFO::{}::P(1001,4)*UFO::{}::P(2003,1)+UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,2003)*UFO::{}::P(1001,2)*UFO::{}::P(1002,1)+UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,2003)*UFO::{}::P(1001,2)*UFO::{}::P(1002,3)+UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,2003)*UFO::{}::P(1001,3)*UFO::{}::P(1002,4)+UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,2003)*UFO::{}::P(1001,4)*UFO::{}::P(1002,1)+UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2003,1002)*UFO::{}::P(1001,2)*UFO::{}::P(2002,1)+UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2003,1002)*UFO::{}::P(1001,2)*UFO::{}::P(2002,3)+UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2003,1002)*UFO::{}::P(1001,3)*UFO::{}::P(2002,4)+UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2003,1002)*UFO::{}::P(1001,4)*UFO::{}::P(2002,1)+UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,1002)*UFO::{}::P(2001,2)*UFO::{}::P(2004,1)+UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,1002)*UFO::{}::P(2001,3)*UFO::{}::P(2004,2)+UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,1002)*UFO::{}::P(2001,4)*UFO::{}::P(2004,1)+UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,1002)*UFO::{}::P(2001,4)*UFO::{}::P(2004,3)+UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2004,1002)*UFO::{}::P(2001,2)*UFO::{}::P(2003,1)+UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2004,1002)*UFO::{}::P(2001,3)*UFO::{}::P(2003,1)+UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2004,1002)*UFO::{}::P(2001,3)*UFO::{}::P(2003,4)+UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2004,1002)*UFO::{}::P(2001,4)*UFO::{}::P(2003,2)+UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2002,1002)*UFO::{}::Metric(2003,1001)*UFO::{}::P(2001,3)*UFO::{}::P(2004,1)+UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2002,1002)*UFO::{}::Metric(2003,1001)*UFO::{}::P(2001,4)*UFO::{}::P(2004,3)+UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2002,1002)*UFO::{}::Metric(2004,1001)*UFO::{}::P(2001,3)*UFO::{}::P(2003,4)+UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2002,1002)*UFO::{}::Metric(2004,1001)*UFO::{}::P(2001,4)*UFO::{}::P(2003,1)+UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2002,2003)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1002,1)*UFO::{}::P(2001,2)+UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2002,2003)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1002,1)*UFO::{}::P(2001,4)+UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2002,2003)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1002,3)*UFO::{}::P(2001,2)+UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2002,2003)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1002,4)*UFO::{}::P(2001,3)+UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2002,2004)*UFO::{}::Metric(2003,1001)*UFO::{}::P(1002,1)*UFO::{}::P(2001,2)+UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2002,2004)*UFO::{}::Metric(2003,1001)*UFO::{}::P(1002,1)*UFO::{}::P(2001,3)+UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2002,2004)*UFO::{}::Metric(2003,1001)*UFO::{}::P(1002,3)*UFO::{}::P(2001,4)+UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2002,2004)*UFO::{}::Metric(2003,1001)*UFO::{}::P(1002,4)*UFO::{}::P(2001,2)+UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2003,1001)*UFO::{}::Metric(2004,1002)*UFO::{}::P(2001,2)*UFO::{}::P(2002,1)+UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2003,1001)*UFO::{}::Metric(2004,1002)*UFO::{}::P(2001,2)*UFO::{}::P(2002,4)+UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2003,1001)*UFO::{}::Metric(2004,1002)*UFO::{}::P(2001,3)*UFO::{}::P(2002,1)+UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2003,1001)*UFO::{}::Metric(2004,1002)*UFO::{}::P(2001,4)*UFO::{}::P(2002,3)+UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2003,1002)*UFO::{}::Metric(2004,1001)*UFO::{}::P(2001,2)*UFO::{}::P(2002,1)+UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2003,1002)*UFO::{}::Metric(2004,1001)*UFO::{}::P(2001,2)*UFO::{}::P(2002,3)+UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2003,1002)*UFO::{}::Metric(2004,1001)*UFO::{}::P(2001,3)*UFO::{}::P(2002,4)+UFO::{}::Metric(1003,1004)*UFO::{}::Metric(2003,1002)*UFO::{}::Metric(2004,1001)*UFO::{}::P(2001,4)*UFO::{}::P(2002,1)+UFO::{}::Metric(2001,1001)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,1004)*UFO::{}::P(1002,3)*UFO::{}::P(2004,2)+UFO::{}::Metric(2001,1001)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,1004)*UFO::{}::P(1002,4)*UFO::{}::P(2004,3)+UFO::{}::Metric(2001,1001)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1002,3)*UFO::{}::P(1004,2)+UFO::{}::Metric(2001,1001)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1002,4)*UFO::{}::P(1004,3)+UFO::{}::Metric(2001,1001)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1004,2)*UFO::{}::P(2003,4)+UFO::{}::Metric(2001,1001)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1004,3)*UFO::{}::P(2003,2)+UFO::{}::Metric(2001,1001)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2003,1002)*UFO::{}::P(1003,2)*UFO::{}::P(2004,3)+UFO::{}::Metric(2001,1001)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2003,1002)*UFO::{}::P(1003,4)*UFO::{}::P(2004,2)+UFO::{}::Metric(2001,1001)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1002,3)*UFO::{}::P(1003,4)+UFO::{}::Metric(2001,1001)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1002,4)*UFO::{}::P(1003,2)+UFO::{}::Metric(2001,1001)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1002,3)*UFO::{}::P(2003,4)+UFO::{}::Metric(2001,1001)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1002,4)*UFO::{}::P(2003,2)+UFO::{}::Metric(2001,1001)*UFO::{}::Metric(2002,2003)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1003,2)*UFO::{}::P(1004,3)+UFO::{}::Metric(2001,1001)*UFO::{}::Metric(2002,2003)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1003,4)*UFO::{}::P(1004,2)+UFO::{}::Metric(2001,1001)*UFO::{}::Metric(2002,2003)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1002,3)*UFO::{}::P(1004,2)+UFO::{}::Metric(2001,1001)*UFO::{}::Metric(2002,2003)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1002,4)*UFO::{}::P(1004,3)+UFO::{}::Metric(2001,1001)*UFO::{}::Metric(2002,2004)*UFO::{}::Metric(2003,1002)*UFO::{}::P(1003,2)*UFO::{}::P(1004,3)+UFO::{}::Metric(2001,1001)*UFO::{}::Metric(2002,2004)*UFO::{}::Metric(2003,1002)*UFO::{}::P(1003,4)*UFO::{}::P(1004,2)+UFO::{}::Metric(2001,1001)*UFO::{}::Metric(2002,2004)*UFO::{}::Metric(2003,1004)*UFO::{}::P(1002,3)*UFO::{}::P(1003,4)+UFO::{}::Metric(2001,1001)*UFO::{}::Metric(2002,2004)*UFO::{}::Metric(2003,1004)*UFO::{}::P(1002,4)*UFO::{}::P(1003,2)+UFO::{}::Metric(2001,1001)*UFO::{}::Metric(2003,1002)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1004,2)*UFO::{}::P(2002,3)+UFO::{}::Metric(2001,1001)*UFO::{}::Metric(2003,1002)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1004,3)*UFO::{}::P(2002,4)+UFO::{}::Metric(2001,1001)*UFO::{}::Metric(2003,1004)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1003,2)*UFO::{}::P(2002,4)+UFO::{}::Metric(2001,1001)*UFO::{}::Metric(2003,1004)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1003,4)*UFO::{}::P(2002,3)+UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,1004)*UFO::{}::P(1001,2)*UFO::{}::P(2004,1)+UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,1004)*UFO::{}::P(1001,3)*UFO::{}::P(2004,2)+UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,1004)*UFO::{}::P(1001,4)*UFO::{}::P(2004,1)+UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,1004)*UFO::{}::P(1001,4)*UFO::{}::P(2004,3)+UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1001,2)*UFO::{}::P(1004,1)+UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1001,3)*UFO::{}::P(1004,2)+UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1001,4)*UFO::{}::P(1004,1)+UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1001,4)*UFO::{}::P(1004,3)+UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1004,1)*UFO::{}::P(2003,4)+UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1004,2)*UFO::{}::P(2003,1)+UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1004,3)*UFO::{}::P(2003,2)+UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1004,3)*UFO::{}::P(2003,4)+UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2004,1004)*UFO::{}::P(1001,2)*UFO::{}::P(2003,1)+UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2004,1004)*UFO::{}::P(1001,3)*UFO::{}::P(2003,2)+UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2003,1001)*UFO::{}::P(1003,1)*UFO::{}::P(2004,3)+UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2003,1001)*UFO::{}::P(1003,2)*UFO::{}::P(2004,1)+UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2003,1001)*UFO::{}::P(1003,4)*UFO::{}::P(2004,2)+UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2003,1001)*UFO::{}::P(1003,4)*UFO::{}::P(2004,3)+UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2003,1003)*UFO::{}::P(1001,2)*UFO::{}::P(2004,1)+UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2003,1003)*UFO::{}::P(1001,4)*UFO::{}::P(2004,2)+UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1001,2)*UFO::{}::P(1003,1)+UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1001,3)*UFO::{}::P(1003,1)+UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1001,3)*UFO::{}::P(1003,4)+UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1001,4)*UFO::{}::P(1003,2)+UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1001,2)*UFO::{}::P(2003,1)+UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1001,3)*UFO::{}::P(2003,1)+UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1001,3)*UFO::{}::P(2003,4)+UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1001,4)*UFO::{}::P(2003,2)+UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,2003)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1003,1)*UFO::{}::P(1004,2)+UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,2003)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1003,2)*UFO::{}::P(1004,3)+UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,2003)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1003,4)*UFO::{}::P(1004,1)+UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,2003)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1003,4)*UFO::{}::P(1004,3)+UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,2003)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1001,2)*UFO::{}::P(1004,1)+UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,2003)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1001,3)*UFO::{}::P(1004,2)+UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,2003)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1001,4)*UFO::{}::P(1004,1)+UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,2003)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1001,4)*UFO::{}::P(1004,3)+UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,2003)*UFO::{}::Metric(2004,1004)*UFO::{}::P(1001,2)*UFO::{}::P(1003,1)+UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,2003)*UFO::{}::Metric(2004,1004)*UFO::{}::P(1001,3)*UFO::{}::P(1003,2)+UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,2004)*UFO::{}::Metric(2003,1001)*UFO::{}::P(1003,1)*UFO::{}::P(1004,3)+UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,2004)*UFO::{}::Metric(2003,1001)*UFO::{}::P(1003,2)*UFO::{}::P(1004,1)+UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,2004)*UFO::{}::Metric(2003,1001)*UFO::{}::P(1003,4)*UFO::{}::P(1004,2)+UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,2004)*UFO::{}::Metric(2003,1001)*UFO::{}::P(1003,4)*UFO::{}::P(1004,3)+UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,2004)*UFO::{}::Metric(2003,1003)*UFO::{}::P(1001,2)*UFO::{}::P(1004,1)+UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,2004)*UFO::{}::Metric(2003,1003)*UFO::{}::P(1001,4)*UFO::{}::P(1004,2)+UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,2004)*UFO::{}::Metric(2003,1004)*UFO::{}::P(1001,2)*UFO::{}::P(1003,1)+UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,2004)*UFO::{}::Metric(2003,1004)*UFO::{}::P(1001,3)*UFO::{}::P(1003,1)+UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,2004)*UFO::{}::Metric(2003,1004)*UFO::{}::P(1001,3)*UFO::{}::P(1003,4)+UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2002,2004)*UFO::{}::Metric(2003,1004)*UFO::{}::P(1001,4)*UFO::{}::P(1003,2)+UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2003,1001)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1004,1)*UFO::{}::P(2002,3)+UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2003,1001)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1004,2)*UFO::{}::P(2002,1)+UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2003,1001)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1004,2)*UFO::{}::P(2002,4)+UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2003,1001)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1004,3)*UFO::{}::P(2002,4)+UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2003,1001)*UFO::{}::Metric(2004,1004)*UFO::{}::P(1003,1)*UFO::{}::P(2002,3)+UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2003,1001)*UFO::{}::Metric(2004,1004)*UFO::{}::P(1003,2)*UFO::{}::P(2002,1)+UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2003,1003)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1004,1)*UFO::{}::P(2002,4)+UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2003,1003)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1004,2)*UFO::{}::P(2002,1)+UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2003,1004)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1003,1)*UFO::{}::P(2002,4)+UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2003,1004)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1003,2)*UFO::{}::P(2002,1)+UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2003,1004)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1003,2)*UFO::{}::P(2002,3)+UFO::{}::Metric(2001,1002)*UFO::{}::Metric(2003,1004)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1003,4)*UFO::{}::P(2002,3)+UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,1004)*UFO::{}::P(1002,1)*UFO::{}::P(2004,2)+UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,1004)*UFO::{}::P(1002,3)*UFO::{}::P(2004,1)+UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,1004)*UFO::{}::P(1002,4)*UFO::{}::P(2004,2)+UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,1004)*UFO::{}::P(1002,4)*UFO::{}::P(2004,3)+UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1002,1)*UFO::{}::P(1004,2)+UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1002,3)*UFO::{}::P(1004,1)+UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1002,4)*UFO::{}::P(1004,2)+UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1002,4)*UFO::{}::P(1004,3)+UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1004,1)*UFO::{}::P(2003,2)+UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1004,2)*UFO::{}::P(2003,4)+UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1004,3)*UFO::{}::P(2003,1)+UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1004,3)*UFO::{}::P(2003,4)+UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2004,1004)*UFO::{}::P(1002,1)*UFO::{}::P(2003,2)+UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2004,1004)*UFO::{}::P(1002,3)*UFO::{}::P(2003,1)+UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,1002)*UFO::{}::Metric(2003,1004)*UFO::{}::P(1001,3)*UFO::{}::P(2004,1)+UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,1002)*UFO::{}::Metric(2003,1004)*UFO::{}::P(1001,4)*UFO::{}::P(2004,3)+UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,1002)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1001,3)*UFO::{}::P(1004,1)+UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,1002)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1001,4)*UFO::{}::P(1004,3)+UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,1002)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1004,1)*UFO::{}::P(2003,4)+UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,1002)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1004,3)*UFO::{}::P(2003,1)+UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2003,1002)*UFO::{}::P(1001,2)*UFO::{}::P(2004,3)+UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2003,1002)*UFO::{}::P(1001,3)*UFO::{}::P(2004,1)+UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2003,1002)*UFO::{}::P(1001,4)*UFO::{}::P(2004,1)+UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2003,1002)*UFO::{}::P(1001,4)*UFO::{}::P(2004,2)+UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1001,2)*UFO::{}::P(1002,1)+UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1001,2)*UFO::{}::P(1002,4)+UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1001,3)*UFO::{}::P(1002,1)+UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1001,4)*UFO::{}::P(1002,3)+UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1002,1)*UFO::{}::P(2003,4)+UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1002,3)*UFO::{}::P(2003,1)+UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1002,3)*UFO::{}::P(2003,2)+UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1002,4)*UFO::{}::P(2003,2)+UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,2003)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1002,1)*UFO::{}::P(1004,3)+UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,2003)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1002,3)*UFO::{}::P(1004,2)+UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,2003)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1002,4)*UFO::{}::P(1004,1)+UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,2003)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1002,4)*UFO::{}::P(1004,2)+UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,2003)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1001,2)*UFO::{}::P(1004,3)+UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,2003)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1001,3)*UFO::{}::P(1004,1)+UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,2003)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1001,4)*UFO::{}::P(1004,1)+UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,2003)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1001,4)*UFO::{}::P(1004,2)+UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,2003)*UFO::{}::Metric(2004,1004)*UFO::{}::P(1001,2)*UFO::{}::P(1002,3)+UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,2003)*UFO::{}::Metric(2004,1004)*UFO::{}::P(1001,3)*UFO::{}::P(1002,1)+UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,2004)*UFO::{}::Metric(2003,1002)*UFO::{}::P(1001,2)*UFO::{}::P(1004,3)+UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,2004)*UFO::{}::Metric(2003,1002)*UFO::{}::P(1001,3)*UFO::{}::P(1004,1)+UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,2004)*UFO::{}::Metric(2003,1002)*UFO::{}::P(1001,4)*UFO::{}::P(1004,1)+UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,2004)*UFO::{}::Metric(2003,1002)*UFO::{}::P(1001,4)*UFO::{}::P(1004,2)+UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,2004)*UFO::{}::Metric(2003,1004)*UFO::{}::P(1001,2)*UFO::{}::P(1002,1)+UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,2004)*UFO::{}::Metric(2003,1004)*UFO::{}::P(1001,2)*UFO::{}::P(1002,4)+UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,2004)*UFO::{}::Metric(2003,1004)*UFO::{}::P(1001,3)*UFO::{}::P(1002,1)+UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2002,2004)*UFO::{}::Metric(2003,1004)*UFO::{}::P(1001,4)*UFO::{}::P(1002,3)+UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2003,1002)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1004,1)*UFO::{}::P(2002,4)+UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2003,1002)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1004,2)*UFO::{}::P(2002,3)+UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2003,1002)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1004,2)*UFO::{}::P(2002,4)+UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2003,1002)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1004,3)*UFO::{}::P(2002,1)+UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2003,1002)*UFO::{}::Metric(2004,1004)*UFO::{}::P(1001,2)*UFO::{}::P(2002,3)+UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2003,1002)*UFO::{}::Metric(2004,1004)*UFO::{}::P(1001,3)*UFO::{}::P(2002,1)+UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2003,1004)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1001,2)*UFO::{}::P(2002,1)+UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2003,1004)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1001,2)*UFO::{}::P(2002,4)+UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2003,1004)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1001,3)*UFO::{}::P(2002,1)+UFO::{}::Metric(2001,1003)*UFO::{}::Metric(2003,1004)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1001,4)*UFO::{}::P(2002,3)+UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,1002)*UFO::{}::P(1003,1)*UFO::{}::P(2004,2)+UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,1002)*UFO::{}::P(1003,2)*UFO::{}::P(2004,3)+UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,1002)*UFO::{}::P(1003,4)*UFO::{}::P(2004,1)+UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,1002)*UFO::{}::P(1003,4)*UFO::{}::P(2004,3)+UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,1003)*UFO::{}::P(1002,1)*UFO::{}::P(2004,2)+UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,1003)*UFO::{}::P(1002,4)*UFO::{}::P(2004,1)+UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1002,1)*UFO::{}::P(1003,2)+UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1002,3)*UFO::{}::P(1003,2)+UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1002,3)*UFO::{}::P(1003,4)+UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1002,4)*UFO::{}::P(1003,1)+UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1002,1)*UFO::{}::P(2003,2)+UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1002,3)*UFO::{}::P(2003,2)+UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1002,3)*UFO::{}::P(2003,4)+UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1002,4)*UFO::{}::P(2003,1)+UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,1002)*UFO::{}::Metric(2003,1001)*UFO::{}::P(1003,1)*UFO::{}::P(2004,3)+UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,1002)*UFO::{}::Metric(2003,1001)*UFO::{}::P(1003,4)*UFO::{}::P(2004,1)+UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,1002)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1001,3)*UFO::{}::P(1003,4)+UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,1002)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1001,4)*UFO::{}::P(1003,1)+UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,1002)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1001,3)*UFO::{}::P(2003,4)+UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,1002)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1001,4)*UFO::{}::P(2003,1)+UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,1001)*UFO::{}::P(1002,1)*UFO::{}::P(2004,3)+UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,1001)*UFO::{}::P(1002,3)*UFO::{}::P(2004,2)+UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,1001)*UFO::{}::P(1002,4)*UFO::{}::P(2004,1)+UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,1001)*UFO::{}::P(1002,4)*UFO::{}::P(2004,2)+UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1001,2)*UFO::{}::P(1002,1)+UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1001,2)*UFO::{}::P(1002,3)+UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1001,3)*UFO::{}::P(1002,4)+UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,2004)*UFO::{}::P(1001,4)*UFO::{}::P(1002,1)+UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1001,2)*UFO::{}::P(2003,4)+UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1001,3)*UFO::{}::P(2003,1)+UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1001,3)*UFO::{}::P(2003,2)+UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1001,4)*UFO::{}::P(2003,1)+UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,2003)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1001,2)*UFO::{}::P(1003,4)+UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,2003)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1001,3)*UFO::{}::P(1003,1)+UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,2003)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1001,3)*UFO::{}::P(1003,2)+UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,2003)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1001,4)*UFO::{}::P(1003,1)+UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,2003)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1001,2)*UFO::{}::P(1002,1)+UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,2003)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1001,2)*UFO::{}::P(1002,3)+UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,2003)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1001,3)*UFO::{}::P(1002,4)+UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,2003)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1001,4)*UFO::{}::P(1002,1)+UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,2004)*UFO::{}::Metric(2003,1001)*UFO::{}::P(1002,1)*UFO::{}::P(1003,4)+UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,2004)*UFO::{}::Metric(2003,1001)*UFO::{}::P(1002,3)*UFO::{}::P(1003,1)+UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,2004)*UFO::{}::Metric(2003,1001)*UFO::{}::P(1002,3)*UFO::{}::P(1003,2)+UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,2004)*UFO::{}::Metric(2003,1001)*UFO::{}::P(1002,4)*UFO::{}::P(1003,2)+UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,2004)*UFO::{}::Metric(2003,1002)*UFO::{}::P(1001,2)*UFO::{}::P(1003,4)+UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,2004)*UFO::{}::Metric(2003,1002)*UFO::{}::P(1001,3)*UFO::{}::P(1003,1)+UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,2004)*UFO::{}::Metric(2003,1002)*UFO::{}::P(1001,3)*UFO::{}::P(1003,2)+UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,2004)*UFO::{}::Metric(2003,1002)*UFO::{}::P(1001,4)*UFO::{}::P(1003,1)+UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,2004)*UFO::{}::Metric(2003,1003)*UFO::{}::P(1001,2)*UFO::{}::P(1002,4)+UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2002,2004)*UFO::{}::Metric(2003,1003)*UFO::{}::P(1001,4)*UFO::{}::P(1002,1)+UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2003,1001)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1003,1)*UFO::{}::P(2002,3)+UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2003,1001)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1003,2)*UFO::{}::P(2002,3)+UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2003,1001)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1003,2)*UFO::{}::P(2002,4)+UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2003,1001)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1003,4)*UFO::{}::P(2002,1)+UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2003,1002)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1001,2)*UFO::{}::P(2002,1)+UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2003,1002)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1001,2)*UFO::{}::P(2002,3)+UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2003,1002)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1001,3)*UFO::{}::P(2002,4)+UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2003,1002)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1001,4)*UFO::{}::P(2002,1)+UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2003,1003)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1001,2)*UFO::{}::P(2002,4)+UFO::{}::Metric(2001,1004)*UFO::{}::Metric(2003,1003)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1001,4)*UFO::{}::P(2002,1)+UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,1001)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1003,1)*UFO::{}::P(1004,3)+UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,1001)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1003,2)*UFO::{}::P(1004,1)+UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,1001)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1003,4)*UFO::{}::P(1004,2)+UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,1001)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1003,4)*UFO::{}::P(1004,3)+UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,1001)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1002,1)*UFO::{}::P(1004,2)+UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,1001)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1002,3)*UFO::{}::P(1004,1)+UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,1001)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1002,4)*UFO::{}::P(1004,2)+UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,1001)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1002,4)*UFO::{}::P(1004,3)+UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,1001)*UFO::{}::Metric(2004,1004)*UFO::{}::P(1002,1)*UFO::{}::P(1003,2)+UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,1001)*UFO::{}::Metric(2004,1004)*UFO::{}::P(1002,3)*UFO::{}::P(1003,1)+UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,1002)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1003,1)*UFO::{}::P(1004,2)+UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,1002)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1003,2)*UFO::{}::P(1004,3)+UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,1002)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1003,4)*UFO::{}::P(1004,1)+UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,1002)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1003,4)*UFO::{}::P(1004,3)+UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,1002)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1001,2)*UFO::{}::P(1004,1)+UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,1002)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1001,3)*UFO::{}::P(1004,2)+UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,1002)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1001,4)*UFO::{}::P(1004,1)+UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,1002)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1001,4)*UFO::{}::P(1004,3)+UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,1002)*UFO::{}::Metric(2004,1004)*UFO::{}::P(1001,2)*UFO::{}::P(1003,1)+UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,1002)*UFO::{}::Metric(2004,1004)*UFO::{}::P(1001,3)*UFO::{}::P(1003,2)+UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,1003)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1002,1)*UFO::{}::P(1004,2)+UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,1003)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1002,4)*UFO::{}::P(1004,1)+UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,1003)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1001,2)*UFO::{}::P(1004,1)+UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,1003)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1001,4)*UFO::{}::P(1004,2)+UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,1004)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1002,1)*UFO::{}::P(1003,2)+UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,1004)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1002,3)*UFO::{}::P(1003,2)+UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,1004)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1002,3)*UFO::{}::P(1003,4)+UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,1004)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1002,4)*UFO::{}::P(1003,1)+UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,1004)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1001,2)*UFO::{}::P(1003,1)+UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,1004)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1001,3)*UFO::{}::P(1003,1)+UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,1004)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1001,3)*UFO::{}::P(1003,4)+UFO::{}::Metric(2001,2002)*UFO::{}::Metric(2003,1004)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1001,4)*UFO::{}::P(1003,2)+UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1003,1)*UFO::{}::P(1004,3)+UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1003,2)*UFO::{}::P(1004,1)+UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1003,4)*UFO::{}::P(1004,2)+UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1003,4)*UFO::{}::P(1004,3)+UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1002,1)*UFO::{}::P(1004,2)+UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1002,3)*UFO::{}::P(1004,1)+UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1002,4)*UFO::{}::P(1004,2)+UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1002,4)*UFO::{}::P(1004,3)+UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2004,1004)*UFO::{}::P(1002,1)*UFO::{}::P(1003,2)+UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2004,1004)*UFO::{}::P(1002,3)*UFO::{}::P(1003,1)+UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,1002)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1003,1)*UFO::{}::P(1004,3)+UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,1002)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1003,4)*UFO::{}::P(1004,1)+UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,1002)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1001,3)*UFO::{}::P(1004,1)+UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,1002)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1001,4)*UFO::{}::P(1004,3)+UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1002,1)*UFO::{}::P(1004,3)+UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1002,3)*UFO::{}::P(1004,2)+UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1002,4)*UFO::{}::P(1004,1)+UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1002,4)*UFO::{}::P(1004,2)+UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1001,2)*UFO::{}::P(1004,3)+UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1001,3)*UFO::{}::P(1004,1)+UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1001,4)*UFO::{}::P(1004,1)+UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1001,4)*UFO::{}::P(1004,2)+UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2004,1004)*UFO::{}::P(1001,2)*UFO::{}::P(1002,3)+UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2004,1004)*UFO::{}::P(1001,3)*UFO::{}::P(1002,1)+UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1002,1)*UFO::{}::P(1003,4)+UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1002,3)*UFO::{}::P(1003,1)+UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1002,3)*UFO::{}::P(1003,2)+UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1002,4)*UFO::{}::P(1003,2)+UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1001,2)*UFO::{}::P(1002,1)+UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1001,2)*UFO::{}::P(1002,4)+UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1001,3)*UFO::{}::P(1002,1)+UFO::{}::Metric(2001,2003)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1001,4)*UFO::{}::P(1002,3)+UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,1002)*UFO::{}::P(1003,1)*UFO::{}::P(1004,2)+UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,1002)*UFO::{}::P(1003,2)*UFO::{}::P(1004,3)+UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,1002)*UFO::{}::P(1003,4)*UFO::{}::P(1004,1)+UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,1002)*UFO::{}::P(1003,4)*UFO::{}::P(1004,3)+UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,1003)*UFO::{}::P(1002,1)*UFO::{}::P(1004,2)+UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,1003)*UFO::{}::P(1002,4)*UFO::{}::P(1004,1)+UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,1004)*UFO::{}::P(1002,1)*UFO::{}::P(1003,2)+UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,1004)*UFO::{}::P(1002,3)*UFO::{}::P(1003,2)+UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,1004)*UFO::{}::P(1002,3)*UFO::{}::P(1003,4)+UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,1004)*UFO::{}::P(1002,4)*UFO::{}::P(1003,1)+UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,1002)*UFO::{}::Metric(2003,1001)*UFO::{}::P(1003,1)*UFO::{}::P(1004,3)+UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,1002)*UFO::{}::Metric(2003,1001)*UFO::{}::P(1003,4)*UFO::{}::P(1004,1)+UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,1002)*UFO::{}::Metric(2003,1004)*UFO::{}::P(1001,3)*UFO::{}::P(1003,4)+UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,1002)*UFO::{}::Metric(2003,1004)*UFO::{}::P(1001,4)*UFO::{}::P(1003,1)+UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,1001)*UFO::{}::P(1002,1)*UFO::{}::P(1004,3)+UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,1001)*UFO::{}::P(1002,3)*UFO::{}::P(1004,2)+UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,1001)*UFO::{}::P(1002,4)*UFO::{}::P(1004,1)+UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,1001)*UFO::{}::P(1002,4)*UFO::{}::P(1004,2)+UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,1004)*UFO::{}::P(1001,2)*UFO::{}::P(1002,1)+UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,1004)*UFO::{}::P(1001,2)*UFO::{}::P(1002,3)+UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,1004)*UFO::{}::P(1001,3)*UFO::{}::P(1002,4)+UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,1004)*UFO::{}::P(1001,4)*UFO::{}::P(1002,1)+UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2003,1001)*UFO::{}::P(1002,1)*UFO::{}::P(1003,4)+UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2003,1001)*UFO::{}::P(1002,3)*UFO::{}::P(1003,1)+UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2003,1001)*UFO::{}::P(1002,3)*UFO::{}::P(1003,2)+UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2003,1001)*UFO::{}::P(1002,4)*UFO::{}::P(1003,2)+UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2003,1002)*UFO::{}::P(1001,2)*UFO::{}::P(1003,4)+UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2003,1002)*UFO::{}::P(1001,3)*UFO::{}::P(1003,1)+UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2003,1002)*UFO::{}::P(1001,3)*UFO::{}::P(1003,2)+UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2003,1002)*UFO::{}::P(1001,4)*UFO::{}::P(1003,1)+UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2003,1003)*UFO::{}::P(1001,2)*UFO::{}::P(1002,4)+UFO::{}::Metric(2001,2004)*UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2003,1003)*UFO::{}::P(1001,4)*UFO::{}::P(1002,1)+UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,1002)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1004,1)*UFO::{}::P(2001,2)+UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,1002)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1004,1)*UFO::{}::P(2001,4)+UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,1002)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1004,2)*UFO::{}::P(2001,3)+UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,1002)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1004,3)*UFO::{}::P(2001,4)+UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,1002)*UFO::{}::Metric(2004,1004)*UFO::{}::P(1003,1)*UFO::{}::P(2001,2)+UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,1002)*UFO::{}::Metric(2004,1004)*UFO::{}::P(1003,2)*UFO::{}::P(2001,3)+UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,1003)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1004,1)*UFO::{}::P(2001,2)+UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,1003)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1004,2)*UFO::{}::P(2001,4)+UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,1004)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1003,1)*UFO::{}::P(2001,2)+UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,1004)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1003,1)*UFO::{}::P(2001,3)+UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,1004)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1003,2)*UFO::{}::P(2001,4)+UFO::{}::Metric(2002,1001)*UFO::{}::Metric(2003,1004)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1003,4)*UFO::{}::P(2001,3)+UFO::{}::Metric(2002,1002)*UFO::{}::Metric(2003,1001)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1004,1)*UFO::{}::P(2001,3)+UFO::{}::Metric(2002,1002)*UFO::{}::Metric(2003,1001)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1004,3)*UFO::{}::P(2001,4)+UFO::{}::Metric(2002,1002)*UFO::{}::Metric(2003,1004)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1003,1)*UFO::{}::P(2001,4)+UFO::{}::Metric(2002,1002)*UFO::{}::Metric(2003,1004)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1003,4)*UFO::{}::P(2001,3)+UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,1001)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1004,1)*UFO::{}::P(2001,3)+UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,1001)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1004,1)*UFO::{}::P(2001,4)+UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,1001)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1004,2)*UFO::{}::P(2001,4)+UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,1001)*UFO::{}::Metric(2004,1002)*UFO::{}::P(1004,3)*UFO::{}::P(2001,2)+UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,1001)*UFO::{}::Metric(2004,1004)*UFO::{}::P(1002,1)*UFO::{}::P(2001,3)+UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,1001)*UFO::{}::Metric(2004,1004)*UFO::{}::P(1002,3)*UFO::{}::P(2001,2)+UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,1004)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1002,1)*UFO::{}::P(2001,2)+UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,1004)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1002,1)*UFO::{}::P(2001,4)+UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,1004)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1002,3)*UFO::{}::P(2001,2)+UFO::{}::Metric(2002,1003)*UFO::{}::Metric(2003,1004)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1002,4)*UFO::{}::P(2001,3)+UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2003,1001)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1002,1)*UFO::{}::P(2001,2)+UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2003,1001)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1002,1)*UFO::{}::P(2001,3)+UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2003,1001)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1002,3)*UFO::{}::P(2001,4)+UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2003,1001)*UFO::{}::Metric(2004,1003)*UFO::{}::P(1002,4)*UFO::{}::P(2001,2)+UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2003,1002)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1003,1)*UFO::{}::P(2001,3)+UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2003,1002)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1003,1)*UFO::{}::P(2001,4)+UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2003,1002)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1003,2)*UFO::{}::P(2001,3)+UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2003,1002)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1003,4)*UFO::{}::P(2001,2)+UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2003,1003)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1002,1)*UFO::{}::P(2001,4)+UFO::{}::Metric(2002,1004)*UFO::{}::Metric(2003,1003)*UFO::{}::Metric(2004,1001)*UFO::{}::P(1002,4)*UFO::{}::P(2001,2)" + } + ], + "name": "scalar_gravity", + "orders": [ + { + "expansion_order": 99, + "hierarchy": 1, + "name": "QCD" + }, + { + "expansion_order": 99, + "hierarchy": 2, + "name": "QED" + }, + { + "expansion_order": 99, + "hierarchy": 0, + "name": "GRAV" + } + ], + "parameters": [ + { + "expression": null, + "lhablock": "GRAVITY", + "lhacode": [ + 1337 + ], + "name": "kappa", + "nature": "external", + "parameter_type": "real", + "value": [ + 1.0, + 0.0 + ] + }, + { + "expression": null, + "lhablock": "GRAVITY", + "lhacode": [ + 1338 + ], + "name": "dim", + "nature": "external", + "parameter_type": "real", + "value": [ + 4.0, + 0.0 + ] + }, + { + "expression": null, + "lhablock": "SCALARS", + "lhacode": [ + 2002 + ], + "name": "mass_scalar_1", + "nature": "external", + "parameter_type": "real", + "value": [ + 1.0, + 0.0 + ] + }, + { + "expression": null, + "lhablock": "SCALARS", + "lhacode": [ + 3002 + ], + "name": "width_scalar_1", + "nature": "external", + "parameter_type": "real", + "value": [ + 0.0, + 0.0 + ] + }, + { + "expression": null, + "lhablock": "SCALARS", + "lhacode": [ + 2003 + ], + "name": "mass_scalar_2", + "nature": "external", + "parameter_type": "real", + "value": [ + 2.0, + 0.0 + ] + }, + { + "expression": null, + "lhablock": "SCALARS", + "lhacode": [ + 3003 + ], + "name": "width_scalar_2", + "nature": "external", + "parameter_type": "real", + "value": [ + 0.0, + 0.0 + ] + }, + { + "expression": null, + "lhablock": "SMINPUTS", + "lhacode": [ + 1 + ], + "name": "aEWM1", + "nature": "external", + "parameter_type": "real", + "value": [ + 132.50698, + 0.0 + ] + }, + { + "expression": null, + "lhablock": "SMINPUTS", + "lhacode": [ + 2 + ], + "name": "Gf", + "nature": "external", + "parameter_type": "real", + "value": [ + 1.16639e-05, + 0.0 + ] + }, + { + "expression": null, + "lhablock": "SMINPUTS", + "lhacode": [ + 3 + ], + "name": "aS", + "nature": "external", + "parameter_type": "real", + "value": [ + 0.118, + 0.0 + ] + }, + { + "expression": null, + "lhablock": null, + "lhacode": null, + "name": "ZERO", + "nature": "internal", + "parameter_type": "real", + "value": [ + 0.0, + 0.0 + ] + }, + { + "expression": "UFO::{}::aEWM1^(-1)", + "lhablock": null, + "lhacode": null, + "name": "aEW", + "nature": "internal", + "parameter_type": "real", + "value": [ + 0.00754677225305414, + 0.0 + ] + }, + { + "expression": "2*UFO::{}::aS^(1/2)*𝜋^(1/2)", + "lhablock": null, + "lhacode": null, + "name": "G", + "nature": "internal", + "parameter_type": "real", + "value": [ + 1.2177157847767195, + 0.0 + ] + }, + { + "expression": "2*UFO::{}::aEW^(1/2)*𝜋^(1/2)", + "lhablock": null, + "lhacode": null, + "name": "ee", + "nature": "internal", + "parameter_type": "real", + "value": [ + 0.3079537904849374, + 0.0 + ] + } + ], + "particles": [ + { + "antiname": "scalar_0", + "antitexname": "\\phi_0", + "charge": 0.0, + "color": 1, + "ghost_number": 0, + "goldstoneboson": false, + "lepton_number": 0, + "mass": "ZERO", + "name": "scalar_0", + "pdg_code": 2000, + "propagating": true, + "propagator": "scalar_0_propFeynman", + "spin": 1, + "texname": "\\phi_0", + "width": "ZERO", + "y_charge": 0 + }, + { + "antiname": "scalar_1", + "antitexname": "\\phi_1", + "charge": 0.0, + "color": 1, + "ghost_number": 0, + "goldstoneboson": false, + "lepton_number": 0, + "mass": "mass_scalar_1", + "name": "scalar_1", + "pdg_code": 2001, + "propagating": true, + "propagator": "scalar_1_propFeynman", + "spin": 1, + "texname": "\\phi_1", + "width": "width_scalar_1", + "y_charge": 0 + }, + { + "antiname": "scalar_2", + "antitexname": "\\phi_2", + "charge": 0.0, + "color": 1, + "ghost_number": 0, + "goldstoneboson": false, + "lepton_number": 0, + "mass": "mass_scalar_2", + "name": "scalar_2", + "pdg_code": 2002, + "propagating": true, + "propagator": "scalar_2_propFeynman", + "spin": 1, + "texname": "\\phi_2", + "width": "width_scalar_2", + "y_charge": 0 + }, + { + "antiname": "graviton", + "antitexname": "\\tilde{g}", + "charge": 0.0, + "color": 1, + "ghost_number": 0, + "goldstoneboson": false, + "lepton_number": 0, + "mass": "ZERO", + "name": "graviton", + "pdg_code": 1337, + "propagating": true, + "propagator": "graviton_propFeynman", + "spin": 5, + "texname": "\\tilde{g}", + "width": "ZERO", + "y_charge": 0 + } + ], + "propagators": [ + { + "denominator": "(UFO::{}::P(1))^2", + "name": "scalar_0_propFeynman", + "numerator": "1𝑖", + "particle": "scalar_0" + }, + { + "denominator": "(UFO::{}::P(1))^2+-1*UFO::{}::mass_scalar_1^2", + "name": "scalar_1_propFeynman", + "numerator": "1𝑖", + "particle": "scalar_1" + }, + { + "denominator": "(UFO::{}::P(1))^2+-1*UFO::{}::mass_scalar_2^2", + "name": "scalar_2_propFeynman", + "numerator": "1𝑖", + "particle": "scalar_2" + }, + { + "denominator": "(UFO::{}::P(1))^2", + "name": "graviton_propFeynman", + "numerator": "((-2+UFO::{}::dim)^(-1)*-2*UFO::{}::Metric(1001,2001)*UFO::{}::Metric(1002,2002)+UFO::{}::Metric(1001,1002)*UFO::{}::Metric(2001,2002)+UFO::{}::Metric(1001,2002)*UFO::{}::Metric(2001,1002))*1𝑖/2", + "particle": "graviton" + } + ], + "restriction": null, + "vertex_rules": [ + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SSTmpart0", + "SST" + ] + ], + "lorentz_structures": [ + "SSTmpart", + "SST" + ], + "name": "V_S0S0Gr", + "particles": [ + "scalar_0", + "scalar_0", + "graviton" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SSTTmpart0", + "SSTT" + ] + ], + "lorentz_structures": [ + "SSTTmpart", + "SSTT" + ], + "name": "V_S0S0GrGr", + "particles": [ + "scalar_0", + "scalar_0", + "graviton", + "graviton" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SSTTTmpart0", + "SSTTT" + ] + ], + "lorentz_structures": [ + "SSTTTmpart", + "SSTTT" + ], + "name": "V_S0S0GrGrGr", + "particles": [ + "scalar_0", + "scalar_0", + "graviton", + "graviton", + "graviton" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SSTmpart1", + "SST" + ] + ], + "lorentz_structures": [ + "SSTmpart", + "SST" + ], + "name": "V_S1S1Gr", + "particles": [ + "scalar_1", + "scalar_1", + "graviton" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SSTTmpart1", + "SSTT" + ] + ], + "lorentz_structures": [ + "SSTTmpart", + "SSTT" + ], + "name": "V_S1S1GrGr", + "particles": [ + "scalar_1", + "scalar_1", + "graviton", + "graviton" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SSTTTmpart1", + "SSTTT" + ] + ], + "lorentz_structures": [ + "SSTTTmpart", + "SSTTT" + ], + "name": "V_S1S1GrGrGr", + "particles": [ + "scalar_1", + "scalar_1", + "graviton", + "graviton", + "graviton" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SSTmpart2", + "SST" + ] + ], + "lorentz_structures": [ + "SSTmpart", + "SST" + ], + "name": "V_S2S2Gr", + "particles": [ + "scalar_2", + "scalar_2", + "graviton" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SSTTmpart2", + "SSTT" + ] + ], + "lorentz_structures": [ + "SSTTmpart", + "SSTT" + ], + "name": "V_S2S2GrGr", + "particles": [ + "scalar_2", + "scalar_2", + "graviton", + "graviton" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SSTTTmpart2", + "SSTTT" + ] + ], + "lorentz_structures": [ + "SSTTTmpart", + "SSTTT" + ], + "name": "V_S2S2GrGrGr", + "particles": [ + "scalar_2", + "scalar_2", + "graviton", + "graviton", + "graviton" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "TTT" + ] + ], + "lorentz_structures": [ + "TTT" + ], + "name": "V_GrGrGr", + "particles": [ + "graviton", + "graviton", + "graviton" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "TTTT" + ] + ], + "lorentz_structures": [ + "TTTT" + ], + "name": "V_GrGrGrGr", + "particles": [ + "graviton", + "graviton", + "graviton", + "graviton" + ] + } + ] +} \ No newline at end of file diff --git a/pyamplicol/src/pyamplicol/assets/models/json/scalars/restrict_default.json b/pyamplicol/src/pyamplicol/assets/models/json/scalars/restrict_default.json new file mode 100644 index 0000000000..29f8a712ac --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/models/json/scalars/restrict_default.json @@ -0,0 +1,34 @@ +{ + "Gf": [ + 1.16639e-05, + 0.0 + ], + "aEWM1": [ + 132.507, + 0.0 + ], + "aS": [ + 0.118, + 0.0 + ], + "lam": [ + 1.0, + 0.0 + ], + "mass_scalar_1": [ + 1.0, + 0.0 + ], + "mass_scalar_2": [ + 2.0, + 0.0 + ], + "width_scalar_1": [ + 0.0, + 0.0 + ], + "width_scalar_2": [ + 0.0, + 0.0 + ] +} \ No newline at end of file diff --git a/pyamplicol/src/pyamplicol/assets/models/json/scalars/scalars.json b/pyamplicol/src/pyamplicol/assets/models/json/scalars/scalars.json new file mode 100644 index 0000000000..ea57faa89b --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/models/json/scalars/scalars.json @@ -0,0 +1,6972 @@ +{ + "couplings": [ + { + "expression": "1𝑖*UFO::{}::lam", + "name": "SCALAR_COUPLING", + "orders": [ + [ + "QCD", + 1 + ] + ], + "value": [ + 0.0, + 1.0 + ] + } + ], + "form_factors": [], + "functions": [ + { + "arguments": [ + "z" + ], + "expression": null, + "name": "complexconjugate" + }, + { + "arguments": [ + "z" + ], + "expression": null, + "name": "re" + }, + { + "arguments": [ + "z" + ], + "expression": null, + "name": "im" + }, + { + "arguments": [ + "z" + ], + "expression": null, + "name": "sec" + }, + { + "arguments": [ + "z" + ], + "expression": null, + "name": "asec" + }, + { + "arguments": [ + "z" + ], + "expression": null, + "name": "csc" + }, + { + "arguments": [ + "z" + ], + "expression": null, + "name": "acsc" + } + ], + "lorentz_structures": [ + { + "name": "SCALAR_3_LORENTZ_STRUCTURE", + "spins": [ + 1, + 1, + 1 + ], + "structure": "1" + }, + { + "name": "SCALAR_4_LORENTZ_STRUCTURE", + "spins": [ + 1, + 1, + 1, + 1 + ], + "structure": "1" + }, + { + "name": "SCALAR_5_LORENTZ_STRUCTURE", + "spins": [ + 1, + 1, + 1, + 1, + 1 + ], + "structure": "1" + }, + { + "name": "SCALAR_6_LORENTZ_STRUCTURE", + "spins": [ + 1, + 1, + 1, + 1, + 1, + 1 + ], + "structure": "1" + }, + { + "name": "SCALAR_7_LORENTZ_STRUCTURE", + "spins": [ + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ], + "structure": "1" + }, + { + "name": "SCALAR_8_LORENTZ_STRUCTURE", + "spins": [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ], + "structure": "1" + }, + { + "name": "SCALAR_9_LORENTZ_STRUCTURE", + "spins": [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ], + "structure": "1" + }, + { + "name": "SCALAR_10_LORENTZ_STRUCTURE", + "spins": [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ], + "structure": "1" + } + ], + "name": "scalars", + "orders": [ + { + "expansion_order": 99, + "hierarchy": 1, + "name": "QCD" + }, + { + "expansion_order": 99, + "hierarchy": 2, + "name": "QED" + } + ], + "parameters": [ + { + "expression": null, + "lhablock": "SCALARS", + "lhacode": [ + 1337 + ], + "name": "lam", + "nature": "external", + "parameter_type": "real", + "value": [ + 1.0, + 0.0 + ] + }, + { + "expression": null, + "lhablock": "SCALARS", + "lhacode": [ + 2002 + ], + "name": "mass_scalar_1", + "nature": "external", + "parameter_type": "real", + "value": [ + 1.0, + 0.0 + ] + }, + { + "expression": null, + "lhablock": "SCALARS", + "lhacode": [ + 3002 + ], + "name": "width_scalar_1", + "nature": "external", + "parameter_type": "real", + "value": [ + 0.0, + 0.0 + ] + }, + { + "expression": null, + "lhablock": "SCALARS", + "lhacode": [ + 2003 + ], + "name": "mass_scalar_2", + "nature": "external", + "parameter_type": "real", + "value": [ + 2.0, + 0.0 + ] + }, + { + "expression": null, + "lhablock": "SCALARS", + "lhacode": [ + 3003 + ], + "name": "width_scalar_2", + "nature": "external", + "parameter_type": "real", + "value": [ + 0.0, + 0.0 + ] + }, + { + "expression": null, + "lhablock": "SMINPUTS", + "lhacode": [ + 1 + ], + "name": "aEWM1", + "nature": "external", + "parameter_type": "real", + "value": [ + 132.50698, + 0.0 + ] + }, + { + "expression": null, + "lhablock": "SMINPUTS", + "lhacode": [ + 2 + ], + "name": "Gf", + "nature": "external", + "parameter_type": "real", + "value": [ + 1.16639e-05, + 0.0 + ] + }, + { + "expression": null, + "lhablock": "SMINPUTS", + "lhacode": [ + 3 + ], + "name": "aS", + "nature": "external", + "parameter_type": "real", + "value": [ + 0.118, + 0.0 + ] + }, + { + "expression": null, + "lhablock": null, + "lhacode": null, + "name": "ZERO", + "nature": "internal", + "parameter_type": "real", + "value": [ + 0.0, + 0.0 + ] + }, + { + "expression": "UFO::{}::aEWM1^(-1)", + "lhablock": null, + "lhacode": null, + "name": "aEW", + "nature": "internal", + "parameter_type": "real", + "value": [ + 0.00754677225305414, + 0.0 + ] + }, + { + "expression": "2*UFO::{}::aS^(1/2)*𝜋^(1/2)", + "lhablock": null, + "lhacode": null, + "name": "G", + "nature": "internal", + "parameter_type": "real", + "value": [ + 1.2177157847767195, + 0.0 + ] + }, + { + "expression": "2*UFO::{}::aEW^(1/2)*𝜋^(1/2)", + "lhablock": null, + "lhacode": null, + "name": "ee", + "nature": "internal", + "parameter_type": "real", + "value": [ + 0.3079537904849374, + 0.0 + ] + } + ], + "particles": [ + { + "antiname": "scalar_0", + "antitexname": "\\phi_0", + "charge": 0.0, + "color": 1, + "ghost_number": 0, + "goldstoneboson": false, + "lepton_number": 0, + "mass": "ZERO", + "name": "scalar_0", + "pdg_code": 1000, + "propagating": true, + "propagator": "scalar_0_propFeynman", + "spin": 1, + "texname": "\\phi_0", + "width": "ZERO", + "y_charge": 0 + }, + { + "antiname": "scalar_1", + "antitexname": "\\phi_1", + "charge": 0.0, + "color": 1, + "ghost_number": 0, + "goldstoneboson": false, + "lepton_number": 0, + "mass": "mass_scalar_1", + "name": "scalar_1", + "pdg_code": 1001, + "propagating": true, + "propagator": "scalar_1_propFeynman", + "spin": 1, + "texname": "\\phi_1", + "width": "width_scalar_1", + "y_charge": 0 + }, + { + "antiname": "scalar_2", + "antitexname": "\\phi_2", + "charge": 0.0, + "color": 1, + "ghost_number": 0, + "goldstoneboson": false, + "lepton_number": 0, + "mass": "mass_scalar_2", + "name": "scalar_2", + "pdg_code": 1002, + "propagating": true, + "propagator": "scalar_2_propFeynman", + "spin": 1, + "texname": "\\phi_2", + "width": "width_scalar_2", + "y_charge": 0 + } + ], + "propagators": [ + { + "denominator": "(UFO::{}::P(1))^2", + "name": "scalar_0_propFeynman", + "numerator": "1𝑖", + "particle": "scalar_0" + }, + { + "denominator": "(UFO::{}::P(1))^2+-1*UFO::{}::mass_scalar_1^2", + "name": "scalar_1_propFeynman", + "numerator": "1𝑖", + "particle": "scalar_1" + }, + { + "denominator": "(UFO::{}::P(1))^2+-1*UFO::{}::mass_scalar_2^2", + "name": "scalar_2_propFeynman", + "numerator": "1𝑖", + "particle": "scalar_2" + } + ], + "restriction": null, + "vertex_rules": [ + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_3_LORENTZ_STRUCTURE" + ], + "name": "V_3_SCALAR_000", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_3_LORENTZ_STRUCTURE" + ], + "name": "V_3_SCALAR_001", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_1" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_3_LORENTZ_STRUCTURE" + ], + "name": "V_3_SCALAR_002", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_3_LORENTZ_STRUCTURE" + ], + "name": "V_3_SCALAR_011", + "particles": [ + "scalar_0", + "scalar_1", + "scalar_1" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_3_LORENTZ_STRUCTURE" + ], + "name": "V_3_SCALAR_012", + "particles": [ + "scalar_0", + "scalar_1", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_3_LORENTZ_STRUCTURE" + ], + "name": "V_3_SCALAR_022", + "particles": [ + "scalar_0", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_3_LORENTZ_STRUCTURE" + ], + "name": "V_3_SCALAR_111", + "particles": [ + "scalar_1", + "scalar_1", + "scalar_1" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_3_LORENTZ_STRUCTURE" + ], + "name": "V_3_SCALAR_112", + "particles": [ + "scalar_1", + "scalar_1", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_3_LORENTZ_STRUCTURE" + ], + "name": "V_3_SCALAR_122", + "particles": [ + "scalar_1", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_3_LORENTZ_STRUCTURE" + ], + "name": "V_3_SCALAR_222", + "particles": [ + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_4_LORENTZ_STRUCTURE" + ], + "name": "V_4_SCALAR_0000", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_4_LORENTZ_STRUCTURE" + ], + "name": "V_4_SCALAR_0001", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_1" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_4_LORENTZ_STRUCTURE" + ], + "name": "V_4_SCALAR_0002", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_4_LORENTZ_STRUCTURE" + ], + "name": "V_4_SCALAR_0011", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_1" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_4_LORENTZ_STRUCTURE" + ], + "name": "V_4_SCALAR_0012", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_4_LORENTZ_STRUCTURE" + ], + "name": "V_4_SCALAR_0022", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_4_LORENTZ_STRUCTURE" + ], + "name": "V_4_SCALAR_0111", + "particles": [ + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_1" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_4_LORENTZ_STRUCTURE" + ], + "name": "V_4_SCALAR_0112", + "particles": [ + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_4_LORENTZ_STRUCTURE" + ], + "name": "V_4_SCALAR_0122", + "particles": [ + "scalar_0", + "scalar_1", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_4_LORENTZ_STRUCTURE" + ], + "name": "V_4_SCALAR_0222", + "particles": [ + "scalar_0", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_4_LORENTZ_STRUCTURE" + ], + "name": "V_4_SCALAR_1111", + "particles": [ + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_4_LORENTZ_STRUCTURE" + ], + "name": "V_4_SCALAR_1112", + "particles": [ + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_4_LORENTZ_STRUCTURE" + ], + "name": "V_4_SCALAR_1122", + "particles": [ + "scalar_1", + "scalar_1", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_4_LORENTZ_STRUCTURE" + ], + "name": "V_4_SCALAR_1222", + "particles": [ + "scalar_1", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_4_LORENTZ_STRUCTURE" + ], + "name": "V_4_SCALAR_2222", + "particles": [ + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_5_LORENTZ_STRUCTURE" + ], + "name": "V_5_SCALAR_00000", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_5_LORENTZ_STRUCTURE" + ], + "name": "V_5_SCALAR_00001", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_1" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_5_LORENTZ_STRUCTURE" + ], + "name": "V_5_SCALAR_00002", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_5_LORENTZ_STRUCTURE" + ], + "name": "V_5_SCALAR_00011", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_1" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_5_LORENTZ_STRUCTURE" + ], + "name": "V_5_SCALAR_00012", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_5_LORENTZ_STRUCTURE" + ], + "name": "V_5_SCALAR_00022", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_5_LORENTZ_STRUCTURE" + ], + "name": "V_5_SCALAR_00111", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_1" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_5_LORENTZ_STRUCTURE" + ], + "name": "V_5_SCALAR_00112", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_5_LORENTZ_STRUCTURE" + ], + "name": "V_5_SCALAR_00122", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_5_LORENTZ_STRUCTURE" + ], + "name": "V_5_SCALAR_00222", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_5_LORENTZ_STRUCTURE" + ], + "name": "V_5_SCALAR_01111", + "particles": [ + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_5_LORENTZ_STRUCTURE" + ], + "name": "V_5_SCALAR_01112", + "particles": [ + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_5_LORENTZ_STRUCTURE" + ], + "name": "V_5_SCALAR_01122", + "particles": [ + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_5_LORENTZ_STRUCTURE" + ], + "name": "V_5_SCALAR_01222", + "particles": [ + "scalar_0", + "scalar_1", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_5_LORENTZ_STRUCTURE" + ], + "name": "V_5_SCALAR_02222", + "particles": [ + "scalar_0", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_5_LORENTZ_STRUCTURE" + ], + "name": "V_5_SCALAR_11111", + "particles": [ + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_5_LORENTZ_STRUCTURE" + ], + "name": "V_5_SCALAR_11112", + "particles": [ + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_5_LORENTZ_STRUCTURE" + ], + "name": "V_5_SCALAR_11122", + "particles": [ + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_5_LORENTZ_STRUCTURE" + ], + "name": "V_5_SCALAR_11222", + "particles": [ + "scalar_1", + "scalar_1", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_5_LORENTZ_STRUCTURE" + ], + "name": "V_5_SCALAR_12222", + "particles": [ + "scalar_1", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_5_LORENTZ_STRUCTURE" + ], + "name": "V_5_SCALAR_22222", + "particles": [ + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_6_LORENTZ_STRUCTURE" + ], + "name": "V_6_SCALAR_000000", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_6_LORENTZ_STRUCTURE" + ], + "name": "V_6_SCALAR_000001", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_1" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_6_LORENTZ_STRUCTURE" + ], + "name": "V_6_SCALAR_000002", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_6_LORENTZ_STRUCTURE" + ], + "name": "V_6_SCALAR_000011", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_1" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_6_LORENTZ_STRUCTURE" + ], + "name": "V_6_SCALAR_000012", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_6_LORENTZ_STRUCTURE" + ], + "name": "V_6_SCALAR_000022", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_6_LORENTZ_STRUCTURE" + ], + "name": "V_6_SCALAR_000111", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_1" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_6_LORENTZ_STRUCTURE" + ], + "name": "V_6_SCALAR_000112", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_6_LORENTZ_STRUCTURE" + ], + "name": "V_6_SCALAR_000122", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_6_LORENTZ_STRUCTURE" + ], + "name": "V_6_SCALAR_000222", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_6_LORENTZ_STRUCTURE" + ], + "name": "V_6_SCALAR_001111", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_6_LORENTZ_STRUCTURE" + ], + "name": "V_6_SCALAR_001112", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_6_LORENTZ_STRUCTURE" + ], + "name": "V_6_SCALAR_001122", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_6_LORENTZ_STRUCTURE" + ], + "name": "V_6_SCALAR_001222", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_6_LORENTZ_STRUCTURE" + ], + "name": "V_6_SCALAR_002222", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_6_LORENTZ_STRUCTURE" + ], + "name": "V_6_SCALAR_011111", + "particles": [ + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_6_LORENTZ_STRUCTURE" + ], + "name": "V_6_SCALAR_011112", + "particles": [ + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_6_LORENTZ_STRUCTURE" + ], + "name": "V_6_SCALAR_011122", + "particles": [ + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_6_LORENTZ_STRUCTURE" + ], + "name": "V_6_SCALAR_011222", + "particles": [ + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_6_LORENTZ_STRUCTURE" + ], + "name": "V_6_SCALAR_012222", + "particles": [ + "scalar_0", + "scalar_1", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_6_LORENTZ_STRUCTURE" + ], + "name": "V_6_SCALAR_022222", + "particles": [ + "scalar_0", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_6_LORENTZ_STRUCTURE" + ], + "name": "V_6_SCALAR_111111", + "particles": [ + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_6_LORENTZ_STRUCTURE" + ], + "name": "V_6_SCALAR_111112", + "particles": [ + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_6_LORENTZ_STRUCTURE" + ], + "name": "V_6_SCALAR_111122", + "particles": [ + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_6_LORENTZ_STRUCTURE" + ], + "name": "V_6_SCALAR_111222", + "particles": [ + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_6_LORENTZ_STRUCTURE" + ], + "name": "V_6_SCALAR_112222", + "particles": [ + "scalar_1", + "scalar_1", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_6_LORENTZ_STRUCTURE" + ], + "name": "V_6_SCALAR_122222", + "particles": [ + "scalar_1", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_6_LORENTZ_STRUCTURE" + ], + "name": "V_6_SCALAR_222222", + "particles": [ + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_7_LORENTZ_STRUCTURE" + ], + "name": "V_7_SCALAR_0000000", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_7_LORENTZ_STRUCTURE" + ], + "name": "V_7_SCALAR_0000001", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_1" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_7_LORENTZ_STRUCTURE" + ], + "name": "V_7_SCALAR_0000002", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_7_LORENTZ_STRUCTURE" + ], + "name": "V_7_SCALAR_0000011", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_1" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_7_LORENTZ_STRUCTURE" + ], + "name": "V_7_SCALAR_0000012", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_7_LORENTZ_STRUCTURE" + ], + "name": "V_7_SCALAR_0000022", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_7_LORENTZ_STRUCTURE" + ], + "name": "V_7_SCALAR_0000111", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_1" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_7_LORENTZ_STRUCTURE" + ], + "name": "V_7_SCALAR_0000112", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_7_LORENTZ_STRUCTURE" + ], + "name": "V_7_SCALAR_0000122", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_7_LORENTZ_STRUCTURE" + ], + "name": "V_7_SCALAR_0000222", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_7_LORENTZ_STRUCTURE" + ], + "name": "V_7_SCALAR_0001111", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_7_LORENTZ_STRUCTURE" + ], + "name": "V_7_SCALAR_0001112", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_7_LORENTZ_STRUCTURE" + ], + "name": "V_7_SCALAR_0001122", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_7_LORENTZ_STRUCTURE" + ], + "name": "V_7_SCALAR_0001222", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_7_LORENTZ_STRUCTURE" + ], + "name": "V_7_SCALAR_0002222", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_7_LORENTZ_STRUCTURE" + ], + "name": "V_7_SCALAR_0011111", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_7_LORENTZ_STRUCTURE" + ], + "name": "V_7_SCALAR_0011112", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_7_LORENTZ_STRUCTURE" + ], + "name": "V_7_SCALAR_0011122", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_7_LORENTZ_STRUCTURE" + ], + "name": "V_7_SCALAR_0011222", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_7_LORENTZ_STRUCTURE" + ], + "name": "V_7_SCALAR_0012222", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_7_LORENTZ_STRUCTURE" + ], + "name": "V_7_SCALAR_0022222", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_7_LORENTZ_STRUCTURE" + ], + "name": "V_7_SCALAR_0111111", + "particles": [ + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_7_LORENTZ_STRUCTURE" + ], + "name": "V_7_SCALAR_0111112", + "particles": [ + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_7_LORENTZ_STRUCTURE" + ], + "name": "V_7_SCALAR_0111122", + "particles": [ + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_7_LORENTZ_STRUCTURE" + ], + "name": "V_7_SCALAR_0111222", + "particles": [ + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_7_LORENTZ_STRUCTURE" + ], + "name": "V_7_SCALAR_0112222", + "particles": [ + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_7_LORENTZ_STRUCTURE" + ], + "name": "V_7_SCALAR_0122222", + "particles": [ + "scalar_0", + "scalar_1", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_7_LORENTZ_STRUCTURE" + ], + "name": "V_7_SCALAR_0222222", + "particles": [ + "scalar_0", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_7_LORENTZ_STRUCTURE" + ], + "name": "V_7_SCALAR_1111111", + "particles": [ + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_7_LORENTZ_STRUCTURE" + ], + "name": "V_7_SCALAR_1111112", + "particles": [ + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_7_LORENTZ_STRUCTURE" + ], + "name": "V_7_SCALAR_1111122", + "particles": [ + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_7_LORENTZ_STRUCTURE" + ], + "name": "V_7_SCALAR_1111222", + "particles": [ + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_7_LORENTZ_STRUCTURE" + ], + "name": "V_7_SCALAR_1112222", + "particles": [ + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_7_LORENTZ_STRUCTURE" + ], + "name": "V_7_SCALAR_1122222", + "particles": [ + "scalar_1", + "scalar_1", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_7_LORENTZ_STRUCTURE" + ], + "name": "V_7_SCALAR_1222222", + "particles": [ + "scalar_1", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_7_LORENTZ_STRUCTURE" + ], + "name": "V_7_SCALAR_2222222", + "particles": [ + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_8_LORENTZ_STRUCTURE" + ], + "name": "V_8_SCALAR_00000000", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_8_LORENTZ_STRUCTURE" + ], + "name": "V_8_SCALAR_00000001", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_1" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_8_LORENTZ_STRUCTURE" + ], + "name": "V_8_SCALAR_00000002", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_8_LORENTZ_STRUCTURE" + ], + "name": "V_8_SCALAR_00000011", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_1" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_8_LORENTZ_STRUCTURE" + ], + "name": "V_8_SCALAR_00000012", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_8_LORENTZ_STRUCTURE" + ], + "name": "V_8_SCALAR_00000022", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_8_LORENTZ_STRUCTURE" + ], + "name": "V_8_SCALAR_00000111", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_1" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_8_LORENTZ_STRUCTURE" + ], + "name": "V_8_SCALAR_00000112", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_8_LORENTZ_STRUCTURE" + ], + "name": "V_8_SCALAR_00000122", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_8_LORENTZ_STRUCTURE" + ], + "name": "V_8_SCALAR_00000222", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_8_LORENTZ_STRUCTURE" + ], + "name": "V_8_SCALAR_00001111", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_8_LORENTZ_STRUCTURE" + ], + "name": "V_8_SCALAR_00001112", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_8_LORENTZ_STRUCTURE" + ], + "name": "V_8_SCALAR_00001122", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_8_LORENTZ_STRUCTURE" + ], + "name": "V_8_SCALAR_00001222", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_8_LORENTZ_STRUCTURE" + ], + "name": "V_8_SCALAR_00002222", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_8_LORENTZ_STRUCTURE" + ], + "name": "V_8_SCALAR_00011111", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_8_LORENTZ_STRUCTURE" + ], + "name": "V_8_SCALAR_00011112", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_8_LORENTZ_STRUCTURE" + ], + "name": "V_8_SCALAR_00011122", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_8_LORENTZ_STRUCTURE" + ], + "name": "V_8_SCALAR_00011222", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_8_LORENTZ_STRUCTURE" + ], + "name": "V_8_SCALAR_00012222", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_8_LORENTZ_STRUCTURE" + ], + "name": "V_8_SCALAR_00022222", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_8_LORENTZ_STRUCTURE" + ], + "name": "V_8_SCALAR_00111111", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_8_LORENTZ_STRUCTURE" + ], + "name": "V_8_SCALAR_00111112", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_8_LORENTZ_STRUCTURE" + ], + "name": "V_8_SCALAR_00111122", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_8_LORENTZ_STRUCTURE" + ], + "name": "V_8_SCALAR_00111222", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_8_LORENTZ_STRUCTURE" + ], + "name": "V_8_SCALAR_00112222", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_8_LORENTZ_STRUCTURE" + ], + "name": "V_8_SCALAR_00122222", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_8_LORENTZ_STRUCTURE" + ], + "name": "V_8_SCALAR_00222222", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_8_LORENTZ_STRUCTURE" + ], + "name": "V_8_SCALAR_01111111", + "particles": [ + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_8_LORENTZ_STRUCTURE" + ], + "name": "V_8_SCALAR_01111112", + "particles": [ + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_8_LORENTZ_STRUCTURE" + ], + "name": "V_8_SCALAR_01111122", + "particles": [ + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_8_LORENTZ_STRUCTURE" + ], + "name": "V_8_SCALAR_01111222", + "particles": [ + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_8_LORENTZ_STRUCTURE" + ], + "name": "V_8_SCALAR_01112222", + "particles": [ + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_8_LORENTZ_STRUCTURE" + ], + "name": "V_8_SCALAR_01122222", + "particles": [ + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_8_LORENTZ_STRUCTURE" + ], + "name": "V_8_SCALAR_01222222", + "particles": [ + "scalar_0", + "scalar_1", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_8_LORENTZ_STRUCTURE" + ], + "name": "V_8_SCALAR_02222222", + "particles": [ + "scalar_0", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_8_LORENTZ_STRUCTURE" + ], + "name": "V_8_SCALAR_11111111", + "particles": [ + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_8_LORENTZ_STRUCTURE" + ], + "name": "V_8_SCALAR_11111112", + "particles": [ + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_8_LORENTZ_STRUCTURE" + ], + "name": "V_8_SCALAR_11111122", + "particles": [ + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_8_LORENTZ_STRUCTURE" + ], + "name": "V_8_SCALAR_11111222", + "particles": [ + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_8_LORENTZ_STRUCTURE" + ], + "name": "V_8_SCALAR_11112222", + "particles": [ + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_8_LORENTZ_STRUCTURE" + ], + "name": "V_8_SCALAR_11122222", + "particles": [ + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_8_LORENTZ_STRUCTURE" + ], + "name": "V_8_SCALAR_11222222", + "particles": [ + "scalar_1", + "scalar_1", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_8_LORENTZ_STRUCTURE" + ], + "name": "V_8_SCALAR_12222222", + "particles": [ + "scalar_1", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_8_LORENTZ_STRUCTURE" + ], + "name": "V_8_SCALAR_22222222", + "particles": [ + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_9_LORENTZ_STRUCTURE" + ], + "name": "V_9_SCALAR_000000000", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_9_LORENTZ_STRUCTURE" + ], + "name": "V_9_SCALAR_000000001", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_1" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_9_LORENTZ_STRUCTURE" + ], + "name": "V_9_SCALAR_000000002", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_9_LORENTZ_STRUCTURE" + ], + "name": "V_9_SCALAR_000000011", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_1" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_9_LORENTZ_STRUCTURE" + ], + "name": "V_9_SCALAR_000000012", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_9_LORENTZ_STRUCTURE" + ], + "name": "V_9_SCALAR_000000022", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_9_LORENTZ_STRUCTURE" + ], + "name": "V_9_SCALAR_000000111", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_1" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_9_LORENTZ_STRUCTURE" + ], + "name": "V_9_SCALAR_000000112", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_9_LORENTZ_STRUCTURE" + ], + "name": "V_9_SCALAR_000000122", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_9_LORENTZ_STRUCTURE" + ], + "name": "V_9_SCALAR_000000222", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_9_LORENTZ_STRUCTURE" + ], + "name": "V_9_SCALAR_000001111", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_9_LORENTZ_STRUCTURE" + ], + "name": "V_9_SCALAR_000001112", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_9_LORENTZ_STRUCTURE" + ], + "name": "V_9_SCALAR_000001122", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_9_LORENTZ_STRUCTURE" + ], + "name": "V_9_SCALAR_000001222", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_9_LORENTZ_STRUCTURE" + ], + "name": "V_9_SCALAR_000002222", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_9_LORENTZ_STRUCTURE" + ], + "name": "V_9_SCALAR_000011111", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_9_LORENTZ_STRUCTURE" + ], + "name": "V_9_SCALAR_000011112", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_9_LORENTZ_STRUCTURE" + ], + "name": "V_9_SCALAR_000011122", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_9_LORENTZ_STRUCTURE" + ], + "name": "V_9_SCALAR_000011222", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_9_LORENTZ_STRUCTURE" + ], + "name": "V_9_SCALAR_000012222", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_9_LORENTZ_STRUCTURE" + ], + "name": "V_9_SCALAR_000022222", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_9_LORENTZ_STRUCTURE" + ], + "name": "V_9_SCALAR_000111111", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_9_LORENTZ_STRUCTURE" + ], + "name": "V_9_SCALAR_000111112", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_9_LORENTZ_STRUCTURE" + ], + "name": "V_9_SCALAR_000111122", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_9_LORENTZ_STRUCTURE" + ], + "name": "V_9_SCALAR_000111222", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_9_LORENTZ_STRUCTURE" + ], + "name": "V_9_SCALAR_000112222", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_9_LORENTZ_STRUCTURE" + ], + "name": "V_9_SCALAR_000122222", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_9_LORENTZ_STRUCTURE" + ], + "name": "V_9_SCALAR_000222222", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_9_LORENTZ_STRUCTURE" + ], + "name": "V_9_SCALAR_001111111", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_9_LORENTZ_STRUCTURE" + ], + "name": "V_9_SCALAR_001111112", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_9_LORENTZ_STRUCTURE" + ], + "name": "V_9_SCALAR_001111122", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_9_LORENTZ_STRUCTURE" + ], + "name": "V_9_SCALAR_001111222", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_9_LORENTZ_STRUCTURE" + ], + "name": "V_9_SCALAR_001112222", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_9_LORENTZ_STRUCTURE" + ], + "name": "V_9_SCALAR_001122222", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_9_LORENTZ_STRUCTURE" + ], + "name": "V_9_SCALAR_001222222", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_9_LORENTZ_STRUCTURE" + ], + "name": "V_9_SCALAR_002222222", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_9_LORENTZ_STRUCTURE" + ], + "name": "V_9_SCALAR_011111111", + "particles": [ + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_9_LORENTZ_STRUCTURE" + ], + "name": "V_9_SCALAR_011111112", + "particles": [ + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_9_LORENTZ_STRUCTURE" + ], + "name": "V_9_SCALAR_011111122", + "particles": [ + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_9_LORENTZ_STRUCTURE" + ], + "name": "V_9_SCALAR_011111222", + "particles": [ + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_9_LORENTZ_STRUCTURE" + ], + "name": "V_9_SCALAR_011112222", + "particles": [ + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_9_LORENTZ_STRUCTURE" + ], + "name": "V_9_SCALAR_011122222", + "particles": [ + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_9_LORENTZ_STRUCTURE" + ], + "name": "V_9_SCALAR_011222222", + "particles": [ + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_9_LORENTZ_STRUCTURE" + ], + "name": "V_9_SCALAR_012222222", + "particles": [ + "scalar_0", + "scalar_1", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_9_LORENTZ_STRUCTURE" + ], + "name": "V_9_SCALAR_022222222", + "particles": [ + "scalar_0", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_9_LORENTZ_STRUCTURE" + ], + "name": "V_9_SCALAR_111111111", + "particles": [ + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_9_LORENTZ_STRUCTURE" + ], + "name": "V_9_SCALAR_111111112", + "particles": [ + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_9_LORENTZ_STRUCTURE" + ], + "name": "V_9_SCALAR_111111122", + "particles": [ + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_9_LORENTZ_STRUCTURE" + ], + "name": "V_9_SCALAR_111111222", + "particles": [ + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_9_LORENTZ_STRUCTURE" + ], + "name": "V_9_SCALAR_111112222", + "particles": [ + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_9_LORENTZ_STRUCTURE" + ], + "name": "V_9_SCALAR_111122222", + "particles": [ + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_9_LORENTZ_STRUCTURE" + ], + "name": "V_9_SCALAR_111222222", + "particles": [ + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_9_LORENTZ_STRUCTURE" + ], + "name": "V_9_SCALAR_112222222", + "particles": [ + "scalar_1", + "scalar_1", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_9_LORENTZ_STRUCTURE" + ], + "name": "V_9_SCALAR_122222222", + "particles": [ + "scalar_1", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_9_LORENTZ_STRUCTURE" + ], + "name": "V_9_SCALAR_222222222", + "particles": [ + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_10_LORENTZ_STRUCTURE" + ], + "name": "V_10_SCALAR_0000000000", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_10_LORENTZ_STRUCTURE" + ], + "name": "V_10_SCALAR_0000000001", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_1" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_10_LORENTZ_STRUCTURE" + ], + "name": "V_10_SCALAR_0000000002", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_10_LORENTZ_STRUCTURE" + ], + "name": "V_10_SCALAR_0000000011", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_1" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_10_LORENTZ_STRUCTURE" + ], + "name": "V_10_SCALAR_0000000012", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_10_LORENTZ_STRUCTURE" + ], + "name": "V_10_SCALAR_0000000022", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_10_LORENTZ_STRUCTURE" + ], + "name": "V_10_SCALAR_0000000111", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_1" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_10_LORENTZ_STRUCTURE" + ], + "name": "V_10_SCALAR_0000000112", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_10_LORENTZ_STRUCTURE" + ], + "name": "V_10_SCALAR_0000000122", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_10_LORENTZ_STRUCTURE" + ], + "name": "V_10_SCALAR_0000000222", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_10_LORENTZ_STRUCTURE" + ], + "name": "V_10_SCALAR_0000001111", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_10_LORENTZ_STRUCTURE" + ], + "name": "V_10_SCALAR_0000001112", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_10_LORENTZ_STRUCTURE" + ], + "name": "V_10_SCALAR_0000001122", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_10_LORENTZ_STRUCTURE" + ], + "name": "V_10_SCALAR_0000001222", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_10_LORENTZ_STRUCTURE" + ], + "name": "V_10_SCALAR_0000002222", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_10_LORENTZ_STRUCTURE" + ], + "name": "V_10_SCALAR_0000011111", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_10_LORENTZ_STRUCTURE" + ], + "name": "V_10_SCALAR_0000011112", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_10_LORENTZ_STRUCTURE" + ], + "name": "V_10_SCALAR_0000011122", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_10_LORENTZ_STRUCTURE" + ], + "name": "V_10_SCALAR_0000011222", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_10_LORENTZ_STRUCTURE" + ], + "name": "V_10_SCALAR_0000012222", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_10_LORENTZ_STRUCTURE" + ], + "name": "V_10_SCALAR_0000022222", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_10_LORENTZ_STRUCTURE" + ], + "name": "V_10_SCALAR_0000111111", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_10_LORENTZ_STRUCTURE" + ], + "name": "V_10_SCALAR_0000111112", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_10_LORENTZ_STRUCTURE" + ], + "name": "V_10_SCALAR_0000111122", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_10_LORENTZ_STRUCTURE" + ], + "name": "V_10_SCALAR_0000111222", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_10_LORENTZ_STRUCTURE" + ], + "name": "V_10_SCALAR_0000112222", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_10_LORENTZ_STRUCTURE" + ], + "name": "V_10_SCALAR_0000122222", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_10_LORENTZ_STRUCTURE" + ], + "name": "V_10_SCALAR_0000222222", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_10_LORENTZ_STRUCTURE" + ], + "name": "V_10_SCALAR_0001111111", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_10_LORENTZ_STRUCTURE" + ], + "name": "V_10_SCALAR_0001111112", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_10_LORENTZ_STRUCTURE" + ], + "name": "V_10_SCALAR_0001111122", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_10_LORENTZ_STRUCTURE" + ], + "name": "V_10_SCALAR_0001111222", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_10_LORENTZ_STRUCTURE" + ], + "name": "V_10_SCALAR_0001112222", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_10_LORENTZ_STRUCTURE" + ], + "name": "V_10_SCALAR_0001122222", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_10_LORENTZ_STRUCTURE" + ], + "name": "V_10_SCALAR_0001222222", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_10_LORENTZ_STRUCTURE" + ], + "name": "V_10_SCALAR_0002222222", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_10_LORENTZ_STRUCTURE" + ], + "name": "V_10_SCALAR_0011111111", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_10_LORENTZ_STRUCTURE" + ], + "name": "V_10_SCALAR_0011111112", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_10_LORENTZ_STRUCTURE" + ], + "name": "V_10_SCALAR_0011111122", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_10_LORENTZ_STRUCTURE" + ], + "name": "V_10_SCALAR_0011111222", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_10_LORENTZ_STRUCTURE" + ], + "name": "V_10_SCALAR_0011112222", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_10_LORENTZ_STRUCTURE" + ], + "name": "V_10_SCALAR_0011122222", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_10_LORENTZ_STRUCTURE" + ], + "name": "V_10_SCALAR_0011222222", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_10_LORENTZ_STRUCTURE" + ], + "name": "V_10_SCALAR_0012222222", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_1", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_10_LORENTZ_STRUCTURE" + ], + "name": "V_10_SCALAR_0022222222", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_10_LORENTZ_STRUCTURE" + ], + "name": "V_10_SCALAR_0111111111", + "particles": [ + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_10_LORENTZ_STRUCTURE" + ], + "name": "V_10_SCALAR_0111111112", + "particles": [ + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_10_LORENTZ_STRUCTURE" + ], + "name": "V_10_SCALAR_0111111122", + "particles": [ + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_10_LORENTZ_STRUCTURE" + ], + "name": "V_10_SCALAR_0111111222", + "particles": [ + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_10_LORENTZ_STRUCTURE" + ], + "name": "V_10_SCALAR_0111112222", + "particles": [ + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_10_LORENTZ_STRUCTURE" + ], + "name": "V_10_SCALAR_0111122222", + "particles": [ + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_10_LORENTZ_STRUCTURE" + ], + "name": "V_10_SCALAR_0111222222", + "particles": [ + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_10_LORENTZ_STRUCTURE" + ], + "name": "V_10_SCALAR_0112222222", + "particles": [ + "scalar_0", + "scalar_1", + "scalar_1", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_10_LORENTZ_STRUCTURE" + ], + "name": "V_10_SCALAR_0122222222", + "particles": [ + "scalar_0", + "scalar_1", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_10_LORENTZ_STRUCTURE" + ], + "name": "V_10_SCALAR_0222222222", + "particles": [ + "scalar_0", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_10_LORENTZ_STRUCTURE" + ], + "name": "V_10_SCALAR_1111111111", + "particles": [ + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_10_LORENTZ_STRUCTURE" + ], + "name": "V_10_SCALAR_1111111112", + "particles": [ + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_10_LORENTZ_STRUCTURE" + ], + "name": "V_10_SCALAR_1111111122", + "particles": [ + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_10_LORENTZ_STRUCTURE" + ], + "name": "V_10_SCALAR_1111111222", + "particles": [ + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_10_LORENTZ_STRUCTURE" + ], + "name": "V_10_SCALAR_1111112222", + "particles": [ + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_10_LORENTZ_STRUCTURE" + ], + "name": "V_10_SCALAR_1111122222", + "particles": [ + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_10_LORENTZ_STRUCTURE" + ], + "name": "V_10_SCALAR_1111222222", + "particles": [ + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_10_LORENTZ_STRUCTURE" + ], + "name": "V_10_SCALAR_1112222222", + "particles": [ + "scalar_1", + "scalar_1", + "scalar_1", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_10_LORENTZ_STRUCTURE" + ], + "name": "V_10_SCALAR_1122222222", + "particles": [ + "scalar_1", + "scalar_1", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_10_LORENTZ_STRUCTURE" + ], + "name": "V_10_SCALAR_1222222222", + "particles": [ + "scalar_1", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_10_LORENTZ_STRUCTURE" + ], + "name": "V_10_SCALAR_2222222222", + "particles": [ + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2", + "scalar_2" + ] + } + ] +} \ No newline at end of file diff --git a/pyamplicol/src/pyamplicol/assets/models/json/scalars/scalars_2p_3p.json b/pyamplicol/src/pyamplicol/assets/models/json/scalars/scalars_2p_3p.json new file mode 100644 index 0000000000..c21b4f8ffd --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/models/json/scalars/scalars_2p_3p.json @@ -0,0 +1,591 @@ +{ + "couplings": [ + { + "expression": "1𝑖*UFO::{}::lam", + "name": "SCALAR_COUPLING", + "orders": [ + [ + "QCD", + 1 + ] + ], + "value": [ + 0.0, + 1.0 + ] + } + ], + "form_factors": [], + "functions": [], + "lorentz_structures": [ + { + "name": "SCALAR_2_LORENTZ_STRUCTURE", + "spins": [ + 1, + 1 + ], + "structure": "1" + }, + { + "name": "SCALAR_3_LORENTZ_STRUCTURE", + "spins": [ + 1, + 1, + 1 + ], + "structure": "1" + } + ], + "name": "scalars", + "orders": [ + { + "expansion_order": 99, + "hierarchy": 1, + "name": "QCD" + }, + { + "expansion_order": 99, + "hierarchy": 2, + "name": "QED" + } + ], + "parameters": [ + { + "expression": null, + "lhablock": null, + "lhacode": null, + "name": "ZERO", + "nature": "internal", + "parameter_type": "real", + "value": [ + 0.0, + 0.0 + ] + }, + { + "expression": null, + "lhablock": "SCALARS", + "lhacode": [ + 1337 + ], + "name": "lam", + "nature": "external", + "parameter_type": "real", + "value": [ + 1.0, + 0.0 + ] + }, + { + "expression": null, + "lhablock": "SCALARS", + "lhacode": [ + 2002 + ], + "name": "mass_scalar_1", + "nature": "external", + "parameter_type": "real", + "value": [ + 1.0, + 0.0 + ] + }, + { + "expression": "UFO::{}::ZERO", + "lhablock": "SCALARS", + "lhacode": [ + 3002 + ], + "name": "width_scalar_1", + "nature": "internal", + "parameter_type": "real", + "value": [ + 0.0, + 0.0 + ] + }, + { + "expression": null, + "lhablock": "SCALARS", + "lhacode": [ + 2003 + ], + "name": "mass_scalar_2", + "nature": "external", + "parameter_type": "real", + "value": [ + 2.0, + 0.0 + ] + }, + { + "expression": "UFO::{}::ZERO", + "lhablock": "SCALARS", + "lhacode": [ + 3003 + ], + "name": "width_scalar_2", + "nature": "internal", + "parameter_type": "real", + "value": [ + 0.0, + 0.0 + ] + }, + { + "expression": null, + "lhablock": "SMINPUTS", + "lhacode": [ + 1 + ], + "name": "aEWM1", + "nature": "external", + "parameter_type": "real", + "value": [ + 132.507, + 0.0 + ] + }, + { + "expression": null, + "lhablock": "SMINPUTS", + "lhacode": [ + 2 + ], + "name": "Gf", + "nature": "external", + "parameter_type": "real", + "value": [ + 1.16639e-05, + 0.0 + ] + }, + { + "expression": null, + "lhablock": "SMINPUTS", + "lhacode": [ + 3 + ], + "name": "aS", + "nature": "external", + "parameter_type": "real", + "value": [ + 0.118, + 0.0 + ] + }, + { + "expression": "UFO::{}::aEWM1^(-1)", + "lhablock": null, + "lhacode": null, + "name": "aEW", + "nature": "internal", + "parameter_type": "real", + "value": [ + 0.0075467711139788835, + -0.0 + ] + }, + { + "expression": "2*UFO::{}::aS^(1/2)*𝜋^(1/2)", + "lhablock": null, + "lhacode": null, + "name": "G", + "nature": "internal", + "parameter_type": "real", + "value": [ + 1.2177157847767195, + 0.0 + ] + }, + { + "expression": "2*UFO::{}::aEW^(1/2)*𝜋^(1/2)", + "lhablock": null, + "lhacode": null, + "name": "ee", + "nature": "internal", + "parameter_type": "real", + "value": [ + 0.3079537672443688, + 0.0 + ] + } + ], + "particles": [ + { + "antiname": "scalar_0", + "antitexname": "\\phi_0", + "charge": 0.0, + "color": 1, + "ghost_number": 0, + "goldstoneboson": false, + "lepton_number": 0, + "mass": "ZERO", + "name": "scalar_0", + "pdg_code": 1000, + "propagating": true, + "propagator": "scalar_0_propFeynman", + "spin": 1, + "texname": "\\phi_0", + "width": "ZERO", + "y_charge": 0 + }, + { + "antiname": "scalar_1", + "antitexname": "\\phi_1", + "charge": 0.0, + "color": 1, + "ghost_number": 0, + "goldstoneboson": false, + "lepton_number": 0, + "mass": "mass_scalar_1", + "name": "scalar_1", + "pdg_code": 1001, + "propagating": true, + "propagator": "scalar_1_propFeynman", + "spin": 1, + "texname": "\\phi_1", + "width": "width_scalar_1", + "y_charge": 0 + }, + { + "antiname": "scalar_2", + "antitexname": "\\phi_2", + "charge": 0.0, + "color": 1, + "ghost_number": 0, + "goldstoneboson": false, + "lepton_number": 0, + "mass": "mass_scalar_2", + "name": "scalar_2", + "pdg_code": 1002, + "propagating": true, + "propagator": "scalar_2_propFeynman", + "spin": 1, + "texname": "\\phi_2", + "width": "width_scalar_2", + "y_charge": 0 + } + ], + "propagators": [ + { + "denominator": "(UFO::{}::P(UFO::{}::idx(1,1)))^2", + "name": "scalar_0_propFeynman", + "numerator": "1", + "particle": "scalar_0" + }, + { + "denominator": "(UFO::{}::P(UFO::{}::idx(1,1)))^2+-1*UFO::{}::mass_scalar_1^2", + "name": "scalar_1_propFeynman", + "numerator": "1", + "particle": "scalar_1" + }, + { + "denominator": "(UFO::{}::P(UFO::{}::idx(1,1)))^2+-1*UFO::{}::mass_scalar_2^2", + "name": "scalar_2_propFeynman", + "numerator": "1", + "particle": "scalar_2" + } + ], + "restriction": "default", + "vertex_rules": [ + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_2_LORENTZ_STRUCTURE" + ], + "name": "V_2_SCALAR_00", + "particles": [ + "scalar_0", + "scalar_0" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_2_LORENTZ_STRUCTURE" + ], + "name": "V_2_SCALAR_01", + "particles": [ + "scalar_0", + "scalar_1" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_2_LORENTZ_STRUCTURE" + ], + "name": "V_2_SCALAR_02", + "particles": [ + "scalar_0", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_2_LORENTZ_STRUCTURE" + ], + "name": "V_2_SCALAR_11", + "particles": [ + "scalar_1", + "scalar_1" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_2_LORENTZ_STRUCTURE" + ], + "name": "V_2_SCALAR_12", + "particles": [ + "scalar_1", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_2_LORENTZ_STRUCTURE" + ], + "name": "V_2_SCALAR_22", + "particles": [ + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_3_LORENTZ_STRUCTURE" + ], + "name": "V_3_SCALAR_000", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_0" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_3_LORENTZ_STRUCTURE" + ], + "name": "V_3_SCALAR_001", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_1" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_3_LORENTZ_STRUCTURE" + ], + "name": "V_3_SCALAR_002", + "particles": [ + "scalar_0", + "scalar_0", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_3_LORENTZ_STRUCTURE" + ], + "name": "V_3_SCALAR_011", + "particles": [ + "scalar_0", + "scalar_1", + "scalar_1" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_3_LORENTZ_STRUCTURE" + ], + "name": "V_3_SCALAR_012", + "particles": [ + "scalar_0", + "scalar_1", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_3_LORENTZ_STRUCTURE" + ], + "name": "V_3_SCALAR_022", + "particles": [ + "scalar_0", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_3_LORENTZ_STRUCTURE" + ], + "name": "V_3_SCALAR_111", + "particles": [ + "scalar_1", + "scalar_1", + "scalar_1" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_3_LORENTZ_STRUCTURE" + ], + "name": "V_3_SCALAR_112", + "particles": [ + "scalar_1", + "scalar_1", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_3_LORENTZ_STRUCTURE" + ], + "name": "V_3_SCALAR_122", + "particles": [ + "scalar_1", + "scalar_2", + "scalar_2" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "SCALAR_COUPLING" + ] + ], + "lorentz_structures": [ + "SCALAR_3_LORENTZ_STRUCTURE" + ], + "name": "V_3_SCALAR_222", + "particles": [ + "scalar_2", + "scalar_2", + "scalar_2" + ] + } + ] +} \ No newline at end of file diff --git a/pyamplicol/src/pyamplicol/assets/models/json/sm/restrict_c_mass.json b/pyamplicol/src/pyamplicol/assets/models/json/sm/restrict_c_mass.json new file mode 100644 index 0000000000..1caa7684d2 --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/models/json/sm/restrict_c_mass.json @@ -0,0 +1,106 @@ +{ + "AWS": [ + 0.0, + 0.0 + ], + "Gf": [ + 1.16639e-05, + 0.0 + ], + "MB": [ + 4.7, + 0.0 + ], + "MC": [ + 1.55, + 0.0 + ], + "MH": [ + 125.0, + 0.0 + ], + "MM": [ + 0.0, + 0.0 + ], + "MT": [ + 173.0, + 0.0 + ], + "MTA": [ + 1.777, + 0.0 + ], + "MZ": [ + 91.188, + 0.0 + ], + "Me": [ + 0.0, + 0.0 + ], + "WH": [ + 0.00638233934, + 0.0 + ], + "WT": [ + 1.4915, + 0.0 + ], + "WTau": [ + 0.0, + 0.0 + ], + "WW": [ + 2.0476, + 0.0 + ], + "WZ": [ + 2.441404, + 0.0 + ], + "aEWM1": [ + 132.507, + 0.0 + ], + "aS": [ + 0.118, + 0.0 + ], + "etaWS": [ + 0.0, + 0.0 + ], + "lamWS": [ + 0.0, + 0.0 + ], + "rhoWS": [ + 0.0, + 0.0 + ], + "ymb": [ + 4.7, + 0.0 + ], + "ymc": [ + 1.55, + 0.0 + ], + "yme": [ + 0.0, + 0.0 + ], + "ymm": [ + 0.0, + 0.0 + ], + "ymt": [ + 173.0, + 0.0 + ], + "ymtau": [ + 1.777, + 0.0 + ] +} \ No newline at end of file diff --git a/pyamplicol/src/pyamplicol/assets/models/json/sm/restrict_ckm.json b/pyamplicol/src/pyamplicol/assets/models/json/sm/restrict_ckm.json new file mode 100644 index 0000000000..0747393d26 --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/models/json/sm/restrict_ckm.json @@ -0,0 +1,106 @@ +{ + "AWS": [ + 0.808, + 0.0 + ], + "Gf": [ + 1.16639e-05, + 0.0 + ], + "MB": [ + 4.7, + 0.0 + ], + "MC": [ + 0.0, + 0.0 + ], + "MH": [ + 125.0, + 0.0 + ], + "MM": [ + 0.0, + 0.0 + ], + "MT": [ + 173.0, + 0.0 + ], + "MTA": [ + 1.777, + 0.0 + ], + "MZ": [ + 91.188, + 0.0 + ], + "Me": [ + 0.0, + 0.0 + ], + "WH": [ + 0.00638233934, + 0.0 + ], + "WT": [ + 1.4915, + 0.0 + ], + "WTau": [ + 0.0, + 0.0 + ], + "WW": [ + 2.0476, + 0.0 + ], + "WZ": [ + 2.441404, + 0.0 + ], + "aEWM1": [ + 132.507, + 0.0 + ], + "aS": [ + 0.118, + 0.0 + ], + "etaWS": [ + 0.341, + 0.0 + ], + "lamWS": [ + 0.2253, + 0.0 + ], + "rhoWS": [ + 0.132, + 0.0 + ], + "ymb": [ + 4.7, + 0.0 + ], + "ymc": [ + 0.0, + 0.0 + ], + "yme": [ + 0.0, + 0.0 + ], + "ymm": [ + 0.0, + 0.0 + ], + "ymt": [ + 173.0, + 0.0 + ], + "ymtau": [ + 1.777, + 0.0 + ] +} \ No newline at end of file diff --git a/pyamplicol/src/pyamplicol/assets/models/json/sm/restrict_default.json b/pyamplicol/src/pyamplicol/assets/models/json/sm/restrict_default.json new file mode 100644 index 0000000000..91cdfff0c8 --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/models/json/sm/restrict_default.json @@ -0,0 +1,106 @@ +{ + "AWS": [ + 0.0, + 0.0 + ], + "Gf": [ + 1.16639e-05, + 0.0 + ], + "MB": [ + 4.7, + 0.0 + ], + "MC": [ + 0.0, + 0.0 + ], + "MH": [ + 125.0, + 0.0 + ], + "MM": [ + 0.0, + 0.0 + ], + "MT": [ + 173.0, + 0.0 + ], + "MTA": [ + 1.777, + 0.0 + ], + "MZ": [ + 91.188, + 0.0 + ], + "Me": [ + 0.0, + 0.0 + ], + "WH": [ + 0.00638233934, + 0.0 + ], + "WT": [ + 1.4915, + 0.0 + ], + "WTau": [ + 0.0, + 0.0 + ], + "WW": [ + 2.0476, + 0.0 + ], + "WZ": [ + 2.441404, + 0.0 + ], + "aEWM1": [ + 132.507, + 0.0 + ], + "aS": [ + 0.118, + 0.0 + ], + "etaWS": [ + 0.0, + 0.0 + ], + "lamWS": [ + 0.0, + 0.0 + ], + "rhoWS": [ + 0.0, + 0.0 + ], + "ymb": [ + 4.7, + 0.0 + ], + "ymc": [ + 0.0, + 0.0 + ], + "yme": [ + 0.0, + 0.0 + ], + "ymm": [ + 0.0, + 0.0 + ], + "ymt": [ + 173.0, + 0.0 + ], + "ymtau": [ + 1.777, + 0.0 + ] +} \ No newline at end of file diff --git a/pyamplicol/src/pyamplicol/assets/models/json/sm/restrict_lepton_masses.json b/pyamplicol/src/pyamplicol/assets/models/json/sm/restrict_lepton_masses.json new file mode 100644 index 0000000000..fc24994d66 --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/models/json/sm/restrict_lepton_masses.json @@ -0,0 +1,106 @@ +{ + "AWS": [ + 0.0, + 0.0 + ], + "Gf": [ + 1.16639e-05, + 0.0 + ], + "MB": [ + 4.7, + 0.0 + ], + "MC": [ + 0.0, + 0.0 + ], + "MH": [ + 125.0, + 0.0 + ], + "MM": [ + 0.10566, + 0.0 + ], + "MT": [ + 173.0, + 0.0 + ], + "MTA": [ + 1.777, + 0.0 + ], + "MZ": [ + 91.188, + 0.0 + ], + "Me": [ + 0.000511, + 0.0 + ], + "WH": [ + 0.00638233934, + 0.0 + ], + "WT": [ + 1.4915, + 0.0 + ], + "WTau": [ + 2.27e-12, + 0.0 + ], + "WW": [ + 2.0476, + 0.0 + ], + "WZ": [ + 2.441404, + 0.0 + ], + "aEWM1": [ + 132.507, + 0.0 + ], + "aS": [ + 0.118, + 0.0 + ], + "etaWS": [ + 0.0, + 0.0 + ], + "lamWS": [ + 0.0, + 0.0 + ], + "rhoWS": [ + 0.0, + 0.0 + ], + "ymb": [ + 4.7, + 0.0 + ], + "ymc": [ + 0.0, + 0.0 + ], + "yme": [ + 0.000511, + 0.0 + ], + "ymm": [ + 0.10566, + 0.0 + ], + "ymt": [ + 173.0, + 0.0 + ], + "ymtau": [ + 1.777, + 0.0 + ] +} \ No newline at end of file diff --git a/pyamplicol/src/pyamplicol/assets/models/json/sm/restrict_no_b_mass.json b/pyamplicol/src/pyamplicol/assets/models/json/sm/restrict_no_b_mass.json new file mode 100644 index 0000000000..cea4f7f1e0 --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/models/json/sm/restrict_no_b_mass.json @@ -0,0 +1,106 @@ +{ + "AWS": [ + 0.0, + 0.0 + ], + "Gf": [ + 1.16639e-05, + 0.0 + ], + "MB": [ + 0.0, + 0.0 + ], + "MC": [ + 0.0, + 0.0 + ], + "MH": [ + 125.0, + 0.0 + ], + "MM": [ + 0.0, + 0.0 + ], + "MT": [ + 173.0, + 0.0 + ], + "MTA": [ + 1.777, + 0.0 + ], + "MZ": [ + 91.188, + 0.0 + ], + "Me": [ + 0.0, + 0.0 + ], + "WH": [ + 0.00638233934, + 0.0 + ], + "WT": [ + 1.4915, + 0.0 + ], + "WTau": [ + 0.0, + 0.0 + ], + "WW": [ + 2.0476, + 0.0 + ], + "WZ": [ + 2.441404, + 0.0 + ], + "aEWM1": [ + 132.507, + 0.0 + ], + "aS": [ + 0.118, + 0.0 + ], + "etaWS": [ + 0.0, + 0.0 + ], + "lamWS": [ + 0.0, + 0.0 + ], + "rhoWS": [ + 0.0, + 0.0 + ], + "ymb": [ + 0.0, + 0.0 + ], + "ymc": [ + 0.0, + 0.0 + ], + "yme": [ + 0.0, + 0.0 + ], + "ymm": [ + 0.0, + 0.0 + ], + "ymt": [ + 173.0, + 0.0 + ], + "ymtau": [ + 1.777, + 0.0 + ] +} \ No newline at end of file diff --git a/pyamplicol/src/pyamplicol/assets/models/json/sm/restrict_no_masses.json b/pyamplicol/src/pyamplicol/assets/models/json/sm/restrict_no_masses.json new file mode 100644 index 0000000000..2d575cfc72 --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/models/json/sm/restrict_no_masses.json @@ -0,0 +1,106 @@ +{ + "AWS": [ + 0.0, + 0.0 + ], + "Gf": [ + 1.16639e-05, + 0.0 + ], + "MB": [ + 0.0, + 0.0 + ], + "MC": [ + 0.0, + 0.0 + ], + "MH": [ + 125.0, + 0.0 + ], + "MM": [ + 0.0, + 0.0 + ], + "MT": [ + 173.0, + 0.0 + ], + "MTA": [ + 0.0, + 0.0 + ], + "MZ": [ + 91.188, + 0.0 + ], + "Me": [ + 0.0, + 0.0 + ], + "WH": [ + 0.00638233934, + 0.0 + ], + "WT": [ + 1.4915, + 0.0 + ], + "WTau": [ + 0.0, + 0.0 + ], + "WW": [ + 2.0476, + 0.0 + ], + "WZ": [ + 2.441404, + 0.0 + ], + "aEWM1": [ + 132.507, + 0.0 + ], + "aS": [ + 0.118, + 0.0 + ], + "etaWS": [ + 0.0, + 0.0 + ], + "lamWS": [ + 0.0, + 0.0 + ], + "rhoWS": [ + 0.0, + 0.0 + ], + "ymb": [ + 0.0, + 0.0 + ], + "ymc": [ + 0.0, + 0.0 + ], + "yme": [ + 0.0, + 0.0 + ], + "ymm": [ + 0.0, + 0.0 + ], + "ymt": [ + 173.0, + 0.0 + ], + "ymtau": [ + 0.0, + 0.0 + ] +} \ No newline at end of file diff --git a/pyamplicol/src/pyamplicol/assets/models/json/sm/restrict_no_tau_mass.json b/pyamplicol/src/pyamplicol/assets/models/json/sm/restrict_no_tau_mass.json new file mode 100644 index 0000000000..c503b11fa7 --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/models/json/sm/restrict_no_tau_mass.json @@ -0,0 +1,106 @@ +{ + "AWS": [ + 0.0, + 0.0 + ], + "Gf": [ + 1.16639e-05, + 0.0 + ], + "MB": [ + 4.7, + 0.0 + ], + "MC": [ + 0.0, + 0.0 + ], + "MH": [ + 125.0, + 0.0 + ], + "MM": [ + 0.0, + 0.0 + ], + "MT": [ + 173.0, + 0.0 + ], + "MTA": [ + 0.0, + 0.0 + ], + "MZ": [ + 91.188, + 0.0 + ], + "Me": [ + 0.0, + 0.0 + ], + "WH": [ + 0.00638233934, + 0.0 + ], + "WT": [ + 1.4915, + 0.0 + ], + "WTau": [ + 0.0, + 0.0 + ], + "WW": [ + 2.0476, + 0.0 + ], + "WZ": [ + 2.441404, + 0.0 + ], + "aEWM1": [ + 132.507, + 0.0 + ], + "aS": [ + 0.118, + 0.0 + ], + "etaWS": [ + 0.0, + 0.0 + ], + "lamWS": [ + 0.0, + 0.0 + ], + "rhoWS": [ + 0.0, + 0.0 + ], + "ymb": [ + 4.7, + 0.0 + ], + "ymc": [ + 0.0, + 0.0 + ], + "yme": [ + 0.0, + 0.0 + ], + "ymm": [ + 0.0, + 0.0 + ], + "ymt": [ + 173.0, + 0.0 + ], + "ymtau": [ + 0.0, + 0.0 + ] +} \ No newline at end of file diff --git a/pyamplicol/src/pyamplicol/assets/models/json/sm/restrict_no_widths.json b/pyamplicol/src/pyamplicol/assets/models/json/sm/restrict_no_widths.json new file mode 100644 index 0000000000..50ddc241c1 --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/models/json/sm/restrict_no_widths.json @@ -0,0 +1,106 @@ +{ + "AWS": [ + 0.0, + 0.0 + ], + "Gf": [ + 1.16639e-05, + 0.0 + ], + "MB": [ + 4.7, + 0.0 + ], + "MC": [ + 0.0, + 0.0 + ], + "MH": [ + 120.0, + 0.0 + ], + "MM": [ + 0.0, + 0.0 + ], + "MT": [ + 173.0, + 0.0 + ], + "MTA": [ + 1.777, + 0.0 + ], + "MZ": [ + 91.188, + 0.0 + ], + "Me": [ + 0.0, + 0.0 + ], + "WH": [ + 0.0, + 0.0 + ], + "WT": [ + 0.0, + 0.0 + ], + "WTau": [ + 0.0, + 0.0 + ], + "WW": [ + 0.0, + 0.0 + ], + "WZ": [ + 0.0, + 0.0 + ], + "aEWM1": [ + 132.507, + 0.0 + ], + "aS": [ + 0.118, + 0.0 + ], + "etaWS": [ + 0.0, + 0.0 + ], + "lamWS": [ + 0.0, + 0.0 + ], + "rhoWS": [ + 0.0, + 0.0 + ], + "ymb": [ + 4.2, + 0.0 + ], + "ymc": [ + 0.0, + 0.0 + ], + "yme": [ + 0.0, + 0.0 + ], + "ymm": [ + 0.0, + 0.0 + ], + "ymt": [ + 164.5, + 0.0 + ], + "ymtau": [ + 1.777, + 0.0 + ] +} \ No newline at end of file diff --git a/pyamplicol/src/pyamplicol/assets/models/json/sm/restrict_zeromass_ckm.json b/pyamplicol/src/pyamplicol/assets/models/json/sm/restrict_zeromass_ckm.json new file mode 100644 index 0000000000..cbe68c5f43 --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/models/json/sm/restrict_zeromass_ckm.json @@ -0,0 +1,106 @@ +{ + "AWS": [ + 0.808, + 0.0 + ], + "Gf": [ + 1.16639e-05, + 0.0 + ], + "MB": [ + 0.0, + 0.0 + ], + "MC": [ + 0.0, + 0.0 + ], + "MH": [ + 125.0, + 0.0 + ], + "MM": [ + 0.0, + 0.0 + ], + "MT": [ + 173.0, + 0.0 + ], + "MTA": [ + 0.0, + 0.0 + ], + "MZ": [ + 91.188, + 0.0 + ], + "Me": [ + 0.0, + 0.0 + ], + "WH": [ + 0.00638233934, + 0.0 + ], + "WT": [ + 1.4915, + 0.0 + ], + "WTau": [ + 0.0, + 0.0 + ], + "WW": [ + 2.0476, + 0.0 + ], + "WZ": [ + 2.441404, + 0.0 + ], + "aEWM1": [ + 132.507, + 0.0 + ], + "aS": [ + 0.118, + 0.0 + ], + "etaWS": [ + 0.341, + 0.0 + ], + "lamWS": [ + 0.2253, + 0.0 + ], + "rhoWS": [ + 0.132, + 0.0 + ], + "ymb": [ + 0.0, + 0.0 + ], + "ymc": [ + 0.0, + 0.0 + ], + "yme": [ + 0.0, + 0.0 + ], + "ymm": [ + 0.0, + 0.0 + ], + "ymt": [ + 173.0, + 0.0 + ], + "ymtau": [ + 0.0, + 0.0 + ] +} \ No newline at end of file diff --git a/pyamplicol/src/pyamplicol/assets/models/json/sm/sm.json b/pyamplicol/src/pyamplicol/assets/models/json/sm/sm.json new file mode 100644 index 0000000000..618095a67d --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/models/json/sm/sm.json @@ -0,0 +1,6721 @@ +{ + "couplings": [ + { + "expression": "-1𝑖/3*UFO::{}::ee", + "name": "GC_1", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.0, + -0.10265126349497912 + ] + }, + { + "expression": "2𝑖/3*UFO::{}::ee", + "name": "GC_2", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.0, + 0.20530252698995824 + ] + }, + { + "expression": "-1𝑖*UFO::{}::ee", + "name": "GC_3", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.0, + -0.3079537904849374 + ] + }, + { + "expression": "1𝑖*UFO::{}::ee", + "name": "GC_4", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.0, + 0.3079537904849374 + ] + }, + { + "expression": "1𝑖*UFO::{}::ee^2", + "name": "GC_5", + "orders": [ + [ + "QED", + 2 + ] + ], + "value": [ + 0.0, + 0.0948355370740407 + ] + }, + { + "expression": "2𝑖*UFO::{}::ee^2", + "name": "GC_6", + "orders": [ + [ + "QED", + 2 + ] + ], + "value": [ + 0.0, + 0.1896710741480814 + ] + }, + { + "expression": "-1/2*UFO::{}::cw^(-1)*UFO::{}::ee^2", + "name": "GC_7", + "orders": [ + [ + "QED", + 2 + ] + ], + "value": [ + -0.053767535985328165, + 0.0 + ] + }, + { + "expression": "1𝑖/2*UFO::{}::cw^(-1)*UFO::{}::ee^2", + "name": "GC_8", + "orders": [ + [ + "QED", + 2 + ] + ], + "value": [ + 0.0, + 0.053767535985328165 + ] + }, + { + "expression": "1/2*UFO::{}::cw^(-1)*UFO::{}::ee^2", + "name": "GC_9", + "orders": [ + [ + "QED", + 2 + ] + ], + "value": [ + 0.053767535985328165, + 0.0 + ] + }, + { + "expression": "-1*UFO::{}::G", + "name": "GC_10", + "orders": [ + [ + "QCD", + 1 + ] + ], + "value": [ + -1.2177157847767195, + 0.0 + ] + }, + { + "expression": "1𝑖*UFO::{}::G", + "name": "GC_11", + "orders": [ + [ + "QCD", + 1 + ] + ], + "value": [ + 0.0, + 1.2177157847767195 + ] + }, + { + "expression": "1𝑖*UFO::{}::G^2", + "name": "GC_12", + "orders": [ + [ + "QCD", + 2 + ] + ], + "value": [ + 0.0, + 1.4828317324943818 + ] + }, + { + "expression": "UFO::{}::I1x31", + "name": "GC_13", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 2.9424734175077786e-05, + 7.601389661895094e-05 + ] + }, + { + "expression": "UFO::{}::I1x32", + "name": "GC_14", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.0009894125736418036, + 0.0 + ] + }, + { + "expression": "UFO::{}::I1x33", + "name": "GC_15", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.024123686777011728, + 0.0 + ] + }, + { + "expression": "-1*UFO::{}::I2x12", + "name": "GC_16", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.0016434606240936052, + 0.0 + ] + }, + { + "expression": "-1*UFO::{}::I2x13", + "name": "GC_17", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + -0.007578355147768138, + -0.0029772109509089116 + ] + }, + { + "expression": "-1*UFO::{}::I2x22", + "name": "GC_18", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + -0.007109407543268448, + 0.0 + ] + }, + { + "expression": "-1*UFO::{}::I2x23", + "name": "GC_19", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.0387519924676373, + 0.0 + ] + }, + { + "expression": "-1*UFO::{}::I2x32", + "name": "GC_20", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + -0.00029917951631549774, + 0.0 + ] + }, + { + "expression": "-1*UFO::{}::I2x33", + "name": "GC_21", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + -0.9448443987662926, + 0.0 + ] + }, + { + "expression": "UFO::{}::I3x21", + "name": "GC_22", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + -0.0016434606240936052, + 0.0 + ] + }, + { + "expression": "UFO::{}::I3x22", + "name": "GC_23", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.007109407543268448, + 0.0 + ] + }, + { + "expression": "UFO::{}::I3x23", + "name": "GC_24", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.00029917951631549774, + 0.0 + ] + }, + { + "expression": "UFO::{}::I3x31", + "name": "GC_25", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.007578355147768138, + -0.0029772109509089116 + ] + }, + { + "expression": "UFO::{}::I3x32", + "name": "GC_26", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + -0.0387519924676373, + 0.0 + ] + }, + { + "expression": "UFO::{}::I3x33", + "name": "GC_27", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.9448443987662926, + 0.0 + ] + }, + { + "expression": "-1*UFO::{}::I4x13", + "name": "GC_28", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + -2.9424734175077786e-05, + 7.601389661895094e-05 + ] + }, + { + "expression": "-1*UFO::{}::I4x23", + "name": "GC_29", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + -0.0009894125736418036, + 0.0 + ] + }, + { + "expression": "-1*UFO::{}::I4x33", + "name": "GC_30", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + -0.024123686777011728, + 0.0 + ] + }, + { + "expression": "-2𝑖*UFO::{}::lam", + "name": "GC_31", + "orders": [ + [ + "QED", + 2 + ] + ], + "value": [ + 0.0, + -0.25773821203380554 + ] + }, + { + "expression": "-4𝑖*UFO::{}::lam", + "name": "GC_32", + "orders": [ + [ + "QED", + 2 + ] + ], + "value": [ + 0.0, + -0.5154764240676111 + ] + }, + { + "expression": "-6𝑖*UFO::{}::lam", + "name": "GC_33", + "orders": [ + [ + "QED", + 2 + ] + ], + "value": [ + 0.0, + -0.7732146361014166 + ] + }, + { + "expression": "1𝑖/2*UFO::{}::ee^2*UFO::{}::sw^(-2)", + "name": "GC_34", + "orders": [ + [ + "QED", + 2 + ] + ], + "value": [ + 0.0, + 0.2133566177614792 + ] + }, + { + "expression": "-1𝑖*UFO::{}::ee^2*UFO::{}::sw^(-2)", + "name": "GC_35", + "orders": [ + [ + "QED", + 2 + ] + ], + "value": [ + 0.0, + -0.4267132355229584 + ] + }, + { + "expression": "1𝑖*UFO::{}::cw^2*UFO::{}::ee^2*UFO::{}::sw^(-2)", + "name": "GC_36", + "orders": [ + [ + "QED", + 2 + ] + ], + "value": [ + 0.0, + 0.3318776984489177 + ] + }, + { + "expression": "-1/2*UFO::{}::ee*UFO::{}::sw^(-1)", + "name": "GC_37", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + -0.326616455312251, + 0.0 + ] + }, + { + "expression": "-1𝑖/2*UFO::{}::ee*UFO::{}::sw^(-1)", + "name": "GC_38", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.0, + -0.326616455312251 + ] + }, + { + "expression": "1𝑖/2*UFO::{}::ee*UFO::{}::sw^(-1)", + "name": "GC_39", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.0, + 0.326616455312251 + ] + }, + { + "expression": "(1/2)^(1/2)*1𝑖*UFO::{}::ee*UFO::{}::sw^(-1)", + "name": "GC_40", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.0, + 0.46190542079681124 + ] + }, + { + "expression": "(1/2)^(1/2)*1𝑖*UFO::{}::CKM1x1*UFO::{}::ee*UFO::{}::sw^(-1)", + "name": "GC_41", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.0, + 0.4501822404312442 + ] + }, + { + "expression": "(1/2)^(1/2)*1𝑖*UFO::{}::CKM1x2*UFO::{}::ee*UFO::{}::sw^(-1)", + "name": "GC_42", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.0, + 0.10406729130552157 + ] + }, + { + "expression": "(1/2)^(1/2)*1𝑖*UFO::{}::CKM1x3*UFO::{}::ee*UFO::{}::sw^(-1)", + "name": "GC_43", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.001455467036557634, + 0.0005634065947965036 + ] + }, + { + "expression": "(1/2)^(1/2)*1𝑖*UFO::{}::CKM2x1*UFO::{}::ee*UFO::{}::sw^(-1)", + "name": "GC_44", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.0, + -0.10406729130552157 + ] + }, + { + "expression": "(1/2)^(1/2)*1𝑖*UFO::{}::CKM2x2*UFO::{}::ee*UFO::{}::sw^(-1)", + "name": "GC_45", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.0, + 0.4501822404312442 + ] + }, + { + "expression": "(1/2)^(1/2)*1𝑖*UFO::{}::CKM2x3*UFO::{}::ee*UFO::{}::sw^(-1)", + "name": "GC_46", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.0, + 0.01894465947075628 + ] + }, + { + "expression": "(1/2)^(1/2)*1𝑖*UFO::{}::CKM3x1*UFO::{}::ee*UFO::{}::sw^(-1)", + "name": "GC_47", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.001455467036557634, + 0.003704825183964887 + ] + }, + { + "expression": "(1/2)^(1/2)*1𝑖*UFO::{}::CKM3x2*UFO::{}::ee*UFO::{}::sw^(-1)", + "name": "GC_48", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.0, + -0.01894465947075628 + ] + }, + { + "expression": "(1/2)^(1/2)*1𝑖*UFO::{}::CKM3x3*UFO::{}::ee*UFO::{}::sw^(-1)", + "name": "GC_49", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.0, + 0.46190542079681124 + ] + }, + { + "expression": "-1𝑖/2*UFO::{}::cw*UFO::{}::ee*UFO::{}::sw^(-1)", + "name": "GC_50", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.0, + -0.2880441365697789 + ] + }, + { + "expression": "1𝑖/2*UFO::{}::cw*UFO::{}::ee*UFO::{}::sw^(-1)", + "name": "GC_51", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.0, + 0.2880441365697789 + ] + }, + { + "expression": "-1𝑖*UFO::{}::cw*UFO::{}::ee*UFO::{}::sw^(-1)", + "name": "GC_52", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.0, + -0.5760882731395578 + ] + }, + { + "expression": "1𝑖*UFO::{}::cw*UFO::{}::ee*UFO::{}::sw^(-1)", + "name": "GC_53", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.0, + 0.5760882731395578 + ] + }, + { + "expression": "-1/2*UFO::{}::ee^2*UFO::{}::sw^(-1)", + "name": "GC_54", + "orders": [ + [ + "QED", + 2 + ] + ], + "value": [ + -0.10058277544816184, + 0.0 + ] + }, + { + "expression": "-1𝑖/2*UFO::{}::ee^2*UFO::{}::sw^(-1)", + "name": "GC_55", + "orders": [ + [ + "QED", + 2 + ] + ], + "value": [ + 0.0, + -0.10058277544816184 + ] + }, + { + "expression": "1/2*UFO::{}::ee^2*UFO::{}::sw^(-1)", + "name": "GC_56", + "orders": [ + [ + "QED", + 2 + ] + ], + "value": [ + 0.10058277544816184, + 0.0 + ] + }, + { + "expression": "-2𝑖*UFO::{}::cw*UFO::{}::ee^2*UFO::{}::sw^(-1)", + "name": "GC_57", + "orders": [ + [ + "QED", + 2 + ] + ], + "value": [ + 0.0, + -0.35481713473449755 + ] + }, + { + "expression": "-1𝑖/6*UFO::{}::cw^(-1)*UFO::{}::ee*UFO::{}::sw", + "name": "GC_58", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.0, + -0.027436633555365665 + ] + }, + { + "expression": "1𝑖/2*UFO::{}::cw^(-1)*UFO::{}::ee*UFO::{}::sw", + "name": "GC_59", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.0, + 0.082309900666097 + ] + }, + { + "expression": "-1/2*UFO::{}::cw*UFO::{}::ee*UFO::{}::sw^(-1)+-1/2*UFO::{}::cw^(-1)*UFO::{}::ee*UFO::{}::sw", + "name": "GC_60", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + -0.3703540372358759, + 0.0 + ] + }, + { + "expression": "-1𝑖/2*UFO::{}::cw*UFO::{}::ee*UFO::{}::sw^(-1)+1𝑖/2*UFO::{}::cw^(-1)*UFO::{}::ee*UFO::{}::sw", + "name": "GC_61", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.0, + -0.2057342359036819 + ] + }, + { + "expression": "1𝑖/2*UFO::{}::cw*UFO::{}::ee*UFO::{}::sw^(-1)+1𝑖/2*UFO::{}::cw^(-1)*UFO::{}::ee*UFO::{}::sw", + "name": "GC_62", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.0, + 0.3703540372358759 + ] + }, + { + "expression": "-1𝑖*UFO::{}::cw^(-1)*UFO::{}::ee^2*UFO::{}::sw+1𝑖*UFO::{}::cw*UFO::{}::ee^2*UFO::{}::sw^(-1)", + "name": "GC_63", + "orders": [ + [ + "QED", + 2 + ] + ], + "value": [ + 0.0, + 0.12671327555812228 + ] + }, + { + "expression": "-1𝑖*UFO::{}::ee^2+1𝑖/2*UFO::{}::cw^(-2)*UFO::{}::ee^2*UFO::{}::sw^2+1𝑖/2*UFO::{}::cw^2*UFO::{}::ee^2*UFO::{}::sw^(-2)", + "name": "GC_64", + "orders": [ + [ + "QED", + 2 + ] + ], + "value": [ + 0.0, + 0.08465315164574365 + ] + }, + { + "expression": "1𝑖*UFO::{}::ee^2+1𝑖/2*UFO::{}::cw^(-2)*UFO::{}::ee^2*UFO::{}::sw^2+1𝑖/2*UFO::{}::cw^2*UFO::{}::ee^2*UFO::{}::sw^(-2)", + "name": "GC_65", + "orders": [ + [ + "QED", + 2 + ] + ], + "value": [ + 0.0, + 0.27432422579382504 + ] + }, + { + "expression": "-1/2*UFO::{}::cw^(-1)*UFO::{}::ee^2*UFO::{}::vev", + "name": "GC_66", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + -13.23855980624142, + 0.0 + ] + }, + { + "expression": "1/2*UFO::{}::cw^(-1)*UFO::{}::ee^2*UFO::{}::vev", + "name": "GC_67", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 13.23855980624142, + 0.0 + ] + }, + { + "expression": "-2𝑖*UFO::{}::lam*UFO::{}::vev", + "name": "GC_68", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.0, + -63.459905160882585 + ] + }, + { + "expression": "-6𝑖*UFO::{}::lam*UFO::{}::vev", + "name": "GC_69", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.0, + -190.37971548264773 + ] + }, + { + "expression": "-1/4*UFO::{}::ee^2*UFO::{}::sw^(-2)*UFO::{}::vev", + "name": "GC_70", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + -26.266168725525, + 0.0 + ] + }, + { + "expression": "-1𝑖/4*UFO::{}::ee^2*UFO::{}::sw^(-2)*UFO::{}::vev", + "name": "GC_71", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.0, + -26.266168725525 + ] + }, + { + "expression": "1𝑖/2*UFO::{}::ee^2*UFO::{}::sw^(-2)*UFO::{}::vev", + "name": "GC_72", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.0, + 52.53233745105 + ] + }, + { + "expression": "1/4*UFO::{}::ee^2*UFO::{}::sw^(-2)*UFO::{}::vev", + "name": "GC_73", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 26.266168725525, + 0.0 + ] + }, + { + "expression": "-1/2*UFO::{}::ee^2*UFO::{}::sw^(-1)*UFO::{}::vev", + "name": "GC_74", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + -24.76533588244763, + 0.0 + ] + }, + { + "expression": "1/2*UFO::{}::ee^2*UFO::{}::sw^(-1)*UFO::{}::vev", + "name": "GC_75", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 24.76533588244763, + 0.0 + ] + }, + { + "expression": "-1/4*UFO::{}::cw*UFO::{}::ee^2*UFO::{}::sw^(-2)*UFO::{}::vev+-1/4*UFO::{}::cw^(-1)*UFO::{}::ee^2*UFO::{}::vev", + "name": "GC_76", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + -29.78350132701354, + 0.0 + ] + }, + { + "expression": "-1/4*UFO::{}::cw*UFO::{}::ee^2*UFO::{}::sw^(-2)*UFO::{}::vev+1/4*UFO::{}::cw^(-1)*UFO::{}::ee^2*UFO::{}::vev", + "name": "GC_77", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + -16.54494152077212, + 0.0 + ] + }, + { + "expression": "-1/4*UFO::{}::cw^(-1)*UFO::{}::ee^2*UFO::{}::vev+1/4*UFO::{}::cw*UFO::{}::ee^2*UFO::{}::sw^(-2)*UFO::{}::vev", + "name": "GC_78", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 16.54494152077212, + 0.0 + ] + }, + { + "expression": "1/4*UFO::{}::cw*UFO::{}::ee^2*UFO::{}::sw^(-2)*UFO::{}::vev+1/4*UFO::{}::cw^(-1)*UFO::{}::ee^2*UFO::{}::vev", + "name": "GC_79", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 29.78350132701354, + 0.0 + ] + }, + { + "expression": "-1𝑖/2*UFO::{}::ee^2*UFO::{}::vev+-1𝑖/4*UFO::{}::cw^(-2)*UFO::{}::ee^2*UFO::{}::sw^2*UFO::{}::vev+-1𝑖/4*UFO::{}::cw^2*UFO::{}::ee^2*UFO::{}::sw^(-2)*UFO::{}::vev", + "name": "GC_80", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.0, + -33.77184394746505 + ] + }, + { + "expression": "1𝑖*UFO::{}::ee^2*UFO::{}::vev+1𝑖/2*UFO::{}::cw^(-2)*UFO::{}::ee^2*UFO::{}::sw^2*UFO::{}::vev+1𝑖/2*UFO::{}::cw^2*UFO::{}::ee^2*UFO::{}::sw^(-2)*UFO::{}::vev", + "name": "GC_81", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.0, + 67.5436878949301 + ] + }, + { + "expression": "(1/2)^(1/2)*-1*UFO::{}::yb", + "name": "GC_82", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + -0.01705802250724524, + 0.0 + ] + }, + { + "expression": "(1/2)^(1/2)*-1𝑖*UFO::{}::yb", + "name": "GC_83", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.0, + -0.01705802250724524 + ] + }, + { + "expression": "(1/2)^(1/2)*-1𝑖*UFO::{}::yc", + "name": "GC_84", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.0, + -0.005158021091476537 + ] + }, + { + "expression": "(1/2)^(1/2)*UFO::{}::yc", + "name": "GC_85", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.005158021091476537, + 0.0 + ] + }, + { + "expression": "-1*UFO::{}::ye", + "name": "GC_86", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + -2.93504855786976e-06, + 0.0 + ] + }, + { + "expression": "UFO::{}::ye", + "name": "GC_87", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 2.93504855786976e-06, + 0.0 + ] + }, + { + "expression": "(1/2)^(1/2)*-1*UFO::{}::ye", + "name": "GC_88", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + -2.0753927383815043e-06, + 0.0 + ] + }, + { + "expression": "(1/2)^(1/2)*-1𝑖*UFO::{}::ye", + "name": "GC_89", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.0, + -2.0753927383815043e-06 + ] + }, + { + "expression": "-1*UFO::{}::ym", + "name": "GC_90", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + -0.0006068830344902521, + 0.0 + ] + }, + { + "expression": "UFO::{}::ym", + "name": "GC_91", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.0006068830344902521, + 0.0 + ] + }, + { + "expression": "(1/2)^(1/2)*-1*UFO::{}::ym", + "name": "GC_92", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + -0.0004291311090751267, + 0.0 + ] + }, + { + "expression": "(1/2)^(1/2)*-1𝑖*UFO::{}::ym", + "name": "GC_93", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.0, + -0.0004291311090751267 + ] + }, + { + "expression": "(1/2)^(1/2)*-1𝑖*UFO::{}::yt", + "name": "GC_94", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.0, + -0.6681058815337719 + ] + }, + { + "expression": "(1/2)^(1/2)*UFO::{}::yt", + "name": "GC_95", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.6681058815337719, + 0.0 + ] + }, + { + "expression": "-1*UFO::{}::ytau", + "name": "GC_96", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + -0.010206617000654722, + 0.0 + ] + }, + { + "expression": "UFO::{}::ytau", + "name": "GC_97", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.010206617000654722, + 0.0 + ] + }, + { + "expression": "(1/2)^(1/2)*-1*UFO::{}::ytau", + "name": "GC_98", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + -0.007217168094136855, + 0.0 + ] + }, + { + "expression": "(1/2)^(1/2)*-1𝑖*UFO::{}::ytau", + "name": "GC_99", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.0, + -0.007217168094136855 + ] + }, + { + "expression": "(1/2)^(1/2)*1𝑖*UFO::{}::ee*UFO::{}::sw^(-1)*conj(UFO::{}::CKM1x1)", + "name": "GC_100", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.0, + 0.4501822404312442 + ] + }, + { + "expression": "(1/2)^(1/2)*1𝑖*UFO::{}::ee*UFO::{}::sw^(-1)*conj(UFO::{}::CKM1x2)", + "name": "GC_101", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.0, + 0.10406729130552157 + ] + }, + { + "expression": "(1/2)^(1/2)*1𝑖*UFO::{}::ee*UFO::{}::sw^(-1)*conj(UFO::{}::CKM1x3)", + "name": "GC_102", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + -0.001455467036557634, + 0.0005634065947965036 + ] + }, + { + "expression": "(1/2)^(1/2)*1𝑖*UFO::{}::ee*UFO::{}::sw^(-1)*conj(UFO::{}::CKM2x1)", + "name": "GC_103", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.0, + -0.10406729130552157 + ] + }, + { + "expression": "(1/2)^(1/2)*1𝑖*UFO::{}::ee*UFO::{}::sw^(-1)*conj(UFO::{}::CKM2x2)", + "name": "GC_104", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.0, + 0.4501822404312442 + ] + }, + { + "expression": "(1/2)^(1/2)*1𝑖*UFO::{}::ee*UFO::{}::sw^(-1)*conj(UFO::{}::CKM2x3)", + "name": "GC_105", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.0, + 0.01894465947075628 + ] + }, + { + "expression": "(1/2)^(1/2)*1𝑖*UFO::{}::ee*UFO::{}::sw^(-1)*conj(UFO::{}::CKM3x1)", + "name": "GC_106", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + -0.001455467036557634, + 0.003704825183964887 + ] + }, + { + "expression": "(1/2)^(1/2)*1𝑖*UFO::{}::ee*UFO::{}::sw^(-1)*conj(UFO::{}::CKM3x2)", + "name": "GC_107", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.0, + -0.01894465947075628 + ] + }, + { + "expression": "(1/2)^(1/2)*1𝑖*UFO::{}::ee*UFO::{}::sw^(-1)*conj(UFO::{}::CKM3x3)", + "name": "GC_108", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.0, + 0.46190542079681124 + ] + } + ], + "form_factors": [], + "functions": [ + { + "arguments": [ + "z" + ], + "expression": null, + "name": "complexconjugate" + }, + { + "arguments": [ + "z" + ], + "expression": null, + "name": "re" + }, + { + "arguments": [ + "z" + ], + "expression": null, + "name": "im" + }, + { + "arguments": [ + "z" + ], + "expression": null, + "name": "sec" + }, + { + "arguments": [ + "z" + ], + "expression": null, + "name": "asec" + }, + { + "arguments": [ + "z" + ], + "expression": null, + "name": "csc" + }, + { + "arguments": [ + "z" + ], + "expression": null, + "name": "acsc" + } + ], + "lorentz_structures": [ + { + "name": "UUS1", + "spins": [ + -1, + -1, + 1 + ], + "structure": "1" + }, + { + "name": "UUV1", + "spins": [ + -1, + -1, + 3 + ], + "structure": "-1*UFO::{}::P(3,2)" + }, + { + "name": "SSS1", + "spins": [ + 1, + 1, + 1 + ], + "structure": "1" + }, + { + "name": "FFS1", + "spins": [ + 2, + 2, + 1 + ], + "structure": "UFO::{}::ProjM(2,1)" + }, + { + "name": "FFS2", + "spins": [ + 2, + 2, + 1 + ], + "structure": "-1*UFO::{}::ProjP(2,1)+UFO::{}::ProjM(2,1)" + }, + { + "name": "FFS3", + "spins": [ + 2, + 2, + 1 + ], + "structure": "UFO::{}::ProjP(2,1)" + }, + { + "name": "FFS4", + "spins": [ + 2, + 2, + 1 + ], + "structure": "UFO::{}::ProjM(2,1)+UFO::{}::ProjP(2,1)" + }, + { + "name": "FFV1", + "spins": [ + 2, + 2, + 3 + ], + "structure": "UFO::{}::Gamma(3,2,1)" + }, + { + "name": "FFV2", + "spins": [ + 2, + 2, + 3 + ], + "structure": "UFO::{}::Gamma(3,2,-1)*UFO::{}::ProjM(-1,1)" + }, + { + "name": "FFV3", + "spins": [ + 2, + 2, + 3 + ], + "structure": "-2*UFO::{}::Gamma(3,2,-1)*UFO::{}::ProjP(-1,1)+UFO::{}::Gamma(3,2,-1)*UFO::{}::ProjM(-1,1)" + }, + { + "name": "FFV4", + "spins": [ + 2, + 2, + 3 + ], + "structure": "2*UFO::{}::Gamma(3,2,-1)*UFO::{}::ProjP(-1,1)+UFO::{}::Gamma(3,2,-1)*UFO::{}::ProjM(-1,1)" + }, + { + "name": "FFV5", + "spins": [ + 2, + 2, + 3 + ], + "structure": "4*UFO::{}::Gamma(3,2,-1)*UFO::{}::ProjP(-1,1)+UFO::{}::Gamma(3,2,-1)*UFO::{}::ProjM(-1,1)" + }, + { + "name": "VSS1", + "spins": [ + 3, + 1, + 1 + ], + "structure": "-1*UFO::{}::P(1,3)+UFO::{}::P(1,2)" + }, + { + "name": "VVS1", + "spins": [ + 3, + 3, + 1 + ], + "structure": "UFO::{}::Metric(1,2)" + }, + { + "name": "VVV1", + "spins": [ + 3, + 3, + 3 + ], + "structure": "-1*UFO::{}::Metric(1,2)*UFO::{}::P(3,2)+-1*UFO::{}::Metric(1,3)*UFO::{}::P(2,1)+-1*UFO::{}::Metric(2,3)*UFO::{}::P(1,3)+UFO::{}::Metric(1,2)*UFO::{}::P(3,1)+UFO::{}::Metric(1,3)*UFO::{}::P(2,3)+UFO::{}::Metric(2,3)*UFO::{}::P(1,2)" + }, + { + "name": "SSSS1", + "spins": [ + 1, + 1, + 1, + 1 + ], + "structure": "1" + }, + { + "name": "VVSS1", + "spins": [ + 3, + 3, + 1, + 1 + ], + "structure": "UFO::{}::Metric(1,2)" + }, + { + "name": "VVVV1", + "spins": [ + 3, + 3, + 3, + 3 + ], + "structure": "-1*UFO::{}::Metric(1,3)*UFO::{}::Metric(2,4)+UFO::{}::Metric(1,4)*UFO::{}::Metric(2,3)" + }, + { + "name": "VVVV2", + "spins": [ + 3, + 3, + 3, + 3 + ], + "structure": "-2*UFO::{}::Metric(1,2)*UFO::{}::Metric(3,4)+UFO::{}::Metric(1,3)*UFO::{}::Metric(2,4)+UFO::{}::Metric(1,4)*UFO::{}::Metric(2,3)" + }, + { + "name": "VVVV3", + "spins": [ + 3, + 3, + 3, + 3 + ], + "structure": "-1*UFO::{}::Metric(1,2)*UFO::{}::Metric(3,4)+UFO::{}::Metric(1,4)*UFO::{}::Metric(2,3)" + }, + { + "name": "VVVV4", + "spins": [ + 3, + 3, + 3, + 3 + ], + "structure": "-1*UFO::{}::Metric(1,2)*UFO::{}::Metric(3,4)+UFO::{}::Metric(1,3)*UFO::{}::Metric(2,4)" + }, + { + "name": "VVVV5", + "spins": [ + 3, + 3, + 3, + 3 + ], + "structure": "-1/2*UFO::{}::Metric(1,2)*UFO::{}::Metric(3,4)+-1/2*UFO::{}::Metric(1,3)*UFO::{}::Metric(2,4)+UFO::{}::Metric(1,4)*UFO::{}::Metric(2,3)" + } + ], + "name": "sm", + "orders": [ + { + "expansion_order": 99, + "hierarchy": 1, + "name": "QCD" + }, + { + "expansion_order": 99, + "hierarchy": 2, + "name": "QED" + } + ], + "parameters": [ + { + "expression": null, + "lhablock": "SMINPUTS", + "lhacode": [ + 1 + ], + "name": "aEWM1", + "nature": "external", + "parameter_type": "real", + "value": [ + 132.50698, + 0.0 + ] + }, + { + "expression": null, + "lhablock": "SMINPUTS", + "lhacode": [ + 2 + ], + "name": "Gf", + "nature": "external", + "parameter_type": "real", + "value": [ + 1.16639e-05, + 0.0 + ] + }, + { + "expression": null, + "lhablock": "SMINPUTS", + "lhacode": [ + 3 + ], + "name": "aS", + "nature": "external", + "parameter_type": "real", + "value": [ + 0.118, + 0.0 + ] + }, + { + "expression": null, + "lhablock": "Wolfenstein", + "lhacode": [ + 1 + ], + "name": "lamWS", + "nature": "external", + "parameter_type": "real", + "value": [ + 0.2253, + 0.0 + ] + }, + { + "expression": null, + "lhablock": "Wolfenstein", + "lhacode": [ + 2 + ], + "name": "AWS", + "nature": "external", + "parameter_type": "real", + "value": [ + 0.808, + 0.0 + ] + }, + { + "expression": null, + "lhablock": "Wolfenstein", + "lhacode": [ + 3 + ], + "name": "rhoWS", + "nature": "external", + "parameter_type": "real", + "value": [ + 0.132, + 0.0 + ] + }, + { + "expression": null, + "lhablock": "Wolfenstein", + "lhacode": [ + 4 + ], + "name": "etaWS", + "nature": "external", + "parameter_type": "real", + "value": [ + 0.341, + 0.0 + ] + }, + { + "expression": null, + "lhablock": "YUKAWA", + "lhacode": [ + 4 + ], + "name": "ymc", + "nature": "external", + "parameter_type": "real", + "value": [ + 1.27, + 0.0 + ] + }, + { + "expression": null, + "lhablock": "YUKAWA", + "lhacode": [ + 5 + ], + "name": "ymb", + "nature": "external", + "parameter_type": "real", + "value": [ + 4.2, + 0.0 + ] + }, + { + "expression": null, + "lhablock": "YUKAWA", + "lhacode": [ + 6 + ], + "name": "ymt", + "nature": "external", + "parameter_type": "real", + "value": [ + 164.5, + 0.0 + ] + }, + { + "expression": null, + "lhablock": "YUKAWA", + "lhacode": [ + 11 + ], + "name": "yme", + "nature": "external", + "parameter_type": "real", + "value": [ + 0.000511, + 0.0 + ] + }, + { + "expression": null, + "lhablock": "YUKAWA", + "lhacode": [ + 13 + ], + "name": "ymm", + "nature": "external", + "parameter_type": "real", + "value": [ + 0.10566, + 0.0 + ] + }, + { + "expression": null, + "lhablock": "YUKAWA", + "lhacode": [ + 15 + ], + "name": "ymtau", + "nature": "external", + "parameter_type": "real", + "value": [ + 1.777, + 0.0 + ] + }, + { + "expression": null, + "lhablock": "MASS", + "lhacode": [ + 23 + ], + "name": "MZ", + "nature": "external", + "parameter_type": "real", + "value": [ + 91.188, + 0.0 + ] + }, + { + "expression": null, + "lhablock": "MASS", + "lhacode": [ + 4 + ], + "name": "MC", + "nature": "external", + "parameter_type": "real", + "value": [ + 1.27, + 0.0 + ] + }, + { + "expression": null, + "lhablock": "MASS", + "lhacode": [ + 6 + ], + "name": "MT", + "nature": "external", + "parameter_type": "real", + "value": [ + 173.0, + 0.0 + ] + }, + { + "expression": null, + "lhablock": "MASS", + "lhacode": [ + 5 + ], + "name": "MB", + "nature": "external", + "parameter_type": "real", + "value": [ + 4.7, + 0.0 + ] + }, + { + "expression": null, + "lhablock": "MASS", + "lhacode": [ + 25 + ], + "name": "MH", + "nature": "external", + "parameter_type": "real", + "value": [ + 125.0, + 0.0 + ] + }, + { + "expression": null, + "lhablock": "MASS", + "lhacode": [ + 11 + ], + "name": "Me", + "nature": "external", + "parameter_type": "real", + "value": [ + 0.000511, + 0.0 + ] + }, + { + "expression": null, + "lhablock": "MASS", + "lhacode": [ + 13 + ], + "name": "MM", + "nature": "external", + "parameter_type": "real", + "value": [ + 0.10566, + 0.0 + ] + }, + { + "expression": null, + "lhablock": "MASS", + "lhacode": [ + 15 + ], + "name": "MTA", + "nature": "external", + "parameter_type": "real", + "value": [ + 1.777, + 0.0 + ] + }, + { + "expression": null, + "lhablock": "DECAY", + "lhacode": [ + 23 + ], + "name": "WZ", + "nature": "external", + "parameter_type": "real", + "value": [ + 2.44140351, + 0.0 + ] + }, + { + "expression": null, + "lhablock": "DECAY", + "lhacode": [ + 24 + ], + "name": "WW", + "nature": "external", + "parameter_type": "real", + "value": [ + 2.04759951, + 0.0 + ] + }, + { + "expression": null, + "lhablock": "DECAY", + "lhacode": [ + 6 + ], + "name": "WT", + "nature": "external", + "parameter_type": "real", + "value": [ + 1.50833649, + 0.0 + ] + }, + { + "expression": null, + "lhablock": "DECAY", + "lhacode": [ + 25 + ], + "name": "WH", + "nature": "external", + "parameter_type": "real", + "value": [ + 0.00638233934, + 0.0 + ] + }, + { + "expression": null, + "lhablock": "DECAY", + "lhacode": [ + 15 + ], + "name": "WTau", + "nature": "external", + "parameter_type": "real", + "value": [ + 2.27e-12, + 0.0 + ] + }, + { + "expression": null, + "lhablock": null, + "lhacode": null, + "name": "ZERO", + "nature": "internal", + "parameter_type": "real", + "value": [ + 0.0, + 0.0 + ] + }, + { + "expression": null, + "lhablock": null, + "lhacode": null, + "name": "CKM3x3", + "nature": "internal", + "parameter_type": "complex", + "value": [ + 1.0, + 0.0 + ] + }, + { + "expression": "-1/2*UFO::{}::lamWS^2+1", + "lhablock": null, + "lhacode": null, + "name": "CKM1x1", + "nature": "internal", + "parameter_type": "complex", + "value": [ + 0.974619955, + 0.0 + ] + }, + { + "expression": "UFO::{}::lamWS", + "lhablock": null, + "lhacode": null, + "name": "CKM1x2", + "nature": "internal", + "parameter_type": "complex", + "value": [ + 0.2253, + 0.0 + ] + }, + { + "expression": "(-1𝑖*UFO::{}::etaWS+UFO::{}::rhoWS)*UFO::{}::AWS*UFO::{}::lamWS^3", + "lhablock": null, + "lhacode": null, + "name": "CKM1x3", + "nature": "internal", + "parameter_type": "complex", + "value": [ + 0.0012197444962317122, + -0.0031510066152652563 + ] + }, + { + "expression": "-1*UFO::{}::lamWS", + "lhablock": null, + "lhacode": null, + "name": "CKM2x1", + "nature": "internal", + "parameter_type": "complex", + "value": [ + -0.2253, + 0.0 + ] + }, + { + "expression": "-1/2*UFO::{}::lamWS^2+1", + "lhablock": null, + "lhacode": null, + "name": "CKM2x2", + "nature": "internal", + "parameter_type": "complex", + "value": [ + 0.974619955, + 0.0 + ] + }, + { + "expression": "UFO::{}::AWS*UFO::{}::lamWS^2", + "lhablock": null, + "lhacode": null, + "name": "CKM2x3", + "nature": "internal", + "parameter_type": "complex", + "value": [ + 0.04101415272, + 0.0 + ] + }, + { + "expression": "(-1*UFO::{}::rhoWS+-1𝑖*UFO::{}::etaWS+1)*UFO::{}::AWS*UFO::{}::lamWS^3", + "lhablock": null, + "lhacode": null, + "name": "CKM3x1", + "nature": "internal", + "parameter_type": "complex", + "value": [ + 0.008020744111584288, + -0.0031510066152652563 + ] + }, + { + "expression": "-1*UFO::{}::AWS*UFO::{}::lamWS^2", + "lhablock": null, + "lhacode": null, + "name": "CKM3x2", + "nature": "internal", + "parameter_type": "complex", + "value": [ + -0.04101415272, + 0.0 + ] + }, + { + "expression": "UFO::{}::aEWM1^(-1)", + "lhablock": null, + "lhacode": null, + "name": "aEW", + "nature": "internal", + "parameter_type": "real", + "value": [ + 0.00754677225305414, + 0.0 + ] + }, + { + "expression": "2*UFO::{}::aS^(1/2)*𝜋^(1/2)", + "lhablock": null, + "lhacode": null, + "name": "G", + "nature": "internal", + "parameter_type": "real", + "value": [ + 1.2177157847767195, + 0.0 + ] + }, + { + "expression": "(((1/2)^(1/2)*-1*UFO::{}::Gf^(-1)*UFO::{}::MZ^2*UFO::{}::aEW*𝜋+1/4*UFO::{}::MZ^4)^(1/2)+1/2*UFO::{}::MZ^2)^(1/2)", + "lhablock": null, + "lhacode": null, + "name": "MW", + "nature": "internal", + "parameter_type": "real", + "value": [ + 80.4190000176632, + 0.0 + ] + }, + { + "expression": "2*UFO::{}::aEW^(1/2)*𝜋^(1/2)", + "lhablock": null, + "lhacode": null, + "name": "ee", + "nature": "internal", + "parameter_type": "real", + "value": [ + 0.3079537904849374, + 0.0 + ] + }, + { + "expression": "-1*UFO::{}::MW^2*UFO::{}::MZ^(-2)+1", + "lhablock": null, + "lhacode": null, + "name": "sw2", + "nature": "internal", + "parameter_type": "real", + "value": [ + 0.22224653275123984, + 0.0 + ] + }, + { + "expression": "(-1*UFO::{}::sw2+1)^(1/2)", + "lhablock": null, + "lhacode": null, + "name": "cw", + "nature": "internal", + "parameter_type": "real", + "value": [ + 0.8819033208060622, + 0.0 + ] + }, + { + "expression": "UFO::{}::sw2^(1/2)", + "lhablock": null, + "lhacode": null, + "name": "sw", + "nature": "internal", + "parameter_type": "real", + "value": [ + 0.47143030529574553, + 0.0 + ] + }, + { + "expression": "UFO::{}::cw^(-1)*UFO::{}::ee", + "lhablock": null, + "lhacode": null, + "name": "g1", + "nature": "internal", + "parameter_type": "real", + "value": [ + 0.3491922336832418, + 0.0 + ] + }, + { + "expression": "UFO::{}::ee*UFO::{}::sw^(-1)", + "lhablock": null, + "lhacode": null, + "name": "gw", + "nature": "internal", + "parameter_type": "real", + "value": [ + 0.653232910624502, + 0.0 + ] + }, + { + "expression": "2*UFO::{}::MW*UFO::{}::ee^(-1)*UFO::{}::sw", + "lhablock": null, + "lhacode": null, + "name": "vev", + "nature": "internal", + "parameter_type": "real", + "value": [ + 246.21845810181625, + 0.0 + ] + }, + { + "expression": "1/2*UFO::{}::MH^2*UFO::{}::vev^(-2)", + "lhablock": null, + "lhacode": null, + "name": "lam", + "nature": "internal", + "parameter_type": "real", + "value": [ + 0.12886910601690277, + 0.0 + ] + }, + { + "expression": "2^(1/2)*UFO::{}::vev^(-1)*UFO::{}::ymb", + "lhablock": null, + "lhacode": null, + "name": "yb", + "nature": "internal", + "parameter_type": "real", + "value": [ + 0.024123686777011728, + 0.0 + ] + }, + { + "expression": "2^(1/2)*UFO::{}::vev^(-1)*UFO::{}::ymc", + "lhablock": null, + "lhacode": null, + "name": "yc", + "nature": "internal", + "parameter_type": "real", + "value": [ + 0.007294543382572593, + 0.0 + ] + }, + { + "expression": "2^(1/2)*UFO::{}::vev^(-1)*UFO::{}::yme", + "lhablock": null, + "lhacode": null, + "name": "ye", + "nature": "internal", + "parameter_type": "real", + "value": [ + 2.93504855786976e-06, + 0.0 + ] + }, + { + "expression": "2^(1/2)*UFO::{}::vev^(-1)*UFO::{}::ymm", + "lhablock": null, + "lhacode": null, + "name": "ym", + "nature": "internal", + "parameter_type": "real", + "value": [ + 0.0006068830344902521, + 0.0 + ] + }, + { + "expression": "2^(1/2)*UFO::{}::vev^(-1)*UFO::{}::ymt", + "lhablock": null, + "lhacode": null, + "name": "yt", + "nature": "internal", + "parameter_type": "real", + "value": [ + 0.9448443987662926, + 0.0 + ] + }, + { + "expression": "2^(1/2)*UFO::{}::vev^(-1)*UFO::{}::ymtau", + "lhablock": null, + "lhacode": null, + "name": "ytau", + "nature": "internal", + "parameter_type": "real", + "value": [ + 0.010206617000654722, + 0.0 + ] + }, + { + "expression": "(UFO::{}::lam*UFO::{}::vev^2)^(1/2)", + "lhablock": null, + "lhacode": null, + "name": "muH", + "nature": "internal", + "parameter_type": "real", + "value": [ + 88.38834764831844, + 0.0 + ] + }, + { + "expression": "UFO::{}::yb*conj(UFO::{}::CKM1x3)", + "lhablock": null, + "lhacode": null, + "name": "I1x31", + "nature": "internal", + "parameter_type": "complex", + "value": [ + 2.9424734175077786e-05, + 7.601389661895094e-05 + ] + }, + { + "expression": "UFO::{}::yb*conj(UFO::{}::CKM2x3)", + "lhablock": null, + "lhacode": null, + "name": "I1x32", + "nature": "internal", + "parameter_type": "complex", + "value": [ + 0.0009894125736418036, + 0.0 + ] + }, + { + "expression": "UFO::{}::yb*conj(UFO::{}::CKM3x3)", + "lhablock": null, + "lhacode": null, + "name": "I1x33", + "nature": "internal", + "parameter_type": "complex", + "value": [ + 0.024123686777011728, + 0.0 + ] + }, + { + "expression": "UFO::{}::yc*conj(UFO::{}::CKM2x1)", + "lhablock": null, + "lhacode": null, + "name": "I2x12", + "nature": "internal", + "parameter_type": "complex", + "value": [ + -0.0016434606240936052, + 0.0 + ] + }, + { + "expression": "UFO::{}::yt*conj(UFO::{}::CKM3x1)", + "lhablock": null, + "lhacode": null, + "name": "I2x13", + "nature": "internal", + "parameter_type": "complex", + "value": [ + 0.007578355147768138, + 0.0029772109509089116 + ] + }, + { + "expression": "UFO::{}::yc*conj(UFO::{}::CKM2x2)", + "lhablock": null, + "lhacode": null, + "name": "I2x22", + "nature": "internal", + "parameter_type": "complex", + "value": [ + 0.007109407543268448, + 0.0 + ] + }, + { + "expression": "UFO::{}::yt*conj(UFO::{}::CKM3x2)", + "lhablock": null, + "lhacode": null, + "name": "I2x23", + "nature": "internal", + "parameter_type": "complex", + "value": [ + -0.0387519924676373, + 0.0 + ] + }, + { + "expression": "UFO::{}::yc*conj(UFO::{}::CKM2x3)", + "lhablock": null, + "lhacode": null, + "name": "I2x32", + "nature": "internal", + "parameter_type": "complex", + "value": [ + 0.00029917951631549774, + 0.0 + ] + }, + { + "expression": "UFO::{}::yt*conj(UFO::{}::CKM3x3)", + "lhablock": null, + "lhacode": null, + "name": "I2x33", + "nature": "internal", + "parameter_type": "complex", + "value": [ + 0.9448443987662926, + 0.0 + ] + }, + { + "expression": "UFO::{}::CKM2x1*UFO::{}::yc", + "lhablock": null, + "lhacode": null, + "name": "I3x21", + "nature": "internal", + "parameter_type": "complex", + "value": [ + -0.0016434606240936052, + 0.0 + ] + }, + { + "expression": "UFO::{}::CKM2x2*UFO::{}::yc", + "lhablock": null, + "lhacode": null, + "name": "I3x22", + "nature": "internal", + "parameter_type": "complex", + "value": [ + 0.007109407543268448, + 0.0 + ] + }, + { + "expression": "UFO::{}::CKM2x3*UFO::{}::yc", + "lhablock": null, + "lhacode": null, + "name": "I3x23", + "nature": "internal", + "parameter_type": "complex", + "value": [ + 0.00029917951631549774, + 0.0 + ] + }, + { + "expression": "UFO::{}::CKM3x1*UFO::{}::yt", + "lhablock": null, + "lhacode": null, + "name": "I3x31", + "nature": "internal", + "parameter_type": "complex", + "value": [ + 0.007578355147768138, + -0.0029772109509089116 + ] + }, + { + "expression": "UFO::{}::CKM3x2*UFO::{}::yt", + "lhablock": null, + "lhacode": null, + "name": "I3x32", + "nature": "internal", + "parameter_type": "complex", + "value": [ + -0.0387519924676373, + 0.0 + ] + }, + { + "expression": "UFO::{}::CKM3x3*UFO::{}::yt", + "lhablock": null, + "lhacode": null, + "name": "I3x33", + "nature": "internal", + "parameter_type": "complex", + "value": [ + 0.9448443987662926, + 0.0 + ] + }, + { + "expression": "UFO::{}::CKM1x3*UFO::{}::yb", + "lhablock": null, + "lhacode": null, + "name": "I4x13", + "nature": "internal", + "parameter_type": "complex", + "value": [ + 2.9424734175077786e-05, + -7.601389661895094e-05 + ] + }, + { + "expression": "UFO::{}::CKM2x3*UFO::{}::yb", + "lhablock": null, + "lhacode": null, + "name": "I4x23", + "nature": "internal", + "parameter_type": "complex", + "value": [ + 0.0009894125736418036, + 0.0 + ] + }, + { + "expression": "UFO::{}::CKM3x3*UFO::{}::yb", + "lhablock": null, + "lhacode": null, + "name": "I4x33", + "nature": "internal", + "parameter_type": "complex", + "value": [ + 0.024123686777011728, + 0.0 + ] + } + ], + "particles": [ + { + "antiname": "a", + "antitexname": "{\\gamma}", + "charge": 0.0, + "color": 1, + "ghost_number": 0, + "goldstoneboson": false, + "lepton_number": 0, + "mass": "ZERO", + "name": "a", + "pdg_code": 22, + "propagating": true, + "propagator": "a_propFeynman", + "spin": 3, + "texname": "{\\gamma}", + "width": "ZERO", + "y_charge": 0 + }, + { + "antiname": "Z", + "antitexname": "{Z}", + "charge": 0.0, + "color": 1, + "ghost_number": 0, + "goldstoneboson": false, + "lepton_number": 0, + "mass": "MZ", + "name": "Z", + "pdg_code": 23, + "propagating": true, + "propagator": "Z_propFeynman", + "spin": 3, + "texname": "{Z}", + "width": "WZ", + "y_charge": 0 + }, + { + "antiname": "W-", + "antitexname": "{W^-}", + "charge": 1.0, + "color": 1, + "ghost_number": 0, + "goldstoneboson": false, + "lepton_number": 0, + "mass": "MW", + "name": "W+", + "pdg_code": 24, + "propagating": true, + "propagator": "W+_propFeynman", + "spin": 3, + "texname": "{W^+}", + "width": "WW", + "y_charge": 0 + }, + { + "antiname": "W+", + "antitexname": "{W^+}", + "charge": -1.0, + "color": 1, + "ghost_number": 0, + "goldstoneboson": false, + "lepton_number": 0, + "mass": "MW", + "name": "W-", + "pdg_code": -24, + "propagating": true, + "propagator": "W-_propFeynman", + "spin": 3, + "texname": "{W^-}", + "width": "WW", + "y_charge": 0 + }, + { + "antiname": "g", + "antitexname": "{g}", + "charge": 0.0, + "color": 8, + "ghost_number": 0, + "goldstoneboson": false, + "lepton_number": 0, + "mass": "ZERO", + "name": "g", + "pdg_code": 21, + "propagating": true, + "propagator": "g_propFeynman", + "spin": 3, + "texname": "{g}", + "width": "ZERO", + "y_charge": 0 + }, + { + "antiname": "ghA~", + "antitexname": "\\overline{\\tilde{\\gamma}}", + "charge": 0.0, + "color": 1, + "ghost_number": 1, + "goldstoneboson": false, + "lepton_number": 0, + "mass": "ZERO", + "name": "ghA", + "pdg_code": 9000001, + "propagating": true, + "propagator": "ghA_propFeynman", + "spin": -1, + "texname": "\\tilde{\\gamma}", + "width": "ZERO", + "y_charge": 0 + }, + { + "antiname": "ghA", + "antitexname": "\\tilde{\\gamma}", + "charge": -0.0, + "color": 1, + "ghost_number": -1, + "goldstoneboson": false, + "lepton_number": 0, + "mass": "ZERO", + "name": "ghA~", + "pdg_code": -9000001, + "propagating": true, + "propagator": "ghA~_propFeynman", + "spin": -1, + "texname": "\\overline{\\tilde{\\gamma}}", + "width": "ZERO", + "y_charge": 0 + }, + { + "antiname": "ghZ~", + "antitexname": "\\bar{\\tilde{Z}}", + "charge": 0.0, + "color": 1, + "ghost_number": 1, + "goldstoneboson": false, + "lepton_number": 0, + "mass": "MZ", + "name": "ghZ", + "pdg_code": 9000002, + "propagating": true, + "propagator": "ghZ_propFeynman", + "spin": -1, + "texname": "\\tilde{Z}", + "width": "WZ", + "y_charge": 0 + }, + { + "antiname": "ghZ", + "antitexname": "\\tilde{Z}", + "charge": -0.0, + "color": 1, + "ghost_number": -1, + "goldstoneboson": false, + "lepton_number": 0, + "mass": "MZ", + "name": "ghZ~", + "pdg_code": -9000002, + "propagating": true, + "propagator": "ghZ~_propFeynman", + "spin": -1, + "texname": "\\bar{\\tilde{Z}}", + "width": "WZ", + "y_charge": 0 + }, + { + "antiname": "ghWp~", + "antitexname": "{\\bar{\\tilde{W}}^+}", + "charge": 1.0, + "color": 1, + "ghost_number": 1, + "goldstoneboson": false, + "lepton_number": 0, + "mass": "MW", + "name": "ghWp", + "pdg_code": 9000003, + "propagating": true, + "propagator": "ghWp_propFeynman", + "spin": -1, + "texname": "{\\tilde{W}^+}", + "width": "WW", + "y_charge": 0 + }, + { + "antiname": "ghWp", + "antitexname": "{\\tilde{W}^+}", + "charge": -1.0, + "color": 1, + "ghost_number": -1, + "goldstoneboson": false, + "lepton_number": 0, + "mass": "MW", + "name": "ghWp~", + "pdg_code": -9000003, + "propagating": true, + "propagator": "ghWp~_propFeynman", + "spin": -1, + "texname": "{\\bar{\\tilde{W}}^+}", + "width": "WW", + "y_charge": 0 + }, + { + "antiname": "ghWm~", + "antitexname": "{\\bar{\\tilde{W}}^-}", + "charge": -1.0, + "color": 1, + "ghost_number": 1, + "goldstoneboson": false, + "lepton_number": 0, + "mass": "MW", + "name": "ghWm", + "pdg_code": 9000004, + "propagating": true, + "propagator": "ghWm_propFeynman", + "spin": -1, + "texname": "{\\tilde{W}^-}", + "width": "WW", + "y_charge": 0 + }, + { + "antiname": "ghWm", + "antitexname": "{\\tilde{W}^-}", + "charge": 1.0, + "color": 1, + "ghost_number": -1, + "goldstoneboson": false, + "lepton_number": 0, + "mass": "MW", + "name": "ghWm~", + "pdg_code": -9000004, + "propagating": true, + "propagator": "ghWm~_propFeynman", + "spin": -1, + "texname": "{\\bar{\\tilde{W}}^-}", + "width": "WW", + "y_charge": 0 + }, + { + "antiname": "ghG~", + "antitexname": "{\\bar{\\tilde{g}}}", + "charge": 0.0, + "color": 8, + "ghost_number": 1, + "goldstoneboson": false, + "lepton_number": 0, + "mass": "ZERO", + "name": "ghG", + "pdg_code": 9000005, + "propagating": true, + "propagator": "ghG_propFeynman", + "spin": -1, + "texname": "{\\tilde{g}}", + "width": "ZERO", + "y_charge": 0 + }, + { + "antiname": "ghG", + "antitexname": "{\\tilde{g}}", + "charge": -0.0, + "color": 8, + "ghost_number": -1, + "goldstoneboson": false, + "lepton_number": 0, + "mass": "ZERO", + "name": "ghG~", + "pdg_code": -9000005, + "propagating": true, + "propagator": "ghG~_propFeynman", + "spin": -1, + "texname": "{\\bar{\\tilde{g}}}", + "width": "ZERO", + "y_charge": 0 + }, + { + "antiname": "ve~", + "antitexname": "{\\overline{\\nu}_e}", + "charge": 0.0, + "color": 1, + "ghost_number": 0, + "goldstoneboson": false, + "lepton_number": 1, + "mass": "ZERO", + "name": "ve", + "pdg_code": 12, + "propagating": true, + "propagator": "ve_propFeynman", + "spin": 2, + "texname": "{\\nu_e}", + "width": "ZERO", + "y_charge": 0 + }, + { + "antiname": "ve", + "antitexname": "{\\nu_e}", + "charge": -0.0, + "color": 1, + "ghost_number": 0, + "goldstoneboson": false, + "lepton_number": -1, + "mass": "ZERO", + "name": "ve~", + "pdg_code": -12, + "propagating": true, + "propagator": "ve~_propFeynman", + "spin": 2, + "texname": "{\\overline{\\nu}_e}", + "width": "ZERO", + "y_charge": 0 + }, + { + "antiname": "vm~", + "antitexname": "{\\overline{\\nu}_\\mu}", + "charge": 0.0, + "color": 1, + "ghost_number": 0, + "goldstoneboson": false, + "lepton_number": 1, + "mass": "ZERO", + "name": "vm", + "pdg_code": 14, + "propagating": true, + "propagator": "vm_propFeynman", + "spin": 2, + "texname": "{\\nu_\\mu}", + "width": "ZERO", + "y_charge": 0 + }, + { + "antiname": "vm", + "antitexname": "{\\nu_\\mu}", + "charge": -0.0, + "color": 1, + "ghost_number": 0, + "goldstoneboson": false, + "lepton_number": -1, + "mass": "ZERO", + "name": "vm~", + "pdg_code": -14, + "propagating": true, + "propagator": "vm~_propFeynman", + "spin": 2, + "texname": "{\\overline{\\nu}_\\mu}", + "width": "ZERO", + "y_charge": 0 + }, + { + "antiname": "vt~", + "antitexname": "{\\overline{\\nu}_\\tau}", + "charge": 0.0, + "color": 1, + "ghost_number": 0, + "goldstoneboson": false, + "lepton_number": 1, + "mass": "ZERO", + "name": "vt", + "pdg_code": 16, + "propagating": true, + "propagator": "vt_propFeynman", + "spin": 2, + "texname": "{\\nu_\\tau}", + "width": "ZERO", + "y_charge": 0 + }, + { + "antiname": "vt", + "antitexname": "{\\nu_\\tau}", + "charge": -0.0, + "color": 1, + "ghost_number": 0, + "goldstoneboson": false, + "lepton_number": -1, + "mass": "ZERO", + "name": "vt~", + "pdg_code": -16, + "propagating": true, + "propagator": "vt~_propFeynman", + "spin": 2, + "texname": "{\\overline{\\nu}_\\tau}", + "width": "ZERO", + "y_charge": 0 + }, + { + "antiname": "u~", + "antitexname": "{\\overline{u}}", + "charge": 0.6666666666666666, + "color": 3, + "ghost_number": 0, + "goldstoneboson": false, + "lepton_number": 0, + "mass": "ZERO", + "name": "u", + "pdg_code": 2, + "propagating": true, + "propagator": "u_propFeynman", + "spin": 2, + "texname": "{u}", + "width": "ZERO", + "y_charge": 0 + }, + { + "antiname": "u", + "antitexname": "{u}", + "charge": -0.6666666666666666, + "color": -3, + "ghost_number": 0, + "goldstoneboson": false, + "lepton_number": 0, + "mass": "ZERO", + "name": "u~", + "pdg_code": -2, + "propagating": true, + "propagator": "u~_propFeynman", + "spin": 2, + "texname": "{\\overline{u}}", + "width": "ZERO", + "y_charge": 0 + }, + { + "antiname": "c~", + "antitexname": "{\\overline{c}}", + "charge": 0.6666666666666666, + "color": 3, + "ghost_number": 0, + "goldstoneboson": false, + "lepton_number": 0, + "mass": "MC", + "name": "c", + "pdg_code": 4, + "propagating": true, + "propagator": "c_propFeynman", + "spin": 2, + "texname": "{c}", + "width": "ZERO", + "y_charge": 0 + }, + { + "antiname": "c", + "antitexname": "{c}", + "charge": -0.6666666666666666, + "color": -3, + "ghost_number": 0, + "goldstoneboson": false, + "lepton_number": 0, + "mass": "MC", + "name": "c~", + "pdg_code": -4, + "propagating": true, + "propagator": "c~_propFeynman", + "spin": 2, + "texname": "{\\overline{c}}", + "width": "ZERO", + "y_charge": 0 + }, + { + "antiname": "t~", + "antitexname": "{\\overline{t}}", + "charge": 0.6666666666666666, + "color": 3, + "ghost_number": 0, + "goldstoneboson": false, + "lepton_number": 0, + "mass": "MT", + "name": "t", + "pdg_code": 6, + "propagating": true, + "propagator": "t_propFeynman", + "spin": 2, + "texname": "{t}", + "width": "WT", + "y_charge": 0 + }, + { + "antiname": "t", + "antitexname": "{t}", + "charge": -0.6666666666666666, + "color": -3, + "ghost_number": 0, + "goldstoneboson": false, + "lepton_number": 0, + "mass": "MT", + "name": "t~", + "pdg_code": -6, + "propagating": true, + "propagator": "t~_propFeynman", + "spin": 2, + "texname": "{\\overline{t}}", + "width": "WT", + "y_charge": 0 + }, + { + "antiname": "d~", + "antitexname": "{\\overline{d}}", + "charge": -0.3333333333333333, + "color": 3, + "ghost_number": 0, + "goldstoneboson": false, + "lepton_number": 0, + "mass": "ZERO", + "name": "d", + "pdg_code": 1, + "propagating": true, + "propagator": "d_propFeynman", + "spin": 2, + "texname": "{d}", + "width": "ZERO", + "y_charge": 0 + }, + { + "antiname": "d", + "antitexname": "{d}", + "charge": 0.3333333333333333, + "color": -3, + "ghost_number": 0, + "goldstoneboson": false, + "lepton_number": 0, + "mass": "ZERO", + "name": "d~", + "pdg_code": -1, + "propagating": true, + "propagator": "d~_propFeynman", + "spin": 2, + "texname": "{\\overline{d}}", + "width": "ZERO", + "y_charge": 0 + }, + { + "antiname": "s~", + "antitexname": "{\\overline{s}}", + "charge": -0.3333333333333333, + "color": 3, + "ghost_number": 0, + "goldstoneboson": false, + "lepton_number": 0, + "mass": "ZERO", + "name": "s", + "pdg_code": 3, + "propagating": true, + "propagator": "s_propFeynman", + "spin": 2, + "texname": "{s}", + "width": "ZERO", + "y_charge": 0 + }, + { + "antiname": "s", + "antitexname": "{s}", + "charge": 0.3333333333333333, + "color": -3, + "ghost_number": 0, + "goldstoneboson": false, + "lepton_number": 0, + "mass": "ZERO", + "name": "s~", + "pdg_code": -3, + "propagating": true, + "propagator": "s~_propFeynman", + "spin": 2, + "texname": "{\\overline{s}}", + "width": "ZERO", + "y_charge": 0 + }, + { + "antiname": "b~", + "antitexname": "{\\overline{b}}", + "charge": -0.3333333333333333, + "color": 3, + "ghost_number": 0, + "goldstoneboson": false, + "lepton_number": 0, + "mass": "MB", + "name": "b", + "pdg_code": 5, + "propagating": true, + "propagator": "b_propFeynman", + "spin": 2, + "texname": "{b}", + "width": "ZERO", + "y_charge": 0 + }, + { + "antiname": "b", + "antitexname": "{b}", + "charge": 0.3333333333333333, + "color": -3, + "ghost_number": 0, + "goldstoneboson": false, + "lepton_number": 0, + "mass": "MB", + "name": "b~", + "pdg_code": -5, + "propagating": true, + "propagator": "b~_propFeynman", + "spin": 2, + "texname": "{\\overline{b}}", + "width": "ZERO", + "y_charge": 0 + }, + { + "antiname": "H", + "antitexname": "{H}", + "charge": 0.0, + "color": 1, + "ghost_number": 0, + "goldstoneboson": false, + "lepton_number": 0, + "mass": "MH", + "name": "H", + "pdg_code": 25, + "propagating": true, + "propagator": "H_propFeynman", + "spin": 1, + "texname": "{H}", + "width": "WH", + "y_charge": 0 + }, + { + "antiname": "G0", + "antitexname": "{G_0}", + "charge": 0.0, + "color": 1, + "ghost_number": 0, + "goldstoneboson": true, + "lepton_number": 0, + "mass": "MZ", + "name": "G0", + "pdg_code": 250, + "propagating": true, + "propagator": "G0_propFeynman", + "spin": 1, + "texname": "{G_0}", + "width": "WZ", + "y_charge": 0 + }, + { + "antiname": "G-", + "antitexname": "{G^-}", + "charge": 1.0, + "color": 1, + "ghost_number": 0, + "goldstoneboson": true, + "lepton_number": 0, + "mass": "MW", + "name": "G+", + "pdg_code": 251, + "propagating": true, + "propagator": "G+_propFeynman", + "spin": 1, + "texname": "{G^+}", + "width": "WW", + "y_charge": 0 + }, + { + "antiname": "G+", + "antitexname": "{G^+}", + "charge": -1.0, + "color": 1, + "ghost_number": 0, + "goldstoneboson": true, + "lepton_number": 0, + "mass": "MW", + "name": "G-", + "pdg_code": -251, + "propagating": true, + "propagator": "G-_propFeynman", + "spin": 1, + "texname": "{G^-}", + "width": "WW", + "y_charge": 0 + }, + { + "antiname": "e+", + "antitexname": "{e^+}", + "charge": -1.0, + "color": 1, + "ghost_number": 0, + "goldstoneboson": false, + "lepton_number": 1, + "mass": "Me", + "name": "e-", + "pdg_code": 11, + "propagating": true, + "propagator": "e-_propFeynman", + "spin": 2, + "texname": "{e^-}", + "width": "ZERO", + "y_charge": 0 + }, + { + "antiname": "e-", + "antitexname": "{e^-}", + "charge": 1.0, + "color": 1, + "ghost_number": 0, + "goldstoneboson": false, + "lepton_number": -1, + "mass": "Me", + "name": "e+", + "pdg_code": -11, + "propagating": true, + "propagator": "e+_propFeynman", + "spin": 2, + "texname": "{e^+}", + "width": "ZERO", + "y_charge": 0 + }, + { + "antiname": "mu+", + "antitexname": "{\\mu^+}", + "charge": -1.0, + "color": 1, + "ghost_number": 0, + "goldstoneboson": false, + "lepton_number": 1, + "mass": "MM", + "name": "mu-", + "pdg_code": 13, + "propagating": true, + "propagator": "mu-_propFeynman", + "spin": 2, + "texname": "{\\mu^-}", + "width": "ZERO", + "y_charge": 0 + }, + { + "antiname": "mu-", + "antitexname": "{\\mu^-}", + "charge": 1.0, + "color": 1, + "ghost_number": 0, + "goldstoneboson": false, + "lepton_number": -1, + "mass": "MM", + "name": "mu+", + "pdg_code": -13, + "propagating": true, + "propagator": "mu+_propFeynman", + "spin": 2, + "texname": "{\\mu^+}", + "width": "ZERO", + "y_charge": 0 + }, + { + "antiname": "ta+", + "antitexname": "{\\tau^+}", + "charge": -1.0, + "color": 1, + "ghost_number": 0, + "goldstoneboson": false, + "lepton_number": 1, + "mass": "MTA", + "name": "ta-", + "pdg_code": 15, + "propagating": true, + "propagator": "ta-_propFeynman", + "spin": 2, + "texname": "{\\tau^-}", + "width": "WTau", + "y_charge": 0 + }, + { + "antiname": "ta-", + "antitexname": "{\\tau^-}", + "charge": 1.0, + "color": 1, + "ghost_number": 0, + "goldstoneboson": false, + "lepton_number": -1, + "mass": "MTA", + "name": "ta+", + "pdg_code": -15, + "propagating": true, + "propagator": "ta+_propFeynman", + "spin": 2, + "texname": "{\\tau^+}", + "width": "WTau", + "y_charge": 0 + } + ], + "propagators": [ + { + "denominator": "(UFO::{}::P(1))^2", + "name": "a_propFeynman", + "numerator": "-1𝑖*UFO::{}::Metric(1,2)", + "particle": "a" + }, + { + "denominator": "(UFO::{}::P(1))^2+-1*UFO::{}::MZ^2", + "name": "Z_propFeynman", + "numerator": "(-1*UFO::{}::Metric(1,2)+UFO::{}::MZ^(-2)*UFO::{}::P(1)*UFO::{}::P(2))*1𝑖", + "particle": "Z" + }, + { + "denominator": "(UFO::{}::P(1))^2+-1*UFO::{}::MW^2", + "name": "W+_propFeynman", + "numerator": "(-1*UFO::{}::Metric(1,2)+UFO::{}::MW^(-2)*UFO::{}::P(1)*UFO::{}::P(2))*1𝑖", + "particle": "W+" + }, + { + "denominator": "(UFO::{}::P(1))^2+-1*UFO::{}::MW^2", + "name": "W-_propFeynman", + "numerator": "(-1*UFO::{}::Metric(1,2)+UFO::{}::MW^(-2)*UFO::{}::P(1)*UFO::{}::P(2))*1𝑖", + "particle": "W-" + }, + { + "denominator": "(UFO::{}::P(1))^2", + "name": "g_propFeynman", + "numerator": "-1𝑖*UFO::{}::Metric(1,2)", + "particle": "g" + }, + { + "denominator": "(UFO::{}::P(1))^2", + "name": "ghA_propFeynman", + "numerator": "1𝑖", + "particle": "ghA" + }, + { + "denominator": "(UFO::{}::P(1))^2", + "name": "ghA~_propFeynman", + "numerator": "1𝑖", + "particle": "ghA~" + }, + { + "denominator": "(UFO::{}::P(1))^2+-1*UFO::{}::MZ^2", + "name": "ghZ_propFeynman", + "numerator": "1𝑖", + "particle": "ghZ" + }, + { + "denominator": "(UFO::{}::P(1))^2+-1*UFO::{}::MZ^2", + "name": "ghZ~_propFeynman", + "numerator": "1𝑖", + "particle": "ghZ~" + }, + { + "denominator": "(UFO::{}::P(1))^2+-1*UFO::{}::MW^2", + "name": "ghWp_propFeynman", + "numerator": "1𝑖", + "particle": "ghWp" + }, + { + "denominator": "(UFO::{}::P(1))^2+-1*UFO::{}::MW^2", + "name": "ghWp~_propFeynman", + "numerator": "1𝑖", + "particle": "ghWp~" + }, + { + "denominator": "(UFO::{}::P(1))^2+-1*UFO::{}::MW^2", + "name": "ghWm_propFeynman", + "numerator": "1𝑖", + "particle": "ghWm" + }, + { + "denominator": "(UFO::{}::P(1))^2+-1*UFO::{}::MW^2", + "name": "ghWm~_propFeynman", + "numerator": "1𝑖", + "particle": "ghWm~" + }, + { + "denominator": "(UFO::{}::P(1))^2", + "name": "ghG_propFeynman", + "numerator": "1𝑖", + "particle": "ghG" + }, + { + "denominator": "(UFO::{}::P(1))^2", + "name": "ghG~_propFeynman", + "numerator": "1𝑖", + "particle": "ghG~" + }, + { + "denominator": "(UFO::{}::P(1))^2", + "name": "ve_propFeynman", + "numerator": "1𝑖*UFO::{}::PSlash(2,1)", + "particle": "ve" + }, + { + "denominator": "(UFO::{}::P(1))^2", + "name": "ve~_propFeynman", + "numerator": "-1𝑖*UFO::{}::PSlash(1,2)", + "particle": "ve~" + }, + { + "denominator": "(UFO::{}::P(1))^2", + "name": "vm_propFeynman", + "numerator": "1𝑖*UFO::{}::PSlash(2,1)", + "particle": "vm" + }, + { + "denominator": "(UFO::{}::P(1))^2", + "name": "vm~_propFeynman", + "numerator": "-1𝑖*UFO::{}::PSlash(1,2)", + "particle": "vm~" + }, + { + "denominator": "(UFO::{}::P(1))^2", + "name": "vt_propFeynman", + "numerator": "1𝑖*UFO::{}::PSlash(2,1)", + "particle": "vt" + }, + { + "denominator": "(UFO::{}::P(1))^2", + "name": "vt~_propFeynman", + "numerator": "-1𝑖*UFO::{}::PSlash(1,2)", + "particle": "vt~" + }, + { + "denominator": "(UFO::{}::P(1))^2", + "name": "u_propFeynman", + "numerator": "1𝑖*UFO::{}::PSlash(2,1)", + "particle": "u" + }, + { + "denominator": "(UFO::{}::P(1))^2", + "name": "u~_propFeynman", + "numerator": "-1𝑖*UFO::{}::PSlash(1,2)", + "particle": "u~" + }, + { + "denominator": "(UFO::{}::P(1))^2+-1*UFO::{}::MC^2", + "name": "c_propFeynman", + "numerator": "(UFO::{}::Identity(2,1)*UFO::{}::MC+UFO::{}::PSlash(2,1))*1𝑖", + "particle": "c" + }, + { + "denominator": "(UFO::{}::P(1))^2+-1*UFO::{}::MC^2", + "name": "c~_propFeynman", + "numerator": "(-1*UFO::{}::PSlash(1,2)+UFO::{}::Identity(1,2)*UFO::{}::MC)*1𝑖", + "particle": "c~" + }, + { + "denominator": "(UFO::{}::P(1))^2+-1*UFO::{}::MT^2", + "name": "t_propFeynman", + "numerator": "(UFO::{}::Identity(2,1)*UFO::{}::MT+UFO::{}::PSlash(2,1))*1𝑖", + "particle": "t" + }, + { + "denominator": "(UFO::{}::P(1))^2+-1*UFO::{}::MT^2", + "name": "t~_propFeynman", + "numerator": "(-1*UFO::{}::PSlash(1,2)+UFO::{}::Identity(1,2)*UFO::{}::MT)*1𝑖", + "particle": "t~" + }, + { + "denominator": "(UFO::{}::P(1))^2", + "name": "d_propFeynman", + "numerator": "1𝑖*UFO::{}::PSlash(2,1)", + "particle": "d" + }, + { + "denominator": "(UFO::{}::P(1))^2", + "name": "d~_propFeynman", + "numerator": "-1𝑖*UFO::{}::PSlash(1,2)", + "particle": "d~" + }, + { + "denominator": "(UFO::{}::P(1))^2", + "name": "s_propFeynman", + "numerator": "1𝑖*UFO::{}::PSlash(2,1)", + "particle": "s" + }, + { + "denominator": "(UFO::{}::P(1))^2", + "name": "s~_propFeynman", + "numerator": "-1𝑖*UFO::{}::PSlash(1,2)", + "particle": "s~" + }, + { + "denominator": "(UFO::{}::P(1))^2+-1*UFO::{}::MB^2", + "name": "b_propFeynman", + "numerator": "(UFO::{}::Identity(2,1)*UFO::{}::MB+UFO::{}::PSlash(2,1))*1𝑖", + "particle": "b" + }, + { + "denominator": "(UFO::{}::P(1))^2+-1*UFO::{}::MB^2", + "name": "b~_propFeynman", + "numerator": "(-1*UFO::{}::PSlash(1,2)+UFO::{}::Identity(1,2)*UFO::{}::MB)*1𝑖", + "particle": "b~" + }, + { + "denominator": "(UFO::{}::P(1))^2+-1*UFO::{}::MH^2", + "name": "H_propFeynman", + "numerator": "1𝑖", + "particle": "H" + }, + { + "denominator": "(UFO::{}::P(1))^2+-1*UFO::{}::MZ^2", + "name": "G0_propFeynman", + "numerator": "1𝑖", + "particle": "G0" + }, + { + "denominator": "(UFO::{}::P(1))^2+-1*UFO::{}::MW^2", + "name": "G+_propFeynman", + "numerator": "1𝑖", + "particle": "G+" + }, + { + "denominator": "(UFO::{}::P(1))^2+-1*UFO::{}::MW^2", + "name": "G-_propFeynman", + "numerator": "1𝑖", + "particle": "G-" + }, + { + "denominator": "(UFO::{}::P(1))^2+-1*UFO::{}::Me^2", + "name": "e-_propFeynman", + "numerator": "(UFO::{}::Identity(2,1)*UFO::{}::Me+UFO::{}::PSlash(2,1))*1𝑖", + "particle": "e-" + }, + { + "denominator": "(UFO::{}::P(1))^2+-1*UFO::{}::Me^2", + "name": "e+_propFeynman", + "numerator": "(-1*UFO::{}::PSlash(1,2)+UFO::{}::Identity(1,2)*UFO::{}::Me)*1𝑖", + "particle": "e+" + }, + { + "denominator": "(UFO::{}::P(1))^2+-1*UFO::{}::MM^2", + "name": "mu-_propFeynman", + "numerator": "(UFO::{}::Identity(2,1)*UFO::{}::MM+UFO::{}::PSlash(2,1))*1𝑖", + "particle": "mu-" + }, + { + "denominator": "(UFO::{}::P(1))^2+-1*UFO::{}::MM^2", + "name": "mu+_propFeynman", + "numerator": "(-1*UFO::{}::PSlash(1,2)+UFO::{}::Identity(1,2)*UFO::{}::MM)*1𝑖", + "particle": "mu+" + }, + { + "denominator": "(UFO::{}::P(1))^2+-1*UFO::{}::MTA^2", + "name": "ta-_propFeynman", + "numerator": "(UFO::{}::Identity(2,1)*UFO::{}::MTA+UFO::{}::PSlash(2,1))*1𝑖", + "particle": "ta-" + }, + { + "denominator": "(UFO::{}::P(1))^2+-1*UFO::{}::MTA^2", + "name": "ta+_propFeynman", + "numerator": "(-1*UFO::{}::PSlash(1,2)+UFO::{}::Identity(1,2)*UFO::{}::MTA)*1𝑖", + "particle": "ta+" + } + ], + "restriction": null, + "vertex_rules": [ + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_33" + ] + ], + "lorentz_structures": [ + "SSSS1" + ], + "name": "V_1", + "particles": [ + "G0", + "G0", + "G0", + "G0" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_31" + ] + ], + "lorentz_structures": [ + "SSSS1" + ], + "name": "V_2", + "particles": [ + "G0", + "G0", + "G-", + "G+" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_32" + ] + ], + "lorentz_structures": [ + "SSSS1" + ], + "name": "V_3", + "particles": [ + "G-", + "G-", + "G+", + "G+" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_31" + ] + ], + "lorentz_structures": [ + "SSSS1" + ], + "name": "V_4", + "particles": [ + "G0", + "G0", + "H", + "H" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_31" + ] + ], + "lorentz_structures": [ + "SSSS1" + ], + "name": "V_5", + "particles": [ + "G-", + "G+", + "H", + "H" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_33" + ] + ], + "lorentz_structures": [ + "SSSS1" + ], + "name": "V_6", + "particles": [ + "H", + "H", + "H", + "H" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_68" + ] + ], + "lorentz_structures": [ + "SSS1" + ], + "name": "V_7", + "particles": [ + "G0", + "G0", + "H" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_68" + ] + ], + "lorentz_structures": [ + "SSS1" + ], + "name": "V_8", + "particles": [ + "G-", + "G+", + "H" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_69" + ] + ], + "lorentz_structures": [ + "SSS1" + ], + "name": "V_9", + "particles": [ + "H", + "H", + "H" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_6" + ] + ], + "lorentz_structures": [ + "VVSS1" + ], + "name": "V_10", + "particles": [ + "a", + "a", + "G-", + "G+" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_3" + ] + ], + "lorentz_structures": [ + "VSS1" + ], + "name": "V_11", + "particles": [ + "a", + "G-", + "G+" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_3" + ] + ], + "lorentz_structures": [ + "UUV1" + ], + "name": "V_12", + "particles": [ + "ghA", + "ghWm~", + "W-" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_4" + ] + ], + "lorentz_structures": [ + "UUV1" + ], + "name": "V_13", + "particles": [ + "ghA", + "ghWp~", + "W+" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_75" + ] + ], + "lorentz_structures": [ + "UUS1" + ], + "name": "V_14", + "particles": [ + "ghWm", + "ghA~", + "G+" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_3" + ] + ], + "lorentz_structures": [ + "UUV1" + ], + "name": "V_15", + "particles": [ + "ghWm", + "ghA~", + "W+" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_70" + ] + ], + "lorentz_structures": [ + "UUS1" + ], + "name": "V_16", + "particles": [ + "ghWm", + "ghWm~", + "G0" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_71" + ] + ], + "lorentz_structures": [ + "UUS1" + ], + "name": "V_17", + "particles": [ + "ghWm", + "ghWm~", + "H" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_4" + ] + ], + "lorentz_structures": [ + "UUV1" + ], + "name": "V_18", + "particles": [ + "ghWm", + "ghWm~", + "a" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_53" + ] + ], + "lorentz_structures": [ + "UUV1" + ], + "name": "V_19", + "particles": [ + "ghWm", + "ghWm~", + "Z" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_78" + ] + ], + "lorentz_structures": [ + "UUS1" + ], + "name": "V_20", + "particles": [ + "ghWm", + "ghZ~", + "G+" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_52" + ] + ], + "lorentz_structures": [ + "UUV1" + ], + "name": "V_21", + "particles": [ + "ghWm", + "ghZ~", + "W+" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_74" + ] + ], + "lorentz_structures": [ + "UUS1" + ], + "name": "V_22", + "particles": [ + "ghWp", + "ghA~", + "G-" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_4" + ] + ], + "lorentz_structures": [ + "UUV1" + ], + "name": "V_23", + "particles": [ + "ghWp", + "ghA~", + "W-" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_73" + ] + ], + "lorentz_structures": [ + "UUS1" + ], + "name": "V_24", + "particles": [ + "ghWp", + "ghWp~", + "G0" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_71" + ] + ], + "lorentz_structures": [ + "UUS1" + ], + "name": "V_25", + "particles": [ + "ghWp", + "ghWp~", + "H" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_3" + ] + ], + "lorentz_structures": [ + "UUV1" + ], + "name": "V_26", + "particles": [ + "ghWp", + "ghWp~", + "a" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_52" + ] + ], + "lorentz_structures": [ + "UUV1" + ], + "name": "V_27", + "particles": [ + "ghWp", + "ghWp~", + "Z" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_77" + ] + ], + "lorentz_structures": [ + "UUS1" + ], + "name": "V_28", + "particles": [ + "ghWp", + "ghZ~", + "G-" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_53" + ] + ], + "lorentz_structures": [ + "UUV1" + ], + "name": "V_29", + "particles": [ + "ghWp", + "ghZ~", + "W-" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_79" + ] + ], + "lorentz_structures": [ + "UUS1" + ], + "name": "V_30", + "particles": [ + "ghZ", + "ghWm~", + "G-" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_52" + ] + ], + "lorentz_structures": [ + "UUV1" + ], + "name": "V_31", + "particles": [ + "ghZ", + "ghWm~", + "W-" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_76" + ] + ], + "lorentz_structures": [ + "UUS1" + ], + "name": "V_32", + "particles": [ + "ghZ", + "ghWp~", + "G+" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_53" + ] + ], + "lorentz_structures": [ + "UUV1" + ], + "name": "V_33", + "particles": [ + "ghZ", + "ghWp~", + "W+" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_80" + ] + ], + "lorentz_structures": [ + "UUS1" + ], + "name": "V_34", + "particles": [ + "ghZ", + "ghZ~", + "H" + ] + }, + { + "color_structures": [ + "UFO::{}::f(3,1,2)" + ], + "couplings": [ + [ + "GC_10" + ] + ], + "lorentz_structures": [ + "UUV1" + ], + "name": "V_35", + "particles": [ + "ghG", + "ghG~", + "g" + ] + }, + { + "color_structures": [ + "UFO::{}::f(1,2,3)" + ], + "couplings": [ + [ + "GC_10" + ] + ], + "lorentz_structures": [ + "VVV1" + ], + "name": "V_36", + "particles": [ + "g", + "g", + "g" + ] + }, + { + "color_structures": [ + "UFO::{}::f(-1,1,2)*UFO::{}::f(3,4,-1)", + "UFO::{}::f(-1,1,3)*UFO::{}::f(2,4,-1)", + "UFO::{}::f(-1,1,4)*UFO::{}::f(2,3,-1)" + ], + "couplings": [ + [ + "GC_12", + null, + null + ], + [ + null, + "GC_12", + null + ], + [ + null, + null, + "GC_12" + ] + ], + "lorentz_structures": [ + "VVVV1", + "VVVV3", + "VVVV4" + ], + "name": "V_37", + "particles": [ + "g", + "g", + "g", + "g" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_55" + ] + ], + "lorentz_structures": [ + "VVSS1" + ], + "name": "V_38", + "particles": [ + "a", + "W-", + "G0", + "G+" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_54" + ] + ], + "lorentz_structures": [ + "VVSS1" + ], + "name": "V_39", + "particles": [ + "a", + "W-", + "G+", + "H" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_74" + ] + ], + "lorentz_structures": [ + "VVS1" + ], + "name": "V_40", + "particles": [ + "a", + "W-", + "G+" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_39" + ] + ], + "lorentz_structures": [ + "VSS1" + ], + "name": "V_41", + "particles": [ + "W-", + "G0", + "G+" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_37" + ] + ], + "lorentz_structures": [ + "VSS1" + ], + "name": "V_42", + "particles": [ + "W-", + "G+", + "H" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_4" + ] + ], + "lorentz_structures": [ + "VVV1" + ], + "name": "V_43", + "particles": [ + "a", + "W-", + "W+" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_55" + ] + ], + "lorentz_structures": [ + "VVSS1" + ], + "name": "V_44", + "particles": [ + "a", + "W+", + "G0", + "G-" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_56" + ] + ], + "lorentz_structures": [ + "VVSS1" + ], + "name": "V_45", + "particles": [ + "a", + "W+", + "G-", + "H" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_75" + ] + ], + "lorentz_structures": [ + "VVS1" + ], + "name": "V_46", + "particles": [ + "a", + "W+", + "G-" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_38" + ] + ], + "lorentz_structures": [ + "VSS1" + ], + "name": "V_47", + "particles": [ + "W+", + "G0", + "G-" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_37" + ] + ], + "lorentz_structures": [ + "VSS1" + ], + "name": "V_48", + "particles": [ + "W+", + "G-", + "H" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_34" + ] + ], + "lorentz_structures": [ + "VVSS1" + ], + "name": "V_49", + "particles": [ + "W-", + "W+", + "G0", + "G0" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_34" + ] + ], + "lorentz_structures": [ + "VVSS1" + ], + "name": "V_50", + "particles": [ + "W-", + "W+", + "G-", + "G+" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_34" + ] + ], + "lorentz_structures": [ + "VVSS1" + ], + "name": "V_51", + "particles": [ + "W-", + "W+", + "H", + "H" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_72" + ] + ], + "lorentz_structures": [ + "VVS1" + ], + "name": "V_52", + "particles": [ + "W-", + "W+", + "H" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_5" + ] + ], + "lorentz_structures": [ + "VVVV2" + ], + "name": "V_53", + "particles": [ + "a", + "a", + "W-", + "W+" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_53" + ] + ], + "lorentz_structures": [ + "VVV1" + ], + "name": "V_54", + "particles": [ + "W-", + "W+", + "Z" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_35" + ] + ], + "lorentz_structures": [ + "VVVV2" + ], + "name": "V_55", + "particles": [ + "W-", + "W-", + "W+", + "W+" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_63" + ] + ], + "lorentz_structures": [ + "VVSS1" + ], + "name": "V_56", + "particles": [ + "a", + "Z", + "G-", + "G+" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_60" + ] + ], + "lorentz_structures": [ + "VSS1" + ], + "name": "V_57", + "particles": [ + "Z", + "G0", + "H" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_61" + ] + ], + "lorentz_structures": [ + "VSS1" + ], + "name": "V_58", + "particles": [ + "Z", + "G-", + "G+" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_8" + ] + ], + "lorentz_structures": [ + "VVSS1" + ], + "name": "V_59", + "particles": [ + "W-", + "Z", + "G0", + "G+" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_9" + ] + ], + "lorentz_structures": [ + "VVSS1" + ], + "name": "V_60", + "particles": [ + "W-", + "Z", + "G+", + "H" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_67" + ] + ], + "lorentz_structures": [ + "VVS1" + ], + "name": "V_61", + "particles": [ + "W-", + "Z", + "G+" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_8" + ] + ], + "lorentz_structures": [ + "VVSS1" + ], + "name": "V_62", + "particles": [ + "W+", + "Z", + "G0", + "G-" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_7" + ] + ], + "lorentz_structures": [ + "VVSS1" + ], + "name": "V_63", + "particles": [ + "W+", + "Z", + "G-", + "H" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_66" + ] + ], + "lorentz_structures": [ + "VVS1" + ], + "name": "V_64", + "particles": [ + "W+", + "Z", + "G-" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_57" + ] + ], + "lorentz_structures": [ + "VVVV5" + ], + "name": "V_65", + "particles": [ + "a", + "W-", + "W+", + "Z" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_65" + ] + ], + "lorentz_structures": [ + "VVSS1" + ], + "name": "V_66", + "particles": [ + "Z", + "Z", + "G0", + "G0" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_64" + ] + ], + "lorentz_structures": [ + "VVSS1" + ], + "name": "V_67", + "particles": [ + "Z", + "Z", + "G-", + "G+" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_65" + ] + ], + "lorentz_structures": [ + "VVSS1" + ], + "name": "V_68", + "particles": [ + "Z", + "Z", + "H", + "H" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_81" + ] + ], + "lorentz_structures": [ + "VVS1" + ], + "name": "V_69", + "particles": [ + "Z", + "Z", + "H" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_36" + ] + ], + "lorentz_structures": [ + "VVVV2" + ], + "name": "V_70", + "particles": [ + "W-", + "W+", + "Z", + "Z" + ] + }, + { + "color_structures": [ + "UFO::{}::Identity(1,2)" + ], + "couplings": [ + [ + "GC_1" + ] + ], + "lorentz_structures": [ + "FFV1" + ], + "name": "V_71", + "particles": [ + "d~", + "d", + "a" + ] + }, + { + "color_structures": [ + "UFO::{}::Identity(1,2)" + ], + "couplings": [ + [ + "GC_1" + ] + ], + "lorentz_structures": [ + "FFV1" + ], + "name": "V_72", + "particles": [ + "s~", + "s", + "a" + ] + }, + { + "color_structures": [ + "UFO::{}::Identity(1,2)" + ], + "couplings": [ + [ + "GC_1" + ] + ], + "lorentz_structures": [ + "FFV1" + ], + "name": "V_73", + "particles": [ + "b~", + "b", + "a" + ] + }, + { + "color_structures": [ + "UFO::{}::T(3,2,1)" + ], + "couplings": [ + [ + "GC_11" + ] + ], + "lorentz_structures": [ + "FFV1" + ], + "name": "V_74", + "particles": [ + "d~", + "d", + "g" + ] + }, + { + "color_structures": [ + "UFO::{}::T(3,2,1)" + ], + "couplings": [ + [ + "GC_11" + ] + ], + "lorentz_structures": [ + "FFV1" + ], + "name": "V_75", + "particles": [ + "s~", + "s", + "g" + ] + }, + { + "color_structures": [ + "UFO::{}::T(3,2,1)" + ], + "couplings": [ + [ + "GC_11" + ] + ], + "lorentz_structures": [ + "FFV1" + ], + "name": "V_76", + "particles": [ + "b~", + "b", + "g" + ] + }, + { + "color_structures": [ + "UFO::{}::Identity(1,2)" + ], + "couplings": [ + [ + "GC_82" + ] + ], + "lorentz_structures": [ + "FFS2" + ], + "name": "V_77", + "particles": [ + "b~", + "b", + "G0" + ] + }, + { + "color_structures": [ + "UFO::{}::Identity(1,2)" + ], + "couplings": [ + [ + "GC_83" + ] + ], + "lorentz_structures": [ + "FFS4" + ], + "name": "V_78", + "particles": [ + "b~", + "b", + "H" + ] + }, + { + "color_structures": [ + "UFO::{}::Identity(1,2)" + ], + "couplings": [ + [ + "GC_50", + "GC_58" + ] + ], + "lorentz_structures": [ + "FFV2", + "FFV3" + ], + "name": "V_79", + "particles": [ + "d~", + "d", + "Z" + ] + }, + { + "color_structures": [ + "UFO::{}::Identity(1,2)" + ], + "couplings": [ + [ + "GC_50", + "GC_58" + ] + ], + "lorentz_structures": [ + "FFV2", + "FFV3" + ], + "name": "V_80", + "particles": [ + "s~", + "s", + "Z" + ] + }, + { + "color_structures": [ + "UFO::{}::Identity(1,2)" + ], + "couplings": [ + [ + "GC_50", + "GC_58" + ] + ], + "lorentz_structures": [ + "FFV2", + "FFV3" + ], + "name": "V_81", + "particles": [ + "b~", + "b", + "Z" + ] + }, + { + "color_structures": [ + "UFO::{}::Identity(1,2)" + ], + "couplings": [ + [ + "GC_16" + ] + ], + "lorentz_structures": [ + "FFS3" + ], + "name": "V_82", + "particles": [ + "c~", + "d", + "G+" + ] + }, + { + "color_structures": [ + "UFO::{}::Identity(1,2)" + ], + "couplings": [ + [ + "GC_17" + ] + ], + "lorentz_structures": [ + "FFS3" + ], + "name": "V_83", + "particles": [ + "t~", + "d", + "G+" + ] + }, + { + "color_structures": [ + "UFO::{}::Identity(1,2)" + ], + "couplings": [ + [ + "GC_18" + ] + ], + "lorentz_structures": [ + "FFS3" + ], + "name": "V_84", + "particles": [ + "c~", + "s", + "G+" + ] + }, + { + "color_structures": [ + "UFO::{}::Identity(1,2)" + ], + "couplings": [ + [ + "GC_19" + ] + ], + "lorentz_structures": [ + "FFS3" + ], + "name": "V_85", + "particles": [ + "t~", + "s", + "G+" + ] + }, + { + "color_structures": [ + "UFO::{}::Identity(1,2)" + ], + "couplings": [ + [ + "GC_13" + ] + ], + "lorentz_structures": [ + "FFS1" + ], + "name": "V_86", + "particles": [ + "u~", + "b", + "G+" + ] + }, + { + "color_structures": [ + "UFO::{}::Identity(1,2)" + ], + "couplings": [ + [ + "GC_14", + "GC_20" + ] + ], + "lorentz_structures": [ + "FFS1", + "FFS3" + ], + "name": "V_87", + "particles": [ + "c~", + "b", + "G+" + ] + }, + { + "color_structures": [ + "UFO::{}::Identity(1,2)" + ], + "couplings": [ + [ + "GC_15", + "GC_21" + ] + ], + "lorentz_structures": [ + "FFS1", + "FFS3" + ], + "name": "V_88", + "particles": [ + "t~", + "b", + "G+" + ] + }, + { + "color_structures": [ + "UFO::{}::Identity(1,2)" + ], + "couplings": [ + [ + "GC_100" + ] + ], + "lorentz_structures": [ + "FFV2" + ], + "name": "V_89", + "particles": [ + "u~", + "d", + "W+" + ] + }, + { + "color_structures": [ + "UFO::{}::Identity(1,2)" + ], + "couplings": [ + [ + "GC_103" + ] + ], + "lorentz_structures": [ + "FFV2" + ], + "name": "V_90", + "particles": [ + "c~", + "d", + "W+" + ] + }, + { + "color_structures": [ + "UFO::{}::Identity(1,2)" + ], + "couplings": [ + [ + "GC_106" + ] + ], + "lorentz_structures": [ + "FFV2" + ], + "name": "V_91", + "particles": [ + "t~", + "d", + "W+" + ] + }, + { + "color_structures": [ + "UFO::{}::Identity(1,2)" + ], + "couplings": [ + [ + "GC_101" + ] + ], + "lorentz_structures": [ + "FFV2" + ], + "name": "V_92", + "particles": [ + "u~", + "s", + "W+" + ] + }, + { + "color_structures": [ + "UFO::{}::Identity(1,2)" + ], + "couplings": [ + [ + "GC_104" + ] + ], + "lorentz_structures": [ + "FFV2" + ], + "name": "V_93", + "particles": [ + "c~", + "s", + "W+" + ] + }, + { + "color_structures": [ + "UFO::{}::Identity(1,2)" + ], + "couplings": [ + [ + "GC_107" + ] + ], + "lorentz_structures": [ + "FFV2" + ], + "name": "V_94", + "particles": [ + "t~", + "s", + "W+" + ] + }, + { + "color_structures": [ + "UFO::{}::Identity(1,2)" + ], + "couplings": [ + [ + "GC_102" + ] + ], + "lorentz_structures": [ + "FFV2" + ], + "name": "V_95", + "particles": [ + "u~", + "b", + "W+" + ] + }, + { + "color_structures": [ + "UFO::{}::Identity(1,2)" + ], + "couplings": [ + [ + "GC_105" + ] + ], + "lorentz_structures": [ + "FFV2" + ], + "name": "V_96", + "particles": [ + "c~", + "b", + "W+" + ] + }, + { + "color_structures": [ + "UFO::{}::Identity(1,2)" + ], + "couplings": [ + [ + "GC_108" + ] + ], + "lorentz_structures": [ + "FFV2" + ], + "name": "V_97", + "particles": [ + "t~", + "b", + "W+" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_3" + ] + ], + "lorentz_structures": [ + "FFV1" + ], + "name": "V_98", + "particles": [ + "e+", + "e-", + "a" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_3" + ] + ], + "lorentz_structures": [ + "FFV1" + ], + "name": "V_99", + "particles": [ + "mu+", + "mu-", + "a" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_3" + ] + ], + "lorentz_structures": [ + "FFV1" + ], + "name": "V_100", + "particles": [ + "ta+", + "ta-", + "a" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_88" + ] + ], + "lorentz_structures": [ + "FFS2" + ], + "name": "V_101", + "particles": [ + "e+", + "e-", + "G0" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_92" + ] + ], + "lorentz_structures": [ + "FFS2" + ], + "name": "V_102", + "particles": [ + "mu+", + "mu-", + "G0" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_98" + ] + ], + "lorentz_structures": [ + "FFS2" + ], + "name": "V_103", + "particles": [ + "ta+", + "ta-", + "G0" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_89" + ] + ], + "lorentz_structures": [ + "FFS4" + ], + "name": "V_104", + "particles": [ + "e+", + "e-", + "H" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_93" + ] + ], + "lorentz_structures": [ + "FFS4" + ], + "name": "V_105", + "particles": [ + "mu+", + "mu-", + "H" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_99" + ] + ], + "lorentz_structures": [ + "FFS4" + ], + "name": "V_106", + "particles": [ + "ta+", + "ta-", + "H" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_50", + "GC_59" + ] + ], + "lorentz_structures": [ + "FFV2", + "FFV4" + ], + "name": "V_107", + "particles": [ + "e+", + "e-", + "Z" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_50", + "GC_59" + ] + ], + "lorentz_structures": [ + "FFV2", + "FFV4" + ], + "name": "V_108", + "particles": [ + "mu+", + "mu-", + "Z" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_50", + "GC_59" + ] + ], + "lorentz_structures": [ + "FFV2", + "FFV4" + ], + "name": "V_109", + "particles": [ + "ta+", + "ta-", + "Z" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_87" + ] + ], + "lorentz_structures": [ + "FFS1" + ], + "name": "V_110", + "particles": [ + "ve~", + "e-", + "G+" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_91" + ] + ], + "lorentz_structures": [ + "FFS1" + ], + "name": "V_111", + "particles": [ + "vm~", + "mu-", + "G+" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_97" + ] + ], + "lorentz_structures": [ + "FFS1" + ], + "name": "V_112", + "particles": [ + "vt~", + "ta-", + "G+" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_40" + ] + ], + "lorentz_structures": [ + "FFV2" + ], + "name": "V_113", + "particles": [ + "ve~", + "e-", + "W+" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_40" + ] + ], + "lorentz_structures": [ + "FFV2" + ], + "name": "V_114", + "particles": [ + "vm~", + "mu-", + "W+" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_40" + ] + ], + "lorentz_structures": [ + "FFV2" + ], + "name": "V_115", + "particles": [ + "vt~", + "ta-", + "W+" + ] + }, + { + "color_structures": [ + "UFO::{}::Identity(1,2)" + ], + "couplings": [ + [ + "GC_28" + ] + ], + "lorentz_structures": [ + "FFS3" + ], + "name": "V_116", + "particles": [ + "b~", + "u", + "G-" + ] + }, + { + "color_structures": [ + "UFO::{}::Identity(1,2)" + ], + "couplings": [ + [ + "GC_22" + ] + ], + "lorentz_structures": [ + "FFS1" + ], + "name": "V_117", + "particles": [ + "d~", + "c", + "G-" + ] + }, + { + "color_structures": [ + "UFO::{}::Identity(1,2)" + ], + "couplings": [ + [ + "GC_23" + ] + ], + "lorentz_structures": [ + "FFS1" + ], + "name": "V_118", + "particles": [ + "s~", + "c", + "G-" + ] + }, + { + "color_structures": [ + "UFO::{}::Identity(1,2)" + ], + "couplings": [ + [ + "GC_24", + "GC_29" + ] + ], + "lorentz_structures": [ + "FFS1", + "FFS3" + ], + "name": "V_119", + "particles": [ + "b~", + "c", + "G-" + ] + }, + { + "color_structures": [ + "UFO::{}::Identity(1,2)" + ], + "couplings": [ + [ + "GC_25" + ] + ], + "lorentz_structures": [ + "FFS1" + ], + "name": "V_120", + "particles": [ + "d~", + "t", + "G-" + ] + }, + { + "color_structures": [ + "UFO::{}::Identity(1,2)" + ], + "couplings": [ + [ + "GC_26" + ] + ], + "lorentz_structures": [ + "FFS1" + ], + "name": "V_121", + "particles": [ + "s~", + "t", + "G-" + ] + }, + { + "color_structures": [ + "UFO::{}::Identity(1,2)" + ], + "couplings": [ + [ + "GC_27", + "GC_30" + ] + ], + "lorentz_structures": [ + "FFS1", + "FFS3" + ], + "name": "V_122", + "particles": [ + "b~", + "t", + "G-" + ] + }, + { + "color_structures": [ + "UFO::{}::Identity(1,2)" + ], + "couplings": [ + [ + "GC_41" + ] + ], + "lorentz_structures": [ + "FFV2" + ], + "name": "V_123", + "particles": [ + "d~", + "u", + "W-" + ] + }, + { + "color_structures": [ + "UFO::{}::Identity(1,2)" + ], + "couplings": [ + [ + "GC_42" + ] + ], + "lorentz_structures": [ + "FFV2" + ], + "name": "V_124", + "particles": [ + "s~", + "u", + "W-" + ] + }, + { + "color_structures": [ + "UFO::{}::Identity(1,2)" + ], + "couplings": [ + [ + "GC_43" + ] + ], + "lorentz_structures": [ + "FFV2" + ], + "name": "V_125", + "particles": [ + "b~", + "u", + "W-" + ] + }, + { + "color_structures": [ + "UFO::{}::Identity(1,2)" + ], + "couplings": [ + [ + "GC_44" + ] + ], + "lorentz_structures": [ + "FFV2" + ], + "name": "V_126", + "particles": [ + "d~", + "c", + "W-" + ] + }, + { + "color_structures": [ + "UFO::{}::Identity(1,2)" + ], + "couplings": [ + [ + "GC_45" + ] + ], + "lorentz_structures": [ + "FFV2" + ], + "name": "V_127", + "particles": [ + "s~", + "c", + "W-" + ] + }, + { + "color_structures": [ + "UFO::{}::Identity(1,2)" + ], + "couplings": [ + [ + "GC_46" + ] + ], + "lorentz_structures": [ + "FFV2" + ], + "name": "V_128", + "particles": [ + "b~", + "c", + "W-" + ] + }, + { + "color_structures": [ + "UFO::{}::Identity(1,2)" + ], + "couplings": [ + [ + "GC_47" + ] + ], + "lorentz_structures": [ + "FFV2" + ], + "name": "V_129", + "particles": [ + "d~", + "t", + "W-" + ] + }, + { + "color_structures": [ + "UFO::{}::Identity(1,2)" + ], + "couplings": [ + [ + "GC_48" + ] + ], + "lorentz_structures": [ + "FFV2" + ], + "name": "V_130", + "particles": [ + "s~", + "t", + "W-" + ] + }, + { + "color_structures": [ + "UFO::{}::Identity(1,2)" + ], + "couplings": [ + [ + "GC_49" + ] + ], + "lorentz_structures": [ + "FFV2" + ], + "name": "V_131", + "particles": [ + "b~", + "t", + "W-" + ] + }, + { + "color_structures": [ + "UFO::{}::Identity(1,2)" + ], + "couplings": [ + [ + "GC_2" + ] + ], + "lorentz_structures": [ + "FFV1" + ], + "name": "V_132", + "particles": [ + "u~", + "u", + "a" + ] + }, + { + "color_structures": [ + "UFO::{}::Identity(1,2)" + ], + "couplings": [ + [ + "GC_2" + ] + ], + "lorentz_structures": [ + "FFV1" + ], + "name": "V_133", + "particles": [ + "c~", + "c", + "a" + ] + }, + { + "color_structures": [ + "UFO::{}::Identity(1,2)" + ], + "couplings": [ + [ + "GC_2" + ] + ], + "lorentz_structures": [ + "FFV1" + ], + "name": "V_134", + "particles": [ + "t~", + "t", + "a" + ] + }, + { + "color_structures": [ + "UFO::{}::T(3,2,1)" + ], + "couplings": [ + [ + "GC_11" + ] + ], + "lorentz_structures": [ + "FFV1" + ], + "name": "V_135", + "particles": [ + "u~", + "u", + "g" + ] + }, + { + "color_structures": [ + "UFO::{}::T(3,2,1)" + ], + "couplings": [ + [ + "GC_11" + ] + ], + "lorentz_structures": [ + "FFV1" + ], + "name": "V_136", + "particles": [ + "c~", + "c", + "g" + ] + }, + { + "color_structures": [ + "UFO::{}::T(3,2,1)" + ], + "couplings": [ + [ + "GC_11" + ] + ], + "lorentz_structures": [ + "FFV1" + ], + "name": "V_137", + "particles": [ + "t~", + "t", + "g" + ] + }, + { + "color_structures": [ + "UFO::{}::Identity(1,2)" + ], + "couplings": [ + [ + "GC_85" + ] + ], + "lorentz_structures": [ + "FFS2" + ], + "name": "V_138", + "particles": [ + "c~", + "c", + "G0" + ] + }, + { + "color_structures": [ + "UFO::{}::Identity(1,2)" + ], + "couplings": [ + [ + "GC_95" + ] + ], + "lorentz_structures": [ + "FFS2" + ], + "name": "V_139", + "particles": [ + "t~", + "t", + "G0" + ] + }, + { + "color_structures": [ + "UFO::{}::Identity(1,2)" + ], + "couplings": [ + [ + "GC_84" + ] + ], + "lorentz_structures": [ + "FFS4" + ], + "name": "V_140", + "particles": [ + "c~", + "c", + "H" + ] + }, + { + "color_structures": [ + "UFO::{}::Identity(1,2)" + ], + "couplings": [ + [ + "GC_94" + ] + ], + "lorentz_structures": [ + "FFS4" + ], + "name": "V_141", + "particles": [ + "t~", + "t", + "H" + ] + }, + { + "color_structures": [ + "UFO::{}::Identity(1,2)" + ], + "couplings": [ + [ + "GC_51", + "GC_58" + ] + ], + "lorentz_structures": [ + "FFV2", + "FFV5" + ], + "name": "V_142", + "particles": [ + "u~", + "u", + "Z" + ] + }, + { + "color_structures": [ + "UFO::{}::Identity(1,2)" + ], + "couplings": [ + [ + "GC_51", + "GC_58" + ] + ], + "lorentz_structures": [ + "FFV2", + "FFV5" + ], + "name": "V_143", + "particles": [ + "c~", + "c", + "Z" + ] + }, + { + "color_structures": [ + "UFO::{}::Identity(1,2)" + ], + "couplings": [ + [ + "GC_51", + "GC_58" + ] + ], + "lorentz_structures": [ + "FFV2", + "FFV5" + ], + "name": "V_144", + "particles": [ + "t~", + "t", + "Z" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_86" + ] + ], + "lorentz_structures": [ + "FFS3" + ], + "name": "V_145", + "particles": [ + "e+", + "ve", + "G-" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_90" + ] + ], + "lorentz_structures": [ + "FFS3" + ], + "name": "V_146", + "particles": [ + "mu+", + "vm", + "G-" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_96" + ] + ], + "lorentz_structures": [ + "FFS3" + ], + "name": "V_147", + "particles": [ + "ta+", + "vt", + "G-" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_40" + ] + ], + "lorentz_structures": [ + "FFV2" + ], + "name": "V_148", + "particles": [ + "e+", + "ve", + "W-" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_40" + ] + ], + "lorentz_structures": [ + "FFV2" + ], + "name": "V_149", + "particles": [ + "mu+", + "vm", + "W-" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_40" + ] + ], + "lorentz_structures": [ + "FFV2" + ], + "name": "V_150", + "particles": [ + "ta+", + "vt", + "W-" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_62" + ] + ], + "lorentz_structures": [ + "FFV2" + ], + "name": "V_151", + "particles": [ + "ve~", + "ve", + "Z" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_62" + ] + ], + "lorentz_structures": [ + "FFV2" + ], + "name": "V_152", + "particles": [ + "vm~", + "vm", + "Z" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_62" + ] + ], + "lorentz_structures": [ + "FFV2" + ], + "name": "V_153", + "particles": [ + "vt~", + "vt", + "Z" + ] + } + ] +} \ No newline at end of file diff --git a/pyamplicol/src/pyamplicol/assets/models/json/sm/sm_wrapped_indices.json b/pyamplicol/src/pyamplicol/assets/models/json/sm/sm_wrapped_indices.json new file mode 100644 index 0000000000..2996cf8183 --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/models/json/sm/sm_wrapped_indices.json @@ -0,0 +1,6721 @@ +{ + "couplings": [ + { + "expression": "-1𝑖/3*UFO::{}::ee", + "name": "GC_1", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.0, + -0.10265126349497912 + ] + }, + { + "expression": "2𝑖/3*UFO::{}::ee", + "name": "GC_2", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.0, + 0.20530252698995824 + ] + }, + { + "expression": "-1𝑖*UFO::{}::ee", + "name": "GC_3", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.0, + -0.3079537904849374 + ] + }, + { + "expression": "1𝑖*UFO::{}::ee", + "name": "GC_4", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.0, + 0.3079537904849374 + ] + }, + { + "expression": "1𝑖*UFO::{}::ee^2", + "name": "GC_5", + "orders": [ + [ + "QED", + 2 + ] + ], + "value": [ + 0.0, + 0.0948355370740407 + ] + }, + { + "expression": "2𝑖*UFO::{}::ee^2", + "name": "GC_6", + "orders": [ + [ + "QED", + 2 + ] + ], + "value": [ + 0.0, + 0.1896710741480814 + ] + }, + { + "expression": "-1/2*UFO::{}::cw^(-1)*UFO::{}::ee^2", + "name": "GC_7", + "orders": [ + [ + "QED", + 2 + ] + ], + "value": [ + -0.053767535985328165, + 0.0 + ] + }, + { + "expression": "1𝑖/2*UFO::{}::cw^(-1)*UFO::{}::ee^2", + "name": "GC_8", + "orders": [ + [ + "QED", + 2 + ] + ], + "value": [ + 0.0, + 0.053767535985328165 + ] + }, + { + "expression": "1/2*UFO::{}::cw^(-1)*UFO::{}::ee^2", + "name": "GC_9", + "orders": [ + [ + "QED", + 2 + ] + ], + "value": [ + 0.053767535985328165, + 0.0 + ] + }, + { + "expression": "-1*UFO::{}::G", + "name": "GC_10", + "orders": [ + [ + "QCD", + 1 + ] + ], + "value": [ + -1.2177157847767195, + 0.0 + ] + }, + { + "expression": "1𝑖*UFO::{}::G", + "name": "GC_11", + "orders": [ + [ + "QCD", + 1 + ] + ], + "value": [ + 0.0, + 1.2177157847767195 + ] + }, + { + "expression": "1𝑖*UFO::{}::G^2", + "name": "GC_12", + "orders": [ + [ + "QCD", + 2 + ] + ], + "value": [ + 0.0, + 1.4828317324943818 + ] + }, + { + "expression": "UFO::{}::I1x31", + "name": "GC_13", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 2.9424734175077786e-05, + 7.601389661895094e-05 + ] + }, + { + "expression": "UFO::{}::I1x32", + "name": "GC_14", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.0009894125736418036, + 0.0 + ] + }, + { + "expression": "UFO::{}::I1x33", + "name": "GC_15", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.024123686777011728, + 0.0 + ] + }, + { + "expression": "-1*UFO::{}::I2x12", + "name": "GC_16", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.0016434606240936052, + 0.0 + ] + }, + { + "expression": "-1*UFO::{}::I2x13", + "name": "GC_17", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + -0.007578355147768138, + -0.0029772109509089116 + ] + }, + { + "expression": "-1*UFO::{}::I2x22", + "name": "GC_18", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + -0.007109407543268448, + 0.0 + ] + }, + { + "expression": "-1*UFO::{}::I2x23", + "name": "GC_19", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.0387519924676373, + 0.0 + ] + }, + { + "expression": "-1*UFO::{}::I2x32", + "name": "GC_20", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + -0.00029917951631549774, + 0.0 + ] + }, + { + "expression": "-1*UFO::{}::I2x33", + "name": "GC_21", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + -0.9448443987662926, + 0.0 + ] + }, + { + "expression": "UFO::{}::I3x21", + "name": "GC_22", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + -0.0016434606240936052, + 0.0 + ] + }, + { + "expression": "UFO::{}::I3x22", + "name": "GC_23", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.007109407543268448, + 0.0 + ] + }, + { + "expression": "UFO::{}::I3x23", + "name": "GC_24", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.00029917951631549774, + 0.0 + ] + }, + { + "expression": "UFO::{}::I3x31", + "name": "GC_25", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.007578355147768138, + -0.0029772109509089116 + ] + }, + { + "expression": "UFO::{}::I3x32", + "name": "GC_26", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + -0.0387519924676373, + 0.0 + ] + }, + { + "expression": "UFO::{}::I3x33", + "name": "GC_27", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.9448443987662926, + 0.0 + ] + }, + { + "expression": "-1*UFO::{}::I4x13", + "name": "GC_28", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + -2.9424734175077786e-05, + 7.601389661895094e-05 + ] + }, + { + "expression": "-1*UFO::{}::I4x23", + "name": "GC_29", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + -0.0009894125736418036, + 0.0 + ] + }, + { + "expression": "-1*UFO::{}::I4x33", + "name": "GC_30", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + -0.024123686777011728, + 0.0 + ] + }, + { + "expression": "-2𝑖*UFO::{}::lam", + "name": "GC_31", + "orders": [ + [ + "QED", + 2 + ] + ], + "value": [ + 0.0, + -0.25773821203380554 + ] + }, + { + "expression": "-4𝑖*UFO::{}::lam", + "name": "GC_32", + "orders": [ + [ + "QED", + 2 + ] + ], + "value": [ + 0.0, + -0.5154764240676111 + ] + }, + { + "expression": "-6𝑖*UFO::{}::lam", + "name": "GC_33", + "orders": [ + [ + "QED", + 2 + ] + ], + "value": [ + 0.0, + -0.7732146361014166 + ] + }, + { + "expression": "1𝑖/2*UFO::{}::ee^2*UFO::{}::sw^(-2)", + "name": "GC_34", + "orders": [ + [ + "QED", + 2 + ] + ], + "value": [ + 0.0, + 0.2133566177614792 + ] + }, + { + "expression": "-1𝑖*UFO::{}::ee^2*UFO::{}::sw^(-2)", + "name": "GC_35", + "orders": [ + [ + "QED", + 2 + ] + ], + "value": [ + 0.0, + -0.4267132355229584 + ] + }, + { + "expression": "1𝑖*UFO::{}::cw^2*UFO::{}::ee^2*UFO::{}::sw^(-2)", + "name": "GC_36", + "orders": [ + [ + "QED", + 2 + ] + ], + "value": [ + 0.0, + 0.3318776984489177 + ] + }, + { + "expression": "-1/2*UFO::{}::ee*UFO::{}::sw^(-1)", + "name": "GC_37", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + -0.326616455312251, + 0.0 + ] + }, + { + "expression": "-1𝑖/2*UFO::{}::ee*UFO::{}::sw^(-1)", + "name": "GC_38", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.0, + -0.326616455312251 + ] + }, + { + "expression": "1𝑖/2*UFO::{}::ee*UFO::{}::sw^(-1)", + "name": "GC_39", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.0, + 0.326616455312251 + ] + }, + { + "expression": "(1/2)^(1/2)*1𝑖*UFO::{}::ee*UFO::{}::sw^(-1)", + "name": "GC_40", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.0, + 0.46190542079681124 + ] + }, + { + "expression": "(1/2)^(1/2)*1𝑖*UFO::{}::CKM1x1*UFO::{}::ee*UFO::{}::sw^(-1)", + "name": "GC_41", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.0, + 0.4501822404312442 + ] + }, + { + "expression": "(1/2)^(1/2)*1𝑖*UFO::{}::CKM1x2*UFO::{}::ee*UFO::{}::sw^(-1)", + "name": "GC_42", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.0, + 0.10406729130552157 + ] + }, + { + "expression": "(1/2)^(1/2)*1𝑖*UFO::{}::CKM1x3*UFO::{}::ee*UFO::{}::sw^(-1)", + "name": "GC_43", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.001455467036557634, + 0.0005634065947965036 + ] + }, + { + "expression": "(1/2)^(1/2)*1𝑖*UFO::{}::CKM2x1*UFO::{}::ee*UFO::{}::sw^(-1)", + "name": "GC_44", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.0, + -0.10406729130552157 + ] + }, + { + "expression": "(1/2)^(1/2)*1𝑖*UFO::{}::CKM2x2*UFO::{}::ee*UFO::{}::sw^(-1)", + "name": "GC_45", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.0, + 0.4501822404312442 + ] + }, + { + "expression": "(1/2)^(1/2)*1𝑖*UFO::{}::CKM2x3*UFO::{}::ee*UFO::{}::sw^(-1)", + "name": "GC_46", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.0, + 0.01894465947075628 + ] + }, + { + "expression": "(1/2)^(1/2)*1𝑖*UFO::{}::CKM3x1*UFO::{}::ee*UFO::{}::sw^(-1)", + "name": "GC_47", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.001455467036557634, + 0.003704825183964887 + ] + }, + { + "expression": "(1/2)^(1/2)*1𝑖*UFO::{}::CKM3x2*UFO::{}::ee*UFO::{}::sw^(-1)", + "name": "GC_48", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.0, + -0.01894465947075628 + ] + }, + { + "expression": "(1/2)^(1/2)*1𝑖*UFO::{}::CKM3x3*UFO::{}::ee*UFO::{}::sw^(-1)", + "name": "GC_49", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.0, + 0.46190542079681124 + ] + }, + { + "expression": "-1𝑖/2*UFO::{}::cw*UFO::{}::ee*UFO::{}::sw^(-1)", + "name": "GC_50", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.0, + -0.2880441365697789 + ] + }, + { + "expression": "1𝑖/2*UFO::{}::cw*UFO::{}::ee*UFO::{}::sw^(-1)", + "name": "GC_51", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.0, + 0.2880441365697789 + ] + }, + { + "expression": "-1𝑖*UFO::{}::cw*UFO::{}::ee*UFO::{}::sw^(-1)", + "name": "GC_52", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.0, + -0.5760882731395578 + ] + }, + { + "expression": "1𝑖*UFO::{}::cw*UFO::{}::ee*UFO::{}::sw^(-1)", + "name": "GC_53", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.0, + 0.5760882731395578 + ] + }, + { + "expression": "-1/2*UFO::{}::ee^2*UFO::{}::sw^(-1)", + "name": "GC_54", + "orders": [ + [ + "QED", + 2 + ] + ], + "value": [ + -0.10058277544816184, + 0.0 + ] + }, + { + "expression": "-1𝑖/2*UFO::{}::ee^2*UFO::{}::sw^(-1)", + "name": "GC_55", + "orders": [ + [ + "QED", + 2 + ] + ], + "value": [ + 0.0, + -0.10058277544816184 + ] + }, + { + "expression": "1/2*UFO::{}::ee^2*UFO::{}::sw^(-1)", + "name": "GC_56", + "orders": [ + [ + "QED", + 2 + ] + ], + "value": [ + 0.10058277544816184, + 0.0 + ] + }, + { + "expression": "-2𝑖*UFO::{}::cw*UFO::{}::ee^2*UFO::{}::sw^(-1)", + "name": "GC_57", + "orders": [ + [ + "QED", + 2 + ] + ], + "value": [ + 0.0, + -0.35481713473449755 + ] + }, + { + "expression": "-1𝑖/6*UFO::{}::cw^(-1)*UFO::{}::ee*UFO::{}::sw", + "name": "GC_58", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.0, + -0.027436633555365665 + ] + }, + { + "expression": "1𝑖/2*UFO::{}::cw^(-1)*UFO::{}::ee*UFO::{}::sw", + "name": "GC_59", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.0, + 0.082309900666097 + ] + }, + { + "expression": "-1/2*UFO::{}::cw*UFO::{}::ee*UFO::{}::sw^(-1)+-1/2*UFO::{}::cw^(-1)*UFO::{}::ee*UFO::{}::sw", + "name": "GC_60", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + -0.3703540372358759, + 0.0 + ] + }, + { + "expression": "-1𝑖/2*UFO::{}::cw*UFO::{}::ee*UFO::{}::sw^(-1)+1𝑖/2*UFO::{}::cw^(-1)*UFO::{}::ee*UFO::{}::sw", + "name": "GC_61", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.0, + -0.2057342359036819 + ] + }, + { + "expression": "1𝑖/2*UFO::{}::cw*UFO::{}::ee*UFO::{}::sw^(-1)+1𝑖/2*UFO::{}::cw^(-1)*UFO::{}::ee*UFO::{}::sw", + "name": "GC_62", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.0, + 0.3703540372358759 + ] + }, + { + "expression": "-1𝑖*UFO::{}::cw^(-1)*UFO::{}::ee^2*UFO::{}::sw+1𝑖*UFO::{}::cw*UFO::{}::ee^2*UFO::{}::sw^(-1)", + "name": "GC_63", + "orders": [ + [ + "QED", + 2 + ] + ], + "value": [ + 0.0, + 0.12671327555812228 + ] + }, + { + "expression": "-1𝑖*UFO::{}::ee^2+1𝑖/2*UFO::{}::cw^(-2)*UFO::{}::ee^2*UFO::{}::sw^2+1𝑖/2*UFO::{}::cw^2*UFO::{}::ee^2*UFO::{}::sw^(-2)", + "name": "GC_64", + "orders": [ + [ + "QED", + 2 + ] + ], + "value": [ + 0.0, + 0.08465315164574365 + ] + }, + { + "expression": "1𝑖*UFO::{}::ee^2+1𝑖/2*UFO::{}::cw^(-2)*UFO::{}::ee^2*UFO::{}::sw^2+1𝑖/2*UFO::{}::cw^2*UFO::{}::ee^2*UFO::{}::sw^(-2)", + "name": "GC_65", + "orders": [ + [ + "QED", + 2 + ] + ], + "value": [ + 0.0, + 0.27432422579382504 + ] + }, + { + "expression": "-1/2*UFO::{}::cw^(-1)*UFO::{}::ee^2*UFO::{}::vev", + "name": "GC_66", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + -13.23855980624142, + 0.0 + ] + }, + { + "expression": "1/2*UFO::{}::cw^(-1)*UFO::{}::ee^2*UFO::{}::vev", + "name": "GC_67", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 13.23855980624142, + 0.0 + ] + }, + { + "expression": "-2𝑖*UFO::{}::lam*UFO::{}::vev", + "name": "GC_68", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.0, + -63.459905160882585 + ] + }, + { + "expression": "-6𝑖*UFO::{}::lam*UFO::{}::vev", + "name": "GC_69", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.0, + -190.37971548264773 + ] + }, + { + "expression": "-1/4*UFO::{}::ee^2*UFO::{}::sw^(-2)*UFO::{}::vev", + "name": "GC_70", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + -26.266168725525, + 0.0 + ] + }, + { + "expression": "-1𝑖/4*UFO::{}::ee^2*UFO::{}::sw^(-2)*UFO::{}::vev", + "name": "GC_71", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.0, + -26.266168725525 + ] + }, + { + "expression": "1𝑖/2*UFO::{}::ee^2*UFO::{}::sw^(-2)*UFO::{}::vev", + "name": "GC_72", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.0, + 52.53233745105 + ] + }, + { + "expression": "1/4*UFO::{}::ee^2*UFO::{}::sw^(-2)*UFO::{}::vev", + "name": "GC_73", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 26.266168725525, + 0.0 + ] + }, + { + "expression": "-1/2*UFO::{}::ee^2*UFO::{}::sw^(-1)*UFO::{}::vev", + "name": "GC_74", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + -24.76533588244763, + 0.0 + ] + }, + { + "expression": "1/2*UFO::{}::ee^2*UFO::{}::sw^(-1)*UFO::{}::vev", + "name": "GC_75", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 24.76533588244763, + 0.0 + ] + }, + { + "expression": "-1/4*UFO::{}::cw*UFO::{}::ee^2*UFO::{}::sw^(-2)*UFO::{}::vev+-1/4*UFO::{}::cw^(-1)*UFO::{}::ee^2*UFO::{}::vev", + "name": "GC_76", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + -29.78350132701354, + 0.0 + ] + }, + { + "expression": "-1/4*UFO::{}::cw*UFO::{}::ee^2*UFO::{}::sw^(-2)*UFO::{}::vev+1/4*UFO::{}::cw^(-1)*UFO::{}::ee^2*UFO::{}::vev", + "name": "GC_77", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + -16.54494152077212, + 0.0 + ] + }, + { + "expression": "-1/4*UFO::{}::cw^(-1)*UFO::{}::ee^2*UFO::{}::vev+1/4*UFO::{}::cw*UFO::{}::ee^2*UFO::{}::sw^(-2)*UFO::{}::vev", + "name": "GC_78", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 16.54494152077212, + 0.0 + ] + }, + { + "expression": "1/4*UFO::{}::cw*UFO::{}::ee^2*UFO::{}::sw^(-2)*UFO::{}::vev+1/4*UFO::{}::cw^(-1)*UFO::{}::ee^2*UFO::{}::vev", + "name": "GC_79", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 29.78350132701354, + 0.0 + ] + }, + { + "expression": "-1𝑖/2*UFO::{}::ee^2*UFO::{}::vev+-1𝑖/4*UFO::{}::cw^(-2)*UFO::{}::ee^2*UFO::{}::sw^2*UFO::{}::vev+-1𝑖/4*UFO::{}::cw^2*UFO::{}::ee^2*UFO::{}::sw^(-2)*UFO::{}::vev", + "name": "GC_80", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.0, + -33.77184394746505 + ] + }, + { + "expression": "1𝑖*UFO::{}::ee^2*UFO::{}::vev+1𝑖/2*UFO::{}::cw^(-2)*UFO::{}::ee^2*UFO::{}::sw^2*UFO::{}::vev+1𝑖/2*UFO::{}::cw^2*UFO::{}::ee^2*UFO::{}::sw^(-2)*UFO::{}::vev", + "name": "GC_81", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.0, + 67.5436878949301 + ] + }, + { + "expression": "(1/2)^(1/2)*-1*UFO::{}::yb", + "name": "GC_82", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + -0.01705802250724524, + 0.0 + ] + }, + { + "expression": "(1/2)^(1/2)*-1𝑖*UFO::{}::yb", + "name": "GC_83", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.0, + -0.01705802250724524 + ] + }, + { + "expression": "(1/2)^(1/2)*-1𝑖*UFO::{}::yc", + "name": "GC_84", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.0, + -0.005158021091476537 + ] + }, + { + "expression": "(1/2)^(1/2)*UFO::{}::yc", + "name": "GC_85", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.005158021091476537, + 0.0 + ] + }, + { + "expression": "-1*UFO::{}::ye", + "name": "GC_86", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + -2.93504855786976e-06, + 0.0 + ] + }, + { + "expression": "UFO::{}::ye", + "name": "GC_87", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 2.93504855786976e-06, + 0.0 + ] + }, + { + "expression": "(1/2)^(1/2)*-1*UFO::{}::ye", + "name": "GC_88", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + -2.0753927383815043e-06, + 0.0 + ] + }, + { + "expression": "(1/2)^(1/2)*-1𝑖*UFO::{}::ye", + "name": "GC_89", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.0, + -2.0753927383815043e-06 + ] + }, + { + "expression": "-1*UFO::{}::ym", + "name": "GC_90", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + -0.0006068830344902521, + 0.0 + ] + }, + { + "expression": "UFO::{}::ym", + "name": "GC_91", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.0006068830344902521, + 0.0 + ] + }, + { + "expression": "(1/2)^(1/2)*-1*UFO::{}::ym", + "name": "GC_92", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + -0.0004291311090751267, + 0.0 + ] + }, + { + "expression": "(1/2)^(1/2)*-1𝑖*UFO::{}::ym", + "name": "GC_93", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.0, + -0.0004291311090751267 + ] + }, + { + "expression": "(1/2)^(1/2)*-1𝑖*UFO::{}::yt", + "name": "GC_94", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.0, + -0.6681058815337719 + ] + }, + { + "expression": "(1/2)^(1/2)*UFO::{}::yt", + "name": "GC_95", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.6681058815337719, + 0.0 + ] + }, + { + "expression": "-1*UFO::{}::ytau", + "name": "GC_96", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + -0.010206617000654722, + 0.0 + ] + }, + { + "expression": "UFO::{}::ytau", + "name": "GC_97", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.010206617000654722, + 0.0 + ] + }, + { + "expression": "(1/2)^(1/2)*-1*UFO::{}::ytau", + "name": "GC_98", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + -0.007217168094136855, + 0.0 + ] + }, + { + "expression": "(1/2)^(1/2)*-1𝑖*UFO::{}::ytau", + "name": "GC_99", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.0, + -0.007217168094136855 + ] + }, + { + "expression": "(1/2)^(1/2)*1𝑖*UFO::{}::ee*UFO::{}::sw^(-1)*conj(UFO::{}::CKM1x1)", + "name": "GC_100", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.0, + 0.4501822404312442 + ] + }, + { + "expression": "(1/2)^(1/2)*1𝑖*UFO::{}::ee*UFO::{}::sw^(-1)*conj(UFO::{}::CKM1x2)", + "name": "GC_101", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.0, + 0.10406729130552157 + ] + }, + { + "expression": "(1/2)^(1/2)*1𝑖*UFO::{}::ee*UFO::{}::sw^(-1)*conj(UFO::{}::CKM1x3)", + "name": "GC_102", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + -0.001455467036557634, + 0.0005634065947965036 + ] + }, + { + "expression": "(1/2)^(1/2)*1𝑖*UFO::{}::ee*UFO::{}::sw^(-1)*conj(UFO::{}::CKM2x1)", + "name": "GC_103", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.0, + -0.10406729130552157 + ] + }, + { + "expression": "(1/2)^(1/2)*1𝑖*UFO::{}::ee*UFO::{}::sw^(-1)*conj(UFO::{}::CKM2x2)", + "name": "GC_104", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.0, + 0.4501822404312442 + ] + }, + { + "expression": "(1/2)^(1/2)*1𝑖*UFO::{}::ee*UFO::{}::sw^(-1)*conj(UFO::{}::CKM2x3)", + "name": "GC_105", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.0, + 0.01894465947075628 + ] + }, + { + "expression": "(1/2)^(1/2)*1𝑖*UFO::{}::ee*UFO::{}::sw^(-1)*conj(UFO::{}::CKM3x1)", + "name": "GC_106", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + -0.001455467036557634, + 0.003704825183964887 + ] + }, + { + "expression": "(1/2)^(1/2)*1𝑖*UFO::{}::ee*UFO::{}::sw^(-1)*conj(UFO::{}::CKM3x2)", + "name": "GC_107", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.0, + -0.01894465947075628 + ] + }, + { + "expression": "(1/2)^(1/2)*1𝑖*UFO::{}::ee*UFO::{}::sw^(-1)*conj(UFO::{}::CKM3x3)", + "name": "GC_108", + "orders": [ + [ + "QED", + 1 + ] + ], + "value": [ + 0.0, + 0.46190542079681124 + ] + } + ], + "form_factors": [], + "functions": [ + { + "arguments": [ + "z" + ], + "expression": null, + "name": "complexconjugate" + }, + { + "arguments": [ + "z" + ], + "expression": null, + "name": "re" + }, + { + "arguments": [ + "z" + ], + "expression": null, + "name": "im" + }, + { + "arguments": [ + "z" + ], + "expression": null, + "name": "sec" + }, + { + "arguments": [ + "z" + ], + "expression": null, + "name": "asec" + }, + { + "arguments": [ + "z" + ], + "expression": null, + "name": "csc" + }, + { + "arguments": [ + "z" + ], + "expression": null, + "name": "acsc" + } + ], + "lorentz_structures": [ + { + "name": "UUS1", + "spins": [ + -1, + -1, + 1 + ], + "structure": "1" + }, + { + "name": "UUV1", + "spins": [ + -1, + -1, + 3 + ], + "structure": "-1*UFO::{}::P(UFO::{}::idx(1,3),UFO::{}::idx(1,2))" + }, + { + "name": "SSS1", + "spins": [ + 1, + 1, + 1 + ], + "structure": "1" + }, + { + "name": "FFS1", + "spins": [ + 2, + 2, + 1 + ], + "structure": "UFO::{}::ProjM(UFO::{}::idx(1,2),UFO::{}::idx(1,1))" + }, + { + "name": "FFS2", + "spins": [ + 2, + 2, + 1 + ], + "structure": "-1*UFO::{}::ProjP(UFO::{}::idx(1,2),UFO::{}::idx(1,1))+UFO::{}::ProjM(UFO::{}::idx(1,2),UFO::{}::idx(1,1))" + }, + { + "name": "FFS3", + "spins": [ + 2, + 2, + 1 + ], + "structure": "UFO::{}::ProjP(UFO::{}::idx(1,2),UFO::{}::idx(1,1))" + }, + { + "name": "FFS4", + "spins": [ + 2, + 2, + 1 + ], + "structure": "UFO::{}::ProjM(UFO::{}::idx(1,2),UFO::{}::idx(1,1))+UFO::{}::ProjP(UFO::{}::idx(1,2),UFO::{}::idx(1,1))" + }, + { + "name": "FFV1", + "spins": [ + 2, + 2, + 3 + ], + "structure": "UFO::{}::Gamma(UFO::{}::idx(1,3),UFO::{}::idx(1,2),UFO::{}::idx(1,1))" + }, + { + "name": "FFV2", + "spins": [ + 2, + 2, + 3 + ], + "structure": "UFO::{}::Gamma(UFO::{}::idx(1,3),UFO::{}::idx(1,2),UFO::{}::dummy(1))*UFO::{}::ProjM(UFO::{}::dummy(1),UFO::{}::idx(1,1))" + }, + { + "name": "FFV3", + "spins": [ + 2, + 2, + 3 + ], + "structure": "-2*UFO::{}::Gamma(UFO::{}::idx(1,3),UFO::{}::idx(1,2),UFO::{}::dummy(1))*UFO::{}::ProjP(UFO::{}::dummy(1),UFO::{}::idx(1,1))+UFO::{}::Gamma(UFO::{}::idx(1,3),UFO::{}::idx(1,2),UFO::{}::dummy(1))*UFO::{}::ProjM(UFO::{}::dummy(1),UFO::{}::idx(1,1))" + }, + { + "name": "FFV4", + "spins": [ + 2, + 2, + 3 + ], + "structure": "2*UFO::{}::Gamma(UFO::{}::idx(1,3),UFO::{}::idx(1,2),UFO::{}::dummy(1))*UFO::{}::ProjP(UFO::{}::dummy(1),UFO::{}::idx(1,1))+UFO::{}::Gamma(UFO::{}::idx(1,3),UFO::{}::idx(1,2),UFO::{}::dummy(1))*UFO::{}::ProjM(UFO::{}::dummy(1),UFO::{}::idx(1,1))" + }, + { + "name": "FFV5", + "spins": [ + 2, + 2, + 3 + ], + "structure": "4*UFO::{}::Gamma(UFO::{}::idx(1,3),UFO::{}::idx(1,2),UFO::{}::dummy(1))*UFO::{}::ProjP(UFO::{}::dummy(1),UFO::{}::idx(1,1))+UFO::{}::Gamma(UFO::{}::idx(1,3),UFO::{}::idx(1,2),UFO::{}::dummy(1))*UFO::{}::ProjM(UFO::{}::dummy(1),UFO::{}::idx(1,1))" + }, + { + "name": "VSS1", + "spins": [ + 3, + 1, + 1 + ], + "structure": "-1*UFO::{}::P(UFO::{}::idx(1,1),UFO::{}::idx(1,3))+UFO::{}::P(UFO::{}::idx(1,1),UFO::{}::idx(1,2))" + }, + { + "name": "VVS1", + "spins": [ + 3, + 3, + 1 + ], + "structure": "UFO::{}::Metric(UFO::{}::idx(1,1),UFO::{}::idx(1,2))" + }, + { + "name": "VVV1", + "spins": [ + 3, + 3, + 3 + ], + "structure": "-1*UFO::{}::Metric(UFO::{}::idx(1,1),UFO::{}::idx(1,2))*UFO::{}::P(UFO::{}::idx(1,3),UFO::{}::idx(1,2))+-1*UFO::{}::Metric(UFO::{}::idx(1,1),UFO::{}::idx(1,3))*UFO::{}::P(UFO::{}::idx(1,2),UFO::{}::idx(1,1))+-1*UFO::{}::Metric(UFO::{}::idx(1,2),UFO::{}::idx(1,3))*UFO::{}::P(UFO::{}::idx(1,1),UFO::{}::idx(1,3))+UFO::{}::Metric(UFO::{}::idx(1,1),UFO::{}::idx(1,2))*UFO::{}::P(UFO::{}::idx(1,3),UFO::{}::idx(1,1))+UFO::{}::Metric(UFO::{}::idx(1,1),UFO::{}::idx(1,3))*UFO::{}::P(UFO::{}::idx(1,2),UFO::{}::idx(1,3))+UFO::{}::Metric(UFO::{}::idx(1,2),UFO::{}::idx(1,3))*UFO::{}::P(UFO::{}::idx(1,1),UFO::{}::idx(1,2))" + }, + { + "name": "SSSS1", + "spins": [ + 1, + 1, + 1, + 1 + ], + "structure": "1" + }, + { + "name": "VVSS1", + "spins": [ + 3, + 3, + 1, + 1 + ], + "structure": "UFO::{}::Metric(UFO::{}::idx(1,1),UFO::{}::idx(1,2))" + }, + { + "name": "VVVV1", + "spins": [ + 3, + 3, + 3, + 3 + ], + "structure": "-1*UFO::{}::Metric(UFO::{}::idx(1,1),UFO::{}::idx(1,3))*UFO::{}::Metric(UFO::{}::idx(1,2),UFO::{}::idx(1,4))+UFO::{}::Metric(UFO::{}::idx(1,1),UFO::{}::idx(1,4))*UFO::{}::Metric(UFO::{}::idx(1,2),UFO::{}::idx(1,3))" + }, + { + "name": "VVVV2", + "spins": [ + 3, + 3, + 3, + 3 + ], + "structure": "-2*UFO::{}::Metric(UFO::{}::idx(1,1),UFO::{}::idx(1,2))*UFO::{}::Metric(UFO::{}::idx(1,3),UFO::{}::idx(1,4))+UFO::{}::Metric(UFO::{}::idx(1,1),UFO::{}::idx(1,3))*UFO::{}::Metric(UFO::{}::idx(1,2),UFO::{}::idx(1,4))+UFO::{}::Metric(UFO::{}::idx(1,1),UFO::{}::idx(1,4))*UFO::{}::Metric(UFO::{}::idx(1,2),UFO::{}::idx(1,3))" + }, + { + "name": "VVVV3", + "spins": [ + 3, + 3, + 3, + 3 + ], + "structure": "-1*UFO::{}::Metric(UFO::{}::idx(1,1),UFO::{}::idx(1,2))*UFO::{}::Metric(UFO::{}::idx(1,3),UFO::{}::idx(1,4))+UFO::{}::Metric(UFO::{}::idx(1,1),UFO::{}::idx(1,4))*UFO::{}::Metric(UFO::{}::idx(1,2),UFO::{}::idx(1,3))" + }, + { + "name": "VVVV4", + "spins": [ + 3, + 3, + 3, + 3 + ], + "structure": "-1*UFO::{}::Metric(UFO::{}::idx(1,1),UFO::{}::idx(1,2))*UFO::{}::Metric(UFO::{}::idx(1,3),UFO::{}::idx(1,4))+UFO::{}::Metric(UFO::{}::idx(1,1),UFO::{}::idx(1,3))*UFO::{}::Metric(UFO::{}::idx(1,2),UFO::{}::idx(1,4))" + }, + { + "name": "VVVV5", + "spins": [ + 3, + 3, + 3, + 3 + ], + "structure": "-1/2*UFO::{}::Metric(UFO::{}::idx(1,1),UFO::{}::idx(1,2))*UFO::{}::Metric(UFO::{}::idx(1,3),UFO::{}::idx(1,4))+-1/2*UFO::{}::Metric(UFO::{}::idx(1,1),UFO::{}::idx(1,3))*UFO::{}::Metric(UFO::{}::idx(1,2),UFO::{}::idx(1,4))+UFO::{}::Metric(UFO::{}::idx(1,1),UFO::{}::idx(1,4))*UFO::{}::Metric(UFO::{}::idx(1,2),UFO::{}::idx(1,3))" + } + ], + "name": "sm", + "orders": [ + { + "expansion_order": 99, + "hierarchy": 1, + "name": "QCD" + }, + { + "expansion_order": 99, + "hierarchy": 2, + "name": "QED" + } + ], + "parameters": [ + { + "expression": null, + "lhablock": "SMINPUTS", + "lhacode": [ + 1 + ], + "name": "aEWM1", + "nature": "external", + "parameter_type": "real", + "value": [ + 132.50698, + 0.0 + ] + }, + { + "expression": null, + "lhablock": "SMINPUTS", + "lhacode": [ + 2 + ], + "name": "Gf", + "nature": "external", + "parameter_type": "real", + "value": [ + 1.16639e-05, + 0.0 + ] + }, + { + "expression": null, + "lhablock": "SMINPUTS", + "lhacode": [ + 3 + ], + "name": "aS", + "nature": "external", + "parameter_type": "real", + "value": [ + 0.118, + 0.0 + ] + }, + { + "expression": null, + "lhablock": "Wolfenstein", + "lhacode": [ + 1 + ], + "name": "lamWS", + "nature": "external", + "parameter_type": "real", + "value": [ + 0.2253, + 0.0 + ] + }, + { + "expression": null, + "lhablock": "Wolfenstein", + "lhacode": [ + 2 + ], + "name": "AWS", + "nature": "external", + "parameter_type": "real", + "value": [ + 0.808, + 0.0 + ] + }, + { + "expression": null, + "lhablock": "Wolfenstein", + "lhacode": [ + 3 + ], + "name": "rhoWS", + "nature": "external", + "parameter_type": "real", + "value": [ + 0.132, + 0.0 + ] + }, + { + "expression": null, + "lhablock": "Wolfenstein", + "lhacode": [ + 4 + ], + "name": "etaWS", + "nature": "external", + "parameter_type": "real", + "value": [ + 0.341, + 0.0 + ] + }, + { + "expression": null, + "lhablock": "YUKAWA", + "lhacode": [ + 4 + ], + "name": "ymc", + "nature": "external", + "parameter_type": "real", + "value": [ + 1.27, + 0.0 + ] + }, + { + "expression": null, + "lhablock": "YUKAWA", + "lhacode": [ + 5 + ], + "name": "ymb", + "nature": "external", + "parameter_type": "real", + "value": [ + 4.2, + 0.0 + ] + }, + { + "expression": null, + "lhablock": "YUKAWA", + "lhacode": [ + 6 + ], + "name": "ymt", + "nature": "external", + "parameter_type": "real", + "value": [ + 164.5, + 0.0 + ] + }, + { + "expression": null, + "lhablock": "YUKAWA", + "lhacode": [ + 11 + ], + "name": "yme", + "nature": "external", + "parameter_type": "real", + "value": [ + 0.000511, + 0.0 + ] + }, + { + "expression": null, + "lhablock": "YUKAWA", + "lhacode": [ + 13 + ], + "name": "ymm", + "nature": "external", + "parameter_type": "real", + "value": [ + 0.10566, + 0.0 + ] + }, + { + "expression": null, + "lhablock": "YUKAWA", + "lhacode": [ + 15 + ], + "name": "ymtau", + "nature": "external", + "parameter_type": "real", + "value": [ + 1.777, + 0.0 + ] + }, + { + "expression": null, + "lhablock": "MASS", + "lhacode": [ + 23 + ], + "name": "MZ", + "nature": "external", + "parameter_type": "real", + "value": [ + 91.188, + 0.0 + ] + }, + { + "expression": null, + "lhablock": "MASS", + "lhacode": [ + 4 + ], + "name": "MC", + "nature": "external", + "parameter_type": "real", + "value": [ + 1.27, + 0.0 + ] + }, + { + "expression": null, + "lhablock": "MASS", + "lhacode": [ + 6 + ], + "name": "MT", + "nature": "external", + "parameter_type": "real", + "value": [ + 173.0, + 0.0 + ] + }, + { + "expression": null, + "lhablock": "MASS", + "lhacode": [ + 5 + ], + "name": "MB", + "nature": "external", + "parameter_type": "real", + "value": [ + 4.7, + 0.0 + ] + }, + { + "expression": null, + "lhablock": "MASS", + "lhacode": [ + 25 + ], + "name": "MH", + "nature": "external", + "parameter_type": "real", + "value": [ + 125.0, + 0.0 + ] + }, + { + "expression": null, + "lhablock": "MASS", + "lhacode": [ + 11 + ], + "name": "Me", + "nature": "external", + "parameter_type": "real", + "value": [ + 0.000511, + 0.0 + ] + }, + { + "expression": null, + "lhablock": "MASS", + "lhacode": [ + 13 + ], + "name": "MM", + "nature": "external", + "parameter_type": "real", + "value": [ + 0.10566, + 0.0 + ] + }, + { + "expression": null, + "lhablock": "MASS", + "lhacode": [ + 15 + ], + "name": "MTA", + "nature": "external", + "parameter_type": "real", + "value": [ + 1.777, + 0.0 + ] + }, + { + "expression": null, + "lhablock": "DECAY", + "lhacode": [ + 23 + ], + "name": "WZ", + "nature": "external", + "parameter_type": "real", + "value": [ + 2.44140351, + 0.0 + ] + }, + { + "expression": null, + "lhablock": "DECAY", + "lhacode": [ + 24 + ], + "name": "WW", + "nature": "external", + "parameter_type": "real", + "value": [ + 2.04759951, + 0.0 + ] + }, + { + "expression": null, + "lhablock": "DECAY", + "lhacode": [ + 6 + ], + "name": "WT", + "nature": "external", + "parameter_type": "real", + "value": [ + 1.50833649, + 0.0 + ] + }, + { + "expression": null, + "lhablock": "DECAY", + "lhacode": [ + 25 + ], + "name": "WH", + "nature": "external", + "parameter_type": "real", + "value": [ + 0.00638233934, + 0.0 + ] + }, + { + "expression": null, + "lhablock": "DECAY", + "lhacode": [ + 15 + ], + "name": "WTau", + "nature": "external", + "parameter_type": "real", + "value": [ + 2.27e-12, + 0.0 + ] + }, + { + "expression": null, + "lhablock": null, + "lhacode": null, + "name": "ZERO", + "nature": "internal", + "parameter_type": "real", + "value": [ + 0.0, + 0.0 + ] + }, + { + "expression": null, + "lhablock": null, + "lhacode": null, + "name": "CKM3x3", + "nature": "internal", + "parameter_type": "complex", + "value": [ + 1.0, + 0.0 + ] + }, + { + "expression": "-1/2*UFO::{}::lamWS^2+1", + "lhablock": null, + "lhacode": null, + "name": "CKM1x1", + "nature": "internal", + "parameter_type": "complex", + "value": [ + 0.974619955, + 0.0 + ] + }, + { + "expression": "UFO::{}::lamWS", + "lhablock": null, + "lhacode": null, + "name": "CKM1x2", + "nature": "internal", + "parameter_type": "complex", + "value": [ + 0.2253, + 0.0 + ] + }, + { + "expression": "(-1𝑖*UFO::{}::etaWS+UFO::{}::rhoWS)*UFO::{}::AWS*UFO::{}::lamWS^3", + "lhablock": null, + "lhacode": null, + "name": "CKM1x3", + "nature": "internal", + "parameter_type": "complex", + "value": [ + 0.0012197444962317122, + -0.0031510066152652563 + ] + }, + { + "expression": "-1*UFO::{}::lamWS", + "lhablock": null, + "lhacode": null, + "name": "CKM2x1", + "nature": "internal", + "parameter_type": "complex", + "value": [ + -0.2253, + 0.0 + ] + }, + { + "expression": "-1/2*UFO::{}::lamWS^2+1", + "lhablock": null, + "lhacode": null, + "name": "CKM2x2", + "nature": "internal", + "parameter_type": "complex", + "value": [ + 0.974619955, + 0.0 + ] + }, + { + "expression": "UFO::{}::AWS*UFO::{}::lamWS^2", + "lhablock": null, + "lhacode": null, + "name": "CKM2x3", + "nature": "internal", + "parameter_type": "complex", + "value": [ + 0.04101415272, + 0.0 + ] + }, + { + "expression": "(-1*UFO::{}::rhoWS+-1𝑖*UFO::{}::etaWS+1)*UFO::{}::AWS*UFO::{}::lamWS^3", + "lhablock": null, + "lhacode": null, + "name": "CKM3x1", + "nature": "internal", + "parameter_type": "complex", + "value": [ + 0.008020744111584288, + -0.0031510066152652563 + ] + }, + { + "expression": "-1*UFO::{}::AWS*UFO::{}::lamWS^2", + "lhablock": null, + "lhacode": null, + "name": "CKM3x2", + "nature": "internal", + "parameter_type": "complex", + "value": [ + -0.04101415272, + 0.0 + ] + }, + { + "expression": "UFO::{}::aEWM1^(-1)", + "lhablock": null, + "lhacode": null, + "name": "aEW", + "nature": "internal", + "parameter_type": "real", + "value": [ + 0.00754677225305414, + 0.0 + ] + }, + { + "expression": "2*UFO::{}::aS^(1/2)*𝜋^(1/2)", + "lhablock": null, + "lhacode": null, + "name": "G", + "nature": "internal", + "parameter_type": "real", + "value": [ + 1.2177157847767195, + 0.0 + ] + }, + { + "expression": "(((1/2)^(1/2)*-1*UFO::{}::Gf^(-1)*UFO::{}::MZ^2*UFO::{}::aEW*𝜋+1/4*UFO::{}::MZ^4)^(1/2)+1/2*UFO::{}::MZ^2)^(1/2)", + "lhablock": null, + "lhacode": null, + "name": "MW", + "nature": "internal", + "parameter_type": "real", + "value": [ + 80.4190000176632, + 0.0 + ] + }, + { + "expression": "2*UFO::{}::aEW^(1/2)*𝜋^(1/2)", + "lhablock": null, + "lhacode": null, + "name": "ee", + "nature": "internal", + "parameter_type": "real", + "value": [ + 0.3079537904849374, + 0.0 + ] + }, + { + "expression": "-1*UFO::{}::MW^2*UFO::{}::MZ^(-2)+1", + "lhablock": null, + "lhacode": null, + "name": "sw2", + "nature": "internal", + "parameter_type": "real", + "value": [ + 0.22224653275123984, + 0.0 + ] + }, + { + "expression": "(-1*UFO::{}::sw2+1)^(1/2)", + "lhablock": null, + "lhacode": null, + "name": "cw", + "nature": "internal", + "parameter_type": "real", + "value": [ + 0.8819033208060622, + 0.0 + ] + }, + { + "expression": "UFO::{}::sw2^(1/2)", + "lhablock": null, + "lhacode": null, + "name": "sw", + "nature": "internal", + "parameter_type": "real", + "value": [ + 0.47143030529574553, + 0.0 + ] + }, + { + "expression": "UFO::{}::cw^(-1)*UFO::{}::ee", + "lhablock": null, + "lhacode": null, + "name": "g1", + "nature": "internal", + "parameter_type": "real", + "value": [ + 0.3491922336832418, + 0.0 + ] + }, + { + "expression": "UFO::{}::ee*UFO::{}::sw^(-1)", + "lhablock": null, + "lhacode": null, + "name": "gw", + "nature": "internal", + "parameter_type": "real", + "value": [ + 0.653232910624502, + 0.0 + ] + }, + { + "expression": "2*UFO::{}::MW*UFO::{}::ee^(-1)*UFO::{}::sw", + "lhablock": null, + "lhacode": null, + "name": "vev", + "nature": "internal", + "parameter_type": "real", + "value": [ + 246.21845810181625, + 0.0 + ] + }, + { + "expression": "1/2*UFO::{}::MH^2*UFO::{}::vev^(-2)", + "lhablock": null, + "lhacode": null, + "name": "lam", + "nature": "internal", + "parameter_type": "real", + "value": [ + 0.12886910601690277, + 0.0 + ] + }, + { + "expression": "2^(1/2)*UFO::{}::vev^(-1)*UFO::{}::ymb", + "lhablock": null, + "lhacode": null, + "name": "yb", + "nature": "internal", + "parameter_type": "real", + "value": [ + 0.024123686777011728, + 0.0 + ] + }, + { + "expression": "2^(1/2)*UFO::{}::vev^(-1)*UFO::{}::ymc", + "lhablock": null, + "lhacode": null, + "name": "yc", + "nature": "internal", + "parameter_type": "real", + "value": [ + 0.007294543382572593, + 0.0 + ] + }, + { + "expression": "2^(1/2)*UFO::{}::vev^(-1)*UFO::{}::yme", + "lhablock": null, + "lhacode": null, + "name": "ye", + "nature": "internal", + "parameter_type": "real", + "value": [ + 2.93504855786976e-06, + 0.0 + ] + }, + { + "expression": "2^(1/2)*UFO::{}::vev^(-1)*UFO::{}::ymm", + "lhablock": null, + "lhacode": null, + "name": "ym", + "nature": "internal", + "parameter_type": "real", + "value": [ + 0.0006068830344902521, + 0.0 + ] + }, + { + "expression": "2^(1/2)*UFO::{}::vev^(-1)*UFO::{}::ymt", + "lhablock": null, + "lhacode": null, + "name": "yt", + "nature": "internal", + "parameter_type": "real", + "value": [ + 0.9448443987662926, + 0.0 + ] + }, + { + "expression": "2^(1/2)*UFO::{}::vev^(-1)*UFO::{}::ymtau", + "lhablock": null, + "lhacode": null, + "name": "ytau", + "nature": "internal", + "parameter_type": "real", + "value": [ + 0.010206617000654722, + 0.0 + ] + }, + { + "expression": "(UFO::{}::lam*UFO::{}::vev^2)^(1/2)", + "lhablock": null, + "lhacode": null, + "name": "muH", + "nature": "internal", + "parameter_type": "real", + "value": [ + 88.38834764831844, + 0.0 + ] + }, + { + "expression": "UFO::{}::yb*conj(UFO::{}::CKM1x3)", + "lhablock": null, + "lhacode": null, + "name": "I1x31", + "nature": "internal", + "parameter_type": "complex", + "value": [ + 2.9424734175077786e-05, + 7.601389661895094e-05 + ] + }, + { + "expression": "UFO::{}::yb*conj(UFO::{}::CKM2x3)", + "lhablock": null, + "lhacode": null, + "name": "I1x32", + "nature": "internal", + "parameter_type": "complex", + "value": [ + 0.0009894125736418036, + 0.0 + ] + }, + { + "expression": "UFO::{}::yb*conj(UFO::{}::CKM3x3)", + "lhablock": null, + "lhacode": null, + "name": "I1x33", + "nature": "internal", + "parameter_type": "complex", + "value": [ + 0.024123686777011728, + 0.0 + ] + }, + { + "expression": "UFO::{}::yc*conj(UFO::{}::CKM2x1)", + "lhablock": null, + "lhacode": null, + "name": "I2x12", + "nature": "internal", + "parameter_type": "complex", + "value": [ + -0.0016434606240936052, + 0.0 + ] + }, + { + "expression": "UFO::{}::yt*conj(UFO::{}::CKM3x1)", + "lhablock": null, + "lhacode": null, + "name": "I2x13", + "nature": "internal", + "parameter_type": "complex", + "value": [ + 0.007578355147768138, + 0.0029772109509089116 + ] + }, + { + "expression": "UFO::{}::yc*conj(UFO::{}::CKM2x2)", + "lhablock": null, + "lhacode": null, + "name": "I2x22", + "nature": "internal", + "parameter_type": "complex", + "value": [ + 0.007109407543268448, + 0.0 + ] + }, + { + "expression": "UFO::{}::yt*conj(UFO::{}::CKM3x2)", + "lhablock": null, + "lhacode": null, + "name": "I2x23", + "nature": "internal", + "parameter_type": "complex", + "value": [ + -0.0387519924676373, + 0.0 + ] + }, + { + "expression": "UFO::{}::yc*conj(UFO::{}::CKM2x3)", + "lhablock": null, + "lhacode": null, + "name": "I2x32", + "nature": "internal", + "parameter_type": "complex", + "value": [ + 0.00029917951631549774, + 0.0 + ] + }, + { + "expression": "UFO::{}::yt*conj(UFO::{}::CKM3x3)", + "lhablock": null, + "lhacode": null, + "name": "I2x33", + "nature": "internal", + "parameter_type": "complex", + "value": [ + 0.9448443987662926, + 0.0 + ] + }, + { + "expression": "UFO::{}::CKM2x1*UFO::{}::yc", + "lhablock": null, + "lhacode": null, + "name": "I3x21", + "nature": "internal", + "parameter_type": "complex", + "value": [ + -0.0016434606240936052, + 0.0 + ] + }, + { + "expression": "UFO::{}::CKM2x2*UFO::{}::yc", + "lhablock": null, + "lhacode": null, + "name": "I3x22", + "nature": "internal", + "parameter_type": "complex", + "value": [ + 0.007109407543268448, + 0.0 + ] + }, + { + "expression": "UFO::{}::CKM2x3*UFO::{}::yc", + "lhablock": null, + "lhacode": null, + "name": "I3x23", + "nature": "internal", + "parameter_type": "complex", + "value": [ + 0.00029917951631549774, + 0.0 + ] + }, + { + "expression": "UFO::{}::CKM3x1*UFO::{}::yt", + "lhablock": null, + "lhacode": null, + "name": "I3x31", + "nature": "internal", + "parameter_type": "complex", + "value": [ + 0.007578355147768138, + -0.0029772109509089116 + ] + }, + { + "expression": "UFO::{}::CKM3x2*UFO::{}::yt", + "lhablock": null, + "lhacode": null, + "name": "I3x32", + "nature": "internal", + "parameter_type": "complex", + "value": [ + -0.0387519924676373, + 0.0 + ] + }, + { + "expression": "UFO::{}::CKM3x3*UFO::{}::yt", + "lhablock": null, + "lhacode": null, + "name": "I3x33", + "nature": "internal", + "parameter_type": "complex", + "value": [ + 0.9448443987662926, + 0.0 + ] + }, + { + "expression": "UFO::{}::CKM1x3*UFO::{}::yb", + "lhablock": null, + "lhacode": null, + "name": "I4x13", + "nature": "internal", + "parameter_type": "complex", + "value": [ + 2.9424734175077786e-05, + -7.601389661895094e-05 + ] + }, + { + "expression": "UFO::{}::CKM2x3*UFO::{}::yb", + "lhablock": null, + "lhacode": null, + "name": "I4x23", + "nature": "internal", + "parameter_type": "complex", + "value": [ + 0.0009894125736418036, + 0.0 + ] + }, + { + "expression": "UFO::{}::CKM3x3*UFO::{}::yb", + "lhablock": null, + "lhacode": null, + "name": "I4x33", + "nature": "internal", + "parameter_type": "complex", + "value": [ + 0.024123686777011728, + 0.0 + ] + } + ], + "particles": [ + { + "antiname": "a", + "antitexname": "{\\gamma}", + "charge": 0.0, + "color": 1, + "ghost_number": 0, + "goldstoneboson": false, + "lepton_number": 0, + "mass": "ZERO", + "name": "a", + "pdg_code": 22, + "propagating": true, + "propagator": "a_propFeynman", + "spin": 3, + "texname": "{\\gamma}", + "width": "ZERO", + "y_charge": 0 + }, + { + "antiname": "Z", + "antitexname": "{Z}", + "charge": 0.0, + "color": 1, + "ghost_number": 0, + "goldstoneboson": false, + "lepton_number": 0, + "mass": "MZ", + "name": "Z", + "pdg_code": 23, + "propagating": true, + "propagator": "Z_propFeynman", + "spin": 3, + "texname": "{Z}", + "width": "WZ", + "y_charge": 0 + }, + { + "antiname": "W-", + "antitexname": "{W^-}", + "charge": 1.0, + "color": 1, + "ghost_number": 0, + "goldstoneboson": false, + "lepton_number": 0, + "mass": "MW", + "name": "W+", + "pdg_code": 24, + "propagating": true, + "propagator": "W+_propFeynman", + "spin": 3, + "texname": "{W^+}", + "width": "WW", + "y_charge": 0 + }, + { + "antiname": "W+", + "antitexname": "{W^+}", + "charge": -1.0, + "color": 1, + "ghost_number": 0, + "goldstoneboson": false, + "lepton_number": 0, + "mass": "MW", + "name": "W-", + "pdg_code": -24, + "propagating": true, + "propagator": "W-_propFeynman", + "spin": 3, + "texname": "{W^-}", + "width": "WW", + "y_charge": 0 + }, + { + "antiname": "g", + "antitexname": "{g}", + "charge": 0.0, + "color": 8, + "ghost_number": 0, + "goldstoneboson": false, + "lepton_number": 0, + "mass": "ZERO", + "name": "g", + "pdg_code": 21, + "propagating": true, + "propagator": "g_propFeynman", + "spin": 3, + "texname": "{g}", + "width": "ZERO", + "y_charge": 0 + }, + { + "antiname": "ghA~", + "antitexname": "\\overline{\\tilde{\\gamma}}", + "charge": 0.0, + "color": 1, + "ghost_number": 1, + "goldstoneboson": false, + "lepton_number": 0, + "mass": "ZERO", + "name": "ghA", + "pdg_code": 9000001, + "propagating": true, + "propagator": "ghA_propFeynman", + "spin": -1, + "texname": "\\tilde{\\gamma}", + "width": "ZERO", + "y_charge": 0 + }, + { + "antiname": "ghA", + "antitexname": "\\tilde{\\gamma}", + "charge": -0.0, + "color": 1, + "ghost_number": -1, + "goldstoneboson": false, + "lepton_number": 0, + "mass": "ZERO", + "name": "ghA~", + "pdg_code": -9000001, + "propagating": true, + "propagator": "ghA~_propFeynman", + "spin": -1, + "texname": "\\overline{\\tilde{\\gamma}}", + "width": "ZERO", + "y_charge": 0 + }, + { + "antiname": "ghZ~", + "antitexname": "\\bar{\\tilde{Z}}", + "charge": 0.0, + "color": 1, + "ghost_number": 1, + "goldstoneboson": false, + "lepton_number": 0, + "mass": "MZ", + "name": "ghZ", + "pdg_code": 9000002, + "propagating": true, + "propagator": "ghZ_propFeynman", + "spin": -1, + "texname": "\\tilde{Z}", + "width": "WZ", + "y_charge": 0 + }, + { + "antiname": "ghZ", + "antitexname": "\\tilde{Z}", + "charge": -0.0, + "color": 1, + "ghost_number": -1, + "goldstoneboson": false, + "lepton_number": 0, + "mass": "MZ", + "name": "ghZ~", + "pdg_code": -9000002, + "propagating": true, + "propagator": "ghZ~_propFeynman", + "spin": -1, + "texname": "\\bar{\\tilde{Z}}", + "width": "WZ", + "y_charge": 0 + }, + { + "antiname": "ghWp~", + "antitexname": "{\\bar{\\tilde{W}}^+}", + "charge": 1.0, + "color": 1, + "ghost_number": 1, + "goldstoneboson": false, + "lepton_number": 0, + "mass": "MW", + "name": "ghWp", + "pdg_code": 9000003, + "propagating": true, + "propagator": "ghWp_propFeynman", + "spin": -1, + "texname": "{\\tilde{W}^+}", + "width": "WW", + "y_charge": 0 + }, + { + "antiname": "ghWp", + "antitexname": "{\\tilde{W}^+}", + "charge": -1.0, + "color": 1, + "ghost_number": -1, + "goldstoneboson": false, + "lepton_number": 0, + "mass": "MW", + "name": "ghWp~", + "pdg_code": -9000003, + "propagating": true, + "propagator": "ghWp~_propFeynman", + "spin": -1, + "texname": "{\\bar{\\tilde{W}}^+}", + "width": "WW", + "y_charge": 0 + }, + { + "antiname": "ghWm~", + "antitexname": "{\\bar{\\tilde{W}}^-}", + "charge": -1.0, + "color": 1, + "ghost_number": 1, + "goldstoneboson": false, + "lepton_number": 0, + "mass": "MW", + "name": "ghWm", + "pdg_code": 9000004, + "propagating": true, + "propagator": "ghWm_propFeynman", + "spin": -1, + "texname": "{\\tilde{W}^-}", + "width": "WW", + "y_charge": 0 + }, + { + "antiname": "ghWm", + "antitexname": "{\\tilde{W}^-}", + "charge": 1.0, + "color": 1, + "ghost_number": -1, + "goldstoneboson": false, + "lepton_number": 0, + "mass": "MW", + "name": "ghWm~", + "pdg_code": -9000004, + "propagating": true, + "propagator": "ghWm~_propFeynman", + "spin": -1, + "texname": "{\\bar{\\tilde{W}}^-}", + "width": "WW", + "y_charge": 0 + }, + { + "antiname": "ghG~", + "antitexname": "{\\bar{\\tilde{g}}}", + "charge": 0.0, + "color": 8, + "ghost_number": 1, + "goldstoneboson": false, + "lepton_number": 0, + "mass": "ZERO", + "name": "ghG", + "pdg_code": 9000005, + "propagating": true, + "propagator": "ghG_propFeynman", + "spin": -1, + "texname": "{\\tilde{g}}", + "width": "ZERO", + "y_charge": 0 + }, + { + "antiname": "ghG", + "antitexname": "{\\tilde{g}}", + "charge": -0.0, + "color": 8, + "ghost_number": -1, + "goldstoneboson": false, + "lepton_number": 0, + "mass": "ZERO", + "name": "ghG~", + "pdg_code": -9000005, + "propagating": true, + "propagator": "ghG~_propFeynman", + "spin": -1, + "texname": "{\\bar{\\tilde{g}}}", + "width": "ZERO", + "y_charge": 0 + }, + { + "antiname": "ve~", + "antitexname": "{\\overline{\\nu}_e}", + "charge": 0.0, + "color": 1, + "ghost_number": 0, + "goldstoneboson": false, + "lepton_number": 1, + "mass": "ZERO", + "name": "ve", + "pdg_code": 12, + "propagating": true, + "propagator": "ve_propFeynman", + "spin": 2, + "texname": "{\\nu_e}", + "width": "ZERO", + "y_charge": 0 + }, + { + "antiname": "ve", + "antitexname": "{\\nu_e}", + "charge": -0.0, + "color": 1, + "ghost_number": 0, + "goldstoneboson": false, + "lepton_number": -1, + "mass": "ZERO", + "name": "ve~", + "pdg_code": -12, + "propagating": true, + "propagator": "ve~_propFeynman", + "spin": 2, + "texname": "{\\overline{\\nu}_e}", + "width": "ZERO", + "y_charge": 0 + }, + { + "antiname": "vm~", + "antitexname": "{\\overline{\\nu}_\\mu}", + "charge": 0.0, + "color": 1, + "ghost_number": 0, + "goldstoneboson": false, + "lepton_number": 1, + "mass": "ZERO", + "name": "vm", + "pdg_code": 14, + "propagating": true, + "propagator": "vm_propFeynman", + "spin": 2, + "texname": "{\\nu_\\mu}", + "width": "ZERO", + "y_charge": 0 + }, + { + "antiname": "vm", + "antitexname": "{\\nu_\\mu}", + "charge": -0.0, + "color": 1, + "ghost_number": 0, + "goldstoneboson": false, + "lepton_number": -1, + "mass": "ZERO", + "name": "vm~", + "pdg_code": -14, + "propagating": true, + "propagator": "vm~_propFeynman", + "spin": 2, + "texname": "{\\overline{\\nu}_\\mu}", + "width": "ZERO", + "y_charge": 0 + }, + { + "antiname": "vt~", + "antitexname": "{\\overline{\\nu}_\\tau}", + "charge": 0.0, + "color": 1, + "ghost_number": 0, + "goldstoneboson": false, + "lepton_number": 1, + "mass": "ZERO", + "name": "vt", + "pdg_code": 16, + "propagating": true, + "propagator": "vt_propFeynman", + "spin": 2, + "texname": "{\\nu_\\tau}", + "width": "ZERO", + "y_charge": 0 + }, + { + "antiname": "vt", + "antitexname": "{\\nu_\\tau}", + "charge": -0.0, + "color": 1, + "ghost_number": 0, + "goldstoneboson": false, + "lepton_number": -1, + "mass": "ZERO", + "name": "vt~", + "pdg_code": -16, + "propagating": true, + "propagator": "vt~_propFeynman", + "spin": 2, + "texname": "{\\overline{\\nu}_\\tau}", + "width": "ZERO", + "y_charge": 0 + }, + { + "antiname": "u~", + "antitexname": "{\\overline{u}}", + "charge": 0.6666666666666666, + "color": 3, + "ghost_number": 0, + "goldstoneboson": false, + "lepton_number": 0, + "mass": "ZERO", + "name": "u", + "pdg_code": 2, + "propagating": true, + "propagator": "u_propFeynman", + "spin": 2, + "texname": "{u}", + "width": "ZERO", + "y_charge": 0 + }, + { + "antiname": "u", + "antitexname": "{u}", + "charge": -0.6666666666666666, + "color": -3, + "ghost_number": 0, + "goldstoneboson": false, + "lepton_number": 0, + "mass": "ZERO", + "name": "u~", + "pdg_code": -2, + "propagating": true, + "propagator": "u~_propFeynman", + "spin": 2, + "texname": "{\\overline{u}}", + "width": "ZERO", + "y_charge": 0 + }, + { + "antiname": "c~", + "antitexname": "{\\overline{c}}", + "charge": 0.6666666666666666, + "color": 3, + "ghost_number": 0, + "goldstoneboson": false, + "lepton_number": 0, + "mass": "MC", + "name": "c", + "pdg_code": 4, + "propagating": true, + "propagator": "c_propFeynman", + "spin": 2, + "texname": "{c}", + "width": "ZERO", + "y_charge": 0 + }, + { + "antiname": "c", + "antitexname": "{c}", + "charge": -0.6666666666666666, + "color": -3, + "ghost_number": 0, + "goldstoneboson": false, + "lepton_number": 0, + "mass": "MC", + "name": "c~", + "pdg_code": -4, + "propagating": true, + "propagator": "c~_propFeynman", + "spin": 2, + "texname": "{\\overline{c}}", + "width": "ZERO", + "y_charge": 0 + }, + { + "antiname": "t~", + "antitexname": "{\\overline{t}}", + "charge": 0.6666666666666666, + "color": 3, + "ghost_number": 0, + "goldstoneboson": false, + "lepton_number": 0, + "mass": "MT", + "name": "t", + "pdg_code": 6, + "propagating": true, + "propagator": "t_propFeynman", + "spin": 2, + "texname": "{t}", + "width": "WT", + "y_charge": 0 + }, + { + "antiname": "t", + "antitexname": "{t}", + "charge": -0.6666666666666666, + "color": -3, + "ghost_number": 0, + "goldstoneboson": false, + "lepton_number": 0, + "mass": "MT", + "name": "t~", + "pdg_code": -6, + "propagating": true, + "propagator": "t~_propFeynman", + "spin": 2, + "texname": "{\\overline{t}}", + "width": "WT", + "y_charge": 0 + }, + { + "antiname": "d~", + "antitexname": "{\\overline{d}}", + "charge": -0.3333333333333333, + "color": 3, + "ghost_number": 0, + "goldstoneboson": false, + "lepton_number": 0, + "mass": "ZERO", + "name": "d", + "pdg_code": 1, + "propagating": true, + "propagator": "d_propFeynman", + "spin": 2, + "texname": "{d}", + "width": "ZERO", + "y_charge": 0 + }, + { + "antiname": "d", + "antitexname": "{d}", + "charge": 0.3333333333333333, + "color": -3, + "ghost_number": 0, + "goldstoneboson": false, + "lepton_number": 0, + "mass": "ZERO", + "name": "d~", + "pdg_code": -1, + "propagating": true, + "propagator": "d~_propFeynman", + "spin": 2, + "texname": "{\\overline{d}}", + "width": "ZERO", + "y_charge": 0 + }, + { + "antiname": "s~", + "antitexname": "{\\overline{s}}", + "charge": -0.3333333333333333, + "color": 3, + "ghost_number": 0, + "goldstoneboson": false, + "lepton_number": 0, + "mass": "ZERO", + "name": "s", + "pdg_code": 3, + "propagating": true, + "propagator": "s_propFeynman", + "spin": 2, + "texname": "{s}", + "width": "ZERO", + "y_charge": 0 + }, + { + "antiname": "s", + "antitexname": "{s}", + "charge": 0.3333333333333333, + "color": -3, + "ghost_number": 0, + "goldstoneboson": false, + "lepton_number": 0, + "mass": "ZERO", + "name": "s~", + "pdg_code": -3, + "propagating": true, + "propagator": "s~_propFeynman", + "spin": 2, + "texname": "{\\overline{s}}", + "width": "ZERO", + "y_charge": 0 + }, + { + "antiname": "b~", + "antitexname": "{\\overline{b}}", + "charge": -0.3333333333333333, + "color": 3, + "ghost_number": 0, + "goldstoneboson": false, + "lepton_number": 0, + "mass": "MB", + "name": "b", + "pdg_code": 5, + "propagating": true, + "propagator": "b_propFeynman", + "spin": 2, + "texname": "{b}", + "width": "ZERO", + "y_charge": 0 + }, + { + "antiname": "b", + "antitexname": "{b}", + "charge": 0.3333333333333333, + "color": -3, + "ghost_number": 0, + "goldstoneboson": false, + "lepton_number": 0, + "mass": "MB", + "name": "b~", + "pdg_code": -5, + "propagating": true, + "propagator": "b~_propFeynman", + "spin": 2, + "texname": "{\\overline{b}}", + "width": "ZERO", + "y_charge": 0 + }, + { + "antiname": "H", + "antitexname": "{H}", + "charge": 0.0, + "color": 1, + "ghost_number": 0, + "goldstoneboson": false, + "lepton_number": 0, + "mass": "MH", + "name": "H", + "pdg_code": 25, + "propagating": true, + "propagator": "H_propFeynman", + "spin": 1, + "texname": "{H}", + "width": "WH", + "y_charge": 0 + }, + { + "antiname": "G0", + "antitexname": "{G_0}", + "charge": 0.0, + "color": 1, + "ghost_number": 0, + "goldstoneboson": true, + "lepton_number": 0, + "mass": "MZ", + "name": "G0", + "pdg_code": 250, + "propagating": true, + "propagator": "G0_propFeynman", + "spin": 1, + "texname": "{G_0}", + "width": "WZ", + "y_charge": 0 + }, + { + "antiname": "G-", + "antitexname": "{G^-}", + "charge": 1.0, + "color": 1, + "ghost_number": 0, + "goldstoneboson": true, + "lepton_number": 0, + "mass": "MW", + "name": "G+", + "pdg_code": 251, + "propagating": true, + "propagator": "G+_propFeynman", + "spin": 1, + "texname": "{G^+}", + "width": "WW", + "y_charge": 0 + }, + { + "antiname": "G+", + "antitexname": "{G^+}", + "charge": -1.0, + "color": 1, + "ghost_number": 0, + "goldstoneboson": true, + "lepton_number": 0, + "mass": "MW", + "name": "G-", + "pdg_code": -251, + "propagating": true, + "propagator": "G-_propFeynman", + "spin": 1, + "texname": "{G^-}", + "width": "WW", + "y_charge": 0 + }, + { + "antiname": "e+", + "antitexname": "{e^+}", + "charge": -1.0, + "color": 1, + "ghost_number": 0, + "goldstoneboson": false, + "lepton_number": 1, + "mass": "Me", + "name": "e-", + "pdg_code": 11, + "propagating": true, + "propagator": "e-_propFeynman", + "spin": 2, + "texname": "{e^-}", + "width": "ZERO", + "y_charge": 0 + }, + { + "antiname": "e-", + "antitexname": "{e^-}", + "charge": 1.0, + "color": 1, + "ghost_number": 0, + "goldstoneboson": false, + "lepton_number": -1, + "mass": "Me", + "name": "e+", + "pdg_code": -11, + "propagating": true, + "propagator": "e+_propFeynman", + "spin": 2, + "texname": "{e^+}", + "width": "ZERO", + "y_charge": 0 + }, + { + "antiname": "mu+", + "antitexname": "{\\mu^+}", + "charge": -1.0, + "color": 1, + "ghost_number": 0, + "goldstoneboson": false, + "lepton_number": 1, + "mass": "MM", + "name": "mu-", + "pdg_code": 13, + "propagating": true, + "propagator": "mu-_propFeynman", + "spin": 2, + "texname": "{\\mu^-}", + "width": "ZERO", + "y_charge": 0 + }, + { + "antiname": "mu-", + "antitexname": "{\\mu^-}", + "charge": 1.0, + "color": 1, + "ghost_number": 0, + "goldstoneboson": false, + "lepton_number": -1, + "mass": "MM", + "name": "mu+", + "pdg_code": -13, + "propagating": true, + "propagator": "mu+_propFeynman", + "spin": 2, + "texname": "{\\mu^+}", + "width": "ZERO", + "y_charge": 0 + }, + { + "antiname": "ta+", + "antitexname": "{\\tau^+}", + "charge": -1.0, + "color": 1, + "ghost_number": 0, + "goldstoneboson": false, + "lepton_number": 1, + "mass": "MTA", + "name": "ta-", + "pdg_code": 15, + "propagating": true, + "propagator": "ta-_propFeynman", + "spin": 2, + "texname": "{\\tau^-}", + "width": "WTau", + "y_charge": 0 + }, + { + "antiname": "ta-", + "antitexname": "{\\tau^-}", + "charge": 1.0, + "color": 1, + "ghost_number": 0, + "goldstoneboson": false, + "lepton_number": -1, + "mass": "MTA", + "name": "ta+", + "pdg_code": -15, + "propagating": true, + "propagator": "ta+_propFeynman", + "spin": 2, + "texname": "{\\tau^+}", + "width": "WTau", + "y_charge": 0 + } + ], + "propagators": [ + { + "denominator": "(UFO::{}::P(UFO::{}::idx(1,1)))^2", + "name": "a_propFeynman", + "numerator": "-1𝑖*UFO::{}::Metric(UFO::{}::idx(1,1),UFO::{}::idx(1,2))", + "particle": "a" + }, + { + "denominator": "(UFO::{}::P(UFO::{}::idx(1,1)))^2+-1*UFO::{}::MZ^2", + "name": "Z_propFeynman", + "numerator": "(-1*UFO::{}::Metric(UFO::{}::idx(1,1),UFO::{}::idx(1,2))+UFO::{}::MZ^(-2)*UFO::{}::P(UFO::{}::idx(1,1))*UFO::{}::P(UFO::{}::idx(1,2)))*1𝑖", + "particle": "Z" + }, + { + "denominator": "(UFO::{}::P(UFO::{}::idx(1,1)))^2+-1*UFO::{}::MW^2", + "name": "W+_propFeynman", + "numerator": "(-1*UFO::{}::Metric(UFO::{}::idx(1,1),UFO::{}::idx(1,2))+UFO::{}::MW^(-2)*UFO::{}::P(UFO::{}::idx(1,1))*UFO::{}::P(UFO::{}::idx(1,2)))*1𝑖", + "particle": "W+" + }, + { + "denominator": "(UFO::{}::P(UFO::{}::idx(1,1)))^2+-1*UFO::{}::MW^2", + "name": "W-_propFeynman", + "numerator": "(-1*UFO::{}::Metric(UFO::{}::idx(1,1),UFO::{}::idx(1,2))+UFO::{}::MW^(-2)*UFO::{}::P(UFO::{}::idx(1,1))*UFO::{}::P(UFO::{}::idx(1,2)))*1𝑖", + "particle": "W-" + }, + { + "denominator": "(UFO::{}::P(UFO::{}::idx(1,1)))^2", + "name": "g_propFeynman", + "numerator": "-1𝑖*UFO::{}::Metric(UFO::{}::idx(1,1),UFO::{}::idx(1,2))", + "particle": "g" + }, + { + "denominator": "(UFO::{}::P(UFO::{}::idx(1,1)))^2", + "name": "ghA_propFeynman", + "numerator": "1𝑖", + "particle": "ghA" + }, + { + "denominator": "(UFO::{}::P(UFO::{}::idx(1,1)))^2", + "name": "ghA~_propFeynman", + "numerator": "1𝑖", + "particle": "ghA~" + }, + { + "denominator": "(UFO::{}::P(UFO::{}::idx(1,1)))^2+-1*UFO::{}::MZ^2", + "name": "ghZ_propFeynman", + "numerator": "1𝑖", + "particle": "ghZ" + }, + { + "denominator": "(UFO::{}::P(UFO::{}::idx(1,1)))^2+-1*UFO::{}::MZ^2", + "name": "ghZ~_propFeynman", + "numerator": "1𝑖", + "particle": "ghZ~" + }, + { + "denominator": "(UFO::{}::P(UFO::{}::idx(1,1)))^2+-1*UFO::{}::MW^2", + "name": "ghWp_propFeynman", + "numerator": "1𝑖", + "particle": "ghWp" + }, + { + "denominator": "(UFO::{}::P(UFO::{}::idx(1,1)))^2+-1*UFO::{}::MW^2", + "name": "ghWp~_propFeynman", + "numerator": "1𝑖", + "particle": "ghWp~" + }, + { + "denominator": "(UFO::{}::P(UFO::{}::idx(1,1)))^2+-1*UFO::{}::MW^2", + "name": "ghWm_propFeynman", + "numerator": "1𝑖", + "particle": "ghWm" + }, + { + "denominator": "(UFO::{}::P(UFO::{}::idx(1,1)))^2+-1*UFO::{}::MW^2", + "name": "ghWm~_propFeynman", + "numerator": "1𝑖", + "particle": "ghWm~" + }, + { + "denominator": "(UFO::{}::P(UFO::{}::idx(1,1)))^2", + "name": "ghG_propFeynman", + "numerator": "1𝑖", + "particle": "ghG" + }, + { + "denominator": "(UFO::{}::P(UFO::{}::idx(1,1)))^2", + "name": "ghG~_propFeynman", + "numerator": "1𝑖", + "particle": "ghG~" + }, + { + "denominator": "(UFO::{}::P(UFO::{}::idx(1,1)))^2", + "name": "ve_propFeynman", + "numerator": "1𝑖*UFO::{}::PSlash(UFO::{}::idx(1,2),UFO::{}::idx(1,1))", + "particle": "ve" + }, + { + "denominator": "(UFO::{}::P(UFO::{}::idx(1,1)))^2", + "name": "ve~_propFeynman", + "numerator": "-1𝑖*UFO::{}::PSlash(UFO::{}::idx(1,1),UFO::{}::idx(1,2))", + "particle": "ve~" + }, + { + "denominator": "(UFO::{}::P(UFO::{}::idx(1,1)))^2", + "name": "vm_propFeynman", + "numerator": "1𝑖*UFO::{}::PSlash(UFO::{}::idx(1,2),UFO::{}::idx(1,1))", + "particle": "vm" + }, + { + "denominator": "(UFO::{}::P(UFO::{}::idx(1,1)))^2", + "name": "vm~_propFeynman", + "numerator": "-1𝑖*UFO::{}::PSlash(UFO::{}::idx(1,1),UFO::{}::idx(1,2))", + "particle": "vm~" + }, + { + "denominator": "(UFO::{}::P(UFO::{}::idx(1,1)))^2", + "name": "vt_propFeynman", + "numerator": "1𝑖*UFO::{}::PSlash(UFO::{}::idx(1,2),UFO::{}::idx(1,1))", + "particle": "vt" + }, + { + "denominator": "(UFO::{}::P(UFO::{}::idx(1,1)))^2", + "name": "vt~_propFeynman", + "numerator": "-1𝑖*UFO::{}::PSlash(UFO::{}::idx(1,1),UFO::{}::idx(1,2))", + "particle": "vt~" + }, + { + "denominator": "(UFO::{}::P(UFO::{}::idx(1,1)))^2", + "name": "u_propFeynman", + "numerator": "1𝑖*UFO::{}::PSlash(UFO::{}::idx(1,2),UFO::{}::idx(1,1))", + "particle": "u" + }, + { + "denominator": "(UFO::{}::P(UFO::{}::idx(1,1)))^2", + "name": "u~_propFeynman", + "numerator": "-1𝑖*UFO::{}::PSlash(UFO::{}::idx(1,1),UFO::{}::idx(1,2))", + "particle": "u~" + }, + { + "denominator": "(UFO::{}::P(UFO::{}::idx(1,1)))^2+-1*UFO::{}::MC^2", + "name": "c_propFeynman", + "numerator": "(UFO::{}::Identity(UFO::{}::idx(1,2),UFO::{}::idx(1,1))*UFO::{}::MC+UFO::{}::PSlash(UFO::{}::idx(1,2),UFO::{}::idx(1,1)))*1𝑖", + "particle": "c" + }, + { + "denominator": "(UFO::{}::P(UFO::{}::idx(1,1)))^2+-1*UFO::{}::MC^2", + "name": "c~_propFeynman", + "numerator": "(-1*UFO::{}::PSlash(UFO::{}::idx(1,1),UFO::{}::idx(1,2))+UFO::{}::Identity(UFO::{}::idx(1,1),UFO::{}::idx(1,2))*UFO::{}::MC)*1𝑖", + "particle": "c~" + }, + { + "denominator": "(UFO::{}::P(UFO::{}::idx(1,1)))^2+-1*UFO::{}::MT^2", + "name": "t_propFeynman", + "numerator": "(UFO::{}::Identity(UFO::{}::idx(1,2),UFO::{}::idx(1,1))*UFO::{}::MT+UFO::{}::PSlash(UFO::{}::idx(1,2),UFO::{}::idx(1,1)))*1𝑖", + "particle": "t" + }, + { + "denominator": "(UFO::{}::P(UFO::{}::idx(1,1)))^2+-1*UFO::{}::MT^2", + "name": "t~_propFeynman", + "numerator": "(-1*UFO::{}::PSlash(UFO::{}::idx(1,1),UFO::{}::idx(1,2))+UFO::{}::Identity(UFO::{}::idx(1,1),UFO::{}::idx(1,2))*UFO::{}::MT)*1𝑖", + "particle": "t~" + }, + { + "denominator": "(UFO::{}::P(UFO::{}::idx(1,1)))^2", + "name": "d_propFeynman", + "numerator": "1𝑖*UFO::{}::PSlash(UFO::{}::idx(1,2),UFO::{}::idx(1,1))", + "particle": "d" + }, + { + "denominator": "(UFO::{}::P(UFO::{}::idx(1,1)))^2", + "name": "d~_propFeynman", + "numerator": "-1𝑖*UFO::{}::PSlash(UFO::{}::idx(1,1),UFO::{}::idx(1,2))", + "particle": "d~" + }, + { + "denominator": "(UFO::{}::P(UFO::{}::idx(1,1)))^2", + "name": "s_propFeynman", + "numerator": "1𝑖*UFO::{}::PSlash(UFO::{}::idx(1,2),UFO::{}::idx(1,1))", + "particle": "s" + }, + { + "denominator": "(UFO::{}::P(UFO::{}::idx(1,1)))^2", + "name": "s~_propFeynman", + "numerator": "-1𝑖*UFO::{}::PSlash(UFO::{}::idx(1,1),UFO::{}::idx(1,2))", + "particle": "s~" + }, + { + "denominator": "(UFO::{}::P(UFO::{}::idx(1,1)))^2+-1*UFO::{}::MB^2", + "name": "b_propFeynman", + "numerator": "(UFO::{}::Identity(UFO::{}::idx(1,2),UFO::{}::idx(1,1))*UFO::{}::MB+UFO::{}::PSlash(UFO::{}::idx(1,2),UFO::{}::idx(1,1)))*1𝑖", + "particle": "b" + }, + { + "denominator": "(UFO::{}::P(UFO::{}::idx(1,1)))^2+-1*UFO::{}::MB^2", + "name": "b~_propFeynman", + "numerator": "(-1*UFO::{}::PSlash(UFO::{}::idx(1,1),UFO::{}::idx(1,2))+UFO::{}::Identity(UFO::{}::idx(1,1),UFO::{}::idx(1,2))*UFO::{}::MB)*1𝑖", + "particle": "b~" + }, + { + "denominator": "(UFO::{}::P(UFO::{}::idx(1,1)))^2+-1*UFO::{}::MH^2", + "name": "H_propFeynman", + "numerator": "1𝑖", + "particle": "H" + }, + { + "denominator": "(UFO::{}::P(UFO::{}::idx(1,1)))^2+-1*UFO::{}::MZ^2", + "name": "G0_propFeynman", + "numerator": "1𝑖", + "particle": "G0" + }, + { + "denominator": "(UFO::{}::P(UFO::{}::idx(1,1)))^2+-1*UFO::{}::MW^2", + "name": "G+_propFeynman", + "numerator": "1𝑖", + "particle": "G+" + }, + { + "denominator": "(UFO::{}::P(UFO::{}::idx(1,1)))^2+-1*UFO::{}::MW^2", + "name": "G-_propFeynman", + "numerator": "1𝑖", + "particle": "G-" + }, + { + "denominator": "(UFO::{}::P(UFO::{}::idx(1,1)))^2+-1*UFO::{}::Me^2", + "name": "e-_propFeynman", + "numerator": "(UFO::{}::Identity(UFO::{}::idx(1,2),UFO::{}::idx(1,1))*UFO::{}::Me+UFO::{}::PSlash(UFO::{}::idx(1,2),UFO::{}::idx(1,1)))*1𝑖", + "particle": "e-" + }, + { + "denominator": "(UFO::{}::P(UFO::{}::idx(1,1)))^2+-1*UFO::{}::Me^2", + "name": "e+_propFeynman", + "numerator": "(-1*UFO::{}::PSlash(UFO::{}::idx(1,1),UFO::{}::idx(1,2))+UFO::{}::Identity(UFO::{}::idx(1,1),UFO::{}::idx(1,2))*UFO::{}::Me)*1𝑖", + "particle": "e+" + }, + { + "denominator": "(UFO::{}::P(UFO::{}::idx(1,1)))^2+-1*UFO::{}::MM^2", + "name": "mu-_propFeynman", + "numerator": "(UFO::{}::Identity(UFO::{}::idx(1,2),UFO::{}::idx(1,1))*UFO::{}::MM+UFO::{}::PSlash(UFO::{}::idx(1,2),UFO::{}::idx(1,1)))*1𝑖", + "particle": "mu-" + }, + { + "denominator": "(UFO::{}::P(UFO::{}::idx(1,1)))^2+-1*UFO::{}::MM^2", + "name": "mu+_propFeynman", + "numerator": "(-1*UFO::{}::PSlash(UFO::{}::idx(1,1),UFO::{}::idx(1,2))+UFO::{}::Identity(UFO::{}::idx(1,1),UFO::{}::idx(1,2))*UFO::{}::MM)*1𝑖", + "particle": "mu+" + }, + { + "denominator": "(UFO::{}::P(UFO::{}::idx(1,1)))^2+-1*UFO::{}::MTA^2", + "name": "ta-_propFeynman", + "numerator": "(UFO::{}::Identity(UFO::{}::idx(1,2),UFO::{}::idx(1,1))*UFO::{}::MTA+UFO::{}::PSlash(UFO::{}::idx(1,2),UFO::{}::idx(1,1)))*1𝑖", + "particle": "ta-" + }, + { + "denominator": "(UFO::{}::P(UFO::{}::idx(1,1)))^2+-1*UFO::{}::MTA^2", + "name": "ta+_propFeynman", + "numerator": "(-1*UFO::{}::PSlash(UFO::{}::idx(1,1),UFO::{}::idx(1,2))+UFO::{}::Identity(UFO::{}::idx(1,1),UFO::{}::idx(1,2))*UFO::{}::MTA)*1𝑖", + "particle": "ta+" + } + ], + "restriction": null, + "vertex_rules": [ + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_33" + ] + ], + "lorentz_structures": [ + "SSSS1" + ], + "name": "V_1", + "particles": [ + "G0", + "G0", + "G0", + "G0" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_31" + ] + ], + "lorentz_structures": [ + "SSSS1" + ], + "name": "V_2", + "particles": [ + "G0", + "G0", + "G-", + "G+" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_32" + ] + ], + "lorentz_structures": [ + "SSSS1" + ], + "name": "V_3", + "particles": [ + "G-", + "G-", + "G+", + "G+" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_31" + ] + ], + "lorentz_structures": [ + "SSSS1" + ], + "name": "V_4", + "particles": [ + "G0", + "G0", + "H", + "H" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_31" + ] + ], + "lorentz_structures": [ + "SSSS1" + ], + "name": "V_5", + "particles": [ + "G-", + "G+", + "H", + "H" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_33" + ] + ], + "lorentz_structures": [ + "SSSS1" + ], + "name": "V_6", + "particles": [ + "H", + "H", + "H", + "H" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_68" + ] + ], + "lorentz_structures": [ + "SSS1" + ], + "name": "V_7", + "particles": [ + "G0", + "G0", + "H" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_68" + ] + ], + "lorentz_structures": [ + "SSS1" + ], + "name": "V_8", + "particles": [ + "G-", + "G+", + "H" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_69" + ] + ], + "lorentz_structures": [ + "SSS1" + ], + "name": "V_9", + "particles": [ + "H", + "H", + "H" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_6" + ] + ], + "lorentz_structures": [ + "VVSS1" + ], + "name": "V_10", + "particles": [ + "a", + "a", + "G-", + "G+" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_3" + ] + ], + "lorentz_structures": [ + "VSS1" + ], + "name": "V_11", + "particles": [ + "a", + "G-", + "G+" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_3" + ] + ], + "lorentz_structures": [ + "UUV1" + ], + "name": "V_12", + "particles": [ + "ghA", + "ghWm~", + "W-" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_4" + ] + ], + "lorentz_structures": [ + "UUV1" + ], + "name": "V_13", + "particles": [ + "ghA", + "ghWp~", + "W+" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_75" + ] + ], + "lorentz_structures": [ + "UUS1" + ], + "name": "V_14", + "particles": [ + "ghWm", + "ghA~", + "G+" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_3" + ] + ], + "lorentz_structures": [ + "UUV1" + ], + "name": "V_15", + "particles": [ + "ghWm", + "ghA~", + "W+" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_70" + ] + ], + "lorentz_structures": [ + "UUS1" + ], + "name": "V_16", + "particles": [ + "ghWm", + "ghWm~", + "G0" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_71" + ] + ], + "lorentz_structures": [ + "UUS1" + ], + "name": "V_17", + "particles": [ + "ghWm", + "ghWm~", + "H" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_4" + ] + ], + "lorentz_structures": [ + "UUV1" + ], + "name": "V_18", + "particles": [ + "ghWm", + "ghWm~", + "a" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_53" + ] + ], + "lorentz_structures": [ + "UUV1" + ], + "name": "V_19", + "particles": [ + "ghWm", + "ghWm~", + "Z" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_78" + ] + ], + "lorentz_structures": [ + "UUS1" + ], + "name": "V_20", + "particles": [ + "ghWm", + "ghZ~", + "G+" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_52" + ] + ], + "lorentz_structures": [ + "UUV1" + ], + "name": "V_21", + "particles": [ + "ghWm", + "ghZ~", + "W+" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_74" + ] + ], + "lorentz_structures": [ + "UUS1" + ], + "name": "V_22", + "particles": [ + "ghWp", + "ghA~", + "G-" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_4" + ] + ], + "lorentz_structures": [ + "UUV1" + ], + "name": "V_23", + "particles": [ + "ghWp", + "ghA~", + "W-" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_73" + ] + ], + "lorentz_structures": [ + "UUS1" + ], + "name": "V_24", + "particles": [ + "ghWp", + "ghWp~", + "G0" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_71" + ] + ], + "lorentz_structures": [ + "UUS1" + ], + "name": "V_25", + "particles": [ + "ghWp", + "ghWp~", + "H" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_3" + ] + ], + "lorentz_structures": [ + "UUV1" + ], + "name": "V_26", + "particles": [ + "ghWp", + "ghWp~", + "a" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_52" + ] + ], + "lorentz_structures": [ + "UUV1" + ], + "name": "V_27", + "particles": [ + "ghWp", + "ghWp~", + "Z" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_77" + ] + ], + "lorentz_structures": [ + "UUS1" + ], + "name": "V_28", + "particles": [ + "ghWp", + "ghZ~", + "G-" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_53" + ] + ], + "lorentz_structures": [ + "UUV1" + ], + "name": "V_29", + "particles": [ + "ghWp", + "ghZ~", + "W-" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_79" + ] + ], + "lorentz_structures": [ + "UUS1" + ], + "name": "V_30", + "particles": [ + "ghZ", + "ghWm~", + "G-" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_52" + ] + ], + "lorentz_structures": [ + "UUV1" + ], + "name": "V_31", + "particles": [ + "ghZ", + "ghWm~", + "W-" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_76" + ] + ], + "lorentz_structures": [ + "UUS1" + ], + "name": "V_32", + "particles": [ + "ghZ", + "ghWp~", + "G+" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_53" + ] + ], + "lorentz_structures": [ + "UUV1" + ], + "name": "V_33", + "particles": [ + "ghZ", + "ghWp~", + "W+" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_80" + ] + ], + "lorentz_structures": [ + "UUS1" + ], + "name": "V_34", + "particles": [ + "ghZ", + "ghZ~", + "H" + ] + }, + { + "color_structures": [ + "UFO::{}::f(3,1,2)" + ], + "couplings": [ + [ + "GC_10" + ] + ], + "lorentz_structures": [ + "UUV1" + ], + "name": "V_35", + "particles": [ + "ghG", + "ghG~", + "g" + ] + }, + { + "color_structures": [ + "UFO::{}::f(1,2,3)" + ], + "couplings": [ + [ + "GC_10" + ] + ], + "lorentz_structures": [ + "VVV1" + ], + "name": "V_36", + "particles": [ + "g", + "g", + "g" + ] + }, + { + "color_structures": [ + "UFO::{}::f(-1,1,2)*UFO::{}::f(3,4,-1)", + "UFO::{}::f(-1,1,3)*UFO::{}::f(2,4,-1)", + "UFO::{}::f(-1,1,4)*UFO::{}::f(2,3,-1)" + ], + "couplings": [ + [ + "GC_12", + null, + null + ], + [ + null, + "GC_12", + null + ], + [ + null, + null, + "GC_12" + ] + ], + "lorentz_structures": [ + "VVVV1", + "VVVV3", + "VVVV4" + ], + "name": "V_37", + "particles": [ + "g", + "g", + "g", + "g" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_55" + ] + ], + "lorentz_structures": [ + "VVSS1" + ], + "name": "V_38", + "particles": [ + "a", + "W-", + "G0", + "G+" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_54" + ] + ], + "lorentz_structures": [ + "VVSS1" + ], + "name": "V_39", + "particles": [ + "a", + "W-", + "G+", + "H" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_74" + ] + ], + "lorentz_structures": [ + "VVS1" + ], + "name": "V_40", + "particles": [ + "a", + "W-", + "G+" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_39" + ] + ], + "lorentz_structures": [ + "VSS1" + ], + "name": "V_41", + "particles": [ + "W-", + "G0", + "G+" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_37" + ] + ], + "lorentz_structures": [ + "VSS1" + ], + "name": "V_42", + "particles": [ + "W-", + "G+", + "H" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_4" + ] + ], + "lorentz_structures": [ + "VVV1" + ], + "name": "V_43", + "particles": [ + "a", + "W-", + "W+" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_55" + ] + ], + "lorentz_structures": [ + "VVSS1" + ], + "name": "V_44", + "particles": [ + "a", + "W+", + "G0", + "G-" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_56" + ] + ], + "lorentz_structures": [ + "VVSS1" + ], + "name": "V_45", + "particles": [ + "a", + "W+", + "G-", + "H" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_75" + ] + ], + "lorentz_structures": [ + "VVS1" + ], + "name": "V_46", + "particles": [ + "a", + "W+", + "G-" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_38" + ] + ], + "lorentz_structures": [ + "VSS1" + ], + "name": "V_47", + "particles": [ + "W+", + "G0", + "G-" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_37" + ] + ], + "lorentz_structures": [ + "VSS1" + ], + "name": "V_48", + "particles": [ + "W+", + "G-", + "H" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_34" + ] + ], + "lorentz_structures": [ + "VVSS1" + ], + "name": "V_49", + "particles": [ + "W-", + "W+", + "G0", + "G0" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_34" + ] + ], + "lorentz_structures": [ + "VVSS1" + ], + "name": "V_50", + "particles": [ + "W-", + "W+", + "G-", + "G+" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_34" + ] + ], + "lorentz_structures": [ + "VVSS1" + ], + "name": "V_51", + "particles": [ + "W-", + "W+", + "H", + "H" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_72" + ] + ], + "lorentz_structures": [ + "VVS1" + ], + "name": "V_52", + "particles": [ + "W-", + "W+", + "H" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_5" + ] + ], + "lorentz_structures": [ + "VVVV2" + ], + "name": "V_53", + "particles": [ + "a", + "a", + "W-", + "W+" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_53" + ] + ], + "lorentz_structures": [ + "VVV1" + ], + "name": "V_54", + "particles": [ + "W-", + "W+", + "Z" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_35" + ] + ], + "lorentz_structures": [ + "VVVV2" + ], + "name": "V_55", + "particles": [ + "W-", + "W-", + "W+", + "W+" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_63" + ] + ], + "lorentz_structures": [ + "VVSS1" + ], + "name": "V_56", + "particles": [ + "a", + "Z", + "G-", + "G+" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_60" + ] + ], + "lorentz_structures": [ + "VSS1" + ], + "name": "V_57", + "particles": [ + "Z", + "G0", + "H" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_61" + ] + ], + "lorentz_structures": [ + "VSS1" + ], + "name": "V_58", + "particles": [ + "Z", + "G-", + "G+" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_8" + ] + ], + "lorentz_structures": [ + "VVSS1" + ], + "name": "V_59", + "particles": [ + "W-", + "Z", + "G0", + "G+" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_9" + ] + ], + "lorentz_structures": [ + "VVSS1" + ], + "name": "V_60", + "particles": [ + "W-", + "Z", + "G+", + "H" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_67" + ] + ], + "lorentz_structures": [ + "VVS1" + ], + "name": "V_61", + "particles": [ + "W-", + "Z", + "G+" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_8" + ] + ], + "lorentz_structures": [ + "VVSS1" + ], + "name": "V_62", + "particles": [ + "W+", + "Z", + "G0", + "G-" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_7" + ] + ], + "lorentz_structures": [ + "VVSS1" + ], + "name": "V_63", + "particles": [ + "W+", + "Z", + "G-", + "H" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_66" + ] + ], + "lorentz_structures": [ + "VVS1" + ], + "name": "V_64", + "particles": [ + "W+", + "Z", + "G-" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_57" + ] + ], + "lorentz_structures": [ + "VVVV5" + ], + "name": "V_65", + "particles": [ + "a", + "W-", + "W+", + "Z" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_65" + ] + ], + "lorentz_structures": [ + "VVSS1" + ], + "name": "V_66", + "particles": [ + "Z", + "Z", + "G0", + "G0" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_64" + ] + ], + "lorentz_structures": [ + "VVSS1" + ], + "name": "V_67", + "particles": [ + "Z", + "Z", + "G-", + "G+" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_65" + ] + ], + "lorentz_structures": [ + "VVSS1" + ], + "name": "V_68", + "particles": [ + "Z", + "Z", + "H", + "H" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_81" + ] + ], + "lorentz_structures": [ + "VVS1" + ], + "name": "V_69", + "particles": [ + "Z", + "Z", + "H" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_36" + ] + ], + "lorentz_structures": [ + "VVVV2" + ], + "name": "V_70", + "particles": [ + "W-", + "W+", + "Z", + "Z" + ] + }, + { + "color_structures": [ + "UFO::{}::Identity(1,2)" + ], + "couplings": [ + [ + "GC_1" + ] + ], + "lorentz_structures": [ + "FFV1" + ], + "name": "V_71", + "particles": [ + "d~", + "d", + "a" + ] + }, + { + "color_structures": [ + "UFO::{}::Identity(1,2)" + ], + "couplings": [ + [ + "GC_1" + ] + ], + "lorentz_structures": [ + "FFV1" + ], + "name": "V_72", + "particles": [ + "s~", + "s", + "a" + ] + }, + { + "color_structures": [ + "UFO::{}::Identity(1,2)" + ], + "couplings": [ + [ + "GC_1" + ] + ], + "lorentz_structures": [ + "FFV1" + ], + "name": "V_73", + "particles": [ + "b~", + "b", + "a" + ] + }, + { + "color_structures": [ + "UFO::{}::T(3,2,1)" + ], + "couplings": [ + [ + "GC_11" + ] + ], + "lorentz_structures": [ + "FFV1" + ], + "name": "V_74", + "particles": [ + "d~", + "d", + "g" + ] + }, + { + "color_structures": [ + "UFO::{}::T(3,2,1)" + ], + "couplings": [ + [ + "GC_11" + ] + ], + "lorentz_structures": [ + "FFV1" + ], + "name": "V_75", + "particles": [ + "s~", + "s", + "g" + ] + }, + { + "color_structures": [ + "UFO::{}::T(3,2,1)" + ], + "couplings": [ + [ + "GC_11" + ] + ], + "lorentz_structures": [ + "FFV1" + ], + "name": "V_76", + "particles": [ + "b~", + "b", + "g" + ] + }, + { + "color_structures": [ + "UFO::{}::Identity(1,2)" + ], + "couplings": [ + [ + "GC_82" + ] + ], + "lorentz_structures": [ + "FFS2" + ], + "name": "V_77", + "particles": [ + "b~", + "b", + "G0" + ] + }, + { + "color_structures": [ + "UFO::{}::Identity(1,2)" + ], + "couplings": [ + [ + "GC_83" + ] + ], + "lorentz_structures": [ + "FFS4" + ], + "name": "V_78", + "particles": [ + "b~", + "b", + "H" + ] + }, + { + "color_structures": [ + "UFO::{}::Identity(1,2)" + ], + "couplings": [ + [ + "GC_50", + "GC_58" + ] + ], + "lorentz_structures": [ + "FFV2", + "FFV3" + ], + "name": "V_79", + "particles": [ + "d~", + "d", + "Z" + ] + }, + { + "color_structures": [ + "UFO::{}::Identity(1,2)" + ], + "couplings": [ + [ + "GC_50", + "GC_58" + ] + ], + "lorentz_structures": [ + "FFV2", + "FFV3" + ], + "name": "V_80", + "particles": [ + "s~", + "s", + "Z" + ] + }, + { + "color_structures": [ + "UFO::{}::Identity(1,2)" + ], + "couplings": [ + [ + "GC_50", + "GC_58" + ] + ], + "lorentz_structures": [ + "FFV2", + "FFV3" + ], + "name": "V_81", + "particles": [ + "b~", + "b", + "Z" + ] + }, + { + "color_structures": [ + "UFO::{}::Identity(1,2)" + ], + "couplings": [ + [ + "GC_16" + ] + ], + "lorentz_structures": [ + "FFS3" + ], + "name": "V_82", + "particles": [ + "c~", + "d", + "G+" + ] + }, + { + "color_structures": [ + "UFO::{}::Identity(1,2)" + ], + "couplings": [ + [ + "GC_17" + ] + ], + "lorentz_structures": [ + "FFS3" + ], + "name": "V_83", + "particles": [ + "t~", + "d", + "G+" + ] + }, + { + "color_structures": [ + "UFO::{}::Identity(1,2)" + ], + "couplings": [ + [ + "GC_18" + ] + ], + "lorentz_structures": [ + "FFS3" + ], + "name": "V_84", + "particles": [ + "c~", + "s", + "G+" + ] + }, + { + "color_structures": [ + "UFO::{}::Identity(1,2)" + ], + "couplings": [ + [ + "GC_19" + ] + ], + "lorentz_structures": [ + "FFS3" + ], + "name": "V_85", + "particles": [ + "t~", + "s", + "G+" + ] + }, + { + "color_structures": [ + "UFO::{}::Identity(1,2)" + ], + "couplings": [ + [ + "GC_13" + ] + ], + "lorentz_structures": [ + "FFS1" + ], + "name": "V_86", + "particles": [ + "u~", + "b", + "G+" + ] + }, + { + "color_structures": [ + "UFO::{}::Identity(1,2)" + ], + "couplings": [ + [ + "GC_14", + "GC_20" + ] + ], + "lorentz_structures": [ + "FFS1", + "FFS3" + ], + "name": "V_87", + "particles": [ + "c~", + "b", + "G+" + ] + }, + { + "color_structures": [ + "UFO::{}::Identity(1,2)" + ], + "couplings": [ + [ + "GC_15", + "GC_21" + ] + ], + "lorentz_structures": [ + "FFS1", + "FFS3" + ], + "name": "V_88", + "particles": [ + "t~", + "b", + "G+" + ] + }, + { + "color_structures": [ + "UFO::{}::Identity(1,2)" + ], + "couplings": [ + [ + "GC_100" + ] + ], + "lorentz_structures": [ + "FFV2" + ], + "name": "V_89", + "particles": [ + "u~", + "d", + "W+" + ] + }, + { + "color_structures": [ + "UFO::{}::Identity(1,2)" + ], + "couplings": [ + [ + "GC_103" + ] + ], + "lorentz_structures": [ + "FFV2" + ], + "name": "V_90", + "particles": [ + "c~", + "d", + "W+" + ] + }, + { + "color_structures": [ + "UFO::{}::Identity(1,2)" + ], + "couplings": [ + [ + "GC_106" + ] + ], + "lorentz_structures": [ + "FFV2" + ], + "name": "V_91", + "particles": [ + "t~", + "d", + "W+" + ] + }, + { + "color_structures": [ + "UFO::{}::Identity(1,2)" + ], + "couplings": [ + [ + "GC_101" + ] + ], + "lorentz_structures": [ + "FFV2" + ], + "name": "V_92", + "particles": [ + "u~", + "s", + "W+" + ] + }, + { + "color_structures": [ + "UFO::{}::Identity(1,2)" + ], + "couplings": [ + [ + "GC_104" + ] + ], + "lorentz_structures": [ + "FFV2" + ], + "name": "V_93", + "particles": [ + "c~", + "s", + "W+" + ] + }, + { + "color_structures": [ + "UFO::{}::Identity(1,2)" + ], + "couplings": [ + [ + "GC_107" + ] + ], + "lorentz_structures": [ + "FFV2" + ], + "name": "V_94", + "particles": [ + "t~", + "s", + "W+" + ] + }, + { + "color_structures": [ + "UFO::{}::Identity(1,2)" + ], + "couplings": [ + [ + "GC_102" + ] + ], + "lorentz_structures": [ + "FFV2" + ], + "name": "V_95", + "particles": [ + "u~", + "b", + "W+" + ] + }, + { + "color_structures": [ + "UFO::{}::Identity(1,2)" + ], + "couplings": [ + [ + "GC_105" + ] + ], + "lorentz_structures": [ + "FFV2" + ], + "name": "V_96", + "particles": [ + "c~", + "b", + "W+" + ] + }, + { + "color_structures": [ + "UFO::{}::Identity(1,2)" + ], + "couplings": [ + [ + "GC_108" + ] + ], + "lorentz_structures": [ + "FFV2" + ], + "name": "V_97", + "particles": [ + "t~", + "b", + "W+" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_3" + ] + ], + "lorentz_structures": [ + "FFV1" + ], + "name": "V_98", + "particles": [ + "e+", + "e-", + "a" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_3" + ] + ], + "lorentz_structures": [ + "FFV1" + ], + "name": "V_99", + "particles": [ + "mu+", + "mu-", + "a" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_3" + ] + ], + "lorentz_structures": [ + "FFV1" + ], + "name": "V_100", + "particles": [ + "ta+", + "ta-", + "a" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_88" + ] + ], + "lorentz_structures": [ + "FFS2" + ], + "name": "V_101", + "particles": [ + "e+", + "e-", + "G0" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_92" + ] + ], + "lorentz_structures": [ + "FFS2" + ], + "name": "V_102", + "particles": [ + "mu+", + "mu-", + "G0" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_98" + ] + ], + "lorentz_structures": [ + "FFS2" + ], + "name": "V_103", + "particles": [ + "ta+", + "ta-", + "G0" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_89" + ] + ], + "lorentz_structures": [ + "FFS4" + ], + "name": "V_104", + "particles": [ + "e+", + "e-", + "H" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_93" + ] + ], + "lorentz_structures": [ + "FFS4" + ], + "name": "V_105", + "particles": [ + "mu+", + "mu-", + "H" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_99" + ] + ], + "lorentz_structures": [ + "FFS4" + ], + "name": "V_106", + "particles": [ + "ta+", + "ta-", + "H" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_50", + "GC_59" + ] + ], + "lorentz_structures": [ + "FFV2", + "FFV4" + ], + "name": "V_107", + "particles": [ + "e+", + "e-", + "Z" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_50", + "GC_59" + ] + ], + "lorentz_structures": [ + "FFV2", + "FFV4" + ], + "name": "V_108", + "particles": [ + "mu+", + "mu-", + "Z" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_50", + "GC_59" + ] + ], + "lorentz_structures": [ + "FFV2", + "FFV4" + ], + "name": "V_109", + "particles": [ + "ta+", + "ta-", + "Z" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_87" + ] + ], + "lorentz_structures": [ + "FFS1" + ], + "name": "V_110", + "particles": [ + "ve~", + "e-", + "G+" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_91" + ] + ], + "lorentz_structures": [ + "FFS1" + ], + "name": "V_111", + "particles": [ + "vm~", + "mu-", + "G+" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_97" + ] + ], + "lorentz_structures": [ + "FFS1" + ], + "name": "V_112", + "particles": [ + "vt~", + "ta-", + "G+" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_40" + ] + ], + "lorentz_structures": [ + "FFV2" + ], + "name": "V_113", + "particles": [ + "ve~", + "e-", + "W+" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_40" + ] + ], + "lorentz_structures": [ + "FFV2" + ], + "name": "V_114", + "particles": [ + "vm~", + "mu-", + "W+" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_40" + ] + ], + "lorentz_structures": [ + "FFV2" + ], + "name": "V_115", + "particles": [ + "vt~", + "ta-", + "W+" + ] + }, + { + "color_structures": [ + "UFO::{}::Identity(1,2)" + ], + "couplings": [ + [ + "GC_28" + ] + ], + "lorentz_structures": [ + "FFS3" + ], + "name": "V_116", + "particles": [ + "b~", + "u", + "G-" + ] + }, + { + "color_structures": [ + "UFO::{}::Identity(1,2)" + ], + "couplings": [ + [ + "GC_22" + ] + ], + "lorentz_structures": [ + "FFS1" + ], + "name": "V_117", + "particles": [ + "d~", + "c", + "G-" + ] + }, + { + "color_structures": [ + "UFO::{}::Identity(1,2)" + ], + "couplings": [ + [ + "GC_23" + ] + ], + "lorentz_structures": [ + "FFS1" + ], + "name": "V_118", + "particles": [ + "s~", + "c", + "G-" + ] + }, + { + "color_structures": [ + "UFO::{}::Identity(1,2)" + ], + "couplings": [ + [ + "GC_24", + "GC_29" + ] + ], + "lorentz_structures": [ + "FFS1", + "FFS3" + ], + "name": "V_119", + "particles": [ + "b~", + "c", + "G-" + ] + }, + { + "color_structures": [ + "UFO::{}::Identity(1,2)" + ], + "couplings": [ + [ + "GC_25" + ] + ], + "lorentz_structures": [ + "FFS1" + ], + "name": "V_120", + "particles": [ + "d~", + "t", + "G-" + ] + }, + { + "color_structures": [ + "UFO::{}::Identity(1,2)" + ], + "couplings": [ + [ + "GC_26" + ] + ], + "lorentz_structures": [ + "FFS1" + ], + "name": "V_121", + "particles": [ + "s~", + "t", + "G-" + ] + }, + { + "color_structures": [ + "UFO::{}::Identity(1,2)" + ], + "couplings": [ + [ + "GC_27", + "GC_30" + ] + ], + "lorentz_structures": [ + "FFS1", + "FFS3" + ], + "name": "V_122", + "particles": [ + "b~", + "t", + "G-" + ] + }, + { + "color_structures": [ + "UFO::{}::Identity(1,2)" + ], + "couplings": [ + [ + "GC_41" + ] + ], + "lorentz_structures": [ + "FFV2" + ], + "name": "V_123", + "particles": [ + "d~", + "u", + "W-" + ] + }, + { + "color_structures": [ + "UFO::{}::Identity(1,2)" + ], + "couplings": [ + [ + "GC_42" + ] + ], + "lorentz_structures": [ + "FFV2" + ], + "name": "V_124", + "particles": [ + "s~", + "u", + "W-" + ] + }, + { + "color_structures": [ + "UFO::{}::Identity(1,2)" + ], + "couplings": [ + [ + "GC_43" + ] + ], + "lorentz_structures": [ + "FFV2" + ], + "name": "V_125", + "particles": [ + "b~", + "u", + "W-" + ] + }, + { + "color_structures": [ + "UFO::{}::Identity(1,2)" + ], + "couplings": [ + [ + "GC_44" + ] + ], + "lorentz_structures": [ + "FFV2" + ], + "name": "V_126", + "particles": [ + "d~", + "c", + "W-" + ] + }, + { + "color_structures": [ + "UFO::{}::Identity(1,2)" + ], + "couplings": [ + [ + "GC_45" + ] + ], + "lorentz_structures": [ + "FFV2" + ], + "name": "V_127", + "particles": [ + "s~", + "c", + "W-" + ] + }, + { + "color_structures": [ + "UFO::{}::Identity(1,2)" + ], + "couplings": [ + [ + "GC_46" + ] + ], + "lorentz_structures": [ + "FFV2" + ], + "name": "V_128", + "particles": [ + "b~", + "c", + "W-" + ] + }, + { + "color_structures": [ + "UFO::{}::Identity(1,2)" + ], + "couplings": [ + [ + "GC_47" + ] + ], + "lorentz_structures": [ + "FFV2" + ], + "name": "V_129", + "particles": [ + "d~", + "t", + "W-" + ] + }, + { + "color_structures": [ + "UFO::{}::Identity(1,2)" + ], + "couplings": [ + [ + "GC_48" + ] + ], + "lorentz_structures": [ + "FFV2" + ], + "name": "V_130", + "particles": [ + "s~", + "t", + "W-" + ] + }, + { + "color_structures": [ + "UFO::{}::Identity(1,2)" + ], + "couplings": [ + [ + "GC_49" + ] + ], + "lorentz_structures": [ + "FFV2" + ], + "name": "V_131", + "particles": [ + "b~", + "t", + "W-" + ] + }, + { + "color_structures": [ + "UFO::{}::Identity(1,2)" + ], + "couplings": [ + [ + "GC_2" + ] + ], + "lorentz_structures": [ + "FFV1" + ], + "name": "V_132", + "particles": [ + "u~", + "u", + "a" + ] + }, + { + "color_structures": [ + "UFO::{}::Identity(1,2)" + ], + "couplings": [ + [ + "GC_2" + ] + ], + "lorentz_structures": [ + "FFV1" + ], + "name": "V_133", + "particles": [ + "c~", + "c", + "a" + ] + }, + { + "color_structures": [ + "UFO::{}::Identity(1,2)" + ], + "couplings": [ + [ + "GC_2" + ] + ], + "lorentz_structures": [ + "FFV1" + ], + "name": "V_134", + "particles": [ + "t~", + "t", + "a" + ] + }, + { + "color_structures": [ + "UFO::{}::T(3,2,1)" + ], + "couplings": [ + [ + "GC_11" + ] + ], + "lorentz_structures": [ + "FFV1" + ], + "name": "V_135", + "particles": [ + "u~", + "u", + "g" + ] + }, + { + "color_structures": [ + "UFO::{}::T(3,2,1)" + ], + "couplings": [ + [ + "GC_11" + ] + ], + "lorentz_structures": [ + "FFV1" + ], + "name": "V_136", + "particles": [ + "c~", + "c", + "g" + ] + }, + { + "color_structures": [ + "UFO::{}::T(3,2,1)" + ], + "couplings": [ + [ + "GC_11" + ] + ], + "lorentz_structures": [ + "FFV1" + ], + "name": "V_137", + "particles": [ + "t~", + "t", + "g" + ] + }, + { + "color_structures": [ + "UFO::{}::Identity(1,2)" + ], + "couplings": [ + [ + "GC_85" + ] + ], + "lorentz_structures": [ + "FFS2" + ], + "name": "V_138", + "particles": [ + "c~", + "c", + "G0" + ] + }, + { + "color_structures": [ + "UFO::{}::Identity(1,2)" + ], + "couplings": [ + [ + "GC_95" + ] + ], + "lorentz_structures": [ + "FFS2" + ], + "name": "V_139", + "particles": [ + "t~", + "t", + "G0" + ] + }, + { + "color_structures": [ + "UFO::{}::Identity(1,2)" + ], + "couplings": [ + [ + "GC_84" + ] + ], + "lorentz_structures": [ + "FFS4" + ], + "name": "V_140", + "particles": [ + "c~", + "c", + "H" + ] + }, + { + "color_structures": [ + "UFO::{}::Identity(1,2)" + ], + "couplings": [ + [ + "GC_94" + ] + ], + "lorentz_structures": [ + "FFS4" + ], + "name": "V_141", + "particles": [ + "t~", + "t", + "H" + ] + }, + { + "color_structures": [ + "UFO::{}::Identity(1,2)" + ], + "couplings": [ + [ + "GC_51", + "GC_58" + ] + ], + "lorentz_structures": [ + "FFV2", + "FFV5" + ], + "name": "V_142", + "particles": [ + "u~", + "u", + "Z" + ] + }, + { + "color_structures": [ + "UFO::{}::Identity(1,2)" + ], + "couplings": [ + [ + "GC_51", + "GC_58" + ] + ], + "lorentz_structures": [ + "FFV2", + "FFV5" + ], + "name": "V_143", + "particles": [ + "c~", + "c", + "Z" + ] + }, + { + "color_structures": [ + "UFO::{}::Identity(1,2)" + ], + "couplings": [ + [ + "GC_51", + "GC_58" + ] + ], + "lorentz_structures": [ + "FFV2", + "FFV5" + ], + "name": "V_144", + "particles": [ + "t~", + "t", + "Z" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_86" + ] + ], + "lorentz_structures": [ + "FFS3" + ], + "name": "V_145", + "particles": [ + "e+", + "ve", + "G-" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_90" + ] + ], + "lorentz_structures": [ + "FFS3" + ], + "name": "V_146", + "particles": [ + "mu+", + "vm", + "G-" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_96" + ] + ], + "lorentz_structures": [ + "FFS3" + ], + "name": "V_147", + "particles": [ + "ta+", + "vt", + "G-" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_40" + ] + ], + "lorentz_structures": [ + "FFV2" + ], + "name": "V_148", + "particles": [ + "e+", + "ve", + "W-" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_40" + ] + ], + "lorentz_structures": [ + "FFV2" + ], + "name": "V_149", + "particles": [ + "mu+", + "vm", + "W-" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_40" + ] + ], + "lorentz_structures": [ + "FFV2" + ], + "name": "V_150", + "particles": [ + "ta+", + "vt", + "W-" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_62" + ] + ], + "lorentz_structures": [ + "FFV2" + ], + "name": "V_151", + "particles": [ + "ve~", + "ve", + "Z" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_62" + ] + ], + "lorentz_structures": [ + "FFV2" + ], + "name": "V_152", + "particles": [ + "vm~", + "vm", + "Z" + ] + }, + { + "color_structures": [ + "1" + ], + "couplings": [ + [ + "GC_62" + ] + ], + "lorentz_structures": [ + "FFV2" + ], + "name": "V_153", + "particles": [ + "vt~", + "vt", + "Z" + ] + } + ] +} \ No newline at end of file diff --git a/pyamplicol/src/pyamplicol/assets/models/ufo/scalar_gravity/__init__.py b/pyamplicol/src/pyamplicol/assets/models/ufo/scalar_gravity/__init__.py new file mode 100644 index 0000000000..9a3ad6aac9 --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/models/ufo/scalar_gravity/__init__.py @@ -0,0 +1,25 @@ +# type: ignore +from __future__ import absolute_import +from . import particles +from . import couplings +from . import lorentz +from . import parameters +from . import vertices +from . import coupling_orders +from . import function_library +from . import object_library + +gauge = [0] + +all_particles = particles.all_particles +all_vertices = vertices.all_vertices +all_couplings = couplings.all_couplings +all_lorentz = lorentz.all_lorentz +all_parameters = parameters.all_parameters +all_orders = coupling_orders.all_orders +all_functions = function_library.all_functions + + +__author__ = "Valentin Hirschi" +__version__ = "1.0" +__email__ = "valentin.hirschi@gmail.com" diff --git a/pyamplicol/src/pyamplicol/assets/models/ufo/scalar_gravity/coupling_orders.py b/pyamplicol/src/pyamplicol/assets/models/ufo/scalar_gravity/coupling_orders.py new file mode 100644 index 0000000000..f6a1ec99fb --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/models/ufo/scalar_gravity/coupling_orders.py @@ -0,0 +1,16 @@ +# type: ignore +from __future__ import absolute_import +from .object_library import all_orders, CouplingOrder + +# Keep the coupling order names QCD and QED as they are hard-coded standards in some generators +QCD = CouplingOrder(name='QCD', + expansion_order=99, + hierarchy=1) + +QED = CouplingOrder(name='QED', + expansion_order=99, + hierarchy=2) + +GRAV = CouplingOrder(name='GRAV', + expansion_order=99, + hierarchy=0) diff --git a/pyamplicol/src/pyamplicol/assets/models/ufo/scalar_gravity/couplings.py b/pyamplicol/src/pyamplicol/assets/models/ufo/scalar_gravity/couplings.py new file mode 100644 index 0000000000..643747e8f3 --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/models/ufo/scalar_gravity/couplings.py @@ -0,0 +1,40 @@ +# type: ignore +from __future__ import absolute_import +from .object_library import all_couplings, Coupling +from . import parameters as Param + +for i_scalar in list(range(Param.N_SCALARS)): + globals()['SSTmpart%d' % i_scalar] = Coupling(name='SSTmpart%d' % i_scalar, + value='%s**2*kappa*complex(0,1)/2' % Param.scalar_mass_parameters[ + i_scalar].name if i_scalar > 0 else "ZERO", + order={'GRAV': 1}) + + globals()['SSTTTmpart%d' % i_scalar] = Coupling(name='SSTTTmpart%d' % i_scalar, + value='%s**2*kappa**3*complex(0,1)/32' % Param.scalar_mass_parameters[ + i_scalar].name if i_scalar > 0 else "ZERO", + order={'GRAV': 3}) + + globals()['SSTTmpart%d' % i_scalar] = Coupling(name='SSTTmpart%d' % i_scalar, + value='%s**2*kappa**2*complex(0,1)/8' % Param.scalar_mass_parameters[ + i_scalar].name if i_scalar > 0 else "ZERO", + order={'GRAV': 2}) + +SST = Coupling(name='SST', + value='kappa*complex(0,1)/2', + order={'GRAV': 1}) + +SSTT = Coupling(name='SSTT', + value='kappa**2*complex(0,1)/8', + order={'GRAV': 2}) + +SSTTT = Coupling(name='SSTTT', + value='kappa**3*complex(0,1)/32', + order={'GRAV': 3}) + +TTT = Coupling(name='TTT', + value='kappa*complex(0,1)/8', + order={'GRAV': 1}) + +TTTT = Coupling(name='TTTT', + value='kappa**2*complex(0,1)/32', + order={'GRAV': 2}) diff --git a/pyamplicol/src/pyamplicol/assets/models/ufo/scalar_gravity/function_library.py b/pyamplicol/src/pyamplicol/assets/models/ufo/scalar_gravity/function_library.py new file mode 100644 index 0000000000..b2843b6ca0 --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/models/ufo/scalar_gravity/function_library.py @@ -0,0 +1,52 @@ +# type: ignore +# This file is part of the UFO. +# +# This file contains definitions for functions that +# are extensions of the cmath library, and correspond +# either to functions that are in cmath, but inconvenient +# to access from there (e.g. z.conjugate()), +# or functions that are simply not defined. +# +# +from __future__ import absolute_import +__date__ = "22 July 2010" +__author__ = "claude.duhr@durham.ac.uk" + +from cmath import cos, sin, acos, asin +from .object_library import all_functions, Function + +# +# shortcuts for functions from cmath +# + +complexconjugate = Function(name='complexconjugate', + arguments=('z',), + expression='z.conjugate()') + + +re = Function(name='re', + arguments=('z',), + expression='z.real') + +im = Function(name='im', + arguments=('z',), + expression='z.imag') + + +# New functions (trigonometric) + +sec = Function(name='sec', + arguments=('z',), + expression='1./cos(z)') + +asec = Function(name='asec', + arguments=('z',), + expression='acos(1./z)') + +csc = Function(name='csc', + arguments=('z',), + expression='1./sin(z)') + +acsc = Function(name='acsc', + arguments=('z',), + expression='asin(1./z)') diff --git a/pyamplicol/src/pyamplicol/assets/models/ufo/scalar_gravity/lorentz.py b/pyamplicol/src/pyamplicol/assets/models/ufo/scalar_gravity/lorentz.py new file mode 100644 index 0000000000..768a0b9363 --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/models/ufo/scalar_gravity/lorentz.py @@ -0,0 +1,56 @@ +# type: ignore +from __future__ import absolute_import +from .object_library import all_lorentz, Lorentz + +from .function_library import complexconjugate, re, im, csc, sec, acsc, asec +from . import parameters as Param + +try: + import form_factors as ForFac +except ImportError: + pass + +SSTmpart = Lorentz( + name='SSTmpart', + spins=[1, 1, 5], + structure='-Metric(1003,2003)') + +SST = Lorentz( + name='SST', + spins=[1, 1, 5], + structure='-P(-1,1)*P(-1,2)*Metric(1003,2003) + P(1003,1)*P(2003,2) + P(2003,1)*P(1003,2)') + +SSTTmpart = Lorentz( + name='SSTTmpart', + spins=[1, 1, 5, 5], + structure='2*Metric(1003,1004)*Metric(2003,2004) + 2*Metric(2003,1004)*Metric(2004,1003) - 2*Metric(2003,1003)*Metric(2004,1004)') + +SSTT = Lorentz( + name='SSTT', + spins=[1, 1, 5, 5], + structure='(-(Metric(2003,1003)*Metric(2004,1004)*P(-1,1)*P(-1,2)) - Metric(2003,2004)*P(1003,2)*P(1004,1) - Metric(2003,2004)*P(1003,1)*P(1004,2) + Metric(2004,1004)*P(1003,2)*P(2003,1) - Metric(2004,1003)*P(1004,2)*P(2003,1) + Metric(2004,1004)*P(1003,1)*P(2003,2) - Metric(2004,1003)*P(1004,1)*P(2003,2) + Metric(2003,1003)*P(1004,2)*P(2004,1) + Metric(2003,1003)*P(1004,1)*P(2004,2) + Metric(2003,1004)*(Metric(2004,1003)*P(-1,1)*P(-1,2) - P(1003,2)*P(2004,1) - P(1003,1)*P(2004,2)) + Metric(1003,1004)*(Metric(2003,2004)*P(-1,1)*P(-1,2) - P(2003,2)*P(2004,1) - P(2003,1)*P(2004,2)))') + +SSTTTmpart = Lorentz( + name='SSTTTmpart', + spins=[1, 1, 5, 5, 5], + structure='Metric(1004,1005)*Metric(2003,2005)*Metric(2004,1003) - Metric(1003,1005)*Metric(2003,2005)*Metric(2004,1004) + Metric(1003,1004)*Metric(2003,2005)*Metric(2004,1005) - Metric(1004,1005)*Metric(2003,1003)*Metric(2004,2005) + Metric(1003,1005)*Metric(2003,1004)*Metric(2004,2005) + Metric(1003,1004)*Metric(2003,1005)*Metric(2004,2005) + Metric(1004,1005)*Metric(2003,2004)*Metric(2005,1003) - Metric(2003,1005)*Metric(2004,1004)*Metric(2005,1003) + Metric(2003,1004)*Metric(2004,1005)*Metric(2005,1003) + Metric(1003,1005)*Metric(2003,2004)*Metric(2005,1004) + Metric(2003,1005)*Metric(2004,1003)*Metric(2005,1004) - Metric(2003,1003)*Metric(2004,1005)*Metric(2005,1004) - Metric(1003,1004)*Metric(2003,2004)*Metric(2005,1005) - Metric(2003,1004)*Metric(2004,1003)*Metric(2005,1005)') + +SSTTT = Lorentz( + name='SSTTT', + spins=[1, 1, 5, 5, 5], + structure=('-(Metric(2003,1005)*Metric(2004,1004)*Metric(2005,1003)*P(-1,1)*P(-1,2)) - Metric(2003,1003)*Metric(2004,1005)*Metric(2005,1004)*P(-1,1)*P(-1,2) - Metric(1003,1004)*Metric(2003,2004)*Metric(2005,1005)*P(-1,1)*P(-1,2) - Metric(2003,1004)*Metric(2004,1003)*Metric(2005,1005)*P(-1,1)*P(-1,2) + Metric(2003,1003)*Metric(2004,1004)*Metric(2005,1005)*P(-1,1)*P(-1,2) + Metric(2003,2005)*Metric(2004,1005)*P(1003,2)*P(1004,1) + Metric(2003,1005)*Metric(2004,2005)*P(1003,2)*P(1004,1) + Metric(2003,2005)*Metric(2004,1005)*P(1003,1)*P(1004,2) + Metric(2003,1005)*Metric(2004,2005)*P(1003,1)*P(1004,2) + Metric(2003,1004)*Metric(2004,2005)*P(1003,2)*P(1005,1) + Metric(2003,2004)*Metric(2005,1004)*P(1003,2)*P(1005,1) + Metric(2003,2005)*Metric(2004,1003)*P(1004,2)*P(1005,1) + Metric(2003,2004)*Metric(2005,1003)*P(1004,2)*P(1005,1) + Metric(2003,1004)*Metric(2004,2005)*P(1003,1)*P(1005,2) + Metric(2003,2004)*Metric(2005,1004)*P(1003,1)*P(1005,2) + Metric(2003,2005)*Metric(2004,1003)*P(1004,1)*P(1005,2) + Metric(2003,2004)*Metric(2005,1003)*P(1004,1)*P(1005,2) - Metric(2004,1005)*Metric(2005,1004)*P(1003,2)*P(2003,1) + Metric(2004,1005)*Metric(2005,1003)*P(1004,2)*P(2003,1) + Metric(1003,1004)*Metric(2004,2005)*P(1005,2)*P(2003,1) + Metric(2004,1003)*Metric(2005,1004)*P(1005,2)*P(2003,1) - Metric(2004,1005)*Metric(2005,1004)*P(1003,1)*P(2003,2) + Metric(2004,1005)*Metric(2005,1003)*P(1004,1)*P(2003,2) + Metric(1003,1004)*Metric(2004,2005)*P(1005,1)*P(2003,2) + Metric(2004,1003)*Metric(2005,1004)*P(1005,1)*P(2003,2) + Metric(2003,1005)*Metric(2005,1004)*P(1003,2)*P(2004,1) - Metric(2003,1005)*Metric(2005,1003)*P(1004,2)*P(2004,1) + Metric(1003,1004)*Metric(2003,2005)*P(1005,2)*P(2004,1) + Metric(2003,1004)*Metric(2005,1003)*P(1005,2)*P(2004,1) + Metric(2003,1005)*Metric(2005,1004)*P(1003,1)*P(2004,2) - Metric(2003,1005)*Metric(2005,1003)*P(1004,1)*P(2004,2) + Metric(1003,1004)*Metric(2003,2005)*P(1005,1)*P(2004,2) + Metric(2003,1004)*Metric(2005,1003)*P(1005,1)*P(2004,2) + Metric(2003,1004)*Metric(2004,1005)*P(1003,2)*P(2005,1) + Metric(2003,1005)*Metric(2004,1003)*P(1004,2)*P(2005,1) - Metric(1003,1004)*Metric(2003,2004)*P(1005,2)*P(2005,1) - Metric(2003,1004)*Metric(2004,1003)*P(1005,2)*P(2005,1) + Metric(1003,1004)*Metric(2004,1005)*P(2003,2)*P(2005,1) + Metric(1003,1004)*Metric(2003,1005)*P(2004,2)*P(2005,1) + Metric(2003,1004)*Metric(2004,1005)*P(1003,1)*P(2005,2) + Metric(2003,1005)*Metric(2004,1003)*P(1004,1)*P(2005,2) - Metric(1003,1004)*Metric(2003,2004)*P(1005,1)*P(2005,2) - Metric(2003,1004)*Metric(2004,1003)*P(1005,1)*P(2005,2) + Metric(1003,1004)*Metric(2004,1005)*P(2003,1)*P(2005,2) + Metric(1003,1004)*Metric(2003,1005)*P(2004,1)*P(2005,2) + Metric(1004,1005)*(-(Metric(2003,1003)*Metric(2004,2005)*P(-1,1)*P(-1,2)) - Metric(2004,2005)*(P(1003,2)*P(2003,1) + P(1003,1)*P(2003,2)) + Metric(2003,2005)*P(1003,2)*P(2004,1) + Metric(2005,1003)*P(2003,2)*P(2004,1) + Metric(2003,2005)*P(1003,1)*P(2004,2) + Metric(2005,1003)*P(2003,1)*P(2004,2) + Metric(2003,2004)*P(1003,2)*P(2005,1) + Metric(2004,1003)*P(2003,2)*P(2005,1) + Metric(2003,2004)*P(1003,1)*P(2005,2) + Metric(2004,1003)*P(2003,1)*P(2005,2)) + Metric(1003,1005)*(Metric(2004,2005)*(P(1004,2)*P(2003,1) + P(1004,1)*P(2003,2)) + Metric(2005,1004)*P(2003,2)*P(2004,1) + Metric(2005,1004)*P(2003,1)*P(2004,2) - Metric(2003,2005)*(Metric(2004,1004)*P(-1,1)*P(-1,2) + P(1004,2)*P(2004,1) + P(1004,1)*P(2004,2)) + Metric(2003,2004)*P(1004,2)*P(2005,1) + Metric(2003,1004)*P(2004,2)*P(2005,1) + Metric(2003,2004)*P(1004,1)*P(2005,2) + Metric(2003,1004)*P(2004,1)*P(2005,2))' + )) + +TTT = Lorentz( + name='TTT', + spins=[5, 5, 5], + structure=( + '-(Metric(2001,1003)*Metric(2002,2003)*P(1001,3)*P(1002,1)) - Metric(2001,1003)*Metric(2002,2003)*P(1001,2)*P(1002,3) - Metric(2001,1002)*Metric(2002,2003)*P(1001,2)*P(1003,1) - Metric(2001,2002)*Metric(2003,1002)*P(1001,2)*P(1003,1) - Metric(2001,2002)*Metric(2003,1001)*P(1002,3)*P(1003,1) - Metric(2001,1002)*Metric(2002,2003)*P(1001,3)*P(1003,2) - Metric(2001,2002)*Metric(2003,1002)*P(1001,3)*P(1003,2) - Metric(2001,2002)*Metric(2003,1001)*P(1002,1)*P(1003,2) + 2*Metric(1002,1003)*Metric(2002,2003)*P(1001,3)*P(2001,2) - Metric(1001,1003)*Metric(2002,2003)*P(1002,3)*P(2001,2) - Metric(1001,1002)*Metric(2002,2003)*P(1003,1)*P(2001,2) + 2*Metric(1002,1003)*Metric(2002,2003)*P(1001,2)*P(2001,3) - Metric(1001,1003)*Metric(2002,2003)*P(1002,1)*P(2001,3) - Metric(1001,1002)*Metric(2002,2003)*P(1003,2)*P(2001,3) - Metric(1002,1003)*Metric(2001,2003)*P(1001,3)*P(2002,1) - Metric(2001,1003)*Metric(2003,1002)*P(1001,3)*P(2002,1) + 2*Metric(1001,1003)*Metric(2001,2003)*P(1002,3)*P(2002,1) + 2*Metric(2001,1003)*Metric(2003,1001)*P(1002,3)*P(2002,1) - Metric(1001,1002)*Metric(2001,2003)*P(1003,2)*P(2002,1) - Metric(2001,1002)*Metric(2003,1001)*P(1003,2)*P(2002,1) - Metric(1002,1003)*Metric(2003,1001)*P(2001,3)*P(2002,1) - Metric(1001,1003)*Metric(2003,1002)*P(2001,3)*P(2002,1) - Metric(1002,1003)*Metric(2001,2003)*P(1001,2)*P(2002,3) - Metric(2001,1003)*Metric(2003,1002)*P(1001,2)*P(2002,3) + 2*Metric(1001,1003)*Metric(2001,2003)*P(1002,1)*P(2002,3) + 2*Metric(2001,1003)*Metric(2003,1001)*P(1002,1)*P(2002,3) - Metric(1001,1002)*Metric(2001,2003)*P(1003,1)*P(2002,3) - Metric(2001,1002)*Metric(2003,1001)*P(1003,1)*P(2002,3) - Metric(1002,1003)*Metric(2003,1001)*P(2001,2)*P(2002,3) - Metric(1001,1003)*Metric(2003,1002)*P(2001,2)*P(2002,3) - Metric(1002,1003)*Metric(2001,2002)*P(1001,2)*P(2003,1) - Metric(1001,1003)*Metric(2001,2002)*P(1002,3)*P(2003,1) + 2*Metric(1001,1002)*Metric(2001,2002)*P(1003,2)*P(2003,1) - Metric(1001,1003)*Metric(2001,1002)*P(2002,3)*P(2003,1) - Metric(1001,1002)*Metric(2001,1003)*P(2002,3)*P(2003,1) - Metric(1002,1003)*Metric(2001,2002)*P(1001,3)*P(2003,2) - Metric(1001,1003)*Metric(2001,2002)*P(1002,1)*P(2003,2) + 2*Metric(1001,1002)*Metric(2001,2002)*P(1003,1)*P(2003,2) - Metric(1001,1003)*Metric(2001,1002)*P(2002,1)*P(2003,2) - Metric(1001,1002)*Metric(2001,1003)*P(2002,1)*P(2003,2) - Metric(2002,1003)*(Metric(2001,2003)*P(1001,3)*P(1002,1) + Metric(2001,2003)*P(1001,2)*P(1002,3) - 2*Metric(2003,1002)*P(1001,3)*P(2001,2) + Metric(2003,1001)*P(1002,3)*P(2001,2) - 2*Metric(2003,1002)*P(1001,2)*P(2001,3) + Metric(2003,1001)*P(1002,1)*P(2001,3) + Metric(2001,1002)*(P(1001,2)*P(2003,1) + P(1001,3)*P(2003,2)) + Metric(1001,1002)*(P(2001,2)*P(2003,1) + P(2001,3)*P(2003,2))) - Metric(2002,1001)*(Metric(2001,2003)*P(1002,3)*P(1003,1) + Metric(2001,2003)*P(1002,1)*P(1003,2) + Metric(2003,1002)*(P(1003,1)*P(2001,2) + P(1003,2)*P(2001,3)) + Metric(2001,1003)*P(1002,3)*P(2003,1) - 2*Metric(2001,1002)*P(1003,2)*P(2003,1) + Metric(2001,1003)*P(1002,1)*P(2003,2) - 2*Metric(2001,1002)*P(1003,1)*P(2003,2) + Metric(1002,1003)*(P(2001,2)*P(2003,1) + P(2001,3)*P(2003,2)))' + )) + +TTTT = Lorentz( + name='TTTT', + spins=[5, 5, 5, 5], + structure=('-(Metric(1001,1002)*Metric(1003,1004)*Metric(2001,2004)*Metric(2002,2003)*P(-1,1)*P(-1,2)) - Metric(1001,1002)*Metric(1003,1004)*Metric(2001,2003)*Metric(2002,2004)*P(-1,1)*P(-1,2) - Metric(1003,1004)*Metric(2001,1002)*Metric(2002,2004)*Metric(2003,1001)*P(-1,1)*P(-1,2) - Metric(1003,1004)*Metric(2001,2004)*Metric(2002,1001)*Metric(2003,1002)*P(-1,1)*P(-1,2) - Metric(1002,1003)*Metric(2001,2004)*Metric(2002,1001)*Metric(2003,1004)*P(-1,1)*P(-1,2) - Metric(1001,1002)*Metric(2001,2004)*Metric(2002,1003)*Metric(2003,1004)*P(-1,1)*P(-1,2) - Metric(1001,1003)*Metric(2001,1002)*Metric(2002,2004)*Metric(2003,1004)*P(-1,1)*P(-1,2) - Metric(1001,1002)*Metric(2001,1003)*Metric(2002,2004)*Metric(2003,1004)*P(-1,1)*P(-1,2) - Metric(1001,1004)*Metric(1002,1003)*Metric(2001,2002)*Metric(2003,2004)*P(-1,1)*P(-1,2) - Metric(1001,1003)*Metric(1002,1004)*Metric(2001,2002)*Metric(2003,2004)*P(-1,1)*P(-1,2) - Metric(1002,1004)*Metric(2001,1003)*Metric(2002,1001)*Metric(2003,2004)*P(-1,1)*P(-1,2) - Metric(1002,1003)*Metric(2001,1004)*Metric(2002,1001)*Metric(2003,2004)*P(-1,1)*P(-1,2) + 6*Metric(1003,1004)*Metric(2001,1001)*Metric(2002,1002)*Metric(2003,2004)*P(-1,1)*P(-1,2) - Metric(1001,1004)*Metric(2001,1002)*Metric(2002,1003)*Metric(2003,2004)*P(-1,1)*P(-1,2) - Metric(1001,1002)*Metric(2001,1004)*Metric(2002,1003)*Metric(2003,2004)*P(-1,1)*P(-1,2) - Metric(1001,1003)*Metric(2001,1002)*Metric(2002,1004)*Metric(2003,2004)*P(-1,1)*P(-1,2) - Metric(1001,1002)*Metric(2001,1003)*Metric(2002,1004)*Metric(2003,2004)*P(-1,1)*P(-1,2) - Metric(1003,1004)*Metric(2001,1002)*Metric(2002,2003)*Metric(2004,1001)*P(-1,1)*P(-1,2) - Metric(1003,1004)*Metric(2001,2002)*Metric(2003,1002)*Metric(2004,1001)*P(-1,1)*P(-1,2) - Metric(1002,1003)*Metric(2001,2002)*Metric(2003,1004)*Metric(2004,1001)*P(-1,1)*P(-1,2) - Metric(2001,1002)*Metric(2002,1003)*Metric(2003,1004)*Metric(2004,1001)*P(-1,1)*P(-1,2) - Metric(1003,1004)*Metric(2001,2003)*Metric(2002,1001)*Metric(2004,1002)*P(-1,1)*P(-1,2) - Metric(1003,1004)*Metric(2001,2002)*Metric(2003,1001)*Metric(2004,1002)*P(-1,1)*P(-1,2) - Metric(1001,1003)*Metric(2001,2002)*Metric(2003,1004)*Metric(2004,1002)*P(-1,1)*P(-1,2) - Metric(2001,1003)*Metric(2002,1001)*Metric(2003,1004)*Metric(2004,1002)*P(-1,1)*P(-1,2) - Metric(1002,1004)*Metric(2001,2003)*Metric(2002,1001)*Metric(2004,1003)*P(-1,1)*P(-1,2) - Metric(1001,1002)*Metric(2001,2003)*Metric(2002,1004)*Metric(2004,1003)*P(-1,1)*P(-1,2) - Metric(1001,1004)*Metric(2001,1002)*Metric(2002,2003)*Metric(2004,1003)*P(-1,1)*P(-1,2) - Metric(1001,1002)*Metric(2001,1004)*Metric(2002,2003)*Metric(2004,1003)*P(-1,1)*P(-1,2) - Metric(1002,1004)*Metric(2001,2002)*Metric(2003,1001)*Metric(2004,1003)*P(-1,1)*P(-1,2) - Metric(2001,1002)*Metric(2002,1004)*Metric(2003,1001)*Metric(2004,1003)*P(-1,1)*P(-1,2) - Metric(1001,1004)*Metric(2001,2002)*Metric(2003,1002)*Metric(2004,1003)*P(-1,1)*P(-1,2) - Metric(2001,1004)*Metric(2002,1001)*Metric(2003,1002)*Metric(2004,1003)*P(-1,1)*P(-1,2) + 6*Metric(2001,1001)*Metric(2002,1002)*Metric(2003,1004)*Metric(2004,1003)*P(-1,1)*P(-1,2) - Metric(1001,1003)*Metric(1002,1004)*Metric(2001,2004)*Metric(2002,2003)*P(-1,1)*P(-1,3) - Metric(1001,1004)*Metric(1002,1003)*Metric(2001,2003)*Metric(2002,2004)*P(-1,1)*P(-1,3) - Metric(1001,1002)*Metric(1003,1004)*Metric(2001,2003)*Metric(2002,2004)*P(-1,1)*P(-1,3) - Metric(1002,1004)*Metric(2001,2004)*Metric(2002,1003)*Metric(2003,1001)*P(-1,1)*P(-1,3) - Metric(1002,1003)*Metric(2001,2004)*Metric(2002,1004)*Metric(2003,1001)*P(-1,1)*P(-1,3) - Metric(1003,1004)*Metric(2001,1002)*Metric(2002,2004)*Metric(2003,1001)*P(-1,1)*P(-1,3) - Metric(1002,1003)*Metric(2001,1004)*Metric(2002,2004)*Metric(2003,1001)*P(-1,1)*P(-1,3) - Metric(1001,1003)*Metric(2001,2004)*Metric(2002,1004)*Metric(2003,1002)*P(-1,1)*P(-1,3) - Metric(1001,1004)*Metric(2001,1003)*Metric(2002,2004)*Metric(2003,1002)*P(-1,1)*P(-1,3) - Metric(1001,1003)*Metric(2001,1004)*Metric(2002,2004)*Metric(2003,1002)*P(-1,1)*P(-1,3) + 6*Metric(1002,1004)*Metric(2001,1001)*Metric(2002,2004)*Metric(2003,1003)*P(-1,1)*P(-1,3) - Metric(1001,1003)*Metric(2001,1002)*Metric(2002,2004)*Metric(2003,1004)*P(-1,1)*P(-1,3) - Metric(1001,1002)*Metric(2001,1003)*Metric(2002,2004)*Metric(2003,1004)*P(-1,1)*P(-1,3) - Metric(1001,1003)*Metric(1002,1004)*Metric(2001,2002)*Metric(2003,2004)*P(-1,1)*P(-1,3) - Metric(1002,1004)*Metric(2001,1003)*Metric(2002,1001)*Metric(2003,2004)*P(-1,1)*P(-1,3) - Metric(1001,1003)*Metric(2001,1002)*Metric(2002,1004)*Metric(2003,2004)*P(-1,1)*P(-1,3) - Metric(1001,1002)*Metric(2001,1003)*Metric(2002,1004)*Metric(2003,2004)*P(-1,1)*P(-1,3) - Metric(1002,1004)*Metric(2001,2003)*Metric(2002,1003)*Metric(2004,1001)*P(-1,1)*P(-1,3) - Metric(1002,1003)*Metric(2001,2003)*Metric(2002,1004)*Metric(2004,1001)*P(-1,1)*P(-1,3) - Metric(1002,1004)*Metric(2001,1003)*Metric(2002,2003)*Metric(2004,1001)*P(-1,1)*P(-1,3) - Metric(2001,1003)*Metric(2002,1004)*Metric(2003,1002)*Metric(2004,1001)*P(-1,1)*P(-1,3) - Metric(1003,1004)*Metric(2001,2003)*Metric(2002,1001)*Metric(2004,1002)*P(-1,1)*P(-1,3) - Metric(1001,1004)*Metric(2001,2003)*Metric(2002,1003)*Metric(2004,1002)*P(-1,1)*P(-1,3) - Metric(1001,1004)*Metric(2001,1003)*Metric(2002,2003)*Metric(2004,1002)*P(-1,1)*P(-1,3) - Metric(1001,1003)*Metric(2001,1004)*Metric(2002,2003)*Metric(2004,1002)*P(-1,1)*P(-1,3) - Metric(1003,1004)*Metric(2001,2002)*Metric(2003,1001)*Metric(2004,1002)*P(-1,1)*P(-1,3) - Metric(2001,1004)*Metric(2002,1003)*Metric(2003,1001)*Metric(2004,1002)*P(-1,1)*P(-1,3) + 6*Metric(2001,1001)*Metric(2002,1004)*Metric(2003,1003)*Metric(2004,1002)*P(-1,1)*P(-1,3) - Metric(1001,1003)*Metric(2001,2002)*Metric(2003,1004)*Metric(2004,1002)*P(-1,1)*P(-1,3) - Metric(2001,1003)*Metric(2002,1001)*Metric(2003,1004)*Metric(2004,1002)*P(-1,1)*P(-1,3) - Metric(1002,1004)*Metric(2001,2003)*Metric(2002,1001)*Metric(2004,1003)*P(-1,1)*P(-1,3) - Metric(1001,1002)*Metric(2001,2003)*Metric(2002,1004)*Metric(2004,1003)*P(-1,1)*P(-1,3) - Metric(1002,1004)*Metric(2001,2002)*Metric(2003,1001)*Metric(2004,1003)*P(-1,1)*P(-1,3) - Metric(2001,1002)*Metric(2002,1004)*Metric(2003,1001)*Metric(2004,1003)*P(-1,1)*P(-1,3) - Metric(1001,1003)*Metric(1002,1004)*Metric(2001,2004)*Metric(2002,2003)*P(-1,2)*P(-1,3) - Metric(1001,1002)*Metric(1003,1004)*Metric(2001,2004)*Metric(2002,2003)*P(-1,2)*P(-1,3) - Metric(1001,1004)*Metric(1002,1003)*Metric(2001,2003)*Metric(2002,2004)*P(-1,2)*P(-1,3) - Metric(1002,1004)*Metric(2001,2004)*Metric(2002,1003)*Metric(2003,1001)*P(-1,2)*P(-1,3) - Metric(1002,1003)*Metric(2001,2004)*Metric(2002,1004)*Metric(2003,1001)*P(-1,2)*P(-1,3) - Metric(1002,1003)*Metric(2001,1004)*Metric(2002,2004)*Metric(2003,1001)*P(-1,2)*P(-1,3) - Metric(1003,1004)*Metric(2001,2004)*Metric(2002,1001)*Metric(2003,1002)*P(-1,2)*P(-1,3) - Metric(1001,1003)*Metric(2001,2004)*Metric(2002,1004)*Metric(2003,1002)*P(-1,2)*P(-1,3) - Metric(1001,1004)*Metric(2001,1003)*Metric(2002,2004)*Metric(2003,1002)*P(-1,2)*P(-1,3) - Metric(1001,1003)*Metric(2001,1004)*Metric(2002,2004)*Metric(2003,1002)*P(-1,2)*P(-1,3) + 6*Metric(1001,1004)*Metric(2001,2004)*Metric(2002,1002)*Metric(2003,1003)*P(-1,2)*P(-1,3) - Metric(1002,1003)*Metric(2001,2004)*Metric(2002,1001)*Metric(2003,1004)*P(-1,2)*P(-1,3) - Metric(1001,1002)*Metric(2001,2004)*Metric(2002,1003)*Metric(2003,1004)*P(-1,2)*P(-1,3) - Metric(1001,1004)*Metric(1002,1003)*Metric(2001,2002)*Metric(2003,2004)*P(-1,2)*P(-1,3) - Metric(1002,1003)*Metric(2001,1004)*Metric(2002,1001)*Metric(2003,2004)*P(-1,2)*P(-1,3) - Metric(1001,1004)*Metric(2001,1002)*Metric(2002,1003)*Metric(2003,2004)*P(-1,2)*P(-1,3) - Metric(1001,1002)*Metric(2001,1004)*Metric(2002,1003)*Metric(2003,2004)*P(-1,2)*P(-1,3) - Metric(1002,1004)*Metric(2001,2003)*Metric(2002,1003)*Metric(2004,1001)*P(-1,2)*P(-1,3) - Metric(1002,1003)*Metric(2001,2003)*Metric(2002,1004)*Metric(2004,1001)*P(-1,2)*P(-1,3) - Metric(1003,1004)*Metric(2001,1002)*Metric(2002,2003)*Metric(2004,1001)*P(-1,2)*P(-1,3) - Metric(1002,1004)*Metric(2001,1003)*Metric(2002,2003)*Metric(2004,1001)*P(-1,2)*P(-1,3) - Metric(1003,1004)*Metric(2001,2002)*Metric(2003,1002)*Metric(2004,1001)*P(-1,2)*P(-1,3) - Metric(2001,1003)*Metric(2002,1004)*Metric(2003,1002)*Metric(2004,1001)*P(-1,2)*P(-1,3) + 6*Metric(2001,1004)*Metric(2002,1002)*Metric(2003,1003)*Metric(2004,1001)*P(-1,2)*P(-1,3) - Metric(1002,1003)*Metric(2001,2002)*Metric(2003,1004)*Metric(2004,1001)*P(-1,2)*P(-1,3) - Metric(2001,1002)*Metric(2002,1003)*Metric(2003,1004)*Metric(2004,1001)*P(-1,2)*P(-1,3) - Metric(1001,1004)*Metric(2001,2003)*Metric(2002,1003)*Metric(2004,1002)*P(-1,2)*P(-1,3) - Metric(1001,1004)*Metric(2001,1003)*Metric(2002,2003)*Metric(2004,1002)*P(-1,2)*P(-1,3) - Metric(1001,1003)*Metric(2001,1004)*Metric(2002,2003)*Metric(2004,1002)*P(-1,2)*P(-1,3) - Metric(2001,1004)*Metric(2002,1003)*Metric(2003,1001)*Metric(2004,1002)*P(-1,2)*P(-1,3) - Metric(1001,1004)*Metric(2001,1002)*Metric(2002,2003)*Metric(2004,1003)*P(-1,2)*P(-1,3) - Metric(1001,1002)*Metric(2001,1004)*Metric(2002,2003)*Metric(2004,1003)*P(-1,2)*P(-1,3) - Metric(1001,1004)*Metric(2001,2002)*Metric(2003,1002)*Metric(2004,1003)*P(-1,2)*P(-1,3) - Metric(2001,1004)*Metric(2002,1001)*Metric(2003,1002)*Metric(2004,1003)*P(-1,2)*P(-1,3) - Metric(1001,1003)*Metric(1002,1004)*Metric(2001,2004)*Metric(2002,2003)*P(-1,1)*P(-1,4) - Metric(1001,1002)*Metric(1003,1004)*Metric(2001,2004)*Metric(2002,2003)*P(-1,1)*P(-1,4) - Metric(1001,1004)*Metric(1002,1003)*Metric(2001,2003)*Metric(2002,2004)*P(-1,1)*P(-1,4) - Metric(1002,1004)*Metric(2001,2004)*Metric(2002,1003)*Metric(2003,1001)*P(-1,1)*P(-1,4) - Metric(1002,1003)*Metric(2001,2004)*Metric(2002,1004)*Metric(2003,1001)*P(-1,1)*P(-1,4) - Metric(1002,1003)*Metric(2001,1004)*Metric(2002,2004)*Metric(2003,1001)*P(-1,1)*P(-1,4) - Metric(1003,1004)*Metric(2001,2004)*Metric(2002,1001)*Metric(2003,1002)*P(-1,1)*P(-1,4) - Metric(1001,1003)*Metric(2001,2004)*Metric(2002,1004)*Metric(2003,1002)*P(-1,1)*P(-1,4) - Metric(1001,1004)*Metric(2001,1003)*Metric(2002,2004)*Metric(2003,1002)*P(-1,1)*P(-1,4) - Metric(1001,1003)*Metric(2001,1004)*Metric(2002,2004)*Metric(2003,1002)*P(-1,1)*P(-1,4) - Metric(1002,1003)*Metric(2001,2004)*Metric(2002,1001)*Metric(2003,1004)*P(-1,1)*P(-1,4) - Metric(1001,1002)*Metric(2001,2004)*Metric(2002,1003)*Metric(2003,1004)*P(-1,1)*P(-1,4) - Metric(1001,1004)*Metric(1002,1003)*Metric(2001,2002)*Metric(2003,2004)*P(-1,1)*P(-1,4) - Metric(1002,1003)*Metric(2001,1004)*Metric(2002,1001)*Metric(2003,2004)*P(-1,1)*P(-1,4) - Metric(1001,1004)*Metric(2001,1002)*Metric(2002,1003)*Metric(2003,2004)*P(-1,1)*P(-1,4) - Metric(1001,1002)*Metric(2001,1004)*Metric(2002,1003)*Metric(2003,2004)*P(-1,1)*P(-1,4) - Metric(1002,1004)*Metric(2001,2003)*Metric(2002,1003)*Metric(2004,1001)*P(-1,1)*P(-1,4) - Metric(1002,1003)*Metric(2001,2003)*Metric(2002,1004)*Metric(2004,1001)*P(-1,1)*P(-1,4) - Metric(1003,1004)*Metric(2001,1002)*Metric(2002,2003)*Metric(2004,1001)*P(-1,1)*P(-1,4) - Metric(1002,1004)*Metric(2001,1003)*Metric(2002,2003)*Metric(2004,1001)*P(-1,1)*P(-1,4) - Metric(1003,1004)*Metric(2001,2002)*Metric(2003,1002)*Metric(2004,1001)*P(-1,1)*P(-1,4) - Metric(2001,1003)*Metric(2002,1004)*Metric(2003,1002)*Metric(2004,1001)*P(-1,1)*P(-1,4) - Metric(1002,1003)*Metric(2001,2002)*Metric(2003,1004)*Metric(2004,1001)*P(-1,1)*P(-1,4) - Metric(2001,1002)*Metric(2002,1003)*Metric(2003,1004)*Metric(2004,1001)*P(-1,1)*P(-1,4) - Metric(1001,1004)*Metric(2001,2003)*Metric(2002,1003)*Metric(2004,1002)*P(-1,1)*P(-1,4) - Metric(1001,1004)*Metric(2001,1003)*Metric(2002,2003)*Metric(2004,1002)*P(-1,1)*P(-1,4) - Metric(1001,1003)*Metric(2001,1004)*Metric(2002,2003)*Metric(2004,1002)*P(-1,1)*P(-1,4) - Metric(2001,1004)*Metric(2002,1003)*Metric(2003,1001)*Metric(2004,1002)*P(-1,1)*P(-1,4) - Metric(1001,1004)*Metric(2001,1002)*Metric(2002,2003)*Metric(2004,1003)*P(-1,1)*P(-1,4) - Metric(1001,1002)*Metric(2001,1004)*Metric(2002,2003)*Metric(2004,1003)*P(-1,1)*P(-1,4) - Metric(1001,1004)*Metric(2001,2002)*Metric(2003,1002)*Metric(2004,1003)*P(-1,1)*P(-1,4) - Metric(2001,1004)*Metric(2002,1001)*Metric(2003,1002)*Metric(2004,1003)*P(-1,1)*P(-1,4) + 6*Metric(1002,1003)*Metric(2001,1001)*Metric(2002,2003)*Metric(2004,1004)*P(-1,1)*P(-1,4) + 6*Metric(2001,1001)*Metric(2002,1003)*Metric(2003,1002)*Metric(2004,1004)*P(-1,1)*P(-1,4) - Metric(1001,1003)*Metric(1002,1004)*Metric(2001,2004)*Metric(2002,2003)*P(-1,2)*P(-1,4) - Metric(1001,1004)*Metric(1002,1003)*Metric(2001,2003)*Metric(2002,2004)*P(-1,2)*P(-1,4) - Metric(1001,1002)*Metric(1003,1004)*Metric(2001,2003)*Metric(2002,2004)*P(-1,2)*P(-1,4) - Metric(1002,1004)*Metric(2001,2004)*Metric(2002,1003)*Metric(2003,1001)*P(-1,2)*P(-1,4) - Metric(1002,1003)*Metric(2001,2004)*Metric(2002,1004)*Metric(2003,1001)*P(-1,2)*P(-1,4) - Metric(1003,1004)*Metric(2001,1002)*Metric(2002,2004)*Metric(2003,1001)*P(-1,2)*P(-1,4) - Metric(1002,1003)*Metric(2001,1004)*Metric(2002,2004)*Metric(2003,1001)*P(-1,2)*P(-1,4) - Metric(1001,1003)*Metric(2001,2004)*Metric(2002,1004)*Metric(2003,1002)*P(-1,2)*P(-1,4) - Metric(1001,1004)*Metric(2001,1003)*Metric(2002,2004)*Metric(2003,1002)*P(-1,2)*P(-1,4) - Metric(1001,1003)*Metric(2001,1004)*Metric(2002,2004)*Metric(2003,1002)*P(-1,2)*P(-1,4) - Metric(1001,1003)*Metric(2001,1002)*Metric(2002,2004)*Metric(2003,1004)*P(-1,2)*P(-1,4) - Metric(1001,1002)*Metric(2001,1003)*Metric(2002,2004)*Metric(2003,1004)*P(-1,2)*P(-1,4) - Metric(1001,1003)*Metric(1002,1004)*Metric(2001,2002)*Metric(2003,2004)*P(-1,2)*P(-1,4) - Metric(1002,1004)*Metric(2001,1003)*Metric(2002,1001)*Metric(2003,2004)*P(-1,2)*P(-1,4) - Metric(1001,1003)*Metric(2001,1002)*Metric(2002,1004)*Metric(2003,2004)*P(-1,2)*P(-1,4) - Metric(1001,1002)*Metric(2001,1003)*Metric(2002,1004)*Metric(2003,2004)*P(-1,2)*P(-1,4) - Metric(1002,1004)*Metric(2001,2003)*Metric(2002,1003)*Metric(2004,1001)*P(-1,2)*P(-1,4) - Metric(1002,1003)*Metric(2001,2003)*Metric(2002,1004)*Metric(2004,1001)*P(-1,2)*P(-1,4) - Metric(1002,1004)*Metric(2001,1003)*Metric(2002,2003)*Metric(2004,1001)*P(-1,2)*P(-1,4) - Metric(2001,1003)*Metric(2002,1004)*Metric(2003,1002)*Metric(2004,1001)*P(-1,2)*P(-1,4) - Metric(1003,1004)*Metric(2001,2003)*Metric(2002,1001)*Metric(2004,1002)*P(-1,2)*P(-1,4) - Metric(1001,1004)*Metric(2001,2003)*Metric(2002,1003)*Metric(2004,1002)*P(-1,2)*P(-1,4) - Metric(1001,1004)*Metric(2001,1003)*Metric(2002,2003)*Metric(2004,1002)*P(-1,2)*P(-1,4) - Metric(1001,1003)*Metric(2001,1004)*Metric(2002,2003)*Metric(2004,1002)*P(-1,2)*P(-1,4) - Metric(1003,1004)*Metric(2001,2002)*Metric(2003,1001)*Metric(2004,1002)*P(-1,2)*P(-1,4) - Metric(2001,1004)*Metric(2002,1003)*Metric(2003,1001)*Metric(2004,1002)*P(-1,2)*P(-1,4) - Metric(1001,1003)*Metric(2001,2002)*Metric(2003,1004)*Metric(2004,1002)*P(-1,2)*P(-1,4) - Metric(2001,1003)*Metric(2002,1001)*Metric(2003,1004)*Metric(2004,1002)*P(-1,2)*P(-1,4) - Metric(1002,1004)*Metric(2001,2003)*Metric(2002,1001)*Metric(2004,1003)*P(-1,2)*P(-1,4) - Metric(1001,1002)*Metric(2001,2003)*Metric(2002,1004)*Metric(2004,1003)*P(-1,2)*P(-1,4) - Metric(1002,1004)*Metric(2001,2002)*Metric(2003,1001)*Metric(2004,1003)*P(-1,2)*P(-1,4) - Metric(2001,1002)*Metric(2002,1004)*Metric(2003,1001)*Metric(2004,1003)*P(-1,2)*P(-1,4) + 6*Metric(1001,1003)*Metric(2001,2003)*Metric(2002,1002)*Metric(2004,1004)*P(-1,2)*P(-1,4) + 6*Metric(2001,1003)*Metric(2002,1002)*Metric(2003,1001)*Metric(2004,1004)*P(-1,2)*P(-1,4) - Metric(1001,1002)*Metric(1003,1004)*Metric(2001,2004)*Metric(2002,2003)*P(-1,3)*P(-1,4) - Metric(1001,1002)*Metric(1003,1004)*Metric(2001,2003)*Metric(2002,2004)*P(-1,3)*P(-1,4) - Metric(1003,1004)*Metric(2001,1002)*Metric(2002,2004)*Metric(2003,1001)*P(-1,3)*P(-1,4) - Metric(1003,1004)*Metric(2001,2004)*Metric(2002,1001)*Metric(2003,1002)*P(-1,3)*P(-1,4) - Metric(1002,1003)*Metric(2001,2004)*Metric(2002,1001)*Metric(2003,1004)*P(-1,3)*P(-1,4) - Metric(1001,1002)*Metric(2001,2004)*Metric(2002,1003)*Metric(2003,1004)*P(-1,3)*P(-1,4) - Metric(1001,1003)*Metric(2001,1002)*Metric(2002,2004)*Metric(2003,1004)*P(-1,3)*P(-1,4) - Metric(1001,1002)*Metric(2001,1003)*Metric(2002,2004)*Metric(2003,1004)*P(-1,3)*P(-1,4) - Metric(1001,1004)*Metric(1002,1003)*Metric(2001,2002)*Metric(2003,2004)*P(-1,3)*P(-1,4) - Metric(1001,1003)*Metric(1002,1004)*Metric(2001,2002)*Metric(2003,2004)*P(-1,3)*P(-1,4) - Metric(1002,1004)*Metric(2001,1003)*Metric(2002,1001)*Metric(2003,2004)*P(-1,3)*P(-1,4) - Metric(1002,1003)*Metric(2001,1004)*Metric(2002,1001)*Metric(2003,2004)*P(-1,3)*P(-1,4) - Metric(1001,1004)*Metric(2001,1002)*Metric(2002,1003)*Metric(2003,2004)*P(-1,3)*P(-1,4) - Metric(1001,1002)*Metric(2001,1004)*Metric(2002,1003)*Metric(2003,2004)*P(-1,3)*P(-1,4) - Metric(1001,1003)*Metric(2001,1002)*Metric(2002,1004)*Metric(2003,2004)*P(-1,3)*P(-1,4) - Metric(1001,1002)*Metric(2001,1003)*Metric(2002,1004)*Metric(2003,2004)*P(-1,3)*P(-1,4) - Metric(1003,1004)*Metric(2001,1002)*Metric(2002,2003)*Metric(2004,1001)*P(-1,3)*P(-1,4) - Metric(1003,1004)*Metric(2001,2002)*Metric(2003,1002)*Metric(2004,1001)*P(-1,3)*P(-1,4) - Metric(1002,1003)*Metric(2001,2002)*Metric(2003,1004)*Metric(2004,1001)*P(-1,3)*P(-1,4) - Metric(2001,1002)*Metric(2002,1003)*Metric(2003,1004)*Metric(2004,1001)*P(-1,3)*P(-1,4) - Metric(1003,1004)*Metric(2001,2003)*Metric(2002,1001)*Metric(2004,1002)*P(-1,3)*P(-1,4) - Metric(1003,1004)*Metric(2001,2002)*Metric(2003,1001)*Metric(2004,1002)*P(-1,3)*P(-1,4) - Metric(1001,1003)*Metric(2001,2002)*Metric(2003,1004)*Metric(2004,1002)*P(-1,3)*P(-1,4) - Metric(2001,1003)*Metric(2002,1001)*Metric(2003,1004)*Metric(2004,1002)*P(-1,3)*P(-1,4) - Metric(1002,1004)*Metric(2001,2003)*Metric(2002,1001)*Metric(2004,1003)*P(-1,3)*P(-1,4) - Metric(1001,1002)*Metric(2001,2003)*Metric(2002,1004)*Metric(2004,1003)*P(-1,3)*P(-1,4) - Metric(1001,1004)*Metric(2001,1002)*Metric(2002,2003)*Metric(2004,1003)*P(-1,3)*P(-1,4) - Metric(1001,1002)*Metric(2001,1004)*Metric(2002,2003)*Metric(2004,1003)*P(-1,3)*P(-1,4) - Metric(1002,1004)*Metric(2001,2002)*Metric(2003,1001)*Metric(2004,1003)*P(-1,3)*P(-1,4) - Metric(2001,1002)*Metric(2002,1004)*Metric(2003,1001)*Metric(2004,1003)*P(-1,3)*P(-1,4) - Metric(1001,1004)*Metric(2001,2002)*Metric(2003,1002)*Metric(2004,1003)*P(-1,3)*P(-1,4) - Metric(2001,1004)*Metric(2002,1001)*Metric(2003,1002)*Metric(2004,1003)*P(-1,3)*P(-1,4) + 6*Metric(1001,1002)*Metric(2001,2002)*Metric(2003,1003)*Metric(2004,1004)*P(-1,3)*P(-1,4) + 6*Metric(2001,1002)*Metric(2002,1001)*Metric(2003,1003)*Metric(2004,1004)*P(-1,3)*P(-1,4) + Metric(1003,1004)*Metric(2001,2004)*Metric(2002,2003)*P(1001,2)*P(1002,1) + Metric(1003,1004)*Metric(2001,2003)*Metric(2002,2004)*P(1001,2)*P(1002,1) + Metric(2001,2004)*Metric(2002,1003)*Metric(2003,1004)*P(1001,2)*P(1002,1) + Metric(2001,1003)*Metric(2002,2004)*Metric(2003,1004)*P(1001,2)*P(1002,1) - 4*Metric(1003,1004)*Metric(2001,2002)*Metric(2003,2004)*P(1001,2)*P(1002,1) + Metric(2001,1004)*Metric(2002,1003)*Metric(2003,2004)*P(1001,2)*P(1002,1) + Metric(2001,1003)*Metric(2002,1004)*Metric(2003,2004)*P(1001,2)*P(1002,1) + Metric(2001,2003)*Metric(2002,1004)*Metric(2004,1003)*P(1001,2)*P(1002,1) + Metric(2001,1004)*Metric(2002,2003)*Metric(2004,1003)*P(1001,2)*P(1002,1) - 4*Metric(2001,2002)*Metric(2003,1004)*Metric(2004,1003)*P(1001,2)*P(1002,1) + Metric(1003,1004)*Metric(2001,2003)*Metric(2002,2004)*P(1001,3)*P(1002,1) + Metric(2001,1003)*Metric(2002,2004)*Metric(2003,1004)*P(1001,3)*P(1002,1) - 2*Metric(1003,1004)*Metric(2001,2002)*Metric(2003,2004)*P(1001,3)*P(1002,1) + Metric(2001,1003)*Metric(2002,1004)*Metric(2003,2004)*P(1001,3)*P(1002,1) + Metric(2001,2003)*Metric(2002,1004)*Metric(2004,1003)*P(1001,3)*P(1002,1) - 2*Metric(2001,2002)*Metric(2003,1004)*Metric(2004,1003)*P(1001,3)*P(1002,1) + Metric(2001,2003)*Metric(2002,1003)*Metric(2004,1004)*P(1001,3)*P(1002,1) + Metric(2001,1003)*Metric(2002,2003)*Metric(2004,1004)*P(1001,3)*P(1002,1) + Metric(1003,1004)*Metric(2001,2004)*Metric(2002,2003)*P(1001,4)*P(1002,1) + Metric(2001,2004)*Metric(2002,1004)*Metric(2003,1003)*P(1001,4)*P(1002,1) + Metric(2001,1004)*Metric(2002,2004)*Metric(2003,1003)*P(1001,4)*P(1002,1) + Metric(2001,2004)*Metric(2002,1003)*Metric(2003,1004)*P(1001,4)*P(1002,1) - 2*Metric(1003,1004)*Metric(2001,2002)*Metric(2003,2004)*P(1001,4)*P(1002,1) + Metric(2001,1004)*Metric(2002,1003)*Metric(2003,2004)*P(1001,4)*P(1002,1) + Metric(2001,1004)*Metric(2002,2003)*Metric(2004,1003)*P(1001,4)*P(1002,1) - 2*Metric(2001,2002)*Metric(2003,1004)*Metric(2004,1003)*P(1001,4)*P(1002,1) + Metric(1003,1004)*Metric(2001,2004)*Metric(2002,2003)*P(1001,2)*P(1002,3) + Metric(2001,2004)*Metric(2002,1003)*Metric(2003,1004)*P(1001,2)*P(1002,3) - 2*Metric(1003,1004)*Metric(2001,2002)*Metric(2003,2004)*P(1001,2)*P(1002,3) + Metric(2001,1004)*Metric(2002,1003)*Metric(2003,2004)*P(1001,2)*P(1002,3) + Metric(2001,1004)*Metric(2002,2003)*Metric(2004,1003)*P(1001,2)*P(1002,3) - 2*Metric(2001,2002)*Metric(2003,1004)*Metric(2004,1003)*P(1001,2)*P(1002,3) + Metric(2001,2003)*Metric(2002,1003)*Metric(2004,1004)*P(1001,2)*P(1002,3) + Metric(2001,1003)*Metric(2002,2003)*Metric(2004,1004)*P(1001,2)*P(1002,3) + Metric(1003,1004)*Metric(2001,2003)*Metric(2002,2004)*P(1001,4)*P(1002,3) + Metric(2001,1003)*Metric(2002,2004)*Metric(2003,1004)*P(1001,4)*P(1002,3) - 2*Metric(1003,1004)*Metric(2001,2002)*Metric(2003,2004)*P(1001,4)*P(1002,3) + Metric(2001,1003)*Metric(2002,1004)*Metric(2003,2004)*P(1001,4)*P(1002,3) + Metric(2001,2003)*Metric(2002,1004)*Metric(2004,1003)*P(1001,4)*P(1002,3) - 2*Metric(2001,2002)*Metric(2003,1004)*Metric(2004,1003)*P(1001,4)*P(1002,3) - 2*Metric(2001,2002)*Metric(2003,1003)*Metric(2004,1004)*P(1001,4)*P(1002,3) + Metric(1003,1004)*Metric(2001,2003)*Metric(2002,2004)*P(1001,2)*P(1002,4) + Metric(2001,2004)*Metric(2002,1004)*Metric(2003,1003)*P(1001,2)*P(1002,4) + Metric(2001,1004)*Metric(2002,2004)*Metric(2003,1003)*P(1001,2)*P(1002,4) + Metric(2001,1003)*Metric(2002,2004)*Metric(2003,1004)*P(1001,2)*P(1002,4) - 2*Metric(1003,1004)*Metric(2001,2002)*Metric(2003,2004)*P(1001,2)*P(1002,4) + Metric(2001,1003)*Metric(2002,1004)*Metric(2003,2004)*P(1001,2)*P(1002,4) + Metric(2001,2003)*Metric(2002,1004)*Metric(2004,1003)*P(1001,2)*P(1002,4) - 2*Metric(2001,2002)*Metric(2003,1004)*Metric(2004,1003)*P(1001,2)*P(1002,4) + Metric(1003,1004)*Metric(2001,2004)*Metric(2002,2003)*P(1001,3)*P(1002,4) + Metric(2001,2004)*Metric(2002,1003)*Metric(2003,1004)*P(1001,3)*P(1002,4) - 2*Metric(1003,1004)*Metric(2001,2002)*Metric(2003,2004)*P(1001,3)*P(1002,4) + Metric(2001,1004)*Metric(2002,1003)*Metric(2003,2004)*P(1001,3)*P(1002,4) + Metric(2001,1004)*Metric(2002,2003)*Metric(2004,1003)*P(1001,3)*P(1002,4) - 2*Metric(2001,2002)*Metric(2003,1004)*Metric(2004,1003)*P(1001,3)*P(1002,4) - 2*Metric(2001,2002)*Metric(2003,1003)*Metric(2004,1004)*P(1001,3)*P(1002,4) - 2*Metric(1002,1004)*Metric(2001,2003)*Metric(2002,2004)*P(1001,2)*P(1003,1) + Metric(2001,1002)*Metric(2002,2004)*Metric(2003,1004)*P(1001,2)*P(1003,1) + Metric(1002,1004)*Metric(2001,2002)*Metric(2003,2004)*P(1001,2)*P(1003,1) + Metric(2001,1002)*Metric(2002,1004)*Metric(2003,2004)*P(1001,2)*P(1003,1) - 2*Metric(2001,2003)*Metric(2002,1004)*Metric(2004,1002)*P(1001,2)*P(1003,1) + Metric(2001,2002)*Metric(2003,1004)*Metric(2004,1002)*P(1001,2)*P(1003,1) + Metric(2001,1002)*Metric(2002,2003)*Metric(2004,1004)*P(1001,2)*P(1003,1) + Metric(2001,2002)*Metric(2003,1002)*Metric(2004,1004)*P(1001,2)*P(1003,1) + Metric(1002,1004)*Metric(2001,2004)*Metric(2002,2003)*P(1001,3)*P(1003,1) - 4*Metric(1002,1004)*Metric(2001,2003)*Metric(2002,2004)*P(1001,3)*P(1003,1) + Metric(2001,2004)*Metric(2002,1004)*Metric(2003,1002)*P(1001,3)*P(1003,1) + Metric(2001,1004)*Metric(2002,2004)*Metric(2003,1002)*P(1001,3)*P(1003,1) + Metric(2001,1002)*Metric(2002,2004)*Metric(2003,1004)*P(1001,3)*P(1003,1) + Metric(1002,1004)*Metric(2001,2002)*Metric(2003,2004)*P(1001,3)*P(1003,1) + Metric(2001,1002)*Metric(2002,1004)*Metric(2003,2004)*P(1001,3)*P(1003,1) - 4*Metric(2001,2003)*Metric(2002,1004)*Metric(2004,1002)*P(1001,3)*P(1003,1) + Metric(2001,1004)*Metric(2002,2003)*Metric(2004,1002)*P(1001,3)*P(1003,1) + Metric(2001,2002)*Metric(2003,1004)*Metric(2004,1002)*P(1001,3)*P(1003,1) + Metric(1002,1004)*Metric(2001,2004)*Metric(2002,2003)*P(1001,4)*P(1003,1) - 2*Metric(1002,1004)*Metric(2001,2003)*Metric(2002,2004)*P(1001,4)*P(1003,1) + Metric(2001,2004)*Metric(2002,1004)*Metric(2003,1002)*P(1001,4)*P(1003,1) + Metric(2001,1004)*Metric(2002,2004)*Metric(2003,1002)*P(1001,4)*P(1003,1) + Metric(2001,2004)*Metric(2002,1002)*Metric(2003,1004)*P(1001,4)*P(1003,1) + Metric(2001,1004)*Metric(2002,1002)*Metric(2003,2004)*P(1001,4)*P(1003,1) - 2*Metric(2001,2003)*Metric(2002,1004)*Metric(2004,1002)*P(1001,4)*P(1003,1) + Metric(2001,1004)*Metric(2002,2003)*Metric(2004,1002)*P(1001,4)*P(1003,1) - 2*Metric(1001,1004)*Metric(2001,2004)*Metric(2002,2003)*P(1002,3)*P(1003,1) + Metric(1001,1004)*Metric(2001,2003)*Metric(2002,2004)*P(1002,3)*P(1003,1) + Metric(2001,2004)*Metric(2002,1004)*Metric(2003,1001)*P(1002,3)*P(1003,1) + Metric(2001,1004)*Metric(2002,2004)*Metric(2003,1001)*P(1002,3)*P(1003,1) + Metric(2001,2003)*Metric(2002,1004)*Metric(2004,1001)*P(1002,3)*P(1003,1) - 2*Metric(2001,1004)*Metric(2002,2003)*Metric(2004,1001)*P(1002,3)*P(1003,1) + Metric(2001,2003)*Metric(2002,1001)*Metric(2004,1004)*P(1002,3)*P(1003,1) + Metric(2001,2002)*Metric(2003,1001)*Metric(2004,1004)*P(1002,3)*P(1003,1) - 2*Metric(1001,1004)*Metric(2001,2004)*Metric(2002,2003)*P(1002,4)*P(1003,1) + Metric(2001,2004)*Metric(2002,1001)*Metric(2003,1004)*P(1002,4)*P(1003,1) + Metric(1001,1004)*Metric(2001,2002)*Metric(2003,2004)*P(1002,4)*P(1003,1) + Metric(2001,1004)*Metric(2002,1001)*Metric(2003,2004)*P(1002,4)*P(1003,1) - 2*Metric(2001,1004)*Metric(2002,2003)*Metric(2004,1001)*P(1002,4)*P(1003,1) + Metric(2001,2002)*Metric(2003,1004)*Metric(2004,1001)*P(1002,4)*P(1003,1) - 2*Metric(2001,1001)*Metric(2002,2003)*Metric(2004,1004)*P(1002,4)*P(1003,1) + Metric(1002,1004)*Metric(2001,2004)*Metric(2002,2003)*P(1001,3)*P(1003,2) - 2*Metric(1002,1004)*Metric(2001,2003)*Metric(2002,2004)*P(1001,3)*P(1003,2) + Metric(2001,2004)*Metric(2002,1004)*Metric(2003,1002)*P(1001,3)*P(1003,2) + Metric(2001,1004)*Metric(2002,2004)*Metric(2003,1002)*P(1001,3)*P(1003,2) - 2*Metric(2001,2003)*Metric(2002,1004)*Metric(2004,1002)*P(1001,3)*P(1003,2) + Metric(2001,1004)*Metric(2002,2003)*Metric(2004,1002)*P(1001,3)*P(1003,2) + Metric(2001,1002)*Metric(2002,2003)*Metric(2004,1004)*P(1001,3)*P(1003,2) + Metric(2001,2002)*Metric(2003,1002)*Metric(2004,1004)*P(1001,3)*P(1003,2) - 2*Metric(1002,1004)*Metric(2001,2003)*Metric(2002,2004)*P(1001,4)*P(1003,2) + Metric(2001,1002)*Metric(2002,2004)*Metric(2003,1004)*P(1001,4)*P(1003,2) + Metric(1002,1004)*Metric(2001,2002)*Metric(2003,2004)*P(1001,4)*P(1003,2) + Metric(2001,1002)*Metric(2002,1004)*Metric(2003,2004)*P(1001,4)*P(1003,2) - 2*Metric(2001,2003)*Metric(2002,1004)*Metric(2004,1002)*P(1001,4)*P(1003,2) + Metric(2001,2002)*Metric(2003,1004)*Metric(2004,1002)*P(1001,4)*P(1003,2) - 2*Metric(2001,2003)*Metric(2002,1002)*Metric(2004,1004)*P(1001,4)*P(1003,2) - 2*Metric(1001,1004)*Metric(2001,2004)*Metric(2002,2003)*P(1002,1)*P(1003,2) + Metric(2001,2004)*Metric(2002,1001)*Metric(2003,1004)*P(1002,1)*P(1003,2) + Metric(1001,1004)*Metric(2001,2002)*Metric(2003,2004)*P(1002,1)*P(1003,2) + Metric(2001,1004)*Metric(2002,1001)*Metric(2003,2004)*P(1002,1)*P(1003,2) - 2*Metric(2001,1004)*Metric(2002,2003)*Metric(2004,1001)*P(1002,1)*P(1003,2) + Metric(2001,2002)*Metric(2003,1004)*Metric(2004,1001)*P(1002,1)*P(1003,2) + Metric(2001,2003)*Metric(2002,1001)*Metric(2004,1004)*P(1002,1)*P(1003,2) + Metric(2001,2002)*Metric(2003,1001)*Metric(2004,1004)*P(1002,1)*P(1003,2) - 4*Metric(1001,1004)*Metric(2001,2004)*Metric(2002,2003)*P(1002,3)*P(1003,2) + Metric(1001,1004)*Metric(2001,2003)*Metric(2002,2004)*P(1002,3)*P(1003,2) + Metric(2001,2004)*Metric(2002,1004)*Metric(2003,1001)*P(1002,3)*P(1003,2) + Metric(2001,1004)*Metric(2002,2004)*Metric(2003,1001)*P(1002,3)*P(1003,2) + Metric(2001,2004)*Metric(2002,1001)*Metric(2003,1004)*P(1002,3)*P(1003,2) + Metric(1001,1004)*Metric(2001,2002)*Metric(2003,2004)*P(1002,3)*P(1003,2) + Metric(2001,1004)*Metric(2002,1001)*Metric(2003,2004)*P(1002,3)*P(1003,2) + Metric(2001,2003)*Metric(2002,1004)*Metric(2004,1001)*P(1002,3)*P(1003,2) - 4*Metric(2001,1004)*Metric(2002,2003)*Metric(2004,1001)*P(1002,3)*P(1003,2) + Metric(2001,2002)*Metric(2003,1004)*Metric(2004,1001)*P(1002,3)*P(1003,2) - 2*Metric(1001,1004)*Metric(2001,2004)*Metric(2002,2003)*P(1002,4)*P(1003,2) + Metric(1001,1004)*Metric(2001,2003)*Metric(2002,2004)*P(1002,4)*P(1003,2) + Metric(2001,2004)*Metric(2002,1004)*Metric(2003,1001)*P(1002,4)*P(1003,2) + Metric(2001,1004)*Metric(2002,2004)*Metric(2003,1001)*P(1002,4)*P(1003,2) + Metric(2001,1001)*Metric(2002,2004)*Metric(2003,1004)*P(1002,4)*P(1003,2) + Metric(2001,1001)*Metric(2002,1004)*Metric(2003,2004)*P(1002,4)*P(1003,2) + Metric(2001,2003)*Metric(2002,1004)*Metric(2004,1001)*P(1002,4)*P(1003,2) - 2*Metric(2001,1004)*Metric(2002,2003)*Metric(2004,1001)*P(1002,4)*P(1003,2) + Metric(1002,1004)*Metric(2001,2004)*Metric(2002,2003)*P(1001,2)*P(1003,4) - 2*Metric(1002,1004)*Metric(2001,2003)*Metric(2002,2004)*P(1001,2)*P(1003,4) + Metric(2001,2004)*Metric(2002,1004)*Metric(2003,1002)*P(1001,2)*P(1003,4) + Metric(2001,1004)*Metric(2002,2004)*Metric(2003,1002)*P(1001,2)*P(1003,4) - 2*Metric(2001,2003)*Metric(2002,1004)*Metric(2004,1002)*P(1001,2)*P(1003,4) + Metric(2001,1004)*Metric(2002,2003)*Metric(2004,1002)*P(1001,2)*P(1003,4) - 2*Metric(2001,2003)*Metric(2002,1002)*Metric(2004,1004)*P(1001,2)*P(1003,4) - 2*Metric(1002,1004)*Metric(2001,2003)*Metric(2002,2004)*P(1001,3)*P(1003,4) + Metric(2001,2004)*Metric(2002,1002)*Metric(2003,1004)*P(1001,3)*P(1003,4) + Metric(2001,1002)*Metric(2002,2004)*Metric(2003,1004)*P(1001,3)*P(1003,4) + Metric(1002,1004)*Metric(2001,2002)*Metric(2003,2004)*P(1001,3)*P(1003,4) + Metric(2001,1004)*Metric(2002,1002)*Metric(2003,2004)*P(1001,3)*P(1003,4) + Metric(2001,1002)*Metric(2002,1004)*Metric(2003,2004)*P(1001,3)*P(1003,4) - 2*Metric(2001,2003)*Metric(2002,1004)*Metric(2004,1002)*P(1001,3)*P(1003,4) + Metric(2001,2002)*Metric(2003,1004)*Metric(2004,1002)*P(1001,3)*P(1003,4) - 2*Metric(1001,1004)*Metric(2001,2004)*Metric(2002,2003)*P(1002,1)*P(1003,4) + Metric(1001,1004)*Metric(2001,2003)*Metric(2002,2004)*P(1002,1)*P(1003,4) + Metric(2001,2004)*Metric(2002,1004)*Metric(2003,1001)*P(1002,1)*P(1003,4) + Metric(2001,1004)*Metric(2002,2004)*Metric(2003,1001)*P(1002,1)*P(1003,4) + Metric(2001,2003)*Metric(2002,1004)*Metric(2004,1001)*P(1002,1)*P(1003,4) - 2*Metric(2001,1004)*Metric(2002,2003)*Metric(2004,1001)*P(1002,1)*P(1003,4) - 2*Metric(2001,1001)*Metric(2002,2003)*Metric(2004,1004)*P(1002,1)*P(1003,4) - 2*Metric(1001,1004)*Metric(2001,2004)*Metric(2002,2003)*P(1002,3)*P(1003,4) + Metric(2001,2004)*Metric(2002,1001)*Metric(2003,1004)*P(1002,3)*P(1003,4) + Metric(2001,1001)*Metric(2002,2004)*Metric(2003,1004)*P(1002,3)*P(1003,4) + Metric(1001,1004)*Metric(2001,2002)*Metric(2003,2004)*P(1002,3)*P(1003,4) + Metric(2001,1004)*Metric(2002,1001)*Metric(2003,2004)*P(1002,3)*P(1003,4) + Metric(2001,1001)*Metric(2002,1004)*Metric(2003,2004)*P(1002,3)*P(1003,4) - 2*Metric(2001,1004)*Metric(2002,2003)*Metric(2004,1001)*P(1002,3)*P(1003,4) + Metric(2001,2002)*Metric(2003,1004)*Metric(2004,1001)*P(1002,3)*P(1003,4) - 2*Metric(1002,1003)*Metric(2001,2004)*Metric(2002,2003)*P(1001,2)*P(1004,1) - 2*Metric(2001,2004)*Metric(2002,1003)*Metric(2003,1002)*P(1001,2)*P(1004,1) + Metric(2001,1002)*Metric(2002,2004)*Metric(2003,1003)*P(1001,2)*P(1004,1) + Metric(1002,1003)*Metric(2001,2002)*Metric(2003,2004)*P(1001,2)*P(1004,1) + Metric(2001,1002)*Metric(2002,1003)*Metric(2003,2004)*P(1001,2)*P(1004,1) + Metric(2001,2002)*Metric(2003,1003)*Metric(2004,1002)*P(1001,2)*P(1004,1) + Metric(2001,1002)*Metric(2002,2003)*Metric(2004,1003)*P(1001,2)*P(1004,1) + Metric(2001,2002)*Metric(2003,1002)*Metric(2004,1003)*P(1001,2)*P(1004,1) - 2*Metric(1002,1003)*Metric(2001,2004)*Metric(2002,2003)*P(1001,3)*P(1004,1) + Metric(1002,1003)*Metric(2001,2003)*Metric(2002,2004)*P(1001,3)*P(1004,1) - 2*Metric(2001,2004)*Metric(2002,1003)*Metric(2003,1002)*P(1001,3)*P(1004,1) + Metric(2001,1003)*Metric(2002,2004)*Metric(2003,1002)*P(1001,3)*P(1004,1) + Metric(2001,1003)*Metric(2002,1002)*Metric(2003,2004)*P(1001,3)*P(1004,1) + Metric(2001,2003)*Metric(2002,1003)*Metric(2004,1002)*P(1001,3)*P(1004,1) + Metric(2001,1003)*Metric(2002,2003)*Metric(2004,1002)*P(1001,3)*P(1004,1) + Metric(2001,2003)*Metric(2002,1002)*Metric(2004,1003)*P(1001,3)*P(1004,1) - 4*Metric(1002,1003)*Metric(2001,2004)*Metric(2002,2003)*P(1001,4)*P(1004,1) + Metric(1002,1003)*Metric(2001,2003)*Metric(2002,2004)*P(1001,4)*P(1004,1) - 4*Metric(2001,2004)*Metric(2002,1003)*Metric(2003,1002)*P(1001,4)*P(1004,1) + Metric(2001,1003)*Metric(2002,2004)*Metric(2003,1002)*P(1001,4)*P(1004,1) + Metric(1002,1003)*Metric(2001,2002)*Metric(2003,2004)*P(1001,4)*P(1004,1) + Metric(2001,1002)*Metric(2002,1003)*Metric(2003,2004)*P(1001,4)*P(1004,1) + Metric(2001,2003)*Metric(2002,1003)*Metric(2004,1002)*P(1001,4)*P(1004,1) + Metric(2001,1003)*Metric(2002,2003)*Metric(2004,1002)*P(1001,4)*P(1004,1) + Metric(2001,1002)*Metric(2002,2003)*Metric(2004,1003)*P(1001,4)*P(1004,1) + Metric(2001,2002)*Metric(2003,1002)*Metric(2004,1003)*P(1001,4)*P(1004,1) - 2*Metric(1001,1003)*Metric(2001,2003)*Metric(2002,2004)*P(1002,3)*P(1004,1) - 2*Metric(2001,1003)*Metric(2002,2004)*Metric(2003,1001)*P(1002,3)*P(1004,1) - 2*Metric(2001,1001)*Metric(2002,2004)*Metric(2003,1003)*P(1002,3)*P(1004,1) + Metric(1001,1003)*Metric(2001,2002)*Metric(2003,2004)*P(1002,3)*P(1004,1) + Metric(2001,1003)*Metric(2002,1001)*Metric(2003,2004)*P(1002,3)*P(1004,1) + Metric(2001,2003)*Metric(2002,1001)*Metric(2004,1003)*P(1002,3)*P(1004,1) + Metric(2001,2002)*Metric(2003,1001)*Metric(2004,1003)*P(1002,3)*P(1004,1) + Metric(1001,1003)*Metric(2001,2004)*Metric(2002,2003)*P(1002,4)*P(1004,1) - 2*Metric(1001,1003)*Metric(2001,2003)*Metric(2002,2004)*P(1002,4)*P(1004,1) + Metric(2001,2004)*Metric(2002,1003)*Metric(2003,1001)*P(1002,4)*P(1004,1) - 2*Metric(2001,1003)*Metric(2002,2004)*Metric(2003,1001)*P(1002,4)*P(1004,1) + Metric(2001,2004)*Metric(2002,1001)*Metric(2003,1003)*P(1002,4)*P(1004,1) + Metric(2001,2003)*Metric(2002,1003)*Metric(2004,1001)*P(1002,4)*P(1004,1) + Metric(2001,1003)*Metric(2002,2003)*Metric(2004,1001)*P(1002,4)*P(1004,1) + Metric(2001,2002)*Metric(2003,1003)*Metric(2004,1001)*P(1002,4)*P(1004,1) + Metric(1001,1002)*Metric(2001,2003)*Metric(2002,2004)*P(1003,2)*P(1004,1) + Metric(2001,1002)*Metric(2002,2004)*Metric(2003,1001)*P(1003,2)*P(1004,1) - 2*Metric(1001,1002)*Metric(2001,2002)*Metric(2003,2004)*P(1003,2)*P(1004,1) - 2*Metric(2001,1002)*Metric(2002,1001)*Metric(2003,2004)*P(1003,2)*P(1004,1) - 2*Metric(2001,1001)*Metric(2002,1002)*Metric(2003,2004)*P(1003,2)*P(1004,1) + Metric(2001,2003)*Metric(2002,1001)*Metric(2004,1002)*P(1003,2)*P(1004,1) + Metric(2001,2002)*Metric(2003,1001)*Metric(2004,1002)*P(1003,2)*P(1004,1) + Metric(1001,1002)*Metric(2001,2004)*Metric(2002,2003)*P(1003,4)*P(1004,1) + Metric(2001,2004)*Metric(2002,1002)*Metric(2003,1001)*P(1003,4)*P(1004,1) + Metric(2001,2004)*Metric(2002,1001)*Metric(2003,1002)*P(1003,4)*P(1004,1) - 2*Metric(1001,1002)*Metric(2001,2002)*Metric(2003,2004)*P(1003,4)*P(1004,1) - 2*Metric(2001,1002)*Metric(2002,1001)*Metric(2003,2004)*P(1003,4)*P(1004,1) + Metric(2001,2003)*Metric(2002,1002)*Metric(2004,1001)*P(1003,4)*P(1004,1) + Metric(2001,1002)*Metric(2002,2003)*Metric(2004,1001)*P(1003,4)*P(1004,1) + Metric(2001,2002)*Metric(2003,1002)*Metric(2004,1001)*P(1003,4)*P(1004,1) - 2*Metric(1002,1003)*Metric(2001,2004)*Metric(2002,2003)*P(1001,3)*P(1004,2) - 2*Metric(2001,2004)*Metric(2002,1003)*Metric(2003,1002)*P(1001,3)*P(1004,2) - 2*Metric(2001,2004)*Metric(2002,1002)*Metric(2003,1003)*P(1001,3)*P(1004,2) + Metric(1002,1003)*Metric(2001,2002)*Metric(2003,2004)*P(1001,3)*P(1004,2) + Metric(2001,1002)*Metric(2002,1003)*Metric(2003,2004)*P(1001,3)*P(1004,2) + Metric(2001,1002)*Metric(2002,2003)*Metric(2004,1003)*P(1001,3)*P(1004,2) + Metric(2001,2002)*Metric(2003,1002)*Metric(2004,1003)*P(1001,3)*P(1004,2) - 2*Metric(1002,1003)*Metric(2001,2004)*Metric(2002,2003)*P(1001,4)*P(1004,2) + Metric(1002,1003)*Metric(2001,2003)*Metric(2002,2004)*P(1001,4)*P(1004,2) - 2*Metric(2001,2004)*Metric(2002,1003)*Metric(2003,1002)*P(1001,4)*P(1004,2) + Metric(2001,1003)*Metric(2002,2004)*Metric(2003,1002)*P(1001,4)*P(1004,2) + Metric(2001,1002)*Metric(2002,2004)*Metric(2003,1003)*P(1001,4)*P(1004,2) + Metric(2001,2003)*Metric(2002,1003)*Metric(2004,1002)*P(1001,4)*P(1004,2) + Metric(2001,1003)*Metric(2002,2003)*Metric(2004,1002)*P(1001,4)*P(1004,2) + Metric(2001,2002)*Metric(2003,1003)*Metric(2004,1002)*P(1001,4)*P(1004,2) - 2*Metric(1001,1003)*Metric(2001,2003)*Metric(2002,2004)*P(1002,1)*P(1004,2) - 2*Metric(2001,1003)*Metric(2002,2004)*Metric(2003,1001)*P(1002,1)*P(1004,2) + Metric(2001,2004)*Metric(2002,1001)*Metric(2003,1003)*P(1002,1)*P(1004,2) + Metric(1001,1003)*Metric(2001,2002)*Metric(2003,2004)*P(1002,1)*P(1004,2) + Metric(2001,1003)*Metric(2002,1001)*Metric(2003,2004)*P(1002,1)*P(1004,2) + Metric(2001,2002)*Metric(2003,1003)*Metric(2004,1001)*P(1002,1)*P(1004,2) + Metric(2001,2003)*Metric(2002,1001)*Metric(2004,1003)*P(1002,1)*P(1004,2) + Metric(2001,2002)*Metric(2003,1001)*Metric(2004,1003)*P(1002,1)*P(1004,2) + Metric(1001,1003)*Metric(2001,2004)*Metric(2002,2003)*P(1002,3)*P(1004,2) - 2*Metric(1001,1003)*Metric(2001,2003)*Metric(2002,2004)*P(1002,3)*P(1004,2) + Metric(2001,2004)*Metric(2002,1003)*Metric(2003,1001)*P(1002,3)*P(1004,2) - 2*Metric(2001,1003)*Metric(2002,2004)*Metric(2003,1001)*P(1002,3)*P(1004,2) + Metric(2001,1001)*Metric(2002,1003)*Metric(2003,2004)*P(1002,3)*P(1004,2) + Metric(2001,2003)*Metric(2002,1003)*Metric(2004,1001)*P(1002,3)*P(1004,2) + Metric(2001,1003)*Metric(2002,2003)*Metric(2004,1001)*P(1002,3)*P(1004,2) + Metric(2001,1001)*Metric(2002,2003)*Metric(2004,1003)*P(1002,3)*P(1004,2) + Metric(1001,1003)*Metric(2001,2004)*Metric(2002,2003)*P(1002,4)*P(1004,2) - 4*Metric(1001,1003)*Metric(2001,2003)*Metric(2002,2004)*P(1002,4)*P(1004,2) + Metric(2001,2004)*Metric(2002,1003)*Metric(2003,1001)*P(1002,4)*P(1004,2) - 4*Metric(2001,1003)*Metric(2002,2004)*Metric(2003,1001)*P(1002,4)*P(1004,2) + Metric(1001,1003)*Metric(2001,2002)*Metric(2003,2004)*P(1002,4)*P(1004,2) + Metric(2001,1003)*Metric(2002,1001)*Metric(2003,2004)*P(1002,4)*P(1004,2) + Metric(2001,2003)*Metric(2002,1003)*Metric(2004,1001)*P(1002,4)*P(1004,2) + Metric(2001,1003)*Metric(2002,2003)*Metric(2004,1001)*P(1002,4)*P(1004,2) + Metric(2001,2003)*Metric(2002,1001)*Metric(2004,1003)*P(1002,4)*P(1004,2) + Metric(2001,2002)*Metric(2003,1001)*Metric(2004,1003)*P(1002,4)*P(1004,2) + Metric(1001,1002)*Metric(2001,2004)*Metric(2002,2003)*P(1003,1)*P(1004,2) + Metric(2001,2004)*Metric(2002,1001)*Metric(2003,1002)*P(1003,1)*P(1004,2) - 2*Metric(1001,1002)*Metric(2001,2002)*Metric(2003,2004)*P(1003,1)*P(1004,2) - 2*Metric(2001,1002)*Metric(2002,1001)*Metric(2003,2004)*P(1003,1)*P(1004,2) - 2*Metric(2001,1001)*Metric(2002,1002)*Metric(2003,2004)*P(1003,1)*P(1004,2) + Metric(2001,1002)*Metric(2002,2003)*Metric(2004,1001)*P(1003,1)*P(1004,2) + Metric(2001,2002)*Metric(2003,1002)*Metric(2004,1001)*P(1003,1)*P(1004,2) + Metric(1001,1002)*Metric(2001,2003)*Metric(2002,2004)*P(1003,4)*P(1004,2) + Metric(2001,1002)*Metric(2002,2004)*Metric(2003,1001)*P(1003,4)*P(1004,2) + Metric(2001,1001)*Metric(2002,2004)*Metric(2003,1002)*P(1003,4)*P(1004,2) - 2*Metric(1001,1002)*Metric(2001,2002)*Metric(2003,2004)*P(1003,4)*P(1004,2) - 2*Metric(2001,1002)*Metric(2002,1001)*Metric(2003,2004)*P(1003,4)*P(1004,2) + Metric(2001,2003)*Metric(2002,1001)*Metric(2004,1002)*P(1003,4)*P(1004,2) + Metric(2001,1001)*Metric(2002,2003)*Metric(2004,1002)*P(1003,4)*P(1004,2) + Metric(2001,2002)*Metric(2003,1001)*Metric(2004,1002)*P(1003,4)*P(1004,2) - 2*Metric(1002,1003)*Metric(2001,2004)*Metric(2002,2003)*P(1001,2)*P(1004,3) + Metric(1002,1003)*Metric(2001,2003)*Metric(2002,2004)*P(1001,2)*P(1004,3) - 2*Metric(2001,2004)*Metric(2002,1003)*Metric(2003,1002)*P(1001,2)*P(1004,3) + Metric(2001,1003)*Metric(2002,2004)*Metric(2003,1002)*P(1001,2)*P(1004,3) - 2*Metric(2001,2004)*Metric(2002,1002)*Metric(2003,1003)*P(1001,2)*P(1004,3) + Metric(2001,2003)*Metric(2002,1003)*Metric(2004,1002)*P(1001,2)*P(1004,3) + Metric(2001,1003)*Metric(2002,2003)*Metric(2004,1002)*P(1001,2)*P(1004,3) - 2*Metric(1002,1003)*Metric(2001,2004)*Metric(2002,2003)*P(1001,4)*P(1004,3) - 2*Metric(2001,2004)*Metric(2002,1003)*Metric(2003,1002)*P(1001,4)*P(1004,3) + Metric(1002,1003)*Metric(2001,2002)*Metric(2003,2004)*P(1001,4)*P(1004,3) + Metric(2001,1003)*Metric(2002,1002)*Metric(2003,2004)*P(1001,4)*P(1004,3) + Metric(2001,1002)*Metric(2002,1003)*Metric(2003,2004)*P(1001,4)*P(1004,3) + Metric(2001,2003)*Metric(2002,1002)*Metric(2004,1003)*P(1001,4)*P(1004,3) + Metric(2001,1002)*Metric(2002,2003)*Metric(2004,1003)*P(1001,4)*P(1004,3) + Metric(2001,2002)*Metric(2003,1002)*Metric(2004,1003)*P(1001,4)*P(1004,3) + Metric(1001,1003)*Metric(2001,2004)*Metric(2002,2003)*P(1002,1)*P(1004,3) - 2*Metric(1001,1003)*Metric(2001,2003)*Metric(2002,2004)*P(1002,1)*P(1004,3) + Metric(2001,2004)*Metric(2002,1003)*Metric(2003,1001)*P(1002,1)*P(1004,3) - 2*Metric(2001,1003)*Metric(2002,2004)*Metric(2003,1001)*P(1002,1)*P(1004,3) - 2*Metric(2001,1001)*Metric(2002,2004)*Metric(2003,1003)*P(1002,1)*P(1004,3) + Metric(2001,2003)*Metric(2002,1003)*Metric(2004,1001)*P(1002,1)*P(1004,3) + Metric(2001,1003)*Metric(2002,2003)*Metric(2004,1001)*P(1002,1)*P(1004,3) - 2*Metric(1001,1003)*Metric(2001,2003)*Metric(2002,2004)*P(1002,4)*P(1004,3) - 2*Metric(2001,1003)*Metric(2002,2004)*Metric(2003,1001)*P(1002,4)*P(1004,3) + Metric(1001,1003)*Metric(2001,2002)*Metric(2003,2004)*P(1002,4)*P(1004,3) + Metric(2001,1003)*Metric(2002,1001)*Metric(2003,2004)*P(1002,4)*P(1004,3) + Metric(2001,1001)*Metric(2002,1003)*Metric(2003,2004)*P(1002,4)*P(1004,3) + Metric(2001,2003)*Metric(2002,1001)*Metric(2004,1003)*P(1002,4)*P(1004,3) + Metric(2001,1001)*Metric(2002,2003)*Metric(2004,1003)*P(1002,4)*P(1004,3) + Metric(2001,2002)*Metric(2003,1001)*Metric(2004,1003)*P(1002,4)*P(1004,3) + Metric(1001,1002)*Metric(2001,2003)*Metric(2002,2004)*P(1003,1)*P(1004,3) + Metric(2001,2004)*Metric(2002,1002)*Metric(2003,1001)*P(1003,1)*P(1004,3) + Metric(2001,1002)*Metric(2002,2004)*Metric(2003,1001)*P(1003,1)*P(1004,3) - 2*Metric(1001,1002)*Metric(2001,2002)*Metric(2003,2004)*P(1003,1)*P(1004,3) - 2*Metric(2001,1002)*Metric(2002,1001)*Metric(2003,2004)*P(1003,1)*P(1004,3) + Metric(2001,2003)*Metric(2002,1002)*Metric(2004,1001)*P(1003,1)*P(1004,3) + Metric(2001,2003)*Metric(2002,1001)*Metric(2004,1002)*P(1003,1)*P(1004,3) + Metric(2001,2002)*Metric(2003,1001)*Metric(2004,1002)*P(1003,1)*P(1004,3) + Metric(1001,1002)*Metric(2001,2004)*Metric(2002,2003)*P(1003,2)*P(1004,3) + Metric(2001,2004)*Metric(2002,1001)*Metric(2003,1002)*P(1003,2)*P(1004,3) + Metric(2001,1001)*Metric(2002,2004)*Metric(2003,1002)*P(1003,2)*P(1004,3) - 2*Metric(1001,1002)*Metric(2001,2002)*Metric(2003,2004)*P(1003,2)*P(1004,3) - 2*Metric(2001,1002)*Metric(2002,1001)*Metric(2003,2004)*P(1003,2)*P(1004,3) + Metric(2001,1002)*Metric(2002,2003)*Metric(2004,1001)*P(1003,2)*P(1004,3) + Metric(2001,2002)*Metric(2003,1002)*Metric(2004,1001)*P(1003,2)*P(1004,3) + Metric(2001,1001)*Metric(2002,2003)*Metric(2004,1002)*P(1003,2)*P(1004,3) + Metric(1001,1002)*Metric(2001,2004)*Metric(2002,2003)*P(1003,4)*P(1004,3) + Metric(1001,1002)*Metric(2001,2003)*Metric(2002,2004)*P(1003,4)*P(1004,3) + Metric(2001,1002)*Metric(2002,2004)*Metric(2003,1001)*P(1003,4)*P(1004,3) + Metric(2001,2004)*Metric(2002,1001)*Metric(2003,1002)*P(1003,4)*P(1004,3) - 4*Metric(1001,1002)*Metric(2001,2002)*Metric(2003,2004)*P(1003,4)*P(1004,3) - 4*Metric(2001,1002)*Metric(2002,1001)*Metric(2003,2004)*P(1003,4)*P(1004,3) + Metric(2001,1002)*Metric(2002,2003)*Metric(2004,1001)*P(1003,4)*P(1004,3) + Metric(2001,2002)*Metric(2003,1002)*Metric(2004,1001)*P(1003,4)*P(1004,3) + Metric(2001,2003)*Metric(2002,1001)*Metric(2004,1002)*P(1003,4)*P(1004,3) + Metric(2001,2002)*Metric(2003,1001)*Metric(2004,1002)*P(1003,4)*P(1004,3) - 2*Metric(1002,1003)*Metric(2002,2003)*Metric(2004,1004)*P(1001,3)*P(2001,2) - 2*Metric(2002,1003)*Metric(2003,1002)*Metric(2004,1004)*P(1001,3)*P(2001,2) - 2*Metric(1002,1004)*Metric(2002,2004)*Metric(2003,1003)*P(1001,4)*P(2001,2) - 2*Metric(2002,1004)*Metric(2003,1003)*Metric(2004,1002)*P(1001,4)*P(2001,2) + Metric(1003,1004)*Metric(2002,2004)*Metric(2003,1001)*P(1002,1)*P(2001,2) + Metric(1001,1003)*Metric(2002,2004)*Metric(2003,1004)*P(1002,1)*P(2001,2) - 4*Metric(1003,1004)*Metric(2002,1001)*Metric(2003,2004)*P(1002,1)*P(2001,2) + Metric(1001,1004)*Metric(2002,1003)*Metric(2003,2004)*P(1002,1)*P(2001,2) + Metric(1001,1003)*Metric(2002,1004)*Metric(2003,2004)*P(1002,1)*P(2001,2) + Metric(1003,1004)*Metric(2002,2003)*Metric(2004,1001)*P(1002,1)*P(2001,2) + Metric(2002,1003)*Metric(2003,1004)*Metric(2004,1001)*P(1002,1)*P(2001,2) + Metric(1001,1004)*Metric(2002,2003)*Metric(2004,1003)*P(1002,1)*P(2001,2) + Metric(2002,1004)*Metric(2003,1001)*Metric(2004,1003)*P(1002,1)*P(2001,2) - 4*Metric(2002,1001)*Metric(2003,1004)*Metric(2004,1003)*P(1002,1)*P(2001,2) - 2*Metric(1003,1004)*Metric(2002,1001)*Metric(2003,2004)*P(1002,3)*P(2001,2) + Metric(1001,1004)*Metric(2002,1003)*Metric(2003,2004)*P(1002,3)*P(2001,2) + Metric(1003,1004)*Metric(2002,2003)*Metric(2004,1001)*P(1002,3)*P(2001,2) + Metric(2002,1003)*Metric(2003,1004)*Metric(2004,1001)*P(1002,3)*P(2001,2) + Metric(1001,1004)*Metric(2002,2003)*Metric(2004,1003)*P(1002,3)*P(2001,2) - 2*Metric(2002,1001)*Metric(2003,1004)*Metric(2004,1003)*P(1002,3)*P(2001,2) + Metric(1001,1003)*Metric(2002,2003)*Metric(2004,1004)*P(1002,3)*P(2001,2) + Metric(2002,1003)*Metric(2003,1001)*Metric(2004,1004)*P(1002,3)*P(2001,2) + Metric(1003,1004)*Metric(2002,2004)*Metric(2003,1001)*P(1002,4)*P(2001,2) + Metric(1001,1004)*Metric(2002,2004)*Metric(2003,1003)*P(1002,4)*P(2001,2) + Metric(1001,1003)*Metric(2002,2004)*Metric(2003,1004)*P(1002,4)*P(2001,2) - 2*Metric(1003,1004)*Metric(2002,1001)*Metric(2003,2004)*P(1002,4)*P(2001,2) + Metric(1001,1003)*Metric(2002,1004)*Metric(2003,2004)*P(1002,4)*P(2001,2) + Metric(2002,1004)*Metric(2003,1003)*Metric(2004,1001)*P(1002,4)*P(2001,2) + Metric(2002,1004)*Metric(2003,1001)*Metric(2004,1003)*P(1002,4)*P(2001,2) - 2*Metric(2002,1001)*Metric(2003,1004)*Metric(2004,1003)*P(1002,4)*P(2001,2) - 2*Metric(1002,1004)*Metric(2002,2004)*Metric(2003,1001)*P(1003,1)*P(2001,2) + Metric(1001,1002)*Metric(2002,2004)*Metric(2003,1004)*P(1003,1)*P(2001,2) + Metric(1002,1004)*Metric(2002,1001)*Metric(2003,2004)*P(1003,1)*P(2001,2) + Metric(1001,1002)*Metric(2002,1004)*Metric(2003,2004)*P(1003,1)*P(2001,2) - 2*Metric(2002,1004)*Metric(2003,1001)*Metric(2004,1002)*P(1003,1)*P(2001,2) + Metric(2002,1001)*Metric(2003,1004)*Metric(2004,1002)*P(1003,1)*P(2001,2) + Metric(1001,1002)*Metric(2002,2003)*Metric(2004,1004)*P(1003,1)*P(2001,2) + Metric(2002,1001)*Metric(2003,1002)*Metric(2004,1004)*P(1003,1)*P(2001,2) - 2*Metric(1002,1004)*Metric(2002,2004)*Metric(2003,1001)*P(1003,4)*P(2001,2) + Metric(1001,1004)*Metric(2002,2004)*Metric(2003,1002)*P(1003,4)*P(2001,2) + Metric(1002,1004)*Metric(2002,2003)*Metric(2004,1001)*P(1003,4)*P(2001,2) + Metric(2002,1004)*Metric(2003,1002)*Metric(2004,1001)*P(1003,4)*P(2001,2) + Metric(1001,1004)*Metric(2002,2003)*Metric(2004,1002)*P(1003,4)*P(2001,2) - 2*Metric(2002,1004)*Metric(2003,1001)*Metric(2004,1002)*P(1003,4)*P(2001,2) - 2*Metric(2002,1002)*Metric(2003,1001)*Metric(2004,1004)*P(1003,4)*P(2001,2) + Metric(1001,1002)*Metric(2002,2004)*Metric(2003,1003)*P(1004,1)*P(2001,2) + Metric(1002,1003)*Metric(2002,1001)*Metric(2003,2004)*P(1004,1)*P(2001,2) + Metric(1001,1002)*Metric(2002,1003)*Metric(2003,2004)*P(1004,1)*P(2001,2) - 2*Metric(1002,1003)*Metric(2002,2003)*Metric(2004,1001)*P(1004,1)*P(2001,2) - 2*Metric(2002,1003)*Metric(2003,1002)*Metric(2004,1001)*P(1004,1)*P(2001,2) + Metric(2002,1001)*Metric(2003,1003)*Metric(2004,1002)*P(1004,1)*P(2001,2) + Metric(1001,1002)*Metric(2002,2003)*Metric(2004,1003)*P(1004,1)*P(2001,2) + Metric(2002,1001)*Metric(2003,1002)*Metric(2004,1003)*P(1004,1)*P(2001,2) + Metric(1002,1003)*Metric(2002,2004)*Metric(2003,1001)*P(1004,3)*P(2001,2) + Metric(1001,1003)*Metric(2002,2004)*Metric(2003,1002)*P(1004,3)*P(2001,2) - 2*Metric(1002,1003)*Metric(2002,2003)*Metric(2004,1001)*P(1004,3)*P(2001,2) - 2*Metric(2002,1003)*Metric(2003,1002)*Metric(2004,1001)*P(1004,3)*P(2001,2) - 2*Metric(2002,1002)*Metric(2003,1003)*Metric(2004,1001)*P(1004,3)*P(2001,2) + Metric(1001,1003)*Metric(2002,2003)*Metric(2004,1002)*P(1004,3)*P(2001,2) + Metric(2002,1003)*Metric(2003,1001)*Metric(2004,1002)*P(1004,3)*P(2001,2) - 2*Metric(1002,1003)*Metric(2002,2003)*Metric(2004,1004)*P(1001,2)*P(2001,3) - 2*Metric(2002,1003)*Metric(2003,1002)*Metric(2004,1004)*P(1001,2)*P(2001,3) - 2*Metric(1003,1004)*Metric(2002,1002)*Metric(2003,2004)*P(1001,4)*P(2001,3) - 2*Metric(2002,1002)*Metric(2003,1004)*Metric(2004,1003)*P(1001,4)*P(2001,3) + Metric(1003,1004)*Metric(2002,2004)*Metric(2003,1001)*P(1002,1)*P(2001,3) + Metric(1001,1003)*Metric(2002,2004)*Metric(2003,1004)*P(1002,1)*P(2001,3) - 2*Metric(1003,1004)*Metric(2002,1001)*Metric(2003,2004)*P(1002,1)*P(2001,3) + Metric(1001,1003)*Metric(2002,1004)*Metric(2003,2004)*P(1002,1)*P(2001,3) + Metric(2002,1004)*Metric(2003,1001)*Metric(2004,1003)*P(1002,1)*P(2001,3) - 2*Metric(2002,1001)*Metric(2003,1004)*Metric(2004,1003)*P(1002,1)*P(2001,3) + Metric(1001,1003)*Metric(2002,2003)*Metric(2004,1004)*P(1002,1)*P(2001,3) + Metric(2002,1003)*Metric(2003,1001)*Metric(2004,1004)*P(1002,1)*P(2001,3) - 2*Metric(1003,1004)*Metric(2002,1001)*Metric(2003,2004)*P(1002,4)*P(2001,3) + Metric(1001,1004)*Metric(2002,1003)*Metric(2003,2004)*P(1002,4)*P(2001,3) + Metric(1003,1004)*Metric(2002,2003)*Metric(2004,1001)*P(1002,4)*P(2001,3) + Metric(2002,1003)*Metric(2003,1004)*Metric(2004,1001)*P(1002,4)*P(2001,3) + Metric(1001,1004)*Metric(2002,2003)*Metric(2004,1003)*P(1002,4)*P(2001,3) - 2*Metric(2002,1001)*Metric(2003,1004)*Metric(2004,1003)*P(1002,4)*P(2001,3) - 2*Metric(2002,1001)*Metric(2003,1003)*Metric(2004,1004)*P(1002,4)*P(2001,3) - 4*Metric(1002,1004)*Metric(2002,2004)*Metric(2003,1001)*P(1003,1)*P(2001,3) + Metric(1001,1004)*Metric(2002,2004)*Metric(2003,1002)*P(1003,1)*P(2001,3) + Metric(1001,1002)*Metric(2002,2004)*Metric(2003,1004)*P(1003,1)*P(2001,3) + Metric(1002,1004)*Metric(2002,1001)*Metric(2003,2004)*P(1003,1)*P(2001,3) + Metric(1001,1002)*Metric(2002,1004)*Metric(2003,2004)*P(1003,1)*P(2001,3) + Metric(1002,1004)*Metric(2002,2003)*Metric(2004,1001)*P(1003,1)*P(2001,3) + Metric(2002,1004)*Metric(2003,1002)*Metric(2004,1001)*P(1003,1)*P(2001,3) + Metric(1001,1004)*Metric(2002,2003)*Metric(2004,1002)*P(1003,1)*P(2001,3) - 4*Metric(2002,1004)*Metric(2003,1001)*Metric(2004,1002)*P(1003,1)*P(2001,3) + Metric(2002,1001)*Metric(2003,1004)*Metric(2004,1002)*P(1003,1)*P(2001,3) - 2*Metric(1002,1004)*Metric(2002,2004)*Metric(2003,1001)*P(1003,2)*P(2001,3) + Metric(1001,1004)*Metric(2002,2004)*Metric(2003,1002)*P(1003,2)*P(2001,3) + Metric(1002,1004)*Metric(2002,2003)*Metric(2004,1001)*P(1003,2)*P(2001,3) + Metric(2002,1004)*Metric(2003,1002)*Metric(2004,1001)*P(1003,2)*P(2001,3) + Metric(1001,1004)*Metric(2002,2003)*Metric(2004,1002)*P(1003,2)*P(2001,3) - 2*Metric(2002,1004)*Metric(2003,1001)*Metric(2004,1002)*P(1003,2)*P(2001,3) + Metric(1001,1002)*Metric(2002,2003)*Metric(2004,1004)*P(1003,2)*P(2001,3) + Metric(2002,1001)*Metric(2003,1002)*Metric(2004,1004)*P(1003,2)*P(2001,3) - 2*Metric(1002,1004)*Metric(2002,2004)*Metric(2003,1001)*P(1003,4)*P(2001,3) + Metric(1001,1002)*Metric(2002,2004)*Metric(2003,1004)*P(1003,4)*P(2001,3) + Metric(1002,1004)*Metric(2002,1001)*Metric(2003,2004)*P(1003,4)*P(2001,3) + Metric(1001,1004)*Metric(2002,1002)*Metric(2003,2004)*P(1003,4)*P(2001,3) + Metric(1001,1002)*Metric(2002,1004)*Metric(2003,2004)*P(1003,4)*P(2001,3) + Metric(2002,1002)*Metric(2003,1004)*Metric(2004,1001)*P(1003,4)*P(2001,3) - 2*Metric(2002,1004)*Metric(2003,1001)*Metric(2004,1002)*P(1003,4)*P(2001,3) + Metric(2002,1001)*Metric(2003,1004)*Metric(2004,1002)*P(1003,4)*P(2001,3) + Metric(1002,1003)*Metric(2002,2004)*Metric(2003,1001)*P(1004,1)*P(2001,3) + Metric(1001,1003)*Metric(2002,2004)*Metric(2003,1002)*P(1004,1)*P(2001,3) + Metric(1001,1003)*Metric(2002,1002)*Metric(2003,2004)*P(1004,1)*P(2001,3) - 2*Metric(1002,1003)*Metric(2002,2003)*Metric(2004,1001)*P(1004,1)*P(2001,3) - 2*Metric(2002,1003)*Metric(2003,1002)*Metric(2004,1001)*P(1004,1)*P(2001,3) + Metric(1001,1003)*Metric(2002,2003)*Metric(2004,1002)*P(1004,1)*P(2001,3) + Metric(2002,1003)*Metric(2003,1001)*Metric(2004,1002)*P(1004,1)*P(2001,3) + Metric(2002,1002)*Metric(2003,1001)*Metric(2004,1003)*P(1004,1)*P(2001,3) + Metric(1002,1003)*Metric(2002,1001)*Metric(2003,2004)*P(1004,2)*P(2001,3) + Metric(1001,1002)*Metric(2002,1003)*Metric(2003,2004)*P(1004,2)*P(2001,3) - 2*Metric(1002,1003)*Metric(2002,2003)*Metric(2004,1001)*P(1004,2)*P(2001,3) - 2*Metric(2002,1003)*Metric(2003,1002)*Metric(2004,1001)*P(1004,2)*P(2001,3) - 2*Metric(2002,1002)*Metric(2003,1003)*Metric(2004,1001)*P(1004,2)*P(2001,3) + Metric(1001,1002)*Metric(2002,2003)*Metric(2004,1003)*P(1004,2)*P(2001,3) + Metric(2002,1001)*Metric(2003,1002)*Metric(2004,1003)*P(1004,2)*P(2001,3) - 2*Metric(1002,1004)*Metric(2002,2004)*Metric(2003,1003)*P(1001,2)*P(2001,4) - 2*Metric(2002,1004)*Metric(2003,1003)*Metric(2004,1002)*P(1001,2)*P(2001,4) - 2*Metric(1003,1004)*Metric(2002,1002)*Metric(2003,2004)*P(1001,3)*P(2001,4) - 2*Metric(2002,1002)*Metric(2003,1004)*Metric(2004,1003)*P(1001,3)*P(2001,4) + Metric(1001,1004)*Metric(2002,2004)*Metric(2003,1003)*P(1002,1)*P(2001,4) - 2*Metric(1003,1004)*Metric(2002,1001)*Metric(2003,2004)*P(1002,1)*P(2001,4) + Metric(1001,1004)*Metric(2002,1003)*Metric(2003,2004)*P(1002,1)*P(2001,4) + Metric(1003,1004)*Metric(2002,2003)*Metric(2004,1001)*P(1002,1)*P(2001,4) + Metric(2002,1004)*Metric(2003,1003)*Metric(2004,1001)*P(1002,1)*P(2001,4) + Metric(2002,1003)*Metric(2003,1004)*Metric(2004,1001)*P(1002,1)*P(2001,4) + Metric(1001,1004)*Metric(2002,2003)*Metric(2004,1003)*P(1002,1)*P(2001,4) - 2*Metric(2002,1001)*Metric(2003,1004)*Metric(2004,1003)*P(1002,1)*P(2001,4) + Metric(1003,1004)*Metric(2002,2004)*Metric(2003,1001)*P(1002,3)*P(2001,4) + Metric(1001,1003)*Metric(2002,2004)*Metric(2003,1004)*P(1002,3)*P(2001,4) - 2*Metric(1003,1004)*Metric(2002,1001)*Metric(2003,2004)*P(1002,3)*P(2001,4) + Metric(1001,1003)*Metric(2002,1004)*Metric(2003,2004)*P(1002,3)*P(2001,4) + Metric(2002,1004)*Metric(2003,1001)*Metric(2004,1003)*P(1002,3)*P(2001,4) - 2*Metric(2002,1001)*Metric(2003,1004)*Metric(2004,1003)*P(1002,3)*P(2001,4) - 2*Metric(2002,1001)*Metric(2003,1003)*Metric(2004,1004)*P(1002,3)*P(2001,4) - 2*Metric(1002,1004)*Metric(2002,2004)*Metric(2003,1001)*P(1003,1)*P(2001,4) + Metric(1001,1004)*Metric(2002,2004)*Metric(2003,1002)*P(1003,1)*P(2001,4) + Metric(1001,1004)*Metric(2002,1002)*Metric(2003,2004)*P(1003,1)*P(2001,4) + Metric(1002,1004)*Metric(2002,2003)*Metric(2004,1001)*P(1003,1)*P(2001,4) + Metric(2002,1004)*Metric(2003,1002)*Metric(2004,1001)*P(1003,1)*P(2001,4) + Metric(2002,1002)*Metric(2003,1004)*Metric(2004,1001)*P(1003,1)*P(2001,4) + Metric(1001,1004)*Metric(2002,2003)*Metric(2004,1002)*P(1003,1)*P(2001,4) - 2*Metric(2002,1004)*Metric(2003,1001)*Metric(2004,1002)*P(1003,1)*P(2001,4) - 2*Metric(1002,1004)*Metric(2002,2004)*Metric(2003,1001)*P(1003,2)*P(2001,4) + Metric(1001,1002)*Metric(2002,2004)*Metric(2003,1004)*P(1003,2)*P(2001,4) + Metric(1002,1004)*Metric(2002,1001)*Metric(2003,2004)*P(1003,2)*P(2001,4) + Metric(1001,1002)*Metric(2002,1004)*Metric(2003,2004)*P(1003,2)*P(2001,4) - 2*Metric(2002,1004)*Metric(2003,1001)*Metric(2004,1002)*P(1003,2)*P(2001,4) + Metric(2002,1001)*Metric(2003,1004)*Metric(2004,1002)*P(1003,2)*P(2001,4) - 2*Metric(2002,1002)*Metric(2003,1001)*Metric(2004,1004)*P(1003,2)*P(2001,4) + Metric(1002,1003)*Metric(2002,2004)*Metric(2003,1001)*P(1004,1)*P(2001,4) + Metric(1001,1003)*Metric(2002,2004)*Metric(2003,1002)*P(1004,1)*P(2001,4) + Metric(1002,1003)*Metric(2002,1001)*Metric(2003,2004)*P(1004,1)*P(2001,4) + Metric(1001,1002)*Metric(2002,1003)*Metric(2003,2004)*P(1004,1)*P(2001,4) - 4*Metric(1002,1003)*Metric(2002,2003)*Metric(2004,1001)*P(1004,1)*P(2001,4) - 4*Metric(2002,1003)*Metric(2003,1002)*Metric(2004,1001)*P(1004,1)*P(2001,4) + Metric(1001,1003)*Metric(2002,2003)*Metric(2004,1002)*P(1004,1)*P(2001,4) + Metric(2002,1003)*Metric(2003,1001)*Metric(2004,1002)*P(1004,1)*P(2001,4) + Metric(1001,1002)*Metric(2002,2003)*Metric(2004,1003)*P(1004,1)*P(2001,4) + Metric(2002,1001)*Metric(2003,1002)*Metric(2004,1003)*P(1004,1)*P(2001,4) + Metric(1002,1003)*Metric(2002,2004)*Metric(2003,1001)*P(1004,2)*P(2001,4) + Metric(1001,1003)*Metric(2002,2004)*Metric(2003,1002)*P(1004,2)*P(2001,4) + Metric(1001,1002)*Metric(2002,2004)*Metric(2003,1003)*P(1004,2)*P(2001,4) - 2*Metric(1002,1003)*Metric(2002,2003)*Metric(2004,1001)*P(1004,2)*P(2001,4) - 2*Metric(2002,1003)*Metric(2003,1002)*Metric(2004,1001)*P(1004,2)*P(2001,4) + Metric(1001,1003)*Metric(2002,2003)*Metric(2004,1002)*P(1004,2)*P(2001,4) + Metric(2002,1003)*Metric(2003,1001)*Metric(2004,1002)*P(1004,2)*P(2001,4) + Metric(2002,1001)*Metric(2003,1003)*Metric(2004,1002)*P(1004,2)*P(2001,4) + Metric(1002,1003)*Metric(2002,1001)*Metric(2003,2004)*P(1004,3)*P(2001,4) + Metric(1001,1003)*Metric(2002,1002)*Metric(2003,2004)*P(1004,3)*P(2001,4) + Metric(1001,1002)*Metric(2002,1003)*Metric(2003,2004)*P(1004,3)*P(2001,4) - 2*Metric(1002,1003)*Metric(2002,2003)*Metric(2004,1001)*P(1004,3)*P(2001,4) - 2*Metric(2002,1003)*Metric(2003,1002)*Metric(2004,1001)*P(1004,3)*P(2001,4) + Metric(1001,1002)*Metric(2002,2003)*Metric(2004,1003)*P(1004,3)*P(2001,4) + Metric(2002,1002)*Metric(2003,1001)*Metric(2004,1003)*P(1004,3)*P(2001,4) + Metric(2002,1001)*Metric(2003,1002)*Metric(2004,1003)*P(1004,3)*P(2001,4) + Metric(1003,1004)*Metric(2001,2004)*Metric(2003,1002)*P(1001,2)*P(2002,1) + Metric(1002,1003)*Metric(2001,2004)*Metric(2003,1004)*P(1001,2)*P(2002,1) - 4*Metric(1003,1004)*Metric(2001,1002)*Metric(2003,2004)*P(1001,2)*P(2002,1) + Metric(1002,1004)*Metric(2001,1003)*Metric(2003,2004)*P(1001,2)*P(2002,1) + Metric(1002,1003)*Metric(2001,1004)*Metric(2003,2004)*P(1001,2)*P(2002,1) + Metric(1003,1004)*Metric(2001,2003)*Metric(2004,1002)*P(1001,2)*P(2002,1) + Metric(2001,1003)*Metric(2003,1004)*Metric(2004,1002)*P(1001,2)*P(2002,1) + Metric(1002,1004)*Metric(2001,2003)*Metric(2004,1003)*P(1001,2)*P(2002,1) + Metric(2001,1004)*Metric(2003,1002)*Metric(2004,1003)*P(1001,2)*P(2002,1) - 4*Metric(2001,1002)*Metric(2003,1004)*Metric(2004,1003)*P(1001,2)*P(2002,1) - 2*Metric(1003,1004)*Metric(2001,1002)*Metric(2003,2004)*P(1001,3)*P(2002,1) + Metric(1002,1004)*Metric(2001,1003)*Metric(2003,2004)*P(1001,3)*P(2002,1) + Metric(1003,1004)*Metric(2001,2003)*Metric(2004,1002)*P(1001,3)*P(2002,1) + Metric(2001,1003)*Metric(2003,1004)*Metric(2004,1002)*P(1001,3)*P(2002,1) + Metric(1002,1004)*Metric(2001,2003)*Metric(2004,1003)*P(1001,3)*P(2002,1) - 2*Metric(2001,1002)*Metric(2003,1004)*Metric(2004,1003)*P(1001,3)*P(2002,1) + Metric(1002,1003)*Metric(2001,2003)*Metric(2004,1004)*P(1001,3)*P(2002,1) + Metric(2001,1003)*Metric(2003,1002)*Metric(2004,1004)*P(1001,3)*P(2002,1) + Metric(1003,1004)*Metric(2001,2004)*Metric(2003,1002)*P(1001,4)*P(2002,1) + Metric(1002,1004)*Metric(2001,2004)*Metric(2003,1003)*P(1001,4)*P(2002,1) + Metric(1002,1003)*Metric(2001,2004)*Metric(2003,1004)*P(1001,4)*P(2002,1) - 2*Metric(1003,1004)*Metric(2001,1002)*Metric(2003,2004)*P(1001,4)*P(2002,1) + Metric(1002,1003)*Metric(2001,1004)*Metric(2003,2004)*P(1001,4)*P(2002,1) + Metric(2001,1004)*Metric(2003,1003)*Metric(2004,1002)*P(1001,4)*P(2002,1) + Metric(2001,1004)*Metric(2003,1002)*Metric(2004,1003)*P(1001,4)*P(2002,1) - 2*Metric(2001,1002)*Metric(2003,1004)*Metric(2004,1003)*P(1001,4)*P(2002,1) - 2*Metric(1001,1003)*Metric(2001,2003)*Metric(2004,1004)*P(1002,3)*P(2002,1) - 2*Metric(2001,1003)*Metric(2003,1001)*Metric(2004,1004)*P(1002,3)*P(2002,1) - 2*Metric(1001,1004)*Metric(2001,2004)*Metric(2003,1003)*P(1002,4)*P(2002,1) - 2*Metric(2001,1004)*Metric(2003,1003)*Metric(2004,1001)*P(1002,4)*P(2002,1) - 2*Metric(1001,1004)*Metric(2001,2004)*Metric(2003,1002)*P(1003,2)*P(2002,1) + Metric(1001,1002)*Metric(2001,2004)*Metric(2003,1004)*P(1003,2)*P(2002,1) + Metric(1001,1004)*Metric(2001,1002)*Metric(2003,2004)*P(1003,2)*P(2002,1) + Metric(1001,1002)*Metric(2001,1004)*Metric(2003,2004)*P(1003,2)*P(2002,1) - 2*Metric(2001,1004)*Metric(2003,1002)*Metric(2004,1001)*P(1003,2)*P(2002,1) + Metric(2001,1002)*Metric(2003,1004)*Metric(2004,1001)*P(1003,2)*P(2002,1) + Metric(1001,1002)*Metric(2001,2003)*Metric(2004,1004)*P(1003,2)*P(2002,1) + Metric(2001,1002)*Metric(2003,1001)*Metric(2004,1004)*P(1003,2)*P(2002,1) + Metric(1002,1004)*Metric(2001,2004)*Metric(2003,1001)*P(1003,4)*P(2002,1) - 2*Metric(1001,1004)*Metric(2001,2004)*Metric(2003,1002)*P(1003,4)*P(2002,1) + Metric(1002,1004)*Metric(2001,2003)*Metric(2004,1001)*P(1003,4)*P(2002,1) - 2*Metric(2001,1004)*Metric(2003,1002)*Metric(2004,1001)*P(1003,4)*P(2002,1) + Metric(1001,1004)*Metric(2001,2003)*Metric(2004,1002)*P(1003,4)*P(2002,1) + Metric(2001,1004)*Metric(2003,1001)*Metric(2004,1002)*P(1003,4)*P(2002,1) - 2*Metric(2001,1001)*Metric(2003,1002)*Metric(2004,1004)*P(1003,4)*P(2002,1) + Metric(1001,1002)*Metric(2001,2004)*Metric(2003,1003)*P(1004,2)*P(2002,1) + Metric(1001,1003)*Metric(2001,1002)*Metric(2003,2004)*P(1004,2)*P(2002,1) + Metric(1001,1002)*Metric(2001,1003)*Metric(2003,2004)*P(1004,2)*P(2002,1) + Metric(2001,1002)*Metric(2003,1003)*Metric(2004,1001)*P(1004,2)*P(2002,1) - 2*Metric(1001,1003)*Metric(2001,2003)*Metric(2004,1002)*P(1004,2)*P(2002,1) - 2*Metric(2001,1003)*Metric(2003,1001)*Metric(2004,1002)*P(1004,2)*P(2002,1) + Metric(1001,1002)*Metric(2001,2003)*Metric(2004,1003)*P(1004,2)*P(2002,1) + Metric(2001,1002)*Metric(2003,1001)*Metric(2004,1003)*P(1004,2)*P(2002,1) + Metric(1002,1003)*Metric(2001,2004)*Metric(2003,1001)*P(1004,3)*P(2002,1) + Metric(1001,1003)*Metric(2001,2004)*Metric(2003,1002)*P(1004,3)*P(2002,1) + Metric(1002,1003)*Metric(2001,2003)*Metric(2004,1001)*P(1004,3)*P(2002,1) + Metric(2001,1003)*Metric(2003,1002)*Metric(2004,1001)*P(1004,3)*P(2002,1) - 2*Metric(1001,1003)*Metric(2001,2003)*Metric(2004,1002)*P(1004,3)*P(2002,1) - 2*Metric(2001,1003)*Metric(2003,1001)*Metric(2004,1002)*P(1004,3)*P(2002,1) - 2*Metric(2001,1001)*Metric(2003,1003)*Metric(2004,1002)*P(1004,3)*P(2002,1) + Metric(1001,1004)*Metric(1002,1003)*Metric(2003,2004)*P(2001,2)*P(2002,1) + Metric(1001,1003)*Metric(1002,1004)*Metric(2003,2004)*P(2001,2)*P(2002,1) - 4*Metric(1001,1002)*Metric(1003,1004)*Metric(2003,2004)*P(2001,2)*P(2002,1) + Metric(1003,1004)*Metric(2003,1002)*Metric(2004,1001)*P(2001,2)*P(2002,1) + Metric(1002,1003)*Metric(2003,1004)*Metric(2004,1001)*P(2001,2)*P(2002,1) + Metric(1003,1004)*Metric(2003,1001)*Metric(2004,1002)*P(2001,2)*P(2002,1) + Metric(1001,1003)*Metric(2003,1004)*Metric(2004,1002)*P(2001,2)*P(2002,1) + Metric(1002,1004)*Metric(2003,1001)*Metric(2004,1003)*P(2001,2)*P(2002,1) + Metric(1001,1004)*Metric(2003,1002)*Metric(2004,1003)*P(2001,2)*P(2002,1) - 4*Metric(1001,1002)*Metric(2003,1004)*Metric(2004,1003)*P(2001,2)*P(2002,1) + Metric(1001,1003)*Metric(1002,1004)*Metric(2003,2004)*P(2001,3)*P(2002,1) - 2*Metric(1001,1002)*Metric(1003,1004)*Metric(2003,2004)*P(2001,3)*P(2002,1) + Metric(1003,1004)*Metric(2003,1001)*Metric(2004,1002)*P(2001,3)*P(2002,1) + Metric(1001,1003)*Metric(2003,1004)*Metric(2004,1002)*P(2001,3)*P(2002,1) + Metric(1002,1004)*Metric(2003,1001)*Metric(2004,1003)*P(2001,3)*P(2002,1) - 2*Metric(1001,1002)*Metric(2003,1004)*Metric(2004,1003)*P(2001,3)*P(2002,1) + Metric(1002,1003)*Metric(2003,1001)*Metric(2004,1004)*P(2001,3)*P(2002,1) + Metric(1001,1003)*Metric(2003,1002)*Metric(2004,1004)*P(2001,3)*P(2002,1) + Metric(1001,1004)*Metric(1002,1003)*Metric(2003,2004)*P(2001,4)*P(2002,1) - 2*Metric(1001,1002)*Metric(1003,1004)*Metric(2003,2004)*P(2001,4)*P(2002,1) + Metric(1003,1004)*Metric(2003,1002)*Metric(2004,1001)*P(2001,4)*P(2002,1) + Metric(1002,1004)*Metric(2003,1003)*Metric(2004,1001)*P(2001,4)*P(2002,1) + Metric(1002,1003)*Metric(2003,1004)*Metric(2004,1001)*P(2001,4)*P(2002,1) + Metric(1001,1004)*Metric(2003,1003)*Metric(2004,1002)*P(2001,4)*P(2002,1) + Metric(1001,1004)*Metric(2003,1002)*Metric(2004,1003)*P(2001,4)*P(2002,1) - 2*Metric(1001,1002)*Metric(2003,1004)*Metric(2004,1003)*P(2001,4)*P(2002,1) + Metric(1003,1004)*Metric(2001,2004)*Metric(2003,1002)*P(1001,2)*P(2002,3) + Metric(1002,1003)*Metric(2001,2004)*Metric(2003,1004)*P(1001,2)*P(2002,3) - 2*Metric(1003,1004)*Metric(2001,1002)*Metric(2003,2004)*P(1001,2)*P(2002,3) + Metric(1002,1003)*Metric(2001,1004)*Metric(2003,2004)*P(1001,2)*P(2002,3) + Metric(2001,1004)*Metric(2003,1002)*Metric(2004,1003)*P(1001,2)*P(2002,3) - 2*Metric(2001,1002)*Metric(2003,1004)*Metric(2004,1003)*P(1001,2)*P(2002,3) + Metric(1002,1003)*Metric(2001,2003)*Metric(2004,1004)*P(1001,2)*P(2002,3) + Metric(2001,1003)*Metric(2003,1002)*Metric(2004,1004)*P(1001,2)*P(2002,3) - 2*Metric(1003,1004)*Metric(2001,1002)*Metric(2003,2004)*P(1001,4)*P(2002,3) + Metric(1002,1004)*Metric(2001,1003)*Metric(2003,2004)*P(1001,4)*P(2002,3) + Metric(1003,1004)*Metric(2001,2003)*Metric(2004,1002)*P(1001,4)*P(2002,3) + Metric(2001,1003)*Metric(2003,1004)*Metric(2004,1002)*P(1001,4)*P(2002,3) + Metric(1002,1004)*Metric(2001,2003)*Metric(2004,1003)*P(1001,4)*P(2002,3) - 2*Metric(2001,1002)*Metric(2003,1004)*Metric(2004,1003)*P(1001,4)*P(2002,3) - 2*Metric(2001,1002)*Metric(2003,1003)*Metric(2004,1004)*P(1001,4)*P(2002,3) - 2*Metric(1001,1003)*Metric(2001,2003)*Metric(2004,1004)*P(1002,1)*P(2002,3) - 2*Metric(2001,1003)*Metric(2003,1001)*Metric(2004,1004)*P(1002,1)*P(2002,3) - 2*Metric(1003,1004)*Metric(2001,1001)*Metric(2003,2004)*P(1002,4)*P(2002,3) - 2*Metric(2001,1001)*Metric(2003,1004)*Metric(2004,1003)*P(1002,4)*P(2002,3) + Metric(1002,1004)*Metric(2001,2004)*Metric(2003,1001)*P(1003,1)*P(2002,3) - 2*Metric(1001,1004)*Metric(2001,2004)*Metric(2003,1002)*P(1003,1)*P(2002,3) + Metric(1002,1004)*Metric(2001,2003)*Metric(2004,1001)*P(1003,1)*P(2002,3) - 2*Metric(2001,1004)*Metric(2003,1002)*Metric(2004,1001)*P(1003,1)*P(2002,3) + Metric(1001,1004)*Metric(2001,2003)*Metric(2004,1002)*P(1003,1)*P(2002,3) + Metric(2001,1004)*Metric(2003,1001)*Metric(2004,1002)*P(1003,1)*P(2002,3) + Metric(1001,1002)*Metric(2001,2003)*Metric(2004,1004)*P(1003,1)*P(2002,3) + Metric(2001,1002)*Metric(2003,1001)*Metric(2004,1004)*P(1003,1)*P(2002,3) + Metric(1002,1004)*Metric(2001,2004)*Metric(2003,1001)*P(1003,2)*P(2002,3) - 4*Metric(1001,1004)*Metric(2001,2004)*Metric(2003,1002)*P(1003,2)*P(2002,3) + Metric(1001,1002)*Metric(2001,2004)*Metric(2003,1004)*P(1003,2)*P(2002,3) + Metric(1001,1004)*Metric(2001,1002)*Metric(2003,2004)*P(1003,2)*P(2002,3) + Metric(1001,1002)*Metric(2001,1004)*Metric(2003,2004)*P(1003,2)*P(2002,3) + Metric(1002,1004)*Metric(2001,2003)*Metric(2004,1001)*P(1003,2)*P(2002,3) - 4*Metric(2001,1004)*Metric(2003,1002)*Metric(2004,1001)*P(1003,2)*P(2002,3) + Metric(2001,1002)*Metric(2003,1004)*Metric(2004,1001)*P(1003,2)*P(2002,3) + Metric(1001,1004)*Metric(2001,2003)*Metric(2004,1002)*P(1003,2)*P(2002,3) + Metric(2001,1004)*Metric(2003,1001)*Metric(2004,1002)*P(1003,2)*P(2002,3) - 2*Metric(1001,1004)*Metric(2001,2004)*Metric(2003,1002)*P(1003,4)*P(2002,3) + Metric(1001,1002)*Metric(2001,2004)*Metric(2003,1004)*P(1003,4)*P(2002,3) + Metric(1002,1004)*Metric(2001,1001)*Metric(2003,2004)*P(1003,4)*P(2002,3) + Metric(1001,1004)*Metric(2001,1002)*Metric(2003,2004)*P(1003,4)*P(2002,3) + Metric(1001,1002)*Metric(2001,1004)*Metric(2003,2004)*P(1003,4)*P(2002,3) - 2*Metric(2001,1004)*Metric(2003,1002)*Metric(2004,1001)*P(1003,4)*P(2002,3) + Metric(2001,1002)*Metric(2003,1004)*Metric(2004,1001)*P(1003,4)*P(2002,3) + Metric(2001,1001)*Metric(2003,1004)*Metric(2004,1002)*P(1003,4)*P(2002,3) + Metric(1001,1003)*Metric(2001,1002)*Metric(2003,2004)*P(1004,1)*P(2002,3) + Metric(1001,1002)*Metric(2001,1003)*Metric(2003,2004)*P(1004,1)*P(2002,3) - 2*Metric(1001,1003)*Metric(2001,2003)*Metric(2004,1002)*P(1004,1)*P(2002,3) - 2*Metric(2001,1003)*Metric(2003,1001)*Metric(2004,1002)*P(1004,1)*P(2002,3) - 2*Metric(2001,1001)*Metric(2003,1003)*Metric(2004,1002)*P(1004,1)*P(2002,3) + Metric(1001,1002)*Metric(2001,2003)*Metric(2004,1003)*P(1004,1)*P(2002,3) + Metric(2001,1002)*Metric(2003,1001)*Metric(2004,1003)*P(1004,1)*P(2002,3) + Metric(1002,1003)*Metric(2001,2004)*Metric(2003,1001)*P(1004,2)*P(2002,3) + Metric(1001,1003)*Metric(2001,2004)*Metric(2003,1002)*P(1004,2)*P(2002,3) + Metric(1002,1003)*Metric(2001,1001)*Metric(2003,2004)*P(1004,2)*P(2002,3) + Metric(1002,1003)*Metric(2001,2003)*Metric(2004,1001)*P(1004,2)*P(2002,3) + Metric(2001,1003)*Metric(2003,1002)*Metric(2004,1001)*P(1004,2)*P(2002,3) - 2*Metric(1001,1003)*Metric(2001,2003)*Metric(2004,1002)*P(1004,2)*P(2002,3) - 2*Metric(2001,1003)*Metric(2003,1001)*Metric(2004,1002)*P(1004,2)*P(2002,3) + Metric(2001,1001)*Metric(2003,1002)*Metric(2004,1003)*P(1004,2)*P(2002,3) + Metric(1001,1004)*Metric(1002,1003)*Metric(2003,2004)*P(2001,2)*P(2002,3) - 2*Metric(1001,1002)*Metric(1003,1004)*Metric(2003,2004)*P(2001,2)*P(2002,3) + Metric(1003,1004)*Metric(2003,1002)*Metric(2004,1001)*P(2001,2)*P(2002,3) + Metric(1002,1003)*Metric(2003,1004)*Metric(2004,1001)*P(2001,2)*P(2002,3) + Metric(1001,1004)*Metric(2003,1002)*Metric(2004,1003)*P(2001,2)*P(2002,3) - 2*Metric(1001,1002)*Metric(2003,1004)*Metric(2004,1003)*P(2001,2)*P(2002,3) + Metric(1002,1003)*Metric(2003,1001)*Metric(2004,1004)*P(2001,2)*P(2002,3) + Metric(1001,1003)*Metric(2003,1002)*Metric(2004,1004)*P(2001,2)*P(2002,3) + Metric(1001,1003)*Metric(1002,1004)*Metric(2003,2004)*P(2001,4)*P(2002,3) - 2*Metric(1001,1002)*Metric(1003,1004)*Metric(2003,2004)*P(2001,4)*P(2002,3) + Metric(1003,1004)*Metric(2003,1001)*Metric(2004,1002)*P(2001,4)*P(2002,3) + Metric(1001,1003)*Metric(2003,1004)*Metric(2004,1002)*P(2001,4)*P(2002,3) + Metric(1002,1004)*Metric(2003,1001)*Metric(2004,1003)*P(2001,4)*P(2002,3) - 2*Metric(1001,1002)*Metric(2003,1004)*Metric(2004,1003)*P(2001,4)*P(2002,3) - 2*Metric(1001,1002)*Metric(2003,1003)*Metric(2004,1004)*P(2001,4)*P(2002,3) + Metric(1002,1004)*Metric(2001,2004)*Metric(2003,1003)*P(1001,2)*P(2002,4) - 2*Metric(1003,1004)*Metric(2001,1002)*Metric(2003,2004)*P(1001,2)*P(2002,4) + Metric(1002,1004)*Metric(2001,1003)*Metric(2003,2004)*P(1001,2)*P(2002,4) + Metric(1003,1004)*Metric(2001,2003)*Metric(2004,1002)*P(1001,2)*P(2002,4) + Metric(2001,1004)*Metric(2003,1003)*Metric(2004,1002)*P(1001,2)*P(2002,4) + Metric(2001,1003)*Metric(2003,1004)*Metric(2004,1002)*P(1001,2)*P(2002,4) + Metric(1002,1004)*Metric(2001,2003)*Metric(2004,1003)*P(1001,2)*P(2002,4) - 2*Metric(2001,1002)*Metric(2003,1004)*Metric(2004,1003)*P(1001,2)*P(2002,4) + Metric(1003,1004)*Metric(2001,2004)*Metric(2003,1002)*P(1001,3)*P(2002,4) + Metric(1002,1003)*Metric(2001,2004)*Metric(2003,1004)*P(1001,3)*P(2002,4) - 2*Metric(1003,1004)*Metric(2001,1002)*Metric(2003,2004)*P(1001,3)*P(2002,4) + Metric(1002,1003)*Metric(2001,1004)*Metric(2003,2004)*P(1001,3)*P(2002,4) + Metric(2001,1004)*Metric(2003,1002)*Metric(2004,1003)*P(1001,3)*P(2002,4) - 2*Metric(2001,1002)*Metric(2003,1004)*Metric(2004,1003)*P(1001,3)*P(2002,4) - 2*Metric(2001,1002)*Metric(2003,1003)*Metric(2004,1004)*P(1001,3)*P(2002,4) - 2*Metric(1001,1004)*Metric(2001,2004)*Metric(2003,1003)*P(1002,1)*P(2002,4) - 2*Metric(2001,1004)*Metric(2003,1003)*Metric(2004,1001)*P(1002,1)*P(2002,4) - 2*Metric(1003,1004)*Metric(2001,1001)*Metric(2003,2004)*P(1002,3)*P(2002,4) - 2*Metric(2001,1001)*Metric(2003,1004)*Metric(2004,1003)*P(1002,3)*P(2002,4) - 2*Metric(1001,1004)*Metric(2001,2004)*Metric(2003,1002)*P(1003,1)*P(2002,4) + Metric(1001,1002)*Metric(2001,2004)*Metric(2003,1004)*P(1003,1)*P(2002,4) + Metric(1001,1004)*Metric(2001,1002)*Metric(2003,2004)*P(1003,1)*P(2002,4) + Metric(1001,1002)*Metric(2001,1004)*Metric(2003,2004)*P(1003,1)*P(2002,4) - 2*Metric(2001,1004)*Metric(2003,1002)*Metric(2004,1001)*P(1003,1)*P(2002,4) + Metric(2001,1002)*Metric(2003,1004)*Metric(2004,1001)*P(1003,1)*P(2002,4) - 2*Metric(2001,1001)*Metric(2003,1002)*Metric(2004,1004)*P(1003,1)*P(2002,4) + Metric(1002,1004)*Metric(2001,2004)*Metric(2003,1001)*P(1003,2)*P(2002,4) - 2*Metric(1001,1004)*Metric(2001,2004)*Metric(2003,1002)*P(1003,2)*P(2002,4) + Metric(1002,1004)*Metric(2001,1001)*Metric(2003,2004)*P(1003,2)*P(2002,4) + Metric(1002,1004)*Metric(2001,2003)*Metric(2004,1001)*P(1003,2)*P(2002,4) - 2*Metric(2001,1004)*Metric(2003,1002)*Metric(2004,1001)*P(1003,2)*P(2002,4) + Metric(1001,1004)*Metric(2001,2003)*Metric(2004,1002)*P(1003,2)*P(2002,4) + Metric(2001,1004)*Metric(2003,1001)*Metric(2004,1002)*P(1003,2)*P(2002,4) + Metric(2001,1001)*Metric(2003,1004)*Metric(2004,1002)*P(1003,2)*P(2002,4) + Metric(1002,1003)*Metric(2001,2004)*Metric(2003,1001)*P(1004,1)*P(2002,4) + Metric(1001,1003)*Metric(2001,2004)*Metric(2003,1002)*P(1004,1)*P(2002,4) + Metric(1001,1002)*Metric(2001,2004)*Metric(2003,1003)*P(1004,1)*P(2002,4) + Metric(1002,1003)*Metric(2001,2003)*Metric(2004,1001)*P(1004,1)*P(2002,4) + Metric(2001,1003)*Metric(2003,1002)*Metric(2004,1001)*P(1004,1)*P(2002,4) + Metric(2001,1002)*Metric(2003,1003)*Metric(2004,1001)*P(1004,1)*P(2002,4) - 2*Metric(1001,1003)*Metric(2001,2003)*Metric(2004,1002)*P(1004,1)*P(2002,4) - 2*Metric(2001,1003)*Metric(2003,1001)*Metric(2004,1002)*P(1004,1)*P(2002,4) + Metric(1002,1003)*Metric(2001,2004)*Metric(2003,1001)*P(1004,2)*P(2002,4) + Metric(1001,1003)*Metric(2001,2004)*Metric(2003,1002)*P(1004,2)*P(2002,4) + Metric(1001,1003)*Metric(2001,1002)*Metric(2003,2004)*P(1004,2)*P(2002,4) + Metric(1001,1002)*Metric(2001,1003)*Metric(2003,2004)*P(1004,2)*P(2002,4) + Metric(1002,1003)*Metric(2001,2003)*Metric(2004,1001)*P(1004,2)*P(2002,4) + Metric(2001,1003)*Metric(2003,1002)*Metric(2004,1001)*P(1004,2)*P(2002,4) - 4*Metric(1001,1003)*Metric(2001,2003)*Metric(2004,1002)*P(1004,2)*P(2002,4) - 4*Metric(2001,1003)*Metric(2003,1001)*Metric(2004,1002)*P(1004,2)*P(2002,4) + Metric(1001,1002)*Metric(2001,2003)*Metric(2004,1003)*P(1004,2)*P(2002,4) + Metric(2001,1002)*Metric(2003,1001)*Metric(2004,1003)*P(1004,2)*P(2002,4) + Metric(1002,1003)*Metric(2001,1001)*Metric(2003,2004)*P(1004,3)*P(2002,4) + Metric(1001,1003)*Metric(2001,1002)*Metric(2003,2004)*P(1004,3)*P(2002,4) + Metric(1001,1002)*Metric(2001,1003)*Metric(2003,2004)*P(1004,3)*P(2002,4) - 2*Metric(1001,1003)*Metric(2001,2003)*Metric(2004,1002)*P(1004,3)*P(2002,4) - 2*Metric(2001,1003)*Metric(2003,1001)*Metric(2004,1002)*P(1004,3)*P(2002,4) + Metric(1001,1002)*Metric(2001,2003)*Metric(2004,1003)*P(1004,3)*P(2002,4) + Metric(2001,1002)*Metric(2003,1001)*Metric(2004,1003)*P(1004,3)*P(2002,4) + Metric(2001,1001)*Metric(2003,1002)*Metric(2004,1003)*P(1004,3)*P(2002,4) + Metric(1001,1003)*Metric(1002,1004)*Metric(2003,2004)*P(2001,2)*P(2002,4) - 2*Metric(1001,1002)*Metric(1003,1004)*Metric(2003,2004)*P(2001,2)*P(2002,4) + Metric(1002,1004)*Metric(2003,1003)*Metric(2004,1001)*P(2001,2)*P(2002,4) + Metric(1003,1004)*Metric(2003,1001)*Metric(2004,1002)*P(2001,2)*P(2002,4) + Metric(1001,1004)*Metric(2003,1003)*Metric(2004,1002)*P(2001,2)*P(2002,4) + Metric(1001,1003)*Metric(2003,1004)*Metric(2004,1002)*P(2001,2)*P(2002,4) + Metric(1002,1004)*Metric(2003,1001)*Metric(2004,1003)*P(2001,2)*P(2002,4) - 2*Metric(1001,1002)*Metric(2003,1004)*Metric(2004,1003)*P(2001,2)*P(2002,4) + Metric(1001,1004)*Metric(1002,1003)*Metric(2003,2004)*P(2001,3)*P(2002,4) - 2*Metric(1001,1002)*Metric(1003,1004)*Metric(2003,2004)*P(2001,3)*P(2002,4) + Metric(1003,1004)*Metric(2003,1002)*Metric(2004,1001)*P(2001,3)*P(2002,4) + Metric(1002,1003)*Metric(2003,1004)*Metric(2004,1001)*P(2001,3)*P(2002,4) + Metric(1001,1004)*Metric(2003,1002)*Metric(2004,1003)*P(2001,3)*P(2002,4) - 2*Metric(1001,1002)*Metric(2003,1004)*Metric(2004,1003)*P(2001,3)*P(2002,4) - 2*Metric(1001,1002)*Metric(2003,1003)*Metric(2004,1004)*P(2001,3)*P(2002,4) + Metric(1003,1004)*Metric(2001,1002)*Metric(2002,2004)*P(1001,2)*P(2003,1) - 2*Metric(1002,1004)*Metric(2001,1003)*Metric(2002,2004)*P(1001,2)*P(2003,1) + Metric(1003,1004)*Metric(2001,2002)*Metric(2004,1002)*P(1001,2)*P(2003,1) - 2*Metric(2001,1003)*Metric(2002,1004)*Metric(2004,1002)*P(1001,2)*P(2003,1) + Metric(1002,1004)*Metric(2001,2002)*Metric(2004,1003)*P(1001,2)*P(2003,1) + Metric(2001,1002)*Metric(2002,1004)*Metric(2004,1003)*P(1001,2)*P(2003,1) + Metric(1002,1003)*Metric(2001,2002)*Metric(2004,1004)*P(1001,2)*P(2003,1) + Metric(2001,1002)*Metric(2002,1003)*Metric(2004,1004)*P(1001,2)*P(2003,1) + Metric(1002,1004)*Metric(2001,2004)*Metric(2002,1003)*P(1001,3)*P(2003,1) + Metric(1002,1003)*Metric(2001,2004)*Metric(2002,1004)*P(1001,3)*P(2003,1) + Metric(1003,1004)*Metric(2001,1002)*Metric(2002,2004)*P(1001,3)*P(2003,1) - 4*Metric(1002,1004)*Metric(2001,1003)*Metric(2002,2004)*P(1001,3)*P(2003,1) + Metric(1002,1003)*Metric(2001,1004)*Metric(2002,2004)*P(1001,3)*P(2003,1) + Metric(1003,1004)*Metric(2001,2002)*Metric(2004,1002)*P(1001,3)*P(2003,1) + Metric(2001,1004)*Metric(2002,1003)*Metric(2004,1002)*P(1001,3)*P(2003,1) - 4*Metric(2001,1003)*Metric(2002,1004)*Metric(2004,1002)*P(1001,3)*P(2003,1) + Metric(1002,1004)*Metric(2001,2002)*Metric(2004,1003)*P(1001,3)*P(2003,1) + Metric(2001,1002)*Metric(2002,1004)*Metric(2004,1003)*P(1001,3)*P(2003,1) + Metric(1003,1004)*Metric(2001,2004)*Metric(2002,1002)*P(1001,4)*P(2003,1) + Metric(1002,1004)*Metric(2001,2004)*Metric(2002,1003)*P(1001,4)*P(2003,1) + Metric(1002,1003)*Metric(2001,2004)*Metric(2002,1004)*P(1001,4)*P(2003,1) - 2*Metric(1002,1004)*Metric(2001,1003)*Metric(2002,2004)*P(1001,4)*P(2003,1) + Metric(1002,1003)*Metric(2001,1004)*Metric(2002,2004)*P(1001,4)*P(2003,1) + Metric(2001,1004)*Metric(2002,1003)*Metric(2004,1002)*P(1001,4)*P(2003,1) - 2*Metric(2001,1003)*Metric(2002,1004)*Metric(2004,1002)*P(1001,4)*P(2003,1) + Metric(2001,1004)*Metric(2002,1002)*Metric(2004,1003)*P(1001,4)*P(2003,1) - 2*Metric(1001,1004)*Metric(2001,2004)*Metric(2002,1003)*P(1002,3)*P(2003,1) + Metric(1001,1003)*Metric(2001,2004)*Metric(2002,1004)*P(1002,3)*P(2003,1) + Metric(1001,1004)*Metric(2001,1003)*Metric(2002,2004)*P(1002,3)*P(2003,1) + Metric(1001,1003)*Metric(2001,1004)*Metric(2002,2004)*P(1002,3)*P(2003,1) - 2*Metric(2001,1004)*Metric(2002,1003)*Metric(2004,1001)*P(1002,3)*P(2003,1) + Metric(2001,1003)*Metric(2002,1004)*Metric(2004,1001)*P(1002,3)*P(2003,1) + Metric(1001,1003)*Metric(2001,2002)*Metric(2004,1004)*P(1002,3)*P(2003,1) + Metric(2001,1003)*Metric(2002,1001)*Metric(2004,1004)*P(1002,3)*P(2003,1) + Metric(1003,1004)*Metric(2001,2004)*Metric(2002,1001)*P(1002,4)*P(2003,1) - 2*Metric(1001,1004)*Metric(2001,2004)*Metric(2002,1003)*P(1002,4)*P(2003,1) + Metric(1003,1004)*Metric(2001,2002)*Metric(2004,1001)*P(1002,4)*P(2003,1) - 2*Metric(2001,1004)*Metric(2002,1003)*Metric(2004,1001)*P(1002,4)*P(2003,1) + Metric(1001,1004)*Metric(2001,2002)*Metric(2004,1003)*P(1002,4)*P(2003,1) + Metric(2001,1004)*Metric(2002,1001)*Metric(2004,1003)*P(1002,4)*P(2003,1) - 2*Metric(2001,1001)*Metric(2002,1003)*Metric(2004,1004)*P(1002,4)*P(2003,1) - 2*Metric(1001,1002)*Metric(2001,2002)*Metric(2004,1004)*P(1003,2)*P(2003,1) - 2*Metric(2001,1002)*Metric(2002,1001)*Metric(2004,1004)*P(1003,2)*P(2003,1) - 2*Metric(1001,1004)*Metric(2001,2004)*Metric(2002,1002)*P(1003,4)*P(2003,1) - 2*Metric(2001,1004)*Metric(2002,1002)*Metric(2004,1001)*P(1003,4)*P(2003,1) + Metric(1002,1003)*Metric(2001,2004)*Metric(2002,1001)*P(1004,2)*P(2003,1) + Metric(1001,1002)*Metric(2001,2004)*Metric(2002,1003)*P(1004,2)*P(2003,1) + Metric(1002,1003)*Metric(2001,2002)*Metric(2004,1001)*P(1004,2)*P(2003,1) + Metric(2001,1002)*Metric(2002,1003)*Metric(2004,1001)*P(1004,2)*P(2003,1) - 2*Metric(1001,1002)*Metric(2001,2002)*Metric(2004,1003)*P(1004,2)*P(2003,1) - 2*Metric(2001,1002)*Metric(2002,1001)*Metric(2004,1003)*P(1004,2)*P(2003,1) - 2*Metric(2001,1001)*Metric(2002,1002)*Metric(2004,1003)*P(1004,2)*P(2003,1) + Metric(1001,1003)*Metric(2001,2004)*Metric(2002,1002)*P(1004,3)*P(2003,1) + Metric(1001,1003)*Metric(2001,1002)*Metric(2002,2004)*P(1004,3)*P(2003,1) + Metric(1001,1002)*Metric(2001,1003)*Metric(2002,2004)*P(1004,3)*P(2003,1) + Metric(2001,1003)*Metric(2002,1002)*Metric(2004,1001)*P(1004,3)*P(2003,1) + Metric(1001,1003)*Metric(2001,2002)*Metric(2004,1002)*P(1004,3)*P(2003,1) + Metric(2001,1003)*Metric(2002,1001)*Metric(2004,1002)*P(1004,3)*P(2003,1) - 2*Metric(1001,1002)*Metric(2001,2002)*Metric(2004,1003)*P(1004,3)*P(2003,1) - 2*Metric(2001,1002)*Metric(2002,1001)*Metric(2004,1003)*P(1004,3)*P(2003,1) - 2*Metric(1001,1003)*Metric(1002,1004)*Metric(2002,2004)*P(2001,2)*P(2003,1) + Metric(1001,1002)*Metric(1003,1004)*Metric(2002,2004)*P(2001,2)*P(2003,1) + Metric(1003,1004)*Metric(2002,1001)*Metric(2004,1002)*P(2001,2)*P(2003,1) - 2*Metric(1001,1003)*Metric(2002,1004)*Metric(2004,1002)*P(2001,2)*P(2003,1) + Metric(1002,1004)*Metric(2002,1001)*Metric(2004,1003)*P(2001,2)*P(2003,1) + Metric(1001,1002)*Metric(2002,1004)*Metric(2004,1003)*P(2001,2)*P(2003,1) + Metric(1002,1003)*Metric(2002,1001)*Metric(2004,1004)*P(2001,2)*P(2003,1) + Metric(1001,1002)*Metric(2002,1003)*Metric(2004,1004)*P(2001,2)*P(2003,1) + Metric(1001,1004)*Metric(1002,1003)*Metric(2002,2004)*P(2001,3)*P(2003,1) - 4*Metric(1001,1003)*Metric(1002,1004)*Metric(2002,2004)*P(2001,3)*P(2003,1) + Metric(1001,1002)*Metric(1003,1004)*Metric(2002,2004)*P(2001,3)*P(2003,1) + Metric(1002,1004)*Metric(2002,1003)*Metric(2004,1001)*P(2001,3)*P(2003,1) + Metric(1002,1003)*Metric(2002,1004)*Metric(2004,1001)*P(2001,3)*P(2003,1) + Metric(1003,1004)*Metric(2002,1001)*Metric(2004,1002)*P(2001,3)*P(2003,1) + Metric(1001,1004)*Metric(2002,1003)*Metric(2004,1002)*P(2001,3)*P(2003,1) - 4*Metric(1001,1003)*Metric(2002,1004)*Metric(2004,1002)*P(2001,3)*P(2003,1) + Metric(1002,1004)*Metric(2002,1001)*Metric(2004,1003)*P(2001,3)*P(2003,1) + Metric(1001,1002)*Metric(2002,1004)*Metric(2004,1003)*P(2001,3)*P(2003,1) + Metric(1001,1004)*Metric(1002,1003)*Metric(2002,2004)*P(2001,4)*P(2003,1) - 2*Metric(1001,1003)*Metric(1002,1004)*Metric(2002,2004)*P(2001,4)*P(2003,1) + Metric(1003,1004)*Metric(2002,1002)*Metric(2004,1001)*P(2001,4)*P(2003,1) + Metric(1002,1004)*Metric(2002,1003)*Metric(2004,1001)*P(2001,4)*P(2003,1) + Metric(1002,1003)*Metric(2002,1004)*Metric(2004,1001)*P(2001,4)*P(2003,1) + Metric(1001,1004)*Metric(2002,1003)*Metric(2004,1002)*P(2001,4)*P(2003,1) - 2*Metric(1001,1003)*Metric(2002,1004)*Metric(2004,1002)*P(2001,4)*P(2003,1) + Metric(1001,1004)*Metric(2002,1002)*Metric(2004,1003)*P(2001,4)*P(2003,1) - 2*Metric(1001,1004)*Metric(1002,1003)*Metric(2001,2004)*P(2002,3)*P(2003,1) + Metric(1001,1003)*Metric(1002,1004)*Metric(2001,2004)*P(2002,3)*P(2003,1) + Metric(1002,1004)*Metric(2001,1003)*Metric(2004,1001)*P(2002,3)*P(2003,1) - 2*Metric(1002,1003)*Metric(2001,1004)*Metric(2004,1001)*P(2002,3)*P(2003,1) + Metric(1001,1004)*Metric(2001,1003)*Metric(2004,1002)*P(2002,3)*P(2003,1) + Metric(1001,1003)*Metric(2001,1004)*Metric(2004,1002)*P(2002,3)*P(2003,1) + Metric(1001,1003)*Metric(2001,1002)*Metric(2004,1004)*P(2002,3)*P(2003,1) + Metric(1001,1002)*Metric(2001,1003)*Metric(2004,1004)*P(2002,3)*P(2003,1) - 2*Metric(1001,1004)*Metric(1002,1003)*Metric(2001,2004)*P(2002,4)*P(2003,1) + Metric(1001,1002)*Metric(1003,1004)*Metric(2001,2004)*P(2002,4)*P(2003,1) + Metric(1003,1004)*Metric(2001,1002)*Metric(2004,1001)*P(2002,4)*P(2003,1) - 2*Metric(1002,1003)*Metric(2001,1004)*Metric(2004,1001)*P(2002,4)*P(2003,1) + Metric(1001,1004)*Metric(2001,1002)*Metric(2004,1003)*P(2002,4)*P(2003,1) + Metric(1001,1002)*Metric(2001,1004)*Metric(2004,1003)*P(2002,4)*P(2003,1) - 2*Metric(1002,1003)*Metric(2001,1001)*Metric(2004,1004)*P(2002,4)*P(2003,1) + Metric(1002,1004)*Metric(2001,2004)*Metric(2002,1003)*P(1001,3)*P(2003,2) + Metric(1002,1003)*Metric(2001,2004)*Metric(2002,1004)*P(1001,3)*P(2003,2) - 2*Metric(1002,1004)*Metric(2001,1003)*Metric(2002,2004)*P(1001,3)*P(2003,2) + Metric(1002,1003)*Metric(2001,1004)*Metric(2002,2004)*P(1001,3)*P(2003,2) + Metric(2001,1004)*Metric(2002,1003)*Metric(2004,1002)*P(1001,3)*P(2003,2) - 2*Metric(2001,1003)*Metric(2002,1004)*Metric(2004,1002)*P(1001,3)*P(2003,2) + Metric(1002,1003)*Metric(2001,2002)*Metric(2004,1004)*P(1001,3)*P(2003,2) + Metric(2001,1002)*Metric(2002,1003)*Metric(2004,1004)*P(1001,3)*P(2003,2) + Metric(1003,1004)*Metric(2001,1002)*Metric(2002,2004)*P(1001,4)*P(2003,2) - 2*Metric(1002,1004)*Metric(2001,1003)*Metric(2002,2004)*P(1001,4)*P(2003,2) + Metric(1003,1004)*Metric(2001,2002)*Metric(2004,1002)*P(1001,4)*P(2003,2) - 2*Metric(2001,1003)*Metric(2002,1004)*Metric(2004,1002)*P(1001,4)*P(2003,2) + Metric(1002,1004)*Metric(2001,2002)*Metric(2004,1003)*P(1001,4)*P(2003,2) + Metric(2001,1002)*Metric(2002,1004)*Metric(2004,1003)*P(1001,4)*P(2003,2) - 2*Metric(2001,1003)*Metric(2002,1002)*Metric(2004,1004)*P(1001,4)*P(2003,2) + Metric(1003,1004)*Metric(2001,2004)*Metric(2002,1001)*P(1002,1)*P(2003,2) - 2*Metric(1001,1004)*Metric(2001,2004)*Metric(2002,1003)*P(1002,1)*P(2003,2) + Metric(1003,1004)*Metric(2001,2002)*Metric(2004,1001)*P(1002,1)*P(2003,2) - 2*Metric(2001,1004)*Metric(2002,1003)*Metric(2004,1001)*P(1002,1)*P(2003,2) + Metric(1001,1004)*Metric(2001,2002)*Metric(2004,1003)*P(1002,1)*P(2003,2) + Metric(2001,1004)*Metric(2002,1001)*Metric(2004,1003)*P(1002,1)*P(2003,2) + Metric(1001,1003)*Metric(2001,2002)*Metric(2004,1004)*P(1002,1)*P(2003,2) + Metric(2001,1003)*Metric(2002,1001)*Metric(2004,1004)*P(1002,1)*P(2003,2) + Metric(1003,1004)*Metric(2001,2004)*Metric(2002,1001)*P(1002,3)*P(2003,2) - 4*Metric(1001,1004)*Metric(2001,2004)*Metric(2002,1003)*P(1002,3)*P(2003,2) + Metric(1001,1003)*Metric(2001,2004)*Metric(2002,1004)*P(1002,3)*P(2003,2) + Metric(1001,1004)*Metric(2001,1003)*Metric(2002,2004)*P(1002,3)*P(2003,2) + Metric(1001,1003)*Metric(2001,1004)*Metric(2002,2004)*P(1002,3)*P(2003,2) + Metric(1003,1004)*Metric(2001,2002)*Metric(2004,1001)*P(1002,3)*P(2003,2) - 4*Metric(2001,1004)*Metric(2002,1003)*Metric(2004,1001)*P(1002,3)*P(2003,2) + Metric(2001,1003)*Metric(2002,1004)*Metric(2004,1001)*P(1002,3)*P(2003,2) + Metric(1001,1004)*Metric(2001,2002)*Metric(2004,1003)*P(1002,3)*P(2003,2) + Metric(2001,1004)*Metric(2002,1001)*Metric(2004,1003)*P(1002,3)*P(2003,2) - 2*Metric(1001,1004)*Metric(2001,2004)*Metric(2002,1003)*P(1002,4)*P(2003,2) + Metric(1001,1003)*Metric(2001,2004)*Metric(2002,1004)*P(1002,4)*P(2003,2) + Metric(1003,1004)*Metric(2001,1001)*Metric(2002,2004)*P(1002,4)*P(2003,2) + Metric(1001,1004)*Metric(2001,1003)*Metric(2002,2004)*P(1002,4)*P(2003,2) + Metric(1001,1003)*Metric(2001,1004)*Metric(2002,2004)*P(1002,4)*P(2003,2) - 2*Metric(2001,1004)*Metric(2002,1003)*Metric(2004,1001)*P(1002,4)*P(2003,2) + Metric(2001,1003)*Metric(2002,1004)*Metric(2004,1001)*P(1002,4)*P(2003,2) + Metric(2001,1001)*Metric(2002,1004)*Metric(2004,1003)*P(1002,4)*P(2003,2) - 2*Metric(1001,1002)*Metric(2001,2002)*Metric(2004,1004)*P(1003,1)*P(2003,2) - 2*Metric(2001,1002)*Metric(2002,1001)*Metric(2004,1004)*P(1003,1)*P(2003,2) - 2*Metric(1002,1004)*Metric(2001,1001)*Metric(2002,2004)*P(1003,4)*P(2003,2) - 2*Metric(2001,1001)*Metric(2002,1004)*Metric(2004,1002)*P(1003,4)*P(2003,2) + Metric(1001,1003)*Metric(2001,1002)*Metric(2002,2004)*P(1004,1)*P(2003,2) + Metric(1001,1002)*Metric(2001,1003)*Metric(2002,2004)*P(1004,1)*P(2003,2) + Metric(1001,1003)*Metric(2001,2002)*Metric(2004,1002)*P(1004,1)*P(2003,2) + Metric(2001,1003)*Metric(2002,1001)*Metric(2004,1002)*P(1004,1)*P(2003,2) - 2*Metric(1001,1002)*Metric(2001,2002)*Metric(2004,1003)*P(1004,1)*P(2003,2) - 2*Metric(2001,1002)*Metric(2002,1001)*Metric(2004,1003)*P(1004,1)*P(2003,2) - 2*Metric(2001,1001)*Metric(2002,1002)*Metric(2004,1003)*P(1004,1)*P(2003,2) + Metric(1002,1003)*Metric(2001,2004)*Metric(2002,1001)*P(1004,3)*P(2003,2) + Metric(1001,1002)*Metric(2001,2004)*Metric(2002,1003)*P(1004,3)*P(2003,2) + Metric(1002,1003)*Metric(2001,1001)*Metric(2002,2004)*P(1004,3)*P(2003,2) + Metric(1002,1003)*Metric(2001,2002)*Metric(2004,1001)*P(1004,3)*P(2003,2) + Metric(2001,1002)*Metric(2002,1003)*Metric(2004,1001)*P(1004,3)*P(2003,2) + Metric(2001,1001)*Metric(2002,1003)*Metric(2004,1002)*P(1004,3)*P(2003,2) - 2*Metric(1001,1002)*Metric(2001,2002)*Metric(2004,1003)*P(1004,3)*P(2003,2) - 2*Metric(2001,1002)*Metric(2002,1001)*Metric(2004,1003)*P(1004,3)*P(2003,2) + Metric(1001,1004)*Metric(1002,1003)*Metric(2002,2004)*P(2001,3)*P(2003,2) - 2*Metric(1001,1003)*Metric(1002,1004)*Metric(2002,2004)*P(2001,3)*P(2003,2) + Metric(1002,1004)*Metric(2002,1003)*Metric(2004,1001)*P(2001,3)*P(2003,2) + Metric(1002,1003)*Metric(2002,1004)*Metric(2004,1001)*P(2001,3)*P(2003,2) + Metric(1001,1004)*Metric(2002,1003)*Metric(2004,1002)*P(2001,3)*P(2003,2) - 2*Metric(1001,1003)*Metric(2002,1004)*Metric(2004,1002)*P(2001,3)*P(2003,2) + Metric(1002,1003)*Metric(2002,1001)*Metric(2004,1004)*P(2001,3)*P(2003,2) + Metric(1001,1002)*Metric(2002,1003)*Metric(2004,1004)*P(2001,3)*P(2003,2) - 2*Metric(1001,1003)*Metric(1002,1004)*Metric(2002,2004)*P(2001,4)*P(2003,2) + Metric(1001,1002)*Metric(1003,1004)*Metric(2002,2004)*P(2001,4)*P(2003,2) + Metric(1003,1004)*Metric(2002,1001)*Metric(2004,1002)*P(2001,4)*P(2003,2) - 2*Metric(1001,1003)*Metric(2002,1004)*Metric(2004,1002)*P(2001,4)*P(2003,2) + Metric(1002,1004)*Metric(2002,1001)*Metric(2004,1003)*P(2001,4)*P(2003,2) + Metric(1001,1002)*Metric(2002,1004)*Metric(2004,1003)*P(2001,4)*P(2003,2) - 2*Metric(1001,1003)*Metric(2002,1002)*Metric(2004,1004)*P(2001,4)*P(2003,2) - 2*Metric(1001,1004)*Metric(1002,1003)*Metric(2001,2004)*P(2002,1)*P(2003,2) + Metric(1001,1002)*Metric(1003,1004)*Metric(2001,2004)*P(2002,1)*P(2003,2) + Metric(1003,1004)*Metric(2001,1002)*Metric(2004,1001)*P(2002,1)*P(2003,2) - 2*Metric(1002,1003)*Metric(2001,1004)*Metric(2004,1001)*P(2002,1)*P(2003,2) + Metric(1001,1004)*Metric(2001,1002)*Metric(2004,1003)*P(2002,1)*P(2003,2) + Metric(1001,1002)*Metric(2001,1004)*Metric(2004,1003)*P(2002,1)*P(2003,2) + Metric(1001,1003)*Metric(2001,1002)*Metric(2004,1004)*P(2002,1)*P(2003,2) + Metric(1001,1002)*Metric(2001,1003)*Metric(2004,1004)*P(2002,1)*P(2003,2) - 4*Metric(1001,1004)*Metric(1002,1003)*Metric(2001,2004)*P(2002,3)*P(2003,2) + Metric(1001,1003)*Metric(1002,1004)*Metric(2001,2004)*P(2002,3)*P(2003,2) + Metric(1001,1002)*Metric(1003,1004)*Metric(2001,2004)*P(2002,3)*P(2003,2) + Metric(1003,1004)*Metric(2001,1002)*Metric(2004,1001)*P(2002,3)*P(2003,2) + Metric(1002,1004)*Metric(2001,1003)*Metric(2004,1001)*P(2002,3)*P(2003,2) - 4*Metric(1002,1003)*Metric(2001,1004)*Metric(2004,1001)*P(2002,3)*P(2003,2) + Metric(1001,1004)*Metric(2001,1003)*Metric(2004,1002)*P(2002,3)*P(2003,2) + Metric(1001,1003)*Metric(2001,1004)*Metric(2004,1002)*P(2002,3)*P(2003,2) + Metric(1001,1004)*Metric(2001,1002)*Metric(2004,1003)*P(2002,3)*P(2003,2) + Metric(1001,1002)*Metric(2001,1004)*Metric(2004,1003)*P(2002,3)*P(2003,2) - 2*Metric(1001,1004)*Metric(1002,1003)*Metric(2001,2004)*P(2002,4)*P(2003,2) + Metric(1001,1003)*Metric(1002,1004)*Metric(2001,2004)*P(2002,4)*P(2003,2) + Metric(1002,1004)*Metric(2001,1003)*Metric(2004,1001)*P(2002,4)*P(2003,2) - 2*Metric(1002,1003)*Metric(2001,1004)*Metric(2004,1001)*P(2002,4)*P(2003,2) + Metric(1003,1004)*Metric(2001,1001)*Metric(2004,1002)*P(2002,4)*P(2003,2) + Metric(1001,1004)*Metric(2001,1003)*Metric(2004,1002)*P(2002,4)*P(2003,2) + Metric(1001,1003)*Metric(2001,1004)*Metric(2004,1002)*P(2002,4)*P(2003,2) + Metric(1002,1004)*Metric(2001,1001)*Metric(2004,1003)*P(2002,4)*P(2003,2) + Metric(1002,1004)*Metric(2001,2004)*Metric(2002,1003)*P(1001,2)*P(2003,4) + Metric(1002,1003)*Metric(2001,2004)*Metric(2002,1004)*P(1001,2)*P(2003,4) - 2*Metric(1002,1004)*Metric(2001,1003)*Metric(2002,2004)*P(1001,2)*P(2003,4) + Metric(1002,1003)*Metric(2001,1004)*Metric(2002,2004)*P(1001,2)*P(2003,4) + Metric(2001,1004)*Metric(2002,1003)*Metric(2004,1002)*P(1001,2)*P(2003,4) - 2*Metric(2001,1003)*Metric(2002,1004)*Metric(2004,1002)*P(1001,2)*P(2003,4) - 2*Metric(2001,1003)*Metric(2002,1002)*Metric(2004,1004)*P(1001,2)*P(2003,4) + Metric(1003,1004)*Metric(2001,2004)*Metric(2002,1002)*P(1001,3)*P(2003,4) + Metric(1003,1004)*Metric(2001,1002)*Metric(2002,2004)*P(1001,3)*P(2003,4) - 2*Metric(1002,1004)*Metric(2001,1003)*Metric(2002,2004)*P(1001,3)*P(2003,4) + Metric(1003,1004)*Metric(2001,2002)*Metric(2004,1002)*P(1001,3)*P(2003,4) - 2*Metric(2001,1003)*Metric(2002,1004)*Metric(2004,1002)*P(1001,3)*P(2003,4) + Metric(1002,1004)*Metric(2001,2002)*Metric(2004,1003)*P(1001,3)*P(2003,4) + Metric(2001,1004)*Metric(2002,1002)*Metric(2004,1003)*P(1001,3)*P(2003,4) + Metric(2001,1002)*Metric(2002,1004)*Metric(2004,1003)*P(1001,3)*P(2003,4) - 2*Metric(1001,1004)*Metric(2001,2004)*Metric(2002,1003)*P(1002,1)*P(2003,4) + Metric(1001,1003)*Metric(2001,2004)*Metric(2002,1004)*P(1002,1)*P(2003,4) + Metric(1001,1004)*Metric(2001,1003)*Metric(2002,2004)*P(1002,1)*P(2003,4) + Metric(1001,1003)*Metric(2001,1004)*Metric(2002,2004)*P(1002,1)*P(2003,4) - 2*Metric(2001,1004)*Metric(2002,1003)*Metric(2004,1001)*P(1002,1)*P(2003,4) + Metric(2001,1003)*Metric(2002,1004)*Metric(2004,1001)*P(1002,1)*P(2003,4) - 2*Metric(2001,1001)*Metric(2002,1003)*Metric(2004,1004)*P(1002,1)*P(2003,4) + Metric(1003,1004)*Metric(2001,2004)*Metric(2002,1001)*P(1002,3)*P(2003,4) - 2*Metric(1001,1004)*Metric(2001,2004)*Metric(2002,1003)*P(1002,3)*P(2003,4) + Metric(1003,1004)*Metric(2001,1001)*Metric(2002,2004)*P(1002,3)*P(2003,4) + Metric(1003,1004)*Metric(2001,2002)*Metric(2004,1001)*P(1002,3)*P(2003,4) - 2*Metric(2001,1004)*Metric(2002,1003)*Metric(2004,1001)*P(1002,3)*P(2003,4) + Metric(1001,1004)*Metric(2001,2002)*Metric(2004,1003)*P(1002,3)*P(2003,4) + Metric(2001,1004)*Metric(2002,1001)*Metric(2004,1003)*P(1002,3)*P(2003,4) + Metric(2001,1001)*Metric(2002,1004)*Metric(2004,1003)*P(1002,3)*P(2003,4) - 2*Metric(1001,1004)*Metric(2001,2004)*Metric(2002,1002)*P(1003,1)*P(2003,4) - 2*Metric(2001,1004)*Metric(2002,1002)*Metric(2004,1001)*P(1003,1)*P(2003,4) - 2*Metric(1002,1004)*Metric(2001,1001)*Metric(2002,2004)*P(1003,2)*P(2003,4) - 2*Metric(2001,1001)*Metric(2002,1004)*Metric(2004,1002)*P(1003,2)*P(2003,4) + Metric(1002,1003)*Metric(2001,2004)*Metric(2002,1001)*P(1004,1)*P(2003,4) + Metric(1001,1003)*Metric(2001,2004)*Metric(2002,1002)*P(1004,1)*P(2003,4) + Metric(1001,1002)*Metric(2001,2004)*Metric(2002,1003)*P(1004,1)*P(2003,4) + Metric(1002,1003)*Metric(2001,2002)*Metric(2004,1001)*P(1004,1)*P(2003,4) + Metric(2001,1003)*Metric(2002,1002)*Metric(2004,1001)*P(1004,1)*P(2003,4) + Metric(2001,1002)*Metric(2002,1003)*Metric(2004,1001)*P(1004,1)*P(2003,4) - 2*Metric(1001,1002)*Metric(2001,2002)*Metric(2004,1003)*P(1004,1)*P(2003,4) - 2*Metric(2001,1002)*Metric(2002,1001)*Metric(2004,1003)*P(1004,1)*P(2003,4) + Metric(1002,1003)*Metric(2001,1001)*Metric(2002,2004)*P(1004,2)*P(2003,4) + Metric(1001,1003)*Metric(2001,1002)*Metric(2002,2004)*P(1004,2)*P(2003,4) + Metric(1001,1002)*Metric(2001,1003)*Metric(2002,2004)*P(1004,2)*P(2003,4) + Metric(1001,1003)*Metric(2001,2002)*Metric(2004,1002)*P(1004,2)*P(2003,4) + Metric(2001,1003)*Metric(2002,1001)*Metric(2004,1002)*P(1004,2)*P(2003,4) + Metric(2001,1001)*Metric(2002,1003)*Metric(2004,1002)*P(1004,2)*P(2003,4) - 2*Metric(1001,1002)*Metric(2001,2002)*Metric(2004,1003)*P(1004,2)*P(2003,4) - 2*Metric(2001,1002)*Metric(2002,1001)*Metric(2004,1003)*P(1004,2)*P(2003,4) + Metric(1002,1003)*Metric(2001,2004)*Metric(2002,1001)*P(1004,3)*P(2003,4) + Metric(1001,1002)*Metric(2001,2004)*Metric(2002,1003)*P(1004,3)*P(2003,4) + Metric(1001,1003)*Metric(2001,1002)*Metric(2002,2004)*P(1004,3)*P(2003,4) + Metric(1001,1002)*Metric(2001,1003)*Metric(2002,2004)*P(1004,3)*P(2003,4) + Metric(1002,1003)*Metric(2001,2002)*Metric(2004,1001)*P(1004,3)*P(2003,4) + Metric(2001,1002)*Metric(2002,1003)*Metric(2004,1001)*P(1004,3)*P(2003,4) + Metric(1001,1003)*Metric(2001,2002)*Metric(2004,1002)*P(1004,3)*P(2003,4) + Metric(2001,1003)*Metric(2002,1001)*Metric(2004,1002)*P(1004,3)*P(2003,4) - 4*Metric(1001,1002)*Metric(2001,2002)*Metric(2004,1003)*P(1004,3)*P(2003,4) - 4*Metric(2001,1002)*Metric(2002,1001)*Metric(2004,1003)*P(1004,3)*P(2003,4) + Metric(1001,1004)*Metric(1002,1003)*Metric(2002,2004)*P(2001,2)*P(2003,4) - 2*Metric(1001,1003)*Metric(1002,1004)*Metric(2002,2004)*P(2001,2)*P(2003,4) + Metric(1002,1004)*Metric(2002,1003)*Metric(2004,1001)*P(2001,2)*P(2003,4) + Metric(1002,1003)*Metric(2002,1004)*Metric(2004,1001)*P(2001,2)*P(2003,4) + Metric(1001,1004)*Metric(2002,1003)*Metric(2004,1002)*P(2001,2)*P(2003,4) - 2*Metric(1001,1003)*Metric(2002,1004)*Metric(2004,1002)*P(2001,2)*P(2003,4) - 2*Metric(1001,1003)*Metric(2002,1002)*Metric(2004,1004)*P(2001,2)*P(2003,4) - 2*Metric(1001,1003)*Metric(1002,1004)*Metric(2002,2004)*P(2001,3)*P(2003,4) + Metric(1001,1002)*Metric(1003,1004)*Metric(2002,2004)*P(2001,3)*P(2003,4) + Metric(1003,1004)*Metric(2002,1002)*Metric(2004,1001)*P(2001,3)*P(2003,4) + Metric(1003,1004)*Metric(2002,1001)*Metric(2004,1002)*P(2001,3)*P(2003,4) - 2*Metric(1001,1003)*Metric(2002,1004)*Metric(2004,1002)*P(2001,3)*P(2003,4) + Metric(1002,1004)*Metric(2002,1001)*Metric(2004,1003)*P(2001,3)*P(2003,4) + Metric(1001,1004)*Metric(2002,1002)*Metric(2004,1003)*P(2001,3)*P(2003,4) + Metric(1001,1002)*Metric(2002,1004)*Metric(2004,1003)*P(2001,3)*P(2003,4) - 2*Metric(1001,1004)*Metric(1002,1003)*Metric(2001,2004)*P(2002,1)*P(2003,4) + Metric(1001,1003)*Metric(1002,1004)*Metric(2001,2004)*P(2002,1)*P(2003,4) + Metric(1002,1004)*Metric(2001,1003)*Metric(2004,1001)*P(2002,1)*P(2003,4) - 2*Metric(1002,1003)*Metric(2001,1004)*Metric(2004,1001)*P(2002,1)*P(2003,4) + Metric(1001,1004)*Metric(2001,1003)*Metric(2004,1002)*P(2002,1)*P(2003,4) + Metric(1001,1003)*Metric(2001,1004)*Metric(2004,1002)*P(2002,1)*P(2003,4) - 2*Metric(1002,1003)*Metric(2001,1001)*Metric(2004,1004)*P(2002,1)*P(2003,4) - 2*Metric(1001,1004)*Metric(1002,1003)*Metric(2001,2004)*P(2002,3)*P(2003,4) + Metric(1001,1002)*Metric(1003,1004)*Metric(2001,2004)*P(2002,3)*P(2003,4) + Metric(1003,1004)*Metric(2001,1002)*Metric(2004,1001)*P(2002,3)*P(2003,4) - 2*Metric(1002,1003)*Metric(2001,1004)*Metric(2004,1001)*P(2002,3)*P(2003,4) + Metric(1003,1004)*Metric(2001,1001)*Metric(2004,1002)*P(2002,3)*P(2003,4) + Metric(1002,1004)*Metric(2001,1001)*Metric(2004,1003)*P(2002,3)*P(2003,4) + Metric(1001,1004)*Metric(2001,1002)*Metric(2004,1003)*P(2002,3)*P(2003,4) + Metric(1001,1002)*Metric(2001,1004)*Metric(2004,1003)*P(2002,3)*P(2003,4) + Metric(1003,1004)*Metric(2001,1002)*Metric(2002,2003)*P(1001,2)*P(2004,1) - 2*Metric(1002,1003)*Metric(2001,1004)*Metric(2002,2003)*P(1001,2)*P(2004,1) + Metric(1003,1004)*Metric(2001,2002)*Metric(2003,1002)*P(1001,2)*P(2004,1) - 2*Metric(2001,1004)*Metric(2002,1003)*Metric(2003,1002)*P(1001,2)*P(2004,1) + Metric(1002,1004)*Metric(2001,2002)*Metric(2003,1003)*P(1001,2)*P(2004,1) + Metric(2001,1002)*Metric(2002,1004)*Metric(2003,1003)*P(1001,2)*P(2004,1) + Metric(1002,1003)*Metric(2001,2002)*Metric(2003,1004)*P(1001,2)*P(2004,1) + Metric(2001,1002)*Metric(2002,1003)*Metric(2003,1004)*P(1001,2)*P(2004,1) + Metric(1003,1004)*Metric(2001,2003)*Metric(2002,1002)*P(1001,3)*P(2004,1) + Metric(1002,1004)*Metric(2001,2003)*Metric(2002,1003)*P(1001,3)*P(2004,1) + Metric(1002,1003)*Metric(2001,2003)*Metric(2002,1004)*P(1001,3)*P(2004,1) + Metric(1002,1004)*Metric(2001,1003)*Metric(2002,2003)*P(1001,3)*P(2004,1) - 2*Metric(1002,1003)*Metric(2001,1004)*Metric(2002,2003)*P(1001,3)*P(2004,1) - 2*Metric(2001,1004)*Metric(2002,1003)*Metric(2003,1002)*P(1001,3)*P(2004,1) + Metric(2001,1003)*Metric(2002,1004)*Metric(2003,1002)*P(1001,3)*P(2004,1) + Metric(2001,1003)*Metric(2002,1002)*Metric(2003,1004)*P(1001,3)*P(2004,1) + Metric(1002,1004)*Metric(2001,2003)*Metric(2002,1003)*P(1001,4)*P(2004,1) + Metric(1002,1003)*Metric(2001,2003)*Metric(2002,1004)*P(1001,4)*P(2004,1) + Metric(1003,1004)*Metric(2001,1002)*Metric(2002,2003)*P(1001,4)*P(2004,1) + Metric(1002,1004)*Metric(2001,1003)*Metric(2002,2003)*P(1001,4)*P(2004,1) - 4*Metric(1002,1003)*Metric(2001,1004)*Metric(2002,2003)*P(1001,4)*P(2004,1) + Metric(1003,1004)*Metric(2001,2002)*Metric(2003,1002)*P(1001,4)*P(2004,1) - 4*Metric(2001,1004)*Metric(2002,1003)*Metric(2003,1002)*P(1001,4)*P(2004,1) + Metric(2001,1003)*Metric(2002,1004)*Metric(2003,1002)*P(1001,4)*P(2004,1) + Metric(1002,1003)*Metric(2001,2002)*Metric(2003,1004)*P(1001,4)*P(2004,1) + Metric(2001,1002)*Metric(2002,1003)*Metric(2003,1004)*P(1001,4)*P(2004,1) + Metric(1003,1004)*Metric(2001,2003)*Metric(2002,1001)*P(1002,3)*P(2004,1) - 2*Metric(1001,1003)*Metric(2001,2003)*Metric(2002,1004)*P(1002,3)*P(2004,1) + Metric(1003,1004)*Metric(2001,2002)*Metric(2003,1001)*P(1002,3)*P(2004,1) - 2*Metric(2001,1003)*Metric(2002,1004)*Metric(2003,1001)*P(1002,3)*P(2004,1) - 2*Metric(2001,1001)*Metric(2002,1004)*Metric(2003,1003)*P(1002,3)*P(2004,1) + Metric(1001,1003)*Metric(2001,2002)*Metric(2003,1004)*P(1002,3)*P(2004,1) + Metric(2001,1003)*Metric(2002,1001)*Metric(2003,1004)*P(1002,3)*P(2004,1) + Metric(1001,1004)*Metric(2001,2003)*Metric(2002,1003)*P(1002,4)*P(2004,1) - 2*Metric(1001,1003)*Metric(2001,2003)*Metric(2002,1004)*P(1002,4)*P(2004,1) + Metric(1001,1004)*Metric(2001,1003)*Metric(2002,2003)*P(1002,4)*P(2004,1) + Metric(1001,1003)*Metric(2001,1004)*Metric(2002,2003)*P(1002,4)*P(2004,1) + Metric(2001,1004)*Metric(2002,1003)*Metric(2003,1001)*P(1002,4)*P(2004,1) - 2*Metric(2001,1003)*Metric(2002,1004)*Metric(2003,1001)*P(1002,4)*P(2004,1) + Metric(1001,1004)*Metric(2001,2002)*Metric(2003,1003)*P(1002,4)*P(2004,1) + Metric(2001,1004)*Metric(2002,1001)*Metric(2003,1003)*P(1002,4)*P(2004,1) + Metric(1002,1004)*Metric(2001,2003)*Metric(2002,1001)*P(1003,2)*P(2004,1) + Metric(1001,1002)*Metric(2001,2003)*Metric(2002,1004)*P(1003,2)*P(2004,1) + Metric(1002,1004)*Metric(2001,2002)*Metric(2003,1001)*P(1003,2)*P(2004,1) + Metric(2001,1002)*Metric(2002,1004)*Metric(2003,1001)*P(1003,2)*P(2004,1) - 2*Metric(1001,1002)*Metric(2001,2002)*Metric(2003,1004)*P(1003,2)*P(2004,1) - 2*Metric(2001,1002)*Metric(2002,1001)*Metric(2003,1004)*P(1003,2)*P(2004,1) - 2*Metric(2001,1001)*Metric(2002,1002)*Metric(2003,1004)*P(1003,2)*P(2004,1) + Metric(1001,1004)*Metric(2001,2003)*Metric(2002,1002)*P(1003,4)*P(2004,1) + Metric(1001,1004)*Metric(2001,1002)*Metric(2002,2003)*P(1003,4)*P(2004,1) + Metric(1001,1002)*Metric(2001,1004)*Metric(2002,2003)*P(1003,4)*P(2004,1) + Metric(2001,1004)*Metric(2002,1002)*Metric(2003,1001)*P(1003,4)*P(2004,1) + Metric(1001,1004)*Metric(2001,2002)*Metric(2003,1002)*P(1003,4)*P(2004,1) + Metric(2001,1004)*Metric(2002,1001)*Metric(2003,1002)*P(1003,4)*P(2004,1) - 2*Metric(1001,1002)*Metric(2001,2002)*Metric(2003,1004)*P(1003,4)*P(2004,1) - 2*Metric(2001,1002)*Metric(2002,1001)*Metric(2003,1004)*P(1003,4)*P(2004,1) - 2*Metric(1001,1002)*Metric(2001,2002)*Metric(2003,1003)*P(1004,2)*P(2004,1) - 2*Metric(2001,1002)*Metric(2002,1001)*Metric(2003,1003)*P(1004,2)*P(2004,1) - 2*Metric(1001,1003)*Metric(2001,2003)*Metric(2002,1002)*P(1004,3)*P(2004,1) - 2*Metric(2001,1003)*Metric(2002,1002)*Metric(2003,1001)*P(1004,3)*P(2004,1) - 2*Metric(1001,1004)*Metric(1002,1003)*Metric(2002,2003)*P(2001,2)*P(2004,1) + Metric(1001,1002)*Metric(1003,1004)*Metric(2002,2003)*P(2001,2)*P(2004,1) + Metric(1003,1004)*Metric(2002,1001)*Metric(2003,1002)*P(2001,2)*P(2004,1) - 2*Metric(1001,1004)*Metric(2002,1003)*Metric(2003,1002)*P(2001,2)*P(2004,1) + Metric(1002,1004)*Metric(2002,1001)*Metric(2003,1003)*P(2001,2)*P(2004,1) + Metric(1001,1002)*Metric(2002,1004)*Metric(2003,1003)*P(2001,2)*P(2004,1) + Metric(1002,1003)*Metric(2002,1001)*Metric(2003,1004)*P(2001,2)*P(2004,1) + Metric(1001,1002)*Metric(2002,1003)*Metric(2003,1004)*P(2001,2)*P(2004,1) - 2*Metric(1001,1004)*Metric(1002,1003)*Metric(2002,2003)*P(2001,3)*P(2004,1) + Metric(1001,1003)*Metric(1002,1004)*Metric(2002,2003)*P(2001,3)*P(2004,1) + Metric(1003,1004)*Metric(2002,1002)*Metric(2003,1001)*P(2001,3)*P(2004,1) + Metric(1002,1004)*Metric(2002,1003)*Metric(2003,1001)*P(2001,3)*P(2004,1) + Metric(1002,1003)*Metric(2002,1004)*Metric(2003,1001)*P(2001,3)*P(2004,1) - 2*Metric(1001,1004)*Metric(2002,1003)*Metric(2003,1002)*P(2001,3)*P(2004,1) + Metric(1001,1003)*Metric(2002,1004)*Metric(2003,1002)*P(2001,3)*P(2004,1) + Metric(1001,1003)*Metric(2002,1002)*Metric(2003,1004)*P(2001,3)*P(2004,1) - 4*Metric(1001,1004)*Metric(1002,1003)*Metric(2002,2003)*P(2001,4)*P(2004,1) + Metric(1001,1003)*Metric(1002,1004)*Metric(2002,2003)*P(2001,4)*P(2004,1) + Metric(1001,1002)*Metric(1003,1004)*Metric(2002,2003)*P(2001,4)*P(2004,1) + Metric(1002,1004)*Metric(2002,1003)*Metric(2003,1001)*P(2001,4)*P(2004,1) + Metric(1002,1003)*Metric(2002,1004)*Metric(2003,1001)*P(2001,4)*P(2004,1) + Metric(1003,1004)*Metric(2002,1001)*Metric(2003,1002)*P(2001,4)*P(2004,1) - 4*Metric(1001,1004)*Metric(2002,1003)*Metric(2003,1002)*P(2001,4)*P(2004,1) + Metric(1001,1003)*Metric(2002,1004)*Metric(2003,1002)*P(2001,4)*P(2004,1) + Metric(1002,1003)*Metric(2002,1001)*Metric(2003,1004)*P(2001,4)*P(2004,1) + Metric(1001,1002)*Metric(2002,1003)*Metric(2003,1004)*P(2001,4)*P(2004,1) - 2*Metric(1001,1003)*Metric(1002,1004)*Metric(2001,2003)*P(2002,3)*P(2004,1) + Metric(1001,1002)*Metric(1003,1004)*Metric(2001,2003)*P(2002,3)*P(2004,1) + Metric(1003,1004)*Metric(2001,1002)*Metric(2003,1001)*P(2002,3)*P(2004,1) - 2*Metric(1002,1004)*Metric(2001,1003)*Metric(2003,1001)*P(2002,3)*P(2004,1) - 2*Metric(1002,1004)*Metric(2001,1001)*Metric(2003,1003)*P(2002,3)*P(2004,1) + Metric(1001,1003)*Metric(2001,1002)*Metric(2003,1004)*P(2002,3)*P(2004,1) + Metric(1001,1002)*Metric(2001,1003)*Metric(2003,1004)*P(2002,3)*P(2004,1) + Metric(1001,1004)*Metric(1002,1003)*Metric(2001,2003)*P(2002,4)*P(2004,1) - 2*Metric(1001,1003)*Metric(1002,1004)*Metric(2001,2003)*P(2002,4)*P(2004,1) - 2*Metric(1002,1004)*Metric(2001,1003)*Metric(2003,1001)*P(2002,4)*P(2004,1) + Metric(1002,1003)*Metric(2001,1004)*Metric(2003,1001)*P(2002,4)*P(2004,1) + Metric(1001,1004)*Metric(2001,1003)*Metric(2003,1002)*P(2002,4)*P(2004,1) + Metric(1001,1003)*Metric(2001,1004)*Metric(2003,1002)*P(2002,4)*P(2004,1) + Metric(1001,1004)*Metric(2001,1002)*Metric(2003,1003)*P(2002,4)*P(2004,1) + Metric(1001,1002)*Metric(2001,1004)*Metric(2003,1003)*P(2002,4)*P(2004,1) + Metric(1001,1003)*Metric(1002,1004)*Metric(2001,2002)*P(2003,2)*P(2004,1) - 2*Metric(1001,1002)*Metric(1003,1004)*Metric(2001,2002)*P(2003,2)*P(2004,1) - 2*Metric(1003,1004)*Metric(2001,1002)*Metric(2002,1001)*P(2003,2)*P(2004,1) + Metric(1002,1004)*Metric(2001,1003)*Metric(2002,1001)*P(2003,2)*P(2004,1) - 2*Metric(1003,1004)*Metric(2001,1001)*Metric(2002,1002)*P(2003,2)*P(2004,1) + Metric(1001,1003)*Metric(2001,1002)*Metric(2002,1004)*P(2003,2)*P(2004,1) + Metric(1001,1002)*Metric(2001,1003)*Metric(2002,1004)*P(2003,2)*P(2004,1) + Metric(1001,1004)*Metric(1002,1003)*Metric(2001,2002)*P(2003,4)*P(2004,1) - 2*Metric(1001,1002)*Metric(1003,1004)*Metric(2001,2002)*P(2003,4)*P(2004,1) - 2*Metric(1003,1004)*Metric(2001,1002)*Metric(2002,1001)*P(2003,4)*P(2004,1) + Metric(1002,1003)*Metric(2001,1004)*Metric(2002,1001)*P(2003,4)*P(2004,1) + Metric(1001,1004)*Metric(2001,1003)*Metric(2002,1002)*P(2003,4)*P(2004,1) + Metric(1001,1003)*Metric(2001,1004)*Metric(2002,1002)*P(2003,4)*P(2004,1) + Metric(1001,1004)*Metric(2001,1002)*Metric(2002,1003)*P(2003,4)*P(2004,1) + Metric(1001,1002)*Metric(2001,1004)*Metric(2002,1003)*P(2003,4)*P(2004,1) + Metric(1003,1004)*Metric(2001,1002)*Metric(2002,2003)*P(1001,3)*P(2004,2) - 2*Metric(1002,1003)*Metric(2001,1004)*Metric(2002,2003)*P(1001,3)*P(2004,2) + Metric(1003,1004)*Metric(2001,2002)*Metric(2003,1002)*P(1001,3)*P(2004,2) - 2*Metric(2001,1004)*Metric(2002,1003)*Metric(2003,1002)*P(1001,3)*P(2004,2) - 2*Metric(2001,1004)*Metric(2002,1002)*Metric(2003,1003)*P(1001,3)*P(2004,2) + Metric(1002,1003)*Metric(2001,2002)*Metric(2003,1004)*P(1001,3)*P(2004,2) + Metric(2001,1002)*Metric(2002,1003)*Metric(2003,1004)*P(1001,3)*P(2004,2) + Metric(1002,1004)*Metric(2001,2003)*Metric(2002,1003)*P(1001,4)*P(2004,2) + Metric(1002,1003)*Metric(2001,2003)*Metric(2002,1004)*P(1001,4)*P(2004,2) + Metric(1002,1004)*Metric(2001,1003)*Metric(2002,2003)*P(1001,4)*P(2004,2) - 2*Metric(1002,1003)*Metric(2001,1004)*Metric(2002,2003)*P(1001,4)*P(2004,2) - 2*Metric(2001,1004)*Metric(2002,1003)*Metric(2003,1002)*P(1001,4)*P(2004,2) + Metric(2001,1003)*Metric(2002,1004)*Metric(2003,1002)*P(1001,4)*P(2004,2) + Metric(1002,1004)*Metric(2001,2002)*Metric(2003,1003)*P(1001,4)*P(2004,2) + Metric(2001,1002)*Metric(2002,1004)*Metric(2003,1003)*P(1001,4)*P(2004,2) + Metric(1003,1004)*Metric(2001,2003)*Metric(2002,1001)*P(1002,1)*P(2004,2) - 2*Metric(1001,1003)*Metric(2001,2003)*Metric(2002,1004)*P(1002,1)*P(2004,2) + Metric(1003,1004)*Metric(2001,2002)*Metric(2003,1001)*P(1002,1)*P(2004,2) - 2*Metric(2001,1003)*Metric(2002,1004)*Metric(2003,1001)*P(1002,1)*P(2004,2) + Metric(1001,1004)*Metric(2001,2002)*Metric(2003,1003)*P(1002,1)*P(2004,2) + Metric(2001,1004)*Metric(2002,1001)*Metric(2003,1003)*P(1002,1)*P(2004,2) + Metric(1001,1003)*Metric(2001,2002)*Metric(2003,1004)*P(1002,1)*P(2004,2) + Metric(2001,1003)*Metric(2002,1001)*Metric(2003,1004)*P(1002,1)*P(2004,2) + Metric(1001,1004)*Metric(2001,2003)*Metric(2002,1003)*P(1002,3)*P(2004,2) - 2*Metric(1001,1003)*Metric(2001,2003)*Metric(2002,1004)*P(1002,3)*P(2004,2) + Metric(1003,1004)*Metric(2001,1001)*Metric(2002,2003)*P(1002,3)*P(2004,2) + Metric(1001,1004)*Metric(2001,1003)*Metric(2002,2003)*P(1002,3)*P(2004,2) + Metric(1001,1003)*Metric(2001,1004)*Metric(2002,2003)*P(1002,3)*P(2004,2) + Metric(2001,1004)*Metric(2002,1003)*Metric(2003,1001)*P(1002,3)*P(2004,2) - 2*Metric(2001,1003)*Metric(2002,1004)*Metric(2003,1001)*P(1002,3)*P(2004,2) + Metric(2001,1001)*Metric(2002,1003)*Metric(2003,1004)*P(1002,3)*P(2004,2) + Metric(1003,1004)*Metric(2001,2003)*Metric(2002,1001)*P(1002,4)*P(2004,2) + Metric(1001,1004)*Metric(2001,2003)*Metric(2002,1003)*P(1002,4)*P(2004,2) - 4*Metric(1001,1003)*Metric(2001,2003)*Metric(2002,1004)*P(1002,4)*P(2004,2) + Metric(1001,1004)*Metric(2001,1003)*Metric(2002,2003)*P(1002,4)*P(2004,2) + Metric(1001,1003)*Metric(2001,1004)*Metric(2002,2003)*P(1002,4)*P(2004,2) + Metric(1003,1004)*Metric(2001,2002)*Metric(2003,1001)*P(1002,4)*P(2004,2) + Metric(2001,1004)*Metric(2002,1003)*Metric(2003,1001)*P(1002,4)*P(2004,2) - 4*Metric(2001,1003)*Metric(2002,1004)*Metric(2003,1001)*P(1002,4)*P(2004,2) + Metric(1001,1003)*Metric(2001,2002)*Metric(2003,1004)*P(1002,4)*P(2004,2) + Metric(2001,1003)*Metric(2002,1001)*Metric(2003,1004)*P(1002,4)*P(2004,2) + Metric(1001,1004)*Metric(2001,1002)*Metric(2002,2003)*P(1003,1)*P(2004,2) + Metric(1001,1002)*Metric(2001,1004)*Metric(2002,2003)*P(1003,1)*P(2004,2) + Metric(1001,1004)*Metric(2001,2002)*Metric(2003,1002)*P(1003,1)*P(2004,2) + Metric(2001,1004)*Metric(2002,1001)*Metric(2003,1002)*P(1003,1)*P(2004,2) - 2*Metric(1001,1002)*Metric(2001,2002)*Metric(2003,1004)*P(1003,1)*P(2004,2) - 2*Metric(2001,1002)*Metric(2002,1001)*Metric(2003,1004)*P(1003,1)*P(2004,2) - 2*Metric(2001,1001)*Metric(2002,1002)*Metric(2003,1004)*P(1003,1)*P(2004,2) + Metric(1002,1004)*Metric(2001,2003)*Metric(2002,1001)*P(1003,4)*P(2004,2) + Metric(1001,1002)*Metric(2001,2003)*Metric(2002,1004)*P(1003,4)*P(2004,2) + Metric(1002,1004)*Metric(2001,1001)*Metric(2002,2003)*P(1003,4)*P(2004,2) + Metric(1002,1004)*Metric(2001,2002)*Metric(2003,1001)*P(1003,4)*P(2004,2) + Metric(2001,1002)*Metric(2002,1004)*Metric(2003,1001)*P(1003,4)*P(2004,2) + Metric(2001,1001)*Metric(2002,1004)*Metric(2003,1002)*P(1003,4)*P(2004,2) - 2*Metric(1001,1002)*Metric(2001,2002)*Metric(2003,1004)*P(1003,4)*P(2004,2) - 2*Metric(2001,1002)*Metric(2002,1001)*Metric(2003,1004)*P(1003,4)*P(2004,2) - 2*Metric(1001,1002)*Metric(2001,2002)*Metric(2003,1003)*P(1004,1)*P(2004,2) - 2*Metric(2001,1002)*Metric(2002,1001)*Metric(2003,1003)*P(1004,1)*P(2004,2) - 2*Metric(1002,1003)*Metric(2001,1001)*Metric(2002,2003)*P(1004,3)*P(2004,2) - 2*Metric(2001,1001)*Metric(2002,1003)*Metric(2003,1002)*P(1004,3)*P(2004,2) - 2*Metric(1001,1004)*Metric(1002,1003)*Metric(2002,2003)*P(2001,3)*P(2004,2) + Metric(1001,1002)*Metric(1003,1004)*Metric(2002,2003)*P(2001,3)*P(2004,2) + Metric(1003,1004)*Metric(2002,1001)*Metric(2003,1002)*P(2001,3)*P(2004,2) - 2*Metric(1001,1004)*Metric(2002,1003)*Metric(2003,1002)*P(2001,3)*P(2004,2) - 2*Metric(1001,1004)*Metric(2002,1002)*Metric(2003,1003)*P(2001,3)*P(2004,2) + Metric(1002,1003)*Metric(2002,1001)*Metric(2003,1004)*P(2001,3)*P(2004,2) + Metric(1001,1002)*Metric(2002,1003)*Metric(2003,1004)*P(2001,3)*P(2004,2) - 2*Metric(1001,1004)*Metric(1002,1003)*Metric(2002,2003)*P(2001,4)*P(2004,2) + Metric(1001,1003)*Metric(1002,1004)*Metric(2002,2003)*P(2001,4)*P(2004,2) + Metric(1002,1004)*Metric(2002,1003)*Metric(2003,1001)*P(2001,4)*P(2004,2) + Metric(1002,1003)*Metric(2002,1004)*Metric(2003,1001)*P(2001,4)*P(2004,2) - 2*Metric(1001,1004)*Metric(2002,1003)*Metric(2003,1002)*P(2001,4)*P(2004,2) + Metric(1001,1003)*Metric(2002,1004)*Metric(2003,1002)*P(2001,4)*P(2004,2) + Metric(1002,1004)*Metric(2002,1001)*Metric(2003,1003)*P(2001,4)*P(2004,2) + Metric(1001,1002)*Metric(2002,1004)*Metric(2003,1003)*P(2001,4)*P(2004,2) - 2*Metric(1001,1003)*Metric(1002,1004)*Metric(2001,2003)*P(2002,1)*P(2004,2) + Metric(1001,1002)*Metric(1003,1004)*Metric(2001,2003)*P(2002,1)*P(2004,2) + Metric(1003,1004)*Metric(2001,1002)*Metric(2003,1001)*P(2002,1)*P(2004,2) - 2*Metric(1002,1004)*Metric(2001,1003)*Metric(2003,1001)*P(2002,1)*P(2004,2) + Metric(1001,1004)*Metric(2001,1002)*Metric(2003,1003)*P(2002,1)*P(2004,2) + Metric(1001,1002)*Metric(2001,1004)*Metric(2003,1003)*P(2002,1)*P(2004,2) + Metric(1001,1003)*Metric(2001,1002)*Metric(2003,1004)*P(2002,1)*P(2004,2) + Metric(1001,1002)*Metric(2001,1003)*Metric(2003,1004)*P(2002,1)*P(2004,2) + Metric(1001,1004)*Metric(1002,1003)*Metric(2001,2003)*P(2002,3)*P(2004,2) - 2*Metric(1001,1003)*Metric(1002,1004)*Metric(2001,2003)*P(2002,3)*P(2004,2) - 2*Metric(1002,1004)*Metric(2001,1003)*Metric(2003,1001)*P(2002,3)*P(2004,2) + Metric(1002,1003)*Metric(2001,1004)*Metric(2003,1001)*P(2002,3)*P(2004,2) + Metric(1003,1004)*Metric(2001,1001)*Metric(2003,1002)*P(2002,3)*P(2004,2) + Metric(1001,1004)*Metric(2001,1003)*Metric(2003,1002)*P(2002,3)*P(2004,2) + Metric(1001,1003)*Metric(2001,1004)*Metric(2003,1002)*P(2002,3)*P(2004,2) + Metric(1002,1003)*Metric(2001,1001)*Metric(2003,1004)*P(2002,3)*P(2004,2) + Metric(1001,1004)*Metric(1002,1003)*Metric(2001,2003)*P(2002,4)*P(2004,2) - 4*Metric(1001,1003)*Metric(1002,1004)*Metric(2001,2003)*P(2002,4)*P(2004,2) + Metric(1001,1002)*Metric(1003,1004)*Metric(2001,2003)*P(2002,4)*P(2004,2) + Metric(1003,1004)*Metric(2001,1002)*Metric(2003,1001)*P(2002,4)*P(2004,2) - 4*Metric(1002,1004)*Metric(2001,1003)*Metric(2003,1001)*P(2002,4)*P(2004,2) + Metric(1002,1003)*Metric(2001,1004)*Metric(2003,1001)*P(2002,4)*P(2004,2) + Metric(1001,1004)*Metric(2001,1003)*Metric(2003,1002)*P(2002,4)*P(2004,2) + Metric(1001,1003)*Metric(2001,1004)*Metric(2003,1002)*P(2002,4)*P(2004,2) + Metric(1001,1003)*Metric(2001,1002)*Metric(2003,1004)*P(2002,4)*P(2004,2) + Metric(1001,1002)*Metric(2001,1003)*Metric(2003,1004)*P(2002,4)*P(2004,2) + Metric(1001,1004)*Metric(1002,1003)*Metric(2001,2002)*P(2003,1)*P(2004,2) - 2*Metric(1001,1002)*Metric(1003,1004)*Metric(2001,2002)*P(2003,1)*P(2004,2) - 2*Metric(1003,1004)*Metric(2001,1002)*Metric(2002,1001)*P(2003,1)*P(2004,2) + Metric(1002,1003)*Metric(2001,1004)*Metric(2002,1001)*P(2003,1)*P(2004,2) - 2*Metric(1003,1004)*Metric(2001,1001)*Metric(2002,1002)*P(2003,1)*P(2004,2) + Metric(1001,1004)*Metric(2001,1002)*Metric(2002,1003)*P(2003,1)*P(2004,2) + Metric(1001,1002)*Metric(2001,1004)*Metric(2002,1003)*P(2003,1)*P(2004,2) + Metric(1001,1003)*Metric(1002,1004)*Metric(2001,2002)*P(2003,4)*P(2004,2) - 2*Metric(1001,1002)*Metric(1003,1004)*Metric(2001,2002)*P(2003,4)*P(2004,2) - 2*Metric(1003,1004)*Metric(2001,1002)*Metric(2002,1001)*P(2003,4)*P(2004,2) + Metric(1002,1004)*Metric(2001,1003)*Metric(2002,1001)*P(2003,4)*P(2004,2) + Metric(1002,1004)*Metric(2001,1001)*Metric(2002,1003)*P(2003,4)*P(2004,2) + Metric(1002,1003)*Metric(2001,1001)*Metric(2002,1004)*P(2003,4)*P(2004,2) + Metric(1001,1003)*Metric(2001,1002)*Metric(2002,1004)*P(2003,4)*P(2004,2) + Metric(1001,1002)*Metric(2001,1003)*Metric(2002,1004)*P(2003,4)*P(2004,2) + Metric(1002,1004)*Metric(2001,2003)*Metric(2002,1003)*P(1001,2)*P(2004,3) + Metric(1002,1003)*Metric(2001,2003)*Metric(2002,1004)*P(1001,2)*P(2004,3) + Metric(1002,1004)*Metric(2001,1003)*Metric(2002,2003)*P(1001,2)*P(2004,3) - 2*Metric(1002,1003)*Metric(2001,1004)*Metric(2002,2003)*P(1001,2)*P(2004,3) - 2*Metric(2001,1004)*Metric(2002,1003)*Metric(2003,1002)*P(1001,2)*P(2004,3) + Metric(2001,1003)*Metric(2002,1004)*Metric(2003,1002)*P(1001,2)*P(2004,3) - 2*Metric(2001,1004)*Metric(2002,1002)*Metric(2003,1003)*P(1001,2)*P(2004,3) + Metric(1003,1004)*Metric(2001,2003)*Metric(2002,1002)*P(1001,4)*P(2004,3) + Metric(1003,1004)*Metric(2001,1002)*Metric(2002,2003)*P(1001,4)*P(2004,3) - 2*Metric(1002,1003)*Metric(2001,1004)*Metric(2002,2003)*P(1001,4)*P(2004,3) + Metric(1003,1004)*Metric(2001,2002)*Metric(2003,1002)*P(1001,4)*P(2004,3) - 2*Metric(2001,1004)*Metric(2002,1003)*Metric(2003,1002)*P(1001,4)*P(2004,3) + Metric(1002,1003)*Metric(2001,2002)*Metric(2003,1004)*P(1001,4)*P(2004,3) + Metric(2001,1003)*Metric(2002,1002)*Metric(2003,1004)*P(1001,4)*P(2004,3) + Metric(2001,1002)*Metric(2002,1003)*Metric(2003,1004)*P(1001,4)*P(2004,3) + Metric(1001,1004)*Metric(2001,2003)*Metric(2002,1003)*P(1002,1)*P(2004,3) - 2*Metric(1001,1003)*Metric(2001,2003)*Metric(2002,1004)*P(1002,1)*P(2004,3) + Metric(1001,1004)*Metric(2001,1003)*Metric(2002,2003)*P(1002,1)*P(2004,3) + Metric(1001,1003)*Metric(2001,1004)*Metric(2002,2003)*P(1002,1)*P(2004,3) + Metric(2001,1004)*Metric(2002,1003)*Metric(2003,1001)*P(1002,1)*P(2004,3) - 2*Metric(2001,1003)*Metric(2002,1004)*Metric(2003,1001)*P(1002,1)*P(2004,3) - 2*Metric(2001,1001)*Metric(2002,1004)*Metric(2003,1003)*P(1002,1)*P(2004,3) + Metric(1003,1004)*Metric(2001,2003)*Metric(2002,1001)*P(1002,4)*P(2004,3) - 2*Metric(1001,1003)*Metric(2001,2003)*Metric(2002,1004)*P(1002,4)*P(2004,3) + Metric(1003,1004)*Metric(2001,1001)*Metric(2002,2003)*P(1002,4)*P(2004,3) + Metric(1003,1004)*Metric(2001,2002)*Metric(2003,1001)*P(1002,4)*P(2004,3) - 2*Metric(2001,1003)*Metric(2002,1004)*Metric(2003,1001)*P(1002,4)*P(2004,3) + Metric(1001,1003)*Metric(2001,2002)*Metric(2003,1004)*P(1002,4)*P(2004,3) + Metric(2001,1003)*Metric(2002,1001)*Metric(2003,1004)*P(1002,4)*P(2004,3) + Metric(2001,1001)*Metric(2002,1003)*Metric(2003,1004)*P(1002,4)*P(2004,3) + Metric(1002,1004)*Metric(2001,2003)*Metric(2002,1001)*P(1003,1)*P(2004,3) + Metric(1001,1004)*Metric(2001,2003)*Metric(2002,1002)*P(1003,1)*P(2004,3) + Metric(1001,1002)*Metric(2001,2003)*Metric(2002,1004)*P(1003,1)*P(2004,3) + Metric(1002,1004)*Metric(2001,2002)*Metric(2003,1001)*P(1003,1)*P(2004,3) + Metric(2001,1004)*Metric(2002,1002)*Metric(2003,1001)*P(1003,1)*P(2004,3) + Metric(2001,1002)*Metric(2002,1004)*Metric(2003,1001)*P(1003,1)*P(2004,3) - 2*Metric(1001,1002)*Metric(2001,2002)*Metric(2003,1004)*P(1003,1)*P(2004,3) - 2*Metric(2001,1002)*Metric(2002,1001)*Metric(2003,1004)*P(1003,1)*P(2004,3) + Metric(1002,1004)*Metric(2001,1001)*Metric(2002,2003)*P(1003,2)*P(2004,3) + Metric(1001,1004)*Metric(2001,1002)*Metric(2002,2003)*P(1003,2)*P(2004,3) + Metric(1001,1002)*Metric(2001,1004)*Metric(2002,2003)*P(1003,2)*P(2004,3) + Metric(1001,1004)*Metric(2001,2002)*Metric(2003,1002)*P(1003,2)*P(2004,3) + Metric(2001,1004)*Metric(2002,1001)*Metric(2003,1002)*P(1003,2)*P(2004,3) + Metric(2001,1001)*Metric(2002,1004)*Metric(2003,1002)*P(1003,2)*P(2004,3) - 2*Metric(1001,1002)*Metric(2001,2002)*Metric(2003,1004)*P(1003,2)*P(2004,3) - 2*Metric(2001,1002)*Metric(2002,1001)*Metric(2003,1004)*P(1003,2)*P(2004,3) + Metric(1002,1004)*Metric(2001,2003)*Metric(2002,1001)*P(1003,4)*P(2004,3) + Metric(1001,1002)*Metric(2001,2003)*Metric(2002,1004)*P(1003,4)*P(2004,3) + Metric(1001,1004)*Metric(2001,1002)*Metric(2002,2003)*P(1003,4)*P(2004,3) + Metric(1001,1002)*Metric(2001,1004)*Metric(2002,2003)*P(1003,4)*P(2004,3) + Metric(1002,1004)*Metric(2001,2002)*Metric(2003,1001)*P(1003,4)*P(2004,3) + Metric(2001,1002)*Metric(2002,1004)*Metric(2003,1001)*P(1003,4)*P(2004,3) + Metric(1001,1004)*Metric(2001,2002)*Metric(2003,1002)*P(1003,4)*P(2004,3) + Metric(2001,1004)*Metric(2002,1001)*Metric(2003,1002)*P(1003,4)*P(2004,3) - 4*Metric(1001,1002)*Metric(2001,2002)*Metric(2003,1004)*P(1003,4)*P(2004,3) - 4*Metric(2001,1002)*Metric(2002,1001)*Metric(2003,1004)*P(1003,4)*P(2004,3) - 2*Metric(1001,1003)*Metric(2001,2003)*Metric(2002,1002)*P(1004,1)*P(2004,3) - 2*Metric(2001,1003)*Metric(2002,1002)*Metric(2003,1001)*P(1004,1)*P(2004,3) - 2*Metric(1002,1003)*Metric(2001,1001)*Metric(2002,2003)*P(1004,2)*P(2004,3) - 2*Metric(2001,1001)*Metric(2002,1003)*Metric(2003,1002)*P(1004,2)*P(2004,3) - 2*Metric(1001,1004)*Metric(1002,1003)*Metric(2002,2003)*P(2001,2)*P(2004,3) + Metric(1001,1003)*Metric(1002,1004)*Metric(2002,2003)*P(2001,2)*P(2004,3) + Metric(1002,1004)*Metric(2002,1003)*Metric(2003,1001)*P(2001,2)*P(2004,3) + Metric(1002,1003)*Metric(2002,1004)*Metric(2003,1001)*P(2001,2)*P(2004,3) - 2*Metric(1001,1004)*Metric(2002,1003)*Metric(2003,1002)*P(2001,2)*P(2004,3) + Metric(1001,1003)*Metric(2002,1004)*Metric(2003,1002)*P(2001,2)*P(2004,3) - 2*Metric(1001,1004)*Metric(2002,1002)*Metric(2003,1003)*P(2001,2)*P(2004,3) - 2*Metric(1001,1004)*Metric(1002,1003)*Metric(2002,2003)*P(2001,4)*P(2004,3) + Metric(1001,1002)*Metric(1003,1004)*Metric(2002,2003)*P(2001,4)*P(2004,3) + Metric(1003,1004)*Metric(2002,1002)*Metric(2003,1001)*P(2001,4)*P(2004,3) + Metric(1003,1004)*Metric(2002,1001)*Metric(2003,1002)*P(2001,4)*P(2004,3) - 2*Metric(1001,1004)*Metric(2002,1003)*Metric(2003,1002)*P(2001,4)*P(2004,3) + Metric(1002,1003)*Metric(2002,1001)*Metric(2003,1004)*P(2001,4)*P(2004,3) + Metric(1001,1003)*Metric(2002,1002)*Metric(2003,1004)*P(2001,4)*P(2004,3) + Metric(1001,1002)*Metric(2002,1003)*Metric(2003,1004)*P(2001,4)*P(2004,3) + Metric(1001,1004)*Metric(1002,1003)*Metric(2001,2003)*P(2002,1)*P(2004,3) - 2*Metric(1001,1003)*Metric(1002,1004)*Metric(2001,2003)*P(2002,1)*P(2004,3) - 2*Metric(1002,1004)*Metric(2001,1003)*Metric(2003,1001)*P(2002,1)*P(2004,3) + Metric(1002,1003)*Metric(2001,1004)*Metric(2003,1001)*P(2002,1)*P(2004,3) + Metric(1001,1004)*Metric(2001,1003)*Metric(2003,1002)*P(2002,1)*P(2004,3) + Metric(1001,1003)*Metric(2001,1004)*Metric(2003,1002)*P(2002,1)*P(2004,3) - 2*Metric(1002,1004)*Metric(2001,1001)*Metric(2003,1003)*P(2002,1)*P(2004,3) - 2*Metric(1001,1003)*Metric(1002,1004)*Metric(2001,2003)*P(2002,4)*P(2004,3) + Metric(1001,1002)*Metric(1003,1004)*Metric(2001,2003)*P(2002,4)*P(2004,3) + Metric(1003,1004)*Metric(2001,1002)*Metric(2003,1001)*P(2002,4)*P(2004,3) - 2*Metric(1002,1004)*Metric(2001,1003)*Metric(2003,1001)*P(2002,4)*P(2004,3) + Metric(1003,1004)*Metric(2001,1001)*Metric(2003,1002)*P(2002,4)*P(2004,3) + Metric(1002,1003)*Metric(2001,1001)*Metric(2003,1004)*P(2002,4)*P(2004,3) + Metric(1001,1003)*Metric(2001,1002)*Metric(2003,1004)*P(2002,4)*P(2004,3) + Metric(1001,1002)*Metric(2001,1003)*Metric(2003,1004)*P(2002,4)*P(2004,3) + Metric(1001,1003)*Metric(1002,1004)*Metric(2001,2002)*P(2003,1)*P(2004,3) - 2*Metric(1001,1002)*Metric(1003,1004)*Metric(2001,2002)*P(2003,1)*P(2004,3) - 2*Metric(1003,1004)*Metric(2001,1002)*Metric(2002,1001)*P(2003,1)*P(2004,3) + Metric(1002,1004)*Metric(2001,1003)*Metric(2002,1001)*P(2003,1)*P(2004,3) + Metric(1001,1004)*Metric(2001,1003)*Metric(2002,1002)*P(2003,1)*P(2004,3) + Metric(1001,1003)*Metric(2001,1004)*Metric(2002,1002)*P(2003,1)*P(2004,3) + Metric(1001,1003)*Metric(2001,1002)*Metric(2002,1004)*P(2003,1)*P(2004,3) + Metric(1001,1002)*Metric(2001,1003)*Metric(2002,1004)*P(2003,1)*P(2004,3) + Metric(1001,1004)*Metric(1002,1003)*Metric(2001,2002)*P(2003,2)*P(2004,3) - 2*Metric(1001,1002)*Metric(1003,1004)*Metric(2001,2002)*P(2003,2)*P(2004,3) - 2*Metric(1003,1004)*Metric(2001,1002)*Metric(2002,1001)*P(2003,2)*P(2004,3) + Metric(1002,1003)*Metric(2001,1004)*Metric(2002,1001)*P(2003,2)*P(2004,3) + Metric(1002,1004)*Metric(2001,1001)*Metric(2002,1003)*P(2003,2)*P(2004,3) + Metric(1001,1004)*Metric(2001,1002)*Metric(2002,1003)*P(2003,2)*P(2004,3) + Metric(1001,1002)*Metric(2001,1004)*Metric(2002,1003)*P(2003,2)*P(2004,3) + Metric(1002,1003)*Metric(2001,1001)*Metric(2002,1004)*P(2003,2)*P(2004,3) + Metric(1001,1004)*Metric(1002,1003)*Metric(2001,2002)*P(2003,4)*P(2004,3) + Metric(1001,1003)*Metric(1002,1004)*Metric(2001,2002)*P(2003,4)*P(2004,3) - 4*Metric(1001,1002)*Metric(1003,1004)*Metric(2001,2002)*P(2003,4)*P(2004,3) - 4*Metric(1003,1004)*Metric(2001,1002)*Metric(2002,1001)*P(2003,4)*P(2004,3) + Metric(1002,1004)*Metric(2001,1003)*Metric(2002,1001)*P(2003,4)*P(2004,3) + Metric(1002,1003)*Metric(2001,1004)*Metric(2002,1001)*P(2003,4)*P(2004,3) + Metric(1001,1004)*Metric(2001,1002)*Metric(2002,1003)*P(2003,4)*P(2004,3) + Metric(1001,1002)*Metric(2001,1004)*Metric(2002,1003)*P(2003,4)*P(2004,3) + Metric(1001,1003)*Metric(2001,1002)*Metric(2002,1004)*P(2003,4)*P(2004,3) + Metric(1001,1002)*Metric(2001,1003)*Metric(2002,1004)*P(2003,4)*P(2004,3)') + + ) diff --git a/pyamplicol/src/pyamplicol/assets/models/ufo/scalar_gravity/object_library.py b/pyamplicol/src/pyamplicol/assets/models/ufo/scalar_gravity/object_library.py new file mode 100644 index 0000000000..85c41db185 --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/models/ufo/scalar_gravity/object_library.py @@ -0,0 +1,280 @@ +# type: ignore +## +## +# Feynrules Header +## +## +## +## +## + +from __future__ import absolute_import +import cmath + + +class UFOBaseClass(object): + """The class from which all FeynRules classes are derived.""" + + require_args = [] + + def __init__(self, *args, **options): + assert (len(self.require_args) == len(args)) + + for i, name in enumerate(self.require_args): + setattr(self, name, args[i]) + + for (option, value) in options.items(): + setattr(self, option, value) + + def get(self, name): + return getattr(self, name) + + def set(self, name, value): + setattr(self, name, value) + + def get_all(self): + """Return a dictionary containing all the information of the object""" + return self.__dict__ + + def __str__(self): + return self.name + + def nice_string(self): + """ return string with the full information """ + return '\n'.join(['%s \t: %s' % (name, value) for name, value in self.__dict__.items()]) + + def __repr__(self): + replacements = [ + ('+', '__plus__'), + ('-', '__minus__'), + ('@', '__at__'), + ('!', '__exclam__'), + ('?', '__quest__'), + ('*', '__star__'), + ('~', '__tilde__') + ] + text = self.name + for orig, sub in replacements: + text = text.replace(orig, sub) + return text + + +all_particles = [] + + +class Particle(UFOBaseClass): + """A standard Particle""" + + require_args = ['pdg_code', 'name', 'antiname', 'spin', + 'color', 'mass', 'width', 'texname', 'antitexname', 'charge'] + + require_args_all = ['pdg_code', 'name', 'antiname', 'spin', 'color', 'mass', 'width', + 'texname', 'antitexname', 'charge', 'line', 'propagating', 'goldstoneboson'] + + def __init__(self, pdg_code, name, antiname, spin, color, mass, width, texname, + antitexname, charge, line=None, propagating=True, goldstoneboson=False, **options): + + args = (pdg_code, name, antiname, spin, color, mass, width, texname, + antitexname, float(charge)) + + UFOBaseClass.__init__(self, *args, **options) + + global all_particles + all_particles.append(self) + + self.propagating = propagating + self.goldstoneboson = goldstoneboson + + self.selfconjugate = (name == antiname) + if 1: # not line: + self.line = self.find_line_type() + else: + self.line = line + + def find_line_type(self): + """ find how we draw a line if not defined + valid output: dashed/straight/wavy/curly/double/swavy/scurly + """ + + spin = self.spin + color = self.color + + # use default + if spin == 1: + return 'dashed' + elif spin == 2: + if not self.selfconjugate: + return 'straight' + elif color == 1: + return 'swavy' + else: + return 'scurly' + elif spin == 3: + if color == 1: + return 'wavy' + + else: + return 'curly' + elif spin == 5: + return 'double' + elif spin == -1: + return 'dotted' + else: + return 'dashed' # not supported yet + + def anti(self): + if self.selfconjugate: + raise Exception('%s has no anti particle.' % self.name) + outdic = {} + for k, v in self.__dict__.items(): + if k not in self.require_args_all: + outdic[k] = -v + if self.color in [1, 8]: + newcolor = self.color + else: + newcolor = -self.color + + return Particle(-self.pdg_code, self.antiname, self.name, self.spin, newcolor, self.mass, self.width, + self.antitexname, self.texname, -self.charge, self.line, self.propagating, self.goldstoneboson, **outdic) + + +all_parameters = [] + + +class Parameter(UFOBaseClass): + + require_args = ['name', 'nature', 'type', 'value', 'texname'] + + def __init__(self, name, nature, type, value, texname, lhablock=None, lhacode=None): + + args = (name, nature, type, value, texname) + + UFOBaseClass.__init__(self, *args) + + args = (name, nature, type, value, texname) + + global all_parameters + all_parameters.append(self) + + if (lhablock is None or lhacode is None) and nature == 'external': + raise Exception( + 'Need LHA information for external parameter "%s".' % name) + self.lhablock = lhablock + self.lhacode = lhacode + + +all_vertices = [] + + +class Vertex(UFOBaseClass): + + require_args = ['name', 'particles', 'color', 'lorentz', 'couplings'] + + def __init__(self, name, particles, color, lorentz, couplings, **opt): + + args = (name, particles, color, lorentz, couplings) + + UFOBaseClass.__init__(self, *args, **opt) + + args = (particles, color, lorentz, couplings) + + global all_vertices + all_vertices.append(self) + + +all_couplings = [] + + +class Coupling(UFOBaseClass): + + require_args = ['name', 'value', 'order'] + + def __init__(self, name, value, order, **opt): + + args = (name, value, order) + UFOBaseClass.__init__(self, *args, **opt) + global all_couplings + all_couplings.append(self) + + +all_lorentz = [] + + +class Lorentz(UFOBaseClass): + + require_args = ['name', 'spins', 'structure'] + + def __init__(self, name, spins, structure='external', **opt): + args = (name, spins, structure) + UFOBaseClass.__init__(self, *args, **opt) + + global all_lorentz + all_lorentz.append(self) + + +all_functions = [] + + +class Function(object): + + def __init__(self, name, arguments, expression): + + global all_functions + all_functions.append(self) + + self.name = name + self.arguments = arguments + self.expr = expression + + def __call__(self, *opt): + + for i, arg in enumerate(self.arguments): + exec('%s = %s' % (arg, opt[i])) + + return eval(self.expr) + + +all_orders = [] + + +class CouplingOrder(object): + + def __init__(self, name, expansion_order, hierarchy, perturbative_expansion=0): + + global all_orders + all_orders.append(self) + + self.name = name + self.expansion_order = expansion_order + self.hierarchy = hierarchy + + +all_decays = [] + + +class Decay(UFOBaseClass): + require_args = ['particle', 'partial_widths'] + + def __init__(self, particle, partial_widths, **opt): + args = (particle, partial_widths) + UFOBaseClass.__init__(self, *args, **opt) + + global all_decays + all_decays.append(self) + + # Add the information directly to the particle + particle.partial_widths = partial_widths + + +all_form_factors = [] + + +class FormFactor(UFOBaseClass): + require_args = ['name', 'type', 'value'] + + def __init__(self, name, type, value, **opt): + args = (name, type, value) + UFOBaseClass.__init__(self, *args, **opt) + + global all_form_factors + all_form_factors.append(self) diff --git a/pyamplicol/src/pyamplicol/assets/models/ufo/scalar_gravity/param_card.dat b/pyamplicol/src/pyamplicol/assets/models/ufo/scalar_gravity/param_card.dat new file mode 100644 index 0000000000..84e32fdbbb --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/models/ufo/scalar_gravity/param_card.dat @@ -0,0 +1,51 @@ +###################################################################### +## PARAM_CARD AUTOMATICALY GENERATED BY THE UFO ##################### +###################################################################### + +################################### +## INFORMATION FOR SMINPUTS +################################### +Block SMINPUTS + 1 1.325070e+02 # aEWM1 + 2 1.166390e-05 # Gf + 3 1.180000e-01 # aS + +################################### +## INFORMATION FOR GRAVITY +################################### +Block GRAVITY + 1337 1.000000e+00 # kappa + 1338 4.000000e+00 # dim + +################################### +## INFORMATION FOR SCALARS +################################### +Block SCALARS + 2002 1.000000e+00 # mass_scalar_1 + 2003 2.000000e+00 # mass_scalar_2 + 3002 0.000000e+00 # width_scalar_1 + 3003 0.000000e+00 # width_scalar_2 +#=========================================================== +# QUANTUM NUMBERS OF NEW STATE(S) (NON SM PDG CODE) +#=========================================================== + +Block QNUMBERS 2000 # scalar_0 + 1 0 # 3 times electric charge + 2 3 # number of spin states (2S+1) + 3 1 # colour rep (1: singlet, 3: triplet, 8: octet) + 4 0 # Particle/Antiparticle distinction (0=own anti) +Block QNUMBERS 2001 # scalar_1 + 1 0 # 3 times electric charge + 2 3 # number of spin states (2S+1) + 3 1 # colour rep (1: singlet, 3: triplet, 8: octet) + 4 0 # Particle/Antiparticle distinction (0=own anti) +Block QNUMBERS 2002 # scalar_2 + 1 0 # 3 times electric charge + 2 3 # number of spin states (2S+1) + 3 1 # colour rep (1: singlet, 3: triplet, 8: octet) + 4 0 # Particle/Antiparticle distinction (0=own anti) +Block QNUMBERS 1337 # graviton + 1 0 # 3 times electric charge + 2 11 # number of spin states (2S+1) + 3 1 # colour rep (1: singlet, 3: triplet, 8: octet) + 4 0 # Particle/Antiparticle distinction (0=own anti) diff --git a/pyamplicol/src/pyamplicol/assets/models/ufo/scalar_gravity/parameters.py b/pyamplicol/src/pyamplicol/assets/models/ufo/scalar_gravity/parameters.py new file mode 100644 index 0000000000..fc7068f097 --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/models/ufo/scalar_gravity/parameters.py @@ -0,0 +1,100 @@ +# type: ignore +from __future__ import absolute_import +from .object_library import all_parameters, Parameter + +from .function_library import complexconjugate, re, im, csc, sec, acsc, asec + +# Standalone assets are immutable; model shape must not depend on host state. +N_SCALARS = 3 + +# This is a default parameter object representing 0. +ZERO = Parameter(name='ZERO', + nature='internal', + type='real', + value='0.0', + texname='0') + +# Introduce the scalar coupling lambda +kappa = Parameter(name='kappa', + nature='external', + type='real', + value=1.0, + texname='\\kappa', + lhablock='GRAVITY', + lhacode=[1337]) + +dim = Parameter(name='dim', + nature='external', + type='real', + value=4.0, + texname='\\text{dim}', + lhablock='GRAVITY', + lhacode=[1338]) + +# Assign masses and widths to the scalars +scalar_mass_parameters = [] +scalar_width_parameters = [] +for i_scalar in range(N_SCALARS): + # Set the first scalar with id 0 to always be massless + if i_scalar == 0: + scalar_mass_parameters.append(ZERO) + scalar_width_parameters.append(ZERO) + continue + # remember that the constructor automatically adds the objects to all_parameters + scalar_mass_parameters.append(Parameter(name='mass_scalar_%s' % i_scalar, + nature='external', + type='real', + value=1.0*i_scalar, + texname='m_%d' % i_scalar, + lhablock='SCALARS', + lhacode=[2001+i_scalar])) + scalar_width_parameters.append(Parameter(name='width_scalar_%s' % i_scalar, + nature='external', + type='real', + value=0.0, + texname='w_%d' % i_scalar, + lhablock='SCALARS', + lhacode=[3001+i_scalar])) + +# User-defined parameters. Keep key SM parameters as their use may be hardcoded in some generators +aEWM1 = Parameter(name='aEWM1', + nature='external', + type='real', + value=132.50698, + texname='\\text{aEWM1}', + lhablock='SMINPUTS', + lhacode=[1]) + +Gf = Parameter(name='Gf', + nature='external', + type='real', + value=0.0000116639, + texname='G_f', + lhablock='SMINPUTS', + lhacode=[2]) + +aS = Parameter(name='aS', + nature='external', + type='real', + value=0.118, + texname='\\alpha _s', + lhablock='SMINPUTS', + lhacode=[3]) + +aEW = Parameter(name='aEW', + nature='internal', + type='real', + value='1/aEWM1', + texname='\\alpha _{\\text{EW}}') + +G = Parameter(name='G', + nature='internal', + type='real', + value='2*cmath.sqrt(aS)*cmath.sqrt(cmath.pi)', + texname='G') + +ee = Parameter(name='ee', + nature='internal', + type='real', + value='2*cmath.sqrt(aEW)*cmath.sqrt(cmath.pi)', + texname='e') diff --git a/pyamplicol/src/pyamplicol/assets/models/ufo/scalar_gravity/particles.py b/pyamplicol/src/pyamplicol/assets/models/ufo/scalar_gravity/particles.py new file mode 100644 index 0000000000..bea3ca350e --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/models/ufo/scalar_gravity/particles.py @@ -0,0 +1,40 @@ +# type: ignore +# This file was automatically created by FeynRules 1.7.69 +# Mathematica version: 8.0 for Mac OS X x86 (64-bit) (November 6, 2010) +# Date: Mon 1 Oct 2012 14:58:25 + + +from __future__ import division +from __future__ import absolute_import +from .object_library import all_particles, Particle # pylint: disable=unused-import +from . import parameters as Param + +scalar_particles = [] +for i_scalar in range(Param.N_SCALARS): + scalar_particles.append(Particle(pdg_code=2000+i_scalar, + name='scalar_%d' % i_scalar, + antiname='scalar_%d' % i_scalar, + spin=1, + color=1, + mass=Param.scalar_mass_parameters[i_scalar], + width=Param.scalar_width_parameters[i_scalar], + texname='\\phi_%d' % i_scalar, + antitexname='\\phi_%d' % i_scalar, + charge=0, + GhostNumber=0, + LeptonNumber=0, + Y=0)) + +graviton = Particle(pdg_code=1337, + name='graviton', + antiname='graviton', + spin=5, + color=1, + mass=Param.ZERO, + width=Param.ZERO, + texname='\\tilde{g}', + antitexname='\\tilde{g}', + charge=0, + GhostNumber=0, + LeptonNumber=0, + Y=0) diff --git a/pyamplicol/src/pyamplicol/assets/models/ufo/scalar_gravity/restrict_default.dat b/pyamplicol/src/pyamplicol/assets/models/ufo/scalar_gravity/restrict_default.dat new file mode 100644 index 0000000000..84e32fdbbb --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/models/ufo/scalar_gravity/restrict_default.dat @@ -0,0 +1,51 @@ +###################################################################### +## PARAM_CARD AUTOMATICALY GENERATED BY THE UFO ##################### +###################################################################### + +################################### +## INFORMATION FOR SMINPUTS +################################### +Block SMINPUTS + 1 1.325070e+02 # aEWM1 + 2 1.166390e-05 # Gf + 3 1.180000e-01 # aS + +################################### +## INFORMATION FOR GRAVITY +################################### +Block GRAVITY + 1337 1.000000e+00 # kappa + 1338 4.000000e+00 # dim + +################################### +## INFORMATION FOR SCALARS +################################### +Block SCALARS + 2002 1.000000e+00 # mass_scalar_1 + 2003 2.000000e+00 # mass_scalar_2 + 3002 0.000000e+00 # width_scalar_1 + 3003 0.000000e+00 # width_scalar_2 +#=========================================================== +# QUANTUM NUMBERS OF NEW STATE(S) (NON SM PDG CODE) +#=========================================================== + +Block QNUMBERS 2000 # scalar_0 + 1 0 # 3 times electric charge + 2 3 # number of spin states (2S+1) + 3 1 # colour rep (1: singlet, 3: triplet, 8: octet) + 4 0 # Particle/Antiparticle distinction (0=own anti) +Block QNUMBERS 2001 # scalar_1 + 1 0 # 3 times electric charge + 2 3 # number of spin states (2S+1) + 3 1 # colour rep (1: singlet, 3: triplet, 8: octet) + 4 0 # Particle/Antiparticle distinction (0=own anti) +Block QNUMBERS 2002 # scalar_2 + 1 0 # 3 times electric charge + 2 3 # number of spin states (2S+1) + 3 1 # colour rep (1: singlet, 3: triplet, 8: octet) + 4 0 # Particle/Antiparticle distinction (0=own anti) +Block QNUMBERS 1337 # graviton + 1 0 # 3 times electric charge + 2 11 # number of spin states (2S+1) + 3 1 # colour rep (1: singlet, 3: triplet, 8: octet) + 4 0 # Particle/Antiparticle distinction (0=own anti) diff --git a/pyamplicol/src/pyamplicol/assets/models/ufo/scalar_gravity/restrict_full.dat b/pyamplicol/src/pyamplicol/assets/models/ufo/scalar_gravity/restrict_full.dat new file mode 100644 index 0000000000..b206643b68 --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/models/ufo/scalar_gravity/restrict_full.dat @@ -0,0 +1,40 @@ +###################################################################### +## PARAM_CARD AUTOMATICALY GENERATED BY GAMMALOOP ## +###################################################################### + +################################### +## INFORMATION FOR SMINPUTS +################################### +Block SMINPUTS + 1 1.325070e+02 # aEWM1 + 2 1.166390e-05 # Gf + 3 1.180000e-01 # aS + +################################### +## INFORMATION FOR SCALARS +################################### +Block SCALARS + 1337 1.000000e+00 # lam + 2002 1.000000e+00 # mass_scalar_1 + 2003 2.000000e+00 # mass_scalar_2 + 3002 0.000000e+00 # width_scalar_1 + 3003 0.000000e+00 # width_scalar_2 +#=========================================================== +# QUANTUM NUMBERS OF NEW STATE(S) (NON SM PDG CODE) +#=========================================================== + +Block QNUMBERS 1000 # scalar_0 + 1 0 # 3 times electric charge + 2 3 # number of spin states (2S+1) + 3 1 # colour rep (1: singlet, 3: triplet, 8: octet) + 4 0 # Particle/Antiparticle distinction (0=own anti) +Block QNUMBERS 1001 # scalar_1 + 1 0 # 3 times electric charge + 2 3 # number of spin states (2S+1) + 3 1 # colour rep (1: singlet, 3: triplet, 8: octet) + 4 0 # Particle/Antiparticle distinction (0=own anti) +Block QNUMBERS 1002 # scalar_2 + 1 0 # 3 times electric charge + 2 3 # number of spin states (2S+1) + 3 1 # colour rep (1: singlet, 3: triplet, 8: octet) + 4 0 # Particle/Antiparticle distinction (0=own anti) diff --git a/pyamplicol/src/pyamplicol/assets/models/ufo/scalar_gravity/run_write_param_card.py b/pyamplicol/src/pyamplicol/assets/models/ufo/scalar_gravity/run_write_param_card.py new file mode 100755 index 0000000000..d87ecc25b9 --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/models/ufo/scalar_gravity/run_write_param_card.py @@ -0,0 +1,19 @@ +# type: ignore +#!/usr/bin/env python +from __future__ import absolute_import +from __future__ import print_function +from six.moves import range + +import sys +import os +import importlib + +if '__main__' == __name__: + dir_path = os.path.join(os.path.dirname(__file__)) + sys.path.insert(0, os.path.join(dir_path, os.path.pardir)) + pw = importlib.import_module('%s.%s' % ( + os.path.basename(dir_path), 'write_param_card')) + del sys.path[0] + + pw.ParamCardWriter('./param_card.dat', generic=True) + print('write ./param_card.dat') diff --git a/pyamplicol/src/pyamplicol/assets/models/ufo/scalar_gravity/vertices.py b/pyamplicol/src/pyamplicol/assets/models/ufo/scalar_gravity/vertices.py new file mode 100644 index 0000000000..ca314996c7 --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/models/ufo/scalar_gravity/vertices.py @@ -0,0 +1,53 @@ +# type: ignore +# This file was automatically created by FeynRules 1.7.69 +# Mathematica version: 8.0 for Mac OS X x86 (64-bit) (November 6, 2010) +# Date: Mon 1 Oct 2012 14:58:25 + + +from __future__ import absolute_import +from .object_library import all_vertices, Vertex +from . import particles as P +from . import couplings as C +from . import lorentz as L +from . import parameters as Param +import itertools + +for i_scalar in list(range(Param.N_SCALARS)): + Vertex(name='V_S%sS%sGr' % (i_scalar, i_scalar), + particles=[P.scalar_particles[i_scalar], + P.scalar_particles[i_scalar], P.graviton], + color=['1'], + lorentz=[L.SSTmpart, L.SST], + couplings={(0, 0): getattr( + C, ("SSTmpart%d" % i_scalar)), (0, 1): C.SST} + ) + Vertex(name='V_S%sS%sGrGr' % (i_scalar, i_scalar), + particles=[P.scalar_particles[i_scalar], + P.scalar_particles[i_scalar], P.graviton, P.graviton], + color=['1'], + lorentz=[L.SSTTmpart, L.SSTT], + couplings={(0, 0): getattr( + C, ("SSTTmpart%d" % i_scalar)), (0, 1): C.SSTT} + ) + Vertex(name='V_S%sS%sGrGrGr' % (i_scalar, i_scalar), + particles=[P.scalar_particles[i_scalar], + P.scalar_particles[i_scalar], P.graviton, P.graviton, P.graviton], + color=['1'], + lorentz=[L.SSTTTmpart, L.SSTTT], + couplings={(0, 0): getattr( + C, ("SSTTTmpart%d" % i_scalar)), (0, 1): C.SSTTT} + ) + +Vertex(name='V_GrGrGr', + particles=[P.graviton, P.graviton, P.graviton], + color=['1'], + lorentz=[L.TTT], + couplings={(0, 0): C.TTT} + ) + +Vertex(name='V_GrGrGrGr', + particles=[P.graviton, P.graviton, P.graviton, P.graviton], + color=['1'], + lorentz=[L.TTTT], + couplings={(0, 0): C.TTTT} + ) diff --git a/pyamplicol/src/pyamplicol/assets/models/ufo/scalar_gravity/write_param_card.py b/pyamplicol/src/pyamplicol/assets/models/ufo/scalar_gravity/write_param_card.py new file mode 100644 index 0000000000..bcf80e9955 --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/models/ufo/scalar_gravity/write_param_card.py @@ -0,0 +1,174 @@ +# type: ignore +from __future__ import absolute_import +from __future__ import print_function +from six.moves import range + + +class ParamCardWriter(object): + + header = \ + """######################################################################\n""" + \ + """## PARAM_CARD AUTOMATICALY GENERATED BY THE UFO #####################\n""" + \ + """######################################################################\n""" + + def __init__(self, filename, list_of_parameters=None, generic=False): + """write a valid param_card.dat""" + + if not list_of_parameters: + from .parameters import all_parameters + list_of_parameters = [param for param in all_parameters if + param.nature == 'external'] + + self.generic_output = generic + if generic: + self.define_not_dep_param(list_of_parameters) + + self.fsock = open(filename, 'w') + self.fsock.write(self.header) + + self.write_card(list_of_parameters) + + def define_not_dep_param(self, list_of_parameters): + """define self.dep_mass and self.dep_width in case that they are + requested in the param_card.dat""" + from .particles import all_particles + + self.dep_mass = [(part, part.mass) for part in all_particles + if part.pdg_code > 0 and + part.mass not in list_of_parameters] + self.dep_width = [(part, part.width) for part in all_particles + if part.pdg_code > 0 and + part.width not in list_of_parameters] + + @staticmethod + def order_param(obj1, obj2): + """ order parameter of a given block """ + + maxlen = min([len(obj1.lhacode), len(obj2.lhacode)]) + + for i in range(maxlen): + if obj1.lhacode[i] < obj2.lhacode[i]: + return -1 + elif obj1.lhacode[i] == obj2.lhacode[i]: + return 0 + else: + return 1 + # identical up to the first finish + if len(obj1.lhacode) > len(obj2.lhacode): + return 1 + elif len(obj1.lhacode) == len(obj2.lhacode): + return 0 + else: + return -1 + + def write_card(self, all_ext_param): + """ """ + + # list all lhablock + all_lhablock = set([param.lhablock for param in all_ext_param]) + + # ordonate lhablock alphabeticaly + all_lhablock = list(all_lhablock) + all_lhablock.sort() + # put at the beginning SMINPUT + MASS + DECAY + for name in ['DECAY', 'MASS', 'SMINPUTS']: + if name in all_lhablock: + all_lhablock.remove(name) + all_lhablock.insert(0, name) + + for lhablock in all_lhablock: + self.write_block(lhablock) + need_writing = [param for param in all_ext_param if + param.lhablock == lhablock] + from functools import cmp_to_key + need_writing.sort(key=cmp_to_key(self.order_param)) + [self.write_param(param, lhablock) for param in need_writing] + + if self.generic_output: + if lhablock in ['MASS', 'DECAY']: + self.write_dep_param_block(lhablock) + + if self.generic_output: + self.write_qnumber() + + def write_block(self, name): + """ write a comment for a block""" + + self.fsock.writelines( + """\n###################################""" + + """\n## INFORMATION FOR %s""" % name.upper() + + """\n###################################\n""" + ) + if name != 'DECAY': + self.fsock.write("""Block %s \n""" % name) + + def write_param(self, param, lhablock): + + lhacode = ' '.join(['%3s' % key for key in param.lhacode]) + if lhablock != 'DECAY': + text = """ %s %e # %s \n""" % ( + lhacode, complex(param.value).real, param.name) + else: + text = '''DECAY %s %e \n''' % (lhacode, complex(param.value).real) + self.fsock.write(text) + + def write_dep_param_block(self, lhablock): + import cmath + from .parameters import all_parameters + for parameter in all_parameters: + try: + exec("%s = %s" % (parameter.name, parameter.value)) + except Exception: + pass + text = "## Not dependent paramater.\n" + text += "## Those values should be edited following analytical the \n" + text += "## analytical expression. Some generator could simply ignore \n" + text += "## those values and use the analytical expression\n" + + if lhablock == 'MASS': + data = self.dep_mass + prefix = " " + else: + data = self.dep_width + prefix = "DECAY " + for part, param in data: + if isinstance(param.value, str): + value = complex(eval(param.value)).real + else: + value = param.value + + text += """%s %s %f # %s : %s \n""" % (prefix, part.pdg_code, + value, part.name, param.value) + self.fsock.write(text) + + sm_pdg = [1, 2, 3, 4, 5, 6, 11, 12, 13, 13, 14, 15, 16, 21, 22, 23, 24, 25] + data = """Block QNUMBERS %(pdg)d # %(name)s + 1 %(charge)d # 3 times electric charge + 2 %(spin)d # number of spin states (2S+1) + 3 %(color)d # colour rep (1: singlet, 3: triplet, 8: octet) + 4 %(antipart)d # Particle/Antiparticle distinction (0=own anti)\n""" + + def write_qnumber(self): + """ write qnumber """ + from .particles import all_particles + + text = """#===========================================================\n""" + text += """# QUANTUM NUMBERS OF NEW STATE(S) (NON SM PDG CODE)\n""" + text += """#===========================================================\n\n""" + + for part in all_particles: + if part.pdg_code in self.sm_pdg or part.pdg_code < 0: + continue + text += self.data % {'pdg': part.pdg_code, + 'name': part.name, + 'charge': 3 * part.charge, + 'spin': 2 * part.spin + 1, + 'color': part.color, + 'antipart': part.name != part.antiname and 1 or 0} + + self.fsock.write(text) + + +if '__main__' == __name__: + ParamCardWriter('./param_card.dat', generic=True) + print('write ./param_card.dat') diff --git a/pyamplicol/src/pyamplicol/assets/models/ufo/scalars/__init__.py b/pyamplicol/src/pyamplicol/assets/models/ufo/scalars/__init__.py new file mode 100644 index 0000000000..9a3ad6aac9 --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/models/ufo/scalars/__init__.py @@ -0,0 +1,25 @@ +# type: ignore +from __future__ import absolute_import +from . import particles +from . import couplings +from . import lorentz +from . import parameters +from . import vertices +from . import coupling_orders +from . import function_library +from . import object_library + +gauge = [0] + +all_particles = particles.all_particles +all_vertices = vertices.all_vertices +all_couplings = couplings.all_couplings +all_lorentz = lorentz.all_lorentz +all_parameters = parameters.all_parameters +all_orders = coupling_orders.all_orders +all_functions = function_library.all_functions + + +__author__ = "Valentin Hirschi" +__version__ = "1.0" +__email__ = "valentin.hirschi@gmail.com" diff --git a/pyamplicol/src/pyamplicol/assets/models/ufo/scalars/coupling_orders.py b/pyamplicol/src/pyamplicol/assets/models/ufo/scalars/coupling_orders.py new file mode 100644 index 0000000000..cb073b19ef --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/models/ufo/scalars/coupling_orders.py @@ -0,0 +1,12 @@ +# type: ignore +from __future__ import absolute_import +from .object_library import all_orders, CouplingOrder + +# Keep the coupling order names QCD and QED as they are hard-coded standards in some generators +QCD = CouplingOrder(name='QCD', + expansion_order=99, + hierarchy=1) + +QED = CouplingOrder(name='QED', + expansion_order=99, + hierarchy=2) diff --git a/pyamplicol/src/pyamplicol/assets/models/ufo/scalars/couplings.py b/pyamplicol/src/pyamplicol/assets/models/ufo/scalars/couplings.py new file mode 100644 index 0000000000..9a3f7c38b0 --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/models/ufo/scalars/couplings.py @@ -0,0 +1,8 @@ +# type: ignore +from __future__ import absolute_import +from .object_library import all_couplings, Coupling + + +SCALAR_COUPLING = Coupling(name='SCALAR_COUPLING', + value='lam*complex(0,1)', + order={'QCD': 1}) diff --git a/pyamplicol/src/pyamplicol/assets/models/ufo/scalars/function_library.py b/pyamplicol/src/pyamplicol/assets/models/ufo/scalars/function_library.py new file mode 100644 index 0000000000..b2843b6ca0 --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/models/ufo/scalars/function_library.py @@ -0,0 +1,52 @@ +# type: ignore +# This file is part of the UFO. +# +# This file contains definitions for functions that +# are extensions of the cmath library, and correspond +# either to functions that are in cmath, but inconvenient +# to access from there (e.g. z.conjugate()), +# or functions that are simply not defined. +# +# +from __future__ import absolute_import +__date__ = "22 July 2010" +__author__ = "claude.duhr@durham.ac.uk" + +from cmath import cos, sin, acos, asin +from .object_library import all_functions, Function + +# +# shortcuts for functions from cmath +# + +complexconjugate = Function(name='complexconjugate', + arguments=('z',), + expression='z.conjugate()') + + +re = Function(name='re', + arguments=('z',), + expression='z.real') + +im = Function(name='im', + arguments=('z',), + expression='z.imag') + + +# New functions (trigonometric) + +sec = Function(name='sec', + arguments=('z',), + expression='1./cos(z)') + +asec = Function(name='asec', + arguments=('z',), + expression='acos(1./z)') + +csc = Function(name='csc', + arguments=('z',), + expression='1./sin(z)') + +acsc = Function(name='acsc', + arguments=('z',), + expression='asin(1./z)') diff --git a/pyamplicol/src/pyamplicol/assets/models/ufo/scalars/lorentz.py b/pyamplicol/src/pyamplicol/assets/models/ufo/scalars/lorentz.py new file mode 100644 index 0000000000..85a4a01cac --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/models/ufo/scalars/lorentz.py @@ -0,0 +1,18 @@ +# type: ignore +from __future__ import absolute_import +from .object_library import all_lorentz, Lorentz + +from .function_library import complexconjugate, re, im, csc, sec, acsc, asec +from . import parameters as Param + +try: + import form_factors as ForFac +except ImportError: + pass + +scalar_lorentz_structures = {} +for n_point_interaction in Param.N_POINT_INTERACTIONS: + scalar_lorentz_structures[n_point_interaction] = Lorentz(name='SCALAR_%d_LORENTZ_STRUCTURE' % n_point_interaction, + spins=[ + 1, ]*n_point_interaction, + structure='1') diff --git a/pyamplicol/src/pyamplicol/assets/models/ufo/scalars/object_library.py b/pyamplicol/src/pyamplicol/assets/models/ufo/scalars/object_library.py new file mode 100644 index 0000000000..85c41db185 --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/models/ufo/scalars/object_library.py @@ -0,0 +1,280 @@ +# type: ignore +## +## +# Feynrules Header +## +## +## +## +## + +from __future__ import absolute_import +import cmath + + +class UFOBaseClass(object): + """The class from which all FeynRules classes are derived.""" + + require_args = [] + + def __init__(self, *args, **options): + assert (len(self.require_args) == len(args)) + + for i, name in enumerate(self.require_args): + setattr(self, name, args[i]) + + for (option, value) in options.items(): + setattr(self, option, value) + + def get(self, name): + return getattr(self, name) + + def set(self, name, value): + setattr(self, name, value) + + def get_all(self): + """Return a dictionary containing all the information of the object""" + return self.__dict__ + + def __str__(self): + return self.name + + def nice_string(self): + """ return string with the full information """ + return '\n'.join(['%s \t: %s' % (name, value) for name, value in self.__dict__.items()]) + + def __repr__(self): + replacements = [ + ('+', '__plus__'), + ('-', '__minus__'), + ('@', '__at__'), + ('!', '__exclam__'), + ('?', '__quest__'), + ('*', '__star__'), + ('~', '__tilde__') + ] + text = self.name + for orig, sub in replacements: + text = text.replace(orig, sub) + return text + + +all_particles = [] + + +class Particle(UFOBaseClass): + """A standard Particle""" + + require_args = ['pdg_code', 'name', 'antiname', 'spin', + 'color', 'mass', 'width', 'texname', 'antitexname', 'charge'] + + require_args_all = ['pdg_code', 'name', 'antiname', 'spin', 'color', 'mass', 'width', + 'texname', 'antitexname', 'charge', 'line', 'propagating', 'goldstoneboson'] + + def __init__(self, pdg_code, name, antiname, spin, color, mass, width, texname, + antitexname, charge, line=None, propagating=True, goldstoneboson=False, **options): + + args = (pdg_code, name, antiname, spin, color, mass, width, texname, + antitexname, float(charge)) + + UFOBaseClass.__init__(self, *args, **options) + + global all_particles + all_particles.append(self) + + self.propagating = propagating + self.goldstoneboson = goldstoneboson + + self.selfconjugate = (name == antiname) + if 1: # not line: + self.line = self.find_line_type() + else: + self.line = line + + def find_line_type(self): + """ find how we draw a line if not defined + valid output: dashed/straight/wavy/curly/double/swavy/scurly + """ + + spin = self.spin + color = self.color + + # use default + if spin == 1: + return 'dashed' + elif spin == 2: + if not self.selfconjugate: + return 'straight' + elif color == 1: + return 'swavy' + else: + return 'scurly' + elif spin == 3: + if color == 1: + return 'wavy' + + else: + return 'curly' + elif spin == 5: + return 'double' + elif spin == -1: + return 'dotted' + else: + return 'dashed' # not supported yet + + def anti(self): + if self.selfconjugate: + raise Exception('%s has no anti particle.' % self.name) + outdic = {} + for k, v in self.__dict__.items(): + if k not in self.require_args_all: + outdic[k] = -v + if self.color in [1, 8]: + newcolor = self.color + else: + newcolor = -self.color + + return Particle(-self.pdg_code, self.antiname, self.name, self.spin, newcolor, self.mass, self.width, + self.antitexname, self.texname, -self.charge, self.line, self.propagating, self.goldstoneboson, **outdic) + + +all_parameters = [] + + +class Parameter(UFOBaseClass): + + require_args = ['name', 'nature', 'type', 'value', 'texname'] + + def __init__(self, name, nature, type, value, texname, lhablock=None, lhacode=None): + + args = (name, nature, type, value, texname) + + UFOBaseClass.__init__(self, *args) + + args = (name, nature, type, value, texname) + + global all_parameters + all_parameters.append(self) + + if (lhablock is None or lhacode is None) and nature == 'external': + raise Exception( + 'Need LHA information for external parameter "%s".' % name) + self.lhablock = lhablock + self.lhacode = lhacode + + +all_vertices = [] + + +class Vertex(UFOBaseClass): + + require_args = ['name', 'particles', 'color', 'lorentz', 'couplings'] + + def __init__(self, name, particles, color, lorentz, couplings, **opt): + + args = (name, particles, color, lorentz, couplings) + + UFOBaseClass.__init__(self, *args, **opt) + + args = (particles, color, lorentz, couplings) + + global all_vertices + all_vertices.append(self) + + +all_couplings = [] + + +class Coupling(UFOBaseClass): + + require_args = ['name', 'value', 'order'] + + def __init__(self, name, value, order, **opt): + + args = (name, value, order) + UFOBaseClass.__init__(self, *args, **opt) + global all_couplings + all_couplings.append(self) + + +all_lorentz = [] + + +class Lorentz(UFOBaseClass): + + require_args = ['name', 'spins', 'structure'] + + def __init__(self, name, spins, structure='external', **opt): + args = (name, spins, structure) + UFOBaseClass.__init__(self, *args, **opt) + + global all_lorentz + all_lorentz.append(self) + + +all_functions = [] + + +class Function(object): + + def __init__(self, name, arguments, expression): + + global all_functions + all_functions.append(self) + + self.name = name + self.arguments = arguments + self.expr = expression + + def __call__(self, *opt): + + for i, arg in enumerate(self.arguments): + exec('%s = %s' % (arg, opt[i])) + + return eval(self.expr) + + +all_orders = [] + + +class CouplingOrder(object): + + def __init__(self, name, expansion_order, hierarchy, perturbative_expansion=0): + + global all_orders + all_orders.append(self) + + self.name = name + self.expansion_order = expansion_order + self.hierarchy = hierarchy + + +all_decays = [] + + +class Decay(UFOBaseClass): + require_args = ['particle', 'partial_widths'] + + def __init__(self, particle, partial_widths, **opt): + args = (particle, partial_widths) + UFOBaseClass.__init__(self, *args, **opt) + + global all_decays + all_decays.append(self) + + # Add the information directly to the particle + particle.partial_widths = partial_widths + + +all_form_factors = [] + + +class FormFactor(UFOBaseClass): + require_args = ['name', 'type', 'value'] + + def __init__(self, name, type, value, **opt): + args = (name, type, value) + UFOBaseClass.__init__(self, *args, **opt) + + global all_form_factors + all_form_factors.append(self) diff --git a/pyamplicol/src/pyamplicol/assets/models/ufo/scalars/parameters.py b/pyamplicol/src/pyamplicol/assets/models/ufo/scalars/parameters.py new file mode 100644 index 0000000000..1c02381ca0 --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/models/ufo/scalars/parameters.py @@ -0,0 +1,93 @@ +# type: ignore +from __future__ import absolute_import +from .object_library import all_parameters, Parameter + +from .function_library import complexconjugate, re, im, csc, sec, acsc, asec + +# Standalone assets are immutable; model shape must not depend on host state. +N_SCALARS = 3 +N_POINT_INTERACTIONS = [3, 4, 5, 6, 7, 8, 9, 10] + +# This is a default parameter object representing 0. +ZERO = Parameter(name='ZERO', + nature='internal', + type='real', + value='0.0', + texname='0') + +# Introduce the scalar coupling lambda +lam = Parameter(name='lam', + nature='external', + type='real', + value=1.0, + texname='\\lambda', + lhablock='SCALARS', + lhacode=[1337]) + +# Assign masses and widths to the scalars +scalar_mass_parameters = [] +scalar_width_parameters = [] +for i_scalar in range(N_SCALARS): + # Set the first scalar with id 0 to always be massless + if i_scalar == 0: + scalar_mass_parameters.append(ZERO) + scalar_width_parameters.append(ZERO) + continue + # remember that the constructor automatically adds the objects to all_parameters + scalar_mass_parameters.append(Parameter(name='mass_scalar_%s' % i_scalar, + nature='external', + type='real', + value=1.0*i_scalar, + texname='m_%d' % i_scalar, + lhablock='SCALARS', + lhacode=[2001+i_scalar])) + scalar_width_parameters.append(Parameter(name='width_scalar_%s' % i_scalar, + nature='external', + type='real', + value=0.0, + texname='w_%d' % i_scalar, + lhablock='SCALARS', + lhacode=[3001+i_scalar])) + +# User-defined parameters. Keep key SM parameters as their use may be hardcoded in some generators +aEWM1 = Parameter(name='aEWM1', + nature='external', + type='real', + value=132.50698, + texname='\\text{aEWM1}', + lhablock='SMINPUTS', + lhacode=[1]) + +Gf = Parameter(name='Gf', + nature='external', + type='real', + value=0.0000116639, + texname='G_f', + lhablock='SMINPUTS', + lhacode=[2]) + +aS = Parameter(name='aS', + nature='external', + type='real', + value=0.118, + texname='\\alpha _s', + lhablock='SMINPUTS', + lhacode=[3]) + +aEW = Parameter(name='aEW', + nature='internal', + type='real', + value='1/aEWM1', + texname='\\alpha _{\\text{EW}}') + +G = Parameter(name='G', + nature='internal', + type='real', + value='2*cmath.sqrt(aS)*cmath.sqrt(cmath.pi)', + texname='G') + +ee = Parameter(name='ee', + nature='internal', + type='real', + value='2*cmath.sqrt(aEW)*cmath.sqrt(cmath.pi)', + texname='e') diff --git a/pyamplicol/src/pyamplicol/assets/models/ufo/scalars/particles.py b/pyamplicol/src/pyamplicol/assets/models/ufo/scalars/particles.py new file mode 100644 index 0000000000..20399e78be --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/models/ufo/scalars/particles.py @@ -0,0 +1,26 @@ +# type: ignore +# This file was automatically created by FeynRules 1.7.69 +# Mathematica version: 8.0 for Mac OS X x86 (64-bit) (November 6, 2010) +# Date: Mon 1 Oct 2012 14:58:25 + + +from __future__ import division +from __future__ import absolute_import +from .object_library import all_particles, Particle # pylint: disable=unused-import +from . import parameters as Param + +scalar_particles = [] +for i_scalar in range(Param.N_SCALARS): + scalar_particles.append(Particle(pdg_code=1000+i_scalar, + name='scalar_%d' % i_scalar, + antiname='scalar_%d' % i_scalar, + spin=1, + color=1, + mass=Param.scalar_mass_parameters[i_scalar], + width=Param.scalar_width_parameters[i_scalar], + texname='\\phi_%d' % i_scalar, + antitexname='\\phi_%d' % i_scalar, + charge=0, + GhostNumber=0, + LeptonNumber=0, + Y=0)) diff --git a/pyamplicol/src/pyamplicol/assets/models/ufo/scalars/restrict_default.dat b/pyamplicol/src/pyamplicol/assets/models/ufo/scalars/restrict_default.dat new file mode 100644 index 0000000000..8d45909b65 --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/models/ufo/scalars/restrict_default.dat @@ -0,0 +1,45 @@ +###################################################################### +## PARAM_CARD AUTOMATICALY GENERATED BY THE UFO ##################### +###################################################################### + +################################### +## INFORMATION FOR SMINPUTS +################################### +Block SMINPUTS + 1 1.325070e+02 # aEWM1 + 2 1.166390e-05 # Gf + 3 1.180000e-01 # aS + +################################### +## INFORMATION FOR SCALARS +################################### +Block SCALARS + 1337 1.000000e+00 # lambda + 2002 1.000000e+00 # mass_scalar_1 + 2003 2.000000e+00 # mass_scalar_2 + 3002 0.000000e+00 # width_scalar_1 + 3003 0.000000e+00 # width_scalar_2 +#=========================================================== +# QUANTUM NUMBERS OF NEW STATE(S) (NON SM PDG CODE) +#=========================================================== + +Block QNUMBERS 1000 # scalar_0 + 1 0 # 3 times electric charge + 2 3 # number of spin states (2S+1) + 3 1 # colour rep (1: singlet, 3: triplet, 8: octet) + 4 0 # Particle/Antiparticle distinction (0=own anti) +Block QNUMBERS 1001 # scalar_1 + 1 0 # 3 times electric charge + 2 3 # number of spin states (2S+1) + 3 1 # colour rep (1: singlet, 3: triplet, 8: octet) + 4 0 # Particle/Antiparticle distinction (0=own anti) +Block QNUMBERS 1002 # scalar_2 + 1 0 # 3 times electric charge + 2 3 # number of spin states (2S+1) + 3 1 # colour rep (1: singlet, 3: triplet, 8: octet) + 4 0 # Particle/Antiparticle distinction (0=own anti) + +########################################### +## MadGraph needs at least one DECAY entry +########################################## +DECAY 1000 0.000000e+00 # scalar_0 \ No newline at end of file diff --git a/pyamplicol/src/pyamplicol/assets/models/ufo/scalars/restrict_full.dat b/pyamplicol/src/pyamplicol/assets/models/ufo/scalars/restrict_full.dat new file mode 100644 index 0000000000..b206643b68 --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/models/ufo/scalars/restrict_full.dat @@ -0,0 +1,40 @@ +###################################################################### +## PARAM_CARD AUTOMATICALY GENERATED BY GAMMALOOP ## +###################################################################### + +################################### +## INFORMATION FOR SMINPUTS +################################### +Block SMINPUTS + 1 1.325070e+02 # aEWM1 + 2 1.166390e-05 # Gf + 3 1.180000e-01 # aS + +################################### +## INFORMATION FOR SCALARS +################################### +Block SCALARS + 1337 1.000000e+00 # lam + 2002 1.000000e+00 # mass_scalar_1 + 2003 2.000000e+00 # mass_scalar_2 + 3002 0.000000e+00 # width_scalar_1 + 3003 0.000000e+00 # width_scalar_2 +#=========================================================== +# QUANTUM NUMBERS OF NEW STATE(S) (NON SM PDG CODE) +#=========================================================== + +Block QNUMBERS 1000 # scalar_0 + 1 0 # 3 times electric charge + 2 3 # number of spin states (2S+1) + 3 1 # colour rep (1: singlet, 3: triplet, 8: octet) + 4 0 # Particle/Antiparticle distinction (0=own anti) +Block QNUMBERS 1001 # scalar_1 + 1 0 # 3 times electric charge + 2 3 # number of spin states (2S+1) + 3 1 # colour rep (1: singlet, 3: triplet, 8: octet) + 4 0 # Particle/Antiparticle distinction (0=own anti) +Block QNUMBERS 1002 # scalar_2 + 1 0 # 3 times electric charge + 2 3 # number of spin states (2S+1) + 3 1 # colour rep (1: singlet, 3: triplet, 8: octet) + 4 0 # Particle/Antiparticle distinction (0=own anti) diff --git a/pyamplicol/src/pyamplicol/assets/models/ufo/scalars/run_write_param_card.py b/pyamplicol/src/pyamplicol/assets/models/ufo/scalars/run_write_param_card.py new file mode 100755 index 0000000000..d87ecc25b9 --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/models/ufo/scalars/run_write_param_card.py @@ -0,0 +1,19 @@ +# type: ignore +#!/usr/bin/env python +from __future__ import absolute_import +from __future__ import print_function +from six.moves import range + +import sys +import os +import importlib + +if '__main__' == __name__: + dir_path = os.path.join(os.path.dirname(__file__)) + sys.path.insert(0, os.path.join(dir_path, os.path.pardir)) + pw = importlib.import_module('%s.%s' % ( + os.path.basename(dir_path), 'write_param_card')) + del sys.path[0] + + pw.ParamCardWriter('./param_card.dat', generic=True) + print('write ./param_card.dat') diff --git a/pyamplicol/src/pyamplicol/assets/models/ufo/scalars/vertices.py b/pyamplicol/src/pyamplicol/assets/models/ufo/scalars/vertices.py new file mode 100644 index 0000000000..b58af0f4ce --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/models/ufo/scalars/vertices.py @@ -0,0 +1,23 @@ +# type: ignore +# This file was automatically created by FeynRules 1.7.69 +# Mathematica version: 8.0 for Mac OS X x86 (64-bit) (November 6, 2010) +# Date: Mon 1 Oct 2012 14:58:25 + + +from __future__ import absolute_import +from .object_library import all_vertices, Vertex +from . import particles as P +from . import couplings as C +from . import lorentz as L +from . import parameters as Param +import itertools + +for n_point_interaction in Param.N_POINT_INTERACTIONS: + for i_inter, interaction in enumerate(sorted(list(set(tuple(sorted(inter)) for inter in itertools.product(*[list(range(Param.N_SCALARS)),]*n_point_interaction))))): + # Remember that the constructor automatically adds the objects to all_vertices + Vertex(name='V_%d_SCALAR_%s' % (n_point_interaction, ''.join('%d' % scalar_id for scalar_id in interaction)), + particles=[P.scalar_particles[scalar_id] + for scalar_id in interaction], + color=['1'], + lorentz=[L.scalar_lorentz_structures[n_point_interaction]], + couplings={(0, 0): C.SCALAR_COUPLING}) diff --git a/pyamplicol/src/pyamplicol/assets/models/ufo/scalars/write_param_card.py b/pyamplicol/src/pyamplicol/assets/models/ufo/scalars/write_param_card.py new file mode 100644 index 0000000000..bcf80e9955 --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/models/ufo/scalars/write_param_card.py @@ -0,0 +1,174 @@ +# type: ignore +from __future__ import absolute_import +from __future__ import print_function +from six.moves import range + + +class ParamCardWriter(object): + + header = \ + """######################################################################\n""" + \ + """## PARAM_CARD AUTOMATICALY GENERATED BY THE UFO #####################\n""" + \ + """######################################################################\n""" + + def __init__(self, filename, list_of_parameters=None, generic=False): + """write a valid param_card.dat""" + + if not list_of_parameters: + from .parameters import all_parameters + list_of_parameters = [param for param in all_parameters if + param.nature == 'external'] + + self.generic_output = generic + if generic: + self.define_not_dep_param(list_of_parameters) + + self.fsock = open(filename, 'w') + self.fsock.write(self.header) + + self.write_card(list_of_parameters) + + def define_not_dep_param(self, list_of_parameters): + """define self.dep_mass and self.dep_width in case that they are + requested in the param_card.dat""" + from .particles import all_particles + + self.dep_mass = [(part, part.mass) for part in all_particles + if part.pdg_code > 0 and + part.mass not in list_of_parameters] + self.dep_width = [(part, part.width) for part in all_particles + if part.pdg_code > 0 and + part.width not in list_of_parameters] + + @staticmethod + def order_param(obj1, obj2): + """ order parameter of a given block """ + + maxlen = min([len(obj1.lhacode), len(obj2.lhacode)]) + + for i in range(maxlen): + if obj1.lhacode[i] < obj2.lhacode[i]: + return -1 + elif obj1.lhacode[i] == obj2.lhacode[i]: + return 0 + else: + return 1 + # identical up to the first finish + if len(obj1.lhacode) > len(obj2.lhacode): + return 1 + elif len(obj1.lhacode) == len(obj2.lhacode): + return 0 + else: + return -1 + + def write_card(self, all_ext_param): + """ """ + + # list all lhablock + all_lhablock = set([param.lhablock for param in all_ext_param]) + + # ordonate lhablock alphabeticaly + all_lhablock = list(all_lhablock) + all_lhablock.sort() + # put at the beginning SMINPUT + MASS + DECAY + for name in ['DECAY', 'MASS', 'SMINPUTS']: + if name in all_lhablock: + all_lhablock.remove(name) + all_lhablock.insert(0, name) + + for lhablock in all_lhablock: + self.write_block(lhablock) + need_writing = [param for param in all_ext_param if + param.lhablock == lhablock] + from functools import cmp_to_key + need_writing.sort(key=cmp_to_key(self.order_param)) + [self.write_param(param, lhablock) for param in need_writing] + + if self.generic_output: + if lhablock in ['MASS', 'DECAY']: + self.write_dep_param_block(lhablock) + + if self.generic_output: + self.write_qnumber() + + def write_block(self, name): + """ write a comment for a block""" + + self.fsock.writelines( + """\n###################################""" + + """\n## INFORMATION FOR %s""" % name.upper() + + """\n###################################\n""" + ) + if name != 'DECAY': + self.fsock.write("""Block %s \n""" % name) + + def write_param(self, param, lhablock): + + lhacode = ' '.join(['%3s' % key for key in param.lhacode]) + if lhablock != 'DECAY': + text = """ %s %e # %s \n""" % ( + lhacode, complex(param.value).real, param.name) + else: + text = '''DECAY %s %e \n''' % (lhacode, complex(param.value).real) + self.fsock.write(text) + + def write_dep_param_block(self, lhablock): + import cmath + from .parameters import all_parameters + for parameter in all_parameters: + try: + exec("%s = %s" % (parameter.name, parameter.value)) + except Exception: + pass + text = "## Not dependent paramater.\n" + text += "## Those values should be edited following analytical the \n" + text += "## analytical expression. Some generator could simply ignore \n" + text += "## those values and use the analytical expression\n" + + if lhablock == 'MASS': + data = self.dep_mass + prefix = " " + else: + data = self.dep_width + prefix = "DECAY " + for part, param in data: + if isinstance(param.value, str): + value = complex(eval(param.value)).real + else: + value = param.value + + text += """%s %s %f # %s : %s \n""" % (prefix, part.pdg_code, + value, part.name, param.value) + self.fsock.write(text) + + sm_pdg = [1, 2, 3, 4, 5, 6, 11, 12, 13, 13, 14, 15, 16, 21, 22, 23, 24, 25] + data = """Block QNUMBERS %(pdg)d # %(name)s + 1 %(charge)d # 3 times electric charge + 2 %(spin)d # number of spin states (2S+1) + 3 %(color)d # colour rep (1: singlet, 3: triplet, 8: octet) + 4 %(antipart)d # Particle/Antiparticle distinction (0=own anti)\n""" + + def write_qnumber(self): + """ write qnumber """ + from .particles import all_particles + + text = """#===========================================================\n""" + text += """# QUANTUM NUMBERS OF NEW STATE(S) (NON SM PDG CODE)\n""" + text += """#===========================================================\n\n""" + + for part in all_particles: + if part.pdg_code in self.sm_pdg or part.pdg_code < 0: + continue + text += self.data % {'pdg': part.pdg_code, + 'name': part.name, + 'charge': 3 * part.charge, + 'spin': 2 * part.spin + 1, + 'color': part.color, + 'antipart': part.name != part.antiname and 1 or 0} + + self.fsock.write(text) + + +if '__main__' == __name__: + ParamCardWriter('./param_card.dat', generic=True) + print('write ./param_card.dat') diff --git a/pyamplicol/src/pyamplicol/assets/models/ufo/sm/__init__.py b/pyamplicol/src/pyamplicol/assets/models/ufo/sm/__init__.py new file mode 100644 index 0000000000..a4b9447bca --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/models/ufo/sm/__init__.py @@ -0,0 +1,37 @@ +# type: ignore +from __future__ import absolute_import +from . import particles +from . import couplings +from . import lorentz +from . import parameters +from . import vertices +from . import coupling_orders +from . import function_library +from . import object_library +# from . import write_param_card +try: + from . import decays +except ImportError: + pass +try: + from . import build_restrict +except ImportError: + pass + +# model options +gauge = [0, 1] + + +all_particles = particles.all_particles +all_vertices = vertices.all_vertices +all_couplings = couplings.all_couplings +all_lorentz = lorentz.all_lorentz +all_parameters = parameters.all_parameters +all_orders = coupling_orders.all_orders +all_functions = function_library.all_functions +# all_decays = decays.all_decays + + +__author__ = "N. Christensen, C. Duhr" +__version__ = "1.3" +__email__ = "neil@pa.msu.edu, claude.duhr@uclouvain.be" diff --git a/pyamplicol/src/pyamplicol/assets/models/ufo/sm/build_restrict.py b/pyamplicol/src/pyamplicol/assets/models/ufo/sm/build_restrict.py new file mode 100644 index 0000000000..838268f208 --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/models/ufo/sm/build_restrict.py @@ -0,0 +1,70 @@ +# type: ignore +################################################################################ +# +# Copyright (c) 2012 The MadGraph5_aMC@NLO Development team and Contributors +# +# This file is a part of the MadGraph5_aMC@NLO project, an application which +# automatically generates Feynman diagrams and matrix elements for arbitrary +# high-energy processes in the Standard Model and beyond. +# +# It is subject to the MadGraph5_aMC@NLO license which should accompany this +# distribution. +# +# For more information, visit madgraph.phys.ucl.ac.be and amcatnlo.web.cern.ch +# +################################################################################ +""" This part is not part of the UFO Model but only of MG5 suite. +This files defines how the restrict card can be build automatically """ + +### Important Warning ### +# When you develop such file. Please cross check that they are NO +# unwanted simplification to your model. This can happen especially +# if a bunch of value are set to 1.0 by default. This YOUR responsability +# to check that you get the expected behavior +### Important Warning ### + +from __future__ import absolute_import +import models.build_restriction_lib as build_restrict_lib +all_categories = [] + + +first_category = build_restrict_lib.Category('sm customization') +all_categories.append(first_category) + +first_category.add_options(name='diagonal ckm', # name + default=True, # default + rules=[('WOLFENSTEIN', [1], 0.0), + ('WOLFENSTEIN', [2], 0.0), + ('WOLFENSTEIN', [3], 0.0), + ('WOLFENSTEIN', [4], 0.0)] + ) + +first_category.add_options(name='c mass = 0', # name + default=True, # default + rules=[('MASS', [4], 0.0), + ('YUKAWA', [4], 0.0)] + ) + +first_category.add_options(name='b mass = 0', + default=False, + rules=[('MASS', [5], 0.0), + ('YUKAWA', [5], 0.0)] + ) + +first_category.add_options(name='tau mass = 0', + default=False, + rules=[('MASS', [15], 0.0), + ('YUKAWA', [15], 0.0)] + ) + +first_category.add_options(name='muon mass = 0', + default=True, + rules=[('MASS', [13], 0.0), + ('YUKAWA', [13], 0.0)] + ) + +first_category.add_options(name='electron mass = 0', + default=True, + rules=[('MASS', [11], 0.0), + ('YUKAWA', [11], 0.0)] + ) diff --git a/pyamplicol/src/pyamplicol/assets/models/ufo/sm/coupling_orders.py b/pyamplicol/src/pyamplicol/assets/models/ufo/sm/coupling_orders.py new file mode 100644 index 0000000000..3a0bd46b64 --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/models/ufo/sm/coupling_orders.py @@ -0,0 +1,17 @@ +# type: ignore +# This file was automatically created by FeynRules 1.7.69 +# Mathematica version: 8.0 for Mac OS X x86 (64-bit) (November 6, 2010) +# Date: Mon 1 Oct 2012 14:58:26 + + +from __future__ import absolute_import +from .object_library import all_orders, CouplingOrder + + +QCD = CouplingOrder(name='QCD', + expansion_order=99, + hierarchy=1) + +QED = CouplingOrder(name='QED', + expansion_order=99, + hierarchy=2) diff --git a/pyamplicol/src/pyamplicol/assets/models/ufo/sm/couplings.py b/pyamplicol/src/pyamplicol/assets/models/ufo/sm/couplings.py new file mode 100644 index 0000000000..5e3e67ffd2 --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/models/ufo/sm/couplings.py @@ -0,0 +1,443 @@ +# This file was automatically created by FeynRules 1.7.69 +# Mathematica version: 8.0 for Mac OS X x86 (64-bit) (November 6, 2010) +# Date: Mon 1 Oct 2012 14:58:26 + + +from __future__ import absolute_import +from .object_library import all_couplings, Coupling + +from .function_library import complexconjugate, re, im, csc, sec, acsc, asec + + + +GC_1 = Coupling(name = 'GC_1', + value = '-(ee*complex(0,1))/3.', + order = {'QED':1}) + +GC_2 = Coupling(name = 'GC_2', + value = '(2*ee*complex(0,1))/3.', + order = {'QED':1}) + +GC_3 = Coupling(name = 'GC_3', + value = '-(ee*complex(0,1))', + order = {'QED':1}) + +GC_4 = Coupling(name = 'GC_4', + value = 'ee*complex(0,1)', + order = {'QED':1}) + +GC_5 = Coupling(name = 'GC_5', + value = 'ee**2*complex(0,1)', + order = {'QED':2}) + +GC_6 = Coupling(name = 'GC_6', + value = '2*ee**2*complex(0,1)', + order = {'QED':2}) + +GC_7 = Coupling(name = 'GC_7', + value = '-ee**2/(2.*cw)', + order = {'QED':2}) + +GC_8 = Coupling(name = 'GC_8', + value = '(ee**2*complex(0,1))/(2.*cw)', + order = {'QED':2}) + +GC_9 = Coupling(name = 'GC_9', + value = 'ee**2/(2.*cw)', + order = {'QED':2}) + +GC_10 = Coupling(name = 'GC_10', + value = '-G', + order = {'QCD':1}) + +GC_11 = Coupling(name = 'GC_11', + value = 'complex(0,1)*G', + order = {'QCD':1}) + +GC_12 = Coupling(name = 'GC_12', + value = 'complex(0,1)*G**2', + order = {'QCD':2}) + +GC_13 = Coupling(name = 'GC_13', + value = 'I1x31', + order = {'QED':1}) + +GC_14 = Coupling(name = 'GC_14', + value = 'I1x32', + order = {'QED':1}) + +GC_15 = Coupling(name = 'GC_15', + value = 'I1x33', + order = {'QED':1}) + +GC_16 = Coupling(name = 'GC_16', + value = '-I2x12', + order = {'QED':1}) + +GC_17 = Coupling(name = 'GC_17', + value = '-I2x13', + order = {'QED':1}) + +GC_18 = Coupling(name = 'GC_18', + value = '-I2x22', + order = {'QED':1}) + +GC_19 = Coupling(name = 'GC_19', + value = '-I2x23', + order = {'QED':1}) + +GC_20 = Coupling(name = 'GC_20', + value = '-I2x32', + order = {'QED':1}) + +GC_21 = Coupling(name = 'GC_21', + value = '-I2x33', + order = {'QED':1}) + +GC_22 = Coupling(name = 'GC_22', + value = 'I3x21', + order = {'QED':1}) + +GC_23 = Coupling(name = 'GC_23', + value = 'I3x22', + order = {'QED':1}) + +GC_24 = Coupling(name = 'GC_24', + value = 'I3x23', + order = {'QED':1}) + +GC_25 = Coupling(name = 'GC_25', + value = 'I3x31', + order = {'QED':1}) + +GC_26 = Coupling(name = 'GC_26', + value = 'I3x32', + order = {'QED':1}) + +GC_27 = Coupling(name = 'GC_27', + value = 'I3x33', + order = {'QED':1}) + +GC_28 = Coupling(name = 'GC_28', + value = '-I4x13', + order = {'QED':1}) + +GC_29 = Coupling(name = 'GC_29', + value = '-I4x23', + order = {'QED':1}) + +GC_30 = Coupling(name = 'GC_30', + value = '-I4x33', + order = {'QED':1}) + +GC_31 = Coupling(name = 'GC_31', + value = '-2*complex(0,1)*lam', + order = {'QED':2}) + +GC_32 = Coupling(name = 'GC_32', + value = '-4*complex(0,1)*lam', + order = {'QED':2}) + +GC_33 = Coupling(name = 'GC_33', + value = '-6*complex(0,1)*lam', + order = {'QED':2}) + +GC_34 = Coupling(name = 'GC_34', + value = '(ee**2*complex(0,1))/(2.*sw**2)', + order = {'QED':2}) + +GC_35 = Coupling(name = 'GC_35', + value = '-((ee**2*complex(0,1))/sw**2)', + order = {'QED':2}) + +GC_36 = Coupling(name = 'GC_36', + value = '(cw**2*ee**2*complex(0,1))/sw**2', + order = {'QED':2}) + +GC_37 = Coupling(name = 'GC_37', + value = '-ee/(2.*sw)', + order = {'QED':1}) + +GC_38 = Coupling(name = 'GC_38', + value = '-(ee*complex(0,1))/(2.*sw)', + order = {'QED':1}) + +GC_39 = Coupling(name = 'GC_39', + value = '(ee*complex(0,1))/(2.*sw)', + order = {'QED':1}) + +GC_40 = Coupling(name = 'GC_40', + value = '(ee*complex(0,1))/(sw*cmath.sqrt(2))', + order = {'QED':1}) + +GC_41 = Coupling(name = 'GC_41', + value = '(CKM1x1*ee*complex(0,1))/(sw*cmath.sqrt(2))', + order = {'QED':1}) + +GC_42 = Coupling(name = 'GC_42', + value = '(CKM1x2*ee*complex(0,1))/(sw*cmath.sqrt(2))', + order = {'QED':1}) + +GC_43 = Coupling(name = 'GC_43', + value = '(CKM1x3*ee*complex(0,1))/(sw*cmath.sqrt(2))', + order = {'QED':1}) + +GC_44 = Coupling(name = 'GC_44', + value = '(CKM2x1*ee*complex(0,1))/(sw*cmath.sqrt(2))', + order = {'QED':1}) + +GC_45 = Coupling(name = 'GC_45', + value = '(CKM2x2*ee*complex(0,1))/(sw*cmath.sqrt(2))', + order = {'QED':1}) + +GC_46 = Coupling(name = 'GC_46', + value = '(CKM2x3*ee*complex(0,1))/(sw*cmath.sqrt(2))', + order = {'QED':1}) + +GC_47 = Coupling(name = 'GC_47', + value = '(CKM3x1*ee*complex(0,1))/(sw*cmath.sqrt(2))', + order = {'QED':1}) + +GC_48 = Coupling(name = 'GC_48', + value = '(CKM3x2*ee*complex(0,1))/(sw*cmath.sqrt(2))', + order = {'QED':1}) + +GC_49 = Coupling(name = 'GC_49', + value = '(CKM3x3*ee*complex(0,1))/(sw*cmath.sqrt(2))', + order = {'QED':1}) + +GC_50 = Coupling(name = 'GC_50', + value = '-(cw*ee*complex(0,1))/(2.*sw)', + order = {'QED':1}) + +GC_51 = Coupling(name = 'GC_51', + value = '(cw*ee*complex(0,1))/(2.*sw)', + order = {'QED':1}) + +GC_52 = Coupling(name = 'GC_52', + value = '-((cw*ee*complex(0,1))/sw)', + order = {'QED':1}) + +GC_53 = Coupling(name = 'GC_53', + value = '(cw*ee*complex(0,1))/sw', + order = {'QED':1}) + +GC_54 = Coupling(name = 'GC_54', + value = '-ee**2/(2.*sw)', + order = {'QED':2}) + +GC_55 = Coupling(name = 'GC_55', + value = '-(ee**2*complex(0,1))/(2.*sw)', + order = {'QED':2}) + +GC_56 = Coupling(name = 'GC_56', + value = 'ee**2/(2.*sw)', + order = {'QED':2}) + +GC_57 = Coupling(name = 'GC_57', + value = '(-2*cw*ee**2*complex(0,1))/sw', + order = {'QED':2}) + +GC_58 = Coupling(name = 'GC_58', + value = '-(ee*complex(0,1)*sw)/(6.*cw)', + order = {'QED':1}) + +GC_59 = Coupling(name = 'GC_59', + value = '(ee*complex(0,1)*sw)/(2.*cw)', + order = {'QED':1}) + +GC_60 = Coupling(name = 'GC_60', + value = '-(cw*ee)/(2.*sw) - (ee*sw)/(2.*cw)', + order = {'QED':1}) + +GC_61 = Coupling(name = 'GC_61', + value = '-(cw*ee*complex(0,1))/(2.*sw) + (ee*complex(0,1)*sw)/(2.*cw)', + order = {'QED':1}) + +GC_62 = Coupling(name = 'GC_62', + value = '(cw*ee*complex(0,1))/(2.*sw) + (ee*complex(0,1)*sw)/(2.*cw)', + order = {'QED':1}) + +GC_63 = Coupling(name = 'GC_63', + value = '(cw*ee**2*complex(0,1))/sw - (ee**2*complex(0,1)*sw)/cw', + order = {'QED':2}) + +GC_64 = Coupling(name = 'GC_64', + value = '-(ee**2*complex(0,1)) + (cw**2*ee**2*complex(0,1))/(2.*sw**2) + (ee**2*complex(0,1)*sw**2)/(2.*cw**2)', + order = {'QED':2}) + +GC_65 = Coupling(name = 'GC_65', + value = 'ee**2*complex(0,1) + (cw**2*ee**2*complex(0,1))/(2.*sw**2) + (ee**2*complex(0,1)*sw**2)/(2.*cw**2)', + order = {'QED':2}) + +GC_66 = Coupling(name = 'GC_66', + value = '-(ee**2*vev)/(2.*cw)', + order = {'QED':1}) + +GC_67 = Coupling(name = 'GC_67', + value = '(ee**2*vev)/(2.*cw)', + order = {'QED':1}) + +GC_68 = Coupling(name = 'GC_68', + value = '-2*complex(0,1)*lam*vev', + order = {'QED':1}) + +GC_69 = Coupling(name = 'GC_69', + value = '-6*complex(0,1)*lam*vev', + order = {'QED':1}) + +GC_70 = Coupling(name = 'GC_70', + value = '-(ee**2*vev)/(4.*sw**2)', + order = {'QED':1}) + +GC_71 = Coupling(name = 'GC_71', + value = '-(ee**2*complex(0,1)*vev)/(4.*sw**2)', + order = {'QED':1}) + +GC_72 = Coupling(name = 'GC_72', + value = '(ee**2*complex(0,1)*vev)/(2.*sw**2)', + order = {'QED':1}) + +GC_73 = Coupling(name = 'GC_73', + value = '(ee**2*vev)/(4.*sw**2)', + order = {'QED':1}) + +GC_74 = Coupling(name = 'GC_74', + value = '-(ee**2*vev)/(2.*sw)', + order = {'QED':1}) + +GC_75 = Coupling(name = 'GC_75', + value = '(ee**2*vev)/(2.*sw)', + order = {'QED':1}) + +GC_76 = Coupling(name = 'GC_76', + value = '-(ee**2*vev)/(4.*cw) - (cw*ee**2*vev)/(4.*sw**2)', + order = {'QED':1}) + +GC_77 = Coupling(name = 'GC_77', + value = '(ee**2*vev)/(4.*cw) - (cw*ee**2*vev)/(4.*sw**2)', + order = {'QED':1}) + +GC_78 = Coupling(name = 'GC_78', + value = '-(ee**2*vev)/(4.*cw) + (cw*ee**2*vev)/(4.*sw**2)', + order = {'QED':1}) + +GC_79 = Coupling(name = 'GC_79', + value = '(ee**2*vev)/(4.*cw) + (cw*ee**2*vev)/(4.*sw**2)', + order = {'QED':1}) + +GC_80 = Coupling(name = 'GC_80', + value = '-(ee**2*complex(0,1)*vev)/2. - (cw**2*ee**2*complex(0,1)*vev)/(4.*sw**2) - (ee**2*complex(0,1)*sw**2*vev)/(4.*cw**2)', + order = {'QED':1}) + +GC_81 = Coupling(name = 'GC_81', + value = 'ee**2*complex(0,1)*vev + (cw**2*ee**2*complex(0,1)*vev)/(2.*sw**2) + (ee**2*complex(0,1)*sw**2*vev)/(2.*cw**2)', + order = {'QED':1}) + +GC_82 = Coupling(name = 'GC_82', + value = '-(yb/cmath.sqrt(2))', + order = {'QED':1}) + +GC_83 = Coupling(name = 'GC_83', + value = '-((complex(0,1)*yb)/cmath.sqrt(2))', + order = {'QED':1}) + +GC_84 = Coupling(name = 'GC_84', + value = '-((complex(0,1)*yc)/cmath.sqrt(2))', + order = {'QED':1}) + +GC_85 = Coupling(name = 'GC_85', + value = 'yc/cmath.sqrt(2)', + order = {'QED':1}) + +GC_86 = Coupling(name = 'GC_86', + value = '-ye', + order = {'QED':1}) + +GC_87 = Coupling(name = 'GC_87', + value = 'ye', + order = {'QED':1}) + +GC_88 = Coupling(name = 'GC_88', + value = '-(ye/cmath.sqrt(2))', + order = {'QED':1}) + +GC_89 = Coupling(name = 'GC_89', + value = '-((complex(0,1)*ye)/cmath.sqrt(2))', + order = {'QED':1}) + +GC_90 = Coupling(name = 'GC_90', + value = '-ym', + order = {'QED':1}) + +GC_91 = Coupling(name = 'GC_91', + value = 'ym', + order = {'QED':1}) + +GC_92 = Coupling(name = 'GC_92', + value = '-(ym/cmath.sqrt(2))', + order = {'QED':1}) + +GC_93 = Coupling(name = 'GC_93', + value = '-((complex(0,1)*ym)/cmath.sqrt(2))', + order = {'QED':1}) + +GC_94 = Coupling(name = 'GC_94', + value = '-((complex(0,1)*yt)/cmath.sqrt(2))', + order = {'QED':1}) + +GC_95 = Coupling(name = 'GC_95', + value = 'yt/cmath.sqrt(2)', + order = {'QED':1}) + +GC_96 = Coupling(name = 'GC_96', + value = '-ytau', + order = {'QED':1}) + +GC_97 = Coupling(name = 'GC_97', + value = 'ytau', + order = {'QED':1}) + +GC_98 = Coupling(name = 'GC_98', + value = '-(ytau/cmath.sqrt(2))', + order = {'QED':1}) + +GC_99 = Coupling(name = 'GC_99', + value = '-((complex(0,1)*ytau)/cmath.sqrt(2))', + order = {'QED':1}) + +GC_100 = Coupling(name = 'GC_100', + value = '(ee*complex(0,1)*complexconjugate(CKM1x1))/(sw*cmath.sqrt(2))', + order = {'QED':1}) + +GC_101 = Coupling(name = 'GC_101', + value = '(ee*complex(0,1)*complexconjugate(CKM1x2))/(sw*cmath.sqrt(2))', + order = {'QED':1}) + +GC_102 = Coupling(name = 'GC_102', + value = '(ee*complex(0,1)*complexconjugate(CKM1x3))/(sw*cmath.sqrt(2))', + order = {'QED':1}) + +GC_103 = Coupling(name = 'GC_103', + value = '(ee*complex(0,1)*complexconjugate(CKM2x1))/(sw*cmath.sqrt(2))', + order = {'QED':1}) + +GC_104 = Coupling(name = 'GC_104', + value = '(ee*complex(0,1)*complexconjugate(CKM2x2))/(sw*cmath.sqrt(2))', + order = {'QED':1}) + +GC_105 = Coupling(name = 'GC_105', + value = '(ee*complex(0,1)*complexconjugate(CKM2x3))/(sw*cmath.sqrt(2))', + order = {'QED':1}) + +GC_106 = Coupling(name = 'GC_106', + value = '(ee*complex(0,1)*complexconjugate(CKM3x1))/(sw*cmath.sqrt(2))', + order = {'QED':1}) + +GC_107 = Coupling(name = 'GC_107', + value = '(ee*complex(0,1)*complexconjugate(CKM3x2))/(sw*cmath.sqrt(2))', + order = {'QED':1}) + +GC_108 = Coupling(name = 'GC_108', + value = '(ee*complex(0,1)*complexconjugate(CKM3x3))/(sw*cmath.sqrt(2))', + order = {'QED':1}) diff --git a/pyamplicol/src/pyamplicol/assets/models/ufo/sm/decays.py b/pyamplicol/src/pyamplicol/assets/models/ufo/sm/decays.py new file mode 100644 index 0000000000..23af667e4c --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/models/ufo/sm/decays.py @@ -0,0 +1,81 @@ +# This file was automatically created by FeynRules 1.7.69 +# Mathematica version: 8.0 for Mac OS X x86 (64-bit) (November 6, 2010) +# Date: Mon 1 Oct 2012 14:58:26 + + +from __future__ import absolute_import +from .object_library import all_decays, Decay +from . import particles as P + + +Decay_H = Decay(name = 'Decay_H', + particle = P.H, + partial_widths = {(P.W__minus__,P.W__plus__):'(((3*ee**4*vev**2)/(4.*sw**4) + (ee**4*MH**4*vev**2)/(16.*MW**4*sw**4) - (ee**4*MH**2*vev**2)/(4.*MW**2*sw**4))*cmath.sqrt(MH**4 - 4*MH**2*MW**2))/(16.*cmath.pi*abs(MH)**3)', + (P.Z,P.Z):'(((9*ee**4*vev**2)/2. + (3*ee**4*MH**4*vev**2)/(8.*MZ**4) - (3*ee**4*MH**2*vev**2)/(2.*MZ**2) + (3*cw**4*ee**4*vev**2)/(4.*sw**4) + (cw**4*ee**4*MH**4*vev**2)/(16.*MZ**4*sw**4) - (cw**4*ee**4*MH**2*vev**2)/(4.*MZ**2*sw**4) + (3*cw**2*ee**4*vev**2)/sw**2 + (cw**2*ee**4*MH**4*vev**2)/(4.*MZ**4*sw**2) - (cw**2*ee**4*MH**2*vev**2)/(MZ**2*sw**2) + (3*ee**4*sw**2*vev**2)/cw**2 + (ee**4*MH**4*sw**2*vev**2)/(4.*cw**2*MZ**4) - (ee**4*MH**2*sw**2*vev**2)/(cw**2*MZ**2) + (3*ee**4*sw**4*vev**2)/(4.*cw**4) + (ee**4*MH**4*sw**4*vev**2)/(16.*cw**4*MZ**4) - (ee**4*MH**2*sw**4*vev**2)/(4.*cw**4*MZ**2))*cmath.sqrt(MH**4 - 4*MH**2*MZ**2))/(32.*cmath.pi*abs(MH)**3)', + (P.b,P.b__tilde__):'((-12*MB**2*yb**2 + 3*MH**2*yb**2)*cmath.sqrt(-4*MB**2*MH**2 + MH**4))/(16.*cmath.pi*abs(MH)**3)', + (P.e__minus__,P.e__plus__):'((-4*Me**2*ye**2 + MH**2*ye**2)*cmath.sqrt(-4*Me**2*MH**2 + MH**4))/(16.*cmath.pi*abs(MH)**3)', + (P.mu__minus__,P.mu__plus__):'((MH**2*ym**2 - 4*MM**2*ym**2)*cmath.sqrt(MH**4 - 4*MH**2*MM**2))/(16.*cmath.pi*abs(MH)**3)', + (P.ta__minus__,P.ta__plus__):'((MH**2*ytau**2 - 4*MTA**2*ytau**2)*cmath.sqrt(MH**4 - 4*MH**2*MTA**2))/(16.*cmath.pi*abs(MH)**3)', + (P.c,P.c__tilde__):'((-12*MC**2*yc**2 + 3*MH**2*yc**2)*cmath.sqrt(-4*MC**2*MH**2 + MH**4))/(16.*cmath.pi*abs(MH)**3)', + (P.t,P.t__tilde__):'((3*MH**2*yt**2 - 12*MT**2*yt**2)*cmath.sqrt(MH**4 - 4*MH**2*MT**2))/(16.*cmath.pi*abs(MH)**3)'}) + +Decay_Z = Decay(name = 'Decay_Z', + particle = P.Z, + partial_widths = {(P.W__minus__,P.W__plus__):'(((-12*cw**2*ee**2*MW**2)/sw**2 - (17*cw**2*ee**2*MZ**2)/sw**2 + (4*cw**2*ee**2*MZ**4)/(MW**2*sw**2) + (cw**2*ee**2*MZ**6)/(4.*MW**4*sw**2))*cmath.sqrt(-4*MW**2*MZ**2 + MZ**4))/(48.*cmath.pi*abs(MZ)**3)', + (P.d,P.d__tilde__):'(MZ**2*(ee**2*MZ**2 + (3*cw**2*ee**2*MZ**2)/(2.*sw**2) + (5*ee**2*MZ**2*sw**2)/(6.*cw**2)))/(48.*cmath.pi*abs(MZ)**3)', + (P.s,P.s__tilde__):'(MZ**2*(ee**2*MZ**2 + (3*cw**2*ee**2*MZ**2)/(2.*sw**2) + (5*ee**2*MZ**2*sw**2)/(6.*cw**2)))/(48.*cmath.pi*abs(MZ)**3)', + (P.b,P.b__tilde__):'((-7*ee**2*MB**2 + ee**2*MZ**2 - (3*cw**2*ee**2*MB**2)/(2.*sw**2) + (3*cw**2*ee**2*MZ**2)/(2.*sw**2) - (17*ee**2*MB**2*sw**2)/(6.*cw**2) + (5*ee**2*MZ**2*sw**2)/(6.*cw**2))*cmath.sqrt(-4*MB**2*MZ**2 + MZ**4))/(48.*cmath.pi*abs(MZ)**3)', + (P.e__minus__,P.e__plus__):'((-5*ee**2*Me**2 - ee**2*MZ**2 - (cw**2*ee**2*Me**2)/(2.*sw**2) + (cw**2*ee**2*MZ**2)/(2.*sw**2) + (7*ee**2*Me**2*sw**2)/(2.*cw**2) + (5*ee**2*MZ**2*sw**2)/(2.*cw**2))*cmath.sqrt(-4*Me**2*MZ**2 + MZ**4))/(48.*cmath.pi*abs(MZ)**3)', + (P.mu__minus__,P.mu__plus__):'((-5*ee**2*MM**2 - ee**2*MZ**2 - (cw**2*ee**2*MM**2)/(2.*sw**2) + (cw**2*ee**2*MZ**2)/(2.*sw**2) + (7*ee**2*MM**2*sw**2)/(2.*cw**2) + (5*ee**2*MZ**2*sw**2)/(2.*cw**2))*cmath.sqrt(-4*MM**2*MZ**2 + MZ**4))/(48.*cmath.pi*abs(MZ)**3)', + (P.ta__minus__,P.ta__plus__):'((-5*ee**2*MTA**2 - ee**2*MZ**2 - (cw**2*ee**2*MTA**2)/(2.*sw**2) + (cw**2*ee**2*MZ**2)/(2.*sw**2) + (7*ee**2*MTA**2*sw**2)/(2.*cw**2) + (5*ee**2*MZ**2*sw**2)/(2.*cw**2))*cmath.sqrt(-4*MTA**2*MZ**2 + MZ**4))/(48.*cmath.pi*abs(MZ)**3)', + (P.u,P.u__tilde__):'(MZ**2*(-(ee**2*MZ**2) + (3*cw**2*ee**2*MZ**2)/(2.*sw**2) + (17*ee**2*MZ**2*sw**2)/(6.*cw**2)))/(48.*cmath.pi*abs(MZ)**3)', + (P.c,P.c__tilde__):'((-11*ee**2*MC**2 - ee**2*MZ**2 - (3*cw**2*ee**2*MC**2)/(2.*sw**2) + (3*cw**2*ee**2*MZ**2)/(2.*sw**2) + (7*ee**2*MC**2*sw**2)/(6.*cw**2) + (17*ee**2*MZ**2*sw**2)/(6.*cw**2))*cmath.sqrt(-4*MC**2*MZ**2 + MZ**4))/(48.*cmath.pi*abs(MZ)**3)', + (P.t,P.t__tilde__):'((-11*ee**2*MT**2 - ee**2*MZ**2 - (3*cw**2*ee**2*MT**2)/(2.*sw**2) + (3*cw**2*ee**2*MZ**2)/(2.*sw**2) + (7*ee**2*MT**2*sw**2)/(6.*cw**2) + (17*ee**2*MZ**2*sw**2)/(6.*cw**2))*cmath.sqrt(-4*MT**2*MZ**2 + MZ**4))/(48.*cmath.pi*abs(MZ)**3)', + (P.ve,P.ve__tilde__):'(MZ**2*(ee**2*MZ**2 + (cw**2*ee**2*MZ**2)/(2.*sw**2) + (ee**2*MZ**2*sw**2)/(2.*cw**2)))/(48.*cmath.pi*abs(MZ)**3)', + (P.vm,P.vm__tilde__):'(MZ**2*(ee**2*MZ**2 + (cw**2*ee**2*MZ**2)/(2.*sw**2) + (ee**2*MZ**2*sw**2)/(2.*cw**2)))/(48.*cmath.pi*abs(MZ)**3)', + (P.vt,P.vt__tilde__):'(MZ**2*(ee**2*MZ**2 + (cw**2*ee**2*MZ**2)/(2.*sw**2) + (ee**2*MZ**2*sw**2)/(2.*cw**2)))/(48.*cmath.pi*abs(MZ)**3)'}) + +Decay_c = Decay(name = 'Decay_c', + particle = P.c, + partial_widths = {(P.W__plus__,P.d):'((MC**2 - MW**2)*((3*CKM2x1*ee**2*MC**2*complexconjugate(CKM2x1))/(2.*sw**2) + (3*CKM2x1*ee**2*MC**4*complexconjugate(CKM2x1))/(2.*MW**2*sw**2) - (3*CKM2x1*ee**2*MW**2*complexconjugate(CKM2x1))/sw**2))/(96.*cmath.pi*abs(MC)**3)', + (P.W__plus__,P.s):'((MC**2 - MW**2)*((3*CKM2x2*ee**2*MC**2*complexconjugate(CKM2x2))/(2.*sw**2) + (3*CKM2x2*ee**2*MC**4*complexconjugate(CKM2x2))/(2.*MW**2*sw**2) - (3*CKM2x2*ee**2*MW**2*complexconjugate(CKM2x2))/sw**2))/(96.*cmath.pi*abs(MC)**3)', + (P.W__plus__,P.b):'(((3*CKM2x3*ee**2*MB**2*complexconjugate(CKM2x3))/(2.*sw**2) + (3*CKM2x3*ee**2*MC**2*complexconjugate(CKM2x3))/(2.*sw**2) + (3*CKM2x3*ee**2*MB**4*complexconjugate(CKM2x3))/(2.*MW**2*sw**2) - (3*CKM2x3*ee**2*MB**2*MC**2*complexconjugate(CKM2x3))/(MW**2*sw**2) + (3*CKM2x3*ee**2*MC**4*complexconjugate(CKM2x3))/(2.*MW**2*sw**2) - (3*CKM2x3*ee**2*MW**2*complexconjugate(CKM2x3))/sw**2)*cmath.sqrt(MB**4 - 2*MB**2*MC**2 + MC**4 - 2*MB**2*MW**2 - 2*MC**2*MW**2 + MW**4))/(96.*cmath.pi*abs(MC)**3)'}) + +Decay_t = Decay(name = 'Decay_t', + particle = P.t, + partial_widths = {(P.W__plus__,P.d):'((MT**2 - MW**2)*((3*CKM3x1*ee**2*MT**2*complexconjugate(CKM3x1))/(2.*sw**2) + (3*CKM3x1*ee**2*MT**4*complexconjugate(CKM3x1))/(2.*MW**2*sw**2) - (3*CKM3x1*ee**2*MW**2*complexconjugate(CKM3x1))/sw**2))/(96.*cmath.pi*abs(MT)**3)', + (P.W__plus__,P.s):'((MT**2 - MW**2)*((3*CKM3x2*ee**2*MT**2*complexconjugate(CKM3x2))/(2.*sw**2) + (3*CKM3x2*ee**2*MT**4*complexconjugate(CKM3x2))/(2.*MW**2*sw**2) - (3*CKM3x2*ee**2*MW**2*complexconjugate(CKM3x2))/sw**2))/(96.*cmath.pi*abs(MT)**3)', + (P.W__plus__,P.b):'(((3*CKM3x3*ee**2*MB**2*complexconjugate(CKM3x3))/(2.*sw**2) + (3*CKM3x3*ee**2*MT**2*complexconjugate(CKM3x3))/(2.*sw**2) + (3*CKM3x3*ee**2*MB**4*complexconjugate(CKM3x3))/(2.*MW**2*sw**2) - (3*CKM3x3*ee**2*MB**2*MT**2*complexconjugate(CKM3x3))/(MW**2*sw**2) + (3*CKM3x3*ee**2*MT**4*complexconjugate(CKM3x3))/(2.*MW**2*sw**2) - (3*CKM3x3*ee**2*MW**2*complexconjugate(CKM3x3))/sw**2)*cmath.sqrt(MB**4 - 2*MB**2*MT**2 + MT**4 - 2*MB**2*MW**2 - 2*MT**2*MW**2 + MW**4))/(96.*cmath.pi*abs(MT)**3)'}) + +Decay_W__plus__ = Decay(name = 'Decay_W__plus__', + particle = P.W__plus__, + partial_widths = {(P.u,P.d__tilde__):'(CKM1x1*ee**2*MW**4*complexconjugate(CKM1x1))/(16.*cmath.pi*sw**2*abs(MW)**3)', + (P.u,P.s__tilde__):'(CKM1x2*ee**2*MW**4*complexconjugate(CKM1x2))/(16.*cmath.pi*sw**2*abs(MW)**3)', + (P.u,P.b__tilde__):'((-MB**2 + MW**2)*((-3*CKM1x3*ee**2*MB**2*complexconjugate(CKM1x3))/(2.*sw**2) - (3*CKM1x3*ee**2*MB**4*complexconjugate(CKM1x3))/(2.*MW**2*sw**2) + (3*CKM1x3*ee**2*MW**2*complexconjugate(CKM1x3))/sw**2))/(48.*cmath.pi*abs(MW)**3)', + (P.c,P.d__tilde__):'((-MC**2 + MW**2)*((-3*CKM2x1*ee**2*MC**2*complexconjugate(CKM2x1))/(2.*sw**2) - (3*CKM2x1*ee**2*MC**4*complexconjugate(CKM2x1))/(2.*MW**2*sw**2) + (3*CKM2x1*ee**2*MW**2*complexconjugate(CKM2x1))/sw**2))/(48.*cmath.pi*abs(MW)**3)', + (P.c,P.s__tilde__):'((-MC**2 + MW**2)*((-3*CKM2x2*ee**2*MC**2*complexconjugate(CKM2x2))/(2.*sw**2) - (3*CKM2x2*ee**2*MC**4*complexconjugate(CKM2x2))/(2.*MW**2*sw**2) + (3*CKM2x2*ee**2*MW**2*complexconjugate(CKM2x2))/sw**2))/(48.*cmath.pi*abs(MW)**3)', + (P.c,P.b__tilde__):'(((-3*CKM2x3*ee**2*MB**2*complexconjugate(CKM2x3))/(2.*sw**2) - (3*CKM2x3*ee**2*MC**2*complexconjugate(CKM2x3))/(2.*sw**2) - (3*CKM2x3*ee**2*MB**4*complexconjugate(CKM2x3))/(2.*MW**2*sw**2) + (3*CKM2x3*ee**2*MB**2*MC**2*complexconjugate(CKM2x3))/(MW**2*sw**2) - (3*CKM2x3*ee**2*MC**4*complexconjugate(CKM2x3))/(2.*MW**2*sw**2) + (3*CKM2x3*ee**2*MW**2*complexconjugate(CKM2x3))/sw**2)*cmath.sqrt(MB**4 - 2*MB**2*MC**2 + MC**4 - 2*MB**2*MW**2 - 2*MC**2*MW**2 + MW**4))/(48.*cmath.pi*abs(MW)**3)', + (P.t,P.d__tilde__):'((-MT**2 + MW**2)*((-3*CKM3x1*ee**2*MT**2*complexconjugate(CKM3x1))/(2.*sw**2) - (3*CKM3x1*ee**2*MT**4*complexconjugate(CKM3x1))/(2.*MW**2*sw**2) + (3*CKM3x1*ee**2*MW**2*complexconjugate(CKM3x1))/sw**2))/(48.*cmath.pi*abs(MW)**3)', + (P.t,P.s__tilde__):'((-MT**2 + MW**2)*((-3*CKM3x2*ee**2*MT**2*complexconjugate(CKM3x2))/(2.*sw**2) - (3*CKM3x2*ee**2*MT**4*complexconjugate(CKM3x2))/(2.*MW**2*sw**2) + (3*CKM3x2*ee**2*MW**2*complexconjugate(CKM3x2))/sw**2))/(48.*cmath.pi*abs(MW)**3)', + (P.t,P.b__tilde__):'(((-3*CKM3x3*ee**2*MB**2*complexconjugate(CKM3x3))/(2.*sw**2) - (3*CKM3x3*ee**2*MT**2*complexconjugate(CKM3x3))/(2.*sw**2) - (3*CKM3x3*ee**2*MB**4*complexconjugate(CKM3x3))/(2.*MW**2*sw**2) + (3*CKM3x3*ee**2*MB**2*MT**2*complexconjugate(CKM3x3))/(MW**2*sw**2) - (3*CKM3x3*ee**2*MT**4*complexconjugate(CKM3x3))/(2.*MW**2*sw**2) + (3*CKM3x3*ee**2*MW**2*complexconjugate(CKM3x3))/sw**2)*cmath.sqrt(MB**4 - 2*MB**2*MT**2 + MT**4 - 2*MB**2*MW**2 - 2*MT**2*MW**2 + MW**4))/(48.*cmath.pi*abs(MW)**3)', + (P.ve,P.e__plus__):'((-Me**2 + MW**2)*(-(ee**2*Me**2)/(2.*sw**2) - (ee**2*Me**4)/(2.*MW**2*sw**2) + (ee**2*MW**2)/sw**2))/(48.*cmath.pi*abs(MW)**3)', + (P.vm,P.mu__plus__):'((-MM**2 + MW**2)*(-(ee**2*MM**2)/(2.*sw**2) - (ee**2*MM**4)/(2.*MW**2*sw**2) + (ee**2*MW**2)/sw**2))/(48.*cmath.pi*abs(MW)**3)', + (P.vt,P.ta__plus__):'((-MTA**2 + MW**2)*(-(ee**2*MTA**2)/(2.*sw**2) - (ee**2*MTA**4)/(2.*MW**2*sw**2) + (ee**2*MW**2)/sw**2))/(48.*cmath.pi*abs(MW)**3)'}) + +Decay_b = Decay(name = 'Decay_b', + particle = P.b, + partial_widths = {(P.W__minus__,P.u):'((MB**2 - MW**2)*((3*CKM1x3*ee**2*MB**2*complexconjugate(CKM1x3))/(2.*sw**2) + (3*CKM1x3*ee**2*MB**4*complexconjugate(CKM1x3))/(2.*MW**2*sw**2) - (3*CKM1x3*ee**2*MW**2*complexconjugate(CKM1x3))/sw**2))/(96.*cmath.pi*abs(MB)**3)', + (P.W__minus__,P.c):'(((3*CKM2x3*ee**2*MB**2*complexconjugate(CKM2x3))/(2.*sw**2) + (3*CKM2x3*ee**2*MC**2*complexconjugate(CKM2x3))/(2.*sw**2) + (3*CKM2x3*ee**2*MB**4*complexconjugate(CKM2x3))/(2.*MW**2*sw**2) - (3*CKM2x3*ee**2*MB**2*MC**2*complexconjugate(CKM2x3))/(MW**2*sw**2) + (3*CKM2x3*ee**2*MC**4*complexconjugate(CKM2x3))/(2.*MW**2*sw**2) - (3*CKM2x3*ee**2*MW**2*complexconjugate(CKM2x3))/sw**2)*cmath.sqrt(MB**4 - 2*MB**2*MC**2 + MC**4 - 2*MB**2*MW**2 - 2*MC**2*MW**2 + MW**4))/(96.*cmath.pi*abs(MB)**3)', + (P.W__minus__,P.t):'(((3*CKM3x3*ee**2*MB**2*complexconjugate(CKM3x3))/(2.*sw**2) + (3*CKM3x3*ee**2*MT**2*complexconjugate(CKM3x3))/(2.*sw**2) + (3*CKM3x3*ee**2*MB**4*complexconjugate(CKM3x3))/(2.*MW**2*sw**2) - (3*CKM3x3*ee**2*MB**2*MT**2*complexconjugate(CKM3x3))/(MW**2*sw**2) + (3*CKM3x3*ee**2*MT**4*complexconjugate(CKM3x3))/(2.*MW**2*sw**2) - (3*CKM3x3*ee**2*MW**2*complexconjugate(CKM3x3))/sw**2)*cmath.sqrt(MB**4 - 2*MB**2*MT**2 + MT**4 - 2*MB**2*MW**2 - 2*MT**2*MW**2 + MW**4))/(96.*cmath.pi*abs(MB)**3)'}) + +Decay_e__minus__ = Decay(name = 'Decay_e__minus__', + particle = P.e__minus__, + partial_widths = {(P.W__minus__,P.ve):'((Me**2 - MW**2)*((ee**2*Me**2)/(2.*sw**2) + (ee**2*Me**4)/(2.*MW**2*sw**2) - (ee**2*MW**2)/sw**2))/(32.*cmath.pi*abs(Me)**3)'}) + +Decay_mu__minus__ = Decay(name = 'Decay_mu__minus__', + particle = P.mu__minus__, + partial_widths = {(P.W__minus__,P.vm):'((MM**2 - MW**2)*((ee**2*MM**2)/(2.*sw**2) + (ee**2*MM**4)/(2.*MW**2*sw**2) - (ee**2*MW**2)/sw**2))/(32.*cmath.pi*abs(MM)**3)'}) + +Decay_ta__minus__ = Decay(name = 'Decay_ta__minus__', + particle = P.ta__minus__, + partial_widths = {(P.W__minus__,P.vt):'((MTA**2 - MW**2)*((ee**2*MTA**2)/(2.*sw**2) + (ee**2*MTA**4)/(2.*MW**2*sw**2) - (ee**2*MW**2)/sw**2))/(32.*cmath.pi*abs(MTA)**3)'}) diff --git a/pyamplicol/src/pyamplicol/assets/models/ufo/sm/function_library.py b/pyamplicol/src/pyamplicol/assets/models/ufo/sm/function_library.py new file mode 100644 index 0000000000..33a8975a58 --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/models/ufo/sm/function_library.py @@ -0,0 +1,51 @@ +# This file is part of the UFO. +# +# This file contains definitions for functions that +# are extensions of the cmath library, and correspond +# either to functions that are in cmath, but inconvenient +# to access from there (e.g. z.conjugate()), +# or functions that are simply not defined. +# +# +from __future__ import absolute_import +__date__ = "22 July 2010" +__author__ = "claude.duhr@durham.ac.uk" + +from cmath import cos, sin, acos, asin +from .object_library import all_functions, Function + +# +# shortcuts for functions from cmath +# + +complexconjugate = Function(name = 'complexconjugate', + arguments = ('z',), + expression = 'z.conjugate()') + + +re = Function(name = 're', + arguments = ('z',), + expression = 'z.real') + +im = Function(name = 'im', + arguments = ('z',), + expression = 'z.imag') + + +# New functions (trigonometric) + +sec = Function(name = 'sec', + arguments = ('z',), + expression = '1./cos(z)') + +asec = Function(name = 'asec', + arguments = ('z',), + expression = 'acos(1./z)') + +csc = Function(name = 'csc', + arguments = ('z',), + expression = '1./sin(z)') + +acsc = Function(name = 'acsc', + arguments = ('z',), + expression = 'asin(1./z)') diff --git a/pyamplicol/src/pyamplicol/assets/models/ufo/sm/lorentz.py b/pyamplicol/src/pyamplicol/assets/models/ufo/sm/lorentz.py new file mode 100644 index 0000000000..b4221f8c4d --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/models/ufo/sm/lorentz.py @@ -0,0 +1,96 @@ +# type: ignore +# This file was automatically created by FeynRules 1.7.69 +# Mathematica version: 8.0 for Mac OS X x86 (64-bit) (November 6, 2010) +# Date: Mon 1 Oct 2012 14:58:26 + + +from __future__ import absolute_import + +from .function_library import acsc, asec, complexconjugate, csc, im, re, sec +from .object_library import Lorentz, all_lorentz + +try: + import form_factors as ForFac +except ImportError: + pass + + +UUS1 = Lorentz(name="UUS1", spins=[-1, -1, 1], structure="1") + +UUV1 = Lorentz(name="UUV1", spins=[-1, -1, 3], structure="-P(3,2)") + +SSS1 = Lorentz(name="SSS1", spins=[1, 1, 1], structure="1") + +FFS1 = Lorentz(name="FFS1", spins=[2, 2, 1], structure="ProjM(2,1)") + +FFS2 = Lorentz(name="FFS2", spins=[2, 2, 1], structure="ProjM(2,1) - ProjP(2,1)") + +FFS3 = Lorentz(name="FFS3", spins=[2, 2, 1], structure="ProjP(2,1)") + +FFS4 = Lorentz(name="FFS4", spins=[2, 2, 1], structure="ProjM(2,1) + ProjP(2,1)") + +FFV1 = Lorentz(name="FFV1", spins=[2, 2, 3], structure="Gamma(3,2,1)") + +FFV2 = Lorentz(name="FFV2", spins=[2, 2, 3], structure="Gamma(3,2,-1)*ProjM(-1,1)") + +FFV3 = Lorentz( + name="FFV3", + spins=[2, 2, 3], + structure="Gamma(3,2,-1)*ProjM(-1,1) - 2*Gamma(3,2,-1)*ProjP(-1,1)", +) + +FFV4 = Lorentz( + name="FFV4", + spins=[2, 2, 3], + structure="Gamma(3,2,-1)*ProjM(-1,1) + 2*Gamma(3,2,-1)*ProjP(-1,1)", +) + +FFV5 = Lorentz( + name="FFV5", + spins=[2, 2, 3], + structure="Gamma(3,2,-1)*ProjM(-1,1) + 4*Gamma(3,2,-1)*ProjP(-1,1)", +) + +VSS1 = Lorentz(name="VSS1", spins=[3, 1, 1], structure="P(1,2) - P(1,3)") + +VVS1 = Lorentz(name="VVS1", spins=[3, 3, 1], structure="Metric(1,2)") + +VVV1 = Lorentz( + name="VVV1", + spins=[3, 3, 3], + structure="P(3,1)*Metric(1,2) - P(3,2)*Metric(1,2) - P(2,1)*Metric(1,3) + P(2,3)*Metric(1,3) + P(1,2)*Metric(2,3) - P(1,3)*Metric(2,3)", +) + +SSSS1 = Lorentz(name="SSSS1", spins=[1, 1, 1, 1], structure="1") + +VVSS1 = Lorentz(name="VVSS1", spins=[3, 3, 1, 1], structure="Metric(1,2)") + +VVVV1 = Lorentz( + name="VVVV1", + spins=[3, 3, 3, 3], + structure="Metric(1,4)*Metric(2,3) - Metric(1,3)*Metric(2,4)", +) + +VVVV2 = Lorentz( + name="VVVV2", + spins=[3, 3, 3, 3], + structure="Metric(1,4)*Metric(2,3) + Metric(1,3)*Metric(2,4) - 2*Metric(1,2)*Metric(3,4)", +) + +VVVV3 = Lorentz( + name="VVVV3", + spins=[3, 3, 3, 3], + structure="Metric(1,4)*Metric(2,3) - Metric(1,2)*Metric(3,4)", +) + +VVVV4 = Lorentz( + name="VVVV4", + spins=[3, 3, 3, 3], + structure="Metric(1,3)*Metric(2,4) - Metric(1,2)*Metric(3,4)", +) + +VVVV5 = Lorentz( + name="VVVV5", + spins=[3, 3, 3, 3], + structure="Metric(1,4)*Metric(2,3) - (Metric(1,3)*Metric(2,4))/2. - (Metric(1,2)*Metric(3,4))/2.", +) diff --git a/pyamplicol/src/pyamplicol/assets/models/ufo/sm/object_library.py b/pyamplicol/src/pyamplicol/assets/models/ufo/sm/object_library.py new file mode 100644 index 0000000000..d63a4532dd --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/models/ufo/sm/object_library.py @@ -0,0 +1,270 @@ +## +## +## Feynrules Header +## +## +## +## +## + +from __future__ import absolute_import +import cmath + + +class UFOBaseClass(object): + """The class from which all FeynRules classes are derived.""" + + require_args = [] + + def __init__(self, *args, **options): + assert(len(self.require_args) == len (args)) + + for i, name in enumerate(self.require_args): + setattr(self, name, args[i]) + + for (option, value) in options.items(): + setattr(self, option, value) + + def get(self, name): + return getattr(self, name) + + def set(self, name, value): + setattr(self, name, value) + + def get_all(self): + """Return a dictionary containing all the information of the object""" + return self.__dict__ + + def __str__(self): + return self.name + + def nice_string(self): + """ return string with the full information """ + return '\n'.join(['%s \t: %s' %(name, value) for name, value in self.__dict__.items()]) + + def __repr__(self): + replacements = [ + ('+','__plus__'), + ('-','__minus__'), + ('@','__at__'), + ('!','__exclam__'), + ('?','__quest__'), + ('*','__star__'), + ('~','__tilde__') + ] + text = self.name + for orig,sub in replacements: + text = text.replace(orig,sub) + return text + + + +all_particles = [] + + + +class Particle(UFOBaseClass): + """A standard Particle""" + + require_args=['pdg_code', 'name', 'antiname', 'spin', 'color', 'mass', 'width', 'texname', 'antitexname', 'charge'] + + require_args_all = ['pdg_code', 'name', 'antiname', 'spin', 'color', 'mass', 'width', 'texname', 'antitexname', 'charge', 'line', 'propagating', 'goldstoneboson'] + + def __init__(self, pdg_code, name, antiname, spin, color, mass, width, texname, + antitexname, charge , line=None, propagating=True, goldstoneboson=False, **options): + + args= (pdg_code, name, antiname, spin, color, mass, width, texname, + antitexname, float(charge)) + + UFOBaseClass.__init__(self, *args, **options) + + global all_particles + all_particles.append(self) + + self.propagating = propagating + self.goldstoneboson= goldstoneboson + + self.selfconjugate = (name == antiname) + if 1: #not line: + self.line = self.find_line_type() + else: + self.line = line + + + + + def find_line_type(self): + """ find how we draw a line if not defined + valid output: dashed/straight/wavy/curly/double/swavy/scurly + """ + + spin = self.spin + color = self.color + + #use default + if spin == 1: + return 'dashed' + elif spin == 2: + if not self.selfconjugate: + return 'straight' + elif color == 1: + return 'swavy' + else: + return 'scurly' + elif spin == 3: + if color == 1: + return 'wavy' + + else: + return 'curly' + elif spin == 5: + return 'double' + elif spin == -1: + return 'dotted' + else: + return 'dashed' # not supported yet + + def anti(self): + if self.selfconjugate: + raise Exception('%s has no anti particle.' % self.name) + outdic = {} + for k,v in self.__dict__.items(): + if k not in self.require_args_all: + outdic[k] = -v + if self.color in [1,8]: + newcolor = self.color + else: + newcolor = -self.color + + return Particle(-self.pdg_code, self.antiname, self.name, self.spin, newcolor, self.mass, self.width, + self.antitexname, self.texname, -self.charge, self.line, self.propagating, self.goldstoneboson, **outdic) + + + +all_parameters = [] + +class Parameter(UFOBaseClass): + + require_args=['name', 'nature', 'type', 'value', 'texname'] + + def __init__(self, name, nature, type, value, texname, lhablock=None, lhacode=None): + + args = (name,nature,type,value,texname) + + UFOBaseClass.__init__(self, *args) + + args=(name,nature,type,value,texname) + + global all_parameters + all_parameters.append(self) + + if (lhablock is None or lhacode is None) and nature == 'external': + raise Exception('Need LHA information for external parameter "%s".' % name) + self.lhablock = lhablock + self.lhacode = lhacode + +all_vertices = [] + +class Vertex(UFOBaseClass): + + require_args=['name', 'particles', 'color', 'lorentz', 'couplings'] + + def __init__(self, name, particles, color, lorentz, couplings, **opt): + + args = (name, particles, color, lorentz, couplings) + + UFOBaseClass.__init__(self, *args, **opt) + + args=(particles,color,lorentz,couplings) + + global all_vertices + all_vertices.append(self) + +all_couplings = [] + +class Coupling(UFOBaseClass): + + require_args=['name', 'value', 'order'] + + def __init__(self, name, value, order, **opt): + + args =(name, value, order) + UFOBaseClass.__init__(self, *args, **opt) + global all_couplings + all_couplings.append(self) + + + +all_lorentz = [] + +class Lorentz(UFOBaseClass): + + require_args=['name','spins','structure'] + + def __init__(self, name, spins, structure='external', **opt): + args = (name, spins, structure) + UFOBaseClass.__init__(self, *args, **opt) + + global all_lorentz + all_lorentz.append(self) + + +all_functions = [] + +class Function(object): + + def __init__(self, name, arguments, expression): + + global all_functions + all_functions.append(self) + + self.name = name + self.arguments = arguments + self.expr = expression + + def __call__(self, *opt): + + for i, arg in enumerate(self.arguments): + exec('%s = %s' % (arg, opt[i] )) + + return eval(self.expr) + +all_orders = [] + +class CouplingOrder(object): + + def __init__(self, name, expansion_order, hierarchy, perturbative_expansion = 0): + + global all_orders + all_orders.append(self) + + self.name = name + self.expansion_order = expansion_order + self.hierarchy = hierarchy + +all_decays = [] + +class Decay(UFOBaseClass): + require_args = ['particle','partial_widths'] + + def __init__(self, particle, partial_widths, **opt): + args = (particle, partial_widths) + UFOBaseClass.__init__(self, *args, **opt) + + global all_decays + all_decays.append(self) + + # Add the information directly to the particle + particle.partial_widths = partial_widths + +all_form_factors = [] + +class FormFactor(UFOBaseClass): + require_args = ['name','type','value'] + + def __init__(self, name, type, value, **opt): + args = (name, type, value) + UFOBaseClass.__init__(self, *args, **opt) + + global all_form_factors + all_form_factors.append(self) diff --git a/pyamplicol/src/pyamplicol/assets/models/ufo/sm/parameters.py b/pyamplicol/src/pyamplicol/assets/models/ufo/sm/parameters.py new file mode 100644 index 0000000000..8cf6168093 --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/models/ufo/sm/parameters.py @@ -0,0 +1,495 @@ +# This file was automatically created by FeynRules 1.7.69 +# Mathematica version: 8.0 for Mac OS X x86 (64-bit) (November 6, 2010) +# Date: Mon 1 Oct 2012 14:58:25 + + +from __future__ import absolute_import +from .object_library import all_parameters, Parameter + +from .function_library import complexconjugate, re, im, csc, sec, acsc, asec + +# This is a default parameter object representing 0. +ZERO = Parameter(name='ZERO', + nature='internal', + type='real', + value=0.0, + texname='0') + +# User-defined parameters. +aEWM1 = Parameter(name='aEWM1', + nature='external', + type='real', + value=132.50698, + texname='\\text{aEWM1}', + lhablock='SMINPUTS', + lhacode=[1]) + +Gf = Parameter(name='Gf', + nature='external', + type='real', + value=0.0000116639, + texname='G_f', + lhablock='SMINPUTS', + lhacode=[2]) + +aS = Parameter(name='aS', + nature='external', + type='real', + value=0.118, + texname='\\alpha _s', + lhablock='SMINPUTS', + lhacode=[3]) + +lamWS = Parameter(name='lamWS', + nature='external', + type='real', + value=0.2253, + texname='\\text{lamWS}', + lhablock='Wolfenstein', + lhacode=[1]) + +AWS = Parameter(name='AWS', + nature='external', + type='real', + value=0.808, + texname='\\text{AWS}', + lhablock='Wolfenstein', + lhacode=[2]) + +rhoWS = Parameter(name='rhoWS', + nature='external', + type='real', + value=0.132, + texname='\\text{rhoWS}', + lhablock='Wolfenstein', + lhacode=[3]) + +etaWS = Parameter(name='etaWS', + nature='external', + type='real', + value=0.341, + texname='\\text{etaWS}', + lhablock='Wolfenstein', + lhacode=[4]) + +ymc = Parameter(name='ymc', + nature='external', + type='real', + value=1.27, + texname='\\text{ymc}', + lhablock='YUKAWA', + lhacode=[4]) + +ymb = Parameter(name='ymb', + nature='external', + type='real', + value=4.2, + texname='\\text{ymb}', + lhablock='YUKAWA', + lhacode=[5]) + +ymt = Parameter(name='ymt', + nature='external', + type='real', + value=164.5, + texname='\\text{ymt}', + lhablock='YUKAWA', + lhacode=[6]) + +yme = Parameter(name='yme', + nature='external', + type='real', + value=0.000511, + texname='\\text{yme}', + lhablock='YUKAWA', + lhacode=[11]) + +ymm = Parameter(name='ymm', + nature='external', + type='real', + value=0.10566, + texname='\\text{ymm}', + lhablock='YUKAWA', + lhacode=[13]) + +ymtau = Parameter(name='ymtau', + nature='external', + type='real', + value=1.777, + texname='\\text{ymtau}', + lhablock='YUKAWA', + lhacode=[15]) + +MZ = Parameter(name='MZ', + nature='external', + type='real', + value=91.188, + texname='\\text{MZ}', + lhablock='MASS', + lhacode=[23]) + +MC = Parameter(name='MC', + nature='external', + type='real', + value=1.27, + texname='\\text{MC}', + lhablock='MASS', + lhacode=[4]) + +MT = Parameter(name='MT', + nature='external', + type='real', + value=173., + texname='\\text{MT}', + lhablock='MASS', + lhacode=[6]) + +MB = Parameter(name='MB', + nature='external', + type='real', + value=4.7, + texname='\\text{MB}', + lhablock='MASS', + lhacode=[5]) + +MH = Parameter(name='MH', + nature='external', + type='real', + value=125., + texname='\\text{MH}', + lhablock='MASS', + lhacode=[25]) + +Me = Parameter(name='Me', + nature='external', + type='real', + value=0.000511, + texname='\\text{Me}', + lhablock='MASS', + lhacode=[11]) + +MM = Parameter(name='MM', + nature='external', + type='real', + value=0.10566, + texname='\\text{MM}', + lhablock='MASS', + lhacode=[13]) + +MTA = Parameter(name='MTA', + nature='external', + type='real', + value=1.777, + texname='\\text{MTA}', + lhablock='MASS', + lhacode=[15]) + +WZ = Parameter(name='WZ', + nature='external', + type='real', + value=2.44140351, + texname='\\text{WZ}', + lhablock='DECAY', + lhacode=[23]) + +WW = Parameter(name='WW', + nature='external', + type='real', + value=2.04759951, + texname='\\text{WW}', + lhablock='DECAY', + lhacode=[24]) + +WT = Parameter(name='WT', + nature='external', + type='real', + value=1.50833649, + texname='\\text{WT}', + lhablock='DECAY', + lhacode=[6]) + +WH = Parameter(name='WH', + nature='external', + type='real', + value=6.38233934e-03, + texname='\\text{WH}', + lhablock='DECAY', + lhacode=[25]) + +WTau = Parameter(name='WTau', + nature='external', + type='real', + value=2.27e-12, + texname='\\text{WTau}', + lhablock='DECAY', + lhacode=[15]) + +CKM1x1 = Parameter(name='CKM1x1', + nature='internal', + type='complex', + value='1 - lamWS**2/2.', + texname='\\text{CKM1x1}') + +CKM1x2 = Parameter(name='CKM1x2', + nature='internal', + type='complex', + value='lamWS', + texname='\\text{CKM1x2}') + +CKM1x3 = Parameter(name='CKM1x3', + nature='internal', + type='complex', + value='AWS*lamWS**3*(-(etaWS*complex(0,1)) + rhoWS)', + texname='\\text{CKM1x3}') + +CKM2x1 = Parameter(name='CKM2x1', + nature='internal', + type='complex', + value='-lamWS', + texname='\\text{CKM2x1}') + +CKM2x2 = Parameter(name='CKM2x2', + nature='internal', + type='complex', + value='1 - lamWS**2/2.', + texname='\\text{CKM2x2}') + +CKM2x3 = Parameter(name='CKM2x3', + nature='internal', + type='complex', + value='AWS*lamWS**2', + texname='\\text{CKM2x3}') + +CKM3x1 = Parameter(name='CKM3x1', + nature='internal', + type='complex', + value='AWS*lamWS**3*(1 - etaWS*complex(0,1) - rhoWS)', + texname='\\text{CKM3x1}') + +CKM3x2 = Parameter(name='CKM3x2', + nature='internal', + type='complex', + value='-(AWS*lamWS**2)', + texname='\\text{CKM3x2}') + +CKM3x3 = Parameter(name='CKM3x3', + nature='internal', + type='complex', + value='1', + texname='\\text{CKM3x3}') + +aEW = Parameter(name='aEW', + nature='internal', + type='real', + value='1/aEWM1', + texname='\\alpha _{\\text{EW}}') + +G = Parameter(name='G', + nature='internal', + type='real', + value='2*cmath.sqrt(aS)*cmath.sqrt(cmath.pi)', + texname='G') + +MW = Parameter(name='MW', + nature='internal', + type='real', + value='cmath.sqrt(MZ**2/2. + cmath.sqrt(MZ**4/4. - (aEW*cmath.pi*MZ**2)/(Gf*cmath.sqrt(2))))', + texname='M_W') + +ee = Parameter(name='ee', + nature='internal', + type='real', + value='2*cmath.sqrt(aEW)*cmath.sqrt(cmath.pi)', + texname='e') + +sw2 = Parameter(name='sw2', + nature='internal', + type='real', + value='1 - MW**2/MZ**2', + texname='\\text{sw2}') + +cw = Parameter(name='cw', + nature='internal', + type='real', + value='cmath.sqrt(1 - sw2)', + texname='c_w') + +sw = Parameter(name='sw', + nature='internal', + type='real', + value='cmath.sqrt(sw2)', + texname='s_w') + +g1 = Parameter(name='g1', + nature='internal', + type='real', + value='ee/cw', + texname='g_1') + +gw = Parameter(name='gw', + nature='internal', + type='real', + value='ee/sw', + texname='g_w') + +vev = Parameter(name='vev', + nature='internal', + type='real', + value='(2*MW*sw)/ee', + texname='\\text{vev}') + +lam = Parameter(name='lam', + nature='internal', + type='real', + value='MH**2/(2.*vev**2)', + texname='\\text{lam}') + +yb = Parameter(name='yb', + nature='internal', + type='real', + value='(ymb*cmath.sqrt(2))/vev', + texname='\\text{yb}') + +yc = Parameter(name='yc', + nature='internal', + type='real', + value='(ymc*cmath.sqrt(2))/vev', + texname='\\text{yc}') + +ye = Parameter(name='ye', + nature='internal', + type='real', + value='(yme*cmath.sqrt(2))/vev', + texname='\\text{ye}') + +ym = Parameter(name='ym', + nature='internal', + type='real', + value='(ymm*cmath.sqrt(2))/vev', + texname='\\text{ym}') + +yt = Parameter(name='yt', + nature='internal', + type='real', + value='(ymt*cmath.sqrt(2))/vev', + texname='\\text{yt}') + +ytau = Parameter(name='ytau', + nature='internal', + type='real', + value='(ymtau*cmath.sqrt(2))/vev', + texname='\\text{ytau}') + +muH = Parameter(name='muH', + nature='internal', + type='real', + value='cmath.sqrt(lam*vev**2)', + texname='\\mu') + +I1x31 = Parameter(name='I1x31', + nature='internal', + type='complex', + value='yb*complexconjugate(CKM1x3)', + texname='\\text{I1x31}') + +I1x32 = Parameter(name='I1x32', + nature='internal', + type='complex', + value='yb*complexconjugate(CKM2x3)', + texname='\\text{I1x32}') + +I1x33 = Parameter(name='I1x33', + nature='internal', + type='complex', + value='yb*complexconjugate(CKM3x3)', + texname='\\text{I1x33}') + +I2x12 = Parameter(name='I2x12', + nature='internal', + type='complex', + value='yc*complexconjugate(CKM2x1)', + texname='\\text{I2x12}') + +I2x13 = Parameter(name='I2x13', + nature='internal', + type='complex', + value='yt*complexconjugate(CKM3x1)', + texname='\\text{I2x13}') + +I2x22 = Parameter(name='I2x22', + nature='internal', + type='complex', + value='yc*complexconjugate(CKM2x2)', + texname='\\text{I2x22}') + +I2x23 = Parameter(name='I2x23', + nature='internal', + type='complex', + value='yt*complexconjugate(CKM3x2)', + texname='\\text{I2x23}') + +I2x32 = Parameter(name='I2x32', + nature='internal', + type='complex', + value='yc*complexconjugate(CKM2x3)', + texname='\\text{I2x32}') + +I2x33 = Parameter(name='I2x33', + nature='internal', + type='complex', + value='yt*complexconjugate(CKM3x3)', + texname='\\text{I2x33}') + +I3x21 = Parameter(name='I3x21', + nature='internal', + type='complex', + value='CKM2x1*yc', + texname='\\text{I3x21}') + +I3x22 = Parameter(name='I3x22', + nature='internal', + type='complex', + value='CKM2x2*yc', + texname='\\text{I3x22}') + +I3x23 = Parameter(name='I3x23', + nature='internal', + type='complex', + value='CKM2x3*yc', + texname='\\text{I3x23}') + +I3x31 = Parameter(name='I3x31', + nature='internal', + type='complex', + value='CKM3x1*yt', + texname='\\text{I3x31}') + +I3x32 = Parameter(name='I3x32', + nature='internal', + type='complex', + value='CKM3x2*yt', + texname='\\text{I3x32}') + +I3x33 = Parameter(name='I3x33', + nature='internal', + type='complex', + value='CKM3x3*yt', + texname='\\text{I3x33}') + +I4x13 = Parameter(name='I4x13', + nature='internal', + type='complex', + value='CKM1x3*yb', + texname='\\text{I4x13}') + +I4x23 = Parameter(name='I4x23', + nature='internal', + type='complex', + value='CKM2x3*yb', + texname='\\text{I4x23}') + +I4x33 = Parameter(name='I4x33', + nature='internal', + type='complex', + value='CKM3x3*yb', + texname='\\text{I4x33}') diff --git a/pyamplicol/src/pyamplicol/assets/models/ufo/sm/particles.py b/pyamplicol/src/pyamplicol/assets/models/ufo/sm/particles.py new file mode 100644 index 0000000000..161bd4d13e --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/models/ufo/sm/particles.py @@ -0,0 +1,385 @@ +# This file was automatically created by FeynRules 1.7.69 +# Mathematica version: 8.0 for Mac OS X x86 (64-bit) (November 6, 2010) +# Date: Mon 1 Oct 2012 14:58:25 + + +from __future__ import division +from __future__ import absolute_import +from .object_library import all_particles, Particle +from . import parameters as Param + +a = Particle(pdg_code=22, + name='a', + antiname='a', + spin=3, + color=1, + mass=Param.ZERO, + width=Param.ZERO, + texname=r'{\gamma}', + antitexname=r'{\gamma}', + charge=0, + GhostNumber=0, + LeptonNumber=0, + Y=0) + +Z = Particle(pdg_code=23, + name='Z', + antiname='Z', + spin=3, + color=1, + mass=Param.MZ, + width=Param.WZ, + texname=r'{Z}', + antitexname=r'{Z}', + charge=0, + GhostNumber=0, + LeptonNumber=0, + Y=0) + +W__plus__ = Particle(pdg_code=24, + name='W+', + antiname='W-', + spin=3, + color=1, + mass=Param.MW, + width=Param.WW, + texname=r'{W^+}', + antitexname=r'{W^-}', + charge=1, + GhostNumber=0, + LeptonNumber=0, + Y=0) + +W__minus__ = W__plus__.anti() + +g = Particle(pdg_code=21, + name='g', + antiname='g', + spin=3, + color=8, + mass=Param.ZERO, + width=Param.ZERO, + texname=r'{g}', + antitexname=r'{g}', + charge=0, + GhostNumber=0, + LeptonNumber=0, + Y=0) + +ghA = Particle(pdg_code=9000001, + name='ghA', + antiname='ghA~', + spin=-1, + color=1, + mass=Param.ZERO, + width=Param.ZERO, + texname=r'\tilde{\gamma}', + antitexname=r'\overline{\tilde{\gamma}}', + charge=0, + GhostNumber=1, + LeptonNumber=0, + Y=0) + +ghA__tilde__ = ghA.anti() + +ghZ = Particle(pdg_code=9000002, + name='ghZ', + antiname='ghZ~', + spin=-1, + color=1, + mass=Param.MZ, + width=Param.WZ, + texname=r'\tilde{Z}', + antitexname=r'\bar{\tilde{Z}}', + charge=0, + GhostNumber=1, + LeptonNumber=0, + Y=0) + +ghZ__tilde__ = ghZ.anti() + +ghWp = Particle(pdg_code=9000003, + name='ghWp', + antiname='ghWp~', + spin=-1, + color=1, + mass=Param.MW, + width=Param.WW, + texname=r'{\tilde{W}^+}', + antitexname=r'{\bar{\tilde{W}}^+}', + charge=1, + GhostNumber=1, + LeptonNumber=0, + Y=0) + +ghWp__tilde__ = ghWp.anti() + +ghWm = Particle(pdg_code=9000004, + name='ghWm', + antiname='ghWm~', + spin=-1, + color=1, + mass=Param.MW, + width=Param.WW, + texname=r'{\tilde{W}^-}', + antitexname=r'{\bar{\tilde{W}}^-}', + charge=-1, + GhostNumber=1, + LeptonNumber=0, + Y=0) + +ghWm__tilde__ = ghWm.anti() + +ghG = Particle(pdg_code=9000005, + name='ghG', + antiname='ghG~', + spin=-1, + color=8, + mass=Param.ZERO, + width=Param.ZERO, + texname=r'{\tilde{g}}', + antitexname=r'{\bar{\tilde{g}}}', + charge=0, + GhostNumber=1, + LeptonNumber=0, + Y=0) + +ghG__tilde__ = ghG.anti() + +ve = Particle(pdg_code=12, + name='ve', + antiname='ve~', + spin=2, + color=1, + mass=Param.ZERO, + width=Param.ZERO, + texname=r'{\nu_e}', + antitexname=r'{\overline{\nu}_e}', + charge=0, + GhostNumber=0, + LeptonNumber=1, + Y=0) + +ve__tilde__ = ve.anti() + +vm = Particle(pdg_code=14, + name='vm', + antiname='vm~', + spin=2, + color=1, + mass=Param.ZERO, + width=Param.ZERO, + texname=r'{\nu_\mu}', + antitexname=r'{\overline{\nu}_\mu}', + charge=0, + GhostNumber=0, + LeptonNumber=1, + Y=0) + +vm__tilde__ = vm.anti() + +vt = Particle(pdg_code=16, + name='vt', + antiname='vt~', + spin=2, + color=1, + mass=Param.ZERO, + width=Param.ZERO, + texname=r'{\nu_\tau}', + antitexname=r'{\overline{\nu}_\tau}', + charge=0, + GhostNumber=0, + LeptonNumber=1, + Y=0) + +vt__tilde__ = vt.anti() + +u = Particle(pdg_code=2, + name='u', + antiname='u~', + spin=2, + color=3, + mass=Param.ZERO, + width=Param.ZERO, + texname=r'{u}', + antitexname=r'{\overline{u}}', + charge=2/3, + GhostNumber=0, + LeptonNumber=0, + Y=0) + +u__tilde__ = u.anti() + +c = Particle(pdg_code=4, + name='c', + antiname='c~', + spin=2, + color=3, + mass=Param.MC, + width=Param.ZERO, + texname=r'{c}', + antitexname=r'{\overline{c}}', + charge=2/3, + GhostNumber=0, + LeptonNumber=0, + Y=0) + +c__tilde__ = c.anti() + +t = Particle(pdg_code=6, + name='t', + antiname='t~', + spin=2, + color=3, + mass=Param.MT, + width=Param.WT, + texname=r'{t}', + antitexname=r'{\overline{t}}', + charge=2/3, + GhostNumber=0, + LeptonNumber=0, + Y=0) + +t__tilde__ = t.anti() + +d = Particle(pdg_code=1, + name='d', + antiname='d~', + spin=2, + color=3, + mass=Param.ZERO, + width=Param.ZERO, + texname=r'{d}', + antitexname=r'{\overline{d}}', + charge=-1/3, + GhostNumber=0, + LeptonNumber=0, + Y=0) + +d__tilde__ = d.anti() + +s = Particle(pdg_code=3, + name='s', + antiname='s~', + spin=2, + color=3, + mass=Param.ZERO, + width=Param.ZERO, + texname=r'{s}', + antitexname=r'{\overline{s}}', + charge=-1/3, + GhostNumber=0, + LeptonNumber=0, + Y=0) + +s__tilde__ = s.anti() + +b = Particle(pdg_code=5, + name='b', + antiname='b~', + spin=2, + color=3, + mass=Param.MB, + width=Param.ZERO, + texname=r'{b}', + antitexname=r'{\overline{b}}', + charge=-1/3, + GhostNumber=0, + LeptonNumber=0, + Y=0) + +b__tilde__ = b.anti() + +H = Particle(pdg_code=25, + name='H', + antiname='H', + spin=1, + color=1, + mass=Param.MH, + width=Param.WH, + texname=r'{H}', + antitexname=r'{H}', + charge=0, + GhostNumber=0, + LeptonNumber=0, + Y=0) + +G0 = Particle(pdg_code=250, + name='G0', + antiname='G0', + spin=1, + color=1, + mass=Param.MZ, + width=Param.WZ, + texname=r'{G_0}', + antitexname=r'{G_0}', + GoldstoneBoson=True, + charge=0, + GhostNumber=0, + LeptonNumber=0, + Y=0) + +G__plus__ = Particle(pdg_code=251, + name='G+', + antiname='G-', + spin=1, + color=1, + mass=Param.MW, + width=Param.WW, + texname=r'{G^+}', + antitexname=r'{G^-}', + GoldstoneBoson=True, + charge=1, + GhostNumber=0, + LeptonNumber=0, + Y=0) + +G__minus__ = G__plus__.anti() + +e__minus__ = Particle(pdg_code=11, + name='e-', + antiname='e+', + spin=2, + color=1, + mass=Param.Me, + width=Param.ZERO, + texname=r'{e^-}', + antitexname=r'{e^+}', + charge=-1, + GhostNumber=0, + LeptonNumber=1, + Y=0) + +e__plus__ = e__minus__.anti() + +mu__minus__ = Particle(pdg_code=13, + name='mu-', + antiname='mu+', + spin=2, + color=1, + mass=Param.MM, + width=Param.ZERO, + texname=r'{\mu^-}', + antitexname=r'{\mu^+}', + charge=-1, + GhostNumber=0, + LeptonNumber=1, + Y=0) + +mu__plus__ = mu__minus__.anti() + +ta__minus__ = Particle(pdg_code=15, + name='ta-', + antiname='ta+', + spin=2, + color=1, + mass=Param.MTA, + width=Param.WTau, + texname=r'{\tau^-}', + antitexname=r'{\tau^+}', + charge=-1, + GhostNumber=0, + LeptonNumber=1, + Y=0) + +ta__plus__ = ta__minus__.anti() diff --git a/pyamplicol/src/pyamplicol/assets/models/ufo/sm/restrict_c_mass.dat b/pyamplicol/src/pyamplicol/assets/models/ufo/sm/restrict_c_mass.dat new file mode 100644 index 0000000000..153d8c6509 --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/models/ufo/sm/restrict_c_mass.dat @@ -0,0 +1,53 @@ +###################################################################### +## PARAM_CARD AUTOMATICALY GENERATED BY THE UFO ##################### +###################################################################### + +################################### +## INFORMATION FOR SMINPUTS +################################### +Block SMINPUTS + 1 1.325070e+02 # aEWM1 + 2 1.166390e-05 # Gf + 3 1.180000e-01 # aS + +################################### +## INFORMATION FOR MASS +################################### +Block MASS + 4 1.550000e+00 # MC + 5 4.700000e+00 # MB + 6 1.730000e+02 # MT + 11 0.000000e+00 # Me + 13 0.000000e+00 # MM + 15 1.777000e+00 # MTA + 23 9.118800e+01 # MZ + 25 1.250000e+02 # MH + +################################### +## INFORMATION FOR DECAY +################################### +DECAY 6 1.491500E+00 +DECAY 15 0.000000e+00 +DECAY 23 2.441404e+00 +DECAY 24 2.047600e+00 +DECAY 25 6.38233934e-03 + +################################### +## INFORMATION FOR WOLFENSTEIN +################################### +Block Wolfenstein + 1 0.000000e+00 # lamWS + 2 0.000000e+00 # AWS + 3 0.000000e+00 # rhoWS + 4 0.000000e+00 # etaWS + +################################### +## INFORMATION FOR YUKAWA +################################### +Block YUKAWA + 4 1.550000e+00 # ymc + 5 4.700000e+00 # ymb + 6 1.730000e+02 # ymt + 11 0.000000e+00 # yme + 13 0.000000e+00 # ymm + 15 1.777000e+00 # ymtau diff --git a/pyamplicol/src/pyamplicol/assets/models/ufo/sm/restrict_ckm.dat b/pyamplicol/src/pyamplicol/assets/models/ufo/sm/restrict_ckm.dat new file mode 100644 index 0000000000..3b0e9f9db1 --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/models/ufo/sm/restrict_ckm.dat @@ -0,0 +1,53 @@ +###################################################################### +## PARAM_CARD AUTOMATICALY GENERATED BY THE UFO ##################### +###################################################################### + +################################### +## INFORMATION FOR SMINPUTS +################################### +Block SMINPUTS + 1 1.325070e+02 # aEWM1 + 2 1.166390e-05 # Gf + 3 1.180000e-01 # aS + +################################### +## INFORMATION FOR MASS +################################### +Block MASS + 4 0.000000e+00 # MC + 5 4.700000e+00 # MB + 6 1.730000e+02 # MT + 11 0.000000e+00 # Me + 13 0.000000e+00 # MM + 15 1.777000e+00 # MTA + 23 9.118800e+01 # MZ + 25 1.250000e+02 # MH + +################################### +## INFORMATION FOR DECAY +################################### +DECAY 6 1.491500E+00 +DECAY 15 0.000000e+00 +DECAY 23 2.441404e+00 +DECAY 24 2.047600e+00 +DECAY 25 6.38233934e-03 + +################################### +## INFORMATION FOR WOLFENSTEIN +################################### +Block Wolfenstein + 1 2.253000e-01 # lamWS + 2 8.080000e-01 # AWS + 3 1.320000e-01 # rhoWS + 4 3.410000e-01 # etaWS + +################################### +## INFORMATION FOR YUKAWA +################################### +Block YUKAWA + 4 0.000000e+00 # ymc + 5 4.700000e+00 # ymb + 6 1.730000e+02 # ymt + 11 0.000000e+00 # yme + 13 0.000000e+00 # ymm + 15 1.777000e+00 # ymtau diff --git a/pyamplicol/src/pyamplicol/assets/models/ufo/sm/restrict_default.dat b/pyamplicol/src/pyamplicol/assets/models/ufo/sm/restrict_default.dat new file mode 100644 index 0000000000..2ae557d5ad --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/models/ufo/sm/restrict_default.dat @@ -0,0 +1,53 @@ +###################################################################### +## PARAM_CARD AUTOMATICALY GENERATED BY THE UFO ##################### +###################################################################### + +################################### +## INFORMATION FOR SMINPUTS +################################### +Block SMINPUTS + 1 1.325070e+02 # aEWM1 + 2 1.166390e-05 # Gf + 3 1.180000e-01 # aS + +################################### +## INFORMATION FOR MASS +################################### +Block MASS + 4 0.000000e+00 # MC + 5 4.700000e+00 # MB + 6 1.730000e+02 # MT + 11 0.000000e+00 # Me + 13 0.000000e+00 # MM + 15 1.777000e+00 # MTA + 23 9.118800e+01 # MZ + 25 1.250000e+02 # MH + +################################### +## INFORMATION FOR DECAY +################################### +DECAY 6 1.491500E+00 # WT +DECAY 15 0.000000e+00 # WTau +DECAY 23 2.441404e+00 # WZ +DECAY 24 2.047600e+00 # WW +DECAY 25 6.38233934e-03 # WH + +################################### +## INFORMATION FOR WOLFENSTEIN +################################### +Block Wolfenstein + 1 0.000000e+00 # lamWS + 2 0.000000e+00 # AWS + 3 0.000000e+00 # rhoWS + 4 0.000000e+00 # etaWS + +################################### +## INFORMATION FOR YUKAWA +################################### +Block YUKAWA + 4 0.000000e+00 # ymc + 5 4.700000e+00 # ymb + 6 1.730000e+02 # ymt + 11 0.000000e+00 # yme + 13 0.000000e+00 # ymm + 15 1.777000e+00 # ymtau diff --git a/pyamplicol/src/pyamplicol/assets/models/ufo/sm/restrict_full.dat b/pyamplicol/src/pyamplicol/assets/models/ufo/sm/restrict_full.dat new file mode 100644 index 0000000000..38ec0ccc52 --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/models/ufo/sm/restrict_full.dat @@ -0,0 +1,128 @@ +###################################################################### +## PARAM_CARD AUTOMATICALY GENERATED BY GAMMALOOP ## +###################################################################### + +################################### +## INFORMATION FOR SMINPUTS +################################### +Block SMINPUTS + 1 1.325070e+02 # aEWM1 + 2 1.166390e-05 # Gf + 3 1.180000e-01 # aS + +################################### +## INFORMATION FOR MASS +################################### +Block MASS + 4 1.270000e+00 # MC + 5 4.700000e+00 # MB + 6 1.730000e+02 # MT + 11 5.110000e-04 # Me + 13 1.056600e-01 # MM + 15 1.777000e+00 # MTA + 23 9.118800e+01 # MZ + 25 1.250000e+02 # MH +## Not dependent paramater. +## Those values should be edited following analytical the +## analytical expression. Some generator could simply ignore +## those values and use the analytical expression + 22 0.000000 # a : None + 24 80.419000 # W+ : sqrt(1/2*MZ^2+sqrt(1/4*MZ^4-pi*MZ^2*aEW*(Gf*sqrt(2))^-1)) + 21 0.000000 # g : None + 9000001 0.000000 # ghA : None + 9000003 80.419000 # ghWp : sqrt(1/2*MZ^2+sqrt(1/4*MZ^4-pi*MZ^2*aEW*(Gf*sqrt(2))^-1)) + 9000004 80.419000 # ghWm : sqrt(1/2*MZ^2+sqrt(1/4*MZ^4-pi*MZ^2*aEW*(Gf*sqrt(2))^-1)) + 9000005 0.000000 # ghG : None + 12 0.000000 # ve : None + 14 0.000000 # vm : None + 16 0.000000 # vt : None + 2 0.000000 # u : None + 1 0.000000 # d : None + 3 0.000000 # s : None + 251 80.419000 # G+ : sqrt(1/2*MZ^2+sqrt(1/4*MZ^4-pi*MZ^2*aEW*(Gf*sqrt(2))^-1)) + +################################### +## INFORMATION FOR DECAY +################################### +DECAY 6 1.491500E+00 # WT +DECAY 15 2.270000e-12 +DECAY 23 2.441404e+00 +DECAY 24 2.047600e+00 +DECAY 25 6.382339e-03 +## Not dependent paramater. +## Those values should be edited following analytical the +## analytical expression. Some generator could simply ignore +## those values and use the analytical expression +DECAY 22 0.000000 # a : None +DECAY 21 0.000000 # g : None +DECAY 9000001 0.000000 # ghA : None +DECAY 9000005 0.000000 # ghG : None +DECAY 12 0.000000 # ve : None +DECAY 14 0.000000 # vm : None +DECAY 16 0.000000 # vt : None +DECAY 2 0.000000 # u : None +DECAY 4 0.000000 # c : None +DECAY 1 0.000000 # d : None +DECAY 3 0.000000 # s : None +DECAY 5 0.000000 # b : None +DECAY 11 0.000000 # e- : None +DECAY 13 0.000000 # mu- : None + +################################### +## INFORMATION FOR WOLFENSTEIN +################################### +Block Wolfenstein + 1 2.253000e-01 # lamWS + 2 8.080000e-01 # AWS + 3 1.320000e-01 # rhoWS + 4 3.410000e-01 # etaWS + +################################### +## INFORMATION FOR YUKAWA +################################### +Block YUKAWA + 4 1.270000e+00 # ymc + 5 4.200000e+00 # ymb + 6 1.645000e+02 # ymt + 11 5.110000e-04 # yme + 13 1.056600e-01 # ymm + 15 1.777000e+00 # ymtau +#=========================================================== +# QUANTUM NUMBERS OF NEW STATE(S) (NON SM PDG CODE) +#=========================================================== + +Block QNUMBERS 9000001 # ghA + 1 0 # 3 times electric charge + 2 -1 # number of spin states (2S+1) + 3 1 # colour rep (1: singlet, 3: triplet, 8: octet) + 4 1 # Particle/Antiparticle distinction (0=own anti) +Block QNUMBERS 9000002 # ghZ + 1 0 # 3 times electric charge + 2 -1 # number of spin states (2S+1) + 3 1 # colour rep (1: singlet, 3: triplet, 8: octet) + 4 1 # Particle/Antiparticle distinction (0=own anti) +Block QNUMBERS 9000003 # ghWp + 1 3 # 3 times electric charge + 2 -1 # number of spin states (2S+1) + 3 1 # colour rep (1: singlet, 3: triplet, 8: octet) + 4 1 # Particle/Antiparticle distinction (0=own anti) +Block QNUMBERS 9000004 # ghWm + 1 -3 # 3 times electric charge + 2 -1 # number of spin states (2S+1) + 3 1 # colour rep (1: singlet, 3: triplet, 8: octet) + 4 1 # Particle/Antiparticle distinction (0=own anti) +Block QNUMBERS 9000005 # ghG + 1 0 # 3 times electric charge + 2 -1 # number of spin states (2S+1) + 3 8 # colour rep (1: singlet, 3: triplet, 8: octet) + 4 1 # Particle/Antiparticle distinction (0=own anti) +Block QNUMBERS 250 # G0 + 1 0 # 3 times electric charge + 2 3 # number of spin states (2S+1) + 3 1 # colour rep (1: singlet, 3: triplet, 8: octet) + 4 0 # Particle/Antiparticle distinction (0=own anti) +Block QNUMBERS 251 # G+ + 1 3 # 3 times electric charge + 2 3 # number of spin states (2S+1) + 3 1 # colour rep (1: singlet, 3: triplet, 8: octet) + 4 1 # Particle/Antiparticle distinction (0=own anti) diff --git a/pyamplicol/src/pyamplicol/assets/models/ufo/sm/restrict_lepton_masses.dat b/pyamplicol/src/pyamplicol/assets/models/ufo/sm/restrict_lepton_masses.dat new file mode 100644 index 0000000000..b8d90f3e35 --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/models/ufo/sm/restrict_lepton_masses.dat @@ -0,0 +1,53 @@ +###################################################################### +## PARAM_CARD AUTOMATICALY GENERATED BY THE UFO ##################### +###################################################################### + +################################### +## INFORMATION FOR SMINPUTS +################################### +Block SMINPUTS + 1 1.325070e+02 # aEWM1 + 2 1.166390e-05 # Gf + 3 1.180000e-01 # aS + +################################### +## INFORMATION FOR MASS +################################### +Block MASS + 4 0.000000e+00 # MC + 5 4.700000e+00 # MB + 6 1.730000e+02 # MT + 11 5.110000e-04 # Me + 13 1.056600e-01 # MM + 15 1.777000e+00 # MTA + 23 9.118800e+01 # MZ + 25 1.250000e+02 # MH + +################################### +## INFORMATION FOR DECAY +################################### +DECAY 6 1.491500E+00 # WT +DECAY 15 2.270000e-12 # WTau +DECAY 23 2.441404e+00 # WZ +DECAY 24 2.047600e+00 # WW +DECAY 25 6.38233934e-03 # WH + +################################### +## INFORMATION FOR WOLFENSTEIN +################################### +Block Wolfenstein + 1 0.000000e+00 # lamWS + 2 0.000000e+00 # AWS + 3 0.000000e+00 # rhoWS + 4 0.000000e+00 # etaWS + +################################### +## INFORMATION FOR YUKAWA +################################### +Block YUKAWA + 4 0.000000e+00 # ymc + 5 4.700000e+00 # ymb + 6 1.730000e+02 # ymt + 11 5.110000e-04 # yme + 13 1.056600e-01 # ymm + 15 1.777000e+00 # ymtau diff --git a/pyamplicol/src/pyamplicol/assets/models/ufo/sm/restrict_no_b_mass.dat b/pyamplicol/src/pyamplicol/assets/models/ufo/sm/restrict_no_b_mass.dat new file mode 100644 index 0000000000..f42e3375d1 --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/models/ufo/sm/restrict_no_b_mass.dat @@ -0,0 +1,53 @@ +###################################################################### +## PARAM_CARD AUTOMATICALY GENERATED BY THE UFO ##################### +###################################################################### + +################################### +## INFORMATION FOR SMINPUTS +################################### +Block SMINPUTS + 1 1.325070e+02 # aEWM1 + 2 1.166390e-05 # Gf + 3 1.180000e-01 # aS + +################################### +## INFORMATION FOR MASS +################################### +Block MASS + 4 0.000000e+00 # MC + 5 0.000000e+00 # MB + 6 1.730000e+02 # MT + 11 0.000000e+00 # Me + 13 0.000000e+00 # MM + 15 1.777000e+00 # MTA + 23 9.118800e+01 # MZ + 25 1.250000e+02 # MH + +################################### +## INFORMATION FOR DECAY +################################### +DECAY 6 1.491500E+00 +DECAY 15 0.000000e+00 +DECAY 23 2.441404e+00 +DECAY 24 2.047600e+00 +DECAY 25 6.38233934e-03 + +################################### +## INFORMATION FOR WOLFENSTEIN +################################### +Block Wolfenstein + 1 0.000000e+00 # lamWS + 2 0.000000e+00 # AWS + 3 0.000000e+00 # rhoWS + 4 0.000000e+00 # etaWS + +################################### +## INFORMATION FOR YUKAWA +################################### +Block YUKAWA + 4 0.000000e+00 # ymc + 5 0.000000e+00 # ymb + 6 1.730000e+02 # ymt + 11 0.000000e+00 # yme + 13 0.000000e+00 # ymm + 15 1.777000e+00 # ymtau diff --git a/pyamplicol/src/pyamplicol/assets/models/ufo/sm/restrict_no_masses.dat b/pyamplicol/src/pyamplicol/assets/models/ufo/sm/restrict_no_masses.dat new file mode 100644 index 0000000000..31a72cedc7 --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/models/ufo/sm/restrict_no_masses.dat @@ -0,0 +1,53 @@ +###################################################################### +## PARAM_CARD AUTOMATICALY GENERATED BY THE UFO ##################### +###################################################################### + +################################### +## INFORMATION FOR SMINPUTS +################################### +Block SMINPUTS + 1 1.325070e+02 # aEWM1 + 2 1.166390e-05 # Gf + 3 1.180000e-01 # aS + +################################### +## INFORMATION FOR MASS +################################### +Block MASS + 4 0.000000e+00 # MC + 5 0.000000e+00 # MB + 6 1.730000e+02 # MT + 11 0.000000e+00 # Me + 13 0.000000e+00 # MM + 15 0.000000e+00 # MTA + 23 9.118800e+01 # MZ + 25 1.250000e+02 # MH + +################################### +## INFORMATION FOR DECAY +################################### +DECAY 6 1.491500E+00 +DECAY 15 0.000000e+00 +DECAY 23 2.441404e+00 +DECAY 24 2.047600e+00 +DECAY 25 6.38233934e-03 + +################################### +## INFORMATION FOR WOLFENSTEIN +################################### +Block Wolfenstein + 1 0.000000e+00 # lamWS + 2 0.000000e+00 # AWS + 3 0.000000e+00 # rhoWS + 4 0.000000e+00 # etaWS + +################################### +## INFORMATION FOR YUKAWA +################################### +Block YUKAWA + 4 0.000000e+00 # ymc + 5 0.000000e+00 # ymb + 6 1.730000e+02 # ymt + 11 0.000000e+00 # yme + 13 0.000000e+00 # ymm + 15 0.000000e+00 # ymtau diff --git a/pyamplicol/src/pyamplicol/assets/models/ufo/sm/restrict_no_tau_mass.dat b/pyamplicol/src/pyamplicol/assets/models/ufo/sm/restrict_no_tau_mass.dat new file mode 100644 index 0000000000..6497e444d2 --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/models/ufo/sm/restrict_no_tau_mass.dat @@ -0,0 +1,53 @@ +###################################################################### +## PARAM_CARD AUTOMATICALY GENERATED BY THE UFO ##################### +###################################################################### + +################################### +## INFORMATION FOR SMINPUTS +################################### +Block SMINPUTS + 1 1.325070e+02 # aEWM1 + 2 1.166390e-05 # Gf + 3 1.180000e-01 # aS + +################################### +## INFORMATION FOR MASS +################################### +Block MASS + 4 0.000000e+00 # MC + 5 4.700000e+00 # MB + 6 1.730000e+02 # MT + 11 0.000000e+00 # Me + 13 0.000000e+00 # MM + 15 0.000000e+00 # MTA + 23 9.118800e+01 # MZ + 25 1.250000e+02 # MH + +################################### +## INFORMATION FOR DECAY +################################### +DECAY 6 1.491500E+00 +DECAY 15 0.000000e+00 +DECAY 23 2.441404e+00 +DECAY 24 2.047600e+00 +DECAY 25 6.38233934e-03 + +################################### +## INFORMATION FOR WOLFENSTEIN +################################### +Block Wolfenstein + 1 0.000000e+00 # lamWS + 2 0.000000e+00 # AWS + 3 0.000000e+00 # rhoWS + 4 0.000000e+00 # etaWS + +################################### +## INFORMATION FOR YUKAWA +################################### +Block YUKAWA + 4 0.000000e+00 # ymc + 5 4.700000e+00 # ymb + 6 1.730000e+02 # ymt + 11 0.000000e+00 # yme + 13 0.000000e+00 # ymm + 15 0.000000e+00 # ymtau diff --git a/pyamplicol/src/pyamplicol/assets/models/ufo/sm/restrict_no_widths.dat b/pyamplicol/src/pyamplicol/assets/models/ufo/sm/restrict_no_widths.dat new file mode 100644 index 0000000000..55e78884b9 --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/models/ufo/sm/restrict_no_widths.dat @@ -0,0 +1,53 @@ +###################################################################### +## PARAM_CARD AUTOMATICALY GENERATED BY THE UFO ##################### +###################################################################### + +################################### +## INFORMATION FOR SMINPUTS +################################### +Block SMINPUTS + 1 1.325070e+02 # aEWM1 + 2 1.166390e-05 # Gf + 3 1.180000e-01 # aS + +################################### +## INFORMATION FOR MASS +################################### +Block MASS + 4 0.000000e+00 # MC + 5 4.700000e+00 # MB + 6 1.730000e+02 # MT + 11 0.000000e+00 # Me + 13 0.000000e+00 # MM + 15 1.777000e+00 # MTA + 23 9.118800e+01 # MZ + 25 1.200000e+02 # MH + +################################### +## INFORMATION FOR DECAY +################################### +DECAY 6 0.000000E+00 +DECAY 15 0.000000e+00 +DECAY 23 0.000000e+00 +DECAY 24 0.000000e+00 +DECAY 25 0.000000e+00 + +################################### +## INFORMATION FOR WOLFENSTEIN +################################### +Block Wolfenstein + 1 0.000000e+00 # lamWS + 2 0.000000e+00 # AWS + 3 0.000000e+00 # rhoWS + 4 0.000000e+00 # etaWS + +################################### +## INFORMATION FOR YUKAWA +################################### +Block YUKAWA + 4 0.000000e+00 # ymc + 5 4.200000e+00 # ymb + 6 1.645000e+02 # ymt + 11 0.000000e+00 # yme + 13 0.000000e+00 # ymm + 15 1.777000e+00 # ymtau diff --git a/pyamplicol/src/pyamplicol/assets/models/ufo/sm/restrict_zeromass_ckm.dat b/pyamplicol/src/pyamplicol/assets/models/ufo/sm/restrict_zeromass_ckm.dat new file mode 100644 index 0000000000..c82164bac6 --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/models/ufo/sm/restrict_zeromass_ckm.dat @@ -0,0 +1,53 @@ +###################################################################### +## PARAM_CARD AUTOMATICALY GENERATED BY THE UFO ##################### +###################################################################### + +################################### +## INFORMATION FOR SMINPUTS +################################### +Block SMINPUTS + 1 1.325070e+02 # aEWM1 + 2 1.166390e-05 # Gf + 3 1.180000e-01 # aS + +################################### +## INFORMATION FOR MASS +################################### +Block MASS + 4 0.000000e+00 # MC + 5 0.000000e+00 # MB + 6 1.730000e+02 # MT + 11 0.000000e+00 # Me + 13 0.000000e+00 # MM + 15 0.000000e+00 # MTA + 23 9.118800e+01 # MZ + 25 1.250000e+02 # MH + +################################### +## INFORMATION FOR DECAY +################################### +DECAY 6 1.491500E+00 +DECAY 15 0.000000e+00 +DECAY 23 2.441404e+00 +DECAY 24 2.047600e+00 +DECAY 25 6.38233934e-03 + +################################### +## INFORMATION FOR WOLFENSTEIN +################################### +Block Wolfenstein + 1 2.253000e-01 # lamWS + 2 8.080000e-01 # AWS + 3 1.320000e-01 # rhoWS + 4 3.410000e-01 # etaWS + +################################### +## INFORMATION FOR YUKAWA +################################### +Block YUKAWA + 4 0.000000e+00 # ymc + 5 0.000000e+00 # ymb + 6 1.730000e+02 # ymt + 11 0.000000e+00 # yme + 13 0.000000e+00 # ymm + 15 0.000000e+00 # ymtau diff --git a/pyamplicol/src/pyamplicol/assets/models/ufo/sm/run_write_param_card.py b/pyamplicol/src/pyamplicol/assets/models/ufo/sm/run_write_param_card.py new file mode 100755 index 0000000000..d87ecc25b9 --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/models/ufo/sm/run_write_param_card.py @@ -0,0 +1,19 @@ +# type: ignore +#!/usr/bin/env python +from __future__ import absolute_import +from __future__ import print_function +from six.moves import range + +import sys +import os +import importlib + +if '__main__' == __name__: + dir_path = os.path.join(os.path.dirname(__file__)) + sys.path.insert(0, os.path.join(dir_path, os.path.pardir)) + pw = importlib.import_module('%s.%s' % ( + os.path.basename(dir_path), 'write_param_card')) + del sys.path[0] + + pw.ParamCardWriter('./param_card.dat', generic=True) + print('write ./param_card.dat') diff --git a/pyamplicol/src/pyamplicol/assets/models/ufo/sm/vertices.py b/pyamplicol/src/pyamplicol/assets/models/ufo/sm/vertices.py new file mode 100644 index 0000000000..d8c6ceb815 --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/models/ufo/sm/vertices.py @@ -0,0 +1,929 @@ +# This file was automatically created by FeynRules 1.7.69 +# Mathematica version: 8.0 for Mac OS X x86 (64-bit) (November 6, 2010) +# Date: Mon 1 Oct 2012 14:58:25 + + +from __future__ import absolute_import +from .object_library import all_vertices, Vertex +from . import particles as P +from . import couplings as C +from . import lorentz as L + + +V_1 = Vertex(name = 'V_1', + particles = [ P.G0, P.G0, P.G0, P.G0 ], + color = [ '1' ], + lorentz = [ L.SSSS1 ], + couplings = {(0,0):C.GC_33}) + +V_2 = Vertex(name = 'V_2', + particles = [ P.G0, P.G0, P.G__minus__, P.G__plus__ ], + color = [ '1' ], + lorentz = [ L.SSSS1 ], + couplings = {(0,0):C.GC_31}) + +V_3 = Vertex(name = 'V_3', + particles = [ P.G__minus__, P.G__minus__, P.G__plus__, P.G__plus__ ], + color = [ '1' ], + lorentz = [ L.SSSS1 ], + couplings = {(0,0):C.GC_32}) + +V_4 = Vertex(name = 'V_4', + particles = [ P.G0, P.G0, P.H, P.H ], + color = [ '1' ], + lorentz = [ L.SSSS1 ], + couplings = {(0,0):C.GC_31}) + +V_5 = Vertex(name = 'V_5', + particles = [ P.G__minus__, P.G__plus__, P.H, P.H ], + color = [ '1' ], + lorentz = [ L.SSSS1 ], + couplings = {(0,0):C.GC_31}) + +V_6 = Vertex(name = 'V_6', + particles = [ P.H, P.H, P.H, P.H ], + color = [ '1' ], + lorentz = [ L.SSSS1 ], + couplings = {(0,0):C.GC_33}) + +V_7 = Vertex(name = 'V_7', + particles = [ P.G0, P.G0, P.H ], + color = [ '1' ], + lorentz = [ L.SSS1 ], + couplings = {(0,0):C.GC_68}) + +V_8 = Vertex(name = 'V_8', + particles = [ P.G__minus__, P.G__plus__, P.H ], + color = [ '1' ], + lorentz = [ L.SSS1 ], + couplings = {(0,0):C.GC_68}) + +V_9 = Vertex(name = 'V_9', + particles = [ P.H, P.H, P.H ], + color = [ '1' ], + lorentz = [ L.SSS1 ], + couplings = {(0,0):C.GC_69}) + +V_10 = Vertex(name = 'V_10', + particles = [ P.a, P.a, P.G__minus__, P.G__plus__ ], + color = [ '1' ], + lorentz = [ L.VVSS1 ], + couplings = {(0,0):C.GC_6}) + +V_11 = Vertex(name = 'V_11', + particles = [ P.a, P.G__minus__, P.G__plus__ ], + color = [ '1' ], + lorentz = [ L.VSS1 ], + couplings = {(0,0):C.GC_3}) + +V_12 = Vertex(name = 'V_12', + particles = [ P.ghA, P.ghWm__tilde__, P.W__minus__ ], + color = [ '1' ], + lorentz = [ L.UUV1 ], + couplings = {(0,0):C.GC_3}) + +V_13 = Vertex(name = 'V_13', + particles = [ P.ghA, P.ghWp__tilde__, P.W__plus__ ], + color = [ '1' ], + lorentz = [ L.UUV1 ], + couplings = {(0,0):C.GC_4}) + +V_14 = Vertex(name = 'V_14', + particles = [ P.ghWm, P.ghA__tilde__, P.G__plus__ ], + color = [ '1' ], + lorentz = [ L.UUS1 ], + couplings = {(0,0):C.GC_75}) + +V_15 = Vertex(name = 'V_15', + particles = [ P.ghWm, P.ghA__tilde__, P.W__plus__ ], + color = [ '1' ], + lorentz = [ L.UUV1 ], + couplings = {(0,0):C.GC_3}) + +V_16 = Vertex(name = 'V_16', + particles = [ P.ghWm, P.ghWm__tilde__, P.G0 ], + color = [ '1' ], + lorentz = [ L.UUS1 ], + couplings = {(0,0):C.GC_70}) + +V_17 = Vertex(name = 'V_17', + particles = [ P.ghWm, P.ghWm__tilde__, P.H ], + color = [ '1' ], + lorentz = [ L.UUS1 ], + couplings = {(0,0):C.GC_71}) + +V_18 = Vertex(name = 'V_18', + particles = [ P.ghWm, P.ghWm__tilde__, P.a ], + color = [ '1' ], + lorentz = [ L.UUV1 ], + couplings = {(0,0):C.GC_4}) + +V_19 = Vertex(name = 'V_19', + particles = [ P.ghWm, P.ghWm__tilde__, P.Z ], + color = [ '1' ], + lorentz = [ L.UUV1 ], + couplings = {(0,0):C.GC_53}) + +V_20 = Vertex(name = 'V_20', + particles = [ P.ghWm, P.ghZ__tilde__, P.G__plus__ ], + color = [ '1' ], + lorentz = [ L.UUS1 ], + couplings = {(0,0):C.GC_78}) + +V_21 = Vertex(name = 'V_21', + particles = [ P.ghWm, P.ghZ__tilde__, P.W__plus__ ], + color = [ '1' ], + lorentz = [ L.UUV1 ], + couplings = {(0,0):C.GC_52}) + +V_22 = Vertex(name = 'V_22', + particles = [ P.ghWp, P.ghA__tilde__, P.G__minus__ ], + color = [ '1' ], + lorentz = [ L.UUS1 ], + couplings = {(0,0):C.GC_74}) + +V_23 = Vertex(name = 'V_23', + particles = [ P.ghWp, P.ghA__tilde__, P.W__minus__ ], + color = [ '1' ], + lorentz = [ L.UUV1 ], + couplings = {(0,0):C.GC_4}) + +V_24 = Vertex(name = 'V_24', + particles = [ P.ghWp, P.ghWp__tilde__, P.G0 ], + color = [ '1' ], + lorentz = [ L.UUS1 ], + couplings = {(0,0):C.GC_73}) + +V_25 = Vertex(name = 'V_25', + particles = [ P.ghWp, P.ghWp__tilde__, P.H ], + color = [ '1' ], + lorentz = [ L.UUS1 ], + couplings = {(0,0):C.GC_71}) + +V_26 = Vertex(name = 'V_26', + particles = [ P.ghWp, P.ghWp__tilde__, P.a ], + color = [ '1' ], + lorentz = [ L.UUV1 ], + couplings = {(0,0):C.GC_3}) + +V_27 = Vertex(name = 'V_27', + particles = [ P.ghWp, P.ghWp__tilde__, P.Z ], + color = [ '1' ], + lorentz = [ L.UUV1 ], + couplings = {(0,0):C.GC_52}) + +V_28 = Vertex(name = 'V_28', + particles = [ P.ghWp, P.ghZ__tilde__, P.G__minus__ ], + color = [ '1' ], + lorentz = [ L.UUS1 ], + couplings = {(0,0):C.GC_77}) + +V_29 = Vertex(name = 'V_29', + particles = [ P.ghWp, P.ghZ__tilde__, P.W__minus__ ], + color = [ '1' ], + lorentz = [ L.UUV1 ], + couplings = {(0,0):C.GC_53}) + +V_30 = Vertex(name = 'V_30', + particles = [ P.ghZ, P.ghWm__tilde__, P.G__minus__ ], + color = [ '1' ], + lorentz = [ L.UUS1 ], + couplings = {(0,0):C.GC_79}) + +V_31 = Vertex(name = 'V_31', + particles = [ P.ghZ, P.ghWm__tilde__, P.W__minus__ ], + color = [ '1' ], + lorentz = [ L.UUV1 ], + couplings = {(0,0):C.GC_52}) + +V_32 = Vertex(name = 'V_32', + particles = [ P.ghZ, P.ghWp__tilde__, P.G__plus__ ], + color = [ '1' ], + lorentz = [ L.UUS1 ], + couplings = {(0,0):C.GC_76}) + +V_33 = Vertex(name = 'V_33', + particles = [ P.ghZ, P.ghWp__tilde__, P.W__plus__ ], + color = [ '1' ], + lorentz = [ L.UUV1 ], + couplings = {(0,0):C.GC_53}) + +V_34 = Vertex(name = 'V_34', + particles = [ P.ghZ, P.ghZ__tilde__, P.H ], + color = [ '1' ], + lorentz = [ L.UUS1 ], + couplings = {(0,0):C.GC_80}) + +V_35 = Vertex(name = 'V_35', + particles = [ P.ghG, P.ghG__tilde__, P.g ], + color = [ 'f(3,1,2)' ], + lorentz = [ L.UUV1 ], + couplings = {(0,0):C.GC_10}) + +V_36 = Vertex(name = 'V_36', + particles = [ P.g, P.g, P.g ], + color = [ 'f(1,2,3)' ], + lorentz = [ L.VVV1 ], + couplings = {(0,0):C.GC_10}) + +V_37 = Vertex(name = 'V_37', + particles = [ P.g, P.g, P.g, P.g ], + color = [ 'f(-1,1,2)*f(3,4,-1)', 'f(-1,1,3)*f(2,4,-1)', 'f(-1,1,4)*f(2,3,-1)' ], + lorentz = [ L.VVVV1, L.VVVV3, L.VVVV4 ], + couplings = {(1,1):C.GC_12,(0,0):C.GC_12,(2,2):C.GC_12}) + +V_38 = Vertex(name = 'V_38', + particles = [ P.a, P.W__minus__, P.G0, P.G__plus__ ], + color = [ '1' ], + lorentz = [ L.VVSS1 ], + couplings = {(0,0):C.GC_55}) + +V_39 = Vertex(name = 'V_39', + particles = [ P.a, P.W__minus__, P.G__plus__, P.H ], + color = [ '1' ], + lorentz = [ L.VVSS1 ], + couplings = {(0,0):C.GC_54}) + +V_40 = Vertex(name = 'V_40', + particles = [ P.a, P.W__minus__, P.G__plus__ ], + color = [ '1' ], + lorentz = [ L.VVS1 ], + couplings = {(0,0):C.GC_74}) + +V_41 = Vertex(name = 'V_41', + particles = [ P.W__minus__, P.G0, P.G__plus__ ], + color = [ '1' ], + lorentz = [ L.VSS1 ], + couplings = {(0,0):C.GC_39}) + +V_42 = Vertex(name = 'V_42', + particles = [ P.W__minus__, P.G__plus__, P.H ], + color = [ '1' ], + lorentz = [ L.VSS1 ], + couplings = {(0,0):C.GC_37}) + +V_43 = Vertex(name = 'V_43', + particles = [ P.a, P.W__minus__, P.W__plus__ ], + color = [ '1' ], + lorentz = [ L.VVV1 ], + couplings = {(0,0):C.GC_4}) + +V_44 = Vertex(name = 'V_44', + particles = [ P.a, P.W__plus__, P.G0, P.G__minus__ ], + color = [ '1' ], + lorentz = [ L.VVSS1 ], + couplings = {(0,0):C.GC_55}) + +V_45 = Vertex(name = 'V_45', + particles = [ P.a, P.W__plus__, P.G__minus__, P.H ], + color = [ '1' ], + lorentz = [ L.VVSS1 ], + couplings = {(0,0):C.GC_56}) + +V_46 = Vertex(name = 'V_46', + particles = [ P.a, P.W__plus__, P.G__minus__ ], + color = [ '1' ], + lorentz = [ L.VVS1 ], + couplings = {(0,0):C.GC_75}) + +V_47 = Vertex(name = 'V_47', + particles = [ P.W__plus__, P.G0, P.G__minus__ ], + color = [ '1' ], + lorentz = [ L.VSS1 ], + couplings = {(0,0):C.GC_38}) + +V_48 = Vertex(name = 'V_48', + particles = [ P.W__plus__, P.G__minus__, P.H ], + color = [ '1' ], + lorentz = [ L.VSS1 ], + couplings = {(0,0):C.GC_37}) + +V_49 = Vertex(name = 'V_49', + particles = [ P.W__minus__, P.W__plus__, P.G0, P.G0 ], + color = [ '1' ], + lorentz = [ L.VVSS1 ], + couplings = {(0,0):C.GC_34}) + +V_50 = Vertex(name = 'V_50', + particles = [ P.W__minus__, P.W__plus__, P.G__minus__, P.G__plus__ ], + color = [ '1' ], + lorentz = [ L.VVSS1 ], + couplings = {(0,0):C.GC_34}) + +V_51 = Vertex(name = 'V_51', + particles = [ P.W__minus__, P.W__plus__, P.H, P.H ], + color = [ '1' ], + lorentz = [ L.VVSS1 ], + couplings = {(0,0):C.GC_34}) + +V_52 = Vertex(name = 'V_52', + particles = [ P.W__minus__, P.W__plus__, P.H ], + color = [ '1' ], + lorentz = [ L.VVS1 ], + couplings = {(0,0):C.GC_72}) + +V_53 = Vertex(name = 'V_53', + particles = [ P.a, P.a, P.W__minus__, P.W__plus__ ], + color = [ '1' ], + lorentz = [ L.VVVV2 ], + couplings = {(0,0):C.GC_5}) + +V_54 = Vertex(name = 'V_54', + particles = [ P.W__minus__, P.W__plus__, P.Z ], + color = [ '1' ], + lorentz = [ L.VVV1 ], + couplings = {(0,0):C.GC_53}) + +V_55 = Vertex(name = 'V_55', + particles = [ P.W__minus__, P.W__minus__, P.W__plus__, P.W__plus__ ], + color = [ '1' ], + lorentz = [ L.VVVV2 ], + couplings = {(0,0):C.GC_35}) + +V_56 = Vertex(name = 'V_56', + particles = [ P.a, P.Z, P.G__minus__, P.G__plus__ ], + color = [ '1' ], + lorentz = [ L.VVSS1 ], + couplings = {(0,0):C.GC_63}) + +V_57 = Vertex(name = 'V_57', + particles = [ P.Z, P.G0, P.H ], + color = [ '1' ], + lorentz = [ L.VSS1 ], + couplings = {(0,0):C.GC_60}) + +V_58 = Vertex(name = 'V_58', + particles = [ P.Z, P.G__minus__, P.G__plus__ ], + color = [ '1' ], + lorentz = [ L.VSS1 ], + couplings = {(0,0):C.GC_61}) + +V_59 = Vertex(name = 'V_59', + particles = [ P.W__minus__, P.Z, P.G0, P.G__plus__ ], + color = [ '1' ], + lorentz = [ L.VVSS1 ], + couplings = {(0,0):C.GC_8}) + +V_60 = Vertex(name = 'V_60', + particles = [ P.W__minus__, P.Z, P.G__plus__, P.H ], + color = [ '1' ], + lorentz = [ L.VVSS1 ], + couplings = {(0,0):C.GC_9}) + +V_61 = Vertex(name = 'V_61', + particles = [ P.W__minus__, P.Z, P.G__plus__ ], + color = [ '1' ], + lorentz = [ L.VVS1 ], + couplings = {(0,0):C.GC_67}) + +V_62 = Vertex(name = 'V_62', + particles = [ P.W__plus__, P.Z, P.G0, P.G__minus__ ], + color = [ '1' ], + lorentz = [ L.VVSS1 ], + couplings = {(0,0):C.GC_8}) + +V_63 = Vertex(name = 'V_63', + particles = [ P.W__plus__, P.Z, P.G__minus__, P.H ], + color = [ '1' ], + lorentz = [ L.VVSS1 ], + couplings = {(0,0):C.GC_7}) + +V_64 = Vertex(name = 'V_64', + particles = [ P.W__plus__, P.Z, P.G__minus__ ], + color = [ '1' ], + lorentz = [ L.VVS1 ], + couplings = {(0,0):C.GC_66}) + +V_65 = Vertex(name = 'V_65', + particles = [ P.a, P.W__minus__, P.W__plus__, P.Z ], + color = [ '1' ], + lorentz = [ L.VVVV5 ], + couplings = {(0,0):C.GC_57}) + +V_66 = Vertex(name = 'V_66', + particles = [ P.Z, P.Z, P.G0, P.G0 ], + color = [ '1' ], + lorentz = [ L.VVSS1 ], + couplings = {(0,0):C.GC_65}) + +V_67 = Vertex(name = 'V_67', + particles = [ P.Z, P.Z, P.G__minus__, P.G__plus__ ], + color = [ '1' ], + lorentz = [ L.VVSS1 ], + couplings = {(0,0):C.GC_64}) + +V_68 = Vertex(name = 'V_68', + particles = [ P.Z, P.Z, P.H, P.H ], + color = [ '1' ], + lorentz = [ L.VVSS1 ], + couplings = {(0,0):C.GC_65}) + +V_69 = Vertex(name = 'V_69', + particles = [ P.Z, P.Z, P.H ], + color = [ '1' ], + lorentz = [ L.VVS1 ], + couplings = {(0,0):C.GC_81}) + +V_70 = Vertex(name = 'V_70', + particles = [ P.W__minus__, P.W__plus__, P.Z, P.Z ], + color = [ '1' ], + lorentz = [ L.VVVV2 ], + couplings = {(0,0):C.GC_36}) + +V_71 = Vertex(name = 'V_71', + particles = [ P.d__tilde__, P.d, P.a ], + color = [ 'Identity(1,2)' ], + lorentz = [ L.FFV1 ], + couplings = {(0,0):C.GC_1}) + +V_72 = Vertex(name = 'V_72', + particles = [ P.s__tilde__, P.s, P.a ], + color = [ 'Identity(1,2)' ], + lorentz = [ L.FFV1 ], + couplings = {(0,0):C.GC_1}) + +V_73 = Vertex(name = 'V_73', + particles = [ P.b__tilde__, P.b, P.a ], + color = [ 'Identity(1,2)' ], + lorentz = [ L.FFV1 ], + couplings = {(0,0):C.GC_1}) + +V_74 = Vertex(name = 'V_74', + particles = [ P.d__tilde__, P.d, P.g ], + color = [ 'T(3,2,1)' ], + lorentz = [ L.FFV1 ], + couplings = {(0,0):C.GC_11}) + +V_75 = Vertex(name = 'V_75', + particles = [ P.s__tilde__, P.s, P.g ], + color = [ 'T(3,2,1)' ], + lorentz = [ L.FFV1 ], + couplings = {(0,0):C.GC_11}) + +V_76 = Vertex(name = 'V_76', + particles = [ P.b__tilde__, P.b, P.g ], + color = [ 'T(3,2,1)' ], + lorentz = [ L.FFV1 ], + couplings = {(0,0):C.GC_11}) + +V_77 = Vertex(name = 'V_77', + particles = [ P.b__tilde__, P.b, P.G0 ], + color = [ 'Identity(1,2)' ], + lorentz = [ L.FFS2 ], + couplings = {(0,0):C.GC_82}) + +V_78 = Vertex(name = 'V_78', + particles = [ P.b__tilde__, P.b, P.H ], + color = [ 'Identity(1,2)' ], + lorentz = [ L.FFS4 ], + couplings = {(0,0):C.GC_83}) + +V_79 = Vertex(name = 'V_79', + particles = [ P.d__tilde__, P.d, P.Z ], + color = [ 'Identity(1,2)' ], + lorentz = [ L.FFV2, L.FFV3 ], + couplings = {(0,0):C.GC_50,(0,1):C.GC_58}) + +V_80 = Vertex(name = 'V_80', + particles = [ P.s__tilde__, P.s, P.Z ], + color = [ 'Identity(1,2)' ], + lorentz = [ L.FFV2, L.FFV3 ], + couplings = {(0,0):C.GC_50,(0,1):C.GC_58}) + +V_81 = Vertex(name = 'V_81', + particles = [ P.b__tilde__, P.b, P.Z ], + color = [ 'Identity(1,2)' ], + lorentz = [ L.FFV2, L.FFV3 ], + couplings = {(0,1):C.GC_58,(0,0):C.GC_50}) + +V_82 = Vertex(name = 'V_82', + particles = [ P.c__tilde__, P.d, P.G__plus__ ], + color = [ 'Identity(1,2)' ], + lorentz = [ L.FFS3 ], + couplings = {(0,0):C.GC_16}) + +V_83 = Vertex(name = 'V_83', + particles = [ P.t__tilde__, P.d, P.G__plus__ ], + color = [ 'Identity(1,2)' ], + lorentz = [ L.FFS3 ], + couplings = {(0,0):C.GC_17}) + +V_84 = Vertex(name = 'V_84', + particles = [ P.c__tilde__, P.s, P.G__plus__ ], + color = [ 'Identity(1,2)' ], + lorentz = [ L.FFS3 ], + couplings = {(0,0):C.GC_18}) + +V_85 = Vertex(name = 'V_85', + particles = [ P.t__tilde__, P.s, P.G__plus__ ], + color = [ 'Identity(1,2)' ], + lorentz = [ L.FFS3 ], + couplings = {(0,0):C.GC_19}) + +V_86 = Vertex(name = 'V_86', + particles = [ P.u__tilde__, P.b, P.G__plus__ ], + color = [ 'Identity(1,2)' ], + lorentz = [ L.FFS1 ], + couplings = {(0,0):C.GC_13}) + +V_87 = Vertex(name = 'V_87', + particles = [ P.c__tilde__, P.b, P.G__plus__ ], + color = [ 'Identity(1,2)' ], + lorentz = [ L.FFS1, L.FFS3 ], + couplings = {(0,0):C.GC_14,(0,1):C.GC_20}) + +V_88 = Vertex(name = 'V_88', + particles = [ P.t__tilde__, P.b, P.G__plus__ ], + color = [ 'Identity(1,2)' ], + lorentz = [ L.FFS1, L.FFS3 ], + couplings = {(0,0):C.GC_15,(0,1):C.GC_21}) + +V_89 = Vertex(name = 'V_89', + particles = [ P.u__tilde__, P.d, P.W__plus__ ], + color = [ 'Identity(1,2)' ], + lorentz = [ L.FFV2 ], + couplings = {(0,0):C.GC_100}) + +V_90 = Vertex(name = 'V_90', + particles = [ P.c__tilde__, P.d, P.W__plus__ ], + color = [ 'Identity(1,2)' ], + lorentz = [ L.FFV2 ], + couplings = {(0,0):C.GC_103}) + +V_91 = Vertex(name = 'V_91', + particles = [ P.t__tilde__, P.d, P.W__plus__ ], + color = [ 'Identity(1,2)' ], + lorentz = [ L.FFV2 ], + couplings = {(0,0):C.GC_106}) + +V_92 = Vertex(name = 'V_92', + particles = [ P.u__tilde__, P.s, P.W__plus__ ], + color = [ 'Identity(1,2)' ], + lorentz = [ L.FFV2 ], + couplings = {(0,0):C.GC_101}) + +V_93 = Vertex(name = 'V_93', + particles = [ P.c__tilde__, P.s, P.W__plus__ ], + color = [ 'Identity(1,2)' ], + lorentz = [ L.FFV2 ], + couplings = {(0,0):C.GC_104}) + +V_94 = Vertex(name = 'V_94', + particles = [ P.t__tilde__, P.s, P.W__plus__ ], + color = [ 'Identity(1,2)' ], + lorentz = [ L.FFV2 ], + couplings = {(0,0):C.GC_107}) + +V_95 = Vertex(name = 'V_95', + particles = [ P.u__tilde__, P.b, P.W__plus__ ], + color = [ 'Identity(1,2)' ], + lorentz = [ L.FFV2 ], + couplings = {(0,0):C.GC_102}) + +V_96 = Vertex(name = 'V_96', + particles = [ P.c__tilde__, P.b, P.W__plus__ ], + color = [ 'Identity(1,2)' ], + lorentz = [ L.FFV2 ], + couplings = {(0,0):C.GC_105}) + +V_97 = Vertex(name = 'V_97', + particles = [ P.t__tilde__, P.b, P.W__plus__ ], + color = [ 'Identity(1,2)' ], + lorentz = [ L.FFV2 ], + couplings = {(0,0):C.GC_108}) + +V_98 = Vertex(name = 'V_98', + particles = [ P.e__plus__, P.e__minus__, P.a ], + color = [ '1' ], + lorentz = [ L.FFV1 ], + couplings = {(0,0):C.GC_3}) + +V_99 = Vertex(name = 'V_99', + particles = [ P.mu__plus__, P.mu__minus__, P.a ], + color = [ '1' ], + lorentz = [ L.FFV1 ], + couplings = {(0,0):C.GC_3}) + +V_100 = Vertex(name = 'V_100', + particles = [ P.ta__plus__, P.ta__minus__, P.a ], + color = [ '1' ], + lorentz = [ L.FFV1 ], + couplings = {(0,0):C.GC_3}) + +V_101 = Vertex(name = 'V_101', + particles = [ P.e__plus__, P.e__minus__, P.G0 ], + color = [ '1' ], + lorentz = [ L.FFS2 ], + couplings = {(0,0):C.GC_88}) + +V_102 = Vertex(name = 'V_102', + particles = [ P.mu__plus__, P.mu__minus__, P.G0 ], + color = [ '1' ], + lorentz = [ L.FFS2 ], + couplings = {(0,0):C.GC_92}) + +V_103 = Vertex(name = 'V_103', + particles = [ P.ta__plus__, P.ta__minus__, P.G0 ], + color = [ '1' ], + lorentz = [ L.FFS2 ], + couplings = {(0,0):C.GC_98}) + +V_104 = Vertex(name = 'V_104', + particles = [ P.e__plus__, P.e__minus__, P.H ], + color = [ '1' ], + lorentz = [ L.FFS4 ], + couplings = {(0,0):C.GC_89}) + +V_105 = Vertex(name = 'V_105', + particles = [ P.mu__plus__, P.mu__minus__, P.H ], + color = [ '1' ], + lorentz = [ L.FFS4 ], + couplings = {(0,0):C.GC_93}) + +V_106 = Vertex(name = 'V_106', + particles = [ P.ta__plus__, P.ta__minus__, P.H ], + color = [ '1' ], + lorentz = [ L.FFS4 ], + couplings = {(0,0):C.GC_99}) + +V_107 = Vertex(name = 'V_107', + particles = [ P.e__plus__, P.e__minus__, P.Z ], + color = [ '1' ], + lorentz = [ L.FFV2, L.FFV4 ], + couplings = {(0,0):C.GC_50,(0,1):C.GC_59}) + +V_108 = Vertex(name = 'V_108', + particles = [ P.mu__plus__, P.mu__minus__, P.Z ], + color = [ '1' ], + lorentz = [ L.FFV2, L.FFV4 ], + couplings = {(0,0):C.GC_50,(0,1):C.GC_59}) + +V_109 = Vertex(name = 'V_109', + particles = [ P.ta__plus__, P.ta__minus__, P.Z ], + color = [ '1' ], + lorentz = [ L.FFV2, L.FFV4 ], + couplings = {(0,0):C.GC_50,(0,1):C.GC_59}) + +V_110 = Vertex(name = 'V_110', + particles = [ P.ve__tilde__, P.e__minus__, P.G__plus__ ], + color = [ '1' ], + lorentz = [ L.FFS1 ], + couplings = {(0,0):C.GC_87}) + +V_111 = Vertex(name = 'V_111', + particles = [ P.vm__tilde__, P.mu__minus__, P.G__plus__ ], + color = [ '1' ], + lorentz = [ L.FFS1 ], + couplings = {(0,0):C.GC_91}) + +V_112 = Vertex(name = 'V_112', + particles = [ P.vt__tilde__, P.ta__minus__, P.G__plus__ ], + color = [ '1' ], + lorentz = [ L.FFS1 ], + couplings = {(0,0):C.GC_97}) + +V_113 = Vertex(name = 'V_113', + particles = [ P.ve__tilde__, P.e__minus__, P.W__plus__ ], + color = [ '1' ], + lorentz = [ L.FFV2 ], + couplings = {(0,0):C.GC_40}) + +V_114 = Vertex(name = 'V_114', + particles = [ P.vm__tilde__, P.mu__minus__, P.W__plus__ ], + color = [ '1' ], + lorentz = [ L.FFV2 ], + couplings = {(0,0):C.GC_40}) + +V_115 = Vertex(name = 'V_115', + particles = [ P.vt__tilde__, P.ta__minus__, P.W__plus__ ], + color = [ '1' ], + lorentz = [ L.FFV2 ], + couplings = {(0,0):C.GC_40}) + +V_116 = Vertex(name = 'V_116', + particles = [ P.b__tilde__, P.u, P.G__minus__ ], + color = [ 'Identity(1,2)' ], + lorentz = [ L.FFS3 ], + couplings = {(0,0):C.GC_28}) + +V_117 = Vertex(name = 'V_117', + particles = [ P.d__tilde__, P.c, P.G__minus__ ], + color = [ 'Identity(1,2)' ], + lorentz = [ L.FFS1 ], + couplings = {(0,0):C.GC_22}) + +V_118 = Vertex(name = 'V_118', + particles = [ P.s__tilde__, P.c, P.G__minus__ ], + color = [ 'Identity(1,2)' ], + lorentz = [ L.FFS1 ], + couplings = {(0,0):C.GC_23}) + +V_119 = Vertex(name = 'V_119', + particles = [ P.b__tilde__, P.c, P.G__minus__ ], + color = [ 'Identity(1,2)' ], + lorentz = [ L.FFS1, L.FFS3 ], + couplings = {(0,0):C.GC_24,(0,1):C.GC_29}) + +V_120 = Vertex(name = 'V_120', + particles = [ P.d__tilde__, P.t, P.G__minus__ ], + color = [ 'Identity(1,2)' ], + lorentz = [ L.FFS1 ], + couplings = {(0,0):C.GC_25}) + +V_121 = Vertex(name = 'V_121', + particles = [ P.s__tilde__, P.t, P.G__minus__ ], + color = [ 'Identity(1,2)' ], + lorentz = [ L.FFS1 ], + couplings = {(0,0):C.GC_26}) + +V_122 = Vertex(name = 'V_122', + particles = [ P.b__tilde__, P.t, P.G__minus__ ], + color = [ 'Identity(1,2)' ], + lorentz = [ L.FFS1, L.FFS3 ], + couplings = {(0,0):C.GC_27,(0,1):C.GC_30}) + +V_123 = Vertex(name = 'V_123', + particles = [ P.d__tilde__, P.u, P.W__minus__ ], + color = [ 'Identity(1,2)' ], + lorentz = [ L.FFV2 ], + couplings = {(0,0):C.GC_41}) + +V_124 = Vertex(name = 'V_124', + particles = [ P.s__tilde__, P.u, P.W__minus__ ], + color = [ 'Identity(1,2)' ], + lorentz = [ L.FFV2 ], + couplings = {(0,0):C.GC_42}) + +V_125 = Vertex(name = 'V_125', + particles = [ P.b__tilde__, P.u, P.W__minus__ ], + color = [ 'Identity(1,2)' ], + lorentz = [ L.FFV2 ], + couplings = {(0,0):C.GC_43}) + +V_126 = Vertex(name = 'V_126', + particles = [ P.d__tilde__, P.c, P.W__minus__ ], + color = [ 'Identity(1,2)' ], + lorentz = [ L.FFV2 ], + couplings = {(0,0):C.GC_44}) + +V_127 = Vertex(name = 'V_127', + particles = [ P.s__tilde__, P.c, P.W__minus__ ], + color = [ 'Identity(1,2)' ], + lorentz = [ L.FFV2 ], + couplings = {(0,0):C.GC_45}) + +V_128 = Vertex(name = 'V_128', + particles = [ P.b__tilde__, P.c, P.W__minus__ ], + color = [ 'Identity(1,2)' ], + lorentz = [ L.FFV2 ], + couplings = {(0,0):C.GC_46}) + +V_129 = Vertex(name = 'V_129', + particles = [ P.d__tilde__, P.t, P.W__minus__ ], + color = [ 'Identity(1,2)' ], + lorentz = [ L.FFV2 ], + couplings = {(0,0):C.GC_47}) + +V_130 = Vertex(name = 'V_130', + particles = [ P.s__tilde__, P.t, P.W__minus__ ], + color = [ 'Identity(1,2)' ], + lorentz = [ L.FFV2 ], + couplings = {(0,0):C.GC_48}) + +V_131 = Vertex(name = 'V_131', + particles = [ P.b__tilde__, P.t, P.W__minus__ ], + color = [ 'Identity(1,2)' ], + lorentz = [ L.FFV2 ], + couplings = {(0,0):C.GC_49}) + +V_132 = Vertex(name = 'V_132', + particles = [ P.u__tilde__, P.u, P.a ], + color = [ 'Identity(1,2)' ], + lorentz = [ L.FFV1 ], + couplings = {(0,0):C.GC_2}) + +V_133 = Vertex(name = 'V_133', + particles = [ P.c__tilde__, P.c, P.a ], + color = [ 'Identity(1,2)' ], + lorentz = [ L.FFV1 ], + couplings = {(0,0):C.GC_2}) + +V_134 = Vertex(name = 'V_134', + particles = [ P.t__tilde__, P.t, P.a ], + color = [ 'Identity(1,2)' ], + lorentz = [ L.FFV1 ], + couplings = {(0,0):C.GC_2}) + +V_135 = Vertex(name = 'V_135', + particles = [ P.u__tilde__, P.u, P.g ], + color = [ 'T(3,2,1)' ], + lorentz = [ L.FFV1 ], + couplings = {(0,0):C.GC_11}) + +V_136 = Vertex(name = 'V_136', + particles = [ P.c__tilde__, P.c, P.g ], + color = [ 'T(3,2,1)' ], + lorentz = [ L.FFV1 ], + couplings = {(0,0):C.GC_11}) + +V_137 = Vertex(name = 'V_137', + particles = [ P.t__tilde__, P.t, P.g ], + color = [ 'T(3,2,1)' ], + lorentz = [ L.FFV1 ], + couplings = {(0,0):C.GC_11}) + +V_138 = Vertex(name = 'V_138', + particles = [ P.c__tilde__, P.c, P.G0 ], + color = [ 'Identity(1,2)' ], + lorentz = [ L.FFS2 ], + couplings = {(0,0):C.GC_85}) + +V_139 = Vertex(name = 'V_139', + particles = [ P.t__tilde__, P.t, P.G0 ], + color = [ 'Identity(1,2)' ], + lorentz = [ L.FFS2 ], + couplings = {(0,0):C.GC_95}) + +V_140 = Vertex(name = 'V_140', + particles = [ P.c__tilde__, P.c, P.H ], + color = [ 'Identity(1,2)' ], + lorentz = [ L.FFS4 ], + couplings = {(0,0):C.GC_84}) + +V_141 = Vertex(name = 'V_141', + particles = [ P.t__tilde__, P.t, P.H ], + color = [ 'Identity(1,2)' ], + lorentz = [ L.FFS4 ], + couplings = {(0,0):C.GC_94}) + +V_142 = Vertex(name = 'V_142', + particles = [ P.u__tilde__, P.u, P.Z ], + color = [ 'Identity(1,2)' ], + lorentz = [ L.FFV2, L.FFV5 ], + couplings = {(0,0):C.GC_51,(0,1):C.GC_58}) + +V_143 = Vertex(name = 'V_143', + particles = [ P.c__tilde__, P.c, P.Z ], + color = [ 'Identity(1,2)' ], + lorentz = [ L.FFV2, L.FFV5 ], + couplings = {(0,0):C.GC_51,(0,1):C.GC_58}) + +V_144 = Vertex(name = 'V_144', + particles = [ P.t__tilde__, P.t, P.Z ], + color = [ 'Identity(1,2)' ], + lorentz = [ L.FFV2, L.FFV5 ], + couplings = {(0,0):C.GC_51,(0,1):C.GC_58}) + +V_145 = Vertex(name = 'V_145', + particles = [ P.e__plus__, P.ve, P.G__minus__ ], + color = [ '1' ], + lorentz = [ L.FFS3 ], + couplings = {(0,0):C.GC_86}) + +V_146 = Vertex(name = 'V_146', + particles = [ P.mu__plus__, P.vm, P.G__minus__ ], + color = [ '1' ], + lorentz = [ L.FFS3 ], + couplings = {(0,0):C.GC_90}) + +V_147 = Vertex(name = 'V_147', + particles = [ P.ta__plus__, P.vt, P.G__minus__ ], + color = [ '1' ], + lorentz = [ L.FFS3 ], + couplings = {(0,0):C.GC_96}) + +V_148 = Vertex(name = 'V_148', + particles = [ P.e__plus__, P.ve, P.W__minus__ ], + color = [ '1' ], + lorentz = [ L.FFV2 ], + couplings = {(0,0):C.GC_40}) + +V_149 = Vertex(name = 'V_149', + particles = [ P.mu__plus__, P.vm, P.W__minus__ ], + color = [ '1' ], + lorentz = [ L.FFV2 ], + couplings = {(0,0):C.GC_40}) + +V_150 = Vertex(name = 'V_150', + particles = [ P.ta__plus__, P.vt, P.W__minus__ ], + color = [ '1' ], + lorentz = [ L.FFV2 ], + couplings = {(0,0):C.GC_40}) + +V_151 = Vertex(name = 'V_151', + particles = [ P.ve__tilde__, P.ve, P.Z ], + color = [ '1' ], + lorentz = [ L.FFV2 ], + couplings = {(0,0):C.GC_62}) + +V_152 = Vertex(name = 'V_152', + particles = [ P.vm__tilde__, P.vm, P.Z ], + color = [ '1' ], + lorentz = [ L.FFV2 ], + couplings = {(0,0):C.GC_62}) + +V_153 = Vertex(name = 'V_153', + particles = [ P.vt__tilde__, P.vt, P.Z ], + color = [ '1' ], + lorentz = [ L.FFV2 ], + couplings = {(0,0):C.GC_62}) diff --git a/pyamplicol/src/pyamplicol/assets/models/ufo/sm/write_param_card.py b/pyamplicol/src/pyamplicol/assets/models/ufo/sm/write_param_card.py new file mode 100644 index 0000000000..3abf4cff0c --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/models/ufo/sm/write_param_card.py @@ -0,0 +1,184 @@ + +from __future__ import absolute_import +from __future__ import print_function +from six.moves import range +__date__ = "3 june 2010" +__author__ = 'olivier.mattelaer@uclouvain.be' + +class ParamCardWriter(object): + + header = \ + """######################################################################\n""" + \ + """## PARAM_CARD AUTOMATICALY GENERATED BY THE UFO #####################\n""" + \ + """######################################################################\n""" + + def __init__(self, filename, list_of_parameters=None, generic=False): + """write a valid param_card.dat""" + + if not list_of_parameters: + from .parameters import all_parameters + list_of_parameters = [param for param in all_parameters if \ + param.nature=='external'] + + self.generic_output = generic + if generic: + self.define_not_dep_param(list_of_parameters) + + + self.fsock = open(filename, 'w') + self.fsock.write(self.header) + + self.write_card(list_of_parameters) + + def define_not_dep_param(self, list_of_parameters): + """define self.dep_mass and self.dep_width in case that they are + requested in the param_card.dat""" + from .particles import all_particles + + self.dep_mass = [(part, part.mass) for part in all_particles \ + if part.pdg_code > 0 and \ + part.mass not in list_of_parameters] + self.dep_width = [(part, part.width) for part in all_particles\ + if part.pdg_code > 0 and \ + part.width not in list_of_parameters] + + @staticmethod + def order_param(obj1, obj2): + """ order parameter of a given block """ + + maxlen = min([len(obj1.lhacode), len(obj2.lhacode)]) + + for i in range(maxlen): + if obj1.lhacode[i] < obj2.lhacode[i]: + return -1 + elif obj1.lhacode[i] == obj2.lhacode[i]: + return 0 + else: + return 1 + #identical up to the first finish + if len(obj1.lhacode) > len(obj2.lhacode): + return 1 + elif len(obj1.lhacode) == len(obj2.lhacode): + return 0 + else: + return -1 + + def write_card(self, all_ext_param): + """ """ + + # list all lhablock + all_lhablock = set([param.lhablock for param in all_ext_param]) + + # ordonate lhablock alphabeticaly + all_lhablock = list(all_lhablock) + all_lhablock.sort() + # put at the beginning SMINPUT + MASS + DECAY + for name in ['DECAY', 'MASS','SMINPUTS']: + if name in all_lhablock: + all_lhablock.remove(name) + all_lhablock.insert(0, name) + + for lhablock in all_lhablock: + self.write_block(lhablock) + need_writing = [ param for param in all_ext_param if \ + param.lhablock == lhablock] + from functools import cmp_to_key + need_writing.sort(key=cmp_to_key(self.order_param)) + [self.write_param(param, lhablock) for param in need_writing] + + if self.generic_output: + if lhablock in ['MASS', 'DECAY']: + self.write_dep_param_block(lhablock) + + if self.generic_output: + self.write_qnumber() + + def write_block(self, name): + """ write a comment for a block""" + + self.fsock.writelines( + """\n###################################""" + \ + """\n## INFORMATION FOR %s""" % name.upper() +\ + """\n###################################\n""" + ) + if name!='DECAY': + self.fsock.write("""Block %s \n""" % name) + + def write_param(self, param, lhablock): + + lhacode=' '.join(['%3s' % key for key in param.lhacode]) + if lhablock != 'DECAY': + text = """ %s %e # %s \n""" % (lhacode, complex(param.value).real, param.name ) + else: + text = '''DECAY %s %e \n''' % (lhacode, complex(param.value).real) + self.fsock.write(text) + + + + + def write_dep_param_block(self, lhablock): + import cmath + from .parameters import all_parameters + for parameter in all_parameters: + try: + exec("%s = %s" % (parameter.name, parameter.value)) + except Exception: + pass + text = "## Not dependent paramater.\n" + text += "## Those values should be edited following analytical the \n" + text += "## analytical expression. Some generator could simply ignore \n" + text += "## those values and use the analytical expression\n" + + if lhablock == 'MASS': + data = self.dep_mass + prefix = " " + else: + data = self.dep_width + prefix = "DECAY " + for part, param in data: + if isinstance(param.value, str): + value = complex(eval(param.value)).real + else: + value = param.value + + text += """%s %s %f # %s : %s \n""" %(prefix, part.pdg_code, + value, part.name, param.value) + self.fsock.write(text) + + sm_pdg = [1,2,3,4,5,6,11,12,13,13,14,15,16,21,22,23,24,25] + data="""Block QNUMBERS %(pdg)d # %(name)s + 1 %(charge)d # 3 times electric charge + 2 %(spin)d # number of spin states (2S+1) + 3 %(color)d # colour rep (1: singlet, 3: triplet, 8: octet) + 4 %(antipart)d # Particle/Antiparticle distinction (0=own anti)\n""" + + def write_qnumber(self): + """ write qnumber """ + from .particles import all_particles + + text="""#===========================================================\n""" + text += """# QUANTUM NUMBERS OF NEW STATE(S) (NON SM PDG CODE)\n""" + text += """#===========================================================\n\n""" + + for part in all_particles: + if part.pdg_code in self.sm_pdg or part.pdg_code < 0: + continue + text += self.data % {'pdg': part.pdg_code, + 'name': part.name, + 'charge': 3 * part.charge, + 'spin': 2 * part.spin + 1, + 'color': part.color, + 'antipart': part.name != part.antiname and 1 or 0} + + self.fsock.write(text) + + + + + + + + +if '__main__' == __name__: + ParamCardWriter('./param_card.dat', generic=True) + print('write ./param_card.dat') diff --git a/pyamplicol/src/pyamplicol/assets/prepared_models/__init__.py b/pyamplicol/src/pyamplicol/assets/prepared_models/__init__.py new file mode 100644 index 0000000000..b5aa349ed2 --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/prepared_models/__init__.py @@ -0,0 +1,367 @@ +# SPDX-License-Identifier: 0BSD +"""Discovery and validation for wheel-owned prepared model bundles.""" + +from __future__ import annotations + +import hashlib +import json +import os +import tempfile +from collections.abc import Iterator, Mapping, Sequence +from contextlib import contextmanager +from importlib import resources +from pathlib import Path +from typing import TYPE_CHECKING, cast + +if TYPE_CHECKING: + from pyamplicol.models.prepared import PreparedModelBundle + +BUILTIN_SM_JIT_O2 = "built-in-sm-jit-o2" +BUILTIN_SM_HEFT_JIT_O2 = "built-in-sm-heft-jit-o2" +_PACKAGED_ARCHITECTURES = ("aarch64", "x86_64") +_MODEL_SOURCE_BY_ID = { + BUILTIN_SM_JIT_O2: "built-in-sm", + BUILTIN_SM_HEFT_JIT_O2: "built-in-sm-heft", +} +_KNOWN_MODELS = tuple(_MODEL_SOURCE_BY_ID) +_METADATA_KEYS = frozenset( + { + "backend", + "bundle", + "bundle_sha256", + "bundle_size", + "build_contract", + "dependencies", + "eager_kernel_abi", + "id", + "jit_optimization_level", + "kernel_count", + "model", + "prepared_model_bundle_schema", + "producer", + "schema_version", + "target", + } +) + + +class PackagedPreparedModelError(RuntimeError): + """Raised when an installed prepared-model resource is absent or stale.""" + + +def available_prepared_models() -> tuple[str, ...]: + """Return stable identifiers for prepared models shipped in this wheel.""" + + return _KNOWN_MODELS + + +@contextmanager +def packaged_prepared_model_path(identifier: str) -> Iterator[Path]: + """Materialize and validate one packaged bundle for path-based consumers.""" + + if identifier not in _KNOWN_MODELS: + choices = ", ".join(_KNOWN_MODELS) + raise PackagedPreparedModelError( + f"unknown packaged prepared model {identifier!r}; available: {choices}" + ) + from pyamplicol.models.prepared_target import ( + PreparedTargetError, + canonical_architecture, + ) + + try: + architecture = canonical_architecture() + except PreparedTargetError as error: + raise PackagedPreparedModelError(str(error)) from error + if architecture not in _PACKAGED_ARCHITECTURES: # pragma: no cover + raise PackagedPreparedModelError( + f"no wheel-owned prepared model supports {architecture!r}" + ) + metadata_name, bundle_name = _asset_names(identifier, architecture) + root = resources.files(__package__) + metadata_resource = root.joinpath(metadata_name) + bundle_resource = root.joinpath(bundle_name) + try: + metadata = _metadata( + json.loads(metadata_resource.read_text(encoding="utf-8")), + identifier=identifier, + bundle_name=bundle_name, + ) + except ( + FileNotFoundError, + OSError, + UnicodeDecodeError, + json.JSONDecodeError, + ) as error: + raise PackagedPreparedModelError( + f"cannot read packaged prepared-model metadata: {error}" + ) from error + with resources.as_file(bundle_resource) as path: + bundle_path = Path(path) + if not bundle_path.is_file() or bundle_path.is_symlink(): + raise PackagedPreparedModelError( + "packaged prepared-model bundle is not a regular file" + ) + data = bundle_path.read_bytes() + if metadata.get("bundle_size") != len(data): + raise PackagedPreparedModelError( + "packaged prepared-model bundle size does not match metadata" + ) + if metadata.get("bundle_sha256") != hashlib.sha256(data).hexdigest(): + raise PackagedPreparedModelError( + "packaged prepared-model bundle SHA-256 does not match metadata" + ) + _validate_bundle( + bundle_path, + metadata, + identifier=identifier, + architecture=architecture, + ) + yield bundle_path + + +@contextmanager +def open_packaged_prepared_model( + identifier: str = BUILTIN_SM_JIT_O2, +) -> Iterator[PreparedModelBundle]: + """Open a validated bundle while keeping zip-installed resources alive.""" + + from pyamplicol.models.prepared import load_prepared_model_bundle + + with packaged_prepared_model_path(identifier) as path: + yield load_prepared_model_bundle(path) + + +def materialize_packaged_prepared_model( + identifier: str = BUILTIN_SM_JIT_O2, + *, + cache_dir: Path | None = None, +) -> Path: + """Return a stable path for a validated wheel-owned prepared model. + + Installed wheels normally expose resources as ordinary files, but the + resource API also permits transient extraction from archive importers. + Eager generation retains the bundle path while it copies referenced + kernels into the process artifact, so materialize one content-addressed + cache entry instead of leaking a temporary resource path. + """ + + with packaged_prepared_model_path(identifier) as source: + bundle_name = source.name + data = source.read_bytes() + digest = hashlib.sha256(data).hexdigest() + if cache_dir is None: + from platformdirs import user_cache_path + + root = user_cache_path("pyamplicol") / "prepared_models" / digest + else: + root = Path(cache_dir).expanduser().resolve(strict=False) / digest + root.mkdir(parents=True, exist_ok=True) + if root.is_symlink() or not root.is_dir(): + raise PackagedPreparedModelError( + "packaged prepared-model cache must be a regular directory" + ) + output = root / bundle_name + if output.exists(): + if output.is_symlink() or not output.is_file(): + raise PackagedPreparedModelError( + "packaged prepared-model cache entry must be a regular file" + ) + if hashlib.sha256(output.read_bytes()).hexdigest() == digest: + return output + + descriptor, temporary_name = tempfile.mkstemp( + prefix=f".{bundle_name}.", + suffix=".tmp", + dir=root, + ) + temporary = Path(temporary_name) + try: + with os.fdopen(descriptor, "wb") as stream: + stream.write(data) + stream.flush() + os.fsync(stream.fileno()) + temporary.replace(output) + finally: + temporary.unlink(missing_ok=True) + if hashlib.sha256(output.read_bytes()).hexdigest() != digest: + raise PackagedPreparedModelError( + "materialized prepared-model cache entry has a SHA-256 mismatch" + ) + return output + + +def _validate_bundle( + path: Path, + metadata: Mapping[str, object], + *, + identifier: str, + architecture: str, +) -> None: + from pyamplicol._internal.versions import ( + COMPILED_MODEL_SCHEMA_VERSION, + SYMBOLICA_SERIALIZATION_ABI, + SYMJIT_APPLICATION_ABI, + SYMJIT_PLANE_APPLICATION_ABI, + ) + from pyamplicol.models.loading import MODEL_COMPILER_VERSION, compiler_fingerprint + from pyamplicol.models.prepared import ( + EAGER_KERNEL_ABI, + PREPARED_MODEL_BUNDLE_SCHEMA_VERSION, + load_prepared_model_bundle, + ) + from pyamplicol.models.prepared_target import symjit_storage_v3_target + + try: + bundle = load_prepared_model_bundle(path) + except (OSError, TypeError, ValueError) as error: + raise PackagedPreparedModelError( + f"packaged prepared-model bundle is invalid: {error}" + ) from error + pack = bundle.kernel_pack + producer = _mapping(metadata.get("producer"), "metadata.producer") + dependencies = _mapping(metadata.get("dependencies"), "metadata.dependencies") + compiled_producer = _mapping( + bundle.compiled_model.get("producer"), "compiled_model.producer" + ) + compiled_source = _mapping( + bundle.compiled_model.get("source"), "compiled_model.source" + ) + fingerprint = compiler_fingerprint() + expected = { + "compiled_model_schema": COMPILED_MODEL_SCHEMA_VERSION, + "model_compiler_version": MODEL_COMPILER_VERSION, + } + for key, value in expected.items(): + if producer.get(key) != value: + raise PackagedPreparedModelError( + f"packaged prepared-model producer {key} is stale: " + f"expected {value!r}, got {producer.get(key)!r}" + ) + packaged_package_version = producer.get("package_version") + if compiled_producer.get("pyamplicol") != packaged_package_version: + raise PackagedPreparedModelError( + "prepared compiled-model package version disagrees with metadata" + ) + if ( + compiled_producer.get("compiled_model_schema_version") + != expected["compiled_model_schema"] + ): + raise PackagedPreparedModelError("prepared compiled-model schema is stale") + if ( + compiled_producer.get("model_compiler_version") + != expected["model_compiler_version"] + ): + raise PackagedPreparedModelError("prepared model compiler version is stale") + model_source = _MODEL_SOURCE_BY_ID[identifier] + if compiled_source.get("kind") != model_source: + raise PackagedPreparedModelError(f"prepared model source is not {model_source}") + if pack.producer.get("version") != packaged_package_version: + raise PackagedPreparedModelError( + "prepared kernel-pack package version disagrees with metadata" + ) + dependency_expected = { + "symbolica_version": fingerprint["symbolica"], + "ufo_model_loader_version": fingerprint["ufo_model_loader"], + "symbolica_serialization_abi": SYMBOLICA_SERIALIZATION_ABI, + "symjit_application_abi": SYMJIT_APPLICATION_ABI, + "symjit_plane_application_abi": SYMJIT_PLANE_APPLICATION_ABI, + } + for key, value in dependency_expected.items(): + if dependencies.get(key) != value: + raise PackagedPreparedModelError( + f"packaged prepared-model dependency {key} is stale" + ) + pack_dependencies = { + "symbolica_version": dependencies["symbolica_version"], + "symbolica_serialization": dependencies["symbolica_serialization_abi"], + "symjit_application": dependencies["symjit_application_abi"], + "symjit_plane_application": dependencies["symjit_plane_application_abi"], + } + for key, value in pack_dependencies.items(): + if pack.dependency_abis.get(key) != value: + raise PackagedPreparedModelError( + f"prepared kernel-pack dependency {key} is stale" + ) + if metadata.get("eager_kernel_abi") != EAGER_KERNEL_ABI: + raise PackagedPreparedModelError("packaged eager kernel ABI is stale") + if ( + metadata.get("prepared_model_bundle_schema") + != PREPARED_MODEL_BUNDLE_SCHEMA_VERSION + ): + raise PackagedPreparedModelError("packaged model bundle schema is stale") + if metadata.get("backend") != "jit" or bundle.backend != "jit": + raise PackagedPreparedModelError("packaged built-in model is not JIT-backed") + if metadata.get("jit_optimization_level") != 2: + raise PackagedPreparedModelError( + "packaged built-in model is not portable JIT O2" + ) + if pack.optimization_settings.get("jit_optimization_level") != 2: + raise PackagedPreparedModelError("prepared kernel pack is not portable JIT O2") + if metadata.get("kernel_count") != len(pack.kernels) or not pack.kernels: + raise PackagedPreparedModelError( + "packaged prepared-model kernel count is invalid" + ) + target = _plain_json(pack.target) + expected_target = symjit_storage_v3_target(machine=architecture) + if target != metadata.get("target") or target != expected_target: + raise PackagedPreparedModelError( + "packaged prepared model target does not match its architecture asset" + ) + + +def _metadata( + value: object, + *, + identifier: str, + bundle_name: str, +) -> Mapping[str, object]: + metadata = _mapping(value, "prepared-model metadata") + missing = _METADATA_KEYS - metadata.keys() + unknown = metadata.keys() - _METADATA_KEYS + if missing or unknown: + raise PackagedPreparedModelError( + "packaged prepared-model metadata fields are invalid" + ) + if metadata.get("schema_version") != 1: + raise PackagedPreparedModelError( + "unsupported packaged prepared-model metadata schema" + ) + if metadata.get("id") != identifier: + raise PackagedPreparedModelError("packaged prepared-model identity is invalid") + if ( + metadata.get("model") != _MODEL_SOURCE_BY_ID[identifier] + or metadata.get("bundle") != bundle_name + ): + raise PackagedPreparedModelError("packaged prepared-model resource is invalid") + return metadata + + +def _asset_names(identifier: str, architecture: str) -> tuple[str, str]: + stem = f"{identifier}-{architecture}" + return f"{stem}.metadata.json", f"{stem}.pyamplicol-model" + + +def _mapping(value: object, context: str) -> Mapping[str, object]: + if not isinstance(value, Mapping) or not all(isinstance(key, str) for key in value): + raise PackagedPreparedModelError(f"{context} must be an object") + return cast(Mapping[str, object], value) + + +def _plain_json(value: object) -> object: + if isinstance(value, Mapping): + return {str(key): _plain_json(item) for key, item in value.items()} + if isinstance(value, Sequence) and not isinstance(value, (str, bytes, bytearray)): + return [_plain_json(item) for item in value] + return value + + +__all__ = [ + "BUILTIN_SM_HEFT_JIT_O2", + "BUILTIN_SM_JIT_O2", + "PackagedPreparedModelError", + "available_prepared_models", + "materialize_packaged_prepared_model", + "open_packaged_prepared_model", + "packaged_prepared_model_path", +] diff --git a/pyamplicol/src/pyamplicol/assets/selftest/portable-64le/artifact/API/c/Makefile b/pyamplicol/src/pyamplicol/assets/selftest/portable-64le/artifact/API/c/Makefile new file mode 100644 index 0000000000..4c0d18953f --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/selftest/portable-64le/artifact/API/c/Makefile @@ -0,0 +1,42 @@ +# SPDX-License-Identifier: 0BSD +RUSTICOL_CONFIG ?= rusticol-config +RUSTICOL_CONFIG_PATH := $(shell \ + if command -v "$(RUSTICOL_CONFIG)" >/dev/null 2>&1; then \ + command -v "$(RUSTICOL_CONFIG)"; \ + elif [ "$(RUSTICOL_CONFIG)" = rusticol-config ]; then \ + directory="$(CURDIR)"; \ + while [ "$$directory" != / ]; do \ + candidate="$$directory/.venv/bin/rusticol-config"; \ + if [ -x "$$candidate" ]; then printf '%s\n' "$$candidate"; break; fi; \ + directory=$$(dirname "$$directory"); \ + done; \ + fi) +ifeq ($(strip $(RUSTICOL_CONFIG_PATH)),) +$(error rusticol-config is unavailable; activate the pyAmpliCol environment or set RUSTICOL_CONFIG=/absolute/path) +endif +CC ?= cc +ARTIFACT_DIR := $(abspath ../..) +BUILD_DIR ?= $(abspath ../../..)/.pyamplicol-api-build/$(notdir $(ARTIFACT_DIR))/c +TARGET := $(BUILD_DIR)/check_standalone +CPPFLAGS += $(shell "$(RUSTICOL_CONFIG_PATH)" --cflags) +CFLAGS ?= -O2 +CFLAGS += -std=c11 -Wall -Wextra -pedantic +LDLIBS += $(shell "$(RUSTICOL_CONFIG_PATH)" --libs) + +.PHONY: all check_standalone clean run + +all: $(TARGET) + +check_standalone: $(TARGET) + +$(BUILD_DIR): + mkdir -p "$@" + +$(TARGET): check_standalone.c | $(BUILD_DIR) + $(CC) $(CPPFLAGS) $(CFLAGS) "$<" -o "$@" $(LDLIBS) + +run: $(TARGET) + cd "$(ARTIFACT_DIR)" && "$(TARGET)" $(ARGS) + +clean: + $(RM) "$(TARGET)" diff --git a/pyamplicol/src/pyamplicol/assets/selftest/portable-64le/artifact/API/c/check_standalone.c b/pyamplicol/src/pyamplicol/assets/selftest/portable-64le/artifact/API/c/check_standalone.c new file mode 100644 index 0000000000..defccd3210 --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/selftest/portable-64le/artifact/API/c/check_standalone.c @@ -0,0 +1,885 @@ +/* SPDX-License-Identifier: 0BSD */ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define MAX_OVERRIDES 128 + +typedef struct { + const char *process; + const char *model_parameters; + const char *kinematics; + const char *parameter_names[MAX_OVERRIDES]; + double parameter_real[MAX_OVERRIDES]; + double parameter_imaginary[MAX_OVERRIDES]; + size_t parameter_count; + int precision; + bool json; +} Options; + +typedef struct { + char *id; + int32_t *values; + size_t value_count; +} HelicityMetadata; + +typedef struct { + char *id; + char *kind; + size_t *word; + size_t word_count; +} ColorMetadata; + +typedef int (*RuntimeStringGetter)( + const RusticolRuntimeHandle *, char *, size_t, size_t * +); +typedef int (*IndexedStringGetter)( + const RusticolRuntimeHandle *, size_t, char *, size_t, size_t * +); + +static void fail(const char *message) { + fprintf(stderr, "check_standalone: %s\n", message); + exit(EXIT_FAILURE); +} + +static void fail_errno(const char *operation) { + fprintf(stderr, "check_standalone: %s: %s\n", operation, strerror(errno)); + exit(EXIT_FAILURE); +} + +static void *allocate(size_t count, size_t size) { + void *output; + if (count != 0 && size > SIZE_MAX / count) { + fail("allocation size overflow"); + } + output = calloc(count == 0 ? 1 : count, size == 0 ? 1 : size); + if (output == NULL) { + fail("out of memory"); + } + return output; +} + +static void *resize(void *pointer, size_t size) { + void *output = realloc(pointer, size == 0 ? 1 : size); + if (output == NULL) { + free(pointer); + fail("out of memory"); + } + return output; +} + +static void check_rusticol(int status, const char *operation) { + size_t required = 0; + char *message = NULL; + if (status == RUSTICOL_STATUS_OK) { + return; + } + if (rusticol_last_error_message(NULL, 0, &required) == RUSTICOL_STATUS_OK && + required > 0) { + message = allocate(required, sizeof(*message)); + if (rusticol_last_error_message(message, required, &required) != + RUSTICOL_STATUS_OK) { + free(message); + message = NULL; + } + } + fprintf( + stderr, + "check_standalone: %s failed (status %d)%s%s\n", + operation, + status, + message == NULL ? "" : ": ", + message == NULL ? "" : message + ); + free(message); + exit(EXIT_FAILURE); +} + +static double parse_double(const char *text, const char *description) { + char *end = NULL; + double value; + errno = 0; + value = strtod(text, &end); + if (text == end || end == NULL || *end != '\0' || errno == ERANGE || + !isfinite(value)) { + fprintf(stderr, "check_standalone: invalid %s: %s\n", description, text); + exit(EXIT_FAILURE); + } + return value; +} + +static size_t parse_size(const char *text, const char *description) { + char *end = NULL; + unsigned long long value; + errno = 0; + value = strtoull(text, &end, 10); + if (text == end || end == NULL || *end != '\0' || errno == ERANGE || + value > SIZE_MAX) { + fprintf(stderr, "check_standalone: invalid %s: %s\n", description, text); + exit(EXIT_FAILURE); + } + return (size_t)value; +} + +static Options parse_options(int argc, char **argv) { + Options options = {0}; + int index; + options.precision = 16; + for (index = 1; index < argc; ++index) { + const char *argument = argv[index]; + if (strcmp(argument, "--") == 0) { + continue; + } + if (strcmp(argument, "--process") == 0) { + if (++index >= argc) { + fail("missing value after --process"); + } + options.process = argv[index]; + } else if (strcmp(argument, "--model-parameters") == 0) { + if (++index >= argc) { + fail("missing value after --model-parameters"); + } + options.model_parameters = argv[index]; + } else if (strcmp(argument, "--kinematics") == 0) { + if (++index >= argc) { + fail("missing value after --kinematics"); + } + options.kinematics = argv[index]; + } else if (strcmp(argument, "--set-parameter") == 0) { + size_t slot = options.parameter_count; + if (index + 3 >= argc) { + fail("--set-parameter requires NAME REAL IMAG"); + } + if (slot >= MAX_OVERRIDES) { + fail("too many --set-parameter options"); + } + options.parameter_names[slot] = argv[++index]; + options.parameter_real[slot] = + parse_double(argv[++index], "real model-parameter component"); + options.parameter_imaginary[slot] = + parse_double(argv[++index], "imaginary model-parameter component"); + options.parameter_count += 1; + } else if (strcmp(argument, "--precision") == 0) { + size_t precision; + if (++index >= argc) { + fail("missing value after --precision"); + } + precision = parse_size(argv[index], "--precision value"); + if (precision > INT32_MAX) { + fail("invalid --precision value"); + } + options.precision = (int)precision; + } else if (strcmp(argument, "--json") == 0) { + options.json = true; + } else if (strcmp(argument, "--help") == 0 || strcmp(argument, "-h") == 0) { + puts( + "usage: check_standalone [--process ID|EXPRESSION] " + "[--model-parameters PATH] " + "[--kinematics PATH] " + "[--set-parameter NAME REAL IMAG] " + "[--precision 16] [--json]" + ); + exit(EXIT_SUCCESS); + } else { + fprintf(stderr, "check_standalone: unknown option: %s\n", argument); + exit(EXIT_FAILURE); + } + } + if (options.precision != 16) { + fail("the C Rusticol API supports only double precision (--precision 16)"); + } + return options; +} + +static char *runtime_string( + const RusticolRuntimeHandle *runtime, + RuntimeStringGetter getter, + const char *operation +) { + size_t required = 0; + char *output; + check_rusticol(getter(runtime, NULL, 0, &required), operation); + if (required == 0) { + fail("Rusticol returned an invalid string size"); + } + output = allocate(required, sizeof(*output)); + check_rusticol(getter(runtime, output, required, &required), operation); + return output; +} + +static char *indexed_string( + const RusticolRuntimeHandle *runtime, + size_t index, + IndexedStringGetter getter, + const char *operation +) { + size_t required = 0; + char *output; + check_rusticol(getter(runtime, index, NULL, 0, &required), operation); + if (required == 0) { + fail("Rusticol returned an invalid indexed string size"); + } + output = allocate(required, sizeof(*output)); + check_rusticol(getter(runtime, index, output, required, &required), operation); + return output; +} + +static char *read_line(FILE *stream) { + size_t capacity = 256; + size_t length = 0; + char *line = allocate(capacity, sizeof(*line)); + int character; + while ((character = fgetc(stream)) != EOF && character != '\n') { + if (length + 1 >= capacity) { + if (capacity > SIZE_MAX / 2) { + free(line); + fail("validation-point row is too large"); + } + capacity *= 2; + line = resize(line, capacity); + } + line[length++] = (char)character; + } + if (ferror(stream)) { + free(line); + fail_errno("read API/validation_points.dat"); + } + if (character == EOF && length == 0) { + free(line); + return NULL; + } + if (length > 0 && line[length - 1] == '\r') { + length -= 1; + } + line[length] = '\0'; + return line; +} + +static char *take_field(char **cursor) { + char *field; + char *separator; + if (*cursor == NULL) { + return NULL; + } + field = *cursor; + separator = strchr(field, '\t'); + if (separator == NULL) { + *cursor = NULL; + } else { + *separator = '\0'; + *cursor = separator + 1; + } + return field; +} + +static double *load_validation_point( + const char *path, + const char *process_key, + size_t external_count +) { + FILE *input = fopen(path, "r"); + char *line; + if (input == NULL) { + if (errno == ENOENT) { + return NULL; + } + fail_errno("open API/validation_points.dat"); + } + line = read_line(input); + if (line == NULL || strcmp(line, "RUSTICOL_VALIDATION_POINTS_V1") != 0) { + free(line); + fclose(input); + fail("unsupported validation_points.dat format"); + } + free(line); + + while ((line = read_line(input)) != NULL) { + char *cursor = line; + char *row_process; + char *row_count_text; + size_t row_count; + size_t momentum_count; + size_t index; + double *momenta; + if (line[0] == '\0' || line[0] == '#') { + free(line); + continue; + } + row_process = take_field(&cursor); + row_count_text = take_field(&cursor); + if (row_process == NULL || row_count_text == NULL) { + free(line); + fclose(input); + fail("invalid validation point row"); + } + if (strcmp(row_process, process_key) != 0) { + free(line); + continue; + } + row_count = parse_size(row_count_text, "validation external-particle count"); + if (row_count != external_count || external_count > SIZE_MAX / 4) { + free(line); + fclose(input); + fail("validation point has an incompatible external-particle count"); + } + momentum_count = 4 * external_count; + momenta = allocate(momentum_count, sizeof(*momenta)); + for (index = 0; index < momentum_count; ++index) { + char *component = take_field(&cursor); + if (component == NULL) { + free(momenta); + free(line); + fclose(input); + fail("validation point has too few momentum components"); + } + momenta[index] = + parse_double(component, "validation-point momentum component"); + } + if (cursor != NULL) { + free(momenta); + free(line); + fclose(input); + fail("validation point has too many momentum components"); + } + free(line); + if (fclose(input) != 0) { + free(momenta); + fail_errno("close API/validation_points.dat"); + } + return momenta; + } + if (fclose(input) != 0) { + fail_errno("close API/validation_points.dat"); + } + return NULL; +} + +static size_t *load_external_permutation( + const RusticolRuntimeHandle *runtime, + size_t expected_count +) { + size_t required = 0; + size_t *permutation; + check_rusticol( + rusticol_runtime_external_permutation(runtime, NULL, 0, &required), + "query external permutation size" + ); + if (required != expected_count) { + fail("external permutation has an incompatible size"); + } + permutation = allocate(required, sizeof(*permutation)); + check_rusticol( + rusticol_runtime_external_permutation( + runtime, permutation, required, &required + ), + "query external permutation" + ); + return permutation; +} + +static double *reorder_validation_point( + double *representative, + const size_t *permutation, + size_t external_count +) { + double *public_point = allocate(4 * external_count, sizeof(*public_point)); + bool *seen = allocate(external_count, sizeof(*seen)); + size_t representative_index; + for (representative_index = 0; representative_index < external_count; + ++representative_index) { + size_t public_index = permutation[representative_index]; + if (public_index >= external_count || seen[public_index]) { + free(seen); + free(public_point); + free(representative); + fail("runtime returned an invalid external permutation"); + } + seen[public_index] = true; + memcpy( + &public_point[4 * public_index], + &representative[4 * representative_index], + 4 * sizeof(*public_point) + ); + } + free(seen); + free(representative); + return public_point; +} + +static double *load_kinematics_json( + const RusticolRuntimeHandle *runtime, + const char *path, + size_t expected_count +) { + size_t required = 0; + double *momenta; + check_rusticol( + rusticol_runtime_load_kinematics_json( + runtime, path, NULL, 0, &required + ), + "query kinematics JSON size" + ); + if (required != 4 * expected_count) { + fail("kinematics JSON has an incompatible external-particle count"); + } + momenta = allocate(required, sizeof(*momenta)); + check_rusticol( + rusticol_runtime_load_kinematics_json( + runtime, path, momenta, required, &required + ), + "load kinematics JSON" + ); + return momenta; +} + +static int32_t *load_external_particles( + const RusticolRuntimeHandle *runtime, + size_t *count +) { + int32_t *particles; + size_t index; + check_rusticol( + rusticol_runtime_external_count(runtime, count), + "query external-particle count" + ); + particles = allocate(*count, sizeof(*particles)); + for (index = 0; index < *count; ++index) { + check_rusticol( + rusticol_runtime_external_pdg(runtime, index, &particles[index]), + "query external-particle PDG" + ); + } + return particles; +} + +static HelicityMetadata *load_helicities( + const RusticolRuntimeHandle *runtime, + size_t *count +) { + HelicityMetadata *items; + size_t index; + check_rusticol( + rusticol_runtime_helicity_count(runtime, count), + "query helicity count" + ); + items = allocate(*count, sizeof(*items)); + for (index = 0; index < *count; ++index) { + items[index].id = indexed_string( + runtime, + index, + rusticol_runtime_helicity_id, + "query helicity ID" + ); + check_rusticol( + rusticol_runtime_helicity_vector( + runtime, index, NULL, 0, &items[index].value_count + ), + "query helicity-vector size" + ); + items[index].values = + allocate(items[index].value_count, sizeof(*items[index].values)); + if (items[index].value_count > 0) { + check_rusticol( + rusticol_runtime_helicity_vector( + runtime, + index, + items[index].values, + items[index].value_count, + &items[index].value_count + ), + "query helicity vector" + ); + } + } + return items; +} + +static ColorMetadata *load_colors( + const RusticolRuntimeHandle *runtime, + size_t *count +) { + ColorMetadata *items; + size_t index; + check_rusticol(rusticol_runtime_color_count(runtime, count), "query color count"); + items = allocate(*count, sizeof(*items)); + for (index = 0; index < *count; ++index) { + items[index].id = indexed_string( + runtime, index, rusticol_runtime_color_id, "query color ID" + ); + items[index].kind = indexed_string( + runtime, index, rusticol_runtime_color_kind, "query color kind" + ); + check_rusticol( + rusticol_runtime_color_word( + runtime, index, NULL, 0, &items[index].word_count + ), + "query color-word size" + ); + items[index].word = + allocate(items[index].word_count, sizeof(*items[index].word)); + if (items[index].word_count > 0) { + check_rusticol( + rusticol_runtime_color_word( + runtime, + index, + items[index].word, + items[index].word_count, + &items[index].word_count + ), + "query color word" + ); + } + } + return items; +} + +static void write_json_string(const char *value) { + const unsigned char *cursor = (const unsigned char *)value; + putchar('"'); + while (*cursor != '\0') { + unsigned char character = *cursor++; + switch (character) { + case '"': fputs("\\\"", stdout); break; + case '\\': fputs("\\\\", stdout); break; + case '\b': fputs("\\b", stdout); break; + case '\f': fputs("\\f", stdout); break; + case '\n': fputs("\\n", stdout); break; + case '\r': fputs("\\r", stdout); break; + case '\t': fputs("\\t", stdout); break; + default: + if (character < 0x20) { + printf("\\u%04x", (unsigned int)character); + } else { + putchar((int)character); + } + } + } + putchar('"'); +} + +static void write_common_json( + const char *process, + const char *process_key, + const char *color_accuracy, + const int32_t *particles, + size_t particle_count, + const HelicityMetadata *helicities, + size_t helicity_count, + const ColorMetadata *colors, + size_t color_count +) { + size_t index; + fputs("\"process\":", stdout); + write_json_string(process); + fputs(",\"process_key\":", stdout); + write_json_string(process_key); + fputs(",\"color_accuracy\":", stdout); + write_json_string(color_accuracy); + fputs(",\"external_particles\":[", stdout); + for (index = 0; index < particle_count; ++index) { + if (index != 0) { + putchar(','); + } + printf("{\"index\":%zu,\"pdg\":%" PRId32 "}", index, particles[index]); + } + fputs("],\"helicities\":[", stdout); + for (index = 0; index < helicity_count; ++index) { + size_t component; + if (index != 0) { + putchar(','); + } + fputs("{\"id\":", stdout); + write_json_string(helicities[index].id); + fputs(",\"helicities\":[", stdout); + for (component = 0; component < helicities[index].value_count; ++component) { + if (component != 0) { + putchar(','); + } + printf("%" PRId32, helicities[index].values[component]); + } + fputs("]}", stdout); + } + fputs("],\"colors\":[", stdout); + for (index = 0; index < color_count; ++index) { + size_t component; + if (index != 0) { + putchar(','); + } + fputs("{\"id\":", stdout); + write_json_string(colors[index].id); + fputs(",\"kind\":", stdout); + write_json_string(colors[index].kind); + fputs(",\"word\":[", stdout); + for (component = 0; component < colors[index].word_count; ++component) { + if (component != 0) { + putchar(','); + } + printf("%zu", colors[index].word[component]); + } + fputs("]}", stdout); + } + putchar(']'); +} + +static void free_helicities(HelicityMetadata *items, size_t count) { + size_t index; + for (index = 0; index < count; ++index) { + free(items[index].id); + free(items[index].values); + } + free(items); +} + +static void free_colors(ColorMetadata *items, size_t count) { + size_t index; + for (index = 0; index < count; ++index) { + free(items[index].id); + free(items[index].kind); + free(items[index].word); + } + free(items); +} + +int main(int argc, char **argv) { + Options options = parse_options(argc, argv); + RusticolRuntimeHandle *runtime = NULL; + FILE *manifest; + char *process; + char *process_key; + char *representative_process_key; + char *color_accuracy; + int32_t *particles; + HelicityMetadata *helicities; + ColorMetadata *colors; + size_t particle_count = 0; + size_t helicity_count = 0; + size_t color_count = 0; + double *momenta; + size_t *external_permutation = NULL; + double total = NAN; + double *resolved = NULL; + size_t resolved_helicity_count = 0; + size_t resolved_color_count = 0; + size_t resolved_count; + double explicit_total = 0.0; + size_t index; + + manifest = fopen("artifact.json", "r"); + if (manifest == NULL) { + fail("run check_standalone from a generated artifact directory"); + } + if (fclose(manifest) != 0) { + fail_errno("close artifact.json"); + } + + check_rusticol( + rusticol_runtime_load( + ".", options.process, options.model_parameters, &runtime + ), + "load artifact" + ); + if (options.parameter_count > 0) { + check_rusticol( + rusticol_runtime_set_model_parameters( + runtime, + options.parameter_names, + options.parameter_real, + options.parameter_imaginary, + options.parameter_count + ), + "set model parameters" + ); + } + + process = runtime_string(runtime, rusticol_runtime_process, "query process"); + process_key = + runtime_string(runtime, rusticol_runtime_process_key, "query process key"); + representative_process_key = runtime_string( + runtime, + rusticol_runtime_representative_process_key, + "query representative process key" + ); + color_accuracy = runtime_string( + runtime, rusticol_runtime_color_accuracy, "query color accuracy" + ); + particles = load_external_particles(runtime, &particle_count); + helicities = load_helicities(runtime, &helicity_count); + colors = load_colors(runtime, &color_count); + if (options.kinematics != NULL) { + momenta = load_kinematics_json(runtime, options.kinematics, particle_count); + } else { + external_permutation = load_external_permutation(runtime, particle_count); + momenta = load_validation_point( + "API/validation_points.dat", representative_process_key, particle_count + ); + if (momenta != NULL) { + momenta = reorder_validation_point( + momenta, external_permutation, particle_count + ); + } + } + + if (momenta == NULL) { + if (options.json) { + fputs("{\"language\":\"c\",\"available\":false,", stdout); + write_common_json( + process, + process_key, + color_accuracy, + particles, + particle_count, + helicities, + helicity_count, + colors, + color_count + ); + fputs( + ",\"diagnostic\":\"no bundled validation point is available\"}\n", + stdout + ); + } else { + printf("process: %s\n", process); + puts("no bundled validation point is available; metadata load succeeded"); + } + } else { + check_rusticol( + rusticol_runtime_evaluate_f64( + runtime, momenta, 4 * particle_count, 1, &total, 1 + ), + "evaluate compatibility total" + ); + check_rusticol( + rusticol_runtime_resolved_shape( + runtime, + NULL, + 0, + NULL, + 0, + &resolved_helicity_count, + &resolved_color_count + ), + "query resolved shape" + ); + if (resolved_helicity_count != helicity_count || + resolved_color_count != color_count) { + fail("resolved shape does not match runtime metadata"); + } + if (resolved_helicity_count != 0 && + resolved_color_count > SIZE_MAX / resolved_helicity_count) { + fail("resolved output size overflow"); + } + resolved_count = resolved_helicity_count * resolved_color_count; + resolved = allocate(resolved_count, sizeof(*resolved)); + check_rusticol( + rusticol_runtime_evaluate_resolved_f64( + runtime, + momenta, + 4 * particle_count, + 1, + NULL, + 0, + NULL, + 0, + resolved, + resolved_count, + &resolved_helicity_count, + &resolved_color_count + ), + "evaluate resolved components" + ); + if (!isfinite(total)) { + fail("matrix-element total is not finite"); + } + for (index = 0; index < resolved_count; ++index) { + if (!isfinite(resolved[index])) { + fail("resolved matrix-element output is not finite"); + } + explicit_total += resolved[index]; + } + + if (options.json) { + fputs( + "{\"language\":\"c\",\"available\":true,\"precision\":16,", + stdout + ); + write_common_json( + process, + process_key, + color_accuracy, + particles, + particle_count, + helicities, + helicity_count, + colors, + color_count + ); + printf( + ",\"shape\":[1,%zu,%zu],\"values\":[", + resolved_helicity_count, + resolved_color_count + ); + for (index = 0; index < resolved_count; ++index) { + if (index != 0) { + putchar(','); + } + printf("%.17g", resolved[index]); + } + printf( + "],\"resolved_sum\":[%.17g],\"compatibility_total\":[%.17g]}\n", + explicit_total, + total + ); + } else { + size_t helicity_index; + size_t color_index; + printf("process: %s [%s]\n", process, process_key); + printf( + "resolved shape: (1, %zu, %zu)\n", + resolved_helicity_count, + resolved_color_count + ); + for (helicity_index = 0; + helicity_index < resolved_helicity_count; + ++helicity_index) { + for (color_index = 0; color_index < resolved_color_count; ++color_index) { + size_t offset = + helicity_index * resolved_color_count + color_index; + printf( + " %s %s %.17g\n", + helicities[helicity_index].id, + colors[color_index].id, + resolved[offset] + ); + } + } + printf("explicit resolved sum: %.17g\n", explicit_total); + printf("compatibility total: %.17g\n", total); + } + if (!isfinite(explicit_total) || + fabs(explicit_total - total) > 1.0e-12 * fmax(fabs(total), 1.0)) { + fail("resolved components do not reproduce the compatibility total"); + } + } + + check_rusticol(rusticol_runtime_free(runtime), "free runtime"); + free(momenta); + free(resolved); + free(process); + free(process_key); + free(representative_process_key); + free(external_permutation); + free(color_accuracy); + free(particles); + free_helicities(helicities, helicity_count); + free_colors(colors, color_count); + return EXIT_SUCCESS; +} diff --git a/pyamplicol/src/pyamplicol/assets/selftest/portable-64le/artifact/API/cpp/Makefile b/pyamplicol/src/pyamplicol/assets/selftest/portable-64le/artifact/API/cpp/Makefile new file mode 100644 index 0000000000..3edf40f252 --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/selftest/portable-64le/artifact/API/cpp/Makefile @@ -0,0 +1,42 @@ +# SPDX-License-Identifier: 0BSD +RUSTICOL_CONFIG ?= rusticol-config +RUSTICOL_CONFIG_PATH := $(shell \ + if command -v "$(RUSTICOL_CONFIG)" >/dev/null 2>&1; then \ + command -v "$(RUSTICOL_CONFIG)"; \ + elif [ "$(RUSTICOL_CONFIG)" = rusticol-config ]; then \ + directory="$(CURDIR)"; \ + while [ "$$directory" != / ]; do \ + candidate="$$directory/.venv/bin/rusticol-config"; \ + if [ -x "$$candidate" ]; then printf '%s\n' "$$candidate"; break; fi; \ + directory=$$(dirname "$$directory"); \ + done; \ + fi) +ifeq ($(strip $(RUSTICOL_CONFIG_PATH)),) +$(error rusticol-config is unavailable; activate the pyAmpliCol environment or set RUSTICOL_CONFIG=/absolute/path) +endif +CXX ?= c++ +ARTIFACT_DIR := $(abspath ../..) +BUILD_DIR ?= $(abspath ../../..)/.pyamplicol-api-build/$(notdir $(ARTIFACT_DIR))/cpp +TARGET := $(BUILD_DIR)/check_standalone +CPPFLAGS += $(shell "$(RUSTICOL_CONFIG_PATH)" --cflags) +CXXFLAGS ?= -O2 +CXXFLAGS += -std=c++17 -Wall -Wextra -pedantic +LDLIBS += $(shell "$(RUSTICOL_CONFIG_PATH)" --libs) + +.PHONY: all check_standalone clean run + +all: $(TARGET) + +check_standalone: $(TARGET) + +$(BUILD_DIR): + mkdir -p "$@" + +$(TARGET): check_standalone.cpp | $(BUILD_DIR) + $(CXX) $(CPPFLAGS) $(CXXFLAGS) "$<" -o "$@" $(LDLIBS) + +run: $(TARGET) + cd "$(ARTIFACT_DIR)" && "$(TARGET)" $(ARGS) + +clean: + $(RM) "$(TARGET)" diff --git a/pyamplicol/src/pyamplicol/assets/selftest/portable-64le/artifact/API/cpp/check_standalone.cpp b/pyamplicol/src/pyamplicol/assets/selftest/portable-64le/artifact/API/cpp/check_standalone.cpp new file mode 100644 index 0000000000..8938ae2a3b --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/selftest/portable-64le/artifact/API/cpp/check_standalone.cpp @@ -0,0 +1,293 @@ +// SPDX-License-Identifier: 0BSD + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace fs = std::filesystem; + +struct Options { + std::string process; + std::string model_parameters; + std::string kinematics; + std::vector parameter_names; + std::vector parameter_real; + std::vector parameter_imaginary; + int precision = 16; + bool json = false; +}; + +std::string json_string(const std::string &value) { + std::ostringstream output; + output << '"'; + for (const unsigned char character : value) { + switch (character) { + case '"': output << "\\\""; break; + case '\\': output << "\\\\"; break; + case '\n': output << "\\n"; break; + case '\r': output << "\\r"; break; + case '\t': output << "\\t"; break; + default: + if (character < 0x20) { + output << "\\u" << std::hex << std::setw(4) << std::setfill('0') + << static_cast(character) << std::dec << std::setfill(' '); + } else { + output << character; + } + } + } + output << '"'; + return output.str(); +} + +Options parse_options(int argc, char **argv) { + Options options; + for (int index = 1; index < argc; ++index) { + const std::string argument = argv[index]; + auto require = [&](int count) { + if (index + count >= argc) { + throw std::invalid_argument("missing value after " + argument); + } + }; + if (argument == "--process") { + require(1); + options.process = argv[++index]; + } else if (argument == "--model-parameters") { + require(1); + options.model_parameters = argv[++index]; + } else if (argument == "--kinematics") { + require(1); + options.kinematics = argv[++index]; + } else if (argument == "--set-parameter") { + require(3); + options.parameter_names.emplace_back(argv[++index]); + options.parameter_real.push_back(std::stod(argv[++index])); + options.parameter_imaginary.push_back(std::stod(argv[++index])); + } else if (argument == "--precision") { + require(1); + options.precision = std::stoi(argv[++index]); + } else if (argument == "--json") { + options.json = true; + } else if (argument == "--help" || argument == "-h") { + std::cout << "usage: check_standalone [--process ID|EXPRESSION] " + "[--model-parameters PATH] " + "[--kinematics PATH] " + "[--set-parameter NAME REAL IMAG] " + "[--precision 16] [--json]\n"; + std::exit(0); + } else { + throw std::invalid_argument("unknown option: " + argument); + } + } + if (options.precision != 16) { + throw std::invalid_argument( + "the C++ Rusticol API supports only double precision (--precision 16)"); + } + return options; +} + +std::vector split_tabs(const std::string &line) { + std::vector fields; + std::size_t start = 0; + while (true) { + const auto separator = line.find('\t', start); + fields.push_back(line.substr(start, separator - start)); + if (separator == std::string::npos) break; + start = separator + 1; + } + return fields; +} + +std::vector load_validation_point( + const fs::path &path, const std::string &process_id, std::size_t external_count) { + std::ifstream input(path); + if (!input) return {}; + std::string line; + std::getline(input, line); + if (line != "RUSTICOL_VALIDATION_POINTS_V1") { + throw std::runtime_error("unsupported validation_points.dat format"); + } + while (std::getline(input, line)) { + if (line.empty() || line[0] == '#') continue; + const auto fields = split_tabs(line); + if (fields.size() < 2 || fields[0] != process_id) continue; + const auto row_count = static_cast(std::stoull(fields[1])); + if (row_count != external_count || fields.size() != 2 + 4 * external_count) { + throw std::runtime_error( + "validation point has an incompatible external-particle count"); + } + std::vector values; + values.reserve(4 * external_count); + for (std::size_t index = 2; index < fields.size(); ++index) { + values.push_back(std::stod(fields[index])); + } + return values; + } + return {}; +} + +std::vector reorder_validation_point( + const std::vector &representative, + const std::vector &permutation) { + if (representative.empty()) return {}; + if (representative.size() != 4 * permutation.size()) { + throw std::runtime_error("validation point does not match external permutation"); + } + std::vector public_point(representative.size()); + std::vector seen(permutation.size(), false); + for (std::size_t representative_index = 0; + representative_index < permutation.size(); ++representative_index) { + const auto public_index = permutation[representative_index]; + if (public_index >= permutation.size() || seen[public_index]) { + throw std::runtime_error("runtime returned an invalid external permutation"); + } + seen[public_index] = true; + std::copy_n( + representative.begin() + 4 * representative_index, + 4, + public_point.begin() + 4 * public_index); + } + return public_point; +} + +template +void write_integer_array(std::ostream &output, const std::vector &values) { + output << '['; + for (std::size_t index = 0; index < values.size(); ++index) { + if (index) output << ','; + output << values[index]; + } + output << ']'; +} + +void write_common_json( + std::ostream &output, + const rusticol::Runtime &runtime, + const std::vector &particles, + const std::vector &helicities, + const std::vector &colors) { + output << "\"process\":" << json_string(runtime.process()) + << ",\"process_key\":" << json_string(runtime.process_key()) + << ",\"color_accuracy\":" << json_string(runtime.color_accuracy()); + output << ",\"external_particles\":["; + for (std::size_t index = 0; index < particles.size(); ++index) { + if (index) output << ','; + output << "{\"index\":" << particles[index].index + << ",\"pdg\":" << particles[index].pdg << '}'; + } + output << "],\"helicities\":["; + for (std::size_t index = 0; index < helicities.size(); ++index) { + if (index) output << ','; + output << "{\"id\":" << json_string(helicities[index].id) + << ",\"helicities\":"; + write_integer_array(output, helicities[index].helicities); + output << '}'; + } + output << "],\"colors\":["; + for (std::size_t index = 0; index < colors.size(); ++index) { + if (index) output << ','; + output << "{\"id\":" << json_string(colors[index].id) + << ",\"kind\":" << json_string(colors[index].kind) + << ",\"word\":"; + write_integer_array(output, colors[index].word); + output << '}'; + } + output << ']'; +} + +int main(int argc, char **argv) { + try { + const auto options = parse_options(argc, argv); + const fs::path executable = fs::weakly_canonical(fs::absolute(argv[0])); + fs::path root = fs::current_path(); + if (!fs::is_regular_file(root / "artifact.json")) { + root = executable.parent_path().parent_path().parent_path(); + } + rusticol::Runtime runtime(root.string(), options.process, options.model_parameters); + if (!options.parameter_names.empty()) { + runtime.set_model_parameters( + options.parameter_names, + options.parameter_real, + options.parameter_imaginary); + } + const auto particles = runtime.external_particles(); + const auto helicities = runtime.helicities(); + const auto colors = runtime.colors(); + const auto momenta = options.kinematics.empty() + ? reorder_validation_point( + load_validation_point( + root / "API" / "validation_points.dat", + runtime.representative_process_key(), + particles.size()), + runtime.external_permutation()) + : runtime.load_kinematics_json(options.kinematics); + if (momenta.empty()) { + if (options.json) { + std::cout << "{\"language\":\"cpp\",\"available\":false,"; + write_common_json(std::cout, runtime, particles, helicities, colors); + std::cout << ",\"diagnostic\":" + "\"no bundled validation point is available\"}\n"; + } else { + std::cout << "process: " << runtime.process() << "\n" + << "no bundled validation point is available; " + "metadata load succeeded\n"; + } + return 0; + } + const auto total = runtime.evaluate(momenta, 1); + const auto resolved = runtime.evaluate_resolved(momenta, 1); + const auto explicit_total = resolved.total(); + + if (options.json) { + std::cout << std::setprecision(17) + << "{\"language\":\"cpp\",\"available\":true," + "\"precision\":16,"; + write_common_json(std::cout, runtime, particles, helicities, colors); + std::cout << ",\"shape\":[1," << resolved.helicities.size() << ',' + << resolved.colors.size() << "],\"values\":["; + for (std::size_t index = 0; index < resolved.values.size(); ++index) { + if (index) std::cout << ','; + std::cout << resolved.values[index]; + } + std::cout << "],\"resolved_sum\":[" << explicit_total[0] + << "],\"compatibility_total\":[" << total[0] << "]}\n"; + } else { + std::cout << std::setprecision(17) + << "process: " << runtime.process() << " [" + << runtime.process_key() << "]\n" + << "resolved shape: (1, " << resolved.helicities.size() << ", " + << resolved.colors.size() << ")\n"; + for (std::size_t helicity = 0; + helicity < resolved.helicities.size(); + ++helicity) { + for (std::size_t color = 0; color < resolved.colors.size(); ++color) { + std::cout << " " << resolved.helicities[helicity].id << " " + << resolved.colors[color].id << " " + << resolved(0, helicity, color) << '\n'; + } + } + std::cout << "explicit resolved sum: " << explicit_total[0] << '\n' + << "compatibility total: " << total[0] << '\n'; + } + const double scale = std::max(std::abs(total[0]), 1.0); + if (std::abs(explicit_total[0] - total[0]) > 1.0e-12 * scale) { + throw std::runtime_error( + "resolved components do not reproduce the compatibility total"); + } + return 0; + } catch (const std::exception &error) { + std::cerr << "check_standalone: " << error.what() << '\n'; + return 1; + } +} diff --git a/pyamplicol/src/pyamplicol/assets/selftest/portable-64le/artifact/API/fortran/Makefile b/pyamplicol/src/pyamplicol/assets/selftest/portable-64le/artifact/API/fortran/Makefile new file mode 100644 index 0000000000..e626df0d15 --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/selftest/portable-64le/artifact/API/fortran/Makefile @@ -0,0 +1,46 @@ +# SPDX-License-Identifier: 0BSD +RUSTICOL_CONFIG ?= rusticol-config +RUSTICOL_CONFIG_PATH := $(shell \ + if command -v "$(RUSTICOL_CONFIG)" >/dev/null 2>&1; then \ + command -v "$(RUSTICOL_CONFIG)"; \ + elif [ "$(RUSTICOL_CONFIG)" = rusticol-config ]; then \ + directory="$(CURDIR)"; \ + while [ "$$directory" != / ]; do \ + candidate="$$directory/.venv/bin/rusticol-config"; \ + if [ -x "$$candidate" ]; then printf '%s\n' "$$candidate"; break; fi; \ + directory=$$(dirname "$$directory"); \ + done; \ + fi) +ifeq ($(strip $(RUSTICOL_CONFIG_PATH)),) +$(error rusticol-config is unavailable; activate the pyAmpliCol environment or set RUSTICOL_CONFIG=/absolute/path) +endif +ifeq ($(origin FC), default) +FC := gfortran +endif +FC ?= gfortran +ARTIFACT_DIR := $(abspath ../..) +BUILD_DIR ?= $(abspath ../../..)/.pyamplicol-api-build/$(notdir $(ARTIFACT_DIR))/fortran +TARGET := $(BUILD_DIR)/check_standalone +RUSTICOL_FORTRAN := $(shell "$(RUSTICOL_CONFIG_PATH)" --fortran-source) +FFLAGS ?= -O2 +FFLAGS += -std=f2008 -ffree-line-length-none -Wall -Wextra +LDLIBS += $(shell "$(RUSTICOL_CONFIG_PATH)" --libs) + +.PHONY: all check_standalone clean run + +all: $(TARGET) + +check_standalone: $(TARGET) + +$(BUILD_DIR): + mkdir -p "$@" + +$(TARGET): check_standalone.f90 | $(BUILD_DIR) + test -f "$(RUSTICOL_FORTRAN)" + $(FC) $(FFLAGS) -J"$(BUILD_DIR)" "$(RUSTICOL_FORTRAN)" "$<" -o "$@" $(LDLIBS) + +run: $(TARGET) + cd "$(ARTIFACT_DIR)" && "$(TARGET)" $(ARGS) + +clean: + $(RM) "$(TARGET)" "$(BUILD_DIR)/rusticol.mod" diff --git a/pyamplicol/src/pyamplicol/assets/selftest/portable-64le/artifact/API/fortran/check_standalone.f90 b/pyamplicol/src/pyamplicol/assets/selftest/portable-64le/artifact/API/fortran/check_standalone.f90 new file mode 100644 index 0000000000..d52a39379a --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/selftest/portable-64le/artifact/API/fortran/check_standalone.f90 @@ -0,0 +1,374 @@ +! SPDX-License-Identifier: 0BSD + +program check_standalone + use, intrinsic :: iso_c_binding + use, intrinsic :: iso_fortran_env, only: error_unit + use rusticol + implicit none + + integer, parameter :: MAX_OVERRIDES = 128 + type(rusticol_runtime) :: runtime + type(rusticol_external_particle), allocatable :: particles(:) + type(rusticol_helicity_configuration), allocatable :: helicities(:) + type(rusticol_color_component), allocatable :: colors(:) + character(len=4096) :: process_dir, process_key, representative_process_key + character(len=4096) :: model_parameters, kinematics + character(len=4096) :: executable + character(len=256) :: override_names_buffer(MAX_OVERRIDES) + character(len=256), allocatable :: override_names(:) + real(c_double) :: override_real_buffer(MAX_OVERRIDES) + real(c_double) :: override_imaginary_buffer(MAX_OVERRIDES) + real(c_double), allocatable :: override_real(:), override_imaginary(:) + real(c_double), allocatable, target :: momenta(:), totals(:), resolved(:, :, :) + integer(c_size_t), allocatable :: external_permutation(:) + logical :: json_output, point_available + integer :: precision, override_count + integer(c_int) :: status + integer :: color_index, helicity_index + real(c_double) :: explicit_total, scale + + call parse_options(process_key, model_parameters, kinematics, precision, json_output, & + override_names_buffer, override_real_buffer, override_imaginary_buffer, & + override_count) + if (precision /= 16) call fail( & + "the Fortran Rusticol API supports only double precision (--precision 16)") + call resolve_process_dir(executable, process_dir) + + if (len_trim(process_key) > 0 .and. len_trim(model_parameters) > 0) then + call runtime%load(trim(process_dir), trim(process_key), & + trim(model_parameters), status) + else if (len_trim(process_key) > 0) then + call runtime%load(trim(process_dir), trim(process_key), ierr=status) + else if (len_trim(model_parameters) > 0) then + call runtime%load(trim(process_dir), model_parameters=trim(model_parameters), & + ierr=status) + else + call runtime%load(trim(process_dir), ierr=status) + end if + if (status /= RUSTICOL_STATUS_OK) call fail(rusticol_last_error()) + + if (override_count > 0) then + allocate(override_names(override_count), override_real(override_count), & + override_imaginary(override_count)) + override_names = override_names_buffer(1:override_count) + override_real = override_real_buffer(1:override_count) + override_imaginary = override_imaginary_buffer(1:override_count) + call runtime%set_model_parameters( & + override_names, override_real, override_imaginary, ierr=status) + if (status /= RUSTICOL_STATUS_OK) call fail(rusticol_last_error()) + end if + + particles = runtime%external_particles(ierr=status) + if (status /= RUSTICOL_STATUS_OK) call fail(rusticol_last_error()) + helicities = runtime%helicities(ierr=status) + if (status /= RUSTICOL_STATUS_OK) call fail(rusticol_last_error()) + colors = runtime%colors(ierr=status) + if (status /= RUSTICOL_STATUS_OK) call fail(rusticol_last_error()) + process_key = runtime%process_key(ierr=status) + if (status /= RUSTICOL_STATUS_OK) call fail(rusticol_last_error()) + if (len_trim(kinematics) > 0) then + call runtime%load_kinematics_json(trim(kinematics), momenta, ierr=status) + if (status /= RUSTICOL_STATUS_OK) call fail(rusticol_last_error()) + point_available = .true. + else + representative_process_key = runtime%representative_process_key(ierr=status) + if (status /= RUSTICOL_STATUS_OK) call fail(rusticol_last_error()) + external_permutation = runtime%external_permutation(ierr=status) + if (status /= RUSTICOL_STATUS_OK) call fail(rusticol_last_error()) + call load_validation_point( & + trim(process_dir) // "/API/validation_points.dat", & + trim(representative_process_key), size(particles), momenta, point_available) + if (point_available) call reorder_validation_point(momenta, external_permutation) + end if + + if (.not. point_available) then + if (json_output) then + call emit_json_prefix(runtime, particles, helicities, colors, .false.) + write(*, '(A)') & + ',"diagnostic":"no bundled validation point is available"}' + else + write(*, '(A)') "process: " // runtime%process() + write(*, '(A)') & + "no bundled validation point is available; metadata load succeeded" + end if + stop 0 + end if + + call runtime%evaluate(momenta, 1_c_size_t, totals, ierr=status) + if (status /= RUSTICOL_STATUS_OK) call fail(rusticol_last_error()) + call runtime%evaluate_resolved(momenta, 1_c_size_t, resolved, ierr=status) + if (status /= RUSTICOL_STATUS_OK) call fail(rusticol_last_error()) + explicit_total = sum(resolved(:, :, 1)) + + if (json_output) then + call emit_json_prefix(runtime, particles, helicities, colors, .true.) + write(*, '(A,I0,A,I0,A)', advance='no') & + ',"shape":[1,', size(resolved, 2), ',', size(resolved, 1), & + '],"values":[' + do helicity_index = 1, size(resolved, 2) + do color_index = 1, size(resolved, 1) + if (helicity_index > 1 .or. color_index > 1) & + write(*, '(A)', advance='no') ',' + write(*, '(G0.17)', advance='no') & + resolved(color_index, helicity_index, 1) + end do + end do + write(*, '(A,G0.17,A,G0.17,A)') '],"resolved_sum":[', & + explicit_total, '],"compatibility_total":[', totals(1), ']}' + else + write(*, '(A)') "process: " // runtime%process() // & + " [" // trim(process_key) // "]" + write(*, '(A,I0,A,I0,A)') "resolved shape: (1, ", & + size(resolved, 2), ", ", size(resolved, 1), ")" + do helicity_index = 1, size(helicities) + do color_index = 1, size(colors) + write(*, '(2X,A,2X,A,2X,G0.17)') & + trim(helicities(helicity_index)%id), & + trim(colors(color_index)%id), & + resolved(color_index, helicity_index, 1) + end do + end do + write(*, '(A,G0.17)') "explicit resolved sum: ", explicit_total + write(*, '(A,G0.17)') "compatibility total: ", totals(1) + end if + scale = max(abs(totals(1)), 1.0_c_double) + if (abs(explicit_total - totals(1)) > 1.0e-12_c_double * scale) then + call fail("resolved components do not reproduce the compatibility total") + end if + +contains + + subroutine fail(message) + character(len=*), intent(in) :: message + write(error_unit, '(A)') "check_standalone: " // trim(message) + stop 1 + end subroutine fail + + subroutine parse_options(selected_process, model_card, kinematics_path, selected_precision, & + json, names, real_values, imaginary_values, count) + character(len=*), intent(out) :: selected_process, model_card, kinematics_path + integer, intent(out) :: selected_precision, count + logical, intent(out) :: json + character(len=*), intent(out) :: names(:) + real(c_double), intent(out) :: real_values(:), imaginary_values(:) + character(len=4096) :: argument, value + integer :: argc, index, parse_status + + selected_process = "" + model_card = "" + kinematics_path = "" + selected_precision = 16 + count = 0 + json = .false. + argc = command_argument_count() + index = 1 + do while (index <= argc) + call get_command_argument(index, argument) + select case (trim(argument)) + case ("--process") + if (index + 1 > argc) call fail("missing value after --process") + index = index + 1 + call get_command_argument(index, selected_process) + case ("--model-parameters") + if (index + 1 > argc) call fail( & + "missing value after --model-parameters") + index = index + 1 + call get_command_argument(index, model_card) + case ("--kinematics") + if (index + 1 > argc) call fail("missing value after --kinematics") + index = index + 1 + call get_command_argument(index, kinematics_path) + case ("--set-parameter") + if (index + 3 > argc) call fail( & + "--set-parameter requires NAME REAL IMAG") + if (count >= size(names)) call fail( & + "too many --set-parameter options") + count = count + 1 + index = index + 1 + call get_command_argument(index, names(count)) + index = index + 1 + call get_command_argument(index, value) + read(value, *, iostat=parse_status) real_values(count) + if (parse_status /= 0) call fail( & + "invalid real model-parameter component") + index = index + 1 + call get_command_argument(index, value) + read(value, *, iostat=parse_status) imaginary_values(count) + if (parse_status /= 0) call fail( & + "invalid imaginary model-parameter component") + case ("--precision") + if (index + 1 > argc) call fail("missing value after --precision") + index = index + 1 + call get_command_argument(index, value) + read(value, *, iostat=parse_status) selected_precision + if (parse_status /= 0) call fail("invalid --precision value") + case ("--json") + json = .true. + case ("--help", "-h") + write(*, '(A)') "usage: check_standalone [--process ID|EXPRESSION] " // & + "[--model-parameters PATH] [--kinematics PATH] " // & + "[--set-parameter NAME REAL IMAG] " // & + "[--precision 16] [--json]" + stop 0 + case default + call fail("unknown option: " // trim(argument)) + end select + index = index + 1 + end do + end subroutine parse_options + + subroutine resolve_process_dir(program_path, root) + character(len=*), intent(out) :: program_path, root + integer :: slash + logical :: artifact_in_working_directory + call get_command_argument(0, program_path) + inquire(file="artifact.json", exist=artifact_in_working_directory) + if (artifact_in_working_directory) then + root = "." + return + end if + slash = scan(trim(program_path), "/", back=.true.) + if (slash == 0) then + root = "../.." + else + root = program_path(:slash - 1) // "/../.." + end if + end subroutine resolve_process_dir + + subroutine load_validation_point(path, key, external_count, values, available) + character(len=*), intent(in) :: path, key + integer, intent(in) :: external_count + real(c_double), allocatable, target, intent(out) :: values(:) + logical, intent(out) :: available + character(len=65536) :: line + character(len=4096) :: row_key + integer :: unit, io_status, row_count + + available = .false. + allocate(values(4 * external_count)) + open(newunit=unit, file=path, status="old", action="read", & + iostat=io_status) + if (io_status /= 0) return + read(unit, '(A)', iostat=io_status) line + if (io_status /= 0 .or. & + trim(line) /= "RUSTICOL_VALIDATION_POINTS_V1") then + close(unit) + call fail("unsupported validation_points.dat format") + end if + do + read(unit, '(A)', iostat=io_status) line + if (io_status /= 0) exit + if (len_trim(line) == 0 .or. line(1:1) == "#") cycle + read(line, *, iostat=io_status) row_key, row_count + if (io_status /= 0 .or. trim(row_key) /= trim(key)) cycle + if (row_count /= external_count) then + close(unit) + call fail( & + "validation point has an incompatible external-particle count") + end if + read(line, *, iostat=io_status) row_key, row_count, values + if (io_status /= 0) then + close(unit) + call fail("invalid validation point row") + end if + available = .true. + exit + end do + close(unit) + end subroutine load_validation_point + + subroutine reorder_validation_point(values, permutation) + real(c_double), allocatable, intent(inout) :: values(:) + integer(c_size_t), intent(in) :: permutation(:) + real(c_double), allocatable :: reordered(:) + logical, allocatable :: seen(:) + integer :: representative_index, public_index + + if (size(values) /= 4 * size(permutation)) call fail( & + "validation point does not match external permutation") + allocate(reordered(size(values)), seen(size(permutation))) + seen = .false. + do representative_index = 1, size(permutation) + public_index = int(permutation(representative_index)) + 1 + if (public_index < 1 .or. public_index > size(permutation) .or. & + seen(public_index)) call fail( & + "runtime returned an invalid external permutation") + seen(public_index) = .true. + reordered(4 * public_index - 3:4 * public_index) = & + values(4 * representative_index - 3:4 * representative_index) + end do + call move_alloc(reordered, values) + end subroutine reorder_validation_point + + function json_escape(value) result(escaped) + character(len=*), intent(in) :: value + character(len=:), allocatable :: escaped + integer :: index + escaped = "" + do index = 1, len_trim(value) + select case (value(index:index)) + case ('"') + escaped = escaped // '\"' + case ('\') + escaped = escaped // '\\' + case default + escaped = escaped // value(index:index) + end select + end do + end function json_escape + + subroutine emit_json_prefix(active_runtime, external, helicity_items, & + color_items, available) + type(rusticol_runtime), intent(inout) :: active_runtime + type(rusticol_external_particle), intent(in) :: external(:) + type(rusticol_helicity_configuration), intent(in) :: helicity_items(:) + type(rusticol_color_component), intent(in) :: color_items(:) + logical, intent(in) :: available + integer :: item, component + + write(*, '(A)', advance='no') '{"language":"fortran","available":' + if (available) then + write(*, '(A)', advance='no') 'true,"precision":16' + else + write(*, '(A)', advance='no') 'false' + end if + write(*, '(A)', advance='no') ',"process":"' // & + json_escape(active_runtime%process()) // '","process_key":"' // & + json_escape(active_runtime%process_key()) // & + '","color_accuracy":"' // & + json_escape(active_runtime%color_accuracy()) // & + '","external_particles":[' + do item = 1, size(external) + if (item > 1) write(*, '(A)', advance='no') ',' + write(*, '(A,I0,A,I0,A)', advance='no') & + '{"index":', external(item)%index, ',"pdg":', & + external(item)%pdg, '}' + end do + write(*, '(A)', advance='no') '],"helicities":[' + do item = 1, size(helicity_items) + if (item > 1) write(*, '(A)', advance='no') ',' + write(*, '(A)', advance='no') '{"id":"' // & + json_escape(helicity_items(item)%id) // '","helicities":[' + do component = 1, size(helicity_items(item)%helicities) + if (component > 1) write(*, '(A)', advance='no') ',' + write(*, '(I0)', advance='no') & + helicity_items(item)%helicities(component) + end do + write(*, '(A)', advance='no') ']}' + end do + write(*, '(A)', advance='no') '],"colors":[' + do item = 1, size(color_items) + if (item > 1) write(*, '(A)', advance='no') ',' + write(*, '(A)', advance='no') '{"id":"' // & + json_escape(color_items(item)%id) // '","kind":"' // & + json_escape(color_items(item)%kind) // '","word":[' + do component = 1, size(color_items(item)%word) + if (component > 1) write(*, '(A)', advance='no') ',' + write(*, '(I0)', advance='no') color_items(item)%word(component) + end do + write(*, '(A)', advance='no') ']}' + end do + write(*, '(A)', advance='no') ']' + end subroutine emit_json_prefix + +end program check_standalone diff --git a/pyamplicol/src/pyamplicol/assets/selftest/portable-64le/artifact/API/python/check_standalone.py b/pyamplicol/src/pyamplicol/assets/selftest/portable-64le/artifact/API/python/check_standalone.py new file mode 100755 index 0000000000..3e22ac3d56 --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/selftest/portable-64le/artifact/API/python/check_standalone.py @@ -0,0 +1,401 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: 0BSD +"""Evaluate a generated process through pyAmpliCol's installed Python API.""" + +from __future__ import annotations + +import argparse +import importlib +import json +import math +import os +import statistics +import sys +import time +from decimal import Decimal +from pathlib import Path +from typing import Any + + +def _load_pyamplicol() -> Any: + try: + return importlib.import_module("pyamplicol") + except ModuleNotFoundError as error: + if error.name != "pyamplicol": + raise + previous = os.environ.get("_PYAMPLICOL_API_BOOTSTRAP") + if previous is not None: + raise SystemExit( + "pyamplicol remained unavailable after retrying with " + previous + ) from error + script = Path(__file__).resolve() + layouts = ( + (Path("bin/python"), Path("bin/rusticol-config")), + (Path("Scripts/python.exe"), Path("Scripts/rusticol-config.exe")), + ) + for parent in script.parents: + for python_relative, config_relative in layouts: + python = parent / ".venv" / python_relative + config = parent / ".venv" / config_relative + if ( + not python.is_file() + or not os.access(python, os.X_OK) + or not config.is_file() + or not os.access(config, os.X_OK) + ): + continue + environment = os.environ.copy() + environment["_PYAMPLICOL_API_BOOTSTRAP"] = str(python) + os.execve( + python, + (str(python), str(script), *sys.argv[1:]), + environment, + ) + raise SystemExit( + "pyamplicol is not importable; activate its environment or place this " + "artifact below a checkout containing .venv from `just dev-install`" + ) from error + + +pyamplicol = _load_pyamplicol() + + +def _validation_point(path: Path, process_id: str, precision: int): + if not path.is_file(): + return None + lines = path.read_text(encoding="ascii").splitlines() + if not lines or lines[0] != "RUSTICOL_VALIDATION_POINTS_V1": + raise SystemExit("unsupported validation_points.dat format") + for line in lines[1:]: + if not line or line.startswith("#"): + continue + fields = line.split("\t") + if len(fields) < 2 or fields[0] != process_id: + continue + external_count = int(fields[1]) + components = fields[2:] + if len(components) != external_count * 4: + raise SystemExit(f"invalid validation row for {process_id!r}") + convert = float if precision == 16 else Decimal + return [ + [convert(value) for value in components[index : index + 4]] + for index in range(0, len(components), 4) + ] + return None + + +def _reorder_point( + point: list[list[Any]], permutation: tuple[int, ...] +) -> list[list[Any]]: + if len(point) != len(permutation) or sorted(permutation) != list(range(len(point))): + raise SystemExit("runtime returned an invalid external permutation") + reordered: list[list[Any] | None] = [None] * len(point) + for representative_index, public_index in enumerate(permutation): + reordered[public_index] = point[representative_index] + if any(vector is None for vector in reordered): + raise SystemExit("runtime returned an incomplete external permutation") + return [vector for vector in reordered if vector is not None] + + +def _kinematics(path: str, precision: int, external_count: int) -> list[list[Any]]: + source = Path(path).expanduser().resolve(strict=True) + + def invalid_constant(value: str) -> None: + raise ValueError(f"non-finite JSON constant {value!r}") + + try: + payload = json.loads( + source.read_text(encoding="utf-8"), + parse_float=Decimal if precision != 16 else float, + parse_int=Decimal if precision != 16 else int, + parse_constant=invalid_constant, + ) + except (json.JSONDecodeError, OSError, ValueError) as error: + raise SystemExit(f"could not read kinematics JSON {source}: {error}") from error + if ( + isinstance(payload, list) + and len(payload) == 1 + and isinstance(payload[0], list) + and payload[0] + and isinstance(payload[0][0], list) + ): + payload = payload[0] + if not isinstance(payload, list) or len(payload) != external_count: + raise SystemExit( + f"kinematics JSON must contain one [{external_count}][4] point" + ) + convert = float if precision == 16 else Decimal + point: list[list[Any]] = [] + for leg_index, raw_leg in enumerate(payload): + if not isinstance(raw_leg, list) or len(raw_leg) != 4: + raise SystemExit( + f"kinematics leg {leg_index} must contain [E, px, py, pz]" + ) + leg: list[Any] = [] + for component_index, raw in enumerate(raw_leg): + if isinstance(raw, bool) or not isinstance(raw, (int, float, Decimal, str)): + raise SystemExit( + f"kinematics leg {leg_index} component {component_index} " + "must be a number or decimal string" + ) + try: + value = convert(raw) + except (TypeError, ValueError) as error: + raise SystemExit( + f"kinematics leg {leg_index} component {component_index} " + "is not a decimal value" + ) from error + finite = ( + math.isfinite(value) + if isinstance(value, float) + else value.is_finite() + ) + if not finite: + raise SystemExit("kinematics components must be finite") + leg.append(value) + point.append(leg) + return point + + +def _repeat_point(point: list[list[Any]], count: int) -> list[list[list[Any]]]: + return [point for _ in range(count)] + + +def _flatten(values: Any) -> list[Any]: + if hasattr(values, "reshape"): + return values.reshape(-1).tolist() + output: list[Any] = [] + + def visit(value: Any) -> None: + if isinstance(value, (list, tuple)): + for entry in value: + visit(entry) + else: + output.append(value) + + visit(values) + return output + + +def _real(value: Any) -> Any: + if isinstance(value, Decimal): + return value + converted = complex(value) + if abs(converted.imag) > 1.0e-14 * max(1.0, abs(converted.real)): + raise SystemExit("matrix-element output unexpectedly has an imaginary part") + return converted.real + + +def _json_scalar(value: Any, precision: int) -> float | str: + real = _real(value) + return float(real) if precision == 16 else str(real) + + +def _parameter_component(value: Any) -> float: + if isinstance(value, bool) or not isinstance(value, (int, float)): + raise SystemExit("model-parameter components must be JSON numbers") + component = float(value) + if not math.isfinite(component): + raise SystemExit("model-parameter components must be finite") + return component + + +def _parameter_value(value: Any) -> complex | float: + if isinstance(value, list): + if len(value) != 2: + raise SystemExit("model-parameter arrays must contain [real, imaginary]") + return complex( + _parameter_component(value[0]), + _parameter_component(value[1]), + ) + return _parameter_component(value) + + +def _model_parameters(path: str | None) -> dict[str, complex | float] | None: + if path is None: + return None + source = Path(path).expanduser().resolve(strict=True) + payload = json.loads(source.read_text(encoding="utf-8")) + if not isinstance(payload, dict): + raise SystemExit("model-parameter JSON must be an object") + return {str(name): _parameter_value(value) for name, value in payload.items()} + + +def _physics_payload(physics: pyamplicol.ProcessPhysics) -> dict[str, Any]: + colors = [ + {"id": item.id, "kind": "lc-flow", "word": list(item.word)} + for item in physics.color_flows + ] + colors.extend( + {"id": item.id, "kind": "contracted-color", "word": []} + for item in physics.contracted_color_components + ) + return { + "process": physics.process, + "process_key": physics.process_id, + "color_accuracy": physics.color_accuracy, + "external_particles": [ + {"index": item.index, "pdg": item.pdg_id} + for item in physics.external_particles + ], + "helicities": [ + {"id": item.id, "helicities": list(item.values)} + for item in physics.helicities + ], + "colors": colors, + } + + +def _profile( + runtime: pyamplicol.Runtime, + point: list[list[Any]], + precision: int, + target_runtime: float, + batch_size: int, +) -> dict[str, float | int]: + samples: list[float] = [] + elapsed = 0.0 + while len(samples) < 8 or elapsed < target_runtime: + batch = _repeat_point(point, batch_size) + started = time.perf_counter() + runtime.evaluate(batch, precision=precision) + wall = time.perf_counter() - started + elapsed += wall + samples.append(wall / batch_size) + return { + "samples": len(samples) * batch_size, + "wall_us_per_point": statistics.mean(samples) * 1.0e6, + } + + +def _parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + description="Resolved pyAmpliCol Python API example" + ) + parser.add_argument( + "--process", + help="stable process/alias ID or exact concrete process expression", + ) + parser.add_argument("--model-parameters") + parser.add_argument( + "--kinematics", + help="JSON point in the external-particle order supplied by --process", + ) + parser.add_argument( + "--set-parameter", + nargs=3, + action="append", + default=[], + metavar=("NAME", "REAL", "IMAG"), + ) + parser.add_argument("--precision", type=int, default=16) + parser.add_argument("--profile", action="store_true") + parser.add_argument("--target-runtime", type=float, default=10.0) + parser.add_argument("--batch-size", type=int, default=128) + parser.add_argument("--json", action="store_true") + return parser + + +def main() -> int: + args = _parser().parse_args() + root = Path(__file__).resolve().parents[2] + runtime = pyamplicol.Runtime.load( + root, + process=args.process, + model_parameters=_model_parameters(args.model_parameters), + ) + if args.set_parameter: + runtime.set_model_parameters( + { + name: complex(float(real), float(imaginary)) + for name, real, imaginary in args.set_parameter + } + ) + physics = runtime.physics + metadata = _physics_payload(physics) + if args.kinematics is None: + point = _validation_point( + root / "API" / "validation_points.dat", + runtime.representative_process_key, + args.precision, + ) + if point is not None: + point = _reorder_point(point, runtime.external_permutation) + else: + point = _kinematics( + args.kinematics, + args.precision, + len(physics.external_particles), + ) + if point is None: + payload = { + "language": "python", + "available": False, + "diagnostic": "no bundled validation point is available", + **metadata, + } + if args.json: + print(json.dumps(payload, separators=(",", ":"), sort_keys=True)) + else: + print(f"process: {physics.process}") + print("no bundled validation point is available; metadata load succeeded") + return 0 + + batch = _repeat_point(point, 1) + resolved = runtime.evaluate_resolved(batch, precision=args.precision) + total = runtime.evaluate(batch, precision=args.precision) + values = _flatten(resolved.values) + explicit_total = _flatten(resolved.total()) + compatibility_total = _flatten(total) + payload: dict[str, Any] = { + "language": "python", + "available": True, + "precision": args.precision, + **metadata, + "shape": list(resolved.shape), + "values": [_json_scalar(value, args.precision) for value in values], + "resolved_sum": [ + _json_scalar(value, args.precision) for value in explicit_total + ], + "compatibility_total": [ + _json_scalar(value, args.precision) for value in compatibility_total + ], + } + if args.profile: + payload["profile"] = _profile( + runtime, + point, + args.precision, + max(args.target_runtime, 0.0), + max(args.batch_size, 1), + ) + if args.json: + print(json.dumps(payload, separators=(",", ":"), sort_keys=True)) + return 0 + + print(f"process: {physics.process} [{physics.process_id}]") + print(f"resolved shape: {resolved.shape}") + color_items = metadata["colors"] + color_count = len(color_items) + for helicity_index, helicity in enumerate(metadata["helicities"]): + for color_index, color in enumerate(color_items): + offset = helicity_index * color_count + color_index + print(f" {helicity['id']} {color['id']} {values[offset]}") + print(f"explicit resolved sum: {explicit_total[0]}") + print(f"compatibility total: {compatibility_total[0]}") + if not math.isclose( + float(_real(explicit_total[0])), + float(_real(compatibility_total[0])), + rel_tol=1.0e-12, + abs_tol=1.0e-15, + ): + raise SystemExit("resolved components do not reproduce the compatibility total") + if args.profile: + profile = payload["profile"] + assert isinstance(profile, dict) + print(f"timing: {profile['wall_us_per_point']:.6g} us/point") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/pyamplicol/src/pyamplicol/assets/selftest/portable-64le/artifact/API/rust/Makefile b/pyamplicol/src/pyamplicol/assets/selftest/portable-64le/artifact/API/rust/Makefile new file mode 100644 index 0000000000..3f8824f58a --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/selftest/portable-64le/artifact/API/rust/Makefile @@ -0,0 +1,49 @@ +# SPDX-License-Identifier: 0BSD +RUSTICOL_CONFIG ?= rusticol-config +RUSTICOL_CONFIG_PATH := $(shell \ + if command -v "$(RUSTICOL_CONFIG)" >/dev/null 2>&1; then \ + command -v "$(RUSTICOL_CONFIG)"; \ + elif [ "$(RUSTICOL_CONFIG)" = rusticol-config ]; then \ + directory="$(CURDIR)"; \ + while [ "$$directory" != / ]; do \ + candidate="$$directory/.venv/bin/rusticol-config"; \ + if [ -x "$$candidate" ]; then printf '%s\n' "$$candidate"; break; fi; \ + directory=$$(dirname "$$directory"); \ + done; \ + fi) +ifeq ($(strip $(RUSTICOL_CONFIG_PATH)),) +$(error rusticol-config is unavailable; activate the pyAmpliCol environment or set RUSTICOL_CONFIG=/absolute/path) +endif +RUSTC ?= rustc +RUST_SCRIPT ?= rust-script +ARTIFACT_DIR := $(abspath ../..) +BUILD_DIR ?= $(abspath ../../..)/.pyamplicol-api-build/$(notdir $(ARTIFACT_DIR))/rust +TARGET := $(BUILD_DIR)/check_standalone +RUSTFLAGS ?= -C opt-level=2 +RUSTICOL_RUST_SOURCE := $(shell "$(RUSTICOL_CONFIG_PATH)" --rust-source) +RUSTICOL_RUSTFLAGS := $(shell "$(RUSTICOL_CONFIG_PATH)" --rustflags) + +.PHONY: all check_standalone clean run run-script + +all: $(TARGET) + +check_standalone: $(TARGET) + +$(BUILD_DIR): + mkdir -p "$@" + +$(TARGET): check_standalone.rs | $(BUILD_DIR) + RUSTICOL_RUST_SOURCE="$(RUSTICOL_RUST_SOURCE)" \ + $(RUSTC) --edition=2021 $(RUSTFLAGS) "$<" -o "$@" $(RUSTICOL_RUSTFLAGS) + +run: $(TARGET) + cd "$(ARTIFACT_DIR)" && "$(TARGET)" $(ARGS) + +run-script: + cd "$(ARTIFACT_DIR)" && \ + RUSTICOL_RUST_SOURCE="$(RUSTICOL_RUST_SOURCE)" \ + CARGO_ENCODED_RUSTFLAGS="$$("$(RUSTICOL_CONFIG_PATH)" --cargo-rustflags)" \ + $(RUST_SCRIPT) "API/rust/check_standalone.rs" -- $(ARGS) + +clean: + $(RM) "$(TARGET)" diff --git a/pyamplicol/src/pyamplicol/assets/selftest/portable-64le/artifact/API/rust/check_standalone.rs b/pyamplicol/src/pyamplicol/assets/selftest/portable-64le/artifact/API/rust/check_standalone.rs new file mode 100644 index 0000000000..5c91f5c862 --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/selftest/portable-64le/artifact/API/rust/check_standalone.rs @@ -0,0 +1,454 @@ +#!/usr/bin/env rust-script +// SPDX-License-Identifier: 0BSD +//! ```cargo +//! [package] +//! edition = "2021" +//! ``` + +use std::env; +use std::fmt::Write as _; +use std::fs; +use std::path::{Path, PathBuf}; + +#[allow(dead_code)] +mod rusticol { + include!(env!("RUSTICOL_RUST_SOURCE")); +} + +use rusticol::{ParameterUpdate, PhysicsMetadata, ResolvedEvaluation, Runtime, Selectors}; + +#[derive(Default)] +struct Options { + process: Option, + model_parameters: Option, + kinematics: Option, + overrides: Vec, + precision: u32, + json: bool, + help: bool, +} + +fn sdk(result: rusticol::Result) -> Result { + result.map_err(|error| error.to_string()) +} + +fn usage() -> &'static str { + "usage: check_standalone [--process ID|EXPRESSION] [--model-parameters PATH] \ +[--kinematics PATH] [--set-parameter NAME REAL IMAG] [--precision 16] [--json]" +} + +fn parse_options() -> Result { + let arguments = env::args().skip(1).collect::>(); + let mut options = Options { + precision: 16, + ..Options::default() + }; + let mut index = 0; + while index < arguments.len() { + let argument = &arguments[index]; + match argument.as_str() { + "--" => { + index += 1; + } + "--process" => { + options.process = Some(required_argument(&arguments, index, 1)?.to_owned()); + index += 2; + } + "--model-parameters" => { + options.model_parameters = + Some(required_argument(&arguments, index, 1)?.to_owned()); + index += 2; + } + "--kinematics" => { + options.kinematics = + Some(required_argument(&arguments, index, 1)?.to_owned()); + index += 2; + } + "--set-parameter" => { + let name = required_argument(&arguments, index, 1)?.to_owned(); + let real = parse_f64( + required_argument(&arguments, index, 2)?, + "real model-parameter component", + )?; + let imaginary = parse_f64( + required_argument(&arguments, index, 3)?, + "imaginary model-parameter component", + )?; + options + .overrides + .push(ParameterUpdate::new(name, real, imaginary)); + index += 4; + } + "--precision" => { + options.precision = required_argument(&arguments, index, 1)? + .parse() + .map_err(|_| "invalid --precision value".to_owned())?; + index += 2; + } + "--json" => { + options.json = true; + index += 1; + } + "--help" | "-h" => { + options.help = true; + index += 1; + } + _ => return Err(format!("unknown option: {argument}")), + } + } + if options.precision != 16 { + return Err( + "the Rust Rusticol API supports only double precision (--precision 16)".to_owned(), + ); + } + Ok(options) +} + +fn required_argument<'a>( + arguments: &'a [String], + option_index: usize, + offset: usize, +) -> Result<&'a str, String> { + arguments + .get(option_index + offset) + .map(String::as_str) + .ok_or_else(|| format!("missing value after {}", arguments[option_index])) +} + +fn parse_f64(value: &str, description: &str) -> Result { + let parsed = value + .parse::() + .map_err(|_| format!("invalid {description}"))?; + if parsed.is_finite() { + Ok(parsed) + } else { + Err(format!("invalid {description}")) + } +} + +fn artifact_root() -> Result { + let current = + env::current_dir().map_err(|error| format!("cannot read current directory: {error}"))?; + if current.join("artifact.json").is_file() { + return Ok(current); + } + + let executable = env::current_exe() + .map_err(|error| format!("cannot locate check_standalone executable: {error}"))?; + for ancestor in executable.ancestors().skip(1) { + if ancestor.join("artifact.json").is_file() { + return Ok(ancestor.to_path_buf()); + } + } + + let language_dir = executable.parent(); + let build_artifact = language_dir.and_then(Path::parent); + let build_root = build_artifact.and_then(Path::parent); + if build_root.and_then(Path::file_name) == Some(".pyamplicol-api-build".as_ref()) { + if let (Some(parent), Some(name)) = ( + build_root.and_then(Path::parent), + build_artifact.and_then(Path::file_name), + ) { + let candidate = parent.join(name); + if candidate.join("artifact.json").is_file() { + return Ok(candidate); + } + } + } + + Err("run check_standalone from a generated artifact directory".to_owned()) +} + +fn load_validation_point( + path: &Path, + process_key: &str, + external_count: usize, +) -> Result>, String> { + let contents = match fs::read_to_string(path) { + Ok(contents) => contents, + Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None), + Err(error) => return Err(format!("cannot read {}: {error}", path.display())), + }; + let mut lines = contents.lines(); + if lines.next() != Some("RUSTICOL_VALIDATION_POINTS_V1") { + return Err("unsupported validation_points.dat format".to_owned()); + } + for line in lines { + if line.is_empty() || line.starts_with('#') { + continue; + } + let fields = line.split('\t').collect::>(); + if fields.len() < 2 || fields[0] != process_key { + continue; + } + let row_count = fields[1] + .parse::() + .map_err(|_| "invalid validation point row".to_owned())?; + if row_count != external_count || fields.len() != 2 + 4 * external_count { + return Err("validation point has an incompatible external-particle count".to_owned()); + } + let momenta = fields[2..] + .iter() + .map(|value| parse_f64(value, "validation-point component")) + .collect::, _>>()?; + return Ok(Some(momenta)); + } + Ok(None) +} + +fn reorder_validation_point( + representative: Vec, + permutation: &[usize], +) -> Result, String> { + if representative.len() != 4 * permutation.len() { + return Err("validation point does not match external permutation".to_owned()); + } + let mut public_point = vec![0.0; representative.len()]; + let mut seen = vec![false; permutation.len()]; + for (representative_index, &public_index) in permutation.iter().enumerate() { + if public_index >= permutation.len() || seen[public_index] { + return Err("runtime returned an invalid external permutation".to_owned()); + } + seen[public_index] = true; + public_point[4 * public_index..4 * public_index + 4].copy_from_slice( + &representative[4 * representative_index..4 * representative_index + 4], + ); + } + Ok(public_point) +} + +fn json_string(value: &str) -> String { + let mut output = String::with_capacity(value.len() + 2); + output.push('"'); + for character in value.chars() { + match character { + '"' => output.push_str("\\\""), + '\\' => output.push_str("\\\\"), + '\u{08}' => output.push_str("\\b"), + '\u{0c}' => output.push_str("\\f"), + '\n' => output.push_str("\\n"), + '\r' => output.push_str("\\r"), + '\t' => output.push_str("\\t"), + character if character <= '\u{1f}' => { + write!(&mut output, "\\u{:04x}", character as u32) + .expect("writing to a String cannot fail"); + } + character => output.push(character), + } + } + output.push('"'); + output +} + +fn write_i32_array(output: &mut String, values: &[i32]) { + output.push('['); + for (index, value) in values.iter().enumerate() { + if index != 0 { + output.push(','); + } + write!(output, "{value}").expect("writing to a String cannot fail"); + } + output.push(']'); +} + +fn write_usize_array(output: &mut String, values: &[usize]) { + output.push('['); + for (index, value) in values.iter().enumerate() { + if index != 0 { + output.push(','); + } + write!(output, "{value}").expect("writing to a String cannot fail"); + } + output.push(']'); +} + +fn write_f64_array(output: &mut String, values: &[f64]) -> Result<(), String> { + output.push('['); + for (index, value) in values.iter().enumerate() { + if !value.is_finite() { + return Err("matrix-element output is not finite".to_owned()); + } + if index != 0 { + output.push(','); + } + write!(output, "{value}").expect("writing to a String cannot fail"); + } + output.push(']'); + Ok(()) +} + +fn write_common_json(output: &mut String, metadata: &PhysicsMetadata) { + write!( + output, + "\"process\":{},\"process_key\":{},\"color_accuracy\":{}", + json_string(&metadata.process), + json_string(&metadata.process_key), + json_string(&metadata.color_accuracy), + ) + .expect("writing to a String cannot fail"); + + output.push_str(",\"external_particles\":["); + for (index, particle) in metadata.external_particles.iter().enumerate() { + if index != 0 { + output.push(','); + } + write!( + output, + "{{\"index\":{},\"pdg\":{}}}", + particle.index, particle.pdg + ) + .expect("writing to a String cannot fail"); + } + + output.push_str("],\"helicities\":["); + for (index, helicity) in metadata.helicities.iter().enumerate() { + if index != 0 { + output.push(','); + } + write!( + output, + "{{\"id\":{},\"helicities\":", + json_string(&helicity.id) + ) + .expect("writing to a String cannot fail"); + write_i32_array(output, &helicity.helicities); + output.push('}'); + } + + output.push_str("],\"colors\":["); + for (index, color) in metadata.colors.iter().enumerate() { + if index != 0 { + output.push(','); + } + write!( + output, + "{{\"id\":{},\"kind\":{},\"word\":", + json_string(&color.id), + json_string(&color.kind), + ) + .expect("writing to a String cannot fail"); + write_usize_array(output, &color.word); + output.push('}'); + } + output.push(']'); +} + +fn available_json( + metadata: &PhysicsMetadata, + resolved: &ResolvedEvaluation, + explicit_total: f64, + total: f64, +) -> Result { + let (_, helicity_count, color_count) = resolved.shape(); + let mut output = "{\"language\":\"rust\",\"available\":true,\"precision\":16,".to_owned(); + write_common_json(&mut output, metadata); + write!( + &mut output, + ",\"shape\":[1,{helicity_count},{color_count}],\"values\":" + ) + .expect("writing to a String cannot fail"); + write_f64_array(&mut output, resolved.values())?; + output.push_str(",\"resolved_sum\":"); + write_f64_array(&mut output, &[explicit_total])?; + output.push_str(",\"compatibility_total\":"); + write_f64_array(&mut output, &[total])?; + output.push_str("}\n"); + Ok(output) +} + +fn unavailable_json(metadata: &PhysicsMetadata) -> String { + let mut output = "{\"language\":\"rust\",\"available\":false,".to_owned(); + write_common_json(&mut output, metadata); + output.push_str(",\"diagnostic\":\"no bundled validation point is available\"}\n"); + output +} + +fn run() -> Result<(), String> { + let options = parse_options()?; + if options.help { + println!("{}", usage()); + return Ok(()); + } + let root = artifact_root()?; + let model_parameters = options.model_parameters.as_deref().map(Path::new); + let mut runtime = sdk(Runtime::load( + &root, + options.process.as_deref(), + model_parameters, + ))?; + sdk(runtime.set_model_parameters(&options.overrides))?; + let metadata = sdk(runtime.physics())?; + let point = if let Some(path) = options.kinematics.as_deref() { + Some(sdk(runtime.load_kinematics_json(path))?) + } else { + load_validation_point( + &root.join("API/validation_points.dat"), + &sdk(runtime.representative_process_key())?, + metadata.external_particles.len(), + )? + .map(|point| { + let permutation = sdk(runtime.external_permutation())?; + reorder_validation_point(point, &permutation) + }) + .transpose()? + }; + let Some(momenta) = point else { + if options.json { + print!("{}", unavailable_json(&metadata)); + } else { + println!("process: {}", metadata.process); + println!("no bundled validation point is available; metadata load succeeded"); + } + return Ok(()); + }; + + let totals = sdk(runtime.evaluate_f64(&momenta, 1))?; + let total = *totals + .first() + .ok_or_else(|| "Rusticol returned no compatibility total".to_owned())?; + let resolved = sdk(runtime.evaluate_resolved_f64(&momenta, 1, &Selectors::all()))?; + let (_, helicity_count, color_count) = resolved.shape(); + if helicity_count != metadata.helicities.len() || color_count != metadata.colors.len() { + return Err("resolved shape does not match runtime metadata".to_owned()); + } + let resolved_totals = resolved.totals(); + let explicit_total = *resolved_totals + .first() + .ok_or_else(|| "Rusticol returned no resolved total".to_owned())?; + if !explicit_total.is_finite() || !total.is_finite() { + return Err("matrix-element output is not finite".to_owned()); + } + let scale = total.abs().max(1.0); + if (explicit_total - total).abs() > 1.0e-12 * scale { + return Err("resolved components do not reproduce the compatibility total".to_owned()); + } + + if options.json { + print!( + "{}", + available_json(&metadata, &resolved, explicit_total, total)? + ); + return Ok(()); + } + + println!("process: {} [{}]", metadata.process, metadata.process_key); + println!("resolved shape: (1, {helicity_count}, {color_count})"); + for (helicity_index, helicity) in resolved.helicities().iter().enumerate() { + for (color_index, color) in resolved.colors().iter().enumerate() { + let value = resolved + .get(0, helicity_index, color_index) + .ok_or_else(|| "resolved result index is out of range".to_owned())?; + println!(" {} {} {}", helicity.id, color.id, value); + } + } + println!("explicit resolved sum: {explicit_total}"); + println!("compatibility total: {total}"); + Ok(()) +} + +fn main() { + if let Err(error) = run() { + eprintln!("check_standalone: {error}"); + std::process::exit(1); + } +} diff --git a/pyamplicol/src/pyamplicol/assets/selftest/portable-64le/artifact/API/validation_points.dat b/pyamplicol/src/pyamplicol/assets/selftest/portable-64le/artifact/API/validation_points.dat new file mode 100644 index 0000000000..88c07164fe --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/selftest/portable-64le/artifact/API/validation_points.dat @@ -0,0 +1,2 @@ +RUSTICOL_VALIDATION_POINTS_V1 +d_dbar_to_z 3 45.594000000000001 0 0 45.594000000000001 45.594000000000001 0 0 -45.594000000000001 91.188000000000002 0 0 0 diff --git a/pyamplicol/src/pyamplicol/assets/selftest/portable-64le/artifact/artifact.json b/pyamplicol/src/pyamplicol/assets/selftest/portable-64le/artifact/artifact.json new file mode 100644 index 0000000000..e571bc2687 --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/selftest/portable-64le/artifact/artifact.json @@ -0,0 +1 @@ +{"artifact_id":"a40595626b095cbbb547b775c57c5a71037c24b3a94effbf13facb2dd819d246","configuration":{"adjustments":[{"path":"evaluator.optimization.cores","reason":"shared affinity-aware CPU budget for Symbolica evaluator work"}],"effective_path":"config/effective.toml","requested_path":"config/requested.toml","toml_schema_version":1},"created_utc":"2026-10-03T12:47:49.472223Z","default_process_id":"d_dbar_to_z","dependencies":[{"license":"Symbolica Software License Agreement","name":"symbolica","source":"https://symbolica.io/","version":"3.0.0"}],"extensions":{"artifact_identity":{"kind":"pyamplicol-runtime-payload-identity","schema_version":1},"evaluator_payload_container":{"index_sha256":"dcf435a1434274e07601f4094d7c24e96d1a7047b3a91040068738a65cb5e412","kind":"pyamplicol-evaluator-payload-container","member_count":24,"path":"evaluators.pacbin","schema_version":1,"storage_abi":"pacbin-v1","unpacked_size_bytes":11078},"generation":{"api_bundle":{"emitted":true,"path":"API","requested":true,"scope":"root-artifact"},"concrete_processes":[{"expression":"d d~ > z","filters":{"dynamic_color_projection":{"abi":"pyamplicol-generic-dag-dynamic-color-projection-v2","after_amplitude_root_count":6,"after_current_count":13,"after_evaluation_count":6,"after_interaction_count":6,"applied":false,"before_amplitude_root_count":6,"before_current_count":13,"before_evaluation_count":6,"before_interaction_count":6,"closure_groups_sha256":"a844600669f75d6d329f9e3f0c8ecc9e5c6a668f8528ce538efd2162192fea1d","current_class_members_sha256":"ebafa2f11a32ca532efe90dd3b0bb40b11f2af3ab3b37132abc92ecc4aed8ff1","current_remap_sha256":"0caccba1e53a62e913e0ee51176dd08f281de9886b7f51ba3d8ec5bfb15fe41b","equality_check_status":"passed-no-projectable-classes","interaction_groups_sha256":"a844600669f75d6d329f9e3f0c8ecc9e5c6a668f8528ce538efd2162192fea1d","old_to_new_current_ids":[0,1,2,3,4,5,6,7,8,9,10,11,12],"projected_current_class_count":0,"rectangle_cardinalities_sha256":"38e050b6fa3804225180673e6fb5ec63776ecaa8063a507902484166d46595cb","rectangular_closure_group_count":0,"rectangular_interaction_group_count":0,"retained_color_metadata":"accuracy-line-groups-basis-key-identical; representative-sector-metadata-only","root_sector_policy":"pre-projection-resolved-sector-is-explicit-and-never-cross-projected","row_identity_sha256":"ab5790a4dacbf477223543dec077842044ad549e8e326b3c4b7ab30009b48889","selector_domains_sha256":"917fbb6f23cc4580be5c90a9e11c6f4d32534c0f7aaaba4d2e586549cf54dc64","source_revision":null,"source_semantics_sha256":"bdc021db2a2392e9ef7cb0e18d7a030f17203de9f67ba2ca59611cf6f1cd1326","split_current_class_count":0},"helicity_recurrence":{"amplitude_class_count":2,"contract_version":1,"materialization_strategy":"quotient","materialized_amplitude_count":2,"materialized_current_count":7,"optimized_amplitude_class_count":2,"optimized_current_count":9,"optimized_recurrence_class_count":3,"physical_helicity_count":12,"recurrence_class_count":7,"residual_amplitude_count":0,"residual_current_count":0,"source_state_mapping_count":7,"structural_zero_helicity_count":6},"lc_flow_layout":"topology-replay","relation_discovery":{"abi":"pyamplicol-artifact-numerical-current-reuse-v1","applied_relation_count":0,"certified_relation_count":0,"effective_mode":"certified-reuse","effective_reuse_state":"enabled","execution_mode":"compiled","lane_count":2,"lanes":{"helicity-sum":{"abi":"pyamplicol-generic-dag-numerical-current-warmup-v1","application":{"abi":"pyamplicol-authenticated-numerical-current-relation-set-v1","applied_relation_count":0,"candidate_capture_sha256":"9a9407d1c969921dab470127ef2b1b57d566d6f5a983a913cf769b519e81df28","certificate_replay":{"algorithm":"authenticated-independent-recursive-decimal-probes-v1","certificate_set_sha256":"dafbcb50c5c5fa03191abda625ab67056f35ff63f18d27c0108bfe8f68c89fef","status":"no_certified_numerical_relation"},"certificates":[],"certified_relation_count":0,"interaction_evaluation_count_before":6,"interaction_evaluation_count_projected":6,"interaction_evaluation_savings_projected":0,"mappings":[],"relation_kind_counts":{"equal":0,"opposite":0,"zero":0},"requested_mode":"certified-reuse","runtime_schema_sha256":"e2ff47ba61f9bb1ac9d9ad57731238ed0734b7c088fc75de57d74446cdffda07","schema_version":1,"scope":{"color_accuracy":"lc","execution_mode":"compiled","representation":"generic-dag"},"source_dag_sha256":"bdc021db2a2392e9ef7cb0e18d7a030f17203de9f67ba2ca59611cf6f1cd1326","source_semantics":{"abi":"pyamplicol-generic-dag-numerical-current-source-v1","sha256":"4794f99ccbc88dea1a22b767b7f8e0e3424eff81bc2731a56304ab1f191692fe"},"state":"no_certified_numerical_relation","verification_capture_sha256":"8ce4ac32cfedaefcbe01591c7a598e49a726b09d313b222248274eb7a94a93bf","warning":{"code":null,"emit":"never","message":null,"required":false}},"application_capture":null,"application_scope":{"abi":"pyamplicol-generic-dag-numerical-relation-application-scope-v1","allowed_relation_kinds":["equal","opposite","zero"],"application_eligible_relation_count":0,"discovered_relation_count":0,"policy":"exact-physical-selector-member-set-v1","reason":"complete-generic-dag","scope_suppressed_relation_count":0},"application_validation":{"checked_component_count":0,"checked_current_count":0,"maximum_absolute_residual":null,"maximum_relative_residual":null,"maximum_tolerance_ratio":null,"status":"not-required-no-applied-relations"},"applied_relation_count":0,"candidate_capture":{"abi":"pyamplicol-generic-dag-current-observation-capture-v2","capture_contract_sha256":"9a9407d1c969921dab470127ef2b1b57d566d6f5a983a913cf769b519e81df28","complete_current_component_digest":true,"components_embedded":false,"current_count":13,"evaluator":"symbolica-interpreted-high-precision-stage-replay","evaluator_output_partition":{"abi":"pyamplicol-generic-dag-current-id-output-partitions-v1","sha256":"91787d155d08a95fdad9fd74653fe688b91501a66209d22788363d1143e102fb"},"kinematic_sha256s":["07e4764a3e34af43f874738e5f1726cc6d45c1317bdabcf6dea30904aa7f991a","70cd9a12303937f454b0c30763aa465692698b281d0a4385a3f2f6357510253b","64b49558828caaedb30476a6de774e99a9b48635bacb4343815901eded648b85","570330aa5081a5326a51192fc9def5fcb0aaf97a59481b2e03728449f3bf4674"],"model_parameter_schema_sha256":"44b9ce16b482f552b778174bed4355f47e6836ce0057d6f8649c2c5242ec2d4d","observation_batch_sha256":"a1f271189335460a687c51a32564d8c413e5d26d419de788023c7624485fa421","parameter_context_sha256s":["7de3097f42e8a88f7c0fc730051918a7241b969722aa9a7cde918ee30a31bc48","798756bb3e1b110e6a1106fe7f8bdfb564095b1bba24c9a75ae1f959c55172d0","e96b72f59471a7c2585673d46f0bf1abd14e9550f0d4698d4c3369846fa7ce54","2d75c06a7a780edfd3553773aa87e6a42a7be8c36bafdb52b6960f004c0bc713"],"parameter_contexts":[["7546771113978883484263437964045806438662111759185791015625e-60","11799999999999999378275106209912337362766265869140625e-53","173e0","14915000000000000479616346638067625463008880615234375e-52","911880000000000023874235921539366245269775390625e-46","2441403999999999907544179222895763814449310302734375e-51","804190024457561634108060388825833797454833984375e-46","204760000000000008668621376273222267627716064453125e-50","125e0","6382338999999999894219282481344635016284883022308349609375e-60","-10244420275940371301004461201955564320087432861328125e-52","1781866674528745642103189084082259796559810638427734375e-55"],["-134847569752588090791494031261965602652708184905350208282470703125e-67","594236468801730715431365890955017761143608368001878261566162109375e-67","180017912417472714819833656645087227587964662234298884868621826171875e-66","1418607520800001666640541972417661876671966282909820125025755491178502191556556541462974507794569944962859153748e-111","8845655440625352648857741117815739972876412900659526159953506315626337854970961949163665849482640624046325683594e-110","2440844827436804634318075561842410785553470886133209208296951315132136921118907041394763268726819660514593124390e-111","7663581892294273545448908358214496202797406717397960807541474497626878947115214302243657584767788648605346679688e-110","1969240825499461802211304919239819353753191159531314854075138421568181711258926913643563239020295441150665283203e-111","1325364572440657479309700206814015555067953755497001111507415771484375e-67","6282212318214615756465681040143778091078274883329868316650390625e-65","-1072072896851752300436215264505413093610426376587095133168104624067673550947285901480121594886441016569733619690e-111","124566021831671000418754850780356235873114201240241527557373046875e-66"],["655617805825475631221880591648076830324498587287962436676025390625e-67","9508743447340736756304320487220849145160173065960407257080078125e-65","1833800730995227375994240863292528143091431047650985419750213623046875e-67","1417917885253575031749239105231976588110775334943781102058718916214643546711612204225261280043923761695623397827e-111","9296679062386854901358783194859654609329226514583805008942638117660874140339710702818365462007932364940643310547e-110","2577944736949078494160337607692592781342881499602461238125442612439294946878425018876868080042186193168163299560e-111","7878882507835004983969788421954676388128613320152368876411796051959239746420582406472021830268204212188720703125e-110","2169217507358018313945853497769126166281712195718656383954739160729475598750337628217721430701203644275665283203e-111","1252367052285700050356484123136036057388764675124548375606536865234375e-67","309195150609194360628341480268232999151223339140415191650390625e-64","-1058333689713571520504797215545769574180750731277403150472198582723563643195538452879134183604037389159202575684e-111","1270997660933430103646587551458058129583150730468332767486572265625e-67"],["-198677693676611585179799836231584464485422358848154544830322265625e-67","57609320846059259091181091461120189478606334887444972991943359375e-66","1775254064703433294292463204451515412785056469147093594074249267578125e-67","1511820147641718622918476971741807207880306295993783934112160787230623145896814610300573633594467537477612495422e-111","960258630601225151717874972757501900456619206584656923989050339660338180547416442323083174414932727813720703125e-109","2440647682677040963607622677350874631388510252718515686492712612734090805608963559159008127608103677630424499512e-111","8465342011233619871896312517565779815734231039347538616153274912377665211739297124182712650508619844913482666016e-110","2026505099471559043755406829896063764118357882619362331215937805892309193141892498246647846826817840337753295898e-111","11831816824116907918028654932152132772671393468044698238372802734375e-65","21978402204792821893046018966177967968178563751280307769775390625e-66","-9864287940456069940876250511362369096925883042059576142516236410718659728842899053757875549308664631098508834839e-112","117420221879594306973841610908237953481147997081279754638671875e-63"]],"point_count":4,"point_major":true,"point_sha256s":["159a3337d66cad590f1248fa621d3f4f35332fdf4d0995d8ba70ab0f5c9f1508","7eb16f897bae1b5c22cd43a774924b49b010f6fb7258b31afd0f8466e1f1ca4a","2bcc3bc060de99925b5f3bd43f2a61e7f11cc081e39c95de695744af1692c471","c22ca25c310ae54fe59d75f21fdf0d15f6098c9c123cffafdb59b5afee38fc3a"],"points":[{"available":true,"error":null,"kind":"pyamplicol-rusticol-validation-momenta","points":[[{"momentum":["45.594000000000001","-19.238453227798498","8.8163655276172204","-40.385225668371007"],"pdg":1},{"momentum":["45.594000000000001","19.238453227798498","-8.8163655276172204","40.385225668371007"],"pdg":-1},{"momentum":["91.188000000000002","0","0","0"],"pdg":23}]],"process":"d d~ > z","process_id":"d_dbar_to_z","schema_version":1,"seed":5934198374352107017},{"available":true,"error":null,"kind":"pyamplicol-rusticol-validation-momenta","points":[[{"momentum":["45.594000000000001","-43.81788816339764","10.048872829830856","-7.6043190194694503"],"pdg":1},{"momentum":["45.594000000000001","43.81788816339764","-10.048872829830856","7.6043190194694503"],"pdg":-1},{"momentum":["91.188000000000002","0","0","0"],"pdg":23}]],"process":"d d~ > z","process_id":"d_dbar_to_z","schema_version":1,"seed":7090654850851803862},{"available":true,"error":null,"kind":"pyamplicol-rusticol-validation-momenta","points":[[{"momentum":["45.594000000000001","-3.5617290341759489","19.061639270670348","41.264765001171959"],"pdg":1},{"momentum":["45.594000000000001","3.5617290341759489","-19.061639270670348","-41.264765001171959"],"pdg":-1},{"momentum":["91.188000000000002","0","0","0"],"pdg":23}]],"process":"d d~ > z","process_id":"d_dbar_to_z","schema_version":1,"seed":5624516564888253598},{"available":true,"error":null,"kind":"pyamplicol-rusticol-validation-momenta","points":[[{"momentum":["45.594000000000001","9.9768452702989343","-21.600291839977629","38.893480004755425"],"pdg":1},{"momentum":["45.594000000000001","-9.9768452702989343","21.600291839977629","-38.893480004755425"],"pdg":-1},{"momentum":["91.188000000000002","0","0","0"],"pdg":23}]],"process":"d d~ > z","process_id":"d_dbar_to_z","schema_version":1,"seed":1523975234645041793}],"precision_digits":96,"runtime_schema_sha256":"e2ff47ba61f9bb1ac9d9ad57731238ed0734b7c088fc75de57d74446cdffda07","source_dag_sha256":"bdc021db2a2392e9ef7cb0e18d7a030f17203de9f67ba2ca59611cf6f1cd1326"},"certified_relation_count":0,"discovery":{"candidate_index":{"algorithm":"complete-contract-anchor-tolerance-window-v1","completeness":"complete-within-configured-tolerance","contract_count":13,"exhaustive_fallback_contract_count":0,"nearest_rejected_scope":"zero-and-tolerance-window-screened-hypotheses","screened_pair_hypothesis_count":0,"theoretical_pair_hypothesis_count":0,"zero_hypothesis_count":6},"certificates":[],"certified_numerical_relation_count":0,"inspected_current_count":13,"nearest_rejected_hypothesis":{"current_id":9,"maximum_absolute_residual":"1164392548371285446027469689761581223628934989881795114030898221715273124934654529090207931656910454026508559578e-111","maximum_relative_residual":"1e0","maximum_tolerance_ratio":"9999999999141183142361652196748286659932854983968146242634831872161993352649924186297596743770544361841046167790e-42","relation_kind":"zero","representative_id":null},"numerical_candidate_count":0,"probe_contract":{"absolute_tolerance_binary64":"0x1.2f8ac174d6123p-266","algorithm":"authenticated-independent-recursive-decimal-probes-v1","candidate_capture_sha256":"9a9407d1c969921dab470127ef2b1b57d566d6f5a983a913cf769b519e81df28","candidate_kinematic_sha256s":["07e4764a3e34af43f874738e5f1726cc6d45c1317bdabcf6dea30904aa7f991a","70cd9a12303937f454b0c30763aa465692698b281d0a4385a3f2f6357510253b","64b49558828caaedb30476a6de774e99a9b48635bacb4343815901eded648b85","570330aa5081a5326a51192fc9def5fcb0aaf97a59481b2e03728449f3bf4674"],"candidate_observation_batch_sha256":"a1f271189335460a687c51a32564d8c413e5d26d419de788023c7624485fa421","candidate_parameter_context_sha256s":["7de3097f42e8a88f7c0fc730051918a7241b969722aa9a7cde918ee30a31bc48","798756bb3e1b110e6a1106fe7f8bdfb564095b1bba24c9a75ae1f959c55172d0","e96b72f59471a7c2585673d46f0bf1abd14e9550f0d4698d4c3369846fa7ce54","2d75c06a7a780edfd3553773aa87e6a42a7be8c36bafdb52b6960f004c0bc713"],"candidate_point_sha256s":["159a3337d66cad590f1248fa621d3f4f35332fdf4d0995d8ba70ab0f5c9f1508","7eb16f897bae1b5c22cd43a774924b49b010f6fb7258b31afd0f8466e1f1ca4a","2bcc3bc060de99925b5f3bd43f2a61e7f11cc081e39c95de695744af1692c471","c22ca25c310ae54fe59d75f21fdf0d15f6098c9c123cffafdb59b5afee38fc3a"],"deterministic":true,"independent_verification":true,"precision_digits":96,"relative_tolerance_binary64":"0x1.615e91d8f359dp-233","seed":1348026701,"verification_capture_sha256":"8ce4ac32cfedaefcbe01591c7a598e49a726b09d313b222248274eb7a94a93bf","verification_kinematic_sha256s":["c31ed6cc961f2a4a241e11711a35b830cc738d555c8e2fc7df00153e0dab3016","b4975d446bde8d6ba5bd12051c7767f1879b754d6e8b2aa492a899a6670b066c","5659e201917daf51114242cee2c73657dba48562762481a9efd8b10627743a36","f5a236b7f8fada514b6063f2dbcc63550f61048a9cc38aeef4a478ba58a4113d"],"verification_observation_batch_sha256":"cc27ba094f29ef2a37a6dc0ca8bdbe7fb9346b53936abad43b8be8b1720dd8bc","verification_parameter_context_sha256s":["8dda4f6392efe352d109ca46ce8f83ed689704d28d89d3c9df321e3f3be0e0fd","5179d9a73795cd5654ab34d720df261807d8ded5ea3a9aebe8c7196c9b701532","53554302860cbb36666f4db3c356be5ab4d8710ea0ebf76d7788bfcd60d0cc2f","76ebd8a6f280948ac619addce844a809e7706fbcf045777db7305eeef1ca8fbb"],"verification_point_sha256s":["38ed0b50de6095b7253d13e8981effe0dfffd4e713e4607112e4e3ecd2fa7cf9","4023248b796ba4b7bc20e64b6b5105aec8dbb8e24e574d6de0941c60a9c9da63","a9921723991888cfd7763f5e2d72dac750e0a48a1cc129ea98e4508dc1bf0757","d528614e20be9132c5681278e00726318484fd834a4a66f2ebbe7bf56d6c2471"]},"rejected_candidate_diagnostics":{"full_census_sha256":"e42bed471be043845d809a9b3b35fa5db88bd3581de89a7febb534fb70e37e8a","full_rejection_sha256":"344c32dc4f15362dc0a54b3a47d636e8160ce4849d84877d02652cab428be888","retained_count":6,"retained_sha256":"2716afbf79e1f11f1a82a46e01ffef60c99988653e889988145fc3b575790fa5","total_rejected_hypothesis_count":6,"truncated":false,"truncation_policy":"first-16-in-canonical-discovery-order-v1"},"rejected_candidates":[{"current_id":7,"maximum_absolute_residual":"1652235580118060607749261511176162534946511739379000450523047197337260455353682209680093564910839189023743200020e-111","maximum_relative_residual":"1e0","maximum_tolerance_ratio":"9999999999394759475969713566174655839364276451377038033568776217365162501060346088283174954243130258240751242537e-42","reason":"candidate-observations-not-equal","relation_kind":"zero","representative_id":null},{"current_id":8,"maximum_absolute_residual":"1168306982819169892451327541493810713291168212890625e-51","maximum_relative_residual":"1e0","maximum_tolerance_ratio":"9999999999144060624488151038517879690206808288796491042162793088938906758252406308380154985900508100600493984810e-42","reason":"candidate-observations-not-equal","relation_kind":"zero","representative_id":null},{"current_id":9,"maximum_absolute_residual":"1164392548371285446027469689761581223628934989881795114030898221715273124934654529090207931656910454026508559578e-111","maximum_relative_residual":"1e0","maximum_tolerance_ratio":"9999999999141183142361652196748286659932854983968146242634831872161993352649924186297596743770544361841046167790e-42","reason":"candidate-observations-not-equal","relation_kind":"zero","representative_id":null},{"current_id":10,"maximum_absolute_residual":"9695657984828618214780757810531834293463132347640931891757681861689768023207352297861870262983611296475618346152e-111","maximum_relative_residual":"1e0","maximum_tolerance_ratio":"9999999999896861091702298446687246774373803108151076260350345999523037547941496937054637321589883566856300078061e-42","reason":"candidate-observations-not-equal","relation_kind":"zero","representative_id":null},{"current_id":11,"maximum_absolute_residual":"68558655091378124524226223002187907695770263671875e-49","maximum_relative_residual":"1e0","maximum_tolerance_ratio":"9999999999854139539125781613658798877168975239601146772161282520238656184075944459478761896242213207914868027014e-42","reason":"candidate-observations-not-equal","relation_kind":"zero","representative_id":null},{"current_id":12,"maximum_absolute_residual":"9499137011509613433159712876250745750736940258969428366405225306572731102076613349371086233989171343803722964041e-111","maximum_relative_residual":"1e0","maximum_tolerance_ratio":"9999999999894727322377979320922707021146156027133230720620324960978969629429517351723925264292341389756253692312e-42","reason":"candidate-observations-not-equal","relation_kind":"zero","representative_id":null}],"rejected_hypothesis_count":6,"runtime_schema_sha256":"e2ff47ba61f9bb1ac9d9ad57731238ed0734b7c088fc75de57d74446cdffda07","schema_version":1,"scope":{"color_accuracy":"lc","execution_mode":"compiled","representation":"generic-dag"},"source_dag_sha256":"bdc021db2a2392e9ef7cb0e18d7a030f17203de9f67ba2ca59611cf6f1cd1326","source_semantics":{"abi":"pyamplicol-generic-dag-numerical-current-source-v1","sha256":"4794f99ccbc88dea1a22b767b7f8e0e3424eff81bc2731a56304ab1f191692fe"},"state":"no_certified_numerical_relation","structurally_proven_current_count":0,"tested_hypothesis_count":6,"verification_rejected_count":0,"warning":{"reason":"discovery alone never warns; the artifact aggregator warns once only when certified proof-less mappings are actually applied","required":false}},"effective_mode":"certified-reuse","effective_reuse_state":"enabled","relation_correctness":{"abi":"pyamplicol-numerical-current-relation-correctness-v1","applied_relation_count":0,"state":"no-applied-relations"},"requested_mode":"certified-reuse","schema_version":1,"scope":{"color_accuracy":"lc","execution_mode":"compiled","representation":"generic-dag"},"source_semantics":{"sha256":"4794f99ccbc88dea1a22b767b7f8e0e3424eff81bc2731a56304ab1f191692fe"},"state":"no_certified_numerical_relation","verification_capture":{"abi":"pyamplicol-generic-dag-current-observation-capture-v2","capture_contract_sha256":"8ce4ac32cfedaefcbe01591c7a598e49a726b09d313b222248274eb7a94a93bf","complete_current_component_digest":true,"components_embedded":false,"current_count":13,"evaluator":"symbolica-interpreted-high-precision-stage-replay","evaluator_output_partition":{"abi":"pyamplicol-generic-dag-current-id-output-partitions-v1","sha256":"91787d155d08a95fdad9fd74653fe688b91501a66209d22788363d1143e102fb"},"kinematic_sha256s":["c31ed6cc961f2a4a241e11711a35b830cc738d555c8e2fc7df00153e0dab3016","b4975d446bde8d6ba5bd12051c7767f1879b754d6e8b2aa492a899a6670b066c","5659e201917daf51114242cee2c73657dba48562762481a9efd8b10627743a36","f5a236b7f8fada514b6063f2dbcc63550f61048a9cc38aeef4a478ba58a4113d"],"model_parameter_schema_sha256":"44b9ce16b482f552b778174bed4355f47e6836ce0057d6f8649c2c5242ec2d4d","observation_batch_sha256":"cc27ba094f29ef2a37a6dc0ca8bdbe7fb9346b53936abad43b8be8b1720dd8bc","parameter_context_sha256s":["8dda4f6392efe352d109ca46ce8f83ed689704d28d89d3c9df321e3f3be0e0fd","5179d9a73795cd5654ab34d720df261807d8ded5ea3a9aebe8c7196c9b701532","53554302860cbb36666f4db3c356be5ab4d8710ea0ebf76d7788bfcd60d0cc2f","76ebd8a6f280948ac619addce844a809e7706fbcf045777db7305eeef1ca8fbb"],"parameter_contexts":[["-428743123839055681839030516322441854981661890633404254913330078125e-67","1229337671608740661891999608468228899482710403390228748321533203125e-67","1814639669513577216059053488252106323130874443450011312961578369140625e-67","1473671772981664783759571700717208013445460371498901712225635926635762647447010914580900475812086369842290878296e-111","8843125580122167002844104223045051953458192202676716725019566046137861397777313321455494588008150458335876464844e-110","2289441304993553825072887073985273072170840029234440951548546443206770741745931935229307896406680811196565628052e-111","8456073069005570746018969754733054419972008814063024396454775721011697868327583460512641977402381598949432373047e-110","2068602749199903732969189208280149108381521728426447693180137255794031212667409558303432959291967563331127166748e-111","1279266708316745219044968141786089432798689813353121280670166015625e-64","524333998498528991750468851440469819635836756788194179534912109375e-67","-1085929727281062708451575873809950624815601737563875183896125805926657086509113520067515423761506099253892898560e-111","1522854766045041089556587310394064616048126481473445892333984375e-64"],["-35418375322064492101881323649070765213764389045536518096923828125e-67","1660962210334748333031315396801641526280945981852710247039794921875e-67","17493592069728094710409529825845265094130809302441775798797607421875e-65","1530406092065955264673871307483923964621295097973176123710813060086357152798225422785094451683107763528823852539e-111","8906689954329377606370498684149517754779489336041931892357142477629163208364959647411751575418747961521148681641e-110","2580121239147156642219500040447588748961658131580312754416048594926021891914156430880566972518863622099161148071e-111","7734081846978071728573140569593726170375753456828630580649800857247352318252220371164185053203254938125610351562e-110","2015701399593413225619641804985017050659171719898402855246566655294162954438486057284762864583171904087066650391e-111","12700791754025640589611904907041406431744690053164958953857421875e-62","-264681122030299864446366113479580661760337534360587596893310546875e-67","-1060901414344373237434382915390280660417560973951806154604697837167962419692305089946593454897083574905991554260e-111","1338982902624709452643375802438630017832110752351582050323486328125e-67"],["-336272653953548561611285212802346933358421665616333484649658203125e-67","88586745445987840038627215977218298803563811816275119781494140625e-66","162846372680865255608016251877290248017970952787436544895172119140625e-66","1492340508937200786725469244048655715224167723830991532060027760483190030217333713058458499745029257610440254211e-111","9421268372781718216318104960461624046206706349307729337504427790325281532636536585556541467667557299137115478516e-110","2483305896079634061934368964301559209718570351082119579049858724800376483070329935998188375378958880901336669922e-111","7983858962816643230338749771824556398846028995714598277998142041976214447647486327497290403698571026325225830078e-110","2113660793170497975583220687988284652552932576582990698851978087427923416784583676708564325963379815220832824707e-111","1324304162192006783244831191227730560200370746315456926822662353515625e-67","-259913796597456512934708368345315676606332999654114246368408203125e-67","-9685953671008229505428711647131589734155372223015304768985735886181764535505112762026058703668240923434495925903e-112","20033115843573731009049330997751070526646799407899379730224609375e-65"],["250919662272214291775661483552273267605414730496704578399658203125e-67","1615809963278920216423850501896719578098782221786677837371826171875e-67","17558079431371018799414138351899783430098977987654507160186767578125e-65","1513783766555074105242574941698622142531153479225013798943917579008455705425655393736583675945439608767628669739e-111","9061637160718448622289697114359255838654422885908568502452253909498135165875853935801842453656718134880065917969e-110","2355706548278581027945285432992524857842153847738050148868578662600358575123209864399242974286607932299375534058e-111","8333521853907423237813651698536875392301829639782776542224265016481447041277186293939394090557470917701721191406e-110","2030340797976007219626355200275548987320739533553519716663044757842158737721691141864255314430920407176017761230e-111","1192779619339804226174202804976187675123355802497826516628265380859375e-67","3364632371368749970386678727063411997733055613934993743896484375e-66","-1065393925959148812223570530257695392403323799708863122130061030802530443710321797490259143614821368828415870666e-111","16553785603523037510951577966356040860773646272718906402587890625e-65"]],"point_count":4,"point_major":true,"point_sha256s":["38ed0b50de6095b7253d13e8981effe0dfffd4e713e4607112e4e3ecd2fa7cf9","4023248b796ba4b7bc20e64b6b5105aec8dbb8e24e574d6de0941c60a9c9da63","a9921723991888cfd7763f5e2d72dac750e0a48a1cc129ea98e4508dc1bf0757","d528614e20be9132c5681278e00726318484fd834a4a66f2ebbe7bf56d6c2471"],"points":[{"available":true,"error":null,"kind":"pyamplicol-rusticol-validation-momenta","points":[[{"momentum":["45.594000000000001","-22.698385413478491","25.794624041395899","-29.97054397544742"],"pdg":1},{"momentum":["45.594000000000001","22.698385413478491","-25.794624041395899","29.97054397544742"],"pdg":-1},{"momentum":["91.188000000000002","0","0","0"],"pdg":23}]],"process":"d d~ > z","process_id":"d_dbar_to_z","schema_version":1,"seed":8804252587108628234},{"available":true,"error":null,"kind":"pyamplicol-rusticol-validation-momenta","points":[[{"momentum":["45.594000000000001","-27.64422199849648","26.414157231586692","24.837514445750397"],"pdg":1},{"momentum":["45.594000000000001","27.64422199849648","-26.414157231586692","-24.837514445750397"],"pdg":-1},{"momentum":["91.188000000000002","0","0","0"],"pdg":23}]],"process":"d d~ > z","process_id":"d_dbar_to_z","schema_version":1,"seed":8157271186662746954},{"available":true,"error":null,"kind":"pyamplicol-rusticol-validation-momenta","points":[[{"momentum":["45.594000000000001","-7.6309323217086416","26.256974175185977","-36.48496971446076"],"pdg":1},{"momentum":["45.594000000000001","7.6309323217086416","-26.256974175185977","36.48496971446076"],"pdg":-1},{"momentum":["91.188000000000002","0","0","0"],"pdg":23}]],"process":"d d~ > z","process_id":"d_dbar_to_z","schema_version":1,"seed":3763731385338703666},{"available":true,"error":null,"kind":"pyamplicol-rusticol-validation-momenta","points":[[{"momentum":["45.594000000000001","-40.50878035289923","17.470481847616863","11.516675489595725"],"pdg":1},{"momentum":["45.594000000000001","40.50878035289923","-17.470481847616863","-11.516675489595725"],"pdg":-1},{"momentum":["91.188000000000002","0","0","0"],"pdg":23}]],"process":"d d~ > z","process_id":"d_dbar_to_z","schema_version":1,"seed":5222851728213758142}],"precision_digits":96,"runtime_schema_sha256":"e2ff47ba61f9bb1ac9d9ad57731238ed0734b7c088fc75de57d74446cdffda07","source_dag_sha256":"bdc021db2a2392e9ef7cb0e18d7a030f17203de9f67ba2ca59611cf6f1cd1326"},"warning":{"code":null,"emit":"never","message":null,"required":false}},"primary":{"abi":"pyamplicol-generic-dag-numerical-current-warmup-v1","application":{"abi":"pyamplicol-authenticated-numerical-current-relation-set-v1","applied_relation_count":0,"candidate_capture_sha256":"bcaa65582e991b4a76f8de27ecbaefe690b19ff897485b7c43f2af5ede917dd8","certificate_replay":{"algorithm":"authenticated-independent-recursive-decimal-probes-v1","certificate_set_sha256":"f723a545b9a6993645136423ad45e32094a551327fabf88878a71fef3894783c","status":"no_certified_numerical_relation"},"certificates":[],"certified_relation_count":0,"interaction_evaluation_count_before":2,"interaction_evaluation_count_projected":2,"interaction_evaluation_savings_projected":0,"mappings":[],"relation_kind_counts":{"equal":0,"opposite":0,"zero":0},"requested_mode":"certified-reuse","runtime_schema_sha256":"5489f282ed0ac3fb29ca65a609f7b3a3dcc12d0b631a69a9959182c74ea58be4","schema_version":1,"scope":{"color_accuracy":"lc","execution_mode":"compiled","representation":"generic-dag"},"source_dag_sha256":"b98dd714a832e270769a03580c60dba91db15d6d0198b9cebbca0223072cc2d2","source_semantics":{"abi":"pyamplicol-generic-dag-numerical-current-source-v1","sha256":"e8323adb262a224d3dc35de452f9d139861670bd44715348e16b52578950b127"},"state":"no_certified_numerical_relation","verification_capture_sha256":"eb94dbc941b9dbf25d623f3966178198efe93ee7d16f6fc4eebfa87ad7669c13","warning":{"code":null,"emit":"never","message":null,"required":false}},"application_capture":null,"application_scope":{"abi":"pyamplicol-generic-dag-numerical-relation-application-scope-v1","allowed_relation_kinds":["equal","opposite","zero"],"application_eligible_relation_count":0,"discovered_relation_count":0,"policy":"exact-physical-selector-member-set-v1","reason":"complete-generic-dag","scope_suppressed_relation_count":0},"application_validation":{"checked_component_count":0,"checked_current_count":0,"maximum_absolute_residual":null,"maximum_relative_residual":null,"maximum_tolerance_ratio":null,"status":"not-required-no-applied-relations"},"applied_relation_count":0,"candidate_capture":{"abi":"pyamplicol-generic-dag-current-observation-capture-v2","capture_contract_sha256":"bcaa65582e991b4a76f8de27ecbaefe690b19ff897485b7c43f2af5ede917dd8","complete_current_component_digest":true,"components_embedded":false,"current_count":7,"evaluator":"symbolica-interpreted-high-precision-stage-replay","evaluator_output_partition":{"abi":"pyamplicol-generic-dag-current-id-output-partitions-v1","sha256":"bffc25fd5ab44df7a300465635437af32a2484147204a7942d4c7e1dcb899e06"},"kinematic_sha256s":["07e4764a3e34af43f874738e5f1726cc6d45c1317bdabcf6dea30904aa7f991a","70cd9a12303937f454b0c30763aa465692698b281d0a4385a3f2f6357510253b","64b49558828caaedb30476a6de774e99a9b48635bacb4343815901eded648b85","570330aa5081a5326a51192fc9def5fcb0aaf97a59481b2e03728449f3bf4674"],"model_parameter_schema_sha256":"44b9ce16b482f552b778174bed4355f47e6836ce0057d6f8649c2c5242ec2d4d","observation_batch_sha256":"e2bd8fb3beb6d7a4518d1413f0af42540b9daa23a78fedc4fd0c202556ba8646","parameter_context_sha256s":["7de3097f42e8a88f7c0fc730051918a7241b969722aa9a7cde918ee30a31bc48","798756bb3e1b110e6a1106fe7f8bdfb564095b1bba24c9a75ae1f959c55172d0","e96b72f59471a7c2585673d46f0bf1abd14e9550f0d4698d4c3369846fa7ce54","2d75c06a7a780edfd3553773aa87e6a42a7be8c36bafdb52b6960f004c0bc713"],"parameter_contexts":[["7546771113978883484263437964045806438662111759185791015625e-60","11799999999999999378275106209912337362766265869140625e-53","173e0","14915000000000000479616346638067625463008880615234375e-52","911880000000000023874235921539366245269775390625e-46","2441403999999999907544179222895763814449310302734375e-51","804190024457561634108060388825833797454833984375e-46","204760000000000008668621376273222267627716064453125e-50","125e0","6382338999999999894219282481344635016284883022308349609375e-60","-10244420275940371301004461201955564320087432861328125e-52","1781866674528745642103189084082259796559810638427734375e-55"],["-134847569752588090791494031261965602652708184905350208282470703125e-67","594236468801730715431365890955017761143608368001878261566162109375e-67","180017912417472714819833656645087227587964662234298884868621826171875e-66","1418607520800001666640541972417661876671966282909820125025755491178502191556556541462974507794569944962859153748e-111","8845655440625352648857741117815739972876412900659526159953506315626337854970961949163665849482640624046325683594e-110","2440844827436804634318075561842410785553470886133209208296951315132136921118907041394763268726819660514593124390e-111","7663581892294273545448908358214496202797406717397960807541474497626878947115214302243657584767788648605346679688e-110","1969240825499461802211304919239819353753191159531314854075138421568181711258926913643563239020295441150665283203e-111","1325364572440657479309700206814015555067953755497001111507415771484375e-67","6282212318214615756465681040143778091078274883329868316650390625e-65","-1072072896851752300436215264505413093610426376587095133168104624067673550947285901480121594886441016569733619690e-111","124566021831671000418754850780356235873114201240241527557373046875e-66"],["655617805825475631221880591648076830324498587287962436676025390625e-67","9508743447340736756304320487220849145160173065960407257080078125e-65","1833800730995227375994240863292528143091431047650985419750213623046875e-67","1417917885253575031749239105231976588110775334943781102058718916214643546711612204225261280043923761695623397827e-111","9296679062386854901358783194859654609329226514583805008942638117660874140339710702818365462007932364940643310547e-110","2577944736949078494160337607692592781342881499602461238125442612439294946878425018876868080042186193168163299560e-111","7878882507835004983969788421954676388128613320152368876411796051959239746420582406472021830268204212188720703125e-110","2169217507358018313945853497769126166281712195718656383954739160729475598750337628217721430701203644275665283203e-111","1252367052285700050356484123136036057388764675124548375606536865234375e-67","309195150609194360628341480268232999151223339140415191650390625e-64","-1058333689713571520504797215545769574180750731277403150472198582723563643195538452879134183604037389159202575684e-111","1270997660933430103646587551458058129583150730468332767486572265625e-67"],["-198677693676611585179799836231584464485422358848154544830322265625e-67","57609320846059259091181091461120189478606334887444972991943359375e-66","1775254064703433294292463204451515412785056469147093594074249267578125e-67","1511820147641718622918476971741807207880306295993783934112160787230623145896814610300573633594467537477612495422e-111","960258630601225151717874972757501900456619206584656923989050339660338180547416442323083174414932727813720703125e-109","2440647682677040963607622677350874631388510252718515686492712612734090805608963559159008127608103677630424499512e-111","8465342011233619871896312517565779815734231039347538616153274912377665211739297124182712650508619844913482666016e-110","2026505099471559043755406829896063764118357882619362331215937805892309193141892498246647846826817840337753295898e-111","11831816824116907918028654932152132772671393468044698238372802734375e-65","21978402204792821893046018966177967968178563751280307769775390625e-66","-9864287940456069940876250511362369096925883042059576142516236410718659728842899053757875549308664631098508834839e-112","117420221879594306973841610908237953481147997081279754638671875e-63"]],"point_count":4,"point_major":true,"point_sha256s":["159a3337d66cad590f1248fa621d3f4f35332fdf4d0995d8ba70ab0f5c9f1508","7eb16f897bae1b5c22cd43a774924b49b010f6fb7258b31afd0f8466e1f1ca4a","2bcc3bc060de99925b5f3bd43f2a61e7f11cc081e39c95de695744af1692c471","c22ca25c310ae54fe59d75f21fdf0d15f6098c9c123cffafdb59b5afee38fc3a"],"points":[{"available":true,"error":null,"kind":"pyamplicol-rusticol-validation-momenta","points":[[{"momentum":["45.594000000000001","-19.238453227798498","8.8163655276172204","-40.385225668371007"],"pdg":1},{"momentum":["45.594000000000001","19.238453227798498","-8.8163655276172204","40.385225668371007"],"pdg":-1},{"momentum":["91.188000000000002","0","0","0"],"pdg":23}]],"process":"d d~ > z","process_id":"d_dbar_to_z","schema_version":1,"seed":5934198374352107017},{"available":true,"error":null,"kind":"pyamplicol-rusticol-validation-momenta","points":[[{"momentum":["45.594000000000001","-43.81788816339764","10.048872829830856","-7.6043190194694503"],"pdg":1},{"momentum":["45.594000000000001","43.81788816339764","-10.048872829830856","7.6043190194694503"],"pdg":-1},{"momentum":["91.188000000000002","0","0","0"],"pdg":23}]],"process":"d d~ > z","process_id":"d_dbar_to_z","schema_version":1,"seed":7090654850851803862},{"available":true,"error":null,"kind":"pyamplicol-rusticol-validation-momenta","points":[[{"momentum":["45.594000000000001","-3.5617290341759489","19.061639270670348","41.264765001171959"],"pdg":1},{"momentum":["45.594000000000001","3.5617290341759489","-19.061639270670348","-41.264765001171959"],"pdg":-1},{"momentum":["91.188000000000002","0","0","0"],"pdg":23}]],"process":"d d~ > z","process_id":"d_dbar_to_z","schema_version":1,"seed":5624516564888253598},{"available":true,"error":null,"kind":"pyamplicol-rusticol-validation-momenta","points":[[{"momentum":["45.594000000000001","9.9768452702989343","-21.600291839977629","38.893480004755425"],"pdg":1},{"momentum":["45.594000000000001","-9.9768452702989343","21.600291839977629","-38.893480004755425"],"pdg":-1},{"momentum":["91.188000000000002","0","0","0"],"pdg":23}]],"process":"d d~ > z","process_id":"d_dbar_to_z","schema_version":1,"seed":1523975234645041793}],"precision_digits":96,"runtime_schema_sha256":"5489f282ed0ac3fb29ca65a609f7b3a3dcc12d0b631a69a9959182c74ea58be4","source_dag_sha256":"b98dd714a832e270769a03580c60dba91db15d6d0198b9cebbca0223072cc2d2"},"certified_relation_count":0,"discovery":{"candidate_index":{"algorithm":"complete-contract-anchor-tolerance-window-v1","completeness":"complete-within-configured-tolerance","contract_count":7,"exhaustive_fallback_contract_count":0,"nearest_rejected_scope":"zero-and-tolerance-window-screened-hypotheses","screened_pair_hypothesis_count":0,"theoretical_pair_hypothesis_count":0,"zero_hypothesis_count":2},"certificates":[],"certified_numerical_relation_count":0,"inspected_current_count":7,"nearest_rejected_hypothesis":{"current_id":5,"maximum_absolute_residual":"1652235580118060607749261511176162534946511739379000450523047197337260455353682209680093564910839189023743200020e-111","maximum_relative_residual":"1e0","maximum_tolerance_ratio":"9999999999394759475969713566174655839364276451377038033568776217365162501060346088283174954243130258240751242537e-42","relation_kind":"zero","representative_id":null},"numerical_candidate_count":0,"probe_contract":{"absolute_tolerance_binary64":"0x1.2f8ac174d6123p-266","algorithm":"authenticated-independent-recursive-decimal-probes-v1","candidate_capture_sha256":"bcaa65582e991b4a76f8de27ecbaefe690b19ff897485b7c43f2af5ede917dd8","candidate_kinematic_sha256s":["07e4764a3e34af43f874738e5f1726cc6d45c1317bdabcf6dea30904aa7f991a","70cd9a12303937f454b0c30763aa465692698b281d0a4385a3f2f6357510253b","64b49558828caaedb30476a6de774e99a9b48635bacb4343815901eded648b85","570330aa5081a5326a51192fc9def5fcb0aaf97a59481b2e03728449f3bf4674"],"candidate_observation_batch_sha256":"e2bd8fb3beb6d7a4518d1413f0af42540b9daa23a78fedc4fd0c202556ba8646","candidate_parameter_context_sha256s":["7de3097f42e8a88f7c0fc730051918a7241b969722aa9a7cde918ee30a31bc48","798756bb3e1b110e6a1106fe7f8bdfb564095b1bba24c9a75ae1f959c55172d0","e96b72f59471a7c2585673d46f0bf1abd14e9550f0d4698d4c3369846fa7ce54","2d75c06a7a780edfd3553773aa87e6a42a7be8c36bafdb52b6960f004c0bc713"],"candidate_point_sha256s":["159a3337d66cad590f1248fa621d3f4f35332fdf4d0995d8ba70ab0f5c9f1508","7eb16f897bae1b5c22cd43a774924b49b010f6fb7258b31afd0f8466e1f1ca4a","2bcc3bc060de99925b5f3bd43f2a61e7f11cc081e39c95de695744af1692c471","c22ca25c310ae54fe59d75f21fdf0d15f6098c9c123cffafdb59b5afee38fc3a"],"deterministic":true,"independent_verification":true,"precision_digits":96,"relative_tolerance_binary64":"0x1.615e91d8f359dp-233","seed":1348026701,"verification_capture_sha256":"eb94dbc941b9dbf25d623f3966178198efe93ee7d16f6fc4eebfa87ad7669c13","verification_kinematic_sha256s":["c31ed6cc961f2a4a241e11711a35b830cc738d555c8e2fc7df00153e0dab3016","b4975d446bde8d6ba5bd12051c7767f1879b754d6e8b2aa492a899a6670b066c","5659e201917daf51114242cee2c73657dba48562762481a9efd8b10627743a36","f5a236b7f8fada514b6063f2dbcc63550f61048a9cc38aeef4a478ba58a4113d"],"verification_observation_batch_sha256":"443741c28cb19a946b9189f072a4ed604989d9b73ca885051dc61a51ea096f5f","verification_parameter_context_sha256s":["8dda4f6392efe352d109ca46ce8f83ed689704d28d89d3c9df321e3f3be0e0fd","5179d9a73795cd5654ab34d720df261807d8ded5ea3a9aebe8c7196c9b701532","53554302860cbb36666f4db3c356be5ab4d8710ea0ebf76d7788bfcd60d0cc2f","76ebd8a6f280948ac619addce844a809e7706fbcf045777db7305eeef1ca8fbb"],"verification_point_sha256s":["38ed0b50de6095b7253d13e8981effe0dfffd4e713e4607112e4e3ecd2fa7cf9","4023248b796ba4b7bc20e64b6b5105aec8dbb8e24e574d6de0941c60a9c9da63","a9921723991888cfd7763f5e2d72dac750e0a48a1cc129ea98e4508dc1bf0757","d528614e20be9132c5681278e00726318484fd834a4a66f2ebbe7bf56d6c2471"]},"rejected_candidate_diagnostics":{"full_census_sha256":"789c7ce83848301f8d9c0b84b47dfcf74bf99d42e54580c6ca97f354c7041fd3","full_rejection_sha256":"04ed9fe54992a29b1867ed1134679975d002be41baabddb6bfee516c6a9c1689","retained_count":2,"retained_sha256":"963064c684d2f3d565c58d9375bb19561d0571b31121eebfd58b0652474e8f54","total_rejected_hypothesis_count":2,"truncated":false,"truncation_policy":"first-16-in-canonical-discovery-order-v1"},"rejected_candidates":[{"current_id":5,"maximum_absolute_residual":"1652235580118060607749261511176162534946511739379000450523047197337260455353682209680093564910839189023743200020e-111","maximum_relative_residual":"1e0","maximum_tolerance_ratio":"9999999999394759475969713566174655839364276451377038033568776217365162501060346088283174954243130258240751242537e-42","reason":"candidate-observations-not-equal","relation_kind":"zero","representative_id":null},{"current_id":6,"maximum_absolute_residual":"9695657984828618214780757810531834293463132347640931891757681861689768023207352297861870262983611296475618346152e-111","maximum_relative_residual":"1e0","maximum_tolerance_ratio":"9999999999896861091702298446687246774373803108151076260350345999523037547941496937054637321589883566856300078061e-42","reason":"candidate-observations-not-equal","relation_kind":"zero","representative_id":null}],"rejected_hypothesis_count":2,"runtime_schema_sha256":"5489f282ed0ac3fb29ca65a609f7b3a3dcc12d0b631a69a9959182c74ea58be4","schema_version":1,"scope":{"color_accuracy":"lc","execution_mode":"compiled","representation":"generic-dag"},"source_dag_sha256":"b98dd714a832e270769a03580c60dba91db15d6d0198b9cebbca0223072cc2d2","source_semantics":{"abi":"pyamplicol-generic-dag-numerical-current-source-v1","sha256":"e8323adb262a224d3dc35de452f9d139861670bd44715348e16b52578950b127"},"state":"no_certified_numerical_relation","structurally_proven_current_count":0,"tested_hypothesis_count":2,"verification_rejected_count":0,"warning":{"reason":"discovery alone never warns; the artifact aggregator warns once only when certified proof-less mappings are actually applied","required":false}},"effective_mode":"certified-reuse","effective_reuse_state":"enabled","relation_correctness":{"abi":"pyamplicol-numerical-current-relation-correctness-v1","applied_relation_count":0,"state":"no-applied-relations"},"requested_mode":"certified-reuse","schema_version":1,"scope":{"color_accuracy":"lc","execution_mode":"compiled","representation":"generic-dag"},"source_semantics":{"sha256":"e8323adb262a224d3dc35de452f9d139861670bd44715348e16b52578950b127"},"state":"no_certified_numerical_relation","verification_capture":{"abi":"pyamplicol-generic-dag-current-observation-capture-v2","capture_contract_sha256":"eb94dbc941b9dbf25d623f3966178198efe93ee7d16f6fc4eebfa87ad7669c13","complete_current_component_digest":true,"components_embedded":false,"current_count":7,"evaluator":"symbolica-interpreted-high-precision-stage-replay","evaluator_output_partition":{"abi":"pyamplicol-generic-dag-current-id-output-partitions-v1","sha256":"bffc25fd5ab44df7a300465635437af32a2484147204a7942d4c7e1dcb899e06"},"kinematic_sha256s":["c31ed6cc961f2a4a241e11711a35b830cc738d555c8e2fc7df00153e0dab3016","b4975d446bde8d6ba5bd12051c7767f1879b754d6e8b2aa492a899a6670b066c","5659e201917daf51114242cee2c73657dba48562762481a9efd8b10627743a36","f5a236b7f8fada514b6063f2dbcc63550f61048a9cc38aeef4a478ba58a4113d"],"model_parameter_schema_sha256":"44b9ce16b482f552b778174bed4355f47e6836ce0057d6f8649c2c5242ec2d4d","observation_batch_sha256":"443741c28cb19a946b9189f072a4ed604989d9b73ca885051dc61a51ea096f5f","parameter_context_sha256s":["8dda4f6392efe352d109ca46ce8f83ed689704d28d89d3c9df321e3f3be0e0fd","5179d9a73795cd5654ab34d720df261807d8ded5ea3a9aebe8c7196c9b701532","53554302860cbb36666f4db3c356be5ab4d8710ea0ebf76d7788bfcd60d0cc2f","76ebd8a6f280948ac619addce844a809e7706fbcf045777db7305eeef1ca8fbb"],"parameter_contexts":[["-428743123839055681839030516322441854981661890633404254913330078125e-67","1229337671608740661891999608468228899482710403390228748321533203125e-67","1814639669513577216059053488252106323130874443450011312961578369140625e-67","1473671772981664783759571700717208013445460371498901712225635926635762647447010914580900475812086369842290878296e-111","8843125580122167002844104223045051953458192202676716725019566046137861397777313321455494588008150458335876464844e-110","2289441304993553825072887073985273072170840029234440951548546443206770741745931935229307896406680811196565628052e-111","8456073069005570746018969754733054419972008814063024396454775721011697868327583460512641977402381598949432373047e-110","2068602749199903732969189208280149108381521728426447693180137255794031212667409558303432959291967563331127166748e-111","1279266708316745219044968141786089432798689813353121280670166015625e-64","524333998498528991750468851440469819635836756788194179534912109375e-67","-1085929727281062708451575873809950624815601737563875183896125805926657086509113520067515423761506099253892898560e-111","1522854766045041089556587310394064616048126481473445892333984375e-64"],["-35418375322064492101881323649070765213764389045536518096923828125e-67","1660962210334748333031315396801641526280945981852710247039794921875e-67","17493592069728094710409529825845265094130809302441775798797607421875e-65","1530406092065955264673871307483923964621295097973176123710813060086357152798225422785094451683107763528823852539e-111","8906689954329377606370498684149517754779489336041931892357142477629163208364959647411751575418747961521148681641e-110","2580121239147156642219500040447588748961658131580312754416048594926021891914156430880566972518863622099161148071e-111","7734081846978071728573140569593726170375753456828630580649800857247352318252220371164185053203254938125610351562e-110","2015701399593413225619641804985017050659171719898402855246566655294162954438486057284762864583171904087066650391e-111","12700791754025640589611904907041406431744690053164958953857421875e-62","-264681122030299864446366113479580661760337534360587596893310546875e-67","-1060901414344373237434382915390280660417560973951806154604697837167962419692305089946593454897083574905991554260e-111","1338982902624709452643375802438630017832110752351582050323486328125e-67"],["-336272653953548561611285212802346933358421665616333484649658203125e-67","88586745445987840038627215977218298803563811816275119781494140625e-66","162846372680865255608016251877290248017970952787436544895172119140625e-66","1492340508937200786725469244048655715224167723830991532060027760483190030217333713058458499745029257610440254211e-111","9421268372781718216318104960461624046206706349307729337504427790325281532636536585556541467667557299137115478516e-110","2483305896079634061934368964301559209718570351082119579049858724800376483070329935998188375378958880901336669922e-111","7983858962816643230338749771824556398846028995714598277998142041976214447647486327497290403698571026325225830078e-110","2113660793170497975583220687988284652552932576582990698851978087427923416784583676708564325963379815220832824707e-111","1324304162192006783244831191227730560200370746315456926822662353515625e-67","-259913796597456512934708368345315676606332999654114246368408203125e-67","-9685953671008229505428711647131589734155372223015304768985735886181764535505112762026058703668240923434495925903e-112","20033115843573731009049330997751070526646799407899379730224609375e-65"],["250919662272214291775661483552273267605414730496704578399658203125e-67","1615809963278920216423850501896719578098782221786677837371826171875e-67","17558079431371018799414138351899783430098977987654507160186767578125e-65","1513783766555074105242574941698622142531153479225013798943917579008455705425655393736583675945439608767628669739e-111","9061637160718448622289697114359255838654422885908568502452253909498135165875853935801842453656718134880065917969e-110","2355706548278581027945285432992524857842153847738050148868578662600358575123209864399242974286607932299375534058e-111","8333521853907423237813651698536875392301829639782776542224265016481447041277186293939394090557470917701721191406e-110","2030340797976007219626355200275548987320739533553519716663044757842158737721691141864255314430920407176017761230e-111","1192779619339804226174202804976187675123355802497826516628265380859375e-67","3364632371368749970386678727063411997733055613934993743896484375e-66","-1065393925959148812223570530257695392403323799708863122130061030802530443710321797490259143614821368828415870666e-111","16553785603523037510951577966356040860773646272718906402587890625e-65"]],"point_count":4,"point_major":true,"point_sha256s":["38ed0b50de6095b7253d13e8981effe0dfffd4e713e4607112e4e3ecd2fa7cf9","4023248b796ba4b7bc20e64b6b5105aec8dbb8e24e574d6de0941c60a9c9da63","a9921723991888cfd7763f5e2d72dac750e0a48a1cc129ea98e4508dc1bf0757","d528614e20be9132c5681278e00726318484fd834a4a66f2ebbe7bf56d6c2471"],"points":[{"available":true,"error":null,"kind":"pyamplicol-rusticol-validation-momenta","points":[[{"momentum":["45.594000000000001","-22.698385413478491","25.794624041395899","-29.97054397544742"],"pdg":1},{"momentum":["45.594000000000001","22.698385413478491","-25.794624041395899","29.97054397544742"],"pdg":-1},{"momentum":["91.188000000000002","0","0","0"],"pdg":23}]],"process":"d d~ > z","process_id":"d_dbar_to_z","schema_version":1,"seed":8804252587108628234},{"available":true,"error":null,"kind":"pyamplicol-rusticol-validation-momenta","points":[[{"momentum":["45.594000000000001","-27.64422199849648","26.414157231586692","24.837514445750397"],"pdg":1},{"momentum":["45.594000000000001","27.64422199849648","-26.414157231586692","-24.837514445750397"],"pdg":-1},{"momentum":["91.188000000000002","0","0","0"],"pdg":23}]],"process":"d d~ > z","process_id":"d_dbar_to_z","schema_version":1,"seed":8157271186662746954},{"available":true,"error":null,"kind":"pyamplicol-rusticol-validation-momenta","points":[[{"momentum":["45.594000000000001","-7.6309323217086416","26.256974175185977","-36.48496971446076"],"pdg":1},{"momentum":["45.594000000000001","7.6309323217086416","-26.256974175185977","36.48496971446076"],"pdg":-1},{"momentum":["91.188000000000002","0","0","0"],"pdg":23}]],"process":"d d~ > z","process_id":"d_dbar_to_z","schema_version":1,"seed":3763731385338703666},{"available":true,"error":null,"kind":"pyamplicol-rusticol-validation-momenta","points":[[{"momentum":["45.594000000000001","-40.50878035289923","17.470481847616863","11.516675489595725"],"pdg":1},{"momentum":["45.594000000000001","40.50878035289923","-17.470481847616863","-11.516675489595725"],"pdg":-1},{"momentum":["91.188000000000002","0","0","0"],"pdg":23}]],"process":"d d~ > z","process_id":"d_dbar_to_z","schema_version":1,"seed":5222851728213758142}],"precision_digits":96,"runtime_schema_sha256":"5489f282ed0ac3fb29ca65a609f7b3a3dcc12d0b631a69a9959182c74ea58be4","source_dag_sha256":"b98dd714a832e270769a03580c60dba91db15d6d0198b9cebbca0223072cc2d2"},"warning":{"code":null,"emit":"never","message":null,"required":false}}},"opt_out_flag":"--no-numerical-current-reuse","relation_correctness":{"abi":"pyamplicol-numerical-current-relation-correctness-v1","applied_relation_count":0,"state":"no-applied-relations"},"requested_mode":"certified-reuse","schema_version":1,"structural_exact_discovery":null,"warning":{"code":null,"emit":"never","message":null,"required":false}},"structural_helicity_reduction":{"after_amplitude_roots":2,"applied":true,"before_amplitude_roots":6,"mode":"proven global-helicity-flip equivalence"}},"helicity_selector_runtime_schema_sha256s":["4dd26bae3d23f4e6de0249aa8fb8534e08c130c1425a2d20a076f9f6ddf3f44c","7a919ac899d90294670b45c8e4ca7c2e1f03552d8c8247af337b910c6b7144a1"],"helicity_sum_runtime_schema_sha256":"e2ff47ba61f9bb1ac9d9ad57731238ed0734b7c088fc75de57d74446cdffda07","id":"d_dbar_to_z","runtime_schema_sha256":"5489f282ed0ac3fb29ca65a609f7b3a3dcc12d0b631a69a9959182c74ea58be4","validation_momenta_path":"processes/d_dbar_to_z/validation-momenta.json"}],"phase_timings_seconds":{"dag":0.0015193750150501728,"evaluator-construction":0.0053629581816494465,"jit":0.4948723749257624,"model-loading":0.06822012504562736,"process-expansion":0.0011749169789254665,"warmup-filter":0.02937237499281764},"schema_version":1}},"kind":"pyamplicol-process","model":{"compiled_schema_version":9,"content_sha256":"db50b0cb34998c1a9f7bf63c2b7a4d80c01f373a00d9bee81372b2d43a506962","name":"built-in-sm","restriction":null,"source_kind":"built-in-sm"},"payloads":[{"executable":false,"media_type":"text/x-makefile","path":"API/c/Makefile","role":"api-build-file","sha256":"732ce49a9d3001664e38a4ef4254eb4a2e60d7c09860d27861b559618abc30c5","size_bytes":1300},{"executable":false,"media_type":"text/x-csrc","path":"API/c/check_standalone.c","role":"api-source","sha256":"a8a32263c2a1db282d15711ebf8fbb68b45fb05af317c18302726b021b862337","size_bytes":27726},{"executable":false,"media_type":"text/x-makefile","path":"API/cpp/Makefile","role":"api-build-file","sha256":"4d14b139fd6ae56345951656ff9a46c478433e4f99eaf039643dd1defdb3d4c3","size_bytes":1315},{"executable":false,"media_type":"text/x-c++src","path":"API/cpp/check_standalone.cpp","role":"api-source","sha256":"35d42c32d6e046f533040f5158ba039a9736fd540d690f71ce8509ef788d8fd2","size_bytes":11822},{"executable":false,"media_type":"text/x-makefile","path":"API/fortran/Makefile","role":"api-build-file","sha256":"264c677c1accb48b00eea59c6787a1a7550541897b0a96b2cffeeb6ba4d42427","size_bytes":1482},{"executable":false,"media_type":"text/x-fortran","path":"API/fortran/check_standalone.f90","role":"api-source","sha256":"530ddeafafd7980fd3e7eafae1149f5a2ce759f3937e9c67751108a7edd7d394","size_bytes":15533},{"executable":true,"media_type":"text/x-python","path":"API/python/check_standalone.py","role":"api-source","sha256":"a1b1c7900737fdf5d1b392e0dd9d048156c49269e3ab3654aca32e348080b490","size_bytes":14172},{"executable":false,"media_type":"text/x-makefile","path":"API/rust/Makefile","role":"api-build-file","sha256":"9116f9eea3414e024107c57043d8898b316abafbe9f8757e5dca3e9878b110d8","size_bytes":1653},{"executable":false,"media_type":"text/x-rust","path":"API/rust/check_standalone.rs","role":"api-source","sha256":"6010ce4ab7f7443a490b12d9a2fcfe99c94ce00246fd9675cec0bad84cbebb2f","size_bytes":15278},{"executable":false,"media_type":"text/tab-separated-values","path":"API/validation_points.dat","role":"validation-momenta","sha256":"aced8cb80350c33759d1f40fcaee8a70af13a1a4214bc7cd4653a6c5d0d7211f","size_bytes":154},{"executable":false,"media_type":"application/toml","path":"config/effective.toml","role":"configuration-effective","sha256":"92efe66607e964cddbfd527bc78c2d3b9c11ba96630d24fca007f73361b86835","size_bytes":1764},{"executable":false,"media_type":"application/toml","path":"config/requested.toml","role":"configuration-requested","sha256":"2001e198cade9cdca3a6f3bebc3c99f3349f7f5b95152ea322410299e926820d","size_bytes":1772},{"executable":false,"media_type":"application/octet-stream","path":"evaluators.pacbin","role":"evaluator-state","sha256":"c213ab3d9e4a562765b1d2f22454747f46a8641f5aa6b07e68f3cb5f077ce805","size_bytes":16000,"target":{"cpu_features":[],"triple":"portable-64le"}},{"executable":false,"media_type":"application/json","path":"model/compiled-model.json","role":"compiled-model","sha256":"895f42c74958c37e91c6a281166a6bd6e720c78b5d57b071624548e6b424316f","size_bytes":230932},{"executable":false,"media_type":"application/json","path":"model/parameters.json","role":"model-parameters","sha256":"e0bc97dba08061be379f09c58ed03bc78c3429be6a5796b8ee39355730e15e86","size_bytes":610},{"executable":false,"media_type":"application/json","path":"processes/d_dbar_to_z/execution.json","process_id":"d_dbar_to_z","role":"evaluator-manifest","sha256":"5d6eff063088c6194f2f2ff163ed6859f6a43b6489a3d7aa466dc2861ba81f18","size_bytes":266668},{"executable":false,"media_type":"application/json","path":"processes/d_dbar_to_z/physics.json","process_id":"d_dbar_to_z","role":"runtime-physics","sha256":"de34d694c7b149e94676f2e9e362ad4b46e20d320133e1197eba82bdb2d3180e","size_bytes":5819},{"executable":false,"media_type":"application/json","path":"processes/d_dbar_to_z/validation-momenta.json","process_id":"d_dbar_to_z","role":"validation-momenta","sha256":"9a552a52ce11ab5d1fbe56c7a87f2fe7face06eb51ca562777efff58b87a9f0a","size_bytes":685},{"executable":false,"media_type":"application/json","path":"processes/evaluators.json","role":"evaluator-manifest","sha256":"c41035da94339a62092ea0ede7349ca3145edade1087f1a8100e43d72affb052","size_bytes":839}],"processes":[{"aliases":[],"color_accuracy":"lc","expression":"d d~ > z","external_pdgs":[1,-1,23],"id":"d_dbar_to_z","physics_path":"processes/d_dbar_to_z/physics.json","required_runtime_capabilities":["compiled-plane-arena-v1","rusticol.compiled.color-topology-lanes.v1","rusticol.compiled.helicity-dual-lane.v1","rusticol.compiled.helicity-selector-union.v1","rusticol.compiled.runtime-selectors.v1","symjit.application.complex-f64.v1"]}],"producer":{"distribution":"pyamplicol","native_build_inputs_sha256":"4ab41fd55da0d2e7cccd1e964b3b087a5bb277fda1c668d1b9848c31cfdcda54","target":{"cpu_features":[],"triple":"portable-64le"},"version":"1.0.0","versions":{"c_abi":1,"compiled_model":9,"process_artifact":3,"python_api":1,"runtime_physics":1,"symbolica_serialization":"symbolica-bincode2-v1","toml":1}},"runtime":{"api_bundle_path":"API","engine":"rusticol","engine_version":"1.0.0","evaluator_manifest_path":"processes/evaluators.json","required_runtime_capabilities":["compiled-plane-arena-v1","rusticol.compiled.color-topology-lanes.v1","rusticol.compiled.helicity-dual-lane.v1","rusticol.compiled.helicity-selector-union.v1","rusticol.compiled.runtime-selectors.v1","symjit.application.complex-f64.v1"]},"schema_version":3} diff --git a/pyamplicol/src/pyamplicol/assets/selftest/portable-64le/artifact/config/effective.toml b/pyamplicol/src/pyamplicol/assets/selftest/portable-64le/artifact/config/effective.toml new file mode 100644 index 0000000000..7e17a4edbe --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/selftest/portable-64le/artifact/config/effective.toml @@ -0,0 +1,104 @@ +action = "generate" +schema_version = 1 + +[model] +source = "built-in-sm" +simplify = true +cache = true + +[process] +entries = [{ expression = "d d~ > z" }] +flavor_scheme = 5 +coupling_order_policy = "minimal" +reference_color_order = [] +selected_color_sector_ids = [] + +[process.multiparticles] + +[process.max_coupling_orders] + +[process.selected_source_helicities] + +[color] +accuracy = "lc" +contraction = "direct" +lc_flow_layout = "topology-replay" +fft_basis = "trace" + +[generation] +output = "." +mode = "error" +workers = 1 +emit_api_bundle = true + +[generation.validation] +enabled = true +samples = 1 +seed = 12345 +relative_tolerance = 1e-12 +absolute_tolerance = 1e-300 +post_build_validation = false + +[generation.relation_discovery] +mode = "certified-reuse" +precision_digits = 96 +probe_count = 4 +verification_probe_count = 4 +relative_tolerance = 1e-70 +absolute_tolerance = 1e-80 +seed = 1348026701 + +[evaluator] +backend = "jit" +execution_mode = "compiled" +batch_size = 128 +output_chunk_size = 512 + +[evaluator.optimization] +horner_iterations = 10 +cores = 12 +max_horner_variables = 1000 +max_common_pair_cache_entries = 5000000 +max_common_pair_distance = 1000 +collect_factors = "auto" + +[evaluator.jit] +optimization_level = 2 +compress = true + +[evaluator.cpp] +optimization = "O3" +native_arch = false +extra_flags = [] + +[evaluator.eager] +point_tile_size = 1024 +workspace_mib = 256 + +[evaluator.recurrence] +point_tile_size = 1024 +workspace_mib = 256 + +[evaluation] +precision = 16 +resolved = false +helicity_ids = [] +color_flow_ids = [] + +[benchmark] +target_runtime = 10.0 +batch_size = 128 +precision = 16 +warmup_runs = 2 +minimum_samples = 5 +helicity_ids = [] +color_flow_ids = [] + +[output] +format = "json" +color = "auto" +progress = "off" +log_level = "info" + +[symbolica] +suggest_license = false diff --git a/pyamplicol/src/pyamplicol/assets/selftest/portable-64le/artifact/config/requested.toml b/pyamplicol/src/pyamplicol/assets/selftest/portable-64le/artifact/config/requested.toml new file mode 100644 index 0000000000..d5f7e35274 --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/selftest/portable-64le/artifact/config/requested.toml @@ -0,0 +1,104 @@ +action = "generate" +schema_version = 1 + +[model] +source = "built-in-sm" +simplify = true +cache = true + +[process] +entries = [{ expression = "d d~ > z" }] +flavor_scheme = 5 +coupling_order_policy = "minimal" +reference_color_order = [] +selected_color_sector_ids = [] + +[process.multiparticles] + +[process.max_coupling_orders] + +[process.selected_source_helicities] + +[color] +accuracy = "lc" +contraction = "direct" +lc_flow_layout = "topology-replay" +fft_basis = "adjoint" + +[generation] +output = "." +mode = "error" +workers = 1 +emit_api_bundle = true + +[generation.validation] +enabled = true +samples = 1 +seed = 12345 +relative_tolerance = 1e-12 +absolute_tolerance = 1e-300 +post_build_validation = false + +[generation.relation_discovery] +mode = "certified-reuse" +precision_digits = 96 +probe_count = 4 +verification_probe_count = 4 +relative_tolerance = 1e-70 +absolute_tolerance = 1e-80 +seed = 1348026701 + +[evaluator] +backend = "jit" +execution_mode = "compiled" +batch_size = 128 +output_chunk_size = 512 + +[evaluator.optimization] +horner_iterations = 10 +cores = "auto" +max_horner_variables = 1000 +max_common_pair_cache_entries = 5000000 +max_common_pair_distance = 1000 +collect_factors = "auto" + +[evaluator.jit] +optimization_level = 2 +compress = "auto" + +[evaluator.cpp] +optimization = "O3" +native_arch = false +extra_flags = [] + +[evaluator.eager] +point_tile_size = 1024 +workspace_mib = 256 + +[evaluator.recurrence] +point_tile_size = 1024 +workspace_mib = 256 + +[evaluation] +precision = 16 +resolved = false +helicity_ids = [] +color_flow_ids = [] + +[benchmark] +target_runtime = 10.0 +batch_size = 128 +precision = 16 +warmup_runs = 2 +minimum_samples = 5 +helicity_ids = [] +color_flow_ids = [] + +[output] +format = "json" +color = "auto" +progress = "off" +log_level = "info" + +[symbolica] +suggest_license = false diff --git a/pyamplicol/src/pyamplicol/assets/selftest/portable-64le/artifact/evaluators.pacbin b/pyamplicol/src/pyamplicol/assets/selftest/portable-64le/artifact/evaluators.pacbin new file mode 100644 index 0000000000..5405414e6f Binary files /dev/null and b/pyamplicol/src/pyamplicol/assets/selftest/portable-64le/artifact/evaluators.pacbin differ diff --git a/pyamplicol/src/pyamplicol/assets/selftest/portable-64le/artifact/model/compiled-model.json b/pyamplicol/src/pyamplicol/assets/selftest/portable-64le/artifact/model/compiled-model.json new file mode 100644 index 0000000000..0668f393c7 --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/selftest/portable-64le/artifact/model/compiled-model.json @@ -0,0 +1 @@ +{"capabilities":{"color_accuracy_modes":["lc","nlc","full"],"color_representations":[-3,1,3,8],"compiled_contact_term_count":0,"compiled_propagator_count":0,"compiled_vertex_term_count":211,"declared_functions":[],"exchange_symmetric_kernel_evaluation_relation_count":0,"expression_functions":[],"form_factor_count":0,"has_custom_propagators":false,"max_vertex_valence":3,"parameter_count":10,"particle_count":38,"reusable_kernel_evaluation_class_count":0,"signed_kernel_evaluation_relation_count":0,"spins":[-1,1,2,3],"supported":true,"swapped_kernel_evaluation_relation_count":0,"unlowered_contact_term_count":0,"verified_kernel_evaluation_class_count":0,"vertex_count":211},"conversion_seconds":0.010124958120286465,"ir":{"closure_contractions":[{"chirality":0,"contraction_ir":{"chirality_relation":"any","coefficients":[[1.0,0.0]],"left_basis":"scalar","metric_signature":null,"name":"scalar","right_basis":"scalar"},"particle":"h"},{"chirality":0,"contraction_ir":{"chirality_relation":"any","coefficients":[[1.0,0.0]],"left_basis":"scalar","metric_signature":null,"name":"scalar","right_basis":"scalar"},"particle":"pdg_125"},{"chirality":0,"contraction_ir":{"chirality_relation":"any","coefficients":[[1.0,0.0]],"left_basis":"scalar","metric_signature":null,"name":"scalar","right_basis":"scalar"},"particle":"pdg_126"},{"chirality":0,"contraction_ir":{"chirality_relation":"any","coefficients":[[1.0,0.0]],"left_basis":"scalar","metric_signature":null,"name":"scalar","right_basis":"scalar"},"particle":"pdg_127"}],"couplings":[],"current_orderings":[],"direct_contractions":[{"contraction_ir":{"chirality_relation":"any","coefficients":[[1.0,0.0],[1.0,0.0],[1.0,0.0],[1.0,0.0],[1.0,0.0],[1.0,0.0]],"left_basis":"auxiliary:antisymmetric-tensor","metric_signature":null,"name":"antisymmetric-tensor","right_basis":"auxiliary:antisymmetric-tensor"},"left_chirality":0,"left_particle":"pdg_-26","right_chirality":0,"right_particle":"pdg_26"},{"contraction_ir":{"chirality_relation":"any","coefficients":[[1.0,0.0],[-1.0,0.0],[-1.0,0.0],[-1.0,0.0]],"left_basis":"lorentz-vector","metric_signature":"mostly-minus","name":"lorentz","right_basis":"lorentz-vector"},"left_chirality":0,"left_particle":"w-","right_chirality":0,"right_particle":"w+"},{"contraction_ir":{"chirality_relation":"any","coefficients":[[1.0,0.0],[1.0,0.0],[1.0,0.0],[1.0,0.0],[1.0,0.0],[1.0,0.0]],"left_basis":"auxiliary:antisymmetric-tensor","metric_signature":null,"name":"antisymmetric-tensor","right_basis":"auxiliary:antisymmetric-tensor"},"left_chirality":0,"left_particle":"pdg_-23","right_chirality":0,"right_particle":"pdg_-23"},{"contraction_ir":{"chirality_relation":"any","coefficients":[[1.0,0.0],[1.0,0.0],[1.0,0.0],[1.0,0.0],[1.0,0.0],[1.0,0.0]],"left_basis":"auxiliary:antisymmetric-tensor","metric_signature":null,"name":"antisymmetric-tensor","right_basis":"auxiliary:antisymmetric-tensor"},"left_chirality":0,"left_particle":"pdg_-21","right_chirality":0,"right_particle":"pdg_-21"},{"contraction_ir":{"chirality_relation":"opposite","coefficients":[[1.0,0.0],[1.0,0.0]],"left_basis":"weyl-chiral","metric_signature":null,"name":"weyl","right_basis":"weyl-chiral"},"left_chirality":-1,"left_particle":"vt~","right_chirality":1,"right_particle":"vt"},{"contraction_ir":{"chirality_relation":"any","coefficients":[[1.0,0.0],[1.0,0.0],[1.0,0.0],[1.0,0.0]],"left_basis":"dirac","metric_signature":null,"name":"dirac","right_basis":"dirac"},"left_chirality":0,"left_particle":"vt~","right_chirality":0,"right_particle":"vt"},{"contraction_ir":{"chirality_relation":"opposite","coefficients":[[1.0,0.0],[1.0,0.0]],"left_basis":"weyl-chiral","metric_signature":null,"name":"weyl","right_basis":"weyl-chiral"},"left_chirality":1,"left_particle":"vt~","right_chirality":-1,"right_particle":"vt"},{"contraction_ir":{"chirality_relation":"opposite","coefficients":[[1.0,0.0],[1.0,0.0]],"left_basis":"weyl-chiral","metric_signature":null,"name":"weyl","right_basis":"weyl-chiral"},"left_chirality":-1,"left_particle":"ta+","right_chirality":1,"right_particle":"ta-"},{"contraction_ir":{"chirality_relation":"any","coefficients":[[1.0,0.0],[1.0,0.0],[1.0,0.0],[1.0,0.0]],"left_basis":"dirac","metric_signature":null,"name":"dirac","right_basis":"dirac"},"left_chirality":0,"left_particle":"ta+","right_chirality":0,"right_particle":"ta-"},{"contraction_ir":{"chirality_relation":"opposite","coefficients":[[1.0,0.0],[1.0,0.0]],"left_basis":"weyl-chiral","metric_signature":null,"name":"weyl","right_basis":"weyl-chiral"},"left_chirality":1,"left_particle":"ta+","right_chirality":-1,"right_particle":"ta-"},{"contraction_ir":{"chirality_relation":"opposite","coefficients":[[1.0,0.0],[1.0,0.0]],"left_basis":"weyl-chiral","metric_signature":null,"name":"weyl","right_basis":"weyl-chiral"},"left_chirality":-1,"left_particle":"vm~","right_chirality":1,"right_particle":"vm"},{"contraction_ir":{"chirality_relation":"any","coefficients":[[1.0,0.0],[1.0,0.0],[1.0,0.0],[1.0,0.0]],"left_basis":"dirac","metric_signature":null,"name":"dirac","right_basis":"dirac"},"left_chirality":0,"left_particle":"vm~","right_chirality":0,"right_particle":"vm"},{"contraction_ir":{"chirality_relation":"opposite","coefficients":[[1.0,0.0],[1.0,0.0]],"left_basis":"weyl-chiral","metric_signature":null,"name":"weyl","right_basis":"weyl-chiral"},"left_chirality":1,"left_particle":"vm~","right_chirality":-1,"right_particle":"vm"},{"contraction_ir":{"chirality_relation":"opposite","coefficients":[[1.0,0.0],[1.0,0.0]],"left_basis":"weyl-chiral","metric_signature":null,"name":"weyl","right_basis":"weyl-chiral"},"left_chirality":-1,"left_particle":"mu+","right_chirality":1,"right_particle":"mu-"},{"contraction_ir":{"chirality_relation":"any","coefficients":[[1.0,0.0],[1.0,0.0],[1.0,0.0],[1.0,0.0]],"left_basis":"dirac","metric_signature":null,"name":"dirac","right_basis":"dirac"},"left_chirality":0,"left_particle":"mu+","right_chirality":0,"right_particle":"mu-"},{"contraction_ir":{"chirality_relation":"opposite","coefficients":[[1.0,0.0],[1.0,0.0]],"left_basis":"weyl-chiral","metric_signature":null,"name":"weyl","right_basis":"weyl-chiral"},"left_chirality":1,"left_particle":"mu+","right_chirality":-1,"right_particle":"mu-"},{"contraction_ir":{"chirality_relation":"opposite","coefficients":[[1.0,0.0],[1.0,0.0]],"left_basis":"weyl-chiral","metric_signature":null,"name":"weyl","right_basis":"weyl-chiral"},"left_chirality":-1,"left_particle":"ve~","right_chirality":1,"right_particle":"ve"},{"contraction_ir":{"chirality_relation":"any","coefficients":[[1.0,0.0],[1.0,0.0],[1.0,0.0],[1.0,0.0]],"left_basis":"dirac","metric_signature":null,"name":"dirac","right_basis":"dirac"},"left_chirality":0,"left_particle":"ve~","right_chirality":0,"right_particle":"ve"},{"contraction_ir":{"chirality_relation":"opposite","coefficients":[[1.0,0.0],[1.0,0.0]],"left_basis":"weyl-chiral","metric_signature":null,"name":"weyl","right_basis":"weyl-chiral"},"left_chirality":1,"left_particle":"ve~","right_chirality":-1,"right_particle":"ve"},{"contraction_ir":{"chirality_relation":"opposite","coefficients":[[1.0,0.0],[1.0,0.0]],"left_basis":"weyl-chiral","metric_signature":null,"name":"weyl","right_basis":"weyl-chiral"},"left_chirality":-1,"left_particle":"e+","right_chirality":1,"right_particle":"e-"},{"contraction_ir":{"chirality_relation":"any","coefficients":[[1.0,0.0],[1.0,0.0],[1.0,0.0],[1.0,0.0]],"left_basis":"dirac","metric_signature":null,"name":"dirac","right_basis":"dirac"},"left_chirality":0,"left_particle":"e+","right_chirality":0,"right_particle":"e-"},{"contraction_ir":{"chirality_relation":"opposite","coefficients":[[1.0,0.0],[1.0,0.0]],"left_basis":"weyl-chiral","metric_signature":null,"name":"weyl","right_basis":"weyl-chiral"},"left_chirality":1,"left_particle":"e+","right_chirality":-1,"right_particle":"e-"},{"contraction_ir":{"chirality_relation":"any","coefficients":[[1.0,0.0],[1.0,0.0],[1.0,0.0],[1.0,0.0]],"left_basis":"dirac","metric_signature":null,"name":"dirac","right_basis":"dirac"},"left_chirality":0,"left_particle":"t~","right_chirality":0,"right_particle":"t"},{"contraction_ir":{"chirality_relation":"opposite","coefficients":[[1.0,0.0],[1.0,0.0]],"left_basis":"weyl-chiral","metric_signature":null,"name":"weyl","right_basis":"weyl-chiral"},"left_chirality":-1,"left_particle":"b~","right_chirality":1,"right_particle":"b"},{"contraction_ir":{"chirality_relation":"any","coefficients":[[1.0,0.0],[1.0,0.0],[1.0,0.0],[1.0,0.0]],"left_basis":"dirac","metric_signature":null,"name":"dirac","right_basis":"dirac"},"left_chirality":0,"left_particle":"b~","right_chirality":0,"right_particle":"b"},{"contraction_ir":{"chirality_relation":"opposite","coefficients":[[1.0,0.0],[1.0,0.0]],"left_basis":"weyl-chiral","metric_signature":null,"name":"weyl","right_basis":"weyl-chiral"},"left_chirality":1,"left_particle":"b~","right_chirality":-1,"right_particle":"b"},{"contraction_ir":{"chirality_relation":"opposite","coefficients":[[1.0,0.0],[1.0,0.0]],"left_basis":"weyl-chiral","metric_signature":null,"name":"weyl","right_basis":"weyl-chiral"},"left_chirality":-1,"left_particle":"c~","right_chirality":1,"right_particle":"c"},{"contraction_ir":{"chirality_relation":"any","coefficients":[[1.0,0.0],[1.0,0.0],[1.0,0.0],[1.0,0.0]],"left_basis":"dirac","metric_signature":null,"name":"dirac","right_basis":"dirac"},"left_chirality":0,"left_particle":"c~","right_chirality":0,"right_particle":"c"},{"contraction_ir":{"chirality_relation":"opposite","coefficients":[[1.0,0.0],[1.0,0.0]],"left_basis":"weyl-chiral","metric_signature":null,"name":"weyl","right_basis":"weyl-chiral"},"left_chirality":1,"left_particle":"c~","right_chirality":-1,"right_particle":"c"},{"contraction_ir":{"chirality_relation":"opposite","coefficients":[[1.0,0.0],[1.0,0.0]],"left_basis":"weyl-chiral","metric_signature":null,"name":"weyl","right_basis":"weyl-chiral"},"left_chirality":-1,"left_particle":"s~","right_chirality":1,"right_particle":"s"},{"contraction_ir":{"chirality_relation":"any","coefficients":[[1.0,0.0],[1.0,0.0],[1.0,0.0],[1.0,0.0]],"left_basis":"dirac","metric_signature":null,"name":"dirac","right_basis":"dirac"},"left_chirality":0,"left_particle":"s~","right_chirality":0,"right_particle":"s"},{"contraction_ir":{"chirality_relation":"opposite","coefficients":[[1.0,0.0],[1.0,0.0]],"left_basis":"weyl-chiral","metric_signature":null,"name":"weyl","right_basis":"weyl-chiral"},"left_chirality":1,"left_particle":"s~","right_chirality":-1,"right_particle":"s"},{"contraction_ir":{"chirality_relation":"opposite","coefficients":[[1.0,0.0],[1.0,0.0]],"left_basis":"weyl-chiral","metric_signature":null,"name":"weyl","right_basis":"weyl-chiral"},"left_chirality":-1,"left_particle":"u~","right_chirality":1,"right_particle":"u"},{"contraction_ir":{"chirality_relation":"any","coefficients":[[1.0,0.0],[1.0,0.0],[1.0,0.0],[1.0,0.0]],"left_basis":"dirac","metric_signature":null,"name":"dirac","right_basis":"dirac"},"left_chirality":0,"left_particle":"u~","right_chirality":0,"right_particle":"u"},{"contraction_ir":{"chirality_relation":"opposite","coefficients":[[1.0,0.0],[1.0,0.0]],"left_basis":"weyl-chiral","metric_signature":null,"name":"weyl","right_basis":"weyl-chiral"},"left_chirality":1,"left_particle":"u~","right_chirality":-1,"right_particle":"u"},{"contraction_ir":{"chirality_relation":"opposite","coefficients":[[1.0,0.0],[1.0,0.0]],"left_basis":"weyl-chiral","metric_signature":null,"name":"weyl","right_basis":"weyl-chiral"},"left_chirality":-1,"left_particle":"d~","right_chirality":1,"right_particle":"d"},{"contraction_ir":{"chirality_relation":"any","coefficients":[[1.0,0.0],[1.0,0.0],[1.0,0.0],[1.0,0.0]],"left_basis":"dirac","metric_signature":null,"name":"dirac","right_basis":"dirac"},"left_chirality":0,"left_particle":"d~","right_chirality":0,"right_particle":"d"},{"contraction_ir":{"chirality_relation":"opposite","coefficients":[[1.0,0.0],[1.0,0.0]],"left_basis":"weyl-chiral","metric_signature":null,"name":"weyl","right_basis":"weyl-chiral"},"left_chirality":1,"left_particle":"d~","right_chirality":-1,"right_particle":"d"},{"contraction_ir":{"chirality_relation":"opposite","coefficients":[[1.0,0.0],[1.0,0.0]],"left_basis":"weyl-chiral","metric_signature":null,"name":"weyl","right_basis":"weyl-chiral"},"left_chirality":-1,"left_particle":"d","right_chirality":1,"right_particle":"d~"},{"contraction_ir":{"chirality_relation":"any","coefficients":[[1.0,0.0],[1.0,0.0],[1.0,0.0],[1.0,0.0]],"left_basis":"dirac","metric_signature":null,"name":"dirac","right_basis":"dirac"},"left_chirality":0,"left_particle":"d","right_chirality":0,"right_particle":"d~"},{"contraction_ir":{"chirality_relation":"opposite","coefficients":[[1.0,0.0],[1.0,0.0]],"left_basis":"weyl-chiral","metric_signature":null,"name":"weyl","right_basis":"weyl-chiral"},"left_chirality":1,"left_particle":"d","right_chirality":-1,"right_particle":"d~"},{"contraction_ir":{"chirality_relation":"opposite","coefficients":[[1.0,0.0],[1.0,0.0]],"left_basis":"weyl-chiral","metric_signature":null,"name":"weyl","right_basis":"weyl-chiral"},"left_chirality":-1,"left_particle":"u","right_chirality":1,"right_particle":"u~"},{"contraction_ir":{"chirality_relation":"any","coefficients":[[1.0,0.0],[1.0,0.0],[1.0,0.0],[1.0,0.0]],"left_basis":"dirac","metric_signature":null,"name":"dirac","right_basis":"dirac"},"left_chirality":0,"left_particle":"u","right_chirality":0,"right_particle":"u~"},{"contraction_ir":{"chirality_relation":"opposite","coefficients":[[1.0,0.0],[1.0,0.0]],"left_basis":"weyl-chiral","metric_signature":null,"name":"weyl","right_basis":"weyl-chiral"},"left_chirality":1,"left_particle":"u","right_chirality":-1,"right_particle":"u~"},{"contraction_ir":{"chirality_relation":"opposite","coefficients":[[1.0,0.0],[1.0,0.0]],"left_basis":"weyl-chiral","metric_signature":null,"name":"weyl","right_basis":"weyl-chiral"},"left_chirality":-1,"left_particle":"s","right_chirality":1,"right_particle":"s~"},{"contraction_ir":{"chirality_relation":"any","coefficients":[[1.0,0.0],[1.0,0.0],[1.0,0.0],[1.0,0.0]],"left_basis":"dirac","metric_signature":null,"name":"dirac","right_basis":"dirac"},"left_chirality":0,"left_particle":"s","right_chirality":0,"right_particle":"s~"},{"contraction_ir":{"chirality_relation":"opposite","coefficients":[[1.0,0.0],[1.0,0.0]],"left_basis":"weyl-chiral","metric_signature":null,"name":"weyl","right_basis":"weyl-chiral"},"left_chirality":1,"left_particle":"s","right_chirality":-1,"right_particle":"s~"},{"contraction_ir":{"chirality_relation":"opposite","coefficients":[[1.0,0.0],[1.0,0.0]],"left_basis":"weyl-chiral","metric_signature":null,"name":"weyl","right_basis":"weyl-chiral"},"left_chirality":-1,"left_particle":"c","right_chirality":1,"right_particle":"c~"},{"contraction_ir":{"chirality_relation":"any","coefficients":[[1.0,0.0],[1.0,0.0],[1.0,0.0],[1.0,0.0]],"left_basis":"dirac","metric_signature":null,"name":"dirac","right_basis":"dirac"},"left_chirality":0,"left_particle":"c","right_chirality":0,"right_particle":"c~"},{"contraction_ir":{"chirality_relation":"opposite","coefficients":[[1.0,0.0],[1.0,0.0]],"left_basis":"weyl-chiral","metric_signature":null,"name":"weyl","right_basis":"weyl-chiral"},"left_chirality":1,"left_particle":"c","right_chirality":-1,"right_particle":"c~"},{"contraction_ir":{"chirality_relation":"opposite","coefficients":[[1.0,0.0],[1.0,0.0]],"left_basis":"weyl-chiral","metric_signature":null,"name":"weyl","right_basis":"weyl-chiral"},"left_chirality":-1,"left_particle":"b","right_chirality":1,"right_particle":"b~"},{"contraction_ir":{"chirality_relation":"any","coefficients":[[1.0,0.0],[1.0,0.0],[1.0,0.0],[1.0,0.0]],"left_basis":"dirac","metric_signature":null,"name":"dirac","right_basis":"dirac"},"left_chirality":0,"left_particle":"b","right_chirality":0,"right_particle":"b~"},{"contraction_ir":{"chirality_relation":"opposite","coefficients":[[1.0,0.0],[1.0,0.0]],"left_basis":"weyl-chiral","metric_signature":null,"name":"weyl","right_basis":"weyl-chiral"},"left_chirality":1,"left_particle":"b","right_chirality":-1,"right_particle":"b~"},{"contraction_ir":{"chirality_relation":"any","coefficients":[[1.0,0.0],[1.0,0.0],[1.0,0.0],[1.0,0.0]],"left_basis":"dirac","metric_signature":null,"name":"dirac","right_basis":"dirac"},"left_chirality":0,"left_particle":"t","right_chirality":0,"right_particle":"t~"},{"contraction_ir":{"chirality_relation":"opposite","coefficients":[[1.0,0.0],[1.0,0.0]],"left_basis":"weyl-chiral","metric_signature":null,"name":"weyl","right_basis":"weyl-chiral"},"left_chirality":-1,"left_particle":"e-","right_chirality":1,"right_particle":"e+"},{"contraction_ir":{"chirality_relation":"any","coefficients":[[1.0,0.0],[1.0,0.0],[1.0,0.0],[1.0,0.0]],"left_basis":"dirac","metric_signature":null,"name":"dirac","right_basis":"dirac"},"left_chirality":0,"left_particle":"e-","right_chirality":0,"right_particle":"e+"},{"contraction_ir":{"chirality_relation":"opposite","coefficients":[[1.0,0.0],[1.0,0.0]],"left_basis":"weyl-chiral","metric_signature":null,"name":"weyl","right_basis":"weyl-chiral"},"left_chirality":1,"left_particle":"e-","right_chirality":-1,"right_particle":"e+"},{"contraction_ir":{"chirality_relation":"opposite","coefficients":[[1.0,0.0],[1.0,0.0]],"left_basis":"weyl-chiral","metric_signature":null,"name":"weyl","right_basis":"weyl-chiral"},"left_chirality":-1,"left_particle":"ve","right_chirality":1,"right_particle":"ve~"},{"contraction_ir":{"chirality_relation":"any","coefficients":[[1.0,0.0],[1.0,0.0],[1.0,0.0],[1.0,0.0]],"left_basis":"dirac","metric_signature":null,"name":"dirac","right_basis":"dirac"},"left_chirality":0,"left_particle":"ve","right_chirality":0,"right_particle":"ve~"},{"contraction_ir":{"chirality_relation":"opposite","coefficients":[[1.0,0.0],[1.0,0.0]],"left_basis":"weyl-chiral","metric_signature":null,"name":"weyl","right_basis":"weyl-chiral"},"left_chirality":1,"left_particle":"ve","right_chirality":-1,"right_particle":"ve~"},{"contraction_ir":{"chirality_relation":"opposite","coefficients":[[1.0,0.0],[1.0,0.0]],"left_basis":"weyl-chiral","metric_signature":null,"name":"weyl","right_basis":"weyl-chiral"},"left_chirality":-1,"left_particle":"mu-","right_chirality":1,"right_particle":"mu+"},{"contraction_ir":{"chirality_relation":"any","coefficients":[[1.0,0.0],[1.0,0.0],[1.0,0.0],[1.0,0.0]],"left_basis":"dirac","metric_signature":null,"name":"dirac","right_basis":"dirac"},"left_chirality":0,"left_particle":"mu-","right_chirality":0,"right_particle":"mu+"},{"contraction_ir":{"chirality_relation":"opposite","coefficients":[[1.0,0.0],[1.0,0.0]],"left_basis":"weyl-chiral","metric_signature":null,"name":"weyl","right_basis":"weyl-chiral"},"left_chirality":1,"left_particle":"mu-","right_chirality":-1,"right_particle":"mu+"},{"contraction_ir":{"chirality_relation":"opposite","coefficients":[[1.0,0.0],[1.0,0.0]],"left_basis":"weyl-chiral","metric_signature":null,"name":"weyl","right_basis":"weyl-chiral"},"left_chirality":-1,"left_particle":"vm","right_chirality":1,"right_particle":"vm~"},{"contraction_ir":{"chirality_relation":"any","coefficients":[[1.0,0.0],[1.0,0.0],[1.0,0.0],[1.0,0.0]],"left_basis":"dirac","metric_signature":null,"name":"dirac","right_basis":"dirac"},"left_chirality":0,"left_particle":"vm","right_chirality":0,"right_particle":"vm~"},{"contraction_ir":{"chirality_relation":"opposite","coefficients":[[1.0,0.0],[1.0,0.0]],"left_basis":"weyl-chiral","metric_signature":null,"name":"weyl","right_basis":"weyl-chiral"},"left_chirality":1,"left_particle":"vm","right_chirality":-1,"right_particle":"vm~"},{"contraction_ir":{"chirality_relation":"opposite","coefficients":[[1.0,0.0],[1.0,0.0]],"left_basis":"weyl-chiral","metric_signature":null,"name":"weyl","right_basis":"weyl-chiral"},"left_chirality":-1,"left_particle":"ta-","right_chirality":1,"right_particle":"ta+"},{"contraction_ir":{"chirality_relation":"any","coefficients":[[1.0,0.0],[1.0,0.0],[1.0,0.0],[1.0,0.0]],"left_basis":"dirac","metric_signature":null,"name":"dirac","right_basis":"dirac"},"left_chirality":0,"left_particle":"ta-","right_chirality":0,"right_particle":"ta+"},{"contraction_ir":{"chirality_relation":"opposite","coefficients":[[1.0,0.0],[1.0,0.0]],"left_basis":"weyl-chiral","metric_signature":null,"name":"weyl","right_basis":"weyl-chiral"},"left_chirality":1,"left_particle":"ta-","right_chirality":-1,"right_particle":"ta+"},{"contraction_ir":{"chirality_relation":"opposite","coefficients":[[1.0,0.0],[1.0,0.0]],"left_basis":"weyl-chiral","metric_signature":null,"name":"weyl","right_basis":"weyl-chiral"},"left_chirality":-1,"left_particle":"vt","right_chirality":1,"right_particle":"vt~"},{"contraction_ir":{"chirality_relation":"any","coefficients":[[1.0,0.0],[1.0,0.0],[1.0,0.0],[1.0,0.0]],"left_basis":"dirac","metric_signature":null,"name":"dirac","right_basis":"dirac"},"left_chirality":0,"left_particle":"vt","right_chirality":0,"right_particle":"vt~"},{"contraction_ir":{"chirality_relation":"opposite","coefficients":[[1.0,0.0],[1.0,0.0]],"left_basis":"weyl-chiral","metric_signature":null,"name":"weyl","right_basis":"weyl-chiral"},"left_chirality":1,"left_particle":"vt","right_chirality":-1,"right_particle":"vt~"},{"contraction_ir":{"chirality_relation":"any","coefficients":[[1.0,0.0],[-1.0,0.0],[-1.0,0.0],[-1.0,0.0]],"left_basis":"lorentz-vector","metric_signature":"mostly-minus","name":"lorentz","right_basis":"lorentz-vector"},"left_chirality":0,"left_particle":"g","right_chirality":0,"right_particle":"g"},{"contraction_ir":{"chirality_relation":"any","coefficients":[[1.0,0.0],[-1.0,0.0],[-1.0,0.0],[-1.0,0.0]],"left_basis":"lorentz-vector","metric_signature":"mostly-minus","name":"lorentz","right_basis":"lorentz-vector"},"left_chirality":0,"left_particle":"a","right_chirality":0,"right_particle":"a"},{"contraction_ir":{"chirality_relation":"any","coefficients":[[1.0,0.0],[-1.0,0.0],[-1.0,0.0],[-1.0,0.0]],"left_basis":"lorentz-vector","metric_signature":"mostly-minus","name":"lorentz","right_basis":"lorentz-vector"},"left_chirality":0,"left_particle":"z","right_chirality":0,"right_particle":"z"},{"contraction_ir":{"chirality_relation":"any","coefficients":[[1.0,0.0],[-1.0,0.0],[-1.0,0.0],[-1.0,0.0]],"left_basis":"lorentz-vector","metric_signature":"mostly-minus","name":"lorentz","right_basis":"lorentz-vector"},"left_chirality":0,"left_particle":"w+","right_chirality":0,"right_particle":"w-"},{"contraction_ir":{"chirality_relation":"any","coefficients":[[1.0,0.0]],"left_basis":"scalar","metric_signature":null,"name":"scalar","right_basis":"scalar"},"left_chirality":0,"left_particle":"h","right_chirality":0,"right_particle":"h"},{"contraction_ir":{"chirality_relation":"any","coefficients":[[1.0,0.0],[1.0,0.0],[1.0,0.0],[1.0,0.0],[1.0,0.0],[1.0,0.0]],"left_basis":"auxiliary:antisymmetric-tensor","metric_signature":null,"name":"antisymmetric-tensor","right_basis":"auxiliary:antisymmetric-tensor"},"left_chirality":0,"left_particle":"pdg_26","right_chirality":0,"right_particle":"pdg_-26"},{"contraction_ir":{"chirality_relation":"any","coefficients":[[1.0,0.0],[-1.0,0.0],[-1.0,0.0],[-1.0,0.0]],"left_basis":"auxiliary:u1-subtraction-color-flow-vector","metric_signature":"mostly-minus","name":"lorentz","right_basis":"auxiliary:u1-subtraction-color-flow-vector"},"left_chirality":0,"left_particle":"pdg_99","right_chirality":0,"right_particle":"pdg_99"},{"contraction_ir":{"chirality_relation":"any","coefficients":[[1.0,0.0]],"left_basis":"scalar","metric_signature":null,"name":"scalar","right_basis":"scalar"},"left_chirality":0,"left_particle":"pdg_125","right_chirality":0,"right_particle":"pdg_125"},{"contraction_ir":{"chirality_relation":"any","coefficients":[[1.0,0.0]],"left_basis":"scalar","metric_signature":null,"name":"scalar","right_basis":"scalar"},"left_chirality":0,"left_particle":"pdg_126","right_chirality":0,"right_particle":"pdg_126"},{"contraction_ir":{"chirality_relation":"any","coefficients":[[1.0,0.0]],"left_basis":"scalar","metric_signature":null,"name":"scalar","right_basis":"scalar"},"left_chirality":0,"left_particle":"pdg_127","right_chirality":0,"right_particle":"pdg_127"}],"goldstone_partners":[],"max_vertex_valence":3,"name":"built-in-sm","orders":[{"expansion_order":99,"hierarchy":1,"name":"QCD"},{"expansion_order":99,"hierarchy":2,"name":"QED"}],"oriented_kernels":[],"parameters":[{"expression":null,"lhablock":"PYAMPLICOL","lhacode":[1],"name":"alpha_ew","nature":"external","parameter_type":"real","resolved_expression":"7.5467711139788835e-3","value":[0.0075467711139788835,0.0]},{"expression":null,"lhablock":"PYAMPLICOL","lhacode":[2],"name":"alpha_s","nature":"external","parameter_type":"real","resolved_expression":"1.18000000000000e-1","value":[0.118,0.0]},{"expression":null,"lhablock":"PYAMPLICOL","lhacode":[3],"name":"mass_23","nature":"external","parameter_type":"real","resolved_expression":"91.1880000000000","value":[91.188,0.0]},{"expression":null,"lhablock":"PYAMPLICOL","lhacode":[4],"name":"mass_24","nature":"external","parameter_type":"real","resolved_expression":"80.41900244575616","value":[80.41900244575616,0.0]},{"expression":null,"lhablock":"PYAMPLICOL","lhacode":[5],"name":"mass_25","nature":"external","parameter_type":"real","resolved_expression":"125.000000000000","value":[125.0,0.0]},{"expression":null,"lhablock":"PYAMPLICOL","lhacode":[6],"name":"mass_6","nature":"external","parameter_type":"real","resolved_expression":"173.000000000000","value":[173.0,0.0]},{"expression":null,"lhablock":"PYAMPLICOL","lhacode":[7],"name":"width_23","nature":"external","parameter_type":"real","resolved_expression":"2.44140400000000","value":[2.441404,0.0]},{"expression":null,"lhablock":"PYAMPLICOL","lhacode":[8],"name":"width_24","nature":"external","parameter_type":"real","resolved_expression":"2.04760000000000","value":[2.0476,0.0]},{"expression":null,"lhablock":"PYAMPLICOL","lhacode":[9],"name":"width_25","nature":"external","parameter_type":"real","resolved_expression":"6.38233900000000e-3","value":[0.006382339,0.0]},{"expression":null,"lhablock":"PYAMPLICOL","lhacode":[10],"name":"width_6","nature":"external","parameter_type":"real","resolved_expression":"1.49150000000000","value":[1.4915,0.0]}],"particles":[{"antiname":"pdg_-23","auxiliary_kind":"antisymmetric-tensor","charge":0.0,"color":1,"color_role":"singlet","component_dimension":6,"ghost_number":0,"goldstoneboson":false,"mass":"ZERO","name":"pdg_-23","pdg_code":-23,"propagating":false,"propagator":null,"quantum_numbers":[["electric_charge","0"]],"self_conjugate":true,"source_orientation":"self-conjugate","spin":-1,"statistics":"auxiliary","wavefunction_family":"auxiliary","width":"ZERO"},{"antiname":"pdg_-21","auxiliary_kind":"antisymmetric-tensor","charge":0.0,"color":8,"color_role":"adjoint","component_dimension":6,"ghost_number":0,"goldstoneboson":false,"mass":"ZERO","name":"pdg_-21","pdg_code":-21,"propagating":false,"propagator":null,"quantum_numbers":[["electric_charge","0"]],"self_conjugate":true,"source_orientation":"self-conjugate","spin":-1,"statistics":"auxiliary","wavefunction_family":"auxiliary","width":"ZERO"},{"antiname":"d~","auxiliary_kind":null,"charge":-0.3333333333333333,"color":3,"color_role":"fundamental","component_dimension":4,"ghost_number":0,"goldstoneboson":false,"mass":"ZERO","name":"d","pdg_code":1,"propagating":true,"propagator":null,"quantum_numbers":[["electric_charge","-1/3"]],"self_conjugate":false,"source_orientation":"particle","spin":2,"statistics":"fermion","wavefunction_family":"fermion","width":"ZERO"},{"antiname":"d","auxiliary_kind":null,"charge":0.3333333333333333,"color":-3,"color_role":"antifundamental","component_dimension":4,"ghost_number":0,"goldstoneboson":false,"mass":"ZERO","name":"d~","pdg_code":-1,"propagating":true,"propagator":null,"quantum_numbers":[["electric_charge","1/3"]],"self_conjugate":false,"source_orientation":"antiparticle","spin":2,"statistics":"fermion","wavefunction_family":"fermion","width":"ZERO"},{"antiname":"u~","auxiliary_kind":null,"charge":0.6666666666666666,"color":3,"color_role":"fundamental","component_dimension":4,"ghost_number":0,"goldstoneboson":false,"mass":"ZERO","name":"u","pdg_code":2,"propagating":true,"propagator":null,"quantum_numbers":[["electric_charge","2/3"]],"self_conjugate":false,"source_orientation":"particle","spin":2,"statistics":"fermion","wavefunction_family":"fermion","width":"ZERO"},{"antiname":"u","auxiliary_kind":null,"charge":-0.6666666666666666,"color":-3,"color_role":"antifundamental","component_dimension":4,"ghost_number":0,"goldstoneboson":false,"mass":"ZERO","name":"u~","pdg_code":-2,"propagating":true,"propagator":null,"quantum_numbers":[["electric_charge","-2/3"]],"self_conjugate":false,"source_orientation":"antiparticle","spin":2,"statistics":"fermion","wavefunction_family":"fermion","width":"ZERO"},{"antiname":"s~","auxiliary_kind":null,"charge":-0.3333333333333333,"color":3,"color_role":"fundamental","component_dimension":4,"ghost_number":0,"goldstoneboson":false,"mass":"ZERO","name":"s","pdg_code":3,"propagating":true,"propagator":null,"quantum_numbers":[["electric_charge","-1/3"]],"self_conjugate":false,"source_orientation":"particle","spin":2,"statistics":"fermion","wavefunction_family":"fermion","width":"ZERO"},{"antiname":"s","auxiliary_kind":null,"charge":0.3333333333333333,"color":-3,"color_role":"antifundamental","component_dimension":4,"ghost_number":0,"goldstoneboson":false,"mass":"ZERO","name":"s~","pdg_code":-3,"propagating":true,"propagator":null,"quantum_numbers":[["electric_charge","1/3"]],"self_conjugate":false,"source_orientation":"antiparticle","spin":2,"statistics":"fermion","wavefunction_family":"fermion","width":"ZERO"},{"antiname":"c~","auxiliary_kind":null,"charge":0.6666666666666666,"color":3,"color_role":"fundamental","component_dimension":4,"ghost_number":0,"goldstoneboson":false,"mass":"ZERO","name":"c","pdg_code":4,"propagating":true,"propagator":null,"quantum_numbers":[["electric_charge","2/3"]],"self_conjugate":false,"source_orientation":"particle","spin":2,"statistics":"fermion","wavefunction_family":"fermion","width":"ZERO"},{"antiname":"c","auxiliary_kind":null,"charge":-0.6666666666666666,"color":-3,"color_role":"antifundamental","component_dimension":4,"ghost_number":0,"goldstoneboson":false,"mass":"ZERO","name":"c~","pdg_code":-4,"propagating":true,"propagator":null,"quantum_numbers":[["electric_charge","-2/3"]],"self_conjugate":false,"source_orientation":"antiparticle","spin":2,"statistics":"fermion","wavefunction_family":"fermion","width":"ZERO"},{"antiname":"b~","auxiliary_kind":null,"charge":-0.3333333333333333,"color":3,"color_role":"fundamental","component_dimension":4,"ghost_number":0,"goldstoneboson":false,"mass":"ZERO","name":"b","pdg_code":5,"propagating":true,"propagator":null,"quantum_numbers":[["electric_charge","-1/3"]],"self_conjugate":false,"source_orientation":"particle","spin":2,"statistics":"fermion","wavefunction_family":"fermion","width":"ZERO"},{"antiname":"b","auxiliary_kind":null,"charge":0.3333333333333333,"color":-3,"color_role":"antifundamental","component_dimension":4,"ghost_number":0,"goldstoneboson":false,"mass":"ZERO","name":"b~","pdg_code":-5,"propagating":true,"propagator":null,"quantum_numbers":[["electric_charge","1/3"]],"self_conjugate":false,"source_orientation":"antiparticle","spin":2,"statistics":"fermion","wavefunction_family":"fermion","width":"ZERO"},{"antiname":"t~","auxiliary_kind":null,"charge":0.6666666666666666,"color":3,"color_role":"fundamental","component_dimension":4,"ghost_number":0,"goldstoneboson":false,"mass":"mass_6","name":"t","pdg_code":6,"propagating":true,"propagator":null,"quantum_numbers":[["electric_charge","2/3"]],"self_conjugate":false,"source_orientation":"particle","spin":2,"statistics":"fermion","wavefunction_family":"fermion","width":"width_6"},{"antiname":"t","auxiliary_kind":null,"charge":-0.6666666666666666,"color":-3,"color_role":"antifundamental","component_dimension":4,"ghost_number":0,"goldstoneboson":false,"mass":"mass_6","name":"t~","pdg_code":-6,"propagating":true,"propagator":null,"quantum_numbers":[["electric_charge","-2/3"]],"self_conjugate":false,"source_orientation":"antiparticle","spin":2,"statistics":"fermion","wavefunction_family":"fermion","width":"width_6"},{"antiname":"e+","auxiliary_kind":null,"charge":-1.0,"color":1,"color_role":"singlet","component_dimension":4,"ghost_number":0,"goldstoneboson":false,"mass":"ZERO","name":"e-","pdg_code":11,"propagating":true,"propagator":null,"quantum_numbers":[["electric_charge","-1"]],"self_conjugate":false,"source_orientation":"particle","spin":2,"statistics":"fermion","wavefunction_family":"fermion","width":"ZERO"},{"antiname":"e-","auxiliary_kind":null,"charge":1.0,"color":1,"color_role":"singlet","component_dimension":4,"ghost_number":0,"goldstoneboson":false,"mass":"ZERO","name":"e+","pdg_code":-11,"propagating":true,"propagator":null,"quantum_numbers":[["electric_charge","1"]],"self_conjugate":false,"source_orientation":"antiparticle","spin":2,"statistics":"fermion","wavefunction_family":"fermion","width":"ZERO"},{"antiname":"ve~","auxiliary_kind":null,"charge":0.0,"color":1,"color_role":"singlet","component_dimension":4,"ghost_number":0,"goldstoneboson":false,"mass":"ZERO","name":"ve","pdg_code":12,"propagating":true,"propagator":null,"quantum_numbers":[["electric_charge","0"]],"self_conjugate":false,"source_orientation":"particle","spin":2,"statistics":"fermion","wavefunction_family":"fermion","width":"ZERO"},{"antiname":"ve","auxiliary_kind":null,"charge":-0.0,"color":1,"color_role":"singlet","component_dimension":4,"ghost_number":0,"goldstoneboson":false,"mass":"ZERO","name":"ve~","pdg_code":-12,"propagating":true,"propagator":null,"quantum_numbers":[["electric_charge","0"]],"self_conjugate":false,"source_orientation":"antiparticle","spin":2,"statistics":"fermion","wavefunction_family":"fermion","width":"ZERO"},{"antiname":"mu+","auxiliary_kind":null,"charge":-1.0,"color":1,"color_role":"singlet","component_dimension":4,"ghost_number":0,"goldstoneboson":false,"mass":"ZERO","name":"mu-","pdg_code":13,"propagating":true,"propagator":null,"quantum_numbers":[["electric_charge","-1"]],"self_conjugate":false,"source_orientation":"particle","spin":2,"statistics":"fermion","wavefunction_family":"fermion","width":"ZERO"},{"antiname":"mu-","auxiliary_kind":null,"charge":1.0,"color":1,"color_role":"singlet","component_dimension":4,"ghost_number":0,"goldstoneboson":false,"mass":"ZERO","name":"mu+","pdg_code":-13,"propagating":true,"propagator":null,"quantum_numbers":[["electric_charge","1"]],"self_conjugate":false,"source_orientation":"antiparticle","spin":2,"statistics":"fermion","wavefunction_family":"fermion","width":"ZERO"},{"antiname":"vm~","auxiliary_kind":null,"charge":0.0,"color":1,"color_role":"singlet","component_dimension":4,"ghost_number":0,"goldstoneboson":false,"mass":"ZERO","name":"vm","pdg_code":14,"propagating":true,"propagator":null,"quantum_numbers":[["electric_charge","0"]],"self_conjugate":false,"source_orientation":"particle","spin":2,"statistics":"fermion","wavefunction_family":"fermion","width":"ZERO"},{"antiname":"vm","auxiliary_kind":null,"charge":-0.0,"color":1,"color_role":"singlet","component_dimension":4,"ghost_number":0,"goldstoneboson":false,"mass":"ZERO","name":"vm~","pdg_code":-14,"propagating":true,"propagator":null,"quantum_numbers":[["electric_charge","0"]],"self_conjugate":false,"source_orientation":"antiparticle","spin":2,"statistics":"fermion","wavefunction_family":"fermion","width":"ZERO"},{"antiname":"ta+","auxiliary_kind":null,"charge":-1.0,"color":1,"color_role":"singlet","component_dimension":4,"ghost_number":0,"goldstoneboson":false,"mass":"ZERO","name":"ta-","pdg_code":15,"propagating":true,"propagator":null,"quantum_numbers":[["electric_charge","-1"]],"self_conjugate":false,"source_orientation":"particle","spin":2,"statistics":"fermion","wavefunction_family":"fermion","width":"ZERO"},{"antiname":"ta-","auxiliary_kind":null,"charge":1.0,"color":1,"color_role":"singlet","component_dimension":4,"ghost_number":0,"goldstoneboson":false,"mass":"ZERO","name":"ta+","pdg_code":-15,"propagating":true,"propagator":null,"quantum_numbers":[["electric_charge","1"]],"self_conjugate":false,"source_orientation":"antiparticle","spin":2,"statistics":"fermion","wavefunction_family":"fermion","width":"ZERO"},{"antiname":"vt~","auxiliary_kind":null,"charge":0.0,"color":1,"color_role":"singlet","component_dimension":4,"ghost_number":0,"goldstoneboson":false,"mass":"ZERO","name":"vt","pdg_code":16,"propagating":true,"propagator":null,"quantum_numbers":[["electric_charge","0"]],"self_conjugate":false,"source_orientation":"particle","spin":2,"statistics":"fermion","wavefunction_family":"fermion","width":"ZERO"},{"antiname":"vt","auxiliary_kind":null,"charge":-0.0,"color":1,"color_role":"singlet","component_dimension":4,"ghost_number":0,"goldstoneboson":false,"mass":"ZERO","name":"vt~","pdg_code":-16,"propagating":true,"propagator":null,"quantum_numbers":[["electric_charge","0"]],"self_conjugate":false,"source_orientation":"antiparticle","spin":2,"statistics":"fermion","wavefunction_family":"fermion","width":"ZERO"},{"antiname":"g","auxiliary_kind":null,"charge":0.0,"color":8,"color_role":"adjoint","component_dimension":4,"ghost_number":0,"goldstoneboson":false,"mass":"ZERO","name":"g","pdg_code":21,"propagating":true,"propagator":null,"quantum_numbers":[["electric_charge","0"]],"self_conjugate":true,"source_orientation":"self-conjugate","spin":3,"statistics":"boson","wavefunction_family":"vector","width":"ZERO"},{"antiname":"a","auxiliary_kind":null,"charge":0.0,"color":1,"color_role":"singlet","component_dimension":4,"ghost_number":0,"goldstoneboson":false,"mass":"ZERO","name":"a","pdg_code":22,"propagating":true,"propagator":null,"quantum_numbers":[["electric_charge","0"]],"self_conjugate":true,"source_orientation":"self-conjugate","spin":3,"statistics":"boson","wavefunction_family":"vector","width":"ZERO"},{"antiname":"z","auxiliary_kind":null,"charge":0.0,"color":1,"color_role":"singlet","component_dimension":4,"ghost_number":0,"goldstoneboson":false,"mass":"mass_23","name":"z","pdg_code":23,"propagating":true,"propagator":null,"quantum_numbers":[["electric_charge","0"]],"self_conjugate":true,"source_orientation":"self-conjugate","spin":3,"statistics":"boson","wavefunction_family":"vector","width":"width_23"},{"antiname":"w-","auxiliary_kind":null,"charge":1.0,"color":1,"color_role":"singlet","component_dimension":4,"ghost_number":0,"goldstoneboson":false,"mass":"mass_24","name":"w+","pdg_code":24,"propagating":true,"propagator":null,"quantum_numbers":[["electric_charge","1"]],"self_conjugate":false,"source_orientation":"particle","spin":3,"statistics":"boson","wavefunction_family":"vector","width":"width_24"},{"antiname":"w+","auxiliary_kind":null,"charge":-1.0,"color":1,"color_role":"singlet","component_dimension":4,"ghost_number":0,"goldstoneboson":false,"mass":"mass_24","name":"w-","pdg_code":-24,"propagating":true,"propagator":null,"quantum_numbers":[["electric_charge","-1"]],"self_conjugate":false,"source_orientation":"antiparticle","spin":3,"statistics":"boson","wavefunction_family":"vector","width":"width_24"},{"antiname":"h","auxiliary_kind":null,"charge":0.0,"color":1,"color_role":"singlet","component_dimension":1,"ghost_number":0,"goldstoneboson":false,"mass":"mass_25","name":"h","pdg_code":25,"propagating":true,"propagator":null,"quantum_numbers":[["electric_charge","0"]],"self_conjugate":true,"source_orientation":"self-conjugate","spin":1,"statistics":"boson","wavefunction_family":"scalar","width":"width_25"},{"antiname":"pdg_-26","auxiliary_kind":"antisymmetric-tensor","charge":1.0,"color":1,"color_role":"singlet","component_dimension":6,"ghost_number":0,"goldstoneboson":false,"mass":"ZERO","name":"pdg_26","pdg_code":26,"propagating":false,"propagator":null,"quantum_numbers":[["electric_charge","1"]],"self_conjugate":false,"source_orientation":"particle","spin":-1,"statistics":"auxiliary","wavefunction_family":"auxiliary","width":"ZERO"},{"antiname":"pdg_26","auxiliary_kind":"antisymmetric-tensor","charge":-1.0,"color":1,"color_role":"singlet","component_dimension":6,"ghost_number":0,"goldstoneboson":false,"mass":"ZERO","name":"pdg_-26","pdg_code":-26,"propagating":false,"propagator":null,"quantum_numbers":[["electric_charge","-1"]],"self_conjugate":false,"source_orientation":"antiparticle","spin":-1,"statistics":"auxiliary","wavefunction_family":"auxiliary","width":"ZERO"},{"antiname":"pdg_99","auxiliary_kind":"u1-subtraction-color-flow-vector","charge":0.0,"color":1,"color_role":"singlet","component_dimension":4,"ghost_number":0,"goldstoneboson":false,"mass":"ZERO","name":"pdg_99","pdg_code":99,"propagating":false,"propagator":null,"quantum_numbers":[["electric_charge","0"]],"self_conjugate":true,"source_orientation":"self-conjugate","spin":-1,"statistics":"auxiliary","wavefunction_family":"auxiliary","width":"ZERO"},{"antiname":"pdg_125","auxiliary_kind":null,"charge":0.0,"color":1,"color_role":"singlet","component_dimension":1,"ghost_number":0,"goldstoneboson":false,"mass":"ZERO","name":"pdg_125","pdg_code":125,"propagating":false,"propagator":null,"quantum_numbers":[["electric_charge","0"]],"self_conjugate":true,"source_orientation":"self-conjugate","spin":-1,"statistics":"auxiliary","wavefunction_family":"auxiliary","width":"ZERO"},{"antiname":"pdg_126","auxiliary_kind":null,"charge":0.0,"color":1,"color_role":"singlet","component_dimension":1,"ghost_number":0,"goldstoneboson":false,"mass":"ZERO","name":"pdg_126","pdg_code":126,"propagating":false,"propagator":null,"quantum_numbers":[["electric_charge","0"]],"self_conjugate":true,"source_orientation":"self-conjugate","spin":-1,"statistics":"auxiliary","wavefunction_family":"auxiliary","width":"ZERO"},{"antiname":"pdg_127","auxiliary_kind":null,"charge":0.0,"color":1,"color_role":"singlet","component_dimension":1,"ghost_number":0,"goldstoneboson":false,"mass":"ZERO","name":"pdg_127","pdg_code":127,"propagating":false,"propagator":null,"quantum_numbers":[["electric_charge","0"]],"self_conjugate":true,"source_orientation":"self-conjugate","spin":-1,"statistics":"auxiliary","wavefunction_family":"auxiliary","width":"ZERO"}],"propagators":[],"symbol_namespace":"model_built_in_sm_1cbc83b85d7e","tensor_orderings":[],"vertex_terms":[{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_0","coupling_expression":"[1.0, 0.0]","coupling_orders":[],"id":0,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"0","lorentz_source":"built-in","particles":["g","g","g"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_0"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_1","coupling_expression":"[1.0, 0.0]","coupling_orders":[],"id":1,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"1","lorentz_source":"built-in","particles":["g","g","pdg_-21"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_1"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_2","coupling_expression":"[1.0, 0.0]","coupling_orders":[],"id":2,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"2","lorentz_source":"built-in","particles":["pdg_-21","g","g"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_2"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_3","coupling_expression":"[1.0, 0.0]","coupling_orders":[],"id":3,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"3","lorentz_source":"built-in","particles":["g","pdg_-21","g"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_3"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_4","coupling_expression":"[1.0, 0.0]","coupling_orders":[],"id":4,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"4","lorentz_source":"built-in","particles":["g","d","d"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_4"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_5","coupling_expression":"[1.0, 0.0]","coupling_orders":[],"id":5,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"5","lorentz_source":"built-in","particles":["g","d~","d~"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_5"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_6","coupling_expression":"[1.0, 0.0]","coupling_orders":[],"id":6,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"6","lorentz_source":"built-in","particles":["d","g","d"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_6"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_7","coupling_expression":"[1.0, 0.0]","coupling_orders":[],"id":7,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"7","lorentz_source":"built-in","particles":["d~","g","d~"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_7"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_8","coupling_expression":"[1.0, 0.0]","coupling_orders":[],"id":8,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"9","lorentz_source":"built-in","particles":["d~","d","g"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_8"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_9","coupling_expression":"[0.3333333333333333, 0.0]","coupling_orders":[],"id":9,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"8","lorentz_source":"built-in","particles":["d","d~","pdg_99"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_9"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_10","coupling_expression":"[1.0, 0.0]","coupling_orders":[],"id":10,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"4","lorentz_source":"built-in","particles":["pdg_99","d","d"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_10"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_11","coupling_expression":"[1.0, 0.0]","coupling_orders":[],"id":11,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"5","lorentz_source":"built-in","particles":["pdg_99","d~","d~"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_11"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_12","coupling_expression":"[1.0, 0.0]","coupling_orders":[],"id":12,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"6","lorentz_source":"built-in","particles":["d","pdg_99","d"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_12"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_13","coupling_expression":"[1.0, 0.0]","coupling_orders":[],"id":13,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"7","lorentz_source":"built-in","particles":["d~","pdg_99","d~"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_13"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_14","coupling_expression":"[-0.3333333333333333, -0.3333333333333333]","coupling_orders":[],"id":14,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"10","lorentz_source":"built-in","particles":["d","a","d"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_14"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_15","coupling_expression":"[-0.3333333333333333, -0.3333333333333333]","coupling_orders":[],"id":15,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"11","lorentz_source":"built-in","particles":["d~","a","d~"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_15"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_16","coupling_expression":"[-1.0244420275940371, 0.17818666745287456]","coupling_orders":[],"id":16,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"10","lorentz_source":"built-in","particles":["d","z","d"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_16"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_17","coupling_expression":"[-1.0244420275940371, 0.17818666745287456]","coupling_orders":[],"id":17,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"11","lorentz_source":"built-in","particles":["d~","z","d~"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_17"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_18","coupling_expression":"[1.4999181171782847, 0.0]","coupling_orders":[],"id":18,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"10","lorentz_source":"built-in","particles":["d","w+","u"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_18"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_19","coupling_expression":"[1.4999181171782847, 0.0]","coupling_orders":[],"id":19,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"11","lorentz_source":"built-in","particles":["d~","w-","u~"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_19"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_20","coupling_expression":"[1.0, 0.0]","coupling_orders":[],"id":20,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"4","lorentz_source":"built-in","particles":["g","u","u"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_20"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_21","coupling_expression":"[1.0, 0.0]","coupling_orders":[],"id":21,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"5","lorentz_source":"built-in","particles":["g","u~","u~"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_21"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_22","coupling_expression":"[1.0, 0.0]","coupling_orders":[],"id":22,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"6","lorentz_source":"built-in","particles":["u","g","u"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_22"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_23","coupling_expression":"[1.0, 0.0]","coupling_orders":[],"id":23,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"7","lorentz_source":"built-in","particles":["u~","g","u~"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_23"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_24","coupling_expression":"[1.0, 0.0]","coupling_orders":[],"id":24,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"9","lorentz_source":"built-in","particles":["u~","u","g"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_24"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_25","coupling_expression":"[0.3333333333333333, 0.0]","coupling_orders":[],"id":25,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"8","lorentz_source":"built-in","particles":["u","u~","pdg_99"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_25"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_26","coupling_expression":"[1.0, 0.0]","coupling_orders":[],"id":26,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"4","lorentz_source":"built-in","particles":["pdg_99","u","u"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_26"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_27","coupling_expression":"[1.0, 0.0]","coupling_orders":[],"id":27,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"5","lorentz_source":"built-in","particles":["pdg_99","u~","u~"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_27"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_28","coupling_expression":"[1.0, 0.0]","coupling_orders":[],"id":28,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"6","lorentz_source":"built-in","particles":["u","pdg_99","u"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_28"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_29","coupling_expression":"[1.0, 0.0]","coupling_orders":[],"id":29,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"7","lorentz_source":"built-in","particles":["u~","pdg_99","u~"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_29"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_30","coupling_expression":"[0.6666666666666666, 0.6666666666666666]","coupling_orders":[],"id":30,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"10","lorentz_source":"built-in","particles":["u","a","u"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_30"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_31","coupling_expression":"[0.6666666666666666, 0.6666666666666666]","coupling_orders":[],"id":31,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"11","lorentz_source":"built-in","particles":["u~","a","u~"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_31"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_32","coupling_expression":"[0.8462553601411626, -0.35637333490574913]","coupling_orders":[],"id":32,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"10","lorentz_source":"built-in","particles":["u","z","u"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_32"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_33","coupling_expression":"[0.8462553601411626, -0.35637333490574913]","coupling_orders":[],"id":33,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"11","lorentz_source":"built-in","particles":["u~","z","u~"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_33"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_34","coupling_expression":"[1.4999181171782847, 0.0]","coupling_orders":[],"id":34,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"10","lorentz_source":"built-in","particles":["u","w-","d"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_34"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_35","coupling_expression":"[1.4999181171782847, 0.0]","coupling_orders":[],"id":35,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"11","lorentz_source":"built-in","particles":["u~","w+","d~"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_35"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_36","coupling_expression":"[1.0, 0.0]","coupling_orders":[],"id":36,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"4","lorentz_source":"built-in","particles":["g","s","s"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_36"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_37","coupling_expression":"[1.0, 0.0]","coupling_orders":[],"id":37,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"5","lorentz_source":"built-in","particles":["g","s~","s~"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_37"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_38","coupling_expression":"[1.0, 0.0]","coupling_orders":[],"id":38,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"6","lorentz_source":"built-in","particles":["s","g","s"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_38"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_39","coupling_expression":"[1.0, 0.0]","coupling_orders":[],"id":39,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"7","lorentz_source":"built-in","particles":["s~","g","s~"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_39"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_40","coupling_expression":"[1.0, 0.0]","coupling_orders":[],"id":40,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"9","lorentz_source":"built-in","particles":["s~","s","g"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_40"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_41","coupling_expression":"[0.3333333333333333, 0.0]","coupling_orders":[],"id":41,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"8","lorentz_source":"built-in","particles":["s","s~","pdg_99"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_41"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_42","coupling_expression":"[1.0, 0.0]","coupling_orders":[],"id":42,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"4","lorentz_source":"built-in","particles":["pdg_99","s","s"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_42"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_43","coupling_expression":"[1.0, 0.0]","coupling_orders":[],"id":43,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"5","lorentz_source":"built-in","particles":["pdg_99","s~","s~"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_43"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_44","coupling_expression":"[1.0, 0.0]","coupling_orders":[],"id":44,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"6","lorentz_source":"built-in","particles":["s","pdg_99","s"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_44"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_45","coupling_expression":"[1.0, 0.0]","coupling_orders":[],"id":45,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"7","lorentz_source":"built-in","particles":["s~","pdg_99","s~"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_45"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_46","coupling_expression":"[-0.3333333333333333, -0.3333333333333333]","coupling_orders":[],"id":46,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"10","lorentz_source":"built-in","particles":["s","a","s"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_46"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_47","coupling_expression":"[-0.3333333333333333, -0.3333333333333333]","coupling_orders":[],"id":47,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"11","lorentz_source":"built-in","particles":["s~","a","s~"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_47"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_48","coupling_expression":"[-1.0244420275940371, 0.17818666745287456]","coupling_orders":[],"id":48,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"10","lorentz_source":"built-in","particles":["s","z","s"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_48"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_49","coupling_expression":"[-1.0244420275940371, 0.17818666745287456]","coupling_orders":[],"id":49,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"11","lorentz_source":"built-in","particles":["s~","z","s~"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_49"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_50","coupling_expression":"[1.4999181171782847, 0.0]","coupling_orders":[],"id":50,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"10","lorentz_source":"built-in","particles":["s","w+","c"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_50"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_51","coupling_expression":"[1.4999181171782847, 0.0]","coupling_orders":[],"id":51,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"11","lorentz_source":"built-in","particles":["s~","w-","c~"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_51"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_52","coupling_expression":"[1.0, 0.0]","coupling_orders":[],"id":52,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"4","lorentz_source":"built-in","particles":["g","c","c"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_52"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_53","coupling_expression":"[1.0, 0.0]","coupling_orders":[],"id":53,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"5","lorentz_source":"built-in","particles":["g","c~","c~"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_53"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_54","coupling_expression":"[1.0, 0.0]","coupling_orders":[],"id":54,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"6","lorentz_source":"built-in","particles":["c","g","c"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_54"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_55","coupling_expression":"[1.0, 0.0]","coupling_orders":[],"id":55,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"7","lorentz_source":"built-in","particles":["c~","g","c~"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_55"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_56","coupling_expression":"[1.0, 0.0]","coupling_orders":[],"id":56,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"9","lorentz_source":"built-in","particles":["c~","c","g"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_56"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_57","coupling_expression":"[0.3333333333333333, 0.0]","coupling_orders":[],"id":57,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"8","lorentz_source":"built-in","particles":["c","c~","pdg_99"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_57"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_58","coupling_expression":"[1.0, 0.0]","coupling_orders":[],"id":58,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"4","lorentz_source":"built-in","particles":["pdg_99","c","c"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_58"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_59","coupling_expression":"[1.0, 0.0]","coupling_orders":[],"id":59,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"5","lorentz_source":"built-in","particles":["pdg_99","c~","c~"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_59"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_60","coupling_expression":"[1.0, 0.0]","coupling_orders":[],"id":60,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"6","lorentz_source":"built-in","particles":["c","pdg_99","c"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_60"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_61","coupling_expression":"[1.0, 0.0]","coupling_orders":[],"id":61,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"7","lorentz_source":"built-in","particles":["c~","pdg_99","c~"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_61"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_62","coupling_expression":"[0.6666666666666666, 0.6666666666666666]","coupling_orders":[],"id":62,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"10","lorentz_source":"built-in","particles":["c","a","c"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_62"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_63","coupling_expression":"[0.6666666666666666, 0.6666666666666666]","coupling_orders":[],"id":63,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"11","lorentz_source":"built-in","particles":["c~","a","c~"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_63"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_64","coupling_expression":"[0.8462553601411626, -0.35637333490574913]","coupling_orders":[],"id":64,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"10","lorentz_source":"built-in","particles":["c","z","c"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_64"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_65","coupling_expression":"[0.8462553601411626, -0.35637333490574913]","coupling_orders":[],"id":65,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"11","lorentz_source":"built-in","particles":["c~","z","c~"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_65"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_66","coupling_expression":"[1.4999181171782847, 0.0]","coupling_orders":[],"id":66,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"10","lorentz_source":"built-in","particles":["c","w-","s"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_66"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_67","coupling_expression":"[1.4999181171782847, 0.0]","coupling_orders":[],"id":67,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"11","lorentz_source":"built-in","particles":["c~","w+","s~"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_67"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_68","coupling_expression":"[1.0, 0.0]","coupling_orders":[],"id":68,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"4","lorentz_source":"built-in","particles":["g","b","b"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_68"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_69","coupling_expression":"[1.0, 0.0]","coupling_orders":[],"id":69,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"5","lorentz_source":"built-in","particles":["g","b~","b~"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_69"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_70","coupling_expression":"[1.0, 0.0]","coupling_orders":[],"id":70,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"6","lorentz_source":"built-in","particles":["b","g","b"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_70"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_71","coupling_expression":"[1.0, 0.0]","coupling_orders":[],"id":71,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"7","lorentz_source":"built-in","particles":["b~","g","b~"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_71"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_72","coupling_expression":"[1.0, 0.0]","coupling_orders":[],"id":72,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"9","lorentz_source":"built-in","particles":["b~","b","g"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_72"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_73","coupling_expression":"[0.3333333333333333, 0.0]","coupling_orders":[],"id":73,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"8","lorentz_source":"built-in","particles":["b","b~","pdg_99"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_73"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_74","coupling_expression":"[1.0, 0.0]","coupling_orders":[],"id":74,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"4","lorentz_source":"built-in","particles":["pdg_99","b","b"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_74"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_75","coupling_expression":"[1.0, 0.0]","coupling_orders":[],"id":75,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"5","lorentz_source":"built-in","particles":["pdg_99","b~","b~"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_75"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_76","coupling_expression":"[1.0, 0.0]","coupling_orders":[],"id":76,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"6","lorentz_source":"built-in","particles":["b","pdg_99","b"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_76"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_77","coupling_expression":"[1.0, 0.0]","coupling_orders":[],"id":77,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"7","lorentz_source":"built-in","particles":["b~","pdg_99","b~"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_77"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_78","coupling_expression":"[-0.3333333333333333, -0.3333333333333333]","coupling_orders":[],"id":78,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"10","lorentz_source":"built-in","particles":["b","a","b"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_78"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_79","coupling_expression":"[-0.3333333333333333, -0.3333333333333333]","coupling_orders":[],"id":79,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"11","lorentz_source":"built-in","particles":["b~","a","b~"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_79"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_80","coupling_expression":"[-1.0244420275940371, 0.17818666745287456]","coupling_orders":[],"id":80,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"10","lorentz_source":"built-in","particles":["b","z","b"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_80"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_81","coupling_expression":"[-1.0244420275940371, 0.17818666745287456]","coupling_orders":[],"id":81,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"11","lorentz_source":"built-in","particles":["b~","z","b~"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_81"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_82","coupling_expression":"[1.4999181171782847, 0.0]","coupling_orders":[],"id":82,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"10","lorentz_source":"built-in","particles":["b","w+","t"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_82"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_83","coupling_expression":"[1.4999181171782847, 0.0]","coupling_orders":[],"id":83,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"11","lorentz_source":"built-in","particles":["b~","w-","t~"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_83"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_84","coupling_expression":"[1.0, 0.0]","coupling_orders":[],"id":84,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"4","lorentz_source":"built-in","particles":["g","t","t"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_84"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_85","coupling_expression":"[1.0, 0.0]","coupling_orders":[],"id":85,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"5","lorentz_source":"built-in","particles":["g","t~","t~"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_85"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_86","coupling_expression":"[1.0, 0.0]","coupling_orders":[],"id":86,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"6","lorentz_source":"built-in","particles":["t","g","t"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_86"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_87","coupling_expression":"[1.0, 0.0]","coupling_orders":[],"id":87,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"7","lorentz_source":"built-in","particles":["t~","g","t~"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_87"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_88","coupling_expression":"[1.0, 0.0]","coupling_orders":[],"id":88,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"9","lorentz_source":"built-in","particles":["t~","t","g"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_88"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_89","coupling_expression":"[0.3333333333333333, 0.0]","coupling_orders":[],"id":89,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"8","lorentz_source":"built-in","particles":["t","t~","pdg_99"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_89"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_90","coupling_expression":"[1.0, 0.0]","coupling_orders":[],"id":90,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"4","lorentz_source":"built-in","particles":["pdg_99","t","t"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_90"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_91","coupling_expression":"[1.0, 0.0]","coupling_orders":[],"id":91,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"5","lorentz_source":"built-in","particles":["pdg_99","t~","t~"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_91"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_92","coupling_expression":"[1.0, 0.0]","coupling_orders":[],"id":92,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"6","lorentz_source":"built-in","particles":["t","pdg_99","t"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_92"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_93","coupling_expression":"[1.0, 0.0]","coupling_orders":[],"id":93,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"7","lorentz_source":"built-in","particles":["t~","pdg_99","t~"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_93"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_94","coupling_expression":"[0.6666666666666666, 0.6666666666666666]","coupling_orders":[],"id":94,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"10","lorentz_source":"built-in","particles":["t","a","t"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_94"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_95","coupling_expression":"[0.6666666666666666, 0.6666666666666666]","coupling_orders":[],"id":95,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"11","lorentz_source":"built-in","particles":["t~","a","t~"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_95"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_96","coupling_expression":"[0.8462553601411626, -0.35637333490574913]","coupling_orders":[],"id":96,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"10","lorentz_source":"built-in","particles":["t","z","t"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_96"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_97","coupling_expression":"[0.8462553601411626, -0.35637333490574913]","coupling_orders":[],"id":97,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"11","lorentz_source":"built-in","particles":["t~","z","t~"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_97"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_98","coupling_expression":"[1.4999181171782847, 0.0]","coupling_orders":[],"id":98,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"10","lorentz_source":"built-in","particles":["t","w-","b"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_98"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_99","coupling_expression":"[1.4999181171782847, 0.0]","coupling_orders":[],"id":99,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"11","lorentz_source":"built-in","particles":["t~","w+","b~"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_99"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_100","coupling_expression":"[-1.8706973877352002, 0.0]","coupling_orders":[],"id":100,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"12","lorentz_source":"built-in","particles":["w+","w-","z"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_100"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_101","coupling_expression":"[1.8706973877352002, 0.0]","coupling_orders":[],"id":101,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"12","lorentz_source":"built-in","particles":["w-","w+","z"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_101"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_102","coupling_expression":"[-1.0, 0.0]","coupling_orders":[],"id":102,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"12","lorentz_source":"built-in","particles":["w+","w-","a"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_102"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_103","coupling_expression":"[1.0, 0.0]","coupling_orders":[],"id":103,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"12","lorentz_source":"built-in","particles":["w-","w+","a"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_103"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_104","coupling_expression":"[1.0, 0.0]","coupling_orders":[],"id":104,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"12","lorentz_source":"built-in","particles":["w+","a","w+"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_104"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_105","coupling_expression":"[-1.0, 0.0]","coupling_orders":[],"id":105,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"12","lorentz_source":"built-in","particles":["w-","a","w-"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_105"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_106","coupling_expression":"[-1.0, 0.0]","coupling_orders":[],"id":106,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"12","lorentz_source":"built-in","particles":["a","w+","w+"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_106"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_107","coupling_expression":"[1.0, 0.0]","coupling_orders":[],"id":107,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"12","lorentz_source":"built-in","particles":["a","w-","w-"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_107"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_108","coupling_expression":"[1.8706973877352002, 0.0]","coupling_orders":[],"id":108,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"12","lorentz_source":"built-in","particles":["w+","z","w+"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_108"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_109","coupling_expression":"[-1.8706973877352002, 0.0]","coupling_orders":[],"id":109,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"12","lorentz_source":"built-in","particles":["w-","z","w-"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_109"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_110","coupling_expression":"[-1.8706973877352002, 0.0]","coupling_orders":[],"id":110,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"12","lorentz_source":"built-in","particles":["z","w+","w+"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_110"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_111","coupling_expression":"[1.8706973877352002, 0.0]","coupling_orders":[],"id":111,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"12","lorentz_source":"built-in","particles":["z","w-","w-"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_111"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_112","coupling_expression":"[2.1212045437626474, 0.0]","coupling_orders":[],"id":112,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"13","lorentz_source":"built-in","particles":["w+","w-","pdg_-23"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_112"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_113","coupling_expression":"[-2.1212045437626474, 0.0]","coupling_orders":[],"id":113,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"13","lorentz_source":"built-in","particles":["w-","w+","pdg_-23"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_113"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_114","coupling_expression":"[2.1212045437626474, 0.0]","coupling_orders":[],"id":114,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"14","lorentz_source":"built-in","particles":["pdg_-23","w+","w+"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_114"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_115","coupling_expression":"[-2.1212045437626474, 0.0]","coupling_orders":[],"id":115,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"14","lorentz_source":"built-in","particles":["pdg_-23","w-","w-"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_115"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_116","coupling_expression":"[-2.1212045437626474, 0.0]","coupling_orders":[],"id":116,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"15","lorentz_source":"built-in","particles":["w+","pdg_-23","w+"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_116"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_117","coupling_expression":"[2.1212045437626474, 0.0]","coupling_orders":[],"id":117,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"15","lorentz_source":"built-in","particles":["w-","pdg_-23","w-"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_117"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_118","coupling_expression":"[1.0, 0.0]","coupling_orders":[],"id":118,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"13","lorentz_source":"built-in","particles":["w+","a","pdg_26"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_118"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_119","coupling_expression":"[1.0, 0.0]","coupling_orders":[],"id":119,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"13","lorentz_source":"built-in","particles":["w-","a","pdg_-26"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_119"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_120","coupling_expression":"[-1.0, 0.0]","coupling_orders":[],"id":120,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"13","lorentz_source":"built-in","particles":["a","w+","pdg_26"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_120"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_121","coupling_expression":"[-1.0, 0.0]","coupling_orders":[],"id":121,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"13","lorentz_source":"built-in","particles":["a","w-","pdg_-26"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_121"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_122","coupling_expression":"[1.8706973877352002, 0.0]","coupling_orders":[],"id":122,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"13","lorentz_source":"built-in","particles":["w+","z","pdg_26"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_122"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_123","coupling_expression":"[1.8706973877352002, 0.0]","coupling_orders":[],"id":123,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"13","lorentz_source":"built-in","particles":["w-","z","pdg_-26"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_123"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_124","coupling_expression":"[-1.8706973877352002, 0.0]","coupling_orders":[],"id":124,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"13","lorentz_source":"built-in","particles":["z","w+","pdg_26"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_124"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_125","coupling_expression":"[-1.8706973877352002, 0.0]","coupling_orders":[],"id":125,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"13","lorentz_source":"built-in","particles":["z","w-","pdg_-26"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_125"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_126","coupling_expression":"[1.0, 0.0]","coupling_orders":[],"id":126,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"14","lorentz_source":"built-in","particles":["pdg_26","a","w+"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_126"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_127","coupling_expression":"[1.0, 0.0]","coupling_orders":[],"id":127,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"14","lorentz_source":"built-in","particles":["pdg_-26","a","w-"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_127"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_128","coupling_expression":"[-1.0, 0.0]","coupling_orders":[],"id":128,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"14","lorentz_source":"built-in","particles":["pdg_26","w-","a"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_128"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_129","coupling_expression":"[-1.0, 0.0]","coupling_orders":[],"id":129,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"14","lorentz_source":"built-in","particles":["pdg_-26","w+","a"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_129"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_130","coupling_expression":"[1.8706973877352002, 0.0]","coupling_orders":[],"id":130,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"14","lorentz_source":"built-in","particles":["pdg_26","z","w+"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_130"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_131","coupling_expression":"[1.8706973877352002, 0.0]","coupling_orders":[],"id":131,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"14","lorentz_source":"built-in","particles":["pdg_-26","z","w-"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_131"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_132","coupling_expression":"[-1.8706973877352002, 0.0]","coupling_orders":[],"id":132,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"14","lorentz_source":"built-in","particles":["pdg_26","w-","z"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_132"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_133","coupling_expression":"[-1.8706973877352002, 0.0]","coupling_orders":[],"id":133,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"14","lorentz_source":"built-in","particles":["pdg_-26","w+","z"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_133"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_134","coupling_expression":"[-1.0, 0.0]","coupling_orders":[],"id":134,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"15","lorentz_source":"built-in","particles":["a","pdg_26","w+"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_134"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_135","coupling_expression":"[-1.0, 0.0]","coupling_orders":[],"id":135,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"15","lorentz_source":"built-in","particles":["a","pdg_-26","w-"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_135"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_136","coupling_expression":"[1.0, 0.0]","coupling_orders":[],"id":136,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"15","lorentz_source":"built-in","particles":["w+","pdg_-26","a"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_136"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_137","coupling_expression":"[1.0, 0.0]","coupling_orders":[],"id":137,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"15","lorentz_source":"built-in","particles":["w-","pdg_26","a"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_137"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_138","coupling_expression":"[-1.8706973877352002, 0.0]","coupling_orders":[],"id":138,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"15","lorentz_source":"built-in","particles":["z","pdg_26","w+"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_138"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_139","coupling_expression":"[-1.8706973877352002, 0.0]","coupling_orders":[],"id":139,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"15","lorentz_source":"built-in","particles":["z","pdg_-26","w-"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_139"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_140","coupling_expression":"[1.8706973877352002, 0.0]","coupling_orders":[],"id":140,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"15","lorentz_source":"built-in","particles":["w+","pdg_-26","z"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_140"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_141","coupling_expression":"[1.8706973877352002, 0.0]","coupling_orders":[],"id":141,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"15","lorentz_source":"built-in","particles":["w-","pdg_26","z"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_141"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_142","coupling_expression":"[2.281602450356579, 0.0]","coupling_orders":[],"id":142,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"16","lorentz_source":"built-in","particles":["t","h","t"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_142"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_143","coupling_expression":"[2.281602450356579, 0.0]","coupling_orders":[],"id":143,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"16","lorentz_source":"built-in","particles":["t~","h","t~"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_143"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_144","coupling_expression":"[170.58515339279742, 0.0]","coupling_orders":[],"id":144,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"17","lorentz_source":"built-in","particles":["w+","w-","h"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_144"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_145","coupling_expression":"[170.58515339279742, 0.0]","coupling_orders":[],"id":145,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"17","lorentz_source":"built-in","particles":["w-","w+","h"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_145"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_146","coupling_expression":"[170.58515339279742, 0.0]","coupling_orders":[],"id":146,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"18","lorentz_source":"built-in","particles":["h","w+","w+"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_146"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_147","coupling_expression":"[170.58515339279742, 0.0]","coupling_orders":[],"id":147,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"18","lorentz_source":"built-in","particles":["h","w-","w-"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_147"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_148","coupling_expression":"[170.58515339279742, 0.0]","coupling_orders":[],"id":148,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"19","lorentz_source":"built-in","particles":["w+","h","w+"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_148"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_149","coupling_expression":"[170.58515339279742, 0.0]","coupling_orders":[],"id":149,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"19","lorentz_source":"built-in","particles":["w-","h","w-"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_149"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_150","coupling_expression":"[219.3306108878756, 0.0]","coupling_orders":[],"id":150,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"17","lorentz_source":"built-in","particles":["z","z","h"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_150"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_151","coupling_expression":"[219.3306108878756, 0.0]","coupling_orders":[],"id":151,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"18","lorentz_source":"built-in","particles":["h","z","z"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_151"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_152","coupling_expression":"[219.3306108878756, 0.0]","coupling_orders":[],"id":152,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"19","lorentz_source":"built-in","particles":["z","h","z"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_152"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_153","coupling_expression":"[-618.2087564188708, 0.0]","coupling_orders":[],"id":153,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"20","lorentz_source":"built-in","particles":["h","h","h"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_153"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_154","coupling_expression":"[2.2497543582396506, 0.0]","coupling_orders":[],"id":154,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"17","lorentz_source":"built-in","particles":["w+","w-","pdg_127"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_154"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_155","coupling_expression":"[2.2497543582396506, 0.0]","coupling_orders":[],"id":155,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"17","lorentz_source":"built-in","particles":["w-","w+","pdg_127"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_155"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_156","coupling_expression":"[2.892631556300476, 0.0]","coupling_orders":[],"id":156,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"17","lorentz_source":"built-in","particles":["z","z","pdg_127"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_156"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_157","coupling_expression":"[1.0, 0.0]","coupling_orders":[],"id":157,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"20","lorentz_source":"built-in","particles":["pdg_125","h","h"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_157"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_158","coupling_expression":"[1.0, 0.0]","coupling_orders":[],"id":158,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"20","lorentz_source":"built-in","particles":["h","pdg_125","h"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_158"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_159","coupling_expression":"[-8.153217418943292, 0.0]","coupling_orders":[],"id":159,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"20","lorentz_source":"built-in","particles":["h","h","pdg_125"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_159"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_160","coupling_expression":"[1.0, 0.0]","coupling_orders":[],"id":160,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"20","lorentz_source":"built-in","particles":["pdg_127","h","h"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_160"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_161","coupling_expression":"[1.0, 0.0]","coupling_orders":[],"id":161,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"20","lorentz_source":"built-in","particles":["h","pdg_127","h"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_161"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_162","coupling_expression":"[1.0, -10.0]","coupling_orders":[],"id":162,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"20","lorentz_source":"built-in","particles":["h","h","pdg_126"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_162"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_163","coupling_expression":"[-2.892631556300476, 0.0]","coupling_orders":[],"id":163,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"18","lorentz_source":"built-in","particles":["pdg_126","z","z"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_163"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_164","coupling_expression":"[-2.892631556300476, 0.0]","coupling_orders":[],"id":164,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"19","lorentz_source":"built-in","particles":["z","pdg_126","z"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_164"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_165","coupling_expression":"[-2.2497543582396506, 0.0]","coupling_orders":[],"id":165,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"18","lorentz_source":"built-in","particles":["pdg_126","w+","w+"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_165"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_166","coupling_expression":"[-2.2497543582396506, 0.0]","coupling_orders":[],"id":166,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"18","lorentz_source":"built-in","particles":["pdg_126","w-","w-"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_166"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_167","coupling_expression":"[-2.2497543582396506, 0.0]","coupling_orders":[],"id":167,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"19","lorentz_source":"built-in","particles":["w+","pdg_126","w+"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_167"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_168","coupling_expression":"[-2.2497543582396506, 0.0]","coupling_orders":[],"id":168,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"19","lorentz_source":"built-in","particles":["w-","pdg_126","w-"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_168"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_169","coupling_expression":"[-1.0, -1.0]","coupling_orders":[],"id":169,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"21","lorentz_source":"built-in","particles":["e-","e+","a"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_169"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_170","coupling_expression":"[-1.0, -1.0]","coupling_orders":[],"id":170,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"22","lorentz_source":"built-in","particles":["e+","e-","a"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_170"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_171","coupling_expression":"[-0.6680686926882882, 0.5345600023586237]","coupling_orders":[],"id":171,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"21","lorentz_source":"built-in","particles":["e-","e+","z"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_171"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_172","coupling_expression":"[-0.6680686926882882, 0.5345600023586237]","coupling_orders":[],"id":172,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"22","lorentz_source":"built-in","particles":["e+","e-","z"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_172"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_173","coupling_expression":"[1.4999181171782847, 0.0]","coupling_orders":[],"id":173,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"21","lorentz_source":"built-in","particles":["e-","ve~","w-"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_173"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_174","coupling_expression":"[1.4999181171782847, 0.0]","coupling_orders":[],"id":174,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"22","lorentz_source":"built-in","particles":["e+","ve","w+"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_174"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_175","coupling_expression":"[-1.0, -1.0]","coupling_orders":[],"id":175,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"10","lorentz_source":"built-in","particles":["e-","a","e-"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_175"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_176","coupling_expression":"[-1.0, -1.0]","coupling_orders":[],"id":176,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"11","lorentz_source":"built-in","particles":["e+","a","e+"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_176"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_177","coupling_expression":"[-1.0, -1.0]","coupling_orders":[],"id":177,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"23","lorentz_source":"built-in","particles":["a","e-","e-"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_177"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_178","coupling_expression":"[-1.0, -1.0]","coupling_orders":[],"id":178,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"24","lorentz_source":"built-in","particles":["a","e+","e+"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_178"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_179","coupling_expression":"[-0.6680686926882882, 0.5345600023586237]","coupling_orders":[],"id":179,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"10","lorentz_source":"built-in","particles":["e-","z","e-"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_179"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_180","coupling_expression":"[-0.6680686926882882, 0.5345600023586237]","coupling_orders":[],"id":180,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"11","lorentz_source":"built-in","particles":["e+","z","e+"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_180"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_181","coupling_expression":"[-0.6680686926882882, 0.5345600023586237]","coupling_orders":[],"id":181,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"23","lorentz_source":"built-in","particles":["z","e-","e-"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_181"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_182","coupling_expression":"[-0.6680686926882882, 0.5345600023586237]","coupling_orders":[],"id":182,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"24","lorentz_source":"built-in","particles":["z","e+","e+"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_182"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_183","coupling_expression":"[-1.0, -1.0]","coupling_orders":[],"id":183,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"21","lorentz_source":"built-in","particles":["mu-","mu+","a"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_183"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_184","coupling_expression":"[-1.0, -1.0]","coupling_orders":[],"id":184,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"22","lorentz_source":"built-in","particles":["mu+","mu-","a"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_184"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_185","coupling_expression":"[-0.6680686926882882, 0.5345600023586237]","coupling_orders":[],"id":185,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"21","lorentz_source":"built-in","particles":["mu-","mu+","z"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_185"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_186","coupling_expression":"[-0.6680686926882882, 0.5345600023586237]","coupling_orders":[],"id":186,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"22","lorentz_source":"built-in","particles":["mu+","mu-","z"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_186"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_187","coupling_expression":"[1.4999181171782847, 0.0]","coupling_orders":[],"id":187,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"21","lorentz_source":"built-in","particles":["mu-","vm~","w-"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_187"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_188","coupling_expression":"[1.4999181171782847, 0.0]","coupling_orders":[],"id":188,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"22","lorentz_source":"built-in","particles":["mu+","vm","w+"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_188"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_189","coupling_expression":"[-1.0, -1.0]","coupling_orders":[],"id":189,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"10","lorentz_source":"built-in","particles":["mu-","a","mu-"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_189"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_190","coupling_expression":"[-1.0, -1.0]","coupling_orders":[],"id":190,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"11","lorentz_source":"built-in","particles":["mu+","a","mu+"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_190"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_191","coupling_expression":"[-1.0, -1.0]","coupling_orders":[],"id":191,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"23","lorentz_source":"built-in","particles":["a","mu-","mu-"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_191"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_192","coupling_expression":"[-1.0, -1.0]","coupling_orders":[],"id":192,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"24","lorentz_source":"built-in","particles":["a","mu+","mu+"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_192"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_193","coupling_expression":"[-0.6680686926882882, 0.5345600023586237]","coupling_orders":[],"id":193,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"10","lorentz_source":"built-in","particles":["mu-","z","mu-"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_193"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_194","coupling_expression":"[-0.6680686926882882, 0.5345600023586237]","coupling_orders":[],"id":194,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"11","lorentz_source":"built-in","particles":["mu+","z","mu+"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_194"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_195","coupling_expression":"[-0.6680686926882882, 0.5345600023586237]","coupling_orders":[],"id":195,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"23","lorentz_source":"built-in","particles":["z","mu-","mu-"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_195"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_196","coupling_expression":"[-0.6680686926882882, 0.5345600023586237]","coupling_orders":[],"id":196,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"24","lorentz_source":"built-in","particles":["z","mu+","mu+"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_196"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_197","coupling_expression":"[-1.0, -1.0]","coupling_orders":[],"id":197,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"21","lorentz_source":"built-in","particles":["ta-","ta+","a"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_197"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_198","coupling_expression":"[-1.0, -1.0]","coupling_orders":[],"id":198,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"22","lorentz_source":"built-in","particles":["ta+","ta-","a"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_198"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_199","coupling_expression":"[-0.6680686926882882, 0.5345600023586237]","coupling_orders":[],"id":199,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"21","lorentz_source":"built-in","particles":["ta-","ta+","z"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_199"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_200","coupling_expression":"[-0.6680686926882882, 0.5345600023586237]","coupling_orders":[],"id":200,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"22","lorentz_source":"built-in","particles":["ta+","ta-","z"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_200"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_201","coupling_expression":"[1.4999181171782847, 0.0]","coupling_orders":[],"id":201,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"21","lorentz_source":"built-in","particles":["ta-","vt~","w-"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_201"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_202","coupling_expression":"[1.4999181171782847, 0.0]","coupling_orders":[],"id":202,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"22","lorentz_source":"built-in","particles":["ta+","vt","w+"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_202"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_203","coupling_expression":"[-1.0, -1.0]","coupling_orders":[],"id":203,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"10","lorentz_source":"built-in","particles":["ta-","a","ta-"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_203"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_204","coupling_expression":"[-1.0, -1.0]","coupling_orders":[],"id":204,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"11","lorentz_source":"built-in","particles":["ta+","a","ta+"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_204"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_205","coupling_expression":"[-1.0, -1.0]","coupling_orders":[],"id":205,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"23","lorentz_source":"built-in","particles":["a","ta-","ta-"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_205"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_206","coupling_expression":"[-1.0, -1.0]","coupling_orders":[],"id":206,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"24","lorentz_source":"built-in","particles":["a","ta+","ta+"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_206"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_207","coupling_expression":"[-0.6680686926882882, 0.5345600023586237]","coupling_orders":[],"id":207,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"10","lorentz_source":"built-in","particles":["ta-","z","ta-"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_207"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_208","coupling_expression":"[-0.6680686926882882, 0.5345600023586237]","coupling_orders":[],"id":208,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"11","lorentz_source":"built-in","particles":["ta+","z","ta+"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_208"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_209","coupling_expression":"[-0.6680686926882882, 0.5345600023586237]","coupling_orders":[],"id":209,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"23","lorentz_source":"built-in","particles":["z","ta-","ta-"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_209"},{"backend":"built-in","color_expression":"built-in","color_index":0,"color_source":"built-in","contact_orbit_certificate":null,"coupling":"builtin_coupling_210","coupling_expression":"[-0.6680686926882882, 0.5345600023586237]","coupling_orders":[],"id":210,"index_bindings":[],"lc_color_normalization_power":0,"lorentz_expression":"built-in","lorentz_index":0,"lorentz_name":"24","lorentz_source":"built-in","particles":["z","ta+","ta+"],"source_ordering_ids":[],"valence":3,"vertex":"builtin_vertex_210"}]},"issues":[],"kind":"pyamplicol-compiled-model","model":{"builtin_model":true,"closure_contractions":[{"chirality":0,"contraction_ir":{"chirality_relation":"any","coefficients":[[1.0,0.0]],"left_basis":"scalar","metric_signature":null,"name":"scalar","right_basis":"scalar"},"particle":"h"},{"chirality":0,"contraction_ir":{"chirality_relation":"any","coefficients":[[1.0,0.0]],"left_basis":"scalar","metric_signature":null,"name":"scalar","right_basis":"scalar"},"particle":"pdg_125"},{"chirality":0,"contraction_ir":{"chirality_relation":"any","coefficients":[[1.0,0.0]],"left_basis":"scalar","metric_signature":null,"name":"scalar","right_basis":"scalar"},"particle":"pdg_126"},{"chirality":0,"contraction_ir":{"chirality_relation":"any","coefficients":[[1.0,0.0]],"left_basis":"scalar","metric_signature":null,"name":"scalar","right_basis":"scalar"},"particle":"pdg_127"}],"couplings":[],"direct_contractions":[{"contraction_ir":{"chirality_relation":"any","coefficients":[[1.0,0.0],[1.0,0.0],[1.0,0.0],[1.0,0.0],[1.0,0.0],[1.0,0.0]],"left_basis":"auxiliary:antisymmetric-tensor","metric_signature":null,"name":"antisymmetric-tensor","right_basis":"auxiliary:antisymmetric-tensor"},"left_chirality":0,"left_particle":"pdg_-26","right_chirality":0,"right_particle":"pdg_26"},{"contraction_ir":{"chirality_relation":"any","coefficients":[[1.0,0.0],[-1.0,0.0],[-1.0,0.0],[-1.0,0.0]],"left_basis":"lorentz-vector","metric_signature":"mostly-minus","name":"lorentz","right_basis":"lorentz-vector"},"left_chirality":0,"left_particle":"w-","right_chirality":0,"right_particle":"w+"},{"contraction_ir":{"chirality_relation":"any","coefficients":[[1.0,0.0],[1.0,0.0],[1.0,0.0],[1.0,0.0],[1.0,0.0],[1.0,0.0]],"left_basis":"auxiliary:antisymmetric-tensor","metric_signature":null,"name":"antisymmetric-tensor","right_basis":"auxiliary:antisymmetric-tensor"},"left_chirality":0,"left_particle":"pdg_-23","right_chirality":0,"right_particle":"pdg_-23"},{"contraction_ir":{"chirality_relation":"any","coefficients":[[1.0,0.0],[1.0,0.0],[1.0,0.0],[1.0,0.0],[1.0,0.0],[1.0,0.0]],"left_basis":"auxiliary:antisymmetric-tensor","metric_signature":null,"name":"antisymmetric-tensor","right_basis":"auxiliary:antisymmetric-tensor"},"left_chirality":0,"left_particle":"pdg_-21","right_chirality":0,"right_particle":"pdg_-21"},{"contraction_ir":{"chirality_relation":"opposite","coefficients":[[1.0,0.0],[1.0,0.0]],"left_basis":"weyl-chiral","metric_signature":null,"name":"weyl","right_basis":"weyl-chiral"},"left_chirality":-1,"left_particle":"vt~","right_chirality":1,"right_particle":"vt"},{"contraction_ir":{"chirality_relation":"any","coefficients":[[1.0,0.0],[1.0,0.0],[1.0,0.0],[1.0,0.0]],"left_basis":"dirac","metric_signature":null,"name":"dirac","right_basis":"dirac"},"left_chirality":0,"left_particle":"vt~","right_chirality":0,"right_particle":"vt"},{"contraction_ir":{"chirality_relation":"opposite","coefficients":[[1.0,0.0],[1.0,0.0]],"left_basis":"weyl-chiral","metric_signature":null,"name":"weyl","right_basis":"weyl-chiral"},"left_chirality":1,"left_particle":"vt~","right_chirality":-1,"right_particle":"vt"},{"contraction_ir":{"chirality_relation":"opposite","coefficients":[[1.0,0.0],[1.0,0.0]],"left_basis":"weyl-chiral","metric_signature":null,"name":"weyl","right_basis":"weyl-chiral"},"left_chirality":-1,"left_particle":"ta+","right_chirality":1,"right_particle":"ta-"},{"contraction_ir":{"chirality_relation":"any","coefficients":[[1.0,0.0],[1.0,0.0],[1.0,0.0],[1.0,0.0]],"left_basis":"dirac","metric_signature":null,"name":"dirac","right_basis":"dirac"},"left_chirality":0,"left_particle":"ta+","right_chirality":0,"right_particle":"ta-"},{"contraction_ir":{"chirality_relation":"opposite","coefficients":[[1.0,0.0],[1.0,0.0]],"left_basis":"weyl-chiral","metric_signature":null,"name":"weyl","right_basis":"weyl-chiral"},"left_chirality":1,"left_particle":"ta+","right_chirality":-1,"right_particle":"ta-"},{"contraction_ir":{"chirality_relation":"opposite","coefficients":[[1.0,0.0],[1.0,0.0]],"left_basis":"weyl-chiral","metric_signature":null,"name":"weyl","right_basis":"weyl-chiral"},"left_chirality":-1,"left_particle":"vm~","right_chirality":1,"right_particle":"vm"},{"contraction_ir":{"chirality_relation":"any","coefficients":[[1.0,0.0],[1.0,0.0],[1.0,0.0],[1.0,0.0]],"left_basis":"dirac","metric_signature":null,"name":"dirac","right_basis":"dirac"},"left_chirality":0,"left_particle":"vm~","right_chirality":0,"right_particle":"vm"},{"contraction_ir":{"chirality_relation":"opposite","coefficients":[[1.0,0.0],[1.0,0.0]],"left_basis":"weyl-chiral","metric_signature":null,"name":"weyl","right_basis":"weyl-chiral"},"left_chirality":1,"left_particle":"vm~","right_chirality":-1,"right_particle":"vm"},{"contraction_ir":{"chirality_relation":"opposite","coefficients":[[1.0,0.0],[1.0,0.0]],"left_basis":"weyl-chiral","metric_signature":null,"name":"weyl","right_basis":"weyl-chiral"},"left_chirality":-1,"left_particle":"mu+","right_chirality":1,"right_particle":"mu-"},{"contraction_ir":{"chirality_relation":"any","coefficients":[[1.0,0.0],[1.0,0.0],[1.0,0.0],[1.0,0.0]],"left_basis":"dirac","metric_signature":null,"name":"dirac","right_basis":"dirac"},"left_chirality":0,"left_particle":"mu+","right_chirality":0,"right_particle":"mu-"},{"contraction_ir":{"chirality_relation":"opposite","coefficients":[[1.0,0.0],[1.0,0.0]],"left_basis":"weyl-chiral","metric_signature":null,"name":"weyl","right_basis":"weyl-chiral"},"left_chirality":1,"left_particle":"mu+","right_chirality":-1,"right_particle":"mu-"},{"contraction_ir":{"chirality_relation":"opposite","coefficients":[[1.0,0.0],[1.0,0.0]],"left_basis":"weyl-chiral","metric_signature":null,"name":"weyl","right_basis":"weyl-chiral"},"left_chirality":-1,"left_particle":"ve~","right_chirality":1,"right_particle":"ve"},{"contraction_ir":{"chirality_relation":"any","coefficients":[[1.0,0.0],[1.0,0.0],[1.0,0.0],[1.0,0.0]],"left_basis":"dirac","metric_signature":null,"name":"dirac","right_basis":"dirac"},"left_chirality":0,"left_particle":"ve~","right_chirality":0,"right_particle":"ve"},{"contraction_ir":{"chirality_relation":"opposite","coefficients":[[1.0,0.0],[1.0,0.0]],"left_basis":"weyl-chiral","metric_signature":null,"name":"weyl","right_basis":"weyl-chiral"},"left_chirality":1,"left_particle":"ve~","right_chirality":-1,"right_particle":"ve"},{"contraction_ir":{"chirality_relation":"opposite","coefficients":[[1.0,0.0],[1.0,0.0]],"left_basis":"weyl-chiral","metric_signature":null,"name":"weyl","right_basis":"weyl-chiral"},"left_chirality":-1,"left_particle":"e+","right_chirality":1,"right_particle":"e-"},{"contraction_ir":{"chirality_relation":"any","coefficients":[[1.0,0.0],[1.0,0.0],[1.0,0.0],[1.0,0.0]],"left_basis":"dirac","metric_signature":null,"name":"dirac","right_basis":"dirac"},"left_chirality":0,"left_particle":"e+","right_chirality":0,"right_particle":"e-"},{"contraction_ir":{"chirality_relation":"opposite","coefficients":[[1.0,0.0],[1.0,0.0]],"left_basis":"weyl-chiral","metric_signature":null,"name":"weyl","right_basis":"weyl-chiral"},"left_chirality":1,"left_particle":"e+","right_chirality":-1,"right_particle":"e-"},{"contraction_ir":{"chirality_relation":"any","coefficients":[[1.0,0.0],[1.0,0.0],[1.0,0.0],[1.0,0.0]],"left_basis":"dirac","metric_signature":null,"name":"dirac","right_basis":"dirac"},"left_chirality":0,"left_particle":"t~","right_chirality":0,"right_particle":"t"},{"contraction_ir":{"chirality_relation":"opposite","coefficients":[[1.0,0.0],[1.0,0.0]],"left_basis":"weyl-chiral","metric_signature":null,"name":"weyl","right_basis":"weyl-chiral"},"left_chirality":-1,"left_particle":"b~","right_chirality":1,"right_particle":"b"},{"contraction_ir":{"chirality_relation":"any","coefficients":[[1.0,0.0],[1.0,0.0],[1.0,0.0],[1.0,0.0]],"left_basis":"dirac","metric_signature":null,"name":"dirac","right_basis":"dirac"},"left_chirality":0,"left_particle":"b~","right_chirality":0,"right_particle":"b"},{"contraction_ir":{"chirality_relation":"opposite","coefficients":[[1.0,0.0],[1.0,0.0]],"left_basis":"weyl-chiral","metric_signature":null,"name":"weyl","right_basis":"weyl-chiral"},"left_chirality":1,"left_particle":"b~","right_chirality":-1,"right_particle":"b"},{"contraction_ir":{"chirality_relation":"opposite","coefficients":[[1.0,0.0],[1.0,0.0]],"left_basis":"weyl-chiral","metric_signature":null,"name":"weyl","right_basis":"weyl-chiral"},"left_chirality":-1,"left_particle":"c~","right_chirality":1,"right_particle":"c"},{"contraction_ir":{"chirality_relation":"any","coefficients":[[1.0,0.0],[1.0,0.0],[1.0,0.0],[1.0,0.0]],"left_basis":"dirac","metric_signature":null,"name":"dirac","right_basis":"dirac"},"left_chirality":0,"left_particle":"c~","right_chirality":0,"right_particle":"c"},{"contraction_ir":{"chirality_relation":"opposite","coefficients":[[1.0,0.0],[1.0,0.0]],"left_basis":"weyl-chiral","metric_signature":null,"name":"weyl","right_basis":"weyl-chiral"},"left_chirality":1,"left_particle":"c~","right_chirality":-1,"right_particle":"c"},{"contraction_ir":{"chirality_relation":"opposite","coefficients":[[1.0,0.0],[1.0,0.0]],"left_basis":"weyl-chiral","metric_signature":null,"name":"weyl","right_basis":"weyl-chiral"},"left_chirality":-1,"left_particle":"s~","right_chirality":1,"right_particle":"s"},{"contraction_ir":{"chirality_relation":"any","coefficients":[[1.0,0.0],[1.0,0.0],[1.0,0.0],[1.0,0.0]],"left_basis":"dirac","metric_signature":null,"name":"dirac","right_basis":"dirac"},"left_chirality":0,"left_particle":"s~","right_chirality":0,"right_particle":"s"},{"contraction_ir":{"chirality_relation":"opposite","coefficients":[[1.0,0.0],[1.0,0.0]],"left_basis":"weyl-chiral","metric_signature":null,"name":"weyl","right_basis":"weyl-chiral"},"left_chirality":1,"left_particle":"s~","right_chirality":-1,"right_particle":"s"},{"contraction_ir":{"chirality_relation":"opposite","coefficients":[[1.0,0.0],[1.0,0.0]],"left_basis":"weyl-chiral","metric_signature":null,"name":"weyl","right_basis":"weyl-chiral"},"left_chirality":-1,"left_particle":"u~","right_chirality":1,"right_particle":"u"},{"contraction_ir":{"chirality_relation":"any","coefficients":[[1.0,0.0],[1.0,0.0],[1.0,0.0],[1.0,0.0]],"left_basis":"dirac","metric_signature":null,"name":"dirac","right_basis":"dirac"},"left_chirality":0,"left_particle":"u~","right_chirality":0,"right_particle":"u"},{"contraction_ir":{"chirality_relation":"opposite","coefficients":[[1.0,0.0],[1.0,0.0]],"left_basis":"weyl-chiral","metric_signature":null,"name":"weyl","right_basis":"weyl-chiral"},"left_chirality":1,"left_particle":"u~","right_chirality":-1,"right_particle":"u"},{"contraction_ir":{"chirality_relation":"opposite","coefficients":[[1.0,0.0],[1.0,0.0]],"left_basis":"weyl-chiral","metric_signature":null,"name":"weyl","right_basis":"weyl-chiral"},"left_chirality":-1,"left_particle":"d~","right_chirality":1,"right_particle":"d"},{"contraction_ir":{"chirality_relation":"any","coefficients":[[1.0,0.0],[1.0,0.0],[1.0,0.0],[1.0,0.0]],"left_basis":"dirac","metric_signature":null,"name":"dirac","right_basis":"dirac"},"left_chirality":0,"left_particle":"d~","right_chirality":0,"right_particle":"d"},{"contraction_ir":{"chirality_relation":"opposite","coefficients":[[1.0,0.0],[1.0,0.0]],"left_basis":"weyl-chiral","metric_signature":null,"name":"weyl","right_basis":"weyl-chiral"},"left_chirality":1,"left_particle":"d~","right_chirality":-1,"right_particle":"d"},{"contraction_ir":{"chirality_relation":"opposite","coefficients":[[1.0,0.0],[1.0,0.0]],"left_basis":"weyl-chiral","metric_signature":null,"name":"weyl","right_basis":"weyl-chiral"},"left_chirality":-1,"left_particle":"d","right_chirality":1,"right_particle":"d~"},{"contraction_ir":{"chirality_relation":"any","coefficients":[[1.0,0.0],[1.0,0.0],[1.0,0.0],[1.0,0.0]],"left_basis":"dirac","metric_signature":null,"name":"dirac","right_basis":"dirac"},"left_chirality":0,"left_particle":"d","right_chirality":0,"right_particle":"d~"},{"contraction_ir":{"chirality_relation":"opposite","coefficients":[[1.0,0.0],[1.0,0.0]],"left_basis":"weyl-chiral","metric_signature":null,"name":"weyl","right_basis":"weyl-chiral"},"left_chirality":1,"left_particle":"d","right_chirality":-1,"right_particle":"d~"},{"contraction_ir":{"chirality_relation":"opposite","coefficients":[[1.0,0.0],[1.0,0.0]],"left_basis":"weyl-chiral","metric_signature":null,"name":"weyl","right_basis":"weyl-chiral"},"left_chirality":-1,"left_particle":"u","right_chirality":1,"right_particle":"u~"},{"contraction_ir":{"chirality_relation":"any","coefficients":[[1.0,0.0],[1.0,0.0],[1.0,0.0],[1.0,0.0]],"left_basis":"dirac","metric_signature":null,"name":"dirac","right_basis":"dirac"},"left_chirality":0,"left_particle":"u","right_chirality":0,"right_particle":"u~"},{"contraction_ir":{"chirality_relation":"opposite","coefficients":[[1.0,0.0],[1.0,0.0]],"left_basis":"weyl-chiral","metric_signature":null,"name":"weyl","right_basis":"weyl-chiral"},"left_chirality":1,"left_particle":"u","right_chirality":-1,"right_particle":"u~"},{"contraction_ir":{"chirality_relation":"opposite","coefficients":[[1.0,0.0],[1.0,0.0]],"left_basis":"weyl-chiral","metric_signature":null,"name":"weyl","right_basis":"weyl-chiral"},"left_chirality":-1,"left_particle":"s","right_chirality":1,"right_particle":"s~"},{"contraction_ir":{"chirality_relation":"any","coefficients":[[1.0,0.0],[1.0,0.0],[1.0,0.0],[1.0,0.0]],"left_basis":"dirac","metric_signature":null,"name":"dirac","right_basis":"dirac"},"left_chirality":0,"left_particle":"s","right_chirality":0,"right_particle":"s~"},{"contraction_ir":{"chirality_relation":"opposite","coefficients":[[1.0,0.0],[1.0,0.0]],"left_basis":"weyl-chiral","metric_signature":null,"name":"weyl","right_basis":"weyl-chiral"},"left_chirality":1,"left_particle":"s","right_chirality":-1,"right_particle":"s~"},{"contraction_ir":{"chirality_relation":"opposite","coefficients":[[1.0,0.0],[1.0,0.0]],"left_basis":"weyl-chiral","metric_signature":null,"name":"weyl","right_basis":"weyl-chiral"},"left_chirality":-1,"left_particle":"c","right_chirality":1,"right_particle":"c~"},{"contraction_ir":{"chirality_relation":"any","coefficients":[[1.0,0.0],[1.0,0.0],[1.0,0.0],[1.0,0.0]],"left_basis":"dirac","metric_signature":null,"name":"dirac","right_basis":"dirac"},"left_chirality":0,"left_particle":"c","right_chirality":0,"right_particle":"c~"},{"contraction_ir":{"chirality_relation":"opposite","coefficients":[[1.0,0.0],[1.0,0.0]],"left_basis":"weyl-chiral","metric_signature":null,"name":"weyl","right_basis":"weyl-chiral"},"left_chirality":1,"left_particle":"c","right_chirality":-1,"right_particle":"c~"},{"contraction_ir":{"chirality_relation":"opposite","coefficients":[[1.0,0.0],[1.0,0.0]],"left_basis":"weyl-chiral","metric_signature":null,"name":"weyl","right_basis":"weyl-chiral"},"left_chirality":-1,"left_particle":"b","right_chirality":1,"right_particle":"b~"},{"contraction_ir":{"chirality_relation":"any","coefficients":[[1.0,0.0],[1.0,0.0],[1.0,0.0],[1.0,0.0]],"left_basis":"dirac","metric_signature":null,"name":"dirac","right_basis":"dirac"},"left_chirality":0,"left_particle":"b","right_chirality":0,"right_particle":"b~"},{"contraction_ir":{"chirality_relation":"opposite","coefficients":[[1.0,0.0],[1.0,0.0]],"left_basis":"weyl-chiral","metric_signature":null,"name":"weyl","right_basis":"weyl-chiral"},"left_chirality":1,"left_particle":"b","right_chirality":-1,"right_particle":"b~"},{"contraction_ir":{"chirality_relation":"any","coefficients":[[1.0,0.0],[1.0,0.0],[1.0,0.0],[1.0,0.0]],"left_basis":"dirac","metric_signature":null,"name":"dirac","right_basis":"dirac"},"left_chirality":0,"left_particle":"t","right_chirality":0,"right_particle":"t~"},{"contraction_ir":{"chirality_relation":"opposite","coefficients":[[1.0,0.0],[1.0,0.0]],"left_basis":"weyl-chiral","metric_signature":null,"name":"weyl","right_basis":"weyl-chiral"},"left_chirality":-1,"left_particle":"e-","right_chirality":1,"right_particle":"e+"},{"contraction_ir":{"chirality_relation":"any","coefficients":[[1.0,0.0],[1.0,0.0],[1.0,0.0],[1.0,0.0]],"left_basis":"dirac","metric_signature":null,"name":"dirac","right_basis":"dirac"},"left_chirality":0,"left_particle":"e-","right_chirality":0,"right_particle":"e+"},{"contraction_ir":{"chirality_relation":"opposite","coefficients":[[1.0,0.0],[1.0,0.0]],"left_basis":"weyl-chiral","metric_signature":null,"name":"weyl","right_basis":"weyl-chiral"},"left_chirality":1,"left_particle":"e-","right_chirality":-1,"right_particle":"e+"},{"contraction_ir":{"chirality_relation":"opposite","coefficients":[[1.0,0.0],[1.0,0.0]],"left_basis":"weyl-chiral","metric_signature":null,"name":"weyl","right_basis":"weyl-chiral"},"left_chirality":-1,"left_particle":"ve","right_chirality":1,"right_particle":"ve~"},{"contraction_ir":{"chirality_relation":"any","coefficients":[[1.0,0.0],[1.0,0.0],[1.0,0.0],[1.0,0.0]],"left_basis":"dirac","metric_signature":null,"name":"dirac","right_basis":"dirac"},"left_chirality":0,"left_particle":"ve","right_chirality":0,"right_particle":"ve~"},{"contraction_ir":{"chirality_relation":"opposite","coefficients":[[1.0,0.0],[1.0,0.0]],"left_basis":"weyl-chiral","metric_signature":null,"name":"weyl","right_basis":"weyl-chiral"},"left_chirality":1,"left_particle":"ve","right_chirality":-1,"right_particle":"ve~"},{"contraction_ir":{"chirality_relation":"opposite","coefficients":[[1.0,0.0],[1.0,0.0]],"left_basis":"weyl-chiral","metric_signature":null,"name":"weyl","right_basis":"weyl-chiral"},"left_chirality":-1,"left_particle":"mu-","right_chirality":1,"right_particle":"mu+"},{"contraction_ir":{"chirality_relation":"any","coefficients":[[1.0,0.0],[1.0,0.0],[1.0,0.0],[1.0,0.0]],"left_basis":"dirac","metric_signature":null,"name":"dirac","right_basis":"dirac"},"left_chirality":0,"left_particle":"mu-","right_chirality":0,"right_particle":"mu+"},{"contraction_ir":{"chirality_relation":"opposite","coefficients":[[1.0,0.0],[1.0,0.0]],"left_basis":"weyl-chiral","metric_signature":null,"name":"weyl","right_basis":"weyl-chiral"},"left_chirality":1,"left_particle":"mu-","right_chirality":-1,"right_particle":"mu+"},{"contraction_ir":{"chirality_relation":"opposite","coefficients":[[1.0,0.0],[1.0,0.0]],"left_basis":"weyl-chiral","metric_signature":null,"name":"weyl","right_basis":"weyl-chiral"},"left_chirality":-1,"left_particle":"vm","right_chirality":1,"right_particle":"vm~"},{"contraction_ir":{"chirality_relation":"any","coefficients":[[1.0,0.0],[1.0,0.0],[1.0,0.0],[1.0,0.0]],"left_basis":"dirac","metric_signature":null,"name":"dirac","right_basis":"dirac"},"left_chirality":0,"left_particle":"vm","right_chirality":0,"right_particle":"vm~"},{"contraction_ir":{"chirality_relation":"opposite","coefficients":[[1.0,0.0],[1.0,0.0]],"left_basis":"weyl-chiral","metric_signature":null,"name":"weyl","right_basis":"weyl-chiral"},"left_chirality":1,"left_particle":"vm","right_chirality":-1,"right_particle":"vm~"},{"contraction_ir":{"chirality_relation":"opposite","coefficients":[[1.0,0.0],[1.0,0.0]],"left_basis":"weyl-chiral","metric_signature":null,"name":"weyl","right_basis":"weyl-chiral"},"left_chirality":-1,"left_particle":"ta-","right_chirality":1,"right_particle":"ta+"},{"contraction_ir":{"chirality_relation":"any","coefficients":[[1.0,0.0],[1.0,0.0],[1.0,0.0],[1.0,0.0]],"left_basis":"dirac","metric_signature":null,"name":"dirac","right_basis":"dirac"},"left_chirality":0,"left_particle":"ta-","right_chirality":0,"right_particle":"ta+"},{"contraction_ir":{"chirality_relation":"opposite","coefficients":[[1.0,0.0],[1.0,0.0]],"left_basis":"weyl-chiral","metric_signature":null,"name":"weyl","right_basis":"weyl-chiral"},"left_chirality":1,"left_particle":"ta-","right_chirality":-1,"right_particle":"ta+"},{"contraction_ir":{"chirality_relation":"opposite","coefficients":[[1.0,0.0],[1.0,0.0]],"left_basis":"weyl-chiral","metric_signature":null,"name":"weyl","right_basis":"weyl-chiral"},"left_chirality":-1,"left_particle":"vt","right_chirality":1,"right_particle":"vt~"},{"contraction_ir":{"chirality_relation":"any","coefficients":[[1.0,0.0],[1.0,0.0],[1.0,0.0],[1.0,0.0]],"left_basis":"dirac","metric_signature":null,"name":"dirac","right_basis":"dirac"},"left_chirality":0,"left_particle":"vt","right_chirality":0,"right_particle":"vt~"},{"contraction_ir":{"chirality_relation":"opposite","coefficients":[[1.0,0.0],[1.0,0.0]],"left_basis":"weyl-chiral","metric_signature":null,"name":"weyl","right_basis":"weyl-chiral"},"left_chirality":1,"left_particle":"vt","right_chirality":-1,"right_particle":"vt~"},{"contraction_ir":{"chirality_relation":"any","coefficients":[[1.0,0.0],[-1.0,0.0],[-1.0,0.0],[-1.0,0.0]],"left_basis":"lorentz-vector","metric_signature":"mostly-minus","name":"lorentz","right_basis":"lorentz-vector"},"left_chirality":0,"left_particle":"g","right_chirality":0,"right_particle":"g"},{"contraction_ir":{"chirality_relation":"any","coefficients":[[1.0,0.0],[-1.0,0.0],[-1.0,0.0],[-1.0,0.0]],"left_basis":"lorentz-vector","metric_signature":"mostly-minus","name":"lorentz","right_basis":"lorentz-vector"},"left_chirality":0,"left_particle":"a","right_chirality":0,"right_particle":"a"},{"contraction_ir":{"chirality_relation":"any","coefficients":[[1.0,0.0],[-1.0,0.0],[-1.0,0.0],[-1.0,0.0]],"left_basis":"lorentz-vector","metric_signature":"mostly-minus","name":"lorentz","right_basis":"lorentz-vector"},"left_chirality":0,"left_particle":"z","right_chirality":0,"right_particle":"z"},{"contraction_ir":{"chirality_relation":"any","coefficients":[[1.0,0.0],[-1.0,0.0],[-1.0,0.0],[-1.0,0.0]],"left_basis":"lorentz-vector","metric_signature":"mostly-minus","name":"lorentz","right_basis":"lorentz-vector"},"left_chirality":0,"left_particle":"w+","right_chirality":0,"right_particle":"w-"},{"contraction_ir":{"chirality_relation":"any","coefficients":[[1.0,0.0]],"left_basis":"scalar","metric_signature":null,"name":"scalar","right_basis":"scalar"},"left_chirality":0,"left_particle":"h","right_chirality":0,"right_particle":"h"},{"contraction_ir":{"chirality_relation":"any","coefficients":[[1.0,0.0],[1.0,0.0],[1.0,0.0],[1.0,0.0],[1.0,0.0],[1.0,0.0]],"left_basis":"auxiliary:antisymmetric-tensor","metric_signature":null,"name":"antisymmetric-tensor","right_basis":"auxiliary:antisymmetric-tensor"},"left_chirality":0,"left_particle":"pdg_26","right_chirality":0,"right_particle":"pdg_-26"},{"contraction_ir":{"chirality_relation":"any","coefficients":[[1.0,0.0],[-1.0,0.0],[-1.0,0.0],[-1.0,0.0]],"left_basis":"auxiliary:u1-subtraction-color-flow-vector","metric_signature":"mostly-minus","name":"lorentz","right_basis":"auxiliary:u1-subtraction-color-flow-vector"},"left_chirality":0,"left_particle":"pdg_99","right_chirality":0,"right_particle":"pdg_99"},{"contraction_ir":{"chirality_relation":"any","coefficients":[[1.0,0.0]],"left_basis":"scalar","metric_signature":null,"name":"scalar","right_basis":"scalar"},"left_chirality":0,"left_particle":"pdg_125","right_chirality":0,"right_particle":"pdg_125"},{"contraction_ir":{"chirality_relation":"any","coefficients":[[1.0,0.0]],"left_basis":"scalar","metric_signature":null,"name":"scalar","right_basis":"scalar"},"left_chirality":0,"left_particle":"pdg_126","right_chirality":0,"right_particle":"pdg_126"},{"contraction_ir":{"chirality_relation":"any","coefficients":[[1.0,0.0]],"left_basis":"scalar","metric_signature":null,"name":"scalar","right_basis":"scalar"},"left_chirality":0,"left_particle":"pdg_127","right_chirality":0,"right_particle":"pdg_127"}],"form_factors":[],"functions":[],"lorentz_structures":[],"name":"built-in-sm","orders":[{"expansion_order":99,"hierarchy":1,"name":"QCD"},{"expansion_order":99,"hierarchy":2,"name":"QED"}],"parameters":[{"expression":null,"lhablock":"PYAMPLICOL","lhacode":[1],"name":"alpha_ew","nature":"external","parameter_type":"real","value":[0.0075467711139788835,0.0]},{"expression":null,"lhablock":"PYAMPLICOL","lhacode":[2],"name":"alpha_s","nature":"external","parameter_type":"real","value":[0.118,0.0]},{"expression":null,"lhablock":"PYAMPLICOL","lhacode":[3],"name":"mass_23","nature":"external","parameter_type":"real","value":[91.188,0.0]},{"expression":null,"lhablock":"PYAMPLICOL","lhacode":[4],"name":"mass_24","nature":"external","parameter_type":"real","value":[80.41900244575616,0.0]},{"expression":null,"lhablock":"PYAMPLICOL","lhacode":[5],"name":"mass_25","nature":"external","parameter_type":"real","value":[125.0,0.0]},{"expression":null,"lhablock":"PYAMPLICOL","lhacode":[6],"name":"mass_6","nature":"external","parameter_type":"real","value":[173.0,0.0]},{"expression":null,"lhablock":"PYAMPLICOL","lhacode":[7],"name":"width_23","nature":"external","parameter_type":"real","value":[2.441404,0.0]},{"expression":null,"lhablock":"PYAMPLICOL","lhacode":[8],"name":"width_24","nature":"external","parameter_type":"real","value":[2.0476,0.0]},{"expression":null,"lhablock":"PYAMPLICOL","lhacode":[9],"name":"width_25","nature":"external","parameter_type":"real","value":[0.006382339,0.0]},{"expression":null,"lhablock":"PYAMPLICOL","lhacode":[10],"name":"width_6","nature":"external","parameter_type":"real","value":[1.4915,0.0]}],"particles":[{"antiname":"pdg_-23","antitexname":"pdg_-23","auxiliary_kind":"antisymmetric-tensor","charge":0.0,"color":1,"component_dimension":6,"ghost_number":0,"goldstoneboson":false,"lepton_number":0,"mass":"ZERO","name":"pdg_-23","pdg_code":-23,"propagating":false,"propagator":null,"quantum_numbers":[["electric_charge","0"]],"spin":-1,"texname":"pdg_-23","width":"ZERO","y_charge":0},{"antiname":"pdg_-21","antitexname":"pdg_-21","auxiliary_kind":"antisymmetric-tensor","charge":0.0,"color":8,"component_dimension":6,"ghost_number":0,"goldstoneboson":false,"lepton_number":0,"mass":"ZERO","name":"pdg_-21","pdg_code":-21,"propagating":false,"propagator":null,"quantum_numbers":[["electric_charge","0"]],"spin":-1,"texname":"pdg_-21","width":"ZERO","y_charge":0},{"antiname":"d~","antitexname":"d~","auxiliary_kind":null,"charge":-0.3333333333333333,"color":3,"component_dimension":4,"ghost_number":0,"goldstoneboson":false,"lepton_number":0,"mass":"ZERO","name":"d","pdg_code":1,"propagating":true,"propagator":null,"quantum_numbers":[["electric_charge","-1/3"]],"spin":2,"texname":"d","width":"ZERO","y_charge":0},{"antiname":"d","antitexname":"d","auxiliary_kind":null,"charge":0.3333333333333333,"color":-3,"component_dimension":4,"ghost_number":0,"goldstoneboson":false,"lepton_number":0,"mass":"ZERO","name":"d~","pdg_code":-1,"propagating":true,"propagator":null,"quantum_numbers":[["electric_charge","1/3"]],"spin":2,"texname":"d~","width":"ZERO","y_charge":0},{"antiname":"u~","antitexname":"u~","auxiliary_kind":null,"charge":0.6666666666666666,"color":3,"component_dimension":4,"ghost_number":0,"goldstoneboson":false,"lepton_number":0,"mass":"ZERO","name":"u","pdg_code":2,"propagating":true,"propagator":null,"quantum_numbers":[["electric_charge","2/3"]],"spin":2,"texname":"u","width":"ZERO","y_charge":0},{"antiname":"u","antitexname":"u","auxiliary_kind":null,"charge":-0.6666666666666666,"color":-3,"component_dimension":4,"ghost_number":0,"goldstoneboson":false,"lepton_number":0,"mass":"ZERO","name":"u~","pdg_code":-2,"propagating":true,"propagator":null,"quantum_numbers":[["electric_charge","-2/3"]],"spin":2,"texname":"u~","width":"ZERO","y_charge":0},{"antiname":"s~","antitexname":"s~","auxiliary_kind":null,"charge":-0.3333333333333333,"color":3,"component_dimension":4,"ghost_number":0,"goldstoneboson":false,"lepton_number":0,"mass":"ZERO","name":"s","pdg_code":3,"propagating":true,"propagator":null,"quantum_numbers":[["electric_charge","-1/3"]],"spin":2,"texname":"s","width":"ZERO","y_charge":0},{"antiname":"s","antitexname":"s","auxiliary_kind":null,"charge":0.3333333333333333,"color":-3,"component_dimension":4,"ghost_number":0,"goldstoneboson":false,"lepton_number":0,"mass":"ZERO","name":"s~","pdg_code":-3,"propagating":true,"propagator":null,"quantum_numbers":[["electric_charge","1/3"]],"spin":2,"texname":"s~","width":"ZERO","y_charge":0},{"antiname":"c~","antitexname":"c~","auxiliary_kind":null,"charge":0.6666666666666666,"color":3,"component_dimension":4,"ghost_number":0,"goldstoneboson":false,"lepton_number":0,"mass":"ZERO","name":"c","pdg_code":4,"propagating":true,"propagator":null,"quantum_numbers":[["electric_charge","2/3"]],"spin":2,"texname":"c","width":"ZERO","y_charge":0},{"antiname":"c","antitexname":"c","auxiliary_kind":null,"charge":-0.6666666666666666,"color":-3,"component_dimension":4,"ghost_number":0,"goldstoneboson":false,"lepton_number":0,"mass":"ZERO","name":"c~","pdg_code":-4,"propagating":true,"propagator":null,"quantum_numbers":[["electric_charge","-2/3"]],"spin":2,"texname":"c~","width":"ZERO","y_charge":0},{"antiname":"b~","antitexname":"b~","auxiliary_kind":null,"charge":-0.3333333333333333,"color":3,"component_dimension":4,"ghost_number":0,"goldstoneboson":false,"lepton_number":0,"mass":"ZERO","name":"b","pdg_code":5,"propagating":true,"propagator":null,"quantum_numbers":[["electric_charge","-1/3"]],"spin":2,"texname":"b","width":"ZERO","y_charge":0},{"antiname":"b","antitexname":"b","auxiliary_kind":null,"charge":0.3333333333333333,"color":-3,"component_dimension":4,"ghost_number":0,"goldstoneboson":false,"lepton_number":0,"mass":"ZERO","name":"b~","pdg_code":-5,"propagating":true,"propagator":null,"quantum_numbers":[["electric_charge","1/3"]],"spin":2,"texname":"b~","width":"ZERO","y_charge":0},{"antiname":"t~","antitexname":"t~","auxiliary_kind":null,"charge":0.6666666666666666,"color":3,"component_dimension":4,"ghost_number":0,"goldstoneboson":false,"lepton_number":0,"mass":"mass_6","name":"t","pdg_code":6,"propagating":true,"propagator":null,"quantum_numbers":[["electric_charge","2/3"]],"spin":2,"texname":"t","width":"width_6","y_charge":0},{"antiname":"t","antitexname":"t","auxiliary_kind":null,"charge":-0.6666666666666666,"color":-3,"component_dimension":4,"ghost_number":0,"goldstoneboson":false,"lepton_number":0,"mass":"mass_6","name":"t~","pdg_code":-6,"propagating":true,"propagator":null,"quantum_numbers":[["electric_charge","-2/3"]],"spin":2,"texname":"t~","width":"width_6","y_charge":0},{"antiname":"e+","antitexname":"e+","auxiliary_kind":null,"charge":-1.0,"color":1,"component_dimension":4,"ghost_number":0,"goldstoneboson":false,"lepton_number":0,"mass":"ZERO","name":"e-","pdg_code":11,"propagating":true,"propagator":null,"quantum_numbers":[["electric_charge","-1"]],"spin":2,"texname":"e-","width":"ZERO","y_charge":0},{"antiname":"e-","antitexname":"e-","auxiliary_kind":null,"charge":1.0,"color":1,"component_dimension":4,"ghost_number":0,"goldstoneboson":false,"lepton_number":0,"mass":"ZERO","name":"e+","pdg_code":-11,"propagating":true,"propagator":null,"quantum_numbers":[["electric_charge","1"]],"spin":2,"texname":"e+","width":"ZERO","y_charge":0},{"antiname":"ve~","antitexname":"ve~","auxiliary_kind":null,"charge":0.0,"color":1,"component_dimension":4,"ghost_number":0,"goldstoneboson":false,"lepton_number":0,"mass":"ZERO","name":"ve","pdg_code":12,"propagating":true,"propagator":null,"quantum_numbers":[["electric_charge","0"]],"spin":2,"texname":"ve","width":"ZERO","y_charge":0},{"antiname":"ve","antitexname":"ve","auxiliary_kind":null,"charge":-0.0,"color":1,"component_dimension":4,"ghost_number":0,"goldstoneboson":false,"lepton_number":0,"mass":"ZERO","name":"ve~","pdg_code":-12,"propagating":true,"propagator":null,"quantum_numbers":[["electric_charge","0"]],"spin":2,"texname":"ve~","width":"ZERO","y_charge":0},{"antiname":"mu+","antitexname":"mu+","auxiliary_kind":null,"charge":-1.0,"color":1,"component_dimension":4,"ghost_number":0,"goldstoneboson":false,"lepton_number":0,"mass":"ZERO","name":"mu-","pdg_code":13,"propagating":true,"propagator":null,"quantum_numbers":[["electric_charge","-1"]],"spin":2,"texname":"mu-","width":"ZERO","y_charge":0},{"antiname":"mu-","antitexname":"mu-","auxiliary_kind":null,"charge":1.0,"color":1,"component_dimension":4,"ghost_number":0,"goldstoneboson":false,"lepton_number":0,"mass":"ZERO","name":"mu+","pdg_code":-13,"propagating":true,"propagator":null,"quantum_numbers":[["electric_charge","1"]],"spin":2,"texname":"mu+","width":"ZERO","y_charge":0},{"antiname":"vm~","antitexname":"vm~","auxiliary_kind":null,"charge":0.0,"color":1,"component_dimension":4,"ghost_number":0,"goldstoneboson":false,"lepton_number":0,"mass":"ZERO","name":"vm","pdg_code":14,"propagating":true,"propagator":null,"quantum_numbers":[["electric_charge","0"]],"spin":2,"texname":"vm","width":"ZERO","y_charge":0},{"antiname":"vm","antitexname":"vm","auxiliary_kind":null,"charge":-0.0,"color":1,"component_dimension":4,"ghost_number":0,"goldstoneboson":false,"lepton_number":0,"mass":"ZERO","name":"vm~","pdg_code":-14,"propagating":true,"propagator":null,"quantum_numbers":[["electric_charge","0"]],"spin":2,"texname":"vm~","width":"ZERO","y_charge":0},{"antiname":"ta+","antitexname":"ta+","auxiliary_kind":null,"charge":-1.0,"color":1,"component_dimension":4,"ghost_number":0,"goldstoneboson":false,"lepton_number":0,"mass":"ZERO","name":"ta-","pdg_code":15,"propagating":true,"propagator":null,"quantum_numbers":[["electric_charge","-1"]],"spin":2,"texname":"ta-","width":"ZERO","y_charge":0},{"antiname":"ta-","antitexname":"ta-","auxiliary_kind":null,"charge":1.0,"color":1,"component_dimension":4,"ghost_number":0,"goldstoneboson":false,"lepton_number":0,"mass":"ZERO","name":"ta+","pdg_code":-15,"propagating":true,"propagator":null,"quantum_numbers":[["electric_charge","1"]],"spin":2,"texname":"ta+","width":"ZERO","y_charge":0},{"antiname":"vt~","antitexname":"vt~","auxiliary_kind":null,"charge":0.0,"color":1,"component_dimension":4,"ghost_number":0,"goldstoneboson":false,"lepton_number":0,"mass":"ZERO","name":"vt","pdg_code":16,"propagating":true,"propagator":null,"quantum_numbers":[["electric_charge","0"]],"spin":2,"texname":"vt","width":"ZERO","y_charge":0},{"antiname":"vt","antitexname":"vt","auxiliary_kind":null,"charge":-0.0,"color":1,"component_dimension":4,"ghost_number":0,"goldstoneboson":false,"lepton_number":0,"mass":"ZERO","name":"vt~","pdg_code":-16,"propagating":true,"propagator":null,"quantum_numbers":[["electric_charge","0"]],"spin":2,"texname":"vt~","width":"ZERO","y_charge":0},{"antiname":"g","antitexname":"g","auxiliary_kind":null,"charge":0.0,"color":8,"component_dimension":4,"ghost_number":0,"goldstoneboson":false,"lepton_number":0,"mass":"ZERO","name":"g","pdg_code":21,"propagating":true,"propagator":null,"quantum_numbers":[["electric_charge","0"]],"spin":3,"texname":"g","width":"ZERO","y_charge":0},{"antiname":"a","antitexname":"a","auxiliary_kind":null,"charge":0.0,"color":1,"component_dimension":4,"ghost_number":0,"goldstoneboson":false,"lepton_number":0,"mass":"ZERO","name":"a","pdg_code":22,"propagating":true,"propagator":null,"quantum_numbers":[["electric_charge","0"]],"spin":3,"texname":"a","width":"ZERO","y_charge":0},{"antiname":"z","antitexname":"z","auxiliary_kind":null,"charge":0.0,"color":1,"component_dimension":4,"ghost_number":0,"goldstoneboson":false,"lepton_number":0,"mass":"mass_23","name":"z","pdg_code":23,"propagating":true,"propagator":null,"quantum_numbers":[["electric_charge","0"]],"spin":3,"texname":"z","width":"width_23","y_charge":0},{"antiname":"w-","antitexname":"w-","auxiliary_kind":null,"charge":1.0,"color":1,"component_dimension":4,"ghost_number":0,"goldstoneboson":false,"lepton_number":0,"mass":"mass_24","name":"w+","pdg_code":24,"propagating":true,"propagator":null,"quantum_numbers":[["electric_charge","1"]],"spin":3,"texname":"w+","width":"width_24","y_charge":0},{"antiname":"w+","antitexname":"w+","auxiliary_kind":null,"charge":-1.0,"color":1,"component_dimension":4,"ghost_number":0,"goldstoneboson":false,"lepton_number":0,"mass":"mass_24","name":"w-","pdg_code":-24,"propagating":true,"propagator":null,"quantum_numbers":[["electric_charge","-1"]],"spin":3,"texname":"w-","width":"width_24","y_charge":0},{"antiname":"h","antitexname":"h","auxiliary_kind":null,"charge":0.0,"color":1,"component_dimension":1,"ghost_number":0,"goldstoneboson":false,"lepton_number":0,"mass":"mass_25","name":"h","pdg_code":25,"propagating":true,"propagator":null,"quantum_numbers":[["electric_charge","0"]],"spin":1,"texname":"h","width":"width_25","y_charge":0},{"antiname":"pdg_-26","antitexname":"pdg_-26","auxiliary_kind":"antisymmetric-tensor","charge":1.0,"color":1,"component_dimension":6,"ghost_number":0,"goldstoneboson":false,"lepton_number":0,"mass":"ZERO","name":"pdg_26","pdg_code":26,"propagating":false,"propagator":null,"quantum_numbers":[["electric_charge","1"]],"spin":-1,"texname":"pdg_26","width":"ZERO","y_charge":0},{"antiname":"pdg_26","antitexname":"pdg_26","auxiliary_kind":"antisymmetric-tensor","charge":-1.0,"color":1,"component_dimension":6,"ghost_number":0,"goldstoneboson":false,"lepton_number":0,"mass":"ZERO","name":"pdg_-26","pdg_code":-26,"propagating":false,"propagator":null,"quantum_numbers":[["electric_charge","-1"]],"spin":-1,"texname":"pdg_-26","width":"ZERO","y_charge":0},{"antiname":"pdg_99","antitexname":"pdg_99","auxiliary_kind":"u1-subtraction-color-flow-vector","charge":0.0,"color":1,"component_dimension":4,"ghost_number":0,"goldstoneboson":false,"lepton_number":0,"mass":"ZERO","name":"pdg_99","pdg_code":99,"propagating":false,"propagator":null,"quantum_numbers":[["electric_charge","0"]],"spin":-1,"texname":"pdg_99","width":"ZERO","y_charge":0},{"antiname":"pdg_125","antitexname":"pdg_125","auxiliary_kind":null,"charge":0.0,"color":1,"component_dimension":1,"ghost_number":0,"goldstoneboson":false,"lepton_number":0,"mass":"ZERO","name":"pdg_125","pdg_code":125,"propagating":false,"propagator":null,"quantum_numbers":[["electric_charge","0"]],"spin":-1,"texname":"pdg_125","width":"ZERO","y_charge":0},{"antiname":"pdg_126","antitexname":"pdg_126","auxiliary_kind":null,"charge":0.0,"color":1,"component_dimension":1,"ghost_number":0,"goldstoneboson":false,"lepton_number":0,"mass":"ZERO","name":"pdg_126","pdg_code":126,"propagating":false,"propagator":null,"quantum_numbers":[["electric_charge","0"]],"spin":-1,"texname":"pdg_126","width":"ZERO","y_charge":0},{"antiname":"pdg_127","antitexname":"pdg_127","auxiliary_kind":null,"charge":0.0,"color":1,"component_dimension":1,"ghost_number":0,"goldstoneboson":false,"lepton_number":0,"mass":"ZERO","name":"pdg_127","pdg_code":127,"propagating":false,"propagator":null,"quantum_numbers":[["electric_charge","0"]],"spin":-1,"texname":"pdg_127","width":"ZERO","y_charge":0}],"propagators":[],"restriction":null,"vertex_rules":[{"builtin_coupling":[1.0,0.0],"builtin_kind":0,"color_structures":["1"],"couplings":[["builtin_coupling_0"]],"lorentz_structures":["builtin_kind_0"],"name":"builtin_vertex_0","particles":["g","g","g"]},{"builtin_coupling":[1.0,0.0],"builtin_kind":1,"color_structures":["1"],"couplings":[["builtin_coupling_1"]],"lorentz_structures":["builtin_kind_1"],"name":"builtin_vertex_1","particles":["g","g","pdg_-21"]},{"builtin_coupling":[1.0,0.0],"builtin_kind":2,"color_structures":["1"],"couplings":[["builtin_coupling_2"]],"lorentz_structures":["builtin_kind_2"],"name":"builtin_vertex_2","particles":["pdg_-21","g","g"]},{"builtin_coupling":[1.0,0.0],"builtin_kind":3,"color_structures":["1"],"couplings":[["builtin_coupling_3"]],"lorentz_structures":["builtin_kind_3"],"name":"builtin_vertex_3","particles":["g","pdg_-21","g"]},{"builtin_coupling":[1.0,0.0],"builtin_kind":4,"color_structures":["1"],"couplings":[["builtin_coupling_4"]],"lorentz_structures":["builtin_kind_4"],"name":"builtin_vertex_4","particles":["g","d","d"]},{"builtin_coupling":[1.0,0.0],"builtin_kind":5,"color_structures":["1"],"couplings":[["builtin_coupling_5"]],"lorentz_structures":["builtin_kind_5"],"name":"builtin_vertex_5","particles":["g","d~","d~"]},{"builtin_coupling":[1.0,0.0],"builtin_kind":6,"color_structures":["1"],"couplings":[["builtin_coupling_6"]],"lorentz_structures":["builtin_kind_6"],"name":"builtin_vertex_6","particles":["d","g","d"]},{"builtin_coupling":[1.0,0.0],"builtin_kind":7,"color_structures":["1"],"couplings":[["builtin_coupling_7"]],"lorentz_structures":["builtin_kind_7"],"name":"builtin_vertex_7","particles":["d~","g","d~"]},{"builtin_coupling":[1.0,0.0],"builtin_kind":9,"color_structures":["1"],"couplings":[["builtin_coupling_8"]],"lorentz_structures":["builtin_kind_9"],"name":"builtin_vertex_8","particles":["d~","d","g"]},{"builtin_coupling":[0.3333333333333333,0.0],"builtin_kind":8,"color_structures":["1"],"couplings":[["builtin_coupling_9"]],"lorentz_structures":["builtin_kind_8"],"name":"builtin_vertex_9","particles":["d","d~","pdg_99"]},{"builtin_coupling":[1.0,0.0],"builtin_kind":4,"color_structures":["1"],"couplings":[["builtin_coupling_10"]],"lorentz_structures":["builtin_kind_4"],"name":"builtin_vertex_10","particles":["pdg_99","d","d"]},{"builtin_coupling":[1.0,0.0],"builtin_kind":5,"color_structures":["1"],"couplings":[["builtin_coupling_11"]],"lorentz_structures":["builtin_kind_5"],"name":"builtin_vertex_11","particles":["pdg_99","d~","d~"]},{"builtin_coupling":[1.0,0.0],"builtin_kind":6,"color_structures":["1"],"couplings":[["builtin_coupling_12"]],"lorentz_structures":["builtin_kind_6"],"name":"builtin_vertex_12","particles":["d","pdg_99","d"]},{"builtin_coupling":[1.0,0.0],"builtin_kind":7,"color_structures":["1"],"couplings":[["builtin_coupling_13"]],"lorentz_structures":["builtin_kind_7"],"name":"builtin_vertex_13","particles":["d~","pdg_99","d~"]},{"builtin_coupling":[-0.3333333333333333,-0.3333333333333333],"builtin_kind":10,"color_structures":["1"],"couplings":[["builtin_coupling_14"]],"lorentz_structures":["builtin_kind_10"],"name":"builtin_vertex_14","particles":["d","a","d"]},{"builtin_coupling":[-0.3333333333333333,-0.3333333333333333],"builtin_kind":11,"color_structures":["1"],"couplings":[["builtin_coupling_15"]],"lorentz_structures":["builtin_kind_11"],"name":"builtin_vertex_15","particles":["d~","a","d~"]},{"builtin_coupling":[-1.0244420275940371,0.17818666745287456],"builtin_kind":10,"color_structures":["1"],"couplings":[["builtin_coupling_16"]],"lorentz_structures":["builtin_kind_10"],"name":"builtin_vertex_16","particles":["d","z","d"]},{"builtin_coupling":[-1.0244420275940371,0.17818666745287456],"builtin_kind":11,"color_structures":["1"],"couplings":[["builtin_coupling_17"]],"lorentz_structures":["builtin_kind_11"],"name":"builtin_vertex_17","particles":["d~","z","d~"]},{"builtin_coupling":[1.4999181171782847,0.0],"builtin_kind":10,"color_structures":["1"],"couplings":[["builtin_coupling_18"]],"lorentz_structures":["builtin_kind_10"],"name":"builtin_vertex_18","particles":["d","w+","u"]},{"builtin_coupling":[1.4999181171782847,0.0],"builtin_kind":11,"color_structures":["1"],"couplings":[["builtin_coupling_19"]],"lorentz_structures":["builtin_kind_11"],"name":"builtin_vertex_19","particles":["d~","w-","u~"]},{"builtin_coupling":[1.0,0.0],"builtin_kind":4,"color_structures":["1"],"couplings":[["builtin_coupling_20"]],"lorentz_structures":["builtin_kind_4"],"name":"builtin_vertex_20","particles":["g","u","u"]},{"builtin_coupling":[1.0,0.0],"builtin_kind":5,"color_structures":["1"],"couplings":[["builtin_coupling_21"]],"lorentz_structures":["builtin_kind_5"],"name":"builtin_vertex_21","particles":["g","u~","u~"]},{"builtin_coupling":[1.0,0.0],"builtin_kind":6,"color_structures":["1"],"couplings":[["builtin_coupling_22"]],"lorentz_structures":["builtin_kind_6"],"name":"builtin_vertex_22","particles":["u","g","u"]},{"builtin_coupling":[1.0,0.0],"builtin_kind":7,"color_structures":["1"],"couplings":[["builtin_coupling_23"]],"lorentz_structures":["builtin_kind_7"],"name":"builtin_vertex_23","particles":["u~","g","u~"]},{"builtin_coupling":[1.0,0.0],"builtin_kind":9,"color_structures":["1"],"couplings":[["builtin_coupling_24"]],"lorentz_structures":["builtin_kind_9"],"name":"builtin_vertex_24","particles":["u~","u","g"]},{"builtin_coupling":[0.3333333333333333,0.0],"builtin_kind":8,"color_structures":["1"],"couplings":[["builtin_coupling_25"]],"lorentz_structures":["builtin_kind_8"],"name":"builtin_vertex_25","particles":["u","u~","pdg_99"]},{"builtin_coupling":[1.0,0.0],"builtin_kind":4,"color_structures":["1"],"couplings":[["builtin_coupling_26"]],"lorentz_structures":["builtin_kind_4"],"name":"builtin_vertex_26","particles":["pdg_99","u","u"]},{"builtin_coupling":[1.0,0.0],"builtin_kind":5,"color_structures":["1"],"couplings":[["builtin_coupling_27"]],"lorentz_structures":["builtin_kind_5"],"name":"builtin_vertex_27","particles":["pdg_99","u~","u~"]},{"builtin_coupling":[1.0,0.0],"builtin_kind":6,"color_structures":["1"],"couplings":[["builtin_coupling_28"]],"lorentz_structures":["builtin_kind_6"],"name":"builtin_vertex_28","particles":["u","pdg_99","u"]},{"builtin_coupling":[1.0,0.0],"builtin_kind":7,"color_structures":["1"],"couplings":[["builtin_coupling_29"]],"lorentz_structures":["builtin_kind_7"],"name":"builtin_vertex_29","particles":["u~","pdg_99","u~"]},{"builtin_coupling":[0.6666666666666666,0.6666666666666666],"builtin_kind":10,"color_structures":["1"],"couplings":[["builtin_coupling_30"]],"lorentz_structures":["builtin_kind_10"],"name":"builtin_vertex_30","particles":["u","a","u"]},{"builtin_coupling":[0.6666666666666666,0.6666666666666666],"builtin_kind":11,"color_structures":["1"],"couplings":[["builtin_coupling_31"]],"lorentz_structures":["builtin_kind_11"],"name":"builtin_vertex_31","particles":["u~","a","u~"]},{"builtin_coupling":[0.8462553601411626,-0.35637333490574913],"builtin_kind":10,"color_structures":["1"],"couplings":[["builtin_coupling_32"]],"lorentz_structures":["builtin_kind_10"],"name":"builtin_vertex_32","particles":["u","z","u"]},{"builtin_coupling":[0.8462553601411626,-0.35637333490574913],"builtin_kind":11,"color_structures":["1"],"couplings":[["builtin_coupling_33"]],"lorentz_structures":["builtin_kind_11"],"name":"builtin_vertex_33","particles":["u~","z","u~"]},{"builtin_coupling":[1.4999181171782847,0.0],"builtin_kind":10,"color_structures":["1"],"couplings":[["builtin_coupling_34"]],"lorentz_structures":["builtin_kind_10"],"name":"builtin_vertex_34","particles":["u","w-","d"]},{"builtin_coupling":[1.4999181171782847,0.0],"builtin_kind":11,"color_structures":["1"],"couplings":[["builtin_coupling_35"]],"lorentz_structures":["builtin_kind_11"],"name":"builtin_vertex_35","particles":["u~","w+","d~"]},{"builtin_coupling":[1.0,0.0],"builtin_kind":4,"color_structures":["1"],"couplings":[["builtin_coupling_36"]],"lorentz_structures":["builtin_kind_4"],"name":"builtin_vertex_36","particles":["g","s","s"]},{"builtin_coupling":[1.0,0.0],"builtin_kind":5,"color_structures":["1"],"couplings":[["builtin_coupling_37"]],"lorentz_structures":["builtin_kind_5"],"name":"builtin_vertex_37","particles":["g","s~","s~"]},{"builtin_coupling":[1.0,0.0],"builtin_kind":6,"color_structures":["1"],"couplings":[["builtin_coupling_38"]],"lorentz_structures":["builtin_kind_6"],"name":"builtin_vertex_38","particles":["s","g","s"]},{"builtin_coupling":[1.0,0.0],"builtin_kind":7,"color_structures":["1"],"couplings":[["builtin_coupling_39"]],"lorentz_structures":["builtin_kind_7"],"name":"builtin_vertex_39","particles":["s~","g","s~"]},{"builtin_coupling":[1.0,0.0],"builtin_kind":9,"color_structures":["1"],"couplings":[["builtin_coupling_40"]],"lorentz_structures":["builtin_kind_9"],"name":"builtin_vertex_40","particles":["s~","s","g"]},{"builtin_coupling":[0.3333333333333333,0.0],"builtin_kind":8,"color_structures":["1"],"couplings":[["builtin_coupling_41"]],"lorentz_structures":["builtin_kind_8"],"name":"builtin_vertex_41","particles":["s","s~","pdg_99"]},{"builtin_coupling":[1.0,0.0],"builtin_kind":4,"color_structures":["1"],"couplings":[["builtin_coupling_42"]],"lorentz_structures":["builtin_kind_4"],"name":"builtin_vertex_42","particles":["pdg_99","s","s"]},{"builtin_coupling":[1.0,0.0],"builtin_kind":5,"color_structures":["1"],"couplings":[["builtin_coupling_43"]],"lorentz_structures":["builtin_kind_5"],"name":"builtin_vertex_43","particles":["pdg_99","s~","s~"]},{"builtin_coupling":[1.0,0.0],"builtin_kind":6,"color_structures":["1"],"couplings":[["builtin_coupling_44"]],"lorentz_structures":["builtin_kind_6"],"name":"builtin_vertex_44","particles":["s","pdg_99","s"]},{"builtin_coupling":[1.0,0.0],"builtin_kind":7,"color_structures":["1"],"couplings":[["builtin_coupling_45"]],"lorentz_structures":["builtin_kind_7"],"name":"builtin_vertex_45","particles":["s~","pdg_99","s~"]},{"builtin_coupling":[-0.3333333333333333,-0.3333333333333333],"builtin_kind":10,"color_structures":["1"],"couplings":[["builtin_coupling_46"]],"lorentz_structures":["builtin_kind_10"],"name":"builtin_vertex_46","particles":["s","a","s"]},{"builtin_coupling":[-0.3333333333333333,-0.3333333333333333],"builtin_kind":11,"color_structures":["1"],"couplings":[["builtin_coupling_47"]],"lorentz_structures":["builtin_kind_11"],"name":"builtin_vertex_47","particles":["s~","a","s~"]},{"builtin_coupling":[-1.0244420275940371,0.17818666745287456],"builtin_kind":10,"color_structures":["1"],"couplings":[["builtin_coupling_48"]],"lorentz_structures":["builtin_kind_10"],"name":"builtin_vertex_48","particles":["s","z","s"]},{"builtin_coupling":[-1.0244420275940371,0.17818666745287456],"builtin_kind":11,"color_structures":["1"],"couplings":[["builtin_coupling_49"]],"lorentz_structures":["builtin_kind_11"],"name":"builtin_vertex_49","particles":["s~","z","s~"]},{"builtin_coupling":[1.4999181171782847,0.0],"builtin_kind":10,"color_structures":["1"],"couplings":[["builtin_coupling_50"]],"lorentz_structures":["builtin_kind_10"],"name":"builtin_vertex_50","particles":["s","w+","c"]},{"builtin_coupling":[1.4999181171782847,0.0],"builtin_kind":11,"color_structures":["1"],"couplings":[["builtin_coupling_51"]],"lorentz_structures":["builtin_kind_11"],"name":"builtin_vertex_51","particles":["s~","w-","c~"]},{"builtin_coupling":[1.0,0.0],"builtin_kind":4,"color_structures":["1"],"couplings":[["builtin_coupling_52"]],"lorentz_structures":["builtin_kind_4"],"name":"builtin_vertex_52","particles":["g","c","c"]},{"builtin_coupling":[1.0,0.0],"builtin_kind":5,"color_structures":["1"],"couplings":[["builtin_coupling_53"]],"lorentz_structures":["builtin_kind_5"],"name":"builtin_vertex_53","particles":["g","c~","c~"]},{"builtin_coupling":[1.0,0.0],"builtin_kind":6,"color_structures":["1"],"couplings":[["builtin_coupling_54"]],"lorentz_structures":["builtin_kind_6"],"name":"builtin_vertex_54","particles":["c","g","c"]},{"builtin_coupling":[1.0,0.0],"builtin_kind":7,"color_structures":["1"],"couplings":[["builtin_coupling_55"]],"lorentz_structures":["builtin_kind_7"],"name":"builtin_vertex_55","particles":["c~","g","c~"]},{"builtin_coupling":[1.0,0.0],"builtin_kind":9,"color_structures":["1"],"couplings":[["builtin_coupling_56"]],"lorentz_structures":["builtin_kind_9"],"name":"builtin_vertex_56","particles":["c~","c","g"]},{"builtin_coupling":[0.3333333333333333,0.0],"builtin_kind":8,"color_structures":["1"],"couplings":[["builtin_coupling_57"]],"lorentz_structures":["builtin_kind_8"],"name":"builtin_vertex_57","particles":["c","c~","pdg_99"]},{"builtin_coupling":[1.0,0.0],"builtin_kind":4,"color_structures":["1"],"couplings":[["builtin_coupling_58"]],"lorentz_structures":["builtin_kind_4"],"name":"builtin_vertex_58","particles":["pdg_99","c","c"]},{"builtin_coupling":[1.0,0.0],"builtin_kind":5,"color_structures":["1"],"couplings":[["builtin_coupling_59"]],"lorentz_structures":["builtin_kind_5"],"name":"builtin_vertex_59","particles":["pdg_99","c~","c~"]},{"builtin_coupling":[1.0,0.0],"builtin_kind":6,"color_structures":["1"],"couplings":[["builtin_coupling_60"]],"lorentz_structures":["builtin_kind_6"],"name":"builtin_vertex_60","particles":["c","pdg_99","c"]},{"builtin_coupling":[1.0,0.0],"builtin_kind":7,"color_structures":["1"],"couplings":[["builtin_coupling_61"]],"lorentz_structures":["builtin_kind_7"],"name":"builtin_vertex_61","particles":["c~","pdg_99","c~"]},{"builtin_coupling":[0.6666666666666666,0.6666666666666666],"builtin_kind":10,"color_structures":["1"],"couplings":[["builtin_coupling_62"]],"lorentz_structures":["builtin_kind_10"],"name":"builtin_vertex_62","particles":["c","a","c"]},{"builtin_coupling":[0.6666666666666666,0.6666666666666666],"builtin_kind":11,"color_structures":["1"],"couplings":[["builtin_coupling_63"]],"lorentz_structures":["builtin_kind_11"],"name":"builtin_vertex_63","particles":["c~","a","c~"]},{"builtin_coupling":[0.8462553601411626,-0.35637333490574913],"builtin_kind":10,"color_structures":["1"],"couplings":[["builtin_coupling_64"]],"lorentz_structures":["builtin_kind_10"],"name":"builtin_vertex_64","particles":["c","z","c"]},{"builtin_coupling":[0.8462553601411626,-0.35637333490574913],"builtin_kind":11,"color_structures":["1"],"couplings":[["builtin_coupling_65"]],"lorentz_structures":["builtin_kind_11"],"name":"builtin_vertex_65","particles":["c~","z","c~"]},{"builtin_coupling":[1.4999181171782847,0.0],"builtin_kind":10,"color_structures":["1"],"couplings":[["builtin_coupling_66"]],"lorentz_structures":["builtin_kind_10"],"name":"builtin_vertex_66","particles":["c","w-","s"]},{"builtin_coupling":[1.4999181171782847,0.0],"builtin_kind":11,"color_structures":["1"],"couplings":[["builtin_coupling_67"]],"lorentz_structures":["builtin_kind_11"],"name":"builtin_vertex_67","particles":["c~","w+","s~"]},{"builtin_coupling":[1.0,0.0],"builtin_kind":4,"color_structures":["1"],"couplings":[["builtin_coupling_68"]],"lorentz_structures":["builtin_kind_4"],"name":"builtin_vertex_68","particles":["g","b","b"]},{"builtin_coupling":[1.0,0.0],"builtin_kind":5,"color_structures":["1"],"couplings":[["builtin_coupling_69"]],"lorentz_structures":["builtin_kind_5"],"name":"builtin_vertex_69","particles":["g","b~","b~"]},{"builtin_coupling":[1.0,0.0],"builtin_kind":6,"color_structures":["1"],"couplings":[["builtin_coupling_70"]],"lorentz_structures":["builtin_kind_6"],"name":"builtin_vertex_70","particles":["b","g","b"]},{"builtin_coupling":[1.0,0.0],"builtin_kind":7,"color_structures":["1"],"couplings":[["builtin_coupling_71"]],"lorentz_structures":["builtin_kind_7"],"name":"builtin_vertex_71","particles":["b~","g","b~"]},{"builtin_coupling":[1.0,0.0],"builtin_kind":9,"color_structures":["1"],"couplings":[["builtin_coupling_72"]],"lorentz_structures":["builtin_kind_9"],"name":"builtin_vertex_72","particles":["b~","b","g"]},{"builtin_coupling":[0.3333333333333333,0.0],"builtin_kind":8,"color_structures":["1"],"couplings":[["builtin_coupling_73"]],"lorentz_structures":["builtin_kind_8"],"name":"builtin_vertex_73","particles":["b","b~","pdg_99"]},{"builtin_coupling":[1.0,0.0],"builtin_kind":4,"color_structures":["1"],"couplings":[["builtin_coupling_74"]],"lorentz_structures":["builtin_kind_4"],"name":"builtin_vertex_74","particles":["pdg_99","b","b"]},{"builtin_coupling":[1.0,0.0],"builtin_kind":5,"color_structures":["1"],"couplings":[["builtin_coupling_75"]],"lorentz_structures":["builtin_kind_5"],"name":"builtin_vertex_75","particles":["pdg_99","b~","b~"]},{"builtin_coupling":[1.0,0.0],"builtin_kind":6,"color_structures":["1"],"couplings":[["builtin_coupling_76"]],"lorentz_structures":["builtin_kind_6"],"name":"builtin_vertex_76","particles":["b","pdg_99","b"]},{"builtin_coupling":[1.0,0.0],"builtin_kind":7,"color_structures":["1"],"couplings":[["builtin_coupling_77"]],"lorentz_structures":["builtin_kind_7"],"name":"builtin_vertex_77","particles":["b~","pdg_99","b~"]},{"builtin_coupling":[-0.3333333333333333,-0.3333333333333333],"builtin_kind":10,"color_structures":["1"],"couplings":[["builtin_coupling_78"]],"lorentz_structures":["builtin_kind_10"],"name":"builtin_vertex_78","particles":["b","a","b"]},{"builtin_coupling":[-0.3333333333333333,-0.3333333333333333],"builtin_kind":11,"color_structures":["1"],"couplings":[["builtin_coupling_79"]],"lorentz_structures":["builtin_kind_11"],"name":"builtin_vertex_79","particles":["b~","a","b~"]},{"builtin_coupling":[-1.0244420275940371,0.17818666745287456],"builtin_kind":10,"color_structures":["1"],"couplings":[["builtin_coupling_80"]],"lorentz_structures":["builtin_kind_10"],"name":"builtin_vertex_80","particles":["b","z","b"]},{"builtin_coupling":[-1.0244420275940371,0.17818666745287456],"builtin_kind":11,"color_structures":["1"],"couplings":[["builtin_coupling_81"]],"lorentz_structures":["builtin_kind_11"],"name":"builtin_vertex_81","particles":["b~","z","b~"]},{"builtin_coupling":[1.4999181171782847,0.0],"builtin_kind":10,"color_structures":["1"],"couplings":[["builtin_coupling_82"]],"lorentz_structures":["builtin_kind_10"],"name":"builtin_vertex_82","particles":["b","w+","t"]},{"builtin_coupling":[1.4999181171782847,0.0],"builtin_kind":11,"color_structures":["1"],"couplings":[["builtin_coupling_83"]],"lorentz_structures":["builtin_kind_11"],"name":"builtin_vertex_83","particles":["b~","w-","t~"]},{"builtin_coupling":[1.0,0.0],"builtin_kind":4,"color_structures":["1"],"couplings":[["builtin_coupling_84"]],"lorentz_structures":["builtin_kind_4"],"name":"builtin_vertex_84","particles":["g","t","t"]},{"builtin_coupling":[1.0,0.0],"builtin_kind":5,"color_structures":["1"],"couplings":[["builtin_coupling_85"]],"lorentz_structures":["builtin_kind_5"],"name":"builtin_vertex_85","particles":["g","t~","t~"]},{"builtin_coupling":[1.0,0.0],"builtin_kind":6,"color_structures":["1"],"couplings":[["builtin_coupling_86"]],"lorentz_structures":["builtin_kind_6"],"name":"builtin_vertex_86","particles":["t","g","t"]},{"builtin_coupling":[1.0,0.0],"builtin_kind":7,"color_structures":["1"],"couplings":[["builtin_coupling_87"]],"lorentz_structures":["builtin_kind_7"],"name":"builtin_vertex_87","particles":["t~","g","t~"]},{"builtin_coupling":[1.0,0.0],"builtin_kind":9,"color_structures":["1"],"couplings":[["builtin_coupling_88"]],"lorentz_structures":["builtin_kind_9"],"name":"builtin_vertex_88","particles":["t~","t","g"]},{"builtin_coupling":[0.3333333333333333,0.0],"builtin_kind":8,"color_structures":["1"],"couplings":[["builtin_coupling_89"]],"lorentz_structures":["builtin_kind_8"],"name":"builtin_vertex_89","particles":["t","t~","pdg_99"]},{"builtin_coupling":[1.0,0.0],"builtin_kind":4,"color_structures":["1"],"couplings":[["builtin_coupling_90"]],"lorentz_structures":["builtin_kind_4"],"name":"builtin_vertex_90","particles":["pdg_99","t","t"]},{"builtin_coupling":[1.0,0.0],"builtin_kind":5,"color_structures":["1"],"couplings":[["builtin_coupling_91"]],"lorentz_structures":["builtin_kind_5"],"name":"builtin_vertex_91","particles":["pdg_99","t~","t~"]},{"builtin_coupling":[1.0,0.0],"builtin_kind":6,"color_structures":["1"],"couplings":[["builtin_coupling_92"]],"lorentz_structures":["builtin_kind_6"],"name":"builtin_vertex_92","particles":["t","pdg_99","t"]},{"builtin_coupling":[1.0,0.0],"builtin_kind":7,"color_structures":["1"],"couplings":[["builtin_coupling_93"]],"lorentz_structures":["builtin_kind_7"],"name":"builtin_vertex_93","particles":["t~","pdg_99","t~"]},{"builtin_coupling":[0.6666666666666666,0.6666666666666666],"builtin_kind":10,"color_structures":["1"],"couplings":[["builtin_coupling_94"]],"lorentz_structures":["builtin_kind_10"],"name":"builtin_vertex_94","particles":["t","a","t"]},{"builtin_coupling":[0.6666666666666666,0.6666666666666666],"builtin_kind":11,"color_structures":["1"],"couplings":[["builtin_coupling_95"]],"lorentz_structures":["builtin_kind_11"],"name":"builtin_vertex_95","particles":["t~","a","t~"]},{"builtin_coupling":[0.8462553601411626,-0.35637333490574913],"builtin_kind":10,"color_structures":["1"],"couplings":[["builtin_coupling_96"]],"lorentz_structures":["builtin_kind_10"],"name":"builtin_vertex_96","particles":["t","z","t"]},{"builtin_coupling":[0.8462553601411626,-0.35637333490574913],"builtin_kind":11,"color_structures":["1"],"couplings":[["builtin_coupling_97"]],"lorentz_structures":["builtin_kind_11"],"name":"builtin_vertex_97","particles":["t~","z","t~"]},{"builtin_coupling":[1.4999181171782847,0.0],"builtin_kind":10,"color_structures":["1"],"couplings":[["builtin_coupling_98"]],"lorentz_structures":["builtin_kind_10"],"name":"builtin_vertex_98","particles":["t","w-","b"]},{"builtin_coupling":[1.4999181171782847,0.0],"builtin_kind":11,"color_structures":["1"],"couplings":[["builtin_coupling_99"]],"lorentz_structures":["builtin_kind_11"],"name":"builtin_vertex_99","particles":["t~","w+","b~"]},{"builtin_coupling":[-1.8706973877352002,0.0],"builtin_kind":12,"color_structures":["1"],"couplings":[["builtin_coupling_100"]],"lorentz_structures":["builtin_kind_12"],"name":"builtin_vertex_100","particles":["w+","w-","z"]},{"builtin_coupling":[1.8706973877352002,0.0],"builtin_kind":12,"color_structures":["1"],"couplings":[["builtin_coupling_101"]],"lorentz_structures":["builtin_kind_12"],"name":"builtin_vertex_101","particles":["w-","w+","z"]},{"builtin_coupling":[-1.0,0.0],"builtin_kind":12,"color_structures":["1"],"couplings":[["builtin_coupling_102"]],"lorentz_structures":["builtin_kind_12"],"name":"builtin_vertex_102","particles":["w+","w-","a"]},{"builtin_coupling":[1.0,0.0],"builtin_kind":12,"color_structures":["1"],"couplings":[["builtin_coupling_103"]],"lorentz_structures":["builtin_kind_12"],"name":"builtin_vertex_103","particles":["w-","w+","a"]},{"builtin_coupling":[1.0,0.0],"builtin_kind":12,"color_structures":["1"],"couplings":[["builtin_coupling_104"]],"lorentz_structures":["builtin_kind_12"],"name":"builtin_vertex_104","particles":["w+","a","w+"]},{"builtin_coupling":[-1.0,0.0],"builtin_kind":12,"color_structures":["1"],"couplings":[["builtin_coupling_105"]],"lorentz_structures":["builtin_kind_12"],"name":"builtin_vertex_105","particles":["w-","a","w-"]},{"builtin_coupling":[-1.0,0.0],"builtin_kind":12,"color_structures":["1"],"couplings":[["builtin_coupling_106"]],"lorentz_structures":["builtin_kind_12"],"name":"builtin_vertex_106","particles":["a","w+","w+"]},{"builtin_coupling":[1.0,0.0],"builtin_kind":12,"color_structures":["1"],"couplings":[["builtin_coupling_107"]],"lorentz_structures":["builtin_kind_12"],"name":"builtin_vertex_107","particles":["a","w-","w-"]},{"builtin_coupling":[1.8706973877352002,0.0],"builtin_kind":12,"color_structures":["1"],"couplings":[["builtin_coupling_108"]],"lorentz_structures":["builtin_kind_12"],"name":"builtin_vertex_108","particles":["w+","z","w+"]},{"builtin_coupling":[-1.8706973877352002,0.0],"builtin_kind":12,"color_structures":["1"],"couplings":[["builtin_coupling_109"]],"lorentz_structures":["builtin_kind_12"],"name":"builtin_vertex_109","particles":["w-","z","w-"]},{"builtin_coupling":[-1.8706973877352002,0.0],"builtin_kind":12,"color_structures":["1"],"couplings":[["builtin_coupling_110"]],"lorentz_structures":["builtin_kind_12"],"name":"builtin_vertex_110","particles":["z","w+","w+"]},{"builtin_coupling":[1.8706973877352002,0.0],"builtin_kind":12,"color_structures":["1"],"couplings":[["builtin_coupling_111"]],"lorentz_structures":["builtin_kind_12"],"name":"builtin_vertex_111","particles":["z","w-","w-"]},{"builtin_coupling":[2.1212045437626474,0.0],"builtin_kind":13,"color_structures":["1"],"couplings":[["builtin_coupling_112"]],"lorentz_structures":["builtin_kind_13"],"name":"builtin_vertex_112","particles":["w+","w-","pdg_-23"]},{"builtin_coupling":[-2.1212045437626474,0.0],"builtin_kind":13,"color_structures":["1"],"couplings":[["builtin_coupling_113"]],"lorentz_structures":["builtin_kind_13"],"name":"builtin_vertex_113","particles":["w-","w+","pdg_-23"]},{"builtin_coupling":[2.1212045437626474,0.0],"builtin_kind":14,"color_structures":["1"],"couplings":[["builtin_coupling_114"]],"lorentz_structures":["builtin_kind_14"],"name":"builtin_vertex_114","particles":["pdg_-23","w+","w+"]},{"builtin_coupling":[-2.1212045437626474,0.0],"builtin_kind":14,"color_structures":["1"],"couplings":[["builtin_coupling_115"]],"lorentz_structures":["builtin_kind_14"],"name":"builtin_vertex_115","particles":["pdg_-23","w-","w-"]},{"builtin_coupling":[-2.1212045437626474,0.0],"builtin_kind":15,"color_structures":["1"],"couplings":[["builtin_coupling_116"]],"lorentz_structures":["builtin_kind_15"],"name":"builtin_vertex_116","particles":["w+","pdg_-23","w+"]},{"builtin_coupling":[2.1212045437626474,0.0],"builtin_kind":15,"color_structures":["1"],"couplings":[["builtin_coupling_117"]],"lorentz_structures":["builtin_kind_15"],"name":"builtin_vertex_117","particles":["w-","pdg_-23","w-"]},{"builtin_coupling":[1.0,0.0],"builtin_kind":13,"color_structures":["1"],"couplings":[["builtin_coupling_118"]],"lorentz_structures":["builtin_kind_13"],"name":"builtin_vertex_118","particles":["w+","a","pdg_26"]},{"builtin_coupling":[1.0,0.0],"builtin_kind":13,"color_structures":["1"],"couplings":[["builtin_coupling_119"]],"lorentz_structures":["builtin_kind_13"],"name":"builtin_vertex_119","particles":["w-","a","pdg_-26"]},{"builtin_coupling":[-1.0,0.0],"builtin_kind":13,"color_structures":["1"],"couplings":[["builtin_coupling_120"]],"lorentz_structures":["builtin_kind_13"],"name":"builtin_vertex_120","particles":["a","w+","pdg_26"]},{"builtin_coupling":[-1.0,0.0],"builtin_kind":13,"color_structures":["1"],"couplings":[["builtin_coupling_121"]],"lorentz_structures":["builtin_kind_13"],"name":"builtin_vertex_121","particles":["a","w-","pdg_-26"]},{"builtin_coupling":[1.8706973877352002,0.0],"builtin_kind":13,"color_structures":["1"],"couplings":[["builtin_coupling_122"]],"lorentz_structures":["builtin_kind_13"],"name":"builtin_vertex_122","particles":["w+","z","pdg_26"]},{"builtin_coupling":[1.8706973877352002,0.0],"builtin_kind":13,"color_structures":["1"],"couplings":[["builtin_coupling_123"]],"lorentz_structures":["builtin_kind_13"],"name":"builtin_vertex_123","particles":["w-","z","pdg_-26"]},{"builtin_coupling":[-1.8706973877352002,0.0],"builtin_kind":13,"color_structures":["1"],"couplings":[["builtin_coupling_124"]],"lorentz_structures":["builtin_kind_13"],"name":"builtin_vertex_124","particles":["z","w+","pdg_26"]},{"builtin_coupling":[-1.8706973877352002,0.0],"builtin_kind":13,"color_structures":["1"],"couplings":[["builtin_coupling_125"]],"lorentz_structures":["builtin_kind_13"],"name":"builtin_vertex_125","particles":["z","w-","pdg_-26"]},{"builtin_coupling":[1.0,0.0],"builtin_kind":14,"color_structures":["1"],"couplings":[["builtin_coupling_126"]],"lorentz_structures":["builtin_kind_14"],"name":"builtin_vertex_126","particles":["pdg_26","a","w+"]},{"builtin_coupling":[1.0,0.0],"builtin_kind":14,"color_structures":["1"],"couplings":[["builtin_coupling_127"]],"lorentz_structures":["builtin_kind_14"],"name":"builtin_vertex_127","particles":["pdg_-26","a","w-"]},{"builtin_coupling":[-1.0,0.0],"builtin_kind":14,"color_structures":["1"],"couplings":[["builtin_coupling_128"]],"lorentz_structures":["builtin_kind_14"],"name":"builtin_vertex_128","particles":["pdg_26","w-","a"]},{"builtin_coupling":[-1.0,0.0],"builtin_kind":14,"color_structures":["1"],"couplings":[["builtin_coupling_129"]],"lorentz_structures":["builtin_kind_14"],"name":"builtin_vertex_129","particles":["pdg_-26","w+","a"]},{"builtin_coupling":[1.8706973877352002,0.0],"builtin_kind":14,"color_structures":["1"],"couplings":[["builtin_coupling_130"]],"lorentz_structures":["builtin_kind_14"],"name":"builtin_vertex_130","particles":["pdg_26","z","w+"]},{"builtin_coupling":[1.8706973877352002,0.0],"builtin_kind":14,"color_structures":["1"],"couplings":[["builtin_coupling_131"]],"lorentz_structures":["builtin_kind_14"],"name":"builtin_vertex_131","particles":["pdg_-26","z","w-"]},{"builtin_coupling":[-1.8706973877352002,0.0],"builtin_kind":14,"color_structures":["1"],"couplings":[["builtin_coupling_132"]],"lorentz_structures":["builtin_kind_14"],"name":"builtin_vertex_132","particles":["pdg_26","w-","z"]},{"builtin_coupling":[-1.8706973877352002,0.0],"builtin_kind":14,"color_structures":["1"],"couplings":[["builtin_coupling_133"]],"lorentz_structures":["builtin_kind_14"],"name":"builtin_vertex_133","particles":["pdg_-26","w+","z"]},{"builtin_coupling":[-1.0,0.0],"builtin_kind":15,"color_structures":["1"],"couplings":[["builtin_coupling_134"]],"lorentz_structures":["builtin_kind_15"],"name":"builtin_vertex_134","particles":["a","pdg_26","w+"]},{"builtin_coupling":[-1.0,0.0],"builtin_kind":15,"color_structures":["1"],"couplings":[["builtin_coupling_135"]],"lorentz_structures":["builtin_kind_15"],"name":"builtin_vertex_135","particles":["a","pdg_-26","w-"]},{"builtin_coupling":[1.0,0.0],"builtin_kind":15,"color_structures":["1"],"couplings":[["builtin_coupling_136"]],"lorentz_structures":["builtin_kind_15"],"name":"builtin_vertex_136","particles":["w+","pdg_-26","a"]},{"builtin_coupling":[1.0,0.0],"builtin_kind":15,"color_structures":["1"],"couplings":[["builtin_coupling_137"]],"lorentz_structures":["builtin_kind_15"],"name":"builtin_vertex_137","particles":["w-","pdg_26","a"]},{"builtin_coupling":[-1.8706973877352002,0.0],"builtin_kind":15,"color_structures":["1"],"couplings":[["builtin_coupling_138"]],"lorentz_structures":["builtin_kind_15"],"name":"builtin_vertex_138","particles":["z","pdg_26","w+"]},{"builtin_coupling":[-1.8706973877352002,0.0],"builtin_kind":15,"color_structures":["1"],"couplings":[["builtin_coupling_139"]],"lorentz_structures":["builtin_kind_15"],"name":"builtin_vertex_139","particles":["z","pdg_-26","w-"]},{"builtin_coupling":[1.8706973877352002,0.0],"builtin_kind":15,"color_structures":["1"],"couplings":[["builtin_coupling_140"]],"lorentz_structures":["builtin_kind_15"],"name":"builtin_vertex_140","particles":["w+","pdg_-26","z"]},{"builtin_coupling":[1.8706973877352002,0.0],"builtin_kind":15,"color_structures":["1"],"couplings":[["builtin_coupling_141"]],"lorentz_structures":["builtin_kind_15"],"name":"builtin_vertex_141","particles":["w-","pdg_26","z"]},{"builtin_coupling":[2.281602450356579,0.0],"builtin_kind":16,"color_structures":["1"],"couplings":[["builtin_coupling_142"]],"lorentz_structures":["builtin_kind_16"],"name":"builtin_vertex_142","particles":["t","h","t"]},{"builtin_coupling":[2.281602450356579,0.0],"builtin_kind":16,"color_structures":["1"],"couplings":[["builtin_coupling_143"]],"lorentz_structures":["builtin_kind_16"],"name":"builtin_vertex_143","particles":["t~","h","t~"]},{"builtin_coupling":[170.58515339279742,0.0],"builtin_kind":17,"color_structures":["1"],"couplings":[["builtin_coupling_144"]],"lorentz_structures":["builtin_kind_17"],"name":"builtin_vertex_144","particles":["w+","w-","h"]},{"builtin_coupling":[170.58515339279742,0.0],"builtin_kind":17,"color_structures":["1"],"couplings":[["builtin_coupling_145"]],"lorentz_structures":["builtin_kind_17"],"name":"builtin_vertex_145","particles":["w-","w+","h"]},{"builtin_coupling":[170.58515339279742,0.0],"builtin_kind":18,"color_structures":["1"],"couplings":[["builtin_coupling_146"]],"lorentz_structures":["builtin_kind_18"],"name":"builtin_vertex_146","particles":["h","w+","w+"]},{"builtin_coupling":[170.58515339279742,0.0],"builtin_kind":18,"color_structures":["1"],"couplings":[["builtin_coupling_147"]],"lorentz_structures":["builtin_kind_18"],"name":"builtin_vertex_147","particles":["h","w-","w-"]},{"builtin_coupling":[170.58515339279742,0.0],"builtin_kind":19,"color_structures":["1"],"couplings":[["builtin_coupling_148"]],"lorentz_structures":["builtin_kind_19"],"name":"builtin_vertex_148","particles":["w+","h","w+"]},{"builtin_coupling":[170.58515339279742,0.0],"builtin_kind":19,"color_structures":["1"],"couplings":[["builtin_coupling_149"]],"lorentz_structures":["builtin_kind_19"],"name":"builtin_vertex_149","particles":["w-","h","w-"]},{"builtin_coupling":[219.3306108878756,0.0],"builtin_kind":17,"color_structures":["1"],"couplings":[["builtin_coupling_150"]],"lorentz_structures":["builtin_kind_17"],"name":"builtin_vertex_150","particles":["z","z","h"]},{"builtin_coupling":[219.3306108878756,0.0],"builtin_kind":18,"color_structures":["1"],"couplings":[["builtin_coupling_151"]],"lorentz_structures":["builtin_kind_18"],"name":"builtin_vertex_151","particles":["h","z","z"]},{"builtin_coupling":[219.3306108878756,0.0],"builtin_kind":19,"color_structures":["1"],"couplings":[["builtin_coupling_152"]],"lorentz_structures":["builtin_kind_19"],"name":"builtin_vertex_152","particles":["z","h","z"]},{"builtin_coupling":[-618.2087564188708,0.0],"builtin_kind":20,"color_structures":["1"],"couplings":[["builtin_coupling_153"]],"lorentz_structures":["builtin_kind_20"],"name":"builtin_vertex_153","particles":["h","h","h"]},{"builtin_coupling":[2.2497543582396506,0.0],"builtin_kind":17,"color_structures":["1"],"couplings":[["builtin_coupling_154"]],"lorentz_structures":["builtin_kind_17"],"name":"builtin_vertex_154","particles":["w+","w-","pdg_127"]},{"builtin_coupling":[2.2497543582396506,0.0],"builtin_kind":17,"color_structures":["1"],"couplings":[["builtin_coupling_155"]],"lorentz_structures":["builtin_kind_17"],"name":"builtin_vertex_155","particles":["w-","w+","pdg_127"]},{"builtin_coupling":[2.892631556300476,0.0],"builtin_kind":17,"color_structures":["1"],"couplings":[["builtin_coupling_156"]],"lorentz_structures":["builtin_kind_17"],"name":"builtin_vertex_156","particles":["z","z","pdg_127"]},{"builtin_coupling":[1.0,0.0],"builtin_kind":20,"color_structures":["1"],"couplings":[["builtin_coupling_157"]],"lorentz_structures":["builtin_kind_20"],"name":"builtin_vertex_157","particles":["pdg_125","h","h"]},{"builtin_coupling":[1.0,0.0],"builtin_kind":20,"color_structures":["1"],"couplings":[["builtin_coupling_158"]],"lorentz_structures":["builtin_kind_20"],"name":"builtin_vertex_158","particles":["h","pdg_125","h"]},{"builtin_coupling":[-8.153217418943292,0.0],"builtin_kind":20,"color_structures":["1"],"couplings":[["builtin_coupling_159"]],"lorentz_structures":["builtin_kind_20"],"name":"builtin_vertex_159","particles":["h","h","pdg_125"]},{"builtin_coupling":[1.0,0.0],"builtin_kind":20,"color_structures":["1"],"couplings":[["builtin_coupling_160"]],"lorentz_structures":["builtin_kind_20"],"name":"builtin_vertex_160","particles":["pdg_127","h","h"]},{"builtin_coupling":[1.0,0.0],"builtin_kind":20,"color_structures":["1"],"couplings":[["builtin_coupling_161"]],"lorentz_structures":["builtin_kind_20"],"name":"builtin_vertex_161","particles":["h","pdg_127","h"]},{"builtin_coupling":[1.0,-10.0],"builtin_kind":20,"color_structures":["1"],"couplings":[["builtin_coupling_162"]],"lorentz_structures":["builtin_kind_20"],"name":"builtin_vertex_162","particles":["h","h","pdg_126"]},{"builtin_coupling":[-2.892631556300476,0.0],"builtin_kind":18,"color_structures":["1"],"couplings":[["builtin_coupling_163"]],"lorentz_structures":["builtin_kind_18"],"name":"builtin_vertex_163","particles":["pdg_126","z","z"]},{"builtin_coupling":[-2.892631556300476,0.0],"builtin_kind":19,"color_structures":["1"],"couplings":[["builtin_coupling_164"]],"lorentz_structures":["builtin_kind_19"],"name":"builtin_vertex_164","particles":["z","pdg_126","z"]},{"builtin_coupling":[-2.2497543582396506,0.0],"builtin_kind":18,"color_structures":["1"],"couplings":[["builtin_coupling_165"]],"lorentz_structures":["builtin_kind_18"],"name":"builtin_vertex_165","particles":["pdg_126","w+","w+"]},{"builtin_coupling":[-2.2497543582396506,0.0],"builtin_kind":18,"color_structures":["1"],"couplings":[["builtin_coupling_166"]],"lorentz_structures":["builtin_kind_18"],"name":"builtin_vertex_166","particles":["pdg_126","w-","w-"]},{"builtin_coupling":[-2.2497543582396506,0.0],"builtin_kind":19,"color_structures":["1"],"couplings":[["builtin_coupling_167"]],"lorentz_structures":["builtin_kind_19"],"name":"builtin_vertex_167","particles":["w+","pdg_126","w+"]},{"builtin_coupling":[-2.2497543582396506,0.0],"builtin_kind":19,"color_structures":["1"],"couplings":[["builtin_coupling_168"]],"lorentz_structures":["builtin_kind_19"],"name":"builtin_vertex_168","particles":["w-","pdg_126","w-"]},{"builtin_coupling":[-1.0,-1.0],"builtin_kind":21,"color_structures":["1"],"couplings":[["builtin_coupling_169"]],"lorentz_structures":["builtin_kind_21"],"name":"builtin_vertex_169","particles":["e-","e+","a"]},{"builtin_coupling":[-1.0,-1.0],"builtin_kind":22,"color_structures":["1"],"couplings":[["builtin_coupling_170"]],"lorentz_structures":["builtin_kind_22"],"name":"builtin_vertex_170","particles":["e+","e-","a"]},{"builtin_coupling":[-0.6680686926882882,0.5345600023586237],"builtin_kind":21,"color_structures":["1"],"couplings":[["builtin_coupling_171"]],"lorentz_structures":["builtin_kind_21"],"name":"builtin_vertex_171","particles":["e-","e+","z"]},{"builtin_coupling":[-0.6680686926882882,0.5345600023586237],"builtin_kind":22,"color_structures":["1"],"couplings":[["builtin_coupling_172"]],"lorentz_structures":["builtin_kind_22"],"name":"builtin_vertex_172","particles":["e+","e-","z"]},{"builtin_coupling":[1.4999181171782847,0.0],"builtin_kind":21,"color_structures":["1"],"couplings":[["builtin_coupling_173"]],"lorentz_structures":["builtin_kind_21"],"name":"builtin_vertex_173","particles":["e-","ve~","w-"]},{"builtin_coupling":[1.4999181171782847,0.0],"builtin_kind":22,"color_structures":["1"],"couplings":[["builtin_coupling_174"]],"lorentz_structures":["builtin_kind_22"],"name":"builtin_vertex_174","particles":["e+","ve","w+"]},{"builtin_coupling":[-1.0,-1.0],"builtin_kind":10,"color_structures":["1"],"couplings":[["builtin_coupling_175"]],"lorentz_structures":["builtin_kind_10"],"name":"builtin_vertex_175","particles":["e-","a","e-"]},{"builtin_coupling":[-1.0,-1.0],"builtin_kind":11,"color_structures":["1"],"couplings":[["builtin_coupling_176"]],"lorentz_structures":["builtin_kind_11"],"name":"builtin_vertex_176","particles":["e+","a","e+"]},{"builtin_coupling":[-1.0,-1.0],"builtin_kind":23,"color_structures":["1"],"couplings":[["builtin_coupling_177"]],"lorentz_structures":["builtin_kind_23"],"name":"builtin_vertex_177","particles":["a","e-","e-"]},{"builtin_coupling":[-1.0,-1.0],"builtin_kind":24,"color_structures":["1"],"couplings":[["builtin_coupling_178"]],"lorentz_structures":["builtin_kind_24"],"name":"builtin_vertex_178","particles":["a","e+","e+"]},{"builtin_coupling":[-0.6680686926882882,0.5345600023586237],"builtin_kind":10,"color_structures":["1"],"couplings":[["builtin_coupling_179"]],"lorentz_structures":["builtin_kind_10"],"name":"builtin_vertex_179","particles":["e-","z","e-"]},{"builtin_coupling":[-0.6680686926882882,0.5345600023586237],"builtin_kind":11,"color_structures":["1"],"couplings":[["builtin_coupling_180"]],"lorentz_structures":["builtin_kind_11"],"name":"builtin_vertex_180","particles":["e+","z","e+"]},{"builtin_coupling":[-0.6680686926882882,0.5345600023586237],"builtin_kind":23,"color_structures":["1"],"couplings":[["builtin_coupling_181"]],"lorentz_structures":["builtin_kind_23"],"name":"builtin_vertex_181","particles":["z","e-","e-"]},{"builtin_coupling":[-0.6680686926882882,0.5345600023586237],"builtin_kind":24,"color_structures":["1"],"couplings":[["builtin_coupling_182"]],"lorentz_structures":["builtin_kind_24"],"name":"builtin_vertex_182","particles":["z","e+","e+"]},{"builtin_coupling":[-1.0,-1.0],"builtin_kind":21,"color_structures":["1"],"couplings":[["builtin_coupling_183"]],"lorentz_structures":["builtin_kind_21"],"name":"builtin_vertex_183","particles":["mu-","mu+","a"]},{"builtin_coupling":[-1.0,-1.0],"builtin_kind":22,"color_structures":["1"],"couplings":[["builtin_coupling_184"]],"lorentz_structures":["builtin_kind_22"],"name":"builtin_vertex_184","particles":["mu+","mu-","a"]},{"builtin_coupling":[-0.6680686926882882,0.5345600023586237],"builtin_kind":21,"color_structures":["1"],"couplings":[["builtin_coupling_185"]],"lorentz_structures":["builtin_kind_21"],"name":"builtin_vertex_185","particles":["mu-","mu+","z"]},{"builtin_coupling":[-0.6680686926882882,0.5345600023586237],"builtin_kind":22,"color_structures":["1"],"couplings":[["builtin_coupling_186"]],"lorentz_structures":["builtin_kind_22"],"name":"builtin_vertex_186","particles":["mu+","mu-","z"]},{"builtin_coupling":[1.4999181171782847,0.0],"builtin_kind":21,"color_structures":["1"],"couplings":[["builtin_coupling_187"]],"lorentz_structures":["builtin_kind_21"],"name":"builtin_vertex_187","particles":["mu-","vm~","w-"]},{"builtin_coupling":[1.4999181171782847,0.0],"builtin_kind":22,"color_structures":["1"],"couplings":[["builtin_coupling_188"]],"lorentz_structures":["builtin_kind_22"],"name":"builtin_vertex_188","particles":["mu+","vm","w+"]},{"builtin_coupling":[-1.0,-1.0],"builtin_kind":10,"color_structures":["1"],"couplings":[["builtin_coupling_189"]],"lorentz_structures":["builtin_kind_10"],"name":"builtin_vertex_189","particles":["mu-","a","mu-"]},{"builtin_coupling":[-1.0,-1.0],"builtin_kind":11,"color_structures":["1"],"couplings":[["builtin_coupling_190"]],"lorentz_structures":["builtin_kind_11"],"name":"builtin_vertex_190","particles":["mu+","a","mu+"]},{"builtin_coupling":[-1.0,-1.0],"builtin_kind":23,"color_structures":["1"],"couplings":[["builtin_coupling_191"]],"lorentz_structures":["builtin_kind_23"],"name":"builtin_vertex_191","particles":["a","mu-","mu-"]},{"builtin_coupling":[-1.0,-1.0],"builtin_kind":24,"color_structures":["1"],"couplings":[["builtin_coupling_192"]],"lorentz_structures":["builtin_kind_24"],"name":"builtin_vertex_192","particles":["a","mu+","mu+"]},{"builtin_coupling":[-0.6680686926882882,0.5345600023586237],"builtin_kind":10,"color_structures":["1"],"couplings":[["builtin_coupling_193"]],"lorentz_structures":["builtin_kind_10"],"name":"builtin_vertex_193","particles":["mu-","z","mu-"]},{"builtin_coupling":[-0.6680686926882882,0.5345600023586237],"builtin_kind":11,"color_structures":["1"],"couplings":[["builtin_coupling_194"]],"lorentz_structures":["builtin_kind_11"],"name":"builtin_vertex_194","particles":["mu+","z","mu+"]},{"builtin_coupling":[-0.6680686926882882,0.5345600023586237],"builtin_kind":23,"color_structures":["1"],"couplings":[["builtin_coupling_195"]],"lorentz_structures":["builtin_kind_23"],"name":"builtin_vertex_195","particles":["z","mu-","mu-"]},{"builtin_coupling":[-0.6680686926882882,0.5345600023586237],"builtin_kind":24,"color_structures":["1"],"couplings":[["builtin_coupling_196"]],"lorentz_structures":["builtin_kind_24"],"name":"builtin_vertex_196","particles":["z","mu+","mu+"]},{"builtin_coupling":[-1.0,-1.0],"builtin_kind":21,"color_structures":["1"],"couplings":[["builtin_coupling_197"]],"lorentz_structures":["builtin_kind_21"],"name":"builtin_vertex_197","particles":["ta-","ta+","a"]},{"builtin_coupling":[-1.0,-1.0],"builtin_kind":22,"color_structures":["1"],"couplings":[["builtin_coupling_198"]],"lorentz_structures":["builtin_kind_22"],"name":"builtin_vertex_198","particles":["ta+","ta-","a"]},{"builtin_coupling":[-0.6680686926882882,0.5345600023586237],"builtin_kind":21,"color_structures":["1"],"couplings":[["builtin_coupling_199"]],"lorentz_structures":["builtin_kind_21"],"name":"builtin_vertex_199","particles":["ta-","ta+","z"]},{"builtin_coupling":[-0.6680686926882882,0.5345600023586237],"builtin_kind":22,"color_structures":["1"],"couplings":[["builtin_coupling_200"]],"lorentz_structures":["builtin_kind_22"],"name":"builtin_vertex_200","particles":["ta+","ta-","z"]},{"builtin_coupling":[1.4999181171782847,0.0],"builtin_kind":21,"color_structures":["1"],"couplings":[["builtin_coupling_201"]],"lorentz_structures":["builtin_kind_21"],"name":"builtin_vertex_201","particles":["ta-","vt~","w-"]},{"builtin_coupling":[1.4999181171782847,0.0],"builtin_kind":22,"color_structures":["1"],"couplings":[["builtin_coupling_202"]],"lorentz_structures":["builtin_kind_22"],"name":"builtin_vertex_202","particles":["ta+","vt","w+"]},{"builtin_coupling":[-1.0,-1.0],"builtin_kind":10,"color_structures":["1"],"couplings":[["builtin_coupling_203"]],"lorentz_structures":["builtin_kind_10"],"name":"builtin_vertex_203","particles":["ta-","a","ta-"]},{"builtin_coupling":[-1.0,-1.0],"builtin_kind":11,"color_structures":["1"],"couplings":[["builtin_coupling_204"]],"lorentz_structures":["builtin_kind_11"],"name":"builtin_vertex_204","particles":["ta+","a","ta+"]},{"builtin_coupling":[-1.0,-1.0],"builtin_kind":23,"color_structures":["1"],"couplings":[["builtin_coupling_205"]],"lorentz_structures":["builtin_kind_23"],"name":"builtin_vertex_205","particles":["a","ta-","ta-"]},{"builtin_coupling":[-1.0,-1.0],"builtin_kind":24,"color_structures":["1"],"couplings":[["builtin_coupling_206"]],"lorentz_structures":["builtin_kind_24"],"name":"builtin_vertex_206","particles":["a","ta+","ta+"]},{"builtin_coupling":[-0.6680686926882882,0.5345600023586237],"builtin_kind":10,"color_structures":["1"],"couplings":[["builtin_coupling_207"]],"lorentz_structures":["builtin_kind_10"],"name":"builtin_vertex_207","particles":["ta-","z","ta-"]},{"builtin_coupling":[-0.6680686926882882,0.5345600023586237],"builtin_kind":11,"color_structures":["1"],"couplings":[["builtin_coupling_208"]],"lorentz_structures":["builtin_kind_11"],"name":"builtin_vertex_208","particles":["ta+","z","ta+"]},{"builtin_coupling":[-0.6680686926882882,0.5345600023586237],"builtin_kind":23,"color_structures":["1"],"couplings":[["builtin_coupling_209"]],"lorentz_structures":["builtin_kind_23"],"name":"builtin_vertex_209","particles":["z","ta-","ta-"]},{"builtin_coupling":[-0.6680686926882882,0.5345600023586237],"builtin_kind":24,"color_structures":["1"],"couplings":[["builtin_coupling_210"]],"lorentz_structures":["builtin_kind_24"],"name":"builtin_vertex_210","particles":["z","ta+","ta+"]}]},"model_compiler_version":16,"parameter_defaults":{"alpha_ew":[0.0075467711139788835,0.0],"alpha_s":[0.118,0.0],"mass_23":[91.188,0.0],"mass_24":[80.41900244575616,0.0],"mass_25":[125.0,0.0],"mass_6":[173.0,0.0],"width_23":[2.441404,0.0],"width_24":[2.0476,0.0],"width_25":[0.006382339,0.0],"width_6":[1.4915,0.0]},"phase_timings":{"model_loading":0.0028243749402463436,"preflight":0.00040449993684887886,"tensor_lowering":0.006884499918669462,"total":0.010124958120286465},"producer":{"compiled_model_schema_version":9,"contact_decomposition_policy":"ufo-four-point-contact-decomposition-v2","model_compiler_sha256":"2a3cce12de8775926f302ed07706c45298727ffe690c3379b010012f7a7b77f8","model_compiler_version":16,"model_environment_policy":"sanitize-historical-scalar-options-v1","pyamplicol":"1.0.0","symbol_namespace_policy":"model-name-and-pyamplicol-registry-v1","symbolica":"3.0.0","symbolica_serialization_abi":"symbolica-bincode2-v1","tensor_ordering_contract":"explicit-canonical-component-order-v1","ufo_model_loader":"1.0.0"},"schema_version":9,"source":{"digest":"db50b0cb34998c1a9f7bf63c2b7a4d80c01f373a00d9bee81372b2d43a506962","kind":"built-in-sm","options":{"restriction":{"kind":"name","value":"default"},"simplify":true},"source_name":null}} diff --git a/pyamplicol/src/pyamplicol/assets/selftest/portable-64le/artifact/model/parameters.json b/pyamplicol/src/pyamplicol/assets/selftest/portable-64le/artifact/model/parameters.json new file mode 100644 index 0000000000..3de027c9a2 --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/selftest/portable-64le/artifact/model/parameters.json @@ -0,0 +1,46 @@ +{ + "kind": "pyamplicol-model-parameter-defaults", + "parameters": { + "alpha_ew": [ + 0.0075467711139788835, + 0.0 + ], + "alpha_s": [ + 0.118, + 0.0 + ], + "mass_23": [ + 91.188, + 0.0 + ], + "mass_24": [ + 80.41900244575616, + 0.0 + ], + "mass_25": [ + 125.0, + 0.0 + ], + "mass_6": [ + 173.0, + 0.0 + ], + "width_23": [ + 2.441404, + 0.0 + ], + "width_24": [ + 2.0476, + 0.0 + ], + "width_25": [ + 0.006382339, + 0.0 + ], + "width_6": [ + 1.4915, + 0.0 + ] + }, + "schema_version": 1 +} diff --git a/pyamplicol/src/pyamplicol/assets/selftest/portable-64le/artifact/processes/d_dbar_to_z/execution.json b/pyamplicol/src/pyamplicol/assets/selftest/portable-64le/artifact/processes/d_dbar_to_z/execution.json new file mode 100644 index 0000000000..ef4185d726 --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/selftest/portable-64le/artifact/processes/d_dbar_to_z/execution.json @@ -0,0 +1 @@ +{"color_accuracy":"lc","compiled":{"helicity_recurrence":{"amplitude_classes":[{"class_id":"helicity-amplitude-sha256:d56fc183f4e6996740930cb59d082773bc739fdc54de6c684a44b2d643911365","members":[{"factor":[1.0,0.0],"root_id":0,"selector_domain_ids":[8]},{"factor":[1.0,0.0],"root_id":1,"selector_domain_ids":[9]},{"factor":[1.0,0.0],"root_id":2,"selector_domain_ids":[10]}],"proof":{"algorithm":"canonical-source-transition-dependency-shape-v1","digest":"d56fc183f4e6996740930cb59d082773bc739fdc54de6c684a44b2d643911365","status":"proven","transition_contract_ids":[]},"representative_root_id":0},{"class_id":"helicity-amplitude-sha256:19760a2fbd284e1f9c6b8d22028a99181ead6fa6e4437c0c42786703785a310a","members":[{"factor":[1.0,0.0],"root_id":3,"selector_domain_ids":[4]},{"factor":[1.0,0.0],"root_id":4,"selector_domain_ids":[5]},{"factor":[1.0,0.0],"root_id":5,"selector_domain_ids":[6]}],"proof":{"algorithm":"canonical-source-transition-dependency-shape-v1","digest":"19760a2fbd284e1f9c6b8d22028a99181ead6fa6e4437c0c42786703785a310a","status":"proven","transition_contract_ids":[]},"representative_root_id":3}],"amplitude_root_count":6,"contract_version":1,"current_count":13,"diagnostics":[],"kind":"pyamplicol-helicity-recurrence","materialization":{"amplitude_routes":[{"factor":[1.0,0.0],"materialized_root_id":0,"residual":false,"selector_domain_ids":[8]},{"factor":[1.0,0.0],"materialized_root_id":0,"residual":false,"selector_domain_ids":[9]},{"factor":[1.0,0.0],"materialized_root_id":0,"residual":false,"selector_domain_ids":[10]},{"factor":[1.0,0.0],"materialized_root_id":1,"residual":false,"selector_domain_ids":[4]},{"factor":[1.0,0.0],"materialized_root_id":1,"residual":false,"selector_domain_ids":[5]},{"factor":[1.0,0.0],"materialized_root_id":1,"residual":false,"selector_domain_ids":[6]}],"contract_version":1,"kind":"pyamplicol-helicity-recurrence-materialization","materialized_current_count":7,"materialized_root_count":2,"proof_current_count":13,"proof_root_count":6,"proof_to_materialized_current":[0,1,2,3,4,4,4,5,5,5,6,6,6],"selector_schedules":[{"active_current_ids":[],"active_root_ids":[],"selector_domain_id":1,"structural_zero":true},{"active_current_ids":[],"active_root_ids":[],"selector_domain_id":2,"structural_zero":true},{"active_current_ids":[],"active_root_ids":[],"selector_domain_id":3,"structural_zero":true},{"active_current_ids":[0,3,4,6],"active_root_ids":[1],"selector_domain_id":4,"structural_zero":false},{"active_current_ids":[0,3,4,6],"active_root_ids":[1],"selector_domain_id":5,"structural_zero":false},{"active_current_ids":[0,3,4,6],"active_root_ids":[1],"selector_domain_id":6,"structural_zero":false},{"active_current_ids":[1,2,4,5],"active_root_ids":[0],"selector_domain_id":8,"structural_zero":false},{"active_current_ids":[1,2,4,5],"active_root_ids":[0],"selector_domain_id":9,"structural_zero":false},{"active_current_ids":[1,2,4,5],"active_root_ids":[0],"selector_domain_id":10,"structural_zero":false},{"active_current_ids":[],"active_root_ids":[],"selector_domain_id":11,"structural_zero":true},{"active_current_ids":[],"active_root_ids":[],"selector_domain_id":12,"structural_zero":true},{"active_current_ids":[],"active_root_ids":[],"selector_domain_id":13,"structural_zero":true}],"source_routes":[{"chirality":1,"declared_state_index":0,"external_label":1,"factor":[1.0,0.0],"helicity":-1,"materialized_current_id":0,"selector_domain_id":0,"spin_state":1},{"chirality":-1,"declared_state_index":1,"external_label":1,"factor":[1.0,0.0],"helicity":1,"materialized_current_id":1,"selector_domain_id":7,"spin_state":-1},{"chirality":1,"declared_state_index":0,"external_label":2,"factor":[1.0,0.0],"helicity":-1,"materialized_current_id":2,"selector_domain_id":14,"spin_state":1},{"chirality":-1,"declared_state_index":1,"external_label":2,"factor":[1.0,0.0],"helicity":1,"materialized_current_id":3,"selector_domain_id":18,"spin_state":-1},{"chirality":0,"declared_state_index":0,"external_label":3,"factor":[1.0,0.0],"helicity":-1,"materialized_current_id":4,"selector_domain_id":22,"spin_state":-1},{"chirality":0,"declared_state_index":1,"external_label":3,"factor":[1.0,0.0],"helicity":0,"materialized_current_id":4,"selector_domain_id":23,"spin_state":0},{"chirality":0,"declared_state_index":2,"external_label":3,"factor":[1.0,0.0],"helicity":1,"materialized_current_id":4,"selector_domain_id":24,"spin_state":1}],"strategy":"quotient"},"proof_algorithm":"canonical-source-transition-dependency-shape-v1","proof_counts":{"amplitude_class_count":2,"optimized_amplitude_class_count":2,"optimized_current_count":9,"optimized_recurrence_class_count":3,"physical_helicity_count":12,"recurrence_class_count":7,"residual_amplitude_count":0,"residual_current_count":0,"source_state_mapping_count":7,"structural_zero_helicity_count":6},"recurrence_classes":[{"class_id":"helicity-current-sha256:8c682736c4ae8e56465e5fd7b1c300f8379118af92486f9a571ca3a915572ccf","external_labels":[1],"members":[{"current_id":0,"factor":[1.0,0.0],"selector_domain_id":0}],"proof":{"algorithm":"canonical-source-transition-dependency-shape-v1","digest":"8c682736c4ae8e56465e5fd7b1c300f8379118af92486f9a571ca3a915572ccf","status":"proven","transition_contract_ids":[]},"representative_current_id":0,"source_class":true},{"class_id":"helicity-current-sha256:20f4ac2ac00a74044681ff5aab37d2abc31e375d1473e8b0a8198af4432c0ad4","external_labels":[1],"members":[{"current_id":1,"factor":[1.0,0.0],"selector_domain_id":7}],"proof":{"algorithm":"canonical-source-transition-dependency-shape-v1","digest":"20f4ac2ac00a74044681ff5aab37d2abc31e375d1473e8b0a8198af4432c0ad4","status":"proven","transition_contract_ids":[]},"representative_current_id":1,"source_class":true},{"class_id":"helicity-current-sha256:099ab31b76e6d55df90ac39798446ac28b3bbca173b59d516940595c4f951ec7","external_labels":[2],"members":[{"current_id":2,"factor":[1.0,0.0],"selector_domain_id":14}],"proof":{"algorithm":"canonical-source-transition-dependency-shape-v1","digest":"099ab31b76e6d55df90ac39798446ac28b3bbca173b59d516940595c4f951ec7","status":"proven","transition_contract_ids":[]},"representative_current_id":2,"source_class":true},{"class_id":"helicity-current-sha256:1e587b1494ff92be5c28fa63676222417931b6d82f93eb12232f90e10bc22924","external_labels":[2],"members":[{"current_id":3,"factor":[1.0,0.0],"selector_domain_id":18}],"proof":{"algorithm":"canonical-source-transition-dependency-shape-v1","digest":"1e587b1494ff92be5c28fa63676222417931b6d82f93eb12232f90e10bc22924","status":"proven","transition_contract_ids":[]},"representative_current_id":3,"source_class":true},{"class_id":"helicity-current-sha256:e9562e75376f52432b31f6754f778a8f5ab63333f8169428a2bbb50dcee492d0","external_labels":[3],"members":[{"current_id":4,"factor":[1.0,0.0],"selector_domain_id":22},{"current_id":5,"factor":[1.0,0.0],"selector_domain_id":23},{"current_id":6,"factor":[1.0,0.0],"selector_domain_id":24}],"proof":{"algorithm":"canonical-source-transition-dependency-shape-v1","digest":"e9562e75376f52432b31f6754f778a8f5ab63333f8169428a2bbb50dcee492d0","status":"proven","transition_contract_ids":[]},"representative_current_id":4,"source_class":true},{"class_id":"helicity-current-sha256:11b0f53aeaf5619f44673e1ac3a81db4742588d5f49c08fa7f514b0556bcada7","external_labels":[2,3],"members":[{"current_id":7,"factor":[1.0,0.0],"selector_domain_id":15},{"current_id":8,"factor":[1.0,0.0],"selector_domain_id":16},{"current_id":9,"factor":[1.0,0.0],"selector_domain_id":17}],"proof":{"algorithm":"canonical-source-transition-dependency-shape-v1","digest":"11b0f53aeaf5619f44673e1ac3a81db4742588d5f49c08fa7f514b0556bcada7","status":"proven","transition_contract_ids":["builtin-sm:fermion-vector-to-fermion"]},"representative_current_id":7,"source_class":false},{"class_id":"helicity-current-sha256:7690f565a16395860f30f2971bfea1c77a8db9d3e60d91d4e6b2d2f5549153a8","external_labels":[2,3],"members":[{"current_id":10,"factor":[1.0,0.0],"selector_domain_id":19},{"current_id":11,"factor":[1.0,0.0],"selector_domain_id":20},{"current_id":12,"factor":[1.0,0.0],"selector_domain_id":21}],"proof":{"algorithm":"canonical-source-transition-dependency-shape-v1","digest":"7690f565a16395860f30f2971bfea1c77a8db9d3e60d91d4e6b2d2f5549153a8","status":"proven","transition_contract_ids":["builtin-sm:fermion-vector-to-fermion"]},"representative_current_id":10,"source_class":false}],"residual_current_ids":[],"residual_root_ids":[],"selector_domains":[{"complete":false,"id":0,"source_states":[{"external_label":1,"helicity":-1}]},{"complete":true,"id":1,"source_states":[{"external_label":1,"helicity":-1},{"external_label":2,"helicity":-1},{"external_label":3,"helicity":-1}]},{"complete":true,"id":2,"source_states":[{"external_label":1,"helicity":-1},{"external_label":2,"helicity":-1},{"external_label":3,"helicity":0}]},{"complete":true,"id":3,"source_states":[{"external_label":1,"helicity":-1},{"external_label":2,"helicity":-1},{"external_label":3,"helicity":1}]},{"complete":true,"id":4,"source_states":[{"external_label":1,"helicity":-1},{"external_label":2,"helicity":1},{"external_label":3,"helicity":-1}]},{"complete":true,"id":5,"source_states":[{"external_label":1,"helicity":-1},{"external_label":2,"helicity":1},{"external_label":3,"helicity":0}]},{"complete":true,"id":6,"source_states":[{"external_label":1,"helicity":-1},{"external_label":2,"helicity":1},{"external_label":3,"helicity":1}]},{"complete":false,"id":7,"source_states":[{"external_label":1,"helicity":1}]},{"complete":true,"id":8,"source_states":[{"external_label":1,"helicity":1},{"external_label":2,"helicity":-1},{"external_label":3,"helicity":-1}]},{"complete":true,"id":9,"source_states":[{"external_label":1,"helicity":1},{"external_label":2,"helicity":-1},{"external_label":3,"helicity":0}]},{"complete":true,"id":10,"source_states":[{"external_label":1,"helicity":1},{"external_label":2,"helicity":-1},{"external_label":3,"helicity":1}]},{"complete":true,"id":11,"source_states":[{"external_label":1,"helicity":1},{"external_label":2,"helicity":1},{"external_label":3,"helicity":-1}]},{"complete":true,"id":12,"source_states":[{"external_label":1,"helicity":1},{"external_label":2,"helicity":1},{"external_label":3,"helicity":0}]},{"complete":true,"id":13,"source_states":[{"external_label":1,"helicity":1},{"external_label":2,"helicity":1},{"external_label":3,"helicity":1}]},{"complete":false,"id":14,"source_states":[{"external_label":2,"helicity":-1}]},{"complete":false,"id":15,"source_states":[{"external_label":2,"helicity":-1},{"external_label":3,"helicity":-1}]},{"complete":false,"id":16,"source_states":[{"external_label":2,"helicity":-1},{"external_label":3,"helicity":0}]},{"complete":false,"id":17,"source_states":[{"external_label":2,"helicity":-1},{"external_label":3,"helicity":1}]},{"complete":false,"id":18,"source_states":[{"external_label":2,"helicity":1}]},{"complete":false,"id":19,"source_states":[{"external_label":2,"helicity":1},{"external_label":3,"helicity":-1}]},{"complete":false,"id":20,"source_states":[{"external_label":2,"helicity":1},{"external_label":3,"helicity":0}]},{"complete":false,"id":21,"source_states":[{"external_label":2,"helicity":1},{"external_label":3,"helicity":1}]},{"complete":false,"id":22,"source_states":[{"external_label":3,"helicity":-1}]},{"complete":false,"id":23,"source_states":[{"external_label":3,"helicity":0}]},{"complete":false,"id":24,"source_states":[{"external_label":3,"helicity":1}]}],"source_state_mappings":[{"chirality":1,"current_id":0,"declared_state_index":0,"external_label":1,"factor":[1.0,0.0],"helicity":-1,"recurrence_class_id":"helicity-current-sha256:8c682736c4ae8e56465e5fd7b1c300f8379118af92486f9a571ca3a915572ccf","representative_current_id":0,"selector_domain_id":0,"source_contract_digest":"44fa57cb6571ac7524f9e30554d29cf20f6e4fbdfd443edf0afdbc8617c0620e","spin_state":1},{"chirality":-1,"current_id":1,"declared_state_index":1,"external_label":1,"factor":[1.0,0.0],"helicity":1,"recurrence_class_id":"helicity-current-sha256:20f4ac2ac00a74044681ff5aab37d2abc31e375d1473e8b0a8198af4432c0ad4","representative_current_id":1,"selector_domain_id":7,"source_contract_digest":"44fa57cb6571ac7524f9e30554d29cf20f6e4fbdfd443edf0afdbc8617c0620e","spin_state":-1},{"chirality":1,"current_id":2,"declared_state_index":0,"external_label":2,"factor":[1.0,0.0],"helicity":-1,"recurrence_class_id":"helicity-current-sha256:099ab31b76e6d55df90ac39798446ac28b3bbca173b59d516940595c4f951ec7","representative_current_id":2,"selector_domain_id":14,"source_contract_digest":"98b493b16090bfd79101115ca8697310721cebf636cced331f90bcbc32cd9ef6","spin_state":1},{"chirality":-1,"current_id":3,"declared_state_index":1,"external_label":2,"factor":[1.0,0.0],"helicity":1,"recurrence_class_id":"helicity-current-sha256:1e587b1494ff92be5c28fa63676222417931b6d82f93eb12232f90e10bc22924","representative_current_id":3,"selector_domain_id":18,"source_contract_digest":"98b493b16090bfd79101115ca8697310721cebf636cced331f90bcbc32cd9ef6","spin_state":-1},{"chirality":0,"current_id":4,"declared_state_index":0,"external_label":3,"factor":[1.0,0.0],"helicity":-1,"recurrence_class_id":"helicity-current-sha256:e9562e75376f52432b31f6754f778a8f5ab63333f8169428a2bbb50dcee492d0","representative_current_id":4,"selector_domain_id":22,"source_contract_digest":"c2bcedb500cc3f9692e3626cf25b332071eba27ad54025db1ddeaa31e0b57124","spin_state":-1},{"chirality":0,"current_id":5,"declared_state_index":1,"external_label":3,"factor":[1.0,0.0],"helicity":0,"recurrence_class_id":"helicity-current-sha256:e9562e75376f52432b31f6754f778a8f5ab63333f8169428a2bbb50dcee492d0","representative_current_id":4,"selector_domain_id":23,"source_contract_digest":"c2bcedb500cc3f9692e3626cf25b332071eba27ad54025db1ddeaa31e0b57124","spin_state":0},{"chirality":0,"current_id":6,"declared_state_index":2,"external_label":3,"factor":[1.0,0.0],"helicity":1,"recurrence_class_id":"helicity-current-sha256:e9562e75376f52432b31f6754f778a8f5ab63333f8169428a2bbb50dcee492d0","representative_current_id":4,"selector_domain_id":24,"source_contract_digest":"c2bcedb500cc3f9692e3626cf25b332071eba27ad54025db1ddeaa31e0b57124","spin_state":1}],"structural_zero_selector_domain_ids":[1,2,3,11,12,13]},"kind":"generic-dag-stage-blueprint","model_parameter_evaluator":null,"momentum_slot_ids":[],"runtime_available":true,"runtime_unavailable_message":null,"stage_evaluators":{"amplitude_stage":{"blockers":[],"compiled_plane_arena":{"application_abi":"pyamplicol-compiled-plane-kernel-v2","element_layout":"split-complex-component-major","input_bindings":[{"component":0,"global_component":0,"kind":"value","parameter_index":0,"real_valued":false,"source_id":0},{"component":1,"global_component":1,"kind":"value","parameter_index":1,"real_valued":false,"source_id":0},{"component":0,"global_component":2,"kind":"value","parameter_index":2,"real_valued":false,"source_id":1},{"component":1,"global_component":3,"kind":"value","parameter_index":3,"real_valued":false,"source_id":1},{"component":0,"global_component":12,"kind":"value","parameter_index":4,"real_valued":false,"source_id":5},{"component":1,"global_component":13,"kind":"value","parameter_index":5,"real_valued":false,"source_id":5},{"component":0,"global_component":14,"kind":"value","parameter_index":6,"real_valued":false,"source_id":6},{"component":1,"global_component":15,"kind":"value","parameter_index":7,"real_valued":false,"source_id":6}],"input_output_aliasing":"forbidden","kind":"compiled-plane-arena-stage","leaves":[{"application_path":"evaluators/generic_amplitude_stage_chunk_0_4b58da2eb1324c828330d810090ae21f.plane.symjit","direct_codegen_optimization_level":2,"input_indices":[2,3,4,5],"input_len":4,"optimization_level":2,"output_len":1,"output_start":0,"output_stop":1,"source_application_abi":"pyamplicol-symjit-plane-application-v2"},{"application_path":"evaluators/generic_amplitude_stage_chunk_1_7966f81ea457430fb1873046b2986dab.plane.symjit","direct_codegen_optimization_level":2,"input_indices":[0,1,6,7],"input_len":4,"optimization_level":2,"output_len":1,"output_start":1,"output_stop":2,"source_application_abi":"pyamplicol-symjit-plane-application-v2"}],"output_bindings":[{"arena":"amplitude","component":0,"output_index":0},{"arena":"amplitude","component":1,"output_index":1}],"output_factor":"identity","output_operation":"overwrite","output_output_aliasing":"forbidden","schema_version":1,"source_application_abi":"pyamplicol-symjit-plane-application-v2"},"evaluator":{"chunk_input_indices":[[2,3,4,5],[0,1,6,7]],"chunks":[{"application_abi":"symjit-application-storage-v3","application_path":"evaluators/generic_amplitude_stage_chunk_0_4b58da2eb1324c828330d810090ae21f.symjit","batch_layout":"row-major","compiler_type":"native","element_layout":"complex-f64","endianness":"little","evaluator_state_path":null,"evaluator_state_runtime_capability":null,"input_len":4,"kind":"symjit-application-evaluator","optimization_level":2,"output_len":1,"plane_application":{"application_abi":"pyamplicol-symjit-plane-application-v2","application_path":"evaluators/generic_amplitude_stage_chunk_0_4b58da2eb1324c828330d810090ae21f.plane.symjit","compiler_type":"native","complex":true,"compression":true,"descriptor_order":"inputs-re-im-then-outputs-re-im","direct_arena":true,"element_layout":"split-complex-plane-major","fast_complex":false,"fast_math":true,"input_complex_count":4,"input_plane_count":8,"optimization_level":2,"output_complex_count":1,"output_plane_count":2,"simd":true,"source_digest":"db70c7e9412289ef9f4406c1ffbfe8144dc3c67d5801fbf0342dc26d8f30d6ad","storage_abi":"symjit-application-storage-v3","target":{"cpu_features":["aes","bf16","bti","crc","dit","dotprod","dpb","dpb2","fcma","fhm","flagm","fp","fp16","frintts","i8mm","jsconv","lse","lse2","neon","paca","pacg","rcpc","rcpc2","rdm","sb","sha2","sha3","ssbs"],"endianness":"little","triple":"aarch64-apple-darwin","word_bits":64},"threading":false,"translation_mode":"symbolica-structured-instructions"},"required_defuns":[],"runtime_capability":"symjit.application.complex-f64.v1","translation_mode":"indirect","word_bits":64},{"application_abi":"symjit-application-storage-v3","application_path":"evaluators/generic_amplitude_stage_chunk_1_7966f81ea457430fb1873046b2986dab.symjit","batch_layout":"row-major","compiler_type":"native","element_layout":"complex-f64","endianness":"little","evaluator_state_path":null,"evaluator_state_runtime_capability":null,"input_len":4,"kind":"symjit-application-evaluator","optimization_level":2,"output_len":1,"plane_application":{"application_abi":"pyamplicol-symjit-plane-application-v2","application_path":"evaluators/generic_amplitude_stage_chunk_1_7966f81ea457430fb1873046b2986dab.plane.symjit","compiler_type":"native","complex":true,"compression":true,"descriptor_order":"inputs-re-im-then-outputs-re-im","direct_arena":true,"element_layout":"split-complex-plane-major","fast_complex":false,"fast_math":true,"input_complex_count":4,"input_plane_count":8,"optimization_level":2,"output_complex_count":1,"output_plane_count":2,"simd":true,"source_digest":"db70c7e9412289ef9f4406c1ffbfe8144dc3c67d5801fbf0342dc26d8f30d6ad","storage_abi":"symjit-application-storage-v3","target":{"cpu_features":["aes","bf16","bti","crc","dit","dotprod","dpb","dpb2","fcma","fhm","flagm","fp","fp16","frintts","i8mm","jsconv","lse","lse2","neon","paca","pacg","rcpc","rcpc2","rdm","sb","sha2","sha3","ssbs"],"endianness":"little","triple":"aarch64-apple-darwin","word_bits":64},"threading":false,"translation_mode":"symbolica-structured-instructions"},"required_defuns":[],"runtime_capability":"symjit.application.complex-f64.v1","translation_mode":"indirect","word_bits":64}],"input_len":8,"kind":"chunked-symbolica-evaluator","required_runtime_capabilities":["symjit.application.complex-f64.v1"]},"evaluator_label":"generic_amplitude_stage","expression_ready":true,"input_components":[{"component":0,"global_component":0,"kind":"value","parameter_index":0,"real_valued":false,"source_id":0},{"component":1,"global_component":1,"kind":"value","parameter_index":1,"real_valued":false,"source_id":0},{"component":0,"global_component":2,"kind":"value","parameter_index":2,"real_valued":false,"source_id":1},{"component":1,"global_component":3,"kind":"value","parameter_index":3,"real_valued":false,"source_id":1},{"component":0,"global_component":12,"kind":"value","parameter_index":4,"real_valued":false,"source_id":5},{"component":1,"global_component":13,"kind":"value","parameter_index":5,"real_valued":false,"source_id":5},{"component":0,"global_component":14,"kind":"value","parameter_index":6,"real_valued":false,"source_id":6},{"component":1,"global_component":15,"kind":"value","parameter_index":7,"real_valued":false,"source_id":6}],"input_value_slot_ids":[0,1,5,6],"interaction_ids":[],"model_parameter_count":0,"momentum_parameter_count":0,"output_length":2,"output_slots":[{"color_selector_domain_ids":[0],"component_start":0,"component_stop":1,"current_id":-1,"output_start":0,"output_stop":1,"value_slot_id":-1,"variant":"amplitude-root"},{"color_selector_domain_ids":[0],"component_start":1,"component_stop":2,"current_id":-1,"output_start":1,"output_stop":2,"value_slot_id":-1,"variant":"amplitude-root"}],"output_value_slot_ids":[],"parameter_count":8,"parameter_layout":"stage-local-value-momentum","real_valued_inputs":[],"stage_index":0,"stage_kind":"amplitude-roots","subset_size":null,"value_parameter_count":8},"kind":"generic-dag-stage-evaluator-artifacts","model_parameter_count":0,"momentum_parameter_count":0,"parameter_count":0,"parameter_layout":"stage-local-value-momentum","real_valued_inputs":[],"required_runtime_capabilities":["compiled-plane-arena-v1","rusticol.compiled.runtime-selectors.v1","symjit.application.complex-f64.v1"],"runtime_available":true,"runtime_unavailable_message":null,"stage_count":2,"stages":[{"blockers":[],"compiled_plane_arena":{"application_abi":"pyamplicol-compiled-plane-kernel-v2","element_layout":"split-complex-component-major","input_bindings":[{"component":0,"global_component":4,"kind":"value","parameter_index":0,"real_valued":false,"source_id":2},{"component":1,"global_component":5,"kind":"value","parameter_index":1,"real_valued":false,"source_id":2},{"component":0,"global_component":6,"kind":"value","parameter_index":2,"real_valued":false,"source_id":3},{"component":1,"global_component":7,"kind":"value","parameter_index":3,"real_valued":false,"source_id":3},{"component":0,"global_component":8,"kind":"value","parameter_index":4,"real_valued":false,"source_id":4},{"component":1,"global_component":9,"kind":"value","parameter_index":5,"real_valued":false,"source_id":4},{"component":2,"global_component":10,"kind":"value","parameter_index":6,"real_valued":false,"source_id":4},{"component":3,"global_component":11,"kind":"value","parameter_index":7,"real_valued":false,"source_id":4},{"component":0,"global_component":20,"kind":"momentum","parameter_index":8,"real_valued":true,"source_id":1},{"component":1,"global_component":21,"kind":"momentum","parameter_index":9,"real_valued":true,"source_id":1},{"component":2,"global_component":22,"kind":"momentum","parameter_index":10,"real_valued":true,"source_id":1},{"component":3,"global_component":23,"kind":"momentum","parameter_index":11,"real_valued":true,"source_id":1},{"component":0,"global_component":24,"kind":"momentum","parameter_index":12,"real_valued":true,"source_id":2},{"component":1,"global_component":25,"kind":"momentum","parameter_index":13,"real_valued":true,"source_id":2},{"component":2,"global_component":26,"kind":"momentum","parameter_index":14,"real_valued":true,"source_id":2},{"component":3,"global_component":27,"kind":"momentum","parameter_index":15,"real_valued":true,"source_id":2},{"component":0,"global_component":28,"kind":"momentum","parameter_index":16,"real_valued":true,"source_id":3},{"component":1,"global_component":29,"kind":"momentum","parameter_index":17,"real_valued":true,"source_id":3},{"component":2,"global_component":30,"kind":"momentum","parameter_index":18,"real_valued":true,"source_id":3},{"component":3,"global_component":31,"kind":"momentum","parameter_index":19,"real_valued":true,"source_id":3},{"component":0,"global_component":42,"kind":"model_parameter","parameter_index":20,"real_valued":true,"source_id":10},{"component":0,"global_component":43,"kind":"model_parameter","parameter_index":21,"real_valued":true,"source_id":11}],"input_output_aliasing":"forbidden","kind":"compiled-plane-arena-stage","leaves":[{"application_path":"evaluators/generic_stage_1_subset_2_chunk_0_8d246ffe1be84520b5f98a14ab9e364a.plane.symjit","direct_codegen_optimization_level":2,"input_indices":[0,1,4,5,6,7,21],"input_len":7,"optimization_level":2,"output_len":2,"output_start":0,"output_stop":2,"source_application_abi":"pyamplicol-symjit-plane-application-v2"},{"application_path":"evaluators/generic_stage_1_subset_2_chunk_1_8e8022a30237453eb7f2c7dd6c975ba3.plane.symjit","direct_codegen_optimization_level":2,"input_indices":[2,3,4,5,6,7,20],"input_len":7,"optimization_level":2,"output_len":2,"output_start":2,"output_stop":4,"source_application_abi":"pyamplicol-symjit-plane-application-v2"}],"output_bindings":[{"arena":"current","component":12,"output_index":0},{"arena":"current","component":13,"output_index":1},{"arena":"current","component":14,"output_index":2},{"arena":"current","component":15,"output_index":3}],"output_factor":"identity","output_operation":"overwrite","output_output_aliasing":"forbidden","schema_version":1,"source_application_abi":"pyamplicol-symjit-plane-application-v2"},"evaluator":{"chunk_input_indices":[[0,1,4,5,6,7,21],[2,3,4,5,6,7,20]],"chunks":[{"application_abi":"symjit-application-storage-v3","application_path":"evaluators/generic_stage_1_subset_2_chunk_0_8d246ffe1be84520b5f98a14ab9e364a.symjit","batch_layout":"row-major","compiler_type":"native","element_layout":"complex-f64","endianness":"little","evaluator_state_path":null,"evaluator_state_runtime_capability":null,"input_len":7,"kind":"symjit-application-evaluator","optimization_level":2,"output_len":2,"plane_application":{"application_abi":"pyamplicol-symjit-plane-application-v2","application_path":"evaluators/generic_stage_1_subset_2_chunk_0_8d246ffe1be84520b5f98a14ab9e364a.plane.symjit","compiler_type":"native","complex":true,"compression":true,"descriptor_order":"inputs-re-im-then-outputs-re-im","direct_arena":true,"element_layout":"split-complex-plane-major","fast_complex":false,"fast_math":true,"input_complex_count":7,"input_plane_count":14,"optimization_level":2,"output_complex_count":2,"output_plane_count":4,"simd":true,"source_digest":"5da722fc102ba7065505a8107012cda41e3946437b0916057ab4d9768a6a6d88","storage_abi":"symjit-application-storage-v3","target":{"cpu_features":["aes","bf16","bti","crc","dit","dotprod","dpb","dpb2","fcma","fhm","flagm","fp","fp16","frintts","i8mm","jsconv","lse","lse2","neon","paca","pacg","rcpc","rcpc2","rdm","sb","sha2","sha3","ssbs"],"endianness":"little","triple":"aarch64-apple-darwin","word_bits":64},"threading":false,"translation_mode":"symbolica-structured-instructions"},"required_defuns":[],"runtime_capability":"symjit.application.complex-f64.v1","translation_mode":"indirect","word_bits":64},{"application_abi":"symjit-application-storage-v3","application_path":"evaluators/generic_stage_1_subset_2_chunk_1_8e8022a30237453eb7f2c7dd6c975ba3.symjit","batch_layout":"row-major","compiler_type":"native","element_layout":"complex-f64","endianness":"little","evaluator_state_path":null,"evaluator_state_runtime_capability":null,"input_len":7,"kind":"symjit-application-evaluator","optimization_level":2,"output_len":2,"plane_application":{"application_abi":"pyamplicol-symjit-plane-application-v2","application_path":"evaluators/generic_stage_1_subset_2_chunk_1_8e8022a30237453eb7f2c7dd6c975ba3.plane.symjit","compiler_type":"native","complex":true,"compression":true,"descriptor_order":"inputs-re-im-then-outputs-re-im","direct_arena":true,"element_layout":"split-complex-plane-major","fast_complex":false,"fast_math":true,"input_complex_count":7,"input_plane_count":14,"optimization_level":2,"output_complex_count":2,"output_plane_count":4,"simd":true,"source_digest":"ba9446d38ebfa0b7b6afa2f706b096589e98f1b3a4f539b5741d59006d305928","storage_abi":"symjit-application-storage-v3","target":{"cpu_features":["aes","bf16","bti","crc","dit","dotprod","dpb","dpb2","fcma","fhm","flagm","fp","fp16","frintts","i8mm","jsconv","lse","lse2","neon","paca","pacg","rcpc","rcpc2","rdm","sb","sha2","sha3","ssbs"],"endianness":"little","triple":"aarch64-apple-darwin","word_bits":64},"threading":false,"translation_mode":"symbolica-structured-instructions"},"required_defuns":[],"runtime_capability":"symjit.application.complex-f64.v1","translation_mode":"indirect","word_bits":64}],"input_len":22,"kind":"chunked-symbolica-evaluator","required_runtime_capabilities":["symjit.application.complex-f64.v1"]},"evaluator_label":"generic_stage_1_subset_2","expression_ready":true,"input_components":[{"component":0,"global_component":4,"kind":"value","parameter_index":0,"real_valued":false,"source_id":2},{"component":1,"global_component":5,"kind":"value","parameter_index":1,"real_valued":false,"source_id":2},{"component":0,"global_component":6,"kind":"value","parameter_index":2,"real_valued":false,"source_id":3},{"component":1,"global_component":7,"kind":"value","parameter_index":3,"real_valued":false,"source_id":3},{"component":0,"global_component":8,"kind":"value","parameter_index":4,"real_valued":false,"source_id":4},{"component":1,"global_component":9,"kind":"value","parameter_index":5,"real_valued":false,"source_id":4},{"component":2,"global_component":10,"kind":"value","parameter_index":6,"real_valued":false,"source_id":4},{"component":3,"global_component":11,"kind":"value","parameter_index":7,"real_valued":false,"source_id":4},{"component":0,"global_component":20,"kind":"momentum","parameter_index":8,"real_valued":true,"source_id":1},{"component":1,"global_component":21,"kind":"momentum","parameter_index":9,"real_valued":true,"source_id":1},{"component":2,"global_component":22,"kind":"momentum","parameter_index":10,"real_valued":true,"source_id":1},{"component":3,"global_component":23,"kind":"momentum","parameter_index":11,"real_valued":true,"source_id":1},{"component":0,"global_component":24,"kind":"momentum","parameter_index":12,"real_valued":true,"source_id":2},{"component":1,"global_component":25,"kind":"momentum","parameter_index":13,"real_valued":true,"source_id":2},{"component":2,"global_component":26,"kind":"momentum","parameter_index":14,"real_valued":true,"source_id":2},{"component":3,"global_component":27,"kind":"momentum","parameter_index":15,"real_valued":true,"source_id":2},{"component":0,"global_component":28,"kind":"momentum","parameter_index":16,"real_valued":true,"source_id":3},{"component":1,"global_component":29,"kind":"momentum","parameter_index":17,"real_valued":true,"source_id":3},{"component":2,"global_component":30,"kind":"momentum","parameter_index":18,"real_valued":true,"source_id":3},{"component":3,"global_component":31,"kind":"momentum","parameter_index":19,"real_valued":true,"source_id":3},{"component":0,"global_component":42,"kind":"model_parameter","parameter_index":20,"real_valued":true,"source_id":10},{"component":0,"global_component":43,"kind":"model_parameter","parameter_index":21,"real_valued":true,"source_id":11}],"input_value_slot_ids":[2,3,4],"interaction_ids":[0,1],"model_parameter_count":2,"momentum_parameter_count":12,"output_length":4,"output_slots":[{"color_selector_domain_ids":[0],"component_start":12,"component_stop":14,"current_id":5,"output_start":0,"output_stop":2,"value_slot_id":5,"variant":"unpropagated"},{"color_selector_domain_ids":[0],"component_start":14,"component_stop":16,"current_id":6,"output_start":2,"output_stop":4,"value_slot_id":6,"variant":"unpropagated"}],"output_value_slot_ids":[5,6],"parameter_count":22,"parameter_layout":"stage-local-value-momentum","real_valued_inputs":[8,9,10,11,12,13,14,15,16,17,18,19,20,21],"stage_index":1,"stage_kind":"current-combine","subset_size":2,"value_parameter_count":8}],"value_parameter_count":0}},"dag_summary":{"amplitude_root_count":2,"current_count":7,"interaction_count":2,"interaction_evaluation_count":2,"source_count":5,"truncated":false},"external_pdg_order":[1,-1,23],"helicity_selector_executions":[{"execution":{"color_accuracy":"lc","compiled":{"kind":"generic-dag-stage-blueprint","model_parameter_evaluator":null,"momentum_slot_ids":[1,2,3],"runtime_available":true,"runtime_unavailable_message":null,"stage_evaluators":{"amplitude_stage":{"blockers":[],"compiled_plane_arena":{"application_abi":"pyamplicol-compiled-plane-kernel-v2","element_layout":"split-complex-component-major","input_bindings":[{"component":0,"global_component":0,"kind":"value","parameter_index":0,"real_valued":false,"source_id":0},{"component":1,"global_component":1,"kind":"value","parameter_index":1,"real_valued":false,"source_id":0},{"component":0,"global_component":2,"kind":"value","parameter_index":2,"real_valued":false,"source_id":1},{"component":1,"global_component":3,"kind":"value","parameter_index":3,"real_valued":false,"source_id":1},{"component":0,"global_component":12,"kind":"value","parameter_index":4,"real_valued":false,"source_id":5},{"component":1,"global_component":13,"kind":"value","parameter_index":5,"real_valued":false,"source_id":5},{"component":0,"global_component":14,"kind":"value","parameter_index":6,"real_valued":false,"source_id":6},{"component":1,"global_component":15,"kind":"value","parameter_index":7,"real_valued":false,"source_id":6}],"input_output_aliasing":"forbidden","kind":"compiled-plane-arena-stage","leaves":[{"application_path":"helicity-selector-union/class-0/evaluators/generic_amplitude_stage_7c8f49c219a64c318bb490009aa01673.plane.symjit","direct_codegen_optimization_level":2,"input_indices":[0,1,2,3,4,5,6,7],"input_len":8,"optimization_level":2,"output_len":2,"output_start":0,"output_stop":2,"source_application_abi":"pyamplicol-symjit-plane-application-v2"}],"output_bindings":[{"arena":"amplitude","component":0,"output_index":0},{"arena":"amplitude","component":1,"output_index":1}],"output_factor":"identity","output_operation":"overwrite","output_output_aliasing":"forbidden","schema_version":1,"source_application_abi":"pyamplicol-symjit-plane-application-v2"},"evaluator":{"application_abi":"symjit-application-storage-v3","application_path":"helicity-selector-union/class-0/evaluators/generic_amplitude_stage_7c8f49c219a64c318bb490009aa01673.symjit","batch_layout":"row-major","compiler_type":"native","element_layout":"complex-f64","endianness":"little","evaluator_state_path":null,"evaluator_state_runtime_capability":null,"input_len":8,"kind":"symjit-application-evaluator","optimization_level":2,"output_len":2,"plane_application":{"application_abi":"pyamplicol-symjit-plane-application-v2","application_path":"helicity-selector-union/class-0/evaluators/generic_amplitude_stage_7c8f49c219a64c318bb490009aa01673.plane.symjit","compiler_type":"native","complex":true,"compression":true,"descriptor_order":"inputs-re-im-then-outputs-re-im","direct_arena":true,"element_layout":"split-complex-plane-major","fast_complex":false,"fast_math":true,"input_complex_count":8,"input_plane_count":16,"optimization_level":2,"output_complex_count":2,"output_plane_count":4,"simd":true,"source_digest":"993dd48ecd84d24358fd411b0f35d3e5a178608a3dd91199c1c0b8b260cfe3b2","storage_abi":"symjit-application-storage-v3","target":{"cpu_features":["aes","bf16","bti","crc","dit","dotprod","dpb","dpb2","fcma","fhm","flagm","fp","fp16","frintts","i8mm","jsconv","lse","lse2","neon","paca","pacg","rcpc","rcpc2","rdm","sb","sha2","sha3","ssbs"],"endianness":"little","triple":"aarch64-apple-darwin","word_bits":64},"threading":false,"translation_mode":"symbolica-structured-instructions"},"required_defuns":[],"runtime_capability":"symjit.application.complex-f64.v1","translation_mode":"indirect","word_bits":64},"evaluator_label":"generic_amplitude_stage","expression_ready":true,"input_components":[{"component":0,"global_component":0,"kind":"value","parameter_index":0,"real_valued":false,"source_id":0},{"component":1,"global_component":1,"kind":"value","parameter_index":1,"real_valued":false,"source_id":0},{"component":0,"global_component":2,"kind":"value","parameter_index":2,"real_valued":false,"source_id":1},{"component":1,"global_component":3,"kind":"value","parameter_index":3,"real_valued":false,"source_id":1},{"component":0,"global_component":12,"kind":"value","parameter_index":4,"real_valued":false,"source_id":5},{"component":1,"global_component":13,"kind":"value","parameter_index":5,"real_valued":false,"source_id":5},{"component":0,"global_component":14,"kind":"value","parameter_index":6,"real_valued":false,"source_id":6},{"component":1,"global_component":15,"kind":"value","parameter_index":7,"real_valued":false,"source_id":6}],"input_value_slot_ids":[0,1,5,6],"interaction_ids":[],"model_parameter_count":0,"momentum_parameter_count":0,"output_length":2,"output_slots":[{"component_start":0,"component_stop":1,"current_id":-1,"output_start":0,"output_stop":1,"value_slot_id":-1,"variant":"amplitude-root"},{"component_start":1,"component_stop":2,"current_id":-1,"output_start":1,"output_stop":2,"value_slot_id":-1,"variant":"amplitude-root"}],"output_value_slot_ids":[],"parameter_count":8,"parameter_layout":"stage-local-value-momentum","real_valued_inputs":[],"stage_index":0,"stage_kind":"amplitude-roots","subset_size":null,"value_parameter_count":8},"kind":"generic-dag-stage-evaluator-artifacts","model_parameter_count":0,"momentum_parameter_count":0,"parameter_count":0,"parameter_layout":"stage-local-value-momentum","real_valued_inputs":[],"required_runtime_capabilities":["compiled-plane-arena-v1","symjit.application.complex-f64.v1"],"runtime_available":true,"runtime_unavailable_message":null,"stage_count":2,"stages":[{"blockers":[],"compiled_plane_arena":{"application_abi":"pyamplicol-compiled-plane-kernel-v2","element_layout":"split-complex-component-major","input_bindings":[{"component":0,"global_component":6,"kind":"value","parameter_index":0,"real_valued":false,"source_id":3},{"component":1,"global_component":7,"kind":"value","parameter_index":1,"real_valued":false,"source_id":3},{"component":0,"global_component":8,"kind":"value","parameter_index":2,"real_valued":false,"source_id":4},{"component":1,"global_component":9,"kind":"value","parameter_index":3,"real_valued":false,"source_id":4},{"component":2,"global_component":10,"kind":"value","parameter_index":4,"real_valued":false,"source_id":4},{"component":3,"global_component":11,"kind":"value","parameter_index":5,"real_valued":false,"source_id":4},{"component":0,"global_component":20,"kind":"momentum","parameter_index":6,"real_valued":true,"source_id":1},{"component":1,"global_component":21,"kind":"momentum","parameter_index":7,"real_valued":true,"source_id":1},{"component":2,"global_component":22,"kind":"momentum","parameter_index":8,"real_valued":true,"source_id":1},{"component":3,"global_component":23,"kind":"momentum","parameter_index":9,"real_valued":true,"source_id":1},{"component":0,"global_component":24,"kind":"momentum","parameter_index":10,"real_valued":true,"source_id":2},{"component":1,"global_component":25,"kind":"momentum","parameter_index":11,"real_valued":true,"source_id":2},{"component":2,"global_component":26,"kind":"momentum","parameter_index":12,"real_valued":true,"source_id":2},{"component":3,"global_component":27,"kind":"momentum","parameter_index":13,"real_valued":true,"source_id":2},{"component":0,"global_component":28,"kind":"momentum","parameter_index":14,"real_valued":true,"source_id":3},{"component":1,"global_component":29,"kind":"momentum","parameter_index":15,"real_valued":true,"source_id":3},{"component":2,"global_component":30,"kind":"momentum","parameter_index":16,"real_valued":true,"source_id":3},{"component":3,"global_component":31,"kind":"momentum","parameter_index":17,"real_valued":true,"source_id":3},{"component":0,"global_component":42,"kind":"model_parameter","parameter_index":18,"real_valued":true,"source_id":10},{"component":0,"global_component":43,"kind":"model_parameter","parameter_index":19,"real_valued":true,"source_id":11}],"input_output_aliasing":"forbidden","kind":"compiled-plane-arena-stage","leaves":[{"application_path":"helicity-selector-union/class-0/evaluators/generic_stage_1_subset_2_df25a79e1b6e4b8fa69548b41073407f.plane.symjit","direct_codegen_optimization_level":2,"input_indices":[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19],"input_len":20,"optimization_level":2,"output_len":2,"output_start":0,"output_stop":2,"source_application_abi":"pyamplicol-symjit-plane-application-v2"}],"output_bindings":[{"arena":"current","component":14,"output_index":0},{"arena":"current","component":15,"output_index":1}],"output_factor":"identity","output_operation":"overwrite","output_output_aliasing":"forbidden","schema_version":1,"source_application_abi":"pyamplicol-symjit-plane-application-v2"},"evaluator":{"application_abi":"symjit-application-storage-v3","application_path":"helicity-selector-union/class-0/evaluators/generic_stage_1_subset_2_df25a79e1b6e4b8fa69548b41073407f.symjit","batch_layout":"row-major","compiler_type":"native","element_layout":"complex-f64","endianness":"little","evaluator_state_path":null,"evaluator_state_runtime_capability":null,"input_len":20,"kind":"symjit-application-evaluator","optimization_level":2,"output_len":2,"plane_application":{"application_abi":"pyamplicol-symjit-plane-application-v2","application_path":"helicity-selector-union/class-0/evaluators/generic_stage_1_subset_2_df25a79e1b6e4b8fa69548b41073407f.plane.symjit","compiler_type":"native","complex":true,"compression":true,"descriptor_order":"inputs-re-im-then-outputs-re-im","direct_arena":true,"element_layout":"split-complex-plane-major","fast_complex":false,"fast_math":true,"input_complex_count":20,"input_plane_count":40,"optimization_level":2,"output_complex_count":2,"output_plane_count":4,"simd":true,"source_digest":"faae1df95ce1f1e47f46e9d3f40c7a2ed45e355dbf85badaee6e990c8e3eeced","storage_abi":"symjit-application-storage-v3","target":{"cpu_features":["aes","bf16","bti","crc","dit","dotprod","dpb","dpb2","fcma","fhm","flagm","fp","fp16","frintts","i8mm","jsconv","lse","lse2","neon","paca","pacg","rcpc","rcpc2","rdm","sb","sha2","sha3","ssbs"],"endianness":"little","triple":"aarch64-apple-darwin","word_bits":64},"threading":false,"translation_mode":"symbolica-structured-instructions"},"required_defuns":[],"runtime_capability":"symjit.application.complex-f64.v1","translation_mode":"indirect","word_bits":64},"evaluator_label":"generic_stage_1_subset_2","expression_ready":true,"input_components":[{"component":0,"global_component":6,"kind":"value","parameter_index":0,"real_valued":false,"source_id":3},{"component":1,"global_component":7,"kind":"value","parameter_index":1,"real_valued":false,"source_id":3},{"component":0,"global_component":8,"kind":"value","parameter_index":2,"real_valued":false,"source_id":4},{"component":1,"global_component":9,"kind":"value","parameter_index":3,"real_valued":false,"source_id":4},{"component":2,"global_component":10,"kind":"value","parameter_index":4,"real_valued":false,"source_id":4},{"component":3,"global_component":11,"kind":"value","parameter_index":5,"real_valued":false,"source_id":4},{"component":0,"global_component":20,"kind":"momentum","parameter_index":6,"real_valued":true,"source_id":1},{"component":1,"global_component":21,"kind":"momentum","parameter_index":7,"real_valued":true,"source_id":1},{"component":2,"global_component":22,"kind":"momentum","parameter_index":8,"real_valued":true,"source_id":1},{"component":3,"global_component":23,"kind":"momentum","parameter_index":9,"real_valued":true,"source_id":1},{"component":0,"global_component":24,"kind":"momentum","parameter_index":10,"real_valued":true,"source_id":2},{"component":1,"global_component":25,"kind":"momentum","parameter_index":11,"real_valued":true,"source_id":2},{"component":2,"global_component":26,"kind":"momentum","parameter_index":12,"real_valued":true,"source_id":2},{"component":3,"global_component":27,"kind":"momentum","parameter_index":13,"real_valued":true,"source_id":2},{"component":0,"global_component":28,"kind":"momentum","parameter_index":14,"real_valued":true,"source_id":3},{"component":1,"global_component":29,"kind":"momentum","parameter_index":15,"real_valued":true,"source_id":3},{"component":2,"global_component":30,"kind":"momentum","parameter_index":16,"real_valued":true,"source_id":3},{"component":3,"global_component":31,"kind":"momentum","parameter_index":17,"real_valued":true,"source_id":3},{"component":0,"global_component":42,"kind":"model_parameter","parameter_index":18,"real_valued":true,"source_id":10},{"component":0,"global_component":43,"kind":"model_parameter","parameter_index":19,"real_valued":true,"source_id":11}],"input_value_slot_ids":[3,4],"interaction_ids":[1],"model_parameter_count":2,"momentum_parameter_count":12,"output_length":2,"output_slots":[{"component_start":14,"component_stop":16,"current_id":6,"output_start":0,"output_stop":2,"value_slot_id":6,"variant":"unpropagated"}],"output_value_slot_ids":[6],"parameter_count":20,"parameter_layout":"stage-local-value-momentum","real_valued_inputs":[6,7,8,9,10,11,12,13,14,15,16,17,18,19],"stage_index":1,"stage_kind":"current-combine","subset_size":2,"value_parameter_count":6}],"value_parameter_count":0}},"dag_summary":{"amplitude_root_count":2,"current_count":7,"interaction_count":1,"interaction_evaluation_count":1,"source_count":5,"truncated":false},"external_pdg_order":[1,-1,23],"key":"d_dbar_to_z","kind":"pyamplicol-runtime-execution","materialization_census":{"abi":"pyamplicol-fully-resident-materialization-census-v1","basis":"immutable-fully-resident-compiled-dag","final":{"amplitude_root_count":2,"current_count":7,"interaction_count":1,"interaction_evaluation_count":1,"source_count":5},"final_equals_peak":true,"peak":{"amplitude_root_count":2,"current_count":7,"interaction_count":1,"interaction_evaluation_count":1,"source_count":5}},"physics_reduction":{"groups":[{"id":"reduction:0","physical_color_ids":["flow:2,1"],"physical_helicity_ids":["h:+1,-1,-1","h:+1,-1,+0","h:+1,-1,+1"],"representative_color_id":"flow:2,1","representative_helicity_id":"h:+1,-1,-1"},{"id":"reduction:1","physical_color_ids":["flow:2,1"],"physical_helicity_ids":["h:-1,+1,-1","h:-1,+1,+0","h:-1,+1,+1"],"representative_color_id":"flow:2,1","representative_helicity_id":"h:-1,+1,-1"}],"kind":"lc-diagonal"},"process":"d d~ > z","required_runtime_capabilities":["compiled-plane-arena-v1","symjit.application.complex-f64.v1"],"runtime_schema":{"amplitude_stage":{"color_contraction":null,"color_topology_replay":null,"output_count":2,"roots":[{"all_sector_weight":1.0,"coherent_group_id":0,"color_sector_id":0,"color_weight":[1.0,0.0],"contraction":"weyl","contraction_ir":{"chirality_relation":"opposite","coefficients":[[1.0,0.0],[1.0,0.0]],"left_basis":"weyl-chiral","metric_signature":null,"name":"weyl","right_basis":"weyl-chiral"},"coupling":[1.0,0.0],"helicity_weight":1.0,"kind":"direct-contraction","left_current_id":5,"left_slot":{"component_start":12,"component_stop":14,"current_id":5,"dimension":2},"left_value_slot":{"component_start":12,"component_stop":14,"current_id":5,"dimension":2,"value_slot_id":5,"variant":"unpropagated"},"output_index":0,"right_current_id":1,"right_slot":{"component_start":2,"component_stop":4,"current_id":1,"dimension":2},"right_value_slot":{"component_start":2,"component_stop":4,"current_id":1,"dimension":2,"value_slot_id":1,"variant":"source"},"root_id":0,"vertex_kind":null,"vertex_particles":null},{"all_sector_weight":1.0,"coherent_group_id":1,"color_sector_id":0,"color_weight":[1.0,0.0],"contraction":"weyl","contraction_ir":{"chirality_relation":"opposite","coefficients":[[1.0,0.0],[1.0,0.0]],"left_basis":"weyl-chiral","metric_signature":null,"name":"weyl","right_basis":"weyl-chiral"},"coupling":[1.0,0.0],"helicity_weight":1.0,"kind":"direct-contraction","left_current_id":6,"left_slot":{"component_start":14,"component_stop":16,"current_id":6,"dimension":2},"left_value_slot":{"component_start":14,"component_stop":16,"current_id":6,"dimension":2,"value_slot_id":6,"variant":"unpropagated"},"output_index":1,"right_current_id":0,"right_slot":{"component_start":0,"component_stop":2,"current_id":0,"dimension":2},"right_value_slot":{"component_start":0,"component_stop":2,"current_id":0,"dimension":2,"value_slot_id":0,"variant":"source"},"root_id":1,"vertex_kind":null,"vertex_particles":null}],"stage_kind":"amplitude-roots"},"current_storage":{"component_count":16,"current_slots":[{"auxiliary_kind":null,"chirality":1,"color_state":{"accuracy":"lc","basis_key":[],"line_groups":[0],"sector_id":0},"component_start":0,"component_stop":2,"current_id":0,"dimension":2,"external_labels":[1],"external_mask":1,"flavour_flow":[-1],"helicity_ancestry":"0x1","is_source":true,"momentum_mask":1,"particle_id":-1,"spin_state":1},{"auxiliary_kind":null,"chirality":-1,"color_state":{"accuracy":"lc","basis_key":[],"line_groups":[0],"sector_id":0},"component_start":2,"component_stop":4,"current_id":1,"dimension":2,"external_labels":[1],"external_mask":1,"flavour_flow":[-1],"helicity_ancestry":"0x2","is_source":true,"momentum_mask":1,"particle_id":-1,"spin_state":-1},{"auxiliary_kind":null,"chirality":1,"color_state":{"accuracy":"lc","basis_key":[],"line_groups":[0],"sector_id":0},"component_start":4,"component_stop":6,"current_id":2,"dimension":2,"external_labels":[2],"external_mask":2,"flavour_flow":[1],"helicity_ancestry":"0x4","is_source":true,"momentum_mask":2,"particle_id":1,"spin_state":1},{"auxiliary_kind":null,"chirality":-1,"color_state":{"accuracy":"lc","basis_key":[],"line_groups":[0],"sector_id":0},"component_start":6,"component_stop":8,"current_id":3,"dimension":2,"external_labels":[2],"external_mask":2,"flavour_flow":[1],"helicity_ancestry":"0x8","is_source":true,"momentum_mask":2,"particle_id":1,"spin_state":-1},{"auxiliary_kind":null,"chirality":0,"color_state":{"accuracy":"lc","basis_key":[],"line_groups":[],"sector_id":0},"component_start":8,"component_stop":12,"current_id":4,"dimension":4,"external_labels":[3],"external_mask":4,"flavour_flow":[23],"helicity_ancestry":"0x10","is_source":true,"momentum_mask":4,"particle_id":23,"spin_state":-1},{"auxiliary_kind":null,"chirality":1,"color_state":{"accuracy":"lc","basis_key":[],"line_groups":[0],"sector_id":0},"component_start":12,"component_stop":14,"current_id":5,"dimension":2,"external_labels":[2,3],"external_mask":6,"flavour_flow":[1],"helicity_ancestry":"0x14","is_source":false,"momentum_mask":6,"particle_id":1,"spin_state":1},{"auxiliary_kind":null,"chirality":-1,"color_state":{"accuracy":"lc","basis_key":[],"line_groups":[0],"sector_id":0},"component_start":14,"component_stop":16,"current_id":6,"dimension":2,"external_labels":[2,3],"external_mask":6,"flavour_flow":[1],"helicity_ancestry":"0x18","is_source":false,"momentum_mask":6,"particle_id":1,"spin_state":-1}],"metadata_compacted":true,"number_type":"complex"},"external_particles":[{"index":0,"label":1,"momentum_slot":0,"outgoing_pdg":-1,"pdg":1,"role":"initial"},{"index":1,"label":2,"momentum_slot":1,"outgoing_pdg":1,"pdg":-1,"role":"initial"},{"index":2,"label":3,"momentum_slot":2,"outgoing_pdg":23,"pdg":23,"role":"final"}],"kind":"pyamplicol-runtime-execution-plan","model":{"particles":[{"mass":0.0,"mass_parameter":null,"pdg":1},{"mass":91.188,"mass_parameter":"particle.23.mass","pdg":23}]},"model_parameters":[{"default":0.0075467711139788835,"kind":"normalization","name":"normalization.alpha_ew","parameter_index":0},{"default":0.118,"kind":"normalization","name":"normalization.alpha_s_me_check","parameter_index":1},{"default":173.0,"kind":"particle_mass","name":"particle.6.mass","parameter_index":2,"pdg":6},{"default":1.4915,"kind":"particle_width","name":"particle.6.width","parameter_index":3,"pdg":6},{"default":91.188,"kind":"particle_mass","name":"particle.23.mass","parameter_index":4,"pdg":23},{"default":2.441404,"kind":"particle_width","name":"particle.23.width","parameter_index":5,"pdg":23},{"default":80.41900244575616,"kind":"particle_mass","name":"particle.24.mass","parameter_index":6,"pdg":24},{"default":2.0476,"kind":"particle_width","name":"particle.24.width","parameter_index":7,"pdg":24},{"default":125.0,"kind":"particle_mass","name":"particle.25.mass","parameter_index":8,"pdg":25},{"default":0.006382339,"kind":"particle_width","name":"particle.25.width","parameter_index":9,"pdg":25},{"default":-1.0244420275940371,"kind":"coupling_component","name":"coupling.10.1_23_1.component_0","parameter_index":10},{"default":0.17818666745287456,"kind":"coupling_component","name":"coupling.10.1_23_1.component_1","parameter_index":11}],"momentum_slots":[{"component_start":0,"component_stop":4,"external_labels":[1],"momentum_mask":1,"momentum_slot_id":0,"real_valued":true},{"component_start":4,"component_stop":8,"external_labels":[2],"momentum_mask":2,"momentum_slot_id":1,"real_valued":true},{"component_start":8,"component_stop":12,"external_labels":[3],"momentum_mask":4,"momentum_slot_id":2,"real_valued":true},{"component_start":12,"component_stop":16,"external_labels":[2,3],"momentum_mask":6,"momentum_slot_id":3,"real_valued":true}],"normalization":{"average_factor":36.0,"color_factor":3.0,"electroweak_coupling_power":1,"global_coupling_factor":0.18967104551999775,"identical_factor":1.0,"qcd_coupling_power":0},"parameter_layout":{"model_parameter_count":12,"momentum_components_real":true,"momentum_parameter_count":16,"parameter_count_if_flattened":40,"real_valued_inputs":[12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39],"source_component_parameter_count":12,"source_components_complex":true,"value_component_count":16},"process":"d d~ > z","process_key":"d_dbar_to_z","schema_version":3,"source_fill":{"source_count":5,"sources":[{"anti_particle_id":1,"applied_crossing":{"chirality_factor":-1,"helicity_factor":1,"momentum_transform":"negate-four-momentum","phase":[1.0,0.0],"spin_state_factor":-1},"chirality":1,"color_state":{"accuracy":"lc","basis_key":[],"line_groups":[0],"sector_id":0},"crossing":"negate-incoming-momentum","current_component_start":0,"current_component_stop":2,"current_id":0,"dimension":2,"helicity_ancestry":"0x1","input_momentum_slot":0,"leg_label":1,"outgoing_pdg":-1,"particle_id":-1,"physical_pdg":1,"side":"initial","source_basis":"weyl-chiral","source_helicity":-1,"source_id":0,"source_ir":{"basis":"weyl-chiral","component_dimension":2,"crossing":{"chirality_factor":-1,"helicity_factor":1,"momentum_transform":"negate-four-momentum","phase":[1.0,0.0],"spin_state_factor":-1},"identity":{"anti_canonical_id":"model:built-in-sm-leading-color:state:d","anti_display_name":"d","anti_pdg_label":1,"canonical_id":"model:built-in-sm-leading-color:state:d~","display_name":"d~","orientation":"antiparticle","pdg_label":-1,"self_conjugate":false,"species_id":"model:built-in-sm-leading-color:species:d"},"mass_parameter":null,"states":[{"chirality":-1,"helicity":-1,"spin_state":-1},{"chirality":1,"helicity":1,"spin_state":1}],"statistics":"fermion","wavefunction_family":"fermion","width_parameter":null},"source_kind":"external-wavefunction","source_orientation":"antiparticle","source_parameter_start":0,"source_parameter_stop":2,"spin_state":1,"value_slot":{"component_start":0,"component_stop":2,"current_id":0,"dimension":2,"value_slot_id":0,"variant":"source"},"wavefunction_kind":"fermion"},{"anti_particle_id":1,"applied_crossing":{"chirality_factor":-1,"helicity_factor":1,"momentum_transform":"negate-four-momentum","phase":[1.0,0.0],"spin_state_factor":-1},"chirality":-1,"color_state":{"accuracy":"lc","basis_key":[],"line_groups":[0],"sector_id":0},"crossing":"negate-incoming-momentum","current_component_start":2,"current_component_stop":4,"current_id":1,"dimension":2,"helicity_ancestry":"0x2","input_momentum_slot":0,"leg_label":1,"outgoing_pdg":-1,"particle_id":-1,"physical_pdg":1,"side":"initial","source_basis":"weyl-chiral","source_helicity":1,"source_id":1,"source_ir":{"basis":"weyl-chiral","component_dimension":2,"crossing":{"chirality_factor":-1,"helicity_factor":1,"momentum_transform":"negate-four-momentum","phase":[1.0,0.0],"spin_state_factor":-1},"identity":{"anti_canonical_id":"model:built-in-sm-leading-color:state:d","anti_display_name":"d","anti_pdg_label":1,"canonical_id":"model:built-in-sm-leading-color:state:d~","display_name":"d~","orientation":"antiparticle","pdg_label":-1,"self_conjugate":false,"species_id":"model:built-in-sm-leading-color:species:d"},"mass_parameter":null,"states":[{"chirality":-1,"helicity":-1,"spin_state":-1},{"chirality":1,"helicity":1,"spin_state":1}],"statistics":"fermion","wavefunction_family":"fermion","width_parameter":null},"source_kind":"external-wavefunction","source_orientation":"antiparticle","source_parameter_start":2,"source_parameter_stop":4,"spin_state":-1,"value_slot":{"component_start":2,"component_stop":4,"current_id":1,"dimension":2,"value_slot_id":1,"variant":"source"},"wavefunction_kind":"fermion"},{"anti_particle_id":-1,"applied_crossing":{"chirality_factor":-1,"helicity_factor":1,"momentum_transform":"negate-four-momentum","phase":[1.0,0.0],"spin_state_factor":-1},"chirality":1,"color_state":{"accuracy":"lc","basis_key":[],"line_groups":[0],"sector_id":0},"crossing":"negate-incoming-momentum","current_component_start":4,"current_component_stop":6,"current_id":2,"dimension":2,"helicity_ancestry":"0x4","input_momentum_slot":1,"leg_label":2,"outgoing_pdg":1,"particle_id":1,"physical_pdg":-1,"side":"initial","source_basis":"weyl-chiral","source_helicity":-1,"source_id":2,"source_ir":{"basis":"weyl-chiral","component_dimension":2,"crossing":{"chirality_factor":-1,"helicity_factor":1,"momentum_transform":"negate-four-momentum","phase":[1.0,0.0],"spin_state_factor":-1},"identity":{"anti_canonical_id":"model:built-in-sm-leading-color:state:d~","anti_display_name":"d~","anti_pdg_label":-1,"canonical_id":"model:built-in-sm-leading-color:state:d","display_name":"d","orientation":"particle","pdg_label":1,"self_conjugate":false,"species_id":"model:built-in-sm-leading-color:species:d"},"mass_parameter":null,"states":[{"chirality":-1,"helicity":-1,"spin_state":-1},{"chirality":1,"helicity":1,"spin_state":1}],"statistics":"fermion","wavefunction_family":"fermion","width_parameter":null},"source_kind":"external-wavefunction","source_orientation":"particle","source_parameter_start":4,"source_parameter_stop":6,"spin_state":1,"value_slot":{"component_start":4,"component_stop":6,"current_id":2,"dimension":2,"value_slot_id":2,"variant":"source"},"wavefunction_kind":"fermion"},{"anti_particle_id":-1,"applied_crossing":{"chirality_factor":-1,"helicity_factor":1,"momentum_transform":"negate-four-momentum","phase":[1.0,0.0],"spin_state_factor":-1},"chirality":-1,"color_state":{"accuracy":"lc","basis_key":[],"line_groups":[0],"sector_id":0},"crossing":"negate-incoming-momentum","current_component_start":6,"current_component_stop":8,"current_id":3,"dimension":2,"helicity_ancestry":"0x8","input_momentum_slot":1,"leg_label":2,"outgoing_pdg":1,"particle_id":1,"physical_pdg":-1,"side":"initial","source_basis":"weyl-chiral","source_helicity":1,"source_id":3,"source_ir":{"basis":"weyl-chiral","component_dimension":2,"crossing":{"chirality_factor":-1,"helicity_factor":1,"momentum_transform":"negate-four-momentum","phase":[1.0,0.0],"spin_state_factor":-1},"identity":{"anti_canonical_id":"model:built-in-sm-leading-color:state:d~","anti_display_name":"d~","anti_pdg_label":-1,"canonical_id":"model:built-in-sm-leading-color:state:d","display_name":"d","orientation":"particle","pdg_label":1,"self_conjugate":false,"species_id":"model:built-in-sm-leading-color:species:d"},"mass_parameter":null,"states":[{"chirality":-1,"helicity":-1,"spin_state":-1},{"chirality":1,"helicity":1,"spin_state":1}],"statistics":"fermion","wavefunction_family":"fermion","width_parameter":null},"source_kind":"external-wavefunction","source_orientation":"particle","source_parameter_start":6,"source_parameter_stop":8,"spin_state":-1,"value_slot":{"component_start":6,"component_stop":8,"current_id":3,"dimension":2,"value_slot_id":3,"variant":"source"},"wavefunction_kind":"fermion"},{"anti_particle_id":23,"applied_crossing":{"chirality_factor":1,"helicity_factor":1,"momentum_transform":"identity","phase":[1.0,0.0],"spin_state_factor":1},"chirality":0,"color_state":{"accuracy":"lc","basis_key":[],"line_groups":[],"sector_id":0},"crossing":"identity","current_component_start":8,"current_component_stop":12,"current_id":4,"dimension":4,"helicity_ancestry":"0x10","input_momentum_slot":2,"leg_label":3,"outgoing_pdg":23,"particle_id":23,"physical_pdg":23,"side":"final","source_basis":"lorentz-vector","source_helicity":-1,"source_id":4,"source_ir":{"basis":"lorentz-vector","component_dimension":4,"crossing":{"chirality_factor":1,"helicity_factor":-1,"momentum_transform":"negate-four-momentum","phase":[1.0,0.0],"spin_state_factor":-1},"identity":{"anti_canonical_id":"model:built-in-sm-leading-color:state:z","anti_display_name":"z","anti_pdg_label":23,"canonical_id":"model:built-in-sm-leading-color:state:z","display_name":"z","orientation":"self-conjugate","pdg_label":23,"self_conjugate":true,"species_id":"model:built-in-sm-leading-color:species:z"},"mass_parameter":null,"states":[{"chirality":0,"helicity":-1,"spin_state":-1},{"chirality":0,"helicity":0,"spin_state":0},{"chirality":0,"helicity":1,"spin_state":1}],"statistics":"boson","wavefunction_family":"vector","width_parameter":null},"source_kind":"external-wavefunction","source_orientation":"self-conjugate","source_parameter_start":8,"source_parameter_stop":12,"spin_state":-1,"value_slot":{"component_start":8,"component_stop":12,"current_id":4,"dimension":4,"value_slot_id":4,"variant":"source"},"wavefunction_kind":"vector"}]},"stages":[{"input_current_ids":[3,4],"input_value_slot_ids":[3,4],"interaction_count":1,"interaction_ids":[1],"interactions":[],"interactions_compacted":true,"output_current_ids":[6],"output_value_slot_ids":[6],"stage_index":1,"stage_kind":"current-combine","subset_size":2}],"value_storage":{"component_count":16,"metadata_compacted":true,"number_type":"complex","value_slots":[{"applies_propagator":false,"chirality":1,"component_start":0,"component_stop":2,"current_component_start":0,"current_component_stop":2,"current_id":0,"dimension":2,"external_labels":[1],"external_mask":1,"is_source":true,"momentum_mask":1,"particle_id":-1,"propagator":{"applies_propagator":true,"auxiliary_policy":null,"backend":"spenso","basis":"weyl-chiral","chirality":1,"custom_source":null,"denominator":"momentum_squared","description":"massless Weyl fermion propagator","full_tensor_network_ready":true,"gauge":null,"goldstone_policy":"not-applicable","identity":{"anti_canonical_id":"model:built-in-sm-leading-color:state:d","anti_display_name":"d","anti_pdg_label":1,"canonical_id":"model:built-in-sm-leading-color:state:d~","display_name":"d~","orientation":"antiparticle","pdg_label":-1,"self_conjugate":false,"species_id":"model:built-in-sm-leading-color:species:d"},"kernel":"weyl_fermion","kind":"weyl-fermion","mass_class":"massless","mass_parameter":null,"numerator":"i*weyl_slash(momentum)","particle_id":-1,"width_parameter":null},"value_slot_id":0,"variant":"source"},{"applies_propagator":false,"chirality":-1,"component_start":2,"component_stop":4,"current_component_start":2,"current_component_stop":4,"current_id":1,"dimension":2,"external_labels":[1],"external_mask":1,"is_source":true,"momentum_mask":1,"particle_id":-1,"propagator":{"applies_propagator":true,"auxiliary_policy":null,"backend":"spenso","basis":"weyl-chiral","chirality":-1,"custom_source":null,"denominator":"momentum_squared","description":"massless Weyl fermion propagator","full_tensor_network_ready":true,"gauge":null,"goldstone_policy":"not-applicable","identity":{"anti_canonical_id":"model:built-in-sm-leading-color:state:d","anti_display_name":"d","anti_pdg_label":1,"canonical_id":"model:built-in-sm-leading-color:state:d~","display_name":"d~","orientation":"antiparticle","pdg_label":-1,"self_conjugate":false,"species_id":"model:built-in-sm-leading-color:species:d"},"kernel":"weyl_fermion","kind":"weyl-fermion","mass_class":"massless","mass_parameter":null,"numerator":"i*weyl_slash(momentum)","particle_id":-1,"width_parameter":null},"value_slot_id":1,"variant":"source"},{"applies_propagator":false,"chirality":1,"component_start":4,"component_stop":6,"current_component_start":4,"current_component_stop":6,"current_id":2,"dimension":2,"external_labels":[2],"external_mask":2,"is_source":true,"momentum_mask":2,"particle_id":1,"propagator":{"applies_propagator":true,"auxiliary_policy":null,"backend":"spenso","basis":"weyl-chiral","chirality":1,"custom_source":null,"denominator":"momentum_squared","description":"massless Weyl fermion propagator","full_tensor_network_ready":true,"gauge":null,"goldstone_policy":"not-applicable","identity":{"anti_canonical_id":"model:built-in-sm-leading-color:state:d~","anti_display_name":"d~","anti_pdg_label":-1,"canonical_id":"model:built-in-sm-leading-color:state:d","display_name":"d","orientation":"particle","pdg_label":1,"self_conjugate":false,"species_id":"model:built-in-sm-leading-color:species:d"},"kernel":"weyl_fermion","kind":"weyl-fermion","mass_class":"massless","mass_parameter":null,"numerator":"i*weyl_slash(momentum)","particle_id":1,"width_parameter":null},"value_slot_id":2,"variant":"source"},{"applies_propagator":false,"chirality":-1,"component_start":6,"component_stop":8,"current_component_start":6,"current_component_stop":8,"current_id":3,"dimension":2,"external_labels":[2],"external_mask":2,"is_source":true,"momentum_mask":2,"particle_id":1,"propagator":{"applies_propagator":true,"auxiliary_policy":null,"backend":"spenso","basis":"weyl-chiral","chirality":-1,"custom_source":null,"denominator":"momentum_squared","description":"massless Weyl fermion propagator","full_tensor_network_ready":true,"gauge":null,"goldstone_policy":"not-applicable","identity":{"anti_canonical_id":"model:built-in-sm-leading-color:state:d~","anti_display_name":"d~","anti_pdg_label":-1,"canonical_id":"model:built-in-sm-leading-color:state:d","display_name":"d","orientation":"particle","pdg_label":1,"self_conjugate":false,"species_id":"model:built-in-sm-leading-color:species:d"},"kernel":"weyl_fermion","kind":"weyl-fermion","mass_class":"massless","mass_parameter":null,"numerator":"i*weyl_slash(momentum)","particle_id":1,"width_parameter":null},"value_slot_id":3,"variant":"source"},{"applies_propagator":false,"chirality":0,"component_start":8,"component_stop":12,"current_component_start":8,"current_component_stop":12,"current_id":4,"dimension":4,"external_labels":[3],"external_mask":4,"is_source":true,"momentum_mask":4,"particle_id":23,"propagator":{"applies_propagator":true,"auxiliary_policy":null,"backend":"symbolica","basis":"lorentz-vector","chirality":0,"custom_source":null,"denominator":"momentum_squared-mass_squared+i*mass*width","description":"massive vector propagator with width","full_tensor_network_ready":true,"gauge":"unitary","goldstone_policy":"absorbed","identity":{"anti_canonical_id":"model:built-in-sm-leading-color:state:z","anti_display_name":"z","anti_pdg_label":23,"canonical_id":"model:built-in-sm-leading-color:state:z","display_name":"z","orientation":"self-conjugate","pdg_label":23,"self_conjugate":true,"species_id":"model:built-in-sm-leading-color:species:z"},"kernel":"massive_vector_unitary_gauge","kind":"vector","mass_class":"massive","mass_parameter":null,"numerator":"-i*(metric-momentum_outer/mass_squared)","particle_id":23,"width_parameter":null},"value_slot_id":4,"variant":"source"},{"applies_propagator":false,"chirality":1,"component_start":12,"component_stop":14,"current_component_start":12,"current_component_stop":14,"current_id":5,"dimension":2,"external_labels":[2,3],"external_mask":6,"is_source":false,"momentum_mask":6,"particle_id":1,"propagator":{"applies_propagator":true,"auxiliary_policy":null,"backend":"spenso","basis":"weyl-chiral","chirality":1,"custom_source":null,"denominator":"momentum_squared","description":"massless Weyl fermion propagator","full_tensor_network_ready":true,"gauge":null,"goldstone_policy":"not-applicable","identity":{"anti_canonical_id":"model:built-in-sm-leading-color:state:d~","anti_display_name":"d~","anti_pdg_label":-1,"canonical_id":"model:built-in-sm-leading-color:state:d","display_name":"d","orientation":"particle","pdg_label":1,"self_conjugate":false,"species_id":"model:built-in-sm-leading-color:species:d"},"kernel":"weyl_fermion","kind":"weyl-fermion","mass_class":"massless","mass_parameter":null,"numerator":"i*weyl_slash(momentum)","particle_id":1,"width_parameter":null},"value_slot_id":5,"variant":"unpropagated"},{"applies_propagator":false,"chirality":-1,"component_start":14,"component_stop":16,"current_component_start":14,"current_component_stop":16,"current_id":6,"dimension":2,"external_labels":[2,3],"external_mask":6,"is_source":false,"momentum_mask":6,"particle_id":1,"propagator":{"applies_propagator":true,"auxiliary_policy":null,"backend":"spenso","basis":"weyl-chiral","chirality":-1,"custom_source":null,"denominator":"momentum_squared","description":"massless Weyl fermion propagator","full_tensor_network_ready":true,"gauge":null,"goldstone_policy":"not-applicable","identity":{"anti_canonical_id":"model:built-in-sm-leading-color:state:d~","anti_display_name":"d~","anti_pdg_label":-1,"canonical_id":"model:built-in-sm-leading-color:state:d","display_name":"d","orientation":"particle","pdg_label":1,"self_conjugate":false,"species_id":"model:built-in-sm-leading-color:species:d"},"kernel":"weyl_fermion","kind":"weyl-fermion","mass_class":"massless","mass_parameter":null,"numerator":"i*weyl_slash(momentum)","particle_id":1,"width_parameter":null},"value_slot_id":6,"variant":"unpropagated"}]}},"schema_version":3},"schedule_mode":"parent-closure","selector_domain_ids":[4,5,6]},{"execution":{"color_accuracy":"lc","compiled":{"kind":"generic-dag-stage-blueprint","model_parameter_evaluator":null,"momentum_slot_ids":[1,2,3],"runtime_available":true,"runtime_unavailable_message":null,"stage_evaluators":{"amplitude_stage":{"blockers":[],"compiled_plane_arena":{"application_abi":"pyamplicol-compiled-plane-kernel-v2","element_layout":"split-complex-component-major","input_bindings":[{"component":0,"global_component":0,"kind":"value","parameter_index":0,"real_valued":false,"source_id":0},{"component":1,"global_component":1,"kind":"value","parameter_index":1,"real_valued":false,"source_id":0},{"component":0,"global_component":2,"kind":"value","parameter_index":2,"real_valued":false,"source_id":1},{"component":1,"global_component":3,"kind":"value","parameter_index":3,"real_valued":false,"source_id":1},{"component":0,"global_component":12,"kind":"value","parameter_index":4,"real_valued":false,"source_id":5},{"component":1,"global_component":13,"kind":"value","parameter_index":5,"real_valued":false,"source_id":5},{"component":0,"global_component":14,"kind":"value","parameter_index":6,"real_valued":false,"source_id":6},{"component":1,"global_component":15,"kind":"value","parameter_index":7,"real_valued":false,"source_id":6}],"input_output_aliasing":"forbidden","kind":"compiled-plane-arena-stage","leaves":[{"application_path":"helicity-selector-union/class-1/evaluators/generic_amplitude_stage_9a660a891f3544c5801ca3482ab26b85.plane.symjit","direct_codegen_optimization_level":2,"input_indices":[0,1,2,3,4,5,6,7],"input_len":8,"optimization_level":2,"output_len":2,"output_start":0,"output_stop":2,"source_application_abi":"pyamplicol-symjit-plane-application-v2"}],"output_bindings":[{"arena":"amplitude","component":0,"output_index":0},{"arena":"amplitude","component":1,"output_index":1}],"output_factor":"identity","output_operation":"overwrite","output_output_aliasing":"forbidden","schema_version":1,"source_application_abi":"pyamplicol-symjit-plane-application-v2"},"evaluator":{"application_abi":"symjit-application-storage-v3","application_path":"helicity-selector-union/class-1/evaluators/generic_amplitude_stage_9a660a891f3544c5801ca3482ab26b85.symjit","batch_layout":"row-major","compiler_type":"native","element_layout":"complex-f64","endianness":"little","evaluator_state_path":null,"evaluator_state_runtime_capability":null,"input_len":8,"kind":"symjit-application-evaluator","optimization_level":2,"output_len":2,"plane_application":{"application_abi":"pyamplicol-symjit-plane-application-v2","application_path":"helicity-selector-union/class-1/evaluators/generic_amplitude_stage_9a660a891f3544c5801ca3482ab26b85.plane.symjit","compiler_type":"native","complex":true,"compression":true,"descriptor_order":"inputs-re-im-then-outputs-re-im","direct_arena":true,"element_layout":"split-complex-plane-major","fast_complex":false,"fast_math":true,"input_complex_count":8,"input_plane_count":16,"optimization_level":2,"output_complex_count":2,"output_plane_count":4,"simd":true,"source_digest":"993dd48ecd84d24358fd411b0f35d3e5a178608a3dd91199c1c0b8b260cfe3b2","storage_abi":"symjit-application-storage-v3","target":{"cpu_features":["aes","bf16","bti","crc","dit","dotprod","dpb","dpb2","fcma","fhm","flagm","fp","fp16","frintts","i8mm","jsconv","lse","lse2","neon","paca","pacg","rcpc","rcpc2","rdm","sb","sha2","sha3","ssbs"],"endianness":"little","triple":"aarch64-apple-darwin","word_bits":64},"threading":false,"translation_mode":"symbolica-structured-instructions"},"required_defuns":[],"runtime_capability":"symjit.application.complex-f64.v1","translation_mode":"indirect","word_bits":64},"evaluator_label":"generic_amplitude_stage","expression_ready":true,"input_components":[{"component":0,"global_component":0,"kind":"value","parameter_index":0,"real_valued":false,"source_id":0},{"component":1,"global_component":1,"kind":"value","parameter_index":1,"real_valued":false,"source_id":0},{"component":0,"global_component":2,"kind":"value","parameter_index":2,"real_valued":false,"source_id":1},{"component":1,"global_component":3,"kind":"value","parameter_index":3,"real_valued":false,"source_id":1},{"component":0,"global_component":12,"kind":"value","parameter_index":4,"real_valued":false,"source_id":5},{"component":1,"global_component":13,"kind":"value","parameter_index":5,"real_valued":false,"source_id":5},{"component":0,"global_component":14,"kind":"value","parameter_index":6,"real_valued":false,"source_id":6},{"component":1,"global_component":15,"kind":"value","parameter_index":7,"real_valued":false,"source_id":6}],"input_value_slot_ids":[0,1,5,6],"interaction_ids":[],"model_parameter_count":0,"momentum_parameter_count":0,"output_length":2,"output_slots":[{"component_start":0,"component_stop":1,"current_id":-1,"output_start":0,"output_stop":1,"value_slot_id":-1,"variant":"amplitude-root"},{"component_start":1,"component_stop":2,"current_id":-1,"output_start":1,"output_stop":2,"value_slot_id":-1,"variant":"amplitude-root"}],"output_value_slot_ids":[],"parameter_count":8,"parameter_layout":"stage-local-value-momentum","real_valued_inputs":[],"stage_index":0,"stage_kind":"amplitude-roots","subset_size":null,"value_parameter_count":8},"kind":"generic-dag-stage-evaluator-artifacts","model_parameter_count":0,"momentum_parameter_count":0,"parameter_count":0,"parameter_layout":"stage-local-value-momentum","real_valued_inputs":[],"required_runtime_capabilities":["compiled-plane-arena-v1","symjit.application.complex-f64.v1"],"runtime_available":true,"runtime_unavailable_message":null,"stage_count":2,"stages":[{"blockers":[],"compiled_plane_arena":{"application_abi":"pyamplicol-compiled-plane-kernel-v2","element_layout":"split-complex-component-major","input_bindings":[{"component":0,"global_component":4,"kind":"value","parameter_index":0,"real_valued":false,"source_id":2},{"component":1,"global_component":5,"kind":"value","parameter_index":1,"real_valued":false,"source_id":2},{"component":0,"global_component":8,"kind":"value","parameter_index":2,"real_valued":false,"source_id":4},{"component":1,"global_component":9,"kind":"value","parameter_index":3,"real_valued":false,"source_id":4},{"component":2,"global_component":10,"kind":"value","parameter_index":4,"real_valued":false,"source_id":4},{"component":3,"global_component":11,"kind":"value","parameter_index":5,"real_valued":false,"source_id":4},{"component":0,"global_component":20,"kind":"momentum","parameter_index":6,"real_valued":true,"source_id":1},{"component":1,"global_component":21,"kind":"momentum","parameter_index":7,"real_valued":true,"source_id":1},{"component":2,"global_component":22,"kind":"momentum","parameter_index":8,"real_valued":true,"source_id":1},{"component":3,"global_component":23,"kind":"momentum","parameter_index":9,"real_valued":true,"source_id":1},{"component":0,"global_component":24,"kind":"momentum","parameter_index":10,"real_valued":true,"source_id":2},{"component":1,"global_component":25,"kind":"momentum","parameter_index":11,"real_valued":true,"source_id":2},{"component":2,"global_component":26,"kind":"momentum","parameter_index":12,"real_valued":true,"source_id":2},{"component":3,"global_component":27,"kind":"momentum","parameter_index":13,"real_valued":true,"source_id":2},{"component":0,"global_component":28,"kind":"momentum","parameter_index":14,"real_valued":true,"source_id":3},{"component":1,"global_component":29,"kind":"momentum","parameter_index":15,"real_valued":true,"source_id":3},{"component":2,"global_component":30,"kind":"momentum","parameter_index":16,"real_valued":true,"source_id":3},{"component":3,"global_component":31,"kind":"momentum","parameter_index":17,"real_valued":true,"source_id":3},{"component":0,"global_component":42,"kind":"model_parameter","parameter_index":18,"real_valued":true,"source_id":10},{"component":0,"global_component":43,"kind":"model_parameter","parameter_index":19,"real_valued":true,"source_id":11}],"input_output_aliasing":"forbidden","kind":"compiled-plane-arena-stage","leaves":[{"application_path":"helicity-selector-union/class-1/evaluators/generic_stage_1_subset_2_aefbc6d0a5894adebc89d2966b0453b1.plane.symjit","direct_codegen_optimization_level":2,"input_indices":[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19],"input_len":20,"optimization_level":2,"output_len":2,"output_start":0,"output_stop":2,"source_application_abi":"pyamplicol-symjit-plane-application-v2"}],"output_bindings":[{"arena":"current","component":12,"output_index":0},{"arena":"current","component":13,"output_index":1}],"output_factor":"identity","output_operation":"overwrite","output_output_aliasing":"forbidden","schema_version":1,"source_application_abi":"pyamplicol-symjit-plane-application-v2"},"evaluator":{"application_abi":"symjit-application-storage-v3","application_path":"helicity-selector-union/class-1/evaluators/generic_stage_1_subset_2_aefbc6d0a5894adebc89d2966b0453b1.symjit","batch_layout":"row-major","compiler_type":"native","element_layout":"complex-f64","endianness":"little","evaluator_state_path":null,"evaluator_state_runtime_capability":null,"input_len":20,"kind":"symjit-application-evaluator","optimization_level":2,"output_len":2,"plane_application":{"application_abi":"pyamplicol-symjit-plane-application-v2","application_path":"helicity-selector-union/class-1/evaluators/generic_stage_1_subset_2_aefbc6d0a5894adebc89d2966b0453b1.plane.symjit","compiler_type":"native","complex":true,"compression":true,"descriptor_order":"inputs-re-im-then-outputs-re-im","direct_arena":true,"element_layout":"split-complex-plane-major","fast_complex":false,"fast_math":true,"input_complex_count":20,"input_plane_count":40,"optimization_level":2,"output_complex_count":2,"output_plane_count":4,"simd":true,"source_digest":"fb2be831a91ef913681e7f1084e98149caaa2ccf4ce35813ec5ce1b9c3280d49","storage_abi":"symjit-application-storage-v3","target":{"cpu_features":["aes","bf16","bti","crc","dit","dotprod","dpb","dpb2","fcma","fhm","flagm","fp","fp16","frintts","i8mm","jsconv","lse","lse2","neon","paca","pacg","rcpc","rcpc2","rdm","sb","sha2","sha3","ssbs"],"endianness":"little","triple":"aarch64-apple-darwin","word_bits":64},"threading":false,"translation_mode":"symbolica-structured-instructions"},"required_defuns":[],"runtime_capability":"symjit.application.complex-f64.v1","translation_mode":"indirect","word_bits":64},"evaluator_label":"generic_stage_1_subset_2","expression_ready":true,"input_components":[{"component":0,"global_component":4,"kind":"value","parameter_index":0,"real_valued":false,"source_id":2},{"component":1,"global_component":5,"kind":"value","parameter_index":1,"real_valued":false,"source_id":2},{"component":0,"global_component":8,"kind":"value","parameter_index":2,"real_valued":false,"source_id":4},{"component":1,"global_component":9,"kind":"value","parameter_index":3,"real_valued":false,"source_id":4},{"component":2,"global_component":10,"kind":"value","parameter_index":4,"real_valued":false,"source_id":4},{"component":3,"global_component":11,"kind":"value","parameter_index":5,"real_valued":false,"source_id":4},{"component":0,"global_component":20,"kind":"momentum","parameter_index":6,"real_valued":true,"source_id":1},{"component":1,"global_component":21,"kind":"momentum","parameter_index":7,"real_valued":true,"source_id":1},{"component":2,"global_component":22,"kind":"momentum","parameter_index":8,"real_valued":true,"source_id":1},{"component":3,"global_component":23,"kind":"momentum","parameter_index":9,"real_valued":true,"source_id":1},{"component":0,"global_component":24,"kind":"momentum","parameter_index":10,"real_valued":true,"source_id":2},{"component":1,"global_component":25,"kind":"momentum","parameter_index":11,"real_valued":true,"source_id":2},{"component":2,"global_component":26,"kind":"momentum","parameter_index":12,"real_valued":true,"source_id":2},{"component":3,"global_component":27,"kind":"momentum","parameter_index":13,"real_valued":true,"source_id":2},{"component":0,"global_component":28,"kind":"momentum","parameter_index":14,"real_valued":true,"source_id":3},{"component":1,"global_component":29,"kind":"momentum","parameter_index":15,"real_valued":true,"source_id":3},{"component":2,"global_component":30,"kind":"momentum","parameter_index":16,"real_valued":true,"source_id":3},{"component":3,"global_component":31,"kind":"momentum","parameter_index":17,"real_valued":true,"source_id":3},{"component":0,"global_component":42,"kind":"model_parameter","parameter_index":18,"real_valued":true,"source_id":10},{"component":0,"global_component":43,"kind":"model_parameter","parameter_index":19,"real_valued":true,"source_id":11}],"input_value_slot_ids":[2,4],"interaction_ids":[0],"model_parameter_count":2,"momentum_parameter_count":12,"output_length":2,"output_slots":[{"component_start":12,"component_stop":14,"current_id":5,"output_start":0,"output_stop":2,"value_slot_id":5,"variant":"unpropagated"}],"output_value_slot_ids":[5],"parameter_count":20,"parameter_layout":"stage-local-value-momentum","real_valued_inputs":[6,7,8,9,10,11,12,13,14,15,16,17,18,19],"stage_index":1,"stage_kind":"current-combine","subset_size":2,"value_parameter_count":6}],"value_parameter_count":0}},"dag_summary":{"amplitude_root_count":2,"current_count":7,"interaction_count":1,"interaction_evaluation_count":1,"source_count":5,"truncated":false},"external_pdg_order":[1,-1,23],"key":"d_dbar_to_z","kind":"pyamplicol-runtime-execution","materialization_census":{"abi":"pyamplicol-fully-resident-materialization-census-v1","basis":"immutable-fully-resident-compiled-dag","final":{"amplitude_root_count":2,"current_count":7,"interaction_count":1,"interaction_evaluation_count":1,"source_count":5},"final_equals_peak":true,"peak":{"amplitude_root_count":2,"current_count":7,"interaction_count":1,"interaction_evaluation_count":1,"source_count":5}},"physics_reduction":{"groups":[{"id":"reduction:0","physical_color_ids":["flow:2,1"],"physical_helicity_ids":["h:+1,-1,-1","h:+1,-1,+0","h:+1,-1,+1"],"representative_color_id":"flow:2,1","representative_helicity_id":"h:+1,-1,-1"},{"id":"reduction:1","physical_color_ids":["flow:2,1"],"physical_helicity_ids":["h:-1,+1,-1","h:-1,+1,+0","h:-1,+1,+1"],"representative_color_id":"flow:2,1","representative_helicity_id":"h:-1,+1,-1"}],"kind":"lc-diagonal"},"process":"d d~ > z","required_runtime_capabilities":["compiled-plane-arena-v1","symjit.application.complex-f64.v1"],"runtime_schema":{"amplitude_stage":{"color_contraction":null,"color_topology_replay":null,"output_count":2,"roots":[{"all_sector_weight":1.0,"coherent_group_id":0,"color_sector_id":0,"color_weight":[1.0,0.0],"contraction":"weyl","contraction_ir":{"chirality_relation":"opposite","coefficients":[[1.0,0.0],[1.0,0.0]],"left_basis":"weyl-chiral","metric_signature":null,"name":"weyl","right_basis":"weyl-chiral"},"coupling":[1.0,0.0],"helicity_weight":1.0,"kind":"direct-contraction","left_current_id":5,"left_slot":{"component_start":12,"component_stop":14,"current_id":5,"dimension":2},"left_value_slot":{"component_start":12,"component_stop":14,"current_id":5,"dimension":2,"value_slot_id":5,"variant":"unpropagated"},"output_index":0,"right_current_id":1,"right_slot":{"component_start":2,"component_stop":4,"current_id":1,"dimension":2},"right_value_slot":{"component_start":2,"component_stop":4,"current_id":1,"dimension":2,"value_slot_id":1,"variant":"source"},"root_id":0,"vertex_kind":null,"vertex_particles":null},{"all_sector_weight":1.0,"coherent_group_id":1,"color_sector_id":0,"color_weight":[1.0,0.0],"contraction":"weyl","contraction_ir":{"chirality_relation":"opposite","coefficients":[[1.0,0.0],[1.0,0.0]],"left_basis":"weyl-chiral","metric_signature":null,"name":"weyl","right_basis":"weyl-chiral"},"coupling":[1.0,0.0],"helicity_weight":1.0,"kind":"direct-contraction","left_current_id":6,"left_slot":{"component_start":14,"component_stop":16,"current_id":6,"dimension":2},"left_value_slot":{"component_start":14,"component_stop":16,"current_id":6,"dimension":2,"value_slot_id":6,"variant":"unpropagated"},"output_index":1,"right_current_id":0,"right_slot":{"component_start":0,"component_stop":2,"current_id":0,"dimension":2},"right_value_slot":{"component_start":0,"component_stop":2,"current_id":0,"dimension":2,"value_slot_id":0,"variant":"source"},"root_id":1,"vertex_kind":null,"vertex_particles":null}],"stage_kind":"amplitude-roots"},"current_storage":{"component_count":16,"current_slots":[{"auxiliary_kind":null,"chirality":1,"color_state":{"accuracy":"lc","basis_key":[],"line_groups":[0],"sector_id":0},"component_start":0,"component_stop":2,"current_id":0,"dimension":2,"external_labels":[1],"external_mask":1,"flavour_flow":[-1],"helicity_ancestry":"0x1","is_source":true,"momentum_mask":1,"particle_id":-1,"spin_state":1},{"auxiliary_kind":null,"chirality":-1,"color_state":{"accuracy":"lc","basis_key":[],"line_groups":[0],"sector_id":0},"component_start":2,"component_stop":4,"current_id":1,"dimension":2,"external_labels":[1],"external_mask":1,"flavour_flow":[-1],"helicity_ancestry":"0x2","is_source":true,"momentum_mask":1,"particle_id":-1,"spin_state":-1},{"auxiliary_kind":null,"chirality":1,"color_state":{"accuracy":"lc","basis_key":[],"line_groups":[0],"sector_id":0},"component_start":4,"component_stop":6,"current_id":2,"dimension":2,"external_labels":[2],"external_mask":2,"flavour_flow":[1],"helicity_ancestry":"0x4","is_source":true,"momentum_mask":2,"particle_id":1,"spin_state":1},{"auxiliary_kind":null,"chirality":-1,"color_state":{"accuracy":"lc","basis_key":[],"line_groups":[0],"sector_id":0},"component_start":6,"component_stop":8,"current_id":3,"dimension":2,"external_labels":[2],"external_mask":2,"flavour_flow":[1],"helicity_ancestry":"0x8","is_source":true,"momentum_mask":2,"particle_id":1,"spin_state":-1},{"auxiliary_kind":null,"chirality":0,"color_state":{"accuracy":"lc","basis_key":[],"line_groups":[],"sector_id":0},"component_start":8,"component_stop":12,"current_id":4,"dimension":4,"external_labels":[3],"external_mask":4,"flavour_flow":[23],"helicity_ancestry":"0x10","is_source":true,"momentum_mask":4,"particle_id":23,"spin_state":-1},{"auxiliary_kind":null,"chirality":1,"color_state":{"accuracy":"lc","basis_key":[],"line_groups":[0],"sector_id":0},"component_start":12,"component_stop":14,"current_id":5,"dimension":2,"external_labels":[2,3],"external_mask":6,"flavour_flow":[1],"helicity_ancestry":"0x14","is_source":false,"momentum_mask":6,"particle_id":1,"spin_state":1},{"auxiliary_kind":null,"chirality":-1,"color_state":{"accuracy":"lc","basis_key":[],"line_groups":[0],"sector_id":0},"component_start":14,"component_stop":16,"current_id":6,"dimension":2,"external_labels":[2,3],"external_mask":6,"flavour_flow":[1],"helicity_ancestry":"0x18","is_source":false,"momentum_mask":6,"particle_id":1,"spin_state":-1}],"metadata_compacted":true,"number_type":"complex"},"external_particles":[{"index":0,"label":1,"momentum_slot":0,"outgoing_pdg":-1,"pdg":1,"role":"initial"},{"index":1,"label":2,"momentum_slot":1,"outgoing_pdg":1,"pdg":-1,"role":"initial"},{"index":2,"label":3,"momentum_slot":2,"outgoing_pdg":23,"pdg":23,"role":"final"}],"kind":"pyamplicol-runtime-execution-plan","model":{"particles":[{"mass":0.0,"mass_parameter":null,"pdg":1},{"mass":91.188,"mass_parameter":"particle.23.mass","pdg":23}]},"model_parameters":[{"default":0.0075467711139788835,"kind":"normalization","name":"normalization.alpha_ew","parameter_index":0},{"default":0.118,"kind":"normalization","name":"normalization.alpha_s_me_check","parameter_index":1},{"default":173.0,"kind":"particle_mass","name":"particle.6.mass","parameter_index":2,"pdg":6},{"default":1.4915,"kind":"particle_width","name":"particle.6.width","parameter_index":3,"pdg":6},{"default":91.188,"kind":"particle_mass","name":"particle.23.mass","parameter_index":4,"pdg":23},{"default":2.441404,"kind":"particle_width","name":"particle.23.width","parameter_index":5,"pdg":23},{"default":80.41900244575616,"kind":"particle_mass","name":"particle.24.mass","parameter_index":6,"pdg":24},{"default":2.0476,"kind":"particle_width","name":"particle.24.width","parameter_index":7,"pdg":24},{"default":125.0,"kind":"particle_mass","name":"particle.25.mass","parameter_index":8,"pdg":25},{"default":0.006382339,"kind":"particle_width","name":"particle.25.width","parameter_index":9,"pdg":25},{"default":-1.0244420275940371,"kind":"coupling_component","name":"coupling.10.1_23_1.component_0","parameter_index":10},{"default":0.17818666745287456,"kind":"coupling_component","name":"coupling.10.1_23_1.component_1","parameter_index":11}],"momentum_slots":[{"component_start":0,"component_stop":4,"external_labels":[1],"momentum_mask":1,"momentum_slot_id":0,"real_valued":true},{"component_start":4,"component_stop":8,"external_labels":[2],"momentum_mask":2,"momentum_slot_id":1,"real_valued":true},{"component_start":8,"component_stop":12,"external_labels":[3],"momentum_mask":4,"momentum_slot_id":2,"real_valued":true},{"component_start":12,"component_stop":16,"external_labels":[2,3],"momentum_mask":6,"momentum_slot_id":3,"real_valued":true}],"normalization":{"average_factor":36.0,"color_factor":3.0,"electroweak_coupling_power":1,"global_coupling_factor":0.18967104551999775,"identical_factor":1.0,"qcd_coupling_power":0},"parameter_layout":{"model_parameter_count":12,"momentum_components_real":true,"momentum_parameter_count":16,"parameter_count_if_flattened":40,"real_valued_inputs":[12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39],"source_component_parameter_count":12,"source_components_complex":true,"value_component_count":16},"process":"d d~ > z","process_key":"d_dbar_to_z","schema_version":3,"source_fill":{"source_count":5,"sources":[{"anti_particle_id":1,"applied_crossing":{"chirality_factor":-1,"helicity_factor":1,"momentum_transform":"negate-four-momentum","phase":[1.0,0.0],"spin_state_factor":-1},"chirality":1,"color_state":{"accuracy":"lc","basis_key":[],"line_groups":[0],"sector_id":0},"crossing":"negate-incoming-momentum","current_component_start":0,"current_component_stop":2,"current_id":0,"dimension":2,"helicity_ancestry":"0x1","input_momentum_slot":0,"leg_label":1,"outgoing_pdg":-1,"particle_id":-1,"physical_pdg":1,"side":"initial","source_basis":"weyl-chiral","source_helicity":-1,"source_id":0,"source_ir":{"basis":"weyl-chiral","component_dimension":2,"crossing":{"chirality_factor":-1,"helicity_factor":1,"momentum_transform":"negate-four-momentum","phase":[1.0,0.0],"spin_state_factor":-1},"identity":{"anti_canonical_id":"model:built-in-sm-leading-color:state:d","anti_display_name":"d","anti_pdg_label":1,"canonical_id":"model:built-in-sm-leading-color:state:d~","display_name":"d~","orientation":"antiparticle","pdg_label":-1,"self_conjugate":false,"species_id":"model:built-in-sm-leading-color:species:d"},"mass_parameter":null,"states":[{"chirality":-1,"helicity":-1,"spin_state":-1},{"chirality":1,"helicity":1,"spin_state":1}],"statistics":"fermion","wavefunction_family":"fermion","width_parameter":null},"source_kind":"external-wavefunction","source_orientation":"antiparticle","source_parameter_start":0,"source_parameter_stop":2,"spin_state":1,"value_slot":{"component_start":0,"component_stop":2,"current_id":0,"dimension":2,"value_slot_id":0,"variant":"source"},"wavefunction_kind":"fermion"},{"anti_particle_id":1,"applied_crossing":{"chirality_factor":-1,"helicity_factor":1,"momentum_transform":"negate-four-momentum","phase":[1.0,0.0],"spin_state_factor":-1},"chirality":-1,"color_state":{"accuracy":"lc","basis_key":[],"line_groups":[0],"sector_id":0},"crossing":"negate-incoming-momentum","current_component_start":2,"current_component_stop":4,"current_id":1,"dimension":2,"helicity_ancestry":"0x2","input_momentum_slot":0,"leg_label":1,"outgoing_pdg":-1,"particle_id":-1,"physical_pdg":1,"side":"initial","source_basis":"weyl-chiral","source_helicity":1,"source_id":1,"source_ir":{"basis":"weyl-chiral","component_dimension":2,"crossing":{"chirality_factor":-1,"helicity_factor":1,"momentum_transform":"negate-four-momentum","phase":[1.0,0.0],"spin_state_factor":-1},"identity":{"anti_canonical_id":"model:built-in-sm-leading-color:state:d","anti_display_name":"d","anti_pdg_label":1,"canonical_id":"model:built-in-sm-leading-color:state:d~","display_name":"d~","orientation":"antiparticle","pdg_label":-1,"self_conjugate":false,"species_id":"model:built-in-sm-leading-color:species:d"},"mass_parameter":null,"states":[{"chirality":-1,"helicity":-1,"spin_state":-1},{"chirality":1,"helicity":1,"spin_state":1}],"statistics":"fermion","wavefunction_family":"fermion","width_parameter":null},"source_kind":"external-wavefunction","source_orientation":"antiparticle","source_parameter_start":2,"source_parameter_stop":4,"spin_state":-1,"value_slot":{"component_start":2,"component_stop":4,"current_id":1,"dimension":2,"value_slot_id":1,"variant":"source"},"wavefunction_kind":"fermion"},{"anti_particle_id":-1,"applied_crossing":{"chirality_factor":-1,"helicity_factor":1,"momentum_transform":"negate-four-momentum","phase":[1.0,0.0],"spin_state_factor":-1},"chirality":1,"color_state":{"accuracy":"lc","basis_key":[],"line_groups":[0],"sector_id":0},"crossing":"negate-incoming-momentum","current_component_start":4,"current_component_stop":6,"current_id":2,"dimension":2,"helicity_ancestry":"0x4","input_momentum_slot":1,"leg_label":2,"outgoing_pdg":1,"particle_id":1,"physical_pdg":-1,"side":"initial","source_basis":"weyl-chiral","source_helicity":-1,"source_id":2,"source_ir":{"basis":"weyl-chiral","component_dimension":2,"crossing":{"chirality_factor":-1,"helicity_factor":1,"momentum_transform":"negate-four-momentum","phase":[1.0,0.0],"spin_state_factor":-1},"identity":{"anti_canonical_id":"model:built-in-sm-leading-color:state:d~","anti_display_name":"d~","anti_pdg_label":-1,"canonical_id":"model:built-in-sm-leading-color:state:d","display_name":"d","orientation":"particle","pdg_label":1,"self_conjugate":false,"species_id":"model:built-in-sm-leading-color:species:d"},"mass_parameter":null,"states":[{"chirality":-1,"helicity":-1,"spin_state":-1},{"chirality":1,"helicity":1,"spin_state":1}],"statistics":"fermion","wavefunction_family":"fermion","width_parameter":null},"source_kind":"external-wavefunction","source_orientation":"particle","source_parameter_start":4,"source_parameter_stop":6,"spin_state":1,"value_slot":{"component_start":4,"component_stop":6,"current_id":2,"dimension":2,"value_slot_id":2,"variant":"source"},"wavefunction_kind":"fermion"},{"anti_particle_id":-1,"applied_crossing":{"chirality_factor":-1,"helicity_factor":1,"momentum_transform":"negate-four-momentum","phase":[1.0,0.0],"spin_state_factor":-1},"chirality":-1,"color_state":{"accuracy":"lc","basis_key":[],"line_groups":[0],"sector_id":0},"crossing":"negate-incoming-momentum","current_component_start":6,"current_component_stop":8,"current_id":3,"dimension":2,"helicity_ancestry":"0x8","input_momentum_slot":1,"leg_label":2,"outgoing_pdg":1,"particle_id":1,"physical_pdg":-1,"side":"initial","source_basis":"weyl-chiral","source_helicity":1,"source_id":3,"source_ir":{"basis":"weyl-chiral","component_dimension":2,"crossing":{"chirality_factor":-1,"helicity_factor":1,"momentum_transform":"negate-four-momentum","phase":[1.0,0.0],"spin_state_factor":-1},"identity":{"anti_canonical_id":"model:built-in-sm-leading-color:state:d~","anti_display_name":"d~","anti_pdg_label":-1,"canonical_id":"model:built-in-sm-leading-color:state:d","display_name":"d","orientation":"particle","pdg_label":1,"self_conjugate":false,"species_id":"model:built-in-sm-leading-color:species:d"},"mass_parameter":null,"states":[{"chirality":-1,"helicity":-1,"spin_state":-1},{"chirality":1,"helicity":1,"spin_state":1}],"statistics":"fermion","wavefunction_family":"fermion","width_parameter":null},"source_kind":"external-wavefunction","source_orientation":"particle","source_parameter_start":6,"source_parameter_stop":8,"spin_state":-1,"value_slot":{"component_start":6,"component_stop":8,"current_id":3,"dimension":2,"value_slot_id":3,"variant":"source"},"wavefunction_kind":"fermion"},{"anti_particle_id":23,"applied_crossing":{"chirality_factor":1,"helicity_factor":1,"momentum_transform":"identity","phase":[1.0,0.0],"spin_state_factor":1},"chirality":0,"color_state":{"accuracy":"lc","basis_key":[],"line_groups":[],"sector_id":0},"crossing":"identity","current_component_start":8,"current_component_stop":12,"current_id":4,"dimension":4,"helicity_ancestry":"0x10","input_momentum_slot":2,"leg_label":3,"outgoing_pdg":23,"particle_id":23,"physical_pdg":23,"side":"final","source_basis":"lorentz-vector","source_helicity":-1,"source_id":4,"source_ir":{"basis":"lorentz-vector","component_dimension":4,"crossing":{"chirality_factor":1,"helicity_factor":-1,"momentum_transform":"negate-four-momentum","phase":[1.0,0.0],"spin_state_factor":-1},"identity":{"anti_canonical_id":"model:built-in-sm-leading-color:state:z","anti_display_name":"z","anti_pdg_label":23,"canonical_id":"model:built-in-sm-leading-color:state:z","display_name":"z","orientation":"self-conjugate","pdg_label":23,"self_conjugate":true,"species_id":"model:built-in-sm-leading-color:species:z"},"mass_parameter":null,"states":[{"chirality":0,"helicity":-1,"spin_state":-1},{"chirality":0,"helicity":0,"spin_state":0},{"chirality":0,"helicity":1,"spin_state":1}],"statistics":"boson","wavefunction_family":"vector","width_parameter":null},"source_kind":"external-wavefunction","source_orientation":"self-conjugate","source_parameter_start":8,"source_parameter_stop":12,"spin_state":-1,"value_slot":{"component_start":8,"component_stop":12,"current_id":4,"dimension":4,"value_slot_id":4,"variant":"source"},"wavefunction_kind":"vector"}]},"stages":[{"input_current_ids":[2,4],"input_value_slot_ids":[2,4],"interaction_count":1,"interaction_ids":[0],"interactions":[],"interactions_compacted":true,"output_current_ids":[5],"output_value_slot_ids":[5],"stage_index":1,"stage_kind":"current-combine","subset_size":2}],"value_storage":{"component_count":16,"metadata_compacted":true,"number_type":"complex","value_slots":[{"applies_propagator":false,"chirality":1,"component_start":0,"component_stop":2,"current_component_start":0,"current_component_stop":2,"current_id":0,"dimension":2,"external_labels":[1],"external_mask":1,"is_source":true,"momentum_mask":1,"particle_id":-1,"propagator":{"applies_propagator":true,"auxiliary_policy":null,"backend":"spenso","basis":"weyl-chiral","chirality":1,"custom_source":null,"denominator":"momentum_squared","description":"massless Weyl fermion propagator","full_tensor_network_ready":true,"gauge":null,"goldstone_policy":"not-applicable","identity":{"anti_canonical_id":"model:built-in-sm-leading-color:state:d","anti_display_name":"d","anti_pdg_label":1,"canonical_id":"model:built-in-sm-leading-color:state:d~","display_name":"d~","orientation":"antiparticle","pdg_label":-1,"self_conjugate":false,"species_id":"model:built-in-sm-leading-color:species:d"},"kernel":"weyl_fermion","kind":"weyl-fermion","mass_class":"massless","mass_parameter":null,"numerator":"i*weyl_slash(momentum)","particle_id":-1,"width_parameter":null},"value_slot_id":0,"variant":"source"},{"applies_propagator":false,"chirality":-1,"component_start":2,"component_stop":4,"current_component_start":2,"current_component_stop":4,"current_id":1,"dimension":2,"external_labels":[1],"external_mask":1,"is_source":true,"momentum_mask":1,"particle_id":-1,"propagator":{"applies_propagator":true,"auxiliary_policy":null,"backend":"spenso","basis":"weyl-chiral","chirality":-1,"custom_source":null,"denominator":"momentum_squared","description":"massless Weyl fermion propagator","full_tensor_network_ready":true,"gauge":null,"goldstone_policy":"not-applicable","identity":{"anti_canonical_id":"model:built-in-sm-leading-color:state:d","anti_display_name":"d","anti_pdg_label":1,"canonical_id":"model:built-in-sm-leading-color:state:d~","display_name":"d~","orientation":"antiparticle","pdg_label":-1,"self_conjugate":false,"species_id":"model:built-in-sm-leading-color:species:d"},"kernel":"weyl_fermion","kind":"weyl-fermion","mass_class":"massless","mass_parameter":null,"numerator":"i*weyl_slash(momentum)","particle_id":-1,"width_parameter":null},"value_slot_id":1,"variant":"source"},{"applies_propagator":false,"chirality":1,"component_start":4,"component_stop":6,"current_component_start":4,"current_component_stop":6,"current_id":2,"dimension":2,"external_labels":[2],"external_mask":2,"is_source":true,"momentum_mask":2,"particle_id":1,"propagator":{"applies_propagator":true,"auxiliary_policy":null,"backend":"spenso","basis":"weyl-chiral","chirality":1,"custom_source":null,"denominator":"momentum_squared","description":"massless Weyl fermion propagator","full_tensor_network_ready":true,"gauge":null,"goldstone_policy":"not-applicable","identity":{"anti_canonical_id":"model:built-in-sm-leading-color:state:d~","anti_display_name":"d~","anti_pdg_label":-1,"canonical_id":"model:built-in-sm-leading-color:state:d","display_name":"d","orientation":"particle","pdg_label":1,"self_conjugate":false,"species_id":"model:built-in-sm-leading-color:species:d"},"kernel":"weyl_fermion","kind":"weyl-fermion","mass_class":"massless","mass_parameter":null,"numerator":"i*weyl_slash(momentum)","particle_id":1,"width_parameter":null},"value_slot_id":2,"variant":"source"},{"applies_propagator":false,"chirality":-1,"component_start":6,"component_stop":8,"current_component_start":6,"current_component_stop":8,"current_id":3,"dimension":2,"external_labels":[2],"external_mask":2,"is_source":true,"momentum_mask":2,"particle_id":1,"propagator":{"applies_propagator":true,"auxiliary_policy":null,"backend":"spenso","basis":"weyl-chiral","chirality":-1,"custom_source":null,"denominator":"momentum_squared","description":"massless Weyl fermion propagator","full_tensor_network_ready":true,"gauge":null,"goldstone_policy":"not-applicable","identity":{"anti_canonical_id":"model:built-in-sm-leading-color:state:d~","anti_display_name":"d~","anti_pdg_label":-1,"canonical_id":"model:built-in-sm-leading-color:state:d","display_name":"d","orientation":"particle","pdg_label":1,"self_conjugate":false,"species_id":"model:built-in-sm-leading-color:species:d"},"kernel":"weyl_fermion","kind":"weyl-fermion","mass_class":"massless","mass_parameter":null,"numerator":"i*weyl_slash(momentum)","particle_id":1,"width_parameter":null},"value_slot_id":3,"variant":"source"},{"applies_propagator":false,"chirality":0,"component_start":8,"component_stop":12,"current_component_start":8,"current_component_stop":12,"current_id":4,"dimension":4,"external_labels":[3],"external_mask":4,"is_source":true,"momentum_mask":4,"particle_id":23,"propagator":{"applies_propagator":true,"auxiliary_policy":null,"backend":"symbolica","basis":"lorentz-vector","chirality":0,"custom_source":null,"denominator":"momentum_squared-mass_squared+i*mass*width","description":"massive vector propagator with width","full_tensor_network_ready":true,"gauge":"unitary","goldstone_policy":"absorbed","identity":{"anti_canonical_id":"model:built-in-sm-leading-color:state:z","anti_display_name":"z","anti_pdg_label":23,"canonical_id":"model:built-in-sm-leading-color:state:z","display_name":"z","orientation":"self-conjugate","pdg_label":23,"self_conjugate":true,"species_id":"model:built-in-sm-leading-color:species:z"},"kernel":"massive_vector_unitary_gauge","kind":"vector","mass_class":"massive","mass_parameter":null,"numerator":"-i*(metric-momentum_outer/mass_squared)","particle_id":23,"width_parameter":null},"value_slot_id":4,"variant":"source"},{"applies_propagator":false,"chirality":1,"component_start":12,"component_stop":14,"current_component_start":12,"current_component_stop":14,"current_id":5,"dimension":2,"external_labels":[2,3],"external_mask":6,"is_source":false,"momentum_mask":6,"particle_id":1,"propagator":{"applies_propagator":true,"auxiliary_policy":null,"backend":"spenso","basis":"weyl-chiral","chirality":1,"custom_source":null,"denominator":"momentum_squared","description":"massless Weyl fermion propagator","full_tensor_network_ready":true,"gauge":null,"goldstone_policy":"not-applicable","identity":{"anti_canonical_id":"model:built-in-sm-leading-color:state:d~","anti_display_name":"d~","anti_pdg_label":-1,"canonical_id":"model:built-in-sm-leading-color:state:d","display_name":"d","orientation":"particle","pdg_label":1,"self_conjugate":false,"species_id":"model:built-in-sm-leading-color:species:d"},"kernel":"weyl_fermion","kind":"weyl-fermion","mass_class":"massless","mass_parameter":null,"numerator":"i*weyl_slash(momentum)","particle_id":1,"width_parameter":null},"value_slot_id":5,"variant":"unpropagated"},{"applies_propagator":false,"chirality":-1,"component_start":14,"component_stop":16,"current_component_start":14,"current_component_stop":16,"current_id":6,"dimension":2,"external_labels":[2,3],"external_mask":6,"is_source":false,"momentum_mask":6,"particle_id":1,"propagator":{"applies_propagator":true,"auxiliary_policy":null,"backend":"spenso","basis":"weyl-chiral","chirality":-1,"custom_source":null,"denominator":"momentum_squared","description":"massless Weyl fermion propagator","full_tensor_network_ready":true,"gauge":null,"goldstone_policy":"not-applicable","identity":{"anti_canonical_id":"model:built-in-sm-leading-color:state:d~","anti_display_name":"d~","anti_pdg_label":-1,"canonical_id":"model:built-in-sm-leading-color:state:d","display_name":"d","orientation":"particle","pdg_label":1,"self_conjugate":false,"species_id":"model:built-in-sm-leading-color:species:d"},"kernel":"weyl_fermion","kind":"weyl-fermion","mass_class":"massless","mass_parameter":null,"numerator":"i*weyl_slash(momentum)","particle_id":1,"width_parameter":null},"value_slot_id":6,"variant":"unpropagated"}]}},"schema_version":3},"schedule_mode":"parent-closure","selector_domain_ids":[8,9,10]}],"helicity_sum_execution":{"color_accuracy":"lc","color_selector_executions":[{"execution":{"color_accuracy":"lc","compiled":{"kind":"generic-dag-stage-blueprint","model_parameter_evaluator":null,"momentum_slot_ids":[1,2,3],"runtime_available":true,"runtime_unavailable_message":null,"stage_evaluators":{"amplitude_stage":{"blockers":[],"compiled_plane_arena":{"application_abi":"pyamplicol-compiled-plane-kernel-v2","element_layout":"split-complex-component-major","input_bindings":[{"component":0,"global_component":0,"kind":"value","parameter_index":0,"real_valued":false,"source_id":0},{"component":1,"global_component":1,"kind":"value","parameter_index":1,"real_valued":false,"source_id":0},{"component":0,"global_component":2,"kind":"value","parameter_index":2,"real_valued":false,"source_id":1},{"component":1,"global_component":3,"kind":"value","parameter_index":3,"real_valued":false,"source_id":1},{"component":0,"global_component":20,"kind":"value","parameter_index":4,"real_valued":false,"source_id":7},{"component":1,"global_component":21,"kind":"value","parameter_index":5,"real_valued":false,"source_id":7},{"component":0,"global_component":22,"kind":"value","parameter_index":6,"real_valued":false,"source_id":8},{"component":1,"global_component":23,"kind":"value","parameter_index":7,"real_valued":false,"source_id":8},{"component":0,"global_component":24,"kind":"value","parameter_index":8,"real_valued":false,"source_id":9},{"component":1,"global_component":25,"kind":"value","parameter_index":9,"real_valued":false,"source_id":9},{"component":0,"global_component":26,"kind":"value","parameter_index":10,"real_valued":false,"source_id":10},{"component":1,"global_component":27,"kind":"value","parameter_index":11,"real_valued":false,"source_id":10},{"component":0,"global_component":28,"kind":"value","parameter_index":12,"real_valued":false,"source_id":11},{"component":1,"global_component":29,"kind":"value","parameter_index":13,"real_valued":false,"source_id":11},{"component":0,"global_component":30,"kind":"value","parameter_index":14,"real_valued":false,"source_id":12},{"component":1,"global_component":31,"kind":"value","parameter_index":15,"real_valued":false,"source_id":12}],"input_output_aliasing":"forbidden","kind":"compiled-plane-arena-stage","leaves":[{"application_path":"helicity-sum/color-selector/sector-0/evaluators/generic_amplitude_stage_d16d6a1bc14b4523b1ef68089829af31.plane.symjit","direct_codegen_optimization_level":2,"input_indices":[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15],"input_len":16,"optimization_level":2,"output_len":6,"output_start":0,"output_stop":6,"source_application_abi":"pyamplicol-symjit-plane-application-v2"}],"output_bindings":[{"arena":"amplitude","component":0,"output_index":0},{"arena":"amplitude","component":1,"output_index":1},{"arena":"amplitude","component":2,"output_index":2},{"arena":"amplitude","component":3,"output_index":3},{"arena":"amplitude","component":4,"output_index":4},{"arena":"amplitude","component":5,"output_index":5}],"output_factor":"identity","output_operation":"overwrite","output_output_aliasing":"forbidden","schema_version":1,"source_application_abi":"pyamplicol-symjit-plane-application-v2"},"evaluator":{"application_abi":"symjit-application-storage-v3","application_path":"helicity-sum/color-selector/sector-0/evaluators/generic_amplitude_stage_d16d6a1bc14b4523b1ef68089829af31.symjit","batch_layout":"row-major","compiler_type":"native","element_layout":"complex-f64","endianness":"little","evaluator_state_path":null,"evaluator_state_runtime_capability":null,"input_len":16,"kind":"symjit-application-evaluator","optimization_level":2,"output_len":6,"plane_application":{"application_abi":"pyamplicol-symjit-plane-application-v2","application_path":"helicity-sum/color-selector/sector-0/evaluators/generic_amplitude_stage_d16d6a1bc14b4523b1ef68089829af31.plane.symjit","compiler_type":"native","complex":true,"compression":true,"descriptor_order":"inputs-re-im-then-outputs-re-im","direct_arena":true,"element_layout":"split-complex-plane-major","fast_complex":false,"fast_math":true,"input_complex_count":16,"input_plane_count":32,"optimization_level":2,"output_complex_count":6,"output_plane_count":12,"simd":true,"source_digest":"2a37e3f222d3a34a70cc55f8517b6823d0c17e6efad8f06f75e39ed367d6e6b1","storage_abi":"symjit-application-storage-v3","target":{"cpu_features":["aes","bf16","bti","crc","dit","dotprod","dpb","dpb2","fcma","fhm","flagm","fp","fp16","frintts","i8mm","jsconv","lse","lse2","neon","paca","pacg","rcpc","rcpc2","rdm","sb","sha2","sha3","ssbs"],"endianness":"little","triple":"aarch64-apple-darwin","word_bits":64},"threading":false,"translation_mode":"symbolica-structured-instructions"},"required_defuns":[],"runtime_capability":"symjit.application.complex-f64.v1","translation_mode":"indirect","word_bits":64},"evaluator_label":"generic_amplitude_stage","expression_ready":true,"input_components":[{"component":0,"global_component":0,"kind":"value","parameter_index":0,"real_valued":false,"source_id":0},{"component":1,"global_component":1,"kind":"value","parameter_index":1,"real_valued":false,"source_id":0},{"component":0,"global_component":2,"kind":"value","parameter_index":2,"real_valued":false,"source_id":1},{"component":1,"global_component":3,"kind":"value","parameter_index":3,"real_valued":false,"source_id":1},{"component":0,"global_component":20,"kind":"value","parameter_index":4,"real_valued":false,"source_id":7},{"component":1,"global_component":21,"kind":"value","parameter_index":5,"real_valued":false,"source_id":7},{"component":0,"global_component":22,"kind":"value","parameter_index":6,"real_valued":false,"source_id":8},{"component":1,"global_component":23,"kind":"value","parameter_index":7,"real_valued":false,"source_id":8},{"component":0,"global_component":24,"kind":"value","parameter_index":8,"real_valued":false,"source_id":9},{"component":1,"global_component":25,"kind":"value","parameter_index":9,"real_valued":false,"source_id":9},{"component":0,"global_component":26,"kind":"value","parameter_index":10,"real_valued":false,"source_id":10},{"component":1,"global_component":27,"kind":"value","parameter_index":11,"real_valued":false,"source_id":10},{"component":0,"global_component":28,"kind":"value","parameter_index":12,"real_valued":false,"source_id":11},{"component":1,"global_component":29,"kind":"value","parameter_index":13,"real_valued":false,"source_id":11},{"component":0,"global_component":30,"kind":"value","parameter_index":14,"real_valued":false,"source_id":12},{"component":1,"global_component":31,"kind":"value","parameter_index":15,"real_valued":false,"source_id":12}],"input_value_slot_ids":[0,1,7,8,9,10,11,12],"interaction_ids":[],"model_parameter_count":0,"momentum_parameter_count":0,"output_length":6,"output_slots":[{"component_start":0,"component_stop":1,"current_id":-1,"output_start":0,"output_stop":1,"value_slot_id":-1,"variant":"amplitude-root"},{"component_start":1,"component_stop":2,"current_id":-1,"output_start":1,"output_stop":2,"value_slot_id":-1,"variant":"amplitude-root"},{"component_start":2,"component_stop":3,"current_id":-1,"output_start":2,"output_stop":3,"value_slot_id":-1,"variant":"amplitude-root"},{"component_start":3,"component_stop":4,"current_id":-1,"output_start":3,"output_stop":4,"value_slot_id":-1,"variant":"amplitude-root"},{"component_start":4,"component_stop":5,"current_id":-1,"output_start":4,"output_stop":5,"value_slot_id":-1,"variant":"amplitude-root"},{"component_start":5,"component_stop":6,"current_id":-1,"output_start":5,"output_stop":6,"value_slot_id":-1,"variant":"amplitude-root"}],"output_value_slot_ids":[],"parameter_count":16,"parameter_layout":"stage-local-value-momentum","real_valued_inputs":[],"stage_index":0,"stage_kind":"amplitude-roots","subset_size":null,"value_parameter_count":16},"kind":"generic-dag-stage-evaluator-artifacts","model_parameter_count":0,"momentum_parameter_count":0,"parameter_count":0,"parameter_layout":"stage-local-value-momentum","real_valued_inputs":[],"required_runtime_capabilities":["compiled-plane-arena-v1","symjit.application.complex-f64.v1"],"runtime_available":true,"runtime_unavailable_message":null,"stage_count":2,"stages":[{"blockers":[],"compiled_plane_arena":{"application_abi":"pyamplicol-compiled-plane-kernel-v2","element_layout":"split-complex-component-major","input_bindings":[{"component":0,"global_component":4,"kind":"value","parameter_index":0,"real_valued":false,"source_id":2},{"component":1,"global_component":5,"kind":"value","parameter_index":1,"real_valued":false,"source_id":2},{"component":0,"global_component":6,"kind":"value","parameter_index":2,"real_valued":false,"source_id":3},{"component":1,"global_component":7,"kind":"value","parameter_index":3,"real_valued":false,"source_id":3},{"component":0,"global_component":8,"kind":"value","parameter_index":4,"real_valued":false,"source_id":4},{"component":1,"global_component":9,"kind":"value","parameter_index":5,"real_valued":false,"source_id":4},{"component":2,"global_component":10,"kind":"value","parameter_index":6,"real_valued":false,"source_id":4},{"component":3,"global_component":11,"kind":"value","parameter_index":7,"real_valued":false,"source_id":4},{"component":0,"global_component":12,"kind":"value","parameter_index":8,"real_valued":false,"source_id":5},{"component":1,"global_component":13,"kind":"value","parameter_index":9,"real_valued":false,"source_id":5},{"component":2,"global_component":14,"kind":"value","parameter_index":10,"real_valued":false,"source_id":5},{"component":3,"global_component":15,"kind":"value","parameter_index":11,"real_valued":false,"source_id":5},{"component":0,"global_component":16,"kind":"value","parameter_index":12,"real_valued":false,"source_id":6},{"component":1,"global_component":17,"kind":"value","parameter_index":13,"real_valued":false,"source_id":6},{"component":2,"global_component":18,"kind":"value","parameter_index":14,"real_valued":false,"source_id":6},{"component":3,"global_component":19,"kind":"value","parameter_index":15,"real_valued":false,"source_id":6},{"component":0,"global_component":36,"kind":"momentum","parameter_index":16,"real_valued":true,"source_id":1},{"component":1,"global_component":37,"kind":"momentum","parameter_index":17,"real_valued":true,"source_id":1},{"component":2,"global_component":38,"kind":"momentum","parameter_index":18,"real_valued":true,"source_id":1},{"component":3,"global_component":39,"kind":"momentum","parameter_index":19,"real_valued":true,"source_id":1},{"component":0,"global_component":40,"kind":"momentum","parameter_index":20,"real_valued":true,"source_id":2},{"component":1,"global_component":41,"kind":"momentum","parameter_index":21,"real_valued":true,"source_id":2},{"component":2,"global_component":42,"kind":"momentum","parameter_index":22,"real_valued":true,"source_id":2},{"component":3,"global_component":43,"kind":"momentum","parameter_index":23,"real_valued":true,"source_id":2},{"component":0,"global_component":44,"kind":"momentum","parameter_index":24,"real_valued":true,"source_id":3},{"component":1,"global_component":45,"kind":"momentum","parameter_index":25,"real_valued":true,"source_id":3},{"component":2,"global_component":46,"kind":"momentum","parameter_index":26,"real_valued":true,"source_id":3},{"component":3,"global_component":47,"kind":"momentum","parameter_index":27,"real_valued":true,"source_id":3},{"component":0,"global_component":58,"kind":"model_parameter","parameter_index":28,"real_valued":true,"source_id":10},{"component":0,"global_component":59,"kind":"model_parameter","parameter_index":29,"real_valued":true,"source_id":11}],"input_output_aliasing":"forbidden","kind":"compiled-plane-arena-stage","leaves":[{"application_path":"helicity-sum/color-selector/sector-0/evaluators/generic_stage_1_subset_2_bbc3a3ebdb2248798889aa13eafb5e51.plane.symjit","direct_codegen_optimization_level":2,"input_indices":[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29],"input_len":30,"optimization_level":2,"output_len":12,"output_start":0,"output_stop":12,"source_application_abi":"pyamplicol-symjit-plane-application-v2"}],"output_bindings":[{"arena":"current","component":20,"output_index":0},{"arena":"current","component":21,"output_index":1},{"arena":"current","component":22,"output_index":2},{"arena":"current","component":23,"output_index":3},{"arena":"current","component":24,"output_index":4},{"arena":"current","component":25,"output_index":5},{"arena":"current","component":26,"output_index":6},{"arena":"current","component":27,"output_index":7},{"arena":"current","component":28,"output_index":8},{"arena":"current","component":29,"output_index":9},{"arena":"current","component":30,"output_index":10},{"arena":"current","component":31,"output_index":11}],"output_factor":"identity","output_operation":"overwrite","output_output_aliasing":"forbidden","schema_version":1,"source_application_abi":"pyamplicol-symjit-plane-application-v2"},"evaluator":{"application_abi":"symjit-application-storage-v3","application_path":"helicity-sum/color-selector/sector-0/evaluators/generic_stage_1_subset_2_bbc3a3ebdb2248798889aa13eafb5e51.symjit","batch_layout":"row-major","compiler_type":"native","element_layout":"complex-f64","endianness":"little","evaluator_state_path":null,"evaluator_state_runtime_capability":null,"input_len":30,"kind":"symjit-application-evaluator","optimization_level":2,"output_len":12,"plane_application":{"application_abi":"pyamplicol-symjit-plane-application-v2","application_path":"helicity-sum/color-selector/sector-0/evaluators/generic_stage_1_subset_2_bbc3a3ebdb2248798889aa13eafb5e51.plane.symjit","compiler_type":"native","complex":true,"compression":true,"descriptor_order":"inputs-re-im-then-outputs-re-im","direct_arena":true,"element_layout":"split-complex-plane-major","fast_complex":false,"fast_math":true,"input_complex_count":30,"input_plane_count":60,"optimization_level":2,"output_complex_count":12,"output_plane_count":24,"simd":true,"source_digest":"932205a6edcc3a03c036ff3ac62bcbb2189b3471d544fffa5d82f5a25033b0c2","storage_abi":"symjit-application-storage-v3","target":{"cpu_features":["aes","bf16","bti","crc","dit","dotprod","dpb","dpb2","fcma","fhm","flagm","fp","fp16","frintts","i8mm","jsconv","lse","lse2","neon","paca","pacg","rcpc","rcpc2","rdm","sb","sha2","sha3","ssbs"],"endianness":"little","triple":"aarch64-apple-darwin","word_bits":64},"threading":false,"translation_mode":"symbolica-structured-instructions"},"required_defuns":[],"runtime_capability":"symjit.application.complex-f64.v1","translation_mode":"indirect","word_bits":64},"evaluator_label":"generic_stage_1_subset_2","expression_ready":true,"input_components":[{"component":0,"global_component":4,"kind":"value","parameter_index":0,"real_valued":false,"source_id":2},{"component":1,"global_component":5,"kind":"value","parameter_index":1,"real_valued":false,"source_id":2},{"component":0,"global_component":6,"kind":"value","parameter_index":2,"real_valued":false,"source_id":3},{"component":1,"global_component":7,"kind":"value","parameter_index":3,"real_valued":false,"source_id":3},{"component":0,"global_component":8,"kind":"value","parameter_index":4,"real_valued":false,"source_id":4},{"component":1,"global_component":9,"kind":"value","parameter_index":5,"real_valued":false,"source_id":4},{"component":2,"global_component":10,"kind":"value","parameter_index":6,"real_valued":false,"source_id":4},{"component":3,"global_component":11,"kind":"value","parameter_index":7,"real_valued":false,"source_id":4},{"component":0,"global_component":12,"kind":"value","parameter_index":8,"real_valued":false,"source_id":5},{"component":1,"global_component":13,"kind":"value","parameter_index":9,"real_valued":false,"source_id":5},{"component":2,"global_component":14,"kind":"value","parameter_index":10,"real_valued":false,"source_id":5},{"component":3,"global_component":15,"kind":"value","parameter_index":11,"real_valued":false,"source_id":5},{"component":0,"global_component":16,"kind":"value","parameter_index":12,"real_valued":false,"source_id":6},{"component":1,"global_component":17,"kind":"value","parameter_index":13,"real_valued":false,"source_id":6},{"component":2,"global_component":18,"kind":"value","parameter_index":14,"real_valued":false,"source_id":6},{"component":3,"global_component":19,"kind":"value","parameter_index":15,"real_valued":false,"source_id":6},{"component":0,"global_component":36,"kind":"momentum","parameter_index":16,"real_valued":true,"source_id":1},{"component":1,"global_component":37,"kind":"momentum","parameter_index":17,"real_valued":true,"source_id":1},{"component":2,"global_component":38,"kind":"momentum","parameter_index":18,"real_valued":true,"source_id":1},{"component":3,"global_component":39,"kind":"momentum","parameter_index":19,"real_valued":true,"source_id":1},{"component":0,"global_component":40,"kind":"momentum","parameter_index":20,"real_valued":true,"source_id":2},{"component":1,"global_component":41,"kind":"momentum","parameter_index":21,"real_valued":true,"source_id":2},{"component":2,"global_component":42,"kind":"momentum","parameter_index":22,"real_valued":true,"source_id":2},{"component":3,"global_component":43,"kind":"momentum","parameter_index":23,"real_valued":true,"source_id":2},{"component":0,"global_component":44,"kind":"momentum","parameter_index":24,"real_valued":true,"source_id":3},{"component":1,"global_component":45,"kind":"momentum","parameter_index":25,"real_valued":true,"source_id":3},{"component":2,"global_component":46,"kind":"momentum","parameter_index":26,"real_valued":true,"source_id":3},{"component":3,"global_component":47,"kind":"momentum","parameter_index":27,"real_valued":true,"source_id":3},{"component":0,"global_component":58,"kind":"model_parameter","parameter_index":28,"real_valued":true,"source_id":10},{"component":0,"global_component":59,"kind":"model_parameter","parameter_index":29,"real_valued":true,"source_id":11}],"input_value_slot_ids":[2,3,4,5,6],"interaction_ids":[0,1,2,3,4,5],"model_parameter_count":2,"momentum_parameter_count":12,"output_length":12,"output_slots":[{"component_start":20,"component_stop":22,"current_id":7,"output_start":0,"output_stop":2,"value_slot_id":7,"variant":"unpropagated"},{"component_start":22,"component_stop":24,"current_id":8,"output_start":2,"output_stop":4,"value_slot_id":8,"variant":"unpropagated"},{"component_start":24,"component_stop":26,"current_id":9,"output_start":4,"output_stop":6,"value_slot_id":9,"variant":"unpropagated"},{"component_start":26,"component_stop":28,"current_id":10,"output_start":6,"output_stop":8,"value_slot_id":10,"variant":"unpropagated"},{"component_start":28,"component_stop":30,"current_id":11,"output_start":8,"output_stop":10,"value_slot_id":11,"variant":"unpropagated"},{"component_start":30,"component_stop":32,"current_id":12,"output_start":10,"output_stop":12,"value_slot_id":12,"variant":"unpropagated"}],"output_value_slot_ids":[7,8,9,10,11,12],"parameter_count":30,"parameter_layout":"stage-local-value-momentum","real_valued_inputs":[16,17,18,19,20,21,22,23,24,25,26,27,28,29],"stage_index":1,"stage_kind":"current-combine","subset_size":2,"value_parameter_count":16}],"value_parameter_count":0}},"dag_summary":{"amplitude_root_count":6,"current_count":13,"interaction_count":6,"interaction_evaluation_count":6,"source_count":7,"truncated":false},"external_pdg_order":[1,-1,23],"key":"d_dbar_to_z","kind":"pyamplicol-runtime-execution","materialization_census":{"abi":"pyamplicol-fully-resident-materialization-census-v1","basis":"immutable-fully-resident-compiled-dag","final":{"amplitude_root_count":6,"current_count":13,"interaction_count":6,"interaction_evaluation_count":6,"source_count":7},"final_equals_peak":true,"peak":{"amplitude_root_count":6,"current_count":13,"interaction_count":6,"interaction_evaluation_count":6,"source_count":7}},"physics_reduction":{"groups":[{"id":"reduction:0","physical_color_ids":["flow:2,1"],"physical_helicity_ids":["h:+1,-1,-1"],"representative_color_id":"flow:2,1","representative_helicity_id":"h:+1,-1,-1"},{"id":"reduction:1","physical_color_ids":["flow:2,1"],"physical_helicity_ids":["h:+1,-1,+0"],"representative_color_id":"flow:2,1","representative_helicity_id":"h:+1,-1,+0"},{"id":"reduction:2","physical_color_ids":["flow:2,1"],"physical_helicity_ids":["h:+1,-1,+1"],"representative_color_id":"flow:2,1","representative_helicity_id":"h:+1,-1,+1"},{"id":"reduction:3","physical_color_ids":["flow:2,1"],"physical_helicity_ids":["h:-1,+1,-1"],"representative_color_id":"flow:2,1","representative_helicity_id":"h:-1,+1,-1"},{"id":"reduction:4","physical_color_ids":["flow:2,1"],"physical_helicity_ids":["h:-1,+1,+0"],"representative_color_id":"flow:2,1","representative_helicity_id":"h:-1,+1,+0"},{"id":"reduction:5","physical_color_ids":["flow:2,1"],"physical_helicity_ids":["h:-1,+1,+1"],"representative_color_id":"flow:2,1","representative_helicity_id":"h:-1,+1,+1"}],"kind":"lc-diagonal"},"process":"d d~ > z","required_runtime_capabilities":["compiled-plane-arena-v1","symjit.application.complex-f64.v1"],"runtime_schema":{"amplitude_stage":{"color_contraction":null,"color_topology_replay":null,"output_count":6,"roots":[{"all_sector_weight":1.0,"coherent_group_id":0,"color_sector_id":0,"color_weight":[1.0,0.0],"contraction":"weyl","contraction_ir":{"chirality_relation":"opposite","coefficients":[[1.0,0.0],[1.0,0.0]],"left_basis":"weyl-chiral","metric_signature":null,"name":"weyl","right_basis":"weyl-chiral"},"coupling":[1.0,0.0],"helicity_weight":1.0,"kind":"direct-contraction","left_current_id":7,"left_slot":{"component_start":20,"component_stop":22,"current_id":7,"dimension":2},"left_value_slot":{"component_start":20,"component_stop":22,"current_id":7,"dimension":2,"value_slot_id":7,"variant":"unpropagated"},"output_index":0,"right_current_id":1,"right_slot":{"component_start":2,"component_stop":4,"current_id":1,"dimension":2},"right_value_slot":{"component_start":2,"component_stop":4,"current_id":1,"dimension":2,"value_slot_id":1,"variant":"source"},"root_id":0,"vertex_kind":null,"vertex_particles":null},{"all_sector_weight":1.0,"coherent_group_id":1,"color_sector_id":0,"color_weight":[1.0,0.0],"contraction":"weyl","contraction_ir":{"chirality_relation":"opposite","coefficients":[[1.0,0.0],[1.0,0.0]],"left_basis":"weyl-chiral","metric_signature":null,"name":"weyl","right_basis":"weyl-chiral"},"coupling":[1.0,0.0],"helicity_weight":1.0,"kind":"direct-contraction","left_current_id":8,"left_slot":{"component_start":22,"component_stop":24,"current_id":8,"dimension":2},"left_value_slot":{"component_start":22,"component_stop":24,"current_id":8,"dimension":2,"value_slot_id":8,"variant":"unpropagated"},"output_index":1,"right_current_id":1,"right_slot":{"component_start":2,"component_stop":4,"current_id":1,"dimension":2},"right_value_slot":{"component_start":2,"component_stop":4,"current_id":1,"dimension":2,"value_slot_id":1,"variant":"source"},"root_id":1,"vertex_kind":null,"vertex_particles":null},{"all_sector_weight":1.0,"coherent_group_id":2,"color_sector_id":0,"color_weight":[1.0,0.0],"contraction":"weyl","contraction_ir":{"chirality_relation":"opposite","coefficients":[[1.0,0.0],[1.0,0.0]],"left_basis":"weyl-chiral","metric_signature":null,"name":"weyl","right_basis":"weyl-chiral"},"coupling":[1.0,0.0],"helicity_weight":1.0,"kind":"direct-contraction","left_current_id":9,"left_slot":{"component_start":24,"component_stop":26,"current_id":9,"dimension":2},"left_value_slot":{"component_start":24,"component_stop":26,"current_id":9,"dimension":2,"value_slot_id":9,"variant":"unpropagated"},"output_index":2,"right_current_id":1,"right_slot":{"component_start":2,"component_stop":4,"current_id":1,"dimension":2},"right_value_slot":{"component_start":2,"component_stop":4,"current_id":1,"dimension":2,"value_slot_id":1,"variant":"source"},"root_id":2,"vertex_kind":null,"vertex_particles":null},{"all_sector_weight":1.0,"coherent_group_id":3,"color_sector_id":0,"color_weight":[1.0,0.0],"contraction":"weyl","contraction_ir":{"chirality_relation":"opposite","coefficients":[[1.0,0.0],[1.0,0.0]],"left_basis":"weyl-chiral","metric_signature":null,"name":"weyl","right_basis":"weyl-chiral"},"coupling":[1.0,0.0],"helicity_weight":1.0,"kind":"direct-contraction","left_current_id":10,"left_slot":{"component_start":26,"component_stop":28,"current_id":10,"dimension":2},"left_value_slot":{"component_start":26,"component_stop":28,"current_id":10,"dimension":2,"value_slot_id":10,"variant":"unpropagated"},"output_index":3,"right_current_id":0,"right_slot":{"component_start":0,"component_stop":2,"current_id":0,"dimension":2},"right_value_slot":{"component_start":0,"component_stop":2,"current_id":0,"dimension":2,"value_slot_id":0,"variant":"source"},"root_id":3,"vertex_kind":null,"vertex_particles":null},{"all_sector_weight":1.0,"coherent_group_id":4,"color_sector_id":0,"color_weight":[1.0,0.0],"contraction":"weyl","contraction_ir":{"chirality_relation":"opposite","coefficients":[[1.0,0.0],[1.0,0.0]],"left_basis":"weyl-chiral","metric_signature":null,"name":"weyl","right_basis":"weyl-chiral"},"coupling":[1.0,0.0],"helicity_weight":1.0,"kind":"direct-contraction","left_current_id":11,"left_slot":{"component_start":28,"component_stop":30,"current_id":11,"dimension":2},"left_value_slot":{"component_start":28,"component_stop":30,"current_id":11,"dimension":2,"value_slot_id":11,"variant":"unpropagated"},"output_index":4,"right_current_id":0,"right_slot":{"component_start":0,"component_stop":2,"current_id":0,"dimension":2},"right_value_slot":{"component_start":0,"component_stop":2,"current_id":0,"dimension":2,"value_slot_id":0,"variant":"source"},"root_id":4,"vertex_kind":null,"vertex_particles":null},{"all_sector_weight":1.0,"coherent_group_id":5,"color_sector_id":0,"color_weight":[1.0,0.0],"contraction":"weyl","contraction_ir":{"chirality_relation":"opposite","coefficients":[[1.0,0.0],[1.0,0.0]],"left_basis":"weyl-chiral","metric_signature":null,"name":"weyl","right_basis":"weyl-chiral"},"coupling":[1.0,0.0],"helicity_weight":1.0,"kind":"direct-contraction","left_current_id":12,"left_slot":{"component_start":30,"component_stop":32,"current_id":12,"dimension":2},"left_value_slot":{"component_start":30,"component_stop":32,"current_id":12,"dimension":2,"value_slot_id":12,"variant":"unpropagated"},"output_index":5,"right_current_id":0,"right_slot":{"component_start":0,"component_stop":2,"current_id":0,"dimension":2},"right_value_slot":{"component_start":0,"component_stop":2,"current_id":0,"dimension":2,"value_slot_id":0,"variant":"source"},"root_id":5,"vertex_kind":null,"vertex_particles":null}],"stage_kind":"amplitude-roots"},"current_storage":{"component_count":32,"current_slots":[{"auxiliary_kind":null,"chirality":1,"color_state":{"accuracy":"lc","basis_key":[],"line_groups":[0],"sector_id":0},"component_start":0,"component_stop":2,"current_id":0,"dimension":2,"external_labels":[1],"external_mask":1,"flavour_flow":[-1],"helicity_ancestry":"0x1","is_source":true,"momentum_mask":1,"particle_id":-1,"spin_state":1},{"auxiliary_kind":null,"chirality":-1,"color_state":{"accuracy":"lc","basis_key":[],"line_groups":[0],"sector_id":0},"component_start":2,"component_stop":4,"current_id":1,"dimension":2,"external_labels":[1],"external_mask":1,"flavour_flow":[-1],"helicity_ancestry":"0x2","is_source":true,"momentum_mask":1,"particle_id":-1,"spin_state":-1},{"auxiliary_kind":null,"chirality":1,"color_state":{"accuracy":"lc","basis_key":[],"line_groups":[0],"sector_id":0},"component_start":4,"component_stop":6,"current_id":2,"dimension":2,"external_labels":[2],"external_mask":2,"flavour_flow":[1],"helicity_ancestry":"0x4","is_source":true,"momentum_mask":2,"particle_id":1,"spin_state":1},{"auxiliary_kind":null,"chirality":-1,"color_state":{"accuracy":"lc","basis_key":[],"line_groups":[0],"sector_id":0},"component_start":6,"component_stop":8,"current_id":3,"dimension":2,"external_labels":[2],"external_mask":2,"flavour_flow":[1],"helicity_ancestry":"0x8","is_source":true,"momentum_mask":2,"particle_id":1,"spin_state":-1},{"auxiliary_kind":null,"chirality":0,"color_state":{"accuracy":"lc","basis_key":[],"line_groups":[],"sector_id":0},"component_start":8,"component_stop":12,"current_id":4,"dimension":4,"external_labels":[3],"external_mask":4,"flavour_flow":[23],"helicity_ancestry":"0x10","is_source":true,"momentum_mask":4,"particle_id":23,"spin_state":-1},{"auxiliary_kind":null,"chirality":0,"color_state":{"accuracy":"lc","basis_key":[],"line_groups":[],"sector_id":0},"component_start":12,"component_stop":16,"current_id":5,"dimension":4,"external_labels":[3],"external_mask":4,"flavour_flow":[23],"helicity_ancestry":"0x20","is_source":true,"momentum_mask":4,"particle_id":23,"spin_state":0},{"auxiliary_kind":null,"chirality":0,"color_state":{"accuracy":"lc","basis_key":[],"line_groups":[],"sector_id":0},"component_start":16,"component_stop":20,"current_id":6,"dimension":4,"external_labels":[3],"external_mask":4,"flavour_flow":[23],"helicity_ancestry":"0x40","is_source":true,"momentum_mask":4,"particle_id":23,"spin_state":1},{"auxiliary_kind":null,"chirality":1,"color_state":{"accuracy":"lc","basis_key":[],"line_groups":[0],"sector_id":0},"component_start":20,"component_stop":22,"current_id":7,"dimension":2,"external_labels":[2,3],"external_mask":6,"flavour_flow":[1],"helicity_ancestry":"0x14","is_source":false,"momentum_mask":6,"particle_id":1,"spin_state":1},{"auxiliary_kind":null,"chirality":1,"color_state":{"accuracy":"lc","basis_key":[],"line_groups":[0],"sector_id":0},"component_start":22,"component_stop":24,"current_id":8,"dimension":2,"external_labels":[2,3],"external_mask":6,"flavour_flow":[1],"helicity_ancestry":"0x24","is_source":false,"momentum_mask":6,"particle_id":1,"spin_state":1},{"auxiliary_kind":null,"chirality":1,"color_state":{"accuracy":"lc","basis_key":[],"line_groups":[0],"sector_id":0},"component_start":24,"component_stop":26,"current_id":9,"dimension":2,"external_labels":[2,3],"external_mask":6,"flavour_flow":[1],"helicity_ancestry":"0x44","is_source":false,"momentum_mask":6,"particle_id":1,"spin_state":1},{"auxiliary_kind":null,"chirality":-1,"color_state":{"accuracy":"lc","basis_key":[],"line_groups":[0],"sector_id":0},"component_start":26,"component_stop":28,"current_id":10,"dimension":2,"external_labels":[2,3],"external_mask":6,"flavour_flow":[1],"helicity_ancestry":"0x18","is_source":false,"momentum_mask":6,"particle_id":1,"spin_state":-1},{"auxiliary_kind":null,"chirality":-1,"color_state":{"accuracy":"lc","basis_key":[],"line_groups":[0],"sector_id":0},"component_start":28,"component_stop":30,"current_id":11,"dimension":2,"external_labels":[2,3],"external_mask":6,"flavour_flow":[1],"helicity_ancestry":"0x28","is_source":false,"momentum_mask":6,"particle_id":1,"spin_state":-1},{"auxiliary_kind":null,"chirality":-1,"color_state":{"accuracy":"lc","basis_key":[],"line_groups":[0],"sector_id":0},"component_start":30,"component_stop":32,"current_id":12,"dimension":2,"external_labels":[2,3],"external_mask":6,"flavour_flow":[1],"helicity_ancestry":"0x48","is_source":false,"momentum_mask":6,"particle_id":1,"spin_state":-1}],"metadata_compacted":true,"number_type":"complex"},"external_particles":[{"index":0,"label":1,"momentum_slot":0,"outgoing_pdg":-1,"pdg":1,"role":"initial"},{"index":1,"label":2,"momentum_slot":1,"outgoing_pdg":1,"pdg":-1,"role":"initial"},{"index":2,"label":3,"momentum_slot":2,"outgoing_pdg":23,"pdg":23,"role":"final"}],"kind":"pyamplicol-runtime-execution-plan","model":{"particles":[{"mass":0.0,"mass_parameter":null,"pdg":1},{"mass":91.188,"mass_parameter":"particle.23.mass","pdg":23}]},"model_parameters":[{"default":0.0075467711139788835,"kind":"normalization","name":"normalization.alpha_ew","parameter_index":0},{"default":0.118,"kind":"normalization","name":"normalization.alpha_s_me_check","parameter_index":1},{"default":173.0,"kind":"particle_mass","name":"particle.6.mass","parameter_index":2,"pdg":6},{"default":1.4915,"kind":"particle_width","name":"particle.6.width","parameter_index":3,"pdg":6},{"default":91.188,"kind":"particle_mass","name":"particle.23.mass","parameter_index":4,"pdg":23},{"default":2.441404,"kind":"particle_width","name":"particle.23.width","parameter_index":5,"pdg":23},{"default":80.41900244575616,"kind":"particle_mass","name":"particle.24.mass","parameter_index":6,"pdg":24},{"default":2.0476,"kind":"particle_width","name":"particle.24.width","parameter_index":7,"pdg":24},{"default":125.0,"kind":"particle_mass","name":"particle.25.mass","parameter_index":8,"pdg":25},{"default":0.006382339,"kind":"particle_width","name":"particle.25.width","parameter_index":9,"pdg":25},{"default":-1.0244420275940371,"kind":"coupling_component","name":"coupling.10.1_23_1.component_0","parameter_index":10},{"default":0.17818666745287456,"kind":"coupling_component","name":"coupling.10.1_23_1.component_1","parameter_index":11}],"momentum_slots":[{"component_start":0,"component_stop":4,"external_labels":[1],"momentum_mask":1,"momentum_slot_id":0,"real_valued":true},{"component_start":4,"component_stop":8,"external_labels":[2],"momentum_mask":2,"momentum_slot_id":1,"real_valued":true},{"component_start":8,"component_stop":12,"external_labels":[3],"momentum_mask":4,"momentum_slot_id":2,"real_valued":true},{"component_start":12,"component_stop":16,"external_labels":[2,3],"momentum_mask":6,"momentum_slot_id":3,"real_valued":true}],"normalization":{"average_factor":36.0,"color_factor":3.0,"electroweak_coupling_power":1,"global_coupling_factor":0.18967104551999775,"identical_factor":1.0,"qcd_coupling_power":0},"parameter_layout":{"model_parameter_count":12,"momentum_components_real":true,"momentum_parameter_count":16,"parameter_count_if_flattened":48,"real_valued_inputs":[20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47],"source_component_parameter_count":20,"source_components_complex":true,"value_component_count":32},"process":"d d~ > z","process_key":"d_dbar_to_z","schema_version":3,"source_fill":{"source_count":7,"sources":[{"anti_particle_id":1,"applied_crossing":{"chirality_factor":-1,"helicity_factor":1,"momentum_transform":"negate-four-momentum","phase":[1.0,0.0],"spin_state_factor":-1},"chirality":1,"color_state":{"accuracy":"lc","basis_key":[],"line_groups":[0],"sector_id":0},"crossing":"negate-incoming-momentum","current_component_start":0,"current_component_stop":2,"current_id":0,"dimension":2,"helicity_ancestry":"0x1","input_momentum_slot":0,"leg_label":1,"outgoing_pdg":-1,"particle_id":-1,"physical_pdg":1,"side":"initial","source_basis":"weyl-chiral","source_helicity":-1,"source_id":0,"source_ir":{"basis":"weyl-chiral","component_dimension":2,"crossing":{"chirality_factor":-1,"helicity_factor":1,"momentum_transform":"negate-four-momentum","phase":[1.0,0.0],"spin_state_factor":-1},"identity":{"anti_canonical_id":"model:built-in-sm-leading-color:state:d","anti_display_name":"d","anti_pdg_label":1,"canonical_id":"model:built-in-sm-leading-color:state:d~","display_name":"d~","orientation":"antiparticle","pdg_label":-1,"self_conjugate":false,"species_id":"model:built-in-sm-leading-color:species:d"},"mass_parameter":null,"states":[{"chirality":-1,"helicity":-1,"spin_state":-1},{"chirality":1,"helicity":1,"spin_state":1}],"statistics":"fermion","wavefunction_family":"fermion","width_parameter":null},"source_kind":"external-wavefunction","source_orientation":"antiparticle","source_parameter_start":0,"source_parameter_stop":2,"spin_state":1,"value_slot":{"component_start":0,"component_stop":2,"current_id":0,"dimension":2,"value_slot_id":0,"variant":"source"},"wavefunction_kind":"fermion"},{"anti_particle_id":1,"applied_crossing":{"chirality_factor":-1,"helicity_factor":1,"momentum_transform":"negate-four-momentum","phase":[1.0,0.0],"spin_state_factor":-1},"chirality":-1,"color_state":{"accuracy":"lc","basis_key":[],"line_groups":[0],"sector_id":0},"crossing":"negate-incoming-momentum","current_component_start":2,"current_component_stop":4,"current_id":1,"dimension":2,"helicity_ancestry":"0x2","input_momentum_slot":0,"leg_label":1,"outgoing_pdg":-1,"particle_id":-1,"physical_pdg":1,"side":"initial","source_basis":"weyl-chiral","source_helicity":1,"source_id":1,"source_ir":{"basis":"weyl-chiral","component_dimension":2,"crossing":{"chirality_factor":-1,"helicity_factor":1,"momentum_transform":"negate-four-momentum","phase":[1.0,0.0],"spin_state_factor":-1},"identity":{"anti_canonical_id":"model:built-in-sm-leading-color:state:d","anti_display_name":"d","anti_pdg_label":1,"canonical_id":"model:built-in-sm-leading-color:state:d~","display_name":"d~","orientation":"antiparticle","pdg_label":-1,"self_conjugate":false,"species_id":"model:built-in-sm-leading-color:species:d"},"mass_parameter":null,"states":[{"chirality":-1,"helicity":-1,"spin_state":-1},{"chirality":1,"helicity":1,"spin_state":1}],"statistics":"fermion","wavefunction_family":"fermion","width_parameter":null},"source_kind":"external-wavefunction","source_orientation":"antiparticle","source_parameter_start":2,"source_parameter_stop":4,"spin_state":-1,"value_slot":{"component_start":2,"component_stop":4,"current_id":1,"dimension":2,"value_slot_id":1,"variant":"source"},"wavefunction_kind":"fermion"},{"anti_particle_id":-1,"applied_crossing":{"chirality_factor":-1,"helicity_factor":1,"momentum_transform":"negate-four-momentum","phase":[1.0,0.0],"spin_state_factor":-1},"chirality":1,"color_state":{"accuracy":"lc","basis_key":[],"line_groups":[0],"sector_id":0},"crossing":"negate-incoming-momentum","current_component_start":4,"current_component_stop":6,"current_id":2,"dimension":2,"helicity_ancestry":"0x4","input_momentum_slot":1,"leg_label":2,"outgoing_pdg":1,"particle_id":1,"physical_pdg":-1,"side":"initial","source_basis":"weyl-chiral","source_helicity":-1,"source_id":2,"source_ir":{"basis":"weyl-chiral","component_dimension":2,"crossing":{"chirality_factor":-1,"helicity_factor":1,"momentum_transform":"negate-four-momentum","phase":[1.0,0.0],"spin_state_factor":-1},"identity":{"anti_canonical_id":"model:built-in-sm-leading-color:state:d~","anti_display_name":"d~","anti_pdg_label":-1,"canonical_id":"model:built-in-sm-leading-color:state:d","display_name":"d","orientation":"particle","pdg_label":1,"self_conjugate":false,"species_id":"model:built-in-sm-leading-color:species:d"},"mass_parameter":null,"states":[{"chirality":-1,"helicity":-1,"spin_state":-1},{"chirality":1,"helicity":1,"spin_state":1}],"statistics":"fermion","wavefunction_family":"fermion","width_parameter":null},"source_kind":"external-wavefunction","source_orientation":"particle","source_parameter_start":4,"source_parameter_stop":6,"spin_state":1,"value_slot":{"component_start":4,"component_stop":6,"current_id":2,"dimension":2,"value_slot_id":2,"variant":"source"},"wavefunction_kind":"fermion"},{"anti_particle_id":-1,"applied_crossing":{"chirality_factor":-1,"helicity_factor":1,"momentum_transform":"negate-four-momentum","phase":[1.0,0.0],"spin_state_factor":-1},"chirality":-1,"color_state":{"accuracy":"lc","basis_key":[],"line_groups":[0],"sector_id":0},"crossing":"negate-incoming-momentum","current_component_start":6,"current_component_stop":8,"current_id":3,"dimension":2,"helicity_ancestry":"0x8","input_momentum_slot":1,"leg_label":2,"outgoing_pdg":1,"particle_id":1,"physical_pdg":-1,"side":"initial","source_basis":"weyl-chiral","source_helicity":1,"source_id":3,"source_ir":{"basis":"weyl-chiral","component_dimension":2,"crossing":{"chirality_factor":-1,"helicity_factor":1,"momentum_transform":"negate-four-momentum","phase":[1.0,0.0],"spin_state_factor":-1},"identity":{"anti_canonical_id":"model:built-in-sm-leading-color:state:d~","anti_display_name":"d~","anti_pdg_label":-1,"canonical_id":"model:built-in-sm-leading-color:state:d","display_name":"d","orientation":"particle","pdg_label":1,"self_conjugate":false,"species_id":"model:built-in-sm-leading-color:species:d"},"mass_parameter":null,"states":[{"chirality":-1,"helicity":-1,"spin_state":-1},{"chirality":1,"helicity":1,"spin_state":1}],"statistics":"fermion","wavefunction_family":"fermion","width_parameter":null},"source_kind":"external-wavefunction","source_orientation":"particle","source_parameter_start":6,"source_parameter_stop":8,"spin_state":-1,"value_slot":{"component_start":6,"component_stop":8,"current_id":3,"dimension":2,"value_slot_id":3,"variant":"source"},"wavefunction_kind":"fermion"},{"anti_particle_id":23,"applied_crossing":{"chirality_factor":1,"helicity_factor":1,"momentum_transform":"identity","phase":[1.0,0.0],"spin_state_factor":1},"chirality":0,"color_state":{"accuracy":"lc","basis_key":[],"line_groups":[],"sector_id":0},"crossing":"identity","current_component_start":8,"current_component_stop":12,"current_id":4,"dimension":4,"helicity_ancestry":"0x10","input_momentum_slot":2,"leg_label":3,"outgoing_pdg":23,"particle_id":23,"physical_pdg":23,"side":"final","source_basis":"lorentz-vector","source_helicity":-1,"source_id":4,"source_ir":{"basis":"lorentz-vector","component_dimension":4,"crossing":{"chirality_factor":1,"helicity_factor":-1,"momentum_transform":"negate-four-momentum","phase":[1.0,0.0],"spin_state_factor":-1},"identity":{"anti_canonical_id":"model:built-in-sm-leading-color:state:z","anti_display_name":"z","anti_pdg_label":23,"canonical_id":"model:built-in-sm-leading-color:state:z","display_name":"z","orientation":"self-conjugate","pdg_label":23,"self_conjugate":true,"species_id":"model:built-in-sm-leading-color:species:z"},"mass_parameter":null,"states":[{"chirality":0,"helicity":-1,"spin_state":-1},{"chirality":0,"helicity":0,"spin_state":0},{"chirality":0,"helicity":1,"spin_state":1}],"statistics":"boson","wavefunction_family":"vector","width_parameter":null},"source_kind":"external-wavefunction","source_orientation":"self-conjugate","source_parameter_start":8,"source_parameter_stop":12,"spin_state":-1,"value_slot":{"component_start":8,"component_stop":12,"current_id":4,"dimension":4,"value_slot_id":4,"variant":"source"},"wavefunction_kind":"vector"},{"anti_particle_id":23,"applied_crossing":{"chirality_factor":1,"helicity_factor":1,"momentum_transform":"identity","phase":[1.0,0.0],"spin_state_factor":1},"chirality":0,"color_state":{"accuracy":"lc","basis_key":[],"line_groups":[],"sector_id":0},"crossing":"identity","current_component_start":12,"current_component_stop":16,"current_id":5,"dimension":4,"helicity_ancestry":"0x20","input_momentum_slot":2,"leg_label":3,"outgoing_pdg":23,"particle_id":23,"physical_pdg":23,"side":"final","source_basis":"lorentz-vector","source_helicity":0,"source_id":5,"source_ir":{"basis":"lorentz-vector","component_dimension":4,"crossing":{"chirality_factor":1,"helicity_factor":-1,"momentum_transform":"negate-four-momentum","phase":[1.0,0.0],"spin_state_factor":-1},"identity":{"anti_canonical_id":"model:built-in-sm-leading-color:state:z","anti_display_name":"z","anti_pdg_label":23,"canonical_id":"model:built-in-sm-leading-color:state:z","display_name":"z","orientation":"self-conjugate","pdg_label":23,"self_conjugate":true,"species_id":"model:built-in-sm-leading-color:species:z"},"mass_parameter":null,"states":[{"chirality":0,"helicity":-1,"spin_state":-1},{"chirality":0,"helicity":0,"spin_state":0},{"chirality":0,"helicity":1,"spin_state":1}],"statistics":"boson","wavefunction_family":"vector","width_parameter":null},"source_kind":"external-wavefunction","source_orientation":"self-conjugate","source_parameter_start":12,"source_parameter_stop":16,"spin_state":0,"value_slot":{"component_start":12,"component_stop":16,"current_id":5,"dimension":4,"value_slot_id":5,"variant":"source"},"wavefunction_kind":"vector"},{"anti_particle_id":23,"applied_crossing":{"chirality_factor":1,"helicity_factor":1,"momentum_transform":"identity","phase":[1.0,0.0],"spin_state_factor":1},"chirality":0,"color_state":{"accuracy":"lc","basis_key":[],"line_groups":[],"sector_id":0},"crossing":"identity","current_component_start":16,"current_component_stop":20,"current_id":6,"dimension":4,"helicity_ancestry":"0x40","input_momentum_slot":2,"leg_label":3,"outgoing_pdg":23,"particle_id":23,"physical_pdg":23,"side":"final","source_basis":"lorentz-vector","source_helicity":1,"source_id":6,"source_ir":{"basis":"lorentz-vector","component_dimension":4,"crossing":{"chirality_factor":1,"helicity_factor":-1,"momentum_transform":"negate-four-momentum","phase":[1.0,0.0],"spin_state_factor":-1},"identity":{"anti_canonical_id":"model:built-in-sm-leading-color:state:z","anti_display_name":"z","anti_pdg_label":23,"canonical_id":"model:built-in-sm-leading-color:state:z","display_name":"z","orientation":"self-conjugate","pdg_label":23,"self_conjugate":true,"species_id":"model:built-in-sm-leading-color:species:z"},"mass_parameter":null,"states":[{"chirality":0,"helicity":-1,"spin_state":-1},{"chirality":0,"helicity":0,"spin_state":0},{"chirality":0,"helicity":1,"spin_state":1}],"statistics":"boson","wavefunction_family":"vector","width_parameter":null},"source_kind":"external-wavefunction","source_orientation":"self-conjugate","source_parameter_start":16,"source_parameter_stop":20,"spin_state":1,"value_slot":{"component_start":16,"component_stop":20,"current_id":6,"dimension":4,"value_slot_id":6,"variant":"source"},"wavefunction_kind":"vector"}]},"stages":[{"input_current_ids":[2,3,4,5,6],"input_value_slot_ids":[2,3,4,5,6],"interaction_count":6,"interaction_ids":[0,1,2,3,4,5],"interactions":[],"interactions_compacted":true,"output_current_ids":[7,8,9,10,11,12],"output_value_slot_ids":[7,8,9,10,11,12],"stage_index":1,"stage_kind":"current-combine","subset_size":2}],"value_storage":{"component_count":32,"metadata_compacted":true,"number_type":"complex","value_slots":[{"applies_propagator":false,"chirality":1,"component_start":0,"component_stop":2,"current_component_start":0,"current_component_stop":2,"current_id":0,"dimension":2,"external_labels":[1],"external_mask":1,"is_source":true,"momentum_mask":1,"particle_id":-1,"propagator":{"applies_propagator":true,"auxiliary_policy":null,"backend":"spenso","basis":"weyl-chiral","chirality":1,"custom_source":null,"denominator":"momentum_squared","description":"massless Weyl fermion propagator","full_tensor_network_ready":true,"gauge":null,"goldstone_policy":"not-applicable","identity":{"anti_canonical_id":"model:built-in-sm-leading-color:state:d","anti_display_name":"d","anti_pdg_label":1,"canonical_id":"model:built-in-sm-leading-color:state:d~","display_name":"d~","orientation":"antiparticle","pdg_label":-1,"self_conjugate":false,"species_id":"model:built-in-sm-leading-color:species:d"},"kernel":"weyl_fermion","kind":"weyl-fermion","mass_class":"massless","mass_parameter":null,"numerator":"i*weyl_slash(momentum)","particle_id":-1,"width_parameter":null},"value_slot_id":0,"variant":"source"},{"applies_propagator":false,"chirality":-1,"component_start":2,"component_stop":4,"current_component_start":2,"current_component_stop":4,"current_id":1,"dimension":2,"external_labels":[1],"external_mask":1,"is_source":true,"momentum_mask":1,"particle_id":-1,"propagator":{"applies_propagator":true,"auxiliary_policy":null,"backend":"spenso","basis":"weyl-chiral","chirality":-1,"custom_source":null,"denominator":"momentum_squared","description":"massless Weyl fermion propagator","full_tensor_network_ready":true,"gauge":null,"goldstone_policy":"not-applicable","identity":{"anti_canonical_id":"model:built-in-sm-leading-color:state:d","anti_display_name":"d","anti_pdg_label":1,"canonical_id":"model:built-in-sm-leading-color:state:d~","display_name":"d~","orientation":"antiparticle","pdg_label":-1,"self_conjugate":false,"species_id":"model:built-in-sm-leading-color:species:d"},"kernel":"weyl_fermion","kind":"weyl-fermion","mass_class":"massless","mass_parameter":null,"numerator":"i*weyl_slash(momentum)","particle_id":-1,"width_parameter":null},"value_slot_id":1,"variant":"source"},{"applies_propagator":false,"chirality":1,"component_start":4,"component_stop":6,"current_component_start":4,"current_component_stop":6,"current_id":2,"dimension":2,"external_labels":[2],"external_mask":2,"is_source":true,"momentum_mask":2,"particle_id":1,"propagator":{"applies_propagator":true,"auxiliary_policy":null,"backend":"spenso","basis":"weyl-chiral","chirality":1,"custom_source":null,"denominator":"momentum_squared","description":"massless Weyl fermion propagator","full_tensor_network_ready":true,"gauge":null,"goldstone_policy":"not-applicable","identity":{"anti_canonical_id":"model:built-in-sm-leading-color:state:d~","anti_display_name":"d~","anti_pdg_label":-1,"canonical_id":"model:built-in-sm-leading-color:state:d","display_name":"d","orientation":"particle","pdg_label":1,"self_conjugate":false,"species_id":"model:built-in-sm-leading-color:species:d"},"kernel":"weyl_fermion","kind":"weyl-fermion","mass_class":"massless","mass_parameter":null,"numerator":"i*weyl_slash(momentum)","particle_id":1,"width_parameter":null},"value_slot_id":2,"variant":"source"},{"applies_propagator":false,"chirality":-1,"component_start":6,"component_stop":8,"current_component_start":6,"current_component_stop":8,"current_id":3,"dimension":2,"external_labels":[2],"external_mask":2,"is_source":true,"momentum_mask":2,"particle_id":1,"propagator":{"applies_propagator":true,"auxiliary_policy":null,"backend":"spenso","basis":"weyl-chiral","chirality":-1,"custom_source":null,"denominator":"momentum_squared","description":"massless Weyl fermion propagator","full_tensor_network_ready":true,"gauge":null,"goldstone_policy":"not-applicable","identity":{"anti_canonical_id":"model:built-in-sm-leading-color:state:d~","anti_display_name":"d~","anti_pdg_label":-1,"canonical_id":"model:built-in-sm-leading-color:state:d","display_name":"d","orientation":"particle","pdg_label":1,"self_conjugate":false,"species_id":"model:built-in-sm-leading-color:species:d"},"kernel":"weyl_fermion","kind":"weyl-fermion","mass_class":"massless","mass_parameter":null,"numerator":"i*weyl_slash(momentum)","particle_id":1,"width_parameter":null},"value_slot_id":3,"variant":"source"},{"applies_propagator":false,"chirality":0,"component_start":8,"component_stop":12,"current_component_start":8,"current_component_stop":12,"current_id":4,"dimension":4,"external_labels":[3],"external_mask":4,"is_source":true,"momentum_mask":4,"particle_id":23,"propagator":{"applies_propagator":true,"auxiliary_policy":null,"backend":"symbolica","basis":"lorentz-vector","chirality":0,"custom_source":null,"denominator":"momentum_squared-mass_squared+i*mass*width","description":"massive vector propagator with width","full_tensor_network_ready":true,"gauge":"unitary","goldstone_policy":"absorbed","identity":{"anti_canonical_id":"model:built-in-sm-leading-color:state:z","anti_display_name":"z","anti_pdg_label":23,"canonical_id":"model:built-in-sm-leading-color:state:z","display_name":"z","orientation":"self-conjugate","pdg_label":23,"self_conjugate":true,"species_id":"model:built-in-sm-leading-color:species:z"},"kernel":"massive_vector_unitary_gauge","kind":"vector","mass_class":"massive","mass_parameter":null,"numerator":"-i*(metric-momentum_outer/mass_squared)","particle_id":23,"width_parameter":null},"value_slot_id":4,"variant":"source"},{"applies_propagator":false,"chirality":0,"component_start":12,"component_stop":16,"current_component_start":12,"current_component_stop":16,"current_id":5,"dimension":4,"external_labels":[3],"external_mask":4,"is_source":true,"momentum_mask":4,"particle_id":23,"propagator":{"applies_propagator":true,"auxiliary_policy":null,"backend":"symbolica","basis":"lorentz-vector","chirality":0,"custom_source":null,"denominator":"momentum_squared-mass_squared+i*mass*width","description":"massive vector propagator with width","full_tensor_network_ready":true,"gauge":"unitary","goldstone_policy":"absorbed","identity":{"anti_canonical_id":"model:built-in-sm-leading-color:state:z","anti_display_name":"z","anti_pdg_label":23,"canonical_id":"model:built-in-sm-leading-color:state:z","display_name":"z","orientation":"self-conjugate","pdg_label":23,"self_conjugate":true,"species_id":"model:built-in-sm-leading-color:species:z"},"kernel":"massive_vector_unitary_gauge","kind":"vector","mass_class":"massive","mass_parameter":null,"numerator":"-i*(metric-momentum_outer/mass_squared)","particle_id":23,"width_parameter":null},"value_slot_id":5,"variant":"source"},{"applies_propagator":false,"chirality":0,"component_start":16,"component_stop":20,"current_component_start":16,"current_component_stop":20,"current_id":6,"dimension":4,"external_labels":[3],"external_mask":4,"is_source":true,"momentum_mask":4,"particle_id":23,"propagator":{"applies_propagator":true,"auxiliary_policy":null,"backend":"symbolica","basis":"lorentz-vector","chirality":0,"custom_source":null,"denominator":"momentum_squared-mass_squared+i*mass*width","description":"massive vector propagator with width","full_tensor_network_ready":true,"gauge":"unitary","goldstone_policy":"absorbed","identity":{"anti_canonical_id":"model:built-in-sm-leading-color:state:z","anti_display_name":"z","anti_pdg_label":23,"canonical_id":"model:built-in-sm-leading-color:state:z","display_name":"z","orientation":"self-conjugate","pdg_label":23,"self_conjugate":true,"species_id":"model:built-in-sm-leading-color:species:z"},"kernel":"massive_vector_unitary_gauge","kind":"vector","mass_class":"massive","mass_parameter":null,"numerator":"-i*(metric-momentum_outer/mass_squared)","particle_id":23,"width_parameter":null},"value_slot_id":6,"variant":"source"},{"applies_propagator":false,"chirality":1,"component_start":20,"component_stop":22,"current_component_start":20,"current_component_stop":22,"current_id":7,"dimension":2,"external_labels":[2,3],"external_mask":6,"is_source":false,"momentum_mask":6,"particle_id":1,"propagator":{"applies_propagator":true,"auxiliary_policy":null,"backend":"spenso","basis":"weyl-chiral","chirality":1,"custom_source":null,"denominator":"momentum_squared","description":"massless Weyl fermion propagator","full_tensor_network_ready":true,"gauge":null,"goldstone_policy":"not-applicable","identity":{"anti_canonical_id":"model:built-in-sm-leading-color:state:d~","anti_display_name":"d~","anti_pdg_label":-1,"canonical_id":"model:built-in-sm-leading-color:state:d","display_name":"d","orientation":"particle","pdg_label":1,"self_conjugate":false,"species_id":"model:built-in-sm-leading-color:species:d"},"kernel":"weyl_fermion","kind":"weyl-fermion","mass_class":"massless","mass_parameter":null,"numerator":"i*weyl_slash(momentum)","particle_id":1,"width_parameter":null},"value_slot_id":7,"variant":"unpropagated"},{"applies_propagator":false,"chirality":1,"component_start":22,"component_stop":24,"current_component_start":22,"current_component_stop":24,"current_id":8,"dimension":2,"external_labels":[2,3],"external_mask":6,"is_source":false,"momentum_mask":6,"particle_id":1,"propagator":{"applies_propagator":true,"auxiliary_policy":null,"backend":"spenso","basis":"weyl-chiral","chirality":1,"custom_source":null,"denominator":"momentum_squared","description":"massless Weyl fermion propagator","full_tensor_network_ready":true,"gauge":null,"goldstone_policy":"not-applicable","identity":{"anti_canonical_id":"model:built-in-sm-leading-color:state:d~","anti_display_name":"d~","anti_pdg_label":-1,"canonical_id":"model:built-in-sm-leading-color:state:d","display_name":"d","orientation":"particle","pdg_label":1,"self_conjugate":false,"species_id":"model:built-in-sm-leading-color:species:d"},"kernel":"weyl_fermion","kind":"weyl-fermion","mass_class":"massless","mass_parameter":null,"numerator":"i*weyl_slash(momentum)","particle_id":1,"width_parameter":null},"value_slot_id":8,"variant":"unpropagated"},{"applies_propagator":false,"chirality":1,"component_start":24,"component_stop":26,"current_component_start":24,"current_component_stop":26,"current_id":9,"dimension":2,"external_labels":[2,3],"external_mask":6,"is_source":false,"momentum_mask":6,"particle_id":1,"propagator":{"applies_propagator":true,"auxiliary_policy":null,"backend":"spenso","basis":"weyl-chiral","chirality":1,"custom_source":null,"denominator":"momentum_squared","description":"massless Weyl fermion propagator","full_tensor_network_ready":true,"gauge":null,"goldstone_policy":"not-applicable","identity":{"anti_canonical_id":"model:built-in-sm-leading-color:state:d~","anti_display_name":"d~","anti_pdg_label":-1,"canonical_id":"model:built-in-sm-leading-color:state:d","display_name":"d","orientation":"particle","pdg_label":1,"self_conjugate":false,"species_id":"model:built-in-sm-leading-color:species:d"},"kernel":"weyl_fermion","kind":"weyl-fermion","mass_class":"massless","mass_parameter":null,"numerator":"i*weyl_slash(momentum)","particle_id":1,"width_parameter":null},"value_slot_id":9,"variant":"unpropagated"},{"applies_propagator":false,"chirality":-1,"component_start":26,"component_stop":28,"current_component_start":26,"current_component_stop":28,"current_id":10,"dimension":2,"external_labels":[2,3],"external_mask":6,"is_source":false,"momentum_mask":6,"particle_id":1,"propagator":{"applies_propagator":true,"auxiliary_policy":null,"backend":"spenso","basis":"weyl-chiral","chirality":-1,"custom_source":null,"denominator":"momentum_squared","description":"massless Weyl fermion propagator","full_tensor_network_ready":true,"gauge":null,"goldstone_policy":"not-applicable","identity":{"anti_canonical_id":"model:built-in-sm-leading-color:state:d~","anti_display_name":"d~","anti_pdg_label":-1,"canonical_id":"model:built-in-sm-leading-color:state:d","display_name":"d","orientation":"particle","pdg_label":1,"self_conjugate":false,"species_id":"model:built-in-sm-leading-color:species:d"},"kernel":"weyl_fermion","kind":"weyl-fermion","mass_class":"massless","mass_parameter":null,"numerator":"i*weyl_slash(momentum)","particle_id":1,"width_parameter":null},"value_slot_id":10,"variant":"unpropagated"},{"applies_propagator":false,"chirality":-1,"component_start":28,"component_stop":30,"current_component_start":28,"current_component_stop":30,"current_id":11,"dimension":2,"external_labels":[2,3],"external_mask":6,"is_source":false,"momentum_mask":6,"particle_id":1,"propagator":{"applies_propagator":true,"auxiliary_policy":null,"backend":"spenso","basis":"weyl-chiral","chirality":-1,"custom_source":null,"denominator":"momentum_squared","description":"massless Weyl fermion propagator","full_tensor_network_ready":true,"gauge":null,"goldstone_policy":"not-applicable","identity":{"anti_canonical_id":"model:built-in-sm-leading-color:state:d~","anti_display_name":"d~","anti_pdg_label":-1,"canonical_id":"model:built-in-sm-leading-color:state:d","display_name":"d","orientation":"particle","pdg_label":1,"self_conjugate":false,"species_id":"model:built-in-sm-leading-color:species:d"},"kernel":"weyl_fermion","kind":"weyl-fermion","mass_class":"massless","mass_parameter":null,"numerator":"i*weyl_slash(momentum)","particle_id":1,"width_parameter":null},"value_slot_id":11,"variant":"unpropagated"},{"applies_propagator":false,"chirality":-1,"component_start":30,"component_stop":32,"current_component_start":30,"current_component_stop":32,"current_id":12,"dimension":2,"external_labels":[2,3],"external_mask":6,"is_source":false,"momentum_mask":6,"particle_id":1,"propagator":{"applies_propagator":true,"auxiliary_policy":null,"backend":"spenso","basis":"weyl-chiral","chirality":-1,"custom_source":null,"denominator":"momentum_squared","description":"massless Weyl fermion propagator","full_tensor_network_ready":true,"gauge":null,"goldstone_policy":"not-applicable","identity":{"anti_canonical_id":"model:built-in-sm-leading-color:state:d~","anti_display_name":"d~","anti_pdg_label":-1,"canonical_id":"model:built-in-sm-leading-color:state:d","display_name":"d","orientation":"particle","pdg_label":1,"self_conjugate":false,"species_id":"model:built-in-sm-leading-color:species:d"},"kernel":"weyl_fermion","kind":"weyl-fermion","mass_class":"massless","mass_parameter":null,"numerator":"i*weyl_slash(momentum)","particle_id":1,"width_parameter":null},"value_slot_id":12,"variant":"unpropagated"}]}},"schema_version":3},"materialized_sector_id":0}],"compiled":{"kind":"generic-dag-stage-blueprint","model_parameter_evaluator":null,"momentum_slot_ids":[1,2,3],"runtime_available":true,"runtime_unavailable_message":null,"stage_evaluators":{"amplitude_stage":{"blockers":[],"compiled_plane_arena":{"application_abi":"pyamplicol-compiled-plane-kernel-v2","element_layout":"split-complex-component-major","input_bindings":[{"component":0,"global_component":0,"kind":"value","parameter_index":0,"real_valued":false,"source_id":0},{"component":1,"global_component":1,"kind":"value","parameter_index":1,"real_valued":false,"source_id":0},{"component":0,"global_component":2,"kind":"value","parameter_index":2,"real_valued":false,"source_id":1},{"component":1,"global_component":3,"kind":"value","parameter_index":3,"real_valued":false,"source_id":1},{"component":0,"global_component":20,"kind":"value","parameter_index":4,"real_valued":false,"source_id":7},{"component":1,"global_component":21,"kind":"value","parameter_index":5,"real_valued":false,"source_id":7},{"component":0,"global_component":22,"kind":"value","parameter_index":6,"real_valued":false,"source_id":8},{"component":1,"global_component":23,"kind":"value","parameter_index":7,"real_valued":false,"source_id":8},{"component":0,"global_component":24,"kind":"value","parameter_index":8,"real_valued":false,"source_id":9},{"component":1,"global_component":25,"kind":"value","parameter_index":9,"real_valued":false,"source_id":9},{"component":0,"global_component":26,"kind":"value","parameter_index":10,"real_valued":false,"source_id":10},{"component":1,"global_component":27,"kind":"value","parameter_index":11,"real_valued":false,"source_id":10},{"component":0,"global_component":28,"kind":"value","parameter_index":12,"real_valued":false,"source_id":11},{"component":1,"global_component":29,"kind":"value","parameter_index":13,"real_valued":false,"source_id":11},{"component":0,"global_component":30,"kind":"value","parameter_index":14,"real_valued":false,"source_id":12},{"component":1,"global_component":31,"kind":"value","parameter_index":15,"real_valued":false,"source_id":12}],"input_output_aliasing":"forbidden","kind":"compiled-plane-arena-stage","leaves":[{"application_path":"helicity-sum/evaluators/generic_amplitude_stage_c364260eb0094c4d9759fd633eb269fe.plane.symjit","direct_codegen_optimization_level":2,"input_indices":[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15],"input_len":16,"optimization_level":2,"output_len":6,"output_start":0,"output_stop":6,"source_application_abi":"pyamplicol-symjit-plane-application-v2"}],"output_bindings":[{"arena":"amplitude","component":0,"output_index":0},{"arena":"amplitude","component":1,"output_index":1},{"arena":"amplitude","component":2,"output_index":2},{"arena":"amplitude","component":3,"output_index":3},{"arena":"amplitude","component":4,"output_index":4},{"arena":"amplitude","component":5,"output_index":5}],"output_factor":"identity","output_operation":"overwrite","output_output_aliasing":"forbidden","schema_version":1,"source_application_abi":"pyamplicol-symjit-plane-application-v2"},"evaluator":{"application_abi":"symjit-application-storage-v3","application_path":"helicity-sum/evaluators/generic_amplitude_stage_c364260eb0094c4d9759fd633eb269fe.symjit","batch_layout":"row-major","compiler_type":"native","element_layout":"complex-f64","endianness":"little","evaluator_state_path":null,"evaluator_state_runtime_capability":null,"input_len":16,"kind":"symjit-application-evaluator","optimization_level":2,"output_len":6,"plane_application":{"application_abi":"pyamplicol-symjit-plane-application-v2","application_path":"helicity-sum/evaluators/generic_amplitude_stage_c364260eb0094c4d9759fd633eb269fe.plane.symjit","compiler_type":"native","complex":true,"compression":true,"descriptor_order":"inputs-re-im-then-outputs-re-im","direct_arena":true,"element_layout":"split-complex-plane-major","fast_complex":false,"fast_math":true,"input_complex_count":16,"input_plane_count":32,"optimization_level":2,"output_complex_count":6,"output_plane_count":12,"simd":true,"source_digest":"2a37e3f222d3a34a70cc55f8517b6823d0c17e6efad8f06f75e39ed367d6e6b1","storage_abi":"symjit-application-storage-v3","target":{"cpu_features":["aes","bf16","bti","crc","dit","dotprod","dpb","dpb2","fcma","fhm","flagm","fp","fp16","frintts","i8mm","jsconv","lse","lse2","neon","paca","pacg","rcpc","rcpc2","rdm","sb","sha2","sha3","ssbs"],"endianness":"little","triple":"aarch64-apple-darwin","word_bits":64},"threading":false,"translation_mode":"symbolica-structured-instructions"},"required_defuns":[],"runtime_capability":"symjit.application.complex-f64.v1","translation_mode":"indirect","word_bits":64},"evaluator_label":"generic_amplitude_stage","expression_ready":true,"input_components":[{"component":0,"global_component":0,"kind":"value","parameter_index":0,"real_valued":false,"source_id":0},{"component":1,"global_component":1,"kind":"value","parameter_index":1,"real_valued":false,"source_id":0},{"component":0,"global_component":2,"kind":"value","parameter_index":2,"real_valued":false,"source_id":1},{"component":1,"global_component":3,"kind":"value","parameter_index":3,"real_valued":false,"source_id":1},{"component":0,"global_component":20,"kind":"value","parameter_index":4,"real_valued":false,"source_id":7},{"component":1,"global_component":21,"kind":"value","parameter_index":5,"real_valued":false,"source_id":7},{"component":0,"global_component":22,"kind":"value","parameter_index":6,"real_valued":false,"source_id":8},{"component":1,"global_component":23,"kind":"value","parameter_index":7,"real_valued":false,"source_id":8},{"component":0,"global_component":24,"kind":"value","parameter_index":8,"real_valued":false,"source_id":9},{"component":1,"global_component":25,"kind":"value","parameter_index":9,"real_valued":false,"source_id":9},{"component":0,"global_component":26,"kind":"value","parameter_index":10,"real_valued":false,"source_id":10},{"component":1,"global_component":27,"kind":"value","parameter_index":11,"real_valued":false,"source_id":10},{"component":0,"global_component":28,"kind":"value","parameter_index":12,"real_valued":false,"source_id":11},{"component":1,"global_component":29,"kind":"value","parameter_index":13,"real_valued":false,"source_id":11},{"component":0,"global_component":30,"kind":"value","parameter_index":14,"real_valued":false,"source_id":12},{"component":1,"global_component":31,"kind":"value","parameter_index":15,"real_valued":false,"source_id":12}],"input_value_slot_ids":[0,1,7,8,9,10,11,12],"interaction_ids":[],"model_parameter_count":0,"momentum_parameter_count":0,"output_length":6,"output_slots":[{"component_start":0,"component_stop":1,"current_id":-1,"output_start":0,"output_stop":1,"value_slot_id":-1,"variant":"amplitude-root"},{"component_start":1,"component_stop":2,"current_id":-1,"output_start":1,"output_stop":2,"value_slot_id":-1,"variant":"amplitude-root"},{"component_start":2,"component_stop":3,"current_id":-1,"output_start":2,"output_stop":3,"value_slot_id":-1,"variant":"amplitude-root"},{"component_start":3,"component_stop":4,"current_id":-1,"output_start":3,"output_stop":4,"value_slot_id":-1,"variant":"amplitude-root"},{"component_start":4,"component_stop":5,"current_id":-1,"output_start":4,"output_stop":5,"value_slot_id":-1,"variant":"amplitude-root"},{"component_start":5,"component_stop":6,"current_id":-1,"output_start":5,"output_stop":6,"value_slot_id":-1,"variant":"amplitude-root"}],"output_value_slot_ids":[],"parameter_count":16,"parameter_layout":"stage-local-value-momentum","real_valued_inputs":[],"stage_index":0,"stage_kind":"amplitude-roots","subset_size":null,"value_parameter_count":16},"kind":"generic-dag-stage-evaluator-artifacts","model_parameter_count":0,"momentum_parameter_count":0,"parameter_count":0,"parameter_layout":"stage-local-value-momentum","real_valued_inputs":[],"required_runtime_capabilities":["compiled-plane-arena-v1","symjit.application.complex-f64.v1"],"runtime_available":true,"runtime_unavailable_message":null,"stage_count":2,"stages":[{"blockers":[],"compiled_plane_arena":{"application_abi":"pyamplicol-compiled-plane-kernel-v2","element_layout":"split-complex-component-major","input_bindings":[{"component":0,"global_component":4,"kind":"value","parameter_index":0,"real_valued":false,"source_id":2},{"component":1,"global_component":5,"kind":"value","parameter_index":1,"real_valued":false,"source_id":2},{"component":0,"global_component":6,"kind":"value","parameter_index":2,"real_valued":false,"source_id":3},{"component":1,"global_component":7,"kind":"value","parameter_index":3,"real_valued":false,"source_id":3},{"component":0,"global_component":8,"kind":"value","parameter_index":4,"real_valued":false,"source_id":4},{"component":1,"global_component":9,"kind":"value","parameter_index":5,"real_valued":false,"source_id":4},{"component":2,"global_component":10,"kind":"value","parameter_index":6,"real_valued":false,"source_id":4},{"component":3,"global_component":11,"kind":"value","parameter_index":7,"real_valued":false,"source_id":4},{"component":0,"global_component":12,"kind":"value","parameter_index":8,"real_valued":false,"source_id":5},{"component":1,"global_component":13,"kind":"value","parameter_index":9,"real_valued":false,"source_id":5},{"component":2,"global_component":14,"kind":"value","parameter_index":10,"real_valued":false,"source_id":5},{"component":3,"global_component":15,"kind":"value","parameter_index":11,"real_valued":false,"source_id":5},{"component":0,"global_component":16,"kind":"value","parameter_index":12,"real_valued":false,"source_id":6},{"component":1,"global_component":17,"kind":"value","parameter_index":13,"real_valued":false,"source_id":6},{"component":2,"global_component":18,"kind":"value","parameter_index":14,"real_valued":false,"source_id":6},{"component":3,"global_component":19,"kind":"value","parameter_index":15,"real_valued":false,"source_id":6},{"component":0,"global_component":36,"kind":"momentum","parameter_index":16,"real_valued":true,"source_id":1},{"component":1,"global_component":37,"kind":"momentum","parameter_index":17,"real_valued":true,"source_id":1},{"component":2,"global_component":38,"kind":"momentum","parameter_index":18,"real_valued":true,"source_id":1},{"component":3,"global_component":39,"kind":"momentum","parameter_index":19,"real_valued":true,"source_id":1},{"component":0,"global_component":40,"kind":"momentum","parameter_index":20,"real_valued":true,"source_id":2},{"component":1,"global_component":41,"kind":"momentum","parameter_index":21,"real_valued":true,"source_id":2},{"component":2,"global_component":42,"kind":"momentum","parameter_index":22,"real_valued":true,"source_id":2},{"component":3,"global_component":43,"kind":"momentum","parameter_index":23,"real_valued":true,"source_id":2},{"component":0,"global_component":44,"kind":"momentum","parameter_index":24,"real_valued":true,"source_id":3},{"component":1,"global_component":45,"kind":"momentum","parameter_index":25,"real_valued":true,"source_id":3},{"component":2,"global_component":46,"kind":"momentum","parameter_index":26,"real_valued":true,"source_id":3},{"component":3,"global_component":47,"kind":"momentum","parameter_index":27,"real_valued":true,"source_id":3},{"component":0,"global_component":58,"kind":"model_parameter","parameter_index":28,"real_valued":true,"source_id":10},{"component":0,"global_component":59,"kind":"model_parameter","parameter_index":29,"real_valued":true,"source_id":11}],"input_output_aliasing":"forbidden","kind":"compiled-plane-arena-stage","leaves":[{"application_path":"helicity-sum/evaluators/generic_stage_1_subset_2_90fc7853c5a54d81b162083c4bf08e08.plane.symjit","direct_codegen_optimization_level":2,"input_indices":[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29],"input_len":30,"optimization_level":2,"output_len":12,"output_start":0,"output_stop":12,"source_application_abi":"pyamplicol-symjit-plane-application-v2"}],"output_bindings":[{"arena":"current","component":20,"output_index":0},{"arena":"current","component":21,"output_index":1},{"arena":"current","component":22,"output_index":2},{"arena":"current","component":23,"output_index":3},{"arena":"current","component":24,"output_index":4},{"arena":"current","component":25,"output_index":5},{"arena":"current","component":26,"output_index":6},{"arena":"current","component":27,"output_index":7},{"arena":"current","component":28,"output_index":8},{"arena":"current","component":29,"output_index":9},{"arena":"current","component":30,"output_index":10},{"arena":"current","component":31,"output_index":11}],"output_factor":"identity","output_operation":"overwrite","output_output_aliasing":"forbidden","schema_version":1,"source_application_abi":"pyamplicol-symjit-plane-application-v2"},"evaluator":{"application_abi":"symjit-application-storage-v3","application_path":"helicity-sum/evaluators/generic_stage_1_subset_2_90fc7853c5a54d81b162083c4bf08e08.symjit","batch_layout":"row-major","compiler_type":"native","element_layout":"complex-f64","endianness":"little","evaluator_state_path":null,"evaluator_state_runtime_capability":null,"input_len":30,"kind":"symjit-application-evaluator","optimization_level":2,"output_len":12,"plane_application":{"application_abi":"pyamplicol-symjit-plane-application-v2","application_path":"helicity-sum/evaluators/generic_stage_1_subset_2_90fc7853c5a54d81b162083c4bf08e08.plane.symjit","compiler_type":"native","complex":true,"compression":true,"descriptor_order":"inputs-re-im-then-outputs-re-im","direct_arena":true,"element_layout":"split-complex-plane-major","fast_complex":false,"fast_math":true,"input_complex_count":30,"input_plane_count":60,"optimization_level":2,"output_complex_count":12,"output_plane_count":24,"simd":true,"source_digest":"932205a6edcc3a03c036ff3ac62bcbb2189b3471d544fffa5d82f5a25033b0c2","storage_abi":"symjit-application-storage-v3","target":{"cpu_features":["aes","bf16","bti","crc","dit","dotprod","dpb","dpb2","fcma","fhm","flagm","fp","fp16","frintts","i8mm","jsconv","lse","lse2","neon","paca","pacg","rcpc","rcpc2","rdm","sb","sha2","sha3","ssbs"],"endianness":"little","triple":"aarch64-apple-darwin","word_bits":64},"threading":false,"translation_mode":"symbolica-structured-instructions"},"required_defuns":[],"runtime_capability":"symjit.application.complex-f64.v1","translation_mode":"indirect","word_bits":64},"evaluator_label":"generic_stage_1_subset_2","expression_ready":true,"input_components":[{"component":0,"global_component":4,"kind":"value","parameter_index":0,"real_valued":false,"source_id":2},{"component":1,"global_component":5,"kind":"value","parameter_index":1,"real_valued":false,"source_id":2},{"component":0,"global_component":6,"kind":"value","parameter_index":2,"real_valued":false,"source_id":3},{"component":1,"global_component":7,"kind":"value","parameter_index":3,"real_valued":false,"source_id":3},{"component":0,"global_component":8,"kind":"value","parameter_index":4,"real_valued":false,"source_id":4},{"component":1,"global_component":9,"kind":"value","parameter_index":5,"real_valued":false,"source_id":4},{"component":2,"global_component":10,"kind":"value","parameter_index":6,"real_valued":false,"source_id":4},{"component":3,"global_component":11,"kind":"value","parameter_index":7,"real_valued":false,"source_id":4},{"component":0,"global_component":12,"kind":"value","parameter_index":8,"real_valued":false,"source_id":5},{"component":1,"global_component":13,"kind":"value","parameter_index":9,"real_valued":false,"source_id":5},{"component":2,"global_component":14,"kind":"value","parameter_index":10,"real_valued":false,"source_id":5},{"component":3,"global_component":15,"kind":"value","parameter_index":11,"real_valued":false,"source_id":5},{"component":0,"global_component":16,"kind":"value","parameter_index":12,"real_valued":false,"source_id":6},{"component":1,"global_component":17,"kind":"value","parameter_index":13,"real_valued":false,"source_id":6},{"component":2,"global_component":18,"kind":"value","parameter_index":14,"real_valued":false,"source_id":6},{"component":3,"global_component":19,"kind":"value","parameter_index":15,"real_valued":false,"source_id":6},{"component":0,"global_component":36,"kind":"momentum","parameter_index":16,"real_valued":true,"source_id":1},{"component":1,"global_component":37,"kind":"momentum","parameter_index":17,"real_valued":true,"source_id":1},{"component":2,"global_component":38,"kind":"momentum","parameter_index":18,"real_valued":true,"source_id":1},{"component":3,"global_component":39,"kind":"momentum","parameter_index":19,"real_valued":true,"source_id":1},{"component":0,"global_component":40,"kind":"momentum","parameter_index":20,"real_valued":true,"source_id":2},{"component":1,"global_component":41,"kind":"momentum","parameter_index":21,"real_valued":true,"source_id":2},{"component":2,"global_component":42,"kind":"momentum","parameter_index":22,"real_valued":true,"source_id":2},{"component":3,"global_component":43,"kind":"momentum","parameter_index":23,"real_valued":true,"source_id":2},{"component":0,"global_component":44,"kind":"momentum","parameter_index":24,"real_valued":true,"source_id":3},{"component":1,"global_component":45,"kind":"momentum","parameter_index":25,"real_valued":true,"source_id":3},{"component":2,"global_component":46,"kind":"momentum","parameter_index":26,"real_valued":true,"source_id":3},{"component":3,"global_component":47,"kind":"momentum","parameter_index":27,"real_valued":true,"source_id":3},{"component":0,"global_component":58,"kind":"model_parameter","parameter_index":28,"real_valued":true,"source_id":10},{"component":0,"global_component":59,"kind":"model_parameter","parameter_index":29,"real_valued":true,"source_id":11}],"input_value_slot_ids":[2,3,4,5,6],"interaction_ids":[0,1,2,3,4,5],"model_parameter_count":2,"momentum_parameter_count":12,"output_length":12,"output_slots":[{"component_start":20,"component_stop":22,"current_id":7,"output_start":0,"output_stop":2,"value_slot_id":7,"variant":"unpropagated"},{"component_start":22,"component_stop":24,"current_id":8,"output_start":2,"output_stop":4,"value_slot_id":8,"variant":"unpropagated"},{"component_start":24,"component_stop":26,"current_id":9,"output_start":4,"output_stop":6,"value_slot_id":9,"variant":"unpropagated"},{"component_start":26,"component_stop":28,"current_id":10,"output_start":6,"output_stop":8,"value_slot_id":10,"variant":"unpropagated"},{"component_start":28,"component_stop":30,"current_id":11,"output_start":8,"output_stop":10,"value_slot_id":11,"variant":"unpropagated"},{"component_start":30,"component_stop":32,"current_id":12,"output_start":10,"output_stop":12,"value_slot_id":12,"variant":"unpropagated"}],"output_value_slot_ids":[7,8,9,10,11,12],"parameter_count":30,"parameter_layout":"stage-local-value-momentum","real_valued_inputs":[16,17,18,19,20,21,22,23,24,25,26,27,28,29],"stage_index":1,"stage_kind":"current-combine","subset_size":2,"value_parameter_count":16}],"value_parameter_count":0}},"dag_summary":{"amplitude_root_count":6,"current_count":13,"interaction_count":6,"interaction_evaluation_count":6,"source_count":7,"truncated":false},"external_pdg_order":[1,-1,23],"key":"d_dbar_to_z","kind":"pyamplicol-runtime-execution","materialization_census":{"abi":"pyamplicol-fully-resident-materialization-census-v1","basis":"immutable-fully-resident-compiled-dag","final":{"amplitude_root_count":6,"current_count":13,"interaction_count":6,"interaction_evaluation_count":6,"source_count":7},"final_equals_peak":true,"peak":{"amplitude_root_count":6,"current_count":13,"interaction_count":6,"interaction_evaluation_count":6,"source_count":7}},"physics_reduction":{"groups":[{"id":"reduction:0","physical_color_ids":["flow:2,1"],"physical_helicity_ids":["h:+1,-1,-1"],"representative_color_id":"flow:2,1","representative_helicity_id":"h:+1,-1,-1"},{"id":"reduction:1","physical_color_ids":["flow:2,1"],"physical_helicity_ids":["h:+1,-1,+0"],"representative_color_id":"flow:2,1","representative_helicity_id":"h:+1,-1,+0"},{"id":"reduction:2","physical_color_ids":["flow:2,1"],"physical_helicity_ids":["h:+1,-1,+1"],"representative_color_id":"flow:2,1","representative_helicity_id":"h:+1,-1,+1"},{"id":"reduction:3","physical_color_ids":["flow:2,1"],"physical_helicity_ids":["h:-1,+1,-1"],"representative_color_id":"flow:2,1","representative_helicity_id":"h:-1,+1,-1"},{"id":"reduction:4","physical_color_ids":["flow:2,1"],"physical_helicity_ids":["h:-1,+1,+0"],"representative_color_id":"flow:2,1","representative_helicity_id":"h:-1,+1,+0"},{"id":"reduction:5","physical_color_ids":["flow:2,1"],"physical_helicity_ids":["h:-1,+1,+1"],"representative_color_id":"flow:2,1","representative_helicity_id":"h:-1,+1,+1"}],"kind":"lc-diagonal"},"process":"d d~ > z","required_runtime_capabilities":["compiled-plane-arena-v1","rusticol.compiled.color-topology-lanes.v1","rusticol.compiled.runtime-selectors.v1","symjit.application.complex-f64.v1"],"runtime_schema":{"amplitude_stage":{"color_contraction":null,"color_topology_replay":null,"output_count":6,"roots":[{"all_sector_weight":1.0,"coherent_group_id":0,"color_sector_id":0,"color_weight":[1.0,0.0],"contraction":"weyl","contraction_ir":{"chirality_relation":"opposite","coefficients":[[1.0,0.0],[1.0,0.0]],"left_basis":"weyl-chiral","metric_signature":null,"name":"weyl","right_basis":"weyl-chiral"},"coupling":[1.0,0.0],"helicity_weight":1.0,"kind":"direct-contraction","left_current_id":7,"left_slot":{"component_start":20,"component_stop":22,"current_id":7,"dimension":2},"left_value_slot":{"component_start":20,"component_stop":22,"current_id":7,"dimension":2,"value_slot_id":7,"variant":"unpropagated"},"output_index":0,"right_current_id":1,"right_slot":{"component_start":2,"component_stop":4,"current_id":1,"dimension":2},"right_value_slot":{"component_start":2,"component_stop":4,"current_id":1,"dimension":2,"value_slot_id":1,"variant":"source"},"root_id":0,"vertex_kind":null,"vertex_particles":null},{"all_sector_weight":1.0,"coherent_group_id":1,"color_sector_id":0,"color_weight":[1.0,0.0],"contraction":"weyl","contraction_ir":{"chirality_relation":"opposite","coefficients":[[1.0,0.0],[1.0,0.0]],"left_basis":"weyl-chiral","metric_signature":null,"name":"weyl","right_basis":"weyl-chiral"},"coupling":[1.0,0.0],"helicity_weight":1.0,"kind":"direct-contraction","left_current_id":8,"left_slot":{"component_start":22,"component_stop":24,"current_id":8,"dimension":2},"left_value_slot":{"component_start":22,"component_stop":24,"current_id":8,"dimension":2,"value_slot_id":8,"variant":"unpropagated"},"output_index":1,"right_current_id":1,"right_slot":{"component_start":2,"component_stop":4,"current_id":1,"dimension":2},"right_value_slot":{"component_start":2,"component_stop":4,"current_id":1,"dimension":2,"value_slot_id":1,"variant":"source"},"root_id":1,"vertex_kind":null,"vertex_particles":null},{"all_sector_weight":1.0,"coherent_group_id":2,"color_sector_id":0,"color_weight":[1.0,0.0],"contraction":"weyl","contraction_ir":{"chirality_relation":"opposite","coefficients":[[1.0,0.0],[1.0,0.0]],"left_basis":"weyl-chiral","metric_signature":null,"name":"weyl","right_basis":"weyl-chiral"},"coupling":[1.0,0.0],"helicity_weight":1.0,"kind":"direct-contraction","left_current_id":9,"left_slot":{"component_start":24,"component_stop":26,"current_id":9,"dimension":2},"left_value_slot":{"component_start":24,"component_stop":26,"current_id":9,"dimension":2,"value_slot_id":9,"variant":"unpropagated"},"output_index":2,"right_current_id":1,"right_slot":{"component_start":2,"component_stop":4,"current_id":1,"dimension":2},"right_value_slot":{"component_start":2,"component_stop":4,"current_id":1,"dimension":2,"value_slot_id":1,"variant":"source"},"root_id":2,"vertex_kind":null,"vertex_particles":null},{"all_sector_weight":1.0,"coherent_group_id":3,"color_sector_id":0,"color_weight":[1.0,0.0],"contraction":"weyl","contraction_ir":{"chirality_relation":"opposite","coefficients":[[1.0,0.0],[1.0,0.0]],"left_basis":"weyl-chiral","metric_signature":null,"name":"weyl","right_basis":"weyl-chiral"},"coupling":[1.0,0.0],"helicity_weight":1.0,"kind":"direct-contraction","left_current_id":10,"left_slot":{"component_start":26,"component_stop":28,"current_id":10,"dimension":2},"left_value_slot":{"component_start":26,"component_stop":28,"current_id":10,"dimension":2,"value_slot_id":10,"variant":"unpropagated"},"output_index":3,"right_current_id":0,"right_slot":{"component_start":0,"component_stop":2,"current_id":0,"dimension":2},"right_value_slot":{"component_start":0,"component_stop":2,"current_id":0,"dimension":2,"value_slot_id":0,"variant":"source"},"root_id":3,"vertex_kind":null,"vertex_particles":null},{"all_sector_weight":1.0,"coherent_group_id":4,"color_sector_id":0,"color_weight":[1.0,0.0],"contraction":"weyl","contraction_ir":{"chirality_relation":"opposite","coefficients":[[1.0,0.0],[1.0,0.0]],"left_basis":"weyl-chiral","metric_signature":null,"name":"weyl","right_basis":"weyl-chiral"},"coupling":[1.0,0.0],"helicity_weight":1.0,"kind":"direct-contraction","left_current_id":11,"left_slot":{"component_start":28,"component_stop":30,"current_id":11,"dimension":2},"left_value_slot":{"component_start":28,"component_stop":30,"current_id":11,"dimension":2,"value_slot_id":11,"variant":"unpropagated"},"output_index":4,"right_current_id":0,"right_slot":{"component_start":0,"component_stop":2,"current_id":0,"dimension":2},"right_value_slot":{"component_start":0,"component_stop":2,"current_id":0,"dimension":2,"value_slot_id":0,"variant":"source"},"root_id":4,"vertex_kind":null,"vertex_particles":null},{"all_sector_weight":1.0,"coherent_group_id":5,"color_sector_id":0,"color_weight":[1.0,0.0],"contraction":"weyl","contraction_ir":{"chirality_relation":"opposite","coefficients":[[1.0,0.0],[1.0,0.0]],"left_basis":"weyl-chiral","metric_signature":null,"name":"weyl","right_basis":"weyl-chiral"},"coupling":[1.0,0.0],"helicity_weight":1.0,"kind":"direct-contraction","left_current_id":12,"left_slot":{"component_start":30,"component_stop":32,"current_id":12,"dimension":2},"left_value_slot":{"component_start":30,"component_stop":32,"current_id":12,"dimension":2,"value_slot_id":12,"variant":"unpropagated"},"output_index":5,"right_current_id":0,"right_slot":{"component_start":0,"component_stop":2,"current_id":0,"dimension":2},"right_value_slot":{"component_start":0,"component_stop":2,"current_id":0,"dimension":2,"value_slot_id":0,"variant":"source"},"root_id":5,"vertex_kind":null,"vertex_particles":null}],"stage_kind":"amplitude-roots"},"current_storage":{"component_count":32,"current_slots":[{"auxiliary_kind":null,"chirality":1,"color_state":{"accuracy":"lc","basis_key":[],"line_groups":[0],"sector_id":0},"component_start":0,"component_stop":2,"current_id":0,"dimension":2,"external_labels":[1],"external_mask":1,"flavour_flow":[-1],"helicity_ancestry":"0x1","is_source":true,"momentum_mask":1,"particle_id":-1,"spin_state":1},{"auxiliary_kind":null,"chirality":-1,"color_state":{"accuracy":"lc","basis_key":[],"line_groups":[0],"sector_id":0},"component_start":2,"component_stop":4,"current_id":1,"dimension":2,"external_labels":[1],"external_mask":1,"flavour_flow":[-1],"helicity_ancestry":"0x2","is_source":true,"momentum_mask":1,"particle_id":-1,"spin_state":-1},{"auxiliary_kind":null,"chirality":1,"color_state":{"accuracy":"lc","basis_key":[],"line_groups":[0],"sector_id":0},"component_start":4,"component_stop":6,"current_id":2,"dimension":2,"external_labels":[2],"external_mask":2,"flavour_flow":[1],"helicity_ancestry":"0x4","is_source":true,"momentum_mask":2,"particle_id":1,"spin_state":1},{"auxiliary_kind":null,"chirality":-1,"color_state":{"accuracy":"lc","basis_key":[],"line_groups":[0],"sector_id":0},"component_start":6,"component_stop":8,"current_id":3,"dimension":2,"external_labels":[2],"external_mask":2,"flavour_flow":[1],"helicity_ancestry":"0x8","is_source":true,"momentum_mask":2,"particle_id":1,"spin_state":-1},{"auxiliary_kind":null,"chirality":0,"color_state":{"accuracy":"lc","basis_key":[],"line_groups":[],"sector_id":0},"component_start":8,"component_stop":12,"current_id":4,"dimension":4,"external_labels":[3],"external_mask":4,"flavour_flow":[23],"helicity_ancestry":"0x10","is_source":true,"momentum_mask":4,"particle_id":23,"spin_state":-1},{"auxiliary_kind":null,"chirality":0,"color_state":{"accuracy":"lc","basis_key":[],"line_groups":[],"sector_id":0},"component_start":12,"component_stop":16,"current_id":5,"dimension":4,"external_labels":[3],"external_mask":4,"flavour_flow":[23],"helicity_ancestry":"0x20","is_source":true,"momentum_mask":4,"particle_id":23,"spin_state":0},{"auxiliary_kind":null,"chirality":0,"color_state":{"accuracy":"lc","basis_key":[],"line_groups":[],"sector_id":0},"component_start":16,"component_stop":20,"current_id":6,"dimension":4,"external_labels":[3],"external_mask":4,"flavour_flow":[23],"helicity_ancestry":"0x40","is_source":true,"momentum_mask":4,"particle_id":23,"spin_state":1},{"auxiliary_kind":null,"chirality":1,"color_state":{"accuracy":"lc","basis_key":[],"line_groups":[0],"sector_id":0},"component_start":20,"component_stop":22,"current_id":7,"dimension":2,"external_labels":[2,3],"external_mask":6,"flavour_flow":[1],"helicity_ancestry":"0x14","is_source":false,"momentum_mask":6,"particle_id":1,"spin_state":1},{"auxiliary_kind":null,"chirality":1,"color_state":{"accuracy":"lc","basis_key":[],"line_groups":[0],"sector_id":0},"component_start":22,"component_stop":24,"current_id":8,"dimension":2,"external_labels":[2,3],"external_mask":6,"flavour_flow":[1],"helicity_ancestry":"0x24","is_source":false,"momentum_mask":6,"particle_id":1,"spin_state":1},{"auxiliary_kind":null,"chirality":1,"color_state":{"accuracy":"lc","basis_key":[],"line_groups":[0],"sector_id":0},"component_start":24,"component_stop":26,"current_id":9,"dimension":2,"external_labels":[2,3],"external_mask":6,"flavour_flow":[1],"helicity_ancestry":"0x44","is_source":false,"momentum_mask":6,"particle_id":1,"spin_state":1},{"auxiliary_kind":null,"chirality":-1,"color_state":{"accuracy":"lc","basis_key":[],"line_groups":[0],"sector_id":0},"component_start":26,"component_stop":28,"current_id":10,"dimension":2,"external_labels":[2,3],"external_mask":6,"flavour_flow":[1],"helicity_ancestry":"0x18","is_source":false,"momentum_mask":6,"particle_id":1,"spin_state":-1},{"auxiliary_kind":null,"chirality":-1,"color_state":{"accuracy":"lc","basis_key":[],"line_groups":[0],"sector_id":0},"component_start":28,"component_stop":30,"current_id":11,"dimension":2,"external_labels":[2,3],"external_mask":6,"flavour_flow":[1],"helicity_ancestry":"0x28","is_source":false,"momentum_mask":6,"particle_id":1,"spin_state":-1},{"auxiliary_kind":null,"chirality":-1,"color_state":{"accuracy":"lc","basis_key":[],"line_groups":[0],"sector_id":0},"component_start":30,"component_stop":32,"current_id":12,"dimension":2,"external_labels":[2,3],"external_mask":6,"flavour_flow":[1],"helicity_ancestry":"0x48","is_source":false,"momentum_mask":6,"particle_id":1,"spin_state":-1}],"metadata_compacted":true,"number_type":"complex"},"external_particles":[{"index":0,"label":1,"momentum_slot":0,"outgoing_pdg":-1,"pdg":1,"role":"initial"},{"index":1,"label":2,"momentum_slot":1,"outgoing_pdg":1,"pdg":-1,"role":"initial"},{"index":2,"label":3,"momentum_slot":2,"outgoing_pdg":23,"pdg":23,"role":"final"}],"kind":"pyamplicol-runtime-execution-plan","model":{"particles":[{"mass":0.0,"mass_parameter":null,"pdg":1},{"mass":91.188,"mass_parameter":"particle.23.mass","pdg":23}]},"model_parameters":[{"default":0.0075467711139788835,"kind":"normalization","name":"normalization.alpha_ew","parameter_index":0},{"default":0.118,"kind":"normalization","name":"normalization.alpha_s_me_check","parameter_index":1},{"default":173.0,"kind":"particle_mass","name":"particle.6.mass","parameter_index":2,"pdg":6},{"default":1.4915,"kind":"particle_width","name":"particle.6.width","parameter_index":3,"pdg":6},{"default":91.188,"kind":"particle_mass","name":"particle.23.mass","parameter_index":4,"pdg":23},{"default":2.441404,"kind":"particle_width","name":"particle.23.width","parameter_index":5,"pdg":23},{"default":80.41900244575616,"kind":"particle_mass","name":"particle.24.mass","parameter_index":6,"pdg":24},{"default":2.0476,"kind":"particle_width","name":"particle.24.width","parameter_index":7,"pdg":24},{"default":125.0,"kind":"particle_mass","name":"particle.25.mass","parameter_index":8,"pdg":25},{"default":0.006382339,"kind":"particle_width","name":"particle.25.width","parameter_index":9,"pdg":25},{"default":-1.0244420275940371,"kind":"coupling_component","name":"coupling.10.1_23_1.component_0","parameter_index":10},{"default":0.17818666745287456,"kind":"coupling_component","name":"coupling.10.1_23_1.component_1","parameter_index":11}],"momentum_slots":[{"component_start":0,"component_stop":4,"external_labels":[1],"momentum_mask":1,"momentum_slot_id":0,"real_valued":true},{"component_start":4,"component_stop":8,"external_labels":[2],"momentum_mask":2,"momentum_slot_id":1,"real_valued":true},{"component_start":8,"component_stop":12,"external_labels":[3],"momentum_mask":4,"momentum_slot_id":2,"real_valued":true},{"component_start":12,"component_stop":16,"external_labels":[2,3],"momentum_mask":6,"momentum_slot_id":3,"real_valued":true}],"normalization":{"average_factor":36.0,"color_factor":3.0,"electroweak_coupling_power":1,"global_coupling_factor":0.18967104551999775,"identical_factor":1.0,"qcd_coupling_power":0},"parameter_layout":{"model_parameter_count":12,"momentum_components_real":true,"momentum_parameter_count":16,"parameter_count_if_flattened":48,"real_valued_inputs":[20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47],"source_component_parameter_count":20,"source_components_complex":true,"value_component_count":32},"process":"d d~ > z","process_key":"d_dbar_to_z","schema_version":3,"source_fill":{"source_count":7,"sources":[{"anti_particle_id":1,"applied_crossing":{"chirality_factor":-1,"helicity_factor":1,"momentum_transform":"negate-four-momentum","phase":[1.0,0.0],"spin_state_factor":-1},"chirality":1,"color_state":{"accuracy":"lc","basis_key":[],"line_groups":[0],"sector_id":0},"crossing":"negate-incoming-momentum","current_component_start":0,"current_component_stop":2,"current_id":0,"dimension":2,"helicity_ancestry":"0x1","input_momentum_slot":0,"leg_label":1,"outgoing_pdg":-1,"particle_id":-1,"physical_pdg":1,"side":"initial","source_basis":"weyl-chiral","source_helicity":-1,"source_id":0,"source_ir":{"basis":"weyl-chiral","component_dimension":2,"crossing":{"chirality_factor":-1,"helicity_factor":1,"momentum_transform":"negate-four-momentum","phase":[1.0,0.0],"spin_state_factor":-1},"identity":{"anti_canonical_id":"model:built-in-sm-leading-color:state:d","anti_display_name":"d","anti_pdg_label":1,"canonical_id":"model:built-in-sm-leading-color:state:d~","display_name":"d~","orientation":"antiparticle","pdg_label":-1,"self_conjugate":false,"species_id":"model:built-in-sm-leading-color:species:d"},"mass_parameter":null,"states":[{"chirality":-1,"helicity":-1,"spin_state":-1},{"chirality":1,"helicity":1,"spin_state":1}],"statistics":"fermion","wavefunction_family":"fermion","width_parameter":null},"source_kind":"external-wavefunction","source_orientation":"antiparticle","source_parameter_start":0,"source_parameter_stop":2,"spin_state":1,"value_slot":{"component_start":0,"component_stop":2,"current_id":0,"dimension":2,"value_slot_id":0,"variant":"source"},"wavefunction_kind":"fermion"},{"anti_particle_id":1,"applied_crossing":{"chirality_factor":-1,"helicity_factor":1,"momentum_transform":"negate-four-momentum","phase":[1.0,0.0],"spin_state_factor":-1},"chirality":-1,"color_state":{"accuracy":"lc","basis_key":[],"line_groups":[0],"sector_id":0},"crossing":"negate-incoming-momentum","current_component_start":2,"current_component_stop":4,"current_id":1,"dimension":2,"helicity_ancestry":"0x2","input_momentum_slot":0,"leg_label":1,"outgoing_pdg":-1,"particle_id":-1,"physical_pdg":1,"side":"initial","source_basis":"weyl-chiral","source_helicity":1,"source_id":1,"source_ir":{"basis":"weyl-chiral","component_dimension":2,"crossing":{"chirality_factor":-1,"helicity_factor":1,"momentum_transform":"negate-four-momentum","phase":[1.0,0.0],"spin_state_factor":-1},"identity":{"anti_canonical_id":"model:built-in-sm-leading-color:state:d","anti_display_name":"d","anti_pdg_label":1,"canonical_id":"model:built-in-sm-leading-color:state:d~","display_name":"d~","orientation":"antiparticle","pdg_label":-1,"self_conjugate":false,"species_id":"model:built-in-sm-leading-color:species:d"},"mass_parameter":null,"states":[{"chirality":-1,"helicity":-1,"spin_state":-1},{"chirality":1,"helicity":1,"spin_state":1}],"statistics":"fermion","wavefunction_family":"fermion","width_parameter":null},"source_kind":"external-wavefunction","source_orientation":"antiparticle","source_parameter_start":2,"source_parameter_stop":4,"spin_state":-1,"value_slot":{"component_start":2,"component_stop":4,"current_id":1,"dimension":2,"value_slot_id":1,"variant":"source"},"wavefunction_kind":"fermion"},{"anti_particle_id":-1,"applied_crossing":{"chirality_factor":-1,"helicity_factor":1,"momentum_transform":"negate-four-momentum","phase":[1.0,0.0],"spin_state_factor":-1},"chirality":1,"color_state":{"accuracy":"lc","basis_key":[],"line_groups":[0],"sector_id":0},"crossing":"negate-incoming-momentum","current_component_start":4,"current_component_stop":6,"current_id":2,"dimension":2,"helicity_ancestry":"0x4","input_momentum_slot":1,"leg_label":2,"outgoing_pdg":1,"particle_id":1,"physical_pdg":-1,"side":"initial","source_basis":"weyl-chiral","source_helicity":-1,"source_id":2,"source_ir":{"basis":"weyl-chiral","component_dimension":2,"crossing":{"chirality_factor":-1,"helicity_factor":1,"momentum_transform":"negate-four-momentum","phase":[1.0,0.0],"spin_state_factor":-1},"identity":{"anti_canonical_id":"model:built-in-sm-leading-color:state:d~","anti_display_name":"d~","anti_pdg_label":-1,"canonical_id":"model:built-in-sm-leading-color:state:d","display_name":"d","orientation":"particle","pdg_label":1,"self_conjugate":false,"species_id":"model:built-in-sm-leading-color:species:d"},"mass_parameter":null,"states":[{"chirality":-1,"helicity":-1,"spin_state":-1},{"chirality":1,"helicity":1,"spin_state":1}],"statistics":"fermion","wavefunction_family":"fermion","width_parameter":null},"source_kind":"external-wavefunction","source_orientation":"particle","source_parameter_start":4,"source_parameter_stop":6,"spin_state":1,"value_slot":{"component_start":4,"component_stop":6,"current_id":2,"dimension":2,"value_slot_id":2,"variant":"source"},"wavefunction_kind":"fermion"},{"anti_particle_id":-1,"applied_crossing":{"chirality_factor":-1,"helicity_factor":1,"momentum_transform":"negate-four-momentum","phase":[1.0,0.0],"spin_state_factor":-1},"chirality":-1,"color_state":{"accuracy":"lc","basis_key":[],"line_groups":[0],"sector_id":0},"crossing":"negate-incoming-momentum","current_component_start":6,"current_component_stop":8,"current_id":3,"dimension":2,"helicity_ancestry":"0x8","input_momentum_slot":1,"leg_label":2,"outgoing_pdg":1,"particle_id":1,"physical_pdg":-1,"side":"initial","source_basis":"weyl-chiral","source_helicity":1,"source_id":3,"source_ir":{"basis":"weyl-chiral","component_dimension":2,"crossing":{"chirality_factor":-1,"helicity_factor":1,"momentum_transform":"negate-four-momentum","phase":[1.0,0.0],"spin_state_factor":-1},"identity":{"anti_canonical_id":"model:built-in-sm-leading-color:state:d~","anti_display_name":"d~","anti_pdg_label":-1,"canonical_id":"model:built-in-sm-leading-color:state:d","display_name":"d","orientation":"particle","pdg_label":1,"self_conjugate":false,"species_id":"model:built-in-sm-leading-color:species:d"},"mass_parameter":null,"states":[{"chirality":-1,"helicity":-1,"spin_state":-1},{"chirality":1,"helicity":1,"spin_state":1}],"statistics":"fermion","wavefunction_family":"fermion","width_parameter":null},"source_kind":"external-wavefunction","source_orientation":"particle","source_parameter_start":6,"source_parameter_stop":8,"spin_state":-1,"value_slot":{"component_start":6,"component_stop":8,"current_id":3,"dimension":2,"value_slot_id":3,"variant":"source"},"wavefunction_kind":"fermion"},{"anti_particle_id":23,"applied_crossing":{"chirality_factor":1,"helicity_factor":1,"momentum_transform":"identity","phase":[1.0,0.0],"spin_state_factor":1},"chirality":0,"color_state":{"accuracy":"lc","basis_key":[],"line_groups":[],"sector_id":0},"crossing":"identity","current_component_start":8,"current_component_stop":12,"current_id":4,"dimension":4,"helicity_ancestry":"0x10","input_momentum_slot":2,"leg_label":3,"outgoing_pdg":23,"particle_id":23,"physical_pdg":23,"side":"final","source_basis":"lorentz-vector","source_helicity":-1,"source_id":4,"source_ir":{"basis":"lorentz-vector","component_dimension":4,"crossing":{"chirality_factor":1,"helicity_factor":-1,"momentum_transform":"negate-four-momentum","phase":[1.0,0.0],"spin_state_factor":-1},"identity":{"anti_canonical_id":"model:built-in-sm-leading-color:state:z","anti_display_name":"z","anti_pdg_label":23,"canonical_id":"model:built-in-sm-leading-color:state:z","display_name":"z","orientation":"self-conjugate","pdg_label":23,"self_conjugate":true,"species_id":"model:built-in-sm-leading-color:species:z"},"mass_parameter":null,"states":[{"chirality":0,"helicity":-1,"spin_state":-1},{"chirality":0,"helicity":0,"spin_state":0},{"chirality":0,"helicity":1,"spin_state":1}],"statistics":"boson","wavefunction_family":"vector","width_parameter":null},"source_kind":"external-wavefunction","source_orientation":"self-conjugate","source_parameter_start":8,"source_parameter_stop":12,"spin_state":-1,"value_slot":{"component_start":8,"component_stop":12,"current_id":4,"dimension":4,"value_slot_id":4,"variant":"source"},"wavefunction_kind":"vector"},{"anti_particle_id":23,"applied_crossing":{"chirality_factor":1,"helicity_factor":1,"momentum_transform":"identity","phase":[1.0,0.0],"spin_state_factor":1},"chirality":0,"color_state":{"accuracy":"lc","basis_key":[],"line_groups":[],"sector_id":0},"crossing":"identity","current_component_start":12,"current_component_stop":16,"current_id":5,"dimension":4,"helicity_ancestry":"0x20","input_momentum_slot":2,"leg_label":3,"outgoing_pdg":23,"particle_id":23,"physical_pdg":23,"side":"final","source_basis":"lorentz-vector","source_helicity":0,"source_id":5,"source_ir":{"basis":"lorentz-vector","component_dimension":4,"crossing":{"chirality_factor":1,"helicity_factor":-1,"momentum_transform":"negate-four-momentum","phase":[1.0,0.0],"spin_state_factor":-1},"identity":{"anti_canonical_id":"model:built-in-sm-leading-color:state:z","anti_display_name":"z","anti_pdg_label":23,"canonical_id":"model:built-in-sm-leading-color:state:z","display_name":"z","orientation":"self-conjugate","pdg_label":23,"self_conjugate":true,"species_id":"model:built-in-sm-leading-color:species:z"},"mass_parameter":null,"states":[{"chirality":0,"helicity":-1,"spin_state":-1},{"chirality":0,"helicity":0,"spin_state":0},{"chirality":0,"helicity":1,"spin_state":1}],"statistics":"boson","wavefunction_family":"vector","width_parameter":null},"source_kind":"external-wavefunction","source_orientation":"self-conjugate","source_parameter_start":12,"source_parameter_stop":16,"spin_state":0,"value_slot":{"component_start":12,"component_stop":16,"current_id":5,"dimension":4,"value_slot_id":5,"variant":"source"},"wavefunction_kind":"vector"},{"anti_particle_id":23,"applied_crossing":{"chirality_factor":1,"helicity_factor":1,"momentum_transform":"identity","phase":[1.0,0.0],"spin_state_factor":1},"chirality":0,"color_state":{"accuracy":"lc","basis_key":[],"line_groups":[],"sector_id":0},"crossing":"identity","current_component_start":16,"current_component_stop":20,"current_id":6,"dimension":4,"helicity_ancestry":"0x40","input_momentum_slot":2,"leg_label":3,"outgoing_pdg":23,"particle_id":23,"physical_pdg":23,"side":"final","source_basis":"lorentz-vector","source_helicity":1,"source_id":6,"source_ir":{"basis":"lorentz-vector","component_dimension":4,"crossing":{"chirality_factor":1,"helicity_factor":-1,"momentum_transform":"negate-four-momentum","phase":[1.0,0.0],"spin_state_factor":-1},"identity":{"anti_canonical_id":"model:built-in-sm-leading-color:state:z","anti_display_name":"z","anti_pdg_label":23,"canonical_id":"model:built-in-sm-leading-color:state:z","display_name":"z","orientation":"self-conjugate","pdg_label":23,"self_conjugate":true,"species_id":"model:built-in-sm-leading-color:species:z"},"mass_parameter":null,"states":[{"chirality":0,"helicity":-1,"spin_state":-1},{"chirality":0,"helicity":0,"spin_state":0},{"chirality":0,"helicity":1,"spin_state":1}],"statistics":"boson","wavefunction_family":"vector","width_parameter":null},"source_kind":"external-wavefunction","source_orientation":"self-conjugate","source_parameter_start":16,"source_parameter_stop":20,"spin_state":1,"value_slot":{"component_start":16,"component_stop":20,"current_id":6,"dimension":4,"value_slot_id":6,"variant":"source"},"wavefunction_kind":"vector"}]},"stages":[{"input_current_ids":[2,3,4,5,6],"input_value_slot_ids":[2,3,4,5,6],"interaction_count":6,"interaction_ids":[0,1,2,3,4,5],"interactions":[],"interactions_compacted":true,"output_current_ids":[7,8,9,10,11,12],"output_value_slot_ids":[7,8,9,10,11,12],"stage_index":1,"stage_kind":"current-combine","subset_size":2}],"value_storage":{"component_count":32,"metadata_compacted":true,"number_type":"complex","value_slots":[{"applies_propagator":false,"chirality":1,"component_start":0,"component_stop":2,"current_component_start":0,"current_component_stop":2,"current_id":0,"dimension":2,"external_labels":[1],"external_mask":1,"is_source":true,"momentum_mask":1,"particle_id":-1,"propagator":{"applies_propagator":true,"auxiliary_policy":null,"backend":"spenso","basis":"weyl-chiral","chirality":1,"custom_source":null,"denominator":"momentum_squared","description":"massless Weyl fermion propagator","full_tensor_network_ready":true,"gauge":null,"goldstone_policy":"not-applicable","identity":{"anti_canonical_id":"model:built-in-sm-leading-color:state:d","anti_display_name":"d","anti_pdg_label":1,"canonical_id":"model:built-in-sm-leading-color:state:d~","display_name":"d~","orientation":"antiparticle","pdg_label":-1,"self_conjugate":false,"species_id":"model:built-in-sm-leading-color:species:d"},"kernel":"weyl_fermion","kind":"weyl-fermion","mass_class":"massless","mass_parameter":null,"numerator":"i*weyl_slash(momentum)","particle_id":-1,"width_parameter":null},"value_slot_id":0,"variant":"source"},{"applies_propagator":false,"chirality":-1,"component_start":2,"component_stop":4,"current_component_start":2,"current_component_stop":4,"current_id":1,"dimension":2,"external_labels":[1],"external_mask":1,"is_source":true,"momentum_mask":1,"particle_id":-1,"propagator":{"applies_propagator":true,"auxiliary_policy":null,"backend":"spenso","basis":"weyl-chiral","chirality":-1,"custom_source":null,"denominator":"momentum_squared","description":"massless Weyl fermion propagator","full_tensor_network_ready":true,"gauge":null,"goldstone_policy":"not-applicable","identity":{"anti_canonical_id":"model:built-in-sm-leading-color:state:d","anti_display_name":"d","anti_pdg_label":1,"canonical_id":"model:built-in-sm-leading-color:state:d~","display_name":"d~","orientation":"antiparticle","pdg_label":-1,"self_conjugate":false,"species_id":"model:built-in-sm-leading-color:species:d"},"kernel":"weyl_fermion","kind":"weyl-fermion","mass_class":"massless","mass_parameter":null,"numerator":"i*weyl_slash(momentum)","particle_id":-1,"width_parameter":null},"value_slot_id":1,"variant":"source"},{"applies_propagator":false,"chirality":1,"component_start":4,"component_stop":6,"current_component_start":4,"current_component_stop":6,"current_id":2,"dimension":2,"external_labels":[2],"external_mask":2,"is_source":true,"momentum_mask":2,"particle_id":1,"propagator":{"applies_propagator":true,"auxiliary_policy":null,"backend":"spenso","basis":"weyl-chiral","chirality":1,"custom_source":null,"denominator":"momentum_squared","description":"massless Weyl fermion propagator","full_tensor_network_ready":true,"gauge":null,"goldstone_policy":"not-applicable","identity":{"anti_canonical_id":"model:built-in-sm-leading-color:state:d~","anti_display_name":"d~","anti_pdg_label":-1,"canonical_id":"model:built-in-sm-leading-color:state:d","display_name":"d","orientation":"particle","pdg_label":1,"self_conjugate":false,"species_id":"model:built-in-sm-leading-color:species:d"},"kernel":"weyl_fermion","kind":"weyl-fermion","mass_class":"massless","mass_parameter":null,"numerator":"i*weyl_slash(momentum)","particle_id":1,"width_parameter":null},"value_slot_id":2,"variant":"source"},{"applies_propagator":false,"chirality":-1,"component_start":6,"component_stop":8,"current_component_start":6,"current_component_stop":8,"current_id":3,"dimension":2,"external_labels":[2],"external_mask":2,"is_source":true,"momentum_mask":2,"particle_id":1,"propagator":{"applies_propagator":true,"auxiliary_policy":null,"backend":"spenso","basis":"weyl-chiral","chirality":-1,"custom_source":null,"denominator":"momentum_squared","description":"massless Weyl fermion propagator","full_tensor_network_ready":true,"gauge":null,"goldstone_policy":"not-applicable","identity":{"anti_canonical_id":"model:built-in-sm-leading-color:state:d~","anti_display_name":"d~","anti_pdg_label":-1,"canonical_id":"model:built-in-sm-leading-color:state:d","display_name":"d","orientation":"particle","pdg_label":1,"self_conjugate":false,"species_id":"model:built-in-sm-leading-color:species:d"},"kernel":"weyl_fermion","kind":"weyl-fermion","mass_class":"massless","mass_parameter":null,"numerator":"i*weyl_slash(momentum)","particle_id":1,"width_parameter":null},"value_slot_id":3,"variant":"source"},{"applies_propagator":false,"chirality":0,"component_start":8,"component_stop":12,"current_component_start":8,"current_component_stop":12,"current_id":4,"dimension":4,"external_labels":[3],"external_mask":4,"is_source":true,"momentum_mask":4,"particle_id":23,"propagator":{"applies_propagator":true,"auxiliary_policy":null,"backend":"symbolica","basis":"lorentz-vector","chirality":0,"custom_source":null,"denominator":"momentum_squared-mass_squared+i*mass*width","description":"massive vector propagator with width","full_tensor_network_ready":true,"gauge":"unitary","goldstone_policy":"absorbed","identity":{"anti_canonical_id":"model:built-in-sm-leading-color:state:z","anti_display_name":"z","anti_pdg_label":23,"canonical_id":"model:built-in-sm-leading-color:state:z","display_name":"z","orientation":"self-conjugate","pdg_label":23,"self_conjugate":true,"species_id":"model:built-in-sm-leading-color:species:z"},"kernel":"massive_vector_unitary_gauge","kind":"vector","mass_class":"massive","mass_parameter":null,"numerator":"-i*(metric-momentum_outer/mass_squared)","particle_id":23,"width_parameter":null},"value_slot_id":4,"variant":"source"},{"applies_propagator":false,"chirality":0,"component_start":12,"component_stop":16,"current_component_start":12,"current_component_stop":16,"current_id":5,"dimension":4,"external_labels":[3],"external_mask":4,"is_source":true,"momentum_mask":4,"particle_id":23,"propagator":{"applies_propagator":true,"auxiliary_policy":null,"backend":"symbolica","basis":"lorentz-vector","chirality":0,"custom_source":null,"denominator":"momentum_squared-mass_squared+i*mass*width","description":"massive vector propagator with width","full_tensor_network_ready":true,"gauge":"unitary","goldstone_policy":"absorbed","identity":{"anti_canonical_id":"model:built-in-sm-leading-color:state:z","anti_display_name":"z","anti_pdg_label":23,"canonical_id":"model:built-in-sm-leading-color:state:z","display_name":"z","orientation":"self-conjugate","pdg_label":23,"self_conjugate":true,"species_id":"model:built-in-sm-leading-color:species:z"},"kernel":"massive_vector_unitary_gauge","kind":"vector","mass_class":"massive","mass_parameter":null,"numerator":"-i*(metric-momentum_outer/mass_squared)","particle_id":23,"width_parameter":null},"value_slot_id":5,"variant":"source"},{"applies_propagator":false,"chirality":0,"component_start":16,"component_stop":20,"current_component_start":16,"current_component_stop":20,"current_id":6,"dimension":4,"external_labels":[3],"external_mask":4,"is_source":true,"momentum_mask":4,"particle_id":23,"propagator":{"applies_propagator":true,"auxiliary_policy":null,"backend":"symbolica","basis":"lorentz-vector","chirality":0,"custom_source":null,"denominator":"momentum_squared-mass_squared+i*mass*width","description":"massive vector propagator with width","full_tensor_network_ready":true,"gauge":"unitary","goldstone_policy":"absorbed","identity":{"anti_canonical_id":"model:built-in-sm-leading-color:state:z","anti_display_name":"z","anti_pdg_label":23,"canonical_id":"model:built-in-sm-leading-color:state:z","display_name":"z","orientation":"self-conjugate","pdg_label":23,"self_conjugate":true,"species_id":"model:built-in-sm-leading-color:species:z"},"kernel":"massive_vector_unitary_gauge","kind":"vector","mass_class":"massive","mass_parameter":null,"numerator":"-i*(metric-momentum_outer/mass_squared)","particle_id":23,"width_parameter":null},"value_slot_id":6,"variant":"source"},{"applies_propagator":false,"chirality":1,"component_start":20,"component_stop":22,"current_component_start":20,"current_component_stop":22,"current_id":7,"dimension":2,"external_labels":[2,3],"external_mask":6,"is_source":false,"momentum_mask":6,"particle_id":1,"propagator":{"applies_propagator":true,"auxiliary_policy":null,"backend":"spenso","basis":"weyl-chiral","chirality":1,"custom_source":null,"denominator":"momentum_squared","description":"massless Weyl fermion propagator","full_tensor_network_ready":true,"gauge":null,"goldstone_policy":"not-applicable","identity":{"anti_canonical_id":"model:built-in-sm-leading-color:state:d~","anti_display_name":"d~","anti_pdg_label":-1,"canonical_id":"model:built-in-sm-leading-color:state:d","display_name":"d","orientation":"particle","pdg_label":1,"self_conjugate":false,"species_id":"model:built-in-sm-leading-color:species:d"},"kernel":"weyl_fermion","kind":"weyl-fermion","mass_class":"massless","mass_parameter":null,"numerator":"i*weyl_slash(momentum)","particle_id":1,"width_parameter":null},"value_slot_id":7,"variant":"unpropagated"},{"applies_propagator":false,"chirality":1,"component_start":22,"component_stop":24,"current_component_start":22,"current_component_stop":24,"current_id":8,"dimension":2,"external_labels":[2,3],"external_mask":6,"is_source":false,"momentum_mask":6,"particle_id":1,"propagator":{"applies_propagator":true,"auxiliary_policy":null,"backend":"spenso","basis":"weyl-chiral","chirality":1,"custom_source":null,"denominator":"momentum_squared","description":"massless Weyl fermion propagator","full_tensor_network_ready":true,"gauge":null,"goldstone_policy":"not-applicable","identity":{"anti_canonical_id":"model:built-in-sm-leading-color:state:d~","anti_display_name":"d~","anti_pdg_label":-1,"canonical_id":"model:built-in-sm-leading-color:state:d","display_name":"d","orientation":"particle","pdg_label":1,"self_conjugate":false,"species_id":"model:built-in-sm-leading-color:species:d"},"kernel":"weyl_fermion","kind":"weyl-fermion","mass_class":"massless","mass_parameter":null,"numerator":"i*weyl_slash(momentum)","particle_id":1,"width_parameter":null},"value_slot_id":8,"variant":"unpropagated"},{"applies_propagator":false,"chirality":1,"component_start":24,"component_stop":26,"current_component_start":24,"current_component_stop":26,"current_id":9,"dimension":2,"external_labels":[2,3],"external_mask":6,"is_source":false,"momentum_mask":6,"particle_id":1,"propagator":{"applies_propagator":true,"auxiliary_policy":null,"backend":"spenso","basis":"weyl-chiral","chirality":1,"custom_source":null,"denominator":"momentum_squared","description":"massless Weyl fermion propagator","full_tensor_network_ready":true,"gauge":null,"goldstone_policy":"not-applicable","identity":{"anti_canonical_id":"model:built-in-sm-leading-color:state:d~","anti_display_name":"d~","anti_pdg_label":-1,"canonical_id":"model:built-in-sm-leading-color:state:d","display_name":"d","orientation":"particle","pdg_label":1,"self_conjugate":false,"species_id":"model:built-in-sm-leading-color:species:d"},"kernel":"weyl_fermion","kind":"weyl-fermion","mass_class":"massless","mass_parameter":null,"numerator":"i*weyl_slash(momentum)","particle_id":1,"width_parameter":null},"value_slot_id":9,"variant":"unpropagated"},{"applies_propagator":false,"chirality":-1,"component_start":26,"component_stop":28,"current_component_start":26,"current_component_stop":28,"current_id":10,"dimension":2,"external_labels":[2,3],"external_mask":6,"is_source":false,"momentum_mask":6,"particle_id":1,"propagator":{"applies_propagator":true,"auxiliary_policy":null,"backend":"spenso","basis":"weyl-chiral","chirality":-1,"custom_source":null,"denominator":"momentum_squared","description":"massless Weyl fermion propagator","full_tensor_network_ready":true,"gauge":null,"goldstone_policy":"not-applicable","identity":{"anti_canonical_id":"model:built-in-sm-leading-color:state:d~","anti_display_name":"d~","anti_pdg_label":-1,"canonical_id":"model:built-in-sm-leading-color:state:d","display_name":"d","orientation":"particle","pdg_label":1,"self_conjugate":false,"species_id":"model:built-in-sm-leading-color:species:d"},"kernel":"weyl_fermion","kind":"weyl-fermion","mass_class":"massless","mass_parameter":null,"numerator":"i*weyl_slash(momentum)","particle_id":1,"width_parameter":null},"value_slot_id":10,"variant":"unpropagated"},{"applies_propagator":false,"chirality":-1,"component_start":28,"component_stop":30,"current_component_start":28,"current_component_stop":30,"current_id":11,"dimension":2,"external_labels":[2,3],"external_mask":6,"is_source":false,"momentum_mask":6,"particle_id":1,"propagator":{"applies_propagator":true,"auxiliary_policy":null,"backend":"spenso","basis":"weyl-chiral","chirality":-1,"custom_source":null,"denominator":"momentum_squared","description":"massless Weyl fermion propagator","full_tensor_network_ready":true,"gauge":null,"goldstone_policy":"not-applicable","identity":{"anti_canonical_id":"model:built-in-sm-leading-color:state:d~","anti_display_name":"d~","anti_pdg_label":-1,"canonical_id":"model:built-in-sm-leading-color:state:d","display_name":"d","orientation":"particle","pdg_label":1,"self_conjugate":false,"species_id":"model:built-in-sm-leading-color:species:d"},"kernel":"weyl_fermion","kind":"weyl-fermion","mass_class":"massless","mass_parameter":null,"numerator":"i*weyl_slash(momentum)","particle_id":1,"width_parameter":null},"value_slot_id":11,"variant":"unpropagated"},{"applies_propagator":false,"chirality":-1,"component_start":30,"component_stop":32,"current_component_start":30,"current_component_stop":32,"current_id":12,"dimension":2,"external_labels":[2,3],"external_mask":6,"is_source":false,"momentum_mask":6,"particle_id":1,"propagator":{"applies_propagator":true,"auxiliary_policy":null,"backend":"spenso","basis":"weyl-chiral","chirality":-1,"custom_source":null,"denominator":"momentum_squared","description":"massless Weyl fermion propagator","full_tensor_network_ready":true,"gauge":null,"goldstone_policy":"not-applicable","identity":{"anti_canonical_id":"model:built-in-sm-leading-color:state:d~","anti_display_name":"d~","anti_pdg_label":-1,"canonical_id":"model:built-in-sm-leading-color:state:d","display_name":"d","orientation":"particle","pdg_label":1,"self_conjugate":false,"species_id":"model:built-in-sm-leading-color:species:d"},"kernel":"weyl_fermion","kind":"weyl-fermion","mass_class":"massless","mass_parameter":null,"numerator":"i*weyl_slash(momentum)","particle_id":1,"width_parameter":null},"value_slot_id":12,"variant":"unpropagated"}]}},"schema_version":3},"key":"d_dbar_to_z","kind":"pyamplicol-runtime-execution","materialization_census":{"abi":"pyamplicol-fully-resident-materialization-census-v1","basis":"immutable-fully-resident-compiled-dag","final":{"amplitude_root_count":2,"current_count":7,"interaction_count":2,"interaction_evaluation_count":2,"source_count":5},"final_equals_peak":true,"peak":{"amplitude_root_count":2,"current_count":7,"interaction_count":2,"interaction_evaluation_count":2,"source_count":5}},"process":"d d~ > z","required_runtime_capabilities":["compiled-plane-arena-v1","rusticol.compiled.color-topology-lanes.v1","rusticol.compiled.helicity-dual-lane.v1","rusticol.compiled.helicity-selector-union.v1","rusticol.compiled.runtime-selectors.v1","symjit.application.complex-f64.v1"],"runtime_schema":{"amplitude_stage":{"color_contraction":null,"color_topology_replay":null,"output_count":2,"roots":[{"all_sector_weight":1.0,"coherent_group_id":0,"color_sector_id":0,"color_weight":[1.0,0.0],"contraction":"weyl","contraction_ir":{"chirality_relation":"opposite","coefficients":[[1.0,0.0],[1.0,0.0]],"left_basis":"weyl-chiral","metric_signature":null,"name":"weyl","right_basis":"weyl-chiral"},"coupling":[1.0,0.0],"helicity_weight":1.0,"kind":"direct-contraction","left_current_id":5,"left_slot":{"component_start":12,"component_stop":14,"current_id":5,"dimension":2},"left_value_slot":{"component_start":12,"component_stop":14,"current_id":5,"dimension":2,"value_slot_id":5,"variant":"unpropagated"},"output_index":0,"right_current_id":1,"right_slot":{"component_start":2,"component_stop":4,"current_id":1,"dimension":2},"right_value_slot":{"component_start":2,"component_stop":4,"current_id":1,"dimension":2,"value_slot_id":1,"variant":"source"},"root_id":0,"vertex_kind":null,"vertex_particles":null},{"all_sector_weight":1.0,"coherent_group_id":1,"color_sector_id":0,"color_weight":[1.0,0.0],"contraction":"weyl","contraction_ir":{"chirality_relation":"opposite","coefficients":[[1.0,0.0],[1.0,0.0]],"left_basis":"weyl-chiral","metric_signature":null,"name":"weyl","right_basis":"weyl-chiral"},"coupling":[1.0,0.0],"helicity_weight":1.0,"kind":"direct-contraction","left_current_id":6,"left_slot":{"component_start":14,"component_stop":16,"current_id":6,"dimension":2},"left_value_slot":{"component_start":14,"component_stop":16,"current_id":6,"dimension":2,"value_slot_id":6,"variant":"unpropagated"},"output_index":1,"right_current_id":0,"right_slot":{"component_start":0,"component_stop":2,"current_id":0,"dimension":2},"right_value_slot":{"component_start":0,"component_stop":2,"current_id":0,"dimension":2,"value_slot_id":0,"variant":"source"},"root_id":1,"vertex_kind":null,"vertex_particles":null}],"stage_kind":"amplitude-roots"},"current_storage":{"component_count":16,"current_slots":[{"auxiliary_kind":null,"chirality":1,"color_state":{"accuracy":"lc","basis_key":[],"line_groups":[0],"sector_id":0},"component_start":0,"component_stop":2,"current_id":0,"dimension":2,"external_labels":[1],"external_mask":1,"flavour_flow":[-1],"helicity_ancestry":"0x1","is_source":true,"momentum_mask":1,"particle_id":-1,"spin_state":1},{"auxiliary_kind":null,"chirality":-1,"color_state":{"accuracy":"lc","basis_key":[],"line_groups":[0],"sector_id":0},"component_start":2,"component_stop":4,"current_id":1,"dimension":2,"external_labels":[1],"external_mask":1,"flavour_flow":[-1],"helicity_ancestry":"0x2","is_source":true,"momentum_mask":1,"particle_id":-1,"spin_state":-1},{"auxiliary_kind":null,"chirality":1,"color_state":{"accuracy":"lc","basis_key":[],"line_groups":[0],"sector_id":0},"component_start":4,"component_stop":6,"current_id":2,"dimension":2,"external_labels":[2],"external_mask":2,"flavour_flow":[1],"helicity_ancestry":"0x4","is_source":true,"momentum_mask":2,"particle_id":1,"spin_state":1},{"auxiliary_kind":null,"chirality":-1,"color_state":{"accuracy":"lc","basis_key":[],"line_groups":[0],"sector_id":0},"component_start":6,"component_stop":8,"current_id":3,"dimension":2,"external_labels":[2],"external_mask":2,"flavour_flow":[1],"helicity_ancestry":"0x8","is_source":true,"momentum_mask":2,"particle_id":1,"spin_state":-1},{"auxiliary_kind":null,"chirality":0,"color_state":{"accuracy":"lc","basis_key":[],"line_groups":[],"sector_id":0},"component_start":8,"component_stop":12,"current_id":4,"dimension":4,"external_labels":[3],"external_mask":4,"flavour_flow":[23],"helicity_ancestry":"0x10","is_source":true,"momentum_mask":4,"particle_id":23,"spin_state":-1},{"auxiliary_kind":null,"chirality":1,"color_state":{"accuracy":"lc","basis_key":[],"line_groups":[0],"sector_id":0},"component_start":12,"component_stop":14,"current_id":5,"dimension":2,"external_labels":[2,3],"external_mask":6,"flavour_flow":[1],"helicity_ancestry":"0x14","is_source":false,"momentum_mask":6,"particle_id":1,"spin_state":1},{"auxiliary_kind":null,"chirality":-1,"color_state":{"accuracy":"lc","basis_key":[],"line_groups":[0],"sector_id":0},"component_start":14,"component_stop":16,"current_id":6,"dimension":2,"external_labels":[2,3],"external_mask":6,"flavour_flow":[1],"helicity_ancestry":"0x18","is_source":false,"momentum_mask":6,"particle_id":1,"spin_state":-1}],"metadata_compacted":true,"number_type":"complex"},"external_particles":[{"index":0,"label":1,"momentum_slot":0,"outgoing_pdg":-1,"pdg":1,"role":"initial"},{"index":1,"label":2,"momentum_slot":1,"outgoing_pdg":1,"pdg":-1,"role":"initial"},{"index":2,"label":3,"momentum_slot":2,"outgoing_pdg":23,"pdg":23,"role":"final"}],"helicity_recurrence":{"amplitude_classes":[{"class_id":"helicity-amplitude-sha256:d56fc183f4e6996740930cb59d082773bc739fdc54de6c684a44b2d643911365","members":[{"factor":[1.0,0.0],"root_id":0,"selector_domain_ids":[8]},{"factor":[1.0,0.0],"root_id":1,"selector_domain_ids":[9]},{"factor":[1.0,0.0],"root_id":2,"selector_domain_ids":[10]}],"proof":{"algorithm":"canonical-source-transition-dependency-shape-v1","digest":"d56fc183f4e6996740930cb59d082773bc739fdc54de6c684a44b2d643911365","status":"proven","transition_contract_ids":[]},"representative_root_id":0},{"class_id":"helicity-amplitude-sha256:19760a2fbd284e1f9c6b8d22028a99181ead6fa6e4437c0c42786703785a310a","members":[{"factor":[1.0,0.0],"root_id":3,"selector_domain_ids":[4]},{"factor":[1.0,0.0],"root_id":4,"selector_domain_ids":[5]},{"factor":[1.0,0.0],"root_id":5,"selector_domain_ids":[6]}],"proof":{"algorithm":"canonical-source-transition-dependency-shape-v1","digest":"19760a2fbd284e1f9c6b8d22028a99181ead6fa6e4437c0c42786703785a310a","status":"proven","transition_contract_ids":[]},"representative_root_id":3}],"amplitude_root_count":6,"contract_version":1,"current_count":13,"diagnostics":[],"kind":"pyamplicol-helicity-recurrence","materialization":{"amplitude_routes":[{"factor":[1.0,0.0],"materialized_root_id":0,"residual":false,"selector_domain_ids":[8]},{"factor":[1.0,0.0],"materialized_root_id":0,"residual":false,"selector_domain_ids":[9]},{"factor":[1.0,0.0],"materialized_root_id":0,"residual":false,"selector_domain_ids":[10]},{"factor":[1.0,0.0],"materialized_root_id":1,"residual":false,"selector_domain_ids":[4]},{"factor":[1.0,0.0],"materialized_root_id":1,"residual":false,"selector_domain_ids":[5]},{"factor":[1.0,0.0],"materialized_root_id":1,"residual":false,"selector_domain_ids":[6]}],"contract_version":1,"kind":"pyamplicol-helicity-recurrence-materialization","materialized_current_count":7,"materialized_root_count":2,"proof_current_count":13,"proof_root_count":6,"proof_to_materialized_current":[0,1,2,3,4,4,4,5,5,5,6,6,6],"selector_schedules":[{"active_current_ids":[],"active_root_ids":[],"selector_domain_id":1,"structural_zero":true},{"active_current_ids":[],"active_root_ids":[],"selector_domain_id":2,"structural_zero":true},{"active_current_ids":[],"active_root_ids":[],"selector_domain_id":3,"structural_zero":true},{"active_current_ids":[0,3,4,6],"active_root_ids":[1],"selector_domain_id":4,"structural_zero":false},{"active_current_ids":[0,3,4,6],"active_root_ids":[1],"selector_domain_id":5,"structural_zero":false},{"active_current_ids":[0,3,4,6],"active_root_ids":[1],"selector_domain_id":6,"structural_zero":false},{"active_current_ids":[1,2,4,5],"active_root_ids":[0],"selector_domain_id":8,"structural_zero":false},{"active_current_ids":[1,2,4,5],"active_root_ids":[0],"selector_domain_id":9,"structural_zero":false},{"active_current_ids":[1,2,4,5],"active_root_ids":[0],"selector_domain_id":10,"structural_zero":false},{"active_current_ids":[],"active_root_ids":[],"selector_domain_id":11,"structural_zero":true},{"active_current_ids":[],"active_root_ids":[],"selector_domain_id":12,"structural_zero":true},{"active_current_ids":[],"active_root_ids":[],"selector_domain_id":13,"structural_zero":true}],"source_routes":[{"chirality":1,"declared_state_index":0,"external_label":1,"factor":[1.0,0.0],"helicity":-1,"materialized_current_id":0,"selector_domain_id":0,"spin_state":1},{"chirality":-1,"declared_state_index":1,"external_label":1,"factor":[1.0,0.0],"helicity":1,"materialized_current_id":1,"selector_domain_id":7,"spin_state":-1},{"chirality":1,"declared_state_index":0,"external_label":2,"factor":[1.0,0.0],"helicity":-1,"materialized_current_id":2,"selector_domain_id":14,"spin_state":1},{"chirality":-1,"declared_state_index":1,"external_label":2,"factor":[1.0,0.0],"helicity":1,"materialized_current_id":3,"selector_domain_id":18,"spin_state":-1},{"chirality":0,"declared_state_index":0,"external_label":3,"factor":[1.0,0.0],"helicity":-1,"materialized_current_id":4,"selector_domain_id":22,"spin_state":-1},{"chirality":0,"declared_state_index":1,"external_label":3,"factor":[1.0,0.0],"helicity":0,"materialized_current_id":4,"selector_domain_id":23,"spin_state":0},{"chirality":0,"declared_state_index":2,"external_label":3,"factor":[1.0,0.0],"helicity":1,"materialized_current_id":4,"selector_domain_id":24,"spin_state":1}],"strategy":"quotient"},"proof_algorithm":"canonical-source-transition-dependency-shape-v1","proof_counts":{"amplitude_class_count":2,"optimized_amplitude_class_count":2,"optimized_current_count":9,"optimized_recurrence_class_count":3,"physical_helicity_count":12,"recurrence_class_count":7,"residual_amplitude_count":0,"residual_current_count":0,"source_state_mapping_count":7,"structural_zero_helicity_count":6},"recurrence_classes":[{"class_id":"helicity-current-sha256:8c682736c4ae8e56465e5fd7b1c300f8379118af92486f9a571ca3a915572ccf","external_labels":[1],"members":[{"current_id":0,"factor":[1.0,0.0],"selector_domain_id":0}],"proof":{"algorithm":"canonical-source-transition-dependency-shape-v1","digest":"8c682736c4ae8e56465e5fd7b1c300f8379118af92486f9a571ca3a915572ccf","status":"proven","transition_contract_ids":[]},"representative_current_id":0,"source_class":true},{"class_id":"helicity-current-sha256:20f4ac2ac00a74044681ff5aab37d2abc31e375d1473e8b0a8198af4432c0ad4","external_labels":[1],"members":[{"current_id":1,"factor":[1.0,0.0],"selector_domain_id":7}],"proof":{"algorithm":"canonical-source-transition-dependency-shape-v1","digest":"20f4ac2ac00a74044681ff5aab37d2abc31e375d1473e8b0a8198af4432c0ad4","status":"proven","transition_contract_ids":[]},"representative_current_id":1,"source_class":true},{"class_id":"helicity-current-sha256:099ab31b76e6d55df90ac39798446ac28b3bbca173b59d516940595c4f951ec7","external_labels":[2],"members":[{"current_id":2,"factor":[1.0,0.0],"selector_domain_id":14}],"proof":{"algorithm":"canonical-source-transition-dependency-shape-v1","digest":"099ab31b76e6d55df90ac39798446ac28b3bbca173b59d516940595c4f951ec7","status":"proven","transition_contract_ids":[]},"representative_current_id":2,"source_class":true},{"class_id":"helicity-current-sha256:1e587b1494ff92be5c28fa63676222417931b6d82f93eb12232f90e10bc22924","external_labels":[2],"members":[{"current_id":3,"factor":[1.0,0.0],"selector_domain_id":18}],"proof":{"algorithm":"canonical-source-transition-dependency-shape-v1","digest":"1e587b1494ff92be5c28fa63676222417931b6d82f93eb12232f90e10bc22924","status":"proven","transition_contract_ids":[]},"representative_current_id":3,"source_class":true},{"class_id":"helicity-current-sha256:e9562e75376f52432b31f6754f778a8f5ab63333f8169428a2bbb50dcee492d0","external_labels":[3],"members":[{"current_id":4,"factor":[1.0,0.0],"selector_domain_id":22},{"current_id":5,"factor":[1.0,0.0],"selector_domain_id":23},{"current_id":6,"factor":[1.0,0.0],"selector_domain_id":24}],"proof":{"algorithm":"canonical-source-transition-dependency-shape-v1","digest":"e9562e75376f52432b31f6754f778a8f5ab63333f8169428a2bbb50dcee492d0","status":"proven","transition_contract_ids":[]},"representative_current_id":4,"source_class":true},{"class_id":"helicity-current-sha256:11b0f53aeaf5619f44673e1ac3a81db4742588d5f49c08fa7f514b0556bcada7","external_labels":[2,3],"members":[{"current_id":7,"factor":[1.0,0.0],"selector_domain_id":15},{"current_id":8,"factor":[1.0,0.0],"selector_domain_id":16},{"current_id":9,"factor":[1.0,0.0],"selector_domain_id":17}],"proof":{"algorithm":"canonical-source-transition-dependency-shape-v1","digest":"11b0f53aeaf5619f44673e1ac3a81db4742588d5f49c08fa7f514b0556bcada7","status":"proven","transition_contract_ids":["builtin-sm:fermion-vector-to-fermion"]},"representative_current_id":7,"source_class":false},{"class_id":"helicity-current-sha256:7690f565a16395860f30f2971bfea1c77a8db9d3e60d91d4e6b2d2f5549153a8","external_labels":[2,3],"members":[{"current_id":10,"factor":[1.0,0.0],"selector_domain_id":19},{"current_id":11,"factor":[1.0,0.0],"selector_domain_id":20},{"current_id":12,"factor":[1.0,0.0],"selector_domain_id":21}],"proof":{"algorithm":"canonical-source-transition-dependency-shape-v1","digest":"7690f565a16395860f30f2971bfea1c77a8db9d3e60d91d4e6b2d2f5549153a8","status":"proven","transition_contract_ids":["builtin-sm:fermion-vector-to-fermion"]},"representative_current_id":10,"source_class":false}],"residual_current_ids":[],"residual_root_ids":[],"selector_domains":[{"complete":false,"id":0,"source_states":[{"external_label":1,"helicity":-1}]},{"complete":true,"id":1,"source_states":[{"external_label":1,"helicity":-1},{"external_label":2,"helicity":-1},{"external_label":3,"helicity":-1}]},{"complete":true,"id":2,"source_states":[{"external_label":1,"helicity":-1},{"external_label":2,"helicity":-1},{"external_label":3,"helicity":0}]},{"complete":true,"id":3,"source_states":[{"external_label":1,"helicity":-1},{"external_label":2,"helicity":-1},{"external_label":3,"helicity":1}]},{"complete":true,"id":4,"source_states":[{"external_label":1,"helicity":-1},{"external_label":2,"helicity":1},{"external_label":3,"helicity":-1}]},{"complete":true,"id":5,"source_states":[{"external_label":1,"helicity":-1},{"external_label":2,"helicity":1},{"external_label":3,"helicity":0}]},{"complete":true,"id":6,"source_states":[{"external_label":1,"helicity":-1},{"external_label":2,"helicity":1},{"external_label":3,"helicity":1}]},{"complete":false,"id":7,"source_states":[{"external_label":1,"helicity":1}]},{"complete":true,"id":8,"source_states":[{"external_label":1,"helicity":1},{"external_label":2,"helicity":-1},{"external_label":3,"helicity":-1}]},{"complete":true,"id":9,"source_states":[{"external_label":1,"helicity":1},{"external_label":2,"helicity":-1},{"external_label":3,"helicity":0}]},{"complete":true,"id":10,"source_states":[{"external_label":1,"helicity":1},{"external_label":2,"helicity":-1},{"external_label":3,"helicity":1}]},{"complete":true,"id":11,"source_states":[{"external_label":1,"helicity":1},{"external_label":2,"helicity":1},{"external_label":3,"helicity":-1}]},{"complete":true,"id":12,"source_states":[{"external_label":1,"helicity":1},{"external_label":2,"helicity":1},{"external_label":3,"helicity":0}]},{"complete":true,"id":13,"source_states":[{"external_label":1,"helicity":1},{"external_label":2,"helicity":1},{"external_label":3,"helicity":1}]},{"complete":false,"id":14,"source_states":[{"external_label":2,"helicity":-1}]},{"complete":false,"id":15,"source_states":[{"external_label":2,"helicity":-1},{"external_label":3,"helicity":-1}]},{"complete":false,"id":16,"source_states":[{"external_label":2,"helicity":-1},{"external_label":3,"helicity":0}]},{"complete":false,"id":17,"source_states":[{"external_label":2,"helicity":-1},{"external_label":3,"helicity":1}]},{"complete":false,"id":18,"source_states":[{"external_label":2,"helicity":1}]},{"complete":false,"id":19,"source_states":[{"external_label":2,"helicity":1},{"external_label":3,"helicity":-1}]},{"complete":false,"id":20,"source_states":[{"external_label":2,"helicity":1},{"external_label":3,"helicity":0}]},{"complete":false,"id":21,"source_states":[{"external_label":2,"helicity":1},{"external_label":3,"helicity":1}]},{"complete":false,"id":22,"source_states":[{"external_label":3,"helicity":-1}]},{"complete":false,"id":23,"source_states":[{"external_label":3,"helicity":0}]},{"complete":false,"id":24,"source_states":[{"external_label":3,"helicity":1}]}],"source_state_mappings":[{"chirality":1,"current_id":0,"declared_state_index":0,"external_label":1,"factor":[1.0,0.0],"helicity":-1,"recurrence_class_id":"helicity-current-sha256:8c682736c4ae8e56465e5fd7b1c300f8379118af92486f9a571ca3a915572ccf","representative_current_id":0,"selector_domain_id":0,"source_contract_digest":"44fa57cb6571ac7524f9e30554d29cf20f6e4fbdfd443edf0afdbc8617c0620e","spin_state":1},{"chirality":-1,"current_id":1,"declared_state_index":1,"external_label":1,"factor":[1.0,0.0],"helicity":1,"recurrence_class_id":"helicity-current-sha256:20f4ac2ac00a74044681ff5aab37d2abc31e375d1473e8b0a8198af4432c0ad4","representative_current_id":1,"selector_domain_id":7,"source_contract_digest":"44fa57cb6571ac7524f9e30554d29cf20f6e4fbdfd443edf0afdbc8617c0620e","spin_state":-1},{"chirality":1,"current_id":2,"declared_state_index":0,"external_label":2,"factor":[1.0,0.0],"helicity":-1,"recurrence_class_id":"helicity-current-sha256:099ab31b76e6d55df90ac39798446ac28b3bbca173b59d516940595c4f951ec7","representative_current_id":2,"selector_domain_id":14,"source_contract_digest":"98b493b16090bfd79101115ca8697310721cebf636cced331f90bcbc32cd9ef6","spin_state":1},{"chirality":-1,"current_id":3,"declared_state_index":1,"external_label":2,"factor":[1.0,0.0],"helicity":1,"recurrence_class_id":"helicity-current-sha256:1e587b1494ff92be5c28fa63676222417931b6d82f93eb12232f90e10bc22924","representative_current_id":3,"selector_domain_id":18,"source_contract_digest":"98b493b16090bfd79101115ca8697310721cebf636cced331f90bcbc32cd9ef6","spin_state":-1},{"chirality":0,"current_id":4,"declared_state_index":0,"external_label":3,"factor":[1.0,0.0],"helicity":-1,"recurrence_class_id":"helicity-current-sha256:e9562e75376f52432b31f6754f778a8f5ab63333f8169428a2bbb50dcee492d0","representative_current_id":4,"selector_domain_id":22,"source_contract_digest":"c2bcedb500cc3f9692e3626cf25b332071eba27ad54025db1ddeaa31e0b57124","spin_state":-1},{"chirality":0,"current_id":5,"declared_state_index":1,"external_label":3,"factor":[1.0,0.0],"helicity":0,"recurrence_class_id":"helicity-current-sha256:e9562e75376f52432b31f6754f778a8f5ab63333f8169428a2bbb50dcee492d0","representative_current_id":4,"selector_domain_id":23,"source_contract_digest":"c2bcedb500cc3f9692e3626cf25b332071eba27ad54025db1ddeaa31e0b57124","spin_state":0},{"chirality":0,"current_id":6,"declared_state_index":2,"external_label":3,"factor":[1.0,0.0],"helicity":1,"recurrence_class_id":"helicity-current-sha256:e9562e75376f52432b31f6754f778a8f5ab63333f8169428a2bbb50dcee492d0","representative_current_id":4,"selector_domain_id":24,"source_contract_digest":"c2bcedb500cc3f9692e3626cf25b332071eba27ad54025db1ddeaa31e0b57124","spin_state":1}],"structural_zero_selector_domain_ids":[1,2,3,11,12,13]},"kind":"pyamplicol-runtime-execution-plan","model":{"particles":[{"mass":0.0,"mass_parameter":null,"pdg":1},{"mass":91.188,"mass_parameter":"particle.23.mass","pdg":23}]},"model_parameters":[{"default":0.0075467711139788835,"kind":"normalization","name":"normalization.alpha_ew","parameter_index":0},{"default":0.118,"kind":"normalization","name":"normalization.alpha_s_me_check","parameter_index":1},{"default":173.0,"kind":"particle_mass","name":"particle.6.mass","parameter_index":2,"pdg":6},{"default":1.4915,"kind":"particle_width","name":"particle.6.width","parameter_index":3,"pdg":6},{"default":91.188,"kind":"particle_mass","name":"particle.23.mass","parameter_index":4,"pdg":23},{"default":2.441404,"kind":"particle_width","name":"particle.23.width","parameter_index":5,"pdg":23},{"default":80.41900244575616,"kind":"particle_mass","name":"particle.24.mass","parameter_index":6,"pdg":24},{"default":2.0476,"kind":"particle_width","name":"particle.24.width","parameter_index":7,"pdg":24},{"default":125.0,"kind":"particle_mass","name":"particle.25.mass","parameter_index":8,"pdg":25},{"default":0.006382339,"kind":"particle_width","name":"particle.25.width","parameter_index":9,"pdg":25},{"default":-1.0244420275940371,"kind":"coupling_component","name":"coupling.10.1_23_1.component_0","parameter_index":10},{"default":0.17818666745287456,"kind":"coupling_component","name":"coupling.10.1_23_1.component_1","parameter_index":11}],"momentum_slots":[{"component_start":0,"component_stop":4,"external_labels":[1],"momentum_mask":1,"momentum_slot_id":0,"real_valued":true},{"component_start":4,"component_stop":8,"external_labels":[2],"momentum_mask":2,"momentum_slot_id":1,"real_valued":true},{"component_start":8,"component_stop":12,"external_labels":[3],"momentum_mask":4,"momentum_slot_id":2,"real_valued":true},{"component_start":12,"component_stop":16,"external_labels":[2,3],"momentum_mask":6,"momentum_slot_id":3,"real_valued":true}],"normalization":{"average_factor":36.0,"color_factor":3.0,"electroweak_coupling_power":1,"global_coupling_factor":0.18967104551999775,"identical_factor":1.0,"qcd_coupling_power":0},"parameter_layout":{"model_parameter_count":12,"momentum_components_real":true,"momentum_parameter_count":16,"parameter_count_if_flattened":40,"real_valued_inputs":[12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39],"source_component_parameter_count":12,"source_components_complex":true,"value_component_count":16},"process":"d d~ > z","process_key":"d_dbar_to_z","schema_version":3,"source_fill":{"source_count":5,"sources":[{"anti_particle_id":1,"applied_crossing":{"chirality_factor":-1,"helicity_factor":1,"momentum_transform":"negate-four-momentum","phase":[1.0,0.0],"spin_state_factor":-1},"chirality":1,"color_state":{"accuracy":"lc","basis_key":[],"line_groups":[0],"sector_id":0},"crossing":"negate-incoming-momentum","current_component_start":0,"current_component_stop":2,"current_id":0,"dimension":2,"helicity_ancestry":"0x1","input_momentum_slot":0,"leg_label":1,"outgoing_pdg":-1,"particle_id":-1,"physical_pdg":1,"side":"initial","source_basis":"weyl-chiral","source_helicity":-1,"source_id":0,"source_ir":{"basis":"weyl-chiral","component_dimension":2,"crossing":{"chirality_factor":-1,"helicity_factor":1,"momentum_transform":"negate-four-momentum","phase":[1.0,0.0],"spin_state_factor":-1},"identity":{"anti_canonical_id":"model:built-in-sm-leading-color:state:d","anti_display_name":"d","anti_pdg_label":1,"canonical_id":"model:built-in-sm-leading-color:state:d~","display_name":"d~","orientation":"antiparticle","pdg_label":-1,"self_conjugate":false,"species_id":"model:built-in-sm-leading-color:species:d"},"mass_parameter":null,"states":[{"chirality":-1,"helicity":-1,"spin_state":-1},{"chirality":1,"helicity":1,"spin_state":1}],"statistics":"fermion","wavefunction_family":"fermion","width_parameter":null},"source_kind":"external-wavefunction","source_orientation":"antiparticle","source_parameter_start":0,"source_parameter_stop":2,"spin_state":1,"value_slot":{"component_start":0,"component_stop":2,"current_id":0,"dimension":2,"value_slot_id":0,"variant":"source"},"wavefunction_kind":"fermion"},{"anti_particle_id":1,"applied_crossing":{"chirality_factor":-1,"helicity_factor":1,"momentum_transform":"negate-four-momentum","phase":[1.0,0.0],"spin_state_factor":-1},"chirality":-1,"color_state":{"accuracy":"lc","basis_key":[],"line_groups":[0],"sector_id":0},"crossing":"negate-incoming-momentum","current_component_start":2,"current_component_stop":4,"current_id":1,"dimension":2,"helicity_ancestry":"0x2","input_momentum_slot":0,"leg_label":1,"outgoing_pdg":-1,"particle_id":-1,"physical_pdg":1,"side":"initial","source_basis":"weyl-chiral","source_helicity":1,"source_id":1,"source_ir":{"basis":"weyl-chiral","component_dimension":2,"crossing":{"chirality_factor":-1,"helicity_factor":1,"momentum_transform":"negate-four-momentum","phase":[1.0,0.0],"spin_state_factor":-1},"identity":{"anti_canonical_id":"model:built-in-sm-leading-color:state:d","anti_display_name":"d","anti_pdg_label":1,"canonical_id":"model:built-in-sm-leading-color:state:d~","display_name":"d~","orientation":"antiparticle","pdg_label":-1,"self_conjugate":false,"species_id":"model:built-in-sm-leading-color:species:d"},"mass_parameter":null,"states":[{"chirality":-1,"helicity":-1,"spin_state":-1},{"chirality":1,"helicity":1,"spin_state":1}],"statistics":"fermion","wavefunction_family":"fermion","width_parameter":null},"source_kind":"external-wavefunction","source_orientation":"antiparticle","source_parameter_start":2,"source_parameter_stop":4,"spin_state":-1,"value_slot":{"component_start":2,"component_stop":4,"current_id":1,"dimension":2,"value_slot_id":1,"variant":"source"},"wavefunction_kind":"fermion"},{"anti_particle_id":-1,"applied_crossing":{"chirality_factor":-1,"helicity_factor":1,"momentum_transform":"negate-four-momentum","phase":[1.0,0.0],"spin_state_factor":-1},"chirality":1,"color_state":{"accuracy":"lc","basis_key":[],"line_groups":[0],"sector_id":0},"crossing":"negate-incoming-momentum","current_component_start":4,"current_component_stop":6,"current_id":2,"dimension":2,"helicity_ancestry":"0x4","input_momentum_slot":1,"leg_label":2,"outgoing_pdg":1,"particle_id":1,"physical_pdg":-1,"side":"initial","source_basis":"weyl-chiral","source_helicity":-1,"source_id":2,"source_ir":{"basis":"weyl-chiral","component_dimension":2,"crossing":{"chirality_factor":-1,"helicity_factor":1,"momentum_transform":"negate-four-momentum","phase":[1.0,0.0],"spin_state_factor":-1},"identity":{"anti_canonical_id":"model:built-in-sm-leading-color:state:d~","anti_display_name":"d~","anti_pdg_label":-1,"canonical_id":"model:built-in-sm-leading-color:state:d","display_name":"d","orientation":"particle","pdg_label":1,"self_conjugate":false,"species_id":"model:built-in-sm-leading-color:species:d"},"mass_parameter":null,"states":[{"chirality":-1,"helicity":-1,"spin_state":-1},{"chirality":1,"helicity":1,"spin_state":1}],"statistics":"fermion","wavefunction_family":"fermion","width_parameter":null},"source_kind":"external-wavefunction","source_orientation":"particle","source_parameter_start":4,"source_parameter_stop":6,"spin_state":1,"value_slot":{"component_start":4,"component_stop":6,"current_id":2,"dimension":2,"value_slot_id":2,"variant":"source"},"wavefunction_kind":"fermion"},{"anti_particle_id":-1,"applied_crossing":{"chirality_factor":-1,"helicity_factor":1,"momentum_transform":"negate-four-momentum","phase":[1.0,0.0],"spin_state_factor":-1},"chirality":-1,"color_state":{"accuracy":"lc","basis_key":[],"line_groups":[0],"sector_id":0},"crossing":"negate-incoming-momentum","current_component_start":6,"current_component_stop":8,"current_id":3,"dimension":2,"helicity_ancestry":"0x8","input_momentum_slot":1,"leg_label":2,"outgoing_pdg":1,"particle_id":1,"physical_pdg":-1,"side":"initial","source_basis":"weyl-chiral","source_helicity":1,"source_id":3,"source_ir":{"basis":"weyl-chiral","component_dimension":2,"crossing":{"chirality_factor":-1,"helicity_factor":1,"momentum_transform":"negate-four-momentum","phase":[1.0,0.0],"spin_state_factor":-1},"identity":{"anti_canonical_id":"model:built-in-sm-leading-color:state:d~","anti_display_name":"d~","anti_pdg_label":-1,"canonical_id":"model:built-in-sm-leading-color:state:d","display_name":"d","orientation":"particle","pdg_label":1,"self_conjugate":false,"species_id":"model:built-in-sm-leading-color:species:d"},"mass_parameter":null,"states":[{"chirality":-1,"helicity":-1,"spin_state":-1},{"chirality":1,"helicity":1,"spin_state":1}],"statistics":"fermion","wavefunction_family":"fermion","width_parameter":null},"source_kind":"external-wavefunction","source_orientation":"particle","source_parameter_start":6,"source_parameter_stop":8,"spin_state":-1,"value_slot":{"component_start":6,"component_stop":8,"current_id":3,"dimension":2,"value_slot_id":3,"variant":"source"},"wavefunction_kind":"fermion"},{"anti_particle_id":23,"applied_crossing":{"chirality_factor":1,"helicity_factor":1,"momentum_transform":"identity","phase":[1.0,0.0],"spin_state_factor":1},"chirality":0,"color_state":{"accuracy":"lc","basis_key":[],"line_groups":[],"sector_id":0},"crossing":"identity","current_component_start":8,"current_component_stop":12,"current_id":4,"dimension":4,"helicity_ancestry":"0x10","input_momentum_slot":2,"leg_label":3,"outgoing_pdg":23,"particle_id":23,"physical_pdg":23,"side":"final","source_basis":"lorentz-vector","source_helicity":-1,"source_id":4,"source_ir":{"basis":"lorentz-vector","component_dimension":4,"crossing":{"chirality_factor":1,"helicity_factor":-1,"momentum_transform":"negate-four-momentum","phase":[1.0,0.0],"spin_state_factor":-1},"identity":{"anti_canonical_id":"model:built-in-sm-leading-color:state:z","anti_display_name":"z","anti_pdg_label":23,"canonical_id":"model:built-in-sm-leading-color:state:z","display_name":"z","orientation":"self-conjugate","pdg_label":23,"self_conjugate":true,"species_id":"model:built-in-sm-leading-color:species:z"},"mass_parameter":null,"states":[{"chirality":0,"helicity":-1,"spin_state":-1},{"chirality":0,"helicity":0,"spin_state":0},{"chirality":0,"helicity":1,"spin_state":1}],"statistics":"boson","wavefunction_family":"vector","width_parameter":null},"source_kind":"external-wavefunction","source_orientation":"self-conjugate","source_parameter_start":8,"source_parameter_stop":12,"spin_state":-1,"value_slot":{"component_start":8,"component_stop":12,"current_id":4,"dimension":4,"value_slot_id":4,"variant":"source"},"wavefunction_kind":"vector"}]},"stages":[{"input_current_ids":[2,3,4],"input_value_slot_ids":[2,3,4],"interaction_count":2,"interaction_ids":[0,1],"interactions":[],"interactions_compacted":true,"output_current_ids":[5,6],"output_value_slot_ids":[5,6],"stage_index":1,"stage_kind":"current-combine","subset_size":2}],"value_storage":{"component_count":16,"metadata_compacted":true,"number_type":"complex","value_slots":[{"applies_propagator":false,"chirality":1,"component_start":0,"component_stop":2,"current_component_start":0,"current_component_stop":2,"current_id":0,"dimension":2,"external_labels":[1],"external_mask":1,"is_source":true,"momentum_mask":1,"particle_id":-1,"propagator":{"applies_propagator":true,"auxiliary_policy":null,"backend":"spenso","basis":"weyl-chiral","chirality":1,"custom_source":null,"denominator":"momentum_squared","description":"massless Weyl fermion propagator","full_tensor_network_ready":true,"gauge":null,"goldstone_policy":"not-applicable","identity":{"anti_canonical_id":"model:built-in-sm-leading-color:state:d","anti_display_name":"d","anti_pdg_label":1,"canonical_id":"model:built-in-sm-leading-color:state:d~","display_name":"d~","orientation":"antiparticle","pdg_label":-1,"self_conjugate":false,"species_id":"model:built-in-sm-leading-color:species:d"},"kernel":"weyl_fermion","kind":"weyl-fermion","mass_class":"massless","mass_parameter":null,"numerator":"i*weyl_slash(momentum)","particle_id":-1,"width_parameter":null},"value_slot_id":0,"variant":"source"},{"applies_propagator":false,"chirality":-1,"component_start":2,"component_stop":4,"current_component_start":2,"current_component_stop":4,"current_id":1,"dimension":2,"external_labels":[1],"external_mask":1,"is_source":true,"momentum_mask":1,"particle_id":-1,"propagator":{"applies_propagator":true,"auxiliary_policy":null,"backend":"spenso","basis":"weyl-chiral","chirality":-1,"custom_source":null,"denominator":"momentum_squared","description":"massless Weyl fermion propagator","full_tensor_network_ready":true,"gauge":null,"goldstone_policy":"not-applicable","identity":{"anti_canonical_id":"model:built-in-sm-leading-color:state:d","anti_display_name":"d","anti_pdg_label":1,"canonical_id":"model:built-in-sm-leading-color:state:d~","display_name":"d~","orientation":"antiparticle","pdg_label":-1,"self_conjugate":false,"species_id":"model:built-in-sm-leading-color:species:d"},"kernel":"weyl_fermion","kind":"weyl-fermion","mass_class":"massless","mass_parameter":null,"numerator":"i*weyl_slash(momentum)","particle_id":-1,"width_parameter":null},"value_slot_id":1,"variant":"source"},{"applies_propagator":false,"chirality":1,"component_start":4,"component_stop":6,"current_component_start":4,"current_component_stop":6,"current_id":2,"dimension":2,"external_labels":[2],"external_mask":2,"is_source":true,"momentum_mask":2,"particle_id":1,"propagator":{"applies_propagator":true,"auxiliary_policy":null,"backend":"spenso","basis":"weyl-chiral","chirality":1,"custom_source":null,"denominator":"momentum_squared","description":"massless Weyl fermion propagator","full_tensor_network_ready":true,"gauge":null,"goldstone_policy":"not-applicable","identity":{"anti_canonical_id":"model:built-in-sm-leading-color:state:d~","anti_display_name":"d~","anti_pdg_label":-1,"canonical_id":"model:built-in-sm-leading-color:state:d","display_name":"d","orientation":"particle","pdg_label":1,"self_conjugate":false,"species_id":"model:built-in-sm-leading-color:species:d"},"kernel":"weyl_fermion","kind":"weyl-fermion","mass_class":"massless","mass_parameter":null,"numerator":"i*weyl_slash(momentum)","particle_id":1,"width_parameter":null},"value_slot_id":2,"variant":"source"},{"applies_propagator":false,"chirality":-1,"component_start":6,"component_stop":8,"current_component_start":6,"current_component_stop":8,"current_id":3,"dimension":2,"external_labels":[2],"external_mask":2,"is_source":true,"momentum_mask":2,"particle_id":1,"propagator":{"applies_propagator":true,"auxiliary_policy":null,"backend":"spenso","basis":"weyl-chiral","chirality":-1,"custom_source":null,"denominator":"momentum_squared","description":"massless Weyl fermion propagator","full_tensor_network_ready":true,"gauge":null,"goldstone_policy":"not-applicable","identity":{"anti_canonical_id":"model:built-in-sm-leading-color:state:d~","anti_display_name":"d~","anti_pdg_label":-1,"canonical_id":"model:built-in-sm-leading-color:state:d","display_name":"d","orientation":"particle","pdg_label":1,"self_conjugate":false,"species_id":"model:built-in-sm-leading-color:species:d"},"kernel":"weyl_fermion","kind":"weyl-fermion","mass_class":"massless","mass_parameter":null,"numerator":"i*weyl_slash(momentum)","particle_id":1,"width_parameter":null},"value_slot_id":3,"variant":"source"},{"applies_propagator":false,"chirality":0,"component_start":8,"component_stop":12,"current_component_start":8,"current_component_stop":12,"current_id":4,"dimension":4,"external_labels":[3],"external_mask":4,"is_source":true,"momentum_mask":4,"particle_id":23,"propagator":{"applies_propagator":true,"auxiliary_policy":null,"backend":"symbolica","basis":"lorentz-vector","chirality":0,"custom_source":null,"denominator":"momentum_squared-mass_squared+i*mass*width","description":"massive vector propagator with width","full_tensor_network_ready":true,"gauge":"unitary","goldstone_policy":"absorbed","identity":{"anti_canonical_id":"model:built-in-sm-leading-color:state:z","anti_display_name":"z","anti_pdg_label":23,"canonical_id":"model:built-in-sm-leading-color:state:z","display_name":"z","orientation":"self-conjugate","pdg_label":23,"self_conjugate":true,"species_id":"model:built-in-sm-leading-color:species:z"},"kernel":"massive_vector_unitary_gauge","kind":"vector","mass_class":"massive","mass_parameter":null,"numerator":"-i*(metric-momentum_outer/mass_squared)","particle_id":23,"width_parameter":null},"value_slot_id":4,"variant":"source"},{"applies_propagator":false,"chirality":1,"component_start":12,"component_stop":14,"current_component_start":12,"current_component_stop":14,"current_id":5,"dimension":2,"external_labels":[2,3],"external_mask":6,"is_source":false,"momentum_mask":6,"particle_id":1,"propagator":{"applies_propagator":true,"auxiliary_policy":null,"backend":"spenso","basis":"weyl-chiral","chirality":1,"custom_source":null,"denominator":"momentum_squared","description":"massless Weyl fermion propagator","full_tensor_network_ready":true,"gauge":null,"goldstone_policy":"not-applicable","identity":{"anti_canonical_id":"model:built-in-sm-leading-color:state:d~","anti_display_name":"d~","anti_pdg_label":-1,"canonical_id":"model:built-in-sm-leading-color:state:d","display_name":"d","orientation":"particle","pdg_label":1,"self_conjugate":false,"species_id":"model:built-in-sm-leading-color:species:d"},"kernel":"weyl_fermion","kind":"weyl-fermion","mass_class":"massless","mass_parameter":null,"numerator":"i*weyl_slash(momentum)","particle_id":1,"width_parameter":null},"value_slot_id":5,"variant":"unpropagated"},{"applies_propagator":false,"chirality":-1,"component_start":14,"component_stop":16,"current_component_start":14,"current_component_stop":16,"current_id":6,"dimension":2,"external_labels":[2,3],"external_mask":6,"is_source":false,"momentum_mask":6,"particle_id":1,"propagator":{"applies_propagator":true,"auxiliary_policy":null,"backend":"spenso","basis":"weyl-chiral","chirality":-1,"custom_source":null,"denominator":"momentum_squared","description":"massless Weyl fermion propagator","full_tensor_network_ready":true,"gauge":null,"goldstone_policy":"not-applicable","identity":{"anti_canonical_id":"model:built-in-sm-leading-color:state:d~","anti_display_name":"d~","anti_pdg_label":-1,"canonical_id":"model:built-in-sm-leading-color:state:d","display_name":"d","orientation":"particle","pdg_label":1,"self_conjugate":false,"species_id":"model:built-in-sm-leading-color:species:d"},"kernel":"weyl_fermion","kind":"weyl-fermion","mass_class":"massless","mass_parameter":null,"numerator":"i*weyl_slash(momentum)","particle_id":1,"width_parameter":null},"value_slot_id":6,"variant":"unpropagated"}]}},"schema_version":3} diff --git a/pyamplicol/src/pyamplicol/assets/selftest/portable-64le/artifact/processes/d_dbar_to_z/physics.json b/pyamplicol/src/pyamplicol/assets/selftest/portable-64le/artifact/processes/d_dbar_to_z/physics.json new file mode 100644 index 0000000000..061afd6fc6 --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/selftest/portable-64le/artifact/processes/d_dbar_to_z/physics.json @@ -0,0 +1 @@ +{"color_accuracy":"lc","color_components":[{"coefficient":1.0,"computed":true,"id":"flow:2,1","index":0,"kind":"lc-flow","representative_id":"flow:2,1","word":[2,1]}],"coverage":{"color":"complete","color_kind":"physical-lc-flows","helicities":"complete","structural_zero_helicity_count":6},"extensions":{"normalization":{"average_factor":36,"color_accuracy":"lc","color_factor":3,"coupling_policy":"stage evaluators include local vertex couplings","couplings_in_stage_evaluators":true,"electroweak_coupling_power":1,"global_coupling_factor":0.18967104551999775,"identical_factor":1,"qcd_coupling_power":0},"process_key":"d_dbar_to_z","runtime_selectors":{"axes":{"color_flow":{"generation_coverage":"complete","generation_selection":[],"runtime_contract":"complete-reusable"},"helicity":{"generation_coverage":"complete","generation_selection":{},"runtime_contract":"complete-reusable"}},"contract_version":1,"generation_specialized_axes":[],"helicity_recurrence":{"contract_version":1,"execution":"materialized-recurrence-quotient","materialization_strategy":"quotient","materialized_amplitude_count":2,"materialized_current_count":7,"proof_algorithm":"canonical-source-transition-dependency-shape-v1","proof_amplitude_count":6,"proof_counts":{"amplitude_class_count":2,"optimized_amplitude_class_count":2,"optimized_current_count":9,"optimized_recurrence_class_count":3,"physical_helicity_count":12,"recurrence_class_count":7,"residual_amplitude_count":0,"residual_current_count":0,"source_state_mapping_count":7,"structural_zero_helicity_count":6},"proof_current_count":13,"status":"available"},"kind":"pyamplicol-runtime-selectors","provenance":"pyamplicol-runtime-selectors-v1"},"selected_source_helicities":{}},"external_particles":[{"index":0,"label":1,"momentum_components":["E","px","py","pz"],"momentum_slot":0,"particle":"d","pdg":1,"role":"initial"},{"index":1,"label":2,"momentum_components":["E","px","py","pz"],"momentum_slot":1,"particle":"d~","pdg":-1,"role":"initial"},{"index":2,"label":3,"momentum_components":["E","px","py","pz"],"momentum_slot":2,"particle":"z","pdg":23,"role":"final"}],"helicities":[{"coefficient":0.0,"computed":false,"id":"h:-1,-1,-1","index":0,"representative_id":"h:-1,-1,-1","structural_zero":true,"values":[-1,-1,-1]},{"coefficient":0.0,"computed":false,"id":"h:-1,-1,+0","index":1,"representative_id":"h:-1,-1,+0","structural_zero":true,"values":[-1,-1,0]},{"coefficient":0.0,"computed":false,"id":"h:-1,-1,+1","index":2,"representative_id":"h:-1,-1,+1","structural_zero":true,"values":[-1,-1,1]},{"coefficient":1.0,"computed":true,"id":"h:-1,+1,-1","index":3,"representative_id":"h:-1,+1,-1","structural_zero":false,"values":[-1,1,-1]},{"coefficient":1.0,"computed":false,"id":"h:-1,+1,+0","index":4,"representative_id":"h:-1,+1,-1","structural_zero":false,"values":[-1,1,0]},{"coefficient":1.0,"computed":false,"id":"h:-1,+1,+1","index":5,"representative_id":"h:-1,+1,-1","structural_zero":false,"values":[-1,1,1]},{"coefficient":1.0,"computed":true,"id":"h:+1,-1,-1","index":6,"representative_id":"h:+1,-1,-1","structural_zero":false,"values":[1,-1,-1]},{"coefficient":1.0,"computed":false,"id":"h:+1,-1,+0","index":7,"representative_id":"h:+1,-1,-1","structural_zero":false,"values":[1,-1,0]},{"coefficient":1.0,"computed":false,"id":"h:+1,-1,+1","index":8,"representative_id":"h:+1,-1,-1","structural_zero":false,"values":[1,-1,1]},{"coefficient":0.0,"computed":false,"id":"h:+1,+1,-1","index":9,"representative_id":"h:+1,+1,-1","structural_zero":true,"values":[1,1,-1]},{"coefficient":0.0,"computed":false,"id":"h:+1,+1,+0","index":10,"representative_id":"h:+1,+1,+0","structural_zero":true,"values":[1,1,0]},{"coefficient":0.0,"computed":false,"id":"h:+1,+1,+1","index":11,"representative_id":"h:+1,+1,+1","structural_zero":true,"values":[1,1,1]}],"kind":"pyamplicol-resolved-physics","model_parameters":[{"default_imaginary":0.0,"default_real":0.0075467711139788835,"kind":"normalization","mutable":true,"name":"normalization.alpha_ew"},{"default_imaginary":0.0,"default_real":0.118,"kind":"normalization","mutable":true,"name":"normalization.alpha_s_me_check"},{"default_imaginary":0.0,"default_real":173.0,"kind":"mass","mutable":true,"name":"particle.6.mass"},{"default_imaginary":0.0,"default_real":1.4915,"kind":"width","mutable":true,"name":"particle.6.width"},{"default_imaginary":0.0,"default_real":91.188,"kind":"mass","mutable":true,"name":"particle.23.mass"},{"default_imaginary":0.0,"default_real":2.441404,"kind":"width","mutable":true,"name":"particle.23.width"},{"default_imaginary":0.0,"default_real":80.41900244575616,"kind":"mass","mutable":true,"name":"particle.24.mass"},{"default_imaginary":0.0,"default_real":2.0476,"kind":"width","mutable":true,"name":"particle.24.width"},{"default_imaginary":0.0,"default_real":125.0,"kind":"mass","mutable":true,"name":"particle.25.mass"},{"default_imaginary":0.0,"default_real":0.006382339,"kind":"width","mutable":true,"name":"particle.25.width"},{"default_imaginary":0.0,"default_real":-1.0244420275940371,"kind":"coupling","mutable":true,"name":"coupling.10.1_23_1.component_0"},{"default_imaginary":0.0,"default_real":0.17818666745287456,"kind":"coupling","mutable":true,"name":"coupling.10.1_23_1.component_1"}],"process":"d d~ > z","process_id":"d_dbar_to_z","reduction":{"groups":[{"id":"reduction:0","physical_color_ids":["flow:2,1"],"physical_helicity_ids":["h:+1,-1,-1","h:+1,-1,+0","h:+1,-1,+1"],"representative_color_id":"flow:2,1","representative_helicity_id":"h:+1,-1,-1"},{"id":"reduction:1","physical_color_ids":["flow:2,1"],"physical_helicity_ids":["h:-1,+1,-1","h:-1,+1,+0","h:-1,+1,+1"],"representative_color_id":"flow:2,1","representative_helicity_id":"h:-1,+1,-1"}],"kind":"lc-diagonal"},"schema_version":1,"selectors":{"color_flow":true,"contracted_color":false,"helicity":true}} diff --git a/pyamplicol/src/pyamplicol/assets/selftest/portable-64le/artifact/processes/d_dbar_to_z/validation-momenta.json b/pyamplicol/src/pyamplicol/assets/selftest/portable-64le/artifact/processes/d_dbar_to_z/validation-momenta.json new file mode 100644 index 0000000000..209c2c78a2 --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/selftest/portable-64le/artifact/processes/d_dbar_to_z/validation-momenta.json @@ -0,0 +1,40 @@ +{ + "available": true, + "error": null, + "kind": "pyamplicol-rusticol-validation-momenta", + "points": [ + [ + { + "momentum": [ + "45.594000000000001", + "0", + "0", + "45.594000000000001" + ], + "pdg": 1 + }, + { + "momentum": [ + "45.594000000000001", + "0", + "0", + "-45.594000000000001" + ], + "pdg": -1 + }, + { + "momentum": [ + "91.188000000000002", + "0", + "0", + "0" + ], + "pdg": 23 + } + ] + ], + "process": "d d~ > z", + "process_id": "d_dbar_to_z", + "schema_version": 1, + "seed": 12345 +} diff --git a/pyamplicol/src/pyamplicol/assets/selftest/portable-64le/artifact/processes/evaluators.json b/pyamplicol/src/pyamplicol/assets/selftest/portable-64le/artifact/processes/evaluators.json new file mode 100644 index 0000000000..c69209e976 --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/selftest/portable-64le/artifact/processes/evaluators.json @@ -0,0 +1,26 @@ +{ + "kind": "pyamplicol-runtime-execution-set", + "processes": [ + { + "manifest_path": "d_dbar_to_z/execution.json", + "process_id": "d_dbar_to_z", + "required_runtime_capabilities": [ + "compiled-plane-arena-v1", + "rusticol.compiled.color-topology-lanes.v1", + "rusticol.compiled.helicity-dual-lane.v1", + "rusticol.compiled.helicity-selector-union.v1", + "rusticol.compiled.runtime-selectors.v1", + "symjit.application.complex-f64.v1" + ] + } + ], + "required_runtime_capabilities": [ + "compiled-plane-arena-v1", + "rusticol.compiled.color-topology-lanes.v1", + "rusticol.compiled.helicity-dual-lane.v1", + "rusticol.compiled.helicity-selector-union.v1", + "rusticol.compiled.runtime-selectors.v1", + "symjit.application.complex-f64.v1" + ], + "schema_version": 3 +} diff --git a/pyamplicol/src/pyamplicol/assets/selftest/portable-64le/expected.json b/pyamplicol/src/pyamplicol/assets/selftest/portable-64le/expected.json new file mode 100644 index 0000000000..8b02d99476 --- /dev/null +++ b/pyamplicol/src/pyamplicol/assets/selftest/portable-64le/expected.json @@ -0,0 +1,70 @@ +{ + "artifact_path": "artifact", + "color_ids": [ + "flow:2,1" + ], + "compatible_targets": [ + "aarch64-apple-darwin", + "x86_64-apple-darwin", + "x86_64-unknown-linux-gnu" + ], + "helicity_ids": [ + "h:-1,-1,-1", + "h:-1,-1,+0", + "h:-1,-1,+1", + "h:-1,+1,-1", + "h:-1,+1,+0", + "h:-1,+1,+1", + "h:+1,-1,-1", + "h:+1,-1,+0", + "h:+1,-1,+1", + "h:+1,+1,-1", + "h:+1,+1,+0", + "h:+1,+1,+1" + ], + "momenta": [ + [ + [ + 45.594, + 0.0, + 0.0, + 45.594 + ], + [ + 45.594, + 0.0, + 0.0, + -45.594 + ], + [ + 91.188, + 0.0, + 0.0, + 0.0 + ] + ] + ], + "process": "d d~ > z", + "process_id": "d_dbar_to_z", + "resolved_shape": [ + 1, + 12, + 1 + ], + "schema_version": 1, + "serialization": { + "endianness": "little", + "kind": "symjit-application-mir-v3", + "load_behavior": "recompile-mir-for-loading-host", + "source_optimization_level": 2, + "word_size_bits": 64 + }, + "source_generation_target": "portable-64le", + "target": "portable-64le", + "total": [ + [ + 142.10653372833232, + 0.0 + ] + ] +} diff --git a/pyamplicol/src/pyamplicol/benchmarking.py b/pyamplicol/src/pyamplicol/benchmarking.py new file mode 100644 index 0000000000..2a62981c46 --- /dev/null +++ b/pyamplicol/src/pyamplicol/benchmarking.py @@ -0,0 +1,2774 @@ +# SPDX-License-Identifier: 0BSD +"""Typed calibrated runtime profiling for Rusticol runtime backends.""" + +from __future__ import annotations + +import math +import os +import platform +import statistics +import threading +import time +import tomllib +import warnings +from collections.abc import Callable, Mapping, Sequence +from copy import deepcopy +from dataclasses import dataclass, fields, replace +from pathlib import Path +from typing import Any, Literal, cast + +from pyamplicol.api.errors import EvaluationError +from pyamplicol.api.protocols import Momenta, RuntimeBackend +from pyamplicol.api.results import ( + BenchmarkComponentTiming, + BenchmarkProfileCounters, + BenchmarkResult, + BenchmarkStageTiming, + BenchmarkStatistics, + BenchmarkTimingBreakdown, + ProcessPhysics, +) +from pyamplicol.config import BenchmarkConfig, RunConfig +from pyamplicol.reporting import ( + ProgressEnd, + ProgressSink, + ProgressStart, + ProgressUpdate, +) + +_MAX_SAMPLE_RUNTIME_SECONDS = 0.25 +_MAX_CALIBRATION_BLOCKS = 2 +_MAX_REPETITIONS_PER_SAMPLE = 1_000_000_000 +# The calibrated runtime may be optimistic or the host may speed up after +# warmup. Keep extending complete blocks, but fail closed if timing changes so +# drastically that four times the initial plan still cannot reach the target. +_MAX_TARGET_RUNTIME_SAMPLE_FACTOR = 4 +_CALIBRATION_LOWER_RATIO = 0.8 +_CALIBRATION_UPPER_RATIO = 1.25 +_MIN_CLOCK_INTERVAL_SECONDS = 1.0e-12 +_MAX_NATIVE_PROFILE_SAMPLES = 8 +_LC_TOPOLOGY_REPLAY_LAYOUT = "topology-replay" +_LC_ALL_FLOW_UNION_LAYOUT = "all-flow-union" +_LC_TOPOLOGY_REPLAY_PROFILE_RECOMMENDATION = ( + "this LC topology-replay artifact is being profiled outside its optimized " + "single-flow/helicity-sum workload; pass exactly one --color-flow and no " + "--helicity, or regenerate with --lc-flow-layout all-flow-union for the " + "all-flows/single-helicity workload" +) +_LC_ALL_FLOW_UNION_PROFILE_RECOMMENDATION = ( + "this LC all-flow-union artifact is being profiled outside its optimized " + "all-flows/single-helicity workload; pass exactly one --helicity and no " + "--color-flow" +) +_LC_PROFILE_WARNING_ATTRIBUTE = "_pyamplicol_non_hot_profile_warning_emitted" +_LC_PROFILE_WARNING_LOCK = threading.Lock() +_LC_PROFILE_WARNING_FALLBACK_OWNERS: dict[int, object] = {} +_EVALUATOR_TOTAL_SAMPLE_CONTRACT = "accumulated-repeated-warmed-evaluator-total-v1" +_RECURRENCE_LIKE_EXECUTION_MODES = {"recurrence", "on-the-fly"} +_OTF_RUNTIME_STATE_CENSUS_KIND = "rusticol-on-the-fly-runtime-state-census-v1" +_OTF_FAMILY_CACHE_POLICY = "last-family-only" +_OTF_FAMILY_CACHE_LIMIT = 1 +_OTF_RUNTIME_STATE_COUNT_FIELDS = ( + "process_preparation_count", + "retained_family_count", + "pending_family_count", + "retained_selection_count", + "retained_request_count", + "retained_amplitude_destination_count", + "retained_executor_handle_count", + "retained_query_local_trace_count", + "retained_embedded_lookup_key_count", + "semantic_executor_binding_count", +) +_OTF_RETAINED_STATE_EXECUTABLE_COUNT_FIELDS = ( + "retained_amplitude_destination_count", + "retained_executor_handle_count", + "semantic_executor_binding_count", +) +_OTF_ACTIVE_FAMILY_COUNT_FIELDS = ( + "query_count", + "union_unique_current_count", + "union_unique_current_component_count", + "union_source_rows", + "union_contribution_rows", + "union_finalization_rows", + "union_closure_rows", + "union_amplitude_destination_count", + "union_source_executor_call_groups", + "union_contribution_executor_call_groups", + "union_finalization_executor_call_groups", + "union_closure_executor_call_groups", +) +_OTF_OPERATION_ROLES = ("source", "contribution", "finalization", "closure") +_COMPILED_DIRECT_ARENA_COUNTER_KEYS = ( + "compiled_direct_arena_engine_count", + "compiled_direct_arena_call_count", + "compiled_direct_arena_boundary_input_bytes", + "compiled_direct_arena_boundary_current_output_bytes", + "compiled_direct_arena_boundary_amplitude_output_bytes", +) + + +@dataclass(frozen=True, slots=True) +class _Calibration: + sample_count: int + repetitions_per_sample: int + target_sample_seconds: float + probe_seconds: float + block_count: int + evaluation_count: int + elapsed_seconds: float + + +@dataclass(frozen=True, slots=True) +class _CompactBenchmarkContext: + process_id: str + process_expression: str + color_accuracy: str + helicity_count: int + color_count: int + selected_color_ids: tuple[str, ...] + + +@dataclass(frozen=True, slots=True) +class _NativeProfileSample: + execution_mode: str | None + compiled_direct_arena_active: bool + wall_time: float | None + native_input_pack_time: float | None + native_input_crossing_time: float | None + orchestration_time: float | None + state_prepare_time: float | None + state_clear_time: float | None + source_fill_time: float | None + momentum_input_setup_time: float | None + momentum_setup_time: float | None + model_parameter_setup_time: float | None + stage_input_pack_time: float | None + stage_leaf_input_pack_time: float | None + stage_evaluator_call_time: float + stage_backend_call_time: float | None + stage_evaluator_output_gather_time: float | None + output_assign_time: float | None + amplitude_input_pack_time: float | None + amplitude_evaluator_call_time: float | None + amplitude_leaf_input_pack_time: float | None + amplitude_backend_call_time: float | None + amplitude_evaluator_output_gather_time: float | None + amplitude_output_remap_time: float | None + reduction_time: float | None + total_materialization_time: float | None + final_output_copy_time: float | None + selector_planner_time: float | None + selector_gather_time: float | None + selector_scatter_time: float | None + other_core_time: float | None + stage_input_pack_times: tuple[float, ...] | None + stage_leaf_input_pack_times: tuple[float, ...] | None + stage_evaluator_call_times: tuple[float, ...] | None + stage_backend_call_times: tuple[float, ...] | None + stage_evaluator_output_gather_times: tuple[float, ...] | None + stage_output_assign_times: tuple[float, ...] | None + eager_initialize_time: float | None + eager_gather_time: float | None + eager_kernel_call_time: float | None + eager_invocation_scatter_time: float | None + eager_finalization_time: float | None + eager_scatter_finalization_time: float | None + eager_closure_time: float | None + eager_copy_out_time: float | None + recurrence_momentum_fill_time: float | None + recurrence_union_source_fill_time: float | None + recurrence_schedule_time: float | None + recurrence_source_kernel_time: float | None + recurrence_contribution_kernel_time: float | None + recurrence_finalization_time: float | None + recurrence_closure_time: float | None + recurrence_replay_output_mapping_time: float | None + counters: _NativeProfileCounterSample | None + + +@dataclass(frozen=True, slots=True) +class _NativeProfileCounterSample: + native_input_components_per_point: float | None + native_input_pack_bytes_per_point: float | None + native_input_crossing_bytes_per_point: float | None + state_components_per_point: float | None + state_clear_components_per_point: float | None + source_components_per_point: float | None + momentum_components_per_point: float | None + model_parameter_components_per_point: float | None + stage_input_copy_components_per_point: float | None + stage_leaf_input_copy_components_per_point: float | None + stage_evaluator_output_gather_components_per_point: float | None + stage_output_assign_components_per_point: float | None + amplitude_input_copy_components_per_point: float | None + amplitude_leaf_input_copy_components_per_point: float | None + amplitude_evaluator_output_gather_components_per_point: float | None + amplitude_output_remap_components_per_point: float | None + reduction_input_components_per_point: float | None + selector_gather_points_per_point: float | None + selector_gather_bytes_per_point: float | None + selector_scatter_values_per_point: float | None + resolved_materialized_components_per_point: float | None + total_materialized_values_per_point: float | None + final_output_copy_values_per_point: float | None + native_input_container_allocations_per_call: float | None + evaluator_backend_calls_per_call: float | None + compiled_direct_arena_engines_per_call: float | None + compiled_direct_arena_calls_per_call: float | None + compiled_direct_arena_boundary_input_bytes_per_call: float | None + compiled_direct_arena_boundary_current_output_bytes_per_call: float | None + compiled_direct_arena_boundary_amplitude_output_bytes_per_call: float | None + recurrence_momentum_scalar_values_per_point: float | None + recurrence_schedule_executions_per_call: float | None + recurrence_replay_schedule_executions_per_call: float | None + recurrence_union_schedule_executions_per_call: float | None + recurrence_union_source_rows_per_call: float | None + recurrence_replay_output_values_per_point: float | None + recurrence_source_calls_per_call: float | None + recurrence_source_rows_per_call: float | None + recurrence_contribution_calls_per_call: float | None + recurrence_contribution_rows_per_call: float | None + recurrence_finalization_calls_per_call: float | None + recurrence_finalization_rows_per_call: float | None + recurrence_closure_calls_per_call: float | None + recurrence_closure_rows_per_call: float | None + observed_scratch_reallocations_per_call: float | None + native_output_allocations_per_call: float | None + + +def _benchmark_target_path( + target: RuntimeBackend | os.PathLike[str] | str, +) -> Path | None: + if not isinstance(target, (str, os.PathLike)): + return None + return Path(os.fspath(target)).expanduser().resolve(strict=False) + + +class BenchmarkBackend: + """Measure the optimized summed runtime path without changing its semantics.""" + + def __init__( + self, + config: BenchmarkConfig | RunConfig | None, + progress: ProgressSink | None, + ) -> None: + self._run_config = config if isinstance(config, RunConfig) else None + self._config = ( + config.benchmark + if isinstance(config, RunConfig) + else config or BenchmarkConfig() + ) + self._progress = progress + self._chunk_guard: Callable[[float | None, str], None] | None = None + + def set_chunk_guard( + self, + guard: Callable[[float | None, str], None], + ) -> None: + """Install a report-owned guard called before every profiling chunk.""" + + if not callable(guard): + raise TypeError("benchmark chunk guard must be callable") + self._chunk_guard = guard + + def run( + self, + target: RuntimeBackend | os.PathLike[str] | str, + *, + points: Momenta | None = None, + ) -> BenchmarkResult: + target_path = _benchmark_target_path(target) + runtime = self._runtime(target) + if ( + str(getattr(runtime, "execution_mode", "compiled")) == "on-the-fly" + and self._config.precision != 16 + ): + raise EvaluationError( + "on-the-fly benchmarking supports only precision=16 (native f64); " + f"received precision={self._config.precision}" + ) + compact_context = _on_the_fly_benchmark_context( + runtime, + self._config.color_flow_ids, + ) + physics = None if compact_context is not None else runtime.physics + if points is None: + loader = getattr(runtime, "validation_momenta", None) + points = loader() if callable(loader) else None + if points is None or len(points) == 0: + raise EvaluationError( + "benchmarking requires at least one phase-space point and the " + "selected runtime has no deterministic validation point" + ) + batch = _benchmark_batch(points, self._config.batch_size) + helicities = self._config.helicity_ids or None + color_flows = ( + compact_context.selected_color_ids + if compact_context is not None + else _resolve_color_flow_ordinals( + cast(ProcessPhysics, physics), + self._config.color_flow_ids, + ) + ) or None + color_accuracy = ( + compact_context.color_accuracy + if compact_context is not None + else cast(ProcessPhysics, physics).color_accuracy + ) + process_id = ( + compact_context.process_id + if compact_context is not None + else cast(ProcessPhysics, physics).process_id + ) + process_expression = ( + compact_context.process_expression + if compact_context is not None + else cast(ProcessPhysics, physics).process + ) + lc_flow_layout: str | None = None + if compact_context is None: + artifact_path = target_path or _runtime_artifact_path(runtime) + lc_flow_layout = _artifact_lc_flow_layout(artifact_path) + helicities, color_flows = _default_lc_profile_selectors( + physics=cast(ProcessPhysics, physics), + lc_flow_layout=lc_flow_layout, + selected_helicity_ids=tuple(helicities or ()), + selected_color_ids=tuple(color_flows or ()), + ) + lc_flow_layout_recommendation = _lc_flow_layout_recommendation( + color_accuracy=color_accuracy, + lc_flow_layout=lc_flow_layout, + selected_helicity_ids=tuple(helicities or ()), + selected_color_ids=tuple(color_flows or ()), + ) + if lc_flow_layout_recommendation is not None: + _warn_non_hot_lc_profile_once( + runtime, + lc_flow_layout_recommendation, + ) + profiler = _native_profiler(runtime) if self._config.precision == 16 else None + repeated_profiler = ( + _native_repeated_profiler(runtime) if self._config.precision == 16 else None + ) + native_wall_timer = ( + _native_wall_timer(runtime) if self._config.precision == 16 else None + ) + timer_seconds_per_repetition: float | None = None + profiler_seconds_per_repetition: float | None = None + chunk_guard_failure: Exception | None = None + + def guard_chunk(estimated_seconds: float | None, description: str) -> None: + nonlocal chunk_guard_failure + if self._chunk_guard is not None: + try: + self._chunk_guard(estimated_seconds, description) + except Exception as error: + chunk_guard_failure = error + raise + + def evaluate_once() -> object: + return runtime.evaluate( + batch, + helicities=helicities, + color_flows=color_flows, + precision=self._config.precision, + ) + + def measure_repetitions(repetitions: int) -> float: + nonlocal timer_seconds_per_repetition + estimate = ( + None + if timer_seconds_per_repetition is None + else timer_seconds_per_repetition * repetitions + ) + guard_chunk(estimate, "runtime evaluator timing chunk") + if native_wall_timer is not None: + observed = native_wall_timer( + batch, + repetitions, + helicities=helicities, + color_flows=color_flows, + precision=self._config.precision, + ) + else: + observed = _timed_repetitions(evaluate_once, repetitions) + timer_seconds_per_repetition = observed / repetitions + return observed + + def profile_repetitions( + repetitions: int, + *, + measure_elapsed: bool = True, + ) -> tuple[object, float]: + nonlocal profiler_seconds_per_repetition + estimated_per_repetition = ( + profiler_seconds_per_repetition + if profiler_seconds_per_repetition is not None + else timer_seconds_per_repetition + ) + estimate = ( + None + if estimated_per_repetition is None + else estimated_per_repetition * repetitions + ) + guard_chunk(estimate, "runtime attribution chunk") + started = time.perf_counter() if measure_elapsed else None + if repeated_profiler is not None: + result = repeated_profiler( + batch, + repetitions, + helicities=helicities, + color_flows=color_flows, + precision=self._config.precision, + include_values=False, + ) + divisor = repetitions + elif profiler is not None: + result = profiler( + batch, + helicities=helicities, + color_flows=color_flows, + precision=self._config.precision, + include_values=False, + ) + divisor = 1 + else: # pragma: no cover - caller checks profiler availability. + raise EvaluationError("runtime profiler is unavailable") + elapsed_seconds = ( + (time.perf_counter() - started) + if started is not None + else (estimate or 0.0) + ) + if elapsed_seconds > 0.0: + profiler_seconds_per_repetition = elapsed_seconds / divisor + return result, elapsed_seconds + + task_id = "runtime-benchmark" + calibration_task_id = "runtime-profile-calibration" + active_task_id = calibration_task_id + cold_warmup_elapsed: float | None = None + cold_warmup_state_before: dict[str, object] | None = None + cold_warmup_state_after: dict[str, object] | None = None + cold_warmup_runtime_was_cold: bool | None = None + cold_warmup_runtime_was_retained: bool | None = None + cold_warmup_runtime_is_retained: bool | None = None + warmup_elapsed = 0.0 + warmup_run_outer_wall_seconds: list[float] = [] + calibration: _Calibration | None = None + samples: list[float] = [] + evaluator_samples: list[float] | None = ( + [] if profiler is not None or repeated_profiler is not None else None + ) + native_profile_samples: list[_NativeProfileSample] | None = ( + [] if profiler is not None or repeated_profiler is not None else None + ) + elapsed = 0.0 + evaluator_total_elapsed = 0.0 + evaluator_elapsed = 0.0 + interrupted = False + if self._progress is not None: + self._progress.emit( + ProgressStart( + calibration_task_id, + "Calibrating runtime profile", + total=None, + ) + ) + try: + if compact_context is not None: + cold_warmup_state_before = _on_the_fly_runtime_state_census( + runtime, + expected_process_id=compact_context.process_id, + ) + cold_warmup_runtime_was_cold = _on_the_fly_runtime_state_is_cold( + cold_warmup_state_before + ) + cold_warmup_runtime_was_retained = ( + _on_the_fly_runtime_state_is_retained(cold_warmup_state_before) + ) + if not ( + cold_warmup_runtime_was_cold or cold_warmup_runtime_was_retained + ): + raise EvaluationError( + "on-the-fly runtime state before the first requested " + "evaluation is neither cold nor fully retained" + ) + cold_warmup_started = time.perf_counter() + measure_repetitions(1) + cold_warmup_elapsed = time.perf_counter() - cold_warmup_started + cold_warmup_state_after = _on_the_fly_runtime_state_census( + runtime, + expected_process_id=compact_context.process_id, + ) + cold_warmup_runtime_is_retained = _on_the_fly_runtime_state_is_retained( + cold_warmup_state_after + ) + if not cold_warmup_runtime_is_retained: + raise EvaluationError( + "on-the-fly first requested evaluation did not leave a fully " + "retained runtime family" + ) + # The required first-selector evaluation can spend minutes + # constructing a cold high-multiplicity family. It is reported + # separately as OTF warm-up evidence and must not become the + # estimate used to guard or calibrate retained evaluations. + timer_seconds_per_repetition = None + last_warmup_seconds: float | None = None + for warmup_index in range(self._config.warmup_runs): + warmup_started = time.perf_counter() + last_warmup_seconds = measure_repetitions(1) + if repeated_profiler is not None or profiler is not None: + profile_repetitions(1, measure_elapsed=False) + warmup_run_elapsed = time.perf_counter() - warmup_started + warmup_run_outer_wall_seconds.append(warmup_run_elapsed) + warmup_elapsed += warmup_run_elapsed + if self._progress is not None: + self._progress.emit( + ProgressUpdate( + calibration_task_id, + completed=warmup_index + 1, + total=None, + message="warmup", + ) + ) + + calibration_outer_started = time.perf_counter() + calibration = _calibrate_repetitions( + evaluate_once, + self._config, + initial_seconds=last_warmup_seconds, + timer=measure_repetitions, + ) + calibration_outer_elapsed = time.perf_counter() - calibration_outer_started + if self._progress is not None: + self._progress.emit( + ProgressEnd( + calibration_task_id, + message=( + f"{calibration.sample_count} blocks x " + f"{calibration.repetitions_per_sample} repetitions" + ), + ) + ) + self._progress.emit( + ProgressStart( + task_id, + "Profiling runtime", + total=calibration.sample_count, + ) + ) + active_task_id = task_id + repetitions = calibration.repetitions_per_sample + initial_planned_sample_count = calibration.sample_count + planned_sample_count = initial_planned_sample_count + maximum_sample_count = ( + initial_planned_sample_count * _MAX_TARGET_RUNTIME_SAMPLE_FACTOR + ) + native_profile_sample_limit = ( + planned_sample_count + if repeated_profiler is not None + else min( + planned_sample_count, + max( + 1, + min( + self._config.minimum_samples, + _MAX_NATIVE_PROFILE_SAMPLES, + ), + ), + ) + ) + try: + sample_index = 0 + while sample_index < planned_sample_count: + native_sample: _NativeProfileSample | None = None + profile_duration = 0.0 + sample_started = time.perf_counter() + evaluator_total_duration = measure_repetitions(repetitions) + wall_duration = time.perf_counter() - sample_started + sample_seconds_per_point = wall_duration / ( + repetitions * len(batch) + ) + if repeated_profiler is not None: + profile, profile_duration = profile_repetitions(repetitions) + native_sample = _native_profile_sample( + profile, + len(batch) * repetitions, + repetitions=repetitions, + ) + if native_sample.wall_time is None: + raise EvaluationError( + "repeated native profile did not report core wall time" + ) + elif ( + profiler is not None + and evaluator_samples is not None + and native_profile_samples is not None + and sample_index < native_profile_sample_limit + ): + profile, profile_duration = profile_repetitions(1) + native_sample = _native_profile_sample(profile, len(batch)) + + samples.append(sample_seconds_per_point) + elapsed += wall_duration + evaluator_total_elapsed += evaluator_total_duration + if native_sample is not None: + assert evaluator_samples is not None + assert native_profile_samples is not None + native_profile_samples.append(native_sample) + if ( + native_sample.execution_mode + in _RECURRENCE_LIKE_EXECUTION_MODES + ): + if native_sample.recurrence_schedule_time is None: + raise EvaluationError( + "native recurrence schedule timing is unavailable" + ) + evaluator_samples.append( + native_sample.recurrence_schedule_time + ) + elif native_sample.compiled_direct_arena_active: + if native_sample.orchestration_time is None: + raise EvaluationError( + "native compiled Direct-Arena orchestration " + "timing is unavailable" + ) + evaluator_samples.append(native_sample.orchestration_time) + else: + evaluator_samples.append( + native_sample.stage_evaluator_call_time + + (native_sample.amplitude_evaluator_call_time or 0.0) + ) + evaluator_elapsed += profile_duration + sample_index += 1 + if ( + sample_index == planned_sample_count + and elapsed < self._config.target_runtime + ): + if planned_sample_count >= maximum_sample_count: + raise EvaluationError( + "runtime benchmark could not reach its target " + f"duration of {self._config.target_runtime:g}s " + f"within {maximum_sample_count} complete blocks" + ) + mean_block_seconds = elapsed / len(samples) + additional_samples = max( + 1, + math.ceil( + (self._config.target_runtime - elapsed) + / max( + mean_block_seconds, + _MIN_CLOCK_INTERVAL_SECONDS, + ) + ), + ) + planned_sample_count = min( + maximum_sample_count, + planned_sample_count + additional_samples, + ) + if repeated_profiler is not None: + native_profile_sample_limit = planned_sample_count + if self._progress is not None: + message, details = _sample_progress_payload( + samples, + elapsed_seconds=elapsed, + target_seconds=self._config.target_runtime, + repetitions=repetitions, + batch_size=len(batch), + ) + self._progress.emit( + ProgressUpdate( + task_id, + completed=len(samples), + total=planned_sample_count, + message=message, + details=details, + ) + ) + except KeyboardInterrupt: + if not samples: + raise + interrupted = True + if self._progress is not None: + self._progress.emit( + ProgressEnd( + task_id, + success=False, + message=( + f"interrupted after {len(samples)}/" + f"{planned_sample_count} complete blocks; " + "reporting partial statistics" + ), + ) + ) + except KeyboardInterrupt: + if self._progress is not None: + self._progress.emit( + ProgressEnd( + active_task_id, + success=False, + message="interrupted before a complete timing block", + ) + ) + raise + except Exception as exc: + if self._progress is not None: + self._progress.emit( + ProgressEnd(active_task_id, success=False, message=str(exc)) + ) + if isinstance(exc, EvaluationError): + raise + if exc is chunk_guard_failure: + raise + raise EvaluationError(f"runtime benchmark failed: {exc}") from exc + if self._progress is not None and not interrupted: + self._progress.emit(ProgressEnd(task_id)) + + assert calibration is not None + + wall_samples = samples + wall_time_source = "python_outer_perf_counter_wall_time" + wall_sample_pass = "time.perf_counter[measure_repetitions]" + evaluator_total_sample_pass = ( + "runtime._benchmark_f64_wall_time" + if native_wall_timer is not None + else "runtime.evaluate" + ) + timing_sample_contract = ( + "paired_unprofiled_headline_profiled_attribution_v1" + if repeated_profiler is not None + else "separate_native_profile_diagnostic_v1" + ) + mean, deviation, error, relative_error = _sample_statistics(wall_samples) + uncertainty = BenchmarkStatistics(deviation, error, relative_error) + compiled_direct_arena_active = False + if evaluator_samples is None: + evaluator_time_per_point = None + evaluator_uncertainty = None + timing_breakdown = None + evaluator_environment: dict[str, object] = { + "wall_time_source": wall_time_source, + "wall_time_sample_pass": wall_sample_pass, + "evaluator_time_source": "unavailable", + "evaluator_time_sample_pass": "unavailable", + "timing_sample_contract": "headline_only_no_breakdown_v1", + "native_profile_unavailable_reason": ( + "non_f64_precision" if self._config.precision != 16 else None + ), + } + else: + assert native_profile_samples is not None + compiled_direct_arena_states = { + sample.compiled_direct_arena_active for sample in native_profile_samples + } + if len(compiled_direct_arena_states) > 1: + raise EvaluationError( + "native compiled Direct-Arena activity changed between samples" + ) + compiled_direct_arena_active = next( + iter(compiled_direct_arena_states), + False, + ) + ( + evaluator_time_per_point, + evaluator_deviation, + evaluator_error, + evaluator_relative, + ) = _sample_statistics(evaluator_samples) + evaluator_uncertainty = BenchmarkStatistics( + evaluator_deviation, + evaluator_error, + evaluator_relative, + ) + timing_breakdown = _timing_breakdown(native_profile_samples) + if timing_breakdown.execution_mode in _RECURRENCE_LIKE_EXECUTION_MODES: + evaluator_time_source = "runtime_profile_core_recurrence_schedule_time" + elif compiled_direct_arena_active: + evaluator_time_source = ( + "runtime_profile_core_compiled_direct_arena_orchestration_time" + ) + else: + evaluator_time_source = "runtime_profile_core_evaluator_call_time" + native_profile_repetitions = ( + calibration.repetitions_per_sample + if repeated_profiler is not None + else 1 + ) + native_profile_warmup_calls = ( + self._config.warmup_runs + if profiler is not None or repeated_profiler is not None + else 0 + ) + paired_profile_attribution = repeated_profiler is not None + evaluator_environment = { + "wall_time_source": wall_time_source, + "wall_time_sample_pass": wall_sample_pass, + "evaluator_time_source": evaluator_time_source, + "compiled_direct_arena_active": compiled_direct_arena_active, + "evaluator_time_sample_pass": ( + "runtime.profile_repeated" + if repeated_profiler is not None + else "runtime.profile" + ), + "timing_breakdown_sample_pass": ( + "runtime.profile_repeated" + if repeated_profiler is not None + else "runtime.profile" + ), + "evaluator_sample_count": len(evaluator_samples), + "evaluator_elapsed_seconds": evaluator_elapsed, + "native_profile_sample_count": len(evaluator_samples), + "native_profile_sample_limit": native_profile_sample_limit, + "native_profile_repetitions_per_sample": (native_profile_repetitions), + "native_profile_batch_size": len(batch), + "native_profile_points_per_sample": ( + native_profile_repetitions * len(batch) + ), + "native_profile_calls_per_block": ( + len(evaluator_samples) / len(samples) if samples else 0.0 + ), + "native_profile_warmup_call_count": native_profile_warmup_calls, + "native_profile_total_call_count": ( + native_profile_warmup_calls + len(evaluator_samples) + ), + "profile_attribution_paired_with_headline": ( + paired_profile_attribution + ), + "profile_attribution_identical_batch": paired_profile_attribution, + "profile_attribution_identical_repetitions": ( + paired_profile_attribution + ), + "profile_attribution_evaluation_count": ( + len(evaluator_samples) * native_profile_repetitions + ), + "profile_attribution_point_count": ( + len(evaluator_samples) * native_profile_repetitions * len(batch) + ), + "timing_sample_contract": timing_sample_contract, + "evaluator_standard_deviation_seconds_per_point": (evaluator_deviation), + "evaluator_standard_error_seconds_per_point": evaluator_error, + "evaluator_relative_standard_error": evaluator_relative, + } + raw_evaluator_time_per_point = evaluator_time_per_point + evaluator_below_timer_resolution = ( + raw_evaluator_time_per_point is not None + and compiled_direct_arena_active + and raw_evaluator_time_per_point == 0.0 + ) + if evaluator_below_timer_resolution: + evaluator_time_per_point = None + evaluator_environment.update( + { + "evaluator_time_status": ( + "below_timer_resolution" + if evaluator_below_timer_resolution + else ( + "measured" + if raw_evaluator_time_per_point is not None + else "unavailable" + ) + ), + "evaluator_time_ratio_eligible": ( + raw_evaluator_time_per_point is not None + and not evaluator_below_timer_resolution + and raw_evaluator_time_per_point > 0.0 + ), + "evaluator_time_raw_seconds_per_point": (raw_evaluator_time_per_point), + } + ) + execution_mode = ( + timing_breakdown.execution_mode + if timing_breakdown is not None + else str(getattr(runtime, "execution_mode", "unavailable")) + ) + selected_helicity_ids = tuple(helicities or ()) + selected_color_ids = tuple(color_flows or ()) + effective_config = replace( + self._config, + helicity_ids=selected_helicity_ids, + color_flow_ids=selected_color_ids, + ) + measured_evaluations = len(samples) * calibration.repetitions_per_sample + measured_point_count = measured_evaluations * len(batch) + evaluator_total_time_per_point = evaluator_total_elapsed / measured_point_count + layout_environment: dict[str, object] = {} + if lc_flow_layout is not None: + layout_environment = { + "lc_flow_layout": lc_flow_layout, + "lc_flow_layout_recommendation": lc_flow_layout_recommendation, + } + cold_warmup_environment: dict[str, object] = {} + if cold_warmup_elapsed is not None: + if ( + cold_warmup_state_before is None + or cold_warmup_state_after is None + or cold_warmup_runtime_was_cold is None + or cold_warmup_runtime_was_retained is None + or cold_warmup_runtime_is_retained is None + ): + raise EvaluationError( + "on-the-fly benchmark did not retain complete runtime state " + "evidence" + ) + cold_warmup_environment = { + "cold_warmup_elapsed_seconds": cold_warmup_elapsed, + "cold_warmup_run_count": 1, + "cold_warmup_batch_size": len(batch), + "cold_warmup_point_count": len(batch), + "cold_warmup_timer_source": "python_outer_time.perf_counter", + "cold_warmup_timing_scope": ( + "one initial requested-selector Runtime evaluation on the full " + "benchmark batch; artifact generation and Runtime/artifact load " + "are excluded" + ), + "cold_warmup_runtime_freshness": ( + "authenticated-cold" + if cold_warmup_runtime_was_cold + else "authenticated-already-retained" + ), + "cold_warmup_runtime_state_evidence": ( + "authenticated-native-otf-census-v1" + ), + "cold_warmup_runtime_state_before": cold_warmup_state_before, + "cold_warmup_runtime_state_after": cold_warmup_state_after, + "cold_warmup_runtime_cold_before_first_evaluation": ( + cold_warmup_runtime_was_cold + ), + "cold_warmup_runtime_retained_before_first_evaluation": ( + cold_warmup_runtime_was_retained + ), + "cold_warmup_runtime_retained_after_first_evaluation": ( + cold_warmup_runtime_is_retained + ), + "cold_warmup_ratio_eligible": False, + "cold_warmup_acceptance_eligible": False, + } + return BenchmarkResult( + requested_config=self._config, + effective_config=effective_config, + sample_count=len(samples), + wall_time_per_point=mean, + evaluator_time_per_point=evaluator_time_per_point, + uncertainty=uncertainty, + environment={ + "python": platform.python_version(), + "platform": platform.platform(), + "machine": platform.machine(), + "target": None if target_path is None else str(target_path), + "batch_size": len(batch), + "precision": self._config.precision, + "execution_mode": execution_mode, + "color_accuracy": color_accuracy, + "color_workload": ( + _compact_color_workload_text(compact_context, selected_color_ids) + if compact_context is not None + else _color_workload_text( + cast(ProcessPhysics, physics), + selected_color_ids, + ) + ), + "helicity_workload": ( + _compact_helicity_workload_text( + compact_context, + selected_helicity_ids, + ) + if compact_context is not None + else _helicity_workload_text( + cast(ProcessPhysics, physics), + selected_helicity_ids, + ) + ), + "selected_color_ids": selected_color_ids, + "selected_helicity_ids": selected_helicity_ids, + "elapsed_seconds": elapsed, + "interrupted": interrupted, + "completed_sample_count": len(samples), + "completion_fraction": len(samples) / planned_sample_count, + "warmup_elapsed_seconds": warmup_elapsed, + "warmup_configured_run_count": self._config.warmup_runs, + "warmup_batch_size": len(batch), + "warmup_point_count": self._config.warmup_runs * len(batch), + "warmup_run_outer_wall_seconds": tuple(warmup_run_outer_wall_seconds), + "first_warmup_run_outer_wall_seconds": ( + warmup_run_outer_wall_seconds[0] + if warmup_run_outer_wall_seconds + else None + ), + "warmup_timer_source": "python_outer_time.perf_counter", + "warmup_timing_scope": ( + "configured benchmark warm-up iteration outer wall; includes " + "the headline evaluation and optional native-profile warm-up; " + "artifact generation and Runtime/artifact load are excluded" + ), + "planned_sample_count": planned_sample_count, + "initial_planned_sample_count": initial_planned_sample_count, + "adaptive_extension_sample_count": ( + planned_sample_count - initial_planned_sample_count + ), + "repetitions_per_sample": calibration.repetitions_per_sample, + "measured_evaluation_count": measured_evaluations, + "measured_point_count": measured_point_count, + "evaluator_total_time_raw_seconds_per_point": ( + evaluator_total_time_per_point + ), + "evaluator_total_time_status": "measured", + "evaluator_total_time_ratio_eligible": False, + "evaluator_total_time_source": ( + f"{evaluator_total_sample_pass}.accumulated" + ), + "evaluator_total_time_sample_contract": ( + _EVALUATOR_TOTAL_SAMPLE_CONTRACT + ), + "evaluator_total_accumulated_seconds": evaluator_total_elapsed, + "target_sample_seconds": calibration.target_sample_seconds, + "calibration_probe_seconds": calibration.probe_seconds, + "calibration_block_count": calibration.block_count, + "calibration_evaluation_count": calibration.evaluation_count, + "calibration_elapsed_seconds": calibration.elapsed_seconds, + "calibration_outer_elapsed_seconds": calibration_outer_elapsed, + **cold_warmup_environment, + **layout_environment, + **evaluator_environment, + }, + interrupted=interrupted, + repetitions_per_sample=calibration.repetitions_per_sample, + evaluator_uncertainty=evaluator_uncertainty, + evaluator_total_time_per_point=evaluator_total_time_per_point, + process_id=process_id, + process_expression=process_expression, + timing_breakdown=timing_breakdown, + ) + + def _runtime( + self, + target: RuntimeBackend | os.PathLike[str] | str, + ) -> RuntimeBackend: + target_path = _benchmark_target_path(target) + if target_path is None: + return cast(RuntimeBackend, target) + from pyamplicol.runtime import load_runtime_backend + + run = self._run_config + process = None if run is None else run.evaluation.process + path = target_path + task_id = "process-output-load" + started = time.perf_counter() + if self._progress is not None: + self._progress.emit( + ProgressStart( + task_id, + f"Loading process output {path}", + details={"step": "loading process output", "path": str(path)}, + ) + ) + try: + runtime = load_runtime_backend( + path, + process=process, + model_parameters=None, + mute_warnings=False, + ) + except Exception as exc: + if self._progress is not None: + self._progress.emit( + ProgressEnd( + task_id, + success=False, + message=str(exc), + elapsed_seconds=time.perf_counter() - started, + ) + ) + raise + if self._progress is not None: + self._progress.emit( + ProgressEnd( + task_id, + elapsed_seconds=time.perf_counter() - started, + ) + ) + return runtime + + +def _artifact_lc_flow_layout(target_path: Path | None) -> str | None: + if target_path is None: + return None + effective_path = target_path / "config" / "effective.toml" + try: + payload = tomllib.loads(effective_path.read_text(encoding="utf-8")) + except (OSError, tomllib.TOMLDecodeError): + return None + color = payload.get("color") + if not isinstance(color, Mapping) or color.get("accuracy") != "lc": + return None + layout = color.get("lc_flow_layout", _LC_TOPOLOGY_REPLAY_LAYOUT) + if layout not in {_LC_TOPOLOGY_REPLAY_LAYOUT, _LC_ALL_FLOW_UNION_LAYOUT}: + return None + return str(layout) + + +def _runtime_artifact_path(runtime: RuntimeBackend) -> Path | None: + """Recover the built-in runtime's artifact root without extending the protocol.""" + + candidate: object | None = runtime + seen: set[int] = set() + while candidate is not None and id(candidate) not in seen: + seen.add(id(candidate)) + raw_path = getattr(candidate, "_artifact_path", None) + if isinstance(raw_path, (str, os.PathLike)): + return Path(os.fspath(raw_path)).expanduser().resolve(strict=False) + candidate = getattr(candidate, "_backend", None) + return None + + +def _warn_non_hot_lc_profile_once( + runtime: RuntimeBackend, + message: str, +) -> None: + """Warn once for one loaded process, never once per timing sample.""" + + owner: object = runtime + seen: set[int] = set() + while id(owner) not in seen: + seen.add(id(owner)) + backend = getattr(owner, "_backend", None) + if backend is None: + break + owner = backend + with _LC_PROFILE_WARNING_LOCK: + if bool(getattr(owner, _LC_PROFILE_WARNING_ATTRIBUTE, False)): + return + try: + setattr(owner, _LC_PROFILE_WARNING_ATTRIBUTE, True) + except (AttributeError, TypeError): + # Keep an identity-stable fallback for immutable protocol + # implementations so they cannot emit once per benchmark run. + owner_id = id(owner) + if _LC_PROFILE_WARNING_FALLBACK_OWNERS.get(owner_id) is owner: + return + _LC_PROFILE_WARNING_FALLBACK_OWNERS[owner_id] = owner + warnings.warn(message, UserWarning, stacklevel=3) + + +def _default_lc_profile_selectors( + *, + physics: ProcessPhysics, + lc_flow_layout: str | None, + selected_helicity_ids: Sequence[str], + selected_color_ids: Sequence[str], +) -> tuple[tuple[str, ...] | None, tuple[str, ...] | None]: + """Choose the LC hot workload while preserving explicit subset intent.""" + + helicities = tuple(selected_helicity_ids) + color_flows = tuple(selected_color_ids) + if physics.color_accuracy != "lc" or lc_flow_layout is None: + return helicities or None, color_flows or None + complete_helicities = _is_complete_selector_set( + helicities, physics.helicity_ids + ) + complete_color_flows = _is_complete_selector_set( + color_flows, physics.color_ids + ) + if lc_flow_layout == _LC_TOPOLOGY_REPLAY_LAYOUT: + normalized_helicities = None if complete_helicities else helicities or None + normalized_color_flows = ( + color_flows + if color_flows and (not complete_color_flows or len(color_flows) == 1) + else None + ) + if color_flows or normalized_helicities is not None: + return normalized_helicities, normalized_color_flows + selectable_flows = tuple( + flow.id for flow in physics.color_flows if flow.computed + ) + if not selectable_flows: + selectable_flows = physics.color_ids + if not selectable_flows: + raise EvaluationError("LC profiling requires at least one physical flow") + return None, (selectable_flows[0],) + if lc_flow_layout == _LC_ALL_FLOW_UNION_LAYOUT: + normalized_color_flows = None if complete_color_flows else color_flows or None + normalized_helicities = ( + helicities + if helicities and (not complete_helicities or len(helicities) == 1) + else None + ) + if helicities or normalized_color_flows is not None: + return normalized_helicities, normalized_color_flows + selectable = tuple( + helicity.id + for helicity in physics.helicities + if helicity.computed and not helicity.structural_zero + ) + if not selectable: + selectable = tuple( + helicity.id for helicity in physics.helicities if helicity.computed + ) + if not selectable: + selectable = physics.helicity_ids + if not selectable: + raise EvaluationError( + "LC profiling requires at least one physical helicity" + ) + return (selectable[0],), None + return None, None + + +def _is_complete_selector_set( + selected: Sequence[str], + available: Sequence[str], +) -> bool: + """Return whether an explicit selector enumerates one complete physical axis.""" + + return ( + bool(selected) + and len(selected) == len(available) + and set(selected) == set(available) + ) + + +def _lc_flow_layout_recommendation( + *, + color_accuracy: str, + lc_flow_layout: str | None, + selected_helicity_ids: Sequence[str], + selected_color_ids: Sequence[str], +) -> str | None: + if color_accuracy != "lc": + return None + if lc_flow_layout == _LC_TOPOLOGY_REPLAY_LAYOUT: + if not selected_helicity_ids and len(selected_color_ids) == 1: + return None + return _LC_TOPOLOGY_REPLAY_PROFILE_RECOMMENDATION + if lc_flow_layout == _LC_ALL_FLOW_UNION_LAYOUT: + if len(selected_helicity_ids) == 1 and not selected_color_ids: + return None + return _LC_ALL_FLOW_UNION_PROFILE_RECOMMENDATION + return None + + +def _benchmark_batch(points: Momenta, batch_size: int) -> Momenta: + source = tuple(points) + return tuple(source[index % len(source)] for index in range(batch_size)) + + +def _on_the_fly_benchmark_context( + runtime: RuntimeBackend, + requested_color_ids: Sequence[str], +) -> _CompactBenchmarkContext | None: + if str(getattr(runtime, "execution_mode", "compiled")) != "on-the-fly": + return None + loader = getattr(runtime, "_on_the_fly_benchmark_context", None) + if not callable(loader): + raise EvaluationError( + "on-the-fly benchmark requires the installed compact runtime context" + ) + raw = loader(tuple(requested_color_ids)) + if not isinstance(raw, Mapping): + raise EvaluationError("on-the-fly benchmark context is unavailable") + + def required_text(key: str) -> str: + value = raw.get(key) + if not isinstance(value, str) or not value: + raise EvaluationError( + f"on-the-fly benchmark context field {key!r} is invalid" + ) + return value + + def required_count(key: str) -> int: + value = raw.get(key) + if isinstance(value, bool) or not isinstance(value, int) or value < 1: + raise EvaluationError( + f"on-the-fly benchmark context field {key!r} is invalid" + ) + return value + + selected = raw.get("selected_color_ids") + if not isinstance(selected, Sequence) or isinstance(selected, (str, bytes)): + raise EvaluationError("on-the-fly benchmark selected color IDs are invalid") + selected_color_ids = tuple(str(value) for value in selected) + if len(selected_color_ids) != len(requested_color_ids) or any( + not value for value in selected_color_ids + ): + raise EvaluationError( + "on-the-fly benchmark selected color IDs do not match the request" + ) + return _CompactBenchmarkContext( + process_id=required_text("process_id"), + process_expression=required_text("process_expression"), + color_accuracy=required_text("color_accuracy"), + helicity_count=required_count("helicity_count"), + color_count=required_count("color_count"), + selected_color_ids=selected_color_ids, + ) + + +def _on_the_fly_runtime_state_census( + runtime: RuntimeBackend, + *, + expected_process_id: str, +) -> dict[str, object]: + loader = getattr(runtime, "_on_the_fly_runtime_state_census", None) + if not callable(loader): + raise EvaluationError( + "on-the-fly benchmark requires the installed compact runtime state census" + ) + raw = loader() + if not isinstance(raw, Mapping): + raise EvaluationError("on-the-fly runtime state census is unavailable") + value = deepcopy(dict(raw)) + if value.get("kind") != _OTF_RUNTIME_STATE_CENSUS_KIND: + raise EvaluationError("on-the-fly runtime state census has an invalid kind") + if value.get("process_id") != expected_process_id: + raise EvaluationError( + "on-the-fly runtime state census does not match the benchmark process" + ) + if value.get("family_cache_policy") != _OTF_FAMILY_CACHE_POLICY: + raise EvaluationError( + "on-the-fly runtime state census has an invalid family cache policy" + ) + cache_limit = value.get("family_cache_limit") + if ( + isinstance(cache_limit, bool) + or not isinstance(cache_limit, int) + or cache_limit != _OTF_FAMILY_CACHE_LIMIT + ): + raise EvaluationError( + "on-the-fly runtime state census has an invalid family cache limit" + ) + + counts: dict[str, int] = {} + for field in _OTF_RUNTIME_STATE_COUNT_FIELDS: + count = value.get(field) + if isinstance(count, bool) or not isinstance(count, int) or count < 0: + raise EvaluationError( + f"on-the-fly runtime state census field {field!r} is invalid" + ) + counts[field] = count + if counts["retained_family_count"] > _OTF_FAMILY_CACHE_LIMIT: + raise EvaluationError( + "on-the-fly runtime state census retained family count exceeds its " + "cache limit" + ) + + active = value.get("active_family_union_census") + if active is None: + return value + if not isinstance(active, Mapping): + raise EvaluationError( + "on-the-fly active-family runtime state census is invalid" + ) + active_value = dict(active) + if ( + active_value.get("basis") != "shared-query-family-union-v1" + or active_value.get("scope") != "active-family-union" + ): + raise EvaluationError( + "on-the-fly active-family runtime state census has an invalid identity" + ) + active_counts: dict[str, int] = {} + for field in _OTF_ACTIVE_FAMILY_COUNT_FIELDS: + count = active_value.get(field) + if isinstance(count, bool) or not isinstance(count, int) or count < 0: + raise EvaluationError( + "on-the-fly active-family runtime state census field " + f"{field!r} is invalid" + ) + active_counts[field] = count + if ( + active_counts["query_count"] < 1 + or active_counts["union_unique_current_count"] + > active_counts["union_unique_current_component_count"] + or active_counts["union_amplitude_destination_count"] < 1 + or active_counts["union_amplitude_destination_count"] + > active_counts["query_count"] + or active_counts["query_count"] > counts["retained_request_count"] + or active_counts["union_amplitude_destination_count"] + > counts["retained_amplitude_destination_count"] + ): + raise EvaluationError( + "on-the-fly active-family runtime state census is inconsistent" + ) + for role in _OTF_OPERATION_ROLES: + if ( + active_counts[f"union_{role}_executor_call_groups"] + > active_counts[f"union_{role}_rows"] + ): + raise EvaluationError( + "on-the-fly active-family runtime state census has more " + f"{role} executor call groups than rows" + ) + value["active_family_union_census"] = active_value + return value + + +def _on_the_fly_runtime_state_is_cold(census: Mapping[str, object]) -> bool: + return all(census[field] == 0 for field in _OTF_RUNTIME_STATE_COUNT_FIELDS) and ( + census["active_family_union_census"] is None + ) + + +def _on_the_fly_runtime_state_is_retained(census: Mapping[str, object]) -> bool: + if ( + census["pending_family_count"] != 0 + or census["process_preparation_count"] != 1 + or census["retained_family_count"] != _OTF_FAMILY_CACHE_LIMIT + or census["retained_selection_count"] != _OTF_FAMILY_CACHE_LIMIT + or cast(int, census["retained_request_count"]) < 1 + ): + return False + executable_counts = tuple( + cast(int, census[field]) + for field in _OTF_RETAINED_STATE_EXECUTABLE_COUNT_FIELDS + ) + active = census["active_family_union_census"] + if isinstance(active, Mapping): + return ( + executable_counts[0] > 0 + and executable_counts[1] == _OTF_FAMILY_CACHE_LIMIT + and executable_counts[2] > 0 + ) + return active is None and all(count == 0 for count in executable_counts) + + +def _resolve_color_flow_ordinals( + physics: ProcessPhysics, + requested: Sequence[str], +) -> tuple[str, ...]: + available = physics.color_ids + resolved: list[str] = [] + for value in requested: + if value in available: + resolved.append(value) + continue + try: + ordinal = int(value, 10) + except ValueError: + resolved.append(value) + continue + if str(ordinal) != value.strip() or ordinal < 1 or ordinal > len(available): + maximum = len(available) + raise EvaluationError( + f"color-flow ordinal {value!r} is out of range; choose 1..{maximum} " + "or a stable color component ID" + ) + resolved.append(available[ordinal - 1]) + return tuple(resolved) + + +def _color_workload_text( + physics: ProcessPhysics, + selected: Sequence[str], +) -> str: + if physics.color_accuracy != "lc": + return f"contracted {physics.color_accuracy.upper()} color total" + count = len(physics.color_ids) + if not selected: + return f"all {count} generated physical LC flows" + return f"selected {len(selected)}/{count} physical LC flows: {', '.join(selected)}" + + +def _compact_color_workload_text( + context: _CompactBenchmarkContext, + selected: Sequence[str], +) -> str: + if context.color_accuracy != "lc": + return f"contracted {context.color_accuracy.upper()} color total" + if not selected: + return f"all {context.color_count} generated physical LC flows" + return ( + f"selected {len(selected)}/{context.color_count} physical LC flows: " + f"{', '.join(selected)}" + ) + + +def _helicity_workload_text( + physics: ProcessPhysics, + selected: Sequence[str], +) -> str: + count = len(physics.helicity_ids) + if not selected: + structural = physics.structural_zero_helicity_count + suffix = f"; {structural} structural zeros" if structural else "" + return f"all {count} generated helicity configurations{suffix}" + return f"selected {len(selected)}/{count} helicities: {', '.join(selected)}" + + +def _compact_helicity_workload_text( + context: _CompactBenchmarkContext, + selected: Sequence[str], +) -> str: + if not selected: + return f"all {context.helicity_count} generated helicity configurations" + return ( + f"selected {len(selected)}/{context.helicity_count} helicities: " + f"{', '.join(selected)}" + ) + + +def _sample_progress_payload( + samples: list[float], + *, + elapsed_seconds: float, + target_seconds: float, + repetitions: int, + batch_size: int, +) -> tuple[str, dict[str, str | int | float | None]]: + mean, _deviation, error, relative_error = _sample_statistics(samples) + if mean < 1.0e-6: + scale, unit = 1.0e9, "ns" + elif mean < 1.0e-3: + scale, unit = 1.0e6, "us" + elif mean < 1.0: + scale, unit = 1.0e3, "ms" + else: + scale, unit = 1.0, "s" + uncertainty = ( + "SE pending" + if len(samples) < 2 + else ( + f"+/- {error * scale:.2g} {unit} " + f"(relative standard error {relative_error:.2%})" + ) + ) + message = ( + f"{elapsed_seconds:.3g}/{target_seconds:.3g}s; " + f"wall {mean * scale:.5g} {unit}/point {uncertainty}; " + f"{repetitions} calls x {batch_size} points" + ) + return message, { + "progress_kind": "benchmark-statistics", + "elapsed_seconds": elapsed_seconds, + "target_seconds": target_seconds, + "wall_seconds_per_point": mean, + "wall_standard_error_seconds_per_point": (error if len(samples) >= 2 else None), + "wall_relative_standard_error": (relative_error if len(samples) >= 2 else None), + "repetitions": repetitions, + "batch_size": batch_size, + "sample_count": len(samples), + } + + +def _planned_sample_count( + config: BenchmarkConfig, + *, + probe_seconds: float, +) -> int: + if config.minimum_samples == 1: + return 1 + runtime_samples = math.ceil(config.target_runtime / _MAX_SAMPLE_RUNTIME_SECONDS) + desired = max(config.minimum_samples, runtime_samples) + maximum_for_runtime = max( + math.floor( + config.target_runtime / max(probe_seconds, _MIN_CLOCK_INTERVAL_SECONDS) + ), + 1, + ) + return max(config.minimum_samples, min(desired, maximum_for_runtime)) + + +def _timed_repetitions(callback: Callable[[], object], repetitions: int) -> float: + started = time.perf_counter() + for _ in range(repetitions): + callback() + return time.perf_counter() - started + + +def _estimated_repetitions( + current: int, + observed_seconds: float, + target_seconds: float, +) -> int: + observed = max(observed_seconds, _MIN_CLOCK_INTERVAL_SECONDS) + estimate = math.ceil(current * target_seconds / observed) + return min(max(estimate, 1), _MAX_REPETITIONS_PER_SAMPLE) + + +def _calibrate_repetitions( + callback: Callable[[], object], + config: BenchmarkConfig, + *, + initial_seconds: float | None, + timer: Callable[[int], float] | None = None, +) -> _Calibration: + measure = timer or (lambda repetitions: _timed_repetitions(callback, repetitions)) + block_count = 0 + evaluation_count = 0 + calibration_elapsed = 0.0 + if initial_seconds is None: + initial_seconds = measure(1) + block_count = 1 + evaluation_count = 1 + calibration_elapsed = initial_seconds + + probe_seconds = initial_seconds + sample_count = _planned_sample_count(config, probe_seconds=probe_seconds) + target_sample_seconds = config.target_runtime / sample_count + if sample_count == 1: + return _Calibration( + sample_count=sample_count, + repetitions_per_sample=_estimated_repetitions( + 1, + probe_seconds, + target_sample_seconds, + ), + target_sample_seconds=target_sample_seconds, + probe_seconds=probe_seconds, + block_count=block_count, + evaluation_count=evaluation_count, + elapsed_seconds=calibration_elapsed, + ) + + observed_seconds = initial_seconds + repetitions = 1 + for _ in range(_MAX_CALIBRATION_BLOCKS): + candidate = _estimated_repetitions( + repetitions, + observed_seconds, + target_sample_seconds, + ) + if candidate == repetitions: + break + observed_seconds = measure(candidate) + block_count += 1 + evaluation_count += candidate + calibration_elapsed += observed_seconds + repetitions = candidate + ratio = observed_seconds / target_sample_seconds + if _CALIBRATION_LOWER_RATIO <= ratio <= _CALIBRATION_UPPER_RATIO: + break + else: + repetitions = _estimated_repetitions( + repetitions, + observed_seconds, + target_sample_seconds, + ) + + return _Calibration( + sample_count=sample_count, + repetitions_per_sample=repetitions, + target_sample_seconds=target_sample_seconds, + probe_seconds=probe_seconds, + block_count=block_count, + evaluation_count=evaluation_count, + elapsed_seconds=calibration_elapsed, + ) + + +def _native_profiler( + runtime: RuntimeBackend, +) -> Callable[..., Mapping[str, object]] | None: + if getattr(runtime, "supports_profiling", None) is False: + return None + for name in ("profile", "evaluate_profile"): + profiler = getattr(runtime, name, None) + if callable(profiler): + return cast(Callable[..., Mapping[str, object]], profiler) + return None + + +def _native_repeated_profiler( + runtime: RuntimeBackend, +) -> Callable[..., Mapping[str, object]] | None: + if getattr(runtime, "supports_profiling", None) is False: + return None + profiler = getattr(runtime, "profile_repeated", None) + return ( + cast(Callable[..., Mapping[str, object]], profiler) + if callable(profiler) + else None + ) + + +def _native_wall_timer(runtime: RuntimeBackend) -> Callable[..., float] | None: + timer = getattr(runtime, "_benchmark_f64_wall_time", None) + return timer if callable(timer) else None + + +def _profile_float(profile: Mapping[str, object], key: str) -> float: + value = profile.get(key) + if isinstance(value, bool) or not isinstance(value, (float, int)): + raise EvaluationError(f"native runtime profile field {key!r} is not numeric") + value = float(value) + if not math.isfinite(value) or value < 0.0: + raise EvaluationError(f"native runtime profile field {key!r} is invalid") + return value + + +def _profile_float_or_none(profile: Mapping[str, object], key: str) -> float | None: + value = profile.get(key) + if isinstance(value, bool) or not isinstance(value, (float, int)): + return None + result = float(value) + if not math.isfinite(result) or result < 0.0: + return None + return result + + +def _profile_count_or_none( + profile: Mapping[str, object], + key: str, + *, + denominator: int, +) -> float | None: + if key not in profile: + return None + value = profile[key] + if isinstance(value, bool) or not isinstance(value, int) or value < 0: + raise EvaluationError( + f"native runtime profile counter {key!r} is not a non-negative integer" + ) + return value / denominator + + +def _native_profile_counters( + profile: Mapping[str, object], + *, + points: int, + repetitions: int, +) -> _NativeProfileCounterSample | None: + arena_counter_presence = tuple( + key in profile for key in _COMPILED_DIRECT_ARENA_COUNTER_KEYS + ) + if any(arena_counter_presence) and not all(arena_counter_presence): + missing = tuple( + key + for key, present in zip( + _COMPILED_DIRECT_ARENA_COUNTER_KEYS, + arena_counter_presence, + strict=True, + ) + if not present + ) + raise EvaluationError( + "native compiled Direct-Arena profile counters must be jointly " + f"available; missing {missing!r}" + ) + + def per_point(key: str) -> float | None: + return _profile_count_or_none(profile, key, denominator=points) + + def per_call(key: str) -> float | None: + return _profile_count_or_none(profile, key, denominator=repetitions) + + counters = _NativeProfileCounterSample( + native_input_components_per_point=per_point("native_input_component_count"), + native_input_pack_bytes_per_point=per_point("native_input_pack_bytes"), + native_input_crossing_bytes_per_point=per_point("native_input_crossing_bytes"), + state_components_per_point=per_point("state_component_count"), + state_clear_components_per_point=per_point("state_clear_component_count"), + source_components_per_point=per_point("source_component_count"), + momentum_components_per_point=per_point("momentum_component_count"), + model_parameter_components_per_point=per_point( + "model_parameter_component_count" + ), + stage_input_copy_components_per_point=per_point( + "stage_input_copy_component_count" + ), + stage_leaf_input_copy_components_per_point=per_point( + "stage_leaf_input_copy_component_count" + ), + stage_evaluator_output_gather_components_per_point=per_point( + "stage_evaluator_output_gather_component_count" + ), + stage_output_assign_components_per_point=per_point( + "stage_output_assign_component_count" + ), + amplitude_input_copy_components_per_point=per_point( + "amplitude_input_copy_component_count" + ), + amplitude_leaf_input_copy_components_per_point=per_point( + "amplitude_leaf_input_copy_component_count" + ), + amplitude_evaluator_output_gather_components_per_point=per_point( + "amplitude_evaluator_output_gather_component_count" + ), + amplitude_output_remap_components_per_point=per_point( + "amplitude_output_remap_component_count" + ), + reduction_input_components_per_point=per_point( + "reduction_input_component_count" + ), + selector_gather_points_per_point=per_point("selector_gather_point_count"), + selector_gather_bytes_per_point=per_point("selector_gather_bytes"), + selector_scatter_values_per_point=per_point("selector_scatter_value_count"), + resolved_materialized_components_per_point=per_point( + "resolved_materialized_component_count" + ), + total_materialized_values_per_point=per_point("total_materialized_value_count"), + final_output_copy_values_per_point=per_point("final_output_copy_value_count"), + native_input_container_allocations_per_call=per_call( + "native_input_container_allocation_count" + ), + evaluator_backend_calls_per_call=per_call("evaluator_backend_call_count"), + compiled_direct_arena_engines_per_call=per_call( + "compiled_direct_arena_engine_count" + ), + compiled_direct_arena_calls_per_call=per_call( + "compiled_direct_arena_call_count" + ), + compiled_direct_arena_boundary_input_bytes_per_call=per_call( + "compiled_direct_arena_boundary_input_bytes" + ), + compiled_direct_arena_boundary_current_output_bytes_per_call=per_call( + "compiled_direct_arena_boundary_current_output_bytes" + ), + compiled_direct_arena_boundary_amplitude_output_bytes_per_call=per_call( + "compiled_direct_arena_boundary_amplitude_output_bytes" + ), + recurrence_momentum_scalar_values_per_point=per_point( + "recurrence_momentum_scalar_value_count" + ), + recurrence_schedule_executions_per_call=per_call( + "recurrence_schedule_execution_count" + ), + recurrence_replay_schedule_executions_per_call=per_call( + "recurrence_replay_schedule_execution_count" + ), + recurrence_union_schedule_executions_per_call=per_call( + "recurrence_union_schedule_execution_count" + ), + recurrence_union_source_rows_per_call=per_call( + "recurrence_union_source_row_count" + ), + recurrence_replay_output_values_per_point=per_point( + "recurrence_replay_output_value_count" + ), + recurrence_source_calls_per_call=per_call("recurrence_source_call_count"), + recurrence_source_rows_per_call=per_call("recurrence_source_row_count"), + recurrence_contribution_calls_per_call=per_call( + "recurrence_contribution_call_count" + ), + recurrence_contribution_rows_per_call=per_call( + "recurrence_contribution_row_count" + ), + recurrence_finalization_calls_per_call=per_call( + "recurrence_finalization_call_count" + ), + recurrence_finalization_rows_per_call=per_call( + "recurrence_finalization_row_count" + ), + recurrence_closure_calls_per_call=per_call("recurrence_closure_call_count"), + recurrence_closure_rows_per_call=per_call("recurrence_closure_row_count"), + observed_scratch_reallocations_per_call=per_call( + "observed_scratch_reallocation_count" + ), + native_output_allocations_per_call=per_call("native_output_allocation_count"), + ) + if not any(getattr(counters, field.name) is not None for field in fields(counters)): + return None + arena_engines = counters.compiled_direct_arena_engines_per_call + if arena_engines is not None: + arena_calls = counters.compiled_direct_arena_calls_per_call + assert arena_calls is not None + boundary_counters = ( + ( + "compiled_direct_arena_boundary_input_bytes", + counters.compiled_direct_arena_boundary_input_bytes_per_call, + ), + ( + "compiled_direct_arena_boundary_current_output_bytes", + counters.compiled_direct_arena_boundary_current_output_bytes_per_call, + ), + ( + "compiled_direct_arena_boundary_amplitude_output_bytes", + counters.compiled_direct_arena_boundary_amplitude_output_bytes_per_call, + ), + ) + for name, value in boundary_counters: + assert value is not None + if value != 0.0: + raise EvaluationError( + "native compiled Direct-Arena profile observed forbidden " + f"boundary traffic: {name}={value:g} bytes/runtime call" + ) + if (arena_engines > 0.0) != (arena_calls > 0.0): + raise EvaluationError( + "native compiled Direct-Arena engine and call counters disagree" + ) + if arena_engines > 0.0: + evaluator_calls = counters.evaluator_backend_calls_per_call + if evaluator_calls is None or evaluator_calls != arena_calls: + raise EvaluationError( + "native compiled Direct-Arena calls do not cover every " + "evaluator backend call" + ) + return counters + + +def _profile_float_sequence( + profile: Mapping[str, object], key: str +) -> tuple[float, ...]: + values = profile.get(key) + if not isinstance(values, Sequence) or isinstance(values, (str, bytes)): + raise EvaluationError( + f"native runtime profile field {key!r} is not a numeric sequence" + ) + result: list[float] = [] + for value in values: + if isinstance(value, bool) or not isinstance(value, (float, int)): + raise EvaluationError( + f"native runtime profile field {key!r} is not a numeric sequence" + ) + entry = float(value) + if not math.isfinite(entry) or entry < 0.0: + raise EvaluationError(f"native runtime profile field {key!r} is invalid") + result.append(entry) + return tuple(result) + + +def _profile_float_sequence_or_none( + profile: Mapping[str, object], key: str +) -> tuple[float, ...] | None: + if key not in profile: + return None + values = _profile_float_sequence(profile, key) + # Eager execution has no compiled-stage timing vector. Rusticol still + # supplies the aggregate evaluator timer, so an empty vector means + # "unavailable", not a measured zero. + return values or None + + +def _profile_execution_mode( + profile: Mapping[str, object], + *, + stage_vectors_present_but_empty: bool, +) -> str | None: + value = profile.get("execution_mode") + if value is not None: + if value not in {"compiled", "eager", "recurrence", "on-the-fly"}: + raise EvaluationError( + "native runtime profile execution_mode must be compiled, eager, " + "recurrence, or on-the-fly" + ) + return str(value) + stage_aggregate = profile.get("stage_evaluator_call_time_s") + amplitude_aggregate = profile.get("amplitude_evaluator_call_time_s") + if ( + stage_vectors_present_but_empty + and isinstance(stage_aggregate, (float, int)) + and not isinstance(stage_aggregate, bool) + and float(stage_aggregate) > 0.0 + and isinstance(amplitude_aggregate, (float, int)) + and not isinstance(amplitude_aggregate, bool) + and float(amplitude_aggregate) == 0.0 + ): + return "eager" + return None + + +def _profile_point_count(profile: Mapping[str, object], fallback: int) -> int: + value: Any = profile.get("points", fallback) + if isinstance(value, bool) or not isinstance(value, int) or value < 1: + raise EvaluationError("native runtime profile point count is invalid") + return int(value) + + +def _native_profile_sample( + profile: Mapping[str, object], + fallback_points: int, + *, + repetitions: int = 1, +) -> _NativeProfileSample: + points = _profile_point_count(profile, fallback_points) + if ( + isinstance(repetitions, bool) + or not isinstance(repetitions, int) + or repetitions < 1 + ): + raise EvaluationError("native runtime profile repetition count is invalid") + if points % repetitions != 0: + raise EvaluationError( + "native runtime profile point count is not divisible by repetitions" + ) + per_point = 1.0 / points + counters = _native_profile_counters( + profile, + points=points, + repetitions=repetitions, + ) + stage_vector_keys = ( + "stage_input_pack_by_stage_time_s", + "stage_evaluator_call_by_stage_time_s", + "stage_output_assign_by_stage_time_s", + ) + stage_vectors_present_but_empty = all( + key in profile + and isinstance(profile[key], Sequence) + and not isinstance(profile[key], (str, bytes)) + and len(profile[key]) == 0 # type: ignore[arg-type] + for key in stage_vector_keys + ) + stage_input_pack = _profile_float_sequence_or_none( + profile, "stage_input_pack_by_stage_time_s" + ) + stage_leaf_input_pack = _profile_float_sequence_or_none( + profile, "stage_leaf_input_pack_by_stage_time_s" + ) + stage_evaluator_call = _profile_float_sequence_or_none( + profile, "stage_evaluator_call_by_stage_time_s" + ) + stage_backend_call = _profile_float_sequence_or_none( + profile, "stage_backend_call_by_stage_time_s" + ) + stage_evaluator_output_gather = _profile_float_sequence_or_none( + profile, "stage_evaluator_output_gather_by_stage_time_s" + ) + stage_output_assign = _profile_float_sequence_or_none( + profile, "stage_output_assign_by_stage_time_s" + ) + execution_mode = _profile_execution_mode( + profile, + stage_vectors_present_but_empty=stage_vectors_present_but_empty, + ) + compiled_direct_arena_active = bool( + counters is not None + and counters.compiled_direct_arena_engines_per_call is not None + and counters.compiled_direct_arena_engines_per_call > 0.0 + ) + if compiled_direct_arena_active and execution_mode != "compiled": + raise EvaluationError( + "native compiled Direct-Arena counters require compiled execution mode" + ) + + stage_input_pack_total = ( + sum(stage_input_pack) + if stage_input_pack is not None + else _profile_float_or_none(profile, "stage_input_pack_time_s") + ) + stage_evaluator_call_total = ( + sum(stage_evaluator_call) + if stage_evaluator_call is not None + else _profile_float_or_none(profile, "stage_evaluator_call_time_s") + ) + if ( + stage_evaluator_call_total is None + and execution_mode not in _RECURRENCE_LIKE_EXECUTION_MODES + ): + raise EvaluationError("native runtime stage evaluator timing is unavailable") + if stage_evaluator_call_total is None: + stage_evaluator_call_total = 0.0 + output_assign_total = ( + sum(stage_output_assign) + if stage_output_assign is not None + else _profile_float_or_none(profile, "output_assign_time_s") + ) + stage_leaf_input_pack_total = ( + sum(stage_leaf_input_pack) + if stage_leaf_input_pack is not None + else _profile_float_or_none(profile, "stage_leaf_input_pack_time_s") + ) + stage_backend_call_total = ( + sum(stage_backend_call) + if stage_backend_call is not None + else _profile_float_or_none(profile, "stage_backend_call_time_s") + ) + stage_evaluator_output_gather_total = ( + sum(stage_evaluator_output_gather) + if stage_evaluator_output_gather is not None + else _profile_float_or_none(profile, "stage_evaluator_output_gather_time_s") + ) + amplitude_evaluator_call: float | None = None + if execution_mode not in {"eager", *_RECURRENCE_LIKE_EXECUTION_MODES}: + amplitude_evaluator_call = _profile_float_or_none( + profile, "amplitude_evaluator_call_time_s" + ) + if amplitude_evaluator_call is None: + amplitude_evaluator_call = _profile_float( + profile, "amplitude_evaluator_time_s" + ) + + def normalized(key: str) -> float | None: + value = _profile_float_or_none(profile, key) + return None if value is None else value * per_point + + def normalized_sequence( + values: tuple[float, ...] | None, + ) -> tuple[float, ...] | None: + if values is None: + return None + return tuple(value * per_point for value in values) + + wall_time = normalized("wall_time_s") + native_input_pack_time = normalized("native_input_pack_time_s") + native_input_crossing_time = normalized("native_input_crossing_time_s") + orchestration_time = normalized("orchestration_time_s") + if compiled_direct_arena_active and orchestration_time is None: + raise EvaluationError( + "native compiled Direct-Arena orchestration timing is unavailable" + ) + state_prepare_time = normalized("state_prepare_time_s") + state_clear_time = normalized("state_clear_time_s") + source_fill_time = normalized("source_fill_time_s") + momentum_setup_time = normalized("momentum_setup_time_s") + momentum_input_setup_time = normalized("momentum_input_setup_time_s") + model_parameter_setup_time = normalized("model_parameter_setup_time_s") + if momentum_input_setup_time is None: + momentum_input_setup_time = momentum_setup_time + if ( + momentum_input_setup_time is not None + and model_parameter_setup_time is not None + ): + momentum_input_setup_time = max( + momentum_input_setup_time - model_parameter_setup_time, + 0.0, + ) + compiled_profile = execution_mode not in { + "eager", + *_RECURRENCE_LIKE_EXECUTION_MODES, + } + stage_input_pack_time = ( + None + if not compiled_profile or stage_input_pack_total is None + else stage_input_pack_total * per_point + ) + stage_leaf_input_pack_time = ( + None + if not compiled_profile or stage_leaf_input_pack_total is None + else stage_leaf_input_pack_total * per_point + ) + stage_evaluator_call_time = stage_evaluator_call_total * per_point + stage_backend_call_time = ( + None + if not compiled_profile or stage_backend_call_total is None + else stage_backend_call_total * per_point + ) + stage_evaluator_output_gather_time = ( + None + if not compiled_profile or stage_evaluator_output_gather_total is None + else stage_evaluator_output_gather_total * per_point + ) + output_assign_time = ( + None + if not compiled_profile or output_assign_total is None + else output_assign_total * per_point + ) + amplitude_input_pack_time = ( + normalized("amplitude_input_pack_time_s") if compiled_profile else None + ) + amplitude_evaluator_call_time = ( + None + if amplitude_evaluator_call is None + else amplitude_evaluator_call * per_point + ) + amplitude_leaf_input_pack_time = ( + normalized("amplitude_leaf_input_pack_time_s") if compiled_profile else None + ) + amplitude_backend_call_time = ( + normalized("amplitude_backend_call_time_s") if compiled_profile else None + ) + amplitude_evaluator_output_gather_time = ( + normalized("amplitude_evaluator_output_gather_time_s") + if compiled_profile + else None + ) + amplitude_output_remap_time = ( + normalized("amplitude_output_remap_time_s") if compiled_profile else None + ) + reduction_time = ( + normalized("eager_reduction_time_s") + if execution_mode == "eager" + else normalized("reduction_time_s") + ) + total_materialization_time = normalized("total_materialization_time_s") + final_output_copy_time = normalized("final_output_copy_time_s") + selector_planner_time = normalized("selector_planner_time_s") + selector_gather_time = normalized("selector_gather_time_s") + selector_scatter_time = normalized("selector_scatter_time_s") + eager_execution_time = ( + stage_evaluator_call_time if execution_mode == "eager" else None + ) + eager_initialize_time = normalized("eager_initialize_time_s") + eager_gather_time = normalized("eager_gather_time_s") + eager_kernel_call_time = normalized("eager_kernel_call_time_s") + eager_invocation_scatter_time = normalized("eager_invocation_scatter_time_s") + eager_finalization_time = normalized("eager_finalization_time_s") + eager_scatter_finalization_time = normalized("eager_scatter_finalization_time_s") + eager_closure_time = normalized("eager_closure_time_s") + eager_copy_out_time = normalized("eager_copy_out_time_s") + recurrence_momentum_fill_time = normalized("recurrence_momentum_fill_time_s") + recurrence_union_source_fill_time = normalized( + "recurrence_union_source_fill_time_s" + ) + recurrence_schedule_time = normalized("recurrence_schedule_time_s") + recurrence_source_kernel_time = normalized("recurrence_source_kernel_time_s") + recurrence_contribution_kernel_time = normalized( + "recurrence_contribution_kernel_time_s" + ) + recurrence_finalization_time = normalized("recurrence_finalization_time_s") + recurrence_closure_time = normalized("recurrence_closure_time_s") + recurrence_replay_output_mapping_time = normalized( + "recurrence_replay_output_mapping_time_s" + ) + if ( + execution_mode in _RECURRENCE_LIKE_EXECUTION_MODES + and recurrence_schedule_time is None + ): + raise EvaluationError("native recurrence-like schedule timing is unavailable") + + common_accounted = ( + native_input_pack_time, + native_input_crossing_time, + orchestration_time, + state_prepare_time, + state_clear_time, + source_fill_time, + momentum_input_setup_time, + model_parameter_setup_time, + ) + if execution_mode == "eager": + mode_accounted = (eager_execution_time,) + elif execution_mode in _RECURRENCE_LIKE_EXECUTION_MODES: + mode_accounted = ( + recurrence_momentum_fill_time, + recurrence_union_source_fill_time, + recurrence_schedule_time, + recurrence_replay_output_mapping_time, + reduction_time, + ) + else: + mode_accounted = ( + stage_input_pack_time, + stage_evaluator_call_time, + output_assign_time, + amplitude_input_pack_time, + amplitude_evaluator_call_time, + reduction_time, + ) + accounted = ( + *common_accounted, + *mode_accounted, + total_materialization_time, + final_output_copy_time, + selector_planner_time, + selector_gather_time, + selector_scatter_time, + ) + accounted_total = sum(value or 0.0 for value in accounted) + accounting_tolerance = ( + 1.0e-12 if wall_time is None else max(1.0e-12, wall_time * 1.0e-12) + ) + if wall_time is not None and accounted_total > wall_time + accounting_tolerance: + raise EvaluationError( + "native profile exclusive top-level phases account for " + f"{accounted_total:.9e}s/point, exceeding wall time " + f"{wall_time:.9e}s/point" + ) + if execution_mode == "eager" and eager_execution_time is not None: + scatter_phases = ( + (eager_invocation_scatter_time, eager_finalization_time) + if eager_invocation_scatter_time is not None + or eager_finalization_time is not None + else (eager_scatter_finalization_time,) + ) + exclusive_eager_phases = ( + eager_initialize_time, + eager_gather_time, + eager_kernel_call_time, + *scatter_phases, + eager_closure_time, + reduction_time, + eager_copy_out_time, + ) + exclusive_eager_total = sum(value or 0.0 for value in exclusive_eager_phases) + if exclusive_eager_total > eager_execution_time + accounting_tolerance: + raise EvaluationError( + "native eager profile exclusive execution phases account for " + f"{exclusive_eager_total:.9e}s/point, exceeding the inclusive " + f"eager execution time {eager_execution_time:.9e}s/point" + ) + if ( + execution_mode in _RECURRENCE_LIKE_EXECUTION_MODES + and recurrence_schedule_time is not None + ): + recurrence_attribution = ( + recurrence_source_kernel_time, + recurrence_contribution_kernel_time, + recurrence_finalization_time, + recurrence_closure_time, + ) + recurrence_attributed_total = sum( + value or 0.0 for value in recurrence_attribution + ) + if recurrence_attributed_total > ( + recurrence_schedule_time + accounting_tolerance + ): + raise EvaluationError( + "native recurrence profile schedule sub-attribution accounts for " + f"{recurrence_attributed_total:.9e}s/point, exceeding the " + "inclusive recurrence schedule time " + f"{recurrence_schedule_time:.9e}s/point" + ) + other_core_time = ( + None if wall_time is None else max(wall_time - accounted_total, 0.0) + ) + + return _NativeProfileSample( + execution_mode=execution_mode, + compiled_direct_arena_active=compiled_direct_arena_active, + wall_time=wall_time, + native_input_pack_time=native_input_pack_time, + native_input_crossing_time=native_input_crossing_time, + orchestration_time=orchestration_time, + state_prepare_time=state_prepare_time, + state_clear_time=state_clear_time, + source_fill_time=source_fill_time, + momentum_input_setup_time=momentum_input_setup_time, + momentum_setup_time=momentum_setup_time, + model_parameter_setup_time=model_parameter_setup_time, + stage_input_pack_time=stage_input_pack_time, + stage_leaf_input_pack_time=stage_leaf_input_pack_time, + stage_evaluator_call_time=stage_evaluator_call_time, + stage_backend_call_time=stage_backend_call_time, + stage_evaluator_output_gather_time=stage_evaluator_output_gather_time, + output_assign_time=output_assign_time, + amplitude_input_pack_time=amplitude_input_pack_time, + amplitude_evaluator_call_time=amplitude_evaluator_call_time, + amplitude_leaf_input_pack_time=amplitude_leaf_input_pack_time, + amplitude_backend_call_time=amplitude_backend_call_time, + amplitude_evaluator_output_gather_time=(amplitude_evaluator_output_gather_time), + amplitude_output_remap_time=amplitude_output_remap_time, + reduction_time=reduction_time, + total_materialization_time=total_materialization_time, + final_output_copy_time=final_output_copy_time, + selector_planner_time=selector_planner_time, + selector_gather_time=selector_gather_time, + selector_scatter_time=selector_scatter_time, + other_core_time=other_core_time, + stage_input_pack_times=( + normalized_sequence(stage_input_pack) if compiled_profile else None + ), + stage_leaf_input_pack_times=( + normalized_sequence(stage_leaf_input_pack) if compiled_profile else None + ), + stage_evaluator_call_times=( + normalized_sequence(stage_evaluator_call) if compiled_profile else None + ), + stage_backend_call_times=( + normalized_sequence(stage_backend_call) if compiled_profile else None + ), + stage_evaluator_output_gather_times=normalized_sequence( + stage_evaluator_output_gather if compiled_profile else None + ), + stage_output_assign_times=( + normalized_sequence(stage_output_assign) if compiled_profile else None + ), + eager_initialize_time=eager_initialize_time, + eager_gather_time=eager_gather_time, + eager_kernel_call_time=eager_kernel_call_time, + eager_invocation_scatter_time=eager_invocation_scatter_time, + eager_finalization_time=eager_finalization_time, + eager_scatter_finalization_time=eager_scatter_finalization_time, + eager_closure_time=eager_closure_time, + eager_copy_out_time=eager_copy_out_time, + recurrence_momentum_fill_time=recurrence_momentum_fill_time, + recurrence_union_source_fill_time=recurrence_union_source_fill_time, + recurrence_schedule_time=recurrence_schedule_time, + recurrence_source_kernel_time=recurrence_source_kernel_time, + recurrence_contribution_kernel_time=recurrence_contribution_kernel_time, + recurrence_finalization_time=recurrence_finalization_time, + recurrence_closure_time=recurrence_closure_time, + recurrence_replay_output_mapping_time=(recurrence_replay_output_mapping_time), + counters=counters, + ) + + +def _component_timing( + values: Sequence[float | None], +) -> BenchmarkComponentTiming | None: + available = [value for value in values if value is not None] + if not available: + return None + mean, deviation, error, relative_error = _sample_statistics(available) + return BenchmarkComponentTiming( + mean_seconds_per_point=mean, + uncertainty=BenchmarkStatistics(deviation, error, relative_error), + sample_count=len(available), + ) + + +def _stage_component_timing( + samples: Sequence[_NativeProfileSample], + attribute: str, + stage_index: int, +) -> BenchmarkComponentTiming | None: + values: list[float | None] = [] + for sample in samples: + stage_values = getattr(sample, attribute) + if not isinstance(stage_values, tuple) or stage_index >= len(stage_values): + values.append(None) + else: + values.append(stage_values[stage_index]) + return _component_timing(values) + + +def _profile_counter_summary( + samples: Sequence[_NativeProfileSample], +) -> BenchmarkProfileCounters | None: + counter_samples = [ + sample.counters for sample in samples if sample.counters is not None + ] + if not counter_samples: + return None + if len(counter_samples) != len(samples): + raise EvaluationError( + "native runtime profile counter availability changed between samples" + ) + + def mean(attribute: str) -> float | None: + values = [ + cast(float | None, getattr(sample, attribute)) for sample in counter_samples + ] + available = [value for value in values if value is not None] + if available and len(available) != len(values): + raise EvaluationError( + f"native runtime profile counter {attribute!r} changed availability" + ) + return statistics.fmean(available) if available else None + + return BenchmarkProfileCounters( + sample_count=len(counter_samples), + native_input_components_per_point=mean("native_input_components_per_point"), + native_input_pack_bytes_per_point=mean("native_input_pack_bytes_per_point"), + native_input_crossing_bytes_per_point=mean( + "native_input_crossing_bytes_per_point" + ), + state_components_per_point=mean("state_components_per_point"), + state_clear_components_per_point=mean("state_clear_components_per_point"), + source_components_per_point=mean("source_components_per_point"), + momentum_components_per_point=mean("momentum_components_per_point"), + model_parameter_components_per_point=mean( + "model_parameter_components_per_point" + ), + stage_input_copy_components_per_point=mean( + "stage_input_copy_components_per_point" + ), + stage_leaf_input_copy_components_per_point=mean( + "stage_leaf_input_copy_components_per_point" + ), + stage_evaluator_output_gather_components_per_point=mean( + "stage_evaluator_output_gather_components_per_point" + ), + stage_output_assign_components_per_point=mean( + "stage_output_assign_components_per_point" + ), + amplitude_input_copy_components_per_point=mean( + "amplitude_input_copy_components_per_point" + ), + amplitude_leaf_input_copy_components_per_point=mean( + "amplitude_leaf_input_copy_components_per_point" + ), + amplitude_evaluator_output_gather_components_per_point=mean( + "amplitude_evaluator_output_gather_components_per_point" + ), + amplitude_output_remap_components_per_point=mean( + "amplitude_output_remap_components_per_point" + ), + reduction_input_components_per_point=mean( + "reduction_input_components_per_point" + ), + selector_gather_points_per_point=mean("selector_gather_points_per_point"), + selector_gather_bytes_per_point=mean("selector_gather_bytes_per_point"), + selector_scatter_values_per_point=mean("selector_scatter_values_per_point"), + resolved_materialized_components_per_point=mean( + "resolved_materialized_components_per_point" + ), + total_materialized_values_per_point=mean("total_materialized_values_per_point"), + final_output_copy_values_per_point=mean("final_output_copy_values_per_point"), + native_input_container_allocations_per_call=mean( + "native_input_container_allocations_per_call" + ), + evaluator_backend_calls_per_call=mean("evaluator_backend_calls_per_call"), + compiled_direct_arena_engines_per_call=mean( + "compiled_direct_arena_engines_per_call" + ), + compiled_direct_arena_calls_per_call=mean( + "compiled_direct_arena_calls_per_call" + ), + compiled_direct_arena_boundary_input_bytes_per_call=mean( + "compiled_direct_arena_boundary_input_bytes_per_call" + ), + compiled_direct_arena_boundary_current_output_bytes_per_call=mean( + "compiled_direct_arena_boundary_current_output_bytes_per_call" + ), + compiled_direct_arena_boundary_amplitude_output_bytes_per_call=mean( + "compiled_direct_arena_boundary_amplitude_output_bytes_per_call" + ), + recurrence_momentum_scalar_values_per_point=mean( + "recurrence_momentum_scalar_values_per_point" + ), + recurrence_schedule_executions_per_call=mean( + "recurrence_schedule_executions_per_call" + ), + recurrence_replay_schedule_executions_per_call=mean( + "recurrence_replay_schedule_executions_per_call" + ), + recurrence_union_schedule_executions_per_call=mean( + "recurrence_union_schedule_executions_per_call" + ), + recurrence_union_source_rows_per_call=mean( + "recurrence_union_source_rows_per_call" + ), + recurrence_replay_output_values_per_point=mean( + "recurrence_replay_output_values_per_point" + ), + recurrence_source_calls_per_call=mean("recurrence_source_calls_per_call"), + recurrence_source_rows_per_call=mean("recurrence_source_rows_per_call"), + recurrence_contribution_calls_per_call=mean( + "recurrence_contribution_calls_per_call" + ), + recurrence_contribution_rows_per_call=mean( + "recurrence_contribution_rows_per_call" + ), + recurrence_finalization_calls_per_call=mean( + "recurrence_finalization_calls_per_call" + ), + recurrence_finalization_rows_per_call=mean( + "recurrence_finalization_rows_per_call" + ), + recurrence_closure_calls_per_call=mean("recurrence_closure_calls_per_call"), + recurrence_closure_rows_per_call=mean("recurrence_closure_rows_per_call"), + observed_scratch_reallocations_per_call=mean( + "observed_scratch_reallocations_per_call" + ), + native_output_allocations_per_call=mean("native_output_allocations_per_call"), + ) + + +def _timing_breakdown( + samples: Sequence[_NativeProfileSample], +) -> BenchmarkTimingBreakdown: + if not samples: + raise EvaluationError("native runtime profile returned no timing samples") + stage_count = max( + ( + len(values) + for sample in samples + for values in ( + sample.stage_input_pack_times, + sample.stage_leaf_input_pack_times, + sample.stage_evaluator_call_times, + sample.stage_backend_call_times, + sample.stage_evaluator_output_gather_times, + sample.stage_output_assign_times, + ) + if values is not None + ), + default=0, + ) + stages: list[BenchmarkStageTiming] = [] + for stage_index in range(stage_count): + input_pack = _stage_component_timing( + samples, "stage_input_pack_times", stage_index + ) + evaluator_call = _stage_component_timing( + samples, "stage_evaluator_call_times", stage_index + ) + leaf_input_pack = _stage_component_timing( + samples, "stage_leaf_input_pack_times", stage_index + ) + backend_call = _stage_component_timing( + samples, "stage_backend_call_times", stage_index + ) + evaluator_output_gather = _stage_component_timing( + samples, "stage_evaluator_output_gather_times", stage_index + ) + output_assign = _stage_component_timing( + samples, "stage_output_assign_times", stage_index + ) + if any( + value is not None + for value in ( + input_pack, + evaluator_call, + output_assign, + leaf_input_pack, + backend_call, + evaluator_output_gather, + ) + ): + stages.append( + BenchmarkStageTiming( + stage_index=stage_index + 1, + input_pack_time=input_pack, + evaluator_call_time=evaluator_call, + output_assign_time=output_assign, + leaf_input_pack_time=leaf_input_pack, + backend_call_time=backend_call, + evaluator_output_gather_time=evaluator_output_gather, + ) + ) + + execution_modes = { + sample.execution_mode for sample in samples if sample.execution_mode is not None + } + if len(execution_modes) > 1: + raise EvaluationError("native runtime profile changed execution mode") + execution_mode = cast( + Literal["compiled", "eager", "recurrence", "on-the-fly"], + next(iter(execution_modes), "compiled"), + ) + evaluator_call_time = _component_timing( + [sample.stage_evaluator_call_time for sample in samples] + ) + return BenchmarkTimingBreakdown( + sample_count=len(samples), + execution_mode=execution_mode, + wall_time=_component_timing([sample.wall_time for sample in samples]), + native_input_pack_time=_component_timing( + [sample.native_input_pack_time for sample in samples] + ), + native_input_crossing_time=_component_timing( + [sample.native_input_crossing_time for sample in samples] + ), + orchestration_time=_component_timing( + [sample.orchestration_time for sample in samples] + ), + state_prepare_time=_component_timing( + [sample.state_prepare_time for sample in samples] + ), + state_clear_time=_component_timing( + [sample.state_clear_time for sample in samples] + ), + source_fill_time=_component_timing( + [sample.source_fill_time for sample in samples] + ), + momentum_setup_time=_component_timing( + [sample.momentum_setup_time for sample in samples] + ), + momentum_input_setup_time=_component_timing( + [sample.momentum_input_setup_time for sample in samples] + ), + model_parameter_setup_time=_component_timing( + [sample.model_parameter_setup_time for sample in samples] + ), + stage_input_pack_time=_component_timing( + [sample.stage_input_pack_time for sample in samples] + ), + stage_leaf_input_pack_time=_component_timing( + [sample.stage_leaf_input_pack_time for sample in samples] + ), + stage_evaluator_call_time=( + None + if execution_mode in {"eager", *_RECURRENCE_LIKE_EXECUTION_MODES} + else evaluator_call_time + ), + stage_backend_call_time=_component_timing( + [sample.stage_backend_call_time for sample in samples] + ), + stage_evaluator_output_gather_time=_component_timing( + [sample.stage_evaluator_output_gather_time for sample in samples] + ), + output_assign_time=_component_timing( + [sample.output_assign_time for sample in samples] + ), + amplitude_input_pack_time=_component_timing( + [sample.amplitude_input_pack_time for sample in samples] + ), + amplitude_evaluator_call_time=_component_timing( + [sample.amplitude_evaluator_call_time for sample in samples] + ), + amplitude_leaf_input_pack_time=_component_timing( + [sample.amplitude_leaf_input_pack_time for sample in samples] + ), + amplitude_backend_call_time=_component_timing( + [sample.amplitude_backend_call_time for sample in samples] + ), + amplitude_evaluator_output_gather_time=_component_timing( + [sample.amplitude_evaluator_output_gather_time for sample in samples] + ), + amplitude_output_remap_time=_component_timing( + [sample.amplitude_output_remap_time for sample in samples] + ), + reduction_time=_component_timing([sample.reduction_time for sample in samples]), + total_materialization_time=_component_timing( + [sample.total_materialization_time for sample in samples] + ), + final_output_copy_time=_component_timing( + [sample.final_output_copy_time for sample in samples] + ), + selector_planner_time=_component_timing( + [sample.selector_planner_time for sample in samples] + ), + selector_gather_time=_component_timing( + [sample.selector_gather_time for sample in samples] + ), + selector_scatter_time=_component_timing( + [sample.selector_scatter_time for sample in samples] + ), + other_core_time=_component_timing( + [sample.other_core_time for sample in samples] + ), + eager_execution_time=( + evaluator_call_time if execution_mode == "eager" else None + ), + eager_initialize_time=_component_timing( + [sample.eager_initialize_time for sample in samples] + ), + eager_gather_time=_component_timing( + [sample.eager_gather_time for sample in samples] + ), + eager_kernel_call_time=_component_timing( + [sample.eager_kernel_call_time for sample in samples] + ), + eager_invocation_scatter_time=_component_timing( + [sample.eager_invocation_scatter_time for sample in samples] + ), + eager_finalization_time=_component_timing( + [sample.eager_finalization_time for sample in samples] + ), + eager_scatter_finalization_time=_component_timing( + [sample.eager_scatter_finalization_time for sample in samples] + ), + eager_closure_time=_component_timing( + [sample.eager_closure_time for sample in samples] + ), + eager_copy_out_time=_component_timing( + [sample.eager_copy_out_time for sample in samples] + ), + recurrence_momentum_fill_time=_component_timing( + [sample.recurrence_momentum_fill_time for sample in samples] + ), + recurrence_union_source_fill_time=_component_timing( + [sample.recurrence_union_source_fill_time for sample in samples] + ), + recurrence_schedule_time=_component_timing( + [sample.recurrence_schedule_time for sample in samples] + ), + recurrence_source_kernel_time=_component_timing( + [sample.recurrence_source_kernel_time for sample in samples] + ), + recurrence_contribution_kernel_time=_component_timing( + [sample.recurrence_contribution_kernel_time for sample in samples] + ), + recurrence_finalization_time=_component_timing( + [sample.recurrence_finalization_time for sample in samples] + ), + recurrence_closure_time=_component_timing( + [sample.recurrence_closure_time for sample in samples] + ), + recurrence_replay_output_mapping_time=_component_timing( + [sample.recurrence_replay_output_mapping_time for sample in samples] + ), + stages=tuple(stages), + counters=_profile_counter_summary(samples), + ) + + +def _sample_statistics(samples: list[float]) -> tuple[float, float, float, float]: + mean = statistics.fmean(samples) + deviation = statistics.stdev(samples) if len(samples) > 1 else 0.0 + error = deviation / math.sqrt(len(samples)) + relative_error = error / mean if mean > 0.0 else 0.0 + return mean, deviation, error, relative_error + + +def create_benchmark_backend( + config: BenchmarkConfig | RunConfig | None, + progress: ProgressSink | None, +) -> BenchmarkBackend: + return BenchmarkBackend(config, progress) + + +__all__ = ["BenchmarkBackend", "create_benchmark_backend"] diff --git a/pyamplicol/src/pyamplicol/cli/__init__.py b/pyamplicol/src/pyamplicol/cli/__init__.py new file mode 100644 index 0000000000..925f965073 --- /dev/null +++ b/pyamplicol/src/pyamplicol/cli/__init__.py @@ -0,0 +1,24 @@ +# SPDX-License-Identifier: 0BSD +from __future__ import annotations + +from .handlers import CliServices, DefaultCliServices, dispatch +from .licensing import LicenseRequestInvocation +from .main import main, run_cli, write_result +from .parser import CliInvocation, build_card_parser, build_parser, parse_cli +from .utilities import ExampleEntry, UtilityInvocation + +__all__ = [ + "CliInvocation", + "CliServices", + "DefaultCliServices", + "ExampleEntry", + "LicenseRequestInvocation", + "UtilityInvocation", + "build_card_parser", + "build_parser", + "dispatch", + "main", + "parse_cli", + "run_cli", + "write_result", +] diff --git a/pyamplicol/src/pyamplicol/cli/__main__.py b/pyamplicol/src/pyamplicol/cli/__main__.py new file mode 100644 index 0000000000..6ef9301e68 --- /dev/null +++ b/pyamplicol/src/pyamplicol/cli/__main__.py @@ -0,0 +1,7 @@ +# SPDX-License-Identifier: 0BSD +from __future__ import annotations + +from .main import main + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/pyamplicol/src/pyamplicol/cli/handlers.py b/pyamplicol/src/pyamplicol/cli/handlers.py new file mode 100644 index 0000000000..fcea3f4b51 --- /dev/null +++ b/pyamplicol/src/pyamplicol/cli/handlers.py @@ -0,0 +1,643 @@ +# SPDX-License-Identifier: 0BSD +from __future__ import annotations + +import json +import time +from collections.abc import Callable, Mapping +from dataclasses import fields, replace +from pathlib import Path +from typing import TYPE_CHECKING, Literal, Protocol, TypeVar, cast + +from pyamplicol.api.errors import ConfigurationError +from pyamplicol.config import ConfigResolution, EvaluatorConfig, RunConfig +from pyamplicol.reporting import ( + ProgressEnd, + ProgressSink, + ProgressStart, + ProgressUpdate, +) + +if TYPE_CHECKING: + from pyamplicol.api import ModelSource, ProcessSet + from pyamplicol.artifacts.inspection import ( + ArtifactInspection, + ArtifactProcessInspection, + ) + from pyamplicol.correlators import CorrelatorConfig + from pyamplicol.models.loading import CompiledModel as _CompiledModelPayload + +_T = TypeVar("_T") + + +class CliServices(Protocol): + """Command-level service boundary used by CLI handlers.""" + + def generate(self, config: RunConfig, progress: ProgressSink) -> object: ... + + def plan(self, config: RunConfig, progress: ProgressSink) -> object: ... + + def evaluate(self, config: RunConfig, progress: ProgressSink) -> object: ... + + def benchmark(self, config: RunConfig, progress: ProgressSink) -> object: ... + + def inspect( + self, + config: RunConfig, + progress: ProgressSink, + *, + full_physics: bool = False, + ) -> object: ... + + def model_inspect(self, config: RunConfig, progress: ProgressSink) -> object: ... + + def model_compile(self, config: RunConfig, progress: ProgressSink) -> object: ... + + def model_processes(self, config: RunConfig, progress: ProgressSink) -> object: ... + + def group_processes(self, config: RunConfig, progress: ProgressSink) -> object: ... + + +def dispatch( + config: RunConfig, + services: CliServices, + progress: ProgressSink, + *, + dry_run: bool = False, + full_physics: bool = False, +) -> object: + """Dispatch a typed config without importing a concrete domain backend.""" + + if config.action == "generate": + return ( + services.plan(config, progress) + if dry_run + else services.generate(config, progress) + ) + if config.action == "evaluate": + return services.evaluate(config, progress) + if config.action == "benchmark": + return services.benchmark(config, progress) + if config.action == "inspect": + if full_physics: + return services.inspect(config, progress, full_physics=True) + return services.inspect(config, progress) + if config.action == "model-inspect": + return services.model_inspect(config, progress) + if config.action == "model-compile": + return services.model_compile(config, progress) + if config.action == "model-processes": + return services.model_processes(config, progress) + if config.action == "group-processes": + return services.group_processes(config, progress) + raise ConfigurationError(f"unsupported action {config.action!r}") + + +def _model_source(config: RunConfig) -> ModelSource: + from pyamplicol.api import ModelSource + + return ModelSource.from_config(config.model) + + +def _process_set(config: RunConfig) -> ProcessSet: + from pyamplicol.api import ProcessRequest, ProcessSet + + requests: list[ProcessRequest] = [] + errors: list[str] = [] + for index, entry in enumerate(config.process.entries): + try: + requests.append(ProcessRequest.parse(entry.expression, name=entry.name)) + except (TypeError, ValueError) as exc: + errors.append(f"process.entries[{index}]: {exc}") + if errors: + raise ConfigurationError(errors) + try: + return ProcessSet(tuple(requests)) + except (TypeError, ValueError) as exc: + raise ConfigurationError(str(exc)) from exc + + +def _read_json(path: Path) -> object: + try: + return json.loads(path.read_text(encoding="utf-8")) + except OSError as exc: + raise ConfigurationError(f"cannot read {path}: {exc}") from exc + except json.JSONDecodeError as exc: + raise ConfigurationError(f"invalid JSON in {path}: {exc}") from exc + + +def _load_process_output( + path: Path, + progress: ProgressSink, + callback: Callable[[], _T], +) -> _T: + resolved = path.expanduser().resolve(strict=False) + task_id = "process-output-load" + started = time.perf_counter() + progress.emit( + ProgressStart( + task_id, + f"Loading process output {resolved}", + details={"step": "loading process output", "path": str(resolved)}, + ) + ) + try: + result = callback() + except Exception as exc: + progress.emit( + ProgressEnd( + task_id, + success=False, + message=str(exc), + elapsed_seconds=time.perf_counter() - started, + ) + ) + raise + progress.emit( + ProgressEnd( + task_id, + elapsed_seconds=time.perf_counter() - started, + ) + ) + return result + + +def _select_compact_process_inspection( + inspection: ArtifactInspection, + selector: str, +) -> ArtifactInspection: + """Select one representative without loading its executable runtime.""" + + def select(matches: list[ArtifactProcessInspection]) -> ArtifactInspection: + representatives = {item.id: item for item in matches} + if len(representatives) > 1: + identifiers = ", ".join(sorted(representatives)) + raise ConfigurationError( + f"inspect process selector {selector!r} is ambiguous across " + f"representatives: {identifiers}" + ) + if representatives: + selected = next(iter(representatives.values())) + groups = inspection.process_groups + if groups is not None: + groups = replace( + groups, + groups=tuple( + group + for group in groups.groups + if any(member.id == selected.id for member in group.members) + ), + ) + return replace(inspection, processes=(selected,), process_groups=groups) + groups = inspection.process_groups + for group in () if groups is None else groups.groups: + member = next( + ( + item + for item in group.members[1:] + if not item.generated and selector in {item.id, item.process} + ), + None, + ) + if member is not None: + raise ConfigurationError( + f"inspect process {member.id} ({member.process}) is a routed " + f"member of process group {group.representative_id} " + f"({group.representative}) with no runtime of its own; " + f"--process {group.representative_id} lists the group, and " + "--full-physics shows the member's relation and physics" + ) + raise ConfigurationError( + f"inspect process selector {selector!r} did not match a stable " + "representative ID, alias ID, or exact process expression; add " + "--full-physics to resolve permutations and process-group members" + ) + + identifier_matches = [ + process + for process in inspection.processes + if process.id == selector + or any(alias.id == selector for alias in process.aliases) + ] + if identifier_matches: + return select(identifier_matches) + return select( + [ + process + for process in inspection.processes + if process.expression == selector + or any(alias.expression == selector for alias in process.aliases) + ] + ) + + +def _full_physics_inspection(artifact: Path, selector: str | None) -> object: + """Return the selected process physics and, when grouped, its relation. + + Rusticol owns process selection, so side permutations and non-generated + process-group members resolve as in `evaluate`. A grouped selection keeps + every physics field at the top level and adds ``process_group``. + """ + + from pyamplicol.api import Runtime + from pyamplicol.api.results import ProcessPhysics + from pyamplicol.artifacts.inspection import process_group_selection + + runtime = Runtime.load(artifact, process=selector) + physics = runtime.physics + if not isinstance(physics, ProcessPhysics): + return physics + group = process_group_selection( + artifact, + physics.process_id, + permutation=runtime.external_permutation, + ) + if group is None: + return physics + return { + **{item.name: getattr(physics, item.name) for item in fields(physics)}, + "process_group": group, + } + + +def _compile_configured_model( + config: RunConfig, + *, + require_supported: bool, +) -> _CompiledModelPayload: + from pyamplicol.api.requests import ModelSource, _compile_model_source_payload + + payload = _compile_model_source_payload( + ModelSource.from_config(config.model), + cache_dir=config.model.cache_dir, + use_cache=config.model.cache, + require_supported=require_supported, + ) + return cast("_CompiledModelPayload", payload) + + +def _model_inspection_payload(compiled: _CompiledModelPayload) -> dict[str, object]: + return { + "name": compiled.name, + "supported": compiled.supported, + "source": dict(compiled.source), + "producer": dict(compiled.producer), + "capabilities": dict(compiled.capabilities), + "issues": [issue.to_dict() for issue in compiled.issues], + "conversion_seconds": compiled.conversion_seconds, + "phase_timings": dict(compiled.phase_timings), + "contents": { + "orders": [order.name for order in compiled.ir.orders], + "particles": [particle.to_dict() for particle in compiled.ir.particles], + "runtime_parameter_names": sorted(compiled.parameter_defaults), + }, + } + + +class DefaultCliServices: + """Thin adapters over the public API; backend imports remain first-use only.""" + + def __init__( + self, + *, + resolution: ConfigResolution | None = None, + correlators: CorrelatorConfig | None = None, + ) -> None: + self._resolution = resolution + self._correlators = correlators + + def _generation_config(self, config: RunConfig) -> RunConfig | ConfigResolution: + if self._resolution is None: + return config + if self._resolution.effective != config: + raise ConfigurationError( + "CLI generation config does not match its resolved provenance" + ) + return self._resolution + + def generate(self, config: RunConfig, progress: ProgressSink) -> object: + from pyamplicol.api import Generator + + if not config.process.entries: + raise ConfigurationError("generate requires at least one process request") + if config.generation.output is None: + raise ConfigurationError("generate requires generation.output") + processes = _process_set(config) + return Generator( + config=self._generation_config(config), progress=progress + ).generate( + processes, + config.generation.output, + model=_model_source(config), + mode=cast( + Literal["error", "append", "replace"], + str(config.generation.mode), + ), + **( + {"correlators": self._correlators} + if self._correlators is not None + else {} + ), + ) + + def plan(self, config: RunConfig, progress: ProgressSink) -> object: + from pyamplicol.api import Generator + + if not config.process.entries: + raise ConfigurationError("generate --dry-run requires a process request") + processes = _process_set(config) + return Generator( + config=self._generation_config(config), progress=progress + ).plan( + processes, + model=_model_source(config), + ) + + def evaluate(self, config: RunConfig, progress: ProgressSink) -> object: + from pyamplicol.api import Runtime + + if config.evaluation.artifact is None: + raise ConfigurationError("evaluate requires evaluation.artifact") + if config.evaluation.momenta is None: + raise ConfigurationError("evaluate requires evaluation.momenta") + parameters: ( + Mapping[ + str, + complex + | float + | int + | list[float | int] + | tuple[float | int, float | int], + ] + | None + ) = None + if config.evaluation.model_parameters is not None: + raw_parameters = _read_json(config.evaluation.model_parameters) + if not isinstance(raw_parameters, Mapping): + raise ConfigurationError("model parameter JSON must be an object") + parameters = raw_parameters + momenta = _read_json(config.evaluation.momenta) + if not isinstance(momenta, list): + raise ConfigurationError("momenta JSON must be a point list") + runtime = _load_process_output( + config.evaluation.artifact, + progress, + lambda: Runtime.load( + config.evaluation.artifact, + process=config.evaluation.process, + model_parameters=parameters, + ), + ) + selectors = { + "helicities": config.evaluation.helicity_ids or None, + "color_flows": config.evaluation.color_flow_ids or None, + } + if config.evaluation.resolved: + return runtime.evaluate_resolved( + momenta, + precision=config.evaluation.precision, + **selectors, + ) + return runtime.evaluate( + momenta, + precision=config.evaluation.precision, + **selectors, + ) + + def benchmark(self, config: RunConfig, progress: ProgressSink) -> object: + from pyamplicol.api import BenchmarkRunner + + if config.evaluation.artifact is None: + raise ConfigurationError("benchmark requires an artifact target") + points = None + if config.evaluation.momenta is not None: + raw_points = _read_json(config.evaluation.momenta) + if not isinstance(raw_points, list): + raise ConfigurationError("momenta JSON must be a point list") + points = raw_points + return BenchmarkRunner(config=config, progress=progress).run( + config.evaluation.artifact, + points=points, + ) + + def inspect( + self, + config: RunConfig, + progress: ProgressSink, + *, + full_physics: bool = False, + ) -> object: + if config.evaluation.artifact is None: + raise ConfigurationError("inspect requires evaluation.artifact") + if full_physics and config.evaluation.process is None: + raise ConfigurationError("inspect --full-physics requires --process") + + selector = config.evaluation.process + if full_physics: + artifact = config.evaluation.artifact + return _load_process_output( + artifact, + progress, + lambda: _full_physics_inspection(artifact, selector), + ) + + from pyamplicol.artifacts import inspect_artifact + + inspection = _load_process_output( + config.evaluation.artifact, + progress, + lambda: inspect_artifact(config.evaluation.artifact), + ) + if selector is None: + return inspection + return _select_compact_process_inspection(inspection, selector) + + def model_inspect(self, config: RunConfig, progress: ProgressSink) -> object: + del progress + return _model_inspection_payload( + _compile_configured_model(config, require_supported=False) + ) + + def model_compile(self, config: RunConfig, progress: ProgressSink) -> object: + if config.generation.output is None: + raise ConfigurationError("model compile requires generation.output") + requested_output = config.generation.output + prepared_output = requested_output.name.lower().endswith(".pyamplicol-model") + default_evaluator = EvaluatorConfig() + resolved_default_evaluator = replace( + default_evaluator, + jit=replace( + default_evaluator.jit, compress=default_evaluator.resolved_jit_compress + ), + ) + if not prepared_output and config.evaluator not in ( + default_evaluator, + resolved_default_evaluator, + ): + raise ConfigurationError( + "model compile evaluator settings require an output ending " + "with '.pyamplicol-model'; IR-only outputs do not compile kernels" + ) + compiled = _compile_configured_model(config, require_supported=True) + result: dict[str, object] = { + "model": compiled.name, + "supported": compiled.supported, + "conversion_seconds": compiled.conversion_seconds, + "phase_timings": dict(compiled.phase_timings), + "source": dict(compiled.source), + "capabilities": dict(compiled.capabilities), + } + if not prepared_output: + output = compiled.write(requested_output) + result["output"] = str(output) + return result + + from pyamplicol.models.prepared_compile import prepare_model_bundle + + task_id = "model-prepare" + started = False + task_started_at = time.perf_counter() + backend = str(config.evaluator.backend).upper() + + def report_preparation(label: str, completed: int, total: int) -> None: + nonlocal started + if not started: + progress.emit( + ProgressStart( + task_id=task_id, + description="Prepare model evaluator kernels", + total=total, + unit="kernels", + details={ + "step": "prepared kernel compilation", + "backend": backend, + "kernel_total": total, + }, + ) + ) + started = True + progress.emit( + ProgressUpdate( + task_id=task_id, + completed=completed, + total=total, + message=label, + details={ + "step": label, + "backend": backend, + "kernel_index": min(completed + 1, total), + "kernel_total": total, + }, + ) + ) + + try: + prepared = prepare_model_bundle( + compiled, + requested_output, + evaluator=config.evaluator, + progress=report_preparation, + ) + except BaseException: + if started: + progress.emit( + ProgressEnd( + task_id=task_id, + success=False, + message="Prepared model compilation failed", + elapsed_seconds=time.perf_counter() - task_started_at, + ) + ) + raise + if started: + progress.emit( + ProgressEnd( + task_id=task_id, + message="Prepared model compilation complete", + elapsed_seconds=time.perf_counter() - task_started_at, + ) + ) + result.update( + { + "output": str(prepared.output), + "prepared_backend": prepared.bundle.backend, + "kernel_count": prepared.kernel_count, + "preparation_phase_timings": dict(prepared.phase_timings_seconds), + } + ) + return result + + def model_processes(self, config: RunConfig, progress: ProgressSink) -> object: + del progress + if not config.process.entries: + raise ConfigurationError("model processes requires a process request") + + from pyamplicol.processes.conservation import conserved_charges + from pyamplicol.processes.model import ( + ModelParticleCatalog, + build_model_process_ir, + expand_model_processes, + ) + + compiled = _compile_configured_model(config, require_supported=True) + charges = conserved_charges(compiled.ir) + catalog = ModelParticleCatalog( + compiled.ir.name, + compiled.ir.particles, + compiled.ir.parameters, + ) + multiparticles = { + **catalog.default_multiparticles(), + **config.process.multiparticles, + } + entries: dict[str, dict[str, object]] = {} + for entry in config.process.entries: + request = entry.expression + for process in expand_model_processes( + request, + catalog, + multiparticles=multiparticles, + charges=charges, + ): + process_ir = build_model_process_ir( + process, + compiled.ir, + color_accuracy=config.color.accuracy, + ) + entries.setdefault( + process_ir.key, + { + "key": process_ir.key, + "process": process_ir.process, + "ir": process_ir.to_json_dict(), + }, + ) + concrete = list(entries.values()) + return { + "available": True, + "model": compiled.name, + "model_source": dict(compiled.source), + "requests": [entry.expression for entry in config.process.entries], + "default_key": concrete[0]["key"] if concrete else None, + "n_entries": len(concrete), + "multiparticles": { + name: list(values) for name, values in sorted(multiparticles.items()) + }, + "entries": concrete, + } + + def group_processes(self, config: RunConfig, progress: ProgressSink) -> object: + del progress + if config.evaluation.artifact is None: + raise ConfigurationError("group-processes requires an artifact") + + from pyamplicol.api.process_groups import group_processes + + return group_processes( + config.evaluation.artifact, + analysis=str(config.process.analyze_process_groups), + only_unit_multiplier=config.process.only_unit_multiplier, + consider_charge_conjugation=config.process.consider_charge_conjugation, + ) + + +__all__ = ["CliServices", "DefaultCliServices", "dispatch"] diff --git a/pyamplicol/src/pyamplicol/cli/licensing.py b/pyamplicol/src/pyamplicol/cli/licensing.py new file mode 100644 index 0000000000..cb9ae458b0 --- /dev/null +++ b/pyamplicol/src/pyamplicol/cli/licensing.py @@ -0,0 +1,85 @@ +# SPDX-License-Identifier: 0BSD +"""Transient CLI requests for Symbolica licenses.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Literal, TextIO + +from pyamplicol.licensing import request_hobbyist_license, request_trial_license + +LicenseKind = Literal["trial", "hobbyist"] + + +@dataclass(frozen=True, slots=True) +class LicenseRequestResult: + request: LicenseKind + status: Literal["accepted"] = "accepted" + next_step: str = "The license key will be sent by email." + + +@dataclass(frozen=True, slots=True) +class LicenseRequestInvocation: + kind: LicenseKind + name: str | None + email: str | None + organization: str | None = None + assume_yes: bool = False + output_format: Literal["human", "json"] = "human" + output_color: Literal["auto", "always", "never"] = "auto" + + def run(self, *, stdin: TextIO, stdout: TextIO) -> LicenseRequestResult: + if self.output_format == "json" and ( + self.name is None + or self.email is None + or (self.kind == "trial" and self.organization is None) + or not self.assume_yes + ): + raise ValueError( + "--json license requests require all fields and --yes; " + "interactive prompts are disabled to keep stdout machine-readable" + ) + name = self.name or _prompt(stdin, stdout, "Name") + email = self.email or _prompt(stdin, stdout, "Email") + organization: str | None = None + if self.kind == "trial": + organization = self.organization or _prompt( + stdin, + stdout, + "Organization", + ) + if not self.assume_yes: + if not bool(getattr(stdin, "isatty", lambda: False)()): + raise ValueError( + "noninteractive license requests require complete fields and --yes" + ) + answer = _prompt( + stdin, + stdout, + "Submit this request to Symbolica? [y/N]", + ) + if answer.casefold() not in {"y", "yes"}: + raise ValueError("Symbolica license request cancelled") + if self.kind == "trial": + assert organization is not None + request_trial_license(name, email, organization) + else: + request_hobbyist_license(name, email) + return LicenseRequestResult(self.kind) + + +def _prompt(stdin: TextIO, stdout: TextIO, label: str) -> str: + if not bool(getattr(stdin, "isatty", lambda: False)()): + raise ValueError(f"noninteractive license request is missing {label.lower()}") + stdout.write(f"{label}: ") + stdout.flush() + value = stdin.readline() + if not value: + raise ValueError("unexpected end of input during license request") + value = value.strip() + if not value: + raise ValueError(f"{label} must not be empty") + return value + + +__all__ = ["LicenseKind", "LicenseRequestInvocation", "LicenseRequestResult"] diff --git a/pyamplicol/src/pyamplicol/cli/main.py b/pyamplicol/src/pyamplicol/cli/main.py new file mode 100644 index 0000000000..de3bd62872 --- /dev/null +++ b/pyamplicol/src/pyamplicol/cli/main.py @@ -0,0 +1,221 @@ +# SPDX-License-Identifier: 0BSD +from __future__ import annotations + +import json +import sys +from collections.abc import Mapping, Sequence +from dataclasses import fields, is_dataclass +from decimal import Decimal +from pathlib import Path +from typing import TextIO + +from pyamplicol.config import PyAmpliColError +from pyamplicol.licensing import detect_symbolica_license +from pyamplicol.reporting import ( + TtyProgressSink, + close_progress_sink, + configure_cli_logging, + get_logger, + progress_sink, + render_summary, + reset_cli_logging, +) + +from .handlers import CliServices, DefaultCliServices, dispatch +from .licensing import LicenseRequestInvocation +from .parser import parse_cli +from .utilities import UtilityInvocation, example_card, execute_utility + + +def _json_value(value: object) -> object: + if is_dataclass(value) and not isinstance(value, type): + return { + item.name: _json_value(getattr(value, item.name)) for item in fields(value) + } + if isinstance(value, Path): + return str(value) + if isinstance(value, Mapping): + return {str(key): _json_value(entry) for key, entry in value.items()} + if isinstance(value, (tuple, list)): + return [_json_value(entry) for entry in value] + if isinstance(value, complex): + return {"real": value.real, "imag": value.imag} + if isinstance(value, Decimal): + return str(value) + if isinstance(value, (str, int, float, bool)) or value is None: + return value + raise TypeError(f"cannot serialize CLI result of type {type(value).__name__}") + + +def write_result( + value: object, + *, + format: str, + stream: TextIO, + color: bool = False, +) -> None: + if value is None: + return + plain = _json_value(value) + if format == "json": + json.dump(plain, stream, indent=2, sort_keys=True, allow_nan=False) + stream.write("\n") + elif format == "human": + rendered = render_summary(value, color=color) + if rendered is None: + raise RuntimeError( + "human table rendering is unavailable; use --json for " + "machine-readable output" + ) + stream.write(f"{rendered}\n") + else: + raise ValueError(f"unsupported output format {format!r}") + stream.flush() + + +def _color_enabled(mode: str, stream: TextIO) -> bool: + return mode == "always" or ( + mode == "auto" and bool(getattr(stream, "isatty", lambda: False)()) + ) + + +def run_cli( + argv: Sequence[str] | None = None, + *, + services: CliServices | None = None, + stdin: TextIO | None = None, + stdout: TextIO | None = None, + stderr: TextIO | None = None, +) -> int: + output_stream = sys.stdout if stdout is None else stdout + diagnostic_stream = sys.stderr if stderr is None else stderr + input_stream = sys.stdin if stdin is None else stdin + logging_configured = False + sink = None + try: + invocation = parse_cli(argv) + if isinstance(invocation, LicenseRequestInvocation): + result = invocation.run(stdin=input_stream, stdout=output_stream) + write_result( + result, + format=invocation.output_format, + stream=output_stream, + color=_color_enabled(invocation.output_color, output_stream), + ) + return 0 + if isinstance(invocation, UtilityInvocation): + if invocation.kind == "examples-run": + assert invocation.name is not None + arguments = [str(example_card(invocation.name))] + for override in invocation.overrides: + arguments.extend(("--set", override)) + if invocation.output_format == "json": + arguments.append("--json") + arguments.extend(("--color", invocation.output_color)) + invocation = parse_cli(arguments) + assert not isinstance( + invocation, (LicenseRequestInvocation, UtilityInvocation) + ) + else: + result = execute_utility(invocation) + write_result( + result, + format=invocation.output_format, + stream=output_stream, + color=_color_enabled(invocation.output_color, output_stream), + ) + if invocation.kind in {"doctor", "self-test"}: + return 0 if bool(getattr(result, "ok", False)) else 1 + return 0 + resolution = invocation.resolve() + if services is None and resolution.effective.action == "model-compile": + initial = resolution.effective + detect_symbolica_license( + suggest=initial.symbolica.suggest_license, + json_mode=initial.output.format == "json", + stream=diagnostic_stream, + ) + config = resolution.effective + progress_color = _color_enabled(config.output.color, diagnostic_stream) + sink = progress_sink( + config.output.progress, + stream=diagnostic_stream, + logger=get_logger("progress"), + color=progress_color, + ) + logging_stream = ( + sink.message_stream + if isinstance(sink, TtyProgressSink) + else diagnostic_stream + ) + configure_cli_logging( + config.output.log_level, + stream=logging_stream, + color=progress_color, + ) + logging_configured = True + selected_services = ( + DefaultCliServices(resolution=resolution) if services is None else services + ) + if invocation.correlators is not None: + from pyamplicol.correlators import CorrelatorConfig + + if invocation.dry_run: + raise ValueError("--correlators requires generation, not --dry-run") + if services is not None: + raise ValueError( + "--correlators requires the built-in generation service" + ) + declarations = CorrelatorConfig.from_json_dict( + json.loads(invocation.correlators.read_text()) + ) + selected_services = DefaultCliServices( + resolution=resolution, correlators=declarations + ) + result = dispatch( + config, + selected_services, + sink, + dry_run=invocation.dry_run, + full_physics=invocation.full_physics, + ) + close_progress_sink(sink) + sink = None + color = _color_enabled(config.output.color, output_stream) + write_result( + result, + format=config.output.format, + stream=output_stream, + color=color, + ) + return 130 if bool(getattr(result, "interrupted", False)) else 0 + except (PyAmpliColError, OSError, RuntimeError, TypeError, ValueError) as exc: + close_progress_sink(sink) + sink = None + if logging_configured: + get_logger("cli").error("%s", exc) + else: + diagnostic_stream.write(f"error: {exc}\n") + diagnostic_stream.flush() + return 2 + except KeyboardInterrupt: + close_progress_sink(sink) + sink = None + message = "interrupted before a complete result was available" + if logging_configured: + get_logger("cli").warning("%s", message) + else: + diagnostic_stream.write(f"{message}\n") + diagnostic_stream.flush() + return 130 + finally: + close_progress_sink(sink) + if logging_configured: + reset_cli_logging() + + +def main(argv: Sequence[str] | None = None) -> int: + return run_cli(argv) + + +__all__ = ["main", "run_cli", "write_result"] diff --git a/pyamplicol/src/pyamplicol/cli/parser.py b/pyamplicol/src/pyamplicol/cli/parser.py new file mode 100644 index 0000000000..f6699b0868 --- /dev/null +++ b/pyamplicol/src/pyamplicol/cli/parser.py @@ -0,0 +1,1045 @@ +# SPDX-License-Identifier: 0BSD +from __future__ import annotations + +import argparse +import os +import sys +from collections.abc import Mapping, Sequence +from dataclasses import dataclass +from pathlib import Path +from types import MappingProxyType + +from pyamplicol.config import ( + ACTIONS, + Action, + ClampRequest, + ColorContraction, + ColorFFTBasis, + ConfigResolution, + ConfigurationError, + EvaluatorExecutionMode, + ProcessGroupAnalysis, + resolve_config, +) + +from .licensing import LicenseKind, LicenseRequestInvocation +from .utilities import UtilityInvocation, parse_utility + +_LICENSE_ACTIONS: dict[str, LicenseKind] = { + "request-symbolica-trial-license": "trial", + "request-symbolica-hobbyist-license": "hobbyist", +} +_MODEL_ACTIONS: dict[str, Action] = { + "inspect": Action.MODEL_INSPECT, + "compile": Action.MODEL_COMPILE, + "processes": Action.MODEL_PROCESSES, +} +_UTILITY_COMMANDS = frozenset( + {"config", "examples", "profiling-campaign", "doctor", "self-test"} +) +_DIRECT_COMMANDS = frozenset( + { + *(action.value for action in ACTIONS if not action.value.startswith("model-")), + "profile", + "model", + *_LICENSE_ACTIONS, + } +) +_REMOVED_FLAT_MODEL_COMMANDS = frozenset( + action.value for action in _MODEL_ACTIONS.values() +) + + +def _freeze(value: object) -> object: + if isinstance(value, Mapping): + return MappingProxyType( + {str(key): _freeze(entry) for key, entry in value.items()} + ) + if isinstance(value, (list, tuple)): + return tuple(_freeze(entry) for entry in value) + return value + + +def _auto_int(value: str) -> str | int: + if value == "auto": + return value + try: + return int(value) + except ValueError as exc: + raise argparse.ArgumentTypeError("expected 'auto' or an integer") from exc + + +def _nullable_int(value: str) -> int | None: + if value.lower() in ("none", "null"): + return None + try: + return int(value) + except ValueError as exc: + raise argparse.ArgumentTypeError("expected an integer or 'null'") from exc + + +def _key_list(value: str) -> tuple[str, tuple[str, ...]]: + if "=" not in value: + raise argparse.ArgumentTypeError("expected NAME=ITEM[,ITEM...]") + key, raw_entries = value.split("=", 1) + entries = tuple(entry.strip() for entry in raw_entries.split(",") if entry.strip()) + if not key.strip() or not entries: + raise argparse.ArgumentTypeError("expected NAME=ITEM[,ITEM...]") + return key.strip(), entries + + +def _key_int(value: str) -> tuple[str, int]: + if "=" not in value: + raise argparse.ArgumentTypeError("expected NAME=INTEGER") + key, raw_entry = value.split("=", 1) + try: + entry = int(raw_entry) + except ValueError as exc: + raise argparse.ArgumentTypeError("expected NAME=INTEGER") from exc + if not key.strip(): + raise argparse.ArgumentTypeError("expected NAME=INTEGER") + return key.strip(), entry + + +@dataclass(frozen=True, slots=True) +class CliInvocation: + action: Action | None + card: Path | None + dedicated: Mapping[str, object] + overrides: tuple[str, ...] + dry_run: bool = False + full_physics: bool = False + correlators: Path | None = None + + def __post_init__(self) -> None: + frozen = _freeze(self.dedicated) + assert isinstance(frozen, Mapping) + object.__setattr__(self, "dedicated", frozen) + object.__setattr__(self, "overrides", tuple(self.overrides)) + + def resolve( + self, + *, + clamps: Sequence[ClampRequest] = (), + ) -> ConfigResolution: + if any( + str(override).partition("=")[0].strip() == "output.format" + and str(override).partition("=")[2].strip().strip("\"'").casefold() + == "json" + for override in self.overrides + ): + raise ConfigurationError( + "CLI JSON output is selected only with --json; " + "remove the output.format override" + ) + return resolve_config( + self.card, + action=self.action, + dedicated=self.dedicated, + overrides=self.overrides, + clamps=clamps, + ) + + +def _common_parent(*, include_card: bool = True) -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(add_help=False) + if include_card: + parser.add_argument( + "--card", + type=Path, + default=argparse.SUPPRESS, + help="Read defaults from a TOML run card.", + ) + parser.add_argument( + "--set", + dest="_overrides", + action="append", + default=argparse.SUPPRESS, + metavar="PATH=VALUE", + help="Override one schema field; repeat to apply overrides in order.", + ) + parser.add_argument( + "--json", + dest="_json_output", + action="store_true", + default=argparse.SUPPRESS, + help="Emit machine-readable JSON instead of human-facing tables.", + ) + parser.add_argument( + "--color", + dest="output.color", + choices=("auto", "always", "never"), + default=argparse.SUPPRESS, + ) + parser.add_argument( + "--progress", + dest="output.progress", + choices=("auto", "tty", "log", "off"), + default=argparse.SUPPRESS, + ) + parser.add_argument( + "--log-level", + dest="output.log_level", + choices=("debug", "info", "warning", "error"), + default=argparse.SUPPRESS, + ) + parser.add_argument( + "--symbolica-suggestion", + dest="symbolica.suggest_license", + action=argparse.BooleanOptionalAction, + default=argparse.SUPPRESS, + ) + return parser + + +def _add_model_options(parser: argparse.ArgumentParser) -> None: + parser.add_argument("--model", dest="model.source", default=argparse.SUPPRESS) + parser.add_argument( + "--restriction", dest="model.restriction", default=argparse.SUPPRESS + ) + parser.add_argument( + "--simplify", + dest="model.simplify", + action=argparse.BooleanOptionalAction, + default=argparse.SUPPRESS, + ) + parser.add_argument( + "--model-cache", + dest="model.cache", + action=argparse.BooleanOptionalAction, + default=argparse.SUPPRESS, + ) + parser.add_argument( + "--model-cache-dir", + dest="model.cache_dir", + type=Path, + default=argparse.SUPPRESS, + ) + + +def _add_process_options(parser: argparse.ArgumentParser) -> None: + parser.add_argument( + "--name", + dest="_process_names", + action="append", + default=argparse.SUPPRESS, + ) + parser.add_argument( + "--multiparticle", + dest="_multiparticles", + type=_key_list, + action="append", + default=argparse.SUPPRESS, + metavar="NAME=ITEM[,ITEM...]", + ) + parser.add_argument( + "--flavor-scheme", + dest="process.flavor_scheme", + type=int, + default=argparse.SUPPRESS, + ) + parser.add_argument( + "--max-quark-lines", + dest="process.max_quark_lines", + type=_nullable_int, + default=argparse.SUPPRESS, + ) + parser.add_argument( + "--coupling-order-policy", + dest="process.coupling_order_policy", + choices=("minimal", "explicit"), + default=argparse.SUPPRESS, + ) + parser.add_argument( + "--max-coupling-order", + dest="_max_coupling_orders", + type=_key_int, + action="append", + default=argparse.SUPPRESS, + metavar="NAME=INTEGER", + ) + + +def _add_process_grouping_options( + parser: argparse.ArgumentParser, + *, + generation: bool, +) -> None: + if generation: + parser.add_argument( + "--flavor-blind", + dest="process.flavor_blind", + action="store_true", + default=argparse.SUPPRESS, + help=( + "generate one representative per flavour-relabelling class and " + "record the others as its group members (assumed, not verified)" + ), + ) + parser.add_argument( + "--consider-charge-conjugation", + dest="process.consider_charge_conjugation", + action=argparse.BooleanOptionalAction, + default=argparse.SUPPRESS, + help="also relate processes through charge conjugation (default: on)", + ) + parser.add_argument( + "--only-unit-multiplier", + dest="process.only_unit_multiplier", + action="store_true", + default=argparse.SUPPRESS, + help="accept only numerically found relations with factor 1", + ) + parser.add_argument( + "--analyze-process-groups", + dest="process.analyze_process_groups", + choices=tuple(ProcessGroupAnalysis), + default=argparse.SUPPRESS if generation else ProcessGroupAnalysis.NUMERICAL, + help=( + "search for subprocesses with equal matrix elements up to a " + f"rational factor (default: {'none' if generation else 'numerical'})" + ), + ) + + +def _add_color_options(parser: argparse.ArgumentParser) -> None: + parser.add_argument( + "--color-accuracy", + dest="color.accuracy", + choices=("lc", "nlc", "full"), + default=argparse.SUPPRESS, + ) + contraction = parser.add_mutually_exclusive_group() + contraction.add_argument( + "--color-contraction", + dest="color.contraction", + choices=tuple(ColorContraction), + default=argparse.SUPPRESS, + help=( + "Default: auto, using adaptive FFT for supported NLC/full recurrence " + "or on-the-fly plans and direct otherwise. Select direct to opt out." + ), + ) + contraction.add_argument( + "--fft", + dest="_fft_basis", + choices=tuple(ColorFFTBasis), + default=argparse.SUPPRESS, + help=( + "Force symmetric-group FFT contraction. 'adjoint' selects certified " + "DDM where applicable, otherwise exact fundamental chains or an " + "explicit trace fallback; requires --color-accuracy nlc or full." + ), + ) + parser.add_argument( + "--lc-flow-layout", + dest="color.lc_flow_layout", + choices=("topology-replay", "all-flow-union"), + default=argparse.SUPPRESS, + ) + + +def _add_generation_options(parser: argparse.ArgumentParser) -> None: + parser.add_argument( + "--mode", + dest="generation.mode", + choices=("error", "append", "replace"), + default=argparse.SUPPRESS, + ) + parser.add_argument( + "--force", + dest="generation.mode", + action="store_const", + const="replace", + default=argparse.SUPPRESS, + help="atomically replace an existing output artifact", + ) + parser.add_argument( + "--workers", + dest="generation.workers", + type=_auto_int, + default=argparse.SUPPRESS, + ) + parser.add_argument( + "--emit-api-bundle", + dest="generation.emit_api_bundle", + action=argparse.BooleanOptionalAction, + default=argparse.SUPPRESS, + ) + parser.add_argument( + "--validation", + dest="generation.validation.enabled", + action=argparse.BooleanOptionalAction, + default=argparse.SUPPRESS, + ) + parser.add_argument( + "--validation-samples", + dest="generation.validation.samples", + type=int, + default=argparse.SUPPRESS, + ) + parser.add_argument( + "--validation-seed", + dest="generation.validation.seed", + type=int, + default=argparse.SUPPRESS, + ) + parser.add_argument( + "--relative-tolerance", + dest="generation.validation.relative_tolerance", + type=float, + default=argparse.SUPPRESS, + ) + parser.add_argument( + "--absolute-tolerance", + dest="generation.validation.absolute_tolerance", + type=float, + default=argparse.SUPPRESS, + ) + parser.add_argument( + "--post-build-validation", + dest="generation.validation.post_build_validation", + action=argparse.BooleanOptionalAction, + default=argparse.SUPPRESS, + help=( + "re-open and numerically smoke-test the written artifact (off by default)" + ), + ) + numerical_reuse = parser.add_mutually_exclusive_group() + numerical_reuse.add_argument( + "--numerical-current-reuse", + dest="generation.relation_discovery.mode", + action="store_const", + const="certified-reuse", + default=argparse.SUPPRESS, + help=( + "search for and apply independently certified numerical current " + "relations (off by default: the high-precision probes are costly at " + "generation time)" + ), + ) + numerical_reuse.add_argument( + "--no-numerical-current-reuse", + dest="generation.relation_discovery.mode", + action="store_const", + const="off", + default=argparse.SUPPRESS, + help="disable numerical current-relation discovery and reuse (the default)", + ) + + +def _add_evaluator_options( + parser: argparse.ArgumentParser, + *, + include_runtime_controls: bool = True, +) -> None: + parser.add_argument( + "--backend", + dest="evaluator.backend", + choices=("jit", "asm", "cpp"), + default=argparse.SUPPRESS, + ) + if include_runtime_controls: + parser.add_argument( + "--execution-mode", + dest="evaluator.execution_mode", + choices=tuple(mode.value for mode in EvaluatorExecutionMode), + default=argparse.SUPPRESS, + ) + parser.add_argument( + "--batch-size", + dest="evaluator.batch_size", + type=int, + default=argparse.SUPPRESS, + ) + parser.add_argument( + "--output-chunk-size", + dest="evaluator.output_chunk_size", + type=_nullable_int, + default=argparse.SUPPRESS, + ) + parser.add_argument( + "--horner-iterations", + dest="evaluator.optimization.horner_iterations", + type=int, + default=argparse.SUPPRESS, + ) + parser.add_argument( + "--cpe-iterations", + dest="evaluator.optimization.cpe_iterations", + type=_nullable_int, + default=argparse.SUPPRESS, + ) + parser.add_argument( + "--cores", + dest="evaluator.optimization.cores", + type=_auto_int, + default=argparse.SUPPRESS, + ) + parser.add_argument( + "--max-horner-variables", + dest="evaluator.optimization.max_horner_variables", + type=int, + default=argparse.SUPPRESS, + ) + parser.add_argument( + "--max-common-pair-cache-entries", + dest="evaluator.optimization.max_common_pair_cache_entries", + type=int, + default=argparse.SUPPRESS, + ) + parser.add_argument( + "--max-common-pair-distance", + dest="evaluator.optimization.max_common_pair_distance", + type=int, + default=argparse.SUPPRESS, + ) + parser.add_argument( + "--collect-factors", + dest="evaluator.optimization.collect_factors", + action=argparse.BooleanOptionalAction, + default=argparse.SUPPRESS, + ) + parser.add_argument( + "--jit-optimization-level", + dest="evaluator.jit.optimization_level", + type=int, + choices=(0, 1, 2, 3), + default=argparse.SUPPRESS, + ) + parser.add_argument( + "--jit-compress", + dest="evaluator.jit.compress", + action=argparse.BooleanOptionalAction, + default=argparse.SUPPRESS, + ) + parser.add_argument( + "--cpp-optimization", + dest="evaluator.cpp.optimization", + default=argparse.SUPPRESS, + ) + parser.add_argument( + "--cpp-compiler", + dest="evaluator.cpp.compiler", + default=argparse.SUPPRESS, + ) + parser.add_argument( + "--cpp-native-arch", + dest="evaluator.cpp.native_arch", + action=argparse.BooleanOptionalAction, + default=argparse.SUPPRESS, + ) + parser.add_argument( + "--cpp-extra-flag", + dest="evaluator.cpp.extra_flags", + action="append", + default=argparse.SUPPRESS, + ) + + +def _add_evaluation_options( + parser: argparse.ArgumentParser, *, include_artifact: bool = True +) -> None: + if include_artifact: + parser.add_argument("artifact", type=Path, nargs="?", default=None) + parser.add_argument( + "--process", + dest="evaluation.process", + default=argparse.SUPPRESS, + help="stable process/alias ID or exact concrete process expression", + ) + parser.add_argument( + "--precision", + dest="evaluation.precision", + type=int, + default=argparse.SUPPRESS, + ) + parser.add_argument( + "--resolved", + dest="evaluation.resolved", + action=argparse.BooleanOptionalAction, + default=argparse.SUPPRESS, + ) + parser.add_argument( + "--helicity", + dest="evaluation.helicity_ids", + action="append", + default=argparse.SUPPRESS, + ) + parser.add_argument( + "--color-flow", + dest="evaluation.color_flow_ids", + action="append", + default=argparse.SUPPRESS, + metavar="ID_OR_NUMBER", + help="stable color-flow ID or one-based ordinal shown by inspect", + ) + parser.add_argument( + "--model-parameters", + dest="evaluation.model_parameters", + type=Path, + default=argparse.SUPPRESS, + ) + parser.add_argument( + "--momenta", + dest="evaluation.momenta", + type=Path, + default=argparse.SUPPRESS, + ) + + +def _add_profile_options(parser: argparse.ArgumentParser) -> None: + parser.add_argument("target", type=Path, nargs="?", default=None, metavar="OUTPUT") + parser.add_argument( + "--process", + dest="evaluation.process", + default=argparse.SUPPRESS, + metavar="PROCESS", + help="select a stable process/alias ID or exact process expression", + ) + parser.add_argument( + "--target-runtime", + dest="benchmark.target_runtime", + type=float, + default=argparse.SUPPRESS, + metavar="SECONDS", + help=( + "target cumulative Rusticol timing duration; Ctrl-C reports " + "statistics from complete samples collected so far" + ), + ) + parser.add_argument( + "--batch-size", + dest="benchmark.batch_size", + type=int, + default=argparse.SUPPRESS, + metavar="POINTS", + help="number of phase-space points evaluated by each runtime call", + ) + parser.add_argument( + "--precision", + dest="benchmark.precision", + type=int, + default=argparse.SUPPRESS, + metavar="DIGITS", + help="decimal digits used by the Python runtime evaluator", + ) + parser.add_argument( + "--warmup-runs", + dest="benchmark.warmup_runs", + type=int, + default=argparse.SUPPRESS, + metavar="COUNT", + ) + parser.add_argument( + "--minimum-samples", + dest="benchmark.minimum_samples", + type=int, + default=argparse.SUPPRESS, + metavar="COUNT", + help="minimum number of independent timed blocks", + ) + parser.add_argument( + "--helicity", + dest="benchmark.helicity_ids", + action="append", + default=argparse.SUPPRESS, + metavar="ID", + help=( + "stable helicity ID; omitting both selector options chooses the " + "artifact layout's optimized profiling workload" + ), + ) + parser.add_argument( + "--color-flow", + dest="benchmark.color_flow_ids", + action="append", + default=argparse.SUPPRESS, + metavar="ID_OR_NUMBER", + help=( + "stable color-flow ID or one-based ordinal shown by inspect; " + "omitting both selector options chooses the artifact layout's " + "optimized profiling workload" + ), + ) + parser.add_argument( + "--momenta", + dest="evaluation.momenta", + type=Path, + default=argparse.SUPPRESS, + metavar="PATH", + ) + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + prog="pyamplicol", + description=( + "Generate, inspect, evaluate, and profile pyAmpliCol process artifacts." + ), + ) + subparsers = parser.add_subparsers(dest="_action", required=True) + common = _common_parent() + + generate = subparsers.add_parser( + "generate", + parents=[common], + help="Generate a process artifact.", + ) + generate.add_argument("process", nargs="?", default=None) + generate.add_argument("output", type=Path, nargs="?", default=None) + generate.add_argument( + "--process", + dest="_extra_processes", + action="append", + default=argparse.SUPPRESS, + ) + generate.add_argument( + "--dry-run", + dest="_dry_run", + action="store_true", + help="Plan and validate generation without writing an artifact.", + ) + _add_model_options(generate) + _add_process_options(generate) + _add_process_grouping_options(generate, generation=True) + _add_color_options(generate) + _add_generation_options(generate) + generate.add_argument( + "--correlators", + dest="_correlators", + type=Path, + default=argparse.SUPPRESS, + help="JSON declaration of colour IDs and allowed joint spin-correlation legs", + ) + _add_evaluator_options(generate) + + evaluate = subparsers.add_parser( + "evaluate", + parents=[common], + help="Evaluate momenta with a generated artifact.", + ) + _add_evaluation_options(evaluate) + + profile = subparsers.add_parser( + "profile", + parents=[common], + help="Profile a generated artifact.", + ) + _add_profile_options(profile) + + benchmark = subparsers.add_parser( + "benchmark", + parents=[common], + help="Compatibility alias for the profile command.", + ) + _add_profile_options(benchmark) + + inspect = subparsers.add_parser( + "inspect", + parents=[common], + help="Summarize a generated artifact and its processes.", + ) + inspect.add_argument("artifact", type=Path, nargs="?", default=None) + inspect.add_argument( + "--process", + dest="evaluation.process", + default=argparse.SUPPRESS, + help=( + "show compact metadata for one stable process/alias ID or exact " + "concrete process expression" + ), + ) + inspect.add_argument( + "--full-physics", + dest="_full_physics", + action="store_true", + help=( + "load the selected runtime and materialize complete helicity/color " + "physics (potentially very large at high multiplicity)" + ), + ) + + group_processes = subparsers.add_parser( + "group-processes", + parents=[common], + help=( + "Find generated subprocesses with equal matrix elements and update " + "the artifact's process-groups.json in place." + ), + ) + group_processes.add_argument("artifact", type=Path, nargs="?", default=None) + _add_process_grouping_options(group_processes, generation=False) + + model = subparsers.add_parser( + "model", + help="Inspect, compile, or enumerate processes for a model.", + ) + model_commands = model.add_subparsers(dest="_model_action", required=True) + + model_inspect = model_commands.add_parser( + "inspect", + parents=[common], + help="Inspect a built-in, UFO, JSON, or compiled model.", + ) + model_inspect.add_argument("source", nargs="?", default=None) + _add_model_options(model_inspect) + + model_compile = model_commands.add_parser( + "compile", + parents=[common], + help=( + "Compile portable model IR or prepare one evaluator kernel bundle " + "shared by eager, recurrence, and on-the-fly execution." + ), + ) + model_compile.add_argument("source", nargs="?", default=None) + model_compile.add_argument( + "output", + type=Path, + nargs="?", + default=None, + help=( + "IR output path, or a .pyamplicol-model path for a self-contained " + "prepared bundle" + ), + ) + _add_model_options(model_compile) + _add_generation_options(model_compile) + _add_evaluator_options(model_compile, include_runtime_controls=False) + + model_processes = model_commands.add_parser( + "processes", + parents=[common], + help="Expand concrete processes using a model's particle catalog.", + ) + model_processes.add_argument("process", nargs="?", default=None) + model_processes.add_argument( + "--process", + dest="_extra_processes", + action="append", + default=argparse.SUPPRESS, + ) + _add_model_options(model_processes) + _add_process_options(model_processes) + + for name, help_text in ( + ("config", "Create or resolve run configuration."), + ("examples", "List, copy, or run packaged examples."), + ("profiling-campaign", "Copy the packaged profiling campaign."), + ("doctor", "Diagnose the installed pyAmpliCol environment."), + ("self-test", "Run the installed-package self-test."), + ): + subparsers.add_parser(name, help=help_text) + + trial = subparsers.add_parser( + "request-symbolica-trial-license", + help="Request a Symbolica trial license.", + ) + trial.add_argument("--name") + trial.add_argument("--email") + trial.add_argument("--organization") + trial.add_argument("--yes", dest="assume_yes", action="store_true") + trial.add_argument("--json", dest="_json_output", action="store_true") + trial.add_argument( + "--color", + dest="_output_color", + choices=("auto", "always", "never"), + default="auto", + ) + + hobbyist = subparsers.add_parser( + "request-symbolica-hobbyist-license", + help="Request a Symbolica hobbyist license.", + ) + hobbyist.add_argument("--name") + hobbyist.add_argument("--email") + hobbyist.add_argument("--yes", dest="assume_yes", action="store_true") + hobbyist.add_argument("--json", dest="_json_output", action="store_true") + hobbyist.add_argument( + "--color", + dest="_output_color", + choices=("auto", "always", "never"), + default="auto", + ) + return parser + + +def build_card_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + prog="pyamplicol", parents=[_common_parent(include_card=False)] + ) + parser.add_argument("card", type=Path) + return parser + + +def _is_prepared_model_output(value: object) -> bool: + if value is None or value == argparse.SUPPRESS: + return False + try: + return str(os.fspath(value)).lower().endswith(".pyamplicol-model") + except TypeError: + return False + + +def _namespace_to_invocation( + namespace: argparse.Namespace, +) -> CliInvocation | LicenseRequestInvocation: + raw = vars(namespace).copy() + action_value = raw.pop("_action", None) + model_action = raw.pop("_model_action", None) + if action_value == "model": + if model_action not in _MODEL_ACTIONS: + raise ValueError("model requires an inspect, compile, or processes command") + action_value = _MODEL_ACTIONS[model_action].value + if action_value == "profile": + action_value = Action.BENCHMARK.value + if action_value in _LICENSE_ACTIONS: + return LicenseRequestInvocation( + kind=_LICENSE_ACTIONS[action_value], + name=raw.pop("name", None), + email=raw.pop("email", None), + organization=raw.pop("organization", None), + assume_yes=bool(raw.pop("assume_yes", False)), + output_format="json" if bool(raw.pop("_json_output", False)) else "human", + output_color=str(raw.pop("_output_color", "auto")), + ) + action = Action(action_value) if action_value is not None else None + if action is Action.MODEL_COMPILE: + evaluator_arguments = tuple( + sorted(key for key in raw if key.startswith("evaluator.")) + ) + evaluator_overrides = tuple( + value + for value in raw.get("_overrides", ()) + if str(value).partition("=")[0].strip().startswith("evaluator.") + ) + has_evaluator_options = evaluator_arguments or evaluator_overrides + if has_evaluator_options and not _is_prepared_model_output(raw.get("output")): + raise ConfigurationError( + "model compile evaluator options require an output ending with " + "'.pyamplicol-model'; IR-only model outputs do not compile kernels" + ) + card_value = raw.pop("card", None) + card = ( + Path(os.fspath(card_value)).expanduser().resolve(strict=False) + if card_value is not None + else None + ) + overrides = tuple(raw.pop("_overrides", ())) + raw["output.format"] = "json" if bool(raw.pop("_json_output", False)) else "human" + dry_run = bool(raw.pop("_dry_run", False)) + full_physics = bool(raw.pop("_full_physics", False)) + correlators = raw.pop("_correlators", None) + + positional_process = raw.pop("process", None) + extra_processes = tuple(raw.pop("_extra_processes", ())) + process_names = tuple(raw.pop("_process_names", ())) + if positional_process == argparse.SUPPRESS: + positional_process = None + process_expressions = tuple( + entry for entry in (positional_process, *extra_processes) if entry is not None + ) + if process_names and len(process_names) != len(process_expressions): + raise ConfigurationError( + "direct process names must be empty or aligned with process expressions" + ) + if process_expressions: + raw["process.entries"] = tuple( + { + "expression": expression, + **({"name": process_names[index]} if process_names else {}), + } + for index, expression in enumerate(process_expressions) + ) + + positional_output = raw.pop("output", None) + if positional_output != argparse.SUPPRESS and positional_output is not None: + raw["generation.output"] = positional_output + positional_artifact = raw.pop("artifact", None) + if positional_artifact != argparse.SUPPRESS and positional_artifact is not None: + raw["evaluation.artifact"] = positional_artifact + positional_target = raw.pop("target", None) + if positional_target != argparse.SUPPRESS and positional_target is not None: + raw["evaluation.artifact"] = positional_target + positional_source = raw.pop("source", None) + if positional_source != argparse.SUPPRESS and positional_source is not None: + raw["model.source"] = positional_source + + multiparticles = raw.pop("_multiparticles", None) + if multiparticles is not None: + raw["process.multiparticles"] = dict(multiparticles) + coupling_orders = raw.pop("_max_coupling_orders", None) + if coupling_orders is not None: + raw["process.max_coupling_orders"] = dict(coupling_orders) + fft_basis = raw.pop("_fft_basis", None) + if fft_basis is not None: + raw["color.contraction"] = ColorContraction.SYMMETRIC_GROUP_FFT + raw["color.fft_basis"] = fft_basis + return CliInvocation( + action=action, + card=card, + dedicated=raw, + overrides=overrides, + dry_run=dry_run, + full_physics=full_physics, + correlators=correlators, + ) + + +def _normalize_direct_arguments( + arguments: Sequence[str], + command_index: int, +) -> list[str]: + command = arguments[command_index] + before = list(arguments[:command_index]) + after = list(arguments[command_index + 1 :]) + if command != "model": + return [command, *before, *after] + + model_index = next( + (index for index, argument in enumerate(after) if argument in _MODEL_ACTIONS), + None, + ) + if model_index is None: + return [command, *before, *after] + return [ + command, + after[model_index], + *before, + *after[:model_index], + *after[model_index + 1 :], + ] + + +def parse_cli( + argv: Sequence[str] | None = None, +) -> CliInvocation | LicenseRequestInvocation | UtilityInvocation: + arguments = list(sys.argv[1:] if argv is None else argv) + if arguments and arguments[0] in _UTILITY_COMMANDS: + return parse_utility(arguments) + if any(argument in _REMOVED_FLAT_MODEL_COMMANDS for argument in arguments): + return _namespace_to_invocation(build_parser().parse_args(arguments)) + command_index = next( + ( + index + for index, argument in enumerate(arguments) + if argument in _DIRECT_COMMANDS + ), + None, + ) + if command_index is not None: + normalized = _normalize_direct_arguments(arguments, command_index) + return _namespace_to_invocation(build_parser().parse_args(normalized)) + if arguments == ["--help"]: + return _namespace_to_invocation(build_parser().parse_args(arguments)) + return _namespace_to_invocation(build_card_parser().parse_args(arguments)) + + +__all__ = [ + "CliInvocation", + "UtilityInvocation", + "build_card_parser", + "build_parser", + "parse_cli", +] diff --git a/pyamplicol/src/pyamplicol/cli/utilities.py b/pyamplicol/src/pyamplicol/cli/utilities.py new file mode 100644 index 0000000000..3fc3a588bd --- /dev/null +++ b/pyamplicol/src/pyamplicol/cli/utilities.py @@ -0,0 +1,748 @@ +# SPDX-License-Identifier: 0BSD +"""Checkout-independent utility commands for configuration and examples.""" + +from __future__ import annotations + +import argparse +import errno +import os +import shutil +import stat +import sys +from collections.abc import Sequence +from dataclasses import dataclass +from importlib import resources +from importlib.resources.abc import Traversable +from pathlib import Path +from typing import Literal + +from pyamplicol.api.errors import ConfigurationError +from pyamplicol.config import resolution_to_dict, resolve_config +from pyamplicol.diagnostics import run_doctor, run_self_test + +UtilityKind = Literal[ + "config-template", + "config-resolve", + "examples-list", + "examples-copy", + "examples-run", + "profiling-campaign-copy", + "doctor", + "self-test", +] + +_PROFILING_CAMPAIGN_REQUIRED_FILES = ( + Path("README.md"), + Path("TABLE_FILLING.md"), + Path("build_pdf.py"), + Path("pyAmpliCol.tex"), + Path("report-workspace.json"), + Path("report_environment.json"), + Path("report_environment.tex"), + Path("result_tables.py"), + Path("result_validation_summary.tex"), + Path("steer_performance_campaign.py"), +) +_PROFILING_CAMPAIGN_GLOBS = ( + "result_*_table.tex", + "section_*.tex", + "results/*.json", +) +_PROFILING_CAMPAIGN_FILE_COUNT = 69 +_PROFILING_CAMPAIGN_LOCAL_AMPLICOL = ".pyamplicol-original-amplicol" +_PROFILING_CAMPAIGN_LOCAL_MADGRAPH = ".pyamplicol-madgraph" +_PROFILING_CAMPAIGN_STATE = "campaign_artifacts" +_PROFILING_CAMPAIGN_SUMMARY = "campaign_summary_ids" +_PROFILING_CAMPAIGN_PDF = "pyAmpliCol.pdf" +_PROFILING_CAMPAIGN_GENERATED_FILES = ( + "measurement_lineage.json", + "pyAmpliCol.aux", + "pyAmpliCol.bbl", + "pyAmpliCol.bcf", + "pyAmpliCol.blg", + "pyAmpliCol.fdb_latexmk", + "pyAmpliCol.fls", + "pyAmpliCol.log", + "pyAmpliCol.out", + "pyAmpliCol.run.xml", + "pyAmpliCol.synctex.gz", + "pyAmpliCol.toc", +) + + +@dataclass(frozen=True, slots=True) +class UtilityInvocation: + kind: UtilityKind + output_format: Literal["human", "json"] = "human" + output_color: Literal["auto", "always", "never"] = "auto" + path: Path | None = None + name: str | None = None + force: bool = False + overrides: tuple[str, ...] = () + local_amplicol: Path | None = None + local_madgraph: Path | None = None + + +@dataclass(frozen=True, slots=True) +class ExampleEntry: + name: str + action: str + description: str + + +@dataclass(frozen=True, slots=True) +class UtilityResult: + operation: str + status: Literal["complete"] + destination: Path + + +def _add_output_options(parser: argparse.ArgumentParser) -> None: + parser.add_argument( + "--json", + dest="output_format", + action="store_const", + const="json", + default="human", + help="emit stable machine-readable JSON instead of a human table", + ) + parser.add_argument( + "--color", + dest="output_color", + choices=("auto", "always", "never"), + default="auto", + help="detect a terminal, force ANSI color, or disable color", + ) + + +def _utility_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(prog="pyamplicol") + commands = parser.add_subparsers(dest="utility", required=True) + + config = commands.add_parser("config") + config_commands = config.add_subparsers(dest="config_command", required=True) + template = config_commands.add_parser("template") + template.add_argument("output", type=Path, nargs="?") + template.add_argument("--force", action="store_true") + _add_output_options(template) + resolve = config_commands.add_parser("resolve") + resolve.add_argument("card", type=Path) + resolve.add_argument("--set", dest="overrides", action="append", default=[]) + _add_output_options(resolve) + + examples = commands.add_parser("examples") + example_commands = examples.add_subparsers(dest="examples_command", required=True) + listing = example_commands.add_parser("list") + _add_output_options(listing) + copy = example_commands.add_parser("copy") + copy.add_argument("destination", type=Path) + copy.add_argument("--force", action="store_true") + _add_output_options(copy) + runnable_names = _runnable_example_names() + run = example_commands.add_parser( + "run", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog="available names:\n " + "\n ".join(runnable_names), + ) + run.add_argument( + "name", + choices=runnable_names, + metavar="NAME", + help="packaged example to run", + ) + run.add_argument("--set", dest="overrides", action="append", default=[]) + _add_output_options(run) + + profiling_campaign = commands.add_parser("profiling-campaign") + profiling_commands = profiling_campaign.add_subparsers( + dest="profiling_campaign_command", required=True + ) + campaign_copy = profiling_commands.add_parser( + "copy", + description=( + "Create a self-contained profiling campaign. Runtime state is stored " + "in the visible DEST/campaign_artifacts directory and moves with DEST." + ), + ) + campaign_copy.add_argument("destination", type=Path) + campaign_copy.add_argument( + "--force", + action="store_true", + help=( + "overwrite the managed template files and reset only " + "DEST/campaign_artifacts, DEST/pyAmpliCol.pdf, and " + "DEST/campaign_summary_ids, plus known lineage/LaTeX byproducts; " + "unrelated files and recorded local AmpliCol/MadGraph installations " + "are preserved (stop an active campaign first)" + ), + ) + campaign_copy.add_argument( + "--local-amplicol", + type=Path, + metavar="PATH_TO_COMPLETE_CHECKOUT", + help=( + "Record a clean, complete original-AmpliCol checkout as the " + "default for this copied campaign." + ), + ) + campaign_copy.add_argument( + "--local-madgraph", + type=Path, + metavar="PATH_TO_INSTALLATION", + help=( + "Record a complete MadGraph installation as the default for this " + "copied campaign." + ), + ) + _add_output_options(campaign_copy) + + for name in ("doctor", "self-test"): + command = commands.add_parser(name) + _add_output_options(command) + return parser + + +def parse_utility(argv: Sequence[str]) -> UtilityInvocation: + namespace = _utility_parser().parse_args(argv) + if namespace.utility == "config": + if namespace.config_command == "template": + return UtilityInvocation( + "config-template", + output_format=namespace.output_format, + output_color=namespace.output_color, + path=namespace.output, + force=bool(namespace.force), + ) + return UtilityInvocation( + "config-resolve", + output_format=namespace.output_format, + output_color=namespace.output_color, + path=namespace.card, + overrides=tuple(namespace.overrides), + ) + if namespace.utility == "examples": + if namespace.examples_command == "list": + return UtilityInvocation( + "examples-list", + output_format=namespace.output_format, + output_color=namespace.output_color, + ) + if namespace.examples_command == "copy": + return UtilityInvocation( + "examples-copy", + output_format=namespace.output_format, + output_color=namespace.output_color, + path=namespace.destination, + force=bool(namespace.force), + ) + return UtilityInvocation( + "examples-run", + output_format=namespace.output_format, + output_color=namespace.output_color, + name=namespace.name, + overrides=tuple(namespace.overrides), + ) + if namespace.utility == "profiling-campaign": + return UtilityInvocation( + "profiling-campaign-copy", + output_format=namespace.output_format, + output_color=namespace.output_color, + path=namespace.destination, + force=bool(namespace.force), + local_amplicol=namespace.local_amplicol, + local_madgraph=namespace.local_madgraph, + ) + return UtilityInvocation( + namespace.utility, + output_format=namespace.output_format, + output_color=namespace.output_color, + ) + + +def _source_examples_root() -> Path: + for parent in Path(__file__).resolve().parents: + candidate = parent / "examples" + if (candidate / "all_options.toml").is_file(): + return candidate + raise ConfigurationError("packaged examples are unavailable") + + +def examples_root() -> Path: + packaged = resources.files("pyamplicol").joinpath("_examples") + if not isinstance(packaged, os.PathLike): + return _source_examples_root() + path = Path(os.fspath(packaged)) + if (path / "all_options.toml").is_file(): + return path.resolve() + return _source_examples_root() + + +def _runnable_example_names() -> tuple[str, ...]: + return tuple( + card.stem + for card in sorted(examples_root().glob("*.toml")) + if card.name != "all_options.toml" + ) + + +def _profiling_campaign_inventory(source: Path) -> tuple[Path, ...]: + selected = set(_PROFILING_CAMPAIGN_REQUIRED_FILES) + for pattern in _PROFILING_CAMPAIGN_GLOBS: + selected.update(path.relative_to(source) for path in source.glob(pattern)) + missing = tuple( + relative + for relative in _PROFILING_CAMPAIGN_REQUIRED_FILES + if not (source / relative).is_file() + ) + if missing or len(selected) != _PROFILING_CAMPAIGN_FILE_COUNT: + detail = ", ".join(path.as_posix() for path in missing) + raise ConfigurationError( + "packaged profiling campaign has an invalid inventory" + + (f": missing {detail}" if detail else "") + ) + for relative in selected: + path = source / relative + if path.is_symlink() or not path.is_file(): + raise ConfigurationError( + f"packaged profiling campaign has an unsafe member: {relative}" + ) + return tuple(sorted(selected, key=lambda path: path.as_posix())) + + +def profiling_campaign_root() -> Path: + packaged = resources.files("pyamplicol").joinpath("_profiling_campaign") + if not isinstance(packaged, os.PathLike): + raise ConfigurationError("packaged profiling campaign is unavailable") + path = Path(os.fspath(packaged)) + _profiling_campaign_inventory(path) + return path.resolve() + + +def _copy_tree(source: Path, destination: Path, *, force: bool) -> Path: + target = destination.expanduser().resolve(strict=False) + if target.exists(): + if not target.is_dir(): + raise ConfigurationError(f"destination is not a directory: {target}") + if any(target.iterdir()) and not force: + raise ConfigurationError( + f"destination is not empty: {target}; pass --force to merge" + ) + target.mkdir(parents=True, exist_ok=True) + shutil.copytree(source, target, dirs_exist_ok=True) + return target + + +def _resolved_original_amplicol_checkout(path: Path) -> Path: + try: + repository = path.expanduser().resolve(strict=True) + except OSError as error: + raise ConfigurationError( + f"--local-amplicol checkout is unavailable: {path}" + ) from error + if not repository.is_dir(): + raise ConfigurationError(f"--local-amplicol is not a directory: {repository}") + return repository + + +def _resolved_madgraph_installation(path: Path) -> Path: + try: + installation = path.expanduser().resolve(strict=True) + except OSError as error: + raise ConfigurationError( + f"--local-madgraph installation is unavailable: {path}" + ) from error + if not installation.is_dir(): + raise ConfigurationError( + f"--local-madgraph is not a directory: {installation}" + ) + launcher = installation / "bin" / "mg5_aMC" + try: + launcher_mode = launcher.stat(follow_symlinks=False).st_mode + except OSError as error: + raise ConfigurationError( + "--local-madgraph must contain a regular executable bin/mg5_aMC" + ) from error + if not stat.S_ISREG(launcher_mode) or not os.access(launcher, os.X_OK): + raise ConfigurationError( + "--local-madgraph must contain a regular executable bin/mg5_aMC" + ) + return installation + + +def _literal_campaign_destination(destination: Path) -> Path: + """Return an absolute destination without accepting symlink traversal.""" + + target = Path(os.path.abspath(os.fspath(destination.expanduser()))) + try: + canonical = target.resolve(strict=False) + except OSError as error: + raise ConfigurationError( + f"profiling campaign destination is unavailable: {target}" + ) from error + if canonical != target: + raise ConfigurationError( + "profiling campaign destination must not traverse a symlink: " + f"{target}" + ) + if os.path.lexists(target): + try: + mode = target.lstat().st_mode + except OSError as error: + raise ConfigurationError( + f"cannot inspect profiling campaign destination: {target}" + ) from error + if stat.S_ISLNK(mode) or not stat.S_ISDIR(mode): + raise ConfigurationError(f"destination is not a real directory: {target}") + return target + + +def _acquire_campaign_reset_lock(target: Path) -> int: + """Exclusively claim one real campaign directory without creating state.""" + + import fcntl + + flags = os.O_RDONLY | getattr(os, "O_DIRECTORY", 0) + try: + descriptor = os.open(target, flags) + except OSError as error: + raise ConfigurationError( + f"cannot open profiling campaign destination: {target}" + ) from error + try: + fcntl.flock(descriptor, fcntl.LOCK_EX | fcntl.LOCK_NB) + except OSError as error: + os.close(descriptor) + if error.errno in {errno.EACCES, errno.EAGAIN}: + raise ConfigurationError( + "profiling campaign is active; stop it before using --force: " + f"{target}" + ) from None + raise ConfigurationError( + f"cannot lock profiling campaign destination: {target}" + ) from error + return descriptor + + +def _release_campaign_reset_lock(descriptor: int) -> None: + import fcntl + + try: + fcntl.flock(descriptor, fcntl.LOCK_UN) + finally: + os.close(descriptor) + + +def _require_campaign_member(target: Path, relative: Path) -> Path: + """Prove that a literal managed member remains beneath ``target``.""" + + if relative.is_absolute() or not relative.parts or any( + part in {"", ".", ".."} for part in relative.parts + ): + raise ConfigurationError(f"invalid profiling campaign member: {relative}") + member = target.joinpath(*relative.parts) + try: + member.relative_to(target) + canonical_target = target.resolve(strict=False) + member.resolve(strict=False).relative_to(canonical_target) + except (OSError, ValueError) as error: + raise ConfigurationError( + f"profiling campaign member escapes its destination: {relative}" + ) from error + return member + + +def _preflight_managed_file(target: Path, relative: Path) -> Path: + """Reject links or non-files along one managed template output path.""" + + output = _require_campaign_member(target, relative) + cursor = target + for part in relative.parts[:-1]: + cursor /= part + if not os.path.lexists(cursor): + continue + mode = cursor.lstat().st_mode + if stat.S_ISLNK(mode) or not stat.S_ISDIR(mode): + raise ConfigurationError( + f"unsafe profiling campaign output ancestor: {cursor}" + ) + if os.path.lexists(output): + mode = output.lstat().st_mode + if stat.S_ISLNK(mode) or not stat.S_ISREG(mode): + raise ConfigurationError(f"unsafe profiling campaign output: {output}") + return output + + +def _preflight_removable_tree(path: Path) -> None: + """Validate one exact campaign-managed directory before its removal.""" + + if not os.path.lexists(path): + return + mode = path.lstat().st_mode + if stat.S_ISLNK(mode) or not stat.S_ISDIR(mode) or os.path.ismount(path): + raise ConfigurationError(f"unsafe campaign reset directory: {path}") + pending = [path] + while pending: + directory = pending.pop() + try: + entries = tuple(os.scandir(directory)) + except OSError as error: + raise ConfigurationError( + f"cannot inspect campaign reset directory: {directory}" + ) from error + for entry in entries: + child = Path(entry.path) + try: + child_mode = entry.stat(follow_symlinks=False).st_mode + except OSError as error: + raise ConfigurationError( + f"cannot inspect campaign reset member: {child}" + ) from error + if stat.S_ISLNK(child_mode): + raise ConfigurationError( + f"campaign reset directory contains a symlink: {child}" + ) + if stat.S_ISDIR(child_mode): + if os.path.ismount(child): + raise ConfigurationError( + f"campaign reset directory contains a mount: {child}" + ) + pending.append(child) + elif not stat.S_ISREG(child_mode): + raise ConfigurationError( + f"campaign reset directory contains a special file: {child}" + ) + + +def _preflight_removable_file(path: Path) -> None: + if not os.path.lexists(path): + return + mode = path.lstat().st_mode + if stat.S_ISLNK(mode) or not stat.S_ISREG(mode): + raise ConfigurationError(f"unsafe campaign reset file: {path}") + + +def _reset_profiling_campaign_outputs(target: Path) -> None: + """Remove only the exact campaign-managed runtime publications.""" + + state = _require_campaign_member(target, Path(_PROFILING_CAMPAIGN_STATE)) + summary = _require_campaign_member(target, Path(_PROFILING_CAMPAIGN_SUMMARY)) + pdf = _require_campaign_member(target, Path(_PROFILING_CAMPAIGN_PDF)) + generated = tuple( + _require_campaign_member(target, Path(name)) + for name in _PROFILING_CAMPAIGN_GENERATED_FILES + ) + _preflight_removable_tree(state) + _preflight_removable_tree(summary) + _preflight_removable_file(pdf) + for path in generated: + _preflight_removable_file(path) + if state.exists(): + shutil.rmtree(state) + if summary.exists(): + shutil.rmtree(summary) + if pdf.exists(): + pdf.unlink() + for path in generated: + if path.exists(): + path.unlink() + + +def _copy_profiling_campaign( + destination: Path, + *, + force: bool, + local_amplicol: Path | None = None, + local_madgraph: Path | None = None, +) -> Path: + validated_amplicol = ( + None + if local_amplicol is None + else _resolved_original_amplicol_checkout(local_amplicol) + ) + validated_madgraph = ( + None + if local_madgraph is None + else _resolved_madgraph_installation(local_madgraph) + ) + source = profiling_campaign_root() + target = _literal_campaign_destination(destination) + if target.exists() and any(target.iterdir()) and not force: + raise ConfigurationError( + f"destination is not empty: {target}; pass --force to reset" + ) + target.mkdir(parents=True, exist_ok=True) + if target.is_symlink() or not target.is_dir(): + raise ConfigurationError(f"destination is not a real directory: {target}") + reset_lock = _acquire_campaign_reset_lock(target) + try: + inventory = _profiling_campaign_inventory(source) + for relative in inventory: + _preflight_managed_file(target, relative) + if validated_amplicol is not None: + _preflight_managed_file( + target, + Path(_PROFILING_CAMPAIGN_LOCAL_AMPLICOL), + ) + if validated_madgraph is not None: + _preflight_managed_file( + target, + Path(_PROFILING_CAMPAIGN_LOCAL_MADGRAPH), + ) + if force: + _reset_profiling_campaign_outputs(target) + for relative in inventory: + output = target / relative + output.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(source / relative, output) + launcher = target / "steer_performance_campaign.py" + launcher_lines = launcher.read_bytes().splitlines(keepends=True) + if not launcher_lines or not launcher_lines[0].startswith(b"#!"): + raise ConfigurationError("profiling campaign launcher has no shebang") + launcher.write_bytes( + f"#!{sys.executable}\n".encode() + b"".join(launcher_lines[1:]) + ) + launcher.chmod( + launcher.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH + ) + local_amplicol_path = target / _PROFILING_CAMPAIGN_LOCAL_AMPLICOL + if validated_amplicol is not None: + local_amplicol_path.write_text( + f"{validated_amplicol}\n", + encoding="utf-8", + ) + local_madgraph_path = target / _PROFILING_CAMPAIGN_LOCAL_MADGRAPH + if validated_madgraph is not None: + local_madgraph_path.write_text( + f"{validated_madgraph}\n", + encoding="utf-8", + ) + state_root = target / _PROFILING_CAMPAIGN_STATE + state_root.mkdir() + finally: + _release_campaign_reset_lock(reset_lock) + return target + + +def _copy_resource_tree(source: Traversable, destination: Path) -> None: + destination.mkdir(parents=True, exist_ok=True) + for child in source.iterdir(): + if child.name == "__pycache__": + continue + target = destination / child.name + if child.is_dir(): + _copy_resource_tree(child, target) + elif child.is_file(): + target.write_bytes(child.read_bytes()) + + +def _copy_packaged_models(destination: Path) -> None: + source = resources.files("pyamplicol.assets").joinpath("models") + if not source.is_dir(): + raise ConfigurationError("packaged model assets are unavailable") + _copy_resource_tree(source, destination) + + +def _card_description(path: Path) -> str: + descriptions: list[str] = [] + for line in path.read_text(encoding="utf-8").splitlines(): + stripped = line.strip() + description = stripped.lstrip("# ") if stripped.startswith("#") else "" + if description.startswith("SPDX-License-Identifier:"): + continue + if description: + descriptions.append(description) + continue + if descriptions: + break + if stripped and not stripped.startswith("#"): + break + return " ".join(descriptions) or "Packaged run-card example" + + +def list_examples() -> tuple[ExampleEntry, ...]: + entries: list[ExampleEntry] = [] + for card in sorted(examples_root().glob("*.toml")): + if card.name == "all_options.toml": + action = "reference" + else: + action = str(resolve_config(card).effective.action) + entries.append(ExampleEntry(card.stem, action, _card_description(card))) + return tuple(entries) + + +def example_card(name: str) -> Path: + if not name or Path(name).name != name: + raise ConfigurationError("example name must not contain a path") + source = examples_root() + card = source / f"{name.removesuffix('.toml')}.toml" + if not card.is_file() or card.name == "all_options.toml": + available = ", ".join(entry.name for entry in list_examples()) + raise ConfigurationError(f"unknown example {name!r}; available: {available}") + override = os.environ.get("PYAMPLICOL_EXAMPLE_CACHE") + if override: + workspace = Path(override).expanduser().resolve(strict=False) + else: + from platformdirs import user_cache_path + + from pyamplicol import __version__ + + workspace = user_cache_path("pyamplicol") / "examples" / __version__ + workspace.mkdir(parents=True, exist_ok=True) + shutil.copytree(source, workspace, dirs_exist_ok=True) + _copy_packaged_models(workspace / "models") + return workspace / card.name + + +def execute_utility(invocation: UtilityInvocation) -> object: + if invocation.kind == "config-template": + destination = ( + Path("pyamplicol.toml") if invocation.path is None else invocation.path + ) + source = examples_root() / "all_options.toml" + target = destination.expanduser().resolve(strict=False) + if target.exists() and not invocation.force: + raise ConfigurationError(f"configuration file exists: {target}") + target.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(source, target) + return UtilityResult("configuration template", "complete", target) + if invocation.kind == "config-resolve": + assert invocation.path is not None + return resolution_to_dict( + resolve_config(invocation.path, overrides=invocation.overrides) + ) + if invocation.kind == "examples-list": + return list_examples() + if invocation.kind == "examples-copy": + assert invocation.path is not None + destination = _copy_tree( + examples_root(), invocation.path, force=invocation.force + ) + _copy_packaged_models(destination / "models") + return UtilityResult("examples copy", "complete", destination) + if invocation.kind == "profiling-campaign-copy": + assert invocation.path is not None + destination = _copy_profiling_campaign( + invocation.path, + force=invocation.force, + local_amplicol=invocation.local_amplicol, + local_madgraph=invocation.local_madgraph, + ) + return UtilityResult("profiling campaign copy", "complete", destination) + if invocation.kind == "doctor": + return run_doctor() + if invocation.kind == "self-test": + return run_self_test() + raise ConfigurationError(f"utility {invocation.kind!r} must be dispatched by CLI") + + +__all__ = [ + "ExampleEntry", + "UtilityInvocation", + "UtilityResult", + "example_card", + "examples_root", + "execute_utility", + "list_examples", + "parse_utility", + "profiling_campaign_root", +] diff --git a/pyamplicol/src/pyamplicol/color/__init__.py b/pyamplicol/src/pyamplicol/color/__init__.py new file mode 100644 index 0000000000..9dc41adc19 --- /dev/null +++ b/pyamplicol/src/pyamplicol/color/__init__.py @@ -0,0 +1,66 @@ +# SPDX-License-Identifier: 0BSD +from .contraction import ( + ColorContractionEntry, + ColorContractionPlan, + ColorContractionTemplateEntry, + ColorGroupDescriptor, + FactorizedColorContractionBlock, + RepeatedColorContractionBlock, + SymmetricGroupColorContractionBlock, + build_color_contraction_plan, + color_contraction_factor, + color_contraction_factors, + exact_color_contraction_factor, + exact_color_contraction_factors, +) +from .plan import ( + ColorTopologyReplayCertificate, + GenericColorPlan, + LCColorSector, + LCColorSectorReplayPartition, + LCColorSectorTopologyGroup, + LCColorTopologyReplayPlan, + LCOpenColorLine, + build_color_plan, + build_color_topology_replay_certificate, + build_lc_topology_replay_plan, + color_topology_replay_partitions, +) +from .symmetric_group import ( + CertifiedSymmetricGroupOrbit, + SymmetricGroupOrbitPartition, + build_symmetric_group_color_contraction_plan, + certify_symmetric_group_orbits, + reconstruct_symmetric_group_dense_exact, +) + +__all__ = [ + "CertifiedSymmetricGroupOrbit", + "ColorContractionEntry", + "ColorContractionPlan", + "ColorContractionTemplateEntry", + "ColorGroupDescriptor", + "ColorTopologyReplayCertificate", + "FactorizedColorContractionBlock", + "GenericColorPlan", + "LCColorSector", + "LCColorSectorReplayPartition", + "LCColorSectorTopologyGroup", + "LCColorTopologyReplayPlan", + "LCOpenColorLine", + "RepeatedColorContractionBlock", + "SymmetricGroupColorContractionBlock", + "SymmetricGroupOrbitPartition", + "build_color_contraction_plan", + "build_color_plan", + "build_color_topology_replay_certificate", + "build_lc_topology_replay_plan", + "build_symmetric_group_color_contraction_plan", + "certify_symmetric_group_orbits", + "color_contraction_factor", + "color_contraction_factors", + "color_topology_replay_partitions", + "exact_color_contraction_factor", + "exact_color_contraction_factors", + "reconstruct_symmetric_group_dense_exact", +] diff --git a/pyamplicol/src/pyamplicol/color/adjoint_kernel.py b/pyamplicol/src/pyamplicol/color/adjoint_kernel.py new file mode 100644 index 0000000000..183aee9fca --- /dev/null +++ b/pyamplicol/src/pyamplicol/color/adjoint_kernel.py @@ -0,0 +1,167 @@ +# SPDX-License-Identifier: 0BSD +"""Exact DDM Gram kernels without constructing the quadratic trace matrix. + +With Tr(Ta Tb) = delta_ab, a half-ladder is the nested-commutator trace +Tr([...[T1,T2],...,T(n-1)] Tn). Both U(1) contributions vanish under the +commutators, so the inexpensive U(Nc) trace cycle kernel is sufficient. +Applying the left/right commutator finite differences takes 2*(n-2) sweeps +over S_(n-1); the result retained on the two-anchor subgroup has (n-2)! +entries. All arithmetic is integer, including NLC's Nc-power truncation. +""" + +from __future__ import annotations + +import math +from functools import lru_cache +from itertools import chain, permutations + +import numpy as np + +MAX_ADJOINT_GLUONS = 12 # Two anchors plus the supported FFT degree of ten. + + +def _permutation_ranks(rows: np.ndarray) -> np.ndarray: + """Rank a batch of image permutations in lexicographic order.""" + + degree = rows.shape[1] + ranks = np.zeros(len(rows), dtype=np.int64) + for position in range(degree - 1): + digit = rows[:, position].astype(np.int64) + for preceding in range(position): + digit -= rows[:, preceding] < rows[:, position] + ranks += digit * math.factorial(degree - position - 1) + return ranks + + +def _trace_cycle_counts(rows: np.ndarray) -> np.ndarray: + """Cycles of c^-1 h c h^-1, with h fixing the final external label.""" + + count, degree = rows.shape + total = degree + 1 + indices = np.arange(count) + commutator = np.empty((count, total), dtype=np.uint8) + for position in range(degree - 1): + commutator[indices, rows[:, position]] = ( + rows[:, position + 1].astype(np.int16) - 1 + ) % total + commutator[indices, rows[:, -1]] = total - 2 + commutator[:, -1] = (rows[:, 0].astype(np.int16) - 1) % total + visited = np.zeros(count, dtype=np.uint32) + cycles = np.zeros(count, dtype=np.uint8) + bits = np.left_shift(np.uint32(1), np.arange(total, dtype=np.uint32)) + for start in range(total): + active = (visited & bits[start]) == 0 + cycles += active + position = np.full(count, start, dtype=np.uint8) + while np.any(active): + visited |= np.where(active, bits[position], np.uint32(0)) + position = commutator[indices, position] + active &= (visited & bits[position]) == 0 + return cycles + + +@lru_cache(maxsize=4) +def adjoint_color_kernel( + total_gluons: int, + *, + accuracy: str = "full", + nc: int = 3, + full_col_acc: int = 20, +) -> np.ndarray: + """Return an immutable exact relative DDM kernel in lexicographic order. + + ``lc`` and ``nlc`` retain powers >= n and >= n-2 respectively. Since the + finite differences have Nc-independent integer coefficients, truncating + the seed commutes with the basis transformation. The LC helper exists + for factor diagnostics; adjoint generation itself requires NLC/full. + """ + + if type(total_gluons) is not int or not 2 <= total_gluons <= MAX_ADJOINT_GLUONS: + raise ValueError("adjoint colour kernel requires 2 to 12 external adjoints") + if accuracy not in {"lc", "nlc", "full"}: + raise ValueError(f"unknown adjoint colour accuracy {accuracy!r}") + if type(nc) is not int or nc < 2: + raise ValueError("adjoint colour kernel requires integer Nc >= 2") + if type(full_col_acc) is not int or full_col_acc < 0: + raise ValueError("adjoint full-colour accuracy must be nonnegative") + if total_gluons == 2: + # The degenerate ladder is Tr(Ta Tb)=delta_ab. There is no + # commutator to remove U(1), so retain SU(Nc)'s exact dimension. + leading_only = accuracy == "lc" or (accuracy == "full" and full_col_acc == 0) + kernel = np.asarray([nc * nc - (0 if leading_only else 1)], dtype=np.int64) + kernel.flags.writeable = False + return kernel + degree = total_gluons - 1 + # Each difference at most doubles the largest absolute input. Check once + # before allocating; numpy's int64 arithmetic must never silently wrap. + if nc**total_gluons * 4 ** (total_gluons - 2) > np.iinfo(np.int64).max: + raise ValueError("adjoint finite differences exceed exact int64 range") + order = math.factorial(degree) + rows = np.fromiter( + chain.from_iterable(permutations(range(degree))), + dtype=np.uint8, + count=order * degree, + ).reshape(order, degree) + cycles = _trace_cycle_counts(rows) + powers = np.asarray( + [nc**power for power in range(total_gluons + 1)], dtype=np.int64 + ) + current = powers[cycles] + minimum_power = max( + total_gluons + - (2 * full_col_acc if accuracy == "full" else 2 if accuracy == "nlc" else 0), + 0, + ) + if minimum_power: + current[cycles < minimum_power] = 0 + for generator in range(degree, 1, -1): + # s_i^-1 acts on images: 0->1->...->i-1->0. + relabel = np.arange(degree, dtype=np.uint8) + relabel[:generator] = (relabel[:generator] + 1) % generator + relative = relabel[rows] + ranks = _permutation_ranks(relative) + current = current - current[ranks] + for generator in range(degree, 1, -1): + # Right multiplication by s_i moves position i-1 to the beginning. + relative = rows.copy() + relative[:, 0] = rows[:, generator - 1] + relative[:, 1:generator] = rows[:, : generator - 1] + ranks = _permutation_ranks(relative) + current = current - current[ranks] + kernel = current[: math.factorial(degree - 1)].copy() + kernel.flags.writeable = False + return kernel + + +def adjoint_color_factor( + left: tuple[int, ...], + right: tuple[int, ...], + *, + accuracy: str, + nc: int = 3, + full_col_acc: int = 20, +) -> int: + """Evaluate one DDM overlap from its middle-label relative permutation.""" + + if ( + len(left) < 2 + or len(left) != len(right) + or left[0] != right[0] + or left[-1] != right[-1] + or len(set(left)) != len(left) + or set(left) != set(right) + ): + raise ValueError("DDM colour words must have matching fixed endpoints") + ranks_by_label = {label: index for index, label in enumerate(left[1:-1])} + relative = [ranks_by_label[label] for label in right[1:-1]] + rank = 0 + remaining = list(range(len(relative))) + for position, value in enumerate(relative): + offset = remaining.index(value) + rank += offset * math.factorial(len(relative) - position - 1) + remaining.pop(offset) + return int( + adjoint_color_kernel( + len(left), accuracy=accuracy, nc=nc, full_col_acc=full_col_acc + )[rank] + ) diff --git a/pyamplicol/src/pyamplicol/color/connection_catalogue.py b/pyamplicol/src/pyamplicol/color/connection_catalogue.py new file mode 100644 index 0000000000..b37ca6addf --- /dev/null +++ b/pyamplicol/src/pyamplicol/color/connection_catalogue.py @@ -0,0 +1,202 @@ +# SPDX-License-Identifier: 0BSD +"""Structural catalogues of ordered soft colour connections at arbitrary order. + +Every active coloured leg can emit a gluon; only an auxiliary gluon can split +into a quark pair. Born labels are positive. At order r, the r final auxiliary +partons receive every assignment of labels -1,...,-r. Retired intermediates +have separate dummy labels. Only independent chronological interleavings are +identified: shared-line ordering and creation dependencies are retained. + +The catalogue contains all directed overlaps with matching final typed legs, +including self-overlaps. It is not a minimal operator basis or a subtraction +formula. No multiplicities, flavour sums, kinematic factors or numerical +weights are attached to the canonical histories. The complete catalogue can +grow rapidly; explicit requests remain preferable when only a subset is used. +""" + +from __future__ import annotations + +from collections import defaultdict +from collections.abc import Iterator, Sequence +from itertools import permutations + +from .connections import ( + ColorConnection, + ColorEmission, + ColorLeg, + EmitGluon, + SplitGluon, +) +from .correlator_matrices import ColorCorrelator + +_HistoryKey = tuple[tuple[int, ...], ...] + + +def _parent(operation: ColorEmission) -> int: + return ( + operation.emitter_label + if isinstance(operation, EmitGluon) + else operation.parent_label + ) + + +def _children(operation: ColorEmission) -> tuple[int, ...]: + return ( + (operation.emitted_label,) + if isinstance(operation, EmitGluon) + else (operation.quark_label, operation.antiquark_label) + ) + + +def _operation_key(operation: ColorEmission) -> tuple[int, ...]: + return ( + (0, operation.emitter_label, operation.emitted_label) + if isinstance(operation, EmitGluon) + else ( + 1, + operation.parent_label, + operation.quark_label, + operation.antiquark_label, + ) + ) + + +def _canonical_order( + operations: tuple[ColorEmission, ...], +) -> tuple[_HistoryKey, tuple[ColorEmission, ...]]: + """Return a comparison key and a valid chronological representative. + + Sorting operation tokens alone would move consumers before producers. + Greedily taking the smallest available token gives the lexicographically + first topological order without enumerating independent interleavings. + """ + dependencies = tuple( + (before, after) + for before in range(len(operations)) + for after in range(before + 1, len(operations)) + if _parent(operations[before]) == _parent(operations[after]) + or _parent(operations[after]) in _children(operations[before]) + ) + remaining = set(range(len(operations))) + order = [] + while remaining: + available = ( + index + for index in remaining + if not any( + after == index and before in remaining for before, after in dependencies + ) + ) + next_index = min(available, key=lambda index: _operation_key(operations[index])) + order.append(next_index) + remaining.remove(next_index) + chronological = tuple(operations[index] for index in order) + return tuple( + _operation_key(operation) for operation in chronological + ), chronological + + +def _rename(operation: ColorEmission, labels: dict[int, int]) -> ColorEmission: + def label(original: int) -> int: + return labels.get(original, original) + + if isinstance(operation, EmitGluon): + return EmitGluon(label(operation.emitter_label), label(operation.emitted_label)) + return SplitGluon( + label(operation.parent_label), + label(operation.quark_label), + label(operation.antiquark_label), + ) + + +def _labelled_histories( + connection: ColorConnection, +) -> Iterator[tuple[_HistoryKey, tuple[ColorEmission, ...]]]: + final = tuple(leg.label for leg in connection.output_legs if leg.label < 0) + created = {child for step in connection.emissions for child in _children(step)} + retired = tuple(sorted(created.difference(final))) + final_labels = tuple(-index for index in range(1, connection.order + 1)) + retired_labels = tuple( + -connection.order - index for index in range(1, len(retired) + 1) + ) + assert len(final) == connection.order + for final_assignment in permutations(final_labels): + representatives = [] + for retired_assignment in permutations(retired_labels): + labels = dict(zip(final, final_assignment, strict=True)) + labels.update(zip(retired, retired_assignment, strict=True)) + representatives.append( + _canonical_order( + tuple(_rename(step, labels) for step in connection.emissions) + ) + ) + # Retired names are dummy indices, unlike the externally matched final + # labels. They must not create additional catalogue connections. + yield min(representatives, key=lambda candidate: candidate[0]) + + +def _extensions(connection: ColorConnection) -> Iterator[ColorConnection]: + created = tuple(child for step in connection.emissions for child in _children(step)) + fresh = min(created, default=0) - 1 + for leg in connection.output_legs: + if leg.representation == 1: + continue + yield ColorConnection( + connection.initial_legs, + (*connection.emissions, EmitGluon(leg.label, fresh)), + ) + if leg.label < 0 and leg.representation == 8: + yield ColorConnection( + connection.initial_legs, + (*connection.emissions, SplitGluon(leg.label, fresh, fresh - 1)), + ) + + +def build_color_correlator_catalogue( + legs: Sequence[ColorLeg], *, through_order: int +) -> tuple[ColorCorrelator, ...]: + """Prepare all ordered soft connections at orders 1 through ``through_order``. + + ``legs`` are all-outgoing Born colour roles with positive public labels. + Singlets are allowed but do not emit. ``through_order`` is a positive integer. + The ordinary ``born`` identity is not included; ``CorrelatorConfig`` adds it. + + IDs ``N{r}LO/c{i}/c{j}`` use zero-based indices in the deterministically + sorted connection list at each order. They are catalogue-local identifiers, + not process-independent operator names; the bra/ket operations encode the + actual connection. Every compatible directed pair is retained, without + assuming that the result is real, nonzero, positive or independent. + """ + if type(through_order) is not int or through_order < 1: + raise ValueError("catalogue through_order must be a positive integer") + initial = ColorConnection(tuple(legs)) + if any(leg.label <= 0 for leg in initial.initial_legs): + raise ValueError("catalogue Born legs must have positive labels") + histories: tuple[ColorConnection, ...] = (initial,) + result = [] + for order in range(1, through_order + 1): + histories = tuple( + child for parent in histories for child in _extensions(parent) + ) + unique = dict( + labelled + for connection in histories + for labelled in _labelled_histories(connection) + ) + ordered = tuple(unique[key] for key in sorted(unique)) + by_output: dict[tuple[ColorLeg, ...], list[int]] = defaultdict(list) + outputs = tuple( + ColorConnection(initial.initial_legs, operations).output_legs + for operations in ordered + ) + for index, output in enumerate(outputs): + by_output[output].append(index) + for left, bra in enumerate(ordered): + for right in by_output[outputs[left]]: + result.append( + ColorCorrelator(f"N{order}LO/c{left}/c{right}", bra, ordered[right]) + ) + return tuple(result) + + +__all__ = ["build_color_correlator_catalogue"] diff --git a/pyamplicol/src/pyamplicol/color/connections.py b/pyamplicol/src/pyamplicol/color/connections.py new file mode 100644 index 0000000000..303958b748 --- /dev/null +++ b/pyamplicol/src/pyamplicol/color/connections.py @@ -0,0 +1,612 @@ +# SPDX-License-Identifier: 0BSD +"""Exact, generation-independent ordered colour connections. + +Tensors are literal products of open strings and traces of generators ``tau`` +with ``tr(tau[a] tau[b]) = delta[a,b]``. Physical charges use +``t = tau / sqrt(2)``. Every emission therefore contributes one common inverse +sqrt(2), kept outside the rational tensor coefficients until the final overlap. +The existing exact trace/Fierz reducer supplies the final colour-index sum. + +All legs are outgoing. Quark emission prepends a generator, antiquark emission +appends its negative, and gluon emission replaces ``tau[a]`` by +``[tau[a], tau[b]]``. A gluon splitting into a quark pair contracts its adjoint +index against ``t[a]``. Connections are ordered, and later operations may act +on previously emitted legs. No kinematic splitting factor is included. + +There is deliberately no AmpliCol endpoint-pairing phase in a literal tensor. +A generation adapter must attach its basis convention as a tensor coefficient. +In particular, overlap matrices are not operator action matrices and must not +be multiplied to construct higher connections. +""" + +from __future__ import annotations + +from collections.abc import Mapping +from dataclasses import dataclass +from fractions import Fraction +from functools import cached_property +from typing import TypeAlias + +from .contraction_trace import _simplify_trace_terms_nc_power +from .contraction_types import NC + +COLOR_CONNECTION_CONVENTION = "su3-literal-tau-physical-charge-all-outgoing-v1" + + +def _integer(value: object, name: str) -> int: + if type(value) is not int: + raise TypeError(f"{name} must be an integer") + return value + + +def _fraction(value: int | Fraction) -> Fraction: + if not isinstance(value, (int, Fraction)) or isinstance(value, bool): + raise TypeError("exact colour coefficients must be integers or Fractions") + return Fraction(value) + + +@dataclass(frozen=True) +class ExactColorCoefficient: + """A complex number with exact rational real and imaginary parts.""" + + real: Fraction = Fraction(0) + imag: Fraction = Fraction(0) + + def __post_init__(self) -> None: + object.__setattr__(self, "real", _fraction(self.real)) + object.__setattr__(self, "imag", _fraction(self.imag)) + + def __bool__(self) -> bool: + return bool(self.real or self.imag) + + def __complex__(self) -> complex: + return complex(float(self.real), float(self.imag)) + + def __add__(self, other: ExactColorCoefficient) -> ExactColorCoefficient: + return ExactColorCoefficient(self.real + other.real, self.imag + other.imag) + + def __mul__(self, other: ExactColorCoefficient) -> ExactColorCoefficient: + return ExactColorCoefficient( + self.real * other.real - self.imag * other.imag, + self.real * other.imag + self.imag * other.real, + ) + + def conjugate(self) -> ExactColorCoefficient: + return ExactColorCoefficient(self.real, -self.imag) + + def scaled(self, factor: int | Fraction) -> ExactColorCoefficient: + factor = _fraction(factor) + return ExactColorCoefficient(self.real * factor, self.imag * factor) + + +_ONE = ExactColorCoefficient(1) + + +@dataclass(frozen=True, order=True) +class ColorLeg: + label: int + representation: int + + def __post_init__(self) -> None: + _integer(self.label, "colour leg label") + _integer(self.representation, "colour representation") + if self.representation not in (1, 3, -3, 8): + raise ValueError("colour connections support only singlets, 3, -3 and 8") + + +def all_outgoing_color_leg( + label: int, representation: int, *, incoming: bool = False +) -> ColorLeg: + """Dualize incoming fundamental representations, without changing momenta. + + ``representation`` is the physical particle's signed representation before + crossing. Adjoint and singlet representations are self-conjugate. + """ + + leg = ColorLeg(label, representation) + if type(incoming) is not bool: + raise TypeError("incoming must be a boolean") + return ( + ColorLeg(label, -representation) + if incoming and abs(representation) == 3 + else leg + ) + + +@dataclass(frozen=True, order=True) +class OpenColorString: + fundamental_label: int + adjoint_labels: tuple[int, ...] + antifundamental_label: int + + def __post_init__(self) -> None: + _integer(self.fundamental_label, "fundamental label") + _integer(self.antifundamental_label, "antifundamental label") + labels = tuple(self.adjoint_labels) + for label in labels: + _integer(label, "adjoint label") + object.__setattr__(self, "adjoint_labels", labels) + + +def _canonical_trace(trace: tuple[int, ...]) -> tuple[int, ...]: + return ( + min(trace[index:] + trace[:index] for index in range(len(trace))) + if trace + else () + ) + + +@dataclass(frozen=True, order=True) +class ColorTensor: + """An unordered product of open strings and cyclic (not reflected) traces.""" + + open_strings: tuple[OpenColorString, ...] = () + traces: tuple[tuple[int, ...], ...] = () + + def __post_init__(self) -> None: + strings = tuple(self.open_strings) + if any(not isinstance(string, OpenColorString) for string in strings): + raise TypeError("open strings must be OpenColorString records") + traces = tuple(tuple(trace) for trace in self.traces) + for trace in traces: + for label in trace: + _integer(label, "trace adjoint label") + object.__setattr__(self, "open_strings", tuple(sorted(strings))) + object.__setattr__( + self, "traces", tuple(sorted(_canonical_trace(t) for t in traces)) + ) + + +@dataclass(frozen=True) +class EmitGluon: + emitter_label: int + emitted_label: int + + def __post_init__(self) -> None: + _integer(self.emitter_label, "emitter label") + _integer(self.emitted_label, "emitted label") + + +@dataclass(frozen=True) +class SplitGluon: + parent_label: int + quark_label: int + antiquark_label: int + + def __post_init__(self) -> None: + _integer(self.parent_label, "splitting parent label") + _integer(self.quark_label, "splitting quark label") + _integer(self.antiquark_label, "splitting antiquark label") + + +ColorEmission: TypeAlias = EmitGluon | SplitGluon + + +def _canonical_legs(legs: tuple[ColorLeg, ...]) -> tuple[ColorLeg, ...]: + if any(not isinstance(leg, ColorLeg) for leg in legs): + raise TypeError("connection legs must be ColorLeg records") + if len({leg.label for leg in legs}) != len(legs): + raise ValueError("connection leg labels must be unique") + return tuple(sorted(legs)) + + +@dataclass(frozen=True) +class ColorConnection: + initial_legs: tuple[ColorLeg, ...] + emissions: tuple[ColorEmission, ...] = () + + def __post_init__(self) -> None: + object.__setattr__( + self, "initial_legs", _canonical_legs(tuple(self.initial_legs)) + ) + object.__setattr__(self, "emissions", tuple(self.emissions)) + _ = self.output_legs # Validate parents, representations and fresh labels once. + + @property + def order(self) -> int: + return len(self.emissions) + + @cached_property + def output_legs(self) -> tuple[ColorLeg, ...]: + active = {leg.label: leg.representation for leg in self.initial_legs} + seen = set(active) + for emission in self.emissions: + if isinstance(emission, EmitGluon): + if active.get(emission.emitter_label) not in (3, -3, 8): + raise ValueError("gluon emitter must be an active coloured leg") + additions = (ColorLeg(emission.emitted_label, 8),) + elif isinstance(emission, SplitGluon): + if active.get(emission.parent_label) != 8: + raise ValueError( + "quark-pair splitting parent must be an active gluon" + ) + additions = ( + ColorLeg(emission.quark_label, 3), + ColorLeg(emission.antiquark_label, -3), + ) + del active[emission.parent_label] + else: + raise TypeError("unknown colour emission record") + labels = tuple(leg.label for leg in additions) + if len(set(labels)) != len(labels) or seen.intersection(labels): + raise ValueError( + "emitted colour labels must be fresh, including retired labels" + ) + seen.update(labels) + active.update((leg.label, leg.representation) for leg in additions) + return tuple(ColorLeg(label, rep) for label, rep in sorted(active.items())) + + +@dataclass(frozen=True) +class TensorTerm: + coefficient: ExactColorCoefficient + tensor: ColorTensor + nc_power: int = 0 + + def __post_init__(self) -> None: + if not isinstance(self.coefficient, ExactColorCoefficient): + raise TypeError("tensor coefficients must be ExactColorCoefficient records") + if not isinstance(self.tensor, ColorTensor): + raise TypeError("tensor terms must contain ColorTensor records") + _integer(self.nc_power, "tensor Nc power") + + +def _validate_tensor_legs(tensor: ColorTensor, legs: tuple[ColorLeg, ...]) -> None: + labels = [] + for string in tensor.open_strings: + labels.extend( + ( + ColorLeg(string.fundamental_label, 3), + ColorLeg(string.antifundamental_label, -3), + ) + ) + labels.extend(ColorLeg(label, 8) for label in string.adjoint_labels) + labels.extend(ColorLeg(label, 8) for trace in tensor.traces for label in trace) + if tuple(sorted(labels)) != tuple(leg for leg in legs if leg.representation != 1): + raise ValueError( + "tensor indices do not match the typed external colour legs exactly once" + ) + + +def _combine_terms(terms: tuple[TensorTerm, ...]) -> tuple[TensorTerm, ...]: + combined: dict[tuple[ColorTensor, int], ExactColorCoefficient] = {} + for term in terms: + if not isinstance(term, TensorTerm): + raise TypeError("connected tensors require TensorTerm records") + if any(len(trace) == 1 for trace in term.tensor.traces): + continue + empty_traces = sum(not trace for trace in term.tensor.traces) + tensor = ColorTensor( + term.tensor.open_strings, tuple(t for t in term.tensor.traces if t) + ) + key = (tensor, term.nc_power + empty_traces) + combined[key] = combined.get(key, ExactColorCoefficient()) + term.coefficient + return tuple( + TensorTerm(value, tensor, power) + for (tensor, power), value in sorted(combined.items()) + if value + ) + + +@dataclass(frozen=True) +class ConnectedColorTensor: + legs: tuple[ColorLeg, ...] + terms: tuple[TensorTerm, ...] + normalization_power: int = 0 + + def __post_init__(self) -> None: + legs = _canonical_legs(tuple(self.legs)) + terms = tuple(self.terms) + power = _integer( + self.normalization_power, "inverse sqrt(2) normalization power" + ) + if power < 0: + raise ValueError("normalization power must be nonnegative") + for term in terms: + if not isinstance(term, TensorTerm): + raise TypeError("connected tensors require TensorTerm records") + _validate_tensor_legs(term.tensor, legs) + object.__setattr__(self, "legs", legs) + object.__setattr__(self, "terms", _combine_terms(terms)) + + +def _replace_string( + tensor: ColorTensor, index: int, replacements: tuple[OpenColorString, ...] +) -> ColorTensor: + return ColorTensor( + tensor.open_strings[:index] + replacements + tensor.open_strings[index + 1 :], + tensor.traces, + ) + + +def _emit( + tensor: ColorTensor, emission: EmitGluon +) -> tuple[tuple[Fraction, ColorTensor], ...]: + emitter, emitted = emission.emitter_label, emission.emitted_label + for index, string in enumerate(tensor.open_strings): + q, word, qbar = ( + string.fundamental_label, + string.adjoint_labels, + string.antifundamental_label, + ) + if emitter == q: + return ( + ( + Fraction(1), + _replace_string( + tensor, index, (OpenColorString(q, (emitted, *word), qbar),) + ), + ), + ) + if emitter == qbar: + return ( + ( + Fraction(-1), + _replace_string( + tensor, index, (OpenColorString(q, (*word, emitted), qbar),) + ), + ), + ) + if emitter in word: + position = word.index(emitter) + before, after = word[:position], word[position + 1 :] + return tuple( + ( + Fraction(sign), + _replace_string( + tensor, + index, + (OpenColorString(q, before + middle + after, qbar),), + ), + ) + for sign, middle in ((1, (emitter, emitted)), (-1, (emitted, emitter))) + ) + for index, trace in enumerate(tensor.traces): + if emitter in trace: + position = trace.index(emitter) + before, after = trace[:position], trace[position + 1 :] + return tuple( + ( + Fraction(sign), + ColorTensor( + tensor.open_strings, + ( + *tensor.traces[:index], + before + middle + after, + *tensor.traces[index + 1 :], + ), + ), + ) + for sign, middle in ((1, (emitter, emitted)), (-1, (emitted, emitter))) + ) + raise ValueError("emitter has no tensor index") + + +def _split( + tensor: ColorTensor, emission: SplitGluon +) -> tuple[tuple[Fraction, int, ColorTensor], ...]: + parent, q, qbar = ( + emission.parent_label, + emission.quark_label, + emission.antiquark_label, + ) + for index, string in enumerate(tensor.open_strings): + if parent not in string.adjoint_labels: + continue + position = string.adjoint_labels.index(parent) + before, after = ( + string.adjoint_labels[:position], + string.adjoint_labels[position + 1 :], + ) + return ( + ( + Fraction(1), + 0, + _replace_string( + tensor, + index, + ( + OpenColorString(string.fundamental_label, before, qbar), + OpenColorString(q, after, string.antifundamental_label), + ), + ), + ), + ( + Fraction(-1), + -1, + _replace_string( + tensor, + index, + ( + OpenColorString( + string.fundamental_label, + before + after, + string.antifundamental_label, + ), + OpenColorString(q, (), qbar), + ), + ), + ), + ) + for index, trace in enumerate(tensor.traces): + if parent not in trace: + continue + position = trace.index(parent) + before, after = trace[:position], trace[position + 1 :] + remaining = tensor.traces[:index] + tensor.traces[index + 1 :] + return ( + ( + Fraction(1), + 0, + ColorTensor( + (*tensor.open_strings, OpenColorString(q, after + before, qbar)), + remaining, + ), + ), + ( + Fraction(-1), + -1, + ColorTensor( + (*tensor.open_strings, OpenColorString(q, (), qbar)), + (*remaining, before + after), + ), + ), + ) + raise ValueError("splitting parent has no tensor index") + + +def apply_color_connection( + tensor: ColorTensor, + connection: ColorConnection, + *, + coefficient: ExactColorCoefficient = _ONE, +) -> ConnectedColorTensor: + """Apply an ordered connection; at most two word terms arise per operation.""" + + if not isinstance(tensor, ColorTensor) or not isinstance( + connection, ColorConnection + ): + raise TypeError("apply_color_connection requires a tensor and a connection") + _validate_tensor_legs(tensor, connection.initial_legs) + terms = (TensorTerm(coefficient, tensor),) + for emission in connection.emissions: + transformed = [] + for term in terms: + changes = ( + tuple( + (factor, 0, image) for factor, image in _emit(term.tensor, emission) + ) + if isinstance(emission, EmitGluon) + else _split(term.tensor, emission) + ) + for factor, power, image in changes: + transformed.append( + TensorTerm( + term.coefficient.scaled(factor), image, term.nc_power + power + ) + ) + terms = _combine_terms(tuple(transformed)) + return ConnectedColorTensor(connection.output_legs, terms, connection.order) + + +def _literal_tensor_overlap_nc_terms( + left: ColorTensor, right: ColorTensor +) -> Mapping[int, Fraction]: + traces = list(right.traces) + [tuple(reversed(trace)) for trace in left.traces] + right_by_q = {string.fundamental_label: string for string in right.open_strings} + left_by_qbar = { + string.antifundamental_label: string for string in left.open_strings + } + visited: set[int] = set() + for start in sorted(right_by_q): + if start in visited: + continue + current = start + trace: list[int] = [] + while current not in visited: + visited.add(current) + right_string = right_by_q[current] + left_string = left_by_qbar[right_string.antifundamental_label] + trace.extend(right_string.adjoint_labels) + trace.extend(reversed(left_string.adjoint_labels)) + current = left_string.fundamental_label + traces.append(tuple(trace)) + return _simplify_trace_terms_nc_power(((Fraction(1), 0, tuple(traces)),)) + + +def contract_connected_tensor_nc_terms( + left: ConnectedColorTensor, right: ConnectedColorTensor +) -> dict[int, ExactColorCoefficient]: + """Return the exact Laurent coefficients of ``left.conjugate() * right``. + + The scalar overlap is sesquilinear, not forcibly real or positive. Matching + physical bra/ket connection orders have even combined normalization power. + Odd powers (unlike these correlators) need an irrational coefficient type + and are rejected instead of silently rounded. + """ + + if left.legs != right.legs: + raise ValueError( + "bra and ket connections must have identical typed output legs" + ) + power = left.normalization_power + right.normalization_power + if power % 2: + raise ValueError("bra/ket normalization has an odd inverse sqrt(2) power") + result: dict[int, ExactColorCoefficient] = {} + normalization = Fraction(1, 2 ** (power // 2)) + for left_term in left.terms: + for right_term in right.terms: + coefficient = ( + left_term.coefficient.conjugate() * right_term.coefficient + ).scaled(normalization) + for nc_power, factor in _literal_tensor_overlap_nc_terms( + left_term.tensor, right_term.tensor + ).items(): + exponent = nc_power + left_term.nc_power + right_term.nc_power + result[exponent] = result.get( + exponent, ExactColorCoefficient() + ) + coefficient.scaled(factor) + return {power: coefficient for power, coefficient in result.items() if coefficient} + + +def evaluate_color_nc_terms( + terms: Mapping[int, ExactColorCoefficient], +) -> ExactColorCoefficient: + """Evaluate retained colour Laurent coefficients at physical ``Nc = 3``.""" + + result = ExactColorCoefficient() + for power, coefficient in terms.items(): + result = result + coefficient.scaled(Fraction(NC) ** power) + return result + + +def contract_connected_tensors( + left: ConnectedColorTensor, right: ConnectedColorTensor +) -> ExactColorCoefficient: + """Sum the full connected colour overlap exactly at physical ``Nc = 3``.""" + + return evaluate_color_nc_terms(contract_connected_tensor_nc_terms(left, right)) + + +def color_connection_matrix_element( + left_tensor: ColorTensor, + left_connection: ColorConnection, + right_tensor: ColorTensor, + right_connection: ColorConnection, + *, + left_coefficient: ExactColorCoefficient = _ONE, + right_coefficient: ExactColorCoefficient = _ONE, +) -> ExactColorCoefficient: + """One exact connected matrix entry, before any amplitude/helicity sum.""" + + if left_connection.initial_legs != right_connection.initial_legs: + raise ValueError("bra and ket connections must start from identical typed legs") + return contract_connected_tensors( + apply_color_connection( + left_tensor, left_connection, coefficient=left_coefficient + ), + apply_color_connection( + right_tensor, right_connection, coefficient=right_coefficient + ), + ) + + +__all__ = [ + "COLOR_CONNECTION_CONVENTION", + "ColorConnection", + "ColorEmission", + "ColorLeg", + "ColorTensor", + "ConnectedColorTensor", + "EmitGluon", + "ExactColorCoefficient", + "OpenColorString", + "SplitGluon", + "TensorTerm", + "all_outgoing_color_leg", + "apply_color_connection", + "color_connection_matrix_element", + "contract_connected_tensor_nc_terms", + "contract_connected_tensors", + "evaluate_color_nc_terms", +] diff --git a/pyamplicol/src/pyamplicol/color/contraction.py b/pyamplicol/src/pyamplicol/color/contraction.py new file mode 100644 index 0000000000..d22bb9a95e --- /dev/null +++ b/pyamplicol/src/pyamplicol/color/contraction.py @@ -0,0 +1,530 @@ +# SPDX-License-Identifier: 0BSD +"""Color-contraction planning and public factor selection.""" + +from __future__ import annotations + +import math +import struct +from collections.abc import Sequence +from fractions import Fraction + +from .contraction_factors import ( + _coloured_word, + _common_helicity_weight, + _is_open_line_pair, + _multi_open_line_color_factors, + _one_open_line_color_factors, + _pure_adjoint_color_factor, + _pure_adjoint_color_factors, + _two_open_line_color_factors, + exact_color_contraction_factor, + exact_color_contraction_factors, +) +from .contraction_types import ( + ColorContractionEntry, + ColorContractionPlan, + ColorContractionTemplateEntry, + ColorGroupDescriptor, + FactorizedColorContractionBlock, + RepeatedColorContractionBlock, + SymmetricGroupColorContractionBlock, +) +from .plan import GenericColorPlan, LCColorSector + + +def build_color_contraction_plan( + color_plan: GenericColorPlan, + groups: Sequence[ColorGroupDescriptor], +) -> ColorContractionPlan | None: + """Build the final colour contraction over coherent amplitude groups. + + LC artifacts keep the historical Rusticol diagonal reduction path. NLC and + full-colour artifacts attach an explicit sparse colour matrix whose entries + include AmpliCol's colour factors, so no leading-colour scalar factor should + be applied again at runtime. + """ + + accuracy = color_plan.color_accuracy + if accuracy == "lc": + return None + if accuracy not in {"nlc", "full"}: + return ColorContractionPlan( + color_accuracy=accuracy, + supported=False, + reason=f"unknown colour accuracy {accuracy!r}", + group_count=len(groups), + entries=(), + ) + descriptors = tuple(groups) + sector_by_id = {sector.id: sector for sector in color_plan.sectors} + descriptors_by_helicity: dict[tuple[object, ...], list[ColorGroupDescriptor]] = {} + for descriptor in descriptors: + descriptors_by_helicity.setdefault(descriptor.helicity_key, []).append( + descriptor + ) + components = tuple( + tuple(component) for component in descriptors_by_helicity.values() + ) + repeated_block = _build_repeated_color_contraction_block( + color_plan, + components, + sector_by_id=sector_by_id, + accuracy=accuracy, + ) + if repeated_block is not None: + return ColorContractionPlan( + color_accuracy=accuracy, + supported=True, + reason=None, + group_count=len(groups), + entries=(), + repeated_block=repeated_block, + ) + + entries: list[ColorContractionEntry] = [] + factor_cache: dict[tuple[int, int], Fraction] = {} + for helicity_descriptors in descriptors_by_helicity.values(): + for left_offset, left in enumerate(helicity_descriptors): + left_sector = sector_by_id.get(left.sector_id) + if left_sector is None: + return ColorContractionPlan( + color_accuracy=accuracy, + supported=False, + reason=f"missing colour sector {left.sector_id}", + group_count=len(groups), + entries=(), + ) + for right in helicity_descriptors[left_offset:]: + right_sector = sector_by_id.get(right.sector_id) + if right_sector is None: + return ColorContractionPlan( + color_accuracy=accuracy, + supported=False, + reason=f"missing colour sector {right.sector_id}", + group_count=len(groups), + entries=(), + ) + sector_pair = (left.sector_id, right.sector_id) + weight = factor_cache.get(sector_pair) + if weight is None: + weight = exact_color_contraction_factor( + color_plan, + left_sector, + right_sector, + accuracy=accuracy, + full_col_acc=20, + ) + factor_cache[sector_pair] = weight + if abs(weight) <= 0.0: + continue + symmetry = 1.0 if left.group_id == right.group_id else 2.0 + helicity_weight = _common_helicity_weight(left, right) + entries.append( + ColorContractionEntry( + left_group_id=left.group_id, + right_group_id=right.group_id, + weight_re=helicity_weight * float(weight), + symmetry_factor=symmetry, + exact_weight=(Fraction(helicity_weight) * weight, Fraction(0)), + ) + ) + return ColorContractionPlan( + color_accuracy=accuracy, + supported=True, + reason=None, + group_count=len(groups), + entries=tuple(entries), + ) + + +def _build_repeated_color_contraction_block( + color_plan: GenericColorPlan, + components: Sequence[Sequence[ColorGroupDescriptor]], + *, + sector_by_id: dict[int, LCColorSector], + accuracy: str, +) -> RepeatedColorContractionBlock | None: + if len(components) < 2 or not components[0]: + return None + sorted_sector_ids = tuple( + sorted(descriptor.sector_id for descriptor in components[0]) + ) + if len(set(sorted_sector_ids)) != len(sorted_sector_ids): + return None + reference_weight_bits = _binary64_bits(components[0][0].helicity_weight) + descriptors_by_component_and_sector: list[dict[int, ColorGroupDescriptor]] = [] + for component in components: + component_sector_ids = tuple( + sorted(descriptor.sector_id for descriptor in component) + ) + if ( + component_sector_ids != sorted_sector_ids + or len(set(component_sector_ids)) != len(component_sector_ids) + or any( + _binary64_bits(descriptor.helicity_weight) != reference_weight_bits + for descriptor in component + ) + ): + return None + descriptors_by_component_and_sector.append( + {descriptor.sector_id: descriptor for descriptor in component} + ) + + component_group_ids = tuple( + descriptors_by_sector[sector_id].group_id + for sector_id in sorted_sector_ids + for descriptors_by_sector in descriptors_by_component_and_sector + ) + base_entries: list[ColorContractionTemplateEntry] = [] + helicity_weight = components[0][0].helicity_weight + for left_group_index, left_sector_id in enumerate(sorted_sector_ids): + left_sector = sector_by_id.get(left_sector_id) + if left_sector is None: + return None + for right_group_index in range(left_group_index, len(sorted_sector_ids)): + right_sector_id = sorted_sector_ids[right_group_index] + right_sector = sector_by_id.get(right_sector_id) + if right_sector is None: + return None + weight = exact_color_contraction_factor( + color_plan, + left_sector, + right_sector, + accuracy=accuracy, + full_col_acc=20, + ) + if abs(weight) <= 0.0: + continue + base_entries.append( + ColorContractionTemplateEntry( + left_group_index=left_group_index, + right_group_index=right_group_index, + weight_re=helicity_weight * float(weight), + symmetry_factor=( + 1.0 if left_group_index == right_group_index else 2.0 + ), + exact_weight=(Fraction(helicity_weight) * weight, Fraction(0)), + ) + ) + return RepeatedColorContractionBlock( + component_count=len(components), + component_group_ids=component_group_ids, + entries=tuple(base_entries), + factorized_block=_build_walsh_color_contraction_block( + tuple( + descriptors_by_component_and_sector[0][sector_id].word + for sector_id in sorted_sector_ids + ), + tuple(base_entries), + ), + ) + + +def _build_walsh_color_contraction_block( + words: Sequence[Sequence[int]], + entries: Sequence[ColorContractionTemplateEntry], +) -> FactorizedColorContractionBlock | None: + """Recognize an exact free elementary-Abelian action on one color matrix.""" + + if not words: + return None + canonical_words = tuple(tuple(word) for word in words) + canonical_word = canonical_words[0] + if len(set(canonical_word)) != len(canonical_word): + return None + if any( + len(word) != len(canonical_word) + or len(set(word)) != len(word) + or set(word) != set(canonical_word) + for word in canonical_words + ): + return None + + matrix: dict[tuple[int, int], float] = {} + for entry in entries: + if ( + entry.weight_im != 0.0 + or not math.isfinite(entry.weight_re) + or not math.isfinite(entry.symmetry_factor) + ): + return None + left, right = sorted((entry.left_group_index, entry.right_group_index)) + if (left, right) in matrix: + return None + weight = ( + entry.symmetry_factor * entry.weight_re + if left == right + else 0.5 * entry.symmetry_factor * entry.weight_re + ) + if not math.isfinite(weight): + return None + matrix[(left, right)] = weight + + def matrix_value(left: int, right: int) -> float: + return matrix.get((min(left, right), max(left, right)), 0.0) + + def matrix_is_invariant(action: tuple[int, ...]) -> bool: + return all( + matrix_value(action[left], action[right]) == weight + for (left, right), weight in matrix.items() + ) + + def action_cosets( + actions: Sequence[tuple[int, ...]], + ) -> tuple[tuple[int, ...], ...] | None: + if len(actions) < 2: + return None + expected_indices = list(range(len(canonical_words))) + for action in actions: + if sorted(action) != expected_indices or any( + action[action[index]] != index for index in expected_indices + ): + return None + for first_offset, first_action in enumerate(actions): + for second_action in actions[first_offset + 1 :]: + if any( + first_action[second_action[index]] + != second_action[first_action[index]] + for index in expected_indices + ): + return None + + group_actions: list[tuple[int, ...]] = [] + for mask in range(1 << len(actions)): + action = tuple(expected_indices) + for generator_index, generator in enumerate(actions): + if mask & (1 << generator_index): + action = tuple(generator[index] for index in action) + group_actions.append(action) + + remaining = set(expected_indices) + cosets: list[tuple[int, ...]] = [] + while remaining: + representative = min(remaining) + coset = tuple(action[representative] for action in group_actions) + if len(set(coset)) != len(group_actions) or not set(coset).issubset( + remaining + ): + return None + cosets.append(coset) + remaining.difference_update(coset) + return tuple(cosets) + + def matrix_has_walsh_blocks( + cosets: Sequence[tuple[int, ...]], + ) -> bool: + if not cosets: + return False + group_size = len(cosets[0]) + if group_size < 4 or any(len(coset) != group_size for coset in cosets): + return False + return all( + matrix_value( + left_coset[left_subgroup_index], + right_coset[right_subgroup_index], + ) + == matrix_value( + left_coset[0], + right_coset[left_subgroup_index ^ right_subgroup_index], + ) + for left_coset in cosets + for right_coset in cosets + for left_subgroup_index in range(group_size) + for right_subgroup_index in range(group_size) + ) + + def factorized_block( + actions: Sequence[tuple[int, ...]], + ) -> FactorizedColorContractionBlock | None: + # H8 is the largest physically measured transform. Preserve K4 as the + # fallback and do not emit wider transforms until realistic multi-coset + # workloads demonstrate that their locality/transform cost is useful. + for rank in range(min(len(actions), 3), 1, -1): + cosets = action_cosets(actions[:rank]) + if cosets is None or not matrix_has_walsh_blocks(cosets): + continue + if rank == 2: + return FactorizedColorContractionBlock( + kind="klein-four-walsh", + cosets=cosets, + ) + return FactorizedColorContractionBlock( + kind="elementary-abelian-walsh", + rank=rank, + cosets=cosets, + ) + return None + + # Preserve the established plan for a complete scalar permutation orbit. + positions = { + label: tuple(word.index(label) for word in canonical_words) + for label in canonical_word + } + variable_labels = tuple( + label for label in canonical_word if len(set(positions[label])) > 1 + ) + variable_count = len(variable_labels) + if variable_count >= 4 and len(canonical_words) == math.factorial(variable_count): + rank_by_label = { + label: rank for rank, label in enumerate(sorted(variable_labels)) + } + permutations = tuple( + tuple(rank_by_label[label] for label in word if label in rank_by_label) + for word in canonical_words + ) + expected_ranks = set(range(variable_count)) + index_by_permutation = { + permutation: index for index, permutation in enumerate(permutations) + } + if all( + set(permutation) == expected_ranks for permutation in permutations + ) and len(index_by_permutation) == math.factorial(variable_count): + substitutions = tuple( + tuple( + pair_start + 1 + if rank == pair_start + else pair_start + if rank == pair_start + 1 + else rank + for rank in range(variable_count) + ) + for pair_start in range(0, variable_count - 1, 2) + ) + actions = tuple( + tuple( + index_by_permutation[ + tuple(substitution[rank] for rank in permutation) + ] + for permutation in permutations + ) + for substitution in substitutions + ) + block = factorized_block(actions) + if block is not None: + return block + + # Multi-open-line bases are often unions of permutation orbits rather than + # one full symmetric-group orbit. Discover exact label-swap automorphisms + # of the actual word set and matrix; no particle or process identity is + # assumed, and any incomplete or weight-asymmetric orbit falls back. + index_by_word = {word: index for index, word in enumerate(canonical_words)} + if len(index_by_word) != len(canonical_words): + return None + invariant_swaps: list[tuple[tuple[int, int], tuple[int, ...]]] = [] + ordered_labels = sorted(canonical_word) + for left_offset, left_label in enumerate(ordered_labels): + for right_label in ordered_labels[left_offset + 1 :]: + substitution = {left_label: right_label, right_label: left_label} + try: + action = tuple( + index_by_word[ + tuple(substitution.get(label, label) for label in word) + ] + for word in canonical_words + ) + except KeyError: + continue + if matrix_is_invariant(action): + invariant_swaps.append(((left_label, right_label), action)) + + selected_actions: list[tuple[int, ...]] = [] + selected_labels: set[int] = set() + for labels, action in invariant_swaps: + if selected_labels.isdisjoint(labels): + selected_actions.append(action) + selected_labels.update(labels) + return factorized_block(selected_actions) + + +def _binary64_bits(value: float) -> bytes: + return struct.pack(">d", float(value)) + + +def color_contraction_factors( + color_plan: GenericColorPlan, + left: LCColorSector, + right: LCColorSector, + *, + full_col_acc: int = 20, +) -> tuple[float, float, float]: + """Return reference-normalized (LC, NLC, full) colour factors.""" + + if color_plan.basis == "adjoint": + return tuple( + float(value) + for value in exact_color_contraction_factors( + color_plan, left, right, full_col_acc=full_col_acc + ) + ) + open_line_count = color_plan.process.color_endpoints.pair_count + n_ord = len(_coloured_word(left)) + if len(_coloured_word(right)) != n_ord: + return (0.0, 0.0, 0.0) + if open_line_count == 0: + return _pure_adjoint_color_factors(left, right, n_ord, full_col_acc) + if open_line_count == 1: + return _one_open_line_color_factors(left, right, n_ord) + if open_line_count == 2: + return _two_open_line_color_factors(color_plan, left, right, n_ord) + if open_line_count >= 3 and _is_open_line_pair(left, right): + return _multi_open_line_color_factors(color_plan, left, right) + return (0.0, 0.0, 0.0) + + +def color_contraction_factor( + color_plan: GenericColorPlan, + left: LCColorSector, + right: LCColorSector, + *, + accuracy: str, + full_col_acc: int = 20, +) -> float: + """Return only the requested reference-normalized colour factor.""" + + if color_plan.basis == "adjoint": + return float( + exact_color_contraction_factor( + color_plan, left, right, accuracy=accuracy, full_col_acc=full_col_acc + ) + ) + open_line_count = color_plan.process.color_endpoints.pair_count + n_ord = len(_coloured_word(left)) + if len(_coloured_word(right)) != n_ord: + return 0.0 + if open_line_count == 0: + return _pure_adjoint_color_factor( + left, + right, + n_ord, + accuracy=accuracy, + full_col_acc=full_col_acc, + ) + values = color_contraction_factors( + color_plan, + left, + right, + full_col_acc=full_col_acc, + ) + if accuracy == "lc": + return values[0] + if accuracy == "nlc": + return values[1] + if accuracy == "full": + return values[2] + return 0.0 + + +__all__ = [ + "ColorContractionEntry", + "ColorContractionPlan", + "ColorContractionTemplateEntry", + "ColorGroupDescriptor", + "FactorizedColorContractionBlock", + "RepeatedColorContractionBlock", + "SymmetricGroupColorContractionBlock", + "build_color_contraction_plan", + "color_contraction_factor", + "color_contraction_factors", + "exact_color_contraction_factor", + "exact_color_contraction_factors", +] diff --git a/pyamplicol/src/pyamplicol/color/contraction_factors.py b/pyamplicol/src/pyamplicol/color/contraction_factors.py new file mode 100644 index 0000000000..80f1e70c3b --- /dev/null +++ b/pyamplicol/src/pyamplicol/color/contraction_factors.py @@ -0,0 +1,961 @@ +# SPDX-License-Identifier: 0BSD +"""Representation-based color-factor algorithms.""" + +from __future__ import annotations + +from collections.abc import Mapping +from fractions import Fraction +from functools import lru_cache + +from .contraction_trace import ( + _check_nlc, + _check_nlc_one_open_line, + _eval_nc_terms, + _eval_nc_terms_exact, + _eval_trace, + _eval_trace_exact, + _simplify_trace_terms, + _simplify_trace_terms_nc_power, +) +from .contraction_types import NC, ColorGroupDescriptor +from .plan import GenericColorPlan, LCColorSector, LCOpenColorLine + + +def _pure_adjoint_color_factors( + left: LCColorSector, + right: LCColorSector, + n_ord: int, + full_col_acc: int, +) -> tuple[float, float, float]: + return ( + _pure_adjoint_color_factor( + left, + right, + n_ord, + accuracy="lc", + full_col_acc=full_col_acc, + ), + _pure_adjoint_color_factor( + left, + right, + n_ord, + accuracy="nlc", + full_col_acc=full_col_acc, + ), + _pure_adjoint_color_factor( + left, + right, + n_ord, + accuracy="full", + full_col_acc=full_col_acc, + ), + ) + + +def _pure_adjoint_color_factors_exact( + left: LCColorSector, + right: LCColorSector, + n_ord: int, + full_col_acc: int, +) -> tuple[Fraction, Fraction, Fraction]: + return ( + _pure_adjoint_color_factor_exact( + left, + right, + n_ord, + accuracy="lc", + full_col_acc=full_col_acc, + ), + _pure_adjoint_color_factor_exact( + left, + right, + n_ord, + accuracy="nlc", + full_col_acc=full_col_acc, + ), + _pure_adjoint_color_factor_exact( + left, + right, + n_ord, + accuracy="full", + full_col_acc=full_col_acc, + ), + ) + + +def _pure_adjoint_color_factor( + left: LCColorSector, + right: LCColorSector, + n_ord: int, + *, + accuracy: str, + full_col_acc: int, +) -> float: + iper = _coloured_word(left) + jper = _coloured_word(right) + if n_ord == 0: + return 1.0 if not iper and not jper else 0.0 + if accuracy == "lc": + return float(NC**n_ord) if iper == jper else 0.0 + if accuracy == "nlc": + if n_ord == 2: + # Tr(Ta Tb)=delta_ab, whose norm Nc^2-1 is already exact at + # NLC. The n>=3 shortcut below would incorrectly count two + # independent U(1) subtractions for the same identity tensor. + return float(NC**2 - 1) + if iper == jper: + return float(NC**n_ord - n_ord * NC ** (n_ord - 2)) + return float(_check_nlc(tuple(jper), tuple(iper)) * NC ** (n_ord - 2)) + if accuracy != "full": + return 0.0 + relative = _relative_adjoint_permutation(iper, jper) + if relative is not None: + return _pure_adjoint_full_factor_by_relative_permutation( + relative, + n_ord, + full_col_acc, + ) + return _pure_adjoint_full_factor_uncached( + tuple(iper), + tuple(jper), + n_ord, + full_col_acc, + ) + + +def _pure_adjoint_color_factor_exact( + left: LCColorSector, + right: LCColorSector, + n_ord: int, + *, + accuracy: str, + full_col_acc: int, +) -> Fraction: + iper = _coloured_word(left) + jper = _coloured_word(right) + if n_ord == 0: + return Fraction(1 if not iper and not jper else 0) + nc = Fraction(NC) + if accuracy == "lc": + return nc**n_ord if iper == jper else Fraction(0) + if accuracy == "nlc": + if n_ord == 2: + return nc**2 - 1 + if iper == jper: + return nc**n_ord - n_ord * nc ** (n_ord - 2) + return Fraction(_check_nlc(tuple(jper), tuple(iper))) * nc ** (n_ord - 2) + if accuracy != "full": + return Fraction(0) + relative = _relative_adjoint_permutation(iper, jper) + if relative is not None: + return _pure_adjoint_full_factor_exact_by_relative_permutation( + relative, + n_ord, + full_col_acc, + ) + return _pure_adjoint_full_factor_exact_uncached( + tuple(iper), + tuple(jper), + n_ord, + full_col_acc, + ) + + +def _relative_adjoint_permutation( + left: tuple[int, ...], + right: tuple[int, ...], +) -> tuple[int, ...] | None: + """Canonicalize a trace pair under simultaneous adjoint relabelling.""" + + if len(left) != len(right) or len(set(left)) != len(left): + return None + position = {label: index for index, label in enumerate(left)} + try: + relative = tuple(position[label] for label in right) + except KeyError: + return None + if len(set(relative)) != len(relative): + return None + return relative + + +@lru_cache(maxsize=65536) +def _pure_adjoint_full_factor_by_relative_permutation( + relative: tuple[int, ...], + n_ord: int, + full_col_acc: int, +) -> float: + canonical = tuple(range(len(relative))) + return _pure_adjoint_full_factor_uncached( + canonical, + relative, + n_ord, + full_col_acc, + ) + + +@lru_cache(maxsize=65536) +def _pure_adjoint_full_factor_exact_by_relative_permutation( + relative: tuple[int, ...], + n_ord: int, + full_col_acc: int, +) -> Fraction: + canonical = tuple(range(len(relative))) + return _pure_adjoint_full_factor_exact_uncached( + canonical, + relative, + n_ord, + full_col_acc, + ) + + +def _pure_adjoint_full_factor_uncached( + iper: tuple[int, ...], + jper: tuple[int, ...], + n_ord: int, + full_col_acc: int, +) -> float: + full_terms = _simplify_trace_terms( + ((Fraction(1), (tuple(iper), tuple(reversed(jper)))),) + ) + return _eval_nc_terms( + full_terms, + min_power=max(n_ord - 2 * full_col_acc, 0), + ) + + +def _pure_adjoint_full_factor_exact_uncached( + iper: tuple[int, ...], + jper: tuple[int, ...], + n_ord: int, + full_col_acc: int, +) -> Fraction: + full_terms = _simplify_trace_terms( + ((Fraction(1), (tuple(iper), tuple(reversed(jper)))),) + ) + return _eval_nc_terms_exact( + full_terms, + min_power=max(n_ord - 2 * full_col_acc, 0), + ) + + +def _one_open_line_color_factors( + left: LCColorSector, + right: LCColorSector, + n_ord: int, +) -> tuple[float, float, float]: + iper = _coloured_word(left) + jper = _coloured_word(right) + lc = float(NC ** (n_ord - 1)) if iper == jper else 0.0 + full = _eval_trace( + (tuple((*iper[1:-1], *reversed(jper[1:-1]))),), + ) + if iper == jper: + nlc = full + else: + acc = _check_nlc_one_open_line(tuple(jper[1:-1]), tuple(iper[1:-1])) + nlc = full if acc != 0 else 0.0 + return (lc, nlc, full) + + +def _one_open_line_color_factors_exact( + left: LCColorSector, + right: LCColorSector, + n_ord: int, +) -> tuple[Fraction, Fraction, Fraction]: + iper = _coloured_word(left) + jper = _coloured_word(right) + lc = Fraction(NC) ** (n_ord - 1) if iper == jper else Fraction(0) + full = _eval_trace_exact( + (tuple((*iper[1:-1], *reversed(jper[1:-1]))),), + ) + if iper == jper: + nlc = full + else: + acc = _check_nlc_one_open_line(tuple(jper[1:-1]), tuple(iper[1:-1])) + nlc = full if acc != 0 else Fraction(0) + return (lc, nlc, full) + + +def _two_open_line_color_factors( + color_plan: GenericColorPlan, + left: LCColorSector, + right: LCColorSector, + n_ord: int, +) -> tuple[float, float, float]: + reference_start = _two_line_reference_start(color_plan) + iper = _rotate_to_reference_start(_coloured_word(left), reference_start) + jper = _rotate_to_reference_start(_coloured_word(right), reference_start) + reference = ( + _rotate_to_reference_start( + _coloured_word(color_plan.sectors[0]), reference_start + ) + if color_plan.sectors + else iper + ) + gi, ui = _two_line_gi_ui(color_plan, iper, reference) + gj, uj = _two_line_gi_ui(color_plan, jper, reference) + repeated_fundamental_species = _has_repeated_fundamental_species(color_plan) + lc = 0.0 + if iper == jper: + if ui == 1 and uj == 1: + lc = float(NC ** (n_ord - 2)) + elif ui == 2 and uj == 2 and not repeated_fundamental_species: + lc = float(NC ** (n_ord - 4) * 9.0) + elif ui == 2 and uj == 2 and repeated_fundamental_species: + lc = float(NC ** (n_ord - 2)) + full = _two_open_line_full_factor( + iper, + jper, + n_ord=n_ord, + gi=gi, + gj=gj, + ui=ui, + uj=uj, + ) + nlc = 0.0 + if abs(full) > 0.0: + iper_adj, jper_adj = _two_line_ordered_adjoint_strings( + color_plan, + iper, + jper, + ) + iper_ord, jper_ord = _convert_two_line_adjoint_strings( + n_ord, + iper_adj, + jper_adj, + ) + acc = _check_nlc_two_open_lines_same_species( + n_ord, + iper_ord, + jper_ord, + gi, + gj, + ui, + uj, + ) + if acc != 0: + nlc = full + return (lc, nlc, full) + + +def _two_open_line_color_factors_exact( + color_plan: GenericColorPlan, + left: LCColorSector, + right: LCColorSector, + n_ord: int, +) -> tuple[Fraction, Fraction, Fraction]: + reference_start = _two_line_reference_start(color_plan) + iper = _rotate_to_reference_start(_coloured_word(left), reference_start) + jper = _rotate_to_reference_start(_coloured_word(right), reference_start) + reference = ( + _rotate_to_reference_start( + _coloured_word(color_plan.sectors[0]), reference_start + ) + if color_plan.sectors + else iper + ) + gi, ui = _two_line_gi_ui(color_plan, iper, reference) + gj, uj = _two_line_gi_ui(color_plan, jper, reference) + repeated_fundamental_species = _has_repeated_fundamental_species(color_plan) + lc = Fraction(0) + if iper == jper: + if ui == 1 and uj == 1: + lc = Fraction(NC) ** (n_ord - 2) + elif ui == 2 and uj == 2 and not repeated_fundamental_species: + lc = Fraction(9) * Fraction(NC) ** (n_ord - 4) + elif ui == 2 and uj == 2 and repeated_fundamental_species: + lc = Fraction(NC) ** (n_ord - 2) + full = _two_open_line_full_factor_exact( + iper, + jper, + n_ord=n_ord, + gi=gi, + gj=gj, + ui=ui, + uj=uj, + ) + nlc = Fraction(0) + if full: + iper_adj, jper_adj = _two_line_ordered_adjoint_strings( + color_plan, + iper, + jper, + ) + iper_ord, jper_ord = _convert_two_line_adjoint_strings( + n_ord, + iper_adj, + jper_adj, + ) + acc = _check_nlc_two_open_lines_same_species( + n_ord, + iper_ord, + jper_ord, + gi, + gj, + ui, + uj, + ) + if acc != 0: + nlc = full + return (lc, nlc, full) + + +def _two_open_line_full_factor( + iper: tuple[int, ...], + jper: tuple[int, ...], + *, + n_ord: int, + gi: int, + gj: int, + ui: int, + uj: int, +) -> float: + if ui == uj: + traces = ( + tuple((*iper[1 : 1 + gi], *reversed(jper[1 : 1 + gj]))), + tuple((*iper[gi + 3 : n_ord - 1], *reversed(jper[gj + 3 : n_ord - 1]))), + ) + coeff = Fraction(1) + elif (ui, uj) in {(1, 2), (2, 1)}: + traces = ( + tuple( + ( + *iper[1 : 1 + gi], + *reversed(jper[gj + 3 : n_ord - 1]), + *iper[gi + 3 : n_ord - 1], + *reversed(jper[1 : 1 + gj]), + ) + ), + ) + coeff = Fraction(-1) + else: + return 0.0 + return _eval_trace(traces, coeff=coeff) + + +def _two_open_line_full_factor_exact( + iper: tuple[int, ...], + jper: tuple[int, ...], + *, + n_ord: int, + gi: int, + gj: int, + ui: int, + uj: int, +) -> Fraction: + if ui == uj: + traces = ( + tuple((*iper[1 : 1 + gi], *reversed(jper[1 : 1 + gj]))), + tuple((*iper[gi + 3 : n_ord - 1], *reversed(jper[gj + 3 : n_ord - 1]))), + ) + coeff = Fraction(1) + elif (ui, uj) in {(1, 2), (2, 1)}: + traces = ( + tuple( + ( + *iper[1 : 1 + gi], + *reversed(jper[gj + 3 : n_ord - 1]), + *iper[gi + 3 : n_ord - 1], + *reversed(jper[1 : 1 + gj]), + ) + ), + ) + coeff = Fraction(-1) + else: + return Fraction(0) + return _eval_trace_exact(traces, coeff=coeff) + + +def _is_open_line_pair( + left: LCColorSector, + right: LCColorSector, +) -> bool: + if left.kind != "open-lines" or right.kind != "open-lines": + return False + return len(left.open_color_lines) == len(right.open_color_lines) + + +def _multi_open_line_color_factors( + color_plan: GenericColorPlan, + left: LCColorSector, + right: LCColorSector, +) -> tuple[float, float, float]: + """Generic overlap of multi-open-line colour-flow tensors. + + Each sector is a product of open strings + ``(T...T)_{i,\bar j}``. The right sector is conjugated, so its adjoint + string is traversed in reverse order, and the fundamental colour indices + form closed alternating left/right cycles. The resulting traces are reduced + with the same Fierz machinery used for pure traces, but we keep powers of + ``Nc`` symbolic so that NLC can be obtained by dropping terms beyond the + first ``1/Nc**2`` suppression. + """ + + terms = _open_line_nc_power_terms( + left.open_color_lines, + right.open_color_lines, + ) + if not terms: + return (0.0, 0.0, 0.0) + leading_power = color_plan.process.color_endpoints.pair_count + len( + color_plan.process.adjoint_labels + ) + lc = _eval_nc_terms(terms, min_power=leading_power) + full = _eval_nc_terms(terms) + # NLC selects matrix entries by their leading Nc power, but retains the + # exact coefficient of every selected entry. Truncating the coefficient + # itself changes off-diagonal multi-line contractions once adjoints are + # present (for example 8 -> 9 for three open lines plus one adjoint). + nlc = full if max(terms) >= leading_power - 2 else 0.0 + return (lc, nlc, full) + + +def _multi_open_line_color_factors_exact( + color_plan: GenericColorPlan, + left: LCColorSector, + right: LCColorSector, +) -> tuple[Fraction, Fraction, Fraction]: + terms = _open_line_nc_power_terms( + left.open_color_lines, + right.open_color_lines, + ) + if not terms: + return (Fraction(0), Fraction(0), Fraction(0)) + leading_power = color_plan.process.color_endpoints.pair_count + len( + color_plan.process.adjoint_labels + ) + lc = _eval_nc_terms_exact(terms, min_power=leading_power) + full = _eval_nc_terms_exact(terms) + nlc = full if max(terms) >= leading_power - 2 else Fraction(0) + return (lc, nlc, full) + + +def exact_color_contraction_factors( + color_plan: GenericColorPlan, + left: LCColorSector, + right: LCColorSector, + *, + full_col_acc: int = 20, +) -> tuple[Fraction, Fraction, Fraction]: + """Return exact reference-normalized ``(LC, NLC, full)`` color factors.""" + + if color_plan.basis == "adjoint": + return tuple( + exact_color_contraction_factor( + color_plan, left, right, accuracy=accuracy, full_col_acc=full_col_acc + ) + for accuracy in ("lc", "nlc", "full") + ) + open_line_count = color_plan.process.color_endpoints.pair_count + n_ord = len(_coloured_word(left)) + if len(_coloured_word(right)) != n_ord: + return (Fraction(0), Fraction(0), Fraction(0)) + if open_line_count == 0: + return _pure_adjoint_color_factors_exact( + left, + right, + n_ord, + full_col_acc, + ) + if open_line_count == 1: + return _one_open_line_color_factors_exact(left, right, n_ord) + if open_line_count == 2: + return _two_open_line_color_factors_exact(color_plan, left, right, n_ord) + if open_line_count >= 3 and _is_open_line_pair(left, right): + return _multi_open_line_color_factors_exact(color_plan, left, right) + return (Fraction(0), Fraction(0), Fraction(0)) + + +def exact_color_contraction_factor( + color_plan: GenericColorPlan, + left: LCColorSector, + right: LCColorSector, + *, + accuracy: str, + full_col_acc: int = 20, +) -> Fraction: + """Return one exact reference-normalized color factor.""" + + if accuracy not in {"lc", "nlc", "full"}: + return Fraction(0) + if color_plan.basis == "adjoint": + from .adjoint_kernel import adjoint_color_factor + + return Fraction( + adjoint_color_factor( + _coloured_word(left), + _coloured_word(right), + accuracy=accuracy, + nc=NC, + full_col_acc=full_col_acc, + ) + ) + if color_plan.process.color_endpoints.pair_count == 0: + n_ord = len(_coloured_word(left)) + if len(_coloured_word(right)) != n_ord: + return Fraction(0) + # LC/NLC have direct formulae and must not evaluate the full trace. + return _pure_adjoint_color_factor_exact( + left, + right, + n_ord, + accuracy=accuracy, + full_col_acc=full_col_acc, + ) + values = exact_color_contraction_factors( + color_plan, + left, + right, + full_col_acc=full_col_acc, + ) + if accuracy == "lc": + return values[0] + if accuracy == "nlc": + return values[1] + if accuracy == "full": + return values[2] + return Fraction(0) + + +def _open_line_nc_power_terms( + left_lines: tuple[LCOpenColorLine, ...], + right_lines: tuple[LCOpenColorLine, ...], +) -> Mapping[int, Fraction]: + left_by_fundamental = {line.fundamental_label: line for line in left_lines} + right_by_antifundamental = { + line.antifundamental_label: line for line in right_lines + } + if set(left_by_fundamental) != {line.fundamental_label for line in right_lines}: + return {} + if {line.antifundamental_label for line in left_lines} != set( + right_by_antifundamental + ): + return {} + + visited: set[int] = set() + traces: list[tuple[int, ...]] = [] + for start in sorted(left_by_fundamental): + if start in visited: + continue + current = start + trace: list[int] = [] + while current not in visited: + visited.add(current) + left_line = left_by_fundamental.get(current) + if left_line is None: + return {} + trace.extend(left_line.adjoint_labels) + right_line = right_by_antifundamental.get(left_line.antifundamental_label) + if right_line is None: + return {} + trace.extend(reversed(right_line.adjoint_labels)) + current = right_line.fundamental_label + traces.append(tuple(trace)) + permutation_sign = -1 if (len(left_lines) - len(traces)) % 2 else 1 + return _simplify_trace_terms_nc_power( + ((Fraction(permutation_sign), 0, tuple(traces)),) + ) + + +def _coloured_word(sector: LCColorSector) -> tuple[int, ...]: + if sector.kind == "single-trace": + return tuple(sector.trace_labels) + if sector.kind == "open-lines": + return tuple(sector.word_labels or sector.color_words[0]) + return () + + +def _two_line_reference_start(color_plan: GenericColorPlan) -> int | None: + if color_plan.process.fundamental_labels: + return int(color_plan.process.fundamental_labels[0]) + return None + + +def _rotate_to_reference_start( + word: tuple[int, ...], + reference_start: int | None, +) -> tuple[int, ...]: + if reference_start is None or reference_start not in word: + return word + offset = word.index(reference_start) + return word[offset:] + word[:offset] + + +def _two_line_gi_ui( + color_plan: GenericColorPlan, + word: tuple[int, ...], + reference_word: tuple[int, ...], +) -> tuple[int, int]: + fundamental_endpoints = set(color_plan.process.fundamental_labels) | set( + color_plan.process.antifundamental_labels + ) + gi = 0 + for position in range(1, max(len(word) - 1, 1)): + if word[position] in fundamental_endpoints: + gi = position - 1 + break + ui = 1 + if len(word) >= 2 and len(reference_word) >= 2: + same_ends = word[0] == reference_word[0] and word[-1] == reference_word[-1] + opposite_ends = word[0] != reference_word[0] and word[-1] != reference_word[-1] + ui = 1 if same_ends or opposite_ends else 2 + return gi, ui + + +def _two_line_ordered_adjoint_strings( + color_plan: GenericColorPlan, + iper: tuple[int, ...], + jper: tuple[int, ...], +) -> tuple[tuple[int, ...], tuple[int, ...]]: + fundamental_endpoints = set(color_plan.process.fundamental_labels) | set( + color_plan.process.antifundamental_labels + ) + return ( + tuple(label for label in iper if label not in fundamental_endpoints), + tuple(label for label in jper if label not in fundamental_endpoints), + ) + + +def _convert_two_line_adjoint_strings( + n_ord: int, + iper: tuple[int, ...], + jper: tuple[int, ...], +) -> tuple[tuple[int, ...], tuple[int, ...]]: + """Mirror AmpliCol's ordered-adjoint-string conversion. + + The NLC topology tests depend only on the relative order of adjoints. The + Fortran routine maps the largest external label to 1, the next largest to + 2, and so on, separately for the row and column strings. + """ + + expected = max(n_ord - 4, 0) + if len(iper) != expected or len(jper) != expected: + return iper, jper + + def convert(word: tuple[int, ...]) -> tuple[int, ...]: + ordered = sorted(word, reverse=True) + rank = {label: index + 1 for index, label in enumerate(ordered)} + return tuple(rank[label] for label in word) + + return convert(iper), convert(jper) + + +def _check_nlc_two_open_lines_same_species( + n_ord: int, + iper: tuple[int, ...], + jper: tuple[int, ...], + ri: int, + rj: int, + ii: int, + jj: int, +) -> int: + """Port of AmpliCol's same-species two-open-line NLC topology filter. + + It returns 99 for LC-like entries, +/-1 for first subleading entries, and + 0 for NNLC-or-beyond entries. The final NLC coefficient is still the full + trace overlap; this function only decides whether that overlap belongs in + the NLC-expanded matrix. + """ + + m = max(n_ord - 4, 0) + if len(iper) != m or len(jper) != m: + return 0 + if (ii, jj) in {(1, 2), (2, 1)}: + temp = ( + iper[:ri] + + tuple(reversed(jper[rj:])) + + iper[ri:] + + tuple(reversed(jper[:rj])) + ) + return -1 if _nlc_pairing_is_planar(temp, m) else 0 + if (ii, jj) not in {(1, 1), (2, 2)}: + return 0 + if ri == rj and iper == jper: + return 99 + + aa = iper[:ri] + bb = iper[ri:] + cc = jper[:rj] + dd = jper[rj:] + temp2 = aa + tuple(reversed(cc)) + temp3 = bb + tuple(reversed(dd)) + disjoint = not (set(temp2) & set(temp3)) + if disjoint: + if aa and (bb == dd or not bb): + return _check_nlc_subword(cc, aa, threshold=m - 4) + if bb and (aa == cc or not aa): + return _check_nlc_subword(dd, bb, threshold=m - rj - 4) + return 0 + return _check_nlc_two_open_lines_overlap(aa, bb, cc, dd, m, ri, rj) + + +def _nlc_pairing_is_planar(word: tuple[int, ...], m: int) -> bool: + if m == 0: + return True + positions: dict[int, list[int]] = {label: [] for label in range(1, m + 1)} + for offset, label in enumerate(word): + if label in positions: + positions[label].append(offset) + if any(len(item) != 2 for item in positions.values()): + return False + for first, pair in positions.items(): + if abs(pair[0] - pair[1]) % 2 != 1: + return False + for second in range(first + 1, m + 1): + if not _intervals_disjoint_or_nested(tuple(pair), tuple(positions[second])): + return False + return True + + +def _intervals_disjoint_or_nested( + first: tuple[int, int], + second: tuple[int, int], +) -> bool: + a1, a2 = first + b1, b2 = second + return ( + (a1 < b1 and a2 < b1) + or (a1 > b2 and a2 > b2) + or (a1 > b1 and a2 < b2) + or (b1 > a1 and b2 < a2) + ) + + +def _check_nlc_subword( + candidate: tuple[int, ...], + target: tuple[int, ...], + *, + threshold: int, +) -> int: + if candidate == target: + return 99 + if not candidate or len(candidate) != len(target): + return 0 + sign = _check_nlc(candidate, target) + if sign == 0: + return 0 + if sign < 0: + return sign + return sign + + +def _check_nlc_two_open_lines_overlap( + aa: tuple[int, ...], + bb: tuple[int, ...], + cc: tuple[int, ...], + dd: tuple[int, ...], + m: int, + ri: int, + rj: int, +) -> int: + """Remaining overlapping-sets branch of the two-open-line NLC check.""" + + temp2 = aa + tuple(reversed(cc)) + temp3 = bb + tuple(reversed(dd)) + common = set(temp2) & set(temp3) + if not common: + return 0 + skipped = None + ind_i = ind_j = -1 + for i, label in enumerate(temp2): + if label not in common: + continue + ind_i = i + ind_j = temp3.index(label) + skipped = label + break + if skipped is None: + return 0 + if len(temp2) == 1 or len(temp3) == 1: + return 0 + + perm: tuple[int, ...] + if ind_i < ri and ind_j >= len(bb): + # Common generator in the A-D pair. + perm = ( + temp2[ind_i + 1 : ri] + + temp2[ri : ri + rj] + + temp2[:ind_i] + + temp3[ind_j + 1 :] + + temp3[: len(bb)] + + temp3[len(bb) : ind_j] + ) + itemp4 = temp2[:ind_i] + temp2[ind_i + 1 : ri] + itemp5 = tuple(reversed(temp3[ind_j + 1 :])) + tuple( + reversed(temp3[len(bb) : ind_j]) + ) + if rj == ri - 1 and itemp4 == cc: + return 0 + if rj + 1 == ri and itemp5 == bb: + return 0 + elif ind_i >= ri and ind_j < len(bb): + # Common generator in the B-C pair. + perm = ( + temp2[ind_i + 1 :] + + temp2[:ri] + + temp2[ri:ind_i] + + temp3[ind_j + 1 : len(bb)] + + temp3[len(bb) :] + + temp3[:ind_j] + ) + itemp6 = tuple(reversed(temp2[ind_i + 1 :])) + tuple(reversed(temp2[ri:ind_i])) + itemp7 = temp3[:ind_j] + temp3[ind_j + 1 : len(bb)] + if rj - 1 == ri and itemp6 == aa: + return 0 + if rj == ri + 1 and itemp7 == dd: + return 0 + else: + return 0 + + positions: dict[int, list[int]] = {label: [] for label in range(1, m + 1)} + for offset, label in enumerate(perm): + if label == skipped: + continue + if label in positions: + positions[label].append(offset) + for label, pair in positions.items(): + if label == skipped: + continue + if len(pair) != 2 or abs(pair[0] - pair[1]) % 2 != 1: + return 0 + for first in range(1, m + 1): + if first == skipped: + continue + for second in range(first + 1, m + 1): + if second == skipped: + continue + if not _intervals_disjoint_or_nested( + tuple(positions[first]), + tuple(positions[second]), + ): + return 0 + return 1 + + +def _has_repeated_fundamental_species(color_plan: GenericColorPlan) -> bool: + pdg_by_label: dict[int, int] = { + leg.label: abs(int(leg.outgoing_pdg)) + for leg in color_plan.process.legs + if leg.outgoing_pdg is not None + } + fundamental_flavours = [ + pdg_by_label[label] + for label in color_plan.process.fundamental_labels + if label in pdg_by_label + ] + return len(set(fundamental_flavours)) < len(fundamental_flavours) + + +def _common_helicity_weight( + left: ColorGroupDescriptor, + right: ColorGroupDescriptor, +) -> float: + if abs(left.helicity_weight - right.helicity_weight) > 1.0e-12: + raise ValueError( + "inconsistent helicity weights in color contraction group " + f"{left.helicity_key!r}: group {left.group_id} has " + f"{left.helicity_weight!r}, group {right.group_id} has " + f"{right.helicity_weight!r}" + ) + return left.helicity_weight diff --git a/pyamplicol/src/pyamplicol/color/contraction_trace.py b/pyamplicol/src/pyamplicol/color/contraction_trace.py new file mode 100644 index 0000000000..e6771475d9 --- /dev/null +++ b/pyamplicol/src/pyamplicol/color/contraction_trace.py @@ -0,0 +1,435 @@ +# SPDX-License-Identifier: 0BSD +"""Exact trace and NLC algebra used by color contraction.""" + +from __future__ import annotations + +from collections.abc import Iterable, Mapping, Sequence +from fractions import Fraction + +from .contraction_types import NC + + +def _eval_trace( + traces: tuple[tuple[int, ...], ...], + *, + coeff: Fraction = Fraction(1), +) -> float: + terms = _simplify_trace_terms(((coeff, traces),)) + return _eval_nc_terms(terms) + + +def _eval_trace_exact( + traces: tuple[tuple[int, ...], ...], + *, + coeff: Fraction = Fraction(1), +) -> Fraction: + """Evaluate a trace product at ``Nc`` without passing through binary64.""" + + terms = _simplify_trace_terms(((coeff, traces),)) + return _eval_nc_terms_exact(terms) + + +def _simplify_trace_terms( + initial: Iterable[tuple[Fraction, tuple[tuple[int, ...], ...]]], +) -> Mapping[int, Fraction]: + terms: list[tuple[Fraction, tuple[tuple[int, ...], ...]]] = [ + (coeff, tuple(tuple(trace) for trace in traces)) + for coeff, traces in initial + if coeff + ] + guard = 0 + while True: + guard += 1 + if guard > 100000: + raise RuntimeError("colour trace simplification did not converge") + terms = _simplify_tr1_tr0(terms) + if not terms: + return {} + if all(not traces for _coeff, traces in terms): + result: dict[int, Fraction] = {} + for coeff, _traces in terms: + result[0] = result.get(0, Fraction(0)) + coeff + return result + next_terms, changed = _trace_pair_simplify(terms) + terms = _simplify_tr1_tr0(next_terms) + next_terms, changed_dup = _trace_duplicate_simplify(terms) + terms = next_terms + if not changed and not changed_dup and any(traces for _coeff, traces in terms): + raise RuntimeError(f"cannot simplify colour traces: {terms[:3]}") + + +def _simplify_trace_terms_nc_power( + initial: Iterable[tuple[Fraction, int, tuple[tuple[int, ...], ...]]], +) -> Mapping[int, Fraction]: + terms: list[tuple[Fraction, int, tuple[tuple[int, ...], ...]]] = [ + (coeff, power, tuple(tuple(trace) for trace in traces)) + for coeff, power, traces in initial + if coeff + ] + guard = 0 + while True: + guard += 1 + if guard > 100000: + raise RuntimeError("colour trace simplification did not converge") + terms = _simplify_tr1_tr0_nc_power(terms) + if not terms: + return {} + if all(not traces for _coeff, _power, traces in terms): + result: dict[int, Fraction] = {} + for coeff, power, _traces in terms: + result[power] = result.get(power, Fraction(0)) + coeff + return {power: coeff for power, coeff in result.items() if coeff} + next_terms, changed = _trace_pair_simplify_nc_power(terms) + terms = _simplify_tr1_tr0_nc_power(next_terms) + next_terms, changed_dup = _trace_duplicate_simplify_nc_power(terms) + terms = _combine_nc_power_terms(next_terms) + if ( + not changed + and not changed_dup + and any(traces for _coeff, _power, traces in terms) + ): + raise RuntimeError(f"cannot simplify colour traces: {terms[:3]}") + + +def _simplify_tr1_tr0_nc_power( + terms: list[tuple[Fraction, int, tuple[tuple[int, ...], ...]]], +) -> list[tuple[Fraction, int, tuple[tuple[int, ...], ...]]]: + result: list[tuple[Fraction, int, tuple[tuple[int, ...], ...]]] = [] + for coeff, power, traces in terms: + if any(len(trace) == 1 for trace in traces): + continue + extra_power = sum(1 for trace in traces if len(trace) == 0) + remaining = tuple(trace for trace in traces if len(trace) != 0) + result.append((coeff, power + extra_power, remaining)) + return _combine_nc_power_terms(result) + + +def _trace_pair_simplify_nc_power( + terms: list[tuple[Fraction, int, tuple[tuple[int, ...], ...]]], +) -> tuple[list[tuple[Fraction, int, tuple[tuple[int, ...], ...]]], bool]: + result: list[tuple[Fraction, int, tuple[tuple[int, ...], ...]]] = [] + changed = False + for coeff, power, traces in terms: + replaced = False + for first_index, first in enumerate(traces): + for second_index in range(first_index + 1, len(traces)): + second = traces[second_index] + found = _first_common_position(first, second) + if found is None: + continue + first_pos, second_pos = found + combined = ( + first[:first_pos] + + second[second_pos + 1 :] + + second[:second_pos] + + first[first_pos + 1 :] + ) + rest = tuple( + trace + for index, trace in enumerate(traces) + if index not in {first_index, second_index} + ) + result.append((coeff, power, (combined, *rest))) + reduced_first = first[:first_pos] + first[first_pos + 1 :] + reduced_second = second[:second_pos] + second[second_pos + 1 :] + result.append( + ( + -coeff, + power - 1, + tuple( + trace + if index not in {first_index, second_index} + else ( + reduced_first + if index == first_index + else reduced_second + ) + for index, trace in enumerate(traces) + ), + ) + ) + changed = True + replaced = True + break + if replaced: + break + if not replaced: + result.append((coeff, power, traces)) + return result, changed + + +def _trace_duplicate_simplify_nc_power( + terms: list[tuple[Fraction, int, tuple[tuple[int, ...], ...]]], +) -> tuple[list[tuple[Fraction, int, tuple[tuple[int, ...], ...]]], bool]: + result: list[tuple[Fraction, int, tuple[tuple[int, ...], ...]]] = [] + changed = False + for coeff, power, traces in terms: + replaced = False + for trace_index, trace in enumerate(traces): + positions = _first_duplicate_positions(trace) + if positions is None: + continue + first_pos, second_pos = positions + a = trace[:first_pos] + b = trace[first_pos + 1 : second_pos] + c = trace[second_pos + 1 :] + rest = tuple( + item for index, item in enumerate(traces) if index != trace_index + ) + result.append((coeff, power, (a + c, b, *rest))) + result.append((-coeff, power - 1, (a + b + c, *rest))) + changed = True + replaced = True + break + if not replaced: + result.append((coeff, power, traces)) + return result, changed + + +def _combine_nc_power_terms( + terms: Iterable[tuple[Fraction, int, tuple[tuple[int, ...], ...]]], +) -> list[tuple[Fraction, int, tuple[tuple[int, ...], ...]]]: + combined: dict[tuple[int, tuple[tuple[int, ...], ...]], Fraction] = {} + for coeff, power, traces in terms: + if not coeff: + continue + key = (power, tuple(sorted(tuple(trace) for trace in traces))) + combined[key] = combined.get(key, Fraction(0)) + coeff + return [ + (coeff, power, traces) for (power, traces), coeff in combined.items() if coeff + ] + + +def _simplify_tr1_tr0( + terms: list[tuple[Fraction, tuple[tuple[int, ...], ...]]], +) -> list[tuple[Fraction, tuple[tuple[int, ...], ...]]]: + result: list[tuple[Fraction, tuple[tuple[int, ...], ...]]] = [] + for coeff, traces in terms: + if any(len(trace) == 1 for trace in traces): + continue + power = sum(1 for trace in traces if len(trace) == 0) + remaining = tuple(trace for trace in traces if len(trace) != 0) + result.append((coeff * (NC**power), remaining)) + return [(coeff, traces) for coeff, traces in result if coeff] + + +def _trace_pair_simplify( + terms: list[tuple[Fraction, tuple[tuple[int, ...], ...]]], +) -> tuple[list[tuple[Fraction, tuple[tuple[int, ...], ...]]], bool]: + result: list[tuple[Fraction, tuple[tuple[int, ...], ...]]] = [] + changed = False + for coeff, traces in terms: + replaced = False + for first_index, first in enumerate(traces): + for second_index in range(first_index + 1, len(traces)): + second = traces[second_index] + found = _first_common_position(first, second) + if found is None: + continue + first_pos, second_pos = found + combined = ( + first[:first_pos] + + second[second_pos + 1 :] + + second[:second_pos] + + first[first_pos + 1 :] + ) + rest = tuple( + trace + for index, trace in enumerate(traces) + if index not in {first_index, second_index} + ) + result.append((coeff, (combined, *rest))) + reduced_first = first[:first_pos] + first[first_pos + 1 :] + reduced_second = second[:second_pos] + second[second_pos + 1 :] + result.append( + ( + -coeff / NC, + tuple( + trace + if index not in {first_index, second_index} + else ( + reduced_first + if index == first_index + else reduced_second + ) + for index, trace in enumerate(traces) + ), + ) + ) + changed = True + replaced = True + break + if replaced: + break + if not replaced: + result.append((coeff, traces)) + return result, changed + + +def _trace_duplicate_simplify( + terms: list[tuple[Fraction, tuple[tuple[int, ...], ...]]], +) -> tuple[list[tuple[Fraction, tuple[tuple[int, ...], ...]]], bool]: + result: list[tuple[Fraction, tuple[tuple[int, ...], ...]]] = [] + changed = False + for coeff, traces in terms: + replaced = False + for trace_index, trace in enumerate(traces): + positions = _first_duplicate_positions(trace) + if positions is None: + continue + first_pos, second_pos = positions + a = trace[:first_pos] + b = trace[first_pos + 1 : second_pos] + c = trace[second_pos + 1 :] + rest = tuple( + item for index, item in enumerate(traces) if index != trace_index + ) + result.append((coeff, (a + c, b, *rest))) + result.append((-coeff / NC, (a + b + c, *rest))) + changed = True + replaced = True + break + if not replaced: + result.append((coeff, traces)) + return result, changed + + +def _first_common_position( + left: Sequence[int], + right: Sequence[int], +) -> tuple[int, int] | None: + for left_index, label in enumerate(left): + for right_index, other in enumerate(right): + if label == other: + return left_index, right_index + return None + + +def _first_duplicate_positions(trace: Sequence[int]) -> tuple[int, int] | None: + seen: dict[int, int] = {} + for index, label in enumerate(trace): + if label in seen: + return seen[label], index + seen[label] = index + return None + + +def _eval_nc_terms( + terms: Mapping[int, Fraction], + *, + min_power: int | None = None, +) -> float: + value = 0.0 + for power, coeff in terms.items(): + if min_power is not None and power < min_power: + continue + value += float(coeff) * (float(NC) ** power) + return value + + +def _eval_nc_terms_exact( + terms: Mapping[int, Fraction], + *, + min_power: int | None = None, +) -> Fraction: + """Evaluate an exact Laurent polynomial in ``Nc`` as a ``Fraction``.""" + + value = Fraction(0) + nc = Fraction(NC) + for power, coeff in terms.items(): + if min_power is not None and power < min_power: + continue + value += coeff * nc**power + return value + + +def _check_nlc(jper: tuple[int, ...], iper: tuple[int, ...]) -> int: + n = len(iper) + if n == 0: + return 99 + i1 = next((i for i in range(n) if jper[i] != iper[i]), n) + if i1 == n: + return 99 + try: + i2 = next(i for i in range(i1 + 1, n) if jper[i] == iper[i1]) + except StopIteration: + return 0 + offset = 0 + while ( + i2 + offset < n and i1 + offset < n and jper[i2 + offset] == iper[i1 + offset] + ): + offset += 1 + i3 = i2 + offset - 1 + i4 = i1 + offset - 1 + if i4 + 1 >= n: + return 0 + try: + i5 = next(i for i in range(i1, n) if jper[i] == iper[i4 + 1]) + except StopIteration: + return 0 + if i5 > i3: + return 0 + sign = 1 + if i1 > i5 - 1: + left_len = i4 - i1 + right_len = i2 - 1 - i5 + if (left_len == 0 and right_len == 0) or ( + n > 0 + and ( + (left_len == 0 and right_len == n - 3) + or (left_len == n - 3 and right_len == 0) + ) + ): + sign = -1 + elif left_len == 0 or right_len == 0: + return 0 + elif left_len + right_len <= max(n - 4, 0): + pass + else: + return 0 + itemp = jper[:i1] + jper[i2 : i3 + 1] + jper[i5:i2] + jper[i1:i5] + jper[i3 + 1 :] + return sign if itemp == iper else 0 + + +def _check_nlc_one_open_line(jper: tuple[int, ...], iper: tuple[int, ...]) -> int: + """Port AmpliCol's one-open-line NLC topology check.""" + + n = len(iper) + if len(jper) != n: + return 0 + i1 = next((i for i in range(n) if jper[i] != iper[i]), n) + if i1 >= n: + return 99 + try: + i2 = next(i for i in range(i1 + 1, n) if jper[i] == iper[i1]) + except StopIteration: + return 0 + offset = 0 + while ( + i2 + offset < n and i1 + offset < n and jper[i2 + offset] == iper[i1 + offset] + ): + offset += 1 + i3 = i2 + offset - 1 + i4 = i1 + offset - 1 + if i4 + 1 >= n: + return 0 + try: + i5 = next(i for i in range(i1, n) if jper[i] == iper[i4 + 1]) + except StopIteration: + return 0 + if i5 > i3: + return 0 + sign = 1 + if i1 > i5 - 1: + left_len = i4 - i1 + right_len = i2 - 1 - i5 + if left_len == 0 and right_len == 0: + sign = -1 + elif left_len == 0 or right_len == 0: + return 0 + elif left_len + right_len <= n - 4: + pass + else: + sign = 1 + itemp = jper[:i1] + jper[i2 : i3 + 1] + jper[i5:i2] + jper[i1:i5] + jper[i3 + 1 :] + return sign if itemp == iper else 0 diff --git a/pyamplicol/src/pyamplicol/color/contraction_types.py b/pyamplicol/src/pyamplicol/color/contraction_types.py new file mode 100644 index 0000000000..9305feda02 --- /dev/null +++ b/pyamplicol/src/pyamplicol/color/contraction_types.py @@ -0,0 +1,575 @@ +# SPDX-License-Identifier: 0BSD +"""Frozen color-contraction records.""" + +from __future__ import annotations + +import math +from collections.abc import Iterator +from dataclasses import dataclass +from fractions import Fraction + +NC = 3 + + +@dataclass(frozen=True) +class ColorContractionEntry: + left_group_id: int + right_group_id: int + weight_re: float + weight_im: float = 0.0 + symmetry_factor: float = 1.0 + exact_weight: tuple[Fraction, Fraction] | None = None + + def to_json_dict(self) -> dict[str, object]: + result = { + "left_group_id": self.left_group_id, + "right_group_id": self.right_group_id, + "weight": [self.weight_re, self.weight_im], + "symmetry_factor": self.symmetry_factor, + } + if self.exact_weight is not None: + result["exact_weight"] = _exact_weight_payload(self.exact_weight) + return result + + +@dataclass(frozen=True) +class ColorContractionTemplateEntry: + left_group_index: int + right_group_index: int + weight_re: float + weight_im: float = 0.0 + symmetry_factor: float = 1.0 + exact_weight: tuple[Fraction, Fraction] | None = None + + def to_json_dict(self) -> dict[str, object]: + result = { + "left_group_index": self.left_group_index, + "right_group_index": self.right_group_index, + "weight": [self.weight_re, self.weight_im], + "symmetry_factor": self.symmetry_factor, + } + if self.exact_weight is not None: + result["exact_weight"] = _exact_weight_payload(self.exact_weight) + return result + + +def _exact_weight_payload(weight: tuple[Fraction, Fraction]) -> list[str]: + # Strings keep arbitrarily large integers exact in every JSON consumer. + return [ + str(part) for value in weight for part in (value.numerator, value.denominator) + ] + + +@dataclass(frozen=True) +class FactorizedColorContractionBlock: + """A compact transform plan for one repeated color matrix. + + ``klein-four-walsh`` records a free action of a Klein-four subgroup. Each + coset is ordered as identity, first generator, second generator, and their + product. The runtime validates the matrix invariance before using the + corresponding four-point Walsh transform. + + ``elementary-abelian-walsh`` generalizes the same representation to a + free ``C2**rank`` action. Each coset is ordered by generator bitmask, so + group multiplication is bitwise XOR. + """ + + kind: str + cosets: tuple[tuple[int, ...], ...] + rank: int | None = None + + def __post_init__(self) -> None: + if not self.cosets: + raise ValueError("factorized color contraction coset map is empty") + if self.kind == "klein-four-walsh": + if self.rank is not None: + raise ValueError("Klein-four color factorization cannot declare rank") + expected_coset_size = 4 + elif self.kind == "elementary-abelian-walsh": + if self.rank is None or self.rank < 3: + raise ValueError( + "elementary-Abelian color factorization requires rank >= 3" + ) + expected_coset_size = len(self.cosets[0]) + if ( + expected_coset_size & (expected_coset_size - 1) + or expected_coset_size.bit_length() - 1 != self.rank + ): + raise ValueError( + "elementary-Abelian color factorization rank is inconsistent" + ) + else: + raise ValueError(f"unknown color contraction factorization {self.kind!r}") + if any(len(coset) != expected_coset_size for coset in self.cosets): + raise ValueError( + "factorized color contraction coset size does not match its rank" + ) + flattened = tuple(index for coset in self.cosets for index in coset) + if any(index < 0 for index in flattened): + raise ValueError( + "factorized color contraction coset map has a negative index" + ) + if len(set(flattened)) != len(flattened): + raise ValueError( + "factorized color contraction coset map contains duplicate indices" + ) + + def to_json_dict(self) -> dict[str, object]: + cosets = [list(coset) for coset in self.cosets] + if self.rank is None: + return {"kind": self.kind, "cosets": cosets} + return {"kind": self.kind, "rank": self.rank, "cosets": cosets} + + +@dataclass(frozen=True) +class RepeatedColorContractionBlock: + component_count: int + component_group_ids: tuple[int, ...] + entries: tuple[ColorContractionTemplateEntry, ...] + factorized_block: FactorizedColorContractionBlock | None = None + + def __post_init__(self) -> None: + if self.component_count < 2: + raise ValueError( + "repeated color contraction requires at least two components" + ) + if not self.component_group_ids: + raise ValueError("repeated color contraction group map is empty") + if len(self.component_group_ids) % self.component_count != 0: + raise ValueError( + "repeated color contraction group IDs do not form a rectangular map" + ) + if len(set(self.component_group_ids)) != len(self.component_group_ids): + raise ValueError("repeated color contraction group IDs are not unique") + local_group_count = self.local_group_count + if any( + entry.left_group_index < 0 + or entry.left_group_index >= local_group_count + or entry.right_group_index < 0 + or entry.right_group_index >= local_group_count + for entry in self.entries + ): + raise ValueError( + "repeated color contraction entry references an unknown local group" + ) + if self.factorized_block is not None: + flattened = tuple( + index for coset in self.factorized_block.cosets for index in coset + ) + if sorted(flattened) != list(range(local_group_count)): + raise ValueError( + "factorized color contraction cosets do not partition local groups" + ) + + @property + def local_group_count(self) -> int: + return len(self.component_group_ids) // self.component_count + + def to_json_dict(self) -> dict[str, object]: + result: dict[str, object] = { + "component_count": self.component_count, + "component_group_ids": list(self.component_group_ids), + "entries": [entry.to_json_dict() for entry in self.entries], + } + if self.factorized_block is not None: + result["factorized_block"] = self.factorized_block.to_json_dict() + return result + + +@dataclass(frozen=True) +class SymmetricGroupColorContractionBlock: + """Certified regular symmetric-group orbits plus exact residual metric. + + Local groups are ordered channel-major and permutation-major. Every + channel coset contains the lexicographic permutations of ``range(degree)`` + with the identity at offset zero. Groups outside certified ``S_degree`` + orbits form one consecutive residual suffix. ``kernel_entries`` stores + exactly one complete relative-permutation row for every upper-triangular + channel pair; it is not an expanded color matrix. + + The component map follows the existing local-group-major/component-minor + convention. Exact weights are parallel to their binary64 template entries + and exclude the entry's upper-triangle symmetry factor. + """ + + degree: int + component_count: int + component_group_ids: tuple[int, ...] + local_sector_ids: tuple[int, ...] + channel_cosets: tuple[tuple[int, ...], ...] + kernel_entries: tuple[ColorContractionTemplateEntry, ...] + kernel_exact_weights: tuple[Fraction, ...] + residual_entries: tuple[ColorContractionTemplateEntry, ...] + residual_exact_weights: tuple[Fraction, ...] + residual_local_group_indices: tuple[int, ...] + hermiticity_check_mode: str = "vacuous" + hermiticity_relative_indices: tuple[int, ...] = () + + def __post_init__(self) -> None: + if isinstance(self.degree, bool) or not isinstance(self.degree, int): + raise ValueError("symmetric-group degree must be an integer") + if self.degree < 2: + raise ValueError("symmetric-group degree must be at least two") + if self.degree > 10: + raise ValueError("symmetric-group degree exceeds the supported maximum 10") + if self.component_count < 1: + raise ValueError("symmetric-group color contraction requires a component") + if not self.component_group_ids: + raise ValueError("symmetric-group component group map is empty") + if len(self.component_group_ids) % self.component_count: + raise ValueError( + "symmetric-group component group IDs do not form a rectangular map" + ) + if len(set(self.component_group_ids)) != len(self.component_group_ids): + raise ValueError("symmetric-group component group IDs contain duplicates") + if any(group_id < 0 for group_id in self.component_group_ids): + raise ValueError( + "symmetric-group component group IDs contain a negative ID" + ) + + local_group_count = self.local_group_count + if len(self.local_sector_ids) != local_group_count: + raise ValueError( + "symmetric-group local sector map does not match local groups" + ) + if len(set(self.local_sector_ids)) != len(self.local_sector_ids) or any( + sector_id < 0 for sector_id in self.local_sector_ids + ): + raise ValueError( + "symmetric-group local sector map must contain unique nonnegative IDs" + ) + group_order = math.factorial(self.degree) + if not self.channel_cosets: + raise ValueError("symmetric-group color contraction has no channels") + eligible_group_count = len(self.channel_cosets) * group_order + if eligible_group_count > local_group_count: + raise ValueError( + "symmetric-group channel cosets exceed the local group domain" + ) + for channel_index, coset in enumerate(self.channel_cosets): + start = channel_index * group_order + if coset != tuple(range(start, start + group_order)): + raise ValueError( + "symmetric-group channel cosets must be consecutive and " + "channel-major" + ) + expected_residual = tuple(range(eligible_group_count, local_group_count)) + if self.residual_local_group_indices != expected_residual: + raise ValueError( + "symmetric-group residual groups must be the consecutive " + "complement after channel cosets" + ) + + expected_kernel_count = ( + len(self.channel_cosets) * (len(self.channel_cosets) + 1) // 2 * group_order + ) + if len(self.kernel_entries) != expected_kernel_count: + raise ValueError( + "symmetric-group kernel rows do not cover every channel pair" + ) + if len(self.kernel_exact_weights) != len(self.kernel_entries): + raise ValueError( + "symmetric-group exact kernel weights do not match kernel entries" + ) + kernel_offset = 0 + for left_channel, left_coset in enumerate(self.channel_cosets): + for right_channel in range(left_channel, len(self.channel_cosets)): + right_coset = self.channel_cosets[right_channel] + expected_symmetry = 1.0 if left_channel == right_channel else 2.0 + for relative_index in range(group_order): + entry = self.kernel_entries[kernel_offset] + if ( + entry.left_group_index != left_coset[0] + or entry.right_group_index != right_coset[relative_index] + or entry.symmetry_factor != expected_symmetry + ): + raise ValueError( + "symmetric-group kernel rows are not in canonical " + "channel/relative-permutation order" + ) + kernel_offset += 1 + self._validate_exact_entries( + self.kernel_entries, + self.kernel_exact_weights, + local_group_count=local_group_count, + label="kernel", + ) + + if len(self.residual_exact_weights) != len(self.residual_entries): + raise ValueError( + "symmetric-group exact residual weights do not match residual entries" + ) + expected_residual_pairs = ( + tuple( + (left_index, right_index) + for left_index in range(local_group_count) + for right_index in range(left_index, local_group_count) + if right_index >= eligible_group_count + ) + if self.residual_local_group_indices + else () + ) + residual_pairs = tuple( + (entry.left_group_index, entry.right_group_index) + for entry in self.residual_entries + ) + if residual_pairs != expected_residual_pairs: + raise ValueError( + "symmetric-group residual rows must exhaust every canonical pair " + "touching the residual suffix" + ) + for entry in self.residual_entries: + pair = (entry.left_group_index, entry.right_group_index) + expected_symmetry = 1.0 if pair[0] == pair[1] else 2.0 + if entry.symmetry_factor != expected_symmetry: + raise ValueError( + "symmetric-group residual entry has an invalid symmetry factor" + ) + self._validate_exact_entries( + self.residual_entries, + self.residual_exact_weights, + local_group_count=local_group_count, + label="residual", + ) + + cross_channel_count = self.channel_count * (self.channel_count - 1) // 2 + if cross_channel_count == 0: + if ( + self.hermiticity_check_mode != "vacuous" + or self.hermiticity_relative_indices + ): + raise ValueError( + "single-channel symmetric-group Hermiticity provenance must " + "be vacuous" + ) + elif self.degree <= 4: + if ( + self.hermiticity_check_mode != "full" + or self.hermiticity_relative_indices != tuple(range(group_order)) + ): + raise ValueError( + "small symmetric-group cross-channel kernels require a full " + "Hermiticity certificate" + ) + elif ( + self.hermiticity_check_mode != "deterministic-samples" + or not self.hermiticity_relative_indices + or self.hermiticity_relative_indices[0] != 0 + or len(set(self.hermiticity_relative_indices)) + != len(self.hermiticity_relative_indices) + or any( + index < 0 or index >= group_order + for index in self.hermiticity_relative_indices + ) + ): + raise ValueError( + "large symmetric-group cross-channel kernels require canonical " + "sampled Hermiticity provenance" + ) + + @staticmethod + def _validate_exact_entries( + entries: tuple[ColorContractionTemplateEntry, ...], + exact_weights: tuple[Fraction, ...], + *, + local_group_count: int, + label: str, + ) -> None: + for index, (entry, exact) in enumerate( + zip(entries, exact_weights, strict=True) + ): + if not isinstance(exact, Fraction): + raise ValueError( + f"symmetric-group {label} exact weight {index} is not a Fraction" + ) + if ( + entry.left_group_index < 0 + or entry.left_group_index >= local_group_count + or entry.right_group_index < 0 + or entry.right_group_index >= local_group_count + ): + raise ValueError( + f"symmetric-group {label} entry references an unknown local group" + ) + if entry.weight_im != 0.0: + raise ValueError( + f"symmetric-group {label} entry has a complex color weight" + ) + if not math.isfinite(entry.weight_re) or float(exact) != entry.weight_re: + raise ValueError( + f"symmetric-group {label} binary64 and exact weights disagree" + ) + + @property + def local_group_count(self) -> int: + return len(self.component_group_ids) // self.component_count + + @property + def group_order(self) -> int: + return math.factorial(self.degree) + + @property + def channel_count(self) -> int: + return len(self.channel_cosets) + + @property + def stored_entry_count(self) -> int: + return len(self.kernel_entries) + len(self.residual_entries) + + @property + def hermiticity_check_count(self) -> int: + return ( + self.channel_count + * (self.channel_count - 1) + // 2 + * len(self.hermiticity_relative_indices) + ) + + def to_json_dict(self) -> dict[str, object]: + def exact_payload(value: Fraction) -> list[int]: + return [value.numerator, value.denominator] + + return { + "kind": "symmetric-group-fourier", + "degree": self.degree, + "component_count": self.component_count, + "component_group_ids": list(self.component_group_ids), + "local_sector_ids": list(self.local_sector_ids), + "channel_cosets": [list(coset) for coset in self.channel_cosets], + "kernel_entries": [entry.to_json_dict() for entry in self.kernel_entries], + "kernel_exact_weights": [ + exact_payload(value) for value in self.kernel_exact_weights + ], + "residual_entries": [ + entry.to_json_dict() for entry in self.residual_entries + ], + "residual_exact_weights": [ + exact_payload(value) for value in self.residual_exact_weights + ], + "residual_local_group_indices": list(self.residual_local_group_indices), + "hermiticity_certificate": { + "mode": self.hermiticity_check_mode, + "relative_indices": list(self.hermiticity_relative_indices), + "check_count": self.hermiticity_check_count, + }, + } + + +@dataclass(frozen=True) +class ColorContractionPlan: + color_accuracy: str + supported: bool + reason: str | None + group_count: int + entries: tuple[ColorContractionEntry, ...] + repeated_block: RepeatedColorContractionBlock | None = None + symmetric_group_block: SymmetricGroupColorContractionBlock | None = None + destination_by_group: tuple[int, ...] | None = None + includes_color_factor: bool = True + + def __post_init__(self) -> None: + storage_count = sum( + ( + bool(self.entries), + self.repeated_block is not None, + self.symmetric_group_block is not None, + ) + ) + if storage_count > 1: + raise ValueError( + "color contraction cannot mix expanded, repeated, and " + "symmetric-group storage" + ) + if ( + self.repeated_block is not None + and len(self.repeated_block.component_group_ids) != self.group_count + ): + raise ValueError( + "repeated color contraction group map does not match group count" + ) + if ( + self.symmetric_group_block is not None + and len(self.symmetric_group_block.component_group_ids) != self.group_count + ): + raise ValueError( + "symmetric-group color contraction group map does not match group count" + ) + if self.destination_by_group is not None: + if len(self.destination_by_group) != self.group_count: + raise ValueError( + "color contraction destination projection does not match " + "group count" + ) + if len(set(self.destination_by_group)) != len( + self.destination_by_group + ) or any(value < 0 for value in self.destination_by_group): + raise ValueError( + "color contraction destination projection must contain " + "unique nonnegative IDs" + ) + + @property + def logical_entry_count(self) -> int: + if self.repeated_block is None: + if self.symmetric_group_block is None: + return len(self.entries) + return ( + self.symmetric_group_block.component_count + * self.symmetric_group_block.stored_entry_count + ) + return self.repeated_block.component_count * len(self.repeated_block.entries) + + def iter_logical_entries(self) -> Iterator[ColorContractionEntry]: + if self.symmetric_group_block is not None: + raise ValueError( + "symmetric-group kernel rows are not an expanded color matrix" + ) + if self.repeated_block is None: + yield from self.entries + return + block = self.repeated_block + for component_index in range(block.component_count): + for entry in block.entries: + yield ColorContractionEntry( + left_group_id=block.component_group_ids[ + entry.left_group_index * block.component_count + component_index + ], + right_group_id=block.component_group_ids[ + entry.right_group_index * block.component_count + + component_index + ], + weight_re=entry.weight_re, + weight_im=entry.weight_im, + symmetry_factor=entry.symmetry_factor, + exact_weight=entry.exact_weight, + ) + + def to_json_dict(self) -> dict[str, object]: + result: dict[str, object] = { + "kind": "pyamplicol-color-contraction-plan", + "color_accuracy": self.color_accuracy, + "supported": self.supported, + "reason": self.reason, + "group_count": self.group_count, + "includes_color_factor": self.includes_color_factor, + "entry_count": len(self.entries), + "logical_entry_count": self.logical_entry_count, + "storage": "upper-triangular sparse metric over coherent amplitude groups", + "entries": [entry.to_json_dict() for entry in self.entries], + } + if self.repeated_block is not None: + result["repeated_block"] = self.repeated_block.to_json_dict() + if self.symmetric_group_block is not None: + result["symmetric_group_block"] = self.symmetric_group_block.to_json_dict() + if self.destination_by_group is not None: + result["destination_by_group"] = list(self.destination_by_group) + return result + + +@dataclass(frozen=True) +class ColorGroupDescriptor: + group_id: int + helicity_key: tuple[object, ...] + sector_id: int + word: tuple[int, ...] + helicity_weight: float diff --git a/pyamplicol/src/pyamplicol/color/correlator_matrices.py b/pyamplicol/src/pyamplicol/color/correlator_matrices.py new file mode 100644 index 0000000000..762d4e32cd --- /dev/null +++ b/pyamplicol/src/pyamplicol/color/correlator_matrices.py @@ -0,0 +1,421 @@ +# SPDX-License-Identifier: 0BSD +"""Direct exact connected matrices in the generated tree colour basis. + +The adapter adds the endpoint-pairing phase used by AmpliCol's ordinary metric +to otherwise literal colour tensors. Every stored entry is directed and means +````; no upper-triangle +doubling or assumption of Hermiticity is implicit. Runtime contraction therefore +uses ``conjugate(a[left]) * entry * a[right]``. + +Only complete full-colour trace/open-line amplitude plans are accepted, even +for LC/NLC output. Adjoint DDM sectors retain single-trace ordering records for +current construction, but these are not their literal colour tensors. +The approximation acts on the inserted metric at fixed physical amplitudes; +it is not a strict expansion of their hidden colour weights. The Born matrix +uses the inherited ordinary factors. The ordinary matrix's zero pattern +is deliberately not consulted: a connection can turn a zero overlap into a +nonzero one. Planning is direct and quadratic in the selected basis size, with +each connected tensor reused across all requested matrix entries. +""" + +from __future__ import annotations + +from collections.abc import Mapping, Sequence +from dataclasses import dataclass + +from ..processes.ir import CanonicalProcessIR +from .connections import ( + COLOR_CONNECTION_CONVENTION, + ColorConnection, + ColorEmission, + ColorLeg, + ColorTensor, + ConnectedColorTensor, + EmitGluon, + ExactColorCoefficient, + OpenColorString, + SplitGluon, + TensorTerm, + _validate_tensor_legs, + apply_color_connection, + contract_connected_tensor_nc_terms, + evaluate_color_nc_terms, +) +from .contraction_factors import exact_color_contraction_factor +from .plan_types import ColorAccuracy, GenericColorPlan, LCColorSector + +_COLOR_ROLE_REPRESENTATIONS = { + "singlet": 1, + "fundamental": 3, + "antifundamental": -3, + "adjoint": 8, +} + + +def _emission_json(emission: ColorEmission) -> dict[str, object]: + if isinstance(emission, EmitGluon): + return { + "kind": "emit-gluon", + "emitter": emission.emitter_label, + "emitted": emission.emitted_label, + } + return { + "kind": "split-gluon", + "parent": emission.parent_label, + "quark": emission.quark_label, + "antiquark": emission.antiquark_label, + } + + +@dataclass(frozen=True) +class ColorCorrelator: + """A named pair of ordered colour connections on the same Born amplitude. + + Labels refer to canonical external process labels (positive integers). + Fresh negative labels are convenient for auxiliary emitted particles. The + bra and ket must finish with identical labelled colour representations. + """ + + id: str + bra: tuple[ColorEmission, ...] = () + ket: tuple[ColorEmission, ...] = () + + def __post_init__(self) -> None: + if not isinstance(self.id, str) or not self.id.strip(): + raise ValueError("colour correlator ID must be a nonempty string") + object.__setattr__(self, "bra", tuple(self.bra)) + object.__setattr__(self, "ket", tuple(self.ket)) + if len(self.bra) != len(self.ket): + raise ValueError("bra and ket must have the same number of emissions") + if any( + not isinstance(op, (EmitGluon, SplitGluon)) for op in (*self.bra, *self.ket) + ): + raise TypeError( + "colour correlator operations must be typed emission records" + ) + + @property + def order(self) -> int: + return len(self.bra) + + @classmethod + def dipole(cls, id: str, left_leg: int, right_leg: int) -> ColorCorrelator: + """Request ```` using auxiliary gluon label -1.""" + + return cls(id, (EmitGluon(left_leg, -1),), (EmitGluon(right_leg, -1),)) + + def to_json_dict(self) -> dict[str, object]: + return { + "id": self.id, + "bra": [_emission_json(op) for op in self.bra], + "ket": [_emission_json(op) for op in self.ket], + } + + +def process_color_legs(process: CanonicalProcessIR) -> tuple[ColorLeg, ...]: + """Read all-outgoing roles from process IR; do not cross them a second time.""" + + if not isinstance(process, CanonicalProcessIR): + raise TypeError("colour correlators require a CanonicalProcessIR") + legs = [] + for leg in process.legs: + if type(leg.label) is not int or leg.label <= 0: + raise ValueError( + "canonical external colour labels must be positive integers" + ) + try: + representation = _COLOR_ROLE_REPRESENTATIONS[leg.color_role] + except KeyError as exc: + raise ValueError( + "colour correlators require concrete resolved colour roles" + ) from exc + legs.append(ColorLeg(leg.label, representation)) + return ColorConnection(tuple(legs)).initial_legs + + +def _sector_color_tensor( + sector: LCColorSector, legs: tuple[ColorLeg, ...] +) -> TensorTerm: + if not isinstance(sector, LCColorSector): + raise TypeError("colour correlator sectors must be LCColorSector records") + if sector.kind == "open-lines": + strings = tuple( + OpenColorString( + line.fundamental_label, line.adjoint_labels, line.antifundamental_label + ) + for line in sector.open_color_lines + ) + tensor = ColorTensor(strings) + # sign(pairing_left) * sign(pairing_right) is exactly the permutation + # parity (-1)^(line_count - alternating_cycle_count) in the legacy metric. + paired_antiquarks = tuple( + string.antifundamental_label + for string in sorted(strings, key=lambda item: item.fundamental_label) + ) + inversions = sum( + first > second + for index, first in enumerate(paired_antiquarks) + for second in paired_antiquarks[index + 1 :] + ) + coefficient = ExactColorCoefficient(-1 if inversions % 2 else 1) + elif sector.kind == "single-trace": + tensor = ColorTensor(traces=(sector.trace_labels,)) + coefficient = ExactColorCoefficient(1) + elif sector.kind == "singlet": + tensor = ColorTensor() + coefficient = ExactColorCoefficient(1) + else: + raise ValueError(f"unsupported colour sector kind {sector.kind!r}") + _validate_tensor_legs(tensor, legs) + return TensorTerm(coefficient, tensor) + + +def sector_color_tensor( + sector: LCColorSector, process: CanonicalProcessIR +) -> TensorTerm: + """Convert one generated sector to a phase-weighted literal tensor.""" + + return _sector_color_tensor(sector, process_color_legs(process)) + + +@dataclass(frozen=True) +class ColorCorrelatorMatrixEntry: + left_sector_id: int + right_sector_id: int + weight: ExactColorCoefficient + + def to_json_dict(self) -> dict[str, object]: + return { + "left_sector_id": self.left_sector_id, + "right_sector_id": self.right_sector_id, + "weight": { + "real": [ + str(self.weight.real.numerator), + str(self.weight.real.denominator), + ], + "imag": [ + str(self.weight.imag.numerator), + str(self.weight.imag.denominator), + ], + }, + } + + +@dataclass(frozen=True) +class ColorCorrelatorMatrix: + id: str + order: int + sector_ids: tuple[int, ...] + output_legs: tuple[ColorLeg, ...] + entries: tuple[ColorCorrelatorMatrixEntry, ...] + bra: tuple[ColorEmission, ...] = () + ket: tuple[ColorEmission, ...] = () + color_accuracy: ColorAccuracy = "full" + + def to_json_dict(self) -> dict[str, object]: + request = ColorCorrelator(self.id, self.bra, self.ket) + return { + "id": self.id, + "order": self.order, + "convention": COLOR_CONNECTION_CONVENTION, + "color_accuracy": self.color_accuracy, + "storage": "sparse-directed", + "includes_color_factor": True, + "sector_ids": list(self.sector_ids), + "output_legs": [ + {"label": leg.label, "representation": leg.representation} + for leg in self.output_legs + ], + "bra": request.to_json_dict()["bra"], + "ket": request.to_json_dict()["ket"], + "entries": [entry.to_json_dict() for entry in self.entries], + } + + +def _selected_sectors( + plan: GenericColorPlan, sector_ids: Sequence[int] | None +) -> tuple[LCColorSector, ...]: + if not isinstance(plan, GenericColorPlan): + raise TypeError("colour correlator planning requires a GenericColorPlan") + if plan.basis == "adjoint": + raise ValueError("colour correlators do not support the adjoint DDM basis") + if plan.color_accuracy != "full" or plan.process.color_accuracy != "full": + raise ValueError("colour correlators require a full-colour plan") + if plan.truncated or plan.trace_reflections_folded or not plan.sectors: + raise ValueError("colour correlators require a complete unfolded nonempty plan") + by_id = {sector.id: sector for sector in plan.sectors} + if len(by_id) != len(plan.sectors) or any( + type(index) is not int for index in by_id + ): + raise ValueError("colour sector IDs must be unique integers") + requested = tuple(by_id) if sector_ids is None else tuple(sector_ids) + if not requested or any(type(index) is not int for index in requested): + raise ValueError("selected colour sector IDs must be nonempty integers") + if len(set(requested)) != len(requested) or any( + index not in by_id for index in requested + ): + raise ValueError( + "selected colour sector IDs must be unique and belong to the plan" + ) + return tuple(by_id[index] for index in requested) + + +def _select_color_nc_terms( + terms: Mapping[int, ExactColorCoefficient], + *, + color_accuracy: ColorAccuracy, + leading_power: int, + fundamental_output: bool, +) -> dict[int, ExactColorCoefficient]: + """Apply the inherited matrix-level accuracy style to a complete entry. + + The threshold is shared by the whole connected family, never inferred from + this entry. Open-line NLC retains the exact coefficient of an admitted + entry, including its lower powers. Thus its coherence is a retained-order + statement, not an exact finite-Nc identity. + """ + + nonzero = { + power: coefficient for power, coefficient in terms.items() if coefficient + } + if color_accuracy == "full": + return nonzero + if color_accuracy not in ("lc", "nlc"): + raise ValueError("colour correlator accuracy must be lc, nlc or full") + threshold = leading_power - (2 if color_accuracy == "nlc" else 0) + if color_accuracy == "nlc" and fundamental_output: + return nonzero if nonzero and max(nonzero) >= threshold else {} + return { + power: coefficient + for power, coefficient in nonzero.items() + if power >= threshold + } + + +def build_color_correlator_matrices( + color_plan: GenericColorPlan, + correlators: Sequence[ColorCorrelator], + *, + sector_ids: Sequence[int] | None = None, + color_accuracy: ColorAccuracy = "full", +) -> tuple[ColorCorrelatorMatrix, ...]: + """Build exact directed matrices, sharing graph images within this request. + + ``sector_ids`` may select canonical owners from a complete generated plan; + it does not reinterpret a truncated colour plan as a complete amplitude. + An empty request is a no-op and leaves the ordinary colour path untouched. + ``color_accuracy`` selects inherited LC/NLC/full matrix rules; the supplied + plan and physical partial amplitudes always remain complete full colour. + """ + + requests = tuple(correlators) + if not requests: + return () + if color_accuracy not in ("lc", "nlc", "full"): + raise ValueError("colour correlator accuracy must be lc, nlc or full") + if any(not isinstance(request, ColorCorrelator) for request in requests): + raise TypeError("colour correlators must be ColorCorrelator requests") + if len({request.id for request in requests}) != len(requests): + raise ValueError("colour correlator IDs must be unique") + sectors = _selected_sectors(color_plan, sector_ids) + ids = tuple(sector.id for sector in sectors) + legs = process_color_legs(color_plan.process) + terms = tuple(_sector_color_tensor(sector, legs) for sector in sectors) + images: dict[tuple[ColorEmission, ...], tuple[ConnectedColorTensor, ...]] = {} + connections: dict[tuple[ColorEmission, ...], ColorConnection] = {} + for request in requests: + for emissions in (request.bra, request.ket): + if emissions not in connections: + connections[emissions] = ColorConnection(legs, emissions) + if connections[request.bra].output_legs != connections[request.ket].output_legs: + raise ValueError( + f"colour correlator {request.id!r} has incompatible " + "typed bra/ket outputs" + ) + result = [] + for request in requests: + for emissions in (request.bra, request.ket) if request.order else (): + if emissions not in images: + images[emissions] = tuple( + apply_color_connection( + term.tensor, + connections[emissions], + coefficient=term.coefficient, + ) + for term in terms + ) + output_legs = connections[request.bra].output_legs + leading_power = ( + len(color_plan.process.adjoint_labels) + + color_plan.process.color_endpoints.pair_count + + request.order + - sum(isinstance(step, SplitGluon) for step in request.bra) + ) + fundamental_output = any(abs(leg.representation) == 3 for leg in output_legs) + entries = [] + for left_index, left_sector in enumerate(sectors): + for right_index, right_sector in enumerate(sectors): + if not request.order: + weight = ExactColorCoefficient( + exact_color_contraction_factor( + color_plan, + left_sector, + right_sector, + accuracy=color_accuracy, + ) + ) + else: + terms_nc = contract_connected_tensor_nc_terms( + images[request.bra][left_index], + images[request.ket][right_index], + ) + weight = evaluate_color_nc_terms( + _select_color_nc_terms( + terms_nc, + color_accuracy=color_accuracy, + leading_power=leading_power, + fundamental_output=fundamental_output, + ) + ) + if weight: + entries.append( + ColorCorrelatorMatrixEntry( + left_sector.id, right_sector.id, weight + ) + ) + result.append( + ColorCorrelatorMatrix( + request.id, + request.order, + ids, + output_legs, + tuple(entries), + request.bra, + request.ket, + color_accuracy, + ) + ) + return tuple(result) + + +def build_color_correlator_matrix( + color_plan: GenericColorPlan, + correlator: ColorCorrelator, + *, + sector_ids: Sequence[int] | None = None, + color_accuracy: ColorAccuracy = "full", +) -> ColorCorrelatorMatrix: + return build_color_correlator_matrices( + color_plan, (correlator,), sector_ids=sector_ids, color_accuracy=color_accuracy + )[0] + + +__all__ = [ + "ColorCorrelator", + "ColorCorrelatorMatrix", + "ColorCorrelatorMatrixEntry", + "build_color_correlator_matrices", + "build_color_correlator_matrix", + "process_color_legs", + "sector_color_tensor", +] diff --git a/pyamplicol/src/pyamplicol/color/fft_basis_strategy.py b/pyamplicol/src/pyamplicol/color/fft_basis_strategy.py new file mode 100644 index 0000000000..5cf753cec3 --- /dev/null +++ b/pyamplicol/src/pyamplicol/color/fft_basis_strategy.py @@ -0,0 +1,116 @@ +# SPDX-License-Identifier: 0BSD +"""Choose an exact FFT colour representation without inventing amplitude identities. + +DDM half-ladders apply only when the model supplies their algebraic certificate. +Fundamental chains are a different representation, already natural for quark +amplitudes. Products of those chains must not be renamed DDM: their coefficients +are colour-flow amplitudes, not the distinct-flavour Melia/JO primitives. +""" + +from __future__ import annotations + +import math +from dataclasses import dataclass +from typing import Literal + +from ..processes.ir import CanonicalProcessIR + + +@dataclass(frozen=True) +class FFTBasisStrategy: + """Selected metric and its truthful mathematical description. + + ``actual_basis`` selects the existing metric implementation; ``name`` states + the colour tensors used. ``tensor_count`` counts the complete tensor list, + not its rank at a particular Nc or its kinematically nonzero amplitudes. + It excludes the construction-only traversal aliases of open-line products. + """ + + actual_basis: Literal["trace", "adjoint"] + name: Literal[ + "ddm", "fundamental-chain", "fundamental-chain-products", "singlet", "trace" + ] + reason: str + permutation_degree: int + tensor_count: int | None + + +def best_general_fft_basis( + process: CanonicalProcessIR, + *, + ddm_proven: bool, +) -> FFTBasisStrategy: + """Select a certified reduction, otherwise retain the exact existing basis. + + This inexpensive decision uses only canonical colour roles and an already + computed model certificate. It deliberately does not infer tree-level + Kleiss--Kuijf identities from external adjoint particles alone. Colourless + external particles do not change the permutation group or the tensor count. + """ + + gluons = len(process.adjoint_labels) + fundamentals = len(process.fundamental_labels) + antifundamentals = len(process.antifundamental_labels) + if fundamentals != antifundamentals: + return FFTBasisStrategy( + "trace", + "trace", + "No balanced fundamental-chain domain; ordinary colour validation applies.", + gluons, + None, + ) + if fundamentals: + # Allocate a permutation of m adjoints to k ordered open strings, then + # choose the k! antifundamental routings. Different block traversals + # are construction aliases, not additional colour tensors. + tensor_count = ( + math.factorial(fundamentals) + * math.factorial(gluons) + * math.comb(gluons + fundamentals - 1, fundamentals - 1) + ) + return FFTBasisStrategy( + "trace", + "fundamental-chain" if fundamentals == 1 else "fundamental-chain-products", + ( + "One open fundamental chain already fixes both quark endpoints; " + "there is no trace-to-DDM ordering reduction." + if fundamentals == 1 + else "Retain exact products of fundamental chains and all endpoint " + "routings; distinct-flavour DDM generalizations are not identities " + "of arbitrary colour-flow amplitudes." + ), + gluons, + tensor_count, + ) + if not gluons: + return FFTBasisStrategy( + "trace", + "singlet", + "The colour-singlet domain has one trivial colour tensor.", + 0, + 1, + ) + if ddm_proven and gluons >= 2: + return FFTBasisStrategy( + "adjoint", + "ddm", + ( + "The certified two-adjoint metric is the single delta tensor." + if gluons == 2 + else "The model certifies the connected structure-constant " + "tree identities." + ), + gluons - 2, + math.factorial(gluons - 2), + ) + return FFTBasisStrategy( + "trace", + "trace", + ( + "A single adjoint leg leaves no two-anchor DDM domain." + if gluons < 2 + else "No DDM amplitude identity is certified; retain the exact trace basis." + ), + max(0, gluons - 1), + math.factorial(max(0, gluons - 1)), + ) diff --git a/pyamplicol/src/pyamplicol/color/plan.py b/pyamplicol/src/pyamplicol/color/plan.py new file mode 100644 index 0000000000..b06be50662 --- /dev/null +++ b/pyamplicol/src/pyamplicol/color/plan.py @@ -0,0 +1,44 @@ +# SPDX-License-Identifier: 0BSD +"""Compatibility facade for color-sector planning.""" + +from __future__ import annotations + +from .plan_build import build_color_plan +from .plan_replay import ( + build_color_topology_replay_certificate, + build_lc_topology_replay_plan, + color_topology_replay_partitions, + lc_line_pairing_representative_ids, + lc_topology_replay_partitions, + lc_topology_replay_safe_groups, +) +from .plan_types import ( + ColorAccuracy, + ColorSectorKind, + ColorTopologyReplayCertificate, + GenericColorPlan, + LCColorSector, + LCColorSectorReplayPartition, + LCColorSectorTopologyGroup, + LCColorTopologyReplayPlan, + LCOpenColorLine, +) + +__all__ = [ + "ColorAccuracy", + "ColorSectorKind", + "ColorTopologyReplayCertificate", + "GenericColorPlan", + "LCColorSector", + "LCColorSectorReplayPartition", + "LCColorSectorTopologyGroup", + "LCColorTopologyReplayPlan", + "LCOpenColorLine", + "build_color_plan", + "build_color_topology_replay_certificate", + "build_lc_topology_replay_plan", + "color_topology_replay_partitions", + "lc_line_pairing_representative_ids", + "lc_topology_replay_partitions", + "lc_topology_replay_safe_groups", +] diff --git a/pyamplicol/src/pyamplicol/color/plan_build.py b/pyamplicol/src/pyamplicol/color/plan_build.py new file mode 100644 index 0000000000..2b5a28bbe5 --- /dev/null +++ b/pyamplicol/src/pyamplicol/color/plan_build.py @@ -0,0 +1,519 @@ +# SPDX-License-Identifier: 0BSD +"""Construction of color sectors from canonical process data.""" + +from __future__ import annotations + +from collections.abc import Iterable, Sequence +from itertools import permutations + +from ..processes.ir import CanonicalProcessIR, ProcessLegIR +from .plan_types import ( + GenericColorPlan, + LCColorSector, + LCOpenColorLine, +) + + +def build_color_plan( + process: CanonicalProcessIR, + *, + color_accuracy: str = "lc", + max_sectors: int | None = None, + reference_color_order: Sequence[int] | None = None, + fold_trace_reflections: bool = False, + basis: str = "trace", +) -> GenericColorPlan: + if not isinstance(process, CanonicalProcessIR): + raise TypeError("color planning requires a model-resolved CanonicalProcessIR") + process_ir = process + max_sector_count = _normalize_sector_cap(max_sectors) + if color_accuracy != process_ir.color_accuracy: + raise ValueError("color plan accuracy must match the model-resolved process IR") + if basis not in {"trace", "adjoint"}: + raise ValueError(f"unknown colour basis {basis!r}") + if basis == "adjoint": + return _build_adjoint_color_plan( + process_ir, + max_sectors=max_sector_count, + reference_color_order=reference_color_order, + ) + fundamental_legs = _legs_by_labels( + process_ir, + process_ir.fundamental_labels, + ) + antifundamental_legs = _legs_by_labels( + process_ir, + process_ir.antifundamental_labels, + ) + adjoint_labels = process_ir.adjoint_labels + singlet_labels = process_ir.singlet_labels + if len(fundamental_legs) != len(antifundamental_legs): + return GenericColorPlan( + process=process_ir, + color_accuracy=color_accuracy, + sectors=(), + diagnostics=( + "leading-colour open-line plan requires balanced outgoing " + "fundamental and antifundamental counts", + ), + ) + if not fundamental_legs: + return _build_no_fundamental_color_plan( + process_ir, + adjoint_labels=adjoint_labels, + singlet_labels=singlet_labels, + max_sectors=max_sector_count, + reference_color_order=reference_color_order, + fold_trace_reflections=fold_trace_reflections, + ) + + sectors: list[LCColorSector] = list( + _reference_lc_color_sectors(process_ir, reference_color_order) + ) + seen_sector_keys = {_sector_dedup_key(sector) for sector in sectors} + truncated = False + for antifundamental_permutation in permutations(antifundamental_legs): + for adjoint_allocation in _iter_ordered_adjoint_allocations( + adjoint_labels, + len(fundamental_legs), + ): + lines = tuple( + LCOpenColorLine( + fundamental_label=fundamental.label, + antifundamental_label=antifundamental.label, + adjoint_labels=tuple(adjoint_allocation[index]), + ) + for index, (fundamental, antifundamental) in enumerate( + zip( + fundamental_legs, + antifundamental_permutation, + strict=True, + ) + ) + ) + for word_labels in _iter_open_line_color_words( + lines, + include_block_permutations=color_accuracy != "lc", + ): + candidate = LCColorSector( + id=len(sectors), + kind="open-lines", + open_color_lines=lines, + singlet_labels=singlet_labels, + word_labels=word_labels, + ) + key = _sector_dedup_key(candidate) + if key in seen_sector_keys: + continue + seen_sector_keys.add(key) + sectors.append(candidate) + if max_sector_count is not None and len(sectors) >= max_sector_count: + truncated = True + break + if truncated: + break + if truncated: + break + + diagnostics: tuple[str, ...] = () + if truncated: + diagnostics = ( + f"leading-colour sector enumeration reached max_sectors={max_sector_count}", + ) + return GenericColorPlan( + process=process_ir, + color_accuracy=color_accuracy, + sectors=tuple(sectors), + diagnostics=diagnostics, + truncated=truncated, + ) + + +def _normalize_sector_cap(value: int | None) -> int | None: + if value is None: + return None + normalized = int(value) + return None if normalized < 0 else normalized + + +def _build_adjoint_color_plan( + process: CanonicalProcessIR, + *, + max_sectors: int | None, + reference_color_order: Sequence[int] | None, +) -> GenericColorPlan: + """Choose two-anchor primitive words for the DDM half-ladder metric. + + The generation service additionally proves connected f-tree colour for + the selected model interactions and coupling orders. These records retain + their single-trace *ordering* shape, leaving the ordered-current grammar intact. + """ + + from .adjoint_kernel import MAX_ADJOINT_GLUONS + + labels = tuple(sorted(process.adjoint_labels)) + if ( + len(labels) < 2 + or len(labels) + len(process.singlet_labels) != len(process.legs) + or process.fundamental_labels + or process.antifundamental_labels + ): + raise ValueError( + "DDM colour basis requires external adjoints and optional singlets" + ) + if len(labels) > MAX_ADJOINT_GLUONS: + raise ValueError( + f"adjoint colour basis supports at most {MAX_ADJOINT_GLUONS} " + "external gluons" + ) + if process.color_accuracy not in {"nlc", "full"}: + raise ValueError("adjoint colour basis requires NLC or full colour") + first, last = labels[0], labels[-1] + reference = ( + () + if reference_color_order is None + else tuple(int(label) for label in reference_color_order) + ) + if reference: + if len(reference) != len(labels) or set(reference) != set(labels): + raise ValueError("adjoint reference colour order must cover every gluon") + pivot = reference.index(first) + reference = reference[pivot:] + reference[:pivot] + if reference[-1] != last: + raise ValueError( + "adjoint reference colour order must preserve both anchors" + ) + sectors: list[LCColorSector] = [] + if reference and (max_sectors is None or max_sectors > 0): + sectors.append( + LCColorSector( + id=0, + kind="single-trace", + trace_labels=reference, + singlet_labels=process.singlet_labels, + ) + ) + truncated = False + for middle in permutations(labels[1:-1]): + word = (first, *middle, last) + if sectors and word == reference: + continue + if max_sectors is not None and len(sectors) >= max_sectors: + truncated = True + break + sectors.append( + LCColorSector( + id=len(sectors), + kind="single-trace", + trace_labels=word, + singlet_labels=process.singlet_labels, + ) + ) + return GenericColorPlan( + process=process, + color_accuracy=process.color_accuracy, + sectors=tuple(sectors), + basis="adjoint", + truncated=truncated, + diagnostics=( + (f"adjoint sector enumeration reached max_sectors={max_sectors}",) + if truncated + else () + ), + ) + + +def _build_no_fundamental_color_plan( + process: CanonicalProcessIR, + *, + adjoint_labels: tuple[int, ...], + singlet_labels: tuple[int, ...], + max_sectors: int | None, + reference_color_order: Sequence[int] | None = None, + fold_trace_reflections: bool = False, +) -> GenericColorPlan: + if not adjoint_labels: + return GenericColorPlan( + process=process, + color_accuracy=process.color_accuracy, + sectors=( + LCColorSector( + id=0, + kind="singlet", + singlet_labels=singlet_labels, + ), + ), + ) + + first = min(adjoint_labels) + rest = tuple(label for label in adjoint_labels if label != first) + sectors: list[LCColorSector] = list( + _reference_lc_color_sectors(process, reference_color_order) + ) + seen_sector_keys = {_sector_dedup_key(sector) for sector in sectors} + truncated = False + fold_reflections = process.color_accuracy == "lc" and fold_trace_reflections + seen_reversal_classes: set[tuple[int, ...]] = set() + for ordered_rest in permutations(rest): + if fold_reflections: + canonical = min(ordered_rest, tuple(reversed(ordered_rest))) + if canonical in seen_reversal_classes: + continue + seen_reversal_classes.add(canonical) + candidate = LCColorSector( + id=len(sectors), + kind="single-trace", + trace_labels=(first, *ordered_rest), + singlet_labels=singlet_labels, + ) + key = _sector_dedup_key(candidate) + if key in seen_sector_keys: + continue + seen_sector_keys.add(key) + sectors.append(candidate) + if max_sectors is not None and len(sectors) >= max_sectors: + truncated = True + break + diagnostics: tuple[str, ...] = () + if truncated: + diagnostics = ( + f"leading-colour trace enumeration reached max_sectors={max_sectors}", + ) + return GenericColorPlan( + process=process, + color_accuracy=process.color_accuracy, + sectors=tuple(sectors), + diagnostics=diagnostics, + truncated=truncated, + trace_reflections_folded=fold_reflections, + ) + + +def _legs_by_labels( + process: CanonicalProcessIR, + labels: tuple[int, ...], +) -> tuple[ProcessLegIR, ...]: + by_label = {leg.label: leg for leg in process.legs} + return tuple(by_label[label] for label in labels) + + +def _reference_lc_color_sectors( + process: CanonicalProcessIR, + reference_color_order: Sequence[int] | None, +) -> tuple[LCColorSector, ...]: + if reference_color_order is None: + return () + reference = tuple(int(label) for label in reference_color_order) + if not reference: + return () + by_label = {leg.label: leg for leg in process.legs} + coloured_labels = { + *process.fundamental_labels, + *process.antifundamental_labels, + *process.adjoint_labels, + } + coloured_word = tuple(label for label in reference if label in coloured_labels) + if sorted(coloured_word) != sorted(coloured_labels): + return () + if not process.fundamental_labels: + if ( + tuple(label for label in coloured_word if label in process.adjoint_labels) + != coloured_word + ): + return () + return ( + LCColorSector( + id=0, + kind="single-trace", + trace_labels=coloured_word, + singlet_labels=process.singlet_labels, + ), + ) + lines = _reference_open_lines(process, coloured_word, by_label) + if lines is None: + return () + word_labels = tuple(label for line in lines for label in line.coloured_labels) + return ( + LCColorSector( + id=0, + kind="open-lines", + open_color_lines=lines, + singlet_labels=process.singlet_labels, + word_labels=word_labels, + ), + ) + + +def _reference_open_lines( + process: CanonicalProcessIR, + coloured_word: tuple[int, ...], + by_label: dict[int, ProcessLegIR], +) -> tuple[LCOpenColorLine, ...] | None: + fundamental_labels = set(process.fundamental_labels) + antifundamental_labels = set(process.antifundamental_labels) + adjoint_labels = set(process.adjoint_labels) + lines: list[LCOpenColorLine] = [] + offset = 0 + while offset < len(coloured_word): + start = coloured_word[offset] + if start in fundamental_labels: + end_labels = antifundamental_labels + start_is_fundamental = True + elif start in antifundamental_labels: + end_labels = fundamental_labels + start_is_fundamental = False + else: + return None + end = offset + 1 + while end < len(coloured_word) and coloured_word[end] in adjoint_labels: + end += 1 + if end >= len(coloured_word) or coloured_word[end] not in end_labels: + return None + middle = coloured_word[offset + 1 : end] + if any(label not in adjoint_labels for label in middle): + return None + stop = coloured_word[end] + if start_is_fundamental: + fundamental_label = start + antifundamental_label = stop + else: + fundamental_label = stop + antifundamental_label = start + if by_label[fundamental_label].color_role != "fundamental": + return None + if by_label[antifundamental_label].color_role != "antifundamental": + return None + lines.append( + LCOpenColorLine( + fundamental_label=fundamental_label, + antifundamental_label=antifundamental_label, + adjoint_labels=middle, + ) + ) + offset = end + 1 + if len(lines) != len(process.fundamental_labels): + return None + return tuple(lines) + + +def _sector_dedup_key(sector: LCColorSector) -> tuple[object, ...]: + return ( + sector.kind, + tuple( + ( + line.fundamental_label, + line.antifundamental_label, + line.adjoint_labels, + line.singlet_labels, + ) + for line in sector.open_color_lines + ), + sector.trace_labels, + sector.singlet_labels, + sector.word_labels, + ) + + +def _iter_open_line_color_words( + lines: tuple[LCOpenColorLine, ...], + *, + include_block_permutations: bool = False, +) -> tuple[tuple[int, ...], ...]: + """Return explicit colour words for one open-line pairing/allocation.""" + + if not include_block_permutations or len(lines) < 2: + return (tuple(label for line in lines for label in line.coloured_labels),) + words: list[tuple[int, ...]] = [] + seen: set[tuple[int, ...]] = set() + for line_permutation in permutations(lines): + word = tuple( + label for line in line_permutation for label in line.coloured_labels + ) + if word in seen: + continue + seen.add(word) + words.append(word) + return tuple(words) + + +def _ordered_open_line_blocks( + word: tuple[int, ...], + lines: tuple[LCOpenColorLine, ...], +) -> tuple[tuple[int, ...], ...] | None: + remaining = list(lines) + blocks: list[tuple[int, ...]] = [] + offset = 0 + while offset < len(word): + matched_index = None + for index, line in enumerate(remaining): + block = line.coloured_labels + if word[offset : offset + len(block)] == block: + matched_index = index + blocks.append(block) + offset += len(block) + break + if matched_index is None: + return None + remaining.pop(matched_index) + if remaining: + return None + return tuple(blocks) + + +def _iter_ordered_adjoint_allocations( + adjoint_labels: tuple[int, ...], + line_count: int, +) -> Iterable[tuple[tuple[int, ...], ...]]: + if line_count <= 0: + yield () + return + for ordered_adjoint_labels in permutations(adjoint_labels): + yield from _iter_split_ordered_sequence( + tuple(ordered_adjoint_labels), + line_count, + ) + + +def _iter_ordered_label_allocations( + labels: tuple[int, ...], + line_count: int, +) -> Iterable[tuple[tuple[int, ...], ...]]: + if line_count <= 0: + yield () + return + if not labels: + yield tuple(() for _ in range(line_count)) + return + buckets: list[list[int]] = [[] for _ in range(line_count)] + yield from _assign_unordered_labels_to_buckets(labels, buckets, 0) + + +def _assign_unordered_labels_to_buckets( + labels: tuple[int, ...], + buckets: list[list[int]], + index: int, +) -> Iterable[tuple[tuple[int, ...], ...]]: + if index == len(labels): + yield tuple(tuple(bucket) for bucket in buckets) + return + label = labels[index] + for bucket in buckets: + bucket.append(label) + yield from _assign_unordered_labels_to_buckets(labels, buckets, index + 1) + bucket.pop() + + +def _iter_split_ordered_sequence( + sequence: tuple[int, ...], + bin_count: int, +) -> Iterable[tuple[tuple[int, ...], ...]]: + if bin_count == 1: + yield (sequence,) + return + for split_index in range(len(sequence) + 1): + head = sequence[:split_index] + for tail in _iter_split_ordered_sequence( + sequence[split_index:], + bin_count - 1, + ): + yield (head, *tail) diff --git a/pyamplicol/src/pyamplicol/color/plan_replay.py b/pyamplicol/src/pyamplicol/color/plan_replay.py new file mode 100644 index 0000000000..ed359468bf --- /dev/null +++ b/pyamplicol/src/pyamplicol/color/plan_replay.py @@ -0,0 +1,819 @@ +# SPDX-License-Identifier: 0BSD +"""Topology grouping and replay safety for color sectors.""" + +from __future__ import annotations + +import hashlib +import json +from collections.abc import Mapping +from dataclasses import replace +from typing import TYPE_CHECKING + +from ..processes.ir import CanonicalProcessIR +from .plan_build import _ordered_open_line_blocks +from .plan_types import ( + ColorTopologyReplayCertificate, + GenericColorPlan, + LCColorSector, + LCColorSectorReplayPartition, + LCColorSectorTopologyGroup, + LCColorTopologyReplayPlan, +) + +if TYPE_CHECKING: + from ..models.base import Model + +_LC_REPLAY_PROOF_ALGORITHM = "canonical-model-contract-label-equivariance-v1" +_COLOR_REPLAY_PROOF_ALGORITHM = ( + "canonical-model-contract-color-label-equivariance-v2" +) + + +def _sector_topology_groups( + process: CanonicalProcessIR, + sectors: tuple[LCColorSector, ...], +) -> tuple[LCColorSectorTopologyGroup, ...]: + by_signature: dict[tuple[object, ...], list[LCColorSector]] = {} + for sector in sectors: + by_signature.setdefault( + _sector_topology_signature(process, sector), + [], + ).append(sector) + + groups: list[LCColorSectorTopologyGroup] = [] + for signature, sector_group in by_signature.items(): + representative = sector_group[0] + representative_labels = _sector_topology_labels(representative) + permutations: list[tuple[tuple[int, int], ...]] = [] + for sector in sector_group: + sector_labels = _sector_topology_labels(sector) + if len(sector_labels) != len(representative_labels): + raise ValueError("isomorphic colour sectors have mismatched labels") + permutations.append( + tuple(zip(representative_labels, sector_labels, strict=True)) + ) + groups.append( + LCColorSectorTopologyGroup( + signature=signature, + representative_sector_id=representative.id, + sector_ids=tuple(sector.id for sector in sector_group), + label_permutations=tuple(permutations), + ) + ) + return tuple(groups) + + +def _sector_topology_signature( + process: CanonicalProcessIR, + sector: LCColorSector, +) -> tuple[object, ...]: + pdg_by_label = { + leg.label: leg.outgoing_pdg + for leg in process.legs + if leg.outgoing_pdg is not None + } + if sector.kind == "open-lines": + line_by_coloured = { + line.coloured_labels: line for line in sector.open_color_lines + } + ordered_blocks = ( + _ordered_open_line_blocks(sector.word_labels, sector.open_color_lines) + if sector.word_labels + else None + ) + ordered_lines = ( + tuple(line_by_coloured[block] for block in ordered_blocks) + if ordered_blocks is not None + else sector.open_color_lines + ) + return ( + sector.kind, + tuple( + ( + pdg_by_label[line.fundamental_label], + tuple(pdg_by_label[label] for label in line.adjoint_labels), + pdg_by_label[line.antifundamental_label], + tuple(pdg_by_label[label] for label in line.singlet_labels), + ) + for line in ordered_lines + ), + ) + if sector.kind == "single-trace": + return ( + sector.kind, + tuple(pdg_by_label[label] for label in sector.trace_labels), + tuple(pdg_by_label[label] for label in sector.singlet_labels), + ) + return ( + sector.kind, + tuple(pdg_by_label[label] for label in sector.singlet_labels), + ) + + +def _sector_topology_labels(sector: LCColorSector) -> tuple[int, ...]: + if sector.kind == "open-lines": + line_by_coloured = { + line.coloured_labels: line for line in sector.open_color_lines + } + ordered_blocks = ( + _ordered_open_line_blocks(sector.word_labels, sector.open_color_lines) + if sector.word_labels + else None + ) + ordered_lines = ( + tuple(line_by_coloured[block] for block in ordered_blocks) + if ordered_blocks is not None + else sector.open_color_lines + ) + return tuple(label for line in ordered_lines for label in line.line_labels) + if sector.kind == "single-trace": + return (*sector.trace_labels, *sector.singlet_labels) + return sector.singlet_labels + + +def _jsonable_signature(signature: tuple[object, ...]) -> list[object]: + def convert(value: object) -> object: + if isinstance(value, tuple): + return [convert(item) for item in value] + return value + + return [convert(item) for item in signature] + + +def lc_topology_replay_safe_groups( + color_plan: GenericColorPlan, +) -> tuple[LCColorSectorTopologyGroup, ...]: + """Return LC topology groups safe for physical-input replay. + + Runtime replay currently evaluates one representative sector with physical + external momenta in the user's process ordering. Therefore every replay + permutation must preserve the initial-state label set. Pure single-trace + sectors are also kept out of this fast path for now because their trace + symmetries need dedicated colour-convention validation. + """ + + if color_plan.color_accuracy != "lc": + return () + return tuple( + group + for group in color_plan.topology_groups + if _lc_topology_group_replay_safe(color_plan, group) + ) + + +def color_topology_replay_partitions( + color_plan: GenericColorPlan, +) -> tuple[LCColorSectorReplayPartition, ...]: + """Partition topology groups into exact replay-safe representatives. + + The selected-sector ``lc_topology_replay_safe_groups`` helper only accepts + groups whose full topology orbit preserves the initial-state label set. + Complete artifacts can be more general: they split one topology group into + several initial-label-safe blocks and materialize one representative + sidecar per block. The partition is purely structural and is valid for + every requested colour accuracy; mode-specific proof and runtime code must + still authenticate the color reduction before executing it. + + In particular, a full-color seven-gluon plan has 720 physical trace + sectors but exactly six 120-sector partitions. Those are the same six + crossing templates generated by the original AmpliCol library. Keeping + this structural primitive color-generic lets recurrence, compiled, and + eager lowering share one fail-closed orbit certificate. + """ + + if color_plan.color_accuracy not in {"lc", "nlc", "full"}: + return () + initial_labels = {leg.label for leg in color_plan.process.initial_legs} + if not initial_labels: + return () + partitions: list[LCColorSectorReplayPartition] = [] + for group in color_plan.topology_groups: + sectors = tuple( + sector + for sector_id in group.sector_ids + if (sector := color_plan.sector(sector_id)) is not None + ) + if len(sectors) != len(group.sector_ids): + continue + if any(sector.kind not in {"open-lines", "single-trace"} for sector in sectors): + continue + base_maps = { + int(sector_id): { + int(representative_label): int(sector_label) + for representative_label, sector_label in permutation + } + for sector_id, permutation in zip( + group.sector_ids, + group.label_permutations, + strict=True, + ) + } + sector_ids_by_initial_preimage: dict[tuple[int, ...], list[int]] = {} + complete_initial_maps = True + for sector_id in group.sector_ids: + sector_map = base_maps[int(sector_id)] + inverse_sector_map = { + sector_label: representative_label + for representative_label, sector_label in sector_map.items() + } + if not initial_labels.issubset(inverse_sector_map): + complete_initial_maps = False + break + initial_preimage = tuple( + sorted(inverse_sector_map[label] for label in initial_labels) + ) + sector_ids_by_initial_preimage.setdefault(initial_preimage, []).append( + int(sector_id) + ) + if not complete_initial_maps: + continue + partition_sector_ids = sorted( + sector_ids_by_initial_preimage.values(), + key=lambda sector_ids: min(sector_ids), + ) + for grouped_sector_ids in partition_sector_ids: + representative = min(grouped_sector_ids) + representative_map = base_maps[representative] + inverse_representative_map = { + sector_label: representative_label + for representative_label, sector_label in representative_map.items() + } + active_sector_ids: list[int] = [] + relative_permutations: list[tuple[tuple[int, int], ...]] = [] + replay_weights: list[float] = [] + grouped_sector_id_set = set(grouped_sector_ids) + for sector_id in group.sector_ids: + if int(sector_id) not in grouped_sector_id_set: + continue + sector = color_plan.sector(int(sector_id)) + if sector is None: + continue + sector_map = base_maps[int(sector_id)] + relative_map = { + representative_label: sector_map[ + inverse_representative_map[representative_label] + ] + for representative_label in sorted(representative_map.values()) + } + if {relative_map[label] for label in initial_labels} != initial_labels: + raise RuntimeError( + "internal LC replay partitioning error: sectors grouped " + "by initial-label preimage do not preserve the initial set" + ) + active_sector_ids.append(int(sector_id)) + relative_permutations.append(tuple(sorted(relative_map.items()))) + replay_weights.append( + _lc_topology_replay_sector_weight(color_plan, sector) + ) + if not active_sector_ids: + raise RuntimeError( + "internal LC replay partitioning error: representative sector " + f"{representative} did not produce a non-empty replay block" + ) + partitions.append( + LCColorSectorReplayPartition( + representative_sector_id=representative, + active_sector_ids=tuple(active_sector_ids), + label_permutations=tuple(relative_permutations), + replay_weights=tuple(replay_weights), + ) + ) + return tuple(partitions) + + +def lc_topology_replay_partitions( + color_plan: GenericColorPlan, +) -> tuple[LCColorSectorReplayPartition, ...]: + """Return color-generic partitions only for the existing LC runtime.""" + + if color_plan.color_accuracy != "lc": + return () + return color_topology_replay_partitions(color_plan) + + +def build_color_topology_replay_certificate( + color_plan: GenericColorPlan, + model: Model, +) -> ColorTopologyReplayCertificate | None: + """Prove replay orbits without selecting a runtime generation mode. + + The proof binds the requested color accuracy in addition to the canonical + model, source, sector, and relabeling contracts. A consumer may only use + these representatives after separately proving that its color reduction + accepts replayed physical-sector amplitudes. + """ + + if ( + color_plan.color_accuracy not in {"lc", "nlc", "full"} + or color_plan.truncated + or len(color_plan.sectors) < 2 + ): + return None + physical_sector_ids = tuple(sorted(int(sector.id) for sector in color_plan.sectors)) + try: + model_contract_digest = _canonical_contract_digest( + _canonical_model_replay_contract(model) + ) + except Exception: + return None + residual: set[int] = set(physical_sector_ids) + proven: list[LCColorSectorReplayPartition] = [] + diagnostics: list[str] = [] + for candidate in color_topology_replay_partitions(color_plan): + if len(candidate.active_sector_ids) < 2: + continue + try: + proof = _prove_topology_replay_partition( + color_plan, + candidate, + model, + model_contract_digest=model_contract_digest, + proof_algorithm=_COLOR_REPLAY_PROOF_ALGORITHM, + bind_color_accuracy=True, + ) + except Exception as exc: + proof = None + diagnostics.append( + "color replay partition " + f"{candidate.representative_sector_id} proof failed closed: {exc}" + ) + if proof is None: + diagnostics.append( + "color replay partition " + f"{candidate.representative_sector_id} remains materialized" + ) + continue + proof_digest, replay_signs = proof + partition = replace( + candidate, + materialized_sector_id=candidate.representative_sector_id, + replay_signs=replay_signs, + proof_algorithm=_COLOR_REPLAY_PROOF_ALGORITHM, + proof_digest=proof_digest, + ) + proven.append(partition) + residual.difference_update(partition.active_sector_ids) + if not proven: + return None + return ColorTopologyReplayCertificate( + color_accuracy=color_plan.color_accuracy, + physical_sector_ids=physical_sector_ids, + partitions=tuple(proven), + residual_sector_ids=tuple(sorted(residual)), + diagnostics=tuple(diagnostics), + ) + + +def build_lc_topology_replay_plan( + color_plan: GenericColorPlan, + model: Model, +) -> LCColorTopologyReplayPlan | None: + """Build independently proof-gated replay classes for a complete LC plan. + + Candidate topology partitions are cheap structural orbits. This second + pass certifies each orbit against canonical source/kernel contracts and + exact relabelled colour structures. A failed orbit becomes residual + materialization without disabling any independently proven orbit. + """ + + if ( + color_plan.color_accuracy != "lc" + or color_plan.truncated + or len(color_plan.sectors) < 2 + ): + return None + physical_sector_ids = tuple(sorted(int(sector.id) for sector in color_plan.sectors)) + try: + model_contract_digest = _canonical_contract_digest( + _canonical_model_replay_contract(model) + ) + except Exception: + return None + residual: set[int] = set(physical_sector_ids) + proven: list[LCColorSectorReplayPartition] = [] + diagnostics: list[str] = [] + for candidate in lc_topology_replay_partitions(color_plan): + if len(candidate.active_sector_ids) < 2: + continue + try: + proof = _prove_lc_topology_replay_partition( + color_plan, + candidate, + model, + model_contract_digest=model_contract_digest, + ) + except Exception as exc: + proof = None + diagnostics.append( + "LC replay partition " + f"{candidate.representative_sector_id} proof failed closed: {exc}" + ) + if proof is None: + diagnostics.append( + "LC replay partition " + f"{candidate.representative_sector_id} remains materialized" + ) + continue + proof_digest, replay_signs = proof + partition = replace( + candidate, + materialized_sector_id=candidate.representative_sector_id, + replay_signs=replay_signs, + proof_algorithm=_LC_REPLAY_PROOF_ALGORITHM, + proof_digest=proof_digest, + ) + proven.append(partition) + residual.difference_update(partition.active_sector_ids) + if not proven: + return None + return LCColorTopologyReplayPlan( + physical_sector_ids=physical_sector_ids, + partitions=tuple(proven), + residual_sector_ids=tuple(sorted(residual)), + diagnostics=tuple(diagnostics), + ) + + +def _prove_lc_topology_replay_partition( + color_plan: GenericColorPlan, + partition: LCColorSectorReplayPartition, + model: Model, + *, + model_contract_digest: str, +) -> tuple[str, tuple[int, ...]] | None: + """Preserve the accepted LC proof and digest contract.""" + + return _prove_topology_replay_partition( + color_plan, + partition, + model, + model_contract_digest=model_contract_digest, + proof_algorithm=_LC_REPLAY_PROOF_ALGORITHM, + bind_color_accuracy=False, + ) + + +def _prove_topology_replay_partition( + color_plan: GenericColorPlan, + partition: LCColorSectorReplayPartition, + model: Model, + *, + model_contract_digest: str, + proof_algorithm: str, + bind_color_accuracy: bool, +) -> tuple[str, tuple[int, ...]] | None: + """Certify label equivariance for one local replay partition. + + External UFO tensors have already been canonized by the model compiler; + their exact oriented-kernel expressions and proof digests are included in + the contract below. Built-in models contribute the same model-generic + source/lowering/evaluation contracts. No process name or SM particle list + participates in this proof. + """ + + representative = color_plan.sector(partition.representative_sector_id) + if representative is None: + return None + labels = tuple(sorted(int(leg.label) for leg in color_plan.process.legs)) + initial_labels = frozenset( + int(leg.label) for leg in color_plan.process.initial_legs + ) + leg_by_label = {int(leg.label): leg for leg in color_plan.process.legs} + source_contracts = { + label: _canonical_source_contract(model, leg_by_label[label]) + for label in labels + } + replay_signs: list[int] = [] + mapping_contracts: list[dict[str, object]] = [] + for sector_id, permutation, weight in zip( + partition.active_sector_ids, + partition.label_permutations, + partition.weights, + strict=True, + ): + sector = color_plan.sector(sector_id) + if sector is None: + return None + explicit_mapping = {int(left): int(right) for left, right in permutation} + if set(explicit_mapping) != set(explicit_mapping.values()): + return None + if not set(explicit_mapping).issubset(labels): + return None + mapping = { + label: explicit_mapping.get(label, label) + for label in labels + } + if set(mapping.values()) != set(labels): + return None + if {mapping[label] for label in initial_labels} != set(initial_labels): + return None + if _remapped_sector_contract(representative, mapping) != _sector_contract( + sector + ): + return None + for source_label, target_label in mapping.items(): + if source_contracts[source_label] != source_contracts[target_label]: + return None + sign = _external_fermion_permutation_sign( + labels, + mapping, + source_contracts, + ) + replay_signs.append(sign) + mapping_contracts.append( + { + "sector_id": int(sector_id), + "weight": float(weight), + "sign": sign, + "label_permutation": [ + list(item) for item in sorted(explicit_mapping.items()) + ], + "sector_contract": _sector_contract(sector), + } + ) + proof_payload = { + "algorithm": proof_algorithm, + "process": { + "initial_labels": sorted(initial_labels), + "external_source_contracts": [ + [label, source_contracts[label]] for label in labels + ], + }, + "representative_sector_id": partition.representative_sector_id, + "representative_sector_contract": _sector_contract(representative), + "mappings": mapping_contracts, + "model_contract_digest": model_contract_digest, + } + if bind_color_accuracy: + proof_payload["color_accuracy"] = color_plan.color_accuracy + return _canonical_contract_digest(proof_payload), tuple(replay_signs) + + +def _canonical_contract_digest(payload: Mapping[str, object]) -> str: + canonical = json.dumps( + payload, + allow_nan=False, + ensure_ascii=True, + separators=(",", ":"), + sort_keys=True, + ).encode("ascii") + return hashlib.sha256(canonical).hexdigest() + + +def _canonical_source_contract(model: Model, leg: object) -> dict[str, object]: + outgoing_pdg = getattr(leg, "outgoing_pdg", None) + if outgoing_pdg is None: + raise ValueError("LC replay source has no outgoing particle identity") + source = model._source_ir(int(outgoing_pdg)) + return { + "role": "initial" if bool(getattr(leg, "is_initial", False)) else "final", + "source": source.to_json_dict(), + } + + +def _canonical_model_replay_contract(model: Model) -> dict[str, object]: + oriented_kernels: list[object] = [] + compiled = getattr(model, "compiled", None) + ir = getattr(compiled, "ir", None) + for kernel in getattr(ir, "oriented_kernels", ()): + serializer = getattr(kernel, "to_dict", None) + if callable(serializer): + oriented_kernels.append(serializer()) + vertices = [] + seen_kinds: set[int] = set() + for vertex in sorted( + model.vertices, + key=lambda item: (int(item.kind), tuple(item.particles), tuple(item.coupling)), + ): + kind = int(vertex.kind) + if kind in seen_kinds: + continue + seen_kinds.add(kind) + vertices.append( + { + "kind": kind, + "lowering": model.vertex_lowering_rule(kind).to_json_dict(), + "evaluation_equivalence": ( + model.vertex_evaluation_equivalence(kind).to_json_dict() + ), + } + ) + certificates = getattr(model, "_symmetry_certificates", None) + certificate_contract = None + if certificates is not None: + certificate_contract = { + name: _json_contract_value(getattr(certificates, name)) + for name in getattr(certificates, "__dataclass_fields__", {}) + } + return { + "model_type": f"{type(model).__module__}.{type(model).__qualname__}", + "vertices": vertices, + "oriented_kernels": oriented_kernels, + "external_symmetry_certificates": certificate_contract, + } + + +def _json_contract_value(value: object) -> object: + if isinstance(value, Mapping): + return { + str(key): _json_contract_value(item) + for key, item in sorted(value.items(), key=lambda item: str(item[0])) + } + if isinstance(value, frozenset | set): + return sorted(_json_contract_value(item) for item in value) + if isinstance(value, tuple | list): + return [_json_contract_value(item) for item in value] + return value + + +def _sector_contract(sector: LCColorSector) -> dict[str, object]: + return { + "kind": sector.kind, + "open_color_lines": sorted( + ( + int(line.fundamental_label), + int(line.antifundamental_label), + tuple(int(label) for label in line.adjoint_labels), + tuple(int(label) for label in line.singlet_labels), + ) + for line in sector.open_color_lines + ), + "trace_labels": tuple(int(label) for label in sector.trace_labels), + "singlet_labels": tuple(int(label) for label in sector.singlet_labels), + "word_labels": tuple(int(label) for label in sector.word_labels), + } + + +def _remapped_sector_contract( + sector: LCColorSector, + mapping: Mapping[int, int], +) -> dict[str, object]: + def mapped(labels: tuple[int, ...]) -> tuple[int, ...]: + return tuple(mapping[int(label)] for label in labels) + + return { + "kind": sector.kind, + "open_color_lines": sorted( + ( + mapping[int(line.fundamental_label)], + mapping[int(line.antifundamental_label)], + mapped(line.adjoint_labels), + mapped(line.singlet_labels), + ) + for line in sector.open_color_lines + ), + "trace_labels": mapped(sector.trace_labels), + "singlet_labels": mapped(sector.singlet_labels), + "word_labels": mapped(sector.word_labels), + } + + +def _external_fermion_permutation_sign( + labels: tuple[int, ...], + mapping: Mapping[int, int], + source_contracts: Mapping[int, Mapping[str, object]], +) -> int: + classes: dict[tuple[str, str], list[int]] = {} + for label in labels: + contract = source_contracts[label] + source = contract["source"] + if not isinstance(source, Mapping) or source.get("statistics") != "fermion": + continue + identity = source.get("identity") + if not isinstance(identity, Mapping): + raise ValueError("fermion source identity contract is missing") + classes.setdefault( + (str(contract["role"]), str(identity["canonical_id"])), + [], + ).append(label) + sign = 1 + for class_labels in classes.values(): + ordered = sorted(class_labels) + positions = {label: index for index, label in enumerate(ordered)} + permutation = [positions[mapping[label]] for label in ordered] + inversions = sum( + permutation[left] > permutation[right] + for left in range(len(permutation)) + for right in range(left + 1, len(permutation)) + ) + if inversions % 2: + sign = -sign + return sign + + +def _lc_topology_replay_sector_weight( + color_plan: GenericColorPlan, + sector: LCColorSector, +) -> float: + """Return the LC multiplicity represented by a materialized sector. + + A model-proven pure single-trace LC plan may fold trace reflections during + colour-sector enumeration. Replaying such a materialized representative + with weight two preserves the full ordering sum. + """ + + if ( + color_plan.color_accuracy == "lc" + and color_plan.trace_reflections_folded + and sector.kind == "single-trace" + and len(sector.trace_labels) > 2 + ): + return 2.0 + return 1.0 + + +def lc_line_pairing_representative_ids( + color_plan: GenericColorPlan, +) -> tuple[int, ...]: + """Return one sector per LC open-line pairing/allocation. + + This is a generic colour-flow pruning helper. It keeps distinct + fundamental-antifundamental pairings and distinct adjoint/singlet + attachments, but drops + duplicate sectors that only permute complete open-line blocks in the colour + word. It deliberately does not group sectors with different flavour + pairings or different particles assigned to a line. + """ + + if color_plan.color_accuracy != "lc": + return () + representatives: list[int] = [] + seen: set[tuple[object, ...]] = set() + pdg_by_label = { + leg.label: leg.outgoing_pdg + for leg in color_plan.process.legs + if leg.outgoing_pdg is not None + } + for sector in color_plan.sectors: + signature = _sector_line_pairing_signature(pdg_by_label, sector) + if signature in seen: + continue + seen.add(signature) + representatives.append(int(sector.id)) + return tuple(representatives) + + +def _sector_line_pairing_signature( + pdg_by_label: dict[int, int | None], + sector: LCColorSector, +) -> tuple[object, ...]: + if sector.kind == "open-lines": + return ( + sector.kind, + tuple( + sorted( + ( + line.fundamental_label, + pdg_by_label.get(line.fundamental_label), + line.antifundamental_label, + pdg_by_label.get(line.antifundamental_label), + tuple( + (label, pdg_by_label.get(label)) + for label in line.adjoint_labels + ), + tuple( + (label, pdg_by_label.get(label)) + for label in line.singlet_labels + ), + ) + for line in sector.open_color_lines + ) + ), + ) + if sector.kind == "single-trace": + canonical_trace = min(sector.trace_labels, tuple(reversed(sector.trace_labels))) + return ( + sector.kind, + tuple((label, pdg_by_label.get(label)) for label in canonical_trace), + tuple((label, pdg_by_label.get(label)) for label in sector.singlet_labels), + ) + return ( + sector.kind, + tuple((label, pdg_by_label.get(label)) for label in sector.singlet_labels), + ) + + +def _lc_topology_group_replay_safe( + color_plan: GenericColorPlan, + group: LCColorSectorTopologyGroup, +) -> bool: + sectors = tuple( + sector + for sector_id in group.sector_ids + if (sector := color_plan.sector(sector_id)) is not None + ) + if len(sectors) != len(group.sector_ids): + return False + if any(sector.kind != "open-lines" for sector in sectors): + return False + initial_labels = {leg.label for leg in color_plan.process.initial_legs} + if not initial_labels: + return False + for permutation in group.label_permutations: + mapping = { + int(representative_label): int(sector_label) + for representative_label, sector_label in permutation + } + if {mapping.get(label) for label in initial_labels} != initial_labels: + return False + return True diff --git a/pyamplicol/src/pyamplicol/color/plan_types.py b/pyamplicol/src/pyamplicol/color/plan_types.py new file mode 100644 index 0000000000..3abff0008f --- /dev/null +++ b/pyamplicol/src/pyamplicol/color/plan_types.py @@ -0,0 +1,663 @@ +# SPDX-License-Identifier: 0BSD +"""Frozen color-sector and color-plan records.""" + +from __future__ import annotations + +import math +from collections.abc import Iterable +from dataclasses import dataclass +from functools import cached_property +from itertools import permutations +from typing import Literal + +from ..processes.ir import CanonicalProcessIR + +ColorAccuracy = Literal["lc", "nlc", "full"] +ColorSectorKind = Literal["singlet", "open-lines", "single-trace"] + + +def _canonical_open_string_product_key( + open_strings: Iterable[tuple[int, Iterable[int], int, Iterable[int]]], +) -> tuple[tuple[int, tuple[int, ...], int, tuple[int, ...]], ...]: + """Return the unordered identity of a product of complete open strings.""" + + return tuple( + sorted( + ( + int(fundamental), + tuple(int(label) for label in adjoints), + int(antifundamental), + tuple(int(label) for label in singlets), + ) + for fundamental, adjoints, antifundamental, singlets in open_strings + ) + ) + + +@dataclass(frozen=True) +class LCOpenColorLine: + """One leading-colour open line in all-outgoing conventions.""" + + fundamental_label: int + antifundamental_label: int + adjoint_labels: tuple[int, ...] + singlet_labels: tuple[int, ...] = () + + @property + def coloured_labels(self) -> tuple[int, ...]: + return ( + self.fundamental_label, + *self.adjoint_labels, + self.antifundamental_label, + ) + + @property + def line_labels(self) -> tuple[int, ...]: + return (*self.coloured_labels, *self.singlet_labels) + + def to_json_dict(self) -> dict[str, object]: + return { + "fundamental_label": self.fundamental_label, + "antifundamental_label": self.antifundamental_label, + "adjoint_labels": list(self.adjoint_labels), + "singlet_labels": list(self.singlet_labels), + "line_labels": list(self.line_labels), + } + + +@dataclass(frozen=True) +class LCColorSector: + """One colour-flow sector produced during generation warmup.""" + + id: int + kind: ColorSectorKind + open_color_lines: tuple[LCOpenColorLine, ...] = () + trace_labels: tuple[int, ...] = () + singlet_labels: tuple[int, ...] = () + word_labels: tuple[int, ...] = () + + @property + def coloured_label_groups(self) -> tuple[tuple[int, ...], ...]: + if self.kind == "open-lines": + return tuple(line.coloured_labels for line in self.open_color_lines) + if self.kind == "single-trace": + return (self.trace_labels,) + return () + + @property + def line_label_groups(self) -> tuple[tuple[int, ...], ...]: + if self.kind == "open-lines": + return tuple(line.coloured_labels for line in self.open_color_lines) + if self.kind == "single-trace": + return ((*self.trace_labels, *self.singlet_labels),) + if self.kind == "singlet": + return (self.singlet_labels,) + return () + + @cached_property + def color_words(self) -> tuple[tuple[int, ...], ...]: + if self.word_labels: + return (self.word_labels,) + if self.kind == "open-lines": + return ( + tuple( + label + for line in self.open_color_lines + for label in line.coloured_labels + ), + ) + if self.kind == "single-trace": + return (self.trace_labels,) + if self.kind == "singlet": + return ((),) + return () + + @cached_property + def admissible_traversal_words(self) -> tuple[tuple[int, ...], ...]: + """Colour words accepted while constructing currents for this sector. + + The sector itself has one physical colour word. During current + construction, however, complete open-line blocks can be traversed + in different intermediate orders. This lets the generic recursion + reproduce AmpliCol's colour-ordered current closures without naming a + process family. + """ + + from .plan_build import _ordered_open_line_blocks + + if self.kind != "open-lines" or len(self.open_color_lines) < 2: + return self.color_words + primary = self.color_words[0] + blocks = _ordered_open_line_blocks(primary, self.open_color_lines) + if blocks is None or len(blocks) < 2: + return self.color_words + words: list[tuple[int, ...]] = [primary] + seen = {primary} + for block_permutation in permutations(blocks): + word = tuple(label for block in block_permutation for label in block) + if word in seen: + continue + seen.add(word) + words.append(word) + return tuple(words) + + def canonical_closure_traversal_word( + self, + process: CanonicalProcessIR, + ) -> tuple[int, ...]: + """Return one deterministic block-equivalent traversal for closure. + + Intermediate currents may use every admissible complete-open-line + block permutation, but the final amplitude must close on one cyclic + traversal. AmpliCol's representation convention starts with the + complete open-line block containing the minimum canonical coloured + source slot, preserves the public cyclic block order, and closes on the + terminal endpoint of the preceding block. Colour-singlet sources are + skipped when selecting that slot. This uses only source ordering, the + physical colour word, and declared open-line blocks; particle flavour, + mass, process identity, and multiplicity do not enter. + """ + + blocks = self._canonical_closure_blocks(process) + return tuple(label for block in blocks for label in block) + + def _canonical_closure_blocks( + self, + process: CanonicalProcessIR, + ) -> tuple[tuple[int, ...], ...]: + """Reconstruct open-line blocks with the canonical source block first.""" + + primary = self.color_words[0] + if self.kind != "open-lines" or not self.open_color_lines: + return (primary,) + from .plan_build import _ordered_open_line_blocks + + blocks = _ordered_open_line_blocks(primary, self.open_color_lines) + if blocks is None or len(blocks) != len(self.open_color_lines): + raise ValueError( + f"LC sector {self.id} colour word is not a complete open-line " + "block traversal" + ) + known_labels = {int(leg.label) for leg in process.legs} + coloured_labels = tuple(label for block in blocks for label in block) + unknown_labels = sorted(set(coloured_labels) - known_labels) + if unknown_labels: + raise ValueError( + f"LC sector {self.id} closure line references unknown external " + f"labels {unknown_labels}" + ) + if not coloured_labels or not process.legs: + return blocks + coloured_label_set = set(coloured_labels) + distinguished_label = next( + ( + int(leg.label) + for leg in process.legs + if int(leg.label) in coloured_label_set + ), + None, + ) + if distinguished_label is None: + return blocks + positions = tuple( + index for index, block in enumerate(blocks) if distinguished_label in block + ) + if not positions: + return blocks + if len(positions) != 1: + raise ValueError( + f"LC sector {self.id} distinguished source label " + f"{distinguished_label} belongs to {len(positions)} open-line blocks" + ) + first_position = positions[0] + return (*blocks[first_position:], *blocks[:first_position]) + + def canonical_closure_sink_label(self, process: CanonicalProcessIR) -> int | None: + """Return the terminal label of the canonical closure traversal.""" + + word = self.canonical_closure_traversal_word(process) + return None if not word else int(word[-1]) + + def to_json_dict(self) -> dict[str, object]: + return { + "id": self.id, + "kind": self.kind, + "open_color_lines": [line.to_json_dict() for line in self.open_color_lines], + "trace_labels": list(self.trace_labels), + "singlet_labels": list(self.singlet_labels), + "word_labels": list(self.word_labels), + "coloured_label_groups": [ + list(group) for group in self.coloured_label_groups + ], + "line_label_groups": [list(group) for group in self.line_label_groups], + "color_words": [list(word) for word in self.color_words], + "admissible_traversal_words": [ + list(word) for word in self.admissible_traversal_words + ], + } + + +@dataclass(frozen=True) +class LCColorSectorTopologyGroup: + """Colour sectors that can share one compiled current topology. + + The group key is built from model/process data carried by the external + labels, not from a process-family name. Runtime reuse still evaluates each + sector with its own external-label permutation, so this is a generation-time + sharing plan rather than a physics approximation. + """ + + signature: tuple[object, ...] + representative_sector_id: int + sector_ids: tuple[int, ...] + label_permutations: tuple[tuple[tuple[int, int], ...], ...] + + def to_json_dict(self) -> dict[str, object]: + from .plan_replay import _jsonable_signature + + return { + "signature": _jsonable_signature(self.signature), + "representative_sector_id": self.representative_sector_id, + "sector_ids": list(self.sector_ids), + "label_permutations": [ + [[left, right] for left, right in permutation] + for permutation in self.label_permutations + ], + } + + +@dataclass(frozen=True) +class LCColorSectorReplayPartition: + """Initial-label-safe replay block inside one LC topology group.""" + + representative_sector_id: int + active_sector_ids: tuple[int, ...] + label_permutations: tuple[tuple[tuple[int, int], ...], ...] + replay_weights: tuple[float, ...] = () + replay_signs: tuple[int, ...] = () + materialized_sector_id: int | None = None + proof_algorithm: str | None = None + proof_digest: str | None = None + + def __post_init__(self) -> None: + if self.materialized_sector_id is None: + object.__setattr__( + self, + "materialized_sector_id", + int(self.representative_sector_id), + ) + count = len(self.active_sector_ids) + if len(self.label_permutations) != count: + raise ValueError( + "LC replay partition permutations do not match active sectors" + ) + if self.replay_weights and len(self.replay_weights) != count: + raise ValueError("LC replay partition weights do not match active sectors") + if self.replay_signs and len(self.replay_signs) != count: + raise ValueError("LC replay partition signs do not match active sectors") + if any(not math.isfinite(weight) or weight <= 0.0 for weight in self.weights): + raise ValueError("LC replay partition weights must be positive and finite") + if any(sign not in {-1, 1} for sign in self.signs): + raise ValueError("LC replay partition signs must be -1 or 1") + if self.representative_sector_id not in self.active_sector_ids: + raise ValueError("LC replay partition does not contain its representative") + if self.materialized_sector_id != self.representative_sector_id: + raise ValueError( + "LC replay currently materializes the partition representative" + ) + if (self.proof_algorithm is None) != (self.proof_digest is None): + raise ValueError( + "LC replay proof algorithm and digest must be present together" + ) + if self.proof_digest is not None and ( + len(self.proof_digest) != 64 + or any( + character not in "0123456789abcdef" for character in self.proof_digest + ) + ): + raise ValueError("LC replay proof digest must be a lowercase SHA-256") + + @property + def weights(self) -> tuple[float, ...]: + return self.replay_weights or (1.0,) * len(self.active_sector_ids) + + @property + def signs(self) -> tuple[int, ...]: + return self.replay_signs or (1,) * len(self.active_sector_ids) + + def to_json_dict(self) -> dict[str, object]: + return { + "representative_sector_id": self.representative_sector_id, + "materialized_sector_id": self.materialized_sector_id, + "active_sector_ids": list(self.active_sector_ids), + "label_permutations": [ + [[left, right] for left, right in permutation] + for permutation in self.label_permutations + ], + "replay_weights": list(self.weights), + "replay_signs": list(self.signs), + "proof": ( + None + if self.proof_digest is None + else { + "status": "proven", + "algorithm": self.proof_algorithm, + "digest": self.proof_digest, + } + ), + } + + def to_runtime_manifest(self) -> dict[str, object]: + """Return the additive compiled-runtime replay group contract.""" + + return { + "representative_sector_id": self.representative_sector_id, + "materialized_sector_id": self.materialized_sector_id, + "active_sector_ids": list(self.active_sector_ids), + "proof": { + "status": "proven", + "algorithm": self.proof_algorithm, + "digest": self.proof_digest, + }, + "sector_permutations": [ + { + "sector_id": sector_id, + "weight": weight, + "sign": sign, + "factor": [weight * sign, 0.0], + "label_permutation": [ + { + "representative_label": representative_label, + "sector_label": sector_label, + } + for representative_label, sector_label in permutation + ], + } + for sector_id, permutation, weight, sign in zip( + self.active_sector_ids, + self.label_permutations, + self.weights, + self.signs, + strict=True, + ) + ], + } + + +@dataclass(frozen=True) +class ColorTopologyReplayCertificate: + """Color-accuracy-bound proof for external-label topology replay. + + This record is deliberately runtime-neutral. Recurrence, compiled, and + eager lowering may consume the same proven partitions, but each mode must + separately authenticate how representative amplitudes enter its color + reduction. + """ + + color_accuracy: ColorAccuracy + physical_sector_ids: tuple[int, ...] + partitions: tuple[LCColorSectorReplayPartition, ...] + residual_sector_ids: tuple[int, ...] + diagnostics: tuple[str, ...] = () + + def __post_init__(self) -> None: + if self.color_accuracy not in {"lc", "nlc", "full"}: + raise ValueError( + f"unsupported topology replay accuracy {self.color_accuracy!r}" + ) + physical = tuple(sorted(int(value) for value in self.physical_sector_ids)) + residual = tuple(sorted(int(value) for value in self.residual_sector_ids)) + object.__setattr__(self, "physical_sector_ids", physical) + object.__setattr__(self, "residual_sector_ids", residual) + if len(set(physical)) != len(physical): + raise ValueError("topology replay physical sectors contain duplicates") + if len(set(residual)) != len(residual): + raise ValueError("topology replay residual sectors contain duplicates") + if any( + partition.proof_algorithm is None or partition.proof_digest is None + for partition in self.partitions + ): + raise ValueError( + "topology replay certificate contains an unproven partition" + ) + replayed = tuple( + sector_id + for partition in self.partitions + for sector_id in partition.active_sector_ids + ) + if len(set(replayed)) != len(replayed): + raise ValueError("topology replay certificate partitions overlap") + if set(replayed) & set(residual): + raise ValueError("topology replay sectors overlap residual coverage") + if set(replayed) | set(residual) != set(physical): + raise ValueError( + "topology replay certificate does not cover every physical sector" + ) + + @property + def replayed_sector_count(self) -> int: + return sum(len(partition.active_sector_ids) for partition in self.partitions) + + @property + def materialized_sector_ids(self) -> tuple[int, ...]: + return tuple( + sorted( + { + *( + int(partition.materialized_sector_id) + for partition in self.partitions + ), + *self.residual_sector_ids, + } + ) + ) + + @property + def optimized(self) -> bool: + return any( + len(partition.active_sector_ids) > 1 for partition in self.partitions + ) + + def representative_for(self, sector_id: int) -> int: + sector_id = int(sector_id) + for partition in self.partitions: + if sector_id in partition.active_sector_ids: + return int(partition.materialized_sector_id) + if sector_id in self.residual_sector_ids: + return sector_id + raise KeyError(f"topology replay certificate has no sector {sector_id}") + + def to_json_dict(self) -> dict[str, object]: + return { + "schema_version": 1, + "kind": "pyamplicol-color-topology-replay-certificate", + "mode": "external-label-permutation", + "color_accuracy": self.color_accuracy, + "physical_sector_ids": list(self.physical_sector_ids), + "materialized_sector_ids": list(self.materialized_sector_ids), + "residual_sector_ids": list(self.residual_sector_ids), + "replayed_sector_count": self.replayed_sector_count, + "partitions": [partition.to_json_dict() for partition in self.partitions], + "diagnostics": list(self.diagnostics), + } + + def to_runtime_manifest(self) -> dict[str, object]: + """Return the color-generic representative-lane runtime contract.""" + + return { + "enabled": self.optimized, + "mode": "external-label-permutation", + "contract_version": 3, + "color_accuracy": self.color_accuracy, + "physical_sector_count": len(self.physical_sector_ids), + "replayed_sector_count": self.replayed_sector_count, + "materialized_sector_ids": list(self.materialized_sector_ids), + "residual_sector_ids": list(self.residual_sector_ids), + "groups": [ + partition.to_runtime_manifest() for partition in self.partitions + ], + } + + +@dataclass(frozen=True) +class LCColorTopologyReplayPlan: + """Proof-gated replay classes plus independently materialized residuals.""" + + physical_sector_ids: tuple[int, ...] + partitions: tuple[LCColorSectorReplayPartition, ...] + residual_sector_ids: tuple[int, ...] + diagnostics: tuple[str, ...] = () + + def __post_init__(self) -> None: + physical = tuple(sorted(int(value) for value in self.physical_sector_ids)) + residual = tuple(sorted(int(value) for value in self.residual_sector_ids)) + object.__setattr__(self, "physical_sector_ids", physical) + object.__setattr__(self, "residual_sector_ids", residual) + if len(set(physical)) != len(physical): + raise ValueError("LC replay physical sectors contain duplicates") + if len(set(residual)) != len(residual): + raise ValueError("LC replay residual sectors contain duplicates") + if any( + partition.proof_algorithm is None or partition.proof_digest is None + for partition in self.partitions + ): + raise ValueError("LC replay plan contains an unproven partition") + replayed = tuple( + sector_id + for partition in self.partitions + for sector_id in partition.active_sector_ids + ) + if len(set(replayed)) != len(replayed): + raise ValueError("LC replay partitions overlap") + if set(replayed) & set(residual): + raise ValueError("LC replay sectors overlap residual coverage") + if set(replayed) | set(residual) != set(physical): + raise ValueError("LC replay plan does not cover every physical sector") + + @property + def replayed_sector_count(self) -> int: + return sum(len(partition.active_sector_ids) for partition in self.partitions) + + @property + def materialized_sector_ids(self) -> tuple[int, ...]: + return tuple( + sorted( + { + *( + int(partition.materialized_sector_id) + for partition in self.partitions + ), + *self.residual_sector_ids, + } + ) + ) + + @property + def optimized(self) -> bool: + return any( + len(partition.active_sector_ids) > 1 for partition in self.partitions + ) + + def representative_for(self, sector_id: int) -> int: + sector_id = int(sector_id) + for partition in self.partitions: + if sector_id in partition.active_sector_ids: + return int(partition.materialized_sector_id) + if sector_id in self.residual_sector_ids: + return sector_id + raise KeyError(f"LC replay plan has no physical sector {sector_id}") + + def to_json_dict(self) -> dict[str, object]: + return { + "schema_version": 1, + "kind": "pyamplicol-lc-topology-replay-plan", + "mode": "external-label-permutation", + "physical_sector_ids": list(self.physical_sector_ids), + "materialized_sector_ids": list(self.materialized_sector_ids), + "residual_sector_ids": list(self.residual_sector_ids), + "replayed_sector_count": self.replayed_sector_count, + "partitions": [partition.to_json_dict() for partition in self.partitions], + "diagnostics": list(self.diagnostics), + } + + def to_runtime_manifest(self) -> dict[str, object]: + return { + "enabled": self.optimized, + "mode": "external-label-permutation", + "contract_version": 2, + "physical_sector_count": len(self.physical_sector_ids), + "replayed_sector_count": self.replayed_sector_count, + "materialized_sector_ids": list(self.materialized_sector_ids), + "residual_sector_ids": list(self.residual_sector_ids), + "groups": [ + partition.to_runtime_manifest() for partition in self.partitions + ], + } + + +@dataclass(frozen=True) +class GenericColorPlan: + """Colour-flow planning payload shared by future Python/Rust runtimes.""" + + process: CanonicalProcessIR + color_accuracy: str + sectors: tuple[LCColorSector, ...] + diagnostics: tuple[str, ...] = () + truncated: bool = False + trace_reflections_folded: bool = False + # The ordered words select primitive amplitudes in either basis. Only the + # associated colour tensors/metric differ; adjoint means DDM half-ladders. + basis: Literal["trace", "adjoint"] = "trace" + + @property + def sector_count(self) -> int: + return len(self.sectors) + + @property + def ready_for_leading_colour(self) -> bool: + return self.color_accuracy == "lc" and bool(self.sectors) and not self.truncated + + @property + def ready_for_requested_colour(self) -> bool: + return bool(self.sectors) and not self.truncated + + @property + def coloured_labels(self) -> tuple[int, ...]: + return tuple( + sorted( + { + *self.process.fundamental_labels, + *self.process.antifundamental_labels, + *self.process.adjoint_labels, + } + ) + ) + + @cached_property + def _sectors_by_id(self) -> dict[int, LCColorSector]: + return {int(sector.id): sector for sector in self.sectors} + + def sector(self, color_sector: int) -> LCColorSector | None: + return self._sectors_by_id.get(int(color_sector)) + + @cached_property + def topology_groups(self) -> tuple[LCColorSectorTopologyGroup, ...]: + from .plan_replay import _sector_topology_groups + + return _sector_topology_groups(self.process, self.sectors) + + def to_json_dict(self) -> dict[str, object]: + return { + "process": self.process.to_json_dict(), + "color_accuracy": self.color_accuracy, + **({"basis": self.basis} if self.basis != "trace" else {}), + "sector_count": self.sector_count, + "truncated": self.truncated, + "trace_reflections_folded": self.trace_reflections_folded, + "ready_for_leading_colour": self.ready_for_leading_colour, + "ready_for_requested_colour": self.ready_for_requested_colour, + "coloured_labels": list(self.coloured_labels), + "diagnostics": list(self.diagnostics), + "sectors": [sector.to_json_dict() for sector in self.sectors], + "topology_groups": [group.to_json_dict() for group in self.topology_groups], + } diff --git a/pyamplicol/src/pyamplicol/color/symmetric_group.py b/pyamplicol/src/pyamplicol/color/symmetric_group.py new file mode 100644 index 0000000000..49528e5d68 --- /dev/null +++ b/pyamplicol/src/pyamplicol/color/symmetric_group.py @@ -0,0 +1,1194 @@ +# SPDX-License-Identifier: 0BSD +"""Generic symmetric-group color-contraction planning. + +The planner knows only canonical external color roles and color-sector +structures. It discovers complete regular permutation orbits, stores exact +relative color kernels for those orbits, and leaves every uncertified group in +an ordinary direct residual. Model names, particle identities, and process +families never enter the construction. +""" + +from __future__ import annotations + +import math +import struct +from collections.abc import Iterator, Sequence +from dataclasses import dataclass, replace +from fractions import Fraction +from typing import cast, overload + +from .contraction_factors import exact_color_contraction_factor +from .contraction_types import ( + ColorContractionPlan, + ColorContractionTemplateEntry, + ColorGroupDescriptor, + SymmetricGroupColorContractionBlock, +) +from .plan_types import GenericColorPlan, LCColorSector, LCOpenColorLine + +_MAX_SYMMETRIC_GROUP_DEGREE = 10 +_ZERO_SECTOR_OWNER = 0xFFFF_FFFF +_SYMMETRIC_GROUP_ORDERS = tuple( + math.factorial(degree) for degree in range(_MAX_SYMMETRIC_GROUP_DEGREE + 1) +) + + +def _symmetric_group_order(degree: int) -> int: + if ( + not isinstance(degree, int) + or isinstance(degree, bool) + or not 0 <= degree <= _MAX_SYMMETRIC_GROUP_DEGREE + ): + raise ValueError("symmetric-group degree is unsupported") + return _SYMMETRIC_GROUP_ORDERS[degree] + + +def _lexicographic_permutation_rank( + permutation: Sequence[int], + *, + degree: int | None = None, +) -> int: + """Return the checked ``itertools.permutations`` index in ``O(m**2)``.""" + + expected_degree = len(permutation) if degree is None else degree + _symmetric_group_order(expected_degree) + if len(permutation) != expected_degree: + raise ValueError("permutation degree is inconsistent") + available = list(range(expected_degree)) + rank = 0 + for position, value in enumerate(permutation): + if not isinstance(value, int) or isinstance(value, bool): + raise ValueError("permutation contains a non-integer label") + try: + available_index = available.index(value) + except ValueError as exc: + raise ValueError("permutation labels are not a bijection") from exc + rank += ( + available_index * _SYMMETRIC_GROUP_ORDERS[expected_degree - position - 1] + ) + del available[available_index] + return rank + + +def _lexicographic_permutation_unrank( + degree: int, + rank: int, +) -> tuple[int, ...]: + """Return one lexicographic permutation without materializing its group.""" + + group_order = _symmetric_group_order(degree) + if ( + not isinstance(rank, int) + or isinstance(rank, bool) + or not 0 <= rank < group_order + ): + raise ValueError("permutation rank is out of bounds") + available = list(range(degree)) + permutation: list[int] = [] + remainder = rank + for position in range(degree): + suffix_order = _SYMMETRIC_GROUP_ORDERS[degree - position - 1] + available_index, remainder = divmod(remainder, suffix_order) + permutation.append(available.pop(available_index)) + return tuple(permutation) + + +@dataclass(frozen=True, slots=True) +class _LexicographicPermutations(Sequence[tuple[int, ...]]): + """Constant-space sequence view of canonical symmetric-group elements.""" + + degree: int + + def __post_init__(self) -> None: + _symmetric_group_order(self.degree) + + def __len__(self) -> int: + return _SYMMETRIC_GROUP_ORDERS[self.degree] + + @overload + def __getitem__(self, index: int) -> tuple[int, ...]: ... + + @overload + def __getitem__(self, index: slice) -> tuple[tuple[int, ...], ...]: ... + + def __getitem__( + self, + index: int | slice, + ) -> tuple[int, ...] | tuple[tuple[int, ...], ...]: + if isinstance(index, slice): + return tuple( + _lexicographic_permutation_unrank(self.degree, rank) + for rank in range(*index.indices(len(self))) + ) + if not isinstance(index, int) or isinstance(index, bool): + raise TypeError("permutation index must be an integer or slice") + normalized_index = index if index >= 0 else len(self) + index + if not 0 <= normalized_index < len(self): + raise IndexError("permutation index is out of range") + return _lexicographic_permutation_unrank(self.degree, normalized_index) + + def __iter__(self) -> Iterator[tuple[int, ...]]: + for rank in range(len(self)): + yield _lexicographic_permutation_unrank(self.degree, rank) + + +@dataclass(frozen=True) +class CertifiedSymmetricGroupOrbit: + """One structurally certified regular ``S_m`` color-sector orbit.""" + + channel_key: tuple[object, ...] + sector_ids: tuple[int, ...] + permutations: Sequence[tuple[int, ...]] + + def __post_init__(self) -> None: + if not self.permutations: + raise ValueError("symmetric-group orbit has no permutations") + if isinstance(self.permutations, _LexicographicPermutations): + degree = self.permutations.degree + else: + degree = len(self.permutations[0]) + _symmetric_group_order(degree) + if any( + _lexicographic_permutation_rank(permutation, degree=degree) != index + for index, permutation in enumerate(self.permutations) + ): + raise ValueError( + "symmetric-group orbit permutations are not lexicographic" + ) + if len(self.permutations) != _SYMMETRIC_GROUP_ORDERS[degree]: + raise ValueError("symmetric-group orbit has the wrong group order") + if len(self.sector_ids) != len(self.permutations): + raise ValueError( + "symmetric-group orbit sectors do not match the group order" + ) + if len(set(self.sector_ids)) != len(self.sector_ids) or any( + sector_id < 0 for sector_id in self.sector_ids + ): + raise ValueError( + "symmetric-group orbit sector IDs must be unique and nonnegative" + ) + + @property + def degree(self) -> int: + if isinstance(self.permutations, _LexicographicPermutations): + return self.permutations.degree + return len(self.permutations[0]) + + +@dataclass(frozen=True) +class SymmetricGroupOrbitPartition: + """Certified orbit partition of one active structural color domain.""" + + degree: int + permuted_adjoint_labels: tuple[int, ...] + fixed_adjoint_labels: tuple[int, ...] + orbits: tuple[CertifiedSymmetricGroupOrbit, ...] + residual_sector_ids: tuple[int, ...] + sector_owner_ids: tuple[int, ...] + + def __post_init__(self) -> None: + if self.degree != len(self.permuted_adjoint_labels): + raise ValueError( + "symmetric-group degree does not match permuted adjoint labels" + ) + if self.degree < 0 or self.degree > _MAX_SYMMETRIC_GROUP_DEGREE: + raise ValueError("symmetric-group orbit degree is unsupported") + labels = (*self.fixed_adjoint_labels, *self.permuted_adjoint_labels) + if len(set(labels)) != len(labels): + raise ValueError("symmetric-group adjoint label partition overlaps") + if tuple(sorted(self.residual_sector_ids)) != self.residual_sector_ids: + raise ValueError( + "symmetric-group residual sectors are not canonically ordered" + ) + covered: set[int] = set() + for orbit in self.orbits: + previous_count = len(covered) + covered.update(orbit.sector_ids) + if len(covered) != previous_count + len(orbit.sector_ids): + raise ValueError("symmetric-group certified orbits overlap") + if covered.intersection(self.residual_sector_ids): + raise ValueError("symmetric-group orbit and residual sectors overlap") + if any(orbit.degree != self.degree for orbit in self.orbits): + raise ValueError("symmetric-group orbit degrees are inconsistent") + if self.orbits: + # Internally this is one constant-space canonical sequence view; + # retain compatibility with explicitly constructed canonical orbits. + shared_permutations = self.orbits[0].permutations + if any( + orbit.permutations is not shared_permutations + for orbit in self.orbits[1:] + ): + raise ValueError( + "symmetric-group channels do not share their canonical view" + ) + + @property + def ordered_sector_ids(self) -> tuple[int, ...]: + return ( + *(sector_id for orbit in self.orbits for sector_id in orbit.sector_ids), + *self.residual_sector_ids, + ) + + +def certify_symmetric_group_orbits( + color_plan: GenericColorPlan, + sector_ids: Sequence[int], + *, + sector_owner_ids: Sequence[int] | None = None, +) -> SymmetricGroupOrbitPartition: + """Partition active sectors into complete free adjoint-label orbits. + + Certification is structural: every channel contains exactly one canonical + sector for every lexicographic permutation, and each sector differs from + the identity only by simultaneous relabelling of the declared adjoint + labels. This is the invariance used by ``exact_color_contraction_factor``; + no dense color matrix is constructed here. + """ + + if not isinstance(color_plan, GenericColorPlan): + raise TypeError("symmetric-group planning requires a GenericColorPlan") + requested_sector_ids = tuple(sorted(int(value) for value in sector_ids)) + if len(set(requested_sector_ids)) != len(requested_sector_ids): + raise ValueError("symmetric-group active sectors contain duplicates") + sectors_by_id = {int(sector.id): sector for sector in color_plan.sectors} + if any(sector_id not in sectors_by_id for sector_id in requested_sector_ids): + raise ValueError("symmetric-group planning references an unknown sector") + + # The caller supplies the schedule-authenticated alias certificate. Orbit + # discovery operates only on its canonical owners; traversal replicas are + # destinations of those owners, never additional FFT channels. + owners = _validated_sector_owner_map(color_plan, sector_owner_ids) + _validate_owner_structures(color_plan, owners) + active_owners = {owners[sector_id] for sector_id in requested_sector_ids} + if _ZERO_SECTOR_OWNER in active_owners: + raise ValueError( + "symmetric-group active sector is certified as structural zero" + ) + active_sector_ids = tuple(sorted(active_owners)) + permuted_labels, fixed_labels = _adjoint_action_labels(color_plan) + degree = len(permuted_labels) + if degree > _MAX_SYMMETRIC_GROUP_DEGREE: + raise ValueError( + "symmetric-group degree exceeds the supported maximum " + f"{_MAX_SYMMETRIC_GROUP_DEGREE}" + ) + group_order = _symmetric_group_order(degree) + lexicographic_permutations = _LexicographicPermutations(degree) + + buckets: dict[ + tuple[object, ...], + dict[int, int | list[int]], + ] = {} + residual: set[int] = set() + for sector_id in active_sector_ids: + candidate = _sector_orbit_coordinate( + sectors_by_id[sector_id], + permuted_labels=permuted_labels, + fixed_labels=fixed_labels, + basis=color_plan.basis, + ) + if candidate is None or degree < 2: + residual.add(sector_id) + continue + channel_key, coordinate = candidate + try: + coordinate_index = _lexicographic_permutation_rank( + coordinate, + degree=degree, + ) + except ValueError: + residual.add(sector_id) + continue + sectors_by_coordinate = buckets.setdefault(channel_key, {}) + existing_sector_ids = sectors_by_coordinate.get(coordinate_index) + if existing_sector_ids is None: + sectors_by_coordinate[coordinate_index] = sector_id + elif isinstance(existing_sector_ids, int): + sectors_by_coordinate[coordinate_index] = [ + existing_sector_ids, + sector_id, + ] + else: + existing_sector_ids.append(sector_id) + + certified_orbit_rows: list[tuple[tuple[object, ...], tuple[int, ...]]] = [] + for channel_key in sorted(buckets): + sectors_by_coordinate = buckets[channel_key] + if len(sectors_by_coordinate) != group_order or any( + isinstance(sector_ids_at_coordinate, list) + for sector_ids_at_coordinate in sectors_by_coordinate.values() + ): + for sector_ids_at_coordinate in sectors_by_coordinate.values(): + if isinstance(sector_ids_at_coordinate, int): + residual.add(sector_ids_at_coordinate) + else: + residual.update(sector_ids_at_coordinate) + continue + orbit_sector_ids = tuple( + cast(int, sectors_by_coordinate[coordinate_index]) + for coordinate_index in range(group_order) + ) + certified_orbit_rows.append( + ( + channel_key, + orbit_sector_ids, + ) + ) + + if buckets: + del sectors_by_coordinate + buckets.clear() + orbits = tuple( + CertifiedSymmetricGroupOrbit( + channel_key=channel_key, + sector_ids=orbit_sector_ids, + permutations=lexicographic_permutations, + ) + for channel_key, orbit_sector_ids in certified_orbit_rows + ) + covered_count = sum(len(orbit.sector_ids) for orbit in orbits) + if covered_count + len(residual) != len(active_sector_ids): + raise AssertionError("symmetric-group orbit partition lost active sectors") + return SymmetricGroupOrbitPartition( + degree=degree, + permuted_adjoint_labels=permuted_labels, + fixed_adjoint_labels=fixed_labels, + orbits=orbits, + residual_sector_ids=tuple(sorted(residual)), + sector_owner_ids=owners, + ) + + +def build_symmetric_group_color_contraction_plan( + color_plan: GenericColorPlan, + groups: Sequence[ColorGroupDescriptor], + *, + sector_owner_ids: Sequence[int] | None = None, +) -> ColorContractionPlan: + """Build exact convolution kernels and a direct residual color metric.""" + + accuracy = color_plan.color_accuracy + descriptors = tuple(groups) + if accuracy not in {"nlc", "full"}: + return _unsupported( + accuracy, + len(descriptors), + "symmetric-group FFT requires NLC or full color", + ) + if color_plan.truncated or not color_plan.sectors: + return _unsupported( + accuracy, + len(descriptors), + "symmetric-group FFT requires a complete nonempty color plan", + ) + if not descriptors: + return _unsupported( + accuracy, + 0, + "symmetric-group FFT requires color-contraction groups", + ) + + try: + owners = _validated_sector_owner_map(color_plan, sector_owner_ids) + _validate_owner_structures(color_plan, owners) + except ValueError as exc: + return _unsupported(accuracy, len(descriptors), str(exc)) + rectangular = _rectangular_components( + color_plan, + descriptors, + owners=owners, + ) + if isinstance(rectangular, str): + return _unsupported(accuracy, len(descriptors), rectangular) + components, component_sector_ids, helicity_weight = rectangular + try: + partition = certify_symmetric_group_orbits( + color_plan, + component_sector_ids, + sector_owner_ids=owners, + ) + except ValueError as exc: + return _unsupported(accuracy, len(descriptors), str(exc)) + + if not partition.orbits and partition.degree >= 2: + return _unsupported( + accuracy, + len(descriptors), + "symmetric-group FFT found no certified orbit at nontrivial " + f"degree {partition.degree}", + ) + + # S_0 and S_1 have no useful transform. They are intentionally represented + # by the established exact direct residual rather than an empty FFT payload. + if not partition.orbits: + from .contraction import build_color_contraction_plan + + normalized_descriptors, destination_by_group = _normalized_descriptors( + components, + partition.ordered_sector_ids, + ) + direct = build_color_contraction_plan(color_plan, normalized_descriptors) + if direct is None: + return _unsupported( + accuracy, + len(descriptors), + "could not build the symmetric-group direct residual", + ) + return replace(direct, destination_by_group=destination_by_group) + + local_sector_ids = partition.ordered_sector_ids + _, destination_by_group = _normalized_descriptors( + components, + local_sector_ids, + ) + component_group_ids = tuple(range(len(local_sector_ids) * len(components))) + group_order = _symmetric_group_order(partition.degree) + channel_cosets = tuple( + tuple( + range( + channel_index * group_order, + (channel_index + 1) * group_order, + ) + ) + for channel_index in range(len(partition.orbits)) + ) + residual_local_group_indices = tuple( + range(len(channel_cosets) * group_order, len(local_sector_ids)) + ) + sector_by_id = {int(sector.id): sector for sector in color_plan.sectors} + weight_fraction = Fraction.from_float(helicity_weight) + + kernel_entries: list[ColorContractionTemplateEntry] = [] + kernel_exact_weights: list[Fraction] = [] + kernel_exact_by_key: dict[tuple[int, int, int], Fraction] = {} + diagonal_trace_exact: dict[tuple[int, tuple[int, ...]], Fraction] = {} + for left_channel, left_orbit in enumerate(partition.orbits): + left_local_index = channel_cosets[left_channel][0] + left_sector = sector_by_id[left_orbit.sector_ids[0]] + for right_channel in range(left_channel, len(partition.orbits)): + right_orbit = partition.orbits[right_channel] + symmetry = 1.0 if left_channel == right_channel else 2.0 + quotient_diagonal_trace = ( + left_channel == right_channel + and color_plan.basis == "trace" + and color_plan.process.color_endpoints.pair_count == 0 + and left_sector.kind == "single-trace" + and left_orbit.channel_key[:1] == ("single-trace",) + ) + for relative_index, right_sector_id in enumerate(right_orbit.sector_ids): + right_local_index = channel_cosets[right_channel][relative_index] + cache_key = None + if quotient_diagonal_trace: + cache_key = ( + left_channel, + _single_trace_diagonal_kernel_representative( + _lexicographic_permutation_unrank( + partition.degree, + relative_index, + ) + ), + ) + exact = ( + None if cache_key is None else diagonal_trace_exact.get(cache_key) + ) + if exact is None: + exact = weight_fraction * exact_color_contraction_factor( + color_plan, + left_sector, + sector_by_id[right_sector_id], + accuracy=accuracy, + full_col_acc=20, + ) + if cache_key is not None: + diagonal_trace_exact[cache_key] = exact + kernel_entries.append( + ColorContractionTemplateEntry( + left_group_index=left_local_index, + right_group_index=right_local_index, + weight_re=float(exact), + symmetry_factor=symmetry, + ) + ) + kernel_exact_weights.append(exact) + kernel_exact_by_key[(left_channel, right_channel, relative_index)] = ( + exact + ) + + hermiticity_relative_indices = _hermiticity_relative_indices( + partition.degree, + channel_count=len(partition.orbits), + ) + _certify_cross_channel_hermiticity( + color_plan, + partition, + sector_by_id=sector_by_id, + accuracy=accuracy, + weight_fraction=weight_fraction, + kernel_exact_by_key=kernel_exact_by_key, + relative_indices=hermiticity_relative_indices, + ) + _certify_sampled_equivariance( + color_plan, + partition, + sector_by_id=sector_by_id, + accuracy=accuracy, + weight_fraction=weight_fraction, + kernel_exact_by_key=kernel_exact_by_key, + ) + hermiticity_check_mode = ( + "vacuous" + if len(partition.orbits) < 2 + else "full" + if partition.degree <= 4 + else "deterministic-samples" + ) + + residual_entries: list[ColorContractionTemplateEntry] = [] + residual_exact_weights: list[Fraction] = [] + if residual_local_group_indices: + residual_set = set(residual_local_group_indices) + for left_local_index, left_sector_id in enumerate(local_sector_ids): + for right_local_index in range(left_local_index, len(local_sector_ids)): + if not residual_set.intersection((left_local_index, right_local_index)): + continue + exact = weight_fraction * exact_color_contraction_factor( + color_plan, + sector_by_id[left_sector_id], + sector_by_id[local_sector_ids[right_local_index]], + accuracy=accuracy, + full_col_acc=20, + ) + residual_entries.append( + ColorContractionTemplateEntry( + left_group_index=left_local_index, + right_group_index=right_local_index, + weight_re=float(exact), + symmetry_factor=( + 1.0 if left_local_index == right_local_index else 2.0 + ), + ) + ) + residual_exact_weights.append(exact) + + block = SymmetricGroupColorContractionBlock( + degree=partition.degree, + component_count=len(components), + component_group_ids=component_group_ids, + local_sector_ids=local_sector_ids, + channel_cosets=channel_cosets, + kernel_entries=tuple(kernel_entries), + kernel_exact_weights=tuple(kernel_exact_weights), + residual_entries=tuple(residual_entries), + residual_exact_weights=tuple(residual_exact_weights), + residual_local_group_indices=residual_local_group_indices, + hermiticity_check_mode=hermiticity_check_mode, + hermiticity_relative_indices=hermiticity_relative_indices, + ) + return ColorContractionPlan( + color_accuracy=accuracy, + supported=True, + reason=None, + group_count=len(component_group_ids), + entries=(), + symmetric_group_block=block, + destination_by_group=destination_by_group, + ) + + +def reconstruct_symmetric_group_dense_exact( + block: SymmetricGroupColorContractionBlock, +) -> dict[tuple[int, int], Fraction]: + """Expand a small plan for tests and generation-time certification tools.""" + + kernel: dict[tuple[int, int, int], Fraction] = {} + offset = 0 + for left_channel in range(block.channel_count): + for right_channel in range(left_channel, block.channel_count): + for relative_index in range(block.group_order): + kernel[(left_channel, right_channel, relative_index)] = ( + block.kernel_exact_weights[offset] + ) + offset += 1 + + dense: dict[tuple[int, int], Fraction] = {} + for left_channel, left_coset in enumerate(block.channel_cosets): + for right_channel in range(left_channel, block.channel_count): + right_coset = block.channel_cosets[right_channel] + for left_permutation_index, left_local in enumerate(left_coset): + left_permutation = _lexicographic_permutation_unrank( + block.degree, + left_permutation_index, + ) + for right_permutation_index, right_local in enumerate(right_coset): + if left_local > right_local: + continue + relative = _relative_permutation( + left_permutation, + _lexicographic_permutation_unrank( + block.degree, + right_permutation_index, + ), + ) + dense[(left_local, right_local)] = kernel[ + ( + left_channel, + right_channel, + _lexicographic_permutation_rank( + relative, + degree=block.degree, + ), + ) + ] + dense.update( + ( + (entry.left_group_index, entry.right_group_index), + exact, + ) + for entry, exact in zip( + block.residual_entries, + block.residual_exact_weights, + strict=True, + ) + ) + return dense + + +def _unsupported(accuracy: str, group_count: int, reason: str) -> ColorContractionPlan: + return ColorContractionPlan( + color_accuracy=accuracy, + supported=False, + reason=reason, + group_count=group_count, + entries=(), + ) + + +def _rectangular_components( + color_plan: GenericColorPlan, + descriptors: tuple[ColorGroupDescriptor, ...], + *, + owners: tuple[int, ...], +) -> ( + tuple[ + tuple[tuple[ColorGroupDescriptor, ...], ...], + tuple[int, ...], + float, + ] + | str +): + if len({descriptor.group_id for descriptor in descriptors}) != len(descriptors): + return "symmetric-group color groups contain duplicate group IDs" + components_by_helicity: dict[tuple[object, ...], list[ColorGroupDescriptor]] = {} + for descriptor in descriptors: + components_by_helicity.setdefault(descriptor.helicity_key, []).append( + descriptor + ) + original_components = tuple( + sorted( + ( + tuple(sorted(component, key=lambda descriptor: descriptor.group_id)) + for component in components_by_helicity.values() + ), + key=lambda component: component[0].group_id, + ) + ) + if any( + descriptor.sector_id < 0 or descriptor.sector_id >= len(owners) + for descriptor in descriptors + ): + return "symmetric-group color group references an unknown sector" + if any( + owners[descriptor.sector_id] == _ZERO_SECTOR_OWNER + for descriptor in descriptors + ): + return ( + "symmetric-group color group references a certified structural-zero sector" + ) + reference_owner_ids = tuple( + sorted({owners[descriptor.sector_id] for descriptor in original_components[0]}) + ) + if not reference_owner_ids: + return "symmetric-group helicity component has no canonical owners" + reference_weight = float(original_components[0][0].helicity_weight) + if not math.isfinite(reference_weight): + return "symmetric-group helicity weight is not finite" + reference_weight_bits = struct.pack(">d", reference_weight) + components: list[tuple[ColorGroupDescriptor, ...]] = [] + sectors_by_id = {int(sector.id): sector for sector in color_plan.sectors} + for component in original_components: + by_sector: dict[int, ColorGroupDescriptor] = {} + for descriptor in component: + if descriptor.sector_id in by_sector: + return "symmetric-group component contains duplicate color sectors" + by_sector[descriptor.sector_id] = descriptor + descriptors_by_owner: dict[int, list[ColorGroupDescriptor]] = {} + for sector_id, descriptor in by_sector.items(): + descriptors_by_owner.setdefault(owners[sector_id], []).append(descriptor) + owner_ids = tuple(sorted(descriptors_by_owner)) + if owner_ids != reference_owner_ids: + return "symmetric-group helicity components do not share one owner domain" + if any( + not math.isfinite(float(descriptor.helicity_weight)) + or struct.pack(">d", float(descriptor.helicity_weight)) + != reference_weight_bits + for descriptor in component + ): + return "symmetric-group helicity components have inconsistent weights" + canonical_component: list[ColorGroupDescriptor] = [] + for owner_id in owner_ids: + aliases = descriptors_by_owner[owner_id] + source = next( + ( + descriptor + for descriptor in aliases + if descriptor.sector_id == owner_id + ), + min(aliases, key=lambda descriptor: descriptor.group_id), + ) + owner_sector = sectors_by_id[owner_id] + canonical_component.append( + replace( + source, + sector_id=owner_id, + word=owner_sector.color_words[0], + ) + ) + components.append(tuple(canonical_component)) + + reference_sector_ids = reference_owner_ids + if len(set(reference_sector_ids)) != len(reference_sector_ids): + return "symmetric-group component contains duplicate color sectors" + return tuple(components), reference_sector_ids, reference_weight + + +def _normalized_descriptors( + components: tuple[tuple[ColorGroupDescriptor, ...], ...], + local_sector_ids: tuple[int, ...], +) -> tuple[tuple[ColorGroupDescriptor, ...], tuple[int, ...]]: + by_component_and_sector = tuple( + {descriptor.sector_id: descriptor for descriptor in component} + for component in components + ) + normalized: list[ColorGroupDescriptor] = [] + destination_by_group = [0] * (len(local_sector_ids) * len(components)) + for component_index, descriptors_by_sector in enumerate(by_component_and_sector): + for local_index, sector_id in enumerate(local_sector_ids): + source = descriptors_by_sector[sector_id] + group_id = local_index * len(components) + component_index + normalized.append( + ColorGroupDescriptor( + group_id=group_id, + helicity_key=source.helicity_key, + sector_id=source.sector_id, + word=source.word, + helicity_weight=source.helicity_weight, + ) + ) + destination_by_group[group_id] = source.group_id + return tuple(normalized), tuple(destination_by_group) + + +def _adjoint_action_labels( + color_plan: GenericColorPlan, +) -> tuple[tuple[int, ...], tuple[int, ...]]: + adjoint_labels = tuple( + sorted(int(label) for label in color_plan.process.adjoint_labels) + ) + if not adjoint_labels: + return (), () + if color_plan.process.fundamental_labels: + return adjoint_labels, () + if color_plan.basis == "adjoint": + return adjoint_labels[1:-1], (adjoint_labels[0], adjoint_labels[-1]) + anchor = adjoint_labels[0] + return adjoint_labels[1:], (anchor,) + + +def _sector_orbit_coordinate( + sector: LCColorSector, + *, + permuted_labels: tuple[int, ...], + fixed_labels: tuple[int, ...], + basis: str = "trace", +) -> tuple[tuple[object, ...], tuple[int, ...]] | None: + rank_by_label = {label: rank for rank, label in enumerate(permuted_labels)} + if basis == "adjoint": + word = tuple(int(label) for label in sector.trace_labels) + if ( + sector.kind != "single-trace" + or len(fixed_labels) != 2 + or len(word) != len(permuted_labels) + 2 + or word[0] != fixed_labels[0] + or word[-1] != fixed_labels[1] + or set(word[1:-1]) != set(permuted_labels) + ): + return None + return ( + ("adjoint", fixed_labels, tuple(sector.singlet_labels)), + tuple(rank_by_label[x] for x in word[1:-1]), + ) + if sector.kind == "single-trace": + if len(fixed_labels) != 1: + return None + anchor = fixed_labels[0] + trace = tuple(int(label) for label in sector.trace_labels) + if len(trace) != len(permuted_labels) + 1 or set(trace) != { + anchor, + *permuted_labels, + }: + return None + anchor_index = trace.index(anchor) + rotated = trace[anchor_index:] + trace[:anchor_index] + try: + coordinate = tuple(rank_by_label[label] for label in rotated[1:]) + except KeyError: + return None + return ( + ("single-trace", tuple(int(label) for label in sector.singlet_labels)), + coordinate, + ) + if sector.kind != "open-lines" or fixed_labels: + return None + + ordered_lines = tuple(sorted(sector.open_color_lines, key=_open_line_sort_key)) + flattened = tuple( + int(label) for line in ordered_lines for label in line.adjoint_labels + ) + if len(flattened) != len(permuted_labels) or set(flattened) != set(permuted_labels): + return None + if _open_line_block_order(sector) is None: + return None + try: + coordinate = tuple(rank_by_label[label] for label in flattened) + except KeyError: + return None + line_skeleton = tuple( + ( + int(line.fundamental_label), + int(line.antifundamental_label), + len(line.adjoint_labels), + tuple(int(label) for label in line.singlet_labels), + ) + for line in ordered_lines + ) + return ( + ( + "open-lines", + line_skeleton, + tuple(int(label) for label in sector.singlet_labels), + ), + coordinate, + ) + + +def _open_line_sort_key(line: LCOpenColorLine) -> tuple[object, ...]: + return ( + int(line.fundamental_label), + int(line.antifundamental_label), + tuple(int(label) for label in line.singlet_labels), + ) + + +def _open_line_block_order( + sector: LCColorSector, +) -> tuple[tuple[int, int, tuple[int, ...]], ...] | None: + lines = tuple(sector.open_color_lines) + word = tuple(int(label) for label in sector.color_words[0]) + remaining = list(lines) + order: list[tuple[int, int, tuple[int, ...]]] = [] + offset = 0 + while offset < len(word): + matching = [ + line + for line in remaining + if word[offset : offset + len(line.coloured_labels)] == line.coloured_labels + ] + if len(matching) != 1: + return None + line = matching[0] + order.append( + ( + int(line.fundamental_label), + int(line.antifundamental_label), + tuple(int(label) for label in line.singlet_labels), + ) + ) + offset += len(line.coloured_labels) + remaining.remove(line) + if remaining: + return None + return tuple(order) + + +def _validated_sector_owner_map( + color_plan: GenericColorPlan, + sector_owner_ids: Sequence[int] | None, +) -> tuple[int, ...]: + sector_count = len(color_plan.sectors) + owners = ( + tuple(range(sector_count)) + if sector_owner_ids is None + else tuple(int(value) for value in sector_owner_ids) + ) + if len(owners) != sector_count: + raise ValueError("symmetric-group sector owner map has the wrong size") + for sector_id, owner_id in enumerate(owners): + if owner_id == _ZERO_SECTOR_OWNER: + continue + if owner_id < 0 or owner_id >= sector_count: + raise ValueError("symmetric-group sector owner map is out of bounds") + if owners[owner_id] != owner_id: + raise ValueError("symmetric-group sector owner map is not idempotent") + if owner_id > sector_id: + raise ValueError("symmetric-group sector owner is not minimal") + return owners + + +def _validate_owner_structures( + color_plan: GenericColorPlan, + owners: tuple[int, ...], +) -> None: + sectors_by_id = {int(sector.id): sector for sector in color_plan.sectors} + if set(sectors_by_id) != set(range(len(color_plan.sectors))): + raise ValueError("symmetric-group color sectors are not densely numbered") + for sector_id, owner_id in enumerate(owners): + if owner_id == _ZERO_SECTOR_OWNER: + continue + if _owner_tensor_key(sectors_by_id[sector_id]) != _owner_tensor_key( + sectors_by_id[owner_id] + ): + raise ValueError( + "symmetric-group sector owner changes the canonical color tensor" + ) + + +def _owner_tensor_key(sector: LCColorSector) -> tuple[object, ...]: + if sector.kind == "open-lines": + return ( + "open-lines", + tuple( + sorted( + ( + int(line.fundamental_label), + tuple(int(label) for label in line.adjoint_labels), + int(line.antifundamental_label), + tuple(int(label) for label in line.singlet_labels), + ) + for line in sector.open_color_lines + ) + ), + tuple(int(label) for label in sector.singlet_labels), + ) + if sector.kind == "single-trace": + return ( + "single-trace", + tuple(int(label) for label in sector.trace_labels), + tuple(int(label) for label in sector.singlet_labels), + ) + return (sector.kind, tuple(int(label) for label in sector.singlet_labels)) + + +def _relative_permutation( + left: tuple[int, ...], + right: tuple[int, ...], +) -> tuple[int, ...]: + inverse = [0] * len(left) + for position, label in enumerate(left): + inverse[label] = position + return tuple(inverse[label] for label in right) + + +def _inverse_permutation(permutation: tuple[int, ...]) -> tuple[int, ...]: + inverse = [0] * len(permutation) + for position, label in enumerate(permutation): + inverse[label] = position + return tuple(inverse) + + +def _single_trace_diagonal_kernel_representative( + permutation: tuple[int, ...], +) -> tuple[int, ...]: + """Canonicalize one anchored trace under cyclic relabelling and inversion.""" + + trace_length = len(permutation) + 1 + anchored_word = (0, *(value + 1 for value in permutation)) + candidates: list[tuple[int, ...]] = [] + for shift in range(trace_length): + relabelled = tuple((value + shift) % trace_length for value in anchored_word) + anchor_index = relabelled.index(0) + rotated = relabelled[anchor_index:] + relabelled[:anchor_index] + coordinate = tuple(value - 1 for value in rotated[1:]) + candidates.extend((coordinate, _inverse_permutation(coordinate))) + return min(candidates) + + +def _hermiticity_relative_indices( + degree: int, + *, + channel_count: int, +) -> tuple[int, ...]: + if channel_count < 2: + return () + group_order = _symmetric_group_order(degree) + if degree <= 4: + return tuple(range(group_order)) + samples: list[tuple[int, ...]] = [tuple(range(degree))] + for left_position in range(degree - 1): + adjacent = list(range(degree)) + adjacent[left_position], adjacent[left_position + 1] = ( + adjacent[left_position + 1], + adjacent[left_position], + ) + samples.append(tuple(adjacent)) + non_involution = list(range(degree)) + non_involution[:3] = (1, 2, 0) + samples.append(tuple(non_involution)) + return tuple( + dict.fromkeys( + _lexicographic_permutation_rank(sample, degree=degree) for sample in samples + ) + ) + + +def _certify_cross_channel_hermiticity( + color_plan: GenericColorPlan, + partition: SymmetricGroupOrbitPartition, + *, + sector_by_id: dict[int, LCColorSector], + accuracy: str, + weight_fraction: Fraction, + kernel_exact_by_key: dict[tuple[int, int, int], Fraction], + relative_indices: tuple[int, ...], +) -> None: + if not relative_indices: + return + inverse_indices = tuple( + ( + relative_index, + _lexicographic_permutation_rank( + _inverse_permutation( + _lexicographic_permutation_unrank( + partition.degree, + relative_index, + ) + ), + degree=partition.degree, + ), + ) + for relative_index in relative_indices + ) + for left_channel, left_orbit in enumerate(partition.orbits): + for right_channel in range(left_channel + 1, len(partition.orbits)): + right_orbit = partition.orbits[right_channel] + for relative_index, inverse_index in inverse_indices: + reverse = weight_fraction * exact_color_contraction_factor( + color_plan, + sector_by_id[right_orbit.sector_ids[0]], + sector_by_id[left_orbit.sector_ids[inverse_index]], + accuracy=accuracy, + full_col_acc=20, + ) + if ( + kernel_exact_by_key[(left_channel, right_channel, relative_index)] + != reverse + ): + raise ValueError( + "symmetric-group cross-channel kernel failed its exact " + "Hermiticity certificate" + ) + + +def _certify_sampled_equivariance( + color_plan: GenericColorPlan, + partition: SymmetricGroupOrbitPartition, + *, + sector_by_id: dict[int, LCColorSector], + accuracy: str, + weight_fraction: Fraction, + kernel_exact_by_key: dict[tuple[int, int, int], Fraction], +) -> None: + """Check exact samples of the structurally proved convolution identity. + + Orbit discovery strips every permuted adjoint label to its rank while + retaining the complete non-adjoint trace/open-line skeleton. A complete + coordinate bucket is therefore a bijective simultaneous relabelling by + ``S_m``. Existing SU(3) contractions are invariant under that dummy-label + relabelling, which proves dependence on ``p^-1 q``. These deterministic + non-identity samples guard the coordinate/composition implementation + without an ``O(channel_count**2 * |S_m|**2)`` dense certificate. + """ + + left_action = list(range(partition.degree)) + left_action[0], left_action[1] = left_action[1], left_action[0] + left_action_tuple = tuple(left_action) + left_action_index = _lexicographic_permutation_rank( + left_action_tuple, + degree=partition.degree, + ) + relative_indices = _equivariance_sample_indices(partition.degree) + right_indices = tuple( + ( + relative_index, + _lexicographic_permutation_rank( + tuple( + left_action_tuple[position] + for position in _lexicographic_permutation_unrank( + partition.degree, + relative_index, + ) + ), + degree=partition.degree, + ), + ) + for relative_index in relative_indices + ) + for left_channel, left_orbit in enumerate(partition.orbits): + left_sector = sector_by_id[left_orbit.sector_ids[left_action_index]] + for right_channel in range(left_channel, len(partition.orbits)): + right_orbit = partition.orbits[right_channel] + for relative_index, right_permutation_index in right_indices: + exact = weight_fraction * exact_color_contraction_factor( + color_plan, + left_sector, + sector_by_id[right_orbit.sector_ids[right_permutation_index]], + accuracy=accuracy, + full_col_acc=20, + ) + if ( + kernel_exact_by_key[(left_channel, right_channel, relative_index)] + != exact + ): + raise ValueError( + "symmetric-group kernel failed its exact sampled " + "equivariance certificate" + ) + + +def _equivariance_sample_indices(degree: int) -> tuple[int, ...]: + _symmetric_group_order(degree) + samples: list[tuple[int, ...]] = [tuple(range(degree))] + for left_position in range(degree - 1): + adjacent = list(range(degree)) + adjacent[left_position], adjacent[left_position + 1] = ( + adjacent[left_position + 1], + adjacent[left_position], + ) + samples.append(tuple(adjacent)) + if degree >= 3: + non_involution = list(range(degree)) + non_involution[:3] = (1, 2, 0) + samples.append(tuple(non_involution)) + return tuple( + dict.fromkeys( + _lexicographic_permutation_rank(sample, degree=degree) for sample in samples + ) + ) + + +__all__ = [ + "CertifiedSymmetricGroupOrbit", + "SymmetricGroupOrbitPartition", + "build_symmetric_group_color_contraction_plan", + "certify_symmetric_group_orbits", + "reconstruct_symmetric_group_dense_exact", +] diff --git a/pyamplicol/src/pyamplicol/config/__init__.py b/pyamplicol/src/pyamplicol/config/__init__.py new file mode 100644 index 0000000000..11a48f1f43 --- /dev/null +++ b/pyamplicol/src/pyamplicol/config/__init__.py @@ -0,0 +1,115 @@ +# SPDX-License-Identifier: 0BSD +from __future__ import annotations + +from .errors import ConfigurationError, PyAmpliColError +from .models import ( + ACTIONS, + Action, + AutoBool, + AutoInt, + BenchmarkConfig, + ColorAccuracy, + ColorConfig, + ColorContraction, + ColorFFTBasis, + ColorMode, + CouplingOrderPolicy, + CppConfig, + EagerEvaluatorConfig, + EvaluationConfig, + EvaluatorBackend, + EvaluatorConfig, + EvaluatorExecutionMode, + EvaluatorOptimizationConfig, + GenerationConfig, + GenerationMode, + GenerationRelationDiscoveryConfig, + GenerationValidationConfig, + JITConfig, + LCFlowLayout, + LogLevel, + ModelConfig, + OutputConfig, + OutputFormat, + ProcessConfig, + ProcessEntry, + ProcessGroupAnalysis, + ProgressMode, + RecurrenceEvaluatorConfig, + RelationDiscoveryMode, + RunConfig, + SymbolicaConfig, +) +from .registry import ( + CONFIG_SECTIONS, + FIELD_REGISTRY, + ConfigField, + get_config_field, +) +from .resolver import ( + ClampRequest, + ConfigClamp, + ConfigOverride, + ConfigResolution, + config_to_dict, + config_to_toml, + load_config, + parse_override, + resolution_to_dict, + resolve_config, +) + +__all__ = [ + "ACTIONS", + "CONFIG_SECTIONS", + "FIELD_REGISTRY", + "Action", + "AutoBool", + "AutoInt", + "BenchmarkConfig", + "ClampRequest", + "ColorAccuracy", + "ColorConfig", + "ColorContraction", + "ColorFFTBasis", + "ColorMode", + "ConfigClamp", + "ConfigField", + "ConfigOverride", + "ConfigResolution", + "ConfigurationError", + "CouplingOrderPolicy", + "CppConfig", + "EagerEvaluatorConfig", + "EvaluationConfig", + "EvaluatorBackend", + "EvaluatorConfig", + "EvaluatorExecutionMode", + "EvaluatorOptimizationConfig", + "GenerationConfig", + "GenerationMode", + "GenerationRelationDiscoveryConfig", + "GenerationValidationConfig", + "JITConfig", + "LCFlowLayout", + "LogLevel", + "ModelConfig", + "OutputConfig", + "OutputFormat", + "ProcessConfig", + "ProcessEntry", + "ProcessGroupAnalysis", + "ProgressMode", + "PyAmpliColError", + "RecurrenceEvaluatorConfig", + "RelationDiscoveryMode", + "RunConfig", + "SymbolicaConfig", + "config_to_dict", + "config_to_toml", + "get_config_field", + "load_config", + "parse_override", + "resolution_to_dict", + "resolve_config", +] diff --git a/pyamplicol/src/pyamplicol/config/errors.py b/pyamplicol/src/pyamplicol/config/errors.py new file mode 100644 index 0000000000..89dd51f3db --- /dev/null +++ b/pyamplicol/src/pyamplicol/config/errors.py @@ -0,0 +1,29 @@ +# SPDX-License-Identifier: 0BSD +from __future__ import annotations + +from collections.abc import Iterable + + +class PyAmpliColError(Exception): + """Base class for stable pyAmpliCol errors.""" + + +class ConfigurationError(PyAmpliColError, ValueError): + """Raised when configuration input violates schema version 1.""" + + def __init__(self, message: str | Iterable[str]) -> None: + messages = (message,) if isinstance(message, str) else tuple(message) + if not messages: + messages = ("invalid configuration",) + if not all(isinstance(entry, str) and entry for entry in messages): + raise TypeError("configuration error messages must be non-empty strings") + self.messages = messages + rendered = messages[0] + if len(messages) > 1: + rendered = f"{len(messages)} configuration errors:\n" + "\n".join( + f"- {entry}" for entry in messages + ) + super().__init__(rendered) + + +__all__ = ["ConfigurationError", "PyAmpliColError"] diff --git a/pyamplicol/src/pyamplicol/config/models.py b/pyamplicol/src/pyamplicol/config/models.py new file mode 100644 index 0000000000..40bc9719da --- /dev/null +++ b/pyamplicol/src/pyamplicol/config/models.py @@ -0,0 +1,1197 @@ +# SPDX-License-Identifier: 0BSD +from __future__ import annotations + +import math +import os +import re +from collections.abc import Mapping, Sequence +from dataclasses import dataclass, field, replace +from enum import StrEnum +from pathlib import Path +from types import MappingProxyType +from typing import Literal, TypeAlias, TypeVar + +from .errors import ConfigurationError + + +class Action(StrEnum): + GENERATE = "generate" + EVALUATE = "evaluate" + BENCHMARK = "benchmark" + INSPECT = "inspect" + MODEL_INSPECT = "model-inspect" + MODEL_COMPILE = "model-compile" + MODEL_PROCESSES = "model-processes" + GROUP_PROCESSES = "group-processes" + + +class CouplingOrderPolicy(StrEnum): + MINIMAL = "minimal" + EXPLICIT = "explicit" + + +class ProcessGroupAnalysis(StrEnum): + NONE = "none" + NUMERICAL = "numerical" + + +class ColorAccuracy(StrEnum): + LC = "lc" + NLC = "nlc" + FULL = "full" + + +class ColorContraction(StrEnum): + AUTO = "auto" + DIRECT = "direct" + SYMMETRIC_GROUP_FFT = "symmetric-group-fft" + + +class ColorFFTBasis(StrEnum): + TRACE = "trace" + ADJOINT = "adjoint" + + +class LCFlowLayout(StrEnum): + TOPOLOGY_REPLAY = "topology-replay" + ALL_FLOW_UNION = "all-flow-union" + + +class GenerationMode(StrEnum): + ERROR = "error" + APPEND = "append" + REPLACE = "replace" + + +class RelationDiscoveryMode(StrEnum): + OFF = "off" + DIAGNOSTIC = "diagnostic" + CERTIFIED_REUSE = "certified-reuse" + + +class EvaluatorBackend(StrEnum): + JIT = "jit" + ASM = "asm" + CPP = "cpp" + + +class EvaluatorExecutionMode(StrEnum): + COMPILED = "compiled" + EAGER = "eager" + RECURRENCE = "recurrence" + ON_THE_FLY = "on-the-fly" + + +_PORTABLE_CPP_EXTRA_FLAGS = frozenset({"-fno-math-errno"}) +_PROCESS_NAME = re.compile(r"^[A-Za-z][A-Za-z0-9_.-]*$") + + +class OutputFormat(StrEnum): + HUMAN = "human" + JSON = "json" + + +class ColorMode(StrEnum): + AUTO = "auto" + ALWAYS = "always" + NEVER = "never" + + +class ProgressMode(StrEnum): + AUTO = "auto" + TTY = "tty" + LOG = "log" + OFF = "off" + + +class LogLevel(StrEnum): + DEBUG = "debug" + INFO = "info" + WARNING = "warning" + ERROR = "error" + + +AutoInt: TypeAlias = Literal["auto"] | int +AutoBool: TypeAlias = Literal["auto"] | bool + +ACTIONS: tuple[Action, ...] = tuple(Action) + +_T = TypeVar("_T") +_EnumT = TypeVar("_EnumT", bound=StrEnum) + + +def _setting( + kind: str, + *, + nullable: bool = False, + choices: Sequence[object] = (), + dynamic_kind: str | None = None, +) -> dict[str, object]: + return { + "config": { + "kind": kind, + "nullable": nullable, + "choices": tuple(choices), + "dynamic_kind": dynamic_kind, + } + } + + +def _section() -> dict[str, object]: + return {"config_section": True} + + +def _path(value: os.PathLike[str] | str | None, name: str) -> Path | None: + if value is None: + return None + try: + return Path(os.fspath(value)).expanduser().resolve(strict=False) + except TypeError as exc: + raise ConfigurationError(f"{name} must be a path or null") from exc + + +def _tuple_of_strings(value: Sequence[str], name: str) -> tuple[str, ...]: + if isinstance(value, (str, bytes)): + raise ConfigurationError(f"{name} must be a list of strings") + result = tuple(value) + if not all(isinstance(item, str) and item for item in result): + raise ConfigurationError(f"{name} must contain non-empty strings") + return result + + +def _tuple_of_ints(value: Sequence[int], name: str) -> tuple[int, ...]: + if isinstance(value, (str, bytes)): + raise ConfigurationError(f"{name} must be a list of integers") + result = tuple(value) + if any(isinstance(item, bool) or not isinstance(item, int) for item in result): + raise ConfigurationError(f"{name} must contain integers") + return result + + +def _string_tuple_mapping( + value: Mapping[str, Sequence[str]], name: str +) -> Mapping[str, tuple[str, ...]]: + if not isinstance(value, Mapping): + raise ConfigurationError(f"{name} must be a table") + result: dict[str, tuple[str, ...]] = {} + for key, entries in value.items(): + if not isinstance(key, str) or not key: + raise ConfigurationError(f"{name} keys must be non-empty strings") + result[key] = _tuple_of_strings(entries, f"{name}.{key}") + return MappingProxyType(result) + + +def _integer_mapping(value: Mapping[str, int], name: str) -> Mapping[str, int]: + if not isinstance(value, Mapping): + raise ConfigurationError(f"{name} must be a table") + result: dict[str, int] = {} + for key, entry in value.items(): + if not isinstance(key, str) or not key: + raise ConfigurationError(f"{name} keys must be non-empty strings") + if isinstance(entry, bool) or not isinstance(entry, int) or entry < 0: + raise ConfigurationError(f"{name}.{key} must be a non-negative integer") + result[key] = entry + return MappingProxyType(result) + + +def _signed_integer_mapping(value: Mapping[str, int], name: str) -> Mapping[str, int]: + if not isinstance(value, Mapping): + raise ConfigurationError(f"{name} must be a table") + result: dict[str, int] = {} + for key, entry in value.items(): + if not isinstance(key, str) or not key: + raise ConfigurationError(f"{name} keys must be non-empty strings") + if isinstance(entry, bool) or not isinstance(entry, int): + raise ConfigurationError(f"{name}.{key} must be an integer") + result[key] = entry + return MappingProxyType(result) + + +def _choice(value: _T, choices: Sequence[_T], name: str) -> _T: + if value not in choices: + allowed = ", ".join(repr(item) for item in choices) + raise ConfigurationError(f"{name} must be one of {allowed}; got {value!r}") + return value + + +def _enum(value: object, enum_type: type[_EnumT], name: str) -> _EnumT: + try: + if not isinstance(value, str): + raise TypeError + return enum_type(value) + except (TypeError, ValueError) as exc: + allowed = ", ".join(repr(item.value) for item in enum_type) + raise ConfigurationError( + f"{name} must be one of {allowed}; got {value!r}" + ) from exc + + +def _integer(value: int, name: str, *, minimum: int = 1) -> int: + if isinstance(value, bool) or not isinstance(value, int) or value < minimum: + qualifier = "positive" if minimum == 1 else f">= {minimum}" + raise ConfigurationError(f"{name} must be an integer {qualifier}") + return value + + +def _optional_integer(value: int | None, name: str, *, minimum: int = 0) -> int | None: + if value is None: + return None + return _integer(value, name, minimum=minimum) + + +def _finite_float( + value: float, name: str, *, minimum: float, exclusive: bool = False +) -> float: + if isinstance(value, bool) or not isinstance(value, (int, float)): + raise ConfigurationError(f"{name} must be a number") + result = float(value) + invalid = result <= minimum if exclusive else result < minimum + if not math.isfinite(result) or invalid: + operator = ">" if exclusive else ">=" + raise ConfigurationError(f"{name} must be finite and {operator} {minimum}") + return result + + +def _auto_integer(value: AutoInt, name: str) -> AutoInt: + if value == "auto": + return value + return _integer(value, name) + + +def _auto_bool(value: AutoBool, name: str) -> AutoBool: + if value == "auto" or isinstance(value, bool): + return value + raise ConfigurationError(f"{name} must be 'auto', true, or false") + + +@dataclass(frozen=True, slots=True) +class ModelConfig: + source: str = field(default="built-in-sm", metadata=_setting("str")) + restriction: str | None = field( + default=None, metadata=_setting("str", nullable=True) + ) + simplify: bool = field(default=True, metadata=_setting("bool")) + cache: bool = field(default=True, metadata=_setting("bool")) + cache_dir: Path | None = field( + default=None, metadata=_setting("path", nullable=True) + ) + + def __post_init__(self) -> None: + if not isinstance(self.source, str) or not self.source: + raise ConfigurationError("model.source must be a non-empty string") + if self.restriction is not None and not isinstance(self.restriction, str): + raise ConfigurationError("model.restriction must be a string or null") + if not isinstance(self.simplify, bool): + raise ConfigurationError("model.simplify must be a boolean") + if not isinstance(self.cache, bool): + raise ConfigurationError("model.cache must be a boolean") + object.__setattr__(self, "cache_dir", _path(self.cache_dir, "model.cache_dir")) + + +@dataclass(frozen=True, slots=True) +class ProcessEntry: + expression: str + name: str | None = None + + def __post_init__(self) -> None: + errors: list[str] = [] + if not isinstance(self.expression, str) or not self.expression.strip(): + errors.append("process entry expression must be a non-empty string") + elif "\n" in self.expression or "\r" in self.expression: + errors.append("process entry expression must contain exactly one line") + if self.name is not None and ( + not isinstance(self.name, str) or not _PROCESS_NAME.fullmatch(self.name) + ): + errors.append( + "process entry name must start with a letter and contain only " + "letters, digits, '.', '_', or '-'" + ) + if errors: + raise ConfigurationError(errors) + + +@dataclass(frozen=True, slots=True) +class ProcessConfig: + entries: tuple[ProcessEntry, ...] = field( + default=(), metadata=_setting("process_entries") + ) + multiparticles: Mapping[str, tuple[str, ...]] = field( + default_factory=dict, + metadata=_setting("map_list_str", dynamic_kind="list_str"), + ) + flavor_scheme: int = field(default=5, metadata=_setting("int")) + max_quark_lines: int | None = field( + default=None, metadata=_setting("int", nullable=True) + ) + coupling_order_policy: CouplingOrderPolicy = field( + default=CouplingOrderPolicy.MINIMAL, + metadata=_setting("str", choices=tuple(CouplingOrderPolicy)), + ) + max_coupling_orders: Mapping[str, int] = field( + default_factory=dict, + metadata=_setting("map_int", dynamic_kind="int"), + ) + max_color_sectors: int | None = field( + default=None, metadata=_setting("int", nullable=True) + ) + reference_color_order: tuple[int, ...] = field( + default=(), metadata=_setting("list_int") + ) + selected_color_sector_ids: tuple[int, ...] = field( + default=(), metadata=_setting("list_int") + ) + selected_source_helicities: Mapping[str, int] = field( + default_factory=dict, + metadata=_setting("map_int", dynamic_kind="int"), + ) + flavor_blind: bool = field(default=False, metadata=_setting("bool")) + consider_charge_conjugation: bool = field(default=True, metadata=_setting("bool")) + only_unit_multiplier: bool = field(default=False, metadata=_setting("bool")) + analyze_process_groups: ProcessGroupAnalysis = field( + default=ProcessGroupAnalysis.NONE, + metadata=_setting("str", choices=tuple(ProcessGroupAnalysis)), + ) + + def __post_init__(self) -> None: + if isinstance(self.entries, (str, bytes)): + raise ConfigurationError( + "process.entries must be a list of process entries" + ) + entries = tuple(self.entries) + if not all(isinstance(entry, ProcessEntry) for entry in entries): + raise ConfigurationError( + "process.entries must contain ProcessEntry objects" + ) + names = tuple(entry.name for entry in entries if entry.name is not None) + duplicate_names = sorted(name for name in set(names) if names.count(name) > 1) + if duplicate_names: + raise ConfigurationError( + "process entry names must be unique; duplicates: " + + ", ".join(repr(name) for name in duplicate_names) + ) + object.__setattr__(self, "entries", entries) + object.__setattr__( + self, + "multiparticles", + _string_tuple_mapping(self.multiparticles, "process.multiparticles"), + ) + object.__setattr__( + self, + "flavor_scheme", + _integer(self.flavor_scheme, "process.flavor_scheme", minimum=0), + ) + object.__setattr__( + self, + "max_quark_lines", + _optional_integer( + self.max_quark_lines, "process.max_quark_lines", minimum=0 + ), + ) + object.__setattr__( + self, + "coupling_order_policy", + _enum( + self.coupling_order_policy, + CouplingOrderPolicy, + "process.coupling_order_policy", + ), + ) + object.__setattr__( + self, + "max_coupling_orders", + _integer_mapping(self.max_coupling_orders, "process.max_coupling_orders"), + ) + object.__setattr__( + self, + "max_color_sectors", + _optional_integer( + self.max_color_sectors, "process.max_color_sectors", minimum=0 + ), + ) + object.__setattr__( + self, + "reference_color_order", + _tuple_of_ints(self.reference_color_order, "process.reference_color_order"), + ) + object.__setattr__( + self, + "selected_color_sector_ids", + _tuple_of_ints( + self.selected_color_sector_ids, + "process.selected_color_sector_ids", + ), + ) + object.__setattr__( + self, + "selected_source_helicities", + _signed_integer_mapping( + self.selected_source_helicities, + "process.selected_source_helicities", + ), + ) + for name in ( + "flavor_blind", + "consider_charge_conjugation", + "only_unit_multiplier", + ): + if not isinstance(getattr(self, name), bool): + raise ConfigurationError(f"process.{name} must be a boolean") + object.__setattr__( + self, + "analyze_process_groups", + _enum( + self.analyze_process_groups, + ProcessGroupAnalysis, + "process.analyze_process_groups", + ), + ) + + +@dataclass(frozen=True, slots=True) +class ColorConfig: + accuracy: ColorAccuracy = field( + default=ColorAccuracy.LC, + metadata=_setting("str", choices=tuple(ColorAccuracy)), + ) + contraction: ColorContraction = field( + default=ColorContraction.AUTO, + metadata=_setting("str", choices=tuple(ColorContraction)), + ) + lc_flow_layout: LCFlowLayout = field( + default=LCFlowLayout.TOPOLOGY_REPLAY, + metadata=_setting("str", choices=tuple(LCFlowLayout)), + ) + fft_basis: ColorFFTBasis = field( + default=ColorFFTBasis.ADJOINT, + metadata=_setting("str", choices=tuple(ColorFFTBasis)), + ) + + def __post_init__(self) -> None: + object.__setattr__( + self, "accuracy", _enum(self.accuracy, ColorAccuracy, "color.accuracy") + ) + object.__setattr__( + self, + "contraction", + _enum( + self.contraction, + ColorContraction, + "color.contraction", + ), + ) + object.__setattr__( + self, + "fft_basis", + _enum(self.fft_basis, ColorFFTBasis, "color.fft_basis"), + ) + object.__setattr__( + self, + "lc_flow_layout", + _enum(self.lc_flow_layout, LCFlowLayout, "color.lc_flow_layout"), + ) + if ( + self.lc_flow_layout is LCFlowLayout.ALL_FLOW_UNION + and self.accuracy is not ColorAccuracy.LC + ): + raise ConfigurationError( + "color.lc_flow_layout='all-flow-union' requires color.accuracy='lc'" + ) + if ( + self.contraction is ColorContraction.SYMMETRIC_GROUP_FFT + and self.accuracy is ColorAccuracy.LC + ): + raise ConfigurationError( + "color.contraction='symmetric-group-fft' requires " + "color.accuracy='nlc' or 'full'" + ) + + +@dataclass(frozen=True, slots=True) +class GenerationValidationConfig: + enabled: bool = field(default=True, metadata=_setting("bool")) + samples: int = field(default=2, metadata=_setting("int")) + seed: int = field(default=12345, metadata=_setting("int")) + relative_tolerance: float = field(default=1e-12, metadata=_setting("float")) + absolute_tolerance: float = field(default=1e-300, metadata=_setting("float")) + post_build_validation: bool = field(default=False, metadata=_setting("bool")) + + def __post_init__(self) -> None: + for name in ( + "enabled", + "post_build_validation", + ): + if not isinstance(getattr(self, name), bool): + raise ConfigurationError( + f"generation.validation.{name} must be a boolean" + ) + object.__setattr__( + self, + "samples", + _integer(self.samples, "generation.validation.samples"), + ) + object.__setattr__( + self, + "seed", + _integer(self.seed, "generation.validation.seed", minimum=0), + ) + object.__setattr__( + self, + "relative_tolerance", + _finite_float( + self.relative_tolerance, + "generation.validation.relative_tolerance", + minimum=0.0, + ), + ) + object.__setattr__( + self, + "absolute_tolerance", + _finite_float( + self.absolute_tolerance, + "generation.validation.absolute_tolerance", + minimum=0.0, + ), + ) + + +@dataclass(frozen=True, slots=True) +class GenerationRelationDiscoveryConfig: + """Opt-in, replayable current-relation discovery policy (off by default).""" + + mode: RelationDiscoveryMode = field( + default=RelationDiscoveryMode.OFF, + metadata=_setting("str", choices=tuple(RelationDiscoveryMode)), + ) + precision_digits: int = field(default=96, metadata=_setting("int")) + probe_count: int = field(default=4, metadata=_setting("int")) + verification_probe_count: int = field(default=4, metadata=_setting("int")) + relative_tolerance: float = field(default=1.0e-70, metadata=_setting("float")) + absolute_tolerance: float = field(default=1.0e-80, metadata=_setting("float")) + seed: int = field(default=0x5059414D, metadata=_setting("int")) + + def __post_init__(self) -> None: + object.__setattr__( + self, + "mode", + _enum( + self.mode, + RelationDiscoveryMode, + "generation.relation_discovery.mode", + ), + ) + object.__setattr__( + self, + "precision_digits", + _integer( + self.precision_digits, + "generation.relation_discovery.precision_digits", + minimum=80, + ), + ) + object.__setattr__( + self, + "probe_count", + _integer( + self.probe_count, + "generation.relation_discovery.probe_count", + minimum=2, + ), + ) + object.__setattr__( + self, + "verification_probe_count", + _integer( + self.verification_probe_count, + "generation.relation_discovery.verification_probe_count", + minimum=2, + ), + ) + object.__setattr__( + self, + "relative_tolerance", + _finite_float( + self.relative_tolerance, + "generation.relation_discovery.relative_tolerance", + minimum=0.0, + ), + ) + object.__setattr__( + self, + "absolute_tolerance", + _finite_float( + self.absolute_tolerance, + "generation.relation_discovery.absolute_tolerance", + minimum=0.0, + ), + ) + if any( + value == 0.0 and math.copysign(1.0, value) < 0.0 + for value in (self.relative_tolerance, self.absolute_tolerance) + ): + raise ConfigurationError( + "generation.relation_discovery tolerances cannot use negative zero" + ) + if self.relative_tolerance == 0.0 and self.absolute_tolerance == 0.0: + raise ConfigurationError( + "generation.relation_discovery tolerances cannot both be zero" + ) + object.__setattr__( + self, + "seed", + _integer( + self.seed, + "generation.relation_discovery.seed", + minimum=0, + ), + ) + + +@dataclass(frozen=True, slots=True) +class GenerationConfig: + output: Path | None = field(default=None, metadata=_setting("path", nullable=True)) + mode: GenerationMode = field( + default=GenerationMode.ERROR, + metadata=_setting("str", choices=tuple(GenerationMode)), + ) + workers: AutoInt = field(default="auto", metadata=_setting("auto_int")) + emit_api_bundle: bool = field(default=True, metadata=_setting("bool")) + validation: GenerationValidationConfig = field( + default_factory=GenerationValidationConfig, metadata=_section() + ) + relation_discovery: GenerationRelationDiscoveryConfig = field( + default_factory=GenerationRelationDiscoveryConfig, + metadata=_section(), + ) + + def __post_init__(self) -> None: + object.__setattr__(self, "output", _path(self.output, "generation.output")) + object.__setattr__( + self, "mode", _enum(self.mode, GenerationMode, "generation.mode") + ) + object.__setattr__( + self, "workers", _auto_integer(self.workers, "generation.workers") + ) + if not isinstance(self.emit_api_bundle, bool): + raise ConfigurationError("generation.emit_api_bundle must be a boolean") + if not isinstance(self.validation, GenerationValidationConfig): + raise ConfigurationError( + "generation.validation must be a GenerationValidationConfig" + ) + if not isinstance( + self.relation_discovery, + GenerationRelationDiscoveryConfig, + ): + raise ConfigurationError( + "generation.relation_discovery must be a " + "GenerationRelationDiscoveryConfig" + ) + + +@dataclass(frozen=True, slots=True) +class EvaluatorOptimizationConfig: + horner_iterations: int = field(default=10, metadata=_setting("int")) + cpe_iterations: int | None = field( + default=None, metadata=_setting("int", nullable=True) + ) + cores: AutoInt = field(default="auto", metadata=_setting("auto_int")) + max_horner_variables: int = field(default=1000, metadata=_setting("int")) + max_common_pair_cache_entries: int = field( + default=5_000_000, metadata=_setting("int") + ) + max_common_pair_distance: int = field(default=1000, metadata=_setting("int")) + collect_factors: AutoBool = field(default="auto", metadata=_setting("auto_bool")) + + def __post_init__(self) -> None: + object.__setattr__( + self, + "horner_iterations", + _integer( + self.horner_iterations, + "evaluator.optimization.horner_iterations", + minimum=0, + ), + ) + object.__setattr__( + self, + "cpe_iterations", + _optional_integer( + self.cpe_iterations, + "evaluator.optimization.cpe_iterations", + minimum=0, + ), + ) + object.__setattr__( + self, + "cores", + _auto_integer(self.cores, "evaluator.optimization.cores"), + ) + for name in ( + "max_horner_variables", + "max_common_pair_cache_entries", + "max_common_pair_distance", + ): + object.__setattr__( + self, + name, + _integer( + getattr(self, name), + f"evaluator.optimization.{name}", + minimum=0, + ), + ) + object.__setattr__( + self, + "collect_factors", + _auto_bool(self.collect_factors, "evaluator.optimization.collect_factors"), + ) + + +@dataclass(frozen=True, slots=True) +class JITConfig: + optimization_level: Literal[0, 1, 2, 3] = field( + default=2, metadata=_setting("int", choices=(0, 1, 2, 3)) + ) + compress: AutoBool = field(default="auto", metadata=_setting("auto_bool")) + + def __post_init__(self) -> None: + if isinstance(self.optimization_level, bool) or not isinstance( + self.optimization_level, int + ): + raise ConfigurationError( + "evaluator.jit.optimization_level must be an integer" + ) + _choice( + self.optimization_level, + (0, 1, 2, 3), + "evaluator.jit.optimization_level", + ) + object.__setattr__( + self, + "compress", + _auto_bool(self.compress, "evaluator.jit.compress"), + ) + + +@dataclass(frozen=True, slots=True) +class CppConfig: + optimization: str = field(default="O3", metadata=_setting("str")) + compiler: str | None = field(default=None, metadata=_setting("str", nullable=True)) + native_arch: bool = field(default=False, metadata=_setting("bool")) + extra_flags: tuple[str, ...] = field(default=(), metadata=_setting("list_str")) + + def __post_init__(self) -> None: + if not isinstance(self.optimization, str) or not self.optimization: + raise ConfigurationError( + "evaluator.cpp.optimization must be a non-empty string" + ) + if self.compiler is not None and not isinstance(self.compiler, str): + raise ConfigurationError("evaluator.cpp.compiler must be a string or null") + if not isinstance(self.native_arch, bool): + raise ConfigurationError("evaluator.cpp.native_arch must be a boolean") + extra_flags = _tuple_of_strings(self.extra_flags, "evaluator.cpp.extra_flags") + unsupported = tuple( + flag for flag in extra_flags if flag not in _PORTABLE_CPP_EXTRA_FLAGS + ) + if unsupported: + rendered = ", ".join(repr(flag) for flag in unsupported) + allowed = ", ".join(sorted(_PORTABLE_CPP_EXTRA_FLAGS)) + raise ConfigurationError( + "evaluator.cpp.extra_flags contains unsupported compiler arguments " + f"({rendered}); arbitrary flags may introduce unrecorded target CPU " + f"requirements. Allowed portable flags: {allowed}" + ) + object.__setattr__(self, "extra_flags", extra_flags) + + +@dataclass(frozen=True, slots=True) +class EagerEvaluatorConfig: + point_tile_size: int = field(default=1024, metadata=_setting("int")) + # Soft batching target, not a cap on the workspace needed by one point. + workspace_mib: int = field(default=256, metadata=_setting("int")) + + def __post_init__(self) -> None: + object.__setattr__( + self, + "point_tile_size", + _integer( + self.point_tile_size, + "evaluator.eager.point_tile_size", + minimum=1, + ), + ) + object.__setattr__( + self, + "workspace_mib", + _integer( + self.workspace_mib, + "evaluator.eager.workspace_mib", + minimum=1, + ), + ) + + +@dataclass(frozen=True, slots=True) +class RecurrenceEvaluatorConfig: + point_tile_size: int = field(default=1024, metadata=_setting("int")) + # Soft batching target, not a cap on the workspace needed by one point. + workspace_mib: int = field(default=256, metadata=_setting("int")) + + def __post_init__(self) -> None: + object.__setattr__( + self, + "point_tile_size", + _integer( + self.point_tile_size, + "evaluator.recurrence.point_tile_size", + minimum=1, + ), + ) + object.__setattr__( + self, + "workspace_mib", + _integer( + self.workspace_mib, + "evaluator.recurrence.workspace_mib", + minimum=1, + ), + ) + + +@dataclass(frozen=True, slots=True) +class EvaluatorConfig: + backend: EvaluatorBackend = field( + default=EvaluatorBackend.JIT, + metadata=_setting("str", choices=tuple(EvaluatorBackend)), + ) + execution_mode: EvaluatorExecutionMode = field( + default=EvaluatorExecutionMode.RECURRENCE, + metadata=_setting("str", choices=tuple(EvaluatorExecutionMode)), + ) + batch_size: int = field(default=128, metadata=_setting("int")) + output_chunk_size: int | None = field( + default=512, metadata=_setting("int", nullable=True) + ) + optimization: EvaluatorOptimizationConfig = field( + default_factory=EvaluatorOptimizationConfig, metadata=_section() + ) + jit: JITConfig = field(default_factory=JITConfig, metadata=_section()) + cpp: CppConfig = field(default_factory=CppConfig, metadata=_section()) + eager: EagerEvaluatorConfig = field( + default_factory=EagerEvaluatorConfig, metadata=_section() + ) + recurrence: RecurrenceEvaluatorConfig = field( + default_factory=RecurrenceEvaluatorConfig, metadata=_section() + ) + + @property + def resolved_jit_compress(self) -> bool: + """Use compressed code for compiled O2, preserving explicit choices.""" + + if self.jit.compress != "auto": + return self.jit.compress + return ( + self.backend is EvaluatorBackend.JIT + and self.execution_mode is EvaluatorExecutionMode.COMPILED + and self.jit.optimization_level == 2 + ) + + def __post_init__(self) -> None: + object.__setattr__( + self, + "backend", + _enum(self.backend, EvaluatorBackend, "evaluator.backend"), + ) + object.__setattr__( + self, + "execution_mode", + _enum( + self.execution_mode, + EvaluatorExecutionMode, + "evaluator.execution_mode", + ), + ) + object.__setattr__( + self, "batch_size", _integer(self.batch_size, "evaluator.batch_size") + ) + object.__setattr__( + self, + "output_chunk_size", + _optional_integer( + self.output_chunk_size, "evaluator.output_chunk_size", minimum=1 + ), + ) + if not isinstance(self.optimization, EvaluatorOptimizationConfig): + raise ConfigurationError( + "evaluator.optimization must be an EvaluatorOptimizationConfig" + ) + if not isinstance(self.jit, JITConfig): + raise ConfigurationError("evaluator.jit must be a JITConfig") + if not isinstance(self.cpp, CppConfig): + raise ConfigurationError("evaluator.cpp must be a CppConfig") + if not isinstance(self.eager, EagerEvaluatorConfig): + raise ConfigurationError("evaluator.eager must be an EagerEvaluatorConfig") + if not isinstance(self.recurrence, RecurrenceEvaluatorConfig): + raise ConfigurationError( + "evaluator.recurrence must be a RecurrenceEvaluatorConfig" + ) + + +@dataclass(frozen=True, slots=True) +class EvaluationConfig: + artifact: Path | None = field( + default=None, metadata=_setting("path", nullable=True) + ) + process: str | None = field(default=None, metadata=_setting("str", nullable=True)) + precision: int = field(default=16, metadata=_setting("int")) + resolved: bool = field(default=False, metadata=_setting("bool")) + helicity_ids: tuple[str, ...] = field(default=(), metadata=_setting("list_str")) + color_flow_ids: tuple[str, ...] = field(default=(), metadata=_setting("list_str")) + model_parameters: Path | None = field( + default=None, metadata=_setting("path", nullable=True) + ) + momenta: Path | None = field(default=None, metadata=_setting("path", nullable=True)) + + def __post_init__(self) -> None: + object.__setattr__( + self, "artifact", _path(self.artifact, "evaluation.artifact") + ) + if self.process is not None and not isinstance(self.process, str): + raise ConfigurationError("evaluation.process must be a string or null") + object.__setattr__( + self, "precision", _integer(self.precision, "evaluation.precision") + ) + if not isinstance(self.resolved, bool): + raise ConfigurationError("evaluation.resolved must be a boolean") + object.__setattr__( + self, + "helicity_ids", + _tuple_of_strings(self.helicity_ids, "evaluation.helicity_ids"), + ) + object.__setattr__( + self, + "color_flow_ids", + _tuple_of_strings(self.color_flow_ids, "evaluation.color_flow_ids"), + ) + object.__setattr__( + self, + "model_parameters", + _path(self.model_parameters, "evaluation.model_parameters"), + ) + object.__setattr__(self, "momenta", _path(self.momenta, "evaluation.momenta")) + + +@dataclass(frozen=True, slots=True) +class BenchmarkConfig: + target_runtime: float = field(default=10.0, metadata=_setting("float")) + batch_size: int = field(default=128, metadata=_setting("int")) + precision: int = field(default=16, metadata=_setting("int")) + warmup_runs: int = field(default=2, metadata=_setting("int")) + minimum_samples: int = field(default=5, metadata=_setting("int")) + helicity_ids: tuple[str, ...] = field(default=(), metadata=_setting("list_str")) + color_flow_ids: tuple[str, ...] = field(default=(), metadata=_setting("list_str")) + + def __post_init__(self) -> None: + object.__setattr__( + self, + "target_runtime", + _finite_float( + self.target_runtime, + "benchmark.target_runtime", + minimum=0.0, + exclusive=True, + ), + ) + object.__setattr__( + self, "batch_size", _integer(self.batch_size, "benchmark.batch_size") + ) + object.__setattr__( + self, "precision", _integer(self.precision, "benchmark.precision") + ) + object.__setattr__( + self, + "warmup_runs", + _integer(self.warmup_runs, "benchmark.warmup_runs", minimum=0), + ) + object.__setattr__( + self, + "minimum_samples", + _integer(self.minimum_samples, "benchmark.minimum_samples"), + ) + object.__setattr__( + self, + "helicity_ids", + _tuple_of_strings(self.helicity_ids, "benchmark.helicity_ids"), + ) + object.__setattr__( + self, + "color_flow_ids", + _tuple_of_strings(self.color_flow_ids, "benchmark.color_flow_ids"), + ) + + +@dataclass(frozen=True, slots=True) +class OutputConfig: + format: OutputFormat = field( + default=OutputFormat.HUMAN, + metadata=_setting("str", choices=tuple(OutputFormat)), + ) + color: ColorMode = field( + default=ColorMode.AUTO, + metadata=_setting("str", choices=tuple(ColorMode)), + ) + progress: ProgressMode = field( + default=ProgressMode.AUTO, + metadata=_setting("str", choices=tuple(ProgressMode)), + ) + log_level: LogLevel = field( + default=LogLevel.INFO, + metadata=_setting("str", choices=tuple(LogLevel)), + ) + + def __post_init__(self) -> None: + object.__setattr__( + self, "format", _enum(self.format, OutputFormat, "output.format") + ) + object.__setattr__(self, "color", _enum(self.color, ColorMode, "output.color")) + object.__setattr__( + self, "progress", _enum(self.progress, ProgressMode, "output.progress") + ) + object.__setattr__( + self, "log_level", _enum(self.log_level, LogLevel, "output.log_level") + ) + + +@dataclass(frozen=True, slots=True) +class SymbolicaConfig: + suggest_license: bool = field(default=True, metadata=_setting("bool")) + + def __post_init__(self) -> None: + if not isinstance(self.suggest_license, bool): + raise ConfigurationError("symbolica.suggest_license must be a boolean") + + +@dataclass(frozen=True, slots=True) +class RunConfig: + action: Action = field(metadata=_setting("str", choices=ACTIONS)) + schema_version: int = field(default=1, metadata=_setting("int", choices=(1,))) + model: ModelConfig = field(default_factory=ModelConfig, metadata=_section()) + process: ProcessConfig = field(default_factory=ProcessConfig, metadata=_section()) + color: ColorConfig = field(default_factory=ColorConfig, metadata=_section()) + generation: GenerationConfig = field( + default_factory=GenerationConfig, metadata=_section() + ) + evaluator: EvaluatorConfig = field( + default_factory=EvaluatorConfig, metadata=_section() + ) + evaluation: EvaluationConfig = field( + default_factory=EvaluationConfig, metadata=_section() + ) + benchmark: BenchmarkConfig = field( + default_factory=BenchmarkConfig, metadata=_section() + ) + output: OutputConfig = field(default_factory=OutputConfig, metadata=_section()) + symbolica: SymbolicaConfig = field( + default_factory=SymbolicaConfig, metadata=_section() + ) + + def __post_init__(self) -> None: + object.__setattr__(self, "action", _enum(self.action, Action, "action")) + if isinstance(self.schema_version, bool) or not isinstance( + self.schema_version, int + ): + raise ConfigurationError("schema_version must be an integer") + _choice(self.schema_version, (1,), "schema_version") + expected = ( + ("model", ModelConfig), + ("process", ProcessConfig), + ("color", ColorConfig), + ("generation", GenerationConfig), + ("evaluator", EvaluatorConfig), + ("evaluation", EvaluationConfig), + ("benchmark", BenchmarkConfig), + ("output", OutputConfig), + ("symbolica", SymbolicaConfig), + ) + for name, expected_type in expected: + if not isinstance(getattr(self, name), expected_type): + raise ConfigurationError(f"{name} must be a {expected_type.__name__}") + if ( + self.color.contraction is ColorContraction.SYMMETRIC_GROUP_FFT + and self.evaluator.execution_mode + not in { + EvaluatorExecutionMode.RECURRENCE, + EvaluatorExecutionMode.ON_THE_FLY, + } + ): + raise ConfigurationError( + "color.contraction='symmetric-group-fft' requires " + "evaluator.execution_mode='recurrence' or 'on-the-fly'" + ) + + @property + def resolved_color(self) -> ColorConfig: + """Keep automatic FFT selection only in its supported execution lanes. + + Eligible automatic requests stay automatic until the process-specific + colour plan can certify FFT support. An explicit direct request ignores + the FFT basis, including its adaptive default. + """ + + color = self.color + if color.contraction is ColorContraction.DIRECT or ( + color.contraction is ColorContraction.AUTO + and ( + color.accuracy is ColorAccuracy.LC + or self.evaluator.execution_mode + not in { + EvaluatorExecutionMode.RECURRENCE, + EvaluatorExecutionMode.ON_THE_FLY, + } + ) + ): + return replace( + color, + contraction=ColorContraction.DIRECT, + fft_basis=ColorFFTBasis.TRACE, + ) + return color + + +__all__ = [ + "ACTIONS", + "Action", + "AutoBool", + "AutoInt", + "BenchmarkConfig", + "ColorAccuracy", + "ColorConfig", + "ColorContraction", + "ColorFFTBasis", + "ColorMode", + "CouplingOrderPolicy", + "CppConfig", + "EagerEvaluatorConfig", + "EvaluationConfig", + "EvaluatorBackend", + "EvaluatorConfig", + "EvaluatorExecutionMode", + "EvaluatorOptimizationConfig", + "GenerationConfig", + "GenerationMode", + "GenerationRelationDiscoveryConfig", + "GenerationValidationConfig", + "JITConfig", + "LCFlowLayout", + "LogLevel", + "ModelConfig", + "OutputConfig", + "OutputFormat", + "ProcessConfig", + "ProcessEntry", + "ProcessGroupAnalysis", + "ProgressMode", + "RecurrenceEvaluatorConfig", + "RelationDiscoveryMode", + "RunConfig", + "SymbolicaConfig", +] diff --git a/pyamplicol/src/pyamplicol/config/registry.py b/pyamplicol/src/pyamplicol/config/registry.py new file mode 100644 index 0000000000..0501669fa7 --- /dev/null +++ b/pyamplicol/src/pyamplicol/config/registry.py @@ -0,0 +1,129 @@ +# SPDX-License-Identifier: 0BSD +from __future__ import annotations + +from collections.abc import Callable, Mapping +from dataclasses import MISSING, dataclass, fields, is_dataclass +from types import MappingProxyType +from typing import cast, get_type_hints + +from .errors import ConfigurationError +from .models import RunConfig + + +@dataclass(frozen=True, slots=True) +class ConfigField: + """One schema-v1 leaf shared by TOML and command-line resolution.""" + + path: str + kind: str + nullable: bool = False + choices: tuple[object, ...] = () + dynamic_kind: str | None = None + required: bool = False + default: object | None = None + + +def _immutable_default(value: object) -> object: + if isinstance(value, Mapping): + return MappingProxyType( + {key: _immutable_default(entry) for key, entry in value.items()} + ) + if isinstance(value, list): + return tuple(_immutable_default(entry) for entry in value) + return value + + +def _build_registry( + cls: type[object], prefix: str = "" +) -> tuple[dict[str, ConfigField], set[str]]: + if not is_dataclass(cls): + raise TypeError(f"{cls!r} is not a dataclass") + registry: dict[str, ConfigField] = {} + sections: set[str] = set() + hints = get_type_hints(cls) + for item in fields(cls): + path = f"{prefix}.{item.name}" if prefix else item.name + if item.metadata.get("config_section"): + sections.add(path) + child_registry, child_sections = _build_registry(hints[item.name], path) + registry.update(child_registry) + sections.update(child_sections) + continue + + raw_spec = item.metadata.get("config") + if not isinstance(raw_spec, Mapping): + raise TypeError(f"configuration field {path!r} has no registry metadata") + + required = item.default is MISSING and item.default_factory is MISSING + if required: + default: object | None = None + elif item.default is not MISSING: + default = item.default + else: + default_factory = cast(Callable[[], object], item.default_factory) + default = default_factory() + registry[path] = ConfigField( + path=path, + kind=str(raw_spec["kind"]), + nullable=bool(raw_spec.get("nullable", False)), + choices=tuple(raw_spec.get("choices", ())), + dynamic_kind=( + str(raw_spec["dynamic_kind"]) + if raw_spec.get("dynamic_kind") is not None + else None + ), + required=required, + default=_immutable_default(default), + ) + return registry, sections + + +_REGISTRY, _SECTIONS = _build_registry(RunConfig) +FIELD_REGISTRY: Mapping[str, ConfigField] = MappingProxyType(_REGISTRY) +CONFIG_SECTIONS: tuple[str, ...] = tuple(sorted(_SECTIONS)) + + +def get_config_field(path: str) -> ConfigField: + """Return the exact or schema-approved dynamic field for a dotted path.""" + + if path in FIELD_REGISTRY: + return FIELD_REGISTRY[path] + for parent_path, item in FIELD_REGISTRY.items(): + prefix = f"{parent_path}." + if item.dynamic_kind is not None and path.startswith(prefix): + key = path[len(prefix) :] + if key and "." not in key: + return ConfigField( + path=path, + kind=item.dynamic_kind, + nullable=False, + required=False, + ) + raise ConfigurationError(f"unknown configuration field {path!r}") + + +def default_values() -> dict[str, object]: + """Return a fresh flat mapping of every schema default.""" + + return { + path: _copy_default(item.default) + for path, item in FIELD_REGISTRY.items() + if not item.required + } + + +def _copy_default(value: object) -> object: + if isinstance(value, Mapping): + return {key: _copy_default(entry) for key, entry in value.items()} + if isinstance(value, tuple): + return tuple(_copy_default(entry) for entry in value) + return value + + +__all__ = [ + "CONFIG_SECTIONS", + "FIELD_REGISTRY", + "ConfigField", + "default_values", + "get_config_field", +] diff --git a/pyamplicol/src/pyamplicol/config/resolver.py b/pyamplicol/src/pyamplicol/config/resolver.py new file mode 100644 index 0000000000..b58e910b74 --- /dev/null +++ b/pyamplicol/src/pyamplicol/config/resolver.py @@ -0,0 +1,632 @@ +# SPDX-License-Identifier: 0BSD +from __future__ import annotations + +import difflib +import os +import tomllib +from collections.abc import Iterable, Mapping, Sequence +from dataclasses import dataclass, fields, is_dataclass, replace +from pathlib import Path +from types import MappingProxyType +from typing import Any, NoReturn, TypeVar, cast, get_type_hints + +from .errors import ConfigurationError +from .models import Action, ProcessEntry, RunConfig +from .registry import CONFIG_SECTIONS, FIELD_REGISTRY, ConfigField, default_values + +_DataclassT = TypeVar("_DataclassT") + + +def _extend_errors(errors: list[str], error: ConfigurationError) -> None: + errors.extend(error.messages) + + +def _raise_errors(errors: Sequence[str]) -> None: + if errors: + raise ConfigurationError(errors) + + +def _freeze_public(value: object) -> object: + if isinstance(value, Mapping): + return MappingProxyType( + {str(key): _freeze_public(entry) for key, entry in value.items()} + ) + if isinstance(value, (list, tuple)): + return tuple(_freeze_public(entry) for entry in value) + return value + + +@dataclass(frozen=True, slots=True) +class ConfigOverride: + path: str + value: object + + def __post_init__(self) -> None: + object.__setattr__(self, "value", _freeze_public(self.value)) + + +@dataclass(frozen=True, slots=True) +class ClampRequest: + path: str + effective: object + reason: str + + def __post_init__(self) -> None: + if not self.reason: + raise ConfigurationError("a clamp reason must not be empty") + object.__setattr__(self, "effective", _freeze_public(self.effective)) + + +@dataclass(frozen=True, slots=True) +class ConfigClamp: + path: str + requested: object + effective: object + reason: str + + def __post_init__(self) -> None: + object.__setattr__(self, "requested", _freeze_public(self.requested)) + object.__setattr__(self, "effective", _freeze_public(self.effective)) + + +@dataclass(frozen=True, slots=True) +class ConfigResolution: + requested: RunConfig + effective: RunConfig + clamps: tuple[ConfigClamp, ...] = () + + @property + def was_clamped(self) -> bool: + return bool(self.clamps) + + def __post_init__(self) -> None: + object.__setattr__(self, "clamps", tuple(self.clamps)) + + +def _field_for_path(path: str) -> tuple[ConfigField, str | None]: + exact = FIELD_REGISTRY.get(path) + if exact is not None: + return exact, None + for parent, item in FIELD_REGISTRY.items(): + prefix = f"{parent}." + if item.dynamic_kind is not None and path.startswith(prefix): + dynamic_key = path[len(prefix) :] + if dynamic_key and "." not in dynamic_key: + return ( + ConfigField(path=path, kind=item.dynamic_kind), + dynamic_key, + ) + _unknown_field(path) + + +def _unknown_field(path: str) -> NoReturn: + raise ConfigurationError(_unknown_field_message(path)) + + +def _unknown_field_message(path: str) -> str: + candidates = list(FIELD_REGISTRY) + suggestion = difflib.get_close_matches(path, candidates, n=1, cutoff=0.55) + suffix = f"; did you mean {suggestion[0]!r}?" if suggestion else "" + return f"unknown configuration field {path!r}{suffix}" + + +def _is_section(path: str) -> bool: + return path in CONFIG_SECTIONS + + +def _flatten_mapping( + value: Mapping[str, object], + *, + prefix: str = "", + _errors: list[str] | None = None, +) -> dict[str, object]: + owns_errors = _errors is None + errors = [] if _errors is None else _errors + result: dict[str, object] = {} + for key, entry in value.items(): + if not isinstance(key, str) or not key: + errors.append("configuration keys must be non-empty strings") + continue + path = f"{prefix}.{key}" if prefix else key + if path in FIELD_REGISTRY: + result[path] = entry + continue + if _is_section(path): + if not isinstance(entry, Mapping): + errors.append(f"configuration section {path!r} must be a table") + continue + result.update(_flatten_mapping(entry, prefix=path, _errors=errors)) + continue + # Dotted dedicated inputs may address one dynamic map element directly. + try: + _spec, dynamic_key = _field_for_path(path) + except ConfigurationError as exc: + _extend_errors(errors, exc) + continue + if dynamic_key is None: + errors.append(_unknown_field_message(path)) + continue + result[path] = entry + if owns_errors: + _raise_errors(errors) + return result + + +def _normalize_dedicated( + value: Mapping[str, object], *, _errors: list[str] +) -> dict[str, object]: + result: dict[str, object] = {} + nested: dict[str, object] = {} + for key, entry in value.items(): + if "." in key: + result[key] = entry + else: + nested[key] = entry + if nested: + result.update(_flatten_mapping(nested, _errors=_errors)) + return result + + +def _resolve_path(value: object, base_dir: Path, path: str) -> Path: + if not isinstance(value, (str, os.PathLike)): + raise ConfigurationError(f"{path} must be a path") + candidate = Path(os.fspath(value)).expanduser() + if not candidate.is_absolute(): + candidate = base_dir / candidate + return candidate.resolve(strict=False) + + +def _coerce_process_entries(value: object, path: str) -> tuple[ProcessEntry, ...]: + if isinstance(value, (str, bytes)) or not isinstance(value, Sequence): + raise ConfigurationError(f"{path} must be a list of process entry tables") + + entries: list[ProcessEntry] = [] + errors: list[str] = [] + allowed_fields = ("expression", "name") + for index, raw_entry in enumerate(value): + entry_path = f"{path}[{index}]" + if isinstance(raw_entry, ProcessEntry): + entries.append(raw_entry) + continue + if not isinstance(raw_entry, Mapping): + errors.append(f"{entry_path} must be a table") + continue + + for key in raw_entry: + if not isinstance(key, str) or not key: + errors.append(f"{entry_path} keys must be non-empty strings") + continue + if key not in allowed_fields: + suggestion = difflib.get_close_matches( + key, allowed_fields, n=1, cutoff=0.55 + ) + suffix = f"; did you mean {suggestion[0]!r}?" if suggestion else "" + errors.append( + f"unknown configuration field {entry_path}.{key!s}{suffix}" + ) + + try: + entries.append( + ProcessEntry( + expression=cast(str, raw_entry.get("expression")), + name=cast(str | None, raw_entry.get("name")), + ) + ) + except ConfigurationError as exc: + errors.extend(f"{entry_path}: {message}" for message in exc.messages) + + _raise_errors(errors) + return tuple(entries) + + +def _coerce( + item: ConfigField, + value: object, + *, + base_dir: Path, +) -> object: + if value is None: + if item.nullable: + return None + raise ConfigurationError(f"{item.path} may not be null") + + kind = item.kind + result: object + if kind == "str": + if not isinstance(value, str): + raise ConfigurationError(f"{item.path} must be a string") + if item.path == "model.source": + from .._internal.sm_heft import is_sm_heft_alias + + if value in {"built-in-sm", "builtin_sm"} or is_sm_heft_alias(value): + result = value + else: + result = str(_resolve_path(value, base_dir, item.path)) + else: + result = value + elif kind == "path": + result = _resolve_path(value, base_dir, item.path) + elif kind == "bool": + if not isinstance(value, bool): + raise ConfigurationError(f"{item.path} must be true or false") + result = value + elif kind == "int": + if isinstance(value, bool) or not isinstance(value, int): + raise ConfigurationError(f"{item.path} must be an integer") + result = value + elif kind == "float": + if isinstance(value, bool) or not isinstance(value, (int, float)): + raise ConfigurationError(f"{item.path} must be a number") + result = float(value) + elif kind == "auto_int": + if value == "auto": + result = value + elif isinstance(value, bool) or not isinstance(value, int): + raise ConfigurationError(f"{item.path} must be 'auto' or an integer") + else: + result = value + elif kind == "auto_bool": + if value == "auto" or isinstance(value, bool): + result = value + else: + raise ConfigurationError(f"{item.path} must be 'auto', true, or false") + elif kind == "list_str": + if isinstance(value, (str, bytes)) or not isinstance(value, Sequence): + raise ConfigurationError(f"{item.path} must be a list of strings") + if not all(isinstance(entry, str) for entry in value): + raise ConfigurationError(f"{item.path} must be a list of strings") + result = tuple(cast(Sequence[str], value)) + elif kind == "list_int": + if isinstance(value, (str, bytes)) or not isinstance(value, Sequence): + raise ConfigurationError(f"{item.path} must be a list of integers") + if any( + isinstance(entry, bool) or not isinstance(entry, int) for entry in value + ): + raise ConfigurationError(f"{item.path} must be a list of integers") + result = tuple(cast(Sequence[int], value)) + elif kind == "process_entries": + result = _coerce_process_entries(value, item.path) + elif kind in ("map_list_str", "map_int"): + if not isinstance(value, Mapping): + raise ConfigurationError(f"{item.path} must be a table") + dynamic_kind = "list_str" if kind == "map_list_str" else "int" + converted: dict[str, object] = {} + for key, entry in value.items(): + if not isinstance(key, str) or not key: + raise ConfigurationError(f"{item.path} keys must be strings") + converted[key] = _coerce( + ConfigField(path=f"{item.path}.{key}", kind=dynamic_kind), + entry, + base_dir=base_dir, + ) + result = converted + else: + raise AssertionError(f"unsupported configuration kind {kind!r}") + + if item.choices and result not in item.choices: + allowed = ", ".join(repr(choice) for choice in item.choices) + raise ConfigurationError( + f"{item.path} must be one of {allowed}; got {result!r}" + ) + return result + + +def parse_override( + expression: str, + *, + base_dir: os.PathLike[str] | str | None = None, +) -> ConfigOverride: + """Parse one schema-aware ``PATH=VALUE`` command-line override.""" + + if "=" not in expression: + raise ConfigurationError( + f"override {expression!r} must use dotted.path=value syntax" + ) + raw_path, raw_value = expression.split("=", 1) + path = raw_path.strip() + if not path: + raise ConfigurationError("override path must not be empty") + item, _dynamic_key = _field_for_path(path) + value_text = raw_value.strip() + if not value_text: + raise ConfigurationError(f"override {path!r} has no value") + + # A choice spelled "none" (process.analyze_process_groups) is not null. + if value_text.lower() in ("null", "none") and value_text not in item.choices: + parsed: object = None + else: + try: + parsed = tomllib.loads(f"value = {value_text}\n")["value"] + except tomllib.TOMLDecodeError as exc: + if item.kind not in ("str", "path", "auto_int", "auto_bool"): + raise ConfigurationError( + f"invalid value for override {path!r}: {value_text!r}" + ) from exc + parsed = value_text + + root = Path.cwd() if base_dir is None else Path(base_dir) + root = root.expanduser().resolve(strict=False) + return ConfigOverride(path, _coerce(item, parsed, base_dir=root)) + + +def _apply_value(values: dict[str, object], path: str, value: object) -> None: + _item, dynamic_key = _field_for_path(path) + if dynamic_key is None: + values[path] = value + return + parent_path = path.rsplit(".", 1)[0] + current = values.get(parent_path, {}) + if not isinstance(current, Mapping): + raise ConfigurationError(f"{parent_path} must be a table") + updated = dict(current) + updated[dynamic_key] = value + values[parent_path] = updated + + +def _coerce_flat( + raw: Mapping[str, object], + *, + base_dir: Path, + _errors: list[str] | None = None, +) -> dict[str, object]: + owns_errors = _errors is None + errors = [] if _errors is None else _errors + result: dict[str, object] = {} + for path, value in raw.items(): + try: + item, _dynamic_key = _field_for_path(path) + result[path] = _coerce(item, value, base_dir=base_dir) + except ConfigurationError as exc: + _extend_errors(errors, exc) + if owns_errors: + _raise_errors(errors) + return result + + +def _build_dataclass( + cls: type[_DataclassT], prefix: str, values: Mapping[str, object] +) -> _DataclassT: + kwargs: dict[str, object] = {} + errors: list[str] = [] + hints = get_type_hints(cls) + for item in fields(cls): # type: ignore[arg-type] + path = f"{prefix}.{item.name}" if prefix else item.name + if item.metadata.get("config_section"): + try: + kwargs[item.name] = _build_dataclass(hints[item.name], path, values) + except ConfigurationError as exc: + _extend_errors(errors, exc) + else: + if path not in values: + errors.append(f"missing required configuration field {path!r}") + else: + kwargs[item.name] = values[path] + _raise_errors(errors) + return cls(**kwargs) + + +def _make_run_config(values: Mapping[str, object]) -> RunConfig: + return _build_dataclass(RunConfig, "", values) + + +def _read_card(path: Path) -> Mapping[str, object]: + try: + with path.open("rb") as stream: + parsed = tomllib.load(stream) + except OSError as exc: + raise ConfigurationError( + f"cannot read configuration card {path}: {exc}" + ) from exc + except tomllib.TOMLDecodeError as exc: + raise ConfigurationError(f"invalid TOML in {path}: {exc}") from exc + return cast(Mapping[str, object], parsed) + + +def resolve_config( + card: os.PathLike[str] | str | Mapping[str, object] | None = None, + *, + action: Action | None = None, + dedicated: Mapping[str, object] | None = None, + overrides: Iterable[str | ConfigOverride] = (), + clamps: Iterable[ClampRequest] = (), + base_dir: os.PathLike[str] | str | None = None, +) -> ConfigResolution: + """Resolve schema-v1 configuration in its normative precedence order.""" + + if isinstance(card, Mapping): + raw_card = card + root = Path.cwd() if base_dir is None else Path(base_dir) + root = root.expanduser().resolve(strict=False) + elif card is not None: + card_path = Path(os.fspath(card)).expanduser().resolve(strict=False) + raw_card = _read_card(card_path) + root = card_path.parent + else: + raw_card = {} + root = Path.cwd() if base_dir is None else Path(base_dir) + root = root.expanduser().resolve(strict=False) + + values = default_values() + card_errors: list[str] = [] + card_fields = _flatten_mapping(raw_card, _errors=card_errors) + card_flat = _coerce_flat(card_fields, base_dir=root, _errors=card_errors) + _raise_errors(card_errors) + for path, value in card_flat.items(): + _apply_value(values, path, value) + + if action is not None: + action_item = FIELD_REGISTRY["action"] + _apply_value(values, "action", _coerce(action_item, action, base_dir=root)) + + if dedicated: + dedicated_errors: list[str] = [] + dedicated_fields = _normalize_dedicated(dedicated, _errors=dedicated_errors) + dedicated_flat = _coerce_flat( + dedicated_fields, base_dir=root, _errors=dedicated_errors + ) + _raise_errors(dedicated_errors) + for path, value in dedicated_flat.items(): + _apply_value(values, path, value) + + override_errors: list[str] = [] + for raw_override in overrides: + try: + parsed_override = ( + raw_override + if isinstance(raw_override, ConfigOverride) + else parse_override(raw_override, base_dir=root) + ) + item, _dynamic_key = _field_for_path(parsed_override.path) + normalized = _coerce(item, parsed_override.value, base_dir=root) + _apply_value(values, parsed_override.path, normalized) + except ConfigurationError as exc: + _extend_errors(override_errors, exc) + _raise_errors(override_errors) + + requested = _make_run_config(values) + + effective_values = dict(values) + records: list[ConfigClamp] = [] + clamp_errors: list[str] = [] + for clamp in clamps: + try: + item, dynamic_key = _field_for_path(clamp.path) + effective_value = _coerce(item, clamp.effective, base_dir=root) + if dynamic_key is None: + requested_value = effective_values[clamp.path] + else: + parent = effective_values[clamp.path.rsplit(".", 1)[0]] + if not isinstance(parent, Mapping) or dynamic_key not in parent: + raise ConfigurationError( + f"cannot clamp missing configuration field {clamp.path!r}" + ) + requested_value = parent[dynamic_key] + _apply_value(effective_values, clamp.path, effective_value) + records.append( + ConfigClamp( + path=clamp.path, + requested=requested_value, + effective=effective_value, + reason=clamp.reason, + ) + ) + except ConfigurationError as exc: + _extend_errors(clamp_errors, exc) + _raise_errors(clamp_errors) + + effective = _make_run_config(effective_values) + effective = replace(effective, color=effective.resolved_color) + if effective.evaluator.jit.compress == "auto": + effective = replace( + effective, + evaluator=replace( + effective.evaluator, + jit=replace( + effective.evaluator.jit, + compress=effective.evaluator.resolved_jit_compress, + ), + ), + ) + return ConfigResolution( + requested=requested, + effective=effective, + clamps=tuple(records), + ) + + +def load_config( + card: os.PathLike[str] | str, + *, + dedicated: Mapping[str, object] | None = None, + overrides: Iterable[str | ConfigOverride] = (), + clamps: Iterable[ClampRequest] = (), +) -> RunConfig: + """Load a card and return its effective immutable configuration.""" + + return resolve_config( + card, + dedicated=dedicated, + overrides=overrides, + clamps=clamps, + ).effective + + +def config_to_dict(config: object) -> dict[str, object]: + """Convert a typed config to a stable JSON/TOML-friendly dictionary.""" + + if not is_dataclass(config): + raise TypeError("config_to_dict expects a configuration dataclass") + return cast(dict[str, object], _to_plain(config)) + + +def config_to_toml(config: object) -> str: + """Serialize one typed configuration as canonical reloadable TOML.""" + + try: + import tomli_w + except ImportError as exc: + raise ConfigurationError( + "TOML serialization requires the pyAmpliCol runtime dependency 'tomli-w'" + ) from exc + payload = cast(dict[str, Any], _without_none(config_to_dict(config))) + return tomli_w.dumps(payload) + + +def resolution_to_dict(resolution: ConfigResolution) -> dict[str, object]: + """Return the requested/effective configuration and every resource clamp.""" + + if not isinstance(resolution, ConfigResolution): + raise TypeError("resolution_to_dict expects a ConfigResolution") + return { + "requested": config_to_dict(resolution.requested), + "effective": config_to_dict(resolution.effective), + "adjustments": [ + { + "path": clamp.path, + "requested": _to_plain(clamp.requested), + "effective": _to_plain(clamp.effective), + "reason": clamp.reason, + } + for clamp in resolution.clamps + ], + } + + +def _to_plain(value: object) -> object: + if is_dataclass(value) and not isinstance(value, type): + return { + item.name: _to_plain(getattr(value, item.name)) for item in fields(value) + } + if isinstance(value, Path): + return str(value) + if isinstance(value, Mapping): + return {str(key): _to_plain(entry) for key, entry in value.items()} + if isinstance(value, tuple): + return [_to_plain(entry) for entry in value] + return value + + +def _without_none(value: object) -> object: + if isinstance(value, Mapping): + return { + str(key): _without_none(entry) + for key, entry in value.items() + if entry is not None + } + if isinstance(value, list): + return [_without_none(entry) for entry in value] + return value + + +__all__ = [ + "ClampRequest", + "ConfigClamp", + "ConfigOverride", + "ConfigResolution", + "config_to_dict", + "config_to_toml", + "load_config", + "parse_override", + "resolution_to_dict", + "resolve_config", +] diff --git a/pyamplicol/src/pyamplicol/correlators.py b/pyamplicol/src/pyamplicol/correlators.py new file mode 100644 index 0000000000..132c1d5929 --- /dev/null +++ b/pyamplicol/src/pyamplicol/correlators.py @@ -0,0 +1,275 @@ +# SPDX-License-Identifier: 0BSD +"""Opt-in declarations for tree-level colour and spin correlations. + +Colour connections are ordered and use all-outgoing colour charges. External +legs keep the process's one-based labels; negative labels are convenient for +auxiliary emitted partons. These are colour operations, not additional external +momenta. The ordinary Born colour metric is always available as ``"born"``. + +Spin classes declare the exact sets of vector legs that may be replaced +simultaneously. Numerical vectors are supplied to the runtime, not generation. +""" + +from __future__ import annotations + +import re +from collections.abc import Mapping, Sequence +from dataclasses import dataclass, replace +from decimal import Decimal +from typing import cast + +from pyamplicol.color.connections import ColorLeg, EmitGluon, SplitGluon +from pyamplicol.color.correlator_matrices import ColorCorrelator + +__all__ = [ + "ColorCorrelator", + "CorrelatedRequest", + "CorrelatedValue", + "CorrelatorConfig", + "EmitGluon", + "SplitGluon", +] + + +@dataclass(frozen=True) +class CorrelatedRequest: + """One labelled request passed to ``Runtime.evaluate_correlated_many``. + + ``spin_vectors=None`` inherits the runtime setter's state at the start of + the call; ``{}`` selects ordinary physical helicities for this request. + Explicit maps use the setter's broadcast/per-point vector convention. + Decimal real components, or pairs of Decimal real/imaginary components, + retain their precision until the requested arithmetic is applied. + The runtime validates and copies all requests before numerical work. + """ + + color_correlation: str = "born" + spin_vectors: Mapping[int, object] | None = None + + def __post_init__(self) -> None: + if not isinstance(self.color_correlation, str) or not self.color_correlation: + raise ValueError("a correlated request requires a colour correlation ID") + if self.spin_vectors is not None and not isinstance(self.spin_vectors, Mapping): + raise TypeError("spin vectors must map public leg labels to vectors") + + +@dataclass(frozen=True) +class CorrelatedValue: + """One possibly complex correlation, retaining the requested precision. + + ``real`` and ``imag`` are decimals. ``complex(value)`` converts to ordinary + double precision. Ordered colour connections need not give a real result; + their imaginary part must not be discarded before physical combinations. + """ + + real: Decimal + imag: Decimal = Decimal(0) + + def __complex__(self) -> complex: + return complex(float(self.real), float(self.imag)) + + +def _fields(value: object, allowed: set[str], context: str) -> Mapping[str, object]: + if not isinstance(value, Mapping): + raise TypeError(f"{context} must be an object") + unknown = set(value) - allowed + if unknown: + raise ValueError(f"unknown {context} fields: {sorted(unknown, key=str)}") + return value + + +def _sequence(value: object, context: str) -> Sequence[object]: + if isinstance(value, (str, bytes)) or not isinstance(value, Sequence): + raise TypeError(f"{context} must be a list") + return value + + +def _emission_from_json(value: object) -> EmitGluon | SplitGluon: + if not isinstance(value, Mapping): + raise TypeError("a colour connection step must be an object") + kind = value.get("kind") + if kind == "emit-gluon": + data = _fields(value, {"kind", "emitter", "emitted"}, "gluon emission") + try: + return EmitGluon(cast(int, data["emitter"]), cast(int, data["emitted"])) + except KeyError as exc: + raise ValueError(f"gluon emission is missing {exc.args[0]!r}") from exc + if kind == "split-gluon": + data = _fields( + value, {"kind", "parent", "quark", "antiquark"}, "gluon splitting" + ) + try: + return SplitGluon( + cast(int, data["parent"]), + cast(int, data["quark"]), + cast(int, data["antiquark"]), + ) + except KeyError as exc: + raise ValueError(f"gluon splitting is missing {exc.args[0]!r}") from exc + raise ValueError(f"unknown colour connection step {kind!r}") + + +@dataclass(frozen=True) +class CorrelatorConfig: + """The correlations to prepare alongside a complete tree amplitude. + + ``color_correlations`` contains the requested ``bra† ket`` connections, + keyed by unique IDs. Each side has the same number of ordered operations. + ``spin_correlations=((3,), (3, 4))`` permits replacing leg 3 alone or legs + 3 and 4 together. An empty runtime vector map always restores the ordinary + helicity sum. Each replaced leg contributes one vector state; the usual + initial-state averaging and identical-particle factors are retained. + + The first correlated executor uses the generic compiled amplitude and + direct exact colour algebra. It does not use FFT or helicity-specific + current recycling. Uncorrelated generation does not construct this object. + """ + + color_correlations: tuple[ColorCorrelator, ...] = () + spin_correlations: tuple[tuple[int, ...], ...] = () + all_color_through_order: int | None = None + + def __post_init__(self) -> None: + order = self.all_color_through_order + if order is not None and ( + type(order) is not int or order < 1 + ): + raise ValueError( + "all_color_through_order must be a positive integer" + ) + colors = tuple(self.color_correlations) + if any(not isinstance(item, ColorCorrelator) for item in colors): + raise TypeError("color_correlations must contain ColorCorrelator records") + ids = tuple(item.id for item in colors) + if "born" in ids: + raise ValueError("the colour correlation ID 'born' is reserved") + if len(set(ids)) != len(ids): + raise ValueError("colour correlation IDs must be unique") + if order is not None and any( + re.fullmatch(r"N[1-9][0-9]*LO/c[0-9]+/c[0-9]+", identifier) + for identifier in ids + ): + raise ValueError("automatic colour catalogue IDs are reserved") + classes: list[tuple[int, ...]] = [] + for group in self.spin_correlations: + legs = tuple(group) + if not legs: + raise ValueError("a spin-correlation class must contain a leg") + if any(type(leg) is not int or leg <= 0 for leg in legs): + raise ValueError("spin-correlation legs must be positive integers") + if len(set(legs)) != len(legs): + raise ValueError("a spin-correlation class repeats a leg") + canonical = tuple(sorted(legs)) + if canonical in classes: + raise ValueError("spin-correlation classes must be distinct") + classes.append(canonical) + object.__setattr__(self, "color_correlations", colors) + object.__setattr__(self, "spin_correlations", tuple(sorted(classes))) + + @classmethod + def all_color( + cls, + *, + through_order: int, + spin_correlations: tuple[tuple[int, ...], ...] = (), + ) -> CorrelatorConfig: + """Prepare every tree-soft colour connection through N^kLO. + + Generation resolves the coloured Born legs separately for each process. + Histories include emission from emitted partons and splitting of emitted + gluons into quark pairs; this is not a catalogue of dipole products. + All directed overlaps with matching final colour spaces are retained. + No splitting kernels, flavour sums or combinatorial weights are included. + ``through_order=k`` includes all orders 1 through k without a fixed cap. + The complete catalogue is nonminimal and grows rapidly with k. + """ + if type(through_order) is not int or through_order < 1: + raise ValueError("all_color_through_order must be a positive integer") + return cls( + spin_correlations=spin_correlations, + all_color_through_order=through_order, + ) + + def _resolve_color(self, legs: Sequence[ColorLeg]) -> CorrelatorConfig: + """Expand the opt-in declaration once the process representations exist.""" + if self.all_color_through_order is None: + return self + from pyamplicol.color.connection_catalogue import ( + build_color_correlator_catalogue, + ) + + return replace( + self, + color_correlations=( + *self.color_correlations, + *build_color_correlator_catalogue( + legs, through_order=self.all_color_through_order + ), + ), + all_color_through_order=None, + ) + + @property + def spin_legs(self) -> tuple[int, ...]: + """All external legs whose source vectors must remain unrestricted.""" + return tuple(sorted({leg for group in self.spin_correlations for leg in group})) + + @property + def color_requests(self) -> tuple[ColorCorrelator, ...]: + """Requested operators, including the ordinary Born overlap.""" + if self.all_color_through_order is not None: + raise ValueError( + "automatic colour requests must first be resolved for a process" + ) + return (ColorCorrelator("born"), *self.color_correlations) + + def to_json_dict(self) -> dict[str, object]: + result: dict[str, object] = { + "color_correlations": [ + item.to_json_dict() for item in self.color_correlations + ], + "spin_correlations": [list(group) for group in self.spin_correlations], + } + if self.all_color_through_order is not None: + result["all_color_through_order"] = self.all_color_through_order + return result + + @classmethod + def from_json_dict(cls, value: object) -> CorrelatorConfig: + """Read the same declaration used by Python and ``--correlators``.""" + data = _fields( + value, + {"color_correlations", "spin_correlations", "all_color_through_order"}, + "correlator declaration", + ) + colors: list[ColorCorrelator] = [] + for record in _sequence( + data.get("color_correlations", ()), "color_correlations" + ): + item = _fields(record, {"id", "bra", "ket"}, "colour correlation") + if "id" not in item: + raise ValueError("a colour correlation requires an ID") + colors.append( + ColorCorrelator( + item["id"], # type: ignore[arg-type] + tuple( + _emission_from_json(step) + for step in _sequence(item.get("bra", ()), "bra") + ), + tuple( + _emission_from_json(step) + for step in _sequence(item.get("ket", ()), "ket") + ), + ) + ) + spins = tuple( + tuple(_sequence(group, "spin-correlation class")) + for group in _sequence( + data.get("spin_correlations", ()), "spin_correlations" + ) + ) + return cls( + tuple(colors), + cast(tuple[tuple[int, ...], ...], spins), + cast(int | None, data.get("all_color_through_order")), + ) diff --git a/pyamplicol/src/pyamplicol/diagnostics.py b/pyamplicol/src/pyamplicol/diagnostics.py new file mode 100644 index 0000000000..260ce586e0 --- /dev/null +++ b/pyamplicol/src/pyamplicol/diagnostics.py @@ -0,0 +1,264 @@ +# SPDX-License-Identifier: 0BSD +"""Fast installed-package diagnostics used by ``doctor`` and ``self-test``.""" + +from __future__ import annotations + +import hashlib +import importlib +import json +import math +import shutil +import sys +from dataclasses import dataclass +from importlib import resources +from pathlib import Path +from typing import Literal + +from . import __version__ +from ._internal.versions import verify_native_module + +CheckStatus = Literal["pass", "warning", "fail"] + + +@dataclass(frozen=True, slots=True) +class DiagnosticCheck: + name: str + status: CheckStatus + detail: str + + +@dataclass(frozen=True, slots=True) +class DiagnosticReport: + package_version: str + checks: tuple[DiagnosticCheck, ...] + + @property + def ok(self) -> bool: + return all(check.status != "fail" for check in self.checks) + + +def _python_check() -> DiagnosticCheck: + version = ".".join(str(item) for item in sys.version_info[:3]) + status: CheckStatus = "pass" if sys.version_info >= (3, 11) else "fail" + return DiagnosticCheck("python", status, version) + + +def _asset_check() -> DiagnosticCheck: + root = resources.files("pyamplicol").joinpath("assets", "models") + try: + lines = ( + root.joinpath("MANIFEST.sha256").read_text(encoding="utf-8").splitlines() + ) + if not lines: + raise ValueError("empty model manifest") + for line in lines: + digest, separator, relative = line.partition(" ") + if not separator or len(digest) != 64 or not relative: + raise ValueError(f"malformed model manifest line: {line!r}") + payload = root.joinpath(*relative.split("/")) + actual = hashlib.sha256(payload.read_bytes()).hexdigest() + if actual != digest: + raise ValueError(f"model asset digest mismatch: {relative}") + except (FileNotFoundError, OSError, ValueError) as exc: + return DiagnosticCheck("model-assets", "fail", str(exc)) + return DiagnosticCheck("model-assets", "pass", f"{len(lines)} verified files") + + +def _native_check(*, required: bool) -> DiagnosticCheck: + try: + native = importlib.import_module("pyamplicol._rusticol") + verify_native_module(native) + abi_version = int(native.abi_version()) + native_version = str(native.package_version()) + except (ImportError, AttributeError, TypeError, ValueError, RuntimeError) as exc: + return DiagnosticCheck( + "rusticol-python", + "fail" if required else "warning", + f"native extension unavailable: {exc}", + ) + if abi_version != 1: + return DiagnosticCheck( + "rusticol-python", + "fail", + f"unsupported C ABI {abi_version}; expected 1", + ) + return DiagnosticCheck( + "rusticol-python", "pass", f"package {native_version}, C ABI {abi_version}" + ) + + +def _sdk_check(*, required: bool) -> DiagnosticCheck: + try: + from ._sdk.config import load_sdk_info + + info = load_sdk_info() + except (ImportError, OSError, RuntimeError, TypeError, ValueError) as exc: + return DiagnosticCheck( + "native-sdk", + "fail" if required else "warning", + f"SDK unavailable: {exc}", + ) + return DiagnosticCheck( + "native-sdk", + "pass", + f"{info.target}; ABI {info.abi_version}; {info.library.name}", + ) + + +def _complex_pair(value: object, description: str) -> complex: + if ( + not isinstance(value, list) + or len(value) != 2 + or isinstance(value[0], bool) + or isinstance(value[1], bool) + or not isinstance(value[0], (int, float)) + or not isinstance(value[1], (int, float)) + ): + raise ValueError(f"{description} must be a [real, imaginary] pair") + return complex(float(value[0]), float(value[1])) + + +def _physics_check() -> DiagnosticCheck: + try: + native = importlib.import_module("pyamplicol._rusticol") + verify_native_module(native) + target = str(native.target_info().triple) + fixture = resources.files("pyamplicol").joinpath("assets", "selftest", target) + expected_raw = json.loads( + fixture.joinpath("expected.json").read_text(encoding="utf-8") + ) + if ( + not isinstance(expected_raw, dict) + or expected_raw.get("schema_version") != 1 + ): + raise ValueError("self-test expectation must be a schema-v1 object") + artifact = fixture.joinpath(str(expected_raw.get("artifact_path", ""))) + artifact_path = Path(str(artifact)) + if not artifact_path.is_dir(): + raise ValueError(f"self-test artifact is unavailable for {target}") + + from .api.services import Runtime + + runtime = Runtime.load(artifact_path, mute_warnings=True) + process_id = str(expected_raw.get("process_id", "")) + if runtime.physics.process_id != process_id: + raise ValueError( + "self-test process metadata differs from its recorded expectation" + ) + momenta = expected_raw.get("momenta") + if not isinstance(momenta, list): + raise ValueError("self-test expectation has no momentum batch") + total = runtime.evaluate(momenta) + resolved = runtime.evaluate_resolved(momenta) + expected_total_raw = expected_raw.get("total") + if not isinstance(expected_total_raw, list): + raise ValueError("self-test expectation has no total values") + expected_total = tuple( + _complex_pair(value, f"self-test total {index}") + for index, value in enumerate(expected_total_raw) + ) + if len(total) != len(expected_total): + raise ValueError("self-test total has the wrong point count") + for index, (actual, expected) in enumerate( + zip(total, expected_total, strict=True) + ): + value = complex(actual) + if not ( + math.isclose( + value.real, expected.real, rel_tol=1.0e-12, abs_tol=1.0e-14 + ) + and math.isclose( + value.imag, expected.imag, rel_tol=1.0e-12, abs_tol=1.0e-14 + ) + ): + raise ValueError( + f"self-test total {index} is {value!r}, expected {expected!r}" + ) + reduced = tuple(complex(value) for value in resolved.total()) + for index, (actual, expected) in enumerate( + zip(reduced, expected_total, strict=True) + ): + if not ( + math.isclose( + actual.real, expected.real, rel_tol=1.0e-12, abs_tol=1.0e-14 + ) + and math.isclose( + actual.imag, expected.imag, rel_tol=1.0e-12, abs_tol=1.0e-14 + ) + ): + raise ValueError( + f"resolved self-test total {index} does not reproduce evaluate()" + ) + expected_shape = expected_raw.get("resolved_shape") + if list(resolved.shape) != expected_shape: + raise ValueError( + f"resolved self-test shape {resolved.shape!r} differs from " + f"{expected_shape!r}" + ) + except Exception as exc: + return DiagnosticCheck("physics-f64", "fail", str(exc)) + return DiagnosticCheck( + "physics-f64", + "pass", + f"{process_id}; shape {resolved.shape}; direct SymJIT on {target}", + ) + + +def _symbolica_check() -> DiagnosticCheck: + try: + from .licensing import detect_symbolica_license + + state = detect_symbolica_license(suggest=False, json_mode=True) + except (ImportError, RuntimeError) as exc: + return DiagnosticCheck("symbolica", "fail", str(exc)) + mode = "licensed" if state.licensed else "restricted single-core" + return DiagnosticCheck("symbolica", "pass", mode) + + +def _tool_check(name: str, *, required: bool) -> DiagnosticCheck: + path = shutil.which(name) + if path is None: + return DiagnosticCheck( + f"tool:{name}", + "fail" if required else "warning", + "not found on PATH", + ) + return DiagnosticCheck(f"tool:{name}", "pass", path) + + +def run_doctor() -> DiagnosticReport: + """Inspect source/install capabilities without running physics generation.""" + + checks = ( + _python_check(), + _asset_check(), + _native_check(required=False), + _sdk_check(required=False), + _symbolica_check(), + _tool_check("cc", required=False), + _tool_check("c++", required=False), + _tool_check("gfortran", required=False), + ) + return DiagnosticReport(__version__, checks) + + +def run_self_test() -> DiagnosticReport: + """Run the fast checks required of an installed binary distribution.""" + + checks = ( + _python_check(), + _asset_check(), + _native_check(required=True), + _sdk_check(required=True), + _physics_check(), + ) + return DiagnosticReport(__version__, checks) + + +__all__ = [ + "CheckStatus", + "DiagnosticCheck", + "DiagnosticReport", + "run_doctor", + "run_self_test", +] diff --git a/pyamplicol/src/pyamplicol/evaluators/__init__.py b/pyamplicol/src/pyamplicol/evaluators/__init__.py new file mode 100644 index 0000000000..e87112d250 --- /dev/null +++ b/pyamplicol/src/pyamplicol/evaluators/__init__.py @@ -0,0 +1,16 @@ +# SPDX-License-Identifier: 0BSD +from __future__ import annotations + +import importlib +from typing import Any + + +def __getattr__(name: str) -> Any: + if name != "SymbolicaEvaluatorSettings": + raise AttributeError(name) + value = getattr(importlib.import_module(".symbolica", __name__), name) + globals()[name] = value + return value + + +__all__ = ["SymbolicaEvaluatorSettings"] diff --git a/pyamplicol/src/pyamplicol/evaluators/execution_schema.py b/pyamplicol/src/pyamplicol/evaluators/execution_schema.py new file mode 100644 index 0000000000..ae7eedb8f5 --- /dev/null +++ b/pyamplicol/src/pyamplicol/evaluators/execution_schema.py @@ -0,0 +1,56 @@ +# SPDX-License-Identifier: 0BSD +"""Evaluator runtime-capability metadata shared by artifact writers.""" + +from __future__ import annotations + +from collections.abc import Iterable, Mapping, Sequence + +from .._internal.physics.types import NativeEvaluationError +from .._internal.versions import EVALUATOR_RUNTIME_CAPABILITIES + + +def evaluator_runtime_capabilities( + manifest: Mapping[str, object], +) -> tuple[str, ...]: + """Return the primary f64 capabilities required by one evaluator tree.""" + + kind = str(manifest.get("kind", "")) + if kind == "chunked-symbolica-evaluator": + chunks = manifest.get("chunks") + if isinstance(chunks, str | bytes) or not isinstance(chunks, Sequence): + raise NativeEvaluationError( + "chunked evaluator capability metadata has no chunk list" + ) + return aggregate_runtime_capabilities( + _mapping(chunk, "chunked evaluator entry") for chunk in chunks + ) + + capability = manifest.get("runtime_capability") + if ( + not isinstance(capability, str) + or capability not in EVALUATOR_RUNTIME_CAPABILITIES + ): + raise NativeEvaluationError( + f"evaluator {kind!r} has no supported runtime capability" + ) + return (capability,) + + +def aggregate_runtime_capabilities( + manifests: Iterable[Mapping[str, object]], +) -> tuple[str, ...]: + """Return a canonical union of the primary f64 evaluator capabilities.""" + + capabilities: set[str] = set() + for manifest in manifests: + capabilities.update(evaluator_runtime_capabilities(manifest)) + return tuple(sorted(capabilities)) + + +def _mapping(value: object, context: str) -> Mapping[str, object]: + if not isinstance(value, Mapping): + raise NativeEvaluationError(f"{context} is not an object") + return {str(key): item for key, item in value.items()} + + +__all__ = ["aggregate_runtime_capabilities", "evaluator_runtime_capabilities"] diff --git a/pyamplicol/src/pyamplicol/evaluators/native_direct_cpp.py b/pyamplicol/src/pyamplicol/evaluators/native_direct_cpp.py new file mode 100644 index 0000000000..d5c39aab0e --- /dev/null +++ b/pyamplicol/src/pyamplicol/evaluators/native_direct_cpp.py @@ -0,0 +1,1204 @@ +# SPDX-License-Identifier: 0BSD +"""Generate a genuine plane-native C++ leaf from a Symbolica evaluator. + +This module is deliberately a narrow producer prototype. It consumes +Symbolica's documented ``Evaluator.get_instructions()`` representation and +emits that optimized instruction stream a second time against fixed +split-complex plane/scalar descriptors. The generated DirectApplication +entry never calls the historical ``*_complexf64`` function and never builds a +dense parameter or output row. + +Only the instruction subset proved by the retained real-process prototype is +accepted. Unknown operations fail closed instead of falling back to the +dense ABI. +""" + +from __future__ import annotations + +import cmath +import hashlib +import math +import os +import re +import signal +import subprocess +import time +from collections.abc import Iterator, Mapping, Sequence +from contextlib import contextmanager +from dataclasses import dataclass +from enum import StrEnum +from pathlib import Path +from typing import Any + +from .._internal.physics.types import NativeEvaluationError +from .._internal.versions import NATIVE_COMPILED_DIRECT_APPLICATION_ABI +from .symbolica_helpers import _symbolica_instruction_program + +_C_IDENTIFIER = re.compile(r"[A-Za-z_][A-Za-z0-9_]*\Z") +_DIRECT_MARKER = "// pyAmpliCol genuine native DirectApplication producer v1" +_DIRECT_FLAGS = 0x3F +_DEFAULT_STACK_LIMIT = 64 * 1024 +_SUPPORTED_LANE_WIDTHS = frozenset({2, 4}) +_COMPILER_GATE_DIR_ENV = "PYAMPLICOL_NATIVE_COMPILER_GATE_DIR" +_COMPILER_GATE_SLOT_COUNT_ENV = "PYAMPLICOL_NATIVE_COMPILER_SLOT_COUNT" +_COMPILER_GATE_POLL_SECONDS = 0.05 +_MAX_COMPILER_GATE_SLOTS = 64 +_COMPILER_TERMINATE_GRACE_SECONDS = 0.5 +_COMPILER_KILL_GRACE_SECONDS = 5.0 + + +class NativeDirectCppParameterKind(StrEnum): + """Storage class for one complex Symbolica source parameter.""" + + COMPLEX_PLANE = "complex-plane" + REAL_PLANE = "real-plane" + COMPLEX_SCALAR = "complex-scalar" + REAL_SCALAR = "real-scalar" + + +@dataclass(frozen=True, slots=True) +class NativeDirectCppSpec: + """Cold-path code-generation contract for one fused evaluator leaf.""" + + function_name: str + parameter_kinds: tuple[NativeDirectCppParameterKind, ...] + output_count: int + target_triple: str + cpu_features: tuple[str, ...] = () + simd_lane_width: int = 2 + max_logical_stack_bytes: int = _DEFAULT_STACK_LIMIT + + def __post_init__(self) -> None: + if _C_IDENTIFIER.fullmatch(self.function_name) is None: + raise NativeEvaluationError( + "native DirectApplication function name is not a portable C identifier" + ) + if not self.parameter_kinds: + raise NativeEvaluationError( + "native DirectApplication needs at least one source parameter" + ) + if self.output_count < 1: + raise NativeEvaluationError( + "native DirectApplication needs at least one output" + ) + if not self.target_triple or "\x00" in self.target_triple: + raise NativeEvaluationError( + "native DirectApplication target triple is invalid" + ) + canonical_features = tuple(sorted(set(self.cpu_features))) + if self.cpu_features != canonical_features or any( + not feature or "\x00" in feature for feature in self.cpu_features + ): + raise NativeEvaluationError( + "native DirectApplication CPU features must be sorted and unique" + ) + if self.simd_lane_width not in _SUPPORTED_LANE_WIDTHS: + raise NativeEvaluationError( + "native DirectApplication SIMD lane width must be 2 or 4" + ) + if self.max_logical_stack_bytes < 1: + raise NativeEvaluationError( + "native DirectApplication logical stack limit must be positive" + ) + + +@dataclass(frozen=True, slots=True) +class NativeDirectCppSource: + """Rendered source and its authenticated producer metadata.""" + + source: str + target_triple: str + cpu_features: tuple[str, ...] + simd_lane_width: int + evaluator_state_sha256: str + instruction_count: int + temporary_count: int + input_plane_count: int + scalar_input_count: int + output_plane_count: int + logical_stack_bytes: int + + +@dataclass(frozen=True, slots=True) +class NativeDirectCppCompiler: + """Compiler settings matching the ordinary compiled-C++ path.""" + + executable: str = "c++" + optimization_level: int = 3 + native_arch: bool = False + extra_flags: tuple[str, ...] = () + timeout_seconds: float = 1800.0 + + def __post_init__(self) -> None: + if not self.executable or "\x00" in self.executable: + raise NativeEvaluationError("native DirectApplication compiler is invalid") + if self.optimization_level not in (0, 1, 2, 3): + raise NativeEvaluationError( + "native DirectApplication compiler optimization must be 0 through 3" + ) + if self.timeout_seconds <= 0 or not math.isfinite(self.timeout_seconds): + raise NativeEvaluationError( + "native DirectApplication compiler timeout must be finite and positive" + ) + if any(not flag or "\x00" in flag for flag in self.extra_flags): + raise NativeEvaluationError( + "native DirectApplication compiler flags are invalid" + ) + + +@dataclass(frozen=True, slots=True) +class NativeDirectCppArtifact: + """One direct-only native library produced from retained instructions.""" + + source_path: Path + library_path: Path + source: NativeDirectCppSource + compiler_command: tuple[str, ...] + compile_seconds: float + compiler_gate_wait_seconds: float = 0.0 + compiler_gate_slot_count: int | None = None + + +@dataclass(frozen=True, slots=True) +class _NativeCompilerGateLease: + wait_seconds: float + slot_count: int | None + + +@dataclass(frozen=True, slots=True) +class _ParameterAccess: + kind: NativeDirectCppParameterKind + first_index: int + second_index: int | None + + +@dataclass(frozen=True, slots=True) +class _InstructionProgram: + statements: tuple[str, ...] + instruction_count: int + temporary_count: int + constants: tuple[complex, ...] + + +def render_native_direct_cpp( + evaluator: Any, + spec: NativeDirectCppSpec, +) -> NativeDirectCppSource: + """Render one factor-free plane-native entry from a Symbolica evaluator. + + ``Evaluator.get_instructions`` is the only accepted expression source. + Generated dense C++ text is never parsed or wrapped. + """ + + get_instructions = getattr(evaluator, "get_instructions", None) + save = getattr(evaluator, "save", None) + if not callable(get_instructions) or not callable(save): + raise NativeEvaluationError( + "this Symbolica evaluator exposes no reusable instruction/state API" + ) + try: + raw_program = _symbolica_instruction_program(get_instructions()) + evaluator_state = save() + except Exception as error: + raise NativeEvaluationError( + "Symbolica could not expose its optimized evaluator instruction stream" + ) from error + if not isinstance(evaluator_state, bytes) or not evaluator_state: + raise NativeEvaluationError( + "Symbolica returned an empty or non-bytes evaluator state" + ) + + accesses, input_plane_count, scalar_input_count = _parameter_accesses( + spec.parameter_kinds + ) + program = _lower_instruction_program(raw_program, spec, accesses) + logical_stack_bytes = ( + (program.temporary_count + spec.output_count) * 2 * 8 * spec.simd_lane_width + ) + if logical_stack_bytes > spec.max_logical_stack_bytes: + raise NativeEvaluationError( + "native DirectApplication logical scratch exceeds its bounded stack " + f"contract: {logical_stack_bytes} > {spec.max_logical_stack_bytes}" + ) + + state_sha256 = hashlib.sha256(evaluator_state).hexdigest() + source = _render_translation_unit( + spec=spec, + program=program, + accesses=accesses, + input_plane_count=input_plane_count, + scalar_input_count=scalar_input_count, + evaluator_state_sha256=state_sha256, + logical_stack_bytes=logical_stack_bytes, + ) + return NativeDirectCppSource( + source=source, + target_triple=spec.target_triple, + cpu_features=spec.cpu_features, + simd_lane_width=spec.simd_lane_width, + evaluator_state_sha256=state_sha256, + instruction_count=program.instruction_count, + temporary_count=program.temporary_count, + input_plane_count=input_plane_count, + scalar_input_count=scalar_input_count, + output_plane_count=2 * spec.output_count, + logical_stack_bytes=logical_stack_bytes, + ) + + +def compile_native_direct_cpp( + evaluator: Any, + *, + spec: NativeDirectCppSpec, + compiler: NativeDirectCppCompiler, + output_source_path: Path, + output_library_path: Path, +) -> NativeDirectCppArtifact: + """Compile one genuine direct-only native leaf. + + The translation unit is generated independently from retained evaluator + instructions. It contains neither the ordinary dense source nor a call to + a dense symbol. Validation may load the original dense library separately + as an exact same-expression oracle. + """ + + rendered = render_native_direct_cpp(evaluator, spec) + output_source_path.parent.mkdir(parents=True, exist_ok=True) + output_library_path.parent.mkdir(parents=True, exist_ok=True) + try: + output_source_path.write_text(rendered.source, encoding="utf-8") + except OSError as error: + raise NativeEvaluationError( + f"could not write native DirectApplication source: {output_source_path}" + ) from error + + temporary_library = output_library_path.with_name( + f".{output_library_path.name}.tmp-{os.getpid()}" + ) + command = [ + compiler.executable, + "-std=c++17", + "-shared", + f"-O{compiler.optimization_level}", + "-fPIC", + "-ffast-math", + "-funsafe-math-optimizations", + ] + if compiler.native_arch: + command.append("-march=native") + command.extend(compiler.extra_flags) + command.extend( + [ + str(output_source_path), + "-o", + str(temporary_library), + ] + ) + with _native_compiler_gate() as compiler_gate: + # Queueing behind another compiler is operational scheduling, not + # compilation time. Start the finite subprocess timeout only after + # this worker owns one shared slot. + started = time.perf_counter() + try: + completed = _run_native_compiler( + command, + timeout_seconds=compiler.timeout_seconds, + ) + except subprocess.TimeoutExpired as error: + temporary_library.unlink(missing_ok=True) + raise NativeEvaluationError( + "native DirectApplication C++ compilation exceeded its finite " + f"{compiler.timeout_seconds:g}s timeout" + ) from error + except NativeEvaluationError: + temporary_library.unlink(missing_ok=True) + raise + except (OSError, subprocess.SubprocessError) as error: + temporary_library.unlink(missing_ok=True) + raise NativeEvaluationError( + "could not execute the native DirectApplication C++ compiler" + ) from error + except BaseException: + # Interrupts must not publish a partial library or release the + # shared compiler slot before the detached compiler tree is gone. + temporary_library.unlink(missing_ok=True) + raise + compile_seconds = time.perf_counter() - started + if completed.returncode != 0: + temporary_library.unlink(missing_ok=True) + detail = completed.stderr.strip() or completed.stdout.strip() + raise NativeEvaluationError( + "native DirectApplication C++ compilation failed" + + (f": {detail}" if detail else "") + ) + try: + os.replace(temporary_library, output_library_path) + except OSError as error: + temporary_library.unlink(missing_ok=True) + raise NativeEvaluationError( + f"could not publish native DirectApplication library: {output_library_path}" + ) from error + return NativeDirectCppArtifact( + source_path=output_source_path, + library_path=output_library_path, + source=rendered, + compiler_command=tuple(command), + compile_seconds=compile_seconds, + compiler_gate_wait_seconds=compiler_gate.wait_seconds, + compiler_gate_slot_count=compiler_gate.slot_count, + ) + + +def _run_native_compiler( + command: Sequence[str], + *, + timeout_seconds: float, +) -> subprocess.CompletedProcess[str]: + """Run one compiler in a disposable process group with finite cleanup.""" + + process = subprocess.Popen( + tuple(command), + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + start_new_session=os.name == "posix", + ) + try: + stdout, stderr = process.communicate(timeout=timeout_seconds) + except BaseException: + # The compiler owns a new session on POSIX, so caller interruption + # does not reach it automatically. Reap the complete tree before + # propagating any timeout, cancellation, or unexpected failure. + _terminate_native_compiler_tree(process) + raise + return subprocess.CompletedProcess( + tuple(command), + process.returncode, + stdout, + stderr, + ) + + +def _terminate_native_compiler_tree(process: subprocess.Popen[str]) -> None: + """Terminate and reap a timed-out compiler and all of its descendants.""" + + if os.name != "posix": + process.kill() + try: + process.communicate(timeout=_COMPILER_KILL_GRACE_SECONDS) + except subprocess.TimeoutExpired as error: + raise NativeEvaluationError( + "native DirectApplication compiler timeout cleanup failed" + ) from error + return + + process_group_id = process.pid + _signal_process_group(process_group_id, signal.SIGTERM) + try: + process.communicate(timeout=_COMPILER_TERMINATE_GRACE_SECONDS) + except subprocess.TimeoutExpired: + pass + + if _process_group_exists(process_group_id): + _signal_process_group(process_group_id, signal.SIGKILL) + try: + process.communicate(timeout=_COMPILER_KILL_GRACE_SECONDS) + except subprocess.TimeoutExpired as error: + _signal_process_group(process_group_id, signal.SIGKILL) + raise NativeEvaluationError( + "native DirectApplication compiler timeout cleanup failed" + ) from error + + deadline = time.monotonic() + _COMPILER_KILL_GRACE_SECONDS + while _process_group_exists(process_group_id): + if time.monotonic() >= deadline: + _signal_process_group(process_group_id, signal.SIGKILL) + raise NativeEvaluationError( + "native DirectApplication compiler timeout cleanup left a " + "live process group" + ) + time.sleep(0.01) + + +def _signal_process_group(process_group_id: int, signal_number: int) -> None: + try: + os.killpg(process_group_id, signal_number) + except ProcessLookupError: + return + except OSError as error: + raise NativeEvaluationError( + "native DirectApplication compiler process-group cleanup failed" + ) from error + + +def _process_group_exists(process_group_id: int) -> bool: + try: + os.killpg(process_group_id, 0) + except ProcessLookupError: + return False + except PermissionError: + return True + return True + + +@contextmanager +def _native_compiler_gate() -> Iterator[_NativeCompilerGateLease]: + """Lease one optional cross-controller native-compiler slot. + + The report scheduler injects the gate only for the EPYC campaign. Other + callers retain the historical ungated behavior. Slot paths and wait time + are deliberately operational metadata and never enter evaluator identity. + """ + + raw_directory = os.environ.get(_COMPILER_GATE_DIR_ENV) + raw_slot_count = os.environ.get(_COMPILER_GATE_SLOT_COUNT_ENV) + if raw_directory is None and raw_slot_count is None: + yield _NativeCompilerGateLease(0.0, None) + return + if raw_directory is None or raw_slot_count is None: + raise NativeEvaluationError( + "native DirectApplication compiler gate environment is incomplete" + ) + if not raw_directory or "\x00" in raw_directory: + raise NativeEvaluationError( + "native DirectApplication compiler gate directory is invalid" + ) + try: + slot_count = int(raw_slot_count) + except ValueError as error: + raise NativeEvaluationError( + "native DirectApplication compiler gate slot count is invalid" + ) from error + if not 1 <= slot_count <= _MAX_COMPILER_GATE_SLOTS: + raise NativeEvaluationError( + "native DirectApplication compiler gate slot count is invalid" + ) + directory = Path(raw_directory) + if not directory.is_absolute(): + raise NativeEvaluationError( + "native DirectApplication compiler gate directory must be absolute" + ) + + descriptors: list[int] = [] + try: + import fcntl + + directory.mkdir(parents=True, exist_ok=True) + for index in range(slot_count): + descriptors.append( + os.open( + directory / f"slot-{index:02d}.lock", + os.O_CREAT | os.O_RDWR, + 0o600, + ) + ) + except (ImportError, OSError) as error: + for descriptor in descriptors: + try: + os.close(descriptor) + except OSError: + pass + raise NativeEvaluationError( + "native DirectApplication compiler gate could not be initialized" + ) from error + + acquired: int | None = None + wait_started = time.perf_counter() + try: + while acquired is None: + for descriptor in descriptors: + try: + fcntl.flock( + descriptor, + fcntl.LOCK_EX | fcntl.LOCK_NB, + ) + except BlockingIOError: + continue + except OSError as error: + raise NativeEvaluationError( + "native DirectApplication compiler gate acquisition failed" + ) from error + acquired = descriptor + break + if acquired is None: + time.sleep(_COMPILER_GATE_POLL_SECONDS) + yield _NativeCompilerGateLease( + time.perf_counter() - wait_started, + slot_count, + ) + finally: + if acquired is not None: + try: + fcntl.flock(acquired, fcntl.LOCK_UN) + except OSError: + pass + for descriptor in descriptors: + try: + os.close(descriptor) + except OSError: + pass + + +def compiler_from_symbolica_settings( + settings: Mapping[str, object], +) -> NativeDirectCppCompiler: + """Translate the adapter's recorded settings into the direct compiler.""" + + # The direct translation is intentionally independent from Symbolica's + # dense scalar-row emitter. An ``asm`` public backend therefore reuses the + # same optimized instruction stream and compiler policy here, without + # parsing, wrapping, or calling its inline-ASM row function. + executable_value = settings.get("compiler_path") + executable = "c++" if executable_value is None else str(executable_value) + optimization = settings.get("compiled_optimization_level", 3) + if isinstance(optimization, bool) or not isinstance(optimization, int): + raise NativeEvaluationError( + "native DirectApplication compiler optimization metadata is invalid" + ) + native_arch = settings.get("compiled_native", False) + if not isinstance(native_arch, bool): + raise NativeEvaluationError( + "native DirectApplication native-architecture metadata is invalid" + ) + raw_flags = settings.get( + "effective_compiler_flags", + settings.get("compiler_flags", ()), + ) + if not isinstance(raw_flags, Sequence) or isinstance(raw_flags, (str, bytes)): + raise NativeEvaluationError( + "native DirectApplication compiler flag metadata is invalid" + ) + extra_flags = tuple(str(flag) for flag in raw_flags if str(flag) != "-std=c++17") + return NativeDirectCppCompiler( + executable=executable, + optimization_level=optimization, + native_arch=native_arch, + extra_flags=extra_flags, + ) + + +def _parameter_accesses( + kinds: Sequence[NativeDirectCppParameterKind], +) -> tuple[tuple[_ParameterAccess, ...], int, int]: + plane_index = 0 + scalar_index = 0 + accesses: list[_ParameterAccess] = [] + for raw_kind in kinds: + try: + kind = NativeDirectCppParameterKind(raw_kind) + except ValueError as error: + raise NativeEvaluationError( + f"unsupported native DirectApplication parameter kind: {raw_kind!r}" + ) from error + if kind is NativeDirectCppParameterKind.COMPLEX_PLANE: + accesses.append(_ParameterAccess(kind, plane_index, plane_index + 1)) + plane_index += 2 + elif kind is NativeDirectCppParameterKind.REAL_PLANE: + accesses.append(_ParameterAccess(kind, plane_index, None)) + plane_index += 1 + elif kind is NativeDirectCppParameterKind.COMPLEX_SCALAR: + accesses.append(_ParameterAccess(kind, scalar_index, scalar_index + 1)) + scalar_index += 2 + else: + accesses.append(_ParameterAccess(kind, scalar_index, None)) + scalar_index += 1 + return tuple(accesses), plane_index, scalar_index + + +def _lower_instruction_program( + raw_program: object, + spec: NativeDirectCppSpec, + accesses: Sequence[_ParameterAccess], +) -> _InstructionProgram: + if not isinstance(raw_program, tuple) or len(raw_program) != 3: + raise NativeEvaluationError( + "Symbolica returned an invalid evaluator instruction program" + ) + raw_instructions, raw_temporary_count, raw_constants = raw_program + if not isinstance(raw_instructions, list): + raise NativeEvaluationError("Symbolica evaluator instructions are not a list") + if ( + isinstance(raw_temporary_count, bool) + or not isinstance(raw_temporary_count, int) + or raw_temporary_count < 0 + ): + raise NativeEvaluationError("Symbolica evaluator temporary count is invalid") + if not isinstance(raw_constants, list): + raise NativeEvaluationError("Symbolica evaluator constants are not a list") + constants = tuple(_finite_complex(value) for value in raw_constants) + + written: set[tuple[str, int]] = set() + real_values: dict[tuple[str, int], bool] = {} + for index, access in enumerate(accesses): + real_values[("param", index)] = access.kind in { + NativeDirectCppParameterKind.REAL_PLANE, + NativeDirectCppParameterKind.REAL_SCALAR, + } + for index, value in enumerate(constants): + real_values[("const", index)] = value.imag == 0.0 + + def parse_reference(raw: object, *, destination: bool = False) -> tuple[str, int]: + if ( + not isinstance(raw, tuple) + or len(raw) != 2 + or not isinstance(raw[0], str) + or isinstance(raw[1], bool) + or not isinstance(raw[1], int) + or raw[1] < 0 + ): + raise NativeEvaluationError( + "Symbolica evaluator instruction contains an invalid reference" + ) + kind, index = raw + allowed = {"temp", "out"} if destination else {"param", "temp", "const", "out"} + if kind not in allowed: + raise NativeEvaluationError( + "Symbolica evaluator instruction reference kind " + f"{kind!r} is unsupported" + ) + if kind == "param" and index >= len(accesses): + raise NativeEvaluationError( + "Symbolica evaluator parameter reference is out of bounds" + ) + if kind == "temp" and index >= raw_temporary_count: + raise NativeEvaluationError( + "Symbolica evaluator temporary reference is out of bounds" + ) + if kind == "const" and index >= len(constants): + raise NativeEvaluationError( + "Symbolica evaluator constant reference is out of bounds" + ) + if kind == "out" and index >= spec.output_count: + raise NativeEvaluationError( + "Symbolica evaluator output reference is out of bounds" + ) + if not destination and kind in {"temp", "out"} and (kind, index) not in written: + raise NativeEvaluationError( + "Symbolica evaluator reads an uninitialized temporary/output" + ) + return kind, index + + def reference_expression(reference: tuple[str, int]) -> str: + kind, index = reference + if kind == "temp": + return f"temporary[{index}]" + if kind == "out": + return f"output[{index}]" + if kind == "const": + value = constants[index] + return ( + f"direct_constant({_cpp_f64(value.real)}, {_cpp_f64(value.imag)})" + ) + access = accesses[index] + if access.kind is NativeDirectCppParameterKind.COMPLEX_PLANE: + assert access.second_index is not None + return ( + "direct_complex_plane(" + f"inputs[{access.first_index}].values, " + f"inputs[{access.second_index}].values, point)" + ) + if access.kind is NativeDirectCppParameterKind.REAL_PLANE: + return ( + f"direct_real_plane(inputs[{access.first_index}].values, point)" + ) + if access.kind is NativeDirectCppParameterKind.COMPLEX_SCALAR: + assert access.second_index is not None + return ( + "direct_complex_scalar(" + f"*scalars[{access.first_index}].value, " + f"*scalars[{access.second_index}].value)" + ) + return f"direct_real_scalar(*scalars[{access.first_index}].value)" + + statements: list[str] = [] + for row_index, raw_row in enumerate(raw_instructions): + if not isinstance(raw_row, tuple) or not raw_row: + raise NativeEvaluationError( + f"Symbolica evaluator instruction {row_index} is invalid" + ) + operation = raw_row[0] + if operation in {"add", "mul"}: + if len(raw_row) != 4 or not isinstance(raw_row[2], list): + raise NativeEvaluationError( + f"Symbolica {operation} instruction has an invalid shape" + ) + destination = parse_reference(raw_row[1], destination=True) + arguments = tuple(parse_reference(item) for item in raw_row[2]) + real_count = raw_row[3] + if ( + not arguments + or isinstance(real_count, bool) + or not isinstance(real_count, int) + or real_count < 0 + or real_count > len(arguments) + ): + raise NativeEvaluationError( + f"Symbolica {operation} instruction has invalid " + "real-argument metadata" + ) + if any( + not real_values.get(argument, False) + for argument in arguments[:real_count] + ): + raise NativeEvaluationError( + f"Symbolica {operation} instruction real-argument " + "metadata is inconsistent" + ) + expression = reference_expression(arguments[0]) + helper = "direct_add" if operation == "add" else "direct_multiply" + for argument in arguments[1:]: + expression = f"{helper}({expression}, {reference_expression(argument)})" + statements.append( + f" {reference_expression(destination)} = {expression};" + ) + written.add(destination) + real_values[destination] = real_count == len(arguments) + continue + if operation == "pow": + if len(raw_row) != 5: + raise NativeEvaluationError( + "Symbolica pow instruction has an invalid shape" + ) + destination = parse_reference(raw_row[1], destination=True) + base = parse_reference(raw_row[2]) + exponent = raw_row[3] + output_is_real = raw_row[4] + if exponent != -1 or not isinstance(output_is_real, bool): + raise NativeEvaluationError( + "native DirectApplication only supports Symbolica " + "reciprocal pow(-1)" + ) + if output_is_real and not real_values.get(base, False): + raise NativeEvaluationError( + "Symbolica real reciprocal metadata is inconsistent" + ) + helper = ( + "direct_real_reciprocal" + if output_is_real + else "direct_complex_reciprocal" + ) + statements.append( + f" {reference_expression(destination)} = " + f"{helper}({reference_expression(base)});" + ) + written.add(destination) + real_values[destination] = output_is_real + continue + if operation == "assign": + if len(raw_row) != 3: + raise NativeEvaluationError( + "Symbolica assign instruction has an invalid shape" + ) + destination = parse_reference(raw_row[1], destination=True) + source = parse_reference(raw_row[2]) + statements.append( + f" {reference_expression(destination)} = " + f"{reference_expression(source)};" + ) + written.add(destination) + real_values[destination] = real_values.get(source, False) + continue + if operation == "fun" and len(raw_row) == 6: + function_name = getattr(raw_row[2], "get_name", None) + arguments = raw_row[4] + if ( + callable(function_name) + and function_name() == "symbolica::sqrt" + and raw_row[3] == [] + and isinstance(arguments, list) + and len(arguments) == 1 + and isinstance(raw_row[5], bool) + ): + destination = parse_reference(raw_row[1], destination=True) + source = parse_reference(arguments[0]) + if source[0] == "const": + # Keep the saved evaluator exact; round constant radicals + # only when emitting this f64 C++ evaluation path. + value = cmath.sqrt(constants[source[1]]) + if raw_row[5] and value.imag != 0.0: + raise NativeEvaluationError( + "Symbolica real square-root metadata is inconsistent" + ) + statements.append( + f" {reference_expression(destination)} = " + f"direct_constant({_cpp_f64(value.real)}, " + f"{_cpp_f64(value.imag)});" + ) + written.add(destination) + real_values[destination] = value.imag == 0.0 + continue + raise NativeEvaluationError( + "native DirectApplication cannot lower Symbolica operation " + f"{operation!r}; refusing a dense-row fallback" + ) + missing_outputs = [ + index for index in range(spec.output_count) if ("out", index) not in written + ] + if missing_outputs: + raise NativeEvaluationError( + "Symbolica evaluator instructions do not assign every output: " + + ", ".join(str(index) for index in missing_outputs) + ) + return _InstructionProgram( + statements=tuple(statements), + instruction_count=len(raw_instructions), + temporary_count=raw_temporary_count, + constants=constants, + ) + + +def _finite_complex(value: object) -> complex: + try: + converted = complex(value) + except (TypeError, ValueError, OverflowError) as error: + raise NativeEvaluationError( + "Symbolica evaluator constant cannot be represented as complex-f64" + ) from error + if not math.isfinite(converted.real) or not math.isfinite(converted.imag): + raise NativeEvaluationError( + "Symbolica evaluator constant is not finite complex-f64" + ) + return converted + + +def _cpp_f64(value: float) -> str: + if not math.isfinite(value): + raise NativeEvaluationError( + "native DirectApplication cannot emit non-finite constants" + ) + return value.hex() + + +def _cpp_string(value: str) -> str: + return ( + value.replace("\\", "\\\\") + .replace('"', '\\"') + .replace("\n", "\\n") + .replace("\r", "\\r") + .replace("\t", "\\t") + ) + + +def _render_translation_unit( + *, + spec: NativeDirectCppSpec, + program: _InstructionProgram, + accesses: Sequence[_ParameterAccess], + input_plane_count: int, + scalar_input_count: int, + evaluator_state_sha256: str, + logical_stack_bytes: int, +) -> str: + function_name = spec.function_name + vector_bytes = spec.simd_lane_width * 8 + output_plane_count = 2 * spec.output_count + temporary_extent = max(program.temporary_count, 1) + output_extent = max(spec.output_count, 1) + feature_string = ",".join(spec.cpu_features) + statements = "\n".join(program.statements) + stores = "\n".join( + ( + f" direct_store_lane(outputs[{2 * index}].values, point, " + f"output[{index}].real);\n" + f" direct_store_lane(outputs[{2 * index + 1}].values, point, " + f"output[{index}].imaginary);" + ) + for index in range(spec.output_count) + ) + del accesses # accesses are fully compiled into the generated statements. + return f"""{_DIRECT_MARKER} +#include +#include +#include +#include +#include +#include + +#if !defined(__clang__) && !defined(__GNUC__) +#error "pyAmpliCol native DirectApplication requires Clang or GCC vector extensions" +#endif + +namespace pyamplicol_native_direct_{function_name} {{ + +constexpr std::uint32_t kInputPlaneCount = {input_plane_count}u; +constexpr std::uint32_t kScalarInputCount = {scalar_input_count}u; +constexpr std::uint32_t kOutputPlaneCount = {output_plane_count}u; +constexpr std::uint32_t kSimdLaneWidth = {spec.simd_lane_width}u; +constexpr std::uint32_t kInstructionCount = {program.instruction_count}u; +constexpr std::uint32_t kTemporaryCount = {program.temporary_count}u; +constexpr std::uint32_t kLogicalStackBytes = {logical_stack_bytes}u; +static_assert(kLogicalStackBytes <= {spec.max_logical_stack_bytes}u); + +using DirectVector = double __attribute__((vector_size({vector_bytes}))); + +template +struct DirectComplex {{ + Lane real; + Lane imaginary; +}}; + +template +inline Lane direct_broadcast(double value) noexcept {{ + if constexpr (std::is_same_v) {{ + return value; + }} else {{ + Lane result{{}}; + for (std::size_t lane = 0; lane < sizeof(Lane) / sizeof(double); ++lane) {{ + result[lane] = value; + }} + return result; + }} +}} + +template +inline Lane direct_load_lane(const double* values, std::uint32_t point) noexcept {{ + Lane result; + std::memcpy(&result, values + point, sizeof(Lane)); + return result; +}} + +template +inline void direct_store_lane( + double* values, + std::uint32_t point, + Lane value +) noexcept {{ + std::memcpy(values + point, &value, sizeof(Lane)); +}} + +template +inline DirectComplex direct_constant(double real, double imaginary) noexcept {{ + return {{direct_broadcast(real), direct_broadcast(imaginary)}}; +}} + +template +inline DirectComplex direct_complex_plane( + const double* real, + const double* imaginary, + std::uint32_t point +) noexcept {{ + return {{direct_load_lane(real, point), + direct_load_lane(imaginary, point)}}; +}} + +template +inline DirectComplex direct_real_plane( + const double* real, + std::uint32_t point +) noexcept {{ + return {{direct_load_lane(real, point), direct_broadcast(0.0)}}; +}} + +template +inline DirectComplex direct_complex_scalar( + double real, + double imaginary +) noexcept {{ + return direct_constant(real, imaginary); +}} + +template +inline DirectComplex direct_real_scalar(double real) noexcept {{ + return direct_constant(real, 0.0); +}} + +template +inline DirectComplex direct_add( + DirectComplex left, + DirectComplex right +) noexcept {{ + return {{left.real + right.real, left.imaginary + right.imaginary}}; +}} + +template +inline DirectComplex direct_multiply( + DirectComplex left, + DirectComplex right +) noexcept {{ + return {{ + left.real * right.real - left.imaginary * right.imaginary, + left.real * right.imaginary + left.imaginary * right.real, + }}; +}} + +template +inline DirectComplex direct_real_reciprocal( + DirectComplex value +) noexcept {{ + return {{direct_broadcast(1.0) / value.real, + direct_broadcast(0.0)}}; +}} + +template +inline DirectComplex direct_complex_reciprocal( + DirectComplex value +) noexcept {{ + const Lane denominator = + value.real * value.real + value.imaginary * value.imaginary; + return {{value.real / denominator, -value.imaginary / denominator}}; +}} + +struct NativeCompiledDirectInputPlaneV1 {{ + const double* values; +}}; + +struct NativeCompiledDirectOutputPlaneV1 {{ + double* values; +}}; + +struct NativeCompiledDirectScalarV1 {{ + const double* value; +}}; + +struct NativeCompiledDirectMetadataV1 {{ + std::uint32_t abi_version; + std::uint32_t struct_size; + std::uint32_t flags; + std::uint32_t input_plane_count; + std::uint32_t scalar_input_count; + std::uint32_t output_plane_count; + std::uint32_t simd_lane_width; + std::uint32_t reserved; +}}; + +template +inline void evaluate_direct_bundle( + const NativeCompiledDirectInputPlaneV1* inputs, + const NativeCompiledDirectScalarV1* scalars, + const NativeCompiledDirectOutputPlaneV1* outputs, + std::uint32_t point +) noexcept {{ + std::array, {temporary_extent}> temporary; + std::array, {output_extent}> output; +{statements} +{stores} +}} + +}} // namespace pyamplicol_native_direct_{function_name} + +extern "C" pyamplicol_native_direct_{function_name}::NativeCompiledDirectMetadataV1 +{function_name}_direct_application_v1_metadata() noexcept {{ + using namespace pyamplicol_native_direct_{function_name}; + return {{ + 1u, + static_cast(sizeof(NativeCompiledDirectMetadataV1)), + {_DIRECT_FLAGS}u, + kInputPlaneCount, + kScalarInputCount, + kOutputPlaneCount, + kSimdLaneWidth, + 0u, + }}; +}} + +extern "C" const char* +{function_name}_direct_application_v1_target_triple() noexcept {{ + return "{_cpp_string(spec.target_triple)}"; +}} + +extern "C" const char* +{function_name}_direct_application_v1_cpu_features() noexcept {{ + return "{_cpp_string(feature_string)}"; +}} + +extern "C" const char* +{function_name}_direct_application_v1_source_sha256() noexcept {{ + return "{evaluator_state_sha256}"; +}} + +extern "C" std::uint32_t +{function_name}_direct_application_v1_logical_stack_bytes() noexcept {{ + return pyamplicol_native_direct_{function_name}::kLogicalStackBytes; +}} + +extern "C" int {function_name}_direct_application_v1( + const pyamplicol_native_direct_{function_name}:: + NativeCompiledDirectInputPlaneV1* inputs, + std::uint32_t input_count, + const pyamplicol_native_direct_{function_name}:: + NativeCompiledDirectScalarV1* scalars, + std::uint32_t scalar_count, + const pyamplicol_native_direct_{function_name}:: + NativeCompiledDirectOutputPlaneV1* outputs, + std::uint32_t output_count, + std::uint32_t point_start, + std::uint32_t point_count +) noexcept {{ + using namespace pyamplicol_native_direct_{function_name}; + try {{ + if (input_count != kInputPlaneCount || + scalar_count != kScalarInputCount || + output_count != kOutputPlaneCount) {{ + return 2; + }} + if ((input_count != 0u && inputs == nullptr) || + (scalar_count != 0u && scalars == nullptr) || + outputs == nullptr) {{ + return 1; + }} + for (std::uint32_t input = 0; input < input_count; ++input) {{ + if (inputs[input].values == nullptr) {{ + return 3; + }} + }} + for (std::uint32_t scalar = 0; scalar < scalar_count; ++scalar) {{ + if (scalars[scalar].value == nullptr) {{ + return 3; + }} + }} + for (std::uint32_t output = 0; output < output_count; ++output) {{ + if (outputs[output].values == nullptr) {{ + return 3; + }} + for (std::uint32_t input = 0; input < input_count; ++input) {{ + if (outputs[output].values == inputs[input].values) {{ + return 4; + }} + }} + for (std::uint32_t earlier = 0; earlier < output; ++earlier) {{ + if (outputs[output].values == outputs[earlier].values) {{ + return 4; + }} + }} + }} + if (point_count == 0u) {{ + return 5; + }} + const std::uint64_t point_stop_wide = + static_cast(point_start) + + static_cast(point_count); + if (point_stop_wide > + static_cast(std::numeric_limits::max())) {{ + return 5; + }} + const std::uint32_t point_stop = + static_cast(point_stop_wide); + const std::uint32_t vector_points = + (point_count / kSimdLaneWidth) * kSimdLaneWidth; + const std::uint32_t vector_stop = point_start + vector_points; + std::uint32_t point = point_start; + for (; point < vector_stop; point += kSimdLaneWidth) {{ + evaluate_direct_bundle(inputs, scalars, outputs, point); + }} + for (; point < point_stop; ++point) {{ + evaluate_direct_bundle(inputs, scalars, outputs, point); + }} + return 0; + }} catch (...) {{ + return 6; + }} +}} +""" + + +__all__ = [ + "NATIVE_COMPILED_DIRECT_APPLICATION_ABI", + "NativeDirectCppArtifact", + "NativeDirectCppCompiler", + "NativeDirectCppParameterKind", + "NativeDirectCppSource", + "NativeDirectCppSpec", + "compile_native_direct_cpp", + "compiler_from_symbolica_settings", + "render_native_direct_cpp", +] diff --git a/pyamplicol/src/pyamplicol/evaluators/native_eager_direct_cpp.py b/pyamplicol/src/pyamplicol/evaluators/native_eager_direct_cpp.py new file mode 100644 index 0000000000..803bb47bae --- /dev/null +++ b/pyamplicol/src/pyamplicol/evaluators/native_eager_direct_cpp.py @@ -0,0 +1,518 @@ +# SPDX-License-Identifier: 0BSD +"""Emit a genuine native eager DirectTable callable. + +The ordinary compiled prepared-kernel entry accepts one dense complex row. +This producer deliberately does not call it. Instead it lowers the retained +Symbolica instruction program directly into a row-outer, point-inner callable +whose inputs and ordered fanout destinations are persistent split-complex +planes. +""" + +from __future__ import annotations + +import hashlib +from dataclasses import dataclass +from typing import Any + +from .._internal.physics.types import NativeEvaluationError +from .native_direct_cpp import ( + NativeDirectCppParameterKind, + NativeDirectCppSpec, + _lower_instruction_program, + _ParameterAccess, +) +from .symbolica_helpers import _symbolica_instruction_program + +NATIVE_EAGER_DIRECT_TABLE_APPLICATION_ABI = "pyamplicol-eager-native-direct-table-v1" +_MARKER = "// pyAmpliCol genuine native eager DirectTable producer v1" +_SUPPORTED_LANES = frozenset({2, 4}) + + +@dataclass(frozen=True, slots=True) +class NativeEagerDirectTableSource: + source: str + function_name: str + evaluator_state_sha256: str + instruction_count: int + temporary_count: int + input_complex_count: int + output_complex_count: int + invocation_stride: int + attachment_stride: int + simd_lane_width: int + + +def render_native_eager_direct_table_cpp( + evaluator: Any, + *, + kernel_id: int, + input_complex_count: int, + output_complex_count: int, + target_triple: str, + simd_lane_width: int = 2, + real_parameter_indices: tuple[int, ...] = (), + evaluator_state_bytes: bytes | None = None, +) -> NativeEagerDirectTableSource: + """Render one direct-only table export from retained evaluator MIR.""" + + if kernel_id < 0 or kernel_id >= 2**32: + raise NativeEvaluationError("native eager DirectTable kernel ID exceeds u32") + if input_complex_count < 1 or output_complex_count < 1: + raise NativeEvaluationError( + "native eager DirectTable input/output widths must be positive" + ) + if simd_lane_width not in _SUPPORTED_LANES: + raise NativeEvaluationError( + "native eager DirectTable SIMD width must be 2 or 4" + ) + if not target_triple or "\x00" in target_triple: + raise NativeEvaluationError("native eager DirectTable target is invalid") + + function_name = f"pyamplicol_eager_direct_table_k{kernel_id:08x}_v1" + get_instructions = getattr(evaluator, "get_instructions", None) + save = getattr(evaluator, "save", None) + if not callable(get_instructions) or ( + evaluator_state_bytes is None and not callable(save) + ): + raise NativeEvaluationError( + "native eager DirectTable production requires retained Symbolica " + "instructions and evaluator state" + ) + try: + raw_program = _symbolica_instruction_program(get_instructions()) + evaluator_state = ( + save() if evaluator_state_bytes is None else evaluator_state_bytes + ) + except Exception as error: + raise NativeEvaluationError( + "Symbolica could not expose the eager DirectTable instruction program" + ) from error + if not isinstance(evaluator_state, bytes) or not evaluator_state: + raise NativeEvaluationError( + "Symbolica returned invalid eager DirectTable evaluator state" + ) + + real_parameters = frozenset(real_parameter_indices) + if any(index < 0 or index >= input_complex_count for index in real_parameters): + raise NativeEvaluationError( + "native eager DirectTable real parameter index is out of bounds" + ) + parameter_kinds = tuple( + ( + NativeDirectCppParameterKind.REAL_PLANE + if index in real_parameters + else NativeDirectCppParameterKind.COMPLEX_PLANE + ) + for index in range(input_complex_count) + ) + spec = NativeDirectCppSpec( + function_name=function_name, + parameter_kinds=parameter_kinds, + output_count=output_complex_count, + target_triple=target_triple, + simd_lane_width=simd_lane_width, + ) + accesses = tuple( + _ParameterAccess( + kind, + 2 * index, + ( + 2 * index + 1 + if kind is NativeDirectCppParameterKind.COMPLEX_PLANE + else None + ), + ) + for index, kind in enumerate(parameter_kinds) + ) + input_plane_count = 2 * input_complex_count + program = _lower_instruction_program(raw_program, spec, accesses) + invocation_stride = (input_plane_count + 2) * 4 + attachment_stride = (2 * output_complex_count + 2) * 4 + source = _render_translation_unit( + function_name=function_name, + statements="\n".join(program.statements), + input_plane_count=input_plane_count, + output_complex_count=output_complex_count, + temporary_count=program.temporary_count, + invocation_stride=invocation_stride, + attachment_stride=attachment_stride, + simd_lane_width=simd_lane_width, + evaluator_state_sha256=hashlib.sha256(evaluator_state).hexdigest(), + target_triple=target_triple, + ) + return NativeEagerDirectTableSource( + source=source, + function_name=function_name, + evaluator_state_sha256=hashlib.sha256(evaluator_state).hexdigest(), + instruction_count=program.instruction_count, + temporary_count=program.temporary_count, + input_complex_count=input_complex_count, + output_complex_count=output_complex_count, + invocation_stride=invocation_stride, + attachment_stride=attachment_stride, + simd_lane_width=simd_lane_width, + ) + + +def _cpp_string(value: str) -> str: + return ( + value.replace("\\", "\\\\") + .replace('"', '\\"') + .replace("\n", "\\n") + .replace("\r", "\\r") + .replace("\t", "\\t") + ) + + +def _render_translation_unit( + *, + function_name: str, + statements: str, + input_plane_count: int, + output_complex_count: int, + temporary_count: int, + invocation_stride: int, + attachment_stride: int, + simd_lane_width: int, + evaluator_state_sha256: str, + target_triple: str, +) -> str: + vector_bytes = 8 * simd_lane_width + temporary_extent = max(temporary_count, 1) + output_extent = max(output_complex_count, 1) + input_extent = max(input_plane_count, 1) + destination_stores = "\n".join( + f""" + const std::uint32_t destination_re_{output} = + read_u32(attachment + {8 * output}u); + const std::uint32_t destination_im_{output} = + read_u32(attachment + {8 * output + 4}u); + if (destination_re_{output} >= view->plane_count || + destination_im_{output} >= view->plane_count) return 2; + const DirectPlane destination_real_{output} = + view->planes[destination_re_{output}]; + const DirectPlane destination_imag_{output} = + view->planes[destination_im_{output}]; + if (destination_real_{output}.values == nullptr || + destination_imag_{output}.values == nullptr || + point_stop > destination_real_{output}.len || + point_stop > destination_imag_{output}.len) return 2; + const Lane scaled_re_{output} = + output[{output}].real * factor_re - + output[{output}].imaginary * factor_im; + const Lane scaled_im_{output} = + output[{output}].real * factor_im + + output[{output}].imaginary * factor_re; + if (operation == 0u) {{ + store_lane(destination_real_{output}.values, point, scaled_re_{output}); + store_lane(destination_imag_{output}.values, point, scaled_im_{output}); + }} else {{ + store_lane( + destination_real_{output}.values, + point, + load_lane(destination_real_{output}.values, point) + + scaled_re_{output}); + store_lane( + destination_imag_{output}.values, + point, + load_lane(destination_imag_{output}.values, point) + + scaled_im_{output}); + }}""" + for output in range(output_complex_count) + ) + return f"""{_MARKER} +#include +#include +#include +#include +#include +#include + +#if !defined(__clang__) && !defined(__GNUC__) +#error "pyAmpliCol native eager DirectTable requires Clang or GCC" +#endif + +namespace pyamplicol_native_eager_{function_name} {{ + +constexpr std::uint32_t kInputPlaneCount = {input_plane_count}u; +constexpr std::uint32_t kOutputComplexCount = {output_complex_count}u; +constexpr std::uint32_t kInvocationStride = {invocation_stride}u; +constexpr std::uint32_t kAttachmentStride = {attachment_stride}u; +constexpr std::uint32_t kSimdLaneWidth = {simd_lane_width}u; + +using DirectVector = double __attribute__((vector_size({vector_bytes}))); + +template +struct DirectComplex {{ + Lane real; + Lane imaginary; +}}; + +struct DirectPlane {{ + double* values; + std::size_t len; +}}; + +struct DirectScalar {{ + const double* value; +}}; + +struct DirectTableCallViewV1 {{ + const std::uint8_t* invocations; + std::uint32_t invocation_count; + std::uint32_t invocation_stride; + const std::uint8_t* attachments; + std::uint32_t attachment_count; + std::uint32_t attachment_stride; + const DirectPlane* planes; + std::uint32_t plane_count; + std::uint32_t scalar_count; + const DirectScalar* scalars; + const double* factor_re; + const double* factor_im; + std::uint32_t factor_count; + std::uint32_t point_start; + std::uint32_t point_count; +}}; + +struct NativeEagerDirectTableMetadataV1 {{ + std::uint32_t struct_size; + std::uint32_t abi_version; + std::uint32_t flags; + std::uint32_t invocation_stride; + std::uint32_t attachment_stride; + std::uint32_t input_complex_count; + std::uint32_t output_complex_count; + std::uint32_t simd_lane_width; + const char* application_abi; + const char* function_name; + const char* target_triple; + const char* evaluator_state_sha256; +}}; + +static_assert(sizeof(void*) == 8u); +static_assert(sizeof(DirectPlane) == 16u); + +inline std::uint32_t read_u32(const std::uint8_t* source) noexcept {{ + std::uint32_t value = 0u; + std::memcpy(&value, source, sizeof(value)); + return value; +}} + +template +inline Lane broadcast(double value) noexcept {{ + if constexpr (std::is_same_v) {{ + return value; + }} else {{ + Lane result{{}}; + for (std::size_t lane = 0; lane < sizeof(Lane) / sizeof(double); ++lane) {{ + result[lane] = value; + }} + return result; + }} +}} + +template +inline Lane load_lane(const double* values, std::uint32_t point) noexcept {{ + Lane result; + std::memcpy(&result, values + point, sizeof(Lane)); + return result; +}} + +template +inline void store_lane( + double* values, std::uint32_t point, Lane value) noexcept {{ + std::memcpy(values + point, &value, sizeof(Lane)); +}} + +template +inline DirectComplex direct_constant( + double real, double imaginary) noexcept {{ + return {{broadcast(real), broadcast(imaginary)}}; +}} + +template +inline DirectComplex direct_complex_plane( + const double* real, + const double* imaginary, + std::uint32_t point) noexcept {{ + return {{load_lane(real, point), load_lane(imaginary, point)}}; +}} + +template +inline DirectComplex direct_real_plane( + const double* real, std::uint32_t point) noexcept {{ + return {{load_lane(real, point), broadcast(0.0)}}; +}} + +template +inline DirectComplex direct_complex_scalar( + double real, double imaginary) noexcept {{ + return direct_constant(real, imaginary); +}} + +template +inline DirectComplex direct_real_scalar(double real) noexcept {{ + return direct_constant(real, 0.0); +}} + +template +inline DirectComplex direct_add( + DirectComplex left, DirectComplex right) noexcept {{ + return {{left.real + right.real, left.imaginary + right.imaginary}}; +}} + +template +inline DirectComplex direct_multiply( + DirectComplex left, DirectComplex right) noexcept {{ + return {{ + left.real * right.real - left.imaginary * right.imaginary, + left.real * right.imaginary + left.imaginary * right.real}}; +}} + +template +inline DirectComplex direct_real_reciprocal( + DirectComplex value) noexcept {{ + return {{broadcast(1.0) / value.real, broadcast(0.0)}}; +}} + +template +inline DirectComplex direct_complex_reciprocal( + DirectComplex value) noexcept {{ + const Lane denominator = + value.real * value.real + value.imaginary * value.imaginary; + return {{value.real / denominator, -value.imaginary / denominator}}; +}} + +template +int evaluate_invocation( + const DirectTableCallViewV1* view, + const std::array& inputs, + std::uint32_t attachment_start, + std::uint32_t attachment_count, + std::uint32_t point, + std::uint32_t point_stop) noexcept {{ + std::array, {temporary_extent}> temporary; + std::array, {output_extent}> output; +{statements} + for (std::uint32_t attachment_index = 0u; + attachment_index < attachment_count; + ++attachment_index) {{ + const std::uint8_t* attachment = + view->attachments + + static_cast(attachment_start + attachment_index) * + kAttachmentStride; + const std::uint32_t factor_id = + read_u32(attachment + {8 * output_complex_count}u); + const std::uint32_t operation = + read_u32(attachment + {8 * output_complex_count + 4}u); + if (factor_id >= view->factor_count || operation > 1u) return 2; + const Lane factor_re = broadcast(view->factor_re[factor_id]); + const Lane factor_im = broadcast(view->factor_im[factor_id]); +{destination_stores} + }} + return 0; +}} + +}} // namespace pyamplicol_native_eager_{function_name} + +extern "C" const pyamplicol_native_eager_{function_name}:: + NativeEagerDirectTableMetadataV1* +{function_name}_metadata_v1() noexcept {{ + using Metadata = pyamplicol_native_eager_{function_name}:: + NativeEagerDirectTableMetadataV1; + static const Metadata metadata{{ + static_cast(sizeof(Metadata)), + 1u, + 0x1fu, + {invocation_stride}u, + {attachment_stride}u, + {input_plane_count // 2}u, + {output_complex_count}u, + {simd_lane_width}u, + "{NATIVE_EAGER_DIRECT_TABLE_APPLICATION_ABI}", + "{function_name}", + "{_cpp_string(target_triple)}", + "{evaluator_state_sha256}"}}; + return &metadata; +}} + +extern "C" const char* {function_name}_target_triple() noexcept {{ + return "{_cpp_string(target_triple)}"; +}} + +extern "C" const char* {function_name}_source_sha256() noexcept {{ + return "{evaluator_state_sha256}"; +}} + +extern "C" int {function_name}( + const pyamplicol_native_eager_{function_name}:: + DirectTableCallViewV1* view) noexcept {{ + using namespace pyamplicol_native_eager_{function_name}; + try {{ + if (view == nullptr || view->invocations == nullptr || + view->planes == nullptr || view->factor_re == nullptr || + view->factor_im == nullptr || + view->invocation_stride != kInvocationStride || + view->attachment_stride != kAttachmentStride || + view->invocation_count == 0u || view->point_count == 0u || + view->scalar_count != 0u || + (view->attachment_count != 0u && view->attachments == nullptr)) {{ + return 2; + }} + const std::uint64_t point_stop_wide = + static_cast(view->point_start) + view->point_count; + if (point_stop_wide > + static_cast( + std::numeric_limits::max())) return 2; + const std::uint32_t point_stop = + static_cast(point_stop_wide); + for (std::uint32_t invocation_index = 0u; + invocation_index < view->invocation_count; + ++invocation_index) {{ + const std::uint8_t* invocation = + view->invocations + + static_cast(invocation_index) * kInvocationStride; + std::array inputs{{}}; + for (std::uint32_t input = 0u; input < kInputPlaneCount; ++input) {{ + const std::uint32_t plane_id = read_u32(invocation + 4u * input); + if (plane_id >= view->plane_count) return 2; + inputs[input] = view->planes[plane_id]; + if (inputs[input].values == nullptr || + point_stop > inputs[input].len) return 2; + }} + const std::uint32_t attachment_start = + read_u32(invocation + 4u * kInputPlaneCount); + const std::uint32_t attachment_count = + read_u32(invocation + 4u * (kInputPlaneCount + 1u)); + if (static_cast(attachment_start) + attachment_count > + view->attachment_count) return 2; + + const std::uint32_t vector_count = + (view->point_count / kSimdLaneWidth) * kSimdLaneWidth; + const std::uint32_t vector_stop = view->point_start + vector_count; + std::uint32_t point = view->point_start; + for (; point < vector_stop; point += kSimdLaneWidth) {{ + const int status = evaluate_invocation( + view, inputs, attachment_start, attachment_count, point, point_stop); + if (status != 0) return status; + }} + for (; point < point_stop; ++point) {{ + const int status = evaluate_invocation( + view, inputs, attachment_start, attachment_count, point, point_stop); + if (status != 0) return status; + }} + }} + return 0; + }} catch (...) {{ + return 3; + }} +}} +""" + + +__all__ = [ + "NATIVE_EAGER_DIRECT_TABLE_APPLICATION_ABI", + "NativeEagerDirectTableSource", + "render_native_eager_direct_table_cpp", +] diff --git a/pyamplicol/src/pyamplicol/evaluators/symbolica.py b/pyamplicol/src/pyamplicol/evaluators/symbolica.py new file mode 100644 index 0000000000..d0663000ad --- /dev/null +++ b/pyamplicol/src/pyamplicol/evaluators/symbolica.py @@ -0,0 +1,37 @@ +# SPDX-License-Identifier: 0BSD +"""Compatibility facade for Symbolica evaluator construction.""" + +from __future__ import annotations + +from .symbolica_adapters import ( + _ChunkedSymbolicaEvaluator, + _load_symbolica_evaluator_artifact, +) +from .symbolica_adapters import ( + _CompiledComplexEvaluatorAdapter as _CompiledComplexEvaluatorAdapter, +) +from .symbolica_adapters import ( + _JITSymbolicaEvaluatorAdapter as _JITSymbolicaEvaluatorAdapter, +) +from .symbolica_compile import _compile_symbolica_outputs +from .symbolica_helpers import ( + ComplexOutput, + _artifact_path_for_manifest, + _artifact_path_from_manifest, + _evaluate_complex_outputs, + _symbolica_evaluator_artifact_manifest, +) +from .symbolica_settings import ProgressCallback, SymbolicaEvaluatorSettings + +__all__ = [ + "ComplexOutput", + "ProgressCallback", + "SymbolicaEvaluatorSettings", + "_ChunkedSymbolicaEvaluator", + "_artifact_path_for_manifest", + "_artifact_path_from_manifest", + "_compile_symbolica_outputs", + "_evaluate_complex_outputs", + "_load_symbolica_evaluator_artifact", + "_symbolica_evaluator_artifact_manifest", +] diff --git a/pyamplicol/src/pyamplicol/evaluators/symbolica_adapters.py b/pyamplicol/src/pyamplicol/evaluators/symbolica_adapters.py new file mode 100644 index 0000000000..2acfb58429 --- /dev/null +++ b/pyamplicol/src/pyamplicol/evaluators/symbolica_adapters.py @@ -0,0 +1,1040 @@ +# SPDX-License-Identifier: 0BSD +"""Runtime adapters for JIT, compiled, and chunked Symbolica evaluators.""" + +from __future__ import annotations + +import hashlib +import importlib +import struct +import sys +import tempfile +import time +import uuid +from collections.abc import Mapping, Sequence +from concurrent.futures import ThreadPoolExecutor +from itertools import pairwise +from pathlib import Path +from typing import Any + +import numpy as np + +from .._internal.physics.types import NativeEvaluationError +from .._internal.versions import ( + SYMBOLICA_ASM_RUNTIME_CAPABILITY, + SYMBOLICA_CPP_RUNTIME_CAPABILITY, + SYMBOLICA_LEGACY_JIT_RUNTIME_CAPABILITY, + SYMJIT_APPLICATION_ABI, + SYMJIT_F64_RUNTIME_CAPABILITY, + SYMJIT_PLANE_APPLICATION_ABI, + verify_native_module, +) +from .execution_schema import aggregate_runtime_capabilities +from .symbolica_helpers import ( + _artifact_path_for_manifest, + _artifact_path_from_manifest, + _artifact_subdirectory, + _complex128_parameter_rows, + _evaluate_prepared_complex, + _evaluate_prepared_complex_profiled, + _safe_symbol_name, + _symbolica_evaluator_artifact_manifest, + _symbolica_instruction_program_repr, +) +from .symbolica_settings import ( + ProgressCallback, + SymbolicaEvaluatorSettings, + _report_progress, +) + + +class _JITSymbolicaEvaluatorAdapter: + def __init__( + self, + evaluator: Any, + settings: SymbolicaEvaluatorSettings, + label: str, + *, + input_len: int, + output_len: int, + progress_callback: ProgressCallback | None = None, + ) -> None: + self.input_len = int(input_len) + self.output_len = int(output_len) + self.backend = settings.backend + self.settings = settings.to_json_dict() + self.label = _safe_symbol_name(label) + self._source_evaluator = evaluator + self._progress_callback = progress_callback + self.application_path: Path | None = None + self.evaluator_state_path: Path | None = None + self.build_timing: dict[str, float] = {} + + def evaluate_complex(self, parameter_rows: Any) -> Any: + return self._source_evaluator.evaluate_complex(parameter_rows) + + def evaluate_complex_profiled( + self, parameter_rows: Any + ) -> tuple[Any, tuple[float, float, float]]: + return self._evaluate_complex_profiled_prepared( + _complex128_parameter_rows(parameter_rows) + ) + + def supports_complex_profiled(self) -> bool: + return callable( + getattr(self._source_evaluator, "evaluate_complex_profiled", None) + ) + + def evaluate(self, parameter_rows: Any) -> Any: + return self._source_evaluator.evaluate(parameter_rows) + + def _evaluate_complex_prepared(self, parameter_rows: np.ndarray) -> Any: + return self._source_evaluator.evaluate_complex(parameter_rows) + + def _evaluate_complex_profiled_prepared( + self, parameter_rows: np.ndarray + ) -> tuple[Any, tuple[float, float, float]]: + profile = getattr(self._source_evaluator, "evaluate_complex_profiled", None) + if not callable(profile): + raise NativeEvaluationError( + "this Symbolica build does not expose native complex profiling" + ) + output, timing = profile(parameter_rows) + if not isinstance(timing, tuple) or len(timing) != 3: + raise NativeEvaluationError( + "Symbolica returned an invalid native complex profile" + ) + return output, tuple(float(value) for value in timing) + + def materialize(self) -> None: + self._ensure_jit_compiled() + + def _ensure_jit_compiled(self) -> None: + dummy = np.ones((1, self.input_len), dtype=np.complex128) + self._source_evaluator.evaluate_complex(dummy) + + @classmethod + def from_artifact( + cls, + manifest: dict[str, Any], + artifact_dir: Path, + ) -> _JITSymbolicaEvaluatorAdapter: + from symbolica import Evaluator + + instance = cls.__new__(cls) + instance.input_len = int(manifest["input_len"]) + instance.output_len = int(manifest["output_len"]) + instance.backend = str(manifest["backend"]) + instance.settings = dict(manifest.get("settings", {})) + instance.label = str(manifest.get("label", "jit_symbolica_evaluator")) + application_path = manifest.get("application_path") + instance.application_path = ( + None + if application_path is None + else _artifact_path_from_manifest(str(application_path), artifact_dir) + ) + instance.evaluator_state_path = _artifact_path_from_manifest( + str(manifest["evaluator_state_path"]), + artifact_dir, + ) + instance._source_evaluator = Evaluator.load( + instance.evaluator_state_path.read_bytes() + ) + instance._progress_callback = None + instance.build_timing = {} + return instance + + def artifact_manifest(self, artifact_dir: Path) -> dict[str, Any]: + if bool(self.settings.get("jit_direct_translation")): + raise NativeEvaluationError( + "direct SymJIT translation cannot be persisted as a portable " + "process artifact; use indirect translation" + ) + _report_progress( + self._progress_callback, + stage="jit materialize", + item=self.label, + ) + materialize_started = time.perf_counter() + self._ensure_jit_compiled() + jit_compile_s = time.perf_counter() - materialize_started + _report_progress( + self._progress_callback, + stage="jit materialized", + item=f"{self.label} {jit_compile_s:.3f}s", + increment=1, + ) + evaluator_dir = _artifact_subdirectory(artifact_dir, "evaluators") + unique = uuid.uuid4().hex + application_path = evaluator_dir / f"{self.label}_{unique}.symjit" + plane_application_path = evaluator_dir / f"{self.label}_{unique}.plane.symjit" + evaluator_state_path = evaluator_dir / f"{self.label}_{unique}.evaluator.bin" + save_started = time.perf_counter() + application, element_layout = self._export_symjit_application() + application_path.write_bytes(application) + application_export_s = time.perf_counter() - save_started + plane_export_started = time.perf_counter() + ( + plane_application, + plane_source_digest, + plane_target, + ) = self._export_symjit_plane_application( + optimization_level=self._optimization_level(), + ) + plane_application_path.write_bytes(plane_application) + plane_application_export_s = time.perf_counter() - plane_export_started + state_save_started = time.perf_counter() + evaluator_state_path.write_bytes(self._source_evaluator.save()) + evaluator_save_s = time.perf_counter() - save_started + evaluator_state_save_s = time.perf_counter() - state_save_started + self.application_path = application_path + self.evaluator_state_path = evaluator_state_path + build_timing = dict(self.build_timing) + build_timing["jit_materialize_s"] = jit_compile_s + build_timing["symjit_application_export_s"] = application_export_s + build_timing["symjit_plane_application_export_s"] = plane_application_export_s + build_timing["evaluator_save_s"] = evaluator_state_save_s + build_timing["artifact_manifest_s"] = jit_compile_s + evaluator_save_s + optimization_level = self._optimization_level() + return { + "kind": "symjit-application-evaluator", + "runtime_capability": SYMJIT_F64_RUNTIME_CAPABILITY, + "backend": self.backend, + "label": self.label, + "input_len": self.input_len, + "output_len": self.output_len, + "application_path": _artifact_path_for_manifest( + application_path, artifact_dir + ), + "application_abi": SYMJIT_APPLICATION_ABI, + "plane_application": { + "application_path": _artifact_path_for_manifest( + plane_application_path, + artifact_dir, + ), + "application_abi": SYMJIT_PLANE_APPLICATION_ABI, + "storage_abi": SYMJIT_APPLICATION_ABI, + "element_layout": "split-complex-plane-major", + "descriptor_order": "inputs-re-im-then-outputs-re-im", + "input_complex_count": self.input_len, + "output_complex_count": self.output_len, + "input_plane_count": 2 * self.input_len, + "output_plane_count": 2 * self.output_len, + "compiler_type": "native", + "translation_mode": "symbolica-structured-instructions", + "optimization_level": optimization_level, + "simd": True, + "complex": True, + "fast_math": True, + "fast_complex": False, + "compression": bool(self.settings.get("jit_compress", False)), + "threading": False, + "direct_arena": True, + "source_digest": plane_source_digest, + "target": plane_target, + }, + "element_layout": element_layout, + "batch_layout": "row-major", + "compiler_type": "native", + "translation_mode": "indirect", + "optimization_level": optimization_level, + "word_bits": struct.calcsize("P") * 8, + "endianness": sys.byteorder, + "required_defuns": [], + "evaluator_state_path": _artifact_path_for_manifest( + evaluator_state_path, artifact_dir + ), + "evaluator_state_runtime_capability": ( + SYMBOLICA_LEGACY_JIT_RUNTIME_CAPABILITY + ), + "settings": self.settings, + "build_timing": build_timing, + } + + def _optimization_level(self) -> int: + optimization_level = self.settings.get("jit_optimization_level", 2) + if ( + isinstance(optimization_level, bool) + or not isinstance(optimization_level, int) + or optimization_level not in {0, 1, 2, 3} + ): + raise NativeEvaluationError("invalid SymJIT optimization-level metadata") + return optimization_level + + def _export_symjit_plane_application( + self, + *, + optimization_level: int, + ) -> tuple[bytes, str, dict[str, object]]: + """Compile a standard complex P-kernel from Symbolica instructions.""" + + instructions = getattr(self._source_evaluator, "get_instructions", None) + if not callable(instructions): + raise NativeEvaluationError( + "this Symbolica build cannot expose structured evaluator " + "instructions for a direct-arena P-kernel; install the pinned " + "pyAmpliCol candidate dependency" + ) + try: + program_repr = _symbolica_instruction_program_repr(instructions()) + except Exception as error: + raise NativeEvaluationError( + "Symbolica could not export structured evaluator instructions " + "for a direct-arena P-kernel" + ) from error + if not program_repr: + raise NativeEvaluationError( + "Symbolica returned empty direct-arena P-kernel instructions" + ) + + try: + rusticol = importlib.import_module("pyamplicol._rusticol") + verify_native_module(rusticol) + except Exception as error: + raise NativeEvaluationError( + "the Rusticol extension required to compile a direct-arena " + "P-kernel is unavailable; reinstall pyAmpliCol" + ) from error + compile_plane = getattr( + rusticol, + "_compile_symjit_plane_application_v2", + None, + ) + if not callable(compile_plane): + raise NativeEvaluationError( + "the installed Rusticol extension does not expose the SymJIT " + "2.22 plane compiler; reinstall pyAmpliCol" + ) + try: + application = compile_plane( + program_repr, + self.input_len, + self.output_len, + optimization_level, + bool(self.settings.get("jit_compress", False)), + ) + except Exception as error: + raise NativeEvaluationError( + "SymJIT could not compile the structured evaluator instructions " + "as a complex direct-arena P-kernel" + ) from error + if not isinstance(application, bytes) or not application: + raise NativeEvaluationError( + "Rusticol returned an empty or non-bytes direct-arena P-kernel" + ) + + target_info = getattr(rusticol, "target_info", None) + target: dict[str, object] = { + "word_bits": struct.calcsize("P") * 8, + "endianness": sys.byteorder, + } + if callable(target_info): + info = target_info() + triple = getattr(info, "triple", None) + cpu_features = getattr(info, "cpu_features", None) + if isinstance(triple, str) and triple: + target["triple"] = triple + if isinstance(cpu_features, list) and all( + isinstance(value, str) for value in cpu_features + ): + target["cpu_features"] = list(cpu_features) + return ( + application, + hashlib.sha256(program_repr.encode("utf-8")).hexdigest(), + target, + ) + + def _export_symjit_application(self) -> tuple[bytes, str]: + if struct.calcsize("P") * 8 != 64 or sys.byteorder != "little": + raise NativeEvaluationError( + "direct SymJIT f64 artifacts require a 64-bit little-endian host" + ) + exporter = getattr(self._source_evaluator, "export_symjit", None) + if not callable(exporter): + raise NativeEvaluationError( + "this Symbolica build cannot export a direct SymJIT f64 application; " + "install the pinned pyAmpliCol candidate dependency" + ) + try: + application = exporter(complex=True) + except Exception as error: + raise NativeEvaluationError( + "Symbolica could not export a self-contained SymJIT f64 application; " + "external evaluator functions are not supported in process artifacts" + ) from error + if not isinstance(application, bytes) or not application: + raise NativeEvaluationError( + "Symbolica returned an empty or non-bytes SymJIT application" + ) + expected_layout = _symjit_element_layout() + return application, expected_layout + + +class _CompiledComplexEvaluatorAdapter: + def __init__( + self, + evaluator: Any, + settings: SymbolicaEvaluatorSettings, + label: str, + *, + input_len: int, + output_len: int, + compile_dense: bool = True, + ) -> None: + safe_label = _safe_symbol_name(label) + unique = uuid.uuid4().hex + function_name = f"pyamplicol_{safe_label}_{unique}" + self.function_name = function_name + self.input_len = int(input_len) + self.output_len = int(output_len) + self.backend = settings.backend + self.settings = settings.to_json_dict() + self.runtime_capability = _compiled_runtime_capability(self.settings) + self.number_type = "complex" + self._source_evaluator = evaluator + self.build_timing: dict[str, float] = {} + if settings.compiled_output_dir is None: + self._tmpdir: tempfile.TemporaryDirectory[str] | None = ( + tempfile.TemporaryDirectory(prefix="pyamplicol-symbolica-") + ) + path = Path(self._tmpdir.name) + else: + self._tmpdir = None + path = Path(settings.compiled_output_dir).expanduser() + path.mkdir(parents=True, exist_ok=True) + self.source_path = path / f"{function_name}.cpp" + self.library_path = path / f"lib{function_name}" + self.evaluator_state_path: Path | None = path / f"{function_name}.evaluator.bin" + save = getattr(evaluator, "save", None) + if callable(save): + save_started = time.perf_counter() + self.evaluator_state_path.write_bytes(save()) + self.build_timing["evaluator_save_s"] = time.perf_counter() - save_started + else: + self.evaluator_state_path = None + self._compiled = None + if compile_dense: + compile_started = time.perf_counter() + compile_kwargs: dict[str, object] = { + "inline_asm": settings.compiled_inline_asm, + "optimization_level": settings.compiled_optimization_level, + "native": settings.compiled_native, + "compiler_path": settings.compiler_path, + "compiler_flags": _compiled_compiler_flags(settings), + } + custom_header = _compiled_complex_custom_header(settings) + if custom_header is not None: + compile_kwargs["custom_header"] = custom_header + self._compiled = evaluator.compile( + function_name, + str(self.source_path), + str(self.library_path), + self.number_type, + **compile_kwargs, + ) + self.build_timing["cxx_compile_s"] = time.perf_counter() - compile_started + self.native_direct_application = None + + def evaluate_complex(self, parameter_rows: Any) -> Any: + return self._evaluate_complex_prepared( + _complex128_parameter_rows(parameter_rows) + ) + + def _evaluate_complex_prepared(self, parameter_rows: np.ndarray) -> Any: + if self._compiled is not None: + return self._compiled.evaluate(parameter_rows) + if self._source_evaluator is None: + raise NativeEvaluationError( + "direct-only compiled evaluator has no generation-time source state" + ) + return self._source_evaluator.evaluate_complex(parameter_rows) + + @classmethod + def from_artifact( + cls, + manifest: dict[str, Any], + artifact_dir: Path, + ) -> _CompiledComplexEvaluatorAdapter: + if manifest.get("native_direct_application") is not None: + raise NativeEvaluationError( + "plane-native compiled process evaluators load only through " + "Rusticol Direct-Arena; no dense callable fallback is retained" + ) + from symbolica import CompiledComplexEvaluator + + instance = cls.__new__(cls) + instance._tmpdir = None + instance.function_name = str(manifest["function_name"]) + instance.input_len = int(manifest["input_len"]) + instance.output_len = int(manifest["output_len"]) + instance.backend = str(manifest["backend"]) + instance.settings = dict(manifest.get("settings", {})) + instance.runtime_capability = str( + manifest.get( + "runtime_capability", + _compiled_runtime_capability(instance.settings), + ) + ) + instance.number_type = str(manifest["number_type"]) + instance._source_evaluator = None + instance.source_path = _artifact_path_from_manifest( + str(manifest["source_path"]), + artifact_dir, + ) + instance.library_path = _artifact_path_from_manifest( + str(manifest["library_path"]), + artifact_dir, + ) + state_path = manifest.get("evaluator_state_path") + instance.evaluator_state_path = ( + None + if state_path is None + else _artifact_path_from_manifest(str(state_path), artifact_dir) + ) + instance.build_timing = {} + instance.native_direct_application = None + if instance.number_type != "complex": + raise NativeEvaluationError( + f"unsupported compiled evaluator number type: {instance.number_type!r}" + ) + instance._compiled = CompiledComplexEvaluator.load( + str(instance.library_path), + instance.function_name, + instance.input_len, + instance.output_len, + ) + return instance + + def compile_native_direct_application( + self, + parameter_kinds: Sequence[object], + *, + target_triple: str, + cpu_features: Sequence[str] = (), + simd_lane_width: int = 2, + output_dir: Path | None = None, + ) -> Any: + """Emit this compiled leaf directly against persistent arena planes. + + Stage lowering supplies semantic storage kinds; guessing model scalars + from evaluator arity is intentionally forbidden. Both public native + backends use this independent plane-native instruction emitter. For an + ``asm`` selection the historical dense scalar-row library may contain + Symbolica inline assembly, while the production DirectApplication is + deliberately regenerated as a plane loop from the same optimized + instruction stream. It never wraps or calls that dense entry point. + """ + + from .native_direct_cpp import ( + NativeDirectCppParameterKind, + NativeDirectCppSpec, + compile_native_direct_cpp, + compiler_from_symbolica_settings, + ) + + if self._source_evaluator is None: + raise NativeEvaluationError( + "native DirectApplication production requires the retained " + "Symbolica evaluator state; regenerate this compiled leaf" + ) + if self.runtime_capability not in { + SYMBOLICA_CPP_RUNTIME_CAPABILITY, + SYMBOLICA_ASM_RUNTIME_CAPABILITY, + }: + raise NativeEvaluationError( + "native DirectApplication production requires a C++ or ASM " + "compiled evaluator" + ) + optimization = self.settings.get("compiled_optimization_level", 3) + if optimization != 3: + raise NativeEvaluationError( + "native DirectApplication production requires compiled " + "optimization level 3" + ) + try: + kinds = tuple( + NativeDirectCppParameterKind(kind) for kind in parameter_kinds + ) + except (TypeError, ValueError) as error: + raise NativeEvaluationError( + "native DirectApplication stage parameter kinds are invalid" + ) from error + if len(kinds) != self.input_len: + raise NativeEvaluationError( + "native DirectApplication stage parameter kinds do not match " + "the evaluator input width" + ) + features = tuple(str(feature) for feature in cpu_features) + destination = self.source_path.parent if output_dir is None else output_dir + spec = NativeDirectCppSpec( + function_name=self.function_name, + parameter_kinds=kinds, + output_count=self.output_len, + target_triple=target_triple, + cpu_features=features, + simd_lane_width=simd_lane_width, + ) + compiler = compiler_from_symbolica_settings(self.settings) + result = compile_native_direct_cpp( + self._source_evaluator, + spec=spec, + compiler=compiler, + output_source_path=destination / f"{self.function_name}.direct.cpp", + output_library_path=destination / f"lib{self.function_name}.direct", + ) + self.native_direct_application = result + self.build_timing["native_direct_cpp_compile_s"] = result.compile_seconds + self.build_timing["native_direct_cpp_gate_wait_s"] = ( + result.compiler_gate_wait_seconds + ) + if result.compiler_gate_slot_count is not None: + self.build_timing["native_direct_cpp_gate_slots"] = float( + result.compiler_gate_slot_count + ) + return result + + # Kept as a narrow source-level alias for existing producer tests. Artifact + # production uses ``compile_native_direct_application`` exclusively. + compile_native_direct_application_prototype = compile_native_direct_application + + def artifact_manifest(self, artifact_dir: Path) -> dict[str, Any]: + direct = self.native_direct_application + source_path = self.source_path if direct is None else direct.source_path + library_path = self.library_path if direct is None else direct.library_path + manifest = { + "kind": "compiled-complex-evaluator", + "runtime_capability": self.runtime_capability, + "backend": self.backend, + "number_type": self.number_type, + "function_name": self.function_name, + "input_len": self.input_len, + "output_len": self.output_len, + "settings": self.settings, + "source_path": _artifact_path_for_manifest( + source_path, + artifact_dir, + ), + "library_path": _artifact_path_for_manifest( + library_path, + artifact_dir, + ), + "evaluator_state_path": ( + None + if self.evaluator_state_path is None + else _artifact_path_for_manifest( + self.evaluator_state_path, + artifact_dir, + ) + ), + "build_timing": dict(self.build_timing), + } + if direct is not None: + manifest["native_direct_application"] = _native_direct_application_manifest( + direct, + artifact_dir, + expected_function_name=self.function_name, + ) + return manifest + + +def _native_direct_application_manifest( + artifact: Any, + artifact_dir: Path, + *, + expected_function_name: str, +) -> dict[str, object]: + """Serialize one direct-only companion without accepting a dense wrapper.""" + + from .native_direct_cpp import ( + NATIVE_COMPILED_DIRECT_APPLICATION_ABI, + NativeDirectCppArtifact, + ) + + if not isinstance(artifact, NativeDirectCppArtifact): + raise NativeEvaluationError( + "native DirectApplication producer returned an invalid artifact" + ) + source = artifact.source + if ( + not artifact.source_path.is_file() + or not artifact.library_path.is_file() + or source.input_plane_count < 1 + or source.output_plane_count < 2 + or source.output_plane_count % 2 != 0 + or source.scalar_input_count < 0 + or source.simd_lane_width not in {2, 4} + or source.logical_stack_bytes < 1 + or source.logical_stack_bytes > 64 * 1024 + or len(source.evaluator_state_sha256) != 64 + ): + raise NativeEvaluationError( + "native DirectApplication artifact metadata is incomplete" + ) + function_name = expected_function_name + if _safe_symbol_name(function_name) != function_name: + raise NativeEvaluationError( + "native DirectApplication function identity is invalid" + ) + return { + "application_abi": NATIVE_COMPILED_DIRECT_APPLICATION_ABI, + "function_name": function_name, + "source_path": _artifact_path_for_manifest( + artifact.source_path, + artifact_dir, + ), + "library_path": _artifact_path_for_manifest( + artifact.library_path, + artifact_dir, + ), + "target": { + "triple": source.target_triple, + "cpu_features": list(source.cpu_features), + }, + "evaluator_state_sha256": source.evaluator_state_sha256, + "instruction_count": source.instruction_count, + "temporary_count": source.temporary_count, + "input_plane_count": source.input_plane_count, + "scalar_input_count": source.scalar_input_count, + "output_plane_count": source.output_plane_count, + "simd_lane_width": source.simd_lane_width, + "logical_stack_bytes": source.logical_stack_bytes, + "output_semantics": "factor-free-overwrite", + } + + +def compile_native_direct_applications( + evaluator: Any, + parameter_kinds: Sequence[object], + *, + target_triple: str, + cpu_features: Sequence[str] = (), + simd_lane_width: int = 2, +) -> bool: + """Compile every native leaf in one evaluator tree as DirectApplication. + + Chunk input maps are composed here, before artifact serialization, so each + leaf receives exactly the semantic storage classes represented by its + retained Symbolica instruction stream. + """ + + kinds = tuple(parameter_kinds) + if isinstance(evaluator, _CompiledComplexEvaluatorAdapter): + evaluator.compile_native_direct_application( + kinds, + target_triple=target_triple, + cpu_features=cpu_features, + simd_lane_width=simd_lane_width, + ) + return True + if isinstance(evaluator, _ChunkedSymbolicaEvaluator): + compiled = False + for child, indices in zip( + evaluator._evaluators, + evaluator._chunk_input_indices, + strict=True, + ): + child_kinds = tuple(kinds[index] for index in indices) + child_compiled = compile_native_direct_applications( + child, + child_kinds, + target_triple=target_triple, + cpu_features=cpu_features, + simd_lane_width=simd_lane_width, + ) + if compiled and not child_compiled: + raise NativeEvaluationError( + "native DirectApplication compilation cannot mix native " + "and non-native evaluator chunks" + ) + compiled = compiled or child_compiled + return compiled + return False + + +def _compiled_compiler_flags(settings: SymbolicaEvaluatorSettings) -> tuple[str, ...]: + flags = tuple(settings.compiler_flags) + if settings.backend == "compiled-complex": + # Symbolica's generated C++ and pyAmpliCol's injected direct-table + # support use C++17 features. Apple clang otherwise defaults to C++98. + return ("-std=c++17", *flags) + return flags + + +_CPP_COMPLEX_LITERAL_COMPAT_HEADER = r""" +template +inline Complex pyamplicol_complex_literal(Value value) { + return Complex(value); +} + +template +inline Complex pyamplicol_complex_literal(Real real, Imag imag) { + return Complex(std::real(real), std::real(imag)); +} + +#define T(...) pyamplicol_complex_literal(__VA_ARGS__) +""".strip() + + +def _compiled_complex_custom_header( + settings: SymbolicaEvaluatorSettings, +) -> str | None: + """Adapt Symbolica 2.2 nested complex literals in generated C++ sources.""" + + if settings.compiled_inline_asm != "none": + return None + return _CPP_COMPLEX_LITERAL_COMPAT_HEADER + + +def _symjit_element_layout() -> str: + return "complex-f64" + + +def _compiled_runtime_capability(settings: Mapping[str, object]) -> str: + if str(settings.get("compiled_inline_asm", "default")) == "none": + return SYMBOLICA_CPP_RUNTIME_CAPABILITY + return SYMBOLICA_ASM_RUNTIME_CAPABILITY + + +class _ChunkedSymbolicaEvaluator: + def __init__( + self, + evaluators: tuple[Any, ...], + *, + input_len: int | None = None, + chunk_input_indices: tuple[tuple[int, ...], ...] | None = None, + ) -> None: + if not evaluators: + raise NativeEvaluationError("chunked evaluator needs at least one chunk") + child_input_lengths = tuple( + _evaluator_input_len(evaluator) for evaluator in evaluators + ) + if input_len is None: + if len(set(child_input_lengths)) != 1: + raise NativeEvaluationError( + "legacy chunked evaluators need equal child input lengths" + ) + input_len = child_input_lengths[0] + if ( + isinstance(input_len, bool) + or not isinstance(input_len, int) + or input_len < 0 + ): + raise NativeEvaluationError("chunked evaluator input length is invalid") + if chunk_input_indices is None: + chunk_input_indices = tuple( + tuple(range(input_len)) for _evaluator in evaluators + ) + if len(chunk_input_indices) != len(evaluators): + raise NativeEvaluationError( + "chunked evaluator input maps do not match evaluator chunks" + ) + normalized_indices: list[tuple[int, ...]] = [] + for child_input_len, raw_indices in zip( + child_input_lengths, + chunk_input_indices, + strict=True, + ): + indices = tuple(raw_indices) + if len(indices) != child_input_len: + raise NativeEvaluationError( + "chunked evaluator input map length does not match child " + "input length" + ) + if any( + isinstance(index, bool) + or not isinstance(index, int) + or index < 0 + or index >= input_len + for index in indices + ): + raise NativeEvaluationError( + "chunked evaluator input map contains an invalid parent index" + ) + if any(left >= right for left, right in pairwise(indices)): + raise NativeEvaluationError( + "chunked evaluator input maps must be strictly increasing" + ) + normalized_indices.append(indices) + self._evaluators = evaluators + self.input_len = input_len + self._chunk_input_indices = tuple(normalized_indices) + self.build_timing: dict[str, float] = {} + + def evaluate_complex(self, parameter_rows: Any) -> Any: + return np.concatenate(self.evaluate_complex_chunks(parameter_rows), axis=1) + + def evaluate_complex_chunks(self, parameter_rows: Any) -> tuple[Any, ...]: + prepared_rows = self._prepare_parameter_rows(parameter_rows) + return tuple( + _evaluate_prepared_complex( + evaluator, + self._chunk_parameter_rows(prepared_rows, indices), + ) + for evaluator, indices in zip( + self._evaluators, + self._chunk_input_indices, + strict=True, + ) + ) + + def evaluate_complex_profiled( + self, parameter_rows: Any + ) -> tuple[tuple[Any, ...], tuple[float, float, float]]: + prepared_rows = self._prepare_parameter_rows(parameter_rows) + outputs: list[Any] = [] + profiles: list[tuple[float, float, float]] = [] + gather_s = 0.0 + for evaluator, indices in zip( + self._evaluators, + self._chunk_input_indices, + strict=True, + ): + gather_started = time.perf_counter() + chunk_rows = self._chunk_parameter_rows(prepared_rows, indices) + gather_s += time.perf_counter() - gather_started + output, profile = _evaluate_prepared_complex_profiled(evaluator, chunk_rows) + outputs.append(output) + profiles.append(profile) + return tuple(outputs), ( + gather_s + sum(profile[0] for profile in profiles), + sum(profile[1] for profile in profiles), + sum(profile[2] for profile in profiles), + ) + + def _prepare_parameter_rows(self, parameter_rows: Any) -> np.ndarray: + prepared_rows = _complex128_parameter_rows(parameter_rows) + if prepared_rows.ndim != 2 or prepared_rows.shape[1] != self.input_len: + raise NativeEvaluationError( + "chunked evaluator parameter rows have an inconsistent width" + ) + return prepared_rows + + def _chunk_parameter_rows( + self, + parameter_rows: np.ndarray, + indices: tuple[int, ...], + ) -> np.ndarray: + if len(indices) == self.input_len and all( + index == expected for expected, index in enumerate(indices) + ): + return parameter_rows + return np.ascontiguousarray(parameter_rows[:, indices]) + + def supports_complex_profiled(self) -> bool: + return all( + bool(getattr(evaluator, "supports_complex_profiled", lambda: False)()) + for evaluator in self._evaluators + ) + + @classmethod + def from_artifact( + cls, + manifest: dict[str, Any], + artifact_dir: Path, + ) -> _ChunkedSymbolicaEvaluator: + chunks = manifest.get("chunks") + if not isinstance(chunks, list): + raise NativeEvaluationError("chunked evaluator artifact is missing chunks") + raw_input_indices = manifest.get("chunk_input_indices") + chunk_input_indices = None + if raw_input_indices is not None: + if not isinstance(raw_input_indices, list): + raise NativeEvaluationError( + "chunked evaluator artifact has invalid input maps" + ) + parsed_indices: list[tuple[int, ...]] = [] + for indices in raw_input_indices: + if not isinstance(indices, list): + raise NativeEvaluationError( + "chunked evaluator artifact has invalid input maps" + ) + parsed_indices.append(tuple(indices)) + chunk_input_indices = tuple(parsed_indices) + raw_input_len = manifest.get("input_len") + if raw_input_len is not None and ( + isinstance(raw_input_len, bool) or not isinstance(raw_input_len, int) + ): + raise NativeEvaluationError( + "chunked evaluator artifact has an invalid input length" + ) + return cls( + tuple( + _load_symbolica_evaluator_artifact(chunk, artifact_dir) + for chunk in chunks + ), + input_len=raw_input_len, + chunk_input_indices=chunk_input_indices, + ) + + def artifact_manifest(self, artifact_dir: Path) -> dict[str, Any]: + worker_counts = [] + for evaluator in self._evaluators: + settings = getattr(evaluator, "settings", None) + if isinstance(settings, Mapping): + workers = settings.get("compiled_chunk_compile_workers") + if isinstance(workers, int): + worker_counts.append(workers) + workers = min(max(worker_counts, default=1), len(self._evaluators)) + if workers <= 1: + chunks = [ + _symbolica_evaluator_artifact_manifest(evaluator, artifact_dir) + for evaluator in self._evaluators + ] + else: + with ThreadPoolExecutor(max_workers=workers) as executor: + futures = [ + executor.submit( + _symbolica_evaluator_artifact_manifest, + evaluator, + artifact_dir, + ) + for evaluator in self._evaluators + ] + chunks = [future.result() for future in futures] + timing_totals = dict(self.build_timing) + timing_totals["chunk_count"] = float(len(chunks)) + for chunk in chunks: + chunk_timing = ( + chunk.get("build_timing") if isinstance(chunk, dict) else None + ) + if not isinstance(chunk_timing, Mapping): + continue + for key, value in chunk_timing.items(): + if isinstance(value, (float, int)): + timing_totals[str(key)] = timing_totals.get(str(key), 0.0) + float( + value + ) + return { + "kind": "chunked-symbolica-evaluator", + "input_len": self.input_len, + "chunk_input_indices": [ + list(indices) for indices in self._chunk_input_indices + ], + "chunks": chunks, + "required_runtime_capabilities": list( + aggregate_runtime_capabilities(chunks) + ), + "build_timing": timing_totals, + } + + +def _evaluator_input_len(evaluator: Any) -> int: + value = getattr(evaluator, "input_len", None) + if isinstance(value, bool) or not isinstance(value, int) or value < 0: + raise NativeEvaluationError("evaluator chunk has no valid input length") + return value + + +def _load_symbolica_evaluator_artifact( + manifest: Any, + artifact_dir: Path, +) -> Any: + if not isinstance(manifest, dict): + raise NativeEvaluationError("compiled evaluator artifact entry is invalid") + kind = manifest.get("kind") + if kind in {"jit-symbolica-evaluator", "symjit-application-evaluator"}: + return _JITSymbolicaEvaluatorAdapter.from_artifact(manifest, artifact_dir) + if kind == "compiled-complex-evaluator": + return _CompiledComplexEvaluatorAdapter.from_artifact(manifest, artifact_dir) + if kind == "chunked-symbolica-evaluator": + return _ChunkedSymbolicaEvaluator.from_artifact(manifest, artifact_dir) + raise NativeEvaluationError(f"unsupported evaluator artifact kind: {kind!r}") diff --git a/pyamplicol/src/pyamplicol/evaluators/symbolica_compile.py b/pyamplicol/src/pyamplicol/evaluators/symbolica_compile.py new file mode 100644 index 0000000000..04187a2a34 --- /dev/null +++ b/pyamplicol/src/pyamplicol/evaluators/symbolica_compile.py @@ -0,0 +1,584 @@ +# SPDX-License-Identifier: 0BSD +"""Compilation pipeline for Symbolica evaluator outputs.""" + +from __future__ import annotations + +import threading +import time +from collections.abc import Mapping, Sequence +from concurrent.futures import ThreadPoolExecutor +from dataclasses import replace +from pathlib import Path +from typing import Any + +from .._internal.physics.types import NativeEvaluationError +from .symbolica_adapters import ( + _ChunkedSymbolicaEvaluator, + _CompiledComplexEvaluatorAdapter, + _JITSymbolicaEvaluatorAdapter, +) +from .symbolica_helpers import _progress_outputs, _safe_symbol_name +from .symbolica_settings import ( + ProgressCallback, + SymbolicaEvaluatorSettings, + _report_progress, +) + + +def _compile_symbolica_outputs( + outputs: tuple[Any, ...], + params: list[Any], + *, + merge_evaluators_strategy: bool, + verbose_evaluator_build: bool, + functions: Mapping[tuple[Any, tuple[Any, ...]], Any] | None = None, + real_params: Sequence[int] = (), + symbolica_settings: SymbolicaEvaluatorSettings | None = None, + jit_compile: bool = True, + label: str = "symbolica", + progress_callback: ProgressCallback | None = None, + output_partitions: Sequence[tuple[int, int]] = (), + native_direct_only: bool = False, +) -> Any: + if not outputs: + raise NativeEvaluationError("cannot build evaluator with zero outputs") + settings = symbolica_settings or SymbolicaEvaluatorSettings() + progress_stage = _symbolica_progress_stage(settings, jit_compile=jit_compile) + _report_progress( + progress_callback, + stage=progress_stage, + item=f"{label} prepare {len(outputs)}", + ) + total_started = time.perf_counter() + prepare_started = time.perf_counter() + outputs = tuple(_prepare_symbolica_output(output, settings) for output in outputs) + output_prepare_s = time.perf_counter() - prepare_started + chunk_size = settings.compiled_output_chunk_size + chunk_ranges = _partitioned_output_chunk_ranges( + len(outputs), + chunk_size=chunk_size, + output_partitions=output_partitions, + ) + if len(chunk_ranges) > 1: + chunk_output_dir = ( + None + if settings.compiled_output_dir is None + else str(Path(settings.compiled_output_dir) / _safe_symbol_name(label)) + ) + unchunked_settings = replace(settings, compiled_output_chunk_size=None) + if chunk_output_dir is not None: + unchunked_settings = replace( + unchunked_settings, + compiled_output_dir=chunk_output_dir, + ) + + def compile_chunk( + chunk_index: int, + start: int, + stop: int, + ) -> tuple[Any, tuple[int, ...]]: + chunk_outputs = outputs[start:stop] + chunk_input_indices = _chunk_parameter_indices( + chunk_outputs, + params, + functions=functions, + ) + chunk_params = [params[index] for index in chunk_input_indices] + parent_real_params = set(real_params) + chunk_real_params = tuple( + local_index + for local_index, parent_index in enumerate(chunk_input_indices) + if parent_index in parent_real_params + ) + _report_progress( + progress_callback, + stage=progress_stage, + item=( + f"{label} chunk {chunk_index + 1}/{len(chunk_ranges)} " + f"p={len(chunk_params)}/{len(params)}" + ), + ) + return ( + _compile_symbolica_outputs( + chunk_outputs, + chunk_params, + merge_evaluators_strategy=merge_evaluators_strategy, + verbose_evaluator_build=verbose_evaluator_build, + functions=functions, + real_params=chunk_real_params, + symbolica_settings=unchunked_settings, + jit_compile=jit_compile, + label=f"{label}_chunk_{chunk_index}", + progress_callback=progress_callback, + output_partitions=(), + native_direct_only=native_direct_only, + ), + chunk_input_indices, + ) + + workers = min(settings.compiled_chunk_compile_workers, len(chunk_ranges)) + if workers <= 1: + compiled_chunks = [ + compile_chunk(chunk_index, start, stop) + for chunk_index, (start, stop) in enumerate(chunk_ranges) + ] + else: + with ThreadPoolExecutor(max_workers=workers) as executor: + futures = [ + executor.submit(compile_chunk, chunk_index, start, stop) + for chunk_index, (start, stop) in enumerate(chunk_ranges) + ] + compiled_chunks = [future.result() for future in futures] + chunks, chunk_input_indices = zip(*compiled_chunks, strict=True) + chunked = _ChunkedSymbolicaEvaluator( + tuple(chunks), + input_len=len(params), + chunk_input_indices=tuple(chunk_input_indices), + ) + chunked.build_timing = { + "output_prepare_s": output_prepare_s, + "symbolica_evaluator_build_s": time.perf_counter() - total_started, + } + return chunked + evaluator_kwargs = _symbolica_evaluator_kwargs( + settings, + verbose=verbose_evaluator_build, + jit_compile=jit_compile, + ) + function_kwargs: dict[str, Any] = {} + if functions: + from symbolica import FunctionDefinition + + # Process kernels retain one flat instruction program, including bodies + # of tagged functions and functions that close over model parameters. + function_kwargs["functions"] = [ + FunctionDefinition(function, arguments, body, inlining="always") + for (function, arguments), body in functions.items() + ] + if merge_evaluators_strategy: + _report_progress( + progress_callback, + stage=progress_stage, + item=f"{label} evaluator 1/{len(outputs)}", + ) + _report_jit_boundary( + progress_callback, + settings, + jit_compile=jit_compile, + phase="initialize", + item=f"{label} eval 1/{len(outputs)} p={len(params)}", + ) + with _JITBoundaryHeartbeat( + progress_callback, + settings, + jit_compile=jit_compile, + phase="initialize", + item=f"{label} eval 1/{len(outputs)} p={len(params)}", + ): + evaluator_started = time.perf_counter() + evaluator = outputs[0].evaluator( + params, + **function_kwargs, + **evaluator_kwargs, + ) + evaluator_construct_s = time.perf_counter() - evaluator_started + _report_jit_boundary( + progress_callback, + settings, + jit_compile=jit_compile, + phase="returned", + item=f"{label} eval 1/{len(outputs)}", + ) + for expression in _progress_outputs( + outputs[1:], + enabled=verbose_evaluator_build, + ): + _report_progress( + progress_callback, + stage=progress_stage, + item=f"{label} merge", + ) + _report_jit_boundary( + progress_callback, + settings, + jit_compile=jit_compile, + phase="initialize", + item=f"{label} merge p={len(params)}", + ) + with _JITBoundaryHeartbeat( + progress_callback, + settings, + jit_compile=jit_compile, + phase="initialize", + item=f"{label} merge p={len(params)}", + ): + merge_construct_started = time.perf_counter() + other = expression.evaluator( + params, + **function_kwargs, + **evaluator_kwargs, + ) + evaluator_construct_s += time.perf_counter() - merge_construct_started + _report_jit_boundary( + progress_callback, + settings, + jit_compile=jit_compile, + phase="returned", + item=f"{label} merge", + ) + evaluator.merge( + other, + cpe_iterations=( + 1 if settings.cpe_iterations is None else settings.cpe_iterations + ), + ) + if real_params: + real_params_started = time.perf_counter() + _report_jit_boundary( + progress_callback, + settings, + jit_compile=jit_compile, + phase="real params", + item=f"{label} real={len(real_params)}", + ) + evaluator.set_real_params( + list(real_params), + sqrt_real=settings.real_param_sqrt_real, + log_real=settings.real_param_log_real, + powf_real=settings.real_param_powf_real, + real_if_args_real=settings.real_param_real_if_args_real, + verbose=verbose_evaluator_build, + ) + real_params_s = time.perf_counter() - real_params_started + else: + real_params_s = 0.0 + adapter = _finalize_symbolica_evaluator( + evaluator, + settings, + label, + input_len=len(params), + output_len=len(outputs), + progress_callback=progress_callback, + native_direct_only=native_direct_only, + ) + _set_evaluator_build_timing( + adapter, + { + "output_prepare_s": output_prepare_s, + "evaluator_construct_s": evaluator_construct_s, + "real_params_s": real_params_s, + "symbolica_evaluator_build_s": time.perf_counter() - total_started, + }, + ) + return adapter + + from symbolica import Expression + + _report_progress( + progress_callback, + stage=progress_stage, + item=f"{label} evaluator {len(outputs)}", + ) + _report_jit_boundary( + progress_callback, + settings, + jit_compile=jit_compile, + phase="initialize", + item=f"{label} out={len(outputs)} p={len(params)}", + ) + with _JITBoundaryHeartbeat( + progress_callback, + settings, + jit_compile=jit_compile, + phase="initialize", + item=f"{label} out={len(outputs)} p={len(params)}", + ): + evaluator_started = time.perf_counter() + evaluator = Expression.evaluator_multiple( + outputs, + params, + **function_kwargs, + **evaluator_kwargs, + ) + evaluator_construct_s = time.perf_counter() - evaluator_started + _report_jit_boundary( + progress_callback, + settings, + jit_compile=jit_compile, + phase="returned", + item=f"{label} out={len(outputs)}", + ) + if real_params: + real_params_started = time.perf_counter() + _report_jit_boundary( + progress_callback, + settings, + jit_compile=jit_compile, + phase="real params", + item=f"{label} real={len(real_params)}", + ) + evaluator.set_real_params( + list(real_params), + sqrt_real=settings.real_param_sqrt_real, + log_real=settings.real_param_log_real, + powf_real=settings.real_param_powf_real, + real_if_args_real=settings.real_param_real_if_args_real, + verbose=verbose_evaluator_build, + ) + real_params_s = time.perf_counter() - real_params_started + else: + real_params_s = 0.0 + adapter = _finalize_symbolica_evaluator( + evaluator, + settings, + label, + input_len=len(params), + output_len=len(outputs), + progress_callback=progress_callback, + native_direct_only=native_direct_only, + ) + _set_evaluator_build_timing( + adapter, + { + "output_prepare_s": output_prepare_s, + "evaluator_construct_s": evaluator_construct_s, + "real_params_s": real_params_s, + "symbolica_evaluator_build_s": time.perf_counter() - total_started, + }, + ) + return adapter + + +def _partitioned_output_chunk_ranges( + output_count: int, + *, + chunk_size: int | None, + output_partitions: Sequence[tuple[int, int]], +) -> tuple[tuple[int, int], ...]: + if output_count < 1: + raise NativeEvaluationError("cannot partition zero evaluator outputs") + partitions = tuple(output_partitions) or ((0, output_count),) + expected_start = 0 + for start, stop in partitions: + if start != expected_start or stop <= start or stop > output_count: + raise NativeEvaluationError( + "evaluator output partitions must be contiguous and exhaustive" + ) + expected_start = stop + if expected_start != output_count: + raise NativeEvaluationError( + "evaluator output partitions must cover every output" + ) + + ranges: list[tuple[int, int]] = [] + for start, stop in partitions: + if chunk_size is None: + ranges.append((start, stop)) + continue + if chunk_size < 1: + raise NativeEvaluationError("evaluator output chunk size must be positive") + for chunk_start in range(start, stop, chunk_size): + ranges.append((chunk_start, min(chunk_start + chunk_size, stop))) + return tuple(ranges) + + +def _chunk_parameter_indices( + outputs: Sequence[Any], + params: Sequence[Any], + *, + functions: Mapping[tuple[Any, tuple[Any, ...]], Any] | None = None, +) -> tuple[int, ...]: + """Return parent parameter indices needed by one output chunk. + + Symbolica already exposes structural symbol discovery, including symbols in + function arguments. Function bodies are included + conservatively so a model kernel that closes over a runtime parameter can + never be under-specified. Parent ordering is retained to keep generated + evaluator signatures and real-parameter metadata deterministic. + """ + + used_symbols: set[Any] = set() + for expression in outputs: + used_symbols.update(_expression_symbols(expression)) + if functions: + for body in functions.values(): + used_symbols.update(_expression_symbols(body)) + return tuple( + index for index, parameter in enumerate(params) if parameter in used_symbols + ) + + +def _expression_symbols(expression: Any) -> set[Any]: + getter = getattr(expression, "get_all_symbols", None) + if not callable(getter): + raise NativeEvaluationError( + "Symbolica expression does not expose structural symbol discovery" + ) + return set(getter(False)) + + +def _set_evaluator_build_timing(evaluator: Any, timing: Mapping[str, float]) -> None: + try: + current = getattr(evaluator, "build_timing", {}) + if not isinstance(current, dict): + current = {} + current.update({str(key): float(value) for key, value in timing.items()}) + evaluator.build_timing = current + except Exception: + return + + +def _report_jit_boundary( + progress_callback: ProgressCallback | None, + settings: SymbolicaEvaluatorSettings, + *, + jit_compile: bool, + phase: str, + item: str, +) -> None: + if settings.backend != "jit" or not jit_compile: + return + _report_progress( + progress_callback, + stage=f"jit {phase}", + item=item, + ) + + +class _JITBoundaryHeartbeat: + """Emit progress while Symbolica is inside a blocking JIT build call.""" + + def __init__( + self, + progress_callback: ProgressCallback | None, + settings: SymbolicaEvaluatorSettings, + *, + jit_compile: bool, + phase: str, + item: str, + interval_s: float = 5.0, + ) -> None: + self.progress_callback = progress_callback + self.settings = settings + self.jit_compile = jit_compile + self.phase = phase + self.item = item + self.interval_s = max(float(interval_s), 0.01) + self._stop = threading.Event() + self._thread: threading.Thread | None = None + self._started_at = 0.0 + + def __enter__(self) -> _JITBoundaryHeartbeat: + if ( + self.progress_callback is None + or self.settings.backend != "jit" + or not self.jit_compile + ): + return self + self._started_at = time.perf_counter() + self._thread = threading.Thread(target=self._run, daemon=True) + self._thread.start() + return self + + def __exit__(self, exc_type: object, exc: object, tb: object) -> None: + self._stop.set() + if self._thread is not None: + self._thread.join(timeout=1.0) + + def _run(self) -> None: + while not self._stop.wait(self.interval_s): + elapsed_s = time.perf_counter() - self._started_at + _report_progress( + self.progress_callback, + stage=f"jit {self.phase}", + item=f"{self.item} waiting {elapsed_s:.0f}s", + ) + + +def _symbolica_progress_stage( + settings: SymbolicaEvaluatorSettings, + *, + jit_compile: bool, +) -> str: + if settings.backend == "jit": + return "jit compile" if jit_compile else "jit build" + if settings.backend == "compiled-complex": + return "c++ build" + return "eval build" + + +def _prepare_symbolica_output( + output: Any, + settings: SymbolicaEvaluatorSettings, +) -> Any: + if not settings.collect_factors: + return output + collect_factors = getattr(output, "collect_factors", None) + if callable(collect_factors): + return collect_factors() + return output + + +def _symbolica_evaluator_kwargs( + settings: SymbolicaEvaluatorSettings, + *, + verbose: bool, + jit_compile: bool = True, +) -> dict[str, Any]: + return { + "iterations": settings.iterations, + "cpe_iterations": settings.cpe_iterations, + "n_cores": settings.n_cores, + "verbose": verbose, + "jit_compile": jit_compile, + "direct_translation": settings.direct_translation, + "jit_direct_translation": settings.jit_direct_translation, + "jit_optimization_level": settings.jit_optimization_level, + "jit_options": {"compress": "true" if settings.jit_compress else "false"}, + "max_horner_scheme_variables": settings.max_horner_scheme_variables, + "max_common_pair_cache_entries": settings.max_common_pair_cache_entries, + "max_common_pair_distance": settings.max_common_pair_distance, + } + + +def _finalize_symbolica_evaluator( + evaluator: Any, + settings: SymbolicaEvaluatorSettings, + label: str, + *, + input_len: int, + output_len: int, + progress_callback: ProgressCallback | None = None, + native_direct_only: bool = False, +) -> Any: + if settings.backend == "jit": + _report_progress( + progress_callback, + stage="jit ready", + item=label, + ) + return _JITSymbolicaEvaluatorAdapter( + evaluator, + settings, + label, + input_len=input_len, + output_len=output_len, + progress_callback=progress_callback, + ) + if settings.backend == "compiled-complex": + _report_progress( + progress_callback, + stage="c++ compile", + item=label, + ) + return _CompiledComplexEvaluatorAdapter( + evaluator, + settings, + label, + input_len=input_len, + output_len=output_len, + compile_dense=not native_direct_only, + ) + raise NativeEvaluationError( + f"unsupported symbolica evaluator backend: {settings.backend}" + ) diff --git a/pyamplicol/src/pyamplicol/evaluators/symbolica_helpers.py b/pyamplicol/src/pyamplicol/evaluators/symbolica_helpers.py new file mode 100644 index 0000000000..3cc352f2b5 --- /dev/null +++ b/pyamplicol/src/pyamplicol/evaluators/symbolica_helpers.py @@ -0,0 +1,169 @@ +# SPDX-License-Identifier: 0BSD +"""Array, artifact-path, and progress helpers for evaluator adapters.""" + +from __future__ import annotations + +import os +import re +import shutil +from collections.abc import Iterable +from pathlib import Path +from typing import Any + +import numpy as np + +from .._internal.physics.types import NativeEvaluationError + +ComplexOutput = np.ndarray | tuple[np.ndarray, ...] + + +def _symbolica_instruction_program(exported: Any) -> Any: + """Extract the linear program from Symbolica's structured instruction export.""" + if isinstance(exported, tuple): + return exported + if exported.sub_evaluators: + raise NativeEvaluationError( + "native kernels require inlined Symbolica function bodies" + ) + return exported.instructions, exported.temporary_count, exported.constants + + +def _symbolica_instruction_program_repr(exported: Any) -> str: + """Spell out unit imaginary coefficients for SymJIT's numeric grammar. + + Symbolica omits coefficients of magnitude one before its imaginary-unit + glyph, but SymJIT's instruction parser requires them. Keep the exact + rational constants and the original evaluator state unchanged. + """ + instructions, temporary_count, constants = _symbolica_instruction_program(exported) + serialized_constants = ", ".join( + re.sub(r"(? np.ndarray: + if ( + isinstance(parameter_rows, np.ndarray) + and parameter_rows.dtype == np.complex128 + and parameter_rows.flags.c_contiguous + ): + return parameter_rows + return np.asarray(parameter_rows, dtype=np.complex128) + + +def _evaluate_prepared_complex(evaluator: Any, parameter_rows: np.ndarray) -> Any: + evaluate_prepared = getattr(evaluator, "_evaluate_complex_prepared", None) + if callable(evaluate_prepared): + return evaluate_prepared(parameter_rows) + return evaluator.evaluate_complex(parameter_rows) + + +def _evaluate_prepared_complex_profiled( + evaluator: Any, + parameter_rows: np.ndarray, +) -> tuple[Any, tuple[float, float, float]]: + evaluate_profiled = getattr(evaluator, "_evaluate_complex_profiled_prepared", None) + if not callable(evaluate_profiled): + raise NativeEvaluationError( + "evaluator does not expose native complex profiling" + ) + return evaluate_profiled(parameter_rows) + + +def _evaluate_complex_outputs(evaluator: Any, parameter_rows: Any) -> ComplexOutput: + evaluate_chunks = getattr(evaluator, "evaluate_complex_chunks", None) + if callable(evaluate_chunks): + return tuple( + np.asarray(chunk, dtype=np.complex128) + for chunk in evaluate_chunks(parameter_rows) + ) + return np.asarray(evaluator.evaluate_complex(parameter_rows), dtype=np.complex128) + + +def _symbolica_evaluator_artifact_manifest( + evaluator: Any, + artifact_dir: Path, +) -> dict[str, Any]: + manifest = getattr(evaluator, "artifact_manifest", None) + if not callable(manifest): + raise NativeEvaluationError( + "saving evaluator artifacts is currently supported only for " + "Symbolica evaluator adapters" + ) + return manifest(artifact_dir) + + +def _artifact_path_for_manifest(path: Path, artifact_dir: Path) -> str: + artifact_root = artifact_dir.resolve() + source_path = path.expanduser() + source_resolved = source_path.resolve() + try: + source_resolved.relative_to(artifact_root) + except ValueError: + if source_path.exists(): + target_dir = artifact_dir / "compiled" / "repackaged" + target_dir.mkdir(parents=True, exist_ok=True) + target = target_dir / source_path.name + if source_resolved != target.resolve(): + shutil.copy2(source_path, target) + source_path = target + else: + source_path = source_resolved + try: + return os.path.relpath(source_path, artifact_dir) + except ValueError: + return str(source_path) + + +def _artifact_subdirectory(artifact_dir: Path, name: str) -> Path: + directory = artifact_dir / name + directory.mkdir(parents=True, exist_ok=True) + return directory + + +def _artifact_path_from_manifest(path: str, artifact_dir: Path) -> Path: + candidate = Path(path) + if candidate.is_absolute(): + return candidate + return artifact_dir / candidate + + +def _safe_symbol_name(value: str) -> str: + result = "".join(character if character.isalnum() else "_" for character in value) + if not result: + return "eval" + if result[0].isdigit(): + return f"eval_{result}" + return result + + +def _progress_outputs( + outputs: tuple[Any, ...], + *, + enabled: bool, +) -> Iterable[Any]: + if not enabled: + return outputs + try: + from colorama import Fore, Style # type: ignore[import-untyped] + from progressbar import ( # type: ignore[import-not-found] + ETA, + Bar, + Percentage, + ProgressBar, + ) + except ImportError: + return outputs + widgets = [ + Fore.CYAN, + " merging evaluators ", + Percentage(), + " ", + Bar(), + " ", + ETA(), + Style.RESET_ALL, + ] + return ProgressBar(max_value=len(outputs), widgets=widgets)(outputs) diff --git a/pyamplicol/src/pyamplicol/evaluators/symbolica_settings.py b/pyamplicol/src/pyamplicol/evaluators/symbolica_settings.py new file mode 100644 index 0000000000..e188cd7d53 --- /dev/null +++ b/pyamplicol/src/pyamplicol/evaluators/symbolica_settings.py @@ -0,0 +1,156 @@ +# SPDX-License-Identifier: 0BSD +"""Evaluator settings and progress callback contract.""" + +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass + +from .._internal.physics.types import NativeEvaluationError + +ProgressCallback = Callable[[dict[str, object]], None] + + +def _report_progress( + callback: ProgressCallback | None, + *, + stage: str, + item: str, + increment: int = 0, + total: int | None = None, +) -> None: + if callback is None: + return + payload: dict[str, object] = { + "stage": stage, + "item": item, + } + if increment: + payload["increment"] = int(increment) + if total is not None: + payload["total"] = int(total) + callback(payload) + + +@dataclass(frozen=True) +class SymbolicaEvaluatorSettings: + backend: str = "jit" + iterations: int = 10 + cpe_iterations: int | None = None + n_cores: int = 4 + direct_translation: bool = True + jit_direct_translation: bool = False + jit_optimization_level: int = 2 + jit_compress: bool = False + max_horner_scheme_variables: int = 1000 + max_common_pair_cache_entries: int = 5000000 + max_common_pair_distance: int = 1000 + collect_factors: bool = False + compiled_inline_asm: str = "default" + compiled_optimization_level: int = 3 + compiled_native: bool = True + compiler_path: str | None = None + compiler_flags: tuple[str, ...] = () + compiled_output_chunk_size: int | None = None + output_chunk_strategy: str = "uniform" + output_chunk_autotune_batch_size: int = 128 + compiled_chunk_compile_workers: int = 1 + compiled_output_dir: str | None = None + raw_sum_final_stage: bool = False + real_param_sqrt_real: bool = False + real_param_log_real: bool = False + real_param_powf_real: bool = False + real_param_real_if_args_real: bool = False + + def __post_init__(self) -> None: + if self.backend not in ("jit", "compiled-complex"): + raise NativeEvaluationError( + "symbolica evaluator backend must be 'jit' or 'compiled-complex'" + ) + if self.iterations < 1: + raise NativeEvaluationError("symbolica iterations must be positive") + if self.cpe_iterations is not None and self.cpe_iterations < 0: + raise NativeEvaluationError("symbolica cpe iterations must be non-negative") + if self.n_cores < 1: + raise NativeEvaluationError("symbolica n_cores must be positive") + if self.jit_optimization_level not in (0, 1, 2, 3): + raise NativeEvaluationError( + "symbolica jit optimization level must be 0, 1, 2, or 3" + ) + if not isinstance(self.jit_compress, bool): + raise NativeEvaluationError("symbolica jit_compress must be a boolean") + if self.compiled_optimization_level not in (0, 1, 2, 3): + raise NativeEvaluationError( + "symbolica compiled optimization level must be 0, 1, 2, or 3" + ) + if ( + self.compiled_output_chunk_size is not None + and self.compiled_output_chunk_size < 1 + ): + raise NativeEvaluationError( + "symbolica compiled output chunk size must be positive" + ) + if self.output_chunk_strategy not in ( + "auto", + "uniform", + "tapered-stage", + "measured-stage", + ): + raise NativeEvaluationError( + "symbolica output chunk strategy must be 'auto', 'uniform', " + "'tapered-stage', or 'measured-stage'" + ) + if self.output_chunk_autotune_batch_size < 1: + raise NativeEvaluationError( + "symbolica output chunk autotune batch size must be positive" + ) + if self.compiled_chunk_compile_workers < 1: + raise NativeEvaluationError( + "symbolica compiled chunk compile workers must be positive" + ) + if self.max_horner_scheme_variables < 1: + raise NativeEvaluationError( + "symbolica max_horner_scheme_variables must be positive" + ) + if self.max_common_pair_cache_entries < 1: + raise NativeEvaluationError( + "symbolica max_common_pair_cache_entries must be positive" + ) + if self.max_common_pair_distance < 1: + raise NativeEvaluationError( + "symbolica max_common_pair_distance must be positive" + ) + + def to_json_dict(self) -> dict[str, object]: + from .symbolica_adapters import _compiled_compiler_flags + + return { + "backend": self.backend, + "iterations": self.iterations, + "cpe_iterations": self.cpe_iterations, + "n_cores": self.n_cores, + "direct_translation": self.direct_translation, + "jit_direct_translation": self.jit_direct_translation, + "jit_optimization_level": self.jit_optimization_level, + "jit_compress": self.jit_compress, + "max_horner_scheme_variables": self.max_horner_scheme_variables, + "max_common_pair_cache_entries": self.max_common_pair_cache_entries, + "max_common_pair_distance": self.max_common_pair_distance, + "collect_factors": self.collect_factors, + "compiled_inline_asm": self.compiled_inline_asm, + "compiled_optimization_level": self.compiled_optimization_level, + "compiled_native": self.compiled_native, + "compiler_path": self.compiler_path, + "compiler_flags": list(self.compiler_flags), + "effective_compiler_flags": list(_compiled_compiler_flags(self)), + "compiled_output_chunk_size": self.compiled_output_chunk_size, + "output_chunk_strategy": self.output_chunk_strategy, + "output_chunk_autotune_batch_size": (self.output_chunk_autotune_batch_size), + "compiled_chunk_compile_workers": self.compiled_chunk_compile_workers, + "compiled_output_dir": self.compiled_output_dir, + "raw_sum_final_stage": self.raw_sum_final_stage, + "real_param_sqrt_real": self.real_param_sqrt_real, + "real_param_log_real": self.real_param_log_real, + "real_param_powf_real": self.real_param_powf_real, + "real_param_real_if_args_real": self.real_param_real_if_args_real, + } diff --git a/pyamplicol/src/pyamplicol/generation/__init__.py b/pyamplicol/src/pyamplicol/generation/__init__.py new file mode 100644 index 0000000000..bdd776bd79 --- /dev/null +++ b/pyamplicol/src/pyamplicol/generation/__init__.py @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: 0BSD +"""Private process-generation implementation. + +Use :class:`pyamplicol.Generator` or :func:`pyamplicol.generate` from the +supported public API. +""" + +__all__: tuple[str, ...] = () diff --git a/pyamplicol/src/pyamplicol/generation/artifact_writer.py b/pyamplicol/src/pyamplicol/generation/artifact_writer.py new file mode 100644 index 0000000000..be38636aff --- /dev/null +++ b/pyamplicol/src/pyamplicol/generation/artifact_writer.py @@ -0,0 +1,5916 @@ +# SPDX-License-Identifier: 0BSD +"""Transactional schema-v3 output for compiled concrete processes.""" + +from __future__ import annotations + +import hashlib +import importlib +import importlib.metadata +import json +import math +import os +import re +import time +import zipfile +from collections.abc import Callable, Mapping, Sequence +from dataclasses import dataclass, replace +from functools import cache +from itertools import pairwise +from pathlib import Path +from typing import TYPE_CHECKING, BinaryIO, Literal, cast + +from pyamplicol.api.requests import ModelSource +from pyamplicol.artifacts import ( + ArtifactBuilder, + ArtifactManifest, + PayloadRecord, + load_manifest, +) +from pyamplicol.artifacts.manifest import PORTABLE_64LE_TARGET, compute_artifact_id +from pyamplicol.artifacts.process_groups import ( + PROCESS_GROUPS_MEDIA_TYPE, + initial_process_groups, + process_groups_payload, + validate_process_groups, +) +from pyamplicol.artifacts.security import sha256_file +from pyamplicol.config import ( + ConfigClamp, + ConfigResolution, + GenerationConfig, + RunConfig, + config_to_dict, +) + +from .._internal.versions import ( + COMPILED_COLOR_CONTRACTION_WALSH_C2K_CAPABILITY, + COMPILED_COLOR_CONTRACTION_WALSH_CAPABILITY, + COMPILED_COLOR_TOPOLOGY_LANES_CAPABILITY, + COMPILED_HELICITY_DUAL_LANE_CAPABILITY, + COMPILED_HELICITY_PRIMARY_RECURRENCE_CAPABILITY, + COMPILED_HELICITY_SELECTOR_UNION_CAPABILITY, + COMPILED_PLANE_ARENA_RUNTIME_CAPABILITY, + COMPILED_PLANE_DIRECT_APPLICATION_ABI, + COMPILED_RUNTIME_SELECTORS_CAPABILITY, + EAGER_DIRECT_ARENA_RUNTIME_CAPABILITY, + EAGER_DIRECT_TABLE_BINDING_ABI, + EAGER_DIRECT_TABLE_DESCRIPTOR_ABI, + EVALUATOR_RUNTIME_CAPABILITIES, + NATIVE_COMPILED_DIRECT_APPLICATION_ABI, + ON_THE_FLY_CONTRACTED_COLOR_RUNTIME_CAPABILITY, + ON_THE_FLY_LC_COLOR_RUNTIME_CAPABILITY, + ON_THE_FLY_RUNTIME_CAPABILITY, + PROCESS_ARTIFACT_SCHEMA_VERSION, + PYTHON_API_VERSION, + RECURRENCE_BUILDER_INPUT_ABI, + RECURRENCE_COLOR_RUNTIME_CAPABILITY, + RECURRENCE_CONTRACTED_COLOR_RUNTIME_CAPABILITY, + RECURRENCE_DIRECT_ARENA_RUNTIME_CAPABILITY, + RECURRENCE_DIRECT_BACKEND_ABI, + RECURRENCE_DIRECT_TEMPLATE_ABI, + RECURRENCE_HELICITY_SELECTOR_COMPANION_RUNTIME_CAPABILITY, + RECURRENCE_PLAN_ABI, + RECURRENCE_RUNTIME_LAYOUT_ABI, + RUNTIME_PHYSICS_SCHEMA_VERSION, + SYMBOLICA_ASM_RUNTIME_CAPABILITY, + SYMBOLICA_CPP_RUNTIME_CAPABILITY, + SYMBOLICA_LEGACY_JIT_RUNTIME_CAPABILITY, + SYMBOLICA_SERIALIZATION_ABI, + SYMJIT_APPLICATION_ABI, + SYMJIT_F64_RUNTIME_CAPABILITY, + SYMJIT_PLANE_APPLICATION_ABI, + SYMMETRIC_GROUP_FFT_COLOR_RUNTIME_CAPABILITY, + TOML_SCHEMA_VERSION, + active_native_build_inputs_sha256, + active_source_revision, + package_version, + verify_native_module, +) +from ..evaluators.execution_schema import evaluator_runtime_capabilities +from ..models.loading import COMPILED_MODEL_SCHEMA_VERSION, CompiledModel +from ..models.prepared import ( + PREPARED_KERNEL_PACK_IDENTITY_ABI, + prepared_kernel_pack_manifest_identity_sha256, +) +from .contracts import RuntimeExpressionSchema +from .eager_columnar import EAGER_LOWERING_INPUT_ABI +from .eager_lowering import EAGER_RUNTIME_KIND +from .eager_tables import EAGER_KERNEL_ABI, EAGER_RUNTIME_CAPABILITY +from .evaluator_container import ( + PacbinIndex, + PacbinMemberKind, + PacbinMemberSource, + PacbinReader, + write_pacbin_atomic, +) +from .process_grouping import PROCESS_GROUPS_PATH, PROCESS_GROUPS_ROLE, ProcessGroupMap +from .recurrence_schedule_sharing import ( + RECURRENCE_PROCESS_BINDING_ABI, + RECURRENCE_SCHEDULE_INDEX_PATH, + RECURRENCE_SCHEDULE_SHARING_SCHEMA_VERSION, + RecurrenceProcessBinding, + RecurrenceProcessExecutorPack, + RecurrenceProcessRemap, + RecurrenceScheduleSharingPlan, + intern_recurrence_schedules, + recurrence_helicity_selector_schedule_digest, +) +from .structural_source_proof import ( + ROLE as STRUCTURAL_SOURCE_PROOF_ROLE, +) +from .structural_source_proof import build_generation_structural_proof +from .validation import ValidationPointRecord, validation_point_map + +if TYPE_CHECKING: + from pyamplicol.artifacts.transaction import ArtifactWriteMode + +ApiBundleHook = Callable[ + [ArtifactBuilder, Mapping[str, Sequence[Sequence[float]]]], + Sequence[object], +] +ArtifactPayloadHook = Callable[[ArtifactBuilder], Mapping[str, object]] + +_CONFIG_REQUESTED_PATH = "config/requested.toml" +_CONFIG_EFFECTIVE_PATH = "config/effective.toml" +_COMPILED_MODEL_PATH = "model/compiled-model.json" +_MODEL_PARAMETERS_PATH = "model/parameters.json" +_EVALUATOR_SET_PATH = "processes/evaluators.json" +_EAGER_KERNEL_PACK_PATH = "model/eager-kernel-pack.json" +_EAGER_KERNEL_PAYLOAD_ROOT = "model/eager-kernels" +_HELICITY_SUM_PAYLOAD_ROOT = "helicity-sum" +_HELICITY_SELECTOR_UNION_PAYLOAD_ROOT = "helicity-selector-union" +_COLOR_SELECTOR_PAYLOAD_ROOT = "color-selector" +_EAGER_PACK_IDENTITY_EXTENSION = "eager_prepared_pack" +_EAGER_PACK_IDENTITY_KIND = "pyamplicol-prepared-kernel-pack-identity" +_EAGER_PACK_IDENTITY_SCHEMA_VERSION = 1 +_EVALUATOR_PAYLOAD_CONTAINER_EXTENSION = "evaluator_payload_container" +_RECURRENCE_BOOTSTRAP_FILENAME = "recurrence-bootstrap.bin" +_EVALUATOR_PAYLOAD_CONTAINER_PATH = "evaluators.pacbin" +_COMPILED_COLOR_CONTRACTION_MEMBER_PATH = "compiled-color.pacrclr3" +_EVALUATOR_PAYLOAD_CONTAINER_KIND = "pyamplicol-evaluator-payload-container" +_EVALUATOR_PAYLOAD_CONTAINER_SCHEMA_VERSION = 1 +_EVALUATOR_PAYLOAD_CONTAINER_STORAGE_ABI = "pacbin-v1" +EAGER_PLAN_V3_ABI = "pyamplicol-eager-plan-v3" +EAGER_RUNTIME_LAYOUT_ABI = "pyamplicol-eager-runtime-layout-v1" +EAGER_PLAN_V3_RUNTIME_CAPABILITY = "rusticol.eager-runtime-layout.complex-f64.v1" +_EAGER_PLAN_V3_RUNTIME_CAPABILITIES = tuple( + sorted( + ( + EAGER_DIRECT_ARENA_RUNTIME_CAPABILITY, + EAGER_PLAN_V3_RUNTIME_CAPABILITY, + ) + ) +) +EAGER_RUNTIME_CONTAINER_KIND = "pyamplicol-eager-runtime-container" +EAGER_RUNTIME_CONTAINER_SCHEMA_VERSION = 1 +EAGER_RUNTIME_STORAGE_ABI = "pacbin-v1" +_EAGER_RUNTIME_CONTAINER_PATH = "eager-runtime.pacbin" +RECURRENCE_RUNTIME_KIND = "pyamplicol-runtime-recurrence-execution" +RECURRENCE_RUNTIME_CONTAINER_KIND = "pyamplicol-recurrence-runtime-container" +RECURRENCE_RUNTIME_CONTAINER_SCHEMA_VERSION = 1 +RECURRENCE_RUNTIME_STORAGE_ABI = "pacbin-v1" +_RECURRENCE_RUNTIME_CONTAINER_PATH = "recurrence-runtime.pacbin" +_RECURRENCE_DIRECT_SCHEDULE_MEMBER_PATH = "schedule/recurrence-direct-schedule-v2.bin" +_RECURRENCE_COLOR_CONTRACTION_PATH = "recurrence-color.bin" +_RECURRENCE_SCHEDULE_SHARING_EXTENSION = "recurrence_schedule_sharing" +_RECURRENCE_HELICITY_SELECTOR_SCHEDULE_ROOT = "recurrence/schedules" +_RECURRENCE_HELICITY_SELECTOR_BINDING_PATH = "helicity-selector-binding.bin" +_RECURRENCE_HELICITY_DISPATCH_PATH = "recurrence-helicity-dispatch-v1.bin" +_RECURRENCE_HELICITY_DISPATCH_ABI = "pyamplicol-recurrence-helicity-dispatch-v1" +ON_THE_FLY_RUNTIME_KIND = "pyamplicol-runtime-on-the-fly-execution" +ON_THE_FLY_RUNTIME_CONTAINER_KIND = "pyamplicol-on-the-fly-runtime-container" +ON_THE_FLY_RUNTIME_CONTAINER_SCHEMA_VERSION = 1 +ON_THE_FLY_RUNTIME_STORAGE_ABI = "pacbin-v1" +ON_THE_FLY_PUBLIC_METADATA_KIND = "pyamplicol-on-the-fly-public-metadata" +_RECURRENCE_HELICITY_SELECTOR_COMPANION_V2_KIND = ( + "pyamplicol-recurrence-helicity-selector-companion-v2" +) +_ON_THE_FLY_RUNTIME_CONTAINER_PATH = "on-the-fly-runtime.pacbin" +_ON_THE_FLY_PROCESS_SEED_MEMBER_PATH = "on-the-fly/process-seed-v1.bin" +_ON_THE_FLY_COLOR_CONTRACTION_PATH = "on-the-fly-color.bin" +_SAFE_TOML_KEY = re.compile(r"^[A-Za-z0-9_-]+$") +_SUPPORTED_ARTIFACT_TARGETS = frozenset( + { + "aarch64-apple-darwin", + "x86_64-apple-darwin", + "x86_64-unknown-linux-gnu", + } +) + + +@dataclass(frozen=True, slots=True) +class CompiledExecutionArtifact: + runtime_schema: RuntimeExpressionSchema + stage_manifest: Mapping[str, object] + model_parameter_evaluator: Mapping[str, object] | None + dag_summary: Mapping[str, object] + evaluator_root: Path + color_selector_executions: tuple[CompiledColorSelectorExecutionArtifact, ...] = () + helicity_selector_executions: tuple[ + CompiledHelicitySelectorExecutionArtifact, ... + ] = () + + +@dataclass(frozen=True, slots=True) +class CompiledColorSelectorExecutionArtifact: + materialized_sector_id: int + execution: CompiledExecutionArtifact + + +@dataclass(frozen=True, slots=True) +class CompiledHelicitySelectorExecutionArtifact: + selector_domain_ids: tuple[int, ...] + execution: CompiledExecutionArtifact + schedule_mode: str = "parent-closure" + + +@dataclass(frozen=True, slots=True) +class CompiledProcessArtifact: + process_id: str + expression: str + color_accuracy: str + external_pdgs: tuple[int, ...] + aliases: tuple[Mapping[str, object], ...] + runtime_schema: RuntimeExpressionSchema + stage_manifest: Mapping[str, object] + model_parameter_evaluator: Mapping[str, object] | None + dag_summary: Mapping[str, object] + evaluator_root: Path + validation_point: ValidationPointRecord + generation_filters: Mapping[str, object] + color_contraction_payload: bytes | None = None + helicity_sum_execution: CompiledExecutionArtifact | None = None + helicity_selector_executions: tuple[ + CompiledHelicitySelectorExecutionArtifact, ... + ] = () + color_selector_executions: tuple[CompiledColorSelectorExecutionArtifact, ...] = () + umami_physics: Mapping[str, object] | None = None + umami_color_sectors: tuple[Mapping[str, object], ...] = () + umami_structural_keys: Mapping[str, str] | None = None + + +@dataclass(frozen=True, slots=True) +class EagerPlanV3ProcessArtifact: + """One Rust-lowered eager runtime plus its bounded publication metadata.""" + + process_id: str + expression: str + color_accuracy: str + external_pdgs: tuple[int, ...] + aliases: tuple[Mapping[str, object], ...] + physics: Mapping[str, object] + eager_runtime_path: Path + eager_runtime_size_bytes: int + eager_runtime_sha256: str + eager_runtime_member_count: int + eager_runtime_unpacked_size_bytes: int + eager_runtime_index_sha256: str + lowering_input_sha256: str + referenced_kernel_ids: frozenset[int] + inspection_summary: Mapping[str, object] + point_tile_size: int + workspace_mib: int + dag_summary: Mapping[str, object] + validation_point: ValidationPointRecord + generation_filters: Mapping[str, object] + umami_physics: Mapping[str, object] | None = None + umami_color_sectors: tuple[Mapping[str, object], ...] = () + umami_structural_keys: Mapping[str, str] | None = None + + +@dataclass(frozen=True, slots=True) +class RecurrenceProcessArtifact: + """One compact recurrence runtime plus bounded public metadata.""" + + process_id: str + expression: str + color_accuracy: str + external_pdgs: tuple[int, ...] + aliases: tuple[Mapping[str, object], ...] + physics: Mapping[str, object] + recurrence_schedule_path: Path + recurrence_schedule_digest: str + recurrence_native_schedule_semantic_digest: str + recurrence_schedule_size_bytes: int + recurrence_schedule_sha256: str + recurrence_schedule_member_count: int + recurrence_schedule_unpacked_size_bytes: int + recurrence_schedule_index_sha256: str + builder_input_sha256: str + prepared_kernel_pack_digest: str + direct_template_catalog_digest: str + referenced_kernel_ids: frozenset[int] + inspection_summary: Mapping[str, object] + runtime_metadata: Mapping[str, object] + color_contraction_payload: bytes | None + color_contraction_summary: Mapping[str, object] | None + point_tile_size: int + workspace_mib: int + recurrence_summary: Mapping[str, object] + validation_point: ValidationPointRecord + generation_filters: Mapping[str, object] + generation_profile: Mapping[str, object] + recurrence_process_remap: RecurrenceProcessRemap + recurrence_process_executor_pack: RecurrenceProcessExecutorPack + process_digest: str + process_support_mask: int = 1 + helicity_selector_companion: RecurrenceHelicitySelectorPlanArtifact | None = None + umami_physics: Mapping[str, object] | None = None + umami_color_sectors: tuple[Mapping[str, object], ...] = () + umami_structural_keys: Mapping[str, str] | None = None + + +@dataclass(frozen=True, slots=True) +class RecurrenceHelicitySelectorPlanArtifact: + """Persisted physical-colour schedule for runtime singleton helicities. + + The schedule uses the ordinary recurrence Direct-plan ABI with runtime + source-state dispatch. It deliberately owns neither physics metadata nor + prepared-model state: those identities remain authenticated by the + enclosing recurrence process. + """ + + recurrence_schedule_path: Path + recurrence_base_schedule_digest: str + recurrence_schedule_digest: str + recurrence_native_schedule_semantic_digest: str + recurrence_schedule_size_bytes: int + recurrence_schedule_sha256: str + recurrence_schedule_member_count: int + recurrence_schedule_unpacked_size_bytes: int + recurrence_schedule_index_sha256: str + helicity_dispatch_path: Path + helicity_dispatch_size_bytes: int + helicity_dispatch_sha256: str + helicity_dispatch_base_runtime_layout_digest: str + helicity_dispatch_resolved_helicity_count: int + physical_destination_count: int + builder_input_sha256: str + inspection_summary: Mapping[str, object] + referenced_kernel_ids: frozenset[int] + recurrence_process_remap: RecurrenceProcessRemap + recurrence_process_executor_pack: RecurrenceProcessExecutorPack + + +@dataclass(frozen=True, slots=True) +class OnTheFlyProcessArtifact: + """One source-only runtime seed plus compact public metadata.""" + + process_id: str + expression: str + color_accuracy: str + external_pdgs: tuple[int, ...] + aliases: tuple[Mapping[str, object], ...] + physics: Mapping[str, object] + runtime_path: Path + runtime_size_bytes: int + runtime_sha256: str + runtime_member_count: int + runtime_unpacked_size_bytes: int + runtime_index_sha256: str + referenced_kernel_ids: frozenset[int] + runtime_metadata: Mapping[str, object] + selector_policy: Mapping[str, object] + color_contraction_payload: bytes | None + color_contraction_summary: Mapping[str, object] | None + point_tile_size: int + query_construction_threads: int + validation_point: ValidationPointRecord + generation_filters: Mapping[str, object] + umami_physics: Mapping[str, object] | None = None + umami_color_sectors: tuple[Mapping[str, object], ...] = () + umami_structural_keys: Mapping[str, str] | None = None + + +ProcessArtifact = ( + CompiledProcessArtifact + | EagerPlanV3ProcessArtifact + | RecurrenceProcessArtifact + | OnTheFlyProcessArtifact +) + + +@dataclass(frozen=True, slots=True) +class ArtifactWriteResult: + output: Path + files: tuple[Path, ...] + validation_points: Mapping[str, Mapping[str, object]] + api_bundle_path: str | None + + +@dataclass(frozen=True, slots=True) +class _GenerationConfigProvenance: + requested: GenerationConfig | RunConfig + effective: GenerationConfig | RunConfig + adjustments: tuple[ConfigClamp, ...] = () + + @classmethod + def from_config( + cls, + config: GenerationConfig | RunConfig | ConfigResolution | None, + ) -> _GenerationConfigProvenance: + if isinstance(config, ConfigResolution): + effective = replace(config.effective, color=config.effective.resolved_color) + return cls(config.requested, effective, config.clamps) + effective = GenerationConfig() if config is None else config + if isinstance(effective, RunConfig): + return cls(effective, replace(effective, color=effective.resolved_color)) + return cls(effective, effective) + + +class _EvaluatorPayloadCollector: + """Collect evaluator payloads and publish one root pacbin container.""" + + def __init__( + self, + builder: ArtifactBuilder, + *, + existing: ArtifactManifest | None, + target: Mapping[str, object], + ) -> None: + self._builder = builder + self._existing = existing + self._target = dict(target) + self._new_sources: dict[str, PacbinMemberSource] = {} + self._staged_loose_paths: set[str] = set() + self._discarded_prefixes: set[str] = set() + + def discard_prefix(self, prefix: str) -> None: + normalized = prefix.strip("/") + if not normalized: + raise ValueError("packed evaluator discard prefix must not be empty") + self._discarded_prefixes.add(normalized) + owned_prefix = normalized + "/" + self._new_sources = { + path: source + for path, source in self._new_sources.items() + if path != normalized and not path.startswith(owned_prefix) + } + + def add_file( + self, + relative: str, + source: Path, + *, + process_id: str | None, + ) -> PayloadRecord: + kind = _packed_evaluator_member_kind(relative) + if kind is None: + return self._builder.add_file( + relative, + source, + role="evaluator-state", + media_type=_media_type(source), + target=self._target, + process_id=process_id, + ) + return + if source.is_symlink() or not source.is_file(): + raise ValueError(f"evaluator payload must be a regular file: {source}") + # Evaluator builders may reuse their temporary output paths while + # materializing nested selector lanes. Snapshot each payload when it + # is registered so a later write cannot silently change an earlier + # logical container member before publication. + record = self._builder.add_file( + relative, + source, + role="evaluator-state", + media_type=_media_type(source), + target=self._target, + process_id=process_id, + ) + self._register_staged_source(relative, kind) + return record + + def add_bytes( + self, + relative: str, + content: bytes, + *, + process_id: str | None, + media_type: str | None = None, + ) -> PayloadRecord: + kind = _packed_evaluator_member_kind(relative) + if kind is None: + return self._builder.add_bytes( + relative, + content, + role="evaluator-state", + media_type=media_type or _media_type(Path(relative)), + target=self._target, + process_id=process_id, + ) + record = self._builder.add_bytes( + relative, + content, + role="evaluator-state", + media_type=media_type or _media_type(Path(relative)), + target=self._target, + process_id=process_id, + ) + self._register_staged_source(relative, kind) + return record + + def add_stream( + self, + relative: str, + source: BinaryIO, + *, + process_id: str | None, + media_type: str | None = None, + ) -> None: + kind = _packed_evaluator_member_kind(relative) + self._builder.add_stream( + relative, + source, + role="evaluator-state", + media_type=media_type or _media_type(Path(relative)), + target=self._target, + process_id=process_id, + ) + if kind is not None: + self._register_staged_source(relative, kind) + + def publish(self) -> dict[str, object] | None: + existing_container = _existing_evaluator_container_path(self._existing) + if existing_container is None: + return self._publish_with_old_sources(()) + + container_path = self._builder.staged_path(existing_container) + with PacbinReader.open(container_path, verify_payloads=True) as reader: + old_sources = tuple( + PacbinMemberSource( + member.logical_path, + member.kind, + cast("BinaryIO", reader.open_member_stream(member.logical_path)), + ) + for member in reader.members + if not self._is_discarded(member.logical_path) + and member.logical_path not in self._new_sources + ) + return self._publish_with_old_sources(old_sources) + + def _publish_with_old_sources( + self, + old_container_sources: Sequence[PacbinMemberSource], + ) -> dict[str, object] | None: + combined = {source.logical_path: source for source in old_container_sources} + loose_paths: set[str] = set(self._staged_loose_paths) + if self._existing is not None: + for record in self._existing.payloads: + kind = _packed_evaluator_member_kind(record.path) + if kind is None or self._is_discarded(record.path): + continue + path = self._builder.staged_path(record.path) + if not path.is_file() or path.is_symlink(): + continue + loose_paths.add(record.path) + combined[record.path] = PacbinMemberSource( + record.path, + kind, + path, + ) + combined.update(self._new_sources) + if not combined: + self._builder.discard_payloads(_EVALUATOR_PAYLOAD_CONTAINER_PATH) + return None + + destination = self._builder.staged_path( + _EVALUATOR_PAYLOAD_CONTAINER_PATH, + create_parent=True, + ) + written = write_pacbin_atomic(destination, combined.values()) + with PacbinReader.open(destination, verify_payloads=True) as verified: + if verified.index != written: + raise ValueError("published evaluator container failed verification") + index = verified.index + + for relative in sorted(loose_paths): + self._builder.discard_payloads(relative) + self._builder.register_staged_file( + _EVALUATOR_PAYLOAD_CONTAINER_PATH, + role="evaluator-state", + media_type="application/octet-stream", + target=self._target, + process_id=None, + ) + return _evaluator_payload_container_extension(index) + + def _register_staged_source( + self, + relative: str, + kind: PacbinMemberKind, + ) -> None: + path = self._builder.staged_path(relative) + self._new_sources[relative] = PacbinMemberSource(relative, kind, path) + self._staged_loose_paths.add(relative) + + def _is_discarded(self, relative: str) -> bool: + return any( + relative == prefix or relative.startswith(prefix + "/") + for prefix in self._discarded_prefixes + ) + + +def _packed_evaluator_member_kind(relative: str) -> PacbinMemberKind | None: + if relative.endswith(".symjit"): + return PacbinMemberKind.SYMJIT_APPLICATION + if relative.endswith(".evaluator.bin"): + return PacbinMemberKind.SYMBOLICA_EXACT_STATE + if relative.endswith(".color.pacrclr3") or relative.endswith( + "/compiled-color.pacrclr3" + ): + return PacbinMemberKind.COLOR_CONTRACTION + return None + + +def _existing_evaluator_container_path( + existing: ArtifactManifest | None, +) -> str | None: + if existing is None: + return None + raw = existing.extensions.get(_EVALUATOR_PAYLOAD_CONTAINER_EXTENSION) + if raw is None: + return None + extension = _mapping(raw) + if str(extension.get("path")) != _EVALUATOR_PAYLOAD_CONTAINER_PATH: + raise ValueError("append artifact has an incompatible evaluator container path") + return _EVALUATOR_PAYLOAD_CONTAINER_PATH + + +def _evaluator_payload_container_extension(index: PacbinIndex) -> dict[str, object]: + return { + "kind": _EVALUATOR_PAYLOAD_CONTAINER_KIND, + "schema_version": _EVALUATOR_PAYLOAD_CONTAINER_SCHEMA_VERSION, + "storage_abi": _EVALUATOR_PAYLOAD_CONTAINER_STORAGE_ABI, + "path": _EVALUATOR_PAYLOAD_CONTAINER_PATH, + "member_count": len(index.members), + "unpacked_size_bytes": sum(member.length for member in index.members), + "index_sha256": index.index_sha256, + } + + +def _write_recurrence_schedule_roots( + builder: ArtifactBuilder, + evaluator_payloads: _EvaluatorPayloadCollector, + plan: RecurrenceScheduleSharingPlan, +) -> dict[str, object]: + """Stage each interned schedule once and publish its root binding index.""" + + for schedule in plan.schedules: + record = evaluator_payloads.add_file( + schedule.artifact_path, + schedule.source_path, + process_id=None, + ) + if record.sha256 != schedule.sha256 or record.size_bytes != schedule.size_bytes: + raise ValueError( + f"interned recurrence schedule {schedule.digest} changed before " + "root publication" + ) + index_record = builder.add_json( + RECURRENCE_SCHEDULE_INDEX_PATH, + plan.to_mapping(), + role="evaluator-manifest", + compact=True, + ) + return plan.extension_mapping(index_sha256=index_record.sha256) + + +def _validated_recurrence_helicity_selector_schedule_digest( + companion: RecurrenceHelicitySelectorPlanArtifact, +) -> str: + base_digest = _canonical_sha256( + companion.recurrence_base_schedule_digest, + "recurrence helicity-selector base schedule digest", + ) + native_digest = _canonical_sha256( + companion.recurrence_native_schedule_semantic_digest, + "recurrence helicity-selector native schedule digest", + ) + if base_digest != native_digest: + raise ValueError( + "recurrence helicity-selector plan must use relation-free native " + "schedule identity" + ) + digest = _canonical_sha256( + companion.recurrence_schedule_digest, + "recurrence helicity-selector schedule digest", + ) + expected = recurrence_helicity_selector_schedule_digest( + base_digest, + companion.helicity_dispatch_sha256, + ) + if digest != expected: + raise ValueError( + "recurrence helicity-selector root digest does not bind its Direct " + "plan and helicity dispatch" + ) + return digest + + +def _recurrence_helicity_selector_schedule_path( + companion: RecurrenceHelicitySelectorPlanArtifact, +) -> str: + digest = _validated_recurrence_helicity_selector_schedule_digest(companion) + return ( + f"{_RECURRENCE_HELICITY_SELECTOR_SCHEDULE_ROOT}/{digest}/" + f"{_RECURRENCE_RUNTIME_CONTAINER_PATH}" + ) + + +def _recurrence_helicity_dispatch_path( + companion: RecurrenceHelicitySelectorPlanArtifact, +) -> str: + digest = _validated_recurrence_helicity_selector_schedule_digest(companion) + return ( + f"{_RECURRENCE_HELICITY_SELECTOR_SCHEDULE_ROOT}/{digest}/" + f"{_RECURRENCE_HELICITY_DISPATCH_PATH}" + ) + + +def _write_recurrence_helicity_selector_schedule_roots( + evaluator_payloads: _EvaluatorPayloadCollector, + processes: Sequence[ProcessArtifact], + *, + recurrence_sharing: RecurrenceScheduleSharingPlan | None, +) -> None: + """Stage every persisted selector schedule once by semantic digest.""" + + primary_by_digest = ( + {} + if recurrence_sharing is None + else {schedule.digest: schedule for schedule in recurrence_sharing.schedules} + ) + selector_by_digest: dict[str, RecurrenceHelicitySelectorPlanArtifact] = {} + reused_primary_digests: set[str] = set() + for process in processes: + if not isinstance(process, RecurrenceProcessArtifact): + continue + companion = process.helicity_selector_companion + if not isinstance(companion, RecurrenceHelicitySelectorPlanArtifact): + continue + digest = _validated_recurrence_helicity_selector_schedule_digest(companion) + primary = primary_by_digest.get(digest) + if primary is not None: + if ( + primary.sha256 != companion.recurrence_schedule_sha256 + or primary.size_bytes != companion.recurrence_schedule_size_bytes + or primary.member_count != companion.recurrence_schedule_member_count + or primary.unpacked_size_bytes + != companion.recurrence_schedule_unpacked_size_bytes + or primary.index_sha256 != companion.recurrence_schedule_index_sha256 + ): + raise ValueError( + "recurrence selector and primary schedule share a digest but " + "not an identical payload" + ) + selector_by_digest[digest] = companion + reused_primary_digests.add(digest) + continue + previous = selector_by_digest.get(digest) + if previous is not None: + if ( + previous.recurrence_schedule_sha256 + != companion.recurrence_schedule_sha256 + or previous.recurrence_schedule_size_bytes + != companion.recurrence_schedule_size_bytes + or previous.recurrence_schedule_member_count + != companion.recurrence_schedule_member_count + or previous.recurrence_schedule_unpacked_size_bytes + != companion.recurrence_schedule_unpacked_size_bytes + or previous.recurrence_schedule_index_sha256 + != companion.recurrence_schedule_index_sha256 + or previous.helicity_dispatch_sha256 + != companion.helicity_dispatch_sha256 + or previous.helicity_dispatch_size_bytes + != companion.helicity_dispatch_size_bytes + or previous.helicity_dispatch_base_runtime_layout_digest + != companion.helicity_dispatch_base_runtime_layout_digest + or previous.helicity_dispatch_resolved_helicity_count + != companion.helicity_dispatch_resolved_helicity_count + ): + raise ValueError( + "recurrence helicity-selector schedule digest maps to " + "different payloads" + ) + continue + selector_by_digest[digest] = companion + + for digest, companion in sorted(selector_by_digest.items()): + if digest not in reused_primary_digests: + record = evaluator_payloads.add_file( + _recurrence_helicity_selector_schedule_path(companion), + companion.recurrence_schedule_path, + process_id=None, + ) + if ( + record.sha256 != companion.recurrence_schedule_sha256 + or record.size_bytes != companion.recurrence_schedule_size_bytes + ): + raise ValueError( + f"persisted recurrence helicity-selector schedule {digest} " + "changed before publication" + ) + dispatch_record = evaluator_payloads.add_file( + _recurrence_helicity_dispatch_path(companion), + companion.helicity_dispatch_path, + process_id=None, + ) + if ( + dispatch_record.sha256 != companion.helicity_dispatch_sha256 + or dispatch_record.size_bytes != companion.helicity_dispatch_size_bytes + ): + raise ValueError( + f"persisted recurrence helicity dispatch {digest} changed before " + "publication" + ) + + +def _write_umami_bundle( + builder: ArtifactBuilder, + *, + processes: Sequence[ProcessArtifact], + process_records: Sequence[Mapping[str, object]], + compiled_model: CompiledModel, + process_groups: ProcessGroupMap | None = None, +) -> None: + """Add export-only selector tables after runtime physics has been staged.""" + from ..artifacts.umami import UmamiProcessInput, umami_bundle_payloads + from ..artifacts.umami_otf import restore_umami_on_the_fly_input + + new_processes = {process.process_id: process for process in processes} + inputs = [] + previous_umami = None + for record in process_records: + identifier = str(record["id"]) + process = new_processes.get(identifier) + physics = ( + process.umami_physics + if process is not None and process.umami_physics is not None + else json.loads( + builder.staged_path(str(record["physics_path"])).read_text( + encoding="utf-8" + ) + ) + ) + umami_input = UmamiProcessInput( + process_id=identifier, + expression=str(record["expression"]), + color_accuracy=str(record["color_accuracy"]), + external_pdgs=tuple(int(pdg) for pdg in record["external_pdgs"]), + physics=physics, + aliases=tuple(record.get("aliases", ())), + color_sectors=() if process is None else process.umami_color_sectors, + structural_keys=( + None if process is None else process.umami_structural_keys + ), + # Flavour-blind members were not generated: they are exported as + # selectors evaluated through this representative's runtime. + group_members=( + () + if process_groups is None or identifier not in process_groups.groups + else tuple( + member + for member in process_groups.groups[identifier].members[1:] + if not member.generated + ) + ), + ) + if physics.get("kind") == ON_THE_FLY_PUBLIC_METADATA_KIND: + # Appended processes retain compact runtime physics. Their previous + # SDK already contains every physical selector and colour tag. + if previous_umami is None: + try: + previous = json.loads( + builder.staged_path("API/umami/metadata.json").read_text( + encoding="utf-8" + ) + ) + previous_umami = ( + [ + json.loads( + builder.staged_path( + "API/umami/" + str(provider["metadata"]) + ).read_text(encoding="utf-8") + ) + for provider in previous["providers"] + ] + if previous.get("kind") == "pyamplicol-umami-provider-index" + else [previous] + ) + except FileNotFoundError as exc: + raise ValueError( + "appending this OTF artifact requires its existing UMAMI " + "metadata; regenerate the complete process set" + ) from exc + umami_input = restore_umami_on_the_fly_input(umami_input, previous_umami) + inputs.append(umami_input) + for payload in umami_bundle_payloads( + processes=inputs, + compiled_model=compiled_model.to_dict(), + artifact_id=compute_artifact_id( + {"payloads": [record.as_dict() for record in builder.payload_records()]} + ), + ): + builder.add_bytes( + payload.path, + payload.content, + role=payload.role, + media_type=payload.media_type, + executable=payload.executable, + ) + + +def write_schema_v3_artifact( + destination: str | Path, + *, + mode: Literal["error", "append", "replace"], + source: ModelSource, + compiled_model: CompiledModel, + configuration: _GenerationConfigProvenance, + processes: Sequence[ProcessArtifact], + timings: Mapping[str, float], + api_bundle_hook: ApiBundleHook | None = None, + progress_callback: Callable[[dict[str, object]], None] | None = None, + payload_hook: ArtifactPayloadHook | None = None, + process_groups: ProcessGroupMap | None = None, +) -> ArtifactWriteResult: + if not processes: + raise ValueError("schema-v3 generation requires at least one concrete process") + output = Path(destination).expanduser().resolve(strict=False) + existing = load_manifest(output) if mode == "append" else None + if existing is not None: + _reject_legacy_eager_append(existing) + hook = api_bundle_hook or _default_api_bundle_hook() + requested_config = _config_payload(configuration.requested) + effective_config = _config_payload(configuration.effective) + bundle_requested = _bundle_requested(configuration.effective) + existing_bundle = _existing_bundle_path(existing) + can_emit_bundle = hook is not None or existing_bundle is not None + effective_config = _effective_config_payload( + effective_config, + disable_api_bundle=bundle_requested and not can_emit_bundle, + ) + adjustments = [ + {"path": adjustment.path, "reason": adjustment.reason} + for adjustment in configuration.adjustments + ] + if bundle_requested and not can_emit_bundle: + adjustments.append( + { + "path": "generation.emit_api_bundle", + "reason": "no root API-bundle emitter is installed", + } + ) + requested_bytes = _toml_bytes(requested_config) + effective_bytes = _toml_bytes(effective_config) + required_runtime_capabilities = set( + _existing_required_runtime_capabilities(existing) + ) + for process in processes: + required_runtime_capabilities.update(_process_runtime_capabilities(process)) + canonical_runtime_capabilities = tuple(sorted(required_runtime_capabilities)) + producer = _producer_metadata( + configuration.effective, + runtime_capabilities=canonical_runtime_capabilities, + implicit_portable_jit_evidence=_implicit_generation_portable_jit_evidence( + processes + ), + ) + model = _model_metadata(source, compiled_model) + dependencies = _dependency_metadata(source) + eager_pack_identity = _eager_prepared_pack_identity( + existing, + compiled_model=compiled_model, + processes=processes, + ) + recurrence_processes = tuple( + process + for process in processes + if isinstance(process, RecurrenceProcessArtifact) + ) + if existing is not None and recurrence_processes: + raise ValueError( + "recurrence root schedules and process bindings are immutable; " + "regenerate the recurrence process set instead of appending" + ) + recurrence_sharing_plan = ( + intern_recurrence_schedules(recurrence_processes) + if existing is None and recurrence_processes + else None + ) + _validate_append_compatibility( + existing, + producer=producer, + model=model, + eager_pack_identity=eager_pack_identity, + requested_bytes=requested_bytes, + effective_bytes=effective_bytes, + adjustments=adjustments, + processes=processes, + ) + if existing is not None: + producer = _plain_mapping(existing.producer) + model = _plain_mapping(existing.model) + dependencies = tuple(_plain_mapping(item) for item in existing.dependencies) + + target = _mapping(producer["target"]) + process_records = _existing_process_records(existing) + evaluator_entries = _existing_evaluator_entries(existing) + execution_manifest_sha256_by_process: dict[str, str] = {} + validation_records = tuple(process.validation_point for process in processes) + validations = validation_point_map(validation_records) + bundle_points = _existing_bundle_points(existing) + bundle_points.update(build_api_validation_points(processes)) + api_bundle_path = existing_bundle + write_mode = cast("ArtifactWriteMode", mode) + with ArtifactBuilder( + output, + mode=write_mode, + expected_artifact_id=(existing.artifact_id if existing is not None else None), + ) as builder: + evaluator_payloads = _EvaluatorPayloadCollector( + builder, + existing=existing, + target=target, + ) + recurrence_sharing_extension = ( + _write_recurrence_schedule_roots( + builder, + evaluator_payloads, + recurrence_sharing_plan, + ) + if recurrence_sharing_plan is not None + else None + ) + _write_recurrence_helicity_selector_schedule_roots( + evaluator_payloads, + processes, + recurrence_sharing=recurrence_sharing_plan, + ) + if existing is None: + if progress_callback is not None: + progress_callback( + { + "step": "global payloads", + "completed": 0, + "total": len(processes) + 2, + } + ) + _write_global_payloads( + builder, + compiled_model=compiled_model, + requested_bytes=requested_bytes, + effective_bytes=effective_bytes, + ) + retain_recurrence_templates = ( + RECURRENCE_DIRECT_ARENA_RUNTIME_CAPABILITY in required_runtime_capabilities + or ON_THE_FLY_RUNTIME_CAPABILITY in required_runtime_capabilities + or RECURRENCE_HELICITY_SELECTOR_COMPANION_RUNTIME_CAPABILITY + in required_runtime_capabilities + ) + eager_kernel_ids = _prepared_kernel_ids( + output, + existing, + compiled_model=compiled_model, + processes=processes, + retain_recurrence_templates=retain_recurrence_templates, + ) + if eager_kernel_ids: + if progress_callback is not None: + progress_callback( + { + "step": "prepared kernel pack", + "completed": 1, + "total": len(processes) + 2, + "kernel_count": len(eager_kernel_ids), + } + ) + _write_eager_kernel_pack( + builder, + compiled_model, + kernel_ids=eager_kernel_ids, + evaluator_payloads=evaluator_payloads, + require_eager_direct=( + EAGER_DIRECT_ARENA_RUNTIME_CAPABILITY + in required_runtime_capabilities + or ON_THE_FLY_RUNTIME_CAPABILITY in required_runtime_capabilities + or RECURRENCE_HELICITY_SELECTOR_COMPANION_RUNTIME_CAPABILITY + in required_runtime_capabilities + ), + retain_recurrence_templates=retain_recurrence_templates, + ) + for process_index, process in enumerate(processes, start=1): + if progress_callback is not None: + progress_callback( + { + "step": "process payloads", + "completed": process_index, + "total": len(processes) + 2, + "process": process.process_id, + } + ) + record, evaluator_entry, execution_sha256 = _write_process_payloads( + builder, + process, + evaluator_payloads=evaluator_payloads, + recurrence_sharing=recurrence_sharing_plan, + ) + process_records.append(record) + evaluator_entries.append(evaluator_entry) + execution_manifest_sha256_by_process[process.process_id] = execution_sha256 + builder.add_json( + _EVALUATOR_SET_PATH, + { + "schema_version": PROCESS_ARTIFACT_SCHEMA_VERSION, + "kind": "pyamplicol-runtime-execution-set", + "required_runtime_capabilities": list(canonical_runtime_capabilities), + "processes": evaluator_entries, + }, + role="evaluator-manifest", + ) + if bundle_requested and hook is not None: + api_bundle_path = _call_api_bundle_hook(builder, hook, bundle_points) + evaluator_payload_container = evaluator_payloads.publish() + source_revision = producer.get("git_revision") + native_build_inputs_sha256 = producer.get("native_build_inputs_sha256") + evaluator_container_path = ( + None + if evaluator_payload_container is None + else str(evaluator_payload_container["path"]) + ) + evaluator_container_index_sha256 = ( + None + if evaluator_payload_container is None + else str(evaluator_payload_container["index_sha256"]) + ) + if isinstance(source_revision, str) and isinstance( + native_build_inputs_sha256, str + ): + staged_root = builder.root + if staged_root is None: # pragma: no cover - builder invariant + raise RuntimeError("artifact builder is not active") + for process_record in process_records: + process_id = str(process_record["id"]) + execution_path = f"processes/{process_id}/execution.json" + execution_file = builder.staged_path(execution_path) + execution_payload = json.loads( + execution_file.read_text(encoding="utf-8") + ) + if not isinstance(execution_payload, Mapping): + raise ValueError( + f"execution payload for {process_id!r} is not an object" + ) + if execution_payload.get("kind") == ON_THE_FLY_RUNTIME_KIND: + continue + proof = build_generation_structural_proof( + artifact_root=staged_root, + process_id=process_id, + source_revision=source_revision, + native_build_inputs_sha256=native_build_inputs_sha256, + execution_path=execution_path, + execution_sha256=sha256_file(execution_file), + execution=execution_payload, + evaluator_container_path=evaluator_container_path, + evaluator_container_index_sha256=(evaluator_container_index_sha256), + ) + builder.add_json( + f"processes/{process_id}/structural-source-proof.json", + proof, + role=STRUCTURAL_SOURCE_PROOF_ROLE, + process_id=process_id, + compact=True, + ) + _write_recurrence_bootstraps( + builder, + processes=recurrence_processes, + process_records=process_records, + producer=producer, + runtime={ + "engine": "rusticol", + "engine_version": str(producer["version"]), + "evaluator_manifest_path": _EVALUATOR_SET_PATH, + "api_bundle_path": api_bundle_path, + "required_runtime_capabilities": list(canonical_runtime_capabilities), + }, + evaluator_payload_container=evaluator_payload_container, + target=target, + ) + # Written after the recurrence bootstrap froze its payload inventory: + # the map is rewritten in place later and is not part of that image. + group_map = _write_process_groups( + builder, + process_groups=process_groups, + process_records=process_records, + ) + extensions = _extensions( + existing, + processes=processes, + timings=timings, + api_bundle_requested=bundle_requested, + api_bundle_path=api_bundle_path, + eager_pack_identity=eager_pack_identity, + execution_manifest_sha256_by_process=(execution_manifest_sha256_by_process), + evaluator_payload_container=evaluator_payload_container, + recurrence_schedule_sharing=recurrence_sharing_extension, + ) + if payload_hook is not None: + extensions.update(payload_hook(builder)) + if bundle_requested and hook is not None: + _write_umami_bundle( + builder, + processes=processes, + process_records=process_records, + compiled_model=compiled_model, + process_groups=group_map, + ) + builder.finalize( + kind=( + "pyamplicol-process" + if len(process_records) == 1 + else "pyamplicol-process-set" + ), + producer=producer, + model=model, + configuration={ + "toml_schema_version": 1, + "requested_path": _CONFIG_REQUESTED_PATH, + "effective_path": _CONFIG_EFFECTIVE_PATH, + "adjustments": adjustments, + }, + processes=process_records, + default_process_id=str(process_records[0]["id"]), + runtime={ + "engine": "rusticol", + "engine_version": str(producer["version"]), + "evaluator_manifest_path": _EVALUATOR_SET_PATH, + "api_bundle_path": api_bundle_path, + "required_runtime_capabilities": list(canonical_runtime_capabilities), + }, + dependencies=dependencies, + extensions=extensions, + ) + staged = load_manifest(builder.root) + _validate_artifact_references(staged) + if progress_callback is not None: + progress_callback( + { + "step": "publishing artifact", + "completed": len(processes) + 2, + "total": len(processes) + 2, + "file_count": len(staged.payloads) + 1, + } + ) + + manifest = load_manifest(output) + _validate_artifact_references(manifest) + files = tuple( + [output / record.path for record in manifest.payloads] + + [output / "artifact.json"] + ) + return ArtifactWriteResult( + output=output, + files=files, + validation_points=validations, + api_bundle_path=api_bundle_path, + ) + + +def _write_process_groups( + builder: ArtifactBuilder, + *, + process_groups: ProcessGroupMap | None, + process_records: Sequence[Mapping[str, object]], +) -> ProcessGroupMap: + """Stage ``process-groups.json`` covering every process of the artifact. + + An append keeps the stored groups, and a newly generated process leaves a + group in which it was only an assumed member. Processes without a recorded + group map to themselves. + """ + + stored = builder.staged_path(PROCESS_GROUPS_PATH) + group_map = ( + ProcessGroupMap.from_json(stored.read_text(encoding="utf-8")) + if stored.is_file() + else ProcessGroupMap() + ) + record_ids = [str(record["id"]) for record in process_records] + new_ids = set(record_ids).difference( + member.process_id for member in group_map.members() if member.generated + ) + for group in group_map.groups.values(): + group.members = [ + member + for member in group.members + if member.generated or member.process_id not in new_ids + ] + if process_groups is not None: + for group in process_groups.groups.values(): + group_map.add_group( + [ + member + for member in group.members + if member.generated or group_map.group_of(member.process_id) is None + ] + ) + group_map.analyses.extend(process_groups.analyses) + grouped = {member.process_id for member in group_map.members()} + ungrouped = initial_process_groups( + ( + str(record["id"]), + str(record["expression"]), + cast(Sequence[int], record["external_pdgs"]), + ) + for record in process_records + if str(record["id"]) not in grouped + ) + for group in ungrouped.groups.values(): + group_map.add_group(group.members) + validate_process_groups(group_map, record_ids) + builder.add_bytes( + PROCESS_GROUPS_PATH, + process_groups_payload(group_map), + role=PROCESS_GROUPS_ROLE, + media_type=PROCESS_GROUPS_MEDIA_TYPE, + ) + return group_map + + +def _write_global_payloads( + builder: ArtifactBuilder, + *, + compiled_model: CompiledModel, + requested_bytes: bytes, + effective_bytes: bytes, +) -> None: + builder.add_bytes( + _CONFIG_REQUESTED_PATH, + requested_bytes, + role="configuration-requested", + media_type="application/toml", + ) + builder.add_bytes( + _CONFIG_EFFECTIVE_PATH, + effective_bytes, + role="configuration-effective", + media_type="application/toml", + ) + builder.add_json( + _COMPILED_MODEL_PATH, + compiled_model.to_dict(), + role="compiled-model", + ) + builder.add_json( + _MODEL_PARAMETERS_PATH, + { + "schema_version": 1, + "kind": "pyamplicol-model-parameter-defaults", + "parameters": { + name: [float(value[0]), float(value[1])] + for name, value in sorted(compiled_model.parameter_defaults.items()) + }, + }, + role="model-parameters", + ) + + +def _write_eager_kernel_pack( + builder: ArtifactBuilder, + compiled_model: CompiledModel, + *, + kernel_ids: frozenset[int], + evaluator_payloads: _EvaluatorPayloadCollector, + require_eager_direct: bool, + retain_recurrence_templates: bool, +) -> None: + bundle = compiled_model.prepared_bundle + if bundle is None: + raise ValueError( + "prepared-kernel artifact writing requires a prepared model bundle" + ) + selected = tuple( + kernel + for kernel in bundle.kernel_pack.kernels + if kernel.kernel_id in kernel_ids + ) + if {kernel.kernel_id for kernel in selected} != set(kernel_ids): + missing = sorted(set(kernel_ids) - {kernel.kernel_id for kernel in selected}) + raise ValueError(f"prepared model omits referenced eager kernels {missing}") + selected_variants = tuple( + variant + for variant in bundle.kernel_pack.kernel_variants + if variant.base_kernel_id in kernel_ids + ) + builder.discard_payloads(_EAGER_KERNEL_PACK_PATH) + builder.discard_payloads(_EAGER_KERNEL_PAYLOAD_ROOT, recursive=True) + evaluator_payloads.discard_prefix(_EAGER_KERNEL_PAYLOAD_ROOT) + pack_payload = bundle.kernel_pack.to_dict() + pack_payload["eager_kernel_abi"] = EAGER_KERNEL_ABI + if not retain_recurrence_templates: + # Process artifacts retain only the prepared kernels they execute. A + # model-wide recurrence catalog references kernels outside that + # process-local inventory and therefore cannot remain a valid member + # of an eager-only kernel pack. Eager exact execution uses the + # prepared kernel records and resolver manifest, not recurrence + # templates. + pack_payload["recurrence_template"] = None + pack_payload["recurrence_direct_template"] = None + direct_descriptors: dict[str, bytes] = {} + kernel_payloads: list[dict[str, object]] = [] + for kernel in selected: + payload = kernel.to_dict() + if require_eager_direct and _requires_eager_direct_table(kernel.contract_kind): + manifest = _mapping(payload["f64_evaluator_manifest"]) + if bundle.kernel_pack.backend == "jit": + direct_manifest, descriptor_path, descriptor = ( + _eager_direct_evaluator_manifest( + bundle=bundle, + kernel_id=kernel.kernel_id, + input_complex_count=kernel.input_arity, + output_complex_count=kernel.output_arity, + manifest=manifest, + ) + ) + direct_descriptors[descriptor_path] = descriptor + else: + direct_manifest = _eager_native_direct_evaluator_manifest( + backend=bundle.kernel_pack.backend, + bundle=bundle, + kernel_id=kernel.kernel_id, + input_complex_count=kernel.input_arity, + output_complex_count=kernel.output_arity, + manifest=manifest, + ) + payload["f64_evaluator_manifest"] = direct_manifest + kernel_payloads.append(payload) + pack_payload["kernels"] = kernel_payloads + pack_payload["kernel_variants"] = [ + variant.to_dict() for variant in selected_variants + ] + pack_payload["resolver_manifest"] = _filtered_eager_resolver_manifest( + bundle.kernel_pack.resolver_manifest, + kernel_ids, + ) + from ..models.prepared import PreparedKernelPack + + validated_pack_payload = dict(pack_payload) + validated_pack_payload.pop("eager_kernel_abi") + PreparedKernelPack.from_dict(validated_pack_payload) + builder.add_json( + _EAGER_KERNEL_PACK_PATH, + pack_payload, + role="evaluator-manifest", + compact=True, + ) + for descriptor_path, descriptor in sorted(direct_descriptors.items()): + evaluator_payloads.add_bytes( + f"{_EAGER_KERNEL_PAYLOAD_ROOT}/{descriptor_path}", + descriptor, + process_id=None, + media_type="application/octet-stream", + ) + referenced_payloads = { + path + for record in (*selected, *selected_variants) + for path in record.referenced_payload_paths + } + with zipfile.ZipFile(bundle.path, "r") as archive: + for member_path in sorted(referenced_payloads): + with archive.open(member_path, "r") as stream: + evaluator_payloads.add_stream( + f"{_EAGER_KERNEL_PAYLOAD_ROOT}/{member_path}", + cast("BinaryIO", stream), + process_id=None, + media_type=_media_type(Path(member_path)), + ) + + +def _requires_eager_direct_table(contract_kind: str) -> bool: + """Return whether a prepared kernel executes inside the eager point loop.""" + + # Model-parameter derivation is evaluated separately when the artifact is + # loaded or an override is applied. It remains on the ordinary evaluator + # path, which supports scalar operations such as square roots that are + # deliberately outside the call-free eager DirectTable ABI. + return contract_kind != "model-parameter" + + +def _eager_direct_evaluator_manifest( + *, + bundle: object, + kernel_id: int, + input_complex_count: int, + output_complex_count: int, + manifest: Mapping[str, object], +) -> tuple[dict[str, object], str, bytes]: + if manifest.get("kind") != "symjit-application-evaluator": + raise ValueError( + f"eager-direct-arena-v1 kernel {kernel_id} is not a SymJIT application" + ) + plane_application = manifest.get("plane_application") + if not isinstance(plane_application, Mapping): + raise ValueError( + f"eager-direct-arena-v1 kernel {kernel_id} predates the " + "SymJIT plane-application ABI; regenerate the prepared model" + ) + application_path = plane_application.get("application_path") + if not isinstance(application_path, str) or not application_path: + raise ValueError( + f"eager-direct-arena-v1 kernel {kernel_id} has no plane application" + ) + if ( + plane_application.get("application_abi") != SYMJIT_PLANE_APPLICATION_ABI + or plane_application.get("storage_abi") != SYMJIT_APPLICATION_ABI + or plane_application.get("input_complex_count") != input_complex_count + or plane_application.get("output_complex_count") != output_complex_count + ): + raise ValueError( + f"eager-direct-arena-v1 kernel {kernel_id} has an incompatible " + "plane-application contract" + ) + reader = getattr(bundle, "read_payload", None) + if not callable(reader): # pragma: no cover - internal type invariant + raise TypeError("prepared model bundle cannot read evaluator payloads") + source = reader(application_path) + if not isinstance(source, bytes): # pragma: no cover - bundle invariant + raise TypeError("prepared model bundle returned a non-byte evaluator payload") + started = time.perf_counter() + descriptor = _derive_eager_direct_descriptor( + source, + input_complex_count=input_complex_count, + output_complex_count=output_complex_count, + ) + descriptor_s = time.perf_counter() - started + descriptor_path = f"kernels/{kernel_id}/eager-direct-table-descriptor-v1.bin" + result = _plain_mapping(manifest) + timing = _plain_mapping(_mapping(result.get("build_timing", {}))) + timing["eager_direct_descriptor_s"] = descriptor_s + result["build_timing"] = timing + result["direct_table"] = { + "capability": EAGER_DIRECT_ARENA_RUNTIME_CAPABILITY, + "source_application_abi": SYMJIT_PLANE_APPLICATION_ABI, + "descriptor_abi": EAGER_DIRECT_TABLE_DESCRIPTOR_ABI, + "binding_abi": EAGER_DIRECT_TABLE_BINDING_ABI, + "descriptor_path": descriptor_path, + "descriptor_size_bytes": len(descriptor), + "descriptor_sha256": hashlib.sha256(descriptor).hexdigest(), + "input_complex_count": input_complex_count, + "output_complex_count": output_complex_count, + } + return result, descriptor_path, descriptor + + +def _eager_native_direct_evaluator_manifest( + *, + backend: str, + bundle: object, + kernel_id: int, + input_complex_count: int, + output_complex_count: int, + manifest: Mapping[str, object], +) -> dict[str, object]: + del bundle + if backend not in {"cpp", "asm"}: + raise ValueError(f"unsupported eager native DirectTable backend {backend!r}") + if manifest.get("kind") != "compiled-complex-evaluator": + raise ValueError( + f"eager-direct-arena-v1 kernel {kernel_id} is not a native " + "compiled evaluator" + ) + expected_capability = { + "cpp": SYMBOLICA_CPP_RUNTIME_CAPABILITY, + "asm": SYMBOLICA_ASM_RUNTIME_CAPABILITY, + }[backend] + if manifest.get("runtime_capability") != expected_capability: + raise ValueError( + f"eager-direct-arena-v1 kernel {kernel_id} native capability " + "does not match its prepared backend" + ) + direct = _mapping(manifest.get("direct_table")) + from .._internal.versions import ( + NATIVE_EAGER_DIRECT_TABLE_APPLICATION_ABI, + ) + + expected = { + "capability": EAGER_DIRECT_ARENA_RUNTIME_CAPABILITY, + "source_application_abi": (NATIVE_EAGER_DIRECT_TABLE_APPLICATION_ABI), + "descriptor_abi": EAGER_DIRECT_TABLE_DESCRIPTOR_ABI, + "binding_abi": EAGER_DIRECT_TABLE_BINDING_ABI, + } + for field, value in expected.items(): + if direct.get(field) != value: + raise ValueError( + f"eager-direct-arena-v1 kernel {kernel_id} native " + f"DirectTable {field} is incompatible" + ) + library_path = direct.get("library_path") + function_name = direct.get("function_name") + if ( + not isinstance(library_path, str) + or not library_path + or library_path != manifest.get("library_path") + or not isinstance(function_name, str) + or not function_name + ): + raise ValueError( + f"eager-direct-arena-v1 kernel {kernel_id} native DirectTable " + "library binding is invalid" + ) + if ( + direct.get("input_complex_count") != input_complex_count + or direct.get("output_complex_count") != output_complex_count + ): + raise ValueError( + f"eager-direct-arena-v1 kernel {kernel_id} native DirectTable " + "width is incompatible" + ) + return _plain_mapping(manifest) + + +def _derive_eager_direct_descriptor( + source_application: bytes, + *, + input_complex_count: int, + output_complex_count: int, +) -> bytes: + operation = _eager_direct_descriptor_operation() + descriptor = operation( + source_application, + input_complex_count, + output_complex_count, + ) + if not isinstance(descriptor, bytes) or not descriptor: + raise RuntimeError("Rusticol returned an invalid eager DirectTable descriptor") + return descriptor + + +@cache +def _eager_direct_descriptor_operation() -> Callable[[bytes, int, int], bytes]: + try: + rusticol = importlib.import_module("pyamplicol._rusticol") + verify_native_module(rusticol) + except (ImportError, OSError, RuntimeError) as exc: + raise RuntimeError( + "eager-direct-arena-v1 descriptor generation requires the current " + "pyamplicol native extension" + ) from exc + operation = getattr(rusticol, "_eager_direct_descriptor_v1", None) + if not callable(operation): + raise RuntimeError( + "the current pyamplicol native extension does not expose " + "_eager_direct_descriptor_v1; rebuild it before generating artifacts" + ) + return operation + + +def _prepared_kernel_ids( + output: Path, + existing: ArtifactManifest | None, + *, + compiled_model: CompiledModel, + processes: Sequence[ProcessArtifact], + retain_recurrence_templates: bool, +) -> frozenset[int]: + has_prepared_process = any( + _is_prepared_kernel_process(process) for process in processes + ) + kernel_ids = { + kernel_id + for process in processes + if _is_prepared_kernel_process(process) + for kernel_id in _prepared_referenced_kernel_ids(process) + } + has_existing_pack = ( + existing is not None and (output / _EAGER_KERNEL_PACK_PATH).is_file() + ) + if has_existing_pack: + try: + prior = json.loads( + (output / _EAGER_KERNEL_PACK_PATH).read_text(encoding="utf-8") + ) + kernel_ids.update( + int(_mapping(item)["kernel_id"]) + for item in _sequence(_mapping(prior)["kernels"]) + ) + except (KeyError, OSError, TypeError, ValueError, json.JSONDecodeError) as exc: + raise ValueError( + "existing eager kernel pack is malformed; replace the artifact" + ) from exc + if has_prepared_process or has_existing_pack: + bundle = compiled_model.prepared_bundle + if bundle is None: + raise ValueError( + "prepared-kernel artifact writing requires a prepared model bundle" + ) + parameter_kernel_id = bundle.kernel_pack.resolver_manifest.get( + "model_parameter_kernel_id" + ) + if parameter_kernel_id is not None: + kernel_ids.add(int(parameter_kernel_id)) + if retain_recurrence_templates: + # The authenticated recurrence companions are model-wide rather + # than process-local. Retaining them therefore requires their + # complete prepared-kernel inventory, including bindings that a + # particular process schedule does not happen to exercise. + kernel_ids.update(kernel.kernel_id for kernel in bundle.kernel_pack.kernels) + return frozenset(kernel_ids) + + +def _eager_prepared_pack_identity( + existing: ArtifactManifest | None, + *, + compiled_model: CompiledModel, + processes: Sequence[ProcessArtifact], +) -> dict[str, object] | None: + existing_uses_eager = existing is not None and ( + bool( + { + EAGER_DIRECT_ARENA_RUNTIME_CAPABILITY, + EAGER_RUNTIME_CAPABILITY, + EAGER_PLAN_V3_RUNTIME_CAPABILITY, + RECURRENCE_DIRECT_ARENA_RUNTIME_CAPABILITY, + RECURRENCE_COLOR_RUNTIME_CAPABILITY, + ON_THE_FLY_CONTRACTED_COLOR_RUNTIME_CAPABILITY, + ON_THE_FLY_RUNTIME_CAPABILITY, + ON_THE_FLY_LC_COLOR_RUNTIME_CAPABILITY, + RECURRENCE_HELICITY_SELECTOR_COMPANION_RUNTIME_CAPABILITY, + }.intersection(_required_runtime_capabilities(existing.runtime)) + ) + or any(record.path == _EAGER_KERNEL_PACK_PATH for record in existing.payloads) + ) + incoming_uses_eager = any( + _is_prepared_kernel_process(process) for process in processes + ) + if not existing_uses_eager and not incoming_uses_eager: + return None + bundle = compiled_model.prepared_bundle + if bundle is None: + raise ValueError( + "prepared-kernel artifact writing requires a prepared model bundle" + ) + return { + "kind": _EAGER_PACK_IDENTITY_KIND, + "schema_version": _EAGER_PACK_IDENTITY_SCHEMA_VERSION, + "abi": PREPARED_KERNEL_PACK_IDENTITY_ABI, + "eager_kernel_abi": EAGER_KERNEL_ABI, + "identity_sha256": prepared_kernel_pack_manifest_identity_sha256( + bundle.manifest + ), + "backend": bundle.kernel_pack.backend, + "kernel_count": len(bundle.kernel_pack.kernels), + } + + +def _is_prepared_kernel_process(process: ProcessArtifact) -> bool: + return isinstance( + process, + EagerPlanV3ProcessArtifact + | RecurrenceProcessArtifact + | OnTheFlyProcessArtifact, + ) + + +def _prepared_referenced_kernel_ids( + process: ProcessArtifact, +) -> frozenset[int]: + if isinstance(process, RecurrenceProcessArtifact): + return process.referenced_kernel_ids + if isinstance(process, EagerPlanV3ProcessArtifact): + return process.referenced_kernel_ids + if isinstance(process, OnTheFlyProcessArtifact): + return process.referenced_kernel_ids + raise TypeError("compiled process has no prepared-kernel references") + + +def _filtered_eager_resolver_manifest( + manifest: Mapping[str, object], + kernel_ids: frozenset[int], +) -> dict[str, object]: + result = _plain_mapping(manifest) + for field in ( + "vertex_bindings", + "propagator_bindings", + "closure_bindings", + ): + if field not in manifest: + continue + result[field] = [ + _plain_mapping(record) + for item in _sequence(manifest[field]) + if (record := _mapping(item)).get("kernel_id") is None + or int(record["kernel_id"]) in kernel_ids + ] + parameter_kernel_id = manifest.get("model_parameter_kernel_id") + if parameter_kernel_id is not None and int(parameter_kernel_id) not in kernel_ids: + result["model_parameter_kernel_id"] = None + return result + + +def _write_process_payloads( + builder: ArtifactBuilder, + process: ProcessArtifact, + *, + evaluator_payloads: _EvaluatorPayloadCollector, + recurrence_sharing: RecurrenceScheduleSharingPlan | None = None, +) -> tuple[ + dict[str, object], + dict[str, object], + str, +]: + prefix = f"processes/{process.process_id}" + physics_path = f"{prefix}/physics.json" + execution_path = f"{prefix}/execution.json" + validation_path = f"{prefix}/validation-momenta.json" + schema: Mapping[str, object] + if isinstance( + process, + EagerPlanV3ProcessArtifact + | RecurrenceProcessArtifact + | OnTheFlyProcessArtifact, + ): + schema = {} + physics = _mapping(process.physics) + else: + schema = _runtime_schema_mapping(process.runtime_schema) + physics = _mapping(schema.get("physics")) + if physics.get("process_id") != process.process_id: + raise ValueError( + f"runtime physics process ID does not match {process.process_id!r}" + ) + if isinstance(process, OnTheFlyProcessArtifact): + if ( + physics.get("schema_version") != 1 + or physics.get("kind") != ON_THE_FLY_PUBLIC_METADATA_KIND + or process.color_accuracy not in {"lc", "nlc", "full"} + or physics.get("color_accuracy") != process.color_accuracy + ): + raise ValueError( + "on-the-fly generation returned incompatible compact public metadata" + ) + elif isinstance( + process, EagerPlanV3ProcessArtifact | RecurrenceProcessArtifact + ) and ( + physics.get("schema_version") != RUNTIME_PHYSICS_SCHEMA_VERSION + or physics.get("kind") != "pyamplicol-resolved-physics" + ): + raise ValueError( + "Rust prepared lowering returned incompatible physics metadata" + ) + builder.add_json( + physics_path, + physics, + role="runtime-physics", + process_id=process.process_id, + compact=True, + ) + if isinstance(process, EagerPlanV3ProcessArtifact): + runtime_path = f"{prefix}/{_EAGER_RUNTIME_CONTAINER_PATH}" + runtime_record = evaluator_payloads.add_file( + runtime_path, + process.eager_runtime_path, + process_id=process.process_id, + ) + _validate_staged_eager_runtime(process, runtime_record) + execution_record = builder.add_bytes( + execution_path, + _eager_execution_summary(process), + role="evaluator-manifest", + media_type="application/json", + process_id=process.process_id, + ) + elif isinstance(process, RecurrenceProcessArtifact): + if recurrence_sharing is None: + raise ValueError( + "recurrence artifacts require root schedule and process-binding " + "publication" + ) + binding = recurrence_sharing.binding(process.process_id) + schedule = recurrence_sharing.schedule(binding.schedule_digest) + binding_record = evaluator_payloads.add_bytes( + binding.artifact_path, + binding.payload, + process_id=process.process_id, + ) + if binding_record.sha256 != binding.sha256 or binding_record.size_bytes != len( + binding.payload + ): + raise ValueError( + f"recurrence process binding {process.process_id!r} changed " + "during publication" + ) + color_contraction_record = None + if process.color_contraction_payload is not None: + if process.color_contraction_summary is None: + raise ValueError( + "recurrence color-contraction payload has no bounded summary" + ) + color_contraction_record = evaluator_payloads.add_bytes( + f"{prefix}/{_RECURRENCE_COLOR_CONTRACTION_PATH}", + process.color_contraction_payload, + process_id=process.process_id, + media_type="application/octet-stream", + ) + elif process.color_contraction_summary is not None: + raise ValueError( + "recurrence color-contraction summary has no binary payload" + ) + companion_binding = None + companion = process.helicity_selector_companion + if isinstance(companion, RecurrenceHelicitySelectorPlanArtifact): + if process.color_accuracy not in {"nlc", "full"}: + raise ValueError( + "persisted recurrence helicity-selector plan requires NLC/full" + ) + companion_binding_record = RecurrenceProcessBinding( + process_id=process.process_id, + schedule_digest=companion.recurrence_schedule_digest, + native_schedule_semantic_digest=( + companion.recurrence_native_schedule_semantic_digest + ), + process_digest=process.process_digest, + process_semantic_digest=companion.builder_input_sha256, + process_support_mask=process.process_support_mask, + remap=companion.recurrence_process_remap, + executor_pack=companion.recurrence_process_executor_pack, + ) + stored_binding = evaluator_payloads.add_bytes( + f"{prefix}/{_RECURRENCE_HELICITY_SELECTOR_BINDING_PATH}", + companion_binding_record.payload, + process_id=process.process_id, + ) + if ( + stored_binding.sha256 != companion_binding_record.sha256 + or stored_binding.size_bytes != len(companion_binding_record.payload) + ): + raise ValueError( + "recurrence helicity-selector process binding changed during " + "publication" + ) + companion_binding = { + **companion_binding_record.to_mapping(), + "path": _RECURRENCE_HELICITY_SELECTOR_BINDING_PATH, + } + elif companion is not None: # pragma: no cover - dataclass typing guard + raise TypeError( + "recurrence helicity-selector companion must be a persisted Direct plan" + ) + execution_record = builder.add_bytes( + execution_path, + _bounded_recurrence_execution_summary( + process, + schedule_path=schedule.artifact_path, + binding=binding.to_mapping(), + color_contraction_record=color_contraction_record, + companion_binding=companion_binding, + ), + role="evaluator-manifest", + media_type="application/json", + process_id=process.process_id, + ) + elif isinstance(process, OnTheFlyProcessArtifact): + runtime_path = f"{prefix}/{_ON_THE_FLY_RUNTIME_CONTAINER_PATH}" + runtime_record = evaluator_payloads.add_file( + runtime_path, + process.runtime_path, + process_id=process.process_id, + ) + _validate_staged_on_the_fly_runtime(process, runtime_record) + color_contraction_record = None + if process.color_contraction_payload is not None: + if process.color_contraction_summary is None: + raise ValueError( + "on-the-fly color-contraction payload has no bounded summary" + ) + color_contraction_record = evaluator_payloads.add_bytes( + f"{prefix}/{_ON_THE_FLY_COLOR_CONTRACTION_PATH}", + process.color_contraction_payload, + process_id=process.process_id, + media_type="application/octet-stream", + ) + elif process.color_contraction_summary is not None: + raise ValueError( + "on-the-fly color-contraction summary has no binary payload" + ) + execution_record = builder.add_bytes( + execution_path, + _on_the_fly_execution_summary( + process, + color_contraction_record=color_contraction_record, + ), + role="evaluator-manifest", + media_type="application/json", + process_id=process.process_id, + ) + elif isinstance(process, CompiledProcessArtifact): + color_contraction_payload_path = None + if process.color_contraction_payload is not None: + color_contraction_payload_path = _COMPILED_COLOR_CONTRACTION_MEMBER_PATH + evaluator_payloads.add_bytes( + f"{prefix}/{color_contraction_payload_path}", + process.color_contraction_payload, + process_id=process.process_id, + media_type="application/octet-stream", + ) + execution_record = builder.add_json( + execution_path, + _execution_manifest( + process, + schema, + color_contraction_payload_path=color_contraction_payload_path, + ), + role="evaluator-manifest", + process_id=process.process_id, + compact=True, + ) + else: # pragma: no cover - exhaustive ProcessArtifact union + raise TypeError(f"unsupported process artifact {type(process).__name__}") + builder.add_json( + validation_path, + process.validation_point.to_mapping(), + role="validation-momenta", + process_id=process.process_id, + ) + if isinstance(process, CompiledProcessArtifact): + _copy_evaluator_payloads( + evaluator_payloads, + process.evaluator_root, + prefix=prefix, + process_id=process.process_id, + ) + _copy_color_selector_evaluator_payloads( + evaluator_payloads, + process.color_selector_executions, + prefix=prefix, + process_id=process.process_id, + ) + if process.helicity_sum_execution is not None: + _copy_evaluator_payloads( + evaluator_payloads, + process.helicity_sum_execution.evaluator_root, + prefix=f"{prefix}/{_HELICITY_SUM_PAYLOAD_ROOT}", + process_id=process.process_id, + ) + _copy_color_selector_evaluator_payloads( + evaluator_payloads, + process.helicity_sum_execution.color_selector_executions, + prefix=f"{prefix}/{_HELICITY_SUM_PAYLOAD_ROOT}", + process_id=process.process_id, + ) + _copy_helicity_selector_evaluator_payloads( + evaluator_payloads, + process.helicity_selector_executions, + prefix=prefix, + process_id=process.process_id, + ) + return ( + { + "id": process.process_id, + "expression": process.expression, + "color_accuracy": process.color_accuracy, + "external_pdgs": list(process.external_pdgs), + "physics_path": physics_path, + "required_runtime_capabilities": list( + _process_runtime_capabilities(process) + ), + "aliases": [dict(alias) for alias in process.aliases], + }, + { + "process_id": process.process_id, + "manifest_path": f"{process.process_id}/execution.json", + "required_runtime_capabilities": list( + _process_runtime_capabilities(process) + ), + }, + execution_record.sha256, + ) + + +def _write_recurrence_bootstraps( + builder: ArtifactBuilder, + *, + processes: Sequence[RecurrenceProcessArtifact], + process_records: Sequence[Mapping[str, object]], + producer: Mapping[str, object], + runtime: Mapping[str, object], + evaluator_payload_container: Mapping[str, object] | None, + target: Mapping[str, object], +) -> None: + """Publish authenticated native recurrence load images. + + This v1 image is deliberately emitted only for a single-process artifact: + multiple self-authenticating sidecars would create a cross-hash cycle, and + mixed process sets can have artifact-wide capabilities not owned by this + recurrence process. Such artifacts retain the generic loader. + + The image contains a process-ready recipe referencing the authoritative + schedule/binding payloads, compact physics, and the staged payload + inventory *before* the image is added. Finalization then includes the + image's own SHA-256 record in the ordinary artifact ID. The private staging + root is supplied only while Rust lowers the recipe and is never serialized. + """ + + if not processes: + return + if len(processes) != 1 or len(process_records) != 1: + return + if evaluator_payload_container is None: + raise ValueError("recurrence bootstrap requires an evaluator payload container") + records_by_id = {str(record["id"]): record for record in process_records} + payloads = [record.as_dict() for record in builder.payload_records()] + if builder.root is None: + raise RuntimeError("recurrence bootstrap requires an active artifact builder") + build = _recurrence_bootstrap_operation() + for process in processes: + process_id = process.process_id + prefix = f"processes/{process_id}" + execution_path = f"{prefix}/execution.json" + physics_path = f"{prefix}/physics.json" + context = { + "schema_version": 1, + "staged_root": str(builder.root), + "producer": dict(producer), + "runtime": dict(runtime), + "process": dict(records_by_id[process_id]), + "payloads": payloads, + "evaluator_payload_container": dict(evaluator_payload_container), + } + image = build( + json.dumps( + context, + sort_keys=True, + separators=(",", ":"), + allow_nan=False, + ).encode("utf-8"), + builder.staged_path(execution_path).read_bytes(), + builder.staged_path(physics_path).read_bytes(), + ) + if not isinstance(image, bytes) or not image: + raise RuntimeError( + "Rusticol returned an invalid recurrence bootstrap image" + ) + builder.add_bytes( + f"{prefix}/{_RECURRENCE_BOOTSTRAP_FILENAME}", + image, + role="evaluator-state", + media_type="application/octet-stream", + executable=False, + target=target, + process_id=process_id, + ) + + +@cache +def _recurrence_bootstrap_operation() -> Callable[[bytes, bytes, bytes], bytes]: + try: + rusticol = importlib.import_module("pyamplicol._rusticol") + verify_native_module(rusticol) + except (ImportError, OSError, RuntimeError) as exc: + raise RuntimeError( + "recurrence bootstrap generation requires the current pyamplicol " + "native extension" + ) from exc + operation = getattr(rusticol, "_build_recurrence_bootstrap_v1", None) + if not callable(operation): + raise RuntimeError( + "the current pyamplicol native extension does not expose " + "_build_recurrence_bootstrap_v1; rebuild it before generating artifacts" + ) + return operation + + +def _runtime_schema_mapping( + schema: RuntimeExpressionSchema | Mapping[str, object], +) -> Mapping[str, object]: + if isinstance(schema, RuntimeExpressionSchema): + return schema.to_mapping() + return schema + + +def _validate_staged_eager_runtime( + process: EagerPlanV3ProcessArtifact, + record: PayloadRecord, +) -> None: + if process.eager_runtime_size_bytes <= 0: + raise ValueError("Rust eager runtime payload must not be empty") + payload_sha256 = _canonical_sha256( + process.eager_runtime_sha256, + "Rust eager runtime payload SHA-256", + ) + if ( + record.size_bytes != process.eager_runtime_size_bytes + or record.sha256 != payload_sha256 + ): + raise ValueError( + "Rust eager runtime payload changed after lowering and before publication" + ) + + +def _validate_staged_on_the_fly_runtime( + process: OnTheFlyProcessArtifact, + record: PayloadRecord, +) -> None: + payload_sha256 = _canonical_sha256( + process.runtime_sha256, + "on-the-fly runtime payload SHA-256", + ) + if process.runtime_size_bytes <= 0: + raise ValueError("on-the-fly runtime payload must not be empty") + if ( + record.size_bytes != process.runtime_size_bytes + or record.sha256 != payload_sha256 + ): + raise ValueError( + "on-the-fly runtime payload changed before artifact publication" + ) + with PacbinReader.open(process.runtime_path, verify_payloads=True) as reader: + index = reader.index + if ( + len(index.members) != 1 + or index.members[0].logical_path != _ON_THE_FLY_PROCESS_SEED_MEMBER_PATH + or index.members[0].kind is not PacbinMemberKind.ON_THE_FLY_PROCESS_SEED + ): + raise ValueError( + "on-the-fly runtime must contain exactly one canonical process seed" + ) + if ( + len(index.members) != process.runtime_member_count + or sum(member.length for member in index.members) + != process.runtime_unpacked_size_bytes + or index.index_sha256 != process.runtime_index_sha256 + ): + raise ValueError( + "on-the-fly runtime container metadata changed before publication" + ) + + +def _on_the_fly_execution_summary( + process: OnTheFlyProcessArtifact, + *, + color_contraction_record: PayloadRecord | None = None, +) -> bytes: + capabilities = list(_on_the_fly_process_runtime_capabilities(process)) + selector_policy = _mapping(process.selector_policy) + if set(selector_policy) - {"color_basis"} != { + "color_coverage", + "reference_color_word", + "trace_reflections_folded", + "selector_census", + }: + raise ValueError("on-the-fly selector policy fields are invalid") + color_basis = selector_policy.get("color_basis", "trace") + if color_basis not in ("trace", "adjoint"): + raise ValueError("on-the-fly selector color basis must be trace or adjoint") + if color_basis == "adjoint" and process.color_accuracy not in {"nlc", "full"}: + raise ValueError("adjoint on-the-fly color requires contracted NLC/full") + expected_coverage = "complete" if process.color_accuracy == "lc" else "contracted" + if selector_policy.get("color_coverage") != expected_coverage or not isinstance( + selector_policy.get("trace_reflections_folded"), bool + ): + raise ValueError("on-the-fly selector policy is invalid") + census = _mapping(selector_policy.get("selector_census")) + if set(census) != { + "physical_helicity_count", + "physical_color_flow_count", + }: + raise ValueError("on-the-fly selector census fields are invalid") + census_counts = {} + for field in ("physical_helicity_count", "physical_color_flow_count"): + count = _nonnegative_integer( + census.get(field), + f"on-the-fly selector census {field}", + minimum=1, + ) + if count > (1 << 64) - 1: + raise ValueError(f"on-the-fly selector census {field} exceeds u64") + census_counts[field] = count + if process.color_accuracy in {"nlc", "full"} and ( + census_counts["physical_color_flow_count"] != 1 + or selector_policy.get("trace_reflections_folded") is not False + ): + raise ValueError( + "contracted on-the-fly selector policy must expose one color " + "result without trace-reflection folding" + ) + runtime_metadata = _deep_plain(process.runtime_metadata) + if not isinstance(runtime_metadata, dict): + raise TypeError("on-the-fly runtime metadata must be a mapping") + if color_contraction_record is None: + if process.color_accuracy != "lc": + raise ValueError("contracted on-the-fly execution has no color payload") + else: + if process.color_accuracy not in {"nlc", "full"}: + raise ValueError("LC on-the-fly execution carries a color payload") + summary = _deep_plain(process.color_contraction_summary) + if not isinstance(summary, dict): + raise TypeError("on-the-fly color-contraction summary must be a mapping") + expected_summary_fields = { + "abi", + "color_accuracy", + "storage", + "includes_color_factor", + "group_count", + "sector_count", + "active_sector_count", + "component_count", + "destination_count", + "entry_count", + "logical_entry_count", + "semantic_digest", + "factorization", + } + if ( + isinstance(summary.get("factorization"), Mapping) + and summary["factorization"].get("kind") == "symmetric-group-fourier" + ): + expected_summary_fields.add("fft_provenance") + if set(summary) != expected_summary_fields: + raise ValueError("on-the-fly color-contraction summary fields are invalid") + factorization = summary.get("factorization") + symmetric_group_fft = ( + isinstance(factorization, Mapping) + and factorization.get("kind") == "symmetric-group-fourier" + ) + factorization_rank = ( + factorization.get("rank") if isinstance(factorization, Mapping) else None + ) + factorization_coset_count = ( + factorization.get("coset_count") + if isinstance(factorization, Mapping) + else None + ) + factorization_is_canonical = ( + factorization is None + if not symmetric_group_fft + else isinstance(factorization, Mapping) + and set(factorization) == {"kind", "rank", "coset_count"} + and isinstance(factorization_rank, int) + and not isinstance(factorization_rank, bool) + and 2 <= factorization_rank <= 10 + and isinstance(factorization_coset_count, int) + and not isinstance(factorization_coset_count, bool) + and factorization_coset_count >= 1 + ) + _validate_symmetric_group_fft_provenance( + summary, + context="on-the-fly color-contraction", + ) + if ( + summary.get("abi") != "pyamplicol-recurrence-color-contraction-v3" + or summary.get("color_accuracy") != process.color_accuracy + or summary.get("storage") + != ("convolution-kernels" if symmetric_group_fft else "expanded") + or summary.get("includes_color_factor") is not True + or summary.get("component_count") != 1 + or not factorization_is_canonical + or summary.get("active_sector_count") != summary.get("group_count") + or summary.get("destination_count") != summary.get("group_count") + or summary.get("logical_entry_count") != summary.get("entry_count") + or summary.get("semantic_digest") != color_contraction_record.sha256 + ): + raise ValueError("on-the-fly color-contraction summary is noncanonical") + runtime_metadata["color_contraction"] = { + **summary, + "path": _ON_THE_FLY_COLOR_CONTRACTION_PATH, + "size_bytes": color_contraction_record.size_bytes, + "sha256": color_contraction_record.sha256, + } + payload = { + "schema_version": PROCESS_ARTIFACT_SCHEMA_VERSION, + "kind": ON_THE_FLY_RUNTIME_KIND, + "required_runtime_capabilities": capabilities, + "process": process.expression, + "key": process.process_id, + "color_accuracy": process.color_accuracy, + "external_pdg_order": list(process.external_pdgs), + "kernel_pack": { + "manifest_path": _EAGER_KERNEL_PACK_PATH, + "payload_root": _EAGER_KERNEL_PAYLOAD_ROOT, + }, + "runtime_options": { + "point_tile_size": _nonnegative_integer( + process.point_tile_size, + "on-the-fly point tile size", + minimum=1, + ), + "query_construction_threads": _nonnegative_integer( + process.query_construction_threads, + "on-the-fly query construction threads", + minimum=1, + ), + }, + "selector_policy": _deep_plain(selector_policy), + "runtime_metadata": runtime_metadata, + "runtime_container": { + "kind": ON_THE_FLY_RUNTIME_CONTAINER_KIND, + "schema_version": ON_THE_FLY_RUNTIME_CONTAINER_SCHEMA_VERSION, + "storage_abi": ON_THE_FLY_RUNTIME_STORAGE_ABI, + "path": _ON_THE_FLY_RUNTIME_CONTAINER_PATH, + "seed_member_path": _ON_THE_FLY_PROCESS_SEED_MEMBER_PATH, + }, + } + try: + return ( + json.dumps( + payload, + ensure_ascii=True, + allow_nan=False, + separators=(",", ":"), + sort_keys=True, + ) + + "\n" + ).encode("ascii") + except (TypeError, ValueError) as exc: + raise ValueError( + f"on-the-fly execution summary is not canonical JSON: {exc}" + ) from exc + + +def _recurrence_helicity_selector_companion_v2_manifest( + process: RecurrenceProcessArtifact, + companion: RecurrenceHelicitySelectorPlanArtifact, + *, + binding: Mapping[str, object], +) -> dict[str, object]: + """Describe one pre-lowered, runtime-helicity-dispatched selector plan.""" + + process_digest = _canonical_sha256( + process.process_digest, + "recurrence helicity-selector process digest", + ) + if binding.get("process_digest") != process_digest: + raise ValueError( + "recurrence helicity-selector binding disagrees with its process digest" + ) + if binding.get("process_semantic_digest") != companion.builder_input_sha256: + raise ValueError( + "recurrence helicity-selector binding disagrees with its builder input" + ) + if binding.get("schedule_digest") != companion.recurrence_schedule_digest: + raise ValueError( + "recurrence helicity-selector binding disagrees with its schedule" + ) + + companion_pack = companion.recurrence_process_executor_pack + primary_pack = process.recurrence_process_executor_pack + for name in ( + "compiled_model_digest", + "recurrence_template_catalog_digest", + "prepared_kernel_pack_digest", + "direct_template_catalog_digest", + ): + if getattr(companion_pack, name) != getattr(primary_pack, name): + raise ValueError( + "recurrence helicity-selector executor pack disagrees with the " + f"primary {name.replace('_', ' ')}" + ) + if ( + companion_pack.prepared_kernel_pack_digest + != process.prepared_kernel_pack_digest + ): + raise ValueError( + "recurrence helicity-selector prepared-kernel identity disagrees " + "with its process" + ) + if ( + companion_pack.direct_template_catalog_digest + != process.direct_template_catalog_digest + ): + raise ValueError( + "recurrence helicity-selector direct-template identity disagrees " + "with its process" + ) + + primary_color = _deep_plain(process.color_contraction_summary) + if not isinstance(primary_color, dict): + raise TypeError( + "persisted recurrence helicity-selector plan requires the primary " + "color-contraction summary" + ) + if ( + process.color_accuracy not in {"nlc", "full"} + or primary_color.get("abi") != "pyamplicol-recurrence-color-contraction-v3" + or primary_color.get("color_accuracy") != process.color_accuracy + ): + raise ValueError( + "recurrence helicity-selector primary local-color view requires a " + "compatible complete-color reducer" + ) + _validate_symmetric_group_fft_provenance( + primary_color, + context="recurrence primary color-contraction", + ) + storage = primary_color.get("storage") + factorization_raw = primary_color.get("factorization") + factorization_kind = ( + factorization_raw.get("kind") + if isinstance(factorization_raw, Mapping) + else None + ) + component_count = _nonnegative_integer( + primary_color.get("component_count"), + "recurrence primary color-contraction component count", + minimum=1, + ) + supported_primary_view = ( + (storage == "expanded" and factorization_raw is None and component_count == 1) + or ( + storage == "repeated" + and factorization_kind + in {None, "klein-four-walsh", "elementary-abelian-walsh"} + ) + or ( + storage == "convolution-kernels" + and factorization_kind == "symmetric-group-fourier" + ) + ) + if not supported_primary_view: + raise ValueError( + "recurrence helicity-selector primary local-color view requires a " + "direct, Walsh, or symmetric-group Fourier reducer" + ) + group_count = _nonnegative_integer( + primary_color.get("group_count"), + "recurrence primary color-contraction group count", + minimum=1, + ) + if group_count % component_count: + raise ValueError( + "recurrence primary color-contraction groups do not form complete " + "local component blocks" + ) + local_group_count = group_count // component_count + physical_destination_count = _nonnegative_integer( + companion.physical_destination_count, + "recurrence helicity-selector physical destination count", + minimum=1, + ) + if physical_destination_count != local_group_count: + raise ValueError( + "recurrence helicity-selector physical destinations do not match " + "the primary local color-group domain" + ) + runtime_schedule = { + "kind": RECURRENCE_RUNTIME_CONTAINER_KIND, + "schema_version": RECURRENCE_RUNTIME_CONTAINER_SCHEMA_VERSION, + "storage_abi": RECURRENCE_RUNTIME_STORAGE_ABI, + "path": _recurrence_helicity_selector_schedule_path(companion), + "plan_member_path": _RECURRENCE_DIRECT_SCHEDULE_MEMBER_PATH, + "size_bytes": _nonnegative_integer( + companion.recurrence_schedule_size_bytes, + "recurrence helicity-selector schedule size", + minimum=1, + ), + "sha256": _canonical_sha256( + companion.recurrence_schedule_sha256, + "recurrence helicity-selector schedule SHA-256", + ), + "member_count": _nonnegative_integer( + companion.recurrence_schedule_member_count, + "recurrence helicity-selector schedule member count", + minimum=1, + ), + "unpacked_size_bytes": _nonnegative_integer( + companion.recurrence_schedule_unpacked_size_bytes, + "recurrence helicity-selector schedule unpacked size", + ), + "index_sha256": _canonical_sha256( + companion.recurrence_schedule_index_sha256, + "recurrence helicity-selector schedule index SHA-256", + ), + } + inspection_summary = _deep_plain(companion.inspection_summary) + if not isinstance(inspection_summary, dict): + raise TypeError( + "recurrence helicity-selector inspection summary must be a mapping" + ) + dispatch_layout_digest = _canonical_sha256( + companion.helicity_dispatch_base_runtime_layout_digest, + "recurrence helicity-dispatch base runtime-layout digest", + ) + if inspection_summary.get("runtime_layout_digest") != dispatch_layout_digest: + raise ValueError( + "recurrence helicity dispatch disagrees with its base runtime layout" + ) + native_schedule_digest = _canonical_sha256( + binding.get("native_schedule_semantic_digest"), + "recurrence helicity-selector binding native schedule digest", + ) + if inspection_summary.get("schedule_digest") != native_schedule_digest: + raise ValueError( + "recurrence helicity-selector inspection summary disagrees with its " + "native Direct-plan identity" + ) + helicity_dispatch = { + "abi": _RECURRENCE_HELICITY_DISPATCH_ABI, + "path": _recurrence_helicity_dispatch_path(companion), + "size_bytes": _nonnegative_integer( + companion.helicity_dispatch_size_bytes, + "recurrence helicity-dispatch size", + minimum=1, + ), + "sha256": _canonical_sha256( + companion.helicity_dispatch_sha256, + "recurrence helicity-dispatch SHA-256", + ), + "base_runtime_layout_digest": dispatch_layout_digest, + "resolved_helicity_count": _nonnegative_integer( + companion.helicity_dispatch_resolved_helicity_count, + "recurrence helicity-dispatch resolved-helicity count", + minimum=1, + ), + } + capabilities = list(_recurrence_process_runtime_capabilities(process)) + plan = { + "kind": RECURRENCE_RUNTIME_KIND, + "builder_input_abi": RECURRENCE_BUILDER_INPUT_ABI, + "recurrence_plan_abi": RECURRENCE_PLAN_ABI, + "runtime_layout_abi": RECURRENCE_RUNTIME_LAYOUT_ABI, + "direct_template_abi": RECURRENCE_DIRECT_TEMPLATE_ABI, + "direct_backend_abi": RECURRENCE_DIRECT_BACKEND_ABI, + "builder_input_sha256": _canonical_sha256( + companion.builder_input_sha256, + "recurrence helicity-selector builder input SHA-256", + ), + "prepared_kernel_pack_digest": _canonical_sha256( + process.prepared_kernel_pack_digest, + "recurrence helicity-selector prepared-kernel pack SHA-256", + ), + "direct_template_catalog_digest": _canonical_sha256( + process.direct_template_catalog_digest, + "recurrence helicity-selector direct-template catalog SHA-256", + ), + "required_runtime_capabilities": capabilities, + "runtime_schedule": runtime_schedule, + "helicity_dispatch": helicity_dispatch, + "process_binding": _deep_plain(binding), + "inspection_summary": inspection_summary, + } + return { + "schema_version": 2, + "kind": _RECURRENCE_HELICITY_SELECTOR_COMPANION_V2_KIND, + "process_digest": process_digest, + "plan": plan, + "color_contraction": { + "source": "primary", + "view": "primary-local-color-view-v1", + }, + } + + +def _eager_execution_summary( + process: EagerPlanV3ProcessArtifact, +) -> bytes: + try: + content = ( + json.dumps( + _eager_plan_v3_execution_manifest(process), + ensure_ascii=True, + allow_nan=False, + separators=(",", ":"), + sort_keys=True, + ) + + "\n" + ).encode("utf-8") + except (TypeError, ValueError) as exc: + raise ValueError( + f"Rust eager execution summary is not canonical JSON: {exc}" + ) from exc + return content + + +def _eager_plan_v3_execution_manifest( + process: EagerPlanV3ProcessArtifact, +) -> dict[str, object]: + lowering_input_sha256 = _canonical_sha256( + process.lowering_input_sha256, + "eager lowering input SHA-256", + ) + payload_sha256 = _canonical_sha256( + process.eager_runtime_sha256, + "Rust eager runtime payload SHA-256", + ) + index_sha256 = _canonical_sha256( + process.eager_runtime_index_sha256, + "Rust eager runtime index SHA-256", + ) + member_count = _nonnegative_integer( + process.eager_runtime_member_count, + "Rust eager runtime member count", + minimum=1, + ) + unpacked_size = _nonnegative_integer( + process.eager_runtime_unpacked_size_bytes, + "Rust eager runtime unpacked size", + ) + payload_size = _nonnegative_integer( + process.eager_runtime_size_bytes, + "Rust eager runtime payload size", + minimum=1, + ) + capabilities = list(_EAGER_PLAN_V3_RUNTIME_CAPABILITIES) + plan = { + "kind": EAGER_RUNTIME_KIND, + "eager_plan_abi": EAGER_PLAN_V3_ABI, + "lowering_input_abi": EAGER_LOWERING_INPUT_ABI, + "lowering_input_sha256": lowering_input_sha256, + "runtime_layout_abi": EAGER_RUNTIME_LAYOUT_ABI, + "required_runtime_capabilities": capabilities, + "runtime_container": { + "kind": EAGER_RUNTIME_CONTAINER_KIND, + "schema_version": EAGER_RUNTIME_CONTAINER_SCHEMA_VERSION, + "storage_abi": EAGER_RUNTIME_STORAGE_ABI, + "path": _EAGER_RUNTIME_CONTAINER_PATH, + "size_bytes": payload_size, + "sha256": payload_sha256, + "member_count": member_count, + "unpacked_size_bytes": unpacked_size, + "index_sha256": index_sha256, + }, + "inspection_summary": _deep_plain(process.inspection_summary), + "materialization_census": _eager_materialization_census(process), + } + return { + "schema_version": PROCESS_ARTIFACT_SCHEMA_VERSION, + "kind": EAGER_RUNTIME_KIND, + "required_runtime_capabilities": capabilities, + "process": process.expression, + "key": process.process_id, + "color_accuracy": process.color_accuracy, + "external_pdg_order": list(process.external_pdgs), + "eager_plan_abi": EAGER_PLAN_V3_ABI, + "kernel_pack": { + "manifest_path": _EAGER_KERNEL_PACK_PATH, + "payload_root": _EAGER_KERNEL_PAYLOAD_ROOT, + }, + "runtime_options": { + "point_tile_size": process.point_tile_size, + "workspace_mib": process.workspace_mib, + }, + "plan": plan, + "dag_summary": _dag_summary(process.dag_summary), + } + + +def _bounded_recurrence_execution_summary( + process: RecurrenceProcessArtifact, + *, + schedule_path: str, + binding: Mapping[str, object], + color_contraction_record: PayloadRecord | None, + companion_binding: Mapping[str, object] | None = None, +) -> bytes: + try: + content = ( + json.dumps( + _recurrence_execution_manifest( + process, + schedule_path=schedule_path, + binding=binding, + color_contraction_record=color_contraction_record, + companion_binding=companion_binding, + ), + ensure_ascii=True, + allow_nan=False, + separators=(",", ":"), + sort_keys=True, + ) + + "\n" + ).encode("utf-8") + except (TypeError, ValueError) as exc: + raise ValueError( + f"Rust recurrence execution summary is not canonical JSON: {exc}" + ) from exc + return content + + +def _recurrence_execution_manifest( + process: RecurrenceProcessArtifact, + *, + schedule_path: str, + binding: Mapping[str, object], + color_contraction_record: PayloadRecord | None, + companion_binding: Mapping[str, object] | None = None, +) -> dict[str, object]: + primary_process_digest = _canonical_sha256( + process.process_digest, + "recurrence primary process digest", + ) + if binding.get("process_digest") != primary_process_digest: + raise ValueError( + "recurrence primary process binding disagrees with its process digest" + ) + capabilities = list(_recurrence_process_runtime_capabilities(process)) + runtime_schedule = { + "kind": RECURRENCE_RUNTIME_CONTAINER_KIND, + "schema_version": RECURRENCE_RUNTIME_CONTAINER_SCHEMA_VERSION, + "storage_abi": RECURRENCE_RUNTIME_STORAGE_ABI, + "path": schedule_path, + "plan_member_path": _RECURRENCE_DIRECT_SCHEDULE_MEMBER_PATH, + "size_bytes": _nonnegative_integer( + process.recurrence_schedule_size_bytes, + "Rust recurrence runtime payload size", + minimum=1, + ), + "sha256": _canonical_sha256( + process.recurrence_schedule_sha256, + "Rust recurrence runtime payload SHA-256", + ), + "member_count": _nonnegative_integer( + process.recurrence_schedule_member_count, + "Rust recurrence runtime member count", + minimum=1, + ), + "unpacked_size_bytes": _nonnegative_integer( + process.recurrence_schedule_unpacked_size_bytes, + "Rust recurrence runtime unpacked size", + ), + "index_sha256": _canonical_sha256( + process.recurrence_schedule_index_sha256, + "Rust recurrence runtime index SHA-256", + ), + } + plan = { + "kind": RECURRENCE_RUNTIME_KIND, + "builder_input_abi": RECURRENCE_BUILDER_INPUT_ABI, + "recurrence_plan_abi": RECURRENCE_PLAN_ABI, + "runtime_layout_abi": RECURRENCE_RUNTIME_LAYOUT_ABI, + "direct_template_abi": RECURRENCE_DIRECT_TEMPLATE_ABI, + "direct_backend_abi": RECURRENCE_DIRECT_BACKEND_ABI, + "builder_input_sha256": _canonical_sha256( + process.builder_input_sha256, + "recurrence builder input SHA-256", + ), + "prepared_kernel_pack_digest": _canonical_sha256( + process.prepared_kernel_pack_digest, + "recurrence prepared-kernel pack SHA-256", + ), + "direct_template_catalog_digest": _canonical_sha256( + process.direct_template_catalog_digest, + "recurrence direct-template catalog SHA-256", + ), + "required_runtime_capabilities": capabilities, + "runtime_schedule": runtime_schedule, + "process_binding": _deep_plain(binding), + "inspection_summary": _deep_plain(process.inspection_summary), + } + runtime_metadata = _deep_plain(process.runtime_metadata) + if not isinstance(runtime_metadata, dict): + raise TypeError("recurrence runtime metadata must be a mapping") + if color_contraction_record is None: + runtime_metadata["color_contraction"] = None + else: + summary = _deep_plain(process.color_contraction_summary) + if not isinstance(summary, dict): + raise TypeError("recurrence color-contraction summary must be a mapping") + _validate_symmetric_group_fft_provenance( + summary, + context="recurrence color-contraction", + ) + runtime_metadata["color_contraction"] = { + **summary, + "path": _RECURRENCE_COLOR_CONTRACTION_PATH, + "size_bytes": color_contraction_record.size_bytes, + "sha256": color_contraction_record.sha256, + } + companion = process.helicity_selector_companion + if companion is None: + if companion_binding is not None: + raise ValueError( + "recurrence without a helicity-selector companion carries " + "companion metadata" + ) + companion_manifest = None + process_digest = None + elif isinstance(companion, RecurrenceHelicitySelectorPlanArtifact): + if color_contraction_record is None or companion_binding is None: + raise ValueError( + "persisted recurrence helicity-selector companion is missing its " + "authenticated plan or primary color metadata" + ) + process_digest = primary_process_digest + companion_manifest = _recurrence_helicity_selector_companion_v2_manifest( + process, + companion, + binding=companion_binding, + ) + else: # pragma: no cover - dataclass typing guards ordinary construction + raise TypeError( + "recurrence helicity-selector companion must be a persisted Direct plan" + ) + return { + "schema_version": PROCESS_ARTIFACT_SCHEMA_VERSION, + "kind": RECURRENCE_RUNTIME_KIND, + "required_runtime_capabilities": capabilities, + "process": process.expression, + "key": process.process_id, + "color_accuracy": process.color_accuracy, + "external_pdg_order": list(process.external_pdgs), + "process_digest": process_digest, + "builder_input_abi": RECURRENCE_BUILDER_INPUT_ABI, + "recurrence_plan_abi": RECURRENCE_PLAN_ABI, + "runtime_layout_abi": RECURRENCE_RUNTIME_LAYOUT_ABI, + "direct_template_abi": RECURRENCE_DIRECT_TEMPLATE_ABI, + "direct_backend_abi": RECURRENCE_DIRECT_BACKEND_ABI, + "compiled_model_digest": ( + process.recurrence_process_executor_pack.compiled_model_digest + ), + "recurrence_template_catalog_digest": ( + process.recurrence_process_executor_pack.recurrence_template_catalog_digest + ), + "prepared_kernel_pack_digest": _canonical_sha256( + process.prepared_kernel_pack_digest, + "recurrence prepared-kernel pack SHA-256", + ), + "direct_template_catalog_digest": _canonical_sha256( + process.direct_template_catalog_digest, + "recurrence direct-template catalog SHA-256", + ), + "kernel_pack": { + "manifest_path": _EAGER_KERNEL_PACK_PATH, + "payload_root": _EAGER_KERNEL_PAYLOAD_ROOT, + }, + "runtime_options": { + "point_tile_size": process.point_tile_size, + "workspace_mib": process.workspace_mib, + }, + "runtime_metadata": runtime_metadata, + "helicity_selector_companion": companion_manifest, + "plan": plan, + "recurrence_summary": _deep_plain(process.recurrence_summary), + } + + +def _canonical_sha256(value: object, context: str) -> str: + if not isinstance(value, str) or re.fullmatch(r"[0-9a-f]{64}", value) is None: + raise ValueError(f"{context} must be a lowercase hexadecimal digest") + return value + + +def _nonnegative_integer( + value: object, + context: str, + *, + minimum: int = 0, +) -> int: + if isinstance(value, bool) or not isinstance(value, int) or value < minimum: + raise ValueError( + f"{context} must be an integer greater than or equal to {minimum}" + ) + return value + + +def _execution_manifest( + process: ProcessArtifact, + compiler_schema: Mapping[str, object], + *, + color_contraction_payload_path: str | None = None, +) -> dict[str, object]: + if isinstance(process, OnTheFlyProcessArtifact): + raise TypeError("on-the-fly execution manifests use the compact seed writer") + if isinstance(process, RecurrenceProcessArtifact): + raise TypeError( + "recurrence execution manifests require an interned root schedule " + "and process binding" + ) + if isinstance(process, EagerPlanV3ProcessArtifact): + return _eager_plan_v3_execution_manifest(process) + primary = _compiled_execution_lane_manifest( + runtime_schema=compiler_schema, + stage_manifest=process.stage_manifest, + model_parameter_evaluator=process.model_parameter_evaluator, + dag_summary=process.dag_summary, + payload_prefix=None, + ) + required_runtime_capabilities = set(_required_runtime_capabilities(primary)) + if color_contraction_payload_path is not None: + required_runtime_capabilities.add(SYMMETRIC_GROUP_FFT_COLOR_RUNTIME_CAPABILITY) + color_selector_executions = _compiled_color_selector_execution_manifests( + process=process, + executions=process.color_selector_executions, + parent_payload_prefix=None, + ) + if color_selector_executions: + required_runtime_capabilities.add(COMPILED_COLOR_TOPOLOGY_LANES_CAPABILITY) + required_runtime_capabilities.add(COMPILED_RUNTIME_SELECTORS_CAPABILITY) + for record in color_selector_executions: + required_runtime_capabilities.update( + _required_runtime_capabilities(_mapping(record["execution"])) + ) + helicity_sum_execution = process.helicity_sum_execution + auxiliary: dict[str, object] | None = None + if helicity_sum_execution is not None: + auxiliary = _compiled_nested_execution_manifest( + process=process, + execution=helicity_sum_execution, + payload_prefix=_HELICITY_SUM_PAYLOAD_ROOT, + ) + required_runtime_capabilities.update(_required_runtime_capabilities(auxiliary)) + required_runtime_capabilities.add(COMPILED_HELICITY_DUAL_LANE_CAPABILITY) + helicity_selector_executions = _compiled_helicity_selector_execution_manifests( + process=process, + executions=process.helicity_selector_executions, + parent_payload_prefix=None, + ) + if helicity_selector_executions: + for record in helicity_selector_executions: + required_runtime_capabilities.update( + _required_runtime_capabilities(_mapping(record["execution"])) + ) + required_runtime_capabilities.add(COMPILED_HELICITY_SELECTOR_UNION_CAPABILITY) + required_runtime_capabilities.add(COMPILED_RUNTIME_SELECTORS_CAPABILITY) + if _uses_primary_helicity_recurrence(process): + required_runtime_capabilities.add( + COMPILED_HELICITY_PRIMARY_RECURRENCE_CAPABILITY + ) + return { + "schema_version": PROCESS_ARTIFACT_SCHEMA_VERSION, + "kind": "pyamplicol-runtime-execution", + "required_runtime_capabilities": sorted(required_runtime_capabilities), + "process": process.expression, + "key": process.process_id, + "color_accuracy": process.color_accuracy, + "external_pdg_order": list(process.external_pdgs), + "compiled": primary["compiled"], + "dag_summary": primary["dag_summary"], + "materialization_census": primary["materialization_census"], + "runtime_schema": primary["runtime_schema"], + **( + {} + if color_contraction_payload_path is None + else { + "color_contraction_payload": { + "path": color_contraction_payload_path, + } + } + ), + **({} if auxiliary is None else {"helicity_sum_execution": auxiliary}), + **( + {} + if not helicity_selector_executions + else {"helicity_selector_executions": helicity_selector_executions} + ), + **( + {} + if not color_selector_executions + else {"color_selector_executions": color_selector_executions} + ), + } + + +def _compiled_helicity_selector_execution_manifests( + *, + process: CompiledProcessArtifact, + executions: Sequence[CompiledHelicitySelectorExecutionArtifact], + parent_payload_prefix: str | None, +) -> list[dict[str, object]]: + result: list[dict[str, object]] = [] + for lane_index, record in enumerate( + _ordered_helicity_selector_executions(executions) + ): + execution = record.execution + if execution.color_selector_executions: + raise ValueError( + "compiled helicity-selector closure execution cannot contain " + "nested execution lanes" + ) + lane_prefix = f"{_HELICITY_SELECTOR_UNION_PAYLOAD_ROOT}/class-{lane_index}" + payload_prefix = ( + lane_prefix + if parent_payload_prefix is None + else f"{parent_payload_prefix.rstrip('/')}/{lane_prefix}" + ) + manifest = _compiled_nested_execution_manifest( + process=process, + execution=execution, + payload_prefix=payload_prefix, + ) + if ( + record.schedule_mode == "parent-closure" + and COMPILED_RUNTIME_SELECTORS_CAPABILITY + in _required_runtime_capabilities(manifest) + ): + raise ValueError( + "compiled helicity-selector closure stage evaluators cannot require " + "runtime selectors" + ) + result.append( + { + "selector_domain_ids": list(record.selector_domain_ids), + "schedule_mode": record.schedule_mode, + "execution": manifest, + } + ) + return result + + +def _compiled_nested_execution_manifest( + *, + process: CompiledProcessArtifact, + execution: CompiledExecutionArtifact, + payload_prefix: str, +) -> dict[str, object]: + runtime_schema = _runtime_schema_mapping(execution.runtime_schema) + lane = _compiled_execution_lane_manifest( + runtime_schema=runtime_schema, + stage_manifest=execution.stage_manifest, + model_parameter_evaluator=execution.model_parameter_evaluator, + dag_summary=execution.dag_summary, + payload_prefix=payload_prefix, + ) + color_selector_executions = _compiled_color_selector_execution_manifests( + process=process, + executions=execution.color_selector_executions, + parent_payload_prefix=payload_prefix, + ) + required_runtime_capabilities = set(_required_runtime_capabilities(lane)) + if _runtime_schema_uses_primary_helicity_recurrence( + runtime_schema, + has_helicity_sum_execution=False, + ): + required_runtime_capabilities.add( + COMPILED_HELICITY_PRIMARY_RECURRENCE_CAPABILITY + ) + helicity_selector_executions = _compiled_helicity_selector_execution_manifests( + process=process, + executions=execution.helicity_selector_executions, + parent_payload_prefix=payload_prefix, + ) + if helicity_selector_executions: + required_runtime_capabilities.add(COMPILED_HELICITY_SELECTOR_UNION_CAPABILITY) + required_runtime_capabilities.add(COMPILED_RUNTIME_SELECTORS_CAPABILITY) + for record in helicity_selector_executions: + required_runtime_capabilities.update( + _required_runtime_capabilities(_mapping(record["execution"])) + ) + if color_selector_executions: + required_runtime_capabilities.add(COMPILED_COLOR_TOPOLOGY_LANES_CAPABILITY) + required_runtime_capabilities.add(COMPILED_RUNTIME_SELECTORS_CAPABILITY) + for record in color_selector_executions: + required_runtime_capabilities.update( + _required_runtime_capabilities(_mapping(record["execution"])) + ) + return { + "schema_version": PROCESS_ARTIFACT_SCHEMA_VERSION, + "kind": "pyamplicol-runtime-execution", + "required_runtime_capabilities": sorted(required_runtime_capabilities), + "process": process.expression, + "key": process.process_id, + "color_accuracy": process.color_accuracy, + "external_pdg_order": list(process.external_pdgs), + "compiled": lane["compiled"], + "dag_summary": lane["dag_summary"], + "materialization_census": lane["materialization_census"], + "runtime_schema": lane["runtime_schema"], + "physics_reduction": _plain_mapping( + _mapping(_mapping(runtime_schema["physics"])["reduction"]) + ), + **( + {} + if not color_selector_executions + else {"color_selector_executions": color_selector_executions} + ), + **( + {} + if not helicity_selector_executions + else {"helicity_selector_executions": (helicity_selector_executions)} + ), + } + + +def _compiled_color_selector_execution_manifests( + *, + process: CompiledProcessArtifact, + executions: Sequence[CompiledColorSelectorExecutionArtifact], + parent_payload_prefix: str | None, +) -> list[dict[str, object]]: + result: list[dict[str, object]] = [] + for record in _ordered_color_selector_executions(executions): + lane_prefix = _color_selector_payload_prefix(record.materialized_sector_id) + payload_prefix = ( + lane_prefix + if parent_payload_prefix is None + else f"{parent_payload_prefix.rstrip('/')}/{lane_prefix}" + ) + result.append( + { + "materialized_sector_id": record.materialized_sector_id, + "execution": _compiled_nested_execution_manifest( + process=process, + execution=record.execution, + payload_prefix=payload_prefix, + ), + } + ) + return result + + +def _compiled_execution_lane_manifest( + *, + runtime_schema: Mapping[str, object], + stage_manifest: Mapping[str, object], + model_parameter_evaluator: Mapping[str, object] | None, + dag_summary: Mapping[str, object], + payload_prefix: str | None, +) -> dict[str, object]: + serialized_model_parameters = ( + None + if model_parameter_evaluator is None + else _model_parameter_evaluator(model_parameter_evaluator) + ) + stage_evaluators = _stage_evaluator_set(stage_manifest) + if payload_prefix is not None: + stage_evaluators = _prefix_evaluator_payload_paths( + stage_evaluators, + payload_prefix, + ) + if serialized_model_parameters is not None: + serialized_model_parameters = _prefix_evaluator_payload_paths( + serialized_model_parameters, + payload_prefix, + ) + required_runtime_capabilities = set( + _required_runtime_capabilities(stage_evaluators) + ) + required_runtime_capabilities.update( + _runtime_schema_walsh_color_contraction_capabilities(runtime_schema) + ) + if serialized_model_parameters is not None: + required_runtime_capabilities.update( + _required_runtime_capabilities(serialized_model_parameters) + ) + compiled_manifest: dict[str, object] = { + "kind": "generic-dag-stage-blueprint", + "runtime_available": True, + "runtime_unavailable_message": None, + "model_parameter_evaluator": serialized_model_parameters, + "stage_evaluators": stage_evaluators, + } + if COMPILED_PLANE_ARENA_RUNTIME_CAPABILITY in required_runtime_capabilities: + layout = _mapping(runtime_schema["parameter_layout"]) + compiled_manifest["momentum_slot_ids"] = _compiled_momentum_slot_ids( + stage_evaluators, + value_component_count=int(layout["value_component_count"]), + momentum_component_count=int(layout["momentum_parameter_count"]), + ) + topology_replay = runtime_schema.get("lc_topology_replay") + if topology_replay is not None: + compiled_manifest["lc_topology_replay"] = _plain_mapping( + _mapping(topology_replay) + ) + color_topology_replay = runtime_schema.get("color_topology_replay") + if color_topology_replay is not None: + compiled_manifest["color_topology_replay"] = _plain_mapping( + _mapping(color_topology_replay) + ) + helicity_recurrence = runtime_schema.get("helicity_recurrence") + if helicity_recurrence is not None: + compiled_manifest["helicity_recurrence"] = _plain_mapping( + _mapping(helicity_recurrence) + ) + serialized_dag_summary = _dag_summary( + dag_summary, + require_interaction_evaluation_count=True, + ) + return { + "kind": "pyamplicol-runtime-compiled-execution", + "required_runtime_capabilities": sorted(required_runtime_capabilities), + "compiled": compiled_manifest, + "dag_summary": serialized_dag_summary, + "materialization_census": _fully_resident_materialization_census( + serialized_dag_summary, + basis="immutable-fully-resident-compiled-dag", + ), + "runtime_schema": _execution_plan(runtime_schema), + } + + +def _compiled_momentum_slot_ids( + stage_evaluators: Mapping[str, object], + *, + value_component_count: int, + momentum_component_count: int, +) -> list[int]: + """Retain the union needed by every final compiled leaf in this lane. + + Chunk input maps are final here. Keep canonical momentum-slot numbering + and the complete lane's coverage, independently of runtime selectors. + """ + live: set[int] = set() + stages = ( + *_sequence(stage_evaluators["stages"]), + stage_evaluators["amplitude_stage"], + ) + for raw_stage in stages: + direct = _mapping(_mapping(raw_stage)["compiled_plane_arena"]) + bindings = _sequence(direct["input_bindings"]) + for raw_leaf in _sequence(direct["leaves"]): + for raw_index in _sequence(_mapping(raw_leaf)["input_indices"]): + binding = _mapping(bindings[int(raw_index)]) + if binding["kind"] != "momentum": + continue + component = int(binding["global_component"]) - value_component_count + if not 0 <= component < momentum_component_count: + raise ValueError( + "compiled momentum input is outside momentum slots" + ) + live.add(component // 4) + return sorted(live) + + +def _prefix_evaluator_payload_paths( + record: Mapping[str, object], + prefix: str, +) -> dict[str, object]: + path_fields = { + "application_path", + "descriptor_path", + "evaluator_state_path", + "library_path", + "source_path", + } + + def visit(value: object, *, field: str | None = None) -> object: + if field in path_fields and value is not None: + if not isinstance(value, str): + raise TypeError(f"evaluator payload path {field!r} must be a string") + path = Path(value) + if path.is_absolute() or ".." in path.parts: + raise ValueError( + f"evaluator payload path {value!r} is not artifact-relative" + ) + return f"{prefix.rstrip('/')}/{path.as_posix()}" + if isinstance(value, Mapping): + return { + str(key): visit(item, field=str(key)) for key, item in value.items() + } + if isinstance(value, list): + return [visit(item) for item in value] + if isinstance(value, tuple): + return [visit(item) for item in value] + return value + + result = visit(record) + if not isinstance(result, dict): # pragma: no cover - internal invariant + raise TypeError("compiled evaluator manifest must be an object") + return result + + +def _execution_plan(schema: Mapping[str, object]) -> dict[str, object]: + source_fill = _mapping(schema["source_fill"]) + source_records = tuple(_mapping(item) for item in _sequence(source_fill["sources"])) + source_count = ( + int(source_records[-1]["source_parameter_stop"]) if source_records else 0 + ) + parameter_layout = _mapping(schema["parameter_layout"]) + value_count = int(parameter_layout["value_component_count"]) + momentum_count = int(parameter_layout["momentum_parameter_count"]) + model_parameter_count = int(parameter_layout.get("model_parameter_count", 0)) + parameters = tuple( + _execution_model_parameter(_mapping(item)) + for item in _sequence(schema.get("model_parameters", ())) + ) + mass_parameters = { + int(item["pdg"]): str(item["name"]) + for item in parameters + if item["kind"] == "particle_mass" and item.get("pdg") is not None + } + model = _mapping(schema.get("model", {})) + particles = tuple(_mapping(item) for item in _sequence(model.get("particles", ()))) + normalization = _mapping(schema.get("normalization", {})) + return { + "schema_version": PROCESS_ARTIFACT_SCHEMA_VERSION, + "kind": "pyamplicol-runtime-execution-plan", + "process_key": str(schema["process_key"]), + "process": str(schema["process"]), + "external_particles": [ + _select( + _mapping(item), + "label", + "index", + "pdg", + "outgoing_pdg", + "role", + "momentum_slot", + ) + for item in _sequence(schema["external_particles"]) + ], + "model": { + "particles": [ + { + "pdg": int(particle["pdg"]), + "mass": float(particle.get("mass", 0.0)), + "mass_parameter": ( + str(particle["mass_parameter"]) + if particle.get("mass_parameter") is not None + else mass_parameters.get(int(particle["pdg"])) + ), + } + for particle in particles + ] + }, + "model_parameters": list(parameters), + "normalization": { + "color_factor": float(normalization.get("color_factor", 1.0)), + "global_coupling_factor": float( + normalization.get("global_coupling_factor", 1.0) + ), + "average_factor": float(normalization.get("average_factor", 1.0)), + "identical_factor": float(normalization.get("identical_factor", 1.0)), + "qcd_coupling_power": int(normalization.get("qcd_coupling_power", 0)), + "electroweak_coupling_power": int( + normalization.get("electroweak_coupling_power", 0) + ), + }, + "parameter_layout": { + "source_component_parameter_count": source_count, + "momentum_parameter_count": momentum_count, + "model_parameter_count": model_parameter_count, + "parameter_count_if_flattened": ( + source_count + momentum_count + model_parameter_count + ), + "value_component_count": value_count, + "source_components_complex": True, + "momentum_components_real": True, + "real_valued_inputs": list( + range( + source_count, + source_count + momentum_count + model_parameter_count, + ) + ), + }, + "current_storage": _current_storage(_mapping(schema["current_storage"])), + "value_storage": _value_storage(_mapping(schema["value_storage"])), + "source_fill": { + "source_count": int(source_fill["source_count"]), + "sources": [_source_record(item) for item in source_records], + }, + "momentum_slots": [ + _select( + _mapping(item), + "momentum_slot_id", + "momentum_mask", + "external_labels", + "component_start", + "component_stop", + "real_valued", + ) + for item in _sequence(schema["momentum_slots"]) + ], + "stages": [ + _runtime_stage(_mapping(item)) for item in _sequence(schema["stages"]) + ], + "amplitude_stage": _amplitude_stage(_mapping(schema["amplitude_stage"])), + **( + {} + if schema.get("helicity_recurrence") is None + else { + "helicity_recurrence": _plain_mapping( + _mapping(schema["helicity_recurrence"]) + ) + } + ), + } + + +def _execution_model_parameter(record: Mapping[str, object]) -> dict[str, object]: + result = _select(record, "name", "kind", "parameter_index", "default") + for name in ("pdg", "runtime_name", "complex_component"): + if record.get(name) is not None: + result[name] = record[name] + return result + + +def _current_storage(storage: Mapping[str, object]) -> dict[str, object]: + fields = ( + "current_id", + "component_start", + "component_stop", + "dimension", + "is_source", + "particle_id", + "external_mask", + "external_labels", + "helicity_ancestry", + "chirality", + "spin_state", + "flavour_flow", + "color_state", + "momentum_mask", + "auxiliary_kind", + ) + return { + "component_count": int(storage["component_count"]), + "number_type": str(storage["number_type"]), + "metadata_compacted": True, + "current_slots": [ + _select(_mapping(item), *fields) + for item in _sequence(storage["current_slots"]) + ], + } + + +def _value_storage(storage: Mapping[str, object]) -> dict[str, object]: + fields = ( + "value_slot_id", + "current_id", + "variant", + "component_start", + "component_stop", + "dimension", + "current_component_start", + "current_component_stop", + "is_source", + "applies_propagator", + "particle_id", + "external_mask", + "external_labels", + "momentum_mask", + "chirality", + "propagator", + ) + return { + "component_count": int(storage["component_count"]), + "number_type": str(storage["number_type"]), + "metadata_compacted": True, + "value_slots": [ + _select(_mapping(item), *fields) + for item in _sequence(storage["value_slots"]) + ], + } + + +def _source_record(record: Mapping[str, object]) -> dict[str, object]: + return _select( + record, + "source_id", + "current_id", + "current_component_start", + "current_component_stop", + "value_slot", + "source_parameter_start", + "source_parameter_stop", + "leg_label", + "input_momentum_slot", + "side", + "crossing", + "physical_pdg", + "outgoing_pdg", + "particle_id", + "anti_particle_id", + "source_kind", + "wavefunction_kind", + "source_orientation", + "source_basis", + "source_ir", + "applied_crossing", + "source_helicity", + "chirality", + "spin_state", + "dimension", + "helicity_ancestry", + "color_state", + ) + + +def _runtime_stage(stage: Mapping[str, object]) -> dict[str, object]: + interactions_compacted = bool(stage.get("interactions_compacted", False)) + interaction_ids = ( + [int(value) for value in _sequence(stage.get("interaction_ids", []))] + if interactions_compacted + else [ + int(_mapping(item)["interaction_id"]) + for item in _sequence(stage["interactions"]) + ] + ) + if len(interaction_ids) != int(stage["interaction_count"]): + raise ValueError("runtime stage interaction count is inconsistent") + return { + **_select( + stage, + "stage_index", + "stage_kind", + "subset_size", + "input_current_ids", + "output_current_ids", + "input_value_slot_ids", + "output_value_slot_ids", + "interaction_count", + ), + "interactions_compacted": True, + "interaction_ids": interaction_ids, + "interactions": [], + } + + +def _amplitude_stage(stage: Mapping[str, object]) -> dict[str, object]: + contraction = stage.get("color_contraction") + color_topology_replay = stage.get("color_topology_replay") + return { + "stage_kind": str(stage["stage_kind"]), + "output_count": int(stage["output_count"]), + "color_contraction": ( + None if contraction is None else _color_contraction(_mapping(contraction)) + ), + "color_topology_replay": ( + None + if color_topology_replay is None + else _plain_mapping(_mapping(color_topology_replay)) + ), + "roots": [ + _select( + _mapping(root), + "output_index", + "root_id", + "kind", + "left_current_id", + "right_current_id", + "left_slot", + "right_slot", + "left_value_slot", + "right_value_slot", + "vertex_kind", + "vertex_particles", + "coupling", + "color_weight", + "color_sector_id", + "contraction", + "contraction_ir", + "coherent_group_id", + "helicity_weight", + "all_sector_weight", + ) + for root in _sequence(stage["roots"]) + ], + } + + +def _color_contraction(record: Mapping[str, object]) -> dict[str, object]: + result: dict[str, object] = { + **_select(record, "supported", "reason", "group_count"), + "includes_color_factor": bool(record.get("includes_color_factor", False)), + "entries": [ + _color_contraction_entry( + _mapping(item), + "left_group_id", + "right_group_id", + ) + for item in _sequence(record["entries"]) + ], + } + repeated_block = record.get("repeated_block") + if repeated_block is not None: + repeated = _mapping(repeated_block) + compact: dict[str, object] = { + "component_count": int(repeated["component_count"]), + "component_group_ids": [ + int(value) for value in _sequence(repeated["component_group_ids"]) + ], + "entries": [ + _color_contraction_entry( + _mapping(item), + "left_group_index", + "right_group_index", + ) + for item in _sequence(repeated["entries"]) + ], + } + factorized_block = repeated.get("factorized_block") + if factorized_block is not None: + factorized = _mapping(factorized_block) + compact_factorized: dict[str, object] = { + "kind": str(factorized["kind"]), + } + if "rank" in factorized: + compact_factorized["rank"] = int(factorized["rank"]) + compact_factorized["cosets"] = [ + [int(value) for value in _sequence(coset)] + for coset in _sequence(factorized["cosets"]) + ] + compact["factorized_block"] = compact_factorized + result["repeated_block"] = compact + return result + + +def _color_contraction_entry( + record: Mapping[str, object], left: str, right: str +) -> dict[str, object]: + result = _select(record, left, right, "weight", "symmetry_factor") + if "exact_weight" in record: + result["exact_weight"] = record["exact_weight"] + return result + + +def _stage_evaluator_set(record: Mapping[str, object]) -> dict[str, object]: + result = { + **_select( + record, + "kind", + "runtime_available", + "runtime_unavailable_message", + "parameter_count", + "value_parameter_count", + "momentum_parameter_count", + "model_parameter_count", + "real_valued_inputs", + "parameter_layout", + "stage_count", + "required_runtime_capabilities", + ), + "stages": [ + _serialized_stage(_mapping(item)) for item in _sequence(record["stages"]) + ], + "amplitude_stage": _serialized_stage(_mapping(record["amplitude_stage"])), + } + evaluator_manifests = [ + _mapping(_mapping(stage)["evaluator"]) + for stage in (*_sequence(result["stages"]), result["amplitude_stage"]) + ] + actual = tuple( + sorted( + { + capability + for manifest in evaluator_manifests + for capability in evaluator_runtime_capabilities(manifest) + } + ) + ) + declared = set(_required_runtime_capabilities(result)) + direct_stages = [ + stage.get("compiled_plane_arena") is not None + for stage in (*_sequence(result["stages"]), result["amplitude_stage"]) + ] + has_direct_capability = COMPILED_PLANE_ARENA_RUNTIME_CAPABILITY in declared + direct_evaluator_capabilities = { + SYMJIT_F64_RUNTIME_CAPABILITY, + SYMBOLICA_CPP_RUNTIME_CAPABILITY, + SYMBOLICA_ASM_RUNTIME_CAPABILITY, + } + if set(actual) & direct_evaluator_capabilities and not bool( + direct_stages and all(direct_stages) + ): + raise ValueError( + "compiled f64 artifacts require compiled-plane-arena-v1 metadata " + "for every fused stage" + ) + if has_direct_capability != bool(direct_stages and all(direct_stages)): + raise ValueError( + "compiled plane-arena capability and fused-stage metadata disagree" + ) + if any(direct_stages) and not all(direct_stages): + raise ValueError("compiled plane-arena metadata must cover every fused stage") + evaluator_capabilities = declared - { + COMPILED_PLANE_ARENA_RUNTIME_CAPABILITY, + COMPILED_RUNTIME_SELECTORS_CAPABILITY, + } + if set(actual) != evaluator_capabilities: + raise ValueError( + "stage evaluator runtime capabilities do not match evaluator payloads" + ) + return result + + +def _serialized_stage(record: Mapping[str, object]) -> dict[str, object]: + result = { + **_select( + record, + "stage_index", + "stage_kind", + "subset_size", + "evaluator_label", + "parameter_layout", + "output_length", + "output_slots", + "input_value_slot_ids", + "output_value_slot_ids", + "interaction_ids", + "input_components", + "parameter_count", + "value_parameter_count", + "momentum_parameter_count", + "model_parameter_count", + "real_valued_inputs", + "expression_ready", + "blockers", + ), + "evaluator": _evaluator(_mapping(record["evaluator"])), + } + direct = record.get("compiled_plane_arena") + if direct is not None: + result["compiled_plane_arena"] = _compiled_plane_arena_stage( + _mapping(direct), + stage=result, + ) + return result + + +def _compiled_plane_arena_stage( + record: Mapping[str, object], + *, + stage: Mapping[str, object], +) -> dict[str, object]: + result = { + **_select( + record, + "schema_version", + "kind", + "application_abi", + "source_application_abi", + "element_layout", + "output_operation", + "output_factor", + "input_output_aliasing", + "output_output_aliasing", + ), + "input_bindings": [ + _select( + _mapping(item), + "parameter_index", + "kind", + "source_id", + "component", + "global_component", + "real_valued", + ) + for item in _sequence(record["input_bindings"]) + ], + "output_bindings": [ + _select( + _mapping(item), + "output_index", + "arena", + "component", + ) + for item in _sequence(record["output_bindings"]) + ], + "leaves": [ + _select( + _mapping(item), + "application_path", + "source_application_abi", + "optimization_level", + "direct_codegen_optimization_level", + "input_len", + "output_len", + "input_indices", + "output_start", + "output_stop", + ) + for item in _sequence(record["leaves"]) + ], + } + symjit_contract = ( + result["application_abi"] == COMPILED_PLANE_DIRECT_APPLICATION_ABI + and result["source_application_abi"] == SYMJIT_PLANE_APPLICATION_ABI + ) + native_contract = ( + result["application_abi"] == NATIVE_COMPILED_DIRECT_APPLICATION_ABI + and result["source_application_abi"] == NATIVE_COMPILED_DIRECT_APPLICATION_ABI + ) + if ( + result["schema_version"] != 1 + or result["kind"] != "compiled-plane-arena-stage" + or not (symjit_contract or native_contract) + or result["element_layout"] != "split-complex-component-major" + or result["output_operation"] != "overwrite" + or result["output_factor"] != "identity" + or result["input_output_aliasing"] != "forbidden" + or result["output_output_aliasing"] != "forbidden" + ): + raise ValueError("compiled plane-arena stage contract is incompatible") + if len(result["input_bindings"]) != int(stage["parameter_count"]): + raise ValueError("compiled plane-arena input binding count is invalid") + if len(result["output_bindings"]) != int(stage["output_length"]): + raise ValueError("compiled plane-arena output binding count is invalid") + if not result["leaves"]: + raise ValueError("compiled plane-arena stage has no fused leaves") + output_cursor = 0 + for leaf in result["leaves"]: + leaf_map = _mapping(leaf) + input_indices = list(_sequence(leaf_map["input_indices"])) + if ( + leaf_map["source_application_abi"] != result["source_application_abi"] + or ( + symjit_contract + and leaf_map["direct_codegen_optimization_level"] + != leaf_map["optimization_level"] + ) + or (native_contract and leaf_map["direct_codegen_optimization_level"] != 3) + or len(input_indices) != int(leaf_map["input_len"]) + or leaf_map["output_start"] != output_cursor + or leaf_map["output_stop"] != output_cursor + int(leaf_map["output_len"]) + ): + raise ValueError("compiled plane-arena leaf bindings are invalid") + output_cursor = int(leaf_map["output_stop"]) + if output_cursor != int(stage["output_length"]): + raise ValueError("compiled plane-arena leaves do not cover stage outputs") + return result + + +def _model_parameter_evaluator(record: Mapping[str, object]) -> dict[str, object]: + result = { + **_select( + record, + "kind", + "required_runtime_capabilities", + "input_parameter_indices", + "outputs", + ), + "evaluator": _evaluator(_mapping(record["evaluator"])), + } + if evaluator_runtime_capabilities(_mapping(result["evaluator"])) != ( + _required_runtime_capabilities(result) + ): + raise ValueError( + "model-parameter evaluator runtime capabilities do not match its payload" + ) + return result + + +def _evaluator(record: Mapping[str, object]) -> dict[str, object]: + kind = str(record.get("kind", "")) + if kind == "symjit-application-evaluator": + result = _select( + record, + "kind", + "runtime_capability", + "input_len", + "output_len", + "application_path", + "application_abi", + "element_layout", + "batch_layout", + "compiler_type", + "translation_mode", + "optimization_level", + "word_bits", + "endianness", + "required_defuns", + "evaluator_state_path", + "evaluator_state_runtime_capability", + ) + if result["runtime_capability"] != SYMJIT_F64_RUNTIME_CAPABILITY: + raise ValueError( + "direct SymJIT evaluator has an invalid runtime capability" + ) + if result["application_abi"] != SYMJIT_APPLICATION_ABI: + raise ValueError( + "direct SymJIT evaluator has an incompatible application ABI" + ) + if ( + result["evaluator_state_runtime_capability"] + != SYMBOLICA_LEGACY_JIT_RUNTIME_CAPABILITY + ): + raise ValueError( + "direct SymJIT evaluator has an invalid fallback capability" + ) + if result["element_layout"] != "complex-f64": + raise ValueError("direct SymJIT evaluator has an invalid element layout") + if ( + result["batch_layout"] != "row-major" + or result["compiler_type"] != "native" + or result["word_bits"] != 64 + or result["endianness"] != "little" + or result["required_defuns"] != [] + ): + raise ValueError("direct SymJIT evaluator has invalid execution metadata") + if result["translation_mode"] not in {"direct", "indirect"}: + raise ValueError("direct SymJIT evaluator has an invalid translation mode") + optimization_level = result["optimization_level"] + if ( + isinstance(optimization_level, bool) + or not isinstance(optimization_level, int) + or optimization_level not in {0, 1, 2, 3} + ): + raise ValueError( + "direct SymJIT evaluator has an invalid optimization level" + ) + plane = record.get("plane_application") + if not isinstance(plane, Mapping): + raise ValueError( + "direct SymJIT evaluator predates the SymJIT 2.22 plane " + "binding ABI; regenerate this artifact" + ) + result["plane_application"] = _symjit_plane_application( + plane, + input_complex_count=int(result["input_len"]), + output_complex_count=int(result["output_len"]), + ) + return result + if kind == "jit-symbolica-evaluator": + result = _select( + record, + "kind", + "runtime_capability", + "input_len", + "output_len", + "evaluator_state_path", + ) + if result["runtime_capability"] != SYMBOLICA_LEGACY_JIT_RUNTIME_CAPABILITY: + raise ValueError("legacy JIT evaluator has an invalid runtime capability") + return result + if kind == "compiled-complex-evaluator": + result = _select( + record, + "kind", + "runtime_capability", + "function_name", + "input_len", + "output_len", + "library_path", + "evaluator_state_path", + "number_type", + ) + if result["runtime_capability"] not in { + SYMBOLICA_CPP_RUNTIME_CAPABILITY, + SYMBOLICA_ASM_RUNTIME_CAPABILITY, + }: + raise ValueError("compiled evaluator has an invalid runtime capability") + direct = record.get("native_direct_application") + if direct is not None: + if result["runtime_capability"] not in { + SYMBOLICA_CPP_RUNTIME_CAPABILITY, + SYMBOLICA_ASM_RUNTIME_CAPABILITY, + }: + raise ValueError( + "native DirectApplication metadata requires a compiled " + "C++ or ASM evaluator" + ) + direct_application = _native_compiled_direct_application( + _mapping(direct), + expected_function_name=str(result["function_name"]), + expected_output_count=int(result["output_len"]), + ) + if result["library_path"] != direct_application["library_path"]: + raise ValueError( + "compiled process DirectApplication must be the sole native " + "library payload; dense/direct dual production is forbidden" + ) + result["native_direct_application"] = direct_application + return result + if kind == "chunked-symbolica-evaluator": + result = { + "kind": kind, + "input_len": record["input_len"], + "chunk_input_indices": [ + list(_sequence(indices)) + for indices in _sequence(record["chunk_input_indices"]) + ], + "required_runtime_capabilities": list( + _required_runtime_capabilities(record) + ), + "chunks": [ + _evaluator(_mapping(item)) for item in _sequence(record["chunks"]) + ], + } + actual = evaluator_runtime_capabilities(result) + if actual != _required_runtime_capabilities(record): + raise ValueError( + "chunked evaluator required runtime capabilities do not match chunks" + ) + return result + raise ValueError(f"unsupported evaluator artifact kind {kind!r}") + + +def _symjit_plane_application( + record: Mapping[str, object], + *, + input_complex_count: int, + output_complex_count: int, +) -> dict[str, object]: + result = _select( + record, + "application_path", + "application_abi", + "storage_abi", + "element_layout", + "descriptor_order", + "input_complex_count", + "output_complex_count", + "input_plane_count", + "output_plane_count", + "compiler_type", + "translation_mode", + "optimization_level", + "simd", + "complex", + "fast_math", + "fast_complex", + "compression", + "threading", + "direct_arena", + "source_digest", + "target", + ) + expected = { + "application_abi": SYMJIT_PLANE_APPLICATION_ABI, + "storage_abi": SYMJIT_APPLICATION_ABI, + "element_layout": "split-complex-plane-major", + "descriptor_order": "inputs-re-im-then-outputs-re-im", + "input_complex_count": input_complex_count, + "output_complex_count": output_complex_count, + "input_plane_count": 2 * input_complex_count, + "output_plane_count": 2 * output_complex_count, + "compiler_type": "native", + "translation_mode": "symbolica-structured-instructions", + "simd": True, + "complex": True, + "fast_math": True, + "fast_complex": False, + "threading": False, + "direct_arena": True, + } + for field, expected_value in expected.items(): + if result[field] != expected_value: + raise ValueError( + f"direct SymJIT plane application {field} is incompatible; " + "regenerate this artifact" + ) + path = result["application_path"] + digest = result["source_digest"] + optimization_level = result["optimization_level"] + if not isinstance(path, str) or not path: + raise ValueError("direct SymJIT plane application path is invalid") + if ( + not isinstance(digest, str) + or len(digest) != 64 + or any(character not in "0123456789abcdef" for character in digest) + ): + raise ValueError("direct SymJIT plane source digest is invalid") + if ( + isinstance(optimization_level, bool) + or not isinstance(optimization_level, int) + or optimization_level not in {0, 1, 2, 3} + ): + raise ValueError("direct SymJIT plane optimization level is invalid") + if not isinstance(result["compression"], bool): + raise ValueError("direct SymJIT plane compression flag is invalid") + target = _mapping(result["target"]) + if target.get("word_bits") != 64 or target.get("endianness") != "little": + raise ValueError("direct SymJIT plane target is incompatible") + result["target"] = _plain_mapping(target) + return result + + +def _native_compiled_direct_application( + record: Mapping[str, object], + *, + expected_function_name: str, + expected_output_count: int, +) -> dict[str, object]: + result = _select( + record, + "application_abi", + "function_name", + "source_path", + "library_path", + "target", + "evaluator_state_sha256", + "instruction_count", + "temporary_count", + "input_plane_count", + "scalar_input_count", + "output_plane_count", + "simd_lane_width", + "logical_stack_bytes", + "output_semantics", + ) + if result["application_abi"] != NATIVE_COMPILED_DIRECT_APPLICATION_ABI: + raise ValueError("native DirectApplication has an incompatible ABI") + if result["function_name"] != expected_function_name: + raise ValueError( + "native DirectApplication function identity does not match its evaluator" + ) + target = _mapping(result["target"]) + triple = target.get("triple") + cpu_features = target.get("cpu_features") + if ( + not isinstance(triple, str) + or triple not in _SUPPORTED_ARTIFACT_TARGETS + or isinstance(cpu_features, str | bytes) + or not isinstance(cpu_features, Sequence) + ): + raise ValueError("native DirectApplication target metadata is invalid") + features = [str(item) for item in cpu_features] + if features != sorted(set(features)) or any(not item for item in features): + raise ValueError( + "native DirectApplication CPU features must be sorted and unique" + ) + result["target"] = { + "triple": triple, + "cpu_features": features, + } + digest = result["evaluator_state_sha256"] + if ( + not isinstance(digest, str) + or len(digest) != 64 + or any(character not in "0123456789abcdef" for character in digest) + ): + raise ValueError("native DirectApplication evaluator-state digest is invalid") + for name, minimum in ( + ("instruction_count", 1), + ("temporary_count", 0), + ("input_plane_count", 1), + ("scalar_input_count", 0), + ("output_plane_count", 2), + ("logical_stack_bytes", 1), + ): + value = result[name] + if isinstance(value, bool) or not isinstance(value, int) or value < minimum: + raise ValueError( + f"native DirectApplication {name.replace('_', ' ')} is invalid" + ) + if result["output_plane_count"] != expected_output_count * 2: + raise ValueError( + "native DirectApplication output planes do not match evaluator outputs" + ) + if result["simd_lane_width"] not in {2, 4}: + raise ValueError("native DirectApplication SIMD width is unsupported") + if result["logical_stack_bytes"] > 64 * 1024: + raise ValueError("native DirectApplication logical stack exceeds 64 KiB") + if result["output_semantics"] != "factor-free-overwrite": + raise ValueError("native DirectApplication output semantics are incompatible") + for name in ("source_path", "library_path"): + value = result[name] + if not isinstance(value, str): + raise ValueError(f"native DirectApplication {name} must be a string") + path = Path(value) + if path.is_absolute() or ".." in path.parts: + raise ValueError( + f"native DirectApplication {name} must be artifact-relative" + ) + return result + + +def _dag_summary( + record: Mapping[str, object], + *, + require_interaction_evaluation_count: bool = False, +) -> dict[str, object]: + result = _select( + record, + "current_count", + "source_count", + "interaction_count", + "amplitude_root_count", + "truncated", + ) + interaction_evaluation_count = record.get("interaction_evaluation_count") + if interaction_evaluation_count is None: + if require_interaction_evaluation_count: + raise ValueError( + "compiled DAG summary is missing interaction_evaluation_count" + ) + else: + result["interaction_evaluation_count"] = interaction_evaluation_count + return result + + +def _fully_resident_materialization_census( + counts: Mapping[str, object], + *, + basis: str, +) -> dict[str, object]: + exact = { + str(name): value + for name, value in counts.items() + if str(name).endswith("_count") + } + if not exact: + raise ValueError("fully resident materialization has no exact counters") + for name, value in exact.items(): + if isinstance(value, bool) or not isinstance(value, int) or value < 0: + raise ValueError( + f"fully resident materialization counter {name!r} is invalid" + ) + return { + "abi": "pyamplicol-fully-resident-materialization-census-v1", + "basis": basis, + "final": exact, + "peak": dict(exact), + "final_equals_peak": True, + } + + +def _eager_materialization_census( + process: EagerPlanV3ProcessArtifact, +) -> dict[str, object]: + inspection = process.inspection_summary + counts: dict[str, object] = { + "source_count": process.dag_summary["source_count"], + "current_count": process.dag_summary["current_count"], + "interaction_count": process.dag_summary["interaction_count"], + "amplitude_root_count": process.dag_summary["amplitude_root_count"], + } + for name in ( + "invocation_count", + "attachment_count", + "closure_count", + "finalization_count", + ): + value = inspection.get(name) + if value is not None: + counts[name] = value + return _fully_resident_materialization_census( + counts, + basis="immutable-fully-resident-eager-plan", + ) + + +def _select(record: Mapping[str, object], *names: str) -> dict[str, object]: + missing = [name for name in names if name not in record] + if missing: + raise ValueError( + "runtime execution record is missing fields: " + ", ".join(missing) + ) + return {name: record[name] for name in names} + + +def _sequence(value: object) -> Sequence[object]: + if isinstance(value, str | bytes) or not isinstance(value, Sequence): + raise TypeError("runtime execution field must be a sequence") + return value + + +def _copy_evaluator_payloads( + evaluator_payloads: _EvaluatorPayloadCollector, + root: Path, + *, + prefix: str, + process_id: str, +) -> None: + source_root = root.expanduser().resolve(strict=True) + for path in sorted(source_root.rglob("*")): + if not path.is_file(): + continue + relative = path.relative_to(source_root).as_posix() + evaluator_payloads.add_file( + f"{prefix}/{relative}", + path, + process_id=process_id, + ) + + +def _copy_color_selector_evaluator_payloads( + evaluator_payloads: _EvaluatorPayloadCollector, + executions: Sequence[CompiledColorSelectorExecutionArtifact], + *, + prefix: str, + process_id: str, +) -> None: + for record in _ordered_color_selector_executions(executions): + _copy_evaluator_payloads( + evaluator_payloads, + record.execution.evaluator_root, + prefix=( + f"{prefix}/{_color_selector_payload_prefix(record.materialized_sector_id)}" + ), + process_id=process_id, + ) + + +def _copy_helicity_selector_evaluator_payloads( + evaluator_payloads: _EvaluatorPayloadCollector, + executions: Sequence[CompiledHelicitySelectorExecutionArtifact], + *, + prefix: str, + process_id: str, +) -> None: + for lane_index, record in enumerate( + _ordered_helicity_selector_executions(executions) + ): + lane_prefix = ( + f"{prefix}/{_HELICITY_SELECTOR_UNION_PAYLOAD_ROOT}/class-{lane_index}" + ) + _copy_evaluator_payloads( + evaluator_payloads, + record.execution.evaluator_root, + prefix=lane_prefix, + process_id=process_id, + ) + _copy_helicity_selector_evaluator_payloads( + evaluator_payloads, + record.execution.helicity_selector_executions, + prefix=lane_prefix, + process_id=process_id, + ) + + +def _color_selector_payload_prefix(materialized_sector_id: int) -> str: + if materialized_sector_id < 0: + raise ValueError("materialized colour-sector ids must be non-negative") + return f"{_COLOR_SELECTOR_PAYLOAD_ROOT}/sector-{materialized_sector_id}" + + +def _ordered_color_selector_executions( + executions: Sequence[CompiledColorSelectorExecutionArtifact], +) -> tuple[CompiledColorSelectorExecutionArtifact, ...]: + ordered = tuple( + sorted(executions, key=lambda record: record.materialized_sector_id) + ) + sector_ids = tuple(record.materialized_sector_id for record in ordered) + if len(sector_ids) != len(set(sector_ids)): + raise ValueError("compiled colour-selector lane ids must be unique") + for record in ordered: + if record.materialized_sector_id < 0: + raise ValueError("materialized colour-sector ids must be non-negative") + if record.execution.color_selector_executions: + raise ValueError("compiled colour-selector execution lanes cannot nest") + return ordered + + +def _ordered_helicity_selector_executions( + executions: Sequence[CompiledHelicitySelectorExecutionArtifact], +) -> tuple[CompiledHelicitySelectorExecutionArtifact, ...]: + ordered = tuple(sorted(executions, key=lambda record: record.selector_domain_ids)) + seen: set[int] = set() + for record in ordered: + domain_ids = tuple(sorted(set(record.selector_domain_ids))) + if not domain_ids or domain_ids != record.selector_domain_ids: + raise ValueError( + "compiled helicity-selector domain ids must be non-empty, " + "sorted, and unique" + ) + overlap = seen.intersection(domain_ids) + if overlap: + raise ValueError( + "compiled helicity-selector lanes overlap selector domains: " + + ", ".join(str(item) for item in sorted(overlap)) + ) + seen.update(domain_ids) + if record.schedule_mode not in {"parent-closure", "nested-runtime"}: + raise ValueError( + "compiled helicity-selector schedule mode must be " + "'parent-closure' or 'nested-runtime'" + ) + if record.execution.color_selector_executions: + raise ValueError("compiled helicity-selector execution lanes cannot nest") + children = record.execution.helicity_selector_executions + if children: + if record.schedule_mode != "nested-runtime": + raise ValueError( + "only a nested-runtime helicity-selector execution may " + "contain closure lanes" + ) + for child in _ordered_helicity_selector_executions(children): + if ( + child.schedule_mode != "parent-closure" + or child.execution.helicity_selector_executions + or child.execution.color_selector_executions + ): + raise ValueError( + "nested helicity-selector closure lanes must be " + "terminal parent-closure executions" + ) + return ordered + + +def build_api_validation_points( + processes: Sequence[ProcessArtifact], +) -> dict[str, tuple[tuple[float, float, float, float], ...]]: + """Return concrete and crossing-alias points in each public external order.""" + + points: dict[str, tuple[tuple[float, float, float, float], ...]] = {} + for process in processes: + vectors = process.validation_point.four_vectors + if not vectors: + continue + points[process.process_id] = vectors + _add_alias_points(points, vectors=vectors, aliases=process.aliases) + return points + + +def _existing_bundle_points( + existing: ArtifactManifest | None, +) -> dict[str, tuple[tuple[float, float, float, float], ...]]: + if existing is None: + return {} + points: dict[str, tuple[tuple[float, float, float, float], ...]] = {} + for process in existing.processes: + process_id = str(process["id"]) + validation_path = ( + existing.root / f"processes/{process_id}/validation-momenta.json" + ) + if not validation_path.is_file(): + continue + payload = json.loads(validation_path.read_text(encoding="utf-8")) + raw_points = payload.get("points") if isinstance(payload, Mapping) else None + if not isinstance(raw_points, list) or not raw_points: + continue + raw_point = raw_points[0] + if not isinstance(raw_point, list): + raise ValueError(f"validation point for {process_id!r} is invalid") + vectors = tuple(_validation_four_vector(item) for item in raw_point) + points[process_id] = vectors + aliases = process.get("aliases") + if not isinstance(aliases, Sequence): + raise ValueError(f"aliases for {process_id!r} are invalid") + _add_alias_points( + points, + vectors=vectors, + aliases=tuple(_mapping(alias) for alias in aliases), + ) + return points + + +def _add_alias_points( + points: dict[str, tuple[tuple[float, float, float, float], ...]], + *, + vectors: tuple[tuple[float, float, float, float], ...], + aliases: Sequence[Mapping[str, object]], +) -> None: + for alias in aliases: + alias_id = str(alias["id"]) + raw_permutation = alias.get("external_permutation", ()) + if not isinstance(raw_permutation, Sequence): + raise ValueError(f"alias {alias_id!r} permutation is invalid") + permutation = tuple(int(index) for index in raw_permutation) + if not permutation: + permutation = tuple(range(len(vectors))) + if sorted(permutation) != list(range(len(vectors))): + raise ValueError( + f"alias {alias_id!r} permutation does not match its external momenta" + ) + if alias_id in points: + raise ValueError(f"duplicate validation-point ID {alias_id!r}") + alias_vectors: list[tuple[float, float, float, float] | None] = [ + None for _ in vectors + ] + for representative_index, alias_index in enumerate(permutation): + alias_vectors[alias_index] = vectors[representative_index] + if any(vector is None for vector in alias_vectors): + raise ValueError(f"alias {alias_id!r} permutation is incomplete") + points[alias_id] = tuple( + vector for vector in alias_vectors if vector is not None + ) + + +def _validation_four_vector( + record: object, +) -> tuple[float, float, float, float]: + raw = _mapping(record).get("momentum") + if not isinstance(raw, Sequence) or len(raw) != 4: + raise ValueError("serialized validation momentum is not a four-vector") + values = tuple(float(component) for component in raw) + return values[0], values[1], values[2], values[3] + + +def _producer_metadata( + config: GenerationConfig | RunConfig, + *, + runtime_capabilities: Sequence[str] = (), + implicit_portable_jit_evidence: bool = False, +) -> dict[str, object]: + version = package_version() + target, c_abi = _artifact_target_metadata( + config, + runtime_capabilities=runtime_capabilities, + implicit_portable_jit_evidence=implicit_portable_jit_evidence, + ) + producer: dict[str, object] = { + "distribution": "pyamplicol", + "version": version, + "versions": { + "python_api": PYTHON_API_VERSION, + "toml": TOML_SCHEMA_VERSION, + "compiled_model": COMPILED_MODEL_SCHEMA_VERSION, + "process_artifact": PROCESS_ARTIFACT_SCHEMA_VERSION, + "runtime_physics": RUNTIME_PHYSICS_SCHEMA_VERSION, + "symbolica_serialization": SYMBOLICA_SERIALIZATION_ABI, + "c_abi": c_abi, + }, + "target": target, + } + producer["native_build_inputs_sha256"] = active_native_build_inputs_sha256() + source_revision = active_source_revision() + if source_revision is not None: + producer["git_revision"] = source_revision + return producer + + +def _artifact_target_metadata( + config: GenerationConfig | RunConfig, + *, + runtime_capabilities: Sequence[str] = (), + implicit_portable_jit_evidence: bool = False, +) -> tuple[dict[str, object], int]: + target, c_abi = _target_metadata(config) + requested_portable_jit = ( + implicit_portable_jit_evidence + if isinstance(config, GenerationConfig) + else ( + str(config.evaluator.backend) == "jit" + and config.evaluator.jit.optimization_level in {1, 2} + ) + ) + portable_64le = requested_portable_jit and not { + SYMBOLICA_ASM_RUNTIME_CAPABILITY, + SYMBOLICA_CPP_RUNTIME_CAPABILITY, + SYMBOLICA_LEGACY_JIT_RUNTIME_CAPABILITY, + }.intersection(runtime_capabilities) + if portable_64le: + target = { + "triple": PORTABLE_64LE_TARGET, + "cpu_features": [], + } + return target, c_abi + + +def _implicit_generation_portable_jit_evidence( + processes: Sequence[ProcessArtifact], +) -> bool: + if not processes or any( + not isinstance(process, CompiledProcessArtifact) for process in processes + ): + return False + records: list[Mapping[str, object]] = [] + for process in cast(Sequence[CompiledProcessArtifact], processes): + records.append(process.stage_manifest) + if process.model_parameter_evaluator is not None: + records.append(process.model_parameter_evaluator) + for selector in process.color_selector_executions: + records.extend(_compiled_execution_evidence_records(selector.execution)) + if process.helicity_sum_execution is not None: + records.extend( + _compiled_execution_evidence_records(process.helicity_sum_execution) + ) + for selector in process.helicity_selector_executions: + records.extend(_compiled_execution_evidence_records(selector.execution)) + evaluator_count = 0 + for record in records: + valid, count = _mapping_has_only_portable_symjit_evaluators(record) + if not valid or count == 0: + return False + evaluator_count += count + return evaluator_count > 0 + + +def _compiled_execution_evidence_records( + execution: CompiledExecutionArtifact, +) -> list[Mapping[str, object]]: + records: list[Mapping[str, object]] = [execution.stage_manifest] + if execution.model_parameter_evaluator is not None: + records.append(execution.model_parameter_evaluator) + for selector in execution.color_selector_executions: + records.extend(_compiled_execution_evidence_records(selector.execution)) + for selector in execution.helicity_selector_executions: + records.extend(_compiled_execution_evidence_records(selector.execution)) + return records + + +def _mapping_has_only_portable_symjit_evaluators( + value: Mapping[str, object], +) -> tuple[bool, int]: + evaluator_count = 0 + valid = True + + def visit(item: object) -> None: + nonlocal evaluator_count, valid + if not valid: + return + if isinstance(item, Mapping): + kind = item.get("kind") + if kind == "symjit-application-evaluator": + evaluator_count += 1 + plane = item.get("plane_application") + if ( + item.get("optimization_level") not in (1, 2) + or not isinstance(plane, Mapping) + or plane.get("optimization_level") not in (1, 2) + ): + valid = False + return + elif kind in {"compiled-complex-evaluator", "jit-symbolica-evaluator"}: + valid = False + return + if item.get("source_application_abi") == SYMJIT_PLANE_APPLICATION_ABI: + for field in ( + "optimization_level", + "direct_codegen_optimization_level", + ): + if field in item and item[field] not in (1, 2): + valid = False + return + for nested in item.values(): + visit(nested) + return + if isinstance(item, Sequence) and not isinstance(item, str | bytes): + for nested in item: + visit(nested) + + visit(value) + return valid, evaluator_count + + +def _target_metadata( + config: GenerationConfig | RunConfig, +) -> tuple[dict[str, object], int]: + requires_native_features = ( + isinstance(config, RunConfig) + and str(config.evaluator.backend) != "jit" + and config.evaluator.cpp.native_arch + ) + try: + rusticol = importlib.import_module("pyamplicol._rusticol") + verify_native_module(rusticol) + info = rusticol.target_info() + triple = str(info.triple) + available_features = tuple(str(item) for item in info.cpu_features) + if available_features != tuple(sorted(set(available_features))): + raise RuntimeError( + "Rusticol returned non-canonical target CPU feature metadata" + ) + c_abi = int(rusticol.abi_version()) + except ( + AttributeError, + ImportError, + OSError, + importlib.metadata.PackageNotFoundError, + ) as rusticol_error: + try: + from pyamplicol._sdk import load_sdk_info + + sdk = load_sdk_info() + except ( + ImportError, + OSError, + RuntimeError, + importlib.metadata.PackageNotFoundError, + ) as sdk_error: + raise RuntimeError( + "Rusticol target metadata is unavailable; install or build the " + "pyamplicol native extension before generating process artifacts" + ) from sdk_error + if requires_native_features: + raise RuntimeError( + "native C++ evaluator generation requires Rusticol CPU-feature " + "introspection; SDK target metadata alone is insufficient" + ) from rusticol_error + triple = sdk.target + available_features = () + c_abi = int(sdk.abi_version) + required_features = available_features if requires_native_features else () + if triple not in _SUPPORTED_ARTIFACT_TARGETS: + raise RuntimeError( + f"Rusticol process artifacts are not supported on target {triple!r}" + ) + if requires_native_features and not required_features: + raise RuntimeError( + "Rusticol did not detect any CPU features for a native C++ evaluator; " + "refusing to emit incomplete target metadata" + ) + return { + "triple": triple, + "cpu_features": list(required_features), + }, c_abi + + +def _model_metadata( + source: ModelSource, + compiled: CompiledModel, +) -> dict[str, object]: + source_kind = { + "built-in-sm": "built-in-sm", + "built-in-sm-heft": "built-in-sm-heft", + "ufo": "ufo", + "json": "ufo-json", + "compiled": "compiled-model", + "prepared": "compiled-model", + }[source.kind] + digest = str(compiled.source.get("digest", "")) + if source.kind == "compiled" and source.path is not None: + digest = hashlib.sha256(source.path.read_bytes()).hexdigest() + if len(digest) != 64: + raise ValueError("compiled model source has no canonical SHA-256 digest") + restriction = ( + ( + source.restriction.name + if isinstance(source.restriction, Path) + else source.restriction + ) + if source.restriction is not None + else "default" + if source.kind in {"ufo", "json"} + else None + ) + return { + "name": compiled.name, + "source_kind": source_kind, + "content_sha256": digest, + "compiled_schema_version": COMPILED_MODEL_SCHEMA_VERSION, + "restriction": restriction, + } + + +def _dependency_metadata(source: ModelSource) -> tuple[dict[str, object], ...]: + dependencies: list[dict[str, object]] = [ + { + "name": "symbolica", + "version": _distribution_version("symbolica", "unknown"), + "source": "https://symbolica.io/", + "license": "Symbolica Software License Agreement", + } + ] + if source.kind in {"ufo", "json"}: + dependencies.append( + { + "name": "ufo-model-loader", + "version": _distribution_version("ufo-model-loader", "unknown"), + "source": "https://github.com/alphal00p/ufo_model_loader", + "license": "MIT", + } + ) + return tuple(dependencies) + + +def _validate_append_compatibility( + existing: ArtifactManifest | None, + *, + producer: Mapping[str, object], + model: Mapping[str, object], + eager_pack_identity: Mapping[str, object] | None, + requested_bytes: bytes, + effective_bytes: bytes, + adjustments: Sequence[Mapping[str, str]], + processes: Sequence[ProcessArtifact], +) -> None: + if existing is None: + return + _reject_legacy_eager_append(existing) + existing_producer = _plain_mapping(existing.producer) + producer_identity_fields = ( + ("distribution", "distribution"), + ("version", "version"), + ("git_revision", "source revision"), + ("native_build_inputs_sha256", "native build-input digest"), + ) + for field, label in producer_identity_fields: + if existing_producer.get(field) != producer.get(field): + raise ValueError( + f"append producer {label} differs from the existing artifact" + ) + existing_capabilities = _required_runtime_capabilities(existing.runtime) + existing_uses_eager = ( + EAGER_DIRECT_ARENA_RUNTIME_CAPABILITY in existing_capabilities + or any(record.path == _EAGER_KERNEL_PACK_PATH for record in existing.payloads) + ) + existing_identity = existing.extensions.get(_EAGER_PACK_IDENTITY_EXTENSION) + if existing_uses_eager or existing_identity is not None: + if not isinstance(existing_identity, Mapping): + raise ValueError( + "append eager artifact has no canonical prepared-pack identity; " + "replace the artifact" + ) + if eager_pack_identity is None or _plain_mapping( + existing_identity + ) != _plain_mapping(eager_pack_identity): + raise ValueError( + "append prepared kernel pack identity differs from the existing " + "artifact; replace the artifact" + ) + if _plain_mapping(existing.model) != dict(model): + raise ValueError("append model provenance differs from the existing artifact") + if existing_producer.get("target") != producer.get("target"): + raise ValueError("append target differs from the existing artifact") + requested_path = existing.root / str(existing.configuration["requested_path"]) + effective_path = existing.root / str(existing.configuration["effective_path"]) + if requested_path.read_bytes() != requested_bytes: + raise ValueError("append requested configuration differs from the artifact") + if effective_path.read_bytes() != effective_bytes: + raise ValueError("append effective configuration differs from the artifact") + existing_adjustments: list[dict[str, str]] = [] + for item in _sequence(existing.configuration["adjustments"]): + adjustment = _mapping(item) + existing_adjustments.append( + { + "path": str(adjustment["path"]), + "reason": str(adjustment["reason"]), + } + ) + if existing_adjustments != list(adjustments): + raise ValueError("append configuration adjustments differ from the artifact") + existing_ids = {str(record["id"]) for record in existing.processes} + duplicates = existing_ids.intersection(process.process_id for process in processes) + if duplicates: + raise ValueError( + "append process IDs already exist: " + ", ".join(sorted(duplicates)) + ) + + +def _reject_legacy_eager_append(existing: ArtifactManifest) -> None: + raw_capabilities = existing.runtime.get("required_runtime_capabilities", ()) + if ( + not isinstance(raw_capabilities, str | bytes) + and isinstance(raw_capabilities, Sequence) + and EAGER_RUNTIME_CAPABILITY in raw_capabilities + ): + raise ValueError( + "legacy eager plan-v2 artifacts cannot be extended; regenerate the " + "artifact with the current eager plan-v3 runtime or use replace mode" + ) + + +def _existing_process_records( + existing: ArtifactManifest | None, +) -> list[dict[str, object]]: + if existing is None: + return [] + return [_plain_mapping(record) for record in existing.processes] + + +def _existing_evaluator_entries( + existing: ArtifactManifest | None, +) -> list[dict[str, object]]: + if existing is None: + return [] + path = existing.root / str(existing.runtime["evaluator_manifest_path"]) + payload = json.loads(path.read_text(encoding="utf-8")) + if ( + not isinstance(payload, dict) + or payload.get("schema_version") != PROCESS_ARTIFACT_SCHEMA_VERSION + or payload.get("kind") != "pyamplicol-runtime-execution-set" + ): + raise ValueError("append artifact has an incompatible evaluator-set manifest") + entries = payload.get("processes") + if not isinstance(entries, list) or not all( + isinstance(item, dict) for item in entries + ): + raise ValueError("append evaluator-set process list is invalid") + return [dict(item) for item in entries] + + +def _existing_required_runtime_capabilities( + existing: ArtifactManifest | None, +) -> tuple[str, ...]: + if existing is None: + return () + return _required_runtime_capabilities(existing.runtime) + + +def _compiled_process_runtime_capabilities( + process: CompiledProcessArtifact, +) -> tuple[str, ...]: + capabilities = set(_required_runtime_capabilities(process.stage_manifest)) + capabilities.update( + _runtime_schema_walsh_color_contraction_capabilities( + _runtime_schema_mapping(process.runtime_schema) + ) + ) + if process.color_contraction_payload is not None: + capabilities.add(SYMMETRIC_GROUP_FFT_COLOR_RUNTIME_CAPABILITY) + if process.model_parameter_evaluator is not None: + capabilities.update( + _required_runtime_capabilities(process.model_parameter_evaluator) + ) + if _uses_primary_helicity_recurrence(process): + capabilities.add(COMPILED_HELICITY_PRIMARY_RECURRENCE_CAPABILITY) + if process.color_selector_executions: + capabilities.add(COMPILED_COLOR_TOPOLOGY_LANES_CAPABILITY) + capabilities.add(COMPILED_RUNTIME_SELECTORS_CAPABILITY) + for record in _ordered_color_selector_executions( + process.color_selector_executions + ): + capabilities.update( + _compiled_execution_runtime_capabilities(record.execution) + ) + auxiliary = process.helicity_sum_execution + if auxiliary is not None: + capabilities.add(COMPILED_HELICITY_DUAL_LANE_CAPABILITY) + capabilities.update(_compiled_execution_runtime_capabilities(auxiliary)) + selector_lanes = _ordered_helicity_selector_executions( + process.helicity_selector_executions + ) + if selector_lanes: + capabilities.add(COMPILED_HELICITY_SELECTOR_UNION_CAPABILITY) + capabilities.add(COMPILED_RUNTIME_SELECTORS_CAPABILITY) + for record in selector_lanes: + capabilities.update( + _compiled_execution_runtime_capabilities(record.execution) + ) + return tuple(sorted(capabilities)) + + +def _uses_primary_helicity_recurrence(process: CompiledProcessArtifact) -> bool: + return _runtime_schema_uses_primary_helicity_recurrence( + _runtime_schema_mapping(process.runtime_schema), + has_helicity_sum_execution=process.helicity_sum_execution is not None, + ) + + +def _runtime_schema_uses_primary_helicity_recurrence( + runtime_schema: Mapping[str, object], + *, + has_helicity_sum_execution: bool, +) -> bool: + if has_helicity_sum_execution: + return False + recurrence = runtime_schema.get("helicity_recurrence") + return isinstance(recurrence, Mapping) and isinstance( + recurrence.get("materialization"), Mapping + ) + + +def _runtime_schema_uses_walsh_color_contraction( + runtime_schema: Mapping[str, object], +) -> bool: + return bool(_runtime_schema_walsh_color_contraction_capabilities(runtime_schema)) + + +def _runtime_schema_walsh_color_contraction_capabilities( + runtime_schema: Mapping[str, object], +) -> frozenset[str]: + amplitude_stage = runtime_schema.get("amplitude_stage") + if not isinstance(amplitude_stage, Mapping): + return frozenset() + contraction = amplitude_stage.get("color_contraction") + if not isinstance(contraction, Mapping): + return frozenset() + repeated_block = contraction.get("repeated_block") + if not isinstance(repeated_block, Mapping): + return frozenset() + factorized_block = repeated_block.get("factorized_block") + if not isinstance(factorized_block, Mapping): + return frozenset() + kind = factorized_block.get("kind") + if kind == "klein-four-walsh": + return frozenset({COMPILED_COLOR_CONTRACTION_WALSH_CAPABILITY}) + if kind == "elementary-abelian-walsh": + return frozenset({COMPILED_COLOR_CONTRACTION_WALSH_C2K_CAPABILITY}) + return frozenset() + + +def _compiled_execution_runtime_capabilities( + execution: CompiledExecutionArtifact, +) -> tuple[str, ...]: + capabilities = set(_required_runtime_capabilities(execution.stage_manifest)) + capabilities.update( + _runtime_schema_walsh_color_contraction_capabilities( + _runtime_schema_mapping(execution.runtime_schema) + ) + ) + if _runtime_schema_uses_primary_helicity_recurrence( + _runtime_schema_mapping(execution.runtime_schema), + has_helicity_sum_execution=False, + ): + capabilities.add(COMPILED_HELICITY_PRIMARY_RECURRENCE_CAPABILITY) + if execution.model_parameter_evaluator is not None: + capabilities.update( + _required_runtime_capabilities(execution.model_parameter_evaluator) + ) + if execution.color_selector_executions: + capabilities.add(COMPILED_COLOR_TOPOLOGY_LANES_CAPABILITY) + capabilities.add(COMPILED_RUNTIME_SELECTORS_CAPABILITY) + for record in _ordered_color_selector_executions( + execution.color_selector_executions + ): + capabilities.update( + _compiled_execution_runtime_capabilities(record.execution) + ) + selector_lanes = _ordered_helicity_selector_executions( + execution.helicity_selector_executions + ) + if selector_lanes: + capabilities.add(COMPILED_HELICITY_SELECTOR_UNION_CAPABILITY) + capabilities.add(COMPILED_RUNTIME_SELECTORS_CAPABILITY) + for record in selector_lanes: + capabilities.update( + _compiled_execution_runtime_capabilities(record.execution) + ) + return tuple(sorted(capabilities)) + + +def _process_runtime_capabilities( + process: ProcessArtifact, +) -> tuple[str, ...]: + if isinstance(process, OnTheFlyProcessArtifact): + return _on_the_fly_process_runtime_capabilities(process) + if isinstance(process, RecurrenceProcessArtifact): + return _recurrence_process_runtime_capabilities(process) + if isinstance(process, EagerPlanV3ProcessArtifact): + return _EAGER_PLAN_V3_RUNTIME_CAPABILITIES + return _compiled_process_runtime_capabilities(process) + + +def _on_the_fly_process_runtime_capabilities( + process: OnTheFlyProcessArtifact, +) -> tuple[str, ...]: + if process.color_accuracy == "lc": + color_capability = ON_THE_FLY_LC_COLOR_RUNTIME_CAPABILITY + elif process.color_accuracy in {"nlc", "full"}: + color_capability = ON_THE_FLY_CONTRACTED_COLOR_RUNTIME_CAPABILITY + else: + raise ValueError( + f"unsupported on-the-fly color accuracy {process.color_accuracy!r}" + ) + capabilities = { + color_capability, + ON_THE_FLY_RUNTIME_CAPABILITY, + } + if _uses_symmetric_group_fft_color_contraction(process): + capabilities.add(SYMMETRIC_GROUP_FFT_COLOR_RUNTIME_CAPABILITY) + return tuple(sorted(capabilities)) + + +def _recurrence_process_runtime_capabilities( + process: RecurrenceProcessArtifact, +) -> tuple[str, ...]: + color_capability = ( + RECURRENCE_COLOR_RUNTIME_CAPABILITY + if process.color_accuracy == "lc" + else RECURRENCE_CONTRACTED_COLOR_RUNTIME_CAPABILITY + ) + capabilities = { + RECURRENCE_DIRECT_ARENA_RUNTIME_CAPABILITY, + color_capability, + } + if process.helicity_selector_companion is not None: + capabilities.add(RECURRENCE_HELICITY_SELECTOR_COMPANION_RUNTIME_CAPABILITY) + if _uses_symmetric_group_fft_color_contraction(process): + capabilities.add(SYMMETRIC_GROUP_FFT_COLOR_RUNTIME_CAPABILITY) + return tuple(sorted(capabilities)) + + +def _uses_symmetric_group_fft_color_contraction( + process: OnTheFlyProcessArtifact | RecurrenceProcessArtifact, +) -> bool: + summary = process.color_contraction_summary + if not isinstance(summary, Mapping): + return False + factorization = summary.get("factorization") + return isinstance(factorization, Mapping) and ( + factorization.get("kind") == "symmetric-group-fourier" + ) + + +def _validate_symmetric_group_fft_provenance( + summary: Mapping[str, object], + *, + context: str, +) -> None: + factorization_raw = summary.get("factorization") + symmetric_group_fft = ( + isinstance(factorization_raw, Mapping) + and factorization_raw.get("kind") == "symmetric-group-fourier" + ) + provenance_raw = summary.get("fft_provenance") + if not symmetric_group_fft: + if provenance_raw is not None or "fft_provenance" in summary: + raise ValueError(f"{context} non-FFT summary carries FFT provenance") + return + factorization = _mapping(factorization_raw) + provenance = _mapping(provenance_raw) + if set(provenance) != { + "method", + "degree", + "channel_count", + "covered_local_group_count", + "residual_group_count", + "residual_entry_count", + "raw_kernel_bytes", + "transformed_kernel_bytes", + "capability", + }: + raise ValueError(f"{context} FFT provenance fields are invalid") + degree = _nonnegative_integer( + factorization.get("rank"), f"{context} FFT degree", minimum=2 + ) + channel_count = _nonnegative_integer( + factorization.get("coset_count"), f"{context} FFT channel count", minimum=1 + ) + group_count = _nonnegative_integer( + summary.get("group_count"), f"{context} group count", minimum=1 + ) + component_count = _nonnegative_integer( + summary.get("component_count"), f"{context} component count", minimum=1 + ) + if degree > 10 or group_count % component_count: + raise ValueError(f"{context} FFT group shape is invalid") + group_order = math.factorial(degree) + covered_local_group_count = channel_count * group_order + local_group_count = group_count // component_count + if covered_local_group_count > local_group_count: + raise ValueError(f"{context} FFT channels exceed local groups") + residual_group_count = local_group_count - covered_local_group_count + residual_entry_count = ( + local_group_count * (local_group_count + 1) // 2 + - covered_local_group_count * (covered_local_group_count + 1) // 2 + ) + kernel_entry_count = channel_count * (channel_count + 1) // 2 * group_order + raw_kernel_bytes = kernel_entry_count * 16 + transformed_kernel_bytes = kernel_entry_count * 8 + expected = { + "method": "symmetric-group-fourier", + "degree": degree, + "channel_count": channel_count, + "covered_local_group_count": covered_local_group_count, + "residual_group_count": residual_group_count, + "residual_entry_count": residual_entry_count, + "raw_kernel_bytes": raw_kernel_bytes, + "transformed_kernel_bytes": transformed_kernel_bytes, + "capability": SYMMETRIC_GROUP_FFT_COLOR_RUNTIME_CAPABILITY, + } + if ( + dict(provenance) != expected + or summary.get("entry_count") != kernel_entry_count + residual_entry_count + or summary.get("logical_entry_count") + != (kernel_entry_count + residual_entry_count) * component_count + ): + raise ValueError(f"{context} symmetric-group FFT provenance is inconsistent") + + +def _required_runtime_capabilities( + record: Mapping[str, object], +) -> tuple[str, ...]: + raw = record.get("required_runtime_capabilities") + if isinstance(raw, str | bytes) or not isinstance(raw, Sequence): + raise ValueError("runtime capability metadata must be a sequence") + values = tuple(str(item) for item in raw) + if values != tuple(sorted(set(values))): + raise ValueError("runtime capabilities must be sorted and unique") + unknown = set(values) - ( + EVALUATOR_RUNTIME_CAPABILITIES + | { + EAGER_PLAN_V3_RUNTIME_CAPABILITY, + RECURRENCE_DIRECT_ARENA_RUNTIME_CAPABILITY, + RECURRENCE_COLOR_RUNTIME_CAPABILITY, + } + ) + if unknown: + raise ValueError( + "unsupported evaluator runtime capabilities: " + ", ".join(sorted(unknown)) + ) + return values + + +def _extensions( + existing: ArtifactManifest | None, + *, + processes: Sequence[ProcessArtifact], + timings: Mapping[str, float], + api_bundle_requested: bool, + api_bundle_path: str | None, + eager_pack_identity: Mapping[str, object] | None, + execution_manifest_sha256_by_process: Mapping[str, str], + evaluator_payload_container: Mapping[str, object] | None, + recurrence_schedule_sharing: Mapping[str, object] | None, +) -> dict[str, object]: + result = {} if existing is None else _plain_mapping(existing.extensions) + previous = result.get("generation") + generation = dict(previous) if isinstance(previous, Mapping) else {} + concrete = generation.get("concrete_processes") + process_records = list(concrete) if isinstance(concrete, list) else [] + for process in processes: + record: dict[str, object] = { + "id": process.process_id, + "expression": process.expression, + "validation_momenta_path": ( + f"processes/{process.process_id}/validation-momenta.json" + ), + "filters": dict(process.generation_filters), + } + if _is_prepared_kernel_process(process): + record["execution_manifest_sha256"] = execution_manifest_sha256_by_process[ + process.process_id + ] + else: + record["runtime_schema_sha256"] = process.runtime_schema.sha256 + if process.helicity_sum_execution is not None: + record["helicity_sum_runtime_schema_sha256"] = ( + process.helicity_sum_execution.runtime_schema.sha256 + ) + if process.helicity_selector_executions: + record["helicity_selector_runtime_schema_sha256s"] = [ + lane.execution.runtime_schema.sha256 + for lane in _ordered_helicity_selector_executions( + process.helicity_selector_executions + ) + ] + process_records.append(record) + recurrence_schedule_profiles: dict[str, object] = {} + for process in processes: + if not isinstance(process, RecurrenceProcessArtifact): + continue + schedule_digest = _canonical_sha256( + process.recurrence_schedule_digest, + f"recurrence schedule {process.process_id!r} generation-profile digest", + ) + profile = _deep_plain(process.generation_profile) + previous_profile = recurrence_schedule_profiles.get(schedule_digest) + if previous_profile is not None and previous_profile != profile: + raise ValueError( + "shared recurrence schedule has inconsistent generation telemetry: " + f"{schedule_digest}" + ) + recurrence_schedule_profiles[schedule_digest] = profile + generation.update( + { + "schema_version": 1, + "concrete_processes": process_records, + "phase_timings_seconds": { + str(name): float(value) for name, value in timings.items() + }, + "api_bundle": { + "requested": api_bundle_requested, + "emitted": api_bundle_path is not None, + "path": api_bundle_path, + "scope": "root-artifact", + }, + } + ) + if recurrence_schedule_profiles: + generation["recurrence_schedule_profiles"] = recurrence_schedule_profiles + result["generation"] = generation + if eager_pack_identity is not None: + result[_EAGER_PACK_IDENTITY_EXTENSION] = _plain_mapping(eager_pack_identity) + if recurrence_schedule_sharing is not None: + result[_RECURRENCE_SCHEDULE_SHARING_EXTENSION] = _plain_mapping( + recurrence_schedule_sharing + ) + if evaluator_payload_container is None: + result.pop(_EVALUATOR_PAYLOAD_CONTAINER_EXTENSION, None) + else: + result[_EVALUATOR_PAYLOAD_CONTAINER_EXTENSION] = _plain_mapping( + evaluator_payload_container + ) + return result + + +def _default_api_bundle_hook() -> ApiBundleHook | None: + try: + from pyamplicol.artifacts.api_bundle import emit_api_bundle + except ImportError: + return None + return cast("ApiBundleHook", emit_api_bundle) + + +def _call_api_bundle_hook( + builder: ArtifactBuilder, + hook: ApiBundleHook, + validation_points: Mapping[str, Sequence[Sequence[float]]], +) -> str: + result = hook(builder, validation_points) + paths = tuple( + str(item["path"]) + if isinstance(item, Mapping) + else str(getattr(item, "path", "")) + for item in result + ) + if not paths or any(not path.startswith("API/") for path in paths): + raise ValueError("root API-bundle emitter returned an invalid payload set") + return "API" + + +def _validate_artifact_references(manifest: ArtifactManifest) -> None: + declared = {record.path for record in manifest.payloads} + _validate_evaluator_payload_container(manifest) + _validate_recurrence_schedule_sharing(manifest) + required = { + str(manifest.configuration["requested_path"]), + str(manifest.configuration["effective_path"]), + str(manifest.runtime["evaluator_manifest_path"]), + *(str(process["physics_path"]) for process in manifest.processes), + *( + f"processes/{process['id']}/execution.json" + for process in manifest.processes + ), + } + api_path = manifest.runtime.get("api_bundle_path") + if api_path is not None: + api_prefix = str(api_path).rstrip("/") + "/" + if not any(path.startswith(api_prefix) for path in declared): + raise ValueError("artifact API bundle has no declared payloads") + missing = required - declared + if missing: + raise ValueError( + "artifact references undeclared payloads: " + ", ".join(sorted(missing)) + ) + actual = { + path.relative_to(manifest.root).as_posix() + for path in manifest.root.rglob("*") + if path.is_file() and path.name != "artifact.json" + } + undeclared = actual - declared + if undeclared: + raise ValueError( + "artifact contains undeclared files: " + ", ".join(sorted(undeclared)) + ) + + +def _validate_recurrence_schedule_sharing(manifest: ArtifactManifest) -> None: + raw = manifest.extensions.get(_RECURRENCE_SCHEDULE_SHARING_EXTENSION) + if raw is None: + return + extension = _mapping(raw) + expected_fields = { + "kind", + "schema_version", + "index_path", + "index_sha256", + "schedule_count", + "binding_count", + "schedule_alias_count", + "runtime_ownership", + "interning_phase", + } + if set(extension) != expected_fields: + raise ValueError("recurrence schedule-sharing extension fields are invalid") + if ( + extension.get("kind") != "pyamplicol-recurrence-schedule-sharing" + or extension.get("schema_version") != RECURRENCE_SCHEDULE_SHARING_SCHEMA_VERSION + or extension.get("index_path") != RECURRENCE_SCHEDULE_INDEX_PATH + or extension.get("runtime_ownership") != "root-schedule-plus-process-binding" + or extension.get("interning_phase") != "before-direct-lowering" + ): + raise ValueError("recurrence schedule-sharing extension is incompatible") + records = {record.path: record for record in manifest.payloads} + index_record = records.get(RECURRENCE_SCHEDULE_INDEX_PATH) + if ( + index_record is None + or index_record.role != "evaluator-manifest" + or index_record.process_id is not None + or index_record.sha256 != extension.get("index_sha256") + ): + raise ValueError("recurrence schedule-sharing index record is invalid") + index = _mapping( + json.loads( + (manifest.root / RECURRENCE_SCHEDULE_INDEX_PATH).read_text(encoding="utf-8") + ) + ) + if ( + index.get("kind") != extension["kind"] + or index.get("schema_version") != extension["schema_version"] + or index.get("runtime_ownership") != extension["runtime_ownership"] + or index.get("interning_phase") != extension["interning_phase"] + ): + raise ValueError("recurrence schedule-sharing index is incompatible") + schedules = _sequence(index.get("schedules")) + bindings = _sequence(index.get("bindings")) + counts = { + "schedule_count": len(schedules), + "binding_count": len(bindings), + "schedule_alias_count": len(bindings) - len(schedules), + } + if any(index.get(name) != value for name, value in counts.items()) or any( + extension.get(name) != value for name, value in counts.items() + ): + raise ValueError("recurrence schedule-sharing counts are inconsistent") + + schedule_paths: dict[str, str] = {} + declared_processes_by_schedule: dict[str, tuple[str, ...]] = {} + for position, raw_schedule in enumerate(schedules): + schedule = _mapping(raw_schedule) + digest = _canonical_sha256( + schedule.get("digest"), + f"recurrence shared schedule {position} digest", + ) + if digest in schedule_paths: + raise ValueError(f"recurrence shared schedule {digest} is duplicated") + expected_path = f"recurrence/schedules/{digest}/recurrence-runtime.pacbin" + if schedule.get("path") != expected_path: + raise ValueError( + f"recurrence shared schedule {digest} has an invalid root path" + ) + raw_process_ids = _sequence(schedule.get("process_ids")) + declared_process_ids = tuple(str(value) for value in raw_process_ids) + if not declared_process_ids or declared_process_ids != tuple( + sorted(set(declared_process_ids)) + ): + raise ValueError( + f"recurrence shared schedule {digest} has invalid process ownership" + ) + record = records.get(expected_path) + if ( + record is None + or record.role != "evaluator-state" + or record.process_id is not None + or record.sha256 != schedule.get("sha256") + or record.size_bytes != schedule.get("size_bytes") + ): + raise ValueError( + f"recurrence shared schedule {digest} has an invalid payload record" + ) + schedule_paths[digest] = expected_path + declared_processes_by_schedule[digest] = declared_process_ids + + process_ids = {str(process["id"]) for process in manifest.processes} + binding_process_ids: set[str] = set() + support_masks: set[tuple[int, ...]] = set() + bound_processes_by_schedule: dict[str, list[str]] = {} + for position, raw_binding in enumerate(bindings): + binding = _mapping(raw_binding) + process_id = str(binding.get("process_id")) + if process_id not in process_ids or process_id in binding_process_ids: + raise ValueError( + f"recurrence process binding {position} has an invalid process ID" + ) + binding_process_ids.add(process_id) + if binding.get("abi") != RECURRENCE_PROCESS_BINDING_ABI: + raise ValueError( + f"recurrence process binding {process_id!r} has an invalid ABI" + ) + _canonical_sha256( + binding.get("process_digest"), + f"recurrence process binding {process_id!r} process digest", + ) + _canonical_sha256( + binding.get("process_semantic_digest"), + f"recurrence process binding {process_id!r} semantic digest", + ) + _recurrence_binding_native_schedule_semantic_digest( + binding, + process_id=process_id, + ) + _validate_recurrence_process_remap( + binding.get("remap"), + context=f"recurrence process binding {process_id!r}", + ) + schedule_digest = _canonical_sha256( + binding.get("schedule_digest"), + f"recurrence process binding {process_id!r} schedule digest", + ) + root_path = schedule_paths.get(schedule_digest) + if root_path is None: + raise ValueError( + f"recurrence process binding {process_id!r} references an unknown " + "schedule" + ) + bound_processes_by_schedule.setdefault(schedule_digest, []).append(process_id) + raw_support_words = _sequence(binding.get("process_support_words")) + support_mask = tuple(raw_support_words) + if ( + not support_mask + or any( + isinstance(word, bool) + or not isinstance(word, int) + or word < 0 + or word > (1 << 64) - 1 + for word in support_mask + ) + or sum(int(word).bit_count() for word in support_mask) != 1 + or support_mask[-1] == 0 + or support_mask in support_masks + ): + raise ValueError( + f"recurrence process binding {process_id!r} has an invalid support mask" + ) + support_masks.add(support_mask) + relative_binding_path = binding.get("path") + if relative_binding_path != "recurrence-binding.bin": + raise ValueError( + f"recurrence process binding {process_id!r} has an invalid binding path" + ) + binding_path = f"processes/{process_id}/{relative_binding_path}" + binding_record = records.get(binding_path) + if ( + binding_record is None + or binding_record.role != "evaluator-state" + or binding_record.process_id != process_id + or binding_record.sha256 != binding.get("sha256") + or binding_record.size_bytes != binding.get("size_bytes") + ): + raise ValueError( + f"recurrence process binding {process_id!r} has an invalid payload" + ) + for digest, declared_process_ids in declared_processes_by_schedule.items(): + bound_process_ids = tuple(sorted(bound_processes_by_schedule.get(digest, ()))) + if bound_process_ids != declared_process_ids: + raise ValueError( + f"recurrence shared schedule {digest} binding ownership is inconsistent" + ) + + +def _recurrence_binding_native_schedule_semantic_digest( + binding: Mapping[str, object], + *, + process_id: str, +) -> str: + return _canonical_sha256( + binding.get("native_schedule_semantic_digest"), + (f"recurrence process binding {process_id!r} native schedule semantic digest"), + ) + + +def _validate_recurrence_process_remap( + value: object, + *, + context: str, +) -> None: + remap = _mapping(value) + expected = { + "bijection_digest", + "source_slots", + "source_momentum_signs", + "source_helicity_signs", + "source_state_offsets", + "source_state_indices", + "public_flow_ids", + "physical_sector_ids", + "state_templates", + "source_templates", + "direct_executors", + "parameter_slots", + } + if set(remap) != expected: + raise ValueError(f"{context} remap fields are invalid") + _canonical_sha256( + remap.get("bijection_digest"), + f"{context} process-bijection digest", + ) + + def permutation(name: str) -> tuple[int, ...]: + values = tuple(_sequence(remap.get(name))) + if any( + isinstance(item, bool) or not isinstance(item, int) or item < 0 + for item in values + ) or tuple(sorted(values)) != tuple(range(len(values))): + raise ValueError(f"{context} {name} is not a permutation") + return cast(tuple[int, ...], values) + + sources = permutation("source_slots") + permutation("public_flow_ids") + permutation("physical_sector_ids") + for name in ("source_momentum_signs", "source_helicity_signs"): + signs = tuple(_sequence(remap.get(name))) + if len(signs) != len(sources) or any(sign not in {-1, 1} for sign in signs): + raise ValueError(f"{context} {name} is invalid") + source_state_offsets = tuple(_sequence(remap.get("source_state_offsets"))) + source_state_indices = tuple(_sequence(remap.get("source_state_indices"))) + if ( + len(source_state_offsets) != len(sources) + 1 + or not source_state_offsets + or source_state_offsets[0] != 0 + or source_state_offsets[-1] != len(source_state_indices) + or any( + isinstance(item, bool) or not isinstance(item, int) or item < 0 + for item in (*source_state_offsets, *source_state_indices) + ) + or any(left > right for left, right in pairwise(source_state_offsets)) + ): + raise ValueError(f"{context} source-state remap is invalid") + for start, stop in pairwise(source_state_offsets): + if tuple(sorted(source_state_indices[start:stop])) != tuple( + range(stop - start) + ): + raise ValueError(f"{context} source-state remap is not bijective") + for name in ( + "state_templates", + "source_templates", + "direct_executors", + "parameter_slots", + ): + sparse = _mapping(remap.get(name)) + if set(sparse) != {"count", "changes"}: + raise ValueError(f"{context} {name} fields are invalid") + count = sparse.get("count") + if isinstance(count, bool) or not isinstance(count, int) or count < 0: + raise ValueError(f"{context} {name} count is invalid") + changes = tuple(_sequence(sparse.get("changes"))) + mapping = list(range(count)) + previous = -1 + for raw_change in changes: + change = tuple(_sequence(raw_change)) + if ( + len(change) != 2 + or any( + isinstance(item, bool) + or not isinstance(item, int) + or item < 0 + or item >= count + for item in change + ) + or change[0] <= previous + or change[0] == change[1] + ): + raise ValueError(f"{context} {name} change is invalid") + source, target = cast(tuple[int, int], change) + mapping[source] = target + previous = source + if tuple(sorted(mapping)) != tuple(range(count)): + raise ValueError(f"{context} {name} is not bijective") + + +def _validate_evaluator_payload_container(manifest: ArtifactManifest) -> None: + raw = manifest.extensions.get(_EVALUATOR_PAYLOAD_CONTAINER_EXTENSION) + if raw is None: + return + extension = _mapping(raw) + expected_fields = { + "kind", + "schema_version", + "storage_abi", + "path", + "member_count", + "unpacked_size_bytes", + "index_sha256", + } + if set(extension) != expected_fields: + raise ValueError("evaluator payload container extension fields are invalid") + if extension != { + **extension, + "kind": _EVALUATOR_PAYLOAD_CONTAINER_KIND, + "schema_version": _EVALUATOR_PAYLOAD_CONTAINER_SCHEMA_VERSION, + "storage_abi": _EVALUATOR_PAYLOAD_CONTAINER_STORAGE_ABI, + "path": _EVALUATOR_PAYLOAD_CONTAINER_PATH, + }: + raise ValueError("evaluator payload container extension contract is invalid") + records = { + record.path: record + for record in manifest.payloads + if record.path == _EVALUATOR_PAYLOAD_CONTAINER_PATH + } + if len(records) != 1: + raise ValueError("evaluator payload container is not a declared payload") + record = records[_EVALUATOR_PAYLOAD_CONTAINER_PATH] + if ( + record.role != "evaluator-state" + or record.media_type != "application/octet-stream" + or record.process_id is not None + or record.target != manifest.producer["target"] + ): + raise ValueError("evaluator payload container manifest record is invalid") + loose = sorted( + payload.path + for payload in manifest.payloads + if payload.path != _EVALUATOR_PAYLOAD_CONTAINER_PATH + and _packed_evaluator_member_kind(payload.path) is not None + ) + if loose: + raise ValueError( + "artifact declares loose packed evaluator payloads: " + ", ".join(loose) + ) + with PacbinReader.open( + manifest.root / _EVALUATOR_PAYLOAD_CONTAINER_PATH, + verify_payloads=False, + ) as reader: + expected = _evaluator_payload_container_extension(reader.index) + if extension != expected: + raise ValueError( + "evaluator payload container metadata does not match its index" + ) + + +def _config_payload(config: GenerationConfig | RunConfig) -> dict[str, object]: + payload = _plain_mapping(_mapping(config_to_dict(config))) + if isinstance(config, GenerationConfig): + return {"schema_version": 1, "generation": payload} + return payload + + +def _effective_config_payload( + requested: Mapping[str, object], + *, + disable_api_bundle: bool, +) -> dict[str, object]: + result = _deep_plain(requested) + if disable_api_bundle: + generation = result.get("generation") + if not isinstance(generation, dict): + raise ValueError("generation configuration section is missing") + generation["emit_api_bundle"] = False + return result + + +def _bundle_requested(config: GenerationConfig | RunConfig) -> bool: + generation = config if isinstance(config, GenerationConfig) else config.generation + return bool(generation.emit_api_bundle) + + +def _existing_bundle_path(existing: ArtifactManifest | None) -> str | None: + if existing is None: + return None + value = existing.runtime.get("api_bundle_path") + return None if value is None else str(value) + + +def _toml_bytes(payload: Mapping[str, object]) -> bytes: + lines: list[str] = [] + _write_toml_table(lines, (), payload, emit_header=False) + return ("\n".join(lines).rstrip() + "\n").encode("utf-8") + + +def _write_toml_table( + lines: list[str], + path: tuple[str, ...], + payload: Mapping[str, object], + *, + emit_header: bool, +) -> None: + scalars = [ + (str(key), value) + for key, value in payload.items() + if value is not None and not isinstance(value, Mapping) + ] + tables = [ + (str(key), _mapping(value)) + for key, value in payload.items() + if isinstance(value, Mapping) + ] + if emit_header: + if lines and lines[-1] != "": + lines.append("") + lines.append("[" + ".".join(_toml_key(part) for part in path) + "]") + lines.extend(f"{_toml_key(key)} = {_toml_value(value)}" for key, value in scalars) + for key, table in tables: + _write_toml_table(lines, (*path, key), table, emit_header=True) + + +def _toml_value(value: object) -> str: + if isinstance(value, bool): + return "true" if value else "false" + if isinstance(value, str): + return json.dumps(value, ensure_ascii=True) + if isinstance(value, os.PathLike): + return json.dumps(os.fspath(value), ensure_ascii=True) + if isinstance(value, int | float): + return repr(value) + if isinstance(value, Mapping): + entries = ( + f"{_toml_key(str(key))} = {_toml_value(entry)}" + for key, entry in value.items() + if entry is not None + ) + return "{ " + ", ".join(entries) + " }" + if isinstance(value, Sequence) and not isinstance(value, str | bytes): + return "[" + ", ".join(_toml_value(item) for item in value) + "]" + raise TypeError(f"configuration value is not TOML serializable: {value!r}") + + +def _toml_key(value: str) -> str: + return value if _SAFE_TOML_KEY.fullmatch(value) else json.dumps(value) + + +def _media_type(path: Path) -> str: + suffix = path.suffix.lower() + if suffix in {".json"}: + return "application/json" + if suffix in {".c", ".cc", ".cpp", ".cxx", ".h", ".hpp"}: + return "text/x-c++src" + if suffix == ".symjit": + return "application/vnd.symjit.application" + return "application/octet-stream" + + +def _distribution_version(name: str, fallback: str) -> str: + try: + return importlib.metadata.version(name) + except importlib.metadata.PackageNotFoundError: + return fallback + + +def _mapping(value: object) -> Mapping[str, object]: + if not isinstance(value, Mapping): + raise TypeError("artifact metadata value must be an object") + return {str(key): item for key, item in value.items()} + + +def _plain_mapping(value: Mapping[str, object]) -> dict[str, object]: + return _deep_plain(value) + + +def _deep_plain(value: Mapping[str, object]) -> dict[str, object]: + def convert(item: object) -> object: + if isinstance(item, Mapping): + return {str(key): convert(entry) for key, entry in item.items()} + if isinstance(item, Sequence) and not isinstance(item, str | bytes): + return [convert(entry) for entry in item] + return item + + return {str(key): convert(item) for key, item in value.items()} + + +__all__ = [ + "EAGER_DIRECT_ARENA_RUNTIME_CAPABILITY", + "EAGER_PLAN_V3_ABI", + "EAGER_PLAN_V3_RUNTIME_CAPABILITY", + "EAGER_RUNTIME_CONTAINER_KIND", + "EAGER_RUNTIME_CONTAINER_SCHEMA_VERSION", + "EAGER_RUNTIME_LAYOUT_ABI", + "EAGER_RUNTIME_STORAGE_ABI", + "ON_THE_FLY_PUBLIC_METADATA_KIND", + "ON_THE_FLY_RUNTIME_CONTAINER_KIND", + "ON_THE_FLY_RUNTIME_CONTAINER_SCHEMA_VERSION", + "ON_THE_FLY_RUNTIME_KIND", + "ON_THE_FLY_RUNTIME_STORAGE_ABI", + "RECURRENCE_COLOR_RUNTIME_CAPABILITY", + "RECURRENCE_DIRECT_ARENA_RUNTIME_CAPABILITY", + "RECURRENCE_DIRECT_BACKEND_ABI", + "RECURRENCE_DIRECT_TEMPLATE_ABI", + "RECURRENCE_PLAN_ABI", + "RECURRENCE_RUNTIME_CONTAINER_KIND", + "RECURRENCE_RUNTIME_CONTAINER_SCHEMA_VERSION", + "RECURRENCE_RUNTIME_KIND", + "RECURRENCE_RUNTIME_LAYOUT_ABI", + "RECURRENCE_RUNTIME_STORAGE_ABI", + "ApiBundleHook", + "ArtifactWriteResult", + "CompiledExecutionArtifact", + "CompiledProcessArtifact", + "EagerPlanV3ProcessArtifact", + "OnTheFlyProcessArtifact", + "ProcessArtifact", + "RecurrenceHelicitySelectorPlanArtifact", + "RecurrenceProcessArtifact", + "build_api_validation_points", + "write_schema_v3_artifact", +] diff --git a/pyamplicol/src/pyamplicol/generation/contracts.py b/pyamplicol/src/pyamplicol/generation/contracts.py new file mode 100644 index 0000000000..3f2aa4279f --- /dev/null +++ b/pyamplicol/src/pyamplicol/generation/contracts.py @@ -0,0 +1,147 @@ +# SPDX-License-Identifier: 0BSD +from __future__ import annotations + +import hashlib +import json +import marshal +from collections.abc import Mapping, Sequence +from dataclasses import dataclass, field + +from ..models.base import Model +from .dag_types import GenericDAG + +LC_SECTOR_SELECTOR_PARAMETER = "runtime.lc_sector_id" +EXPRESSION_SCHEMA_CONTRACT_VERSION = 2 +_REQUIRED_RUNTIME_SCHEMA_KEYS = frozenset( + { + "amplitude_stage", + "current_storage", + "momentum_slots", + "parameter_layout", + "stages", + "value_storage", + } +) +@dataclass(frozen=True, slots=True) +class RuntimeExpressionSchema: + """Canonical, dependency-neutral stage compiler input schema.""" + + canonical_json: str + contract_version: int = EXPRESSION_SCHEMA_CONTRACT_VERSION + _payload_blob: bytes = field( + init=False, + repr=False, + compare=False, + hash=False, + ) + + def __post_init__(self) -> None: + if self.contract_version != EXPRESSION_SCHEMA_CONTRACT_VERSION: + raise ValueError( + "unsupported runtime expression-schema contract version " + f"{self.contract_version}" + ) + try: + payload = json.loads(self.canonical_json) + except json.JSONDecodeError as exc: + raise ValueError("runtime expression schema is not valid JSON") from exc + if not isinstance(payload, dict): + raise ValueError("runtime expression schema root must be an object") + missing = sorted(_REQUIRED_RUNTIME_SCHEMA_KEYS - payload.keys()) + if missing: + raise ValueError( + "runtime expression schema is missing: " + ", ".join(missing) + ) + # The blob is an immutable, process-local cache of already validated JSON. + # It is never persisted or loaded from untrusted input. + object.__setattr__(self, "_payload_blob", marshal.dumps(payload)) + + @classmethod + def from_mapping( + cls, + payload: Mapping[str, object] | RuntimeExpressionSchema, + ) -> RuntimeExpressionSchema: + if isinstance(payload, cls): + return payload + try: + canonical = json.dumps( + payload, + allow_nan=False, + separators=(",", ":"), + sort_keys=True, + ) + except (TypeError, ValueError) as exc: + raise ValueError( + "runtime expression schema must contain canonical JSON values" + ) from exc + return cls(canonical_json=canonical) + + def to_mapping(self) -> dict[str, object]: + payload = marshal.loads(self._payload_blob) + if not isinstance(payload, dict): # pragma: no cover - internal invariant + raise RuntimeError("runtime expression schema cache is not an object") + return {str(key): value for key, value in payload.items()} + + def __reduce__(self) -> tuple[object, tuple[str, int]]: + return type(self), (self.canonical_json, self.contract_version) + + @property + def sha256(self) -> str: + return hashlib.sha256(self.canonical_json.encode("utf-8")).hexdigest() + + +@dataclass(frozen=True, slots=True) +class StageCompilationInput: + dag: GenericDAG + model: Model + runtime_schema: RuntimeExpressionSchema + + def __post_init__(self) -> None: + if not isinstance(self.dag, GenericDAG): + raise TypeError("stage compilation input requires a GenericDAG") + if not isinstance(self.model, Model): + raise TypeError("stage compilation input requires a Model") + if not isinstance(self.runtime_schema, RuntimeExpressionSchema): + raise TypeError( + "stage compilation input requires a RuntimeExpressionSchema" + ) + + +def runtime_coupling_parameter_names( + vertex_kind: int, + vertex_particles: Sequence[int], + coupling: Sequence[object], + *, + model: Model | None = None, +) -> list[str | None]: + runtime_names = getattr(model, "runtime_parameter_names_for_vertex", None) + if callable(runtime_names): + return [str(name) for name in runtime_names(int(vertex_kind))] + lowering_rule = getattr(model, "vertex_lowering_rule", None) + if callable(lowering_rule): + rule = lowering_rule(int(vertex_kind)) + if rule.coupling_mode in {"fixed", "none"}: + return [None for _value in coupling] + particles = "_".join(str(int(pdg)) for pdg in vertex_particles) + base = f"coupling.{int(vertex_kind)}.{particles}" + names: list[str | None] = [] + for component, value in enumerate(coupling): + if not isinstance(value, str | int | float): + raise TypeError( + f"runtime coupling components must be numeric, got {value!r}" + ) + numeric_value = float(value) + if component == 1 and numeric_value == -10.0: + names.append(None) + else: + names.append(f"{base}.component_{component}") + return names + + +__all__ = [ + "EXPRESSION_SCHEMA_CONTRACT_VERSION", + "LC_SECTOR_SELECTOR_PARAMETER", + "RuntimeExpressionSchema", + "StageCompilationInput", + "runtime_coupling_parameter_names", +] diff --git a/pyamplicol/src/pyamplicol/generation/correlated.py b/pyamplicol/src/pyamplicol/generation/correlated.py new file mode 100644 index 0000000000..dbe8160862 --- /dev/null +++ b/pyamplicol/src/pyamplicol/generation/correlated.py @@ -0,0 +1,494 @@ +# SPDX-License-Identifier: 0BSD +"""Opt-in generation of complete coherent amplitudes and colour operators. + +The ordinary generator never imports this module. This first implementation +uses the existing generic compiled pipeline, but bypasses reductions whose +proof applies only to physical-helicity squared amplitudes. In particular, +arbitrary external vector sources must not inherit physical-helicity zeros. +""" + +from __future__ import annotations + +from collections.abc import Callable, Mapping, Sequence +from dataclasses import dataclass, replace +from os import PathLike +from pathlib import Path +from types import MappingProxyType +from typing import Literal, cast + +from pyamplicol.api.errors import GenerationError +from pyamplicol.api.models import CompiledModel +from pyamplicol.api.requests import ModelSource, ProcessSet +from pyamplicol.api.results import GenerationResult +from pyamplicol.artifacts import ArtifactBuilder +from pyamplicol.color.correlator_matrices import ( + build_color_correlator_matrices, + process_color_legs, +) +from pyamplicol.color.plan import GenericColorPlan, build_color_plan +from pyamplicol.config import ( + Action, + ColorContraction, + ColorFFTBasis, + ConfigClamp, + ConfigResolution, + EvaluatorExecutionMode, + GenerationConfig, + LCFlowLayout, + RelationDiscoveryMode, + RunConfig, +) +from pyamplicol.correlators import CorrelatorConfig +from pyamplicol.models.base import Model +from pyamplicol.processes.ir import CanonicalProcessIR +from pyamplicol.reporting import ProgressSink + +from .artifact_writer import ApiBundleHook, ArtifactWriteResult +from .contracts import RuntimeExpressionSchema +from .dag_algorithms import _infer_minimal_coupling_order_limits_from_color_plan +from .dag_compiler import compile_generic_dag +from .dag_types import GenericDAG +from .numerical_current_warmup import generic_dag_numerical_current_opt_out_report +from .progress import PhaseHandle +from .service import ( + GenerationBackend, + _CompiledProcess, + _DagProcess, + _EvaluatorProcess, + _NoModelSupportedAmplitudes, + _PreparedProcessConstruction, + _ProcessSelection, +) +from .validation import build_validation_point + +CORRELATOR_PAYLOAD_PATH = "correlators.json" + + +def correlated_configuration( + config: GenerationConfig | RunConfig | ConfigResolution | None, +) -> ConfigResolution: + """Retain requested colour accuracy with direct/compiled source execution.""" + + if isinstance(config, ConfigResolution): + requested, effective, clamps = config.requested, config.effective, config.clamps + elif isinstance(config, RunConfig): + requested = effective = config + clamps = () + elif config is None or isinstance(config, GenerationConfig): + requested = effective = RunConfig( + action=Action.GENERATE, + generation=GenerationConfig() if config is None else config, + ) + clamps = () + else: + raise TypeError("invalid correlated generation configuration") + if ( + effective.color.contraction is ColorContraction.SYMMETRIC_GROUP_FFT + and effective.color.fft_basis is ColorFFTBasis.ADJOINT + ): + raise ValueError( + "adjoint FFT is not supported for correlated matrix elements; " + "use the direct colour contraction" + ) + result = replace( + effective, + color=replace( + effective.color, + contraction=ColorContraction.DIRECT, + fft_basis=ColorFFTBasis.TRACE, + lc_flow_layout=LCFlowLayout.TOPOLOGY_REPLAY, + ), + evaluator=replace( + effective.evaluator, execution_mode=EvaluatorExecutionMode.COMPILED + ), + generation=replace( + effective.generation, + relation_discovery=replace( + effective.generation.relation_discovery, mode=RelationDiscoveryMode.OFF + ), + ), + ) + paths = ( + "color.contraction", + "color.fft_basis", + "color.lc_flow_layout", + "evaluator.execution_mode", + "generation.relation_discovery.mode", + ) + + def field(config: RunConfig, path: str) -> object: + value: object = config + for name in path.split("."): + value = getattr(value, name) + return value + + adjustments = tuple( + ConfigClamp( + path=path, + requested=field(effective, path), + effective=field(result, path), + reason="correlations require complete coherent generic compiled amplitudes", + ) + for path in paths + if field(effective, path) != field(result, path) + ) + # The source configuration may already have resolved recurrence's auto + # compression to false. Re-select the default for this compiled lane, but + # retain explicit user or prepared-pack choices. + evaluator = result.evaluator + if requested.evaluator.jit.compress == "auto" and not any( + clamp.path == "evaluator.jit.compress" for clamp in clamps + ): + evaluator = replace(evaluator, jit=replace(evaluator.jit, compress="auto")) + result = replace( + result, + evaluator=replace( + evaluator, + jit=replace(evaluator.jit, compress=evaluator.resolved_jit_compress), + ), + ) + return ConfigResolution(requested, result, (*clamps, *adjustments)) + + +@dataclass(frozen=True) +class CorrelatedProcessMetadata: + """Generation-local inputs; no new serialized copy of the DAG or model.""" + + process: CanonicalProcessIR + color_plan: GenericColorPlan + runtime_schema: RuntimeExpressionSchema + aliases: tuple[Mapping[str, object], ...] + + +class CorrelatedGenerationBackend(GenerationBackend): + """Safety lane sharing model resolution, stage compilation and publishing.""" + + def __init__( + self, + config: GenerationConfig | RunConfig | ConfigResolution | None, + progress: ProgressSink | None, + *, + declarations: CorrelatorConfig, + api_bundle_hook: ApiBundleHook | None = None, + process_selection: _ProcessSelection | None = None, + ) -> None: + if not isinstance(declarations, CorrelatorConfig): + raise TypeError("correlators must be a CorrelatorConfig") + super().__init__( + correlated_configuration(config), + progress, + api_bundle_hook=api_bundle_hook, + process_selection=process_selection, + ) + self.declarations = declarations + self._correlated_processes: dict[str, CorrelatedProcessMetadata] = {} + selection = self._process_selection + partial = tuple( + name + for name in ( + "max_color_sectors", + "reference_color_order", + "selected_color_sector_ids", + "selected_source_helicities", + ) + if getattr(selection, name) is not None + ) + if partial: + raise GenerationError( + "correlated generation requires complete colour and helicity coverage; " + "remove " + ", ".join(f"process.{name}" for name in partial) + ) + + @property + def _color_accuracy(self) -> str: + # The inherited setting selects the correlated metric, not the source + # basis or native ordinary consumer. All amplitudes remain full colour. + return "full" + + @property + def process_metadata(self) -> Mapping[str, CorrelatedProcessMetadata]: + return MappingProxyType(self._correlated_processes) + + def generate( + self, + processes: ProcessSet, + output: PathLike[str] | str, + *, + model: ModelSource | CompiledModel | None = None, + mode: str = "error", + ) -> GenerationResult: + if mode == "append": + raise GenerationError("correlated generation does not support append mode") + for alias in processes.aliases: + permutation = tuple(alias.particle_permutation) + if permutation and permutation != tuple(range(len(permutation))): + raise GenerationError( + "correlated generation currently requires identity process aliases" + ) + self._correlated_processes.clear() + return super().generate(processes, output, model=model, mode=mode) + + def _prepare_process_construction( + self, process: CanonicalProcessIR, model: Model + ) -> _PreparedProcessConstruction: + if process.color_accuracy != "full": + raise GenerationError("correlated process expansion must use full colour") + by_label = {leg.label: leg for leg in process.legs} + for label in self.declarations.spin_legs: + leg = by_label.get(label) + if leg is None or leg.outgoing_pdg is None: + raise GenerationError( + f"spin-correlated leg {label} is absent from {process.process!r}" + ) + source = model._source_ir(int(leg.outgoing_pdg)) + if ( + source.wavefunction_family != "vector" + or source.component_dimension != 4 + ): + raise GenerationError( + f"spin-correlated leg {label} requires a full " + "four-component vector source" + ) + plan = build_color_plan( + process, color_accuracy="full", fold_trace_reflections=False + ) + if not plan.sectors or plan.truncated: + raise GenerationError( + f"process {process.process!r} has no complete colour plan" + ) + limits = self._coupling_order_limits + run = self._run_config + if run is not None and run.process.coupling_order_policy == "minimal": + limits = ( + _infer_minimal_coupling_order_limits_from_color_plan( + process, + model=model, + color_plan=plan, + max_coupling_orders=limits or None, + ) + or limits + ) + return _PreparedProcessConstruction( + process=process, + complete_color_plan=plan, + restricted_color_plan=plan, + materialized_sector_ids=None, + coupling_order_limits=MappingProxyType(dict(limits)), + topology_replay=None, + ) + + def _compile_concrete_process( + self, + process: CanonicalProcessIR, + model: Model, + *, + progress_callback: Callable[[Mapping[str, str | int]], None] | None = None, + ) -> tuple[GenericDAG, dict[str, object]]: + prepared = self._prepare_process_construction(process, model) + limits = dict(prepared.coupling_order_limits) + dag = compile_generic_dag( + process, + model=model, + color_plan=prepared.complete_color_plan, + max_coupling_orders=limits or None, + max_quark_pairs=self._max_quark_pairs, + selected_source_helicities=None, + selected_color_sector_ids=None, + online_evaluation_reuse=False, + backward_live_planning=False, + lc_all_ordering_symmetry=False, + closure_side_mask_pruning=False, + color_order_mask_pruning=False, + species_reachability_pruning=False, + progress_callback=progress_callback, + ) + if dag.truncated: + raise GenerationError( + f"process {process.process!r} DAG was unexpectedly truncated" + ) + if not dag.has_amplitudes: + raise _NoModelSupportedAmplitudes( + f"process {process.process!r} has no model-supported " + "tree-level amplitudes" + ) + return dag, { + "key": process.key, + "process": process.process, + "color_sector_count": dag.color_plan.sector_count, + "color_coverage": dag.color_coverage, + "helicity_coverage": dag.helicity_coverage, + "source_count": len(dag.sources), + "current_count": len(dag.currents), + "interaction_count": len(dag.interactions), + "interaction_evaluation_count": dag.interaction_evaluation_count, + "amplitude_root_count": len(dag.amplitude_roots), + "coupling_order_limits": limits, + } + + def _prepare_warmup_process_inner( + self, + process: _DagProcess, + model: Model, + *, + index: int, + progress: PhaseHandle, + ) -> _CompiledProcess: + # No parity reduction, source-zero discovery, replay or numerical reuse. + # Even exact identities proved only on physical sources are inapplicable. + dag = process.dag + validation = self._generation_config.validation + sample_count = ( + validation.samples + if validation.enabled and validation.post_build_validation + else 1 + ) + points = tuple( + build_validation_point( + dag, + model, + process_id=process.expanded.request.name, + seed=validation.seed + index * sample_count + sample_index, + ) + for sample_index in range(sample_count) + ) + relation = generic_dag_numerical_current_opt_out_report( + dag, execution_mode="compiled" + ) + filters = { + "structural_helicity_reduction": { + "applied": False, + "before_amplitude_roots": len(dag.amplitude_roots), + "after_amplitude_roots": len(dag.amplitude_roots), + "mode": "complete correlated source basis", + }, + "relation_discovery": relation, + } + progress.update(2, message="complete correlated source basis retained") + return _CompiledProcess( + expanded=process.expanded, + dag=dag, + helicity_sum_dag=None, + helicity_selector_union_dag=None, + coverage={**process.coverage, "relation_discovery": relation}, + filters=filters, + validation_points=points, + ) + + def _construct_evaluator( + self, process: _CompiledProcess, model: Model, phase: PhaseHandle + ) -> _EvaluatorProcess: + result = super()._construct_evaluator(process, model, phase) + if ( + result.helicity_sum_runtime_schema is not None + or result.helicity_selector_lanes + or result.color_selector_lanes + ): + raise GenerationError( + "correlated generation cannot contain companion or replay lanes" + ) + # Full source coverage does not promise the separate native per-point + # selector ABI. This lane deliberately has no selector/recurrence + # schedules; the correlated executor selects its coherent groups itself. + payload = result.runtime_schema.to_mapping() + physics = dict(cast(Mapping[str, object], payload["physics"])) + extensions = dict(cast(Mapping[str, object], physics.get("extensions", {}))) + extensions.pop("runtime_selectors", None) + physics["extensions"] = extensions + payload["physics"] = physics + schema = RuntimeExpressionSchema.from_mapping(payload) + result = replace( + result, + runtime_schema=schema, + stage_input=replace(result.stage_input, runtime_schema=schema), + ) + self._correlated_processes[process.expanded.request.name] = ( + CorrelatedProcessMetadata( + process=process.dag.process, + color_plan=process.dag.color_plan, + runtime_schema=result.runtime_schema, + aliases=process.expanded.aliases, + ) + ) + return result + + def correlator_payload(self) -> dict[str, object]: + """Build the declared exact matrices once, at the ordinary write boundary.""" + + processes: dict[str, object] = {} + for process_id, metadata in self._correlated_processes.items(): + declarations = self.declarations._resolve_color( + process_color_legs(metadata.process) + ) + amplitude = cast( + Mapping[str, object], + metadata.runtime_schema.to_mapping()["amplitude_stage"], + ) + groups = cast(Sequence[Mapping[str, object]], amplitude["coherent_groups"]) + basis_by_helicity: dict[tuple[int, ...], set[int]] = {} + for group in groups: + helicities = tuple(cast(Sequence[int], group["helicities"])) + sector_id = cast(int, group["color_sector_id"]) + basis = basis_by_helicity.setdefault(helicities, set()) + basis.add(sector_id) + if not basis_by_helicity: + raise GenerationError("correlated amplitudes have no coherent groups") + owners = next(iter(basis_by_helicity.values())) + if any(basis != owners for basis in basis_by_helicity.values()): + raise GenerationError( + "correlated colour basis differs between retained helicities" + ) + # Open-line colour plans retain aliases whose amplitudes already + # belong to canonical owners. Never contract those aliases twice. + matrices = build_color_correlator_matrices( + metadata.color_plan, + declarations.color_requests, + sector_ids=sorted(owners), + color_accuracy=cast( + Literal["lc", "nlc", "full"], + self._run_config.color.accuracy + if self._run_config is not None + else "lc", + ), + ) + processes[process_id] = { + "declarations": declarations.to_json_dict(), + "spin_legs": list(declarations.spin_legs), + "matrices": [matrix.to_json_dict() for matrix in matrices], + # The native execution schema keeps root group IDs but omits + # these compiler descriptors, needed only by this consumer. + "coherent_groups": groups, + } + return { + "schema_version": 1, + "complete_source_basis": True, + "declarations": self.declarations.to_json_dict(), + "processes": processes, + } + + def _write_correlator_payload( + self, builder: ArtifactBuilder + ) -> Mapping[str, object]: + builder.add_json( + CORRELATOR_PAYLOAD_PATH, self.correlator_payload(), role="runtime-physics" + ) + return {"correlators": {"schema_version": 1, "path": CORRELATOR_PAYLOAD_PATH}} + + def _write_artifact(self, *args: object, **kwargs: object) -> ArtifactWriteResult: + return super()._write_artifact( + *args, **kwargs, payload_hook=self._write_correlator_payload + ) + + +def generate_correlated( + processes: ProcessSet, + destination: str | Path, + *, + declarations: CorrelatorConfig, + config: GenerationConfig | RunConfig | ConfigResolution | None = None, + progress: ProgressSink | None = None, + model: ModelSource | CompiledModel | None = None, + mode: Literal["error", "append", "replace"] = "error", +) -> GenerationResult: + return CorrelatedGenerationBackend( + config, progress, declarations=declarations + ).generate(processes, destination, model=model, mode=mode) diff --git a/pyamplicol/src/pyamplicol/generation/current_plan.py b/pyamplicol/src/pyamplicol/generation/current_plan.py new file mode 100644 index 0000000000..0551b52c1c --- /dev/null +++ b/pyamplicol/src/pyamplicol/generation/current_plan.py @@ -0,0 +1,509 @@ +# SPDX-License-Identifier: 0BSD +from __future__ import annotations + +from collections import Counter +from collections.abc import Iterable, Mapping +from dataclasses import dataclass + +from ..color.plan import GenericColorPlan +from ..models.base import Model +from ..processes.ir import CanonicalProcessIR +from .dag_algorithms import contributing_color_sector_ids +from .dag_compiler import GenericDAGCompiler +from .dag_types import ( + AmplitudeRoot, + CurrentNode, + GenericDAG, + InteractionNode, +) + +GenericCurrentNode = CurrentNode +GenericInteractionNode = InteractionNode +GenericClosureNode = AmplitudeRoot + + +@dataclass(frozen=True) +class GenericColorSectorUseSummary: + color_sector: int + current_count: int + interaction_count: int + closure_count: int + required_vertex_kind_counts: tuple[tuple[int, int], ...] + ready_vertex_kinds: tuple[int, ...] + pending_vertex_kinds: tuple[int, ...] + unimplemented_vertex_kinds: tuple[int, ...] + + @property + def full_tensor_network_ready(self) -> bool: + return not self.pending_vertex_kinds and not self.unimplemented_vertex_kinds + + def to_json_dict(self) -> dict[str, object]: + return { + "color_sector": self.color_sector, + "current_count": self.current_count, + "interaction_count": self.interaction_count, + "closure_count": self.closure_count, + "full_tensor_network_ready": self.full_tensor_network_ready, + "required_vertex_kind_counts": [ + [kind, count] for kind, count in self.required_vertex_kind_counts + ], + "ready_vertex_kinds": list(self.ready_vertex_kinds), + "pending_vertex_kinds": list(self.pending_vertex_kinds), + "unimplemented_vertex_kinds": list(self.unimplemented_vertex_kinds), + } + + +@dataclass(frozen=True) +class GenericCurrentStage: + stage_index: int + subset_size: int + current_ids: tuple[int, ...] + interaction_ids: tuple[int, ...] + required_vertex_kind_counts: tuple[tuple[int, int], ...] + ready_vertex_kinds: tuple[int, ...] + pending_vertex_kinds: tuple[int, ...] + unimplemented_vertex_kinds: tuple[int, ...] + color_sector_summaries: tuple[GenericColorSectorUseSummary, ...] + + @property + def full_tensor_network_ready(self) -> bool: + return not self.pending_vertex_kinds and not self.unimplemented_vertex_kinds + + def to_json_dict(self) -> dict[str, object]: + return { + "stage_index": self.stage_index, + "subset_size": self.subset_size, + "current_ids": list(self.current_ids), + "interaction_ids": list(self.interaction_ids), + "interaction_count": len(self.interaction_ids), + "full_tensor_network_ready": self.full_tensor_network_ready, + "required_vertex_kind_counts": [ + [kind, count] for kind, count in self.required_vertex_kind_counts + ], + "ready_vertex_kinds": list(self.ready_vertex_kinds), + "pending_vertex_kinds": list(self.pending_vertex_kinds), + "unimplemented_vertex_kinds": list(self.unimplemented_vertex_kinds), + "color_sector_summaries": [ + summary.to_json_dict() for summary in self.color_sector_summaries + ], + } + + +@dataclass(frozen=True) +class GenericAmplitudeStage: + closure_ids: tuple[int, ...] + required_vertex_kind_counts: tuple[tuple[int, int], ...] + ready_vertex_kinds: tuple[int, ...] + pending_vertex_kinds: tuple[int, ...] + unimplemented_vertex_kinds: tuple[int, ...] + color_sector_summaries: tuple[GenericColorSectorUseSummary, ...] + + @property + def full_tensor_network_ready(self) -> bool: + return ( + bool(self.closure_ids) + and not self.pending_vertex_kinds + and not self.unimplemented_vertex_kinds + ) + + def to_json_dict(self) -> dict[str, object]: + return { + "closure_ids": list(self.closure_ids), + "closure_count": len(self.closure_ids), + "full_tensor_network_ready": self.full_tensor_network_ready, + "required_vertex_kind_counts": [ + [kind, count] for kind, count in self.required_vertex_kind_counts + ], + "ready_vertex_kinds": list(self.ready_vertex_kinds), + "pending_vertex_kinds": list(self.pending_vertex_kinds), + "unimplemented_vertex_kinds": list(self.unimplemented_vertex_kinds), + "color_sector_summaries": [ + summary.to_json_dict() for summary in self.color_sector_summaries + ], + } + + +@dataclass(frozen=True) +class GenericStagePlan: + process: CanonicalProcessIR + current_stages: tuple[GenericCurrentStage, ...] + amplitude_stage: GenericAmplitudeStage + + @property + def stage_count(self) -> int: + return len(self.current_stages) + 1 + + @property + def full_tensor_network_ready(self) -> bool: + return self.amplitude_stage.full_tensor_network_ready and all( + stage.full_tensor_network_ready for stage in self.current_stages + ) + + def to_json_dict(self) -> dict[str, object]: + return { + "process": self.process.to_json_dict(), + "stage_count": self.stage_count, + "full_tensor_network_ready": self.full_tensor_network_ready, + "current_stages": [stage.to_json_dict() for stage in self.current_stages], + "amplitude_stage": self.amplitude_stage.to_json_dict(), + } + + +@dataclass(frozen=True) +class GenericCurrentPlan: + """Compatibility view over the production `GenericDAG`. + + This module intentionally no longer owns an independent recursion + implementation. All current identities come from `generic_dag.CurrentIndex`, + so process-support diagnostics and tests inspect the same model-driven DAG + compiler used by schema-v3 artifacts. + """ + + dag: GenericDAG + model: Model + + @property + def process(self) -> CanonicalProcessIR: + return self.dag.process + + @property + def color_plan(self) -> GenericColorPlan: + return self.dag.color_plan + + @property + def currents(self) -> tuple[CurrentNode, ...]: + return self.dag.currents + + @property + def sources(self) -> tuple[int, ...]: + return self.dag.sources + + @property + def interactions(self) -> tuple[InteractionNode, ...]: + return self.dag.interactions + + @property + def closures(self) -> tuple[AmplitudeRoot, ...]: + return self.dag.amplitude_roots + + @property + def truncated(self) -> bool: + return self.dag.truncated + + @property + def color_sectors(self) -> tuple[int, ...]: + root_sectors = contributing_color_sector_ids(self.dag) + if root_sectors: + return root_sectors + return tuple( + sorted({current.index.color_state.sector_id for current in self.currents}) + ) + + @property + def has_closure(self) -> bool: + return bool(self.closures) + + @property + def required_vertex_kind_counts(self) -> tuple[tuple[int, int], ...]: + return _vertex_kind_counts(self.interactions, self.closures) + + @property + def ready_vertex_kinds(self) -> tuple[int, ...]: + return _vertex_kind_status( + (kind for kind, _ in self.required_vertex_kind_counts), + self.model, + )[0] + + @property + def pending_vertex_kinds(self) -> tuple[int, ...]: + return _vertex_kind_status( + (kind for kind, _ in self.required_vertex_kind_counts), + self.model, + )[1] + + @property + def unimplemented_vertex_kinds(self) -> tuple[int, ...]: + return _vertex_kind_status( + (kind for kind, _ in self.required_vertex_kind_counts), + self.model, + )[2] + + @property + def full_tensor_network_ready(self) -> bool: + return ( + self.has_closure + and not self.truncated + and not self.pending_vertex_kinds + and not self.unimplemented_vertex_kinds + ) + + def build_stage_plan(self) -> GenericStagePlan: + return build_generic_stage_plan(self) + + def to_json_dict(self) -> dict[str, object]: + return { + "process": self.process.to_json_dict(), + "color_plan": self.color_plan.to_json_dict(), + "current_count": len(self.currents), + "source_count": len(self.sources), + "interaction_count": len(self.interactions), + "closure_count": len(self.closures), + "truncated": self.truncated, + "has_closure": self.has_closure, + "full_tensor_network_ready": self.full_tensor_network_ready, + "required_vertex_kind_counts": [ + [kind, count] for kind, count in self.required_vertex_kind_counts + ], + "ready_vertex_kinds": list(self.ready_vertex_kinds), + "pending_vertex_kinds": list(self.pending_vertex_kinds), + "unimplemented_vertex_kinds": list(self.unimplemented_vertex_kinds), + "currents": [current.to_json_dict() for current in self.currents], + "sources": list(self.sources), + "interactions": [ + interaction.to_json_dict() for interaction in self.interactions + ], + "closures": [closure.to_json_dict() for closure in self.closures], + "color_sector_summaries": [ + summary.to_json_dict() + for summary in _sector_summaries( + self.currents, + self.interactions, + self.closures, + self.model, + ) + ], + } + + +def build_generic_current_plan( + process: CanonicalProcessIR | GenericDAG, + *, + model: Model, + max_currents: int | None = None, + max_color_sectors: int | None = None, + reference_color_order: tuple[int, ...] | None = None, + selected_color_sector_ids: Iterable[int] | None = None, + max_coupling_orders: Mapping[str, int] | None = None, + max_lc_current_line_groups: int | None = None, + max_quark_pairs: int | None = None, + closure_side_mask_pruning: bool = True, + color_order_mask_pruning: bool = True, + species_reachability_pruning: bool = True, + ignored_particle_ids: Iterable[int] | None = None, + ignored_vertex_kinds: Iterable[int] | None = None, +) -> GenericCurrentPlan: + dag = ( + process + if isinstance(process, GenericDAG) + else GenericDAGCompiler( + model=model, + max_currents=max_currents, + max_color_sectors=max_color_sectors, + reference_color_order=reference_color_order, + selected_color_sector_ids=selected_color_sector_ids, + max_coupling_orders=max_coupling_orders, + max_lc_current_line_groups=max_lc_current_line_groups, + max_quark_pairs=max_quark_pairs, + closure_side_mask_pruning=closure_side_mask_pruning, + color_order_mask_pruning=color_order_mask_pruning, + species_reachability_pruning=species_reachability_pruning, + ignored_particle_ids=ignored_particle_ids, + ignored_vertex_kinds=ignored_vertex_kinds, + ).compile(process) + ) + return GenericCurrentPlan(dag=dag, model=model) + + +def build_generic_stage_plan( + plan: GenericCurrentPlan, +) -> GenericStagePlan: + current_plan = plan + stages: list[GenericCurrentStage] = [] + interactions_by_subset: dict[int, list[InteractionNode]] = {} + for interaction in current_plan.interactions: + result = current_plan.currents[interaction.result_id] + subset_size = len(result.index.external_labels) + interactions_by_subset.setdefault(subset_size, []).append(interaction) + + for stage_index, subset_size in enumerate(sorted(interactions_by_subset)): + interactions = tuple(interactions_by_subset[subset_size]) + current_ids = tuple( + sorted({interaction.result_id for interaction in interactions}) + ) + vertex_counts = _vertex_kind_counts(interactions, ()) + ready, pending, unimplemented = _vertex_kind_status( + (kind for kind, _ in vertex_counts), + current_plan.model, + ) + stages.append( + GenericCurrentStage( + stage_index=stage_index, + subset_size=subset_size, + current_ids=current_ids, + interaction_ids=tuple(interaction.id for interaction in interactions), + required_vertex_kind_counts=vertex_counts, + ready_vertex_kinds=ready, + pending_vertex_kinds=pending, + unimplemented_vertex_kinds=unimplemented, + color_sector_summaries=_sector_summaries( + current_plan.currents, + interactions, + (), + current_plan.model, + ), + ) + ) + + closure_counts = _vertex_kind_counts((), current_plan.closures) + ready, pending, unimplemented = _vertex_kind_status( + (kind for kind, _ in closure_counts), + current_plan.model, + ) + amplitude_stage = GenericAmplitudeStage( + closure_ids=tuple(closure.id for closure in current_plan.closures), + required_vertex_kind_counts=closure_counts, + ready_vertex_kinds=ready, + pending_vertex_kinds=pending, + unimplemented_vertex_kinds=unimplemented, + color_sector_summaries=_sector_summaries( + current_plan.currents, + (), + current_plan.closures, + current_plan.model, + ), + ) + return GenericStagePlan( + process=current_plan.process, + current_stages=tuple(stages), + amplitude_stage=amplitude_stage, + ) + + +def _vertex_kind_counts( + interactions: Iterable[InteractionNode], + closures: Iterable[AmplitudeRoot], +) -> tuple[tuple[int, int], ...]: + counts: Counter[int] = Counter( + interaction.vertex_kind for interaction in interactions + ) + counts.update( + int(closure.vertex_kind) + for closure in closures + if closure.vertex_kind is not None + ) + return tuple(sorted(counts.items())) + + +def _vertex_kind_status( + kinds: Iterable[int], + model: Model, +) -> tuple[tuple[int, ...], tuple[int, ...], tuple[int, ...]]: + ready: set[int] = set() + pending: set[int] = set() + unimplemented: set[int] = set() + for kind in kinds: + rule = model.vertex_lowering_rule(kind) + if rule.backend == "unimplemented": + unimplemented.add(kind) + elif rule.full_tensor_network_ready: + ready.add(kind) + else: + pending.add(kind) + return tuple(sorted(ready)), tuple(sorted(pending)), tuple(sorted(unimplemented)) + + +def _sector_summaries( + currents: tuple[CurrentNode, ...], + interactions: Iterable[InteractionNode], + closures: Iterable[AmplitudeRoot], + model: Model, +) -> tuple[GenericColorSectorUseSummary, ...]: + current_ids_by_sector: dict[int, set[int]] = {} + interactions_by_sector: dict[int, dict[int, InteractionNode]] = {} + closures_by_sector: dict[int, list[AmplitudeRoot]] = {} + interaction_tuple = tuple(interactions) + closure_tuple = tuple(closures) + interactions_by_result: dict[int, list[InteractionNode]] = {} + for interaction in interaction_tuple: + interactions_by_result.setdefault(interaction.result_id, []).append(interaction) + if closure_tuple: + for closure in closure_tuple: + sector = _closure_color_sector(currents, closure) + closures_by_sector.setdefault(sector, []).append(closure) + seen_currents = current_ids_by_sector.setdefault(sector, set()) + sector_interactions = interactions_by_sector.setdefault(sector, {}) + stack = [closure.left_id, closure.right_id] + while stack: + current_id = stack.pop() + if current_id in seen_currents: + continue + seen_currents.add(current_id) + for interaction in interactions_by_result.get(current_id, ()): + sector_interactions.setdefault(interaction.id, interaction) + stack.extend((interaction.left_id, interaction.right_id)) + else: + for current in currents: + current_ids_by_sector.setdefault( + current.index.color_state.sector_id, + set(), + ).add(current.id) + for interaction in interaction_tuple: + sector = currents[interaction.result_id].index.color_state.sector_id + interactions_by_sector.setdefault(sector, {})[interaction.id] = interaction + current_ids_by_sector.setdefault(sector, set()).update( + (interaction.left_id, interaction.right_id, interaction.result_id) + ) + summaries = [] + for sector in sorted( + set(interactions_by_sector) + | set(closures_by_sector) + | set(current_ids_by_sector) + ): + sector_interaction_nodes = tuple( + interactions_by_sector.get(sector, {}).values() + ) + sector_closures = tuple(closures_by_sector.get(sector, ())) + vertex_counts = _vertex_kind_counts( + sector_interaction_nodes, + sector_closures, + ) + ready, pending, unimplemented = _vertex_kind_status( + (kind for kind, _ in vertex_counts), + model, + ) + summaries.append( + GenericColorSectorUseSummary( + color_sector=sector, + current_count=len(current_ids_by_sector.get(sector, ())), + interaction_count=len(sector_interaction_nodes), + closure_count=len(sector_closures), + required_vertex_kind_counts=vertex_counts, + ready_vertex_kinds=ready, + pending_vertex_kinds=pending, + unimplemented_vertex_kinds=unimplemented, + ) + ) + return tuple(summaries) + + +def _closure_color_sector( + currents: tuple[CurrentNode, ...], + closure: AmplitudeRoot, +) -> int: + if closure.color_sector_id is not None: + return int(closure.color_sector_id) + return int(currents[closure.left_id].index.color_state.sector_id) + + +__all__ = [ + "GenericAmplitudeStage", + "GenericClosureNode", + "GenericColorSectorUseSummary", + "GenericCurrentNode", + "GenericCurrentPlan", + "GenericCurrentStage", + "GenericInteractionNode", + "GenericStagePlan", + "build_generic_current_plan", + "build_generic_stage_plan", +] diff --git a/pyamplicol/src/pyamplicol/generation/dag_algorithms.py b/pyamplicol/src/pyamplicol/generation/dag_algorithms.py new file mode 100644 index 0000000000..dcf526d0aa --- /dev/null +++ b/pyamplicol/src/pyamplicol/generation/dag_algorithms.py @@ -0,0 +1,1883 @@ +# SPDX-License-Identifier: 0BSD +"""Post-processing and selection transforms for generic DAGs.""" + +from __future__ import annotations + +from collections import deque +from collections.abc import Iterable, Mapping, Sequence +from dataclasses import dataclass, field, replace +from itertools import pairwise +from typing import cast + +from ..color.plan import GenericColorPlan, build_color_plan +from ..color.plan_types import _canonical_open_string_product_key +from ..models._physics_ir import ContractionIR +from ..models.base import ( + CouplingOrders, + Model, + QuantumFlow, + QuantumNumberFlow, + Vertex, +) +from ..processes.ir import CanonicalProcessIR +from .dag_color import ColorEngine +from .dag_ordering import ( + _closure_candidate_splits, + _closure_side_reachable_masks, + _complex_weight_mul, + _direct_contraction_ir, + _labels_mask, + _lc_color_order_reachable_masks, + _mask_labels, +) +from .dag_reachability import ( + UsefulStateMap, + _closure_total_coupling_orders, + _combine_coupling_order_tuples, + _coupling_order_degree, + _coupling_order_envelope, + _coupling_orders_within_limits, + _lc_line_groups_within_limit, + _mask_allowed_by_reachability, + _masks_by_size, + _normalize_coupling_order_limits, + _ordered_splits, + _right_particles_by_left, + _state_allowed_by_reachability, +) +from .dag_types import ( + AmplitudeRoot, + ColorFlow, + ColorState, + CurrentIndex, + CurrentNode, + GenericDAG, + InteractionNode, +) +from .helicity_replay import _semantic_amplitude_root_alias_signatures + + +def _normalize_generation_cap(value: int | None) -> int | None: + if value is None: + return None + normalized = int(value) + return None if normalized < 0 else normalized + + +@dataclass(frozen=True, slots=True) +class _LiveCurrentShape: + external_mask: int + external_labels: tuple[int, ...] + particle_id: int + ordered_external_labels: tuple[int, ...] + color_state: ColorState + coupling_orders: CouplingOrders + chirality: int + spin_state: int | tuple[int, ...] + flavour_flow: tuple[int, ...] + quantum_number_flow: QuantumNumberFlow + _hash: int = field(init=False, repr=False, compare=False) + + def __post_init__(self) -> None: + object.__setattr__( + self, + "_hash", + hash( + ( + self.external_mask, + self.external_labels, + self.particle_id, + self.ordered_external_labels, + self.color_state, + self.coupling_orders, + self.chirality, + self.spin_state, + self.flavour_flow, + self.quantum_number_flow, + ) + ), + ) + + def __hash__(self) -> int: + return self._hash + + +@dataclass(frozen=True, slots=True) +class BackwardLiveTransition: + """One model-certified abstract current invocation retained by liveness.""" + + left: _LiveCurrentShape + right: _LiveCurrentShape + result: _LiveCurrentShape + vertex_kind: int + vertex_particles: tuple[int, int, int] + coupling: tuple[float, float] + color_weight: tuple[float, float] + + +@dataclass(frozen=True, slots=True) +class BackwardLiveClosure: + """One model-certified abstract amplitude closure.""" + + left: _LiveCurrentShape + right: _LiveCurrentShape + kind: str + color_weight: tuple[float, float] + contraction_ir: ContractionIR + color_sector_id: int | None + vertex_kind: int | None = None + vertex_particles: tuple[int, int, int] | None = None + coupling: tuple[float, float] = (1.0, 0.0) + + +@dataclass(frozen=True, slots=True) +class BackwardLiveSource: + """One retained source with its original ancestry-bit allocation.""" + + shape: _LiveCurrentShape + helicity_ancestry: int + leg_label: int + source_helicity: int + + +@dataclass(frozen=True, slots=True) +class BackwardLiveStatePlan: + """Conservative exact-colour states that can feed a physical closure. + + The planner tracks model quantum flow but intentionally omits helicity + ancestry and tensor payloads. A retained shape can therefore still produce + more than one exact current, while a valid exact current cannot be removed + solely because of those omitted fields. This makes the plan suitable as an + eager-only construction filter without introducing a second physics + implementation. + """ + + shapes: frozenset[_LiveCurrentShape] + active_sector_ids: frozenset[int] + transitions: tuple[BackwardLiveTransition, ...] + closures: tuple[BackwardLiveClosure, ...] + sources: tuple[BackwardLiveSource, ...] + + def allows(self, index: CurrentIndex) -> bool: + return _live_current_shape(index) in self.shapes + + +def _live_current_shape(index: CurrentIndex) -> _LiveCurrentShape: + return _LiveCurrentShape( + external_mask=int(index.external_mask), + external_labels=tuple(index.external_labels), + particle_id=int(index.particle_id), + ordered_external_labels=tuple(index.ordered_external_labels), + color_state=index.color_state, + coupling_orders=tuple(index.coupling_orders), + chirality=int(index.chirality), + spin_state=index.spin_state, + flavour_flow=tuple(index.flavour_flow), + quantum_number_flow=index.quantum_number_flow, + ) + + +def build_backward_live_state_plan( + process: CanonicalProcessIR, + *, + model: Model, + color_engine: ColorEngine, + closure_candidate_splits: Sequence[tuple[int, int]], + closure_reachable_masks: frozenset[int] | None, + color_order_reachable_masks: frozenset[int] | None, + useful_states_by_mask: UsefulStateMap | None, + max_coupling_orders: Mapping[str, int], + max_lc_current_line_groups: int | None, + ignored_particle_ids: frozenset[int], + ignored_vertex_kinds: frozenset[int], + selected_source_helicities: Mapping[int, int] | None, + global_flip_anchor: tuple[int, int] | None, +) -> BackwardLiveStatePlan | None: + """Build an eager-only demand plan from physical amplitude sinks. + + Multi-line colour plans may enumerate many traversal sectors although the + recursion uses one physical sink. The forward compiler previously built + every helicity and quantum-flow current in all of them before discovering + at closure that most sectors were dead. This prepass first keeps only + physical words ending at a configured sink, then performs a lightweight + model-driven recursion over particle species, coupling orders, ordered + labels, and exact colour states. A final backward walk retains only shapes + feeding a model-supported direct or vertex closure. + + Shared-ordering and single-trace plans already collapse sectors before the + current sweep. They deliberately stay on the existing path because this + planner would not remove work there. + """ + + color_plan = color_engine.color_plan + if ( + color_plan.color_accuracy == "lc" + or color_engine.shared_lc_orderings + or color_engine.shared_single_trace + or len(color_plan.sectors) <= 1 + ): + return None + + active_sector_ids = _physical_sink_sector_ids( + color_plan, + closure_candidate_splits, + ) + if active_sector_ids is None: + return None + + full_mask = _labels_mask(leg.label for leg in process.legs) + possible_by_mask: dict[int, list[_LiveCurrentShape]] = {} + possible_seen: set[_LiveCurrentShape] = set() + reverse_transitions: dict[ + _LiveCurrentShape, + set[tuple[_LiveCurrentShape, _LiveCurrentShape]], + ] = {} + transitions: list[BackwardLiveTransition] = [] + + def state_allowed(shape: _LiveCurrentShape) -> bool: + if useful_states_by_mask is None: + return True + return _state_allowed_by_reachability( + useful_states_by_mask, + shape.external_mask, + shape.particle_id, + shape.coupling_orders, + ) + + def add_shape(shape: _LiveCurrentShape) -> bool: + if shape in possible_seen or not state_allowed(shape): + return False + possible_seen.add(shape) + possible_by_mask.setdefault(shape.external_mask, []).append(shape) + return True + + source_candidates: list[BackwardLiveSource] = [] + next_source_bit = 0 + for leg in process.legs: + if leg.outgoing_pdg is None: + continue + particle_id = int(leg.outgoing_pdg) + if particle_id in ignored_particle_ids: + continue + mask = 1 << (leg.label - 1) + source_ir = model._source_ir(particle_id) + source_states = tuple( + source_ir.crossing.apply(state) if leg.is_initial else state + for state in source_ir.states + ) + source_quantum_flow = model.quantum_number_flow(particle_id) + for color_state in color_engine.source_states_for_leg(leg): + if not _lc_line_groups_within_limit( + color_state, + max_lc_current_line_groups, + ): + continue + color_state_is_live = color_state.sector_id in active_sector_ids + for source_state in source_states: + source_helicity = int(source_state.helicity) + if ( + global_flip_anchor is not None + and leg.label == global_flip_anchor[0] + and source_helicity != global_flip_anchor[1] + ): + next_source_bit += 1 + continue + requested_helicity = (selected_source_helicities or {}).get( + int(leg.label) + ) + if requested_helicity is not None and source_helicity != int( + requested_helicity + ): + continue + helicity_ancestry = 1 << next_source_bit + next_source_bit += 1 + if not color_state_is_live: + continue + source_shape = _LiveCurrentShape( + external_mask=mask, + external_labels=(int(leg.label),), + particle_id=particle_id, + ordered_external_labels=(int(leg.label),), + color_state=color_state, + coupling_orders=(), + chirality=int(source_state.chirality), + spin_state=source_state.spin_state, + flavour_flow=(particle_id,), + quantum_number_flow=source_quantum_flow, + ) + add_shape(source_shape) + if source_shape in possible_seen: + source_candidates.append( + BackwardLiveSource( + shape=source_shape, + helicity_ancestry=helicity_ancestry, + leg_label=int(leg.label), + source_helicity=source_helicity, + ) + ) + + right_particles_by_left = _right_particles_by_left( + model, + color_accuracy=process.color_accuracy, + ) + vertices_by_input: dict[tuple[int, int], tuple[Vertex, ...]] = {} + vertex_allowed_cache: dict[Vertex, bool] = {} + duplicate_orientation_cache: dict[Vertex, bool] = {} + coupling_orders_cache: dict[Vertex, CouplingOrders] = {} + vertex_color_weight_cache: dict[Vertex, tuple[float, float]] = {} + combined_orders_cache: dict[ + tuple[CouplingOrders, CouplingOrders, Vertex], + CouplingOrders, + ] = {} + orders_allowed_cache: dict[CouplingOrders, bool] = {} + quantum_flow_cache: dict[tuple[object, ...], tuple[QuantumFlow, ...]] = {} + color_flow_cache: dict[tuple[object, ...], tuple[ColorFlow, ...]] = {} + for mask in _masks_by_size(full_mask): + if mask & (mask - 1) == 0 or mask == full_mask: + continue + if not _mask_allowed_by_reachability( + mask, + closure_reachable_masks, + color_order_reachable_masks, + ): + continue + if useful_states_by_mask is not None and mask not in useful_states_by_mask: + continue + labels = _mask_labels(mask) + for left_mask, right_mask in _ordered_splits(mask): + left_shapes = possible_by_mask.get(left_mask) + right_shapes = possible_by_mask.get(right_mask) + if not left_shapes or not right_shapes: + continue + right_by_sector_particle: dict[ + tuple[int, int], list[_LiveCurrentShape] + ] = {} + for right_shape in right_shapes: + right_by_sector_particle.setdefault( + ( + right_shape.color_state.sector_id, + right_shape.particle_id, + ), + [], + ).append(right_shape) + for left_shape in tuple(left_shapes): + left_index = cast(CurrentIndex, left_shape) + for right_particle in right_particles_by_left.get( + left_shape.particle_id, + (), + ): + for right_shape in right_by_sector_particle.get( + (left_shape.color_state.sector_id, right_particle), + (), + ): + right_index = cast(CurrentIndex, right_shape) + input_key = ( + left_shape.particle_id, + right_shape.particle_id, + ) + vertices = vertices_by_input.get(input_key) + if vertices is None: + vertices = model.vertices_accepting( + *input_key, + color_accuracy=process.color_accuracy, + ) + vertices_by_input[input_key] = vertices + for vertex in vertices: + if ( + vertex.kind in ignored_vertex_kinds + or vertex.particles[2] in ignored_particle_ids + ): + continue + vertex_allowed = vertex_allowed_cache.get(vertex) + if vertex_allowed is None: + vertex_allowed = color_engine.vertex_allowed(vertex) + vertex_allowed_cache[vertex] = vertex_allowed + if not vertex_allowed: + continue + duplicate = duplicate_orientation_cache.get(vertex) + if duplicate is None: + duplicate = model.skip_duplicate_vertex_orientation( + vertex + ) + duplicate_orientation_cache[vertex] = duplicate + if duplicate: + continue + vertex_orders = coupling_orders_cache.get(vertex) + if vertex_orders is None: + vertex_orders = model.vertex_coupling_orders(vertex) + coupling_orders_cache[vertex] = vertex_orders + orders_key = ( + left_shape.coupling_orders, + right_shape.coupling_orders, + vertex, + ) + orders = combined_orders_cache.get(orders_key) + if orders is None: + orders = _combine_coupling_order_tuples( + left_shape.coupling_orders, + right_shape.coupling_orders, + vertex_orders, + ) + combined_orders_cache[orders_key] = orders + orders_allowed = orders_allowed_cache.get(orders) + if orders_allowed is None: + orders_allowed = _coupling_orders_within_limits( + orders, + max_coupling_orders, + ) + orders_allowed_cache[orders] = orders_allowed + if not orders_allowed: + continue + ordered_labels = color_engine.ordered_combination_labels( + left_index, + right_index, + vertex, + ) + if ordered_labels is None: + continue + quantum_key = ( + vertex.kind, + vertex.particles, + left_shape.particle_id, + left_shape.chirality, + left_shape.spin_state, + left_shape.flavour_flow, + left_shape.quantum_number_flow, + right_shape.particle_id, + right_shape.chirality, + right_shape.spin_state, + right_shape.flavour_flow, + right_shape.quantum_number_flow, + ) + quantum_flows = quantum_flow_cache.get(quantum_key) + if quantum_flows is None: + quantum_flows = tuple( + model.allowed_quantum_flows( + vertex, + left_index, + right_index, + ) + ) + quantum_flow_cache[quantum_key] = quantum_flows + if not quantum_flows: + continue + color_key = ( + left_shape.color_state, + right_shape.color_state, + vertex, + ordered_labels, + ) + color_flows = color_flow_cache.get(color_key) + if color_flows is None: + color_flows = tuple( + color_engine.combine( + left_shape.color_state, + right_shape.color_state, + vertex, + ordered_external_labels=ordered_labels, + ) + ) + color_flow_cache[color_key] = color_flows + for color_flow in color_flows: + if not _lc_line_groups_within_limit( + color_flow.state, + max_lc_current_line_groups, + ): + continue + for quantum_flow in quantum_flows: + result_shape = _LiveCurrentShape( + external_mask=mask, + external_labels=labels, + particle_id=int(vertex.particles[2]), + ordered_external_labels=tuple(ordered_labels), + color_state=color_flow.state, + coupling_orders=orders, + chirality=int(quantum_flow.chirality), + spin_state=quantum_flow.spin_state, + flavour_flow=tuple(quantum_flow.flavour_flow), + quantum_number_flow=( + quantum_flow.quantum_number_flow + ), + ) + if not model.current_allowed(result_shape): + continue + add_shape(result_shape) + reverse_transitions.setdefault( + result_shape, + set(), + ).add((left_shape, right_shape)) + vertex_color_weight = vertex_color_weight_cache.get( + vertex + ) + if vertex_color_weight is None: + vertex_color_weight = model.vertex_color_weight( + vertex, + color_accuracy=process.color_accuracy, + ) + vertex_color_weight_cache[vertex] = ( + vertex_color_weight + ) + transitions.append( + BackwardLiveTransition( + left=left_shape, + right=right_shape, + result=result_shape, + vertex_kind=int(vertex.kind), + vertex_particles=tuple(vertex.particles), + coupling=tuple(quantum_flow.coupling), + color_weight=_complex_weight_mul( + color_flow.weight, + vertex_color_weight, + ), + ) + ) + + closures: list[BackwardLiveClosure] = [] + for left_mask, right_mask in closure_candidate_splits: + left_shapes = possible_by_mask.get(left_mask, ()) + right_shapes = possible_by_mask.get(right_mask, ()) + if not left_shapes or not right_shapes: + continue + right_by_sector: dict[int, list[_LiveCurrentShape]] = {} + for right_shape in right_shapes: + right_by_sector.setdefault( + right_shape.color_state.sector_id, + [], + ).append(right_shape) + for left_shape in left_shapes: + left_index = cast(CurrentIndex, left_shape) + for right_shape in right_by_sector.get( + left_shape.color_state.sector_id, + (), + ): + right_index = cast(CurrentIndex, right_shape) + if not color_engine.ordered_closure_allowed(left_index, right_index): + continue + color_flows = color_engine.closure_compatible( + left_shape.color_state, + right_shape.color_state, + full_mask=full_mask, + ) + if not color_flows: + continue + direct_contraction_ir = _direct_contraction_ir( + model, + left_index, + right_index, + ) + pair_closures: list[BackwardLiveClosure] = [] + for color_flow in color_flows: + if direct_contraction_ir is not None: + pair_closures.append( + BackwardLiveClosure( + left=left_shape, + right=right_shape, + kind="direct-contraction", + color_weight=color_flow.weight, + contraction_ir=direct_contraction_ir, + color_sector_id=color_flow.state.sector_id, + ) + ) + for vertex in model.vertices_accepting( + left_shape.particle_id, + right_shape.particle_id, + color_accuracy=process.color_accuracy, + ): + if ( + vertex.kind in ignored_vertex_kinds + or vertex.particles[2] in ignored_particle_ids + or not color_engine.vertex_allowed(vertex) + or not model.vertex_closure_allowed(vertex) + ): + continue + closure_contraction_ir = model.closure_contraction_ir( + vertex.particles[2] + ) + if closure_contraction_ir is None: + continue + total_orders = model.combine_coupling_orders( + left_index, + right_index, + vertex, + ) + if not _coupling_orders_within_limits( + total_orders, + max_coupling_orders, + ) or not model.allowed_quantum_flows( + vertex, + left_index, + right_index, + ): + continue + pair_closures.append( + BackwardLiveClosure( + left=left_shape, + right=right_shape, + kind="vertex-closure", + color_weight=_complex_weight_mul( + color_flow.weight, + model.vertex_color_weight( + vertex, + color_accuracy=process.color_accuracy, + ), + ), + contraction_ir=closure_contraction_ir, + color_sector_id=color_flow.state.sector_id, + vertex_kind=vertex.kind, + vertex_particles=vertex.particles, + coupling=vertex.coupling, + ) + ) + if pair_closures: + closures.extend(pair_closures) + + # Keep the union of every model-valid closure traversal. Whole-sector + # ownership is not sound for three or more open fermion lines: different + # block traversals can expose distinct closure terms (and identical-line + # exchange signs). Exact semantic alias removal happens only after the + # concrete DAG exists and can be certified by the shared replay proof. + useful: set[_LiveCurrentShape] = { + shape for closure in closures for shape in (closure.left, closure.right) + } + + pending = deque(useful) + while pending: + result_shape = pending.popleft() + for left_shape, right_shape in reverse_transitions.get(result_shape, ()): + for parent in (left_shape, right_shape): + if parent in useful: + continue + useful.add(parent) + pending.append(parent) + + if not useful: + return None + return BackwardLiveStatePlan( + shapes=frozenset(useful), + active_sector_ids=frozenset(shape.color_state.sector_id for shape in useful), + transitions=tuple( + transition + for transition in transitions + if transition.result in useful + and transition.left in useful + and transition.right in useful + ), + closures=tuple(closures), + sources=tuple(source for source in source_candidates if source.shape in useful), + ) + + +def _physical_sink_sector_ids( + color_plan: GenericColorPlan, + closure_candidate_splits: Sequence[tuple[int, int]], +) -> frozenset[int] | None: + """Return sectors whose canonical traversal ends at a closure sink. + + Compatibility traversals exist only to build intermediate currents. The + sector's authenticated canonical traversal owns the private recursion sink. + Fail closed when a sector has no canonical sink or when a closure side is + not a singleton. + """ + + sink_labels: set[int] = set() + for _left_mask, right_mask in closure_candidate_splits: + labels = _mask_labels(right_mask) + if len(labels) != 1: + return None + sink_labels.add(labels[0]) + if not sink_labels: + return None + + active: set[int] = set() + for sector in color_plan.sectors: + sink_label = sector.canonical_closure_sink_label(color_plan.process) + if sink_label is None: + return None + if int(sink_label) in sink_labels: + active.add(int(sector.id)) + if not active: + return None + return frozenset(active) + + +def _canonical_open_line_alias_owner_sector_ids( + color_plan: GenericColorPlan, + active_sector_ids: Iterable[int], +) -> frozenset[int]: + """Choose one root-bearing owner per complete-open-line tensor. + + NLC and full-colour plans enumerate permutations of complete open-line + blocks as public traversal sectors. Those permutations are aliases of + the same product of colour tensors and must not each contribute a coherent + amplitude. Choose the smallest *active* sector in each alias class: for + three or more open lines the smallest public sector can be rootless, so an + owner chosen before physical closure would incorrectly erase an amplitude. + """ + + active = frozenset(int(sector_id) for sector_id in active_sector_ids) + sector_by_id = {int(sector.id): sector for sector in color_plan.sectors} + unknown = active.difference(sector_by_id) + if unknown: + raise ValueError( + "active colour sectors are absent from the color plan: " + + ", ".join(str(sector_id) for sector_id in sorted(unknown)) + ) + if color_plan.color_accuracy not in {"nlc", "full"}: + return active + + owner_by_key: dict[tuple[object, ...], int] = {} + owners: set[int] = set() + for sector_id in sorted(active): + sector = sector_by_id[sector_id] + if sector.kind == "open-lines": + open_strings = _canonical_open_string_product_key( + ( + line.fundamental_label, + line.adjoint_labels, + line.antifundamental_label, + line.singlet_labels, + ) + for line in sector.open_color_lines + ) + key: tuple[object, ...] = ("open-lines", open_strings) + else: + key = (sector.kind, sector_id) + owners.add(owner_by_key.setdefault(key, sector_id)) + return frozenset(owners) + + +def _canonical_open_line_alias_owner_sector_map( + color_plan: GenericColorPlan, +) -> dict[int, int]: + """Map traversal sectors onto canonical complete-open-line tensors.""" + + if color_plan.color_accuracy not in {"nlc", "full"}: + return {int(sector.id): int(sector.id) for sector in color_plan.sectors} + sectors_by_key: dict[tuple[object, ...], list[int]] = {} + for sector in color_plan.sectors: + sector_id = int(sector.id) + if sector.kind == "open-lines": + open_strings = _canonical_open_string_product_key( + ( + line.fundamental_label, + line.adjoint_labels, + line.antifundamental_label, + line.singlet_labels, + ) + for line in sector.open_color_lines + ) + key: tuple[object, ...] = ("open-lines", open_strings) + else: + key = (sector.kind, sector_id) + sectors_by_key.setdefault(key, []).append(sector_id) + return { + sector_id: min(sector_ids) + for sector_ids in sectors_by_key.values() + for sector_id in sector_ids + } + + +def _retain_canonical_open_line_alias_roots( + dag: GenericDAG, + model: Model, +) -> GenericDAG: + """Collapse only model-certified traversal aliases of amplitude roots. + + Public NLC/full plans retain every colour sector. Complete open-line block + permutations are private construction aliases, but a sector is not an + indivisible ownership unit: with three or more open lines, separate + traversals carry separate physical closure terms. The shared helicity + replay signature proves equality of the full recursive closure while a + sector projection removes only that traversal identifier. Distinct source + labels, ordered traversals, fermion exchange factors and kernel contracts + therefore remain distinct. + """ + + roots = dag.amplitude_roots + color_plan = dag.color_plan + if not roots or color_plan.color_accuracy not in {"nlc", "full"}: + return dag + + owner_by_sector = _canonical_open_line_alias_owner_sector_map(color_plan) + aliases_by_owner: dict[int, set[int]] = {} + for sector_id, owner_id in owner_by_sector.items(): + aliases_by_owner.setdefault(owner_id, set()).add(sector_id) + if all(len(sector_ids) == 1 for sector_ids in aliases_by_owner.values()): + return dag + + signatures = _semantic_amplitude_root_alias_signatures( + dag, + model, + color_sector_aliases=owner_by_sector, + ) + grouped: dict[ + tuple[ + int, + str, + tuple[tuple[tuple[int, int], ...], ...], + tuple[float, float], + ], + dict[int, list[AmplitudeRoot]], + ] = {} + untouched: list[AmplitudeRoot] = [] + for root, signature in zip(roots, signatures, strict=True): + if root.color_sector_id is None: + untouched.append(root) + continue + sector_id = int(root.color_sector_id) + owner_id = owner_by_sector.get(sector_id, sector_id) + if len(aliases_by_owner.get(owner_id, {sector_id})) == 1: + untouched.append(root) + continue + if signature is None: + raise ValueError( + "open-line traversal alias has no model-certified recursive " + f"closure signature: root {root.id}, sector {sector_id}" + ) + key = ( + owner_id, + signature.proof_digest, + signature.selector_states, + signature.factor, + ) + grouped.setdefault(key, {}).setdefault(sector_id, []).append(root) + + retained = list(untouched) + for key in sorted(grouped): + roots_by_sector = grouped[key] + multiplicities = { + sector_id: len(sector_roots) + for sector_id, sector_roots in roots_by_sector.items() + } + if len(set(multiplicities.values())) != 1: + detail = ", ".join( + f"sector {sector_id}: {count}" + for sector_id, count in sorted(multiplicities.items()) + ) + raise ValueError( + "proof-identical open-line traversal aliases have unequal " + f"root multiplicities for owner {key[0]} ({detail})" + ) + representative_sector = min(roots_by_sector) + owner_id = key[0] + retained.extend( + replace(root, color_sector_id=owner_id) + for root in roots_by_sector[representative_sector] + ) + retained.sort(key=lambda root: root.id) + rebound = tuple( + replace(root, id=index) if root.id != index else root + for index, root in enumerate(retained) + ) + return dag if rebound == roots else replace(dag, amplitude_roots=rebound) + + +def infer_minimal_coupling_order_limits( + process: CanonicalProcessIR, + *, + model: Model, + max_color_sectors: int | None = None, + selected_color_sector_ids: Iterable[int] | None = None, + max_coupling_orders: Mapping[str, int] | None = None, + closure_side_mask_pruning: bool = True, + color_order_mask_pruning: bool = True, + ignored_particle_ids: Iterable[int] | None = None, + ignored_vertex_kinds: Iterable[int] | None = None, +) -> dict[str, int]: + """Infer a generic lowest-order coupling envelope for a process. + + This is an opt-in generation accelerator. It never recognizes a whole + process family; it asks the model which local vertices can connect the + external states, tracks UFO-style coupling orders, and returns the + component-wise maximum over all closure paths with the lowest + model-declared hierarchy-weighted order. The returned dictionary can be + used as ordinary ``max_coupling_orders``. Model-declared orders absent from + the minimal envelope are returned with a zero limit because an omitted + limit means unrestricted to the DAG compiler. + """ + + if not isinstance(process, CanonicalProcessIR): + raise TypeError( + "coupling-order inference requires a model-resolved CanonicalProcessIR" + ) + process_ir = process + color_plan = build_color_plan( + process_ir, + color_accuracy=process_ir.color_accuracy, + max_sectors=max_color_sectors, + fold_trace_reflections=( + model.lc_trace_reflection_equivalence_is_proven(process_ir) + ), + ) + return _infer_minimal_coupling_order_limits_from_color_plan( + process_ir, + model=model, + color_plan=color_plan, + selected_color_sector_ids=selected_color_sector_ids, + max_coupling_orders=max_coupling_orders, + closure_side_mask_pruning=closure_side_mask_pruning, + color_order_mask_pruning=color_order_mask_pruning, + ignored_particle_ids=ignored_particle_ids, + ignored_vertex_kinds=ignored_vertex_kinds, + ) + + +def _infer_minimal_coupling_order_limits_from_color_plan( + process: CanonicalProcessIR, + *, + model: Model, + color_plan: GenericColorPlan, + selected_color_sector_ids: Iterable[int] | None = None, + max_coupling_orders: Mapping[str, int] | None = None, + closure_side_mask_pruning: bool = True, + color_order_mask_pruning: bool = True, + ignored_particle_ids: Iterable[int] | None = None, + ignored_vertex_kinds: Iterable[int] | None = None, +) -> dict[str, int]: + """Infer the minimal coupling envelope from an already complete color plan.""" + + if not isinstance(process, CanonicalProcessIR): + raise TypeError( + "coupling-order inference requires a model-resolved CanonicalProcessIR" + ) + process_ir = process + if color_plan.process is not process_ir and color_plan.process != process_ir: + raise ValueError( + "coupling-order inference color plan belongs to another process" + ) + explicit_sector_ids = ( + None + if selected_color_sector_ids is None + else frozenset(int(sector_id) for sector_id in selected_color_sector_ids) + ) + if explicit_sector_ids is not None and color_plan.color_accuracy == "lc": + color_plan = GenericColorPlan( + process=color_plan.process, + color_accuracy=color_plan.color_accuracy, + sectors=tuple( + sector + for sector in color_plan.sectors + if sector.id in explicit_sector_ids + ), + diagnostics=color_plan.diagnostics, + truncated=color_plan.truncated, + trace_reflections_folded=color_plan.trace_reflections_folded, + basis=color_plan.basis, + ) + totals = _closure_totals_from_color_plan( + process_ir, + model=model, + color_plan=color_plan, + max_coupling_orders=max_coupling_orders, + closure_side_mask_pruning=closure_side_mask_pruning, + color_order_mask_pruning=color_order_mask_pruning, + ignored_particle_ids=ignored_particle_ids, + ignored_vertex_kinds=ignored_vertex_kinds, + ) + if not totals: + return {} + hierarchies = { + str(name).upper(): max(1, int(value)) + for name, value in model.coupling_order_hierarchies().items() + } + minimum_total_order = min( + _coupling_order_degree(total, hierarchies=hierarchies) for total in totals + ) + minimal_totals = tuple( + total + for total in totals + if _coupling_order_degree(total, hierarchies=hierarchies) == minimum_total_order + ) + envelope = _coupling_order_envelope(minimal_totals) + order_names = set(hierarchies) | set(envelope) + for total in totals: + order_names.update(name for name, _value in total) + return {name: envelope.get(name, 0) for name in sorted(order_names)} + + +def _closure_totals_from_color_plan( + process_ir: CanonicalProcessIR, + *, + model: Model, + color_plan: GenericColorPlan, + max_coupling_orders: Mapping[str, int] | None = None, + closure_side_mask_pruning: bool = True, + color_order_mask_pruning: bool = True, + ignored_particle_ids: Iterable[int] | None = None, + ignored_vertex_kinds: Iterable[int] | None = None, +) -> tuple[CouplingOrders, ...]: + """Coupling-order totals of the closures reachable within the limits.""" + + color_engine = ColorEngine(color_plan, model) + full_mask = _labels_mask(leg.label for leg in process_ir.legs) + closure_candidate_splits = _closure_candidate_splits( + process_ir, + model, + color_engine, + ) + closure_reachable_masks = ( + _closure_side_reachable_masks(full_mask, closure_candidate_splits) + if closure_side_mask_pruning + else None + ) + color_order_reachable_masks = ( + _lc_color_order_reachable_masks(process_ir, color_plan, model) + if color_order_mask_pruning + else None + ) + return tuple( + _closure_total_coupling_orders( + process_ir, + model, + color_engine, + closure_candidate_splits, + closure_reachable_masks, + color_order_reachable_masks, + max_coupling_orders=_normalize_coupling_order_limits(max_coupling_orders), + ignored_particle_ids=frozenset( + int(particle_id) for particle_id in (ignored_particle_ids or ()) + ), + ignored_vertex_kinds=frozenset( + int(kind) for kind in (ignored_vertex_kinds or ()) + ), + ) + ) + + +def has_model_supported_amplitudes( + process: CanonicalProcessIR, + *, + model: Model, + color_plan: GenericColorPlan, + max_coupling_orders: Mapping[str, int] | None = None, +) -> bool: + """Whether any tree-level closure of the process exists in the model. + + Species, colour and coupling-order reachability only; it may overestimate, + like the DAG pruning it shares, but never rejects a process with amplitudes. + """ + + return bool( + _closure_totals_from_color_plan( + process, + model=model, + color_plan=color_plan, + max_coupling_orders=max_coupling_orders, + ) + ) + + +def prune_dag_to_amplitude_roots(dag: GenericDAG) -> GenericDAG: + """Drop currents and interactions that cannot feed any amplitude root. + + The forward generic sweep intentionally over-generates valid local currents: + it does not know which of them will survive closure until the full table is + built. Production artifacts should mirror AmpliCol's optimized library + structure and keep only the backward-reachable sub-DAG feeding retained + amplitude roots. + """ + + if not dag.amplitude_roots: + return dag + + required_currents = bytearray(len(dag.currents)) + for root in dag.amplitude_roots: + for current_id in (root.left_id, root.right_id): + required_currents[current_id] = 1 + + required_interactions = bytearray(len(dag.interactions)) + # Interactions are emitted in increasing result-subset order. Walking that + # table backwards visits every consumer before the interactions producing + # its parents and computes the complete live closure in one pass. + for interaction in reversed(dag.interactions): + if required_currents[interaction.result_id]: + required_interactions[interaction.id] = 1 + required_currents[interaction.left_id] = 1 + required_currents[interaction.right_id] = 1 + + if all(required_currents) and all(required_interactions): + return dag + + current_id_map = [-1] * len(dag.currents) + next_current_id = 0 + for old_id, required in enumerate(required_currents): + if required: + current_id_map[old_id] = next_current_id + next_current_id += 1 + pruned_currents = tuple( + CurrentNode( + id=current_id_map[current.id], + index=current.index, + dimension=current.dimension, + is_source=current.is_source, + source_leg_label=current.source_leg_label, + source_helicity=current.source_helicity, + ) + for current in dag.currents + if required_currents[current.id] + ) + interaction_id_map = [-1] * len(dag.interactions) + next_interaction_id = 0 + for old_id, required in enumerate(required_interactions): + if required: + interaction_id_map[old_id] = next_interaction_id + next_interaction_id += 1 + pruned_interactions = tuple( + InteractionNode( + id=interaction_id_map[interaction.id], + vertex_kind=interaction.vertex_kind, + vertex_particles=interaction.vertex_particles, + left_id=current_id_map[interaction.left_id], + right_id=current_id_map[interaction.right_id], + result_id=current_id_map[interaction.result_id], + coupling=interaction.coupling, + color_weight=interaction.color_weight, + lowering_backend=interaction.lowering_backend, + full_tensor_network_ready=interaction.full_tensor_network_ready, + evaluation_group_id=interaction.evaluation_group_id, + evaluation_factor=interaction.evaluation_factor, + ) + for interaction in dag.interactions + if required_interactions[interaction.id] + ) + pruned_roots = tuple( + AmplitudeRoot( + id=new_id, + kind=root.kind, + left_id=current_id_map[root.left_id], + right_id=current_id_map[root.right_id], + color_weight=root.color_weight, + contraction_ir=root.contraction_ir, + color_sector_id=root.color_sector_id, + vertex_kind=root.vertex_kind, + vertex_particles=root.vertex_particles, + coupling=root.coupling, + helicity_weight=root.helicity_weight, + ) + for new_id, root in enumerate(dag.amplitude_roots) + ) + pruned_sources = tuple( + current_id_map[source_id] + for source_id in dag.sources + if required_currents[source_id] + ) + return GenericDAG( + process=dag.process, + color_plan=dag.color_plan, + currents=pruned_currents, + sources=pruned_sources, + interactions=pruned_interactions, + amplitude_roots=pruned_roots, + truncated=dag.truncated, + helicity_coverage=dag.helicity_coverage, + color_coverage=dag.color_coverage, + selected_source_helicities=dag.selected_source_helicities, + selected_color_sector_ids=dag.selected_color_sector_ids, + lc_topology_replay=dag.lc_topology_replay, + color_topology_replay=dag.color_topology_replay, + ) + + +def prune_global_helicity_flip_equivalent_roots( + dag: GenericDAG, + model: Model, +) -> GenericDAG: + """Group roots when the model proves a global-helicity-flip identity. + + This is the safe, structural subset of AmpliCol's numerical helicity + filtering. Keeping one proven representative with doubled helicity weight + reduces amplitude roots, after which dead-tree pruning removes currents + that fed only the discarded partner roots. + """ + + parity_safe, vertex_inventory = _global_helicity_flip_equivalence_proof( + dag, + model, + ) + if not parity_safe: + return dag + if not dag.amplitude_roots: + return dag + + pure_massless_adjoint = _pure_massless_adjoint_helicity_pruning_safe( + dag, + model, + vertex_inventory, + ) + initial_leg_labels = { + int(leg.label) for leg in dag.process.legs if leg.side == "initial" + } + compact_result = _compact_helicity_flip_representatives( + dag, + initial_leg_labels=initial_leg_labels, + pure_massless_adjoint=pure_massless_adjoint, + ) + if compact_result is None: + retained, changed, zero_pruned = _generic_helicity_flip_representatives( + dag, + initial_leg_labels=initial_leg_labels, + pure_massless_adjoint=pure_massless_adjoint, + ) + else: + retained, changed, zero_pruned = compact_result + + if not changed and not zero_pruned: + return dag + return prune_dag_to_amplitude_roots( + GenericDAG( + process=dag.process, + color_plan=dag.color_plan, + currents=dag.currents, + sources=dag.sources, + interactions=dag.interactions, + amplitude_roots=tuple(retained), + truncated=dag.truncated, + helicity_coverage=dag.helicity_coverage, + color_coverage=dag.color_coverage, + selected_source_helicities=dag.selected_source_helicities, + selected_color_sector_ids=dag.selected_color_sector_ids, + lc_topology_replay=dag.lc_topology_replay, + color_topology_replay=dag.color_topology_replay, + ) + ) + + +def _compact_helicity_flip_representatives( + dag: GenericDAG, + *, + initial_leg_labels: set[int], + pure_massless_adjoint: bool, +) -> tuple[list[AmplitudeRoot], bool, bool] | None: + """Select flip representatives using dense physical-helicity bit codes. + + Color-decorated DAGs may contain thousands of source currents even though + each root depends on one source per external leg. Walking the set ancestry + bits avoids scanning every decorated source for every amplitude root. The + conservative checks keep unfamiliar source layouts on the generic path. + """ + + encoding = _compact_source_helicity_encoding(dag, initial_leg_labels) + if encoding is None: + return None + helicity_by_source_bit, leg_by_source_bit, all_legs_mask, initial_mask = encoding + leg_count = all_legs_mask.bit_count() + stride = all_legs_mask + 1 + roots_by_key: dict[int, list[AmplitudeRoot]] = {} + zero_pruned = False + + for root in dag.amplitude_roots: + ancestry = int( + dag.currents[root.left_id].index.helicity_ancestry + | dag.currents[root.right_id].index.helicity_ancestry + ) + helicity_code = 0 + present_legs = 0 + remaining = ancestry + while remaining: + source_bit = remaining & -remaining + bit_index = source_bit.bit_length() - 1 + if bit_index >= len(helicity_by_source_bit): + return None + helicity_value = helicity_by_source_bit[bit_index] + leg_bit = leg_by_source_bit[bit_index] + if helicity_value is None or leg_bit is None or present_legs & leg_bit: + return None + helicity_code |= helicity_value + present_legs |= leg_bit + remaining ^= source_bit + if present_legs != all_legs_mask: + return None + + if pure_massless_adjoint: + positive_count = (helicity_code ^ initial_mask).bit_count() + if positive_count < 2 or leg_count - positive_count < 2: + zero_pruned = True + continue + + sector_id = _amplitude_root_color_sector_id(dag, root) + key = sector_id * stride + helicity_code + roots_by_key.setdefault(key, []).append(root) + + if not roots_by_key: + return [], False, False + + retained: list[AmplitudeRoot] = [] + changed = False + for key in sorted(roots_by_key): + sector_id, helicity_code = divmod(key, stride) + flipped_key = sector_id * stride + (helicity_code ^ all_legs_mask) + partner = roots_by_key.get(flipped_key) + if partner is not None and flipped_key < key: + continue + weight = 1.0 + if partner is not None and flipped_key != key: + weight = 2.0 + changed = True + for root in roots_by_key[key]: + retained.append(_weighted_amplitude_root(root, len(retained), weight)) + return retained, changed, zero_pruned + + +def _compact_source_helicity_encoding( + dag: GenericDAG, + initial_leg_labels: set[int], +) -> ( + tuple[ + list[int | None], + list[int | None], + int, + int, + ] + | None +): + labels = tuple(sorted(int(leg.label) for leg in dag.process.legs)) + if not labels or len(set(labels)) != len(labels) or not dag.sources: + return None + leg_bits = { + label: 1 << (len(labels) - index - 1) for index, label in enumerate(labels) + } + + source_records: list[tuple[int, int, int]] = [] + for source_id in dag.sources: + current = dag.currents[source_id] + ancestry = int(current.index.helicity_ancestry) + label = int(current.source_leg_label or 0) + helicity = int(current.source_helicity or 0) + if ( + ancestry <= 0 + or ancestry & (ancestry - 1) + or label not in leg_bits + or helicity not in {-1, 1} + ): + return None + source_records.append((ancestry, label, helicity)) + source_records.sort() + if any(left[1] > right[1] for left, right in pairwise(source_records)): + return None + + table_size = source_records[-1][0].bit_length() + helicity_by_source_bit: list[int | None] = [None] * table_size + leg_by_source_bit: list[int | None] = [None] * table_size + for ancestry, label, helicity in source_records: + bit_index = ancestry.bit_length() - 1 + leg_bit = leg_bits[label] + helicity_value = leg_bit if helicity == 1 else 0 + existing_helicity = helicity_by_source_bit[bit_index] + existing_leg = leg_by_source_bit[bit_index] + if existing_helicity is not None and ( + existing_helicity != helicity_value or existing_leg != leg_bit + ): + return None + helicity_by_source_bit[bit_index] = helicity_value + leg_by_source_bit[bit_index] = leg_bit + + all_legs_mask = (1 << len(labels)) - 1 + initial_mask = 0 + for label in initial_leg_labels: + leg_bit = leg_bits.get(label) + if leg_bit is None: + return None + initial_mask |= leg_bit + return ( + helicity_by_source_bit, + leg_by_source_bit, + all_legs_mask, + initial_mask, + ) + + +def _generic_helicity_flip_representatives( + dag: GenericDAG, + *, + initial_leg_labels: set[int], + pure_massless_adjoint: bool, +) -> tuple[list[AmplitudeRoot], bool, bool]: + """Retain the fully generic tuple-signature implementation as fallback.""" + + source_by_bit = _source_helicity_signature_by_bit(dag) + roots_by_signature: dict[tuple[object, ...], list[AmplitudeRoot]] = {} + source_helicities_by_ancestry: dict[int, tuple[tuple[int, int], ...]] = {} + zero_pruned = False + for root in dag.amplitude_roots: + signature = _root_physical_helicity_signature( + dag, + root, + source_by_bit, + source_helicities_by_ancestry, + ) + if pure_massless_adjoint and _pure_massless_adjoint_helicity_signature_is_zero( + signature, + initial_leg_labels, + ): + zero_pruned = True + continue + roots_by_signature.setdefault( + signature, + [], + ).append(root) + if not roots_by_signature: + return [], False, False + + handled: set[tuple[object, ...]] = set() + retained: list[AmplitudeRoot] = [] + changed = False + for signature in sorted(roots_by_signature): + if signature in handled: + continue + flipped = _flip_root_physical_helicity_signature(signature) + partner = roots_by_signature.get(flipped) + handled.add(signature) + if partner is not None: + handled.add(flipped) + weight = 1.0 + if partner is not None and flipped != signature: + weight = 2.0 + changed = True + for root in roots_by_signature[signature]: + retained.append(_weighted_amplitude_root(root, len(retained), weight)) + return retained, changed, zero_pruned + + +def _weighted_amplitude_root( + root: AmplitudeRoot, + root_id: int, + weight: float, +) -> AmplitudeRoot: + return AmplitudeRoot( + id=root_id, + kind=root.kind, + left_id=root.left_id, + right_id=root.right_id, + color_weight=root.color_weight, + contraction_ir=root.contraction_ir, + color_sector_id=root.color_sector_id, + vertex_kind=root.vertex_kind, + vertex_particles=root.vertex_particles, + coupling=root.coupling, + helicity_weight=root.helicity_weight * weight, + ) + + +def _global_helicity_flip_equivalence_proof( + dag: GenericDAG, + model: Model, +) -> tuple[bool, tuple[Vertex, ...]]: + if dag.process.color_accuracy not in {"lc", "nlc", "full"}: + return False, () + for leg in dag.process.legs: + if leg.outgoing_pdg is None: + return False, () + pdg = int(leg.outgoing_pdg) + if not ( + model.is_massless_adjoint_vector(pdg) + or model.is_fundamental_colored_fermion(pdg) + ): + return False, () + if model.mass(pdg) != 0.0: + return False, () + vertices = _dag_vertex_inventory(dag) + return ( + model.global_helicity_flip_equivalence_is_proven(vertices), + vertices, + ) + + +def _dag_vertex_inventory(dag: GenericDAG) -> tuple[Vertex, ...]: + """Return each local vertex contract once in deterministic order.""" + + identities = { + (int(interaction.vertex_kind), interaction.vertex_particles) + for interaction in dag.interactions + } + identities.update( + (int(root.vertex_kind), root.vertex_particles) + for root in dag.amplitude_roots + if root.vertex_kind is not None and root.vertex_particles is not None + ) + return tuple(Vertex(kind, particles) for kind, particles in sorted(identities)) + + +def _pure_massless_adjoint_helicity_pruning_safe( + dag: GenericDAG, + model: Model, + vertices: Sequence[Vertex] | None = None, +) -> bool: + return model.pure_massless_adjoint_helicity_zero_rule_is_proven( + dag.process, + _dag_vertex_inventory(dag) if vertices is None else vertices, + ) + + +def _pure_massless_adjoint_helicity_signature_is_zero( + signature: tuple[object, ...], + initial_leg_labels: set[int], +) -> bool: + _sector_id, source_helicities = signature + helicities = [] + for label, helicity in cast(Sequence[tuple[int, int]], source_helicities): + value = int(helicity) + if int(label) in initial_leg_labels: + value = -value + helicities.append(value) + return helicities.count(1) < 2 or helicities.count(-1) < 2 + + +def _root_physical_helicity_signature( + dag: GenericDAG, + root: AmplitudeRoot, + source_by_bit: Mapping[int, tuple[int, int]], + source_helicities_by_ancestry: dict[ + int, + tuple[tuple[int, int], ...], + ] + | None = None, +) -> tuple[object, ...]: + left = dag.currents[root.left_id].index + right = dag.currents[root.right_id].index + ancestry = int(left.helicity_ancestry | right.helicity_ancestry) + source_helicities = ( + None + if source_helicities_by_ancestry is None + else source_helicities_by_ancestry.get(ancestry) + ) + if source_helicities is None: + source_helicities = _source_helicities_for_ancestry( + ancestry, + source_by_bit, + ) + if source_helicities_by_ancestry is not None: + source_helicities_by_ancestry[ancestry] = source_helicities + sector_id = _amplitude_root_color_sector_id(dag, root) + return (sector_id, source_helicities) + + +def _root_source_helicity_mapping( + dag: GenericDAG, + root: AmplitudeRoot, + source_by_bit: Mapping[int, tuple[int, int]], +) -> dict[int, int]: + left = dag.currents[root.left_id].index + right = dag.currents[root.right_id].index + ancestry = int(left.helicity_ancestry | right.helicity_ancestry) + return { + int(label): int(helicity) + for label, helicity in _source_helicities_for_ancestry( + ancestry, + source_by_bit, + ) + } + + +def _source_helicities_for_ancestry( + ancestry: int, + source_by_bit: Mapping[int, tuple[int, int]], +) -> tuple[tuple[int, int], ...]: + """Resolve only set ancestry bits while preserving ascending-bit order.""" + + records: list[tuple[int, int]] = [] + remaining = int(ancestry) + while remaining: + bit = remaining & -remaining + source = source_by_bit.get(bit) + if source is not None: + records.append(source) + remaining ^= bit + return tuple(records) + + +def _source_helicity_signature_by_bit( + dag: GenericDAG, +) -> dict[int, tuple[int, int]]: + records: list[tuple[int, tuple[int, int]]] = [] + for source_id in dag.sources: + current = dag.currents[source_id] + records.append( + ( + int(current.index.helicity_ancestry), + ( + int(current.source_leg_label or 0), + int(current.source_helicity or 0), + ), + ) + ) + return dict(sorted(records)) + + +def _canonicalize_amplitude_root_order(dag: GenericDAG) -> GenericDAG: + """Order roots by their physical colour/helicity representative. + + Eager preselection omits one member of every proven global-helicity-flip + pair before source-bit allocation. Its ancestry integers therefore differ + from a complete DAG even though the retained physical states are identical. + Canonicalizing by physical source labels keeps public reduction identifiers + independent of that implementation detail. + """ + + source_by_bit = _source_helicity_signature_by_bit(dag) + source_helicities_by_ancestry: dict[int, tuple[tuple[int, int], ...]] = {} + ordered = tuple( + sorted( + dag.amplitude_roots, + key=lambda root: ( + _root_physical_helicity_signature( + dag, + root, + source_by_bit, + source_helicities_by_ancestry, + ), + root.id, + ), + ) + ) + if all(root.id == index for index, root in enumerate(ordered)): + return dag + return replace( + dag, + amplitude_roots=tuple( + replace(root, id=index) for index, root in enumerate(ordered) + ), + ) + + +def _flip_root_physical_helicity_signature( + signature: tuple[object, ...], +) -> tuple[object, ...]: + sector_id, source_helicities = signature + return ( + sector_id, + tuple( + (int(label), -int(helicity)) + for label, helicity in cast( + Sequence[tuple[int, int]], + source_helicities, + ) + ), + ) + + +def contributing_color_sector_ids(dag: GenericDAG) -> tuple[int, ...]: + """Return LC colour sectors that actually contribute amplitude roots.""" + + return tuple( + sorted( + {_amplitude_root_color_sector_id(dag, root) for root in dag.amplitude_roots} + ) + ) + + +def _amplitude_root_color_sector_id(dag: GenericDAG, root: AmplitudeRoot) -> int: + if root.color_sector_id is not None: + return int(root.color_sector_id) + left = dag.currents[root.left_id].index + if left.color_state.accuracy in {"lc", "nlc", "full"}: + return int(left.color_state.sector_id) + return 0 + + +def filter_dag_to_color_sectors( + dag: GenericDAG, + sector_ids: Iterable[int], +) -> GenericDAG: + """Return a dense-current DAG restricted to the requested colour sectors. + + Full DAG construction remains useful for diagnostics, but schema-v3 runtime + records expect dense current ids. This helper derives the runtime DAG by selecting + roots whose LC colour sector is in ``sector_ids``, walking backward through + the current DAG, and remapping the required currents/interactions densely. + Root-based filtering is required for shared LC all-ordering DAGs where + internal currents are sector-neutral but amplitude roots still carry the + physical sector identity. + """ + + selected = set(sector_ids) + if not selected: + return GenericDAG( + process=dag.process, + color_plan=dag.color_plan, + currents=(), + sources=(), + interactions=(), + amplitude_roots=(), + truncated=dag.truncated, + helicity_coverage=dag.helicity_coverage, + color_coverage="selected", + selected_source_helicities=dag.selected_source_helicities, + selected_color_sector_ids=tuple(sorted(selected)), + lc_topology_replay=None, + color_topology_replay=None, + ) + + selected_roots = tuple( + root + for root in dag.amplitude_roots + if _amplitude_root_color_sector_id(dag, root) in selected + ) + if not selected_roots: + return GenericDAG( + process=dag.process, + color_plan=dag.color_plan, + currents=(), + sources=(), + interactions=(), + amplitude_roots=(), + truncated=dag.truncated, + helicity_coverage=dag.helicity_coverage, + color_coverage="selected", + selected_source_helicities=dag.selected_source_helicities, + selected_color_sector_ids=tuple(sorted(selected)), + lc_topology_replay=None, + color_topology_replay=None, + ) + + interactions_by_result: dict[int, list[InteractionNode]] = {} + for interaction in dag.interactions: + interactions_by_result.setdefault(interaction.result_id, []).append(interaction) + + required_current_ids: set[int] = set() + required_interaction_ids: set[int] = set() + stack: list[int] = [] + for root in selected_roots: + for current_id in (root.left_id, root.right_id): + if current_id not in required_current_ids: + required_current_ids.add(current_id) + stack.append(current_id) + + while stack: + current_id = stack.pop() + for interaction in interactions_by_result.get(current_id, ()): + if interaction.id in required_interaction_ids: + continue + required_interaction_ids.add(interaction.id) + for parent_id in (interaction.left_id, interaction.right_id): + if parent_id not in required_current_ids: + required_current_ids.add(parent_id) + stack.append(parent_id) + + current_id_map: dict[int, int] = {} + currents: list[CurrentNode] = [] + for current in dag.currents: + if current.id not in required_current_ids: + continue + new_id = len(currents) + current_id_map[current.id] = new_id + currents.append( + CurrentNode( + id=new_id, + index=current.index, + dimension=current.dimension, + is_source=current.is_source, + source_leg_label=current.source_leg_label, + source_helicity=current.source_helicity, + ) + ) + + sources = tuple( + current_id_map[source_id] + for source_id in dag.sources + if source_id in current_id_map + ) + + interactions: list[InteractionNode] = [] + for interaction in dag.interactions: + if interaction.id not in required_interaction_ids: + continue + if ( + interaction.left_id not in current_id_map + or interaction.right_id not in current_id_map + or interaction.result_id not in current_id_map + ): + continue + interactions.append( + InteractionNode( + id=len(interactions), + vertex_kind=interaction.vertex_kind, + vertex_particles=interaction.vertex_particles, + left_id=current_id_map[interaction.left_id], + right_id=current_id_map[interaction.right_id], + result_id=current_id_map[interaction.result_id], + coupling=interaction.coupling, + color_weight=interaction.color_weight, + lowering_backend=interaction.lowering_backend, + full_tensor_network_ready=interaction.full_tensor_network_ready, + evaluation_group_id=interaction.evaluation_group_id, + evaluation_factor=interaction.evaluation_factor, + ) + ) + + amplitude_roots: list[AmplitudeRoot] = [] + for root in selected_roots: + if root.left_id not in current_id_map or root.right_id not in current_id_map: + continue + amplitude_roots.append( + AmplitudeRoot( + id=len(amplitude_roots), + kind=root.kind, + left_id=current_id_map[root.left_id], + right_id=current_id_map[root.right_id], + color_weight=root.color_weight, + contraction_ir=root.contraction_ir, + color_sector_id=root.color_sector_id, + vertex_kind=root.vertex_kind, + vertex_particles=root.vertex_particles, + coupling=root.coupling, + helicity_weight=root.helicity_weight, + ) + ) + + return GenericDAG( + process=dag.process, + color_plan=dag.color_plan, + currents=tuple(currents), + sources=sources, + interactions=tuple(interactions), + amplitude_roots=tuple(amplitude_roots), + truncated=dag.truncated, + helicity_coverage=dag.helicity_coverage, + color_coverage="selected", + selected_source_helicities=dag.selected_source_helicities, + selected_color_sector_ids=tuple(sorted(selected)), + lc_topology_replay=None, + color_topology_replay=None, + ) + + +def filter_dag_to_source_helicities( + dag: GenericDAG, + source_helicities: Mapping[int, int], +) -> GenericDAG: + """Return a DAG restricted to roots matching fixed source helicities.""" + + requested = { + int(label): int(helicity) for label, helicity in source_helicities.items() + } + if not requested: + return dag + + source_by_bit = _source_helicity_signature_by_bit(dag) + selected_roots = tuple( + root + for root in dag.amplitude_roots + if _root_source_helicity_mapping(dag, root, source_by_bit).items() + >= requested.items() + ) + if len(selected_roots) == len(dag.amplitude_roots): + return dag + return prune_dag_to_amplitude_roots( + GenericDAG( + process=dag.process, + color_plan=dag.color_plan, + currents=dag.currents, + sources=dag.sources, + interactions=dag.interactions, + amplitude_roots=selected_roots, + truncated=dag.truncated, + helicity_coverage="selected", + color_coverage=dag.color_coverage, + selected_source_helicities=tuple( + sorted({**dict(dag.selected_source_helicities), **requested}.items()) + ), + selected_color_sector_ids=dag.selected_color_sector_ids, + lc_topology_replay=dag.lc_topology_replay, + color_topology_replay=dag.color_topology_replay, + ) + ) diff --git a/pyamplicol/src/pyamplicol/generation/dag_color.py b/pyamplicol/src/pyamplicol/generation/dag_color.py new file mode 100644 index 0000000000..53cc11d939 --- /dev/null +++ b/pyamplicol/src/pyamplicol/generation/dag_color.py @@ -0,0 +1,1132 @@ +# SPDX-License-Identifier: 0BSD +"""Local color-flow state transitions for generic DAG generation.""" + +from __future__ import annotations + +from collections.abc import Iterable + +from ..color.plan import GenericColorPlan +from ..models.base import Model, Vertex +from ..processes.ir import ProcessLegIR +from .dag_ordering import ( + _closure_combination_matches_word, + _known_color_representation, + _known_fermion_statistics, + _labels_projected_to_word, + _lc_word_with_sink_last, + _line_local_singlet_extras_allowed, + _mask_labels, + _ordered_combination_matches_word, + _ordered_combination_segment, + _sector_group_indices_for_label, + _sector_intermediate_order_words, + _word_contains_ordered_segment, +) +from .dag_types import ( + _LC_FIERZ_SINGLET_BASIS, + ColorFlow, + ColorState, + CurrentIndex, + _lc_color_identity_closure_key, + _lc_color_identity_closures, + _lc_color_identity_closures_compatible, +) + +_LC_SECTOR_SUPPORT_PREFIX = "lc-sector-support:" + + +def _lc_sector_support_key(sector_ids: Iterable[int]) -> str: + normalized = sorted(set(int(value) for value in sector_ids)) + return _LC_SECTOR_SUPPORT_PREFIX + ",".join(str(value) for value in normalized) + + +def _lc_sector_support(basis_keys: Iterable[str]) -> frozenset[int] | None: + supports: list[frozenset[int]] = [] + for key in basis_keys: + if not key.startswith(_LC_SECTOR_SUPPORT_PREFIX): + continue + payload = key.removeprefix(_LC_SECTOR_SUPPORT_PREFIX) + try: + support = frozenset(int(value) for value in payload.split(",") if value) + except ValueError: + return frozenset() + if not support: + return frozenset() + supports.append(support) + if not supports: + return None + result = set(supports[0]) + for support in supports[1:]: + result.intersection_update(support) + return frozenset(result) + + +def _without_lc_sector_support(basis_keys: Iterable[str]) -> set[str]: + return {key for key in basis_keys if not key.startswith(_LC_SECTOR_SUPPORT_PREFIX)} + + +class ColorEngine: + """Local colour-flow engine used by the process-generic recursion.""" + + def __init__( + self, + color_plan: GenericColorPlan, + model: Model, + *, + shared_lc_all_ordering_symmetry: bool = False, + cache_shared_lc_orderings: bool = False, + ) -> None: + self.color_plan = color_plan + self.model = model + self._sector_by_id = {sector.id: sector for sector in color_plan.sectors} + self._leg_by_label = {leg.label: leg for leg in color_plan.process.legs} + self._color_rep_by_label: dict[int, int | None] = {} + for label, leg in self._leg_by_label.items(): + if leg.outgoing_pdg is None: + self._color_rep_by_label[label] = None + continue + self._color_rep_by_label[label] = _known_color_representation( + self.model, + leg.outgoing_pdg, + ) + self._label_is_color_singlet = { + label: representation == 1 + for label, representation in self._color_rep_by_label.items() + } + self._shared_lc_coloured_labels = { + label + for label, representation in self._color_rep_by_label.items() + if representation is not None and representation != 1 + } + self._shared_lc_singlet_labels = { + label + for label, representation in self._color_rep_by_label.items() + if representation == 1 + } + self._fundamental_line_auxiliary_ids = ( + self._find_fundamental_line_auxiliary_ids() + ) + self._fundamental_fermion_pair_count = ( + self._external_fundamental_fermion_pair_count() + ) + self._vertex_has_colour_cache: dict[tuple[int, int, int, int], bool] = {} + self._ordered_combination_labels_cache: dict[ + tuple[int, tuple[int, ...], tuple[int, ...], int, int, int, int], + tuple[int, ...] | None, + ] = {} + self._shared_lc_proposed_labels_cache: ( + dict[tuple[tuple[int, ...], bool], tuple[int, ...] | None] | None + ) = {} if cache_shared_lc_orderings else None + color_plan_matches_model = self._color_plan_matches_model_roles() + all_external_massless_adjoint_vectors = ( + self._all_external_massless_adjoint_vectors() + ) + try: + shared_single_trace_color_basis = ( + self.model.shared_single_trace_color_basis_is_proven(color_plan.process) + ) + except (KeyError, NotImplementedError, TypeError, ValueError): + shared_single_trace_color_basis = False + self._shared_single_trace = bool( + color_plan.color_accuracy in {"nlc", "full"} + and bool(color_plan.sectors) + and color_plan_matches_model + and all_external_massless_adjoint_vectors + and shared_single_trace_color_basis + and all(sector.kind == "single-trace" for sector in color_plan.sectors) + ) + self._shared_single_trace_words = tuple( + word for sector in color_plan.sectors for word in (sector.color_words or ()) + ) + self._shared_single_trace_word_set = frozenset(self._shared_single_trace_words) + self._shared_single_trace_segments = frozenset( + tuple(word[start:stop]) + for word in self._shared_single_trace_words + for start in range(len(word)) + for stop in range(start + 1, len(word) + 1) + ) + closure_sector_ids_by_word: dict[tuple[int, ...], set[int]] = {} + for sector in color_plan.sectors: + for word in sector.admissible_traversal_words: + closure_sector_ids_by_word.setdefault(tuple(word), set()).add( + int(sector.id) + ) + self._shared_single_trace_closure_sector_ids_by_word = { + word: tuple(sorted(sector_ids)) + for word, sector_ids in closure_sector_ids_by_word.items() + } + self._shared_lc_orderings = ( + ( + color_plan.color_accuracy == "lc" + or ( + color_plan.color_accuracy in {"nlc", "full"} + and self._fundamental_fermion_pair_count == 1 + ) + ) + and bool(color_plan.sectors) + and bool(self._shared_lc_coloured_labels) + and color_plan_matches_model + ) + self._shared_lc_all_ordering_symmetry = bool( + shared_lc_all_ordering_symmetry + and self._shared_lc_orderings + and not color_plan.truncated + ) + all_adjoint_process_symmetry = bool( + self._shared_lc_all_ordering_symmetry + and all_external_massless_adjoint_vectors + ) + self._shared_lc_fixed_sink_label = ( + max(self._shared_lc_coloured_labels) + if all_adjoint_process_symmetry and self._shared_lc_coloured_labels + else None + ) + self._shared_lc_words_by_sector = tuple( + ( + sector, + tuple( + _lc_word_with_sink_last( + word, + self._shared_lc_fixed_sink_label, + ) + for word in _sector_intermediate_order_words( + sector, + include_compatible_traversals=self._shared_lc_orderings, + process=color_plan.process, + ) + ), + ) + for sector in color_plan.sectors + ) + self._shared_lc_sector_ids_by_word: dict[tuple[int, ...], tuple[int, ...]] = {} + self._shared_lc_sector_ids_by_segment: dict[ + tuple[int, ...], tuple[int, ...] + ] = {} + if self._shared_lc_orderings: + sector_ids_by_word: dict[tuple[int, ...], list[int]] = {} + sector_ids_by_segment: dict[tuple[int, ...], set[int]] = {} + segments: set[tuple[int, ...]] = set() + for sector in color_plan.sectors: + for word in _sector_intermediate_order_words( + sector, + process=color_plan.process, + ): + normalized_word = _lc_word_with_sink_last( + word, + self._shared_lc_fixed_sink_label, + ) + sector_ids_by_word.setdefault(normalized_word, []).append( + int(sector.id) + ) + for word in _sector_intermediate_order_words( + sector, + process=color_plan.process, + ): + normalized_word = _lc_word_with_sink_last( + word, + self._shared_lc_fixed_sink_label, + ) + for start in range(len(normalized_word)): + for stop in range(start + 1, len(normalized_word) + 1): + segment = tuple(normalized_word[start:stop]) + segments.add(segment) + sector_ids_by_segment.setdefault(segment, set()).add( + int(sector.id) + ) + self._shared_lc_sector_ids_by_word = { + word: tuple(ids) for word, ids in sector_ids_by_word.items() + } + self._shared_lc_sector_ids_by_segment = { + segment: tuple(sorted(ids)) + for segment, ids in sector_ids_by_segment.items() + } + self._shared_lc_segments = frozenset(segments) + else: + self._shared_lc_segments = frozenset() + + def _color_plan_matches_model_roles(self) -> bool: + if any( + representation is None + for representation in self._color_rep_by_label.values() + ): + return False + for sector in self.color_plan.sectors: + sector_coloured_labels = { + label for group in sector.coloured_label_groups for label in group + } + if sector_coloured_labels != self._shared_lc_coloured_labels: + return False + if set(sector.singlet_labels) != self._shared_lc_singlet_labels: + return False + return True + + def _all_external_massless_adjoint_vectors(self) -> bool: + if not self._leg_by_label: + return False + for leg in self._leg_by_label.values(): + if leg.outgoing_pdg is None: + return False + try: + if not self.model.is_massless_adjoint_vector(leg.outgoing_pdg): + return False + except (KeyError, NotImplementedError, TypeError, ValueError): + return False + return True + + def _is_fundamental_colored_fermion(self, particle_id: int) -> bool: + representation = _known_color_representation(self.model, particle_id) + return ( + representation is not None + and abs(representation) == 3 + and _known_fermion_statistics(self.model, particle_id) is True + ) + + def _is_fundamental_line_auxiliary(self, particle_id: int) -> bool: + try: + auxiliary_kind = self.model.auxiliary_kind(particle_id) + except (KeyError, NotImplementedError, TypeError, ValueError): + return False + return auxiliary_kind == "u1-subtraction-color-flow-vector" + + def _find_fundamental_line_auxiliary_ids(self) -> frozenset[int]: + """Identify proven unphysical singlet currents attached to fundamental lines.""" + + auxiliary_ids: set[int] = set() + for vertex in self.model.vertices: + for index, particle_id in enumerate(vertex.particles): + if not self._is_fundamental_line_auxiliary(particle_id): + continue + other_particles = ( + vertex.particles[:index] + vertex.particles[index + 1 :] + ) + if all( + self._is_fundamental_colored_fermion(other_particle) + for other_particle in other_particles + ): + auxiliary_ids.add(particle_id) + return frozenset(auxiliary_ids) + + def _external_fundamental_fermion_pair_count(self) -> int: + fundamental_count = 0 + antifundamental_count = 0 + for leg in self._leg_by_label.values(): + if leg.outgoing_pdg is None or not self._is_fundamental_colored_fermion( + leg.outgoing_pdg + ): + continue + representation = _known_color_representation( + self.model, + leg.outgoing_pdg, + ) + if representation == 3: + fundamental_count += 1 + elif representation == -3: + antifundamental_count += 1 + return min(fundamental_count, antifundamental_count) + + def source_states_for_leg(self, leg: ProcessLegIR) -> tuple[ColorState, ...]: + if self._shared_lc_orderings: + leg_is_singlet = self._label_is_color_singlet.get(leg.label, False) + return ( + ColorState( + accuracy=self.color_plan.color_accuracy, + sector_id=0, + line_groups=() if leg_is_singlet else (0,), + ), + ) + if self._shared_single_trace: + return ( + ColorState( + accuracy=self.color_plan.color_accuracy, + sector_id=0, + line_groups=(0,), + ), + ) + if not self.color_plan.sectors: + return (ColorState(accuracy=self.color_plan.color_accuracy),) + states = [] + leg_is_singlet = self._label_is_color_singlet.get(leg.label, False) + for sector in self.color_plan.sectors: + groups = ( + () + if leg_is_singlet + else _sector_group_indices_for_label(sector, leg.label) + ) + if groups or leg_is_singlet or not sector.line_label_groups: + states.append( + ColorState( + accuracy=self.color_plan.color_accuracy, + sector_id=sector.id, + line_groups=groups, + ) + ) + return tuple(states) + + def combine( + self, + left: ColorState, + right: ColorState, + vertex: Vertex, + *, + ordered_external_labels: tuple[int, ...] = (), + sector_support_label_variants: tuple[tuple[int, ...], ...] | None = None, + ) -> tuple[ColorFlow, ...]: + if left.accuracy != right.accuracy: + return () + if not self.color_plan.sectors: + return ( + ColorFlow( + state=ColorState( + accuracy=left.accuracy, + basis_key=tuple( + sorted(set(left.basis_key) | set(right.basis_key)) + ), + ) + ), + ) + if self._shared_single_trace: + groups = tuple(sorted(set(left.line_groups) | set(right.line_groups))) + return ( + ColorFlow( + state=ColorState( + accuracy=left.accuracy, + sector_id=0, + line_groups=groups or (0,), + ) + ), + ) + if self._shared_lc_orderings: + groups = self._lc_combined_line_groups(left, right, vertex) + if groups is None: + return () + projections = self._lc_projected_basis_and_weights( + left, + right, + vertex, + ordered_external_labels, + sector_support_label_variants=sector_support_label_variants, + ) + if not projections: + return () + return tuple( + ColorFlow( + state=ColorState( + accuracy=left.accuracy, + sector_id=0, + line_groups=groups, + basis_key=basis_key, + ), + weight=weight, + ) + for basis_key, weight in projections + ) + if left.sector_id != right.sector_id: + return () + groups = self._lc_combined_line_groups(left, right, vertex) + if groups is None: + return () + projections = self._lc_projected_basis_and_weights( + left, + right, + vertex, + ordered_external_labels, + sector_support_label_variants=sector_support_label_variants, + ) + if not projections: + return () + return tuple( + ColorFlow( + state=ColorState( + accuracy=left.accuracy, + sector_id=left.sector_id, + line_groups=groups, + basis_key=basis_key, + ), + weight=weight, + ) + for basis_key, weight in projections + ) + + def _lc_projected_basis_and_weights( + self, + left: ColorState, + right: ColorState, + vertex: Vertex, + ordered_external_labels: tuple[int, ...], + *, + sector_support_label_variants: tuple[tuple[int, ...], ...] | None = None, + ) -> tuple[tuple[tuple[str, ...], tuple[float, float]], ...]: + basis_with_support = set(left.basis_key) | set(right.basis_key) + inherited_support = _lc_sector_support(basis_with_support) + basis = _without_lc_sector_support(basis_with_support) + structure = self.model.vertex_color_structure(vertex) + reps = tuple(abs(self.model.color_rep(pdg)) for pdg in vertex.particles) + has_fierz_singlet = _LC_FIERZ_SINGLET_BASIS in basis + if has_fierz_singlet: + consumes_singlet_exchange = ( + structure == "fundamental-generator" + and reps[2] == 3 + and sorted(reps[:2]) == [3, 8] + ) + if not consumes_singlet_exchange: + return () + basis.remove(_LC_FIERZ_SINGLET_BASIS) + + creates_fierz_projection = ( + structure == "fundamental-generator" + and reps[:2] == (3, 3) + and reps[2] == 8 + and not self._fundamental_line_auxiliary_ids + ) + + if ( + self._shared_lc_orderings + and structure == "color-identity" + and sorted(reps[:2]) == [3, 3] + and reps[2] == 1 + and vertex.particles[2] not in self._fundamental_line_auxiliary_ids + ): + colored_labels = tuple( + sorted( + label + for label in ordered_external_labels + if label in self._shared_lc_coloured_labels + ) + ) + if colored_labels: + basis.add(_lc_color_identity_closure_key(colored_labels)) + + if self._shared_lc_orderings: + support_label_variants = ( + (ordered_external_labels,) + if sector_support_label_variants is None + else sector_support_label_variants + ) + compatible_sector_ids: set[int] = set() + for support_labels in support_label_variants: + compatible_sector_ids.update( + self._shared_lc_compatible_sector_ids(support_labels) + ) + if inherited_support is not None: + compatible_sector_ids.intersection_update(inherited_support) + if not compatible_sector_ids: + return () + all_sector_ids = set(self._sector_by_id) + track_sector_support = ( + inherited_support is not None + or creates_fierz_projection + or ( + self.color_plan.color_accuracy == "lc" + and compatible_sector_ids != all_sector_ids + ) + ) + + candidates: list[tuple[set[str], tuple[float, float], set[int]]] = [] + if creates_fierz_projection: + fierz_sector_ids = ( + self._lc_fierz_sector_ids(ordered_external_labels) + & compatible_sector_ids + ) + ordinary_sector_ids = compatible_sector_ids - fierz_sector_ids + if ordinary_sector_ids: + candidates.append((set(basis), (1.0, 0.0), ordinary_sector_ids)) + if fierz_sector_ids: + fierz_basis = set(basis) + fierz_basis.add(_LC_FIERZ_SINGLET_BASIS) + candidates.append( + ( + fierz_basis, + (1.0 / 3.0, 0.0), + fierz_sector_ids, + ) + ) + else: + candidates.append((set(basis), (1.0, 0.0), compatible_sector_ids)) + + projections: list[tuple[tuple[str, ...], tuple[float, float]]] = [] + for candidate_basis, weight, support in candidates: + if track_sector_support and support != all_sector_ids: + candidate_basis.add(_lc_sector_support_key(support)) + if not self._shared_lc_basis_has_compatible_sector( + candidate_basis, + ordered_external_labels=ordered_external_labels, + ): + continue + projections.append((tuple(sorted(candidate_basis)), weight)) + return tuple(projections) + + if creates_fierz_projection and self._lc_labels_close_one_open_line( + ordered_external_labels + ): + basis.add(_LC_FIERZ_SINGLET_BASIS) + return ((tuple(sorted(basis)), (1.0 / 3.0, 0.0)),) + return ((tuple(sorted(basis)), (1.0, 0.0)),) + + def _shared_lc_compatible_sector_ids( + self, + ordered_external_labels: tuple[int, ...], + ) -> set[int]: + colored = tuple( + label + for label in ordered_external_labels + if label in self._shared_lc_coloured_labels + ) + if not colored: + return set(self._sector_by_id) + return set(self._shared_lc_sector_ids_by_segment.get(colored, ())) + + def _lc_fierz_sector_ids( + self, + ordered_external_labels: tuple[int, ...], + ) -> set[int]: + colored = tuple( + label + for label in ordered_external_labels + if label in self._shared_lc_coloured_labels + ) + labels = set(colored) + sector_ids: set[int] = set() + if not labels: + return sector_ids + for sector, words in self._shared_lc_words_by_sector: + if not any(_word_contains_ordered_segment(word, colored) for word in words): + continue + complete_groups = tuple( + set(group) + for group in sector.line_label_groups + if set(group).issubset(labels) + ) + if complete_groups and set().union(*complete_groups) == labels: + sector_ids.add(int(sector.id)) + return sector_ids + + def _shared_lc_basis_has_compatible_sector( + self, + basis_keys: Iterable[str], + *, + ordered_external_labels: Iterable[int] = (), + ) -> bool: + basis_tuple = tuple(basis_keys) + closures = _lc_color_identity_closures(basis_tuple) + support = _lc_sector_support(basis_tuple) + colored_segment = tuple( + label + for label in ordered_external_labels + if label in self._shared_lc_coloured_labels + ) + sector_ids = ( + self._shared_lc_sector_ids_by_segment.get(colored_segment, ()) + if colored_segment + else tuple(self._sector_by_id) + ) + return any( + sector is not None + and (support is None or sector_id in support) + and _lc_color_identity_closures_compatible(closures, sector) + for sector_id in sector_ids + for sector in (self._sector_by_id.get(sector_id),) + ) + + def _lc_labels_close_one_open_line( + self, + ordered_external_labels: tuple[int, ...], + ) -> bool: + colored = tuple( + label + for label in ordered_external_labels + if label in self._shared_lc_coloured_labels + ) + if not colored: + return False + return bool(self._lc_fierz_sector_ids(colored)) + + def _lc_combined_line_groups( + self, + left: ColorState, + right: ColorState, + vertex: Vertex, + ) -> tuple[int, ...] | None: + left_colored = self._particle_has_colour(vertex.particles[0]) + right_colored = self._particle_has_colour(vertex.particles[1]) + result_colored = self._particle_has_colour(vertex.particles[2]) + left_groups = set(left.line_groups) + right_groups = set(right.line_groups) + + if not left_colored and not right_colored: + if left_groups and right_groups and left_groups != right_groups: + return None + return tuple(sorted(left_groups | right_groups)) + + if left_colored != right_colored: + colored_groups = left_groups if left_colored else right_groups + singlet_groups = right_groups if left_colored else left_groups + if ( + singlet_groups + and colored_groups + and not singlet_groups.issubset(colored_groups) + ): + return None + return tuple(sorted(colored_groups or singlet_groups)) + + if ( + not result_colored + and left_groups + and right_groups + and left_groups != right_groups + ): + return None + return tuple(sorted(left_groups | right_groups)) + + def _particle_has_colour(self, pdg: int) -> bool: + representation = _known_color_representation(self.model, pdg) + return ( + representation is not None and representation != 1 + ) or pdg in self._fundamental_line_auxiliary_ids + + def closure_compatible( + self, + left: ColorState, + right: ColorState, + *, + full_mask: int, + ) -> tuple[ColorFlow, ...]: + if left.accuracy != right.accuracy: + return () + if not self.color_plan.sectors: + return ( + ColorFlow( + state=ColorState( + accuracy=left.accuracy, + basis_key=tuple( + sorted(set(left.basis_key) | set(right.basis_key)) + ), + ) + ), + ) + if self._shared_single_trace: + # Shared single-trace closure needs the endpoint order labels, not + # just the state. _build_amplitude_roots therefore calls + # shared_single_trace_closure_flows() with full CurrentIndex data. + return () + if left.sector_id != right.sector_id: + return () + groups = tuple(sorted(set(left.line_groups) | set(right.line_groups))) + sector = self._sector_by_id.get(left.sector_id) + if sector is None: + return (ColorFlow(state=left),) + basis = tuple(sorted(set(left.basis_key) | set(right.basis_key))) + if not _lc_color_identity_closures_compatible( + _lc_color_identity_closures(basis), + sector, + ): + return () + full_labels = set(_mask_labels(full_mask)) + sector_labels = set(sector.singlet_labels) + for group in sector.line_label_groups: + sector_labels.update(group) + if full_labels and not full_labels.issubset(sector_labels): + return () + return ( + ColorFlow( + state=ColorState( + accuracy=left.accuracy, + sector_id=left.sector_id, + line_groups=groups, + ) + ), + ) + + def _vertex_has_colour(self, vertex: Vertex) -> bool: + key = (vertex.kind, *vertex.particles) + cached = self._vertex_has_colour_cache.get(key) + if cached is not None: + return cached + for pdg in vertex.particles: + if self._particle_has_colour(pdg): + self._vertex_has_colour_cache[key] = True + return True + self._vertex_has_colour_cache[key] = False + return False + + def vertex_allowed(self, vertex: Vertex) -> bool: + contains_fundamental_auxiliary = any( + particle_id in self._fundamental_line_auxiliary_ids + for particle_id in vertex.particles + ) + if contains_fundamental_auxiliary: + return self._fundamental_fermion_pair_count >= 2 + if ( + self._fundamental_line_auxiliary_ids + and self._fundamental_fermion_pair_count >= 2 + and self.model.vertex_color_structure(vertex) == "fundamental-generator" + ): + representations = tuple( + self.model.color_rep(particle_id) for particle_id in vertex.particles + ) + if representations.count(8) == 1 and sorted( + abs(value) for value in representations if value != 8 + ) == [3, 3]: + return representations in { + (3, 8, 3), + (8, -3, -3), + (-3, 3, 8), + } + return True + + def ordered_combination_allowed( + self, + left_index: CurrentIndex, + right_index: CurrentIndex, + vertex: Vertex, + ) -> bool: + if not self._vertex_has_colour(vertex): + return True + if self._shared_lc_orderings: + return ( + self._shared_lc_ordered_combination_labels( + left_index, + right_index, + ) + is not None + ) + sector = self._sector_by_id.get(left_index.color_state.sector_id) + if sector is None: + return True + if sector.kind == "singlet" and all( + not word for word in sector.admissible_traversal_words + ): + return True + return any( + _ordered_combination_matches_word( + left_index.ordered_external_labels, + right_index.ordered_external_labels, + word, + ) + for word in _sector_intermediate_order_words( + sector, + include_compatible_traversals=False, + process=self.color_plan.process, + ) + ) + + def ordered_combination_labels( + self, + left_index: CurrentIndex, + right_index: CurrentIndex, + vertex: Vertex, + ) -> tuple[int, ...] | None: + cache_key = ( + left_index.color_state.sector_id, + left_index.ordered_external_labels, + right_index.ordered_external_labels, + vertex.kind, + *vertex.particles, + ) + if cache_key in self._ordered_combination_labels_cache: + return self._ordered_combination_labels_cache[cache_key] + proposed = ( + *left_index.ordered_external_labels, + *right_index.ordered_external_labels, + ) + singlet_order = self._singlet_order_allowed( + left_index, + right_index, + vertex, + ) + if not singlet_order: + self._ordered_combination_labels_cache[cache_key] = None + return None + if self._shared_single_trace: + segment = ( + *left_index.ordered_external_labels, + *right_index.ordered_external_labels, + ) + labels = segment if segment in self._shared_single_trace_segments else None + self._ordered_combination_labels_cache[cache_key] = labels + return labels + if self._shared_lc_orderings: + labels = self._shared_lc_ordered_combination_labels( + left_index, + right_index, + ) + self._ordered_combination_labels_cache[cache_key] = labels + return labels + sector = self._sector_by_id.get(left_index.color_state.sector_id) + if sector is None: + self._ordered_combination_labels_cache[cache_key] = proposed + return proposed + for word in _sector_intermediate_order_words( + sector, + include_compatible_traversals=False, + process=self.color_plan.process, + ): + segment = _ordered_combination_segment( + left_index.ordered_external_labels, + right_index.ordered_external_labels, + word, + ) + if segment is None: + continue + word_labels = set(word) + extras = tuple( + sorted(label for label in proposed if label not in word_labels) + ) + if not _line_local_singlet_extras_allowed(segment, extras, sector): + continue + labels = (*segment, *extras) + self._ordered_combination_labels_cache[cache_key] = labels + return labels + self._ordered_combination_labels_cache[cache_key] = None + return None + + def _singlet_order_allowed( + self, + left_index: CurrentIndex, + right_index: CurrentIndex, + vertex: Vertex, + ) -> bool: + left_singlet = self._all_external_labels_color_singlet( + left_index.external_labels + ) + right_singlet = self._all_external_labels_color_singlet( + right_index.external_labels + ) + if left_singlet and not right_singlet: + return False + if left_singlet and right_singlet: + return max(left_index.external_labels) < max(right_index.external_labels) + return True + + def _all_external_labels_color_singlet(self, labels: Iterable[int]) -> bool: + for label in labels: + is_singlet = self._label_is_color_singlet.get(label) + if is_singlet is None: + return False + if not is_singlet: + return False + return True + + def ordered_closure_allowed( + self, + left_index: CurrentIndex, + right_index: CurrentIndex, + ) -> bool: + if self._shared_single_trace: + return ( + *left_index.ordered_external_labels, + *right_index.ordered_external_labels, + ) in self._shared_single_trace_word_set + if self._shared_lc_orderings: + return self._shared_lc_closure_word(left_index, right_index) is not None + if left_index.color_state.sector_id != right_index.color_state.sector_id: + return False + sector = self._sector_by_id.get(left_index.color_state.sector_id) + if sector is None: + return True + if sector.kind == "singlet" and all( + not word for word in sector.admissible_traversal_words + ): + return True + if sector.kind == "open-lines": + word = sector.canonical_closure_traversal_word(self.color_plan.process) + return _closure_combination_matches_word( + _labels_projected_to_word( + left_index.ordered_external_labels, + word, + ), + _labels_projected_to_word( + right_index.ordered_external_labels, + word, + ), + word, + ) + return any( + _closure_combination_matches_word( + _labels_projected_to_word(left_index.ordered_external_labels, word), + _labels_projected_to_word(right_index.ordered_external_labels, word), + word, + ) + for word in sector.admissible_traversal_words + ) + + def shared_single_trace_closure_flows( + self, + left_index: CurrentIndex, + right_index: CurrentIndex, + ) -> tuple[ColorFlow, ...]: + if not self._shared_single_trace: + return () + closure_word = ( + *left_index.ordered_external_labels, + *right_index.ordered_external_labels, + ) + return tuple( + ColorFlow( + state=ColorState( + accuracy=self.color_plan.color_accuracy, + sector_id=sector_id, + line_groups=(0,), + ) + ) + for sector_id in self._shared_single_trace_closure_sector_ids_by_word.get( + closure_word, + (), + ) + ) + + @property + def shared_single_trace(self) -> bool: + return self._shared_single_trace + + def shared_lc_closure_flows( + self, + left_index: CurrentIndex, + right_index: CurrentIndex, + ) -> tuple[ColorFlow, ...]: + if not self._shared_lc_orderings: + return () + flows: list[ColorFlow] = [] + closure = self._shared_lc_closure(left_index, right_index) + if closure is None: + return () + _word, sector_ids = closure + for sector_id in sector_ids: + flows.append( + ColorFlow( + state=ColorState( + accuracy=self.color_plan.color_accuracy, + sector_id=sector_id, + line_groups=(0,), + ) + ) + ) + return tuple(flows) + + @property + def shared_lc_orderings(self) -> bool: + return self._shared_lc_orderings + + @property + def shared_lc_all_ordering_symmetry(self) -> bool: + return self._shared_lc_all_ordering_symmetry + + @property + def shared_lc_fixed_sink_label(self) -> int | None: + return self._shared_lc_fixed_sink_label + + def _shared_lc_ordered_combination_labels( + self, + left_index: CurrentIndex, + right_index: CurrentIndex, + ) -> tuple[int, ...] | None: + proposed = ( + *left_index.ordered_external_labels, + *right_index.ordered_external_labels, + ) + return self._shared_lc_ordered_proposed_labels(proposed) + + def shared_lc_ordered_proposed_labels( + self, + proposed: Iterable[int], + *, + allow_reversed: bool = False, + ) -> tuple[int, ...] | None: + if not self._shared_lc_orderings: + return None + return self._shared_lc_ordered_proposed_labels( + tuple(proposed), + allow_reversed=allow_reversed, + ) + + def _shared_lc_ordered_proposed_labels( + self, + proposed: tuple[int, ...], + *, + allow_reversed: bool = False, + ) -> tuple[int, ...] | None: + cache = self._shared_lc_proposed_labels_cache + cache_key = (proposed, allow_reversed) + if cache is not None and cache_key in cache: + return cache[cache_key] + coloured_segment = tuple( + label for label in proposed if label in self._shared_lc_coloured_labels + ) + extras = tuple( + sorted( + label + for label in proposed + if label not in self._shared_lc_coloured_labels + ) + ) + result: tuple[int, ...] | None + if extras and not set(extras).issubset(self._shared_lc_singlet_labels): + result = None + elif not coloured_segment: + result = extras + elif coloured_segment not in self._shared_lc_segments: + reversed_segment = tuple(reversed(coloured_segment)) + if not (allow_reversed and reversed_segment in self._shared_lc_segments): + result = None + else: + result = (*coloured_segment, *extras) + else: + result = (*coloured_segment, *extras) + if cache is not None: + cache[cache_key] = result + return result + + def _shared_lc_closure_word( + self, + left_index: CurrentIndex, + right_index: CurrentIndex, + ) -> tuple[int, ...] | None: + closure = self._shared_lc_closure(left_index, right_index) + return None if closure is None else closure[0] + + def _shared_lc_closure( + self, + left_index: CurrentIndex, + right_index: CurrentIndex, + ) -> tuple[tuple[int, ...], tuple[int, ...]] | None: + left_word = tuple( + label + for label in left_index.ordered_external_labels + if label in self._shared_lc_coloured_labels + ) + right_word = tuple( + label + for label in right_index.ordered_external_labels + if label in self._shared_lc_coloured_labels + ) + if not left_word or not right_word: + return None + candidates = [(*left_word, *right_word)] + if self.color_plan.color_accuracy != "lc": + candidates.append((*right_word, *left_word)) + word = next( + ( + candidate + for candidate in candidates + if candidate in self._shared_lc_sector_ids_by_word + ), + None, + ) + if word is None: + return None + sector_ids = self._shared_lc_sector_ids_by_word[word] + basis_keys = ( + *left_index.color_state.basis_key, + *right_index.color_state.basis_key, + ) + closures = _lc_color_identity_closures(basis_keys) + support = _lc_sector_support(basis_keys) + compatible_sector_ids = tuple( + sector_id + for sector_id in sector_ids + if (sector := self._sector_by_id.get(sector_id)) is not None + and (support is None or sector_id in support) + and _lc_color_identity_closures_compatible(closures, sector) + ) + if not compatible_sector_ids: + return None + return word, compatible_sector_ids diff --git a/pyamplicol/src/pyamplicol/generation/dag_compiler.py b/pyamplicol/src/pyamplicol/generation/dag_compiler.py new file mode 100644 index 0000000000..8323e61734 --- /dev/null +++ b/pyamplicol/src/pyamplicol/generation/dag_compiler.py @@ -0,0 +1,2217 @@ +# SPDX-License-Identifier: 0BSD +"""Current-table construction for generic process DAGs.""" + +from __future__ import annotations + +from collections.abc import Callable, Iterable, Mapping, Sequence +from dataclasses import replace + +from ..color.plan import GenericColorPlan, build_color_plan +from ..models._physics_ir import ContractionIR +from ..models.base import ( + CouplingOrders, + Model, + QuantumFlow, + Vertex, + VertexEvaluationEquivalence, + VertexLoweringRule, +) +from ..processes.ir import CanonicalProcessIR +from .dag_algorithms import ( + BackwardLiveStatePlan, + BackwardLiveTransition, + _canonicalize_amplitude_root_order, + _LiveCurrentShape, + _normalize_generation_cap, + _retain_canonical_open_line_alias_roots, + build_backward_live_state_plan, + prune_dag_to_amplitude_roots, +) +from .dag_color import ColorEngine +from .dag_equivalence import ( + RecursiveEvaluationReuseTracker, + _canonical_kernel_evaluation, + assign_recursive_current_evaluation_reuse, +) +from .dag_ordering import ( + _closure_candidate_splits, + _closure_combination_matches_word, + _closure_side_reachable_masks, + _complex_weight_mul, + _direct_contraction_ir, + _labels_mask, + _labels_projected_to_word, + _lc_all_adjoint_symmetry_order_variants, + _lc_color_order_reachable_masks, + _mask_labels, +) +from .dag_reachability import ( + _coupling_orders_within_limits, + _lc_line_groups_within_limit, + _mask_allowed_by_reachability, + _masks_by_size, + _normalize_coupling_order_limits, + _ordered_splits, + _right_particles_by_left, + _state_allowed_by_reachability, + _useful_states_by_mask, +) +from .dag_table import _CurrentTable +from .dag_types import ( + AmplitudeRoot, + ColorFlow, + ColorState, + CurrentIndex, + CurrentNode, + GenericDAG, + InteractionNode, +) +from .fermion_ordering import apply_fermion_ordering_signs + +DAGProgressCallback = Callable[[Mapping[str, str | int]], None] + + +def _restrict_color_plan( + color_plan: GenericColorPlan, + selected_color_sector_ids: frozenset[int] | None, +) -> tuple[GenericColorPlan, tuple[int, ...]]: + """Apply an explicit sector selection without hiding missing ids.""" + + if selected_color_sector_ids is None: + return color_plan, () + selected_sectors = tuple( + sector + for sector in color_plan.sectors + if sector.id in selected_color_sector_ids + ) + selected_ids = {sector.id for sector in selected_sectors} + missing_sector_ids = tuple(sorted(selected_color_sector_ids - selected_ids)) + diagnostics = color_plan.diagnostics + if missing_sector_ids: + diagnostics = ( + *diagnostics, + "selected colour sector ids were not materialized: " + + ", ".join(str(sector_id) for sector_id in missing_sector_ids), + ) + return ( + GenericColorPlan( + process=color_plan.process, + color_accuracy=color_plan.color_accuracy, + sectors=selected_sectors, + diagnostics=diagnostics, + truncated=color_plan.truncated or bool(missing_sector_ids), + trace_reflections_folded=color_plan.trace_reflections_folded, + basis=color_plan.basis, + ), + missing_sector_ids, + ) + + +class GenericDAGCompiler: + """Compile a concrete process into a model-driven current DAG. + + The compiler never classifies the whole process as a family. It sweeps + external subsets, asks the model which local vertices are valid for two + current particle ids, asks the colour engine whether their colour states + combine, and deduplicates solely by ``CurrentIndex`` equality. + """ + + def __init__( + self, + *, + model: Model, + color_plan: GenericColorPlan | None = None, + max_currents: int | None = None, + max_color_sectors: int | None = None, + reference_color_order: tuple[int, ...] | None = None, + selected_color_sector_ids: Iterable[int] | None = None, + max_coupling_orders: Mapping[str, int] | None = None, + max_lc_current_line_groups: int | None = None, + max_quark_pairs: int | None = None, + closure_side_mask_pruning: bool = True, + color_order_mask_pruning: bool = True, + species_reachability_pruning: bool = True, + ignored_particle_ids: Iterable[int] | None = None, + ignored_vertex_kinds: Iterable[int] | None = None, + selected_source_helicities: Mapping[int, int] | None = None, + lc_all_ordering_symmetry: bool = True, + online_evaluation_reuse: bool = False, + backward_live_planning: bool = False, + progress_callback: DAGProgressCallback | None = None, + ) -> None: + self.model = model + self.color_plan = color_plan + self.max_currents = _normalize_generation_cap(max_currents) + self.max_color_sectors = _normalize_generation_cap(max_color_sectors) + self.reference_color_order = reference_color_order + self.selected_color_sector_ids = ( + None + if selected_color_sector_ids is None + else frozenset(int(sector_id) for sector_id in selected_color_sector_ids) + ) + self.max_coupling_orders = _normalize_coupling_order_limits( + max_coupling_orders, + ) + self.max_lc_current_line_groups = ( + None + if max_lc_current_line_groups is None + else max(0, int(max_lc_current_line_groups)) + ) + self.max_quark_pairs = ( + None if max_quark_pairs is None else max(0, int(max_quark_pairs)) + ) + self.closure_side_mask_pruning = bool(closure_side_mask_pruning) + self.color_order_mask_pruning = bool(color_order_mask_pruning) + self.species_reachability_pruning = bool(species_reachability_pruning) + self.ignored_particle_ids = frozenset( + int(particle_id) for particle_id in (ignored_particle_ids or ()) + ) + self.ignored_vertex_kinds = frozenset( + int(kind) for kind in (ignored_vertex_kinds or ()) + ) + self.selected_source_helicities = ( + None + if selected_source_helicities is None + else { + int(label): int(helicity) + for label, helicity in selected_source_helicities.items() + } + ) + self.lc_all_ordering_symmetry = bool(lc_all_ordering_symmetry) + self.online_evaluation_reuse = bool(online_evaluation_reuse) + self.backward_live_planning = bool(backward_live_planning) + self.progress_callback = progress_callback + + def _report_progress(self, step: str, **details: str | int) -> None: + if self.progress_callback is None: + return + self.progress_callback({"step": step, **details}) + + def _apply_fermion_ordering(self, dag: GenericDAG) -> GenericDAG: + return apply_fermion_ordering_signs(dag, self.model) + + def compile(self, process: CanonicalProcessIR) -> GenericDAG: + if not isinstance(process, CanonicalProcessIR): + raise TypeError( + "generic DAG compilation requires a model-resolved CanonicalProcessIR" + ) + process_ir = process + self._report_progress("colour planning") + lc_trace_reflection_proven = bool( + self.lc_all_ordering_symmetry + and self.model.lc_trace_reflection_equivalence_is_proven(process_ir) + ) + color_plan = self.color_plan + if color_plan is None: + color_plan = build_color_plan( + process_ir, + color_accuracy=process_ir.color_accuracy, + max_sectors=self.max_color_sectors, + reference_color_order=self.reference_color_order, + fold_trace_reflections=lc_trace_reflection_proven, + ) + elif color_plan.process != process_ir: + raise ValueError("prebuilt color plan does not match the compiled process") + elif color_plan.color_accuracy != process_ir.color_accuracy: + raise ValueError( + "prebuilt color plan accuracy does not match the compiled process" + ) + self._report_progress( + "colour-plan", + color_sector_count=len(color_plan.sectors), + ) + helicity_coverage = ( + "selected" if self.selected_source_helicities else "complete" + ) + selected_source_helicities = tuple( + sorted((self.selected_source_helicities or {}).items()) + ) + selected_color_sector_ids = tuple(sorted(self.selected_color_sector_ids or ())) + color_coverage = ( + "selected" + if color_plan.truncated or self.selected_color_sector_ids is not None + else "complete" + ) + if ( + self.max_quark_pairs is not None + and process_ir.color_endpoints.pair_count > self.max_quark_pairs + ): + return self._apply_fermion_ordering( + GenericDAG( + process=process_ir, + color_plan=color_plan, + currents=(), + sources=(), + interactions=(), + amplitude_roots=(), + truncated=False, + helicity_coverage=helicity_coverage, + color_coverage=color_coverage, + selected_source_helicities=selected_source_helicities, + selected_color_sector_ids=selected_color_sector_ids, + ) + ) + complete_sector_ids = {sector.id for sector in color_plan.sectors} + color_plan, _missing_sector_ids = _restrict_color_plan( + color_plan, + self.selected_color_sector_ids, + ) + if ( + self.selected_color_sector_ids is not None + or {sector.id for sector in color_plan.sectors} != complete_sector_ids + ): + color_coverage = "selected" + color_engine = ColorEngine( + color_plan, + self.model, + shared_lc_all_ordering_symmetry=( + lc_trace_reflection_proven and self.selected_color_sector_ids is None + ), + cache_shared_lc_orderings=self.online_evaluation_reuse, + ) + global_flip_anchor = self._eager_global_helicity_flip_anchor(process_ir) + interactions: list[InteractionNode] = [] + interaction_keys: set[tuple[object, ...]] = set() + evaluation_group_by_key: dict[tuple[object, ...], int] = {} + evaluation_equivalence_by_kind: dict[int, VertexEvaluationEquivalence] = {} + evaluation_equivalence_for = self.model.vertex_evaluation_equivalence + equivalence_record = VertexEvaluationEquivalence + vertices_by_input: dict[tuple[int, int], tuple[Vertex, ...]] = {} + right_particles_by_left = _right_particles_by_left( + self.model, + color_accuracy=process_ir.color_accuracy, + ) + vertex_allowed_cache: dict[tuple[int, int, int, int], bool] = {} + quantum_flow_cache: dict[tuple[object, ...], tuple[QuantumFlow, ...]] = {} + coupling_order_cache: dict[ + tuple[CouplingOrders, CouplingOrders, int, tuple[int, int, int]], + CouplingOrders, + ] = {} + coupling_order_limit_cache: dict[CouplingOrders, bool] = {} + state_allowed_cache: dict[ + tuple[int, int, CouplingOrders], + bool, + ] = {} + allowed_current_ids_by_mask: dict[int, tuple[int, ...]] = {} + candidate_particles_by_mask: dict[ + int, + dict[int | None, dict[int, list[int]]], + ] = {} + candidate_right_ids_by_mask_sector_particle: dict[ + tuple[int, int | None, int], + tuple[int, ...], + ] = {} + reflection_reuse_cache: dict[int, bool] = {} + lowering_rule_cache: dict[int, VertexLoweringRule] = {} + vertex_color_weight_cache: dict[Vertex, tuple[float, float]] = {} + auxiliary_kind_cache: dict[int, str | None] = {} + duplicate_orientation_cache: dict[Vertex, bool] = {} + color_flow_cache: dict[ + tuple[ + ColorState, + ColorState, + Vertex, + tuple[int, ...], + tuple[tuple[int, ...], ...], + ], + tuple[ColorFlow, ...], + ] = {} + full_mask = _labels_mask(leg.label for leg in process_ir.legs) + eager_shared_lc_orderings = ( + color_engine.shared_lc_orderings if self.online_evaluation_reuse else False + ) + shared_lc_all_ordering_symmetry = color_engine.shared_lc_all_ordering_symmetry + adjoint_vector_labels = frozenset( + leg.label + for leg in process_ir.legs + if leg.outgoing_pdg is not None + and self.model.is_massless_adjoint_vector(int(leg.outgoing_pdg)) + ) + locally_reflectable_current_ids: set[int] = set() + closure_candidate_splits = _closure_candidate_splits( + process_ir, + self.model, + color_engine, + reference_color_order=self.reference_color_order, + ) + self._report_progress( + "reachability", + closure_splits=len(closure_candidate_splits), + ) + closure_reachable_masks = ( + _closure_side_reachable_masks( + full_mask, + closure_candidate_splits, + ) + if self.closure_side_mask_pruning + else None + ) + backward_planner_candidate = bool( + self.backward_live_planning + and process_ir.color_accuracy in {"nlc", "full"} + and not color_engine.shared_lc_orderings + and not color_engine.shared_single_trace + and len(color_plan.sectors) > 1 + ) + color_order_reachable_masks = ( + _lc_color_order_reachable_masks( + process_ir, + color_plan, + self.model, + ) + if self.color_order_mask_pruning and not backward_planner_candidate + else None + ) + useful_states_by_mask = ( + _useful_states_by_mask( + process_ir, + self.model, + color_engine, + closure_candidate_splits, + closure_reachable_masks, + color_order_reachable_masks, + max_coupling_orders=self.max_coupling_orders, + ignored_particle_ids=self.ignored_particle_ids, + ignored_vertex_kinds=self.ignored_vertex_kinds, + ) + if self.species_reachability_pruning + else None + ) + live_state_plan = ( + build_backward_live_state_plan( + process_ir, + model=self.model, + color_engine=color_engine, + closure_candidate_splits=closure_candidate_splits, + closure_reachable_masks=closure_reachable_masks, + color_order_reachable_masks=color_order_reachable_masks, + useful_states_by_mask=useful_states_by_mask, + max_coupling_orders=self.max_coupling_orders, + max_lc_current_line_groups=self.max_lc_current_line_groups, + ignored_particle_ids=self.ignored_particle_ids, + ignored_vertex_kinds=self.ignored_vertex_kinds, + selected_source_helicities=self.selected_source_helicities, + global_flip_anchor=global_flip_anchor, + ) + if self.backward_live_planning + else None + ) + if ( + live_state_plan is None + and backward_planner_candidate + and self.color_order_mask_pruning + ): + color_order_reachable_masks = _lc_color_order_reachable_masks( + process_ir, + color_plan, + self.model, + ) + useful_states_by_mask = ( + _useful_states_by_mask( + process_ir, + self.model, + color_engine, + closure_candidate_splits, + closure_reachable_masks, + color_order_reachable_masks, + max_coupling_orders=self.max_coupling_orders, + ignored_particle_ids=self.ignored_particle_ids, + ignored_vertex_kinds=self.ignored_vertex_kinds, + ) + if self.species_reachability_pruning + else None + ) + table = _CurrentTable(self.model) + current_index_factory = ( + CurrentIndex._from_trusted_values + if self.online_evaluation_reuse + else CurrentIndex + ) + sources = self._build_sources( + process_ir, + color_engine, + table, + global_flip_anchor=global_flip_anchor, + live_state_plan=live_state_plan, + ) + self._report_progress( + "source-currents", + current_count=len(table.currents), + source_count=len(sources), + ) + reuse_tracker = ( + RecursiveEvaluationReuseTracker(self.model) + if self.online_evaluation_reuse + else None + ) + if reuse_tracker is not None: + for current in table.currents: + reuse_tracker.register_source(current) + truncated = False + if any( + int(leg.outgoing_pdg or 0) in self.ignored_particle_ids + for leg in process_ir.legs + ): + return self._apply_fermion_ordering( + GenericDAG( + process=process_ir, + color_plan=color_plan, + currents=tuple(table.currents), + sources=tuple(sources), + interactions=(), + amplitude_roots=(), + truncated=False, + helicity_coverage=helicity_coverage, + color_coverage=color_coverage, + selected_source_helicities=selected_source_helicities, + selected_color_sector_ids=selected_color_sector_ids, + ) + ) + + if live_state_plan is not None: + return self._apply_fermion_ordering( + self._compile_backward_live_plan( + process_ir=process_ir, + color_plan=color_plan, + table=table, + sources=sources, + reuse_tracker=reuse_tracker, + plan=live_state_plan, + global_flip_anchor=global_flip_anchor, + helicity_coverage=helicity_coverage, + color_coverage=color_coverage, + selected_source_helicities=selected_source_helicities, + ) + ) + + def state_allowed( + mask: int, + particle_id: int, + coupling_orders: CouplingOrders, + ) -> bool: + if useful_states_by_mask is None: + return True + key = (mask, particle_id, coupling_orders) + cached = state_allowed_cache.get(key) + if cached is not None: + return cached + allowed = _state_allowed_by_reachability( + useful_states_by_mask, + mask, + particle_id, + coupling_orders, + ) + state_allowed_cache[key] = allowed + return allowed + + def combined_coupling_orders( + left_index: CurrentIndex, + right_index: CurrentIndex, + vertex: Vertex, + ) -> CouplingOrders: + key = ( + left_index.coupling_orders, + right_index.coupling_orders, + vertex.kind, + vertex.particles, + ) + cached = coupling_order_cache.get(key) + if cached is not None: + return cached + orders = self.model.combine_coupling_orders( + left_index, + right_index, + vertex, + ) + coupling_order_cache[key] = orders + return orders + + def coupling_orders_allowed(orders: CouplingOrders) -> bool: + if not self.online_evaluation_reuse: + return _coupling_orders_within_limits( + orders, + self.max_coupling_orders, + ) + cached = coupling_order_limit_cache.get(orders) + if cached is None: + cached = _coupling_orders_within_limits( + orders, + self.max_coupling_orders, + ) + coupling_order_limit_cache[orders] = cached + return cached + + def allowed_current_ids(mask: int) -> tuple[int, ...]: + cached = allowed_current_ids_by_mask.get(mask) + if cached is None: + cached = tuple( + current_id + for current_id in table.ids_by_mask(mask) + if state_allowed( + mask, + table.current(current_id).index.particle_id, + table.current(current_id).index.coupling_orders, + ) + ) + allowed_current_ids_by_mask[mask] = cached + return cached + + def all_adjoint_vector_current(index: CurrentIndex) -> bool: + labels = index.external_labels + return bool(labels) and all( + label in adjoint_vector_labels for label in labels + ) + + def reflection_reusable_current(current_id: int) -> bool: + if self.online_evaluation_reuse: + if not eager_shared_lc_orderings: + return False + cached = reflection_reuse_cache.get(current_id) + if cached is not None: + return cached + elif not color_engine.shared_lc_orderings: + return False + current = table.current(current_id) + if not all_adjoint_vector_current(current.index): + return False + reusable = ( + shared_lc_all_ordering_symmetry + or current.is_source + or current_id in locally_reflectable_current_ids + ) + if self.online_evaluation_reuse: + reflection_reuse_cache[current_id] = reusable + return reusable + + def candidate_right_ids_for_mask( + right_mask: int, + color_sector_id: int | None, + left_particle_id: int, + ) -> tuple[int, ...]: + if not self.online_evaluation_reuse: + raise AssertionError("eager candidate lookup requires online reuse") + key = (right_mask, color_sector_id, left_particle_id) + cached = candidate_right_ids_by_mask_sector_particle.get(key) + if cached is not None: + return cached + particles_by_sector = candidate_particles_by_mask.get(right_mask) + if particles_by_sector is None: + particles_by_sector = {} + for current_id in allowed_current_ids(right_mask): + index = table.currents[current_id].index + sector_id = ( + None + if eager_shared_lc_orderings + else index.color_state.sector_id + ) + ids_by_particle = particles_by_sector.get(sector_id) + if ids_by_particle is None: + ids_by_particle = {} + particles_by_sector[sector_id] = ids_by_particle + ids = ids_by_particle.get(index.particle_id) + if ids is None: + ids = [] + ids_by_particle[index.particle_id] = ids + ids.append(current_id) + candidate_particles_by_mask[right_mask] = particles_by_sector + ids_by_particle = particles_by_sector.get(color_sector_id, {}) + candidates: list[int] = [] + for particle_id in right_particles_by_left.get(left_particle_id, ()): + candidates.extend(ids_by_particle.get(particle_id, ())) + cached = tuple(candidates) + candidate_right_ids_by_mask_sector_particle[key] = cached + return cached + + def cached_auxiliary_kind(particle_id: int) -> str | None: + if particle_id in auxiliary_kind_cache: + return auxiliary_kind_cache[particle_id] + kind = self.model.auxiliary_kind(particle_id) + auxiliary_kind_cache[particle_id] = kind + return kind + + def cached_vertex_color_weight( + vertex: Vertex, + ) -> tuple[float, float]: + cached = vertex_color_weight_cache.get(vertex) + if cached is not None: + return cached + weight = self.model.vertex_color_weight( + vertex, + color_accuracy=process_ir.color_accuracy, + ) + vertex_color_weight_cache[vertex] = weight + return weight + + def combined_color_flows( + left: CurrentIndex, + right: CurrentIndex, + vertex: Vertex, + ordered_external_labels: tuple[int, ...], + sector_support_label_variants: tuple[tuple[int, ...], ...], + ) -> tuple[ColorFlow, ...]: + if not self.online_evaluation_reuse: + return color_engine.combine( + left.color_state, + right.color_state, + vertex, + ordered_external_labels=ordered_external_labels, + sector_support_label_variants=sector_support_label_variants, + ) + key = ( + left.color_state, + right.color_state, + vertex, + ordered_external_labels, + sector_support_label_variants, + ) + cached = color_flow_cache.get(key) + if cached is None: + cached = color_engine.combine( + left.color_state, + right.color_state, + vertex, + ordered_external_labels=ordered_external_labels, + sector_support_label_variants=sector_support_label_variants, + ) + color_flow_cache[key] = cached + return cached + + recursive_masks = tuple( + mask + for mask in _masks_by_size(full_mask) + if mask & (mask - 1) and mask != full_mask + ) + recursion_stage_total = max(len(process_ir.legs) - 2, 1) + for mask_index, mask in enumerate(recursive_masks, start=1): + labels = _mask_labels(mask) + self._report_progress( + "recursion", + stage_index=max(len(labels) - 1, 1), + stage_total=recursion_stage_total, + subset_size=len(labels), + mask_index=mask_index, + mask_total=len(recursive_masks), + current_count=len(table.currents), + interaction_count=len(interactions), + ) + if not _mask_allowed_by_reachability( + mask, + closure_reachable_masks, + color_order_reachable_masks, + ): + continue + if useful_states_by_mask is not None and mask not in useful_states_by_mask: + continue + mask_current_start = len(table.currents) + for left_mask, right_mask in _ordered_splits(mask): + if not ( + _mask_allowed_by_reachability( + left_mask, + closure_reachable_masks, + color_order_reachable_masks, + ) + and _mask_allowed_by_reachability( + right_mask, + closure_reachable_masks, + color_order_reachable_masks, + ) + ): + continue + if useful_states_by_mask is not None and ( + left_mask not in useful_states_by_mask + or right_mask not in useful_states_by_mask + ): + continue + left_ids = ( + allowed_current_ids(left_mask) + if self.online_evaluation_reuse + else table.ids_by_mask(left_mask) + ) + if not left_ids or not table.has_mask(right_mask): + continue + split_candidates: dict[ + tuple[int | None, int], + tuple[int, ...], + ] = {} + for left_id in left_ids: + left = table.current(left_id) + if not self.online_evaluation_reuse and not state_allowed( + left_mask, + left.index.particle_id, + left.index.coupling_orders, + ): + continue + color_sector_id = ( + None + if ( + eager_shared_lc_orderings + if self.online_evaluation_reuse + else color_engine.shared_lc_orderings + ) + else left.index.color_state.sector_id + ) + if self.online_evaluation_reuse: + candidate_key = ( + color_sector_id, + left.index.particle_id, + ) + candidate_right_ids_for_left = split_candidates.get( + candidate_key + ) + if candidate_right_ids_for_left is None: + candidate_right_ids_for_left = candidate_right_ids_for_mask( + right_mask, + color_sector_id, + left.index.particle_id, + ) + split_candidates[candidate_key] = ( + candidate_right_ids_for_left + ) + else: + possible_right_particles = right_particles_by_left.get( + left.index.particle_id + ) + if not possible_right_particles: + continue + candidate_right_ids_for_left = table.ids_by_mask_and_particles( + right_mask, + possible_right_particles, + color_sector_id=color_sector_id, + ) + if not candidate_right_ids_for_left: + continue + left_reflection_reusable = reflection_reusable_current(left_id) + for right_id in candidate_right_ids_for_left: + right = table.current(right_id) + right_reflection_reusable = reflection_reusable_current( + right_id + ) + if not self.online_evaluation_reuse and not state_allowed( + right_mask, + right.index.particle_id, + right.index.coupling_orders, + ): + continue + vertex_lookup_key = ( + left.index.particle_id, + right.index.particle_id, + ) + if vertex_lookup_key in vertices_by_input: + vertices = vertices_by_input[vertex_lookup_key] + else: + vertices = self.model.vertices_accepting( + left.index.particle_id, + right.index.particle_id, + color_accuracy=process_ir.color_accuracy, + ) + vertices_by_input[vertex_lookup_key] = vertices + for vertex in vertices: + if ( + vertex.kind in self.ignored_vertex_kinds + or vertex.particles[2] in self.ignored_particle_ids + ): + continue + if useful_states_by_mask is not None and vertex.particles[ + 2 + ] not in useful_states_by_mask.get(mask, {}): + continue + vertex_key = (vertex.kind, *vertex.particles) + if vertex_key in vertex_allowed_cache: + vertex_allowed = vertex_allowed_cache[vertex_key] + else: + vertex_allowed = color_engine.vertex_allowed(vertex) + vertex_allowed_cache[vertex_key] = vertex_allowed + if not vertex_allowed: + continue + if self.online_evaluation_reuse: + duplicate_orientation = duplicate_orientation_cache.get( + vertex + ) + if duplicate_orientation is None: + duplicate_orientation = ( + self.model.skip_duplicate_vertex_orientation( + vertex + ) + ) + duplicate_orientation_cache[vertex] = ( + duplicate_orientation + ) + else: + duplicate_orientation = ( + self.model.skip_duplicate_vertex_orientation(vertex) + ) + if duplicate_orientation: + continue + coupling_orders = combined_coupling_orders( + left.index, + right.index, + vertex, + ) + if not coupling_orders_allowed(coupling_orders): + continue + if not state_allowed( + mask, + vertex.particles[2], + coupling_orders, + ): + continue + local_two_source_reflection = ( + ( + eager_shared_lc_orderings + if self.online_evaluation_reuse + else color_engine.shared_lc_orderings + ) + and left.is_source + and right.is_source + and len(left.index.external_labels) == 1 + and len(right.index.external_labels) == 1 + and left_reflection_reusable + and right_reflection_reusable + and self.model.adjoint_current_reflection_phase(vertex) + == (-1.0, 0.0) + ) + result_reflection_proven = ( + shared_lc_all_ordering_symmetry + and all_adjoint_vector_current(left.index) + and all_adjoint_vector_current(right.index) + ) or local_two_source_reflection + if result_reflection_proven and max( + left.index.external_labels + ) >= max(right.index.external_labels): + continue + ordered_external_labels = ( + color_engine.ordered_combination_labels( + left.index, + right.index, + vertex, + ) + ) + if ordered_external_labels is None and not ( + left_reflection_reusable or right_reflection_reusable + ): + continue + order_variants: tuple[ + tuple[ + tuple[int, ...], + tuple[tuple[int, ...], ...], + tuple[float, float], + ], + ..., + ] + if left_reflection_reusable or right_reflection_reusable: + variants: list[ + tuple[ + tuple[int, ...], + tuple[tuple[int, ...], ...], + tuple[float, float], + ] + ] = [] + for ( + proposed_labels, + symmetry_weight, + ) in _lc_all_adjoint_symmetry_order_variants( + left.index.ordered_external_labels, + right.index.ordered_external_labels, + left_all_adjoint=left_reflection_reusable, + right_all_adjoint=right_reflection_reusable, + result_reflection_proven=(result_reflection_proven), + fold_result_reflections=( + color_coverage == "complete" + ), + ): + projected = ( + color_engine.shared_lc_ordered_proposed_labels( + proposed_labels, + allow_reversed=result_reflection_proven, + ) + ) + if projected is None: + continue + support_label_variants = (proposed_labels,) + if ( + result_reflection_proven + and color_coverage == "complete" + ): + reflected_labels = tuple( + reversed(proposed_labels) + ) + if reflected_labels != proposed_labels: + support_label_variants = ( + proposed_labels, + reflected_labels, + ) + variants.append( + ( + projected, + support_label_variants, + symmetry_weight, + ) + ) + order_variants = tuple(variants) + if not order_variants: + continue + else: + if ordered_external_labels is None: + continue + order_variants = ( + ( + ordered_external_labels, + (ordered_external_labels,), + (1.0, 0.0), + ), + ) + quantum_flow_key = ( + vertex.kind, + vertex.particles, + left.index.particle_id, + left.index.chirality, + left.index.flavour_flow, + right.index.particle_id, + right.index.chirality, + right.index.flavour_flow, + ) + if quantum_flow_key in quantum_flow_cache: + quantum_flows = quantum_flow_cache[quantum_flow_key] + else: + quantum_flows = self.model.allowed_quantum_flows( + vertex, + left.index, + right.index, + ) + quantum_flow_cache[quantum_flow_key] = quantum_flows + for quantum_flow in quantum_flows: + for variant_index, ( + variant_ordered_labels, + sector_support_label_variants, + symmetry_weight, + ) in enumerate(order_variants): + for color_flow in combined_color_flows( + left.index, + right.index, + vertex, + variant_ordered_labels, + sector_support_label_variants, + ): + if not _lc_line_groups_within_limit( + color_flow.state, + self.max_lc_current_line_groups, + ): + continue + out_index = current_index_factory( + particle_id=vertex.particles[2], + external_mask=mask, + external_labels=labels, + ordered_external_labels=variant_ordered_labels, + helicity_ancestry=( + left.index.helicity_ancestry + | right.index.helicity_ancestry + ), + chirality=quantum_flow.chirality, + spin_state=quantum_flow.spin_state, + flavour_flow=quantum_flow.flavour_flow, + quantum_number_flow=( + quantum_flow.quantum_number_flow + ), + color_state=color_flow.state, + momentum_mask=( + left.index.momentum_mask + | right.index.momentum_mask + ), + coupling_orders=coupling_orders, + auxiliary_kind=( + cached_auxiliary_kind( + vertex.particles[2] + ) + if self.online_evaluation_reuse + else self.model.auxiliary_kind( + vertex.particles[2] + ) + ), + ) + if ( + live_state_plan is not None + and not live_state_plan.allows(out_index) + ): + continue + if not self.model.current_allowed(out_index): + continue + result = table.add_or_get( + out_index, + is_source=False, + ) + if local_two_source_reflection: + locally_reflectable_current_ids.add( + result.id + ) + signed_color_weight = _complex_weight_mul( + _complex_weight_mul( + color_flow.weight, + cached_vertex_color_weight(vertex) + if self.online_evaluation_reuse + else self.model.vertex_color_weight( + vertex, + color_accuracy=( + process_ir.color_accuracy + ), + ), + ), + symmetry_weight, + ) + key = ( + vertex.kind, + left_id, + right_id, + result.id, + variant_index, + signed_color_weight, + ) + if key in interaction_keys: + continue + interaction_keys.add(key) + if self.online_evaluation_reuse: + rule = lowering_rule_cache.get(vertex.kind) + if rule is None: + rule = self.model.vertex_lowering_rule( + vertex.kind + ) + lowering_rule_cache[vertex.kind] = rule + else: + rule = self.model.vertex_lowering_rule( + vertex.kind + ) + if reuse_tracker is not None: + ( + evaluation_group_id, + evaluation_factor, + ) = reuse_tracker.interaction_evaluation( + vertex_kind=vertex.kind, + vertex_particles=vertex.particles, + left_id=left_id, + right_id=right_id, + result=result, + coupling=quantum_flow.coupling, + color_weight=signed_color_weight, + ) + else: + equivalence = ( + evaluation_equivalence_by_kind.get( + vertex.kind + ) + ) + if equivalence is None: + equivalence = ( + evaluation_equivalence_for( + vertex.kind + ) + ) + if not equivalence.verified: + model_type = ( + f"{type(self.model).__module__}." + f"{type(self.model).__qualname__}" + ) + equivalence = equivalence_record( + class_id=( + f"{model_type}:" + f"{int(vertex.kind)}" + ) + ) + evaluation_equivalence_by_kind[ + vertex.kind + ] = equivalence + ( + canonical_inputs, + evaluation_factor, + ) = _canonical_kernel_evaluation( + equivalence, + left_id, + right_id, + ) + evaluation_key = ( + equivalence.class_id, + canonical_inputs, + int(result.index.particle_id), + int(result.index.chirality), + quantum_flow.coupling, + ) + existing_group_id = ( + evaluation_group_by_key.get( + evaluation_key + ) + ) + if existing_group_id is None: + existing_group_id = len( + evaluation_group_by_key + ) + evaluation_group_by_key[ + evaluation_key + ] = existing_group_id + evaluation_group_id = existing_group_id + interactions.append( + InteractionNode( + id=len(interactions), + vertex_kind=vertex.kind, + vertex_particles=vertex.particles, + left_id=left_id, + right_id=right_id, + result_id=result.id, + coupling=quantum_flow.coupling, + color_weight=signed_color_weight, + lowering_backend=rule.backend, + full_tensor_network_ready=( + rule.full_tensor_network_ready + ), + evaluation_group_id=( + evaluation_group_id + ), + evaluation_factor=evaluation_factor, + ) + ) + if ( + self.max_currents is not None + and len(table.currents) > self.max_currents + ): + truncated = True + return self._apply_fermion_ordering( + GenericDAG( + process=process_ir, + color_plan=color_plan, + currents=tuple(table.currents), + sources=tuple(sources), + interactions=tuple(interactions), + amplitude_roots=tuple( + self._build_amplitude_roots( + process_ir, + table, + color_engine, + candidate_splits=closure_candidate_splits, + ) + ), + truncated=truncated, + helicity_coverage=helicity_coverage, + color_coverage=color_coverage, + selected_source_helicities=( + selected_source_helicities + ), + selected_color_sector_ids=( + selected_color_sector_ids + ), + ) + ) + + if reuse_tracker is not None: + reuse_tracker.finalize_currents( + table.currents[mask_current_start:], + ) + + self._report_progress( + "amplitude-closure", + current_count=len(table.currents), + interaction_count=len(interactions), + ) + amplitude_roots = tuple( + self._build_amplitude_roots( + process_ir, + table, + color_engine, + candidate_splits=closure_candidate_splits, + ) + ) + if global_flip_anchor is not None: + amplitude_roots = tuple( + replace( + root, + helicity_weight=2.0 * float(root.helicity_weight), + ) + for root in amplitude_roots + ) + dag = GenericDAG( + process=process_ir, + color_plan=color_plan, + currents=tuple(table.currents), + sources=tuple(sources), + interactions=tuple(interactions), + amplitude_roots=amplitude_roots, + truncated=truncated, + helicity_coverage=helicity_coverage, + color_coverage=color_coverage, + selected_source_helicities=selected_source_helicities, + selected_color_sector_ids=selected_color_sector_ids, + ) + dag = _retain_canonical_open_line_alias_roots(dag, self.model) + if global_flip_anchor is not None: + dag = _canonicalize_amplitude_root_order(dag) + if reuse_tracker is not None: + self._report_progress( + "symmetry-reuse", + current_count=len(dag.currents), + interaction_count=len(dag.interactions), + amplitude_count=len(dag.amplitude_roots), + ) + return self._apply_fermion_ordering(dag) + dag = assign_recursive_current_evaluation_reuse(dag, self.model) + self._report_progress( + "symmetry-reuse", + current_count=len(dag.currents), + interaction_count=len(dag.interactions), + amplitude_count=len(dag.amplitude_roots), + ) + return self._apply_fermion_ordering(dag) + + def _compile_backward_live_plan( + self, + *, + process_ir: CanonicalProcessIR, + color_plan: GenericColorPlan, + table: _CurrentTable, + sources: Sequence[int], + reuse_tracker: RecursiveEvaluationReuseTracker | None, + plan: BackwardLiveStatePlan, + global_flip_anchor: tuple[int, int] | None, + helicity_coverage: str, + color_coverage: str, + selected_source_helicities: tuple[tuple[int, int], ...], + ) -> GenericDAG: + """Replay certified live transition templates over exact helicities. + + The backward planner has already performed model, colour, coupling, + and quantum-flow resolution. Replay only expands independent source + helicity ancestries and records the ordinary current/interactions DTOs. + No backend evaluator or prepared-kernel operation occurs here. + """ + + if len(plan.sources) != len(sources): + raise ValueError("backward-live source plan does not match source table") + current_index_factory = ( + CurrentIndex._from_trusted_values + if self.online_evaluation_reuse + else CurrentIndex + ) + ids_by_shape: dict[_LiveCurrentShape, dict[int, int]] = {} + for source, current_id in zip(plan.sources, sources, strict=True): + ids_by_shape.setdefault(source.shape, {})[source.helicity_ancestry] = ( + current_id + ) + transitions_by_mask: dict[int, list[BackwardLiveTransition]] = {} + for transition in plan.transitions: + transitions_by_mask.setdefault( + transition.result.external_mask, + [], + ).append(transition) + + interactions: list[InteractionNode] = [] + interaction_keys: set[tuple[object, ...]] = set() + lowering_rules: dict[int, VertexLoweringRule] = {} + auxiliary_kinds: dict[int, str | None] = {} + full_mask = _labels_mask(leg.label for leg in process_ir.legs) + truncated = False + + def replay_transition( + transition: BackwardLiveTransition, + left_id: int, + right_id: int, + ) -> None: + nonlocal truncated + left = table.current(left_id) + right = table.current(right_id) + rule = lowering_rules.get(transition.vertex_kind) + if rule is None: + rule = self.model.vertex_lowering_rule(transition.vertex_kind) + lowering_rules[transition.vertex_kind] = rule + result_ancestry = int( + left.index.helicity_ancestry | right.index.helicity_ancestry + ) + result_by_ancestry = ids_by_shape.setdefault(transition.result, {}) + result_id = result_by_ancestry.get(result_ancestry) + if result_id is None: + particle_id = transition.result.particle_id + if particle_id not in auxiliary_kinds: + auxiliary_kinds[particle_id] = self.model.auxiliary_kind( + particle_id + ) + result = table.add_or_get( + current_index_factory( + particle_id=particle_id, + external_mask=transition.result.external_mask, + external_labels=transition.result.external_labels, + ordered_external_labels=( + transition.result.ordered_external_labels + ), + helicity_ancestry=result_ancestry, + chirality=transition.result.chirality, + spin_state=transition.result.spin_state, + flavour_flow=transition.result.flavour_flow, + quantum_number_flow=(transition.result.quantum_number_flow), + color_state=transition.result.color_state, + momentum_mask=transition.result.external_mask, + coupling_orders=transition.result.coupling_orders, + auxiliary_kind=auxiliary_kinds[particle_id], + ), + is_source=False, + ) + result_by_ancestry[result_ancestry] = result.id + else: + result = table.current(result_id) + key = ( + transition.vertex_kind, + left_id, + right_id, + result.id, + 0, + transition.color_weight, + ) + if key in interaction_keys: + return + interaction_keys.add(key) + if reuse_tracker is None: + evaluation_group_id = None + evaluation_factor = (1.0, 0.0) + else: + ( + evaluation_group_id, + evaluation_factor, + ) = reuse_tracker.interaction_evaluation( + vertex_kind=transition.vertex_kind, + vertex_particles=transition.vertex_particles, + left_id=left_id, + right_id=right_id, + result=result, + coupling=transition.coupling, + color_weight=transition.color_weight, + ) + interactions.append( + InteractionNode( + id=len(interactions), + vertex_kind=transition.vertex_kind, + vertex_particles=transition.vertex_particles, + left_id=left_id, + right_id=right_id, + result_id=result.id, + coupling=transition.coupling, + color_weight=transition.color_weight, + lowering_backend=rule.backend, + full_tensor_network_ready=rule.full_tensor_network_ready, + evaluation_group_id=evaluation_group_id, + evaluation_factor=evaluation_factor, + ) + ) + if ( + self.max_currents is not None + and len(table.currents) > self.max_currents + ): + truncated = True + + recursive_masks = tuple( + mask for mask in _masks_by_size(full_mask) if mask in transitions_by_mask + ) + recursion_stage_total = max(len(process_ir.legs) - 2, 1) + for mask_index, mask in enumerate(recursive_masks, start=1): + stage_transitions = transitions_by_mask[mask] + labels = _mask_labels(mask) + self._report_progress( + "recursion", + stage_index=max(len(labels) - 1, 1), + stage_total=recursion_stage_total, + subset_size=len(labels), + mask_index=mask_index, + mask_total=len(recursive_masks), + current_count=len(table.currents), + interaction_count=len(interactions), + ) + mask_current_start = len(table.currents) + transitions_by_split: dict[ + tuple[int, int], + list[BackwardLiveTransition], + ] = {} + for transition in stage_transitions: + transitions_by_split.setdefault( + ( + transition.left.external_mask, + transition.right.external_mask, + ), + [], + ).append(transition) + for split_transitions in transitions_by_split.values(): + transitions_by_left: dict[ + _LiveCurrentShape, + list[BackwardLiveTransition], + ] = {} + for transition in split_transitions: + transitions_by_left.setdefault(transition.left, []).append( + transition + ) + exact_left_currents = sorted( + ( + (left_id, left_shape) + for left_shape in transitions_by_left + for left_id in ids_by_shape.get(left_shape, {}).values() + ), + key=lambda entry: entry[0], + ) + for left_id, left_shape in exact_left_currents: + for transition in transitions_by_left[left_shape]: + right_by_ancestry = ids_by_shape.get(transition.right) + if not right_by_ancestry: + continue + for right_id in sorted(right_by_ancestry.values()): + replay_transition(transition, left_id, right_id) + if truncated: + break + if truncated: + break + if truncated: + break + if truncated: + break + if reuse_tracker is not None: + reuse_tracker.finalize_currents(table.currents[mask_current_start:]) + if truncated: + break + + self._report_progress( + "amplitude-closure", + current_count=len(table.currents), + interaction_count=len(interactions), + ) + amplitude_roots_list: list[AmplitudeRoot] = [] + seen_roots: set[tuple[object, ...]] = set() + for closure in plan.closures: + left_by_ancestry = ids_by_shape.get(closure.left) + right_by_ancestry = ids_by_shape.get(closure.right) + if not left_by_ancestry or not right_by_ancestry: + continue + for left_id in left_by_ancestry.values(): + for right_id in right_by_ancestry.values(): + root_key = ( + closure.kind, + closure.contraction_ir, + left_id, + right_id, + closure.color_sector_id, + closure.vertex_kind, + closure.vertex_particles, + closure.color_weight, + ) + if root_key in seen_roots: + continue + seen_roots.add(root_key) + amplitude_roots_list.append( + AmplitudeRoot( + id=len(amplitude_roots_list), + kind=closure.kind, + left_id=left_id, + right_id=right_id, + color_weight=closure.color_weight, + contraction_ir=closure.contraction_ir, + color_sector_id=closure.color_sector_id, + vertex_kind=closure.vertex_kind, + vertex_particles=closure.vertex_particles, + coupling=closure.coupling, + ) + ) + amplitude_roots = tuple(amplitude_roots_list) + if global_flip_anchor is not None: + amplitude_roots = tuple( + replace( + root, + helicity_weight=2.0 * float(root.helicity_weight), + ) + for root in amplitude_roots + ) + dag = GenericDAG( + process=process_ir, + color_plan=color_plan, + currents=tuple(table.currents), + sources=tuple(sources), + interactions=tuple(interactions), + amplitude_roots=amplitude_roots, + truncated=truncated, + helicity_coverage=helicity_coverage, + color_coverage=color_coverage, + selected_source_helicities=selected_source_helicities, + selected_color_sector_ids=tuple( + sorted(self.selected_color_sector_ids or ()) + ), + ) + dag = _retain_canonical_open_line_alias_roots(dag, self.model) + dag = prune_dag_to_amplitude_roots(dag) + if global_flip_anchor is not None: + dag = _canonicalize_amplitude_root_order(dag) + if reuse_tracker is not None: + self._report_progress( + "symmetry-reuse", + current_count=len(dag.currents), + interaction_count=len(dag.interactions), + amplitude_count=len(dag.amplitude_roots), + ) + return dag + dag = assign_recursive_current_evaluation_reuse(dag, self.model) + self._report_progress( + "symmetry-reuse", + current_count=len(dag.currents), + interaction_count=len(dag.interactions), + amplitude_count=len(dag.amplitude_roots), + ) + return dag + + def _build_sources( + self, + process_ir: CanonicalProcessIR, + color_engine: ColorEngine, + table: _CurrentTable, + *, + global_flip_anchor: tuple[int, int] | None = None, + live_state_plan: BackwardLiveStatePlan | None = None, + ) -> list[int]: + current_index_factory = ( + CurrentIndex._from_trusted_values + if self.online_evaluation_reuse + else CurrentIndex + ) + if live_state_plan is not None: + sources: list[int] = [] + auxiliary_kinds: dict[int, str | None] = {} + for source in live_state_plan.sources: + shape = source.shape + particle_id = shape.particle_id + if particle_id not in auxiliary_kinds: + auxiliary_kinds[particle_id] = self.model.auxiliary_kind( + particle_id + ) + current = table.add_or_get( + current_index_factory( + particle_id=particle_id, + external_mask=shape.external_mask, + external_labels=shape.external_labels, + ordered_external_labels=shape.ordered_external_labels, + helicity_ancestry=source.helicity_ancestry, + chirality=shape.chirality, + spin_state=shape.spin_state, + flavour_flow=shape.flavour_flow, + quantum_number_flow=shape.quantum_number_flow, + color_state=shape.color_state, + momentum_mask=shape.external_mask, + coupling_orders=shape.coupling_orders, + auxiliary_kind=auxiliary_kinds[particle_id], + ), + is_source=True, + source_leg_label=source.leg_label, + source_helicity=source.source_helicity, + ) + sources.append(current.id) + return sources + + sources: list[int] = [] + next_source_bit = 0 + for leg in process_ir.legs: + if leg.outgoing_pdg is None: + continue + particle_id = int(leg.outgoing_pdg) + source_ir = self.model._source_ir(particle_id) + source_quantum_flow = ( + self.model.quantum_number_flow(particle_id) + if live_state_plan is not None + else None + ) + source_auxiliary_kind = ( + self.model.auxiliary_kind(particle_id) + if live_state_plan is not None + else None + ) + for color_state in color_engine.source_states_for_leg(leg): + if not _lc_line_groups_within_limit( + color_state, + self.max_lc_current_line_groups, + ): + continue + color_state_is_live = ( + live_state_plan is None + or color_state.sector_id in live_state_plan.active_sector_ids + ) + for declared_state in source_ir.states: + source_state = ( + source_ir.crossing.apply(declared_state) + if leg.is_initial + else declared_state + ) + chirality = source_state.chirality + source_helicity = source_state.helicity + spin_state = source_state.spin_state + if ( + global_flip_anchor is not None + and leg.label == global_flip_anchor[0] + and int(source_helicity) != global_flip_anchor[1] + ): + # Preserve the ancestry bit positions of the complete + # source basis. This keeps the retained half directly + # comparable with the existing post-DAG parity prune. + next_source_bit += 1 + continue + if ( + self.selected_source_helicities is not None + and ( + requested_helicity := self.selected_source_helicities.get( + leg.label + ) + ) + is not None + and int(source_helicity) != requested_helicity + ): + continue + helicity_ancestry = 1 << next_source_bit + next_source_bit += 1 + if not color_state_is_live: + continue + if live_state_plan is not None: + assert source_quantum_flow is not None + source_shape = _LiveCurrentShape( + external_mask=1 << (leg.label - 1), + external_labels=(leg.label,), + particle_id=particle_id, + ordered_external_labels=(leg.label,), + color_state=color_state, + coupling_orders=(), + chirality=chirality, + spin_state=spin_state, + flavour_flow=(particle_id,), + quantum_number_flow=source_quantum_flow, + ) + if source_shape not in live_state_plan.shapes: + continue + index = current_index_factory( + particle_id=particle_id, + external_mask=1 << (leg.label - 1), + external_labels=(leg.label,), + ordered_external_labels=(leg.label,), + helicity_ancestry=helicity_ancestry, + chirality=chirality, + spin_state=spin_state, + flavour_flow=(particle_id,), + quantum_number_flow=( + source_quantum_flow + if source_quantum_flow is not None + else self.model.quantum_number_flow(particle_id) + ), + color_state=color_state, + momentum_mask=1 << (leg.label - 1), + coupling_orders=(), + auxiliary_kind=( + source_auxiliary_kind + if live_state_plan is not None + else self.model.auxiliary_kind(particle_id) + ), + ) + current = table.add_or_get( + index, + is_source=True, + source_leg_label=leg.label, + source_helicity=source_helicity, + ) + sources.append(current.id) + return sources + + def _eager_global_helicity_flip_anchor( + self, + process_ir: CanonicalProcessIR, + ) -> tuple[int, int] | None: + """Return one safely fixed source helicity for eager parity pairing. + + Eager mode can avoid constructing both members of a global-helicity- + flip pair when the same model proof used by the structural post-pass is + available before recursion. The proof inventory is conservative: it + includes every non-ignored model vertex admitted by the requested + coupling-order limits, not only vertices expected for this process. + """ + + if ( + not self.online_evaluation_reuse + or self.selected_source_helicities + or process_ir.color_accuracy not in {"lc", "nlc", "full"} + ): + return None + + has_fundamental = False + for leg in process_ir.legs: + if leg.outgoing_pdg is None: + return None + particle_id = int(leg.outgoing_pdg) + is_fundamental = self.model.is_fundamental_colored_fermion(particle_id) + if not ( + is_fundamental or self.model.is_massless_adjoint_vector(particle_id) + ): + return None + if self.model.mass(particle_id) != 0.0: + return None + has_fundamental = has_fundamental or is_fundamental + # Pure-adjoint trees also use the stronger all-equal/one-opposite zero + # theorem. Leave them to the existing combined proof pass for now. + if not has_fundamental: + return None + + proof_vertices = tuple( + vertex + for vertex in self.model.vertices + if vertex.kind not in self.ignored_vertex_kinds + and not any( + particle_id in self.ignored_particle_ids + for particle_id in vertex.particles + ) + and _coupling_orders_within_limits( + self.model.vertex_coupling_orders(vertex), + self.max_coupling_orders, + ) + ) + if ( + not proof_vertices + or not self.model.global_helicity_flip_equivalence_is_proven(proof_vertices) + ): + return None + + for leg in process_ir.legs: + assert leg.outgoing_pdg is not None + source_ir = self.model._source_ir(int(leg.outgoing_pdg)) + helicities = sorted( + { + int( + ( + source_ir.crossing.apply(state) if leg.is_initial else state + ).helicity + ) + for state in source_ir.states + } + ) + if ( + len(helicities) == 2 + and helicities[0] != 0 + and helicities[0] == -helicities[1] + ): + return int(leg.label), helicities[0] + return None + + def _build_amplitude_roots( + self, + process_ir: CanonicalProcessIR, + table: _CurrentTable, + color_engine: ColorEngine, + *, + candidate_splits: tuple[tuple[int, int], ...] | None = None, + ) -> list[AmplitudeRoot]: + full_mask = _labels_mask(leg.label for leg in process_ir.legs) + if candidate_splits is None: + candidate_splits = _closure_candidate_splits( + process_ir, + self.model, + color_engine, + ) + roots: list[AmplitudeRoot] = [] + seen: set[tuple[object, ...]] = set() + right_ids_by_mask_sector: dict[tuple[int, int], tuple[int, ...]] = {} + eager_color_flows: dict[ + tuple[ + tuple[int, ...], + tuple[int, ...], + ColorState, + ColorState, + ], + tuple[ColorFlow, ...], + ] = {} + eager_direct_contractions: dict[ + tuple[int, int, int, int], + ContractionIR | None, + ] = {} + eager_vertices: dict[tuple[int, int], tuple[Vertex, ...]] = {} + eager_vertex_allowed: dict[Vertex, bool] = {} + eager_vertex_closure_allowed: dict[Vertex, bool] = {} + eager_vertex_orders: dict[ + tuple[CouplingOrders, CouplingOrders, Vertex], + CouplingOrders, + ] = {} + eager_vertex_quantum_flows: dict[ + tuple[object, ...], + tuple[QuantumFlow, ...], + ] = {} + eager_closure_contractions: dict[int, ContractionIR | None] = {} + eager_vertex_color_weights: dict[Vertex, tuple[float, float]] = {} + + def closure_color_flows( + left: CurrentNode, + right: CurrentNode, + ) -> tuple[ColorFlow, ...]: + if not self.online_evaluation_reuse: + if not color_engine.ordered_closure_allowed(left.index, right.index): + return () + if color_engine.shared_single_trace: + return color_engine.shared_single_trace_closure_flows( + left.index, + right.index, + ) + if color_engine.shared_lc_orderings: + return color_engine.shared_lc_closure_flows( + left.index, + right.index, + ) + return color_engine.closure_compatible( + left.index.color_state, + right.index.color_state, + full_mask=full_mask, + ) + key = ( + left.index.ordered_external_labels, + right.index.ordered_external_labels, + left.index.color_state, + right.index.color_state, + ) + cached = eager_color_flows.get(key) + if cached is not None: + return cached + if color_engine.shared_single_trace: + resolved = color_engine.shared_single_trace_closure_flows( + left.index, + right.index, + ) + elif color_engine.shared_lc_orderings: + resolved = color_engine.shared_lc_closure_flows( + left.index, + right.index, + ) + elif color_engine.ordered_closure_allowed(left.index, right.index): + resolved = color_engine.closure_compatible( + left.index.color_state, + right.index.color_state, + full_mask=full_mask, + ) + else: + resolved = () + eager_color_flows[key] = resolved + return resolved + + def direct_contraction( + left: CurrentNode, + right: CurrentNode, + ) -> ContractionIR | None: + if not self.online_evaluation_reuse: + return _direct_contraction_ir(self.model, left.index, right.index) + key = ( + left.index.particle_id, + right.index.particle_id, + left.index.chirality, + right.index.chirality, + ) + if key not in eager_direct_contractions: + eager_direct_contractions[key] = _direct_contraction_ir( + self.model, + left.index, + right.index, + ) + return eager_direct_contractions[key] + + def closure_right_ids( + right_mask: int, + left: CurrentNode, + ) -> Sequence[int]: + if ( + not self.online_evaluation_reuse + or color_engine.shared_single_trace + or color_engine.shared_lc_orderings + ): + return table.ids_by_mask(right_mask) + key = (right_mask, left.index.color_state.sector_id) + cached = right_ids_by_mask_sector.get(key) + if cached is None: + sector_id = key[1] + cached = tuple( + current_id + for current_id in table.ids_by_mask(right_mask) + if table.current(current_id).index.color_state.sector_id + == sector_id + ) + right_ids_by_mask_sector[key] = cached + return cached + + def closure_vertices( + left: CurrentNode, + right: CurrentNode, + ) -> tuple[Vertex, ...]: + key = (left.index.particle_id, right.index.particle_id) + if not self.online_evaluation_reuse: + return self.model.vertices_accepting( + *key, + color_accuracy=process_ir.color_accuracy, + ) + cached = eager_vertices.get(key) + if cached is None: + cached = self.model.vertices_accepting( + *key, + color_accuracy=process_ir.color_accuracy, + ) + eager_vertices[key] = cached + return cached + + def closure_vertex_allowed(vertex: Vertex) -> bool: + if not self.online_evaluation_reuse: + return color_engine.vertex_allowed(vertex) + cached = eager_vertex_allowed.get(vertex) + if cached is None: + cached = color_engine.vertex_allowed(vertex) + eager_vertex_allowed[vertex] = cached + return cached + + def model_vertex_closure_allowed(vertex: Vertex) -> bool: + if not self.online_evaluation_reuse: + return self.model.vertex_closure_allowed(vertex) + cached = eager_vertex_closure_allowed.get(vertex) + if cached is None: + cached = self.model.vertex_closure_allowed(vertex) + eager_vertex_closure_allowed[vertex] = cached + return cached + + def closure_coupling_orders( + left: CurrentNode, + right: CurrentNode, + vertex: Vertex, + ) -> CouplingOrders: + if not self.online_evaluation_reuse: + return self.model.combine_coupling_orders( + left.index, + right.index, + vertex, + ) + key = ( + left.index.coupling_orders, + right.index.coupling_orders, + vertex, + ) + cached = eager_vertex_orders.get(key) + if cached is None: + cached = self.model.combine_coupling_orders( + left.index, + right.index, + vertex, + ) + eager_vertex_orders[key] = cached + return cached + + def closure_quantum_flows( + left: CurrentNode, + right: CurrentNode, + vertex: Vertex, + ) -> tuple[QuantumFlow, ...]: + if not self.online_evaluation_reuse: + return self.model.allowed_quantum_flows( + vertex, + left.index, + right.index, + ) + key = ( + vertex, + left.index.particle_id, + left.index.chirality, + left.index.spin_state, + left.index.flavour_flow, + left.index.quantum_number_flow, + right.index.particle_id, + right.index.chirality, + right.index.spin_state, + right.index.flavour_flow, + right.index.quantum_number_flow, + ) + cached = eager_vertex_quantum_flows.get(key) + if cached is None: + cached = self.model.allowed_quantum_flows( + vertex, + left.index, + right.index, + ) + eager_vertex_quantum_flows[key] = cached + return cached + + def closure_contraction(vertex: Vertex) -> ContractionIR | None: + particle_id = vertex.particles[2] + if not self.online_evaluation_reuse: + return self.model.closure_contraction_ir(particle_id) + if particle_id not in eager_closure_contractions: + eager_closure_contractions[particle_id] = ( + self.model.closure_contraction_ir(particle_id) + ) + return eager_closure_contractions[particle_id] + + def closure_vertex_color_weight(vertex: Vertex) -> tuple[float, float]: + if not self.online_evaluation_reuse: + return self.model.vertex_color_weight( + vertex, + color_accuracy=process_ir.color_accuracy, + ) + cached = eager_vertex_color_weights.get(vertex) + if cached is None: + cached = self.model.vertex_color_weight( + vertex, + color_accuracy=process_ir.color_accuracy, + ) + eager_vertex_color_weights[vertex] = cached + return cached + + for left_mask, right_mask in candidate_splits: + if left_mask == 0 or right_mask == 0: + continue + for left_id in table.ids_by_mask(left_mask): + left = table.current(left_id) + for right_id in closure_right_ids(right_mask, left): + right = table.current(right_id) + if left.index.overlaps(right.index): + continue + color_flows = closure_color_flows(left, right) + if not color_flows: + continue + if ( + self.reference_color_order is not None + and process_ir.color_accuracy == "lc" + ): + sector = color_engine.color_plan.sector( + left.index.color_state.sector_id + ) + if sector is not None and ( + self.reference_color_order + in sector.admissible_traversal_words + ): + closure_word = sector.canonical_closure_traversal_word( + process_ir + ) + if not _closure_combination_matches_word( + _labels_projected_to_word( + left.index.ordered_external_labels, + closure_word, + ), + _labels_projected_to_word( + right.index.ordered_external_labels, + closure_word, + ), + closure_word, + ): + continue + direct_contraction_ir = direct_contraction(left, right) + for color_flow in color_flows: + if direct_contraction_ir is not None: + direct_key: tuple[object, ...] = ( + "direct", + direct_contraction_ir, + left_id, + right_id, + color_flow.state, + ) + if direct_key not in seen: + seen.add(direct_key) + roots.append( + AmplitudeRoot( + id=len(roots), + kind="direct-contraction", + left_id=left_id, + right_id=right_id, + color_weight=color_flow.weight, + contraction_ir=direct_contraction_ir, + color_sector_id=color_flow.state.sector_id, + ) + ) + for vertex in closure_vertices(left, right): + if ( + vertex.kind in self.ignored_vertex_kinds + or vertex.particles[2] in self.ignored_particle_ids + ): + continue + if not closure_vertex_allowed(vertex): + continue + if not model_vertex_closure_allowed(vertex): + continue + coupling_orders = closure_coupling_orders( + left, + right, + vertex, + ) + if not _coupling_orders_within_limits( + coupling_orders, + self.max_coupling_orders, + ): + continue + closure_contraction_ir = closure_contraction(vertex) + if closure_contraction_ir is None: + continue + quantum_flows = closure_quantum_flows( + left, + right, + vertex, + ) + if not quantum_flows: + continue + vertex_key: tuple[object, ...] = ( + "vertex", + vertex.kind, + vertex.particles, + left_id, + right_id, + color_flow.state, + ) + if vertex_key in seen: + continue + seen.add(vertex_key) + roots.append( + AmplitudeRoot( + id=len(roots), + kind="vertex-closure", + left_id=left_id, + right_id=right_id, + color_weight=_complex_weight_mul( + color_flow.weight, + closure_vertex_color_weight(vertex), + ), + contraction_ir=closure_contraction_ir, + color_sector_id=color_flow.state.sector_id, + vertex_kind=vertex.kind, + vertex_particles=vertex.particles, + coupling=vertex.coupling, + ) + ) + return roots + + +def compile_generic_dag( + process: CanonicalProcessIR, + *, + model: Model, + color_plan: GenericColorPlan | None = None, + max_currents: int | None = None, + max_color_sectors: int | None = None, + reference_color_order: tuple[int, ...] | None = None, + selected_color_sector_ids: Iterable[int] | None = None, + max_coupling_orders: Mapping[str, int] | None = None, + max_lc_current_line_groups: int | None = None, + max_quark_pairs: int | None = None, + closure_side_mask_pruning: bool = True, + color_order_mask_pruning: bool = True, + species_reachability_pruning: bool = True, + ignored_particle_ids: Iterable[int] | None = None, + ignored_vertex_kinds: Iterable[int] | None = None, + selected_source_helicities: Mapping[int, int] | None = None, + lc_all_ordering_symmetry: bool = True, + online_evaluation_reuse: bool = False, + backward_live_planning: bool = False, + progress_callback: DAGProgressCallback | None = None, +) -> GenericDAG: + return GenericDAGCompiler( + model=model, + color_plan=color_plan, + max_currents=max_currents, + max_color_sectors=max_color_sectors, + reference_color_order=reference_color_order, + selected_color_sector_ids=selected_color_sector_ids, + max_coupling_orders=max_coupling_orders, + max_lc_current_line_groups=max_lc_current_line_groups, + max_quark_pairs=max_quark_pairs, + closure_side_mask_pruning=closure_side_mask_pruning, + color_order_mask_pruning=color_order_mask_pruning, + species_reachability_pruning=species_reachability_pruning, + ignored_particle_ids=ignored_particle_ids, + ignored_vertex_kinds=ignored_vertex_kinds, + selected_source_helicities=selected_source_helicities, + lc_all_ordering_symmetry=lc_all_ordering_symmetry, + online_evaluation_reuse=online_evaluation_reuse, + backward_live_planning=backward_live_planning, + progress_callback=progress_callback, + ).compile(process) diff --git a/pyamplicol/src/pyamplicol/generation/dag_equivalence.py b/pyamplicol/src/pyamplicol/generation/dag_equivalence.py new file mode 100644 index 0000000000..c673b8738e --- /dev/null +++ b/pyamplicol/src/pyamplicol/generation/dag_equivalence.py @@ -0,0 +1,5691 @@ +# SPDX-License-Identifier: 0BSD +"""Proof-gated recursive current-value reuse for generated DAGs. + +Current identity includes colour-sector and ordering metadata needed to build +and reduce amplitudes. Those fields do not necessarily change the numerical +current value. This module proves such value equivalences from the complete +recursive computation instead of guessing them from particle names or PDGs. +""" + +from __future__ import annotations + +import hashlib +import json +from collections import defaultdict +from collections.abc import Iterable, Mapping, Sequence +from dataclasses import dataclass, replace +from decimal import ROUND_HALF_EVEN, Decimal, localcontext +from fractions import Fraction +from math import copysign, fsum, isfinite +from typing import Literal, TypeAlias + +from ..models.base import Model, VertexEvaluationEquivalence +from .contracts import runtime_coupling_parameter_names +from .dag_types import AmplitudeRoot, CurrentNode, GenericDAG, InteractionNode +from .numerical_candidate_index import ( + NumericalObservationCandidateIndex, + build_numerical_observation_candidate_index, + numerical_observation_tolerance_window_ids, +) + +_ComplexWeight: TypeAlias = tuple[float, float] +_CurrentContract: TypeAlias = tuple[object, ...] +_EvaluationKey: TypeAlias = tuple[object, ...] +_CurrentTermVector: TypeAlias = tuple[tuple[_EvaluationKey, _ComplexWeight], ...] +_DiscoveryMode: TypeAlias = Literal["diagnostic", "certified-reuse"] + +RELATION_DISCOVERY_SCHEMA_VERSION = 1 +RELATION_DISCOVERY_CERTIFICATE_ALGORITHM = "exact-binary64-term-vector-replay-v1" +NUMERICAL_CURRENT_RELATION_CERTIFICATE_ALGORITHM = ( + "authenticated-independent-recursive-decimal-probes-v1" +) +GENERIC_DAG_NUMERICAL_SOURCE_SEMANTICS_ABI = ( + "pyamplicol-generic-dag-numerical-current-source-v1" +) +NUMERICAL_CURRENT_RELATION_SET_ABI = ( + "pyamplicol-authenticated-numerical-current-relation-set-v1" +) +NUMERICAL_CURRENT_CAPTURE_ABI = ( + "pyamplicol-generic-dag-current-observation-capture-v2" +) +NUMERICAL_CURRENT_CAPTURE_OUTPUT_PARTITION_ABI = ( + "pyamplicol-generic-dag-current-id-output-partitions-v1" +) +NUMERICAL_CURRENT_RELATION_WARNING_CODE = ( + "proofless-numerical-current-relations-applied-v1" +) +NUMERICAL_CURRENT_RELATION_WARNING = ( + "applied authenticated numerical equal/opposite/zero current reuse " + "without an exact structural proof; disable with " + "--no-numerical-current-reuse" +) +_MAX_REJECTED_DIAGNOSTICS = 16 +_NUMERICAL_DECIMAL_GUARD_DIGITS = 16 + + +def _is_negative_zero(value: float) -> bool: + return value == 0.0 and copysign(1.0, value) < 0.0 + + +def _rejected_candidate_diagnostics( + rows: Sequence[Mapping[str, object]], + *, + total_rejected: int, + full_census: Mapping[str, object], + full_rejection_sha256: str, +) -> dict[str, object]: + retained = [dict(row) for row in rows] + return { + "total_rejected_hypothesis_count": total_rejected, + "retained_count": len(retained), + "truncated": total_rejected > len(retained), + "truncation_policy": ( + f"first-{_MAX_REJECTED_DIAGNOSTICS}-in-canonical-discovery-order-v1" + ), + "retained_sha256": _canonical_payload_sha256( + { + "abi": "pyamplicol-rejected-relation-diagnostics-v1", + "rows": retained, + } + ), + "full_census_sha256": _canonical_payload_sha256( + { + "abi": "pyamplicol-relation-discovery-full-census-v1", + **dict(full_census), + } + ), + "full_rejection_sha256": full_rejection_sha256, + } + + +def _advance_rejection_digest(previous: str, row: Mapping[str, object]) -> str: + encoded = json.dumps( + dict(row), + ensure_ascii=True, + separators=(",", ":"), + sort_keys=True, + ).encode("ascii") + return hashlib.sha256(bytes.fromhex(previous) + encoded).hexdigest() + + +@dataclass(frozen=True, slots=True) +class _CurrentValueEquivalence: + """Exact relation ``current = factor * representative``.""" + + representative_id: int + factor: _ComplexWeight + + +@dataclass(frozen=True, slots=True) +class _CanonicalProjectiveTermVector: + """A deterministic lookup key and its concrete normalization factor.""" + + term_vector: _CurrentTermVector + factor: _ComplexWeight + + +@dataclass(frozen=True, slots=True) +class _ProjectiveExpressionRepresentative: + """One concrete current retained as a projective-class representative.""" + + representative_id: int + term_vector: _CurrentTermVector + normalization_factor: _ComplexWeight + + +@dataclass(frozen=True, slots=True) +class DynamicColorProjectionCertificate: + """Fail-closed proof summary for one multilinear color projection. + + The projection removes only ``CurrentIndex.color_state.sector_id``. + Accuracy, line groups, basis keys, every non-colour current field, and the + complete downstream physical-selector domain remain part of the class + key. A projected kernel or closure row is emitted only when the old + ordered parent tuples are exactly the full Cartesian product of the + corresponding member classes, with no duplicate tuple. + """ + + abi: str + source_revision: str | None + source_semantics_sha256: str + selector_domains_sha256: str + current_class_members_sha256: str + old_to_new_current_ids: tuple[int, ...] + current_remap_sha256: str + interaction_groups_sha256: str + closure_groups_sha256: str + rectangle_cardinalities_sha256: str + row_identity_sha256: str + equality_check_status: str + retained_color_metadata: str + root_sector_policy: str + before_current_count: int + after_current_count: int + before_interaction_count: int + after_interaction_count: int + before_evaluation_count: int + after_evaluation_count: int + before_amplitude_root_count: int + after_amplitude_root_count: int + projected_current_class_count: int + rectangular_interaction_group_count: int + rectangular_closure_group_count: int + split_current_class_count: int + + @property + def applied(self) -> bool: + return ( + self.after_current_count < self.before_current_count + or self.after_interaction_count < self.before_interaction_count + or self.after_amplitude_root_count < self.before_amplitude_root_count + ) + + def to_json_dict(self) -> dict[str, object]: + return { + "abi": self.abi, + "source_revision": self.source_revision, + "source_semantics_sha256": self.source_semantics_sha256, + "selector_domains_sha256": self.selector_domains_sha256, + "current_class_members_sha256": self.current_class_members_sha256, + "old_to_new_current_ids": list(self.old_to_new_current_ids), + "current_remap_sha256": self.current_remap_sha256, + "interaction_groups_sha256": self.interaction_groups_sha256, + "closure_groups_sha256": self.closure_groups_sha256, + "rectangle_cardinalities_sha256": ( + self.rectangle_cardinalities_sha256 + ), + "row_identity_sha256": self.row_identity_sha256, + "equality_check_status": self.equality_check_status, + "retained_color_metadata": self.retained_color_metadata, + "root_sector_policy": self.root_sector_policy, + "applied": self.applied, + "before_current_count": self.before_current_count, + "after_current_count": self.after_current_count, + "before_interaction_count": self.before_interaction_count, + "after_interaction_count": self.after_interaction_count, + "before_evaluation_count": self.before_evaluation_count, + "after_evaluation_count": self.after_evaluation_count, + "before_amplitude_root_count": self.before_amplitude_root_count, + "after_amplitude_root_count": self.after_amplitude_root_count, + "projected_current_class_count": self.projected_current_class_count, + "rectangular_interaction_group_count": ( + self.rectangular_interaction_group_count + ), + "rectangular_closure_group_count": self.rectangular_closure_group_count, + "split_current_class_count": self.split_current_class_count, + } + + +@dataclass(frozen=True, slots=True) +class ExactCurrentRelationCertificate: + """Replayable proof that one concrete current is an exact multiple.""" + + current_id: int + representative_id: int + factor: _ComplexWeight + current_term_vector_sha256: str + representative_term_vector_sha256: str + proof_sha256: str + algorithm: str = RELATION_DISCOVERY_CERTIFICATE_ALGORITHM + + def to_json_dict(self) -> dict[str, object]: + return { + "algorithm": self.algorithm, + "current_id": self.current_id, + "representative_id": self.representative_id, + "factor_binary64": [self.factor[0].hex(), self.factor[1].hex()], + "current_term_vector_sha256": self.current_term_vector_sha256, + "representative_term_vector_sha256": ( + self.representative_term_vector_sha256 + ), + "proof_sha256": self.proof_sha256, + } + + @classmethod + def from_json_dict( + cls, + payload: Mapping[str, object], + ) -> ExactCurrentRelationCertificate: + """Decode the exact representation emitted into generation metadata.""" + + expected_fields = { + "algorithm", + "current_id", + "representative_id", + "factor_binary64", + "current_term_vector_sha256", + "representative_term_vector_sha256", + "proof_sha256", + } + if set(payload) != expected_fields: + raise ValueError("relation certificate fields are not canonical") + try: + algorithm = payload["algorithm"] + current_id = payload["current_id"] + representative_id = payload["representative_id"] + raw_factor = payload["factor_binary64"] + current_digest = payload["current_term_vector_sha256"] + representative_digest = payload["representative_term_vector_sha256"] + proof_digest = payload["proof_sha256"] + except KeyError as error: + raise ValueError( + f"relation certificate is missing {error.args[0]!r}" + ) from error + if algorithm != RELATION_DISCOVERY_CERTIFICATE_ALGORITHM: + raise ValueError("relation certificate uses an unsupported algorithm") + if ( + type(current_id) is not int + or type(representative_id) is not int + or current_id < 0 + or representative_id < 0 + ): + raise ValueError("relation certificate current IDs must be nonnegative") + if ( + not isinstance(raw_factor, list) + or len(raw_factor) != 2 + or any(not isinstance(component, str) for component in raw_factor) + ): + raise ValueError( + "relation certificate factor must contain two binary64 hex strings" + ) + try: + factor = (float.fromhex(raw_factor[0]), float.fromhex(raw_factor[1])) + except ValueError as error: + raise ValueError( + "relation certificate factor has invalid binary64 encoding" + ) from error + if not _complex_weight_is_finite(factor) or factor == (0.0, 0.0): + raise ValueError("relation certificate factor must be finite and nonzero") + digests = (current_digest, representative_digest, proof_digest) + if any( + not isinstance(digest, str) + or len(digest) != 64 + or any(character not in "0123456789abcdef" for character in digest) + for digest in digests + ): + raise ValueError("relation certificate digests must be lowercase SHA-256") + assert isinstance(current_digest, str) + assert isinstance(representative_digest, str) + assert isinstance(proof_digest, str) + certificate = cls( + current_id=current_id, + representative_id=representative_id, + factor=factor, + current_term_vector_sha256=current_digest, + representative_term_vector_sha256=representative_digest, + proof_sha256=proof_digest, + ) + if ( + _certificate_proof_sha256( + current_id=certificate.current_id, + representative_id=certificate.representative_id, + factor=certificate.factor, + current_term_vector_sha256=(certificate.current_term_vector_sha256), + representative_term_vector_sha256=( + certificate.representative_term_vector_sha256 + ), + ) + != certificate.proof_sha256 + ): + raise ValueError("relation certificate proof digest does not replay") + return certificate + + +@dataclass(frozen=True, slots=True) +class NumericalCurrentRelationCertificate: + """Replayable evidence for one numerically certified current relation. + + The certificate commits to two disjoint observation sets. The raw current + values remain generation evidence; artifact loading validates this + canonical certificate and never reruns relation discovery. + """ + + current_id: int + representative_id: int | None + relation_kind: Literal["equal", "opposite", "zero"] + factor: _ComplexWeight | None + source_semantics_sha256: str + runtime_schema_sha256: str + source_dag_sha256: str + candidate_capture_sha256: str + verification_capture_sha256: str + candidate_observation_batch_sha256: str + verification_observation_batch_sha256: str + precision_digits: int + seed: int + relative_tolerance: float + absolute_tolerance: float + candidate_probe_count: int + verification_probe_count: int + current_dimension: int + candidate_maximum_absolute_residual: Decimal + candidate_maximum_relative_residual: Decimal + candidate_maximum_tolerance_ratio: Decimal + verification_maximum_absolute_residual: Decimal + verification_maximum_relative_residual: Decimal + verification_maximum_tolerance_ratio: Decimal + candidate_observations_sha256: str + verification_observations_sha256: str + probe_contract_sha256: str + proof_sha256: str + proof_kind: str = "authenticated-numerical" + algorithm: str = NUMERICAL_CURRENT_RELATION_CERTIFICATE_ALGORITHM + + def to_json_dict(self) -> dict[str, object]: + return { + "algorithm": self.algorithm, + "proof_kind": self.proof_kind, + "relation_kind": self.relation_kind, + "current_id": self.current_id, + "representative_id": self.representative_id, + "factor_binary64": ( + None + if self.factor is None + else [self.factor[0].hex(), self.factor[1].hex()] + ), + "source_semantics_sha256": self.source_semantics_sha256, + "runtime_schema_sha256": self.runtime_schema_sha256, + "source_dag_sha256": self.source_dag_sha256, + "candidate_capture_sha256": self.candidate_capture_sha256, + "verification_capture_sha256": ( + self.verification_capture_sha256 + ), + "candidate_observation_batch_sha256": ( + self.candidate_observation_batch_sha256 + ), + "verification_observation_batch_sha256": ( + self.verification_observation_batch_sha256 + ), + "precision_digits": self.precision_digits, + "seed": self.seed, + "relative_tolerance_binary64": self.relative_tolerance.hex(), + "absolute_tolerance_binary64": self.absolute_tolerance.hex(), + "candidate_probe_count": self.candidate_probe_count, + "verification_probe_count": self.verification_probe_count, + "current_dimension": self.current_dimension, + "candidate_maximum_absolute_residual": ( + _canonical_decimal_string( + self.candidate_maximum_absolute_residual + ) + ), + "candidate_maximum_relative_residual": ( + _canonical_decimal_string( + self.candidate_maximum_relative_residual + ) + ), + "candidate_maximum_tolerance_ratio": ( + _canonical_decimal_string( + self.candidate_maximum_tolerance_ratio + ) + ), + "verification_maximum_absolute_residual": ( + _canonical_decimal_string( + self.verification_maximum_absolute_residual + ) + ), + "verification_maximum_relative_residual": ( + _canonical_decimal_string( + self.verification_maximum_relative_residual + ) + ), + "verification_maximum_tolerance_ratio": ( + _canonical_decimal_string( + self.verification_maximum_tolerance_ratio + ) + ), + "candidate_observations_sha256": ( + self.candidate_observations_sha256 + ), + "verification_observations_sha256": ( + self.verification_observations_sha256 + ), + "probe_contract_sha256": self.probe_contract_sha256, + "proof_sha256": self.proof_sha256, + } + + @classmethod + def from_json_dict( + cls, + payload: Mapping[str, object], + ) -> NumericalCurrentRelationCertificate: + expected_fields = { + "algorithm", + "proof_kind", + "relation_kind", + "current_id", + "representative_id", + "factor_binary64", + "source_semantics_sha256", + "runtime_schema_sha256", + "source_dag_sha256", + "candidate_capture_sha256", + "verification_capture_sha256", + "candidate_observation_batch_sha256", + "verification_observation_batch_sha256", + "precision_digits", + "seed", + "relative_tolerance_binary64", + "absolute_tolerance_binary64", + "candidate_probe_count", + "verification_probe_count", + "current_dimension", + "candidate_maximum_absolute_residual", + "candidate_maximum_relative_residual", + "candidate_maximum_tolerance_ratio", + "verification_maximum_absolute_residual", + "verification_maximum_relative_residual", + "verification_maximum_tolerance_ratio", + "candidate_observations_sha256", + "verification_observations_sha256", + "probe_contract_sha256", + "proof_sha256", + } + if set(payload) != expected_fields: + raise ValueError( + "numerical current relation certificate fields are not canonical" + ) + algorithm = payload["algorithm"] + proof_kind = payload["proof_kind"] + relation_kind = payload["relation_kind"] + current_id = payload["current_id"] + representative_id = payload["representative_id"] + factor_payload = payload["factor_binary64"] + source_digest = payload["source_semantics_sha256"] + runtime_schema_digest = payload["runtime_schema_sha256"] + source_dag_digest = payload["source_dag_sha256"] + candidate_capture_digest = payload["candidate_capture_sha256"] + verification_capture_digest = payload[ + "verification_capture_sha256" + ] + candidate_batch_digest = payload[ + "candidate_observation_batch_sha256" + ] + verification_batch_digest = payload[ + "verification_observation_batch_sha256" + ] + precision_digits = payload["precision_digits"] + seed = payload["seed"] + relative_payload = payload["relative_tolerance_binary64"] + absolute_payload = payload["absolute_tolerance_binary64"] + candidate_probe_count = payload["candidate_probe_count"] + verification_probe_count = payload["verification_probe_count"] + current_dimension = payload["current_dimension"] + if ( + algorithm != NUMERICAL_CURRENT_RELATION_CERTIFICATE_ALGORITHM + or proof_kind != "authenticated-numerical" + or relation_kind not in {"equal", "opposite", "zero"} + ): + raise ValueError( + "numerical current relation certificate has unsupported policy" + ) + if type(current_id) is not int or current_id < 0: + raise ValueError( + "numerical current relation current ID must be nonnegative" + ) + if representative_id is not None and ( + type(representative_id) is not int + or representative_id < 0 + or representative_id >= current_id + ): + raise ValueError( + "numerical current relation representative must precede its current" + ) + if ( + type(precision_digits) is not int + or precision_digits < 80 + or type(seed) is not int + or seed < 0 + or type(candidate_probe_count) is not int + or candidate_probe_count < 2 + or type(verification_probe_count) is not int + or verification_probe_count < 2 + or type(current_dimension) is not int + or current_dimension < 1 + ): + raise ValueError( + "numerical current relation probe contract is invalid" + ) + if not isinstance(relative_payload, str) or not isinstance( + absolute_payload, + str, + ): + raise ValueError( + "numerical current relation tolerances must use binary64 hex" + ) + try: + relative_tolerance = float.fromhex(relative_payload) + absolute_tolerance = float.fromhex(absolute_payload) + except ValueError as error: + raise ValueError( + "numerical current relation tolerance encoding is invalid" + ) from error + if ( + not isfinite(relative_tolerance) + or not isfinite(absolute_tolerance) + or relative_tolerance < 0.0 + or absolute_tolerance < 0.0 + or _is_negative_zero(relative_tolerance) + or _is_negative_zero(absolute_tolerance) + or relative_payload != relative_tolerance.hex() + or absolute_payload != absolute_tolerance.hex() + or (relative_tolerance == 0.0 and absolute_tolerance == 0.0) + ): + raise ValueError( + "numerical current relation tolerances are invalid" + ) + factor = _decode_numerical_relation_factor( + relation_kind, + representative_id, + factor_payload, + ) + digest_fields = ( + source_digest, + runtime_schema_digest, + source_dag_digest, + candidate_capture_digest, + verification_capture_digest, + candidate_batch_digest, + verification_batch_digest, + payload["candidate_observations_sha256"], + payload["verification_observations_sha256"], + payload["probe_contract_sha256"], + payload["proof_sha256"], + ) + if any(not _is_sha256(value) for value in digest_fields): + raise ValueError( + "numerical current relation digests must be lowercase SHA-256" + ) + decimal_field_names = ( + "candidate_maximum_absolute_residual", + "candidate_maximum_relative_residual", + "candidate_maximum_tolerance_ratio", + "verification_maximum_absolute_residual", + "verification_maximum_relative_residual", + "verification_maximum_tolerance_ratio", + ) + residuals: list[Decimal] = [] + for field_name in decimal_field_names: + raw = payload[field_name] + if not isinstance(raw, str): + raise ValueError( + "numerical current relation residuals must be decimal strings" + ) + try: + residual = Decimal(raw) + except ArithmeticError as error: + raise ValueError( + "numerical current relation residual encoding is invalid" + ) from error + if ( + not residual.is_finite() + or residual < 0 + or raw != _canonical_decimal_string(residual) + ): + raise ValueError( + "numerical current relation residuals must be finite " + "and nonnegative canonical decimal strings" + ) + residuals.append(residual) + certificate = cls( + current_id=current_id, + representative_id=representative_id, + relation_kind=relation_kind, + factor=factor, + source_semantics_sha256=source_digest, + runtime_schema_sha256=str(runtime_schema_digest), + source_dag_sha256=str(source_dag_digest), + candidate_capture_sha256=str(candidate_capture_digest), + verification_capture_sha256=str( + verification_capture_digest + ), + candidate_observation_batch_sha256=str( + candidate_batch_digest + ), + verification_observation_batch_sha256=str( + verification_batch_digest + ), + precision_digits=precision_digits, + seed=seed, + relative_tolerance=relative_tolerance, + absolute_tolerance=absolute_tolerance, + candidate_probe_count=candidate_probe_count, + verification_probe_count=verification_probe_count, + current_dimension=current_dimension, + candidate_maximum_absolute_residual=residuals[0], + candidate_maximum_relative_residual=residuals[1], + candidate_maximum_tolerance_ratio=residuals[2], + verification_maximum_absolute_residual=residuals[3], + verification_maximum_relative_residual=residuals[4], + verification_maximum_tolerance_ratio=residuals[5], + candidate_observations_sha256=str( + payload["candidate_observations_sha256"] + ), + verification_observations_sha256=str( + payload["verification_observations_sha256"] + ), + probe_contract_sha256=str(payload["probe_contract_sha256"]), + proof_sha256=str(payload["proof_sha256"]), + ) + if not verify_numerical_current_relation_certificate( + certificate, + source_semantics_sha256=source_digest, + ): + raise ValueError( + "numerical current relation certificate proof does not replay" + ) + return certificate + + +@dataclass(frozen=True, slots=True) +class NumericalCurrentAppliedMapping: + """Execution mapping derived from one authenticated certificate.""" + + current_id: int + representative_id: int | None + execution_representative_id: int + relation_kind: Literal["equal", "opposite", "zero"] + factor: _ComplexWeight + certificate_proof_sha256: str + resolved_current_ids: tuple[int, ...] + projected_interaction_count: int + projected_interaction_ids_sha256: str + + def to_json_dict(self) -> dict[str, object]: + return { + "current_id": self.current_id, + "representative_id": self.representative_id, + "execution_representative_id": self.execution_representative_id, + "relation_kind": self.relation_kind, + "factor_binary64": [self.factor[0].hex(), self.factor[1].hex()], + "certificate_proof_sha256": self.certificate_proof_sha256, + "resolved_current_ids": list(self.resolved_current_ids), + "projected_interaction_count": self.projected_interaction_count, + "projected_interaction_ids_sha256": ( + self.projected_interaction_ids_sha256 + ), + } + + +@dataclass(frozen=True, slots=True) +class NumericalCurrentRelationApplicationReport: + """Manifest-ready replay result for one generic-DAG relation set.""" + + requested_mode: Literal["diagnostic", "certified-reuse"] + execution_mode: Literal["compiled", "eager"] + color_accuracy: str + source_semantics_sha256: str + runtime_schema_sha256: str | None + source_dag_sha256: str | None + candidate_capture_sha256: str | None + verification_capture_sha256: str | None + state: str + certificate_replay_status: str + certificate_set_sha256: str + certificates: tuple[NumericalCurrentRelationCertificate, ...] + mappings: tuple[NumericalCurrentAppliedMapping, ...] + interaction_evaluation_count_before: int + interaction_evaluation_count_projected: int + applied_relation_count: int + + @property + def warning_required(self) -> bool: + return self.applied_relation_count > 0 + + def to_json_dict(self) -> dict[str, object]: + relation_kind_counts = { + relation_kind: sum( + certificate.relation_kind == relation_kind + for certificate in self.certificates + ) + for relation_kind in ("equal", "opposite", "zero") + } + return { + "schema_version": 1, + "abi": NUMERICAL_CURRENT_RELATION_SET_ABI, + "requested_mode": self.requested_mode, + "state": self.state, + "scope": { + "execution_mode": self.execution_mode, + "color_accuracy": self.color_accuracy, + "representation": "generic-dag", + }, + "source_semantics": { + "abi": GENERIC_DAG_NUMERICAL_SOURCE_SEMANTICS_ABI, + "sha256": self.source_semantics_sha256, + }, + "runtime_schema_sha256": self.runtime_schema_sha256, + "source_dag_sha256": self.source_dag_sha256, + "candidate_capture_sha256": self.candidate_capture_sha256, + "verification_capture_sha256": ( + self.verification_capture_sha256 + ), + "certificate_replay": { + "algorithm": ( + NUMERICAL_CURRENT_RELATION_CERTIFICATE_ALGORITHM + ), + "status": self.certificate_replay_status, + "certificate_set_sha256": self.certificate_set_sha256, + }, + "certified_relation_count": len(self.certificates), + "applied_relation_count": self.applied_relation_count, + "relation_kind_counts": relation_kind_counts, + "interaction_evaluation_count_before": ( + self.interaction_evaluation_count_before + ), + "interaction_evaluation_count_projected": ( + self.interaction_evaluation_count_projected + ), + "interaction_evaluation_savings_projected": max( + 0, + self.interaction_evaluation_count_before + - self.interaction_evaluation_count_projected, + ), + "certificates": [ + certificate.to_json_dict() + for certificate in self.certificates + ], + "mappings": [mapping.to_json_dict() for mapping in self.mappings], + "warning": ( + { + "required": True, + "emit": "once-per-generated-artifact", + "code": NUMERICAL_CURRENT_RELATION_WARNING_CODE, + "message": NUMERICAL_CURRENT_RELATION_WARNING, + } + if self.warning_required + else { + "required": False, + "emit": "never", + "code": None, + "message": None, + } + ), + } + + +@dataclass(frozen=True, slots=True) +class NumericalCurrentRelationApplicationResult: + dag: GenericDAG + report: NumericalCurrentRelationApplicationReport + + +@dataclass(frozen=True, slots=True) +class NumericalCurrentObservationDiscoveryReport: + """Bounded warm-up evidence before any numerical mapping is applied.""" + + execution_mode: Literal["compiled", "eager"] + color_accuracy: str + source_semantics_sha256: str + runtime_schema_sha256: str + source_dag_sha256: str + precision_digits: int + seed: int + relative_tolerance: float + absolute_tolerance: float + candidate_point_sha256s: tuple[str, ...] + verification_point_sha256s: tuple[str, ...] + candidate_kinematic_sha256s: tuple[str, ...] + verification_kinematic_sha256s: tuple[str, ...] + candidate_parameter_context_sha256s: tuple[str, ...] + verification_parameter_context_sha256s: tuple[str, ...] + candidate_capture_sha256: str + verification_capture_sha256: str + candidate_observation_batch_sha256: str + verification_observation_batch_sha256: str + state: str + inspected_current_count: int + structurally_proven_current_count: int + tested_hypothesis_count: int + theoretical_pair_hypothesis_count: int + screened_pair_hypothesis_count: int + candidate_index_contract_count: int + exhaustive_fallback_contract_count: int + numerical_candidate_count: int + verification_rejected_count: int + rejected_hypothesis_count: int + rejected_decision_sha256: str + certificates: tuple[NumericalCurrentRelationCertificate, ...] + rejected_candidates: tuple[dict[str, object], ...] + nearest_rejected_hypothesis: dict[str, object] | None + + def to_json_dict(self) -> dict[str, object]: + return { + "schema_version": 1, + "state": self.state, + "scope": { + "execution_mode": self.execution_mode, + "color_accuracy": self.color_accuracy, + "representation": "generic-dag", + }, + "source_semantics": { + "abi": GENERIC_DAG_NUMERICAL_SOURCE_SEMANTICS_ABI, + "sha256": self.source_semantics_sha256, + }, + "runtime_schema_sha256": self.runtime_schema_sha256, + "source_dag_sha256": self.source_dag_sha256, + "probe_contract": { + "algorithm": ( + NUMERICAL_CURRENT_RELATION_CERTIFICATE_ALGORITHM + ), + "precision_digits": self.precision_digits, + "seed": self.seed, + "relative_tolerance_binary64": ( + self.relative_tolerance.hex() + ), + "absolute_tolerance_binary64": ( + self.absolute_tolerance.hex() + ), + "candidate_point_sha256s": list( + self.candidate_point_sha256s + ), + "verification_point_sha256s": list( + self.verification_point_sha256s + ), + "candidate_kinematic_sha256s": list( + self.candidate_kinematic_sha256s + ), + "verification_kinematic_sha256s": list( + self.verification_kinematic_sha256s + ), + "candidate_parameter_context_sha256s": list( + self.candidate_parameter_context_sha256s + ), + "verification_parameter_context_sha256s": list( + self.verification_parameter_context_sha256s + ), + "candidate_capture_sha256": ( + self.candidate_capture_sha256 + ), + "verification_capture_sha256": ( + self.verification_capture_sha256 + ), + "candidate_observation_batch_sha256": ( + self.candidate_observation_batch_sha256 + ), + "verification_observation_batch_sha256": ( + self.verification_observation_batch_sha256 + ), + "deterministic": True, + "independent_verification": True, + }, + "inspected_current_count": self.inspected_current_count, + "structurally_proven_current_count": ( + self.structurally_proven_current_count + ), + "tested_hypothesis_count": self.tested_hypothesis_count, + "candidate_index": { + "algorithm": ( + "complete-contract-anchor-tolerance-window-v1" + ), + "completeness": "complete-within-configured-tolerance", + "contract_count": self.candidate_index_contract_count, + "exhaustive_fallback_contract_count": ( + self.exhaustive_fallback_contract_count + ), + "theoretical_pair_hypothesis_count": ( + self.theoretical_pair_hypothesis_count + ), + "screened_pair_hypothesis_count": ( + self.screened_pair_hypothesis_count + ), + "zero_hypothesis_count": ( + self.tested_hypothesis_count + - self.screened_pair_hypothesis_count + ), + "nearest_rejected_scope": ( + "zero-and-tolerance-window-screened-hypotheses" + ), + }, + "numerical_candidate_count": self.numerical_candidate_count, + "verification_rejected_count": self.verification_rejected_count, + "rejected_hypothesis_count": self.rejected_hypothesis_count, + "certified_numerical_relation_count": len(self.certificates), + "certificates": [ + certificate.to_json_dict() + for certificate in self.certificates + ], + "rejected_candidates": list(self.rejected_candidates), + "rejected_candidate_diagnostics": ( + _rejected_candidate_diagnostics( + self.rejected_candidates, + total_rejected=self.rejected_hypothesis_count, + full_census={ + "candidate_capture_sha256": ( + self.candidate_capture_sha256 + ), + "verification_capture_sha256": ( + self.verification_capture_sha256 + ), + "tested_hypothesis_count": ( + self.tested_hypothesis_count + ), + "numerical_candidate_count": ( + self.numerical_candidate_count + ), + "verification_rejected_count": ( + self.verification_rejected_count + ), + "rejected_hypothesis_count": ( + self.rejected_hypothesis_count + ), + "certified_relation_count": len(self.certificates), + "rejection_decision_sha256": ( + self.rejected_decision_sha256 + ), + }, + full_rejection_sha256=self.rejected_decision_sha256, + ) + ), + "nearest_rejected_hypothesis": ( + self.nearest_rejected_hypothesis + ), + "warning": { + "required": False, + "reason": ( + "discovery alone never warns; the artifact aggregator " + "warns once only when certified proof-less mappings are " + "actually applied" + ), + }, + } + + +@dataclass(frozen=True, slots=True) +class NumericalCurrentObservationDiscoveryResult: + certificates: tuple[NumericalCurrentRelationCertificate, ...] + report: NumericalCurrentObservationDiscoveryReport + structural_equivalences: tuple[_CurrentValueEquivalence, ...] + + +_NumericalObservationCandidateIndex = NumericalObservationCandidateIndex[Decimal] + + +def _build_numerical_observation_candidate_index( + member_ids: Sequence[int], + observations: Mapping[int, tuple[tuple[Decimal, Decimal], ...]], +) -> _NumericalObservationCandidateIndex: + return build_numerical_observation_candidate_index( + member_ids, + observations, + normalize=lambda value: value, + ) + + +def _numerical_observation_tolerance_window_ids( + index: _NumericalObservationCandidateIndex, + current_values: tuple[tuple[Decimal, Decimal], ...], + *, + relation_kind: Literal["equal", "opposite"], + current_id: int, + relative_tolerance: Decimal, + absolute_tolerance: Decimal, + precision_digits: int, +) -> tuple[int, ...]: + """Use the shared sound window under the configured Decimal context.""" + + if type(precision_digits) is not int or precision_digits < 80: + raise ValueError("numerical candidate index precision is invalid") + with localcontext() as context: + context.prec = precision_digits + _NUMERICAL_DECIMAL_GUARD_DIGITS + context.rounding = ROUND_HALF_EVEN + return numerical_observation_tolerance_window_ids( + index, + current_values, + relation_kind=relation_kind, + current_id=current_id, + relative_tolerance=relative_tolerance, + absolute_tolerance=absolute_tolerance, + normalize=lambda value: value, + ) + + +def certify_numerical_current_observations( + *, + current_id: int, + representative_id: int | None, + relation_kind: Literal["equal", "opposite", "zero"], + source_semantics_sha256: str, + runtime_schema_sha256: str, + source_dag_sha256: str, + candidate_capture_sha256: str, + verification_capture_sha256: str, + candidate_observation_batch_sha256: str, + verification_observation_batch_sha256: str, + candidate_current_values: Sequence[tuple[Decimal, Decimal]], + candidate_representative_values: ( + Sequence[tuple[Decimal, Decimal]] | None + ), + verification_current_values: Sequence[tuple[Decimal, Decimal]], + verification_representative_values: ( + Sequence[tuple[Decimal, Decimal]] | None + ), + precision_digits: int, + seed: int, + relative_tolerance: float, + absolute_tolerance: float, + candidate_probe_count: int | None = None, + verification_probe_count: int | None = None, +) -> NumericalCurrentRelationCertificate | None: + """Certify one ±1/zero relation over independent Decimal observations. + + Invalid, non-finite, forward, shape-inconsistent, or unstable hypotheses + fail closed by returning ``None``. + """ + + if ( + relation_kind not in {"equal", "opposite", "zero"} + or type(current_id) is not int + or current_id < 0 + or not _is_sha256(source_semantics_sha256) + or any( + not _is_sha256(value) + for value in ( + runtime_schema_sha256, + source_dag_sha256, + candidate_capture_sha256, + verification_capture_sha256, + candidate_observation_batch_sha256, + verification_observation_batch_sha256, + ) + ) + or candidate_capture_sha256 == verification_capture_sha256 + or candidate_observation_batch_sha256 + == verification_observation_batch_sha256 + or type(precision_digits) is not int + or precision_digits < 80 + or type(seed) is not int + or seed < 0 + or not isinstance(relative_tolerance, int | float) + or isinstance(relative_tolerance, bool) + or not isinstance(absolute_tolerance, int | float) + or isinstance(absolute_tolerance, bool) + ): + return None + relative = float(relative_tolerance) + absolute = float(absolute_tolerance) + if ( + not isfinite(relative) + or not isfinite(absolute) + or relative < 0.0 + or absolute < 0.0 + or _is_negative_zero(relative) + or _is_negative_zero(absolute) + or (relative == 0.0 and absolute == 0.0) + ): + return None + candidate_current = _validated_decimal_observations( + candidate_current_values + ) + verification_current = _validated_decimal_observations( + verification_current_values + ) + if ( + candidate_current is None + or verification_current is None + or len(candidate_current) < 2 + or len(verification_current) < 2 + ): + return None + resolved_candidate_probe_count = ( + len(candidate_current) + if candidate_probe_count is None + else candidate_probe_count + ) + resolved_verification_probe_count = ( + len(verification_current) + if verification_probe_count is None + else verification_probe_count + ) + if ( + type(resolved_candidate_probe_count) is not int + or resolved_candidate_probe_count < 2 + or type(resolved_verification_probe_count) is not int + or resolved_verification_probe_count < 2 + or len(candidate_current) % resolved_candidate_probe_count != 0 + or len(verification_current) % resolved_verification_probe_count != 0 + ): + return None + current_dimension = ( + len(candidate_current) // resolved_candidate_probe_count + ) + if ( + current_dimension < 1 + or len(verification_current) + // resolved_verification_probe_count + != current_dimension + ): + return None + if relation_kind == "zero": + if ( + representative_id is not None + or candidate_representative_values is not None + or verification_representative_values is not None + ): + return None + candidate_representative = None + verification_representative = None + factor = None + else: + if ( + type(representative_id) is not int + or representative_id < 0 + or representative_id >= current_id + ): + return None + candidate_representative = _validated_decimal_observations( + candidate_representative_values + ) + verification_representative = _validated_decimal_observations( + verification_representative_values + ) + if ( + candidate_representative is None + or verification_representative is None + or len(candidate_representative) != len(candidate_current) + or len(verification_representative) != len(verification_current) + ): + return None + factor = ( + (1.0, 0.0) + if relation_kind == "equal" + else (-1.0, 0.0) + ) + absolute_decimal = Decimal.from_float(absolute) + relative_decimal = Decimal.from_float(relative) + candidate_residuals = _numerical_relation_residuals( + relation_kind, + candidate_current, + candidate_representative, + relative_tolerance=relative_decimal, + absolute_tolerance=absolute_decimal, + precision_digits=precision_digits, + ) + verification_residuals = _numerical_relation_residuals( + relation_kind, + verification_current, + verification_representative, + relative_tolerance=relative_decimal, + absolute_tolerance=absolute_decimal, + precision_digits=precision_digits, + ) + if candidate_residuals is None or verification_residuals is None: + return None + if not _numerical_relation_residuals_pass( + candidate_residuals, + precision_digits=precision_digits, + ) or not _numerical_relation_residuals_pass( + verification_residuals, + precision_digits=precision_digits, + ): + return None + candidate_digest = _numerical_observations_sha256( + relation_kind=relation_kind, + current_values=candidate_current, + representative_values=candidate_representative, + ) + verification_digest = _numerical_observations_sha256( + relation_kind=relation_kind, + current_values=verification_current, + representative_values=verification_representative, + ) + probe_contract = { + "algorithm": NUMERICAL_CURRENT_RELATION_CERTIFICATE_ALGORITHM, + "source_semantics_sha256": source_semantics_sha256, + "runtime_schema_sha256": runtime_schema_sha256, + "source_dag_sha256": source_dag_sha256, + "candidate_capture_sha256": candidate_capture_sha256, + "verification_capture_sha256": verification_capture_sha256, + "candidate_observation_batch_sha256": ( + candidate_observation_batch_sha256 + ), + "verification_observation_batch_sha256": ( + verification_observation_batch_sha256 + ), + "current_id": current_id, + "representative_id": representative_id, + "relation_kind": relation_kind, + "precision_digits": precision_digits, + "seed": seed, + "candidate_domain": "candidate-current-probes-v1", + "verification_domain": "independent-verification-current-probes-v1", + "relative_tolerance_binary64": relative.hex(), + "absolute_tolerance_binary64": absolute.hex(), + "candidate_probe_count": resolved_candidate_probe_count, + "verification_probe_count": resolved_verification_probe_count, + "current_dimension": current_dimension, + "candidate_observations_sha256": candidate_digest, + "verification_observations_sha256": verification_digest, + } + probe_contract_digest = _canonical_payload_sha256(probe_contract) + proof_payload = { + **probe_contract, + "proof_kind": "authenticated-numerical", + "factor_binary64": ( + None + if factor is None + else [factor[0].hex(), factor[1].hex()] + ), + "candidate_maximum_absolute_residual": _canonical_decimal_string( + candidate_residuals[0] + ), + "candidate_maximum_relative_residual": _canonical_decimal_string( + candidate_residuals[1] + ), + "candidate_maximum_tolerance_ratio": _canonical_decimal_string( + candidate_residuals[2] + ), + "verification_maximum_absolute_residual": _canonical_decimal_string( + verification_residuals[0] + ), + "verification_maximum_relative_residual": _canonical_decimal_string( + verification_residuals[1] + ), + "verification_maximum_tolerance_ratio": _canonical_decimal_string( + verification_residuals[2] + ), + "probe_contract_sha256": probe_contract_digest, + } + return NumericalCurrentRelationCertificate( + current_id=current_id, + representative_id=representative_id, + relation_kind=relation_kind, + factor=factor, + source_semantics_sha256=source_semantics_sha256, + runtime_schema_sha256=runtime_schema_sha256, + source_dag_sha256=source_dag_sha256, + candidate_capture_sha256=candidate_capture_sha256, + verification_capture_sha256=verification_capture_sha256, + candidate_observation_batch_sha256=( + candidate_observation_batch_sha256 + ), + verification_observation_batch_sha256=( + verification_observation_batch_sha256 + ), + precision_digits=precision_digits, + seed=seed, + relative_tolerance=relative, + absolute_tolerance=absolute, + candidate_probe_count=resolved_candidate_probe_count, + verification_probe_count=resolved_verification_probe_count, + current_dimension=current_dimension, + candidate_maximum_absolute_residual=candidate_residuals[0], + candidate_maximum_relative_residual=candidate_residuals[1], + candidate_maximum_tolerance_ratio=candidate_residuals[2], + verification_maximum_absolute_residual=verification_residuals[0], + verification_maximum_relative_residual=verification_residuals[1], + verification_maximum_tolerance_ratio=verification_residuals[2], + candidate_observations_sha256=candidate_digest, + verification_observations_sha256=verification_digest, + probe_contract_sha256=probe_contract_digest, + proof_sha256=_canonical_payload_sha256(proof_payload), + ) + + +def verify_numerical_current_relation_certificate( + certificate: NumericalCurrentRelationCertificate, + *, + source_semantics_sha256: str, +) -> bool: + """Validate canonical certificate integrity without rediscovery.""" + + if ( + not isinstance(certificate, NumericalCurrentRelationCertificate) + or certificate.algorithm + != NUMERICAL_CURRENT_RELATION_CERTIFICATE_ALGORITHM + or certificate.proof_kind != "authenticated-numerical" + or certificate.source_semantics_sha256 != source_semantics_sha256 + or not _is_sha256(source_semantics_sha256) + ): + return False + try: + factor = _decode_numerical_relation_factor( + certificate.relation_kind, + certificate.representative_id, + ( + None + if certificate.factor is None + else [ + certificate.factor[0].hex(), + certificate.factor[1].hex(), + ] + ), + ) + except (TypeError, ValueError): + return False + if factor != certificate.factor: + return False + if ( + type(certificate.current_id) is not int + or certificate.current_id < 0 + or type(certificate.precision_digits) is not int + or certificate.precision_digits < 80 + or type(certificate.seed) is not int + or certificate.seed < 0 + or type(certificate.candidate_probe_count) is not int + or certificate.candidate_probe_count < 2 + or type(certificate.verification_probe_count) is not int + or certificate.verification_probe_count < 2 + or type(certificate.current_dimension) is not int + or certificate.current_dimension < 1 + or not isfinite(certificate.relative_tolerance) + or not isfinite(certificate.absolute_tolerance) + or certificate.relative_tolerance < 0.0 + or certificate.absolute_tolerance < 0.0 + or _is_negative_zero(certificate.relative_tolerance) + or _is_negative_zero(certificate.absolute_tolerance) + or ( + certificate.relative_tolerance == 0.0 + and certificate.absolute_tolerance == 0.0 + ) + or any( + not _is_sha256(value) + for value in ( + certificate.runtime_schema_sha256, + certificate.source_dag_sha256, + certificate.candidate_capture_sha256, + certificate.verification_capture_sha256, + certificate.candidate_observation_batch_sha256, + certificate.verification_observation_batch_sha256, + certificate.candidate_observations_sha256, + certificate.verification_observations_sha256, + certificate.probe_contract_sha256, + certificate.proof_sha256, + ) + ) + or any( + not value.is_finite() or value < 0 + for value in ( + certificate.candidate_maximum_absolute_residual, + certificate.candidate_maximum_relative_residual, + certificate.candidate_maximum_tolerance_ratio, + certificate.verification_maximum_absolute_residual, + certificate.verification_maximum_relative_residual, + certificate.verification_maximum_tolerance_ratio, + ) + ) + or certificate.candidate_maximum_tolerance_ratio > 1 + or certificate.verification_maximum_tolerance_ratio > 1 + or certificate.candidate_capture_sha256 + == certificate.verification_capture_sha256 + or certificate.candidate_observation_batch_sha256 + == certificate.verification_observation_batch_sha256 + ): + return False + probe_contract = { + "algorithm": certificate.algorithm, + "source_semantics_sha256": certificate.source_semantics_sha256, + "runtime_schema_sha256": certificate.runtime_schema_sha256, + "source_dag_sha256": certificate.source_dag_sha256, + "candidate_capture_sha256": certificate.candidate_capture_sha256, + "verification_capture_sha256": ( + certificate.verification_capture_sha256 + ), + "candidate_observation_batch_sha256": ( + certificate.candidate_observation_batch_sha256 + ), + "verification_observation_batch_sha256": ( + certificate.verification_observation_batch_sha256 + ), + "current_id": certificate.current_id, + "representative_id": certificate.representative_id, + "relation_kind": certificate.relation_kind, + "precision_digits": certificate.precision_digits, + "seed": certificate.seed, + "candidate_domain": "candidate-current-probes-v1", + "verification_domain": "independent-verification-current-probes-v1", + "relative_tolerance_binary64": ( + certificate.relative_tolerance.hex() + ), + "absolute_tolerance_binary64": ( + certificate.absolute_tolerance.hex() + ), + "candidate_probe_count": certificate.candidate_probe_count, + "verification_probe_count": certificate.verification_probe_count, + "current_dimension": certificate.current_dimension, + "candidate_observations_sha256": ( + certificate.candidate_observations_sha256 + ), + "verification_observations_sha256": ( + certificate.verification_observations_sha256 + ), + } + if ( + _canonical_payload_sha256(probe_contract) + != certificate.probe_contract_sha256 + ): + return False + proof_payload = { + **probe_contract, + "proof_kind": certificate.proof_kind, + "factor_binary64": ( + None + if certificate.factor is None + else [ + certificate.factor[0].hex(), + certificate.factor[1].hex(), + ] + ), + "candidate_maximum_absolute_residual": _canonical_decimal_string( + certificate.candidate_maximum_absolute_residual + ), + "candidate_maximum_relative_residual": _canonical_decimal_string( + certificate.candidate_maximum_relative_residual + ), + "candidate_maximum_tolerance_ratio": _canonical_decimal_string( + certificate.candidate_maximum_tolerance_ratio + ), + "verification_maximum_absolute_residual": _canonical_decimal_string( + certificate.verification_maximum_absolute_residual + ), + "verification_maximum_relative_residual": _canonical_decimal_string( + certificate.verification_maximum_relative_residual + ), + "verification_maximum_tolerance_ratio": _canonical_decimal_string( + certificate.verification_maximum_tolerance_ratio + ), + "probe_contract_sha256": certificate.probe_contract_sha256, + } + return _canonical_payload_sha256(proof_payload) == certificate.proof_sha256 + + +def generic_dag_numerical_source_semantics_sha256( + dag: GenericDAG, + *, + execution_mode: Literal["compiled", "eager"], +) -> str: + """Bind numerical observations to an execution-scoped source DAG. + + Evaluation-group IDs and factors are derived optimizations rather than + source semantics. Removing them makes the digest stable across baseline + exact sharing and authenticated numerical sharing while retaining every + current, interaction, selector, colour, helicity, and model-facing field. + """ + + if execution_mode not in {"compiled", "eager"}: + raise ValueError( + "generic-DAG numerical relations require compiled or eager mode" + ) + source_dag = _replace_dag_interactions_preserving_materialization( + dag, + tuple( + replace( + interaction, + evaluation_group_id=None, + evaluation_factor=(1.0, 0.0), + ) + for interaction in dag.interactions + ), + ) + return _canonical_payload_sha256( + { + "abi": GENERIC_DAG_NUMERICAL_SOURCE_SEMANTICS_ABI, + "execution_mode": execution_mode, + "color_accuracy": str(dag.process.color_accuracy), + "dag": source_dag.to_json_dict(), + } + ) + + +def generic_dag_numerical_source_dag_sha256(dag: GenericDAG) -> str: + """Return the exact pre-application DAG digest bound into evidence.""" + + return _canonical_payload_sha256(dag.to_json_dict()) + + +def generic_dag_numerical_runtime_schema_sha256( + dag: GenericDAG, + model: Model, + *, + process_id: str | None = None, +) -> str: + """Return the model- and process-bound runtime schema digest.""" + + from .runtime_schema import build_runtime_expression_schema + + return build_runtime_expression_schema( + dag, + model, + process_id=process_id or dag.process.key, + ).sha256 + + +def generic_dag_numerical_model_parameter_schema_sha256( + dag: GenericDAG, + model: Model, + *, + process_id: str | None = None, +) -> str: + """Bind numerical probes to the ordered runtime-parameter schema.""" + + from .runtime_schema import build_runtime_expression_schema + + runtime_schema = build_runtime_expression_schema( + dag, + model, + process_id=process_id or dag.process.key, + ).to_mapping() + return _canonical_payload_sha256( + runtime_schema.get("model_parameters", ()) + ) + + +def generic_dag_numerical_capture_output_partition_sha256( + runtime_schema: Mapping[str, object], +) -> str: + """Authenticate current-ID partitions from source runtime-slot geometry.""" + + raw_stages = runtime_schema.get("stages") + raw_value_storage = runtime_schema.get("value_storage") + if not isinstance(raw_stages, list) or not isinstance( + raw_value_storage, + Mapping, + ): + raise ValueError( + "numerical current partition runtime schema is invalid" + ) + raw_value_slots = raw_value_storage.get("value_slots") + if not isinstance(raw_value_slots, list): + raise ValueError( + "numerical current partition value slots are invalid" + ) + value_slots: dict[int, Mapping[str, object]] = {} + for raw_slot in raw_value_slots: + if not isinstance(raw_slot, Mapping): + raise ValueError( + "numerical current partition value slot is invalid" + ) + value_slot_id = raw_slot.get("value_slot_id") + if ( + type(value_slot_id) is not int + or value_slot_id < 0 + or value_slot_id in value_slots + ): + raise ValueError( + "numerical current partition value-slot identity drifted" + ) + value_slots[value_slot_id] = raw_slot + + stage_contracts: list[dict[str, object]] = [] + previous_stage_index: int | None = None + seen_output_slot_ids: set[int] = set() + for raw_stage in raw_stages: + if not isinstance(raw_stage, Mapping): + raise ValueError( + "numerical current partition stage is invalid" + ) + stage_index = raw_stage.get("stage_index") + stage_kind = raw_stage.get("stage_kind") + subset_size = raw_stage.get("subset_size") + raw_output_slot_ids = raw_stage.get("output_value_slot_ids") + raw_output_current_ids = raw_stage.get("output_current_ids") + if ( + type(stage_index) is not int + or ( + previous_stage_index is not None + and stage_index <= previous_stage_index + ) + or not isinstance(stage_kind, str) + or type(subset_size) is not int + or not isinstance(raw_output_slot_ids, list) + or not raw_output_slot_ids + or not isinstance(raw_output_current_ids, list) + ): + raise ValueError( + "numerical current partition stage identity drifted" + ) + + records: list[dict[str, object]] = [] + partition_slots: list[dict[str, object]] = [] + partition_start = 0 + output_start = 0 + current_id: int | None = None + seen_current_ids: set[int] = set() + + def partition_record( + *, + bound_current_id: int | None, + bound_slots: Sequence[Mapping[str, object]], + bound_start: int, + stop: int, + ) -> dict[str, object]: + if ( + bound_current_id is None + or not bound_slots + or bound_start >= stop + ): + raise ValueError( + "numerical current partition contains an empty range" + ) + return { + "current_id": bound_current_id, + "output_start": bound_start, + "output_stop": stop, + "slots": list(bound_slots), + } + + for raw_value_slot_id in raw_output_slot_ids: + if ( + type(raw_value_slot_id) is not int + or raw_value_slot_id in seen_output_slot_ids + or raw_value_slot_id not in value_slots + ): + raise ValueError( + "numerical current partition output-slot identity drifted" + ) + slot = value_slots[raw_value_slot_id] + slot_current_id = slot.get("current_id") + component_start = slot.get("component_start") + component_stop = slot.get("component_stop") + variant = slot.get("variant") + if ( + type(slot_current_id) is not int + or slot_current_id < 0 + or type(component_start) is not int + or type(component_stop) is not int + or component_start < 0 + or component_stop <= component_start + or not isinstance(variant, str) + or slot.get("is_source") is not False + ): + raise ValueError( + "numerical current partition output slot is invalid" + ) + if current_id is None: + current_id = slot_current_id + seen_current_ids.add(current_id) + elif slot_current_id != current_id: + records.append( + partition_record( + bound_current_id=current_id, + bound_slots=partition_slots, + bound_start=partition_start, + stop=output_start, + ) + ) + if ( + slot_current_id in seen_current_ids + or slot_current_id <= current_id + ): + raise ValueError( + "numerical current partition output current order " + "drifted" + ) + current_id = slot_current_id + seen_current_ids.add(current_id) + partition_start = output_start + partition_slots = [] + output_stop = output_start + component_stop - component_start + partition_slots.append( + { + "value_slot_id": raw_value_slot_id, + "variant": variant, + "component_start": component_start, + "component_stop": component_stop, + "output_start": output_start, + "output_stop": output_stop, + } + ) + output_start = output_stop + seen_output_slot_ids.add(raw_value_slot_id) + records.append( + partition_record( + bound_current_id=current_id, + bound_slots=partition_slots, + bound_start=partition_start, + stop=output_start, + ) + ) + if raw_output_current_ids != [ + record["current_id"] for record in records + ]: + raise ValueError( + "numerical current partition stage current identity drifted" + ) + stage_contracts.append( + { + "stage_index": stage_index, + "stage_kind": stage_kind, + "subset_size": subset_size, + "output_length": output_start, + "partitions": records, + } + ) + previous_stage_index = stage_index + if not stage_contracts: + raise ValueError( + "numerical current partition runtime schema has no stages" + ) + return _canonical_payload_sha256( + { + "abi": NUMERICAL_CURRENT_CAPTURE_OUTPUT_PARTITION_ABI, + "strategy": "one-contiguous-output-partition-per-current-id", + "stages": stage_contracts, + } + ) + + +def discover_generic_dag_numerical_current_relations( + dag: GenericDAG, + model: Model, + *, + candidate_observations: Mapping[ + int, + Sequence[tuple[Decimal, Decimal]], + ], + verification_observations: Mapping[ + int, + Sequence[tuple[Decimal, Decimal]], + ], + candidate_point_sha256s: Sequence[str], + verification_point_sha256s: Sequence[str], + candidate_kinematic_sha256s: Sequence[str], + verification_kinematic_sha256s: Sequence[str], + candidate_parameter_context_sha256s: Sequence[str], + verification_parameter_context_sha256s: Sequence[str], + runtime_schema_sha256: str, + source_dag_sha256: str, + evaluator_output_partition_abi: str, + evaluator_output_partition_sha256: str, + candidate_capture_sha256: str, + verification_capture_sha256: str, + process_id: str | None = None, + execution_mode: Literal["compiled", "eager"], + precision_digits: int, + seed: int, + relative_tolerance: float, + absolute_tolerance: float, +) -> NumericalCurrentObservationDiscoveryResult: + """Discover equal, opposite, and zero currents from complete warm-up data. + + Every generated current is compared exhaustively within its exact runtime + contract. Candidate and verification point domains must be disjoint. + Exact structural relations are retained in preference to numerical + evidence and are not re-certified as proof-less mappings. + """ + + source_digest = generic_dag_numerical_source_semantics_sha256( + dag, + execution_mode=execution_mode, + ) + from .runtime_schema import build_runtime_expression_schema + + actual_runtime_schema = build_runtime_expression_schema( + dag, + model, + process_id=process_id or dag.process.key, + ) + actual_runtime_schema_mapping = actual_runtime_schema.to_mapping() + actual_runtime_schema_digest = actual_runtime_schema.sha256 + actual_model_parameter_schema_digest = _canonical_payload_sha256( + actual_runtime_schema_mapping.get("model_parameters", ()) + ) + actual_output_partition_digest = ( + generic_dag_numerical_capture_output_partition_sha256( + actual_runtime_schema_mapping + ) + ) + actual_source_dag_digest = generic_dag_numerical_source_dag_sha256(dag) + candidate_points = tuple(candidate_point_sha256s) + verification_points = tuple(verification_point_sha256s) + candidate_kinematics = tuple(candidate_kinematic_sha256s) + verification_kinematics = tuple(verification_kinematic_sha256s) + candidate_parameters = tuple( + candidate_parameter_context_sha256s + ) + verification_parameters = tuple( + verification_parameter_context_sha256s + ) + if ( + type(precision_digits) is not int + or precision_digits < 80 + or type(seed) is not int + or seed < 0 + or len(candidate_points) < 2 + or len(verification_points) < 2 + or any(not _is_sha256(value) for value in candidate_points) + or any(not _is_sha256(value) for value in verification_points) + or len(set(candidate_points)) != len(candidate_points) + or len(set(verification_points)) != len(verification_points) + or not set(candidate_points).isdisjoint(verification_points) + or len(candidate_kinematics) != len(candidate_points) + or len(verification_kinematics) != len(verification_points) + or len(candidate_parameters) != len(candidate_points) + or len(verification_parameters) != len(verification_points) + or any(not _is_sha256(value) for value in candidate_kinematics) + or any(not _is_sha256(value) for value in verification_kinematics) + or any(not _is_sha256(value) for value in candidate_parameters) + or any(not _is_sha256(value) for value in verification_parameters) + or len(set(candidate_kinematics)) != len(candidate_kinematics) + or len(set(verification_kinematics)) + != len(verification_kinematics) + or not set(candidate_kinematics).isdisjoint( + verification_kinematics + ) + or not set(candidate_parameters).isdisjoint( + verification_parameters + ) + or any( + not _is_sha256(value) + for value in ( + runtime_schema_sha256, + source_dag_sha256, + evaluator_output_partition_sha256, + candidate_capture_sha256, + verification_capture_sha256, + ) + ) + or evaluator_output_partition_abi + != NUMERICAL_CURRENT_CAPTURE_OUTPUT_PARTITION_ABI + or evaluator_output_partition_sha256 + != actual_output_partition_digest + or candidate_capture_sha256 == verification_capture_sha256 + or runtime_schema_sha256 != actual_runtime_schema_digest + or source_dag_sha256 != actual_source_dag_digest + ): + raise ValueError( + "numerical current discovery point contract is invalid" + ) + if ( + not isinstance(relative_tolerance, int | float) + or isinstance(relative_tolerance, bool) + or not isinstance(absolute_tolerance, int | float) + or isinstance(absolute_tolerance, bool) + ): + raise ValueError( + "numerical current discovery tolerances must be real numbers" + ) + relative = float(relative_tolerance) + absolute = float(absolute_tolerance) + if ( + not isfinite(relative) + or not isfinite(absolute) + or relative < 0.0 + or absolute < 0.0 + or _is_negative_zero(relative) + or _is_negative_zero(absolute) + or (relative == 0.0 and absolute == 0.0) + ): + raise ValueError("numerical current discovery tolerances are invalid") + + current_ids = tuple(range(len(dag.currents))) + if tuple(current.id for current in dag.currents) != current_ids: + raise ValueError( + "numerical current discovery requires canonical current IDs" + ) + if set(candidate_observations) != set(current_ids) or set( + verification_observations + ) != set(current_ids): + raise ValueError( + "numerical current discovery requires observations for every current" + ) + candidate_values: dict[ + int, + tuple[tuple[Decimal, Decimal], ...], + ] = {} + verification_values: dict[ + int, + tuple[tuple[Decimal, Decimal], ...], + ] = {} + for current in dag.currents: + candidate = _validated_decimal_observations( + candidate_observations[current.id] + ) + verification = _validated_decimal_observations( + verification_observations[current.id] + ) + expected_candidate_count = ( + len(candidate_points) * current.dimension + ) + expected_verification_count = ( + len(verification_points) * current.dimension + ) + if ( + candidate is None + or verification is None + or len(candidate) != expected_candidate_count + or len(verification) != expected_verification_count + ): + raise ValueError( + f"numerical observations for current {current.id} do not " + "cover every point and component" + ) + candidate_values[current.id] = candidate + verification_values[current.id] = verification + + candidate_batch_digest = _numerical_current_observation_batch_sha256( + candidate_values, + point_sha256s=candidate_points, + ) + verification_batch_digest = _numerical_current_observation_batch_sha256( + verification_values, + point_sha256s=verification_points, + ) + expected_candidate_capture_digest = _canonical_payload_sha256( + { + "abi": NUMERICAL_CURRENT_CAPTURE_ABI, + "precision_digits": precision_digits, + "point_sha256s": list(candidate_points), + "kinematic_sha256s": list(candidate_kinematics), + "parameter_context_sha256s": list(candidate_parameters), + "runtime_schema_sha256": runtime_schema_sha256, + "model_parameter_schema_sha256": ( + actual_model_parameter_schema_digest + ), + "source_dag_sha256": source_dag_sha256, + "evaluator_output_partition_abi": ( + evaluator_output_partition_abi + ), + "evaluator_output_partition_sha256": ( + evaluator_output_partition_sha256 + ), + "observation_batch_sha256": candidate_batch_digest, + } + ) + expected_verification_capture_digest = _canonical_payload_sha256( + { + "abi": NUMERICAL_CURRENT_CAPTURE_ABI, + "precision_digits": precision_digits, + "point_sha256s": list(verification_points), + "kinematic_sha256s": list(verification_kinematics), + "parameter_context_sha256s": list(verification_parameters), + "runtime_schema_sha256": runtime_schema_sha256, + "model_parameter_schema_sha256": ( + actual_model_parameter_schema_digest + ), + "source_dag_sha256": source_dag_sha256, + "evaluator_output_partition_abi": ( + evaluator_output_partition_abi + ), + "evaluator_output_partition_sha256": ( + evaluator_output_partition_sha256 + ), + "observation_batch_sha256": verification_batch_digest, + } + ) + if ( + candidate_capture_sha256 != expected_candidate_capture_digest + or verification_capture_sha256 + != expected_verification_capture_digest + ): + raise ValueError( + "numerical current capture provenance does not authenticate its " + "complete observation batches" + ) + structural = _derive_current_value_equivalences(dag, model) + contracts = tuple( + _current_evaluation_contract(current) for current in dag.currents + ) + members_by_contract: dict[_CurrentContract, list[int]] = defaultdict(list) + for current_id, contract in enumerate(contracts): + members_by_contract[contract].append(current_id) + candidate_indexes = { + contract: _build_numerical_observation_candidate_index( + member_ids, + candidate_values, + ) + for contract, member_ids in members_by_contract.items() + } + prior_by_contract: dict[_CurrentContract, list[int]] = defaultdict(list) + source_ids = set(dag.sources) + relative_decimal = Decimal.from_float(relative) + absolute_decimal = Decimal.from_float(absolute) + certificates: list[NumericalCurrentRelationCertificate] = [] + rejected_candidates: list[dict[str, object]] = [] + nearest_rejected: tuple[ + Decimal, + Decimal, + Decimal, + int, + int, + str, + ] | None = None + structurally_proven_count = 0 + tested_hypothesis_count = 0 + theoretical_pair_hypothesis_count = 0 + screened_pair_hypothesis_count = 0 + numerical_candidate_count = 0 + verification_rejected_count = 0 + rejected_hypothesis_count = 0 + rejected_decision_sha256 = _canonical_payload_sha256( + { + "abi": "pyamplicol-generic-dag-rejected-decision-chain-v1", + "candidate_capture_sha256": candidate_capture_sha256, + "verification_capture_sha256": verification_capture_sha256, + } + ) + + def record_rejected( + *, + current_id: int, + representative_id: int | None, + relation_kind: str, + residuals: tuple[ + Decimal, + Decimal, + Decimal, + tuple[tuple[Decimal, Decimal], ...], + ], + reason: str, + ) -> None: + nonlocal nearest_rejected + nonlocal rejected_decision_sha256 + nonlocal rejected_hypothesis_count + rejected_hypothesis_count += 1 + candidate = ( + residuals[2], + residuals[0], + residuals[1], + current_id, + -1 if representative_id is None else representative_id, + relation_kind, + ) + if nearest_rejected is None or candidate < nearest_rejected: + nearest_rejected = candidate + row = { + "current_id": current_id, + "representative_id": representative_id, + "relation_kind": relation_kind, + "reason": reason, + "maximum_absolute_residual": ( + _canonical_decimal_string(residuals[0]) + ), + "maximum_relative_residual": ( + _canonical_decimal_string(residuals[1]) + ), + "maximum_tolerance_ratio": ( + _canonical_decimal_string(residuals[2]) + ), + } + rejected_decision_sha256 = _advance_rejection_digest( + rejected_decision_sha256, + row, + ) + if len(rejected_candidates) < _MAX_REJECTED_DIAGNOSTICS: + rejected_candidates.append(row) + + for current in dag.currents: + contract = contracts[current.id] + prior_ids = prior_by_contract.setdefault(contract, []) + if current.id in source_ids or current.is_source: + prior_ids.append(current.id) + continue + baseline = structural[current.id] + if ( + baseline.representative_id != current.id + or baseline.factor != (1.0, 0.0) + ): + structurally_proven_count += 1 + prior_ids.append(current.id) + continue + + equal_representatives = set( + _numerical_observation_tolerance_window_ids( + candidate_indexes[contract], + candidate_values[current.id], + relation_kind="equal", + current_id=current.id, + relative_tolerance=relative_decimal, + absolute_tolerance=absolute_decimal, + precision_digits=precision_digits, + ) + ) + opposite_representatives = set( + _numerical_observation_tolerance_window_ids( + candidate_indexes[contract], + candidate_values[current.id], + relation_kind="opposite", + current_id=current.id, + relative_tolerance=relative_decimal, + absolute_tolerance=absolute_decimal, + precision_digits=precision_digits, + ) + ) + theoretical_pair_hypothesis_count += 2 * len(prior_ids) + screened_pair_hypothesis_count += ( + len(equal_representatives) + len(opposite_representatives) + ) + hypotheses: list[ + tuple[ + Literal["equal", "opposite", "zero"], + int | None, + tuple[tuple[Decimal, Decimal], ...] | None, + tuple[tuple[Decimal, Decimal], ...] | None, + ] + ] = [ + ("zero", None, None, None), + *[ + ( + relation_kind, + representative_id, + candidate_values[representative_id], + verification_values[representative_id], + ) + for representative_id in sorted( + equal_representatives | opposite_representatives + ) + for relation_kind in ( + *( + ("equal",) + if representative_id in equal_representatives + else () + ), + *( + ("opposite",) + if representative_id in opposite_representatives + else () + ), + ) + ], + ] + accepted: NumericalCurrentRelationCertificate | None = None + for ( + relation_kind, + representative_id, + candidate_representative, + verification_representative, + ) in hypotheses: + tested_hypothesis_count += 1 + candidate_residuals = _numerical_relation_residuals( + relation_kind, + candidate_values[current.id], + candidate_representative, + relative_tolerance=relative_decimal, + absolute_tolerance=absolute_decimal, + precision_digits=precision_digits, + ) + if candidate_residuals is None: + raise ValueError( + "numerical current discovery produced a malformed hypothesis" + ) + if not _numerical_relation_residuals_pass( + candidate_residuals, + precision_digits=precision_digits, + ): + record_rejected( + current_id=current.id, + representative_id=representative_id, + relation_kind=relation_kind, + residuals=candidate_residuals, + reason="candidate-observations-not-equal", + ) + continue + numerical_candidate_count += 1 + accepted = certify_numerical_current_observations( + current_id=current.id, + representative_id=representative_id, + relation_kind=relation_kind, + source_semantics_sha256=source_digest, + runtime_schema_sha256=runtime_schema_sha256, + source_dag_sha256=source_dag_sha256, + candidate_capture_sha256=candidate_capture_sha256, + verification_capture_sha256=( + verification_capture_sha256 + ), + candidate_observation_batch_sha256=( + candidate_batch_digest + ), + verification_observation_batch_sha256=( + verification_batch_digest + ), + candidate_current_values=candidate_values[current.id], + candidate_representative_values=candidate_representative, + verification_current_values=verification_values[current.id], + verification_representative_values=( + verification_representative + ), + precision_digits=precision_digits, + seed=seed, + relative_tolerance=relative, + absolute_tolerance=absolute, + candidate_probe_count=len(candidate_points), + verification_probe_count=len(verification_points), + ) + if accepted is not None: + certificates.append(accepted) + break + verification_rejected_count += 1 + verification_residuals = _numerical_relation_residuals( + relation_kind, + verification_values[current.id], + verification_representative, + relative_tolerance=relative_decimal, + absolute_tolerance=absolute_decimal, + precision_digits=precision_digits, + ) + if verification_residuals is None: + raise ValueError( + "numerical current verification produced a malformed " + "hypothesis" + ) + record_rejected( + current_id=current.id, + representative_id=representative_id, + relation_kind=relation_kind, + residuals=verification_residuals, + reason="independent-verification-rejected-candidate", + ) + prior_ids.append(current.id) + + nearest_payload: dict[str, object] | None = None + if nearest_rejected is not None: + ( + tolerance_ratio, + absolute_residual, + relative_residual, + current_id, + representative_key, + nearest_relation_kind, + ) = nearest_rejected + nearest_payload = { + "current_id": current_id, + "representative_id": ( + None if representative_key < 0 else representative_key + ), + "relation_kind": nearest_relation_kind, + "maximum_absolute_residual": ( + _canonical_decimal_string(absolute_residual) + ), + "maximum_relative_residual": ( + _canonical_decimal_string(relative_residual) + ), + "maximum_tolerance_ratio": ( + _canonical_decimal_string(tolerance_ratio) + ), + } + certificate_tuple = tuple(certificates) + if rejected_hypothesis_count != ( + tested_hypothesis_count - len(certificate_tuple) + ): + raise AssertionError( + "generic numerical rejection census is internally inconsistent" + ) + state = ( + "certified_numerical_relations" + if certificate_tuple + else "no_certified_numerical_relation" + ) + report = NumericalCurrentObservationDiscoveryReport( + execution_mode=execution_mode, + color_accuracy=str(dag.process.color_accuracy), + source_semantics_sha256=source_digest, + runtime_schema_sha256=runtime_schema_sha256, + source_dag_sha256=source_dag_sha256, + precision_digits=precision_digits, + seed=seed, + relative_tolerance=relative, + absolute_tolerance=absolute, + candidate_point_sha256s=candidate_points, + verification_point_sha256s=verification_points, + candidate_kinematic_sha256s=candidate_kinematics, + verification_kinematic_sha256s=verification_kinematics, + candidate_parameter_context_sha256s=candidate_parameters, + verification_parameter_context_sha256s=verification_parameters, + candidate_capture_sha256=candidate_capture_sha256, + verification_capture_sha256=verification_capture_sha256, + candidate_observation_batch_sha256=candidate_batch_digest, + verification_observation_batch_sha256=verification_batch_digest, + state=state, + inspected_current_count=len(dag.currents), + structurally_proven_current_count=structurally_proven_count, + tested_hypothesis_count=tested_hypothesis_count, + theoretical_pair_hypothesis_count=( + theoretical_pair_hypothesis_count + ), + screened_pair_hypothesis_count=screened_pair_hypothesis_count, + candidate_index_contract_count=len(candidate_indexes), + exhaustive_fallback_contract_count=( + len(candidate_indexes) if relative_decimal >= 1 else 0 + ), + numerical_candidate_count=numerical_candidate_count, + verification_rejected_count=verification_rejected_count, + rejected_hypothesis_count=rejected_hypothesis_count, + rejected_decision_sha256=rejected_decision_sha256, + certificates=certificate_tuple, + rejected_candidates=tuple(rejected_candidates), + nearest_rejected_hypothesis=nearest_payload, + ) + return NumericalCurrentObservationDiscoveryResult( + certificates=certificate_tuple, + report=report, + structural_equivalences=structural, + ) + + +def _numerical_current_observation_batch_sha256( + observations: Mapping[ + int, + tuple[tuple[Decimal, Decimal], ...], + ], + *, + point_sha256s: tuple[str, ...], +) -> str: + return _canonical_payload_sha256( + { + "point_sha256s": list(point_sha256s), + "currents": [ + { + "current_id": current_id, + "values": [ + [ + _canonical_decimal_string(real), + _canonical_decimal_string(imaginary), + ] + for real, imaginary in observations[current_id] + ], + } + for current_id in sorted(observations) + ], + } + ) + + +def apply_numerical_current_relation_certificates( + dag: GenericDAG, + model: Model, + certificates: Iterable[NumericalCurrentRelationCertificate], + *, + mode: _DiscoveryMode, + execution_mode: Literal["compiled", "eager"], + process_id: str | None = None, + runtime_schema_sha256: str | None = None, + source_dag_sha256: str | None = None, + candidate_capture_sha256: str | None = None, + verification_capture_sha256: str | None = None, + _structural_equivalences: ( + tuple[_CurrentValueEquivalence, ...] | None + ) = None, +) -> NumericalCurrentRelationApplicationResult: + """Replay and apply one canonical authenticated numerical relation set. + + Application is monotonic with respect to the DAG's existing evaluator + partition. Certified zeros suppress only interactions that consume the + zero current or an exactly proven member of its structural class. + Equal/opposite mappings merge such an interaction into an already + compatible concrete evaluator group. Unaffected interactions are never + regrouped, and the evaluator count is forbidden to increase. + + Any malformed, stale, inconsistent, non-topological, or inexact binary64 + factor composition fails closed. A valid certificate for which no safe + downstream reuse opportunity exists remains recorded but unapplied. + """ + + if mode not in {"diagnostic", "certified-reuse"}: + raise ValueError(f"unsupported relation discovery mode {mode!r}") + source_digest = generic_dag_numerical_source_semantics_sha256( + dag, + execution_mode=execution_mode, + ) + ordered = tuple(certificates) + if ordered: + resolved_process_id = process_id or dag.process.key + actual_runtime_schema_digest = ( + generic_dag_numerical_runtime_schema_sha256( + dag, + model, + process_id=resolved_process_id, + ) + ) + actual_source_dag_digest = generic_dag_numerical_source_dag_sha256( + dag + ) + expected_runtime_schema_digest = ( + actual_runtime_schema_digest + if runtime_schema_sha256 is None + else runtime_schema_sha256 + ) + expected_source_dag_digest = ( + actual_source_dag_digest + if source_dag_sha256 is None + else source_dag_sha256 + ) + if ( + expected_runtime_schema_digest != actual_runtime_schema_digest + or expected_source_dag_digest != actual_source_dag_digest + or not _is_sha256(expected_runtime_schema_digest) + or not _is_sha256(expected_source_dag_digest) + ): + raise ValueError( + "numerical current application model/schema provenance " + "drifted from discovery" + ) + expected_candidate_capture_digest = ( + ordered[0].candidate_capture_sha256 + if candidate_capture_sha256 is None + else candidate_capture_sha256 + ) + expected_verification_capture_digest = ( + ordered[0].verification_capture_sha256 + if verification_capture_sha256 is None + else verification_capture_sha256 + ) + if ( + not _is_sha256(expected_candidate_capture_digest) + or not _is_sha256(expected_verification_capture_digest) + or expected_candidate_capture_digest + == expected_verification_capture_digest + ): + raise ValueError( + "numerical current application capture provenance is invalid" + ) + else: + expected_runtime_schema_digest = runtime_schema_sha256 + expected_source_dag_digest = source_dag_sha256 + expected_candidate_capture_digest = candidate_capture_sha256 + expected_verification_capture_digest = ( + verification_capture_sha256 + ) + if tuple(sorted(ordered, key=lambda item: item.current_id)) != ordered: + raise ValueError( + "numerical current certificates must use increasing current IDs" + ) + if len({certificate.current_id for certificate in ordered}) != len(ordered): + raise ValueError( + "numerical current certificates contain duplicate current IDs" + ) + + source_ids = set(dag.sources) + current_contracts = tuple( + _current_evaluation_contract(current) for current in dag.currents + ) + common_probe_contract: tuple[object, ...] | None = None + authenticated_relations: dict[int, _CurrentValueEquivalence] = {} + for certificate in ordered: + if not verify_numerical_current_relation_certificate( + certificate, + source_semantics_sha256=source_digest, + ): + raise ValueError( + f"numerical current certificate {certificate.current_id} " + "does not replay against the source DAG" + ) + if ( + certificate.runtime_schema_sha256 + != expected_runtime_schema_digest + or certificate.source_dag_sha256 + != expected_source_dag_digest + or certificate.candidate_capture_sha256 + != expected_candidate_capture_digest + or certificate.verification_capture_sha256 + != expected_verification_capture_digest + ): + raise ValueError( + "numerical current certificate evidence provenance drifted " + "from its application contract" + ) + current_id = certificate.current_id + if ( + current_id >= len(dag.currents) + or current_id in source_ids + or dag.currents[current_id].is_source + or certificate.current_dimension + != dag.currents[current_id].dimension + ): + raise ValueError( + "numerical current relations may target only matching " + "generated currents" + ) + probe_contract = ( + certificate.precision_digits, + certificate.seed, + certificate.relative_tolerance.hex(), + certificate.absolute_tolerance.hex(), + certificate.candidate_probe_count, + certificate.verification_probe_count, + certificate.runtime_schema_sha256, + certificate.source_dag_sha256, + certificate.candidate_capture_sha256, + certificate.verification_capture_sha256, + certificate.candidate_observation_batch_sha256, + certificate.verification_observation_batch_sha256, + ) + if common_probe_contract is None: + common_probe_contract = probe_contract + elif common_probe_contract != probe_contract: + raise ValueError( + "numerical current relation set mixes probe contracts" + ) + + if certificate.relation_kind == "zero": + compatible_representatives = tuple( + candidate_id + for candidate_id in range(current_id) + if current_contracts[candidate_id] + == current_contracts[current_id] + ) + execution_representative_id = ( + compatible_representatives[0] + if compatible_representatives + else current_id + ) + factor = (0.0, 0.0) + else: + representative_id = certificate.representative_id + if ( + representative_id is None + or representative_id >= current_id + or current_contracts[representative_id] + != current_contracts[current_id] + ): + raise ValueError( + "numerical current representative violates its " + "execution contract" + ) + execution_representative_id = representative_id + assert certificate.factor is not None + factor = certificate.factor + authenticated_relations[current_id] = _CurrentValueEquivalence( + representative_id=execution_representative_id, + factor=factor, + ) + + if ordered: + structural_equivalences = ( + _derive_current_value_equivalences(dag, model) + if _structural_equivalences is None + else _structural_equivalences + ) + if len(structural_equivalences) != len(dag.currents): + raise ValueError( + "numerical current structural equivalence cache drifted" + ) + baseline = _numerical_current_application_baseline(dag, model) + ( + projected, + resolved_relations, + certificate_chains, + projected_interaction_ids, + ) = _project_authenticated_numerical_interaction_reuse( + baseline, + model, + authenticated_relations, + structural_equivalences=structural_equivalences, + ) + else: + baseline = projected = dag + resolved_relations = tuple( + _CurrentValueEquivalence(current.id, (1.0, 0.0)) + for current in dag.currents + ) + certificate_chains = tuple(() for _current in dag.currents) + projected_interaction_ids = {} + + mappings = [ + NumericalCurrentAppliedMapping( + current_id=certificate.current_id, + representative_id=certificate.representative_id, + execution_representative_id=( + resolved_relations[certificate.current_id].representative_id + ), + relation_kind=certificate.relation_kind, + factor=resolved_relations[certificate.current_id].factor, + certificate_proof_sha256=certificate.proof_sha256, + resolved_current_ids=tuple( + current_id + for current_id, chain in enumerate(certificate_chains) + if certificate.current_id in chain + ), + projected_interaction_count=len( + projected_interaction_ids.get(certificate.current_id, ()) + ), + projected_interaction_ids_sha256=_canonical_payload_sha256( + { + "interaction_ids": list( + projected_interaction_ids.get( + certificate.current_id, + (), + ) + ) + } + ), + ) + for certificate in ordered + ] + + mapping_payloads = [mapping.to_json_dict() for mapping in mappings] + certificate_set_digest = _canonical_payload_sha256( + { + "abi": NUMERICAL_CURRENT_RELATION_SET_ABI, + "source_semantics_sha256": source_digest, + "execution_mode": execution_mode, + "certificates": [ + certificate.to_json_dict() for certificate in ordered + ], + "mappings": mapping_payloads, + } + ) + projected_relation_count = sum( + bool(interaction_ids) + for interaction_ids in projected_interaction_ids.values() + ) + apply_relations = ( + mode == "certified-reuse" and projected_relation_count > 0 + ) + if not ordered: + state = "no_certified_numerical_relation" + replay_status = "no_certified_numerical_relation" + elif apply_relations: + state = "authenticated-numerical-applied" + replay_status = "verified" + elif mode == "certified-reuse": + state = "authenticated-numerical-no-reuse-opportunity" + replay_status = "verified" + else: + state = "authenticated-numerical-diagnostic-only" + replay_status = "verified" + report = NumericalCurrentRelationApplicationReport( + requested_mode=mode, + execution_mode=execution_mode, + color_accuracy=str(dag.process.color_accuracy), + source_semantics_sha256=source_digest, + runtime_schema_sha256=expected_runtime_schema_digest, + source_dag_sha256=expected_source_dag_digest, + candidate_capture_sha256=expected_candidate_capture_digest, + verification_capture_sha256=( + expected_verification_capture_digest + ), + state=state, + certificate_replay_status=replay_status, + certificate_set_sha256=certificate_set_digest, + certificates=ordered, + mappings=tuple(mappings), + interaction_evaluation_count_before=( + baseline.interaction_evaluation_count + ), + interaction_evaluation_count_projected=( + projected.interaction_evaluation_count + ), + applied_relation_count=( + projected_relation_count if apply_relations else 0 + ), + ) + return NumericalCurrentRelationApplicationResult( + dag=projected if apply_relations else dag, + report=report, + ) + + +def _numerical_current_application_baseline( + dag: GenericDAG, + model: Model, +) -> GenericDAG: + """Retain an existing evaluator partition or establish one once. + + Final generation DAGs already carry their online structural grouping and + must not be globally regrouped by a numerical pass. Small API-built DAGs + with no evaluation metadata at all receive the ordinary exact structural + baseline before numerical projection. + """ + + has_evaluation_metadata = any( + interaction.evaluation_group_id is not None + or interaction.evaluation_factor != (1.0, 0.0) + for interaction in dag.interactions + ) + baseline = ( + dag + if has_evaluation_metadata + else assign_recursive_current_evaluation_reuse(dag, model) + ) + _validate_numerical_current_application_baseline(baseline, model) + return baseline + + +def _validate_numerical_current_application_baseline( + dag: GenericDAG, + model: Model, +) -> None: + """Authenticate every live member of the existing evaluator partition.""" + + structural = _derive_current_value_equivalences(dag, model) + equivalence_by_kind: dict[int, VertexEvaluationEquivalence] = {} + key_by_group: dict[int, _EvaluationKey] = {} + for interaction in dag.interactions: + if not _complex_weight_is_finite(interaction.evaluation_factor): + raise ValueError( + "numerical current application baseline has a non-finite " + "evaluation factor" + ) + if interaction.evaluation_factor == (0.0, 0.0): + continue + left = structural[interaction.left_id] + right = structural[interaction.right_id] + parent_factor = _exact_representable_complex_product( + left.factor, + right.factor, + ) + key, kernel_factor = _numerical_interaction_evaluation_key( + dag, + model, + interaction, + left_id=left.representative_id, + right_id=right.representative_id, + equivalence_by_kind=equivalence_by_kind, + ) + expected_factor = ( + None + if parent_factor is None + else _exact_representable_complex_product( + kernel_factor, + parent_factor, + ) + ) + if expected_factor is None or not _complex_weight_bits_equal( + _canonical_complex_weight(expected_factor), + _canonical_complex_weight(interaction.evaluation_factor), + ): + raise ValueError( + "numerical current application baseline evaluation metadata " + f"is unauthenticated for interaction {interaction.id}" + ) + group_id = interaction.evaluation_group_id + if group_id is None: + continue + prior = key_by_group.setdefault(group_id, key) + if prior != key: + raise ValueError( + "numerical current application baseline aliases distinct " + f"evaluation contracts in group {group_id}" + ) + + +def _resolve_authenticated_numerical_current_relations( + dag: GenericDAG, + authenticated_relations: Mapping[int, _CurrentValueEquivalence], + *, + structural_equivalences: tuple[_CurrentValueEquivalence, ...], +) -> tuple[ + tuple[_CurrentValueEquivalence, ...], + tuple[tuple[int, ...], ...], +]: + """Resolve authenticated chains through exact structural current classes.""" + + resolved: list[_CurrentValueEquivalence] = [] + certificate_chains: list[tuple[int, ...]] = [] + for expected_id, current in enumerate(dag.currents): + if current.id != expected_id: + raise ValueError( + "numerical current application requires contiguous current IDs" + ) + structural = structural_equivalences[current.id] + if not 0 <= structural.representative_id <= current.id: + raise ValueError( + "numerical current structural equivalence is not topological" + ) + authenticated = authenticated_relations.get(current.id) + if structural.representative_id != current.id: + if authenticated is not None: + raise ValueError( + "authenticated numerical relations must target exact " + "structural current representatives" + ) + representative = resolved[structural.representative_id] + factor = _exact_representable_complex_product( + structural.factor, + representative.factor, + ) + if factor is None: + raise ValueError( + "structural and authenticated numerical current factors " + "are not exactly composable" + ) + resolved.append( + _CurrentValueEquivalence( + representative_id=representative.representative_id, + factor=factor, + ) + ) + certificate_chains.append( + certificate_chains[structural.representative_id] + ) + continue + if structural.factor != (1.0, 0.0): + raise ValueError( + "self-represented structural current equivalence is not " + "canonical" + ) + if authenticated is None: + resolved.append( + _CurrentValueEquivalence(current.id, (1.0, 0.0)) + ) + certificate_chains.append(()) + continue + if authenticated.factor == (0.0, 0.0): + resolved.append(authenticated) + certificate_chains.append((current.id,)) + continue + if not 0 <= authenticated.representative_id < current.id: + raise ValueError( + "authenticated numerical current relation is not topological" + ) + representative = resolved[authenticated.representative_id] + factor = _exact_representable_complex_product( + authenticated.factor, + representative.factor, + ) + if factor is None: + raise ValueError( + "authenticated numerical current relation factor chain is " + "not exactly representable" + ) + resolved.append( + _CurrentValueEquivalence( + representative_id=representative.representative_id, + factor=factor, + ) + ) + certificate_chains.append( + ( + current.id, + *certificate_chains[authenticated.representative_id], + ) + ) + return tuple(resolved), tuple(certificate_chains) + + +def _numerical_interaction_evaluation_key( + dag: GenericDAG, + model: Model, + interaction: InteractionNode, + *, + left_id: int, + right_id: int, + equivalence_by_kind: dict[int, VertexEvaluationEquivalence], +) -> tuple[_EvaluationKey, _ComplexWeight]: + equivalence = _kernel_equivalence( + model, + interaction.vertex_kind, + equivalence_by_kind, + ) + canonical_inputs, kernel_factor = _canonical_kernel_evaluation( + equivalence, + left_id, + right_id, + ) + result = dag.currents[interaction.result_id] + return ( + ( + equivalence.class_id, + canonical_inputs, + int(result.index.particle_id), + int(result.index.chirality), + _runtime_coupling_identity( + model, + vertex_kind=interaction.vertex_kind, + vertex_particles=interaction.vertex_particles, + coupling=interaction.coupling, + ), + ), + kernel_factor, + ) + + +def _project_authenticated_numerical_interaction_reuse( + dag: GenericDAG, + model: Model, + authenticated_relations: Mapping[int, _CurrentValueEquivalence], + *, + structural_equivalences: tuple[_CurrentValueEquivalence, ...], +) -> tuple[ + GenericDAG, + tuple[_CurrentValueEquivalence, ...], + tuple[tuple[int, ...], ...], + dict[int, tuple[int, ...]], +]: + """Monotonically project authenticated current mappings onto interactions.""" + + if tuple(interaction.id for interaction in dag.interactions) != tuple( + range(len(dag.interactions)) + ): + raise ValueError( + "numerical current application requires contiguous interaction IDs" + ) + resolved, certificate_chains = ( + _resolve_authenticated_numerical_current_relations( + dag, + authenticated_relations, + structural_equivalences=structural_equivalences, + ) + ) + equivalence_by_kind: dict[int, VertexEvaluationEquivalence] = {} + targets_by_key: dict[ + _EvaluationKey, + tuple[int, _ComplexWeight], + ] = {} + for interaction in dag.interactions: + if ( + interaction.evaluation_factor == (0.0, 0.0) + or certificate_chains[interaction.left_id] + or certificate_chains[interaction.right_id] + ): + continue + key, kernel_factor = _numerical_interaction_evaluation_key( + dag, + model, + interaction, + left_id=interaction.left_id, + right_id=interaction.right_id, + equivalence_by_kind=equivalence_by_kind, + ) + prior = targets_by_key.get(key) + candidate_rank = ( + interaction.evaluation_group_id is None, + interaction.id, + ) + if prior is None: + targets_by_key[key] = (interaction.id, kernel_factor) + continue + prior_interaction = dag.interactions[prior[0]] + prior_rank = ( + prior_interaction.evaluation_group_id is None, + prior_interaction.id, + ) + if candidate_rank < prior_rank: + targets_by_key[key] = (interaction.id, kernel_factor) + + interactions = list(dag.interactions) + next_group_id = 1 + max( + ( + interaction.evaluation_group_id + for interaction in dag.interactions + if interaction.evaluation_group_id is not None + ), + default=-1, + ) + projected_interaction_ids: dict[int, list[int]] = defaultdict(list) + for interaction in dag.interactions: + left = resolved[interaction.left_id] + right = resolved[interaction.right_id] + left_chain = certificate_chains[interaction.left_id] + right_chain = certificate_chains[interaction.right_id] + if not left_chain and not right_chain: + continue + + if left.factor == (0.0, 0.0) or right.factor == (0.0, 0.0): + if interaction.evaluation_factor == (0.0, 0.0): + continue + interactions[interaction.id] = replace( + interaction, + evaluation_factor=(0.0, 0.0), + ) + used_chains = ( + *(left_chain if left.factor == (0.0, 0.0) else ()), + *(right_chain if right.factor == (0.0, 0.0) else ()), + ) + for certificate_id in set(used_chains): + projected_interaction_ids[certificate_id].append( + interaction.id + ) + continue + + parent_factor = _exact_representable_complex_product( + left.factor, + right.factor, + ) + if parent_factor is None: + continue + key, mapped_kernel_factor = _numerical_interaction_evaluation_key( + dag, + model, + interaction, + left_id=left.representative_id, + right_id=right.representative_id, + equivalence_by_kind=equivalence_by_kind, + ) + target = targets_by_key.get(key) + if target is None: + continue + target_id, target_kernel_factor = target + target_interaction = interactions[target_id] + effective_kernel_factor = _exact_representable_complex_product( + mapped_kernel_factor, + parent_factor, + ) + if effective_kernel_factor is None: + continue + relative_factor = _exact_representable_complex_ratio( + effective_kernel_factor, + target_kernel_factor, + ) + if relative_factor is None: + continue + evaluation_factor = _exact_representable_complex_product( + relative_factor, + target_interaction.evaluation_factor, + ) + if evaluation_factor is None or evaluation_factor == (0.0, 0.0): + continue + + target_group_id = target_interaction.evaluation_group_id + if target_group_id is None: + target_group_id = next_group_id + next_group_id += 1 + target_interaction = replace( + target_interaction, + evaluation_group_id=target_group_id, + ) + interactions[target_id] = target_interaction + if interaction.evaluation_group_id == target_group_id: + continue + interactions[interaction.id] = replace( + interaction, + evaluation_group_id=target_group_id, + evaluation_factor=evaluation_factor, + ) + for certificate_id in set((*left_chain, *right_chain)): + projected_interaction_ids[certificate_id].append(interaction.id) + + # A numerical projection may coarsen an existing evaluator partition, but + # it must never split one old live group. If only part of a cached group + # found a new target, retain that whole group unchanged and record no + # application for those members. + members_by_old_group: dict[tuple[str, int], list[int]] = defaultdict(list) + for interaction in dag.interactions: + if interaction.evaluation_factor == (0.0, 0.0): + continue + token = ( + ("group", interaction.evaluation_group_id) + if interaction.evaluation_group_id is not None + else ("interaction", interaction.id) + ) + members_by_old_group[token].append(interaction.id) + reverted_ids: set[int] = set() + for member_ids in members_by_old_group.values(): + projected_live_groups = { + ( + ("group", projected_member.evaluation_group_id) + if projected_member.evaluation_group_id is not None + else ("interaction", projected_member.id) + ) + for interaction_id in member_ids + if ( + projected_member := interactions[interaction_id] + ).evaluation_factor + != (0.0, 0.0) + } + if len(projected_live_groups) <= 1: + continue + for interaction_id in member_ids: + if interactions[interaction_id] != dag.interactions[interaction_id]: + interactions[interaction_id] = dag.interactions[ + interaction_id + ] + reverted_ids.add(interaction_id) + if reverted_ids: + for certificate_id in tuple(projected_interaction_ids): + retained = [ + interaction_id + for interaction_id in projected_interaction_ids[ + certificate_id + ] + if interaction_id not in reverted_ids + ] + if retained: + projected_interaction_ids[certificate_id] = retained + else: + del projected_interaction_ids[certificate_id] + + rewritten = tuple(interactions) + projected = ( + dag + if rewritten == dag.interactions + else _replace_dag_interactions_preserving_materialization( + dag, + rewritten, + ) + ) + if projected.interaction_evaluation_count > dag.interaction_evaluation_count: + raise ValueError( + "authenticated numerical current application would increase " + "the interaction evaluator count" + ) + for member_ids in members_by_old_group.values(): + projected_live_groups = { + ( + ("group", interaction.evaluation_group_id) + if interaction.evaluation_group_id is not None + else ("interaction", interaction.id) + ) + for interaction_id in member_ids + if ( + interaction := projected.interactions[interaction_id] + ).evaluation_factor + != (0.0, 0.0) + } + if len(projected_live_groups) > 1: + raise ValueError( + "authenticated numerical current application would split " + "an existing evaluator group" + ) + canonical_projected_ids = { + certificate_id: tuple(interaction_ids) + for certificate_id, interaction_ids in projected_interaction_ids.items() + } + return projected, resolved, certificate_chains, canonical_projected_ids + + +def _validated_decimal_observations( + values: Sequence[tuple[Decimal, Decimal]] | None, +) -> tuple[tuple[Decimal, Decimal], ...] | None: + if values is None or isinstance(values, str | bytes): + return None + result: list[tuple[Decimal, Decimal]] = [] + try: + for value in values: + if ( + not isinstance(value, tuple) + or len(value) != 2 + or not all(isinstance(component, Decimal) for component in value) + or not all(component.is_finite() for component in value) + ): + return None + result.append((value[0], value[1])) + except TypeError: + return None + return tuple(result) + + +def _numerical_relation_residuals( + relation_kind: Literal["equal", "opposite", "zero"], + current_values: tuple[tuple[Decimal, Decimal], ...], + representative_values: tuple[tuple[Decimal, Decimal], ...] | None, + *, + relative_tolerance: Decimal, + absolute_tolerance: Decimal, + precision_digits: int, +) -> tuple[ + Decimal, + Decimal, + Decimal, + tuple[tuple[Decimal, Decimal], ...], +] | None: + if type(precision_digits) is not int or precision_digits < 80: + raise ValueError("numerical residual precision is invalid") + with localcontext() as context: + context.prec = precision_digits + _NUMERICAL_DECIMAL_GUARD_DIGITS + context.rounding = ROUND_HALF_EVEN + if relation_kind == "zero": + expected = tuple( + (Decimal(0), Decimal(0)) for _ in current_values + ) + else: + if representative_values is None or len( + representative_values + ) != len(current_values): + return None + sign = Decimal(1) if relation_kind == "equal" else Decimal(-1) + expected = tuple( + (sign * real, sign * imaginary) + for real, imaginary in representative_values + ) + maximum_absolute = Decimal(0) + maximum_relative = Decimal(0) + maximum_tolerance_ratio = Decimal(0) + component_checks: list[tuple[Decimal, Decimal]] = [] + for current, reference in zip( + current_values, + expected, + strict=True, + ): + difference = max( + abs(current[0] - reference[0]), + abs(current[1] - reference[1]), + ) + scale = max( + abs(current[0]), + abs(current[1]), + abs(reference[0]), + abs(reference[1]), + ) + relative = ( + Decimal(0) + if difference == 0 + else difference / max(scale, absolute_tolerance) + ) + maximum_absolute = max(maximum_absolute, difference) + maximum_relative = max(maximum_relative, relative) + allowed = absolute_tolerance + relative_tolerance * scale + tolerance_ratio = ( + Decimal(0) if difference == 0 else difference / allowed + ) + maximum_tolerance_ratio = max( + maximum_tolerance_ratio, + tolerance_ratio, + ) + component_checks.append((difference, allowed)) + return ( + maximum_absolute, + maximum_relative, + maximum_tolerance_ratio, + tuple(component_checks), + ) + + +def _numerical_relation_residuals_pass( + residuals: tuple[ + Decimal, + Decimal, + Decimal, + tuple[tuple[Decimal, Decimal], ...], + ], + *, + precision_digits: int, +) -> bool: + if type(precision_digits) is not int or precision_digits < 80: + raise ValueError("numerical residual precision is invalid") + with localcontext() as context: + context.prec = precision_digits + _NUMERICAL_DECIMAL_GUARD_DIGITS + context.rounding = ROUND_HALF_EVEN + return all( + difference <= allowed + for difference, allowed in residuals[3] + ) + + +def _numerical_observations_sha256( + *, + relation_kind: str, + current_values: tuple[tuple[Decimal, Decimal], ...], + representative_values: tuple[tuple[Decimal, Decimal], ...] | None, +) -> str: + return _canonical_payload_sha256( + { + "relation_kind": relation_kind, + "current": [ + [ + _canonical_decimal_string(real), + _canonical_decimal_string(imaginary), + ] + for real, imaginary in current_values + ], + "representative": ( + None + if representative_values is None + else [ + [ + _canonical_decimal_string(real), + _canonical_decimal_string(imaginary), + ] + for real, imaginary in representative_values + ] + ), + } + ) + + +def _canonical_decimal_string(value: Decimal) -> str: + if not value.is_finite(): + raise ValueError("canonical Decimal serialization requires finiteness") + sign, digits, exponent = value.as_tuple() + if not any(digits): + return "0" + if not isinstance(exponent, int): + raise ValueError("canonical Decimal serialization requires an exponent") + coefficient = "".join(str(digit) for digit in digits) + return f"{'-' if sign else ''}{coefficient}e{exponent}" + + +def _is_sha256(value: object) -> bool: + return ( + isinstance(value, str) + and len(value) == 64 + and all(character in "0123456789abcdef" for character in value) + ) + + +def _decode_numerical_relation_factor( + relation_kind: object, + representative_id: object, + payload: object, +) -> _ComplexWeight | None: + expected: _ComplexWeight | None + if relation_kind == "zero": + if representative_id is not None or payload is not None: + raise ValueError("zero-current relation cannot have a representative") + return None + if relation_kind == "equal": + expected = (1.0, 0.0) + elif relation_kind == "opposite": + expected = (-1.0, 0.0) + else: + raise ValueError("unsupported numerical current relation kind") + if ( + type(representative_id) is not int + or not isinstance(payload, list) + or len(payload) != 2 + or any(not isinstance(component, str) for component in payload) + ): + raise ValueError("numerical current relation factor is malformed") + try: + decoded = (float.fromhex(payload[0]), float.fromhex(payload[1])) + except ValueError as error: + raise ValueError("numerical current relation factor is malformed") from error + if not _complex_weight_bits_equal(decoded, expected): + raise ValueError("numerical current relation factor disagrees with its kind") + return expected + + +@dataclass(frozen=True, slots=True) +class RelationDiscoveryReport: + """Mode- and colour-scoped outcome of one opt-in discovery pass.""" + + requested_mode: str + state: str + execution_mode: str + color_accuracy: str + representation: str + precision_digits: int + probe_count: int + seed: int + probe_status: str + certificate_replay_status: str + numerical_candidate_count: int + uncertified_candidate_count: int + exact_certified_relation_count: int + applied_relation_count: int + interaction_evaluation_count_before: int | None + interaction_evaluation_count_after: int | None + certificates: tuple[ExactCurrentRelationCertificate, ...] = () + rejected_candidates: tuple[dict[str, object], ...] = () + rejected_decision_sha256: str = "" + follow_up_boundary: str | None = None + + def to_json_dict(self) -> dict[str, object]: + return { + "schema_version": RELATION_DISCOVERY_SCHEMA_VERSION, + "requested_mode": self.requested_mode, + "state": self.state, + "scope": { + "execution_mode": self.execution_mode, + "color_accuracy": self.color_accuracy, + "representation": self.representation, + }, + "probe": { + "status": self.probe_status, + "precision_digits": self.precision_digits, + "probe_count": self.probe_count, + "seed": self.seed, + "deterministic": True, + "candidate_only": True, + }, + "certificate_replay": { + "algorithm": RELATION_DISCOVERY_CERTIFICATE_ALGORITHM, + "status": self.certificate_replay_status, + }, + "numerical_candidate_count": self.numerical_candidate_count, + "uncertified_candidate_count": self.uncertified_candidate_count, + "rejected_hypothesis_count": self.uncertified_candidate_count, + "exact_certified_relation_count": (self.exact_certified_relation_count), + "applied_relation_count": self.applied_relation_count, + "interaction_evaluation_count_before": ( + self.interaction_evaluation_count_before + ), + "interaction_evaluation_count_after": ( + self.interaction_evaluation_count_after + ), + "interaction_evaluation_savings": ( + None + if self.interaction_evaluation_count_before is None + or self.interaction_evaluation_count_after is None + else max( + 0, + self.interaction_evaluation_count_before + - self.interaction_evaluation_count_after, + ) + ), + "certificates": [ + certificate.to_json_dict() for certificate in self.certificates + ], + "rejected_candidates": list(self.rejected_candidates), + "rejected_candidate_diagnostics": ( + _rejected_candidate_diagnostics( + self.rejected_candidates, + total_rejected=self.uncertified_candidate_count, + full_census={ + "requested_mode": self.requested_mode, + "numerical_candidate_count": ( + self.numerical_candidate_count + ), + "uncertified_candidate_count": ( + self.uncertified_candidate_count + ), + "rejected_hypothesis_count": ( + self.uncertified_candidate_count + ), + "exact_certified_relation_count": ( + self.exact_certified_relation_count + ), + "applied_relation_count": self.applied_relation_count, + "rejection_decision_sha256": ( + self.rejected_decision_sha256 + ), + }, + full_rejection_sha256=self.rejected_decision_sha256, + ) + ), + "follow_up_boundary": self.follow_up_boundary, + } + + +@dataclass(frozen=True, slots=True) +class RelationDiscoveryResult: + dag: GenericDAG + report: RelationDiscoveryReport + + +class _NumericalRelationDiscovery: + """Nominate projective pairs numerically, then require an exact replay.""" + + def __init__( + self, + *, + precision_digits: int, + probe_count: int, + seed: int, + ) -> None: + if precision_digits < 80: + raise ValueError("relation discovery precision must be at least 80 digits") + if probe_count < 2: + raise ValueError("relation discovery requires at least two probe points") + if seed < 0: + raise ValueError("relation discovery seed must be nonnegative") + self.precision_digits = int(precision_digits) + self.probe_count = int(probe_count) + self.seed = int(seed) + self.numerical_candidate_count = 0 + self.uncertified_candidate_count = 0 + self.certificates: list[ExactCurrentRelationCertificate] = [] + self.rejected_candidates: list[dict[str, object]] = [] + self.rejected_decision_sha256 = _canonical_payload_sha256( + { + "abi": "pyamplicol-exact-rejected-decision-chain-v1", + "precision_digits": self.precision_digits, + "probe_count": self.probe_count, + "seed": self.seed, + } + ) + self._representatives_by_fingerprint: dict[ + tuple[_CurrentContract, tuple[object, ...]], + list[tuple[int, _CurrentTermVector]], + ] = {} + + def record_rejected(self, row: Mapping[str, object]) -> None: + payload = dict(row) + self.rejected_decision_sha256 = _advance_rejection_digest( + self.rejected_decision_sha256, + payload, + ) + if len(self.rejected_candidates) < _MAX_REJECTED_DIAGNOSTICS: + self.rejected_candidates.append(payload) + + def consider( + self, + *, + current_id: int, + contract: _CurrentContract, + term_vector: _CurrentTermVector, + baseline: _CurrentValueEquivalence, + ) -> _CurrentValueEquivalence: + fingerprint = _numerical_projective_fingerprint( + term_vector, + precision_digits=self.precision_digits, + probe_count=self.probe_count, + seed=self.seed, + ) + if fingerprint is None: + return baseline + key = (contract, fingerprint) + representatives = self._representatives_by_fingerprint.setdefault(key, []) + + promoted = baseline + if baseline.representative_id == current_id and baseline.factor == (1.0, 0.0): + exact_matches: list[tuple[int, _ComplexWeight, _CurrentTermVector]] = [] + for representative_id, representative_vector in representatives: + self.numerical_candidate_count += 1 + factor = _certify_exact_term_vector_relation( + representative_vector, + term_vector, + ) + if factor is None: + self.uncertified_candidate_count += 1 + self.record_rejected( + { + "current_id": current_id, + "representative_id": representative_id, + "reason": "numerical-candidate-lacks-exact-certificate", + } + ) + continue + exact_matches.append((representative_id, factor, representative_vector)) + if exact_matches: + representative_id, factor, representative_vector = min( + exact_matches, + key=lambda item: item[0], + ) + certificate = _build_exact_current_relation_certificate( + current_id=current_id, + representative_id=representative_id, + factor=factor, + current_term_vector=term_vector, + representative_term_vector=representative_vector, + ) + self.certificates.append(certificate) + promoted = _CurrentValueEquivalence( + representative_id=representative_id, + factor=factor, + ) + + representatives.append((current_id, term_vector)) + return promoted + + +class RecursiveEvaluationReuseTracker: + """Certify recursive-current reuse as external subsets are completed.""" + + def __init__(self, model: Model) -> None: + self._model = model + self._kernel_equivalences: dict[int, VertexEvaluationEquivalence] = {} + self._runtime_coupling_identities: dict[ + tuple[int, tuple[int, int, int], tuple[float, float]], + tuple[tuple[float, float], tuple[tuple[int, str], ...]], + ] = {} + self._current_equivalences: list[_CurrentValueEquivalence] = [] + self._source_representative_by_key: dict[tuple[object, ...], int] = {} + self._equivalence_by_expression: dict[ + tuple[_CurrentContract, _CurrentTermVector], _CurrentValueEquivalence + ] = {} + self._projective_representatives_by_expression: dict[ + tuple[_CurrentContract, _CurrentTermVector], + list[_ProjectiveExpressionRepresentative], + ] = {} + self._evaluation_group_by_key: dict[_EvaluationKey, int] = {} + self._coefficients_by_result: list[ + dict[int, _ComplexWeight | list[_ComplexWeight]] | None + ] = [] + + def register_source(self, current: CurrentNode) -> None: + if not current.is_source: + raise ValueError("recursive-reuse source registration requires a source") + if current.source_leg_label is None or current.source_helicity is None: + raise ValueError( + f"source current {current.id} lacks physical source metadata" + ) + contract = _current_evaluation_contract(current) + source_key = ( + contract, + int(current.source_leg_label), + int(current.source_helicity), + ) + representative_id = self._source_representative_by_key.setdefault( + source_key, + current.id, + ) + self._append_current_equivalence( + current, + _CurrentValueEquivalence(representative_id, (1.0, 0.0)), + ) + + def interaction_evaluation( + self, + *, + vertex_kind: int, + vertex_particles: tuple[int, int, int], + left_id: int, + right_id: int, + result: CurrentNode, + coupling: tuple[float, float], + color_weight: _ComplexWeight, + ) -> tuple[int, _ComplexWeight]: + kernel_equivalence = _kernel_equivalence( + self._model, + vertex_kind, + self._kernel_equivalences, + ) + try: + left = self._current_equivalences[left_id] + right = self._current_equivalences[right_id] + except IndexError as error: + raise ValueError( + "online recursive reuse requires completed parent subsets" + ) from error + canonical_inputs, evaluation_factor = _canonical_interaction_evaluation( + kernel_equivalence, + left_id=left_id, + right_id=right_id, + left=left, + right=right, + ) + coupling_key = (vertex_kind, vertex_particles, coupling) + coupling_identity = self._runtime_coupling_identities.get(coupling_key) + if coupling_identity is None: + coupling_identity = _runtime_coupling_identity( + self._model, + vertex_kind=vertex_kind, + vertex_particles=vertex_particles, + coupling=coupling, + ) + self._runtime_coupling_identities[coupling_key] = coupling_identity + evaluation_key = ( + kernel_equivalence.class_id, + canonical_inputs, + int(result.index.particle_id), + int(result.index.chirality), + coupling_identity, + ) + evaluation_group_id = self._evaluation_group_by_key.setdefault( + evaluation_key, + len(self._evaluation_group_by_key), + ) + if result.id >= len(self._coefficients_by_result): + self._coefficients_by_result.extend( + [None] * (result.id + 1 - len(self._coefficients_by_result)) + ) + coefficients_by_group = self._coefficients_by_result[result.id] + if coefficients_by_group is None: + coefficients_by_group = {} + self._coefficients_by_result[result.id] = coefficients_by_group + coefficient = _complex_weight_mul(color_weight, evaluation_factor) + coefficients = coefficients_by_group.get(evaluation_group_id) + if coefficients is None: + coefficients_by_group[evaluation_group_id] = coefficient + elif isinstance(coefficients, list): + coefficients.append(coefficient) + else: + coefficients_by_group[evaluation_group_id] = [coefficients, coefficient] + return evaluation_group_id, evaluation_factor + + def finalize_currents( + self, + currents: Iterable[CurrentNode], + ) -> None: + for current in currents: + if current.is_source: + raise ValueError("generated-current finalization received a source") + coefficients_by_group = self._coefficients_by_result[current.id] + self._coefficients_by_result[current.id] = None + terms: list[tuple[_EvaluationKey, _ComplexWeight]] = [] + for group_id in sorted(coefficients_by_group or ()): + assert coefficients_by_group is not None + coefficients = coefficients_by_group[group_id] + if isinstance(coefficients, list): + coefficient = ( + _canonical_zero(fsum(value[0] for value in coefficients)), + _canonical_zero(fsum(value[1] for value in coefficients)), + ) + else: + coefficient = _canonical_complex_weight(coefficients) + if coefficient != (0.0, 0.0): + terms.append(((group_id,), coefficient)) + term_vector = tuple(terms) + contract = _current_evaluation_contract(current) + equivalence = _classify_current_term_vector( + current_id=current.id, + contract=contract, + term_vector=term_vector, + equivalence_by_expression=self._equivalence_by_expression, + projective_representatives_by_expression=( + self._projective_representatives_by_expression + ), + ) + self._append_current_equivalence( + current, + equivalence, + ) + + def _append_current_equivalence( + self, + current: CurrentNode, + equivalence: _CurrentValueEquivalence, + ) -> None: + if current.id != len(self._current_equivalences): + raise ValueError( + "online recursive reuse requires currents in contiguous ID order" + ) + self._current_equivalences.append(equivalence) + if current.id >= len(self._coefficients_by_result): + self._coefficients_by_result.append(None) + + +def _canonical_kernel_evaluation( + equivalence: VertexEvaluationEquivalence, + left_id: int, + right_id: int, +) -> tuple[tuple[int, int], _ComplexWeight]: + """Return canonical representative inputs and the concrete-kernel factor.""" + + canonical_inputs = (left_id, right_id) + if equivalence.input_order == (1, 0): + canonical_inputs = (right_id, left_id) + factor = equivalence.factor + if ( + equivalence.input_exchange_factor is not None + and canonical_inputs[1] < canonical_inputs[0] + ): + canonical_inputs = (canonical_inputs[1], canonical_inputs[0]) + factor = _complex_weight_mul( + factor, + equivalence.input_exchange_factor, + ) + return canonical_inputs, factor + + +def _canonical_interaction_evaluation( + equivalence: VertexEvaluationEquivalence, + *, + left_id: int, + right_id: int, + left: _CurrentValueEquivalence, + right: _CurrentValueEquivalence, +) -> tuple[tuple[int, int], _ComplexWeight]: + """Compose recursive factors exactly or retain the concrete inputs. + + Projective equivalence is an exact statement about the ideal algebraic + current. Its recursive factor may be moved through another kernel only + when both complex products remain finite and exactly representable as + binary64. Otherwise this interaction fails closed to its concrete parent + IDs while retaining the pre-existing model-certified kernel symmetry. + """ + + canonical_inputs, kernel_factor = _canonical_kernel_evaluation( + equivalence, + left.representative_id, + right.representative_id, + ) + parent_factor = _exact_representable_complex_product(left.factor, right.factor) + if parent_factor is not None: + evaluation_factor = _exact_representable_complex_product( + kernel_factor, + parent_factor, + ) + if evaluation_factor is not None: + return canonical_inputs, evaluation_factor + return _canonical_kernel_evaluation(equivalence, left_id, right_id) + + +def assign_recursive_current_evaluation_reuse( + dag: GenericDAG, + model: Model, +) -> GenericDAG: + """Share kernel evaluations through exactly proven current equivalences. + + The proof is recursive. Duplicate source wavefunctions form the base + classes. A generated current joins an existing class only when its full + vector of model-certified kernel terms and coefficients is byte-exactly + equal, opposite, or algebraically projective through a finite nonzero + binary64 factor whose coefficient reconstruction is bit-exact. Moving a + general projective factor changes floating-point association, so runtime + parity remains a tolerance-checked numerical contract rather than a claim + of bit-identical materialized currents. Recursive factor products fail + closed unless they are finite and exactly representable. The current + contract keeps every field consumed by source, kernel, and propagator + evaluation. In particular, ordered external labels and helicity + ancestry remain part of the contract: both encode physical runtime + routing and cannot be inferred from the sorted external-label set. + Dynamic colour-sector bookkeeping is deliberately excluded. Input + ordering may differ only through the exact model-certified permutation + and reflection factors included in the term signature below. + + This recovers AmpliCol-style reflection fan-out, but also recognizes exact + reuse across colour sectors and helicity subgraphs. No approximate + numerical comparison or process/model-family classification is involved. + """ + + if not dag.interactions: + return dag + + equivalence_by_kind: dict[int, VertexEvaluationEquivalence] = {} + current_equivalences = _derive_current_value_equivalences( + dag, + model, + equivalence_by_kind=equivalence_by_kind, + ) + return _rewrite_interaction_evaluation_reuse( + dag, + model, + current_equivalences=current_equivalences, + equivalence_by_kind=equivalence_by_kind, + ) + + +def discover_recursive_evaluation_relations( + dag: GenericDAG, + model: Model, + *, + mode: _DiscoveryMode, + execution_mode: Literal["compiled", "eager"], + precision_digits: int = 96, + probe_count: int = 4, + seed: int = 0x5059414D, +) -> RelationDiscoveryResult: + """Discover candidates numerically and promote only exact DAG relations. + + Probe values are deterministic high-precision synthetic evaluations of the + full current term vectors. They are candidate indexes only. Every promoted + relation is independently replayed over the complete binary64 coefficient + vectors, and unsafe recursive factor composition continues to fail closed. + """ + + if mode not in {"diagnostic", "certified-reuse"}: + raise ValueError(f"unsupported relation discovery mode {mode!r}") + if execution_mode not in {"compiled", "eager"}: + raise ValueError( + "GenericDAG relation discovery supports compiled and eager execution" + ) + + baseline = assign_recursive_current_evaluation_reuse(dag, model) + discovery = _NumericalRelationDiscovery( + precision_digits=precision_digits, + probe_count=probe_count, + seed=seed, + ) + equivalence_by_kind: dict[int, VertexEvaluationEquivalence] = {} + current_equivalences = _derive_current_value_equivalences( + dag, + model, + equivalence_by_kind=equivalence_by_kind, + discovery=discovery, + ) + promoted = _rewrite_interaction_evaluation_reuse( + dag, + model, + current_equivalences=current_equivalences, + equivalence_by_kind=equivalence_by_kind, + ) + if discovery.certificates and not verify_dag_relation_certificates( + dag, + model, + discovery.certificates, + ): + discovery.uncertified_candidate_count += len(discovery.certificates) + for certificate in discovery.certificates: + discovery.record_rejected( + { + "reason": "exact-certificate-chain-replay-failed", + "current_id": certificate.current_id, + "representative_id": certificate.representative_id, + } + ) + discovery.certificates.clear() + promoted = baseline + interaction_changed = _interaction_reuse_signature( + promoted + ) != _interaction_reuse_signature(baseline) + apply_promotions = ( + mode == "certified-reuse" + and bool(discovery.certificates) + and interaction_changed + ) + result_dag = promoted if apply_promotions else dag + report = RelationDiscoveryReport( + requested_mode=mode, + state=("exact-certified-applied" if apply_promotions else "diagnostic-only"), + execution_mode=execution_mode, + color_accuracy=str(dag.process.color_accuracy), + representation="generic-dag", + precision_digits=precision_digits, + probe_count=probe_count, + seed=seed, + probe_status="completed", + certificate_replay_status=( + "verified" if discovery.certificates else "no-certified-relations" + ), + numerical_candidate_count=discovery.numerical_candidate_count, + uncertified_candidate_count=discovery.uncertified_candidate_count, + exact_certified_relation_count=len(discovery.certificates), + applied_relation_count=(len(discovery.certificates) if apply_promotions else 0), + interaction_evaluation_count_before=baseline.interaction_evaluation_count, + interaction_evaluation_count_after=( + promoted.interaction_evaluation_count + if apply_promotions or mode == "diagnostic" + else baseline.interaction_evaluation_count + ), + certificates=tuple(discovery.certificates), + rejected_candidates=tuple(discovery.rejected_candidates), + rejected_decision_sha256=discovery.rejected_decision_sha256, + follow_up_boundary=( + "direct vertex-kernel equivalence remains model-certificate-owned; " + "this pass promotes exact current proportionality and its induced " + "interaction fan-out only" + ), + ) + return RelationDiscoveryResult(dag=result_dag, report=report) + + +def _rewrite_interaction_evaluation_reuse( + dag: GenericDAG, + model: Model, + *, + current_equivalences: tuple[_CurrentValueEquivalence, ...], + equivalence_by_kind: dict[int, VertexEvaluationEquivalence], +) -> GenericDAG: + evaluation_group_by_key: dict[_EvaluationKey, int] = {} + interactions: list[InteractionNode] = [] + + for interaction in dag.interactions: + kernel_equivalence = _kernel_equivalence( + model, + interaction.vertex_kind, + equivalence_by_kind, + ) + left = current_equivalences[interaction.left_id] + right = current_equivalences[interaction.right_id] + canonical_inputs, evaluation_factor = _canonical_interaction_evaluation( + kernel_equivalence, + left_id=interaction.left_id, + right_id=interaction.right_id, + left=left, + right=right, + ) + result = dag.currents[interaction.result_id] + evaluation_key = ( + kernel_equivalence.class_id, + canonical_inputs, + int(result.index.particle_id), + int(result.index.chirality), + _runtime_coupling_identity( + model, + vertex_kind=interaction.vertex_kind, + vertex_particles=interaction.vertex_particles, + coupling=interaction.coupling, + ), + ) + evaluation_group_id = evaluation_group_by_key.setdefault( + evaluation_key, + len(evaluation_group_by_key), + ) + interactions.append( + replace( + interaction, + evaluation_group_id=evaluation_group_id, + evaluation_factor=evaluation_factor, + ) + ) + + rewritten = tuple(interactions) + if rewritten == dag.interactions: + return dag + return _replace_dag_interactions_preserving_materialization(dag, rewritten) + + +def _replace_dag_interactions_preserving_materialization( + dag: GenericDAG, + interactions: tuple[InteractionNode, ...], +) -> GenericDAG: + """Replace evaluation metadata without invalidating a quotient witness.""" + + materialization = dag.helicity_materialization + if materialization is None: + return replace(dag, interactions=interactions) + rebound_dag = replace( + dag, + interactions=interactions, + helicity_materialization=None, + ) + rebound_materialization = replace(materialization, dag=rebound_dag) + return replace( + rebound_dag, + helicity_materialization=rebound_materialization, + ) + + +def _canonical_sha256(value: object) -> str: + payload = json.dumps( + value, + sort_keys=True, + separators=(",", ":"), + ensure_ascii=True, + ).encode("utf-8") + return hashlib.sha256(payload).hexdigest() + + +def _resolved_root_color_sector_id(dag: GenericDAG, root: AmplitudeRoot) -> int: + """Resolve the physical sector before any representative is selected.""" + + if root.color_sector_id is not None: + return int(root.color_sector_id) + return int(dag.currents[root.left_id].index.color_state.sector_id) + + +def _downstream_color_selector_domains( + dag: GenericDAG, +) -> tuple[tuple[int, ...], ...]: + """Return the complete physical LC selector domain of every current.""" + + domains: list[set[int]] = [set() for _current in dag.currents] + for root in dag.amplitude_roots: + sector_id = _resolved_root_color_sector_id(dag, root) + domains[root.left_id].add(sector_id) + domains[root.right_id].add(sector_id) + for interaction in reversed(dag.interactions): + result_domains = domains[interaction.result_id] + if not result_domains: + continue + domains[interaction.left_id].update(result_domains) + domains[interaction.right_id].update(result_domains) + return tuple(tuple(sorted(values)) for values in domains) + + +def _dynamic_color_projection_key( + current: CurrentNode, + selector_domains: tuple[int, ...], +) -> tuple[object, ...]: + """Return exact current identity with only dynamic LC sector omitted. + + ``basis_key`` participates in coherent amplitude grouping and eager + metadata, while ``line_groups`` is the exact open-colour identity. Both + therefore remain invariant within a projected class. The lowerings use + particle/chirality/momentum plus the explicit row identity for numerical + kernels; the retained representative sector is metadata only after every + amplitude root has received its pre-projection resolved sector. + """ + + index = current.index + return ( + int(index.particle_id), + int(index.external_mask), + index.external_labels, + index.ordered_external_labels, + int(index.helicity_ancestry), + int(index.chirality), + index.spin_state, + index.flavour_flow, + index.quantum_number_flow, + index.color_state.accuracy, + index.color_state.line_groups, + index.color_state.basis_key, + int(index.momentum_mask), + index.coupling_orders, + index.auxiliary_kind, + int(current.dimension), + selector_domains, + ) + + +def _projection_partition( + dag: GenericDAG, + selector_domains: tuple[tuple[int, ...], ...], + split_current_ids: set[int], +) -> tuple[tuple[tuple[int, ...], ...], tuple[int, ...]]: + grouped: dict[tuple[object, ...], list[int]] = defaultdict(list) + for current in dag.currents: + # External source rows remain singleton. Their physical source route + # is an initialization contract, not a multilinear recurrence row. + if current.is_source or current.id in split_current_ids: + key = ("singleton", current.id) + else: + key = ( + "dynamic-color", + _dynamic_color_projection_key( + current, + selector_domains[current.id], + ), + ) + grouped[key].append(current.id) + classes = tuple( + sorted( + (tuple(sorted(members)) for members in grouped.values()), + key=lambda members: ( + len(dag.currents[members[0]].index.external_labels), + members[0], + ), + ) + ) + class_by_current = [-1] * len(dag.currents) + for class_id, members in enumerate(classes): + for current_id in members: + class_by_current[current_id] = class_id + if any(class_id < 0 for class_id in class_by_current): + raise ValueError("dynamic-color projection left a current unpartitioned") + return classes, tuple(class_by_current) + + +def _interaction_projection_key( + interaction: InteractionNode, + class_by_current: tuple[int, ...], + *, + keep_row_distinct: bool, +) -> tuple[object, ...]: + return ( + class_by_current[interaction.result_id], + class_by_current[interaction.left_id], + class_by_current[interaction.right_id], + int(interaction.vertex_kind), + interaction.vertex_particles, + interaction.coupling, + interaction.color_weight, + interaction.lowering_backend, + bool(interaction.full_tensor_network_ready), + interaction.id if keep_row_distinct else None, + ) + + +def _root_projection_key( + dag: GenericDAG, + root: AmplitudeRoot, + class_by_current: tuple[int, ...], + *, + keep_row_distinct: bool, +) -> tuple[object, ...]: + return ( + class_by_current[root.left_id], + class_by_current[root.right_id], + root.kind, + root.color_weight, + root.contraction_ir, + _resolved_root_color_sector_id(dag, root), + root.vertex_kind, + root.vertex_particles, + root.coupling, + float(root.helicity_weight), + root.id if keep_row_distinct else None, + ) + + +def _projection_groups( + dag: GenericDAG, + classes: tuple[tuple[int, ...], ...], + class_by_current: tuple[int, ...], +) -> tuple[ + dict[tuple[object, ...], list[InteractionNode]], + dict[tuple[object, ...], list[AmplitudeRoot]], +]: + interaction_groups: dict[tuple[object, ...], list[InteractionNode]] = defaultdict( + list + ) + for interaction in dag.interactions: + involved = ( + class_by_current[interaction.result_id], + class_by_current[interaction.left_id], + class_by_current[interaction.right_id], + ) + keep_distinct = all(len(classes[class_id]) == 1 for class_id in involved) + interaction_groups[ + _interaction_projection_key( + interaction, + class_by_current, + keep_row_distinct=keep_distinct, + ) + ].append(interaction) + + root_groups: dict[tuple[object, ...], list[AmplitudeRoot]] = defaultdict(list) + for root in dag.amplitude_roots: + involved = ( + class_by_current[root.left_id], + class_by_current[root.right_id], + ) + keep_distinct = all(len(classes[class_id]) == 1 for class_id in involved) + root_groups[ + _root_projection_key( + dag, + root, + class_by_current, + keep_row_distinct=keep_distinct, + ) + ].append(root) + return interaction_groups, root_groups + + +def _invalid_projection_classes( + dag: GenericDAG, + classes: tuple[tuple[int, ...], ...], + class_by_current: tuple[int, ...], + interaction_groups: dict[tuple[object, ...], list[InteractionNode]], + root_groups: dict[tuple[object, ...], list[AmplitudeRoot]], +) -> set[int]: + """Return every non-singleton class implicated in a non-rectangle.""" + + invalid: set[int] = set() + produced_by_class: dict[int, set[int]] = defaultdict(set) + for rows in interaction_groups.values(): + first = rows[0] + result_class = class_by_current[first.result_id] + left_class = class_by_current[first.left_id] + right_class = class_by_current[first.right_id] + produced_by_class[result_class].update(row.result_id for row in rows) + if all( + len(classes[class_id]) == 1 + for class_id in (result_class, left_class, right_class) + ): + continue + actual = [(row.left_id, row.right_id) for row in rows] + expected = { + (left_id, right_id) + for left_id in classes[left_class] + for right_id in classes[right_class] + } + if len(actual) != len(set(actual)) or set(actual) != expected: + invalid.update( + class_id + for class_id in (result_class, left_class, right_class) + if len(classes[class_id]) > 1 + ) + + for class_id, members in enumerate(classes): + if len(members) == 1 or dag.currents[members[0]].is_source: + continue + if produced_by_class.get(class_id, set()) != set(members): + invalid.add(class_id) + + for roots in root_groups.values(): + first = roots[0] + left_class = class_by_current[first.left_id] + right_class = class_by_current[first.right_id] + if len(classes[left_class]) == len(classes[right_class]) == 1: + continue + actual = [(root.left_id, root.right_id) for root in roots] + expected = { + (left_id, right_id) + for left_id in classes[left_class] + for right_id in classes[right_class] + } + if len(actual) != len(set(actual)) or set(actual) != expected: + invalid.update( + class_id + for class_id in (left_class, right_class) + if len(classes[class_id]) > 1 + ) + return invalid + + +_DYNAMIC_COLOR_PROJECTION_ABI = ( + "pyamplicol-generic-dag-dynamic-color-projection-v2" +) +_RETAINED_COLOR_METADATA_POLICY = ( + "accuracy-line-groups-basis-key-identical; representative-sector-metadata-only" +) +_ROOT_SECTOR_POLICY = ( + "pre-projection-resolved-sector-is-explicit-and-never-cross-projected" +) + + +def dynamic_color_projection_not_applicable_report( + dag: GenericDAG, +) -> dict[str, object]: + """Return a count-only report when the LC-only projection cannot apply.""" + + if dag.process.color_accuracy == "lc": + raise ValueError("LC DAGs require the dynamic-color projection pass") + return { + "abi": _DYNAMIC_COLOR_PROJECTION_ABI, + "applied": False, + "color_accuracy": str(dag.process.color_accuracy), + "equality_check_status": "not-applicable-color-accuracy", + "before_current_count": len(dag.currents), + "after_current_count": len(dag.currents), + "before_interaction_count": len(dag.interactions), + "after_interaction_count": len(dag.interactions), + "before_evaluation_count": dag.interaction_evaluation_count, + "after_evaluation_count": dag.interaction_evaluation_count, + "before_amplitude_root_count": len(dag.amplitude_roots), + "after_amplitude_root_count": len(dag.amplitude_roots), + } + + +def _initial_dynamic_color_projection_certificate( + dag: GenericDAG, + *, + source_revision: str | None, + equality_check_status: str, +) -> DynamicColorProjectionCertificate: + identity = tuple(range(len(dag.currents))) + return DynamicColorProjectionCertificate( + abi=_DYNAMIC_COLOR_PROJECTION_ABI, + source_revision=source_revision, + source_semantics_sha256=_canonical_sha256(dag.to_json_dict()), + selector_domains_sha256=_canonical_sha256([]), + current_class_members_sha256=_canonical_sha256( + [[current.id] for current in dag.currents] + ), + old_to_new_current_ids=identity, + current_remap_sha256=_canonical_sha256(list(identity)), + interaction_groups_sha256=_canonical_sha256( + [[interaction.id] for interaction in dag.interactions] + ), + closure_groups_sha256=_canonical_sha256( + [[root.id] for root in dag.amplitude_roots] + ), + rectangle_cardinalities_sha256=_canonical_sha256([]), + row_identity_sha256=_canonical_sha256( + { + "interactions": [ + interaction.to_json_dict() for interaction in dag.interactions + ], + "roots": [root.to_json_dict() for root in dag.amplitude_roots], + } + ), + equality_check_status=equality_check_status, + retained_color_metadata=_RETAINED_COLOR_METADATA_POLICY, + root_sector_policy=_ROOT_SECTOR_POLICY, + before_current_count=len(dag.currents), + after_current_count=len(dag.currents), + before_interaction_count=len(dag.interactions), + after_interaction_count=len(dag.interactions), + before_evaluation_count=dag.interaction_evaluation_count, + after_evaluation_count=dag.interaction_evaluation_count, + before_amplitude_root_count=len(dag.amplitude_roots), + after_amplitude_root_count=len(dag.amplitude_roots), + projected_current_class_count=0, + rectangular_interaction_group_count=0, + rectangular_closure_group_count=0, + split_current_class_count=0, + ) + + +def _projection_proof_payloads( + dag: GenericDAG, + selector_domains: tuple[tuple[int, ...], ...], + classes: tuple[tuple[int, ...], ...], + class_by_current: tuple[int, ...], + interaction_groups: dict[tuple[object, ...], list[InteractionNode]], + root_groups: dict[tuple[object, ...], list[AmplitudeRoot]], +) -> dict[str, object]: + """Return the complete deterministic witness committed by the certificate.""" + + ordered_interaction_groups = sorted( + interaction_groups.values(), + key=lambda rows: min(row.id for row in rows), + ) + ordered_root_groups = sorted( + root_groups.values(), + key=lambda rows: min(row.id for row in rows), + ) + rectangle_cardinalities: list[dict[str, object]] = [] + row_identities: list[dict[str, object]] = [] + for rows in ordered_interaction_groups: + first = rows[0] + result_class = class_by_current[first.result_id] + left_class = class_by_current[first.left_id] + right_class = class_by_current[first.right_id] + actual_pairs = sorted((row.left_id, row.right_id) for row in rows) + rectangle_cardinalities.append( + { + "kind": "interaction", + "row_ids": sorted(row.id for row in rows), + "result_class_size": len(classes[result_class]), + "left_class_size": len(classes[left_class]), + "right_class_size": len(classes[right_class]), + "row_count": len(rows), + "unique_parent_pair_count": len(set(actual_pairs)), + "expected_parent_pair_count": ( + len(classes[left_class]) * len(classes[right_class]) + ), + } + ) + row_identities.append( + { + "kind": "interaction", + "result_class_id": result_class, + "left_class_id": left_class, + "right_class_id": right_class, + "vertex_kind": int(first.vertex_kind), + "vertex_particles": list(first.vertex_particles), + "coupling": list(first.coupling), + "color_weight": list(first.color_weight), + "lowering_backend": first.lowering_backend, + "full_tensor_network_ready": bool( + first.full_tensor_network_ready + ), + "row_ids": sorted(row.id for row in rows), + } + ) + for roots in ordered_root_groups: + first = roots[0] + left_class = class_by_current[first.left_id] + right_class = class_by_current[first.right_id] + actual_pairs = sorted((root.left_id, root.right_id) for root in roots) + resolved_sector = _resolved_root_color_sector_id(dag, first) + rectangle_cardinalities.append( + { + "kind": "closure", + "row_ids": sorted(root.id for root in roots), + "left_class_size": len(classes[left_class]), + "right_class_size": len(classes[right_class]), + "row_count": len(roots), + "unique_parent_pair_count": len(set(actual_pairs)), + "expected_parent_pair_count": ( + len(classes[left_class]) * len(classes[right_class]) + ), + "resolved_color_sector_id": resolved_sector, + } + ) + row_identities.append( + { + "kind": "closure", + "left_class_id": left_class, + "right_class_id": right_class, + "root_kind": first.kind, + "color_weight": list(first.color_weight), + "contraction_ir": first.contraction_ir.to_json_dict(), + "resolved_color_sector_id": resolved_sector, + "vertex_kind": first.vertex_kind, + "vertex_particles": ( + None + if first.vertex_particles is None + else list(first.vertex_particles) + ), + "coupling": list(first.coupling), + "helicity_weight": float(first.helicity_weight), + "row_ids": sorted(root.id for root in roots), + } + ) + return { + "selector_domains_sha256": _canonical_sha256( + [list(domain) for domain in selector_domains] + ), + "current_class_members_sha256": _canonical_sha256( + [list(members) for members in classes] + ), + "current_remap_sha256": _canonical_sha256(list(class_by_current)), + "interaction_groups_sha256": _canonical_sha256( + [sorted(row.id for row in rows) for rows in ordered_interaction_groups] + ), + "closure_groups_sha256": _canonical_sha256( + [sorted(root.id for root in roots) for roots in ordered_root_groups] + ), + "rectangle_cardinalities_sha256": _canonical_sha256( + rectangle_cardinalities + ), + "row_identity_sha256": _canonical_sha256(row_identities), + } + + +def project_rectangular_dynamic_color_classes( + dag: GenericDAG, + model: Model, + *, + source_revision: str | None = None, +) -> tuple[GenericDAG, DynamicColorProjectionCertificate]: + """Project exact dynamic-color rectangles into shared sum currents. + + This is the GenericDAG counterpart of topology-replay value projection. + It is intentionally conservative: + + * source currents remain singleton; + * every current field except dynamic LC sector remains in the class key; + * the downstream physical-selector domain is part of that key; + * concrete kernel identity, coupling, coefficient, and parent order remain + exact; + * both transition and amplitude-closure rows must form complete Cartesian + products without duplicates. + + A failed rectangle splits every implicated class and retries to a fixed + point. Therefore a malformed or incomplete color orbit can only reduce + optimization, never alter the generated amplitude. + """ + + before = _initial_dynamic_color_projection_certificate( + dag, + source_revision=source_revision, + equality_check_status="not-applicable", + ) + if ( + dag.process.color_accuracy != "lc" + or not dag.currents + or not dag.interactions + or not dag.amplitude_roots + ): + return dag, before + if dag.helicity_recurrence is not None or dag.helicity_materialization is not None: + raise ValueError( + "dynamic-color projection must precede helicity recurrence materialization" + ) + + selector_domains = _downstream_color_selector_domains(dag) + split_current_ids: set[int] = set() + split_class_count = 0 + while True: + classes, class_by_current = _projection_partition( + dag, + selector_domains, + split_current_ids, + ) + interaction_groups, root_groups = _projection_groups( + dag, + classes, + class_by_current, + ) + invalid_classes = _invalid_projection_classes( + dag, + classes, + class_by_current, + interaction_groups, + root_groups, + ) + newly_split = { + current_id + for class_id in invalid_classes + for current_id in classes[class_id] + if current_id not in split_current_ids + } + if not newly_split: + break + split_class_count += len(invalid_classes) + split_current_ids.update(newly_split) + + proof_payloads = _projection_proof_payloads( + dag, + selector_domains, + classes, + class_by_current, + interaction_groups, + root_groups, + ) + projected_class_count = sum(len(members) > 1 for members in classes) + if projected_class_count == 0: + return dag, replace( + before, + selector_domains_sha256=str( + proof_payloads["selector_domains_sha256"] + ), + current_class_members_sha256=str( + proof_payloads["current_class_members_sha256"] + ), + old_to_new_current_ids=class_by_current, + current_remap_sha256=str(proof_payloads["current_remap_sha256"]), + interaction_groups_sha256=str( + proof_payloads["interaction_groups_sha256"] + ), + closure_groups_sha256=str( + proof_payloads["closure_groups_sha256"] + ), + rectangle_cardinalities_sha256=str( + proof_payloads["rectangle_cardinalities_sha256"] + ), + row_identity_sha256=str(proof_payloads["row_identity_sha256"]), + equality_check_status="passed-no-projectable-classes", + split_current_class_count=split_class_count, + ) + + old_to_new = class_by_current + currents = tuple( + replace(dag.currents[members[0]], id=class_id) + for class_id, members in enumerate(classes) + ) + sources = tuple( + old_to_new[source_id] + for source_id in dag.sources + ) + if len(sources) != len(set(sources)): + raise ValueError("dynamic-color projection unexpectedly merged source currents") + + interactions: list[InteractionNode] = [] + rectangular_interaction_count = 0 + for rows in sorted( + interaction_groups.values(), + key=lambda rows: min(row.id for row in rows), + ): + first = rows[0] + involved = ( + old_to_new[first.result_id], + old_to_new[first.left_id], + old_to_new[first.right_id], + ) + if any(len(classes[class_id]) > 1 for class_id in involved): + rectangular_interaction_count += 1 + interactions.append( + replace( + first, + id=len(interactions), + left_id=old_to_new[first.left_id], + right_id=old_to_new[first.right_id], + result_id=old_to_new[first.result_id], + evaluation_group_id=None, + evaluation_factor=(1.0, 0.0), + ) + ) + + roots: list[AmplitudeRoot] = [] + rectangular_closure_count = 0 + for grouped_roots in sorted( + root_groups.values(), + key=lambda rows: min(row.id for row in rows), + ): + first = grouped_roots[0] + if ( + len(classes[old_to_new[first.left_id]]) > 1 + or len(classes[old_to_new[first.right_id]]) > 1 + ): + rectangular_closure_count += 1 + roots.append( + replace( + first, + id=len(roots), + left_id=old_to_new[first.left_id], + right_id=old_to_new[first.right_id], + color_sector_id=_resolved_root_color_sector_id(dag, first), + ) + ) + + projected = replace( + dag, + currents=currents, + sources=sources, + interactions=tuple(interactions), + amplitude_roots=tuple(roots), + ) + projected = assign_recursive_current_evaluation_reuse(projected, model) + certificate = DynamicColorProjectionCertificate( + abi=_DYNAMIC_COLOR_PROJECTION_ABI, + source_revision=source_revision, + source_semantics_sha256=before.source_semantics_sha256, + selector_domains_sha256=str( + proof_payloads["selector_domains_sha256"] + ), + current_class_members_sha256=str( + proof_payloads["current_class_members_sha256"] + ), + old_to_new_current_ids=old_to_new, + current_remap_sha256=str(proof_payloads["current_remap_sha256"]), + interaction_groups_sha256=str( + proof_payloads["interaction_groups_sha256"] + ), + closure_groups_sha256=str(proof_payloads["closure_groups_sha256"]), + rectangle_cardinalities_sha256=str( + proof_payloads["rectangle_cardinalities_sha256"] + ), + row_identity_sha256=str(proof_payloads["row_identity_sha256"]), + equality_check_status="passed-exact-cartesian-products-no-duplicates", + retained_color_metadata=_RETAINED_COLOR_METADATA_POLICY, + root_sector_policy=_ROOT_SECTOR_POLICY, + before_current_count=len(dag.currents), + after_current_count=len(projected.currents), + before_interaction_count=len(dag.interactions), + after_interaction_count=len(projected.interactions), + before_evaluation_count=dag.interaction_evaluation_count, + after_evaluation_count=projected.interaction_evaluation_count, + before_amplitude_root_count=len(dag.amplitude_roots), + after_amplitude_root_count=len(projected.amplitude_roots), + projected_current_class_count=projected_class_count, + rectangular_interaction_group_count=rectangular_interaction_count, + rectangular_closure_group_count=rectangular_closure_count, + split_current_class_count=split_class_count, + ) + return projected, certificate + + +def _derive_current_value_equivalences( + dag: GenericDAG, + model: Model, + *, + equivalence_by_kind: dict[int, VertexEvaluationEquivalence] | None = None, + discovery: _NumericalRelationDiscovery | None = None, + authenticated_relations: ( + Mapping[int, _CurrentValueEquivalence] | None + ) = None, +) -> tuple[_CurrentValueEquivalence, ...]: + """Derive current classes in increasing external-subset order.""" + + kernel_equivalences = {} if equivalence_by_kind is None else equivalence_by_kind + interactions_by_result: dict[int, list[InteractionNode]] = defaultdict(list) + for interaction in dag.interactions: + interactions_by_result[interaction.result_id].append(interaction) + + current_equivalences: list[_CurrentValueEquivalence | None] = [None] * len( + dag.currents + ) + source_representative_by_key: dict[tuple[object, ...], int] = {} + equivalence_by_expression: dict[ + tuple[_CurrentContract, _CurrentTermVector], _CurrentValueEquivalence + ] = {} + projective_representatives_by_expression: dict[ + tuple[_CurrentContract, _CurrentTermVector], + list[_ProjectiveExpressionRepresentative], + ] = {} + + ordered_currents = sorted( + dag.currents, + key=lambda current: (len(current.index.external_labels), current.id), + ) + for current in ordered_currents: + contract = _current_evaluation_contract(current) + if current.is_source: + if current.source_leg_label is None or current.source_helicity is None: + raise ValueError( + f"source current {current.id} lacks physical source metadata" + ) + source_key = ( + contract, + int(current.source_leg_label), + int(current.source_helicity), + ) + representative_id = source_representative_by_key.setdefault( + source_key, + current.id, + ) + current_equivalences[current.id] = _CurrentValueEquivalence( + representative_id=representative_id, + factor=(1.0, 0.0), + ) + continue + + term_vector = _current_term_vector( + dag, + current, + interactions_by_result[current.id], + model, + current_equivalences=current_equivalences, + equivalence_by_kind=kernel_equivalences, + ) + equivalence = _classify_current_term_vector( + current_id=current.id, + contract=contract, + term_vector=term_vector, + equivalence_by_expression=equivalence_by_expression, + projective_representatives_by_expression=( + projective_representatives_by_expression + ), + ) + if discovery is not None: + equivalence = discovery.consider( + current_id=current.id, + contract=contract, + term_vector=term_vector, + baseline=equivalence, + ) + if authenticated_relations is not None: + authenticated = authenticated_relations.get(current.id) + if authenticated is not None: + if authenticated.representative_id == current.id: + if authenticated.factor != (0.0, 0.0): + raise ValueError( + "self-represented authenticated relation must be zero" + ) + equivalence = authenticated + else: + representative = current_equivalences[ + authenticated.representative_id + ] + if representative is None: + raise ValueError( + "authenticated relation representative is not " + "topologically complete" + ) + factor = _exact_representable_complex_product( + authenticated.factor, + representative.factor, + ) + if factor is None: + raise ValueError( + "authenticated relation factor composition is not exact" + ) + equivalence = _CurrentValueEquivalence( + representative_id=representative.representative_id, + factor=factor, + ) + current_equivalences[current.id] = equivalence + + if any(item is None for item in current_equivalences): + raise ValueError( + "current-value equivalence derivation left an unclassified current" + ) + return tuple(item for item in current_equivalences if item is not None) + + +def verify_dag_relation_certificates( + dag: GenericDAG, + model: Model, + certificates: Iterable[ExactCurrentRelationCertificate], +) -> bool: + """Replay a certificate chain without consulting numerical probe results.""" + + ordered_certificates = tuple(certificates) + if ( + tuple(sorted(ordered_certificates, key=lambda item: item.current_id)) + != ordered_certificates + ): + return False + certificate_by_current = { + certificate.current_id: certificate for certificate in ordered_certificates + } + if len(certificate_by_current) != len(ordered_certificates): + return False + + try: + interactions_by_result: dict[int, list[InteractionNode]] = defaultdict(list) + for interaction in dag.interactions: + interactions_by_result[interaction.result_id].append(interaction) + current_equivalences: list[_CurrentValueEquivalence | None] = [None] * len( + dag.currents + ) + source_representative_by_key: dict[tuple[object, ...], int] = {} + equivalence_by_expression: dict[ + tuple[_CurrentContract, _CurrentTermVector], _CurrentValueEquivalence + ] = {} + projective_representatives_by_expression: dict[ + tuple[_CurrentContract, _CurrentTermVector], + list[_ProjectiveExpressionRepresentative], + ] = {} + kernel_equivalences: dict[int, VertexEvaluationEquivalence] = {} + term_vector_by_current: dict[int, _CurrentTermVector] = {} + contract_by_current: dict[int, _CurrentContract] = {} + consumed: set[int] = set() + + ordered_currents = sorted( + dag.currents, + key=lambda current: (len(current.index.external_labels), current.id), + ) + for current in ordered_currents: + contract = _current_evaluation_contract(current) + contract_by_current[current.id] = contract + if current.is_source: + if current.id in certificate_by_current: + return False + if current.source_leg_label is None or current.source_helicity is None: + return False + source_key = ( + contract, + int(current.source_leg_label), + int(current.source_helicity), + ) + representative_id = source_representative_by_key.setdefault( + source_key, + current.id, + ) + current_equivalences[current.id] = _CurrentValueEquivalence( + representative_id, + (1.0, 0.0), + ) + continue + + term_vector = _current_term_vector( + dag, + current, + interactions_by_result[current.id], + model, + current_equivalences=current_equivalences, + equivalence_by_kind=kernel_equivalences, + ) + term_vector_by_current[current.id] = term_vector + equivalence = _classify_current_term_vector( + current_id=current.id, + contract=contract, + term_vector=term_vector, + equivalence_by_expression=equivalence_by_expression, + projective_representatives_by_expression=( + projective_representatives_by_expression + ), + ) + certificate = certificate_by_current.get(current.id) + if certificate is not None: + if ( + certificate.algorithm != RELATION_DISCOVERY_CERTIFICATE_ALGORITHM + or certificate.representative_id >= current.id + or certificate.representative_id not in term_vector_by_current + or contract_by_current[certificate.representative_id] != contract + ): + return False + representative_vector = term_vector_by_current[ + certificate.representative_id + ] + factor = _certify_exact_term_vector_relation( + representative_vector, + term_vector, + ) + if factor is None or not _complex_weight_bits_equal( + factor, + certificate.factor, + ): + return False + if ( + _term_vector_sha256(term_vector) + != certificate.current_term_vector_sha256 + or _term_vector_sha256(representative_vector) + != certificate.representative_term_vector_sha256 + or _certificate_proof_sha256( + current_id=certificate.current_id, + representative_id=certificate.representative_id, + factor=certificate.factor, + current_term_vector_sha256=( + certificate.current_term_vector_sha256 + ), + representative_term_vector_sha256=( + certificate.representative_term_vector_sha256 + ), + ) + != certificate.proof_sha256 + ): + return False + equivalence = _CurrentValueEquivalence( + certificate.representative_id, + certificate.factor, + ) + consumed.add(current.id) + current_equivalences[current.id] = equivalence + return consumed == set(certificate_by_current) + except (IndexError, KeyError, OverflowError, TypeError, ValueError): + return False + + +def _numerical_projective_fingerprint( + term_vector: _CurrentTermVector, + *, + precision_digits: int, + probe_count: int, + seed: int, +) -> tuple[object, ...] | None: + """Return a scale-free high-precision candidate key. + + This fingerprint is deliberately insufficient as a proof. Its only use is + to reduce the number of pairs sent to exact coefficient-vector replay. + """ + + if not _term_vector_is_finite_nonzero(term_vector): + return None + with localcontext() as context: + context.prec = precision_digits + values: list[tuple[Decimal, Decimal]] = [] + for probe_index in range(probe_count): + total_real = Decimal(0) + total_imaginary = Decimal(0) + for evaluation_key, coefficient in term_vector: + probe_real, probe_imaginary = _deterministic_decimal_probe_value( + evaluation_key, + seed=seed, + probe_index=probe_index, + ) + coefficient_real = Decimal.from_float(coefficient[0]) + coefficient_imaginary = Decimal.from_float(coefficient[1]) + total_real += ( + coefficient_real * probe_real + - coefficient_imaginary * probe_imaginary + ) + total_imaginary += ( + coefficient_real * probe_imaginary + + coefficient_imaginary * probe_real + ) + values.append((total_real, total_imaginary)) + + pivot_index = next( + ( + index + for index, value in enumerate(values) + if value != (Decimal(0), Decimal(0)) + ), + None, + ) + if pivot_index is None: + return None + pivot = values[pivot_index] + normalized = tuple(_decimal_complex_ratio(value, pivot) for value in values) + if any(value is None for value in normalized): + return None + fingerprint_digits = max(24, precision_digits // 2) + return ( + "deterministic-projective-probe-v1", + pivot_index, + tuple( + ( + _decimal_fingerprint_component(value[0], fingerprint_digits), + _decimal_fingerprint_component(value[1], fingerprint_digits), + ) + for value in normalized + if value is not None + ), + ) + + +def _deterministic_decimal_probe_value( + evaluation_key: _EvaluationKey, + *, + seed: int, + probe_index: int, +) -> tuple[Decimal, Decimal]: + payload = json.dumps( + _canonical_proof_value(evaluation_key), + ensure_ascii=True, + separators=(",", ":"), + sort_keys=True, + ).encode("ascii") + digest = hashlib.sha256(f"{seed}:{probe_index}:".encode("ascii") + payload).digest() + + def component(chunk: bytes) -> Decimal: + raw = int.from_bytes(chunk, "big", signed=False) + value = raw % 2_000_003 - 1_000_001 + return Decimal(1 if value == 0 else value) + + return component(digest[:8]), component(digest[8:16]) + + +def _decimal_complex_ratio( + numerator: tuple[Decimal, Decimal], + denominator: tuple[Decimal, Decimal], +) -> tuple[Decimal, Decimal] | None: + denominator_norm = denominator[0] * denominator[0] + denominator[1] * denominator[1] + if denominator_norm == 0: + return None + return ( + (numerator[0] * denominator[0] + numerator[1] * denominator[1]) + / denominator_norm, + (numerator[1] * denominator[0] - numerator[0] * denominator[1]) + / denominator_norm, + ) + + +def _decimal_fingerprint_component(value: Decimal, digits: int) -> str: + if value == 0: + return "0" + return format(value, f".{digits}E") + + +def _certify_exact_term_vector_relation( + representative: _CurrentTermVector, + candidate: _CurrentTermVector, +) -> _ComplexWeight | None: + if ( + not _term_vector_is_finite_nonzero(representative) + or not _term_vector_is_finite_nonzero(candidate) + or len(representative) != len(candidate) + ): + return None + if any( + representative_key != candidate_key + for (representative_key, _), (candidate_key, _) in zip( + representative, + candidate, + strict=True, + ) + ): + return None + factor = _exact_representable_complex_ratio( + candidate[0][1], + representative[0][1], + ) + if factor is None or not _term_vector_scaled_exactly( + representative, + factor, + candidate, + ): + return None + return factor + + +def _build_exact_current_relation_certificate( + *, + current_id: int, + representative_id: int, + factor: _ComplexWeight, + current_term_vector: _CurrentTermVector, + representative_term_vector: _CurrentTermVector, +) -> ExactCurrentRelationCertificate: + current_digest = _term_vector_sha256(current_term_vector) + representative_digest = _term_vector_sha256(representative_term_vector) + return ExactCurrentRelationCertificate( + current_id=current_id, + representative_id=representative_id, + factor=factor, + current_term_vector_sha256=current_digest, + representative_term_vector_sha256=representative_digest, + proof_sha256=_certificate_proof_sha256( + current_id=current_id, + representative_id=representative_id, + factor=factor, + current_term_vector_sha256=current_digest, + representative_term_vector_sha256=representative_digest, + ), + ) + + +def _certificate_proof_sha256( + *, + current_id: int, + representative_id: int, + factor: _ComplexWeight, + current_term_vector_sha256: str, + representative_term_vector_sha256: str, +) -> str: + return _canonical_payload_sha256( + { + "algorithm": RELATION_DISCOVERY_CERTIFICATE_ALGORITHM, + "current_id": current_id, + "representative_id": representative_id, + "factor_binary64": [factor[0].hex(), factor[1].hex()], + "current_term_vector_sha256": current_term_vector_sha256, + "representative_term_vector_sha256": (representative_term_vector_sha256), + } + ) + + +def _term_vector_sha256(term_vector: _CurrentTermVector) -> str: + return _canonical_payload_sha256( + [ + { + "evaluation_key": _canonical_proof_value(evaluation_key), + "coefficient_binary64": [ + coefficient[0].hex(), + coefficient[1].hex(), + ], + } + for evaluation_key, coefficient in term_vector + ] + ) + + +def _canonical_payload_sha256(payload: object) -> str: + encoded = json.dumps( + payload, + ensure_ascii=True, + separators=(",", ":"), + sort_keys=True, + ).encode("ascii") + return hashlib.sha256(encoded).hexdigest() + + +def _canonical_proof_value(value: object) -> object: + if isinstance(value, tuple): + return [_canonical_proof_value(item) for item in value] + if isinstance(value, float): + return {"binary64": value.hex()} + if value is None or isinstance(value, bool | int | str): + return value + raise TypeError( + "relation certificate contains a noncanonical evaluation-key value " + f"{type(value).__name__}" + ) + + +def _interaction_reuse_signature(dag: GenericDAG) -> tuple[tuple[object, ...], ...]: + canonical_group_by_key: dict[tuple[str, int], int] = {} + signature: list[tuple[object, ...]] = [] + for interaction in dag.interactions: + raw_group = ( + ("group", interaction.evaluation_group_id) + if interaction.evaluation_group_id is not None + else ("interaction", interaction.id) + ) + canonical_group = canonical_group_by_key.setdefault( + raw_group, + len(canonical_group_by_key), + ) + signature.append( + ( + canonical_group, + interaction.evaluation_factor[0].hex(), + interaction.evaluation_factor[1].hex(), + ) + ) + return tuple(signature) + + +def _classify_current_term_vector( + *, + current_id: int, + contract: _CurrentContract, + term_vector: _CurrentTermVector, + equivalence_by_expression: dict[ + tuple[_CurrentContract, _CurrentTermVector], _CurrentValueEquivalence + ], + projective_representatives_by_expression: dict[ + tuple[_CurrentContract, _CurrentTermVector], + list[_ProjectiveExpressionRepresentative], + ], +) -> _CurrentValueEquivalence: + """Return the earliest exactly proven representative for one current. + + Exact equality and sign reuse remain the fast paths. More general + projective reuse is accepted only when a finite, nonzero binary64 factor + exists exactly and applying it reproduces every coefficient bit-for-bit. + """ + + identity = _CurrentValueEquivalence(current_id, (1.0, 0.0)) + expression_key = (contract, term_vector) + if not term_vector: + exact_zero = equivalence_by_expression.get(expression_key) + if exact_zero is not None: + return exact_zero + equivalence_by_expression[expression_key] = identity + return identity + if not _term_vector_is_finite_nonzero(term_vector): + return identity + + exact = equivalence_by_expression.get(expression_key) + if exact is not None: + return exact + + opposite = equivalence_by_expression.get( + (contract, _negate_term_vector(term_vector)) + ) + if opposite is not None: + opposite_factor = ( + _canonical_zero(-opposite.factor[0]), + _canonical_zero(-opposite.factor[1]), + ) + representative_vector = _term_vector_divided_by_exact_factor( + term_vector, + opposite_factor, + ) + if representative_vector is not None and _term_vector_scaled_exactly( + representative_vector, + opposite_factor, + term_vector, + ): + equivalence = _CurrentValueEquivalence( + opposite.representative_id, + opposite_factor, + ) + equivalence_by_expression[expression_key] = equivalence + return equivalence + + canonical = _canonicalize_projective_term_vector(term_vector) + if canonical is None: + equivalence_by_expression[expression_key] = identity + return identity + + projective_key = (contract, canonical.term_vector) + representatives = projective_representatives_by_expression.get(projective_key) + matches: list[_CurrentValueEquivalence] = [] + if representatives is not None: + for representative in representatives: + factor = _exact_representable_complex_ratio( + canonical.factor, + representative.normalization_factor, + ) + if factor is None or not _term_vector_scaled_exactly( + representative.term_vector, + factor, + term_vector, + ): + continue + matches.append( + _CurrentValueEquivalence( + representative.representative_id, + factor, + ) + ) + + if len(matches) == 1: + equivalence = matches[0] + equivalence_by_expression[expression_key] = equivalence + return equivalence + + # Zero matches mean that the normalized lookup key was only a rounded + # collision. Multiple matches make the concrete representative + # ambiguous. Both cases fail closed by retaining this current. + projective_representatives_by_expression.setdefault( + projective_key, + [], + ).append( + _ProjectiveExpressionRepresentative( + representative_id=current_id, + term_vector=term_vector, + normalization_factor=canonical.factor, + ) + ) + equivalence_by_expression[expression_key] = identity + return identity + + +def _canonicalize_projective_term_vector( + term_vector: _CurrentTermVector, +) -> _CanonicalProjectiveTermVector | None: + """Normalize a finite nonzero vector for projective-class lookup. + + The first canonical term is the deterministic pivot. The normalized + binary64 coefficients are only an index: every accepted equivalence is + separately proven with an exactly representable factor and bit-exact + coefficient reconstruction. + """ + + if not term_vector or not _term_vector_is_finite_nonzero(term_vector): + return None + keys = tuple(key for key, _coefficient in term_vector) + if len(set(keys)) != len(keys) or keys != tuple(sorted(keys)): + return None + + factor = _canonical_complex_weight(term_vector[0][1]) + normalized_terms: list[tuple[_EvaluationKey, _ComplexWeight]] = [] + for key, raw_coefficient in term_vector: + coefficient = _canonical_complex_weight(raw_coefficient) + normalized = _roundtrip_complex_ratio(coefficient, factor) + if normalized is None: + return None + normalized_terms.append((key, normalized)) + return _CanonicalProjectiveTermVector( + term_vector=tuple(normalized_terms), + factor=factor, + ) + + +def _roundtrip_complex_ratio( + numerator: _ComplexWeight, + denominator: _ComplexWeight, +) -> _ComplexWeight | None: + """Return a finite lookup ratio that reconstructs ``numerator`` exactly.""" + + if ( + not _complex_weight_is_finite(numerator) + or not _complex_weight_is_finite(denominator) + or denominator == (0.0, 0.0) + ): + return None + try: + quotient = complex(*numerator) / complex(*denominator) + except (OverflowError, ZeroDivisionError): + return None + result = _canonical_complex_weight((quotient.real, quotient.imag)) + if not _complex_weight_is_finite(result): + return None + reconstructed = _canonical_complex_weight(_complex_weight_mul(denominator, result)) + if not _complex_weight_bits_equal(reconstructed, numerator): + return None + return result + + +def _exact_representable_complex_ratio( + numerator: _ComplexWeight, + denominator: _ComplexWeight, +) -> _ComplexWeight | None: + """Return the exact binary64 complex quotient, or fail closed. + + ``Fraction`` is used only after two vectors collide on their inexpensive + normalized key. It proves that both quotient components are exactly + representable rather than merely rounded values that happen to be close. + """ + + if ( + not _complex_weight_is_finite(numerator) + or not _complex_weight_is_finite(denominator) + or denominator == (0.0, 0.0) + ): + return None + numerator_real = Fraction.from_float(numerator[0]) + numerator_imag = Fraction.from_float(numerator[1]) + denominator_real = Fraction.from_float(denominator[0]) + denominator_imag = Fraction.from_float(denominator[1]) + denominator_norm = ( + denominator_real * denominator_real + denominator_imag * denominator_imag + ) + if denominator_norm == 0: + return None + real = ( + numerator_real * denominator_real + numerator_imag * denominator_imag + ) / denominator_norm + imaginary = ( + numerator_imag * denominator_real - numerator_real * denominator_imag + ) / denominator_norm + real_f64 = _exact_fraction_as_f64(real) + imaginary_f64 = _exact_fraction_as_f64(imaginary) + if real_f64 is None or imaginary_f64 is None: + return None + factor = (real_f64, imaginary_f64) + reconstructed = _canonical_complex_weight(_complex_weight_mul(denominator, factor)) + if not _complex_weight_bits_equal( + reconstructed, + _canonical_complex_weight(numerator), + ): + return None + return factor + + +def _exact_representable_complex_product( + left: _ComplexWeight, + right: _ComplexWeight, +) -> _ComplexWeight | None: + """Return the exact binary64 complex product, or fail closed.""" + + if not _complex_weight_is_finite(left) or not _complex_weight_is_finite(right): + return None + left_real = Fraction.from_float(left[0]) + left_imaginary = Fraction.from_float(left[1]) + right_real = Fraction.from_float(right[0]) + right_imaginary = Fraction.from_float(right[1]) + real = left_real * right_real - left_imaginary * right_imaginary + imaginary = left_real * right_imaginary + left_imaginary * right_real + real_f64 = _exact_fraction_as_f64(real) + imaginary_f64 = _exact_fraction_as_f64(imaginary) + if real_f64 is None or imaginary_f64 is None: + return None + product = (real_f64, imaginary_f64) + reconstructed = _canonical_complex_weight(_complex_weight_mul(left, right)) + if not _complex_weight_bits_equal(reconstructed, product): + return None + return product + + +def _exact_fraction_as_f64(value: Fraction) -> float | None: + try: + result = float(value) + except OverflowError: + return None + if not isfinite(result) or Fraction.from_float(result) != value: + return None + return _canonical_zero(result) + + +def _term_vector_scaled_exactly( + representative: _CurrentTermVector, + factor: _ComplexWeight, + candidate: _CurrentTermVector, +) -> bool: + if ( + not _complex_weight_is_finite(factor) + or factor == (0.0, 0.0) + or len(representative) != len(candidate) + ): + return False + for (representative_key, representative_value), ( + candidate_key, + candidate_value, + ) in zip(representative, candidate, strict=True): + if representative_key != candidate_key: + return False + scaled = _canonical_complex_weight( + _complex_weight_mul(factor, representative_value) + ) + if not _complex_weight_bits_equal( + scaled, + _canonical_complex_weight(candidate_value), + ): + return False + return True + + +def _term_vector_divided_by_exact_factor( + candidate: _CurrentTermVector, + factor: _ComplexWeight, +) -> _CurrentTermVector | None: + """Recover a representative vector only for the sign fast path.""" + + if factor not in ((1.0, 0.0), (-1.0, 0.0)): + return None + return tuple( + ( + key, + value + if factor == (1.0, 0.0) + else (_canonical_zero(-value[0]), _canonical_zero(-value[1])), + ) + for key, value in candidate + ) + + +def _term_vector_is_finite_nonzero(term_vector: _CurrentTermVector) -> bool: + return bool(term_vector) and all( + coefficient != (0.0, 0.0) and _complex_weight_is_finite(coefficient) + for _key, coefficient in term_vector + ) + + +def _complex_weight_is_finite(value: _ComplexWeight) -> bool: + return isfinite(value[0]) and isfinite(value[1]) + + +def _canonical_complex_weight(value: _ComplexWeight) -> _ComplexWeight: + return (_canonical_zero(value[0]), _canonical_zero(value[1])) + + +def _complex_weight_bits_equal( + left: _ComplexWeight, + right: _ComplexWeight, +) -> bool: + return left[0].hex() == right[0].hex() and left[1].hex() == right[1].hex() + + +def _current_evaluation_contract(current: CurrentNode) -> _CurrentContract: + """Return fields that can affect source, kernel, or propagator values.""" + + index = current.index + return ( + int(index.particle_id), + int(index.external_mask), + index.external_labels, + index.ordered_external_labels, + int(index.helicity_ancestry), + int(index.chirality), + index.spin_state, + index.flavour_flow, + index.quantum_number_flow, + int(index.momentum_mask), + index.coupling_orders, + index.auxiliary_kind, + int(current.dimension), + bool(current.is_source), + ) + + +def _current_term_vector( + dag: GenericDAG, + current: CurrentNode, + interactions: list[InteractionNode], + model: Model, + *, + current_equivalences: list[_CurrentValueEquivalence | None], + equivalence_by_kind: dict[int, VertexEvaluationEquivalence], +) -> _CurrentTermVector: + coefficients_by_key: dict[_EvaluationKey, list[_ComplexWeight]] = defaultdict(list) + for interaction in interactions: + kernel_equivalence = _kernel_equivalence( + model, + interaction.vertex_kind, + equivalence_by_kind, + ) + left = current_equivalences[interaction.left_id] + right = current_equivalences[interaction.right_id] + if left is None or right is None: + raise ValueError( + "current-value equivalence requires parents from an earlier subset" + ) + canonical_inputs, evaluation_factor = _canonical_interaction_evaluation( + kernel_equivalence, + left_id=interaction.left_id, + right_id=interaction.right_id, + left=left, + right=right, + ) + term_key = ( + kernel_equivalence.class_id, + canonical_inputs, + int(current.index.particle_id), + int(current.index.chirality), + _runtime_coupling_identity( + model, + vertex_kind=interaction.vertex_kind, + vertex_particles=interaction.vertex_particles, + coupling=interaction.coupling, + ), + ) + coefficient = _complex_weight_mul( + interaction.color_weight, + evaluation_factor, + ) + coefficients_by_key[term_key].append(coefficient) + + terms: list[tuple[_EvaluationKey, _ComplexWeight]] = [] + for grouped_term_key in sorted(coefficients_by_key): + coefficients = coefficients_by_key[grouped_term_key] + coefficient = ( + _canonical_zero(fsum(value[0] for value in coefficients)), + _canonical_zero(fsum(value[1] for value in coefficients)), + ) + if coefficient != (0.0, 0.0): + terms.append((grouped_term_key, coefficient)) + return tuple(terms) + + +def _kernel_equivalence( + model: Model, + kind: int, + cache: dict[int, VertexEvaluationEquivalence], +) -> VertexEvaluationEquivalence: + cached = cache.get(kind) + if cached is not None: + return cached + equivalence = model.vertex_evaluation_equivalence(kind) + if not equivalence.verified: + model_type = f"{type(model).__module__}.{type(model).__qualname__}" + equivalence = VertexEvaluationEquivalence(class_id=f"{model_type}:{int(kind)}") + cache[kind] = equivalence + return equivalence + + +def _runtime_coupling_identity( + model: Model, + *, + vertex_kind: int, + vertex_particles: tuple[int, int, int], + coupling: tuple[float, float], +) -> tuple[tuple[float, float], tuple[tuple[int, str], ...]]: + """Return defaults plus stable mutable-parameter provenance for reuse.""" + + names = runtime_coupling_parameter_names( + vertex_kind, + vertex_particles, + coupling, + model=model, + ) + provenance = tuple((0, "") if name is None else (1, str(name)) for name in names) + return coupling, provenance + + +def _negate_term_vector(vector: _CurrentTermVector) -> _CurrentTermVector: + return tuple((key, (-value[0], -value[1])) for key, value in vector) + + +def _complex_weight_mul( + left: _ComplexWeight, + right: _ComplexWeight, +) -> _ComplexWeight: + return ( + left[0] * right[0] - left[1] * right[1], + left[0] * right[1] + left[1] * right[0], + ) + + +def _canonical_zero(value: float) -> float: + return 0.0 if value == 0.0 else value + + +__all__ = [ + "GENERIC_DAG_NUMERICAL_SOURCE_SEMANTICS_ABI", + "NUMERICAL_CURRENT_CAPTURE_ABI", + "NUMERICAL_CURRENT_CAPTURE_OUTPUT_PARTITION_ABI", + "NUMERICAL_CURRENT_RELATION_CERTIFICATE_ALGORITHM", + "NUMERICAL_CURRENT_RELATION_SET_ABI", + "NUMERICAL_CURRENT_RELATION_WARNING", + "NUMERICAL_CURRENT_RELATION_WARNING_CODE", + "RELATION_DISCOVERY_CERTIFICATE_ALGORITHM", + "RELATION_DISCOVERY_SCHEMA_VERSION", + "DynamicColorProjectionCertificate", + "ExactCurrentRelationCertificate", + "NumericalCurrentAppliedMapping", + "NumericalCurrentObservationDiscoveryReport", + "NumericalCurrentObservationDiscoveryResult", + "NumericalCurrentRelationApplicationReport", + "NumericalCurrentRelationApplicationResult", + "NumericalCurrentRelationCertificate", + "RecursiveEvaluationReuseTracker", + "RelationDiscoveryReport", + "RelationDiscoveryResult", + "apply_numerical_current_relation_certificates", + "assign_recursive_current_evaluation_reuse", + "certify_numerical_current_observations", + "discover_generic_dag_numerical_current_relations", + "discover_recursive_evaluation_relations", + "dynamic_color_projection_not_applicable_report", + "generic_dag_numerical_capture_output_partition_sha256", + "generic_dag_numerical_model_parameter_schema_sha256", + "generic_dag_numerical_runtime_schema_sha256", + "generic_dag_numerical_source_dag_sha256", + "generic_dag_numerical_source_semantics_sha256", + "project_rectangular_dynamic_color_classes", + "verify_dag_relation_certificates", + "verify_numerical_current_relation_certificate", +] diff --git a/pyamplicol/src/pyamplicol/generation/dag_ordering.py b/pyamplicol/src/pyamplicol/generation/dag_ordering.py new file mode 100644 index 0000000000..9c4509d973 --- /dev/null +++ b/pyamplicol/src/pyamplicol/generation/dag_ordering.py @@ -0,0 +1,575 @@ +# SPDX-License-Identifier: 0BSD +"""Color-order and closure-mask algorithms for generic DAG generation.""" + +from __future__ import annotations + +from collections.abc import Iterable +from typing import TYPE_CHECKING + +from ..color.plan import GenericColorPlan, LCColorSector +from ..models._physics_ir import ContractionIR +from ..models.base import Model +from ..processes.ir import CanonicalProcessIR +from .dag_types import CurrentIndex + +if TYPE_CHECKING: + from .dag_color import ColorEngine + + +def _known_color_representation(model: Model, particle_id: int) -> int | None: + """Return explicit model colour metadata without guessing from identity.""" + + try: + return int(model.color_rep(int(particle_id))) + except (KeyError, NotImplementedError, TypeError, ValueError): + return None + + +def _known_fermion_statistics(model: Model, particle_id: int) -> bool | None: + """Return explicit model statistics, or ``None`` when they are unavailable.""" + + try: + return bool(model.is_fermion(int(particle_id))) + except (KeyError, NotImplementedError, TypeError, ValueError): + return None + + +def _direct_contraction_ir( + model: Model, + left: CurrentIndex, + right: CurrentIndex, +) -> ContractionIR | None: + return model.direct_contraction_ir( + left.particle_id, + right.particle_id, + left_chirality=left.chirality, + right_chirality=right.chirality, + ) + + +def _sector_group_indices_for_label( + sector: LCColorSector, + label: int, +) -> tuple[int, ...]: + return tuple( + index + for index, group in enumerate(sector.line_label_groups) + if label in set(group) + ) + + +def _line_positions(labels: Iterable[int], order: tuple[int, ...]) -> tuple[int, ...]: + positions = {label: index for index, label in enumerate(order)} + return tuple(positions[label] for label in labels if label in positions) + + +def _labels_projected_to_word( + labels: Iterable[int], + word: tuple[int, ...], +) -> tuple[int, ...]: + word_labels = set(word) + return tuple(label for label in labels if label in word_labels) + + +def _positions_contiguous(positions: tuple[int, ...]) -> bool: + if not positions: + return True + return positions == tuple(range(positions[0], positions[-1] + 1)) + + +def _word_contains_ordered_segment( + word: tuple[int, ...], + segment: tuple[int, ...], +) -> bool: + if not segment or len(segment) > len(word): + return False + width = len(segment) + return any( + tuple(word[start : start + width]) == segment + for start in range(len(word) - width + 1) + ) + + +def _ordered_combination_matches_word( + left_labels: Iterable[int], + right_labels: Iterable[int], + word: tuple[int, ...], +) -> bool: + return _ordered_combination_segment(left_labels, right_labels, word) is not None + + +def _closure_combination_matches_word( + left_labels: Iterable[int], + right_labels: Iterable[int], + word: tuple[int, ...], +) -> bool: + """Return whether two endpoint label sets close one colour word. + + Intermediate current construction must preserve the actual line order because + it fixes where colour-singlet insertions may attach. Final closure is + different: the sink current can be oriented opposite to the sector's primary + colour word, while still representing the same contiguous endpoint segment. + Closure therefore checks contiguity of the projected coloured label sets, not + their internal iteration order. + """ + + left_positions = tuple(sorted(_line_positions(left_labels, word))) + right_positions = tuple(sorted(_line_positions(right_labels, word))) + if not left_positions or not right_positions: + return False + if set(left_positions) & set(right_positions): + return False + union = tuple(sorted((*left_positions, *right_positions))) + return ( + _positions_contiguous(left_positions) + and _positions_contiguous(right_positions) + and _positions_contiguous(union) + ) + + +def _shared_single_trace_closure_matches_word( + left_labels: Iterable[int], + right_labels: Iterable[int], + word: tuple[int, ...], +) -> bool: + left_projected = _labels_projected_to_word(left_labels, word) + right_projected = _labels_projected_to_word(right_labels, word) + return (*left_projected, *right_projected) == word + + +def _shared_lc_closure_matches_word( + left_labels: Iterable[int], + right_labels: Iterable[int], + word: tuple[int, ...], +) -> bool: + left_projected = _labels_projected_to_word(left_labels, word) + right_projected = _labels_projected_to_word(right_labels, word) + if not left_projected or not right_projected: + return False + return (*left_projected, *right_projected) == word + + +def _ordered_combination_segment( + left_labels: Iterable[int], + right_labels: Iterable[int], + word: tuple[int, ...], +) -> tuple[int, ...] | None: + left_positions = _line_positions(left_labels, word) + right_positions = _line_positions(right_labels, word) + if not left_positions and not right_positions: + return () + if not left_positions or not right_positions: + positions = left_positions or right_positions + if not _positions_contiguous(positions): + return None + return tuple(word[positions[0] : positions[-1] + 1]) + union = tuple(sorted((*left_positions, *right_positions))) + if ( + _positions_contiguous(left_positions) + and _positions_contiguous(right_positions) + and _positions_contiguous(union) + and max(left_positions) < min(right_positions) + ): + return tuple(word[union[0] : union[-1] + 1]) + return None + + +def _lc_all_adjoint_symmetry_order_variants( + left: tuple[int, ...], + right: tuple[int, ...], + *, + left_all_adjoint: bool, + right_all_adjoint: bool, + result_reflection_proven: bool, + fold_result_reflections: bool = True, +) -> tuple[tuple[tuple[int, ...], tuple[float, float]], ...]: + """Return signed pure-adjoint LC symmetry variants.""" + + n1 = len(left) + n2 = len(right) + switch1 = 2 if left_all_adjoint and n1 >= 2 else 1 + switch2 = 2 if right_all_adjoint and n2 >= 2 else 1 + switch3 = 2 if result_reflection_proven else 1 + variants: list[tuple[tuple[int, ...], tuple[float, float]]] = [] + for i in range(1, switch1 + 1): + for j in range(1, switch2 + 1): + for k in range(1, switch3 + 1): + invert = 0 + if (i == 2 and k == 1) or (j == 2 and k == 2): + invert |= 1 + if (i == 2 and k == 2) or (j == 2 and k == 1): + invert |= 2 + if k == 1: + first = tuple(reversed(left)) if invert & 1 else left + second = tuple(reversed(right)) if invert & 2 else right + else: + first = tuple(reversed(right)) if invert & 1 else right + second = tuple(reversed(left)) if invert & 2 else left + proposed = (*first, *second) + if ( + result_reflection_proven + and fold_result_reflections + and not _lc_all_adjoint_symmetry_order_kept(proposed) + ): + continue + negative = ( + (k == 2) ^ (j == 2 and n2 % 2 == 0) ^ (i == 2 and n1 % 2 == 0) + ) + variants.append((proposed, (-1.0, 0.0) if negative else (1.0, 0.0))) + return tuple(variants) + + +def _lc_all_adjoint_symmetry_order_kept(labels: tuple[int, ...]) -> bool: + if not labels: + return False + min_label = min(labels) + max_label = max(labels) + return labels.index(min_label) < labels.index(max_label) + + +def _complex_weight_mul( + left: tuple[float, float], + right: tuple[float, float], +) -> tuple[float, float]: + return ( + left[0] * right[0] - left[1] * right[1], + left[0] * right[1] + left[1] * right[0], + ) + + +def _canonical_lc_ordered_labels( + labels: Iterable[int], + sector: LCColorSector, +) -> tuple[int, ...]: + label_tuple = tuple(labels) + label_set = set(label_tuple) + for word in _sector_intermediate_order_words(sector): + positions = _line_positions(label_tuple, word) + if not positions: + continue + if not _positions_contiguous(positions): + continue + word_labels = set(word) + extras = tuple(sorted(label for label in label_set if label not in word_labels)) + segment = tuple(word[positions[0] : positions[-1] + 1]) + if not _line_local_singlet_extras_allowed(segment, extras, sector): + continue + return (*segment, *extras) + return tuple(sorted(label_set)) + + +def _sector_intermediate_order_words( + sector: LCColorSector, + *, + include_compatible_traversals: bool = True, + process: CanonicalProcessIR | None = None, +) -> tuple[tuple[int, ...], ...]: + """Return LC words used for intermediate current construction. + + A physical multi-line sector admits several block traversals. Cyclically + equivalent traversals describe the same closure, so normalize every word + to the physical sector's authenticated closure sink and deduplicate it. + These are private compatibility words, not public legacy amplitude-order + representatives. This retains the distinct cyclic classes needed by + three or more open lines without creating duplicate physical sectors. + + Non-LC recursion keeps one sector-private traversal. For open lines this + is the authenticated canonical closure traversal, so intermediate currents + and the final sink use the same orientation without changing the public + physical colour word. + """ + + physical_words = sector.color_words + if not include_compatible_traversals: + if sector.kind == "open-lines" and process is not None: + canonical_word = sector.canonical_closure_traversal_word(process) + return (canonical_word,) if canonical_word else physical_words + return physical_words or sector.admissible_traversal_words + if sector.kind != "open-lines" or not physical_words or not physical_words[0]: + return physical_words or sector.admissible_traversal_words + + sink_label = ( + int(physical_words[0][-1]) + if process is None + else sector.canonical_closure_sink_label(process) + ) + if sink_label is None: + return physical_words + words: list[tuple[int, ...]] = [] + seen: set[tuple[int, ...]] = set() + for raw_word in sector.admissible_traversal_words: + normalized = tuple(int(label) for label in raw_word) + if sink_label in normalized: + sink_index = normalized.index(sink_label) + word = ( + *normalized[sink_index + 1 :], + *normalized[: sink_index + 1], + ) + else: + word = normalized + if word in seen: + continue + seen.add(word) + words.append(word) + return tuple(words) + + +def _lc_word_with_sink_last( + word: Iterable[int], + sink_label: int | None, +) -> tuple[int, ...]: + normalized = tuple(int(label) for label in word) + if sink_label is None or sink_label not in normalized: + return normalized + sink_index = normalized.index(sink_label) + rotated = ( + *normalized[sink_index + 1 :], + *normalized[: sink_index + 1], + ) + current_word = rotated[:-1] + if current_word and not _lc_all_adjoint_symmetry_order_kept(current_word): + current_word = tuple(reversed(current_word)) + return (*current_word, rotated[-1]) + + +def _line_local_singlet_extras_allowed( + colored_segment: Iterable[int], + extras: Iterable[int], + sector: LCColorSector, +) -> bool: + extra_set = set(extras) + if not extra_set: + return True + if sector.kind != "open-lines": + return extra_set.issubset(set(sector.singlet_labels)) + return extra_set.issubset(set(sector.singlet_labels)) + + +def _labels_mask(labels: Iterable[int]) -> int: + mask = 0 + for label in labels: + mask |= 1 << (label - 1) + return mask + + +def _mask_labels(mask: int) -> tuple[int, ...]: + return tuple(index + 1 for index in range(mask.bit_length()) if mask & (1 << index)) + + +def _canonical_sink_mask(process_ir: CanonicalProcessIR, model: Model) -> int: + fermion_labels = [ + leg.label + for leg in process_ir.legs + if leg.outgoing_pdg is not None + and _known_fermion_statistics(model, int(leg.outgoing_pdg)) is True + ] + if fermion_labels: + return 1 << (min(fermion_labels) - 1) + labels = [leg.label for leg in process_ir.legs if leg.outgoing_pdg is not None] + if not labels: + return 0 + return 1 << (min(labels) - 1) + + +def _closure_candidate_splits( + process_ir: CanonicalProcessIR, + model: Model, + color_engine: ColorEngine, + *, + reference_color_order: tuple[int, ...] | None = None, +) -> tuple[tuple[int, int], ...]: + full_mask = _labels_mask(leg.label for leg in process_ir.legs) + splits: list[tuple[int, int]] = [] + split_seen: set[tuple[int, int]] = set() + sink_labels: list[int] = [] + if color_engine.shared_lc_fixed_sink_label is not None: + sink_labels.append(color_engine.shared_lc_fixed_sink_label) + sectors = color_engine.color_plan.sectors + if not sink_labels and ( + color_engine.color_plan.color_accuracy == "lc" + or color_engine.shared_single_trace + or any(sector.kind == "open-lines" for sector in sectors) + ): + if reference_color_order is not None: + selected_sectors = tuple( + sector + for sector in sectors + if reference_color_order in sector.admissible_traversal_words + ) + if selected_sectors: + sectors = selected_sectors + for sector in sectors: + # A public reference word selects a physical sector, but its + # terminal label is not a private recursion sink. Final closure + # always uses that sector's authenticated canonical traversal. + sink_label = sector.canonical_closure_sink_label(process_ir) + if sink_label is not None: + sink_labels.append(sink_label) + if not sink_labels and reference_color_order: + leg_by_label = {leg.label: leg for leg in process_ir.legs} + colored_reference_labels: list[int] = [] + ordered_reference_labels: list[int] = [] + for raw_label in reference_color_order: + label = int(raw_label) + if not (full_mask & (1 << (label - 1))): + continue + ordered_reference_labels.append(label) + leg = leg_by_label.get(label) + if leg is None or leg.outgoing_pdg is None: + continue + representation = _known_color_representation( + model, + int(leg.outgoing_pdg), + ) + if representation is not None and representation != 1: + colored_reference_labels.append(label) + if colored_reference_labels: + sink_labels.append(colored_reference_labels[-1]) + elif ordered_reference_labels: + sink_labels.append(ordered_reference_labels[-1]) + if not sink_labels: + fallback_sink_mask = _canonical_sink_mask(process_ir, model) + fallback_sink_labels = _mask_labels(fallback_sink_mask) + if fallback_sink_labels: + sink_labels.append(fallback_sink_labels[0]) + seen: set[int] = set() + for label in sink_labels: + if label in seen: + continue + seen.add(label) + sink_mask = 1 << (label - 1) + if not (sink_mask & full_mask): + continue + split = (full_mask ^ sink_mask, sink_mask) + if split in split_seen: + continue + split_seen.add(split) + splits.append(split) + return tuple(splits) + + +def _closure_side_reachable_masks( + full_mask: int, + candidate_splits: Iterable[tuple[int, int]], +) -> frozenset[int]: + """Return current masks that can feed one configured amplitude closure. + + The generic forward sweep otherwise builds every locally valid current for + every proper subset, then removes dead currents after closure. AmpliCol's + library generation is faster because it knows which endpoint side each + current can ultimately feed. This helper applies the same idea without + naming a process family: once the generic closure splitter has selected the + possible amplitude endpoint masks, any useful intermediate current must be + a submask of one of those endpoint masks. + """ + + allowed: set[int] = set() + for left_mask, right_mask in candidate_splits: + for side_mask in (left_mask, right_mask): + submask = side_mask & full_mask + while submask: + allowed.add(submask) + submask = (submask - 1) & side_mask + return frozenset(allowed) + + +def _lc_color_order_reachable_masks( + process_ir: CanonicalProcessIR, + color_plan: GenericColorPlan, + model: Model, +) -> frozenset[int] | None: + """Return subset masks compatible with at least one LC colour word. + + This is process-generic pruning. It only uses model colour + representations and the LC words produced by the colour planner. Any + useful coloured current in a colour-ordered recursion must cover a + contiguous segment of one compatibility word. Colour singlets are left as + attachments to those segments because their allowed positions are governed + by ordinary model vertices and the existing singlet-order rule. + """ + + if not color_plan.sectors: + return None + + full_mask = _labels_mask(leg.label for leg in process_ir.legs) + colored_labels: set[int] = set() + singlet_labels: set[int] = set() + for leg in process_ir.legs: + if leg.outgoing_pdg is None: + return None + representation = _known_color_representation(model, int(leg.outgoing_pdg)) + if representation is None: + return None + if representation != 1: + colored_labels.add(leg.label) + else: + singlet_labels.add(leg.label) + + allowed: set[int] = set() + + if not colored_labels: + return frozenset(_nonzero_submasks(full_mask)) + + for sector in color_plan.sectors: + sector_colored_labels = { + label for group in sector.coloured_label_groups for label in group + } + if sector_colored_labels != colored_labels: + return None + if set(sector.singlet_labels) != singlet_labels: + return None + if sector.kind == "open-lines": + for singlet_submask in _submasks_for_labels(sector.singlet_labels): + if singlet_submask: + allowed.add(singlet_submask) + for raw_word in sector.admissible_traversal_words: + word = tuple(label for label in raw_word if label in colored_labels) + for start in range(len(word)): + segment_mask = 0 + for stop in range(start, len(word)): + segment_mask |= 1 << (word[stop] - 1) + allowed.add(segment_mask) + segment_labels = tuple(word[start : stop + 1]) + line_singlets = _line_local_singlet_labels_for_segment( + sector, + segment_labels, + ) + for singlet_submask in _submasks_for_labels(line_singlets): + allowed.add(segment_mask | singlet_submask) + + for label in colored_labels: + allowed.add(1 << (label - 1)) + for label in singlet_labels: + allowed.add(1 << (label - 1)) + return frozenset(mask for mask in allowed if mask & full_mask) + + +def _line_local_singlet_labels_for_segment( + sector: LCColorSector, + colored_segment: Iterable[int], +) -> tuple[int, ...]: + if sector.kind != "open-lines": + return sector.singlet_labels + return sector.singlet_labels + + +def _submasks_for_labels(labels: Iterable[int]) -> tuple[int, ...]: + label_tuple = tuple(labels) + masks: list[int] = [] + count = len(label_tuple) + for bits in range(1 << count): + mask = 0 + for index, label in enumerate(label_tuple): + if bits & (1 << index): + mask |= 1 << (label - 1) + masks.append(mask) + return tuple(masks) + + +def _nonzero_submasks(mask: int) -> tuple[int, ...]: + submasks: list[int] = [] + submask = mask + while submask: + submasks.append(submask) + submask = (submask - 1) & mask + return tuple(submasks) diff --git a/pyamplicol/src/pyamplicol/generation/dag_reachability.py b/pyamplicol/src/pyamplicol/generation/dag_reachability.py new file mode 100644 index 0000000000..f8da362901 --- /dev/null +++ b/pyamplicol/src/pyamplicol/generation/dag_reachability.py @@ -0,0 +1,590 @@ +# SPDX-License-Identifier: 0BSD +"""Particle-species and coupling-order reachability for generic DAGs.""" + +from __future__ import annotations + +from collections import deque +from collections.abc import Iterable, Mapping +from typing import TYPE_CHECKING + +from ..models.base import CouplingOrders, Model, Vertex +from ..processes.ir import CanonicalProcessIR +from .dag_ordering import _labels_mask +from .dag_types import ColorState + +if TYPE_CHECKING: + from .dag_color import ColorEngine + + +def _normalize_coupling_order_limits( + limits: Mapping[str, int] | None, +) -> dict[str, int]: + if limits is None: + return {} + return { + str(name).upper(): int(value) + for name, value in limits.items() + if int(value) >= 0 + } + + +def _coupling_orders_within_limits( + orders: tuple[tuple[str, int], ...], + limits: Mapping[str, int], +) -> bool: + if not limits: + return True + order_map = {str(name).upper(): int(value) for name, value in orders} + return all(order_map.get(name, 0) <= int(limit) for name, limit in limits.items()) + + +def _combine_coupling_order_tuples( + *orders: CouplingOrders, +) -> CouplingOrders: + totals: dict[str, int] = {} + for order_tuple in orders: + for name, value in order_tuple: + totals[str(name).upper()] = totals.get(str(name).upper(), 0) + int(value) + return tuple(sorted((name, value) for name, value in totals.items() if value)) + + +def _lc_line_groups_within_limit( + color_state: ColorState, + limit: int | None, +) -> bool: + if limit is None or color_state.accuracy != "lc": + return True + return len(color_state.line_groups) <= limit + + +def _right_particles_by_left( + model: Model, + *, + color_accuracy: str, +) -> dict[int, tuple[int, ...]]: + rights: dict[int, set[int]] = {} + for vertex in model.iter_vertices(color_accuracy=color_accuracy): + rights.setdefault(vertex.particles[0], set()).add(vertex.particles[1]) + return { + left: tuple(sorted(right_particles)) for left, right_particles in rights.items() + } + + +def _closure_right_particles_by_left( + model: Model, + *, + color_accuracy: str, +) -> dict[int, tuple[int, ...]]: + """Return sparse vertex and direct-contraction partners by left species.""" + + rights = { + left: set(right_particles) + for left, right_particles in _right_particles_by_left( + model, + color_accuracy=color_accuracy, + ).items() + } + for particle in model.particles.values(): + rights.setdefault(particle.pdg, set()).add(particle.anti_pdg) + rights.setdefault(particle.anti_pdg, set()).add(particle.pdg) + return { + left: tuple(sorted(right_particles)) for left, right_particles in rights.items() + } + + +UsefulStateMap = dict[int, dict[int, frozenset[CouplingOrders]]] +_ReachabilityState = tuple[int, int, CouplingOrders] + + +def _useful_states_by_mask( + process_ir: CanonicalProcessIR, + model: Model, + color_engine: ColorEngine, + closure_candidate_splits: Iterable[tuple[int, int]], + closure_reachable_masks: frozenset[int] | None, + color_order_reachable_masks: frozenset[int] | None, + *, + max_coupling_orders: Mapping[str, int], + ignored_particle_ids: frozenset[int], + ignored_vertex_kinds: frozenset[int], +) -> UsefulStateMap: + """Return current states that can feed at least one amplitude closure. + + The full current table is keyed by helicity, chirality, flavour flow and + colour state. This prepass intentionally ignores those expensive labels, + but it keeps particle id and model coupling-order totals. The order + tracking lets user-supplied generic constraints such as ``QED=1`` prune + impossible branches before the expensive helicity/current sweep, without + recognizing any process family. It is allowed to overestimate, but it must + never depend on a process-family name. + """ + + full_mask = _labels_mask(leg.label for leg in process_ir.legs) + possible: dict[int, dict[int, set[CouplingOrders]]] = {} + for leg in process_ir.legs: + if leg.outgoing_pdg is None: + continue + particle_id = int(leg.outgoing_pdg) + if particle_id in ignored_particle_ids: + continue + possible.setdefault(1 << (leg.label - 1), {}).setdefault( + particle_id, + set(), + ).add(()) + + reverse_transitions: dict[ + _ReachabilityState, + set[tuple[_ReachabilityState, _ReachabilityState]], + ] = {} + vertices_by_input: dict[tuple[int, int], tuple[Vertex, ...]] = {} + right_particles_by_left = _right_particles_by_left( + model, + color_accuracy=process_ir.color_accuracy, + ) + for mask in _masks_by_size(full_mask): + if mask & (mask - 1) == 0 or mask == full_mask: + continue + if not _mask_allowed_by_reachability( + mask, + closure_reachable_masks, + color_order_reachable_masks, + ): + continue + for left_mask, right_mask in _ordered_splits(mask): + if not ( + _mask_allowed_by_reachability( + left_mask, + closure_reachable_masks, + color_order_reachable_masks, + ) + and _mask_allowed_by_reachability( + right_mask, + closure_reachable_masks, + color_order_reachable_masks, + ) + ): + continue + left_species = possible.get(left_mask) + right_species = possible.get(right_mask) + if not left_species or not right_species: + continue + for left_particle, left_orders_set in tuple(left_species.items()): + for right_particle in right_particles_by_left.get(left_particle, ()): + right_orders_set = right_species.get(right_particle) + if not right_orders_set: + continue + vertex_key = (left_particle, right_particle) + vertices = vertices_by_input.get(vertex_key) + if vertices is None: + vertices = model.vertices_accepting( + left_particle, + right_particle, + color_accuracy=process_ir.color_accuracy, + ) + vertices_by_input[vertex_key] = vertices + for vertex in vertices: + if ( + vertex.kind in ignored_vertex_kinds + or vertex.particles[2] in ignored_particle_ids + or not color_engine.vertex_allowed(vertex) + or model.skip_duplicate_vertex_orientation(vertex) + ): + continue + result_particle = vertex.particles[2] + for left_orders in tuple(left_orders_set): + for right_orders in tuple(right_orders_set): + coupling_orders = _combine_coupling_order_tuples( + left_orders, + right_orders, + model.vertex_coupling_orders(vertex), + ) + if not _coupling_orders_within_limits( + coupling_orders, + max_coupling_orders, + ): + continue + possible.setdefault(mask, {}).setdefault( + result_particle, + set(), + ).add(coupling_orders) + reverse_transitions.setdefault( + (mask, result_particle, coupling_orders), + set(), + ).add( + ( + (left_mask, left_particle, left_orders), + (right_mask, right_particle, right_orders), + ) + ) + + closure_right_particles_by_left = _closure_right_particles_by_left( + model, + color_accuracy=process_ir.color_accuracy, + ) + useful: dict[int, dict[int, set[CouplingOrders]]] = {} + for left_mask, right_mask in closure_candidate_splits: + left_species = possible.get(left_mask) + right_species = possible.get(right_mask) + if not left_species or not right_species: + continue + for left_particle, left_orders_set in left_species.items(): + for right_particle in closure_right_particles_by_left.get( + left_particle, + (), + ): + right_orders_set = right_species.get(right_particle) + if not right_orders_set: + continue + if model.direct_contraction_possible(left_particle, right_particle): + for left_orders in left_orders_set: + for right_orders in right_orders_set: + total_orders = _combine_coupling_order_tuples( + left_orders, + right_orders, + (), + ) + if not _coupling_orders_within_limits( + total_orders, + max_coupling_orders, + ): + continue + useful.setdefault(left_mask, {}).setdefault( + left_particle, + set(), + ).add(left_orders) + useful.setdefault(right_mask, {}).setdefault( + right_particle, + set(), + ).add(right_orders) + for vertex in model.vertices_accepting( + left_particle, + right_particle, + color_accuracy=process_ir.color_accuracy, + ): + if ( + vertex.kind in ignored_vertex_kinds + or vertex.particles[2] in ignored_particle_ids + or not color_engine.vertex_allowed(vertex) + or model.skip_duplicate_vertex_orientation(vertex) + ): + continue + if model.closure_contraction_ir(vertex.particles[2]) is None: + continue + vertex_orders = model.vertex_coupling_orders(vertex) + for left_orders in left_orders_set: + for right_orders in right_orders_set: + total_orders = _combine_coupling_order_tuples( + left_orders, + right_orders, + vertex_orders, + ) + if not _coupling_orders_within_limits( + total_orders, + max_coupling_orders, + ): + continue + useful.setdefault(left_mask, {}).setdefault( + left_particle, + set(), + ).add(left_orders) + useful.setdefault(right_mask, {}).setdefault( + right_particle, + set(), + ).add(right_orders) + + pending = deque( + (mask, particle, orders) + for mask, species_orders in useful.items() + for particle, order_set in species_orders.items() + for orders in order_set + ) + while pending: + result_state = pending.popleft() + for left_state, right_state in reverse_transitions.get(result_state, ()): + for parent_mask, parent_particle, parent_orders in ( + left_state, + right_state, + ): + parent_useful = useful.setdefault(parent_mask, {}).setdefault( + parent_particle, + set(), + ) + if parent_orders in parent_useful: + continue + parent_useful.add(parent_orders) + pending.append((parent_mask, parent_particle, parent_orders)) + + return { + mask: { + particle: frozenset(orders) + for particle, orders in species_orders.items() + if orders + } + for mask, species_orders in useful.items() + if species_orders + } + + +def _closure_total_coupling_orders( + process_ir: CanonicalProcessIR, + model: Model, + color_engine: ColorEngine, + closure_candidate_splits: Iterable[tuple[int, int]], + closure_reachable_masks: frozenset[int] | None, + color_order_reachable_masks: frozenset[int] | None, + *, + max_coupling_orders: Mapping[str, int], + ignored_particle_ids: frozenset[int], + ignored_vertex_kinds: frozenset[int], +) -> frozenset[CouplingOrders]: + """Return model-reachable total coupling orders for amplitude closures.""" + + full_mask = _labels_mask(leg.label for leg in process_ir.legs) + possible: dict[int, dict[int, set[CouplingOrders]]] = {} + for leg in process_ir.legs: + if leg.outgoing_pdg is None: + continue + particle_id = int(leg.outgoing_pdg) + if particle_id in ignored_particle_ids: + continue + possible.setdefault(1 << (leg.label - 1), {}).setdefault( + particle_id, + set(), + ).add(()) + + vertices_by_input: dict[tuple[int, int], tuple[Vertex, ...]] = {} + right_particles_by_left = _right_particles_by_left( + model, + color_accuracy=process_ir.color_accuracy, + ) + for mask in _masks_by_size(full_mask): + if mask & (mask - 1) == 0 or mask == full_mask: + continue + if not _mask_allowed_by_reachability( + mask, + closure_reachable_masks, + color_order_reachable_masks, + ): + continue + for left_mask, right_mask in _ordered_splits(mask): + if not ( + _mask_allowed_by_reachability( + left_mask, + closure_reachable_masks, + color_order_reachable_masks, + ) + and _mask_allowed_by_reachability( + right_mask, + closure_reachable_masks, + color_order_reachable_masks, + ) + ): + continue + left_species = possible.get(left_mask) + right_species = possible.get(right_mask) + if not left_species or not right_species: + continue + for left_particle, left_orders_set in tuple(left_species.items()): + for right_particle in right_particles_by_left.get(left_particle, ()): + right_orders_set = right_species.get(right_particle) + if not right_orders_set: + continue + vertex_key = (left_particle, right_particle) + vertices = vertices_by_input.get(vertex_key) + if vertices is None: + vertices = model.vertices_accepting( + left_particle, + right_particle, + color_accuracy=process_ir.color_accuracy, + ) + vertices_by_input[vertex_key] = vertices + for vertex in vertices: + if ( + vertex.kind in ignored_vertex_kinds + or vertex.particles[2] in ignored_particle_ids + or not color_engine.vertex_allowed(vertex) + or model.skip_duplicate_vertex_orientation(vertex) + ): + continue + result_particle = vertex.particles[2] + order_bucket = possible.setdefault(mask, {}).setdefault( + result_particle, + set(), + ) + vertex_orders = model.vertex_coupling_orders(vertex) + for left_orders in tuple(left_orders_set): + for right_orders in tuple(right_orders_set): + coupling_orders = _combine_coupling_order_tuples( + left_orders, + right_orders, + vertex_orders, + ) + if not _coupling_orders_within_limits( + coupling_orders, + max_coupling_orders, + ): + continue + order_bucket.add(coupling_orders) + possible[mask][result_particle] = set( + _pareto_minimal_coupling_orders(order_bucket) + ) + + closure_right_particles_by_left = _closure_right_particles_by_left( + model, + color_accuracy=process_ir.color_accuracy, + ) + totals: set[CouplingOrders] = set() + for left_mask, right_mask in closure_candidate_splits: + left_species = possible.get(left_mask) + right_species = possible.get(right_mask) + if not left_species or not right_species: + continue + for left_particle, left_orders_set in left_species.items(): + for right_particle in closure_right_particles_by_left.get( + left_particle, + (), + ): + right_orders_set = right_species.get(right_particle) + if not right_orders_set: + continue + if model.direct_contraction_possible(left_particle, right_particle): + for left_orders in left_orders_set: + for right_orders in right_orders_set: + total_orders = _combine_coupling_order_tuples( + left_orders, + right_orders, + (), + ) + if _coupling_orders_within_limits( + total_orders, + max_coupling_orders, + ): + totals.add(total_orders) + for vertex in model.vertices_accepting( + left_particle, + right_particle, + color_accuracy=process_ir.color_accuracy, + ): + if ( + vertex.kind in ignored_vertex_kinds + or vertex.particles[2] in ignored_particle_ids + or not color_engine.vertex_allowed(vertex) + or model.skip_duplicate_vertex_orientation(vertex) + ): + continue + if model.closure_contraction_ir(vertex.particles[2]) is None: + continue + vertex_orders = model.vertex_coupling_orders(vertex) + for left_orders in left_orders_set: + for right_orders in right_orders_set: + total_orders = _combine_coupling_order_tuples( + left_orders, + right_orders, + vertex_orders, + ) + if _coupling_orders_within_limits( + total_orders, + max_coupling_orders, + ): + totals.add(total_orders) + return frozenset(_pareto_minimal_coupling_orders(totals)) + + +def _coupling_order_degree( + orders: CouplingOrders, + *, + hierarchies: Mapping[str, int] | None = None, +) -> int: + priorities = hierarchies or {} + return sum( + int(value) * max(1, int(priorities.get(str(name).upper(), 1))) + for name, value in orders + ) + + +def _coupling_order_envelope(orders: Iterable[CouplingOrders]) -> dict[str, int]: + envelope: dict[str, int] = {} + for order_tuple in orders: + for name, value in order_tuple: + normalized = str(name).upper() + envelope[normalized] = max(envelope.get(normalized, 0), int(value)) + return envelope + + +def _pareto_minimal_coupling_orders( + orders: Iterable[CouplingOrders], +) -> tuple[CouplingOrders, ...]: + normalized = tuple(sorted(set(orders))) + minimal: list[CouplingOrders] = [] + for candidate in normalized: + if any( + _coupling_orders_dominate(other, candidate) + for other in normalized + if other != candidate + ): + continue + minimal.append(candidate) + return tuple(minimal) + + +def _coupling_orders_dominate(left: CouplingOrders, right: CouplingOrders) -> bool: + left_map = dict(left) + right_map = dict(right) + keys = set(left_map) | set(right_map) + return all(left_map.get(key, 0) <= right_map.get(key, 0) for key in keys) and any( + left_map.get(key, 0) < right_map.get(key, 0) for key in keys + ) + + +def _mask_allowed_by_reachability( + mask: int, + closure_reachable_masks: frozenset[int] | None, + color_order_reachable_masks: frozenset[int] | None, +) -> bool: + if closure_reachable_masks is not None and mask not in closure_reachable_masks: + return False + return not ( + color_order_reachable_masks is not None + and mask not in color_order_reachable_masks + ) + + +def _state_allowed_by_reachability( + useful_states_by_mask: UsefulStateMap, + mask: int, + particle_id: int, + coupling_orders: CouplingOrders, +) -> bool: + return coupling_orders in useful_states_by_mask.get(mask, {}).get( + particle_id, + (), + ) + + +def _nonzero_submasks(mask: int) -> tuple[int, ...]: + submasks: list[int] = [] + submask = mask + while submask: + submasks.append(submask) + submask = (submask - 1) & mask + return tuple(submasks) + + +def _masks_by_size(full_mask: int) -> tuple[int, ...]: + masks = [] + submask = full_mask + while submask: + masks.append(submask) + submask = (submask - 1) & full_mask + return tuple(sorted(masks, key=lambda value: (value.bit_count(), value))) + + +def _ordered_splits(mask: int) -> tuple[tuple[int, int], ...]: + splits: list[tuple[int, int]] = [] + left = (mask - 1) & mask + while left: + right = mask ^ left + if right: + splits.append((left, right)) + left = (left - 1) & mask + return tuple(splits) diff --git a/pyamplicol/src/pyamplicol/generation/dag_table.py b/pyamplicol/src/pyamplicol/generation/dag_table.py new file mode 100644 index 0000000000..e8495de849 --- /dev/null +++ b/pyamplicol/src/pyamplicol/generation/dag_table.py @@ -0,0 +1,100 @@ +# SPDX-License-Identifier: 0BSD +"""Deduplicated current storage used during DAG compilation.""" + +from __future__ import annotations + +from collections.abc import Iterable, Sequence + +from ..models.base import Model +from .dag_types import CurrentIndex, CurrentNode + + +class _CurrentTable: + def __init__(self, model: Model) -> None: + self.model = model + self.currents: list[CurrentNode] = [] + self._ids: dict[CurrentIndex, int] = {} + self._ids_by_mask: dict[int, list[int]] = {} + self._ids_by_mask_particle: dict[tuple[int, int], list[int]] = {} + self._ids_by_mask_particle_sector: dict[tuple[int, int, int], list[int]] = {} + + def add_or_get( + self, + index: CurrentIndex, + *, + is_source: bool, + source_leg_label: int | None = None, + source_helicity: int | None = None, + ) -> CurrentNode: + current_id = self._ids.get(index) + if current_id is not None: + return self.currents[current_id] + current_id = len(self.currents) + node = CurrentNode( + id=current_id, + index=index, + dimension=self.model.current_dimension( + index.particle_id, + index.chirality, + ), + is_source=is_source, + source_leg_label=source_leg_label, + source_helicity=source_helicity, + ) + self._ids[index] = current_id + self.currents.append(node) + self._ids_by_mask.setdefault(index.external_mask, []).append(current_id) + self._ids_by_mask_particle.setdefault( + (index.external_mask, index.particle_id), + [], + ).append(current_id) + self._ids_by_mask_particle_sector.setdefault( + ( + index.external_mask, + index.particle_id, + index.color_state.sector_id, + ), + [], + ).append(current_id) + return node + + def current(self, current_id: int) -> CurrentNode: + return self.currents[current_id] + + def has_mask(self, mask: int) -> bool: + return mask in self._ids_by_mask + + def ids_by_mask(self, mask: int) -> Sequence[int]: + return self._ids_by_mask.get(mask, ()) + + def ids_by_mask_and_particles( + self, + mask: int, + particle_ids: Iterable[int], + *, + color_sector_id: int | None = None, + ) -> Sequence[int]: + try: + particle_count = len(particle_ids) # type: ignore[arg-type] + except TypeError: + particle_count = -1 + if particle_count == 1: + particle_id = next(iter(particle_ids)) + if color_sector_id is None: + return self._ids_by_mask_particle.get((mask, particle_id), ()) + return self._ids_by_mask_particle_sector.get( + (mask, particle_id, color_sector_id), + (), + ) + ids: list[int] = [] + for particle_id in particle_ids: + if color_sector_id is None: + ids.extend(self._ids_by_mask_particle.get((mask, particle_id), ())) + else: + ids.extend( + self._ids_by_mask_particle_sector.get( + (mask, particle_id, color_sector_id), + (), + ) + ) + return ids diff --git a/pyamplicol/src/pyamplicol/generation/dag_types.py b/pyamplicol/src/pyamplicol/generation/dag_types.py new file mode 100644 index 0000000000..6e534a0405 --- /dev/null +++ b/pyamplicol/src/pyamplicol/generation/dag_types.py @@ -0,0 +1,624 @@ +# SPDX-License-Identifier: 0BSD +"""Frozen graph records shared by generation phases.""" + +from __future__ import annotations + +from collections.abc import Iterable +from dataclasses import dataclass, field +from typing import TYPE_CHECKING, Literal + +from ..color.plan import ( + ColorTopologyReplayCertificate, + GenericColorPlan, + LCColorSector, + LCColorTopologyReplayPlan, +) +from ..models._physics_ir import ContractionIR +from ..models.base import QuantumNumberFlow +from ..processes.ir import CanonicalProcessIR + +if TYPE_CHECKING: + from .helicity_materialization import HelicityMaterialization + from .helicity_replay import HelicityRecurrencePlan + +ColorAccuracy = Literal["lc", "nlc", "full"] + +_LC_FIERZ_SINGLET_BASIS = "lc-fierz-singlet" +_LC_COLOR_IDENTITY_CLOSURE_PREFIX = "lc-color-identity-closure:" + + +def _lc_color_identity_closure_key(labels: Iterable[int]) -> str: + normalized = tuple(sorted(set(int(label) for label in labels))) + return _LC_COLOR_IDENTITY_CLOSURE_PREFIX + ",".join( + str(label) for label in normalized + ) + + +def _lc_color_identity_closures( + basis_keys: Iterable[str], +) -> tuple[frozenset[int], ...]: + closures: set[frozenset[int]] = set() + for key in basis_keys: + if not key.startswith(_LC_COLOR_IDENTITY_CLOSURE_PREFIX): + continue + payload = key.removeprefix(_LC_COLOR_IDENTITY_CLOSURE_PREFIX) + try: + labels = frozenset(int(label) for label in payload.split(",") if label) + except ValueError: + return (frozenset(),) + if not labels: + return (frozenset(),) + closures.add(labels) + return tuple(sorted(closures, key=lambda labels: tuple(sorted(labels)))) + + +def _lc_color_identity_closures_compatible( + closures: Iterable[frozenset[int]], + sector: LCColorSector, +) -> bool: + groups = tuple(frozenset(group) for group in sector.coloured_label_groups) + return all( + bool(closure) and any(closure.issubset(group) for group in groups) + for closure in closures + ) + + +@dataclass(frozen=True) +class ColorState: + """Colour identity carried by one current. + + The LC implementation stores the warmup sector and the open-line/trace + groups touched by the current. NLC/full colour will replace ``basis_key`` + with Idenso basis components without changing the current-index contract. + """ + + accuracy: str + sector_id: int = 0 + line_groups: tuple[int, ...] = () + basis_key: tuple[str, ...] = () + _key: tuple[object, ...] = field(init=False, repr=False, compare=False) + _hash: int = field(init=False, repr=False, compare=False) + + def __post_init__(self) -> None: + key = ( + self.accuracy, + self.sector_id, + self.line_groups, + self.basis_key, + ) + object.__setattr__(self, "_key", key) + object.__setattr__(self, "_hash", hash(key)) + + def __eq__(self, other: object) -> bool: + if not isinstance(other, ColorState): + return NotImplemented + return self._key == other._key + + def __hash__(self) -> int: + return self._hash + + def to_json_dict(self) -> dict[str, object]: + return { + "accuracy": self.accuracy, + "sector_id": self.sector_id, + "line_groups": list(self.line_groups), + "basis_key": list(self.basis_key), + } + + +@dataclass(frozen=True) +class ColorFlow: + state: ColorState + weight: tuple[float, float] = (1.0, 0.0) + + def to_json_dict(self) -> dict[str, object]: + return { + "state": self.state.to_json_dict(), + "weight": list(self.weight), + } + + +@dataclass(frozen=True) +class CurrentIndex: + """Complete model-driven current identity. + + Equality of this dataclass is the only current deduplication rule. It + deliberately carries physics state, not a process-family label. + """ + + particle_id: int + external_mask: int + external_labels: tuple[int, ...] + helicity_ancestry: int + chirality: int + spin_state: int | tuple[int, ...] + flavour_flow: tuple[int, ...] + quantum_number_flow: QuantumNumberFlow + color_state: ColorState + momentum_mask: int + coupling_orders: tuple[tuple[str, int], ...] = () + auxiliary_kind: str | None = None + ordered_external_labels: tuple[int, ...] = () + _key: tuple[object, ...] = field(init=False, repr=False, compare=False) + _hash: int = field(init=False, repr=False, compare=False) + + def __post_init__(self) -> None: + labels = tuple(sorted(self.external_labels)) + if labels != self.external_labels: + object.__setattr__(self, "external_labels", labels) + if not self.ordered_external_labels: + object.__setattr__(self, "ordered_external_labels", labels) + elif tuple(sorted(self.ordered_external_labels)) != labels: + raise ValueError( + "ordered_external_labels must contain the same labels as " + "external_labels" + ) + expected_mask = _labels_mask(self.external_labels) + if self.external_mask != expected_mask: + raise ValueError( + "external_mask does not match external_labels: " + f"{self.external_mask} != {expected_mask}" + ) + if self.momentum_mask == 0: + raise ValueError("momentum_mask must be nonzero for a current") + if self.coupling_orders: + object.__setattr__( + self, + "coupling_orders", + tuple( + sorted( + (str(name).upper(), int(value)) + for name, value in self.coupling_orders + if int(value) != 0 + ) + ), + ) + key = ( + self.particle_id, + self.external_mask, + self.external_labels, + self.helicity_ancestry, + self.chirality, + self.spin_state, + self.flavour_flow, + self.quantum_number_flow, + self.color_state, + self.momentum_mask, + self.coupling_orders, + self.auxiliary_kind, + self.ordered_external_labels, + ) + object.__setattr__(self, "_key", key) + object.__setattr__(self, "_hash", hash(key)) + + @classmethod + def _from_trusted_values( + cls, + *, + particle_id: int, + external_mask: int, + external_labels: tuple[int, ...], + helicity_ancestry: int, + chirality: int, + spin_state: int | tuple[int, ...], + flavour_flow: tuple[int, ...], + quantum_number_flow: QuantumNumberFlow, + color_state: ColorState, + momentum_mask: int, + coupling_orders: tuple[tuple[str, int], ...] = (), + auxiliary_kind: str | None = None, + ordered_external_labels: tuple[int, ...] = (), + ) -> CurrentIndex: + """Build an already canonical compiler-owned index. + + Public construction and artifact decoding retain the normal validation + path. The eager compiler uses this only after its mask, ordering, and + coupling-order planners have produced canonical values. + """ + + row = object.__new__(cls) + object.__setattr__(row, "particle_id", particle_id) + object.__setattr__(row, "external_mask", external_mask) + object.__setattr__(row, "external_labels", external_labels) + object.__setattr__(row, "helicity_ancestry", helicity_ancestry) + object.__setattr__(row, "chirality", chirality) + object.__setattr__(row, "spin_state", spin_state) + object.__setattr__(row, "flavour_flow", flavour_flow) + object.__setattr__(row, "quantum_number_flow", quantum_number_flow) + object.__setattr__(row, "color_state", color_state) + object.__setattr__(row, "momentum_mask", momentum_mask) + object.__setattr__(row, "coupling_orders", coupling_orders) + object.__setattr__(row, "auxiliary_kind", auxiliary_kind) + object.__setattr__( + row, + "ordered_external_labels", + ordered_external_labels or external_labels, + ) + key = ( + row.particle_id, + row.external_mask, + row.external_labels, + row.helicity_ancestry, + row.chirality, + row.spin_state, + row.flavour_flow, + row.quantum_number_flow, + row.color_state, + row.momentum_mask, + row.coupling_orders, + row.auxiliary_kind, + row.ordered_external_labels, + ) + object.__setattr__(row, "_key", key) + object.__setattr__(row, "_hash", hash(key)) + return row + + def __eq__(self, other: object) -> bool: + if not isinstance(other, CurrentIndex): + return NotImplemented + return self._key == other._key + + def __hash__(self) -> int: + return self._hash + + def overlaps(self, other: CurrentIndex) -> bool: + return bool(self.external_mask & other.external_mask) + + def to_json_dict(self) -> dict[str, object]: + spin_state: object + if isinstance(self.spin_state, tuple): + spin_state = list(self.spin_state) + else: + spin_state = self.spin_state + return { + "particle_id": self.particle_id, + "external_mask": self.external_mask, + "external_labels": list(self.external_labels), + "ordered_external_labels": list(self.ordered_external_labels), + "helicity_ancestry": self.helicity_ancestry, + "chirality": self.chirality, + "spin_state": spin_state, + "flavour_flow": list(self.flavour_flow), + "quantum_number_flow": [ + [name, expression] for name, expression in self.quantum_number_flow + ], + "color_state": self.color_state.to_json_dict(), + "momentum_mask": self.momentum_mask, + "coupling_orders": [[name, value] for name, value in self.coupling_orders], + "auxiliary_kind": self.auxiliary_kind, + } + + +@dataclass(frozen=True) +class CurrentNode: + id: int + index: CurrentIndex + dimension: int + is_source: bool + source_leg_label: int | None = None + source_helicity: int | None = None + + def to_json_dict(self) -> dict[str, object]: + return { + "id": self.id, + "index": self.index.to_json_dict(), + "dimension": self.dimension, + "is_source": self.is_source, + "source_leg_label": self.source_leg_label, + "source_helicity": self.source_helicity, + } + + +@dataclass(frozen=True) +class InteractionNode: + id: int + vertex_kind: int + vertex_particles: tuple[int, int, int] + left_id: int + right_id: int + result_id: int + coupling: tuple[float, float] + color_weight: tuple[float, float] + lowering_backend: str + full_tensor_network_ready: bool + evaluation_group_id: int | None = None + evaluation_factor: tuple[float, float] = (1.0, 0.0) + + def to_json_dict(self) -> dict[str, object]: + return { + "id": self.id, + "vertex_kind": self.vertex_kind, + "vertex_particles": list(self.vertex_particles), + "left_id": self.left_id, + "right_id": self.right_id, + "result_id": self.result_id, + "coupling": list(self.coupling), + "color_weight": list(self.color_weight), + "lowering_backend": self.lowering_backend, + "full_tensor_network_ready": self.full_tensor_network_ready, + "evaluation_group_id": self.evaluation_group_id, + "evaluation_factor": list(self.evaluation_factor), + } + + +@dataclass(frozen=True) +class AmplitudeRoot: + id: int + kind: str + left_id: int + right_id: int + color_weight: tuple[float, float] + contraction_ir: ContractionIR + color_sector_id: int | None = None + vertex_kind: int | None = None + vertex_particles: tuple[int, int, int] | None = None + coupling: tuple[float, float] = (1.0, 0.0) + helicity_weight: float = 1.0 + + def __post_init__(self) -> None: + if not isinstance(self.contraction_ir, ContractionIR): + raise TypeError("amplitude root requires a frozen ContractionIR") + + @property + def contraction(self) -> str: + """Display/wire projection retained for schema compatibility.""" + + return self.contraction_ir.name + + def to_json_dict(self) -> dict[str, object]: + return { + "id": self.id, + "kind": self.kind, + "left_id": self.left_id, + "right_id": self.right_id, + "color_weight": list(self.color_weight), + "color_sector_id": self.color_sector_id, + "vertex_kind": self.vertex_kind, + "vertex_particles": ( + list(self.vertex_particles) + if self.vertex_particles is not None + else None + ), + "coupling": list(self.coupling), + "contraction": self.contraction, + "contraction_ir": self.contraction_ir.to_json_dict(), + "helicity_weight": self.helicity_weight, + } + + +@dataclass(frozen=True) +class GenericDAG: + process: CanonicalProcessIR + color_plan: GenericColorPlan + currents: tuple[CurrentNode, ...] + sources: tuple[int, ...] + interactions: tuple[InteractionNode, ...] + amplitude_roots: tuple[AmplitudeRoot, ...] + truncated: bool = False + helicity_coverage: str = "complete" + color_coverage: str = "complete" + selected_source_helicities: tuple[tuple[int, int], ...] = () + selected_color_sector_ids: tuple[int, ...] = () + lc_topology_replay: LCColorTopologyReplayPlan | None = None + color_topology_replay: ColorTopologyReplayCertificate | None = None + helicity_recurrence: HelicityRecurrencePlan | None = None + helicity_materialization: HelicityMaterialization | None = None + + def __post_init__(self) -> None: + if self.helicity_coverage not in {"complete", "selected"}: + raise ValueError("DAG helicity coverage must be complete or selected") + if self.color_coverage not in {"complete", "selected"}: + raise ValueError("DAG color coverage must be complete or selected") + selected_color_sector_ids = tuple( + sorted({int(sector_id) for sector_id in self.selected_color_sector_ids}) + ) + object.__setattr__( + self, + "selected_color_sector_ids", + selected_color_sector_ids, + ) + known_sector_ids = {int(sector.id) for sector in self.color_plan.sectors} + if not set(selected_color_sector_ids) <= known_sector_ids: + raise ValueError( + "selected color-sector provenance references an absent sector" + ) + if ( + self.lc_topology_replay is not None + and self.color_topology_replay is not None + ): + raise ValueError("DAG cannot carry both LC and generic topology replay") + if self.lc_topology_replay is not None: + if self.process.color_accuracy != "lc": + raise ValueError("LC topology replay requires LC color accuracy") + if self.color_coverage != "complete": + raise ValueError("LC topology replay requires complete color coverage") + if selected_color_sector_ids: + raise ValueError( + "LC topology replay cannot carry generation-specialized color " + "sectors" + ) + if set(self.lc_topology_replay.physical_sector_ids) != { + int(sector.id) for sector in self.color_plan.sectors + }: + raise ValueError( + "LC topology replay physical sectors do not match the color plan" + ) + if self.color_topology_replay is not None: + if self.process.color_accuracy not in {"nlc", "full"}: + raise ValueError( + "generic topology replay requires NLC or full color accuracy" + ) + if self.color_topology_replay.color_accuracy != self.process.color_accuracy: + raise ValueError( + "generic topology replay color accuracy does not match the DAG" + ) + if self.color_coverage != "complete": + raise ValueError( + "generic topology replay requires complete color coverage" + ) + if selected_color_sector_ids: + raise ValueError( + "generic topology replay cannot carry generation-specialized " + "color sectors" + ) + if set(self.color_topology_replay.physical_sector_ids) != { + int(sector.id) for sector in self.color_plan.sectors + }: + raise ValueError( + "generic topology replay physical sectors do not match the " + "color plan" + ) + if self.helicity_recurrence is not None and ( + self.helicity_coverage != "complete" + or bool(self.selected_source_helicities) + ): + raise ValueError( + "helicity recurrence metadata requires complete helicity coverage" + ) + if self.helicity_materialization is not None: + if self.helicity_recurrence is None: + raise ValueError("helicity materialization requires a recurrence proof") + if self.helicity_materialization.dag is not self: + # The materialization object owns the same immutable payload but + # cannot hold the final self-reference after dataclass creation. + materialized = self.helicity_materialization.dag + if ( + materialized.currents != self.currents + or materialized.interactions != self.interactions + or materialized.amplitude_roots != self.amplitude_roots + ): + raise ValueError( + "helicity materialization does not describe this DAG" + ) + + @property + def has_amplitudes(self) -> bool: + return bool(self.amplitude_roots) + + @property + def required_vertex_kinds(self) -> tuple[int, ...]: + return tuple( + sorted( + {interaction.vertex_kind for interaction in self.interactions} + | { + root.vertex_kind + for root in self.amplitude_roots + if root.vertex_kind is not None + } + ) + ) + + @property + def interaction_evaluation_count(self) -> int: + return len( + { + ( + "group", + interaction.evaluation_group_id, + ) + if interaction.evaluation_group_id is not None + else ("interaction", interaction.id) + for interaction in self.interactions + if interaction.evaluation_factor != (0.0, 0.0) + } + ) + + @property + def interaction_fanout_histogram(self) -> tuple[tuple[int, int], ...]: + group_sizes: dict[tuple[str, int], int] = {} + for interaction in self.interactions: + if interaction.evaluation_factor == (0.0, 0.0): + continue + group = ( + ("group", interaction.evaluation_group_id) + if interaction.evaluation_group_id is not None + else ("interaction", interaction.id) + ) + group_sizes[group] = group_sizes.get(group, 0) + 1 + histogram: dict[int, int] = {} + for size in group_sizes.values(): + histogram[size] = histogram.get(size, 0) + 1 + return tuple(sorted(histogram.items())) + + def currents_by_external_labels( + self, + labels: Iterable[int], + ) -> tuple[CurrentNode, ...]: + wanted = tuple(sorted(labels)) + return tuple( + current + for current in self.currents + if current.index.external_labels == wanted + ) + + def to_json_dict(self) -> dict[str, object]: + return { + "process": self.process.to_json_dict(), + "color_plan": self.color_plan.to_json_dict(), + "current_count": len(self.currents), + "source_count": len(self.sources), + "helicity_coverage": self.helicity_coverage, + "color_coverage": self.color_coverage, + "selected_source_helicities": { + str(label): helicity + for label, helicity in self.selected_source_helicities + }, + **( + {} + if not self.selected_color_sector_ids + else {"selected_color_sector_ids": list(self.selected_color_sector_ids)} + ), + **( + {} + if self.lc_topology_replay is None + else {"lc_topology_replay": self.lc_topology_replay.to_json_dict()} + ), + **( + {} + if self.color_topology_replay is None + else { + "color_topology_replay": (self.color_topology_replay.to_json_dict()) + } + ), + **( + {} + if self.helicity_recurrence is None + else { + "helicity_recurrence": ( + { + **self.helicity_recurrence.to_json_dict(), + **( + {} + if self.helicity_materialization is None + else { + "materialization": ( + self.helicity_materialization.to_runtime_manifest() + ) + } + ), + } + ) + } + ), + "interaction_count": len(self.interactions), + "interaction_evaluation_count": self.interaction_evaluation_count, + "interaction_fanout_histogram": [ + [fanout, count] for fanout, count in self.interaction_fanout_histogram + ], + "amplitude_root_count": len(self.amplitude_roots), + "truncated": self.truncated, + "required_vertex_kinds": list(self.required_vertex_kinds), + "currents": [current.to_json_dict() for current in self.currents], + "sources": list(self.sources), + "interactions": [ + interaction.to_json_dict() for interaction in self.interactions + ], + "amplitude_roots": [root.to_json_dict() for root in self.amplitude_roots], + } + + +def _labels_mask(labels: Iterable[int]) -> int: + mask = 0 + for label in labels: + mask |= 1 << (label - 1) + return mask diff --git a/pyamplicol/src/pyamplicol/generation/eager_columnar.py b/pyamplicol/src/pyamplicol/generation/eager_columnar.py new file mode 100644 index 0000000000..813b5ce849 --- /dev/null +++ b/pyamplicol/src/pyamplicol/generation/eager_columnar.py @@ -0,0 +1,2745 @@ +# SPDX-License-Identifier: 0BSD +"""Columnar Python-to-Rust input contract for eager DAG lowering. + +This module deliberately stops before executable eager-table construction. It +turns the immutable, proof-carrying Python DAG into deterministic primitive +columns that a later Rust lowerer can consume without first constructing the +expanded runtime schema. The production generation path does not use this +contract yet. +""" + +from __future__ import annotations + +import hashlib +import json +import math +import struct +from collections.abc import Iterable, Mapping, Sequence +from dataclasses import dataclass +from itertools import product +from typing import Any + +import numpy as np + +from ..color import build_color_contraction_plan +from ..color.contraction_types import ( + ColorContractionEntry, + ColorContractionTemplateEntry, +) +from ..models.base import Model +from .contracts import runtime_coupling_parameter_names +from .dag_types import GenericDAG +from .eager_lowering import EagerKernelResolver, EagerResolvedKernel +from .helicity_replay import ( + HELICITY_RECURRENCE_CONTRACT_VERSION, + HELICITY_RECURRENCE_PROOF_ALGORITHM, +) +from .runtime_amplitudes import ( + _amplitude_groups, + _color_topology_replay_amplitudes, + _has_multiple_lc_root_sectors, + _root_all_sector_weight, +) + +EAGER_LOWERING_INPUT_ABI = "pyamplicol-eager-lowering-input-v1" +EAGER_LOWERING_INPUT_SCHEMA_SHA256 = ( + "59d5a3193ca25af6224c296297425782c394a34e352be622f47045e90d2bce29" +) +MISSING_U32 = (1 << 32) - 1 +MISSING_I32 = -(1 << 31) + +FACTOR_EXACT_SOURCE_BINARY64 = 0 +FACTOR_EXACT_SOURCE_CANONICAL_IR = 1 + +_U8 = np.dtype("u1") +_U32 = np.dtype(" None: + if not self.name: + raise EagerColumnarInputError("column name must not be empty") + values = self.values + if not isinstance(values, np.ndarray): + raise TypeError(f"column {self.name!r} must be a NumPy array") + if values.ndim < 1: + raise EagerColumnarInputError( + f"column {self.name!r} must have a row dimension" + ) + if not values.flags.c_contiguous: + raise EagerColumnarInputError(f"column {self.name!r} must be C-contiguous") + if values.dtype.str not in _ALLOWED_DTYPES: + raise EagerColumnarInputError( + f"column {self.name!r} has unsupported dtype {values.dtype.str!r}" + ) + if values.flags.writeable: + raise EagerColumnarInputError(f"column {self.name!r} must be read-only") + + +@dataclass(frozen=True, slots=True) +class EagerColumnarTable: + """A deterministic structure-of-arrays table.""" + + name: str + row_count: int + columns: tuple[EagerColumn, ...] + + def __post_init__(self) -> None: + if not self.name: + raise EagerColumnarInputError("table name must not be empty") + _checked_u64(self.row_count, f"{self.name}.row_count") + names = tuple(column.name for column in self.columns) + if len(set(names)) != len(names): + raise EagerColumnarInputError( + f"table {self.name!r} contains duplicate column names" + ) + for column in self.columns: + if len(column.values) != self.row_count: + raise EagerColumnarInputError( + f"table {self.name!r} column {column.name!r} has " + f"{len(column.values)} rows, expected {self.row_count}" + ) + + def column(self, name: str) -> np.ndarray[Any, Any]: + for column in self.columns: + if column.name == name: + return column.values + raise KeyError(f"table {self.name!r} has no column {name!r}") + + +@dataclass(frozen=True, slots=True) +class EagerLoweringInputV1: + """Complete immutable input consumed by the future Rust eager lowerer.""" + + abi: str + process_key: str + model_name: str + string_catalog: tuple[str, ...] + canonical_ir_catalog: tuple[str, ...] + tables: tuple[EagerColumnarTable, ...] + semantic_limitations: tuple[str, ...] = _SEMANTIC_LIMITATIONS + + def __post_init__(self) -> None: + if self.abi != EAGER_LOWERING_INPUT_ABI: + raise EagerColumnarInputError( + f"unsupported eager lowering input ABI {self.abi!r}" + ) + if not self.process_key or not self.model_name: + raise EagerColumnarInputError( + "process key and model name must not be empty" + ) + names = tuple(table.name for table in self.tables) + if names != tuple(sorted(names)): + raise EagerColumnarInputError( + "columnar table names must be sorted deterministically" + ) + if len(set(names)) != len(names): + raise EagerColumnarInputError("columnar table names must be unique") + _validate_catalog(self.string_catalog, "string") + _validate_catalog(self.canonical_ir_catalog, "canonical IR") + if not self.semantic_limitations: + raise EagerColumnarInputError( + "the v1 exact-factor limitation must be reported explicitly" + ) + self._validate_references() + + def table(self, name: str) -> EagerColumnarTable: + for table in self.tables: + if table.name == name: + return table + raise KeyError(f"eager lowering input has no table {name!r}") + + @property + def digest(self) -> str: + """Content digest independent of Python object identity and hash order.""" + + digest = hashlib.sha256() + for value in (self.abi, self.process_key, self.model_name): + _hash_text(digest, value) + for catalog in (self.string_catalog, self.canonical_ir_catalog): + digest.update(len(catalog).to_bytes(8, "little")) + for value in catalog: + _hash_text(digest, value) + digest.update(len(self.tables).to_bytes(8, "little")) + for table in self.tables: + _hash_text(digest, table.name) + digest.update(table.row_count.to_bytes(8, "little")) + digest.update(len(table.columns).to_bytes(4, "little")) + for column in table.columns: + _hash_text(digest, column.name) + _hash_text(digest, column.values.dtype.str) + digest.update(len(column.values.shape).to_bytes(1, "little")) + for size in column.values.shape: + digest.update(int(size).to_bytes(8, "little")) + digest.update(column.values.tobytes(order="C")) + for limitation in self.semantic_limitations: + _hash_text(digest, limitation) + return digest.hexdigest() + + @property + def schema_sha256(self) -> str: + """Digest the fixed table/column ABI independently of row counts.""" + + digest = hashlib.sha256() + digest.update(len(self.tables).to_bytes(8, "little")) + for table in self.tables: + _hash_text(digest, table.name) + digest.update(len(table.columns).to_bytes(4, "little")) + for column in table.columns: + _hash_text(digest, column.name) + _hash_text(digest, column.values.dtype.str) + digest.update(len(column.values.shape).to_bytes(1, "little")) + for size in column.values.shape[1:]: + digest.update(int(size).to_bytes(8, "little")) + return digest.hexdigest() + + def _validate_references(self) -> None: + current_count = self.table("currents").row_count + interaction_count = self.table("interactions").row_count + factor_count = self.table("exact_factors").row_count + string_count = len(self.string_catalog) + ir_count = len(self.canonical_ir_catalog) + + _validate_dense_ids(self.table("currents"), "id") + _validate_dense_ids(self.table("interactions"), "id") + _validate_dense_ids(self.table("roots"), "id") + _validate_bounded_columns( + self.table("interactions"), + ("left_current_id", "right_current_id", "result_current_id"), + current_count, + ) + _validate_bounded_columns( + self.table("roots"), + ("left_current_id", "right_current_id"), + current_count, + ) + _validate_bounded_columns(self.table("sources"), ("current_id",), current_count) + _validate_bounded_columns( + self.table("interaction_group_members"), + ("interaction_id",), + interaction_count, + ) + for table in self.tables: + factor_columns = tuple( + column.name + for column in table.columns + if column.name.endswith("factor_id") + ) + if factor_columns: + _validate_bounded_columns(table, factor_columns, factor_count) + _validate_bounded_columns( + self.table("exact_factors"), ("exact_ir_id",), ir_count + ) + _validate_optional_bounded_columns( + self.table("exact_factors"), ("source_ir_id",), ir_count + ) + _validate_bounded_columns( + self.table("exact_factors"), ("canonical_string_id",), string_count + ) + exact_sources = set( + int(value) for value in self.table("exact_factors").column("exact_source") + ) + if not exact_sources <= { + FACTOR_EXACT_SOURCE_BINARY64, + FACTOR_EXACT_SOURCE_CANONICAL_IR, + }: + raise EagerColumnarInputError( + "exact-factor catalog contains an unsupported provenance code" + ) + for table in self.tables: + string_columns = tuple( + column.name + for column in table.columns + if column.name.endswith("string_id") + ) + if string_columns: + _validate_optional_bounded_columns(table, string_columns, string_count) + _validate_optional_bounded_columns( + self.table("currents"), ("propagator_ir_id",), ir_count + ) + _validate_bounded_columns(self.table("roots"), ("contraction_ir_id",), ir_count) + _validate_bounded_columns( + self.table("contraction_coefficients"), + ("contraction_ir_id",), + ir_count, + ) + _validate_bounded_columns( + self.table("sources"), ("source_ir_id", "crossing_ir_id"), ir_count + ) + + +@dataclass(frozen=True, slots=True) +class _ModelParameterSpec: + name: str + kind: str + default: float + runtime_name: str | None = None + complex_component: int = -1 + parameter_type: str | None = None + pdg: int | None = None + vertex_kind: int | None = None + vertex_particles: tuple[int, int, int] | None = None + coupling_component: int | None = None + derived: bool = False + complex_domain: str | None = None + definition: str | None = None + + +class _StringCatalog: + def __init__(self) -> None: + self.values: list[str] = [] + self.ids: dict[str, int] = {} + + def add(self, value: str) -> int: + if not isinstance(value, str): + raise TypeError(f"catalog value must be a string, got {value!r}") + existing = self.ids.get(value) + if existing is not None: + return existing + result = _checked_u32(len(self.values), "catalog ID") + self.values.append(value) + self.ids[value] = result + return result + + def optional(self, value: str | None) -> int: + return MISSING_U32 if value is None else self.add(value) + + +class _SequenceCatalog: + def __init__(self, *, signed: bool) -> None: + self.signed = signed + self.values: list[tuple[int, ...]] = [] + self.ids: dict[tuple[int, ...], int] = {} + + def add(self, values: Iterable[int]) -> int: + item = tuple(int(value) for value in values) + checker = _checked_i32 if self.signed else _checked_u32 + for index, value in enumerate(item): + checker(value, f"sequence component {index}") + existing = self.ids.get(item) + if existing is not None: + return existing + result = _checked_u32(len(self.values), "sequence ID") + self.values.append(item) + self.ids[item] = result + return result + + def tables(self, prefix: str) -> tuple[EagerColumnarTable, EagerColumnarTable]: + ranges = _allocate(len(self.values), start=_U64, count=_U64) + total = sum(len(value) for value in self.values) + dtype = _I32 if self.signed else _U32 + flat = _allocate(total, value=dtype) + cursor = 0 + for index, values in enumerate(self.values): + ranges["start"][index] = cursor + ranges["count"][index] = len(values) + stop = cursor + len(values) + flat["value"][cursor:stop] = values + cursor = stop + return ( + _freeze_table(f"{prefix}_sequence_ranges", ranges), + _freeze_table(f"{prefix}_sequence_values", flat), + ) + + +class _BitsetCatalog: + def __init__(self) -> None: + self.values: list[int] = [] + self.ids: dict[int, int] = {} + + def add(self, value: int, context: str) -> int: + if isinstance(value, bool) or not isinstance(value, int): + raise TypeError(f"{context} must be an integer") + if value < 0: + raise EagerColumnarInputError(f"{context} must be nonnegative") + existing = self.ids.get(value) + if existing is not None: + return existing + result = _checked_u32(len(self.values), "bitset ID") + self.values.append(value) + self.ids[value] = result + return result + + def tables(self) -> tuple[EagerColumnarTable, EagerColumnarTable]: + ranges = _allocate(len(self.values), start=_U64, count=_U64, bit_count=_U64) + total = sum((value.bit_length() + 63) // 64 for value in self.values) + words = _allocate(total, value=_U64) + cursor = 0 + for index, value in enumerate(self.values): + count = (value.bit_length() + 63) // 64 + ranges["start"][index] = cursor + ranges["count"][index] = count + ranges["bit_count"][index] = value.bit_count() + for word_index in range(count): + words["value"][cursor] = (value >> (64 * word_index)) & ((1 << 64) - 1) + cursor += 1 + return ( + _freeze_table("bitset_ranges", ranges), + _freeze_table("bitset_words", words), + ) + + +class _FactorCatalog: + def __init__(self, canonical_ir: _CanonicalIRCatalog) -> None: + self.canonical_ir = canonical_ir + self.values: list[tuple[tuple[float, float], int, int, int]] = [] + self.ids: dict[tuple[int, int, int, int, int], int] = {} + self.binary_ids: dict[tuple[int, int, int], int] = {} + + def add( + self, + value: Sequence[object], + context: str, + *, + exact_ir: object | None = None, + source_ir_id: int = MISSING_U32, + ) -> int: + if len(value) != 2: + raise EagerColumnarInputError(f"{context} must be a complex pair") + pair = (float(value[0]), float(value[1])) + if not all(math.isfinite(component) for component in pair): + raise EagerColumnarInputError(f"{context} must be finite") + if source_ir_id != MISSING_U32: + _checked_u32(source_ir_id, f"{context} source IR ID") + bits = (_f64_bits(pair[0]), _f64_bits(pair[1])) + if exact_ir is None: + binary_key = (*bits, source_ir_id) + existing = self.binary_ids.get(binary_key) + if existing is not None: + return existing + exact_source = ( + FACTOR_EXACT_SOURCE_BINARY64 + if exact_ir is None + else FACTOR_EXACT_SOURCE_CANONICAL_IR + ) + exact_payload = ( + _binary64_exact_ir(pair) + if exact_ir is None + else { + "abi": "pyamplicol-exact-factor-v1", + "kind": "canonical-source-ir", + "source": exact_ir, + "f64_fallback": _binary64_exact_ir(pair), + } + ) + exact_ir_id = self.canonical_ir.add(exact_payload, f"{context} exact IR") + key = (*bits, exact_source, exact_ir_id, source_ir_id) + existing = self.ids.get(key) + if existing is not None: + return existing + result = _checked_u32(len(self.values), "exact-factor ID") + self.values.append((pair, exact_source, exact_ir_id, source_ir_id)) + self.ids[key] = result + if exact_ir is None: + self.binary_ids[(*bits, source_ir_id)] = result + return result + + def pair(self, factor_id: int) -> tuple[float, float]: + return self.values[factor_id][0] + + def table(self, strings: _StringCatalog) -> EagerColumnarTable: + columns = _allocate( + len(self.values), + real=_F64, + imaginary=_F64, + canonical_string_id=_U32, + exact_source=_U8, + exact_ir_id=_U32, + source_ir_id=_U32, + ) + for index, ( + (real, imaginary), + exact_source, + exact_ir_id, + source_ir_id, + ) in enumerate(self.values): + columns["real"][index] = real + columns["imaginary"][index] = imaginary + columns["canonical_string_id"][index] = strings.add( + f"complex-f64:{real.hex()}:{imaginary.hex()}" + ) + columns["exact_source"][index] = exact_source + columns["exact_ir_id"][index] = exact_ir_id + columns["source_ir_id"][index] = source_ir_id + return _freeze_table("exact_factors", columns) + + +class _CanonicalIRCatalog: + def __init__(self) -> None: + self.values: list[str] = [] + self.ids: dict[str, int] = {} + + def add(self, value: object, context: str) -> int: + try: + encoded = json.dumps( + value, + sort_keys=True, + separators=(",", ":"), + ensure_ascii=True, + allow_nan=False, + ) + except (TypeError, ValueError) as error: + raise EagerColumnarInputError( + f"{context} cannot be represented canonically: {error}" + ) from error + existing = self.ids.get(encoded) + if existing is not None: + return existing + result = _checked_u32(len(self.values), "canonical IR ID") + self.values.append(encoded) + self.ids[encoded] = result + return result + + +class _Builder: + def __init__( + self, + dag: GenericDAG, + model: Model, + resolver: EagerKernelResolver, + process_id: str | None, + ) -> None: + if not isinstance(dag, GenericDAG): + raise TypeError("eager columnar extraction requires a GenericDAG") + if not isinstance(model, Model): + raise TypeError("eager columnar extraction requires a Model") + self.dag = dag + self.model = model + self.resolver = resolver + self.process_key = process_id or dag.process.key + self.strings = _StringCatalog() + self.ir = _CanonicalIRCatalog() + self.u32_sequences = _SequenceCatalog(signed=False) + self.i32_sequences = _SequenceCatalog(signed=True) + self.bitsets = _BitsetCatalog() + self.factors = _FactorCatalog(self.ir) + self.tables: list[EagerColumnarTable] = [] + self.coupling_specs: list[tuple[int, tuple[int, int, int], int, int]] = [] + self.coupling_ids: dict[ + tuple[int, tuple[int, int, int], tuple[float, float]], int + ] = {} + + self._validate_dense_dag() + self.group_ids, self.group_descriptors = _amplitude_groups( + dag, dag.amplitude_roots + ) + self.color_topology_replay = _color_topology_replay_amplitudes( + dag, self.group_descriptors + ) + + def build(self) -> EagerLoweringInputV1: + self._metadata() + self._currents() + self._sources() + self._interactions() + self._roots() + self._interaction_groups() + self._color_plan() + self._topology_replay() + self._color_topology_replay_amplitudes() + self._helicity_proof() + self._helicity_materialization() + self._selectors_and_reductions() + self._couplings() + self._model_parameters() + self.tables.append(self.factors.table(self.strings)) + self.tables.extend(self.bitsets.tables()) + self.tables.extend(self.u32_sequences.tables("u32")) + self.tables.extend(self.i32_sequences.tables("i32")) + return EagerLoweringInputV1( + abi=EAGER_LOWERING_INPUT_ABI, + process_key=self.process_key, + model_name=self.model.name, + string_catalog=tuple(self.strings.values), + canonical_ir_catalog=tuple(self.ir.values), + tables=tuple(sorted(self.tables, key=lambda table: table.name)), + ) + + def _validate_dense_dag(self) -> None: + for context, records in ( + ("current", self.dag.currents), + ("interaction", self.dag.interactions), + ("amplitude root", self.dag.amplitude_roots), + ): + _checked_u32(len(records), f"{context} count") + for expected, record in enumerate(records): + _checked_u32(record.id, f"{context} id") + if record.id != expected: + raise EagerColumnarInputError( + f"{context} ids must be dense; found {record.id} at {expected}" + ) + current_count = len(self.dag.currents) + for interaction in self.dag.interactions: + for field, current_id in ( + ("left", interaction.left_id), + ("right", interaction.right_id), + ("result", interaction.result_id), + ): + _require_index(current_id, current_count, f"interaction {field}") + for root in self.dag.amplitude_roots: + _require_index(root.left_id, current_count, "root left current") + _require_index(root.right_id, current_count, "root right current") + for source_id in self.dag.sources: + _require_index(source_id, current_count, "source current") + if not self.dag.currents[source_id].is_source: + raise EagerColumnarInputError( + f"DAG source {source_id} is not marked as a source current" + ) + + def _metadata(self) -> None: + columns = _allocate( + 1, + process_ir_id=_U32, + normalization_ir_id=_U32, + model_name_string_id=_U32, + process_key_string_id=_U32, + helicity_coverage_string_id=_U32, + color_coverage_string_id=_U32, + color_accuracy_string_id=_U32, + truncated=_U8, + color_plan_truncated=_U8, + trace_reflections_folded=_U8, + ) + columns["process_ir_id"][0] = self.ir.add( + self.dag.process.to_json_dict(), "process IR" + ) + columns["normalization_ir_id"][0] = self.ir.add( + dict(self.model.runtime_normalization_payload(self.dag)), + "runtime normalization", + ) + columns["model_name_string_id"][0] = self.strings.add(self.model.name) + columns["process_key_string_id"][0] = self.strings.add(self.process_key) + columns["helicity_coverage_string_id"][0] = self.strings.add( + self.dag.helicity_coverage + ) + columns["color_coverage_string_id"][0] = self.strings.add( + self.dag.color_coverage + ) + columns["color_accuracy_string_id"][0] = self.strings.add( + self.dag.process.color_accuracy + ) + columns["truncated"][0] = self.dag.truncated + columns["color_plan_truncated"][0] = self.dag.color_plan.truncated + columns["trace_reflections_folded"][0] = ( + self.dag.color_plan.trace_reflections_folded + ) + self.tables.append(_freeze_table("metadata", columns)) + + diagnostics = _allocate( + len(self.dag.color_plan.diagnostics), message_string_id=_U32 + ) + for row, message in enumerate(self.dag.color_plan.diagnostics): + diagnostics["message_string_id"][row] = self.strings.add(str(message)) + self.tables.append(_freeze_table("color_plan_diagnostics", diagnostics)) + + selected_helicities = _allocate( + len(self.dag.selected_source_helicities), + external_label=_U32, + helicity=_I32, + ) + for index, (label, helicity) in enumerate(self.dag.selected_source_helicities): + selected_helicities["external_label"][index] = _checked_u32( + label, "selected-helicity external label" + ) + selected_helicities["helicity"][index] = _checked_i32( + helicity, "selected helicity" + ) + self.tables.append( + _freeze_table("selected_source_helicities", selected_helicities) + ) + selected_colors = _allocate( + len(self.dag.selected_color_sector_ids), sector_id=_U32 + ) + selected_colors["sector_id"][:] = self.dag.selected_color_sector_ids + self.tables.append(_freeze_table("selected_color_sectors", selected_colors)) + + def _currents(self) -> None: + columns = _allocate( + len(self.dag.currents), + id=_U32, + particle_id=_I32, + dimension=_U32, + is_source=_U8, + source_leg_label=_U32, + source_helicity=_I32, + external_mask_bitset_id=_U32, + external_labels_sequence_id=_U32, + ordered_external_labels_sequence_id=_U32, + helicity_ancestry_bitset_id=_U32, + chirality=_I32, + spin_state_sequence_id=_U32, + flavour_flow_sequence_id=_U32, + momentum_mask_bitset_id=_U32, + coupling_order_start=_U64, + coupling_order_count=_U64, + quantum_flow_start=_U64, + quantum_flow_count=_U64, + color_accuracy_string_id=_U32, + color_sector_id=_U32, + color_line_groups_sequence_id=_U32, + color_basis_keys_sequence_id=_U32, + auxiliary_kind_string_id=_U32, + propagator_ir_id=_U32, + propagator_kernel_id=_U32, + ) + coupling_order_count = sum( + len(current.index.coupling_orders) for current in self.dag.currents + ) + quantum_flow_count = sum( + len(current.index.quantum_number_flow) for current in self.dag.currents + ) + coupling_orders = _allocate( + coupling_order_count, name_string_id=_U32, value=_I32 + ) + quantum_flows = _allocate( + quantum_flow_count, name_string_id=_U32, expression_string_id=_U32 + ) + coupling_cursor = 0 + quantum_cursor = 0 + for row, current in enumerate(self.dag.currents): + current_index = current.index + columns["id"][row] = current.id + columns["particle_id"][row] = _checked_i32( + current_index.particle_id, "current particle id" + ) + columns["dimension"][row] = _positive_u32( + current.dimension, "current dimension" + ) + columns["is_source"][row] = current.is_source + columns["source_leg_label"][row] = ( + MISSING_U32 + if current.source_leg_label is None + else _checked_u32(current.source_leg_label, "source leg label") + ) + columns["source_helicity"][row] = ( + MISSING_I32 + if current.source_helicity is None + else _checked_i32(current.source_helicity, "source helicity") + ) + columns["external_mask_bitset_id"][row] = self.bitsets.add( + current_index.external_mask, "current external mask" + ) + columns["external_labels_sequence_id"][row] = self.u32_sequences.add( + current_index.external_labels + ) + columns["ordered_external_labels_sequence_id"][row] = ( + self.u32_sequences.add(current_index.ordered_external_labels) + ) + columns["helicity_ancestry_bitset_id"][row] = self.bitsets.add( + current_index.helicity_ancestry, "current helicity ancestry" + ) + columns["chirality"][row] = _checked_i32( + current_index.chirality, "current chirality" + ) + spin_state = ( + current_index.spin_state + if isinstance(current_index.spin_state, tuple) + else (current_index.spin_state,) + ) + columns["spin_state_sequence_id"][row] = self.i32_sequences.add(spin_state) + columns["flavour_flow_sequence_id"][row] = self.i32_sequences.add( + current_index.flavour_flow + ) + columns["momentum_mask_bitset_id"][row] = self.bitsets.add( + current_index.momentum_mask, "current momentum mask" + ) + columns["coupling_order_start"][row] = coupling_cursor + columns["coupling_order_count"][row] = len(current_index.coupling_orders) + for name, value in current_index.coupling_orders: + coupling_orders["name_string_id"][coupling_cursor] = self.strings.add( + str(name) + ) + coupling_orders["value"][coupling_cursor] = _checked_i32( + value, "coupling order" + ) + coupling_cursor += 1 + columns["quantum_flow_start"][row] = quantum_cursor + columns["quantum_flow_count"][row] = len(current_index.quantum_number_flow) + for name, expression in current_index.quantum_number_flow: + quantum_flows["name_string_id"][quantum_cursor] = self.strings.add( + str(name) + ) + quantum_flows["expression_string_id"][quantum_cursor] = ( + self.strings.add(str(expression)) + ) + quantum_cursor += 1 + color = current_index.color_state + if not all(isinstance(value, str) for value in color.basis_key): + raise EagerColumnarInputError( + f"current {current.id} color basis keys must be strings" + ) + columns["color_accuracy_string_id"][row] = self.strings.add(color.accuracy) + columns["color_sector_id"][row] = _checked_u32( + color.sector_id, "current color sector" + ) + columns["color_line_groups_sequence_id"][row] = self.i32_sequences.add( + color.line_groups + ) + columns["color_basis_keys_sequence_id"][row] = self.u32_sequences.add( + self.strings.add(value) for value in color.basis_key + ) + columns["auxiliary_kind_string_id"][row] = self.strings.optional( + current_index.auxiliary_kind + ) + propagator = self.model._propagator_ir( + current_index.particle_id, current_index.chirality + ) + columns["propagator_ir_id"][row] = self.ir.add( + propagator.to_json_dict(), "propagator IR" + ) + kernel_id = self.resolver.propagator_kernel_id( + current, propagator.to_json_dict() + ) + columns["propagator_kernel_id"][row] = ( + MISSING_U32 + if kernel_id is None + else _checked_u32(kernel_id, "prepared propagator kernel id") + ) + self.tables.append(_freeze_table("currents", columns)) + self.tables.append(_freeze_table("coupling_orders", coupling_orders)) + self.tables.append(_freeze_table("quantum_flows", quantum_flows)) + + momentum_masks = sorted( + {current.index.momentum_mask for current in self.dag.currents}, + key=lambda value: (value.bit_count(), value), + ) + momentum_columns = _allocate(len(momentum_masks), slot_id=_U32, bitset_id=_U32) + for slot_id, mask in enumerate(momentum_masks): + momentum_columns["slot_id"][slot_id] = slot_id + momentum_columns["bitset_id"][slot_id] = self.bitsets.add( + mask, "momentum mask" + ) + self.tables.append(_freeze_table("momentum_masks", momentum_columns)) + + def _sources(self) -> None: + columns = _allocate( + len(self.dag.sources), + source_id=_U32, + current_id=_U32, + external_label=_U32, + input_momentum_slot=_U32, + source_ir_id=_U32, + crossing_ir_id=_U32, + crossing_factor_id=_U32, + declared_state_index=_U32, + ) + legs = {leg.label: leg for leg in self.dag.process.legs} + for source_id, current_id in enumerate(self.dag.sources): + current = self.dag.currents[current_id] + if current.source_leg_label is None or current.source_helicity is None: + raise EagerColumnarInputError( + f"source current {current_id} lacks leg/helicity provenance" + ) + leg = legs.get(current.source_leg_label) + if leg is None: + raise EagerColumnarInputError( + f"source current {current_id} refers to unknown external leg" + ) + source_ir = self.model._source_ir(current.index.particle_id) + crossing = ( + source_ir.crossing + if leg.is_initial + else type(source_ir.crossing).identity() + ) + expected = ( + current.source_helicity, + current.index.chirality, + current.index.spin_state, + ) + state_index = next( + ( + index + for index, state in enumerate(source_ir.states) + if ( + (applied := crossing.apply(state)).helicity, + applied.chirality, + applied.spin_state, + ) + == expected + ), + None, + ) + if state_index is None: + raise EagerColumnarInputError( + f"source current {current_id} state {expected!r} is not declared" + ) + columns["source_id"][source_id] = source_id + columns["current_id"][source_id] = current_id + columns["external_label"][source_id] = current.source_leg_label + columns["input_momentum_slot"][source_id] = leg.label - 1 + source_ir_id = self.ir.add(source_ir.to_json_dict(), "source IR") + crossing_ir_id = self.ir.add(crossing.to_json_dict(), "source crossing IR") + columns["source_ir_id"][source_id] = source_ir_id + columns["crossing_ir_id"][source_id] = crossing_ir_id + columns["crossing_factor_id"][source_id] = self.factors.add( + crossing.phase, + "source crossing phase", + source_ir_id=crossing_ir_id, + ) + columns["declared_state_index"][source_id] = state_index + self.tables.append(_freeze_table("sources", columns)) + + def _interactions(self) -> None: + group_by_key: dict[tuple[str, int], int] = {} + self._interaction_group_members: list[list[int]] = [] + columns = _allocate( + len(self.dag.interactions), + id=_U32, + stage_subset_size=_U32, + vertex_kind=_I32, + vertex_particles=(_I32, (3,)), + left_current_id=_U32, + right_current_id=_U32, + result_current_id=_U32, + coupling_id=_U32, + coupling_factor_id=_U32, + color_factor_id=_U32, + evaluation_factor_id=_U32, + evaluation_group_id=_U32, + lowering_backend_string_id=_U32, + full_tensor_network_ready=_U8, + kernel_id=_U32, + canonical_input_order=(_U8, (2,)), + kernel_normalization_factor_id=_U32, + output_factor_source=_U8, + ) + for row, interaction in enumerate(self.dag.interactions): + resolved = self.resolver.vertex_kernel(interaction) + coupling_id = self._coupling( + interaction.vertex_kind, + interaction.vertex_particles, + interaction.coupling, + ) + columns["id"][row] = interaction.id + columns["stage_subset_size"][row] = len( + self.dag.currents[interaction.result_id].index.external_labels + ) + columns["vertex_kind"][row] = _checked_i32( + interaction.vertex_kind, "vertex kind" + ) + columns["vertex_particles"][row] = interaction.vertex_particles + columns["left_current_id"][row] = interaction.left_id + columns["right_current_id"][row] = interaction.right_id + columns["result_current_id"][row] = interaction.result_id + columns["coupling_id"][row] = coupling_id + columns["coupling_factor_id"][row] = self.coupling_specs[coupling_id][2] + columns["color_factor_id"][row] = self.factors.add( + interaction.color_weight, "interaction color weight" + ) + columns["evaluation_factor_id"][row] = self.factors.add( + interaction.evaluation_factor, "interaction evaluation factor" + ) + columns["lowering_backend_string_id"][row] = self.strings.add( + interaction.lowering_backend + ) + columns["full_tensor_network_ready"][row] = ( + interaction.full_tensor_network_ready + ) + _fill_resolved_kernel(columns, row, resolved, self.factors) + group_key = ( + ("group", int(interaction.evaluation_group_id)) + if interaction.evaluation_group_id is not None + else ("interaction", interaction.id) + ) + group_id = group_by_key.get(group_key) + if group_id is None: + group_id = len(group_by_key) + group_by_key[group_key] = group_id + self._interaction_group_members.append([]) + self._interaction_group_members[group_id].append(interaction.id) + columns["evaluation_group_id"][row] = group_id + self.tables.append(_freeze_table("interactions", columns)) + + def _roots(self) -> None: + contraction_records: dict[int, object] = {} + columns = _allocate( + len(self.dag.amplitude_roots), + id=_U32, + kind_string_id=_U32, + left_current_id=_U32, + right_current_id=_U32, + color_factor_id=_U32, + contraction_ir_id=_U32, + color_sector_id=_U32, + vertex_kind=_I32, + vertex_particles=(_I32, (3,)), + coupling_id=_U32, + coupling_factor_id=_U32, + helicity_weight_factor_id=_U32, + coherent_group_id=_U32, + kernel_id=_U32, + canonical_input_order=(_U8, (2,)), + kernel_normalization_factor_id=_U32, + output_factor_source=_U8, + ) + for row, root in enumerate(self.dag.amplitude_roots): + resolved = self.resolver.closure_kernel(root.to_json_dict()) + columns["id"][row] = root.id + columns["kind_string_id"][row] = self.strings.add(root.kind) + columns["left_current_id"][row] = root.left_id + columns["right_current_id"][row] = root.right_id + columns["color_factor_id"][row] = self.factors.add( + root.color_weight, "root color weight" + ) + contraction_ir_id = self.ir.add( + root.contraction_ir.to_json_dict(), "contraction IR" + ) + columns["contraction_ir_id"][row] = contraction_ir_id + contraction_records.setdefault(contraction_ir_id, root.contraction_ir) + sector_id = ( + root.color_sector_id + if root.color_sector_id is not None + else self.dag.currents[root.left_id].index.color_state.sector_id + ) + columns["color_sector_id"][row] = _checked_u32( + sector_id, "root color sector" + ) + columns["vertex_kind"][row] = ( + MISSING_I32 + if root.vertex_kind is None + else _checked_i32(root.vertex_kind, "root vertex kind") + ) + columns["vertex_particles"][row] = ( + (MISSING_I32, MISSING_I32, MISSING_I32) + if root.vertex_particles is None + else root.vertex_particles + ) + coupling_id = ( + MISSING_U32 + if root.vertex_kind is None or root.vertex_particles is None + else self._coupling( + root.vertex_kind, root.vertex_particles, root.coupling + ) + ) + columns["coupling_id"][row] = coupling_id + columns["coupling_factor_id"][row] = ( + self.factors.add(root.coupling, "direct root coupling") + if coupling_id == MISSING_U32 + else self.coupling_specs[coupling_id][2] + ) + columns["helicity_weight_factor_id"][row] = self.factors.add( + (root.helicity_weight, 0.0), "root helicity weight" + ) + columns["coherent_group_id"][row] = self.group_ids[root.id] + if resolved is None: + columns["kernel_id"][row] = MISSING_U32 + columns["canonical_input_order"][row] = (0, 1) + columns["kernel_normalization_factor_id"][row] = self.factors.add( + (1.0, 0.0), "direct closure normalization" + ) + columns["output_factor_source"][row] = 0 + else: + _fill_resolved_kernel(columns, row, resolved, self.factors) + self.tables.append(_freeze_table("roots", columns)) + + coefficient_count = sum( + len(record.coefficients) for record in contraction_records.values() + ) + coefficients = _allocate( + coefficient_count, + contraction_ir_id=_U32, + component_index=_U32, + factor_id=_U32, + ) + cursor = 0 + for contraction_ir_id, record in sorted(contraction_records.items()): + for component_index, coefficient in enumerate(record.coefficients): + coefficients["contraction_ir_id"][cursor] = contraction_ir_id + coefficients["component_index"][cursor] = component_index + coefficients["factor_id"][cursor] = self.factors.add( + coefficient, + "contraction coefficient", + source_ir_id=contraction_ir_id, + ) + cursor += 1 + self.tables.append(_freeze_table("contraction_coefficients", coefficients)) + + def _interaction_groups(self) -> None: + members_by_group = self._interaction_group_members + groups = _allocate( + len(members_by_group), + representative_interaction_id=_U32, + member_start=_U64, + member_count=_U64, + ) + members = _allocate(len(self.dag.interactions), interaction_id=_U32) + cursor = 0 + for group_id, raw_group_members in enumerate(members_by_group): + # Rust normalizes attachment factors by the representative + # evaluation factor. Keep a live member first for mixed groups; + # an all-zero group remains explicit in the retained input and is + # elided before runtime-plan invocation emission. + group_members = sorted( + raw_group_members, + key=lambda interaction_id: ( + self.dag.interactions[ + interaction_id + ].evaluation_factor + == (0.0, 0.0) + ), + ) + groups["representative_interaction_id"][group_id] = group_members[0] + groups["member_start"][group_id] = cursor + groups["member_count"][group_id] = len(group_members) + stop = cursor + len(group_members) + members["interaction_id"][cursor:stop] = group_members + cursor = stop + self.tables.append(_freeze_table("interaction_groups", groups)) + self.tables.append(_freeze_table("interaction_group_members", members)) + + def _color_plan(self) -> None: + sectors = _allocate( + len(self.dag.color_plan.sectors), + id=_U32, + kind_string_id=_U32, + open_line_start=_U64, + open_line_count=_U64, + trace_labels_sequence_id=_U32, + singlet_labels_sequence_id=_U32, + word_labels_sequence_id=_U32, + ) + open_line_count = sum( + len(sector.open_color_lines) for sector in self.dag.color_plan.sectors + ) + open_lines = _allocate( + open_line_count, + fundamental_label=_U32, + antifundamental_label=_U32, + adjoint_labels_sequence_id=_U32, + singlet_labels_sequence_id=_U32, + ) + cursor = 0 + for index, sector in enumerate(self.dag.color_plan.sectors): + if sector.id != index: + raise EagerColumnarInputError("color-sector ids must be dense") + sectors["id"][index] = sector.id + sectors["kind_string_id"][index] = self.strings.add(sector.kind) + sectors["open_line_start"][index] = cursor + sectors["open_line_count"][index] = len(sector.open_color_lines) + sectors["trace_labels_sequence_id"][index] = self.u32_sequences.add( + sector.trace_labels + ) + sectors["singlet_labels_sequence_id"][index] = self.u32_sequences.add( + sector.singlet_labels + ) + word = sector.word_labels or sector.color_words[0] + sectors["word_labels_sequence_id"][index] = self.u32_sequences.add(word) + for line in sector.open_color_lines: + open_lines["fundamental_label"][cursor] = line.fundamental_label + open_lines["antifundamental_label"][cursor] = line.antifundamental_label + open_lines["adjoint_labels_sequence_id"][cursor] = ( + self.u32_sequences.add(line.adjoint_labels) + ) + open_lines["singlet_labels_sequence_id"][cursor] = ( + self.u32_sequences.add(line.singlet_labels) + ) + cursor += 1 + self.tables.append(_freeze_table("color_sectors", sectors)) + self.tables.append(_freeze_table("color_open_lines", open_lines)) + + def _topology_replay(self) -> None: + replay = self.dag.lc_topology_replay or self.dag.color_topology_replay + replay_kind = ( + 0 + if replay is None + else (1 if self.dag.lc_topology_replay is not None else 2) + ) + replay_metadata = _allocate( + 1, + present=_U8, + replay_kind=_U8, + physical_sector_ids_sequence_id=_U32, + materialized_sector_ids_sequence_id=_U32, + ) + replay_metadata["present"][0] = replay is not None + replay_metadata["replay_kind"][0] = replay_kind + replay_metadata["physical_sector_ids_sequence_id"][0] = ( + MISSING_U32 + if replay is None + else self.u32_sequences.add(replay.physical_sector_ids) + ) + replay_metadata["materialized_sector_ids_sequence_id"][0] = ( + MISSING_U32 + if replay is None + else self.u32_sequences.add(replay.materialized_sector_ids) + ) + self.tables.append(_freeze_table("lc_replay_metadata", replay_metadata)) + diagnostics_values = () if replay is None else replay.diagnostics + diagnostics = _allocate(len(diagnostics_values), message_string_id=_U32) + for row, message in enumerate(diagnostics_values): + diagnostics["message_string_id"][row] = self.strings.add(str(message)) + self.tables.append(_freeze_table("lc_replay_diagnostics", diagnostics)) + + partitions = () if replay is None else replay.partitions + partition_columns = _allocate( + len(partitions), + representative_sector_id=_U32, + materialized_sector_id=_U32, + member_start=_U64, + member_count=_U64, + proof_algorithm_string_id=_U32, + proof_digest_string_id=_U32, + ) + member_count = sum(len(partition.active_sector_ids) for partition in partitions) + members = _allocate( + member_count, + sector_id=_U32, + factor_id=_U32, + sign=_I32, + permutation_start=_U64, + permutation_count=_U64, + ) + permutation_count = sum( + len(permutation) + for partition in partitions + for permutation in partition.label_permutations + ) + permutations = _allocate( + permutation_count, representative_label=_U32, sector_label=_U32 + ) + member_cursor = 0 + permutation_cursor = 0 + for partition_id, partition in enumerate(partitions): + partition_columns["representative_sector_id"][partition_id] = ( + partition.representative_sector_id + ) + partition_columns["materialized_sector_id"][partition_id] = int( + partition.materialized_sector_id + ) + partition_columns["member_start"][partition_id] = member_cursor + partition_columns["member_count"][partition_id] = len( + partition.active_sector_ids + ) + partition_columns["proof_algorithm_string_id"][partition_id] = ( + self.strings.add(str(partition.proof_algorithm)) + ) + partition_columns["proof_digest_string_id"][partition_id] = ( + self.strings.add(str(partition.proof_digest)) + ) + for sector_id, permutation, weight, sign in zip( + partition.active_sector_ids, + partition.label_permutations, + partition.weights, + partition.signs, + strict=True, + ): + members["sector_id"][member_cursor] = sector_id + members["factor_id"][member_cursor] = self.factors.add( + (weight * sign, 0.0), "LC replay factor" + ) + members["sign"][member_cursor] = sign + members["permutation_start"][member_cursor] = permutation_cursor + members["permutation_count"][member_cursor] = len(permutation) + for representative_label, sector_label in permutation: + permutations["representative_label"][permutation_cursor] = ( + representative_label + ) + permutations["sector_label"][permutation_cursor] = sector_label + permutation_cursor += 1 + member_cursor += 1 + residual_ids = () if replay is None else replay.residual_sector_ids + residuals = _allocate(len(residual_ids), sector_id=_U32) + residuals["sector_id"][:] = residual_ids + self.tables.append(_freeze_table("lc_replay_partitions", partition_columns)) + self.tables.append(_freeze_table("lc_replay_members", members)) + self.tables.append(_freeze_table("lc_replay_permutations", permutations)) + self.tables.append(_freeze_table("lc_replay_residual_sectors", residuals)) + + def _color_topology_replay_amplitudes(self) -> None: + replay = self.color_topology_replay + metadata = _allocate( + 1, + present=_U8, + contract_version=_U32, + physical_group_count=_U64, + mapping_count=_U64, + ) + metadata["present"][0] = replay is not None + metadata["contract_version"][0] = 0 if replay is None else 1 + metadata["physical_group_count"][0] = ( + 0 if replay is None else len(replay.physical_descriptors) + ) + metadata["mapping_count"][0] = ( + 0 if replay is None else len(replay.mappings) + ) + self.tables.append(_freeze_table("color_replay_metadata", metadata)) + + physical_groups = () if replay is None else replay.physical_descriptors + group_columns = _allocate( + len(physical_groups), + group_id=_U32, + color_sector_id=_U32, + color_word_sequence_id=_U32, + helicity_values_sequence_id=_U32, + helicity_weight_factor_id=_U32, + ) + for row, descriptor in enumerate(physical_groups): + group_columns["group_id"][row] = descriptor.group_id + group_columns["color_sector_id"][row] = descriptor.sector_id + group_columns["color_word_sequence_id"][row] = self.u32_sequences.add( + descriptor.word + ) + group_columns["helicity_values_sequence_id"][row] = ( + self.i32_sequences.add( + _descriptor_helicity_vector(self.dag, descriptor.helicity_key) + ) + ) + group_columns["helicity_weight_factor_id"][row] = self.factors.add( + (descriptor.helicity_weight, 0.0), + "color replay physical-group helicity weight", + ) + self.tables.append( + _freeze_table("color_replay_physical_groups", group_columns) + ) + + mappings = () if replay is None else replay.mappings + mapping_columns = _allocate( + len(mappings), + permutation_start=_U64, + permutation_count=_U64, + route_start=_U64, + route_count=_U64, + ) + permutation_count = sum(len(mapping.label_permutation) for mapping in mappings) + route_count = sum(len(mapping.group_routes) for mapping in mappings) + permutations = _allocate( + permutation_count, + representative_label=_U32, + sector_label=_U32, + ) + routes = _allocate( + route_count, + source_group_id=_U32, + target_group_id=_U32, + factor_id=_U32, + ) + permutation_cursor = 0 + route_cursor = 0 + for mapping_id, mapping in enumerate(mappings): + mapping_columns["permutation_start"][mapping_id] = permutation_cursor + mapping_columns["permutation_count"][mapping_id] = len( + mapping.label_permutation + ) + mapping_columns["route_start"][mapping_id] = route_cursor + mapping_columns["route_count"][mapping_id] = len(mapping.group_routes) + for representative_label, sector_label in mapping.label_permutation: + permutations["representative_label"][permutation_cursor] = ( + representative_label + ) + permutations["sector_label"][permutation_cursor] = sector_label + permutation_cursor += 1 + for route in mapping.group_routes: + routes["source_group_id"][route_cursor] = route.source_group_id + routes["target_group_id"][route_cursor] = route.target_group_id + routes["factor_id"][route_cursor] = self.factors.add( + route.factor, + "color replay amplitude route factor", + ) + route_cursor += 1 + self.tables.append(_freeze_table("color_replay_mappings", mapping_columns)) + self.tables.append( + _freeze_table("color_replay_permutations", permutations) + ) + self.tables.append(_freeze_table("color_replay_routes", routes)) + + contraction = ( + None + if replay is None + else build_color_contraction_plan( + self.dag.color_plan, replay.physical_descriptors + ) + ) + if replay is not None and ( + contraction is None + or not contraction.supported + or contraction.group_count != len(replay.physical_descriptors) + ): + raise EagerColumnarInputError( + "color replay requires a complete physical color contraction" + ) + contraction_metadata = _allocate( + 1, + present=_U8, + group_count=_U64, + includes_color_factor=_U8, + repeated=_U8, + component_count=_U64, + ) + contraction_metadata["present"][0] = contraction is not None + contraction_metadata["group_count"][0] = ( + 0 if contraction is None else contraction.group_count + ) + contraction_metadata["includes_color_factor"][0] = ( + False if contraction is None else contraction.includes_color_factor + ) + repeated = None if contraction is None else contraction.repeated_block + contraction_metadata["repeated"][0] = repeated is not None + contraction_metadata["component_count"][0] = ( + 0 if repeated is None else repeated.component_count + ) + self.tables.append( + _freeze_table( + "color_replay_contraction_metadata", contraction_metadata + ) + ) + component_group_ids = () if repeated is None else repeated.component_group_ids + component_groups = _allocate( + len(component_group_ids), + group_id=_U32, + ) + for row, group_id in enumerate(component_group_ids): + component_groups["group_id"][row] = group_id + self.tables.append( + _freeze_table( + "color_replay_contraction_component_groups", + component_groups, + ) + ) + + entries = ( + () + if contraction is None + else (contraction.entries if repeated is None else repeated.entries) + ) + contraction_entries = _allocate( + len(entries), + left_group_id=_U32, + right_group_id=_U32, + left_group_index=_U32, + right_group_index=_U32, + weight_factor_id=_U32, + symmetry_factor_id=_U32, + ) + for row, entry in enumerate(entries): + if repeated is None: + contraction_entries["left_group_id"][row] = entry.left_group_id + contraction_entries["right_group_id"][row] = entry.right_group_id + contraction_entries["left_group_index"][row] = MISSING_U32 + contraction_entries["right_group_index"][row] = MISSING_U32 + else: + contraction_entries["left_group_id"][row] = MISSING_U32 + contraction_entries["right_group_id"][row] = MISSING_U32 + contraction_entries["left_group_index"][row] = ( + entry.left_group_index + ) + contraction_entries["right_group_index"][row] = ( + entry.right_group_index + ) + contraction_entries["weight_factor_id"][row] = self.factors.add( + (entry.weight_re, entry.weight_im), + "color replay contraction weight", + exact_ir=_color_weight_ir(entry), + ) + contraction_entries["symmetry_factor_id"][row] = self.factors.add( + (entry.symmetry_factor, 0.0), + "color replay contraction symmetry", + ) + self.tables.append( + _freeze_table( + "color_replay_contraction_entries", contraction_entries + ) + ) + + def _helicity_proof(self) -> None: + proof = self.dag.helicity_recurrence + metadata = _allocate( + 1, + present=_U8, + contract_version=_U32, + proof_algorithm_string_id=_U32, + proof_current_count=_U64, + proof_root_count=_U64, + ) + metadata["present"][0] = proof is not None + metadata["contract_version"][0] = ( + 0 if proof is None else HELICITY_RECURRENCE_CONTRACT_VERSION + ) + metadata["proof_algorithm_string_id"][0] = ( + MISSING_U32 + if proof is None + else self.strings.add(HELICITY_RECURRENCE_PROOF_ALGORITHM) + ) + metadata["proof_current_count"][0] = 0 if proof is None else proof.current_count + metadata["proof_root_count"][0] = ( + 0 if proof is None else proof.amplitude_root_count + ) + self.tables.append(_freeze_table("helicity_proof_metadata", metadata)) + diagnostic_values = () if proof is None else proof.diagnostics + diagnostics = _allocate(len(diagnostic_values), message_string_id=_U32) + for row, message in enumerate(diagnostic_values): + diagnostics["message_string_id"][row] = self.strings.add(str(message)) + self.tables.append(_freeze_table("helicity_proof_diagnostics", diagnostics)) + + domains = () if proof is None else proof.selector_domains + domain_columns = _allocate( + len(domains), id=_U32, complete=_U8, state_start=_U64, state_count=_U64 + ) + state_count = sum(len(domain.source_states) for domain in domains) + states = _allocate(state_count, external_label=_U32, helicity=_I32) + state_cursor = 0 + for domain in domains: + domain_columns["id"][domain.id] = domain.id + domain_columns["complete"][domain.id] = domain.complete + domain_columns["state_start"][domain.id] = state_cursor + domain_columns["state_count"][domain.id] = len(domain.source_states) + for label, helicity in domain.source_states: + states["external_label"][state_cursor] = label + states["helicity"][state_cursor] = helicity + state_cursor += 1 + self.tables.append(_freeze_table("helicity_domains", domain_columns)) + self.tables.append(_freeze_table("helicity_domain_states", states)) + + classes = () if proof is None else proof.recurrence_classes + class_columns = _allocate( + len(classes), + representative_current_id=_U32, + external_labels_sequence_id=_U32, + source_class=_U8, + class_id_string_id=_U32, + proof_digest_string_id=_U32, + transition_contract_ids_sequence_id=_U32, + member_start=_U64, + member_count=_U64, + ) + member_count = sum(len(item.members) for item in classes) + members = _allocate( + member_count, + class_index=_U32, + current_id=_U32, + selector_domain_id=_U32, + factor_id=_U32, + ) + class_index_by_id: dict[str, int] = {} + cursor = 0 + for class_index, recurrence in enumerate(classes): + class_index_by_id[recurrence.class_id] = class_index + class_columns["representative_current_id"][class_index] = ( + recurrence.representative_current_id + ) + class_columns["external_labels_sequence_id"][class_index] = ( + self.u32_sequences.add(recurrence.external_labels) + ) + class_columns["source_class"][class_index] = recurrence.source_class + class_columns["class_id_string_id"][class_index] = self.strings.add( + recurrence.class_id + ) + class_columns["proof_digest_string_id"][class_index] = self.strings.add( + recurrence.proof_digest + ) + class_columns["transition_contract_ids_sequence_id"][class_index] = ( + self.u32_sequences.add( + self.strings.add(value) + for value in recurrence.transition_contract_ids + ) + ) + class_columns["member_start"][class_index] = cursor + class_columns["member_count"][class_index] = len(recurrence.members) + for member in recurrence.members: + members["class_index"][cursor] = class_index + members["current_id"][cursor] = member.current_id + members["selector_domain_id"][cursor] = member.selector_domain_id + members["factor_id"][cursor] = self.factors.add( + member.factor, "helicity recurrence factor" + ) + cursor += 1 + self.tables.append(_freeze_table("helicity_recurrence_classes", class_columns)) + self.tables.append(_freeze_table("helicity_recurrence_members", members)) + + source_mappings = () if proof is None else proof.source_state_mappings + source_columns = _allocate( + len(source_mappings), + current_id=_U32, + external_label=_U32, + helicity=_I32, + chirality=_I32, + spin_state_sequence_id=_U32, + declared_state_index=_U32, + selector_domain_id=_U32, + recurrence_class_index=_U32, + representative_current_id=_U32, + source_contract_digest_string_id=_U32, + factor_id=_U32, + ) + for row, mapping in enumerate(source_mappings): + spin_state = ( + mapping.spin_state + if isinstance(mapping.spin_state, tuple) + else (mapping.spin_state,) + ) + source_columns["current_id"][row] = mapping.current_id + source_columns["external_label"][row] = mapping.external_label + source_columns["helicity"][row] = mapping.helicity + source_columns["chirality"][row] = mapping.chirality + source_columns["spin_state_sequence_id"][row] = self.i32_sequences.add( + spin_state + ) + source_columns["declared_state_index"][row] = mapping.declared_state_index + source_columns["selector_domain_id"][row] = mapping.selector_domain_id + source_columns["recurrence_class_index"][row] = class_index_by_id[ + mapping.recurrence_class_id + ] + source_columns["representative_current_id"][row] = ( + mapping.representative_current_id + ) + source_columns["source_contract_digest_string_id"][row] = self.strings.add( + mapping.source_contract_digest + ) + source_columns["factor_id"][row] = self.factors.add( + mapping.factor, "helicity source factor" + ) + self.tables.append(_freeze_table("helicity_source_mappings", source_columns)) + + amplitude_classes = () if proof is None else proof.amplitude_classes + amplitude_class_columns = _allocate( + len(amplitude_classes), + representative_root_id=_U32, + class_id_string_id=_U32, + proof_digest_string_id=_U32, + transition_contract_ids_sequence_id=_U32, + member_start=_U64, + member_count=_U64, + ) + amplitude_member_count = sum(len(item.members) for item in amplitude_classes) + amplitude_members = _allocate( + amplitude_member_count, + class_index=_U32, + root_id=_U32, + selector_domain_start=_U64, + selector_domain_count=_U64, + factor_id=_U32, + ) + amplitude_domain_count = sum( + len(member.selector_domain_ids) + for item in amplitude_classes + for member in item.members + ) + amplitude_domains = _allocate(amplitude_domain_count, selector_domain_id=_U32) + member_cursor = 0 + domain_cursor = 0 + for class_index, recurrence in enumerate(amplitude_classes): + amplitude_class_columns["representative_root_id"][class_index] = ( + recurrence.representative_root_id + ) + amplitude_class_columns["class_id_string_id"][class_index] = ( + self.strings.add(recurrence.class_id) + ) + amplitude_class_columns["proof_digest_string_id"][class_index] = ( + self.strings.add(recurrence.proof_digest) + ) + amplitude_class_columns["transition_contract_ids_sequence_id"][ + class_index + ] = self.u32_sequences.add( + self.strings.add(value) for value in recurrence.transition_contract_ids + ) + amplitude_class_columns["member_start"][class_index] = member_cursor + amplitude_class_columns["member_count"][class_index] = len( + recurrence.members + ) + for member in recurrence.members: + amplitude_members["class_index"][member_cursor] = class_index + amplitude_members["root_id"][member_cursor] = member.root_id + amplitude_members["selector_domain_start"][member_cursor] = ( + domain_cursor + ) + amplitude_members["selector_domain_count"][member_cursor] = len( + member.selector_domain_ids + ) + for domain_id in member.selector_domain_ids: + amplitude_domains["selector_domain_id"][domain_cursor] = domain_id + domain_cursor += 1 + amplitude_members["factor_id"][member_cursor] = self.factors.add( + member.factor, "helicity amplitude factor" + ) + member_cursor += 1 + self.tables.append( + _freeze_table("helicity_amplitude_classes", amplitude_class_columns) + ) + self.tables.append( + _freeze_table("helicity_amplitude_members", amplitude_members) + ) + self.tables.append( + _freeze_table("helicity_amplitude_member_domains", amplitude_domains) + ) + + for name, values, field_name in ( + ( + "helicity_residual_currents", + () if proof is None else proof.residual_current_ids, + "current_id", + ), + ( + "helicity_residual_roots", + () if proof is None else proof.residual_root_ids, + "root_id", + ), + ( + "helicity_structural_zero_domains", + () if proof is None else proof.structural_zero_selector_domain_ids, + "selector_domain_id", + ), + ): + table = _allocate(len(values), **{field_name: _U32}) + table[field_name][:] = values + self.tables.append(_freeze_table(name, table)) + + def _helicity_materialization(self) -> None: + materialization = self.dag.helicity_materialization + metadata = _allocate( + 1, + present=_U8, + strategy_string_id=_U32, + proof_current_count=_U64, + proof_root_count=_U64, + materialized_current_count=_U64, + materialized_root_count=_U64, + ) + metadata["present"][0] = materialization is not None + metadata["strategy_string_id"][0] = ( + MISSING_U32 + if materialization is None + else self.strings.add(materialization.strategy) + ) + metadata["proof_current_count"][0] = ( + 0 if materialization is None else materialization.proof_current_count + ) + metadata["proof_root_count"][0] = ( + 0 if materialization is None else materialization.proof_root_count + ) + metadata["materialized_current_count"][0] = ( + 0 if materialization is None else materialization.materialized_current_count + ) + metadata["materialized_root_count"][0] = ( + 0 if materialization is None else materialization.materialized_root_count + ) + self.tables.append(_freeze_table("helicity_materialization_metadata", metadata)) + + mapping = ( + () + if materialization is None + else materialization.proof_to_materialized_current + ) + mapping_columns = _allocate(len(mapping), materialized_current_id=_U32) + mapping_columns["materialized_current_id"][:] = mapping + self.tables.append( + _freeze_table("helicity_materialized_current_map", mapping_columns) + ) + + source_routes = () if materialization is None else materialization.source_routes + source_columns = _allocate( + len(source_routes), + materialized_current_id=_U32, + external_label=_U32, + helicity=_I32, + chirality=_I32, + spin_state_sequence_id=_U32, + declared_state_index=_U32, + selector_domain_id=_U32, + factor_id=_U32, + ) + for row, route in enumerate(source_routes): + spin_state = ( + route.spin_state + if isinstance(route.spin_state, tuple) + else (route.spin_state,) + ) + source_columns["materialized_current_id"][row] = ( + route.materialized_current_id + ) + source_columns["external_label"][row] = route.external_label + source_columns["helicity"][row] = route.helicity + source_columns["chirality"][row] = route.chirality + source_columns["spin_state_sequence_id"][row] = self.i32_sequences.add( + spin_state + ) + source_columns["declared_state_index"][row] = route.declared_state_index + source_columns["selector_domain_id"][row] = route.selector_domain_id + source_columns["factor_id"][row] = self.factors.add( + route.factor, "materialized source factor" + ) + self.tables.append( + _freeze_table("helicity_materialized_source_routes", source_columns) + ) + + amplitude_routes = ( + () if materialization is None else materialization.amplitude_routes + ) + amplitude_columns = _allocate( + len(amplitude_routes), + materialized_root_id=_U32, + selector_domain_start=_U64, + selector_domain_count=_U64, + factor_id=_U32, + residual=_U8, + ) + domain_count = sum(len(route.selector_domain_ids) for route in amplitude_routes) + domains = _allocate(domain_count, selector_domain_id=_U32) + cursor = 0 + for row, route in enumerate(amplitude_routes): + amplitude_columns["materialized_root_id"][row] = route.materialized_root_id + amplitude_columns["selector_domain_start"][row] = cursor + amplitude_columns["selector_domain_count"][row] = len( + route.selector_domain_ids + ) + for domain_id in route.selector_domain_ids: + domains["selector_domain_id"][cursor] = domain_id + cursor += 1 + amplitude_columns["factor_id"][row] = self.factors.add( + route.factor, "materialized amplitude factor" + ) + amplitude_columns["residual"][row] = route.residual + self.tables.append( + _freeze_table("helicity_materialized_amplitude_routes", amplitude_columns) + ) + self.tables.append( + _freeze_table("helicity_materialized_amplitude_domains", domains) + ) + + schedules = ( + () if materialization is None else materialization.selector_schedules + ) + schedule_columns = _allocate( + len(schedules), + selector_domain_id=_U32, + active_current_sequence_id=_U32, + active_root_sequence_id=_U32, + structural_zero=_U8, + ) + for row, schedule in enumerate(schedules): + schedule_columns["selector_domain_id"][row] = schedule.selector_domain_id + schedule_columns["active_current_sequence_id"][row] = ( + self.u32_sequences.add(schedule.active_current_ids) + ) + schedule_columns["active_root_sequence_id"][row] = self.u32_sequences.add( + schedule.active_root_ids + ) + schedule_columns["structural_zero"][row] = schedule.structural_zero + self.tables.append( + _freeze_table("helicity_materialized_schedules", schedule_columns) + ) + + def _selectors_and_reductions(self) -> None: + selector_records, selector_id_by_vector = self._helicity_selectors() + helicity_selectors = _allocate( + len(selector_records), + values_sequence_id=_U32, + representative_sequence_id=_U32, + coefficient_factor_id=_U32, + computed=_U8, + structural_zero=_U8, + ) + for row, (vector, representative, coefficient, computed, zero) in enumerate( + selector_records + ): + helicity_selectors["values_sequence_id"][row] = self.i32_sequences.add( + vector + ) + helicity_selectors["representative_sequence_id"][row] = ( + self.i32_sequences.add(representative) + ) + helicity_selectors["coefficient_factor_id"][row] = self.factors.add( + (coefficient, 0.0), "helicity selector coefficient" + ) + helicity_selectors["computed"][row] = computed + helicity_selectors["structural_zero"][row] = zero + self.tables.append(_freeze_table("helicity_selectors", helicity_selectors)) + + color_records, color_id_by_word = self._color_selectors() + color_selectors = _allocate( + len(color_records), + word_sequence_id=_U32, + representative_word_sequence_id=_U32, + coefficient_factor_id=_U32, + computed=_U8, + ) + for row, (word, representative, coefficient, computed) in enumerate( + color_records + ): + color_selectors["word_sequence_id"][row] = self.u32_sequences.add(word) + color_selectors["representative_word_sequence_id"][row] = ( + self.u32_sequences.add(representative) + ) + color_selectors["coefficient_factor_id"][row] = self.factors.add( + (coefficient, 0.0), "color selector coefficient" + ) + color_selectors["computed"][row] = computed + self.tables.append(_freeze_table("color_selectors", color_selectors)) + + multiple_lc_sectors = _has_multiple_lc_root_sectors(self.dag) + group_weights: dict[int, tuple[float, float]] = {} + for root in self.dag.amplitude_roots: + group_weights.setdefault( + self.group_ids[root.id], + ( + root.helicity_weight, + _root_all_sector_weight( + self.dag, + root, + has_multiple_lc_root_sectors=multiple_lc_sectors, + ), + ), + ) + coherent_groups = _allocate( + len(self.group_descriptors), + id=_U32, + color_sector_id=_U32, + color_word_sequence_id=_U32, + helicity_values_sequence_id=_U32, + helicity_weight_factor_id=_U32, + all_sector_weight_factor_id=_U32, + ) + reduction_rows: list[tuple[int, int, int]] = [] + for descriptor in self.group_descriptors: + group_id = descriptor.group_id + vector = _descriptor_helicity_vector(self.dag, descriptor.helicity_key) + word = tuple(int(value) for value in descriptor.word) + helicity_weight, all_sector_weight = group_weights[group_id] + coherent_groups["id"][group_id] = group_id + coherent_groups["color_sector_id"][group_id] = descriptor.sector_id + coherent_groups["color_word_sequence_id"][group_id] = ( + self.u32_sequences.add(word) + ) + coherent_groups["helicity_values_sequence_id"][group_id] = ( + self.i32_sequences.add(vector) + ) + coherent_groups["helicity_weight_factor_id"][group_id] = self.factors.add( + (helicity_weight, 0.0), "group helicity weight" + ) + coherent_groups["all_sector_weight_factor_id"][group_id] = self.factors.add( + (all_sector_weight, 0.0), "group all-sector weight" + ) + helicity_members = [vector] + if helicity_weight > 1.0 + 1.0e-12: + flipped = tuple(-value for value in vector) + if flipped != vector: + helicity_members.append(flipped) + color_members = [word] + if ( + word + and all_sector_weight > helicity_weight + 1.0e-12 + and (reflected := (word[0], *reversed(word[1:]))) != word + ): + color_members.append(reflected) + for helicity in helicity_members: + for color in color_members: + reduction_rows.append( + ( + group_id, + selector_id_by_vector[helicity], + ( + color_id_by_word[color] + if self.dag.process.color_accuracy == "lc" + else 0 + ), + ) + ) + self.tables.append(_freeze_table("coherent_groups", coherent_groups)) + reductions = _allocate( + len(reduction_rows), + coherent_group_id=_U32, + helicity_selector_id=_U32, + color_selector_id=_U32, + ) + for row, values in enumerate(reduction_rows): + ( + reductions["coherent_group_id"][row], + reductions["helicity_selector_id"][row], + reductions["color_selector_id"][row], + ) = values + self.tables.append(_freeze_table("reduction_members", reductions)) + + contraction = ( + None + if self.color_topology_replay is not None + else build_color_contraction_plan( + self.dag.color_plan, self.group_descriptors + ) + ) + contraction_metadata = _allocate( + 1, + present=_U8, + color_accuracy_string_id=_U32, + supported=_U8, + reason_string_id=_U32, + group_count=_U64, + includes_color_factor=_U8, + ) + contraction_metadata["present"][0] = contraction is not None + contraction_metadata["color_accuracy_string_id"][0] = ( + MISSING_U32 + if contraction is None + else self.strings.add(contraction.color_accuracy) + ) + contraction_metadata["supported"][0] = ( + False if contraction is None else contraction.supported + ) + contraction_metadata["reason_string_id"][0] = ( + MISSING_U32 + if contraction is None or contraction.reason is None + else self.strings.add(contraction.reason) + ) + contraction_metadata["group_count"][0] = ( + 0 if contraction is None else contraction.group_count + ) + contraction_metadata["includes_color_factor"][0] = ( + False if contraction is None else contraction.includes_color_factor + ) + self.tables.append( + _freeze_table("color_contraction_metadata", contraction_metadata) + ) + entry_count = 0 if contraction is None else contraction.logical_entry_count + contraction_columns = _allocate( + entry_count, + left_group_id=_U32, + right_group_id=_U32, + weight_factor_id=_U32, + symmetry_factor_id=_U32, + ) + logical_entries = ( + () if contraction is None else contraction.iter_logical_entries() + ) + for row, entry in enumerate(logical_entries): + contraction_columns["left_group_id"][row] = entry.left_group_id + contraction_columns["right_group_id"][row] = entry.right_group_id + contraction_columns["weight_factor_id"][row] = self.factors.add( + (entry.weight_re, entry.weight_im), + "color contraction weight", + exact_ir=_color_weight_ir(entry), + ) + contraction_columns["symmetry_factor_id"][row] = self.factors.add( + (entry.symmetry_factor, 0.0), "color contraction symmetry" + ) + self.tables.append( + _freeze_table("color_contraction_entries", contraction_columns) + ) + + def _helicity_selectors( + self, + ) -> tuple[ + tuple[tuple[tuple[int, ...], tuple[int, ...], float, bool, bool], ...], + dict[tuple[int, ...], int], + ]: + represented: dict[tuple[int, ...], tuple[tuple[int, ...], bool, float]] = {} + materialization = self.dag.helicity_materialization + recurrence = self.dag.helicity_recurrence + if materialization is None: + for descriptor in self.group_descriptors: + vector = _descriptor_helicity_vector(self.dag, descriptor.helicity_key) + weight = float(descriptor.helicity_weight) + represented.setdefault(vector, (vector, True, 1.0)) + if weight > 1.0 + 1.0e-12: + if not math.isclose(weight, 2.0, rel_tol=1.0e-12, abs_tol=1.0e-12): + raise EagerColumnarInputError( + f"unsupported helicity reuse weight {weight!r}" + ) + flipped = tuple(-value for value in vector) + represented.setdefault(flipped, (vector, False, 1.0)) + else: + if recurrence is None: + raise EagerColumnarInputError( + "helicity materialization lacks its recurrence proof" + ) + domain_by_id = {domain.id: domain for domain in recurrence.selector_domains} + group_by_root = self.group_ids + representative_by_group = { + descriptor.group_id: _descriptor_helicity_vector( + self.dag, descriptor.helicity_key + ) + for descriptor in self.group_descriptors + } + for route in materialization.amplitude_routes: + group_id = group_by_root[route.materialized_root_id] + representative = representative_by_group[group_id] + coefficient = route.factor[0] ** 2 + route.factor[1] ** 2 + for domain_id in route.selector_domain_ids: + domain = domain_by_id[domain_id] + by_label = dict(domain.source_states) + vector = tuple( + by_label.get(int(leg.label), 0) for leg in self.dag.process.legs + ) + candidate = ( + representative, + vector == representative, + coefficient, + ) + previous = represented.setdefault(vector, candidate) + if previous != candidate: + raise EagerColumnarInputError( + f"inconsistent materialized helicity route {vector}" + ) + + possible = _possible_helicity_vectors(self.dag, self.model) + rows = [] + for vector in sorted(possible): + if vector in represented: + representative, computed, coefficient = represented[vector] + rows.append((vector, representative, coefficient, computed, False)) + else: + rows.append((vector, vector, 0.0, False, True)) + result = tuple(rows) + return result, {row[0]: index for index, row in enumerate(result)} + + def _color_selectors( + self, + ) -> tuple[ + tuple[tuple[tuple[int, ...], tuple[int, ...], float, bool], ...], + dict[tuple[int, ...], int], + ]: + if self.dag.process.color_accuracy != "lc": + records = (((), (), 1.0, True),) + return records, {(): 0} + replay = self.dag.lc_topology_replay + materialized = ( + set(replay.materialized_sector_ids) if replay is not None else None + ) + records: dict[tuple[int, ...], tuple[tuple[int, ...], float, bool]] = {} + for sector in self.dag.color_plan.sectors: + word = tuple(sector.word_labels or sector.color_words[0]) + representative = sector + if replay is not None: + representative = self.dag.color_plan.sector( + replay.representative_for(sector.id) + ) + if representative is None: + raise EagerColumnarInputError( + f"missing replay representative for sector {sector.id}" + ) + representative_word = tuple( + representative.word_labels or representative.color_words[0] + ) + records.setdefault( + word, + ( + representative_word, + 1.0, + materialized is None or sector.id in materialized, + ), + ) + if ( + self.dag.color_plan.trace_reflections_folded + and sector.kind == "single-trace" + and len(word) > 2 + and (reflected := (word[0], *reversed(word[1:]))) != word + ): + records.setdefault(reflected, (representative_word, 1.0, False)) + rows = tuple((word, *records[word]) for word in sorted(records)) + return rows, {row[0]: index for index, row in enumerate(rows)} + + def _coupling( + self, + kind: int, + particles: tuple[int, int, int], + coupling: tuple[float, float], + ) -> int: + key = (int(kind), tuple(int(value) for value in particles), tuple(coupling)) + existing = self.coupling_ids.get(key) + if existing is not None: + return existing + names = runtime_coupling_parameter_names( + kind, particles, coupling, model=self.model + ) + exact_ir = ( + None + if not any(name is not None for name in names) + else { + "abi": "pyamplicol-runtime-coupling-v1", + "kind": "model-parameter-components", + "components": [ + ( + {"kind": "binary64-constant", "component": component} + if name is None + else {"kind": "model-parameter", "name": str(name)} + ) + for component, name in enumerate(names) + ], + } + ) + name_sequence_id = self.u32_sequences.add( + MISSING_U32 if name is None else self.strings.add(name) for name in names + ) + result = _checked_u32(len(self.coupling_specs), "coupling id") + self.coupling_specs.append( + ( + _checked_i32(kind, "coupling vertex kind"), + tuple(_checked_i32(value, "coupling particle") for value in particles), + self.factors.add( + coupling, + "coupling constant", + exact_ir=exact_ir, + ), + name_sequence_id, + ) + ) + self.coupling_ids[key] = result + return result + + def _couplings(self) -> None: + columns = _allocate( + len(self.coupling_specs), + vertex_kind=_I32, + vertex_particles=(_I32, (3,)), + constant_factor_id=_U32, + parameter_name_ids_sequence_id=_U32, + ) + for row, (kind, particles, factor_id, names_id) in enumerate( + self.coupling_specs + ): + columns["vertex_kind"][row] = kind + columns["vertex_particles"][row] = particles + columns["constant_factor_id"][row] = factor_id + columns["parameter_name_ids_sequence_id"][row] = names_id + self.tables.append(_freeze_table("couplings", columns)) + + def _model_parameters(self) -> None: + specs = _model_parameter_specs( + self.dag, + self.model, + tuple( + ( + kind, + particles, + self.factors.pair(factor_id), + tuple( + None if name_id == MISSING_U32 else self.strings.values[name_id] + for name_id in self.u32_sequences.values[names_id] + ), + ) + for kind, particles, factor_id, names_id in self.coupling_specs + ), + ) + columns = _allocate( + len(specs), + name_string_id=_U32, + kind_string_id=_U32, + default_value=_F64, + default_factor_id=_U32, + runtime_name_string_id=_U32, + complex_component=_I32, + parameter_type_string_id=_U32, + pdg=_I32, + vertex_kind=_I32, + vertex_particles=(_I32, (3,)), + coupling_component=_I32, + derived=_U8, + complex_domain_string_id=_U32, + definition_string_id=_U32, + ) + for row, spec in enumerate(specs): + columns["name_string_id"][row] = self.strings.add(spec.name) + columns["kind_string_id"][row] = self.strings.add(spec.kind) + columns["default_value"][row] = _finite(spec.default, "parameter default") + exact_ir = ( + None + if not spec.derived or spec.definition is None + else { + "abi": "pyamplicol-derived-parameter-v1", + "kind": "canonical-expression-component", + "name": spec.runtime_name or spec.name, + "component": spec.complex_component, + "expression": spec.definition, + "domain": spec.complex_domain, + } + ) + columns["default_factor_id"][row] = self.factors.add( + (spec.default, 0.0), + "model parameter default", + exact_ir=exact_ir, + ) + columns["runtime_name_string_id"][row] = self.strings.optional( + spec.runtime_name + ) + columns["complex_component"][row] = spec.complex_component + columns["parameter_type_string_id"][row] = self.strings.optional( + spec.parameter_type + ) + columns["pdg"][row] = MISSING_I32 if spec.pdg is None else spec.pdg + columns["vertex_kind"][row] = ( + MISSING_I32 if spec.vertex_kind is None else spec.vertex_kind + ) + columns["vertex_particles"][row] = ( + (MISSING_I32, MISSING_I32, MISSING_I32) + if spec.vertex_particles is None + else spec.vertex_particles + ) + columns["coupling_component"][row] = ( + MISSING_I32 + if spec.coupling_component is None + else spec.coupling_component + ) + columns["derived"][row] = spec.derived + columns["complex_domain_string_id"][row] = self.strings.optional( + spec.complex_domain + ) + columns["definition_string_id"][row] = self.strings.optional( + spec.definition + ) + self.tables.append(_freeze_table("model_parameters", columns)) + + +def build_eager_lowering_input_v1( + *, + dag: GenericDAG, + model: Model, + resolver: EagerKernelResolver, + process_id: str | None = None, +) -> EagerLoweringInputV1: + """Build the v1 lowering input without constructing a runtime schema.""" + + return _Builder(dag, model, resolver, process_id).build() + + +def runtime_model_parameter_records_from_eager_input( + lowering_input: EagerLoweringInputV1, +) -> tuple[dict[str, object], ...]: + """Project the authoritative eager parameter columns into physics records.""" + + table = lowering_input.table("model_parameters") + names = table.column("name_string_id") + kinds = table.column("kind_string_id") + defaults = table.column("default_value") + runtime_names = table.column("runtime_name_string_id") + components = table.column("complex_component") + records: list[dict[str, object]] = [] + for row in range(table.row_count): + runtime_name_id = int(runtime_names[row]) + component = int(components[row]) + record: dict[str, object] = { + "name": lowering_input.string_catalog[int(names[row])], + "kind": lowering_input.string_catalog[int(kinds[row])], + "parameter_index": row, + "default": float(defaults[row]), + } + if runtime_name_id != MISSING_U32: + record["runtime_name"] = lowering_input.string_catalog[runtime_name_id] + if component == 0: + record["complex_component"] = "real" + elif component == 1: + record["complex_component"] = "imag" + elif component != -1: + raise EagerColumnarInputError( + f"model parameter {row} has invalid complex component {component}" + ) + records.append(record) + return tuple(records) + + +def _model_parameter_specs( + dag: GenericDAG, + model: Model, + couplings: Sequence[ + tuple[ + int, + tuple[int, int, int], + tuple[float, float], + tuple[str | None, ...], + ] + ], +) -> tuple[_ModelParameterSpec, ...]: + result: list[_ModelParameterSpec] = [] + seen: set[str] = set() + + def add(spec: _ModelParameterSpec) -> None: + if spec.name in seen: + return + seen.add(spec.name) + result.append(spec) + + def add_complex( + name: str, + value: object, + *, + kind: str, + derived: bool = False, + domain: str | None = None, + definition: str | None = None, + ) -> None: + pair = _complex_default(value, name) + for component, default in enumerate(pair): + add( + _ModelParameterSpec( + name=f"{name}.{'real' if component == 0 else 'imag'}", + kind=kind, + default=default, + runtime_name=name, + complex_component=component, + derived=derived, + complex_domain=domain, + definition=definition, + ) + ) + + for name, value in sorted(model.runtime_normalization_parameter_defaults().items()): + add( + _ModelParameterSpec( + name=str(name), kind="normalization", default=float(value) + ) + ) + + defaults_provider = getattr(model, "runtime_parameter_defaults", None) + if callable(defaults_provider): + type_provider = getattr(model, "runtime_parameter_type", None) + for raw_name, value in sorted(defaults_provider().items()): + name = str(raw_name) + declared = ( + str(type_provider(name)).lower() + if callable(type_provider) + else "complex" + ) + if declared == "complex": + add_complex(name, value, kind="external_parameter_component") + else: + real, imaginary = _complex_default(value, name) + if imaginary != 0.0: + raise EagerColumnarInputError( + f"real model parameter {name!r} has imaginary default" + ) + add( + _ModelParameterSpec( + name=name, + kind="external_parameter", + default=real, + parameter_type=declared, + ) + ) + used = {name for *_prefix, names in couplings for name in names if name} + particle_names = getattr(model, "runtime_parameter_names_for_particle", None) + if callable(particle_names): + for particle_id in sorted( + {current.index.particle_id for current in dag.currents} + ): + used.update(str(name) for name in particle_names(particle_id)) + derived_defaults = getattr( + model, "runtime_derived_parameter_defaults_for", None + ) + if callable(derived_defaults): + names = tuple(sorted(used)) + values = derived_defaults(names) + domains_provider = getattr( + model, "runtime_derived_parameter_domains_for", None + ) + domains = domains_provider(names) if callable(domains_provider) else {} + definitions_provider = getattr( + model, "runtime_derived_parameter_definitions_for", None + ) + definitions = ( + definitions_provider(names) if callable(definitions_provider) else {} + ) + for raw_name, value in sorted(values.items()): + name = str(raw_name) + add_complex( + name, + value, + kind="derived_parameter_component", + derived=True, + domain=str(domains.get(raw_name, domains.get(name, "complex"))), + definition=( + None + if definitions.get(raw_name, definitions.get(name)) is None + else str(definitions.get(raw_name, definitions.get(name))) + ), + ) + return tuple(result) + + for particle in sorted(model.particles.values(), key=lambda item: item.pdg): + if float(particle.mass) != 0.0: + add( + _ModelParameterSpec( + name=f"particle.{particle.pdg}.mass", + kind="particle_mass", + default=float(particle.mass), + pdg=particle.pdg, + ) + ) + if float(particle.width) != 0.0: + add( + _ModelParameterSpec( + name=f"particle.{particle.pdg}.width", + kind="particle_width", + default=float(particle.width), + pdg=particle.pdg, + ) + ) + for kind, particles, coupling, names in sorted(couplings): + for component, name in enumerate(names): + if name is None: + continue + add( + _ModelParameterSpec( + name=name, + kind="coupling_component", + default=float(coupling[component]), + vertex_kind=kind, + vertex_particles=particles, + coupling_component=component, + ) + ) + return tuple(result) + + +def _possible_helicity_vectors( + dag: GenericDAG, model: Model +) -> tuple[tuple[int, ...], ...]: + selected = dict(dag.selected_source_helicities) + per_leg: list[tuple[int, ...]] = [] + for leg in dag.process.legs: + if leg.outgoing_pdg is None: + per_leg.append((0,)) + continue + source_ir = model._source_ir(int(leg.outgoing_pdg)) + values = { + int((source_ir.crossing.apply(state) if leg.is_initial else state).helicity) + for state in source_ir.states + } + if leg.label in selected: + values.intersection_update((selected[leg.label],)) + if not values: + raise EagerColumnarInputError( + f"selected helicity is unavailable for leg {leg.label}" + ) + per_leg.append(tuple(sorted(values))) + return tuple(tuple(values) for values in product(*per_leg)) + + +def _descriptor_helicity_vector( + dag: GenericDAG, helicity_key: tuple[object, ...] +) -> tuple[int, ...]: + by_label: dict[int, int] = {} + for item in helicity_key: + if isinstance(item, tuple) and len(item) >= 5: + by_label[int(item[0])] = int(item[4]) + return tuple(by_label.get(leg.label, 0) for leg in dag.process.legs) + + +def _fill_resolved_kernel( + columns: Mapping[str, np.ndarray[Any, Any]], + row: int, + resolved: EagerResolvedKernel, + factors: _FactorCatalog, +) -> None: + columns["kernel_id"][row] = _checked_u32(resolved.kernel_id, "prepared kernel id") + columns["canonical_input_order"][row] = resolved.canonical_input_order + columns["kernel_normalization_factor_id"][row] = factors.add( + resolved.normalization_factor, "prepared kernel normalization" + ) + columns["output_factor_source"][row] = _checked_u8( + resolved.output_factor_source, "prepared output factor source" + ) + + +def _allocate( + row_count: int, + **specifications: np.dtype[Any] | tuple[np.dtype[Any], tuple[int, ...]], +) -> dict[str, np.ndarray[Any, Any]]: + _checked_u64(row_count, "table row count") + result: dict[str, np.ndarray[Any, Any]] = {} + for name, specification in specifications.items(): + if isinstance(specification, tuple): + dtype, tail_shape = specification + else: + dtype, tail_shape = specification, () + result[name] = np.empty((row_count, *tail_shape), dtype=dtype, order="C") + return result + + +def _freeze_table( + name: str, + columns: Mapping[str, np.ndarray[Any, Any]], +) -> EagerColumnarTable: + row_counts = {len(values) for values in columns.values()} + row_count = row_counts.pop() if row_counts else 0 + if row_counts: + raise EagerColumnarInputError(f"table {name!r} columns have unequal lengths") + frozen: list[EagerColumn] = [] + for column_name, values in columns.items(): + values.flags.writeable = False + frozen.append(EagerColumn(column_name, values)) + return EagerColumnarTable(name, row_count, tuple(frozen)) + + +def _validate_catalog(values: tuple[str, ...], context: str) -> None: + if any(not isinstance(value, str) for value in values): + raise TypeError(f"{context} catalog must contain strings") + if len(set(values)) != len(values): + raise EagerColumnarInputError(f"{context} catalog contains duplicates") + + +def _validate_dense_ids(table: EagerColumnarTable, column_name: str) -> None: + values = table.column(column_name) + expected = np.arange(table.row_count, dtype=_U32) + if not np.array_equal(values, expected): + raise EagerColumnarInputError(f"table {table.name!r} ids are not dense") + + +def _validate_bounded_columns( + table: EagerColumnarTable, + columns: Sequence[str], + bound: int, +) -> None: + for column_name in columns: + values = table.column(column_name) + if values.size and int(values.max()) >= bound: + raise EagerColumnarInputError( + f"{table.name}.{column_name} references an absent row" + ) + + +def _validate_optional_bounded_columns( + table: EagerColumnarTable, + columns: Sequence[str], + bound: int, +) -> None: + for column_name in columns: + values = table.column(column_name) + present = values[values != MISSING_U32] + if present.size and int(present.max()) >= bound: + raise EagerColumnarInputError( + f"{table.name}.{column_name} references an absent row" + ) + + +def _require_index(value: int, bound: int, context: str) -> int: + result = _checked_u32(value, context) + if result >= bound: + raise EagerColumnarInputError(f"{context} {result} is outside [0, {bound})") + return result + + +def _checked_u8(value: int, context: str) -> int: + if isinstance(value, bool) or not isinstance(value, int) or not 0 <= value < 256: + raise EagerColumnarInputError(f"{context} does not fit u8: {value!r}") + return value + + +def _checked_u32(value: int, context: str) -> int: + if ( + isinstance(value, bool) + or not isinstance(value, int) + or not 0 <= value <= MISSING_U32 + ): + raise EagerColumnarInputError(f"{context} does not fit u32: {value!r}") + return value + + +def _positive_u32(value: int, context: str) -> int: + result = _checked_u32(value, context) + if result == 0: + raise EagerColumnarInputError(f"{context} must be positive") + return result + + +def _checked_u64(value: int, context: str) -> int: + if ( + isinstance(value, bool) + or not isinstance(value, int) + or not 0 <= value < 1 << 64 + ): + raise EagerColumnarInputError(f"{context} does not fit u64: {value!r}") + return value + + +def _checked_i32(value: int, context: str) -> int: + if ( + isinstance(value, bool) + or not isinstance(value, int) + or not -(1 << 31) <= value < 1 << 31 + ): + raise EagerColumnarInputError(f"{context} does not fit i32: {value!r}") + return value + + +def _finite(value: float, context: str) -> float: + result = float(value) + if not math.isfinite(result): + raise EagerColumnarInputError(f"{context} must be finite") + return result + + +def _complex_default(value: object, context: str) -> tuple[float, float]: + if isinstance(value, complex): + pair = (float(value.real), float(value.imag)) + elif isinstance(value, Sequence) and not isinstance(value, str | bytes): + if len(value) != 2: + raise EagerColumnarInputError(f"{context} must be a complex pair") + pair = (float(value[0]), float(value[1])) + else: + pair = (float(value), 0.0) + if not all(math.isfinite(component) for component in pair): + raise EagerColumnarInputError(f"{context} must be finite") + return pair + + +def _f64_bits(value: float) -> int: + """Return the exact little-endian IEEE-754 payload for one finite f64.""" + + return int.from_bytes(struct.pack(" dict[str, object]: + """Canonical exact representation of a complex value sourced as f64.""" + + return { + "abi": "pyamplicol-exact-factor-v1", + "kind": "complex-ieee754-binary64", + "real_bits": f"{_f64_bits(value[0]):016x}", + "imaginary_bits": f"{_f64_bits(value[1]):016x}", + } + + +def _color_weight_ir( + entry: ColorContractionEntry | ColorContractionTemplateEntry, +) -> dict[str, object] | None: + if entry.exact_weight is None: + return None + return {"kind": "rational-complex", "value": entry.to_json_dict()["exact_weight"]} + + +def _hash_text(digest: Any, value: str) -> None: + encoded = value.encode("utf-8") + digest.update(len(encoded).to_bytes(8, "little")) + digest.update(encoded) + + +__all__ = [ + "EAGER_LOWERING_INPUT_ABI", + "EAGER_LOWERING_INPUT_SCHEMA_SHA256", + "FACTOR_EXACT_SOURCE_BINARY64", + "FACTOR_EXACT_SOURCE_CANONICAL_IR", + "EagerColumn", + "EagerColumnarInputError", + "EagerColumnarTable", + "EagerLoweringInputV1", + "build_eager_lowering_input_v1", + "runtime_model_parameter_records_from_eager_input", +] diff --git a/pyamplicol/src/pyamplicol/generation/eager_lowering.py b/pyamplicol/src/pyamplicol/generation/eager_lowering.py new file mode 100644 index 0000000000..61ccfaba7b --- /dev/null +++ b/pyamplicol/src/pyamplicol/generation/eager_lowering.py @@ -0,0 +1,1515 @@ +# SPDX-License-Identifier: 0BSD +"""Lower proven process DAGs into backend-independent eager execution tables.""" + +from __future__ import annotations + +from collections.abc import Callable, Iterator, Mapping, Sequence +from dataclasses import dataclass +from typing import Protocol + +from ..models.base import Model +from ..models.prepared_catalog import ( + ClosureKernelKey, + PropagatorKernelKey, + VertexKernelKey, +) +from .contracts import runtime_coupling_parameter_names +from .dag_types import CurrentNode, GenericDAG, InteractionNode +from .eager_tables import ( + EAGER_OUTPUT_FACTOR_COUPLING_IMAG, + EAGER_OUTPUT_FACTOR_COUPLING_REAL, + EAGER_OUTPUT_FACTOR_NONE, + EAGER_PLAN_ABI, + EAGER_RUNTIME_CAPABILITY, + EAGER_SELECTOR_DOMAINS_ABI, + MISSING_U32, + EagerAttachmentRow, + EagerClosureRow, + EagerCouplingRow, + EagerFinalizationRow, + EagerInvocationRow, + EagerSelectorDomainIdRow, + EagerSelectorDomainRow, + EagerSelectorGroupRow, + pack_rows, +) +from .runtime_schema import RuntimeSchemaLayout, build_runtime_schema_layout + +EAGER_RUNTIME_KIND = "pyamplicol-runtime-eager-execution" +EagerProgressCallback = Callable[[Mapping[str, str | int]], None] + +# Dense Python integer masks are substantially cheaper than materialized +# membership tuples for the selector universes seen in complete LC artifacts. +# Keep the sparse interning path for unusually large universes, where retaining +# one arbitrary-width integer per distinct domain would dominate memory. +_DENSE_SELECTOR_DOMAIN_GROUP_LIMIT = 4096 + + +class EagerKernelResolver(Protocol): + """Resolve model-local prepared kernels without compiling them.""" + + def vertex_kernel(self, interaction: InteractionNode) -> EagerResolvedKernel: ... + + def propagator_kernel_id( + self, + current: CurrentNode, + propagator: Mapping[str, object], + ) -> int | None: ... + + def closure_kernel( + self, root: Mapping[str, object] + ) -> EagerResolvedKernel | None: ... + + +@dataclass(frozen=True, slots=True) +class EagerResolvedKernel: + """Prepared callable plus the exact transformation into its canonical ABI.""" + + kernel_id: int + canonical_input_order: tuple[int, int] = (0, 1) + normalization_factor: tuple[float, float] = (1.0, 0.0) + output_factor_source: int = EAGER_OUTPUT_FACTOR_NONE + + def __post_init__(self) -> None: + if self.kernel_id < 0: + raise ValueError("prepared kernel ID must be nonnegative") + if self.canonical_input_order not in ((0, 1), (1, 0)): + raise ValueError("prepared binary input order must be a permutation") + if complex(*self.normalization_factor) == 0j: + raise ValueError("prepared kernel normalization factor must be nonzero") + if self.output_factor_source not in ( + EAGER_OUTPUT_FACTOR_NONE, + EAGER_OUTPUT_FACTOR_COUPLING_REAL, + EAGER_OUTPUT_FACTOR_COUPLING_IMAG, + ): + raise ValueError("unsupported prepared output factor source") + + +@dataclass(frozen=True, slots=True) +class MappingEagerKernelResolver: + """Small immutable resolver used by prepared kernel-pack manifests.""" + + vertex_kernels: Mapping[int, int] + propagator_kernels: Mapping[tuple[int, int], int] + closure_kernels: Mapping[tuple[str, int | None], int] + + def vertex_kernel(self, interaction: InteractionNode) -> EagerResolvedKernel: + try: + return EagerResolvedKernel( + int(self.vertex_kernels[interaction.vertex_kind]) + ) + except KeyError as error: + raise ValueError( + f"prepared model has no vertex kernel for kind " + f"{interaction.vertex_kind}" + ) from error + + def propagator_kernel_id( + self, + current: CurrentNode, + propagator: Mapping[str, object], + ) -> int | None: + if not bool(propagator.get("applies_propagator", False)): + return None + key = (int(current.index.particle_id), int(current.index.chirality)) + try: + return int(self.propagator_kernels[key]) + except KeyError as error: + raise ValueError( + f"prepared model has no propagator kernel for particle/chirality {key}" + ) from error + + def closure_kernel(self, root: Mapping[str, object]) -> EagerResolvedKernel | None: + kind = str(root.get("kind")) + vertex_kind = root.get("vertex_kind") + key = (kind, None if vertex_kind is None else int(vertex_kind)) + if kind == "direct-contraction": + return None + try: + return EagerResolvedKernel(int(self.closure_kernels[key])) + except KeyError as error: + raise ValueError( + f"prepared model has no closure kernel for {key}" + ) from error + + +@dataclass(frozen=True, slots=True) +class PreparedCatalogEagerKernelIndex: + """Model-local resolver index parsed once while loading a prepared pack.""" + + vertices: Mapping[VertexKernelKey, EagerResolvedKernel] + propagators: Mapping[PropagatorKernelKey, int | None] + closures: Mapping[ClosureKernelKey, EagerResolvedKernel] + + @classmethod + def from_manifest( + cls, + manifest: Mapping[str, object], + ) -> PreparedCatalogEagerKernelIndex: + if manifest.get("abi") != "pyamplicol-prepared-kernel-catalog-v1": + raise ValueError("prepared model has an incompatible resolver ABI") + vertices: dict[VertexKernelKey, EagerResolvedKernel] = {} + propagators: dict[PropagatorKernelKey, int | None] = {} + closures: dict[ClosureKernelKey, EagerResolvedKernel] = {} + for raw in _mapping_sequence( + manifest.get("vertex_bindings"), "resolver.vertex_bindings" + ): + key = _vertex_kernel_key(_mapping(raw.get("key"), "resolver.vertex.key")) + vertices[key] = _resolved_kernel(raw, "resolver.vertex") + for raw in _mapping_sequence( + manifest.get("propagator_bindings"), "resolver.propagator_bindings" + ): + key_data = _mapping(raw.get("key"), "resolver.propagator.key") + key = PropagatorKernelKey( + int(key_data["particle_id"]), int(key_data["chirality"]) + ) + kernel_id = raw.get("kernel_id") + propagators[key] = None if kernel_id is None else int(kernel_id) + for raw in _mapping_sequence( + manifest.get("closure_bindings"), "resolver.closure_bindings" + ): + key = _closure_kernel_key(_mapping(raw.get("key"), "resolver.closure.key")) + closures[key] = _resolved_kernel(raw, "resolver.closure") + return cls(vertices=vertices, propagators=propagators, closures=closures) + + +class PreparedCatalogEagerKernelResolver: + """Bind one process DAG to a validated prepared-model resolver index.""" + + def __init__( + self, + dag: GenericDAG, + manifest: Mapping[str, object] | PreparedCatalogEagerKernelIndex, + ) -> None: + index = ( + manifest + if isinstance(manifest, PreparedCatalogEagerKernelIndex) + else PreparedCatalogEagerKernelIndex.from_manifest(manifest) + ) + self._dag = dag + self._vertices = index.vertices + self._propagators = index.propagators + self._closures = index.closures + + def vertex_kernel(self, interaction: InteractionNode) -> EagerResolvedKernel: + left = self._dag.currents[interaction.left_id] + right = self._dag.currents[interaction.right_id] + result = self._dag.currents[interaction.result_id] + key = VertexKernelKey( + kind=int(interaction.vertex_kind), + particles=tuple(int(value) for value in interaction.vertex_particles), + left_chirality=int(left.index.chirality), + right_chirality=int(right.index.chirality), + result_chirality=int(result.index.chirality), + coupling=tuple(float(value) for value in interaction.coupling), + ) + try: + return self._vertices[key] + except KeyError as error: + raise ValueError( + f"prepared model has no vertex binding for {key}" + ) from error + + def propagator_kernel_id( + self, + current: CurrentNode, + propagator: Mapping[str, object], + ) -> int | None: + if not bool(propagator.get("applies_propagator", False)): + return None + key = PropagatorKernelKey( + int(current.index.particle_id), int(current.index.chirality) + ) + try: + kernel_id = self._propagators[key] + except KeyError as error: + raise ValueError( + f"prepared model has no propagator binding for {key}" + ) from error + if kernel_id is None: + raise ValueError( + f"prepared propagator binding {key} does not provide a kernel" + ) + return kernel_id + + def closure_kernel(self, root: Mapping[str, object]) -> EagerResolvedKernel | None: + if str(root.get("kind")) == "direct-contraction": + return None + kind = root.get("vertex_kind") + particles = root.get("vertex_particles") + if kind is None or particles is None: + raise ValueError("prepared vertex closure lacks vertex identity") + left = self._dag.currents[int(root["left_current_id"])] + right = self._dag.currents[int(root["right_current_id"])] + key = ClosureKernelKey( + kind=int(kind), + particles=tuple( + int(value) for value in _sequence(particles, "closure.vertex_particles") + ), + left_chirality=int(left.index.chirality), + right_chirality=int(right.index.chirality), + coupling=tuple( + float(value) + for value in _sequence(root.get("coupling"), "closure.coupling") + ), + ) + try: + return self._closures[key] + except KeyError as error: + raise ValueError( + f"prepared model has no closure binding for {key}" + ) from error + + +@dataclass(frozen=True, slots=True) +class EagerStageTables: + stage_index: int + subset_size: int + invocations: tuple[EagerInvocationRow, ...] + attachments: tuple[EagerAttachmentRow, ...] + finalizations: tuple[EagerFinalizationRow, ...] + + def __post_init__(self) -> None: + cursor = 0 + for invocation in self.invocations: + if invocation.attachment_start != cursor: + raise ValueError("eager attachment ranges must be contiguous") + cursor += invocation.attachment_count + if cursor != len(self.attachments): + raise ValueError("eager attachment ranges must cover the attachment table") + + +@dataclass(frozen=True, slots=True) +class EagerStageSelectorDomains: + """Domain references aligned one-for-one with one eager execution stage.""" + + stage_index: int + invocation_domains: tuple[EagerSelectorDomainIdRow, ...] + attachment_domains: tuple[EagerSelectorDomainIdRow, ...] + unpropagated_finalization_domains: tuple[EagerSelectorDomainIdRow, ...] + propagated_finalization_domains: tuple[EagerSelectorDomainIdRow, ...] + + +@dataclass(frozen=True, slots=True) +class EagerSelectorClosureTables: + """Interned coherent-group dependency domains for selector pruning.""" + + domains: tuple[EagerSelectorDomainRow, ...] + domain_group_ids: tuple[EagerSelectorGroupRow, ...] + stages: tuple[EagerStageSelectorDomains, ...] + closure_domains: tuple[EagerSelectorDomainIdRow, ...] + + def __post_init__(self) -> None: + cursor = 0 + for domain_id, domain in enumerate(self.domains): + if domain.member_start != cursor: + raise ValueError( + f"eager selector domain {domain_id} does not start at {cursor}" + ) + stop = cursor + domain.member_count + if stop > len(self.domain_group_ids): + raise ValueError( + f"eager selector domain {domain_id} exceeds its membership table" + ) + previous = -1 + for member_index in range(cursor, stop): + member = self.domain_group_ids[member_index].coherent_group_id + if member <= previous: + raise ValueError( + f"eager selector domain {domain_id} members must be unique " + "and sorted" + ) + previous = member + cursor = stop + if cursor != len(self.domain_group_ids): + raise ValueError( + "eager selector domains do not cover their membership table" + ) + + domain_count = len(self.domains) + for reference in self._domain_references(): + if reference.domain_id >= domain_count: + raise ValueError( + f"eager selector domain ID {reference.domain_id} is out of range" + ) + + def _domain_references(self) -> Iterator[EagerSelectorDomainIdRow]: + yield from self.closure_domains + for stage in self.stages: + for references in ( + stage.invocation_domains, + stage.attachment_domains, + stage.unpropagated_finalization_domains, + stage.propagated_finalization_domains, + ): + yield from references + + def binary_payloads(self, *, prefix: str) -> dict[str, bytes]: + payloads = { + f"{prefix}/selector-domains.bin": pack_rows(self.domains), + f"{prefix}/selector-domain-group-ids.bin": pack_rows(self.domain_group_ids), + f"{prefix}/closure-domains.bin": pack_rows(self.closure_domains), + } + for stage in self.stages: + base = f"{prefix}/stage-{stage.stage_index}" + payloads[f"{base}-invocation-domains.bin"] = pack_rows( + stage.invocation_domains + ) + payloads[f"{base}-attachment-domains.bin"] = pack_rows( + stage.attachment_domains + ) + payloads[f"{base}-unpropagated-finalization-domains.bin"] = pack_rows( + stage.unpropagated_finalization_domains + ) + payloads[f"{base}-propagated-finalization-domains.bin"] = pack_rows( + stage.propagated_finalization_domains + ) + return payloads + + def to_metadata(self, *, prefix: str) -> dict[str, object]: + def table(path: str, count: int, row_size: int) -> dict[str, object]: + return {"path": path, "count": count, "row_size": row_size} + + return { + "abi": EAGER_SELECTOR_DOMAINS_ABI, + "domains": table( + f"{prefix}/selector-domains.bin", + len(self.domains), + EagerSelectorDomainRow._STRUCT.size, + ), + "domain_group_ids": table( + f"{prefix}/selector-domain-group-ids.bin", + len(self.domain_group_ids), + EagerSelectorGroupRow._STRUCT.size, + ), + "stages": [ + { + "stage_index": stage.stage_index, + "invocation_domains": table( + f"{prefix}/stage-{stage.stage_index}-invocation-domains.bin", + len(stage.invocation_domains), + EagerSelectorDomainIdRow._STRUCT.size, + ), + "attachment_domains": table( + f"{prefix}/stage-{stage.stage_index}-attachment-domains.bin", + len(stage.attachment_domains), + EagerSelectorDomainIdRow._STRUCT.size, + ), + "unpropagated_finalization_domains": table( + f"{prefix}/stage-{stage.stage_index}" + "-unpropagated-finalization-domains.bin", + len(stage.unpropagated_finalization_domains), + EagerSelectorDomainIdRow._STRUCT.size, + ), + "propagated_finalization_domains": table( + f"{prefix}/stage-{stage.stage_index}" + "-propagated-finalization-domains.bin", + len(stage.propagated_finalization_domains), + EagerSelectorDomainIdRow._STRUCT.size, + ), + } + for stage in self.stages + ], + "closure_domains": table( + f"{prefix}/closure-domains.bin", + len(self.closure_domains), + EagerSelectorDomainIdRow._STRUCT.size, + ), + } + + +@dataclass(frozen=True, slots=True) +class EagerExecutionTables: + process_key: str + couplings: tuple[EagerCouplingRow, ...] + stages: tuple[EagerStageTables, ...] + closures: tuple[EagerClosureRow, ...] + selector_closures: EagerSelectorClosureTables | None = None + + def __post_init__(self) -> None: + selector_closures = self.selector_closures + if selector_closures is None: + return + if len(selector_closures.stages) != len(self.stages): + raise ValueError("eager selector domains do not cover every stage") + for stage, domains in zip( + self.stages, + selector_closures.stages, + strict=True, + ): + if domains.stage_index != stage.stage_index: + raise ValueError("eager selector-domain stage index mismatch") + expected_counts = ( + len(stage.invocations), + len(stage.attachments), + len(stage.finalizations), + len(stage.finalizations), + ) + actual_counts = ( + len(domains.invocation_domains), + len(domains.attachment_domains), + len(domains.unpropagated_finalization_domains), + len(domains.propagated_finalization_domains), + ) + if actual_counts != expected_counts: + raise ValueError( + f"eager selector-domain row counts do not match stage " + f"{stage.stage_index}" + ) + if len(selector_closures.closure_domains) != len(self.closures): + raise ValueError("eager selector domains do not cover every closure") + + @property + def invocation_count(self) -> int: + return sum(len(stage.invocations) for stage in self.stages) + + @property + def attachment_count(self) -> int: + return sum(len(stage.attachments) for stage in self.stages) + + @property + def referenced_kernel_ids(self) -> frozenset[int]: + return frozenset( + { + *(row.kernel_id for stage in self.stages for row in stage.invocations), + *( + row.kernel_id + for stage in self.stages + for row in stage.finalizations + if row.kernel_id != MISSING_U32 + ), + *( + row.kernel_id + for row in self.closures + if row.kernel_id != MISSING_U32 + ), + } + ) + + def binary_payloads(self, *, prefix: str = "eager") -> dict[str, bytes]: + payloads = {f"{prefix}/couplings.bin": pack_rows(self.couplings)} + for stage in self.stages: + base = f"{prefix}/stage-{stage.stage_index}" + payloads[f"{base}-invocations.bin"] = pack_rows(stage.invocations) + payloads[f"{base}-attachments.bin"] = pack_rows(stage.attachments) + payloads[f"{base}-finalizations.bin"] = pack_rows(stage.finalizations) + payloads[f"{prefix}/closures.bin"] = pack_rows(self.closures) + if self.selector_closures is not None: + payloads.update(self.selector_closures.binary_payloads(prefix=prefix)) + return payloads + + def to_metadata(self, *, prefix: str = "eager") -> dict[str, object]: + metadata: dict[str, object] = { + "kind": EAGER_RUNTIME_KIND, + "eager_plan_abi": EAGER_PLAN_ABI, + "required_runtime_capabilities": [EAGER_RUNTIME_CAPABILITY], + "process_key": self.process_key, + "couplings": { + "path": f"{prefix}/couplings.bin", + "count": len(self.couplings), + "row_size": EagerCouplingRow._STRUCT.size, + }, + "stages": [ + { + "stage_index": stage.stage_index, + "subset_size": stage.subset_size, + "invocations": { + "path": (f"{prefix}/stage-{stage.stage_index}-invocations.bin"), + "count": len(stage.invocations), + "row_size": EagerInvocationRow._STRUCT.size, + }, + "attachments": { + "path": (f"{prefix}/stage-{stage.stage_index}-attachments.bin"), + "count": len(stage.attachments), + "row_size": EagerAttachmentRow._STRUCT.size, + }, + "finalizations": { + "path": ( + f"{prefix}/stage-{stage.stage_index}-finalizations.bin" + ), + "count": len(stage.finalizations), + "row_size": EagerFinalizationRow._STRUCT.size, + }, + } + for stage in self.stages + ], + "closures": { + "path": f"{prefix}/closures.bin", + "count": len(self.closures), + "row_size": EagerClosureRow._STRUCT.size, + }, + } + if self.selector_closures is not None: + metadata["selector_closures"] = self.selector_closures.to_metadata( + prefix=prefix + ) + return metadata + + +class _CouplingCatalog: + def __init__(self, model_parameters: Sequence[Mapping[str, object]]) -> None: + self.rows: list[EagerCouplingRow] = [] + self._row_ids: dict[EagerCouplingRow, int] = {} + self._input_ids: dict[ + tuple[tuple[float, float], tuple[str | None, ...]], + int, + ] = {} + self._direct = { + str(record["name"]): int(record["parameter_index"]) + for record in model_parameters + } + logical: dict[str, dict[str, int]] = {} + for record in model_parameters: + runtime_name = record.get("runtime_name") + component = record.get("complex_component") + if isinstance(runtime_name, str) and component in {"real", "imag"}: + logical.setdefault(runtime_name, {})[str(component)] = int( + record["parameter_index"] + ) + self._logical = logical + + def add( + self, + coupling: Sequence[object], + parameter_names: Sequence[object] | None, + ) -> int: + if len(coupling) != 2: + raise ValueError("eager coupling metadata must contain two components") + constants = (float(coupling[0]), float(coupling[1])) + names = tuple( + name if isinstance(name, str) else None for name in (parameter_names or ()) + ) + input_key = (constants, names) + existing = self._input_ids.get(input_key) + if existing is not None: + return existing + real_parameter = MISSING_U32 + imag_parameter = MISSING_U32 + + first_name = names[0] if names else None + if isinstance(first_name, str) and first_name in self._logical: + components = self._logical[first_name] + if set(components) != {"real", "imag"}: + raise ValueError( + f"logical coupling {first_name!r} lacks real/imaginary slots" + ) + real_parameter = components["real"] + imag_parameter = components["imag"] + else: + for component, name in enumerate(names[:2]): + if not isinstance(name, str): + continue + parameter_id = self._direct.get(name) + if parameter_id is None: + continue + if component == 0: + real_parameter = parameter_id + else: + imag_parameter = parameter_id + + row = EagerCouplingRow._from_trusted_values( + real_parameter, + imag_parameter, + constants[0], + constants[1], + ) + existing = self._row_ids.get(row) + if existing is not None: + self._input_ids[input_key] = existing + return existing + row_id = len(self.rows) + self.rows.append(row) + self._row_ids[row] = row_id + self._input_ids[input_key] = row_id + return row_id + + +@dataclass(frozen=True, slots=True) +class _EagerStageLoweringInput: + record: Mapping[str, object] + evaluation_groups: Sequence[Sequence[InteractionNode]] + input_value_slot_by_current: Mapping[int, int] + output_current_ids: Sequence[int] + + +@dataclass(frozen=True, slots=True) +class _EagerLoweringInput: + process_key: str + value_slots: Mapping[int, Mapping[str, object]] | Sequence[Mapping[str, object]] + value_slots_by_current_variant: Mapping[tuple[int, str], int] + momentum_slot_by_mask: Mapping[int, int] + model_parameters: Sequence[Mapping[str, object]] + stages: Sequence[_EagerStageLoweringInput] + amplitude_stage: Mapping[str, object] + + +def lower_fused_eager_execution( + *, + dag: GenericDAG, + model: Model, + resolver: EagerKernelResolver, + process_id: str | None = None, + progress_callback: EagerProgressCallback | None = None, +) -> tuple[dict[str, object], EagerExecutionTables]: + """Assemble runtime metadata and eager tables from one owned layout.""" + + if progress_callback is not None: + progress_callback({"step": "runtime-layout"}) + layout = build_runtime_schema_layout( + dag, + model, + process_id=process_id, + ) + tables = _lower_eager_execution_tables( + dag, + model, + resolver, + _lowering_input_from_layout(layout), + progress_callback=progress_callback, + ) + return layout.runtime_schema, tables + + +def lower_eager_execution_tables( + dag: GenericDAG, + model: Model, + runtime_schema: Mapping[str, object], + resolver: EagerKernelResolver, + progress_callback: EagerProgressCallback | None = None, +) -> EagerExecutionTables: + """Lower a proven DAG without constructing any backend evaluator.""" + + return _lower_eager_execution_tables( + dag, + model, + resolver, + _lowering_input_from_schema(dag, runtime_schema), + progress_callback=progress_callback, + ) + + +def _lowering_input_from_schema( + dag: GenericDAG, + runtime_schema: Mapping[str, object], +) -> _EagerLoweringInput: + value_slots = { + int(slot["value_slot_id"]): slot + for slot in _mapping_sequence( + _mapping(runtime_schema.get("value_storage"), "value_storage").get( + "value_slots" + ), + "value_storage.value_slots", + ) + } + value_slots_by_current_variant = { + (int(slot["current_id"]), str(slot["variant"])): int(slot_id) + for slot_id, slot in value_slots.items() + } + momentum_slot_by_mask = { + int(slot["momentum_mask"]): int(slot["momentum_slot_id"]) + for slot in _mapping_sequence( + runtime_schema.get("momentum_slots"), + "momentum_slots", + ) + } + model_parameters = _mapping_sequence( + runtime_schema.get("model_parameters"), + "model_parameters", + ) + stages: list[_EagerStageLoweringInput] = [] + for stage_record in _mapping_sequence(runtime_schema.get("stages"), "stages"): + interactions = tuple( + dag.interactions[int(value)] + for value in _sequence( + stage_record.get("interaction_ids"), + "interaction_ids", + ) + ) + groups: dict[tuple[str, int], list[InteractionNode]] = {} + for interaction in interactions: + group_key = ( + ("group", int(interaction.evaluation_group_id)) + if interaction.evaluation_group_id is not None + else ("interaction", interaction.id) + ) + groups.setdefault(group_key, []).append(interaction) + input_value_slot_by_current = { + int(value_slots[slot_id]["current_id"]): slot_id + for slot_id in ( + int(value) + for value in _sequence( + stage_record.get("input_value_slot_ids"), + "input_value_slot_ids", + ) + ) + } + stages.append( + _EagerStageLoweringInput( + record=stage_record, + evaluation_groups=tuple(tuple(group) for group in groups.values()), + input_value_slot_by_current=input_value_slot_by_current, + output_current_ids=tuple( + int(value) + for value in _sequence( + stage_record.get("output_current_ids"), + "output_current_ids", + ) + ), + ) + ) + return _EagerLoweringInput( + process_key=str(runtime_schema.get("process_key", dag.process.key)), + value_slots=value_slots, + value_slots_by_current_variant=value_slots_by_current_variant, + momentum_slot_by_mask=momentum_slot_by_mask, + model_parameters=model_parameters, + stages=tuple(stages), + amplitude_stage=_mapping( + runtime_schema.get("amplitude_stage"), + "amplitude_stage", + ), + ) + + +def _lowering_input_from_layout( + layout: RuntimeSchemaLayout, +) -> _EagerLoweringInput: + stages = tuple( + _EagerStageLoweringInput( + record=stage.record, + evaluation_groups=stage.evaluation_groups, + input_value_slot_by_current=stage.input_value_slot_by_current, + output_current_ids=stage.output_current_ids, + ) + for stage in layout.stages + ) + return _EagerLoweringInput( + process_key=str(layout.runtime_schema["process_key"]), + value_slots=layout.value_slots_by_id, + value_slots_by_current_variant=layout.value_slot_ids_by_current_variant, + momentum_slot_by_mask=layout.momentum_slot_by_mask, + model_parameters=layout.model_parameters, + stages=stages, + amplitude_stage=layout.amplitude_stage, + ) + + +def _lower_eager_execution_tables( + dag: GenericDAG, + model: Model, + resolver: EagerKernelResolver, + lowering: _EagerLoweringInput, + *, + progress_callback: EagerProgressCallback | None = None, +) -> EagerExecutionTables: + value_slots = lowering.value_slots + value_slots_by_current_variant = lowering.value_slots_by_current_variant + momentum_slot_by_mask = lowering.momentum_slot_by_mask + coupling_catalog = _CouplingCatalog(lowering.model_parameters) + coupling_parameter_names: dict[ + tuple[int, tuple[int, ...], tuple[float, ...]], + tuple[str | None, ...], + ] = {} + resolved_vertex_kernels: dict[ + tuple[ + int, + tuple[int, int, int], + int, + int, + int, + tuple[float, float], + ], + EagerResolvedKernel, + ] = {} + propagator_kernel_ids: dict[tuple[int, int], int | None] = {} + + def parameter_names_for(interaction: InteractionNode) -> tuple[str | None, ...]: + coupling_key = ( + interaction.vertex_kind, + tuple(interaction.vertex_particles), + tuple(interaction.coupling), + ) + parameter_names = coupling_parameter_names.get(coupling_key) + if parameter_names is None: + parameter_names = tuple( + runtime_coupling_parameter_names( + interaction.vertex_kind, + interaction.vertex_particles, + interaction.coupling, + model=model, + ) + ) + coupling_parameter_names[coupling_key] = parameter_names + return parameter_names + + def kernel_for(interaction: InteractionNode) -> EagerResolvedKernel: + left = dag.currents[interaction.left_id] + right = dag.currents[interaction.right_id] + result = dag.currents[interaction.result_id] + key = ( + int(interaction.vertex_kind), + interaction.vertex_particles, + int(left.index.chirality), + int(right.index.chirality), + int(result.index.chirality), + interaction.coupling, + ) + resolved = resolved_vertex_kernels.get(key) + if resolved is None: + resolved = resolver.vertex_kernel(interaction) + resolved_vertex_kernels[key] = resolved + return resolved + + stages: list[EagerStageTables] = [] + + stage_total = len(lowering.stages) + for stage_position, stage_input in enumerate(lowering.stages, start=1): + stage_record = stage_input.record + stage_index = int(stage_record["stage_index"]) + subset_size = int(stage_record["subset_size"]) + input_slot_by_current = stage_input.input_value_slot_by_current + + invocations: list[EagerInvocationRow] = [] + attachments: list[EagerAttachmentRow] = [] + for interactions in stage_input.evaluation_groups: + live_interactions = tuple( + interaction + for interaction in interactions + if complex(*interaction.evaluation_factor) != 0j + ) + if not live_interactions: + continue + representative = live_interactions[0] + resolved_kernel = kernel_for(representative) + representative_factor = complex(*representative.evaluation_factor) + parameter_names = parameter_names_for(representative) + coupling_slot_id = coupling_catalog.add( + representative.coupling, + parameter_names, + ) + attachment_start = len(attachments) + group_factor = ( + complex(*resolved_kernel.normalization_factor) + / representative_factor + ) + for interaction in live_interactions: + # The compiler's evaluation-group key already proves identical + # kernel, coupling, and mutable-parameter provenance. Repeating + # those resolver lookups for every fan-out attachment is pure + # lowering overhead. + factor = ( + complex(*interaction.color_weight) + * complex(*interaction.evaluation_factor) + * group_factor + ) + attachments.append( + EagerAttachmentRow._from_trusted_values( + interaction.result_id, + factor.real, + factor.imag, + ) + ) + left = dag.currents[representative.left_id] + right = dag.currents[representative.right_id] + input_currents = (left, right) + ordered_currents = tuple( + input_currents[index] for index in resolved_kernel.canonical_input_order + ) + invocations.append( + EagerInvocationRow._from_trusted_values( + resolved_kernel.kernel_id, + input_slot_by_current[ordered_currents[0].id], + input_slot_by_current[ordered_currents[1].id], + momentum_slot_by_mask[ordered_currents[0].index.momentum_mask], + momentum_slot_by_mask[ordered_currents[1].index.momentum_mask], + coupling_slot_id, + resolved_kernel.output_factor_source, + attachment_start, + len(live_interactions), + ) + ) + + finalizations = tuple( + _finalization_row( + dag.currents[int(current_id)], + value_slots, + value_slots_by_current_variant, + momentum_slot_by_mask, + resolver, + propagator_kernel_ids, + ) + for current_id in stage_input.output_current_ids + ) + stages.append( + EagerStageTables( + stage_index=stage_index, + subset_size=subset_size, + invocations=tuple(invocations), + attachments=tuple(attachments), + finalizations=finalizations, + ) + ) + if progress_callback is not None: + progress_callback( + { + "step": "invocation lowering", + "stage_index": stage_position, + "stage_total": stage_total, + "subset_size": subset_size, + "invocation_count": len(invocations), + "attachment_count": len(attachments), + "finalization_count": len(finalizations), + } + ) + + amplitude_stage = lowering.amplitude_stage + if progress_callback is not None: + progress_callback( + { + "step": "amplitude closures", + "stage_index": stage_total, + "stage_total": stage_total, + } + ) + closures: list[EagerClosureRow] = [] + for root in _mapping_sequence( + amplitude_stage.get("roots"), "amplitude_stage.roots" + ): + closure_kernel = resolver.closure_kernel(root) + coupling_slot_id = MISSING_U32 + if closure_kernel is not None: + coupling_slot_id = coupling_catalog.add( + _sequence(root.get("coupling"), "root.coupling"), + _optional_sequence(root.get("coupling_parameter_names")), + ) + color_weight = _complex_pair(root.get("color_weight"), "root.color_weight") + left_slot_id = int( + _mapping(root.get("left_value_slot"), "left_value_slot")["value_slot_id"] + ) + right_slot_id = int( + _mapping(root.get("right_value_slot"), "right_value_slot")["value_slot_id"] + ) + if closure_kernel is not None: + slots = (left_slot_id, right_slot_id) + left_slot_id, right_slot_id = ( + slots[index] for index in closure_kernel.canonical_input_order + ) + color_weight *= complex(*closure_kernel.normalization_factor) + closures.append( + EagerClosureRow._from_trusted_values( + MISSING_U32 if closure_kernel is None else closure_kernel.kernel_id, + left_slot_id, + right_slot_id, + int(root["output_index"]), + coupling_slot_id, + ( + EAGER_OUTPUT_FACTOR_NONE + if closure_kernel is None + else closure_kernel.output_factor_source + ), + color_weight.real, + color_weight.imag, + ) + ) + + stage_tables = tuple(stages) + closure_rows = tuple(closures) + if progress_callback is not None: + progress_callback( + { + "step": "selector closures", + "stage_index": stage_total, + "stage_total": stage_total, + "closure_count": len(closure_rows), + } + ) + selector_closures = _build_selector_closure_tables( + stage_tables, + closure_rows, + amplitude_stage, + value_slot_count=len(value_slots), + current_count=len(dag.currents), + ) + if progress_callback is not None: + progress_callback( + { + "step": "eager plan ready", + "stage_index": stage_total, + "stage_total": stage_total, + "closure_count": len(closure_rows), + } + ) + return EagerExecutionTables( + process_key=lowering.process_key, + couplings=tuple(coupling_catalog.rows), + stages=stage_tables, + closures=closure_rows, + selector_closures=selector_closures, + ) + + +def _build_selector_closure_tables( + stages: tuple[EagerStageTables, ...], + closures: tuple[EagerClosureRow, ...], + amplitude_stage: Mapping[str, object], + *, + value_slot_count: int, + current_count: int, +) -> EagerSelectorClosureTables: + """Propagate coherent-group dependencies backwards through eager rows.""" + + roots = _mapping_sequence(amplitude_stage.get("roots"), "amplitude_stage.roots") + if len(roots) != len(closures): + raise ValueError("eager closures do not match runtime amplitude roots") + + declared_groups = tuple( + int(group["group_id"]) + for group in _mapping_sequence( + amplitude_stage.get("coherent_groups"), + "amplitude_stage.coherent_groups", + ) + ) + if len(declared_groups) != len(set(declared_groups)) or any( + group_id < 0 or group_id > MISSING_U32 for group_id in declared_groups + ): + raise ValueError("runtime coherent-group IDs must be unique unsigned integers") + ordered_group_ids = tuple(sorted(declared_groups)) + group_bit_indices = { + group_id: bit_index for bit_index, group_id in enumerate(ordered_group_ids) + } + + dense_mask_interning = ( + len(ordered_group_ids) <= _DENSE_SELECTOR_DOMAIN_GROUP_LIMIT + ) + domain_masks: list[int] = [] + domain_ids_by_mask: dict[int, int] = {} + domain_members: list[tuple[int, ...]] = [] + domain_ids_by_members: dict[tuple[int, ...], int] = {} + + def intern_members(members: tuple[int, ...]) -> int: + existing = domain_ids_by_members.get(members) + if existing is not None: + return existing + domain_id = len(domain_members) + domain_members.append(members) + domain_ids_by_members[members] = domain_id + return domain_id + + def members(domain: int) -> tuple[int, ...]: + result: list[int] = [] + remaining = domain + while remaining: + lowest = remaining & -remaining + result.append(ordered_group_ids[lowest.bit_length() - 1]) + remaining ^= lowest + return tuple(result) + + def intern_mask(domain: int) -> int: + existing = domain_ids_by_mask.get(domain) + if existing is not None: + return existing + domain_id = len(domain_masks) + domain_masks.append(domain) + domain_ids_by_mask[domain] = domain_id + return domain_id + + def intern_stage_mask( + domain: int, + stage_domain_ids_by_mask: dict[int, int], + ) -> int: + if dense_mask_interning: + return intern_mask(domain) + existing = stage_domain_ids_by_mask.get(domain) + if existing is not None: + return existing + domain_id = intern_members(members(domain)) + stage_domain_ids_by_mask[domain] = domain_id + return domain_id + + value_domains = [0] * value_slot_count + empty_domain_id = intern_mask(0) if dense_mask_interning else intern_members(()) + closure_domain_ids: list[int] = [] + for closure, root in zip(closures, roots, strict=True): + if closure.amplitude_index != int(root["output_index"]): + raise ValueError( + "eager closure order does not match runtime amplitude roots" + ) + group_id = int(root["coherent_group_id"]) + bit_index = group_bit_indices.get(group_id) + if bit_index is None: + raise ValueError( + f"amplitude root references undeclared coherent group {group_id}" + ) + domain = 1 << bit_index + closure_domain_ids.append( + intern_mask(domain) + if dense_mask_interning + else intern_members((group_id,)) + ) + value_domains[closure.left_value_slot_id] |= domain + value_domains[closure.right_value_slot_id] |= domain + + stage_domain_ids_reversed: list[ + tuple[ + tuple[int, ...], + tuple[int, ...], + tuple[int, ...], + tuple[int, ...], + ], + ] = [] + current_domains = [0] * current_count + current_domain_ids = [empty_domain_id] * current_count + + for stage in reversed(stages): + stage_domain_ids_by_mask = {0: empty_domain_id} + + unpropagated_domain_ids: list[int] = [] + propagated_domain_ids: list[int] = [] + for finalization in stage.finalizations: + unpropagated = ( + value_domains[finalization.unpropagated_value_slot_id] + if finalization.stores_unpropagated + else 0 + ) + propagated = ( + value_domains[finalization.propagated_value_slot_id] + if finalization.stores_propagated + else 0 + ) + unpropagated_domain_ids.append( + intern_stage_mask(unpropagated, stage_domain_ids_by_mask) + ) + propagated_domain_ids.append( + intern_stage_mask(propagated, stage_domain_ids_by_mask) + ) + current_domain = unpropagated | propagated + current_domains[finalization.current_id] = current_domain + current_domain_ids[finalization.current_id] = intern_stage_mask( + current_domain, + stage_domain_ids_by_mask, + ) + if finalization.stores_unpropagated: + value_domains[finalization.unpropagated_value_slot_id] = 0 + if finalization.stores_propagated: + value_domains[finalization.propagated_value_slot_id] = 0 + + attachment_domain_ids = [ + current_domain_ids[attachment.result_current_id] + for attachment in stage.attachments + ] + + invocation_domain_ids: list[int] = [] + for invocation in stage.invocations: + start = invocation.attachment_start + stop = start + invocation.attachment_count + domain = 0 + for attachment_index in range(start, stop): + result_current_id = stage.attachments[ + attachment_index + ].result_current_id + domain |= current_domains[result_current_id] + invocation_domain_ids.append( + intern_stage_mask(domain, stage_domain_ids_by_mask) + ) + value_domains[invocation.left_value_slot_id] |= domain + value_domains[invocation.right_value_slot_id] |= domain + + stage_domain_ids_reversed.append( + ( + tuple(invocation_domain_ids), + tuple(attachment_domain_ids), + tuple(unpropagated_domain_ids), + tuple(propagated_domain_ids), + ) + ) + for finalization in stage.finalizations: + current_domains[finalization.current_id] = 0 + current_domain_ids[finalization.current_id] = empty_domain_id + stage_domain_ids_by_mask.clear() + + # Construction-only masks and interning indexes do not participate in final + # ID assignment or emission. + value_domains.clear() + current_domains.clear() + current_domain_ids.clear() + group_bit_indices.clear() + + # Match the prior dense-mask ordering exactly. For equal-cardinality masks, + # integer comparison examines the highest differing bit first, equivalent + # to lexicographically comparing the reversed, sorted member tuples. + if dense_mask_interning: + ordered_provisional_ids = tuple( + sorted( + range(len(domain_masks)), + key=lambda domain_id: ( + domain_masks[domain_id].bit_count(), + domain_masks[domain_id], + ), + ) + ) + else: + ordered_provisional_ids = tuple( + sorted( + range(len(domain_members)), + key=lambda domain_id: ( + len(domain_members[domain_id]), + tuple(reversed(domain_members[domain_id])), + ), + ) + ) + final_domain_ids = [0] * len(ordered_provisional_ids) + for final_domain_id, provisional_domain_id in enumerate(ordered_provisional_ids): + final_domain_ids[provisional_domain_id] = final_domain_id + + domain_rows: list[EagerSelectorDomainRow] = [] + group_rows: list[EagerSelectorGroupRow] = [] + group_rows_by_bit_index = tuple( + EagerSelectorGroupRow._from_trusted_values(group_id) + for group_id in ordered_group_ids + ) + group_row_by_id = ( + {} + if dense_mask_interning + else { + group_id: group_rows_by_bit_index[bit_index] + for bit_index, group_id in enumerate(ordered_group_ids) + } + ) + for provisional_domain_id in ordered_provisional_ids: + if dense_mask_interning: + domain = domain_masks[provisional_domain_id] + member_count = domain.bit_count() + else: + resolved_members = domain_members[provisional_domain_id] + member_count = len(resolved_members) + domain_rows.append( + EagerSelectorDomainRow._from_trusted_values( + len(group_rows), member_count + ) + ) + if dense_mask_interning: + while domain: + lowest = domain & -domain + group_rows.append( + group_rows_by_bit_index[lowest.bit_length() - 1] + ) + domain ^= lowest + else: + group_rows.extend( + group_row_by_id[group_id] for group_id in resolved_members + ) + + domain_count = len(ordered_provisional_ids) + domain_masks.clear() + domain_ids_by_mask.clear() + domain_members.clear() + domain_ids_by_members.clear() + group_row_by_id.clear() + domain_reference_rows = tuple( + EagerSelectorDomainIdRow._from_trusted_values(domain_id) + for domain_id in range(domain_count) + ) + + def references( + provisional_domain_ids: Sequence[int], + ) -> tuple[EagerSelectorDomainIdRow, ...]: + return tuple( + domain_reference_rows[final_domain_ids[provisional_domain_id]] + for provisional_domain_id in provisional_domain_ids + ) + + selector_stages = [] + for stage, domain_ids in zip( + stages, + reversed(stage_domain_ids_reversed), + strict=True, + ): + ( + stage_invocation_domain_ids, + stage_attachment_domain_ids, + stage_unpropagated_domain_ids, + stage_propagated_domain_ids, + ) = domain_ids + selector_stages.append( + EagerStageSelectorDomains( + stage_index=stage.stage_index, + invocation_domains=references(stage_invocation_domain_ids), + attachment_domains=references(stage_attachment_domain_ids), + unpropagated_finalization_domains=references( + stage_unpropagated_domain_ids + ), + propagated_finalization_domains=references(stage_propagated_domain_ids), + ) + ) + + return EagerSelectorClosureTables( + domains=tuple(domain_rows), + domain_group_ids=tuple(group_rows), + stages=tuple(selector_stages), + closure_domains=references(closure_domain_ids), + ) + + +def _finalization_row( + current: CurrentNode, + value_slots: Mapping[int, Mapping[str, object]] | Sequence[Mapping[str, object]], + value_slots_by_current_variant: Mapping[tuple[int, str], int], + momentum_slot_by_mask: Mapping[int, int], + resolver: EagerKernelResolver, + propagator_kernel_ids: dict[tuple[int, int], int | None], +) -> EagerFinalizationRow: + unpropagated = value_slots_by_current_variant.get( + (current.id, "unpropagated"), + MISSING_U32, + ) + propagated = value_slots_by_current_variant.get( + (current.id, "propagated"), + MISSING_U32, + ) + if unpropagated == MISSING_U32 and propagated == MISSING_U32: + raise ValueError(f"eager current {current.id} has no output value slot") + propagator_slot_id = propagated if propagated != MISSING_U32 else unpropagated + output_slot = value_slots[propagator_slot_id] + propagator = _mapping( + output_slot.get("propagator"), + f"current {current.id} propagator", + ) + propagator_key = (current.index.particle_id, current.index.chirality) + if not bool(output_slot.get("applies_propagator", False)): + kernel_id = None + elif propagator_key in propagator_kernel_ids: + kernel_id = propagator_kernel_ids[propagator_key] + else: + kernel_id = resolver.propagator_kernel_id(current, propagator) + propagator_kernel_ids[propagator_key] = kernel_id + if propagated != MISSING_U32 and kernel_id is None: + raise ValueError( + f"eager propagated current {current.id} has no prepared propagator kernel" + ) + return EagerFinalizationRow._from_trusted_values( + MISSING_U32 if kernel_id is None else kernel_id, + current.id, + unpropagated, + propagated, + momentum_slot_by_mask[current.index.momentum_mask], + ) + + +def _mapping(value: object, context: str) -> Mapping[str, object]: + if not isinstance(value, Mapping): + raise TypeError(f"{context} must be a mapping") + return value + + +def _sequence(value: object, context: str) -> Sequence[object]: + if isinstance(value, (str, bytes, bytearray)) or not isinstance(value, Sequence): + raise TypeError(f"{context} must be a sequence") + return value + + +def _optional_sequence(value: object) -> Sequence[object] | None: + if value is None: + return None + return _sequence(value, "optional sequence") + + +def _mapping_sequence(value: object, context: str) -> tuple[Mapping[str, object], ...]: + return tuple( + _mapping(item, f"{context}[{index}]") + for index, item in enumerate(_sequence(value, context)) + ) + + +def _complex_pair(value: object, context: str) -> complex: + components = _sequence(value, context) + if len(components) != 2: + raise ValueError(f"{context} must contain two components") + return complex(float(components[0]), float(components[1])) + + +def _resolved_kernel( + record: Mapping[str, object], + context: str, +) -> EagerResolvedKernel: + order = tuple( + int(value) + for value in _sequence( + record.get("canonical_input_order"), + f"{context}.canonical_input_order", + ) + ) + factor = tuple( + float(value) + for value in _sequence( + record.get("equivalence_factor"), + f"{context}.equivalence_factor", + ) + ) + if len(order) != 2 or len(factor) != 2: + raise ValueError(f"{context} has malformed input transformation metadata") + factor_source = { + "none": EAGER_OUTPUT_FACTOR_NONE, + "coupling-real": EAGER_OUTPUT_FACTOR_COUPLING_REAL, + "coupling-imag": EAGER_OUTPUT_FACTOR_COUPLING_IMAG, + }.get(str(record.get("output_factor_source", "none"))) + if factor_source is None: + raise ValueError(f"{context} has an unsupported output factor source") + return EagerResolvedKernel( + kernel_id=int(record["kernel_id"]), + canonical_input_order=(order[0], order[1]), + normalization_factor=(factor[0], factor[1]), + output_factor_source=factor_source, + ) + + +def _vertex_kernel_key(record: Mapping[str, object]) -> VertexKernelKey: + particles = tuple( + int(value) for value in _sequence(record.get("particles"), "vertex.particles") + ) + coupling = tuple( + float(value) for value in _sequence(record.get("coupling"), "vertex.coupling") + ) + if len(particles) != 3 or len(coupling) != 2: + raise ValueError("prepared vertex key has malformed particles or coupling") + return VertexKernelKey( + kind=int(record["kind"]), + particles=(particles[0], particles[1], particles[2]), + left_chirality=int(record["left_chirality"]), + right_chirality=int(record["right_chirality"]), + result_chirality=int(record["result_chirality"]), + coupling=(coupling[0], coupling[1]), + ) + + +def _closure_kernel_key(record: Mapping[str, object]) -> ClosureKernelKey: + particles = tuple( + int(value) for value in _sequence(record.get("particles"), "closure.particles") + ) + coupling = tuple( + float(value) for value in _sequence(record.get("coupling"), "closure.coupling") + ) + if len(particles) != 3 or len(coupling) != 2: + raise ValueError("prepared closure key has malformed particles or coupling") + return ClosureKernelKey( + kind=int(record["kind"]), + particles=(particles[0], particles[1], particles[2]), + left_chirality=int(record["left_chirality"]), + right_chirality=int(record["right_chirality"]), + coupling=(coupling[0], coupling[1]), + ) + + +__all__ = [ + "EAGER_RUNTIME_KIND", + "EagerExecutionTables", + "EagerKernelResolver", + "EagerResolvedKernel", + "EagerSelectorClosureTables", + "EagerStageSelectorDomains", + "EagerStageTables", + "MappingEagerKernelResolver", + "PreparedCatalogEagerKernelIndex", + "PreparedCatalogEagerKernelResolver", + "lower_eager_execution_tables", + "lower_fused_eager_execution", +] diff --git a/pyamplicol/src/pyamplicol/generation/eager_tables.py b/pyamplicol/src/pyamplicol/generation/eager_tables.py new file mode 100644 index 0000000000..05d64812d0 --- /dev/null +++ b/pyamplicol/src/pyamplicol/generation/eager_tables.py @@ -0,0 +1,512 @@ +# SPDX-License-Identifier: 0BSD +"""Fixed-width little-endian tables for eager DAG execution.""" + +from __future__ import annotations + +import struct +from collections.abc import Iterable +from dataclasses import dataclass +from math import isfinite +from typing import ClassVar, Protocol, TypeVar + +from .._internal.versions import EAGER_DAG_F64_RUNTIME_CAPABILITY +from ..models.prepared import EAGER_KERNEL_ABI + +EAGER_PLAN_ABI = "pyamplicol-eager-plan-v2" +EAGER_RUNTIME_CAPABILITY = EAGER_DAG_F64_RUNTIME_CAPABILITY +EAGER_SELECTOR_DOMAINS_ABI = "pyamplicol-eager-selector-domains-v1" +MISSING_U32 = (1 << 32) - 1 +EAGER_OUTPUT_FACTOR_NONE = 0 +EAGER_OUTPUT_FACTOR_COUPLING_REAL = 1 +EAGER_OUTPUT_FACTOR_COUPLING_IMAG = 2 +_EAGER_OUTPUT_FACTOR_SOURCES = frozenset( + ( + EAGER_OUTPUT_FACTOR_NONE, + EAGER_OUTPUT_FACTOR_COUPLING_REAL, + EAGER_OUTPUT_FACTOR_COUPLING_IMAG, + ) +) + + +class _FixedWidthRow(Protocol): + _STRUCT: ClassVar[struct.Struct] + + def _values(self) -> tuple[int | float, ...]: ... + + @classmethod + def _from_values(cls, values: tuple[int | float, ...]) -> _FixedWidthRow: ... + + +_RowT = TypeVar("_RowT", bound=_FixedWidthRow) + + +def _u32(value: int, field: str) -> int: + integer = int(value) + if integer < 0 or integer > MISSING_U32: + raise ValueError(f"{field} must fit in an unsigned 32-bit integer") + return integer + + +def _u64(value: int, field: str) -> int: + integer = int(value) + if integer < 0 or integer >= 1 << 64: + raise ValueError(f"{field} must fit in an unsigned 64-bit integer") + return integer + + +@dataclass(frozen=True, slots=True) +class EagerInvocationRow: + """One canonical local-kernel evaluation and its attachment range.""" + + kernel_id: int + left_value_slot_id: int + right_value_slot_id: int + left_momentum_slot_id: int + right_momentum_slot_id: int + coupling_slot_id: int + output_factor_source: int + attachment_start: int + attachment_count: int + + _STRUCT: ClassVar[struct.Struct] = struct.Struct(" None: + for field in ( + "kernel_id", + "left_value_slot_id", + "right_value_slot_id", + "left_momentum_slot_id", + "right_momentum_slot_id", + "coupling_slot_id", + "output_factor_source", + ): + object.__setattr__(self, field, _u32(getattr(self, field), field)) + if self.output_factor_source not in _EAGER_OUTPUT_FACTOR_SOURCES: + raise ValueError("unsupported eager invocation output factor source") + for field in ("attachment_start", "attachment_count"): + object.__setattr__(self, field, _u64(getattr(self, field), field)) + + def _values(self) -> tuple[int | float, ...]: + return ( + self.kernel_id, + self.left_value_slot_id, + self.right_value_slot_id, + self.left_momentum_slot_id, + self.right_momentum_slot_id, + self.coupling_slot_id, + self.output_factor_source, + self.attachment_start, + self.attachment_count, + ) + + @classmethod + def _from_values(cls, values: tuple[int | float, ...]) -> EagerInvocationRow: + return cls(*(int(value) for value in values)) + + @classmethod + def _from_trusted_values( + cls, + kernel_id: int, + left_value_slot_id: int, + right_value_slot_id: int, + left_momentum_slot_id: int, + right_momentum_slot_id: int, + coupling_slot_id: int, + output_factor_source: int, + attachment_start: int, + attachment_count: int, + ) -> EagerInvocationRow: + """Construct an internally proven row without repeating boundary checks.""" + + row = object.__new__(cls) + object.__setattr__(row, "kernel_id", kernel_id) + object.__setattr__(row, "left_value_slot_id", left_value_slot_id) + object.__setattr__(row, "right_value_slot_id", right_value_slot_id) + object.__setattr__(row, "left_momentum_slot_id", left_momentum_slot_id) + object.__setattr__(row, "right_momentum_slot_id", right_momentum_slot_id) + object.__setattr__(row, "coupling_slot_id", coupling_slot_id) + object.__setattr__(row, "output_factor_source", output_factor_source) + object.__setattr__(row, "attachment_start", attachment_start) + object.__setattr__(row, "attachment_count", attachment_count) + return row + + +@dataclass(frozen=True, slots=True) +class EagerAttachmentRow: + """Fan-out from one canonical evaluation to one accumulated current.""" + + result_current_id: int + factor_real: float + factor_imag: float + + _STRUCT: ClassVar[struct.Struct] = struct.Struct(" None: + object.__setattr__( + self, + "result_current_id", + _u32(self.result_current_id, "result_current_id"), + ) + object.__setattr__(self, "factor_real", float(self.factor_real)) + object.__setattr__(self, "factor_imag", float(self.factor_imag)) + if not isfinite(self.factor_real) or not isfinite(self.factor_imag): + raise ValueError("attachment factor must be finite") + + def _values(self) -> tuple[int | float, ...]: + return self.result_current_id, self.factor_real, self.factor_imag + + @classmethod + def _from_values(cls, values: tuple[int | float, ...]) -> EagerAttachmentRow: + return cls(int(values[0]), float(values[1]), float(values[2])) + + @classmethod + def _from_trusted_values( + cls, + result_current_id: int, + factor_real: float, + factor_imag: float, + ) -> EagerAttachmentRow: + row = object.__new__(cls) + object.__setattr__(row, "result_current_id", result_current_id) + object.__setattr__(row, "factor_real", factor_real) + object.__setattr__(row, "factor_imag", factor_imag) + return row + + +@dataclass(frozen=True, slots=True) +class EagerCouplingRow: + """Resolve one complex coupling from constants and/or model parameters.""" + + real_parameter_id: int + imag_parameter_id: int + constant_real: float + constant_imag: float + + _STRUCT: ClassVar[struct.Struct] = struct.Struct(" None: + for field in ("real_parameter_id", "imag_parameter_id"): + object.__setattr__(self, field, _u32(getattr(self, field), field)) + object.__setattr__(self, "constant_real", float(self.constant_real)) + object.__setattr__(self, "constant_imag", float(self.constant_imag)) + if not isfinite(self.constant_real) or not isfinite(self.constant_imag): + raise ValueError("coupling constants must be finite") + + def _values(self) -> tuple[int | float, ...]: + return ( + self.real_parameter_id, + self.imag_parameter_id, + self.constant_real, + self.constant_imag, + ) + + @classmethod + def _from_values(cls, values: tuple[int | float, ...]) -> EagerCouplingRow: + return cls( + int(values[0]), + int(values[1]), + float(values[2]), + float(values[3]), + ) + + @classmethod + def _from_trusted_values( + cls, + real_parameter_id: int, + imag_parameter_id: int, + constant_real: float, + constant_imag: float, + ) -> EagerCouplingRow: + row = object.__new__(cls) + object.__setattr__(row, "real_parameter_id", real_parameter_id) + object.__setattr__(row, "imag_parameter_id", imag_parameter_id) + object.__setattr__(row, "constant_real", constant_real) + object.__setattr__(row, "constant_imag", constant_imag) + return row + + +@dataclass(frozen=True, slots=True) +class EagerFinalizationRow: + """Finalize one accumulated current, applying its propagator at most once.""" + + kernel_id: int + current_id: int + unpropagated_value_slot_id: int + propagated_value_slot_id: int + momentum_slot_id: int + + _STRUCT: ClassVar[struct.Struct] = struct.Struct(" None: + for field in ( + "kernel_id", + "current_id", + "unpropagated_value_slot_id", + "propagated_value_slot_id", + "momentum_slot_id", + ): + object.__setattr__(self, field, _u32(getattr(self, field), field)) + + @property + def applies_kernel(self) -> bool: + return self.kernel_id != MISSING_U32 + + @property + def stores_unpropagated(self) -> bool: + return self.unpropagated_value_slot_id != MISSING_U32 + + @property + def stores_propagated(self) -> bool: + return self.propagated_value_slot_id != MISSING_U32 + + def _values(self) -> tuple[int | float, ...]: + return ( + self.kernel_id, + self.current_id, + self.unpropagated_value_slot_id, + self.propagated_value_slot_id, + self.momentum_slot_id, + ) + + @classmethod + def _from_values(cls, values: tuple[int | float, ...]) -> EagerFinalizationRow: + return cls(*(int(value) for value in values)) + + @classmethod + def _from_trusted_values( + cls, + kernel_id: int, + current_id: int, + unpropagated_value_slot_id: int, + propagated_value_slot_id: int, + momentum_slot_id: int, + ) -> EagerFinalizationRow: + row = object.__new__(cls) + object.__setattr__(row, "kernel_id", kernel_id) + object.__setattr__(row, "current_id", current_id) + object.__setattr__( + row, + "unpropagated_value_slot_id", + unpropagated_value_slot_id, + ) + object.__setattr__(row, "propagated_value_slot_id", propagated_value_slot_id) + object.__setattr__(row, "momentum_slot_id", momentum_slot_id) + return row + + +@dataclass(frozen=True, slots=True) +class EagerClosureRow: + """One prepared amplitude-closure call and its physical output slot.""" + + kernel_id: int + left_value_slot_id: int + right_value_slot_id: int + amplitude_index: int + coupling_slot_id: int + output_factor_source: int + factor_real: float + factor_imag: float + + _STRUCT: ClassVar[struct.Struct] = struct.Struct(" None: + for field in ( + "kernel_id", + "left_value_slot_id", + "right_value_slot_id", + "amplitude_index", + "coupling_slot_id", + "output_factor_source", + ): + object.__setattr__(self, field, _u32(getattr(self, field), field)) + if self.output_factor_source not in _EAGER_OUTPUT_FACTOR_SOURCES: + raise ValueError("unsupported eager closure output factor source") + object.__setattr__(self, "factor_real", float(self.factor_real)) + object.__setattr__(self, "factor_imag", float(self.factor_imag)) + if not isfinite(self.factor_real) or not isfinite(self.factor_imag): + raise ValueError("closure factor must be finite") + + def _values(self) -> tuple[int | float, ...]: + return ( + self.kernel_id, + self.left_value_slot_id, + self.right_value_slot_id, + self.amplitude_index, + self.coupling_slot_id, + self.output_factor_source, + self.factor_real, + self.factor_imag, + ) + + @classmethod + def _from_values(cls, values: tuple[int | float, ...]) -> EagerClosureRow: + return cls( + int(values[0]), + int(values[1]), + int(values[2]), + int(values[3]), + int(values[4]), + int(values[5]), + float(values[6]), + float(values[7]), + ) + + @classmethod + def _from_trusted_values( + cls, + kernel_id: int, + left_value_slot_id: int, + right_value_slot_id: int, + amplitude_index: int, + coupling_slot_id: int, + output_factor_source: int, + factor_real: float, + factor_imag: float, + ) -> EagerClosureRow: + row = object.__new__(cls) + object.__setattr__(row, "kernel_id", kernel_id) + object.__setattr__(row, "left_value_slot_id", left_value_slot_id) + object.__setattr__(row, "right_value_slot_id", right_value_slot_id) + object.__setattr__(row, "amplitude_index", amplitude_index) + object.__setattr__(row, "coupling_slot_id", coupling_slot_id) + object.__setattr__(row, "output_factor_source", output_factor_source) + object.__setattr__(row, "factor_real", factor_real) + object.__setattr__(row, "factor_imag", factor_imag) + return row + + +@dataclass(frozen=True, slots=True) +class EagerSelectorDomainRow: + """One range into the flattened coherent-group membership table.""" + + member_start: int + member_count: int + + _STRUCT: ClassVar[struct.Struct] = struct.Struct(" None: + for field in ("member_start", "member_count"): + object.__setattr__(self, field, _u64(getattr(self, field), field)) + + def _values(self) -> tuple[int | float, ...]: + return self.member_start, self.member_count + + @classmethod + def _from_values(cls, values: tuple[int | float, ...]) -> EagerSelectorDomainRow: + return cls(int(values[0]), int(values[1])) + + @classmethod + def _from_trusted_values( + cls, + member_start: int, + member_count: int, + ) -> EagerSelectorDomainRow: + row = object.__new__(cls) + object.__setattr__(row, "member_start", member_start) + object.__setattr__(row, "member_count", member_count) + return row + + +@dataclass(frozen=True, slots=True) +class EagerSelectorGroupRow: + """One coherent reduction-group member of an interned selector domain.""" + + coherent_group_id: int + + _STRUCT: ClassVar[struct.Struct] = struct.Struct(" None: + object.__setattr__( + self, + "coherent_group_id", + _u32(self.coherent_group_id, "coherent_group_id"), + ) + + def _values(self) -> tuple[int | float, ...]: + return (self.coherent_group_id,) + + @classmethod + def _from_values(cls, values: tuple[int | float, ...]) -> EagerSelectorGroupRow: + return cls(int(values[0])) + + @classmethod + def _from_trusted_values(cls, coherent_group_id: int) -> EagerSelectorGroupRow: + row = object.__new__(cls) + object.__setattr__(row, "coherent_group_id", coherent_group_id) + return row + + +@dataclass(frozen=True, slots=True) +class EagerSelectorDomainIdRow: + """One reference to an interned selector domain.""" + + domain_id: int + + _STRUCT: ClassVar[struct.Struct] = struct.Struct(" None: + object.__setattr__(self, "domain_id", _u32(self.domain_id, "domain_id")) + + def _values(self) -> tuple[int | float, ...]: + return (self.domain_id,) + + @classmethod + def _from_values(cls, values: tuple[int | float, ...]) -> EagerSelectorDomainIdRow: + return cls(int(values[0])) + + @classmethod + def _from_trusted_values(cls, domain_id: int) -> EagerSelectorDomainIdRow: + row = object.__new__(cls) + object.__setattr__(row, "domain_id", domain_id) + return row + + +def pack_rows(rows: Iterable[_FixedWidthRow]) -> bytes: + """Serialize homogeneous rows without padding or a redundant header.""" + + records = tuple(rows) + if not records: + return b"" + row_type = type(records[0]) + if any(type(row) is not row_type for row in records): + raise TypeError("eager binary tables must contain one row type") + layout = records[0]._STRUCT + payload = bytearray(len(records) * layout.size) + for row_index, row in enumerate(records): + layout.pack_into(payload, row_index * layout.size, *row._values()) + return bytes(payload) + + +def unpack_rows(payload: bytes, row_type: type[_RowT]) -> tuple[_RowT, ...]: + """Deserialize one table, rejecting truncated or extended row payloads.""" + + layout = row_type._STRUCT + if len(payload) % layout.size: + raise ValueError( + f"eager table has {len(payload)} bytes, not a multiple of {layout.size}" + ) + return tuple( + row_type._from_values(tuple(values)) for values in layout.iter_unpack(payload) + ) + + +__all__ = [ + "EAGER_KERNEL_ABI", + "EAGER_OUTPUT_FACTOR_COUPLING_IMAG", + "EAGER_OUTPUT_FACTOR_COUPLING_REAL", + "EAGER_OUTPUT_FACTOR_NONE", + "EAGER_PLAN_ABI", + "EAGER_RUNTIME_CAPABILITY", + "EAGER_SELECTOR_DOMAINS_ABI", + "MISSING_U32", + "EagerAttachmentRow", + "EagerClosureRow", + "EagerCouplingRow", + "EagerFinalizationRow", + "EagerInvocationRow", + "EagerSelectorDomainIdRow", + "EagerSelectorDomainRow", + "EagerSelectorGroupRow", + "pack_rows", + "unpack_rows", +] diff --git a/pyamplicol/src/pyamplicol/generation/evaluator_container.py b/pyamplicol/src/pyamplicol/generation/evaluator_container.py new file mode 100644 index 0000000000..e8a5a0fffe --- /dev/null +++ b/pyamplicol/src/pyamplicol/generation/evaluator_container.py @@ -0,0 +1,957 @@ +# SPDX-License-Identifier: 0BSD +"""Portable binary containers for generated evaluator payloads. + +``pacbin-v1`` is deliberately independent of Python serialization formats. A +container consists of a fixed 64-byte header, 64-byte-aligned uncompressed +member payloads, a sorted variable-width index, and a fixed 64-byte footer. +Every integer is little-endian. The footer authenticates the exact index +bytes, while every index entry authenticates its member payload with SHA-256. + +This module implements the producer-side codec. Artifact publication uses it +to write one evaluator pack, while Rusticol implements the independent native +reader for runtime loading. +""" + +from __future__ import annotations + +import hashlib +import os +import struct +import tempfile +import unicodedata +from collections.abc import Iterable, Iterator +from contextlib import contextmanager, suppress +from dataclasses import dataclass +from enum import IntEnum +from pathlib import Path +from typing import BinaryIO + +PACBIN_VERSION = 1 +PACBIN_ALIGNMENT = 64 + +_HEADER_MAGIC = b"PACBIN\x00\x00" +_INDEX_MAGIC = b"PACIDX\x00\x00" +_FOOTER_MAGIC = b"PACEND\x00\x00" +_SUPPORTED_FLAGS = 0 +_INDEX_ALIGNMENT = 8 +_DEFAULT_CHUNK_SIZE = 1024 * 1024 +_MAX_U32 = (1 << 32) - 1 +_MAX_U64 = (1 << 64) - 1 + +# magic, version, size, flags, alignment, reserved, index offset, member count, +# reserved. Keeping the header exactly one payload-alignment unit makes the +# first member naturally aligned. +_HEADER_STRUCT = struct.Struct("<8sHHIIIQQ24s") +# magic, version, size, flags, member count, reserved +_INDEX_HEADER_STRUCT = struct.Struct("<8sHHIQQ") +# path byte count, kind, flags, payload offset, payload length, payload digest +_INDEX_ENTRY_STRUCT = struct.Struct(" None: + object.__setattr__( + self, + "logical_path", + normalize_logical_path(self.logical_path), + ) + try: + kind = PacbinMemberKind(self.kind) + except (TypeError, ValueError) as exc: + raise PacbinError(f"unknown pacbin member kind: {self.kind!r}") from exc + object.__setattr__(self, "kind", kind) + + +@dataclass(frozen=True, slots=True) +class PacbinMember: + """Validated metadata for one indexed member.""" + + logical_path: str + kind: PacbinMemberKind + offset: int + length: int + sha256: str + + +@dataclass(frozen=True, slots=True) +class PacbinIndex: + """Validated top-level metadata returned by the writer or reader.""" + + version: int + index_offset: int + index_sha256: str + file_size: int + members: tuple[PacbinMember, ...] + + def member(self, logical_path: str) -> PacbinMember: + """Return one member by normalized logical path.""" + + normalized = normalize_logical_path(logical_path) + for member in self.members: + if member.logical_path == normalized: + return member + raise KeyError(normalized) + + +def normalize_logical_path(value: str) -> str: + """Return the canonical NFC POSIX path used in a pacbin index.""" + + if not isinstance(value, str): + raise TypeError("pacbin logical path must be a string") + if not value or "\x00" in value: + raise PacbinError("pacbin logical path must be non-empty and contain no NUL") + if "\\" in value: + raise PacbinError("pacbin logical path must use POSIX '/' separators") + if value.startswith("/"): + raise PacbinError("pacbin logical path must be relative") + + parts: list[str] = [] + for part in value.split("/"): + if part in {"", "."}: + continue + if part == "..": + raise PacbinError("pacbin logical path must not contain '..'") + parts.append(unicodedata.normalize("NFC", part)) + if not parts: + raise PacbinError("pacbin logical path must name a member") + return "/".join(parts) + + +def write_pacbin( + destination: str | os.PathLike[str] | BinaryIO, + members: Iterable[PacbinMemberSource], + *, + chunk_size: int = _DEFAULT_CHUNK_SIZE, +) -> PacbinIndex: + """Stream ``members`` into one deterministic pacbin-v1 container. + + Member descriptors and index metadata are retained in memory, but payload + contents are copied and hashed in bounded chunks. File-object destinations + must be seekable because the completed index location is written back into + the header. + """ + + chunk_size = _checked_chunk_size(chunk_size) + ordered = _ordered_sources(members) + expected_index_size = _validate_index_bounds(ordered) + with _open_destination(destination) as stream: + _require_seekable(stream, "pacbin destination") + stream.seek(0) + stream.truncate(0) + _write_all(stream, b"\x00" * _HEADER_STRUCT.size, "pacbin header") + + indexed_members: list[PacbinMember] = [] + for source in ordered: + _write_alignment_padding(stream, PACBIN_ALIGNMENT) + offset = _checked_u64_position(stream.tell(), "pacbin member offset") + digest = hashlib.sha256() + length = 0 + with _open_member_source(source.source) as member_stream: + while True: + chunk = member_stream.read(chunk_size) + if not isinstance(chunk, bytes | bytearray | memoryview): + raise TypeError("pacbin member source must return bytes") + if not chunk: + break + if len(chunk) > chunk_size: + raise PacbinError( + "pacbin member source returned more bytes than requested" + ) + if len(chunk) > _MAX_U64 - length: + raise PacbinError("pacbin member length exceeds u64") + length += len(chunk) + digest.update(chunk) + _write_all(stream, chunk, "pacbin member payload") + indexed_members.append( + PacbinMember( + logical_path=source.logical_path, + kind=source.kind, + offset=offset, + length=length, + sha256=digest.hexdigest(), + ) + ) + + _write_alignment_padding(stream, PACBIN_ALIGNMENT) + index_offset = _checked_u64_position(stream.tell(), "pacbin index offset") + index_digest = hashlib.sha256() + _write_hashed( + stream, + index_digest, + _INDEX_HEADER_STRUCT.pack( + _INDEX_MAGIC, + PACBIN_VERSION, + _INDEX_HEADER_STRUCT.size, + _SUPPORTED_FLAGS, + len(indexed_members), + 0, + ), + ) + for member in indexed_members: + path_bytes = member.logical_path.encode("utf-8") + if len(path_bytes) > _MAX_U32: + raise PacbinError("pacbin logical path exceeds u32 byte length") + prefix = _INDEX_ENTRY_STRUCT.pack( + len(path_bytes), + int(member.kind), + _SUPPORTED_FLAGS, + member.offset, + member.length, + bytes.fromhex(member.sha256), + ) + _write_hashed(stream, index_digest, prefix) + _write_hashed(stream, index_digest, path_bytes) + padding = _padding_length(len(prefix) + len(path_bytes), _INDEX_ALIGNMENT) + if padding: + _write_hashed(stream, index_digest, b"\x00" * padding) + + index_sha256 = index_digest.digest() + if stream.tell() - index_offset != expected_index_size: + raise PacbinError("pacbin index size disagrees with preflight") + _write_all( + stream, + _FOOTER_STRUCT.pack( + _FOOTER_MAGIC, + PACBIN_VERSION, + _FOOTER_STRUCT.size, + _SUPPORTED_FLAGS, + index_offset, + len(indexed_members), + index_sha256, + ), + "pacbin footer", + ) + file_size = _checked_u64_position(stream.tell(), "pacbin file size") + stream.seek(0) + _write_all( + stream, + _HEADER_STRUCT.pack( + _HEADER_MAGIC, + PACBIN_VERSION, + _HEADER_STRUCT.size, + _SUPPORTED_FLAGS, + PACBIN_ALIGNMENT, + 0, + index_offset, + len(indexed_members), + b"\x00" * 24, + ), + "pacbin header", + ) + stream.seek(file_size) + stream.truncate() + stream.flush() + + return PacbinIndex( + version=PACBIN_VERSION, + index_offset=index_offset, + index_sha256=index_sha256.hex(), + file_size=file_size, + members=tuple(indexed_members), + ) + + +def write_pacbin_atomic( + destination: str | os.PathLike[str], + members: Iterable[PacbinMemberSource], + *, + chunk_size: int = _DEFAULT_CHUNK_SIZE, + mode: int = 0o644, +) -> PacbinIndex: + """Atomically publish a complete container at ``destination``. + + The container is written, flushed, and synced through a uniquely named + temporary file in the destination directory before ``os.replace`` makes + it visible. Any failure before replacement leaves an existing + destination untouched and removes the temporary file. Directory syncing + is best effort because not every supported filesystem exposes it. + """ + + if not isinstance(mode, int) or isinstance(mode, bool) or not 0 <= mode <= 0o777: + raise PacbinError("pacbin publication mode must be between 0o000 and 0o777") + path = Path(destination) + parent = path.parent + if not parent.is_dir(): + raise PacbinError(f"pacbin destination directory does not exist: {parent}") + + descriptor, temporary_name = tempfile.mkstemp( + prefix=f".{path.name}.", + suffix=".tmp", + dir=parent, + ) + temporary_path = Path(temporary_name) + descriptor_open = True + try: + with os.fdopen(descriptor, "w+b") as stream: + descriptor_open = False + index = write_pacbin(stream, members, chunk_size=chunk_size) + os.chmod(temporary_path, mode) + stream.flush() + os.fsync(stream.fileno()) + os.replace(temporary_path, path) + _fsync_directory_best_effort(parent) + return index + finally: + if descriptor_open: + os.close(descriptor) + with suppress(FileNotFoundError): + temporary_path.unlink() + + +class PacbinReader: + """Strict, bounded-access reader for one pacbin-v1 container.""" + + def __init__( + self, + stream: BinaryIO, + *, + owns_stream: bool, + verify_payloads: bool, + chunk_size: int, + ) -> None: + self._stream = stream + self._owns_stream = owns_stream + self._closed = False + self._chunk_size = _checked_chunk_size(chunk_size) + self._require_readable_seekable() + self.index = self._load_index() + self._members_by_path = { + member.logical_path: member for member in self.index.members + } + if verify_payloads: + self.verify_payloads() + + @classmethod + def open( + cls, + source: str | os.PathLike[str] | BinaryIO, + *, + verify_payloads: bool = True, + chunk_size: int = _DEFAULT_CHUNK_SIZE, + ) -> PacbinReader: + """Open and structurally validate a container. + + Payload digests are verified by default. Passing an existing binary + stream leaves ownership with the caller. + """ + + if isinstance(source, str | os.PathLike): + # The returned reader owns this stream until PacbinReader.close(). + stream = Path(source).open("rb") # noqa: SIM115 + owns_stream = True + else: + stream = source + owns_stream = False + try: + return cls( + stream, + owns_stream=owns_stream, + verify_payloads=verify_payloads, + chunk_size=chunk_size, + ) + except BaseException: + if owns_stream: + stream.close() + raise + + @property + def members(self) -> tuple[PacbinMember, ...]: + """Return members in canonical logical-path order.""" + + return self.index.members + + def member(self, logical_path: str) -> PacbinMember: + """Return metadata for one logical member.""" + + self._ensure_open() + normalized = normalize_logical_path(logical_path) + try: + return self._members_by_path[normalized] + except KeyError as exc: + raise KeyError(normalized) from exc + + def read_member( + self, + logical_path: str, + *, + offset: int = 0, + length: int, + ) -> bytes: + """Read exactly one bounded range relative to a member's start.""" + + self._ensure_open() + member = self.member(logical_path) + if not isinstance(offset, int) or isinstance(offset, bool) or offset < 0: + raise PacbinError("member read offset must be a non-negative integer") + if not isinstance(length, int) or isinstance(length, bool) or length < 0: + raise PacbinError("member read length must be a non-negative integer") + if offset > member.length or length > member.length - offset: + raise PacbinError("member read exceeds indexed payload bounds") + self._stream.seek(member.offset + offset) + return _read_exact(self._stream, length, "member payload") + + def open_member_stream(self, logical_path: str) -> PacbinMemberStream: + """Return a bounded streaming view over one member. + + The returned view never materializes the complete member and remains + valid only while this reader is open. Each read seeks to its indexed + position, so several member views may safely be consumed in any + sequential order while repacking a container. + """ + + self._ensure_open() + return PacbinMemberStream(self, self.member(logical_path)) + + def verify_payloads(self) -> None: + """Stream and verify every indexed member digest.""" + + self._ensure_open() + for member in self.index.members: + digest = hashlib.sha256() + remaining = member.length + self._stream.seek(member.offset) + while remaining: + requested = min(self._chunk_size, remaining) + chunk = self._stream.read(requested) + if not chunk: + raise PacbinError( + f"truncated pacbin member payload: {member.logical_path}" + ) + if not isinstance(chunk, bytes | bytearray | memoryview): + raise TypeError("pacbin member source must return bytes") + if len(chunk) > requested: + raise PacbinError( + f"oversized read for pacbin member: {member.logical_path}" + ) + digest.update(chunk) + remaining -= len(chunk) + if digest.hexdigest() != member.sha256: + raise PacbinError( + f"pacbin member digest mismatch: {member.logical_path}" + ) + + def close(self) -> None: + """Close an owned stream and invalidate further reads.""" + + if self._closed: + return + self._closed = True + if self._owns_stream: + self._stream.close() + + def __enter__(self) -> PacbinReader: + self._ensure_open() + return self + + def __exit__(self, *_args: object) -> None: + self.close() + + def _require_readable_seekable(self) -> None: + _require_seekable(self._stream, "pacbin source") + readable = getattr(self._stream, "readable", None) + if callable(readable) and not readable(): + raise PacbinError("pacbin source must be readable") + + def _load_index(self) -> PacbinIndex: + stream = self._stream + stream.seek(0, os.SEEK_END) + file_size = stream.tell() + if file_size < 0 or file_size > _MAX_U64: + raise PacbinError("pacbin file size exceeds u64") + minimum_size = ( + _HEADER_STRUCT.size + _INDEX_HEADER_STRUCT.size + _FOOTER_STRUCT.size + ) + if file_size < minimum_size: + raise PacbinError("truncated pacbin container") + + stream.seek(0) + header = _HEADER_STRUCT.unpack( + _read_exact(stream, _HEADER_STRUCT.size, "pacbin header") + ) + ( + magic, + version, + header_size, + flags, + alignment, + reserved, + index_offset, + member_count, + reserved_bytes, + ) = header + if magic != _HEADER_MAGIC: + raise PacbinError("invalid pacbin header magic") + _validate_version_size_flags( + "header", version, header_size, _HEADER_STRUCT.size, flags + ) + if alignment != PACBIN_ALIGNMENT: + raise PacbinError(f"unsupported pacbin payload alignment: {alignment}") + if reserved != 0 or reserved_bytes != b"\x00" * 24: + raise PacbinError("pacbin header reserved fields must be zero") + if index_offset % PACBIN_ALIGNMENT: + raise PacbinError("pacbin index offset is not payload-aligned") + + footer_offset = file_size - _FOOTER_STRUCT.size + if index_offset < _HEADER_STRUCT.size or index_offset >= footer_offset: + raise PacbinError("pacbin index offset is out of bounds") + stream.seek(footer_offset) + footer = _FOOTER_STRUCT.unpack( + _read_exact(stream, _FOOTER_STRUCT.size, "pacbin footer") + ) + ( + footer_magic, + footer_version, + footer_size, + footer_flags, + footer_index_offset, + footer_member_count, + expected_index_digest, + ) = footer + if footer_magic != _FOOTER_MAGIC: + raise PacbinError("invalid pacbin footer magic") + _validate_version_size_flags( + "footer", + footer_version, + footer_size, + _FOOTER_STRUCT.size, + footer_flags, + ) + if footer_index_offset != index_offset: + raise PacbinError("pacbin footer index offset disagrees with header") + if footer_member_count != member_count: + raise PacbinError("pacbin footer member count disagrees with header") + + stream.seek(index_offset) + index_digest = hashlib.sha256() + index_header_bytes = _read_exact( + stream, _INDEX_HEADER_STRUCT.size, "pacbin index header" + ) + index_digest.update(index_header_bytes) + ( + index_magic, + index_version, + index_header_size, + index_flags, + index_member_count, + index_reserved, + ) = _INDEX_HEADER_STRUCT.unpack(index_header_bytes) + if index_magic != _INDEX_MAGIC: + raise PacbinError("invalid pacbin index magic") + _validate_version_size_flags( + "index", + index_version, + index_header_size, + _INDEX_HEADER_STRUCT.size, + index_flags, + ) + if index_reserved != 0: + raise PacbinError("pacbin index reserved field must be zero") + if index_member_count != member_count: + raise PacbinError("pacbin index member count disagrees with header") + available_index_bytes = footer_offset - index_offset + if member_count > available_index_bytes // (_INDEX_ENTRY_STRUCT.size + 8): + raise PacbinError("pacbin member count cannot fit in index") + + members: list[PacbinMember] = [] + seen_paths: set[str] = set() + seen_folded_paths: set[str] = set() + previous_path_bytes: bytes | None = None + for _entry_index in range(member_count): + prefix = _read_index_bytes( + stream, + index_digest, + _INDEX_ENTRY_STRUCT.size, + footer_offset, + "pacbin index entry", + ) + path_length, kind_value, entry_flags, offset, length, digest = ( + _INDEX_ENTRY_STRUCT.unpack(prefix) + ) + if entry_flags != _SUPPORTED_FLAGS: + raise PacbinError(f"unknown pacbin member flags: {entry_flags}") + try: + kind = PacbinMemberKind(kind_value) + except ValueError as exc: + raise PacbinError(f"unknown pacbin member kind: {kind_value}") from exc + path_bytes = _read_index_bytes( + stream, + index_digest, + path_length, + footer_offset, + "pacbin member path", + ) + try: + logical_path = path_bytes.decode("utf-8") + except UnicodeDecodeError as exc: + raise PacbinError("pacbin member path is not valid UTF-8") from exc + if normalize_logical_path(logical_path) != logical_path: + raise PacbinError( + f"pacbin member path is not canonical: {logical_path!r}" + ) + folded = logical_path.casefold() + if logical_path in seen_paths: + raise PacbinError(f"duplicate pacbin member path: {logical_path}") + if folded in seen_folded_paths: + raise PacbinError(f"case-colliding pacbin member path: {logical_path}") + if previous_path_bytes is not None and path_bytes <= previous_path_bytes: + raise PacbinError("pacbin index paths are not strictly sorted") + previous_path_bytes = path_bytes + seen_paths.add(logical_path) + seen_folded_paths.add(folded) + padding = _padding_length( + _INDEX_ENTRY_STRUCT.size + path_length, _INDEX_ALIGNMENT + ) + if padding: + padding_bytes = _read_index_bytes( + stream, + index_digest, + padding, + footer_offset, + "pacbin index padding", + ) + if padding_bytes != b"\x00" * padding: + raise PacbinError("pacbin index padding must be zero") + members.append( + PacbinMember( + logical_path=logical_path, + kind=kind, + offset=offset, + length=length, + sha256=digest.hex(), + ) + ) + + if stream.tell() != footer_offset: + raise PacbinError("pacbin index has trailing or missing bytes") + if index_digest.digest() != expected_index_digest: + raise PacbinError("pacbin index digest mismatch") + _validate_canonical_payload_layout(stream, members, index_offset) + return PacbinIndex( + version=version, + index_offset=index_offset, + index_sha256=index_digest.hexdigest(), + file_size=file_size, + members=tuple(members), + ) + + def _ensure_open(self) -> None: + if self._closed: + raise ValueError("I/O operation on closed pacbin reader") + + +class PacbinMemberStream: + """Read-only, bounded view over one member owned by a :class:`PacbinReader`.""" + + def __init__(self, reader: PacbinReader, member: PacbinMember) -> None: + self._reader = reader + self.member = member + self._position = 0 + + def readable(self) -> bool: + return True + + def seekable(self) -> bool: + return False + + def tell(self) -> int: + return self._position + + def read(self, size: int = -1) -> bytes: + self._reader._ensure_open() + if not isinstance(size, int) or isinstance(size, bool): + raise TypeError("pacbin member stream read size must be an integer") + if size < 0: + raise PacbinError("pacbin member stream requires a bounded read size") + remaining = self.member.length - self._position + if size == 0 or remaining == 0: + return b"" + requested = min(size, remaining) + self._reader._stream.seek(self.member.offset + self._position) + payload = _read_exact( + self._reader._stream, + requested, + f"pacbin member {self.member.logical_path!r}", + ) + self._position += len(payload) + return payload + + +def _ordered_sources( + members: Iterable[PacbinMemberSource], +) -> tuple[PacbinMemberSource, ...]: + prepared = tuple(members) + if not all(isinstance(member, PacbinMemberSource) for member in prepared): + raise TypeError("pacbin members must be PacbinMemberSource instances") + ordered = tuple( + sorted(prepared, key=lambda member: member.logical_path.encode("utf-8")) + ) + seen_paths: set[str] = set() + seen_folded_paths: set[str] = set() + for member in ordered: + folded = member.logical_path.casefold() + if member.logical_path in seen_paths: + raise PacbinError(f"duplicate pacbin member path: {member.logical_path}") + if folded in seen_folded_paths: + raise PacbinError( + f"case-colliding pacbin member path: {member.logical_path}" + ) + seen_paths.add(member.logical_path) + seen_folded_paths.add(folded) + return ordered + + +def _validate_index_bounds(members: tuple[PacbinMemberSource, ...]) -> int: + if len(members) > _MAX_U64: + raise PacbinError("pacbin member count exceeds u64") + index_size = _INDEX_HEADER_STRUCT.size + for member in members: + path_length = len(member.logical_path.encode("utf-8")) + if path_length > _MAX_U32: + raise PacbinError("pacbin logical path exceeds u32 byte length") + record_size = _INDEX_ENTRY_STRUCT.size + path_length + record_size += _padding_length(record_size, _INDEX_ALIGNMENT) + if record_size > _MAX_U64 - index_size: + raise PacbinError("pacbin index size exceeds u64") + index_size += record_size + return index_size + + +@contextmanager +def _open_destination( + destination: str | os.PathLike[str] | BinaryIO, +) -> Iterator[BinaryIO]: + if isinstance(destination, str | os.PathLike): + with Path(destination).open("w+b") as stream: + yield stream + else: + yield destination + + +@contextmanager +def _open_member_source( + source: str | os.PathLike[str] | BinaryIO, +) -> Iterator[BinaryIO]: + if isinstance(source, str | os.PathLike): + path = Path(source) + if not path.is_file(): + raise PacbinError(f"pacbin member source is not a file: {path}") + with path.open("rb") as stream: + yield stream + else: + read = getattr(source, "read", None) + if not callable(read): + raise TypeError("pacbin member source must be a path or binary file object") + yield source + + +def _checked_chunk_size(value: int) -> int: + if not isinstance(value, int) or isinstance(value, bool) or value <= 0: + raise PacbinError("pacbin chunk size must be a positive integer") + return value + + +def _require_seekable(stream: BinaryIO, label: str) -> None: + seekable = getattr(stream, "seekable", None) + if not callable(seekable) or not seekable(): + raise PacbinError(f"{label} must be seekable") + + +def _padding_length(position: int, alignment: int) -> int: + return (-position) % alignment + + +def _write_alignment_padding(stream: BinaryIO, alignment: int) -> None: + padding = _padding_length(stream.tell(), alignment) + if padding: + _write_all(stream, b"\x00" * padding, "pacbin payload padding") + + +def _write_hashed(stream: BinaryIO, digest: object, payload: bytes) -> None: + _write_all(stream, payload, "pacbin index") + digest.update(payload) # type: ignore[attr-defined] + + +def _write_all( + stream: BinaryIO, + payload: bytes | bytearray | memoryview, + label: str, +) -> None: + view = memoryview(payload) + written_total = 0 + while written_total < len(view): + written = stream.write(view[written_total:]) + if ( + not isinstance(written, int) + or isinstance(written, bool) + or written <= 0 + or written > len(view) - written_total + ): + raise PacbinError(f"short write while writing {label}") + written_total += written + + +def _checked_u64_position(value: int, label: str) -> int: + if ( + not isinstance(value, int) + or isinstance(value, bool) + or not 0 <= value <= _MAX_U64 + ): + raise PacbinError(f"{label} exceeds u64") + return value + + +def _read_exact(stream: BinaryIO, length: int, label: str) -> bytes: + remaining = length + chunks: list[bytes] = [] + while remaining: + chunk = stream.read(remaining) + if not chunk: + raise PacbinError(f"truncated {label}") + if not isinstance(chunk, bytes | bytearray | memoryview): + raise TypeError(f"{label} source must return bytes") + if len(chunk) > remaining: + raise PacbinError(f"oversized read while reading {label}") + chunks.append(bytes(chunk)) + remaining -= len(chunk) + return b"".join(chunks) + + +def _read_index_bytes( + stream: BinaryIO, + digest: object, + length: int, + footer_offset: int, + label: str, +) -> bytes: + if length < 0 or stream.tell() > footer_offset - length: + raise PacbinError(f"truncated {label}") + payload = _read_exact(stream, length, label) + digest.update(payload) # type: ignore[attr-defined] + return payload + + +def _validate_version_size_flags( + label: str, + version: int, + encoded_size: int, + expected_size: int, + flags: int, +) -> None: + if version != PACBIN_VERSION: + raise PacbinError(f"unsupported pacbin {label} version: {version}") + if encoded_size != expected_size: + raise PacbinError(f"unsupported pacbin {label} size: {encoded_size}") + if flags != _SUPPORTED_FLAGS: + raise PacbinError(f"unknown pacbin {label} flags: {flags}") + + +def _validate_canonical_payload_layout( + stream: BinaryIO, + members: list[PacbinMember], + index_offset: int, +) -> None: + expected_offset = _HEADER_STRUCT.size + for member in members: + if member.length > _MAX_U64 - member.offset: + raise PacbinError( + f"pacbin member range overflows u64: {member.logical_path}" + ) + unaligned_offset = expected_offset + padding_length = _padding_length(unaligned_offset, PACBIN_ALIGNMENT) + expected_offset += padding_length + if member.offset < expected_offset: + raise PacbinError( + f"overlapping pacbin member payload: {member.logical_path}" + ) + if member.offset > expected_offset: + raise PacbinError(f"non-canonical pacbin member gap: {member.logical_path}") + if member.offset % PACBIN_ALIGNMENT: + raise PacbinError(f"unaligned pacbin member payload: {member.logical_path}") + end = member.offset + member.length + if end > index_offset: + raise PacbinError( + f"pacbin member payload is out of bounds: {member.logical_path}" + ) + if padding_length: + stream.seek(unaligned_offset) + if _read_exact(stream, padding_length, "pacbin payload padding") != ( + b"\x00" * padding_length + ): + raise PacbinError("pacbin payload padding must be zero") + expected_offset = end + + payload_end = expected_offset + canonical_index_offset = payload_end + _padding_length( + payload_end, PACBIN_ALIGNMENT + ) + if canonical_index_offset != index_offset: + if canonical_index_offset > index_offset: + raise PacbinError("pacbin payloads overlap the index") + raise PacbinError("pacbin payload region has a non-canonical trailing gap") + padding_length = index_offset - payload_end + if padding_length: + stream.seek(payload_end) + if _read_exact(stream, padding_length, "pacbin payload padding") != ( + b"\x00" * padding_length + ): + raise PacbinError("pacbin payload padding must be zero") + + +def _fsync_directory_best_effort(path: Path) -> None: + flags = os.O_RDONLY | getattr(os, "O_DIRECTORY", 0) + try: + descriptor = os.open(path, flags) + except OSError: + return + try: + with suppress(OSError): + os.fsync(descriptor) + finally: + os.close(descriptor) + + +__all__ = [ + "PACBIN_ALIGNMENT", + "PACBIN_VERSION", + "PacbinError", + "PacbinIndex", + "PacbinMember", + "PacbinMemberKind", + "PacbinMemberSource", + "PacbinMemberStream", + "PacbinReader", + "normalize_logical_path", + "write_pacbin", + "write_pacbin_atomic", +] diff --git a/pyamplicol/src/pyamplicol/generation/fermion_ordering.py b/pyamplicol/src/pyamplicol/generation/fermion_ordering.py new file mode 100644 index 0000000000..48f9e1c1be --- /dev/null +++ b/pyamplicol/src/pyamplicol/generation/fermion_ordering.py @@ -0,0 +1,246 @@ +# SPDX-License-Identifier: 0BSD +"""Generation-time external-fermion ordering signs. + +Numerical current kernels commute their already-evaluated inputs. Wick +contractions do not: every binary current merge must first restore the +canonical source order of its external fermions, and a two-fermion kernel has +one additional sign when its oriented inputs are particle--antiparticle rather +than antiparticle--particle. Folding this exact ``+/-1`` into the generated +coefficient realizes the exterior algebra without adding runtime state or +splitting currents by factorially many Wick lineages. + +Color-connected fundamental open lines already carry this ordering in their +authenticated color-flow construction. This pass therefore owns only +color-singlet fermion sources; applying it to fundamental lines would count +the same exchange sign twice. +""" + +from __future__ import annotations + +from dataclasses import replace + +from ..models.base import Model +from ..processes.ir import CanonicalProcessIR +from .dag_types import CurrentIndex, GenericDAG + + +def _external_fermion_requires_exterior_sign( + statistics: str, + color_role: str, +) -> bool: + """Classify one canonical source without guessing unsupported color roles.""" + + if statistics != "fermion": + return False + if color_role == "singlet": + return True + if color_role in {"fundamental", "antifundamental"}: + return False + raise ValueError( + f"external-fermion ordering does not support fermion color role {color_role!r}" + ) + + +def _fermion_rep_requires_exterior_sign(color_representation: int) -> bool: + """Return sign ownership for a model fermion in the supported UFO scope.""" + + color_representation = int(color_representation) + if color_representation == 1: + return True + if abs(color_representation) == 3: + return False + raise ValueError( + "external-fermion ordering does not support fermion color " + f"representation {color_representation}" + ) + + +def _external_fermion_support_sign( + left_labels: tuple[int, ...], + right_labels: tuple[int, ...], + fermion_source_ranks: dict[int, int], +) -> int: + """Return the Koszul sign for concatenating two disjoint supports.""" + + left_ranks = tuple( + fermion_source_ranks[label] + for label in left_labels + if label in fermion_source_ranks + ) + right_ranks = tuple( + fermion_source_ranks[label] + for label in right_labels + if label in fermion_source_ranks + ) + inversions = sum( + left_rank > right_rank for left_rank in left_ranks for right_rank in right_ranks + ) + return -1 if inversions % 2 else 1 + + +def _fermion_input_orientation_sign( + left_is_fermion: bool, + left_orientation: str, + left_requires_exterior_sign: bool, + right_is_fermion: bool, + right_orientation: str, + right_requires_exterior_sign: bool, +) -> int: + """Return the field-order sign of one oriented two-fermion kernel.""" + + if not (left_is_fermion and right_is_fermion): + return 1 + if left_requires_exterior_sign != right_requires_exterior_sign: + raise ValueError( + "external-fermion ordering encountered mixed color-sign ownership" + ) + if not left_requires_exterior_sign: + return 1 + supported = {"particle", "antiparticle"} + if left_orientation not in supported or right_orientation not in supported: + raise ValueError( + "external-fermion ordering requires non-self-conjugate model orientations" + ) + pair = (left_orientation, right_orientation) + if pair == ("antiparticle", "particle"): + return 1 + if pair == ("particle", "antiparticle"): + return -1 + raise ValueError("external-fermion ordering requires opposite Dirac orientations") + + +class FermionOrderingSign: + """Cached model/process contract for cold DAG construction.""" + + __slots__ = ( + "_merge_cache", + "_model", + "_particle_contract_cache", + "_source_ranks", + ) + + def __init__(self, process: CanonicalProcessIR, model: Model) -> None: + self._model = model + source_ranks: dict[int, int] = {} + for source_slot, leg in enumerate(process.legs): + if _external_fermion_requires_exterior_sign( + leg.statistics, + leg.color_role, + ): + source_ranks[int(leg.label)] = source_slot + self._source_ranks = source_ranks + self._particle_contract_cache: dict[int, tuple[bool, str, bool]] = {} + self._merge_cache: dict[ + tuple[tuple[int, ...], tuple[int, ...], int, int], int + ] = {} + + @property + def active(self) -> bool: + """Whether this process has any source whose sign is owned here.""" + + return bool(self._source_ranks) + + def _particle_contract(self, particle_id: int) -> tuple[bool, str, bool]: + particle_id = int(particle_id) + cached = self._particle_contract_cache.get(particle_id) + if cached is not None: + return cached + is_fermion = bool(self._model.is_fermion(particle_id)) + orientation = ( + str(self._model.source_orientation(particle_id)) + if is_fermion + else "self-conjugate" + ) + requires_exterior_sign = ( + _fermion_rep_requires_exterior_sign(self._model.color_rep(particle_id)) + if is_fermion + else False + ) + result = (is_fermion, orientation, requires_exterior_sign) + self._particle_contract_cache[particle_id] = result + return result + + def merge_sign(self, left: CurrentIndex, right: CurrentIndex) -> int: + key = ( + left.external_labels, + right.external_labels, + int(left.particle_id), + int(right.particle_id), + ) + cached = self._merge_cache.get(key) + if cached is not None: + return cached + ( + left_is_fermion, + left_orientation, + left_requires_exterior_sign, + ) = self._particle_contract(left.particle_id) + ( + right_is_fermion, + right_orientation, + right_requires_exterior_sign, + ) = self._particle_contract(right.particle_id) + sign = _external_fermion_support_sign( + left.external_labels, + right.external_labels, + self._source_ranks, + ) + sign *= _fermion_input_orientation_sign( + left_is_fermion, + left_orientation, + left_requires_exterior_sign, + right_is_fermion, + right_orientation, + right_requires_exterior_sign, + ) + self._merge_cache[key] = sign + return sign + + +def apply_fermion_ordering_signs(dag: GenericDAG, model: Model) -> GenericDAG: + """Fold every merge sign into existing DAG constants exactly once.""" + + if not dag.interactions and not dag.amplitude_roots: + return dag + ordering = FermionOrderingSign(dag.process, model) + if not ordering.active: + return dag + currents = dag.currents + + def signed_weight( + weight: tuple[float, float], left_id: int, right_id: int + ) -> tuple[float, float]: + sign = ordering.merge_sign( + currents[left_id].index, + currents[right_id].index, + ) + return weight if sign == 1 else (-weight[0], -weight[1]) + + def signed_interaction(interaction): + color_weight = signed_weight( + interaction.color_weight, + interaction.left_id, + interaction.right_id, + ) + if color_weight is interaction.color_weight: + return interaction + return replace(interaction, color_weight=color_weight) + + def signed_root(root): + color_weight = signed_weight( + root.color_weight, + root.left_id, + root.right_id, + ) + if color_weight is root.color_weight: + return root + return replace(root, color_weight=color_weight) + + return replace( + dag, + interactions=tuple(signed_interaction(row) for row in dag.interactions), + amplitude_roots=tuple(signed_root(row) for row in dag.amplitude_roots), + ) + + +__all__ = ["FermionOrderingSign", "apply_fermion_ordering_signs"] diff --git a/pyamplicol/src/pyamplicol/generation/helicity_materialization.py b/pyamplicol/src/pyamplicol/generation/helicity_materialization.py new file mode 100644 index 0000000000..bfa7456cea --- /dev/null +++ b/pyamplicol/src/pyamplicol/generation/helicity_materialization.py @@ -0,0 +1,652 @@ +# SPDX-License-Identifier: 0BSD +"""Materialize one compiled recurrence representative per helicity proof class.""" + +from __future__ import annotations + +import math +from collections import defaultdict +from collections.abc import Mapping +from dataclasses import dataclass, replace + +from .dag_types import AmplitudeRoot, GenericDAG, InteractionNode +from .helicity_replay import HelicityRecurrencePlan + +_Weight = tuple[float, float] + + +@dataclass(frozen=True, slots=True) +class MaterializedSourceRoute: + materialized_current_id: int + external_label: int + helicity: int + chirality: int + spin_state: int | tuple[int, ...] + declared_state_index: int + selector_domain_id: int + factor: _Weight + + def to_runtime_manifest(self) -> dict[str, object]: + spin_state: object = self.spin_state + if isinstance(spin_state, tuple): + spin_state = list(spin_state) + return { + "materialized_current_id": self.materialized_current_id, + "external_label": self.external_label, + "helicity": self.helicity, + "chirality": self.chirality, + "spin_state": spin_state, + "declared_state_index": self.declared_state_index, + "selector_domain_id": self.selector_domain_id, + "factor": list(self.factor), + } + + +@dataclass(frozen=True, slots=True) +class MaterializedAmplitudeRoute: + materialized_root_id: int + selector_domain_ids: tuple[int, ...] + factor: _Weight + residual: bool = False + + def to_runtime_manifest(self) -> dict[str, object]: + return { + "materialized_root_id": self.materialized_root_id, + "selector_domain_ids": list(self.selector_domain_ids), + "factor": list(self.factor), + "residual": self.residual, + } + + +@dataclass(frozen=True, slots=True) +class MaterializedSelectorSchedule: + selector_domain_id: int + active_current_ids: tuple[int, ...] + active_root_ids: tuple[int, ...] + structural_zero: bool + + def to_runtime_manifest(self) -> dict[str, object]: + return { + "selector_domain_id": self.selector_domain_id, + "active_current_ids": list(self.active_current_ids), + "active_root_ids": list(self.active_root_ids), + "structural_zero": self.structural_zero, + } + + +@dataclass(frozen=True, slots=True) +class HelicityMaterialization: + strategy: str + dag: GenericDAG + proof_current_count: int + proof_root_count: int + proof_to_materialized_current: tuple[int, ...] + source_routes: tuple[MaterializedSourceRoute, ...] + amplitude_routes: tuple[MaterializedAmplitudeRoute, ...] + selector_schedules: tuple[MaterializedSelectorSchedule, ...] + + @property + def materialized_current_count(self) -> int: + return len(self.dag.currents) + + @property + def materialized_root_count(self) -> int: + return len(self.dag.amplitude_roots) + + def to_runtime_manifest(self) -> dict[str, object]: + return { + "kind": "pyamplicol-helicity-recurrence-materialization", + "contract_version": 1, + "strategy": self.strategy, + "proof_current_count": self.proof_current_count, + "proof_root_count": self.proof_root_count, + "materialized_current_count": self.materialized_current_count, + "materialized_root_count": self.materialized_root_count, + "proof_to_materialized_current": list(self.proof_to_materialized_current), + "source_routes": [ + route.to_runtime_manifest() for route in self.source_routes + ], + "amplitude_routes": [ + route.to_runtime_manifest() for route in self.amplitude_routes + ], + "selector_schedules": [ + schedule.to_runtime_manifest() for schedule in self.selector_schedules + ], + } + + +def materialize_helicity_recurrence( + dag: GenericDAG, + plan: HelicityRecurrencePlan, +) -> HelicityMaterialization: + """Build the quotient DAG certified by ``plan``. + + Every proven recurrence class contributes exactly one current. Residual + currents remain one-to-one. Parent recurrence factors are folded into the + remapped interaction coefficient, so compiled kernels see ordinary current + slots and require no selector-dependent branches. + """ + + if plan.current_count != len(dag.currents): + raise ValueError("helicity proof current count does not match the DAG") + if plan.amplitude_root_count != len(dag.amplitude_roots): + raise ValueError("helicity proof root count does not match the DAG") + + class_by_current: dict[int, tuple[object, _Weight]] = {} + representative_factor_by_class: dict[str, _Weight] = {} + target_old_ids: set[int] = set(plan.residual_current_ids) + for recurrence in plan.recurrence_classes: + representative = next( + member + for member in recurrence.members + if member.current_id == recurrence.representative_current_id + ) + representative_factor_by_class[recurrence.class_id] = representative.factor + target_old_ids.add(recurrence.representative_current_id) + for member in recurrence.members: + class_by_current[member.current_id] = (recurrence, member.factor) + + target_order = tuple( + sorted( + target_old_ids, + key=lambda current_id: ( + len(dag.currents[current_id].index.external_labels), + current_id, + ), + ) + ) + target_new_id = {old_id: new_id for new_id, old_id in enumerate(target_order)} + + proof_to_materialized: list[int] = [-1] * len(dag.currents) + factor_by_proof_current: list[_Weight] = [(1.0, 0.0)] * len(dag.currents) + materialized_factor_by_proof_current: list[_Weight] = [(1.0, 0.0)] * len( + dag.currents + ) + residual_ids = set(plan.residual_current_ids) + for current in dag.currents: + if current.id in residual_ids: + proof_to_materialized[current.id] = target_new_id[current.id] + continue + recurrence, factor = class_by_current[current.id] + representative_id = recurrence.representative_current_id + proof_to_materialized[current.id] = target_new_id[representative_id] + factor_by_proof_current[current.id] = factor + materialized_factor_by_proof_current[current.id] = ( + representative_factor_by_class[recurrence.class_id] + ) + if any(current_id < 0 for current_id in proof_to_materialized): + raise ValueError("helicity quotient left a proof current unmapped") + + currents = tuple( + replace(dag.currents[old_id], id=new_id) + for new_id, old_id in enumerate(target_order) + ) + interactions_by_result: dict[int, list[InteractionNode]] = defaultdict(list) + for interaction in dag.interactions: + interactions_by_result[interaction.result_id].append(interaction) + interactions: list[InteractionNode] = [] + for old_result_id in target_order: + new_result_id = target_new_id[old_result_id] + for interaction in interactions_by_result[old_result_id]: + left_ratio = _divide_weight( + factor_by_proof_current[interaction.left_id], + materialized_factor_by_proof_current[interaction.left_id], + ) + right_ratio = _divide_weight( + factor_by_proof_current[interaction.right_id], + materialized_factor_by_proof_current[interaction.right_id], + ) + interactions.append( + replace( + interaction, + id=len(interactions), + left_id=proof_to_materialized[interaction.left_id], + right_id=proof_to_materialized[interaction.right_id], + result_id=new_result_id, + color_weight=_multiply_weight( + interaction.color_weight, + _multiply_weight(left_ratio, right_ratio), + ), + ) + ) + + roots: list[AmplitudeRoot] = [] + amplitude_routes: list[MaterializedAmplitudeRoute] = [] + retained_roots: list[tuple[AmplitudeRoot, object | None]] = [] + for recurrence in plan.amplitude_classes: + representative = next( + member + for member in recurrence.members + if member.root_id == recurrence.representative_root_id + ) + retained_roots.append( + (dag.amplitude_roots[recurrence.representative_root_id], recurrence) + ) + materialized_root_id = len(retained_roots) - 1 + for member in recurrence.members: + amplitude_routes.append( + MaterializedAmplitudeRoute( + materialized_root_id=materialized_root_id, + selector_domain_ids=member.selector_domain_ids, + factor=_divide_weight(member.factor, representative.factor), + ) + ) + for root_id in plan.residual_root_ids: + retained_roots.append((dag.amplitude_roots[root_id], None)) + + domain_by_state = { + domain.source_states: domain.id for domain in plan.selector_domains + } + source_state_by_bit = _source_state_by_ancestry_bit(dag) + for root, recurrence in retained_roots: + left_ratio = _divide_weight( + factor_by_proof_current[root.left_id], + materialized_factor_by_proof_current[root.left_id], + ) + right_ratio = _divide_weight( + factor_by_proof_current[root.right_id], + materialized_factor_by_proof_current[root.right_id], + ) + materialized_root_id = len(roots) + roots.append( + replace( + root, + id=materialized_root_id, + left_id=proof_to_materialized[root.left_id], + right_id=proof_to_materialized[root.right_id], + color_weight=_multiply_weight( + root.color_weight, + _multiply_weight(left_ratio, right_ratio), + ), + helicity_weight=1.0, + ) + ) + if recurrence is None: + selector_states = _residual_root_selector_states( + dag, + root, + source_state_by_bit, + ) + selector_domain_ids = tuple( + domain_by_state[state] + for state in selector_states + if state in domain_by_state + ) + if len(selector_domain_ids) != len(selector_states): + raise ValueError( + "residual amplitude root has no runtime selector domain" + ) + amplitude_routes.append( + MaterializedAmplitudeRoute( + materialized_root_id=materialized_root_id, + selector_domain_ids=selector_domain_ids, + factor=(1.0, 0.0), + residual=True, + ) + ) + + source_routes: list[MaterializedSourceRoute] = [] + for mapping in plan.source_state_mappings: + recurrence, _factor = class_by_current[mapping.current_id] + representative_factor = representative_factor_by_class[recurrence.class_id] + source_routes.append( + MaterializedSourceRoute( + materialized_current_id=proof_to_materialized[mapping.current_id], + external_label=mapping.external_label, + helicity=mapping.helicity, + chirality=mapping.chirality, + spin_state=mapping.spin_state, + declared_state_index=mapping.declared_state_index, + selector_domain_id=mapping.selector_domain_id, + factor=_divide_weight(mapping.factor, representative_factor), + ) + ) + + materialized = GenericDAG( + process=dag.process, + color_plan=dag.color_plan, + currents=currents, + sources=tuple( + target_new_id[old_id] + for old_id in target_order + if dag.currents[old_id].is_source + ), + interactions=tuple(interactions), + amplitude_roots=tuple(roots), + truncated=dag.truncated, + helicity_coverage=dag.helicity_coverage, + color_coverage=dag.color_coverage, + selected_source_helicities=dag.selected_source_helicities, + selected_color_sector_ids=dag.selected_color_sector_ids, + lc_topology_replay=dag.lc_topology_replay, + color_topology_replay=dag.color_topology_replay, + ) + routes_by_domain: dict[int, set[int]] = defaultdict(set) + for route in amplitude_routes: + for domain_id in route.selector_domain_ids: + routes_by_domain[domain_id].add(route.materialized_root_id) + interactions_by_materialized_result: dict[int, list[InteractionNode]] = defaultdict( + list + ) + for interaction in interactions: + interactions_by_materialized_result[interaction.result_id].append(interaction) + compiled_dependencies = _compiled_representative_dependencies(materialized) + structural_zeros = set(plan.structural_zero_selector_domain_ids) + selector_schedules: list[MaterializedSelectorSchedule] = [] + for domain in plan.selector_domains: + if not domain.complete: + continue + active_roots = tuple(sorted(routes_by_domain.get(domain.id, set()))) + active_currents = _active_current_closure( + materialized, + active_roots, + interactions_by_materialized_result, + compiled_dependencies=compiled_dependencies, + ) + structural_zero = domain.id in structural_zeros + if structural_zero != (not active_roots): + raise ValueError( + "helicity selector schedule disagrees with structural-zero proof" + ) + selector_schedules.append( + MaterializedSelectorSchedule( + selector_domain_id=domain.id, + active_current_ids=active_currents, + active_root_ids=active_roots, + structural_zero=structural_zero, + ) + ) + return HelicityMaterialization( + strategy="quotient", + dag=materialized, + proof_current_count=len(dag.currents), + proof_root_count=len(dag.amplitude_roots), + proof_to_materialized_current=tuple(proof_to_materialized), + source_routes=tuple(source_routes), + amplitude_routes=tuple(amplitude_routes), + selector_schedules=tuple(selector_schedules), + ) + + +def retain_helicity_recurrence_graph( + dag: GenericDAG, + plan: HelicityRecurrencePlan, +) -> HelicityMaterialization: + """Attach selector schedules while retaining the shared proof DAG. + + Compiled summed-helicity execution benefits from common subexpressions + between physical helicities. Quotienting the graph to one current per + recurrence class removes that sharing and requires repeated evaluator + calls. This form keeps every proof current for the fused summed-axis hot + path, while selector schedules identify the exact chunk closure needed by + a runtime-selected helicity. + """ + + if plan.current_count != len(dag.currents): + raise ValueError("helicity proof current count does not match the DAG") + if plan.amplitude_root_count != len(dag.amplitude_roots): + raise ValueError("helicity proof root count does not match the DAG") + + amplitude_routes: list[MaterializedAmplitudeRoute] = [] + routed_root_ids: set[int] = set() + for recurrence in plan.amplitude_classes: + for member in recurrence.members: + if member.root_id in routed_root_ids: + raise ValueError("helicity proof amplitude root is routed twice") + routed_root_ids.add(member.root_id) + root_weight = float(dag.amplitude_roots[member.root_id].helicity_weight) + if not math.isfinite(root_weight) or root_weight <= 0.0: + raise ValueError( + "retained amplitude root has an invalid helicity weight" + ) + amplitude_routes.append( + MaterializedAmplitudeRoute( + materialized_root_id=member.root_id, + selector_domain_ids=member.selector_domain_ids, + # The fused sum keeps the root's physical-helicity weight. + # Selecting one member must remove that multiplicity before + # the amplitude is squared. + factor=(1.0 / math.sqrt(root_weight), 0.0), + ) + ) + + domain_by_state = { + domain.source_states: domain.id for domain in plan.selector_domains + } + source_state_by_bit = _source_state_by_ancestry_bit(dag) + for root_id in plan.residual_root_ids: + if root_id in routed_root_ids: + raise ValueError("residual amplitude root is also proof-routed") + root = dag.amplitude_roots[root_id] + selector_states = _residual_root_selector_states( + dag, + root, + source_state_by_bit, + ) + selector_domain_ids = tuple( + domain_by_state[state] + for state in selector_states + if state in domain_by_state + ) + if len(selector_domain_ids) != len(selector_states): + raise ValueError("residual amplitude root has no runtime selector domain") + routed_root_ids.add(root_id) + root_weight = float(root.helicity_weight) + if not math.isfinite(root_weight) or root_weight <= 0.0: + raise ValueError("retained residual root has an invalid helicity weight") + amplitude_routes.append( + MaterializedAmplitudeRoute( + materialized_root_id=root_id, + selector_domain_ids=selector_domain_ids, + factor=(1.0 / math.sqrt(root_weight), 0.0), + residual=True, + ) + ) + if routed_root_ids != set(range(len(dag.amplitude_roots))): + raise ValueError("helicity proof does not route every retained amplitude root") + + source_routes = tuple( + MaterializedSourceRoute( + materialized_current_id=mapping.current_id, + external_label=mapping.external_label, + helicity=mapping.helicity, + chirality=mapping.chirality, + spin_state=mapping.spin_state, + declared_state_index=mapping.declared_state_index, + selector_domain_id=mapping.selector_domain_id, + factor=(1.0, 0.0), + ) + for mapping in plan.source_state_mappings + ) + + routes_by_domain: dict[int, set[int]] = defaultdict(set) + for route in amplitude_routes: + for domain_id in route.selector_domain_ids: + routes_by_domain[domain_id].add(route.materialized_root_id) + interactions_by_result: dict[int, list[InteractionNode]] = defaultdict(list) + for interaction in dag.interactions: + interactions_by_result[interaction.result_id].append(interaction) + compiled_dependencies = _compiled_representative_dependencies(dag) + structural_zeros = set(plan.structural_zero_selector_domain_ids) + selector_schedules: list[MaterializedSelectorSchedule] = [] + for domain in plan.selector_domains: + if not domain.complete: + continue + active_roots = tuple(sorted(routes_by_domain.get(domain.id, set()))) + active_currents = _active_current_closure( + dag, + active_roots, + interactions_by_result, + compiled_dependencies=compiled_dependencies, + ) + structural_zero = domain.id in structural_zeros + if structural_zero != (not active_roots): + raise ValueError( + "helicity selector schedule disagrees with structural-zero proof" + ) + selector_schedules.append( + MaterializedSelectorSchedule( + selector_domain_id=domain.id, + active_current_ids=active_currents, + active_root_ids=active_roots, + structural_zero=structural_zero, + ) + ) + + return HelicityMaterialization( + strategy="retained-proof-graph", + dag=dag, + proof_current_count=len(dag.currents), + proof_root_count=len(dag.amplitude_roots), + proof_to_materialized_current=tuple(range(len(dag.currents))), + source_routes=source_routes, + amplitude_routes=tuple( + sorted(amplitude_routes, key=lambda route: route.materialized_root_id) + ), + selector_schedules=tuple(selector_schedules), + ) + + +def _active_current_closure( + dag: GenericDAG, + root_ids: tuple[int, ...], + interactions_by_result: dict[int, list[InteractionNode]], + *, + compiled_dependencies: Mapping[int, tuple[int, ...]] | None = None, +) -> tuple[int, ...]: + active: set[int] = set() + pending: list[int] = [] + for root_id in root_ids: + root = dag.amplitude_roots[root_id] + pending.extend((root.left_id, root.right_id)) + while pending: + current_id = pending.pop() + if current_id in active: + continue + active.add(current_id) + for interaction in interactions_by_result.get(current_id, ()): + pending.extend((interaction.left_id, interaction.right_id)) + if compiled_dependencies is not None: + pending.extend(compiled_dependencies.get(current_id, ())) + return tuple(sorted(active)) + + +def _compiled_representative_dependencies( + dag: GenericDAG, +) -> dict[int, tuple[int, ...]]: + """Return the concrete parents read after evaluation-group reuse. + + A compiled stage caches the first interaction in each evaluation group and + reuses that expression for later group members. Selector schedules must + therefore retain the representative interaction's parents in addition to + each current's physical DAG parents. + """ + + interactions_by_stage: dict[int, dict[int, list[InteractionNode]]] = {} + for interaction in dag.interactions: + result_id = int(interaction.result_id) + subset_size = len(dag.currents[result_id].index.external_labels) + interactions_by_stage.setdefault(subset_size, {}).setdefault( + result_id, [] + ).append(interaction) + + dependencies: dict[int, set[int]] = {} + for results in interactions_by_stage.values(): + representative_by_group: dict[int, InteractionNode] = {} + for result_id in sorted(results): + for interaction in results[result_id]: + group_id = interaction.evaluation_group_id + representative = interaction + if group_id is not None: + representative = representative_by_group.setdefault( + int(group_id), interaction + ) + dependencies.setdefault(result_id, set()).update( + (int(representative.left_id), int(representative.right_id)) + ) + return { + current_id: tuple(sorted(parent_ids)) + for current_id, parent_ids in dependencies.items() + } + + +def _source_state_by_ancestry_bit( + dag: GenericDAG, +) -> dict[int, tuple[int, int]]: + result: dict[int, tuple[int, int]] = {} + for current in dag.currents: + if not current.is_source: + continue + ancestry = int(current.index.helicity_ancestry) + if ancestry <= 0 or ancestry & (ancestry - 1): + continue + if current.source_leg_label is None or current.source_helicity is None: + continue + result[ancestry.bit_length() - 1] = ( + int(current.source_leg_label), + int(current.source_helicity), + ) + return result + + +def _root_selector_state( + dag: GenericDAG, + root: AmplitudeRoot, + source_state_by_bit: dict[int, tuple[int, int]], +) -> tuple[tuple[int, int], ...]: + ancestry = int( + dag.currents[root.left_id].index.helicity_ancestry + | dag.currents[root.right_id].index.helicity_ancestry + ) + state: dict[int, int] = {} + while ancestry: + bit = ancestry & -ancestry + selector = source_state_by_bit.get(bit.bit_length() - 1) + if selector is None: + raise ValueError("residual root has an unknown source ancestry bit") + label, helicity = selector + previous = state.setdefault(label, helicity) + if previous != helicity: + raise ValueError("residual root selects two helicities for one leg") + ancestry ^= bit + return tuple(sorted(state.items())) + + +def _residual_root_selector_states( + dag: GenericDAG, + root: AmplitudeRoot, + source_state_by_bit: dict[int, tuple[int, int]], +) -> tuple[tuple[tuple[int, int], ...], ...]: + selector_state = _root_selector_state(dag, root, source_state_by_bit) + selector_states = [selector_state] + if math.isclose(root.helicity_weight, 2.0, rel_tol=1.0e-12, abs_tol=1.0e-12): + flipped = tuple((label, -helicity) for label, helicity in selector_state) + if flipped != selector_state: + selector_states.append(flipped) + elif not math.isclose( + root.helicity_weight, + 1.0, + rel_tol=1.0e-12, + abs_tol=1.0e-12, + ): + raise ValueError( + "residual amplitude root has unsupported helicity multiplicity " + f"{root.helicity_weight}" + ) + return tuple(selector_states) + + +def _multiply_weight(left: _Weight, right: _Weight) -> _Weight: + value = complex(*left) * complex(*right) + if not math.isfinite(value.real) or not math.isfinite(value.imag): + raise ValueError("helicity replay produced a non-finite weight") + return (float(value.real), float(value.imag)) + + +def _divide_weight(numerator: _Weight, denominator: _Weight) -> _Weight: + denominator_value = complex(*denominator) + if denominator_value == 0j: + raise ValueError("helicity replay representative factor is zero") + value = complex(*numerator) / denominator_value + if not math.isfinite(value.real) or not math.isfinite(value.imag): + raise ValueError("helicity replay produced a non-finite factor ratio") + return (float(value.real), float(value.imag)) diff --git a/pyamplicol/src/pyamplicol/generation/helicity_replay.py b/pyamplicol/src/pyamplicol/generation/helicity_replay.py new file mode 100644 index 0000000000..96f7277c61 --- /dev/null +++ b/pyamplicol/src/pyamplicol/generation/helicity_replay.py @@ -0,0 +1,1330 @@ +# SPDX-License-Identifier: 0BSD +"""Proof-gated reusable-helicity recurrence metadata. + +This module derives the model-generic contract describing which currents have +the same recurrence shape once runtime source states are treated as inputs. +Compiled generation retains the shared summed-helicity graph and attaches the +certified selector closures to it. Rusticol routes explicit runtime helicity +selectors through those closures; independently unproven currents remain +localized residuals rather than disabling proven reuse classes. +""" + +from __future__ import annotations + +import hashlib +import json +import math +from collections import defaultdict +from collections.abc import Mapping, Sequence +from dataclasses import dataclass +from itertools import product +from math import fsum +from typing import TypeAlias + +from ..models.base import Model +from .contracts import runtime_coupling_parameter_names +from .dag_types import AmplitudeRoot, CurrentNode, GenericDAG, InteractionNode + +HELICITY_RECURRENCE_CONTRACT_VERSION = 1 +HELICITY_RECURRENCE_PROOF_ALGORITHM = ( + "canonical-source-transition-dependency-shape-v1" +) +RUNTIME_SELECTOR_PROVENANCE = "pyamplicol-runtime-selectors-v1" + +_ComplexWeight: TypeAlias = tuple[float, float] +_SelectorState: TypeAlias = tuple[tuple[int, int], ...] +_ColorSectorAliases: TypeAlias = Mapping[int, int] + + +@dataclass(frozen=True, slots=True) +class HelicitySelectorDomain: + """One partial or complete assignment of external source helicities.""" + + id: int + source_states: _SelectorState + complete: bool + + def to_runtime_manifest(self) -> dict[str, object]: + return { + "id": self.id, + "complete": self.complete, + "source_states": [ + {"external_label": label, "helicity": helicity} + for label, helicity in self.source_states + ], + } + + +@dataclass(frozen=True, slots=True) +class HelicityCurrentReplayMember: + current_id: int + selector_domain_id: int + factor: _ComplexWeight = (1.0, 0.0) + + def to_runtime_manifest(self) -> dict[str, object]: + return { + "current_id": self.current_id, + "selector_domain_id": self.selector_domain_id, + "factor": list(self.factor), + } + + +@dataclass(frozen=True, slots=True) +class HelicityRecurrenceClass: + """Currents sharing one exact source/transition dependency shape.""" + + class_id: str + representative_current_id: int + external_labels: tuple[int, ...] + source_class: bool + members: tuple[HelicityCurrentReplayMember, ...] + proof_digest: str + transition_contract_ids: tuple[str, ...] = () + + def __post_init__(self) -> None: + if not self.class_id: + raise ValueError("helicity recurrence class id must not be empty") + if not self.members: + raise ValueError("helicity recurrence class must have members") + if self.representative_current_id not in { + member.current_id for member in self.members + }: + raise ValueError("helicity recurrence representative is not a member") + _validate_digest(self.proof_digest, context="helicity recurrence proof") + + @property + def optimized(self) -> bool: + return len(self.members) > 1 + + def to_runtime_manifest(self) -> dict[str, object]: + return { + "class_id": self.class_id, + "representative_current_id": self.representative_current_id, + "external_labels": list(self.external_labels), + "source_class": self.source_class, + "members": [member.to_runtime_manifest() for member in self.members], + "proof": { + "status": "proven", + "algorithm": HELICITY_RECURRENCE_PROOF_ALGORITHM, + "digest": self.proof_digest, + "transition_contract_ids": list(self.transition_contract_ids), + }, + } + + +@dataclass(frozen=True, slots=True) +class HelicitySourceStateMapping: + current_id: int + external_label: int + helicity: int + chirality: int + spin_state: int | tuple[int, ...] + declared_state_index: int + selector_domain_id: int + recurrence_class_id: str + representative_current_id: int + source_contract_digest: str + factor: _ComplexWeight = (1.0, 0.0) + + def __post_init__(self) -> None: + _validate_digest( + self.source_contract_digest, + context="helicity source contract", + ) + + def to_runtime_manifest(self) -> dict[str, object]: + spin_state: object = self.spin_state + if isinstance(spin_state, tuple): + spin_state = list(spin_state) + return { + "current_id": self.current_id, + "external_label": self.external_label, + "helicity": self.helicity, + "chirality": self.chirality, + "spin_state": spin_state, + "declared_state_index": self.declared_state_index, + "selector_domain_id": self.selector_domain_id, + "recurrence_class_id": self.recurrence_class_id, + "representative_current_id": self.representative_current_id, + "source_contract_digest": self.source_contract_digest, + "factor": list(self.factor), + } + + +@dataclass(frozen=True, slots=True) +class HelicityAmplitudeReplayMember: + root_id: int + selector_domain_ids: tuple[int, ...] + factor: _ComplexWeight = (1.0, 0.0) + + def to_runtime_manifest(self) -> dict[str, object]: + return { + "root_id": self.root_id, + "selector_domain_ids": list(self.selector_domain_ids), + "factor": list(self.factor), + } + + +@dataclass(frozen=True, slots=True) +class HelicityAmplitudeReplayClass: + class_id: str + representative_root_id: int + members: tuple[HelicityAmplitudeReplayMember, ...] + proof_digest: str + transition_contract_ids: tuple[str, ...] = () + + def __post_init__(self) -> None: + if not self.members: + raise ValueError("helicity amplitude recurrence class must have members") + if self.representative_root_id not in { + member.root_id for member in self.members + }: + raise ValueError("helicity amplitude representative is not a member") + _validate_digest(self.proof_digest, context="helicity amplitude proof") + + @property + def optimized(self) -> bool: + return len(self.members) > 1 + + def to_runtime_manifest(self) -> dict[str, object]: + return { + "class_id": self.class_id, + "representative_root_id": self.representative_root_id, + "members": [member.to_runtime_manifest() for member in self.members], + "proof": { + "status": "proven", + "algorithm": HELICITY_RECURRENCE_PROOF_ALGORITHM, + "digest": self.proof_digest, + "transition_contract_ids": list(self.transition_contract_ids), + }, + } + + +@dataclass(frozen=True, slots=True) +class HelicityRecurrencePlan: + """Strict additive contract for reusable complete-helicity artifacts.""" + + current_count: int + amplitude_root_count: int + selector_domains: tuple[HelicitySelectorDomain, ...] + source_state_mappings: tuple[HelicitySourceStateMapping, ...] + recurrence_classes: tuple[HelicityRecurrenceClass, ...] + amplitude_classes: tuple[HelicityAmplitudeReplayClass, ...] + residual_current_ids: tuple[int, ...] + residual_root_ids: tuple[int, ...] + structural_zero_selector_domain_ids: tuple[int, ...] + diagnostics: tuple[str, ...] = () + + def __post_init__(self) -> None: + if self.current_count < 0 or self.amplitude_root_count < 0: + raise ValueError("helicity recurrence object counts must be nonnegative") + domain_ids = tuple(domain.id for domain in self.selector_domains) + if domain_ids != tuple(range(len(domain_ids))): + raise ValueError("helicity selector domain ids must be contiguous") + known_domains = set(domain_ids) + referenced_domains = { + member.selector_domain_id + for recurrence in self.recurrence_classes + for member in recurrence.members + } | { + domain_id + for recurrence in self.amplitude_classes + for member in recurrence.members + for domain_id in member.selector_domain_ids + } | set(self.structural_zero_selector_domain_ids) + if not referenced_domains <= known_domains: + raise ValueError("helicity replay references an unknown selector domain") + class_current_ids = { + member.current_id + for recurrence in self.recurrence_classes + for member in recurrence.members + } + if class_current_ids & set(self.residual_current_ids): + raise ValueError("helicity recurrence classes overlap residual currents") + if class_current_ids | set(self.residual_current_ids) != set( + range(self.current_count) + ): + raise ValueError( + "helicity recurrence current ids do not cover the final dense DAG" + ) + class_root_ids = { + member.root_id + for recurrence in self.amplitude_classes + for member in recurrence.members + } + if class_root_ids & set(self.residual_root_ids): + raise ValueError("helicity amplitude classes overlap residual roots") + if class_root_ids | set(self.residual_root_ids) != set( + range(self.amplitude_root_count) + ): + raise ValueError( + "helicity recurrence root ids do not cover the final dense DAG" + ) + source_ids = {mapping.current_id for mapping in self.source_state_mappings} + if not source_ids <= class_current_ids: + raise ValueError("helicity source mapping refers to a residual current") + + @property + def optimized_class_count(self) -> int: + return sum(item.optimized for item in self.recurrence_classes) + + @property + def optimized_current_count(self) -> int: + return sum( + len(item.members) for item in self.recurrence_classes if item.optimized + ) + + @property + def optimized_amplitude_class_count(self) -> int: + return sum(item.optimized for item in self.amplitude_classes) + + @property + def physical_helicity_count(self) -> int: + return sum(domain.complete for domain in self.selector_domains) + + def proof_counts(self) -> dict[str, int]: + return { + "recurrence_class_count": len(self.recurrence_classes), + "optimized_recurrence_class_count": self.optimized_class_count, + "optimized_current_count": self.optimized_current_count, + "residual_current_count": len(self.residual_current_ids), + "amplitude_class_count": len(self.amplitude_classes), + "optimized_amplitude_class_count": ( + self.optimized_amplitude_class_count + ), + "residual_amplitude_count": len(self.residual_root_ids), + "source_state_mapping_count": len(self.source_state_mappings), + "physical_helicity_count": self.physical_helicity_count, + "structural_zero_helicity_count": len( + self.structural_zero_selector_domain_ids + ), + } + + def to_json_dict(self) -> dict[str, object]: + return self.to_runtime_manifest() + + def to_runtime_manifest(self) -> dict[str, object]: + return { + "kind": "pyamplicol-helicity-recurrence", + "contract_version": HELICITY_RECURRENCE_CONTRACT_VERSION, + "proof_algorithm": HELICITY_RECURRENCE_PROOF_ALGORITHM, + "current_count": self.current_count, + "amplitude_root_count": self.amplitude_root_count, + "proof_counts": self.proof_counts(), + "selector_domains": [ + domain.to_runtime_manifest() for domain in self.selector_domains + ], + "source_state_mappings": [ + mapping.to_runtime_manifest() + for mapping in self.source_state_mappings + ], + "recurrence_classes": [ + recurrence.to_runtime_manifest() + for recurrence in self.recurrence_classes + ], + "amplitude_classes": [ + recurrence.to_runtime_manifest() + for recurrence in self.amplitude_classes + ], + "residual_current_ids": list(self.residual_current_ids), + "residual_root_ids": list(self.residual_root_ids), + "structural_zero_selector_domain_ids": list( + self.structural_zero_selector_domain_ids + ), + "diagnostics": list(self.diagnostics), + } + + +@dataclass(frozen=True, slots=True) +class _CurrentProof: + class_index: int | None + factor: _ComplexWeight + selector_state: _SelectorState + residual_reason: str | None = None + + +@dataclass(slots=True) +class _PendingCurrentClass: + class_id: str + representative_current_id: int + external_labels: tuple[int, ...] + source_class: bool + proof_digest: str + transition_contract_ids: tuple[str, ...] + members: list[tuple[int, _SelectorState, _ComplexWeight]] + + +@dataclass(slots=True) +class _PendingAmplitudeClass: + class_id: str + representative_root_id: int + proof_digest: str + transition_contract_ids: tuple[str, ...] + members: list[tuple[int, tuple[_SelectorState, ...], _ComplexWeight]] + + +@dataclass(frozen=True, slots=True) +class _SemanticAmplitudeRootSignature: + """Exact model-certified identity used only for construction aliases.""" + + proof_digest: str + selector_states: tuple[_SelectorState, ...] + factor: _ComplexWeight + + +def _derive_current_replay_proofs( + dag: GenericDAG, + model: Model, + *, + color_sector_aliases: _ColorSectorAliases | None = None, +) -> tuple[ + dict[int, tuple[int, int]], + dict[int, dict[str, object]], + list[_PendingCurrentClass], + tuple[_CurrentProof, ...], + list[int], + list[str], +]: + """Build the shared exact recursive-current proof graph. + + ``color_sector_aliases`` projects only the traversal-sector identifier. + Every external label, ordered traversal, line group, flavour flow, kernel + contract, coupling and coefficient remains part of the proof. The normal + helicity-replay path passes no projection; contracted open-line + construction uses the projection to recognize only proven block-order + aliases. + """ + + source_by_bit, source_contracts, source_diagnostics = _source_contracts( + dag, + model, + ) + interactions_by_result: dict[int, list[InteractionNode]] = defaultdict(list) + for interaction in dag.interactions: + interactions_by_result[int(interaction.result_id)].append(interaction) + + pending_classes: list[_PendingCurrentClass] = [] + class_by_signature: dict[str, int] = {} + current_proofs: list[_CurrentProof | None] = [None] * len(dag.currents) + residual_current_ids: list[int] = [] + diagnostics = list(source_diagnostics) + + ordered_currents = sorted( + dag.currents, + key=lambda current: (len(current.index.external_labels), current.id), + ) + for current in ordered_currents: + selector_state, selector_error = _selector_state_for_ancestry( + int(current.index.helicity_ancestry), + source_by_bit, + ) + if selector_error is not None: + _record_residual_current( + current, + selector_state, + selector_error, + current_proofs, + residual_current_ids, + diagnostics, + ) + continue + + if current.is_source: + source_contract = source_contracts.get(current.id) + if source_contract is None: + _record_residual_current( + current, + selector_state, + "source state has no exact SourceIR contract", + current_proofs, + residual_current_ids, + diagnostics, + ) + continue + signature_payload = { + "kind": "source", + "current_contract": _current_contract( + current, + include_spin_state=False, + color_sector_aliases=color_sector_aliases, + ), + "source_topology": source_contract["topology"], + "source_contract_digest": source_contract["digest"], + } + class_index, factor = _register_current_class( + current, + selector_state, + signature_payload, + transition_contract_ids=(), + source_class=True, + class_by_signature=class_by_signature, + pending_classes=pending_classes, + ) + current_proofs[current.id] = _CurrentProof( + class_index=class_index, + factor=factor, + selector_state=selector_state, + ) + continue + + candidate = _generated_current_signature( + dag, + current, + interactions_by_result[current.id], + model, + current_proofs, + color_sector_aliases=color_sector_aliases, + ) + if isinstance(candidate, str): + _record_residual_current( + current, + selector_state, + candidate, + current_proofs, + residual_current_ids, + diagnostics, + ) + continue + signature_payload, transition_contract_ids = candidate + class_index, factor = _register_current_class( + current, + selector_state, + signature_payload, + transition_contract_ids=transition_contract_ids, + source_class=False, + class_by_signature=class_by_signature, + pending_classes=pending_classes, + ) + current_proofs[current.id] = _CurrentProof( + class_index=class_index, + factor=factor, + selector_state=selector_state, + ) + + if any(item is None for item in current_proofs): + raise ValueError("helicity recurrence derivation left a current unclassified") + return ( + source_by_bit, + source_contracts, + pending_classes, + tuple(item for item in current_proofs if item is not None), + residual_current_ids, + diagnostics, + ) + + +def build_helicity_recurrence_plan( + dag: GenericDAG, + model: Model, +) -> HelicityRecurrencePlan | None: + """Derive reusable recurrence classes for a complete-helicity DAG. + + The selected-helicity generation path deliberately returns ``None`` and is + therefore byte-for-byte unchanged. Proof failures are attached only to the + affected current and descendants that depend on it; independent classes are + retained. + """ + + if dag.helicity_coverage != "complete" or dag.selected_source_helicities: + return None + + ( + source_by_bit, + source_contracts, + pending_classes, + proven_currents, + residual_current_ids, + diagnostics, + ) = _derive_current_replay_proofs(dag, model) + + pending_amplitudes, residual_root_ids, root_diagnostics = _amplitude_classes( + dag, + model, + proven_currents, + source_by_bit, + ) + diagnostics.extend(root_diagnostics) + + residual_root_selector_states: dict[int, tuple[_SelectorState, ...]] = {} + for root_id in residual_root_ids: + selector_states, selector_error = _root_selector_states( + dag.amplitude_roots[root_id], + dag, + source_by_bit, + ) + if selector_error is not None: + diagnostics.append( + f"amplitude root {root_id}: residual selector routing failed: " + f"{selector_error}" + ) + continue + residual_root_selector_states[root_id] = selector_states + + possible_helicities = _possible_helicity_states(dag, model) + represented_helicities = { + state + for recurrence in pending_amplitudes + for _root_id, states, _factor in recurrence.members + for state in states + } | { + state + for states in residual_root_selector_states.values() + for state in states + } + structural_zeros = tuple( + sorted(set(possible_helicities) - represented_helicities) + ) + all_selector_states = { + proof.selector_state for proof in proven_currents + } | { + state + for recurrence in pending_amplitudes + for _root_id, states, _factor in recurrence.members + for state in states + } | { + state + for states in residual_root_selector_states.values() + for state in states + } | set(structural_zeros) + domain_id_by_state = { + state: index for index, state in enumerate(sorted(all_selector_states)) + } + complete_label_count = len( + [leg for leg in dag.process.legs if leg.outgoing_pdg is not None] + ) + selector_domains = tuple( + HelicitySelectorDomain( + id=index, + source_states=state, + complete=len(state) == complete_label_count, + ) + for state, index in sorted( + domain_id_by_state.items(), + key=lambda item: item[1], + ) + ) + + recurrence_classes = tuple( + HelicityRecurrenceClass( + class_id=item.class_id, + representative_current_id=item.representative_current_id, + external_labels=item.external_labels, + source_class=item.source_class, + members=tuple( + HelicityCurrentReplayMember( + current_id=current_id, + selector_domain_id=domain_id_by_state[state], + factor=factor, + ) + for current_id, state, factor in item.members + ), + proof_digest=item.proof_digest, + transition_contract_ids=item.transition_contract_ids, + ) + for item in pending_classes + ) + amplitude_classes = tuple( + HelicityAmplitudeReplayClass( + class_id=item.class_id, + representative_root_id=item.representative_root_id, + members=tuple( + HelicityAmplitudeReplayMember( + root_id=root_id, + selector_domain_ids=tuple( + domain_id_by_state[state] for state in states + ), + factor=factor, + ) + for root_id, states, factor in item.members + ), + proof_digest=item.proof_digest, + transition_contract_ids=item.transition_contract_ids, + ) + for item in pending_amplitudes + ) + + class_by_current = { + member.current_id: recurrence + for recurrence in recurrence_classes + for member in recurrence.members + } + source_state_mappings: list[HelicitySourceStateMapping] = [] + for current in dag.currents: + if not current.is_source or current.id not in source_contracts: + continue + recurrence = class_by_current.get(current.id) + if recurrence is None: + continue + member = next( + member for member in recurrence.members if member.current_id == current.id + ) + source_contract = source_contracts[current.id] + source_state_mappings.append( + HelicitySourceStateMapping( + current_id=current.id, + external_label=int(current.source_leg_label or 0), + helicity=int(current.source_helicity or 0), + chirality=int(current.index.chirality), + spin_state=current.index.spin_state, + declared_state_index=int(source_contract["declared_state_index"]), + selector_domain_id=member.selector_domain_id, + recurrence_class_id=recurrence.class_id, + representative_current_id=recurrence.representative_current_id, + source_contract_digest=str(source_contract["digest"]), + factor=member.factor, + ) + ) + + return HelicityRecurrencePlan( + current_count=len(dag.currents), + amplitude_root_count=len(dag.amplitude_roots), + selector_domains=selector_domains, + source_state_mappings=tuple(source_state_mappings), + recurrence_classes=recurrence_classes, + amplitude_classes=amplitude_classes, + residual_current_ids=tuple(sorted(residual_current_ids)), + residual_root_ids=tuple(sorted(residual_root_ids)), + structural_zero_selector_domain_ids=tuple( + domain_id_by_state[state] for state in structural_zeros + ), + diagnostics=tuple(diagnostics), + ) + + +def _source_contracts( + dag: GenericDAG, + model: Model, +) -> tuple[ + dict[int, tuple[int, int]], + dict[int, dict[str, object]], + tuple[str, ...], +]: + """Return ancestry-bit source selectors and exact SourceIR contracts.""" + + leg_by_label = {int(leg.label): leg for leg in dag.process.legs} + source_by_bit: dict[int, tuple[int, int]] = {} + contracts: dict[int, dict[str, object]] = {} + diagnostics: list[str] = [] + for current in dag.currents: + if not current.is_source: + continue + ancestry = int(current.index.helicity_ancestry) + label = current.source_leg_label + helicity = current.source_helicity + if ( + ancestry <= 0 + or ancestry & (ancestry - 1) + or label is None + or helicity is None + ): + diagnostics.append( + f"source current {current.id} has non-canonical ancestry metadata" + ) + continue + bit = ancestry.bit_length() - 1 + selector = (int(label), int(helicity)) + previous = source_by_bit.setdefault(bit, selector) + if previous != selector: + diagnostics.append( + f"source ancestry bit {bit} maps to inconsistent physical states" + ) + continue + leg = leg_by_label.get(int(label)) + if leg is None or leg.outgoing_pdg is None: + diagnostics.append( + f"source current {current.id} refers to absent external leg {label}" + ) + continue + try: + source_ir = model._source_ir(int(leg.outgoing_pdg)) + matched_index = None + for index, declared in enumerate(source_ir.states): + state = ( + source_ir.crossing.apply(declared) + if leg.is_initial + else declared + ) + if ( + int(state.helicity) == int(helicity) + and int(state.chirality) == int(current.index.chirality) + and state.spin_state == current.index.spin_state + ): + matched_index = index + break + if matched_index is None: + diagnostics.append( + f"source current {current.id} is not declared by its SourceIR" + ) + continue + exact_payload = { + "source_ir": source_ir.to_json_dict(), + "initial_state_crossed": bool(leg.is_initial), + } + contracts[current.id] = { + "digest": _digest(exact_payload), + "declared_state_index": matched_index, + "topology": { + "external_label": int(label), + "statistics": source_ir.statistics, + "wavefunction_family": source_ir.wavefunction_family, + "component_dimension": int(source_ir.component_dimension), + "basis": source_ir.basis, + "orientation": source_ir.identity.orientation, + "self_conjugate": bool(source_ir.identity.self_conjugate), + "chirality": int(current.index.chirality), + "spin_state_shape": _spin_state_shape( + current.index.spin_state + ), + "crossing": source_ir.crossing.to_json_dict(), + "has_mass_parameter": source_ir.mass_parameter is not None, + "has_width_parameter": source_ir.width_parameter is not None, + }, + } + except Exception as error: + diagnostics.append( + f"source current {current.id} contract derivation failed: {error}" + ) + return source_by_bit, contracts, tuple(diagnostics) + + +def _generated_current_signature( + dag: GenericDAG, + current: CurrentNode, + interactions: Sequence[InteractionNode], + model: Model, + current_proofs: Sequence[_CurrentProof | None], + *, + color_sector_aliases: _ColorSectorAliases | None = None, +) -> tuple[dict[str, object], tuple[str, ...]] | str: + terms_by_key: dict[str, list[_ComplexWeight]] = defaultdict(list) + transition_contracts: set[str] = set() + for interaction in interactions: + left = current_proofs[interaction.left_id] + right = current_proofs[interaction.right_id] + if left is None or right is None: + return "transition depends on an unclassified parent current" + if left.class_index is None or right.class_index is None: + return "transition depends on a localized residual parent current" + try: + equivalence = model.vertex_evaluation_equivalence( + int(interaction.vertex_kind) + ) + except Exception as error: + return f"vertex transition contract failed: {error}" + if not equivalence.verified or not equivalence.class_id: + return ( + "vertex transition has no verified canonical kernel contract: " + f"kind {interaction.vertex_kind}" + ) + transition_contracts.add(equivalence.class_id) + parent_classes = (left.class_index, right.class_index) + if equivalence.input_order == (1, 0): + parent_classes = (parent_classes[1], parent_classes[0]) + kernel_factor = equivalence.factor + if ( + equivalence.input_exchange_factor is not None + and parent_classes[1] < parent_classes[0] + ): + parent_classes = (parent_classes[1], parent_classes[0]) + kernel_factor = _complex_mul( + kernel_factor, + equivalence.input_exchange_factor, + ) + coupling_identity = _runtime_coupling_identity(model, interaction) + term_key = _canonical_json( + { + "kernel_contract_id": equivalence.class_id, + "parent_recurrence_classes": list(parent_classes), + "result_particle_id": int(current.index.particle_id), + "result_chirality": int(current.index.chirality), + "coupling_identity": coupling_identity, + } + ) + coefficient = _complex_mul( + interaction.color_weight, + _complex_mul(kernel_factor, _complex_mul(left.factor, right.factor)), + ) + terms_by_key[term_key].append(coefficient) + + terms = [] + for term_key in sorted(terms_by_key): + coefficient = _sum_weights(terms_by_key[term_key]) + if coefficient == (0.0, 0.0): + continue + terms.append( + { + "transition": json.loads(term_key), + "coefficient": list(coefficient), + } + ) + try: + propagator = model._propagator_ir( + int(current.index.particle_id), + int(current.index.chirality), + ).to_json_dict() + except Exception as error: + return f"current finalization contract failed: {error}" + return ( + { + "kind": "generated-current", + "current_contract": _current_contract( + current, + color_sector_aliases=color_sector_aliases, + ), + "propagator_contract": propagator, + "terms": terms, + }, + tuple(sorted(transition_contracts)), + ) + + +def _register_current_class( + current: CurrentNode, + selector_state: _SelectorState, + signature_payload: Mapping[str, object], + *, + transition_contract_ids: tuple[str, ...], + source_class: bool, + class_by_signature: dict[str, int], + pending_classes: list[_PendingCurrentClass], +) -> tuple[int, _ComplexWeight]: + direct_signature = _canonical_json(signature_payload) + opposite_payload = _negated_signature_payload(signature_payload) + opposite_signature = _canonical_json(opposite_payload) + class_index = class_by_signature.get(direct_signature) + factor: _ComplexWeight = (1.0, 0.0) + if class_index is None and opposite_signature != direct_signature: + class_index = class_by_signature.get(opposite_signature) + if class_index is not None: + factor = (-1.0, 0.0) + if class_index is None: + proof_digest = hashlib.sha256(direct_signature.encode("ascii")).hexdigest() + class_index = len(pending_classes) + class_by_signature[direct_signature] = class_index + pending_classes.append( + _PendingCurrentClass( + class_id=f"helicity-current-sha256:{proof_digest}", + representative_current_id=current.id, + external_labels=tuple(current.index.external_labels), + source_class=source_class, + proof_digest=proof_digest, + transition_contract_ids=transition_contract_ids, + members=[], + ) + ) + pending_classes[class_index].members.append( + (current.id, selector_state, factor) + ) + return class_index, factor + + +def _record_residual_current( + current: CurrentNode, + selector_state: _SelectorState, + reason: str, + current_proofs: list[_CurrentProof | None], + residual_current_ids: list[int], + diagnostics: list[str], +) -> None: + current_proofs[current.id] = _CurrentProof( + class_index=None, + factor=(1.0, 0.0), + selector_state=selector_state, + residual_reason=reason, + ) + residual_current_ids.append(current.id) + diagnostics.append(f"current {current.id}: {reason}") + + +def _amplitude_classes( + dag: GenericDAG, + model: Model, + current_proofs: Sequence[_CurrentProof], + source_by_bit: Mapping[int, tuple[int, int]], + *, + color_sector_aliases: _ColorSectorAliases | None = None, +) -> tuple[list[_PendingAmplitudeClass], list[int], list[str]]: + pending: list[_PendingAmplitudeClass] = [] + class_by_signature: dict[str, int] = {} + residual: list[int] = [] + diagnostics: list[str] = [] + for root in dag.amplitude_roots: + left = current_proofs[root.left_id] + right = current_proofs[root.right_id] + selector_states, selector_error = _root_selector_states( + root, + dag, + source_by_bit, + ) + if selector_error is not None: + residual.append(root.id) + diagnostics.append(f"amplitude root {root.id}: {selector_error}") + continue + if left.class_index is None or right.class_index is None: + residual.append(root.id) + diagnostics.append( + f"amplitude root {root.id}: closure depends on a residual current" + ) + continue + + parent_classes = (left.class_index, right.class_index) + factor = _complex_mul(left.factor, right.factor) + contract_ids: tuple[str, ...] = () + if root.vertex_kind is not None: + try: + equivalence = model.vertex_evaluation_equivalence(root.vertex_kind) + except Exception as error: + residual.append(root.id) + diagnostics.append( + f"amplitude root {root.id}: closure contract failed: {error}" + ) + continue + if not equivalence.verified or not equivalence.class_id: + residual.append(root.id) + diagnostics.append( + f"amplitude root {root.id}: unverified closure kernel " + f"{root.vertex_kind}" + ) + continue + if equivalence.input_order == (1, 0): + parent_classes = (parent_classes[1], parent_classes[0]) + kernel_factor = equivalence.factor + if ( + equivalence.input_exchange_factor is not None + and parent_classes[1] < parent_classes[0] + ): + parent_classes = (parent_classes[1], parent_classes[0]) + kernel_factor = _complex_mul( + kernel_factor, + equivalence.input_exchange_factor, + ) + factor = _complex_mul(factor, kernel_factor) + contract_ids = (equivalence.class_id,) + + payload = { + "kind": "amplitude-closure", + "root_contract": _root_contract( + root, + color_sector_aliases=color_sector_aliases, + ), + "parent_recurrence_classes": list(parent_classes), + } + direct_signature = _canonical_json(payload) + opposite_signature = _canonical_json(_negated_root_payload(payload)) + class_index = class_by_signature.get(direct_signature) + member_factor = factor + if class_index is None and direct_signature != opposite_signature: + class_index = class_by_signature.get(opposite_signature) + if class_index is not None: + member_factor = _complex_mul(member_factor, (-1.0, 0.0)) + if class_index is None: + proof_digest = hashlib.sha256(direct_signature.encode("ascii")).hexdigest() + class_index = len(pending) + class_by_signature[direct_signature] = class_index + pending.append( + _PendingAmplitudeClass( + class_id=f"helicity-amplitude-sha256:{proof_digest}", + representative_root_id=root.id, + proof_digest=proof_digest, + transition_contract_ids=contract_ids, + members=[], + ) + ) + pending[class_index].members.append( + (root.id, selector_states, member_factor) + ) + return pending, residual, diagnostics + + +def _semantic_amplitude_root_alias_signatures( + dag: GenericDAG, + model: Model, + *, + color_sector_aliases: _ColorSectorAliases, +) -> tuple[_SemanticAmplitudeRootSignature | None, ...]: + """Return exact root signatures under one traversal-sector projection. + + This is deliberately a construction-only view of the same proof machinery + used by helicity replay. The projection changes only sector identifiers; + traversal order, source labels, fermion exchange factors and every model + kernel contract remain exact. A ``None`` entry means that no proof was + available and callers must fail closed rather than remove that root. + """ + + ( + source_by_bit, + _source_contracts_by_id, + _pending_classes, + current_proofs, + _residual_current_ids, + _diagnostics, + ) = _derive_current_replay_proofs( + dag, + model, + color_sector_aliases=color_sector_aliases, + ) + pending, residual, _root_diagnostics = _amplitude_classes( + dag, + model, + current_proofs, + source_by_bit, + color_sector_aliases=color_sector_aliases, + ) + signatures: list[_SemanticAmplitudeRootSignature | None] = [ + None + ] * len(dag.amplitude_roots) + for recurrence in pending: + for root_id, selector_states, factor in recurrence.members: + signatures[root_id] = _SemanticAmplitudeRootSignature( + proof_digest=recurrence.proof_digest, + selector_states=selector_states, + factor=factor, + ) + for root_id in residual: + signatures[root_id] = None + return tuple(signatures) + + +def _root_selector_states( + root: AmplitudeRoot, + dag: GenericDAG, + source_by_bit: Mapping[int, tuple[int, int]], +) -> tuple[tuple[_SelectorState, ...], str | None]: + selector_state, selector_error = _root_selector_state( + root, + dag, + source_by_bit, + ) + if selector_error is not None: + return (), selector_error + selector_states = [selector_state] + if math.isclose(root.helicity_weight, 2.0, rel_tol=1.0e-12, abs_tol=1.0e-12): + flipped = tuple((label, -helicity) for label, helicity in selector_state) + if flipped != selector_state: + selector_states.append(flipped) + elif not math.isclose( + root.helicity_weight, + 1.0, + rel_tol=1.0e-12, + abs_tol=1.0e-12, + ): + return ( + (), + f"unsupported helicity multiplicity {root.helicity_weight}", + ) + return tuple(selector_states), None + + +def _root_selector_state( + root: AmplitudeRoot, + dag: GenericDAG, + source_by_bit: Mapping[int, tuple[int, int]], +) -> tuple[_SelectorState, str | None]: + ancestry = int( + dag.currents[root.left_id].index.helicity_ancestry + | dag.currents[root.right_id].index.helicity_ancestry + ) + return _selector_state_for_ancestry(ancestry, source_by_bit) + + +def _selector_state_for_ancestry( + ancestry: int, + source_by_bit: Mapping[int, tuple[int, int]], +) -> tuple[_SelectorState, str | None]: + states: dict[int, int] = {} + remaining = int(ancestry) + while remaining: + least = remaining & -remaining + bit = least.bit_length() - 1 + source = source_by_bit.get(bit) + if source is None: + return tuple(sorted(states.items())), f"unknown source ancestry bit {bit}" + label, helicity = source + previous = states.setdefault(label, helicity) + if previous != helicity: + return ( + tuple(sorted(states.items())), + f"ancestry selects multiple helicities for external leg {label}", + ) + remaining ^= least + return tuple(sorted(states.items())), None + + +def _possible_helicity_states( + dag: GenericDAG, + model: Model, +) -> tuple[_SelectorState, ...]: + labels: list[int] = [] + per_leg: list[tuple[int, ...]] = [] + for leg in dag.process.legs: + if leg.outgoing_pdg is None: + continue + source_ir = model._source_ir(int(leg.outgoing_pdg)) + values = { + int( + (source_ir.crossing.apply(state) if leg.is_initial else state).helicity + ) + for state in source_ir.states + } + labels.append(int(leg.label)) + per_leg.append(tuple(sorted(values))) + return tuple( + tuple(zip(labels, values, strict=True)) for values in product(*per_leg) + ) + + +def _current_contract( + current: CurrentNode, + *, + include_spin_state: bool = True, + color_sector_aliases: _ColorSectorAliases | None = None, +) -> dict[str, object]: + index = current.index + color_state = index.color_state.to_json_dict() + if color_sector_aliases is not None: + sector_id = int(color_state["sector_id"]) + color_state["sector_id"] = int( + color_sector_aliases.get(sector_id, sector_id) + ) + payload: dict[str, object] = { + "particle_id": int(index.particle_id), + "external_mask": int(index.external_mask), + "external_labels": list(index.external_labels), + "ordered_external_labels": list(index.ordered_external_labels), + "chirality": int(index.chirality), + "flavour_flow": list(index.flavour_flow), + "quantum_number_flow": [list(item) for item in index.quantum_number_flow], + "color_state": color_state, + "momentum_mask": int(index.momentum_mask), + "coupling_orders": [list(item) for item in index.coupling_orders], + "auxiliary_kind": index.auxiliary_kind, + "dimension": int(current.dimension), + "is_source": bool(current.is_source), + } + if include_spin_state: + payload["spin_state"] = _json_spin_state(index.spin_state) + return payload + + +def _root_contract( + root: AmplitudeRoot, + *, + color_sector_aliases: _ColorSectorAliases | None = None, +) -> dict[str, object]: + color_sector_id = root.color_sector_id + if color_sector_id is not None and color_sector_aliases is not None: + color_sector_id = int( + color_sector_aliases.get(int(color_sector_id), int(color_sector_id)) + ) + return { + "kind": root.kind, + "color_weight": list(root.color_weight), + "color_sector_id": color_sector_id, + "vertex_kind": root.vertex_kind, + "vertex_particles": ( + None if root.vertex_particles is None else list(root.vertex_particles) + ), + "coupling": list(root.coupling), + "contraction_ir": root.contraction_ir.to_json_dict(), + } + + +def _runtime_coupling_identity( + model: Model, + interaction: InteractionNode, +) -> dict[str, object]: + names = runtime_coupling_parameter_names( + interaction.vertex_kind, + interaction.vertex_particles, + interaction.coupling, + model=model, + ) + return { + "default": list(interaction.coupling), + "runtime_parameters": [name for name in names], + } + + +def _negated_signature_payload( + payload: Mapping[str, object], +) -> dict[str, object]: + result = json.loads(_canonical_json(payload)) + terms = result.get("terms") + if not isinstance(terms, list): + return result + for term in terms: + if not isinstance(term, dict): + continue + coefficient = term.get("coefficient") + if isinstance(coefficient, list) and len(coefficient) == 2: + term["coefficient"] = [-float(coefficient[0]), -float(coefficient[1])] + return result + + +def _negated_root_payload(payload: Mapping[str, object]) -> dict[str, object]: + result = json.loads(_canonical_json(payload)) + root = result.get("root_contract") + if not isinstance(root, dict): + return result + weight = root.get("color_weight") + if isinstance(weight, list) and len(weight) == 2: + root["color_weight"] = [-float(weight[0]), -float(weight[1])] + return result + + +def _spin_state_shape(value: int | tuple[int, ...]) -> dict[str, object]: + if isinstance(value, tuple): + return {"kind": "tuple", "length": len(value)} + return {"kind": "scalar"} + + +def _json_spin_state(value: int | tuple[int, ...]) -> object: + return list(value) if isinstance(value, tuple) else value + + +def _complex_mul(left: _ComplexWeight, right: _ComplexWeight) -> _ComplexWeight: + return ( + _canonical_zero(left[0] * right[0] - left[1] * right[1]), + _canonical_zero(left[0] * right[1] + left[1] * right[0]), + ) + + +def _sum_weights(values: Sequence[_ComplexWeight]) -> _ComplexWeight: + return ( + _canonical_zero(fsum(value[0] for value in values)), + _canonical_zero(fsum(value[1] for value in values)), + ) + + +def _canonical_zero(value: float) -> float: + return 0.0 if value == 0.0 else value + + +def _canonical_json(value: object) -> str: + return json.dumps( + value, + ensure_ascii=True, + allow_nan=False, + separators=(",", ":"), + sort_keys=True, + ) + + +def _digest(value: object) -> str: + return hashlib.sha256(_canonical_json(value).encode("ascii")).hexdigest() + + +def _validate_digest(value: str, *, context: str) -> None: + if len(value) != 64 or any( + character not in "0123456789abcdef" for character in value + ): + raise ValueError(f"{context} must be a lowercase SHA-256") + + +__all__ = [ + "HELICITY_RECURRENCE_CONTRACT_VERSION", + "HELICITY_RECURRENCE_PROOF_ALGORITHM", + "RUNTIME_SELECTOR_PROVENANCE", + "HelicityRecurrencePlan", + "build_helicity_recurrence_plan", +] diff --git a/pyamplicol/src/pyamplicol/generation/numerical_candidate_index.py b/pyamplicol/src/pyamplicol/generation/numerical_candidate_index.py new file mode 100644 index 0000000000..958da377af --- /dev/null +++ b/pyamplicol/src/pyamplicol/generation/numerical_candidate_index.py @@ -0,0 +1,143 @@ +# SPDX-License-Identifier: 0BSD +"""Complete scalar tolerance-window candidate indexes. + +The index is only a screening device. Callers must still replay every +returned candidate against every captured component. For one selected +complex observation, let ``X`` and ``Y`` be the infinity norms of the current +and signed representative, and ``D`` their infinity-norm residual. Passing +gives ``D <= a + r*max(X,Y)``; reverse triangle gives ``Y <= X+D``. Thus, for +``r < 1``, ``D <= (a+rX)/(1-r)``, and the chosen scalar residual is no larger +than ``D``. The returned one-dimensional window therefore has no false +negatives. Residual tolerance is independent per complex observation, so an +unrelated large observation does not enlarge the selected pair's bound. +Policies with ``r >= 1`` deliberately fall back to the complete earlier set. +""" + +from __future__ import annotations + +from bisect import bisect_left, bisect_right +from collections.abc import Callable, Mapping, Sequence +from dataclasses import dataclass +from typing import Any, Generic, Literal, TypeVar + +_RawScalar = TypeVar("_RawScalar") +_IndexScalar = TypeVar("_IndexScalar") + + +@dataclass(frozen=True, slots=True) +class NumericalObservationCandidateIndex(Generic[_IndexScalar]): + """Most-discriminating deterministic scalar projection for one contract.""" + + observation_index: int + scalar_component: int + entries: tuple[tuple[_IndexScalar, int], ...] + + +def build_numerical_observation_candidate_index( + member_ids: Sequence[int], + observations: Mapping[ + int, + Sequence[tuple[_RawScalar, _RawScalar]], + ], + *, + normalize: Callable[[_RawScalar], _IndexScalar], +) -> NumericalObservationCandidateIndex[_IndexScalar]: + """Build one deterministic complete index without copying full vectors.""" + + members = tuple(member_ids) + if not members: + raise ValueError("numerical candidate index has no members") + observation_count = len(observations[members[0]]) + if observation_count == 0 or any( + len(observations[current_id]) != observation_count for current_id in members + ): + raise ValueError("numerical candidate index members have inconsistent widths") + + def discrimination(choice: tuple[int, int]) -> tuple[int, int, int, int]: + observation_index, scalar_component = choice + distinct_values: set[_RawScalar] = set() + nonzero_count = 0 + for current_id in members: + value = observations[current_id][observation_index][scalar_component] + distinct_values.add(value) + nonzero_count += value != 0 + return ( + len(distinct_values), + nonzero_count, + -observation_index, + -scalar_component, + ) + + observation_index, scalar_component = max( + ( + (observation_index, scalar_component) + for observation_index in range(observation_count) + for scalar_component in (0, 1) + ), + key=discrimination, + ) + entries = tuple( + sorted( + ( + normalize( + observations[current_id][observation_index][scalar_component] + ), + current_id, + ) + for current_id in members + ) + ) + return NumericalObservationCandidateIndex( + observation_index=observation_index, + scalar_component=scalar_component, + entries=entries, + ) + + +def numerical_observation_tolerance_window_ids( + index: NumericalObservationCandidateIndex[_IndexScalar], + current_values: Sequence[tuple[_RawScalar, _RawScalar]], + *, + relation_kind: Literal["equal", "opposite"], + current_id: int, + relative_tolerance: _IndexScalar, + absolute_tolerance: _IndexScalar, + normalize: Callable[[_RawScalar], _IndexScalar], +) -> tuple[int, ...]: + """Return every earlier representative that can pass the full test.""" + + if relation_kind not in {"equal", "opposite"}: + raise ValueError("numerical candidate index relation is unsupported") + if not 0 <= index.observation_index < len(current_values): + raise ValueError("numerical candidate index observation is invalid") + relative: Any = relative_tolerance + absolute: Any = absolute_tolerance + if relative >= 1: + return tuple( + representative_id + for _value, representative_id in index.entries + if representative_id < current_id + ) + selected = current_values[index.observation_index] + selected_scalar: Any = normalize(selected[index.scalar_component]) + target = selected_scalar if relation_kind == "equal" else -selected_scalar + current_scale = max( + abs(normalize(selected[0])), + abs(normalize(selected[1])), + ) + radius = (absolute + relative * current_scale) / (1 - relative) + entries: Any = index.entries + lower = bisect_left(entries, (target - radius, -1)) + upper = bisect_right(entries, (target + radius, current_id - 1)) + return tuple( + representative_id + for _value, representative_id in entries[lower:upper] + if representative_id < current_id + ) + + +__all__ = [ + "NumericalObservationCandidateIndex", + "build_numerical_observation_candidate_index", + "numerical_observation_tolerance_window_ids", +] diff --git a/pyamplicol/src/pyamplicol/generation/numerical_current_warmup.py b/pyamplicol/src/pyamplicol/generation/numerical_current_warmup.py new file mode 100644 index 0000000000..462f07e1c8 --- /dev/null +++ b/pyamplicol/src/pyamplicol/generation/numerical_current_warmup.py @@ -0,0 +1,1473 @@ +# SPDX-License-Identifier: 0BSD +"""High-precision current snapshots for bounded relation-discovery warm-up.""" + +from __future__ import annotations + +import hashlib +import json +from collections.abc import Mapping, Sequence +from dataclasses import dataclass +from decimal import ROUND_HALF_EVEN, Decimal, localcontext +from itertools import pairwise +from math import isfinite +from typing import Any, Literal + +from ..evaluators.symbolica_compile import _compile_symbolica_outputs +from ..evaluators.symbolica_settings import SymbolicaEvaluatorSettings +from ..models.base import Model +from ..runtime.symbolica_exact import ( + _decimal, + _fill_momenta, + _fill_sources, +) +from .contracts import RuntimeExpressionSchema, StageCompilationInput +from .dag_equivalence import ( + NUMERICAL_CURRENT_CAPTURE_ABI, + NUMERICAL_CURRENT_CAPTURE_OUTPUT_PARTITION_ABI, + NumericalCurrentObservationDiscoveryResult, + NumericalCurrentRelationApplicationResult, + NumericalCurrentRelationCertificate, + apply_numerical_current_relation_certificates, + discover_generic_dag_numerical_current_relations, + generic_dag_numerical_capture_output_partition_sha256, +) +from .dag_types import GenericDAG +from .recurrence_numerical_current_warmup import ( + numerical_relation_correctness_payload, +) +from .runtime_schema import build_runtime_expression_schema +from .stage_planning import build_generic_stage_compiler_blueprint +from .stage_types import GenericCompiledStageBlueprint +from .validation import ( + ValidationPointRecord, + build_validation_point, + rotate_validation_point, +) + +_ComplexDecimal = tuple[Decimal, Decimal] +_CAPTURE_ABI = NUMERICAL_CURRENT_CAPTURE_ABI +_WARMUP_ABI = "pyamplicol-generic-dag-numerical-current-warmup-v1" +_CAPTURE_OUTPUT_PARTITION_ABI = NUMERICAL_CURRENT_CAPTURE_OUTPUT_PARTITION_ABI +_NUMERICAL_DECIMAL_GUARD_DIGITS = 16 + + +@dataclass(frozen=True, slots=True) +class GenericDAGCurrentObservationCapture: + """Complete point-major current observations and replay provenance.""" + + precision_digits: int + points: tuple[ValidationPointRecord, ...] + point_sha256s: tuple[str, ...] + kinematic_sha256s: tuple[str, ...] + parameter_contexts: tuple[tuple[Decimal, ...], ...] + parameter_context_sha256s: tuple[str, ...] + observations: Mapping[int, tuple[_ComplexDecimal, ...]] + runtime_schema_sha256: str + model_parameter_schema_sha256: str + source_dag_sha256: str + evaluator_output_partition_abi: str + evaluator_output_partition_sha256: str + observation_batch_sha256: str + capture_contract_sha256: str + + @property + def point_count(self) -> int: + return len(self.point_sha256s) + + @property + def current_count(self) -> int: + return len(self.observations) + + def to_provenance_dict(self) -> dict[str, object]: + return { + "abi": _CAPTURE_ABI, + "precision_digits": self.precision_digits, + "point_count": self.point_count, + "point_sha256s": list(self.point_sha256s), + "kinematic_sha256s": list(self.kinematic_sha256s), + "points": [point.to_mapping() for point in self.points], + "parameter_contexts": [ + [_decimal_string(value) for value in context] + for context in self.parameter_contexts + ], + "parameter_context_sha256s": list(self.parameter_context_sha256s), + "current_count": self.current_count, + "runtime_schema_sha256": self.runtime_schema_sha256, + "model_parameter_schema_sha256": (self.model_parameter_schema_sha256), + "source_dag_sha256": self.source_dag_sha256, + "evaluator_output_partition": { + "abi": self.evaluator_output_partition_abi, + "sha256": self.evaluator_output_partition_sha256, + }, + "observation_batch_sha256": self.observation_batch_sha256, + "capture_contract_sha256": self.capture_contract_sha256, + "complete_current_component_digest": True, + "components_embedded": False, + "point_major": True, + "evaluator": "symbolica-interpreted-high-precision-stage-replay", + } + + +@dataclass(frozen=True, slots=True) +class _PartitionedCurrentCaptureStageEvaluator: + """Warm-up-only exact replay over current-ID-isolated evaluator leaves.""" + + evaluators: tuple[Any, ...] + chunk_input_indices: tuple[tuple[int, ...], ...] + output_partitions: tuple[tuple[int, int], ...] + input_len: int + output_len: int + + def __post_init__(self) -> None: + if ( + type(self.input_len) is not int + or self.input_len < 0 + or type(self.output_len) is not int + or self.output_len < 1 + or len(self.evaluators) != len(self.chunk_input_indices) + or len(self.evaluators) != len(self.output_partitions) + or not self.evaluators + ): + raise ValueError("numerical current partitioned evaluator shape is invalid") + expected_output_start = 0 + for evaluator, indices, partition in zip( + self.evaluators, + self.chunk_input_indices, + self.output_partitions, + strict=True, + ): + start, stop = partition + if ( + start != expected_output_start + or stop <= start + or stop > self.output_len + or any( + type(index) is not int or index < 0 or index >= self.input_len + for index in indices + ) + or any(left >= right for left, right in pairwise(indices)) + or getattr(evaluator, "input_len", None) != len(indices) + ): + raise ValueError( + "numerical current partitioned evaluator mapping drifted" + ) + expected_output_start = stop + if expected_output_start != self.output_len: + raise ValueError( + "numerical current partitioned evaluator outputs are not exhaustive" + ) + + def evaluate_complex_with_prec( + self, + values: Sequence[_ComplexDecimal], + precision: int, + ) -> tuple[object, ...]: + prepared = tuple(values) + if len(prepared) != self.input_len: + raise ValueError( + "numerical current partitioned evaluator input width drifted" + ) + outputs: list[object] = [] + for evaluator, indices, (start, stop) in zip( + self.evaluators, + self.chunk_input_indices, + self.output_partitions, + strict=True, + ): + source = getattr(evaluator, "_source_evaluator", None) + evaluate = getattr(source, "evaluate_complex_with_prec", None) + if not callable(evaluate): + raise ValueError( + "numerical current warm-up evaluator leaf lacks " + "high-precision replay" + ) + chunk_outputs = tuple( + evaluate( + tuple(prepared[index] for index in indices), + precision, + ) + ) + if len(chunk_outputs) != stop - start: + raise ValueError( + "numerical current warm-up evaluator leaf returned the " + "wrong output width" + ) + outputs.extend(chunk_outputs) + if len(outputs) != self.output_len: + raise ValueError( + "numerical current partitioned evaluator output width drifted" + ) + return tuple(outputs) + + +@dataclass(frozen=True, slots=True) +class _GenericDAGCurrentCaptureSession: + """Reusable interpreted stage evaluators for one immutable source DAG.""" + + dag: GenericDAG + process_id: str + runtime_schema: RuntimeExpressionSchema + schema: Mapping[str, object] + stages: tuple[GenericCompiledStageBlueprint, ...] + stage_evaluators: tuple[Any, ...] + model_parameters: tuple[Decimal, ...] + source_dag_sha256: str + evaluator_output_partition_abi: str + evaluator_output_partition_sha256: str + + +@dataclass(frozen=True, slots=True) +class GenericDAGNumericalCurrentWarmupResult: + """One complete discovery, verification, and application transaction.""" + + dag: GenericDAG + requested_mode: Literal["diagnostic", "certified-reuse"] + execution_mode: Literal["compiled", "eager"] + candidate_capture: GenericDAGCurrentObservationCapture + verification_capture: GenericDAGCurrentObservationCapture + application_capture: GenericDAGCurrentObservationCapture | None + application_validation: Mapping[str, object] + discovery: NumericalCurrentObservationDiscoveryResult + application: NumericalCurrentRelationApplicationResult + application_scope: Mapping[str, object] + + @property + def warning_required(self) -> bool: + return self.application.report.warning_required + + def to_json_dict(self) -> dict[str, object]: + applied_relation_count = self.application.report.applied_relation_count + return { + "schema_version": 1, + "abi": _WARMUP_ABI, + "requested_mode": self.requested_mode, + "effective_mode": self.requested_mode, + "effective_reuse_state": "enabled", + "state": self.application.report.state, + "scope": { + "execution_mode": self.execution_mode, + "color_accuracy": str(self.dag.process.color_accuracy), + "representation": "generic-dag", + }, + "source_semantics": { + "sha256": (self.application.report.source_semantics_sha256), + }, + "candidate_capture": (self.candidate_capture.to_provenance_dict()), + "verification_capture": (self.verification_capture.to_provenance_dict()), + "application_capture": ( + None + if self.application_capture is None + else self.application_capture.to_provenance_dict() + ), + "application_validation": dict(self.application_validation), + "application_scope": dict(self.application_scope), + "discovery": self.discovery.report.to_json_dict(), + "application": self.application.report.to_json_dict(), + "certified_relation_count": len(self.discovery.certificates), + "applied_relation_count": applied_relation_count, + "relation_correctness": numerical_relation_correctness_payload( + applied_relation_count + ), + "warning": (self.application.report.to_json_dict()["warning"]), + } + + +def run_generic_dag_numerical_current_warmup( + dag: GenericDAG, + model: Model, + *, + process_id: str, + mode: Literal["diagnostic", "certified-reuse"], + execution_mode: Literal["compiled", "eager"], + precision_digits: int, + probe_count: int, + verification_probe_count: int, + relative_tolerance: float, + absolute_tolerance: float, + seed: int, + application_relation_kinds: Sequence[Literal["equal", "opposite", "zero"]] = ( + "equal", + "opposite", + "zero", + ), + application_scope_reason: str = "complete-generic-dag", +) -> GenericDAGNumericalCurrentWarmupResult: + """Run the bounded default-on numerical relation transaction.""" + + if mode not in {"diagnostic", "certified-reuse"}: + raise ValueError( + "generic-DAG numerical current warm-up requires diagnostic or " + "certified-reuse mode" + ) + allowed_relation_kinds = _validated_application_relation_kinds( + application_relation_kinds + ) + if not isinstance(application_scope_reason, str) or not application_scope_reason: + raise ValueError("generic-DAG numerical relation application scope is invalid") + candidate_points, verification_points = build_numerical_current_probe_points( + dag, + model, + process_id=process_id, + seed=seed, + candidate_count=probe_count, + verification_count=verification_probe_count, + ) + baseline_session = _build_current_capture_session( + dag, + model, + process_id=process_id, + ) + candidate_parameter_contexts = _build_parameter_probe_contexts( + baseline_session.model_parameters, + precision_digits=precision_digits, + seed=seed, + domain="candidate-current-parameter-probes-v1", + count=len(candidate_points), + include_defaults=True, + ) + verification_parameter_contexts = _build_parameter_probe_contexts( + baseline_session.model_parameters, + precision_digits=precision_digits, + seed=seed, + domain="independent-verification-current-parameter-probes-v1", + count=len(verification_points), + include_defaults=False, + ) + candidate = capture_generic_dag_current_observations( + dag, + model, + candidate_points, + precision_digits=precision_digits, + parameter_contexts=candidate_parameter_contexts, + _session=baseline_session, + ) + verification = capture_generic_dag_current_observations( + dag, + model, + verification_points, + precision_digits=precision_digits, + parameter_contexts=verification_parameter_contexts, + _session=baseline_session, + ) + validate_independent_current_observation_captures( + candidate, + verification, + ) + discovery = discover_generic_dag_numerical_current_relations( + dag, + model, + candidate_observations=candidate.observations, + verification_observations=verification.observations, + candidate_point_sha256s=candidate.point_sha256s, + verification_point_sha256s=verification.point_sha256s, + candidate_kinematic_sha256s=candidate.kinematic_sha256s, + verification_kinematic_sha256s=verification.kinematic_sha256s, + candidate_parameter_context_sha256s=(candidate.parameter_context_sha256s), + verification_parameter_context_sha256s=(verification.parameter_context_sha256s), + runtime_schema_sha256=candidate.runtime_schema_sha256, + source_dag_sha256=candidate.source_dag_sha256, + evaluator_output_partition_abi=(candidate.evaluator_output_partition_abi), + evaluator_output_partition_sha256=(candidate.evaluator_output_partition_sha256), + candidate_capture_sha256=candidate.capture_contract_sha256, + verification_capture_sha256=(verification.capture_contract_sha256), + process_id=process_id, + execution_mode=execution_mode, + precision_digits=precision_digits, + seed=seed, + relative_tolerance=relative_tolerance, + absolute_tolerance=absolute_tolerance, + ) + if ( + discovery.report.candidate_observation_batch_sha256 + != candidate.observation_batch_sha256 + or discovery.report.verification_observation_batch_sha256 + != verification.observation_batch_sha256 + or discovery.report.runtime_schema_sha256 != candidate.runtime_schema_sha256 + or discovery.report.source_dag_sha256 != candidate.source_dag_sha256 + or discovery.report.candidate_capture_sha256 + != candidate.capture_contract_sha256 + or discovery.report.verification_capture_sha256 + != verification.capture_contract_sha256 + ): + raise ValueError( + "numerical current detector did not authenticate the captured " + "observation batches" + ) + applicable_certificates = _application_scoped_certificates( + discovery.certificates, + allowed_relation_kinds=allowed_relation_kinds, + ) + application = apply_numerical_current_relation_certificates( + dag, + model, + applicable_certificates, + mode=mode, + execution_mode=execution_mode, + process_id=process_id, + runtime_schema_sha256=candidate.runtime_schema_sha256, + source_dag_sha256=candidate.source_dag_sha256, + candidate_capture_sha256=candidate.capture_contract_sha256, + verification_capture_sha256=(verification.capture_contract_sha256), + _structural_equivalences=discovery.structural_equivalences, + ) + if ( + application.report.certificates != applicable_certificates + or application.report.source_semantics_sha256 + != discovery.report.source_semantics_sha256 + ): + raise ValueError("numerical current application replay drifted from discovery") + application_capture: GenericDAGCurrentObservationCapture | None = None + if application.report.applied_relation_count: + application_capture = capture_generic_dag_current_observations( + application.dag, + model, + verification_points, + precision_digits=precision_digits, + parameter_contexts=verification_parameter_contexts, + ) + application_validation = _validate_applied_current_observations( + verification, + application_capture, + relative_tolerance=relative_tolerance, + absolute_tolerance=absolute_tolerance, + ) + else: + application_validation = { + "status": "not-required-no-applied-relations", + "checked_current_count": 0, + "checked_component_count": 0, + "maximum_absolute_residual": None, + "maximum_relative_residual": None, + "maximum_tolerance_ratio": None, + } + return GenericDAGNumericalCurrentWarmupResult( + dag=application.dag, + requested_mode=mode, + execution_mode=execution_mode, + candidate_capture=candidate, + verification_capture=verification, + application_capture=application_capture, + application_validation=application_validation, + discovery=discovery, + application=application, + application_scope={ + "abi": ("pyamplicol-generic-dag-numerical-relation-application-scope-v1"), + "policy": "exact-physical-selector-member-set-v1", + "reason": application_scope_reason, + "allowed_relation_kinds": list(allowed_relation_kinds), + "discovered_relation_count": len(discovery.certificates), + "application_eligible_relation_count": len(applicable_certificates), + "scope_suppressed_relation_count": ( + len(discovery.certificates) - len(applicable_certificates) + ), + }, + ) + + +def _validated_application_relation_kinds( + values: Sequence[Literal["equal", "opposite", "zero"]], +) -> tuple[Literal["equal", "opposite", "zero"], ...]: + resolved = tuple(values) + if len(set(resolved)) != len(resolved) or any( + relation_kind not in {"equal", "opposite", "zero"} for relation_kind in resolved + ): + raise ValueError("generic-DAG numerical relation application scope is invalid") + return resolved + + +def _application_scoped_certificates( + certificates: Sequence[NumericalCurrentRelationCertificate], + *, + allowed_relation_kinds: Sequence[Literal["equal", "opposite", "zero"]], +) -> tuple[NumericalCurrentRelationCertificate, ...]: + """Contain application without changing authenticated discovery evidence.""" + + allowed = frozenset(_validated_application_relation_kinds(allowed_relation_kinds)) + result: list[NumericalCurrentRelationCertificate] = [] + for certificate in certificates: + relation_kind = getattr(certificate, "relation_kind", None) + if relation_kind not in {"equal", "opposite", "zero"}: + raise ValueError( + "generic-DAG numerical certificate relation kind is invalid" + ) + if relation_kind in allowed: + result.append(certificate) + return tuple(result) + + +def generic_dag_numerical_current_opt_out_report( + dag: GenericDAG, + *, + execution_mode: Literal["compiled", "eager"], +) -> dict[str, object]: + """Report the explicit opt-out without hashing unused source semantics.""" + + return { + "schema_version": 1, + "abi": _WARMUP_ABI, + "requested_mode": "off", + "effective_mode": "off", + "effective_reuse_state": "disabled", + "state": "disabled-by-user", + "scope": { + "execution_mode": execution_mode, + "color_accuracy": str(dag.process.color_accuracy), + "representation": "generic-dag", + }, + "candidate_capture": None, + "verification_capture": None, + "application_capture": None, + "application_validation": { + "status": "disabled-by-user", + "checked_current_count": 0, + "checked_component_count": 0, + "maximum_absolute_residual": None, + "maximum_relative_residual": None, + "maximum_tolerance_ratio": None, + }, + "application_scope": { + "abi": ("pyamplicol-generic-dag-numerical-relation-application-scope-v1"), + "policy": "disabled-by-user", + "reason": "explicit-opt-out", + "allowed_relation_kinds": [], + "discovered_relation_count": 0, + "application_eligible_relation_count": 0, + "scope_suppressed_relation_count": 0, + }, + "discovery": None, + "application": None, + "certified_relation_count": 0, + "applied_relation_count": 0, + "relation_correctness": numerical_relation_correctness_payload(0), + "warning": { + "required": False, + "emit": "never", + "code": None, + "message": None, + }, + } + + +def build_numerical_current_probe_points( + dag: GenericDAG, + model: Model, + *, + process_id: str, + seed: int, + candidate_count: int, + verification_count: int, +) -> tuple[ + tuple[ValidationPointRecord, ...], + tuple[ValidationPointRecord, ...], +]: + """Build deterministic domain-separated physical warm-up points.""" + + if ( + type(seed) is not int + or seed < 0 + or type(candidate_count) is not int + or candidate_count < 2 + or type(verification_count) is not int + or verification_count < 2 + ): + raise ValueError("numerical current probe-point contract is invalid") + + def domain_points( + domain: str, + count: int, + ) -> tuple[ValidationPointRecord, ...]: + return tuple( + rotate_validation_point( + build_validation_point( + dag, + model, + process_id=process_id, + seed=_domain_seed(seed, domain=domain, index=index), + ), + rotation_seed=_domain_seed( + seed, + domain=f"{domain}:spatial-rotation-v1", + index=index, + ), + ) + for index in range(count) + ) + + candidate = domain_points("candidate-current-probes-v1", candidate_count) + verification = domain_points( + "independent-verification-current-probes-v1", + verification_count, + ) + if any(not point.available for point in (*candidate, *verification)): + errors = tuple( + point.error for point in (*candidate, *verification) if not point.available + ) + raise ValueError( + "numerical current probe-point generation failed: " + + "; ".join(str(error) for error in errors) + ) + candidate_hashes = {_kinematic_sha256(point) for point in candidate} + verification_hashes = {_kinematic_sha256(point) for point in verification} + if ( + len(candidate_hashes) != len(candidate) + or len(verification_hashes) != len(verification) + or not candidate_hashes.isdisjoint(verification_hashes) + ): + raise ValueError( + "numerical current probe domains do not contain distinct, " + "independent physical momentum records" + ) + return candidate, verification + + +def capture_generic_dag_current_observations( + dag: GenericDAG, + model: Model, + points: Sequence[ValidationPointRecord], + *, + precision_digits: int, + parameter_contexts: Sequence[Sequence[Decimal]] | None = None, + _session: _GenericDAGCurrentCaptureSession | None = None, +) -> GenericDAGCurrentObservationCapture: + """Evaluate and snapshot every current at every supplied physical point. + + The evaluators remain interpreted Symbolica states: this warm-up does not + launch a JIT, C++, ASM, or native compiler. Current components are copied + immediately after their producing stage, before value-slot recycling can + overwrite them. + """ + + if type(precision_digits) is not int or precision_digits < 80: + raise ValueError( + "numerical current capture precision must be at least 80 digits" + ) + point_records = tuple(points) + if len(point_records) < 2: + raise ValueError( + "numerical current capture requires at least two physical points" + ) + if any( + not isinstance(point, ValidationPointRecord) or not point.available + for point in point_records + ): + raise ValueError("numerical current capture received an unavailable point") + if any(point.process != dag.process.process for point in point_records): + raise ValueError( + "numerical current capture point process does not match the DAG" + ) + process_ids = {point.process_id for point in point_records} + if len(process_ids) != 1: + raise ValueError("numerical current capture points mix process identities") + process_id = next(iter(process_ids)) + session = ( + _build_current_capture_session( + dag, + model, + process_id=process_id, + ) + if _session is None + else _session + ) + if session.dag is not dag or session.process_id != process_id: + raise ValueError( + "numerical current capture session drifted from its source DAG" + ) + + schema = session.schema + blueprint_stages = session.stages + stage_evaluators = session.stage_evaluators + model_parameters = session.model_parameters + resolved_parameter_contexts = ( + tuple(model_parameters for _point in point_records) + if parameter_contexts is None + else tuple(tuple(context) for context in parameter_contexts) + ) + if len(resolved_parameter_contexts) != len(point_records) or any( + len(context) != len(model_parameters) + or any( + not isinstance(value, Decimal) or not value.is_finite() for value in context + ) + for context in resolved_parameter_contexts + ): + raise ValueError( + "numerical current parameter-probe contexts do not match the runtime schema" + ) + point_hashes = tuple(_validation_point_sha256(point) for point in point_records) + parameter_context_hashes = tuple( + _canonical_sha256( + { + "abi": "pyamplicol-numerical-current-parameter-context-v1", + "point_index": point_index, + "point_sha256": point_hash, + "values": [_decimal_string(value) for value in context], + } + ) + for point_index, (point_hash, context) in enumerate( + zip( + point_hashes, + resolved_parameter_contexts, + strict=True, + ) + ) + ) + kinematic_hashes = tuple(_kinematic_sha256(point) for point in point_records) + if len(set(kinematic_hashes)) != len(kinematic_hashes): + raise ValueError("numerical current capture requires distinct physical momenta") + captured_by_current: dict[int, list[_ComplexDecimal]] = { + current.id: [] for current in dag.currents + } + working_precision = precision_digits + _NUMERICAL_DECIMAL_GUARD_DIGITS + with localcontext() as context: + context.prec = working_precision + context.rounding = ROUND_HALF_EVEN + for point, parameter_context in zip( + point_records, + resolved_parameter_contexts, + strict=True, + ): + point_values = tuple( + tuple(Decimal.from_float(float(value)) for value in momentum) + for momentum in point.four_vectors + ) + point_capture = _capture_one_point( + dag, + schema, + blueprint_stages, + stage_evaluators, + point_values, + parameter_context, + precision=working_precision, + ) + for current in dag.currents: + values = point_capture[current.id] + if len(values) != current.dimension: + raise ValueError( + f"numerical current {current.id} capture has " + f"{len(values)} components, expected {current.dimension}" + ) + captured_by_current[current.id].extend(values) + + observations = { + current_id: tuple(values) for current_id, values in captured_by_current.items() + } + if any( + not real.is_finite() or not imaginary.is_finite() + for values in observations.values() + for real, imaginary in values + ): + raise ValueError("numerical current capture produced a non-finite component") + observation_digest = _current_observation_batch_sha256( + observations, + point_sha256s=point_hashes, + ) + model_parameter_schema_digest = _canonical_sha256( + schema.get("model_parameters", ()) + ) + capture_contract_digest = _canonical_sha256( + { + "abi": _CAPTURE_ABI, + "precision_digits": precision_digits, + "point_sha256s": list(point_hashes), + "kinematic_sha256s": list(kinematic_hashes), + "parameter_context_sha256s": list(parameter_context_hashes), + "runtime_schema_sha256": session.runtime_schema.sha256, + "model_parameter_schema_sha256": (model_parameter_schema_digest), + "source_dag_sha256": session.source_dag_sha256, + "evaluator_output_partition_abi": (session.evaluator_output_partition_abi), + "evaluator_output_partition_sha256": ( + session.evaluator_output_partition_sha256 + ), + "observation_batch_sha256": observation_digest, + } + ) + return GenericDAGCurrentObservationCapture( + precision_digits=precision_digits, + points=point_records, + point_sha256s=point_hashes, + kinematic_sha256s=kinematic_hashes, + parameter_contexts=resolved_parameter_contexts, + parameter_context_sha256s=parameter_context_hashes, + observations=observations, + runtime_schema_sha256=session.runtime_schema.sha256, + model_parameter_schema_sha256=model_parameter_schema_digest, + source_dag_sha256=session.source_dag_sha256, + evaluator_output_partition_abi=(session.evaluator_output_partition_abi), + evaluator_output_partition_sha256=(session.evaluator_output_partition_sha256), + observation_batch_sha256=observation_digest, + capture_contract_sha256=capture_contract_digest, + ) + + +def _current_observation_batch_sha256( + observations: Mapping[int, tuple[_ComplexDecimal, ...]], + *, + point_sha256s: tuple[str, ...], +) -> str: + return _canonical_sha256( + { + "point_sha256s": list(point_sha256s), + "currents": [ + { + "current_id": current_id, + "values": [ + [_decimal_string(real), _decimal_string(imaginary)] + for real, imaginary in observations[current_id] + ], + } + for current_id in sorted(observations) + ], + } + ) + + +def _current_capture_output_partitions( + stage: GenericCompiledStageBlueprint, +) -> tuple[tuple[tuple[int, int], ...], dict[str, object]]: + """Return exhaustive current-ID output partitions and their identity.""" + + if ( + type(stage.output_length) is not int + or stage.output_length < 1 + or not stage.output_slots + ): + raise ValueError("numerical current capture stage has no partitionable outputs") + slot_ids = tuple(slot.value_slot_id for slot in stage.output_slots) + if slot_ids != stage.output_value_slot_ids: + raise ValueError("numerical current capture output-slot identity drifted") + + ranges: list[tuple[int, int]] = [] + records: list[dict[str, object]] = [] + current_id: int | None = None + partition_start = 0 + partition_slots: list[dict[str, object]] = [] + seen_current_ids: set[int] = set() + seen_value_slot_ids: set[int] = set() + expected_output_start = 0 + + def finish_partition(stop: int) -> None: + if current_id is None or not partition_slots or partition_start >= stop: + raise ValueError("numerical current capture output partition is empty") + ranges.append((partition_start, stop)) + records.append( + { + "current_id": current_id, + "output_start": partition_start, + "output_stop": stop, + "slots": list(partition_slots), + } + ) + + for slot in stage.output_slots: + if ( + type(slot.current_id) is not int + or slot.current_id < 0 + or type(slot.value_slot_id) is not int + or slot.value_slot_id < 0 + or slot.value_slot_id in seen_value_slot_ids + or type(slot.output_start) is not int + or type(slot.output_stop) is not int + or slot.output_start != expected_output_start + or slot.output_stop <= slot.output_start + or slot.output_stop > stage.output_length + or slot.output_stop - slot.output_start + != slot.component_stop - slot.component_start + ): + raise ValueError( + "numerical current capture output slots overlap, gap, or " + "have an invalid identity" + ) + if current_id is None: + current_id = slot.current_id + seen_current_ids.add(current_id) + elif slot.current_id != current_id: + finish_partition(slot.output_start) + if slot.current_id in seen_current_ids or slot.current_id <= current_id: + raise ValueError( + "numerical current capture output-slot current order drifted" + ) + current_id = slot.current_id + seen_current_ids.add(current_id) + partition_start = slot.output_start + partition_slots = [] + seen_value_slot_ids.add(slot.value_slot_id) + partition_slots.append( + { + "value_slot_id": slot.value_slot_id, + "variant": slot.variant, + "component_start": slot.component_start, + "component_stop": slot.component_stop, + "output_start": slot.output_start, + "output_stop": slot.output_stop, + } + ) + expected_output_start = slot.output_stop + + if expected_output_start != stage.output_length: + raise ValueError( + "numerical current capture output partitions are not exhaustive" + ) + finish_partition(stage.output_length) + return tuple(ranges), { + "stage_index": stage.stage_index, + "stage_kind": stage.stage_kind, + "subset_size": stage.subset_size, + "output_length": stage.output_length, + "partitions": records, + } + + +def _current_capture_partition_plan( + stages: Sequence[GenericCompiledStageBlueprint], +) -> tuple[tuple[tuple[tuple[int, int], ...], ...], str]: + """Authenticate one stable current-ID partition plan for all stages.""" + + stage_partitions: list[tuple[tuple[int, int], ...]] = [] + stage_contracts: list[dict[str, object]] = [] + previous_stage_index: int | None = None + for stage in stages: + if type(stage.stage_index) is not int or ( + previous_stage_index is not None + and stage.stage_index <= previous_stage_index + ): + raise ValueError("numerical current capture stage order drifted") + partitions, contract = _current_capture_output_partitions(stage) + stage_partitions.append(partitions) + stage_contracts.append(contract) + previous_stage_index = stage.stage_index + if not stage_partitions: + raise ValueError("numerical current capture has no evaluator stages") + payload = { + "abi": _CAPTURE_OUTPUT_PARTITION_ABI, + "strategy": "one-contiguous-output-partition-per-current-id", + "stages": stage_contracts, + } + return tuple(stage_partitions), _canonical_sha256(payload) + + +def _build_current_capture_session( + dag: GenericDAG, + model: Model, + *, + process_id: str, +) -> _GenericDAGCurrentCaptureSession: + runtime_schema = build_runtime_expression_schema( + dag, + model, + process_id=process_id, + ) + schema = runtime_schema.to_mapping() + blueprint = build_generic_stage_compiler_blueprint( + StageCompilationInput(dag, model, runtime_schema) + ) + if not blueprint.expression_ready: + raise ValueError( + "numerical current capture cannot lower stage expressions: " + + "; ".join(blueprint.blockers) + ) + settings = SymbolicaEvaluatorSettings( + iterations=1, + cpe_iterations=0, + n_cores=1, + direct_translation=False, + jit_direct_translation=False, + jit_optimization_level=0, + compiled_output_chunk_size=None, + output_chunk_strategy="uniform", + compiled_chunk_compile_workers=1, + ) + stage_output_partitions, output_partition_digest = _current_capture_partition_plan( + blueprint.stages + ) + runtime_output_partition_digest = ( + generic_dag_numerical_capture_output_partition_sha256(schema) + ) + if output_partition_digest != runtime_output_partition_digest: + raise ValueError( + "numerical current capture evaluator partitions drifted from " + "runtime output-slot geometry" + ) + stage_evaluators = tuple( + _build_interpreted_stage_evaluator( + stage, + settings=settings, + output_partitions=output_partitions, + ) + for stage, output_partitions in zip( + blueprint.stages, + stage_output_partitions, + strict=True, + ) + ) + model_parameters = _runtime_model_parameter_values(schema) + return _GenericDAGCurrentCaptureSession( + dag=dag, + process_id=process_id, + runtime_schema=runtime_schema, + schema=schema, + stages=blueprint.stages, + stage_evaluators=stage_evaluators, + model_parameters=model_parameters, + source_dag_sha256=_canonical_sha256(dag.to_json_dict()), + evaluator_output_partition_abi=_CAPTURE_OUTPUT_PARTITION_ABI, + evaluator_output_partition_sha256=output_partition_digest, + ) + + +def validate_independent_current_observation_captures( + candidate: GenericDAGCurrentObservationCapture, + verification: GenericDAGCurrentObservationCapture, +) -> None: + """Fail closed unless two capture batches form one independent contract.""" + + if ( + candidate.precision_digits != verification.precision_digits + or candidate.runtime_schema_sha256 != verification.runtime_schema_sha256 + or candidate.model_parameter_schema_sha256 + != verification.model_parameter_schema_sha256 + or candidate.source_dag_sha256 != verification.source_dag_sha256 + or candidate.evaluator_output_partition_abi + != verification.evaluator_output_partition_abi + or candidate.evaluator_output_partition_sha256 + != verification.evaluator_output_partition_sha256 + or set(candidate.observations) != set(verification.observations) + or candidate.point_count < 2 + or verification.point_count < 2 + or len(set(candidate.point_sha256s)) != candidate.point_count + or len(set(verification.point_sha256s)) != verification.point_count + or not set(candidate.point_sha256s).isdisjoint(verification.point_sha256s) + or len(set(candidate.kinematic_sha256s)) != candidate.point_count + or len(set(verification.kinematic_sha256s)) != verification.point_count + or not set(candidate.kinematic_sha256s).isdisjoint( + verification.kinematic_sha256s + ) + or len(candidate.parameter_context_sha256s) != candidate.point_count + or len(verification.parameter_context_sha256s) != verification.point_count + or not set(candidate.parameter_context_sha256s).isdisjoint( + verification.parameter_context_sha256s + ) + ): + raise ValueError( + "numerical current candidate and verification captures do not " + "form independent replay domains" + ) + for current_id in candidate.observations: + candidate_values = candidate.observations[current_id] + verification_values = verification.observations[current_id] + if ( + len(candidate_values) % candidate.point_count != 0 + or len(verification_values) % verification.point_count != 0 + or len(candidate_values) // candidate.point_count + != len(verification_values) // verification.point_count + ): + raise ValueError( + f"numerical current {current_id} capture widths drifted " + "between candidate and verification domains" + ) + + +def _validate_applied_current_observations( + reference: GenericDAGCurrentObservationCapture, + applied: GenericDAGCurrentObservationCapture, + *, + relative_tolerance: float, + absolute_tolerance: float, +) -> dict[str, object]: + """Authenticate that reuse application preserves every observed current.""" + + if ( + reference.precision_digits != applied.precision_digits + or reference.point_sha256s != applied.point_sha256s + or reference.kinematic_sha256s != applied.kinematic_sha256s + or reference.parameter_contexts != applied.parameter_contexts + or reference.parameter_context_sha256s != applied.parameter_context_sha256s + or reference.model_parameter_schema_sha256 + != applied.model_parameter_schema_sha256 + or reference.evaluator_output_partition_abi + != applied.evaluator_output_partition_abi + or reference.evaluator_output_partition_sha256 + != applied.evaluator_output_partition_sha256 + or set(reference.observations) != set(applied.observations) + ): + raise ValueError("numerical current application validation provenance drifted") + with localcontext() as context: + context.prec = reference.precision_digits + _NUMERICAL_DECIMAL_GUARD_DIGITS + context.rounding = ROUND_HALF_EVEN + relative = Decimal.from_float(float(relative_tolerance)) + absolute = Decimal.from_float(float(absolute_tolerance)) + if ( + not relative.is_finite() + or not absolute.is_finite() + or relative < 0 + or absolute < 0 + or (relative == 0 and absolute == 0) + ): + raise ValueError( + "numerical current application validation tolerances are invalid" + ) + maximum_absolute = Decimal(0) + maximum_relative = Decimal(0) + maximum_tolerance_ratio = Decimal(0) + checked_components = 0 + for current_id in sorted(reference.observations): + before = reference.observations[current_id] + after = applied.observations[current_id] + if len(before) != len(after): + raise ValueError( + f"numerical current {current_id} application width drifted" + ) + for observation_index, (baseline, optimized) in enumerate( + zip(before, after, strict=True) + ): + difference = max( + abs(baseline[0] - optimized[0]), + abs(baseline[1] - optimized[1]), + ) + scale = max( + abs(baseline[0]), + abs(baseline[1]), + abs(optimized[0]), + abs(optimized[1]), + ) + allowed = absolute + relative * scale + relative_residual = ( + Decimal(0) if difference == 0 else difference / max(scale, absolute) + ) + tolerance_ratio = ( + Decimal(0) if difference == 0 else difference / allowed + ) + maximum_absolute = max(maximum_absolute, difference) + maximum_relative = max( + maximum_relative, + relative_residual, + ) + maximum_tolerance_ratio = max( + maximum_tolerance_ratio, + tolerance_ratio, + ) + checked_components += 1 + if not difference <= allowed: + raise ValueError( + "numerical current application changed current " + f"{current_id} observation {observation_index} beyond " + "its authenticated tolerance: " + f"difference={_decimal_string(difference)}, " + f"allowed={_decimal_string(allowed)}, " + "tolerance_ratio=" + f"{_decimal_string(tolerance_ratio)}" + ) + return { + "status": "verified", + "checked_current_count": len(reference.observations), + "checked_component_count": checked_components, + "maximum_absolute_residual": _decimal_string(maximum_absolute), + "maximum_relative_residual": _decimal_string(maximum_relative), + "maximum_tolerance_ratio": _decimal_string(maximum_tolerance_ratio), + "reference_observation_batch_sha256": (reference.observation_batch_sha256), + "applied_observation_batch_sha256": (applied.observation_batch_sha256), + "evaluator_output_partition_abi": ( + reference.evaluator_output_partition_abi + ), + "evaluator_output_partition_sha256": ( + reference.evaluator_output_partition_sha256 + ), + } + + +def _build_interpreted_stage_evaluator( + stage: GenericCompiledStageBlueprint, + *, + settings: SymbolicaEvaluatorSettings, + output_partitions: Sequence[tuple[int, int]], +) -> Any: + evaluator = _compile_symbolica_outputs( + stage.output_expressions, + list(stage.parameter_symbols), + merge_evaluators_strategy=False, + verbose_evaluator_build=False, + functions={ + (function, arguments): body + for function, arguments, body in stage.symbolica_functions + }, + real_params=stage.real_valued_inputs, + symbolica_settings=settings, + jit_compile=False, + label=f"numerical_current_warmup_stage_{stage.stage_index}", + output_partitions=output_partitions, + ) + partitions = tuple(output_partitions) + if len(partitions) == 1: + evaluators = (evaluator,) + chunk_input_indices = (tuple(range(stage.parameter_count)),) + else: + evaluators = getattr(evaluator, "_evaluators", None) + chunk_input_indices = getattr( + evaluator, + "_chunk_input_indices", + None, + ) + if not isinstance(evaluators, tuple) or not isinstance( + chunk_input_indices, + tuple, + ): + raise ValueError( + "numerical current warm-up partition compilation did not " + "retain its evaluator leaves" + ) + return _PartitionedCurrentCaptureStageEvaluator( + evaluators=evaluators, + chunk_input_indices=chunk_input_indices, + output_partitions=partitions, + input_len=stage.parameter_count, + output_len=stage.output_length, + ) + + +def _capture_one_point( + dag: GenericDAG, + schema: Mapping[str, object], + stages: tuple[GenericCompiledStageBlueprint, ...], + evaluators: tuple[Any, ...], + point: tuple[tuple[Decimal, ...], ...], + model_parameters: tuple[Decimal, ...], + *, + precision: int, +) -> dict[int, tuple[_ComplexDecimal, ...]]: + layout = _mapping(schema.get("parameter_layout"), "parameter layout") + value_count = _integer(layout.get("value_component_count")) + momentum_count = _integer(layout.get("momentum_parameter_count")) + flattened_count = _integer(layout.get("parameter_count_if_flattened")) + model_start = value_count + momentum_count + state = [ + (Decimal(0), Decimal(0)) + for _index in range(max(flattened_count, model_start + len(model_parameters))) + ] + _fill_sources(state, point, schema, model_parameters) + _fill_momenta(state, point, schema) + for index, value in enumerate(model_parameters): + state[model_start + index] = (value, Decimal(0)) + + captured: dict[int, tuple[_ComplexDecimal, ...]] = {} + source_fill = _mapping(schema.get("source_fill"), "source fill") + sources = _sequence(source_fill.get("sources"), "source rows") + for raw_source in sources: + source = _mapping(raw_source, "source row") + slot = _mapping(source.get("value_slot"), "source value slot") + current_id = _integer(source.get("current_id")) + start = _integer(slot.get("component_start")) + stop = _integer(slot.get("component_stop")) + if current_id in captured or not 0 <= start <= stop <= len(state): + raise ValueError("numerical current source capture has an invalid slot") + captured[current_id] = tuple(state[start:stop]) + + for stage, evaluator in zip(stages, evaluators, strict=True): + inputs: list[_ComplexDecimal | None] = [None] * stage.parameter_count + for component in stage.input_components: + if ( + not 0 <= component.parameter_index < len(inputs) + or not 0 <= component.global_component < len(state) + or inputs[component.parameter_index] is not None + ): + raise ValueError("numerical current stage input mapping is invalid") + inputs[component.parameter_index] = state[component.global_component] + if any(value is None for value in inputs): + raise ValueError("numerical current stage input mapping is incomplete") + outputs = _evaluate_interpreted_stage( + evaluator, + tuple(value for value in inputs if value is not None), + precision=precision, + ) + if len(outputs) != stage.output_length: + raise ValueError( + "numerical current stage evaluator returned the wrong width" + ) + for slot in stage.output_slots: + values = outputs[slot.output_start : slot.output_stop] + if ( + slot.current_id in captured + or len(values) != slot.component_stop - slot.component_start + or not 0 <= slot.component_start <= slot.component_stop <= len(state) + ): + raise ValueError("numerical current stage output mapping is invalid") + state[slot.component_start : slot.component_stop] = values + captured[slot.current_id] = values + if set(captured) != {current.id for current in dag.currents}: + raise ValueError( + "numerical current capture did not visit every current exactly once" + ) + return captured + + +def _evaluate_interpreted_stage( + evaluator: Any, + values: tuple[_ComplexDecimal, ...], + *, + precision: int, +) -> tuple[_ComplexDecimal, ...]: + evaluate = getattr(evaluator, "evaluate_complex_with_prec", None) + if not callable(evaluate): + raise ValueError( + "numerical current warm-up evaluator lacks high-precision replay" + ) + try: + raw = evaluate(values, precision) + except Exception as error: + raise ValueError( + f"numerical current high-precision stage replay failed: {error}" + ) from error + return tuple( + ( + _decimal(real, "numerical current real component"), + _decimal(imaginary, "numerical current imaginary component"), + ) + for value in raw + for real, imaginary in (value.to_decimal_tuple(),) + ) + + +def _runtime_model_parameter_values( + schema: Mapping[str, object], +) -> tuple[Decimal, ...]: + records = _sequence(schema.get("model_parameters", ()), "model parameters") + ordered: list[Decimal | None] = [None] * len(records) + for raw_record in records: + record = _mapping(raw_record, "model parameter") + index = _integer(record.get("parameter_index")) + if ( + not 0 <= index < len(ordered) + or ordered[index] is not None + or "default" not in record + ): + raise ValueError("numerical current model-parameter layout is invalid") + value = float(record["default"]) + if not isfinite(value): + raise ValueError("numerical current model parameter is non-finite") + ordered[index] = Decimal.from_float(value) + if any(value is None for value in ordered): + raise ValueError("numerical current model-parameter layout is incomplete") + return tuple(value for value in ordered if value is not None) + + +def _build_parameter_probe_contexts( + defaults: tuple[Decimal, ...], + *, + precision_digits: int, + seed: int, + domain: str, + count: int, + include_defaults: bool, +) -> tuple[tuple[Decimal, ...], ...]: + """Build independent deterministic probes over every runtime slot. + + Runtime APIs can override external and derived flattened parameter slots. + Sampling every slot independently is therefore a conservative check: a + relation that exists only at the model defaults is not eligible for + permanent evaluator reuse. + """ + + if ( + type(precision_digits) is not int + or precision_digits < 80 + or type(seed) is not int + or seed < 0 + or not isinstance(domain, str) + or not domain + or type(count) is not int + or count < 2 + ): + raise ValueError("numerical current parameter-probe contract is invalid") + working_precision = precision_digits + 16 + contexts: list[tuple[Decimal, ...]] = [] + with localcontext() as context: + context.prec = working_precision + context.rounding = ROUND_HALF_EVEN + for probe_index in range(count): + if include_defaults and probe_index == 0: + contexts.append(defaults) + continue + values: list[Decimal] = [] + for parameter_index, default in enumerate(defaults): + digest = hashlib.sha256( + (f"{seed}:{domain}:{probe_index}:{parameter_index}").encode("ascii") + ).digest() + signed = int.from_bytes(digest[:8], "big") - (1 << 63) + if signed == 0: + signed = 1 + # At most a 1/16 perturbation of max(|default|, 1). + relative_offset = Decimal(signed) / Decimal(1 << 67) + scale = max(abs(default), Decimal(1)) + values.append(default + scale * relative_offset) + contexts.append(tuple(values)) + return tuple(contexts) + + +def _validation_point_sha256(point: ValidationPointRecord) -> str: + return _canonical_sha256(point.to_mapping()) + + +def _kinematic_sha256(point: ValidationPointRecord) -> str: + return _canonical_sha256( + [ + { + "pdg": pdg, + "momentum": [format(float(value), ".17g") for value in momentum], + } + for pdg, momentum in point.particles + ] + ) + + +def _domain_seed(seed: int, *, domain: str, index: int) -> int: + digest = hashlib.sha256(f"{seed}:{domain}:{index}".encode("ascii")).digest() + return int.from_bytes(digest[:8], "big") & ((1 << 63) - 1) + + +def _canonical_sha256(value: object) -> str: + payload = json.dumps( + value, + allow_nan=False, + ensure_ascii=True, + separators=(",", ":"), + sort_keys=True, + ).encode("ascii") + return hashlib.sha256(payload).hexdigest() + + +def _decimal_string(value: Decimal) -> str: + if not value.is_finite(): + raise ValueError("canonical Decimal serialization requires finiteness") + sign, digits, exponent = value.as_tuple() + if not any(digits): + return "0" + if not isinstance(exponent, int): + raise ValueError("canonical Decimal serialization requires an exponent") + coefficient = "".join(str(digit) for digit in digits) + return f"{'-' if sign else ''}{coefficient}e{exponent}" + + +def _mapping(value: object, label: str) -> Mapping[str, object]: + if not isinstance(value, Mapping): + raise ValueError(f"numerical current {label} is not an object") + return value + + +def _sequence(value: object, label: str) -> Sequence[object]: + if isinstance(value, str | bytes) or not isinstance(value, Sequence): + raise ValueError(f"numerical current {label} is not a sequence") + return value + + +def _integer(value: object) -> int: + if isinstance(value, bool) or not isinstance(value, int) or value < 0: + raise ValueError("numerical current layout integer is invalid") + return value + + +__all__ = [ + "GenericDAGCurrentObservationCapture", + "GenericDAGNumericalCurrentWarmupResult", + "build_numerical_current_probe_points", + "capture_generic_dag_current_observations", + "generic_dag_numerical_current_opt_out_report", + "run_generic_dag_numerical_current_warmup", + "validate_independent_current_observation_captures", +] diff --git a/pyamplicol/src/pyamplicol/generation/on_the_fly_seed.py b/pyamplicol/src/pyamplicol/generation/on_the_fly_seed.py new file mode 100644 index 0000000000..b4815ada43 --- /dev/null +++ b/pyamplicol/src/pyamplicol/generation/on_the_fly_seed.py @@ -0,0 +1,288 @@ +# SPDX-License-Identifier: 0BSD +"""Compact source projection for the private on-the-fly lane. + +This boundary deliberately stops before color-sector planning, process-DAG +construction, recurrence schedule lowering, and numerical relation discovery. +It retains only the canonical external source domain and the model-owned roots +needed by Rust to discover one requested recurrence at runtime. Contracted +NLC/full generation additionally retains a Python-only color plan beside the +seed; it never enters the process-seed-v1 wire contract. +""" + +from __future__ import annotations + +import json +from collections.abc import Mapping, Sequence +from dataclasses import dataclass +from typing import Final, Literal + +from ..color import GenericColorPlan, build_color_plan +from ..models.base import Model +from ..models.recurrence_template import RecurrenceTemplateCatalog +from ..processes.ir import CanonicalProcessIR +from .recurrence_columnar import ( + ExactComplexRationalV1, + RecurrenceCouplingLimitV1, + RecurrenceExternalLegV1, + RecurrenceNormalizationV1, + RecurrenceParameterProjectionV1, +) +from .recurrence_fermion_pairing import ( + FermionPairingRootsV1, + build_recurrence_fermion_pairing_roots_v1, +) +from .recurrence_physics import build_recurrence_normalization +from .recurrence_projection import ( + RecurrenceProjectionError, + _digest, + _project_coupling_limits, + _project_external_legs, + _project_parameters, + _project_template_references, +) + +ON_THE_FLY_SOURCE_PROJECTION_SCHEMA: Final = 1 + + +@dataclass(frozen=True, slots=True) +class OnTheFlyProcessSeedProjectionV1: + """Minimal deterministic input to the native process-seed builder.""" + + process_digest: str + external_sources: tuple[RecurrenceExternalLegV1, ...] + external_permutation: tuple[int, ...] + parameter_projection: tuple[RecurrenceParameterProjectionV1, ...] + coupling_order_policy: Literal["minimal", "explicit"] + coupling_hierarchies: tuple[tuple[str, int], ...] + coupling_limits: tuple[RecurrenceCouplingLimitV1, ...] + fermion_pairing: FermionPairingRootsV1 | None + normalization: RecurrenceNormalizationV1 + + def to_json_dict(self) -> dict[str, object]: + """Return the exact private JSON root accepted by the Rust builder.""" + + return { + "schema_version": ON_THE_FLY_SOURCE_PROJECTION_SCHEMA, + "process_digest": self.process_digest, + "external_permutation": list(self.external_permutation), + "external_sources": [ + { + "source_slot": leg.source_slot, + "public_label": leg.public_label, + "is_initial": leg.is_initial, + "states": [ + { + "state_index": state.state_index, + "public_helicity": state.public_helicity, + # The direct source executor consumes the same + # crossed helicity as the public recurrence source + # state. The source template retains the declared + # (outgoing-basis) helicity separately by ID. + "source_helicity": state.public_helicity, + "source_template_id": state.source_template_id, + "current_state_template_id": ( + state.current_state_template_id + ), + "momentum_sign": state.momentum_sign, + "crossing_phase": _exact_json(state.crossing_phase), + "spin_state": state.spin_state, + "chirality": state.chirality, + } + for state in leg.source_states + ], + } + for leg in self.external_sources + ], + "parameter_projection": [ + { + "parameter_template_id": row.parameter_template_id, + "prepared_parameter_id": row.prepared_parameter_id, + "component": row.component, + } + for row in self.parameter_projection + ], + "coupling_order_policy": self.coupling_order_policy, + "coupling_hierarchies": [ + {"name": name, "hierarchy": hierarchy} + for name, hierarchy in self.coupling_hierarchies + ], + "coupling_limits": [ + {"name": row.name, "maximum": row.maximum} + for row in self.coupling_limits + ], + "fermion_pairing": ( + None + if self.fermion_pairing is None + else { + "endpoints": [ + { + "source_slot": endpoint.source_slot, + "color_orientation": endpoint.color_orientation, + "contract_digest": endpoint.contract_digest, + } + for endpoint in self.fermion_pairing.endpoints + ], + "classes": [ + { + "species": pairing_class.species, + "proof_digest": pairing_class.proof_digest, + "fundamental_source_slots": list( + pairing_class.fundamental_source_slots + ), + "antifundamental_source_slots": list( + pairing_class.antifundamental_source_slots + ), + } + for pairing_class in self.fermion_pairing.classes + ], + } + ), + "normalization": { + "factor": _exact_json(self.normalization.factor), + "convention": self.normalization.convention, + "semantic_digest": self.normalization.semantic_digest, + }, + } + + def to_json_bytes(self) -> bytes: + """Encode canonical ASCII without adding a second artifact identity.""" + + return json.dumps( + self.to_json_dict(), + allow_nan=False, + ensure_ascii=True, + separators=(",", ":"), + sort_keys=True, + ).encode("ascii") + + +@dataclass(frozen=True, slots=True) +class OnTheFlyGenerationProjectionV1: + """Source seed plus existing runtime-normalization metadata for the writer.""" + + seed: OnTheFlyProcessSeedProjectionV1 + runtime_normalization: Mapping[str, object] + color_plan: GenericColorPlan | None = None + + +def project_on_the_fly_process_seed_v1( + process: CanonicalProcessIR, + template_catalog: RecurrenceTemplateCatalog, + model: Model, + *, + coupling_order_policy: Literal["minimal", "explicit"], + coupling_order_limits: Mapping[str, int], + reference_color_order: Sequence[int] | None = None, + color_basis: Literal["trace", "adjoint"] = "trace", +) -> OnTheFlyGenerationProjectionV1: + """Project one complete source domain without materializing a process DAG. + + The projection always retains every source state. Runtime selectors choose + helicities after loading. LC also chooses a color flow lazily; NLC/full + carries a separate contracted-color payload over the same structural + selector domain. + """ + + if not isinstance(process, CanonicalProcessIR): + raise TypeError("on-the-fly projection requires a CanonicalProcessIR") + if process.color_accuracy not in {"lc", "nlc", "full"}: + raise RecurrenceProjectionError( + "on-the-fly source projection requires LC, NLC, or full color" + ) + if not isinstance(template_catalog, RecurrenceTemplateCatalog): + raise TypeError( + "on-the-fly projection requires a validated recurrence template catalog" + ) + if coupling_order_policy not in {"minimal", "explicit"}: + raise RecurrenceProjectionError( + "on-the-fly coupling-order policy must be 'minimal' or 'explicit'" + ) + + template_ids, _template_references = _project_template_references(template_catalog) + external_sources, selected_sources = _project_external_legs( + process, + template_catalog, + template_ids, + None, + 1, + ) + if selected_sources is not None: # pragma: no cover - fixed by this call + raise AssertionError("complete on-the-fly source projection was trimmed") + pairing = build_recurrence_fermion_pairing_roots_v1( + process, + template_catalog.current_states, + quantum_flows=template_catalog.quantum_flows, + ) + normalization, runtime_normalization = build_recurrence_normalization( + process, + model, + ) + color_plan = None + if process.color_accuracy in {"nlc", "full"}: + color_plan = build_color_plan( + process, + basis=color_basis, + color_accuracy=process.color_accuracy, + reference_color_order=reference_color_order, + fold_trace_reflections=False, + ) + if color_plan.truncated or not color_plan.sectors: + detail = "; ".join(color_plan.diagnostics) or "no color sectors" + raise RecurrenceProjectionError( + "on-the-fly contracted-color projection has no complete color " + f"plan: {detail}" + ) + coupling_order_names = sorted( + { + str(name).upper() + for records in ( + template_catalog.quantum_flows, + template_catalog.transitions, + template_catalog.closures, + ) + for record in records + for name, _power in record.coupling_orders + } + ) + projected_coupling_limits = _project_coupling_limits(coupling_order_limits) + model_hierarchies = { + str(name).upper(): max(1, int(value)) + for name, value in model.coupling_order_hierarchies().items() + } + seed = OnTheFlyProcessSeedProjectionV1( + process_digest=_digest(process.to_json_dict()), + external_sources=external_sources, + external_permutation=tuple(range(len(process.legs))), + parameter_projection=_project_parameters(template_catalog, template_ids), + coupling_order_policy=coupling_order_policy, + coupling_hierarchies=tuple( + (name, model_hierarchies.get(name, 1)) for name in coupling_order_names + ), + coupling_limits=tuple( + row for row in projected_coupling_limits if row.name in coupling_order_names + ), + fermion_pairing=(None if not pairing.endpoints else pairing), + normalization=normalization, + ) + return OnTheFlyGenerationProjectionV1( + seed=seed, + runtime_normalization=runtime_normalization, + color_plan=color_plan, + ) + + +def _exact_json(value: ExactComplexRationalV1) -> dict[str, str]: + return { + "real_numerator": str(value.real_numerator), + "real_denominator": str(value.real_denominator), + "imag_numerator": str(value.imag_numerator), + "imag_denominator": str(value.imag_denominator), + } + + +__all__ = [ + "ON_THE_FLY_SOURCE_PROJECTION_SCHEMA", + "OnTheFlyGenerationProjectionV1", + "OnTheFlyProcessSeedProjectionV1", + "project_on_the_fly_process_seed_v1", +] diff --git a/pyamplicol/src/pyamplicol/generation/phase_space.py b/pyamplicol/src/pyamplicol/generation/phase_space.py new file mode 100644 index 0000000000..744c7cd449 --- /dev/null +++ b/pyamplicol/src/pyamplicol/generation/phase_space.py @@ -0,0 +1,183 @@ +# SPDX-License-Identifier: 0BSD +from __future__ import annotations + +import math +from collections.abc import Sequence + +import numpy as np + +from .._internal.physics.types import FourMomentum, NativeEvaluationError + + +def massive_rambo_final_state( + multiplicity: int, + *, + sqrt_s: float, + masses: Sequence[float], + seed: int, +) -> tuple[FourMomentum, ...]: + """Generate a deterministic massive RAMBO final state. + + This process-independent helper is the public version of the RAMBO + construction used by the generic DAG artifact writer. It returns momenta + in the centre-of-mass frame with total four-momentum + ``(sqrt_s, 0, 0, 0)``. + """ + + return _massive_rambo_final_state( + multiplicity, + sqrt_s=sqrt_s, + masses=masses, + rng=np.random.default_rng(seed), + ) + + +def _massive_rambo_final_state( + multiplicity: int, + *, + sqrt_s: float, + masses: Sequence[float], + rng: np.random.Generator, +) -> tuple[FourMomentum, ...]: + if multiplicity != len(masses): + raise NativeEvaluationError("RAMBO mass list length mismatch") + massless = _massless_rambo_final_state(multiplicity, sqrt_s=sqrt_s, rng=rng) + spatial_norms = tuple( + math.sqrt(momentum[1] ** 2 + momentum[2] ** 2 + momentum[3] ** 2) + for momentum in massless + ) + scale = _massive_spatial_scale(spatial_norms, masses, sqrt_s=sqrt_s) + final: list[FourMomentum] = [] + for momentum, mass, spatial_norm in zip( + massless, + masses, + spatial_norms, + strict=True, + ): + px = scale * momentum[1] + py = scale * momentum[2] + pz = scale * momentum[3] + energy = math.sqrt(mass * mass + (scale * spatial_norm) ** 2) + final.append((energy, px, py, pz)) + return tuple(final) + + +def _massless_rambo_final_state( + multiplicity: int, + *, + sqrt_s: float, + rng: np.random.Generator, +) -> tuple[FourMomentum, ...]: + if multiplicity < 2: + raise NativeEvaluationError("RAMBO needs at least two final particles") + raw: list[FourMomentum] = [] + for _ in range(multiplicity): + r = rng.random(4) + costheta = 2.0 * r[0] - 1.0 + sintheta = math.sqrt(max(0.0, 1.0 - costheta * costheta)) + phi = 2.0 * math.pi * r[1] + energy = -math.log(max(r[2] * r[3], np.finfo(float).tiny)) + raw.append( + ( + energy, + energy * sintheta * math.cos(phi), + energy * sintheta * math.sin(phi), + energy * costheta, + ) + ) + + total = _sum_momenta(raw) + invariant = _minkowski_square(total) + if invariant <= 0.0: + raise NativeEvaluationError("RAMBO generated a non-timelike total momentum") + mass = math.sqrt(invariant) + beta_to_rest = ( + -total[1] / total[0], + -total[2] / total[0], + -total[3] / total[0], + ) + scale = sqrt_s / mass + return tuple( + ( + scale * boosted[0], + scale * boosted[1], + scale * boosted[2], + scale * boosted[3], + ) + for boosted in (_boost_from_rest(momentum, beta_to_rest) for momentum in raw) + ) + + +def _boost_from_rest( + momentum: FourMomentum, beta: tuple[float, float, float] +) -> FourMomentum: + beta2 = beta[0] ** 2 + beta[1] ** 2 + beta[2] ** 2 + if beta2 == 0.0: + return momentum + if beta2 >= 1.0: + raise NativeEvaluationError("invalid canonical boost with beta >= 1") + gamma = 1.0 / math.sqrt(1.0 - beta2) + beta_dot_p = beta[0] * momentum[1] + beta[1] * momentum[2] + beta[2] * momentum[3] + spatial_factor = ((gamma - 1.0) * beta_dot_p / beta2) + gamma * momentum[0] + return ( + gamma * (momentum[0] + beta_dot_p), + momentum[1] + spatial_factor * beta[0], + momentum[2] + spatial_factor * beta[1], + momentum[3] + spatial_factor * beta[2], + ) + + +def _massive_spatial_scale( + spatial_norms: Sequence[float], + masses: Sequence[float], + *, + sqrt_s: float, +) -> float: + if sum(masses) >= sqrt_s: + raise NativeEvaluationError("massive RAMBO point is below threshold") + low = 0.0 + high = 1.0 + while _massive_energy_sum(spatial_norms, masses, high) < sqrt_s: + high *= 2.0 + for _ in range(128): + mid = 0.5 * (low + high) + if _massive_energy_sum(spatial_norms, masses, mid) > sqrt_s: + high = mid + else: + low = mid + return 0.5 * (low + high) + + +def _massive_energy_sum( + spatial_norms: Sequence[float], + masses: Sequence[float], + scale: float, +) -> float: + return sum( + math.sqrt(mass * mass + (scale * spatial_norm) ** 2) + for spatial_norm, mass in zip(spatial_norms, masses, strict=True) + ) + + +def _sum_momenta(momenta: Sequence[FourMomentum]) -> FourMomentum: + total = (0.0, 0.0, 0.0, 0.0) + for momentum in momenta: + total = ( + total[0] + momentum[0], + total[1] + momentum[1], + total[2] + momentum[2], + total[3] + momentum[3], + ) + return total + + +def _minkowski_square(momentum: FourMomentum) -> float: + return ( + momentum[0] * momentum[0] + - momentum[1] * momentum[1] + - momentum[2] * momentum[2] + - momentum[3] * momentum[3] + ) + + +__all__ = ["massive_rambo_final_state"] diff --git a/pyamplicol/src/pyamplicol/generation/physics_metadata.py b/pyamplicol/src/pyamplicol/generation/physics_metadata.py new file mode 100644 index 0000000000..6a0437716b --- /dev/null +++ b/pyamplicol/src/pyamplicol/generation/physics_metadata.py @@ -0,0 +1,988 @@ +# SPDX-License-Identifier: 0BSD +"""Strict runtime-physics-v1 metadata derived from one compiled DAG.""" + +from __future__ import annotations + +import math +from collections.abc import Mapping, Sequence +from itertools import product +from typing import Any + +from ..models.base import Model +from .contracts import runtime_coupling_parameter_names +from .dag_types import GenericDAG +from .helicity_replay import ( + HELICITY_RECURRENCE_CONTRACT_VERSION, + HELICITY_RECURRENCE_PROOF_ALGORITHM, + RUNTIME_SELECTOR_PROVENANCE, +) +from .runtime_amplitudes import build_runtime_amplitude_metadata + +_HELICITY_WEIGHT_TOLERANCE = 1.0e-12 +NATIVE_REDUCTION_GROUPS_EXTENSION_KEY = "native_reduction_groups" +EAGER_PLAN_V3_REDUCTION_GROUPS_KIND = "pyamplicol-eager-plan-v3-reduction-groups" +EAGER_PLAN_V3_REDUCTION_GROUPS_SCHEMA_VERSION = 1 +EAGER_PLAN_V3_REDUCTION_GROUPS_STORAGE_ABI = "pacbin-v1" +EAGER_PLAN_V3_REDUCTION_GROUPS_RUNTIME_LAYOUT_ABI = "pyamplicol-eager-runtime-layout-v1" +EAGER_PLAN_V3_REDUCTION_GROUPS_CONTAINER_PATH = "eager-runtime.pacbin" +EAGER_PLAN_V3_REDUCTION_GROUPS_MEMBER = "reductions/groups.bin" +EAGER_PLAN_V3_REDUCTION_ENTRIES_MEMBER = "reductions/entries.bin" +_NORMALIZATION_EXTENSION_KEYS = ( + "color_accuracy", + "color_factor", + "average_factor", + "identical_factor", + "global_coupling_factor", + "qcd_coupling_power", + "electroweak_coupling_power", + "couplings_in_stage_evaluators", + "coupling_policy", +) + + +def build_resolved_physics_from_dag( + dag: GenericDAG, + model: Model, + *, + process_id: str | None = None, + native_eager_plan_v3_reduction_groups: bool = False, + runtime_model_parameters: Sequence[Mapping[str, object]] | None = None, +) -> dict[str, object]: + """Build strict public physics without constructing runtime storage layouts.""" + + amplitude_metadata = build_runtime_amplitude_metadata(dag, model) + model_parameters = ( + build_runtime_model_parameter_records( + dag, + model, + amplitude_stage=amplitude_metadata, + ) + if runtime_model_parameters is None + else list(runtime_model_parameters) + ) + return build_resolved_physics_payload( + dag, + model, + process_id=process_id or dag.process.key, + amplitude_stage=amplitude_metadata, + model_parameters=model_parameters, + normalization=build_runtime_normalization(dag, model), + native_eager_plan_v3_reduction_groups=(native_eager_plan_v3_reduction_groups), + ) + + +def build_resolved_physics_payload( + dag: GenericDAG, + model: Model, + *, + process_id: str, + amplitude_stage: Mapping[str, object], + model_parameters: Sequence[Mapping[str, object]], + normalization: Mapping[str, object], + native_eager_plan_v3_reduction_groups: bool = False, +) -> dict[str, object]: + """Build strict public axes and exact representative-reuse metadata.""" + + coherent_groups = _mapping_sequence(amplitude_stage.get("coherent_groups", ())) + helicities, group_helicities, structural_zero_count = _helicity_metadata( + dag, + model, + coherent_groups, + _mapping_sequence(amplitude_stage.get("roots", ())), + ) + color_components, group_colors = _color_metadata(dag, coherent_groups) + if native_eager_plan_v3_reduction_groups: + reduction_groups: list[dict[str, object]] = [] + native_reduction_extension = { + NATIVE_REDUCTION_GROUPS_EXTENSION_KEY: { + "kind": EAGER_PLAN_V3_REDUCTION_GROUPS_KIND, + "schema_version": EAGER_PLAN_V3_REDUCTION_GROUPS_SCHEMA_VERSION, + "storage_abi": EAGER_PLAN_V3_REDUCTION_GROUPS_STORAGE_ABI, + "runtime_layout_abi": ( + EAGER_PLAN_V3_REDUCTION_GROUPS_RUNTIME_LAYOUT_ABI + ), + "container_path": EAGER_PLAN_V3_REDUCTION_GROUPS_CONTAINER_PATH, + "group_member": EAGER_PLAN_V3_REDUCTION_GROUPS_MEMBER, + "entry_member": EAGER_PLAN_V3_REDUCTION_ENTRIES_MEMBER, + "group_count": len(coherent_groups), + } + } + else: + helicity_ids = { + tuple(_integer(value) for value in _sequence(record["values"])): str( + record["id"] + ) + for record in helicities + } + reduction_groups = _reduction_groups( + coherent_groups, + group_helicities=group_helicities, + group_colors=group_colors, + helicity_ids=helicity_ids, + ) + native_reduction_extension = {} + lc_color_complete = ( + dag.color_coverage == "complete" + and not dag.selected_color_sector_ids + and not dag.color_plan.truncated + and len(color_components) == _expected_lc_physical_color_count(dag) + ) + is_lc = dag.process.color_accuracy == "lc" + return { + "schema_version": 1, + "kind": "pyamplicol-resolved-physics", + "process_id": process_id, + "process": dag.process.process, + "color_accuracy": dag.process.color_accuracy, + "coverage": { + "helicities": dag.helicity_coverage, + "color": ("complete" if is_lc and lc_color_complete else "selected") + if is_lc + else "contracted", + "color_kind": "physical-lc-flows" if is_lc else "contracted-color", + "structural_zero_helicity_count": structural_zero_count, + }, + "external_particles": _external_particles(dag), + "helicities": helicities, + "color_components": color_components, + "reduction": { + "kind": "lc-diagonal" if is_lc else "contracted-color", + "groups": reduction_groups, + }, + "model_parameters": _public_model_parameters(model_parameters), + "selectors": { + "helicity": True, + "color_flow": is_lc, + "contracted_color": False, + }, + "extensions": { + "process_key": dag.process.key, + "normalization": { + key: normalization[key] + for key in _NORMALIZATION_EXTENSION_KEYS + if key in normalization + }, + "selected_source_helicities": { + str(label): helicity + for label, helicity in dag.selected_source_helicities + }, + **( + {} + if dag.lc_topology_replay is None + else {"lc_topology_replay": dag.lc_topology_replay.to_json_dict()} + ), + **( + {} + if dag.color_topology_replay is None + else { + "color_topology_replay": (dag.color_topology_replay.to_json_dict()) + } + ), + **_runtime_selector_extension(dag), + **native_reduction_extension, + }, + } + + +def build_runtime_model_parameter_records( + dag: GenericDAG, + model: Model, + *, + amplitude_stage: Mapping[str, object], +) -> list[dict[str, object]]: + """Build the shared runtime parameter catalog used by execution and physics.""" + + records: list[dict[str, object]] = [] + seen: set[str] = set() + + def add(name: str, kind: str, default: float, **metadata: object) -> None: + if name in seen: + return + seen.add(name) + records.append( + { + "name": name, + "kind": kind, + "parameter_index": len(records), + "default": float(default), + **metadata, + } + ) + + def add_complex( + name: str, + value: object, + *, + kind: str, + **metadata: object, + ) -> None: + real, imaginary = _complex_pair(value, name) + for component, default in (("real", real), ("imag", imaginary)): + add( + f"{name}.{component}", + kind, + default, + runtime_name=name, + complex_component=component, + **metadata, + ) + + for raw_name, value in sorted( + model.runtime_normalization_parameter_defaults().items() + ): + add(str(raw_name), "normalization", float(value)) + defaults_provider = getattr(model, "runtime_parameter_defaults", None) + if callable(defaults_provider): + type_provider = getattr(model, "runtime_parameter_type", None) + for raw_name, value in sorted(defaults_provider().items()): + name = str(raw_name) + declared = ( + str(type_provider(name)).lower() + if callable(type_provider) + else "complex" + ) + if declared == "complex": + add_complex(name, value, kind="external_parameter_component") + else: + real, imaginary = _complex_pair(value, name) + if imaginary != 0.0: + raise ValueError( + "real runtime model parameter " + f"{name!r} has an imaginary default" + ) + add(name, "external_parameter", real, parameter_type=declared) + _add_derived_parameter_records( + dag, + model, + amplitude_stage=amplitude_stage, + add_complex=add_complex, + ) + return records + + for particle in sorted(model.particles.values(), key=lambda item: item.pdg): + if float(particle.mass) != 0.0: + add( + f"particle.{particle.pdg}.mass", + "particle_mass", + float(particle.mass), + pdg=particle.pdg, + ) + if float(particle.width) != 0.0: + add( + f"particle.{particle.pdg}.width", + "particle_width", + float(particle.width), + pdg=particle.pdg, + ) + for kind, particles, coupling in _coupling_signatures(dag, amplitude_stage): + names = runtime_coupling_parameter_names( + kind, + particles, + coupling, + model=model, + ) + for component, name in enumerate(names): + if name is None: + continue + add( + name, + "coupling_component", + float(coupling[component]), + vertex_kind=kind, + vertex_particles=list(particles), + component=component, + ) + return records + + +def build_runtime_normalization( + dag: GenericDAG, + model: Model, +) -> dict[str, object]: + """Build the shared runtime normalization payload for one DAG.""" + + return dict(model.runtime_normalization_payload(dag)) + + +def _add_derived_parameter_records( + dag: GenericDAG, + model: Model, + *, + amplitude_stage: Mapping[str, object], + add_complex: Any, +) -> None: + defaults_provider = getattr(model, "runtime_derived_parameter_defaults_for", None) + if not callable(defaults_provider): + return + used = _used_coupling_parameter_names(dag, model, amplitude_stage) + values = defaults_provider(tuple(sorted(used))) + domains_provider = getattr(model, "runtime_derived_parameter_domains_for", None) + domains = ( + domains_provider(tuple(sorted(used))) if callable(domains_provider) else {} + ) + for raw_name, value in sorted(values.items()): + name = str(raw_name) + domain = str(domains.get(raw_name, domains.get(name, "complex"))) + if domain not in {"real", "imaginary", "complex"}: + raise ValueError(f"unsupported runtime parameter domain {domain!r}") + add_complex( + name, + value, + kind="derived_parameter_component", + derived=True, + complex_domain=domain, + ) + + +def _used_coupling_parameter_names( + dag: GenericDAG, + model: Model, + amplitude_stage: Mapping[str, object], +) -> set[str]: + names = { + name + for interaction in dag.interactions + for name in runtime_coupling_parameter_names( + interaction.vertex_kind, + interaction.vertex_particles, + interaction.coupling, + model=model, + ) + if name is not None + } + for root in _mapping_sequence(amplitude_stage["roots"]): + raw_names = root.get("coupling_parameter_names") + if isinstance(raw_names, list): + names.update(str(name) for name in raw_names if isinstance(name, str)) + runtime_particle_names = getattr( + model, + "runtime_parameter_names_for_particle", + None, + ) + if callable(runtime_particle_names): + for particle_id in {current.index.particle_id for current in dag.currents}: + names.update(str(name) for name in runtime_particle_names(int(particle_id))) + return names + + +def _coupling_signatures( + dag: GenericDAG, + amplitude_stage: Mapping[str, object], +) -> tuple[tuple[int, tuple[int, ...], tuple[float, ...]], ...]: + signatures = { + ( + interaction.vertex_kind, + tuple(interaction.vertex_particles), + tuple(interaction.coupling), + ) + for interaction in dag.interactions + } + for root in _mapping_sequence(amplitude_stage["roots"]): + kind = root.get("vertex_kind") + particles = root.get("vertex_particles") + coupling = root.get("coupling") + if ( + isinstance(kind, int) + and isinstance(particles, list) + and isinstance(coupling, list) + ): + signatures.add( + ( + kind, + tuple(int(pdg) for pdg in particles), + tuple(float(value) for value in coupling), + ) + ) + return tuple(sorted(signatures)) + + +def _runtime_selector_extension(dag: GenericDAG) -> dict[str, object]: + """Emit independent runtime contracts for both selector axes.""" + + recurrence = dag.helicity_recurrence + helicity_complete = ( + dag.helicity_coverage == "complete" and not dag.selected_source_helicities + ) + recurrence_payload: dict[str, object] | None + if not helicity_complete: + recurrence_payload = None + elif recurrence is None: + recurrence_payload = { + "contract_version": HELICITY_RECURRENCE_CONTRACT_VERSION, + "proof_algorithm": HELICITY_RECURRENCE_PROOF_ALGORITHM, + "status": "unavailable", + "proof_counts": {}, + } + else: + recurrence_payload = { + "contract_version": HELICITY_RECURRENCE_CONTRACT_VERSION, + "proof_algorithm": HELICITY_RECURRENCE_PROOF_ALGORITHM, + "status": "available", + "proof_counts": recurrence.proof_counts(), + **( + {} + if dag.helicity_materialization is None + else { + "execution": ( + "materialized-recurrence-retained-proof-graph" + if dag.helicity_materialization.strategy + == "retained-proof-graph" + else "materialized-recurrence-quotient" + ), + "materialization_strategy": (dag.helicity_materialization.strategy), + "proof_current_count": ( + dag.helicity_materialization.proof_current_count + ), + "proof_amplitude_count": ( + dag.helicity_materialization.proof_root_count + ), + "materialized_current_count": ( + dag.helicity_materialization.materialized_current_count + ), + "materialized_amplitude_count": ( + dag.helicity_materialization.materialized_root_count + ), + } + ), + } + is_lc = dag.process.color_accuracy == "lc" + color_complete = ( + is_lc and dag.color_coverage == "complete" and not dag.selected_color_sector_ids + ) + specialized_axes = [ + *([] if helicity_complete else ["helicity"]), + *([] if not is_lc or color_complete else ["color_flow"]), + ] + selector_payload: dict[str, object] = { + "kind": "pyamplicol-runtime-selectors", + "contract_version": 1, + "provenance": RUNTIME_SELECTOR_PROVENANCE, + "axes": { + "helicity": { + "generation_coverage": dag.helicity_coverage, + "generation_selection": { + str(label): int(helicity) + for label, helicity in dag.selected_source_helicities + }, + "runtime_contract": ( + "complete-reusable" + if helicity_complete + else "generation-specialized" + ), + }, + "color_flow": { + "generation_coverage": (dag.color_coverage if is_lc else "contracted"), + "generation_selection": list(dag.selected_color_sector_ids), + "runtime_contract": ( + "complete-reusable" + if color_complete + else "generation-specialized" + if is_lc + else "contracted-color" + ), + }, + }, + "generation_specialized_axes": specialized_axes, + } + if recurrence_payload is not None: + selector_payload["helicity_recurrence"] = recurrence_payload + return {"runtime_selectors": selector_payload} + + +def _helicity_metadata( + dag: GenericDAG, + model: Model, + groups: Sequence[Mapping[str, object]], + roots: Sequence[Mapping[str, object]], +) -> tuple[list[dict[str, object]], dict[int, tuple[tuple[int, ...], ...]], int]: + if dag.helicity_materialization is not None: + return _materialized_helicity_metadata(dag, model, groups, roots) + + represented: dict[tuple[int, ...], tuple[tuple[int, ...], bool]] = {} + group_members: dict[int, tuple[tuple[int, ...], ...]] = {} + for group in groups: + group_id = _integer(group["group_id"]) + vector = tuple(_integer(value) for value in _sequence(group["helicities"])) + weight = _number(group.get("helicity_weight", 1.0)) + if not math.isfinite(weight) or weight <= 0.0: + raise ValueError(f"invalid helicity weight {weight!r} in group {group_id}") + members = [vector] + if weight > 1.0 + _HELICITY_WEIGHT_TOLERANCE: + if not math.isclose( + weight, + 2.0, + rel_tol=_HELICITY_WEIGHT_TOLERANCE, + abs_tol=_HELICITY_WEIGHT_TOLERANCE, + ): + raise ValueError( + f"unsupported helicity reuse weight {weight!r} in group {group_id}" + ) + flipped = tuple(-value for value in vector) + if flipped != vector: + members.append(flipped) + group_members[group_id] = tuple(members) + for index, member in enumerate(members): + candidate = (vector, index == 0) + previous = represented.setdefault(member, candidate) + if previous != candidate: + raise ValueError( + f"inconsistent helicity representative for physical state {member}" + ) + + possible = _possible_helicity_vectors(dag, model) + structural_zeros = sorted(set(possible) - represented.keys()) + for vector in structural_zeros: + represented[vector] = (vector, False) + + identifiers = {vector: _helicity_id(vector) for vector in represented} + records: list[dict[str, object]] = [] + for index, vector in enumerate(sorted(represented)): + representative, computed = represented[vector] + structural_zero = vector in structural_zeros + records.append( + { + "id": identifiers[vector], + "index": index, + "values": list(vector), + "computed": computed and not structural_zero, + "structural_zero": structural_zero, + "representative_id": identifiers[representative], + "coefficient": 0.0 if structural_zero else 1.0, + } + ) + return records, group_members, len(structural_zeros) + + +def _materialized_helicity_metadata( + dag: GenericDAG, + model: Model, + groups: Sequence[Mapping[str, object]], + roots: Sequence[Mapping[str, object]], +) -> tuple[list[dict[str, object]], dict[int, tuple[tuple[int, ...], ...]], int]: + materialization = dag.helicity_materialization + recurrence = dag.helicity_recurrence + if materialization is None or recurrence is None: + raise ValueError("materialized helicity metadata requires its proof") + + group_by_root_id = { + _integer(root["dag_root_id"]): _integer(root["coherent_group_id"]) + for root in roots + } + domain_by_id = {domain.id: domain for domain in recurrence.selector_domains} + route_vectors_by_group: dict[int, set[tuple[int, ...]]] = { + _integer(group["group_id"]): set() for group in groups + } + route_coefficients: dict[tuple[int, ...], float] = {} + for route in materialization.amplitude_routes: + group_id = group_by_root_id.get(route.materialized_root_id) + if group_id is None: + raise ValueError( + "helicity route refers to an absent materialized amplitude root" + ) + coefficient = route.factor[0] ** 2 + route.factor[1] ** 2 + for domain_id in route.selector_domain_ids: + domain = domain_by_id.get(domain_id) + if domain is None or not domain.complete: + raise ValueError( + "amplitude route refers to a non-complete selector domain" + ) + by_label = dict(domain.source_states) + vector = tuple(by_label.get(int(leg.label), 0) for leg in dag.process.legs) + route_vectors_by_group[group_id].add(vector) + previous = route_coefficients.setdefault(vector, coefficient) + if not math.isclose( + previous, + coefficient, + rel_tol=_HELICITY_WEIGHT_TOLERANCE, + abs_tol=_HELICITY_WEIGHT_TOLERANCE, + ): + raise ValueError( + f"inconsistent recurrence factor for physical helicity {vector}" + ) + + represented: dict[tuple[int, ...], tuple[tuple[int, ...], bool, float]] = {} + group_members: dict[int, tuple[tuple[int, ...], ...]] = {} + for group in groups: + group_id = _integer(group["group_id"]) + representative = tuple( + _integer(value) for value in _sequence(group["helicities"]) + ) + members = tuple(sorted(route_vectors_by_group[group_id])) + if not members: + raise ValueError( + f"materialized helicity group {group_id} has no physical routes" + ) + group_members[group_id] = members + for member in members: + candidate = ( + representative, + member == representative, + route_coefficients[member], + ) + previous = represented.setdefault(member, candidate) + if previous != candidate: + raise ValueError( + f"inconsistent helicity representative for physical state {member}" + ) + + possible = _possible_helicity_vectors(dag, model) + structural_zeros = sorted(set(possible) - represented.keys()) + expected_zero_domains = { + tuple( + dict(domain_by_id[domain_id].source_states).get(int(leg.label), 0) + for leg in dag.process.legs + ) + for domain_id in recurrence.structural_zero_selector_domain_ids + } + if set(structural_zeros) != expected_zero_domains: + raise ValueError( + "materialized helicity routes do not match certified structural zeros" + ) + for vector in structural_zeros: + represented[vector] = (vector, False, 0.0) + + identifiers = {vector: _helicity_id(vector) for vector in represented} + records: list[dict[str, object]] = [] + for index, vector in enumerate(sorted(represented)): + representative, computed, coefficient = represented[vector] + structural_zero = vector in structural_zeros + records.append( + { + "id": identifiers[vector], + "index": index, + "values": list(vector), + "computed": computed and not structural_zero, + "structural_zero": structural_zero, + "representative_id": identifiers[representative], + "coefficient": 0.0 if structural_zero else coefficient, + } + ) + return records, group_members, len(structural_zeros) + + +def _possible_helicity_vectors( + dag: GenericDAG, + model: Model, +) -> tuple[tuple[int, ...], ...]: + selected = dict(dag.selected_source_helicities) + per_leg: list[tuple[int, ...]] = [] + for leg in dag.process.legs: + if leg.outgoing_pdg is None: + per_leg.append((0,)) + continue + particle_id = int(leg.outgoing_pdg) + source_ir = model._source_ir(particle_id) + values: set[int] = set() + for declared_state in source_ir.states: + state = ( + source_ir.crossing.apply(declared_state) + if leg.is_initial + else declared_state + ) + values.add(int(state.helicity)) + if leg.label in selected: + requested = selected[leg.label] + values.intersection_update((requested,)) + if not values: + raise ValueError( + f"selected helicity {requested} is unavailable for external " + f"leg {leg.label}" + ) + per_leg.append(tuple(sorted(values))) + return tuple(tuple(values) for values in product(*per_leg)) + + +def _color_metadata( + dag: GenericDAG, + groups: Sequence[Mapping[str, object]], +) -> tuple[list[dict[str, object]], dict[int, tuple[str, ...]]]: + if dag.process.color_accuracy != "lc": + identifier = "color:contracted" + return [ + { + "kind": "contracted-color", + "id": identifier, + "index": 0, + "description": "coherent sparse contraction of the full color basis", + } + ], {_integer(group["group_id"]): (identifier,) for group in groups} + + records: dict[str, dict[str, object]] = {} + members_by_group: dict[int, tuple[str, ...]] = {} + for group in groups: + group_id = _integer(group["group_id"]) + word = tuple(_integer(value) for value in _sequence(group["color_word"])) + identifier = _color_id(word) + members = [identifier] + records.setdefault( + identifier, + { + "kind": "lc-flow", + "id": identifier, + "word": list(word), + "computed": True, + "representative_id": identifier, + "coefficient": 1.0, + }, + ) + helicity_weight = _number(group.get("helicity_weight", 1.0)) + all_sector_weight = _number(group.get("all_sector_weight", helicity_weight)) + if word and all_sector_weight > helicity_weight + _HELICITY_WEIGHT_TOLERANCE: + reflected = (word[0], *reversed(word[1:])) + reflected_id = _color_id(reflected) + if reflected_id != identifier: + members.append(reflected_id) + records.setdefault( + reflected_id, + { + "kind": "lc-flow", + "id": reflected_id, + "word": list(reflected), + "computed": False, + "representative_id": identifier, + "coefficient": 1.0, + }, + ) + members_by_group[group_id] = tuple(members) + if dag.lc_topology_replay is not None: + _add_replayed_lc_color_components(dag, records) + components = [records[key] for key in sorted(records)] + for index, component in enumerate(components): + component["index"] = index + return components, members_by_group + + +def _add_replayed_lc_color_components( + dag: GenericDAG, + records: dict[str, dict[str, object]], +) -> None: + replay = dag.lc_topology_replay + if replay is None: + return + materialized = set(replay.materialized_sector_ids) + for sector in dag.color_plan.sectors: + word = tuple(sector.word_labels or sector.color_words[0]) + identifier = _color_id(word) + representative = dag.color_plan.sector(replay.representative_for(sector.id)) + if representative is None: + raise ValueError( + f"LC replay representative for sector {sector.id} is missing" + ) + representative_word = tuple( + representative.word_labels or representative.color_words[0] + ) + representative_id = _color_id(representative_word) + records.setdefault( + identifier, + { + "kind": "lc-flow", + "id": identifier, + "word": list(word), + "computed": int(sector.id) in materialized, + "representative_id": representative_id, + "coefficient": 1.0, + }, + ) + if not ( + dag.color_plan.trace_reflections_folded + and sector.kind == "single-trace" + and len(sector.trace_labels) > 2 + ): + continue + reflected = (word[0], *reversed(word[1:])) + reflected_id = _color_id(reflected) + if reflected_id == identifier: + continue + records.setdefault( + reflected_id, + { + "kind": "lc-flow", + "id": reflected_id, + "word": list(reflected), + "computed": False, + "representative_id": representative_id, + "coefficient": 1.0, + }, + ) + + +def _reduction_groups( + groups: Sequence[Mapping[str, object]], + *, + group_helicities: Mapping[int, tuple[tuple[int, ...], ...]], + group_colors: Mapping[int, tuple[str, ...]], + helicity_ids: Mapping[tuple[int, ...], str], +) -> list[dict[str, object]]: + records: list[dict[str, object]] = [] + for group in groups: + group_id = _integer(group["group_id"]) + helicities = group_helicities[group_id] + colors = group_colors[group_id] + representative = tuple( + _integer(value) for value in _sequence(group["helicities"]) + ) + if representative not in helicities: + raise ValueError( + f"helicity representative is not a member of group {group_id}" + ) + records.append( + { + "id": f"reduction:{group_id}", + "representative_helicity_id": helicity_ids[representative], + "representative_color_id": colors[0], + "physical_helicity_ids": [ + helicity_ids[helicity] for helicity in helicities + ], + "physical_color_ids": list(colors), + } + ) + return records + + +def _public_model_parameters( + records: Sequence[Mapping[str, object]], +) -> list[dict[str, object]]: + parameters: dict[str, dict[str, object]] = {} + for record in records: + raw_kind = str(record.get("kind", "")) + if raw_kind == "runtime_control": + continue + name = str(record.get("runtime_name", record.get("name", ""))) + if not name: + raise ValueError("runtime model parameter has no name") + kind = _public_parameter_kind(raw_kind) + parameter = parameters.setdefault( + name, + { + "name": name, + "kind": kind, + "default_real": 0.0, + "default_imaginary": 0.0, + "mutable": kind != "derived", + }, + ) + if parameter["kind"] != kind: + raise ValueError(f"runtime parameter {name!r} has inconsistent kinds") + component = str(record.get("complex_component", "real")) + field = "default_imaginary" if component == "imag" else "default_real" + parameter[field] = _number(record.get("default", 0.0)) + return list(parameters.values()) + + +def _public_parameter_kind(kind: str) -> str: + if kind == "normalization": + return "normalization" + if kind == "particle_mass": + return "mass" + if kind == "particle_width": + return "width" + if kind == "coupling_component": + return "coupling" + if kind in {"external_parameter", "external_parameter_component"}: + return "external" + if kind == "derived_parameter_component": + return "derived" + raise ValueError(f"unsupported public model-parameter kind {kind!r}") + + +def _external_particles(dag: GenericDAG) -> list[dict[str, object]]: + return [ + { + "index": index, + "label": leg.label, + "particle": leg.particle, + "pdg": leg.pdg, + "role": "initial" if leg.is_initial else "final", + "momentum_slot": index, + "momentum_components": ["E", "px", "py", "pz"], + } + for index, leg in enumerate(dag.process.legs) + ] + + +def _expected_lc_physical_color_count(dag: GenericDAG) -> int: + if dag.process.color_accuracy != "lc": + return 0 + count = 0 + for sector in dag.color_plan.sectors: + count += 1 + if ( + dag.color_plan.trace_reflections_folded + and sector.kind == "single-trace" + and len(sector.trace_labels) > 2 + ): + reflected = (sector.trace_labels[0], *reversed(sector.trace_labels[1:])) + if reflected != sector.trace_labels: + count += 1 + return count + + +def _helicity_id(values: Sequence[int]) -> str: + return "h:" + ",".join(f"{int(value):+d}" for value in values) + + +def _color_id(word: Sequence[int]) -> str: + if word: + return "flow:" + ",".join(str(int(label)) for label in word) + return "flow:singlet" + + +def _complex_pair(value: object, name: str) -> tuple[float, float]: + if isinstance(value, complex): + return float(value.real), float(value.imag) + if isinstance(value, Sequence) and not isinstance(value, str | bytes): + if len(value) != 2: + raise ValueError( + f"runtime model parameter {name!r} must have two components" + ) + return float(value[0]), float(value[1]) + if not isinstance(value, str | int | float): + raise ValueError(f"runtime model parameter {name!r} is not numeric") + return float(value), 0.0 + + +def _mapping(value: object) -> Mapping[str, object]: + if not isinstance(value, Mapping): + raise TypeError("runtime physics record must be a mapping") + return {str(key): item for key, item in value.items()} + + +def _mapping_sequence(value: object) -> tuple[Mapping[str, object], ...]: + return tuple(_mapping(item) for item in _sequence(value)) + + +def _sequence(value: object) -> Sequence[object]: + if isinstance(value, (str, bytes)) or not isinstance(value, Sequence): + raise TypeError("runtime physics field must be a sequence") + return value + + +def _integer(value: object) -> int: + if isinstance(value, bool) or not isinstance(value, int): + raise TypeError("runtime physics integer field is invalid") + return value + + +def _number(value: object) -> float: + if isinstance(value, bool) or not isinstance(value, int | float): + raise TypeError("runtime physics numeric field is invalid") + result = float(value) + if not math.isfinite(result): + raise ValueError("runtime physics numeric field must be finite") + return result + + +__all__ = [ + "EAGER_PLAN_V3_REDUCTION_ENTRIES_MEMBER", + "EAGER_PLAN_V3_REDUCTION_GROUPS_CONTAINER_PATH", + "EAGER_PLAN_V3_REDUCTION_GROUPS_KIND", + "EAGER_PLAN_V3_REDUCTION_GROUPS_MEMBER", + "EAGER_PLAN_V3_REDUCTION_GROUPS_RUNTIME_LAYOUT_ABI", + "EAGER_PLAN_V3_REDUCTION_GROUPS_SCHEMA_VERSION", + "EAGER_PLAN_V3_REDUCTION_GROUPS_STORAGE_ABI", + "NATIVE_REDUCTION_GROUPS_EXTENSION_KEY", + "build_resolved_physics_from_dag", + "build_resolved_physics_payload", + "build_runtime_model_parameter_records", + "build_runtime_normalization", +] diff --git a/pyamplicol/src/pyamplicol/generation/process_grouping.py b/pyamplicol/src/pyamplicol/generation/process_grouping.py new file mode 100644 index 0000000000..3069806a50 --- /dev/null +++ b/pyamplicol/src/pyamplicol/generation/process_grouping.py @@ -0,0 +1,800 @@ +# SPDX-License-Identifier: 0BSD +"""Flavour-profile canonicalization and process-group maps. + +Two processes are candidates for one matrix element when their external legs +have the same particle types. A particle type is the spin, the SU(3) dimension, +the resolved mass and width and self-conjugacy; particles and antiparticles +share a type. The leg mapping between two candidates is derived from flavour +profiles: each flavour of a process is described by how many times it appears +as a particle and as an antiparticle in the initial and in the final state. +Sorting legs by profile, and only then by PDG, gives positions that follow any +flavour relabelling, which sorting by PDG alone does not. + +A group map records, for each representative, the members whose matrix +element equals ``factor`` times the representative's at permuted momenta: +``ME_member(p) = factor * ME_representative(q)`` with +``q[r] = p[permutation[r]]``, so ``permutation`` maps representative legs to +member legs. ``conjugated`` members are related by charge conjugation, so LC +colour-flow words map with reversed order. +""" + +from __future__ import annotations + +import itertools +import json +from collections.abc import Iterable, Iterator, Mapping, Sequence +from dataclasses import dataclass, field, replace +from enum import StrEnum +from fractions import Fraction +from typing import Any + +PROCESS_GROUPS_PATH = "process-groups.json" +PROCESS_GROUPS_ROLE = "process-groups" +PROCESS_GROUPS_KIND = "pyamplicol-process-groups" +PROCESS_GROUPS_SCHEMA_VERSION = 1 +MAX_RATIONAL = 99 +"""Largest numerator and denominator of a recorded group factor.""" + +_ZERO_EXPRESSIONS = frozenset({"0", "0.0", "-0", "-0.0", "zero"}) + + +class GroupingBasis(StrEnum): + IDENTITY = "identity" + FLAVOR_BLIND = "flavor-blind" + NUMERICAL = "numerical" + + +# The least verified link decides the basis of a composed relation. +_BASIS_STRENGTH = { + GroupingBasis.IDENTITY: 0, + GroupingBasis.NUMERICAL: 1, + GroupingBasis.FLAVOR_BLIND: 2, +} + + +class GroupingAnalysis(StrEnum): + NONE = "none" + NUMERICAL = "numerical" + + +@dataclass(frozen=True, order=True, slots=True) +class ParticleType: + """Everything a flavour relabelling must preserve about one particle.""" + + spin: int + color: int + mass: str + width: str + self_conjugate: bool + + +@dataclass(frozen=True, slots=True) +class GroupingParticle: + pdg: int + name: str + type: ParticleType + + +ParticleTable = Mapping[int, GroupingParticle] + + +@dataclass(frozen=True, slots=True) +class GroupingProcess: + process_id: str + expression: str + pdgs: tuple[int, ...] + initial_count: int + generated: bool = True + + def __post_init__(self) -> None: + if not 0 < self.initial_count < len(self.pdgs): + raise ValueError( + f"process {self.process_id!r} needs initial and final legs" + ) + + +def resolved_parameter_name( + name: str, + parameters: Mapping[str, object], +) -> str: + """Return ``ZERO`` for compile-time zero parameters, otherwise ``name``.""" + + if not name or name.strip().casefold() == "zero": + return "ZERO" + record = parameters.get(name) + resolved = getattr(record, "resolved_expression", None) + if isinstance(resolved, str) and resolved.strip().casefold() in _ZERO_EXPRESSIONS: + return "ZERO" + return name + + +def particle_table( + particles: Iterable[object], + parameters: Mapping[str, object], +) -> dict[int, GroupingParticle]: + """Build the grouping view of compiled particle records. + + ``particles`` are ``CompiledParticleRecord``-like objects and + ``parameters`` maps parameter names to records with a + ``resolved_expression``, as in ``CompiledModelIR``. + """ + + table: dict[int, GroupingParticle] = {} + for particle in particles: + pdg = int(particle.pdg_code) # type: ignore[attr-defined] + name = str(particle.name) # type: ignore[attr-defined] + antiname = str(particle.antiname) # type: ignore[attr-defined] + declared = getattr(particle, "self_conjugate", None) + self_conjugate = bool(declared) if declared is not None else name == antiname + table[pdg] = GroupingParticle( + pdg=pdg, + name=name, + type=ParticleType( + spin=int(particle.spin), # type: ignore[attr-defined] + color=abs(int(particle.color)), # type: ignore[attr-defined] + mass=resolved_parameter_name( + str(particle.mass), # type: ignore[attr-defined] + parameters, + ), + width=resolved_parameter_name( + str(particle.width), # type: ignore[attr-defined] + parameters, + ), + self_conjugate=self_conjugate, + ), + ) + return table + + +@dataclass(frozen=True, slots=True) +class _Leg: + index: int + initial: bool + pdg: int + flavour: int + orientation: int + type: ParticleType + + +def _particle(table: ParticleTable, pdg: int) -> GroupingParticle: + try: + return table[pdg] + except KeyError as error: + raise ValueError(f"PDG code {pdg} is absent from the model") from error + + +def _legs( + process: GroupingProcess, + table: ParticleTable, + *, + conjugate: bool = False, +) -> tuple[_Leg, ...]: + legs = [] + for index, pdg in enumerate(process.pdgs): + particle = _particle(table, pdg) + if particle.type.self_conjugate: + flavour, orientation = abs(pdg), 0 + else: + flavour, orientation = abs(pdg), (1 if pdg > 0 else -1) + if conjugate: + orientation = -orientation + legs.append( + _Leg( + index=index, + initial=index < process.initial_count, + pdg=pdg if not conjugate or orientation == 0 else -pdg, + flavour=flavour, + orientation=orientation, + type=particle.type, + ) + ) + return tuple(legs) + + +Profile = tuple[int, int, int, int] + + +def _profiles(legs: Sequence[_Leg]) -> dict[int, Profile]: + counts: dict[int, list[int]] = {} + for leg in legs: + slot = (0 if leg.initial else 2) + (1 if leg.orientation < 0 else 0) + counts.setdefault(leg.flavour, [0, 0, 0, 0])[slot] += 1 + return {flavour: tuple(values) for flavour, values in counts.items()} # type: ignore[misc] + + +def coarse_key(process: GroupingProcess, table: ParticleTable) -> tuple[Any, ...]: + """Particle types of each side, with particles and antiparticles merged.""" + + legs = _legs(process, table) + return ( + process.initial_count, + tuple(sorted(leg.type for leg in legs if leg.initial)), + tuple(sorted(leg.type for leg in legs if not leg.initial)), + ) + + +def profile_key( + process: GroupingProcess, + table: ParticleTable, + *, + conjugate: bool = False, +) -> tuple[Any, ...]: + """Equal for exactly the processes related by a type-preserving relabelling.""" + + legs = _legs(process, table, conjugate=conjugate) + types = {leg.flavour: leg.type for leg in legs} + profiles = _profiles(legs) + return ( + process.initial_count, + len(legs), + tuple( + sorted((types[flavour], profile) for flavour, profile in profiles.items()) + ), + ) + + +def flavor_class_key( + process: GroupingProcess, + table: ParticleTable, + *, + consider_charge_conjugation: bool, +) -> tuple[Any, ...]: + """Class of processes assumed equal by ``--flavor-blind``.""" + + key = profile_key(process, table) + if consider_charge_conjugation: + key = min(key, profile_key(process, table, conjugate=True)) + return key + + +def _ordered_legs( + legs: Sequence[_Leg], + ranks: Mapping[int, int], + profiles: Mapping[int, Profile], +) -> tuple[_Leg, ...]: + return tuple( + sorted( + legs, + key=lambda leg: ( + not leg.initial, + leg.type, + profiles[leg.flavour], + ranks[leg.flavour], + -leg.orientation, + leg.index, + ), + ) + ) + + +def _flavour_groups(legs: Sequence[_Leg]) -> dict[tuple[Any, ...], list[int]]: + """Flavours with equal (type, profile), in a fixed order.""" + + types = {leg.flavour: leg.type for leg in legs} + profiles = _profiles(legs) + groups: dict[tuple[Any, ...], list[int]] = {} + for flavour in sorted(profiles): + groups.setdefault((types[flavour], profiles[flavour]), []).append(flavour) + return groups + + +def _profile_order( + legs: Sequence[_Leg], + assignment: Mapping[tuple[Any, ...], Sequence[int]], +) -> tuple[_Leg, ...]: + ranks = { + flavour: rank + for flavours in assignment.values() + for rank, flavour in enumerate(flavours) + } + return _ordered_legs(legs, ranks, _profiles(legs)) + + +@dataclass(frozen=True, slots=True) +class LegMapping: + """One candidate relation ``member -> representative``.""" + + permutation: tuple[int, ...] + conjugated: bool + method: str + + +def _mapping( + member: Sequence[_Leg], + representative: Sequence[_Leg], + *, + conjugated: bool, + method: str, +) -> LegMapping: + permutation = [0] * len(representative) + for member_leg, representative_leg in zip(member, representative, strict=True): + if ( + member_leg.initial != representative_leg.initial + or member_leg.type != representative_leg.type + or member_leg.orientation != representative_leg.orientation + ): + raise ValueError("leg orders do not describe the same process shape") + permutation[representative_leg.index] = member_leg.index + return LegMapping(tuple(permutation), conjugated, method) + + +def profile_mappings( + member: GroupingProcess, + representative: GroupingProcess, + table: ParticleTable, + *, + conjugated: bool = False, + limit: int = 24, +) -> tuple[LegMapping, ...]: + """Leg mappings induced by flavour relabellings, at most ``limit``. + + Flavours with the same type and profile can be paired in any order; each + pairing is one mapping, the first one pairs them by increasing ``|PDG|``. + """ + + member_legs = _legs(member, table, conjugate=conjugated) + representative_legs = _legs(representative, table) + if profile_key(member, table, conjugate=conjugated) != profile_key( + representative, table + ): + return () + representative_order = _profile_order( + representative_legs, _flavour_groups(representative_legs) + ) + member_groups = _flavour_groups(member_legs) + keys = list(member_groups) + choices = [itertools.permutations(member_groups[key]) for key in keys] + mappings: list[LegMapping] = [] + for combination in itertools.product(*choices): + member_order = _profile_order( + member_legs, dict(zip(keys, combination, strict=True)) + ) + mapping = _mapping( + member_order, + representative_order, + conjugated=conjugated, + method="flavor-profile", + ) + if mapping not in mappings: + mappings.append(mapping) + if len(mappings) >= limit: + break + return tuple(mappings) + + +def signed_pdg_mapping( + member: GroupingProcess, + representative: GroupingProcess, + table: ParticleTable, +) -> LegMapping | None: + """Pair legs by sorting each side by type and signed PDG.""" + + if coarse_key(member, table) != coarse_key(representative, table): + return None + + def order(process: GroupingProcess) -> tuple[_Leg, ...]: + return tuple( + sorted( + _legs(process, table), + key=lambda leg: (not leg.initial, leg.type, leg.pdg, leg.index), + ) + ) + + permutation = [0] * len(representative.pdgs) + for member_leg, representative_leg in zip( + order(member), order(representative), strict=True + ): + permutation[representative_leg.index] = member_leg.index + return LegMapping(tuple(permutation), False, "signed-pdg") + + +def candidate_mappings( + member: GroupingProcess, + representative: GroupingProcess, + table: ParticleTable, + *, + consider_charge_conjugation: bool, + limit: int = 24, +) -> tuple[LegMapping, ...]: + """All leg mappings worth testing numerically, most natural first.""" + + mappings = list(profile_mappings(member, representative, table, limit=limit)) + if consider_charge_conjugation: + mappings.extend( + profile_mappings( + member, representative, table, conjugated=True, limit=limit + ) + ) + fallback = signed_pdg_mapping(member, representative, table) + if fallback is not None and all( + (fallback.permutation, False) != (mapping.permutation, mapping.conjugated) + for mapping in mappings + ): + mappings.append(fallback) + return tuple(mappings) + + +def flavor_blind_mapping( + member: GroupingProcess, + representative: GroupingProcess, + table: ParticleTable, + *, + consider_charge_conjugation: bool, +) -> LegMapping: + """The mapping assumed by ``--flavor-blind`` within one flavour class.""" + + mappings = profile_mappings(member, representative, table, limit=1) + if not mappings and consider_charge_conjugation: + mappings = profile_mappings( + member, representative, table, conjugated=True, limit=1 + ) + if not mappings: + raise ValueError( + f"{member.expression!r} and {representative.expression!r} are not " + "related by a flavour relabelling" + ) + return replace(mappings[0], method="flavor-blind") + + +def representative_key( + process: GroupingProcess, + table: ParticleTable, +) -> tuple[Any, ...]: + """Canonical order of processes: type signature, then PDGs. + + Smaller ``|PDG|`` comes first and particles precede antiparticles, so + ``d d~ > g g`` represents ``u u~ > g g`` and ``u g > u g`` represents + ``u~ g > u~ g``. + """ + + legs = sorted( + _legs(process, table), + key=lambda leg: ( + not leg.initial, + leg.type, + abs(leg.pdg), + leg.pdg < 0, + leg.index, + ), + ) + return ( + coarse_key(process, table), + tuple((leg.initial, abs(leg.pdg), leg.pdg < 0) for leg in legs), + process.process_id, + ) + + +def flavor_blind_classes( + processes: Sequence[GroupingProcess], + table: ParticleTable, + *, + consider_charge_conjugation: bool, +) -> list[list[GroupingProcess]]: + """Partition ``processes`` into flavour classes, representative first.""" + + classes: dict[tuple[Any, ...], list[GroupingProcess]] = {} + for process in processes: + key = flavor_class_key( + process, + table, + consider_charge_conjugation=consider_charge_conjugation, + ) + classes.setdefault(key, []).append(process) + result = [] + for members in classes.values(): + members.sort(key=lambda process: representative_key(process, table)) + result.append(members) + return result + + +# -------------------------------------------------------------------------- +# Relations and group maps + + +def identity_permutation(count: int) -> tuple[int, ...]: + return tuple(range(count)) + + +def compose_permutations( + member_to_middle: Sequence[int], + middle_to_representative: Sequence[int], +) -> tuple[int, ...]: + """Compose ``member -> middle`` with ``middle -> representative``.""" + + return tuple(member_to_middle[index] for index in middle_to_representative) + + +def invert_permutation(permutation: Sequence[int]) -> tuple[int, ...]: + inverse = [0] * len(permutation) + for representative_index, member_index in enumerate(permutation): + inverse[member_index] = representative_index + return tuple(inverse) + + +@dataclass(frozen=True, slots=True) +class GroupMember: + process_id: str + expression: str + pdgs: tuple[int, ...] + initial_count: int + factor: Fraction = Fraction(1) + permutation: tuple[int, ...] = () + conjugated: bool = False + basis: GroupingBasis = GroupingBasis.IDENTITY + generated: bool = True + warning: str | None = None + + @classmethod + def identity(cls, process: GroupingProcess) -> GroupMember: + return cls( + process_id=process.process_id, + expression=process.expression, + pdgs=process.pdgs, + initial_count=process.initial_count, + permutation=identity_permutation(len(process.pdgs)), + generated=process.generated, + ) + + @property + def process(self) -> GroupingProcess: + return GroupingProcess( + process_id=self.process_id, + expression=self.expression, + pdgs=self.pdgs, + initial_count=self.initial_count, + generated=self.generated, + ) + + def related( + self, + mapping: LegMapping, + *, + factor: Fraction, + basis: GroupingBasis, + warning: str | None = None, + ) -> GroupMember: + return replace( + self, + factor=Fraction(factor), + permutation=tuple(mapping.permutation), + conjugated=mapping.conjugated, + basis=basis, + warning=warning, + ) + + def through(self, middle: GroupMember) -> GroupMember: + """Re-express ``self -> A`` through ``A -> B`` as ``self -> B``. + + ``middle`` is the relation of the old representative ``A`` to the new + representative ``B``. + """ + + return replace( + self, + factor=self.factor * middle.factor, + permutation=compose_permutations(self.permutation, middle.permutation), + conjugated=self.conjugated != middle.conjugated, + basis=max(self.basis, middle.basis, key=_BASIS_STRENGTH.__getitem__), + warning=self.warning or middle.warning, + ) + + def inverted_to(self, representative: GroupMember) -> GroupMember: + """Relation of ``representative`` to ``self``, the inverse of ``self``.""" + + return replace( + representative, + factor=1 / self.factor, + permutation=invert_permutation(self.permutation), + conjugated=self.conjugated, + basis=self.basis, + warning=self.warning, + ) + + def as_dict(self) -> dict[str, object]: + record: dict[str, object] = { + "process_id": self.process_id, + "process": self.expression, + "pdgs": list(self.pdgs), + "initial_count": self.initial_count, + "factor": { + "numerator": self.factor.numerator, + "denominator": self.factor.denominator, + }, + "permutation": list(self.permutation), + "conjugated": self.conjugated, + "basis": str(self.basis), + "generated": self.generated, + } + if self.warning: + record["warning"] = self.warning + return record + + @classmethod + def from_dict(cls, value: Mapping[str, object]) -> GroupMember: + factor = value["factor"] + if not isinstance(factor, Mapping): + raise ValueError("process-group factor must be an object") + return cls( + process_id=str(value["process_id"]), + expression=str(value["process"]), + pdgs=tuple(int(pdg) for pdg in value["pdgs"]), # type: ignore[attr-defined] + initial_count=int(value["initial_count"]), # type: ignore[call-overload] + factor=Fraction( + int(factor["numerator"]), + int(factor["denominator"]), + ), + permutation=tuple(int(i) for i in value["permutation"]), # type: ignore[attr-defined] + conjugated=bool(value["conjugated"]), + basis=GroupingBasis(str(value["basis"])), + generated=bool(value["generated"]), + warning=(str(value["warning"]) if value.get("warning") else None), + ) + + +@dataclass(slots=True) +class ProcessGroup: + """A representative and every member, the representative first.""" + + members: list[GroupMember] + + @property + def representative(self) -> GroupMember: + return self.members[0] + + def as_dict(self) -> dict[str, object]: + representative = self.representative + return { + "representative": { + "process_id": representative.process_id, + "process": representative.expression, + }, + "members": [member.as_dict() for member in self.members], + } + + +@dataclass(slots=True) +class ProcessGroupMap: + """Process groups keyed by representative process ID.""" + + groups: dict[str, ProcessGroup] = field(default_factory=dict) + analyses: list[dict[str, object]] = field(default_factory=list) + + @classmethod + def trivial(cls, processes: Iterable[GroupingProcess]) -> ProcessGroupMap: + result = cls() + for process in processes: + result.add_group([GroupMember.identity(process)]) + return result + + def add_group(self, members: Sequence[GroupMember]) -> None: + representative = members[0] + if not representative.generated: + raise ValueError( + f"representative {representative.process_id!r} was not generated" + ) + for member in members: + if self.group_of(member.process_id) is not None: + raise ValueError( + f"process {member.process_id!r} already belongs to a group" + ) + self.groups[representative.process_id] = ProcessGroup(list(members)) + + def group_of(self, process_id: str) -> ProcessGroup | None: + for group in self.groups.values(): + if any(member.process_id == process_id for member in group.members): + return group + return None + + @property + def representatives(self) -> tuple[GroupMember, ...]: + return tuple(group.representative for group in self.groups.values()) + + def members(self) -> Iterator[GroupMember]: + for group in self.groups.values(): + yield from group.members + + def merge( + self, + member_representative: str, + relation: GroupMember, + table: ParticleTable, + ) -> str: + """Join two groups given ``relation``, the relation of one + representative (``relation.process_id``) to another + (``member_representative`` is the target). Returns the representative + of the merged group, the canonically smallest generated one.""" + + if relation.process_id == member_representative: + raise ValueError("cannot merge a group with itself") + source = self.groups.pop(relation.process_id) + target = self.groups.pop(member_representative) + source_key = representative_key(source.representative.process, table) + target_key = representative_key(target.representative.process, table) + if target_key <= source_key: + keep, moved, link = target, source, relation + else: + keep, moved = source, target + link = relation.inverted_to(target.representative) + merged = list(keep.members) + merged.extend(member.through(link) for member in moved.members) + self.groups[keep.representative.process_id] = ProcessGroup(merged) + return keep.representative.process_id + + def as_document(self) -> dict[str, object]: + groups = sorted( + self.groups.values(), key=lambda group: group.representative.process_id + ) + return { + "kind": PROCESS_GROUPS_KIND, + "schema_version": PROCESS_GROUPS_SCHEMA_VERSION, + "analyses": list(self.analyses), + "groups": [group.as_dict() for group in groups], + } + + def to_json(self) -> str: + return json.dumps(self.as_document(), indent=2, sort_keys=False) + "\n" + + @classmethod + def from_document(cls, document: Mapping[str, object]) -> ProcessGroupMap: + if document.get("kind") != PROCESS_GROUPS_KIND: + raise ValueError("document is not a pyAmpliCol process-group map") + if document.get("schema_version") != PROCESS_GROUPS_SCHEMA_VERSION: + raise ValueError("unsupported process-group map schema version") + result = cls(analyses=[dict(entry) for entry in document.get("analyses", [])]) # type: ignore[attr-defined] + for group in document.get("groups", []): # type: ignore[attr-defined] + members = [GroupMember.from_dict(member) for member in group["members"]] + result.add_group(members) + return result + + @classmethod + def from_json(cls, text: str) -> ProcessGroupMap: + return cls.from_document(json.loads(text)) + + +def rational_factor(value: float, *, tolerance: float) -> Fraction | None: + """Return ``value`` as a fraction with |numerator|, denominator <= 99.""" + + if value == 0 or value != value: + return None + fraction = Fraction(value).limit_denominator(MAX_RATIONAL) + if ( + fraction == 0 + or abs(fraction.numerator) > MAX_RATIONAL + or abs(float(fraction) - value) > tolerance * abs(value) + ): + return None + return fraction + + +__all__ = [ + "MAX_RATIONAL", + "PROCESS_GROUPS_KIND", + "PROCESS_GROUPS_PATH", + "PROCESS_GROUPS_ROLE", + "PROCESS_GROUPS_SCHEMA_VERSION", + "GroupMember", + "GroupingAnalysis", + "GroupingBasis", + "GroupingParticle", + "GroupingProcess", + "LegMapping", + "ParticleTable", + "ParticleType", + "ProcessGroup", + "ProcessGroupMap", + "candidate_mappings", + "coarse_key", + "compose_permutations", + "flavor_blind_classes", + "flavor_blind_mapping", + "flavor_class_key", + "identity_permutation", + "invert_permutation", + "particle_table", + "profile_key", + "profile_mappings", + "rational_factor", + "representative_key", + "resolved_parameter_name", + "signed_pdg_mapping", +] diff --git a/pyamplicol/src/pyamplicol/generation/process_grouping_numerical.py b/pyamplicol/src/pyamplicol/generation/process_grouping_numerical.py new file mode 100644 index 0000000000..bce9fc726b --- /dev/null +++ b/pyamplicol/src/pyamplicol/generation/process_grouping_numerical.py @@ -0,0 +1,784 @@ +# SPDX-License-Identifier: 0BSD +"""Numerical discovery of process groups in a generated artifact. + +Every stored representative is evaluated in f64 at three on-shell points. Each +point has its own deterministic variation of the free model inputs (external +and normalization parameters other than masses and widths, within +``PARAMETER_VARIATION``); a member and a representative are always compared +with the same values. A relation is +accepted when, through one leg mapping of +:func:`~pyamplicol.generation.process_grouping.candidate_mappings`, every +resolved helicity and colour component of the member is one common rational +factor times the matching component of the representative, at every point. + +Helicity values map unchanged, also under charge conjugation. LC colour-flow +words are relabelled and, for conjugated mappings, reversed; they are compared +as sets of open fundamental chains or as one cyclic adjoint trace. +""" + +from __future__ import annotations + +import hashlib +import json +import logging +import math +from collections.abc import Collection, Hashable, Mapping, Sequence +from dataclasses import dataclass +from fractions import Fraction +from pathlib import Path +from types import SimpleNamespace +from typing import Any, cast + +from ..api.errors import ArtifactError, PyAmpliColError +from ..api.results import ResolvedEvaluation +from ..artifacts.manifest import load_manifest +from ..artifacts.process_groups import ( + initial_process_groups, + read_process_groups, + write_process_groups, +) +from ..artifacts.security import confined_path +from .phase_space import massive_rambo_final_state +from .process_grouping import ( + GroupingAnalysis, + GroupingBasis, + GroupingProcess, + GroupMember, + LegMapping, + ParticleTable, + ProcessGroupMap, + candidate_mappings, + coarse_key, + identity_permutation, + invert_permutation, + particle_table, + rational_factor, + representative_key, + resolved_parameter_name, +) + +POINTS = 3 +PARAMETER_VARIATION = 0.1 +TOLERANCE = 1.0e-9 +DEFAULT_SEED = 20260917 +_VARIED_KINDS = frozenset({"external", "normalization"}) +# Components below _ZERO times the largest one at a point count as zero. The +# others must agree to TOLERANCE relative to themselves, but never more tightly +# than TOLERANCE times _FLOOR times the largest component. +_ZERO = 1.0e-12 +_FLOOR = 1.0e-4 +_MINIMUM_SQRT_S = 1000.0 +_LOGGER = logging.getLogger("pyamplicol.generation") + +FourMomentum = tuple[float, float, float, float] +Point = tuple[FourMomentum, ...] +ComponentKey = tuple[tuple[int, ...], Hashable] +Components = dict[ComponentKey, complex] + + +class _Unanalysable(Exception): + """A representative that cannot take part in the comparison.""" + + +# -------------------------------------------------------------------------- +# Pure helpers + + +def parameter_variation( + parameters: Sequence[Any], + *, + point: int, + seed: int = DEFAULT_SEED, + fixed: Collection[str] = frozenset(), +) -> dict[str, float | complex]: + """Varied values of the free inputs among ``parameters`` at ``point``. + + ``parameters`` are ``ModelParameter``-like records; ``fixed`` names the + mass and width parameters, which UFO models declare as external inputs. + The factor of each parameter depends only on its name, the point and the + seed, so every process sees the same values for the parameters it shares. + """ + + values: dict[str, float | complex] = {} + for parameter in parameters: + if ( + parameter.kind not in _VARIED_KINDS + or not parameter.mutable + or parameter.name in fixed + ): + continue + digest = hashlib.sha256(f"{seed}:{point}:{parameter.name}".encode()).digest() + uniform = int.from_bytes(digest[:8], "big") / float(1 << 64) + factor = 1.0 + PARAMETER_VARIATION * (2.0 * uniform - 1.0) + if parameter.default_imaginary: + values[parameter.name] = factor * complex( + parameter.default_real, parameter.default_imaginary + ) + else: + values[parameter.name] = factor * parameter.default_real + return values + + +def on_shell_point( + initial_masses: Sequence[float], + final_masses: Sequence[float], + *, + seed: int, +) -> Point | None: + """A deterministic on-shell point in the centre-of-mass frame. + + Beams may be massive; a single initial particle decays at rest. Returns + ``None`` when the masses leave no phase space. + """ + + final_masses = tuple(final_masses) + if len(initial_masses) == 1: + (mass,) = initial_masses + if len(final_masses) < 2 or mass <= sum(final_masses): + return None + return ((mass, 0.0, 0.0, 0.0), *_final_state(final_masses, mass, seed)) + first, second = initial_masses + if len(final_masses) == 1: + sqrt_s = final_masses[0] + if sqrt_s <= first + second: + return None + final: tuple[FourMomentum, ...] = ((sqrt_s, 0.0, 0.0, 0.0),) + else: + threshold = max(first + second, sum(final_masses)) + sqrt_s = max(_MINIMUM_SQRT_S, 2.0 * threshold) + final = _final_state(final_masses, sqrt_s, seed) + s = sqrt_s * sqrt_s + first_energy = (s + first * first - second * second) / (2.0 * sqrt_s) + momentum = math.sqrt(max(0.0, first_energy * first_energy - first * first)) + return ( + (first_energy, 0.0, 0.0, momentum), + (sqrt_s - first_energy, 0.0, 0.0, -momentum), + *final, + ) + + +def _final_state( + masses: Sequence[float], + sqrt_s: float, + seed: int, +) -> tuple[FourMomentum, ...]: + return massive_rambo_final_state( + len(masses), sqrt_s=sqrt_s, masses=masses, seed=seed + ) + + +def _point_seed(seed: int, point: int) -> int: + digest = hashlib.sha256(f"{seed}:{point}:phase-space".encode()).digest() + return int.from_bytes(digest[:8], "big") + + +def member_point(representative: Point, permutation: Sequence[int]) -> Point: + """Momenta of the member such that ``q[r] = p[permutation[r]]``.""" + + member: list[FourMomentum] = [representative[0]] * len(representative) + for representative_index, member_index in enumerate(permutation): + member[member_index] = representative[representative_index] + return tuple(member) + + +def color_roles( + pdgs: Sequence[int], + initial_count: int, + colors: Mapping[int, int], +) -> tuple[int, ...]: + """Colour representation of each leg with all legs outgoing.""" + + roles = [] + for index, pdg in enumerate(pdgs): + color = colors.get(pdg) + if color is None: + color = -colors[-pdg] if abs(colors[-pdg]) == 3 else colors[-pdg] + if index < initial_count and abs(color) == 3: + color = -color + roles.append(color) + return tuple(roles) + + +def flow_key( + word: Sequence[int], + roles: Sequence[int], + relabel: Sequence[int], + *, + reverse: bool, +) -> Hashable: + """Canonical colour tensor of an LC flow word in target leg indices. + + ``word`` holds zero-based source leg indices, ``roles`` their all-outgoing + representations and ``relabel`` maps source to target indices. Open lines + run from a fundamental through adjoints to an antifundamental and are + compared as a set; a pure-adjoint word is one trace, compared up to + rotation. ``reverse`` reverses every line and trace (charge conjugation). + A word that is neither is kept verbatim. + """ + + chains: list[tuple[int, ...]] = [] + trace: list[int] = [] + current: list[int] | None = None + decodable = True + for index in word: + role = roles[index] + if role == 3 and current is None: + current = [index] + elif role == -3 and current is not None: + current.append(index) + chains.append(tuple(current)) + current = None + elif role == 8: + (trace if current is None else current).append(index) + elif role != 1: + decodable = False + + def mapped(line: Sequence[int]) -> tuple[int, ...]: + result = tuple(relabel[index] for index in line) + return result[::-1] if reverse else result + + if not decodable or current is not None or (chains and trace): + return ("word", mapped(word)) + if chains: + return ("lines", tuple(sorted(mapped(chain) for chain in chains))) + cycle = mapped(trace) + if not cycle: + return ("singlet",) + start = cycle.index(min(cycle)) + return ("trace", cycle[start:] + cycle[:start]) + + +def resolved_components( + resolved: ResolvedEvaluation, + point: int, + *, + helicities: Mapping[str, Sequence[int]], + words: Mapping[str, Sequence[int]], + roles: Sequence[int], + mapping: LegMapping | None, +) -> Components: + """One point of a member's resolved values keyed in representative legs. + + ``helicities`` and ``words`` give the member's helicity values and LC + flow words (zero-based leg indices) by ID; ``mapping`` is the candidate + relation to the representative, ``None`` for the identity. + """ + + count = len(roles) + permutation = ( + identity_permutation(count) if mapping is None else mapping.permutation + ) + relabel = invert_permutation(permutation) + conjugated = mapping is not None and mapping.conjugated + helicity_keys = [ + tuple(helicities[identifier][index] for index in permutation) + for identifier in resolved.helicity_ids + ] + color_keys = [ + flow_key(words[identifier], roles, relabel, reverse=conjugated) + if resolved.color_accuracy == "lc" + else identifier + for identifier in resolved.color_ids + ] + return { + (helicity, color): complex(cast(complex, value)) + for helicity, row in zip(helicity_keys, resolved.values[point], strict=True) + for color, value in zip(color_keys, row, strict=True) + } + + +def common_factor( + member: Sequence[Components], + representative: Sequence[Components], + *, + tolerance: float = TOLERANCE, +) -> complex | None: + """The common ratio ``member / representative`` of all components. + + Both sequences hold one component table per point. Components that are + zero on one side must be zero on the other; a point that vanishes + entirely on either side gives no ratio. + """ + + factor: complex | None = None + for member_point_values, representative_point_values in zip( + member, representative, strict=True + ): + ratio = _point_factor( + member_point_values, representative_point_values, tolerance + ) + if ratio is None: + return None + if factor is None: + factor = ratio + elif abs(ratio - factor) > tolerance * abs(factor): + return None + return factor + + +def _point_factor( + member: Components, + representative: Components, + tolerance: float, +) -> complex | None: + member_scale = max(map(abs, member.values()), default=0.0) + representative_scale = max(map(abs, representative.values()), default=0.0) + if member_scale == 0.0 or representative_scale == 0.0: + return None + pivot = max(representative, key=lambda key: abs(representative[key])) + ratio = member.get(pivot, 0j) / representative[pivot] + if ratio == 0: + return None + for key in member.keys() | representative.keys(): + member_value = member.get(key, 0j) + representative_value = representative.get(key, 0j) + member_zero = abs(member_value) <= _ZERO * member_scale + representative_zero = abs(representative_value) <= _ZERO * representative_scale + if member_zero or representative_zero: + if member_zero != representative_zero: + return None + continue + allowed = tolerance * max(abs(member_value), _FLOOR * member_scale) + if abs(member_value - ratio * representative_value) > allowed: + return None + return ratio + + +def accepted_factor( + value: complex | None, + *, + only_unit_multiplier: bool, + tolerance: float = TOLERANCE, +) -> Fraction | None: + """The recordable rational factor of a numerical ratio, if any.""" + + if value is None or abs(value.imag) > tolerance * abs(value): + return None + factor = rational_factor(value.real, tolerance=tolerance) + if factor is None or (only_unit_multiplier and factor != 1): + return None + return factor + + +# -------------------------------------------------------------------------- +# Artifact model and runtimes + + +@dataclass(frozen=True, slots=True) +class _Model: + table: ParticleTable + colors: Mapping[int, int] + mass_names: Mapping[int, str] + fixed: frozenset[str] + defaults: Mapping[str, float] + complex_externals: frozenset[str] + + +def _artifact_model(artifact: Path) -> _Model: + manifest = load_manifest(artifact) + records = [item for item in manifest.payloads if item.role == "compiled-model"] + if len(records) != 1: + raise ArtifactError("process grouping requires one compiled-model payload") + ir = json.loads( + confined_path(manifest.root, records[0].path).read_text(encoding="utf-8") + )["ir"] + parameters = { + str(record["name"]): SimpleNamespace(**record) for record in ir["parameters"] + } + particles = [SimpleNamespace(**record) for record in ir["particles"]] + return _Model( + table=particle_table(particles, parameters), + colors={int(record.pdg_code): int(record.color) for record in particles}, + mass_names={ + int(record.pdg_code): resolved_parameter_name(str(record.mass), parameters) + for record in particles + }, + fixed=frozenset( + str(name) for record in particles for name in (record.mass, record.width) + ), + defaults={ + name: float((record.value or (0.0, 0.0))[0]) + for name, record in parameters.items() + }, + complex_externals=frozenset( + name + for name, record in parameters.items() + if record.nature == "external" and record.parameter_type == "complex" + ), + ) + + +@dataclass(slots=True) +class _Reference: + """A representative's own points and resolved components.""" + + process: GroupingProcess + accuracy: str + points: tuple[Point, ...] + components: tuple[Components, ...] + + +class _Probe: + """One loaded process and the component view of its evaluations.""" + + def __init__( + self, + artifact: Path, + process: GroupingProcess, + model: _Model, + seed: int, + ) -> None: + from ..api import Runtime + + self.process = process + self._model = model + self._seed = seed + self.runtime = Runtime.load( + artifact, process=process.process_id, mute_warnings=True + ) + physics = self.runtime.physics + if physics.helicity_coverage != "complete": + raise _Unanalysable("helicity coverage is selected") + if physics.color_coverage == "selected": + raise _Unanalysable("colour coverage is selected") + pdgs = tuple(particle.pdg_id for particle in physics.external_particles) + if pdgs != process.pdgs: + raise _Unanalysable("runtime legs differ from the process map") + self.accuracy = physics.color_accuracy + self.parameters = physics.model_parameters + # Masses are never varied: only derived ones need the runtime state. + derived = {item.name for item in self.parameters if item.kind == "derived"} + self._slots = ( + self._parameter_slots() + if any(model.mass_names[pdg] in derived for pdg in pdgs) + else None + ) + self._helicities = {item.id: item.values for item in physics.helicities} + index_of_label = { + particle.label: particle.index for particle in physics.external_particles + } + self._words = { + item.id: tuple(index_of_label[label] for label in item.word) + for item in physics.color_flows + } + self._roles = color_roles(pdgs, process.initial_count, model.colors) + + def _parameter_slots(self) -> dict[str, int] | None: + """Index of each real parameter in the runtime's current values. + + Derived and complex external parameters occupy a real and an + imaginary slot. ``None`` when the runtime does not expose them. + """ + + values = self._current_values() + if values is None: + return None + slots: dict[str, int] = {} + defaults: list[float] = [] + for parameter in self.parameters: + slots[parameter.name] = len(defaults) + defaults.append(parameter.default_real) + if ( + parameter.kind == "derived" + or parameter.name in self._model.complex_externals + ): + defaults.append(parameter.default_imaginary) + if len(defaults) != len(values) or not all( + math.isclose(value, default, rel_tol=1e-9, abs_tol=1e-12) + for value, default in zip(values, defaults, strict=True) + ): + raise _Unanalysable("runtime model-parameter layout is not recognised") + return slots + + def _current_values(self) -> tuple[float, ...] | None: + native = getattr(getattr(self.runtime, "_backend", None), "_runtime", None) + if native is None: + return None + from ..runtime._native_selection import exact_runtime_state_payload + + values = exact_runtime_state_payload(native)["model_parameter_values"] + return tuple(float(value) for value in cast(Sequence[float], values)) + + def apply(self, point: int) -> None: + values = parameter_variation( + self.parameters, point=point, seed=self._seed, fixed=self._model.fixed + ) + if values: + self.runtime.set_model_parameters(values) + + def masses(self) -> tuple[float, ...]: + """Current mass of every leg, derived ones read back from the runtime.""" + + current = self._current_values() if self._slots is not None else None + if current is not None and self._slots is not None: + known = {name: current[slot] for name, slot in self._slots.items()} + else: + known = { + parameter.name: parameter.default_real for parameter in self.parameters + } + masses = [] + for pdg in self.process.pdgs: + name = self._model.mass_names[pdg] + if name == "ZERO": + masses.append(0.0) + continue + for candidate in (name, f"particle.{abs(pdg)}.mass"): + if candidate in known: + masses.append(abs(known[candidate])) + break + else: + masses.append(abs(self._model.defaults.get(name, 0.0))) + return tuple(masses) + + def reference(self) -> _Reference: + points = [] + components = [] + count = self.process.initial_count + for point in range(POINTS): + self.apply(point) + masses = self.masses() + momenta = on_shell_point( + masses[:count], masses[count:], seed=_point_seed(self._seed, point) + ) + if momenta is None: + raise _Unanalysable("no on-shell phase space for the leg masses") + resolved = self.runtime.evaluate_resolved([momenta]) + points.append(momenta) + components.append(self.components(resolved, 0, None)) + return _Reference(self.process, self.accuracy, tuple(points), tuple(components)) + + def components( + self, + resolved: ResolvedEvaluation, + point: int, + mapping: LegMapping | None, + ) -> Components: + return resolved_components( + resolved, + point, + helicities=self._helicities, + words=self._words, + roles=self._roles, + mapping=mapping, + ) + + def relation( + self, + reference: _Reference, + mappings: Sequence[LegMapping], + *, + only_unit_multiplier: bool, + ) -> tuple[LegMapping, Fraction] | None: + """The first mapping relating this process to ``reference``. + + Each point is evaluated for all mappings still consistent with the + previous points, in one batch. + """ + + alive = list(mappings) + evaluated: dict[LegMapping, list[Components]] = { + mapping: [] for mapping in alive + } + for point in range(POINTS): + if not alive: + return None + self.apply(point) + resolved = self.runtime.evaluate_resolved( + [ + member_point(reference.points[point], mapping.permutation) + for mapping in alive + ] + ) + survivors = [] + for index, mapping in enumerate(alive): + evaluated[mapping].append(self.components(resolved, index, mapping)) + if ( + common_factor(evaluated[mapping], reference.components[: point + 1]) + is not None + ): + survivors.append(mapping) + alive = survivors + for mapping in alive: + factor = accepted_factor( + common_factor(evaluated[mapping], reference.components), + only_unit_multiplier=only_unit_multiplier, + ) + if factor is not None: + return mapping, factor + return None + + +# -------------------------------------------------------------------------- +# Analysis and storage + + +def analyse_process_groups( + artifact: str | Path, + group_map: ProcessGroupMap, + *, + only_unit_multiplier: bool = False, + consider_charge_conjugation: bool = True, + seed: int = DEFAULT_SEED, +) -> dict[str, object]: + """Merge numerically equal representatives of ``group_map`` in place. + + Representatives are visited in canonical order and compared with the + earlier ones that kept their group; the canonically smaller one stays the + representative of a merged group. + """ + + root = Path(artifact).expanduser().resolve(strict=True) + model = _artifact_model(root) + processes = sorted( + (member.process for member in group_map.representatives if member.generated), + key=lambda process: representative_key(process, model.table), + ) + kept: list[_Reference] = [] + relations: list[dict[str, object]] = [] + skipped: list[dict[str, str]] = [] + for process in processes: + try: + probe = _Probe(root, process, model, seed) + key = coarse_key(process, model.table) + found = None + for reference in kept: + if ( + reference.accuracy != probe.accuracy + or coarse_key(reference.process, model.table) != key + ): + continue + mappings = candidate_mappings( + process, + reference.process, + model.table, + consider_charge_conjugation=consider_charge_conjugation, + ) + result = probe.relation( + reference, + mappings, + only_unit_multiplier=only_unit_multiplier, + ) + if result is not None: + found = reference, *result + break + if found is None: + kept.append(probe.reference()) + continue + except (_Unanalysable, PyAmpliColError) as error: + _LOGGER.warning("process grouping skips %s: %s", process.process_id, error) + skipped.append({"process_id": process.process_id, "reason": str(error)}) + continue + reference, mapping, factor = found + relation = GroupMember.identity(process).related( + mapping, factor=factor, basis=GroupingBasis.NUMERICAL + ) + group_map.merge(reference.process.process_id, relation, model.table) + relations.append( + { + "process_id": process.process_id, + "process": process.expression, + "representative": reference.process.process_id, + "factor": str(factor), + "permutation": list(mapping.permutation), + "conjugated": mapping.conjugated, + } + ) + return { + "processes_analysed": len(processes) - len(skipped), + "new_relations": relations, + "skipped": skipped, + } + + +def _analysis_record( + *, + only_unit_multiplier: bool, + consider_charge_conjugation: bool, + seed: int, +) -> dict[str, object]: + return { + "variant": str(GroupingAnalysis.NUMERICAL), + "consider_charge_conjugation": consider_charge_conjugation, + "only_unit_multiplier": only_unit_multiplier, + "points": POINTS, + "parameter_variation": PARAMETER_VARIATION, + "tolerance": TOLERANCE, + "seed": seed, + } + + +def update_artifact_process_groups( + artifact: str | Path, + *, + analysis: GroupingAnalysis | str, + only_unit_multiplier: bool, + consider_charge_conjugation: bool, +) -> dict[str, object]: + """Analyse an artifact's representatives and rewrite its group map. + + ``"none"`` only writes the trivial map when the artifact has none. The + numerical analysis merges new relations into the stored map; the artifact + identity is unchanged. Returns a summary of the update. + """ + + analysis = GroupingAnalysis(analysis) + root = Path(artifact).expanduser().resolve(strict=True) + group_map = read_process_groups(root) + changed = group_map is None + if group_map is None: + group_map = initial_process_groups( + ( + str(process["id"]), + str(process["expression"]), + cast(Sequence[int], process["external_pdgs"]), + ) + for process in load_manifest(root).processes + ) + summary: dict[str, object] = { + "artifact": str(root), + "analysis": str(analysis), + "groups_before": len(group_map.groups), + "processes_analysed": 0, + "new_relations": [], + "skipped": [], + } + if analysis is GroupingAnalysis.NUMERICAL: + summary.update( + analyse_process_groups( + root, + group_map, + only_unit_multiplier=only_unit_multiplier, + consider_charge_conjugation=consider_charge_conjugation, + ) + ) + record = _analysis_record( + only_unit_multiplier=only_unit_multiplier, + consider_charge_conjugation=consider_charge_conjugation, + seed=DEFAULT_SEED, + ) + if record not in group_map.analyses: + group_map.analyses.append(record) + changed = True + changed = changed or bool(summary["new_relations"]) + if changed: + write_process_groups(root, group_map) + summary["groups_after"] = len(group_map.groups) + return summary + + +__all__ = [ + "DEFAULT_SEED", + "PARAMETER_VARIATION", + "POINTS", + "TOLERANCE", + "accepted_factor", + "analyse_process_groups", + "color_roles", + "common_factor", + "flow_key", + "member_point", + "on_shell_point", + "parameter_variation", + "resolved_components", + "update_artifact_process_groups", +] diff --git a/pyamplicol/src/pyamplicol/generation/process_grouping_symmetry.py b/pyamplicol/src/pyamplicol/generation/process_grouping_symmetry.py new file mode 100644 index 0000000000..188de906b5 --- /dev/null +++ b/pyamplicol/src/pyamplicol/generation/process_grouping_symmetry.py @@ -0,0 +1,297 @@ +# SPDX-License-Identifier: 0BSD +"""Whether a flavour-blind relation is a symmetry of the allowed model vertices. + +``--flavor-blind`` assumes that processes related by a type-preserving flavour +relabelling, optionally combined with charge conjugation, have equal matrix +elements. That holds when the relation maps the vertices the representative is +generated with onto themselves, which this module checks locally: + +* The relabelling of the member's flavours onto the representative's is + completed to a permutation of the model's particles (flavours present on + one side only are paired by particle type) and applied to every allowed + vertex. Each moved vertex must have an allowed image with the same particles + in the same order and the same structure and coupling: Lorentz and colour + expressions and resolved coupling for compiled models, kernel kind and + coupling for the built-in SM. +* Charge conjugation with unchanged helicities is a symmetry of vector-like + interactions only. It is accepted when the conjugate of every allowed + vertex is allowed and no allowed vertex couples fermions chirally: a + Lorentz structure that changes when its left- and right-handed projectors + are exchanged (or contains ``gamma5``), or a built-in electroweak current + with different left- and right-handed couplings. + +Allowed vertices are those whose coupling orders fit ``process.max_coupling_orders`` +and, under the default ``minimal`` coupling-order policy, the representative's +minimal coupling-order envelope. Masses and widths already belong to the +particle type of a flavour class. The check inspects vertices, not diagrams, +so it may warn about a relation that happens to hold for the processes at hand. +""" + +from __future__ import annotations + +import functools +import re +from collections import Counter +from collections.abc import Hashable, Iterable, Mapping, Sequence +from dataclasses import dataclass, field + +from ..models.base import Model +from ..models.contracts import CompiledModelIR +from .process_grouping import GroupingProcess, LegMapping, ParticleTable + + +@dataclass(frozen=True, slots=True) +class SymmetryVertex: + particles: tuple[int, ...] + orders: tuple[tuple[str, int], ...] + signature: Hashable + """Structure and coupling that a symmetry must preserve.""" + chiral: bool = False + + +@dataclass(frozen=True, slots=True) +class _AllowedVertices: + by_particles: Mapping[tuple[int, ...], Counter[Hashable]] + multisets: frozenset[tuple[int, ...]] + chiral: tuple[tuple[int, ...], ...] + + +def _orders_allowed( + orders: Iterable[tuple[str, int]], + limits: Mapping[str, int], +) -> bool: + return all( + int(value) <= limits.get(str(name).upper(), int(value)) + for name, value in orders + ) + + +_PROJECTOR = re.compile(r"(proj)([mp])", re.IGNORECASE) +_EXCHANGED_PROJECTOR = {"m": "p", "p": "m", "M": "P", "P": "M"} + + +@functools.cache +def _lorentz_is_chiral(expression: str) -> bool: + if "gamma5" in expression.casefold(): + return True + if _PROJECTOR.search(expression) is None: + return False + from ..models import compiler_symbolica as _sym + + _sym._ensure_symbolica() + exchanged = _PROJECTOR.sub( + lambda match: match.group(1) + _EXCHANGED_PROJECTOR[match.group(2)], + expression, + ) + try: + return bool( + _sym.E(expression).expand().to_canonical_string() + != _sym.E(exchanged).expand().to_canonical_string() + ) + except Exception: # an unparsable structure is not assumed vector-like + return True + + +@dataclass(slots=True) +class VertexSymmetryTable: + """Model vertices and antiparticles, indexed per set of coupling limits.""" + + vertices: Sequence[SymmetryVertex] + antiparticles: Mapping[int, int] + _allowed: dict[tuple[tuple[str, int], ...], _AllowedVertices] = field( + default_factory=dict + ) + + @classmethod + def from_compiled_ir(cls, model: CompiledModelIR) -> VertexSymmetryTable: + by_name = {particle.name: particle for particle in model.particles} + couplings = { + coupling.name: coupling.resolved_expression for coupling in model.couplings + } + vertices = [ + SymmetryVertex( + particles=tuple(by_name[name].pdg_code for name in term.particles), + orders=tuple(term.coupling_orders), + signature=( + term.lorentz_expression, + term.color_expression, + couplings.get(term.coupling, term.coupling_expression), + ), + chiral=_lorentz_is_chiral(term.lorentz_expression), + ) + for term in model.vertex_terms + ] + antiparticles = { + particle.pdg_code: by_name[particle.antiname].pdg_code + for particle in model.particles + if particle.antiname in by_name + } + return cls(vertices, antiparticles) + + @classmethod + def from_builtin_model(cls, model: Model) -> VertexSymmetryTable: + vertices = [] + for vertex in model.vertices: + rule = model.vertex_lowering_rule(vertex.kind) + vertices.append( + SymmetryVertex( + particles=tuple(vertex.particles), + orders=tuple(model.vertex_coupling_orders(vertex)), + signature=(vertex.kind, tuple(vertex.coupling)), + # Built-in electroweak fermion currents carry (left, right) + # couplings. + chiral=rule.expression_head == "fermion_gauge_weyl_current" + and vertex.coupling[0] != vertex.coupling[1], + ) + ) + antiparticles = { + pdg: particle.anti_pdg for pdg, particle in model.particles.items() + } + return cls(vertices, antiparticles) + + def _antiparticle(self, pdg: int) -> int: + return self.antiparticles.get(pdg, -pdg) + + def allowed(self, limits: Mapping[str, int]) -> _AllowedVertices: + key = tuple( + sorted((str(name).upper(), int(value)) for name, value in limits.items()) + ) + cached = self._allowed.get(key) + if cached is not None: + return cached + upper = dict(key) + by_particles: dict[tuple[int, ...], Counter[Hashable]] = {} + chiral: dict[tuple[int, ...], None] = {} + for vertex in self.vertices: + if not _orders_allowed(vertex.orders, upper): + continue + by_particles.setdefault(vertex.particles, Counter())[vertex.signature] += 1 + if vertex.chiral: + chiral.setdefault(vertex.particles, None) + result = _AllowedVertices( + by_particles=by_particles, + multisets=frozenset(tuple(sorted(particles)) for particles in by_particles), + chiral=tuple(chiral), + ) + self._allowed[key] = result + return result + + def relabelling( + self, + member: GroupingProcess, + representative: GroupingProcess, + mapping: LegMapping, + table: ParticleTable, + ) -> dict[int, int]: + """The particle permutation induced by ``mapping``, conjugation aside.""" + + partial: dict[int, int] = {} + for representative_leg, member_leg in enumerate(mapping.permutation): + source = member.pdgs[member_leg] + if mapping.conjugated: + source = self._antiparticle(source) + target = representative.pdgs[representative_leg] + for one, other in ( + (source, target), + (self._antiparticle(source), self._antiparticle(target)), + ): + if partial.setdefault(one, other) != other: + raise ValueError("leg mapping is not a flavour relabelling") + if len(set(partial.values())) != len(partial): + raise ValueError("leg mapping is not a flavour relabelling") + # Complete the injection to a permutation: particles that are only + # images receive the particles that are only sources, type by type. + sources = sorted( + (pdg for pdg in partial if pdg not in partial.values()), + key=lambda pdg: (table[pdg].type, abs(pdg), pdg), + ) + images = sorted( + (pdg for pdg in partial.values() if pdg not in partial), + key=lambda pdg: (table[pdg].type, abs(pdg), pdg), + ) + partial.update(zip(images, sources, strict=True)) + return { + source: target for source, target in partial.items() if source != target + } + + def relation_warning( + self, + member: GroupingProcess, + representative: GroupingProcess, + mapping: LegMapping, + table: ParticleTable, + *, + limits: Mapping[str, int], + ) -> str | None: + """Why ``mapping`` may not be a symmetry, or ``None``.""" + + allowed = self.allowed(limits) + + def name(pdg: int) -> str: + particle = table.get(pdg) + return particle.name if particle is not None else str(pdg) + + def vertex_text(particles: Sequence[int]) -> str: + return "[" + " ".join(name(pdg) for pdg in particles) + "]" + + relation = ( + f"{member.expression!r} assumed equal to {representative.expression!r}" + ) + reasons = [] + permutation = self.relabelling(member, representative, mapping, table) + for particles, signatures in allowed.by_particles.items(): + image = tuple(permutation.get(pdg, pdg) for pdg in particles) + if image != particles and allowed.by_particles.get(image) != signatures: + moved = ", ".join( + f"{name(source)}->{name(target)}" + for source, target in sorted(permutation.items()) + if source > 0 or table[source].type.self_conjugate + ) + reasons.append( + f"relabelling {moved} changes the allowed vertex " + f"{vertex_text(particles)}" + ) + break + if mapping.conjugated: + if allowed.chiral: + # Prefer an example that involves the flavours of the process. + flavours = {abs(pdg) for pdg in representative.pdgs} + chiral = next( + ( + particles + for particles in allowed.chiral + if flavours & {abs(pdg) for pdg in particles} + ), + allowed.chiral[0], + ) + reasons.append( + "charge conjugation is broken by the chiral allowed vertex " + f"{vertex_text(chiral)}" + ) + else: + missing = next( + ( + particles + for particles in allowed.multisets + if tuple(sorted(self._antiparticle(pdg) for pdg in particles)) + not in allowed.multisets + ), + None, + ) + if missing is not None: + reasons.append( + "charge conjugation has no allowed image of the vertex " + f"{vertex_text(missing)}" + ) + if not reasons: + return None + limit_text = ", ".join( + f"{key}<={value}" for key, value in sorted(limits.items()) + ) + return ( + f"{relation}, but {' and '.join(reasons)} " + f"(coupling orders: {limit_text or 'unrestricted'})" + ) + + +__all__ = ["SymmetryVertex", "VertexSymmetryTable"] diff --git a/pyamplicol/src/pyamplicol/generation/progress.py b/pyamplicol/src/pyamplicol/generation/progress.py new file mode 100644 index 0000000000..60c7a9e595 --- /dev/null +++ b/pyamplicol/src/pyamplicol/generation/progress.py @@ -0,0 +1,237 @@ +# SPDX-License-Identifier: 0BSD +"""Typed progress phases for the generation service.""" + +from __future__ import annotations + +import time +from collections.abc import Iterator, Mapping +from contextlib import contextmanager +from dataclasses import dataclass, field +from threading import Lock + +from pyamplicol.reporting import ( + NullProgressSink, + ProgressEnd, + ProgressSink, + ProgressStart, + ProgressUpdate, +) +from pyamplicol.reporting.progress import ProgressDetailValue + + +@dataclass(slots=True) +class PhaseHandle: + task_id: str + sink: ProgressSink | None + total: int | None + completed: int = 0 + _lock: Lock = field(default_factory=Lock, init=False, repr=False) + + def update( + self, + completed: int, + *, + message: str | None = None, + total: int | None = None, + details: Mapping[str, ProgressDetailValue] | None = None, + ) -> None: + with self._lock: + self._update_locked( + completed, + message=message, + total=total, + details=details, + ) + + def advance( + self, + *, + message: str | None = None, + details: Mapping[str, ProgressDetailValue] | None = None, + ) -> None: + with self._lock: + self._update_locked( + self.completed + 1, + message=message, + total=None, + details=details, + ) + + def _update_locked( + self, + completed: int, + *, + message: str | None, + total: int | None, + details: Mapping[str, ProgressDetailValue] | None, + ) -> None: + active_total = self.total if total is None else total + if total is not None: + self.total = total + bounded = completed + if active_total is not None: + bounded = min(completed, active_total) + self.completed = max(self.completed, bounded) + if self.sink is not None: + self.sink.emit( + ProgressUpdate( + self.task_id, + completed=self.completed, + total=active_total, + message=message, + details={} if details is None else details, + ) + ) + + @contextmanager + def child( + self, + key: str, + description: str, + *, + total: int | None = None, + unit: str = "items", + details: Mapping[str, ProgressDetailValue] | None = None, + ) -> Iterator[PhaseHandle]: + task_id = f"{self.task_id}:{key}" + if self.sink is not None: + self.sink.emit( + ProgressStart( + task_id, + description, + total=total, + parent_task_id=self.task_id, + unit=unit, + details={} if details is None else details, + ) + ) + handle = PhaseHandle(task_id, self.sink, total) + started = time.perf_counter() + try: + yield handle + except BaseException as exc: + elapsed = time.perf_counter() - started + if self.sink is not None: + self.sink.emit( + ProgressEnd( + task_id, + success=False, + message=f"{type(exc).__name__}: {exc}", + elapsed_seconds=elapsed, + ) + ) + raise + elapsed = time.perf_counter() - started + if total is not None and handle.completed < total: + handle.update(total) + if self.sink is not None: + self.sink.emit(ProgressEnd(task_id, elapsed_seconds=elapsed)) + + +@dataclass(slots=True) +class GenerationPhaseReporter: + sink: ProgressSink | None + timings: dict[str, float] = field(default_factory=dict) + root: PhaseHandle | None = None + _root_started_at: float | None = None + + def __post_init__(self) -> None: + # Progress-off must not install callbacks into generation machinery. + if isinstance(self.sink, NullProgressSink): + self.sink = None + + def start_generation( + self, + *, + total: int, + details: Mapping[str, ProgressDetailValue], + ) -> None: + task_id = "generation" + self._root_started_at = time.perf_counter() + if self.sink is not None: + self.sink.emit( + ProgressStart( + task_id, + "Generating processes", + total=total, + unit="phases", + details=details, + ) + ) + self.root = PhaseHandle(task_id, self.sink, total) + + def finish_generation( + self, + *, + success: bool = True, + message: str | None = None, + ) -> None: + if self.root is None or self._root_started_at is None: + return + elapsed = time.perf_counter() - self._root_started_at + if ( + success + and self.root.total is not None + and self.root.completed < self.root.total + ): + self.root.update(self.root.total) + if self.sink is not None: + self.sink.emit( + ProgressEnd( + self.root.task_id, + success=success, + message=message, + elapsed_seconds=elapsed, + ) + ) + self._root_started_at = None + + @contextmanager + def phase( + self, + name: str, + description: str, + *, + total: int | None = None, + ) -> Iterator[PhaseHandle]: + task_id = f"generation:{name}" + if self.sink is not None: + self.sink.emit( + ProgressStart( + task_id, + description, + total=total, + parent_task_id=None if self.root is None else self.root.task_id, + ) + ) + handle = PhaseHandle(task_id, self.sink, total) + started = time.perf_counter() + try: + yield handle + except BaseException as exc: + elapsed = time.perf_counter() - started + self.timings[name] = elapsed + if self.sink is not None: + self.sink.emit( + ProgressEnd( + task_id, + success=False, + message=f"{type(exc).__name__}: {exc}", + elapsed_seconds=elapsed, + ) + ) + raise + elapsed = time.perf_counter() - started + self.timings[name] = elapsed + if total is not None and handle.completed < total: + handle.update(total) + if self.sink is not None: + self.sink.emit(ProgressEnd(task_id, elapsed_seconds=elapsed)) + if self.root is not None: + self.root.advance( + message=description, + details={"step": description}, + ) + + +__all__ = ["GenerationPhaseReporter", "PhaseHandle"] diff --git a/pyamplicol/src/pyamplicol/generation/recurrence_color.py b/pyamplicol/src/pyamplicol/generation/recurrence_color.py new file mode 100644 index 0000000000..54716a889f --- /dev/null +++ b/pyamplicol/src/pyamplicol/generation/recurrence_color.py @@ -0,0 +1,987 @@ +# SPDX-License-Identifier: 0BSD +"""Bounded binary codec for process-owned recurrence color contraction.""" + +from __future__ import annotations + +import math +import struct +from collections.abc import Sequence +from hashlib import sha256 + +from pyamplicol.color import ( + ColorContractionEntry, + ColorContractionPlan, + ColorContractionTemplateEntry, + FactorizedColorContractionBlock, + SymmetricGroupColorContractionBlock, +) +from pyamplicol.generation.recurrence_columnar import ExactComplexRationalV1 + +RECURRENCE_COLOR_CONTRACTION_CODEC_ABI = "pyamplicol-recurrence-color-contraction-v3" +RECURRENCE_COLOR_CONTRACTION_MAGIC = b"PACRCLR3" + +_VERSION = 3 +_STORAGE_EXPANDED = 1 +_STORAGE_REPEATED = 2 +_STORAGE_CONVOLUTION_KERNELS = 3 +_ACCURACY_NLC = 1 +_ACCURACY_FULL = 2 +_FACTOR_NONE = 0 +_FACTOR_KLEIN_FOUR = 1 +_FACTOR_ELEMENTARY_ABELIAN = 2 +_FACTOR_SYMMETRIC_GROUP_FOURIER = 3 +_FLAG_INCLUDES_COLOR_FACTOR = 1 << 0 +_KNOWN_FLAGS = _FLAG_INCLUDES_COLOR_FACTOR +_MAX_FACTOR_RANK = 31 # 2**rank must fit the u32 local-group count. +_MAX_SYMMETRIC_GROUP_DEGREE = 10 +_ZERO_SECTOR_OWNER = 0xFFFF_FFFF + +# magic; 14 u32 fields; 7 u64 fields +_HEADER = struct.Struct("<8s14I7Q") +# left/right group or local-group IDs; raw complex weight; raw symmetry factor; +# exact, symmetry-folded coefficient catalog ID. +_ENTRY = struct.Struct(" bytes: + """Encode one supported NLC/full color-contraction plan. + + The caller owns payload hashing and container authentication. The codec + deliberately preserves raw weights and symmetry factors as distinct f64 + fields while binding every row to a canonical exact coefficient. + + >>> plan = ColorContractionPlan( + ... color_accuracy="nlc", + ... supported=True, + ... reason=None, + ... group_count=2, + ... entries=( + ... ColorContractionEntry(0, 0, 3.0), + ... ColorContractionEntry(0, 1, 2.0, symmetry_factor=2.0), + ... ), + ... ) + >>> payload = encode_recurrence_color_contraction( + ... plan, + ... sector_count=2, + ... component_count=1, + ... ordered_group_ids=(0, 1), + ... destination_by_group=(7, 9), + ... group_sector_ids=(0, 1), + ... group_component_ids=(0, 0), + ... sector_owner_ids=(0, 1), + ... exact_coefficients=( + ... ExactComplexRationalV1(3), + ... ExactComplexRationalV1(4), + ... ), + ... destination_count=10, + ... ) + >>> payload[:8] == RECURRENCE_COLOR_CONTRACTION_MAGIC + True + >>> len(payload) == ( + ... _HEADER.size + ... + 2 * _ENTRY.size + ... + 2 * _EXACT_FACTOR_BYTES + ... + 10 * _U32.size + ... ) + True + """ + + if not isinstance(plan, ColorContractionPlan): + raise RecurrenceColorCodecError( + "recurrence color payload requires a ColorContractionPlan" + ) + if not plan.supported: + raise RecurrenceColorCodecError( + "unsupported color-contraction plans cannot be encoded" + ) + if plan.reason is not None: + raise RecurrenceColorCodecError( + "a supported color-contraction plan cannot carry a failure reason" + ) + accuracy = _encode_accuracy(plan.color_accuracy) + group_count = _checked_u32("group_count", plan.group_count) + if group_count == 0: + raise RecurrenceColorCodecError("group_count must be positive") + sector_count = _checked_u32("sector_count", sector_count) + component_count = _checked_u32("component_count", component_count) + if sector_count == 0 or component_count == 0: + raise RecurrenceColorCodecError( + "sector_count and component_count must be positive" + ) + ordered_groups = tuple( + _checked_u32("ordered group ID", value) for value in ordered_group_ids + ) + destinations = tuple( + _checked_u32("Direct-Arena destination ID", value) + for value in destination_by_group + ) + sectors = tuple( + _checked_u32("group sector ID", value) for value in group_sector_ids + ) + components = tuple( + _checked_u32("group component ID", value) for value in group_component_ids + ) + owners = tuple( + _checked_u32("physical sector owner ID", value) for value in sector_owner_ids + ) + destination_count = _checked_u32("destination_count", destination_count) + if destination_count == 0: + raise RecurrenceColorCodecError("destination_count must be positive") + if len(ordered_groups) != group_count: + raise RecurrenceColorCodecError( + "ordered group map count does not match group_count" + ) + if sorted(ordered_groups) != list(range(group_count)): + raise RecurrenceColorCodecError( + "ordered group map is not a permutation of all group IDs" + ) + if len(destinations) != group_count: + raise RecurrenceColorCodecError( + "destination map count does not match group_count" + ) + if len(set(destinations)) != len(destinations): + raise RecurrenceColorCodecError( + "destination map contains duplicate Direct-Arena destinations" + ) + if any(value >= destination_count for value in destinations): + raise RecurrenceColorCodecError( + "destination map references an out-of-bounds Direct-Arena destination" + ) + _validate_group_coordinates( + group_count=group_count, + sector_count=sector_count, + component_count=component_count, + group_sector_ids=sectors, + group_component_ids=components, + ) + _validate_sector_owners( + sector_count=sector_count, + owner_by_sector=owners, + active_sector_ids=set(sectors), + ) + + flags = _FLAG_INCLUDES_COLOR_FACTOR if plan.includes_color_factor else 0 + if flags & ~_KNOWN_FLAGS: + raise AssertionError("internal recurrence color flag drift") + + repeated = plan.repeated_block + symmetric_group = plan.symmetric_group_block + if repeated is None and symmetric_group is None: + storage = _STORAGE_EXPANDED + local_group_count = 0 + entries: Sequence[ColorContractionEntry | ColorContractionTemplateEntry] = ( + plan.entries + ) + factor_kind = _FACTOR_NONE + factor_rank = 0 + cosets: tuple[tuple[int, ...], ...] = () + _validate_entries(entries, group_count=group_count, label="expanded") + _validate_expanded_components( + entries, + group_component_ids=components, + ) + logical_entry_count = len(entries) + elif repeated is not None: + if plan.entries: + raise RecurrenceColorCodecError( + "expanded and repeated color-contraction storage cannot be mixed" + ) + storage = _STORAGE_REPEATED + repeated_component_count = _checked_u32( + "repeated component_count", repeated.component_count + ) + if repeated_component_count < 2: + raise RecurrenceColorCodecError( + "repeated storage requires at least two components" + ) + if repeated_component_count != component_count: + raise RecurrenceColorCodecError( + "repeated component_count does not match the explicit component_count" + ) + repeated_group_map = tuple( + _checked_u32("repeated component group ID", value) + for value in repeated.component_group_ids + ) + if len(repeated_group_map) != group_count: + raise RecurrenceColorCodecError( + "repeated group map count does not match group_count" + ) + if repeated_group_map != ordered_groups: + raise RecurrenceColorCodecError( + "ordered group map does not match repeated local-color-major/" + "component-minor storage" + ) + if len(set(repeated_group_map)) != len(repeated_group_map): + raise RecurrenceColorCodecError( + "repeated group map contains duplicate group IDs" + ) + if any(value >= group_count for value in repeated_group_map): + raise RecurrenceColorCodecError( + "repeated group map references an out-of-bounds group" + ) + if len(repeated_group_map) % component_count: + raise RecurrenceColorCodecError("repeated group map is not rectangular") + local_group_count = len(repeated_group_map) // component_count + if local_group_count == 0: + raise RecurrenceColorCodecError( + "repeated storage requires at least one local color group" + ) + _validate_repeated_group_coordinates( + ordered_group_ids=ordered_groups, + group_sector_ids=sectors, + group_component_ids=components, + sector_count=sector_count, + component_count=component_count, + local_group_count=local_group_count, + ) + entries = repeated.entries + _validate_entries( + entries, + group_count=local_group_count, + label="repeated", + ) + factor_kind, factor_rank, cosets = _encode_factorization( + repeated.factorized_block, + local_group_count=local_group_count, + entries=repeated.entries, + ) + logical_entry_count = _checked_product( + "logical entry count", component_count, len(entries) + ) + else: + if symmetric_group is None: + raise AssertionError("unreachable symmetric-group storage branch") + if plan.entries or repeated is not None: + raise RecurrenceColorCodecError( + "convolution-kernel storage cannot be mixed with expanded or " + "repeated color contraction" + ) + storage = _STORAGE_CONVOLUTION_KERNELS + if symmetric_group.component_count != component_count: + raise RecurrenceColorCodecError( + "symmetric-group component_count does not match the explicit " + "component_count" + ) + symmetric_group_map = tuple( + _checked_u32("symmetric-group component group ID", value) + for value in symmetric_group.component_group_ids + ) + if symmetric_group_map != ordered_groups: + raise RecurrenceColorCodecError( + "ordered group map does not match symmetric-group local-color-major/" + "component-minor storage" + ) + if len(symmetric_group_map) % component_count: + raise RecurrenceColorCodecError( + "symmetric-group component map is not rectangular" + ) + local_group_count = len(symmetric_group_map) // component_count + if local_group_count == 0: + raise RecurrenceColorCodecError( + "convolution-kernel storage requires at least one local color group" + ) + if _checked_u32("symmetric-group local_group_count", local_group_count) != ( + symmetric_group.local_group_count + ): + raise RecurrenceColorCodecError( + "symmetric-group local group count is inconsistent" + ) + _validate_repeated_group_coordinates( + ordered_group_ids=ordered_groups, + group_sector_ids=sectors, + group_component_ids=components, + sector_count=sector_count, + component_count=component_count, + local_group_count=local_group_count, + ) + local_sector_ids = tuple( + sectors[symmetric_group_map[index * component_count]] + for index in range(local_group_count) + ) + if local_sector_ids != symmetric_group.local_sector_ids: + raise RecurrenceColorCodecError( + "symmetric-group local sector order disagrees with the group map" + ) + entries = (*symmetric_group.kernel_entries, *symmetric_group.residual_entries) + _validate_entries( + entries, + group_count=local_group_count, + label="convolution-kernel", + ) + factor_kind, factor_rank, cosets = _encode_symmetric_group_convolution( + symmetric_group, + local_group_count=local_group_count, + entries=entries, + ) + expected_exact_coefficients = _symmetric_group_exact_coefficients( + symmetric_group + ) + if tuple(exact_coefficients) != expected_exact_coefficients: + raise RecurrenceColorCodecError( + "symmetric-group exact color coefficients disagree with the " + "certified convolution plan" + ) + logical_entry_count = _checked_product( + "logical entry count", component_count, len(entries) + ) + + entry_count = _checked_u64("entry_count", len(entries)) + if len(exact_coefficients) != len(entries): + raise RecurrenceColorCodecError( + "exact color coefficient count does not match entry_count" + ) + exact_catalog: list[ExactComplexRationalV1] = [] + exact_id_by_key: dict[tuple[int, int, int, int], int] = {} + exact_factor_ids: list[int] = [] + for index, (entry, factor) in enumerate( + zip(entries, exact_coefficients, strict=True) + ): + if not isinstance(factor, ExactComplexRationalV1): + raise RecurrenceColorCodecError( + f"exact color coefficient {index} is not canonical" + ) + _validate_exact_matches_f64(entry, factor, index) + factor_id = exact_id_by_key.get(factor.canonical_key) + if factor_id is None: + factor_id = len(exact_catalog) + _checked_u32("exact color factor ID", factor_id) + exact_id_by_key[factor.canonical_key] = factor_id + exact_catalog.append(factor) + exact_factor_ids.append(factor_id) + exact_factor_count = _checked_u64("exact_factor_count", len(exact_catalog)) + coset_count = _checked_u64("coset_count", len(cosets)) + flattened_cosets = tuple(value for coset in cosets for value in coset) + coset_index_count = _checked_u64("coset_index_count", len(flattened_cosets)) + logical_entry_count_u64 = _checked_u64("logical_entry_count", logical_entry_count) + owner_map_count = _checked_u64("owner_map_count", len(owners)) + + payload_size = _checked_payload_size( + entry_count=entry_count, + exact_factor_count=exact_factor_count, + group_count=group_count, + sector_count=sector_count, + coset_index_count=coset_index_count, + ) + header = _HEADER.pack( + RECURRENCE_COLOR_CONTRACTION_MAGIC, + _VERSION, + _HEADER.size, + storage, + accuracy, + flags, + group_count, + sector_count, + component_count, + local_group_count, + destination_count, + factor_kind, + factor_rank, + _ENTRY.size, + _EXACT_FACTOR_BYTES, + entry_count, + exact_factor_count, + coset_count, + coset_index_count, + logical_entry_count_u64, + owner_map_count, + payload_size, + ) + output = bytearray(_HEADER.size + payload_size) + output[: _HEADER.size] = header + offset = _HEADER.size + for entry, exact_factor_id in zip(entries, exact_factor_ids, strict=True): + _ENTRY.pack_into( + output, + offset, + _entry_left(entry), + _entry_right(entry), + float(entry.weight_re), + float(entry.weight_im), + float(entry.symmetry_factor), + exact_factor_id, + ) + offset += _ENTRY.size + for factor in exact_catalog: + offset = _pack_exact_factor(output, offset, factor) + for value in ordered_groups: + _U32.pack_into(output, offset, value) + offset += _U32.size + for value in destinations: + _U32.pack_into(output, offset, value) + offset += _U32.size + for value in sectors: + _U32.pack_into(output, offset, value) + offset += _U32.size + for value in components: + _U32.pack_into(output, offset, value) + offset += _U32.size + for value in owners: + _U32.pack_into(output, offset, value) + offset += _U32.size + for value in flattened_cosets: + _U32.pack_into(output, offset, value) + offset += _U32.size + if offset != len(output): + raise AssertionError("internal recurrence color payload size drift") + return bytes(output) + + +def _encode_accuracy(value: str) -> int: + if value == "nlc": + return _ACCURACY_NLC + if value == "full": + return _ACCURACY_FULL + raise RecurrenceColorCodecError( + f"recurrence color payload requires nlc or full accuracy, got {value!r}" + ) + + +def _entry_left( + entry: ColorContractionEntry | ColorContractionTemplateEntry, +) -> int: + if isinstance(entry, ColorContractionEntry): + return entry.left_group_id + return entry.left_group_index + + +def _entry_right( + entry: ColorContractionEntry | ColorContractionTemplateEntry, +) -> int: + if isinstance(entry, ColorContractionEntry): + return entry.right_group_id + return entry.right_group_index + + +def _validate_entries( + entries: Sequence[ColorContractionEntry | ColorContractionTemplateEntry], + *, + group_count: int, + label: str, +) -> None: + seen: set[tuple[int, int]] = set() + for index, entry in enumerate(entries): + left = _checked_u32(f"{label} entry {index} left ID", _entry_left(entry)) + right = _checked_u32(f"{label} entry {index} right ID", _entry_right(entry)) + if left >= group_count or right >= group_count: + raise RecurrenceColorCodecError( + f"{label} entry {index} references an out-of-bounds group" + ) + if left > right: + raise RecurrenceColorCodecError( + f"{label} entry {index} is not canonical upper triangular" + ) + pair = (left, right) + if pair in seen: + raise RecurrenceColorCodecError( + f"{label} entries contain duplicate pair {pair}" + ) + seen.add(pair) + values = ( + float(entry.weight_re), + float(entry.weight_im), + float(entry.symmetry_factor), + ) + if not all(math.isfinite(value) for value in values): + raise RecurrenceColorCodecError( + f"{label} entry {index} contains a non-finite f64" + ) + if not all( + math.isfinite(float(entry.symmetry_factor) * value) + for value in (float(entry.weight_re), float(entry.weight_im)) + ): + raise RecurrenceColorCodecError( + f"{label} entry {index} overflows after symmetry folding" + ) + + +def _validate_expanded_components( + entries: Sequence[ColorContractionEntry | ColorContractionTemplateEntry], + *, + group_component_ids: Sequence[int], +) -> None: + for index, entry in enumerate(entries): + if ( + group_component_ids[_entry_left(entry)] + != group_component_ids[_entry_right(entry)] + ): + raise RecurrenceColorCodecError( + f"expanded entry {index} couples groups from different components" + ) + + +def _validate_exact_matches_f64( + entry: ColorContractionEntry | ColorContractionTemplateEntry, + factor: ExactComplexRationalV1, + index: int, +) -> None: + actual = ( + float(entry.weight_re) * float(entry.symmetry_factor), + float(entry.weight_im) * float(entry.symmetry_factor), + ) + expected = ( + factor.real_numerator / factor.real_denominator, + factor.imag_numerator / factor.imag_denominator, + ) + for component, actual_value, expected_value in zip( + ("real", "imaginary"), + actual, + expected, + strict=True, + ): + tolerance = max(math.ulp(actual_value), math.ulp(expected_value)) + if not math.isclose( + actual_value, + expected_value, + rel_tol=0.0, + abs_tol=tolerance, + ): + raise RecurrenceColorCodecError( + f"entry {index} {component} f64 coefficient disagrees with its " + "exact color factor" + ) + + +def _validate_group_coordinates( + *, + group_count: int, + sector_count: int, + component_count: int, + group_sector_ids: Sequence[int], + group_component_ids: Sequence[int], +) -> None: + if len(group_sector_ids) != group_count or len(group_component_ids) != group_count: + raise RecurrenceColorCodecError( + "group coordinate map count does not match group_count" + ) + if any(value >= sector_count for value in group_sector_ids): + raise RecurrenceColorCodecError( + "group sector map references an out-of-bounds physical sector" + ) + if any(value >= component_count for value in group_component_ids): + raise RecurrenceColorCodecError( + "group component map references an out-of-bounds resolved component" + ) + coordinates = tuple(zip(group_sector_ids, group_component_ids, strict=True)) + if len(set(coordinates)) != group_count: + raise RecurrenceColorCodecError( + "group coordinate maps contain a duplicate sector/component pair" + ) + + +def _validate_sector_owners( + *, + sector_count: int, + owner_by_sector: Sequence[int], + active_sector_ids: set[int], +) -> None: + if len(owner_by_sector) != sector_count: + raise RecurrenceColorCodecError( + "physical sector owner map count does not match sector_count" + ) + fixed_points: set[int] = set() + for sector_id, owner_id in enumerate(owner_by_sector): + if owner_id == _ZERO_SECTOR_OWNER: + continue + if owner_id >= sector_count: + raise RecurrenceColorCodecError( + f"physical sector {sector_id} has an out-of-bounds owner" + ) + if owner_id > sector_id: + raise RecurrenceColorCodecError( + f"physical sector {sector_id} does not use its minimal owner" + ) + if owner_by_sector[owner_id] != owner_id: + raise RecurrenceColorCodecError( + f"physical sector {sector_id} has a non-idempotent owner" + ) + if owner_id == sector_id: + fixed_points.add(sector_id) + if fixed_points != active_sector_ids: + raise RecurrenceColorCodecError( + "active recurrence sectors are not exactly the authenticated " + "physical-sector owners" + ) + + +def _validate_repeated_group_coordinates( + *, + ordered_group_ids: Sequence[int], + group_sector_ids: Sequence[int], + group_component_ids: Sequence[int], + sector_count: int, + component_count: int, + local_group_count: int, +) -> None: + row_sectors: list[int] = [] + for local_group_id in range(local_group_count): + start = local_group_id * component_count + row = ordered_group_ids[start : start + component_count] + sectors = {group_sector_ids[group_id] for group_id in row} + components = tuple(group_component_ids[group_id] for group_id in row) + if len(sectors) != 1 or components != tuple(range(component_count)): + raise RecurrenceColorCodecError( + "repeated local color row does not cover one physical sector " + "and every component exactly once" + ) + row_sectors.append(next(iter(sectors))) + if len(set(row_sectors)) != len(row_sectors) or any( + sector_id >= sector_count for sector_id in row_sectors + ): + raise RecurrenceColorCodecError( + "repeated local color rows do not identify unique physical sectors" + ) + + +def _encode_factorization( + factorization: FactorizedColorContractionBlock | None, + *, + local_group_count: int, + entries: Sequence[ColorContractionTemplateEntry], +) -> tuple[int, int, tuple[tuple[int, ...], ...]]: + if factorization is None: + return _FACTOR_NONE, 0, () + if factorization.kind == "klein-four-walsh": + factor_kind = _FACTOR_KLEIN_FOUR + rank = 2 + elif factorization.kind == "elementary-abelian-walsh": + factor_kind = _FACTOR_ELEMENTARY_ABELIAN + if factorization.rank is None: + raise RecurrenceColorCodecError( + "elementary-Abelian factorization is missing its rank" + ) + rank = _checked_u32("factorization rank", factorization.rank) + if rank < 3 or rank > _MAX_FACTOR_RANK: + raise RecurrenceColorCodecError( + f"elementary-Abelian factorization rank must be in " + f"[3, {_MAX_FACTOR_RANK}]" + ) + else: + raise RecurrenceColorCodecError( + f"unknown color-contraction factorization {factorization.kind!r}" + ) + subgroup_order = 1 << rank + cosets = tuple( + tuple(_checked_u32("factorization local group ID", value) for value in coset) + for coset in factorization.cosets + ) + if not cosets or any(len(coset) != subgroup_order for coset in cosets): + raise RecurrenceColorCodecError( + "factorization coset sizes are inconsistent with its rank" + ) + flattened = tuple(value for coset in cosets for value in coset) + if len(flattened) != local_group_count: + raise RecurrenceColorCodecError( + "factorization cosets do not cover every local group" + ) + if sorted(flattened) != list(range(local_group_count)): + raise RecurrenceColorCodecError( + "factorization cosets are not a partition of local groups" + ) + _validate_walsh_invariance(cosets, entries) + return factor_kind, rank, cosets + + +def _encode_symmetric_group_convolution( + block: SymmetricGroupColorContractionBlock, + *, + local_group_count: int, + entries: Sequence[ColorContractionTemplateEntry], +) -> tuple[int, int, tuple[tuple[int, ...], ...]]: + """Validate and encode canonical ``S_m`` convolution-kernel storage. + + For every channel pair ``c <= d`` and relative permutation ``r``, the + fixed-width entry IDs are ``channel[c][identity]`` and ``channel[d][r]``. + Kernel rows precede ordinary upper-triangle rows touching the consecutive + residual suffix. Zero kernel and residual coefficients remain explicit so + the payload authenticates the complete convolution/direct decomposition. + """ + + degree = _checked_u32("symmetric-group degree", block.degree) + if degree < 2 or degree > _MAX_SYMMETRIC_GROUP_DEGREE: + raise RecurrenceColorCodecError( + "symmetric-group convolution degree must be in " + f"[2, {_MAX_SYMMETRIC_GROUP_DEGREE}]" + ) + group_order = math.factorial(degree) + channels = tuple( + tuple( + _checked_u32("symmetric-group channel local group ID", value) + for value in channel + ) + for channel in block.channel_cosets + ) + if not channels: + raise RecurrenceColorCodecError( + "symmetric-group convolution requires at least one complete channel" + ) + eligible_group_count = _checked_product( + "symmetric-group eligible group count", len(channels), group_order + ) + if eligible_group_count > local_group_count: + raise RecurrenceColorCodecError( + "symmetric-group channels exceed the local group domain" + ) + flattened_channels = tuple(value for channel in channels for value in channel) + if flattened_channels != tuple(range(eligible_group_count)): + raise RecurrenceColorCodecError( + "symmetric-group channels are not canonical channel-major/" + "permutation-major local indices" + ) + if tuple(block.residual_local_group_indices) != tuple( + range(eligible_group_count, local_group_count) + ): + raise RecurrenceColorCodecError( + "symmetric-group residual groups are not the consecutive local suffix" + ) + + kernel_entry_count = _checked_product( + "symmetric-group kernel entry count", + len(channels) * (len(channels) + 1) // 2, + group_order, + ) + if kernel_entry_count != len(block.kernel_entries): + raise RecurrenceColorCodecError( + "symmetric-group kernel rows do not cover every channel pair" + ) + if tuple(entries[:kernel_entry_count]) != block.kernel_entries: + raise RecurrenceColorCodecError( + "symmetric-group kernel rows are not the leading entry section" + ) + offset = 0 + for left_channel, left_coset in enumerate(channels): + for right_channel in range(left_channel, len(channels)): + right_coset = channels[right_channel] + symmetry = 1.0 if left_channel == right_channel else 2.0 + for relative_index in range(group_order): + entry = entries[offset] + if ( + _entry_left(entry) != left_coset[0] + or _entry_right(entry) != right_coset[relative_index] + or entry.symmetry_factor != symmetry + ): + raise RecurrenceColorCodecError( + "symmetric-group kernel rows are not canonical " + "(channel, channel, relative-permutation) records" + ) + offset += 1 + if left_channel == right_channel: + kernel = block.kernel_exact_weights[offset - group_order : offset] + for relative_index, exact in enumerate(kernel): + inverse_index = _inverse_lexicographic_permutation_index( + degree, relative_index + ) + if exact != kernel[inverse_index]: + raise RecurrenceColorCodecError( + "symmetric-group diagonal kernel violates inverse " + "Hermiticity" + ) + + residual_entries = tuple(entries[kernel_entry_count:]) + if residual_entries != block.residual_entries: + raise RecurrenceColorCodecError( + "symmetric-group residual rows are not the trailing entry section" + ) + residual_pairs = tuple( + (_entry_left(entry), _entry_right(entry)) for entry in residual_entries + ) + expected_residual_pairs = ( + tuple( + (left, right) + for left in range(local_group_count) + for right in range(left, local_group_count) + if right >= eligible_group_count + ) + if block.residual_local_group_indices + else () + ) + if residual_pairs != expected_residual_pairs: + raise RecurrenceColorCodecError( + "symmetric-group residual rows do not exhaust the canonical pairs " + "touching the residual suffix" + ) + for entry in residual_entries: + if entry.weight_im != 0.0: + raise RecurrenceColorCodecError( + "symmetric-group residual row contains a complex coefficient" + ) + expected_symmetry = 1.0 if _entry_left(entry) == _entry_right(entry) else 2.0 + if entry.symmetry_factor != expected_symmetry: + raise RecurrenceColorCodecError( + "symmetric-group residual row has a noncanonical symmetry factor" + ) + return _FACTOR_SYMMETRIC_GROUP_FOURIER, degree, channels + + +def _symmetric_group_exact_coefficients( + block: SymmetricGroupColorContractionBlock, +) -> tuple[ExactComplexRationalV1, ...]: + entries = (*block.kernel_entries, *block.residual_entries) + exact_weights = (*block.kernel_exact_weights, *block.residual_exact_weights) + result = [] + for entry, exact in zip(entries, exact_weights, strict=True): + symmetry = int(entry.symmetry_factor) + if float(symmetry) != entry.symmetry_factor or symmetry not in {1, 2}: + raise RecurrenceColorCodecError( + "symmetric-group entry has a noncanonical symmetry factor" + ) + folded = exact * symmetry + result.append( + ExactComplexRationalV1( + real_numerator=folded.numerator, + real_denominator=folded.denominator, + ) + ) + return tuple(result) + + +def _inverse_lexicographic_permutation_index(degree: int, index: int) -> int: + """Return the lexicographic rank of a ranked permutation's inverse.""" + + remaining = list(range(degree)) + permutation: list[int] = [] + rank = index + for width in range(degree, 0, -1): + stride = math.factorial(width - 1) + quotient, rank = divmod(rank, stride) + permutation.append(remaining.pop(quotient)) + inverse = [0] * degree + for position, value in enumerate(permutation): + inverse[value] = position + remaining = list(range(degree)) + inverse_rank = 0 + for position, value in enumerate(inverse): + offset = remaining.index(value) + inverse_rank += offset * math.factorial(degree - position - 1) + remaining.pop(offset) + return inverse_rank + + +def _validate_walsh_invariance( + cosets: Sequence[Sequence[int]], + entries: Sequence[ColorContractionTemplateEntry], +) -> None: + matrix: dict[tuple[int, int], float] = {} + for index, entry in enumerate(entries): + if entry.weight_im != 0.0: + raise RecurrenceColorCodecError( + "factorized color contraction requires real weights" + ) + left = entry.left_group_index + right = entry.right_group_index + coefficient = float(entry.symmetry_factor) * float(entry.weight_re) + if left != right: + coefficient *= 0.5 + if not math.isfinite(coefficient): + raise RecurrenceColorCodecError( + f"factorized entry {index} has a non-finite matrix coefficient" + ) + matrix[(left, right)] = coefficient + + def value(left: int, right: int) -> float: + return matrix.get((min(left, right), max(left, right)), 0.0) + + subgroup_order = len(cosets[0]) + for left_coset in cosets: + for right_coset in cosets: + for left_index in range(subgroup_order): + for right_index in range(subgroup_order): + if value(left_coset[left_index], right_coset[right_index]) != value( + left_coset[0], + right_coset[left_index ^ right_index], + ): + raise RecurrenceColorCodecError( + "factorization cosets are inconsistent with the " + "canonical color matrix" + ) + + +def _checked_u32(label: str, value: int) -> int: + if isinstance(value, bool) or not isinstance(value, int): + raise RecurrenceColorCodecError(f"{label} must be an integer") + if value < 0 or value > 0xFFFF_FFFF: + raise RecurrenceColorCodecError(f"{label} exceeds the u32 domain") + return value + + +def _checked_u64(label: str, value: int) -> int: + if isinstance(value, bool) or not isinstance(value, int): + raise RecurrenceColorCodecError(f"{label} must be an integer") + if value < 0 or value > 0xFFFF_FFFF_FFFF_FFFF: + raise RecurrenceColorCodecError(f"{label} exceeds the u64 domain") + return value + + +def _checked_product(label: str, left: int, right: int) -> int: + result = left * right + return _checked_u64(label, result) + + +def _checked_payload_size( + *, + entry_count: int, + exact_factor_count: int, + group_count: int, + sector_count: int, + coset_index_count: int, +) -> int: + size = ( + entry_count * _ENTRY.size + + exact_factor_count * _EXACT_FACTOR_BYTES + + 2 * group_count * _U32.size + + 2 * group_count * _U32.size + + sector_count * _U32.size + + coset_index_count * _U32.size + ) + return _checked_u64("recurrence color payload byte count", size) + + +def _pack_exact_factor( + output: bytearray, + offset: int, + factor: ExactComplexRationalV1, +) -> int: + for value in factor.canonical_key: + if value < -(1 << 127) or value >= 1 << 127: + raise RecurrenceColorCodecError( + "exact color coefficient exceeds the signed i128 domain" + ) + output[offset : offset + 16] = value.to_bytes( + 16, + byteorder="little", + signed=True, + ) + offset += 16 + return offset + + +def recurrence_color_contraction_digest(payload: bytes) -> str: + """Return the caller-owned deterministic SHA-256 payload digest.""" + + return sha256(payload).hexdigest() + + +__all__ = [ + "RECURRENCE_COLOR_CONTRACTION_CODEC_ABI", + "RECURRENCE_COLOR_CONTRACTION_MAGIC", + "RecurrenceColorCodecError", + "encode_recurrence_color_contraction", + "recurrence_color_contraction_digest", +] diff --git a/pyamplicol/src/pyamplicol/generation/recurrence_columnar.py b/pyamplicol/src/pyamplicol/generation/recurrence_columnar.py new file mode 100644 index 0000000000..cb22c6010f --- /dev/null +++ b/pyamplicol/src/pyamplicol/generation/recurrence_columnar.py @@ -0,0 +1,2795 @@ +# SPDX-License-Identifier: 0BSD +"""Columnar Python-to-Rust contract for recurrence construction. + +This module owns only the compact, model-generic input boundary described by +``pyamplicol-recurrence-builder-input-v2``. Callers provide immutable logical +records produced after process expansion and LC colour planning. The builder +normalises those records into deterministic, owned NumPy columns; it neither +imports nor constructs the process ``GenericDAG`` representation. +""" + +from __future__ import annotations + +import hashlib +import json +import math +from collections.abc import Iterable, Mapping, Sequence +from dataclasses import dataclass, field +from typing import Any, Final, Literal + +import numpy as np + +from .recurrence_fermion_pairing import FermionPairingCatalogV1 + +RECURRENCE_BUILDER_INPUT_ABI: Final = "pyamplicol-recurrence-builder-input-v2" +RECURRENCE_BUILDER_INPUT_SCHEMA_VERSION: Final = 1 +MISSING_U32: Final = (1 << 32) - 1 +_DYNAMIC_UNION_SOURCE_SPIN_STATE: Final = -(1 << 31) + +RecurrenceLCFlowLayout = Literal[ + "topology-replay", + "all-flow-union", + "contracted-color-union", +] +LCColorSectorKind = Literal["singlet", "open-lines", "single-trace"] + +_LAYOUT_CODES: Final = { + "topology-replay": 0, + "all-flow-union": 1, + "contracted-color-union": 2, +} +_SECTOR_KIND_CODES: Final = {"singlet": 0, "open-lines": 1, "single-trace": 2} +_REQUIRED_HEADER_DIGEST_ROLES: Final = frozenset( + {"process", "model-catalog", "prepared-catalog", "color-plan"} +) + +_U8 = np.dtype("u1") +_U32 = np.dtype(" None: + _validate_fraction( + self.real_numerator, + self.real_denominator, + "real recurrence factor", + ) + _validate_fraction( + self.imag_numerator, + self.imag_denominator, + "imaginary recurrence factor", + ) + + @property + def canonical_key(self) -> tuple[int, int, int, int]: + return ( + self.real_numerator, + self.real_denominator, + self.imag_numerator, + self.imag_denominator, + ) + + +@dataclass(frozen=True, slots=True) +class RecurrenceSemanticDigestV1: + """One named semantic SHA-256 bound by the builder header.""" + + role: str + digest: str + + def __post_init__(self) -> None: + _nonempty_text(self.role, "semantic digest role") + _sha256_bytes(self.digest, f"semantic digest {self.role!r}") + + +@dataclass(frozen=True, slots=True) +class RecurrenceSourceStateV1: + """One public source state retained for an external leg.""" + + state_index: int + public_helicity: int + chirality: int + spin_state: int + current_state_template_id: int + source_template_id: int + momentum_sign: int = 1 + crossing_phase: ExactComplexRationalV1 = field( + default_factory=lambda: ExactComplexRationalV1(1) + ) + + def __post_init__(self) -> None: + _checked_u32(self.state_index, "source-state index") + _checked_i32(self.public_helicity, "public source helicity") + _checked_i32(self.chirality, "source chirality") + _checked_concrete_spin_state(self.spin_state, "source spin state") + _checked_u32( + self.current_state_template_id, + "source current-state template ID", + ) + _checked_u32(self.source_template_id, "source template ID") + if self.momentum_sign not in {-1, 1}: + raise RecurrenceColumnarInputError("source momentum sign must be -1 or 1") + if not isinstance(self.crossing_phase, ExactComplexRationalV1): + raise RecurrenceColumnarInputError( + "source crossing phase must be an exact complex rational" + ) + + +@dataclass(frozen=True, slots=True) +class RecurrenceExternalLegV1: + """External process leg and complete generated source-state coverage.""" + + source_slot: int + public_label: int + physical_pdg: int + outgoing_pdg: int + is_initial: bool + is_fermionic: bool + source_states: tuple[RecurrenceSourceStateV1, ...] + momentum_mask: int + support_mask: int + + def __post_init__(self) -> None: + _checked_u32(self.source_slot, "external source slot") + _checked_u32(self.public_label, "external public label") + _checked_i32(self.physical_pdg, "physical PDG") + _checked_i32(self.outgoing_pdg, "outgoing PDG") + if type(self.is_initial) is not bool: + raise RecurrenceColumnarInputError( + "external initial-state marker must be boolean" + ) + if type(self.is_fermionic) is not bool: + raise RecurrenceColumnarInputError( + "external fermionic-state marker must be boolean" + ) + _validate_nonnegative_mask(self.momentum_mask, "external momentum mask") + _validate_nonnegative_mask(self.support_mask, "external support mask") + if not self.source_states: + raise RecurrenceColumnarInputError( + "every external leg must retain at least one source state" + ) + indices = tuple(state.state_index for state in self.source_states) + if indices != tuple(range(len(indices))): + raise RecurrenceColumnarInputError( + "source-state indices must be dense and ordered from zero" + ) + helicities = tuple(state.public_helicity for state in self.source_states) + if len(set(helicities)) != len(helicities): + raise RecurrenceColumnarInputError( + "one external leg cannot expose a public helicity more than once" + ) + source_template_ids = tuple( + state.source_template_id for state in self.source_states + ) + if len(set(source_template_ids)) != len(source_template_ids): + raise RecurrenceColumnarInputError( + "one external leg cannot expose a source template more than once" + ) + + +@dataclass(frozen=True, slots=True) +class RecurrenceLCOpenStringV1: + """One ordered open fundamental colour string, using source slots.""" + + fundamental_source_slot: int + antifundamental_source_slot: int + adjoint_source_slots: tuple[int, ...] = () + singlet_source_slots: tuple[int, ...] = () + + def __post_init__(self) -> None: + _checked_u32(self.fundamental_source_slot, "fundamental source slot") + _checked_u32( + self.antifundamental_source_slot, + "antifundamental source slot", + ) + _validate_u32_sequence(self.adjoint_source_slots, "adjoint source slots") + _validate_u32_sequence(self.singlet_source_slots, "singlet source slots") + + +@dataclass(frozen=True, slots=True) +class RecurrencePhysicalLCSectorV1: + """One physical LC sector before recurrence state construction.""" + + sector_id: int + public_id: str + kind: LCColorSectorKind + closure_source_slot: int + closure_proof_algorithm: str + closure_proof_digest: str + open_strings: tuple[RecurrenceLCOpenStringV1, ...] = () + trace_source_slots: tuple[int, ...] = () + singlet_source_slots: tuple[int, ...] = () + word_source_slots: tuple[int, ...] = () + support_mask: int = 0 + + def __post_init__(self) -> None: + _checked_u32(self.sector_id, "physical LC sector ID") + _nonempty_text(self.public_id, "physical LC sector public ID") + if self.kind not in _SECTOR_KIND_CODES: + raise RecurrenceColumnarInputError( + f"unsupported physical LC sector kind {self.kind!r}" + ) + _checked_u32(self.closure_source_slot, "LC closure source slot") + _nonempty_text(self.closure_proof_algorithm, "LC closure proof algorithm") + _sha256_bytes(self.closure_proof_digest, "LC closure proof digest") + _validate_u32_sequence(self.trace_source_slots, "trace source slots") + _validate_u32_sequence(self.singlet_source_slots, "singlet source slots") + _validate_u32_sequence(self.word_source_slots, "colour-word source slots") + _validate_nonnegative_mask(self.support_mask, "LC sector support mask") + if self.kind == "open-lines" and not self.open_strings: + raise RecurrenceColumnarInputError( + "an open-lines LC sector must contain at least one open string" + ) + if self.kind != "open-lines" and self.open_strings: + raise RecurrenceColumnarInputError( + f"an LC sector of kind {self.kind!r} cannot contain open strings" + ) + if self.kind == "single-trace" and not self.trace_source_slots: + raise RecurrenceColumnarInputError( + "a single-trace LC sector must contain a trace word" + ) + if self.kind != "single-trace" and self.trace_source_slots: + raise RecurrenceColumnarInputError( + f"an LC sector of kind {self.kind!r} cannot contain a trace word" + ) + + +@dataclass(frozen=True, slots=True) +class RecurrencePublicLCFlowV1: + """One runtime-visible flow mapped onto a construction sector. + + ``source_slot_permutation`` is a gather mapping: construction slot ``i`` + receives the public-flow source at ``source_slot_permutation[i]``. + """ + + flow_id: int + public_id: str + construction_sector_id: int + word_source_slots: tuple[int, ...] + source_slot_permutation: tuple[int, ...] + reduction_weight: ExactComplexRationalV1 = field( + default_factory=lambda: ExactComplexRationalV1(1) + ) + + def __post_init__(self) -> None: + _checked_u32(self.flow_id, "public LC flow ID") + _nonempty_text(self.public_id, "public LC flow identifier") + _checked_u32(self.construction_sector_id, "construction LC sector ID") + _validate_u32_sequence(self.word_source_slots, "public LC flow word") + _validate_u32_sequence( + self.source_slot_permutation, + "public LC flow source-slot permutation", + ) + if not isinstance(self.reduction_weight, ExactComplexRationalV1): + raise RecurrenceColumnarInputError( + "public LC flow weight must be an exact complex rational" + ) + + +@dataclass(frozen=True, slots=True) +class RecurrenceReplayTargetV1: + """Exact mapping from a replay representative to one physical sector. + + Both permutations are gather mappings: representative slot ``i`` receives + target slot ``permutation[i]``. They are equal in recurrence ABI v2; both + names are retained to keep the proof and runtime contracts explicit. + """ + + sector_id: int + external_permutation: tuple[int, ...] + source_slot_permutation: tuple[int, ...] + amplitude_phase: ExactComplexRationalV1 = field( + default_factory=lambda: ExactComplexRationalV1(1) + ) + fermion_sign: int = 1 + + def __post_init__(self) -> None: + _checked_u32(self.sector_id, "replay target sector ID") + _validate_u32_sequence( + self.external_permutation, + "replay external permutation", + ) + _validate_u32_sequence( + self.source_slot_permutation, + "replay source-slot permutation", + ) + if not isinstance(self.amplitude_phase, ExactComplexRationalV1): + raise RecurrenceColumnarInputError( + "replay amplitude phase must be an exact complex rational" + ) + if self.fermion_sign not in {-1, 1}: + raise RecurrenceColumnarInputError("replay fermion sign must be -1 or 1") + + +@dataclass(frozen=True, slots=True) +class RecurrenceReplayPartitionV1: + """One exactly certified physical-flow replay partition.""" + + representative_sector_id: int + materialized_sector_id: int + proof_algorithm: str + proof_digest: str + targets: tuple[RecurrenceReplayTargetV1, ...] + + def __post_init__(self) -> None: + _checked_u32(self.representative_sector_id, "replay representative sector") + _checked_u32(self.materialized_sector_id, "replay materialized sector") + _nonempty_text(self.proof_algorithm, "replay proof algorithm") + _sha256_bytes(self.proof_digest, "replay proof digest") + if not self.targets: + raise RecurrenceColumnarInputError( + "a replay partition must contain at least one target" + ) + target_ids = tuple(target.sector_id for target in self.targets) + if len(set(target_ids)) != len(target_ids): + raise RecurrenceColumnarInputError( + "a replay partition contains duplicate target sectors" + ) + if self.representative_sector_id not in target_ids: + raise RecurrenceColumnarInputError( + "a replay partition must contain its representative sector" + ) + + +@dataclass(frozen=True, slots=True) +class RecurrenceSelectedSourceCoverageV1: + """Generation-retained local source states for one external leg.""" + + source_slot: int + source_state_indices: tuple[int, ...] + + def __post_init__(self) -> None: + _checked_u32(self.source_slot, "selected source slot") + _validate_u32_sequence( + self.source_state_indices, + "selected source-state indices", + ) + if not self.source_state_indices: + raise RecurrenceColumnarInputError( + "selected source coverage cannot be empty" + ) + if len(set(self.source_state_indices)) != len(self.source_state_indices): + raise RecurrenceColumnarInputError( + "selected source coverage contains duplicate states" + ) + + +@dataclass(frozen=True, slots=True) +class RecurrenceCouplingLimitV1: + """Resolved inclusive bounds for one coupling-order axis.""" + + name: str + minimum: int + maximum: int + + def __post_init__(self) -> None: + _nonempty_text(self.name, "coupling-order name") + minimum = _checked_u32(self.minimum, f"coupling-order {self.name} minimum") + maximum = _checked_u32(self.maximum, f"coupling-order {self.name} maximum") + if minimum > maximum: + raise RecurrenceColumnarInputError( + f"coupling-order {self.name!r} minimum exceeds its maximum" + ) + + +@dataclass(frozen=True, slots=True) +class RecurrenceSemanticTemplateReferenceV1: + """Content-addressed recurrence template and optional callable binding.""" + + kind: str + template_id: int + semantic_digest: str + prepared_kernel_id: int | None = None + + def __post_init__(self) -> None: + _nonempty_text(self.kind, "semantic-template kind") + _checked_u32(self.template_id, "semantic-template ID") + _sha256_bytes(self.semantic_digest, "semantic-template digest") + if self.prepared_kernel_id is not None: + if self.prepared_kernel_id == MISSING_U32: + raise RecurrenceColumnarInputError( + "prepared-kernel ID reserves the u32 sentinel" + ) + _checked_u32(self.prepared_kernel_id, "prepared-kernel ID") + + +@dataclass(frozen=True, slots=True) +class RecurrenceNormalizationV1: + """Exact process normalization and its semantic convention.""" + + factor: ExactComplexRationalV1 + convention: str + semantic_digest: str + + def __post_init__(self) -> None: + _nonempty_text(self.convention, "normalization convention") + _sha256_bytes(self.semantic_digest, "normalization semantic digest") + + +@dataclass(frozen=True, slots=True) +class RecurrenceParameterProjectionV1: + """Map one public runtime parameter slot to a prepared-model slot.""" + + runtime_slot: int + runtime_name: str + parameter_template_id: int + prepared_parameter_id: int | None + component: int = 0 + + def __post_init__(self) -> None: + _checked_u32(self.runtime_slot, "runtime parameter slot") + _nonempty_text(self.runtime_name, "runtime parameter name") + _checked_u32(self.parameter_template_id, "parameter-template ID") + if self.prepared_parameter_id is not None: + if self.prepared_parameter_id == MISSING_U32: + raise RecurrenceColumnarInputError( + "prepared parameter ID reserves the u32 sentinel" + ) + _checked_u32(self.prepared_parameter_id, "prepared parameter ID") + _checked_u32(self.component, "parameter component") + + +@dataclass(frozen=True, slots=True) +class RecurrenceBuilderLogicalInputV1: + """Compact logical records accepted by the recurrence column encoder.""" + + process_id: str + layout: RecurrenceLCFlowLayout + semantic_digests: tuple[RecurrenceSemanticDigestV1, ...] + external_legs: tuple[RecurrenceExternalLegV1, ...] + physical_sectors: tuple[RecurrencePhysicalLCSectorV1, ...] + public_flows: tuple[RecurrencePublicLCFlowV1, ...] + semantic_template_references: tuple[RecurrenceSemanticTemplateReferenceV1, ...] + normalization: RecurrenceNormalizationV1 + fermion_pairing_catalog: FermionPairingCatalogV1 | None = None + replay_partitions: tuple[RecurrenceReplayPartitionV1, ...] = () + selected_public_flow_ids: tuple[int, ...] | None = None + selected_source_coverage: tuple[RecurrenceSelectedSourceCoverageV1, ...] | None = ( + None + ) + coupling_limits: tuple[RecurrenceCouplingLimitV1, ...] = () + parameter_projection: tuple[RecurrenceParameterProjectionV1, ...] = () + process_support_mask: int = 0 + + def __post_init__(self) -> None: + _nonempty_text(self.process_id, "recurrence process ID") + if self.layout not in _LAYOUT_CODES: + raise RecurrenceColumnarInputError( + f"unsupported recurrence LC flow layout {self.layout!r}" + ) + _validate_nonnegative_mask(self.process_support_mask, "process support mask") + if self.fermion_pairing_catalog is not None and not isinstance( + self.fermion_pairing_catalog, + FermionPairingCatalogV1, + ): + raise TypeError( + "recurrence fermion pairing must be FermionPairingCatalogV1" + ) + + +@dataclass(frozen=True, slots=True) +class RecurrenceColumn: + """One owned, immutable primitive recurrence column.""" + + name: str + values: np.ndarray[Any, Any] + + def __post_init__(self) -> None: + _nonempty_text(self.name, "column name") + values = self.values + if not isinstance(values, np.ndarray): + raise TypeError(f"column {self.name!r} must be a NumPy array") + if values.ndim < 1: + raise RecurrenceColumnarInputError( + f"column {self.name!r} must have a row dimension" + ) + if not values.flags.c_contiguous: + raise RecurrenceColumnarInputError( + f"column {self.name!r} must be C-contiguous" + ) + if not values.flags.owndata: + raise RecurrenceColumnarInputError( + f"column {self.name!r} must own its storage" + ) + if values.flags.writeable: + raise RecurrenceColumnarInputError( + f"column {self.name!r} must be read-only" + ) + if values.dtype.str not in _ALLOWED_DTYPES: + raise RecurrenceColumnarInputError( + f"column {self.name!r} has unsupported or non-little-endian " + f"dtype {values.dtype.str!r}" + ) + + +@dataclass(frozen=True, slots=True) +class RecurrenceColumnarTable: + """One deterministic structure-of-arrays recurrence table.""" + + name: str + row_count: int + columns: tuple[RecurrenceColumn, ...] + + def __post_init__(self) -> None: + _nonempty_text(self.name, "table name") + _checked_u64(self.row_count, f"{self.name}.row_count") + names = tuple(column.name for column in self.columns) + if len(set(names)) != len(names): + raise RecurrenceColumnarInputError( + f"table {self.name!r} contains duplicate column names" + ) + for column in self.columns: + if len(column.values) != self.row_count: + raise RecurrenceColumnarInputError( + f"table {self.name!r} column {column.name!r} has " + f"{len(column.values)} rows, expected {self.row_count}" + ) + + def column(self, name: str) -> np.ndarray[Any, Any]: + for column in self.columns: + if column.name == name: + return column.values + raise KeyError(f"table {self.name!r} has no column {name!r}") + + +_ColumnSpec = tuple[str, tuple[int, ...]] + +_TABLE_SCHEMAS: Final[dict[str, tuple[_ColumnSpec, ...]]] = { + "bitset_ranges": ( + ("id", ()), + ("start", ()), + ("count", ()), + ("bit_count", ()), + ), + "bitset_words": (("value", ()),), + "coupling_limits": ( + ("name_string_id", ()), + ("minimum", ()), + ("maximum", ()), + ), + "digest_catalog": (("id", ()), ("value", (32,))), + "exact_factors": ( + ("id", ()), + ("real_numerator_string_id", ()), + ("real_denominator_string_id", ()), + ("imag_numerator_string_id", ()), + ("imag_denominator_string_id", ()), + ), + "external_legs": ( + ("source_slot", ()), + ("public_label", ()), + ("physical_pdg", ()), + ("outgoing_pdg", ()), + ("is_initial", ()), + ("is_fermionic", ()), + ("source_state_start", ()), + ("source_state_count", ()), + ("momentum_mask_id", ()), + ("support_mask_id", ()), + ), + "header": ( + ("schema_version", ()), + ("abi_string_id", ()), + ("process_id_string_id", ()), + ("layout", ()), + ("selected_flow_mode", ()), + ("selected_source_mode", ()), + ("external_leg_count", ()), + ("physical_sector_count", ()), + ("public_flow_count", ()), + ("replay_partition_count", ()), + ("coupling_limit_count", ()), + ("parameter_projection_count", ()), + ("process_support_mask_id", ()), + ), + "header_digests": (("role_string_id", ()), ("digest_id", ())), + "lc_open_strings": ( + ("sector_id", ()), + ("ordinal", ()), + ("fundamental_source_slot", ()), + ("antifundamental_source_slot", ()), + ("adjoint_sequence_id", ()), + ("singlet_sequence_id", ()), + ), + "normalization": ( + ("factor_id", ()), + ("convention_string_id", ()), + ("semantic_digest_id", ()), + ), + "parameter_projection": ( + ("runtime_slot", ()), + ("runtime_name_string_id", ()), + ("parameter_template_id", ()), + ("prepared_parameter_id", ()), + ("component", ()), + ), + "physical_lc_sectors": ( + ("sector_id", ()), + ("public_id_string_id", ()), + ("kind", ()), + ("closure_source_slot", ()), + ("closure_proof_algorithm_string_id", ()), + ("closure_proof_digest_id", ()), + ("open_string_start", ()), + ("open_string_count", ()), + ("trace_sequence_id", ()), + ("singlet_sequence_id", ()), + ("word_sequence_id", ()), + ("support_mask_id", ()), + ), + "public_lc_flows": ( + ("flow_id", ()), + ("public_id_string_id", ()), + ("construction_sector_id", ()), + ("word_sequence_id", ()), + ("source_slot_permutation_sequence_id", ()), + ("reduction_weight_factor_id", ()), + ), + "replay_partitions": ( + ("partition_id", ()), + ("representative_sector_id", ()), + ("materialized_sector_id", ()), + ("target_start", ()), + ("target_count", ()), + ("proof_algorithm_string_id", ()), + ("proof_digest_id", ()), + ), + "replay_targets": ( + ("partition_id", ()), + ("sector_id", ()), + ("external_permutation_sequence_id", ()), + ("source_slot_permutation_sequence_id", ()), + ("amplitude_phase_factor_id", ()), + ("fermion_sign", ()), + ), + "selected_public_flow_coverage": (("flow_id", ()),), + "selected_source_coverage": ( + ("source_slot", ()), + ("source_state_index", ()), + ), + "semantic_template_references": ( + ("kind_string_id", ()), + ("template_id", ()), + ("semantic_digest_id", ()), + ("prepared_kernel_id", ()), + ), + "source_states": ( + ("source_slot", ()), + ("state_index", ()), + ("public_helicity", ()), + ("chirality", ()), + ("spin_state", ()), + ("current_state_template_id", ()), + ("source_template_id", ()), + ("momentum_sign", ()), + ("crossing_phase_factor_id", ()), + ), + "string_bytes": (("value", ()),), + "string_ranges": (("start", ()), ("count", ())), + "u32_sequence_ranges": (("start", ()), ("count", ())), + "u32_sequence_values": (("value", ()),), +} + +_U8_COLUMN_KEYS: Final = frozenset( + { + ("digest_catalog", "value"), + ("header", "layout"), + ("header", "selected_flow_mode"), + ("header", "selected_source_mode"), + ("external_legs", "is_initial"), + ("external_legs", "is_fermionic"), + ("physical_lc_sectors", "kind"), + ("string_bytes", "value"), + } +) +_U64_COLUMN_KEYS: Final = frozenset( + { + ("bitset_ranges", "start"), + ("bitset_ranges", "count"), + ("bitset_ranges", "bit_count"), + ("bitset_words", "value"), + ("external_legs", "source_state_start"), + ("external_legs", "source_state_count"), + ("physical_lc_sectors", "open_string_start"), + ("physical_lc_sectors", "open_string_count"), + ("replay_partitions", "target_start"), + ("replay_partitions", "target_count"), + ("string_ranges", "start"), + ("string_ranges", "count"), + ("u32_sequence_ranges", "start"), + ("u32_sequence_ranges", "count"), + } +) +_I32_COLUMN_KEYS: Final = frozenset( + { + ("external_legs", "physical_pdg"), + ("external_legs", "outgoing_pdg"), + ("replay_targets", "fermion_sign"), + ("source_states", "public_helicity"), + ("source_states", "chirality"), + ("source_states", "spin_state"), + ("source_states", "momentum_sign"), + } +) + + +@dataclass(frozen=True, slots=True) +class RecurrenceBuilderInputV1: + """Validated immutable payload passed to the future Rust builder.""" + + abi: str + tables: tuple[RecurrenceColumnarTable, ...] + fermion_pairing_tables: tuple[RecurrenceColumnarTable, ...] = () + + def __post_init__(self) -> None: + if self.abi != RECURRENCE_BUILDER_INPUT_ABI: + raise RecurrenceColumnarInputError( + f"unsupported recurrence builder input ABI {self.abi!r}" + ) + names = tuple(table.name for table in self.tables) + expected_names = tuple(sorted(_TABLE_SCHEMAS)) + if names != expected_names: + raise RecurrenceColumnarInputError( + "recurrence columnar tables must match the sorted v2 inventory; " + f"got {names!r}" + ) + self._validate_schemas() + self._validate_catalogs() + self._validate_references() + self._validate_fermion_pairing() + + def table(self, name: str) -> RecurrenceColumnarTable: + for table in self.tables: + if table.name == name: + return table + raise KeyError(f"recurrence builder input has no table {name!r}") + + @property + def layout(self) -> RecurrenceLCFlowLayout: + code = int(self.table("header").column("layout")[0]) + for layout, candidate in _LAYOUT_CODES.items(): + if candidate == code: + return layout + raise RecurrenceColumnarInputError( + f"recurrence header contains unknown layout code {code}" + ) + + def fermion_pairing_table(self, name: str) -> RecurrenceColumnarTable: + """Return one table from the private process-owned pairing payload.""" + + for table in self.fermion_pairing_tables: + if table.name == name: + return table + raise KeyError(f"recurrence fermion pairing has no table {name!r}") + + @property + def fermion_pairing_digest(self) -> str | None: + """Digest the exact private table representation for a future decoder.""" + + if not self.fermion_pairing_tables: + return None + from .recurrence_pairing_columnar import _PAIRING_COLUMNAR_ABI + + digest = hashlib.sha256() + _hash_text(digest, _PAIRING_COLUMNAR_ABI) + _hash_columnar_tables(digest, self.fermion_pairing_tables) + return digest.hexdigest() + + @property + def canonical_digest(self) -> str: + """SHA-256 over ABI, table schemas, shapes, and canonical bytes.""" + + digest = hashlib.sha256() + _hash_text(digest, self.abi) + _hash_columnar_tables(digest, self.tables) + return digest.hexdigest() + + @property + def digest(self) -> str: + """Compatibility spelling for consumers passing the digest to Rust.""" + + return self.canonical_digest + + def require_digest(self, expected: str) -> None: + """Fail before GIL release when a supplied input digest is stale.""" + + _sha256_bytes(expected, "expected recurrence input digest") + if self.canonical_digest != expected: + raise RecurrenceColumnarInputError( + "recurrence builder input canonical digest does not match" + ) + + def _validate_schemas(self) -> None: + for table in self.tables: + expected = _TABLE_SCHEMAS[table.name] + names = tuple(column.name for column in table.columns) + expected_names = tuple(name for name, _shape in expected) + if names != expected_names: + raise RecurrenceColumnarInputError( + f"table {table.name!r} columns do not match ABI v1: {names!r}" + ) + for column, (_name, tail_shape) in zip( + table.columns, + expected, + strict=True, + ): + expected_dtype = _column_dtype(table.name, column.name) + if column.values.dtype.str != expected_dtype.str: + raise RecurrenceColumnarInputError( + f"table {table.name!r} column {column.name!r} has dtype " + f"{column.values.dtype.str!r}, expected " + f"{expected_dtype.str!r}" + ) + if column.values.shape[1:] != tail_shape: + raise RecurrenceColumnarInputError( + f"table {table.name!r} column {column.name!r} has tail " + f"shape {column.values.shape[1:]!r}, expected {tail_shape!r}" + ) + + def _validate_catalogs(self) -> None: + strings = _decode_flat_strings( + self.table("string_ranges"), + self.table("string_bytes"), + ) + if strings != tuple(sorted(strings, key=lambda value: value.encode("utf-8"))): + raise RecurrenceColumnarInputError( + "flat string catalog is not in canonical byte order" + ) + if len(set(strings)) != len(strings): + raise RecurrenceColumnarInputError( + "flat string catalog contains duplicates" + ) + + _validate_flat_ranges( + self.table("u32_sequence_ranges"), + self.table("u32_sequence_values").row_count, + "u32 sequence catalog", + ) + _validate_flat_ranges( + self.table("bitset_ranges"), + self.table("bitset_words").row_count, + "bitset catalog", + ) + _validate_dense_ids(self.table("bitset_ranges"), "id") + _validate_dense_ids(self.table("digest_catalog"), "id") + _validate_dense_ids(self.table("exact_factors"), "id") + + words = self.table("bitset_words").column("value") + ranges = self.table("bitset_ranges") + for row in range(ranges.row_count): + start = int(ranges.column("start")[row]) + count = int(ranges.column("count")[row]) + if count and int(words[start + count - 1]) == 0: + raise RecurrenceColumnarInputError( + "bitset catalog contains a noncanonical leading zero word" + ) + value = sum( + int(words[start + offset]) << (64 * offset) for offset in range(count) + ) + if value.bit_count() != int(ranges.column("bit_count")[row]): + raise RecurrenceColumnarInputError( + "bitset catalog bit count does not match its words" + ) + + def _validate_references(self) -> None: + string_count = self.table("string_ranges").row_count + digest_count = self.table("digest_catalog").row_count + sequence_count = self.table("u32_sequence_ranges").row_count + bitset_count = self.table("bitset_ranges").row_count + factor_count = self.table("exact_factors").row_count + external_count = self.table("external_legs").row_count + sector_count = self.table("physical_lc_sectors").row_count + public_flow_count = self.table("public_lc_flows").row_count + partition_count = self.table("replay_partitions").row_count + + _validate_dense_ids(self.table("external_legs"), "source_slot") + _validate_dense_ids(self.table("physical_lc_sectors"), "sector_id") + _validate_dense_ids(self.table("public_lc_flows"), "flow_id") + _validate_dense_ids(self.table("replay_partitions"), "partition_id") + _validate_dense_ids(self.table("parameter_projection"), "runtime_slot") + + for table, columns in ( + (self.table("header"), ("abi_string_id", "process_id_string_id")), + (self.table("header_digests"), ("role_string_id",)), + (self.table("coupling_limits"), ("name_string_id",)), + ( + self.table("exact_factors"), + ( + "real_numerator_string_id", + "real_denominator_string_id", + "imag_numerator_string_id", + "imag_denominator_string_id", + ), + ), + ( + self.table("physical_lc_sectors"), + ("public_id_string_id", "closure_proof_algorithm_string_id"), + ), + (self.table("public_lc_flows"), ("public_id_string_id",)), + (self.table("replay_partitions"), ("proof_algorithm_string_id",)), + (self.table("semantic_template_references"), ("kind_string_id",)), + (self.table("normalization"), ("convention_string_id",)), + (self.table("parameter_projection"), ("runtime_name_string_id",)), + ): + _validate_bounded_columns(table, columns, string_count) + + for table, columns in ( + (self.table("header_digests"), ("digest_id",)), + (self.table("physical_lc_sectors"), ("closure_proof_digest_id",)), + (self.table("replay_partitions"), ("proof_digest_id",)), + (self.table("semantic_template_references"), ("semantic_digest_id",)), + (self.table("normalization"), ("semantic_digest_id",)), + ): + _validate_bounded_columns(table, columns, digest_count) + + for table, columns in ( + ( + self.table("lc_open_strings"), + ( + "adjoint_sequence_id", + "singlet_sequence_id", + ), + ), + ( + self.table("physical_lc_sectors"), + ( + "trace_sequence_id", + "singlet_sequence_id", + "word_sequence_id", + ), + ), + ( + self.table("replay_targets"), + ( + "external_permutation_sequence_id", + "source_slot_permutation_sequence_id", + ), + ), + ( + self.table("public_lc_flows"), + ("word_sequence_id", "source_slot_permutation_sequence_id"), + ), + ): + _validate_bounded_columns(table, columns, sequence_count) + + for table, columns in ( + (self.table("header"), ("process_support_mask_id",)), + (self.table("external_legs"), ("momentum_mask_id", "support_mask_id")), + (self.table("physical_lc_sectors"), ("support_mask_id",)), + ): + _validate_bounded_columns(table, columns, bitset_count) + + _validate_bounded_columns( + self.table("replay_targets"), + ("amplitude_phase_factor_id",), + factor_count, + ) + _validate_bounded_columns( + self.table("public_lc_flows"), + ("reduction_weight_factor_id",), + factor_count, + ) + _validate_bounded_columns( + self.table("source_states"), + ("crossing_phase_factor_id",), + factor_count, + ) + _validate_bounded_columns( + self.table("normalization"), + ("factor_id",), + factor_count, + ) + + for table, columns in ( + ( + self.table("lc_open_strings"), + ( + "fundamental_source_slot", + "antifundamental_source_slot", + ), + ), + (self.table("source_states"), ("source_slot",)), + (self.table("selected_source_coverage"), ("source_slot",)), + (self.table("physical_lc_sectors"), ("closure_source_slot",)), + ): + _validate_bounded_columns(table, columns, external_count) + + for table, columns in ( + (self.table("lc_open_strings"), ("sector_id",)), + ( + self.table("replay_partitions"), + ( + "representative_sector_id", + "materialized_sector_id", + ), + ), + (self.table("replay_targets"), ("sector_id",)), + (self.table("public_lc_flows"), ("construction_sector_id",)), + ): + _validate_bounded_columns(table, columns, sector_count) + _validate_bounded_columns( + self.table("selected_public_flow_coverage"), + ("flow_id",), + public_flow_count, + ) + _validate_bounded_columns( + self.table("replay_targets"), + ("partition_id",), + partition_count, + ) + + _validate_parent_ranges( + self.table("external_legs"), + "source_state_start", + "source_state_count", + self.table("source_states").row_count, + "external source-state", + ) + _validate_parent_ranges( + self.table("physical_lc_sectors"), + "open_string_start", + "open_string_count", + self.table("lc_open_strings").row_count, + "LC open-string", + ) + _validate_parent_ranges( + self.table("replay_partitions"), + "target_start", + "target_count", + self.table("replay_targets").row_count, + "replay target", + ) + + header = self.table("header") + if header.row_count != 1: + raise RecurrenceColumnarInputError("recurrence header must have one row") + if ( + int(header.column("schema_version")[0]) + != RECURRENCE_BUILDER_INPUT_SCHEMA_VERSION + ): + raise RecurrenceColumnarInputError( + "recurrence header has the wrong schema version" + ) + expected_counts = { + "external_leg_count": external_count, + "physical_sector_count": sector_count, + "public_flow_count": self.table("public_lc_flows").row_count, + "replay_partition_count": partition_count, + "coupling_limit_count": self.table("coupling_limits").row_count, + "parameter_projection_count": self.table("parameter_projection").row_count, + } + for column, expected in expected_counts.items(): + if int(header.column(column)[0]) != expected: + raise RecurrenceColumnarInputError( + f"recurrence header {column} does not match its table" + ) + + strings = _decode_flat_strings( + self.table("string_ranges"), + self.table("string_bytes"), + ) + abi_id = int(header.column("abi_string_id")[0]) + if strings[abi_id] != self.abi: + raise RecurrenceColumnarInputError( + "recurrence header ABI string does not match the payload ABI" + ) + roles = tuple( + strings[int(value)] + for value in self.table("header_digests").column("role_string_id") + ) + if len(set(roles)) != len(roles): + raise RecurrenceColumnarInputError( + "recurrence header contains duplicate semantic digest roles" + ) + if not set(roles) >= _REQUIRED_HEADER_DIGEST_ROLES: + missing = sorted(_REQUIRED_HEADER_DIGEST_ROLES - set(roles)) + raise RecurrenceColumnarInputError( + f"recurrence header is missing semantic digests: {missing!r}" + ) + + selected_flows = self.table("selected_public_flow_coverage").row_count + selected_sources = self.table("selected_source_coverage").row_count + if bool(int(header.column("selected_flow_mode")[0])) != bool(selected_flows): + raise RecurrenceColumnarInputError( + "selected-flow header mode disagrees with its coverage table" + ) + if bool(int(header.column("selected_source_mode")[0])) != bool( + selected_sources + ): + raise RecurrenceColumnarInputError( + "selected-source header mode disagrees with its coverage table" + ) + if any( + int(value) not in {-1, 1} + for value in self.table("source_states").column("momentum_sign") + ): + raise RecurrenceColumnarInputError( + "source-state momentum signs must be -1 or 1" + ) + + def _validate_fermion_pairing(self) -> None: + roles = self._semantic_digest_roles() + pairing_roles = { + "fermion-pairing-semantic", + "fermion-pairing-topology", + } + if not self.fermion_pairing_tables: + unexpected = sorted(pairing_roles & roles.keys()) + if unexpected: + raise RecurrenceColumnarInputError( + "recurrence header claims fermion-pairing semantics without " + f"fixed-width tables: {unexpected!r}" + ) + return + + from .recurrence_pairing_columnar import ( + _PAIRING_COLUMNAR_ABI, + _FermionPairingColumnarV1, + ) + + payload = _FermionPairingColumnarV1( + _PAIRING_COLUMNAR_ABI, + self.fermion_pairing_tables, + ) + missing = sorted(pairing_roles - roles.keys()) + if missing: + raise RecurrenceColumnarInputError( + "recurrence fermion-pairing tables lack semantic header digests: " + f"{missing!r}" + ) + + header = payload.table("header") + external_legs = self.table("external_legs") + if int(header.column("source_count")[0]) != external_legs.row_count: + raise RecurrenceColumnarInputError( + "fermion-pairing source count does not match external legs" + ) + topology_digest = bytes(header.column("topology_digest")[0]).hex() + semantic_digest = bytes(header.column("semantic_digest")[0]).hex() + if roles["fermion-pairing-topology"] != topology_digest: + raise RecurrenceColumnarInputError( + "fermion-pairing topology digest disagrees with the process header" + ) + if roles["fermion-pairing-semantic"] != semantic_digest: + raise RecurrenceColumnarInputError( + "fermion-pairing semantic digest disagrees with the process header" + ) + + pairing_strings = _decode_pairing_strings(payload) + process_key_id = int(header.column("process_key_string_id")[0]) + process_key = pairing_strings[process_key_id] + main_header = self.table("header") + main_strings = _decode_flat_strings( + self.table("string_ranges"), + self.table("string_bytes"), + ) + process_id = main_strings[int(main_header.column("process_id_string_id")[0])] + if process_key != process_id: + raise RecurrenceColumnarInputError( + "fermion-pairing process key does not match recurrence process ID" + ) + + endpoints = payload.table("endpoints") + endpoint_slots = tuple(int(value) for value in endpoints.column("source_slot")) + if len(endpoint_slots) != len(set(endpoint_slots)): + raise RecurrenceColumnarInputError( + "fermion-pairing endpoint source slots must be unique" + ) + expected_orientations: dict[int, int] = {} + open_strings = self.table("lc_open_strings") + for row in range(open_strings.row_count): + for column, orientation in ( + ("fundamental_source_slot", 0), + ("antifundamental_source_slot", 1), + ): + source_slot = int(open_strings.column(column)[row]) + previous = expected_orientations.setdefault(source_slot, orientation) + if previous != orientation: + raise RecurrenceColumnarInputError( + "LC sectors assign inconsistent color orientations to one " + "fermion endpoint" + ) + if set(endpoint_slots) != set(expected_orientations): + raise RecurrenceColumnarInputError( + "fermion-pairing endpoints do not cover the LC open-line endpoints" + ) + public_labels = external_legs.column("public_label") + fermionic = external_legs.column("is_fermionic") + endpoint_labels = endpoints.column("public_label") + endpoint_orientations = endpoints.column("color_orientation") + for row, source_slot in enumerate(endpoint_slots): + if not int(fermionic[source_slot]): + raise RecurrenceColumnarInputError( + "fermion-pairing endpoint references a non-fermionic source" + ) + if int(endpoint_labels[row]) != int(public_labels[source_slot]): + raise RecurrenceColumnarInputError( + "fermion-pairing endpoint public label disagrees with its source" + ) + if int(endpoint_orientations[row]) != expected_orientations[source_slot]: + raise RecurrenceColumnarInputError( + "fermion-pairing endpoint orientation disagrees with LC sectors" + ) + + def _semantic_digest_roles(self) -> dict[str, str]: + strings = _decode_flat_strings( + self.table("string_ranges"), + self.table("string_bytes"), + ) + digests = self.table("digest_catalog").column("value") + result: dict[str, str] = {} + table = self.table("header_digests") + for row in range(table.row_count): + role = strings[int(table.column("role_string_id")[row])] + digest = bytes(digests[int(table.column("digest_id")[row])]).hex() + result[role] = digest + return result + + +class _StringCatalog: + def __init__(self, values: Iterable[str]) -> None: + unique = {_nonempty_text(value, "string catalog value") for value in values} + self.values = tuple(sorted(unique, key=lambda value: value.encode("utf-8"))) + self.ids = {value: index for index, value in enumerate(self.values)} + + def id(self, value: str) -> int: + try: + return self.ids[value] + except KeyError as error: + raise RecurrenceColumnarInputError( + f"string {value!r} was not collected before table construction" + ) from error + + def tables(self) -> tuple[RecurrenceColumnarTable, RecurrenceColumnarTable]: + encoded = tuple(value.encode("utf-8") for value in self.values) + ranges = _allocate(len(encoded), start=_U64, count=_U64) + flat = _allocate(sum(len(value) for value in encoded), value=_U8) + cursor = 0 + for row, value in enumerate(encoded): + ranges["start"][row] = cursor + ranges["count"][row] = len(value) + if value: + flat["value"][cursor : cursor + len(value)] = np.frombuffer( + value, + dtype=_U8, + ) + cursor += len(value) + return ( + _freeze_table("string_ranges", ranges), + _freeze_table("string_bytes", flat), + ) + + +class _DigestCatalog: + def __init__(self, values: Iterable[str]) -> None: + raw = {_sha256_bytes(value, "semantic digest") for value in values} + self.values = tuple(sorted(raw)) + self.ids = {value: index for index, value in enumerate(self.values)} + + def id(self, value: str) -> int: + return self.ids[_sha256_bytes(value, "semantic digest")] + + def table(self) -> RecurrenceColumnarTable: + columns = _allocate(len(self.values), id=_U32, value=(_U8, (32,))) + for row, value in enumerate(self.values): + columns["id"][row] = row + columns["value"][row] = np.frombuffer(value, dtype=_U8) + return _freeze_table("digest_catalog", columns) + + +class _U32SequenceCatalog: + def __init__(self, values: Iterable[Sequence[int]]) -> None: + canonical: set[tuple[int, ...]] = set() + for value in values: + item = tuple(value) + _validate_u32_sequence(item, "u32 sequence catalog value") + canonical.add(item) + self.values = tuple(sorted(canonical)) + self.ids = {value: index for index, value in enumerate(self.values)} + + def id(self, value: Sequence[int]) -> int: + item = tuple(value) + try: + return self.ids[item] + except KeyError as error: + raise RecurrenceColumnarInputError( + f"u32 sequence {item!r} was not collected" + ) from error + + def tables(self) -> tuple[RecurrenceColumnarTable, RecurrenceColumnarTable]: + ranges = _allocate(len(self.values), start=_U64, count=_U64) + flat = _allocate(sum(len(value) for value in self.values), value=_U32) + cursor = 0 + for row, value in enumerate(self.values): + ranges["start"][row] = cursor + ranges["count"][row] = len(value) + if value: + flat["value"][cursor : cursor + len(value)] = value + cursor += len(value) + return ( + _freeze_table("u32_sequence_ranges", ranges), + _freeze_table("u32_sequence_values", flat), + ) + + +class _BitsetCatalog: + def __init__(self, values: Iterable[int]) -> None: + canonical: set[int] = set() + for value in values: + _validate_nonnegative_mask(value, "bitset catalog value") + canonical.add(value) + self.values = tuple(sorted(canonical)) + self.ids = {value: index for index, value in enumerate(self.values)} + + def id(self, value: int) -> int: + return self.ids[value] + + def tables(self) -> tuple[RecurrenceColumnarTable, RecurrenceColumnarTable]: + ranges = _allocate( + len(self.values), + id=_U32, + start=_U64, + count=_U64, + bit_count=_U64, + ) + total_words = sum((value.bit_length() + 63) // 64 for value in self.values) + words = _allocate(total_words, value=_U64) + cursor = 0 + for row, value in enumerate(self.values): + count = (value.bit_length() + 63) // 64 + ranges["id"][row] = row + ranges["start"][row] = cursor + ranges["count"][row] = count + ranges["bit_count"][row] = value.bit_count() + for offset in range(count): + words["value"][cursor] = (value >> (64 * offset)) & ((1 << 64) - 1) + cursor += 1 + return ( + _freeze_table("bitset_ranges", ranges), + _freeze_table("bitset_words", words), + ) + + +class _FactorCatalog: + def __init__(self, values: Iterable[ExactComplexRationalV1]) -> None: + unique = {value.canonical_key: value for value in values} + self.values = tuple(unique[key] for key in sorted(unique)) + self.ids = { + value.canonical_key: index for index, value in enumerate(self.values) + } + + def id(self, value: ExactComplexRationalV1) -> int: + return self.ids[value.canonical_key] + + def table(self, strings: _StringCatalog) -> RecurrenceColumnarTable: + columns = _allocate( + len(self.values), + id=_U32, + real_numerator_string_id=_U32, + real_denominator_string_id=_U32, + imag_numerator_string_id=_U32, + imag_denominator_string_id=_U32, + ) + for row, value in enumerate(self.values): + columns["id"][row] = row + columns["real_numerator_string_id"][row] = strings.id( + str(value.real_numerator) + ) + columns["real_denominator_string_id"][row] = strings.id( + str(value.real_denominator) + ) + columns["imag_numerator_string_id"][row] = strings.id( + str(value.imag_numerator) + ) + columns["imag_denominator_string_id"][row] = strings.id( + str(value.imag_denominator) + ) + return _freeze_table("exact_factors", columns) + + +def build_recurrence_builder_input_v1( + logical: RecurrenceBuilderLogicalInputV1, +) -> RecurrenceBuilderInputV1: + """Validate and flatten compact logical recurrence records. + + The caller retains no mutable storage reachable from the returned payload. + Record collections whose order is not semantic are canonicalised before + catalog and table construction. + """ + + if not isinstance(logical, RecurrenceBuilderLogicalInputV1): + raise TypeError("recurrence columnar extraction requires logical v1 records") + + external_legs = tuple( + sorted(logical.external_legs, key=lambda leg: leg.source_slot) + ) + sectors = tuple( + sorted(logical.physical_sectors, key=lambda sector: sector.sector_id) + ) + public_flows = tuple(sorted(logical.public_flows, key=lambda flow: flow.flow_id)) + partitions = tuple( + sorted( + logical.replay_partitions, + key=lambda partition: ( + partition.representative_sector_id, + partition.materialized_sector_id, + ), + ) + ) + digests = tuple(sorted(logical.semantic_digests, key=lambda item: item.role)) + template_refs = tuple( + sorted( + logical.semantic_template_references, + key=lambda item: (item.kind, item.template_id, item.semantic_digest), + ) + ) + coupling_limits = tuple(sorted(logical.coupling_limits, key=lambda item: item.name)) + parameter_projection = tuple( + sorted(logical.parameter_projection, key=lambda item: item.runtime_slot) + ) + selected_flows = ( + None + if logical.selected_public_flow_ids is None + else tuple(sorted(logical.selected_public_flow_ids)) + ) + selected_sources = ( + None + if logical.selected_source_coverage is None + else tuple( + sorted( + logical.selected_source_coverage, + key=lambda item: item.source_slot, + ) + ) + ) + + _validate_logical_relations( + logical, + external_legs=external_legs, + sectors=sectors, + public_flows=public_flows, + partitions=partitions, + digests=digests, + template_refs=template_refs, + coupling_limits=coupling_limits, + parameter_projection=parameter_projection, + selected_flows=selected_flows, + selected_sources=selected_sources, + ) + + pairing_tables: tuple[RecurrenceColumnarTable, ...] = () + if logical.fermion_pairing_catalog is not None: + from .recurrence_pairing_columnar import ( + _encode_fermion_pairing_catalog_v1, + ) + + pairing_tables = _encode_fermion_pairing_catalog_v1( + logical.fermion_pairing_catalog + ).tables + + all_factors = [logical.normalization.factor] + all_factors.extend( + target.amplitude_phase + for partition in partitions + for target in partition.targets + ) + all_factors.extend(flow.reduction_weight for flow in public_flows) + all_factors.extend( + state.crossing_phase for leg in external_legs for state in leg.source_states + ) + factor_catalog = _FactorCatalog(all_factors) + + string_values: list[str] = [ + RECURRENCE_BUILDER_INPUT_ABI, + logical.process_id, + logical.normalization.convention, + ] + string_values.extend(item.role for item in digests) + string_values.extend(item.public_id for item in sectors) + string_values.extend(item.closure_proof_algorithm for item in sectors) + string_values.extend(item.public_id for item in public_flows) + string_values.extend(partition.proof_algorithm for partition in partitions) + string_values.extend(limit.name for limit in coupling_limits) + string_values.extend(item.kind for item in template_refs) + string_values.extend(item.runtime_name for item in parameter_projection) + for factor in factor_catalog.values: + string_values.extend(str(value) for value in factor.canonical_key) + strings = _StringCatalog(string_values) + + digest_values = [item.digest for item in digests] + digest_values.extend(sector.closure_proof_digest for sector in sectors) + digest_values.extend(partition.proof_digest for partition in partitions) + digest_values.extend(item.semantic_digest for item in template_refs) + digest_values.append(logical.normalization.semantic_digest) + digest_catalog = _DigestCatalog(digest_values) + + sequence_values: list[Sequence[int]] = [()] + for sector in sectors: + sequence_values.extend( + ( + sector.trace_source_slots, + sector.singlet_source_slots, + sector.word_source_slots, + ) + ) + for line in sector.open_strings: + sequence_values.extend( + (line.adjoint_source_slots, line.singlet_source_slots) + ) + for partition in partitions: + for target in partition.targets: + sequence_values.extend( + ( + target.external_permutation, + target.source_slot_permutation, + ) + ) + for flow in public_flows: + sequence_values.extend((flow.word_source_slots, flow.source_slot_permutation)) + sequences = _U32SequenceCatalog(sequence_values) + + bitsets = _BitsetCatalog( + ( + logical.process_support_mask, + *(leg.momentum_mask for leg in external_legs), + *(leg.support_mask for leg in external_legs), + *(sector.support_mask for sector in sectors), + ) + ) + + tables: list[RecurrenceColumnarTable] = [] + tables.extend(bitsets.tables()) + tables.append(_build_coupling_limits(coupling_limits, strings)) + tables.append(digest_catalog.table()) + tables.append(factor_catalog.table(strings)) + external_table, source_state_table = _build_external_tables( + external_legs, + bitsets, + factor_catalog, + ) + tables.extend((external_table, source_state_table)) + tables.append( + _build_header( + logical, + strings, + bitsets, + external_count=len(external_legs), + sector_count=len(sectors), + public_flow_count=len(public_flows), + partition_count=len(partitions), + coupling_limit_count=len(coupling_limits), + parameter_count=len(parameter_projection), + selected_flow_mode=selected_flows is not None, + selected_source_mode=selected_sources is not None, + ) + ) + tables.append(_build_header_digests(digests, strings, digest_catalog)) + sector_table, open_string_table = _build_sector_tables( + sectors, + strings, + digest_catalog, + sequences, + bitsets, + ) + tables.extend((sector_table, open_string_table)) + tables.append(_build_public_flows(public_flows, strings, sequences, factor_catalog)) + tables.append( + _build_normalization( + logical.normalization, + strings, + digest_catalog, + factor_catalog, + ) + ) + tables.append(_build_parameter_projection(parameter_projection, strings)) + partition_table, target_table = _build_replay_tables( + partitions, + strings, + digest_catalog, + sequences, + factor_catalog, + ) + tables.extend((partition_table, target_table)) + tables.append(_build_selected_flows(selected_flows)) + tables.append(_build_selected_sources(selected_sources)) + tables.append(_build_template_references(template_refs, strings, digest_catalog)) + string_ranges, string_bytes = strings.tables() + tables.extend((string_ranges, string_bytes)) + sequence_ranges, sequence_values_table = sequences.tables() + tables.extend((sequence_ranges, sequence_values_table)) + + return RecurrenceBuilderInputV1( + abi=RECURRENCE_BUILDER_INPUT_ABI, + tables=tuple(sorted(tables, key=lambda table: table.name)), + fermion_pairing_tables=pairing_tables, + ) + + +def _validate_logical_relations( + logical: RecurrenceBuilderLogicalInputV1, + *, + external_legs: tuple[RecurrenceExternalLegV1, ...], + sectors: tuple[RecurrencePhysicalLCSectorV1, ...], + public_flows: tuple[RecurrencePublicLCFlowV1, ...], + partitions: tuple[RecurrenceReplayPartitionV1, ...], + digests: tuple[RecurrenceSemanticDigestV1, ...], + template_refs: tuple[RecurrenceSemanticTemplateReferenceV1, ...], + coupling_limits: tuple[RecurrenceCouplingLimitV1, ...], + parameter_projection: tuple[RecurrenceParameterProjectionV1, ...], + selected_flows: tuple[int, ...] | None, + selected_sources: tuple[RecurrenceSelectedSourceCoverageV1, ...] | None, +) -> None: + source_slots = tuple(leg.source_slot for leg in external_legs) + if source_slots != tuple(range(len(source_slots))): + raise RecurrenceColumnarInputError( + "external source slots must be dense from zero" + ) + public_labels = tuple(leg.public_label for leg in external_legs) + if len(set(public_labels)) != len(public_labels): + raise RecurrenceColumnarInputError("external public labels must be unique") + sector_ids = tuple(sector.sector_id for sector in sectors) + if sector_ids != tuple(range(len(sector_ids))): + raise RecurrenceColumnarInputError( + "physical LC sector IDs must be dense from zero" + ) + public_sector_ids = tuple(sector.public_id for sector in sectors) + if len(set(public_sector_ids)) != len(public_sector_ids): + raise RecurrenceColumnarInputError( + "physical LC sector public IDs must be unique" + ) + if not sectors: + raise RecurrenceColumnarInputError( + "recurrence v2 requires at least one physical LC sector" + ) + flow_ids = tuple(flow.flow_id for flow in public_flows) + if flow_ids != tuple(range(len(flow_ids))): + raise RecurrenceColumnarInputError("public LC flow IDs must be dense from zero") + public_flow_ids = tuple(flow.public_id for flow in public_flows) + if not public_flows or len(set(public_flow_ids)) != len(public_flow_ids): + raise RecurrenceColumnarInputError( + "public LC flow identifiers must be nonempty and unique" + ) + for flow in public_flows: + _require_index( + flow.construction_sector_id, + len(sectors), + "public flow construction sector", + ) + _validate_permutation( + flow.source_slot_permutation, + len(external_legs), + "public flow source-slot permutation", + ) + _validate_source_contract_permutation( + external_legs, + flow.source_slot_permutation, + f"public LC flow {flow.flow_id}", + ) + construction_word = sectors[flow.construction_sector_id].word_source_slots + mapped_word = tuple( + flow.source_slot_permutation[source_slot] + for source_slot in construction_word + ) + construction_sector = sectors[flow.construction_sector_id] + reflected_trace = ( + construction_sector.kind == "single-trace" + and len(construction_word) > 2 + and flow.word_source_slots + == (construction_word[0], *reversed(construction_word[1:])) + and flow.source_slot_permutation == tuple(range(len(external_legs))) + ) + if mapped_word != flow.word_source_slots and not reflected_trace: + raise RecurrenceColumnarInputError( + "public flow word is neither its gathered construction word nor " + "an anchored folded-trace reflection" + ) + + roles = tuple(item.role for item in digests) + if len(set(roles)) != len(roles): + raise RecurrenceColumnarInputError( + "semantic header digest roles must be unique" + ) + if not set(roles) >= _REQUIRED_HEADER_DIGEST_ROLES: + missing = sorted(_REQUIRED_HEADER_DIGEST_ROLES - set(roles)) + raise RecurrenceColumnarInputError( + f"recurrence logical input is missing semantic digests: {missing!r}" + ) + pairing_roles = { + item.role: item.digest + for item in digests + if item.role.startswith("fermion-pairing-") + } + pairing = logical.fermion_pairing_catalog + if pairing is None: + if pairing_roles: + raise RecurrenceColumnarInputError( + "recurrence logical input claims fermion-pairing digests without " + "an exact pairing catalog" + ) + else: + expected_pairing_roles = { + "fermion-pairing-semantic": pairing.semantic_digest, + "fermion-pairing-topology": pairing.topology_digest, + } + if pairing_roles != expected_pairing_roles: + raise RecurrenceColumnarInputError( + "recurrence fermion-pairing digest roles are missing or stale" + ) + if pairing.process_key != logical.process_id: + raise RecurrenceColumnarInputError( + "fermion-pairing process key does not match recurrence process ID" + ) + if pairing.source_count != len(external_legs): + raise RecurrenceColumnarInputError( + "fermion-pairing source count does not match external legs" + ) + + names = tuple(limit.name for limit in coupling_limits) + if len(set(names)) != len(names): + raise RecurrenceColumnarInputError("coupling-order names must be unique") + slots = tuple(item.runtime_slot for item in parameter_projection) + if slots != tuple(range(len(slots))): + raise RecurrenceColumnarInputError( + "runtime parameter slots must be dense from zero" + ) + runtime_components = tuple( + (item.runtime_name, item.component) for item in parameter_projection + ) + if len(set(runtime_components)) != len(runtime_components): + raise RecurrenceColumnarInputError( + "runtime parameter name/component pairs must be unique" + ) + template_keys = tuple((item.kind, item.template_id) for item in template_refs) + if len(set(template_keys)) != len(template_keys): + raise RecurrenceColumnarInputError( + "semantic-template kind/ID pairs must be unique" + ) + available_templates = set(template_keys) + for leg in external_legs: + for state in leg.source_states: + if ( + "current-state", + state.current_state_template_id, + ) not in available_templates: + raise RecurrenceColumnarInputError( + "external source coverage references an absent current-state " + "template" + ) + if ("source", state.source_template_id) not in available_templates: + raise RecurrenceColumnarInputError( + "external source coverage references an absent source template" + ) + for projection in parameter_projection: + if ("parameter", projection.parameter_template_id) not in available_templates: + raise RecurrenceColumnarInputError( + "parameter projection references an absent parameter template" + ) + + for sector in sectors: + _require_index( + sector.closure_source_slot, + len(external_legs), + "LC closure source slot", + ) + if sector.word_source_slots: + if sector.closure_proof_algorithm == "canonical-lc-closure-anchor-v2": + if sector.closure_source_slot != sector.word_source_slots[-1]: + raise RecurrenceColumnarInputError( + "v2 LC closure source slot must be the terminal " + "colour-word endpoint" + ) + elif sector.closure_proof_algorithm == "canonical-lc-closure-anchor-v4": + # The producer owns this inherited provenance; the typed Rust + # process boundary validates its structural preconditions once. + pass + elif sector.closure_proof_algorithm == "canonical-lc-closure-anchor-v3": + if sector.kind != "open-lines": + raise RecurrenceColumnarInputError( + "v3 LC closure anchoring is available only for " + "open-line sectors" + ) + open_blocks = tuple( + ( + line.fundamental_source_slot, + *line.adjoint_source_slots, + line.antifundamental_source_slot, + ) + for line in sector.open_strings + ) + physical_blocks: list[tuple[int, ...]] = [] + unused = set(range(len(open_blocks))) + cursor = 0 + while cursor < len(sector.word_source_slots): + matches = tuple( + index + for index in sorted(unused) + if sector.word_source_slots[ + cursor : cursor + len(open_blocks[index]) + ] + == open_blocks[index] + ) + if len(matches) != 1: + raise RecurrenceColumnarInputError( + "v3 LC public word does not have one exact open-line " + f"block at source position {cursor}" + ) + block_index = matches[0] + unused.remove(block_index) + block = open_blocks[block_index] + physical_blocks.append(block) + cursor += len(block) + if unused or cursor != len(sector.word_source_slots): + raise RecurrenceColumnarInputError( + "v3 LC public word is not an exact complete-open-line " + "block traversal" + ) + distinguished_source_slot = min(sector.word_source_slots) + distinguished_positions = tuple( + index + for index, block in enumerate(physical_blocks) + if distinguished_source_slot in block + ) + if len(distinguished_positions) != 1: + raise RecurrenceColumnarInputError( + "v3 LC minimum coloured source slot must belong to " + "exactly one open-line block" + ) + first = distinguished_positions[0] + closure_blocks = ( + *physical_blocks[first:], + *physical_blocks[:first], + ) + closure_traversal = tuple( + slot for block in closure_blocks for slot in block + ) + if ( + not closure_traversal + or sector.closure_source_slot != closure_traversal[-1] + ): + raise RecurrenceColumnarInputError( + "v3 LC closure source slot does not match the independently " + "reconstructed minimum-coloured-source block rotation" + ) + payload = { + "algorithm": "canonical-lc-closure-anchor-v3", + "closure_block_source_slots": closure_blocks, + "closure_source_slot": sector.closure_source_slot, + "closure_traversal_source_slots": closure_traversal, + "component_order_policy": ( + "independent-open-string-block-permutation" + if len(open_blocks) > 1 + else "exact-ordered-components" + ), + "distinguished_source_block": closure_blocks[0], + "distinguished_source_slot": distinguished_source_slot, + "external_source_count": len(external_legs), + "fermionic_source_slots": tuple( + leg.source_slot for leg in external_legs if leg.is_fermionic + ), + "open_string_count": len(open_blocks), + "physical_block_source_slots": physical_blocks, + "policy": ("minimum-coloured-source-slot-open-line-block-rotation"), + "process_source_label_order": tuple( + leg.public_label for leg in external_legs + ), + "sector_id": sector.sector_id, + "sector_kind": sector.kind, + "selected_closure_block": closure_blocks[-1], + "singlet_source_slots": sector.singlet_source_slots, + "word_source_slots": sector.word_source_slots, + } + expected_digest = hashlib.sha256( + json.dumps( + payload, + allow_nan=False, + ensure_ascii=True, + separators=(",", ":"), + sort_keys=True, + ).encode("ascii") + ).hexdigest() + if sector.closure_proof_digest != expected_digest: + raise RecurrenceColumnarInputError( + "v3 LC closure proof digest does not match the independently " + "reconstructed minimum-coloured-source block rotation" + ) + else: + raise RecurrenceColumnarInputError( + "unsupported LC closure-anchor proof algorithm " + f"{sector.closure_proof_algorithm!r}" + ) + elif sector.kind != "singlet": + raise RecurrenceColumnarInputError( + "an LC sector without a colour word must be all-singlet" + ) + elif tuple(sorted(sector.singlet_source_slots)) != tuple( + range(len(external_legs)) + ): + raise RecurrenceColumnarInputError( + "an all-singlet LC sector must cover every external source" + ) + else: + fermionic_slots = tuple( + leg.source_slot for leg in external_legs if leg.is_fermionic + ) + expected_anchor = min(fermionic_slots) if fermionic_slots else 0 + if sector.closure_source_slot != expected_anchor: + raise RecurrenceColumnarInputError( + "an all-singlet LC sector must use the smallest fermionic " + "source slot, or the smallest source slot when no fermion " + "is present" + ) + referenced_slots: list[int] = [ + *sector.trace_source_slots, + *sector.singlet_source_slots, + *sector.word_source_slots, + ] + for line in sector.open_strings: + referenced_slots.extend( + ( + line.fundamental_source_slot, + line.antifundamental_source_slot, + *line.adjoint_source_slots, + *line.singlet_source_slots, + ) + ) + for source_slot in referenced_slots: + _require_index(source_slot, len(external_legs), "LC source slot") + + covered_targets: set[int] = set() + for partition in partitions: + _require_index( + partition.representative_sector_id, + len(sectors), + "replay representative sector", + ) + _require_index( + partition.materialized_sector_id, + len(sectors), + "replay materialized sector", + ) + representative_word = sectors[ + partition.representative_sector_id + ].word_source_slots + representative_anchor = sectors[ + partition.representative_sector_id + ].closure_source_slot + for target in sorted(partition.targets, key=lambda item: item.sector_id): + _require_index(target.sector_id, len(sectors), "replay target sector") + if target.sector_id in covered_targets: + raise RecurrenceColumnarInputError( + "replay partitions overlap one physical sector" + ) + covered_targets.add(target.sector_id) + _validate_permutation( + target.external_permutation, + len(external_legs), + "replay external permutation", + ) + if len(target.source_slot_permutation) != len(external_legs): + raise RecurrenceColumnarInputError( + "replay source-slot permutation must cover every external leg" + ) + _validate_permutation( + target.source_slot_permutation, + len(external_legs), + "replay source-slot permutation", + ) + if target.external_permutation != target.source_slot_permutation: + raise RecurrenceColumnarInputError( + "recurrence ABI v2 requires identical replay external and " + "source-slot permutations" + ) + _validate_source_contract_permutation( + external_legs, + target.source_slot_permutation, + f"replay target sector {target.sector_id}", + ) + mapped_word = tuple( + target.source_slot_permutation[source_slot] + for source_slot in representative_word + ) + if mapped_word != sectors[target.sector_id].word_source_slots: + raise RecurrenceColumnarInputError( + "replay target word does not match its gather permutation" + ) + mapped_anchor = target.source_slot_permutation[representative_anchor] + if mapped_anchor != sectors[target.sector_id].closure_source_slot: + raise RecurrenceColumnarInputError( + "replay target permutation does not map the representative " + "closure anchor onto the target anchor" + ) + + if selected_flows is not None: + if not selected_flows: + raise RecurrenceColumnarInputError( + "selected generation flow coverage cannot be empty" + ) + if len(set(selected_flows)) != len(selected_flows): + raise RecurrenceColumnarInputError( + "selected generation flow coverage contains duplicates" + ) + for flow_id in selected_flows: + _require_index(flow_id, len(public_flows), "selected public flow") + + if selected_sources is not None: + if not selected_sources: + raise RecurrenceColumnarInputError( + "selected source generation coverage cannot be empty" + ) + selected_slots = tuple(item.source_slot for item in selected_sources) + if len(set(selected_slots)) != len(selected_slots): + raise RecurrenceColumnarInputError( + "selected source generation coverage repeats a source slot" + ) + if tuple(sorted(selected_slots)) != tuple(range(len(external_legs))): + raise RecurrenceColumnarInputError( + "selected source generation coverage must retain at least one " + "state for every external source slot" + ) + for item in selected_sources: + source_slot = _require_index( + item.source_slot, + len(external_legs), + "selected source slot", + ) + state_count = len(external_legs[source_slot].source_states) + for state_index in item.source_state_indices: + _require_index( + state_index, + state_count, + "selected source-state index", + ) + + +def _build_header( + logical: RecurrenceBuilderLogicalInputV1, + strings: _StringCatalog, + bitsets: _BitsetCatalog, + *, + external_count: int, + sector_count: int, + public_flow_count: int, + partition_count: int, + coupling_limit_count: int, + parameter_count: int, + selected_flow_mode: bool, + selected_source_mode: bool, +) -> RecurrenceColumnarTable: + columns = _allocate( + 1, + schema_version=_U32, + abi_string_id=_U32, + process_id_string_id=_U32, + layout=_U8, + selected_flow_mode=_U8, + selected_source_mode=_U8, + external_leg_count=_U32, + physical_sector_count=_U32, + public_flow_count=_U32, + replay_partition_count=_U32, + coupling_limit_count=_U32, + parameter_projection_count=_U32, + process_support_mask_id=_U32, + ) + columns["schema_version"][0] = RECURRENCE_BUILDER_INPUT_SCHEMA_VERSION + columns["abi_string_id"][0] = strings.id(RECURRENCE_BUILDER_INPUT_ABI) + columns["process_id_string_id"][0] = strings.id(logical.process_id) + columns["layout"][0] = _LAYOUT_CODES[logical.layout] + columns["selected_flow_mode"][0] = int(selected_flow_mode) + columns["selected_source_mode"][0] = int(selected_source_mode) + columns["external_leg_count"][0] = _checked_u32(external_count, "external count") + columns["physical_sector_count"][0] = _checked_u32(sector_count, "sector count") + columns["public_flow_count"][0] = _checked_u32( + public_flow_count, + "public flow count", + ) + columns["replay_partition_count"][0] = _checked_u32( + partition_count, + "replay partition count", + ) + columns["coupling_limit_count"][0] = _checked_u32( + coupling_limit_count, + "coupling-limit count", + ) + columns["parameter_projection_count"][0] = _checked_u32( + parameter_count, + "parameter-projection count", + ) + columns["process_support_mask_id"][0] = bitsets.id(logical.process_support_mask) + return _freeze_table("header", columns) + + +def _build_header_digests( + values: tuple[RecurrenceSemanticDigestV1, ...], + strings: _StringCatalog, + digests: _DigestCatalog, +) -> RecurrenceColumnarTable: + columns = _allocate(len(values), role_string_id=_U32, digest_id=_U32) + for row, value in enumerate(values): + columns["role_string_id"][row] = strings.id(value.role) + columns["digest_id"][row] = digests.id(value.digest) + return _freeze_table("header_digests", columns) + + +def _build_external_tables( + values: tuple[RecurrenceExternalLegV1, ...], + bitsets: _BitsetCatalog, + factors: _FactorCatalog, +) -> tuple[RecurrenceColumnarTable, RecurrenceColumnarTable]: + legs = _allocate( + len(values), + source_slot=_U32, + public_label=_U32, + physical_pdg=_I32, + outgoing_pdg=_I32, + is_initial=_U8, + is_fermionic=_U8, + source_state_start=_U64, + source_state_count=_U64, + momentum_mask_id=_U32, + support_mask_id=_U32, + ) + state_count = sum(len(value.source_states) for value in values) + states = _allocate( + state_count, + source_slot=_U32, + state_index=_U32, + public_helicity=_I32, + chirality=_I32, + spin_state=_I32, + current_state_template_id=_U32, + source_template_id=_U32, + momentum_sign=_I32, + crossing_phase_factor_id=_U32, + ) + cursor = 0 + for row, value in enumerate(values): + legs["source_slot"][row] = value.source_slot + legs["public_label"][row] = value.public_label + legs["physical_pdg"][row] = value.physical_pdg + legs["outgoing_pdg"][row] = value.outgoing_pdg + legs["is_initial"][row] = int(value.is_initial) + legs["is_fermionic"][row] = int(value.is_fermionic) + legs["source_state_start"][row] = cursor + legs["source_state_count"][row] = len(value.source_states) + legs["momentum_mask_id"][row] = bitsets.id(value.momentum_mask) + legs["support_mask_id"][row] = bitsets.id(value.support_mask) + for state in value.source_states: + states["source_slot"][cursor] = value.source_slot + states["state_index"][cursor] = state.state_index + states["public_helicity"][cursor] = state.public_helicity + states["chirality"][cursor] = state.chirality + states["spin_state"][cursor] = state.spin_state + states["current_state_template_id"][cursor] = ( + state.current_state_template_id + ) + states["source_template_id"][cursor] = state.source_template_id + states["momentum_sign"][cursor] = state.momentum_sign + states["crossing_phase_factor_id"][cursor] = factors.id( + state.crossing_phase + ) + cursor += 1 + return ( + _freeze_table("external_legs", legs), + _freeze_table("source_states", states), + ) + + +def _build_sector_tables( + values: tuple[RecurrencePhysicalLCSectorV1, ...], + strings: _StringCatalog, + digests: _DigestCatalog, + sequences: _U32SequenceCatalog, + bitsets: _BitsetCatalog, +) -> tuple[RecurrenceColumnarTable, RecurrenceColumnarTable]: + sectors = _allocate( + len(values), + sector_id=_U32, + public_id_string_id=_U32, + kind=_U8, + closure_source_slot=_U32, + closure_proof_algorithm_string_id=_U32, + closure_proof_digest_id=_U32, + open_string_start=_U64, + open_string_count=_U64, + trace_sequence_id=_U32, + singlet_sequence_id=_U32, + word_sequence_id=_U32, + support_mask_id=_U32, + ) + line_count = sum(len(value.open_strings) for value in values) + lines = _allocate( + line_count, + sector_id=_U32, + ordinal=_U32, + fundamental_source_slot=_U32, + antifundamental_source_slot=_U32, + adjoint_sequence_id=_U32, + singlet_sequence_id=_U32, + ) + cursor = 0 + for row, value in enumerate(values): + sectors["sector_id"][row] = value.sector_id + sectors["public_id_string_id"][row] = strings.id(value.public_id) + sectors["kind"][row] = _SECTOR_KIND_CODES[value.kind] + sectors["closure_source_slot"][row] = value.closure_source_slot + sectors["closure_proof_algorithm_string_id"][row] = strings.id( + value.closure_proof_algorithm + ) + sectors["closure_proof_digest_id"][row] = digests.id(value.closure_proof_digest) + sectors["open_string_start"][row] = cursor + sectors["open_string_count"][row] = len(value.open_strings) + sectors["trace_sequence_id"][row] = sequences.id(value.trace_source_slots) + sectors["singlet_sequence_id"][row] = sequences.id(value.singlet_source_slots) + sectors["word_sequence_id"][row] = sequences.id(value.word_source_slots) + sectors["support_mask_id"][row] = bitsets.id(value.support_mask) + for ordinal, line in enumerate(value.open_strings): + lines["sector_id"][cursor] = value.sector_id + lines["ordinal"][cursor] = ordinal + lines["fundamental_source_slot"][cursor] = line.fundamental_source_slot + lines["antifundamental_source_slot"][cursor] = ( + line.antifundamental_source_slot + ) + lines["adjoint_sequence_id"][cursor] = sequences.id( + line.adjoint_source_slots + ) + lines["singlet_sequence_id"][cursor] = sequences.id( + line.singlet_source_slots + ) + cursor += 1 + return ( + _freeze_table("physical_lc_sectors", sectors), + _freeze_table("lc_open_strings", lines), + ) + + +def _build_public_flows( + values: tuple[RecurrencePublicLCFlowV1, ...], + strings: _StringCatalog, + sequences: _U32SequenceCatalog, + factors: _FactorCatalog, +) -> RecurrenceColumnarTable: + columns = _allocate( + len(values), + flow_id=_U32, + public_id_string_id=_U32, + construction_sector_id=_U32, + word_sequence_id=_U32, + source_slot_permutation_sequence_id=_U32, + reduction_weight_factor_id=_U32, + ) + for row, value in enumerate(values): + columns["flow_id"][row] = value.flow_id + columns["public_id_string_id"][row] = strings.id(value.public_id) + columns["construction_sector_id"][row] = value.construction_sector_id + columns["word_sequence_id"][row] = sequences.id(value.word_source_slots) + columns["source_slot_permutation_sequence_id"][row] = sequences.id( + value.source_slot_permutation + ) + columns["reduction_weight_factor_id"][row] = factors.id(value.reduction_weight) + return _freeze_table("public_lc_flows", columns) + + +def _build_replay_tables( + values: tuple[RecurrenceReplayPartitionV1, ...], + strings: _StringCatalog, + digests: _DigestCatalog, + sequences: _U32SequenceCatalog, + factors: _FactorCatalog, +) -> tuple[RecurrenceColumnarTable, RecurrenceColumnarTable]: + partitions = _allocate( + len(values), + partition_id=_U32, + representative_sector_id=_U32, + materialized_sector_id=_U32, + target_start=_U64, + target_count=_U64, + proof_algorithm_string_id=_U32, + proof_digest_id=_U32, + ) + target_count = sum(len(value.targets) for value in values) + targets = _allocate( + target_count, + partition_id=_U32, + sector_id=_U32, + external_permutation_sequence_id=_U32, + source_slot_permutation_sequence_id=_U32, + amplitude_phase_factor_id=_U32, + fermion_sign=_I32, + ) + cursor = 0 + for partition_id, value in enumerate(values): + partitions["partition_id"][partition_id] = partition_id + partitions["representative_sector_id"][partition_id] = ( + value.representative_sector_id + ) + partitions["materialized_sector_id"][partition_id] = ( + value.materialized_sector_id + ) + partitions["target_start"][partition_id] = cursor + partitions["target_count"][partition_id] = len(value.targets) + partitions["proof_algorithm_string_id"][partition_id] = strings.id( + value.proof_algorithm + ) + partitions["proof_digest_id"][partition_id] = digests.id(value.proof_digest) + for target in sorted(value.targets, key=lambda item: item.sector_id): + targets["partition_id"][cursor] = partition_id + targets["sector_id"][cursor] = target.sector_id + targets["external_permutation_sequence_id"][cursor] = sequences.id( + target.external_permutation + ) + targets["source_slot_permutation_sequence_id"][cursor] = sequences.id( + target.source_slot_permutation + ) + targets["amplitude_phase_factor_id"][cursor] = factors.id( + target.amplitude_phase + ) + targets["fermion_sign"][cursor] = target.fermion_sign + cursor += 1 + return ( + _freeze_table("replay_partitions", partitions), + _freeze_table("replay_targets", targets), + ) + + +def _build_selected_flows( + values: tuple[int, ...] | None, +) -> RecurrenceColumnarTable: + selected = () if values is None else values + columns = _allocate(len(selected), flow_id=_U32) + if selected: + columns["flow_id"][:] = selected + return _freeze_table("selected_public_flow_coverage", columns) + + +def _build_selected_sources( + values: tuple[RecurrenceSelectedSourceCoverageV1, ...] | None, +) -> RecurrenceColumnarTable: + rows = ( + () + if values is None + else tuple( + (item.source_slot, state_index) + for item in values + for state_index in sorted(item.source_state_indices) + ) + ) + columns = _allocate(len(rows), source_slot=_U32, source_state_index=_U32) + for row, (source_slot, state_index) in enumerate(rows): + columns["source_slot"][row] = source_slot + columns["source_state_index"][row] = state_index + return _freeze_table("selected_source_coverage", columns) + + +def _build_coupling_limits( + values: tuple[RecurrenceCouplingLimitV1, ...], + strings: _StringCatalog, +) -> RecurrenceColumnarTable: + columns = _allocate( + len(values), + name_string_id=_U32, + minimum=_U32, + maximum=_U32, + ) + for row, value in enumerate(values): + columns["name_string_id"][row] = strings.id(value.name) + columns["minimum"][row] = value.minimum + columns["maximum"][row] = value.maximum + return _freeze_table("coupling_limits", columns) + + +def _build_template_references( + values: tuple[RecurrenceSemanticTemplateReferenceV1, ...], + strings: _StringCatalog, + digests: _DigestCatalog, +) -> RecurrenceColumnarTable: + columns = _allocate( + len(values), + kind_string_id=_U32, + template_id=_U32, + semantic_digest_id=_U32, + prepared_kernel_id=_U32, + ) + for row, value in enumerate(values): + columns["kind_string_id"][row] = strings.id(value.kind) + columns["template_id"][row] = value.template_id + columns["semantic_digest_id"][row] = digests.id(value.semantic_digest) + columns["prepared_kernel_id"][row] = ( + MISSING_U32 + if value.prepared_kernel_id is None + else value.prepared_kernel_id + ) + return _freeze_table("semantic_template_references", columns) + + +def _build_normalization( + value: RecurrenceNormalizationV1, + strings: _StringCatalog, + digests: _DigestCatalog, + factors: _FactorCatalog, +) -> RecurrenceColumnarTable: + columns = _allocate( + 1, + factor_id=_U32, + convention_string_id=_U32, + semantic_digest_id=_U32, + ) + columns["factor_id"][0] = factors.id(value.factor) + columns["convention_string_id"][0] = strings.id(value.convention) + columns["semantic_digest_id"][0] = digests.id(value.semantic_digest) + return _freeze_table("normalization", columns) + + +def _build_parameter_projection( + values: tuple[RecurrenceParameterProjectionV1, ...], + strings: _StringCatalog, +) -> RecurrenceColumnarTable: + columns = _allocate( + len(values), + runtime_slot=_U32, + runtime_name_string_id=_U32, + parameter_template_id=_U32, + prepared_parameter_id=_U32, + component=_U32, + ) + for row, value in enumerate(values): + columns["runtime_slot"][row] = value.runtime_slot + columns["runtime_name_string_id"][row] = strings.id(value.runtime_name) + columns["parameter_template_id"][row] = value.parameter_template_id + columns["prepared_parameter_id"][row] = ( + MISSING_U32 + if value.prepared_parameter_id is None + else value.prepared_parameter_id + ) + columns["component"][row] = value.component + return _freeze_table("parameter_projection", columns) + + +def _allocate( + row_count: int, + **specifications: np.dtype[Any] | tuple[np.dtype[Any], tuple[int, ...]], +) -> dict[str, np.ndarray[Any, Any]]: + _checked_u64(row_count, "table row count") + result: dict[str, np.ndarray[Any, Any]] = {} + for name, specification in specifications.items(): + if isinstance(specification, tuple): + dtype, tail_shape = specification + else: + dtype, tail_shape = specification, () + result[name] = np.empty((row_count, *tail_shape), dtype=dtype, order="C") + return result + + +def _freeze_table( + name: str, + columns: Mapping[str, np.ndarray[Any, Any]], +) -> RecurrenceColumnarTable: + row_counts = {len(values) for values in columns.values()} + row_count = row_counts.pop() if row_counts else 0 + if row_counts: + raise RecurrenceColumnarInputError( + f"table {name!r} columns have unequal lengths" + ) + frozen: list[RecurrenceColumn] = [] + for column_name, values in columns.items(): + if values.flags.owndata and values.flags.c_contiguous: + owned = values + else: + owned = np.array(values, dtype=values.dtype, order="C", copy=True) + owned.flags.writeable = False + frozen.append(RecurrenceColumn(column_name, owned)) + return RecurrenceColumnarTable(name, row_count, tuple(frozen)) + + +def _decode_flat_strings( + ranges: RecurrenceColumnarTable, + values: RecurrenceColumnarTable, +) -> tuple[str, ...]: + _validate_flat_ranges(ranges, values.row_count, "string catalog") + flat = values.column("value") + result: list[str] = [] + for row in range(ranges.row_count): + start = int(ranges.column("start")[row]) + count = int(ranges.column("count")[row]) + try: + result.append(bytes(flat[start : start + count]).decode("utf-8")) + except UnicodeDecodeError as error: + raise RecurrenceColumnarInputError( + "flat string catalog contains invalid UTF-8" + ) from error + return tuple(result) + + +def _column_dtype(table_name: str, column_name: str) -> np.dtype[Any]: + key = (table_name, column_name) + if key in _U8_COLUMN_KEYS: + return _U8 + if key in _U64_COLUMN_KEYS: + return _U64 + if key in _I32_COLUMN_KEYS: + return _I32 + return _U32 + + +def _validate_flat_ranges( + ranges: RecurrenceColumnarTable, + value_count: int, + context: str, +) -> None: + cursor = 0 + for row in range(ranges.row_count): + start = int(ranges.column("start")[row]) + count = int(ranges.column("count")[row]) + if start != cursor: + raise RecurrenceColumnarInputError( + f"{context} ranges are not contiguous and canonical" + ) + stop = start + count + if stop > value_count: + raise RecurrenceColumnarInputError( + f"{context} range references values outside its flat catalog" + ) + cursor = stop + if cursor != value_count: + raise RecurrenceColumnarInputError( + f"{context} does not account for every flat value" + ) + + +def _validate_parent_ranges( + table: RecurrenceColumnarTable, + start_column: str, + count_column: str, + child_count: int, + context: str, +) -> None: + cursor = 0 + for row in range(table.row_count): + start = int(table.column(start_column)[row]) + count = int(table.column(count_column)[row]) + if start != cursor or start + count > child_count: + raise RecurrenceColumnarInputError( + f"{context} ranges are not contiguous or are out of bounds" + ) + cursor = start + count + if cursor != child_count: + raise RecurrenceColumnarInputError( + f"{context} ranges do not cover every child row" + ) + + +def _validate_dense_ids(table: RecurrenceColumnarTable, column_name: str) -> None: + expected = np.arange(table.row_count, dtype=_U32) + if not np.array_equal(table.column(column_name), expected): + raise RecurrenceColumnarInputError( + f"table {table.name!r} column {column_name!r} is not dense" + ) + + +def _validate_bounded_columns( + table: RecurrenceColumnarTable, + columns: Sequence[str], + bound: int, +) -> None: + for column_name in columns: + values = table.column(column_name) + if values.size and int(values.max()) >= bound: + raise RecurrenceColumnarInputError( + f"{table.name}.{column_name} references an absent row" + ) + + +def _validate_fraction(numerator: int, denominator: int, context: str) -> None: + if isinstance(numerator, bool) or not isinstance(numerator, int): + raise TypeError(f"{context} numerator must be an integer") + if isinstance(denominator, bool) or not isinstance(denominator, int): + raise TypeError(f"{context} denominator must be an integer") + if denominator <= 0: + raise RecurrenceColumnarInputError(f"{context} denominator must be positive") + if numerator == 0 and denominator != 1: + raise RecurrenceColumnarInputError(f"{context} zero must be encoded as 0/1") + if math.gcd(abs(numerator), denominator) != 1: + raise RecurrenceColumnarInputError(f"{context} must be reduced") + + +def _nonempty_text(value: str, context: str) -> str: + if not isinstance(value, str): + raise TypeError(f"{context} must be a string") + if not value: + raise RecurrenceColumnarInputError(f"{context} must not be empty") + return value + + +def _sha256_bytes(value: str, context: str) -> bytes: + if ( + not isinstance(value, str) + or len(value) != 64 + or any(character not in "0123456789abcdef" for character in value) + ): + raise RecurrenceColumnarInputError(f"{context} must be a lowercase SHA-256") + return bytes.fromhex(value) + + +def _validate_nonnegative_mask(value: int, context: str) -> None: + if isinstance(value, bool) or not isinstance(value, int): + raise TypeError(f"{context} must be an integer") + if value < 0: + raise RecurrenceColumnarInputError(f"{context} must be nonnegative") + + +def _validate_u32_sequence(values: Sequence[int], context: str) -> None: + for index, value in enumerate(values): + _checked_u32(value, f"{context}[{index}]") + + +def _validate_permutation(values: Sequence[int], size: int, context: str) -> None: + if tuple(sorted(values)) != tuple(range(size)): + raise RecurrenceColumnarInputError( + f"{context} must be a permutation of [0, {size})" + ) + + +def _validate_source_contract_permutation( + external_legs: Sequence[RecurrenceExternalLegV1], + permutation: Sequence[int], + context: str, +) -> None: + for representative_slot, target_slot in enumerate(permutation): + representative = external_legs[representative_slot] + target = external_legs[target_slot] + if representative.is_initial != target.is_initial: + raise RecurrenceColumnarInputError( + f"{context} maps source slot {representative_slot} across the " + "initial/final crossing boundary" + ) + representative_contracts = { + state.source_template_id: ( + state.momentum_sign, + state.crossing_phase.canonical_key, + ) + for state in representative.source_states + } + target_contracts = { + state.source_template_id: ( + state.momentum_sign, + state.crossing_phase.canonical_key, + ) + for state in target.source_states + } + if representative_contracts != target_contracts: + raise RecurrenceColumnarInputError( + f"{context} maps source slot {representative_slot} onto slot " + f"{target_slot} with a different source crossing contract" + ) + + +def _require_index(value: int, bound: int, context: str) -> int: + result = _checked_u32(value, context) + if result >= bound: + raise RecurrenceColumnarInputError( + f"{context} {result} is outside [0, {bound})" + ) + return result + + +def _checked_u32(value: int, context: str) -> int: + if ( + isinstance(value, bool) + or not isinstance(value, int) + or not 0 <= value <= MISSING_U32 + ): + raise RecurrenceColumnarInputError(f"{context} does not fit u32: {value!r}") + return value + + +def _checked_u64(value: int, context: str) -> int: + if ( + isinstance(value, bool) + or not isinstance(value, int) + or not 0 <= value < 1 << 64 + ): + raise RecurrenceColumnarInputError(f"{context} does not fit u64: {value!r}") + return value + + +def _checked_i32(value: int, context: str) -> int: + if ( + isinstance(value, bool) + or not isinstance(value, int) + or not -(1 << 31) <= value < 1 << 31 + ): + raise RecurrenceColumnarInputError(f"{context} does not fit i32: {value!r}") + return value + + +def _checked_concrete_spin_state(value: int, context: str) -> int: + result = _checked_i32(value, context) + if result == _DYNAMIC_UNION_SOURCE_SPIN_STATE: + raise RecurrenceColumnarInputError( + f"{context} uses the reserved dynamic-union sentinel" + ) + return result + + +def _hash_text(digest: Any, value: str) -> None: + encoded = value.encode("utf-8") + digest.update(len(encoded).to_bytes(8, "little")) + digest.update(encoded) + + +def _hash_columnar_tables( + digest: Any, + tables: Sequence[RecurrenceColumnarTable], +) -> None: + digest.update(len(tables).to_bytes(8, "little")) + for table in tables: + _hash_text(digest, table.name) + digest.update(table.row_count.to_bytes(8, "little")) + digest.update(len(table.columns).to_bytes(4, "little")) + for column in table.columns: + _hash_text(digest, column.name) + _hash_text(digest, column.values.dtype.str) + digest.update(len(column.values.shape).to_bytes(1, "little")) + for dimension in column.values.shape: + digest.update(int(dimension).to_bytes(8, "little")) + digest.update(column.values.tobytes(order="C")) + + +def _decode_pairing_strings(payload: Any) -> tuple[str, ...]: + ranges = payload.table("string_ranges") + values = payload.table("string_bytes").column("value") + decoded: list[str] = [] + for row in range(ranges.row_count): + start = int(ranges.column("start")[row]) + count = int(ranges.column("count")[row]) + try: + decoded.append(bytes(values[start : start + count]).decode("utf-8")) + except UnicodeDecodeError as error: + raise RecurrenceColumnarInputError( + "fermion-pairing string catalog is not valid UTF-8" + ) from error + return tuple(decoded) + + +__all__ = [ + "MISSING_U32", + "RECURRENCE_BUILDER_INPUT_ABI", + "RECURRENCE_BUILDER_INPUT_SCHEMA_VERSION", + "ExactComplexRationalV1", + "LCColorSectorKind", + "RecurrenceBuilderInputV1", + "RecurrenceBuilderLogicalInputV1", + "RecurrenceColumn", + "RecurrenceColumnarInputError", + "RecurrenceColumnarTable", + "RecurrenceCouplingLimitV1", + "RecurrenceExternalLegV1", + "RecurrenceLCFlowLayout", + "RecurrenceLCOpenStringV1", + "RecurrenceNormalizationV1", + "RecurrenceParameterProjectionV1", + "RecurrencePhysicalLCSectorV1", + "RecurrencePublicLCFlowV1", + "RecurrenceReplayPartitionV1", + "RecurrenceReplayTargetV1", + "RecurrenceSelectedSourceCoverageV1", + "RecurrenceSemanticDigestV1", + "RecurrenceSemanticTemplateReferenceV1", + "RecurrenceSourceStateV1", + "build_recurrence_builder_input_v1", +] diff --git a/pyamplicol/src/pyamplicol/generation/recurrence_fermion_pairing.py b/pyamplicol/src/pyamplicol/generation/recurrence_fermion_pairing.py new file mode 100644 index 0000000000..05a84c197b --- /dev/null +++ b/pyamplicol/src/pyamplicol/generation/recurrence_fermion_pairing.py @@ -0,0 +1,801 @@ +# SPDX-License-Identifier: 0BSD +"""Exact external-fermion pairing rules for recurrence closure reconstruction. + +The catalog in this module is intentionally process-local and bounded. It +uses canonical process roles to bind external source slots to model-owned +current-state contracts, then enumerates only species-compatible Wick +pairings. Particle numbers are used solely to bind a process leg to its +authenticated state contract; all physics matching uses the contract's +species, antiparticle relation, statistics, orientation, and LC color shape. +""" + +from __future__ import annotations + +import hashlib +import itertools +import json +import math +from collections.abc import Iterable, Sequence +from dataclasses import dataclass +from typing import Final, Literal, TypeVar, cast + +from pyamplicol.models.recurrence_template import ( + CurrentStateTemplateV1, + ExactComplexRationalV1, + QuantumFlowTemplateV1, +) +from pyamplicol.processes.ir import CanonicalProcessIR, ProcessLegIR + +FermionColorOrientation = Literal["fundamental", "antifundamental"] +_ValueT = TypeVar("_ValueT") + +NO_FERMION_LINE: Final = 0xFFFF_FFFF +PAIRING_PROOF_ALGORITHM: Final = "canonical-external-fermion-pairing-v1" + + +class RecurrenceFermionPairingError(ValueError): + """A process/model contract cannot define a bounded exact pairing catalog.""" + + +@dataclass(frozen=True, slots=True) +class FermionPairingLimitsV1: + """Explicit guards against factorial closure-rule materialization.""" + + max_endpoints: int = 16 + max_pairings_per_species: int = 720 + max_total_rules: int = 4096 + + def __post_init__(self) -> None: + for name, value in ( + ("max_endpoints", self.max_endpoints), + ("max_pairings_per_species", self.max_pairings_per_species), + ("max_total_rules", self.max_total_rules), + ): + if isinstance(value, bool) or not isinstance(value, int) or value < 1: + raise ValueError(f"{name} must be a positive integer") + + +@dataclass(frozen=True, slots=True) +class ExternalFermionEndpointRowV1: + """One colored external fermion bound to canonical model state contracts.""" + + endpoint_id: int + source_slot: int + public_label: int + species_class_id: int + species_id: str + particle_orientation: str + color_orientation: FermionColorOrientation + state_template_ids: tuple[str, ...] + anti_state_template_ids: tuple[str, ...] + basis_ids: tuple[str, ...] + color_representations: tuple[int, ...] + contract_digest: str + + +@dataclass(frozen=True, slots=True) +class FermionPairingClassRowV1: + """One model-compatible fundamental/antifundamental endpoint set.""" + + class_id: int + species_class_id: int + species_id: str + fundamental_source_slots: tuple[int, ...] + antifundamental_source_slots: tuple[int, ...] + reference_pairings: tuple[tuple[int, int], ...] + pairing_count: int + proof_digest: str + + +@dataclass(frozen=True, slots=True) +class FermionPairingRuleRowV1: + """One exact product of species-local Wick pairings.""" + + rule_id: int + class_pairing_indices: tuple[tuple[int, int], ...] + endpoint_pairings: tuple[tuple[int, int], ...] + source_slot_permutation: tuple[int, ...] + lineage_by_source_slot: tuple[int, ...] + fermion_parity: int + exact_factor: ExactComplexRationalV1 + multiplicity: int + proof_algorithm: str + proof_digest: str + + +@dataclass(frozen=True, slots=True) +class FermionPairingCatalogV1: + """Immutable endpoint, pairing-class, and closure reconstruction rows.""" + + process_key: str + source_count: int + endpoints: tuple[ExternalFermionEndpointRowV1, ...] + pairing_classes: tuple[FermionPairingClassRowV1, ...] + rules: tuple[FermionPairingRuleRowV1, ...] + topology_digest: str + semantic_digest: str + + +@dataclass(frozen=True, slots=True) +class FermionPairingRootEndpointV1: + """One compact endpoint retained before any pairing is selected.""" + + source_slot: int + color_orientation: FermionColorOrientation + contract_digest: str + + +@dataclass(frozen=True, slots=True) +class FermionPairingRootClassV1: + """One model-proven pairing class without factorial rule expansion.""" + + species: str + proof_digest: str + fundamental_source_slots: tuple[int, ...] + antifundamental_source_slots: tuple[int, ...] + + +@dataclass(frozen=True, slots=True) +class FermionPairingRootsV1: + """O(external-leg) roots from which one runtime pairing can be derived.""" + + endpoints: tuple[FermionPairingRootEndpointV1, ...] + classes: tuple[FermionPairingRootClassV1, ...] + + +@dataclass(frozen=True, slots=True) +class _EndpointContract: + source_slot: int + public_label: int + species_id: str + particle_orientation: str + color_orientation: FermionColorOrientation + state_template_ids: tuple[str, ...] + anti_state_template_ids: tuple[str, ...] + basis_ids: tuple[str, ...] + color_representations: tuple[int, ...] + contract_digest: str + + +@dataclass(frozen=True, slots=True) +class _LocalPairing: + index: int + antifundamental_slots: tuple[int, ...] + parity: int + + +def build_recurrence_fermion_pairing_roots_v1( + process: CanonicalProcessIR, + current_states: Sequence[CurrentStateTemplateV1], + *, + quantum_flows: Sequence[QuantumFlowTemplateV1] = (), +) -> FermionPairingRootsV1: + """Derive compact model-compatible pairing roots without enumerating rules. + + The ordinary recurrence builder expands permutations because it publishes a + complete process schedule. The on-the-fly lane instead selects one pairing + at runtime, so retaining endpoints and compatibility classes is sufficient + and scales linearly with the external process. No resource-policy ceiling + belongs at this semantic projection boundary. + """ + + if not isinstance(process, CanonicalProcessIR): + raise TypeError("fermion pairing roots require a CanonicalProcessIR") + states = tuple(current_states) + if any(not isinstance(state, CurrentStateTemplateV1) for state in states): + raise TypeError( + "fermion pairing root current states must be CurrentStateTemplateV1 rows" + ) + flows = tuple(quantum_flows) + if any(not isinstance(flow, QuantumFlowTemplateV1) for flow in flows): + raise TypeError( + "fermion pairing root quantum flows must be QuantumFlowTemplateV1 rows" + ) + + contracts = tuple( + contract + for source_slot, leg in enumerate(process.legs) + if (contract := _derive_endpoint(source_slot, leg, states)) is not None + ) + compatibility_ids = _fermion_compatibility_ids(states, flows) + grouped: dict[str, list[_EndpointContract]] = {} + for contract in contracts: + species = compatibility_ids.get(contract.species_id, contract.species_id) + grouped.setdefault(species, []).append(contract) + + classes: list[FermionPairingRootClassV1] = [] + for species, group in sorted( + grouped.items(), + key=lambda item: tuple(sorted(row.source_slot for row in item[1])), + ): + fundamental = tuple( + sorted( + row.source_slot + for row in group + if row.color_orientation == "fundamental" + ) + ) + antifundamental = tuple( + sorted( + row.source_slot + for row in group + if row.color_orientation == "antifundamental" + ) + ) + if not fundamental or len(fundamental) != len(antifundamental): + raise RecurrenceFermionPairingError( + "incompatible fermion species endpoints: species contract " + f"{species!r} has {len(fundamental)} fundamental and " + f"{len(antifundamental)} antifundamental endpoints" + ) + proof_payload = { + "algorithm": PAIRING_PROOF_ALGORITHM, + "antifundamental_source_slots": antifundamental, + "fundamental_source_slots": fundamental, + "reference_pairings": tuple( + zip(fundamental, antifundamental, strict=True) + ), + "species_contract_digests": tuple( + sorted(row.contract_digest for row in group) + ), + } + classes.append( + FermionPairingRootClassV1( + species=species, + proof_digest=_digest(proof_payload), + fundamental_source_slots=fundamental, + antifundamental_source_slots=antifundamental, + ) + ) + + return FermionPairingRootsV1( + endpoints=tuple( + FermionPairingRootEndpointV1( + source_slot=contract.source_slot, + color_orientation=contract.color_orientation, + contract_digest=contract.contract_digest, + ) + for contract in sorted(contracts, key=lambda row: row.source_slot) + ), + classes=tuple(classes), + ) + + +def build_recurrence_fermion_pairing_catalog_v1( + process: CanonicalProcessIR, + current_states: Sequence[CurrentStateTemplateV1], + *, + quantum_flows: Sequence[QuantumFlowTemplateV1] = (), + limits: FermionPairingLimitsV1 | None = None, +) -> FermionPairingCatalogV1: + """Derive all bounded, model-compatible external-fermion pairings. + + Color-singlet fermions do not define LC open-string endpoints and are + intentionally absent. A process with no colored fermion endpoints has one + trivial parity-even rule, which is convenient for later closure lowering. + + Fermion species connected by an authenticated model quantum flow share a + pairing class. This is required for charged-current lines, where a + fundamental endpoint can carry a different species from its + antifundamental endpoint. In the absence of such a witness the original + same-species rule remains exact. + """ + + if not isinstance(process, CanonicalProcessIR): + raise TypeError("fermion pairing requires a CanonicalProcessIR") + active_limits = limits or FermionPairingLimitsV1() + if not isinstance(active_limits, FermionPairingLimitsV1): + raise TypeError("fermion pairing limits must be FermionPairingLimitsV1") + states = tuple(current_states) + if any(not isinstance(state, CurrentStateTemplateV1) for state in states): + raise TypeError( + "fermion pairing current states must be CurrentStateTemplateV1 rows" + ) + flows = tuple(quantum_flows) + if any(not isinstance(flow, QuantumFlowTemplateV1) for flow in flows): + raise TypeError( + "fermion pairing quantum flows must be QuantumFlowTemplateV1 rows" + ) + + contracts = tuple( + contract + for source_slot, leg in enumerate(process.legs) + if (contract := _derive_endpoint(source_slot, leg, states)) is not None + ) + if len(contracts) > active_limits.max_endpoints: + raise RecurrenceFermionPairingError( + "colored fermion endpoint count exceeds the configured pairing bound: " + f"{len(contracts)} > {active_limits.max_endpoints}" + ) + + compatibility_ids = _fermion_compatibility_ids(states, flows) + grouped: dict[str, list[_EndpointContract]] = {} + for contract in contracts: + grouped.setdefault( + compatibility_ids.get(contract.species_id, contract.species_id), + [], + ).append(contract) + ordered_groups = sorted( + grouped.values(), + key=lambda group: tuple(sorted(item.source_slot for item in group)), + ) + species_class_ids = { + compatibility_ids.get(group[0].species_id, group[0].species_id): class_id + for class_id, group in enumerate(ordered_groups) + } + endpoints = tuple( + ExternalFermionEndpointRowV1( + endpoint_id=endpoint_id, + source_slot=contract.source_slot, + public_label=contract.public_label, + species_class_id=species_class_ids[ + compatibility_ids.get(contract.species_id, contract.species_id) + ], + species_id=compatibility_ids.get( + contract.species_id, contract.species_id + ), + particle_orientation=contract.particle_orientation, + color_orientation=contract.color_orientation, + state_template_ids=contract.state_template_ids, + anti_state_template_ids=contract.anti_state_template_ids, + basis_ids=contract.basis_ids, + color_representations=contract.color_representations, + contract_digest=contract.contract_digest, + ) + for endpoint_id, contract in enumerate( + sorted(contracts, key=lambda item: item.source_slot) + ) + ) + + pairing_classes: list[FermionPairingClassRowV1] = [] + local_options: list[tuple[_LocalPairing, ...]] = [] + total_rule_count = 1 + for class_id, group in enumerate(ordered_groups): + class_species_id = compatibility_ids.get( + group[0].species_id, group[0].species_id + ) + fundamental = tuple( + sorted( + item.source_slot + for item in group + if item.color_orientation == "fundamental" + ) + ) + antifundamental = tuple( + sorted( + item.source_slot + for item in group + if item.color_orientation == "antifundamental" + ) + ) + if len(fundamental) != len(antifundamental): + raise RecurrenceFermionPairingError( + "incompatible fermion species endpoints: species contract " + f"{class_species_id!r} has {len(fundamental)} fundamental " + f"and {len(antifundamental)} antifundamental endpoints" + ) + pairing_count = math.factorial(len(fundamental)) + if pairing_count > active_limits.max_pairings_per_species: + raise RecurrenceFermionPairingError( + "model-compatible fermion pairings exceed the configured bound: " + f"{pairing_count} > {active_limits.max_pairings_per_species}" + ) + total_rule_count *= pairing_count + if total_rule_count > active_limits.max_total_rules: + raise RecurrenceFermionPairingError( + "combined fermion pairing rules exceed the configured bound: " + f"{total_rule_count} > {active_limits.max_total_rules}" + ) + + reference = tuple(zip(fundamental, antifundamental, strict=True)) + class_payload = { + "algorithm": PAIRING_PROOF_ALGORITHM, + "antifundamental_source_slots": antifundamental, + "fundamental_source_slots": fundamental, + "reference_pairings": reference, + "species_contract_digests": tuple( + sorted(item.contract_digest for item in group) + ), + } + pairing_classes.append( + FermionPairingClassRowV1( + class_id=class_id, + species_class_id=class_id, + species_id=class_species_id, + fundamental_source_slots=fundamental, + antifundamental_source_slots=antifundamental, + reference_pairings=reference, + pairing_count=pairing_count, + proof_digest=_digest(class_payload), + ) + ) + local_options.append( + tuple( + _LocalPairing( + index=index, + antifundamental_slots=permutation, + parity=_permutation_parity(antifundamental, permutation), + ) + for index, permutation in enumerate( + itertools.permutations(antifundamental) + ) + ) + ) + + rules = _build_rules( + tuple(pairing_classes), + tuple(local_options), + source_count=len(process.legs), + ) + topology_payload = _topology_payload( + len(process.legs), endpoints, pairing_classes, rules + ) + semantic_payload = { + "process_key": process.key, + "topology": topology_payload, + "endpoint_contracts": tuple( + { + "anti_state_template_ids": endpoint.anti_state_template_ids, + "basis_ids": endpoint.basis_ids, + "color_representations": endpoint.color_representations, + "contract_digest": endpoint.contract_digest, + "particle_orientation": endpoint.particle_orientation, + "species_id": endpoint.species_id, + "state_template_ids": endpoint.state_template_ids, + } + for endpoint in endpoints + ), + } + return FermionPairingCatalogV1( + process_key=process.key, + source_count=len(process.legs), + endpoints=endpoints, + pairing_classes=tuple(pairing_classes), + rules=rules, + topology_digest=_digest(topology_payload), + semantic_digest=_digest(semantic_payload), + ) + + +def _fermion_compatibility_ids( + states: tuple[CurrentStateTemplateV1, ...], + flows: tuple[QuantumFlowTemplateV1, ...], +) -> dict[str, str]: + """Return exact model-proven fermion-line compatibility components.""" + + state_by_id = {state.template_id: state for state in states} + fermion_species = tuple( + sorted({state.species_id for state in states if state.statistics == "fermion"}) + ) + parent = {species_id: species_id for species_id in fermion_species} + + def find(species_id: str) -> str: + root = species_id + while parent[root] != root: + root = parent[root] + while parent[species_id] != species_id: + next_species = parent[species_id] + parent[species_id] = root + species_id = next_species + return root + + def union(left: str, right: str) -> None: + left_root = find(left) + right_root = find(right) + if left_root == right_root: + return + low, high = sorted((left_root, right_root)) + parent[high] = low + + for flow in flows: + referenced = ( + *flow.input_state_template_ids, + flow.result_state_template_id, + ) + try: + flow_states = tuple(state_by_id[template_id] for template_id in referenced) + except KeyError as error: + raise RecurrenceFermionPairingError( + "fermion pairing quantum flow references an unknown current-state " + f"template {error.args[0]!r}" + ) from error + species = tuple( + sorted( + { + state.species_id + for state in flow_states + if state.statistics == "fermion" + } + ) + ) + if len(species) == 2: + union(*species) + + components: dict[str, list[str]] = {} + for species_id in fermion_species: + components.setdefault(find(species_id), []).append(species_id) + result: dict[str, str] = {} + for members in components.values(): + canonical_members = tuple(sorted(members)) + compatibility_id = ( + canonical_members[0] + if len(canonical_members) == 1 + else f"model-fermion-transition-component:{_digest(canonical_members)}" + ) + result.update( + (species_id, compatibility_id) for species_id in canonical_members + ) + return result + + +def _derive_endpoint( + source_slot: int, + leg: ProcessLegIR, + states: tuple[CurrentStateTemplateV1, ...], +) -> _EndpointContract | None: + statistics_is_fermion = leg.statistics == "fermion" + family_is_fermion = leg.wavefunction_family == "fermion" + if statistics_is_fermion != family_is_fermion: + raise RecurrenceFermionPairingError( + f"external leg {leg.label} has inconsistent fermion contracts" + ) + if not statistics_is_fermion: + return None + if leg.color_role == "singlet": + return None + if leg.color_role not in {"fundamental", "antifundamental"}: + raise RecurrenceFermionPairingError( + f"fermion leg {leg.label} has unsupported LC color role {leg.color_role!r}" + ) + if leg.outgoing_pdg is None: + raise RecurrenceFermionPairingError( + f"fermion leg {leg.label} has no concrete outgoing state identifier" + ) + + candidates = tuple( + state + for state in states + if state.particle_id == leg.outgoing_pdg + and state.statistics == "fermion" + and state.auxiliary_kind is None + ) + if not candidates: + raise RecurrenceFermionPairingError( + f"fermion leg {leg.label} has no canonical current-state contract" + ) + species = _one_value( + (state.species_id for state in candidates), + f"fermion leg {leg.label} species", + ) + orientation = _one_value( + (state.orientation for state in candidates), + f"fermion leg {leg.label} particle orientation", + ) + anti_particle_id = _one_value( + (state.anti_particle_id for state in candidates), + f"fermion leg {leg.label} antiparticle state", + ) + expected_shape = { + "fundamental": "fundamental-open-string", + "antifundamental": "antifundamental-open-string", + }[leg.color_role] + if any(state.lc_color_shape_kind != expected_shape for state in candidates): + raise RecurrenceFermionPairingError( + f"fermion leg {leg.label} color role disagrees with its state contract" + ) + if leg.source_orientation != "inclusive" and orientation != leg.source_orientation: + raise RecurrenceFermionPairingError( + f"fermion leg {leg.label} source orientation disagrees with its state " + "contract" + ) + if orientation not in {"particle", "antiparticle"}: + raise RecurrenceFermionPairingError( + "self-conjugate or inclusive fermion pairing is not certified" + ) + + anti_orientation = "antiparticle" if orientation == "particle" else "particle" + anti_candidates = tuple( + state + for state in states + if state.particle_id == anti_particle_id + and state.anti_particle_id == leg.outgoing_pdg + and state.species_id == species + and state.statistics == "fermion" + and state.orientation == anti_orientation + and state.auxiliary_kind is None + ) + if not anti_candidates: + raise RecurrenceFermionPairingError( + f"fermion leg {leg.label} has no involutive antiparticle state contract" + ) + + state_template_ids = tuple(sorted(state.template_id for state in candidates)) + anti_state_template_ids = tuple( + sorted(state.template_id for state in anti_candidates) + ) + basis_ids = tuple(sorted({state.basis for state in candidates})) + color_representations = tuple( + sorted({state.color_representation for state in candidates}) + ) + payload = { + "anti_state_contract_digests": tuple( + sorted(state.semantic_digest for state in anti_candidates) + ), + "color_orientation": leg.color_role, + "public_label": leg.label, + "source_slot": source_slot, + "state_contract_digests": tuple( + sorted(state.semantic_digest for state in candidates) + ), + } + return _EndpointContract( + source_slot=source_slot, + public_label=int(leg.label), + species_id=species, + particle_orientation=orientation, + color_orientation=cast(FermionColorOrientation, leg.color_role), + state_template_ids=state_template_ids, + anti_state_template_ids=anti_state_template_ids, + basis_ids=basis_ids, + color_representations=color_representations, + contract_digest=_digest(payload), + ) + + +def _build_rules( + pairing_classes: tuple[FermionPairingClassRowV1, ...], + local_options: tuple[tuple[_LocalPairing, ...], ...], + *, + source_count: int, +) -> tuple[FermionPairingRuleRowV1, ...]: + combinations: Sequence[tuple[_LocalPairing, ...]] = ( + tuple(itertools.product(*local_options)) if local_options else ((),) + ) + rules: list[FermionPairingRuleRowV1] = [] + for rule_id, selected in enumerate(combinations): + endpoint_pairings: list[tuple[int, int]] = [] + source_permutation = list(range(source_count)) + parity = 1 + class_pairing_indices: list[tuple[int, int]] = [] + for pairing_class, local in zip(pairing_classes, selected, strict=True): + class_pairing_indices.append((pairing_class.class_id, local.index)) + parity *= local.parity + endpoint_pairings.extend( + zip( + pairing_class.fundamental_source_slots, + local.antifundamental_slots, + strict=True, + ) + ) + for reference_slot, selected_slot in zip( + pairing_class.antifundamental_source_slots, + local.antifundamental_slots, + strict=True, + ): + source_permutation[reference_slot] = selected_slot + endpoint_pairings.sort() + lineage = [NO_FERMION_LINE] * source_count + for line_id, (fundamental_slot, antifundamental_slot) in enumerate( + endpoint_pairings + ): + lineage[fundamental_slot] = line_id + lineage[antifundamental_slot] = line_id + rule_payload = { + "algorithm": PAIRING_PROOF_ALGORITHM, + "class_pairing_indices": tuple(class_pairing_indices), + "endpoint_pairings": tuple(endpoint_pairings), + "fermion_parity": parity, + "lineage_by_source_slot": tuple(lineage), + "source_slot_permutation": tuple(source_permutation), + } + rules.append( + FermionPairingRuleRowV1( + rule_id=rule_id, + class_pairing_indices=tuple(class_pairing_indices), + endpoint_pairings=tuple(endpoint_pairings), + source_slot_permutation=tuple(source_permutation), + lineage_by_source_slot=tuple(lineage), + fermion_parity=parity, + exact_factor=ExactComplexRationalV1(parity, 1, 0, 1), + multiplicity=1, + proof_algorithm=PAIRING_PROOF_ALGORITHM, + proof_digest=_digest(rule_payload), + ) + ) + return tuple(rules) + + +def _permutation_parity( + reference: tuple[int, ...], + candidate: tuple[int, ...], +) -> int: + if len(reference) != len(candidate) or set(reference) != set(candidate): + raise RecurrenceFermionPairingError( + "fermion parity requires a permutation of the reference endpoints" + ) + ranks = {value: index for index, value in enumerate(reference)} + permutation = tuple(ranks[value] for value in candidate) + inversions = sum( + left > right + for index, left in enumerate(permutation) + for right in permutation[index + 1 :] + ) + return -1 if inversions % 2 else 1 + + +def _one_value(values: Iterable[_ValueT], context: str) -> _ValueT: + materialized = tuple(values) + unique = set(materialized) + if len(unique) != 1: + raise RecurrenceFermionPairingError( + f"{context} must be uniquely defined, found {materialized!r}" + ) + return materialized[0] + + +def _topology_payload( + source_count: int, + endpoints: tuple[ExternalFermionEndpointRowV1, ...], + pairing_classes: Sequence[FermionPairingClassRowV1], + rules: tuple[FermionPairingRuleRowV1, ...], +) -> dict[str, object]: + return { + "algorithm": PAIRING_PROOF_ALGORITHM, + "source_count": source_count, + "endpoints": tuple( + ( + endpoint.source_slot, + endpoint.public_label, + endpoint.species_class_id, + endpoint.color_orientation, + ) + for endpoint in endpoints + ), + "pairing_classes": tuple( + ( + row.class_id, + row.fundamental_source_slots, + row.antifundamental_source_slots, + row.reference_pairings, + row.pairing_count, + ) + for row in pairing_classes + ), + "rules": tuple( + ( + rule.class_pairing_indices, + rule.endpoint_pairings, + rule.source_slot_permutation, + rule.lineage_by_source_slot, + rule.fermion_parity, + ) + for rule in rules + ), + } + + +def _digest(payload: object) -> str: + encoded = json.dumps( + payload, + ensure_ascii=True, + separators=(",", ":"), + sort_keys=True, + ).encode("ascii") + return hashlib.sha256(encoded).hexdigest() + + +__all__ = [ + "NO_FERMION_LINE", + "PAIRING_PROOF_ALGORITHM", + "ExternalFermionEndpointRowV1", + "FermionPairingCatalogV1", + "FermionPairingClassRowV1", + "FermionPairingLimitsV1", + "FermionPairingRootClassV1", + "FermionPairingRootEndpointV1", + "FermionPairingRootsV1", + "FermionPairingRuleRowV1", + "RecurrenceFermionPairingError", + "build_recurrence_fermion_pairing_catalog_v1", + "build_recurrence_fermion_pairing_roots_v1", +] diff --git a/pyamplicol/src/pyamplicol/generation/recurrence_numerical_current_warmup.py b/pyamplicol/src/pyamplicol/generation/recurrence_numerical_current_warmup.py new file mode 100644 index 0000000000..94f27ec804 --- /dev/null +++ b/pyamplicol/src/pyamplicol/generation/recurrence_numerical_current_warmup.py @@ -0,0 +1,4150 @@ +# SPDX-License-Identifier: 0BSD +"""Authenticated high-precision current reuse for recurrence Direct plans. + +The first native lowering is an unpublished baseline. This module evaluates +that exact plan at disjoint Decimal/Symbolica probe domains, certifies only +equal, opposite, and zero relations, and emits canonical evidence for a +second native lowering. No binary64 runtime result participates in discovery. +""" + +from __future__ import annotations + +import hashlib +import json +import pickle +import struct +import tempfile +import time +import zlib +from collections import defaultdict +from collections.abc import Iterator, Mapping, Sequence +from dataclasses import dataclass, field, replace +from decimal import ROUND_HALF_EVEN, Decimal, localcontext +from fractions import Fraction +from math import copysign, isfinite +from types import MappingProxyType +from typing import TYPE_CHECKING, BinaryIO, Literal, cast + +from pyamplicol.api.errors import ArtifactError +from pyamplicol.models.base import Model +from pyamplicol.runtime.recurrence_exact._execution import ( + _evaluate_contracted_point, + _evaluate_replay_point, + _evaluate_union_point, +) +from pyamplicol.runtime.recurrence_exact._plan import _RecurrenceExactPlan +from pyamplicol.runtime.recurrence_exact._plan_v2 import ( + DIRECT_NONE_U32, + _Current, + _RecurrenceExactSectionsV1, +) +from pyamplicol.runtime.symbolica_exact import _ComplexDecimal + +from .dag_equivalence import ( + NUMERICAL_CURRENT_RELATION_WARNING, + NUMERICAL_CURRENT_RELATION_WARNING_CODE, +) +from .numerical_candidate_index import ( + NumericalObservationCandidateIndex, + build_numerical_observation_candidate_index, + numerical_observation_tolerance_window_ids, +) +from .validation import ( + ValidationPointRecord, + build_process_validation_point, + rotate_validation_point, +) + +if TYPE_CHECKING: + from pyamplicol.processes.ir import CanonicalProcessIR + +_CAPTURE_ABI = "pyamplicol-recurrence-current-observation-capture-v2" +_EVIDENCE_ABI = "pyamplicol-recurrence-numerical-current-evidence-v3" +_SOURCE_ABI = "pyamplicol-recurrence-numerical-current-source-v3" +_WARMUP_ABI = "pyamplicol-recurrence-numerical-current-warmup-v1" +_RELATION_SET_ABI = "pyamplicol-authenticated-numerical-current-relation-set-v2" +_OBSERVATION_BATCH_ABI = "pyamplicol-recurrence-current-observation-batch-v2" +_PARAMETER_CONTEXT_ABI = "pyamplicol-recurrence-parameter-context-v1" +_PARAMETER_SCHEMA_ABI = "pyamplicol-recurrence-runtime-parameter-schema-v2" +_RELATION_OBSERVATION_ABI = "pyamplicol-recurrence-relation-observation-v2" +_DECISION_CHAIN_ABI = "pyamplicol-recurrence-numerical-decision-chain-v1" +_REJECTION_CHAIN_ABI = "pyamplicol-recurrence-rejected-numerical-decision-chain-v1" +_PERSISTED_EVIDENCE_ABI = "pyamplicol-recurrence-numerical-persisted-evidence-v1" +# Select a sequential spool for large captures; this is not a memory ceiling. +_RAW_EVIDENCE_MEMORY_TARGET_BYTES = 1 << 30 +_COMPRESSED_EVIDENCE_MAGIC = b"PACNCEZ1" +_COMPRESSED_EVIDENCE_HEADER = struct.Struct(">8sQ32s") +_COMPRESSED_EVIDENCE_ENCODING = "zlib-canonical-json-v1" +_RAW_EVIDENCE_ENCODING = "canonical-json-v3" +_SPOOLED_CAPTURE_COMPRESSION_RESERVE_BYTES = 8 << 20 +_SPOOLED_CANDIDATE_INDEX_BYTES_PER_CURRENT = 1_024 +# The producer-side peak ends when the canonical bytes have been encoded and +# the complete Decimal graphs are detached. The per-scalar allowance covers +# the Python Decimal/tuple capture graph and encoder temporaries; the row +# allowance covers capture dictionaries and rows. These estimates select +# resident or spooled storage; native validates the observations independently +# without imposing a fixed memory ceiling. +_RAW_EVIDENCE_SCALAR_RESIDENT_BYTES = 640 +_RAW_EVIDENCE_ROW_RESIDENT_BYTES = 512 +_RAW_EVIDENCE_FIXED_RESERVE_BYTES = 32 << 20 +_RAW_EVIDENCE_WIRE_PEAK_COPIES = 2 +_MIN_RAW_EVIDENCE_WIRE_BYTES = 1 << 20 +_RECURRENCE_CERTIFICATE_ALGORITHM = ( + "authenticated-independent-recursive-decimal-raw-probes-v2" +) +_MAX_REJECTED_DIAGNOSTICS = 32 +_CANDIDATE_INDEX_ALGORITHM = "complete-contract-anchor-tolerance-window-v1" +_APPLICATION_SCOPE_ABI = "pyamplicol-recurrence-numerical-relation-application-scope-v1" +NUMERICAL_RELATION_CORRECTNESS_ABI = ( + "pyamplicol-numerical-current-relation-correctness-v1" +) +NUMERICAL_RELATION_FALLBACK_ABI = "pyamplicol-numerical-current-reuse-fallback-v1" +EVIDENCE_ENVELOPE_FALLBACK_REASON = "evidence-envelope-fallback" +EVIDENCE_ENVELOPE_FALLBACK_WARNING = ( + "Numerical-current reuse was disabled because its optional evidence " + "geometry exceeds the bounded memory envelope; recurrence generation " + "continues without numerical relations." +) + +_RelationKind = Literal["equal", "opposite", "zero"] +_Mode = Literal["diagnostic", "certified-reuse"] +_Point = tuple[tuple[Decimal, Decimal, Decimal, Decimal], ...] +_CurrentContract = tuple[object, ...] +_EvidenceEncoding = Literal["canonical-json-v3", "zlib-canonical-json-v1"] +_Residuals = tuple[Fraction, Fraction, Fraction] +_ResidualStrings = tuple[str, str, str] +_Tolerance = Decimal | Fraction + + +@dataclass(frozen=True, slots=True) +class _RawEvidenceStorageGeometry: + scalar_count: int + row_count: int + canonical_byte_limit: int | None + encoding: _EvidenceEncoding + producer_resident_upper_bound: int + + +@dataclass(frozen=True, slots=True) +class RecurrenceNumericalEvidenceGeometry: + """Allocation-free shape of one optional numerical-evidence capture.""" + + current_count: int + component_count: int + candidate_probe_count: int + verification_probe_count: int + runtime_parameter_count: int + scalar_count: int + row_count: int + + @classmethod + def from_counts( + cls, + *, + current_count: int, + component_count: int, + candidate_probe_count: int, + verification_probe_count: int, + runtime_parameter_count: int, + ) -> RecurrenceNumericalEvidenceGeometry: + values = ( + current_count, + component_count, + candidate_probe_count, + verification_probe_count, + runtime_parameter_count, + ) + if ( + any(type(value) is not int or value < 0 for value in values) + or candidate_probe_count == 0 + or verification_probe_count == 0 + ): + raise ValueError("recurrence raw-evidence probe geometry is invalid") + point_count = candidate_probe_count + verification_probe_count + return cls( + current_count=current_count, + component_count=component_count, + candidate_probe_count=candidate_probe_count, + verification_probe_count=verification_probe_count, + runtime_parameter_count=runtime_parameter_count, + scalar_count=( + 2 * component_count * point_count + + runtime_parameter_count * point_count + ), + row_count=current_count * 2 + runtime_parameter_count, + ) + + def to_json_dict(self) -> dict[str, int]: + return { + "current_count": self.current_count, + "component_count": self.component_count, + "candidate_probe_count": self.candidate_probe_count, + "verification_probe_count": self.verification_probe_count, + "runtime_parameter_count": self.runtime_parameter_count, + "scalar_count": self.scalar_count, + "row_count": self.row_count, + } + + def spooled_producer_resident_upper_bound(self) -> int: + """Recompute the sequential-spool producer bound for this geometry.""" + + return _spooled_capture_memory_upper_bound( + current_count=self.current_count, + component_count=self.component_count, + maximum_probe_count=max( + self.candidate_probe_count, + self.verification_probe_count, + ), + runtime_parameter_count=self.runtime_parameter_count, + ) + + +class RecurrenceNumericalEvidenceEnvelopeExceeded(ValueError): + """Optional recurrence evidence cannot fit its authenticated envelope.""" + + def __init__( + self, + geometry: RecurrenceNumericalEvidenceGeometry, + *, + memory_envelope_bytes: int, + spooled_producer_resident_bytes: int, + raw_reason: str, + ) -> None: + self.geometry = geometry + self.memory_envelope_bytes = memory_envelope_bytes + self.spooled_producer_resident_bytes = spooled_producer_resident_bytes + self.raw_reason = raw_reason + values = geometry.to_json_dict() + super().__init__( + "recurrence optional numerical evidence cannot fit its bounded " + "resident or transport envelope " + f"(currents={values['current_count']}, " + f"components={values['component_count']}, " + f"candidate_points={values['candidate_probe_count']}, " + f"verification_points={values['verification_probe_count']}, " + f"runtime_parameters={values['runtime_parameter_count']}, " + f"scalars={values['scalar_count']}, rows={values['row_count']}, " + "spooled_producer_resident=" + f"{spooled_producer_resident_bytes}, raw_reason={raw_reason})" + ) + + def to_json_dict(self) -> dict[str, object]: + return { + "reason": EVIDENCE_ENVELOPE_FALLBACK_REASON, + "memory_envelope_bytes": self.memory_envelope_bytes, + "spooled_producer_resident_bytes": (self.spooled_producer_resident_bytes), + "geometry": self.geometry.to_json_dict(), + } + + +class _RawEvidenceWireEnvelopeExceeded(ValueError): + """The validated raw-wire shape cannot fit its bounded envelope.""" + + +class _CanonicalEvidenceCapacityExceeded(ValueError): + """A valid optional-evidence encoding cannot fit its bounded transport.""" + + +class _IndependentVerificationRequired(Exception): + """Candidate screening found a hypothesis that needs independent probes.""" + + +_NATIVE_OPTIONAL_EVIDENCE_CAPACITY_MARKERS = ( + "raw numerical evidence exceeds the explicit pre-metadata 1 GiB resident " + "memory envelope", + "compressed numerical evidence exceeds the explicit pre-shape 1 GiB resident " + "memory envelope", + "compressed numerical relation evidence exceeds the explicit native 1 GiB " + "resident memory envelope", + "raw numerical evidence exceeds the streaming 1 GiB resident memory envelope", + "raw numerical capture geometry leaves no minimum canonical wire reserve " + "inside the 1 GiB resident memory envelope", + "raw numerical sequential-spool capture geometry exceeds the explicit 1 GiB " + "resident memory envelope", + "could not reserve bounded decompressed numerical evidence", +) + + +@dataclass(frozen=True, slots=True) +class _WarmupStaticContext: + current_dimensions: Mapping[int, int] + current_contracts: tuple[_CurrentContract, ...] + runtime_defaults: tuple[Decimal, ...] + runtime_parameter_schema: Mapping[str, object] + runtime_parameter_schema_sha256: str + source_semantics: Mapping[str, object] + source_semantics_sha256: str + + +class _SpooledObservationMapping(Mapping[int, tuple[_ComplexDecimal, ...]]): + """Trusted temporary row store with bounded one-row materialization.""" + + def __init__( + self, + observations: Mapping[int, Sequence[_ComplexDecimal]], + *, + candidate_indexes: Mapping[ + _CurrentContract, + NumericalObservationCandidateIndex[Fraction], + ] + | None = None, + ) -> None: + self._stream: BinaryIO = tempfile.TemporaryFile( # noqa: SIM115 + prefix="pyamplicol-recurrence-current-observations-", + ) + self._rows: dict[int, tuple[int, int]] = {} + self._candidate_indexes_row: tuple[int, int] | None = None + self._closed = False + try: + for current_id in sorted(observations): + offset = self._stream.tell() + pickle.dump( + tuple(observations[current_id]), + self._stream, + protocol=pickle.HIGHEST_PROTOCOL, + ) + self._rows[current_id] = ( + offset, + self._stream.tell() - offset, + ) + if candidate_indexes is not None: + offset = self._stream.tell() + pickle.dump( + dict(candidate_indexes), + self._stream, + protocol=pickle.HIGHEST_PROTOCOL, + ) + self._candidate_indexes_row = ( + offset, + self._stream.tell() - offset, + ) + self._stream.flush() + except BaseException: + self.close() + raise + + @property + def candidate_indexes( + self, + ) -> ( + Mapping[ + _CurrentContract, + NumericalObservationCandidateIndex[Fraction], + ] + | None + ): + if self._candidate_indexes_row is None: + return None + if self._closed: + raise RuntimeError("recurrence observation spool is closed") + offset, length = self._candidate_indexes_row + self._stream.seek(offset) + payload = self._stream.read(length) + if len(payload) != length: + raise ValueError("recurrence candidate-index spool row is truncated") + return cast( + dict[ + _CurrentContract, + NumericalObservationCandidateIndex[Fraction], + ], + pickle.loads(payload), + ) + + def __getitem__(self, current_id: int) -> tuple[_ComplexDecimal, ...]: + if self._closed: + raise RuntimeError("recurrence observation spool is closed") + try: + offset, length = self._rows[current_id] + except KeyError: + raise KeyError(current_id) from None + self._stream.seek(offset) + payload = self._stream.read(length) + if len(payload) != length: + raise ValueError("recurrence observation spool row is truncated") + values = pickle.loads(payload) + if not isinstance(values, tuple): + raise ValueError("recurrence observation spool row is malformed") + return cast(tuple[_ComplexDecimal, ...], values) + + def __iter__(self) -> Iterator[int]: + return iter(self._rows) + + def __len__(self) -> int: + return len(self._rows) + + def close(self) -> None: + if self._closed: + return + self._closed = True + self._stream.close() + + +class _CanonicalJsonStreamValue: + """A value that writes canonical JSON without first building a JSON graph.""" + + def iter_canonical_json_chunks(self) -> Iterator[bytes]: + raise NotImplementedError + + +@dataclass(frozen=True, slots=True) +class _ObservationValuesStream(_CanonicalJsonStreamValue): + values: Sequence[_ComplexDecimal] + + def iter_canonical_json_chunks(self) -> Iterator[bytes]: + yield b"[" + for index, (real, imaginary) in enumerate(self.values): + if index: + yield b"," + yield b"[" + yield from _iter_canonical_json_chunks(_decimal_string(real)) + yield b"," + yield from _iter_canonical_json_chunks(_decimal_string(imaginary)) + yield b"]" + yield b"]" + + +@dataclass(frozen=True, slots=True) +class _ObservationRowsStream(_CanonicalJsonStreamValue): + observations: Mapping[int, Sequence[_ComplexDecimal]] + dimensions: Mapping[int, int] + + def iter_canonical_json_chunks(self) -> Iterator[bytes]: + yield b"[" + for index, current_id in enumerate(sorted(self.observations)): + if index: + yield b"," + yield from _iter_canonical_json_chunks( + { + "current_id": current_id, + "dimension": self.dimensions[current_id], + "values": _ObservationValuesStream(self.observations[current_id]), + } + ) + yield b"]" + + +@dataclass(frozen=True, slots=True) +class _DimensionRowsStream(_CanonicalJsonStreamValue): + dimensions: Mapping[int, int] + + def iter_canonical_json_chunks(self) -> Iterator[bytes]: + yield b"[" + for index, (current_id, dimension) in enumerate( + sorted(self.dimensions.items()) + ): + if index: + yield b"," + yield from _iter_canonical_json_chunks( + {"current_id": current_id, "dimension": dimension} + ) + yield b"]" + + +@dataclass(frozen=True, slots=True) +class _CertificateRowsStream(_CanonicalJsonStreamValue): + certificates: Sequence[RecurrenceNumericalCurrentCertificate] + + def iter_canonical_json_chunks(self) -> Iterator[bytes]: + yield b"[" + for index, certificate in enumerate(self.certificates): + if index: + yield b"," + yield from _iter_canonical_json_chunks(certificate.to_json_dict()) + yield b"]" + + +@dataclass(frozen=True, slots=True) +class _MappingRowsStream(_CanonicalJsonStreamValue): + certificates: Sequence[RecurrenceNumericalCurrentCertificate] + + def iter_canonical_json_chunks(self) -> Iterator[bytes]: + yield b"[" + for index, certificate in enumerate(self.certificates): + if index: + yield b"," + yield from _iter_canonical_json_chunks(_mapping_payload(certificate)) + yield b"]" + + +@dataclass(frozen=True, slots=True) +class RecurrenceCurrentObservationCapture: + """Complete point-major exact current observations.""" + + precision_digits: int + points: tuple[ValidationPointRecord, ...] + point_sha256s: tuple[str, ...] + kinematic_sha256s: tuple[str, ...] + kinematic_binary64: tuple[tuple[str, ...], ...] + selector_contexts: tuple[object, ...] + context_sha256s: tuple[str, ...] + parameter_contexts: tuple[tuple[Decimal, ...], ...] + parameter_context_sha256s: tuple[str, ...] + observations: Mapping[int, tuple[_ComplexDecimal, ...]] + current_dimensions: Mapping[int, int] + runtime_parameter_schema_sha256: str + source_semantics_sha256: str + observation_batch_sha256: str + capture_contract_sha256: str + context_policy: str + full_current_count: int | None = None + complete_observations_retained: bool = True + + @property + def point_count(self) -> int: + return len(self.points) + + @property + def current_count(self) -> int: + return ( + len(self.observations) + if self.full_current_count is None + else self.full_current_count + ) + + def detached_for_replay( + self, + current_ids: Sequence[int], + ) -> RecurrenceCurrentObservationCapture: + """Retain only rows needed by the persisted certificate replay.""" + + selected = tuple(sorted(set(current_ids))) + if any(current_id not in self.observations for current_id in selected): + raise ValueError( + "certificate replay references an absent recurrence current" + ) + return replace( + self, + observations={ + current_id: self.observations[current_id] for current_id in selected + }, + full_current_count=self.current_count, + complete_observations_retained=False, + ) + + def to_provenance_dict(self) -> dict[str, object]: + return { + "abi": _CAPTURE_ABI, + "precision_digits": self.precision_digits, + "point_count": self.point_count, + "point_sha256s": list(self.point_sha256s), + "kinematic_sha256s": list(self.kinematic_sha256s), + "parameter_contexts": [ + [_decimal_string(value) for value in context] + for context in self.parameter_contexts + ], + "parameter_context_sha256s": list(self.parameter_context_sha256s), + "context_sha256s": list(self.context_sha256s), + "points": [point.to_mapping() for point in self.points], + "current_count": self.current_count, + "runtime_parameter_schema_sha256": (self.runtime_parameter_schema_sha256), + "source_semantics_sha256": self.source_semantics_sha256, + "observation_batch_sha256": self.observation_batch_sha256, + "capture_contract_sha256": self.capture_contract_sha256, + "context_policy": self.context_policy, + "complete_current_components": True, + "point_major": True, + "current_dimensions_sha256": _canonical_sha256( + { + str(current_id): dimension + for current_id, dimension in sorted(self.current_dimensions.items()) + } + ), + "evaluator": "recurrence-direct-plan-decimal-symbolica-exact", + } + + def to_evidence_dict(self) -> dict[str, object]: + if not self.complete_observations_retained: + raise ValueError( + "detached recurrence observations cannot recreate raw evidence" + ) + return { + **self.to_provenance_dict(), + "kinematic_binary64": [list(row) for row in self.kinematic_binary64], + "selector_contexts": list(self.selector_contexts), + "current_dimensions": [ + {"current_id": current_id, "dimension": dimension} + for current_id, dimension in sorted(self.current_dimensions.items()) + ], + "observations": [ + { + "current_id": current_id, + "dimension": self.current_dimensions[current_id], + "values": [ + [_decimal_string(real), _decimal_string(imaginary)] + for real, imaginary in self.observations[current_id] + ], + } + for current_id in sorted(self.observations) + ], + } + + def to_certificate_replay_dict( + self, + current_ids: Sequence[int], + ) -> dict[str, object]: + """Return deduplicated raw rows needed to replay applied certificates.""" + + selected = tuple(sorted(set(current_ids))) + if any(current_id not in self.observations for current_id in selected): + raise ValueError( + "certificate replay references an absent recurrence current" + ) + observation_rows: list[object] = [] + for current_id in selected: + row = { + "current_id": current_id, + "dimension": self.current_dimensions[current_id], + "values": [ + [_decimal_string(real), _decimal_string(imaginary)] + for real, imaginary in self.observations[current_id] + ], + } + observation_rows.append(row) + return { + "abi": "pyamplicol-recurrence-certificate-capture-replay-v1", + "precision_digits": self.precision_digits, + "points": [point.to_mapping() for point in self.points], + "kinematic_binary64": [list(row) for row in self.kinematic_binary64], + "selector_contexts": list(self.selector_contexts), + "parameter_contexts": [ + [_decimal_string(value) for value in context] + for context in self.parameter_contexts + ], + "point_sha256s": list(self.point_sha256s), + "kinematic_sha256s": list(self.kinematic_sha256s), + "selector_context_sha256s": list(self.context_sha256s), + "parameter_context_sha256s": list(self.parameter_context_sha256s), + "runtime_parameter_schema_sha256": (self.runtime_parameter_schema_sha256), + "source_semantics_sha256": self.source_semantics_sha256, + "certificate_current_ids": list(selected), + "observations": observation_rows, + "full_batch_commitment": { + "current_count": self.current_count, + "current_dimensions_sha256": _canonical_sha256( + { + str(current_id): dimension + for current_id, dimension in sorted( + self.current_dimensions.items() + ) + } + ), + "observation_batch_sha256": self.observation_batch_sha256, + "capture_contract_sha256": self.capture_contract_sha256, + }, + } + + +@dataclass(frozen=True, slots=True) +class RecurrenceNumericalCurrentCertificate: + """Canonical candidate plus independent-verification evidence.""" + + current_id: int + representative_id: int | None + execution_representative_id: int + relation_kind: _RelationKind + factor: tuple[int, int] + source_semantics_sha256: str + precision_digits: int + seed: int + relative_tolerance: float + absolute_tolerance: float + candidate_probe_count: int + verification_probe_count: int + current_dimension: int + runtime_parameter_schema_sha256: str + candidate_capture_sha256: str + verification_capture_sha256: str + candidate_maximum_absolute_residual: Fraction + candidate_maximum_relative_residual: Fraction + candidate_maximum_tolerance_ratio: Fraction + verification_maximum_absolute_residual: Fraction + verification_maximum_relative_residual: Fraction + verification_maximum_tolerance_ratio: Fraction + candidate_observations_sha256: str + verification_observations_sha256: str + probe_contract_sha256: str + proof_sha256: str + + def to_json_dict(self) -> dict[str, object]: + return { + "algorithm": _RECURRENCE_CERTIFICATE_ALGORITHM, + "proof_kind": "authenticated-numerical", + "relation_kind": self.relation_kind, + "current_id": self.current_id, + "representative_id": self.representative_id, + "execution_representative_id": self.execution_representative_id, + "factor_integer": list(self.factor), + "source_semantics_sha256": self.source_semantics_sha256, + "precision_digits": self.precision_digits, + "seed": self.seed, + "relative_tolerance_binary64": self.relative_tolerance.hex(), + "absolute_tolerance_binary64": self.absolute_tolerance.hex(), + "candidate_probe_count": self.candidate_probe_count, + "verification_probe_count": self.verification_probe_count, + "current_dimension": self.current_dimension, + "runtime_parameter_schema_sha256": (self.runtime_parameter_schema_sha256), + "candidate_capture_sha256": self.candidate_capture_sha256, + "verification_capture_sha256": self.verification_capture_sha256, + "candidate_maximum_absolute_residual": _fraction_string( + self.candidate_maximum_absolute_residual + ), + "candidate_maximum_relative_residual": _fraction_string( + self.candidate_maximum_relative_residual + ), + "candidate_maximum_tolerance_ratio": _fraction_string( + self.candidate_maximum_tolerance_ratio + ), + "verification_maximum_absolute_residual": _fraction_string( + self.verification_maximum_absolute_residual + ), + "verification_maximum_relative_residual": _fraction_string( + self.verification_maximum_relative_residual + ), + "verification_maximum_tolerance_ratio": _fraction_string( + self.verification_maximum_tolerance_ratio + ), + "candidate_observations_sha256": (self.candidate_observations_sha256), + "verification_observations_sha256": (self.verification_observations_sha256), + "probe_contract_sha256": self.probe_contract_sha256, + "proof_sha256": self.proof_sha256, + } + + +@dataclass(frozen=True, slots=True) +class RecurrenceNumericalCurrentWarmupResult: + """One unpublished-baseline discovery transaction.""" + + requested_mode: _Mode + effective_mode: _Mode + color_accuracy: str + source_semantics_sha256: str + runtime_parameter_schema: Mapping[str, object] + candidate_capture: RecurrenceCurrentObservationCapture + verification_capture: RecurrenceCurrentObservationCapture | None + certificates: tuple[RecurrenceNumericalCurrentCertificate, ...] + evidence_json: bytes + evidence_transport_bytes: int + evidence_encoding: _EvidenceEncoding | None + evidence_canonical_bytes: int + discovery_report: Mapping[str, object] + application_validation: Mapping[str, object] + generation_profile_timings: Mapping[str, float] = field( + default_factory=dict, + compare=False, + repr=False, + ) + + @property + def applied_relation_count(self) -> int: + return len(self.applied_certificates) + + @property + def applied_certificates(self) -> tuple[RecurrenceNumericalCurrentCertificate, ...]: + """Keep discovery complete while applying only supported relations.""" + + if self.effective_mode != "certified-reuse": + return () + return _applicable_numerical_certificates( + self.certificates, + application_scope=cast( + Mapping[str, object], self.discovery_report["application_scope"] + ), + ) + + @property + def warning_required(self) -> bool: + return self.applied_relation_count > 0 + + @property + def effective_mode_reason(self) -> str | None: + """Explain a conservative requested-to-effective mode downgrade.""" + + if self.effective_mode == self.requested_mode: + return None + application_scope = cast( + Mapping[str, object], + self.discovery_report["application_scope"], + ) + if cast(Sequence[int], application_scope["suppressed_selector_domain_ids"]): + return "multi-helicity-all-flow-member-scope-unproven" + if any( + certificate.relation_kind == "opposite" for certificate in self.certificates + ): + return "contracted-opposite-binary64-disabled" + raise AssertionError("recurrence numerical mode changed without a reason") + + def with_application_validation( + self, + payload: Mapping[str, object], + ) -> RecurrenceNumericalCurrentWarmupResult: + return replace(self, application_validation=dict(payload)) + + def without_evidence_transport( + self, + ) -> RecurrenceNumericalCurrentWarmupResult: + """Release the native-only transport after second-pass lowering.""" + + return replace(self, evidence_json=b"") + + def close(self) -> None: + """Close any trusted temporary observation stores owned by the result.""" + + for capture in (self.candidate_capture, self.verification_capture): + if capture is not None and isinstance( + capture.observations, + _SpooledObservationMapping, + ): + capture.observations.close() + + def to_json_dict(self) -> dict[str, object]: + if not self.certificates: + state = "no_certified_numerical_relation" + elif self.effective_mode == "certified-reuse": + state = "authenticated-numerical-applied" + else: + state = "authenticated-numerical-diagnostic-only" + warning = _warning_payload(self.warning_required) + relation_correctness = numerical_relation_correctness_payload( + self.applied_relation_count + ) + applied_current_ids = ( + tuple( + current_id + for certificate in self.applied_certificates + for current_id in ( + certificate.current_id, + certificate.representative_id, + ) + if current_id is not None + ) + if self.effective_mode == "certified-reuse" + else () + ) + persisted_evidence = { + "abi": _PERSISTED_EVIDENCE_ABI, + "generation_raw_evidence_bytes": self.evidence_canonical_bytes, + "generation_evidence_transport_bytes": self.evidence_transport_bytes, + "generation_evidence_encoding": self.evidence_encoding, + "raw_evidence_retained": False, + "runtime_parameter_schema": dict(self.runtime_parameter_schema), + "runtime_parameter_schema_sha256": ( + self.candidate_capture.runtime_parameter_schema_sha256 + ), + "candidate_capture": self.candidate_capture.to_certificate_replay_dict( + applied_current_ids + ), + "verification_capture": ( + None + if self.verification_capture is None + else self.verification_capture.to_certificate_replay_dict( + applied_current_ids, + ) + ), + "full_census": { + "inspected_current_count": self.discovery_report[ + "inspected_current_count" + ], + "tested_hypothesis_count": self.discovery_report[ + "tested_hypothesis_count" + ], + "candidate_index": dict( + cast( + Mapping[str, object], + self.discovery_report["candidate_index"], + ) + ), + "numerical_candidate_count": self.discovery_report[ + "numerical_candidate_count" + ], + "verification_rejected_count": self.discovery_report[ + "verification_rejected_count" + ], + "uncertified_candidate_count": self.discovery_report[ + "verification_rejected_count" + ], + "certified_relation_count": len(self.certificates), + "rejected_hypothesis_count": self.discovery_report[ + "rejected_hypothesis_count" + ], + "decision_sha256": self.discovery_report["decision_sha256"], + "rejection_decision_sha256": self.discovery_report[ + "rejection_decision_sha256" + ], + "certificate_set_sha256": _certificate_set_sha256(self.certificates), + }, + } + persisted_encoded = _bounded_canonical_json_bytes( + persisted_evidence, + byte_limit=None, + label="recurrence persisted numerical evidence", + ) + persisted_evidence["measured_payload_bytes"] = len(persisted_encoded) + persisted_evidence["sha256"] = _canonical_sha256(persisted_evidence) + application_scope = dict( + cast( + Mapping[str, object], + self.discovery_report["application_scope"], + ) + ) + application_scope["effective_mode_reason"] = self.effective_mode_reason + return { + "schema_version": 1, + "abi": _WARMUP_ABI, + "requested_mode": self.requested_mode, + "effective_mode": self.effective_mode, + "effective_mode_reason": self.effective_mode_reason, + "effective_reuse_state": ( + "enabled" + if self.effective_mode == "certified-reuse" + or self.requested_mode == "diagnostic" + else "disabled" + ), + "state": state, + "scope": { + "execution_mode": "recurrence", + "color_accuracy": self.color_accuracy, + "representation": "recurrence-direct-plan-v2", + }, + "source_semantics": { + "abi": _SOURCE_ABI, + "sha256": self.source_semantics_sha256, + }, + "candidate_capture": self.candidate_capture.to_provenance_dict(), + "verification_capture": ( + None + if self.verification_capture is None + else self.verification_capture.to_provenance_dict() + ), + "application_capture": ( + self.application_validation.get("application_capture") + ), + "application_validation": dict(self.application_validation), + "application_scope": application_scope, + "persisted_numerical_evidence": persisted_evidence, + "discovery": dict(self.discovery_report), + "application": { + "schema_version": 1, + "abi": _RELATION_SET_ABI, + "requested_mode": self.requested_mode, + "effective_mode": self.effective_mode, + "state": state, + "certificate_replay": { + "algorithm": _RECURRENCE_CERTIFICATE_ALGORITHM, + "status": ( + "verified" + if self.certificates + else "no_certified_numerical_relation" + ), + "certificate_set_sha256": _certificate_set_sha256( + self.certificates + ), + }, + "certified_relation_count": len(self.certificates), + "applied_relation_count": self.applied_relation_count, + "relation_kind_counts": { + kind: sum( + certificate.relation_kind == kind + for certificate in self.certificates + ) + for kind in ("equal", "opposite", "zero") + }, + "certificates": [ + certificate.to_json_dict() for certificate in self.certificates + ], + "mappings": [ + _mapping_payload(certificate) for certificate in self.certificates + ], + "warning": warning, + }, + "certified_relation_count": len(self.certificates), + "applied_relation_count": self.applied_relation_count, + "relation_correctness": relation_correctness, + "warning": warning, + } + + +def run_recurrence_numerical_current_warmup( + plan: _RecurrenceExactPlan, + *, + candidate_points: Sequence[ValidationPointRecord], + verification_points: Sequence[ValidationPointRecord], + mode: _Mode, + color_accuracy: str, + precision_digits: int, + seed: int, + relative_tolerance: float, + absolute_tolerance: float, +) -> RecurrenceNumericalCurrentWarmupResult: + """Capture, discover, certify, and encode a second-pass relation set.""" + + if mode not in {"diagnostic", "certified-reuse"}: + raise ValueError( + "recurrence numerical current warm-up requires diagnostic or " + "certified-reuse mode" + ) + _validated_tolerances(relative_tolerance, absolute_tolerance) + relative_tolerance = float(relative_tolerance) + absolute_tolerance = float(absolute_tolerance) + generation_profile_timings: dict[str, float] = {} + geometry_started = time.perf_counter() + geometry = _select_raw_evidence_storage_geometry( + plan.sections, + candidate_probe_count=len(candidate_points), + verification_probe_count=len(verification_points), + runtime_parameter_count=len(plan.runtime_parameter_schema), + ) + generation_profile_timings["warmup_geometry"] = ( + time.perf_counter() - geometry_started + ) + static_context_started = time.perf_counter() + static_context = _build_warmup_static_context(plan) + generation_profile_timings["warmup_static_context"] = ( + time.perf_counter() - static_context_started + ) + source_digest = static_context.source_semantics_sha256 + runtime_parameter_schema = static_context.runtime_parameter_schema + runtime_parameter_schema_sha256 = static_context.runtime_parameter_schema_sha256 + runtime_defaults = static_context.runtime_defaults + probe_policies = tuple(row.probe_policy for row in plan.runtime_parameter_schema) + candidate_parameter_contexts = _build_parameter_probe_contexts( + runtime_defaults, + probe_policies=probe_policies, + precision_digits=precision_digits, + seed=seed, + domain="candidate-current-parameter-probes-v1", + count=len(candidate_points), + include_defaults=True, + ) + candidate_spool: _SpooledObservationMapping | None = None + verification_spool: _SpooledObservationMapping | None = None + evidence_spool: BinaryIO | None = None + try: + candidate_probe_timings: list[float] = [] + candidate_capture_started = time.perf_counter() + candidate = _capture_recurrence_current_observations( + plan, + candidate_points, + precision_digits=precision_digits, + source_semantics_sha256=source_digest, + seed=seed, + domain="candidate-current-probes-v1", + parameter_contexts=candidate_parameter_contexts, + runtime_parameter_schema_sha256=runtime_parameter_schema_sha256, + static_context=static_context, + probe_timings=candidate_probe_timings, + ) + generation_profile_timings["warmup_candidate_capture"] = ( + time.perf_counter() - candidate_capture_started + ) + generation_profile_timings.update( + { + f"warmup_candidate_probe_{index}": seconds + for index, seconds in enumerate(candidate_probe_timings) + } + ) + candidate_index_started = time.perf_counter() + candidate_indexes: ( + Mapping[ + _CurrentContract, + NumericalObservationCandidateIndex[Fraction], + ] + | None + ) = _build_candidate_indexes( + plan.sections, + candidate.observations, + contracts=static_context.current_contracts, + ) + generation_profile_timings["warmup_candidate_index"] = ( + time.perf_counter() - candidate_index_started + ) + candidate_preflight_started = time.perf_counter() + try: + certificates, discovery = _discover_relations( + plan.sections, + candidate, + None, + source_semantics_sha256=source_digest, + precision_digits=precision_digits, + seed=seed, + relative_tolerance=relative_tolerance, + absolute_tolerance=absolute_tolerance, + color_accuracy=color_accuracy, + contracts=static_context.current_contracts, + candidate_indexes=candidate_indexes, + ) + except _IndependentVerificationRequired: + generation_profile_timings["warmup_candidate_preflight"] = ( + time.perf_counter() - candidate_preflight_started + ) + else: + generation_profile_timings["warmup_relation_discovery"] = ( + time.perf_counter() - candidate_preflight_started + ) + if certificates: + raise AssertionError( + "candidate-only recurrence discovery produced certificates" + ) + effective_mode = _effective_numerical_relation_mode( + plan.sections, + requested_mode=mode, + certificates=certificates, + ) + candidate = candidate.detached_for_replay(()) + return RecurrenceNumericalCurrentWarmupResult( + requested_mode=mode, + effective_mode=effective_mode, + color_accuracy=color_accuracy, + source_semantics_sha256=source_digest, + runtime_parameter_schema=runtime_parameter_schema, + candidate_capture=candidate, + verification_capture=None, + certificates=certificates, + evidence_json=b"", + evidence_transport_bytes=0, + evidence_encoding=None, + evidence_canonical_bytes=0, + discovery_report=discovery, + application_validation={ + "status": "not-required-no-applied-relations", + "checked_current_count": 0, + "checked_component_count": 0, + "maximum_absolute_residual": None, + "maximum_relative_residual": None, + "maximum_tolerance_ratio": None, + "application_capture": None, + }, + generation_profile_timings=MappingProxyType( + dict(generation_profile_timings) + ), + ) + if geometry.encoding == _COMPRESSED_EVIDENCE_ENCODING: + candidate_spool_started = time.perf_counter() + candidate_spool = _SpooledObservationMapping( + candidate.observations, + candidate_indexes=candidate_indexes, + ) + candidate = replace(candidate, observations=candidate_spool) + # The verification capture can itself be large. Keep the exact + # reusable index on disk until relation discovery instead of + # retaining both resident captures and the index simultaneously. + candidate_indexes = None + generation_profile_timings["warmup_candidate_spool"] = ( + time.perf_counter() - candidate_spool_started + ) + verification_parameter_contexts = _build_parameter_probe_contexts( + runtime_defaults, + probe_policies=probe_policies, + precision_digits=precision_digits, + seed=seed, + domain="independent-verification-current-parameter-probes-v1", + count=len(verification_points), + include_defaults=False, + ) + verification_probe_timings: list[float] = [] + verification_capture_started = time.perf_counter() + verification = _capture_recurrence_current_observations( + plan, + verification_points, + precision_digits=precision_digits, + source_semantics_sha256=source_digest, + seed=seed, + domain="independent-verification-current-probes-v1", + parameter_contexts=verification_parameter_contexts, + runtime_parameter_schema_sha256=runtime_parameter_schema_sha256, + static_context=static_context, + probe_timings=verification_probe_timings, + ) + generation_profile_timings["warmup_verification_capture"] = ( + time.perf_counter() - verification_capture_started + ) + generation_profile_timings.update( + { + f"warmup_verification_probe_{index}": seconds + for index, seconds in enumerate(verification_probe_timings) + } + ) + if geometry.encoding == _COMPRESSED_EVIDENCE_ENCODING: + verification_spool_started = time.perf_counter() + verification_spool = _SpooledObservationMapping(verification.observations) + verification = replace( + verification, + observations=verification_spool, + ) + generation_profile_timings["warmup_verification_spool"] = ( + time.perf_counter() - verification_spool_started + ) + _validate_independent_captures(candidate, verification) + discovery_started = time.perf_counter() + certificates, discovery = _discover_relations( + plan.sections, + candidate, + verification, + source_semantics_sha256=source_digest, + precision_digits=precision_digits, + seed=seed, + relative_tolerance=relative_tolerance, + absolute_tolerance=absolute_tolerance, + color_accuracy=color_accuracy, + contracts=static_context.current_contracts, + candidate_indexes=candidate_indexes, + ) + generation_profile_timings["warmup_relation_discovery"] = ( + time.perf_counter() - discovery_started + ) + evidence_payload_started = time.perf_counter() + effective_mode = _effective_numerical_relation_mode( + plan.sections, + requested_mode=mode, + certificates=certificates, + ) + evidence = _evidence_payload( + plan.sections, + mode=effective_mode, + source_semantics=static_context.source_semantics, + source_semantics_sha256=source_digest, + certificates=certificates, + discovery=discovery, + precision_digits=precision_digits, + seed=seed, + relative_tolerance=relative_tolerance, + absolute_tolerance=absolute_tolerance, + probe_count=candidate.point_count, + verification_probe_count=verification.point_count, + runtime_parameter_schema=runtime_parameter_schema, + candidate=candidate, + verification=verification, + ) + generation_profile_timings["warmup_evidence_payload"] = ( + time.perf_counter() - evidence_payload_started + ) + evidence_serialization_started = time.perf_counter() + if geometry.encoding == _RAW_EVIDENCE_ENCODING: + encoded = _bounded_canonical_json_bytes( + evidence, + byte_limit=geometry.canonical_byte_limit, + label="recurrence numerical raw evidence", + ) + canonical_byte_count = len(encoded) + else: + evidence_spool = tempfile.TemporaryFile( # noqa: SIM115 + prefix="pyamplicol-recurrence-evidence-", + ) + canonical_byte_count = _write_compressed_canonical_evidence( + evidence, + evidence_spool, + canonical_byte_limit=geometry.canonical_byte_limit, + ) + encoded = b"" + generation_profile_timings["warmup_evidence_serialization"] = ( + time.perf_counter() - evidence_serialization_started + ) + replay_current_ids = ( + tuple( + current_id + for certificate in _applicable_numerical_certificates( + certificates, + application_scope=cast( + Mapping[str, object], discovery["application_scope"] + ), + ) + for current_id in ( + certificate.current_id, + certificate.representative_id, + ) + if current_id is not None + ) + if effective_mode == "certified-reuse" + else () + ) + if not isinstance(candidate.observations, _SpooledObservationMapping): + candidate = candidate.detached_for_replay(replay_current_ids) + verification = verification.detached_for_replay(replay_current_ids) + else: + # Transfer both trusted row stores to the result. Certificate + # replay and application validation consume rows lazily, so the + # resident graph never grows with the number of relations. + candidate_spool = None + verification_spool = None + # The streamed evidence mapping owns wrappers pointing at the complete + # captures. Drop it before reading the compressed transport bytes. + del evidence + if evidence_spool is not None: + evidence_transport_started = time.perf_counter() + encoded = _read_compressed_evidence_spool(evidence_spool) + evidence_spool.close() + evidence_spool = None + generation_profile_timings["warmup_evidence_transport_read"] = ( + time.perf_counter() - evidence_transport_started + ) + return RecurrenceNumericalCurrentWarmupResult( + requested_mode=mode, + effective_mode=effective_mode, + color_accuracy=color_accuracy, + source_semantics_sha256=source_digest, + runtime_parameter_schema=runtime_parameter_schema, + candidate_capture=candidate, + verification_capture=verification, + certificates=certificates, + evidence_json=encoded, + evidence_transport_bytes=len(encoded), + evidence_encoding=geometry.encoding, + evidence_canonical_bytes=canonical_byte_count, + discovery_report=discovery, + application_validation={ + "status": ( + "pending-second-pass-validation" + if effective_mode == "certified-reuse" and certificates + else "not-required-no-applied-relations" + ), + "checked_current_count": 0, + "checked_component_count": 0, + "maximum_absolute_residual": None, + "maximum_relative_residual": None, + "maximum_tolerance_ratio": None, + "application_capture": None, + }, + generation_profile_timings=MappingProxyType( + dict(generation_profile_timings) + ), + ) + finally: + if candidate_spool is not None: + candidate_spool.close() + if verification_spool is not None: + verification_spool.close() + if evidence_spool is not None: + evidence_spool.close() + + +def build_recurrence_numerical_current_probe_points( + process: CanonicalProcessIR, + model: Model, + *, + process_id: str, + seed: int, + candidate_count: int, + verification_count: int, +) -> tuple[ + tuple[ValidationPointRecord, ...], + tuple[ValidationPointRecord, ...], +]: + """Build disjoint deterministic physical points for recurrence probes.""" + + if ( + type(seed) is not int + or seed < 0 + or type(candidate_count) is not int + or candidate_count < 2 + or type(verification_count) is not int + or verification_count < 2 + ): + raise ValueError("recurrence numerical probe-point contract is invalid") + + def points(domain: str, count: int) -> tuple[ValidationPointRecord, ...]: + return tuple( + rotate_validation_point( + build_process_validation_point( + process, + model, + process_id=process_id, + seed=_domain_seed(seed, domain=domain, index=index), + ), + rotation_seed=_domain_seed( + seed, + domain=f"{domain}:spatial-rotation-v1", + index=index, + ), + ) + for index in range(count) + ) + + candidate = points("candidate-current-probes-v1", candidate_count) + verification = points( + "independent-verification-current-probes-v1", + verification_count, + ) + if any(not point.available for point in (*candidate, *verification)): + errors = tuple( + point.error for point in (*candidate, *verification) if not point.available + ) + raise ValueError( + "recurrence numerical probe-point generation failed: " + + "; ".join(str(error) for error in errors) + ) + candidate_hashes = {_kinematic_sha256(point) for point in candidate} + verification_hashes = {_kinematic_sha256(point) for point in verification} + if ( + len(candidate_hashes) != len(candidate) + or len(verification_hashes) != len(verification) + or not candidate_hashes.isdisjoint(verification_hashes) + ): + raise ValueError("recurrence numerical probe domains are not independent") + return candidate, verification + + +def validate_recurrence_numerical_current_application( + baseline: RecurrenceNumericalCurrentWarmupResult, + applied_plan: _RecurrenceExactPlan, + *, + baseline_plan: _RecurrenceExactPlan, + precision_digits: int, + seed: int, + relative_tolerance: float, + absolute_tolerance: float, +) -> RecurrenceNumericalCurrentWarmupResult: + """Fail closed unless every second-pass current replays on verification.""" + + if not baseline.applied_relation_count: + return baseline.with_application_validation( + { + "status": "not-required-no-applied-relations", + "checked_current_count": 0, + "checked_component_count": 0, + "maximum_absolute_residual": None, + "maximum_relative_residual": None, + "maximum_tolerance_ratio": None, + "application_capture": None, + } + ) + verification_capture = baseline.verification_capture + if verification_capture is None: + raise ValueError( + "applied recurrence numerical relations lack verification observations" + ) + reference_spool: _SpooledObservationMapping | None = None + try: + reference = capture_recurrence_current_observations( + baseline_plan, + verification_capture.points, + precision_digits=precision_digits, + source_semantics_sha256=baseline.source_semantics_sha256, + seed=seed, + domain="independent-verification-current-probes-v1", + parameter_contexts=verification_capture.parameter_contexts, + runtime_parameter_schema_sha256=( + verification_capture.runtime_parameter_schema_sha256 + ), + ) + _validate_recaptured_reference( + verification_capture, + reference, + ) + reference_spool = _SpooledObservationMapping(reference.observations) + reference = replace(reference, observations=reference_spool) + applied = capture_recurrence_current_observations( + applied_plan, + verification_capture.points, + precision_digits=precision_digits, + source_semantics_sha256=baseline.source_semantics_sha256, + seed=seed, + domain="independent-verification-current-probes-v1", + parameter_contexts=verification_capture.parameter_contexts, + runtime_parameter_schema_sha256=( + verification_capture.runtime_parameter_schema_sha256 + ), + ) + validation = _validate_applied_observations( + reference, + applied, + relative_tolerance=relative_tolerance, + absolute_tolerance=absolute_tolerance, + ) + validation["application_capture"] = applied.to_provenance_dict() + return baseline.with_application_validation(validation) + finally: + if reference_spool is not None: + reference_spool.close() + + +def recurrence_numerical_current_opt_out_report( + sections: _RecurrenceExactSectionsV1, + *, + color_accuracy: str, +) -> dict[str, object]: + """Return stable provenance without evaluating a single exact current.""" + + return { + "schema_version": 1, + "abi": _WARMUP_ABI, + "requested_mode": "off", + "effective_mode": "off", + "effective_reuse_state": "disabled", + "state": "disabled-by-user", + "scope": { + "execution_mode": "recurrence", + "color_accuracy": color_accuracy, + "representation": "recurrence-direct-plan-v2", + }, + "source_semantics": { + "abi": _SOURCE_ABI, + "sha256": recurrence_numerical_source_semantics_sha256(sections), + }, + "candidate_capture": None, + "verification_capture": None, + "application_capture": None, + "application_validation": { + "status": "disabled-by-user", + "checked_current_count": 0, + "checked_component_count": 0, + "maximum_absolute_residual": None, + "maximum_relative_residual": None, + "maximum_tolerance_ratio": None, + "application_capture": None, + }, + "application_scope": { + "abi": _APPLICATION_SCOPE_ABI, + "policy": "disabled-by-user", + "layout": sections.strategy, + "suppressed_selector_domain_ids": [], + "contracted_opposite_application": "not-applicable", + "suppressed_current_count": 0, + }, + "discovery": None, + "application": None, + "certified_relation_count": 0, + "applied_relation_count": 0, + "relation_correctness": numerical_relation_correctness_payload(0), + "warning": _warning_payload(False), + } + + +def recurrence_numerical_current_envelope_fallback_report( + sections: _RecurrenceExactSectionsV1, + *, + color_accuracy: str, + requested_mode: _Mode, + outcome: RecurrenceNumericalEvidenceEnvelopeExceeded, +) -> dict[str, object]: + """Publish the explicit reuse-off result of one safe geometry fallback.""" + + if requested_mode != "certified-reuse": + raise ValueError("the evidence-envelope fallback is limited to certified reuse") + fallback = outcome.to_json_dict() + warning = { + "required": True, + "emit": "once-per-generated-artifact", + "code": EVIDENCE_ENVELOPE_FALLBACK_REASON, + "message": EVIDENCE_ENVELOPE_FALLBACK_WARNING, + } + return { + "schema_version": 1, + "abi": _WARMUP_ABI, + "requested_mode": requested_mode, + "effective_mode": "off", + "effective_reuse_state": "disabled", + "state": "disabled-evidence-envelope-fallback", + "fallback": fallback, + "scope": { + "execution_mode": "recurrence", + "color_accuracy": color_accuracy, + "representation": "recurrence-direct-plan-v2", + }, + "source_semantics": { + "abi": _SOURCE_ABI, + "sha256": recurrence_numerical_source_semantics_sha256(sections), + }, + "candidate_capture": None, + "verification_capture": None, + "application_capture": None, + "application_validation": { + "status": "not-required-reuse-disabled", + "checked_current_count": 0, + "checked_component_count": 0, + "maximum_absolute_residual": None, + "maximum_relative_residual": None, + "maximum_tolerance_ratio": None, + "application_capture": None, + }, + "application_scope": { + "abi": _APPLICATION_SCOPE_ABI, + "policy": "evidence-envelope-fallback", + "layout": sections.strategy, + "suppressed_selector_domain_ids": [], + "contracted_opposite_application": "not-applicable", + "suppressed_current_count": 0, + }, + "discovery": None, + "application": None, + "certified_relation_count": 0, + "applied_relation_count": 0, + "relation_correctness": numerical_relation_correctness_payload(0), + "warning": warning, + } + + +def capture_recurrence_current_observations( + plan: _RecurrenceExactPlan, + points: Sequence[ValidationPointRecord], + *, + precision_digits: int, + source_semantics_sha256: str, + seed: int, + domain: str, + parameter_contexts: Sequence[Sequence[Decimal]] | None = None, + runtime_parameter_schema_sha256: str | None = None, +) -> RecurrenceCurrentObservationCapture: + """Evaluate every current before Direct-Arena stage recycling.""" + + return _capture_recurrence_current_observations( + plan, + points, + precision_digits=precision_digits, + source_semantics_sha256=source_semantics_sha256, + seed=seed, + domain=domain, + parameter_contexts=parameter_contexts, + runtime_parameter_schema_sha256=runtime_parameter_schema_sha256, + static_context=None, + probe_timings=None, + ) + + +def _capture_recurrence_current_observations( + plan: _RecurrenceExactPlan, + points: Sequence[ValidationPointRecord], + *, + precision_digits: int, + source_semantics_sha256: str, + seed: int, + domain: str, + parameter_contexts: Sequence[Sequence[Decimal]] | None, + runtime_parameter_schema_sha256: str | None, + static_context: _WarmupStaticContext | None, + probe_timings: list[float] | None, +) -> RecurrenceCurrentObservationCapture: + records = tuple(points) + if ( + type(precision_digits) is not int + or precision_digits < 80 + or len(records) < 2 + or any(not isinstance(point, ValidationPointRecord) for point in records) + or any(not point.available for point in records) + or len({point.process_id for point in records}) != 1 + ): + raise ValueError("recurrence current capture point contract is invalid") + sections = plan.sections + dimensions = ( + {current.semantic_id: current.component_count for current in sections.currents} + if static_context is None + else static_context.current_dimensions + ) + expected_current_ids = frozenset(dimensions) + observations: dict[int, list[_ComplexDecimal]] = { + current_id: [] for current_id in dimensions + } + point_sha256s = tuple(_canonical_sha256(point.to_mapping()) for point in records) + kinematic_binary64 = tuple(_kinematic_binary64(point) for point in records) + kinematic_sha256s = tuple(_canonical_sha256(row) for row in kinematic_binary64) + if len(set(point_sha256s)) != len(records) or len(set(kinematic_sha256s)) != len( + records + ): + raise ValueError("recurrence current capture requires distinct physical points") + if static_context is None: + runtime_defaults = _runtime_parameter_defaults(plan) + schema_payload = _runtime_parameter_schema_payload( + plan, + defaults=runtime_defaults, + ) + actual_schema_sha256 = _canonical_sha256(schema_payload) + else: + runtime_defaults = static_context.runtime_defaults + actual_schema_sha256 = static_context.runtime_parameter_schema_sha256 + if ( + runtime_parameter_schema_sha256 is not None + and runtime_parameter_schema_sha256 != actual_schema_sha256 + ): + raise ValueError("recurrence runtime parameter schema digest drifted") + resolved_parameter_contexts = ( + tuple(runtime_defaults for _record in records) + if parameter_contexts is None + else tuple(tuple(values) for values in parameter_contexts) + ) + if ( + len(resolved_parameter_contexts) != len(records) + or any( + len(values) != len(runtime_defaults) + or any( + not isinstance(value, Decimal) or not value.is_finite() + for value in values + ) + for values in resolved_parameter_contexts + ) + or any( + row.probe_policy == "derived-overwritten-fixed-zero-v1" + and any( + values[row.runtime_slot] != runtime_defaults[row.runtime_slot] + for values in resolved_parameter_contexts + ) + for row in plan.runtime_parameter_schema + ) + ): + raise ValueError( + "recurrence parameter contexts do not cover every flattened runtime slot" + ) + parameter_context_sha256s = tuple( + _canonical_sha256( + { + "abi": _PARAMETER_CONTEXT_ABI, + "domain": domain, + "point_index": point_index, + "point_sha256": point_sha256, + "runtime_parameter_schema_sha256": actual_schema_sha256, + "values": [_decimal_string(value) for value in values], + } + ) + for point_index, (point_sha256, values) in enumerate( + zip(point_sha256s, resolved_parameter_contexts, strict=True) + ) + ) + context_payloads: list[object] = [] + working_precision = precision_digits + 16 + with localcontext() as context: + context.prec = working_precision + context.rounding = ROUND_HALF_EVEN + for point_index, (record, parameter_context) in enumerate( + zip(records, resolved_parameter_contexts, strict=True) + ): + probe_started = time.perf_counter() + parameters = plan.resolve_model_parameters( + parameter_context, + working_precision, + ) + point = cast( + _Point, + tuple( + tuple(Decimal.from_float(float(value)) for value in momentum) + for momentum in record.four_vectors + ), + ) + captured, context_payload = _capture_one_point( + plan, + point, + parameters.prepared, + working_precision, + selector_seed=_domain_seed( + seed, + domain=domain, + index=point_index, + ), + expected_current_ids=expected_current_ids, + ) + if probe_timings is not None: + probe_timings.append(time.perf_counter() - probe_started) + context_payloads.append(context_payload) + if captured.keys() != expected_current_ids: + raise ValueError("recurrence exact current capture is incomplete") + for current_id, dimension in dimensions.items(): + values = captured[current_id] + if len(values) != dimension: + raise ValueError( + f"recurrence current {current_id} capture width " + f"{len(values)} does not match {dimension}" + ) + observations[current_id].extend(values) + frozen = {current_id: tuple(values) for current_id, values in observations.items()} + if any( + not real.is_finite() or not imaginary.is_finite() + for values in frozen.values() + for real, imaginary in values + ): + raise ValueError( + "recurrence exact current capture produced a non-finite component" + ) + context_sha256s = tuple(_canonical_sha256(payload) for payload in context_payloads) + batch_digest = _canonical_sha256( + { + "abi": _OBSERVATION_BATCH_ABI, + "source_semantics_sha256": source_semantics_sha256, + "runtime_parameter_schema_sha256": actual_schema_sha256, + "point_sha256s": list(point_sha256s), + "selector_context_sha256s": list(context_sha256s), + "parameter_context_sha256s": list(parameter_context_sha256s), + "currents": _ObservationRowsStream(frozen, dimensions), + } + ) + context_policy = _context_policy(sections.strategy) + capture_contract_sha256 = _canonical_sha256( + { + "abi": _CAPTURE_ABI, + "precision_digits": precision_digits, + "source_semantics_sha256": source_semantics_sha256, + "runtime_parameter_schema_sha256": actual_schema_sha256, + "point_sha256s": list(point_sha256s), + "kinematic_sha256s": list(kinematic_sha256s), + "selector_context_sha256s": list(context_sha256s), + "parameter_context_sha256s": list(parameter_context_sha256s), + "current_dimensions": _DimensionRowsStream(dimensions), + "observation_batch_sha256": batch_digest, + "context_policy": context_policy, + } + ) + capture_dimensions = dimensions if static_context is None else dict(dimensions) + return RecurrenceCurrentObservationCapture( + precision_digits=precision_digits, + points=records, + point_sha256s=point_sha256s, + kinematic_sha256s=kinematic_sha256s, + kinematic_binary64=kinematic_binary64, + selector_contexts=tuple(context_payloads), + context_sha256s=context_sha256s, + parameter_contexts=resolved_parameter_contexts, + parameter_context_sha256s=parameter_context_sha256s, + observations=frozen, + current_dimensions=capture_dimensions, + runtime_parameter_schema_sha256=actual_schema_sha256, + source_semantics_sha256=source_semantics_sha256, + observation_batch_sha256=batch_digest, + capture_contract_sha256=capture_contract_sha256, + context_policy=context_policy, + ) + + +def recurrence_numerical_source_semantics_sha256( + sections: _RecurrenceExactSectionsV1, +) -> str: + """Bind probes to the baseline semantic schedule and current contracts.""" + + return _canonical_sha256(_source_semantics_payload(sections)) + + +def _source_semantics_payload( + sections: _RecurrenceExactSectionsV1, + *, + contracts: tuple[_CurrentContract, ...] | None = None, +) -> dict[str, object]: + if contracts is None: + contracts = _current_contracts(sections) + if sections.strategy == "topology-replay": + selector_schedule: object = { + "policy": _context_policy(sections.strategy), + "replay_targets": [ + { + "target_index": target_index, + "public_flow_id": target.public_flow_id, + "representative_id": target.representative_id, + "selector_domain_id": target.selector_domain_id, + } + for target_index, target in enumerate(sections.replay_targets) + ], + } + elif sections.strategy == "all-flow-union": + selector_schedule = { + "policy": _context_policy(sections.strategy), + "resolved_helicities": [ + { + "helicity_index": helicity_index, + "helicity_id": helicity.helicity_id, + "selector_domain_id": helicity.selector_domain_id, + } + for helicity_index, helicity in enumerate(sections.resolved_helicities) + ], + } + else: + selector_schedule = { + "policy": _context_policy(sections.strategy), + "fixed_source_schedule": True, + } + return { + "abi": _SOURCE_ABI, + "process_id": sections.process_id, + "strategy": sections.strategy, + "schedule_semantic_digest": sections.semantic_digest, + "baseline_runtime_layout_digest": sections.runtime_layout_digest, + "selector_schedule": selector_schedule, + "currents": [ + { + "current_id": current.semantic_id, + "is_source": current.source_row != DIRECT_NONE_U32, + "contract": _plain_contract(contracts[current.semantic_id]), + } + for current in sections.currents + ], + } + + +def _capture_one_point( + plan: _RecurrenceExactPlan, + point: _Point, + prepared_parameters: Sequence[_ComplexDecimal], + precision: int, + *, + selector_seed: int, + expected_current_ids: frozenset[int], +) -> tuple[dict[int, tuple[_ComplexDecimal, ...]], object]: + sections = plan.sections + if sections.strategy == "topology-replay": + if not sections.replay_targets: + raise ArtifactError("topology replay has no exact probe target") + target_index = selector_seed % len(sections.replay_targets) + target = sections.replay_targets[target_index] + captured = _capture_execution( + sections, + lambda observer: _evaluate_replay_point( + plan, + point, + target, + prepared_parameters, + precision, + current_observer=observer, + ), + expected_current_ids=expected_current_ids, + ) + return captured, { + "strategy": sections.strategy, + "target_index": target_index, + "public_flow_id": target.public_flow_id, + "representative_id": target.representative_id, + "selector_domain_id": target.selector_domain_id, + } + if sections.strategy == "all-flow-union": + if not sections.resolved_helicities: + raise ArtifactError("all-flow union has no exact probe helicity") + by_domain: dict[int, list[int]] = defaultdict(list) + for index, helicity in enumerate(sections.resolved_helicities): + by_domain[helicity.selector_domain_id].append(index) + captures: dict[int, dict[int, tuple[_ComplexDecimal, ...]]] = {} + selected: list[dict[str, int]] = [] + for domain_id, indices in sorted(by_domain.items()): + position = selector_seed % len(indices) + helicity_index = indices[position] + helicity = sections.resolved_helicities[helicity_index] + captures[domain_id] = _capture_execution( + sections, + lambda observer, helicity=helicity: _evaluate_union_point( + plan, + point, + helicity, + prepared_parameters, + precision, + current_observer=observer, + ), + expected_current_ids=expected_current_ids, + ) + selected.append( + { + "selector_domain_id": domain_id, + "helicity_index": helicity_index, + "helicity_id": helicity.helicity_id, + } + ) + default_capture = captures[min(captures)] + merged = { + current.semantic_id: captures.get( + current.selector_domain_id, + default_capture, + )[current.semantic_id] + for current in sections.currents + } + return merged, { + "strategy": sections.strategy, + "selector_domain_helicities": selected, + } + captured = _capture_execution( + sections, + lambda observer: _evaluate_contracted_point( + plan, + point, + prepared_parameters, + precision, + current_observer=observer, + ), + expected_current_ids=expected_current_ids, + ) + return captured, { + "strategy": sections.strategy, + "fixed_source_schedule": True, + } + + +def _capture_execution( + sections: _RecurrenceExactSectionsV1, + operation: object, + *, + expected_current_ids: frozenset[int] | None = None, +) -> dict[int, tuple[_ComplexDecimal, ...]]: + captured: dict[int, tuple[_ComplexDecimal, ...]] = {} + + def observe( + current_id: int, + values: tuple[_ComplexDecimal, ...], + ) -> None: + if current_id in captured: + raise ArtifactError( + f"recurrence current {current_id} was observed more than once" + ) + captured[current_id] = values + + if not callable(operation): # pragma: no cover - internal type contract + raise TypeError("recurrence capture operation is not callable") + operation(observe) + expected = ( + frozenset(current.semantic_id for current in sections.currents) + if expected_current_ids is None + else expected_current_ids + ) + if captured.keys() != expected: + missing = sorted(expected - captured.keys()) + raise ArtifactError( + f"recurrence current observation missed semantic currents {missing[:8]}" + ) + return captured + + +def _build_raw_numerical_candidate_index( + member_ids: Sequence[int], + observations: Mapping[int, Sequence[_ComplexDecimal]], +) -> NumericalObservationCandidateIndex[Fraction]: + return build_numerical_observation_candidate_index( + member_ids, + observations, + normalize=Fraction, + ) + + +def _raw_numerical_tolerance_window_ids( + index: NumericalObservationCandidateIndex[Fraction], + current_values: Sequence[_ComplexDecimal], + *, + relation_kind: Literal["equal", "opposite"], + current_id: int, + relative_tolerance: Fraction, + absolute_tolerance: Fraction, +) -> tuple[int, ...]: + return numerical_observation_tolerance_window_ids( + index, + current_values, + relation_kind=relation_kind, + current_id=current_id, + relative_tolerance=relative_tolerance, + absolute_tolerance=absolute_tolerance, + normalize=Fraction, + ) + + +def _build_candidate_indexes( + sections: _RecurrenceExactSectionsV1, + observations: Mapping[int, Sequence[_ComplexDecimal]], + *, + contracts: tuple[_CurrentContract, ...] | None = None, +) -> dict[ + _CurrentContract, + NumericalObservationCandidateIndex[Fraction], +]: + if contracts is None: + contracts = _current_contracts(sections) + members_by_contract: dict[_CurrentContract, list[int]] = defaultdict(list) + for current in sections.currents: + members_by_contract[contracts[current.semantic_id]].append(current.semantic_id) + return { + contract: _build_raw_numerical_candidate_index( + member_ids, + observations, + ) + for contract, member_ids in members_by_contract.items() + } + + +def _numerical_relation_application_scope( + sections: _RecurrenceExactSectionsV1, +) -> dict[str, object]: + """Return the narrow execution members eligible for numerical reuse.""" + + helicities_by_domain: dict[int, int] = defaultdict(int) + for helicity in sections.resolved_helicities: + helicities_by_domain[helicity.selector_domain_id] += 1 + unsafe_all_flow_domains = ( + tuple( + domain_id + for domain_id, member_count in sorted(helicities_by_domain.items()) + if member_count > 1 + ) + if sections.strategy == "all-flow-union" + else () + ) + return { + "abi": _APPLICATION_SCOPE_ABI, + "policy": "exact-physical-selector-member-set-v1", + "layout": sections.strategy, + "multi_helicity_all_flow_domain_ids": list(unsafe_all_flow_domains), + "multi_helicity_all_flow_application": ( + "disabled" if unsafe_all_flow_domains else "not-applicable" + ), + "suppressed_selector_domain_ids": list(unsafe_all_flow_domains), + "contracted_opposite_application": ( + "disabled" + if sections.strategy == "contracted-color-union" + else "not-applicable" + ), + } + + +def _effective_numerical_relation_mode( + sections: _RecurrenceExactSectionsV1, + *, + requested_mode: _Mode, + certificates: Sequence[RecurrenceNumericalCurrentCertificate], +) -> _Mode: + """Contain relations that the native mapper cannot apply member-wise.""" + + if requested_mode != "certified-reuse": + return requested_mode + scope = _numerical_relation_application_scope(sections) + if cast(Sequence[int], scope["suppressed_selector_domain_ids"]): + return "diagnostic" + if certificates and not _applicable_numerical_certificates( + certificates, application_scope=scope + ): + return "diagnostic" + return requested_mode + + +def _applicable_numerical_certificates( + certificates: Sequence[RecurrenceNumericalCurrentCertificate], + *, + application_scope: Mapping[str, object], +) -> tuple[RecurrenceNumericalCurrentCertificate, ...]: + """Mirror native application containment without changing its evidence. + + A skipped opposite relation keeps its original current evaluation. Equal + relations may therefore still refer to that current; no representative is + rewritten and no relation depends on applying a skipped certificate. + """ + + if application_scope["suppressed_selector_domain_ids"]: + return () + suppress_opposite = ( + application_scope["contracted_opposite_application"] == "disabled" + ) + return tuple( + certificate + for certificate in certificates + if not (suppress_opposite and certificate.relation_kind == "opposite") + ) + + +def _discover_relations( + sections: _RecurrenceExactSectionsV1, + candidate: RecurrenceCurrentObservationCapture, + verification: RecurrenceCurrentObservationCapture | None, + *, + source_semantics_sha256: str, + precision_digits: int, + seed: int, + relative_tolerance: float, + absolute_tolerance: float, + color_accuracy: str, + contracts: tuple[_CurrentContract, ...] | None = None, + candidate_indexes: Mapping[ + _CurrentContract, + NumericalObservationCandidateIndex[Fraction], + ] + | None = None, +) -> tuple[ + tuple[RecurrenceNumericalCurrentCertificate, ...], + dict[str, object], +]: + relative, absolute = _validated_tolerances( + relative_tolerance, + absolute_tolerance, + ) + relative_fraction = Fraction(relative) + absolute_fraction = Fraction(absolute) + application_scope = _numerical_relation_application_scope(sections) + unsafe_selector_domains = frozenset( + cast( + Sequence[int], + application_scope["suppressed_selector_domain_ids"], + ) + ) + if contracts is None: + contracts = _current_contracts(sections) + if candidate_indexes is None: + spooled_indexes = ( + candidate.observations.candidate_indexes + if isinstance(candidate.observations, _SpooledObservationMapping) + else None + ) + candidate_indexes = spooled_indexes + if candidate_indexes is None: + candidate_indexes = _build_candidate_indexes( + sections, + candidate.observations, + contracts=contracts, + ) + prior_by_contract: dict[_CurrentContract, list[int]] = defaultdict(list) + certificates: list[RecurrenceNumericalCurrentCertificate] = [] + rejected: list[dict[str, object]] = [] + nearest: tuple[Fraction, Fraction, Fraction, int, int, str] | None = None + tested = 0 + theoretical_pair_hypotheses = 0 + screened_pair_hypotheses = 0 + zero_hypotheses = 0 + candidates = 0 + verification_rejected = 0 + rejected_hypotheses = 0 + scope_suppressed_current_count = 0 + decision_chain = _canonical_sha256( + { + "abi": _DECISION_CHAIN_ABI, + "source_semantics_sha256": source_semantics_sha256, + "candidate_capture_sha256": candidate.capture_contract_sha256, + "verification_capture_sha256": ( + None if verification is None else verification.capture_contract_sha256 + ), + } + ) + rejection_chain = _canonical_sha256( + { + "abi": _REJECTION_CHAIN_ABI, + "source_semantics_sha256": source_semantics_sha256, + "candidate_capture_sha256": candidate.capture_contract_sha256, + "verification_capture_sha256": ( + None if verification is None else verification.capture_contract_sha256 + ), + } + ) + + def record_decision( + *, + current_id: int, + representative_id: int | None, + relation_kind: _RelationKind, + candidate_residuals: _Residuals, + candidate_residual_strings: _ResidualStrings, + verification_residuals: _Residuals | None, + verification_residual_strings: _ResidualStrings | None, + selected: bool, + ) -> None: + nonlocal decision_chain + decision_chain = hashlib.sha256( + bytes.fromhex(decision_chain) + + _canonical_json_bytes( + { + "current_id": current_id, + "representative_id": representative_id, + "relation_kind": relation_kind, + "candidate_maximum_absolute_residual": ( + candidate_residual_strings[0] + ), + "candidate_maximum_relative_residual": ( + candidate_residual_strings[1] + ), + "candidate_maximum_tolerance_ratio": ( + candidate_residual_strings[2] + ), + "candidate_accepted": candidate_residuals[2] <= 1, + "verification_maximum_absolute_residual": ( + None + if verification_residual_strings is None + else verification_residual_strings[0] + ), + "verification_maximum_relative_residual": ( + None + if verification_residual_strings is None + else verification_residual_strings[1] + ), + "verification_maximum_tolerance_ratio": ( + None + if verification_residual_strings is None + else verification_residual_strings[2] + ), + "verification_accepted": ( + None + if verification_residuals is None + else verification_residuals[2] <= 1 + ), + "selected": selected, + } + ) + ).hexdigest() + + def record_rejected( + *, + current_id: int, + representative_id: int | None, + relation_kind: _RelationKind, + residuals: _Residuals, + residual_strings: _ResidualStrings, + reason: str, + ) -> None: + nonlocal nearest, rejected_hypotheses, rejection_chain + rejected_hypotheses += 1 + rejected_row = { + "current_id": current_id, + "representative_id": representative_id, + "relation_kind": relation_kind, + "reason": reason, + "maximum_absolute_residual": residual_strings[0], + "maximum_relative_residual": residual_strings[1], + "maximum_tolerance_ratio": residual_strings[2], + } + rejection_chain = hashlib.sha256( + bytes.fromhex(rejection_chain) + _canonical_json_bytes(rejected_row) + ).hexdigest() + item = ( + residuals[2], + residuals[0], + residuals[1], + current_id, + -1 if representative_id is None else representative_id, + relation_kind, + ) + if nearest is None or item < nearest: + nearest = item + if len(rejected) < _MAX_REJECTED_DIAGNOSTICS: + rejected.append(rejected_row) + + for current in sections.currents: + contract = contracts[current.semantic_id] + prior = prior_by_contract[contract] + if current.source_row != DIRECT_NONE_U32: + prior.append(current.semantic_id) + continue + if current.selector_domain_id in unsafe_selector_domains: + scope_suppressed_current_count += 1 + continue + candidate_current_observations = candidate.observations[current.semantic_id] + equal_representatives = set( + _raw_numerical_tolerance_window_ids( + candidate_indexes[contract], + candidate_current_observations, + relation_kind="equal", + current_id=current.semantic_id, + relative_tolerance=relative_fraction, + absolute_tolerance=absolute_fraction, + ) + ) + opposite_representatives = set( + _raw_numerical_tolerance_window_ids( + candidate_indexes[contract], + candidate_current_observations, + relation_kind="opposite", + current_id=current.semantic_id, + relative_tolerance=relative_fraction, + absolute_tolerance=absolute_fraction, + ) + ) + theoretical_pair_hypotheses += 2 * len(prior) + screened_pair_hypotheses += len(equal_representatives) + len( + opposite_representatives + ) + zero_hypotheses += 1 + hypotheses: list[tuple[_RelationKind, int | None]] = [ + ("zero", None), + *[ + (relation_kind, representative_id) + for representative_id in sorted( + equal_representatives | opposite_representatives + ) + for relation_kind in ( + *(("equal",) if representative_id in equal_representatives else ()), + *( + ("opposite",) + if representative_id in opposite_representatives + else () + ), + ) + ], + ] + accepted = False + verification_current_observations: Sequence[_ComplexDecimal] | None = None + for relation_kind, representative_id in hypotheses: + tested += 1 + candidate_residuals = _relation_residuals( + relation_kind, + candidate_current_observations, + ( + None + if representative_id is None + else candidate.observations[representative_id] + ), + relative_tolerance=relative_fraction, + absolute_tolerance=absolute_fraction, + ) + candidate_residual_strings = _residual_strings(candidate_residuals) + if candidate_residuals[2] > 1: + record_decision( + current_id=current.semantic_id, + representative_id=representative_id, + relation_kind=relation_kind, + candidate_residuals=candidate_residuals, + candidate_residual_strings=candidate_residual_strings, + verification_residuals=None, + verification_residual_strings=None, + selected=False, + ) + record_rejected( + current_id=current.semantic_id, + representative_id=representative_id, + relation_kind=relation_kind, + residuals=candidate_residuals, + residual_strings=candidate_residual_strings, + reason="candidate-observations-not-equal", + ) + continue + if verification is None: + raise _IndependentVerificationRequired + candidates += 1 + if verification_current_observations is None: + verification_current_observations = verification.observations[ + current.semantic_id + ] + verification_residuals = _relation_residuals( + relation_kind, + verification_current_observations, + ( + None + if representative_id is None + else verification.observations[representative_id] + ), + relative_tolerance=relative_fraction, + absolute_tolerance=absolute_fraction, + ) + verification_residual_strings = _residual_strings(verification_residuals) + if verification_residuals[2] > 1: + verification_rejected += 1 + record_decision( + current_id=current.semantic_id, + representative_id=representative_id, + relation_kind=relation_kind, + candidate_residuals=candidate_residuals, + candidate_residual_strings=candidate_residual_strings, + verification_residuals=verification_residuals, + verification_residual_strings=(verification_residual_strings), + selected=False, + ) + record_rejected( + current_id=current.semantic_id, + representative_id=representative_id, + relation_kind=relation_kind, + residuals=verification_residuals, + residual_strings=verification_residual_strings, + reason="independent-verification-rejected-candidate", + ) + continue + if relation_kind == "zero": + execution_representative_id = prior[0] if prior else current.semantic_id + else: + if representative_id is None: + raise AssertionError( + "non-zero recurrence relation lacks a representative" + ) + execution_representative_id = representative_id + certificates.append( + _build_certificate( + current=current, + representative_id=representative_id, + execution_representative_id=(execution_representative_id), + relation_kind=relation_kind, + source_semantics_sha256=source_semantics_sha256, + candidate=candidate, + verification=verification, + candidate_residuals=candidate_residuals, + candidate_residual_strings=candidate_residual_strings, + verification_residuals=verification_residuals, + verification_residual_strings=(verification_residual_strings), + precision_digits=precision_digits, + seed=seed, + relative_tolerance=relative_tolerance, + absolute_tolerance=absolute_tolerance, + ) + ) + record_decision( + current_id=current.semantic_id, + representative_id=representative_id, + relation_kind=relation_kind, + candidate_residuals=candidate_residuals, + candidate_residual_strings=candidate_residual_strings, + verification_residuals=verification_residuals, + verification_residual_strings=verification_residual_strings, + selected=True, + ) + accepted = True + break + prior.append(current.semantic_id) + if accepted: + continue + + nearest_payload = None + if nearest is not None: + ratio, absolute_residual, relative_residual, current_id, rep, kind = nearest + nearest_payload = { + "current_id": current_id, + "representative_id": None if rep < 0 else rep, + "relation_kind": kind, + "maximum_absolute_residual": _fraction_string(absolute_residual), + "maximum_relative_residual": _fraction_string(relative_residual), + "maximum_tolerance_ratio": _fraction_string(ratio), + } + certificate_tuple = tuple(certificates) + decision_sha256 = _canonical_sha256( + { + "abi": _DECISION_CHAIN_ABI, + "chain_tail_sha256": decision_chain, + "tested_hypothesis_count": tested, + "theoretical_pair_hypothesis_count": (theoretical_pair_hypotheses), + "screened_pair_hypothesis_count": screened_pair_hypotheses, + "zero_hypothesis_count": zero_hypotheses, + "numerical_candidate_count": candidates, + "verification_rejected_count": verification_rejected, + "rejected_hypothesis_count": rejected_hypotheses, + "certified_relation_count": len(certificate_tuple), + } + ) + rejection_sha256 = rejection_chain + rejected_diagnostics = { + "total_rejected_hypothesis_count": rejected_hypotheses, + "retained_count": len(rejected), + "truncated": rejected_hypotheses > len(rejected), + "truncation_policy": ( + f"first-{_MAX_REJECTED_DIAGNOSTICS}-in-canonical-decision-order-v1" + ), + "retained_sha256": _canonical_sha256( + { + "abi": ("pyamplicol-recurrence-rejected-numerical-diagnostics-v1"), + "rows": rejected, + } + ), + "full_decision_sha256": decision_sha256, + "full_rejection_sha256": rejection_sha256, + } + report: dict[str, object] = { + "schema_version": 1, + "state": ( + "certified_numerical_relations" + if certificate_tuple + else "no_certified_numerical_relation" + ), + "scope": { + "execution_mode": "recurrence", + "color_accuracy": color_accuracy, + "representation": "recurrence-direct-plan-v2", + "lc_flow_layout": sections.strategy, + }, + "application_scope": { + **application_scope, + "suppressed_current_count": scope_suppressed_current_count, + }, + "source_semantics": { + "abi": _SOURCE_ABI, + "sha256": source_semantics_sha256, + }, + "probe_contract": { + "algorithm": _RECURRENCE_CERTIFICATE_ALGORITHM, + "precision_digits": precision_digits, + "seed": seed, + "relative_tolerance_binary64": relative_tolerance.hex(), + "absolute_tolerance_binary64": absolute_tolerance.hex(), + "candidate_point_sha256s": list(candidate.point_sha256s), + "verification_point_sha256s": ( + [] if verification is None else list(verification.point_sha256s) + ), + "candidate_context_sha256s": list(candidate.context_sha256s), + "verification_context_sha256s": ( + [] if verification is None else list(verification.context_sha256s) + ), + "candidate_parameter_context_sha256s": list( + candidate.parameter_context_sha256s + ), + "verification_parameter_context_sha256s": list( + () if verification is None else verification.parameter_context_sha256s + ), + "runtime_parameter_schema_sha256": ( + candidate.runtime_parameter_schema_sha256 + ), + "candidate_capture_sha256": candidate.capture_contract_sha256, + "verification_capture_sha256": ( + None if verification is None else verification.capture_contract_sha256 + ), + "candidate_observation_batch_sha256": (candidate.observation_batch_sha256), + "verification_observation_batch_sha256": ( + None if verification is None else verification.observation_batch_sha256 + ), + "deterministic": True, + "independent_verification": verification is not None, + **( + {"verification_status": ("not-required-no-candidate-hypothesis")} + if verification is None + else {} + ), + "current_dimension_bound": True, + }, + "inspected_current_count": len(sections.currents), + "structurally_proven_current_count": 0, + "tested_hypothesis_count": tested, + "candidate_index": { + "algorithm": _CANDIDATE_INDEX_ALGORITHM, + "completeness": "complete-within-configured-tolerance", + "contract_count": len(candidate_indexes), + "exhaustive_fallback_contract_count": ( + len(candidate_indexes) if relative_fraction >= 1 else 0 + ), + "theoretical_pair_hypothesis_count": (theoretical_pair_hypotheses), + "screened_pair_hypothesis_count": screened_pair_hypotheses, + "zero_hypothesis_count": zero_hypotheses, + "nearest_rejected_scope": ("zero-and-tolerance-window-screened-hypotheses"), + }, + "numerical_candidate_count": candidates, + "verification_rejected_count": verification_rejected, + "rejected_hypothesis_count": rejected_hypotheses, + "decision_sha256": decision_sha256, + "rejection_decision_sha256": rejection_sha256, + "certified_numerical_relation_count": len(certificate_tuple), + "certificates": [ + certificate.to_json_dict() for certificate in certificate_tuple + ], + "rejected_candidates": rejected, + "rejected_candidate_diagnostics": rejected_diagnostics, + "nearest_rejected_hypothesis": nearest_payload, + "warning": _warning_payload(False), + } + return certificate_tuple, report + + +def _build_certificate( + *, + current: _Current, + representative_id: int | None, + execution_representative_id: int, + relation_kind: _RelationKind, + source_semantics_sha256: str, + candidate: RecurrenceCurrentObservationCapture, + verification: RecurrenceCurrentObservationCapture, + candidate_residuals: _Residuals, + candidate_residual_strings: _ResidualStrings, + verification_residuals: _Residuals, + verification_residual_strings: _ResidualStrings, + precision_digits: int, + seed: int, + relative_tolerance: float, + absolute_tolerance: float, +) -> RecurrenceNumericalCurrentCertificate: + factor = { + "equal": (1, 0), + "opposite": (-1, 0), + "zero": (0, 0), + }[relation_kind] + candidate_digest = _relation_observation_sha256( + current.semantic_id, + representative_id, + relation_kind, + candidate, + ) + verification_digest = _relation_observation_sha256( + current.semantic_id, + representative_id, + relation_kind, + verification, + ) + probe_contract = { + "algorithm": _RECURRENCE_CERTIFICATE_ALGORITHM, + "source_semantics_sha256": source_semantics_sha256, + "current_id": current.semantic_id, + "representative_id": representative_id, + "execution_representative_id": execution_representative_id, + "relation_kind": relation_kind, + "precision_digits": precision_digits, + "seed": seed, + "candidate_domain": "candidate-current-probes-v1", + "verification_domain": ("independent-verification-current-probes-v1"), + "relative_tolerance_binary64": relative_tolerance.hex(), + "absolute_tolerance_binary64": absolute_tolerance.hex(), + "candidate_probe_count": candidate.point_count, + "verification_probe_count": verification.point_count, + "current_dimension": current.component_count, + "runtime_parameter_schema_sha256": (candidate.runtime_parameter_schema_sha256), + "candidate_capture_sha256": candidate.capture_contract_sha256, + "verification_capture_sha256": verification.capture_contract_sha256, + "candidate_observations_sha256": candidate_digest, + "verification_observations_sha256": verification_digest, + } + probe_digest = _canonical_sha256(probe_contract) + proof_payload = { + **probe_contract, + "proof_kind": "authenticated-numerical", + "factor_integer": list(factor), + "candidate_maximum_absolute_residual": candidate_residual_strings[0], + "candidate_maximum_relative_residual": candidate_residual_strings[1], + "candidate_maximum_tolerance_ratio": candidate_residual_strings[2], + "verification_maximum_absolute_residual": verification_residual_strings[0], + "verification_maximum_relative_residual": verification_residual_strings[1], + "verification_maximum_tolerance_ratio": verification_residual_strings[2], + "probe_contract_sha256": probe_digest, + } + return RecurrenceNumericalCurrentCertificate( + current_id=current.semantic_id, + representative_id=representative_id, + execution_representative_id=execution_representative_id, + relation_kind=relation_kind, + factor=factor, + source_semantics_sha256=source_semantics_sha256, + precision_digits=precision_digits, + seed=seed, + relative_tolerance=relative_tolerance, + absolute_tolerance=absolute_tolerance, + candidate_probe_count=candidate.point_count, + verification_probe_count=verification.point_count, + current_dimension=current.component_count, + runtime_parameter_schema_sha256=(candidate.runtime_parameter_schema_sha256), + candidate_capture_sha256=candidate.capture_contract_sha256, + verification_capture_sha256=verification.capture_contract_sha256, + candidate_maximum_absolute_residual=candidate_residuals[0], + candidate_maximum_relative_residual=candidate_residuals[1], + candidate_maximum_tolerance_ratio=candidate_residuals[2], + verification_maximum_absolute_residual=verification_residuals[0], + verification_maximum_relative_residual=verification_residuals[1], + verification_maximum_tolerance_ratio=verification_residuals[2], + candidate_observations_sha256=candidate_digest, + verification_observations_sha256=verification_digest, + probe_contract_sha256=probe_digest, + proof_sha256=_canonical_sha256(proof_payload), + ) + + +def _evidence_payload( + sections: _RecurrenceExactSectionsV1, + *, + mode: _Mode, + source_semantics: Mapping[str, object], + source_semantics_sha256: str, + certificates: tuple[RecurrenceNumericalCurrentCertificate, ...], + discovery: Mapping[str, object], + precision_digits: int, + seed: int, + relative_tolerance: float, + absolute_tolerance: float, + probe_count: int, + verification_probe_count: int, + runtime_parameter_schema: Mapping[str, object], + candidate: RecurrenceCurrentObservationCapture, + verification: RecurrenceCurrentObservationCapture, +) -> dict[str, object]: + numerical_candidate_count = _required_report_integer( + discovery, + "numerical_candidate_count", + ) + verification_rejected_count = _required_report_integer( + discovery, + "verification_rejected_count", + ) + tested_hypothesis_count = _required_report_integer( + discovery, + "tested_hypothesis_count", + ) + rejected_hypothesis_count = _required_report_integer( + discovery, + "rejected_hypothesis_count", + ) + return { + "abi": _EVIDENCE_ABI, + "requested_mode": mode, + "schedule_semantic_digest": sections.semantic_digest, + "baseline_runtime_layout_digest": sections.runtime_layout_digest, + "source_semantics": source_semantics, + "source_semantics_sha256": source_semantics_sha256, + "runtime_parameter_schema": dict(runtime_parameter_schema), + "runtime_parameter_schema_sha256": (candidate.runtime_parameter_schema_sha256), + "candidate_capture": _capture_evidence_payload(candidate), + "verification_capture": _capture_evidence_payload(verification), + "certificate_algorithm": _RECURRENCE_CERTIFICATE_ALGORITHM, + "certificate_set_sha256": _certificate_set_sha256(certificates), + "precision_digits": precision_digits, + "probe_count": probe_count, + "verification_probe_count": verification_probe_count, + "relative_tolerance_binary64": relative_tolerance.hex(), + "absolute_tolerance_binary64": absolute_tolerance.hex(), + "seed": seed, + "candidate_index": dict( + cast(Mapping[str, object], discovery["candidate_index"]) + ), + "numerical_candidate_count": numerical_candidate_count, + "verification_rejected_count": verification_rejected_count, + "rejected_hypothesis_count": rejected_hypothesis_count, + "tested_hypothesis_count": tested_hypothesis_count, + "decision_sha256": str(discovery["decision_sha256"]), + "rejection_decision_sha256": str(discovery["rejection_decision_sha256"]), + "certificates": _CertificateRowsStream(certificates), + "mappings": _MappingRowsStream(certificates), + } + + +def _capture_evidence_payload( + capture: RecurrenceCurrentObservationCapture, +) -> dict[str, object]: + """Return a small mapping whose large rows are streamed during encoding.""" + + return { + "abi": _CAPTURE_ABI, + "precision_digits": capture.precision_digits, + "point_count": capture.point_count, + "point_sha256s": capture.point_sha256s, + "kinematic_sha256s": capture.kinematic_sha256s, + "parameter_contexts": tuple( + tuple(_decimal_string(value) for value in context) + for context in capture.parameter_contexts + ), + "parameter_context_sha256s": capture.parameter_context_sha256s, + "context_sha256s": capture.context_sha256s, + "points": tuple(point.to_mapping() for point in capture.points), + "current_count": capture.current_count, + "runtime_parameter_schema_sha256": (capture.runtime_parameter_schema_sha256), + "source_semantics_sha256": capture.source_semantics_sha256, + "observation_batch_sha256": capture.observation_batch_sha256, + "capture_contract_sha256": capture.capture_contract_sha256, + "context_policy": capture.context_policy, + "complete_current_components": True, + "point_major": True, + "current_dimensions_sha256": _canonical_sha256( + { + str(current_id): dimension + for current_id, dimension in sorted(capture.current_dimensions.items()) + } + ), + "evaluator": "recurrence-direct-plan-decimal-symbolica-exact", + "kinematic_binary64": capture.kinematic_binary64, + "selector_contexts": capture.selector_contexts, + "current_dimensions": _DimensionRowsStream(capture.current_dimensions), + "observations": _ObservationRowsStream( + capture.observations, + capture.current_dimensions, + ), + } + + +def _required_report_integer( + report: Mapping[str, object], + key: str, +) -> int: + value = report.get(key) + if type(value) is not int: + raise ValueError(f"recurrence numerical report {key!r} is not an integer") + return value + + +def _mapping_payload( + certificate: RecurrenceNumericalCurrentCertificate, +) -> dict[str, object]: + return { + "current_id": certificate.current_id, + "representative_id": certificate.representative_id, + "execution_representative_id": (certificate.execution_representative_id), + "relation_kind": certificate.relation_kind, + "factor_integer": list(certificate.factor), + "current_dimension": certificate.current_dimension, + "certificate_proof_sha256": certificate.proof_sha256, + "candidate_observations_sha256": (certificate.candidate_observations_sha256), + "verification_observations_sha256": ( + certificate.verification_observations_sha256 + ), + } + + +def _current_contracts( + sections: _RecurrenceExactSectionsV1, +) -> tuple[_CurrentContract, ...]: + finalization_executors: dict[int, int] = {} + for group in sections.row_groups: + if group.role == 2: + for row_id in range(group.row_start, group.row_start + group.row_count): + if row_id in finalization_executors: + raise ArtifactError( + "recurrence finalization row belongs to multiple groups" + ) + finalization_executors[row_id] = group.executor_id + factors = tuple( + ( + str(factor.real_numerator), + str(factor.real_denominator), + str(factor.imaginary_numerator), + str(factor.imaginary_denominator), + ) + for factor in sections.exact_factors + ) + result = [] + for current in sections.currents: + if current.finalization_row == DIRECT_NONE_U32: + finalization = ( + DIRECT_NONE_U32, + DIRECT_NONE_U32, + ("1", "1", "0", "1"), + ) + else: + try: + row = sections.finalizations[current.finalization_row] + executor_id = finalization_executors[current.finalization_row] + factor = factors[row.exact_factor_id] + except (IndexError, KeyError) as exc: + raise ArtifactError( + f"recurrence current {current.semantic_id} has an invalid " + "finalization contract" + ) from exc + finalization = (executor_id, row.momentum_form_id, factor) + result.append( + ( + current.node_kind, + current.stage, + current.state_template_id, + current.component_count, + current.momentum_form_id, + current.selector_domain_id, + *finalization, + ) + ) + return tuple(result) + + +def _validate_independent_captures( + candidate: RecurrenceCurrentObservationCapture, + verification: RecurrenceCurrentObservationCapture, +) -> None: + if ( + candidate.precision_digits != verification.precision_digits + or candidate.source_semantics_sha256 != verification.source_semantics_sha256 + or candidate.runtime_parameter_schema_sha256 + != verification.runtime_parameter_schema_sha256 + or candidate.current_dimensions != verification.current_dimensions + or set(candidate.observations) != set(verification.observations) + or candidate.point_count < 2 + or verification.point_count < 2 + or not set(candidate.point_sha256s).isdisjoint(verification.point_sha256s) + or not set(candidate.kinematic_sha256s).isdisjoint( + verification.kinematic_sha256s + ) + or len(candidate.parameter_context_sha256s) != candidate.point_count + or len(verification.parameter_context_sha256s) != verification.point_count + or not set(candidate.parameter_context_sha256s).isdisjoint( + verification.parameter_context_sha256s + ) + ): + raise ValueError( + "recurrence candidate and verification captures are not independent" + ) + for current_id, dimension in candidate.current_dimensions.items(): + if ( + len(candidate.observations[current_id]) != candidate.point_count * dimension + or len(verification.observations[current_id]) + != verification.point_count * dimension + ): + raise ValueError(f"recurrence current {current_id} probe geometry drifted") + + +def _validate_applied_observations( + reference: RecurrenceCurrentObservationCapture, + applied: RecurrenceCurrentObservationCapture, + *, + relative_tolerance: float, + absolute_tolerance: float, +) -> dict[str, object]: + if ( + reference.precision_digits != applied.precision_digits + or reference.point_sha256s != applied.point_sha256s + or reference.kinematic_sha256s != applied.kinematic_sha256s + or reference.kinematic_binary64 != applied.kinematic_binary64 + or reference.selector_contexts != applied.selector_contexts + or reference.context_sha256s != applied.context_sha256s + or reference.parameter_contexts != applied.parameter_contexts + or reference.parameter_context_sha256s != applied.parameter_context_sha256s + or reference.runtime_parameter_schema_sha256 + != applied.runtime_parameter_schema_sha256 + or reference.current_dimensions != applied.current_dimensions + or set(reference.observations) != set(applied.observations) + ): + raise ValueError("recurrence application validation provenance drifted") + relative, absolute = _validated_tolerances( + relative_tolerance, + absolute_tolerance, + ) + relative_fraction = Fraction(relative) + absolute_fraction = Fraction(absolute) + maximum_absolute = Fraction(0) + maximum_relative = Fraction(0) + maximum_ratio = Fraction(0) + checked = 0 + for current_id in sorted(reference.observations): + before = reference.observations[current_id] + after = applied.observations[current_id] + if len(before) != len(after): + raise ValueError( + f"recurrence current {current_id} application width drifted" + ) + residuals = _pair_residuals( + before, + after, + sign=1, + relative_tolerance=relative_fraction, + absolute_tolerance=absolute_fraction, + ) + maximum_absolute = max(maximum_absolute, residuals[0]) + maximum_relative = max(maximum_relative, residuals[1]) + maximum_ratio = max(maximum_ratio, residuals[2]) + checked += len(before) + if residuals[2] > 1: + raise ValueError( + "recurrence numerical reuse changed current " + f"{current_id} beyond its authenticated tolerance" + ) + return { + "status": "verified", + "checked_current_count": len(reference.observations), + "checked_component_count": checked, + "maximum_absolute_residual": _fraction_string(maximum_absolute), + "maximum_relative_residual": _fraction_string(maximum_relative), + "maximum_tolerance_ratio": _fraction_string(maximum_ratio), + "reference_observation_batch_sha256": (reference.observation_batch_sha256), + "applied_observation_batch_sha256": (applied.observation_batch_sha256), + } + + +def _validate_recaptured_reference( + authenticated: RecurrenceCurrentObservationCapture, + recaptured: RecurrenceCurrentObservationCapture, +) -> None: + """Bind a post-native baseline recapture to the original full commitment.""" + + if ( + authenticated.precision_digits != recaptured.precision_digits + or authenticated.points != recaptured.points + or authenticated.point_sha256s != recaptured.point_sha256s + or authenticated.kinematic_sha256s != recaptured.kinematic_sha256s + or authenticated.kinematic_binary64 != recaptured.kinematic_binary64 + or authenticated.selector_contexts != recaptured.selector_contexts + or authenticated.context_sha256s != recaptured.context_sha256s + or authenticated.parameter_contexts != recaptured.parameter_contexts + or authenticated.parameter_context_sha256s + != recaptured.parameter_context_sha256s + or authenticated.runtime_parameter_schema_sha256 + != recaptured.runtime_parameter_schema_sha256 + or authenticated.source_semantics_sha256 != recaptured.source_semantics_sha256 + or authenticated.current_dimensions != recaptured.current_dimensions + or authenticated.current_count != recaptured.current_count + or authenticated.observation_batch_sha256 != recaptured.observation_batch_sha256 + or authenticated.capture_contract_sha256 != recaptured.capture_contract_sha256 + or authenticated.context_policy != recaptured.context_policy + ): + raise ValueError( + "recurrence baseline application reference did not reproduce its " + "authenticated capture commitment" + ) + + +def _relation_residuals( + relation_kind: _RelationKind, + current: Sequence[_ComplexDecimal], + representative: Sequence[_ComplexDecimal] | None, + *, + relative_tolerance: _Tolerance, + absolute_tolerance: _Tolerance, +) -> _Residuals: + if relation_kind == "zero": + return _zero_residuals( + current, + relative_tolerance=relative_tolerance, + absolute_tolerance=absolute_tolerance, + ) + if representative is None or len(current) != len(representative): + raise ValueError("recurrence numerical relation width is invalid") + return _pair_residuals( + current, + representative, + sign=1 if relation_kind == "equal" else -1, + relative_tolerance=relative_tolerance, + absolute_tolerance=absolute_tolerance, + ) + + +def _zero_residuals( + current: Sequence[_ComplexDecimal], + *, + relative_tolerance: _Tolerance, + absolute_tolerance: _Tolerance, +) -> _Residuals: + maximum_absolute = Fraction(0) + maximum_relative = Fraction(0) + maximum_ratio = Fraction(0) + relative_fraction = _tolerance_fraction(relative_tolerance) + absolute_fraction = _tolerance_fraction(absolute_tolerance) + for current_value in current: + current_real = Fraction(current_value[0]) + current_imaginary = Fraction(current_value[1]) + difference = max(abs(current_real), abs(current_imaginary)) + allowed = absolute_fraction + relative_fraction * difference + relative = ( + Fraction(0) + if difference == 0 + else difference / max(difference, absolute_fraction) + ) + ratio = Fraction(0) if difference == 0 else difference / allowed + maximum_absolute = max(maximum_absolute, difference) + maximum_relative = max(maximum_relative, relative) + maximum_ratio = max(maximum_ratio, ratio) + return maximum_absolute, maximum_relative, maximum_ratio + + +def _pair_residuals( + left: Sequence[_ComplexDecimal], + right: Sequence[_ComplexDecimal], + *, + sign: int, + relative_tolerance: _Tolerance, + absolute_tolerance: _Tolerance, +) -> _Residuals: + if len(left) != len(right): + raise ValueError("recurrence numerical comparison width is invalid") + maximum_absolute = Fraction(0) + maximum_relative = Fraction(0) + maximum_ratio = Fraction(0) + relative_fraction = _tolerance_fraction(relative_tolerance) + absolute_fraction = _tolerance_fraction(absolute_tolerance) + for left_value, right_value in zip(left, right, strict=True): + left_real = Fraction(left_value[0]) + left_imaginary = Fraction(left_value[1]) + right_real = Fraction(right_value[0]) + right_imaginary = Fraction(right_value[1]) + signed = ( + (right_real, right_imaginary) + if sign == 1 + else (-right_real, -right_imaginary) + ) + difference = max( + abs(left_real - signed[0]), + abs(left_imaginary - signed[1]), + ) + scale = max( + abs(left_real), + abs(left_imaginary), + abs(right_real), + abs(right_imaginary), + ) + allowed = absolute_fraction + relative_fraction * scale + relative = ( + Fraction(0) + if difference == 0 + else difference / max(scale, absolute_fraction) + ) + ratio = Fraction(0) if difference == 0 else difference / allowed + maximum_absolute = max(maximum_absolute, difference) + maximum_relative = max(maximum_relative, relative) + maximum_ratio = max(maximum_ratio, ratio) + return maximum_absolute, maximum_relative, maximum_ratio + + +def _tolerance_fraction(value: _Tolerance) -> Fraction: + return value if isinstance(value, Fraction) else Fraction(value) + + +def _relation_observation_sha256( + current_id: int, + representative_id: int | None, + relation_kind: _RelationKind, + capture: RecurrenceCurrentObservationCapture, +) -> str: + return _canonical_sha256( + { + "abi": _RELATION_OBSERVATION_ABI, + "capture_contract_sha256": capture.capture_contract_sha256, + "current_id": current_id, + "representative_id": representative_id, + "relation_kind": relation_kind, + "current_dimension": capture.current_dimensions[current_id], + "current_values": [ + [_decimal_string(real), _decimal_string(imaginary)] + for real, imaginary in capture.observations[current_id] + ], + "representative_values": ( + None + if representative_id is None + else [ + [_decimal_string(real), _decimal_string(imaginary)] + for real, imaginary in capture.observations[representative_id] + ] + ), + } + ) + + +def _runtime_parameter_defaults( + plan: _RecurrenceExactPlan, +) -> tuple[Decimal, ...]: + if ( + len(plan.runtime_parameter_schema) != len(plan.runtime_defaults) + or any( + row.runtime_slot != index + for index, row in enumerate(plan.runtime_parameter_schema) + ) + or any( + row.runtime_slot >= len(plan.runtime_defaults) + for row in plan.parameter_projection + ) + ): + raise ArtifactError( + "recurrence runtime parameter schema/defaults are incomplete" + ) + defaults: list[Decimal] = [] + for row, default in zip( + plan.runtime_parameter_schema, plan.runtime_defaults, strict=True + ): + if row.probe_policy == "derived-overwritten-fixed-zero-v1": + defaults.append(Decimal(0)) + continue + if row.probe_policy != "native-template-default-perturbed-v1": + raise ArtifactError( + "recurrence runtime parameter schema has an unsupported " + "numerical probe policy" + ) + binary64_default = float(default) + if not isfinite(binary64_default): + raise ArtifactError( + "recurrence runtime parameter default is not finite binary64" + ) + defaults.append(Decimal.from_float(binary64_default)) + return tuple(defaults) + + +def _runtime_parameter_schema_payload( + plan: _RecurrenceExactPlan, + *, + defaults: tuple[Decimal, ...] | None = None, +) -> dict[str, object]: + if defaults is None: + defaults = _runtime_parameter_defaults(plan) + return { + "abi": _PARAMETER_SCHEMA_ABI, + "parameters": [ + { + "runtime_slot": row.runtime_slot, + "runtime_name": row.runtime_name, + "parameter_template_id": row.parameter_template_id, + "prepared_parameter_id": row.prepared_parameter_id, + "component": row.component, + "default_binary64": float(default).hex(), + "probe_policy": row.probe_policy, + } + for row, default in zip( + plan.runtime_parameter_schema, defaults, strict=True + ) + ], + } + + +def _build_warmup_static_context( + plan: _RecurrenceExactPlan, +) -> _WarmupStaticContext: + """Build immutable generation-scoped data shared by every warm-up pass.""" + + current_dimensions = { + current.semantic_id: current.component_count + for current in plan.sections.currents + } + current_contracts = _current_contracts(plan.sections) + source_semantics = _source_semantics_payload( + plan.sections, + contracts=current_contracts, + ) + source_semantics_sha256 = _canonical_sha256(source_semantics) + runtime_defaults = _runtime_parameter_defaults(plan) + runtime_parameter_schema = _runtime_parameter_schema_payload( + plan, + defaults=runtime_defaults, + ) + return _WarmupStaticContext( + current_dimensions=current_dimensions, + current_contracts=current_contracts, + runtime_defaults=runtime_defaults, + runtime_parameter_schema=runtime_parameter_schema, + runtime_parameter_schema_sha256=_canonical_sha256(runtime_parameter_schema), + source_semantics=source_semantics, + source_semantics_sha256=source_semantics_sha256, + ) + + +def _build_parameter_probe_contexts( + defaults: tuple[Decimal, ...], + *, + probe_policies: tuple[str, ...], + precision_digits: int, + seed: int, + domain: str, + count: int, + include_defaults: bool, +) -> tuple[tuple[Decimal, ...], ...]: + if ( + type(precision_digits) is not int + or precision_digits < 80 + or type(seed) is not int + or seed < 0 + or not isinstance(domain, str) + or not domain + or type(count) is not int + or count < 2 + or len(probe_policies) != len(defaults) + or any( + policy + not in { + "native-template-default-perturbed-v1", + "derived-overwritten-fixed-zero-v1", + } + for policy in probe_policies + ) + or any(not value.is_finite() for value in defaults) + ): + raise ValueError("recurrence numerical parameter-probe contract is invalid") + contexts: list[tuple[Decimal, ...]] = [] + for probe_index in range(count): + if include_defaults and probe_index == 0: + contexts.append(defaults) + continue + values: list[Decimal] = [] + for parameter_index, (default, probe_policy) in enumerate( + zip(defaults, probe_policies, strict=True) + ): + if probe_policy == "derived-overwritten-fixed-zero-v1": + values.append(default) + continue + digest = hashlib.sha256( + (f"{seed}:{domain}:{probe_index}:{parameter_index}").encode("ascii") + ).digest() + signed = int.from_bytes(digest[:8], "big") - (1 << 63) + if signed == 0: + signed = 1 + scale = max(abs(Fraction(default)), Fraction(1)) + value = Fraction(default) + scale * Fraction(signed, 1 << 67) + values.append(_terminating_fraction_decimal(value)) + contexts.append(tuple(values)) + return tuple(contexts) + + +def _validated_tolerances( + relative_tolerance: float, + absolute_tolerance: float, +) -> tuple[Decimal, Decimal]: + if ( + isinstance(relative_tolerance, bool) + or not isinstance(relative_tolerance, int | float) + or isinstance(absolute_tolerance, bool) + or not isinstance(absolute_tolerance, int | float) + or not isfinite(relative_tolerance) + or not isfinite(absolute_tolerance) + or relative_tolerance < 0 + or absolute_tolerance < 0 + or ( + float(relative_tolerance) == 0.0 + and copysign(1.0, float(relative_tolerance)) < 0 + ) + or ( + float(absolute_tolerance) == 0.0 + and copysign(1.0, float(absolute_tolerance)) < 0 + ) + or (relative_tolerance == 0 and absolute_tolerance == 0) + ): + raise ValueError("recurrence numerical current tolerances are invalid") + return ( + Decimal.from_float(float(relative_tolerance)), + Decimal.from_float(float(absolute_tolerance)), + ) + + +def _certificate_set_sha256( + certificates: Sequence[RecurrenceNumericalCurrentCertificate], +) -> str: + return _canonical_sha256( + { + "abi": _RELATION_SET_ABI, + "certificates": _CertificateRowsStream(certificates), + "mappings": _MappingRowsStream(certificates), + } + ) + + +def _kinematic_binary64( + point: ValidationPointRecord, +) -> tuple[tuple[str, ...], ...]: + return tuple( + tuple(float(value).hex() for value in momentum) + for momentum in point.four_vectors + ) + + +def _kinematic_sha256(point: ValidationPointRecord) -> str: + return _canonical_sha256(_kinematic_binary64(point)) + + +def _domain_seed(seed: int, *, domain: str, index: int) -> int: + digest = hashlib.sha256(f"{seed}:{domain}:{index}".encode("ascii")).digest() + return int.from_bytes(digest[:8], "big") + + +def _context_policy(strategy: str) -> str: + return { + "topology-replay": "seeded-replay-target-per-physical-point-v1", + "all-flow-union": ("seeded-helicity-per-selector-domain-and-physical-point-v1"), + "contracted-color-union": "fixed-contracted-source-schedule-v1", + }[strategy] + + +def _plain_contract(contract: _CurrentContract) -> list[object]: + result = [] + for value in contract: + result.append(list(value) if isinstance(value, tuple) else value) + return result + + +def _warning_payload(required: bool) -> dict[str, object]: + return ( + { + "required": True, + "emit": "once-per-generated-artifact", + "code": NUMERICAL_CURRENT_RELATION_WARNING_CODE, + "message": NUMERICAL_CURRENT_RELATION_WARNING, + } + if required + else { + "required": False, + "emit": "never", + "code": None, + "message": None, + } + ) + + +def numerical_relation_correctness_payload( + applied_relation_count: int, +) -> dict[str, object]: + if type(applied_relation_count) is not int or applied_relation_count < 0: + raise ValueError("numerical relation application count is invalid") + return { + "abi": NUMERICAL_RELATION_CORRECTNESS_ABI, + "state": ( + "member-scoped-v1" if applied_relation_count else "no-applied-relations" + ), + "applied_relation_count": applied_relation_count, + } + + +def recurrence_numerical_relation_correctness_summary( + report: Mapping[str, object], +) -> dict[str, object]: + """Extract the compact metadata needed for lightweight result reuse.""" + + value = report.get("relation_correctness") + if not isinstance(value, Mapping): + raise ValueError( + "recurrence numerical relation report has no correctness state" + ) + applied = report.get("applied_relation_count") + if type(applied) is not int or applied < 0: + raise ValueError( + "recurrence numerical relation report has an invalid applied count" + ) + expected = numerical_relation_correctness_payload(applied) + if any(value.get(key) != expected[key] for key in expected): + raise ValueError( + "recurrence numerical relation correctness state is inconsistent" + ) + return dict(expected) + + +def validate_recurrence_numerical_evidence_fallback( + value: object, +) -> dict[str, object]: + """Validate one full evidence-envelope fallback payload.""" + + required_fields = { + "reason", + "memory_envelope_bytes", + "spooled_producer_resident_bytes", + "geometry", + } + if not isinstance(value, Mapping) or set(value) != required_fields: + raise ValueError("recurrence numerical current fallback provenance is invalid") + if value.get("reason") != EVIDENCE_ENVELOPE_FALLBACK_REASON: + raise ValueError("recurrence numerical current fallback reason is invalid") + + def required_integer( + raw: object, + *, + name: str, + minimum: int, + ) -> int: + if type(raw) is not int or raw < minimum: + raise ValueError(f"recurrence numerical fallback {name} is invalid") + return raw + + memory_envelope_bytes = required_integer( + value.get("memory_envelope_bytes"), + name="memory envelope", + minimum=1, + ) + producer_resident_bytes = required_integer( + value.get("spooled_producer_resident_bytes"), + name="producer resident bound", + minimum=1, + ) + raw_geometry = value.get("geometry") + geometry_fields = { + "current_count", + "component_count", + "candidate_probe_count", + "verification_probe_count", + "runtime_parameter_count", + "scalar_count", + "row_count", + } + if not isinstance(raw_geometry, Mapping) or set(raw_geometry) != geometry_fields: + raise ValueError("recurrence numerical current fallback geometry is invalid") + counts = { + key: required_integer( + raw_geometry.get(key), + name=f"geometry {key}", + minimum=( + 1 if key in {"candidate_probe_count", "verification_probe_count"} else 0 + ), + ) + for key in geometry_fields + } + geometry = RecurrenceNumericalEvidenceGeometry.from_counts( + current_count=counts["current_count"], + component_count=counts["component_count"], + candidate_probe_count=counts["candidate_probe_count"], + verification_probe_count=counts["verification_probe_count"], + runtime_parameter_count=counts["runtime_parameter_count"], + ) + expected_geometry = geometry.to_json_dict() + expected_producer_resident_bytes = geometry.spooled_producer_resident_upper_bound() + # A fallback may occur either before capture because this producer bound + # exceeds the envelope, or later at a recognized canonical/native + # transport boundary. In both cases the recomputed bound must be exact; + # it need not itself be the boundary that fired. + if ( + counts != expected_geometry + or producer_resident_bytes != expected_producer_resident_bytes + ): + raise ValueError( + "recurrence numerical current fallback geometry is inconsistent" + ) + return { + "reason": EVIDENCE_ENVELOPE_FALLBACK_REASON, + "memory_envelope_bytes": memory_envelope_bytes, + "spooled_producer_resident_bytes": producer_resident_bytes, + "geometry": expected_geometry, + } + + +def recurrence_numerical_relation_fallback_summary( + report: Mapping[str, object], +) -> dict[str, object] | None: + """Extract lightweight fallback provenance without expanding correctness ABI.""" + + fallback = report.get("fallback") + if fallback is None: + if ( + report.get("requested_mode") == "certified-reuse" + and report.get("effective_mode") == "off" + ): + raise ValueError( + "recurrence numerical relation fallback provenance is absent" + ) + return None + certified = _required_report_integer(report, "certified_relation_count") + applied = _required_report_integer(report, "applied_relation_count") + if ( + report.get("requested_mode") != "certified-reuse" + or report.get("effective_mode") != "off" + or report.get("effective_reuse_state") != "disabled" + or certified != 0 + or applied != 0 + ): + raise ValueError("recurrence numerical relation fallback state is inconsistent") + validated = validate_recurrence_numerical_evidence_fallback(fallback) + return { + "abi": NUMERICAL_RELATION_FALLBACK_ABI, + "requested_mode": "certified-reuse", + "effective_mode": "off", + "effective_reuse_state": "disabled", + "reason": EVIDENCE_ENVELOPE_FALLBACK_REASON, + "geometry": dict(cast(Mapping[str, int], validated["geometry"])), + "certified_relation_count": 0, + "applied_relation_count": 0, + } + + +def _decimal_string(value: Decimal) -> str: + if not value.is_finite(): + raise ValueError("non-finite Decimal cannot be canonicalized") + if value == 0: + return "0" + text = format(value, "f") + if "." in text: + text = text.rstrip("0").rstrip(".") + return "0" if text in {"-0", "+0", ""} else text + + +def _fraction_string(value: Fraction) -> str: + return ( + str(value.numerator) + if value.denominator == 1 + else f"{value.numerator}/{value.denominator}" + ) + + +def _residual_strings(residuals: _Residuals) -> _ResidualStrings: + return ( + _fraction_string(residuals[0]), + _fraction_string(residuals[1]), + _fraction_string(residuals[2]), + ) + + +def _terminating_fraction_decimal(value: Fraction) -> Decimal: + denominator = value.denominator + twos = 0 + fives = 0 + while denominator % 2 == 0: + denominator //= 2 + twos += 1 + while denominator % 5 == 0: + denominator //= 5 + fives += 1 + if denominator != 1: + raise ValueError("recurrence parameter context is not a finite decimal") + places = max(twos, fives) + scaled = value.numerator + scaled *= 2 ** (places - twos) + scaled *= 5 ** (places - fives) + sign = "-" if scaled < 0 else "" + digits = str(abs(scaled)) + if places == 0: + return Decimal(f"{sign}{digits}") + digits = digits.rjust(places + 1, "0") + return Decimal(f"{sign}{digits[:-places]}.{digits[-places:]}") + + +def _canonical_sha256(value: object) -> str: + digest = hashlib.sha256() + for chunk in _iter_canonical_json_chunks(value): + digest.update(chunk) + return digest.hexdigest() + + +def _canonical_json_bytes(value: object) -> bytes: + if isinstance(value, _CanonicalJsonStreamValue): + return b"".join(_iter_canonical_json_chunks(value)) + return json.dumps( + value, + allow_nan=False, + ensure_ascii=True, + separators=(",", ":"), + sort_keys=True, + ).encode("ascii") + + +def _iter_canonical_json_chunks(value: object) -> Iterator[bytes]: + """Yield the unique ASCII encoding accepted at the native boundary.""" + + if isinstance(value, _CanonicalJsonStreamValue): + yield from value.iter_canonical_json_chunks() + return + if value is None: + yield b"null" + return + if value is True: + yield b"true" + return + if value is False: + yield b"false" + return + if type(value) is int: + yield str(value).encode("ascii") + return + if isinstance(value, str): + yield json.dumps(value, ensure_ascii=True).encode("ascii") + return + if isinstance(value, Mapping): + if not all(isinstance(key, str) for key in value): + raise TypeError("canonical JSON mappings require string keys") + yield b"{" + for index, key in enumerate(sorted(value)): + if index: + yield b"," + yield json.dumps(key, ensure_ascii=True).encode("ascii") + yield b":" + yield from _iter_canonical_json_chunks(value[key]) + yield b"}" + return + if isinstance(value, Sequence) and not isinstance( + value, + str | bytes | bytearray, + ): + yield b"[" + for index, item in enumerate(value): + if index: + yield b"," + yield from _iter_canonical_json_chunks(item) + yield b"]" + return + if isinstance(value, float): + raise TypeError( + "canonical recurrence evidence uses binary64 hex strings, not " + "JSON float spellings" + ) + raise TypeError(f"unsupported canonical JSON value {type(value).__name__}") + + +def _bounded_canonical_json_bytes( + value: object, + *, + byte_limit: int | None, + label: str, +) -> bytes: + """Encode canonically, honoring an explicitly requested byte limit.""" + + if byte_limit is not None and byte_limit <= 0: + raise ValueError(f"{label} byte limit is invalid") + encoded = bytearray() + for chunk in _iter_canonical_json_chunks(value): + next_size = len(encoded) + len(chunk) + if byte_limit is not None and next_size > byte_limit: + raise _CanonicalEvidenceCapacityExceeded( + f"{label} exceeds the explicit {byte_limit}-byte canonical boundary" + ) + encoded.extend(chunk) + return bytes(encoded) + + +def _write_compressed_canonical_evidence( + value: object, + stream: BinaryIO, + *, + canonical_byte_limit: int | None, +) -> int: + """Stream canonical JSON into one zlib transport envelope.""" + + if canonical_byte_limit is not None and canonical_byte_limit <= 0: + raise ValueError("recurrence compressed evidence canonical boundary is invalid") + stream.seek(0) + stream.truncate(0) + stream.write(b"\0" * _COMPRESSED_EVIDENCE_HEADER.size) + compressor = zlib.compressobj(level=1, wbits=zlib.MAX_WBITS) + digest = hashlib.sha256() + canonical_byte_count = 0 + transport_byte_count = _COMPRESSED_EVIDENCE_HEADER.size + + def write_compressed(chunk: bytes) -> None: + nonlocal transport_byte_count + if not chunk: + return + transport_byte_count += len(chunk) + stream.write(chunk) + + for chunk in _iter_canonical_json_chunks(value): + canonical_byte_count += len(chunk) + if ( + canonical_byte_limit is not None + and canonical_byte_count > canonical_byte_limit + ): + raise _CanonicalEvidenceCapacityExceeded( + "recurrence numerical raw evidence exceeds the explicit " + f"{canonical_byte_limit}-byte decompression boundary" + ) + digest.update(chunk) + write_compressed(compressor.compress(chunk)) + write_compressed(compressor.flush()) + stream.seek(0) + stream.write( + _COMPRESSED_EVIDENCE_HEADER.pack( + _COMPRESSED_EVIDENCE_MAGIC, + canonical_byte_count, + digest.digest(), + ) + ) + stream.flush() + stream.seek(0, 2) + if stream.tell() != transport_byte_count: + raise ValueError("recurrence compressed evidence spool length drifted") + return canonical_byte_count + + +def _read_compressed_evidence_spool(stream: BinaryIO) -> bytes: + stream.seek(0) + encoded = stream.read() + if len(encoded) < _COMPRESSED_EVIDENCE_HEADER.size: + raise ValueError("recurrence compressed evidence spool is truncated") + magic, _canonical_byte_count, _digest = _COMPRESSED_EVIDENCE_HEADER.unpack_from( + encoded + ) + if magic != _COMPRESSED_EVIDENCE_MAGIC: + raise ValueError("recurrence compressed evidence spool has invalid magic") + return encoded + + +def _validate_raw_evidence_canonical_size( + size: int, + *, + byte_limit: int | None = None, +) -> None: + if size < 0 or (byte_limit is not None and (byte_limit <= 0 or size > byte_limit)): + raise ValueError( + "recurrence numerical raw evidence exceeds its explicit memory envelope " + f"(size={size}, byte_limit={byte_limit})" + ) + + +def _raw_evidence_wire_byte_limit( + *, + scalar_count: int, + row_count: int, + memory_envelope_bytes: int = _RAW_EVIDENCE_MEMORY_TARGET_BYTES, +) -> int: + """Return the per-shape wire allowance for a supplied batching target.""" + + if ( + scalar_count < 0 + or row_count < 0 + or type(memory_envelope_bytes) is not int + or memory_envelope_bytes <= 0 + ): + raise ValueError("recurrence raw-evidence memory geometry is invalid") + resident_without_wire = _raw_evidence_memory_upper_bound( + 0, + scalar_count=scalar_count, + row_count=row_count, + ) + minimum_resident = resident_without_wire + ( + _MIN_RAW_EVIDENCE_WIRE_BYTES * _RAW_EVIDENCE_WIRE_PEAK_COPIES + ) + if minimum_resident > memory_envelope_bytes: + raise _RawEvidenceWireEnvelopeExceeded( + "recurrence raw-evidence capture geometry leaves less than the " + f"required {_MIN_RAW_EVIDENCE_WIRE_BYTES}-byte canonical wire " + f"reserve inside its {memory_envelope_bytes}-byte memory " + f"envelope (scalars={scalar_count}, rows={row_count}, " + f"resident_without_wire={resident_without_wire})" + ) + return ( + memory_envelope_bytes - resident_without_wire + ) // _RAW_EVIDENCE_WIRE_PEAK_COPIES + + +def _raw_evidence_geometry_counts( + sections: _RecurrenceExactSectionsV1, + *, + candidate_probe_count: int, + verification_probe_count: int, + runtime_parameter_count: int, +) -> tuple[int, int, int, int]: + if ( + candidate_probe_count <= 0 + or verification_probe_count <= 0 + or runtime_parameter_count < 0 + ): + raise ValueError("recurrence raw-evidence probe geometry is invalid") + geometry = RecurrenceNumericalEvidenceGeometry.from_counts( + current_count=len(sections.currents), + component_count=sum(current.component_count for current in sections.currents), + candidate_probe_count=candidate_probe_count, + verification_probe_count=verification_probe_count, + runtime_parameter_count=runtime_parameter_count, + ) + return ( + geometry.current_count, + geometry.component_count, + geometry.scalar_count, + geometry.row_count, + ) + + +def _spooled_capture_memory_upper_bound( + *, + current_count: int, + component_count: int, + maximum_probe_count: int, + runtime_parameter_count: int, + include_candidate_index: bool = True, +) -> int: + """Bound one capture graph plus the global index before both are spooled.""" + + values = ( + current_count, + component_count, + maximum_probe_count, + runtime_parameter_count, + ) + if any(value < 0 for value in values) or maximum_probe_count == 0: + raise ValueError("recurrence spooled capture geometry is invalid") + scalar_count = ( + 2 * component_count * maximum_probe_count + + runtime_parameter_count * maximum_probe_count + ) + row_count = current_count + runtime_parameter_count + return ( + _RAW_EVIDENCE_FIXED_RESERVE_BYTES + + scalar_count * _RAW_EVIDENCE_SCALAR_RESIDENT_BYTES + + row_count * _RAW_EVIDENCE_ROW_RESIDENT_BYTES + + _SPOOLED_CAPTURE_COMPRESSION_RESERVE_BYTES + + ( + current_count * _SPOOLED_CANDIDATE_INDEX_BYTES_PER_CURRENT + if include_candidate_index + else 0 + ) + ) + + +def _select_raw_evidence_storage_geometry( + sections: _RecurrenceExactSectionsV1, + *, + candidate_probe_count: int, + verification_probe_count: int, + runtime_parameter_count: int, +) -> _RawEvidenceStorageGeometry: + geometry = RecurrenceNumericalEvidenceGeometry.from_counts( + current_count=len(sections.currents), + component_count=sum(current.component_count for current in sections.currents), + candidate_probe_count=candidate_probe_count, + verification_probe_count=verification_probe_count, + runtime_parameter_count=runtime_parameter_count, + ) + return _select_raw_evidence_storage_geometry_for_counts(geometry) + + +def _select_raw_evidence_storage_geometry_for_counts( + geometry: RecurrenceNumericalEvidenceGeometry, + *, + memory_envelope_bytes: int | None = None, +) -> _RawEvidenceStorageGeometry: + """Select storage from scalar geometry before allocating observations.""" + + if memory_envelope_bytes is not None and ( + type(memory_envelope_bytes) is not int or memory_envelope_bytes <= 0 + ): + raise ValueError("recurrence raw-evidence memory envelope is invalid") + try: + byte_limit = _raw_evidence_wire_byte_limit( + scalar_count=geometry.scalar_count, + row_count=geometry.row_count, + memory_envelope_bytes=( + memory_envelope_bytes or _RAW_EVIDENCE_MEMORY_TARGET_BYTES + ), + ) + except _RawEvidenceWireEnvelopeExceeded as raw_error: + return _compressed_raw_evidence_storage_geometry_for_counts( + geometry, + memory_envelope_bytes=memory_envelope_bytes, + raw_reason=str(raw_error), + ) + return _RawEvidenceStorageGeometry( + scalar_count=geometry.scalar_count, + row_count=geometry.row_count, + canonical_byte_limit=byte_limit if memory_envelope_bytes is not None else None, + encoding=_RAW_EVIDENCE_ENCODING, + producer_resident_upper_bound=_raw_evidence_memory_upper_bound( + _MIN_RAW_EVIDENCE_WIRE_BYTES, + scalar_count=geometry.scalar_count, + row_count=geometry.row_count, + ), + ) + + +def _compressed_raw_evidence_storage_geometry_for_counts( + geometry: RecurrenceNumericalEvidenceGeometry, + *, + memory_envelope_bytes: int | None = None, + raw_reason: str, +) -> _RawEvidenceStorageGeometry: + """Select the sequential compressed spool or return the typed fallback.""" + + producer_bound = geometry.spooled_producer_resident_upper_bound() + if memory_envelope_bytes is not None and producer_bound > memory_envelope_bytes: + raise RecurrenceNumericalEvidenceEnvelopeExceeded( + geometry, + memory_envelope_bytes=memory_envelope_bytes, + spooled_producer_resident_bytes=producer_bound, + raw_reason=raw_reason, + ) + return _RawEvidenceStorageGeometry( + scalar_count=geometry.scalar_count, + row_count=geometry.row_count, + canonical_byte_limit=None, + encoding=_COMPRESSED_EVIDENCE_ENCODING, + producer_resident_upper_bound=producer_bound, + ) + + +def recurrence_numerical_evidence_capacity_outcome( + sections: _RecurrenceExactSectionsV1, + *, + candidate_probe_count: int, + verification_probe_count: int, + runtime_parameter_count: int, + error: BaseException, +) -> RecurrenceNumericalEvidenceEnvelopeExceeded | None: + """Map only recognized native optional-evidence capacity failures. + + Integrity, lexical, structural, and relation-correctness failures remain + fail-closed. The native messages listed here are the bounded-memory exits + emitted after (or while establishing) authenticated evidence geometry. + """ + + reason = str(error) + if not any( + marker in reason for marker in _NATIVE_OPTIONAL_EVIDENCE_CAPACITY_MARKERS + ): + return None + geometry = RecurrenceNumericalEvidenceGeometry.from_counts( + current_count=len(sections.currents), + component_count=sum(current.component_count for current in sections.currents), + candidate_probe_count=candidate_probe_count, + verification_probe_count=verification_probe_count, + runtime_parameter_count=runtime_parameter_count, + ) + return RecurrenceNumericalEvidenceEnvelopeExceeded( + geometry, + memory_envelope_bytes=_RAW_EVIDENCE_MEMORY_TARGET_BYTES, + spooled_producer_resident_bytes=( + geometry.spooled_producer_resident_upper_bound() + ), + raw_reason=reason, + ) + + +def _validate_raw_evidence_geometry( + sections: _RecurrenceExactSectionsV1, + *, + candidate_probe_count: int, + verification_probe_count: int, + runtime_parameter_count: int, +) -> tuple[int, int, None]: + """Validate capture dimensions without imposing a memory ceiling.""" + + _current_count, _component_count, scalar_count, row_count = ( + _raw_evidence_geometry_counts( + sections, + candidate_probe_count=candidate_probe_count, + verification_probe_count=verification_probe_count, + runtime_parameter_count=runtime_parameter_count, + ) + ) + return scalar_count, row_count, None + + +def _raw_evidence_memory_upper_bound( + size: int, + *, + scalar_count: int, + row_count: int, +) -> int: + if size < 0 or scalar_count < 0 or row_count < 0: + raise ValueError("recurrence raw-evidence memory geometry is invalid") + return ( + _RAW_EVIDENCE_FIXED_RESERVE_BYTES + + scalar_count * _RAW_EVIDENCE_SCALAR_RESIDENT_BYTES + + row_count * _RAW_EVIDENCE_ROW_RESIDENT_BYTES + + size * _RAW_EVIDENCE_WIRE_PEAK_COPIES + ) + + +def _synthetic_raw_evidence_bytes( + current_count: int, + *, + component_count: int, + candidate_probe_count: int, + verification_probe_count: int, + decimal_characters: int, +) -> int: + """Measure the canonical raw-observation footprint without allocating it.""" + + if ( + current_count < 0 + or component_count <= 0 + or candidate_probe_count <= 0 + or verification_probe_count <= 0 + or decimal_characters <= 0 + ): + raise ValueError("synthetic raw-evidence geometry is invalid") + return _synthetic_raw_evidence_bytes_by_dimension_counts( + {component_count: current_count}, + candidate_probe_count=candidate_probe_count, + verification_probe_count=verification_probe_count, + decimal_characters=decimal_characters, + ) + + +def _synthetic_raw_evidence_bytes_by_dimension_counts( + dimension_counts: Mapping[int, int], + *, + candidate_probe_count: int, + verification_probe_count: int, + decimal_characters: int, +) -> int: + """Measure a mixed-dimension canonical raw-observation footprint.""" + + if ( + not dimension_counts + or candidate_probe_count <= 0 + or verification_probe_count <= 0 + or decimal_characters <= 0 + or any( + dimension <= 0 or count < 0 for dimension, count in dimension_counts.items() + ) + ): + raise ValueError("synthetic raw-evidence geometry is invalid") + dimensions = tuple( + dimension + for dimension, count in sorted(dimension_counts.items()) + for _ in range(count) + ) + current_count = len(dimensions) + scalar = "1" + "2" * (decimal_characters - 1) + + def array_size(point_count: int) -> int: + total = 2 + max(0, current_count - 1) + for current_id, dimension in enumerate(dimensions): + values = [[scalar, f"-{scalar}"]] * (dimension * point_count) + total += len( + _canonical_json_bytes( + { + "current_id": current_id, + "dimension": dimension, + "values": values, + } + ) + ) + return total + + skeleton_size = len( + _canonical_json_bytes( + { + "candidate_capture": {"observations": []}, + "verification_capture": {"observations": []}, + } + ) + ) + return ( + skeleton_size + + array_size(candidate_probe_count) + + array_size(verification_probe_count) + - 4 + ) + + +__all__ = [ + "EVIDENCE_ENVELOPE_FALLBACK_REASON", + "EVIDENCE_ENVELOPE_FALLBACK_WARNING", + "NUMERICAL_RELATION_CORRECTNESS_ABI", + "NUMERICAL_RELATION_FALLBACK_ABI", + "RecurrenceCurrentObservationCapture", + "RecurrenceNumericalCurrentCertificate", + "RecurrenceNumericalCurrentWarmupResult", + "RecurrenceNumericalEvidenceEnvelopeExceeded", + "RecurrenceNumericalEvidenceGeometry", + "build_recurrence_numerical_current_probe_points", + "capture_recurrence_current_observations", + "numerical_relation_correctness_payload", + "recurrence_numerical_current_envelope_fallback_report", + "recurrence_numerical_current_opt_out_report", + "recurrence_numerical_evidence_capacity_outcome", + "recurrence_numerical_relation_correctness_summary", + "recurrence_numerical_relation_fallback_summary", + "recurrence_numerical_source_semantics_sha256", + "run_recurrence_numerical_current_warmup", + "validate_recurrence_numerical_current_application", + "validate_recurrence_numerical_evidence_fallback", +] diff --git a/pyamplicol/src/pyamplicol/generation/recurrence_pairing_columnar.py b/pyamplicol/src/pyamplicol/generation/recurrence_pairing_columnar.py new file mode 100644 index 0000000000..da2d36e7ae --- /dev/null +++ b/pyamplicol/src/pyamplicol/generation/recurrence_pairing_columnar.py @@ -0,0 +1,1200 @@ +# SPDX-License-Identifier: 0BSD +"""Private fixed-width encoder for external-fermion pairing catalogs. + +This module is an intentionally unintegrated staging contract for a future +Rust recurrence-builder ABI. It turns :class:`FermionPairingCatalogV1` into +owned, immutable NumPy structure-of-arrays tables without object dtypes. +Variable-length records use contiguous ``u64`` ranges, exact integers use +signed-magnitude little-endian ``u64`` limbs, and textual model contracts use +a deterministic UTF-8 catalog. + +The leading underscore on the encoder and payload is deliberate. Consumers +must not depend on this ABI until a matching Rust decoder is frozen. +""" + +from __future__ import annotations + +import hashlib +import itertools +import json +import math +from collections.abc import Iterable, Mapping, Sequence +from dataclasses import dataclass +from typing import Any, Final + +import numpy as np + +from pyamplicol.generation.recurrence_columnar import ( + RecurrenceColumn, + RecurrenceColumnarInputError, + RecurrenceColumnarTable, +) +from pyamplicol.generation.recurrence_fermion_pairing import ( + NO_FERMION_LINE, + PAIRING_PROOF_ALGORITHM, + ExternalFermionEndpointRowV1, + FermionPairingCatalogV1, + FermionPairingClassRowV1, + FermionPairingRuleRowV1, +) +from pyamplicol.models.recurrence_template import ExactComplexRationalV1 + +_PAIRING_COLUMNAR_ABI: Final = "pyamplicol-recurrence-fermion-pairing-columnar-v1" +_PAIRING_COLUMNAR_SCHEMA_VERSION: Final = 1 +_U8 = np.dtype("u1") +_U32 = np.dtype(" None: + if self.abi != _PAIRING_COLUMNAR_ABI: + raise RecurrenceColumnarInputError( + f"unsupported fermion-pairing columnar ABI {self.abi!r}" + ) + _validate_encoded_tables(self.tables) + + def table(self, name: str) -> RecurrenceColumnarTable: + """Return one immutable table by its private ABI name.""" + + for table in self.tables: + if table.name == name: + return table + raise KeyError(f"fermion-pairing payload has no table {name!r}") + + +class _StringCatalog: + def __init__(self, values: Iterable[str]) -> None: + canonical = {_nonempty_text(value, "pairing string") for value in values} + self.values = tuple(sorted(canonical)) + _checked_u32(len(self.values), "pairing string count") + self._ids = {value: index for index, value in enumerate(self.values)} + + def id(self, value: str) -> int: + try: + return self._ids[value] + except KeyError as error: + raise RecurrenceColumnarInputError( + f"pairing string {value!r} was not collected" + ) from error + + +class _ExactIntegerCatalog: + def __init__(self, values: Iterable[int]) -> None: + canonical = {_checked_int(value, "exact factor integer") for value in values} + self.values = tuple(sorted(canonical)) + _checked_u32(len(self.values), "exact integer count") + self._ids = {value: index for index, value in enumerate(self.values)} + + def id(self, value: int) -> int: + try: + return self._ids[value] + except KeyError as error: + raise RecurrenceColumnarInputError( + f"exact integer {value} was not collected" + ) from error + + +def _encode_fermion_pairing_catalog_v1( + catalog: FermionPairingCatalogV1, +) -> _FermionPairingColumnarV1: + """Validate and encode a canonical pairing catalog. + + The function is private because no Rust consumer is frozen yet. It is + deterministic for semantically identical input and fails before allocation + of large flat arrays when IDs, ranges, proofs, or exact factors are stale. + """ + + _validate_logical_catalog(catalog) + strings = _StringCatalog(_catalog_strings(catalog)) + exact_integers = _ExactIntegerCatalog(_catalog_exact_integers(catalog)) + + endpoint_state_ids = [ + strings.id(value) + for endpoint in catalog.endpoints + for value in endpoint.state_template_ids + ] + endpoint_anti_state_ids = [ + strings.id(value) + for endpoint in catalog.endpoints + for value in endpoint.anti_state_template_ids + ] + endpoint_basis_ids = [ + strings.id(value) + for endpoint in catalog.endpoints + for value in endpoint.basis_ids + ] + endpoint_color_representations = [ + value + for endpoint in catalog.endpoints + for value in endpoint.color_representations + ] + class_fundamental_slots = [ + value + for pairing_class in catalog.pairing_classes + for value in pairing_class.fundamental_source_slots + ] + class_antifundamental_slots = [ + value + for pairing_class in catalog.pairing_classes + for value in pairing_class.antifundamental_source_slots + ] + class_reference_pairings = [ + value + for pairing_class in catalog.pairing_classes + for value in pairing_class.reference_pairings + ] + rule_class_indices = [ + value for rule in catalog.rules for value in rule.class_pairing_indices + ] + rule_endpoint_pairings = [ + value for rule in catalog.rules for value in rule.endpoint_pairings + ] + rule_source_permutations = [ + value for rule in catalog.rules for value in rule.source_slot_permutation + ] + rule_lineages = [ + value for rule in catalog.rules for value in rule.lineage_by_source_slot + ] + + integer_limbs = [ + limb for value in exact_integers.values for limb in _unsigned_limbs(value) + ] + string_payloads = [value.encode("utf-8") for value in strings.values] + string_bytes = b"".join(string_payloads) + + tables: list[RecurrenceColumnarTable] = [] + tables.append( + _build_header_table( + catalog, + strings, + exact_integers, + endpoint_state_ids=endpoint_state_ids, + endpoint_anti_state_ids=endpoint_anti_state_ids, + endpoint_basis_ids=endpoint_basis_ids, + endpoint_color_representations=endpoint_color_representations, + class_fundamental_slots=class_fundamental_slots, + class_antifundamental_slots=class_antifundamental_slots, + class_reference_pairings=class_reference_pairings, + rule_class_indices=rule_class_indices, + rule_endpoint_pairings=rule_endpoint_pairings, + rule_source_permutations=rule_source_permutations, + rule_lineages=rule_lineages, + integer_limbs=integer_limbs, + string_bytes=string_bytes, + ) + ) + tables.append(_build_endpoints_table(catalog.endpoints, strings)) + tables.append(_one_column_table("endpoint_state_template_ids", endpoint_state_ids)) + tables.append( + _one_column_table("endpoint_anti_state_template_ids", endpoint_anti_state_ids) + ) + tables.append(_one_column_table("endpoint_basis_ids", endpoint_basis_ids)) + tables.append( + _one_column_table( + "endpoint_color_representations", endpoint_color_representations + ) + ) + tables.append(_build_pairing_classes_table(catalog.pairing_classes, strings)) + tables.append(_one_column_table("class_fundamental_slots", class_fundamental_slots)) + tables.append( + _one_column_table("class_antifundamental_slots", class_antifundamental_slots) + ) + tables.append(_pair_table("class_reference_pairings", class_reference_pairings)) + tables.append(_build_rules_table(catalog.rules, strings, exact_integers)) + tables.append(_class_index_table(rule_class_indices)) + tables.append(_pair_table("rule_endpoint_pairings", rule_endpoint_pairings)) + tables.append( + _one_column_table("rule_source_slot_permutations", rule_source_permutations) + ) + tables.append(_one_column_table("rule_lineages", rule_lineages)) + tables.extend(_build_exact_integer_tables(exact_integers)) + tables.extend(_build_string_tables(strings, string_payloads)) + return _FermionPairingColumnarV1(_PAIRING_COLUMNAR_ABI, tuple(tables)) + + +def _validate_logical_catalog(catalog: FermionPairingCatalogV1) -> None: + if not isinstance(catalog, FermionPairingCatalogV1): + raise TypeError("pairing columnar encoding requires FermionPairingCatalogV1") + _nonempty_text(catalog.process_key, "pairing process key") + source_count = _checked_u32(catalog.source_count, "pairing source count") + topology_digest = _digest_bytes(catalog.topology_digest, "topology digest") + semantic_digest = _digest_bytes(catalog.semantic_digest, "semantic digest") + del topology_digest, semantic_digest + + endpoints = tuple(catalog.endpoints) + pairing_classes = tuple(catalog.pairing_classes) + rules = tuple(catalog.rules) + _checked_u32(len(endpoints), "pairing endpoint count") + _checked_u32(len(pairing_classes), "pairing class count") + _checked_u32(len(rules), "pairing rule count") + if not rules: + raise RecurrenceColumnarInputError( + "a fermion pairing catalog must contain its trivial or physical rule" + ) + + endpoint_by_slot: dict[int, ExternalFermionEndpointRowV1] = {} + for expected_id, endpoint in enumerate(endpoints): + if not isinstance(endpoint, ExternalFermionEndpointRowV1): + raise TypeError("pairing endpoints must be endpoint row v1 values") + if endpoint.endpoint_id != expected_id: + raise RecurrenceColumnarInputError( + "pairing endpoint IDs must be dense and canonical" + ) + source_slot = _bounded_u32( + endpoint.source_slot, source_count, "pairing endpoint source slot" + ) + if source_slot in endpoint_by_slot: + raise RecurrenceColumnarInputError( + "pairing endpoint source slots must be unique" + ) + endpoint_by_slot[source_slot] = endpoint + _checked_u32(endpoint.public_label, "pairing endpoint public label") + _bounded_u32( + endpoint.species_class_id, + len(pairing_classes), + "endpoint species class", + ) + _nonempty_text(endpoint.species_id, "endpoint species ID") + if endpoint.particle_orientation not in _PARTICLE_ORIENTATION_CODES: + raise RecurrenceColumnarInputError( + "endpoint particle orientation is not canonical" + ) + if endpoint.color_orientation not in _COLOR_ORIENTATION_CODES: + raise RecurrenceColumnarInputError( + "endpoint color orientation is not canonical" + ) + _canonical_strings(endpoint.state_template_ids, "state template IDs") + _canonical_strings( + endpoint.anti_state_template_ids, "antiparticle state template IDs" + ) + _canonical_strings(endpoint.basis_ids, "endpoint basis IDs") + _canonical_i32s( + endpoint.color_representations, "endpoint color representations" + ) + _digest_bytes(endpoint.contract_digest, "endpoint contract digest") + if tuple(endpoint_by_slot) != tuple(sorted(endpoint_by_slot)): + raise RecurrenceColumnarInputError( + "pairing endpoints must be ordered by source slot" + ) + + expected_class_order: list[tuple[int, ...]] = [] + class_permutations: dict[int, tuple[tuple[int, ...], ...]] = {} + for expected_id, pairing_class in enumerate(pairing_classes): + if not isinstance(pairing_class, FermionPairingClassRowV1): + raise TypeError("pairing classes must be pairing class row v1 values") + if ( + pairing_class.class_id != expected_id + or pairing_class.species_class_id != expected_id + ): + raise RecurrenceColumnarInputError( + "pairing class IDs must be dense, aligned, and canonical" + ) + _nonempty_text(pairing_class.species_id, "pairing class species ID") + fundamental = _canonical_u32_slots( + pairing_class.fundamental_source_slots, + source_count, + "fundamental pairing slots", + ) + antifundamental = _canonical_u32_slots( + pairing_class.antifundamental_source_slots, + source_count, + "antifundamental pairing slots", + ) + if len(fundamental) != len(antifundamental): + raise RecurrenceColumnarInputError( + "pairing classes require balanced endpoint orientations" + ) + expected_reference = tuple(zip(fundamental, antifundamental, strict=True)) + if pairing_class.reference_pairings != expected_reference: + raise RecurrenceColumnarInputError( + "pairing class reference pairings are stale" + ) + expected_count = math.factorial(len(fundamental)) + if pairing_class.pairing_count != expected_count: + raise RecurrenceColumnarInputError("pairing class pairing count is stale") + _checked_u64(pairing_class.pairing_count, "pairing class pairing count") + class_endpoints = tuple( + endpoint + for endpoint in endpoints + if endpoint.species_class_id == expected_id + ) + expected_fundamental = tuple( + endpoint.source_slot + for endpoint in class_endpoints + if endpoint.color_orientation == "fundamental" + ) + expected_antifundamental = tuple( + endpoint.source_slot + for endpoint in class_endpoints + if endpoint.color_orientation == "antifundamental" + ) + if ( + fundamental != expected_fundamental + or antifundamental != expected_antifundamental + or any( + endpoint.species_id != pairing_class.species_id + for endpoint in class_endpoints + ) + ): + raise RecurrenceColumnarInputError( + "pairing class endpoint membership is inconsistent" + ) + expected_class_order.append(tuple(sorted((*fundamental, *antifundamental)))) + class_payload = { + "algorithm": PAIRING_PROOF_ALGORITHM, + "antifundamental_source_slots": antifundamental, + "fundamental_source_slots": fundamental, + "reference_pairings": expected_reference, + "species_contract_digests": tuple( + sorted(endpoint.contract_digest for endpoint in class_endpoints) + ), + } + if pairing_class.proof_digest != _digest(class_payload): + raise RecurrenceColumnarInputError("pairing class proof digest is stale") + class_permutations[expected_id] = tuple(itertools.permutations(antifundamental)) + if expected_class_order != sorted(expected_class_order): + raise RecurrenceColumnarInputError( + "pairing classes are not in canonical source-slot order" + ) + if len({row.species_id for row in pairing_classes}) != len(pairing_classes): + raise RecurrenceColumnarInputError("pairing class species IDs must be unique") + + expected_rule_count = math.prod( + pairing_class.pairing_count for pairing_class in pairing_classes + ) + if len(rules) != expected_rule_count: + raise RecurrenceColumnarInputError("pairing rule product is incomplete") + for expected_id, rule in enumerate(rules): + _validate_rule( + rule, + expected_id=expected_id, + source_count=source_count, + pairing_classes=pairing_classes, + class_permutations=class_permutations, + ) + + topology_payload = _topology_payload( + source_count, endpoints, pairing_classes, rules + ) + if catalog.topology_digest != _digest(topology_payload): + raise RecurrenceColumnarInputError("pairing topology digest is stale") + semantic_payload = { + "process_key": catalog.process_key, + "topology": topology_payload, + "endpoint_contracts": tuple( + { + "anti_state_template_ids": endpoint.anti_state_template_ids, + "basis_ids": endpoint.basis_ids, + "color_representations": endpoint.color_representations, + "contract_digest": endpoint.contract_digest, + "particle_orientation": endpoint.particle_orientation, + "species_id": endpoint.species_id, + "state_template_ids": endpoint.state_template_ids, + } + for endpoint in endpoints + ), + } + if catalog.semantic_digest != _digest(semantic_payload): + raise RecurrenceColumnarInputError("pairing semantic digest is stale") + + +def _validate_rule( + rule: FermionPairingRuleRowV1, + *, + expected_id: int, + source_count: int, + pairing_classes: tuple[FermionPairingClassRowV1, ...], + class_permutations: Mapping[int, tuple[tuple[int, ...], ...]], +) -> None: + if not isinstance(rule, FermionPairingRuleRowV1): + raise TypeError("pairing rules must be pairing rule row v1 values") + if rule.rule_id != expected_id: + raise RecurrenceColumnarInputError( + "pairing rule IDs must be dense and canonical" + ) + expected_class_ids = tuple(range(len(pairing_classes))) + if tuple(item[0] for item in rule.class_pairing_indices) != expected_class_ids: + raise RecurrenceColumnarInputError( + "pairing rule class indices must cover every class in order" + ) + + endpoint_pairings: list[tuple[int, int]] = [] + source_permutation = list(range(source_count)) + parity = 1 + for class_id, pairing_index in rule.class_pairing_indices: + _checked_u32(class_id, "rule pairing class ID") + _checked_u64(pairing_index, "rule class-local pairing index") + permutations = class_permutations[class_id] + if pairing_index >= len(permutations): + raise RecurrenceColumnarInputError( + "rule class-local pairing index is out of bounds" + ) + pairing_class = pairing_classes[class_id] + selected = permutations[pairing_index] + parity *= _permutation_parity( + pairing_class.antifundamental_source_slots, selected + ) + endpoint_pairings.extend( + zip(pairing_class.fundamental_source_slots, selected, strict=True) + ) + for reference_slot, selected_slot in zip( + pairing_class.antifundamental_source_slots, selected, strict=True + ): + source_permutation[reference_slot] = selected_slot + expected_pairings = tuple(sorted(endpoint_pairings)) + if rule.endpoint_pairings != expected_pairings: + raise RecurrenceColumnarInputError("rule endpoint pairings are stale") + if rule.source_slot_permutation != tuple(source_permutation): + raise RecurrenceColumnarInputError("rule source-slot permutation is stale") + + lineage = [NO_FERMION_LINE] * source_count + for line_id, (fundamental_slot, antifundamental_slot) in enumerate( + expected_pairings + ): + lineage[fundamental_slot] = line_id + lineage[antifundamental_slot] = line_id + if rule.lineage_by_source_slot != tuple(lineage): + raise RecurrenceColumnarInputError("rule fermion lineage is stale") + if rule.fermion_parity != parity or rule.fermion_parity not in {-1, 1}: + raise RecurrenceColumnarInputError("rule fermion parity is stale") + expected_factor = ExactComplexRationalV1(parity, 1, 0, 1) + if rule.exact_factor != expected_factor: + raise RecurrenceColumnarInputError( + "rule exact factor does not reproduce its fermion parity" + ) + if rule.multiplicity != 1: + raise RecurrenceColumnarInputError("pairing rule multiplicity is not canonical") + _checked_u64(rule.multiplicity, "pairing rule multiplicity") + if rule.proof_algorithm != PAIRING_PROOF_ALGORITHM: + raise RecurrenceColumnarInputError("pairing rule proof algorithm is stale") + rule_payload = { + "algorithm": PAIRING_PROOF_ALGORITHM, + "class_pairing_indices": rule.class_pairing_indices, + "endpoint_pairings": rule.endpoint_pairings, + "fermion_parity": rule.fermion_parity, + "lineage_by_source_slot": rule.lineage_by_source_slot, + "source_slot_permutation": rule.source_slot_permutation, + } + if rule.proof_digest != _digest(rule_payload): + raise RecurrenceColumnarInputError("pairing rule proof digest is stale") + + +def _build_header_table( + catalog: FermionPairingCatalogV1, + strings: _StringCatalog, + exact_integers: _ExactIntegerCatalog, + **flat_values: Sequence[object] | bytes, +) -> RecurrenceColumnarTable: + columns = _allocate("header", 1) + columns["schema_version"][0] = _PAIRING_COLUMNAR_SCHEMA_VERSION + columns["abi_string_id"][0] = strings.id(_PAIRING_COLUMNAR_ABI) + columns["process_key_string_id"][0] = strings.id(catalog.process_key) + columns["proof_algorithm_string_id"][0] = strings.id(PAIRING_PROOF_ALGORITHM) + columns["source_count"][0] = catalog.source_count + columns["endpoint_count"][0] = len(catalog.endpoints) + columns["pairing_class_count"][0] = len(catalog.pairing_classes) + columns["rule_count"][0] = len(catalog.rules) + count_columns = { + "endpoint_state_template_count": "endpoint_state_ids", + "endpoint_anti_state_template_count": "endpoint_anti_state_ids", + "endpoint_basis_count": "endpoint_basis_ids", + "endpoint_color_representation_count": "endpoint_color_representations", + "class_fundamental_slot_count": "class_fundamental_slots", + "class_antifundamental_slot_count": "class_antifundamental_slots", + "class_reference_pairing_count": "class_reference_pairings", + "rule_class_pairing_index_count": "rule_class_indices", + "rule_endpoint_pairing_count": "rule_endpoint_pairings", + "rule_source_permutation_count": "rule_source_permutations", + "rule_lineage_count": "rule_lineages", + "exact_integer_limb_count": "integer_limbs", + "string_byte_count": "string_bytes", + } + for column_name, value_name in count_columns.items(): + columns[column_name][0] = _checked_u64( + len(flat_values[value_name]), column_name + ) + columns["exact_integer_count"][0] = len(exact_integers.values) + columns["string_count"][0] = len(strings.values) + columns["no_fermion_line"][0] = NO_FERMION_LINE + columns["topology_digest"][0] = np.frombuffer( + _digest_bytes(catalog.topology_digest, "topology digest"), dtype=_U8 + ) + columns["semantic_digest"][0] = np.frombuffer( + _digest_bytes(catalog.semantic_digest, "semantic digest"), dtype=_U8 + ) + return _freeze_table("header", columns) + + +def _build_endpoints_table( + endpoints: Sequence[ExternalFermionEndpointRowV1], + strings: _StringCatalog, +) -> RecurrenceColumnarTable: + columns = _allocate("endpoints", len(endpoints)) + state_cursor = anti_cursor = basis_cursor = color_cursor = 0 + for row, endpoint in enumerate(endpoints): + columns["endpoint_id"][row] = endpoint.endpoint_id + columns["source_slot"][row] = endpoint.source_slot + columns["public_label"][row] = endpoint.public_label + columns["species_class_id"][row] = endpoint.species_class_id + columns["species_string_id"][row] = strings.id(endpoint.species_id) + columns["particle_orientation"][row] = _PARTICLE_ORIENTATION_CODES[ + endpoint.particle_orientation + ] + columns["color_orientation"][row] = _COLOR_ORIENTATION_CODES[ + endpoint.color_orientation + ] + for prefix, cursor, values in ( + ("state_template", state_cursor, endpoint.state_template_ids), + ("anti_state_template", anti_cursor, endpoint.anti_state_template_ids), + ("basis", basis_cursor, endpoint.basis_ids), + ( + "color_representation", + color_cursor, + endpoint.color_representations, + ), + ): + columns[f"{prefix}_start"][row] = cursor + columns[f"{prefix}_count"][row] = len(values) + state_cursor += len(endpoint.state_template_ids) + anti_cursor += len(endpoint.anti_state_template_ids) + basis_cursor += len(endpoint.basis_ids) + color_cursor += len(endpoint.color_representations) + columns["contract_digest"][row] = np.frombuffer( + _digest_bytes(endpoint.contract_digest, "endpoint contract digest"), + dtype=_U8, + ) + return _freeze_table("endpoints", columns) + + +def _build_pairing_classes_table( + pairing_classes: Sequence[FermionPairingClassRowV1], + strings: _StringCatalog, +) -> RecurrenceColumnarTable: + columns = _allocate("pairing_classes", len(pairing_classes)) + fundamental_cursor = antifundamental_cursor = reference_cursor = 0 + for row, pairing_class in enumerate(pairing_classes): + columns["class_id"][row] = pairing_class.class_id + columns["species_class_id"][row] = pairing_class.species_class_id + columns["species_string_id"][row] = strings.id(pairing_class.species_id) + columns["fundamental_slot_start"][row] = fundamental_cursor + columns["fundamental_slot_count"][row] = len( + pairing_class.fundamental_source_slots + ) + columns["antifundamental_slot_start"][row] = antifundamental_cursor + columns["antifundamental_slot_count"][row] = len( + pairing_class.antifundamental_source_slots + ) + columns["reference_pairing_start"][row] = reference_cursor + columns["reference_pairing_count"][row] = len(pairing_class.reference_pairings) + columns["pairing_count"][row] = pairing_class.pairing_count + columns["proof_digest"][row] = np.frombuffer( + _digest_bytes(pairing_class.proof_digest, "pairing class proof digest"), + dtype=_U8, + ) + fundamental_cursor += len(pairing_class.fundamental_source_slots) + antifundamental_cursor += len(pairing_class.antifundamental_source_slots) + reference_cursor += len(pairing_class.reference_pairings) + return _freeze_table("pairing_classes", columns) + + +def _build_rules_table( + rules: Sequence[FermionPairingRuleRowV1], + strings: _StringCatalog, + exact_integers: _ExactIntegerCatalog, +) -> RecurrenceColumnarTable: + columns = _allocate("rules", len(rules)) + class_cursor = pair_cursor = permutation_cursor = lineage_cursor = 0 + for row, rule in enumerate(rules): + columns["rule_id"][row] = rule.rule_id + columns["class_pairing_index_start"][row] = class_cursor + columns["class_pairing_index_count"][row] = len(rule.class_pairing_indices) + columns["endpoint_pairing_start"][row] = pair_cursor + columns["endpoint_pairing_count"][row] = len(rule.endpoint_pairings) + columns["source_permutation_start"][row] = permutation_cursor + columns["source_permutation_count"][row] = len(rule.source_slot_permutation) + columns["lineage_start"][row] = lineage_cursor + columns["lineage_count"][row] = len(rule.lineage_by_source_slot) + columns["fermion_parity"][row] = rule.fermion_parity + factor = rule.exact_factor + columns["real_numerator_integer_id"][row] = exact_integers.id( + factor.real_numerator + ) + columns["real_denominator_integer_id"][row] = exact_integers.id( + factor.real_denominator + ) + columns["imag_numerator_integer_id"][row] = exact_integers.id( + factor.imag_numerator + ) + columns["imag_denominator_integer_id"][row] = exact_integers.id( + factor.imag_denominator + ) + columns["multiplicity"][row] = rule.multiplicity + columns["proof_algorithm_string_id"][row] = strings.id(rule.proof_algorithm) + columns["proof_digest"][row] = np.frombuffer( + _digest_bytes(rule.proof_digest, "pairing rule proof digest"), dtype=_U8 + ) + class_cursor += len(rule.class_pairing_indices) + pair_cursor += len(rule.endpoint_pairings) + permutation_cursor += len(rule.source_slot_permutation) + lineage_cursor += len(rule.lineage_by_source_slot) + return _freeze_table("rules", columns) + + +def _build_exact_integer_tables( + catalog: _ExactIntegerCatalog, +) -> tuple[RecurrenceColumnarTable, RecurrenceColumnarTable]: + columns = _allocate("exact_integers", len(catalog.values)) + flat_limbs: list[int] = [] + for row, value in enumerate(catalog.values): + limbs = _unsigned_limbs(value) + columns["integer_id"][row] = row + columns["sign"][row] = -1 if value < 0 else (1 if value > 0 else 0) + columns["limb_start"][row] = len(flat_limbs) + columns["limb_count"][row] = len(limbs) + flat_limbs.extend(limbs) + return ( + _freeze_table("exact_integers", columns), + _one_column_table("exact_integer_limbs", flat_limbs), + ) + + +def _build_string_tables( + strings: _StringCatalog, + payloads: Sequence[bytes], +) -> tuple[RecurrenceColumnarTable, RecurrenceColumnarTable]: + ranges = _allocate("string_ranges", len(strings.values)) + cursor = 0 + for row, payload in enumerate(payloads): + ranges["string_id"][row] = row + ranges["start"][row] = cursor + ranges["count"][row] = len(payload) + cursor += len(payload) + values = _allocate("string_bytes", cursor) + if cursor: + values["value"][:] = np.frombuffer(b"".join(payloads), dtype=_U8) + return ( + _freeze_table("string_ranges", ranges), + _freeze_table("string_bytes", values), + ) + + +def _one_column_table(name: str, values: Sequence[int]) -> RecurrenceColumnarTable: + columns = _allocate(name, len(values)) + column_name = _TABLE_SCHEMAS[name][0][0] + if values: + columns[column_name][:] = values + return _freeze_table(name, columns) + + +def _pair_table( + name: str, values: Sequence[tuple[int, int]] +) -> RecurrenceColumnarTable: + columns = _allocate(name, len(values)) + left_name, right_name = (item[0] for item in _TABLE_SCHEMAS[name]) + for row, (left, right) in enumerate(values): + columns[left_name][row] = left + columns[right_name][row] = right + return _freeze_table(name, columns) + + +def _class_index_table( + values: Sequence[tuple[int, int]], +) -> RecurrenceColumnarTable: + columns = _allocate("rule_class_pairing_indices", len(values)) + for row, (class_id, pairing_index) in enumerate(values): + columns["class_id"][row] = class_id + columns["pairing_index"][row] = pairing_index + return _freeze_table("rule_class_pairing_indices", columns) + + +def _allocate(name: str, row_count: int) -> dict[str, np.ndarray[Any, Any]]: + _checked_u64(row_count, f"{name} row count") + result: dict[str, np.ndarray[Any, Any]] = {} + for column_name, dtype, tail_shape in _TABLE_SCHEMAS[name]: + result[column_name] = np.empty((row_count, *tail_shape), dtype=dtype, order="C") + return result + + +def _freeze_table( + name: str, columns: Mapping[str, np.ndarray[Any, Any]] +) -> RecurrenceColumnarTable: + expected_names = tuple(item[0] for item in _TABLE_SCHEMAS[name]) + if tuple(columns) != expected_names: + raise RecurrenceColumnarInputError( + f"table {name!r} columns do not match its private ABI schema" + ) + frozen: list[RecurrenceColumn] = [] + for column_name, expected_dtype, expected_tail_shape in _TABLE_SCHEMAS[name]: + values = columns[column_name] + can_adopt = ( + values.flags.owndata + and values.flags.c_contiguous + and values.dtype == expected_dtype + and values.shape[1:] == expected_tail_shape + ) + owned = ( + values + if can_adopt + else np.array(values, dtype=values.dtype, order="C", copy=True) + ) + owned.flags.writeable = False + frozen.append(RecurrenceColumn(column_name, owned)) + row_count = len(frozen[0].values) if frozen else 0 + return RecurrenceColumnarTable(name, row_count, tuple(frozen)) + + +def _validate_encoded_tables(tables: tuple[RecurrenceColumnarTable, ...]) -> None: + if tuple(table.name for table in tables) != _TABLE_ORDER: + raise RecurrenceColumnarInputError( + "fermion-pairing tables do not match the private ABI order" + ) + by_name = {table.name: table for table in tables} + for table in tables: + schema = _TABLE_SCHEMAS[table.name] + if tuple(column.name for column in table.columns) != tuple( + item[0] for item in schema + ): + raise RecurrenceColumnarInputError( + f"table {table.name!r} does not match its column schema" + ) + for column, (_, dtype, tail_shape) in zip(table.columns, schema, strict=True): + if column.values.dtype != dtype or column.values.shape[1:] != tail_shape: + raise RecurrenceColumnarInputError( + f"table {table.name!r} column {column.name!r} has the wrong " + "little-endian fixed-width representation" + ) + header = by_name["header"] + if header.row_count != 1: + raise RecurrenceColumnarInputError("pairing header must contain one row") + if int(header.column("schema_version")[0]) != _PAIRING_COLUMNAR_SCHEMA_VERSION: + raise RecurrenceColumnarInputError("pairing columnar schema version is stale") + if int(header.column("no_fermion_line")[0]) != NO_FERMION_LINE: + raise RecurrenceColumnarInputError("pairing lineage sentinel is stale") + row_counts = { + "endpoint_count": "endpoints", + "pairing_class_count": "pairing_classes", + "rule_count": "rules", + "exact_integer_count": "exact_integers", + "string_count": "string_ranges", + } + for count_column, table_name in row_counts.items(): + if int(header.column(count_column)[0]) != by_name[table_name].row_count: + raise RecurrenceColumnarInputError( + f"header {count_column!r} does not match table {table_name!r}" + ) + flat_counts = { + "endpoint_state_template_count": "endpoint_state_template_ids", + "endpoint_anti_state_template_count": "endpoint_anti_state_template_ids", + "endpoint_basis_count": "endpoint_basis_ids", + "endpoint_color_representation_count": "endpoint_color_representations", + "class_fundamental_slot_count": "class_fundamental_slots", + "class_antifundamental_slot_count": "class_antifundamental_slots", + "class_reference_pairing_count": "class_reference_pairings", + "rule_class_pairing_index_count": "rule_class_pairing_indices", + "rule_endpoint_pairing_count": "rule_endpoint_pairings", + "rule_source_permutation_count": "rule_source_slot_permutations", + "rule_lineage_count": "rule_lineages", + "exact_integer_limb_count": "exact_integer_limbs", + "string_byte_count": "string_bytes", + } + for count_column, table_name in flat_counts.items(): + if int(header.column(count_column)[0]) != by_name[table_name].row_count: + raise RecurrenceColumnarInputError( + f"header {count_column!r} does not match table {table_name!r}" + ) + _validate_dense_ids(by_name["endpoints"], "endpoint_id") + _validate_dense_ids(by_name["pairing_classes"], "class_id") + _validate_dense_ids(by_name["rules"], "rule_id") + _validate_dense_ids(by_name["exact_integers"], "integer_id") + _validate_dense_ids(by_name["string_ranges"], "string_id") + _validate_ranges( + by_name["string_ranges"], + "start", + "count", + by_name["string_bytes"].row_count, + "pairing string catalog", + ) + _validate_ranges( + by_name["exact_integers"], + "limb_start", + "limb_count", + by_name["exact_integer_limbs"].row_count, + "pairing exact-integer catalog", + ) + range_groups = ( + ("endpoints", "state_template", "endpoint_state_template_ids"), + ( + "endpoints", + "anti_state_template", + "endpoint_anti_state_template_ids", + ), + ("endpoints", "basis", "endpoint_basis_ids"), + ( + "endpoints", + "color_representation", + "endpoint_color_representations", + ), + ("pairing_classes", "fundamental_slot", "class_fundamental_slots"), + ( + "pairing_classes", + "antifundamental_slot", + "class_antifundamental_slots", + ), + ( + "pairing_classes", + "reference_pairing", + "class_reference_pairings", + ), + ("rules", "class_pairing_index", "rule_class_pairing_indices"), + ("rules", "endpoint_pairing", "rule_endpoint_pairings"), + ("rules", "source_permutation", "rule_source_slot_permutations"), + ("rules", "lineage", "rule_lineages"), + ) + for parent_name, prefix, child_name in range_groups: + _validate_ranges( + by_name[parent_name], + f"{prefix}_start", + f"{prefix}_count", + by_name[child_name].row_count, + f"{parent_name}.{prefix}", + ) + + +def _validate_dense_ids(table: RecurrenceColumnarTable, column: str) -> None: + if not np.array_equal(table.column(column), np.arange(table.row_count, dtype=_U32)): + raise RecurrenceColumnarInputError( + f"table {table.name!r} IDs are not dense and canonical" + ) + + +def _validate_ranges( + table: RecurrenceColumnarTable, + start_column: str, + count_column: str, + child_count: int, + context: str, +) -> None: + cursor = 0 + for row in range(table.row_count): + start = int(table.column(start_column)[row]) + count = int(table.column(count_column)[row]) + if start != cursor or start + count > child_count: + raise RecurrenceColumnarInputError( + f"{context} ranges are not contiguous or are out of bounds" + ) + cursor += count + if cursor != child_count: + raise RecurrenceColumnarInputError( + f"{context} ranges do not cover every flattened row" + ) + + +def _catalog_strings(catalog: FermionPairingCatalogV1) -> Iterable[str]: + yield _PAIRING_COLUMNAR_ABI + yield catalog.process_key + yield PAIRING_PROOF_ALGORITHM + for endpoint in catalog.endpoints: + yield endpoint.species_id + yield from endpoint.state_template_ids + yield from endpoint.anti_state_template_ids + yield from endpoint.basis_ids + for pairing_class in catalog.pairing_classes: + yield pairing_class.species_id + for rule in catalog.rules: + yield rule.proof_algorithm + + +def _catalog_exact_integers(catalog: FermionPairingCatalogV1) -> Iterable[int]: + for rule in catalog.rules: + yield rule.exact_factor.real_numerator + yield rule.exact_factor.real_denominator + yield rule.exact_factor.imag_numerator + yield rule.exact_factor.imag_denominator + + +def _topology_payload( + source_count: int, + endpoints: tuple[ExternalFermionEndpointRowV1, ...], + pairing_classes: tuple[FermionPairingClassRowV1, ...], + rules: tuple[FermionPairingRuleRowV1, ...], +) -> dict[str, object]: + return { + "algorithm": PAIRING_PROOF_ALGORITHM, + "source_count": source_count, + "endpoints": tuple( + ( + endpoint.source_slot, + endpoint.public_label, + endpoint.species_class_id, + endpoint.color_orientation, + ) + for endpoint in endpoints + ), + "pairing_classes": tuple( + ( + row.class_id, + row.fundamental_source_slots, + row.antifundamental_source_slots, + row.reference_pairings, + row.pairing_count, + ) + for row in pairing_classes + ), + "rules": tuple( + ( + rule.class_pairing_indices, + rule.endpoint_pairings, + rule.source_slot_permutation, + rule.lineage_by_source_slot, + rule.fermion_parity, + ) + for rule in rules + ), + } + + +def _permutation_parity(reference: Sequence[int], candidate: Sequence[int]) -> int: + ranks = {value: index for index, value in enumerate(reference)} + permutation = tuple(ranks[value] for value in candidate) + inversions = sum( + left > right + for index, left in enumerate(permutation) + for right in permutation[index + 1 :] + ) + return -1 if inversions % 2 else 1 + + +def _unsigned_limbs(value: int) -> tuple[int, ...]: + magnitude = abs(_checked_int(value, "exact integer")) + limbs: list[int] = [] + while magnitude: + limbs.append(magnitude & ((1 << 64) - 1)) + magnitude >>= 64 + return tuple(limbs) + + +def _canonical_strings(values: Sequence[str], context: str) -> tuple[str, ...]: + materialized = tuple(_nonempty_text(value, context) for value in values) + if materialized != tuple(sorted(set(materialized))): + raise RecurrenceColumnarInputError(f"{context} are not canonical") + return materialized + + +def _canonical_i32s(values: Sequence[int], context: str) -> tuple[int, ...]: + materialized = tuple(_checked_i32(value, context) for value in values) + if materialized != tuple(sorted(set(materialized))): + raise RecurrenceColumnarInputError(f"{context} are not canonical") + return materialized + + +def _canonical_u32_slots( + values: Sequence[int], bound: int, context: str +) -> tuple[int, ...]: + materialized = tuple(_bounded_u32(value, bound, context) for value in values) + if materialized != tuple(sorted(set(materialized))): + raise RecurrenceColumnarInputError(f"{context} are not canonical") + return materialized + + +def _nonempty_text(value: object, context: str) -> str: + if not isinstance(value, str) or not value: + raise RecurrenceColumnarInputError(f"{context} must be nonempty text") + try: + value.encode("utf-8") + except UnicodeEncodeError as error: + raise RecurrenceColumnarInputError( + f"{context} is not valid UTF-8 text" + ) from error + return value + + +def _checked_int(value: object, context: str) -> int: + if isinstance(value, bool) or not isinstance(value, int): + raise RecurrenceColumnarInputError(f"{context} must be an integer") + return value + + +def _checked_u32(value: object, context: str) -> int: + parsed = _checked_int(value, context) + if not 0 <= parsed < (1 << 32): + raise RecurrenceColumnarInputError(f"{context} does not fit u32") + return parsed + + +def _checked_u64(value: object, context: str) -> int: + parsed = _checked_int(value, context) + if not 0 <= parsed < (1 << 64): + raise RecurrenceColumnarInputError(f"{context} does not fit u64") + return parsed + + +def _checked_i32(value: object, context: str) -> int: + parsed = _checked_int(value, context) + if not -(1 << 31) <= parsed < (1 << 31): + raise RecurrenceColumnarInputError(f"{context} does not fit i32") + return parsed + + +def _bounded_u32(value: object, bound: int, context: str) -> int: + parsed = _checked_u32(value, context) + if parsed >= bound: + raise RecurrenceColumnarInputError(f"{context} is out of bounds") + return parsed + + +def _digest_bytes(value: object, context: str) -> bytes: + if not isinstance(value, str) or len(value) != 64: + raise RecurrenceColumnarInputError( + f"{context} must be a lowercase SHA-256 digest" + ) + try: + decoded = bytes.fromhex(value) + except ValueError as error: + raise RecurrenceColumnarInputError( + f"{context} must be a lowercase SHA-256 digest" + ) from error + if decoded.hex() != value: + raise RecurrenceColumnarInputError( + f"{context} must be a lowercase SHA-256 digest" + ) + return decoded + + +def _digest(payload: object) -> str: + encoded = json.dumps( + payload, + ensure_ascii=True, + separators=(",", ":"), + sort_keys=True, + ).encode("ascii") + return hashlib.sha256(encoded).hexdigest() + + +__all__: tuple[str, ...] = () diff --git a/pyamplicol/src/pyamplicol/generation/recurrence_physics.py b/pyamplicol/src/pyamplicol/generation/recurrence_physics.py new file mode 100644 index 0000000000..84e2405548 --- /dev/null +++ b/pyamplicol/src/pyamplicol/generation/recurrence_physics.py @@ -0,0 +1,1168 @@ +# SPDX-License-Identifier: 0BSD +"""Public physics and parameter metadata for compact recurrence artifacts. + +The recurrence builder forks before :class:`GenericDAG`, so its public axes +must be derived from the authenticated process projection rather than from a +materialized DAG. This module keeps that derivation small and deterministic. +""" + +from __future__ import annotations + +import hashlib +import json +from collections.abc import Mapping, Sequence +from itertools import product +from types import SimpleNamespace +from typing import Any + +from .._internal.versions import ( + RECURRENCE_BUILDER_INPUT_ABI, + RECURRENCE_DIRECT_TEMPLATE_ABI, + RECURRENCE_PLAN_ABI, + RECURRENCE_RUNTIME_LAYOUT_ABI, +) +from ..color import ( + ColorContractionPlan, + ColorGroupDescriptor, + GenericColorPlan, + build_color_contraction_plan, + build_symmetric_group_color_contraction_plan, + exact_color_contraction_factor, +) +from ..color.plan_types import _canonical_open_string_product_key +from ..models.base import Model, _runtime_particle_parameter_name +from ..models.recurrence_template import RecurrenceTemplateCatalog +from ..processes.ir import CanonicalProcessIR +from .recurrence_columnar import ( + ExactComplexRationalV1, + RecurrenceBuilderLogicalInputV1, + RecurrenceExternalLegV1, + RecurrenceNormalizationV1, + RecurrenceParameterProjectionV1, + RecurrencePublicLCFlowV1, +) + +_NORMALIZATION_EXTENSION_KEYS = ( + "color_accuracy", + "color_factor", + "average_factor", + "identical_factor", + "global_coupling_factor", + "qcd_coupling_power", + "electroweak_coupling_power", + "couplings_in_stage_evaluators", + "coupling_policy", +) +_GLOBAL_HELICITY_FLIP_EQUIVALENCE_ROLE = "helicity-equivalence:global-flip-v1" +_ZERO_SECTOR_OWNER = 0xFFFF_FFFF + + +def build_recurrence_normalization( + process: CanonicalProcessIR, + model: Model, +) -> tuple[RecurrenceNormalizationV1, dict[str, object]]: + """Bind the existing process normalization without constructing a DAG.""" + + # Existing model implementations inspect only ``dag.process``. Keep the + # compatibility adapter local while preserving the established payload. + payload = dict( + model.runtime_normalization_payload(SimpleNamespace(process=process)) + ) + encoded = json.dumps( + payload, + allow_nan=False, + ensure_ascii=True, + separators=(",", ":"), + sort_keys=True, + ).encode("ascii") + return ( + RecurrenceNormalizationV1( + factor=ExactComplexRationalV1(1), + convention="runtime-normalization-extension-v1", + semantic_digest=hashlib.sha256(encoded).hexdigest(), + ), + payload, + ) + + +def build_recurrence_physics( + process: CanonicalProcessIR, + logical: RecurrenceBuilderLogicalInputV1, + catalog: RecurrenceTemplateCatalog, + *, + process_id: str, + resolved_helicities: Sequence[Sequence[int]], + normalization: Mapping[str, object], + selected_color_sector_ids: Sequence[int] | None, + color_plan: GenericColorPlan | None = None, +) -> dict[str, object]: + """Build strict ``pyamplicol-resolved-physics`` metadata pre-DAG.""" + + if logical.process_id != process.key: + raise ValueError("recurrence projection does not belong to the process IR") + if process.color_accuracy != "lc" and color_plan is None: + raise ValueError("contracted recurrence physics requires its color plan") + if logical.layout != "all-flow-union" and not resolved_helicities: + raise ValueError("fixed-source recurrence has no resolved helicities") + + possible_helicities = _possible_helicities(logical) + resolved = {tuple(int(value) for value in row) for row in resolved_helicities} + unknown = resolved - set(possible_helicities) + if unknown: + raise ValueError( + "recurrence lowering returned helicities outside generated coverage: " + f"{sorted(unknown)!r}" + ) + # All-flow-union selects source states at runtime and deliberately has no + # helicity-expanded destinations. Every retained assignment is therefore + # executable even though some may evaluate to zero dynamically. + computed_helicities = ( + set(possible_helicities) if logical.layout == "all-flow-union" else resolved + ) + parity_proof = next( + ( + row + for row in getattr(logical, "semantic_digests", ()) + if row.role == _GLOBAL_HELICITY_FLIP_EQUIVALENCE_ROLE + ), + None, + ) + parity_reduced = parity_proof is not None and logical.layout != "all-flow-union" + if parity_reduced: + for values in resolved: + flipped = tuple(-value for value in values) + if flipped != values and flipped in resolved: + raise ValueError( + "recurrence parity proof retained both members of global " + f"helicity-flip orbit {values!r} / {flipped!r}" + ) + helicities = [] + structural_zero_count = 0 + parity_aliases: list[dict[str, str]] = [] + for index, values in enumerate(possible_helicities): + identifier = _helicity_id(values) + representative = values + computed = values in computed_helicities + if parity_reduced and not computed: + flipped = tuple(-value for value in values) + if flipped in resolved: + representative = flipped + structural_zero = not computed and representative == values + structural_zero_count += int(structural_zero) + representative_id = _helicity_id(representative) + if representative != values: + parity_aliases.append( + { + "physical_id": identifier, + "representative_id": representative_id, + } + ) + helicities.append( + { + "id": identifier, + "index": index, + "values": list(values), + "computed": computed, + "structural_zero": structural_zero, + "representative_id": representative_id, + "coefficient": 0.0 if structural_zero else 1.0, + } + ) + if parity_reduced: + nonzero_physical = len(helicities) - structural_zero_count + if nonzero_physical != 2 * len(resolved) or len(parity_aliases) != len( + resolved + ): + raise ValueError( + "recurrence global-helicity-flip reduction does not form complete " + "two-member nonzero orbits" + ) + + selected_flow_ids = ( + None + if logical.selected_public_flow_ids is None + else set(logical.selected_public_flow_ids) + ) + selected_sector_ids = ( + None + if selected_color_sector_ids is None + else tuple(sorted({int(value) for value in selected_color_sector_ids})) + ) + if (selected_flow_ids is None) != (selected_sector_ids is None): + raise ValueError( + "recurrence selected public flows and requested color sectors must " + "be recorded together" + ) + if selected_sector_ids == (): + raise ValueError("recurrence selected color sectors cannot be empty") + retained_flows = _retained_public_flows(logical) + if not retained_flows: + raise ValueError("recurrence physics has no retained public LC flow") + labels_by_slot = { + leg.source_slot: leg.public_label for leg in logical.external_legs + } + if process.color_accuracy == "lc": + color_components = [] + for index, flow in enumerate(retained_flows): + weight = _complex_factor(flow.reduction_weight) + color_components.append( + { + "kind": "lc-flow", + "id": flow.public_id, + "index": index, + "word": [labels_by_slot[slot] for slot in flow.word_source_slots], + "computed": True, + "representative_id": flow.public_id, + "coefficient": float( + weight.real * weight.real + weight.imag * weight.imag + ), + } + ) + else: + color_components = [ + { + "kind": "contracted-color", + "id": "color:contracted", + "index": 0, + "description": ( + "coherent sparse contraction of the complete ordered color basis" + ), + } + ] + + selected_sources = { + row.source_slot: tuple(row.source_state_indices) + for row in logical.selected_source_coverage or () + } + selected_source_helicities = {} + for leg in logical.external_legs: + retained = selected_sources.get(leg.source_slot) + if retained is None: + continue + retained_helicities = { + leg.source_states[index].public_helicity for index in retained + } + if len(retained_helicities) == 1: + selected_source_helicities[str(leg.public_label)] = ( + retained_helicities.pop() + ) + + color_coverage = ( + ("complete" if selected_flow_ids is None else "selected") + if process.color_accuracy == "lc" + else "contracted" + ) + helicity_coverage = ( + "complete" if logical.selected_source_coverage is None else "selected" + ) + public_parameters = _public_model_parameters(catalog) + return { + "schema_version": 1, + "kind": "pyamplicol-resolved-physics", + "process_id": process_id, + "process": process.process, + "color_accuracy": process.color_accuracy, + "coverage": { + "helicities": helicity_coverage, + "color": color_coverage, + "color_kind": ( + "physical-lc-flows" + if process.color_accuracy == "lc" + else "contracted-color" + ), + "structural_zero_helicity_count": structural_zero_count, + }, + "external_particles": [ + { + "index": index, + "label": int(leg.label), + "particle": str(leg.particle), + "pdg": int(leg.pdg), + "role": "initial" if leg.is_initial else "final", + "momentum_slot": index, + "momentum_components": ["E", "px", "py", "pz"], + } + for index, leg in enumerate(process.legs) + ], + "helicities": helicities, + "color_components": color_components, + # The compact recurrence plan owns the high-cardinality destination + # expansion. Rusticol hydrates these groups while loading the plan. + "reduction": { + "kind": ( + "lc-diagonal" if process.color_accuracy == "lc" else "contracted-color" + ), + "groups": [], + }, + "model_parameters": public_parameters, + "selectors": { + "helicity": True, + "color_flow": process.color_accuracy == "lc", + "contracted_color": False, + }, + "extensions": { + "process_key": process.key, + "normalization": { + key: normalization[key] + for key in _NORMALIZATION_EXTENSION_KEYS + if key in normalization + }, + "selected_source_helicities": selected_source_helicities, + "runtime_selectors": { + "kind": "pyamplicol-runtime-selectors", + "contract_version": 1, + "provenance": RECURRENCE_PLAN_ABI, + "axes": { + "helicity": { + "generation_coverage": helicity_coverage, + "generation_selection": selected_source_helicities, + "runtime_contract": ( + "complete-reusable" + if helicity_coverage == "complete" + else "generation-specialized" + ), + }, + "color_flow": { + "generation_coverage": color_coverage, + "generation_selection": list(selected_sector_ids or ()), + "runtime_contract": ( + "complete-reusable" + if color_coverage == "complete" + else ( + "generation-specialized" + if color_coverage == "selected" + else "contracted-color" + ) + ), + }, + }, + "generation_specialized_axes": [ + *([] if helicity_coverage == "complete" else ["helicity"]), + *(["color_flow"] if color_coverage == "selected" else []), + ], + }, + "recurrence_runtime_reduction": { + "kind": "pyamplicol-recurrence-native-reduction-v2", + "runtime_layout_abi": RECURRENCE_RUNTIME_LAYOUT_ABI, + "container_path": "recurrence-runtime.pacbin", + "plan_member_path": "schedule/recurrence-direct-schedule-v2.bin", + }, + "recurrence": { + "builder_input_abi": RECURRENCE_BUILDER_INPUT_ABI, + "plan_abi": RECURRENCE_PLAN_ABI, + "runtime_layout_abi": RECURRENCE_RUNTIME_LAYOUT_ABI, + "direct_template_abi": RECURRENCE_DIRECT_TEMPLATE_ABI, + "lc_flow_layout": logical.layout, + }, + **( + {} + if not parity_reduced + else { + "global_helicity_flip_reduction": { + "kind": "pyamplicol-global-helicity-flip-reduction-v1", + "proof_role": parity_proof.role, + "proof_digest": parity_proof.digest, + "physical_nonzero_helicity_count": ( + len(helicities) - structural_zero_count + ), + "representative_helicity_count": len(resolved), + "aliases": parity_aliases, + } + } + ), + }, + } + + +def build_recurrence_runtime_metadata( + logical: RecurrenceBuilderLogicalInputV1, + catalog: RecurrenceTemplateCatalog, + model: Model, + normalization: Mapping[str, object], +) -> dict[str, object]: + """Return bounded source, parameter, and normalization runtime metadata.""" + + return _build_recurrence_runtime_metadata( + logical, + catalog, + model, + normalization, + ) + + +def build_on_the_fly_public_metadata( + process: CanonicalProcessIR, + catalog: RecurrenceTemplateCatalog, + *, + process_id: str, +) -> dict[str, object]: + """Return compact O(externals + parameters) public display metadata.""" + + return { + "schema_version": 1, + "kind": "pyamplicol-on-the-fly-public-metadata", + "process_id": process_id, + "process": process.process, + "color_accuracy": process.color_accuracy, + "external_particles": [ + { + "index": index, + "label": int(leg.label), + "particle": str(leg.particle), + "pdg": int(leg.pdg), + "role": "initial" if leg.is_initial else "final", + "momentum_slot": index, + "momentum_components": ["E", "px", "py", "pz"], + } + for index, leg in enumerate(process.legs) + ], + "model_parameters": _public_model_parameters(catalog), + } + + +def build_on_the_fly_runtime_metadata( + external_legs: Sequence[RecurrenceExternalLegV1], + parameter_projection: Sequence[RecurrenceParameterProjectionV1], + catalog: RecurrenceTemplateCatalog, + model: Model, + normalization: Mapping[str, object], +) -> dict[str, object]: + """Return irreducible runtime support for the source-only OTF lane.""" + + # The established recurrence implementation below already owns parameter + # defaults, source masses, external-slot order, and normalization. Feed it + # only those shared structural rows and discard its recurrence-only source + # replicas instead of introducing a second metadata implementation. + support = _build_recurrence_runtime_metadata( + SimpleNamespace( + parameter_projection=tuple(parameter_projection), + external_legs=tuple(external_legs), + # This branch suppresses public flow projection; the on-the-fly + # selector adapter derives that axis lazily from the compact seed. + layout="contracted-color-union", + ), + catalog, + model, + normalization, + include_source_templates=False, + ) + return { + name: support[name] + for name in ( + "runtime_parameters", + "prepared_parameter_defaults", + "parameter_projection", + "external_legs", + "particle_masses", + "normalization", + ) + } + + +def _build_recurrence_runtime_metadata( + logical: Any, + catalog: RecurrenceTemplateCatalog, + model: Model, + normalization: Mapping[str, object], + *, + include_source_templates: bool = True, +) -> dict[str, object]: + """Build shared prepared-runtime support and recurrence-only companions.""" + + parameter_projection = logical.parameter_projection + external_legs = logical.external_legs + layout = logical.layout + + values_by_name: dict[str, complex] = {} + for provider_name in ( + "runtime_parameter_defaults", + "runtime_derived_parameter_defaults", + ): + provider = getattr(model, provider_name, None) + if not callable(provider): + continue + for name, raw_value in provider().items(): + values_by_name[str(name)] = _complex_value(raw_value) + + prepared_defaults = [0j] * len(catalog.parameters) + for parameter in catalog.parameters: + prepared_id = parameter.prepared_parameter_id + if prepared_id is None: + continue + if prepared_id >= len(prepared_defaults): + raise ValueError( + f"prepared parameter ID {prepared_id} exceeds catalog size" + ) + value = values_by_name.get(parameter.name) + if value is None and parameter.default_value is not None: + value = _complex_factor(parameter.default_value) + if value is None: + raise ValueError( + f"recurrence prepared parameter {parameter.name!r} has no default" + ) + prepared_defaults[prepared_id] = value + + runtime_parameters = [] + for projection in sorted( + parameter_projection, key=lambda item: item.runtime_slot + ): + parameter = catalog.parameters[projection.parameter_template_id] + if parameter.name != projection.runtime_name: + raise ValueError( + "recurrence parameter projection name disagrees with its template" + ) + value = values_by_name.get(parameter.name) + if value is None and parameter.default_value is not None: + value = _complex_factor(parameter.default_value) + if value is None: + raise ValueError( + f"recurrence runtime parameter {parameter.name!r} has no default" + ) + is_complex = parameter.value_type == "complex" + component_name = "real" if projection.component == 0 else "imag" + runtime_parameters.append( + { + "name": ( + f"{parameter.name}.{component_name}" + if is_complex + else parameter.name + ), + "kind": ( + "derived_parameter_component" + if parameter.parameter_kind == "derived" + else parameter.parameter_kind + ), + "parameter_index": projection.runtime_slot, + "default": float( + value.real if projection.component == 0 else value.imag + ), + "runtime_name": parameter.name if is_complex else None, + "complex_component": component_name if is_complex else None, + } + ) + + state_index_by_id = { + state.template_id: index for index, state in enumerate(catalog.current_states) + } + referenced_source_ids = sorted( + { + state.source_template_id + for leg in external_legs + for state in leg.source_states + } + ) + source_templates = [] + particle_masses: dict[int, float] = {} + parameter_names = {parameter.name for parameter in catalog.parameters} + for source_template_id in referenced_source_ids: + source = catalog.sources[source_template_id] + state = next( + item + for item in catalog.current_states + if item.template_id == source.state_template_id + ) + source_ir = model._source_ir(state.particle_id) + particle = model.particle(state.particle_id) + mass_parameter = _runtime_particle_parameter_name( + source_ir.mass_parameter, + particle_pdg=int(particle.pdg), + kind="mass", + available_names=parameter_names, + ) + if include_source_templates: + width_parameter = _runtime_particle_parameter_name( + source_ir.width_parameter, + particle_pdg=int(particle.pdg), + kind="width", + available_names=parameter_names, + ) + try: + crossing = _runtime_crossing(json.loads(source.crossing)) + except json.JSONDecodeError as exc: # pragma: no cover - validated + raise ValueError( + f"recurrence source {source.template_id!r} has malformed " + "crossing JSON" + ) from exc + source_ir_payload = source_ir.to_json_dict() + # Built-in models expose particle masses directly rather than + # through named SourceIR parameters. Prepared kernels use the + # generic particle fallback; retain the same name here. + source_ir_payload["mass_parameter"] = mass_parameter + source_ir_payload["width_parameter"] = width_parameter + if crossing != source_ir_payload["crossing"]: + raise ValueError( + f"recurrence source {source.template_id!r} crossing disagrees " + "with its typed SourceIR" + ) + source_templates.append( + { + "source_template_id": source_template_id, + "current_state_template_id": state_index_by_id[state.template_id], + "dimension": state.dimension, + "helicity": source.helicity, + "chirality": state.chirality, + "spin_state": source.spin_state, + "source_ir": source_ir_payload, + "crossing": crossing, + } + ) + mass = float(model.mass(state.particle_id)) + if mass_parameter is not None: + mass_value = values_by_name.get(mass_parameter) + if mass_value is not None: + if mass_value.imag != 0.0: + raise ValueError( + "recurrence source mass parameter must be real: " + f"{mass_parameter!r}" + ) + mass = float(mass_value.real) + previous = particle_masses.setdefault(state.particle_id, mass) + if previous != mass: + raise ValueError( + "recurrence particle " + f"{state.particle_id} has inconsistent source masses" + ) + + return { + "public_color_flows": ( + [ + { + "public_id": flow.public_id, + "construction_sector_id": flow.construction_sector_id, + "target_sector_id": target_sector_id, + } + for target_sector_id, flow in enumerate(_retained_public_flows(logical)) + ] + if layout != "contracted-color-union" + else [] + ), + "runtime_parameters": runtime_parameters, + "prepared_parameter_defaults": [ + [float(value.real), float(value.imag)] for value in prepared_defaults + ], + "parameter_projection": [ + { + "runtime_slot": row.runtime_slot, + "runtime_name": row.runtime_name, + "parameter_template_id": row.parameter_template_id, + "prepared_parameter_id": row.prepared_parameter_id, + "component": row.component, + } + for row in parameter_projection + ], + "source_templates": source_templates, + "external_legs": [ + { + "source_slot": leg.source_slot, + "public_label": leg.public_label, + "physical_pdg": leg.physical_pdg, + "outgoing_pdg": leg.outgoing_pdg, + "is_initial": leg.is_initial, + } + for leg in external_legs + ], + "particle_masses": [ + {"outgoing_pdg": pdg, "mass": mass} + for pdg, mass in sorted(particle_masses.items()) + ], + "normalization": { + key: normalization[key] + for key in _NORMALIZATION_EXTENSION_KEYS + if key in normalization + }, + } + + +def build_recurrence_color_contraction( + logical: RecurrenceBuilderLogicalInputV1, + color_plan: GenericColorPlan, + resolved_helicities: Sequence[Sequence[int]], + amplitude_destinations: Sequence[tuple[int, int | None]], + certified_structural_zero_sector_ids: Sequence[int], + *, + contraction: str = "direct", +) -> ColorContractionPlan | None: + if color_plan.color_accuracy == "lc": + return None + if logical.layout != "contracted-color-union": + raise ValueError("NLC/full recurrence requires contracted-color-union") + if not logical.physical_sectors or not resolved_helicities: + raise ValueError("contracted recurrence has an empty color/helicity domain") + contraction_destinations = recurrence_color_contraction_destinations( + logical, + resolved_helicities, + amplitude_destinations, + ) + labels_by_slot = { + leg.source_slot: leg.public_label for leg in logical.external_legs + } + sectors_by_id = {sector.sector_id: sector for sector in logical.physical_sectors} + descriptors = [] + for group_id, (sector_id, helicity_id) in enumerate(contraction_destinations): + try: + sector = sectors_by_id[sector_id] + except KeyError as exc: + raise ValueError( + f"contracted recurrence destination {group_id} references " + f"unknown physical sector {sector_id}" + ) from exc + if helicity_id is None: + raise ValueError( + f"contracted recurrence destination {group_id} has no helicity" + ) + try: + helicity = resolved_helicities[helicity_id] + except IndexError as exc: + raise ValueError( + f"contracted recurrence destination {group_id} references " + f"unknown resolved helicity {helicity_id}" + ) from exc + descriptors.append( + ColorGroupDescriptor( + group_id=group_id, + helicity_key=tuple(int(value) for value in helicity), + sector_id=sector.sector_id, + word=tuple(labels_by_slot[slot] for slot in sector.word_source_slots), + helicity_weight=1.0, + ) + ) + if not descriptors: + raise ValueError("contracted recurrence has no nonzero amplitude destination") + if contraction == "direct": + contraction_plan = build_color_contraction_plan( + color_plan, + tuple(descriptors), + ) + elif contraction in {"auto", "symmetric-group-fft"}: + active_sector_ids = {descriptor.sector_id for descriptor in descriptors} + contraction_plan = build_symmetric_group_color_contraction_plan( + color_plan, + tuple(descriptors), + sector_owner_ids=recurrence_color_sector_owner_map( + logical, + active_sector_ids, + set(certified_structural_zero_sector_ids), + ), + ) + if contraction == "auto" and not contraction_plan.supported: + contraction_plan = build_color_contraction_plan( + color_plan, tuple(descriptors) + ) + else: + raise ValueError(f"unknown color contraction method {contraction!r}") + if contraction_plan is None or not contraction_plan.supported: + reason = None if contraction_plan is None else contraction_plan.reason + raise ValueError( + f"could not build recurrence color contraction: {reason or 'unsupported'}" + ) + return contraction_plan + + +def build_on_the_fly_color_contraction( + color_plan: GenericColorPlan, + *, + contraction: str = "direct", +) -> tuple[ColorContractionPlan, tuple[int, ...], tuple[int, ...]]: + """Build the one-component metric over OTF structural color selectors. + + Full/NLC color plans explicitly contain whole-open-string block-order + replicas. The native query decoder already erases that traversal-only + ordering, so one runtime amplitude is requested for each canonical tensor + owner and the color metric contracts those owner amplitudes into one public + result. + """ + + if color_plan.color_accuracy not in {"nlc", "full"}: + raise ValueError("on-the-fly color contraction requires NLC or full color") + if color_plan.trace_reflections_folded: + raise ValueError( + "on-the-fly contracted color cannot fold trace reflections" + ) + owner_by_sector = on_the_fly_color_sector_owner_map(color_plan) + owner_sector_ids = tuple( + sector_id + for sector_id, owner_id in enumerate(owner_by_sector) + if sector_id == owner_id + ) + sector_by_id = {int(sector.id): sector for sector in color_plan.sectors} + descriptors = tuple( + ColorGroupDescriptor( + group_id=group_id, + # All structural selectors contribute to the same contracted + # component. A single component forces expanded v3 storage. + helicity_key=(), + sector_id=sector_id, + word=tuple(sector_by_id[sector_id].color_words[0]), + helicity_weight=1.0, + ) + for group_id, sector_id in enumerate(owner_sector_ids) + ) + if contraction == "direct": + contraction_plan = build_color_contraction_plan(color_plan, descriptors) + elif contraction in {"auto", "symmetric-group-fft"}: + contraction_plan = build_symmetric_group_color_contraction_plan( + color_plan, + descriptors, + sector_owner_ids=owner_by_sector, + ) + if contraction == "auto" and not contraction_plan.supported: + contraction_plan = build_color_contraction_plan(color_plan, descriptors) + else: + raise ValueError(f"unknown color contraction method {contraction!r}") + if contraction_plan is None or not contraction_plan.supported: + reason = None if contraction_plan is None else contraction_plan.reason + raise ValueError( + "could not build on-the-fly color contraction: " + f"{reason or 'unsupported'}" + ) + if contraction_plan.repeated_block is not None: + raise ValueError( + "one-component on-the-fly color contraction must use expanded storage" + ) + return contraction_plan, owner_by_sector, owner_sector_ids + + +def on_the_fly_color_sector_owner_map( + color_plan: GenericColorPlan, +) -> tuple[int, ...]: + """Return each full/NLC sector's canonical structural-selector owner. + + Only permutations of complete open strings are aliases. Reversing a + string, trace orientation, and every other physical sector distinction are + retained. Owner sectors appear in the same order as the lazy compact LC + selector axis, including an optional ordinary reference flow moved first. + """ + + sectors = tuple(sorted(color_plan.sectors, key=lambda item: int(item.id))) + if tuple(int(sector.id) for sector in sectors) != tuple(range(len(sectors))): + raise ValueError("on-the-fly physical color sectors are not densely numbered") + if not sectors: + raise ValueError("on-the-fly contracted color plan has no physical sectors") + + owner_by_key: dict[tuple[object, ...], int] = {} + owners: list[int] = [] + for sector in sectors: + sector_id = int(sector.id) + if sector.kind == "open-lines": + open_strings = _canonical_open_string_product_key( + ( + line.fundamental_label, + line.adjoint_labels, + line.antifundamental_label, + line.singlet_labels, + ) + for line in sector.open_color_lines + ) + key: tuple[object, ...] = ("open-lines", open_strings) + else: + key = (sector.kind, sector_id) + owners.append(owner_by_key.setdefault(key, sector_id)) + return tuple(owners) + + +def recurrence_color_contraction_destinations( + logical: RecurrenceBuilderLogicalInputV1, + resolved_helicities: Sequence[Sequence[int]], + amplitude_destinations: Sequence[tuple[int, int | None]], +) -> tuple[tuple[int, int | None], ...]: + """Return the physical amplitude domain consumed by color contraction. + + A contracted topology-replay schedule materializes only one representative + per certified partition. Color contraction still acts on the complete + physical color basis, so its destination domain is the dense + sector-major/helicity-minor replay scratch rather than the smaller Direct + schedule destination table. + """ + + if logical.layout != "contracted-color-union" or not logical.replay_partitions: + return tuple(amplitude_destinations) + return tuple( + (sector.sector_id, helicity_id) + for sector in sorted(logical.physical_sectors, key=lambda item: item.sector_id) + for helicity_id in range(len(resolved_helicities)) + ) + + +def recurrence_color_sector_owner_map( + logical: RecurrenceBuilderLogicalInputV1, + active_sector_ids: set[int], + certified_structural_zero_sector_ids: set[int], +) -> tuple[int, ...]: + """Return the independently derived canonical owner of every color sector. + + Contracted recurrence construction treats permutations of complete open + strings as aliases of the same product of color tensors. This map binds + that reduction into the process artifact so the loader can reject a + schedule that silently omits, duplicates, or changes an alias class. + """ + + sectors = tuple(sorted(logical.physical_sectors, key=lambda item: item.sector_id)) + if tuple(sector.sector_id for sector in sectors) != tuple(range(len(sectors))): + raise ValueError("recurrence physical color sectors are not densely numbered") + + owner_by_key: dict[tuple[object, ...], int] = {} + owners: list[int] = [] + for sector in sectors: + if sector.kind == "open-lines": + open_strings = _canonical_open_string_product_key( + ( + string.fundamental_source_slot, + string.adjoint_source_slots, + string.antifundamental_source_slot, + string.singlet_source_slots, + ) + for string in sector.open_strings + ) + key: tuple[object, ...] = ("open-lines", open_strings) + else: + # Trace orientation and singlet sectors are already canonicalized + # by their construction-sector identity. Rust does not alias them. + key = (sector.kind, sector.sector_id) + owner = owner_by_key.setdefault(key, sector.sector_id) + owners.append(owner) + active = set(active_sector_ids) + unknown = active.difference(range(len(sectors))) + if unknown: + raise ValueError( + f"recurrence color destinations reference unknown sectors {sorted(unknown)}" + ) + certified = set(certified_structural_zero_sector_ids) + unknown_certified = certified.difference(range(len(sectors))) + if unknown_certified: + raise ValueError( + "recurrence structural-zero certificate references unknown sectors " + f"{sorted(unknown_certified)}" + ) + overlap = certified.intersection(active) + if overlap: + raise ValueError( + "recurrence structural-zero certificate overlaps active sectors " + f"{sorted(overlap)}" + ) + members_by_owner: dict[int, set[int]] = {} + for sector_id, owner_id in enumerate(owners): + members_by_owner.setdefault(owner_id, set()).add(sector_id) + result = [] + for _sector_id, owner_id in enumerate(owners): + if owner_id in active: + result.append(owner_id) + continue + active_members = members_by_owner[owner_id].intersection(active) + if active_members: + raise ValueError( + f"recurrence color sector class owned by {owner_id} has active " + f"non-owner sectors {sorted(active_members)}" + ) + uncertified = members_by_owner[owner_id].difference(certified) + if uncertified: + raise ValueError( + f"recurrence color sector class owned by {owner_id} has no active " + "destination and no complete structural-zero certificate; " + f"uncertified sectors={sorted(uncertified)}" + ) + result.append(_ZERO_SECTOR_OWNER) + return tuple(result) + + +def recurrence_exact_color_coefficients( + color_plan: GenericColorPlan, + contraction: ColorContractionPlan, + group_sector_ids: Sequence[int], +) -> tuple[ExactComplexRationalV1, ...]: + """Return exact symmetry-folded coefficients in compact entry order.""" + + symmetric_group = contraction.symmetric_group_block + if symmetric_group is not None: + entries = ( + *symmetric_group.kernel_entries, + *symmetric_group.residual_entries, + ) + exact_weights = ( + *symmetric_group.kernel_exact_weights, + *symmetric_group.residual_exact_weights, + ) + result = [] + for entry, exact in zip(entries, exact_weights, strict=True): + symmetry = int(entry.symmetry_factor) + if symmetry not in {1, 2} or float(symmetry) != entry.symmetry_factor: + raise ValueError( + "symmetric-group color contraction has a noncanonical " + "symmetry factor" + ) + result.append( + ExactComplexRationalV1( + real_numerator=exact.numerator * symmetry, + real_denominator=exact.denominator, + ) + ) + return tuple(result) + + sector_by_id = {sector.id: sector for sector in color_plan.sectors} + repeated = contraction.repeated_block + entries = contraction.entries if repeated is None else repeated.entries + component_count = 1 if repeated is None else repeated.component_count + + result = [] + for entry in entries: + if repeated is None: + left_group_id = entry.left_group_id + right_group_id = entry.right_group_id + else: + left_group_id = repeated.component_group_ids[ + entry.left_group_index * component_count + ] + right_group_id = repeated.component_group_ids[ + entry.right_group_index * component_count + ] + try: + left_sector = sector_by_id[group_sector_ids[left_group_id]] + right_sector = sector_by_id[group_sector_ids[right_group_id]] + except (IndexError, KeyError) as exc: + raise ValueError( + "recurrence color entry references an unknown physical sector" + ) from exc + exact = exact_color_contraction_factor( + color_plan, + left_sector, + right_sector, + accuracy=contraction.color_accuracy, + full_col_acc=20, + ) + symmetry = entry.symmetry_factor + if symmetry not in {1.0, 2.0}: + raise ValueError( + "recurrence color contraction has a non-integral symmetry factor" + ) + folded = exact * int(symmetry) + result.append( + ExactComplexRationalV1( + real_numerator=folded.numerator, + real_denominator=folded.denominator, + ) + ) + return tuple(result) + + +def _retained_public_flows( + logical: RecurrenceBuilderLogicalInputV1, +) -> tuple[RecurrencePublicLCFlowV1, ...]: + selected = ( + None + if logical.selected_public_flow_ids is None + else set(logical.selected_public_flow_ids) + ) + retained = tuple( + flow + for flow in logical.public_flows + if selected is None or flow.flow_id in selected + ) + if not retained: + raise ValueError("recurrence metadata has no retained public LC flow") + return retained + + +def recurrence_referenced_kernel_ids( + logical: RecurrenceBuilderLogicalInputV1, +) -> frozenset[int]: + """Return exactly the prepared kernels named by process template references.""" + + return frozenset( + reference.prepared_kernel_id + for reference in logical.semantic_template_references + if reference.prepared_kernel_id is not None + ) + + +def _possible_helicities( + logical: RecurrenceBuilderLogicalInputV1, +) -> tuple[tuple[int, ...], ...]: + selected = { + row.source_slot: set(row.source_state_indices) + for row in logical.selected_source_coverage or () + } + choices = [] + for leg in logical.external_legs: + retained = selected.get(leg.source_slot) + choices.append( + tuple( + state.public_helicity + for state in leg.source_states + if retained is None or state.state_index in retained + ) + ) + return tuple(sorted(product(*choices))) + + +def _public_model_parameters( + catalog: RecurrenceTemplateCatalog, +) -> list[dict[str, object]]: + result = [] + for parameter in sorted(catalog.parameters, key=lambda item: item.name): + if parameter.parameter_kind == "constant": + continue + default = ( + 0j + if parameter.default_value is None + else _complex_factor(parameter.default_value) + ) + if parameter.name.startswith("normalization."): + kind = "normalization" + elif parameter.parameter_kind == "derived": + kind = "derived" + else: + kind = "external" + result.append( + { + "name": parameter.name, + "kind": kind, + "default_real": float(default.real), + "default_imaginary": float(default.imag), + "mutable": bool(parameter.mutable), + } + ) + return result + + +def _complex_factor(value: object) -> complex: + return complex( + int(value.real_numerator) / int(value.real_denominator), + int(value.imag_numerator) / int(value.imag_denominator), + ) + + +def _runtime_crossing(value: object) -> dict[str, object]: + if not isinstance(value, Mapping): + raise ValueError("recurrence source crossing must be an object") + result = dict(value) + phase = result.get("phase") + if not isinstance(phase, Mapping): + raise ValueError("recurrence source crossing phase must be exact") + result["phase"] = [ + int(phase["real_numerator"]) / int(phase["real_denominator"]), + int(phase["imag_numerator"]) / int(phase["imag_denominator"]), + ] + return result + + +def _complex_value(value: object) -> complex: + if isinstance(value, tuple): + return complex(*value) + return complex(value) # type: ignore[arg-type] + + +def _helicity_id(values: Sequence[int]) -> str: + return "h:" + ",".join(f"{int(value):+d}" for value in values) + + +__all__ = [ + "build_on_the_fly_public_metadata", + "build_on_the_fly_runtime_metadata", + "build_recurrence_color_contraction", + "build_recurrence_normalization", + "build_recurrence_physics", + "build_recurrence_runtime_metadata", + "recurrence_color_contraction_destinations", + "recurrence_color_sector_owner_map", + "recurrence_exact_color_coefficients", + "recurrence_referenced_kernel_ids", +] diff --git a/pyamplicol/src/pyamplicol/generation/recurrence_projection.py b/pyamplicol/src/pyamplicol/generation/recurrence_projection.py new file mode 100644 index 0000000000..1d8b430a5f --- /dev/null +++ b/pyamplicol/src/pyamplicol/generation/recurrence_projection.py @@ -0,0 +1,1747 @@ +# SPDX-License-Identifier: 0BSD +"""Project proven process semantics into recurrence-builder logical records. + +The projector is deliberately upstream of generic DAG construction. It maps +only canonical process IR, a complete LC colour plan, an optional exact replay +proof, and a prepared recurrence-template catalog into the compact logical ABI +consumed by :mod:`pyamplicol.generation.recurrence_columnar`. +""" + +from __future__ import annotations + +import hashlib +import json +import math +from collections.abc import Mapping, Sequence +from dataclasses import dataclass, replace +from typing import Final + +from ..color.plan import ( + ColorTopologyReplayCertificate, + GenericColorPlan, + LCColorTopologyReplayPlan, +) +from ..color.plan_build import _ordered_open_line_blocks +from ..color.plan_types import _canonical_open_string_product_key +from ..models.base import Model, Vertex +from ..models.recurrence_template import ( + CurrentStateTemplateV1, + EvaluatorBindingV1, + RecurrenceTemplateCatalog, + SourceTemplateV1, +) +from ..models.recurrence_template import ( + ExactComplexRationalV1 as ModelExactComplexRationalV1, +) +from ..processes.ir import CanonicalProcessIR, ProcessLegIR +from .recurrence_columnar import ( + ExactComplexRationalV1, + RecurrenceBuilderLogicalInputV1, + RecurrenceCouplingLimitV1, + RecurrenceExternalLegV1, + RecurrenceLCFlowLayout, + RecurrenceLCOpenStringV1, + RecurrenceNormalizationV1, + RecurrenceParameterProjectionV1, + RecurrencePhysicalLCSectorV1, + RecurrencePublicLCFlowV1, + RecurrenceReplayPartitionV1, + RecurrenceReplayTargetV1, + RecurrenceSelectedSourceCoverageV1, + RecurrenceSemanticDigestV1, + RecurrenceSemanticTemplateReferenceV1, + RecurrenceSourceStateV1, +) +from .recurrence_fermion_pairing import ( + FermionPairingCatalogV1, + build_recurrence_fermion_pairing_catalog_v1, +) + +_TEMPLATE_SECTIONS: Final = ( + ("parameter", "parameters"), + ("current-state", "current_states"), + ("source", "sources"), + ("quantum-flow", "quantum_flows"), + ("transition", "transitions"), + ("propagator", "propagators"), + ("closure", "closures"), + ("color-contraction", "color_contractions"), + ("symmetry-proof", "symmetry_proofs"), +) +_CLOSURE_ANCHOR_PROOF_ALGORITHM_V2: Final = "canonical-lc-closure-anchor-v2" +_CLOSURE_ANCHOR_PROOF_ALGORITHM_V3: Final = "canonical-lc-closure-anchor-v3" +_CLOSURE_ANCHOR_PROOF_ALGORITHM_V4: Final = "canonical-lc-closure-anchor-v4" +_IDENTITY_REPLAY_PROOF_ALGORITHM: Final = ( + "canonical-recurrence-identity-materialization-v1" +) +_PURE_MASSLESS_ADJOINT_HELICITY_SUPPORT_ROLE: Final = ( + "helicity-support:pure-massless-adjoint-tree-v1" +) +_GLOBAL_HELICITY_FLIP_EQUIVALENCE_ROLE: Final = "helicity-equivalence:global-flip-v1" + + +class RecurrenceProjectionError(ValueError): + """The supplied process semantics cannot be projected exactly.""" + + +@dataclass(frozen=True, slots=True) +class RecurrenceGenerationSliceV1: + """Explicit generation-time coverage retained by recurrence v1.""" + + selected_public_flow_ids: tuple[int, ...] | None = None + selected_source_helicities: tuple[tuple[int, int], ...] | None = None + + def __post_init__(self) -> None: + if self.selected_public_flow_ids is not None: + flows = tuple(sorted(int(value) for value in self.selected_public_flow_ids)) + if not flows or len(flows) != len(set(flows)): + raise RecurrenceProjectionError( + "selected recurrence public flows must be nonempty and unique" + ) + object.__setattr__(self, "selected_public_flow_ids", flows) + if self.selected_source_helicities is not None: + selected = tuple( + sorted( + (int(label), int(helicity)) + for label, helicity in self.selected_source_helicities + ) + ) + labels = tuple(label for label, _ in selected) + if not selected or len(labels) != len(set(labels)): + raise RecurrenceProjectionError( + "selected recurrence source labels must be nonempty and unique" + ) + object.__setattr__(self, "selected_source_helicities", selected) + + +def project_recurrence_process_v1( + process: CanonicalProcessIR, + color_plan: GenericColorPlan, + template_catalog: RecurrenceTemplateCatalog, + *, + layout: RecurrenceLCFlowLayout, + normalization: RecurrenceNormalizationV1, + topology_replay: ( + LCColorTopologyReplayPlan | ColorTopologyReplayCertificate | None + ) = None, + generation_slice: RecurrenceGenerationSliceV1 | None = None, + coupling_order_limits: Mapping[str, int] | None = None, + process_support_mask: int = 1, + model: Model | None = None, + prefer_symmetric_group_closure_anchor: bool = False, + contracted_physical_color_union: bool = False, +) -> RecurrenceBuilderLogicalInputV1: + """Return deterministic recurrence-builder records for one LC process. + + Normalization is supplied as an exact, model-owned contract because it is + process dependent and intentionally absent from the process-independent + recurrence template catalog. + """ + + _validate_projection_roots( + process, + color_plan, + template_catalog, + layout, + contracted_physical_color_union=contracted_physical_color_union, + ) + if not isinstance(normalization, RecurrenceNormalizationV1): + raise TypeError("recurrence projection requires exact normalization v1") + if ( + isinstance(process_support_mask, bool) + or not isinstance(process_support_mask, int) + or process_support_mask <= 0 + ): + raise RecurrenceProjectionError( + "recurrence process support mask must be a positive integer bitmap" + ) + selection = generation_slice or RecurrenceGenerationSliceV1() + if contracted_physical_color_union and ( + topology_replay is not None + or selection.selected_public_flow_ids is not None + or selection.selected_source_helicities is not None + ): + raise RecurrenceProjectionError( + "contracted physical-color union requires complete runtime flow and " + "source coverage without replay" + ) + projected_color_plan = ( + _physical_color_owner_plan(color_plan) + if contracted_physical_color_union + else color_plan + ) + reflection_contract = _closure_reflection_contract( + template_catalog, + required=projected_color_plan.trace_reflections_folded, + ) + + template_ids, template_references = _project_template_references(template_catalog) + external_legs, selected_sources = _project_external_legs( + process, + template_catalog, + template_ids, + selection.selected_source_helicities, + process_support_mask, + ) + physical_sectors, public_flows = _project_physical_sectors( + projected_color_plan, + process, + external_legs, + process_support_mask, + ) + selected_flows = selection.selected_public_flow_ids + if selected_flows is not None: + available_flow_ids = {flow.flow_id for flow in public_flows} + unknown = tuple(sorted(set(selected_flows) - available_flow_ids)) + if unknown: + raise RecurrenceProjectionError( + f"generation slice references unknown public LC flows {unknown!r}" + ) + replay_partitions = _project_replay_partitions( + process, + projected_color_plan, + physical_sectors, + external_legs, + topology_replay, + layout, + ) + if prefer_symmetric_group_closure_anchor: + physical_sectors = _project_cyclic_trace_closure_anchors( + physical_sectors, + replay_partitions, + external_legs, + template_catalog, + ) + fermion_pairing = build_recurrence_fermion_pairing_catalog_v1( + process, + template_catalog.current_states, + quantum_flows=template_catalog.quantum_flows, + ) + closure_reconstruction_digest = _project_closure_obligation_roots_digest( + process, + projected_color_plan, + template_catalog, + physical_sectors, + fermion_pairing, + reflection_contract=reflection_contract, + ) + helicity_support = _project_helicity_support_proof( + process, + model, + model_catalog_digest=template_catalog.header.compiled_model_digest, + coupling_order_limits=coupling_order_limits, + ) + global_helicity_flip = _project_global_helicity_flip_proof( + process, + external_legs, + model, + model_catalog_digest=template_catalog.header.compiled_model_digest, + coupling_order_limits=coupling_order_limits, + generation_selected=selection.selected_source_helicities is not None, + ) + + return RecurrenceBuilderLogicalInputV1( + process_id=process.key, + layout=layout, + semantic_digests=( + RecurrenceSemanticDigestV1("process", _digest(process.to_json_dict())), + RecurrenceSemanticDigestV1( + "model-catalog", template_catalog.header.compiled_model_digest + ), + RecurrenceSemanticDigestV1( + "prepared-catalog", template_catalog.catalog_digest + ), + RecurrenceSemanticDigestV1( + "color-plan", _digest(projected_color_plan.to_json_dict()) + ), + RecurrenceSemanticDigestV1( + "fermion-pairing-semantic", fermion_pairing.semantic_digest + ), + RecurrenceSemanticDigestV1( + "fermion-pairing-topology", fermion_pairing.topology_digest + ), + RecurrenceSemanticDigestV1( + "closure-reconstruction", + closure_reconstruction_digest, + ), + *(() if helicity_support is None else (helicity_support,)), + *(() if global_helicity_flip is None else (global_helicity_flip,)), + ), + external_legs=external_legs, + physical_sectors=physical_sectors, + public_flows=public_flows, + semantic_template_references=template_references, + normalization=normalization, + fermion_pairing_catalog=fermion_pairing, + replay_partitions=replay_partitions, + selected_public_flow_ids=selected_flows, + selected_source_coverage=selected_sources, + coupling_limits=_project_coupling_limits(coupling_order_limits), + parameter_projection=_project_parameters(template_catalog, template_ids), + process_support_mask=process_support_mask, + ) + + +def _project_closure_obligation_roots_digest( + process: CanonicalProcessIR, + color_plan: GenericColorPlan, + template_catalog: RecurrenceTemplateCatalog, + sectors: tuple[RecurrencePhysicalLCSectorV1, ...], + pairing: FermionPairingCatalogV1, + *, + reflection_contract: tuple[ + ModelExactComplexRationalV1 | None, + str | None, + ], +) -> str: + """Bind the exact catalogs from which Rust derives closure obligations. + + This digest is deliberately not an expected-row list. Rust independently + derives complete three-line traversal, fermion-pairing, and folded-trace + obligation multisets from these authenticated roots and compares them with + the closure rows it emits. + """ + + reflection_phase, reflection_proof_digest = reflection_contract + return _digest( + { + "algorithm": "model-generic-closure-obligation-roots-v2", + "color_plan_digest": _digest(color_plan.to_json_dict()), + "fermion_pairing_semantic_digest": pairing.semantic_digest, + "fermion_pairing_topology_digest": pairing.topology_digest, + "model_catalog_digest": template_catalog.catalog_digest, + "process_digest": _digest(process.to_json_dict()), + "reflection_phase": ( + None if reflection_phase is None else reflection_phase.to_dict() + ), + "reflection_proof_digest": reflection_proof_digest, + "sectors": tuple( + { + "closure_proof_algorithm": sector.closure_proof_algorithm, + "closure_proof_digest": sector.closure_proof_digest, + "kind": sector.kind, + "open_strings": tuple( + ( + line.fundamental_source_slot, + *line.adjoint_source_slots, + line.antifundamental_source_slot, + ) + for line in sector.open_strings + ), + "sector_id": sector.sector_id, + "trace_word": sector.trace_source_slots, + "word": sector.word_source_slots, + } + for sector in sectors + ), + } + ) + + +def _closure_reflection_contract( + template_catalog: RecurrenceTemplateCatalog, + *, + required: bool, +) -> tuple[ModelExactComplexRationalV1 | None, str | None]: + proofs = tuple( + proof + for proof in template_catalog.symmetry_proofs + if proof.proof_algorithm == "canonical-current-word-reversal-v1" + ) + if not proofs: + if required: + raise RecurrenceProjectionError( + "folded LC trace reflections require an exact model " + "current-reflection proof" + ) + return None, None + phases = {proof.exact_phase for proof in proofs} + if len(phases) != 1: + raise RecurrenceProjectionError( + "model current-reflection proofs carry inconsistent exact phases" + ) + return next(iter(phases)), _digest( + { + "algorithm": "model-current-reflection-proof-set-v1", + "proofs": tuple(proof.to_dict() for proof in proofs), + } + ) + + +def _project_helicity_support_proof( + process: CanonicalProcessIR, + model: Model | None, + *, + model_catalog_digest: str, + coupling_order_limits: Mapping[str, int] | None, +) -> RecurrenceSemanticDigestV1 | None: + """Project a fail-closed exact theorem without constructing a process DAG.""" + + if model is None: + return None + limits = { + str(name).upper(): int(value) + for name, value in (coupling_order_limits or {}).items() + if int(value) >= 0 + } + vertices = _reachable_vertex_inventory( + process, + model, + coupling_order_limits=limits, + ) + if not model.pure_massless_adjoint_helicity_zero_rule_is_proven( + process, + vertices, + ): + return None + return RecurrenceSemanticDigestV1( + _PURE_MASSLESS_ADJOINT_HELICITY_SUPPORT_ROLE, + _digest( + { + "algorithm": _PURE_MASSLESS_ADJOINT_HELICITY_SUPPORT_ROLE, + "coupling_order_limits": sorted(limits.items()), + "model_catalog_digest": model_catalog_digest, + "process": process.to_json_dict(), + "reachable_vertices": [ + { + "kind": int(vertex.kind), + "particles": [int(value) for value in vertex.particles], + } + for vertex in vertices + ], + } + ), + ) + + +def _project_global_helicity_flip_proof( + process: CanonicalProcessIR, + external_legs: Sequence[RecurrenceExternalLegV1], + model: Model | None, + *, + model_catalog_digest: str, + coupling_order_limits: Mapping[str, int] | None, + generation_selected: bool, +) -> RecurrenceSemanticDigestV1 | None: + """Project the same fail-closed parity theorem used by DAG generation. + + The recurrence builder operates before a :class:`GenericDAG` exists, so it + cannot consume the DAG post-pass directly. Instead, bind the exact model + predicate, reachable vertex inventory, and complete two-state external + helicity axes into the process projection. Generation-specialized + helicity slices remain untouched because they need not contain both orbit + members. + """ + + if model is None or generation_selected: + return None + if len(external_legs) != len(process.legs): + raise RecurrenceProjectionError( + "recurrence parity proof external-leg projection is incomplete" + ) + + external_axes: list[tuple[int, tuple[int, int]]] = [] + for process_leg, projected_leg in zip(process.legs, external_legs, strict=True): + outgoing_pdg = process_leg.outgoing_pdg + if outgoing_pdg is None: + return None + particle_id = int(outgoing_pdg) + if model.mass(particle_id) != 0.0 or not ( + model.is_fundamental_colored_fermion(particle_id) + or model.is_massless_adjoint_vector(particle_id) + ): + return None + helicities = tuple( + sorted( + {int(state.public_helicity) for state in projected_leg.source_states} + ) + ) + if ( + len(helicities) != 2 + or helicities[0] == 0 + or helicities[0] != -helicities[1] + ): + return None + external_axes.append((int(projected_leg.source_slot), helicities)) + + limits = { + str(name).upper(): int(value) + for name, value in (coupling_order_limits or {}).items() + if int(value) >= 0 + } + vertices = _reachable_vertex_inventory( + process, + model, + coupling_order_limits=limits, + ) + if not vertices or not model.global_helicity_flip_equivalence_is_proven(vertices): + return None + return RecurrenceSemanticDigestV1( + _GLOBAL_HELICITY_FLIP_EQUIVALENCE_ROLE, + _digest( + { + "algorithm": _GLOBAL_HELICITY_FLIP_EQUIVALENCE_ROLE, + "coupling_order_limits": sorted(limits.items()), + "external_helicity_axes": external_axes, + "model_catalog_digest": model_catalog_digest, + "process": process.to_json_dict(), + "reachable_vertices": [ + { + "kind": int(vertex.kind), + "particles": [int(value) for value in vertex.particles], + } + for vertex in vertices + ], + } + ), + ) + + +def _reachable_vertex_inventory( + process: CanonicalProcessIR, + model: Model, + *, + coupling_order_limits: Mapping[str, int], +) -> tuple[Vertex, ...]: + """Conservatively close the process species under allowed local vertices.""" + + reachable = { + int(leg.outgoing_pdg) for leg in process.legs if leg.outgoing_pdg is not None + } + used: set[Vertex] = set() + vertices = tuple(model.iter_vertices(color_accuracy=process.color_accuracy)) + changed = True + while changed: + changed = False + for vertex in vertices: + left, right, result = (int(value) for value in vertex.particles) + if left not in reachable or right not in reachable: + continue + orders = { + str(name).upper(): int(value) + for name, value in model.vertex_coupling_orders(vertex) + } + if any( + orders.get(name, 0) > limit + for name, limit in coupling_order_limits.items() + ): + continue + used.add(vertex) + if result not in reachable: + reachable.add(result) + changed = True + return tuple( + sorted( + used, + key=lambda vertex: ( + int(vertex.kind), + tuple(int(value) for value in vertex.particles), + ), + ) + ) + + +def _validate_projection_roots( + process: CanonicalProcessIR, + color_plan: GenericColorPlan, + template_catalog: RecurrenceTemplateCatalog, + layout: str, + *, + contracted_physical_color_union: bool, +) -> None: + if not isinstance(process, CanonicalProcessIR): + raise TypeError("recurrence projection requires CanonicalProcessIR") + if not isinstance(color_plan, GenericColorPlan): + raise TypeError("recurrence projection requires GenericColorPlan") + if not isinstance(template_catalog, RecurrenceTemplateCatalog): + raise TypeError("recurrence projection requires RecurrenceTemplateCatalog") + if type(contracted_physical_color_union) is not bool: + raise TypeError("contracted physical-color union opt-in must be boolean") + if layout not in { + "topology-replay", + "all-flow-union", + "contracted-color-union", + }: + raise RecurrenceProjectionError( + f"unsupported recurrence flow layout {layout!r}" + ) + if process.color_accuracy != color_plan.color_accuracy: + raise RecurrenceProjectionError( + "recurrence process and color-plan accuracies disagree" + ) + if contracted_physical_color_union and ( + process.color_accuracy not in {"nlc", "full"} + or layout != "all-flow-union" + or color_plan.trace_reflections_folded + ): + raise RecurrenceProjectionError( + "contracted physical-color union requires complete NLC/full color, " + "an all-flow-union layout, and unfolded physical sectors" + ) + if process.color_accuracy == "lc" and layout == "contracted-color-union": + raise RecurrenceProjectionError( + "LC recurrence requires topology-replay or all-flow-union" + ) + if ( + process.color_accuracy in {"nlc", "full"} + and layout != "contracted-color-union" + and not contracted_physical_color_union + ): + raise RecurrenceProjectionError( + "NLC/full recurrence requires the contracted-color-union strategy" + ) + if process.color_accuracy not in {"lc", "nlc", "full"}: + raise RecurrenceProjectionError( + f"unsupported recurrence color accuracy {process.color_accuracy!r}" + ) + if color_plan.process != process: + raise RecurrenceProjectionError( + "recurrence color plan does not belong to the supplied process IR" + ) + if color_plan.truncated: + raise RecurrenceProjectionError( + "recurrence projection rejects truncated color plans" + ) + if not color_plan.sectors: + raise RecurrenceProjectionError( + "recurrence projection requires physical LC sectors" + ) + sector_ids = tuple(sorted(int(sector.id) for sector in color_plan.sectors)) + if sector_ids != tuple(range(len(sector_ids))): + raise RecurrenceProjectionError( + "recurrence projection requires dense physical LC sector IDs" + ) + + +def _physical_color_owner_plan(color_plan: GenericColorPlan) -> GenericColorPlan: + """Return dense canonical tensor owners for one complete physical plan.""" + + owner_by_key: dict[tuple[object, ...], int] = {} + owners = [] + for sector in sorted(color_plan.sectors, key=lambda item: int(item.id)): + sector_id = int(sector.id) + if sector.kind == "open-lines": + key: tuple[object, ...] = ( + "open-lines", + _canonical_open_string_product_key( + ( + line.fundamental_label, + line.adjoint_labels, + line.antifundamental_label, + line.singlet_labels, + ) + for line in sector.open_color_lines + ), + ) + else: + key = (sector.kind, sector_id) + if owner_by_key.setdefault(key, sector_id) != sector_id: + continue + owners.append(replace(sector, id=len(owners))) + if not owners: + raise RecurrenceProjectionError( + "contracted physical-color union has no canonical owner sectors" + ) + return replace(color_plan, sectors=tuple(owners)) + + +def _project_template_references( + catalog: RecurrenceTemplateCatalog, +) -> tuple[ + dict[tuple[str, str], int], + tuple[RecurrenceSemanticTemplateReferenceV1, ...], +]: + binding_by_template: dict[str, EvaluatorBindingV1] = {} + for binding in catalog.evaluator_bindings: + for semantic_id in binding.semantic_template_ids: + previous = binding_by_template.setdefault(semantic_id, binding) + if previous != binding: + raise RecurrenceProjectionError( + f"semantic template {semantic_id!r} has multiple callables" + ) + + numeric_ids: dict[tuple[str, str], int] = {} + references: list[RecurrenceSemanticTemplateReferenceV1] = [] + for kind, attribute in _TEMPLATE_SECTIONS: + records = tuple( + sorted(getattr(catalog, attribute), key=lambda item: item.template_id) + ) + for numeric_id, record in enumerate(records): + key = (kind, record.template_id) + numeric_ids[key] = numeric_id + binding = binding_by_template.get(record.template_id) + prepared_kernel_id = None if binding is None else binding.prepared_kernel_id + references.append( + RecurrenceSemanticTemplateReferenceV1( + kind=kind, + template_id=numeric_id, + semantic_digest=record.semantic_digest, + prepared_kernel_id=prepared_kernel_id, + ) + ) + return numeric_ids, tuple(references) + + +def _project_external_legs( + process: CanonicalProcessIR, + catalog: RecurrenceTemplateCatalog, + template_ids: Mapping[tuple[str, str], int], + selected_helicities: tuple[tuple[int, int], ...] | None, + support_mask: int, +) -> tuple[ + tuple[RecurrenceExternalLegV1, ...], + tuple[RecurrenceSelectedSourceCoverageV1, ...] | None, +]: + selected = dict(selected_helicities or ()) + known_labels = {int(leg.label) for leg in process.legs} + unknown_labels = tuple(sorted(set(selected) - known_labels)) + if unknown_labels: + raise RecurrenceProjectionError( + f"generation slice references unknown source labels {unknown_labels!r}" + ) + + states_by_id = {record.template_id: record for record in catalog.current_states} + states_by_execution_key: dict[tuple[object, ...], CurrentStateTemplateV1] = {} + for record in catalog.current_states: + key = _current_state_execution_key(record) + previous = states_by_execution_key.setdefault(key, record) + if previous.template_id != record.template_id: + raise RecurrenceProjectionError( + "recurrence current-state catalog contains duplicate execution " + f"contracts {previous.template_id!r} and {record.template_id!r}" + ) + source_rows: dict[ + int, + list[tuple[SourceTemplateV1, CurrentStateTemplateV1]], + ] = {} + for source in catalog.sources: + try: + current = states_by_id[source.state_template_id] + except KeyError as exc: + raise RecurrenceProjectionError( + f"source template {source.template_id!r} references a missing state" + ) from exc + source_rows.setdefault(int(current.particle_id), []).append((source, current)) + + external: list[RecurrenceExternalLegV1] = [] + selected_coverage: list[RecurrenceSelectedSourceCoverageV1] = [] + for source_slot, leg in enumerate(process.legs): + is_fermionic = _is_fermionic_process_leg(leg) + if leg.pdg is None or leg.outgoing_pdg is None: + raise RecurrenceProjectionError( + f"external leg {leg.label} has no concrete physical/outgoing PDG" + ) + candidates = source_rows.get(int(leg.outgoing_pdg), ()) + projected: list[ + tuple[ + SourceTemplateV1, + CurrentStateTemplateV1, + int, + int, + int, + int, + ExactComplexRationalV1, + ] + ] = [] + for source, current in candidates: + if source.wavefunction_family != leg.wavefunction_family: + continue + helicity = int(source.helicity) + chirality = int(current.chirality) + spin_state = int(source.spin_state) + crossing = _crossing_contract(source) + momentum_sign = 1 + phase = ExactComplexRationalV1(1) + if leg.is_initial: + helicity *= crossing[0] + chirality *= crossing[1] + spin_state *= crossing[2] + momentum_sign = crossing[3] + phase = crossing[4] + execution_key = _current_state_execution_key( + current, + chirality=chirality, + ) + try: + execution_current = states_by_execution_key[execution_key] + except KeyError as exc: + raise RecurrenceProjectionError( + "recurrence template catalog has no current-state contract for " + f"crossed source {source.template_id!r}: particle=" + f"{current.particle_id}, chirality={chirality}" + ) from exc + projected.append( + ( + source, + execution_current, + helicity, + chirality, + spin_state, + momentum_sign, + phase, + ) + ) + projected.sort( + key=lambda item: ( + item[2], + item[3], + item[4], + item[1].template_id, + item[0].template_id, + ) + ) + if not projected: + raise RecurrenceProjectionError( + "recurrence template catalog has no supported source semantics for " + f"external leg {leg.label} (outgoing PDG {leg.outgoing_pdg})" + ) + + source_states = tuple( + RecurrenceSourceStateV1( + state_index=index, + public_helicity=helicity, + chirality=chirality, + spin_state=spin_state, + current_state_template_id=_template_id( + template_ids, + "current-state", + current.template_id, + ), + source_template_id=_template_id( + template_ids, "source", source.template_id + ), + momentum_sign=momentum_sign, + crossing_phase=phase, + ) + for index, ( + source, + current, + helicity, + chirality, + spin_state, + momentum_sign, + phase, + ) in enumerate(projected) + ) + external.append( + RecurrenceExternalLegV1( + source_slot=source_slot, + public_label=int(leg.label), + physical_pdg=int(leg.pdg), + outgoing_pdg=int(leg.outgoing_pdg), + is_initial=bool(leg.is_initial), + is_fermionic=is_fermionic, + source_states=source_states, + momentum_mask=1 << source_slot, + support_mask=support_mask, + ) + ) + if selected_helicities is not None: + requested = selected.get(int(leg.label)) + retained = tuple( + index + for index, ( + _source, + _current, + helicity, + _chirality, + _spin_state, + _momentum_sign, + _phase, + ) in enumerate(projected) + if requested is None or helicity == requested + ) + if not retained: + available = tuple(sorted({item[2] for item in projected})) + raise RecurrenceProjectionError( + f"selected helicity {requested} is unavailable for source " + f"label {leg.label}; available={available!r}" + ) + selected_coverage.append( + RecurrenceSelectedSourceCoverageV1(source_slot, retained) + ) + + return ( + tuple(external), + None if selected_helicities is None else tuple(selected_coverage), + ) + + +def _current_state_execution_key( + state: CurrentStateTemplateV1, + *, + chirality: int | None = None, +) -> tuple[object, ...]: + """Identity of a current state after applying external crossing. + + Crossing may change the chiral representation used by subsequent + recurrence transitions, but it does not change the model-owned particle, + basis, colour, tensor, or parameter contracts. + """ + + return ( + state.particle_id, + state.anti_particle_id, + state.species_id, + state.orientation, + state.statistics, + state.color_representation, + state.basis, + state.tensor_ordering, + state.dimension, + state.chirality if chirality is None else chirality, + state.lc_color_shape_kind, + state.auxiliary_kind, + state.mass_parameter_id, + state.width_parameter_id, + ) + + +def _is_fermionic_process_leg(leg: ProcessLegIR) -> bool: + concrete_statistics = {"auxiliary", "boson", "fermion", "ghost"} + concrete_families = { + "auxiliary", + "fermion", + "ghost", + "scalar", + "spin2", + "vector", + } + if ( + leg.statistics not in concrete_statistics + or leg.wavefunction_family not in concrete_families + ): + raise RecurrenceProjectionError( + "recurrence closure-anchor classification requires concrete external " + f"statistics and spin metadata for leg {leg.label}" + ) + statistics_is_fermionic = leg.statistics == "fermion" + family_is_fermionic = leg.wavefunction_family == "fermion" + if statistics_is_fermionic != family_is_fermionic: + raise RecurrenceProjectionError( + "recurrence closure-anchor classification found inconsistent " + f"statistics and spin metadata for leg {leg.label}" + ) + return statistics_is_fermionic + + +def _crossing_contract( + source: SourceTemplateV1, +) -> tuple[int, int, int, int, ExactComplexRationalV1]: + try: + payload = json.loads(source.crossing) + helicity = payload["helicity_factor"] + chirality = payload["chirality_factor"] + spin_state = payload["spin_state_factor"] + momentum_transform = payload["momentum_transform"] + phase = payload["phase"] + except (json.JSONDecodeError, KeyError, TypeError) as exc: + raise RecurrenceProjectionError( + f"source template {source.template_id!r} has malformed crossing semantics" + ) from exc + if ( + helicity not in {-1, 1} + or chirality not in {-1, 1} + or spin_state not in {-1, 1} + or momentum_transform not in {"identity", "negate-four-momentum"} + or not isinstance(phase, Mapping) + ): + raise RecurrenceProjectionError( + f"source template {source.template_id!r} has unsupported crossing factors" + ) + try: + exact_phase = ExactComplexRationalV1( + int(phase["real_numerator"]), + int(phase["real_denominator"]), + int(phase["imag_numerator"]), + int(phase["imag_denominator"]), + ) + except (KeyError, TypeError, ValueError) as exc: + raise RecurrenceProjectionError( + f"source template {source.template_id!r} has malformed crossing phase" + ) from exc + momentum_sign = -1 if momentum_transform == "negate-four-momentum" else 1 + return int(helicity), int(chirality), int(spin_state), momentum_sign, exact_phase + + +def _project_physical_sectors( + color_plan: GenericColorPlan, + process: CanonicalProcessIR, + external_legs: tuple[RecurrenceExternalLegV1, ...], + support_mask: int, +) -> tuple[ + tuple[RecurrencePhysicalLCSectorV1, ...], + tuple[RecurrencePublicLCFlowV1, ...], +]: + slot_by_label = { + int(leg.label): source_slot for source_slot, leg in enumerate(process.legs) + } + + def slots(labels: Sequence[int], context: str) -> tuple[int, ...]: + try: + return tuple(slot_by_label[int(label)] for label in labels) + except KeyError as exc: + raise RecurrenceProjectionError( + f"{context} references unknown external label {exc.args[0]}" + ) from exc + + result: list[RecurrencePhysicalLCSectorV1] = [] + public_flows: list[RecurrencePublicLCFlowV1] = [] + identity_permutation = tuple(range(len(process.legs))) + for sector in sorted(color_plan.sectors, key=lambda item: item.id): + words = sector.color_words + if not words: + raise RecurrenceProjectionError( + f"LC sector {sector.id} has no physical color word" + ) + word = sector.word_labels or words[0] + public_id = ( + "flow:" + ",".join(str(int(label)) for label in word) + if word + else "flow:singlet" + ) + open_strings = tuple( + RecurrenceLCOpenStringV1( + fundamental_source_slot=slots( + (line.fundamental_label,), "LC fundamental endpoint" + )[0], + antifundamental_source_slot=slots( + (line.antifundamental_label,), "LC antifundamental endpoint" + )[0], + adjoint_source_slots=slots( + line.adjoint_labels, "LC open-string adjoints" + ), + singlet_source_slots=slots( + line.singlet_labels, "LC open-string singlets" + ), + ) + for line in sector.open_color_lines + ) + trace_source_slots = slots(sector.trace_labels, "LC trace") + singlet_source_slots = slots(sector.singlet_labels, "LC singlets") + word_source_slots = slots(word, "LC color word") + closure_word = sector.canonical_closure_traversal_word(process) + closure_word_source_slots = slots( + closure_word, + "LC closure traversal", + ) + physical_blocks = ( + _ordered_open_line_blocks(tuple(word), sector.open_color_lines) + if sector.kind == "open-lines" + else () + ) + closure_blocks = ( + _ordered_open_line_blocks(closure_word, sector.open_color_lines) + if sector.kind == "open-lines" + else () + ) + if physical_blocks is None or closure_blocks is None: + raise RecurrenceProjectionError( + f"LC sector {sector.id} open-line blocks cannot be reconstructed" + ) + ( + closure_source_slot, + closure_proof_algorithm, + closure_proof_digest, + ) = _closure_anchor_contract( + sector_id=int(sector.id), + sector_kind=sector.kind, + open_string_count=len(open_strings), + word_source_slots=word_source_slots, + closure_word_source_slots=closure_word_source_slots, + physical_block_source_slots=tuple( + slots(block, "LC physical open-line block") for block in physical_blocks + ), + closure_block_source_slots=tuple( + slots(block, "LC closure open-line block") for block in closure_blocks + ), + process_source_label_order=tuple(int(leg.label) for leg in process.legs), + singlet_source_slots=singlet_source_slots, + external_legs=external_legs, + ) + result.append( + RecurrencePhysicalLCSectorV1( + sector_id=int(sector.id), + public_id=public_id, + kind=sector.kind, + closure_source_slot=closure_source_slot, + closure_proof_algorithm=closure_proof_algorithm, + closure_proof_digest=closure_proof_digest, + open_strings=open_strings, + trace_source_slots=trace_source_slots, + singlet_source_slots=singlet_source_slots, + word_source_slots=word_source_slots, + support_mask=support_mask, + ) + ) + public_flows.append( + RecurrencePublicLCFlowV1( + flow_id=len(public_flows), + public_id=public_id, + construction_sector_id=int(sector.id), + word_source_slots=word_source_slots, + source_slot_permutation=identity_permutation, + ) + ) + if not ( + color_plan.trace_reflections_folded + and sector.kind == "single-trace" + and len(word) > 2 + ): + continue + reflected = (word[0], *reversed(word[1:])) + if reflected == tuple(word): + continue + public_flows.append( + RecurrencePublicLCFlowV1( + flow_id=len(public_flows), + public_id="flow:" + ",".join(str(int(label)) for label in reflected), + construction_sector_id=int(sector.id), + word_source_slots=slots(reflected, "reflected LC public color word"), + # Exact trace reflection changes only the ordered colour word. + # External states and momenta remain attached to their public + # labels; the reflection proof supplies the amplitude relation. + source_slot_permutation=identity_permutation, + ) + ) + return tuple(result), tuple(public_flows) + + +def _closure_anchor_contract( + *, + sector_id: int, + sector_kind: str, + open_string_count: int, + word_source_slots: tuple[int, ...], + closure_word_source_slots: tuple[int, ...], + physical_block_source_slots: tuple[tuple[int, ...], ...], + closure_block_source_slots: tuple[tuple[int, ...], ...], + process_source_label_order: tuple[int, ...], + singlet_source_slots: tuple[int, ...], + external_legs: tuple[RecurrenceExternalLegV1, ...], +) -> tuple[int, str, str]: + external_source_count = len(external_legs) + if external_source_count <= 0: + raise RecurrenceProjectionError( + "a physical LC sector requires at least one external source" + ) + if word_source_slots: + if not closure_word_source_slots: + raise RecurrenceProjectionError( + "a coloured LC sector has an empty closure traversal" + ) + if set(closure_word_source_slots) != set(word_source_slots): + raise RecurrenceProjectionError( + "LC closure traversal does not preserve the physical colour word" + ) + closure_source_slot = closure_word_source_slots[-1] + # Newly projected open-line sectors always carry the v3 structural + # proof, including the no-rotation case. The decoder retains v2 for + # already encoded terminal-word contracts. + proof_algorithm = ( + _CLOSURE_ANCHOR_PROOF_ALGORITHM_V3 + if sector_kind == "open-lines" + else _CLOSURE_ANCHOR_PROOF_ALGORITHM_V2 + ) + policy = ( + "terminal-colour-word-endpoint" + if proof_algorithm == _CLOSURE_ANCHOR_PROOF_ALGORITHM_V2 + else "minimum-coloured-source-slot-open-line-block-rotation" + ) + else: + if sector_kind != "singlet" or tuple(sorted(singlet_source_slots)) != tuple( + range(external_source_count) + ): + raise RecurrenceProjectionError( + "an LC sector without a colour word must be an all-singlet sector" + ) + fermionic_source_slots = tuple( + leg.source_slot for leg in external_legs if leg.is_fermionic + ) + if fermionic_source_slots: + policy = "minimum-fermionic-source-slot" + closure_source_slot = min(fermionic_source_slots) + else: + policy = "minimum-source-slot" + closure_source_slot = min(leg.source_slot for leg in external_legs) + proof_algorithm = _CLOSURE_ANCHOR_PROOF_ALGORITHM_V2 + proof_payload: dict[str, object] = { + "algorithm": proof_algorithm, + "closure_source_slot": closure_source_slot, + "component_order_policy": ( + "independent-open-string-block-permutation" + if sector_kind == "open-lines" and open_string_count > 1 + else "exact-ordered-components" + ), + "external_source_count": external_source_count, + "fermionic_source_slots": tuple( + leg.source_slot for leg in external_legs if leg.is_fermionic + ), + "open_string_count": open_string_count, + "policy": policy, + "sector_id": sector_id, + "sector_kind": sector_kind, + "singlet_source_slots": singlet_source_slots, + "word_source_slots": word_source_slots, + } + if proof_algorithm == _CLOSURE_ANCHOR_PROOF_ALGORITHM_V3: + if not physical_block_source_slots or not closure_block_source_slots: + raise RecurrenceProjectionError( + "v3 LC closure anchor requires reconstructed open-line blocks" + ) + distinguished_source_slot = min(word_source_slots) + if distinguished_source_slot not in closure_block_source_slots[0]: + raise RecurrenceProjectionError( + "v3 LC closure traversal does not start with the block containing " + "the minimum coloured source slot" + ) + proof_payload.update( + { + "closure_block_source_slots": closure_block_source_slots, + "closure_traversal_source_slots": closure_word_source_slots, + "distinguished_source_slot": distinguished_source_slot, + "distinguished_source_block": closure_block_source_slots[0], + "physical_block_source_slots": physical_block_source_slots, + "process_source_label_order": process_source_label_order, + "selected_closure_block": closure_block_source_slots[-1], + } + ) + proof_digest = _digest(proof_payload) + return closure_source_slot, proof_algorithm, proof_digest + + +def _project_cyclic_trace_closure_anchors( + sectors: tuple[RecurrencePhysicalLCSectorV1, ...], + replay_partitions: tuple[RecurrenceReplayPartitionV1, ...], + external_legs: tuple[RecurrenceExternalLegV1, ...], + template_catalog: RecurrenceTemplateCatalog, +) -> tuple[RecurrencePhysicalLCSectorV1, ...]: + """Fix one authenticated adjoint trace source as the closure sink. + + This is deliberately an opt-in projection for the symmetric-group colour + lane. It uses only cyclic trace equivalence plus process/template + contracts. Any heterogeneous, open-line, fermionic, auxiliary, or + replay-incompatible channel retains its established v2/v3 anchor. + """ + + # Replay and reflection folding choose their canonical owners independently. + # A replay representative can therefore be the non-canonical member of a + # reflection orbit whose owner belongs to another replay partition. Keep + # the established per-sector anchors until those two proofs are composed. + if replay_partitions: + return sectors + + state_template_id = _cyclic_trace_state_template_id( + external_legs, + template_catalog, + ) + if state_template_id is None: + return sectors + + candidates: dict[int, int] = {} + full_source_domain = tuple(range(len(external_legs))) + for sector in sectors: + if ( + sector.kind != "single-trace" + or sector.open_strings + or sector.singlet_source_slots + or not sector.trace_source_slots + or sector.trace_source_slots != sector.word_source_slots + or tuple(sorted(sector.word_source_slots)) != full_source_domain + ): + continue + candidates[sector.sector_id] = min(sector.word_source_slots) + + # A replayed representative must keep the same distinguished source under + # every exact target mapping. Fall back for the complete partition when + # any target cannot preserve that closure ancestry. + rejected: set[int] = set() + for partition in replay_partitions: + partition_sector_ids = {target.sector_id for target in partition.targets} + representative_anchor = candidates.get(partition.representative_sector_id) + if representative_anchor is None or not partition_sector_ids.issubset( + candidates + ): + rejected.update(partition_sector_ids) + continue + if any( + target.source_slot_permutation[representative_anchor] + != candidates[target.sector_id] + for target in partition.targets + ): + rejected.update(partition_sector_ids) + + result: list[RecurrencePhysicalLCSectorV1] = [] + process_source_label_order = tuple(leg.public_label for leg in external_legs) + fermionic_source_slots = tuple( + leg.source_slot for leg in external_legs if leg.is_fermionic + ) + for sector in sectors: + closure_source_slot = candidates.get(sector.sector_id) + if closure_source_slot is None or sector.sector_id in rejected: + result.append(sector) + continue + proof_payload = { + "algorithm": _CLOSURE_ANCHOR_PROOF_ALGORITHM_V4, + "closure_source_slot": closure_source_slot, + "component_order_policy": "cyclic-single-trace", + "external_source_count": len(external_legs), + "fermionic_source_slots": fermionic_source_slots, + "open_string_count": 0, + "policy": "minimum-source-slot-cyclic-single-trace", + "process_source_label_order": process_source_label_order, + "sector_id": sector.sector_id, + "sector_kind": sector.kind, + "singlet_source_slots": sector.singlet_source_slots, + "state_template_id": state_template_id, + "trace_source_slots": sector.trace_source_slots, + "word_source_slots": sector.word_source_slots, + } + result.append( + replace( + sector, + closure_source_slot=closure_source_slot, + closure_proof_algorithm=_CLOSURE_ANCHOR_PROOF_ALGORITHM_V4, + closure_proof_digest=_digest(proof_payload), + ) + ) + return tuple(result) + + +def _cyclic_trace_state_template_id( + external_legs: tuple[RecurrenceExternalLegV1, ...], + catalog: RecurrenceTemplateCatalog, +) -> int | None: + """Return one process-wide state ID only for the certified adjoint case.""" + + if not external_legs or any( + leg.is_fermionic or not leg.source_states for leg in external_legs + ): + return None + state_ids = { + state.current_state_template_id + for leg in external_legs + for state in leg.source_states + } + if len(state_ids) != 1: + return None + state_template_id = next(iter(state_ids)) + try: + state = catalog.current_states[state_template_id] + except IndexError: + return None + if ( + state.orientation != "self-conjugate" + or state.statistics != "boson" + or state.particle_id != state.anti_particle_id + or state.color_representation != 8 + or state.lc_color_shape_kind != "adjoint-segment" + or state.auxiliary_kind is not None + ): + return None + + bindings = {binding.resolver_key: binding for binding in catalog.evaluator_bindings} + colors = {color.template_id: color for color in catalog.color_contractions} + for closure in catalog.closures: + if ( + closure.input_state_template_ids != (state.template_id, state.template_id) + or closure.result_state_template_id is not None + or closure.coupling_parameter_ids + or closure.coupling_orders + or closure.eligible_quantum_flow_template_ids + or closure.output_factor_source != "none" + or closure.equivalence_class != "direct-contraction" + ): + continue + binding = bindings.get(closure.evaluator_resolver_key) + color = colors.get(closure.color_contraction_template_id) + if ( + binding is not None + and binding.callable_kind == "rusticol-template" + and binding.contract_kind == "closure" + and binding.input_state_template_ids + == (state.template_id, state.template_id) + and binding.output_state_template_id is None + and color is not None + and color.input_representations == (8, 8) + and color.output_representation is None + ): + return state_template_id + return None + + +def _project_replay_partitions( + process: CanonicalProcessIR, + color_plan: GenericColorPlan, + physical_sectors: tuple[RecurrencePhysicalLCSectorV1, ...], + external_legs: tuple[RecurrenceExternalLegV1, ...], + replay: LCColorTopologyReplayPlan | ColorTopologyReplayCertificate | None, + layout: RecurrenceLCFlowLayout, +) -> tuple[RecurrenceReplayPartitionV1, ...]: + if layout not in {"topology-replay", "contracted-color-union"}: + return () + if replay is None: + return () + if layout == "topology-replay" and not isinstance( + replay, LCColorTopologyReplayPlan + ): + raise RecurrenceProjectionError( + "LC topology-replay layout requires an LC replay plan" + ) + if layout == "contracted-color-union" and not isinstance( + replay, ColorTopologyReplayCertificate + ): + raise RecurrenceProjectionError( + "contracted-color-union replay requires a color-generic certificate" + ) + if isinstance(replay, ColorTopologyReplayCertificate) and ( + replay.color_accuracy != color_plan.color_accuracy + ): + raise RecurrenceProjectionError( + "topology replay certificate color accuracy does not match the plan" + ) + physical_ids = tuple(sorted(int(sector.id) for sector in color_plan.sectors)) + if replay.physical_sector_ids != physical_ids: + raise RecurrenceProjectionError( + "topology replay proof does not cover this physical LC plan" + ) + + source_slot_by_label = { + int(leg.label): source_slot for source_slot, leg in enumerate(process.legs) + } + labels = set(source_slot_by_label) + physical_sector_by_id = {sector.sector_id: sector for sector in physical_sectors} + partitions: list[RecurrenceReplayPartitionV1] = [] + identity_materializations: dict[ + int, tuple[str, int, Mapping[str, object] | None] + ] = {} + for partition in replay.partitions: + if partition.proof_algorithm is None or partition.proof_digest is None: + raise RecurrenceProjectionError( + "topology replay partition lacks an exact proof certificate" + ) + representative_sector_id = int(partition.representative_sector_id) + try: + representative_sector = physical_sector_by_id[representative_sector_id] + except KeyError as exc: + raise RecurrenceProjectionError( + "topology replay references a missing representative LC sector " + f"{representative_sector_id}" + ) from exc + representative_anchor = representative_sector.closure_source_slot + projected_targets: list[ + tuple[RecurrenceReplayTargetV1, bool, int, Mapping[str, object]] + ] = [] + for sector_id, raw_mapping, weight, sign in zip( + partition.active_sector_ids, + partition.label_permutations, + partition.weights, + partition.signs, + strict=True, + ): + mapping = {int(left): int(right) for left, right in raw_mapping} + if not set(mapping).issubset(labels) or not set(mapping.values()).issubset( + labels + ): + raise RecurrenceProjectionError( + "topology replay maps an unknown external label" + ) + complete = {label: mapping.get(label, label) for label in labels} + if set(complete.values()) != labels: + raise RecurrenceProjectionError( + "topology replay external mapping is not a bijection" + ) + permutation = tuple( + source_slot_by_label[complete[int(leg.label)]] for leg in process.legs + ) + for representative_slot, target_slot in enumerate(permutation): + if ( + process.legs[representative_slot].side + != process.legs[target_slot].side + ): + raise RecurrenceProjectionError( + "topology replay maps an external source across the " + "initial/final crossing boundary" + ) + sector = color_plan.sector(int(sector_id)) + if sector is None: + raise RecurrenceProjectionError( + f"topology replay references missing LC sector {sector_id}" + ) + expected_weight = ( + 2.0 + if isinstance(replay, LCColorTopologyReplayPlan) + and color_plan.trace_reflections_folded + and sector.kind == "single-trace" + and len(sector.trace_labels) > 2 + else 1.0 + ) + if not math.isclose( + float(weight), + expected_weight, + rel_tol=0.0, + abs_tol=0.0, + ): + raise RecurrenceProjectionError( + "topology replay carries an unsupported non-public-flow " + f"multiplicity {weight!r} for sector {sector_id}" + ) + target_sector = physical_sector_by_id[int(sector_id)] + target = RecurrenceReplayTargetV1( + sector_id=int(sector_id), + external_permutation=permutation, + source_slot_permutation=permutation, + # Rust derives and certifies source-state and closure + # bijections from this exact slot permutation. + amplitude_phase=ExactComplexRationalV1(1), + fermion_sign=int(sign), + ) + target_anchor = target_sector.closure_source_slot + anchor_compatible = permutation[representative_anchor] == target_anchor + source_replay = { + "fermion_sign": int(sign), + "partition_proof_algorithm": partition.proof_algorithm, + "partition_proof_digest": partition.proof_digest, + "representative_sector_id": representative_sector_id, + "source_slot_permutation": permutation, + } + projected_targets.append( + (target, anchor_compatible, int(expected_weight), source_replay) + ) + + if not isinstance(replay, LCColorTopologyReplayPlan) and any( + not anchor_compatible for _, anchor_compatible, _, _ in projected_targets + ): + raise RecurrenceProjectionError( + "topology replay does not map the representative closure " + "anchor onto the target sector anchor" + ) + representative_is_compatible = any( + target.sector_id == representative_sector_id and anchor_compatible + for target, anchor_compatible, _, _ in projected_targets + ) + retained_targets = ( + tuple( + target + for target, anchor_compatible, _, _ in projected_targets + if anchor_compatible + ) + if representative_is_compatible + else () + ) + if retained_targets: + partitions.append( + RecurrenceReplayPartitionV1( + representative_sector_id=representative_sector_id, + materialized_sector_id=int(partition.materialized_sector_id), + proof_algorithm=partition.proof_algorithm, + proof_digest=partition.proof_digest, + targets=retained_targets, + ) + ) + for target, anchor_compatible, multiplicity, source_replay in projected_targets: + if representative_is_compatible and anchor_compatible: + continue + target_sector = physical_sector_by_id[target.sector_id] + mapped_word = tuple( + target.source_slot_permutation[source_slot] + for source_slot in representative_sector.word_source_slots + ) + if mapped_word != target_sector.word_source_slots: + raise RecurrenceProjectionError( + "topology replay does not map the representative LC word " + "onto the target sector word" + ) + for representative_slot, target_slot in enumerate( + target.source_slot_permutation + ): + representative_leg = external_legs[representative_slot] + target_leg = external_legs[target_slot] + representative_contracts = { + state.source_template_id: ( + state.momentum_sign, + state.crossing_phase.canonical_key, + ) + for state in representative_leg.source_states + } + target_contracts = { + state.source_template_id: ( + state.momentum_sign, + state.crossing_phase.canonical_key, + ) + for state in target_leg.source_states + } + if representative_contracts != target_contracts: + raise RecurrenceProjectionError( + "topology replay maps an external source onto one with " + "a different crossing contract" + ) + identity_materializations[target.sector_id] = ( + ( + "closure-anchor-incompatible" + if representative_is_compatible + else "representative-closure-anchor-incompatible" + ), + multiplicity, + source_replay, + ) + + if isinstance(replay, LCColorTopologyReplayPlan): + for sector_id in replay.residual_sector_ids: + sector = color_plan.sector(int(sector_id)) + if sector is None: + raise RecurrenceProjectionError( + f"topology replay references missing residual LC sector {sector_id}" + ) + multiplicity = ( + 2 + if color_plan.trace_reflections_folded + and sector.kind == "single-trace" + and len(sector.trace_labels) > 2 + else 1 + ) + identity_materializations[int(sector_id)] = ( + "replay-plan-residual", + multiplicity, + None, + ) + + identity_permutation = tuple(range(len(process.legs))) + for sector_id in sorted(identity_materializations): + reason, multiplicity, source_replay = identity_materializations[sector_id] + sector = physical_sector_by_id[sector_id] + proof_payload = { + "algorithm": _IDENTITY_REPLAY_PROOF_ALGORITHM, + "closure_proof_algorithm": sector.closure_proof_algorithm, + "closure_proof_digest": sector.closure_proof_digest, + "closure_source_slot": sector.closure_source_slot, + "identity_source_slot_permutation": identity_permutation, + "policy": "materialize-physical-sector-with-identity-replay", + "process_source_label_order": tuple(int(leg.label) for leg in process.legs), + "public_flow_multiplicity": multiplicity, + "reason": reason, + "sector_id": sector_id, + "source_replay": source_replay, + "word_source_slots": sector.word_source_slots, + } + partitions.append( + RecurrenceReplayPartitionV1( + representative_sector_id=sector_id, + materialized_sector_id=sector_id, + proof_algorithm=_IDENTITY_REPLAY_PROOF_ALGORITHM, + proof_digest=_digest(proof_payload), + targets=( + RecurrenceReplayTargetV1( + sector_id=sector_id, + external_permutation=identity_permutation, + source_slot_permutation=identity_permutation, + amplitude_phase=ExactComplexRationalV1(1), + fermion_sign=1, + ), + ), + ) + ) + return tuple(partitions) + + +def _project_coupling_limits( + limits: Mapping[str, int] | None, +) -> tuple[RecurrenceCouplingLimitV1, ...]: + normalized = tuple( + sorted( + (str(raw_name).upper(), raw_maximum) + for raw_name, raw_maximum in (limits or {}).items() + ) + ) + names = tuple(name for name, _ in normalized) + if len(names) != len(set(names)): + raise RecurrenceProjectionError( + "coupling-order names must be unique after canonicalization" + ) + result: list[RecurrenceCouplingLimitV1] = [] + for name, raw_maximum in normalized: + if not name: + raise RecurrenceProjectionError("coupling-order names must be nonempty") + if isinstance(raw_maximum, bool) or not isinstance(raw_maximum, int): + raise RecurrenceProjectionError( + f"coupling-order limit {name!r} must be an integer" + ) + if raw_maximum < 0: + raise RecurrenceProjectionError( + f"coupling-order limit {name!r} must be nonnegative" + ) + result.append(RecurrenceCouplingLimitV1(name, 0, raw_maximum)) + return tuple(result) + + +def _project_parameters( + catalog: RecurrenceTemplateCatalog, + template_ids: Mapping[tuple[str, str], int], +) -> tuple[RecurrenceParameterProjectionV1, ...]: + runtime_parameters = tuple( + sorted( + ( + parameter + for parameter in catalog.parameters + if parameter.mutable or parameter.parameter_kind == "derived" + ), + key=lambda item: (item.name, item.template_id), + ) + ) + names = tuple(parameter.name for parameter in runtime_parameters) + if len(names) != len(set(names)): + raise RecurrenceProjectionError( + "recurrence catalog exposes duplicate runtime parameter names" + ) + result: list[RecurrenceParameterProjectionV1] = [] + for parameter in runtime_parameters: + components = (0,) if parameter.value_type == "real" else (0, 1) + for component in components: + result.append( + RecurrenceParameterProjectionV1( + runtime_slot=len(result), + runtime_name=parameter.name, + parameter_template_id=_template_id( + template_ids, "parameter", parameter.template_id + ), + prepared_parameter_id=parameter.prepared_parameter_id, + component=component, + ) + ) + return tuple(result) + + +def _template_id( + template_ids: Mapping[tuple[str, str], int], + kind: str, + semantic_id: str, +) -> int: + try: + return template_ids[(kind, semantic_id)] + except KeyError as exc: + raise RecurrenceProjectionError( + f"missing {kind} semantic template {semantic_id!r}" + ) from exc + + +def _digest(payload: object) -> str: + try: + encoded = json.dumps( + payload, + allow_nan=False, + ensure_ascii=True, + separators=(",", ":"), + sort_keys=True, + ).encode("ascii") + except (TypeError, ValueError) as exc: + raise RecurrenceProjectionError( + "recurrence projection input is not canonical JSON" + ) from exc + return hashlib.sha256(encoded).hexdigest() + + +__all__ = [ + "RecurrenceGenerationSliceV1", + "RecurrenceProjectionError", + "project_recurrence_process_v1", +] diff --git a/pyamplicol/src/pyamplicol/generation/recurrence_schedule_sharing.py b/pyamplicol/src/pyamplicol/generation/recurrence_schedule_sharing.py new file mode 100644 index 0000000000..5ea6aed790 --- /dev/null +++ b/pyamplicol/src/pyamplicol/generation/recurrence_schedule_sharing.py @@ -0,0 +1,2504 @@ +# SPDX-License-Identifier: 0BSD +"""Content-addressed recurrence schedules and compact process bindings.""" + +from __future__ import annotations + +import hashlib +import itertools +import json +import math +import re +import struct +from collections.abc import Callable, Mapping, Sequence +from concurrent.futures import Future +from dataclasses import dataclass, fields, is_dataclass +from pathlib import Path +from threading import Lock +from typing import Generic, Protocol, TypeVar + +from .recurrence_columnar import ( + RecurrenceBuilderLogicalInputV1, + RecurrenceExternalLegV1, + RecurrencePhysicalLCSectorV1, + RecurrencePublicLCFlowV1, + RecurrenceSourceStateV1, +) + +RECURRENCE_SCHEDULE_SHARING_KIND = "pyamplicol-recurrence-schedule-sharing" +RECURRENCE_SCHEDULE_SHARING_SCHEMA_VERSION = 3 +RECURRENCE_SCHEDULE_INDEX_PATH = "recurrence/schedule-index.json" +RECURRENCE_PROCESS_BINDING_ABI = "pyamplicol-recurrence-process-binding-v4" +RECURRENCE_PROCESS_BINDING_MAGIC = b"PACRDBN4" +_PROCESS_BINDING_VERSION = 4 +_PROCESS_BINDING_FIXED_SIZE = 344 +_MAX_SOURCE_BIJECTIONS = 4096 + +_DIRECT_ROLE = { + "source": 0, + "contribution": 1, + "finalization": 2, + "closure": 3, +} +_DIRECT_BACKEND = {"jit": 0, "cpp": 1, "asm": 2} +_DIRECT_BINDING_SOURCE = 0 +_DIRECT_BINDING_INTRINSIC = 1 +_DIRECT_BINDING_JIT = 2 +_DIRECT_BINDING_NATIVE = 3 +_DIRECT_FLAG_USES_EXACT_FACTOR = 1 << 0 +_MISSING_U32 = (1 << 32) - 1 +_PROCESS_VARIABLE_SEMANTIC_ROLES = frozenset( + { + "process", + "color-plan", + "fermion-pairing-semantic", + "fermion-pairing-topology", + "closure-reconstruction", + "helicity-support:pure-massless-adjoint-tree-v1", + "helicity-equivalence:global-flip-v1", + } +) + +_SHA256 = re.compile(r"^[0-9a-f]{64}$") +_T = TypeVar("_T") + + +class RecurrenceScheduleSharingError(ValueError): + """Raised when recurrence schedules cannot be shared exactly.""" + + +@dataclass(frozen=True, slots=True) +class RecurrenceProcessExecutorPack: + """Process-owned sparse Direct executor records for one fixed schedule.""" + + compiled_model_digest: str + recurrence_template_catalog_digest: str + prepared_kernel_pack_digest: str + direct_template_catalog_digest: str + runtime_layout_digest: str + backend: str + target_triple: str + portable: bool + cpu_features: tuple[str, ...] + catalog_executor_count: int + executor_ids: tuple[int, ...] + descriptor_payloads: tuple[bytes, ...] + + def __post_init__(self) -> None: + for value, label in ( + (self.compiled_model_digest, "compiled-model digest"), + ( + self.recurrence_template_catalog_digest, + "recurrence-template catalog digest", + ), + (self.prepared_kernel_pack_digest, "prepared-kernel pack digest"), + (self.direct_template_catalog_digest, "direct-template catalog digest"), + (self.runtime_layout_digest, "runtime-layout digest"), + ): + _require_sha256(value, label) + if self.backend not in _DIRECT_BACKEND: + raise RecurrenceScheduleSharingError( + f"unsupported process executor backend {self.backend!r}" + ) + _nonempty_text(self.target_triple, "process executor target triple") + if type(self.portable) is not bool: + raise RecurrenceScheduleSharingError( + "process executor portability must be boolean" + ) + if self.cpu_features != tuple(sorted(set(self.cpu_features))) or any( + not isinstance(feature, str) or not feature for feature in self.cpu_features + ): + raise RecurrenceScheduleSharingError( + "process executor CPU features must be sorted and unique" + ) + count = _nonnegative_integer( + self.catalog_executor_count, "direct-executor catalog count" + ) + if count > _MISSING_U32: + raise RecurrenceScheduleSharingError( + "direct-executor catalog count exceeds the u32 wire domain" + ) + if self.executor_ids != tuple(sorted(set(self.executor_ids))): + raise RecurrenceScheduleSharingError( + "process executor IDs must be sorted and unique" + ) + if len(self.executor_ids) > _MISSING_U32 or any( + isinstance(executor_id, bool) + or not isinstance(executor_id, int) + or executor_id < 0 + or executor_id >= count + for executor_id in self.executor_ids + ): + raise RecurrenceScheduleSharingError( + "process executor ID is outside the complete catalog domain" + ) + if len(self.executor_ids) != len(self.descriptor_payloads) or any( + not isinstance(payload, bytes) or len(payload) < 16 + for payload in self.descriptor_payloads + ): + raise RecurrenceScheduleSharingError( + "process executor descriptors do not match their selected IDs" + ) + for expected_id, payload in zip( + self.executor_ids, self.descriptor_payloads, strict=True + ): + record_size, executor_id = struct.unpack_from(" None: + _permutation(self.source_slots, "source-slot remap") + _signs(self.source_momentum_signs, len(self.source_slots), "momentum signs") + _signs(self.source_helicity_signs, len(self.source_slots), "helicity signs") + _ragged_permutations( + self.source_state_offsets, + self.source_state_indices, + len(self.source_slots), + "source-state remap", + ) + _permutation(self.public_flow_ids, "public-flow remap") + _permutation(self.physical_sector_ids, "physical-sector remap") + for count, label in ( + (self.state_template_count, "state-template count"), + (self.source_template_count, "source-template count"), + (self.direct_executor_count, "direct-executor count"), + (self.parameter_slot_count, "parameter-slot count"), + ): + _nonnegative_integer(count, label) + for changes, count, label in ( + ( + self.state_template_changes, + self.state_template_count, + "state-template remap", + ), + ( + self.source_template_changes, + self.source_template_count, + "source-template remap", + ), + ( + self.direct_executor_changes, + self.direct_executor_count, + "direct-executor remap", + ), + ( + self.parameter_slot_changes, + self.parameter_slot_count, + "parameter-slot remap", + ), + ): + _sparse_permutation(changes, count, label) + if self.bijection_digest: + _require_sha256(self.bijection_digest, "process-bijection digest") + + @classmethod + def identity( + cls, + logical: RecurrenceBuilderLogicalInputV1, + *, + direct_executor_count: int, + parameter_slot_count: int, + ) -> RecurrenceProcessRemap: + state_count = _template_count(logical, "current-state") + source_count = _template_count(logical, "source") + source_state_offsets = [0] + source_state_indices: list[int] = [] + for leg in logical.external_legs: + source_state_indices.extend(range(len(leg.source_states))) + source_state_offsets.append(len(source_state_indices)) + result = cls( + source_slots=tuple(range(len(logical.external_legs))), + source_momentum_signs=(1,) * len(logical.external_legs), + source_helicity_signs=(1,) * len(logical.external_legs), + source_state_offsets=tuple(source_state_offsets), + source_state_indices=tuple(source_state_indices), + public_flow_ids=tuple(range(len(logical.public_flows))), + physical_sector_ids=tuple(range(_physical_sector_domain_count(logical))), + state_template_count=state_count, + source_template_count=source_count, + direct_executor_count=_nonnegative_integer( + direct_executor_count, "direct-executor count" + ), + parameter_slot_count=_nonnegative_integer( + parameter_slot_count, "parameter-slot count" + ), + ) + return result.with_digest( + _canonical_digest( + { + "contract": "pyamplicol-recurrence-process-bijection-v1", + "process": logical.process_id, + "remap": result._digest_payload(), + "relation": "identity", + } + ) + ) + + def with_digest(self, digest: str) -> RecurrenceProcessRemap: + return RecurrenceProcessRemap( + source_slots=self.source_slots, + source_momentum_signs=self.source_momentum_signs, + source_helicity_signs=self.source_helicity_signs, + source_state_offsets=self.source_state_offsets, + source_state_indices=self.source_state_indices, + public_flow_ids=self.public_flow_ids, + physical_sector_ids=self.physical_sector_ids, + state_template_count=self.state_template_count, + source_template_count=self.source_template_count, + direct_executor_count=self.direct_executor_count, + parameter_slot_count=self.parameter_slot_count, + state_template_changes=self.state_template_changes, + source_template_changes=self.source_template_changes, + direct_executor_changes=self.direct_executor_changes, + parameter_slot_changes=self.parameter_slot_changes, + bijection_digest=_require_sha256(digest, "process-bijection digest"), + ) + + def remap_resolved_helicities( + self, + rows: tuple[tuple[int, ...], ...], + ) -> tuple[tuple[int, ...], ...]: + result: list[tuple[int, ...]] = [] + for row in rows: + if len(row) != len(self.source_slots): + raise RecurrenceScheduleSharingError( + "resolved helicity width disagrees with process binding" + ) + target = [0] * len(row) + for root_slot, target_slot in enumerate(self.source_slots): + target[target_slot] = ( + row[root_slot] * self.source_helicity_signs[root_slot] + ) + result.append(tuple(target)) + if len(set(result)) != len(result): + raise RecurrenceScheduleSharingError( + "process binding collapses distinct resolved helicities" + ) + return tuple(result) + + def to_mapping(self) -> dict[str, object]: + return { + "bijection_digest": self.bijection_digest, + "source_slots": list(self.source_slots), + "source_momentum_signs": list(self.source_momentum_signs), + "source_helicity_signs": list(self.source_helicity_signs), + "source_state_offsets": list(self.source_state_offsets), + "source_state_indices": list(self.source_state_indices), + "public_flow_ids": list(self.public_flow_ids), + "physical_sector_ids": list(self.physical_sector_ids), + "state_templates": _sparse_mapping( + self.state_template_count, self.state_template_changes + ), + "source_templates": _sparse_mapping( + self.source_template_count, self.source_template_changes + ), + "direct_executors": _sparse_mapping( + self.direct_executor_count, self.direct_executor_changes + ), + "parameter_slots": _sparse_mapping( + self.parameter_slot_count, self.parameter_slot_changes + ), + } + + def _digest_payload(self) -> dict[str, object]: + payload = self.to_mapping() + payload["bijection_digest"] = None + return payload + + +@dataclass(frozen=True, slots=True) +class RecurrenceSharedLoweringResult(Generic[_T]): + output: _T + schedule_digest: str + native_schedule_semantic_digest: str + remap: RecurrenceProcessRemap + + +@dataclass(slots=True) +class _PendingLowering(Generic[_T]): + logical: RecurrenceBuilderLogicalInputV1 + direct_executor_count: int + parameter_slot_count: int + schedule_digest: str + sharing_domain_digest: str + native_schedule_semantic_digest: str + future: Future[_T] + + +class RecurrenceScheduleLoweringCache(Generic[_T]): + """Thread-safe single-flight cache keyed before native lowering.""" + + def __init__(self) -> None: + self._lock = Lock() + self._pending: dict[str, Future[_T]] = {} + self._processes: list[_PendingLowering[_T]] = [] + + def lower_once(self, digest: str, lower: Callable[[], _T]) -> _T: + key = _require_sha256(digest, "pre-lowering schedule digest") + with self._lock: + pending = self._pending.get(key) + if pending is None: + pending = Future() + self._pending[key] = pending + owner = True + else: + owner = False + if not owner: + return pending.result() + try: + result = lower() + except BaseException as exc: + pending.set_exception(exc) + try: + pending.exception() + finally: + raise + pending.set_result(result) + return result + + def lower_process( + self, + logical: RecurrenceBuilderLogicalInputV1, + *, + schedule_digest: str, + sharing_domain_digest: str, + native_schedule_semantic_digest: str, + direct_executor_count: int, + parameter_slot_count: int, + lower: Callable[[], _T], + ) -> RecurrenceSharedLoweringResult[_T]: + """Lower once across exact process-isomorphic recurrence schedules.""" + + digest = _require_sha256(schedule_digest, "pre-lowering schedule digest") + sharing_domain = _require_sha256( + sharing_domain_digest, + "pre-lowering schedule-sharing domain digest", + ) + native_semantics = _require_sha256( + native_schedule_semantic_digest, + "native schedule semantic digest", + ) + executor_count = _nonnegative_integer( + direct_executor_count, "direct-executor count" + ) + parameter_count = _nonnegative_integer( + parameter_slot_count, "parameter-slot count" + ) + with self._lock: + owner: _PendingLowering[_T] | None = None + remap: RecurrenceProcessRemap | None = None + for candidate in self._processes: + if ( + candidate.sharing_domain_digest != sharing_domain + or candidate.direct_executor_count != executor_count + or candidate.parameter_slot_count != parameter_count + ): + continue + remap = exact_recurrence_process_bijection( + candidate.logical, + logical, + direct_executor_count=executor_count, + parameter_slot_count=parameter_count, + ) + if remap is not None: + owner = candidate + break + if owner is None: + future: Future[_T] = Future() + owner = _PendingLowering( + logical=logical, + direct_executor_count=executor_count, + parameter_slot_count=parameter_count, + schedule_digest=digest, + sharing_domain_digest=sharing_domain, + native_schedule_semantic_digest=native_semantics, + future=future, + ) + self._processes.append(owner) + remap = RecurrenceProcessRemap.identity( + logical, + direct_executor_count=executor_count, + parameter_slot_count=parameter_count, + ) + owns_lowering = True + else: + owns_lowering = False + assert remap is not None + if owns_lowering: + try: + output = lower() + except BaseException as exc: + owner.future.set_exception(exc) + try: + owner.future.exception() + finally: + raise + owner.future.set_result(output) + else: + output = owner.future.result() + return RecurrenceSharedLoweringResult( + output=output, + schedule_digest=owner.schedule_digest, + native_schedule_semantic_digest=(owner.native_schedule_semantic_digest), + remap=remap, + ) + + +class RecurrenceScheduleProcess(Protocol): + process_id: str + recurrence_schedule_path: Path + recurrence_schedule_digest: str + recurrence_native_schedule_semantic_digest: str + recurrence_schedule_size_bytes: int + recurrence_schedule_sha256: str + recurrence_schedule_member_count: int + recurrence_schedule_unpacked_size_bytes: int + recurrence_schedule_index_sha256: str + builder_input_sha256: str + process_digest: str + process_support_mask: int + recurrence_process_remap: RecurrenceProcessRemap + recurrence_process_executor_pack: RecurrenceProcessExecutorPack + + +def _recurrence_schedule_identity_payload( + logical: RecurrenceBuilderLogicalInputV1, + *, + prepared_kernel_pack_digest: str, + direct_template_catalog_digest: str, + point_tile_size: int, + workspace_mib: int, +) -> dict[str, object]: + domain = _recurrence_schedule_domain_payload( + prepared_kernel_pack_digest=prepared_kernel_pack_digest, + direct_template_catalog_digest=direct_template_catalog_digest, + point_tile_size=point_tile_size, + workspace_mib=workspace_mib, + ) + return { + "contract": "pyamplicol-recurrence-prelower-schedule-identity-v1", + "logical": _schedule_plain(logical), + **domain, + } + + +def _recurrence_schedule_domain_payload( + *, + prepared_kernel_pack_digest: str, + direct_template_catalog_digest: str, + point_tile_size: int, + workspace_mib: int, +) -> dict[str, object]: + return { + "prepared_kernel_pack_digest": _require_sha256( + prepared_kernel_pack_digest, "prepared-kernel pack digest" + ), + "direct_template_catalog_digest": _require_sha256( + direct_template_catalog_digest, "direct-template catalog digest" + ), + "point_tile_size": _positive_integer(point_tile_size, "point tile size"), + "workspace_mib": _positive_integer(workspace_mib, "workspace MiB"), + } + + +def _recurrence_schedule_semantic_digests( + logical: RecurrenceBuilderLogicalInputV1, + *, + prepared_kernel_pack_digest: str, + direct_template_catalog_digest: str, + point_tile_size: int, + workspace_mib: int, + relation_discovery: Mapping[str, object] | None = None, +) -> tuple[str, str, str]: + """Return native, request, and sharing-domain schedule identities.""" + + domain = _recurrence_schedule_domain_payload( + prepared_kernel_pack_digest=prepared_kernel_pack_digest, + direct_template_catalog_digest=direct_template_catalog_digest, + point_tile_size=point_tile_size, + workspace_mib=workspace_mib, + ) + payload = { + "contract": "pyamplicol-recurrence-prelower-schedule-identity-v1", + "logical": _schedule_plain(logical), + **domain, + } + native_digest = _canonical_digest(payload) + request_payload = dict(payload) + sharing_domain_payload = { + "contract": "pyamplicol-recurrence-schedule-sharing-domain-v1", + **domain, + } + if relation_discovery is not None: + plain_relation = _schedule_plain(relation_discovery) + request_payload["relation_discovery"] = plain_relation + sharing_domain_payload["relation_discovery"] = plain_relation + return ( + native_digest, + _canonical_digest(request_payload), + _canonical_digest(sharing_domain_payload), + ) + + +def recurrence_helicity_selector_schedule_digest( + base_schedule_digest: str, + helicity_dispatch_sha256: str, +) -> str: + """Bind one Direct plan and its exact-helicity dispatch into one root ID.""" + + return _canonical_digest( + { + "contract": "pyamplicol-recurrence-helicity-selector-schedule-v1", + "base_schedule_digest": _require_sha256( + base_schedule_digest, + "helicity-selector base schedule digest", + ), + "helicity_dispatch": { + "abi": "pyamplicol-recurrence-helicity-dispatch-v1", + "sha256": _require_sha256( + helicity_dispatch_sha256, + "helicity-selector dispatch SHA-256", + ), + }, + } + ) + + +def recurrence_native_schedule_semantic_digest( + logical: RecurrenceBuilderLogicalInputV1, + *, + prepared_kernel_pack_digest: str, + direct_template_catalog_digest: str, + point_tile_size: int, + workspace_mib: int, +) -> str: + """Return the relation-independent semantic identity embedded by native lowering. + + Only concrete process ownership is normalized. All model, crossing, + selector, color, proof, parameter, and runtime-option semantics remain in + the digest. Relation-discovery policy is deliberately excluded: it governs + the lowering request, while certified relations themselves are authenticated + separately and a request that applies none must retain the source schedule's + byte identity. + """ + + return _canonical_digest( + _recurrence_schedule_identity_payload( + logical, + prepared_kernel_pack_digest=prepared_kernel_pack_digest, + direct_template_catalog_digest=direct_template_catalog_digest, + point_tile_size=point_tile_size, + workspace_mib=workspace_mib, + ) + ) + + +def recurrence_schedule_semantic_digest( + logical: RecurrenceBuilderLogicalInputV1, + *, + prepared_kernel_pack_digest: str, + direct_template_catalog_digest: str, + point_tile_size: int, + workspace_mib: int, + relation_discovery: Mapping[str, object] | None = None, +) -> str: + """Return the policy-bearing request/cache identity for schedule lowering. + + This identity isolates discovery policies in temporary paths, lowering + cache ownership, and artifact grouping. Native serialization receives + :func:`recurrence_native_schedule_semantic_digest` instead, so policy alone + cannot perturb a schedule when no certified relation is applied. + """ + + payload = _recurrence_schedule_identity_payload( + logical, + prepared_kernel_pack_digest=prepared_kernel_pack_digest, + direct_template_catalog_digest=direct_template_catalog_digest, + point_tile_size=point_tile_size, + workspace_mib=workspace_mib, + ) + if relation_discovery is not None: + payload["relation_discovery"] = _schedule_plain(relation_discovery) + return _canonical_digest(payload) + + +def build_recurrence_process_executor_pack( + *, + direct_catalog: object, + kernel_pack: object, + required_executor_ids: Sequence[int], + runtime_layout_digest: str, +) -> RecurrenceProcessExecutorPack: + """Project one validated model catalog to its exact fixed-plan executor set.""" + + direct_mapping = _object_mapping(direct_catalog, "direct-template catalog") + pack_mapping = _object_mapping(kernel_pack, "prepared-kernel pack") + raw_templates = _object_sequence( + direct_mapping.get("templates"), "direct-template catalog templates" + ) + catalog_count = len(raw_templates) + templates: dict[int, Mapping[str, object]] = {} + for expected_id, raw_template in enumerate(raw_templates): + template = _require_mapping(raw_template, "direct-template descriptor") + executor_id = _wire_u32( + template.get("direct_executor_id"), "direct-template executor ID" + ) + if executor_id != expected_id or executor_id in templates: + raise RecurrenceScheduleSharingError( + "direct-template executor IDs must be dense, ordered, and unique" + ) + templates[executor_id] = template + + selected_ids = tuple( + _wire_u32(value, "required direct-executor ID") + for value in required_executor_ids + ) + if selected_ids != tuple(sorted(set(selected_ids))): + raise RecurrenceScheduleSharingError( + "required direct-executor IDs must be sorted and unique" + ) + if any(executor_id not in templates for executor_id in selected_ids): + raise RecurrenceScheduleSharingError( + "required direct-executor ID is absent from its complete catalog" + ) + + raw_kernels = _object_sequence( + pack_mapping.get("kernels"), "prepared-kernel pack kernels" + ) + kernels: dict[int, Mapping[str, object]] = {} + for raw_kernel in raw_kernels: + kernel = _require_mapping(raw_kernel, "prepared-kernel descriptor") + kernel_id = _wire_u32(kernel.get("kernel_id"), "prepared-kernel ID") + if kernel_id in kernels: + raise RecurrenceScheduleSharingError( + f"prepared-kernel pack repeats kernel {kernel_id}" + ) + kernels[kernel_id] = kernel + + backend = _wire_text(direct_mapping.get("backend"), "direct backend") + if backend != pack_mapping.get("backend") or backend not in _DIRECT_BACKEND: + raise RecurrenceScheduleSharingError( + "direct-template and prepared-kernel backends disagree" + ) + target = _require_mapping(pack_mapping.get("target"), "prepared-kernel target") + target_triple = _wire_text(target.get("target_triple"), "target triple") + if target_triple != direct_mapping.get("target_triple"): + raise RecurrenceScheduleSharingError( + "direct-template and prepared-kernel targets disagree" + ) + portable = target.get("portable") + if type(portable) is not bool or portable != direct_mapping.get("portable"): + raise RecurrenceScheduleSharingError( + "direct-template and prepared-kernel portability disagree" + ) + if target.get("word_bits") != 64 or target.get("endianness") != "little": + raise RecurrenceScheduleSharingError( + "process executor packs require a 64-bit little-endian target" + ) + cpu_features = tuple( + _wire_text(value, "target CPU feature") + for value in _object_sequence(target.get("cpu_features"), "target CPU features") + ) + if cpu_features != tuple(sorted(set(cpu_features))): + raise RecurrenceScheduleSharingError( + "target CPU features must be sorted and unique" + ) + + descriptors = tuple( + _encode_process_executor_descriptor( + templates[executor_id], + backend=backend, + kernels=kernels, + ) + for executor_id in selected_ids + ) + return RecurrenceProcessExecutorPack( + compiled_model_digest=_wire_sha256( + direct_mapping.get("compiled_model_digest"), "compiled-model digest" + ), + recurrence_template_catalog_digest=_wire_sha256( + direct_mapping.get("recurrence_template_catalog_digest"), + "recurrence-template catalog digest", + ), + prepared_kernel_pack_digest=_wire_sha256( + direct_mapping.get("prepared_kernel_pack_digest"), + "prepared-kernel pack digest", + ), + direct_template_catalog_digest=_wire_sha256( + direct_mapping.get("catalog_digest"), "direct-template catalog digest" + ), + runtime_layout_digest=_wire_sha256( + runtime_layout_digest, "runtime-layout digest" + ), + backend=backend, + target_triple=target_triple, + portable=portable, + cpu_features=cpu_features, + catalog_executor_count=catalog_count, + executor_ids=selected_ids, + descriptor_payloads=descriptors, + ) + + +def _encode_process_executor_descriptor( + template: Mapping[str, object], + *, + backend: str, + kernels: Mapping[int, Mapping[str, object]], +) -> bytes: + executor_id = _wire_u32(template.get("direct_executor_id"), "direct executor ID") + role = _wire_text(template.get("role"), "direct executor role") + role_tag = _DIRECT_ROLE.get(role) + if role_tag is None: + raise RecurrenceScheduleSharingError( + f"unsupported direct executor role {role!r}" + ) + destination_count = _wire_u32( + template.get("destination_component_count"), + "direct destination component count", + positive=True, + ) + binding = _require_mapping( + template.get("payload_binding"), "direct executor payload binding" + ) + binding_kind = _wire_text(binding.get("kind"), "direct binding kind") + exact_factor_slots = _object_sequence( + binding.get("exact_factor_scalar_slots"), "exact-factor scalar slots" + ) + flags = _DIRECT_FLAG_USES_EXACT_FACTOR if len(exact_factor_slots) != 0 else 0 + if role == "source": + record_kind = _DIRECT_BINDING_SOURCE + body = b"" + elif binding_kind == "rusticol-intrinsic": + record_kind = _DIRECT_BINDING_INTRINSIC + body = _encode_intrinsic_binding(binding, role=role) + elif binding_kind == "prepared-direct-call" and backend == "jit": + record_kind = _DIRECT_BINDING_JIT + body = _encode_jit_binding(binding, template=template, kernels=kernels) + elif binding_kind == "prepared-direct-call" and backend in {"cpp", "asm"}: + record_kind = _DIRECT_BINDING_NATIVE + body = _encode_native_binding(binding, template=template, kernels=kernels) + else: + raise RecurrenceScheduleSharingError( + f"unsupported executable direct binding {binding_kind!r} for {backend!r}" + ) + size = 16 + len(body) + if size > _MISSING_U32: + raise RecurrenceScheduleSharingError( + "process direct-executor descriptor exceeds the u32 wire domain" + ) + return ( + struct.pack( + " bytes: + runtime_template = _wire_text( + binding.get("runtime_template"), "intrinsic runtime template" + ) + raw_projections = _object_sequence( + binding.get("scalar_projections"), "intrinsic scalar projections" + ) + if role == "contribution": + if len(raw_projections) != 1: + raise RecurrenceScheduleSharingError( + "contribution intrinsic requires exactly one scale projection" + ) + projection = _require_mapping( + raw_projections[0], "contribution intrinsic scale" + ) + if projection.get("kind") != "intrinsic-scale-v1": + raise RecurrenceScheduleSharingError( + "contribution intrinsic has an unsupported scale projection" + ) + real_bits = _wire_u64( + projection.get("constant_real_bits"), "intrinsic real bits" + ) + imag_bits = _wire_u64( + projection.get("constant_imag_bits"), "intrinsic imaginary bits" + ) + raw_parameter = projection.get("parameter_index") + parameter = ( + _MISSING_U32 + if raw_parameter is None + else _wire_u32(raw_parameter, "intrinsic parameter index") + ) + has_scale = 1 + else: + if raw_projections: + raise RecurrenceScheduleSharingError( + "non-contribution intrinsic cannot carry scalar projections" + ) + real_bits = 0 + imag_bits = 0 + parameter = _MISSING_U32 + has_scale = 0 + return struct.pack( + " bytes: + kernel_id = _wire_u32(binding.get("prepared_kernel_id"), "prepared JIT kernel ID") + kernel = kernels.get(kernel_id) + if kernel is None: + raise RecurrenceScheduleSharingError( + f"prepared JIT kernel {kernel_id} is absent" + ) + evaluator = _require_mapping( + kernel.get("f64_evaluator_manifest"), "prepared JIT evaluator" + ) + plane = _require_mapping( + evaluator.get("plane_application"), "prepared JIT plane application" + ) + compression = plane.get("compression") + if type(compression) is not bool: + raise RecurrenceScheduleSharingError( + "prepared JIT plane compression must be boolean" + ) + optimization_level = _wire_u32( + template.get("optimization_level"), "prepared JIT optimization level" + ) + source_path = _wire_text( + binding.get("source_application_path"), "prepared JIT source path" + ) + source_sha256 = _wire_sha256( + binding.get("source_application_sha256"), "prepared JIT source digest" + ) + source_abi = _wire_text( + binding.get("source_application_abi"), "prepared JIT source ABI" + ) + parameter_bindings = _object_sequence( + binding.get("parameter_bindings"), "prepared JIT parameter bindings" + ) + plane_projections = _object_sequence( + binding.get("input_plane_projections"), "prepared JIT plane projections" + ) + scalar_projections = _object_sequence( + binding.get("scalar_projections"), "prepared JIT scalar projections" + ) + output_aliases = tuple( + _wire_u32(value, "prepared JIT output alias") + for value in _object_sequence( + binding.get("output_alias_inputs"), "prepared JIT output aliases" + ) + ) + counts = tuple( + _wire_u32(len(values), f"prepared JIT {label} count") + for values, label in ( + (parameter_bindings, "parameter-binding"), + (plane_projections, "plane-projection"), + (scalar_projections, "scalar-projection"), + (output_aliases, "output-alias"), + ) + ) + return b"".join( + ( + struct.pack( + " bytes: + kernel_id = _wire_u32( + binding.get("prepared_kernel_id"), "prepared native kernel ID" + ) + kernel = kernels.get(kernel_id) + if kernel is None: + raise RecurrenceScheduleSharingError( + f"prepared native kernel {kernel_id} is absent" + ) + source_path = _wire_text( + binding.get("source_application_path"), "prepared native library path" + ) + entry_point = _wire_text( + binding.get("native_entry_point"), "prepared native entry point" + ) + coupling = _prepared_native_coupling(binding, kernel) + if coupling is None: + has_coupling = 0 + coupling_real_bits = 0 + coupling_imag_bits = 0 + else: + has_coupling = 1 + coupling_real_bits = _f64_bits(coupling[0]) + coupling_imag_bits = _f64_bits(coupling[1]) + return b"".join( + ( + struct.pack( + " tuple[float, float] | None: + parameter_bindings = _object_sequence( + binding.get("parameter_bindings"), "native parameter bindings" + ) + input_contracts = _object_sequence( + kernel.get("input_contracts"), "native kernel input contracts" + ) + if len(parameter_bindings) != 2 * len(input_contracts): + raise RecurrenceScheduleSharingError( + "native direct parameter bindings do not match the kernel inputs" + ) + scalars = _object_sequence( + binding.get("scalar_projections"), "native scalar projections" + ) + coupling_real: float | None = None + coupling_imag: float | None = None + for input_index, raw_contract in enumerate(input_contracts): + contract = _require_mapping(raw_contract, "native kernel input contract") + role = contract.get("role") + if role not in {"coupling-real", "coupling-imag"}: + continue + value = _native_literal_binding( + parameter_bindings, + scalars, + 2 * input_index, + ) + imaginary = _native_literal_binding( + parameter_bindings, + scalars, + 2 * input_index + 1, + ) + if imaginary != 0.0: + raise RecurrenceScheduleSharingError( + "native direct coupling scalar lane has a nonzero imaginary part" + ) + if role == "coupling-real": + if coupling_real is not None: + raise RecurrenceScheduleSharingError( + "native direct kernel repeats its real coupling input" + ) + coupling_real = value + else: + if coupling_imag is not None: + raise RecurrenceScheduleSharingError( + "native direct kernel repeats its imaginary coupling input" + ) + coupling_imag = value + if coupling_real is None and coupling_imag is None: + return None + return ( + 0.0 if coupling_real is None else coupling_real, + 0.0 if coupling_imag is None else coupling_imag, + ) + + +def _native_literal_binding( + parameter_bindings: Sequence[object], + scalars: Sequence[object], + parameter_index: int, +) -> float: + binding = _require_mapping( + parameter_bindings[parameter_index], "native scalar parameter binding" + ) + if binding.get("kind") != "scalar": + raise RecurrenceScheduleSharingError( + "native direct coupling is not bound to a scalar" + ) + scalar_index = _wire_u32(binding.get("index"), "native scalar index") + if scalar_index >= len(scalars): + raise RecurrenceScheduleSharingError( + "native direct coupling scalar index is out of bounds" + ) + scalar = _require_mapping(scalars[scalar_index], "native literal scalar") + raw_value = scalar.get("value") + if ( + scalar.get("kind") != "literal" + or isinstance(raw_value, bool) + or not isinstance(raw_value, int | float) + ): + raise RecurrenceScheduleSharingError( + "native direct coupling is not an immutable literal" + ) + value = float(raw_value) + if not math.isfinite(value): + raise RecurrenceScheduleSharingError( + "native direct coupling literal is not finite" + ) + return value + + +def _encode_parameter_binding(value: object) -> bytes: + binding = _require_mapping(value, "JIT parameter binding") + kind = binding.get("kind") + tag = 0 if kind == "plane" else 1 if kind == "scalar" else None + if tag is None: + raise RecurrenceScheduleSharingError( + f"unsupported JIT parameter binding {kind!r}" + ) + index = _wire_u32(binding.get("index"), "JIT parameter-binding index") + return struct.pack(" bytes: + projection = _require_mapping(value, "JIT plane projection") + kind = projection.get("kind") + if kind == "parent-current": + tag = 0 + operand = _wire_u8(projection.get("parent"), "JIT parent index") + component = _wire_u16(projection.get("component"), "JIT parent component") + imaginary = _wire_bool(projection.get("imaginary"), "JIT imaginary flag") + elif kind == "momentum": + tag = 1 + operand = _wire_u8(projection.get("operand"), "JIT momentum operand") + component = _wire_u16( + projection.get("lorentz_component"), "JIT Lorentz component" + ) + imaginary = False + elif kind in {"destination-current", "destination-amplitude"}: + tag = 2 if kind == "destination-current" else 3 + operand = 0 + component = _wire_u16(projection.get("component"), "JIT destination component") + imaginary = _wire_bool(projection.get("imaginary"), "JIT imaginary flag") + else: + raise RecurrenceScheduleSharingError( + f"unsupported JIT plane projection {kind!r}" + ) + return struct.pack(" bytes: + projection = _require_mapping(value, "JIT scalar projection") + kind = projection.get("kind") + if kind == "exact-factor": + tag = 0 + imaginary = _wire_bool(projection.get("imaginary"), "JIT imaginary flag") + index = 0 + bits = 0 + elif kind == "parameter": + tag = 1 + imaginary = _wire_bool(projection.get("imaginary"), "JIT imaginary flag") + index = _wire_u32(projection.get("index"), "JIT parameter index") + bits = 0 + elif kind == "literal": + tag = 2 + imaginary = False + index = 0 + raw_value = projection.get("value") + if isinstance(raw_value, bool) or not isinstance(raw_value, int | float): + raise RecurrenceScheduleSharingError("JIT literal must be numeric") + value = float(raw_value) + if not math.isfinite(value): + raise RecurrenceScheduleSharingError("JIT literal must be finite") + bits = _f64_bits(value) + else: + raise RecurrenceScheduleSharingError( + f"unsupported JIT scalar projection {kind!r}" + ) + return struct.pack(" Mapping[str, object]: + operation = getattr(value, "to_dict", None) + if not callable(operation): + raise RecurrenceScheduleSharingError(f"{context} is not a typed model object") + return _require_mapping(operation(), context) + + +def _require_mapping(value: object, context: str) -> Mapping[str, object]: + if not isinstance(value, Mapping): + raise RecurrenceScheduleSharingError(f"{context} must be a mapping") + return value + + +def _object_sequence(value: object, context: str) -> tuple[object, ...]: + if isinstance(value, str | bytes) or not isinstance(value, Sequence): + raise RecurrenceScheduleSharingError(f"{context} must be a sequence") + return tuple(value) + + +def _wire_text(value: object, context: str) -> str: + return _nonempty_text(value, context) + + +def _wire_sha256(value: object, context: str) -> str: + return _require_sha256(value, context) + + +def _wire_bool(value: object, context: str) -> bool: + if type(value) is not bool: + raise RecurrenceScheduleSharingError(f"{context} must be boolean") + return value + + +def _wire_integer( + value: object, + context: str, + maximum: int, + *, + positive: bool = False, +) -> int: + if ( + isinstance(value, bool) + or not isinstance(value, int) + or value < int(positive) + or value > maximum + ): + qualifier = "positive " if positive else "" + raise RecurrenceScheduleSharingError( + f"{context} must be a {qualifier}integer no larger than {maximum}" + ) + return value + + +def _wire_u8(value: object, context: str) -> int: + return _wire_integer(value, context, (1 << 8) - 1) + + +def _wire_u16(value: object, context: str) -> int: + return _wire_integer(value, context, (1 << 16) - 1) + + +def _wire_u32(value: object, context: str, *, positive: bool = False) -> int: + return _wire_integer(value, context, _MISSING_U32, positive=positive) + + +def _wire_u64(value: object, context: str) -> int: + return _wire_integer(value, context, (1 << 64) - 1) + + +def _f64_bits(value: float) -> int: + return struct.unpack(" bytes: + encoded = value.encode("utf-8") + if not encoded or len(encoded) > _MISSING_U32: + raise RecurrenceScheduleSharingError( + f"{context} has an invalid UTF-8 wire length" + ) + return struct.pack(" RecurrenceProcessRemap | None: + """Return an exact root-to-target schedule binding or fail closed. + + V1 sharing is intentionally limited to complete topology-replay schedules. + The relation is an all-outgoing external-state isomorphism, not a particle + label erasure: every source state, color forest, replay selector, parameter + projection, and fermion-pairing row must map bijectively. + """ + + if ( + root.layout != "topology-replay" + or target.layout != "topology-replay" + or len(root.external_legs) != len(target.external_legs) + or root.coupling_limits != target.coupling_limits + or root.parameter_projection != target.parameter_projection + or root.semantic_template_references != target.semantic_template_references + or root.selected_public_flow_ids is not None + or target.selected_public_flow_ids is not None + or root.selected_source_coverage is not None + or target.selected_source_coverage is not None + or not _fixed_semantic_digests_match(root, target) + ): + return None + + candidates = _source_slot_bijections(root.external_legs, target.external_legs) + for source_slots in candidates: + source_relation = _external_source_relation( + root.external_legs, + target.external_legs, + source_slots, + ) + if source_relation is None: + continue + ( + momentum_signs, + helicity_signs, + source_state_offsets, + source_state_indices, + source_template_changes, + ) = source_relation + sector_ids = _sector_bijection( + root.physical_sectors, + target.physical_sectors, + source_slots, + ) + if sector_ids is None: + continue + flow_ids = _flow_bijection( + root.public_flows, + target.public_flows, + source_slots, + sector_ids, + ) + if flow_ids is None: + continue + # Direct-Arena proof metadata still owns physical sector IDs. Until + # that metadata has an explicit remap ABI, share only schedules whose + # physical/public sector numbering is already identical. + if sector_ids != tuple(range(len(sector_ids))) or flow_ids != tuple( + range(len(flow_ids)) + ): + continue + if not _replay_contracts_match( + root, + target, + source_slots=source_slots, + sector_ids=sector_ids, + ): + continue + if not _fermion_pairing_contracts_match( + root.fermion_pairing_catalog, + target.fermion_pairing_catalog, + source_slots, + ): + continue + + result = RecurrenceProcessRemap( + source_slots=source_slots, + source_momentum_signs=momentum_signs, + source_helicity_signs=helicity_signs, + source_state_offsets=source_state_offsets, + source_state_indices=source_state_indices, + public_flow_ids=flow_ids, + physical_sector_ids=tuple(range(_physical_sector_domain_count(root))), + state_template_count=_template_count(root, "current-state"), + source_template_count=_template_count(root, "source"), + direct_executor_count=_nonnegative_integer( + direct_executor_count, "direct-executor count" + ), + parameter_slot_count=_nonnegative_integer( + parameter_slot_count, "parameter-slot count" + ), + source_template_changes=source_template_changes, + ) + relation_digest = _canonical_digest( + { + "contract": "pyamplicol-recurrence-process-bijection-v1", + "root_process": root.process_id, + "target_process": target.process_id, + "root_variable_semantics": _variable_semantic_digests(root), + "target_variable_semantics": _variable_semantic_digests(target), + "root_pairing": _pairing_proof_digests(root.fermion_pairing_catalog), + "target_pairing": _pairing_proof_digests( + target.fermion_pairing_catalog + ), + "root_sector_proofs": [ + [sector.closure_proof_algorithm, sector.closure_proof_digest] + for sector in root.physical_sectors + ], + "target_sector_proofs": [ + [sector.closure_proof_algorithm, sector.closure_proof_digest] + for sector in target.physical_sectors + ], + "root_replay_proofs": [ + [partition.proof_algorithm, partition.proof_digest] + for partition in root.replay_partitions + ], + "target_replay_proofs": [ + [partition.proof_algorithm, partition.proof_digest] + for partition in target.replay_partitions + ], + "remap": result._digest_payload(), + } + ) + return result.with_digest(relation_digest) + return None + + +def _fixed_semantic_digests_match( + root: RecurrenceBuilderLogicalInputV1, + target: RecurrenceBuilderLogicalInputV1, +) -> bool: + root_rows = {row.role: row.digest for row in root.semantic_digests} + target_rows = {row.role: row.digest for row in target.semantic_digests} + return set(root_rows) == set(target_rows) and { + role: digest + for role, digest in root_rows.items() + if role not in _PROCESS_VARIABLE_SEMANTIC_ROLES + } == { + role: digest + for role, digest in target_rows.items() + if role not in _PROCESS_VARIABLE_SEMANTIC_ROLES + } + + +def _variable_semantic_digests( + logical: RecurrenceBuilderLogicalInputV1, +) -> dict[str, str]: + return { + row.role: row.digest + for row in logical.semantic_digests + if row.role in _PROCESS_VARIABLE_SEMANTIC_ROLES + } + + +def _source_slot_bijections( + root: tuple[RecurrenceExternalLegV1, ...], + target: tuple[RecurrenceExternalLegV1, ...], +) -> tuple[tuple[int, ...], ...]: + root_groups: dict[tuple[object, ...], list[int]] = {} + target_groups: dict[tuple[object, ...], list[int]] = {} + for leg in root: + root_groups.setdefault(_external_leg_orbit_key(leg), []).append(leg.source_slot) + for leg in target: + target_groups.setdefault(_external_leg_orbit_key(leg), []).append( + leg.source_slot + ) + if set(root_groups) != set(target_groups): + return () + groups: list[tuple[tuple[int, ...], tuple[tuple[int, ...], ...]]] = [] + count = 1 + for key in sorted(root_groups, key=repr): + left = tuple(root_groups[key]) + right = tuple(target_groups[key]) + if len(left) != len(right): + return () + permutations = tuple(itertools.permutations(right)) + count *= len(permutations) + if count > _MAX_SOURCE_BIJECTIONS: + return () + groups.append((left, permutations)) + result: list[tuple[int, ...]] = [] + for selection in itertools.product(*(rows[1] for rows in groups)): + mapping = [0] * len(root) + for (root_slots, _), target_slots in zip(groups, selection, strict=True): + for root_slot, target_slot in zip(root_slots, target_slots, strict=True): + mapping[root_slot] = target_slot + result.append(tuple(mapping)) + return tuple(result) + + +def _external_leg_orbit_key(leg: RecurrenceExternalLegV1) -> tuple[object, ...]: + return ( + leg.outgoing_pdg, + leg.is_fermionic, + tuple( + sorted( + ( + state.current_state_template_id, + state.chirality, + state.spin_state, + state.crossing_phase.canonical_key, + ) + for state in leg.source_states + ) + ), + ) + + +def _external_source_relation( + root: tuple[RecurrenceExternalLegV1, ...], + target: tuple[RecurrenceExternalLegV1, ...], + source_slots: tuple[int, ...], +) -> ( + tuple[ + tuple[int, ...], + tuple[int, ...], + tuple[int, ...], + tuple[int, ...], + tuple[tuple[int, int], ...], + ] + | None +): + momentum_signs: list[int] = [] + helicity_signs: list[int] = [] + source_state_offsets = [0] + source_state_indices: list[int] = [] + source_templates: dict[int, int] = {} + reverse_source_templates: dict[int, int] = {} + for root_slot, target_slot in enumerate(source_slots): + left = root[root_slot] + right = target[target_slot] + if ( + left.outgoing_pdg != right.outgoing_pdg + or left.is_fermionic != right.is_fermionic + or len(left.source_states) != len(right.source_states) + ): + return None + right_states: dict[tuple[object, ...], RecurrenceSourceStateV1] = {} + for state in right.source_states: + key = _source_state_execution_key(state) + if key in right_states: + return None + right_states[key] = state + momentum_factor: int | None = None + helicity_factor: int | None = None + state_indices = [0] * len(left.source_states) + for state in left.source_states: + other = right_states.get(_source_state_execution_key(state)) + if other is None: + return None + state_indices[state.state_index] = other.state_index + candidate_momentum = other.momentum_sign * state.momentum_sign + if momentum_factor is None: + momentum_factor = candidate_momentum + elif momentum_factor != candidate_momentum: + return None + candidate_helicity = _signed_value_relation( + state.public_helicity, + other.public_helicity, + ) + if candidate_helicity is None: + return None + if helicity_factor is None: + helicity_factor = candidate_helicity + elif helicity_factor != candidate_helicity: + return None + previous = source_templates.setdefault( + state.source_template_id, other.source_template_id + ) + reverse = reverse_source_templates.setdefault( + other.source_template_id, state.source_template_id + ) + if ( + previous != other.source_template_id + or reverse != state.source_template_id + ): + return None + momentum_signs.append(1 if momentum_factor is None else momentum_factor) + helicity_signs.append(1 if helicity_factor is None else helicity_factor) + if not _is_permutation(state_indices): + return None + source_state_indices.extend(state_indices) + source_state_offsets.append(len(source_state_indices)) + changes = tuple( + (source, target_id) + for source, target_id in sorted(source_templates.items()) + if source != target_id + ) + return ( + tuple(momentum_signs), + tuple(helicity_signs), + tuple(source_state_offsets), + tuple(source_state_indices), + changes, + ) + + +def _source_state_execution_key( + state: RecurrenceSourceStateV1, +) -> tuple[object, ...]: + return ( + state.current_state_template_id, + state.chirality, + state.spin_state, + state.crossing_phase.canonical_key, + ) + + +def _signed_value_relation(left: int, right: int) -> int | None: + if left == 0 or right == 0: + return 1 if left == right else None + if right == left: + return 1 + if right == -left: + return -1 + return None + + +def _sector_bijection( + root: tuple[RecurrencePhysicalLCSectorV1, ...], + target: tuple[RecurrencePhysicalLCSectorV1, ...], + source_slots: tuple[int, ...], +) -> tuple[int, ...] | None: + inverse = _inverse_permutation(source_slots) + targets: dict[tuple[object, ...], int] = {} + for sector in target: + key = _sector_key(sector, inverse) + if key in targets: + return None + targets[key] = sector.sector_id + result: list[int] = [] + for sector in root: + target_id = targets.get(_sector_key(sector, tuple(range(len(source_slots))))) + if target_id is None: + return None + result.append(target_id) + return tuple(result) if _is_permutation(result) else None + + +def _sector_key( + sector: RecurrencePhysicalLCSectorV1, + slots: tuple[int, ...], +) -> tuple[object, ...]: + def mapped(values: Sequence[int]) -> tuple[int, ...]: + return tuple(slots[value] for value in values) + + return ( + sector.kind, + slots[sector.closure_source_slot], + sector.closure_proof_algorithm, + tuple( + ( + slots[row.fundamental_source_slot], + slots[row.antifundamental_source_slot], + mapped(row.adjoint_source_slots), + mapped(row.singlet_source_slots), + ) + for row in sector.open_strings + ), + mapped(sector.trace_source_slots), + mapped(sector.singlet_source_slots), + mapped(sector.word_source_slots), + ) + + +def _flow_bijection( + root: tuple[RecurrencePublicLCFlowV1, ...], + target: tuple[RecurrencePublicLCFlowV1, ...], + source_slots: tuple[int, ...], + sector_ids: tuple[int, ...], +) -> tuple[int, ...] | None: + inverse_slots = _inverse_permutation(source_slots) + inverse_sectors = _inverse_permutation(sector_ids) + targets: dict[tuple[object, ...], int] = {} + for flow in target: + permutation = tuple( + inverse_slots[flow.source_slot_permutation[source_slots[root_slot]]] + for root_slot in range(len(source_slots)) + ) + key = ( + inverse_sectors[flow.construction_sector_id], + tuple(inverse_slots[value] for value in flow.word_source_slots), + permutation, + flow.reduction_weight.canonical_key, + ) + if key in targets: + return None + targets[key] = flow.flow_id + result = [] + for flow in root: + key = ( + flow.construction_sector_id, + flow.word_source_slots, + flow.source_slot_permutation, + flow.reduction_weight.canonical_key, + ) + target_id = targets.get(key) + if target_id is None: + return None + result.append(target_id) + return tuple(result) if _is_permutation(result) else None + + +def _replay_contracts_match( + root: RecurrenceBuilderLogicalInputV1, + target: RecurrenceBuilderLogicalInputV1, + *, + source_slots: tuple[int, ...], + sector_ids: tuple[int, ...], +) -> bool: + inverse_sectors = _inverse_permutation(sector_ids) + left = tuple( + _replay_partition_key( + row, + tuple(range(len(source_slots))), + tuple(range(len(source_slots))), + tuple(range(len(sector_ids))), + ) + for row in root.replay_partitions + ) + right = tuple( + _replay_partition_key( + row, + source_slots, + _inverse_permutation(source_slots), + inverse_sectors, + ) + for row in target.replay_partitions + ) + return left == right + + +def _replay_partition_key( + row: object, + root_to_concrete: tuple[int, ...], + concrete_to_root: tuple[int, ...], + sectors: tuple[int, ...], +) -> tuple[object, ...]: + def conjugate(values: Sequence[int]) -> tuple[int, ...]: + return tuple( + concrete_to_root[values[root_to_concrete[root_slot]]] + for root_slot in range(len(root_to_concrete)) + ) + + return ( + sectors[row.representative_sector_id], + sectors[row.materialized_sector_id], + row.proof_algorithm, + tuple( + ( + sectors[target.sector_id], + conjugate(target.external_permutation), + conjugate(target.source_slot_permutation), + target.amplitude_phase.canonical_key, + target.fermion_sign, + ) + for target in row.targets + ), + ) + + +def _fermion_pairing_contracts_match( + root: object, + target: object, + source_slots: tuple[int, ...], +) -> bool: + if (root is None) != (target is None): + return False + if root is None: + return True + inverse = _inverse_permutation(source_slots) + return _pairing_key(root, tuple(range(len(source_slots)))) == _pairing_key( + target, inverse + ) + + +def _pairing_key(catalog: object, slots: tuple[int, ...]) -> tuple[object, ...]: + concrete_to_root = slots + root_to_concrete = _inverse_permutation(concrete_to_root) + endpoints = tuple( + sorted( + ( + concrete_to_root[row.source_slot], + row.species_id, + row.particle_orientation, + row.color_orientation, + row.state_template_ids, + row.anti_state_template_ids, + row.basis_ids, + row.color_representations, + ) + for row in catalog.endpoints + ) + ) + classes = tuple( + sorted( + ( + row.species_id, + tuple( + concrete_to_root[value] for value in row.fundamental_source_slots + ), + tuple( + concrete_to_root[value] + for value in row.antifundamental_source_slots + ), + tuple( + ( + concrete_to_root[left], + concrete_to_root[right], + ) + for left, right in row.reference_pairings + ), + row.pairing_count, + ) + for row in catalog.pairing_classes + ) + ) + rules = tuple( + sorted( + ( + row.class_pairing_indices, + tuple( + ( + concrete_to_root[left], + concrete_to_root[right], + ) + for left, right in row.endpoint_pairings + ), + tuple( + concrete_to_root[ + row.source_slot_permutation[root_to_concrete[root_slot]] + ] + for root_slot in range(len(slots)) + ), + tuple( + row.lineage_by_source_slot[root_to_concrete[root_slot]] + for root_slot in range(len(slots)) + ), + row.fermion_parity, + ( + row.exact_factor.real_numerator, + row.exact_factor.real_denominator, + row.exact_factor.imag_numerator, + row.exact_factor.imag_denominator, + ), + row.multiplicity, + row.proof_algorithm, + ) + for row in catalog.rules + ) + ) + return catalog.source_count, endpoints, classes, rules + + +def _pairing_proof_digests(catalog: object) -> object: + if catalog is None: + return None + return { + "semantic": catalog.semantic_digest, + "topology": catalog.topology_digest, + "endpoints": [row.contract_digest for row in catalog.endpoints], + "classes": [row.proof_digest for row in catalog.pairing_classes], + "rules": [row.proof_digest for row in catalog.rules], + } + + +class RecurrenceSharedSchedule: + """One immutable root Direct-Arena schedule.""" + + __slots__ = ( + "digest", + "index_sha256", + "member_count", + "process_ids", + "sha256", + "size_bytes", + "source_path", + "unpacked_size_bytes", + ) + + def __init__( + self, + *, + digest: str, + source_path: Path, + sha256: str, + size_bytes: int, + member_count: int, + unpacked_size_bytes: int, + index_sha256: str, + process_ids: tuple[str, ...], + ) -> None: + self.digest = _require_sha256(digest, "schedule digest") + self.source_path = source_path + self.sha256 = _require_sha256(sha256, "schedule payload SHA-256") + self.size_bytes = _positive_integer(size_bytes, "schedule payload size") + self.member_count = _positive_integer(member_count, "schedule member count") + self.unpacked_size_bytes = _positive_integer( + unpacked_size_bytes, "schedule unpacked size" + ) + self.index_sha256 = _require_sha256( + index_sha256, "schedule PACBIN index SHA-256" + ) + self.process_ids = process_ids + + @property + def artifact_path(self) -> str: + return f"recurrence/schedules/{self.digest}/recurrence-runtime.pacbin" + + def to_mapping(self) -> dict[str, object]: + return { + "digest": self.digest, + "path": self.artifact_path, + "sha256": self.sha256, + "size_bytes": self.size_bytes, + "member_count": self.member_count, + "unpacked_size_bytes": self.unpacked_size_bytes, + "index_sha256": self.index_sha256, + "process_ids": list(self.process_ids), + } + + +class RecurrenceProcessBinding: + """One independent concrete-process binding to a root schedule.""" + + __slots__ = ( + "executor_pack", + "native_schedule_semantic_digest", + "payload", + "process_digest", + "process_id", + "process_semantic_digest", + "process_support_mask", + "process_support_words", + "remap", + "schedule_digest", + "sha256", + ) + + def __init__( + self, + *, + process_id: str, + schedule_digest: str, + native_schedule_semantic_digest: str, + process_digest: str, + process_semantic_digest: str, + process_support_mask: int, + remap: RecurrenceProcessRemap, + executor_pack: RecurrenceProcessExecutorPack, + ) -> None: + self.process_id = _nonempty_text(process_id, "process ID") + self.schedule_digest = _require_sha256(schedule_digest, "schedule digest") + self.native_schedule_semantic_digest = _require_sha256( + native_schedule_semantic_digest, + "native schedule semantic digest", + ) + self.process_digest = _require_sha256(process_digest, "process digest") + self.process_semantic_digest = _require_sha256( + process_semantic_digest, "process semantic digest" + ) + self.process_support_mask = _single_bit( + process_support_mask, "process support mask" + ) + self.process_support_words = tuple( + (self.process_support_mask >> shift) & ((1 << 64) - 1) + for shift in range(0, max(1, self.process_support_mask.bit_length()), 64) + ) + if not isinstance(remap, RecurrenceProcessRemap): + raise RecurrenceScheduleSharingError( + "recurrence process binding requires an exact process remap" + ) + if not remap.bijection_digest: + raise RecurrenceScheduleSharingError( + "recurrence process remap has no authenticated bijection digest" + ) + self.remap = remap + if not isinstance(executor_pack, RecurrenceProcessExecutorPack): + raise RecurrenceScheduleSharingError( + "recurrence process binding requires a typed executor pack" + ) + if executor_pack.catalog_executor_count != remap.direct_executor_count: + raise RecurrenceScheduleSharingError( + "recurrence process executor domain disagrees with its remap" + ) + self.executor_pack = executor_pack + self.payload = encode_recurrence_process_binding( + process_id=self.process_id, + schedule_digest=self.schedule_digest, + process_digest=self.process_digest, + process_semantic_digest=self.process_semantic_digest, + process_support_mask=self.process_support_mask, + remap=self.remap, + executor_pack=self.executor_pack, + ) + self.sha256 = hashlib.sha256(self.payload).hexdigest() + + @property + def artifact_path(self) -> str: + return f"processes/{self.process_id}/recurrence-binding.bin" + + def to_mapping(self) -> dict[str, object]: + return { + "abi": RECURRENCE_PROCESS_BINDING_ABI, + "process_id": self.process_id, + "schedule_digest": self.schedule_digest, + "native_schedule_semantic_digest": (self.native_schedule_semantic_digest), + "process_digest": self.process_digest, + "process_semantic_digest": self.process_semantic_digest, + "process_support_words": list(self.process_support_words), + "remap": self.remap.to_mapping(), + "path": "recurrence-binding.bin", + "size_bytes": len(self.payload), + "sha256": self.sha256, + } + + +class RecurrenceScheduleSharingPlan: + __slots__ = ("bindings", "schedules") + + def __init__( + self, + *, + schedules: tuple[RecurrenceSharedSchedule, ...], + bindings: tuple[RecurrenceProcessBinding, ...], + ) -> None: + self.schedules = schedules + self.bindings = bindings + + def schedule(self, digest: str) -> RecurrenceSharedSchedule: + for schedule in self.schedules: + if schedule.digest == digest: + return schedule + raise RecurrenceScheduleSharingError(f"unknown recurrence schedule {digest!r}") + + def binding(self, process_id: str) -> RecurrenceProcessBinding: + for binding in self.bindings: + if binding.process_id == process_id: + return binding + raise RecurrenceScheduleSharingError( + f"process {process_id!r} has no recurrence binding" + ) + + def to_mapping(self) -> dict[str, object]: + return { + "kind": RECURRENCE_SCHEDULE_SHARING_KIND, + "schema_version": RECURRENCE_SCHEDULE_SHARING_SCHEMA_VERSION, + "schedule_count": len(self.schedules), + "binding_count": len(self.bindings), + "schedule_alias_count": len(self.bindings) - len(self.schedules), + "runtime_ownership": "root-schedule-plus-process-binding", + "interning_phase": "before-direct-lowering", + "schedules": [item.to_mapping() for item in self.schedules], + "bindings": [item.to_mapping() for item in self.bindings], + } + + def extension_mapping(self, *, index_sha256: str) -> dict[str, object]: + return { + "kind": RECURRENCE_SCHEDULE_SHARING_KIND, + "schema_version": RECURRENCE_SCHEDULE_SHARING_SCHEMA_VERSION, + "index_path": RECURRENCE_SCHEDULE_INDEX_PATH, + "index_sha256": _require_sha256(index_sha256, "schedule index SHA-256"), + "schedule_count": len(self.schedules), + "binding_count": len(self.bindings), + "schedule_alias_count": len(self.bindings) - len(self.schedules), + "runtime_ownership": "root-schedule-plus-process-binding", + "interning_phase": "before-direct-lowering", + } + + +def intern_recurrence_schedules( + processes: Sequence[RecurrenceScheduleProcess], +) -> RecurrenceScheduleSharingPlan: + """Validate and intern already pre-lowered root schedules.""" + + schedules: dict[str, RecurrenceSharedSchedule] = {} + schedule_processes: dict[str, list[str]] = {} + schedule_native_semantics: dict[str, str] = {} + bindings: list[RecurrenceProcessBinding] = [] + process_ids: set[str] = set() + support_masks: set[int] = set() + + for process in processes: + process_id = _nonempty_text(process.process_id, "recurrence process ID") + if process_id in process_ids: + raise RecurrenceScheduleSharingError( + f"duplicate recurrence process binding {process_id!r}" + ) + process_ids.add(process_id) + support_mask = _single_bit( + process.process_support_mask, f"process {process_id!r} support mask" + ) + if support_mask in support_masks: + raise RecurrenceScheduleSharingError( + f"recurrence support mask {support_mask} is reused" + ) + support_masks.add(support_mask) + + digest = _require_sha256( + process.recurrence_schedule_digest, + f"process {process_id!r} schedule digest", + ) + native_semantic_digest = _require_sha256( + process.recurrence_native_schedule_semantic_digest, + f"process {process_id!r} native schedule semantic digest", + ) + candidate = RecurrenceSharedSchedule( + digest=digest, + source_path=process.recurrence_schedule_path, + sha256=process.recurrence_schedule_sha256, + size_bytes=process.recurrence_schedule_size_bytes, + member_count=process.recurrence_schedule_member_count, + unpacked_size_bytes=process.recurrence_schedule_unpacked_size_bytes, + index_sha256=process.recurrence_schedule_index_sha256, + process_ids=(), + ) + previous = schedules.get(digest) + if previous is not None and ( + previous.sha256 != candidate.sha256 + or previous.size_bytes != candidate.size_bytes + or previous.member_count != candidate.member_count + or previous.unpacked_size_bytes != candidate.unpacked_size_bytes + or previous.index_sha256 != candidate.index_sha256 + ): + raise RecurrenceScheduleSharingError( + f"schedule digest {digest} maps to different payloads" + ) + previous_native_semantic_digest = schedule_native_semantics.get(digest) + if ( + previous_native_semantic_digest is not None + and previous_native_semantic_digest != native_semantic_digest + ): + raise RecurrenceScheduleSharingError( + f"schedule digest {digest} maps to different native semantics" + ) + schedules.setdefault(digest, candidate) + schedule_native_semantics.setdefault(digest, native_semantic_digest) + schedule_processes.setdefault(digest, []).append(process_id) + bindings.append( + RecurrenceProcessBinding( + process_id=process_id, + schedule_digest=digest, + native_schedule_semantic_digest=native_semantic_digest, + process_digest=process.process_digest, + process_semantic_digest=process.builder_input_sha256, + process_support_mask=support_mask, + remap=process.recurrence_process_remap, + executor_pack=process.recurrence_process_executor_pack, + ) + ) + + shared = tuple( + RecurrenceSharedSchedule( + digest=digest, + source_path=schedule.source_path, + sha256=schedule.sha256, + size_bytes=schedule.size_bytes, + member_count=schedule.member_count, + unpacked_size_bytes=schedule.unpacked_size_bytes, + index_sha256=schedule.index_sha256, + process_ids=tuple(sorted(schedule_processes[digest])), + ) + for digest, schedule in sorted(schedules.items()) + ) + return RecurrenceScheduleSharingPlan( + schedules=shared, + bindings=tuple(sorted(bindings, key=lambda item: item.process_id)), + ) + + +def encode_recurrence_process_binding( + *, + process_id: str, + schedule_digest: str, + process_digest: str, + process_semantic_digest: str, + process_support_mask: int, + remap: RecurrenceProcessRemap, + executor_pack: RecurrenceProcessExecutorPack, +) -> bytes: + process = _nonempty_text(process_id, "process ID").encode("utf-8") + if len(process) > 4096: + raise RecurrenceScheduleSharingError("process ID exceeds 4096 UTF-8 bytes") + mask = _single_bit(process_support_mask, "process support mask") + words = tuple( + (mask >> shift) & ((1 << 64) - 1) + for shift in range(0, max(1, mask.bit_length()), 64) + ) + if not isinstance(remap, RecurrenceProcessRemap) or not remap.bijection_digest: + raise RecurrenceScheduleSharingError( + "recurrence process binding requires an authenticated process remap" + ) + if not isinstance(executor_pack, RecurrenceProcessExecutorPack): + raise RecurrenceScheduleSharingError( + "recurrence process binding requires a typed executor pack" + ) + if executor_pack.catalog_executor_count != remap.direct_executor_count: + raise RecurrenceScheduleSharingError( + "recurrence process executor domain disagrees with its remap" + ) + target = executor_pack.target_triple.encode("utf-8") + if len(target) > _MISSING_U32: + raise RecurrenceScheduleSharingError( + "process executor target triple exceeds the u32 wire domain" + ) + counts = ( + len(remap.source_slots), + len(remap.public_flow_ids), + len(remap.physical_sector_ids), + remap.state_template_count, + remap.source_template_count, + remap.direct_executor_count, + remap.parameter_slot_count, + len(remap.state_template_changes), + len(remap.source_template_changes), + len(remap.direct_executor_changes), + len(remap.parameter_slot_changes), + ) + for value in counts: + if value > (1 << 32) - 1: + raise RecurrenceScheduleSharingError( + "recurrence process remap exceeds the u32 binding ABI" + ) + for value, context in ( + (len(words), "support-word count"), + (len(executor_pack.cpu_features), "CPU-feature count"), + (len(executor_pack.executor_ids), "executor-descriptor count"), + ): + if value > _MISSING_U32: + raise RecurrenceScheduleSharingError( + f"recurrence process {context} exceeds the u32 binding ABI" + ) + header = b"".join( + ( + RECURRENCE_PROCESS_BINDING_MAGIC, + struct.pack( + "<8I", + _PROCESS_BINDING_VERSION, + _PROCESS_BINDING_FIXED_SIZE, + len(process), + len(words), + len(target), + len(executor_pack.cpu_features), + len(executor_pack.executor_ids), + executor_pack.catalog_executor_count, + ), + struct.pack( + "<4B", + _DIRECT_BACKEND[executor_pack.backend], + int(executor_pack.portable), + 64, + 0, + ), + struct.pack("<11I", *counts), + bytes.fromhex(_require_sha256(schedule_digest, "schedule digest")), + bytes.fromhex( + _require_sha256(process_semantic_digest, "process semantic digest") + ), + bytes.fromhex( + _require_sha256( + executor_pack.compiled_model_digest, + "compiled-model digest", + ) + ), + bytes.fromhex(executor_pack.recurrence_template_catalog_digest), + bytes.fromhex(executor_pack.prepared_kernel_pack_digest), + bytes.fromhex(executor_pack.direct_template_catalog_digest), + bytes.fromhex(executor_pack.runtime_layout_digest), + bytes.fromhex(_require_sha256(process_digest, "process digest")), + ) + ) + if len(header) != _PROCESS_BINDING_FIXED_SIZE: + raise AssertionError("recurrence process-binding header size drifted") + sparse_changes = ( + remap.state_template_changes, + remap.source_template_changes, + remap.direct_executor_changes, + remap.parameter_slot_changes, + ) + return b"".join( + ( + header, + process, + target, + *( + _encode_wire_text(feature, "target CPU feature") + for feature in executor_pack.cpu_features + ), + struct.pack(f"<{len(words)}Q", *words), + struct.pack(f"<{len(remap.source_slots)}I", *remap.source_slots), + struct.pack( + f"<{len(remap.source_momentum_signs)}i", + *remap.source_momentum_signs, + ), + struct.pack( + f"<{len(remap.source_helicity_signs)}i", + *remap.source_helicity_signs, + ), + struct.pack( + f"<{len(remap.source_state_offsets)}I", + *remap.source_state_offsets, + ), + struct.pack( + f"<{len(remap.source_state_indices)}I", + *remap.source_state_indices, + ), + struct.pack( + f"<{len(remap.public_flow_ids)}I", + *remap.public_flow_ids, + ), + struct.pack( + f"<{len(remap.physical_sector_ids)}I", + *remap.physical_sector_ids, + ), + *( + struct.pack( + f"<{2 * len(changes)}I", + *(value for pair in changes for value in pair), + ) + for changes in sparse_changes + ), + *executor_pack.descriptor_payloads, + ) + ) + + +def _schedule_plain(value: object) -> object: + if is_dataclass(value) and not isinstance(value, type): + result: dict[str, object] = {} + for item in fields(value): + if item.name == "process_id": + result[item.name] = "" + elif item.name in {"process_support_mask", "support_mask"}: + result[item.name] = 1 + else: + result[item.name] = _schedule_plain(getattr(value, item.name)) + return result + if isinstance(value, Mapping): + return { + str(key): _schedule_plain(item) + for key, item in sorted(value.items(), key=lambda row: str(row[0])) + } + if isinstance(value, (tuple, list)): + return [_schedule_plain(item) for item in value] + if isinstance(value, (set, frozenset)): + return [_schedule_plain(item) for item in sorted(value, key=repr)] + if isinstance(value, Path): + return value.as_posix() + return value + + +def _canonical_digest(value: object) -> str: + payload = json.dumps( + value, + allow_nan=False, + ensure_ascii=True, + separators=(",", ":"), + sort_keys=True, + ).encode("ascii") + return hashlib.sha256(payload).hexdigest() + + +def _nonempty_text(value: object, context: str) -> str: + if not isinstance(value, str) or not value: + raise RecurrenceScheduleSharingError(f"{context} must be nonempty text") + return value + + +def _require_sha256(value: object, context: str) -> str: + if not isinstance(value, str) or _SHA256.fullmatch(value) is None: + raise RecurrenceScheduleSharingError( + f"{context} must be a lowercase hexadecimal SHA-256 digest" + ) + return value + + +def _positive_integer(value: object, context: str) -> int: + if isinstance(value, bool) or not isinstance(value, int) or value <= 0: + raise RecurrenceScheduleSharingError(f"{context} must be a positive integer") + return value + + +def _nonnegative_integer(value: object, context: str) -> int: + if isinstance(value, bool) or not isinstance(value, int) or value < 0: + raise RecurrenceScheduleSharingError(f"{context} must be a nonnegative integer") + return value + + +def _single_bit(value: object, context: str) -> int: + result = _positive_integer(value, context) + if result & (result - 1): + raise RecurrenceScheduleSharingError(f"{context} must contain one bit") + return result + + +def _permutation(values: Sequence[int], context: str) -> tuple[int, ...]: + result = tuple(_nonnegative_integer(value, f"{context} value") for value in values) + if not _is_permutation(result): + raise RecurrenceScheduleSharingError( + f"{context} must be a dense zero-based permutation" + ) + return result + + +def _is_permutation(values: Sequence[int]) -> bool: + return tuple(sorted(values)) == tuple(range(len(values))) + + +def _inverse_permutation(values: Sequence[int]) -> tuple[int, ...]: + permutation = _permutation(values, "permutation") + inverse = [0] * len(permutation) + for source, target in enumerate(permutation): + inverse[target] = source + return tuple(inverse) + + +def _signs( + values: Sequence[int], + expected_count: int, + context: str, +) -> tuple[int, ...]: + result = tuple(values) + if len(result) != expected_count or any(value not in {-1, 1} for value in result): + raise RecurrenceScheduleSharingError( + f"{context} must contain {expected_count} signs in {{-1, 1}}" + ) + return result + + +def _ragged_permutations( + offsets: Sequence[int], + values: Sequence[int], + row_count: int, + context: str, +) -> tuple[tuple[int, ...], tuple[int, ...]]: + checked_offsets = tuple( + _nonnegative_integer(value, f"{context} offset") for value in offsets + ) + checked_values = tuple( + _nonnegative_integer(value, f"{context} value") for value in values + ) + if ( + len(checked_offsets) != row_count + 1 + or not checked_offsets + or checked_offsets[0] != 0 + or checked_offsets[-1] != len(checked_values) + or any(left > right for left, right in itertools.pairwise(checked_offsets)) + ): + raise RecurrenceScheduleSharingError( + f"{context} offsets do not partition {row_count} rows" + ) + for row, (start, stop) in enumerate(itertools.pairwise(checked_offsets)): + if not _is_permutation(checked_values[start:stop]): + raise RecurrenceScheduleSharingError( + f"{context} row {row} is not a dense zero-based permutation" + ) + return checked_offsets, checked_values + + +def _sparse_permutation( + changes: Sequence[tuple[int, int]], + count: int, + context: str, +) -> tuple[tuple[int, int], ...]: + size = _nonnegative_integer(count, f"{context} domain size") + result = tuple(changes) + if result != tuple(sorted(result)): + raise RecurrenceScheduleSharingError(f"{context} changes must be ordered") + sources: set[int] = set() + mapping = list(range(size)) + for source, target in result: + source = _nonnegative_integer(source, f"{context} source") + target = _nonnegative_integer(target, f"{context} target") + if source >= size or target >= size: + raise RecurrenceScheduleSharingError( + f"{context} change is outside its declared domain" + ) + if source == target: + raise RecurrenceScheduleSharingError( + f"{context} must omit identity changes" + ) + if source in sources: + raise RecurrenceScheduleSharingError(f"{context} repeats source {source}") + sources.add(source) + mapping[source] = target + if not _is_permutation(mapping): + raise RecurrenceScheduleSharingError( + f"{context} changes do not define a bijection" + ) + return result + + +def _sparse_mapping( + count: int, + changes: Sequence[tuple[int, int]], +) -> dict[str, object]: + return { + "count": _nonnegative_integer(count, "sparse mapping count"), + "changes": [list(pair) for pair in changes], + } + + +def _template_count( + logical: RecurrenceBuilderLogicalInputV1, + kind: str, +) -> int: + identifiers = tuple( + row.template_id + for row in logical.semantic_template_references + if row.kind == kind + ) + if not identifiers: + raise RecurrenceScheduleSharingError( + f"recurrence process has no {kind!r} template domain" + ) + if identifiers != tuple(range(len(identifiers))): + raise RecurrenceScheduleSharingError( + f"recurrence {kind!r} template IDs are not dense and ordered" + ) + return len(identifiers) + + +def _physical_sector_domain_count( + logical: RecurrenceBuilderLogicalInputV1, +) -> int: + if logical.layout == "topology-replay": + return len(logical.public_flows) + return len(logical.physical_sectors) + + +__all__ = [ + "RECURRENCE_PROCESS_BINDING_ABI", + "RECURRENCE_PROCESS_BINDING_MAGIC", + "RECURRENCE_SCHEDULE_INDEX_PATH", + "RECURRENCE_SCHEDULE_SHARING_KIND", + "RECURRENCE_SCHEDULE_SHARING_SCHEMA_VERSION", + "RecurrenceProcessBinding", + "RecurrenceProcessExecutorPack", + "RecurrenceProcessRemap", + "RecurrenceScheduleLoweringCache", + "RecurrenceScheduleSharingError", + "RecurrenceScheduleSharingPlan", + "RecurrenceSharedLoweringResult", + "RecurrenceSharedSchedule", + "build_recurrence_process_executor_pack", + "encode_recurrence_process_binding", + "exact_recurrence_process_bijection", + "intern_recurrence_schedules", + "recurrence_helicity_selector_schedule_digest", + "recurrence_native_schedule_semantic_digest", + "recurrence_schedule_semantic_digest", +] diff --git a/pyamplicol/src/pyamplicol/generation/recurrence_template_columnar.py b/pyamplicol/src/pyamplicol/generation/recurrence_template_columnar.py new file mode 100644 index 0000000000..aaa151445c --- /dev/null +++ b/pyamplicol/src/pyamplicol/generation/recurrence_template_columnar.py @@ -0,0 +1,1801 @@ +# SPDX-License-Identifier: 0BSD +"""Compact Python-to-Rust projection of recurrence-template-v1. + +The prepared recurrence catalog is model-wide, while +``recurrence_columnar`` is process-specific. Keeping the two authenticated +inputs separate lets a process set reuse one decoded model catalog without +copying it into every process builder input. +""" + +from __future__ import annotations + +import hashlib +import operator +from collections.abc import Iterable, Sequence +from dataclasses import dataclass +from typing import Any, Final + +import numpy as np + +from ..models.recurrence_template import ( + ExactComplexRationalV1, + RecurrenceTemplateCatalog, +) +from .recurrence_columnar import ( + MISSING_U32, + RecurrenceColumn, + RecurrenceColumnarInputError, + RecurrenceColumnarTable, +) + +RECURRENCE_TEMPLATE_INPUT_ABI: Final = "pyamplicol-recurrence-template-input-v1" +RECURRENCE_TEMPLATE_INPUT_SCHEMA_VERSION: Final = 2 + +_U8 = np.dtype("u1") +_U32 = np.dtype(" None: + if self.abi != RECURRENCE_TEMPLATE_INPUT_ABI: + raise RecurrenceColumnarInputError( + f"unsupported recurrence template input ABI {self.abi!r}" + ) + _require_sha256(self.catalog_digest, "recurrence catalog digest") + _require_sha256(self.compiled_model_digest, "compiled-model digest") + _require_sha256(self.prepared_kernel_pack_digest, "prepared kernel pack digest") + names = tuple(table.name for table in self.tables) + if names != tuple(sorted(names)) or len(names) != len(set(names)): + raise RecurrenceColumnarInputError( + "recurrence template tables must be uniquely named and sorted" + ) + + @property + def canonical_digest(self) -> str: + digest = hashlib.sha256() + _hash_text(digest, self.abi) + digest.update(bytes.fromhex(self.catalog_digest)) + digest.update(bytes.fromhex(self.compiled_model_digest)) + digest.update(bytes.fromhex(self.prepared_kernel_pack_digest)) + digest.update(len(self.tables).to_bytes(8, "little")) + for table in self.tables: + _hash_text(digest, table.name) + digest.update(table.row_count.to_bytes(8, "little")) + digest.update(len(table.columns).to_bytes(4, "little")) + for column in table.columns: + _hash_text(digest, column.name) + _hash_text(digest, column.values.dtype.str) + digest.update(len(column.values.shape).to_bytes(1, "little")) + for dimension in column.values.shape: + digest.update(int(dimension).to_bytes(8, "little")) + digest.update(column.values.tobytes(order="C")) + return digest.hexdigest() + + @property + def digest(self) -> str: + return self.canonical_digest + + +class _StringCatalog: + def __init__(self, values: Iterable[str]) -> None: + self.values = tuple(sorted(set(values))) + self._ids = {value: index for index, value in enumerate(self.values)} + + def id(self, value: str | None) -> int: + if value is None: + return MISSING_U32 + return self._ids[value] + + +class _DigestCatalog: + def __init__(self, values: Iterable[str]) -> None: + self.values = tuple(sorted(set(values))) + self._ids = {value: index for index, value in enumerate(self.values)} + + def id(self, value: str | None) -> int: + if value is None: + return MISSING_U32 + return self._ids[value] + + +class _FactorCatalog: + def __init__(self, values: Iterable[ExactComplexRationalV1]) -> None: + by_key = {_factor_key(value): value for value in values} + self.values = tuple(by_key[key] for key in sorted(by_key)) + self._ids = { + _factor_key(value): index for index, value in enumerate(self.values) + } + + def id(self, value: ExactComplexRationalV1 | None) -> int: + if value is None: + return MISSING_U32 + return self._ids[_factor_key(value)] + + +class _SequenceCatalog: + def __init__(self, values: Iterable[Sequence[int]]) -> None: + self.values = tuple( + sorted(set(tuple(int(item) for item in value) for value in values)) + ) + self._ids = {value: index for index, value in enumerate(self.values)} + + def id(self, values: Sequence[int]) -> int: + return self._ids[tuple(int(value) for value in values)] + + +class _QuantumNumberFlowCatalog: + def __init__(self, values: Iterable[Sequence[tuple[str, str]]]) -> None: + self.values = tuple( + sorted( + set( + tuple((str(name), str(expression)) for name, expression in value) + for value in values + ) + ) + ) + self._ids = {value: index for index, value in enumerate(self.values)} + + def id(self, values: Sequence[tuple[str, str]]) -> int: + key = tuple((str(name), str(expression)) for name, expression in values) + return self._ids[key] + + +class _CouplingOrderCatalog: + def __init__(self, values: Iterable[Sequence[tuple[str, int]]]) -> None: + self.values = tuple( + sorted( + set( + tuple((str(name), int(power)) for name, power in value) + for value in values + ) + ) + ) + self._ids = {value: index for index, value in enumerate(self.values)} + + def id(self, values: Sequence[tuple[str, int]]) -> int: + key = tuple((str(name), int(power)) for name, power in values) + return self._ids[key] + + +def build_recurrence_template_input_v1( + catalog: RecurrenceTemplateCatalog, +) -> RecurrenceTemplateInputV1: + """Flatten a validated semantic catalog into deterministic primitive columns.""" + + if not isinstance(catalog, RecurrenceTemplateCatalog): + raise TypeError("recurrence template extraction requires a validated catalog") + _validate_i128_factors(catalog) + + sections = { + "parameters": tuple(catalog.parameters), + "current_states": tuple(catalog.current_states), + "sources": tuple(catalog.sources), + "quantum_flows": tuple(catalog.quantum_flows), + "contact_orbit_certificates": tuple(catalog.contact_orbit_certificates), + "contact_orbit_steps": tuple(catalog.contact_orbit_steps), + "transitions": tuple(catalog.transitions), + "propagators": tuple(catalog.propagators), + "closures": tuple(catalog.closures), + "color_contractions": tuple(catalog.color_contractions), + "symmetry_proofs": tuple(catalog.symmetry_proofs), + "runtime_helicity_contracts": tuple(catalog.runtime_helicity_contracts), + "evaluator_bindings": tuple(catalog.evaluator_bindings), + } + ids = { + name: { + _record_identity(record, name): index + for index, record in enumerate(records) + } + for name, records in sections.items() + } + + strings = _StringCatalog(_all_strings(catalog)) + digests = _DigestCatalog(_all_digests(catalog)) + factors = _FactorCatalog(_all_factors(catalog)) + flavour_flows = _SequenceCatalog(_all_flavour_flows(catalog)) + quantum_number_flows = _QuantumNumberFlowCatalog(_all_quantum_number_flows(catalog)) + u32_sequences = _SequenceCatalog( + _all_u32_sequences( + catalog, + ids, + strings, + digests, + factors, + flavour_flows, + quantum_number_flows, + ) + ) + i32_sequences = _SequenceCatalog(_all_i32_sequences(catalog)) + coupling_orders = _CouplingOrderCatalog(_all_coupling_orders(catalog)) + string_ranges, string_bytes = _string_tables(strings) + + tables = [ + _header_table(catalog, strings, digests, sections), + _catalog_ranges("coupling_order", coupling_orders.values, 2), + _coupling_order_terms(coupling_orders, strings), + _contact_orbit_certificates_table( + catalog, strings, digests, factors, u32_sequences + ), + _contact_orbit_steps_table( + catalog, ids, strings, digests, factors, u32_sequences, i32_sequences + ), + _current_states_table(catalog, ids, strings, digests, u32_sequences), + _digest_table(digests), + _evaluator_bindings_table(catalog, ids, strings, digests, u32_sequences), + _exact_factor_table(factors, strings), + *_flatten_sequence_tables("flavour_flow", flavour_flows, _I32), + *_flatten_sequence_tables("i32_sequence", i32_sequences, _I32), + _parameters_table(catalog, ids, strings, digests, factors, u32_sequences), + _propagators_table(catalog, ids, strings, digests), + _quantum_flows_table( + catalog, + ids, + strings, + digests, + factors, + u32_sequences, + i32_sequences, + coupling_orders, + flavour_flows, + quantum_number_flows, + ), + _quantum_number_flow_ranges(quantum_number_flows), + _quantum_number_flow_terms(quantum_number_flows, strings), + *_runtime_helicity_tables(catalog, ids, strings, digests, factors), + _sources_table( + catalog, + ids, + strings, + digests, + flavour_flows, + quantum_number_flows, + u32_sequences, + ), + string_ranges, + string_bytes, + _symmetry_proofs_table(catalog, strings, digests, factors, u32_sequences), + _transitions_table( + catalog, + ids, + strings, + digests, + factors, + u32_sequences, + coupling_orders, + ), + _closures_table( + catalog, + ids, + strings, + digests, + factors, + u32_sequences, + coupling_orders, + ), + _color_contractions_table(catalog, strings, digests, factors, i32_sequences), + _lc_color_transition_witnesses_table( + catalog, + strings, + digests, + factors, + u32_sequences, + ), + _color_nc_terms_table(catalog, factors), + *_flatten_sequence_tables("u32_sequence", u32_sequences, _U32), + ] + tables.sort(key=lambda table: table.name) + return RecurrenceTemplateInputV1( + abi=RECURRENCE_TEMPLATE_INPUT_ABI, + catalog_digest=catalog.header.catalog_digest, + compiled_model_digest=catalog.header.compiled_model_digest, + prepared_kernel_pack_digest=catalog.header.prepared_kernel_pack_digest, + tables=tuple(tables), + ) + + +def _record_identity(record: object, section: str) -> str: + attribute = "resolver_key" if section == "evaluator_bindings" else "template_id" + return str(getattr(record, attribute)) + + +def _all_strings(catalog: RecurrenceTemplateCatalog) -> Iterable[str]: + yield RECURRENCE_TEMPLATE_INPUT_ABI + yield catalog.header.abi + yield catalog.header.canonicalization_abi + yield catalog.header.exact_scalar_abi + for record in catalog.parameters: + yield from ( + record.template_id, + record.name, + *record.dependency_parameter_ids, + ) + for record in catalog.current_states: + yield from ( + record.template_id, + record.species_id, + record.basis, + record.lc_color_shape_kind, + *record.tensor_ordering, + ) + yield from _present( + record.auxiliary_kind, record.mass_parameter_id, record.width_parameter_id + ) + for record in catalog.sources: + yield from ( + record.template_id, + record.crossing, + record.wavefunction_family, + record.evaluator_resolver_key, + record.lc_color_seed.output_shape_kind, + ) + for key, value in record.lc_color_seed.provenance: + yield key + yield value + for name, expression in record.quantum_number_flow: + yield name + yield expression + for record in catalog.quantum_flows: + yield record.template_id + yield record.flavour_flow_operation + yield record.quantum_number_flow_operation + for flow in ( + *record.input_quantum_number_flows, + record.result_quantum_number_flow, + ): + for name, expression in flow: + yield name + yield expression + yield from (name for name, _ in record.coupling_orders) + for record in catalog.contact_orbit_certificates: + yield from ( + record.template_id, + record.algorithm, + record.vertex, + *record.particles, + record.evaluator_class, + ) + for record in catalog.contact_orbit_steps: + yield from ( + record.template_id, + record.certificate_template_id, + record.stage, + ) + for record in catalog.transitions: + yield from ( + record.template_id, + record.evaluator_resolver_key, + *record.momentum_convention, + record.output_factor_source, + record.equivalence_class, + record.output_projection, + ) + yield from (name for name, _ in record.coupling_orders) + for record in catalog.propagators: + yield record.template_id + yield from _present(record.evaluator_resolver_key, record.gauge) + for record in catalog.closures: + yield from ( + record.template_id, + record.evaluator_resolver_key, + record.output_factor_source, + record.equivalence_class, + record.projection, + record.chirality_relation, + ) + yield from _present(record.metric_signature) + yield from (name for name, _ in record.coupling_orders) + for record in catalog.color_contractions: + yield from (record.template_id, record.rule_kind) + for witness in record.transition_witnesses: + yield from witness.input_shape_kinds + yield from _present(witness.result_shape_kind) + for key, value in witness.provenance: + yield key + yield value + for record in catalog.symmetry_proofs: + yield from ( + record.template_id, + record.proof_algorithm, + *record.subject_template_ids, + ) + for record in catalog.runtime_helicity_contracts: + yield from ( + record.template_id, + record.full_state_template_id, + record.proof_algorithm, + ) + for variant in record.variants: + yield variant.source_template_id + yield variant.source_state_template_id + for record in catalog.evaluator_bindings: + yield from ( + record.resolver_key, + *record.input_layout, + *record.output_layout, + *record.semantic_template_ids, + ) + yield from _present(record.runtime_template) + for factor in _all_factors(catalog): + yield from (str(value) for value in _factor_key(factor)) + + +def _all_digests(catalog: RecurrenceTemplateCatalog) -> Iterable[str]: + yield catalog.header.catalog_digest + yield catalog.header.compiled_model_digest + yield catalog.header.prepared_kernel_pack_digest + for records in ( + catalog.parameters, + catalog.current_states, + catalog.sources, + catalog.quantum_flows, + catalog.contact_orbit_certificates, + catalog.contact_orbit_steps, + catalog.transitions, + catalog.propagators, + catalog.closures, + catalog.color_contractions, + catalog.symmetry_proofs, + catalog.runtime_helicity_contracts, + catalog.evaluator_bindings, + ): + for record in records: + yield record.semantic_digest + for record in catalog.parameters: + yield from _present(record.exact_expression_digest) + for record in catalog.sources: + yield record.wavefunction_expression_digest + yield record.lc_color_seed.proof_digest + for record in catalog.quantum_flows: + yield record.predicate_digest + for record in catalog.propagators: + yield from _present( + record.numerator_expression_digest, record.denominator_expression_digest + ) + for record in catalog.color_contractions: + yield record.expression_digest + yield from (witness.proof_digest for witness in record.transition_witnesses) + for record in catalog.symmetry_proofs: + yield record.witness_digest + yield from record.expression_digests + for record in catalog.runtime_helicity_contracts: + yield record.proof_digest + yield from (variant.proof_digest for variant in record.variants) + for record in catalog.evaluator_bindings: + yield record.callable_signature + yield from record.exact_expression_digests + + +def _all_factors( + catalog: RecurrenceTemplateCatalog, +) -> Iterable[ExactComplexRationalV1]: + for record in catalog.parameters: + if record.default_value is not None: + yield record.default_value + for record in catalog.transitions: + yield record.binding_coupling + yield record.exact_factor + if record.input_exchange_factor is not None: + yield record.input_exchange_factor + for record in catalog.contact_orbit_certificates: + yield record.reconstruction_factor + for record in catalog.contact_orbit_steps: + yield record.reconstruction_factor + for record in catalog.quantum_flows: + yield record.exact_coupling + for record in catalog.closures: + yield record.binding_coupling + yield record.exact_factor + if record.input_exchange_factor is not None: + yield record.input_exchange_factor + yield from record.component_coefficients + for record in catalog.color_contractions: + yield record.exact_coefficient + yield from (factor for _, factor in record.nc_polynomial) + yield from (witness.exact_factor for witness in record.transition_witnesses) + for record in catalog.symmetry_proofs: + yield record.exact_phase + for record in catalog.runtime_helicity_contracts: + for variant in record.variants: + yield from variant.embedding_factors + + +def _all_coupling_orders( + catalog: RecurrenceTemplateCatalog, +) -> Iterable[Sequence[tuple[str, int]]]: + yield () + yield from (record.coupling_orders for record in catalog.quantum_flows) + yield from (record.coupling_orders for record in catalog.transitions) + yield from (record.coupling_orders for record in catalog.closures) + + +def _all_u32_sequences( + catalog: RecurrenceTemplateCatalog, + ids: dict[str, dict[str, int]], + strings: _StringCatalog, + digests: _DigestCatalog, + factors: _FactorCatalog, + flavour_flows: _SequenceCatalog, + quantum_number_flows: _QuantumNumberFlowCatalog, +) -> Iterable[Sequence[int]]: + yield () + for record in catalog.parameters: + yield tuple( + ids["parameters"][value] for value in record.dependency_parameter_ids + ) + for record in catalog.current_states: + yield tuple(strings.id(value) for value in record.tensor_ordering) + for record in catalog.sources: + yield tuple( + strings.id(item) + for pair in record.lc_color_seed.provenance + for item in pair + ) + for record in catalog.quantum_flows: + yield tuple( + ids["current_states"][value] for value in record.input_state_template_ids + ) + yield tuple(flavour_flows.id(value) for value in record.input_flavour_flows) + yield tuple( + quantum_number_flows.id(value) + for value in record.input_quantum_number_flows + ) + for record in catalog.transitions: + yield tuple( + ids["current_states"][value] for value in record.input_state_template_ids + ) + yield record.canonical_input_order + yield tuple(strings.id(value) for value in record.momentum_convention) + yield tuple(ids["parameters"][value] for value in record.coupling_parameter_ids) + yield tuple( + ids["contact_orbit_steps"][value] + for value in record.contact_orbit_step_template_ids + ) + yield tuple( + digests.id(value) + for value in record.contact_orbit_step_semantic_digests + ) + for record in catalog.contact_orbit_certificates: + yield tuple(strings.id(value) for value in record.particles) + yield record.physical_leg_equivalence_classes + for record in catalog.contact_orbit_steps: + yield record.left_covered_legs + yield record.right_covered_legs + for record in catalog.closures: + yield tuple( + ids["current_states"][value] for value in record.input_state_template_ids + ) + yield record.canonical_input_order + yield tuple(ids["parameters"][value] for value in record.coupling_parameter_ids) + yield tuple( + ids["quantum_flows"][value] + for value in record.eligible_quantum_flow_template_ids + ) + yield tuple(factors.id(value) for value in record.component_coefficients) + for record in catalog.color_contractions: + for witness in record.transition_witnesses: + yield tuple( + value + for pairing in witness.input_port_pairings + for port in pairing + for value in port + ) + yield tuple( + value for port in witness.result_port_bindings for value in port + ) + yield tuple( + strings.id(item) for pair in witness.provenance for item in pair + ) + for record in catalog.symmetry_proofs: + yield tuple(strings.id(value) for value in record.subject_template_ids) + yield record.input_permutation + yield tuple(digests.id(value) for value in record.expression_digests) + for record in catalog.evaluator_bindings: + yield tuple( + ids["current_states"][value] for value in record.input_state_template_ids + ) + yield tuple(strings.id(value) for value in record.input_layout) + yield tuple(strings.id(value) for value in record.output_layout) + yield tuple(digests.id(value) for value in record.exact_expression_digests) + yield tuple(strings.id(value) for value in record.semantic_template_ids) + + +def _all_i32_sequences(catalog: RecurrenceTemplateCatalog) -> Iterable[Sequence[int]]: + yield () + yield from (record.input_spin_states for record in catalog.quantum_flows) + yield from (record.input_representations for record in catalog.color_contractions) + yield from ( + record.source_particle_legs for record in catalog.contact_orbit_steps + ) + + +def _all_flavour_flows( + catalog: RecurrenceTemplateCatalog, +) -> Iterable[Sequence[int]]: + yield from (record.flavour_flow for record in catalog.sources) + for record in catalog.quantum_flows: + yield from record.input_flavour_flows + yield record.result_flavour_flow + + +def _all_quantum_number_flows( + catalog: RecurrenceTemplateCatalog, +) -> Iterable[Sequence[tuple[str, str]]]: + yield from (record.quantum_number_flow for record in catalog.sources) + for record in catalog.quantum_flows: + yield from record.input_quantum_number_flows + yield record.result_quantum_number_flow + + +def _header_table(catalog, strings, digests, sections): + names = ( + "schema_version", + "abi_string_id", + "canonicalization_abi_string_id", + "exact_scalar_abi_string_id", + "compiled_model_digest_id", + "prepared_kernel_pack_digest_id", + "catalog_digest_id", + "parameter_count", + "current_state_count", + "source_count", + "quantum_flow_count", + "contact_orbit_certificate_count", + "contact_orbit_step_count", + "transition_count", + "propagator_count", + "closure_count", + "color_contraction_count", + "symmetry_proof_count", + "runtime_helicity_contract_count", + "evaluator_binding_count", + ) + values = ( + RECURRENCE_TEMPLATE_INPUT_SCHEMA_VERSION, + strings.id(catalog.header.abi), + strings.id(catalog.header.canonicalization_abi), + strings.id(catalog.header.exact_scalar_abi), + digests.id(catalog.header.compiled_model_digest), + digests.id(catalog.header.prepared_kernel_pack_digest), + digests.id(catalog.header.catalog_digest), + *(len(sections[name]) for name in sections), + ) + return _table( + "catalog_header", + { + name: _array([value], _U32) + for name, value in zip(names, values, strict=True) + }, + ) + + +def _parameters_table(catalog, ids, strings, digests, factors, sequences): + rows = [] + for index, record in enumerate(catalog.parameters): + rows.append( + ( + index, + strings.id(record.template_id), + strings.id(record.name), + _PARAMETER_KIND[record.parameter_kind], + _PARAMETER_VALUE_TYPE[record.value_type], + int(record.mutable), + factors.id(record.default_value), + digests.id(record.exact_expression_digest), + sequences.id( + tuple( + ids["parameters"][value] + for value in record.dependency_parameter_ids + ) + ), + MISSING_U32 + if record.prepared_parameter_id is None + else record.prepared_parameter_id, + digests.id(record.semantic_digest), + ) + ) + return _rows( + "parameters", + rows, + ( + ("id", _U32), + ("template_string_id", _U32), + ("name_string_id", _U32), + ("kind", _U8), + ("value_type", _U8), + ("mutable", _U8), + ("default_factor_id", _U32), + ("exact_expression_digest_id", _U32), + ("dependency_sequence_id", _U32), + ("prepared_parameter_id", _U32), + ("semantic_digest_id", _U32), + ), + ) + + +def _current_states_table(catalog, ids, strings, digests, sequences): + del ids + rows = [] + parameter_ids = { + record.template_id: i for i, record in enumerate(catalog.parameters) + } + for index, record in enumerate(catalog.current_states): + rows.append( + ( + index, + strings.id(record.template_id), + record.particle_id, + record.anti_particle_id, + strings.id(record.species_id), + _ORIENTATION[record.orientation], + _STATISTICS[record.statistics], + record.color_representation, + strings.id(record.basis), + sequences.id( + tuple(strings.id(value) for value in record.tensor_ordering) + ), + record.dimension, + record.chirality, + strings.id(record.lc_color_shape_kind), + strings.id(record.auxiliary_kind), + _optional_reference(record.mass_parameter_id, parameter_ids), + _optional_reference(record.width_parameter_id, parameter_ids), + digests.id(record.semantic_digest), + ) + ) + return _rows( + "current_states", + rows, + ( + ("id", _U32), + ("template_string_id", _U32), + ("particle_id", _I32), + ("anti_particle_id", _I32), + ("species_string_id", _U32), + ("orientation", _U8), + ("statistics", _U8), + ("color_representation", _I32), + ("basis_string_id", _U32), + ("tensor_ordering_sequence_id", _U32), + ("dimension", _U32), + ("chirality", _I32), + ("lc_color_shape_string_id", _U32), + ("auxiliary_kind_string_id", _U32), + ("mass_parameter_id", _U32), + ("width_parameter_id", _U32), + ("semantic_digest_id", _U32), + ), + ) + + +def _runtime_helicity_tables(catalog, ids, strings, digests, factors): + contract_rows = [] + variant_rows = [] + embedding_rows = [] + projection_rows = [] + variant_offset = 0 + embedding_offset = 0 + projection_offset = 0 + for contract_id, contract in enumerate(catalog.runtime_helicity_contracts): + contract_rows.append( + ( + contract_id, + strings.id(contract.template_id), + ids["current_states"][contract.full_state_template_id], + variant_offset, + len(contract.variants), + strings.id(contract.proof_algorithm), + digests.id(contract.proof_digest), + digests.id(contract.semantic_digest), + ) + ) + for variant in contract.variants: + variant_id = len(variant_rows) + variant_rows.append( + ( + variant_id, + contract_id, + ids["sources"][variant.source_template_id], + ids["current_states"][variant.source_state_template_id], + embedding_offset, + len(variant.embedding_source_components), + projection_offset, + len(variant.projection_full_components), + digests.id(variant.proof_digest), + ) + ) + for full_component, (source_component, factor) in enumerate( + zip( + variant.embedding_source_components, + variant.embedding_factors, + strict=True, + ) + ): + embedding_rows.append( + ( + variant_id, + full_component, + MISSING_U32 if source_component is None else source_component, + factors.id(factor), + ) + ) + for source_component, full_component in enumerate( + variant.projection_full_components + ): + projection_rows.append((variant_id, source_component, full_component)) + embedding_offset += len(variant.embedding_source_components) + projection_offset += len(variant.projection_full_components) + variant_offset += len(contract.variants) + return ( + _rows( + "runtime_helicity_contracts", + contract_rows, + ( + ("id", _U32), + ("template_string_id", _U32), + ("full_state_template_id", _U32), + ("variant_offset", _U32), + ("variant_count", _U32), + ("proof_algorithm_string_id", _U32), + ("proof_digest_id", _U32), + ("semantic_digest_id", _U32), + ), + ), + _rows( + "runtime_helicity_variants", + variant_rows, + ( + ("id", _U32), + ("contract_id", _U32), + ("source_template_id", _U32), + ("source_state_template_id", _U32), + ("embedding_offset", _U32), + ("embedding_count", _U32), + ("projection_offset", _U32), + ("projection_count", _U32), + ("proof_digest_id", _U32), + ), + ), + _rows( + "runtime_helicity_embeddings", + embedding_rows, + ( + ("variant_id", _U32), + ("full_component", _U32), + ("source_component", _U32), + ("factor_id", _U32), + ), + ), + _rows( + "runtime_helicity_projections", + projection_rows, + ( + ("variant_id", _U32), + ("source_component", _U32), + ("full_component", _U32), + ), + ), + ) + + +def _sources_table( + catalog, + ids, + strings, + digests, + flavour_flows, + quantum_number_flows, + sequences, +): + parameter_ids = ids["parameters"] + rows = [] + for index, record in enumerate(catalog.sources): + rows.append( + ( + index, + strings.id(record.template_id), + ids["current_states"][record.state_template_id], + strings.id(record.crossing), + strings.id(record.wavefunction_family), + record.helicity, + record.spin_state, + flavour_flows.id(record.flavour_flow), + quantum_number_flows.id(record.quantum_number_flow), + _LC_COLOR_SOURCE_SEED_OPERATION[record.lc_color_seed.operation], + strings.id(record.lc_color_seed.output_shape_kind), + ( + 255 + if record.lc_color_seed.component_kind is None + else _LC_COLOR_COMPONENT_KIND[record.lc_color_seed.component_kind] + ), + _LC_COLOR_COMPONENT_ROLE[record.lc_color_seed.component_role], + digests.id(record.lc_color_seed.proof_digest), + sequences.id( + tuple( + strings.id(item) + for pair in record.lc_color_seed.provenance + for item in pair + ) + ), + digests.id(record.wavefunction_expression_digest), + ids["evaluator_bindings"][record.evaluator_resolver_key], + _optional_reference(record.mass_parameter_id, parameter_ids), + _optional_reference(record.width_parameter_id, parameter_ids), + digests.id(record.semantic_digest), + ) + ) + return _rows( + "sources", + rows, + ( + ("id", _U32), + ("template_string_id", _U32), + ("state_template_id", _U32), + ("crossing_string_id", _U32), + ("wavefunction_family_string_id", _U32), + ("helicity", _I32), + ("spin_state", _I32), + ("flavour_flow_id", _U32), + ("quantum_number_flow_id", _U32), + ("lc_color_seed_operation", _U8), + ("lc_color_seed_shape_string_id", _U32), + ("lc_color_seed_component_kind", _U8), + ("lc_color_seed_component_role", _U8), + ("lc_color_seed_proof_digest_id", _U32), + ("lc_color_seed_provenance_sequence_id", _U32), + ("wavefunction_expression_digest_id", _U32), + ("evaluator_binding_id", _U32), + ("mass_parameter_id", _U32), + ("width_parameter_id", _U32), + ("semantic_digest_id", _U32), + ), + ) + + +def _quantum_flows_table( + catalog, + ids, + strings, + digests, + factors, + u32_sequences, + i32_sequences, + coupling_orders, + flavour_flows, + quantum_number_flows, +): + rows = [] + for index, record in enumerate(catalog.quantum_flows): + rows.append( + ( + index, + strings.id(record.template_id), + u32_sequences.id( + tuple( + ids["current_states"][value] + for value in record.input_state_template_ids + ) + ), + i32_sequences.id(record.input_spin_states), + u32_sequences.id( + tuple( + flavour_flows.id(value) for value in record.input_flavour_flows + ) + ), + u32_sequences.id( + tuple( + quantum_number_flows.id(value) + for value in record.input_quantum_number_flows + ) + ), + strings.id(record.flavour_flow_operation), + strings.id(record.quantum_number_flow_operation), + coupling_orders.id(record.coupling_orders), + ids["current_states"][record.result_state_template_id], + record.result_spin_state, + flavour_flows.id(record.result_flavour_flow), + quantum_number_flows.id(record.result_quantum_number_flow), + factors.id(record.exact_coupling), + digests.id(record.predicate_digest), + digests.id(record.semantic_digest), + ) + ) + return _rows( + "quantum_flows", + rows, + ( + ("id", _U32), + ("template_string_id", _U32), + ("input_state_sequence_id", _U32), + ("input_spin_sequence_id", _U32), + ("input_flavour_sequence_id", _U32), + ("input_quantum_sequence_id", _U32), + ("flavour_flow_operation_string_id", _U32), + ("quantum_number_flow_operation_string_id", _U32), + ("coupling_order_set_id", _U32), + ("result_state_template_id", _U32), + ("result_spin_state", _I32), + ("result_flavour_flow_id", _U32), + ("result_quantum_number_flow_id", _U32), + ("exact_coupling_factor_id", _U32), + ("predicate_digest_id", _U32), + ("semantic_digest_id", _U32), + ), + ) + + +def _transitions_table( + catalog, ids, strings, digests, factors, sequences, coupling_orders +): + rows = [] + for index, record in enumerate(catalog.transitions): + rows.append( + ( + index, + strings.id(record.template_id), + sequences.id( + tuple( + ids["current_states"][value] + for value in record.input_state_template_ids + ) + ), + ids["current_states"][record.result_state_template_id], + ids["quantum_flows"][record.quantum_flow_template_id], + ids["evaluator_bindings"][record.evaluator_resolver_key], + sequences.id(record.canonical_input_order), + sequences.id( + tuple(strings.id(value) for value in record.momentum_convention) + ), + sequences.id( + tuple( + ids["parameters"][value] + for value in record.coupling_parameter_ids + ) + ), + coupling_orders.id(record.coupling_orders), + ids["color_contractions"][record.color_contraction_template_id], + factors.id(record.binding_coupling), + factors.id(record.exact_factor), + _OUTPUT_FACTOR_SOURCE[record.output_factor_source], + strings.id(record.equivalence_class), + factors.id(record.input_exchange_factor), + strings.id(record.output_projection), + sequences.id( + tuple( + ids["contact_orbit_steps"][value] + for value in record.contact_orbit_step_template_ids + ) + ), + sequences.id( + tuple( + digests.id(value) + for value in record.contact_orbit_step_semantic_digests + ) + ), + digests.id(record.semantic_digest), + ) + ) + return _rows( + "transitions", + rows, + ( + ("id", _U32), + ("template_string_id", _U32), + ("input_state_sequence_id", _U32), + ("result_state_template_id", _U32), + ("quantum_flow_template_id", _U32), + ("evaluator_binding_id", _U32), + ("canonical_input_order_sequence_id", _U32), + ("momentum_convention_sequence_id", _U32), + ("coupling_parameter_sequence_id", _U32), + ("coupling_order_set_id", _U32), + ("color_contraction_template_id", _U32), + ("binding_coupling_factor_id", _U32), + ("exact_factor_id", _U32), + ("output_factor_source", _U8), + ("equivalence_class_string_id", _U32), + ("input_exchange_factor_id", _U32), + ("output_projection_string_id", _U32), + ("contact_orbit_step_sequence_id", _U32), + ("contact_orbit_step_semantic_digest_sequence_id", _U32), + ("semantic_digest_id", _U32), + ), + ) + + +def _contact_orbit_certificates_table( + catalog, strings, digests, factors, sequences +): + rows = [] + for index, record in enumerate(catalog.contact_orbit_certificates): + rows.append( + ( + index, + strings.id(record.template_id), + strings.id(record.algorithm), + record.algorithm_version, + record.term_id, + strings.id(record.vertex), + sequences.id(tuple(strings.id(value) for value in record.particles)), + strings.id(record.evaluator_class), + sequences.id(record.physical_leg_equivalence_classes), + factors.id(record.reconstruction_factor), + digests.id(record.semantic_digest), + ) + ) + return _rows( + "contact_orbit_certificates", + rows, + ( + ("id", _U32), + ("template_string_id", _U32), + ("algorithm_string_id", _U32), + ("algorithm_version", _U32), + ("term_id", _U32), + ("vertex_string_id", _U32), + ("particle_string_sequence_id", _U32), + ("evaluator_class_string_id", _U32), + ("physical_leg_equivalence_sequence_id", _U32), + ("reconstruction_factor_id", _U32), + ("semantic_digest_id", _U32), + ), + ) + + +def _contact_orbit_steps_table( + catalog, ids, strings, digests, factors, u32_sequences, i32_sequences +): + rows = [] + stage_ids = {"partial": 0, "final": 1} + for index, record in enumerate(catalog.contact_orbit_steps): + rows.append( + ( + index, + strings.id(record.template_id), + ids["contact_orbit_certificates"][record.certificate_template_id], + stage_ids[record.stage], + record.result_leg, + u32_sequences.id(record.left_covered_legs), + u32_sequences.id(record.right_covered_legs), + i32_sequences.id(record.source_particle_legs), + factors.id(record.reconstruction_factor), + digests.id(record.semantic_digest), + ) + ) + return _rows( + "contact_orbit_steps", + rows, + ( + ("id", _U32), + ("template_string_id", _U32), + ("certificate_id", _U32), + ("stage", _U8), + ("result_leg", _U8), + ("left_covered_leg_sequence_id", _U32), + ("right_covered_leg_sequence_id", _U32), + ("source_particle_leg_sequence_id", _U32), + ("reconstruction_factor_id", _U32), + ("semantic_digest_id", _U32), + ), + ) + + +def _propagators_table(catalog, ids, strings, digests): + rows = [] + for index, record in enumerate(catalog.propagators): + rows.append( + ( + index, + strings.id(record.template_id), + ids["current_states"][record.state_template_id], + int(record.applies_propagator), + _optional_reference( + record.evaluator_resolver_key, ids["evaluator_bindings"] + ), + digests.id(record.numerator_expression_digest), + digests.id(record.denominator_expression_digest), + _optional_reference(record.mass_parameter_id, ids["parameters"]), + _optional_reference(record.width_parameter_id, ids["parameters"]), + strings.id(record.gauge), + _optional_reference( + record.linearity_proof_template_id, ids["symmetry_proofs"] + ), + digests.id(record.semantic_digest), + ) + ) + return _rows( + "propagators", + rows, + ( + ("id", _U32), + ("template_string_id", _U32), + ("state_template_id", _U32), + ("applies_propagator", _U8), + ("evaluator_binding_id", _U32), + ("numerator_expression_digest_id", _U32), + ("denominator_expression_digest_id", _U32), + ("mass_parameter_id", _U32), + ("width_parameter_id", _U32), + ("gauge_string_id", _U32), + ("linearity_proof_template_id", _U32), + ("semantic_digest_id", _U32), + ), + ) + + +def _closures_table( + catalog, ids, strings, digests, factors, sequences, coupling_orders +): + rows = [] + for index, record in enumerate(catalog.closures): + rows.append( + ( + index, + strings.id(record.template_id), + sequences.id( + tuple( + ids["current_states"][value] + for value in record.input_state_template_ids + ) + ), + _optional_reference( + record.result_state_template_id, ids["current_states"] + ), + ids["evaluator_bindings"][record.evaluator_resolver_key], + sequences.id(record.canonical_input_order), + sequences.id( + tuple( + ids["parameters"][value] + for value in record.coupling_parameter_ids + ) + ), + coupling_orders.id(record.coupling_orders), + sequences.id( + tuple( + ids["quantum_flows"][value] + for value in record.eligible_quantum_flow_template_ids + ) + ), + ids["color_contractions"][record.color_contraction_template_id], + factors.id(record.binding_coupling), + factors.id(record.exact_factor), + _OUTPUT_FACTOR_SOURCE[record.output_factor_source], + strings.id(record.equivalence_class), + factors.id(record.input_exchange_factor), + strings.id(record.projection), + sequences.id( + tuple(factors.id(value) for value in record.component_coefficients) + ), + strings.id(record.chirality_relation), + strings.id(record.metric_signature), + digests.id(record.semantic_digest), + ) + ) + return _rows( + "closures", + rows, + ( + ("id", _U32), + ("template_string_id", _U32), + ("input_state_sequence_id", _U32), + ("result_state_template_id", _U32), + ("evaluator_binding_id", _U32), + ("canonical_input_order_sequence_id", _U32), + ("coupling_parameter_sequence_id", _U32), + ("coupling_order_set_id", _U32), + ("eligible_quantum_flow_sequence_id", _U32), + ("color_contraction_template_id", _U32), + ("binding_coupling_factor_id", _U32), + ("exact_factor_id", _U32), + ("output_factor_source", _U8), + ("equivalence_class_string_id", _U32), + ("input_exchange_factor_id", _U32), + ("projection_string_id", _U32), + ("component_coefficient_sequence_id", _U32), + ("chirality_relation_string_id", _U32), + ("metric_signature_string_id", _U32), + ("semantic_digest_id", _U32), + ), + ) + + +def _color_contractions_table(catalog, strings, digests, factors, i32_sequences): + rows = [] + witness_offset = 0 + nc_offset = 0 + for index, record in enumerate(catalog.color_contractions): + rows.append( + ( + index, + strings.id(record.template_id), + strings.id(record.rule_kind), + i32_sequences.id(record.input_representations), + int(record.output_representation is not None), + 0 + if record.output_representation is None + else record.output_representation, + record.ordered_open_string_arity, + factors.id(record.exact_coefficient), + witness_offset, + len(record.transition_witnesses), + nc_offset, + len(record.nc_polynomial), + digests.id(record.expression_digest), + digests.id(record.semantic_digest), + ) + ) + witness_offset += len(record.transition_witnesses) + nc_offset += len(record.nc_polynomial) + return _rows( + "color_contractions", + rows, + ( + ("id", _U32), + ("template_string_id", _U32), + ("rule_kind_string_id", _U32), + ("input_representation_sequence_id", _U32), + ("has_output_representation", _U8), + ("output_representation", _I32), + ("ordered_open_string_arity", _U32), + ("exact_coefficient_factor_id", _U32), + ("witness_start", _U64), + ("witness_count", _U64), + ("nc_term_start", _U64), + ("nc_term_count", _U64), + ("expression_digest_id", _U32), + ("semantic_digest_id", _U32), + ), + ) + + +def _lc_color_transition_witnesses_table( + catalog, + strings, + digests, + factors, + sequences, +): + rows = [] + for color_id, record in enumerate(catalog.color_contractions): + for ordinal, witness in enumerate(record.transition_witnesses): + rows.append( + ( + color_id, + ordinal, + strings.id(witness.input_shape_kinds[0]), + strings.id(witness.input_shape_kinds[1]), + 0 if witness.input_permutation == (0, 1) else 1, + witness.reverse_parent_mask, + _LC_COLOR_COMPONENT_OPERATION[witness.component_operation], + ( + 255 + if witness.result_component_kind is None + else _LC_COLOR_COMPONENT_KIND[witness.result_component_kind] + ), + _LC_COLOR_COMPONENT_ROLE[witness.result_component_role], + strings.id(witness.result_shape_kind), + factors.id(witness.exact_factor), + digests.id(witness.proof_digest), + sequences.id( + tuple( + value + for pairing in witness.input_port_pairings + for port in pairing + for value in port + ) + ), + sequences.id( + tuple( + value + for port in witness.result_port_bindings + for value in port + ) + ), + sequences.id( + tuple( + strings.id(item) + for pair in witness.provenance + for item in pair + ) + ), + ) + ) + return _rows( + "lc_color_transition_witnesses", + rows, + ( + ("color_contraction_id", _U32), + ("ordinal", _U32), + ("left_shape_string_id", _U32), + ("right_shape_string_id", _U32), + ("input_permutation", _U8), + ("reverse_parent_mask", _U8), + ("component_operation", _U8), + ("result_component_kind", _U8), + ("result_component_role", _U8), + ("result_shape_string_id", _U32), + ("exact_factor_id", _U32), + ("proof_digest_id", _U32), + ("input_port_pairing_sequence_id", _U32), + ("result_port_binding_sequence_id", _U32), + ("provenance_sequence_id", _U32), + ), + ) + + +def _color_nc_terms_table(catalog, factors): + rows = [ + (color_id, exponent, factors.id(factor)) + for color_id, record in enumerate(catalog.color_contractions) + for exponent, factor in record.nc_polynomial + ] + return _rows( + "color_nc_terms", + rows, + ( + ("color_contraction_id", _U32), + ("exponent", _I32), + ("factor_id", _U32), + ), + ) + + +def _symmetry_proofs_table(catalog, strings, digests, factors, sequences): + rows = [] + for index, record in enumerate(catalog.symmetry_proofs): + rows.append( + ( + index, + strings.id(record.template_id), + strings.id(record.proof_algorithm), + sequences.id( + tuple(strings.id(value) for value in record.subject_template_ids) + ), + sequences.id(record.input_permutation), + factors.id(record.exact_phase), + sequences.id( + tuple(digests.id(value) for value in record.expression_digests) + ), + digests.id(record.witness_digest), + digests.id(record.semantic_digest), + ) + ) + return _rows( + "symmetry_proofs", + rows, + ( + ("id", _U32), + ("template_string_id", _U32), + ("proof_algorithm_string_id", _U32), + ("subject_template_sequence_id", _U32), + ("input_permutation_sequence_id", _U32), + ("exact_phase_factor_id", _U32), + ("expression_digest_sequence_id", _U32), + ("witness_digest_id", _U32), + ("semantic_digest_id", _U32), + ), + ) + + +def _evaluator_bindings_table(catalog, ids, strings, digests, sequences): + rows = [] + for index, record in enumerate(catalog.evaluator_bindings): + rows.append( + ( + index, + strings.id(record.resolver_key), + MISSING_U32 + if record.prepared_kernel_id is None + else record.prepared_kernel_id, + _CONTRACT_KIND[record.contract_kind], + digests.id(record.callable_signature), + sequences.id( + tuple( + ids["current_states"][value] + for value in record.input_state_template_ids + ) + ), + _optional_reference( + record.output_state_template_id, ids["current_states"] + ), + sequences.id(tuple(strings.id(value) for value in record.input_layout)), + sequences.id( + tuple(strings.id(value) for value in record.output_layout) + ), + sequences.id( + tuple( + digests.id(value) for value in record.exact_expression_digests + ) + ), + sequences.id( + tuple(strings.id(value) for value in record.semantic_template_ids) + ), + _CALLABLE_KIND[record.callable_kind], + strings.id(record.runtime_template), + digests.id(record.semantic_digest), + ) + ) + return _rows( + "evaluator_bindings", + rows, + ( + ("id", _U32), + ("resolver_key_string_id", _U32), + ("prepared_kernel_id", _U32), + ("contract_kind", _U8), + ("callable_signature_digest_id", _U32), + ("input_state_sequence_id", _U32), + ("output_state_template_id", _U32), + ("input_layout_sequence_id", _U32), + ("output_layout_sequence_id", _U32), + ("exact_expression_digest_sequence_id", _U32), + ("semantic_template_sequence_id", _U32), + ("callable_kind", _U8), + ("runtime_template_string_id", _U32), + ("semantic_digest_id", _U32), + ), + ) + + +def _coupling_order_terms(catalog: _CouplingOrderCatalog, strings: _StringCatalog): + rows = [ + (set_id, strings.id(name), power) + for set_id, values in enumerate(catalog.values) + for name, power in values + ] + return _rows( + "coupling_order_terms", + rows, + ( + ("set_id", _U32), + ("name_string_id", _U32), + ("power", _U32), + ), + ) + + +def _catalog_ranges(prefix: str, values: Sequence[Sequence[object]], _width: int): + del _width + rows = [] + offset = 0 + for index, value in enumerate(values): + rows.append((index, offset, len(value))) + offset += len(value) + return _rows( + f"{prefix}_ranges", rows, (("id", _U32), ("start", _U64), ("count", _U64)) + ) + + +def _flatten_sequence_tables( + prefix: str, + catalog: _SequenceCatalog, + dtype: np.dtype[Any], +) -> tuple[RecurrenceColumnarTable, RecurrenceColumnarTable]: + ranges = _catalog_ranges(prefix, catalog.values, 1) + values = _table( + f"{prefix}_values", + { + "value": _array( + [value for sequence in catalog.values for value in sequence], dtype + ) + }, + ) + return ranges, values + + +def _quantum_number_flow_ranges( + catalog: _QuantumNumberFlowCatalog, +) -> RecurrenceColumnarTable: + return _catalog_ranges("quantum_number_flow", catalog.values, 2) + + +def _quantum_number_flow_terms( + catalog: _QuantumNumberFlowCatalog, + strings: _StringCatalog, +) -> RecurrenceColumnarTable: + rows = [ + (flow_id, strings.id(name), strings.id(expression)) + for flow_id, flow in enumerate(catalog.values) + for name, expression in flow + ] + return _rows( + "quantum_number_flow_terms", + rows, + ( + ("flow_id", _U32), + ("name_string_id", _U32), + ("expression_string_id", _U32), + ), + ) + + +def _string_tables( + strings: _StringCatalog, +) -> tuple[RecurrenceColumnarTable, RecurrenceColumnarTable]: + encoded = tuple(value.encode("utf-8") for value in strings.values) + ranges = [] + offset = 0 + for value in encoded: + ranges.append((offset, len(value))) + offset += len(value) + return ( + _rows("string_ranges", ranges, (("start", _U64), ("count", _U64))), + _table("string_bytes", {"value": _array(b"".join(encoded), _U8)}), + ) + + +def _digest_table(catalog: _DigestCatalog) -> RecurrenceColumnarTable: + values = np.empty((len(catalog.values), 32), dtype=_U8) + for row, value in enumerate(catalog.values): + values[row, :] = np.frombuffer(bytes.fromhex(value), dtype=_U8) + return _table( + "digest_catalog", + {"id": _array(range(len(catalog.values)), _U32), "value": values}, + ) + + +def _exact_factor_table(catalog: _FactorCatalog, strings: _StringCatalog): + rows = [ + (index, *(strings.id(str(value)) for value in _factor_key(factor))) + for index, factor in enumerate(catalog.values) + ] + return _rows( + "exact_factors", + rows, + ( + ("id", _U32), + ("real_numerator_string_id", _U32), + ("real_denominator_string_id", _U32), + ("imag_numerator_string_id", _U32), + ("imag_denominator_string_id", _U32), + ), + ) + + +def _table( + name: str, columns: dict[str, np.ndarray[Any, Any]] +) -> RecurrenceColumnarTable: + frozen = [] + for column_name, values in columns.items(): + owned = ( + values + if values.flags.owndata and values.flags.c_contiguous + else np.ascontiguousarray(values).copy() + ) + owned.flags.writeable = False + frozen.append(RecurrenceColumn(column_name, owned)) + row_count = 0 if not frozen else len(frozen[0].values) + return RecurrenceColumnarTable(name, row_count, tuple(frozen)) + + +def _rows( + name: str, + rows: Sequence[Sequence[int]], + schema: Sequence[tuple[str, np.dtype[Any]]], +): + columns = { + column_name: _array( + (row[index] for row in rows), + dtype, + context=f"{name}.{column_name}", + ) + for index, (column_name, dtype) in enumerate(schema) + } + return _table(name, columns) + + +def _array( + values: Iterable[int] | bytes, + dtype: np.dtype[Any], + *, + context: str = "recurrence template column", +) -> np.ndarray[Any, Any]: + if isinstance(values, bytes): + return np.frombuffer(values, dtype=_U8).copy() + materialized = tuple(values) + limits = np.iinfo(dtype) + checked: list[int] = [] + for row, value in enumerate(materialized): + try: + integer = operator.index(value) + except TypeError as error: + raise RecurrenceColumnarInputError( + f"{context} row {row} must be an integer, got {value!r}" + ) from error + if integer < limits.min or integer > limits.max: + raise RecurrenceColumnarInputError( + f"{context} row {row} value {integer} does not fit {dtype.str}" + ) + checked.append(integer) + return np.asarray(checked, dtype=dtype) + + +def _optional_reference(value: str | None, values: dict[str, int]) -> int: + return MISSING_U32 if value is None else values[value] + + +def _present(*values: str | None) -> Iterable[str]: + return (value for value in values if value is not None) + + +def _factor_key(value: ExactComplexRationalV1) -> tuple[int, int, int, int]: + return ( + value.real_numerator, + value.real_denominator, + value.imag_numerator, + value.imag_denominator, + ) + + +def _validate_i128_factors(catalog: RecurrenceTemplateCatalog) -> None: + for factor_index, factor in enumerate(_all_factors(catalog)): + for component in ( + "real_numerator", + "real_denominator", + "imag_numerator", + "imag_denominator", + ): + value = getattr(factor, component) + if value < _I128_MIN or value > _I128_MAX: + raise RecurrenceColumnarInputError( + "recurrence exact factor cannot cross the Rust ABI as i128: " + f"factor {factor_index} {component}={value}" + ) + + +def _require_sha256(value: str, context: str) -> None: + if len(value) != 64 or any( + character not in "0123456789abcdef" for character in value + ): + raise RecurrenceColumnarInputError(f"{context} must be a lowercase SHA-256") + + +def _hash_text(digest: Any, value: str) -> None: + encoded = value.encode("utf-8") + digest.update(len(encoded).to_bytes(8, "little")) + digest.update(encoded) + + +__all__ = [ + "RECURRENCE_TEMPLATE_INPUT_ABI", + "RECURRENCE_TEMPLATE_INPUT_SCHEMA_VERSION", + "RecurrenceTemplateInputV1", + "build_recurrence_template_input_v1", +] diff --git a/pyamplicol/src/pyamplicol/generation/runtime_amplitudes.py b/pyamplicol/src/pyamplicol/generation/runtime_amplitudes.py new file mode 100644 index 0000000000..59d72fb488 --- /dev/null +++ b/pyamplicol/src/pyamplicol/generation/runtime_amplitudes.py @@ -0,0 +1,740 @@ +# SPDX-License-Identifier: 0BSD +"""Amplitude grouping and color-contraction runtime records.""" + +from __future__ import annotations + +from collections.abc import Mapping, Sequence +from dataclasses import dataclass + +from ..color import ( + ColorContractionPlan, + ColorGroupDescriptor, + build_color_contraction_plan, + build_symmetric_group_color_contraction_plan, +) +from ..color.plan_types import LCColorSectorReplayPartition +from ..models.base import Model +from .contracts import runtime_coupling_parameter_names +from .dag_types import AmplitudeRoot, CurrentNode, GenericDAG + + +@dataclass(frozen=True, slots=True) +class RuntimeAmplitudeStageLayout: + """Runtime amplitude record plus its typed colour-contraction input.""" + + record: dict[str, object] + color_contraction: ColorContractionPlan | None + contraction_descriptors: tuple[ColorGroupDescriptor, ...] + + +def build_runtime_amplitude_stage( + dag: GenericDAG, + model: Model, + *, + current_slots: Sequence[Mapping[str, object]], + value_slots: Mapping[tuple[int, str], Mapping[str, object]], + color_contraction: str = "direct", +) -> dict[str, object]: + """Build evaluator roots and physical coherent-amplitude metadata.""" + + return build_runtime_amplitude_stage_layout( + dag, + model, + current_slots=current_slots, + value_slots=value_slots, + color_contraction=color_contraction, + ).record + + +def build_runtime_amplitude_stage_layout( + dag: GenericDAG, + model: Model, + *, + current_slots: Sequence[Mapping[str, object]], + value_slots: Mapping[tuple[int, str], Mapping[str, object]], + color_contraction: str = "direct", +) -> RuntimeAmplitudeStageLayout: + """Build an amplitude record while retaining its authenticated plan.""" + + metadata, contraction, contraction_descriptors = ( + _build_runtime_amplitude_metadata_layout( + dag, + model, + color_contraction=color_contraction, + ) + ) + metadata_roots = _mapping_sequence(metadata["roots"]) + roots: list[dict[str, object]] = [] + for root, record in zip(dag.amplitude_roots, metadata_roots, strict=True): + roots.append( + { + "output_index": record["output_index"], + "root_id": record["root_id"], + "dag_root_id": record["dag_root_id"], + "kind": record["kind"], + "left_current_id": record["left_current_id"], + "right_current_id": record["right_current_id"], + "left_slot": _current_slot_ref(current_slots[root.left_id]), + "right_slot": _current_slot_ref(current_slots[root.right_id]), + "left_value_slot": _value_slot_ref( + _amplitude_value_slot(dag.currents[root.left_id], value_slots) + ), + "right_value_slot": _value_slot_ref( + _amplitude_value_slot(dag.currents[root.right_id], value_slots) + ), + "vertex_kind": record["vertex_kind"], + "vertex_particles": record["vertex_particles"], + "coupling": record["coupling"], + "coupling_parameter_names": record["coupling_parameter_names"], + "color_weight": record["color_weight"], + "color_sector_id": record["color_sector_id"], + "contraction": record["contraction"], + "contraction_ir": record["contraction_ir"], + "coherent_group_id": record["coherent_group_id"], + "helicity_weight": record["helicity_weight"], + "all_sector_weight": record["all_sector_weight"], + } + ) + return RuntimeAmplitudeStageLayout( + record={**metadata, "roots": roots}, + color_contraction=contraction, + contraction_descriptors=contraction_descriptors, + ) + + +def build_runtime_amplitude_metadata( + dag: GenericDAG, + model: Model, + *, + color_contraction: str = "direct", +) -> dict[str, object]: + """Build slot-free amplitude and coherent-group metadata for one DAG.""" + + metadata, _, _ = _build_runtime_amplitude_metadata_layout( + dag, + model, + color_contraction=color_contraction, + ) + return metadata + + +def _build_runtime_amplitude_metadata_layout( + dag: GenericDAG, + model: Model, + *, + color_contraction: str, +) -> tuple[ + dict[str, object], + ColorContractionPlan | None, + tuple[ColorGroupDescriptor, ...], +]: + if color_contraction not in {"direct", "symmetric-group-fft"}: + raise ValueError( + f"unsupported runtime colour contraction {color_contraction!r}" + ) + + group_ids, descriptors = _amplitude_groups(dag, dag.amplitude_roots) + replay = _color_topology_replay_amplitudes(dag, descriptors) + contraction_descriptors = ( + descriptors if replay is None else replay.physical_descriptors + ) + contraction = ( + build_color_contraction_plan(dag.color_plan, contraction_descriptors) + if color_contraction == "direct" + else build_symmetric_group_color_contraction_plan( + dag.color_plan, + contraction_descriptors, + ) + ) + multiple_lc_sectors = _has_multiple_lc_root_sectors(dag) + roots: list[dict[str, object]] = [] + group_weights: dict[int, tuple[float, float]] = {} + for output_index, root in enumerate(dag.amplitude_roots): + group_id = group_ids[root.id] + all_sector_weight = _root_all_sector_weight( + dag, + root, + has_multiple_lc_root_sectors=multiple_lc_sectors, + ) + group_weights.setdefault( + group_id, + (float(root.helicity_weight), float(all_sector_weight)), + ) + roots.append( + { + "output_index": output_index, + "root_id": output_index, + "dag_root_id": root.id, + "kind": root.kind, + "left_current_id": root.left_id, + "right_current_id": root.right_id, + "vertex_kind": root.vertex_kind, + "vertex_particles": ( + None + if root.vertex_particles is None + else list(root.vertex_particles) + ), + "coupling": list(root.coupling), + "coupling_parameter_names": ( + None + if root.vertex_kind is None or root.vertex_particles is None + else runtime_coupling_parameter_names( + root.vertex_kind, + root.vertex_particles, + root.coupling, + model=model, + ) + ), + "color_weight": list(root.color_weight), + "color_sector_id": _root_color_sector_id(dag, root), + "contraction": root.contraction, + "contraction_ir": root.contraction_ir.to_json_dict(), + "coherent_group_id": group_id, + "helicity_weight": root.helicity_weight, + "all_sector_weight": all_sector_weight, + } + ) + + coherent_groups = [] + for descriptor in descriptors: + helicity_weight, all_sector_weight = group_weights[descriptor.group_id] + coherent_groups.append( + { + "group_id": descriptor.group_id, + "helicities": _helicity_vector(dag, descriptor.helicity_key), + "color_sector_id": descriptor.sector_id, + "color_word": list(descriptor.word), + "helicity_weight": helicity_weight, + "all_sector_weight": all_sector_weight, + } + ) + + return ( + { + "stage_kind": "amplitude-roots", + "output_count": len(roots), + "selected_color_sector_ids": None, + "coherent_groups": coherent_groups, + "color_topology_replay": ( + None if replay is None else replay.to_runtime_manifest(dag) + ), + "roots": roots, + "final_reduction": { + "status": ( + "sparse-color-contraction" + if contraction is not None + else "coherent-leading-color-diagonal" + ), + "operation": ( + "sum root outputs into coherent helicity/color amplitudes, " + "then apply the requested color contraction" + ), + }, + "color_contraction": ( + None if contraction is None else contraction.to_json_dict() + ), + }, + contraction, + contraction_descriptors, + ) + + +@dataclass(frozen=True, slots=True) +class _ColorReplayGroupRoute: + source_group_id: int + target_group_id: int + factor: tuple[float, float] + + +@dataclass(frozen=True, slots=True) +class _ColorReplayMapping: + label_permutation: tuple[tuple[int, int], ...] + group_routes: tuple[_ColorReplayGroupRoute, ...] + + +@dataclass(frozen=True, slots=True) +class _ColorReplayAmplitudePlan: + physical_descriptors: tuple[ColorGroupDescriptor, ...] + mappings: tuple[_ColorReplayMapping, ...] + + def to_runtime_manifest(self, dag: GenericDAG) -> dict[str, object]: + groups = [] + for descriptor in self.physical_descriptors: + groups.append( + { + "group_id": descriptor.group_id, + "helicities": _helicity_vector(dag, descriptor.helicity_key), + "color_sector_id": descriptor.sector_id, + "color_word": list(descriptor.word), + "helicity_weight": descriptor.helicity_weight, + } + ) + return { + "contract_version": 1, + "physical_group_count": len(groups), + "physical_groups": groups, + "mappings": [ + { + "label_permutation": [ + { + "representative_label": representative, + "sector_label": sector, + } + for representative, sector in mapping.label_permutation + ], + "group_routes": [ + { + "source_group_id": route.source_group_id, + "target_group_id": route.target_group_id, + "factor": list(route.factor), + } + for route in mapping.group_routes + ], + } + for mapping in self.mappings + ], + } + + +def _color_topology_replay_amplitudes( + dag: GenericDAG, + materialized_descriptors: Sequence[ColorGroupDescriptor], +) -> _ColorReplayAmplitudePlan | None: + """Derive the amplitude-level gather authenticated by the color proof. + + The generated DAG contains only representative sectors. NLC/full color + contraction is nevertheless defined over every physical sector, so the + runtime must evaluate representatives at the proven external-label + permutations, gather coherent complex amplitudes into the complete + physical group domain, and only then apply the unchanged color metric. + """ + + certificate = dag.color_topology_replay + if certificate is None or not certificate.optimized: + return None + materialized_by_sector: dict[int, list[ColorGroupDescriptor]] = {} + for descriptor in materialized_descriptors: + materialized_by_sector.setdefault(descriptor.sector_id, []).append(descriptor) + expected_materialized = set(certificate.materialized_sector_ids) + if set(materialized_by_sector) != expected_materialized: + raise ValueError( + "color topology replay materialized amplitude sectors do not match " + "the proof certificate" + ) + + pending_routes: list[ + tuple[ + tuple[tuple[int, int], ...], + int, + tuple[object, ...], + int, + tuple[int, ...], + float, + ] + ] = [] + for partition in certificate.partitions: + source_descriptors = materialized_by_sector.get( + int(partition.materialized_sector_id) + ) + if not source_descriptors: + raise ValueError( + "color topology replay partition has no materialized amplitudes" + ) + _append_color_replay_partition_routes( + dag, + partition, + source_descriptors, + pending_routes, + ) + for sector_id in certificate.residual_sector_ids: + source_descriptors = materialized_by_sector.get(int(sector_id)) + if not source_descriptors: + raise ValueError( + f"color topology replay residual sector {sector_id} is absent" + ) + for source in source_descriptors: + pending_routes.append( + ( + (), + source.group_id, + source.helicity_key, + int(sector_id), + source.word, + source.helicity_weight, + ) + ) + + physical_keys = sorted( + { + (helicity_key, sector_id, word, helicity_weight) + for ( + _permutation, + _source_group_id, + helicity_key, + sector_id, + word, + helicity_weight, + ) in pending_routes + }, + key=repr, + ) + physical_id_by_key = {key: group_id for group_id, key in enumerate(physical_keys)} + physical_descriptors = tuple( + ColorGroupDescriptor( + group_id=group_id, + helicity_key=helicity_key, + sector_id=sector_id, + word=word, + helicity_weight=helicity_weight, + ) + for group_id, (helicity_key, sector_id, word, helicity_weight) in enumerate( + physical_keys + ) + ) + routes_by_mapping: dict[ + tuple[tuple[int, int], ...], + list[_ColorReplayGroupRoute], + ] = {} + target_ids: list[int] = [] + for ( + permutation, + source_group_id, + helicity_key, + sector_id, + word, + helicity_weight, + ) in pending_routes: + target_group_id = physical_id_by_key[ + (helicity_key, sector_id, word, helicity_weight) + ] + routes_by_mapping.setdefault(permutation, []).append( + _ColorReplayGroupRoute( + source_group_id=source_group_id, + target_group_id=target_group_id, + factor=(1.0, 0.0), + ) + ) + target_ids.append(target_group_id) + if sorted(target_ids) != list(range(len(physical_descriptors))): + raise ValueError( + "color topology replay amplitude gather is not a physical-group bijection" + ) + mappings = tuple( + _ColorReplayMapping( + label_permutation=permutation, + group_routes=tuple( + sorted( + routes, + key=lambda route: ( + route.target_group_id, + route.source_group_id, + ), + ) + ), + ) + for permutation, routes in sorted(routes_by_mapping.items()) + ) + if not mappings or any(not mapping.group_routes for mapping in mappings): + raise ValueError("color topology replay amplitude gather is empty") + return _ColorReplayAmplitudePlan( + physical_descriptors=physical_descriptors, + mappings=mappings, + ) + + +def _append_color_replay_partition_routes( + dag: GenericDAG, + partition: LCColorSectorReplayPartition, + source_descriptors: Sequence[ColorGroupDescriptor], + pending_routes: list[ + tuple[ + tuple[tuple[int, int], ...], + int, + tuple[object, ...], + int, + tuple[int, ...], + float, + ] + ], +) -> None: + for sector_id, permutation, weight, sign in zip( + partition.active_sector_ids, + partition.label_permutations, + partition.weights, + partition.signs, + strict=True, + ): + if weight != 1.0 or sign != 1: + raise ValueError( + "color topology replay amplitude gather currently requires " + "unit positive proof factors" + ) + target_sector = dag.color_plan.sector(int(sector_id)) + if target_sector is None: + raise ValueError( + f"color topology replay target sector {sector_id} is absent" + ) + target_word = tuple(target_sector.word_labels or target_sector.color_words[0]) + normalized_permutation = tuple( + sorted( + (int(left), int(right)) + for left, right in permutation + if int(left) != int(right) + ) + ) + for source in source_descriptors: + pending_routes.append( + ( + normalized_permutation, + source.group_id, + _permute_helicity_key(source.helicity_key, normalized_permutation), + int(sector_id), + target_word, + source.helicity_weight, + ) + ) + + +def _permute_helicity_key( + helicity_key: tuple[object, ...], + permutation: Sequence[tuple[int, int]], +) -> tuple[object, ...]: + by_label = dict(permutation) + result: list[object] = [] + for item in helicity_key: + if not isinstance(item, tuple) or len(item) < 5: + raise ValueError( + "color topology replay requires explicit external-label helicity keys" + ) + result.append((by_label.get(int(item[0]), int(item[0])), *item[1:])) + return tuple(sorted(result)) + + +def _mapping_sequence(value: object) -> tuple[Mapping[str, object], ...]: + if isinstance(value, str | bytes) or not isinstance(value, Sequence): + raise TypeError("runtime amplitude field must be an array") + result: list[Mapping[str, object]] = [] + for item in value: + if not isinstance(item, Mapping): + raise TypeError("runtime amplitude record must be an object") + result.append(item) + return tuple(result) + + +def _amplitude_groups( + dag: GenericDAG, + roots: Sequence[AmplitudeRoot], +) -> tuple[dict[int, int], tuple[ColorGroupDescriptor, ...]]: + ids: dict[tuple[object, ...], int] = {} + result: dict[int, int] = {} + descriptors: dict[int, ColorGroupDescriptor] = {} + source_by_ancestry = { + int(current.index.helicity_ancestry): ( + int(current.source_leg_label or 0), + int(current.index.particle_id), + int(current.index.chirality), + current.index.spin_state, + current.source_helicity, + ) + for current in dag.currents + if current.is_source + } + single_bit_ancestry = all( + bit > 0 and bit & (bit - 1) == 0 for bit in source_by_ancestry + ) + physical_sources_cache: dict[int, tuple[object, ...]] = {} + materialized_source_keys = _materialized_root_source_keys(dag) + + def physical_sources(ancestry: int) -> tuple[object, ...]: + cached = physical_sources_cache.get(ancestry) + if cached is not None: + return cached + if single_bit_ancestry: + sources: list[tuple[object, ...]] = [] + remaining = ancestry + while remaining: + bit = remaining & -remaining + source = source_by_ancestry.get(bit) + if source is not None: + sources.append(source) + remaining ^= bit + value: tuple[object, ...] = tuple(sorted(sources)) + else: + value = tuple( + sorted( + source + for bit, source in source_by_ancestry.items() + if ancestry & bit + ) + ) + physical_sources_cache[ancestry] = value + return value + + for root in roots: + left = dag.currents[root.left_id].index + right = dag.currents[root.right_id].index + ancestry = int(left.helicity_ancestry | right.helicity_ancestry) + source_key = materialized_source_keys.get(root.id) or physical_sources(ancestry) + sector_id = _root_color_sector_id(dag, root) + sector = dag.color_plan.sector(sector_id) + word = ( + tuple(sector.word_labels or sector.color_words[0]) + if sector is not None and sector.color_words + else () + ) + color_key = ( + left.color_state.accuracy, + word or sector_id, + tuple( + sorted( + set(left.color_state.basis_key) | set(right.color_state.basis_key) + ) + ), + tuple(root.color_weight), + ) + key = (source_key or ancestry, color_key) + group_id = ids.setdefault(key, len(ids)) + result[root.id] = group_id + descriptors.setdefault( + group_id, + ColorGroupDescriptor( + group_id=group_id, + helicity_key=source_key or (ancestry,), + sector_id=sector_id, + word=word, + helicity_weight=float(root.helicity_weight), + ), + ) + return result, tuple(descriptors[index] for index in sorted(descriptors)) + + +def _materialized_root_source_keys( + dag: GenericDAG, +) -> dict[int, tuple[object, ...]]: + materialization = dag.helicity_materialization + recurrence = dag.helicity_recurrence + if materialization is None or recurrence is None: + return {} + if materialization.strategy == "retained-proof-graph": + # The retained graph keeps the original physical roots and their + # ancestry. Preserve the established coherent grouping so selector + # metadata cannot change cancellations in the summed workload. + return {} + domain_by_id = {domain.id: domain for domain in recurrence.selector_domains} + routes_by_root: dict[int, list[object]] = {} + for route in materialization.amplitude_routes: + routes_by_root.setdefault(route.materialized_root_id, []).append(route) + result: dict[int, tuple[object, ...]] = {} + for root_id, routes in routes_by_root.items(): + first_domain_id = next( + (domain_id for route in routes for domain_id in route.selector_domain_ids), + None, + ) + if first_domain_id is None: + raise ValueError( + f"materialized amplitude root {root_id} has no selector domain" + ) + domain = domain_by_id.get(first_domain_id) + if domain is None or not domain.complete: + raise ValueError( + f"materialized amplitude root {root_id} has an invalid selector domain" + ) + result[root_id] = tuple( + (label, 0, 0, 0, helicity) for label, helicity in domain.source_states + ) + return result + + +def _helicity_vector( + dag: GenericDAG, + helicity_key: tuple[object, ...], +) -> list[int]: + by_label: dict[int, int] = {} + for item in helicity_key: + if not isinstance(item, tuple) or len(item) < 5: + continue + by_label[int(item[0])] = int(item[4]) + return [by_label.get(leg.label, 0) for leg in dag.process.legs] + + +def _root_color_sector_id(dag: GenericDAG, root: AmplitudeRoot) -> int: + if root.color_sector_id is not None: + return int(root.color_sector_id) + return int(dag.currents[root.left_id].index.color_state.sector_id) + + +def _root_all_sector_weight( + dag: GenericDAG, + root: AmplitudeRoot, + *, + has_multiple_lc_root_sectors: bool, +) -> float: + weight = float(root.helicity_weight) + if dag.process.color_accuracy != "lc": + return weight + if dag.color_plan.process.color_endpoints.pair_count != 0: + return weight + if not dag.color_plan.trace_reflections_folded: + return weight + if not has_multiple_lc_root_sectors: + return weight + sector = dag.color_plan.sector(_root_color_sector_id(dag, root)) + if sector is None or sector.kind != "single-trace": + return weight + trace = tuple(int(label) for label in sector.trace_labels) + if len(trace) <= 2 or trace[1:] == tuple(reversed(trace[1:])): + return weight + return 2.0 * weight + + +def _has_multiple_lc_root_sectors(dag: GenericDAG) -> bool: + if dag.process.color_accuracy != "lc": + return False + if dag.color_plan.process.color_endpoints.pair_count != 0: + return False + return len({_root_color_sector_id(dag, root) for root in dag.amplitude_roots}) > 1 + + +def _current_slot_ref(slot: Mapping[str, object]) -> dict[str, object]: + return { + key: slot[key] + for key in ( + "current_id", + "component_start", + "component_stop", + "dimension", + ) + } + + +def _value_slot_ref(slot: Mapping[str, object]) -> dict[str, object]: + return { + key: slot[key] + for key in ( + "value_slot_id", + "current_id", + "variant", + "component_start", + "component_stop", + "dimension", + ) + } + + +def _amplitude_value_slot( + current: CurrentNode, + value_slots: Mapping[tuple[int, str], Mapping[str, object]], +) -> Mapping[str, object]: + variant = "source" if current.is_source else "unpropagated" + try: + return value_slots[(current.id, variant)] + except KeyError as exc: + raise ValueError( + f"amplitude current {current.id} has no {variant} runtime value slot" + ) from exc + + +__all__ = [ + "build_runtime_amplitude_metadata", + "build_runtime_amplitude_stage", +] diff --git a/pyamplicol/src/pyamplicol/generation/runtime_schema.py b/pyamplicol/src/pyamplicol/generation/runtime_schema.py new file mode 100644 index 0000000000..b895c29904 --- /dev/null +++ b/pyamplicol/src/pyamplicol/generation/runtime_schema.py @@ -0,0 +1,876 @@ +# SPDX-License-Identifier: 0BSD +"""Neutral schema-v3 runtime layout assembly for generated DAGs.""" + +from __future__ import annotations + +from collections.abc import Mapping, Sequence +from dataclasses import dataclass +from typing import Any + +from ..color import ColorContractionPlan, ColorGroupDescriptor +from ..models.base import Model +from .contracts import RuntimeExpressionSchema +from .dag_types import CurrentNode, GenericDAG, InteractionNode +from .physics_metadata import ( + build_resolved_physics_payload, + build_runtime_model_parameter_records, + build_runtime_normalization, +) +from .runtime_amplitudes import build_runtime_amplitude_stage_layout + +RUNTIME_PHYSICS_SCHEMA_VERSION = 1 + + +@dataclass(frozen=True, slots=True) +class RuntimeStageLayout: + """One runtime stage plus indexes retained for eager table lowering.""" + + record: dict[str, object] + interactions: tuple[InteractionNode, ...] + evaluation_groups: tuple[tuple[InteractionNode, ...], ...] + input_value_slot_by_current: dict[int, int] + output_current_ids: tuple[int, ...] + + +@dataclass(frozen=True, slots=True) +class RuntimeSchemaLayout: + """Owned runtime schema plus the indexes used to assemble it. + + The eager lane consumes these indexes directly so it never has to parse the + freshly constructed JSON-shaped schema back into Python lookup tables. + """ + + runtime_schema: dict[str, object] + current_slots: list[dict[str, object]] + value_slots_by_id: tuple[dict[str, object], ...] + value_slot_ids_by_current_variant: dict[tuple[int, str], int] + momentum_slot_by_mask: dict[int, int] + stages: tuple[RuntimeStageLayout, ...] + amplitude_stage: dict[str, object] + color_contraction: ColorContractionPlan | None + color_contraction_descriptors: tuple[ColorGroupDescriptor, ...] + model_parameters: list[dict[str, object]] + + +@dataclass(frozen=True, slots=True) +class _SourceDescriptor: + source_ir: Any + applied_crossing: Any + expected_states: frozenset[tuple[int, int, object]] + crossing: str + source_ir_payload: dict[str, object] + applied_crossing_payload: dict[str, object] + + +@dataclass(frozen=True, slots=True) +class _ValueSlotLayout: + records: list[dict[str, object]] + by_current_variant: dict[tuple[int, str], dict[str, object]] + slot_ids_by_current_variant: dict[tuple[int, str], int] + input_slot_id_by_current: dict[int, int] + result_slot_ids_by_current: dict[int, tuple[int, ...]] + + +def build_runtime_expression_schema( + dag: GenericDAG, + model: Model, + *, + process_id: str | None = None, + color_contraction: str = "direct", +) -> RuntimeExpressionSchema: + return RuntimeExpressionSchema.from_mapping( + build_runtime_schema( + dag, + model, + process_id=process_id, + color_contraction=color_contraction, + ) + ) + + +def build_runtime_schema( + dag: GenericDAG, + model: Model, + *, + process_id: str | None = None, + color_contraction: str = "direct", +) -> dict[str, object]: + """Build the stage compiler and runtime metadata for one concrete process.""" + + return build_runtime_schema_layout( + dag, + model, + process_id=process_id, + color_contraction=color_contraction, + ).runtime_schema + + +def build_runtime_schema_layout( + dag: GenericDAG, + model: Model, + *, + process_id: str | None = None, + color_contraction: str = "direct", +) -> RuntimeSchemaLayout: + """Build runtime metadata while retaining its typed construction indexes.""" + + public_process_id = process_id or dag.process.key + + color_state_payloads: dict[object, dict[str, object]] = {} + current_slots = _current_slots( + dag, + color_state_payloads=color_state_payloads, + ) + interactions_by_size, interaction_inputs = _interaction_indexes(dag) + amplitude_inputs = { + current_id + for root in dag.amplitude_roots + for current_id in (root.left_id, root.right_id) + } + propagators_by_current = _propagators_by_current(dag, model) + propagator_payloads: dict[tuple[int, int], dict[str, object]] = {} + value_slot_layout = _value_slots( + dag, + current_slots=current_slots, + interaction_inputs=interaction_inputs, + amplitude_inputs=amplitude_inputs, + propagators_by_current=propagators_by_current, + propagator_payloads=propagator_payloads, + ) + value_slot_records = value_slot_layout.records + value_slots = value_slot_layout.by_current_variant + momentum_slots = _momentum_slots(dag) + momentum_slot_by_mask = { + int(slot["momentum_mask"]): int(slot["momentum_slot_id"]) + for slot in momentum_slots + } + amplitude_stage_layout = build_runtime_amplitude_stage_layout( + dag, + model, + current_slots=current_slots, + value_slots=value_slots, + color_contraction=color_contraction, + ) + amplitude_stage = amplitude_stage_layout.record + model_parameters = build_runtime_model_parameter_records( + dag, + model, + amplitude_stage=amplitude_stage, + ) + stage_layouts = _stage_layouts( + dag, + input_value_slot_id_by_current=(value_slot_layout.input_slot_id_by_current), + result_value_slot_ids_by_current=(value_slot_layout.result_slot_ids_by_current), + momentum_slot_by_mask=momentum_slot_by_mask, + interactions_by_size=interactions_by_size, + ) + stages = [stage.record for stage in stage_layouts] + external_particles = _external_particles(dag) + normalization = build_runtime_normalization(dag, model) + physics = build_resolved_physics_payload( + dag, + model, + process_id=public_process_id, + amplitude_stage=amplitude_stage, + model_parameters=model_parameters, + normalization=normalization, + ) + value_count = ( + int(value_slot_records[-1]["component_stop"]) if value_slot_records else 0 + ) + momentum_count = 4 * len(momentum_slots) + source_records = _source_records( + dag, + model, + current_slots=current_slots, + value_slots=value_slots, + color_state_payloads=color_state_payloads, + ) + + runtime_schema: dict[str, object] = { + "schema_version": 1, + "kind": "pyamplicol-runtime-expression-schema", + "process_key": public_process_id, + "process": dag.process.process, + "color_accuracy": dag.process.color_accuracy, + "external_particles": external_particles, + "momentum_conventions": _momentum_conventions(dag), + "model": _model_payload(dag, model), + "normalization": normalization, + "parameter_layout": { + "value_component_count": value_count, + "momentum_parameter_count": momentum_count, + "model_parameter_count": len(model_parameters), + "parameter_count_if_flattened": ( + value_count + momentum_count + len(model_parameters) + ), + "momentum_components_real": True, + "model_parameters_real": True, + }, + "model_parameters": model_parameters, + "physics": physics, + "current_storage": { + "component_count": ( + int(current_slots[-1]["component_stop"]) if current_slots else 0 + ), + "number_type": "complex", + "current_slots": current_slots, + }, + "value_storage": { + "component_count": value_count, + "number_type": "complex", + "value_slots": value_slot_records, + }, + "source_fill": { + "source_count": len(dag.sources), + "sources": source_records, + }, + "momentum_slots": momentum_slots, + "stages": stages, + "amplitude_stage": amplitude_stage, + } + if dag.lc_topology_replay is not None: + runtime_schema["lc_topology_replay"] = ( + dag.lc_topology_replay.to_runtime_manifest() + ) + if dag.color_topology_replay is not None: + runtime_schema["color_topology_replay"] = ( + dag.color_topology_replay.to_runtime_manifest() + ) + if dag.helicity_recurrence is not None: + runtime_schema["helicity_recurrence"] = { + **dag.helicity_recurrence.to_runtime_manifest(), + **( + {} + if dag.helicity_materialization is None + else { + "materialization": ( + dag.helicity_materialization.to_runtime_manifest() + ) + } + ), + } + return RuntimeSchemaLayout( + runtime_schema=runtime_schema, + current_slots=current_slots, + value_slots_by_id=tuple(value_slot_records), + value_slot_ids_by_current_variant=( + value_slot_layout.slot_ids_by_current_variant + ), + momentum_slot_by_mask=momentum_slot_by_mask, + stages=stage_layouts, + amplitude_stage=amplitude_stage, + color_contraction=amplitude_stage_layout.color_contraction, + color_contraction_descriptors=(amplitude_stage_layout.contraction_descriptors), + model_parameters=model_parameters, + ) + + +def _current_slots( + dag: GenericDAG, + *, + color_state_payloads: dict[object, dict[str, object]] | None = None, +) -> list[dict[str, object]]: + offset = 0 + slots: list[dict[str, object]] = [] + color_payloads = {} if color_state_payloads is None else color_state_payloads + for current in dag.currents: + index = current.index + start = offset + offset += current.dimension + color_state = color_payloads.get(index.color_state) + if color_state is None: + color_state = index.color_state.to_json_dict() + color_payloads[index.color_state] = color_state + slots.append( + { + "current_id": current.id, + "component_start": start, + "component_stop": offset, + "dimension": current.dimension, + "is_source": current.is_source, + "particle_id": index.particle_id, + "external_mask": index.external_mask, + "external_labels": list(index.external_labels), + "momentum_mask": index.momentum_mask, + # Decimal conversion is capped by Python for very wide + # full-colour helicity masks. The runtime contract accepts + # canonical hexadecimal integers and does not interpret this + # diagnostic-only field on the hot path. + "helicity_ancestry": hex(index.helicity_ancestry), + "chirality": index.chirality, + "spin_state": _spin_state(index.spin_state), + "flavour_flow": list(index.flavour_flow), + "quantum_number_flow": [ + [name, expression] for name, expression in index.quantum_number_flow + ], + "color_state": color_state, + "auxiliary_kind": index.auxiliary_kind, + } + ) + return slots + + +def _value_slots( + dag: GenericDAG, + *, + current_slots: Sequence[Mapping[str, object]], + interaction_inputs: set[int], + amplitude_inputs: set[int], + propagators_by_current: Sequence[Any], + propagator_payloads: dict[tuple[int, int], dict[str, object]], +) -> _ValueSlotLayout: + records: list[dict[str, object]] = [] + by_current_variant: dict[tuple[int, str], dict[str, object]] = {} + slot_ids_by_current_variant: dict[tuple[int, str], int] = {} + input_slot_id_by_current: dict[int, int] = {} + result_slot_ids_by_current: dict[int, tuple[int, ...]] = {} + offset = 0 + + def add( + current: CurrentNode, + variant: str, + applies_propagator: bool, + ) -> int: + nonlocal offset + current_slot = current_slots[current.id] + propagator = propagators_by_current[current.id] + propagator_key = ( + current.index.particle_id, + current.index.chirality, + ) + propagator_payload = propagator_payloads.get(propagator_key) + if propagator_payload is None: + propagator_payload = propagator.to_json_dict() + propagator_payloads[propagator_key] = propagator_payload + start = offset + offset += current.dimension + slot_id = len(records) + record: dict[str, object] = { + "value_slot_id": slot_id, + "current_id": current.id, + "variant": variant, + "component_start": start, + "component_stop": offset, + "dimension": current.dimension, + "current_component_start": current_slot["component_start"], + "current_component_stop": current_slot["component_stop"], + "is_source": current.is_source, + "applies_propagator": applies_propagator, + "particle_id": current.index.particle_id, + "external_mask": current.index.external_mask, + "external_labels": current_slot["external_labels"], + "momentum_mask": current.index.momentum_mask, + "chirality": current.index.chirality, + "propagator": propagator_payload, + "used_as_interaction_input": current.id in interaction_inputs, + "used_as_amplitude_input": current.id in amplitude_inputs, + } + records.append(record) + key = (current.id, variant) + by_current_variant[key] = record + slot_ids_by_current_variant[key] = slot_id + return slot_id + + for current in dag.currents: + if current.is_source: + source_slot_id = add(current, "source", False) + result_slot_ids_by_current[current.id] = (source_slot_id,) + if current.id in interaction_inputs: + input_slot_id_by_current[current.id] = source_slot_id + continue + propagator = propagators_by_current[current.id] + needs_propagated = ( + current.id in interaction_inputs and propagator.applies_propagator + ) + needs_unpropagated = current.id in amplitude_inputs + unpropagated_slot_id: int | None = None + if ( + needs_unpropagated + or not needs_propagated + or (current.id in interaction_inputs and not propagator.applies_propagator) + ): + unpropagated_slot_id = add(current, "unpropagated", False) + propagated_slot_id: int | None = None + if needs_propagated: + propagated_slot_id = add(current, "propagated", True) + result_slot_ids = tuple( + slot_id + for slot_id in (unpropagated_slot_id, propagated_slot_id) + if slot_id is not None + ) + if not result_slot_ids: + raise ValueError(f"result current {current.id} has no runtime value slots") + result_slot_ids_by_current[current.id] = result_slot_ids + if current.id in interaction_inputs: + input_slot_id = ( + propagated_slot_id + if propagator.applies_propagator + else unpropagated_slot_id + ) + if input_slot_id is None: + variant = ( + "propagated" if propagator.applies_propagator else "unpropagated" + ) + raise ValueError( + f"interaction current {current.id} has no {variant} " + "runtime value slot" + ) + input_slot_id_by_current[current.id] = input_slot_id + return _ValueSlotLayout( + records=records, + by_current_variant=by_current_variant, + slot_ids_by_current_variant=slot_ids_by_current_variant, + input_slot_id_by_current=input_slot_id_by_current, + result_slot_ids_by_current=result_slot_ids_by_current, + ) + + +def _momentum_slots(dag: GenericDAG) -> list[dict[str, object]]: + masks = sorted( + {current.index.momentum_mask for current in dag.currents}, + key=lambda mask: (mask.bit_count(), mask), + ) + incoming = {leg.label for leg in dag.process.initial_legs} + result: list[dict[str, object]] = [] + for slot_id, mask in enumerate(masks): + labels = _mask_labels(mask) + start = 4 * slot_id + result.append( + { + "momentum_slot_id": slot_id, + "momentum_mask": mask, + "external_labels": list(labels), + "component_start": start, + "component_stop": start + 4, + "component_order": ["E", "px", "py", "pz"], + "real_valued": True, + "crossed_incoming_labels": [ + label for label in labels if label in incoming + ], + "construction": ( + "sum all-outgoing momenta for external labels; negate " + "physical incoming momenta first" + ), + } + ) + return result + + +def _stage_layouts( + dag: GenericDAG, + *, + input_value_slot_id_by_current: Mapping[int, int], + result_value_slot_ids_by_current: Mapping[int, tuple[int, ...]], + momentum_slot_by_mask: Mapping[int, int], + interactions_by_size: Mapping[int, Sequence[InteractionNode]], +) -> tuple[RuntimeStageLayout, ...]: + stages: list[RuntimeStageLayout] = [] + for stage_index, size in enumerate(sorted(interactions_by_size), start=1): + interactions = tuple(interactions_by_size[size]) + input_current_ids: set[int] = set() + output_current_ids: set[int] = set() + input_value_slot_ids: set[int] = set() + output_value_slot_ids: set[int] = set() + momentum_ids: set[int] = set() + evaluation_groups: dict[tuple[str, int], list[InteractionNode]] = {} + for interaction in interactions: + left_id = interaction.left_id + right_id = interaction.right_id + result_id = interaction.result_id + input_current_ids.add(left_id) + input_current_ids.add(right_id) + output_current_ids.add(result_id) + input_value_slot_ids.add(input_value_slot_id_by_current[left_id]) + input_value_slot_ids.add(input_value_slot_id_by_current[right_id]) + output_value_slot_ids.update(result_value_slot_ids_by_current[result_id]) + momentum_ids.add( + momentum_slot_by_mask[dag.currents[left_id].index.momentum_mask] + ) + momentum_ids.add( + momentum_slot_by_mask[dag.currents[right_id].index.momentum_mask] + ) + momentum_ids.add( + momentum_slot_by_mask[dag.currents[result_id].index.momentum_mask] + ) + group_key = ( + ("group", int(interaction.evaluation_group_id)) + if interaction.evaluation_group_id is not None + else ("interaction", interaction.id) + ) + if interaction.evaluation_factor != (0.0, 0.0): + evaluation_groups.setdefault(group_key, []).append(interaction) + + ordered_output_current_ids = tuple(sorted(output_current_ids)) + record: dict[str, object] = { + "stage_index": stage_index, + "stage_kind": "current-combine", + "subset_size": size, + "input_current_ids": sorted(input_current_ids), + "output_current_ids": list(ordered_output_current_ids), + "input_value_slot_ids": sorted(input_value_slot_ids), + "output_value_slot_ids": sorted(output_value_slot_ids), + "input_momentum_slot_ids": sorted(momentum_ids), + "interaction_count": len(interactions), + "interaction_evaluation_count": len(evaluation_groups), + "interaction_ids": [interaction.id for interaction in interactions], + "interactions_compacted": True, + "interactions": [], + } + stages.append( + RuntimeStageLayout( + record=record, + interactions=interactions, + evaluation_groups=tuple( + tuple(group) for group in evaluation_groups.values() + ), + input_value_slot_by_current={ + current_id: input_value_slot_id_by_current[current_id] + for current_id in input_current_ids + }, + output_current_ids=ordered_output_current_ids, + ) + ) + return tuple(stages) + + +def _interaction_indexes( + dag: GenericDAG, +) -> tuple[dict[int, list[InteractionNode]], set[int]]: + by_size: dict[int, list[InteractionNode]] = {} + inputs: set[int] = set() + for interaction in dag.interactions: + size = len(dag.currents[interaction.result_id].index.external_labels) + by_size.setdefault(size, []).append(interaction) + inputs.add(interaction.left_id) + inputs.add(interaction.right_id) + return by_size, inputs + + +def _propagators_by_current(dag: GenericDAG, model: Model) -> list[Any]: + cache: dict[tuple[int, int], Any] = {} + result: list[Any] = [] + for current in dag.currents: + key = (current.index.particle_id, current.index.chirality) + propagator = cache.get(key) + if propagator is None: + propagator = model._propagator_ir(*key) + cache[key] = propagator + result.append(propagator) + return result + + +def _source_records( + dag: GenericDAG, + model: Model, + *, + current_slots: Sequence[Mapping[str, object]], + value_slots: Mapping[tuple[int, str], Mapping[str, object]], + color_state_payloads: Mapping[object, dict[str, object]], +) -> list[dict[str, object]]: + records: list[dict[str, object]] = [] + source_start = 0 + legs_by_label = {leg.label: leg for leg in dag.process.legs} + descriptors: dict[tuple[int, bool], _SourceDescriptor] = {} + for source_index, current_id in enumerate(dag.sources): + current = dag.currents[current_id] + source_leg_label = current.source_leg_label + leg = ( + legs_by_label.get(source_leg_label) + if source_leg_label is not None + else None + ) + descriptor_key = ( + current.index.particle_id, + bool(leg is not None and leg.is_initial), + ) + descriptor = descriptors.get(descriptor_key) + if descriptor is None: + descriptor = _source_descriptor( + model, + particle_id=current.index.particle_id, + crossed=descriptor_key[1], + ) + descriptors[descriptor_key] = descriptor + records.append( + _source_record( + current, + leg=leg, + descriptor=descriptor, + current_id=current_id, + source_index=source_index, + source_start=source_start, + current_slot=current_slots[current_id], + value_slot=value_slots[(current_id, "source")], + color_state_payload=color_state_payloads[current.index.color_state], + ) + ) + source_start += current.dimension + return records + + +def _source_descriptor( + model: Model, + *, + particle_id: int, + crossed: bool, +) -> _SourceDescriptor: + source_ir = model._source_ir(particle_id) + applied_crossing = ( + source_ir.crossing if crossed else type(source_ir.crossing).identity() + ) + expected_states = frozenset( + (state.helicity, state.chirality, state.spin_state) + for state in ( + applied_crossing.apply(declared_state) + for declared_state in source_ir.states + ) + ) + return _SourceDescriptor( + source_ir=source_ir, + applied_crossing=applied_crossing, + expected_states=expected_states, + crossing=( + "negate-incoming-momentum" + if applied_crossing.momentum_transform == "negate-four-momentum" + else "identity" + ), + source_ir_payload=source_ir.to_json_dict(), + applied_crossing_payload=applied_crossing.to_json_dict(), + ) + + +def _source_record( + current: CurrentNode, + *, + leg: Any, + descriptor: _SourceDescriptor, + current_id: int, + source_index: int, + source_start: int, + current_slot: Mapping[str, object], + value_slot: Mapping[str, object], + color_state_payload: dict[str, object], +) -> dict[str, object]: + if current.source_helicity is None: + raise ValueError(f"source current {current_id} has no source helicity") + current_state = ( + int(current.source_helicity), + int(current.index.chirality), + current.index.spin_state, + ) + if current_state not in descriptor.expected_states: + raise ValueError( + f"source current {current_id} state {current_state!r} is not declared " + f"by model source metadata" + ) + source_ir = descriptor.source_ir + return { + "source_id": source_index, + "current_id": current_id, + "current_component_start": current_slot["component_start"], + "current_component_stop": current_slot["component_stop"], + "value_slot": _value_slot_ref(value_slot), + "source_parameter_start": source_start, + "source_parameter_stop": source_start + current.dimension, + "leg_label": current.source_leg_label, + "input_momentum_slot": None if leg is None else leg.label - 1, + "side": None if leg is None else leg.side, + "crossing": descriptor.crossing, + "physical_pdg": None if leg is None else leg.pdg, + "outgoing_pdg": current.index.particle_id, + "particle_id": current.index.particle_id, + "anti_particle_id": source_ir.identity.anti_pdg_label, + "source_kind": "external-wavefunction", + "wavefunction_kind": source_ir.wavefunction_family, + "source_orientation": source_ir.identity.orientation, + "source_basis": source_ir.basis, + "source_ir": descriptor.source_ir_payload, + "applied_crossing": descriptor.applied_crossing_payload, + "source_helicity": current.source_helicity, + "chirality": current.index.chirality, + "spin_state": current_slot["spin_state"], + "dimension": current.dimension, + "helicity_ancestry": current_slot["helicity_ancestry"], + "color_state": color_state_payload, + } + + +def _model_payload(dag: GenericDAG, model: Model) -> dict[str, object]: + particle_ids = {current.index.particle_id for current in dag.currents} + vertex_kinds = {interaction.vertex_kind for interaction in dag.interactions} + vertex_kinds.update( + root.vertex_kind for root in dag.amplitude_roots if root.vertex_kind is not None + ) + return { + "name": model.name, + "particles": [ + { + "pdg": particle.pdg, + "anti_pdg": particle.anti_pdg, + "spin": particle.spin, + "dimension": particle.dimension, + "color_rep": particle.color_rep, + "mass": particle.mass, + "width": particle.width, + "mass_parameter": _runtime_particle_parameter_name( + model, + particle.pdg, + kind="mass", + ), + "width_parameter": _runtime_particle_parameter_name( + model, + particle.pdg, + kind="width", + ), + "charge": particle.charge, + } + for particle in sorted(model.particles.values(), key=lambda item: item.pdg) + if particle.pdg in particle_ids or particle.anti_pdg in particle_ids + ], + "vertices": [ + { + "kind": vertex.kind, + "particles": list(vertex.particles), + "coupling": list(vertex.coupling), + "lowering": model.vertex_lowering_rule(vertex.kind).to_json_dict(), + } + for vertex in model.vertices + if vertex.kind in vertex_kinds + ], + } + + +def _runtime_particle_parameter_name( + model: Model, + pdg: int, + *, + kind: str, +) -> str | None: + provider = getattr(model, f"runtime_{kind}_parameter_name", None) + if not callable(provider): + return None + name = provider(int(pdg)) + return None if name is None else str(name) + + +def _external_particles(dag: GenericDAG) -> list[dict[str, object]]: + return [ + { + "label": leg.label, + "index": leg.label - 1, + "side": leg.side, + "role": "initial" if leg.is_initial else "final", + "particle": leg.particle, + "outgoing_particle": leg.outgoing_particle, + "pdg": leg.pdg, + "outgoing_pdg": leg.outgoing_pdg, + "statistics": leg.statistics, + "wavefunction_family": leg.wavefunction_family, + "color_role": leg.color_role, + "source_orientation": leg.source_orientation, + "momentum_slot": leg.label - 1, + "momentum_components": ["E", "px", "py", "pz"], + } + for leg in dag.process.legs + ] + + +def _momentum_conventions(dag: GenericDAG) -> dict[str, object]: + incoming = [leg.label for leg in dag.process.initial_legs] + return { + "input_shape": ["batch", len(dag.process.legs), 4], + "component_order": ["E", "px", "py", "pz"], + "input_momenta": "physical external four-momenta in process order", + "incoming_labels": incoming, + "final_state_labels": [leg.label for leg in dag.process.final_legs], + "all_outgoing_convention": { + "crossed_incoming_labels": incoming, + "operation": "negate incoming four-vectors before current/source use", + }, + "metric": "mostly-minus", + } + + +def _input_value_slot( + current: CurrentNode, + model: Model, + value_slots: Mapping[tuple[int, str], Mapping[str, object]], +) -> Mapping[str, object]: + if current.is_source: + variant = "source" + else: + propagator = model._propagator_ir( + current.index.particle_id, + current.index.chirality, + ) + variant = "propagated" if propagator.applies_propagator else "unpropagated" + try: + return value_slots[(current.id, variant)] + except KeyError as exc: + raise ValueError( + f"interaction current {current.id} has no {variant} runtime value slot" + ) from exc + + +def _result_value_slots( + current: CurrentNode, + value_slots: Mapping[tuple[int, str], Mapping[str, object]], +) -> tuple[Mapping[str, object], ...]: + result = tuple( + value_slots[(current.id, variant)] + for variant in ("unpropagated", "propagated") + if (current.id, variant) in value_slots + ) + if result: + return result + if current.is_source: + return (value_slots[(current.id, "source")],) + raise ValueError(f"result current {current.id} has no runtime value slots") + + +def _value_slot_ref(slot: Mapping[str, object]) -> dict[str, object]: + return { + key: slot[key] + for key in ( + "value_slot_id", + "current_id", + "variant", + "component_start", + "component_stop", + "dimension", + ) + } + + +def _spin_state(value: object) -> object: + return list(value) if isinstance(value, tuple) else value + + +def _mask_labels(mask: int) -> tuple[int, ...]: + return tuple(index + 1 for index in range(mask.bit_length()) if mask & (1 << index)) + + +def _mapping(value: object) -> Mapping[str, object]: + if not isinstance(value, Mapping): + raise TypeError("runtime schema value must be an object") + return value + + +def _sequence(value: object) -> Sequence[object]: + if isinstance(value, str | bytes) or not isinstance(value, Sequence): + raise TypeError("runtime schema value must be an array") + return value + + +def _mapping_sequence(value: object) -> tuple[Mapping[str, object], ...]: + return tuple(_mapping(item) for item in _sequence(value)) + + +__all__ = [ + "RUNTIME_PHYSICS_SCHEMA_VERSION", + "RuntimeSchemaLayout", + "build_runtime_expression_schema", + "build_runtime_schema", + "build_runtime_schema_layout", +] diff --git a/pyamplicol/src/pyamplicol/generation/service.py b/pyamplicol/src/pyamplicol/generation/service.py new file mode 100644 index 0000000000..55dc5f2729 --- /dev/null +++ b/pyamplicol/src/pyamplicol/generation/service.py @@ -0,0 +1,8784 @@ +# SPDX-License-Identifier: 0BSD +from __future__ import annotations + +import gc +import hashlib +import importlib +import io +import json +import logging +import math +import os +import time +from collections.abc import Callable, Iterable, Mapping, Sequence +from concurrent.futures import FIRST_COMPLETED, Future, ThreadPoolExecutor, wait +from dataclasses import dataclass, replace +from fractions import Fraction +from pathlib import Path +from tempfile import TemporaryDirectory +from threading import Lock +from types import MappingProxyType +from typing import TYPE_CHECKING, Any, Literal, Protocol, TypeVar, cast + +from pyamplicol._internal.versions import ( + SYMMETRIC_GROUP_FFT_COLOR_RUNTIME_CAPABILITY, + active_source_revision, + verify_native_module, +) +from pyamplicol.api.errors import ( + ArtifactError, + GenerationError, + ModelError, + PyAmpliColError, +) +from pyamplicol.api.models import CompiledModel, _compiled_model_payload +from pyamplicol.api.requests import ( + ModelSource, + ProcessAlias, + ProcessRequest, + ProcessSet, +) +from pyamplicol.api.results import GenerationPlan, GenerationResult, ProcessPhysics +from pyamplicol.artifacts.process_groups import initial_process_groups +from pyamplicol.config import ( + ClampRequest, + ConfigResolution, + GenerationConfig, + RunConfig, + config_to_dict, + resolve_config, +) +from pyamplicol.reporting import ( + ProgressEnd, + ProgressSink, + ProgressStart, +) + +from ..color.fft_basis_strategy import best_general_fft_basis +from ..color.plan import ( + ColorTopologyReplayCertificate, + GenericColorPlan, + LCColorTopologyReplayPlan, + build_color_plan, + build_color_topology_replay_certificate, + build_lc_topology_replay_plan, +) +from ..models.adjoint_color_certificate import adjoint_tree_color_basis_is_proven +from ..models.base import Model +from ..models.loading import CompiledModel as _CompiledModelPayload +from ..models.prepared import PreparedKernelPack, PreparedModelBundle +from ..models.prepared_target import PreparedTargetError, validate_prepared_target +from ..models.recurrence_direct_template import ( + RECURRENCE_DIRECT_TEMPLATE_ABI, + RecurrenceDirectTemplateCatalogV1, +) +from ..models.recurrence_template import RecurrenceTemplateCatalog +from ..processes.ir import CanonicalProcessIR, ProcessLegIR +from ..runtime.recurrence_exact._plan import _RecurrenceExactPlan +from ..runtime.recurrence_exact._plan_v2 import DIRECT_NONE_U32, _parse_exact_sections +from .artifact_writer import ( + EAGER_DIRECT_ARENA_RUNTIME_CAPABILITY, + EAGER_PLAN_V3_ABI, + EAGER_PLAN_V3_RUNTIME_CAPABILITY, + EAGER_RUNTIME_CONTAINER_KIND, + EAGER_RUNTIME_CONTAINER_SCHEMA_VERSION, + EAGER_RUNTIME_LAYOUT_ABI, + EAGER_RUNTIME_STORAGE_ABI, + RECURRENCE_COLOR_RUNTIME_CAPABILITY, + RECURRENCE_CONTRACTED_COLOR_RUNTIME_CAPABILITY, + RECURRENCE_DIRECT_ARENA_RUNTIME_CAPABILITY, + RECURRENCE_PLAN_ABI, + RECURRENCE_RUNTIME_CONTAINER_KIND, + RECURRENCE_RUNTIME_CONTAINER_SCHEMA_VERSION, + RECURRENCE_RUNTIME_LAYOUT_ABI, + RECURRENCE_RUNTIME_STORAGE_ABI, + ArtifactWriteResult, + CompiledColorSelectorExecutionArtifact, + CompiledExecutionArtifact, + CompiledHelicitySelectorExecutionArtifact, + CompiledProcessArtifact, + EagerPlanV3ProcessArtifact, + OnTheFlyProcessArtifact, + RecurrenceHelicitySelectorPlanArtifact, + RecurrenceProcessArtifact, + _GenerationConfigProvenance, + write_schema_v3_artifact, +) +from .contracts import RuntimeExpressionSchema, StageCompilationInput +from .dag_algorithms import ( + _infer_minimal_coupling_order_limits_from_color_plan, + contributing_color_sector_ids, + filter_dag_to_color_sectors, + has_model_supported_amplitudes, + infer_minimal_coupling_order_limits, + prune_dag_to_amplitude_roots, + prune_global_helicity_flip_equivalent_roots, +) +from .dag_compiler import _restrict_color_plan, compile_generic_dag +from .dag_equivalence import ( + NUMERICAL_CURRENT_RELATION_WARNING, + NUMERICAL_CURRENT_RELATION_WARNING_CODE, + dynamic_color_projection_not_applicable_report, + project_rectangular_dynamic_color_classes, +) +from .dag_types import GenericDAG +from .eager_columnar import ( + EAGER_LOWERING_INPUT_ABI, + EagerLoweringInputV1, + build_eager_lowering_input_v1, + runtime_model_parameter_records_from_eager_input, +) +from .eager_lowering import ( + PreparedCatalogEagerKernelIndex, + PreparedCatalogEagerKernelResolver, +) +from .eager_tables import MISSING_U32 +from .evaluator_container import ( + PacbinMemberKind, + PacbinMemberSource, + write_pacbin_atomic, +) +from .helicity_materialization import materialize_helicity_recurrence +from .helicity_replay import ( + HELICITY_RECURRENCE_CONTRACT_VERSION, + build_helicity_recurrence_plan, +) +from .numerical_current_warmup import ( + generic_dag_numerical_current_opt_out_report, + run_generic_dag_numerical_current_warmup, +) +from .on_the_fly_seed import ( + OnTheFlyGenerationProjectionV1, + OnTheFlyProcessSeedProjectionV1, + project_on_the_fly_process_seed_v1, +) +from .physics_metadata import build_resolved_physics_from_dag +from .process_grouping import ( + GroupingBasis, + GroupingParticle, + GroupingProcess, + GroupMember, + LegMapping, + flavor_blind_classes, + flavor_blind_mapping, + particle_table, +) +from .progress import GenerationPhaseReporter, PhaseHandle +from .recurrence_color import ( + RECURRENCE_COLOR_CONTRACTION_CODEC_ABI, + encode_recurrence_color_contraction, + recurrence_color_contraction_digest, +) +from .recurrence_columnar import ( + RECURRENCE_BUILDER_INPUT_ABI, + RecurrenceBuilderInputV1, + RecurrenceBuilderLogicalInputV1, + build_recurrence_builder_input_v1, +) +from .recurrence_numerical_current_warmup import ( + EVIDENCE_ENVELOPE_FALLBACK_REASON, + EVIDENCE_ENVELOPE_FALLBACK_WARNING, + RecurrenceNumericalCurrentWarmupResult, + RecurrenceNumericalEvidenceEnvelopeExceeded, + build_recurrence_numerical_current_probe_points, + numerical_relation_correctness_payload, + recurrence_numerical_current_envelope_fallback_report, + recurrence_numerical_current_opt_out_report, + recurrence_numerical_evidence_capacity_outcome, + recurrence_numerical_relation_correctness_summary, + run_recurrence_numerical_current_warmup, + validate_recurrence_numerical_current_application, + validate_recurrence_numerical_evidence_fallback, +) +from .recurrence_physics import ( + build_on_the_fly_color_contraction, + build_on_the_fly_public_metadata, + build_on_the_fly_runtime_metadata, + build_recurrence_color_contraction, + build_recurrence_normalization, + build_recurrence_physics, + build_recurrence_runtime_metadata, + recurrence_color_contraction_destinations, + recurrence_color_sector_owner_map, + recurrence_exact_color_coefficients, + recurrence_referenced_kernel_ids, +) +from .recurrence_projection import ( + RecurrenceGenerationSliceV1, + project_recurrence_process_v1, +) +from .recurrence_projection import ( + _digest as _recurrence_projection_digest, +) +from .recurrence_schedule_sharing import ( + RecurrenceProcessRemap, + RecurrenceScheduleLoweringCache, + _recurrence_schedule_semantic_digests, + build_recurrence_process_executor_pack, + recurrence_helicity_selector_schedule_digest, +) +from .recurrence_template_columnar import ( + RECURRENCE_TEMPLATE_INPUT_ABI, + RecurrenceTemplateInputV1, + build_recurrence_template_input_v1, +) +from .runtime_schema import ( + RuntimeSchemaLayout, + build_runtime_expression_schema, + build_runtime_schema_layout, +) +from .stage_compiler import ( + build_and_write_generic_stage_evaluator_artifacts, + write_model_parameter_evaluator_artifact, +) +from .validation import ( + ValidationPointRecord, + build_process_validation_point, + build_validation_point, +) + +if TYPE_CHECKING: + from ..licensing import SymbolicaLicenseState + from .artifact_writer import ApiBundleHook, ProcessArtifact + from .process_grouping import ProcessGroupMap + + +_ProcessInput = TypeVar("_ProcessInput") +_ProcessOutput = TypeVar("_ProcessOutput") +_MISSING_PROCESS_RESULT = object() +_LOGGER = logging.getLogger("pyamplicol.generation") +_MAX_FUSED_HELICITY_SELECTOR_SECTORS = 128 +_MAX_POST_BUILD_RESOLVED_COMPONENTS = 1_000_000 +_EAGER_LOWERING_RESULT_KIND = "pyamplicol-eager-runtime-lowering-result" +_EAGER_LOWERING_RESULT_SCHEMA_VERSION = 1 +_RECURRENCE_LOWERING_RESULT_KIND = "pyamplicol-recurrence-direct-lowering-result" +_RECURRENCE_LOWERING_RESULT_SCHEMA_VERSION = 4 +_RECURRENCE_HELICITY_DISPATCH_ABI = "pyamplicol-recurrence-helicity-dispatch-v1" +_RECURRENCE_STRUCTURAL_ZERO_SECTOR_PROOF_KIND = ( + "complete-recurrence-closure-target-domain-v1" +) +_RECURRENCE_GENERATION_PROFILE_TIMINGS = frozenset( + { + "transition-catalog", + "structural-feasibility", + "color-target-index", + "structural-demand", + "support-indexing", + "candidate-processing", + "closure-processing", + "canonical-emission", + "python-extraction", + "catalog-authentication", + "semantic-construction-total", + "direct-lowering", + "serialization", + "native-total", + } +) +_RECURRENCE_GENERATION_PROFILE_COUNTERS = frozenset( + { + "support_bucket_count", + "support_bucket_probe_count", + "support_bucket_cache_hit_count", + "support_bucket_cache_miss_count", + "candidate_parent_pair_theoretical_count", + "candidate_parent_pair_visited_count", + "structural_feasible_support_count", + "structural_decomposition_count", + "structural_forward_transition_probe_count", + "structural_demand_support_count", + "structural_demand_state_count", + "structural_reject_count", + "transition_index_hit_count", + "transition_index_miss_count", + "transition_candidate_count", + "quantum_match_count", + "coupling_match_count", + "transition_accept_count", + "color_shape_match_count", + "color_result_count", + "color_target_accept_count", + "color_target_reject_count", + "color_acceptance_cache_hit_count", + "color_acceptance_cache_miss_count", + "color_fragment_bucket_count", + "color_fragment_hash_lookup_count", + "color_posting_incidence_count", + "color_sparse_posting_bucket_count", + "color_dense_posting_bucket_count", + "color_sparse_posting_bytes", + "color_dense_posting_bytes", + "accepted_parent_key_clone_count", + "current_key_lookup_count", + "current_key_hit_count", + "current_insert_count", + "current_key_clone_count", + "indexed_hash_lookup_count", + "contribution_attempt_count", + "contribution_insert_count", + "contribution_merge_count", + "closure_candidate_theoretical_count", + "closure_candidate_count", + "closure_support_lookup_count", + "closure_state_match_count", + "closure_color_attempt_count", + "closure_group_count", + "closure_proof_contribution_count", + "constructed_current_count", + "constructed_contribution_count", + "constructed_interaction_count", + "constructed_dynamic_color_state_count", + "emitted_current_count", + "emitted_contribution_count", + "emitted_interaction_count", + "emitted_finalization_count", + "emitted_closure_count", + } +) +_RECURRENCE_GENERATION_PROFILE_SERIALIZED_BYTES = frozenset( + {"plan_payload", "container", "unpacked_container"} +) +# Symbolica releases the GIL while retaining process-wide mutable symbol state. +# Keep each complete lowering/compilation transaction atomic across generators. +_SYMBOLICA_MATERIALIZATION_LOCK = Lock() +_RECURRENCE_PROFILE_LOCK = Lock() + + +def _post_build_validation_slices( + physics: ProcessPhysics, + sample_count: int, +) -> tuple[tuple[str, tuple[str, ...], tuple[str, ...]], ...]: + """Keep resolved-output validation below a bounded component count.""" + + helicities = physics.helicities + color_flows = physics.color_flows + contracted = physics.contracted_color_components + color_count = len(color_flows) or len(contracted) + full_component_count = sample_count * len(helicities) * color_count + if full_component_count <= _MAX_POST_BUILD_RESOLVED_COMPONENTS: + return (("complete", (), ()),) + + helicity = next( + (item for item in helicities if item.computed and not item.structural_zero), + next( + (item for item in helicities if not item.structural_zero), + helicities[0], + ), + ) + helicity_ids = (str(helicity.id),) + slices: list[tuple[str, tuple[str, ...], tuple[str, ...]]] = [] + if sample_count * color_count <= _MAX_POST_BUILD_RESOLVED_COMPONENTS: + slices.append(("selected-helicity", helicity_ids, ())) + + if color_flows: + color_flow = next( + (item for item in color_flows if item.computed), + color_flows[0], + ) + color_flow_ids = (str(color_flow.id),) + if sample_count * len(helicities) <= _MAX_POST_BUILD_RESOLVED_COMPONENTS: + slices.append(("selected-flow", (), color_flow_ids)) + if not slices: + slices.append(("selected-helicity-and-flow", helicity_ids, color_flow_ids)) + elif not slices: + slices.append(("selected-helicity", helicity_ids, ())) + return tuple(slices) + + +class _ValidationSelector(Protocol): + id: str + + +class _ValidationHelicity(_ValidationSelector, Protocol): + structural_zero: bool + + +class _ValidationPhysics(Protocol): + color_flows: Sequence[_ValidationSelector] + helicities: Sequence[_ValidationHelicity] + + +def _spread_selector_ids(values: Sequence[_ValidationSelector]) -> tuple[str, ...]: + """Pick deterministic endpoints and a midpoint without copying an axis.""" + + if not values: + return () + positions = (0, len(values) // 2, len(values) - 1) + selected: list[str] = [] + for position in positions: + identifier = str(values[position].id) + if identifier not in selected: + selected.append(identifier) + return tuple(selected) + + +def _recurrence_validation_selector_cases( + physics: _ValidationPhysics, +) -> tuple[tuple[str, dict[str, tuple[str, ...]]], ...]: + """Bound recurrence validation by axis size instead of their product.""" + + color_flows = tuple(physics.color_flows) + helicities = tuple(physics.helicities) + live_helicities = tuple( + helicity for helicity in helicities if not helicity.structural_zero + ) + structural_zeros = tuple( + helicity for helicity in helicities if helicity.structural_zero + ) + color_ids = _spread_selector_ids(color_flows) + helicity_ids = _spread_selector_ids(live_helicities) + cases: list[tuple[str, dict[str, tuple[str, ...]]]] = [] + cases.extend( + ( + f"flow {identifier}", + {"color_flows": (identifier,)}, + ) + for identifier in color_ids + ) + cases.extend( + ( + f"helicity {identifier}", + {"helicities": (identifier,)}, + ) + for identifier in helicity_ids + ) + if color_ids and helicity_ids: + cases.append( + ( + "combined flow and helicity", + { + "color_flows": (color_ids[0],), + "helicities": (helicity_ids[0],), + }, + ) + ) + if color_ids and structural_zeros: + zero_id = str(structural_zeros[0].id) + cases.append( + ( + "structural-zero helicity", + { + "color_flows": (color_ids[0],), + "helicities": (zero_id,), + }, + ) + ) + return tuple(cases) + + +class _RustEagerLoweringBinding(Protocol): + def __call__( + self, + lowering_input: EagerLoweringInputV1, + destination: str, + /, + ) -> object: ... + + +class _RustRecurrenceLoweringBinding(Protocol): + def __call__( + self, + builder_input: RecurrenceBuilderInputV1, + template_input: RecurrenceTemplateInputV1, + direct_template_catalog_json: bytes, + prepared_kernel_pack_digest: str, + schedule_semantic_digest: str, + destination: str, + /, + *, + point_tile_size: int, + workspace_mib: int, + relation_discovery_mode: str, + relation_discovery_precision_digits: int, + relation_discovery_probe_count: int, + relation_discovery_verification_probe_count: int, + relation_discovery_relative_tolerance: float, + relation_discovery_absolute_tolerance: float, + relation_discovery_seed: int, + color_accuracy: str, + relation_discovery_evidence_json: bytes | None, + helicity_dispatch_destination: str | None, + progress_callback: Callable[[Mapping[str, object]], None] | None, + ) -> object: ... + + +class _RustOnTheFlySeedsBinding(Protocol): + def __call__( + self, + ordered_source_projection_jsons: tuple[bytes, ...], + recurrence_template_catalog_json: bytes, + direct_template_catalog_json: bytes, + prepared_kernel_pack_digest: str, + /, + ) -> object: ... + + +class _RustOnTheFlySeedInspectorBinding(Protocol): + def __call__(self, payload: bytes, /) -> object: ... + + +@dataclass(frozen=True, slots=True) +class _RustEagerLoweringOutput: + inspection_summary: Mapping[str, object] + payload_path: Path + payload_size_bytes: int + payload_sha256: str + member_count: int + unpacked_size_bytes: int + index_sha256: str + + +@dataclass(frozen=True, slots=True) +class _RustRecurrenceHelicityDispatchOutput: + payload_path: Path + payload_size_bytes: int + payload_sha256: str + base_runtime_layout_digest: str + resolved_helicity_count: int + + +@dataclass(frozen=True, slots=True) +class _RustRecurrenceStructuralZeroSectorCertificate: + sector_ids: tuple[int, ...] + semantic_digest: str + sector_count: int + resolved_helicity_count: int + + +@dataclass(frozen=True, slots=True) +class _RustRecurrenceLoweringOutput: + inspection_summary: Mapping[str, object] + resolved_helicities: tuple[tuple[int, ...], ...] + amplitude_destinations: tuple[tuple[int, int | None], ...] + structural_zero_physical_sector_certificate: ( + _RustRecurrenceStructuralZeroSectorCertificate | None + ) + exact_sections: Mapping[str, object] + generation_profile: Mapping[str, object] + numerical_current_reuse_report: Mapping[str, object] | None + payload_path: Path + payload_size_bytes: int + payload_sha256: str + member_count: int + unpacked_size_bytes: int + index_sha256: str + helicity_dispatch: _RustRecurrenceHelicityDispatchOutput | None = None + + +def _process_scoped_recurrence_inspection_summary( + inspection_summary: Mapping[str, object], + *, + process_id: str, + semantic_digest: str, + schedule_digest: str, +) -> dict[str, object]: + """Bind native inspection metadata to its published process identity.""" + + result = dict(inspection_summary) + result["process_id"] = process_id + result["semantic_digest"] = semantic_digest + result["schedule_digest"] = schedule_digest + return result + + +def _required_process_direct_executor_ids( + exact_sections: Mapping[str, object], + remap: object, +) -> tuple[int, ...]: + raw_groups = exact_sections.get("row_groups") + if isinstance(raw_groups, str | bytes) or not isinstance(raw_groups, Sequence): + raise GenerationError( + "recurrence exact sections have no typed row-group sequence" + ) + root_ids: set[int] = set() + for index, raw_group in enumerate(raw_groups): + if ( + isinstance(raw_group, str | bytes) + or not isinstance(raw_group, Sequence) + or len(raw_group) != 6 + ): + raise GenerationError( + f"recurrence row group {index} does not match plan-v2" + ) + raw_executor_id = raw_group[3] + if ( + isinstance(raw_executor_id, bool) + or not isinstance(raw_executor_id, int) + or raw_executor_id < 0 + or raw_executor_id > DIRECT_NONE_U32 + ): + raise GenerationError( + f"recurrence row group {index} has an invalid executor ID" + ) + if raw_executor_id != DIRECT_NONE_U32: + root_ids.add(raw_executor_id) + changes = getattr(remap, "direct_executor_changes", None) + count = getattr(remap, "direct_executor_count", None) + if ( + not isinstance(changes, tuple) + or isinstance(count, bool) + or not isinstance(count, int) + ): + raise GenerationError("recurrence process has no typed executor remap") + mapping = dict(changes) + result = tuple( + sorted(mapping.get(executor_id, executor_id) for executor_id in root_ids) + ) + if ( + len(result) != len(root_ids) + or len(set(result)) != len(result) + or any(executor_id < 0 or executor_id >= count for executor_id in result) + ): + raise GenerationError( + "recurrence process executor remap does not preserve its required set" + ) + return result + + +def _validated_recurrence_evidence_fallback( + value: object, +) -> dict[str, object]: + """Validate the one narrow recurrence evidence-fallback record.""" + + try: + return validate_recurrence_numerical_evidence_fallback(value) + except ValueError as exc: + raise GenerationError(str(exc)) from exc + + +def _recurrence_relation_reporting( + inspection: Mapping[str, object], + *, + mode: str, + lane_report: Mapping[str, object], + zero_certificate_baseline_reused: bool = False, +) -> tuple[dict[str, object], dict[str, object]]: + """Separate strict runtime inspection from artifact-wide provenance.""" + + if mode not in {"off", "diagnostic", "certified-reuse"}: + raise GenerationError(f"unsupported recurrence numerical current mode {mode!r}") + certified = _result_integer( + lane_report.get("certified_relation_count"), + "recurrence certified relation count", + minimum=0, + ) + applied = _result_integer( + lane_report.get("applied_relation_count"), + "recurrence applied relation count", + minimum=0, + ) + if lane_report.get("requested_mode") != mode: + raise GenerationError( + "recurrence numerical current lane mode disagrees with generation" + ) + effective_mode = lane_report.get("effective_mode") + effective_reuse_state = lane_report.get("effective_reuse_state") + effective_mode_reason = lane_report.get("effective_mode_reason") + lane_state = lane_report.get("state") + fallback_payload = lane_report.get("fallback") + warning = lane_report.get("warning") + if not isinstance(warning, Mapping): + raise GenerationError( + "recurrence numerical current report has no warning contract" + ) + try: + relation_correctness = recurrence_numerical_relation_correctness_summary( + lane_report + ) + except ValueError as exc: + raise GenerationError(str(exc)) from exc + no_warning: dict[str, object] = { + "required": False, + "emit": "never", + "code": None, + "message": None, + } + validated_fallback: dict[str, object] | None = None + if mode == "off": + if ( + effective_mode != "off" + or effective_reuse_state != "disabled" + or lane_state != "disabled-by-user" + or fallback_payload is not None + or certified != 0 + or applied != 0 + ): + raise GenerationError( + "explicit recurrence numerical opt-out metadata is inconsistent" + ) + aggregate_warning = no_warning + elif effective_mode == "off": + if ( + mode != "certified-reuse" + or effective_reuse_state != "disabled" + or lane_state != "disabled-evidence-envelope-fallback" + or certified != 0 + or applied != 0 + ): + raise GenerationError( + "recurrence numerical reuse fallback metadata is inconsistent" + ) + validated_fallback = _validated_recurrence_evidence_fallback(fallback_payload) + aggregate_warning = { + "required": True, + "emit": "once-per-generated-artifact", + "code": EVIDENCE_ENVELOPE_FALLBACK_REASON, + "message": EVIDENCE_ENVELOPE_FALLBACK_WARNING, + } + elif effective_mode in {"diagnostic", "certified-reuse"}: + expected_applied = certified if effective_mode == "certified-reuse" else 0 + application_scope = lane_report.get("application_scope") + if ( + effective_mode == "certified-reuse" + and isinstance(application_scope, Mapping) + and application_scope.get("contracted_opposite_application") == "disabled" + ): + application = lane_report.get("application") + relation_counts = ( + application.get("relation_kind_counts") + if isinstance(application, Mapping) + else None + ) + if not isinstance(relation_counts, Mapping): + raise GenerationError( + "recurrence numerical application has no relation-kind counts" + ) + expected_applied -= _result_integer( + relation_counts.get("opposite"), + "recurrence suppressed opposite relation count", + minimum=0, + ) + expected_state = ( + "no_certified_numerical_relation" + if certified == 0 + else ( + "authenticated-numerical-applied" + if effective_mode == "certified-reuse" + else "authenticated-numerical-diagnostic-only" + ) + ) + expected_reuse_state = ( + "enabled" + if effective_mode == "certified-reuse" or mode == "diagnostic" + else "disabled" + ) + if mode == "certified-reuse" and effective_mode == "diagnostic": + if ( + effective_mode_reason + not in { + "multi-helicity-all-flow-member-scope-unproven", + "contracted-opposite-binary64-disabled", + } + or not isinstance(application_scope, Mapping) + or application_scope.get("effective_mode_reason") + != effective_mode_reason + ): + raise GenerationError( + "recurrence numerical diagnostic containment has no " + "application-scope reason" + ) + elif effective_mode_reason is not None: + raise GenerationError( + "recurrence numerical current mode has a spurious downgrade reason" + ) + if ( + (effective_mode == "certified-reuse" and mode != "certified-reuse") + or effective_reuse_state != expected_reuse_state + or lane_state != expected_state + or fallback_payload is not None + or applied != expected_applied + ): + raise GenerationError( + "active recurrence numerical current metadata is inconsistent" + ) + aggregate_warning = ( + { + "required": True, + "emit": "once-per-generated-artifact", + "code": NUMERICAL_CURRENT_RELATION_WARNING_CODE, + "message": NUMERICAL_CURRENT_RELATION_WARNING, + } + if applied + else no_warning + ) + else: + raise GenerationError( + "recurrence numerical current effective mode is inconsistent" + ) + if dict(warning) != aggregate_warning: + raise GenerationError( + "recurrence numerical current report changed its warning contract" + ) + warning_required = bool(aggregate_warning["required"]) + warning_code = aggregate_warning["code"] + runtime_inspection = dict(inspection) + native = runtime_inspection.pop("relation_discovery", None) + if native is not None and not isinstance(native, Mapping): + raise GenerationError( + "native recurrence relation application report is not a mapping" + ) + if effective_mode == "off": + if native is not None: + raise GenerationError( + "disabled recurrence numerical current lane published application state" + ) + elif native is None and zero_certificate_baseline_reused: + if certified != 0 or applied != 0: + raise GenerationError( + "recurrence baseline reuse requires an empty certificate set" + ) + else: + if not isinstance(native, Mapping): + raise GenerationError( + "recurrence numerical current lane has no native application report" + ) + native_certified = _result_integer( + native.get("exact_certified_relation_count"), + "native recurrence certified relation count", + minimum=0, + ) + native_applied = _result_integer( + native.get("applied_relation_count"), + "native recurrence applied relation count", + minimum=0, + ) + native_certificate_count = _result_integer( + native.get("certificate_count"), + "native recurrence certificate count", + minimum=0, + ) + if ( + native.get("requested_mode") != effective_mode + or native_certified != certified + or native_applied != applied + or native_certificate_count != certified + ): + raise GenerationError( + "native recurrence relation counts disagree with the " + "authenticated warm-up" + ) + if warning_required != (applied > 0) or ( + warning_required and warning_code != NUMERICAL_CURRENT_RELATION_WARNING_CODE + ): + raise GenerationError( + "recurrence numerical current application or warning state " + "is inconsistent" + ) + application = lane_report.get("application") + native_replay = native.get("certificate_replay") + if not isinstance(application, Mapping) or not isinstance( + native_replay, Mapping + ): + raise GenerationError( + "recurrence numerical current certificate replay metadata is absent" + ) + lane_replay = application.get("certificate_replay") + if not isinstance(lane_replay, Mapping) or any( + native_replay.get(key) != lane_replay.get(key) + for key in ("algorithm", "certificate_set_sha256") + ): + raise GenerationError( + "native recurrence certificate replay disagrees with the warm-up" + ) + candidate_capture = lane_report.get("candidate_capture") + verification_capture = lane_report.get("verification_capture") + native_probe = native.get("probe") + if ( + not isinstance(candidate_capture, Mapping) + or not isinstance(verification_capture, Mapping) + or not isinstance(native_probe, Mapping) + or native_probe.get("probe_count") != candidate_capture.get("point_count") + or native_probe.get("verification_probe_count") + != verification_capture.get("point_count") + ): + raise GenerationError( + "native recurrence probe counts disagree with the warm-up" + ) + discovery = lane_report.get("discovery") + if not isinstance(discovery, Mapping) or any( + native_value != discovery.get(discovery_key) + for native_value, discovery_key in ( + ( + native.get("numerical_candidate_count"), + "numerical_candidate_count", + ), + ( + native_probe.get("verification_rejected_count"), + "verification_rejected_count", + ), + ( + native.get("uncertified_candidate_count"), + "verification_rejected_count", + ), + ( + native.get("rejected_hypothesis_count"), + "rejected_hypothesis_count", + ), + ( + native_probe.get("tested_hypothesis_count"), + "tested_hypothesis_count", + ), + ) + ): + raise GenerationError( + "native recurrence candidate counters disagree with the warm-up" + ) + candidate_commitment = candidate_capture.get("observation_batch_sha256") + verification_commitment = verification_capture.get("observation_batch_sha256") + if any( + native_probe.get(key) != expected + for key, expected in ( + ( + "runtime_parameter_schema_sha256", + candidate_capture.get("runtime_parameter_schema_sha256"), + ), + ( + "candidate_observation_batch_sha256", + candidate_commitment, + ), + ( + "verification_observation_batch_sha256", + verification_commitment, + ), + ("decision_sha256", discovery.get("decision_sha256")), + ( + "rejection_decision_sha256", + discovery.get("rejection_decision_sha256"), + ), + ) + ): + raise GenerationError( + "native recurrence raw-evidence commitments disagree with " + "the authenticated warm-up" + ) + rejected_diagnostics = native.get("rejected_candidate_diagnostics") + if ( + not isinstance(rejected_diagnostics, Mapping) + or rejected_diagnostics.get("total_rejected_hypothesis_count") + != discovery.get("rejected_hypothesis_count") + or rejected_diagnostics.get("full_rejection_sha256") + != discovery.get("rejection_decision_sha256") + ): + raise GenerationError( + "native recurrence rejected-hypothesis diagnostics disagree " + "with the authenticated warm-up" + ) + aggregate = { + "schema_version": 1, + "abi": "pyamplicol-artifact-numerical-current-reuse-v1", + "requested_mode": mode, + "effective_mode": effective_mode, + "effective_reuse_state": effective_reuse_state, + "execution_mode": "recurrence", + "lane_count": 1, + "lanes": {"primary": dict(lane_report)}, + "certified_relation_count": certified, + "applied_relation_count": applied, + "relation_correctness": relation_correctness, + "structural_exact_discovery": None, + "opt_out_flag": "--no-numerical-current-reuse", + "fallback": validated_fallback, + "warning": aggregate_warning, + "native_relation_application": (None if native is None else dict(native)), + } + return runtime_inspection, aggregate + + +def _publish_validated_recurrence_baseline( + *, + baseline: _RustRecurrenceLoweringOutput, + final_schedule_path: Path, +) -> _RustRecurrenceLoweringOutput: + """Move one validated mode-off payload to its final destination.""" + + if final_schedule_path.exists() or final_schedule_path.is_symlink(): + raise GenerationError( + f"recurrence schedule destination already exists: {final_schedule_path}" + ) + try: + final_schedule_path.parent.mkdir(parents=True, exist_ok=True) + baseline.payload_path.replace(final_schedule_path) + except OSError as exc: + raise GenerationError( + "failed to publish the validated recurrence baseline payload" + ) from exc + return replace(baseline, payload_path=final_schedule_path) + + +def _lower_recurrence_after_numerical_warmup( + *, + lower_once: Callable[..., _RustRecurrenceLoweringOutput], + final_schedule_path: Path, + baseline: _RustRecurrenceLoweringOutput, + warmup: RecurrenceNumericalCurrentWarmupResult, + requested_mode: str, +) -> tuple[_RustRecurrenceLoweringOutput, bool]: + """Lower any certified relation set, or publish its unchanged baseline.""" + + if warmup.certificates: + return ( + lower_once( + final_schedule_path, + mode=warmup.effective_mode, + evidence=warmup.evidence_json, + report_progress=False, + timing_name="native-final-generation", + ), + True, + ) + runtime_inspection, aggregate_report = _recurrence_relation_reporting( + baseline.inspection_summary, + mode=requested_mode, + lane_report=warmup.to_json_dict(), + zero_certificate_baseline_reused=True, + ) + published = _publish_validated_recurrence_baseline( + baseline=baseline, + final_schedule_path=final_schedule_path, + ) + return ( + replace( + published, + inspection_summary=runtime_inspection, + generation_profile={ + "schema_version": 1, + "native_passes": {"final": dict(baseline.generation_profile)}, + }, + numerical_current_reuse_report=aggregate_report, + ), + False, + ) + + +def _complete_recurrence_evidence_envelope_fallback( + *, + final_schedule_path: Path, + baseline: _RustRecurrenceLoweringOutput, + baseline_plan: _RecurrenceExactPlan, + color_accuracy: str, + outcome: RecurrenceNumericalEvidenceEnvelopeExceeded, + warmup_owner: list[RecurrenceNumericalCurrentWarmupResult] | None = None, +) -> _RustRecurrenceLoweringOutput: + """Publish the validated mode-off baseline after a reuse fallback.""" + + if warmup_owner is None: + warmup = None + else: + if len(warmup_owner) != 1: + raise AssertionError( + "recurrence fallback requires exactly one warm-up owner" + ) + warmup = warmup_owner.pop() + warmup.close() + # Drop the transport, both captures and their spool owners before the + # baseline payload is published. The caller has already cleared exception + # chains and transferred its only warm-up reference through this owner. + warmup = None + lane_report = recurrence_numerical_current_envelope_fallback_report( + baseline_plan.sections, + color_accuracy=color_accuracy, + requested_mode="certified-reuse", + outcome=outcome, + ) + runtime_inspection, aggregate_report = _recurrence_relation_reporting( + baseline.inspection_summary, + mode="certified-reuse", + lane_report=lane_report, + ) + published = _publish_validated_recurrence_baseline( + baseline=baseline, + final_schedule_path=final_schedule_path, + ) + return replace( + published, + inspection_summary=runtime_inspection, + generation_profile={ + "schema_version": 1, + "native_passes": {"final": dict(baseline.generation_profile)}, + }, + numerical_current_reuse_report=aggregate_report, + ) + + +def _detached_recurrence_evidence_envelope_outcome( + outcome: RecurrenceNumericalEvidenceEnvelopeExceeded, + *, + source_error: BaseException, +) -> RecurrenceNumericalEvidenceEnvelopeExceeded: + """Copy fallback metadata while releasing exception-retained work state.""" + + detached = RecurrenceNumericalEvidenceEnvelopeExceeded( + outcome.geometry, + memory_envelope_bytes=outcome.memory_envelope_bytes, + spooled_producer_resident_bytes=(outcome.spooled_producer_resident_bytes), + raw_reason=outcome.raw_reason, + ) + pending = [source_error] + if outcome is not source_error: + pending.append(outcome) + seen: set[int] = set() + while pending: + error = pending.pop() + identity = id(error) + if identity in seen: + continue + seen.add(identity) + cause = error.__cause__ + context = error.__context__ + error.__traceback__ = None + error.__cause__ = None + error.__context__ = None + if cause is not None: + pending.append(cause) + if context is not None: + pending.append(context) + return detached + + +def _invoke_rust_eager_lowering_v1( + lowering_input: EagerLoweringInputV1, + destination: Path, +) -> _RustEagerLoweringOutput: + try: + module = importlib.import_module("pyamplicol._rusticol") + verify_native_module(module) + except ImportError as exc: + raise GenerationError( + "eager generation requires pyamplicol._rusticol with the " + "eager plan-v3 lowering binding" + ) from exc + candidate = getattr(module, "_lower_eager_runtime_v1", None) + if not callable(candidate): + raise GenerationError( + "pyamplicol._rusticol does not provide the required private " + "_lower_eager_runtime_v1 binding for eager plan-v3 generation" + ) + binding = cast(_RustEagerLoweringBinding, candidate) + destination.parent.mkdir(parents=True, exist_ok=True) + if destination.exists() or destination.is_symlink(): + raise GenerationError( + f"Rust eager lowering destination already exists: {destination}" + ) + try: + raw_result = binding(lowering_input, os.fspath(destination)) + result = _validate_rust_eager_lowering_result(raw_result, lowering_input) + if destination.is_symlink() or not destination.is_file(): + raise GenerationError( + "Rust eager lowering did not produce a regular eager-runtime.pacbin" + ) + payload_size = destination.stat().st_size + if payload_size <= 0: + raise GenerationError( + "Rust eager lowering produced an empty runtime payload" + ) + payload_sha256 = _file_sha256(destination) + except Exception as exc: + _discard_rust_eager_output(destination) + if isinstance(exc, GenerationError): + raise + raise GenerationError(f"Rust eager plan-v3 lowering failed: {exc}") from exc + return _RustEagerLoweringOutput( + inspection_summary=result["inspection_summary"], + payload_path=destination, + payload_size_bytes=payload_size, + payload_sha256=payload_sha256, + member_count=cast(int, result["member_count"]), + unpacked_size_bytes=cast(int, result["unpacked_size_bytes"]), + index_sha256=cast(str, result["index_sha256"]), + ) + + +def _invoke_rust_on_the_fly_seed_batch_builder_v1( + ordered_source_projection_jsons: Sequence[bytes], + recurrence_template_catalog_json: bytes, + direct_template_catalog_json: bytes, + prepared_kernel_pack_digest: str, + *, + process_identities: Sequence[str] = (), +) -> tuple[bytes, ...]: + ordered_sources = tuple(ordered_source_projection_jsons) + identities = tuple(process_identities) + if identities and len(identities) != len(ordered_sources): + raise GenerationError( + "on-the-fly process identities do not align with source projections" + ) + try: + module = importlib.import_module("pyamplicol._rusticol") + verify_native_module(module) + except (ImportError, OSError, RuntimeError) as exc: + raise GenerationError( + "on-the-fly generation requires the current native " + "_build_on_the_fly_process_seeds_v1 binding" + ) from exc + candidate = getattr(module, "_build_on_the_fly_process_seeds_v1", None) + if not callable(candidate): + raise GenerationError( + "pyamplicol._rusticol does not provide the private " + "_build_on_the_fly_process_seeds_v1 binding" + ) + binding = cast(_RustOnTheFlySeedsBinding, candidate) + try: + result = binding( + ordered_sources, + recurrence_template_catalog_json, + direct_template_catalog_json, + prepared_kernel_pack_digest, + ) + except Exception as exc: + detail = str(exc) + attributed = next( + ( + (index, identities[index]) + for index in range(len(identities) - 1, -1, -1) + if f"index {index} " in detail + ), + None, + ) + context = ( + "" + if attributed is None + else f" for process index {attributed[0]} {attributed[1]!r}" + ) + raise GenerationError( + f"on-the-fly process-seed construction failed{context}: {detail}" + ) from exc + if not isinstance(result, (list, tuple)) or len(result) != len(ordered_sources): + raise GenerationError( + "native on-the-fly process-seed builder returned a result with the " + "wrong process count" + ) + payloads: list[bytes] = [] + for index, payload in enumerate(result): + if not isinstance(payload, bytes) or not payload: + identity = "" if not identities else f" {identities[index]!r}" + raise GenerationError( + "native on-the-fly process-seed builder returned an empty payload " + f"for process index {index}{identity}" + ) + payloads.append(payload) + return tuple(payloads) + + +def _invoke_rust_on_the_fly_seed_builder_v1( + source_projection_json: bytes, + recurrence_template_catalog_json: bytes, + direct_template_catalog_json: bytes, + prepared_kernel_pack_digest: str, +) -> bytes: + """Retain the original singleton bridge as a plural-path wrapper.""" + + return _invoke_rust_on_the_fly_seed_batch_builder_v1( + (source_projection_json,), + recurrence_template_catalog_json, + direct_template_catalog_json, + prepared_kernel_pack_digest, + )[0] + + +_ON_THE_FLY_PROCESS_SEED_IDENTITY_ABI = "pyamplicol-on-the-fly-process-seed-identity-v1" +_U64_MAX = (1 << 64) - 1 +_ON_THE_FLY_PROCESS_SEED_IDENTITY_FIELDS = frozenset( + { + "abi", + "process_digest", + "compiled_model_digest", + "recurrence_template_catalog_digest", + "prepared_kernel_pack_digest", + "recurrence_direct_template_catalog_digest", + "semantic_digest", + "external_permutation", + "external_sources", + } +) + + +def _on_the_fly_selector_census_v1( + projection: OnTheFlyProcessSeedProjectionV1, + process: CanonicalProcessIR, +) -> dict[str, int]: + """Count complete compact selector axes without materializing either axis.""" + + helicity_count = math.prod( + len(source.source_states) for source in projection.external_sources + ) + if len(projection.external_sources) != len(process.legs) or any( + leg.color_role == "inclusive" for leg in process.legs + ): + raise GenerationError( + "on-the-fly selector census requires one concrete source per process leg" + ) + fundamental_count = len(process.fundamental_labels) + antifundamental_count = len(process.antifundamental_labels) + adjoint_count = len(process.adjoint_labels) + if fundamental_count != antifundamental_count: + raise GenerationError( + "on-the-fly selector census found unbalanced LC open-line endpoints" + ) + if fundamental_count: + color_flow_count = ( + math.factorial(fundamental_count) + * math.factorial(adjoint_count) + * math.comb( + adjoint_count + fundamental_count - 1, + fundamental_count - 1, + ) + ) + elif adjoint_count: + color_flow_count = math.factorial(max(adjoint_count - 1, 0)) + else: + color_flow_count = 1 + census = { + "physical_helicity_count": helicity_count, + # Contracted NLC/full exposes one physical result; ``color_flow_count`` + # remains the private structural-selector domain bound by the color + # payload destinations. + "physical_color_flow_count": ( + color_flow_count if getattr(process, "color_accuracy", "lc") == "lc" else 1 + ), + } + for name, count in census.items(): + if count < 1 or count > _U64_MAX: + raise GenerationError( + f"on-the-fly selector census {name} is outside the positive u64 domain" + ) + return census + + +def _on_the_fly_validation_selectors_v1( + projection: OnTheFlyProcessSeedProjectionV1, + *, + color_accuracy: str = "lc", +) -> Mapping[str, tuple[str, ...]]: + """Choose one deterministic compact selector without opening dense axes.""" + + helicities: list[int] = [] + for source_index, source in enumerate(projection.external_sources): + if not source.source_states: + raise GenerationError( + "on-the-fly validation source " + f"{source_index} exposes no public helicity states" + ) + helicities.append(int(source.source_states[0].public_helicity)) + if not helicities: + raise GenerationError("on-the-fly validation requires external sources") + helicity_id = "h:" + ",".join(f"{value:+d}" for value in helicities) + return MappingProxyType( + { + "helicities": (helicity_id,), + "color_flows": (() if color_accuracy in {"nlc", "full"} else ("1",)), + } + ) + + +def _build_on_the_fly_contracted_color_payload_v1( + color_plan: GenericColorPlan, + *, + contraction: str = "direct", +) -> _OnTheFlyContractedColorPayloadV1: + """Encode the contracted metric over compact structural selector ordinals.""" + + contraction, sector_owner_ids, owner_sector_ids = ( + build_on_the_fly_color_contraction( + color_plan, + contraction=contraction, + ) + ) + group_count = contraction.group_count + destination_count = len(owner_sector_ids) + destination_by_group = ( + tuple(range(group_count)) + if contraction.destination_by_group is None + else contraction.destination_by_group + ) + if ( + len(destination_by_group) != group_count + or len(set(destination_by_group)) != group_count + or any( + destination_id < 0 or destination_id >= destination_count + for destination_id in destination_by_group + ) + ): + raise ValueError( + "on-the-fly color contraction has an invalid destination projection" + ) + normalized_owner_sector_ids = tuple( + owner_sector_ids[destination_id] for destination_id in destination_by_group + ) + payload = encode_recurrence_color_contraction( + contraction, + sector_count=color_plan.sector_count, + component_count=1, + ordered_group_ids=tuple(range(group_count)), + destination_by_group=destination_by_group, + destination_count=destination_count, + group_sector_ids=normalized_owner_sector_ids, + group_component_ids=(0,) * group_count, + sector_owner_ids=sector_owner_ids, + exact_coefficients=recurrence_exact_color_coefficients( + color_plan, + contraction, + normalized_owner_sector_ids, + ), + ) + semantic_digest = recurrence_color_contraction_digest(payload) + symmetric_group = contraction.symmetric_group_block + summary = MappingProxyType( + { + "abi": RECURRENCE_COLOR_CONTRACTION_CODEC_ABI, + "color_accuracy": contraction.color_accuracy, + "storage": ( + "convolution-kernels" if symmetric_group is not None else "expanded" + ), + "includes_color_factor": contraction.includes_color_factor, + "group_count": group_count, + "sector_count": color_plan.sector_count, + "active_sector_count": group_count, + "component_count": 1, + "destination_count": destination_count, + "entry_count": ( + len(contraction.entries) + if symmetric_group is None + else symmetric_group.stored_entry_count + ), + "logical_entry_count": contraction.logical_entry_count, + "semantic_digest": semantic_digest, + "factorization": ( + None + if symmetric_group is None + else { + "kind": "symmetric-group-fourier", + "rank": symmetric_group.degree, + "coset_count": symmetric_group.channel_count, + } + ), + **( + {} + if symmetric_group is None + else { + "fft_provenance": { + "method": "symmetric-group-fourier", + "degree": symmetric_group.degree, + "channel_count": symmetric_group.channel_count, + "covered_local_group_count": ( + symmetric_group.channel_count + * math.factorial(symmetric_group.degree) + ), + "residual_group_count": len( + symmetric_group.residual_local_group_indices + ), + "residual_entry_count": len(symmetric_group.residual_entries), + "raw_kernel_bytes": len(symmetric_group.kernel_entries) * 16, + "transformed_kernel_bytes": ( + len(symmetric_group.kernel_entries) * 8 + ), + "capability": (SYMMETRIC_GROUP_FFT_COLOR_RUNTIME_CAPABILITY), + } + } + ), + } + ) + return _OnTheFlyContractedColorPayloadV1( + payload=payload, + summary=summary, + owner_sector_ids=normalized_owner_sector_ids, + destination_by_group=destination_by_group, + ) + + +def _validate_on_the_fly_process_seed_identity_v1( + value: object, +) -> dict[str, object]: + if not isinstance(value, Mapping) or set(value) != ( + _ON_THE_FLY_PROCESS_SEED_IDENTITY_FIELDS + ): + raise GenerationError( + "native on-the-fly process-seed identity has the wrong fields" + ) + result = {str(key): item for key, item in value.items()} + if result["abi"] != _ON_THE_FLY_PROCESS_SEED_IDENTITY_ABI: + raise GenerationError( + "native on-the-fly process-seed identity has an unsupported ABI" + ) + for field in ( + "process_digest", + "compiled_model_digest", + "recurrence_template_catalog_digest", + "prepared_kernel_pack_digest", + "recurrence_direct_template_catalog_digest", + "semantic_digest", + ): + digest = result[field] + if ( + not isinstance(digest, str) + or len(digest) != 64 + or any(character not in "0123456789abcdef" for character in digest) + ): + raise GenerationError( + f"native on-the-fly process-seed identity {field} is not SHA-256" + ) + permutation = result["external_permutation"] + sources = result["external_sources"] + if ( + not isinstance(permutation, list) + or not isinstance(sources, list) + or not sources + or len(permutation) != len(sources) + or any( + isinstance(slot, bool) or not isinstance(slot, int) for slot in permutation + ) + or sorted(permutation) != list(range(len(permutation))) + ): + raise GenerationError( + "native on-the-fly process-seed identity has an invalid external domain" + ) + labels: set[int] = set() + for source_index, raw_source in enumerate(sources): + if not isinstance(raw_source, Mapping) or set(raw_source) != { + "source_slot", + "public_label", + "is_initial", + "states", + }: + raise GenerationError( + "native on-the-fly process-seed identity has an invalid source row" + ) + source_slot = raw_source["source_slot"] + public_label = raw_source["public_label"] + states = raw_source["states"] + if ( + isinstance(source_slot, bool) + or source_slot != source_index + or isinstance(public_label, bool) + or not isinstance(public_label, int) + or public_label < 1 + or public_label in labels + or not isinstance(raw_source["is_initial"], bool) + or not isinstance(states, list) + or not states + ): + raise GenerationError( + "native on-the-fly process-seed identity has a noncanonical " + "source domain" + ) + labels.add(public_label) + previous_state_index = -1 + for raw_state in states: + if not isinstance(raw_state, Mapping) or set(raw_state) != { + "state_index", + "public_helicity", + "prepared_mass_parameter_slot", + }: + raise GenerationError( + "native on-the-fly process-seed identity has an invalid " + "source state" + ) + state_index = raw_state["state_index"] + helicity = raw_state["public_helicity"] + mass_slot = raw_state["prepared_mass_parameter_slot"] + if ( + isinstance(state_index, bool) + or not isinstance(state_index, int) + or state_index <= previous_state_index + or isinstance(helicity, bool) + or not isinstance(helicity, int) + or ( + mass_slot is not None + and ( + isinstance(mass_slot, bool) + or not isinstance(mass_slot, int) + or mass_slot < 0 + ) + ) + ): + raise GenerationError( + "native on-the-fly process-seed identity has a noncanonical " + "source state" + ) + previous_state_index = state_index + return result + + +def _bind_on_the_fly_process_seed_identity_v1( + identity: Mapping[str, object], + projection: OnTheFlyProcessSeedProjectionV1, +) -> None: + """Bind the native seed identity to its authoritative Python projection.""" + + if identity["process_digest"] != projection.process_digest: + raise GenerationError( + "native on-the-fly process-seed process digest disagrees with " + "the source projection" + ) + if identity["external_permutation"] != list(projection.external_permutation): + raise GenerationError( + "native on-the-fly process-seed external permutation disagrees " + "with the source projection" + ) + + native_sources = cast(list[Mapping[str, object]], identity["external_sources"]) + native_domain = tuple( + ( + source["source_slot"], + source["public_label"], + source["is_initial"], + tuple( + (state["state_index"], state["public_helicity"]) + for state in cast(list[Mapping[str, object]], source["states"]) + ), + ) + for source in native_sources + ) + projected_domain = tuple( + ( + source.source_slot, + source.public_label, + source.is_initial, + tuple( + (state.state_index, state.public_helicity) + for state in source.source_states + ), + ) + for source in projection.external_sources + ) + if native_domain != projected_domain: + raise GenerationError( + "native on-the-fly process-seed external source domain " + "disagrees with the source projection" + ) + + +def _invoke_rust_on_the_fly_seed_inspector_v1( + seed_payload: bytes, +) -> dict[str, object]: + try: + module = importlib.import_module("pyamplicol._rusticol") + verify_native_module(module) + except (ImportError, OSError, RuntimeError) as exc: + raise GenerationError( + "on-the-fly generation requires the current native " + "_inspect_on_the_fly_process_seed_v1 binding" + ) from exc + candidate = getattr(module, "_inspect_on_the_fly_process_seed_v1", None) + if not callable(candidate): + raise GenerationError( + "pyamplicol._rusticol does not provide the private " + "_inspect_on_the_fly_process_seed_v1 binding" + ) + binding = cast(_RustOnTheFlySeedInspectorBinding, candidate) + try: + encoded = binding(seed_payload) + if not isinstance(encoded, str): + raise TypeError("native inspector did not return JSON text") + decoded = json.loads(encoded) + except (TypeError, ValueError) as exc: + raise GenerationError( + f"native on-the-fly process-seed inspection failed: {exc}" + ) from exc + except Exception as exc: + raise GenerationError( + f"native on-the-fly process-seed inspection failed: {exc}" + ) from exc + return _validate_on_the_fly_process_seed_identity_v1(decoded) + + +def _invoke_rust_recurrence_lowering_v2( + builder_input: RecurrenceBuilderInputV1, + template_input: RecurrenceTemplateInputV1, + direct_template_catalog: RecurrenceDirectTemplateCatalogV1, + prepared_kernel_pack_digest: str, + schedule_semantic_digest: str, + destination: Path, + *, + point_tile_size: int, + workspace_mib: int, + relation_discovery_mode: str, + relation_discovery_precision_digits: int, + relation_discovery_probe_count: int, + relation_discovery_verification_probe_count: int, + relation_discovery_relative_tolerance: float, + relation_discovery_absolute_tolerance: float, + relation_discovery_seed: int, + color_accuracy: str, + relation_discovery_evidence_json: bytes | None = None, + helicity_dispatch_destination: Path | None = None, + progress_callback: Callable[[Mapping[str, object]], None] | None = None, + direct_template_catalog_json: bytes | None = None, +) -> _RustRecurrenceLoweringOutput: + try: + module = importlib.import_module("pyamplicol._rusticol") + verify_native_module(module) + except ImportError as exc: + raise GenerationError( + "recurrence generation requires the native " + "_lower_recurrence_direct_v2 binding" + ) from exc + candidate = getattr(module, "_lower_recurrence_direct_v2", None) + if not callable(candidate): + raise GenerationError( + "pyamplicol._rusticol does not provide the private " + "_lower_recurrence_direct_v2 binding" + ) + binding = cast(_RustRecurrenceLoweringBinding, candidate) + destination.parent.mkdir(parents=True, exist_ok=True) + if destination.exists() or destination.is_symlink(): + raise GenerationError( + f"Rust recurrence lowering destination already exists: {destination}" + ) + if helicity_dispatch_destination is not None: + helicity_dispatch_destination.parent.mkdir(parents=True, exist_ok=True) + if ( + helicity_dispatch_destination.exists() + or helicity_dispatch_destination.is_symlink() + ): + raise GenerationError( + "Rust recurrence helicity-dispatch destination already exists: " + f"{helicity_dispatch_destination}" + ) + try: + encoded_direct_template_catalog = ( + json.dumps( + direct_template_catalog.to_dict(), + allow_nan=False, + ensure_ascii=True, + separators=(",", ":"), + sort_keys=True, + ).encode("ascii") + if direct_template_catalog_json is None + else direct_template_catalog_json + ) + raw_result = binding( + builder_input, + template_input, + encoded_direct_template_catalog, + prepared_kernel_pack_digest, + schedule_semantic_digest, + os.fspath(destination), + point_tile_size=point_tile_size, + workspace_mib=workspace_mib, + relation_discovery_mode=relation_discovery_mode, + relation_discovery_precision_digits=(relation_discovery_precision_digits), + relation_discovery_probe_count=relation_discovery_probe_count, + relation_discovery_verification_probe_count=( + relation_discovery_verification_probe_count + ), + relation_discovery_relative_tolerance=( + relation_discovery_relative_tolerance + ), + relation_discovery_absolute_tolerance=( + relation_discovery_absolute_tolerance + ), + relation_discovery_seed=relation_discovery_seed, + color_accuracy=color_accuracy, + relation_discovery_evidence_json=relation_discovery_evidence_json, + helicity_dispatch_destination=( + None + if helicity_dispatch_destination is None + else os.fspath(helicity_dispatch_destination) + ), + progress_callback=progress_callback, + ) + result = _validate_rust_recurrence_lowering_result( + raw_result, + builder_input=builder_input, + template_input=template_input, + prepared_kernel_pack_digest=prepared_kernel_pack_digest, + direct_template_catalog_digest=direct_template_catalog.catalog_digest, + expect_helicity_dispatch=(helicity_dispatch_destination is not None), + ) + if destination.is_symlink() or not destination.is_file(): + raise GenerationError( + "Rust recurrence lowering did not produce a regular " + "recurrence-runtime.pacbin" + ) + payload_size = destination.stat().st_size + if payload_size <= 0: + raise GenerationError( + "Rust recurrence lowering produced an empty runtime payload" + ) + serialized_bytes = cast( + Mapping[str, object], + cast(Mapping[str, object], result["generation_profile"])[ + "serialized_bytes" + ], + ) + if serialized_bytes.get("container") != payload_size: + raise GenerationError( + "Rust recurrence generation serialized-byte telemetry disagrees " + "with the emitted runtime container" + ) + payload_sha256 = _file_sha256(destination) + dispatch_output = None + dispatch_metadata = result["helicity_dispatch"] + if helicity_dispatch_destination is not None: + if ( + helicity_dispatch_destination.is_symlink() + or not helicity_dispatch_destination.is_file() + ): + raise GenerationError( + "Rust recurrence lowering did not produce a regular " + "helicity-dispatch payload" + ) + if not isinstance(dispatch_metadata, Mapping): + raise AssertionError( + "validated recurrence lowering omitted helicity dispatch" + ) + dispatch_size = helicity_dispatch_destination.stat().st_size + dispatch_sha256 = _file_sha256(helicity_dispatch_destination) + if ( + dispatch_size != dispatch_metadata["size_bytes"] + or dispatch_sha256 != dispatch_metadata["sha256"] + ): + raise GenerationError( + "Rust recurrence helicity-dispatch metadata disagrees with " + "the emitted payload" + ) + dispatch_output = _RustRecurrenceHelicityDispatchOutput( + payload_path=helicity_dispatch_destination, + payload_size_bytes=dispatch_size, + payload_sha256=dispatch_sha256, + base_runtime_layout_digest=cast( + str, + dispatch_metadata["base_runtime_layout_digest"], + ), + resolved_helicity_count=cast( + int, + dispatch_metadata["resolved_helicity_count"], + ), + ) + except Exception as exc: + _discard_rust_eager_output(destination) + if helicity_dispatch_destination is not None: + _discard_rust_eager_output(helicity_dispatch_destination) + if isinstance(exc, GenerationError | ArtifactError): + raise + raise GenerationError(f"Rust recurrence lowering failed: {exc}") from exc + return _RustRecurrenceLoweringOutput( + inspection_summary=cast(Mapping[str, object], result["inspection_summary"]), + resolved_helicities=cast( + tuple[tuple[int, ...], ...], result["resolved_helicities"] + ), + amplitude_destinations=cast( + tuple[tuple[int, int | None], ...], + result["amplitude_destinations"], + ), + structural_zero_physical_sector_certificate=cast( + _RustRecurrenceStructuralZeroSectorCertificate | None, + result["structural_zero_physical_sector_certificate"], + ), + exact_sections=cast(Mapping[str, object], result["exact_sections"]), + generation_profile=cast(Mapping[str, object], result["generation_profile"]), + numerical_current_reuse_report=None, + payload_path=destination, + payload_size_bytes=payload_size, + payload_sha256=payload_sha256, + member_count=cast(int, result["member_count"]), + unpacked_size_bytes=cast(int, result["unpacked_size_bytes"]), + index_sha256=cast(str, result["index_sha256"]), + helicity_dispatch=dispatch_output, + ) + + +def _validate_rust_recurrence_lowering_result( + value: object, + *, + builder_input: RecurrenceBuilderInputV1, + template_input: RecurrenceTemplateInputV1, + prepared_kernel_pack_digest: str, + direct_template_catalog_digest: str, + expect_helicity_dispatch: bool, +) -> dict[str, object]: + result = _strict_mapping( + value, + "Rust recurrence lowering result", + { + "kind", + "schema_version", + "builder_input_abi", + "builder_input_sha256", + "template_input_abi", + "template_input_sha256", + "prepared_kernel_pack_digest", + "direct_template_abi", + "direct_template_catalog_digest", + "recurrence_plan_abi", + "runtime_layout_abi", + "required_runtime_capabilities", + "runtime_container", + "inspection_summary", + "resolved_helicities", + "amplitude_destinations", + "structural_zero_physical_sector_certificate", + "helicity_dispatch", + "exact_sections", + "generation_profile", + }, + ) + expected = { + "kind": _RECURRENCE_LOWERING_RESULT_KIND, + "schema_version": _RECURRENCE_LOWERING_RESULT_SCHEMA_VERSION, + "builder_input_abi": RECURRENCE_BUILDER_INPUT_ABI, + "builder_input_sha256": builder_input.digest, + "template_input_abi": RECURRENCE_TEMPLATE_INPUT_ABI, + "template_input_sha256": template_input.digest, + "prepared_kernel_pack_digest": prepared_kernel_pack_digest, + "direct_template_abi": RECURRENCE_DIRECT_TEMPLATE_ABI, + "direct_template_catalog_digest": direct_template_catalog_digest, + "recurrence_plan_abi": RECURRENCE_PLAN_ABI, + "runtime_layout_abi": RECURRENCE_RUNTIME_LAYOUT_ABI, + } + mismatched = [ + name + for name, expected_value in expected.items() + if result[name] != expected_value + ] + if mismatched: + raise GenerationError( + "Rust recurrence lowering result has incompatible contract fields: " + + ", ".join(mismatched) + ) + capabilities = result["required_runtime_capabilities"] + color_capability = ( + RECURRENCE_CONTRACTED_COLOR_RUNTIME_CAPABILITY + if builder_input.layout == "contracted-color-union" + else RECURRENCE_COLOR_RUNTIME_CAPABILITY + ) + expected_capabilities = tuple( + sorted( + { + RECURRENCE_DIRECT_ARENA_RUNTIME_CAPABILITY, + color_capability, + } + ) + ) + if ( + isinstance(capabilities, str | bytes) + or not isinstance(capabilities, Sequence) + or tuple(capabilities) != expected_capabilities + ): + raise GenerationError( + "Rust recurrence lowering result has incompatible runtime capabilities" + ) + container = _strict_mapping( + result["runtime_container"], + "Rust recurrence runtime container metadata", + { + "kind", + "schema_version", + "storage_abi", + "member_count", + "unpacked_size_bytes", + "index_sha256", + }, + ) + if container["kind"] != RECURRENCE_RUNTIME_CONTAINER_KIND: + raise GenerationError("Rust recurrence runtime container kind is incompatible") + if container["schema_version"] != RECURRENCE_RUNTIME_CONTAINER_SCHEMA_VERSION: + raise GenerationError( + "Rust recurrence runtime container schema is incompatible" + ) + if container["storage_abi"] != RECURRENCE_RUNTIME_STORAGE_ABI: + raise GenerationError( + "Rust recurrence runtime container storage ABI is incompatible" + ) + member_count = _result_integer( + container["member_count"], + "Rust recurrence runtime member count", + minimum=1, + ) + unpacked_size = _result_integer( + container["unpacked_size_bytes"], + "Rust recurrence runtime unpacked size", + minimum=0, + ) + index_sha256 = _result_sha256( + container["index_sha256"], + "Rust recurrence runtime index SHA-256", + ) + inspection = _canonical_json_mapping( + result["inspection_summary"], + "Rust recurrence inspection summary", + ) + exact_sections = _canonical_json_mapping( + result["exact_sections"], + "Rust recurrence exact sections", + ) + generation_profile = _validate_recurrence_generation_profile( + result["generation_profile"], + inspection=inspection, + unpacked_size_bytes=unpacked_size, + ) + raw_dispatch = result["helicity_dispatch"] + dispatch: dict[str, object] | None + if raw_dispatch is None: + if expect_helicity_dispatch: + raise GenerationError( + "Rust recurrence lowering omitted the requested helicity dispatch" + ) + dispatch = None + else: + if not expect_helicity_dispatch: + raise GenerationError( + "Rust recurrence lowering emitted an unrequested helicity dispatch" + ) + dispatch = _strict_mapping( + raw_dispatch, + "Rust recurrence helicity-dispatch metadata", + { + "abi", + "size_bytes", + "sha256", + "base_runtime_layout_digest", + "resolved_helicity_count", + }, + ) + if dispatch["abi"] != _RECURRENCE_HELICITY_DISPATCH_ABI: + raise GenerationError( + "Rust recurrence helicity dispatch has an incompatible ABI" + ) + dispatch["size_bytes"] = _result_integer( + dispatch["size_bytes"], + "Rust recurrence helicity-dispatch size", + minimum=1, + ) + dispatch["sha256"] = _result_sha256( + dispatch["sha256"], + "Rust recurrence helicity-dispatch SHA-256", + ) + dispatch["base_runtime_layout_digest"] = _result_sha256( + dispatch["base_runtime_layout_digest"], + "Rust recurrence helicity-dispatch runtime-layout digest", + ) + dispatch["resolved_helicity_count"] = _result_integer( + dispatch["resolved_helicity_count"], + "Rust recurrence helicity-dispatch resolved-helicity count", + minimum=1, + ) + if dispatch["base_runtime_layout_digest"] != inspection.get( + "runtime_layout_digest" + ): + raise GenerationError( + "Rust recurrence helicity dispatch disagrees with its runtime layout" + ) + raw_helicities = result["resolved_helicities"] + if isinstance(raw_helicities, str | bytes) or not isinstance( + raw_helicities, Sequence + ): + raise GenerationError("Rust recurrence resolved helicities must be a list") + resolved_helicities = [] + for raw_row in raw_helicities: + if isinstance(raw_row, str | bytes) or not isinstance(raw_row, Sequence): + raise GenerationError( + "Rust recurrence resolved helicity rows must be lists" + ) + row = tuple( + _result_signed_integer(value, "Rust recurrence helicity") + for value in raw_row + ) + resolved_helicities.append(row) + if len(set(resolved_helicities)) != len(resolved_helicities): + raise GenerationError("Rust recurrence resolved helicities are not unique") + if dispatch is not None and dispatch["resolved_helicity_count"] != len( + resolved_helicities + ): + raise GenerationError( + "Rust recurrence helicity dispatch disagrees with the resolved-helicity " + "domain" + ) + raw_destinations = result["amplitude_destinations"] + if isinstance(raw_destinations, str | bytes) or not isinstance( + raw_destinations, Sequence + ): + raise GenerationError("Rust recurrence amplitude destinations must be a list") + amplitude_destinations = [] + for destination_id, raw_row in enumerate(raw_destinations): + if ( + isinstance(raw_row, str | bytes) + or not isinstance(raw_row, Sequence) + or len(raw_row) != 2 + ): + raise GenerationError( + "Rust recurrence amplitude destination rows must be " + "[sector_id, helicity_id-or-null]" + ) + sector_id = _result_integer( + raw_row[0], + f"Rust recurrence amplitude destination {destination_id} sector ID", + minimum=0, + ) + raw_helicity_id = raw_row[1] + helicity_id = ( + None + if raw_helicity_id is None + else _result_integer( + raw_helicity_id, + (f"Rust recurrence amplitude destination {destination_id} helicity ID"), + minimum=0, + ) + ) + amplitude_destinations.append((sector_id, helicity_id)) + if len(set(amplitude_destinations)) != len(amplitude_destinations): + raise GenerationError( + "Rust recurrence amplitude destinations repeat a sector/helicity pair" + ) + raw_zero_certificate = result["structural_zero_physical_sector_certificate"] + zero_certificate: _RustRecurrenceStructuralZeroSectorCertificate | None + if raw_zero_certificate is None: + if builder_input.layout == "contracted-color-union": + raise GenerationError( + "Rust contracted recurrence lowering omitted its complete " + "structural-zero physical-sector certificate" + ) + zero_certificate = None + else: + if builder_input.layout != "contracted-color-union": + raise GenerationError( + "Rust recurrence lowering emitted a structural-zero physical-sector " + "certificate outside contracted color" + ) + certificate = _strict_mapping( + raw_zero_certificate, + "Rust recurrence structural-zero physical-sector certificate", + { + "proof_kind", + "sector_ids", + "semantic_digest", + "sector_count", + "resolved_helicity_count", + }, + ) + if certificate["proof_kind"] != _RECURRENCE_STRUCTURAL_ZERO_SECTOR_PROOF_KIND: + raise GenerationError( + "Rust recurrence structural-zero physical-sector certificate " + "has an incompatible proof kind" + ) + sector_count = _result_integer( + certificate["sector_count"], + "Rust recurrence structural-zero certificate sector count", + minimum=1, + ) + inspection_sector_count = _result_integer( + inspection.get("sector_count"), + "Rust recurrence inspection sector count", + minimum=1, + ) + if sector_count != inspection_sector_count: + raise GenerationError( + "Rust recurrence structural-zero certificate disagrees with the " + "physical-sector domain" + ) + resolved_count = _result_integer( + certificate["resolved_helicity_count"], + "Rust recurrence structural-zero certificate resolved-helicity count", + minimum=1, + ) + if resolved_count != len(resolved_helicities): + raise GenerationError( + "Rust recurrence structural-zero certificate disagrees with the " + "resolved-helicity domain" + ) + raw_sector_ids = certificate["sector_ids"] + if isinstance(raw_sector_ids, str | bytes) or not isinstance( + raw_sector_ids, Sequence + ): + raise GenerationError( + "Rust recurrence structural-zero physical-sector IDs must be a list" + ) + sector_ids = tuple( + _result_integer( + value, + "Rust recurrence certified structural-zero physical-sector ID", + minimum=0, + ) + for value in raw_sector_ids + ) + if tuple(sorted(set(sector_ids))) != sector_ids or any( + sector_id >= sector_count for sector_id in sector_ids + ): + raise GenerationError( + "Rust recurrence certified structural-zero physical-sector IDs " + "are not canonical" + ) + active_sector_ids = {sector_id for sector_id, _ in amplitude_destinations} + if active_sector_ids.intersection(sector_ids): + raise GenerationError( + "Rust recurrence structural-zero certificate overlaps active " + "amplitude destinations" + ) + zero_certificate = _RustRecurrenceStructuralZeroSectorCertificate( + sector_ids=sector_ids, + semantic_digest=_result_sha256( + certificate["semantic_digest"], + "Rust recurrence structural-zero certificate semantic digest", + ), + sector_count=sector_count, + resolved_helicity_count=resolved_count, + ) + provenance = _strict_mapping( + inspection.get("structural_zero_physical_sector_certificate"), + "Rust recurrence structural-zero physical-sector provenance", + { + "proof_kind", + "semantic_digest", + "sector_count", + "resolved_helicity_count", + "certified_structural_zero_sector_count", + }, + ) + expected_provenance = { + "proof_kind": _RECURRENCE_STRUCTURAL_ZERO_SECTOR_PROOF_KIND, + "semantic_digest": zero_certificate.semantic_digest, + "sector_count": zero_certificate.sector_count, + "resolved_helicity_count": zero_certificate.resolved_helicity_count, + "certified_structural_zero_sector_count": len(zero_certificate.sector_ids), + } + if provenance != expected_provenance: + raise GenerationError( + "Rust recurrence structural-zero physical-sector provenance " + "disagrees with its certificate" + ) + return { + "inspection_summary": inspection, + "resolved_helicities": tuple(resolved_helicities), + "amplitude_destinations": tuple(amplitude_destinations), + "structural_zero_physical_sector_certificate": zero_certificate, + "exact_sections": exact_sections, + "generation_profile": generation_profile, + "member_count": member_count, + "unpacked_size_bytes": unpacked_size, + "index_sha256": index_sha256, + "helicity_dispatch": dispatch, + } + + +def _validate_recurrence_generation_profile( + value: object, + *, + inspection: Mapping[str, object], + unpacked_size_bytes: int, +) -> dict[str, object]: + profile = _strict_mapping( + value, + "Rust recurrence generation profile", + { + "schema_version", + "scope", + "timings_seconds", + "operation_counters", + "serialized_bytes", + }, + ) + if profile["schema_version"] != 1 or profile["scope"] != "generation-only": + raise GenerationError( + "Rust recurrence generation profile has an incompatible contract" + ) + timings = _strict_mapping( + profile["timings_seconds"], + "Rust recurrence generation timings", + set(_RECURRENCE_GENERATION_PROFILE_TIMINGS), + ) + normalized_timings: dict[str, float] = {} + for name, value in timings.items(): + if ( + isinstance(value, bool) + or not isinstance(value, int | float) + or not math.isfinite(float(value)) + or float(value) < 0.0 + ): + raise GenerationError( + f"Rust recurrence generation timing {name!r} is invalid" + ) + normalized_timings[name] = float(value) + + counters = _strict_mapping( + profile["operation_counters"], + "Rust recurrence generation operation counters", + set(_RECURRENCE_GENERATION_PROFILE_COUNTERS), + ) + normalized_counters = { + name: _result_integer( + counter, + f"Rust recurrence generation counter {name!r}", + minimum=0, + ) + for name, counter in counters.items() + } + serialized = _strict_mapping( + profile["serialized_bytes"], + "Rust recurrence generation serialized bytes", + set(_RECURRENCE_GENERATION_PROFILE_SERIALIZED_BYTES), + ) + normalized_serialized = { + name: _result_integer( + count, + f"Rust recurrence generation serialized byte count {name!r}", + minimum=0, + ) + for name, count in serialized.items() + } + member = inspection.get("runtime_container_member") + if not isinstance(member, Mapping): + raise GenerationError( + "Rust recurrence inspection has no runtime-container member metadata" + ) + expected_serialized = { + "plan_payload": _result_integer( + member.get("size_bytes"), + "Rust recurrence runtime plan payload size", + minimum=0, + ), + "container": _result_integer( + member.get("container_size_bytes"), + "Rust recurrence runtime container size", + minimum=0, + ), + "unpacked_container": unpacked_size_bytes, + } + if normalized_serialized != expected_serialized: + raise GenerationError( + "Rust recurrence generation serialized-byte telemetry disagrees " + "with authenticated container metadata" + ) + return { + "schema_version": 1, + "scope": "generation-only", + "timings_seconds": normalized_timings, + "operation_counters": normalized_counters, + "serialized_bytes": normalized_serialized, + } + + +def _validate_rust_eager_lowering_result( + value: object, + lowering_input: EagerLoweringInputV1, +) -> dict[str, object]: + result = _strict_mapping( + value, + "Rust eager lowering result", + { + "kind", + "schema_version", + "lowering_input_abi", + "lowering_input_sha256", + "eager_plan_abi", + "runtime_layout_abi", + "required_runtime_capabilities", + "runtime_container", + "inspection_summary", + }, + ) + expected = { + "kind": _EAGER_LOWERING_RESULT_KIND, + "schema_version": _EAGER_LOWERING_RESULT_SCHEMA_VERSION, + "lowering_input_abi": EAGER_LOWERING_INPUT_ABI, + "lowering_input_sha256": lowering_input.digest, + "eager_plan_abi": EAGER_PLAN_V3_ABI, + "runtime_layout_abi": EAGER_RUNTIME_LAYOUT_ABI, + } + mismatched = [ + name + for name, expected_value in expected.items() + if result[name] != expected_value + ] + if mismatched: + raise GenerationError( + "Rust eager lowering result has incompatible contract fields: " + + ", ".join(mismatched) + ) + capabilities = result["required_runtime_capabilities"] + if ( + isinstance(capabilities, str | bytes) + or not isinstance(capabilities, Sequence) + or tuple(capabilities) + != tuple( + sorted( + ( + EAGER_DIRECT_ARENA_RUNTIME_CAPABILITY, + EAGER_PLAN_V3_RUNTIME_CAPABILITY, + ) + ) + ) + ): + raise GenerationError( + "Rust eager lowering result has an incompatible runtime capability" + ) + container = _strict_mapping( + result["runtime_container"], + "Rust eager runtime container metadata", + { + "kind", + "schema_version", + "storage_abi", + "member_count", + "unpacked_size_bytes", + "index_sha256", + }, + ) + if container["kind"] != EAGER_RUNTIME_CONTAINER_KIND: + raise GenerationError("Rust eager runtime container kind is incompatible") + if container["schema_version"] != EAGER_RUNTIME_CONTAINER_SCHEMA_VERSION: + raise GenerationError("Rust eager runtime container schema is incompatible") + if container["storage_abi"] != EAGER_RUNTIME_STORAGE_ABI: + raise GenerationError( + "Rust eager runtime container storage ABI is incompatible" + ) + member_count = _result_integer( + container["member_count"], + "Rust eager runtime member count", + minimum=1, + ) + unpacked_size = _result_integer( + container["unpacked_size_bytes"], + "Rust eager runtime unpacked size", + minimum=0, + ) + index_sha256 = _result_sha256( + container["index_sha256"], + "Rust eager runtime index SHA-256", + ) + inspection = _canonical_json_mapping( + result["inspection_summary"], + "Rust eager inspection summary", + ) + return { + "inspection_summary": inspection, + "member_count": member_count, + "unpacked_size_bytes": unpacked_size, + "index_sha256": index_sha256, + } + + +def _strict_mapping( + value: object, + context: str, + fields: set[str], +) -> dict[str, object]: + if not isinstance(value, Mapping): + raise GenerationError(f"{context} must be an object") + result = {str(key): item for key, item in value.items()} + if set(result) != fields: + raise GenerationError(f"{context} fields do not match the v1 binding protocol") + return result + + +def _canonical_json_mapping(value: object, context: str) -> dict[str, object]: + if not isinstance(value, Mapping): + raise GenerationError(f"{context} must be an object") + try: + payload = json.dumps( + value, + ensure_ascii=True, + allow_nan=False, + separators=(",", ":"), + sort_keys=True, + ) + result = json.loads(payload) + except (TypeError, ValueError) as exc: + raise GenerationError(f"{context} is not canonical JSON: {exc}") from exc + if not isinstance(result, dict): # pragma: no cover - Mapping encoded above + raise GenerationError(f"{context} must encode as an object") + return cast(dict[str, object], result) + + +def _result_integer(value: object, context: str, *, minimum: int) -> int: + if isinstance(value, bool) or not isinstance(value, int) or value < minimum: + raise GenerationError(f"{context} must be at least {minimum}") + return value + + +def _result_signed_integer(value: object, context: str) -> int: + if isinstance(value, bool) or not isinstance(value, int): + raise GenerationError(f"{context} must be an integer") + return value + + +def _result_sha256(value: object, context: str) -> str: + if ( + not isinstance(value, str) + or len(value) != 64 + or any(character not in "0123456789abcdef" for character in value) + ): + raise GenerationError(f"{context} must be a lowercase hexadecimal digest") + return value + + +def _file_sha256(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as stream: + while chunk := stream.read(1024 * 1024): + digest.update(chunk) + return digest.hexdigest() + + +def _discard_rust_eager_output(path: Path) -> None: + try: + if path.is_symlink() or path.is_file(): + path.unlink() + except OSError: + pass + + +def _eager_lowering_kernel_ids( + lowering_input: EagerLoweringInputV1, +) -> frozenset[int]: + references: set[int] = set() + for table_name, column_name in ( + ("currents", "propagator_kernel_id"), + ("interactions", "kernel_id"), + ("roots", "kernel_id"), + ): + for raw_value in lowering_input.table(table_name).column(column_name): + value = int(raw_value) + if value != MISSING_U32: + references.add(value) + return frozenset(references) + + +def _builtin_sm_model() -> Model: + from ..models import BuiltinSMModel + + return BuiltinSMModel() + + +def _nested_config_value(values: Mapping[str, object], path: str) -> object: + current: object = values + for component in path.split("."): + if not isinstance(current, Mapping) or component not in current: + raise GenerationError( + f"prepared model references unknown configuration path {path!r}" + ) + current = current[component] + return current + + +def _prepared_pack_effective_values( + pack: PreparedKernelPack, +) -> dict[str, object]: + settings = pack.optimization_settings + + def required(name: str) -> object: + if name not in settings: + raise GenerationError( + f"prepared kernel pack omits optimization setting {name!r}" + ) + return settings[name] + + def required_bool(name: str) -> bool: + value = required(name) + if not isinstance(value, bool): + raise GenerationError( + f"prepared kernel pack has a non-boolean optimization setting {name!r}" + ) + return value + + values: dict[str, object] = { + "evaluator.backend": pack.backend, + "evaluator.optimization.horner_iterations": required("iterations"), + "evaluator.optimization.cpe_iterations": required("cpe_iterations"), + "evaluator.optimization.max_horner_variables": required( + "max_horner_scheme_variables" + ), + "evaluator.optimization.max_common_pair_cache_entries": required( + "max_common_pair_cache_entries" + ), + "evaluator.optimization.max_common_pair_distance": required( + "max_common_pair_distance" + ), + "evaluator.optimization.collect_factors": required("collect_factors"), + } + if pack.backend == "jit": + values["evaluator.jit.optimization_level"] = required("jit_optimization_level") + values["evaluator.jit.compress"] = required_bool("jit_compress") + return values + optimization_level = required("compiled_optimization_level") + if isinstance(optimization_level, bool) or not isinstance(optimization_level, int): + raise GenerationError( + "prepared native kernel pack has an invalid optimization level" + ) + values.update( + { + "evaluator.cpp.optimization": f"O{optimization_level}", + "evaluator.cpp.native_arch": required("compiled_native"), + "evaluator.cpp.compiler": required("compiler_path"), + "evaluator.cpp.extra_flags": required("compiler_flags"), + } + ) + return values + + +@dataclass(frozen=True, slots=True) +class _ResolvedModel: + source: ModelSource + model: Model | None + compiled: _CompiledModelPayload | None = None + use_compiled_process_catalog: bool = True + eager_kernel_index: PreparedCatalogEagerKernelIndex | None = None + + +@dataclass(frozen=True, slots=True) +class _ProcessSelection: + max_color_sectors: int | None = None + reference_color_order: tuple[int, ...] | None = None + selected_color_sector_ids: frozenset[int] | None = None + selected_source_helicities: Mapping[int, int] | None = None + + def __post_init__(self) -> None: + if self.reference_color_order is not None: + object.__setattr__( + self, + "reference_color_order", + tuple(int(label) for label in self.reference_color_order), + ) + if self.selected_color_sector_ids is not None: + object.__setattr__( + self, + "selected_color_sector_ids", + frozenset(int(value) for value in self.selected_color_sector_ids), + ) + if self.selected_source_helicities is not None: + object.__setattr__( + self, + "selected_source_helicities", + MappingProxyType( + { + int(label): int(helicity) + for label, helicity in self.selected_source_helicities.items() + } + ), + ) + + +@dataclass(frozen=True, slots=True) +class _ExpandedProcess: + request: ProcessRequest + process_ir: CanonicalProcessIR + aliases: tuple[Mapping[str, object], ...] = () + source_expansion_size: int = 1 + source_request: ProcessRequest | None = None + # Flavour-blind members represented by this process, never generated. + group_members: tuple[GroupMember, ...] = () + + +class _NoModelSupportedAmplitudes(GenerationError): + """A concrete tree-level subprocess has no amplitude in the active model.""" + + +@dataclass(frozen=True, slots=True) +class _UnsupportedProcess: + expanded: _ExpandedProcess + reason: str + + +_SupportedT = TypeVar("_SupportedT", "_DagProcess", _ExpandedProcess) + + +def _source_request(expanded: _ExpandedProcess) -> ProcessRequest: + return expanded.source_request or expanded.request + + +def _partition_model_supported_processes( + results: Sequence[_SupportedT | _UnsupportedProcess], +) -> tuple[tuple[_SupportedT, ...], tuple[_UnsupportedProcess, ...]]: + """Retain supported children without silently dropping a whole request.""" + + unsupported = tuple( + result for result in results if isinstance(result, _UnsupportedProcess) + ) + supported = tuple( + cast(_SupportedT, result) + for result in results + if not isinstance(result, _UnsupportedProcess) + ) + supported_request_names = { + _source_request( + result if isinstance(result, _ExpandedProcess) else result.expanded + ).name + for result in supported + } + unsupported_by_request: dict[str, list[_UnsupportedProcess]] = {} + for result in unsupported: + source = _source_request(result.expanded) + unsupported_by_request.setdefault(source.name, []).append(result) + empty_requests = tuple( + (request_name, entries) + for request_name, entries in unsupported_by_request.items() + if request_name not in supported_request_names + ) + if empty_requests: + details = [] + for _request_name, entries in empty_requests: + source = _source_request(entries[0].expanded) + reasons = "; ".join(entry.reason for entry in entries) + details.append(f"{source.expression!r}: {reasons}") + raise GenerationError( + "process expansion produced no model-supported tree-level amplitudes " + "for requested process" + + ("es" if len(details) != 1 else "") + + ": " + + "; ".join(details) + ) + return supported, unsupported + + +def _warn_unsupported_processes(unsupported: Sequence[_UnsupportedProcess]) -> None: + if not unsupported: + return + rendered = ", ".join( + f"{item.expanded.request.name} ({item.expanded.process_ir.process}" + + ( + f", with {len(item.expanded.group_members)} flavor-blind member(s)" + if item.expanded.group_members + else "" + ) + + ")" + for item in unsupported + ) + _LOGGER.warning( + "Skipped %d concrete subprocess%s with no model-supported tree-level " + "amplitudes: %s", + len(unsupported), + "" if len(unsupported) == 1 else "es", + rendered, + ) + + +@dataclass(frozen=True, slots=True) +class _PreparedProcessConstruction: + process: CanonicalProcessIR + complete_color_plan: GenericColorPlan + restricted_color_plan: GenericColorPlan + materialized_sector_ids: frozenset[int] | None + coupling_order_limits: Mapping[str, int] + topology_replay: LCColorTopologyReplayPlan | ColorTopologyReplayCertificate | None + + +@dataclass(frozen=True, slots=True) +class _DagProcess: + expanded: _ExpandedProcess + dag: GenericDAG + coverage: Mapping[str, object] + + +@dataclass(frozen=True, slots=True) +class _CompiledProcess: + expanded: _ExpandedProcess + dag: GenericDAG + helicity_sum_dag: GenericDAG | None + helicity_selector_union_dag: GenericDAG | None + coverage: Mapping[str, object] + filters: Mapping[str, object] + validation_points: tuple[ValidationPointRecord, ...] + + +@dataclass(frozen=True, slots=True) +class _GeneratedRecurrenceProcess: + expanded: _ExpandedProcess + artifact: RecurrenceProcessArtifact + validation_points: tuple[ValidationPointRecord, ...] + + +@dataclass(frozen=True, slots=True) +class _GeneratedOnTheFlyProcess: + expanded: _ExpandedProcess + artifact: OnTheFlyProcessArtifact + validation_points: tuple[ValidationPointRecord, ...] + validation_selectors: Mapping[str, tuple[str, ...]] + + +@dataclass(frozen=True, slots=True) +class _ProjectedOnTheFlyProcess: + index: int + expanded: _ExpandedProcess + projection: OnTheFlyGenerationProjectionV1 + + +@dataclass(frozen=True, slots=True) +class _OnTheFlyContractedColorPayloadV1: + payload: bytes + summary: Mapping[str, object] + owner_sector_ids: tuple[int, ...] + destination_by_group: tuple[int, ...] + + +@dataclass(frozen=True, slots=True) +class _RecurrenceModelInputs: + bundle: PreparedModelBundle + catalog: RecurrenceTemplateCatalog + template_catalog_json: bytes + template_input: RecurrenceTemplateInputV1 + direct_template_catalog: RecurrenceDirectTemplateCatalogV1 + direct_template_catalog_json: bytes + prepared_kernel_pack_digest: str + + +@dataclass(frozen=True, slots=True) +class _EvaluatorProcess: + compiled: _CompiledProcess + runtime_schema: RuntimeExpressionSchema + stage_input: StageCompilationInput + color_contraction_payload: bytes | None = None + helicity_sum_runtime_schema: RuntimeExpressionSchema | None = None + helicity_sum_stage_input: StageCompilationInput | None = None + helicity_selector_lanes: tuple[_HelicitySelectorLane, ...] = () + color_selector_lanes: tuple[_ColorSelectorLane, ...] = () + + +@dataclass(frozen=True, slots=True) +class _ColorSelectorLane: + materialized_sector_id: int + dag: GenericDAG + runtime_schema: RuntimeExpressionSchema + stage_input: StageCompilationInput + + +@dataclass(frozen=True, slots=True) +class _HelicitySelectorLane: + selector_domain_ids: tuple[int, ...] + active_current_ids: tuple[int, ...] + active_root_ids: tuple[int, ...] + dag: GenericDAG + runtime_schema: RuntimeExpressionSchema + stage_input: StageCompilationInput + schedule_mode: Literal["parent-closure", "nested-runtime"] = "parent-closure" + child_lanes: tuple[_HelicitySelectorLane, ...] = () + + +def _validate_compiled_symmetric_group_diagnostic_process( + process: _CompiledProcess, +) -> None: + """Fail closed outside the deliberately narrow compiled diagnostic lane.""" + + dag = process.dag + if dag.color_plan.color_accuracy != "full": + raise GenerationError( + "compiled symmetric-group FFT diagnostic execution requires FullColour" + ) + if not dag.selected_source_helicities or dag.helicity_coverage != "selected": + raise GenerationError( + "compiled symmetric-group FFT diagnostic execution requires one " + "generation-time selected source-helicity assignment" + ) + selected_source_labels = { + int(label) for label, _helicity in dag.selected_source_helicities + } + external_source_labels = {int(leg.label) for leg in dag.process.legs} + if selected_source_labels != external_source_labels: + raise GenerationError( + "compiled symmetric-group FFT diagnostic execution requires the " + "selected source-label domain to match every resolved external leg" + ) + if ( + process.helicity_sum_dag is not None + or process.helicity_selector_union_dag is not None + or dag.helicity_recurrence is not None + or dag.helicity_materialization is not None + ): + raise GenerationError( + "compiled symmetric-group FFT diagnostic execution does not support " + "helicity selector or materialization lanes" + ) + if dag.lc_topology_replay is not None or dag.color_topology_replay is not None: + raise GenerationError( + "compiled symmetric-group FFT diagnostic execution does not support " + "colour-topology replay" + ) + + +def _compiled_symmetric_group_color_contraction_payload( + dag: GenericDAG, + layout: RuntimeSchemaLayout, +) -> bytes: + contraction = layout.color_contraction + if contraction is None or not contraction.supported: + reason = None if contraction is None else contraction.reason + raise GenerationError( + "compiled symmetric-group FFT colour contraction is unavailable" + + ("" if reason is None else f": {reason}") + ) + block = contraction.symmetric_group_block + if block is None: + raise GenerationError( + "compiled symmetric-group FFT is not applicable to this process: " + "color planning found no certified nontrivial symmetric-group orbit " + "to Fourier transform; compiled direct contraction remains supported " + "with color.contraction='direct'" + ) + if block.component_count != 1: + raise GenerationError( + "compiled symmetric-group FFT diagnostic execution requires exactly " + "one selected-helicity contraction component" + ) + group_count = contraction.group_count + descriptor_by_group = { + descriptor.group_id: descriptor + for descriptor in layout.color_contraction_descriptors + } + if len(descriptor_by_group) != group_count or set(descriptor_by_group) != set( + range(group_count) + ): + raise GenerationError( + "compiled symmetric-group FFT coherent group IDs are not a dense " + "authenticated destination domain" + ) + destination_by_group = ( + tuple(range(group_count)) + if contraction.destination_by_group is None + else contraction.destination_by_group + ) + if len(destination_by_group) != group_count or set(destination_by_group) != set( + range(group_count) + ): + raise GenerationError( + "compiled symmetric-group FFT destination projection is not a " + "permutation of coherent group IDs" + ) + group_sector_ids = tuple( + descriptor_by_group[destination_id].sector_id + for destination_id in destination_by_group + ) + active_sector_ids = frozenset(group_sector_ids) + return encode_recurrence_color_contraction( + contraction, + sector_count=dag.color_plan.sector_count, + component_count=1, + ordered_group_ids=block.component_group_ids, + destination_by_group=destination_by_group, + destination_count=group_count, + group_sector_ids=group_sector_ids, + group_component_ids=(0,) * group_count, + sector_owner_ids=tuple( + sector_id if sector_id in active_sector_ids else MISSING_U32 + for sector_id in range(dag.color_plan.sector_count) + ), + exact_coefficients=recurrence_exact_color_coefficients( + dag.color_plan, + contraction, + group_sector_ids, + ), + ) + + +def _compiled_lc_color_selector_lane_dags( + dag: GenericDAG, +) -> tuple[tuple[int, GenericDAG], ...]: + if dag.color_plan.color_accuracy != "lc" or dag.color_coverage != "complete": + return () + lanes: list[tuple[int, GenericDAG]] = [] + for sector_id in contributing_color_sector_ids(dag): + lane_dag = filter_dag_to_color_sectors(dag, (sector_id,)) + if not lane_dag.amplitude_roots: + raise GenerationError( + "compiled LC colour-selector lane has no amplitude roots for " + f"materialized sector {sector_id}" + ) + lanes.append((sector_id, lane_dag)) + return tuple(lanes) + + +def _compiled_helicity_selector_closure_lanes( + dag: GenericDAG, + schema: RuntimeExpressionSchema, + model: Model, +) -> tuple[_HelicitySelectorLane, ...]: + """Compile no-replay LC evaluator lanes for exact helicity closures.""" + + materialization = dag.helicity_materialization + if ( + materialization is None + or dag.lc_topology_replay is not None + or dag.color_topology_replay is not None + ): + return () + grouped_domains: dict[ + tuple[tuple[int, ...], tuple[int, ...]], + list[int], + ] = {} + for schedule in materialization.selector_schedules: + if schedule.structural_zero: + continue + closure = (schedule.active_current_ids, schedule.active_root_ids) + grouped_domains.setdefault(closure, []).append(schedule.selector_domain_id) + + stripped_dag = replace( + dag, + helicity_recurrence=None, + helicity_materialization=None, + ) + compile_dag = replace( + stripped_dag, + lc_topology_replay=None, + color_topology_replay=None, + ) + lanes: list[_HelicitySelectorLane] = [] + for (active_current_ids, active_root_ids), domain_ids in sorted( + grouped_domains.items(), + key=lambda item: tuple(item[1]), + ): + lane_schema = _helicity_closure_runtime_schema( + schema, + dag, + active_current_ids=active_current_ids, + ) + if lane_schema is None: + # Keep the exact partitioned primary path for an unusual closure + # that does not traverse every compiled recursion stage. + continue + lanes.append( + _HelicitySelectorLane( + selector_domain_ids=tuple(sorted(domain_ids)), + active_current_ids=active_current_ids, + active_root_ids=active_root_ids, + dag=stripped_dag, + runtime_schema=lane_schema, + stage_input=StageCompilationInput( + compile_dag, + model, + lane_schema, + ), + ) + ) + return tuple(lanes) + + +def _helicity_closure_runtime_schema( + schema: RuntimeExpressionSchema, + dag: GenericDAG, + *, + active_current_ids: Sequence[int], +) -> RuntimeExpressionSchema | None: + """Filter compiled stages while retaining the parent's stable storage ABI.""" + + payload = schema.to_mapping() + if payload.pop("helicity_recurrence", None) is None: + raise GenerationError( + "compiled helicity closure lane has no recurrence metadata to strip" + ) + active = {int(current_id) for current_id in active_current_ids} + value_slots = { + int(record["value_slot_id"]): record + for record in cast( + "Sequence[Mapping[str, object]]", + cast("Mapping[str, object]", payload["value_storage"])["value_slots"], + ) + } + momentum_slot_by_mask = { + int(record["momentum_mask"]): int(record["momentum_slot_id"]) + for record in cast( + "Sequence[Mapping[str, object]]", + payload["momentum_slots"], + ) + } + filtered_stages: list[dict[str, object]] = [] + for raw_stage in cast("Sequence[Mapping[str, object]]", payload["stages"]): + stage = dict(raw_stage) + interaction_ids = tuple( + int(interaction_id) + for interaction_id in cast( + "Sequence[object]", + stage.get("interaction_ids", ()), + ) + if dag.interactions[int(interaction_id)].result_id in active + ) + if not interaction_ids: + return None + interactions = tuple(dag.interactions[item] for item in interaction_ids) + input_current_ids = { + current_id + for interaction in interactions + for current_id in (interaction.left_id, interaction.right_id) + } + output_current_ids = {interaction.result_id for interaction in interactions} + input_value_slot_ids = tuple( + sorted( + int(slot_id) + for slot_id in cast( + "Sequence[object]", + stage["input_value_slot_ids"], + ) + if int(value_slots[int(slot_id)]["current_id"]) in input_current_ids + ) + ) + output_value_slot_ids = tuple( + sorted( + int(slot_id) + for slot_id in cast( + "Sequence[object]", + stage["output_value_slot_ids"], + ) + if int(value_slots[int(slot_id)]["current_id"]) in output_current_ids + ) + ) + momentum_slot_ids = { + momentum_slot_by_mask[dag.currents[current_id].index.momentum_mask] + for interaction in interactions + for current_id in ( + interaction.left_id, + interaction.right_id, + interaction.result_id, + ) + } + evaluation_groups = { + ( + "group", + int(interaction.evaluation_group_id), + ) + if interaction.evaluation_group_id is not None + else ("interaction", interaction.id) + for interaction in interactions + } + stage.update( + { + "input_current_ids": sorted(input_current_ids), + "output_current_ids": sorted(output_current_ids), + "input_value_slot_ids": list(input_value_slot_ids), + "output_value_slot_ids": list(output_value_slot_ids), + "input_momentum_slot_ids": sorted(momentum_slot_ids), + "interaction_count": len(interaction_ids), + "interaction_evaluation_count": len(evaluation_groups), + "interaction_ids": list(interaction_ids), + "interactions_compacted": True, + "interactions": [], + } + ) + filtered_stages.append(stage) + payload["stages"] = filtered_stages + return RuntimeExpressionSchema.from_mapping(payload) + + +def _complete_helicity_domain_contract( + dag: GenericDAG, +) -> tuple[ + frozenset[tuple[tuple[int, int], ...]], + frozenset[tuple[tuple[int, int], ...]], +]: + plan = dag.helicity_recurrence + if plan is None: + raise GenerationError("helicity selector lane has no recurrence proof") + complete_by_id = { + domain.id: domain.source_states + for domain in plan.selector_domains + if domain.complete + } + complete = frozenset(complete_by_id.values()) + structural_zero = frozenset( + complete_by_id[domain_id] + for domain_id in plan.structural_zero_selector_domain_ids + if domain_id in complete_by_id + ) + return complete, structural_zero + + +def _validate_matching_helicity_selector_domains( + parent: GenericDAG, + nested: GenericDAG, +) -> None: + parent_complete, parent_zero = _complete_helicity_domain_contract(parent) + nested_complete, nested_zero = _complete_helicity_domain_contract(nested) + if parent_complete != nested_complete: + raise GenerationError( + "complete-color helicity selector lane disagrees with the parent " + "source-state domain" + ) + if parent_zero != nested_zero: + raise GenerationError( + "complete-color helicity selector lane disagrees with the parent " + "structural-zero domain" + ) + + +def _numerical_current_warning_required(value: object) -> bool: + if not isinstance(value, Mapping): + return False + warning = value.get("warning") + return isinstance(warning, Mapping) and warning.get("required") is True + + +def _map_process_phase( + items: Sequence[_ProcessInput], + operation: Callable[[_ProcessInput], _ProcessOutput], + *, + executor: ThreadPoolExecutor | None, + max_in_flight: int, + phase_name: str, + item_name: Callable[[_ProcessInput], str], +) -> tuple[_ProcessOutput, ...]: + """Apply one process phase with bounded submissions and stable output order.""" + + if max_in_flight < 1: + raise ValueError("process phase max_in_flight must be positive") + if executor is None: + results: list[_ProcessOutput] = [] + for item in items: + try: + results.append(operation(item)) + except Exception as exc: + raise GenerationError( + f"{phase_name} failed for process {item_name(item)!r}: {exc}" + ) from exc + return tuple(results) + + ordered: list[_ProcessOutput | object] = [_MISSING_PROCESS_RESULT for _ in items] + active: dict[Future[_ProcessOutput], int] = {} + next_index = 0 + + def submit_one(index: int) -> None: + item = items[index] + try: + future = executor.submit(operation, item) + except Exception as exc: + for pending in active: + pending.cancel() + if active: + wait(tuple(active)) + raise GenerationError( + f"{phase_name} failed to schedule process {item_name(item)!r}: {exc}" + ) from exc + active[future] = index + + while next_index < len(items) and len(active) < max_in_flight: + submit_one(next_index) + next_index += 1 + + while active: + done, _pending = wait(tuple(active), return_when=FIRST_COMPLETED) + failures: list[tuple[int, Exception]] = [] + for future in done: + index = active.pop(future) + try: + ordered[index] = future.result() + except Exception as exc: + failures.append((index, exc)) + if failures: + for pending in active: + pending.cancel() + if active: + wait(tuple(active)) + index, exc = min(failures, key=lambda failure: failure[0]) + item = items[index] + raise GenerationError( + f"{phase_name} failed for process {item_name(item)!r}: {exc}" + ) from exc + while next_index < len(items) and len(active) < max_in_flight: + submit_one(next_index) + next_index += 1 + + if any(result is _MISSING_PROCESS_RESULT for result in ordered): + raise GenerationError(f"{phase_name} did not produce every process result") + return tuple(cast(_ProcessOutput, result) for result in ordered) + + +def _initial_process_groups( + artifact_processes: Sequence[ProcessArtifact], + expanded: Iterable[_ExpandedProcess], +) -> ProcessGroupMap: + """Each written process with the members recorded at expansion.""" + + members = {entry.request.name: entry.group_members for entry in expanded} + return initial_process_groups( + ( + (process.process_id, process.expression, process.external_pdgs) + for process in artifact_processes + ), + members, + ) + + +class GenerationBackend: + def __init__( + self, + config: GenerationConfig | RunConfig | ConfigResolution | None, + progress: ProgressSink | None, + *, + api_bundle_hook: ApiBundleHook | None = None, + process_selection: _ProcessSelection | None = None, + ) -> None: + self._resource_config = config + self._configuration = _GenerationConfigProvenance.from_config(config) + self._config = self._configuration.effective + self._progress = progress + self._api_bundle_hook = api_bundle_hook + self._process_selection_override = process_selection + self._prepared_pack_warning_emitted = False + + def plan( + self, + processes: ProcessSet, + *, + model: ModelSource | CompiledModel | None = None, + ) -> GenerationPlan: + source = model or self._configured_model_source() + task_id = "generation-plan" + if self._progress is not None: + self._progress.emit( + ProgressStart( + task_id, + "Planning processes", + total=len(processes.requests), + ) + ) + try: + self._validate_recurrence_request() + self._validate_on_the_fly_process_selection() + license_state = self._detect_symbolica_license() + self._apply_symbolica_resource_policy(license_state) + resolved_model = self._resolve_model_for_plan(source) + self._require_eager_kernel_pack(resolved_model) + self._apply_prepared_kernel_pack_policy(resolved_model) + if resolved_model.model is None and ( + self._on_the_fly_execution_enabled + or ( + self._run_config is not None + and self._run_config.color.fft_basis.value == "adjoint" + ) + ): + compiled = resolved_model.compiled + if compiled is None: # pragma: no cover - model preflight invariant + raise GenerationError( + "on-the-fly or adjoint FFT planning requires a resolved model" + ) + from ..models.external import CompiledUFOModel + + resolved_model = replace( + resolved_model, + model=CompiledUFOModel(compiled), + ) + expanded = self._expand_process_set( + processes, + resolved_model, + PhaseHandle(task_id, self._progress, len(processes.requests)), + ) + self._apply_symbolica_resource_policy( + license_state, + process_count=len(expanded), + ) + coverage: list[dict[str, object]] = [] + if self._on_the_fly_execution_enabled: + generation_model = resolved_model.model + if generation_model is None: # pragma: no cover - guarded above + raise GenerationError( + "on-the-fly planning requires a resolved model" + ) + model_inputs = self._recurrence_model_inputs(resolved_model) + for index, entry in enumerate(expanded): + coverage.append( + self._plan_on_the_fly_process( + self._project_on_the_fly_process( + entry, + generation_model, + model_inputs, + index=index, + ) + ) + ) + else: + for entry in expanded: + coverage.append( + self._plan_concrete_process( + entry.process_ir, + model=resolved_model.model, + ) + ) + result = GenerationPlan( + concrete_processes=tuple(entry.request for entry in expanded), + estimated_coverage={ + "model_kind": resolved_model.source.kind, + "color_accuracy": self._color_accuracy, + "process_count": len(expanded), + "alias_count": sum(len(entry.aliases) for entry in expanded), + "processes": tuple(coverage), + }, + requested_settings=self._configuration.requested, + effective_settings=self._config, + adjustments=self._configuration.adjustments, + unsupported_features=(), + ) + except Exception as exc: + if self._progress is not None: + self._progress.emit( + ProgressEnd(task_id, success=False, message=str(exc)) + ) + if isinstance(exc, (GenerationError, ModelError)): + raise + raise GenerationError(str(exc)) from exc + if self._progress is not None: + self._progress.emit(ProgressEnd(task_id)) + return result + + def generate( + self, + processes: ProcessSet, + output: os.PathLike[str] | str, + *, + model: ModelSource | CompiledModel | None = None, + mode: str = "error", + ) -> GenerationResult: + if mode not in ("error", "append", "replace"): + raise ValueError("generation mode must be 'error', 'append', or 'replace'") + write_mode = cast(Literal["error", "append", "replace"], mode) + reporter = GenerationPhaseReporter(self._progress) + run_config = self._run_config + execution_mode = ( + "compiled" + if run_config is None + else str(run_config.evaluator.execution_mode) + ) + numerical_current_warning_required = False + backend = "jit" if run_config is None else str(run_config.evaluator.backend) + if run_config is None: + optimization = "O2" + elif backend == "jit": + optimization = f"O{run_config.evaluator.jit.optimization_level}" + else: + optimization = str(run_config.evaluator.cpp.optimization) + reporter.start_generation( + total=( + 5 + if execution_mode in {"recurrence", "on-the-fly"} + else 7 + if execution_mode == "eager" + else 8 + ) + + int(self._numerical_process_grouping), + details={ + "execution_mode": execution_mode, + "backend": backend.upper(), + "optimization": optimization.upper(), + }, + ) + generation_started = time.perf_counter() + try: + self._validate_recurrence_request() + self._validate_on_the_fly_process_selection() + license_state = self._detect_symbolica_license() + self._apply_symbolica_resource_policy(license_state) + source = model or self._configured_model_source() + with reporter.phase( + "model-loading", + "Loading and compiling model", + total=1, + ) as phase: + resolved_model = self._resolve_model(source) + artifact_model = self._artifact_model(resolved_model) + self._require_eager_kernel_pack(resolved_model) + resolved_model = self._index_eager_kernel_pack(resolved_model) + self._apply_prepared_kernel_pack_policy(resolved_model) + phase.update(1, message=artifact_model.name) + generation_model = resolved_model.model + if generation_model is None: + raise GenerationError("generation model resolution produced no model") + + with reporter.phase( + "process-expansion", + "Expanding concrete processes", + total=len(processes.requests), + ) as phase: + expanded = self._expand_process_set(processes, resolved_model, phase) + self._apply_symbolica_resource_policy( + license_state, + process_count=len(expanded), + ) + + if self._on_the_fly_execution_enabled: + return self._generate_on_the_fly_processes( + expanded, + output=Path(output), + write_mode=write_mode, + requested_processes=processes, + resolved_model=resolved_model, + artifact_model=artifact_model, + generation_model=generation_model, + reporter=reporter, + generation_started=generation_started, + ) + + if self._recurrence_execution_enabled: + return self._generate_recurrence_processes( + expanded, + output=Path(output), + write_mode=write_mode, + requested_processes=processes, + resolved_model=resolved_model, + artifact_model=artifact_model, + generation_model=generation_model, + reporter=reporter, + generation_started=generation_started, + ) + + with TemporaryDirectory(prefix="pyamplicol-generation-") as temporary: + temporary_root = Path(temporary) + worker_count = self._process_worker_count(len(expanded)) + executor = ( + ThreadPoolExecutor( + max_workers=worker_count, + thread_name_prefix="pyamplicol-generation", + ) + if worker_count > 1 + else None + ) + try: + with reporter.phase( + "dag", + "Compiling process DAGs", + total=len(expanded), + ) as phase: + dag_results = _map_process_phase( + expanded, + lambda entry: self._compile_for_generation( + entry, + generation_model, + phase, + ), + executor=executor, + max_in_flight=worker_count, + phase_name="DAG compilation", + item_name=lambda entry: entry.request.name, + ) + compiled, unsupported = _partition_model_supported_processes( + dag_results + ) + _warn_unsupported_processes(unsupported) + del dag_results + del unsupported + + indexed_compiled = tuple(enumerate(compiled)) + with reporter.phase( + "warmup-filter", + ( + "Applying structural reductions and preparing " + "validation points" + ), + total=len(compiled), + ) as phase: + prepared = _map_process_phase( + indexed_compiled, + lambda indexed: self._prepare_warmup_process( + indexed[1], + generation_model, + index=indexed[0], + phase=phase, + ), + executor=executor, + max_in_flight=worker_count, + phase_name="warmup and validation-point preparation", + item_name=lambda indexed: indexed[1].expanded.request.name, + ) + numerical_current_warning_required = any( + _numerical_current_warning_required( + entry.filters.get("relation_discovery") + ) + for entry in prepared + ) + + if self._eager_execution_enabled: + with reporter.phase( + "eager-lowering", + "Lowering prepared-kernel DAG invocation tables", + total=len(prepared), + ) as phase: + artifact_processes = _map_process_phase( + prepared, + lambda entry: self._construct_eager_artifact( + entry, + generation_model, + resolved_model, + temporary_root, + phase, + ), + executor=executor, + max_in_flight=worker_count, + phase_name="eager DAG-table lowering", + item_name=lambda entry: entry.expanded.request.name, + ) + else: + with reporter.phase( + "evaluator-construction", + "Constructing runtime evaluator schemas", + total=len(prepared), + ) as phase: + evaluators = _map_process_phase( + prepared, + lambda entry: self._construct_evaluator( + entry, + generation_model, + phase, + ), + executor=executor, + max_in_flight=worker_count, + phase_name="runtime evaluator-schema construction", + item_name=lambda entry: entry.expanded.request.name, + ) + + jit_total = sum( + _runtime_stage_count(evaluator.runtime_schema) + + 1 + + ( + 0 + if evaluator.helicity_sum_runtime_schema is None + else _runtime_stage_count( + evaluator.helicity_sum_runtime_schema + ) + + 1 + ) + + sum( + _helicity_selector_lane_compile_task_count(lane) + for lane in evaluator.helicity_selector_lanes + ) + + sum( + _runtime_stage_count(lane.runtime_schema) + 1 + for lane in evaluator.color_selector_lanes + ) + for evaluator in evaluators + ) + with reporter.phase( + "jit", + "Compiling and materializing stage evaluators", + total=jit_total, + ) as phase: + # Symbolica expressions are created while resolving the + # model on this caller thread. Keep materialization on that + # same thread. The process-wide lock already made this phase + # serial; moving between worker threads can violate backend + # thread affinity. + artifact_processes = _map_process_phase( + evaluators, + lambda evaluator: self._materialize_evaluator( + evaluator, + generation_model, + temporary_root, + phase, + ), + executor=None, + max_in_flight=1, + phase_name="evaluator materialization", + item_name=lambda evaluator: ( + evaluator.compiled.expanded.request.name + ), + ) + finally: + if executor is not None: + executor.shutdown(wait=True, cancel_futures=True) + + with reporter.phase( + "artifact-writing", + "Writing schema-v3 artifact", + total=1, + ) as phase: + + def artifact_progress(event: dict[str, object]) -> None: + details = { + str(key): value + for key, value in event.items() + if isinstance(value, (str, int, float, bool, type(None))) + } + phase.update( + int(event.get("completed", phase.completed)), + total=( + None + if event.get("total") is None + else int(event["total"]) + ), + message=str(event.get("step", "writing artifact")), + details=details, + ) + + write_result = self._write_artifact( + Path(output), + mode=write_mode, + source=resolved_model.source, + compiled_model=artifact_model, + configuration=self._configuration, + processes=artifact_processes, + timings=reporter.timings, + api_bundle_hook=self._api_bundle_hook, + progress_callback=( + artifact_progress if phase.sink is not None else None + ), + process_groups=_initial_process_groups( + artifact_processes, + (entry.expanded for entry in prepared), + ), + ) + phase.update( + phase.total or phase.completed, + message=str(write_result.output), + details={ + "step": "artifact ready", + "file_count": len(write_result.files), + }, + ) + if numerical_current_warning_required: + _LOGGER.warning( + "%s [%s]", + NUMERICAL_CURRENT_RELATION_WARNING, + NUMERICAL_CURRENT_RELATION_WARNING_CODE, + ) + + concrete_requests = tuple(entry.expanded.request for entry in prepared) + validation_points_by_process = { + process.expanded.request.name: process.validation_points + for process in prepared + } + expected_process_ids = tuple( + process.process_id for process in artifact_processes + ) + del compiled + del indexed_compiled + del prepared + del artifact_processes + gc.collect() + # After releasing the generation state: analysis loads runtimes. + self._analyze_process_groups(write_result.output, reporter) + + with reporter.phase( + "validation", + "Validating generated artifact", + total=1, + ) as phase: + validation = self._generation_config.validation + if validation.post_build_validation: + self._validate_generated_artifact( + write_result.output, + expected_process_ids, + validation_points=validation_points_by_process, + expected_api_bundle_path=write_result.api_bundle_path, + progress=phase, + ) + message = ( + f"schema and {validation.samples} numerical samples" + if validation.enabled + else "schema, references, hashes, and target" + ) + else: + message = "post-build validation disabled" + phase.update(1, message=message) + + reporter.timings["total"] = time.perf_counter() - generation_started + except KeyboardInterrupt: + reporter.finish_generation(success=False, message="interrupted") + raise + except Exception as exc: + reporter.finish_generation(success=False, message=str(exc)) + if isinstance(exc, PyAmpliColError): + raise + raise GenerationError(str(exc)) from exc + + reporter.finish_generation() + return GenerationResult( + output=write_result.output, + processes=ProcessSet( + requests=concrete_requests, + aliases=processes.aliases, + ), + mode=write_mode, + files=write_result.files, + ) + + def _generate_on_the_fly_processes( + self, + expanded: tuple[_ExpandedProcess, ...], + *, + output: Path, + write_mode: Literal["error", "append", "replace"], + requested_processes: ProcessSet, + resolved_model: _ResolvedModel, + artifact_model: _CompiledModelPayload, + generation_model: Model, + reporter: GenerationPhaseReporter, + generation_started: float, + ) -> GenerationResult: + """Build source-only LC seeds before any color plan or process DAG.""" + + inputs_started = time.perf_counter() + model_inputs = self._recurrence_model_inputs(resolved_model) + reporter.timings["on-the-fly-model-input-preparation"] = ( + time.perf_counter() - inputs_started + ) + with TemporaryDirectory(prefix="pyamplicol-on-the-fly-generation-") as root: + temporary_root = Path(root) + indexed = tuple(enumerate(expanded)) + with reporter.phase( + "on-the-fly-construction", + "Constructing compact on-the-fly process seeds", + total=len(indexed), + ) as phase: + projected = _map_process_phase( + indexed, + lambda item: self._project_on_the_fly_process( + item[1], + generation_model, + model_inputs, + index=item[0], + ), + executor=None, + max_in_flight=1, + phase_name="on-the-fly source projection", + item_name=lambda item: item[1].request.name, + ) + seed_payloads = _invoke_rust_on_the_fly_seed_batch_builder_v1( + tuple(item.projection.seed.to_json_bytes() for item in projected), + model_inputs.template_catalog_json, + model_inputs.direct_template_catalog_json, + model_inputs.prepared_kernel_pack_digest, + process_identities=tuple( + item.expanded.request.name for item in projected + ), + ) + packaged = tuple(zip(projected, seed_payloads, strict=True)) + generated = _map_process_phase( + packaged, + lambda item: self._construct_on_the_fly_artifact( + item[0], + item[1], + generation_model, + model_inputs, + temporary_root, + phase=phase, + ), + executor=None, + max_in_flight=1, + phase_name="on-the-fly artifact packaging", + item_name=lambda item: item[0].expanded.request.name, + ) + + artifact_processes = tuple(item.artifact for item in generated) + with reporter.phase( + "artifact-writing", + "Writing schema-v3 artifact", + total=1, + ) as phase: + + def artifact_progress(event: dict[str, object]) -> None: + details = { + str(key): value + for key, value in event.items() + if isinstance(value, (str, int, float, bool, type(None))) + } + phase.update( + int(event.get("completed", phase.completed)), + total=( + None if event.get("total") is None else int(event["total"]) + ), + message=str(event.get("step", "writing artifact")), + details=details, + ) + + write_result = write_schema_v3_artifact( + output, + mode=write_mode, + source=resolved_model.source, + compiled_model=artifact_model, + configuration=self._configuration, + processes=artifact_processes, + timings=reporter.timings, + api_bundle_hook=self._api_bundle_hook, + progress_callback=( + artifact_progress if phase.sink is not None else None + ), + process_groups=_initial_process_groups( + artifact_processes, + (item.expanded for item in generated), + ), + ) + phase.update( + phase.total or phase.completed, + message=str(write_result.output), + details={ + "step": "artifact ready", + "file_count": len(write_result.files), + }, + ) + validation_points_by_process = { + item.expanded.request.name: item.validation_points for item in generated + } + validation_selectors_by_process = { + item.expanded.request.name: item.validation_selectors + for item in generated + } + expected_process_ids = tuple( + process.process_id for process in artifact_processes + ) + concrete_requests = tuple(item.expanded.request for item in generated) + del artifact_processes + del generated + gc.collect() + self._analyze_process_groups(write_result.output, reporter) + + with reporter.phase( + "validation", + "Validating generated artifact", + total=1, + ) as phase: + validation = self._generation_config.validation + if validation.post_build_validation: + numerical_sample_count = sum( + 1 + for points in validation_points_by_process.values() + if any(point.available for point in points) + ) + self._validate_generated_artifact( + write_result.output, + expected_process_ids, + validation_points=validation_points_by_process, + compact_validation_selectors=(validation_selectors_by_process), + expected_api_bundle_path=write_result.api_bundle_path, + progress=phase, + ) + if not validation.enabled: + message = "compact schema, seed, selectors, and prepared model" + elif numerical_sample_count == 0: + message = "compact schema; zero numerical samples available" + else: + suffix = "" if numerical_sample_count == 1 else "s" + message = ( + f"compact schema and {numerical_sample_count} " + f"selector-bounded numerical sample{suffix}" + ) + else: + message = "post-build validation disabled" + phase.update(1, message=message) + + reporter.timings["total"] = time.perf_counter() - generation_started + reporter.finish_generation() + return GenerationResult( + output=write_result.output, + processes=ProcessSet( + requests=concrete_requests, + aliases=requested_processes.aliases, + ), + mode=write_mode, + files=write_result.files, + ) + + def _project_on_the_fly_process( + self, + expanded: _ExpandedProcess, + model: Model, + model_inputs: _RecurrenceModelInputs, + *, + index: int, + ) -> _ProjectedOnTheFlyProcess: + run = self._run_config + if run is None: # pragma: no cover - guarded by execution mode + raise GenerationError("on-the-fly generation requires RunConfig") + self._validate_on_the_fly_process_selection() + coupling_policy = str(run.process.coupling_order_policy) + explicit_limits = self._coupling_order_limits + projection = project_on_the_fly_process_seed_v1( + expanded.process_ir, + model_inputs.catalog, + model, + coupling_order_policy=cast( + Literal["minimal", "explicit"], + coupling_policy, + ), + coupling_order_limits=explicit_limits, + reference_color_order=self._process_selection.reference_color_order, + color_basis=self._fft_color_basis(expanded.process_ir, model), + ) + return _ProjectedOnTheFlyProcess( + index=index, + expanded=expanded, + projection=projection, + ) + + def _validate_on_the_fly_process_selection(self) -> None: + if not self._on_the_fly_execution_enabled: + return + selection = self._process_selection + incompatible = [] + if selection.max_color_sectors is not None: + incompatible.append("process.max_color_sectors") + if selection.selected_color_sector_ids is not None: + incompatible.append("process.selected_color_sector_ids") + if selection.selected_source_helicities is not None: + incompatible.append("process.selected_source_helicities") + if incompatible: + raise GenerationError( + "on-the-fly artifacts retain complete runtime selector coverage; " + "remove " + ", ".join(incompatible) + ) + + def _recurrence_helicity_selector_plan_enabled(self) -> bool: + """Return whether generation can retain the complete selector plan.""" + + selection = self._process_selection + return self._color_accuracy in {"nlc", "full"} and all( + value is None + for value in ( + selection.max_color_sectors, + selection.selected_color_sector_ids, + selection.selected_source_helicities, + ) + ) + + def _construct_on_the_fly_artifact( + self, + projected: _ProjectedOnTheFlyProcess, + seed_payload: bytes, + model: Model, + model_inputs: _RecurrenceModelInputs, + temporary_root: Path, + *, + phase: PhaseHandle, + ) -> _GeneratedOnTheFlyProcess: + expanded = projected.expanded + projection = projected.projection + index = projected.index + process_id = expanded.request.name + run = self._run_config + if run is None: # pragma: no cover - guarded by execution mode + raise GenerationError("on-the-fly generation requires RunConfig") + query_construction_threads = run.evaluator.optimization.cores + if ( + isinstance(query_construction_threads, bool) + or not isinstance(query_construction_threads, int) + or query_construction_threads < 1 + ): + raise GenerationError( + "on-the-fly generation requires resolved positive " + "evaluator.optimization.cores" + ) + selection = self._process_selection + coupling_policy = str(run.process.coupling_order_policy) + explicit_limits = self._coupling_order_limits + process_seed_identity = _invoke_rust_on_the_fly_seed_inspector_v1(seed_payload) + _bind_on_the_fly_process_seed_identity_v1( + process_seed_identity, + projection.seed, + ) + color_accuracy = str(expanded.process_ir.color_accuracy) + contracted_color = None + if color_accuracy in {"nlc", "full"}: + if projection.color_plan is None: + raise GenerationError( + "on-the-fly contracted-color projection has no color plan" + ) + try: + contracted_color = _build_on_the_fly_contracted_color_payload_v1( + projection.color_plan, + contraction=run.color.contraction.value, + ) + except ValueError as exc: + raise GenerationError( + f"process {process_id!r} on-the-fly color contraction failed: {exc}" + ) from exc + elif projection.color_plan is not None: + raise GenerationError("LC on-the-fly projection carries a color plan") + validation_selectors = _on_the_fly_validation_selectors_v1( + projection.seed, + color_accuracy=color_accuracy, + ) + selector_census = _on_the_fly_selector_census_v1( + projection.seed, + expanded.process_ir, + ) + runtime_path = ( + temporary_root + / "on-the-fly-runtimes" + / process_id + / "on-the-fly-runtime.pacbin" + ) + runtime_path.parent.mkdir(parents=True, exist_ok=True) + runtime_index = write_pacbin_atomic( + runtime_path, + ( + PacbinMemberSource( + "on-the-fly/process-seed-v1.bin", + PacbinMemberKind.ON_THE_FLY_PROCESS_SEED, + io.BytesIO(seed_payload), + ), + ), + ) + runtime_metadata = { + **build_on_the_fly_runtime_metadata( + projection.seed.external_sources, + projection.seed.parameter_projection, + model_inputs.catalog, + model, + projection.runtime_normalization, + ), + "process_seed_identity": process_seed_identity, + } + public_metadata = build_on_the_fly_public_metadata( + expanded.process_ir, + model_inputs.catalog, + process_id=process_id, + ) + umami_physics = None + umami_sectors = () + if run.generation.emit_api_bundle: + from ..artifacts.umami_otf import build_umami_on_the_fly_physics + + umami_physics, umami_sectors = build_umami_on_the_fly_physics( + public_metadata, expanded.process_ir, projection.seed + ) + validation = self._generation_config.validation + sample_count = ( + validation.samples + if validation.enabled and validation.post_build_validation + else 1 + ) + points = tuple( + build_process_validation_point( + expanded.process_ir, + model, + process_id=process_id, + seed=validation.seed + index * sample_count + sample_index, + ) + for sample_index in range(sample_count) + ) + phase.advance( + message=process_id, + details={ + "process": process_id, + "step": "compact on-the-fly seed ready", + "seed_size_bytes": len(seed_payload), + }, + ) + artifact = OnTheFlyProcessArtifact( + process_id=process_id, + expression=expanded.process_ir.process, + color_accuracy=str(expanded.process_ir.color_accuracy), + external_pdgs=tuple(int(leg.pdg) for leg in expanded.process_ir.legs), + aliases=expanded.aliases, + physics=public_metadata, + umami_physics=umami_physics, + umami_color_sectors=umami_sectors, + runtime_path=runtime_path, + runtime_size_bytes=runtime_index.file_size, + runtime_sha256=_file_sha256(runtime_path), + runtime_member_count=len(runtime_index.members), + runtime_unpacked_size_bytes=sum( + member.length for member in runtime_index.members + ), + runtime_index_sha256=runtime_index.index_sha256, + referenced_kernel_ids=frozenset( + kernel.kernel_id for kernel in model_inputs.bundle.kernel_pack.kernels + ), + runtime_metadata=runtime_metadata, + selector_policy={ + **( + {"color_basis": projection.color_plan.basis} + if projection.color_plan is not None + and projection.color_plan.basis != "trace" + else {} + ), + "color_coverage": ( + "complete" if contracted_color is None else "contracted" + ), + "reference_color_word": ( + None + if selection.reference_color_order is None + else list( + projection.color_plan.sectors[0].trace_labels + if projection.color_plan is not None + and projection.color_plan.basis == "adjoint" + else selection.reference_color_order + ) + ), + "trace_reflections_folded": bool( + contracted_color is None + and model.lc_trace_reflection_equivalence_is_proven( + expanded.process_ir + ) + ), + "selector_census": selector_census, + }, + color_contraction_payload=( + None if contracted_color is None else contracted_color.payload + ), + color_contraction_summary=( + None if contracted_color is None else contracted_color.summary + ), + point_tile_size=run.evaluator.recurrence.point_tile_size, + query_construction_threads=query_construction_threads, + validation_point=points[0], + generation_filters={ + **self._fft_basis_metadata(projection.color_plan), + "on_the_fly": { + "coupling_order_policy": coupling_policy, + "coupling_order_limits": dict(explicit_limits), + "selector_coverage": ( + "complete" if contracted_color is None else "contracted" + ), + }, + }, + ) + return _GeneratedOnTheFlyProcess( + expanded=expanded, + artifact=artifact, + validation_points=points, + validation_selectors=validation_selectors, + ) + + def _generate_recurrence_processes( + self, + expanded: tuple[_ExpandedProcess, ...], + *, + output: Path, + write_mode: Literal["error", "append", "replace"], + requested_processes: ProcessSet, + resolved_model: _ResolvedModel, + artifact_model: _CompiledModelPayload, + generation_model: Model, + reporter: GenerationPhaseReporter, + generation_started: float, + ) -> GenerationResult: + """Build and publish compact recurrence schedules before GenericDAG.""" + + expanded = self._drop_processes_without_amplitudes(expanded, generation_model) + model_inputs_started = time.perf_counter() + model_inputs = self._recurrence_model_inputs(resolved_model) + reporter.timings["recurrence-model-input-preparation"] = ( + time.perf_counter() - model_inputs_started + ) + with TemporaryDirectory( + prefix="pyamplicol-recurrence-generation-" + ) as temporary: + temporary_root = Path(temporary) + worker_count = self._process_worker_count(len(expanded)) + executor = ( + ThreadPoolExecutor( + max_workers=worker_count, + thread_name_prefix="pyamplicol-recurrence-generation", + ) + if worker_count > 1 + else None + ) + try: + indexed = tuple(enumerate(expanded)) + lowering_cache = RecurrenceScheduleLoweringCache[ + _RustRecurrenceLoweringOutput + ]() + with reporter.phase( + "recurrence-construction", + "Constructing compact process recurrences", + total=len(expanded), + ) as phase: + generated = _map_process_phase( + indexed, + lambda item: self._construct_recurrence_artifact( + item[1], + generation_model, + model_inputs, + temporary_root, + index=item[0], + phase=phase, + lowering_cache=lowering_cache, + generation_timings=reporter.timings, + ), + executor=executor, + max_in_flight=worker_count, + phase_name="compact recurrence construction", + item_name=lambda item: item[1].request.name, + ) + finally: + if executor is not None: + executor.shutdown(wait=True, cancel_futures=True) + + artifact_processes = tuple(item.artifact for item in generated) + recurrence_warnings: list[tuple[str, str]] = [] + for process in artifact_processes: + relation_report = process.generation_filters.get("relation_discovery") + if not isinstance(relation_report, Mapping): + continue + warning = relation_report.get("warning") + if ( + not isinstance(warning, Mapping) + or warning.get("required") is not True + ): + continue + code = warning.get("code") + message = warning.get("message") + if not isinstance(code, str) or not isinstance(message, str): + raise GenerationError( + "recurrence numerical warning metadata is malformed" + ) + item = (code, message) + if item not in recurrence_warnings: + recurrence_warnings.append(item) + with reporter.phase( + "artifact-writing", + "Writing schema-v3 artifact", + total=1, + ) as phase: + + def artifact_progress(event: dict[str, object]) -> None: + details = { + str(key): value + for key, value in event.items() + if isinstance(value, (str, int, float, bool, type(None))) + } + phase.update( + int(event.get("completed", phase.completed)), + total=( + None if event.get("total") is None else int(event["total"]) + ), + message=str(event.get("step", "writing artifact")), + details=details, + ) + + write_result = write_schema_v3_artifact( + output, + mode=write_mode, + source=resolved_model.source, + compiled_model=artifact_model, + configuration=self._configuration, + processes=artifact_processes, + timings=reporter.timings, + api_bundle_hook=self._api_bundle_hook, + progress_callback=( + artifact_progress if phase.sink is not None else None + ), + process_groups=_initial_process_groups( + artifact_processes, + (item.expanded for item in generated), + ), + ) + phase.update( + phase.total or phase.completed, + message=str(write_result.output), + details={ + "step": "artifact ready", + "file_count": len(write_result.files), + }, + ) + for warning_code, warning_message in recurrence_warnings: + _LOGGER.warning( + "%s [%s]", + warning_message, + warning_code, + ) + + validation_points_by_process = { + item.expanded.request.name: item.validation_points for item in generated + } + expected_process_ids = tuple( + process.process_id for process in artifact_processes + ) + concrete_requests = tuple(item.expanded.request for item in generated) + del artifact_processes + del generated + gc.collect() + self._analyze_process_groups(write_result.output, reporter) + + with reporter.phase( + "validation", + "Validating generated artifact", + total=1, + ) as phase: + validation = self._generation_config.validation + if validation.post_build_validation: + self._validate_generated_artifact( + write_result.output, + expected_process_ids, + validation_points=validation_points_by_process, + expected_api_bundle_path=write_result.api_bundle_path, + progress=phase, + ) + message = ( + f"schema and {validation.samples} numerical samples" + if validation.enabled + else "schema, references, hashes, and target" + ) + else: + message = "post-build validation disabled" + phase.update(1, message=message) + + reporter.timings["total"] = time.perf_counter() - generation_started + reporter.finish_generation() + return GenerationResult( + output=write_result.output, + processes=ProcessSet( + requests=concrete_requests, + aliases=requested_processes.aliases, + ), + mode=write_mode, + files=write_result.files, + ) + + def _artifact_model(self, resolved: _ResolvedModel) -> _CompiledModelPayload: + if resolved.compiled is not None: + return resolved.compiled + from ..models.loading import compile_model_source + + return compile_model_source("BUILTIN_SM", use_cache=False) + + def _expand_process_set( + self, + processes: ProcessSet, + resolved: _ResolvedModel, + phase: PhaseHandle, + ) -> tuple[_ExpandedProcess, ...]: + aliases_by_target: dict[str, list[ProcessAlias]] = {} + for alias in processes.aliases: + aliases_by_target.setdefault(alias.process_name, []).append(alias) + result: list[_ExpandedProcess] = [] + names: set[str] = set() + for request_index, request in enumerate(processes.requests, start=1): + expanded = self._expand_request(request, resolved) + request_aliases = aliases_by_target.get(request.name, []) + if request_aliases and len(expanded) != 1: + raise GenerationError( + f"process aliases for {request.name!r} are ambiguous after " + f"expansion into {len(expanded)} concrete processes" + ) + side_permutation_classes: set[tuple[tuple[str, ...], tuple[str, ...]]] = ( + set() + ) + for concrete_index, process_ir in enumerate(expanded, start=1): + permutation_class = _process_side_permutation_class(process_ir) + if permutation_class in side_permutation_classes: + continue + side_permutation_classes.add(permutation_class) + name = _expanded_name( + request.name, + concrete_index, + len(expanded), + ) + if name in names: + raise GenerationError(f"expanded process ID is not unique: {name}") + names.add(name) + concrete = ProcessRequest.parse(process_ir.process, name=name) + alias_records: list[Mapping[str, object]] = [] + for alias in request_aliases: + permutation = tuple(alias.particle_permutation) or tuple( + range(len(process_ir.legs)) + ) + if len(permutation) != len(process_ir.legs): + raise GenerationError( + f"alias {alias.name!r} permutation has length " + f"{len(permutation)}, expected {len(process_ir.legs)}" + ) + if any( + process_ir.legs[representative_index].side + != process_ir.legs[public_index].side + for representative_index, public_index in enumerate(permutation) + ): + raise GenerationError( + f"alias {alias.name!r} may only permute particles " + "within the incoming and outgoing sides" + ) + alias_expression, alias_pdgs = _permuted_process_identity( + process_ir, + permutation, + ) + alias_records.append( + { + "id": alias.name, + "expression": alias_expression, + "external_pdgs": list(alias_pdgs), + "external_permutation": list(permutation), + } + ) + result.append( + _ExpandedProcess( + request=concrete, + process_ir=process_ir, + aliases=tuple(alias_records), + source_expansion_size=len(expanded), + source_request=request, + ) + ) + phase.update(request_index, message=request.name) + if self._flavor_blind: + # Names are assigned above, so process IDs do not depend on the flag. + table = self._grouping_particle_table(resolved) + result = _collapse_flavor_blind_classes( + result, + table, + consider_charge_conjugation=self._consider_charge_conjugation, + relation_warning=self._flavor_blind_relation_warning(resolved, table), + ) + return tuple(result) + + def _grouping_particle_table( + self, + resolved: _ResolvedModel, + ) -> dict[int, GroupingParticle]: + """Particle types for process grouping, from the compiled model.""" + + model = self._artifact_model(resolved).ir + return particle_table( + model.particles, + {parameter.name: parameter for parameter in model.parameters}, + ) + + def _flavor_blind_relation_warning( + self, + resolved: _ResolvedModel, + table: Mapping[int, GroupingParticle], + ) -> _RelationWarning: + """Check relations against the vertices each representative is built from.""" + + from .process_grouping_symmetry import VertexSymmetryTable + + model = resolved.model + compiled = resolved.compiled + if compiled is not None and resolved.use_compiled_process_catalog: + symmetry = VertexSymmetryTable.from_compiled_ir(compiled.ir) + else: + model = model or _builtin_sm_model() + symmetry = VertexSymmetryTable.from_builtin_model(model) + explicit = self._coupling_order_limits + run = self._run_config + minimal = run is not None and run.process.coupling_order_policy == "minimal" + limits_by_representative: dict[str, Mapping[str, int]] = {} + + def warning( + member: GroupingProcess, + representative: GroupingProcess, + mapping: LegMapping, + representative_ir: CanonicalProcessIR, + ) -> str | None: + nonlocal model + limits = limits_by_representative.get(representative.process_id) + if limits is None: + limits = explicit + if minimal: + if model is None: + from ..models.external import CompiledUFOModel + + model = CompiledUFOModel(cast(_CompiledModelPayload, compiled)) + limits = ( + infer_minimal_coupling_order_limits( + representative_ir, + model=model, + max_coupling_orders=explicit or None, + ) + or explicit + ) + limits_by_representative[representative.process_id] = limits + return symmetry.relation_warning( + member, representative, mapping, table, limits=limits + ) + + return warning + + def _compile_for_generation( + self, + expanded: _ExpandedProcess, + model: Model, + phase: PhaseHandle, + ) -> _DagProcess | _UnsupportedProcess: + process_name = expanded.request.name + with phase.child( + process_name, + f"{process_name}: DAG construction", + details={ + "process": process_name, + "step": "colour-plan", + }, + ) as task: + + def report(details: Mapping[str, str | int]) -> None: + payload = {"process": process_name, **details} + completed = task.completed + total: int | None = None + if "mask_index" in details and "mask_total" in details: + completed = int(details["mask_index"]) + total = int(details["mask_total"]) + task.update( + completed, + total=total, + message=str(details.get("step", "DAG construction")), + details=payload, + ) + + try: + dag, coverage = self._compile_concrete_process( + expanded.process_ir, + model, + progress_callback=report if task.sink is not None else None, + ) + except _NoModelSupportedAmplitudes as exc: + if expanded.source_expansion_size <= 1: + raise + result: _DagProcess | _UnsupportedProcess = _UnsupportedProcess( + expanded=expanded, + reason=str(exc), + ) + step = "unsupported tree-level subprocess" + task.update( + task.completed, + message=step, + details={ + "process": process_name, + "step": step, + "amplitude_count": 0, + }, + ) + else: + result = _DagProcess( + expanded=expanded, + dag=dag, + coverage=coverage, + ) + step = "DAG complete" + task.update( + task.completed, + message=step, + details={ + "process": process_name, + "step": step, + "current_count": len(dag.currents), + "interaction_count": len(dag.interactions), + "amplitude_count": len(dag.amplitude_roots), + }, + ) + phase.advance( + message=process_name, + details={"process": process_name, "step": step}, + ) + return result + + def _prepare_warmup_process( + self, + process: _DagProcess, + model: Model, + *, + index: int, + phase: PhaseHandle, + ) -> _CompiledProcess: + process_name = process.expanded.request.name + with phase.child( + process_name, + f"{process_name}: structural reduction", + total=2, + details={"process": process_name, "step": "structural reduction"}, + ) as task: + result = self._prepare_warmup_process_inner( + process, + model, + index=index, + progress=task, + ) + phase.advance( + message=process_name, + details={"process": process_name, "step": "validation points ready"}, + ) + return result + + def _write_artifact(self, *args: Any, **kwargs: Any) -> ArtifactWriteResult: + """Keep optional payloads inside the existing publication transaction.""" + return write_schema_v3_artifact(*args, **kwargs) + + @property + def _numerical_process_grouping(self) -> bool: + run = self._run_config + return run is not None and run.process.analyze_process_groups == "numerical" + + def _analyze_process_groups( + self, + artifact: Path, + reporter: GenerationPhaseReporter, + ) -> None: + """Merge numerically found process groups into the written artifact.""" + + run = self._run_config + if run is None or not self._numerical_process_grouping: + return + from .process_grouping_numerical import update_artifact_process_groups + + with reporter.phase( + "process-grouping", + "Analyzing process groups numerically", + total=1, + ) as phase: + update_artifact_process_groups( + artifact, + analysis=str(run.process.analyze_process_groups), + only_unit_multiplier=run.process.only_unit_multiplier, + consider_charge_conjugation=run.process.consider_charge_conjugation, + ) + phase.update(1, message="process-groups.json updated") + + def _prepare_warmup_process_inner( + self, + process: _DagProcess, + model: Model, + *, + index: int, + progress: PhaseHandle, + ) -> _CompiledProcess: + process_name = process.expanded.request.name + progress.update( + 0, + message="structural reduction", + details={ + "process": process_name, + "step": "structural reduction", + "current_count": len(process.dag.currents), + "interaction_count": len(process.dag.interactions), + "amplitude_count": len(process.dag.amplitude_roots), + }, + ) + parity_preweighted = any( + float(root.helicity_weight) > 1.0 for root in process.dag.amplitude_roots + ) + reduced = ( + prune_dag_to_amplitude_roots(process.dag) + if parity_preweighted + else prune_global_helicity_flip_equivalent_roots(process.dag, model) + ) + if reduced.process.color_accuracy == "lc": + reduced, dynamic_color_projection_certificate = ( + project_rectangular_dynamic_color_classes( + reduced, + model, + source_revision=active_source_revision(), + ) + ) + dynamic_color_projection = ( + dynamic_color_projection_certificate.to_json_dict() + ) + else: + dynamic_color_projection = dynamic_color_projection_not_applicable_report( + reduced + ) + helicity_sum_dag: GenericDAG | None = None + helicity_selector_union_dag: GenericDAG | None = None + all_flow_union = self._all_flow_union_enabled + helicity_recurrence = build_helicity_recurrence_plan(reduced, model) + if helicity_recurrence is not None: + if self._eager_execution_enabled: + # Eager invocation tables already carry selector domains. Keep + # the exact recurrence proof, but do not serialize the + # compiled lane's evaluator-chunk schedules into an eager + # artifact. + reduced = replace( + reduced, + helicity_recurrence=helicity_recurrence, + helicity_materialization=None, + ) + else: + # Topology replay keeps its existing fused helicity-sum lane. + # The all-flow union is itself the primary reusable lane, so + # compiling that second payload would duplicate the expensive + # cross-flow recurrence without helping its target workload. + if not all_flow_union: + helicity_sum_dag = replace( + reduced, + helicity_recurrence=None, + helicity_materialization=None, + ) + materialization = materialize_helicity_recurrence( + reduced, + helicity_recurrence, + ) + reduced = replace( + materialization.dag, + helicity_recurrence=helicity_recurrence, + helicity_materialization=materialization, + ) + if not all_flow_union: + helicity_selector_union_dag = ( + self._compile_complete_color_helicity_selector_union( + process, + model, + ) + ) + if helicity_selector_union_dag is not None: + _validate_matching_helicity_selector_domains( + reduced, + helicity_selector_union_dag, + ) + relation_config = self._generation_config.relation_discovery + relation_mode = str(relation_config.mode) + relation_execution_mode = cast( + Literal["compiled", "eager"], + "eager" if self._eager_execution_enabled else "compiled", + ) + relation_lanes: dict[str, object] = {} + certified_relation_count = 0 + applied_relation_count = 0 + relation_warning_required = False + + def apply_numerical_relations( + lane: str, + lane_dag: GenericDAG, + ) -> GenericDAG: + nonlocal applied_relation_count + nonlocal certified_relation_count + nonlocal relation_warning_required + if relation_mode == "off": + relation_lanes[lane] = generic_dag_numerical_current_opt_out_report( + lane_dag, + execution_mode=relation_execution_mode, + ) + return lane_dag + if all_flow_union or lane == "helicity-selector-union": + application_relation_kinds: tuple[ + Literal["equal", "opposite", "zero"], ... + ] = () + application_scope_reason = "multi-helicity-all-flow-selector-domain" + elif self._color_accuracy in {"nlc", "full"}: + application_relation_kinds = ("zero", "equal") + application_scope_reason = "contracted-binary64-opposite-contained" + else: + application_relation_kinds = ("equal", "opposite", "zero") + application_scope_reason = "complete-generic-dag" + try: + # Symbolica evaluator construction is process-global. Keep + # bounded high-precision captures serial even when independent + # process DAGs are prepared by worker threads. + with _SYMBOLICA_MATERIALIZATION_LOCK: + result = run_generic_dag_numerical_current_warmup( + lane_dag, + model, + process_id=process_name, + mode=cast( + Literal["diagnostic", "certified-reuse"], + relation_mode, + ), + execution_mode=relation_execution_mode, + precision_digits=relation_config.precision_digits, + probe_count=relation_config.probe_count, + verification_probe_count=( + relation_config.verification_probe_count + ), + relative_tolerance=relation_config.relative_tolerance, + absolute_tolerance=relation_config.absolute_tolerance, + seed=relation_config.seed, + application_relation_kinds=(application_relation_kinds), + application_scope_reason=application_scope_reason, + ) + except ValueError as exc: + raise GenerationError( + f"process {process_name!r} numerical current warm-up " + f"failed closed in lane {lane!r}: {exc}" + ) from exc + certified_relation_count += len(result.discovery.certificates) + applied_relation_count += result.application.report.applied_relation_count + payload = result.to_json_dict() + if result.warning_required: + warning = payload.get("warning") + if ( + not isinstance(warning, Mapping) + or warning.get("code") != NUMERICAL_CURRENT_RELATION_WARNING_CODE + or warning.get("message") != NUMERICAL_CURRENT_RELATION_WARNING + ): + raise GenerationError( + "numerical current warm-up requested a warning " + "without its stable warning contract" + ) + relation_warning_required = True + relation_lanes[lane] = payload + return result.dag + + progress.update( + 1, + message="authenticated numerical current warm-up", + details={ + "process": process_name, + "step": "authenticated numerical current warm-up", + "mode": relation_mode, + }, + ) + reduced = apply_numerical_relations("primary", reduced) + if helicity_sum_dag is not None: + helicity_sum_dag = apply_numerical_relations( + "helicity-sum", + helicity_sum_dag, + ) + if helicity_selector_union_dag is not None: + helicity_selector_union_dag = apply_numerical_relations( + "helicity-selector-union", + helicity_selector_union_dag, + ) + warning_required = relation_warning_required + relation_discovery_payload: dict[str, object] = { + "schema_version": 1, + "abi": "pyamplicol-artifact-numerical-current-reuse-v1", + "requested_mode": relation_mode, + "effective_mode": relation_mode, + "effective_reuse_state": ( + "disabled" if relation_mode == "off" else "enabled" + ), + "execution_mode": relation_execution_mode, + "lane_count": len(relation_lanes), + "lanes": relation_lanes, + "certified_relation_count": certified_relation_count, + "applied_relation_count": applied_relation_count, + "relation_correctness": numerical_relation_correctness_payload( + applied_relation_count + ), + "structural_exact_discovery": None, + "opt_out_flag": "--no-numerical-current-reuse", + "warning": ( + { + "required": True, + "emit": "once-per-generated-artifact", + "code": NUMERICAL_CURRENT_RELATION_WARNING_CODE, + "message": NUMERICAL_CURRENT_RELATION_WARNING, + } + if warning_required + else { + "required": False, + "emit": "never", + "code": None, + "message": None, + } + ), + } + before_amplitude_roots = ( + round( + sum(float(root.helicity_weight) for root in process.dag.amplitude_roots) + ) + if parity_preweighted + else len(process.dag.amplitude_roots) + ) + validation = self._generation_config.validation + filters: dict[str, object] = { + **( + {} + if self._color_accuracy != "lc" + else { + "lc_flow_layout": ( + "all-flow-union" if all_flow_union else "topology-replay" + ) + } + ), + "structural_helicity_reduction": { + "applied": parity_preweighted or reduced is not process.dag, + "before_amplitude_roots": before_amplitude_roots, + "after_amplitude_roots": len(reduced.amplitude_roots), + "mode": "proven global-helicity-flip equivalence", + }, + "dynamic_color_projection": dict(dynamic_color_projection), + "relation_discovery": relation_discovery_payload, + **( + {} + if reduced.helicity_recurrence is None + else { + "helicity_recurrence": { + "contract_version": (HELICITY_RECURRENCE_CONTRACT_VERSION), + **reduced.helicity_recurrence.proof_counts(), + **( + {} + if reduced.helicity_materialization is None + else { + "materialized_current_count": ( + reduced.helicity_materialization.materialized_current_count + ), + "materialized_amplitude_count": ( + reduced.helicity_materialization.materialized_root_count + ), + "materialization_strategy": ( + reduced.helicity_materialization.strategy + ), + } + ), + } + } + ), + } + progress.update( + 1, + message="validation-point preparation", + details={ + "process": process_name, + "step": "validation-point preparation", + "current_count": len(reduced.currents), + "interaction_count": len(reduced.interactions), + "amplitude_count": len(reduced.amplitude_roots), + }, + ) + sample_count = ( + validation.samples + if validation.enabled and validation.post_build_validation + else 1 + ) + points = tuple( + build_validation_point( + reduced, + model, + process_id=process.expanded.request.name, + seed=validation.seed + index * sample_count + sample_index, + ) + for sample_index in range(sample_count) + ) + point = points[0] + progress.update( + 2, + message=( + process_name + if point.available + else f"{process_name}: metadata-only validation" + ), + details={ + "process": process_name, + "step": "validation points ready", + "samples": len(points), + }, + ) + coverage = { + **dict(process.coverage), + "current_count": len(reduced.currents), + "interaction_count": len(reduced.interactions), + "interaction_evaluation_count": reduced.interaction_evaluation_count, + "amplitude_root_count": len(reduced.amplitude_roots), + "dynamic_color_projection": dict(dynamic_color_projection), + "relation_discovery": relation_discovery_payload, + **( + {} + if reduced.helicity_recurrence is None + else { + "helicity_recurrence_contract_version": ( + HELICITY_RECURRENCE_CONTRACT_VERSION + ), + **reduced.helicity_recurrence.proof_counts(), + **( + {} + if reduced.helicity_materialization is None + else { + "proof_current_count": ( + reduced.helicity_materialization.proof_current_count + ), + "proof_amplitude_count": ( + reduced.helicity_materialization.proof_root_count + ), + "helicity_materialization_strategy": ( + reduced.helicity_materialization.strategy + ), + } + ), + } + ), + } + return _CompiledProcess( + expanded=process.expanded, + dag=reduced, + helicity_sum_dag=helicity_sum_dag, + helicity_selector_union_dag=helicity_selector_union_dag, + coverage=coverage, + filters=filters, + validation_points=points, + ) + + def _recurrence_model_inputs( + self, + resolved_model: _ResolvedModel, + ) -> _RecurrenceModelInputs: + compiled = resolved_model.compiled + bundle = None if compiled is None else compiled.prepared_bundle + if bundle is None: # pragma: no cover - preflighted during model loading + raise GenerationError("recurrence generation has no prepared model bundle") + catalog = bundle.kernel_pack.recurrence_template_catalog + if catalog is None: # pragma: no cover - preflighted during model loading + raise GenerationError( + "recurrence generation has no recurrence-template-v1 catalog" + ) + direct_catalog = bundle.kernel_pack.recurrence_direct_template_catalog + if direct_catalog is None: # pragma: no cover - preflighted during loading + raise GenerationError( + "recurrence generation has no recurrence-direct-template-v1 catalog" + ) + return _RecurrenceModelInputs( + bundle=bundle, + catalog=catalog, + template_catalog_json=json.dumps( + catalog.to_dict(), + allow_nan=False, + ensure_ascii=True, + separators=(",", ":"), + sort_keys=True, + ).encode("ascii"), + template_input=build_recurrence_template_input_v1(catalog), + direct_template_catalog=direct_catalog, + direct_template_catalog_json=json.dumps( + direct_catalog.to_dict(), + allow_nan=False, + ensure_ascii=True, + separators=(",", ":"), + sort_keys=True, + ).encode("ascii"), + prepared_kernel_pack_digest=(catalog.header.prepared_kernel_pack_digest), + ) + + def _lower_recurrence_helicity_selector_plan( + self, + logical: RecurrenceBuilderLogicalInputV1, + color_plan: GenericColorPlan, + model_inputs: _RecurrenceModelInputs, + temporary_root: Path, + *, + process_id: str, + progress_callback: Callable[[Mapping[str, object]], None] | None = None, + ) -> RecurrenceHelicitySelectorPlanArtifact: + """Lower and bind one persisted physical-colour/runtime-H schedule. + + The native lowering emits the authenticated dispatch sidecar together + with the ordinary Direct plan; no OTF seed or query family is built. + """ + + if logical.layout != "all-flow-union" or logical.replay_partitions: + raise GenerationError( + "recurrence helicity-selector plan requires physical " + "all-flow-union without replay" + ) + if color_plan.color_accuracy not in {"nlc", "full"}: + raise GenerationError( + "recurrence helicity-selector plan requires complete NLC/full color" + ) + run = self._run_config + if run is None: # pragma: no cover - recurrence requires RunConfig + raise GenerationError( + "recurrence helicity-selector lowering has no evaluator config" + ) + recurrence_evaluator = run.evaluator.recurrence + builder_input = build_recurrence_builder_input_v1(logical) + native_schedule_digest, base_schedule_digest, sharing_domain_digest = ( + _recurrence_schedule_semantic_digests( + logical, + prepared_kernel_pack_digest=(model_inputs.prepared_kernel_pack_digest), + direct_template_catalog_digest=( + model_inputs.direct_template_catalog.catalog_digest + ), + point_tile_size=recurrence_evaluator.point_tile_size, + workspace_mib=recurrence_evaluator.workspace_mib, + relation_discovery=None, + ) + ) + + relation_discovery = self._generation_config.relation_discovery + + def lower_schedule() -> _RustRecurrenceLoweringOutput: + output_root = ( + temporary_root + / "recurrence-helicity-selector-schedules" + / base_schedule_digest + ) + return _invoke_rust_recurrence_lowering_v2( + builder_input, + model_inputs.template_input, + model_inputs.direct_template_catalog, + model_inputs.prepared_kernel_pack_digest, + native_schedule_digest, + output_root / "recurrence-runtime.pacbin", + point_tile_size=recurrence_evaluator.point_tile_size, + workspace_mib=recurrence_evaluator.workspace_mib, + relation_discovery_mode="off", + relation_discovery_precision_digits=( + relation_discovery.precision_digits + ), + relation_discovery_probe_count=relation_discovery.probe_count, + relation_discovery_verification_probe_count=( + relation_discovery.verification_probe_count + ), + relation_discovery_relative_tolerance=( + relation_discovery.relative_tolerance + ), + relation_discovery_absolute_tolerance=( + relation_discovery.absolute_tolerance + ), + relation_discovery_seed=relation_discovery.seed, + color_accuracy=str(color_plan.color_accuracy), + helicity_dispatch_destination=( + output_root / "recurrence-helicity-dispatch-v1.bin" + ), + progress_callback=progress_callback, + direct_template_catalog_json=( + model_inputs.direct_template_catalog_json + ), + ) + + process_scoped_cache = RecurrenceScheduleLoweringCache[ + _RustRecurrenceLoweringOutput + ]() + shared = process_scoped_cache.lower_process( + logical, + schedule_digest=base_schedule_digest, + sharing_domain_digest=sharing_domain_digest, + native_schedule_semantic_digest=native_schedule_digest, + direct_executor_count=len(model_inputs.direct_template_catalog.templates), + parameter_slot_count=len(model_inputs.catalog.parameters), + lower=lower_schedule, + ) + expected_identity_remap = RecurrenceProcessRemap.identity( + logical, + direct_executor_count=len(model_inputs.direct_template_catalog.templates), + parameter_slot_count=len(model_inputs.catalog.parameters), + ) + if shared.remap != expected_identity_remap: + raise GenerationError( + "recurrence helicity-selector AllFlow plan acquired a nonidentity " + "cross-process remap" + ) + output = shared.output + dispatch = output.helicity_dispatch + if dispatch is None: + raise GenerationError( + "native recurrence helicity-selector lowering omitted its exact " + "dispatch" + ) + if output.inspection_summary.get("lc_flow_layout") != "all-flow-union": + raise GenerationError( + "native recurrence helicity-selector lowering changed its layout" + ) + resolved_helicities = shared.remap.remap_resolved_helicities( + output.resolved_helicities + ) + if dispatch.resolved_helicity_count != len(resolved_helicities): + raise GenerationError( + "recurrence helicity dispatch does not cover the auxiliary full " + "helicity domain" + ) + try: + destinations = tuple( + ( + shared.remap.physical_sector_ids[sector_id], + helicity_id, + ) + for sector_id, helicity_id in output.amplitude_destinations + ) + except IndexError as exc: + raise GenerationError( + "recurrence helicity-selector destination lies outside its " + "physical-owner binding" + ) from exc + expected_destinations = tuple( + (sector_id, None) for sector_id in range(len(logical.physical_sectors)) + ) + if destinations != expected_destinations: + raise GenerationError( + "recurrence helicity-selector destinations are not dense " + "runtime-helicity physical owners" + ) + + required_direct_executor_ids = _required_process_direct_executor_ids( + output.exact_sections, + shared.remap, + ) + process_executor_pack = build_recurrence_process_executor_pack( + direct_catalog=model_inputs.direct_template_catalog, + kernel_pack=model_inputs.bundle.kernel_pack, + required_executor_ids=required_direct_executor_ids, + runtime_layout_digest=dispatch.base_runtime_layout_digest, + ) + selector_schedule_digest = recurrence_helicity_selector_schedule_digest( + shared.schedule_digest, + dispatch.payload_sha256, + ) + inspection_summary = _process_scoped_recurrence_inspection_summary( + output.inspection_summary, + process_id=process_id, + semantic_digest=builder_input.digest, + schedule_digest=shared.schedule_digest, + ) + return RecurrenceHelicitySelectorPlanArtifact( + recurrence_schedule_path=output.payload_path, + recurrence_base_schedule_digest=shared.schedule_digest, + recurrence_schedule_digest=selector_schedule_digest, + recurrence_native_schedule_semantic_digest=( + shared.native_schedule_semantic_digest + ), + recurrence_schedule_size_bytes=output.payload_size_bytes, + recurrence_schedule_sha256=output.payload_sha256, + recurrence_schedule_member_count=output.member_count, + recurrence_schedule_unpacked_size_bytes=output.unpacked_size_bytes, + recurrence_schedule_index_sha256=output.index_sha256, + helicity_dispatch_path=dispatch.payload_path, + helicity_dispatch_size_bytes=dispatch.payload_size_bytes, + helicity_dispatch_sha256=dispatch.payload_sha256, + helicity_dispatch_base_runtime_layout_digest=( + dispatch.base_runtime_layout_digest + ), + helicity_dispatch_resolved_helicity_count=( + dispatch.resolved_helicity_count + ), + physical_destination_count=len(destinations), + builder_input_sha256=builder_input.digest, + inspection_summary=inspection_summary, + referenced_kernel_ids=recurrence_referenced_kernel_ids(logical), + recurrence_process_remap=shared.remap, + recurrence_process_executor_pack=process_executor_pack, + ) + + def _drop_processes_without_amplitudes( + self, + expanded: tuple[_ExpandedProcess, ...], + model: Model, + ) -> tuple[_ExpandedProcess, ...]: + """Skip children of a multiparticle request that the model cannot connect. + + The recurrence builders, unlike the DAG compiler, need an amplitude. + """ + + results: list[_ExpandedProcess | _UnsupportedProcess] = [] + for entry in expanded: + process = entry.process_ir + supported = entry.source_expansion_size <= 1 or ( + has_model_supported_amplitudes( + process, + model=model, + color_plan=build_color_plan( + process, + color_accuracy=self._color_accuracy, + fold_trace_reflections=( + model.lc_trace_reflection_equivalence_is_proven(process) + ), + ), + max_coupling_orders=self._coupling_order_limits or None, + ) + ) + results.append( + entry + if supported + else _UnsupportedProcess( + expanded=entry, + reason=( + f"process {process.process!r} has no model-supported " + "tree-level amplitudes" + ), + ) + ) + kept, unsupported = _partition_model_supported_processes(results) + _warn_unsupported_processes(unsupported) + return kept + + def _construct_recurrence_artifact( + self, + expanded: _ExpandedProcess, + model: Model, + model_inputs: _RecurrenceModelInputs, + temporary_root: Path, + *, + index: int, + phase: PhaseHandle, + lowering_cache: RecurrenceScheduleLoweringCache[_RustRecurrenceLoweringOutput], + generation_timings: dict[str, float] | None = None, + ) -> _GeneratedRecurrenceProcess: + process_name = expanded.request.name + profile_started = time.perf_counter() + profile: dict[str, float] = {} + selector_plan_enabled = self._recurrence_helicity_selector_plan_enabled() + with phase.child( + process_name, + f"{process_name}: compact recurrence construction", + total=3, + details={"process": process_name, "step": "colour planning"}, + ) as task: + preparation_started = time.perf_counter() + prepared = self._prepare_process_construction(expanded.process_ir, model) + profile["process-model-color-preparation"] = ( + time.perf_counter() - preparation_started + ) + normalization_started = time.perf_counter() + normalization_contract, normalization_payload = ( + build_recurrence_normalization(expanded.process_ir, model) + ) + profile["normalization"] = time.perf_counter() - normalization_started + layout = ( + "contracted-color-union" + if self._color_accuracy in {"nlc", "full"} + else ( + "all-flow-union" + if self._all_flow_union_enabled + else "topology-replay" + ) + ) + typed_layout = cast( + Literal[ + "topology-replay", + "all-flow-union", + "contracted-color-union", + ], + layout, + ) + selection = self._process_selection + if ( + layout == "contracted-color-union" + and selection.selected_color_sector_ids is not None + ): + raise GenerationError( + "NLC/full recurrence contracts the complete color basis and " + "does not support generation-selected color sectors" + ) + source_selection = ( + None + if selection.selected_source_helicities is None + else tuple(sorted(selection.selected_source_helicities.items())) + ) + projection_color_plan = prepared.complete_color_plan + if selection.selected_color_sector_ids is not None: + # Compact recurrence tables require dense physical-sector IDs. + # A generation-specialized artifact owns only its restricted + # sector domain, so remap that domain before projection instead + # of retaining the unrequested complete-plan aliases. + projection_color_plan = replace( + prepared.restricted_color_plan, + sectors=tuple( + replace(sector, id=dense_id) + for dense_id, sector in enumerate( + prepared.restricted_color_plan.sectors + ) + ), + ) + projection_started = time.perf_counter() + logical = project_recurrence_process_v1( + expanded.process_ir, + projection_color_plan, + model_inputs.catalog, + layout=typed_layout, + normalization=normalization_contract, + topology_replay=prepared.topology_replay, + generation_slice=RecurrenceGenerationSliceV1( + selected_source_helicities=source_selection, + ), + coupling_order_limits=prepared.coupling_order_limits, + model=model, + prefer_symmetric_group_closure_anchor=( + self._run_config is not None + and self._run_config.color.contraction.value + in {"auto", "symmetric-group-fft"} + ), + ) + if selection.selected_color_sector_ids is not None: + selected_public_flow_ids = tuple( + flow.flow_id for flow in logical.public_flows + ) + logical = replace( + logical, + selected_public_flow_ids=selected_public_flow_ids, + ) + profile["projection-semantic-catalog"] = ( + time.perf_counter() - projection_started + ) + task.update( + 1, + message="columnar recurrence input", + details={ + "process": process_name, + "step": "columnar recurrence input", + "color_sector_count": len(logical.physical_sectors), + "public_flow_count": len(logical.public_flows), + }, + ) + columnar_started = time.perf_counter() + builder_input = build_recurrence_builder_input_v1(logical) + profile["columnar-normalization"] = time.perf_counter() - columnar_started + native_process_id = logical.process_id + runtime_metadata_started = time.perf_counter() + runtime_metadata = build_recurrence_runtime_metadata( + logical, + model_inputs.catalog, + model, + normalization_payload, + ) + profile["runtime-metadata"] = time.perf_counter() - runtime_metadata_started + run = self._run_config + if run is None: # pragma: no cover - recurrence requires RunConfig + raise GenerationError( + "recurrence generation has no evaluator configuration" + ) + recurrence_evaluator = run.evaluator.recurrence + relation_discovery = self._generation_config.relation_discovery + relation_discovery_mode = str(relation_discovery.mode) + relation_discovery_identity = ( + None + if relation_discovery_mode == "off" + else { + "mode": relation_discovery_mode, + "precision_digits": relation_discovery.precision_digits, + "probe_count": relation_discovery.probe_count, + "verification_probe_count": ( + relation_discovery.verification_probe_count + ), + "relative_tolerance": (relation_discovery.relative_tolerance), + "absolute_tolerance": (relation_discovery.absolute_tolerance), + "seed": relation_discovery.seed, + "color_accuracy": str(expanded.process_ir.color_accuracy), + "probe_process_id": native_process_id, + } + ) + schedule_digest_started = time.perf_counter() + ( + native_schedule_semantic_digest, + request_schedule_digest, + sharing_domain_digest, + ) = _recurrence_schedule_semantic_digests( + logical, + prepared_kernel_pack_digest=(model_inputs.prepared_kernel_pack_digest), + direct_template_catalog_digest=( + model_inputs.direct_template_catalog.catalog_digest + ), + point_tile_size=recurrence_evaluator.point_tile_size, + workspace_mib=recurrence_evaluator.workspace_mib, + relation_discovery=relation_discovery_identity, + ) + profile["schedule-normalization-and-digests"] = ( + time.perf_counter() - schedule_digest_started + ) + task.update( + 2, + message="Rust recurrence builder", + details={ + "process": process_name, + "step": "Rust recurrence builder", + }, + ) + native_phase_total = len(logical.external_legs) + 1 + with task.child( + "rust-builder", + f"{process_name}: Rust recurrence construction", + total=native_phase_total, + unit="phases", + details={ + "process": process_name, + "step": "source construction", + }, + ) as native_task: + + def report_native(details: Mapping[str, object]) -> None: + progress_index = int( + details.get("phase_index", native_task.completed) + ) + progress_total = int(details.get("phase_total", native_phase_total)) + contribution_count = int(details.get("contribution_count", 0)) + display_details = { + "process": process_name, + "step": str(details.get("step", "recurrence construction")), + "stage_total": int(details.get("stage_total", 0)), + "candidate_parent_pair_count": int( + details.get("candidate_parent_pair_count", 0) + ), + "current_count": int(details.get("current_count", 0)), + "contribution_count": contribution_count, + "dynamic_color_state_count": int( + details.get("dynamic_color_state_count", 0) + ), + "color_target_prune_count": int( + details.get("color_target_prune_count", 0) + ), + } + for name in ( + "stage_index", + "subset_size", + "candidate_parent_pair_total", + ): + value = details.get(name) + if value is not None: + display_details[name] = int(value) + native_task.update( + progress_index, + total=progress_total, + message=str(details.get("step", "recurrence construction")), + details=display_details, + ) + + final_schedule_path = ( + temporary_root + / "recurrence-schedules" + / request_schedule_digest + / "recurrence-runtime.pacbin" + ) + + def lower_schedule() -> _RustRecurrenceLoweringOutput: + def lower_once( + destination: Path, + *, + mode: str, + evidence: bytes | None, + report_progress: bool, + timing_name: str, + ) -> _RustRecurrenceLoweringOutput: + lowering_started = time.perf_counter() + try: + return _invoke_rust_recurrence_lowering_v2( + builder_input, + model_inputs.template_input, + model_inputs.direct_template_catalog, + model_inputs.prepared_kernel_pack_digest, + native_schedule_semantic_digest, + destination, + point_tile_size=(recurrence_evaluator.point_tile_size), + workspace_mib=recurrence_evaluator.workspace_mib, + relation_discovery_mode=mode, + relation_discovery_precision_digits=( + relation_discovery.precision_digits + ), + relation_discovery_probe_count=( + relation_discovery.probe_count + ), + relation_discovery_verification_probe_count=( + relation_discovery.verification_probe_count + ), + relation_discovery_relative_tolerance=( + relation_discovery.relative_tolerance + ), + relation_discovery_absolute_tolerance=( + relation_discovery.absolute_tolerance + ), + relation_discovery_seed=relation_discovery.seed, + color_accuracy=str(expanded.process_ir.color_accuracy), + relation_discovery_evidence_json=evidence, + progress_callback=( + report_native + if report_progress and native_task.sink is not None + else None + ), + direct_template_catalog_json=( + model_inputs.direct_template_catalog_json + ), + ) + finally: + profile[timing_name] = ( + time.perf_counter() - lowering_started + ) + + if relation_discovery_mode == "off": + output = lower_once( + final_schedule_path, + mode="off", + evidence=None, + report_progress=True, + timing_name="native-final-generation", + ) + lane_report = recurrence_numerical_current_opt_out_report( + _parse_exact_sections( + output.exact_sections, + native_process_id, + ), + color_accuracy=str(expanded.process_ir.color_accuracy), + ) + runtime_inspection, aggregate_report = ( + _recurrence_relation_reporting( + output.inspection_summary, + mode=relation_discovery_mode, + lane_report=lane_report, + ) + ) + return replace( + output, + inspection_summary=runtime_inspection, + generation_profile={ + "schema_version": 1, + "native_passes": { + "final": dict(output.generation_profile) + }, + }, + numerical_current_reuse_report=aggregate_report, + ) + + baseline = lower_once( + temporary_root + / ".numerical-current-baseline" + / request_schedule_digest + / "recurrence-runtime.pacbin", + mode="off", + evidence=None, + report_progress=True, + timing_name="native-baseline-generation", + ) + probe_points_started = time.perf_counter() + candidate_points, verification_points = ( + build_recurrence_numerical_current_probe_points( + expanded.process_ir, + model, + process_id=native_process_id, + seed=relation_discovery.seed, + candidate_count=relation_discovery.probe_count, + verification_count=( + relation_discovery.verification_probe_count + ), + ) + ) + profile["numerical-probe-point-preparation"] = ( + time.perf_counter() - probe_points_started + ) + envelope_fallback: ( + RecurrenceNumericalEvidenceEnvelopeExceeded | None + ) = None + warmup = None + try: + with _SYMBOLICA_MATERIALIZATION_LOCK: + baseline_plan_started = time.perf_counter() + baseline_plan = _RecurrenceExactPlan.from_generation( + raw_sections=baseline.exact_sections, + process_id=native_process_id, + bundle=model_inputs.bundle, + runtime_metadata=runtime_metadata, + ) + profile["numerical-baseline-plan-load"] = ( + time.perf_counter() - baseline_plan_started + ) + warmup_started = time.perf_counter() + warmup = run_recurrence_numerical_current_warmup( + baseline_plan, + candidate_points=candidate_points, + verification_points=verification_points, + mode=cast( + Literal[ + "diagnostic", + "certified-reuse", + ], + relation_discovery_mode, + ), + color_accuracy=str(expanded.process_ir.color_accuracy), + precision_digits=(relation_discovery.precision_digits), + seed=relation_discovery.seed, + relative_tolerance=( + relation_discovery.relative_tolerance + ), + absolute_tolerance=( + relation_discovery.absolute_tolerance + ), + ) + profile["numerical-warmup-total"] = ( + time.perf_counter() - warmup_started + ) + profile.update( + { + f"numerical-{name}": seconds + for name, seconds in ( + warmup.generation_profile_timings.items() + ) + } + ) + except RecurrenceNumericalEvidenceEnvelopeExceeded as exc: + profile["numerical-warmup-total"] = ( + time.perf_counter() - warmup_started + ) + if relation_discovery_mode != "certified-reuse": + raise GenerationError( + f"process {process_name!r} recurrence numerical " + "diagnostic evidence exceeds its memory envelope" + ) from exc + envelope_fallback = ( + _detached_recurrence_evidence_envelope_outcome( + exc, + source_error=exc, + ) + ) + except (ArtifactError, ValueError) as exc: + raise GenerationError( + f"process {process_name!r} recurrence numerical " + f"current warm-up failed closed: {exc}" + ) from exc + if envelope_fallback is not None: + fallback_details = envelope_fallback.to_json_dict() + geometry_details = cast( + Mapping[str, object], + fallback_details["geometry"], + ) + native_task.update( + native_task.completed, + message=EVIDENCE_ENVELOPE_FALLBACK_WARNING, + details={ + "process": process_name, + "step": "numerical-current reuse disabled", + "warning_code": (EVIDENCE_ENVELOPE_FALLBACK_REASON), + "current_count": int(geometry_details["current_count"]), + "component_count": int( + geometry_details["component_count"] + ), + "scalar_count": int(geometry_details["scalar_count"]), + "row_count": int(geometry_details["row_count"]), + }, + ) + warmup_owner = None + if warmup is not None: + warmup_owner = [warmup] + warmup = None + return _complete_recurrence_evidence_envelope_fallback( + final_schedule_path=final_schedule_path, + baseline=baseline, + baseline_plan=baseline_plan, + color_accuracy=str(expanded.process_ir.color_accuracy), + outcome=envelope_fallback, + warmup_owner=warmup_owner, + ) + if warmup is None: + raise AssertionError( + "recurrence numerical warm-up completed without a result" + ) + try: + native_capacity_fallback: ( + RecurrenceNumericalEvidenceEnvelopeExceeded | None + ) = None + try: + if not warmup.certificates: + warmup = warmup.without_evidence_transport() + output, application_validation_required = ( + _lower_recurrence_after_numerical_warmup( + lower_once=lower_once, + final_schedule_path=final_schedule_path, + baseline=baseline, + warmup=warmup, + requested_mode=relation_discovery_mode, + ) + ) + except (ArtifactError, GenerationError) as exc: + native_capacity_fallback = ( + recurrence_numerical_evidence_capacity_outcome( + baseline_plan.sections, + candidate_probe_count=len(candidate_points), + verification_probe_count=len(verification_points), + runtime_parameter_count=len( + baseline_plan.runtime_parameter_schema + ), + error=exc, + ) + ) + if ( + native_capacity_fallback is None + or relation_discovery_mode != "certified-reuse" + ): + raise + native_capacity_fallback = ( + _detached_recurrence_evidence_envelope_outcome( + native_capacity_fallback, + source_error=exc, + ) + ) + if native_capacity_fallback is not None: + # The failed native call can retain its evidence + # argument through exception frames. The handler + # above detached those frames; close both trusted + # spools and drop the result (including the evidence + # transport) before publishing the already + # validated reuse-off baseline. + fallback_details = native_capacity_fallback.to_json_dict() + geometry_details = cast( + Mapping[str, object], + fallback_details["geometry"], + ) + native_task.update( + native_task.completed, + message=EVIDENCE_ENVELOPE_FALLBACK_WARNING, + details={ + "process": process_name, + "step": "numerical-current reuse disabled", + "warning_code": (EVIDENCE_ENVELOPE_FALLBACK_REASON), + "current_count": int( + geometry_details["current_count"] + ), + "component_count": int( + geometry_details["component_count"] + ), + "scalar_count": int( + geometry_details["scalar_count"] + ), + "row_count": int(geometry_details["row_count"]), + }, + ) + fallback_color_accuracy = str( + expanded.process_ir.color_accuracy + ) + warmup_owner = [warmup] + warmup = None + return _complete_recurrence_evidence_envelope_fallback( + final_schedule_path=final_schedule_path, + baseline=baseline, + baseline_plan=baseline_plan, + color_accuracy=fallback_color_accuracy, + outcome=native_capacity_fallback, + warmup_owner=warmup_owner, + ) + if warmup is None: + raise AssertionError( + "recurrence numerical warm-up was released " + "without a fallback" + ) + if not application_validation_required: + return output + warmup = warmup.without_evidence_transport() + try: + with _SYMBOLICA_MATERIALIZATION_LOCK: + applied_plan_started = time.perf_counter() + applied_plan = _RecurrenceExactPlan.from_generation( + raw_sections=output.exact_sections, + process_id=native_process_id, + bundle=model_inputs.bundle, + runtime_metadata=runtime_metadata, + ) + profile["numerical-application-plan-load"] = ( + time.perf_counter() - applied_plan_started + ) + application_validation_started = time.perf_counter() + warmup = ( + validate_recurrence_numerical_current_application( + warmup, + applied_plan, + baseline_plan=baseline_plan, + precision_digits=( + relation_discovery.precision_digits + ), + seed=relation_discovery.seed, + relative_tolerance=( + relation_discovery.relative_tolerance + ), + absolute_tolerance=( + relation_discovery.absolute_tolerance + ), + ) + ) + profile["numerical-application-validation"] = ( + time.perf_counter() - application_validation_started + ) + except (ArtifactError, ValueError) as exc: + raise GenerationError( + f"process {process_name!r} optimized recurrence " + f"current replay failed closed: {exc}" + ) from exc + runtime_inspection, aggregate_report = ( + _recurrence_relation_reporting( + output.inspection_summary, + mode=relation_discovery_mode, + lane_report=warmup.to_json_dict(), + ) + ) + return replace( + output, + inspection_summary=runtime_inspection, + generation_profile={ + "schema_version": 1, + "native_passes": { + "baseline": dict(baseline.generation_profile), + "final": dict(output.generation_profile), + }, + }, + numerical_current_reuse_report=aggregate_report, + ) + finally: + if warmup is not None: + warmup.close() + + schedule_lowering_started = time.perf_counter() + shared = lowering_cache.lower_process( + logical, + schedule_digest=request_schedule_digest, + sharing_domain_digest=sharing_domain_digest, + native_schedule_semantic_digest=(native_schedule_semantic_digest), + direct_executor_count=len( + model_inputs.direct_template_catalog.templates + ), + parameter_slot_count=len(model_inputs.catalog.parameters), + lower=lower_schedule, + ) + profile["schedule-sharing-and-lowering"] = ( + time.perf_counter() - schedule_lowering_started + ) + output = shared.output + schedule_digest = shared.schedule_digest + native_schedule_semantic_digest = shared.native_schedule_semantic_digest + process_remap = shared.remap + task.update( + 3, + message="compact recurrence runtime ready", + details={ + "process": process_name, + "step": "compact recurrence runtime ready", + }, + ) + companion_artifact = None + if selector_plan_enabled: + selector_phase_total = len(expanded.process_ir.legs) + 2 + with phase.child( + f"{process_name}:helicity-selector", + f"{process_name}: persisted helicity-selector plan", + total=selector_phase_total, + unit="phases", + details={ + "process": process_name, + "step": "physical-color selector projection", + }, + ) as selector_task: + companion_projection_started = time.perf_counter() + companion_logical = project_recurrence_process_v1( + expanded.process_ir, + prepared.complete_color_plan, + model_inputs.catalog, + layout="all-flow-union", + normalization=normalization_contract, + topology_replay=None, + generation_slice=RecurrenceGenerationSliceV1(), + coupling_order_limits=prepared.coupling_order_limits, + model=model, + prefer_symmetric_group_closure_anchor=( + run.color.contraction.value in {"auto", "symmetric-group-fft"} + ), + contracted_physical_color_union=True, + ) + profile["helicity-selector-physical-projection"] = ( + time.perf_counter() - companion_projection_started + ) + selector_task.update( + 1, + message="physical-color selector input ready", + details={ + "process": process_name, + "step": "physical-color selector input ready", + "color_sector_count": len(companion_logical.physical_sectors), + }, + ) + + def report_selector(details: Mapping[str, object]) -> None: + phase_index = int(details.get("phase_index", 0)) + phase_total = int( + details.get("phase_total", selector_phase_total - 1) + ) + selector_task.update( + 1 + phase_index, + total=1 + phase_total, + message=str( + details.get( + "step", + "persisted helicity-selector construction", + ) + ), + details={ + "process": process_name, + "step": str( + details.get( + "step", + "persisted helicity-selector construction", + ) + ), + "current_count": int(details.get("current_count", 0)), + "contribution_count": int( + details.get("contribution_count", 0) + ), + }, + ) + + companion_lowering_started = time.perf_counter() + companion_artifact = self._lower_recurrence_helicity_selector_plan( + companion_logical, + prepared.complete_color_plan, + model_inputs, + temporary_root / f"recurrence-helicity-selector-process-{index}", + process_id=process_name, + progress_callback=( + report_selector if selector_task.sink is not None else None + ), + ) + profile["helicity-selector-physical-lowering"] = ( + time.perf_counter() - companion_lowering_started + ) + selector_task.update( + selector_task.total or selector_task.completed, + message="persisted helicity-selector plan ready", + details={ + "process": process_name, + "step": "persisted helicity-selector plan ready", + }, + ) + phase.advance( + message=process_name, + details={"process": process_name, "step": "recurrence runtime ready"}, + ) + + process_binding_started = time.perf_counter() + validation = self._generation_config.validation + sample_count = ( + validation.samples + if validation.enabled and validation.post_build_validation + else 1 + ) + points = tuple( + build_process_validation_point( + expanded.process_ir, + model, + process_id=process_name, + seed=validation.seed + index * sample_count + sample_index, + ) + for sample_index in range(sample_count) + ) + process_digest = _recurrence_projection_digest( + expanded.process_ir.to_json_dict() + ) + resolved_helicities = process_remap.remap_resolved_helicities( + output.resolved_helicities + ) + try: + amplitude_destinations = tuple( + ( + process_remap.physical_sector_ids[sector_id], + helicity_id, + ) + for sector_id, helicity_id in output.amplitude_destinations + ) + except IndexError as exc: + raise GenerationError( + "shared recurrence schedule references a physical sector outside " + "the concrete process binding" + ) from exc + structural_zero_certificate = output.structural_zero_physical_sector_certificate + if structural_zero_certificate is None: + certified_structural_zero_sector_ids: tuple[int, ...] = () + else: + if structural_zero_certificate.sector_count != len( + process_remap.physical_sector_ids + ): + raise GenerationError( + "shared recurrence structural-zero certificate disagrees with " + "its physical-sector remap" + ) + try: + certified_structural_zero_sector_ids = tuple( + sorted( + process_remap.physical_sector_ids[sector_id] + for sector_id in structural_zero_certificate.sector_ids + ) + ) + except IndexError as exc: + raise GenerationError( + "shared recurrence structural-zero certificate lies outside " + "the concrete process binding" + ) from exc + profile["process-binding-and-validation-points"] = ( + time.perf_counter() - process_binding_started + ) + physics_started = time.perf_counter() + physics = build_recurrence_physics( + expanded.process_ir, + logical, + model_inputs.catalog, + process_id=process_name, + resolved_helicities=resolved_helicities, + normalization=normalization_payload, + selected_color_sector_ids=( + None + if selection.selected_color_sector_ids is None + else tuple(sorted(selection.selected_color_sector_ids)) + ), + color_plan=prepared.complete_color_plan, + ) + profile["physics-metadata"] = time.perf_counter() - physics_started + run = self._run_config + assert run is not None + schedule_summary = output.inspection_summary.get("schedule", {}) + recurrence_summary = { + "lc_flow_layout": layout, + "builder_input_abi": RECURRENCE_BUILDER_INPUT_ABI, + "template_input_abi": RECURRENCE_TEMPLATE_INPUT_ABI, + "current_count": ( + schedule_summary.get("current_count", 0) + if isinstance(schedule_summary, Mapping) + else 0 + ), + "contribution_count": ( + schedule_summary.get("contribution_count", 0) + if isinstance(schedule_summary, Mapping) + else 0 + ), + "closure_term_count": ( + schedule_summary.get("closure_term_count", 0) + if isinstance(schedule_summary, Mapping) + else 0 + ), + } + inspection_summary = _process_scoped_recurrence_inspection_summary( + output.inspection_summary, + process_id=process_name, + semantic_digest=builder_input.digest, + schedule_digest=schedule_digest, + ) + color_contraction_started = time.perf_counter() + color_contraction = build_recurrence_color_contraction( + logical, + prepared.complete_color_plan, + resolved_helicities, + amplitude_destinations, + certified_structural_zero_sector_ids, + contraction=run.color.contraction.value, + ) + color_contraction_payload = None + color_contraction_summary = None + if color_contraction is not None: + sector_count = len(logical.physical_sectors) + component_count = len(resolved_helicities) + group_count = color_contraction.group_count + schedule_destination_count = int( + schedule_summary.get("amplitude_destination_count", 0) + if isinstance(schedule_summary, Mapping) + else 0 + ) + if schedule_destination_count != len(amplitude_destinations): + raise GenerationError( + "contracted recurrence destination metadata does not match " + "its Direct schedule" + ) + contraction_destinations = recurrence_color_contraction_destinations( + logical, + resolved_helicities, + amplitude_destinations, + ) + destination_count = len(contraction_destinations) + if destination_count < group_count: + raise GenerationError( + "contracted recurrence physical replay domain is smaller " + "than its color-contraction group domain" + ) + if any(helicity_id is None for _, helicity_id in amplitude_destinations): + raise GenerationError( + "contracted recurrence exposes a destination without a " + "resolved helicity" + ) + destination_by_group = ( + tuple(range(group_count)) + if color_contraction.destination_by_group is None + else color_contraction.destination_by_group + ) + if ( + len(destination_by_group) != group_count + or len(set(destination_by_group)) != group_count + or any( + destination_id < 0 or destination_id >= destination_count + for destination_id in destination_by_group + ) + ): + raise GenerationError( + "contracted recurrence color plan has an invalid destination " + "projection" + ) + normalized_destinations = tuple( + contraction_destinations[destination_id] + for destination_id in destination_by_group + ) + group_sector_ids = tuple( + sector_id for sector_id, _ in normalized_destinations + ) + group_component_ids = tuple( + cast(int, helicity_id) for _, helicity_id in normalized_destinations + ) + repeated = color_contraction.repeated_block + symmetric_group = color_contraction.symmetric_group_block + ordered_group_ids = ( + repeated.component_group_ids + if repeated is not None + else symmetric_group.component_group_ids + if symmetric_group is not None + else tuple(range(group_count)) + ) + color_contraction_payload = encode_recurrence_color_contraction( + color_contraction, + sector_count=sector_count, + component_count=component_count, + ordered_group_ids=ordered_group_ids, + destination_by_group=destination_by_group, + destination_count=destination_count, + group_sector_ids=group_sector_ids, + group_component_ids=group_component_ids, + sector_owner_ids=recurrence_color_sector_owner_map( + logical, + set(group_sector_ids), + set(certified_structural_zero_sector_ids), + ), + exact_coefficients=recurrence_exact_color_coefficients( + prepared.complete_color_plan, + color_contraction, + group_sector_ids, + ), + ) + color_digest = recurrence_color_contraction_digest( + color_contraction_payload + ) + color_contraction_summary = { + "abi": RECURRENCE_COLOR_CONTRACTION_CODEC_ABI, + "color_accuracy": color_contraction.color_accuracy, + "storage": ( + "convolution-kernels" + if symmetric_group is not None + else "expanded" + if repeated is None + else "repeated" + ), + "includes_color_factor": color_contraction.includes_color_factor, + "group_count": group_count, + "sector_count": sector_count, + "active_sector_count": len(set(group_sector_ids)), + "component_count": component_count, + "destination_count": destination_count, + "materialized_destination_count": schedule_destination_count, + "entry_count": ( + symmetric_group.stored_entry_count + if symmetric_group is not None + else len(color_contraction.entries) + if repeated is None + else len(repeated.entries) + ), + "logical_entry_count": color_contraction.logical_entry_count, + "semantic_digest": color_digest, + "factorization": ( + { + "kind": "symmetric-group-fourier", + "rank": symmetric_group.degree, + "coset_count": symmetric_group.channel_count, + } + if symmetric_group is not None + else None + if repeated is None or repeated.factorized_block is None + else { + "kind": repeated.factorized_block.kind, + "rank": ( + 2 + if repeated.factorized_block.kind == "klein-four-walsh" + else repeated.factorized_block.rank + ), + "coset_count": len(repeated.factorized_block.cosets), + } + ), + **( + {} + if symmetric_group is None + else { + "fft_provenance": { + "method": "symmetric-group-fourier", + "degree": symmetric_group.degree, + "channel_count": symmetric_group.channel_count, + "covered_local_group_count": ( + symmetric_group.channel_count + * math.factorial(symmetric_group.degree) + ), + "residual_group_count": len( + symmetric_group.residual_local_group_indices + ), + "residual_entry_count": len( + symmetric_group.residual_entries + ), + "raw_kernel_bytes": ( + len(symmetric_group.kernel_entries) * 16 + ), + "transformed_kernel_bytes": ( + len(symmetric_group.kernel_entries) * 8 + ), + "capability": ( + SYMMETRIC_GROUP_FFT_COLOR_RUNTIME_CAPABILITY + ), + } + } + ), + } + profile["color-contraction"] = time.perf_counter() - color_contraction_started + relation_discovery_payload: dict[str, object] + numerical_current_reuse = output.numerical_current_reuse_report + if not isinstance(numerical_current_reuse, Mapping): + raise GenerationError( + "recurrence numerical current lowering did not publish its " + "aggregate report" + ) + relation_discovery_payload = dict(numerical_current_reuse) + artifact_record_started = time.perf_counter() + required_direct_executor_ids = _required_process_direct_executor_ids( + output.exact_sections, + process_remap, + ) + process_executor_pack = build_recurrence_process_executor_pack( + direct_catalog=model_inputs.direct_template_catalog, + kernel_pack=model_inputs.bundle.kernel_pack, + required_executor_ids=required_direct_executor_ids, + runtime_layout_digest=str(inspection_summary["runtime_layout_digest"]), + ) + umami_keys = None + umami_physics = None + umami_sectors = () + if run.generation.emit_api_bundle: + umami_sectors = tuple( + sector.to_json_dict() for sector in prepared.complete_color_plan.sectors + ) + umami_physics = dict(physics) + from .umami_semantics import recurrence_structural_keys + + umami_keys = recurrence_structural_keys( + exact_sections=output.exact_sections, + runtime_metadata=runtime_metadata, + kernel_pack=model_inputs.bundle.kernel_pack, + ir=model_inputs.bundle.compiled_model["ir"], + physics=physics, + remap=process_remap, + color_contraction_payload=color_contraction_payload, + ) + replay = ( + prepared.topology_replay + if isinstance(prepared.topology_replay, LCColorTopologyReplayPlan) + else build_lc_topology_replay_plan(prepared.complete_color_plan, model) + ) + if replay is not None: + umami_physics["extensions"] = { + **cast(Mapping[str, object], physics.get("extensions", {})), + "lc_topology_replay": replay.to_json_dict(), + } + artifact = RecurrenceProcessArtifact( + process_id=process_name, + expression=expanded.process_ir.process, + color_accuracy=expanded.process_ir.color_accuracy, + external_pdgs=( + *expanded.process_ir.initial_pdgs, + *expanded.process_ir.final_pdgs, + ), + aliases=expanded.aliases, + physics=physics, + umami_physics=umami_physics, + umami_color_sectors=umami_sectors, + umami_structural_keys=umami_keys, + recurrence_schedule_path=output.payload_path, + recurrence_schedule_digest=schedule_digest, + recurrence_native_schedule_semantic_digest=( + native_schedule_semantic_digest + ), + recurrence_schedule_size_bytes=output.payload_size_bytes, + recurrence_schedule_sha256=output.payload_sha256, + recurrence_schedule_member_count=output.member_count, + recurrence_schedule_unpacked_size_bytes=output.unpacked_size_bytes, + recurrence_schedule_index_sha256=output.index_sha256, + builder_input_sha256=builder_input.digest, + prepared_kernel_pack_digest=model_inputs.prepared_kernel_pack_digest, + direct_template_catalog_digest=( + model_inputs.direct_template_catalog.catalog_digest + ), + referenced_kernel_ids=( + recurrence_referenced_kernel_ids(logical) + if companion_artifact is None + else frozenset( + kernel.kernel_id + for kernel in model_inputs.bundle.kernel_pack.kernels + ) + ), + inspection_summary=inspection_summary, + runtime_metadata=runtime_metadata, + color_contraction_payload=color_contraction_payload, + color_contraction_summary=color_contraction_summary, + point_tile_size=run.evaluator.recurrence.point_tile_size, + workspace_mib=run.evaluator.recurrence.workspace_mib, + recurrence_summary=recurrence_summary, + validation_point=points[0], + generation_filters={ + **self._fft_basis_metadata(prepared.complete_color_plan), + "lc_flow_layout": layout, + "recurrence": recurrence_summary, + "relation_discovery": relation_discovery_payload, + }, + generation_profile=output.generation_profile, + process_support_mask=1 << index, + recurrence_process_remap=process_remap, + recurrence_process_executor_pack=process_executor_pack, + helicity_selector_companion=companion_artifact, + process_digest=process_digest, + ) + profile["artifact-record-assembly"] = ( + time.perf_counter() - artifact_record_started + ) + profile["process-total"] = time.perf_counter() - profile_started + if generation_timings is not None: + timing_prefix = f"recurrence-process-{index}:{process_name}:" + with _RECURRENCE_PROFILE_LOCK: + generation_timings.update( + { + f"{timing_prefix}{name}": seconds + for name, seconds in profile.items() + } + ) + _LOGGER.debug( + "recurrence generation profile for %s: %s", + process_name, + json.dumps(profile, allow_nan=False, sort_keys=True), + ) + return _GeneratedRecurrenceProcess( + expanded=expanded, + artifact=artifact, + validation_points=points, + ) + + def _construct_evaluator( + self, + process: _CompiledProcess, + model: Model, + phase: PhaseHandle, + ) -> _EvaluatorProcess: + process_name = process.expanded.request.name + run = self._run_config + compiled_symmetric_group = ( + run is not None and run.color.contraction.value == "symmetric-group-fft" + ) + with phase.child( + process_name, + f"{process_name}: runtime schema", + details={"process": process_name, "step": "runtime layout"}, + ) as task: + color_contraction_payload = None + if compiled_symmetric_group: + _validate_compiled_symmetric_group_diagnostic_process(process) + schema_layout = build_runtime_schema_layout( + process.dag, + model, + process_id=process_name, + color_contraction="symmetric-group-fft", + ) + schema = RuntimeExpressionSchema.from_mapping( + schema_layout.runtime_schema + ) + color_contraction_payload = ( + _compiled_symmetric_group_color_contraction_payload( + process.dag, + schema_layout, + ) + ) + else: + schema = build_runtime_expression_schema( + process.dag, + model, + process_id=process_name, + ) + helicity_sum_schema = ( + None + if process.helicity_sum_dag is None + else build_runtime_expression_schema( + process.helicity_sum_dag, + model, + process_id=process_name, + ) + ) + helicity_selector_lanes: tuple[_HelicitySelectorLane, ...] = () + if ( + process.dag.helicity_coverage == "complete" + and not process.dag.selected_source_helicities + and process.dag.helicity_recurrence is not None + and process.dag.helicity_materialization is not None + ): + union_dag = process.helicity_selector_union_dag + if union_dag is not None: + union_schema = build_runtime_expression_schema( + union_dag, + model, + process_id=process_name, + ) + materialization = process.dag.helicity_materialization + assert materialization is not None + helicity_selector_lanes = ( + _HelicitySelectorLane( + selector_domain_ids=tuple( + sorted( + schedule.selector_domain_id + for schedule in materialization.selector_schedules + if not schedule.structural_zero + ) + ), + active_current_ids=(), + active_root_ids=(), + dag=union_dag, + runtime_schema=union_schema, + stage_input=StageCompilationInput( + union_dag, + model, + union_schema, + ), + schedule_mode="nested-runtime", + child_lanes=(), + ), + ) + elif process.dag.process.color_accuracy == "lc": + helicity_selector_lanes = _compiled_helicity_selector_closure_lanes( + process.dag, + schema, + model, + ) + color_selector_dag = ( + process.dag + if process.helicity_sum_dag is None + else process.helicity_sum_dag + ) + color_selector_lanes: list[_ColorSelectorLane] = [] + sector_lanes = ( + () + if self._all_flow_union_enabled + else _compiled_lc_color_selector_lane_dags(color_selector_dag) + ) + for sector_id, lane_dag in sector_lanes: + lane_schema = build_runtime_expression_schema( + lane_dag, + model, + process_id=process_name, + ) + color_selector_lanes.append( + _ColorSelectorLane( + materialized_sector_id=sector_id, + dag=lane_dag, + runtime_schema=lane_schema, + stage_input=StageCompilationInput( + lane_dag, + model, + lane_schema, + ), + ) + ) + stage_count = _runtime_stage_count(schema) + ( + 0 + if helicity_sum_schema is None + else _runtime_stage_count(helicity_sum_schema) + ) + stage_count += sum( + _helicity_selector_lane_stage_count(lane) + for lane in helicity_selector_lanes + ) + stage_count += sum( + _runtime_stage_count(lane.runtime_schema) + for lane in color_selector_lanes + ) + task.update( + stage_count, + total=stage_count, + message="runtime schema ready", + details={ + "process": process_name, + "step": "runtime schema ready", + "stage_index": stage_count, + "stage_total": stage_count, + "current_count": len(process.dag.currents), + "interaction_count": len(process.dag.interactions), + "output_count": len(process.dag.amplitude_roots), + "helicity_sum_lane": helicity_sum_schema is not None, + "helicity_selector_closure_lane_count": len( + helicity_selector_lanes + ), + "color_selector_lane_count": len(color_selector_lanes), + }, + ) + phase.advance( + message=process_name, + details={"process": process_name, "step": "runtime schema ready"}, + ) + return _EvaluatorProcess( + compiled=process, + runtime_schema=schema, + stage_input=StageCompilationInput(process.dag, model, schema), + color_contraction_payload=color_contraction_payload, + helicity_sum_runtime_schema=helicity_sum_schema, + helicity_sum_stage_input=( + None + if helicity_sum_schema is None or process.helicity_sum_dag is None + else StageCompilationInput( + process.helicity_sum_dag, + model, + helicity_sum_schema, + ) + ), + helicity_selector_lanes=helicity_selector_lanes, + color_selector_lanes=tuple(color_selector_lanes), + ) + + def _construct_eager_artifact( + self, + process: _CompiledProcess, + model: Model, + resolved_model: _ResolvedModel, + temporary_root: Path, + phase: PhaseHandle, + ) -> EagerPlanV3ProcessArtifact: + eager_kernel_index = resolved_model.eager_kernel_index + if eager_kernel_index is None: + raise GenerationError( + "eager DAG lowering requires an indexed prepared model bundle" + ) + resolver = PreparedCatalogEagerKernelResolver( + process.dag, + eager_kernel_index, + ) + return self._construct_eager_plan_v3_artifact( + process, + model, + resolver, + temporary_root, + phase, + ) + + def _construct_eager_plan_v3_artifact( + self, + process: _CompiledProcess, + model: Model, + resolver: PreparedCatalogEagerKernelResolver, + temporary_root: Path, + phase: PhaseHandle, + ) -> EagerPlanV3ProcessArtifact: + process_name = process.expanded.request.name + with phase.child( + process_name, + f"{process_name}: Rust eager runtime lowering", + total=2, + details={"process": process_name, "step": "columnar lowering input"}, + ) as task: + lowering_input = build_eager_lowering_input_v1( + dag=process.dag, + model=model, + resolver=resolver, + process_id=process_name, + ) + task.update( + 1, + message="columnar lowering input ready", + details={ + "process": process_name, + "step": "columnar lowering input ready", + }, + ) + output = _invoke_rust_eager_lowering_v1( + lowering_input, + temporary_root + / "eager-plan-v3" + / process_name + / "eager-runtime.pacbin", + ) + task.update( + 2, + message="Rust eager runtime ready", + details={"process": process_name, "step": "eager runtime ready"}, + ) + phase.advance( + message=process_name, + details={"process": process_name, "step": "eager runtime ready"}, + ) + run = self._run_config + if run is None: # pragma: no cover - eager mode requires RunConfig + raise GenerationError("eager generation has no evaluator configuration") + ir = process.expanded.process_ir + dag = process.dag + physics = build_resolved_physics_from_dag( + dag, + model, + process_id=process_name, + native_eager_plan_v3_reduction_groups=True, + runtime_model_parameters=( + runtime_model_parameter_records_from_eager_input(lowering_input) + ), + ) + return EagerPlanV3ProcessArtifact( + process_id=process_name, + expression=ir.process, + color_accuracy=ir.color_accuracy, + external_pdgs=(*ir.initial_pdgs, *ir.final_pdgs), + aliases=process.expanded.aliases, + physics=physics, + umami_color_sectors=tuple( + sector.to_json_dict() for sector in dag.color_plan.sectors + ) + if run.generation.emit_api_bundle + else (), + eager_runtime_path=output.payload_path, + eager_runtime_size_bytes=output.payload_size_bytes, + eager_runtime_sha256=output.payload_sha256, + eager_runtime_member_count=output.member_count, + eager_runtime_unpacked_size_bytes=output.unpacked_size_bytes, + eager_runtime_index_sha256=output.index_sha256, + lowering_input_sha256=lowering_input.digest, + referenced_kernel_ids=_eager_lowering_kernel_ids(lowering_input), + inspection_summary=output.inspection_summary, + point_tile_size=run.evaluator.eager.point_tile_size, + workspace_mib=run.evaluator.eager.workspace_mib, + dag_summary={ + "current_count": len(dag.currents), + "source_count": len(dag.sources), + "interaction_count": len(dag.interactions), + "interaction_evaluation_count": dag.interaction_evaluation_count, + "amplitude_root_count": len(dag.amplitude_roots), + "truncated": False, + }, + validation_point=process.validation_points[0], + generation_filters=process.filters, + ) + + def _materialize_evaluator( + self, + process: _EvaluatorProcess, + model: Model, + temporary_root: Path, + phase: PhaseHandle, + ) -> CompiledProcessArtifact: + process_id = process.compiled.expanded.request.name + run = self._run_config + backend = "JIT" if run is None else str(run.evaluator.backend).upper() + with phase.child( + process_id, + f"{process_id}: evaluator compilation", + details={ + "process": process_id, + "step": "waiting for compiler", + "backend": backend, + }, + ) as task: + task.update( + 0, + message="waiting for compiler", + details={ + "process": process_id, + "step": "waiting for compiler", + "backend": backend, + }, + ) + with _SYMBOLICA_MATERIALIZATION_LOCK: + return self._materialize_evaluator_unlocked( + process, + model, + temporary_root, + phase, + task, + backend=backend, + ) + + def _materialize_evaluator_unlocked( + self, + process: _EvaluatorProcess, + model: Model, + temporary_root: Path, + phase: PhaseHandle, + progress: PhaseHandle, + *, + backend: str, + ) -> CompiledProcessArtifact: + process_id = process.compiled.expanded.request.name + evaluator_root = temporary_root / process_id + total_stage_count = _runtime_stage_count(process.runtime_schema) + 1 + if process.helicity_sum_runtime_schema is not None: + total_stage_count += ( + _runtime_stage_count(process.helicity_sum_runtime_schema) + 1 + ) + total_stage_count += sum( + _helicity_selector_lane_compile_task_count(lane) + for lane in process.helicity_selector_lanes + ) + total_stage_count += sum( + _runtime_stage_count(lane.runtime_schema) + 1 + for lane in process.color_selector_lanes + ) + + def evaluator_progress( + event: dict[str, object], + *, + execution_lane: str, + ) -> None: + details = { + str(key): value + for key, value in event.items() + if isinstance(value, (str, int, float, bool, type(None))) + } + details.update( + { + "process": process_id, + "backend": backend, + "execution_lane": execution_lane, + "lane_stage_total": event.get("total"), + } + ) + if event.get("stage") == "stage complete": + completed = progress.completed + int(event.get("increment", 1)) + progress.update( + completed, + total=total_stage_count, + message=str(event.get("item", "stage complete")), + details=details, + ) + phase.advance( + message=str(event.get("item", process_id)), + details=details, + ) + return + progress.update( + progress.completed, + total=total_stage_count, + message=str(event.get("item", event.get("stage", "compiling"))), + details=details, + ) + + def compile_lane( + stage_input: StageCompilationInput, + runtime_schema: RuntimeExpressionSchema, + output_root: Path, + *, + execution_lane: str, + ) -> tuple[Mapping[str, object], Mapping[str, object] | None]: + callback = ( + None + if progress.sink is None + else lambda event: evaluator_progress( + event, + execution_lane=execution_lane, + ) + ) + _blueprint, stage_manifest = ( + build_and_write_generic_stage_evaluator_artifacts( + stage_input, + runtime_schema.to_mapping(), + output_root, + model=model, + stage_local_parameter_layout=True, + symbolica_settings=self._symbolica_settings(), + jit_compile=True, + evaluator_progress_callback=callback, + ) + ) + if not bool(stage_manifest.get("runtime_available")): + raise GenerationError( + f"{execution_lane} stage evaluator for {process_id!r} " + "is not runtime-available" + ) + model_parameter_evaluator = write_model_parameter_evaluator_artifact( + model, + runtime_schema.to_mapping(), + output_root, + symbolica_settings=self._symbolica_settings(), + jit_compile=True, + progress_callback=callback, + ) + return stage_manifest, model_parameter_evaluator + + stage_manifest, model_parameter_evaluator = compile_lane( + process.stage_input, + process.runtime_schema, + evaluator_root, + execution_lane="selected-helicity", + ) + helicity_sum_execution: CompiledExecutionArtifact | None = None + if ( + process.helicity_sum_runtime_schema is not None + or process.helicity_sum_stage_input is not None + ): + if ( + process.helicity_sum_runtime_schema is None + or process.helicity_sum_stage_input is None + or process.compiled.helicity_sum_dag is None + ): + raise GenerationError( + f"helicity-sum execution inputs for {process_id!r} are inconsistent" + ) + helicity_sum_root = temporary_root / ".helicity-sum" / process_id + ( + helicity_sum_stage_manifest, + helicity_sum_model_parameter_evaluator, + ) = compile_lane( + process.helicity_sum_stage_input, + process.helicity_sum_runtime_schema, + helicity_sum_root, + execution_lane="helicity-sum", + ) + helicity_sum_dag = process.compiled.helicity_sum_dag + helicity_sum_execution = CompiledExecutionArtifact( + runtime_schema=process.helicity_sum_runtime_schema, + stage_manifest=helicity_sum_stage_manifest, + model_parameter_evaluator=(helicity_sum_model_parameter_evaluator), + dag_summary={ + "current_count": len(helicity_sum_dag.currents), + "source_count": len(helicity_sum_dag.sources), + "interaction_count": len(helicity_sum_dag.interactions), + "interaction_evaluation_count": ( + helicity_sum_dag.interaction_evaluation_count + ), + "amplitude_root_count": len(helicity_sum_dag.amplitude_roots), + "truncated": False, + }, + evaluator_root=helicity_sum_root, + ) + helicity_selector_executions: list[ + CompiledHelicitySelectorExecutionArtifact + ] = [] + + def compile_helicity_selector_lane( + lane: _HelicitySelectorLane, + lane_root: Path, + *, + execution_lane: str, + ) -> CompiledHelicitySelectorExecutionArtifact: + lane_stage_manifest, lane_model_parameter_evaluator = compile_lane( + lane.stage_input, + lane.runtime_schema, + lane_root, + execution_lane=execution_lane, + ) + child_executions = tuple( + compile_helicity_selector_lane( + child, + lane_root.parent / f"{lane_root.name}-closure-{child_index}", + execution_lane=(f"{execution_lane}-closure-{child_index}"), + ) + for child_index, child in enumerate(lane.child_lanes) + ) + return CompiledHelicitySelectorExecutionArtifact( + selector_domain_ids=lane.selector_domain_ids, + schedule_mode=lane.schedule_mode, + execution=CompiledExecutionArtifact( + runtime_schema=lane.runtime_schema, + stage_manifest=lane_stage_manifest, + model_parameter_evaluator=(lane_model_parameter_evaluator), + # Closure lanes retain their parent's stable current and + # amplitude storage ABI while filtering stage interactions. + dag_summary=_runtime_schema_dag_summary(lane.runtime_schema), + evaluator_root=lane_root, + helicity_selector_executions=child_executions, + ), + ) + + for lane_index, lane in enumerate(process.helicity_selector_lanes): + helicity_selector_root = ( + temporary_root + / ".helicity-selector-union" + / process_id + / f"class-{lane_index}" + ) + helicity_selector_executions.append( + compile_helicity_selector_lane( + lane, + helicity_selector_root, + execution_lane=f"helicity-selector-class-{lane_index}", + ) + ) + color_selector_executions: list[CompiledColorSelectorExecutionArtifact] = [] + selector_root_name = ( + ".helicity-sum-color-selector" + if helicity_sum_execution is not None + else ".color-selector" + ) + for lane in process.color_selector_lanes: + lane_root = ( + temporary_root + / selector_root_name + / process_id + / f"sector-{lane.materialized_sector_id}" + ) + lane_stage_manifest, lane_model_parameter_evaluator = compile_lane( + lane.stage_input, + lane.runtime_schema, + lane_root, + execution_lane=(f"color-selector sector-{lane.materialized_sector_id}"), + ) + color_selector_executions.append( + CompiledColorSelectorExecutionArtifact( + materialized_sector_id=lane.materialized_sector_id, + execution=CompiledExecutionArtifact( + runtime_schema=lane.runtime_schema, + stage_manifest=lane_stage_manifest, + model_parameter_evaluator=(lane_model_parameter_evaluator), + dag_summary={ + "current_count": len(lane.dag.currents), + "source_count": len(lane.dag.sources), + "interaction_count": len(lane.dag.interactions), + "interaction_evaluation_count": ( + lane.dag.interaction_evaluation_count + ), + "amplitude_root_count": len(lane.dag.amplitude_roots), + "truncated": False, + }, + evaluator_root=lane_root, + ), + ) + ) + primary_color_selector_executions: tuple[ + CompiledColorSelectorExecutionArtifact, ... + ] = tuple(color_selector_executions) + if helicity_sum_execution is not None: + helicity_sum_execution = replace( + helicity_sum_execution, + color_selector_executions=tuple(color_selector_executions), + ) + primary_color_selector_executions = () + progress.update( + progress.completed, + message="model parameters ready", + details={ + "process": process_id, + "step": "model parameters ready", + "backend": backend, + }, + ) + phase.advance( + message=f"{process_id}: model parameters", + details={ + "process": process_id, + "step": "model parameters ready", + "backend": backend, + }, + ) + ir = process.compiled.expanded.process_ir + dag = process.compiled.dag + return CompiledProcessArtifact( + process_id=process_id, + expression=ir.process, + color_accuracy=ir.color_accuracy, + external_pdgs=(*ir.initial_pdgs, *ir.final_pdgs), + aliases=process.compiled.expanded.aliases, + runtime_schema=process.runtime_schema, + umami_color_sectors=tuple( + sector.to_json_dict() for sector in dag.color_plan.sectors + ) + if self._generation_config.emit_api_bundle + else (), + stage_manifest=stage_manifest, + model_parameter_evaluator=model_parameter_evaluator, + dag_summary={ + "current_count": len(dag.currents), + "source_count": len(dag.sources), + "interaction_count": len(dag.interactions), + "interaction_evaluation_count": dag.interaction_evaluation_count, + "amplitude_root_count": len(dag.amplitude_roots), + "truncated": False, + }, + evaluator_root=evaluator_root, + color_contraction_payload=process.color_contraction_payload, + validation_point=process.compiled.validation_points[0], + generation_filters=process.compiled.filters, + helicity_sum_execution=helicity_sum_execution, + helicity_selector_executions=tuple(helicity_selector_executions), + color_selector_executions=primary_color_selector_executions, + ) + + def _process_worker_count(self, process_count: int) -> int: + if process_count < 1: + return 1 + configured = self._generation_config.workers + requested = ( + max(1, os.cpu_count() or 1) + if configured == "auto" + else max(1, int(configured)) + ) + return min(process_count, requested) + + def _detect_symbolica_license(self) -> SymbolicaLicenseState: + from ..licensing import detect_symbolica_license + + run = self._run_config + return detect_symbolica_license( + suggest=True if run is None else run.symbolica.suggest_license, + json_mode=False if run is None else str(run.output.format) == "json", + ) + + def _apply_symbolica_resource_policy( + self, + state: SymbolicaLicenseState, + *, + process_count: int | None = None, + ) -> None: + from ..licensing import resolve_symbolica_resource_config + + resolution = resolve_symbolica_resource_config( + self._resource_config, + state, + process_count=process_count, + ) + self._resource_config = resolution + self._configuration = _GenerationConfigProvenance.from_config(resolution) + self._config = resolution.effective + + def _symbolica_settings(self) -> object: + from ..evaluators.symbolica import SymbolicaEvaluatorSettings + + run = self._run_config + if run is None: + return SymbolicaEvaluatorSettings(jit_compress=True) + optimization = run.evaluator.optimization + if optimization.horner_iterations < 1: + raise GenerationError( + "evaluator.optimization.horner_iterations must be positive " + "for generation" + ) + backend = str(run.evaluator.backend) + evaluator_backend = "jit" if backend == "jit" else "compiled-complex" + cores = ( + max(1, os.cpu_count() or 1) + if optimization.cores == "auto" + else int(optimization.cores) + ) + collect_factors = ( + False + if optimization.collect_factors == "auto" + else bool(optimization.collect_factors) + ) + cpp_level = _cpp_optimization_level(run.evaluator.cpp.optimization) + return SymbolicaEvaluatorSettings( + backend=evaluator_backend, + iterations=optimization.horner_iterations, + cpe_iterations=optimization.cpe_iterations, + n_cores=cores, + jit_direct_translation=False, + jit_optimization_level=run.evaluator.jit.optimization_level, + jit_compress=run.evaluator.resolved_jit_compress, + max_horner_scheme_variables=optimization.max_horner_variables, + max_common_pair_cache_entries=(optimization.max_common_pair_cache_entries), + max_common_pair_distance=optimization.max_common_pair_distance, + collect_factors=collect_factors, + compiled_inline_asm="default" if backend == "asm" else "none", + compiled_optimization_level=cpp_level, + compiled_native=run.evaluator.cpp.native_arch, + compiler_path=run.evaluator.cpp.compiler, + compiler_flags=run.evaluator.cpp.extra_flags, + compiled_output_chunk_size=run.evaluator.output_chunk_size, + output_chunk_strategy="uniform", + output_chunk_autotune_batch_size=run.evaluator.batch_size, + compiled_chunk_compile_workers=1, + ) + + def _validate_generated_artifact( + self, + output: Path, + process_ids: Sequence[str], + *, + validation_points: Mapping[str, Sequence[ValidationPointRecord]], + expected_api_bundle_path: str | None, + compact_validation_selectors: Mapping[str, Mapping[str, tuple[str, ...]]] + | None = None, + progress: PhaseHandle | None = None, + ) -> None: + import cmath + + from pyamplicol.api import Runtime + from pyamplicol.artifacts import load_manifest + + manifest = load_manifest(output) + expected_ids = set(process_ids) + actual_ids = {str(process["id"]) for process in manifest.processes} + if not expected_ids.issubset(actual_ids): + raise GenerationError("generated artifact omitted concrete processes") + by_path = {record.path: record for record in manifest.payloads} + process_record_by_id = { + str(process["id"]): process for process in manifest.processes + } + process_total = len(process_ids) + for process_index, process_id in enumerate(process_ids, start=1): + if progress is not None: + progress.update( + process_index - 1, + total=process_total, + message=f"loading {process_id}", + details={ + "process": process_id, + "step": "runtime validation", + "sample_index": 0, + }, + ) + prefix = f"processes/{process_id}" + required = { + f"{prefix}/physics.json", + f"{prefix}/execution.json", + f"{prefix}/validation-momenta.json", + } + if self._on_the_fly_execution_enabled: + required.add(f"{prefix}/on-the-fly-runtime.pacbin") + if process_record_by_id[process_id].get("color_accuracy") in { + "nlc", + "full", + }: + required.add(f"{prefix}/on-the-fly-color.bin") + if not required.issubset(by_path): + raise GenerationError( + f"artifact payload set is incomplete for {process_id!r}" + ) + runtime = Runtime.load(output, process=process_id) + validation = self._generation_config.validation + samples = tuple( + point.four_vectors + for point in validation_points.get(process_id, ()) + if point.available + ) + if self._on_the_fly_execution_enabled: + if runtime.artifact_id != manifest.artifact_id: + raise GenerationError( + "Rusticol loaded the wrong artifact while validating " + f"{process_id!r}" + ) + if runtime.execution_mode != "on-the-fly": + raise GenerationError( + f"Rusticol selected the wrong execution mode for {process_id!r}" + ) + if runtime.representative_process_key != process_id: + raise GenerationError( + f"Rusticol selected the wrong process for {process_id!r}" + ) + validated_samples = 0 + if validation.enabled and samples: + raw_selectors = ( + None + if compact_validation_selectors is None + else compact_validation_selectors.get(process_id) + ) + if not isinstance(raw_selectors, Mapping) or set(raw_selectors) != { + "helicities", + "color_flows", + }: + raise GenerationError( + "on-the-fly validation has no authenticated compact " + f"selector for {process_id!r}" + ) + helicities = raw_selectors.get("helicities") + color_flows = raw_selectors.get("color_flows") + contracted_color = process_record_by_id[process_id].get( + "color_accuracy" + ) in {"nlc", "full"} + if ( + not isinstance(helicities, tuple) + or len(helicities) != 1 + or not isinstance(helicities[0], str) + or not helicities[0].startswith("h:") + or not isinstance(color_flows, tuple) + or color_flows != (() if contracted_color else ("1",)) + ): + raise GenerationError( + "on-the-fly validation compact selector is malformed " + f"for {process_id!r}" + ) + selector_label = ( + "compact helicity+contracted-color" + if contracted_color + else "compact helicity+flow" + ) + selectors = {"helicities": helicities} + if color_flows: + selectors["color_flows"] = color_flows + selected_samples = samples[:1] + total = runtime.evaluate(selected_samples, **selectors) + resolved_total = runtime.evaluate_resolved( + selected_samples, **selectors + ).total() + if len(total) != 1 or len(resolved_total) != 1: + raise GenerationError( + f"Rusticol returned an invalid validation shape for " + f"{process_id!r} ({selector_label})" + ) + summed = total[0] + resolved = resolved_total[0] + if progress is not None: + progress.update( + process_index - 1, + total=process_total, + message=f"{process_id}: {selector_label}, sample 1", + details={ + "process": process_id, + "step": "numerical validation", + "selector_case": selector_label, + "validation_slice": selector_label, + "sample_index": 1, + "sample_total": 1, + }, + ) + difference = abs(complex(summed) - complex(resolved)) + if not cmath.isclose( + complex(summed), + complex(resolved), + rel_tol=validation.relative_tolerance, + abs_tol=validation.absolute_tolerance, + ): + raise GenerationError( + "resolved Rusticol validation does not reduce to " + f"the total for {process_id!r} ({selector_label}) " + "sample 1 " + f"(absolute difference {difference:.3e})" + ) + validated_samples = 1 + if progress is not None: + progress.update( + process_index, + total=process_total, + message=process_id, + details={ + "process": process_id, + "step": "validated", + "samples": validated_samples, + }, + ) + continue + if runtime.physics.process_id != process_id: + raise GenerationError( + f"Rusticol selected the wrong process for {process_id!r}" + ) + if validation.enabled and samples: + selector_cases = ( + _recurrence_validation_selector_cases(runtime.physics) + if self._recurrence_execution_enabled + else tuple( + ( + validation_slice, + { + **({"helicities": helicities} if helicities else {}), + **({"color_flows": color_flows} if color_flows else {}), + }, + ) + for validation_slice, helicities, color_flows in ( + _post_build_validation_slices( + runtime.physics, + len(samples), + ) + ) + ) + ) + if not selector_cases: + raise GenerationError( + f"generated recurrence exposes no selectable components " + f"for {process_id!r}" + ) + for selector_label, selectors in selector_cases: + total = runtime.evaluate(samples, **selectors) + resolved_total = runtime.evaluate_resolved( + samples, **selectors + ).total() + if len(total) != len(samples) or len(resolved_total) != len( + samples + ): + raise GenerationError( + f"Rusticol returned an invalid validation shape for " + f"{process_id!r} ({selector_label})" + ) + for sample_index, (summed, resolved) in enumerate( + zip(total, resolved_total, strict=True), + start=1, + ): + if progress is not None: + progress.update( + process_index - 1, + total=process_total, + message=( + f"{process_id}: {selector_label}, " + f"sample {sample_index}" + ), + details={ + "process": process_id, + "step": "numerical validation", + "selector_case": selector_label, + "validation_slice": selector_label, + "sample_index": sample_index, + "sample_total": len(samples), + }, + ) + difference = abs(complex(summed) - complex(resolved)) + if not cmath.isclose( + complex(summed), + complex(resolved), + rel_tol=validation.relative_tolerance, + abs_tol=validation.absolute_tolerance, + ): + raise GenerationError( + "resolved Rusticol validation does not reduce to " + f"the total for {process_id!r} ({selector_label}) " + f"sample {sample_index} " + f"(absolute difference {difference:.3e})" + ) + if progress is not None: + progress.update( + process_index, + total=process_total, + message=process_id, + details={ + "process": process_id, + "step": "validated", + "samples": len(samples), + }, + ) + actual_api_bundle_path = manifest.runtime.get("api_bundle_path") + if actual_api_bundle_path != expected_api_bundle_path: + raise GenerationError("artifact API bundle outcome is inconsistent") + + @property + def _run_config(self) -> RunConfig | None: + return self._config if isinstance(self._config, RunConfig) else None + + @property + def _eager_execution_enabled(self) -> bool: + run = self._run_config + return run is not None and str(run.evaluator.execution_mode) == "eager" + + @property + def _recurrence_execution_enabled(self) -> bool: + run = self._run_config + return run is not None and str(run.evaluator.execution_mode) == "recurrence" + + @property + def _on_the_fly_execution_enabled(self) -> bool: + run = self._run_config + return run is not None and str(run.evaluator.execution_mode) == "on-the-fly" + + @property + def _prepared_execution_enabled(self) -> bool: + return ( + self._eager_execution_enabled + or self._recurrence_execution_enabled + or self._on_the_fly_execution_enabled + ) + + def _validate_recurrence_request(self) -> None: + """Keep recurrence-family requests out of compiled/eager lanes.""" + + if self._on_the_fly_execution_enabled and self._color_accuracy not in { + "lc", + "nlc", + "full", + }: + raise GenerationError( + "on-the-fly execution supports LC, NLC, and full color; " + f"got color.accuracy={self._color_accuracy!r}" + ) + if self._recurrence_execution_enabled and self._color_accuracy not in { + "lc", + "nlc", + "full", + }: + raise GenerationError( + "recurrence execution supports LC, NLC, and full color; " + f"got color.accuracy={self._color_accuracy!r}" + ) + + @property + def _all_flow_union_enabled(self) -> bool: + run = self._run_config + if run is None: + return False + return str(getattr(run.color, "lc_flow_layout", "topology-replay")) == ( + "all-flow-union" + ) + + @property + def _generation_config(self) -> GenerationConfig: + config = self._config + return config.generation if isinstance(config, RunConfig) else config + + @property + def _builtin_process_options(self): + from ..models.builtin.process_types import BuiltinProcessOptions + + run = self._run_config + if run is None: + return BuiltinProcessOptions() + max_lines = run.process.max_quark_lines + return BuiltinProcessOptions( + flavour_scheme=run.process.flavor_scheme, + include_3qqbar=max_lines is None or max_lines >= 3, + ) + + @property + def _max_quark_pairs(self) -> int | None: + run = self._run_config + return None if run is None else run.process.max_quark_lines + + @property + def _color_accuracy(self) -> str: + run = self._run_config + return "lc" if run is None else run.color.accuracy + + @property + def _coupling_order_limits(self) -> dict[str, int]: + run = self._run_config + if run is None: + return {} + return { + str(name).upper(): int(value) + for name, value in run.process.max_coupling_orders.items() + } + + @property + def _flavor_blind(self) -> bool: + run = self._run_config + return run is not None and run.process.flavor_blind + + @property + def _consider_charge_conjugation(self) -> bool: + run = self._run_config + return run is None or run.process.consider_charge_conjugation + + @property + def _process_selection(self) -> _ProcessSelection: + if self._process_selection_override is not None: + return self._process_selection_override + run = self._run_config + if run is None: + return _ProcessSelection() + process = run.process + return _ProcessSelection( + max_color_sectors=process.max_color_sectors, + reference_color_order=( + tuple(int(label) for label in process.reference_color_order) + if process.reference_color_order + else None + ), + selected_color_sector_ids=( + frozenset( + int(sector_id) for sector_id in process.selected_color_sector_ids + ) + if process.selected_color_sector_ids + else None + ), + selected_source_helicities=( + { + int(label): int(helicity) + for label, helicity in process.selected_source_helicities.items() + } + if process.selected_source_helicities + else None + ), + ) + + def _configured_model_source(self) -> ModelSource: + run = self._run_config + if run is None: + return ModelSource.built_in_sm() + return ModelSource.from_config(run.model) + + def _resolve_model_for_plan( + self, + source: ModelSource | CompiledModel, + ) -> _ResolvedModel: + if isinstance(source, CompiledModel): + compiled = self._private_compiled_model(source) + is_builtin = self._is_builtin_compiled_model(compiled) + return _ResolvedModel( + self._source_for_compiled_model(compiled), + _builtin_sm_model() if is_builtin else None, + compiled, + use_compiled_process_catalog=not is_builtin, + ) + if source.kind in {"built-in-sm", "built-in-sm-heft"}: + if source.kind == "built-in-sm" and not self._prepared_execution_enabled: + return _ResolvedModel(source, _builtin_sm_model()) + if self._prepared_execution_enabled: + source = self._packaged_builtin_prepared_source(source.kind) + source_input: str | Path + if source.kind == "built-in-sm-heft": + source_input = "built-in-sm-heft" + elif source.path is not None: + source_input = source.path + else: + raise ModelError("external model source has no path") + + from .._internal.sm_heft import load_cached_sm_heft_source + from ..models.loading import load_cached_model_source, load_compiled_model + + try: + if source.kind in {"compiled", "prepared"}: + compiled = load_compiled_model(source.path) + else: + run = self._run_config + use_cache = True if run is None else run.model.cache + if not use_cache: + raise ModelError( + "dry-run does not compile external model sources and model " + "caching is disabled; compile the ModelSource explicitly and " + "pass its CompiledModel" + ) + cache_dir = None if run is None else run.model.cache_dir + compiled = ( + load_cached_sm_heft_source(cache_dir=cache_dir) + if source.kind == "built-in-sm-heft" + else load_cached_model_source( + source_input, + restriction=( + "default" + if source.restriction is None + else str(source.restriction) + ), + simplify=source.simplify, + cache_dir=cache_dir, + ) + ) + if compiled is None: + raise ModelError( + "dry-run does not compile external model sources; call " + "ModelSource.compile() first and pass the returned " + "CompiledModel, or populate the configured model cache" + ) + except ModelError: + raise + except (OSError, RuntimeError, TypeError, ValueError) as exc: + raise ModelError(str(exc)) from exc + is_builtin = self._is_builtin_compiled_model(compiled) + return _ResolvedModel( + source, + _builtin_sm_model() if is_builtin else None, + compiled, + use_compiled_process_catalog=not is_builtin, + ) + + @staticmethod + def _private_compiled_model(compiled: CompiledModel) -> _CompiledModelPayload: + payload = _compiled_model_payload(compiled) + if not isinstance(payload, _CompiledModelPayload): + raise ModelError("compiled model handle has an invalid private payload") + return payload + + @staticmethod + def _source_for_compiled_model(compiled: _CompiledModelPayload) -> ModelSource: + if compiled.prepared_bundle is not None: + return ModelSource(kind="prepared", path=compiled.prepared_bundle.path) + return ModelSource(kind="compiled", path=compiled._serialized_path) + + @staticmethod + def _is_builtin_compiled_model(compiled: _CompiledModelPayload) -> bool: + return compiled.source.get("kind") == "built-in-sm" + + def _resolve_model( + self, + source: ModelSource | CompiledModel, + ) -> _ResolvedModel: + if isinstance(source, CompiledModel): + compiled = self._private_compiled_model(source) + if self._is_builtin_compiled_model(compiled): + return _ResolvedModel( + self._source_for_compiled_model(compiled), + _builtin_sm_model(), + compiled, + use_compiled_process_catalog=False, + ) + + from ..models.external import CompiledUFOModel + + return _ResolvedModel( + self._source_for_compiled_model(compiled), + CompiledUFOModel(compiled), + compiled, + ) + if source.kind in {"built-in-sm", "built-in-sm-heft"}: + if source.kind == "built-in-sm" and not self._prepared_execution_enabled: + return _ResolvedModel(source, _builtin_sm_model()) + if self._prepared_execution_enabled: + source = self._packaged_builtin_prepared_source(source.kind) + source_input: str | Path + if source.kind == "built-in-sm-heft": + source_input = "built-in-sm-heft" + elif source.path is not None: + source_input = source.path + else: + raise ModelError("external model source has no path") + from .._internal.sm_heft import compile_sm_heft_source + from ..models.external import CompiledUFOModel + from ..models.loading import compile_model_source, load_compiled_model + + try: + if source.kind in {"compiled", "prepared"}: + compiled = load_compiled_model(source.path) + else: + run = self._run_config + use_cache = True if run is None else run.model.cache + cache_dir = None if run is None else run.model.cache_dir + compiled = ( + compile_sm_heft_source( + cache_dir=cache_dir, + use_cache=use_cache, + ) + if source.kind == "built-in-sm-heft" + else compile_model_source( + source_input, + restriction=( + "default" + if source.restriction is None + else str(source.restriction) + ), + simplify=source.simplify, + cache_dir=cache_dir, + use_cache=use_cache, + ) + ) + except (OSError, RuntimeError, TypeError, ValueError) as exc: + raise ModelError(str(exc)) from exc + if self._is_builtin_compiled_model(compiled): + return _ResolvedModel( + source, + _builtin_sm_model(), + compiled, + use_compiled_process_catalog=False, + ) + return _ResolvedModel(source, CompiledUFOModel(compiled), compiled) + + @staticmethod + def _packaged_builtin_prepared_source(source_kind: str) -> ModelSource: + from ..assets.prepared_models import ( + BUILTIN_SM_HEFT_JIT_O2, + BUILTIN_SM_JIT_O2, + PackagedPreparedModelError, + materialize_packaged_prepared_model, + ) + + identifiers = { + "built-in-sm": BUILTIN_SM_JIT_O2, + "built-in-sm-heft": BUILTIN_SM_HEFT_JIT_O2, + } + try: + identifier = identifiers[source_kind] + except KeyError as exc: # pragma: no cover - private caller contract + raise ModelError( + f"no packaged prepared model is registered for {source_kind!r}" + ) from exc + try: + path = materialize_packaged_prepared_model(identifier) + except (OSError, PackagedPreparedModelError, RuntimeError, ValueError) as exc: + raise ModelError( + f"the wheel-owned prepared {source_kind} model is unavailable " + f"or stale: {exc}" + ) from exc + return ModelSource.from_path(path) + + def _require_eager_kernel_pack(self, resolved: _ResolvedModel) -> None: + run = self._run_config + if run is None or not self._prepared_execution_enabled: + return + execution_mode = str(run.evaluator.execution_mode) + compiled = resolved.compiled + if compiled is None or compiled.prepared_bundle is None: + source = resolved.source.path or resolved.source.kind + backend = str(run.evaluator.backend) + raise GenerationError( + f"{execution_mode} generation requires a prepared model kernel " + "pack; run: " + f"pyamplicol model compile {source} MODEL.pyamplicol-model " + f"--backend {backend}" + ) + pack = compiled.prepared_bundle.kernel_pack + recurrence_family = execution_mode in {"recurrence", "on-the-fly"} + if recurrence_family and pack.recurrence_template is None: + source = resolved.source.path or resolved.source.kind + backend = str(run.evaluator.backend) + raise GenerationError( + "prepared model kernel pack does not contain " + "pyamplicol-recurrence-template-v1; rebuild it with: " + f"pyamplicol model compile {source} MODEL.pyamplicol-model " + f"--backend {backend}" + ) + if recurrence_family: + direct_catalog = pack.recurrence_direct_template_catalog + if direct_catalog is None: + source = resolved.source.path or resolved.source.kind + backend = str(run.evaluator.backend) + raise GenerationError( + "prepared model kernel pack does not contain " + "pyamplicol-recurrence-direct-template-v1; rebuild it with: " + f"pyamplicol model compile {source} MODEL.pyamplicol-model " + f"--backend {backend}" + ) + pending = tuple( + template.direct_executor_id + for template in direct_catalog.templates + if not template.payload_binding.executable + ) + if pending: + raise GenerationError( + "prepared model recurrence direct-template catalog contains " + "non-executable bindings " + f"{pending}; rebuild the prepared model with a Direct-Arena " + "capable backend" + ) + try: + validate_prepared_target( + pack.target, + backend=pack.backend, + symjit_application_abi=cast( + str | None, + pack.dependency_abis.get("symjit_application"), + ), + ) + except PreparedTargetError as exc: + source = resolved.source.path or resolved.source.kind + raise GenerationError( + f"{execution_mode} prepared model is incompatible with this " + f"host: {exc}; " + "prepare a matching bundle with: " + f"pyamplicol model compile {source} MODEL.pyamplicol-model " + f"--backend {pack.backend}" + ) from exc + + def _index_eager_kernel_pack(self, resolved: _ResolvedModel) -> _ResolvedModel: + if not self._eager_execution_enabled: + return resolved + compiled = resolved.compiled + bundle = None if compiled is None else compiled.prepared_bundle + if bundle is None: # pragma: no cover - guarded by _require_eager_kernel_pack + raise GenerationError("eager generation has no prepared kernel pack") + try: + index = PreparedCatalogEagerKernelIndex.from_manifest( + bundle.kernel_pack.resolver_manifest + ) + except (KeyError, TypeError, ValueError) as exc: + raise GenerationError( + f"prepared model has an invalid eager kernel resolver: {exc}" + ) from exc + return replace(resolved, eager_kernel_index=index) + + def _apply_prepared_kernel_pack_policy(self, resolved: _ResolvedModel) -> None: + if not self._prepared_execution_enabled: + return + run = self._run_config + if run is None: # pragma: no cover - guarded by property above + return + execution_mode = str(run.evaluator.execution_mode) + compiled = resolved.compiled + bundle = None if compiled is None else compiled.prepared_bundle + if bundle is None: # pragma: no cover - guarded by _require_eager_kernel_pack + raise GenerationError( + f"{execution_mode} generation has no prepared kernel pack" + ) + resolution = self._resource_config + if not isinstance(resolution, ConfigResolution): + resolution = resolve_config(config_to_dict(self._config)) + effective_values = config_to_dict(resolution.effective) + pack_values = _prepared_pack_effective_values(bundle.kernel_pack) + replacement_paths = frozenset(pack_values) + inherited = [ + ClampRequest(item.path, item.effective, item.reason) + for item in resolution.clamps + if item.path not in replacement_paths + ] + prepared_clamps: list[ClampRequest] = [] + for path, value in pack_values.items(): + if _nested_config_value(effective_values, path) == value: + continue + prepared_clamps.append( + ClampRequest( + path, + value, + f"prepared {execution_mode} kernel pack is authoritative for " + "backend " + "and code-shaping optimization settings", + ) + ) + if not prepared_clamps: + return + updated = resolve_config( + config_to_dict(resolution.requested), + clamps=(*inherited, *prepared_clamps), + ) + self._resource_config = updated + self._configuration = _GenerationConfigProvenance.from_config(updated) + self._config = updated.effective + if not self._prepared_pack_warning_emitted: + rendered = ", ".join(clamp.path for clamp in prepared_clamps) + _LOGGER.warning( + "%s generation uses prepared model settings for: %s", + execution_mode, + rendered, + ) + self._prepared_pack_warning_emitted = True + + def _expand_request( + self, + request: ProcessRequest, + resolved_model: _ResolvedModel, + ) -> tuple[CanonicalProcessIR, ...]: + if ( + resolved_model.compiled is None + or not resolved_model.use_compiled_process_catalog + ): + from ..models.builtin.process_catalog import PDGS + from ..models.builtin.process_ir import build_process_ir + from ..models.builtin.process_selection import ( + enumerate_generic_process_set, + ) + from ..processes.core_syntax import expand_process_variants + + multiparticles = {"all": tuple(PDGS)} + if self._run_config is not None: + multiparticles.update(self._run_config.process.multiparticles) + expanded_request = " | ".join( + expand_process_variants( + request.expression, + aliases=multiparticles, + ) + ) + + enumeration = enumerate_generic_process_set( + expanded_request, + self._builtin_process_options, + max_quark_pairs=self._max_quark_pairs, + ) + return tuple( + build_process_ir( + entry.process, + color_accuracy=self._color_accuracy, + options=self._builtin_process_options, + ) + for entry in enumeration.entries + ) + + from ..processes.conservation import conserved_charges + from ..processes.model import ( + ModelParticleCatalog, + build_model_process_ir, + expand_model_processes, + ) + + catalog = ModelParticleCatalog( + resolved_model.compiled.ir.name, + resolved_model.compiled.ir.particles, + resolved_model.compiled.ir.parameters, + ) + run = self._run_config + multiparticles = None if run is None else run.process.multiparticles + candidates = tuple( + build_model_process_ir( + process, + resolved_model.compiled.ir, + color_accuracy=self._color_accuracy, + ) + for process in expand_model_processes( + request.expression, + catalog, + multiparticles=multiparticles, + charges=conserved_charges(resolved_model.compiled.ir), + ) + ) + if not candidates: + raise GenerationError( + f"process request {request.expression!r} has no concrete process " + "conserving the additive charges of the model vertices" + ) + selected, rejected = _select_color_ready_processes( + candidates, + color_accuracy=self._color_accuracy, + # Structural colour coverage is independent of the requested + # reduction. Model/coupling-order certification happens below. + basis="trace", + ) + if not selected: + detail = "; ".join(rejected) or "no concrete processes" + raise GenerationError( + f"process request {request.expression!r} has no usable color plan: " + f"{detail}" + ) + return selected + + def _fft_color_basis( + self, process: CanonicalProcessIR, model: Model | None + ) -> Literal["trace", "adjoint"]: + """Select a suitable exact basis, never assume unproved identities.""" + + if ( + self._run_config is None + or self._run_config.color.contraction.value == "direct" + or self._run_config.color.fft_basis.value == "trace" + ): + return "trace" + if self._automatic_adjoint_basis_limit(process) is not None: + return "trace" + ddm_proven = ( + model is not None + and not process.fundamental_labels + and not process.antifundamental_labels + and len(process.adjoint_labels) >= 2 + and adjoint_tree_color_basis_is_proven( + model, process, max_coupling_orders=self._coupling_order_limits or None + ) + ) + return best_general_fft_basis(process, ddm_proven=ddm_proven).actual_basis + + def _automatic_adjoint_basis_limit(self, process: CanonicalProcessIR) -> str | None: + """Known representation limits must not make automatic requests fail.""" + + run = self._run_config + if run is None or run.color.contraction.value != "auto": + return None + if process.fundamental_labels or process.antifundamental_labels: + return None + from ..color.adjoint_kernel import MAX_ADJOINT_GLUONS + + labels = tuple(sorted(process.adjoint_labels)) + if len(labels) > MAX_ADJOINT_GLUONS: + return "The adjoint metric size limit requires the original trace basis." + reference = self._process_selection.reference_color_order + if labels and reference and set(reference) == set(labels): + pivot = reference.index(labels[0]) + if reference[pivot - 1] != labels[-1]: + return ( + "The requested colour ordering requires the original trace basis." + ) + return None + + def _fft_basis_metadata( + self, color_plan: GenericColorPlan | None + ) -> dict[str, object]: + """Expose the actual representation without changing runtime identity.""" + + run = self._run_config + if ( + color_plan is None + or run is None + or run.color.contraction.value == "direct" + or run.color.fft_basis.value != "adjoint" + ): + return {} + strategy = best_general_fft_basis( + color_plan.process, ddm_proven=color_plan.basis == "adjoint" + ) + return { + "fft_basis_selection": { + "requested": "adjoint", + "actual_basis": strategy.actual_basis, + "name": strategy.name, + "reason": ( + self._automatic_adjoint_basis_limit(color_plan.process) + or strategy.reason + ), + "permutation_degree": strategy.permutation_degree, + "tensor_count": strategy.tensor_count, + } + } + + def _plan_concrete_process( + self, + process: CanonicalProcessIR, + *, + model: Model | None, + ) -> dict[str, object]: + selection = self._process_selection + self._validate_lc_flow_layout(selection) + if ( + selection.selected_color_sector_ids is not None + and self._color_accuracy != "lc" + ): + raise GenerationError( + "process.selected_color_sector_ids is available only for LC generation" + ) + color_plan = build_color_plan( + process, + basis=self._fft_color_basis(process, model), + color_accuracy=self._color_accuracy, + max_sectors=selection.max_color_sectors, + reference_color_order=selection.reference_color_order, + fold_trace_reflections=( + model is not None + and model.lc_trace_reflection_equivalence_is_proven(process) + ), + ) + color_plan, missing_sector_ids = _restrict_color_plan( + color_plan, + selection.selected_color_sector_ids, + ) + if missing_sector_ids: + raise GenerationError( + f"process {process.process!r} did not materialize requested LC " + "colour sector ids: " + + ", ".join(str(sector_id) for sector_id in missing_sector_ids) + ) + if not color_plan.sectors: + detail = "; ".join(color_plan.diagnostics) or "no color sectors" + raise GenerationError( + f"process {process.process!r} has no usable color plan: {detail}" + ) + return { + "key": process.key, + "process": process.process, + "external_particle_count": len(process.legs), + "color_sector_count": color_plan.sector_count, + "color_coverage": ( + "selected" + if color_plan.truncated + or selection.selected_color_sector_ids is not None + else "complete" + ), + "helicity_coverage": ( + "selected" + if selection.selected_source_helicities is not None + else "complete" + ), + "color_diagnostics": tuple(color_plan.diagnostics), + **self._fft_basis_metadata(color_plan), + "coupling_order_limits": self._coupling_order_limits, + "dag_compilation_deferred": True, + } + + def _plan_on_the_fly_process( + self, + projected: _ProjectedOnTheFlyProcess, + ) -> dict[str, object]: + process = projected.expanded.process_ir + census = _on_the_fly_selector_census_v1( + projected.projection.seed, + process, + ) + color_plan = projected.projection.color_plan + return { + "key": process.key, + "process": process.process, + "external_particle_count": len(process.legs), + "execution_mode": "on-the-fly", + "selector_representation": "compact-query-local", + "physical_helicity_count": census["physical_helicity_count"], + "physical_color_flow_count": census["physical_color_flow_count"], + "color_sector_count": census["physical_color_flow_count"], + "color_coverage": "complete" if color_plan is None else "contracted", + "helicity_coverage": "complete", + "color_diagnostics": ( + () if color_plan is None else tuple(color_plan.diagnostics) + ), + **self._fft_basis_metadata(color_plan), + "coupling_order_limits": self._coupling_order_limits, + "color_plan_materialized": color_plan is not None, + "source_projection_validated": True, + "native_seed_construction_deferred": True, + "dag_compilation_deferred": True, + } + + def _compile_concrete_process( + self, + process: CanonicalProcessIR, + model: Model, + *, + progress_callback: Callable[[Mapping[str, str | int]], None] | None = None, + ) -> tuple[GenericDAG, dict[str, object]]: + self._validate_recurrence_request() + prepared = self._prepare_process_construction(process, model) + if self._recurrence_execution_enabled: + raise GenerationError( + "recurrence execution is not available in this development " + "milestone: prepared recurrence-template-v1 is validated, but " + "Rust recurrence builder construction has not been installed" + ) + complete_color_plan = prepared.complete_color_plan + materialized_sector_ids = prepared.materialized_sector_ids + limits = dict(prepared.coupling_order_limits) + replay_plan = prepared.topology_replay + selection = self._process_selection + dag = compile_generic_dag( + process, + model=model, + color_plan=complete_color_plan, + max_color_sectors=selection.max_color_sectors, + reference_color_order=selection.reference_color_order, + selected_color_sector_ids=materialized_sector_ids, + max_coupling_orders=limits or None, + max_quark_pairs=self._max_quark_pairs, + selected_source_helicities=selection.selected_source_helicities, + online_evaluation_reuse=( + self._eager_execution_enabled or self._all_flow_union_enabled + ), + backward_live_planning=self._eager_execution_enabled, + progress_callback=progress_callback, + ) + if replay_plan is not None and replay_plan.optimized: + root_sector_ids = { + int(root.color_sector_id) + if root.color_sector_id is not None + else int(dag.currents[root.left_id].index.color_state.sector_id) + for root in dag.amplitude_roots + } + missing_representatives = { + int(partition.materialized_sector_id) + for partition in replay_plan.partitions + if int(partition.materialized_sector_id) not in root_sector_ids + } + if missing_representatives: + replay_plan = None + dag = compile_generic_dag( + process, + model=model, + color_plan=complete_color_plan, + max_color_sectors=selection.max_color_sectors, + reference_color_order=selection.reference_color_order, + selected_color_sector_ids=None, + max_coupling_orders=limits or None, + max_quark_pairs=self._max_quark_pairs, + selected_source_helicities=selection.selected_source_helicities, + online_evaluation_reuse=self._eager_execution_enabled, + backward_live_planning=self._eager_execution_enabled, + progress_callback=progress_callback, + ) + if replay_plan is not None and replay_plan.optimized: + dag = replace( + dag, + color_plan=complete_color_plan, + color_coverage="complete", + selected_color_sector_ids=(), + lc_topology_replay=( + replay_plan + if isinstance(replay_plan, LCColorTopologyReplayPlan) + else None + ), + color_topology_replay=( + replay_plan + if isinstance(replay_plan, ColorTopologyReplayCertificate) + else None + ), + ) + if dag.truncated: + raise GenerationError( + f"process {process.process!r} DAG was unexpectedly truncated" + ) + if not dag.has_amplitudes: + raise _NoModelSupportedAmplitudes( + f"process {process.process!r} has no model-supported tree-level " + "amplitudes" + ) + return ( + dag, + { + "key": process.key, + "process": process.process, + "color_sector_count": dag.color_plan.sector_count, + **( + {} + if self._color_accuracy != "lc" + else { + "lc_flow_layout": ( + "all-flow-union" + if self._all_flow_union_enabled + else "topology-replay" + ) + } + ), + **( + {} + if ( + dag.lc_topology_replay is None + and dag.color_topology_replay is None + ) + else { + "materialized_color_sector_count": len( + ( + dag.lc_topology_replay or dag.color_topology_replay + ).materialized_sector_ids + ), + "replayed_color_sector_count": ( + ( + dag.lc_topology_replay or dag.color_topology_replay + ).replayed_sector_count + ), + "residual_color_sector_count": len( + ( + dag.lc_topology_replay or dag.color_topology_replay + ).residual_sector_ids + ), + } + ), + "color_coverage": dag.color_coverage, + "helicity_coverage": dag.helicity_coverage, + "source_count": len(dag.sources), + "current_count": len(dag.currents), + "interaction_count": len(dag.interactions), + "interaction_evaluation_count": dag.interaction_evaluation_count, + "amplitude_root_count": len(dag.amplitude_roots), + "coupling_order_limits": limits, + }, + ) + + def _prepare_process_construction( + self, + process: CanonicalProcessIR, + model: Model, + ) -> _PreparedProcessConstruction: + """Prepare the shared model/color contract before choosing a builder.""" + + selection = self._process_selection + self._validate_lc_flow_layout(selection) + if ( + selection.selected_color_sector_ids is not None + and self._color_accuracy != "lc" + ): + raise GenerationError( + "process.selected_color_sector_ids is available only for LC generation" + ) + complete_color_plan = build_color_plan( + process, + basis=self._fft_color_basis(process, model), + color_accuracy=self._color_accuracy, + max_sectors=selection.max_color_sectors, + reference_color_order=selection.reference_color_order, + fold_trace_reflections=( + model.lc_trace_reflection_equivalence_is_proven(process) + ), + ) + color_plan, missing_sector_ids = _restrict_color_plan( + complete_color_plan, + selection.selected_color_sector_ids, + ) + if missing_sector_ids: + raise GenerationError( + f"process {process.process!r} did not materialize requested LC " + "colour sector ids: " + + ", ".join(str(sector_id) for sector_id in missing_sector_ids) + ) + if not color_plan.sectors: + detail = "; ".join(color_plan.diagnostics) or "no color sectors" + raise GenerationError( + f"process {process.process!r} has no usable color plan: {detail}" + ) + limits = self._coupling_order_limits + run = self._run_config + if run is not None and run.process.coupling_order_policy == "minimal": + if ( + selection.max_color_sectors is None + and selection.reference_color_order is None + and complete_color_plan.color_accuracy == process.color_accuracy + ): + inferred = _infer_minimal_coupling_order_limits_from_color_plan( + process, + model=model, + color_plan=complete_color_plan, + max_coupling_orders=limits or None, + ) + else: + inferred = infer_minimal_coupling_order_limits( + process, + model=model, + max_coupling_orders=limits or None, + ) + limits = inferred or limits + replay_plan = None + materialized_sector_ids = selection.selected_color_sector_ids + if ( + self._color_accuracy == "lc" + and not self._all_flow_union_enabled + and selection.selected_color_sector_ids is None + and selection.selected_source_helicities is None + ): + replay_plan = build_lc_topology_replay_plan( + complete_color_plan, + model, + ) + if replay_plan is not None and replay_plan.optimized: + materialized_sector_ids = frozenset(replay_plan.materialized_sector_ids) + elif ( + self._color_accuracy in {"nlc", "full"} + and selection.selected_color_sector_ids is None + and selection.selected_source_helicities is None + and all(abs(int(leg.outgoing_pdg or 0)) == 21 for leg in process.legs) + ): + # The first contracted-color consumer is deliberately scoped to + # pure adjoint amplitudes. Their relabeling phase is exactly +1, + # so recurrence, compiled, and eager modes can consume the same + # certificate without introducing a fermion-crossing convention. + replay_plan = build_color_topology_replay_certificate( + complete_color_plan, + model, + ) + if replay_plan is not None and replay_plan.optimized: + materialized_sector_ids = frozenset(replay_plan.materialized_sector_ids) + return _PreparedProcessConstruction( + process=process, + complete_color_plan=complete_color_plan, + restricted_color_plan=color_plan, + materialized_sector_ids=materialized_sector_ids, + coupling_order_limits=MappingProxyType(dict(limits)), + topology_replay=replay_plan, + ) + + def _validate_lc_flow_layout(self, selection: _ProcessSelection) -> None: + if not self._all_flow_union_enabled: + return + if self._color_accuracy != "lc": + raise GenerationError( + "color.lc_flow_layout='all-flow-union' is available only for " + "LC generation" + ) + incompatible: list[str] = [] + if selection.max_color_sectors is not None: + incompatible.append("process.max_color_sectors") + if not self._recurrence_execution_enabled: + if selection.selected_color_sector_ids is not None: + incompatible.append("process.selected_color_sector_ids") + if selection.selected_source_helicities is not None: + incompatible.append("process.selected_source_helicities") + if incompatible: + raise GenerationError( + "color.lc_flow_layout='all-flow-union' requires complete runtime " + "flow and helicity coverage; remove " + ", ".join(incompatible) + ) + + def _compile_complete_color_helicity_selector_union( + self, + process: _DagProcess, + model: Model, + ) -> GenericDAG | None: + """Build a bounded all-colour lane for runtime helicity selection. + + The ordinary complete artifact keeps topology replay for fast runtime + colour selection and helicity sums. Replaying that compact DAG once per + physical colour sector is inefficient for the complementary + all-colours/selected-helicity workload, so bounded colour spaces receive + an auxiliary complete-colour recurrence quotient. + """ + + parent = process.dag + accuracy = parent.process.color_accuracy + replay = ( + parent.lc_topology_replay + if accuracy == "lc" + else parent.color_topology_replay + if accuracy in {"nlc", "full"} + else None + ) + if ( + self._eager_execution_enabled + or accuracy not in {"lc", "nlc", "full"} + or parent.color_coverage != "complete" + or parent.selected_color_sector_ids + or parent.selected_source_helicities + or parent.helicity_coverage != "complete" + or replay is None + or len(parent.color_plan.sectors) > _MAX_FUSED_HELICITY_SELECTOR_SECTORS + ): + return None + + limits_payload = process.coverage.get("coupling_order_limits", {}) + if not isinstance(limits_payload, Mapping): + raise GenerationError("process coupling-order coverage is not a mapping") + limits = {str(name): int(value) for name, value in limits_payload.items()} + selection = self._process_selection + union = compile_generic_dag( + process.expanded.process_ir, + model=model, + color_plan=parent.color_plan, + max_color_sectors=None, + reference_color_order=selection.reference_color_order, + selected_color_sector_ids=None, + max_coupling_orders=limits or None, + max_quark_pairs=self._max_quark_pairs, + selected_source_helicities=None, + online_evaluation_reuse=False, + backward_live_planning=False, + progress_callback=None, + ) + if union.truncated or not union.has_amplitudes: + raise GenerationError( + "complete-color helicity selector lane did not produce a " + "complete amplitude DAG" + ) + parity_preweighted = any( + float(root.helicity_weight) > 1.0 for root in union.amplitude_roots + ) + reduced = ( + prune_dag_to_amplitude_roots(union) + if parity_preweighted + else prune_global_helicity_flip_equivalent_roots(union, model) + ) + recurrence = build_helicity_recurrence_plan(reduced, model) + if recurrence is None: + return None + materialization = materialize_helicity_recurrence( + reduced, + recurrence, + ) + return replace( + materialization.dag, + lc_topology_replay=None, + color_topology_replay=None, + helicity_recurrence=recurrence, + helicity_materialization=materialization, + ) + + +def _runtime_stage_count(schema: RuntimeExpressionSchema) -> int: + stages = schema.to_mapping().get("stages") + if isinstance(stages, str | bytes) or not isinstance(stages, Sequence): + raise GenerationError("runtime expression schema stages must be a sequence") + return len(stages) + + +def _helicity_selector_lane_stage_count( + lane: _HelicitySelectorLane, +) -> int: + return _runtime_stage_count(lane.runtime_schema) + sum( + _helicity_selector_lane_stage_count(child) for child in lane.child_lanes + ) + + +def _helicity_selector_lane_compile_task_count( + lane: _HelicitySelectorLane, +) -> int: + return ( + _runtime_stage_count(lane.runtime_schema) + + 1 + + sum( + _helicity_selector_lane_compile_task_count(child) + for child in lane.child_lanes + ) + ) + + +def _runtime_schema_dag_summary( + schema: RuntimeExpressionSchema, +) -> dict[str, object]: + payload = schema.to_mapping() + current_storage = cast("Mapping[str, object]", payload["current_storage"]) + source_fill = cast("Mapping[str, object]", payload["source_fill"]) + amplitude_stage = cast("Mapping[str, object]", payload["amplitude_stage"]) + stages = cast("Sequence[Mapping[str, object]]", payload["stages"]) + current_slots = cast("Sequence[object]", current_storage["current_slots"]) + return { + "current_count": len(current_slots), + "source_count": int(source_fill["source_count"]), + "interaction_count": sum(int(stage["interaction_count"]) for stage in stages), + "interaction_evaluation_count": sum( + int(stage["interaction_evaluation_count"]) for stage in stages + ), + "amplitude_root_count": int(amplitude_stage["output_count"]), + "truncated": False, + } + + +def _select_color_ready_processes( + processes: Sequence[CanonicalProcessIR], + *, + color_accuracy: str, + basis: str = "trace", +) -> tuple[tuple[CanonicalProcessIR, ...], tuple[str, ...]]: + """Drop structurally impossible children of an external multiparticle request.""" + + selected: list[CanonicalProcessIR] = [] + rejected: list[str] = [] + for process in processes: + color_plan = build_color_plan( + process, + color_accuracy=color_accuracy, + basis=basis, + max_sectors=1, + ) + # This is only a structural existence probe. Its deliberately capped + # plan may be truncated; complete basis construction happens later. + if color_plan.sectors: + selected.append(process) + continue + detail = "; ".join(color_plan.diagnostics) or "no color sectors" + rejected.append(f"{process.process}: {detail}") + return tuple(selected), tuple(rejected) + + +def _expanded_name(base: str, index: int, count: int) -> str: + return base if count == 1 else f"{base}_{index}" + + +def _process_side_permutation_class( + process: CanonicalProcessIR, +) -> tuple[tuple[str, ...], tuple[str, ...]]: + """Identify one reusable representative per unordered external side.""" + + return ( + tuple(sorted(leg.particle.casefold() for leg in process.initial_legs)), + tuple(sorted(leg.particle.casefold() for leg in process.final_legs)), + ) + + +_RelationWarning = Callable[ + [GroupingProcess, GroupingProcess, LegMapping, CanonicalProcessIR], + str | None, +] + + +def _grouping_process(entry: _ExpandedProcess) -> GroupingProcess: + pdgs = tuple(leg.pdg for leg in entry.process_ir.legs) + if any(pdg is None for pdg in pdgs): + raise GenerationError( + f"process {entry.request.name!r} has an unresolved external particle" + ) + return GroupingProcess( + process_id=entry.request.name, + expression=entry.request.expression, + pdgs=tuple(int(cast(int, pdg)) for pdg in pdgs), + initial_count=len(entry.process_ir.initial_legs), + ) + + +def _collapse_flavor_blind_classes( + entries: Sequence[_ExpandedProcess], + table: Mapping[int, GroupingParticle], + *, + consider_charge_conjugation: bool, + relation_warning: _RelationWarning, +) -> list[_ExpandedProcess]: + """Keep the representative of each flavour class and record the others on it. + + Processes with explicit aliases keep their own artifact entry. + """ + + by_name = {entry.request.name: entry for entry in entries} + members: dict[str, list[GroupMember]] = {} + warnings: list[str] = [] + for flavour_class in flavor_blind_classes( + [_grouping_process(entry) for entry in entries], + table, + consider_charge_conjugation=consider_charge_conjugation, + ): + representative = flavour_class[0] + for process in flavour_class[1:]: + if by_name[process.process_id].aliases: + continue + mapping = flavor_blind_mapping( + process, + representative, + table, + consider_charge_conjugation=consider_charge_conjugation, + ) + warning = relation_warning( + process, + representative, + mapping, + by_name[representative.process_id].process_ir, + ) + if warning is not None: + warnings.append(warning) + members.setdefault(representative.process_id, []).append( + GroupMember.identity(replace(process, generated=False)).related( + mapping, + factor=Fraction(1), + basis=GroupingBasis.FLAVOR_BLIND, + warning=warning, + ) + ) + if warnings: + _LOGGER.warning( + "flavor-blind grouping assumes %d relation(s) that are not symmetries " + "of the model vertices allowed by the coupling orders, e.g. %s", + len(warnings), + "; ".join(warnings[:3]), + ) + grouped = {member.process_id for group in members.values() for member in group} + conjugated = sum( + member.conjugated for group in members.values() for member in group + ) + _LOGGER.info( + "Flavor-blind grouping: %d subprocess%s -> %d generated " + "representative%s; %d routed member%s (%d by charge conjugation%s); " + "%d relabelling warning%s", + len(entries), + "" if len(entries) == 1 else "es", + len(entries) - len(grouped), + "" if len(entries) - len(grouped) == 1 else "s", + len(grouped), + "" if len(grouped) == 1 else "s", + conjugated, + "" if consider_charge_conjugation else ", disabled", + len(warnings), + "" if len(warnings) == 1 else "s", + ) + return [ + replace(entry, group_members=tuple(members.get(entry.request.name, ()))) + for entry in entries + if entry.request.name not in grouped + ] + + +def _permuted_process_identity( + process: CanonicalProcessIR, + representative_to_alias: Sequence[int], +) -> tuple[str, tuple[int, ...]]: + """Return public process metadata in a side-preserving alias order.""" + + legs = process.legs + if len(representative_to_alias) != len(legs) or sorted( + representative_to_alias + ) != list(range(len(legs))): + raise GenerationError("alias particle permutation is not complete") + alias_legs: list[ProcessLegIR | None] = [None] * len(legs) + for representative_index, alias_index in enumerate(representative_to_alias): + alias_legs[alias_index] = legs[representative_index] + if any(leg is None for leg in alias_legs): + raise GenerationError("alias particle permutation is incomplete") + ordered_legs = tuple(cast(ProcessLegIR, leg) for leg in alias_legs) + if any(leg.pdg is None for leg in ordered_legs): + raise GenerationError("process alias has an unresolved external particle") + initial = " ".join(leg.particle for leg in ordered_legs if leg.is_initial) + final = " ".join(leg.particle for leg in ordered_legs if leg.is_final) + if not initial or not final: + raise GenerationError("process alias must retain initial and final states") + return ( + f"{initial} > {final}", + tuple(int(leg.pdg) for leg in ordered_legs if leg.pdg is not None), + ) + + +def _cpp_optimization_level(value: str) -> int: + normalized = value.strip().upper() + if normalized.startswith("-"): + normalized = normalized[1:] + if normalized not in {"O0", "O1", "O2", "O3"}: + raise GenerationError("evaluator.cpp.optimization must be O0, O1, O2, or O3") + return int(normalized[1]) + + +def create_generator_backend( + config: GenerationConfig | RunConfig | ConfigResolution | None, + progress: ProgressSink | None, + *, + api_bundle_hook: ApiBundleHook | None = None, +) -> GenerationBackend: + return GenerationBackend(config, progress, api_bundle_hook=api_bundle_hook) + + +__all__ = ["GenerationBackend", "create_generator_backend"] diff --git a/pyamplicol/src/pyamplicol/generation/stage_artifacts.py b/pyamplicol/src/pyamplicol/generation/stage_artifacts.py new file mode 100644 index 0000000000..110825081c --- /dev/null +++ b/pyamplicol/src/pyamplicol/generation/stage_artifacts.py @@ -0,0 +1,1309 @@ +# SPDX-License-Identifier: 0BSD +"""Compilation and persistence of stage evaluator artifacts.""" + +from __future__ import annotations + +import time +from collections.abc import Callable, Mapping, Sequence +from dataclasses import replace +from pathlib import Path +from typing import Any + +import numpy as np + +from .._internal.physics.parameters import ParamBuilder +from .._internal.physics.symbols import symbols +from .._internal.versions import ( + COMPILED_PLANE_ARENA_RUNTIME_CAPABILITY, + COMPILED_PLANE_DIRECT_APPLICATION_ABI, + COMPILED_RUNTIME_SELECTORS_CAPABILITY, + NATIVE_COMPILED_DIRECT_APPLICATION_ABI, + SYMBOLICA_ASM_RUNTIME_CAPABILITY, + SYMBOLICA_CPP_RUNTIME_CAPABILITY, + SYMJIT_APPLICATION_ABI, + SYMJIT_F64_RUNTIME_CAPABILITY, + SYMJIT_PLANE_APPLICATION_ABI, +) +from ..evaluators.execution_schema import ( + aggregate_runtime_capabilities, + evaluator_runtime_capabilities, +) +from ..models.base import Model +from .contracts import StageCompilationInput +from .dag_types import GenericDAG +from .stage_parameters import _dict, _list, _logical_model_parameter_symbols +from .stage_planning import ( + _prepare_stage_for_output_chunking, + build_generic_stage_compiler_blueprint, +) +from .stage_settings import _stage_symbolica_settings +from .stage_types import ( + GenericCompiledStageBlueprint, + GenericStageCompilerBlueprint, + StageBlueprintProgress, + StageEvaluatorCompiler, +) + +_EXPRESSION_PREVIEW_LIMIT = 512 + + +def write_generic_stage_evaluator_artifacts( + blueprint: GenericStageCompilerBlueprint, + artifact_dir: str | Path, + *, + compiler: StageEvaluatorCompiler | None = None, + symbolica_settings: Any | None = None, + merge_evaluators_strategy: bool = False, + verbose_evaluator_build: bool = False, + jit_compile: bool = True, + progress_callback: Any | None = None, +) -> dict[str, object]: + """Serialize evaluator artifacts for a schema-v3 stage blueprint. + + The function is intentionally opt-in. Normal schema-v3 process manifest + generation remains cheap, while this path is the bridge from the + process-generic current DAG to concrete Symbolica evaluator artifacts. + The native runtime can validate, load, and execute the resulting metadata + through its generic staged runtime. + """ + + if not blueprint.expression_ready: + raise ValueError( + "cannot write generic evaluator artifacts with lowering blockers: " + + "; ".join(blueprint.blockers) + ) + + output_dir = Path(artifact_dir).expanduser() + output_dir.mkdir(parents=True, exist_ok=True) + build_started = time.perf_counter() + if progress_callback is not None: + progress_callback( + { + "stage": "stage compile", + "item": "start", + "total": blueprint.stage_count, + } + ) + + def compile_stage(stage: GenericCompiledStageBlueprint) -> dict[str, object]: + return _compile_stage_evaluator_artifact( + stage, + output_dir, + compiler=compiler, + blueprint=blueprint, + symbolica_settings=symbolica_settings, + merge_evaluators_strategy=merge_evaluators_strategy, + verbose_evaluator_build=verbose_evaluator_build, + jit_compile=jit_compile, + progress_callback=progress_callback, + ) + + stage_payloads = [] + stage_timings: list[dict[str, object]] = [] + for stage in blueprint.stages: + prepared_stage = _prepare_stage_for_output_chunking( + stage, + blueprint=blueprint, + symbolica_settings=symbolica_settings, + ) + payload = prepared_stage.to_json_dict() + payload["evaluator"] = compile_stage(prepared_stage) + direct = _compiled_plane_arena_stage(payload) + if direct is not None: + payload["compiled_plane_arena"] = direct + stage_timings.append( + _stage_build_timing_record( + prepared_stage.evaluator_label, + payload["evaluator"], + ) + ) + stage_payloads.append(payload) + if progress_callback is not None: + timing = stage_timings[-1] + progress_callback( + { + "stage": "stage complete", + "item": stage.evaluator_label, + "increment": 1, + "total": blueprint.stage_count, + "duration_s": timing["stage_evaluator_build_s"], + } + ) + + prepared_amplitude_stage = _prepare_stage_for_output_chunking( + blueprint.amplitude_stage, + blueprint=blueprint, + symbolica_settings=symbolica_settings, + ) + amplitude_payload = prepared_amplitude_stage.to_json_dict() + amplitude_payload["evaluator"] = compile_stage(prepared_amplitude_stage) + direct = _compiled_plane_arena_stage(amplitude_payload) + if direct is not None: + amplitude_payload["compiled_plane_arena"] = direct + stage_timings.append( + _stage_build_timing_record( + blueprint.amplitude_stage.evaluator_label, + amplitude_payload["evaluator"], + ) + ) + if progress_callback is not None: + timing = stage_timings[-1] + progress_callback( + { + "stage": "stage complete", + "item": blueprint.amplitude_stage.evaluator_label, + "increment": 1, + "total": blueprint.stage_count, + "duration_s": timing["stage_evaluator_build_s"], + } + ) + + return _finalize_stage_evaluator_payload( + blueprint, + stage_payloads=stage_payloads, + amplitude_payload=amplitude_payload, + stage_timings=stage_timings, + build_started=build_started, + ) + + +def write_model_parameter_evaluator_artifact( + model: Model, + runtime_schema: Mapping[str, object], + artifact_dir: str | Path, + *, + symbolica_settings: Any | None = None, + jit_compile: bool = True, + progress_callback: Any | None = None, +) -> dict[str, object] | None: + schema = _dict(runtime_schema) + records = tuple( + sorted( + (_dict(item) for item in _list(schema.get("model_parameters", []))), + key=lambda item: int(item["parameter_index"]), + ) + ) + input_records = tuple( + record + for record in records + if str(record.get("kind")) + in {"external_parameter", "external_parameter_component"} + ) + derived_components: dict[str, dict[str, int]] = {} + for record in records: + if str(record.get("kind")) != "derived_parameter_component": + continue + runtime_name = record.get("runtime_name") + component = record.get("complex_component") + if isinstance(runtime_name, str) and component in {"real", "imag"}: + derived_components.setdefault(runtime_name, {})[str(component)] = int( + record["parameter_index"] + ) + requested_output_names = tuple( + name + for name, components in sorted( + derived_components.items(), + key=lambda item: min(item[1].values()), + ) + if set(components) == {"real", "imag"} + ) + if not requested_output_names: + return None + + definitions_provider = getattr( + model, + "runtime_derived_parameter_definitions", + None, + ) + if not callable(definitions_provider): + return None + definitions_subset_provider = getattr( + model, + "runtime_derived_parameter_definitions_for", + None, + ) + definition_values = ( + definitions_subset_provider(requested_output_names) + if callable(definitions_subset_provider) + else definitions_provider() + ) + definitions = { + str(name): str(expression) + for name, expression in definition_values.items() + if str(name) in requested_output_names + } + output_names = tuple(name for name in requested_output_names if name in definitions) + if not output_names: + return None + + # Planning imports this module without compiling model parameters. + from ..models.compiler_records import _replace_evaluator_constants + + builder = ParamBuilder() + model_symbols = symbols.model(getattr(model, "name", "unnamed-model")) + parameter_symbols = tuple( + builder.add_parameter_list( + ("artifact_schema_v3", "external_model_parameters"), + len(input_records), + role="generic_external_model_parameters", + real_valued=True, + ) + ) + slot_symbols = { + str(record["name"]): parameter_symbols[index] + for index, record in enumerate(input_records) + } + logical_symbols = _logical_model_parameter_symbols( + input_records, + slot_symbols, + ) + outputs = [] + for name in output_names: + expression = model_symbols.expression(definitions[name]) + for parameter_name, symbol in logical_symbols.items(): + expression = expression.replace( + model_symbols.symbol(parameter_name), + symbol, + ) + outputs.append(expression) + + stage = GenericCompiledStageBlueprint( + stage_index=0, + stage_kind="model-parameter-derivation", + subset_size=None, + evaluator_label="generic_model_parameter_derivation", + parameter_layout="external-model-parameters", + output_length=len(outputs), + output_slots=(), + input_value_slot_ids=(), + output_value_slot_ids=(), + interaction_ids=(), + input_components=(), + parameter_count=len(parameter_symbols), + value_parameter_count=0, + momentum_parameter_count=0, + model_parameter_count=len(parameter_symbols), + real_valued_inputs=tuple(range(len(parameter_symbols))), + expression_ready=True, + blockers=(), + first_output_previews=tuple( + expression.to_canonical_string()[:_EXPRESSION_PREVIEW_LIMIT] + for expression in outputs[:3] + ), + parameter_symbols=parameter_symbols, + output_expressions=tuple(outputs), + ) + parameter_evaluator_settings = ( + None + if symbolica_settings is None + else replace( + symbolica_settings, + compiled_output_chunk_size=None, + output_chunk_strategy="uniform", + ) + ) + evaluator = _compile_stage_evaluator_artifact( + # Native evaluators use f64 constants, whereas the retained compiled + # model keeps the exact definitions used by the high-precision path. + replace( + stage, + output_expressions=tuple( + _replace_evaluator_constants(expression) for expression in outputs + ), + ), + Path(artifact_dir).expanduser(), + compiler=None, + blueprint=None, + symbolica_settings=parameter_evaluator_settings, + merge_evaluators_strategy=False, + verbose_evaluator_build=False, + jit_compile=jit_compile, + progress_callback=progress_callback, + ) + return { + "kind": "generic-model-parameter-evaluator", + "required_runtime_capabilities": list( + evaluator_runtime_capabilities(evaluator) + ), + "input_parameter_indices": [ + int(record["parameter_index"]) for record in input_records + ], + "outputs": [ + { + "runtime_name": name, + "output_index": output_index, + "real_parameter_index": derived_components[name]["real"], + "imag_parameter_index": derived_components[name]["imag"], + } + for output_index, name in enumerate(output_names) + ], + "evaluator": evaluator, + } + + +def _compiled_plane_arena_stage( + stage: Mapping[str, object], +) -> dict[str, object] | None: + """Freeze one fused SymJIT stage's direct plane bindings. + + The ordinary SymJIT application remains a cold source payload. Its + row-major interface is not the execution contract: this record fixes the + canonical preorder leaf maps, arena inputs, and overwrite destinations + used to lower and bind the factor-free DirectApplication at load time. + """ + + if stage.get("parameter_layout") != "stage-local-value-momentum": + return None + evaluator = stage.get("evaluator") + if not isinstance(evaluator, Mapping): + raise ValueError("compiled stage evaluator metadata is invalid") + parameter_count = stage.get("parameter_count") + output_length = stage.get("output_length") + if ( + isinstance(parameter_count, bool) + or not isinstance(parameter_count, int) + or parameter_count < 0 + or isinstance(output_length, bool) + or not isinstance(output_length, int) + or output_length < 1 + ): + raise ValueError("compiled stage dimensions are invalid") + + leaves, output_stop, application_abi, source_application_abi = ( + _compiled_plane_arena_leaves( + evaluator, + tuple(range(parameter_count)), + 0, + ) + ) + if leaves is None: + return None + if output_stop != output_length: + raise ValueError( + "compiled plane-arena leaves do not cover the fused stage outputs" + ) + + raw_inputs = stage.get("input_components") + if not isinstance(raw_inputs, Sequence) or isinstance(raw_inputs, (str, bytes)): + raise ValueError("compiled plane-arena input bindings are absent") + inputs: list[dict[str, object] | None] = [None] * parameter_count + for raw in raw_inputs: + if not isinstance(raw, Mapping): + raise ValueError("compiled plane-arena input binding is invalid") + parameter_index = raw.get("parameter_index") + kind = raw.get("kind") + if ( + isinstance(parameter_index, bool) + or not isinstance(parameter_index, int) + or not 0 <= parameter_index < parameter_count + or inputs[parameter_index] is not None + or kind not in {"value", "momentum", "model_parameter"} + ): + raise ValueError("compiled plane-arena input bindings are inconsistent") + inputs[parameter_index] = { + "parameter_index": parameter_index, + "kind": kind, + "source_id": _required_nonnegative_int(raw, "source_id"), + "component": _required_nonnegative_int(raw, "component"), + "global_component": _required_nonnegative_int(raw, "global_component"), + "real_valued": bool(raw.get("real_valued", False)), + } + if any(binding is None for binding in inputs): + raise ValueError("compiled plane-arena input bindings are incomplete") + + raw_slots = stage.get("output_slots") + if not isinstance(raw_slots, Sequence) or isinstance(raw_slots, (str, bytes)): + raise ValueError("compiled plane-arena output bindings are absent") + arena = ( + "amplitude" + if str(stage.get("stage_kind", "")).startswith("amplitude") + else "current" + ) + outputs: list[dict[str, object] | None] = [None] * output_length + seen_components: set[int] = set() + for raw in raw_slots: + if not isinstance(raw, Mapping): + raise ValueError("compiled plane-arena output slot is invalid") + output_start = _required_nonnegative_int(raw, "output_start") + output_stop = _required_nonnegative_int(raw, "output_stop") + component_start = _required_nonnegative_int(raw, "component_start") + component_stop = _required_nonnegative_int(raw, "component_stop") + if ( + output_stop < output_start + or component_stop - component_start != output_stop - output_start + or output_stop > output_length + ): + raise ValueError("compiled plane-arena output slot range is invalid") + for offset in range(output_stop - output_start): + output_index = output_start + offset + component = component_start + offset + if outputs[output_index] is not None or component in seen_components: + raise ValueError("compiled plane-arena output bindings alias") + seen_components.add(component) + outputs[output_index] = { + "output_index": output_index, + "arena": arena, + "component": component, + } + if any(binding is None for binding in outputs): + raise ValueError("compiled plane-arena output bindings are incomplete") + + return { + "schema_version": 1, + "kind": "compiled-plane-arena-stage", + "application_abi": application_abi, + "source_application_abi": source_application_abi, + "element_layout": "split-complex-component-major", + "output_operation": "overwrite", + "output_factor": "identity", + "input_output_aliasing": "forbidden", + "output_output_aliasing": "forbidden", + "input_bindings": inputs, + "output_bindings": outputs, + "leaves": leaves, + } + + +def _compiled_plane_arena_leaves( + evaluator: Mapping[str, object], + parent_inputs: tuple[int, ...], + output_start: int, +) -> tuple[ + list[dict[str, object]] | None, + int, + str | None, + str | None, +]: + kind = evaluator.get("kind") + if kind == "chunked-symbolica-evaluator": + chunks = evaluator.get("chunks") + if not isinstance(chunks, Sequence) or isinstance(chunks, (str, bytes)): + raise ValueError("compiled chunked evaluator has invalid children") + input_len = evaluator.get("input_len") + if input_len is not None and input_len != len(parent_inputs): + raise ValueError("compiled chunked evaluator input width changed") + raw_maps = evaluator.get("chunk_input_indices") + if raw_maps is None: + input_maps = [parent_inputs] * len(chunks) + else: + if ( + not isinstance(raw_maps, Sequence) + or isinstance(raw_maps, (str, bytes)) + or len(raw_maps) != len(chunks) + ): + raise ValueError("compiled chunk input maps are invalid") + input_maps = [] + for raw_map in raw_maps: + if not isinstance(raw_map, Sequence) or isinstance( + raw_map, (str, bytes) + ): + raise ValueError("compiled chunk input map is invalid") + mapped: list[int] = [] + for raw_index in raw_map: + if ( + isinstance(raw_index, bool) + or not isinstance(raw_index, int) + or not 0 <= raw_index < len(parent_inputs) + ): + raise ValueError( + "compiled chunk input map references an absent input" + ) + mapped.append(parent_inputs[raw_index]) + input_maps.append(tuple(mapped)) + result: list[dict[str, object]] = [] + cursor = output_start + application_abi: str | None = None + source_application_abi: str | None = None + for raw_chunk, mapped in zip(chunks, input_maps, strict=True): + if not isinstance(raw_chunk, Mapping): + raise ValueError("compiled evaluator child is invalid") + child, cursor, child_application_abi, child_source_abi = ( + _compiled_plane_arena_leaves(raw_chunk, mapped, cursor) + ) + if child is None: + return None, output_start, None, None + if application_abi is None: + application_abi = child_application_abi + source_application_abi = child_source_abi + elif ( + child_application_abi != application_abi + or child_source_abi != source_application_abi + ): + raise ValueError( + "compiled plane-arena evaluator chunks mix direct ABIs" + ) + result.extend(child) + return result, cursor, application_abi, source_application_abi + + if kind == "symjit-application-evaluator": + if ( + evaluator.get("runtime_capability") != SYMJIT_F64_RUNTIME_CAPABILITY + or evaluator.get("application_abi") != SYMJIT_APPLICATION_ABI + or evaluator.get("element_layout") != "complex-f64" + or evaluator.get("batch_layout") != "row-major" + ): + raise ValueError("compiled SymJIT leaf has an incompatible source ABI") + plane = evaluator.get("plane_application") + if not isinstance(plane, Mapping): + raise ValueError( + "compiled SymJIT leaf predates the SymJIT 2.22 plane ABI; " + "regenerate this artifact" + ) + if ( + plane.get("application_abi") != SYMJIT_PLANE_APPLICATION_ABI + or plane.get("storage_abi") != SYMJIT_APPLICATION_ABI + or plane.get("element_layout") != "split-complex-plane-major" + or plane.get("descriptor_order") != "inputs-re-im-then-outputs-re-im" + ): + raise ValueError("compiled SymJIT leaf has an incompatible plane ABI") + application_path = plane.get("application_path") + source_application_abi = SYMJIT_PLANE_APPLICATION_ABI + application_abi = COMPILED_PLANE_DIRECT_APPLICATION_ABI + optimization_level = _required_nonnegative_int( + plane, + "optimization_level", + ) + direct_codegen_optimization_level = optimization_level + if optimization_level not in {0, 1, 2, 3}: + raise ValueError( + "compiled SymJIT plane-arena optimization level must be 0, 1, 2, or 3" + ) + elif kind == "compiled-complex-evaluator": + if evaluator.get("runtime_capability") not in { + SYMBOLICA_CPP_RUNTIME_CAPABILITY, + SYMBOLICA_ASM_RUNTIME_CAPABILITY, + }: + raise ValueError( + "compiled native leaf has an incompatible runtime capability" + ) + direct = evaluator.get("native_direct_application") + if not isinstance(direct, Mapping): + raise ValueError( + "compiled native leaf has no plane-native DirectApplication" + ) + application_abi = direct.get("application_abi") + application_path = direct.get("library_path") + source_application_abi = application_abi + optimization_level = 3 + direct_codegen_optimization_level = 3 + if application_abi != NATIVE_COMPILED_DIRECT_APPLICATION_ABI: + raise ValueError("compiled native leaf has an incompatible direct ABI") + else: + return None, output_start, None, None + + input_len = _required_nonnegative_int(evaluator, "input_len") + output_len = _required_nonnegative_int(evaluator, "output_len") + if ( + input_len != len(parent_inputs) + or output_len < 1 + or not isinstance(application_path, str) + or not application_path + ): + raise ValueError("compiled SymJIT leaf metadata is invalid") + output_stop = output_start + output_len + return ( + [ + { + "application_path": application_path, + "source_application_abi": source_application_abi, + "optimization_level": optimization_level, + "direct_codegen_optimization_level": ( + direct_codegen_optimization_level + ), + "input_len": input_len, + "output_len": output_len, + "input_indices": list(parent_inputs), + "output_start": output_start, + "output_stop": output_stop, + } + ], + output_stop, + str(application_abi), + str(source_application_abi), + ) + + +def _required_nonnegative_int( + record: Mapping[str, object], + name: str, +) -> int: + value = record.get(name) + if isinstance(value, bool) or not isinstance(value, int) or value < 0: + raise ValueError(f"compiled plane-arena {name.replace('_', ' ')} is invalid") + return value + + +def _finalize_stage_evaluator_payload( + blueprint: GenericStageCompilerBlueprint, + *, + stage_payloads: list[dict[str, object]], + amplitude_payload: dict[str, object], + stage_timings: list[dict[str, object]], + build_started: float, + total_build_s_override: float | None = None, +) -> dict[str, object]: + stage_local_layout = ( + blueprint.amplitude_stage.parameter_layout == "stage-local-value-momentum" + and all( + stage.parameter_layout == "stage-local-value-momentum" + for stage in blueprint.stages + ) + ) + total_build_s = ( + time.perf_counter() - build_started + if total_build_s_override is None + else float(total_build_s_override) + ) + jit_compile_s = sum( + float(record.get("jit_compile_s") or 0.0) for record in stage_timings + ) + timing_totals: dict[str, object] = {} + non_additive_stage_timing_keys = { + "output_chunk_autotune_batch_size", + "output_chunk_autotune_baseline_size", + "output_chunk_autotune_selected_size", + "output_chunk_autotune_baseline_us", + "output_chunk_autotune_selected_us", + "output_chunk_autotune_gain", + "output_chunk_autotune_shared_pack_scoring", + } + for record in stage_timings: + for key, value in record.items(): + if ( + key == "evaluator_label" + or key == "jit_compile_s" + or key in non_additive_stage_timing_keys + ): + continue + if isinstance(value, (float, int)): + timing_totals[key] = float(timing_totals.get(key, 0.0)) + float(value) + timing_totals["stage_evaluator_build_s"] = total_build_s + timing_totals["jit_compile_s"] = jit_compile_s + timing_totals["jit_fraction_of_stage_evaluator_build"] = ( + None if total_build_s <= 0.0 else jit_compile_s / total_build_s + ) + timing_totals["stages"] = stage_timings + evaluator_manifests = [ + _dict(payload["evaluator"]) for payload in (*stage_payloads, amplitude_payload) + ] + required_runtime_capabilities = set( + aggregate_runtime_capabilities(evaluator_manifests) + ) + if any( + slot.selector_domain_ids or slot.color_selector_domain_ids + for stage in (*blueprint.stages, blueprint.amplitude_stage) + for slot in stage.output_slots + ): + required_runtime_capabilities.add(COMPILED_RUNTIME_SELECTORS_CAPABILITY) + direct_stage_count = sum( + "compiled_plane_arena" in payload + for payload in (*stage_payloads, amplitude_payload) + ) + stage_count = len(stage_payloads) + 1 + requires_direct = bool( + required_runtime_capabilities + & { + SYMJIT_F64_RUNTIME_CAPABILITY, + SYMBOLICA_CPP_RUNTIME_CAPABILITY, + SYMBOLICA_ASM_RUNTIME_CAPABILITY, + } + ) + if requires_direct and direct_stage_count != stage_count: + raise ValueError( + "compiled f64 artifacts require compiled-plane-arena-v1 metadata " + "for every fused stage" + ) + if direct_stage_count: + if direct_stage_count != stage_count: + raise ValueError( + "compiled plane-arena metadata must cover every fused stage" + ) + required_runtime_capabilities.add(COMPILED_PLANE_ARENA_RUNTIME_CAPABILITY) + return { + "kind": "generic-dag-stage-evaluator-artifacts", + "required_runtime_capabilities": sorted(required_runtime_capabilities), + "runtime_available": True, + "runtime_unavailable_message": None, + "parameter_count": 0 if stage_local_layout else blueprint.parameter_count, + "value_parameter_count": ( + 0 if stage_local_layout else blueprint.value_parameter_count + ), + "momentum_parameter_count": ( + 0 if stage_local_layout else blueprint.momentum_parameter_count + ), + "model_parameter_count": ( + 0 if stage_local_layout else blueprint.model_parameter_count + ), + "real_valued_inputs": ( + [] if stage_local_layout else list(blueprint.real_valued_inputs) + ), + "parameter_layout": ( + "stage-local-value-momentum" + if stage_local_layout + else "global-value-momentum" + ), + "stage_count": blueprint.stage_count, + "build_timing": timing_totals, + "stages": stage_payloads, + "amplitude_stage": amplitude_payload, + } + + +def _compile_stage_evaluator_artifact( + stage: GenericCompiledStageBlueprint, + artifact_dir: Path, + *, + compiler: StageEvaluatorCompiler | None, + blueprint: GenericStageCompilerBlueprint | None, + symbolica_settings: Any | None, + merge_evaluators_strategy: bool, + verbose_evaluator_build: bool, + jit_compile: bool, + progress_callback: Any | None, + current_stage_position: int | None = None, + current_stage_count: int | None = None, +) -> dict[str, object]: + if not stage.output_expressions: + raise ValueError( + f"generic stage {stage.evaluator_label!r} has no output expressions" + ) + started = time.perf_counter() + if compiler is not None: + manifest = compiler( + stage, + stage.parameter_symbols, + stage.real_valued_inputs, + ) + else: + manifest = _compile_default_stage_evaluator( + stage, + blueprint, + artifact_dir, + symbolica_settings=symbolica_settings, + merge_evaluators_strategy=merge_evaluators_strategy, + verbose_evaluator_build=verbose_evaluator_build, + jit_compile=jit_compile, + progress_callback=progress_callback, + current_stage_position=current_stage_position, + current_stage_count=current_stage_count, + ) + if not isinstance(manifest, dict): + raise TypeError( + f"generic stage compiler for {stage.evaluator_label!r} " + "did not return a manifest dictionary" + ) + build_s = time.perf_counter() - started + manifest.setdefault("build_timing", {}) + timing = manifest["build_timing"] + if isinstance(timing, dict): + previous_stage_build_s = timing.get("stage_evaluator_build_s") + timing["stage_evaluator_build_s"] = build_s + if previous_stage_build_s is not None: + timing["stage_compiler_wrapper_s"] = build_s - float(previous_stage_build_s) + timing.setdefault("symbolica_evaluator_build_s", build_s) + if _manifest_uses_jit_evaluator(manifest): + timing.setdefault("jit_compile_s", build_s) + return manifest + + +def build_and_write_generic_stage_evaluator_artifacts( + manifest: StageCompilationInput | GenericDAG, + runtime_schema: Mapping[str, object], + artifact_dir: str | Path, + *, + model: Model | None = None, + enable_lc_sector_runtime_selector: bool | None = None, + stage_local_parameter_layout: bool = True, + compiler: StageEvaluatorCompiler | None = None, + symbolica_settings: Any | None = None, + merge_evaluators_strategy: bool = False, + verbose_evaluator_build: bool = False, + jit_compile: bool = True, + blueprint_progress_callback: StageBlueprintProgress | None = None, + evaluator_progress_callback: Any | None = None, +) -> tuple[GenericStageCompilerBlueprint, dict[str, object]]: + """Lower, compile, and release one recursion stage at a time.""" + + output_dir = Path(artifact_dir).expanduser() + output_dir.mkdir(parents=True, exist_ok=True) + schema = _dict(runtime_schema) + current_stage_count = len(_list(schema["stages"])) + stage_count = current_stage_count + 1 + build_started = time.perf_counter() + if evaluator_progress_callback is not None: + evaluator_progress_callback( + { + "stage": "stage compile", + "item": "start", + "total": stage_count, + "step": "evaluator compilation", + "stage_total": stage_count, + } + ) + + stage_payloads: list[dict[str, object]] = [] + amplitude_payload: dict[str, object] | None = None + stage_timings: list[dict[str, object]] = [] + + def consume_stage( + stage: GenericCompiledStageBlueprint, + position: int, + reported_current_stage_count: int, + ) -> None: + nonlocal amplitude_payload + if reported_current_stage_count != current_stage_count: + raise ValueError("streamed stage count changed during blueprint lowering") + if not stage.expression_ready: + raise ValueError( + "cannot write generic evaluator artifact with lowering blockers: " + + "; ".join(stage.blockers) + ) + prepared_stage = _prepare_stage_for_output_chunking( + stage, + blueprint=None, + symbolica_settings=symbolica_settings, + current_stage_position=position, + current_stage_count=current_stage_count, + ) + payload = prepared_stage.to_json_dict() + payload["evaluator"] = _compile_stage_evaluator_artifact( + prepared_stage, + output_dir, + compiler=compiler, + blueprint=None, + symbolica_settings=symbolica_settings, + merge_evaluators_strategy=merge_evaluators_strategy, + verbose_evaluator_build=verbose_evaluator_build, + jit_compile=jit_compile, + progress_callback=evaluator_progress_callback, + current_stage_position=position, + current_stage_count=current_stage_count, + ) + direct = _compiled_plane_arena_stage(payload) + if direct is not None: + payload["compiled_plane_arena"] = direct + timing = _stage_build_timing_record( + prepared_stage.evaluator_label, + payload["evaluator"], + ) + stage_timings.append(timing) + if str(prepared_stage.stage_kind).startswith("amplitude"): + amplitude_payload = payload + else: + stage_payloads.append(payload) + if evaluator_progress_callback is not None: + evaluator_progress_callback( + { + "stage": "stage complete", + "item": prepared_stage.evaluator_label, + "increment": 1, + "total": stage_count, + "duration_s": timing["stage_evaluator_build_s"], + "step": "stage complete", + "stage_index": position + 1, + "stage_total": stage_count, + "subset_size": prepared_stage.subset_size, + "interaction_count": len(prepared_stage.interaction_ids), + "input_count": prepared_stage.parameter_count, + "output_count": prepared_stage.output_length, + } + ) + + blueprint = build_generic_stage_compiler_blueprint( + manifest, + model=model, + enable_lc_sector_runtime_selector=enable_lc_sector_runtime_selector, + runtime_schema=schema, + stage_local_parameter_layout=stage_local_parameter_layout, + progress_callback=blueprint_progress_callback, + stage_consumer=consume_stage, + release_consumed_expressions=True, + ) + if amplitude_payload is None or len(stage_payloads) != current_stage_count: + raise ValueError("streamed stage compilation produced incomplete metadata") + return blueprint, _finalize_stage_evaluator_payload( + blueprint, + stage_payloads=stage_payloads, + amplitude_payload=amplitude_payload, + stage_timings=stage_timings, + build_started=build_started, + total_build_s_override=sum( + float(timing["stage_evaluator_build_s"]) for timing in stage_timings + ), + ) + + +def _stage_build_timing_record( + evaluator_label: str, + evaluator_manifest: object, +) -> dict[str, object]: + manifest = evaluator_manifest if isinstance(evaluator_manifest, dict) else {} + raw_timing = manifest.get("build_timing") if isinstance(manifest, dict) else None + timing = raw_timing if isinstance(raw_timing, dict) else {} + record: dict[str, object] = { + "evaluator_label": evaluator_label, + "stage_evaluator_build_s": float(timing.get("stage_evaluator_build_s") or 0.0), + "symbolica_evaluator_build_s": float( + timing.get("symbolica_evaluator_build_s") or 0.0 + ), + "jit_compile_s": ( + None + if timing.get("jit_compile_s") is None + else float(timing.get("jit_compile_s") or 0.0) + ), + } + for key, value in timing.items(): + if key in record: + continue + if isinstance(value, (float, int)): + record[str(key)] = float(value) + return record + + +def _manifest_uses_jit_evaluator(manifest: Mapping[str, object]) -> bool: + if str(manifest.get("kind", "")) in { + "jit-symbolica-evaluator", + "symjit-application-evaluator", + }: + return True + if str(manifest.get("kind", "")) == "chunked-symbolica-evaluator": + chunks = manifest.get("chunks") + if isinstance(chunks, Sequence) and chunks: + return all( + isinstance(chunk, Mapping) and _manifest_uses_jit_evaluator(chunk) + for chunk in chunks + ) + settings = manifest.get("settings") + if isinstance(settings, Mapping): + return str(settings.get("backend", "")) == "jit" + return False + + +def _compile_default_stage_evaluator( + stage: GenericCompiledStageBlueprint, + blueprint: GenericStageCompilerBlueprint | None, + artifact_dir: Path, + *, + symbolica_settings: Any | None, + merge_evaluators_strategy: bool, + verbose_evaluator_build: bool, + jit_compile: bool, + progress_callback: Any | None, + current_stage_position: int | None = None, + current_stage_count: int | None = None, +) -> dict[str, object]: + from ..evaluators.symbolica import ( + SymbolicaEvaluatorSettings, + _compile_symbolica_outputs, + _symbolica_evaluator_artifact_manifest, + ) + + settings = _stage_symbolica_settings( + stage, + blueprint, + symbolica_settings or SymbolicaEvaluatorSettings(), + current_stage_position=current_stage_position, + current_stage_count=current_stage_count, + ) + symbolica_started = time.perf_counter() + params = list(stage.parameter_symbols) + + def compile_with(candidate_settings: Any, candidate_label: str) -> Any: + return _compile_symbolica_outputs( + stage.output_expressions, + params, + merge_evaluators_strategy=merge_evaluators_strategy, + verbose_evaluator_build=verbose_evaluator_build, + real_params=stage.real_valued_inputs, + symbolica_settings=candidate_settings, + jit_compile=jit_compile, + label=candidate_label, + progress_callback=progress_callback, + functions={ + (function, arguments): body + for function, arguments, body in stage.symbolica_functions + }, + output_partitions=stage.selector_output_partitions, + native_direct_only=( + getattr(candidate_settings, "backend", None) == "compiled-complex" + and stage.parameter_layout == "stage-local-value-momentum" + ), + ) + + autotune_timing: dict[str, float] = {} + if getattr(settings, "output_chunk_strategy", "uniform") == "measured-stage": + evaluator, autotune_timing = _compile_measured_stage_output_chunks( + settings=settings, + output_count=len(stage.output_expressions), + parameter_count=len(params), + real_params=stage.real_valued_inputs, + label=stage.evaluator_label, + compile_with=compile_with, + progress_callback=progress_callback, + jit_compile=jit_compile, + ) + else: + evaluator = compile_with(settings, stage.evaluator_label) + _compile_native_stage_direct_applications( + evaluator, + stage, + settings, + ) + symbolica_build_s = time.perf_counter() - symbolica_started + artifact_started = time.perf_counter() + manifest = _symbolica_evaluator_artifact_manifest(evaluator, artifact_dir) + artifact_manifest_s = time.perf_counter() - artifact_started + timing = manifest.setdefault("build_timing", {}) + if isinstance(timing, dict): + timing.update(autotune_timing) + timing["symbolica_evaluator_build_s"] = symbolica_build_s + timing["artifact_manifest_s"] = artifact_manifest_s + timing["stage_evaluator_build_s"] = symbolica_build_s + artifact_manifest_s + return manifest + + +def _compile_native_stage_direct_applications( + evaluator: Any, + stage: GenericCompiledStageBlueprint, + settings: Any, +) -> None: + """Produce the direct-only native companion before serialization.""" + + if getattr(settings, "backend", None) != "compiled-complex": + return + if stage.parameter_layout != "stage-local-value-momentum": + return + from ..evaluators.native_direct_cpp import NativeDirectCppParameterKind + from ..evaluators.symbolica_adapters import ( + compile_native_direct_applications, + ) + from ..models.prepared_target import native_prepared_target + + components: list[object | None] = [None] * stage.parameter_count + for component in stage.input_components: + if ( + component.parameter_index >= len(components) + or components[component.parameter_index] is not None + ): + raise ValueError( + "native DirectApplication stage input bindings are invalid" + ) + if component.kind == "model_parameter": + kind = ( + NativeDirectCppParameterKind.REAL_SCALAR + if component.real_valued + else NativeDirectCppParameterKind.COMPLEX_SCALAR + ) + else: + kind = ( + NativeDirectCppParameterKind.REAL_PLANE + if component.real_valued + else NativeDirectCppParameterKind.COMPLEX_PLANE + ) + components[component.parameter_index] = kind + if any(component is None for component in components): + missing = tuple( + index for index, component in enumerate(components) if component is None + ) + raise ValueError( + "native DirectApplication stage input bindings are incomplete: " + f"stage={stage.evaluator_label!r}, parameter_count=" + f"{stage.parameter_count}, input_component_count=" + f"{len(stage.input_components)}, missing_parameter_indices={missing!r}" + ) + + include_features = bool(getattr(settings, "compiled_native", False)) + target = native_prepared_target(include_cpu_features=include_features) + target_triple = str(target["target_triple"]) + cpu_features = tuple(str(item) for item in target["cpu_features"]) + simd_lane_width = ( + 4 if target_triple.startswith("x86_64") and "avx2" in cpu_features else 2 + ) + if not compile_native_direct_applications( + evaluator, + components, + target_triple=target_triple, + cpu_features=cpu_features, + simd_lane_width=simd_lane_width, + ): + raise ValueError( + "compiled native stage did not produce a plane-native DirectApplication" + ) + + +def _compile_measured_stage_output_chunks( + *, + settings: Any, + output_count: int, + parameter_count: int, + real_params: Sequence[int], + label: str, + compile_with: Callable[[Any, str], Any], + progress_callback: Any | None, + jit_compile: bool, +) -> tuple[Any, dict[str, float]]: + base = getattr(settings, "compiled_output_chunk_size", None) + if base is None or getattr(settings, "backend", None) != "jit" or not jit_compile: + uniform = replace(settings, output_chunk_strategy="uniform") + return compile_with(uniform, label), {} + + requested_sizes = ( + int(base), + max(1, int(base) // 2), + max(1, 3 * int(base) // 4), + max(1, 3 * int(base) // 2), + int(base) * 2, + None, + ) + effective_sizes: list[int | None] = [] + for requested in requested_sizes: + effective = ( + None + if requested is None or output_count <= int(requested) + else int(requested) + ) + if effective not in effective_sizes: + effective_sizes.append(effective) + baseline_size = None if output_count <= int(base) else int(base) + + started = time.perf_counter() + candidates: dict[int | None, Any] = {} + for chunk_size in effective_sizes: + suffix = "none" if chunk_size is None else str(chunk_size) + candidate_settings = replace( + settings, + compiled_output_chunk_size=chunk_size, + output_chunk_strategy="uniform", + ) + candidate_label = ( + label if chunk_size == baseline_size else f"{label}_autotune_chunk_{suffix}" + ) + candidates[chunk_size] = compile_with(candidate_settings, candidate_label) + + autotune_batch_size = int( + getattr(settings, "output_chunk_autotune_batch_size", 128) + ) + rows = np.full( + (autotune_batch_size, parameter_count), + complex(0.75, 0.125), + dtype=np.complex128, + ) + if real_params: + rows[:, list(real_params)] = 0.75 + + materialize_started = time.perf_counter() + for evaluator in candidates.values(): + evaluator.evaluate_complex(rows) + materialize_s = time.perf_counter() - materialize_started + + shared_pack_scoring = all( + bool(getattr(evaluator, "supports_complex_profiled", lambda: False)()) + for evaluator in candidates.values() + ) + + def score_candidate(evaluator: Any) -> float: + if shared_pack_scoring: + _output, profile = evaluator.evaluate_complex_profiled(rows) + return max(sum(float(value) for value in profile), 1.0e-9) + probe_started = time.perf_counter() + evaluator.evaluate_complex(rows) + return max(time.perf_counter() - probe_started, 1.0e-6) + + benchmark_started = time.perf_counter() + scores: dict[int | None, float] = {} + repeats: dict[int | None, int] = {} + for chunk_size, evaluator in candidates.items(): + probe_s = max(score_candidate(evaluator), 1.0e-6) + repeats[chunk_size] = max(1, min(256, int(0.01 / probe_s))) + scores[chunk_size] = probe_s + + samples: dict[int | None, list[float]] = { + chunk_size: [] for chunk_size in candidates + } + ordered_sizes = list(candidates) + for round_index in range(5): + rotated = ordered_sizes[round_index:] + ordered_sizes[:round_index] + for chunk_size in rotated: + evaluator = candidates[chunk_size] + count = repeats[chunk_size] + if shared_pack_scoring: + samples[chunk_size].append( + sum(score_candidate(evaluator) for _ in range(count)) / count + ) + else: + sample_started = time.perf_counter() + for _ in range(count): + evaluator.evaluate_complex(rows) + samples[chunk_size].append( + (time.perf_counter() - sample_started) / count + ) + scores = { + chunk_size: sorted(values)[len(values) // 2] + for chunk_size, values in samples.items() + } + benchmark_s = time.perf_counter() - benchmark_started + selected_size = _select_measured_chunk_candidate( + scores, + baseline_size=baseline_size, + minimum_gain=0.05, + ) + selected = candidates[selected_size] + baseline_score = scores[baseline_size] + selected_score = scores[selected_size] + autotune_s = time.perf_counter() - started + if progress_callback is not None: + progress_callback( + { + "stage": "chunk autotune", + "item": ( + f"{label} base={baseline_size or 'none'} " + f"selected={selected_size or 'none'} " + f"gain={(1.0 - selected_score / baseline_score):.1%}" + + (" shared-pack" if shared_pack_scoring else "") + ), + } + ) + evaluator_timing = getattr(selected, "build_timing", None) + if isinstance(evaluator_timing, dict): + evaluator_timing.update( + { + "output_chunk_autotune_s": autotune_s, + "output_chunk_autotune_materialize_s": materialize_s, + "output_chunk_autotune_benchmark_s": benchmark_s, + "output_chunk_autotune_candidate_count": float(len(candidates)), + "output_chunk_autotune_batch_size": float(autotune_batch_size), + "output_chunk_autotune_baseline_size": float(baseline_size or 0), + "output_chunk_autotune_selected_size": float(selected_size or 0), + "output_chunk_autotune_baseline_us": baseline_score * 1.0e6, + "output_chunk_autotune_selected_us": selected_score * 1.0e6, + "output_chunk_autotune_gain": 1.0 - selected_score / baseline_score, + "output_chunk_autotune_shared_pack_scoring": float(shared_pack_scoring), + } + ) + return selected, { + "output_chunk_autotune_s": autotune_s, + "output_chunk_autotune_materialize_s": materialize_s, + "output_chunk_autotune_benchmark_s": benchmark_s, + "output_chunk_autotune_candidate_count": float(len(candidates)), + "output_chunk_autotune_batch_size": float(autotune_batch_size), + "output_chunk_autotune_baseline_size": float(baseline_size or 0), + "output_chunk_autotune_selected_size": float(selected_size or 0), + "output_chunk_autotune_baseline_us": baseline_score * 1.0e6, + "output_chunk_autotune_selected_us": selected_score * 1.0e6, + "output_chunk_autotune_gain": 1.0 - selected_score / baseline_score, + "output_chunk_autotune_shared_pack_scoring": float(shared_pack_scoring), + } + + +def _select_measured_chunk_candidate( + scores: Mapping[int | None, float], + *, + baseline_size: int | None, + minimum_gain: float, +) -> int | None: + if baseline_size not in scores: + raise ValueError("measured chunk scores do not include the baseline") + best_size = min(scores, key=scores.__getitem__) + if scores[best_size] <= scores[baseline_size] * (1.0 - minimum_gain): + return best_size + return baseline_size diff --git a/pyamplicol/src/pyamplicol/generation/stage_compiler.py b/pyamplicol/src/pyamplicol/generation/stage_compiler.py new file mode 100644 index 0000000000..28566078cd --- /dev/null +++ b/pyamplicol/src/pyamplicol/generation/stage_compiler.py @@ -0,0 +1,33 @@ +# SPDX-License-Identifier: 0BSD +"""Compatibility facade for stage blueprint and evaluator compilation.""" + +from __future__ import annotations + +from .stage_artifacts import ( + build_and_write_generic_stage_evaluator_artifacts, + write_generic_stage_evaluator_artifacts, + write_model_parameter_evaluator_artifact, +) +from .stage_planning import ( + _fanout_aware_current_order as _fanout_aware_current_order, +) +from .stage_planning import ( + build_generic_stage_compiler_blueprint, +) +from .stage_types import ( + GenericCompiledStageBlueprint, + GenericStageCompilerBlueprint, + GenericStageInputComponent, + GenericStageOutputSlot, +) + +__all__ = [ + "GenericCompiledStageBlueprint", + "GenericStageCompilerBlueprint", + "GenericStageInputComponent", + "GenericStageOutputSlot", + "build_and_write_generic_stage_evaluator_artifacts", + "build_generic_stage_compiler_blueprint", + "write_generic_stage_evaluator_artifacts", + "write_model_parameter_evaluator_artifact", +] diff --git a/pyamplicol/src/pyamplicol/generation/stage_expressions.py b/pyamplicol/src/pyamplicol/generation/stage_expressions.py new file mode 100644 index 0000000000..7b15e5d294 --- /dev/null +++ b/pyamplicol/src/pyamplicol/generation/stage_expressions.py @@ -0,0 +1,713 @@ +# SPDX-License-Identifier: 0BSD +"""Symbolic expression construction for current and amplitude stages.""" + +from __future__ import annotations + +from collections.abc import Mapping, Sequence +from typing import Any + +from ..models._physics_ir import ContractionIR, PropagatorIR +from ..models.base import Model +from ..models.expressions import _exact_complex_expression +from .contracts import ( + runtime_coupling_parameter_names as _runtime_coupling_parameter_names, +) +from .dag_types import GenericDAG +from .stage_parameters import ( + _amplitude_stage_model_parameter_records, + _contract_components, + _coupling, + _current_stage_model_parameter_records, + _dict, + _expression_previews, + _global_stage_inputs, + _list, + _model_symbolica_functions, + _momentum_components, + _project_components, + _runtime_coupling, + _specialize_stage_symbolica_functions, + _stage_input_momentum_slot_ids, + _stage_local_inputs, + _sum_components, + _value_components, +) +from .stage_types import ( + GenericCompiledStageBlueprint, + GenericStageOutputSlot, + _RuntimeParameterizedModel, +) + + +def _compile_current_stage_blueprint( + dag: GenericDAG, + model: Model, + stage: dict[str, Any], + *, + value_slots: dict[int, dict[str, Any]], + current_slots: dict[int, dict[str, Any]], + momentum_slots: dict[int, dict[str, Any]], + global_value_component_count: int, + global_momentum_parameter_count: int, + model_parameter_records: Sequence[dict[str, Any]], + global_parameter_symbols: Sequence[Any], + global_value_symbols: Sequence[Any], + global_momentum_symbols: Sequence[Any], + global_model_parameter_symbols: Mapping[str, Any], + global_real_valued_inputs: Sequence[int], + stage_local_parameter_layout: bool, +) -> GenericCompiledStageBlueprint: + blockers: list[str] = [] + outputs: list[Any] = [] + output_slots: list[GenericStageOutputSlot] = [] + interactions_compacted = bool(stage.get("interactions_compacted", False)) + interactions = ( + [] + if interactions_compacted + else [_dict(item) for item in _list(stage["interactions"])] + ) + interaction_ids = ( + tuple(int(value) for value in _list(stage.get("interaction_ids", []))) + if interactions_compacted + else tuple(int(interaction["interaction_id"]) for interaction in interactions) + ) + input_value_slot_ids = tuple( + int(value) for value in _list(stage["input_value_slot_ids"]) + ) + input_momentum_slot_ids = ( + tuple(int(value) for value in _list(stage.get("input_momentum_slot_ids", []))) + if interactions_compacted + else _stage_input_momentum_slot_ids(interactions) + ) + output_slot_ids = tuple( + int(value) for value in _list(stage["output_value_slot_ids"]) + ) + output_slots_by_current: dict[int, list[dict[str, Any]]] = {} + for slot_id in output_slot_ids: + slot = value_slots[int(slot_id)] + output_slots_by_current.setdefault(int(slot["current_id"]), []).append(slot) + stage_model_parameter_records = ( + _current_stage_model_parameter_records( + model, + model_parameter_records, + dag=dag, + interactions=interactions, + interaction_ids=interaction_ids, + output_slots_by_current=output_slots_by_current, + current_slots=current_slots, + ) + if stage_local_parameter_layout + else model_parameter_records + ) + local_inputs = ( + _stage_local_inputs( + value_slot_ids=input_value_slot_ids, + momentum_slot_ids=input_momentum_slot_ids, + value_slots=value_slots, + momentum_slots=momentum_slots, + global_value_component_count=global_value_component_count, + global_momentum_parameter_count=global_momentum_parameter_count, + model_parameter_records=stage_model_parameter_records, + ) + if stage_local_parameter_layout + else _global_stage_inputs( + parameter_symbols=global_parameter_symbols, + value_symbols=global_value_symbols, + momentum_symbols=global_momentum_symbols, + model_parameter_symbols=global_model_parameter_symbols, + value_parameter_count=global_value_component_count, + momentum_parameter_count=len(global_momentum_symbols), + model_parameter_count=len(model_parameter_records), + real_valued_inputs=global_real_valued_inputs, + ) + ) + stage_model = ( + model.with_runtime_parameters(local_inputs.model_parameter_symbols) + if hasattr(model, "with_runtime_parameters") + else _RuntimeParameterizedModel(model, local_inputs.model_parameter_symbols) + ) + interactions_by_result: dict[int, list[dict[str, Any] | int]] = {} + if interactions_compacted: + for interaction_id in interaction_ids: + interactions_by_result.setdefault( + int(dag.interactions[interaction_id].result_id), + [], + ).append(interaction_id) + else: + for interaction in interactions: + interactions_by_result.setdefault( + int(interaction["result_current_id"]), + [], + ).append(interaction) + value_components_by_slot_id = { + int(slot_id): _value_components( + value_slots[int(slot_id)], + local_inputs.value_symbols, + ) + for slot_id in input_value_slot_ids + } + momentum_components_by_slot_id = { + int(slot_id): _momentum_components( + int(slot_id), + local_inputs.momentum_symbols, + momentum_slots, + by_slot_id=True, + ) + for slot_id in input_momentum_slot_ids + } + momentum_components_by_mask = { + int(slot["momentum_mask"]): momentum_components_by_slot_id[int(slot_id)] + for slot_id, slot in momentum_slots.items() + if int(slot_id) in momentum_components_by_slot_id + } + input_value_slot_by_current_id = { + int(value_slots[slot_id]["current_id"]): int(slot_id) + for slot_id in input_value_slot_ids + } + momentum_slot_by_mask = { + int(slot["momentum_mask"]): int(slot_id) + for slot_id, slot in momentum_slots.items() + } + compact_coupling_cache: dict[ + tuple[int, tuple[int, ...], tuple[float, ...]], + tuple[Any, Any], + ] = {} + compact_evaluation_cache: dict[int, tuple[Any, ...]] = {} + evaluation_groups_by_current = tuple( + ( + current_id, + tuple( + sorted( + { + ( + int(interaction.evaluation_group_id) + if interaction.evaluation_group_id is not None + else -(interaction.id + 1) + ) + for interaction_item in interactions_by_result[current_id] + for interaction in ( + dag.interactions[ + _interaction_item_id( + interaction_item, + compacted=interactions_compacted, + ) + ], + ) + if interaction.evaluation_factor != (0.0, 0.0) + } + ) + ), + ) + for current_id in sorted(interactions_by_result) + ) + + for current_id in sorted(interactions_by_result): + current_slot = current_slots[current_id] + dimension = int(current_slot["dimension"]) + total = tuple(0j for _ in range(dimension)) + for interaction_item in interactions_by_result[current_id]: + interaction_id = _interaction_item_id( + interaction_item, + compacted=interactions_compacted, + ) + try: + contribution = _compact_interaction_contribution( + dag, + stage_model, + interaction_id, + value_components_by_slot_id=value_components_by_slot_id, + input_value_slot_by_current_id=input_value_slot_by_current_id, + momentum_components_by_slot_id=momentum_components_by_slot_id, + momentum_slot_by_mask=momentum_slot_by_mask, + model_parameter_symbols=local_inputs.model_parameter_symbols, + coupling_cache=compact_coupling_cache, + evaluation_cache=compact_evaluation_cache, + ) + except ValueError as error: + blockers.append(f"interaction {interaction_id}: {error}") + continue + total = _sum_components(total, contribution) + result_slots = output_slots_by_current.get(current_id, ()) + for slot in result_slots: + variant = str(slot["variant"]) + try: + components = ( + stage_model.propagator_component_expression( + int(current_slot["particle_id"]), + total, + momentum_components_by_mask[int(current_slot["momentum_mask"])], + chirality=int(current_slot["chirality"]), + propagator=PropagatorIR.from_json_dict( + _dict(slot["propagator"]) + ), + ) + if variant == "propagated" + else total + ) + except ValueError as error: + blockers.append(f"value slot {slot['value_slot_id']}: {error}") + continue + start = len(outputs) + outputs.extend(components) + output_slots.append( + GenericStageOutputSlot( + value_slot_id=int(slot["value_slot_id"]), + current_id=current_id, + variant=variant, + component_start=int(slot["component_start"]), + component_stop=int(slot["component_stop"]), + output_start=start, + output_stop=len(outputs), + ) + ) + + specialized_outputs, symbolica_functions = _specialize_stage_symbolica_functions( + outputs, + _model_symbolica_functions(stage_model), + ) + return GenericCompiledStageBlueprint( + stage_index=int(stage["stage_index"]), + stage_kind=str(stage["stage_kind"]), + subset_size=int(stage["subset_size"]), + evaluator_label=( + f"generic_stage_{int(stage['stage_index'])}_subset_{int(stage['subset_size'])}" + ), + parameter_layout=( + "stage-local-value-momentum" + if stage_local_parameter_layout + else "global-value-momentum" + ), + output_length=len(outputs), + output_slots=tuple(output_slots), + input_value_slot_ids=input_value_slot_ids, + output_value_slot_ids=output_slot_ids, + interaction_ids=interaction_ids, + input_components=local_inputs.input_components, + parameter_count=len(local_inputs.parameter_symbols), + value_parameter_count=local_inputs.value_parameter_count, + momentum_parameter_count=local_inputs.momentum_parameter_count, + model_parameter_count=local_inputs.model_parameter_count, + real_valued_inputs=local_inputs.real_valued_inputs, + expression_ready=not blockers, + blockers=tuple(blockers), + first_output_previews=_expression_previews(specialized_outputs), + evaluation_groups_by_current=evaluation_groups_by_current, + parameter_symbols=local_inputs.parameter_symbols, + output_expressions=specialized_outputs, + symbolica_functions=symbolica_functions, + ) + + +def _compile_amplitude_stage_blueprint( + model: Model, + stage: dict[str, Any], + *, + value_slots: dict[int, dict[str, Any]], + global_value_component_count: int, + global_momentum_parameter_count: int, + model_parameter_records: Sequence[dict[str, Any]], + global_parameter_symbols: Sequence[Any], + global_value_symbols: Sequence[Any], + global_model_parameter_symbols: Mapping[str, Any], + global_real_valued_inputs: Sequence[int], + stage_local_parameter_layout: bool, +) -> GenericCompiledStageBlueprint: + blockers: list[str] = [] + outputs: list[Any] = [] + output_slots: list[GenericStageOutputSlot] = [] + input_value_slot_ids = tuple( + sorted( + { + int(root[side]["value_slot_id"]) + for root in (_dict(item) for item in _list(stage["roots"])) + for side in ("left_value_slot", "right_value_slot") + } + ) + ) + local_inputs = ( + _stage_local_inputs( + value_slot_ids=input_value_slot_ids, + momentum_slot_ids=(), + value_slots=value_slots, + momentum_slots={}, + global_value_component_count=global_value_component_count, + global_momentum_parameter_count=global_momentum_parameter_count, + model_parameter_records=( + _amplitude_stage_model_parameter_records( + model_parameter_records, + roots=tuple(_dict(item) for item in _list(stage["roots"])), + ) + if stage_local_parameter_layout + else model_parameter_records + ), + ) + if stage_local_parameter_layout + else _global_stage_inputs( + parameter_symbols=global_parameter_symbols, + value_symbols=global_value_symbols, + momentum_symbols=(), + model_parameter_symbols=global_model_parameter_symbols, + value_parameter_count=global_value_component_count, + momentum_parameter_count=0, + model_parameter_count=len(model_parameter_records), + real_valued_inputs=global_real_valued_inputs, + ) + ) + stage_model = ( + model.with_runtime_parameters(local_inputs.model_parameter_symbols) + if hasattr(model, "with_runtime_parameters") + else _RuntimeParameterizedModel(model, local_inputs.model_parameter_symbols) + ) + for root in (_dict(item) for item in _list(stage["roots"])): + try: + output = _amplitude_root_expression( + stage_model, + root, + value_symbols=local_inputs.value_symbols, + model_parameter_symbols=local_inputs.model_parameter_symbols, + value_slots=value_slots, + ) + except (TypeError, ValueError) as error: + blockers.append(f"amplitude root {root['root_id']}: {error}") + continue + start = len(outputs) + outputs.append(output) + output_slots.append( + GenericStageOutputSlot( + value_slot_id=-1, + current_id=-1, + variant="amplitude-root", + component_start=int(root["output_index"]), + component_stop=int(root["output_index"]) + 1, + output_start=start, + output_stop=len(outputs), + ) + ) + return GenericCompiledStageBlueprint( + stage_index=0, + stage_kind=str(stage["stage_kind"]), + subset_size=None, + evaluator_label="generic_amplitude_stage", + parameter_layout=( + "stage-local-value-momentum" + if stage_local_parameter_layout + else "global-value-momentum" + ), + output_length=len(outputs), + output_slots=tuple(output_slots), + input_value_slot_ids=input_value_slot_ids, + output_value_slot_ids=(), + interaction_ids=(), + input_components=local_inputs.input_components, + parameter_count=len(local_inputs.parameter_symbols), + value_parameter_count=local_inputs.value_parameter_count, + momentum_parameter_count=local_inputs.momentum_parameter_count, + model_parameter_count=local_inputs.model_parameter_count, + real_valued_inputs=local_inputs.real_valued_inputs, + expression_ready=not blockers, + blockers=tuple(blockers), + first_output_previews=_expression_previews(outputs), + parameter_symbols=local_inputs.parameter_symbols, + output_expressions=tuple(outputs), + ) + + +def _interaction_contribution( + dag: GenericDAG, + model: Model, + interaction: dict[str, Any], + *, + value_components_by_slot_id: Mapping[int, tuple[Any, ...]], + momentum_components_by_slot_id: Mapping[int, tuple[Any, ...]], + model_parameter_symbols: Mapping[str, Any], +) -> tuple[Any, ...]: + left_slot = _dict(interaction["left_value_slot"]) + right_slot = _dict(interaction["right_value_slot"]) + left = value_components_by_slot_id[int(left_slot["value_slot_id"])] + right = value_components_by_slot_id[int(right_slot["value_slot_id"])] + momenta = _dict(interaction["momentum_slots"]) + left_current = dag.currents[int(interaction["left_current_id"])] + right_current = dag.currents[int(interaction["right_current_id"])] + result_current = dag.currents[int(interaction["result_current_id"])] + components = model.vertex_component_expression( + int(interaction["vertex_kind"]), + left, + right, + result_particle_id=int(result_current.index.particle_id), + result_chirality=int(result_current.index.chirality), + left_chirality=int(left_current.index.chirality), + right_chirality=int(right_current.index.chirality), + coupling=_runtime_coupling(interaction, model_parameter_symbols), + left_momentum=momentum_components_by_slot_id[int(momenta["left"])], + right_momentum=momentum_components_by_slot_id[int(momenta["right"])], + ) + color_weight = _coupling(interaction.get("color_weight")) + if color_weight == (1.0, 0.0): + return components + weight = _exact_complex_expression(complex(*color_weight)) + return tuple(weight * component for component in components) + + +def _compact_interaction_contribution( + dag: GenericDAG, + model: Model, + interaction_id: int, + *, + value_components_by_slot_id: Mapping[int, tuple[Any, ...]], + input_value_slot_by_current_id: Mapping[int, int], + momentum_components_by_slot_id: Mapping[int, tuple[Any, ...]], + momentum_slot_by_mask: Mapping[int, int], + model_parameter_symbols: Mapping[str, Any], + coupling_cache: dict[ + tuple[int, tuple[int, ...], tuple[float, ...]], + tuple[Any, Any], + ], + evaluation_cache: dict[int, tuple[Any, ...]], +) -> tuple[Any, ...]: + interaction = dag.interactions[interaction_id] + evaluation_group_id = interaction.evaluation_group_id + canonical_components = ( + None + if evaluation_group_id is None + else evaluation_cache.get(evaluation_group_id) + ) + evaluation_factor = complex(*interaction.evaluation_factor) + if evaluation_factor == 0j: + # Authenticated numerical zero-current relations propagate an exact + # zero attachment into downstream interactions. Do not evaluate a + # kernel whose contribution is certified to be zero. + return tuple( + 0j for _component in range(dag.currents[interaction.result_id].dimension) + ) + if canonical_components is None: + left_current = dag.currents[interaction.left_id] + right_current = dag.currents[interaction.right_id] + result_current = dag.currents[interaction.result_id] + left = value_components_by_slot_id[ + input_value_slot_by_current_id[interaction.left_id] + ] + right = value_components_by_slot_id[ + input_value_slot_by_current_id[interaction.right_id] + ] + coupling_key = ( + int(interaction.vertex_kind), + interaction.vertex_particles, + interaction.coupling, + ) + coupling = coupling_cache.get(coupling_key) + if coupling is None: + resolved_coupling = list(interaction.coupling) + names = _runtime_coupling_parameter_names( + interaction.vertex_kind, + interaction.vertex_particles, + interaction.coupling, + model=model, + ) + for index, name in enumerate(names): + if isinstance(name, str) and name in model_parameter_symbols: + resolved_coupling[index] = model_parameter_symbols[name] + coupling = (resolved_coupling[0], resolved_coupling[1]) + coupling_cache[coupling_key] = coupling + components = model.vertex_component_expression( + int(interaction.vertex_kind), + left, + right, + result_particle_id=int(result_current.index.particle_id), + result_chirality=int(result_current.index.chirality), + left_chirality=int(left_current.index.chirality), + right_chirality=int(right_current.index.chirality), + coupling=coupling, + left_momentum=momentum_components_by_slot_id[ + momentum_slot_by_mask[left_current.index.momentum_mask] + ], + right_momentum=momentum_components_by_slot_id[ + momentum_slot_by_mask[right_current.index.momentum_mask] + ], + ) + if evaluation_factor == 1.0 + 0.0j: + canonical_components = components + else: + exact_factor = _exact_complex_expression(evaluation_factor) + canonical_components = tuple( + component / exact_factor for component in components + ) + if evaluation_group_id is not None: + evaluation_cache[evaluation_group_id] = canonical_components + color_weight = complex(*interaction.color_weight) + if color_weight == evaluation_factor == 1.0 + 0.0j: + return canonical_components + weight = _exact_complex_expression(color_weight) * _exact_complex_expression( + evaluation_factor + ) + return tuple(weight * component for component in canonical_components) + + +def _amplitude_root_expression( + model: Model, + root: dict[str, Any], + *, + value_symbols: Sequence[Any] | Mapping[int, tuple[Any, ...]], + model_parameter_symbols: Mapping[str, Any], + value_slots: dict[int, dict[str, Any]], +) -> Any: + left_value_slot = _dict(root["left_value_slot"]) + right_value_slot = _dict(root["right_value_slot"]) + left = _value_components(left_value_slot, value_symbols) + right = _value_components(right_value_slot, value_symbols) + left_value_metadata = _amplitude_value_slot_metadata( + left_value_slot, + value_slots, + ) + right_value_metadata = _amplitude_value_slot_metadata( + right_value_slot, + value_slots, + ) + kind = str(root["kind"]) + contraction_ir = _root_contraction_ir(root) + coupling = _runtime_coupling(root, model_parameter_symbols) + color_weight = _coupling(root.get("color_weight")) + if kind == "direct-contraction": + _validate_direct_contraction_metadata( + contraction_ir, + left_value_slot, + right_value_slot, + left_value_metadata, + right_value_metadata, + ) + return _apply_amplitude_color_weight( + color_weight, + _contract_components(contraction_ir, left, right), + ) + if kind == "vertex-closure": + _validate_scalar_projection(contraction_ir) + vertex_kind = root.get("vertex_kind") + particles = root.get("vertex_particles") + if ( + vertex_kind is None + or not isinstance(particles, list) + or len(particles) != 3 + ): + raise ValueError("vertex closure is missing vertex metadata") + components = model.vertex_component_expression( + int(vertex_kind), + left, + right, + result_particle_id=int(particles[2]), + result_chirality=0, + left_chirality=int(left_value_metadata["chirality"]), + right_chirality=int(right_value_metadata["chirality"]), + coupling=coupling, + ) + return _apply_amplitude_color_weight( + color_weight, + _project_components(contraction_ir, components), + ) + raise ValueError(f"unsupported amplitude root kind {kind!r}") + + +def _root_contraction_ir(root: Mapping[str, Any]) -> ContractionIR: + payload = root.get("contraction_ir") + if not isinstance(payload, Mapping): + raise TypeError("amplitude root contraction_ir must be a mapping") + contraction_ir = ContractionIR.from_json_dict(payload) + if root.get("contraction") != contraction_ir.name: + raise ValueError( + "amplitude root contraction projection does not match ContractionIR" + ) + return contraction_ir + + +def _amplitude_value_slot_metadata( + reference: Mapping[str, Any], + value_slots: Mapping[int, dict[str, Any]], +) -> dict[str, Any]: + try: + value_slot_id = int(reference["value_slot_id"]) + return value_slots[value_slot_id] + except (KeyError, TypeError, ValueError) as error: + raise ValueError("amplitude root references an unknown value slot") from error + + +def _validate_direct_contraction_metadata( + contraction_ir: ContractionIR, + left_reference: Mapping[str, Any], + right_reference: Mapping[str, Any], + left_value_slot: Mapping[str, Any], + right_value_slot: Mapping[str, Any], +) -> None: + dimension = len(contraction_ir.coefficients) + for side, reference, value_slot, basis in ( + ( + "left", + left_reference, + left_value_slot, + contraction_ir.left_basis, + ), + ( + "right", + right_reference, + right_value_slot, + contraction_ir.right_basis, + ), + ): + if ( + int(reference.get("dimension", -1)) != dimension + or int(value_slot.get("dimension", -1)) != dimension + ): + raise ValueError( + f"direct contraction {side} value slot dimension does not match " + "ContractionIR" + ) + propagator = value_slot.get("propagator") + if not isinstance(propagator, Mapping) or propagator.get("basis") != basis: + raise ValueError( + f"direct contraction {side} value slot basis does not match " + "ContractionIR" + ) + + left_chirality = int(left_value_slot["chirality"]) + right_chirality = int(right_value_slot["chirality"]) + relation_matches = ( + contraction_ir.chirality_relation == "any" + or ( + contraction_ir.chirality_relation == "equal" + and left_chirality == right_chirality + ) + or ( + contraction_ir.chirality_relation == "opposite" + and left_chirality == -right_chirality + ) + ) + if not relation_matches: + raise ValueError("direct contraction violates ContractionIR chirality relation") + + +def _validate_scalar_projection(contraction_ir: ContractionIR) -> None: + if ( + len(contraction_ir.coefficients) != 1 + or contraction_ir.left_basis != "scalar" + or contraction_ir.right_basis != "scalar" + or contraction_ir.chirality_relation != "any" + ): + raise ValueError("vertex closure does not declare a scalar projection IR") + + +def _apply_amplitude_color_weight( + color_weight: tuple[Any, Any], + expression: Any, +) -> Any: + if color_weight == (1.0, 0.0): + return expression + return _exact_complex_expression(complex(*color_weight)) * expression + + +def _interaction_item_id( + item: dict[str, Any] | int, + *, + compacted: bool, +) -> int: + if compacted: + if not isinstance(item, int): + raise TypeError("compacted stage interaction must be an integer id") + return item + return int(_dict(item)["interaction_id"]) diff --git a/pyamplicol/src/pyamplicol/generation/stage_parameters.py b/pyamplicol/src/pyamplicol/generation/stage_parameters.py new file mode 100644 index 0000000000..881af43ba2 --- /dev/null +++ b/pyamplicol/src/pyamplicol/generation/stage_parameters.py @@ -0,0 +1,638 @@ +# SPDX-License-Identifier: 0BSD +"""Stage-local parameter layouts and expression utility functions.""" + +from __future__ import annotations + +from collections.abc import Iterable, Mapping, Sequence +from typing import Any + +from .._internal.physics.parameters import ParamBuilder +from .._internal.physics.symbols import symbols +from ..models._physics_ir import ContractionIR +from ..models.base import Model +from ..models.expressions import _exact_complex_expression +from .contracts import StageCompilationInput +from .contracts import ( + runtime_coupling_parameter_names as _runtime_coupling_parameter_names, +) +from .dag_types import GenericDAG +from .stage_types import GenericStageInputComponent, _StageLocalInputs + +_EXPRESSION_PREVIEW_LIMIT = 512 +_MODEL_FUNCTION_INLINE_MAX_BYTES = 1024 + + +def _contract_components( + contraction_ir: ContractionIR, + left: Sequence[Any], + right: Sequence[Any], +) -> Any: + dimension = len(contraction_ir.coefficients) + if len(left) != dimension or len(right) != dimension: + raise ValueError( + f"contraction {contraction_ir.name!r} requires {dimension} components, " + f"got {len(left)} and {len(right)}" + ) + return _sum_contraction_terms( + _apply_contraction_coefficient( + coefficient, + left[index] * right[index], + ) + for index, coefficient in enumerate(contraction_ir.coefficients) + ) + + +def _project_components( + contraction_ir: ContractionIR, + components: Sequence[Any], +) -> Any: + dimension = len(contraction_ir.coefficients) + if len(components) != dimension: + raise ValueError( + f"projection {contraction_ir.name!r} requires {dimension} components, " + f"got {len(components)}" + ) + return _sum_contraction_terms( + _apply_contraction_coefficient(coefficient, components[index]) + for index, coefficient in enumerate(contraction_ir.coefficients) + ) + + +def _apply_contraction_coefficient( + coefficient: tuple[float, float], + expression: Any, +) -> Any: + if coefficient == (1.0, 0.0): + return expression + if coefficient == (-1.0, 0.0): + return -expression + if isinstance(expression, int | float | complex): + return complex(*coefficient) * expression + return _exact_complex_expression(complex(*coefficient)) * expression + + +def _sum_contraction_terms(terms: Iterable[Any]) -> Any: + iterator = iter(terms) + result = next(iterator) + for term in iterator: + result = result + term + return result + + +def _stage_input_momentum_slot_ids( + interactions: Sequence[dict[str, Any]], +) -> tuple[int, ...]: + slot_ids: set[int] = set() + for interaction in interactions: + momentum_slots = _dict(interaction["momentum_slots"]) + for key in ("left", "right", "result"): + slot_ids.add(int(momentum_slots[key])) + return tuple(sorted(slot_ids)) + + +def _current_stage_model_parameter_records( + model: Model, + model_parameter_records: Sequence[dict[str, Any]], + *, + dag: GenericDAG, + interactions: Sequence[dict[str, Any]], + interaction_ids: Sequence[int], + output_slots_by_current: Mapping[int, Sequence[dict[str, Any]]], + current_slots: Mapping[int, dict[str, Any]], +) -> tuple[dict[str, Any], ...]: + used_names = _coupling_parameter_names_used_by_records(interactions) + if not interactions: + processed_signatures: set[tuple[int, tuple[int, ...], tuple[float, ...]]] = ( + set() + ) + for interaction_id in interaction_ids: + interaction = dag.interactions[int(interaction_id)] + signature = ( + int(interaction.vertex_kind), + interaction.vertex_particles, + interaction.coupling, + ) + if signature in processed_signatures: + continue + processed_signatures.add(signature) + used_names.update( + name + for name in _runtime_coupling_parameter_names( + interaction.vertex_kind, + interaction.vertex_particles, + interaction.coupling, + model=model, + ) + if isinstance(name, str) + ) + for current_id, slots in output_slots_by_current.items(): + if not any(str(slot["variant"]) == "propagated" for slot in slots): + continue + current_slot = current_slots[int(current_id)] + used_names.update( + _particle_model_parameter_names( + model, + int(current_slot["particle_id"]), + ) + ) + return _filter_model_parameter_records(model_parameter_records, used_names) + + +def _amplitude_stage_model_parameter_records( + model_parameter_records: Sequence[dict[str, Any]], + *, + roots: Sequence[dict[str, Any]], +) -> tuple[dict[str, Any], ...]: + used_names = _coupling_parameter_names_used_by_records(roots) + return _filter_model_parameter_records(model_parameter_records, used_names) + + +def _coupling_parameter_names_used_by_records( + records: Sequence[dict[str, Any]], +) -> set[str]: + used_names: set[str] = set() + for record in records: + names = record.get("coupling_parameter_names") + if not isinstance(names, list): + continue + for name in names: + if isinstance(name, str): + used_names.add(name) + return used_names + + +def _particle_model_parameter_names(model: Model, pdg: int) -> tuple[str, ...]: + runtime_names = getattr(model, "runtime_parameter_names_for_particle", None) + if callable(runtime_names): + return tuple(str(name) for name in runtime_names(int(pdg))) + try: + particle = model.particle(pdg) + except KeyError: + return () + return ( + f"particle.{int(particle.pdg)}.mass", + f"particle.{int(particle.pdg)}.width", + ) + + +def _filter_model_parameter_records( + model_parameter_records: Sequence[dict[str, Any]], + used_names: set[str], +) -> tuple[dict[str, Any], ...]: + return tuple( + record + for record in sorted( + model_parameter_records, + key=lambda item: int(item["parameter_index"]), + ) + if str(record.get("runtime_name", record["name"])) in used_names + and not ( + record.get("complex_domain") == "real" + and record.get("complex_component") == "imag" + ) + and not ( + record.get("complex_domain") == "imaginary" + and record.get("complex_component") == "real" + ) + ) + + +def _logical_model_parameter_symbols( + model_parameter_records: Sequence[dict[str, Any]], + slot_symbols: Mapping[str, Any], +) -> dict[str, Any]: + logical_symbols: dict[str, Any] = {} + complex_components: dict[str, dict[str, Any]] = {} + complex_domains: dict[str, str] = {} + for record in model_parameter_records: + slot_name = str(record["name"]) + symbol = slot_symbols[slot_name] + runtime_name = record.get("runtime_name") + component = record.get("complex_component") + if isinstance(runtime_name, str) and component in {"real", "imag"}: + complex_components.setdefault(runtime_name, {})[str(component)] = symbol + domain = str(record.get("complex_domain", "complex")) + previous = complex_domains.setdefault(runtime_name, domain) + if previous != domain: + raise ValueError( + f"runtime model parameter {runtime_name!r} has conflicting domains" + ) + else: + logical_symbols[slot_name] = symbol + for runtime_name, components in complex_components.items(): + domain = complex_domains[runtime_name] + if "real" not in components and domain == "imaginary": + components["real"] = 0.0 + if "imag" not in components and domain == "real": + components["imag"] = 0.0 + if set(components) != {"real", "imag"}: + raise ValueError( + f"runtime model parameter {runtime_name!r} is missing a real " + "or imaginary slot" + ) + logical_symbols[runtime_name] = components["real"] + 1j * components["imag"] + return logical_symbols + + +def _stage_local_inputs( + *, + value_slot_ids: Sequence[int], + momentum_slot_ids: Sequence[int], + value_slots: Mapping[int, dict[str, Any]], + momentum_slots: Mapping[int, dict[str, Any]], + global_value_component_count: int, + global_momentum_parameter_count: int, + model_parameter_records: Sequence[dict[str, Any]], +) -> _StageLocalInputs: + builder = ParamBuilder() + input_components: list[GenericStageInputComponent] = [] + value_symbols: dict[int, tuple[Any, ...]] = {} + momentum_symbols: dict[int, tuple[Any, ...]] = {} + model_parameter_slot_symbols: dict[str, Any] = {} + + value_spans = tuple( + ( + int(value_slot_id), + int(value_slots[int(value_slot_id)]["component_start"]), + int(value_slots[int(value_slot_id)]["component_stop"]), + ) + for value_slot_id in value_slot_ids + ) + momentum_spans = tuple( + ( + int(momentum_slot_id), + int(momentum_slots[int(momentum_slot_id)]["component_start"]), + int(momentum_slots[int(momentum_slot_id)]["component_stop"]), + ) + for momentum_slot_id in momentum_slot_ids + ) + sorted_model_parameter_records = tuple( + sorted( + model_parameter_records, + key=lambda item: int(item["parameter_index"]), + ) + ) + value_parameter_count = sum(stop - start for _, start, stop in value_spans) + momentum_parameter_count = sum(stop - start for _, start, stop in momentum_spans) + model_parameter_count = len(sorted_model_parameter_records) + parameter_count = ( + value_parameter_count + momentum_parameter_count + model_parameter_count + ) + parameter_symbols = ( + builder.add_parameter_list( + ("artifact_schema_v3_stage", "inputs"), + parameter_count, + role="generic_stage_input_storage", + ) + if parameter_count + else () + ) + real_parameter_start = value_parameter_count + builder.real_valued_inputs.extend(range(real_parameter_start, parameter_count)) + parameter_cursor = 0 + + for value_slot_id, start, stop in value_spans: + length = stop - start + local_symbols = parameter_symbols[parameter_cursor : parameter_cursor + length] + value_symbols[value_slot_id] = local_symbols + parameter_start = len(input_components) + for component, global_component in enumerate(range(start, stop)): + input_components.append( + GenericStageInputComponent( + kind="value", + source_id=int(value_slot_id), + component=component, + global_component=global_component, + parameter_index=parameter_start + component, + real_valued=False, + ) + ) + parameter_cursor += length + + for momentum_slot_id, start, stop in momentum_spans: + length = stop - start + local_symbols = parameter_symbols[parameter_cursor : parameter_cursor + length] + momentum_symbols[momentum_slot_id] = local_symbols + parameter_start = len(input_components) + for component, local_component in enumerate(range(start, stop)): + input_components.append( + GenericStageInputComponent( + kind="momentum", + source_id=int(momentum_slot_id), + component=component, + global_component=global_value_component_count + local_component, + parameter_index=parameter_start + component, + real_valued=True, + ) + ) + parameter_cursor += length + + model_parameter_global_start = ( + global_value_component_count + global_momentum_parameter_count + ) + for record in sorted_model_parameter_records: + name = str(record["name"]) + parameter_index = int(record["parameter_index"]) + symbol = parameter_symbols[parameter_cursor] + model_parameter_slot_symbols[name] = symbol + input_components.append( + GenericStageInputComponent( + kind="model_parameter", + source_id=parameter_index, + component=0, + global_component=model_parameter_global_start + parameter_index, + parameter_index=len(input_components), + real_valued=True, + ) + ) + parameter_cursor += 1 + + if parameter_cursor != parameter_count: + raise RuntimeError("stage-local parameter layout cursor mismatch") + + model_parameter_symbols = _logical_model_parameter_symbols( + sorted_model_parameter_records, + model_parameter_slot_symbols, + ) + + return _StageLocalInputs( + parameter_symbols=tuple(parameter_symbols), + input_components=tuple(input_components), + value_symbols=value_symbols, + momentum_symbols=momentum_symbols, + model_parameter_symbols=model_parameter_symbols, + value_parameter_count=value_parameter_count, + momentum_parameter_count=momentum_parameter_count, + model_parameter_count=model_parameter_count, + real_valued_inputs=tuple(int(index) for index in builder.real_valued_inputs), + ) + + +def _global_stage_inputs( + *, + parameter_symbols: Sequence[Any], + value_symbols: Sequence[Any], + momentum_symbols: Sequence[Any], + model_parameter_symbols: Mapping[str, Any], + value_parameter_count: int, + momentum_parameter_count: int, + model_parameter_count: int, + real_valued_inputs: Sequence[int], +) -> _StageLocalInputs: + return _StageLocalInputs( + parameter_symbols=tuple(parameter_symbols), + input_components=(), + value_symbols=tuple(value_symbols), + momentum_symbols=tuple(momentum_symbols), + model_parameter_symbols=dict(model_parameter_symbols), + value_parameter_count=int(value_parameter_count), + momentum_parameter_count=int(momentum_parameter_count), + model_parameter_count=int(model_parameter_count), + real_valued_inputs=tuple(int(index) for index in real_valued_inputs), + ) + + +def _parameter_builder(schema: dict[str, Any]) -> ParamBuilder: + layout = _dict(schema["parameter_layout"]) + builder = ParamBuilder() + value_component_count = int(layout["value_component_count"]) + if value_component_count: + builder.add_parameter_list( + ("artifact_schema_v3", "values"), + value_component_count, + role="generic_value_storage", + ) + momentum_parameter_count = int(layout["momentum_parameter_count"]) + if momentum_parameter_count: + builder.add_parameter_list( + ("artifact_schema_v3", "momenta"), + momentum_parameter_count, + role="generic_momentum_storage", + real_valued=True, + ) + model_parameter_count = int(layout.get("model_parameter_count", 0)) + if model_parameter_count: + builder.add_parameter_list( + ("artifact_schema_v3", "model_parameters"), + model_parameter_count, + role="generic_model_parameters", + real_valued=True, + ) + return builder + + +def _manifest_model(manifest: StageCompilationInput | GenericDAG) -> Model: + if isinstance(manifest, StageCompilationInput): + return manifest.model + raise ValueError("GenericDAG stage compilation requires an explicit model") + + +def _model_symbolica_functions( + model: Model, +) -> tuple[tuple[Any, tuple[Any, ...], Any], ...]: + getter = getattr(model, "symbolica_function_definitions", None) + if not callable(getter): + return () + definitions = getter() + if not isinstance(definitions, Mapping): + raise TypeError("model Symbolica function definitions must be a mapping") + return tuple( + (function, tuple(arguments), body) + for (function, arguments), body in definitions.items() + ) + + +def _specialize_stage_symbolica_functions( + outputs: Sequence[Any], + definitions: Sequence[tuple[Any, tuple[Any, ...], Any]], +) -> tuple[ + tuple[Any, ...], + tuple[tuple[Any, tuple[Any, ...], Any], ...], +]: + """Inline model-kernel calls before constructing a stage evaluator.""" + + output_expressions = tuple(outputs) + function_definitions = tuple(definitions) + if not output_expressions or not function_definitions: + return output_expressions, () + + from symbolica import Replacement + + replacements: list[Any] = [] + for definition_index, (function, arguments, body) in enumerate( + function_definitions + ): + if int(body.get_byte_size()) > _MODEL_FUNCTION_INLINE_MAX_BYTES: + continue + wildcards = tuple( + symbols.inline_function_wildcard( + definition_index, + argument_index, + ) + for argument_index in range(len(arguments)) + ) + pattern = function(*wildcards) + replacement = body + for argument, wildcard in zip(arguments, wildcards, strict=True): + replacement = replacement.replace( + argument, + wildcard, + allow_new_wildcards_on_rhs=True, + ) + replacements.append( + Replacement( + pattern, + replacement, + allow_new_wildcards_on_rhs=True, + ) + ) + + rewritten = tuple( + expression.replace_multiple(replacements, repeat=True) + for expression in output_expressions + ) + + function_names = {function for function, _arguments, _body in function_definitions} + required = { + symbol + for expression in rewritten + for symbol in _expression_symbols(expression, enter_functions=True) + if symbol in function_names + } + while True: + dependencies = { + symbol + for function, _arguments, body in function_definitions + if function in required + for symbol in _expression_symbols(body, enter_functions=True) + if symbol in function_names + } + expanded = required | dependencies + if expanded == required: + break + required = expanded + retained = tuple( + definition for definition in function_definitions if definition[0] in required + ) + return rewritten, retained + + +def _expression_symbols( + expression: Any, + *, + enter_functions: bool, +) -> set[Any]: + getter = getattr(expression, "get_all_symbols", None) + if not callable(getter): + return set() + return set(getter(enter_functions)) + + +def _value_components( + slot: dict[str, Any], + value_symbols: Sequence[Any] | Mapping[int, tuple[Any, ...]], +) -> tuple[Any, ...]: + if isinstance(value_symbols, Mapping): + return tuple(value_symbols[int(slot["value_slot_id"])]) + start = int(slot["component_start"]) + stop = int(slot["component_stop"]) + return tuple(value_symbols[index] for index in range(start, stop)) + + +def _momentum_components( + key: int, + momentum_symbols: Sequence[Any] | Mapping[int, tuple[Any, ...]], + momentum_slots: dict[int, dict[str, Any]], + *, + by_slot_id: bool = False, +) -> tuple[Any, ...]: + slot = ( + momentum_slots[key] + if by_slot_id + else _momentum_slot_by_mask(momentum_slots, key) + ) + if isinstance(momentum_symbols, Mapping): + return tuple(momentum_symbols[int(slot["momentum_slot_id"])]) + start = int(slot["component_start"]) + stop = int(slot["component_stop"]) + return tuple(momentum_symbols[index] for index in range(start, stop)) + + +def _momentum_slot_by_mask( + momentum_slots: dict[int, dict[str, Any]], + momentum_mask: int, +) -> dict[str, Any]: + for slot in momentum_slots.values(): + if int(slot["momentum_mask"]) == momentum_mask: + return slot + raise ValueError(f"no momentum slot for mask {momentum_mask}") + + +def _value_slots_by_id(schema: dict[str, Any]) -> dict[int, dict[str, Any]]: + storage = _dict(schema["value_storage"]) + return { + int(slot["value_slot_id"]): _dict(slot) + for slot in _list(storage["value_slots"]) + } + + +def _current_slots_by_id(schema: dict[str, Any]) -> dict[int, dict[str, Any]]: + storage = _dict(schema["current_storage"]) + return { + int(slot["current_id"]): _dict(slot) for slot in _list(storage["current_slots"]) + } + + +def _momentum_slots_by_id(schema: dict[str, Any]) -> dict[int, dict[str, Any]]: + return { + int(slot["momentum_slot_id"]): _dict(slot) + for slot in _list(schema["momentum_slots"]) + } + + +def _sum_components(left: Sequence[Any], right: Sequence[Any]) -> tuple[Any, ...]: + if len(left) != len(right): + raise ValueError(f"component dimensions differ: {len(left)} != {len(right)}") + return tuple(left[index] + right[index] for index in range(len(left))) + + +def _coupling(value: object) -> tuple[Any, Any]: + if not isinstance(value, list | tuple) or len(value) != 2: + raise ValueError("coupling metadata must have two entries") + return value[0], value[1] + + +def _runtime_coupling( + record: Mapping[str, Any], + model_parameter_symbols: Mapping[str, Any], +) -> tuple[Any, Any]: + values = list(_coupling(record.get("coupling"))) + names = record.get("coupling_parameter_names") + if isinstance(names, list): + for index, name in enumerate(names[: len(values)]): + if isinstance(name, str) and name in model_parameter_symbols: + values[index] = model_parameter_symbols[name] + return values[0], values[1] + + +def _expression_previews(expressions: Sequence[Any]) -> tuple[str, ...]: + return tuple(_preview_expression(expression) for expression in expressions[:4]) + + +def _preview_expression(expression: Any) -> str: + text = str(expression) + if len(text) <= _EXPRESSION_PREVIEW_LIMIT: + return text + return text[:_EXPRESSION_PREVIEW_LIMIT] + "..." + + +def _dict(value: object) -> dict[str, Any]: + if not isinstance(value, dict): + raise TypeError("expected JSON object") + return value + + +def _list(value: object) -> list[Any]: + if not isinstance(value, list): + raise TypeError("expected JSON array") + return value diff --git a/pyamplicol/src/pyamplicol/generation/stage_planning.py b/pyamplicol/src/pyamplicol/generation/stage_planning.py new file mode 100644 index 0000000000..3bbd95119f --- /dev/null +++ b/pyamplicol/src/pyamplicol/generation/stage_planning.py @@ -0,0 +1,745 @@ +# SPDX-License-Identifier: 0BSD +"""Stage blueprint assembly and fanout-aware output scheduling.""" + +from __future__ import annotations + +import heapq +from collections import Counter +from collections.abc import Mapping, Sequence +from dataclasses import replace +from typing import Any + +from ..models.base import Model +from .contracts import StageCompilationInput +from .dag_types import GenericDAG +from .helicity_materialization import _compiled_representative_dependencies +from .stage_expressions import ( + _compile_amplitude_stage_blueprint, + _compile_current_stage_blueprint, +) +from .stage_parameters import ( + _current_slots_by_id, + _dict, + _expression_previews, + _list, + _logical_model_parameter_symbols, + _manifest_model, + _momentum_slots_by_id, + _parameter_builder, + _value_slots_by_id, +) +from .stage_settings import _stage_symbolica_settings +from .stage_types import ( + GenericCompiledStageBlueprint, + GenericStageCompilerBlueprint, + GenericStageOutputSlot, + StageBlueprintConsumer, + StageBlueprintProgress, + _RuntimeParameterizedModel, +) + + +def build_generic_stage_compiler_blueprint( + manifest: StageCompilationInput | GenericDAG, + *, + model: Model | None = None, + selected_color_sector_ids: set[int] | None = None, + enable_lc_sector_runtime_selector: bool | None = None, + runtime_schema: Mapping[str, object] | None = None, + stage_local_parameter_layout: bool = True, + progress_callback: StageBlueprintProgress | None = None, + stage_consumer: StageBlueprintConsumer | None = None, + release_consumed_expressions: bool = False, +) -> GenericStageCompilerBlueprint: + """Build evaluator-ready symbolic stages from a neutral runtime schema. + + This is intentionally separate from the legacy shared-current table path. + It consumes the process-generic current DAG and runtime schema, asks the + model for local vertex and propagator component expressions, and records + stage output slots in terms of stable value-slot identifiers. + """ + + dag = manifest.dag if isinstance(manifest, StageCompilationInput) else manifest + if ( + isinstance(manifest, StageCompilationInput) + and model is not None + and model is not manifest.model + ): + raise ValueError("stage model override conflicts with compilation input") + if not stage_local_parameter_layout: + raise ValueError("stage-local parameter layout is mandatory") + if selected_color_sector_ids is not None: + raise ValueError( + "selected color sectors must already be encoded in the runtime schema" + ) + if enable_lc_sector_runtime_selector is not None: + raise ValueError( + "the LC selector policy must already be encoded in the runtime schema" + ) + if runtime_schema is not None: + schema = _dict(runtime_schema) + elif isinstance(manifest, StageCompilationInput): + schema = _dict(manifest.runtime_schema.to_mapping()) + else: + raise ValueError( + "GenericDAG stage compilation requires an explicit runtime schema" + ) + selected_model = model or _manifest_model(manifest) + parameter_layout = _dict(schema["parameter_layout"]) + global_value_component_count = int(parameter_layout["value_component_count"]) + global_momentum_parameter_count = int(parameter_layout["momentum_parameter_count"]) + global_model_parameter_count = int(parameter_layout.get("model_parameter_count", 0)) + global_parameter_count = ( + global_value_component_count + + global_momentum_parameter_count + + global_model_parameter_count + ) + if stage_local_parameter_layout: + # Every compiled stage constructs its own compact symbols below. Avoid + # materializing the large, otherwise-unused global Symbolica input set. + parameter_symbols: tuple[Any, ...] = () + value_symbols: tuple[Any, ...] = () + momentum_symbols: tuple[Any, ...] = () + model_parameter_symbols: tuple[Any, ...] = () + global_real_valued_inputs = tuple( + range(global_value_component_count, global_parameter_count) + ) + else: + builder = _parameter_builder(schema) + parameter_symbols = tuple(builder.parameter_symbols()) + value_symbols = parameter_symbols[:global_value_component_count] + momentum_start = global_value_component_count + momentum_stop = momentum_start + global_momentum_parameter_count + momentum_symbols = parameter_symbols[momentum_start:momentum_stop] + model_parameter_symbols = parameter_symbols[momentum_stop:] + global_real_valued_inputs = tuple( + int(index) for index in builder.real_valued_inputs + ) + model_parameter_records = tuple( + _dict(item) for item in _list(schema.get("model_parameters", [])) + ) + model_parameter_symbols_by_name = ( + {} + if stage_local_parameter_layout + else _logical_model_parameter_symbols( + model_parameter_records, + { + str(record["name"]): model_parameter_symbols[ + int(record["parameter_index"]) + ] + for record in model_parameter_records + }, + ) + ) + expression_model = ( + selected_model.with_runtime_parameters(model_parameter_symbols_by_name) + if hasattr(selected_model, "with_runtime_parameters") + else _RuntimeParameterizedModel( + selected_model, + model_parameter_symbols_by_name, + ) + ) + value_slots = _value_slots_by_id(schema) + current_slots = _current_slots_by_id(schema) + momentum_slots = _momentum_slots_by_id(schema) + stage_records = tuple(_dict(stage) for stage in _list(schema["stages"])) + stage_total = len(stage_records) + 1 + compiled_stages: list[GenericCompiledStageBlueprint] = [] + for stage_index, stage in enumerate(stage_records, start=1): + if progress_callback is not None: + progress_callback("current stage", stage_index, stage_total) + compiled_stage = _compile_current_stage_blueprint( + dag, + expression_model, + stage, + value_slots=value_slots, + current_slots=current_slots, + momentum_slots=momentum_slots, + global_value_component_count=global_value_component_count, + global_momentum_parameter_count=global_momentum_parameter_count, + model_parameter_records=model_parameter_records, + global_parameter_symbols=parameter_symbols, + global_value_symbols=value_symbols, + global_momentum_symbols=momentum_symbols, + global_model_parameter_symbols=model_parameter_symbols_by_name, + global_real_valued_inputs=global_real_valued_inputs, + stage_local_parameter_layout=stage_local_parameter_layout, + ) + compiled_stage = _stage_with_selector_domain_memberships( + compiled_stage, + dag, + ) + if stage_consumer is not None: + stage_consumer(compiled_stage, stage_index - 1, len(stage_records)) + if release_consumed_expressions and stage_consumer is not None: + compiled_stage = replace( + compiled_stage, + parameter_symbols=(), + output_expressions=(), + symbolica_functions=(), + ) + compiled_stages.append(compiled_stage) + stages = tuple(compiled_stages) + if progress_callback is not None: + progress_callback("amplitude stage", stage_total, stage_total) + amplitude_stage = _compile_amplitude_stage_blueprint( + expression_model, + _dict(schema["amplitude_stage"]), + value_slots=value_slots, + global_value_component_count=global_value_component_count, + global_momentum_parameter_count=global_momentum_parameter_count, + model_parameter_records=model_parameter_records, + global_parameter_symbols=parameter_symbols, + global_value_symbols=value_symbols, + global_model_parameter_symbols=model_parameter_symbols_by_name, + global_real_valued_inputs=global_real_valued_inputs, + stage_local_parameter_layout=stage_local_parameter_layout, + ) + amplitude_stage = _stage_with_selector_domain_memberships( + amplitude_stage, + dag, + ) + if stage_consumer is not None: + stage_consumer(amplitude_stage, len(stage_records), len(stage_records)) + if release_consumed_expressions and stage_consumer is not None: + amplitude_stage = replace( + amplitude_stage, + parameter_symbols=(), + output_expressions=(), + symbolica_functions=(), + ) + blockers = tuple( + blocker for stage in (*stages, amplitude_stage) for blocker in stage.blockers + ) + return GenericStageCompilerBlueprint( + kind="pyamplicol-generic-stage-compiler-blueprint", + runtime_available=False, + parameter_count=global_parameter_count, + value_parameter_count=int(schema["parameter_layout"]["value_component_count"]), + momentum_parameter_count=global_momentum_parameter_count, + model_parameter_count=global_model_parameter_count, + real_valued_inputs=global_real_valued_inputs, + stage_count=len(stages) + 1, + stages=stages, + amplitude_stage=amplitude_stage, + expression_ready=not blockers, + blockers=blockers, + parameter_symbols=parameter_symbols, + ) + + +def _chunk_evaluation_occurrence_count( + current_order: Sequence[int], + *, + output_size_by_current: Mapping[int, int], + evaluation_groups_by_current: Mapping[int, frozenset[int]], + chunk_size: int, +) -> int: + chunk_groups: list[set[int]] = [] + output_offset = 0 + for current_id in current_order: + output_size = int(output_size_by_current[current_id]) + if output_size <= 0: + continue + first_chunk = output_offset // chunk_size + last_chunk = (output_offset + output_size - 1) // chunk_size + while len(chunk_groups) <= last_chunk: + chunk_groups.append(set()) + groups = evaluation_groups_by_current.get(current_id, frozenset()) + for chunk_index in range(first_chunk, last_chunk + 1): + chunk_groups[chunk_index].update(groups) + output_offset += output_size + return sum(len(groups) for groups in chunk_groups) + + +def _fanout_aware_current_order( + current_ids: Sequence[int], + *, + output_size_by_current: Mapping[int, int], + evaluation_groups_by_current: Mapping[int, frozenset[int]], + chunk_size: int, +) -> tuple[tuple[int, ...], int, int]: + """Cluster current outputs whose kernel evaluations have shared fan-out. + + Shared evaluation groups form a sparse hypergraph over result currents. + Indexed heaps reproduce the overlap/benefit greedy choice without scanning + every unplaced current. Pathologically large fan-outs use a deterministic + anchor-group sort, keeping the construction bounded for very large stages. + """ + + natural_order = tuple(int(current_id) for current_id in current_ids) + before = _chunk_evaluation_occurrence_count( + natural_order, + output_size_by_current=output_size_by_current, + evaluation_groups_by_current=evaluation_groups_by_current, + chunk_size=chunk_size, + ) + frequencies = Counter( + group_id + for current_id in natural_order + for group_id in evaluation_groups_by_current.get(current_id, frozenset()) + ) + shared_groups = { + group_id for group_id, frequency in frequencies.items() if frequency > 1 + } + if not shared_groups or len(natural_order) < 2: + return natural_order, before, before + + shared_by_current = { + current_id: tuple( + sorted( + group_id + for group_id in evaluation_groups_by_current.get( + current_id, + frozenset(), + ) + if group_id in shared_groups + ) + ) + for current_id in natural_order + } + members_by_group: dict[int, list[int]] = { + group_id: [] for group_id in shared_groups + } + for current_id, group_ids in shared_by_current.items(): + for group_id in group_ids: + members_by_group[group_id].append(current_id) + + benefit_by_current = { + current_id: sum( + frequencies[group_id] - 1 for group_id in shared_by_current[current_id] + ) + for current_id in natural_order + } + large_fanout_limit = max(1024, 8 * chunk_size) + if max(frequencies.values()) > large_fanout_limit: + + def anchor_key(current_id: int) -> tuple[int, int, int, int, int]: + group_ids = shared_by_current[current_id] + if not group_ids: + return (1, 0, 0, 0, current_id) + anchor = min( + group_ids, + key=lambda group_id: (-frequencies[group_id], group_id), + ) + return ( + 0, + -frequencies[anchor], + anchor, + -benefit_by_current[current_id], + current_id, + ) + + candidate_order = tuple(sorted(natural_order, key=anchor_key)) + else: + remaining = set(natural_order) + seed_heap = [ + ( + -benefit_by_current[current_id], + -len( + evaluation_groups_by_current.get( + current_id, + frozenset(), + ) + ), + current_id, + ) + for current_id in natural_order + ] + heapq.heapify(seed_heap) + fitting_heaps: dict[int, list[tuple[int, int]]] = {} + for current_id in natural_order: + size = int(output_size_by_current[current_id]) + fitting_heaps.setdefault(size, []).append( + (-benefit_by_current[current_id], current_id) + ) + for heap in fitting_heaps.values(): + heapq.heapify(heap) + output_sizes = tuple(sorted(fitting_heaps)) + + def pop_seed() -> int: + while seed_heap: + _benefit, _group_count, current_id = heapq.heappop(seed_heap) + if current_id in remaining: + return current_id + raise ValueError("fan-out ordering lost an unplaced current") + + def pop_fitting(capacity: int) -> int | None: + selected_size: int | None = None + selected_key: tuple[int, int, int] | None = None + for size in output_sizes: + if size > capacity: + break + heap = fitting_heaps[size] + while heap and heap[0][1] not in remaining: + heapq.heappop(heap) + if not heap: + continue + benefit, current_id = heap[0] + key = (benefit, -size, current_id) + if selected_key is None or key < selected_key: + selected_size = size + selected_key = key + if selected_size is None: + return None + _benefit, current_id = heapq.heappop(fitting_heaps[selected_size]) + return current_id + + bins: list[list[int]] = [] + while remaining: + seed = pop_seed() + current_bin: list[int] = [] + groups_in_bin: set[int] = set() + overlap_by_current: dict[int, int] = {} + candidate_heap: list[tuple[int, int, int, int]] = [] + used = 0 + + def add_current( + current_id: int, + current_bin: list[int], + groups_in_bin: set[int], + overlap_by_current: dict[int, int], + candidate_heap: list[tuple[int, int, int, int]], + ) -> None: + nonlocal used + remaining.remove(current_id) + current_bin.append(current_id) + used += int(output_size_by_current[current_id]) + for group_id in shared_by_current[current_id]: + if group_id in groups_in_bin: + continue + groups_in_bin.add(group_id) + for candidate_id in members_by_group[group_id]: + if candidate_id not in remaining: + continue + overlap = overlap_by_current.get(candidate_id, 0) + 1 + overlap_by_current[candidate_id] = overlap + heapq.heappush( + candidate_heap, + ( + -overlap, + -benefit_by_current[candidate_id], + -int(output_size_by_current[candidate_id]), + candidate_id, + ), + ) + + def pop_overlapping( + capacity: int, + candidate_heap: list[tuple[int, int, int, int]], + overlap_by_current: dict[int, int], + ) -> int | None: + postponed: list[tuple[int, int, int, int]] = [] + selected: int | None = None + while candidate_heap: + item = heapq.heappop(candidate_heap) + overlap, _benefit, _size, current_id = item + if current_id not in remaining: + continue + if -overlap != overlap_by_current.get(current_id, 0): + continue + if int(output_size_by_current[current_id]) > capacity: + postponed.append(item) + continue + selected = current_id + break + for item in postponed: + heapq.heappush(candidate_heap, item) + return selected + + add_current( + seed, + current_bin, + groups_in_bin, + overlap_by_current, + candidate_heap, + ) + while remaining: + capacity = chunk_size - used + if capacity < 0: + break + selected = pop_overlapping( + capacity, + candidate_heap, + overlap_by_current, + ) + if selected is None: + selected = pop_fitting(capacity) + if selected is None: + break + add_current( + selected, + current_bin, + groups_in_bin, + overlap_by_current, + candidate_heap, + ) + bins.append(current_bin) + + candidate_order = tuple( + current_id for current_bin in bins for current_id in current_bin + ) + after = _chunk_evaluation_occurrence_count( + candidate_order, + output_size_by_current=output_size_by_current, + evaluation_groups_by_current=evaluation_groups_by_current, + chunk_size=chunk_size, + ) + if after >= before: + return natural_order, before, before + return candidate_order, before, after + + +def _stage_with_selector_domain_memberships( + stage: GenericCompiledStageBlueprint, + dag: GenericDAG, +) -> GenericCompiledStageBlueprint: + materialization = dag.helicity_materialization + replay = dag.lc_topology_replay + if materialization is None and replay is None: + return stage + domains_by_current: dict[int, list[int]] = {} + domains_by_root: dict[int, list[int]] = {} + if materialization is not None: + for schedule in materialization.selector_schedules: + if schedule.structural_zero: + continue + for current_id in schedule.active_current_ids: + domains_by_current.setdefault(int(current_id), []).append( + int(schedule.selector_domain_id) + ) + for root_id in schedule.active_root_ids: + domains_by_root.setdefault(int(root_id), []).append( + int(schedule.selector_domain_id) + ) + color_domains_by_current, color_domains_by_root = ( + _lc_materialized_sector_memberships(dag) + ) + output_slots = [] + for slot in stage.output_slots: + owner_id = ( + int(slot.component_start) + if str(stage.stage_kind).startswith("amplitude") + else int(slot.current_id) + ) + domains = ( + domains_by_root.get(owner_id, ()) + if str(stage.stage_kind).startswith("amplitude") + else domains_by_current.get(owner_id, ()) + ) + output_slots.append( + replace( + slot, + selector_domain_ids=tuple(sorted(set(domains))), + color_selector_domain_ids=( + color_domains_by_root.get(owner_id, ()) + if str(stage.stage_kind).startswith("amplitude") + else color_domains_by_current.get(owner_id, ()) + ), + ) + ) + return replace(stage, output_slots=tuple(output_slots)) + + +def _lc_materialized_sector_memberships( + dag: GenericDAG, +) -> tuple[dict[int, tuple[int, ...]], dict[int, tuple[int, ...]]]: + if dag.process.color_accuracy != "lc" or dag.color_coverage != "complete": + return {}, {} + replay = dag.lc_topology_replay + materialized_sector_ids = ( + {int(sector.id) for sector in dag.color_plan.sectors} + if replay is None + else set(replay.materialized_sector_ids) + ) + roots_by_sector: dict[int, list[int]] = { + sector_id: [] for sector_id in materialized_sector_ids + } + root_by_id = {int(root.id): root for root in dag.amplitude_roots} + for root in dag.amplitude_roots: + sector_id = ( + int(root.color_sector_id) + if root.color_sector_id is not None + else int(dag.currents[root.left_id].index.color_state.sector_id) + ) + if sector_id in roots_by_sector: + roots_by_sector[sector_id].append(int(root.id)) + + interactions_by_result: dict[int, list[tuple[int, int]]] = {} + for interaction in dag.interactions: + interactions_by_result.setdefault(int(interaction.result_id), []).append( + (int(interaction.left_id), int(interaction.right_id)) + ) + compiled_dependencies = _compiled_representative_dependencies(dag) + + sectors_by_current: dict[int, set[int]] = {} + sectors_by_root: dict[int, set[int]] = {} + for sector_id, root_ids in roots_by_sector.items(): + if not root_ids: + raise ValueError( + "LC topology replay materialized sector " + f"{sector_id} has no amplitude root" + ) + active_currents: set[int] = set() + pending: list[int] = [] + for root_id in root_ids: + root = root_by_id[root_id] + sectors_by_root.setdefault(root_id, set()).add(sector_id) + pending.extend((int(root.left_id), int(root.right_id))) + while pending: + current_id = pending.pop() + if current_id in active_currents: + continue + active_currents.add(current_id) + for left_id, right_id in interactions_by_result.get(current_id, ()): + pending.extend((left_id, right_id)) + pending.extend(compiled_dependencies.get(current_id, ())) + for current_id in active_currents: + sectors_by_current.setdefault(current_id, set()).add(sector_id) + + return ( + { + current_id: tuple(sorted(sector_ids)) + for current_id, sector_ids in sectors_by_current.items() + }, + { + root_id: tuple(sorted(sector_ids)) + for root_id, sector_ids in sectors_by_root.items() + }, + ) + + +def _stage_with_fanout_aware_output_order( + stage: GenericCompiledStageBlueprint, + *, + chunk_size: int | None, +) -> GenericCompiledStageBlueprint: + if not stage.output_slots: + return stage + + amplitude_stage = str(stage.stage_kind).startswith("amplitude") + slots_by_owner: dict[int, list[GenericStageOutputSlot]] = {} + for slot_index, slot in enumerate(stage.output_slots): + owner_id = slot_index if amplitude_stage else int(slot.current_id) + slots_by_owner.setdefault(owner_id, []).append(slot) + natural_order = tuple(slots_by_owner) + output_size_by_owner = { + owner_id: sum(slot.output_stop - slot.output_start for slot in slots) + for owner_id, slots in slots_by_owner.items() + } + groups_by_current = { + int(current_id): frozenset(int(group_id) for group_id in group_ids) + for current_id, group_ids in stage.evaluation_groups_by_current + if current_id in slots_by_owner + } + signature_by_owner = { + owner_id: ( + slots[0].selector_domain_ids, + slots[0].color_selector_domain_ids, + ) + for owner_id, slots in slots_by_owner.items() + } + selector_partitioning = any( + helicity_ids or color_ids + for helicity_ids, color_ids in signature_by_owner.values() + ) + owners_by_signature: dict[ + tuple[tuple[int, ...], tuple[int, ...]], list[int] + ] = {} + for owner_id in natural_order: + owners_by_signature.setdefault( + signature_by_owner[owner_id], [] + ).append(owner_id) + + owner_order: list[int] = [] + before = 0 + after = 0 + for signature_owners in owners_by_signature.values(): + ordered = tuple(signature_owners) + if ( + not amplitude_stage + and chunk_size is not None + and int(chunk_size) > 0 + and groups_by_current + ): + ordered, group_before, group_after = _fanout_aware_current_order( + ordered, + output_size_by_current=output_size_by_owner, + evaluation_groups_by_current=groups_by_current, + chunk_size=int(chunk_size), + ) + before += group_before + after += group_after + owner_order.extend(ordered) + + if not selector_partitioning and tuple(owner_order) == natural_order: + if chunk_size is None or int(chunk_size) < 1 or amplitude_stage: + return stage + return replace( + stage, + fanout_chunk_size=int(chunk_size), + fanout_evaluation_occurrences_before=before, + fanout_evaluation_occurrences_after=after, + ) + + outputs: list[Any] = [] + output_slots: list[GenericStageOutputSlot] = [] + selector_output_partitions: list[tuple[int, int]] = [] + active_signature: tuple[tuple[int, ...], tuple[int, ...]] | None = None + partition_start = 0 + for owner_id in owner_order: + signature = signature_by_owner[owner_id] + if active_signature is None: + active_signature = signature + elif signature != active_signature: + selector_output_partitions.append((partition_start, len(outputs))) + partition_start = len(outputs) + active_signature = signature + for slot in slots_by_owner[owner_id]: + components = stage.output_expressions[slot.output_start : slot.output_stop] + start = len(outputs) + outputs.extend(components) + output_slots.append( + replace( + slot, + output_start=start, + output_stop=len(outputs), + ) + ) + if selector_partitioning: + selector_output_partitions.append((partition_start, len(outputs))) + if len(outputs) != len(stage.output_expressions): + raise ValueError("fan-out output ordering lost stage expressions") + return replace( + stage, + output_slots=tuple(output_slots), + first_output_previews=_expression_previews(outputs), + output_expressions=tuple(outputs), + fanout_chunk_size=(None if chunk_size is None else int(chunk_size)), + fanout_evaluation_occurrences_before=(before or None), + fanout_evaluation_occurrences_after=(after or None), + selector_output_partitions=tuple(selector_output_partitions), + ) + + +def _prepare_stage_for_output_chunking( + stage: GenericCompiledStageBlueprint, + *, + blueprint: GenericStageCompilerBlueprint | None, + symbolica_settings: Any | None, + current_stage_position: int | None = None, + current_stage_count: int | None = None, +) -> GenericCompiledStageBlueprint: + if symbolica_settings is None: + return stage + settings = _stage_symbolica_settings( + stage, + blueprint, + symbolica_settings, + current_stage_position=current_stage_position, + current_stage_count=current_stage_count, + ) + return _stage_with_fanout_aware_output_order( + stage, + chunk_size=getattr(settings, "compiled_output_chunk_size", None), + ) diff --git a/pyamplicol/src/pyamplicol/generation/stage_settings.py b/pyamplicol/src/pyamplicol/generation/stage_settings.py new file mode 100644 index 0000000000..8e43c96db1 --- /dev/null +++ b/pyamplicol/src/pyamplicol/generation/stage_settings.py @@ -0,0 +1,106 @@ +# SPDX-License-Identifier: 0BSD +"""Per-stage evaluator settings.""" + +from __future__ import annotations + +from dataclasses import replace +from typing import Any + +from .stage_types import GenericCompiledStageBlueprint, GenericStageCompilerBlueprint + +_LOW_RANK_CURRENT_COMPONENT_LIMIT = 8 +_HIGH_RANK_CURRENT_CHUNK_LIMIT = 256 + + +def _stage_symbolica_settings( + stage: GenericCompiledStageBlueprint, + blueprint: GenericStageCompilerBlueprint | None, + settings: Any, + *, + current_stage_position: int | None = None, + current_stage_count: int | None = None, +) -> Any: + """Derive the stage-specific evaluator output-chunk policy.""" + + settings = _bound_high_rank_current_chunk(stage, settings) + + strategy = getattr(settings, "output_chunk_strategy", "uniform") + if strategy == "auto": + base = getattr(settings, "compiled_output_chunk_size", None) + output_count = int( + getattr( + stage, + "output_length", + 0 if base is None else int(base) + 1, + ) + ) + strategy = ( + "measured-stage" + if getattr(settings, "backend", None) == "jit" + and base is not None + and int(base) <= 256 + and output_count > int(base) + else "uniform" + ) + settings = replace(settings, output_chunk_strategy=strategy) + if strategy != "tapered-stage": + return settings + base = getattr(settings, "compiled_output_chunk_size", None) + if base is None: + return settings + if str(stage.stage_kind).startswith("amplitude"): + return replace(settings, compiled_output_chunk_size=None) + + if blueprint is not None: + try: + position = next( + index + for index, current_stage in enumerate(blueprint.stages) + if current_stage.stage_index == stage.stage_index + ) + except StopIteration: + return settings + stage_count = len(blueprint.stages) + else: + if current_stage_position is None or current_stage_count is None: + return settings + position = int(current_stage_position) + stage_count = int(current_stage_count) + remaining = stage_count - position - 1 + if position < 2: + chunk_size = None + elif remaining == 0: + chunk_size = max(1, int(base) // 2) + elif remaining <= 2: + chunk_size = int(base) + else: + chunk_size = int(base) * 2 + return replace(settings, compiled_output_chunk_size=chunk_size) + + +def _bound_high_rank_current_chunk( + stage: GenericCompiledStageBlueprint, + settings: Any, +) -> Any: + """Keep native evaluators for higher-rank current states at a safe size.""" + + chunk_size = getattr(settings, "compiled_output_chunk_size", None) + if ( + chunk_size is None + or int(chunk_size) <= _HIGH_RANK_CURRENT_CHUNK_LIMIT + or str(stage.stage_kind).startswith("amplitude") + ): + return settings + max_slot_width = max( + ( + int(slot.component_stop) - int(slot.component_start) + for slot in stage.output_slots + ), + default=0, + ) + if max_slot_width <= _LOW_RANK_CURRENT_COMPONENT_LIMIT: + return settings + return replace( + settings, + compiled_output_chunk_size=_HIGH_RANK_CURRENT_CHUNK_LIMIT, + ) diff --git a/pyamplicol/src/pyamplicol/generation/stage_types.py b/pyamplicol/src/pyamplicol/generation/stage_types.py new file mode 100644 index 0000000000..d1830da395 --- /dev/null +++ b/pyamplicol/src/pyamplicol/generation/stage_types.py @@ -0,0 +1,282 @@ +# SPDX-License-Identifier: 0BSD +"""Frozen stage compilation records and runtime-model adapter.""" + +from __future__ import annotations + +from collections.abc import Callable, Mapping, Sequence +from dataclasses import dataclass, field +from typing import Any, cast + +from ..models._physics_ir import PropagatorIR +from ..models.base import Model + + +@dataclass(frozen=True) +class GenericStageInputComponent: + kind: str + source_id: int + component: int + global_component: int + parameter_index: int + real_valued: bool = False + + def to_json_dict(self) -> dict[str, object]: + return { + "kind": self.kind, + "source_id": self.source_id, + "component": self.component, + "global_component": self.global_component, + "parameter_index": self.parameter_index, + "real_valued": self.real_valued, + } + + +@dataclass(frozen=True) +class GenericStageOutputSlot: + value_slot_id: int + current_id: int + variant: str + component_start: int + component_stop: int + output_start: int + output_stop: int + selector_domain_ids: tuple[int, ...] = field( + default=(), + repr=False, + compare=False, + ) + color_selector_domain_ids: tuple[int, ...] = field( + default=(), + repr=False, + compare=False, + ) + + def to_json_dict(self) -> dict[str, object]: + result: dict[str, object] = { + "value_slot_id": self.value_slot_id, + "current_id": self.current_id, + "variant": self.variant, + "component_start": self.component_start, + "component_stop": self.component_stop, + "output_start": self.output_start, + "output_stop": self.output_stop, + } + if self.color_selector_domain_ids: + result["color_selector_domain_ids"] = list( + self.color_selector_domain_ids + ) + return result + + +@dataclass(frozen=True) +class GenericCompiledStageBlueprint: + stage_index: int + stage_kind: str + subset_size: int | None + evaluator_label: str + parameter_layout: str + output_length: int + output_slots: tuple[GenericStageOutputSlot, ...] + input_value_slot_ids: tuple[int, ...] + output_value_slot_ids: tuple[int, ...] + interaction_ids: tuple[int, ...] + input_components: tuple[GenericStageInputComponent, ...] + parameter_count: int + value_parameter_count: int + momentum_parameter_count: int + model_parameter_count: int + real_valued_inputs: tuple[int, ...] + expression_ready: bool + blockers: tuple[str, ...] + first_output_previews: tuple[str, ...] + evaluation_groups_by_current: tuple[tuple[int, tuple[int, ...]], ...] = field( + default=(), + repr=False, + compare=False, + ) + fanout_chunk_size: int | None = None + fanout_evaluation_occurrences_before: int | None = None + fanout_evaluation_occurrences_after: int | None = None + selector_output_partitions: tuple[tuple[int, int], ...] = field( + default=(), + repr=False, + compare=False, + ) + parameter_symbols: tuple[Any, ...] = field( + default=(), + repr=False, + compare=False, + ) + output_expressions: tuple[Any, ...] = field( + default=(), + repr=False, + compare=False, + ) + symbolica_functions: tuple[tuple[Any, tuple[Any, ...], Any], ...] = field( + default=(), + repr=False, + compare=False, + ) + + def to_json_dict(self) -> dict[str, object]: + return { + "stage_index": self.stage_index, + "stage_kind": self.stage_kind, + "subset_size": self.subset_size, + "evaluator_label": self.evaluator_label, + "parameter_layout": self.parameter_layout, + "output_length": self.output_length, + "output_slots": [slot.to_json_dict() for slot in self.output_slots], + "input_value_slot_ids": list(self.input_value_slot_ids), + "output_value_slot_ids": list(self.output_value_slot_ids), + "interaction_ids": list(self.interaction_ids), + "input_components": [ + component.to_json_dict() for component in self.input_components + ], + "parameter_count": self.parameter_count, + "value_parameter_count": self.value_parameter_count, + "momentum_parameter_count": self.momentum_parameter_count, + "model_parameter_count": self.model_parameter_count, + "real_valued_inputs": list(self.real_valued_inputs), + "expression_ready": self.expression_ready, + "blockers": list(self.blockers), + "first_output_previews": list(self.first_output_previews), + "symbolica_function_count": len(self.symbolica_functions), + "evaluation_fanout": { + "strategy": ( + "shared-evaluation-affinity" + if self.fanout_chunk_size is not None + else "natural-current-order" + ), + "chunk_size": self.fanout_chunk_size, + "evaluation_occurrences_before": ( + self.fanout_evaluation_occurrences_before + ), + "evaluation_occurrences_after": ( + self.fanout_evaluation_occurrences_after + ), + }, + } + + +@dataclass(frozen=True) +class GenericStageCompilerBlueprint: + kind: str + runtime_available: bool + parameter_count: int + value_parameter_count: int + momentum_parameter_count: int + model_parameter_count: int + real_valued_inputs: tuple[int, ...] + stage_count: int + stages: tuple[GenericCompiledStageBlueprint, ...] + amplitude_stage: GenericCompiledStageBlueprint + expression_ready: bool + blockers: tuple[str, ...] + parameter_symbols: tuple[Any, ...] = field( + default=(), + repr=False, + compare=False, + ) + + def to_json_dict(self) -> dict[str, object]: + return { + "kind": self.kind, + "runtime_available": self.runtime_available, + "parameter_count": self.parameter_count, + "value_parameter_count": self.value_parameter_count, + "momentum_parameter_count": self.momentum_parameter_count, + "model_parameter_count": self.model_parameter_count, + "real_valued_inputs": list(self.real_valued_inputs), + "stage_count": self.stage_count, + "stages": [stage.to_json_dict() for stage in self.stages], + "amplitude_stage": self.amplitude_stage.to_json_dict(), + "expression_ready": self.expression_ready, + "blockers": list(self.blockers), + } + + +@dataclass(frozen=True) +class _StageLocalInputs: + parameter_symbols: tuple[Any, ...] + input_components: tuple[GenericStageInputComponent, ...] + value_symbols: tuple[Any, ...] | Mapping[int, tuple[Any, ...]] + momentum_symbols: tuple[Any, ...] | Mapping[int, tuple[Any, ...]] + model_parameter_symbols: Mapping[str, Any] + value_parameter_count: int + momentum_parameter_count: int + model_parameter_count: int + real_valued_inputs: tuple[int, ...] + + +StageEvaluatorCompiler = Callable[ + [GenericCompiledStageBlueprint, tuple[Any, ...], tuple[int, ...]], + dict[str, object], +] +StageBlueprintProgress = Callable[[str, int, int], None] +StageBlueprintConsumer = Callable[ + [GenericCompiledStageBlueprint, int, int], + None, +] + + +class _RuntimeParameterizedModel: + """Overlay runtime parameters while delegating every model decision. + + This proxy deliberately does not inherit any concrete model. Models with a + specialized runtime-parameter implementation can still provide + ``with_runtime_parameters`` and bypass it. + """ + + def __init__(self, base: Model, parameters: Mapping[str, Any]) -> None: + self._base_model = base + self._runtime_parameters = parameters + self.name = base.name + self.particles = base.particles + self.vertices = base.vertices + + def __getattr__(self, name: str) -> Any: + return getattr(self._base_model, name) + + def mass(self, pdg: int) -> Any: + particle = self._base_model.particle(pdg) + name = f"particle.{int(particle.pdg)}.mass" + return self._runtime_parameters.get(name, self._base_model.mass(pdg)) + + def width(self, pdg: int) -> Any: + particle = self._base_model.particle(pdg) + name = f"particle.{int(particle.pdg)}.width" + return self._runtime_parameters.get(name, self._base_model.width(pdg)) + + def propagator_lowering_rule(self, particle_id: int, chirality: int = 0) -> Any: + return self._base_model.propagator_lowering_rule(particle_id, chirality) + + def propagator_component_expression( + self, + particle_id: int, + value: Sequence[Any], + momentum: Sequence[Any], + *, + chirality: int = 0, + propagator: PropagatorIR | None = None, + ) -> tuple[Any, ...]: + return cast( + tuple[Any, ...], + type(self._base_model).propagator_component_expression( + self, + particle_id, + value, + momentum, + chirality=chirality, + propagator=propagator, + ), + ) + + def is_chiral_eligible(self, pdg: int) -> bool: + return self._base_model.is_chiral_eligible(pdg) + + def with_runtime_parameters( + self, + parameters: Mapping[str, Any], + ) -> _RuntimeParameterizedModel: + return _RuntimeParameterizedModel(self._base_model, parameters) diff --git a/pyamplicol/src/pyamplicol/generation/structural_source_proof.py b/pyamplicol/src/pyamplicol/generation/structural_source_proof.py new file mode 100644 index 0000000000..ee865f980a --- /dev/null +++ b/pyamplicol/src/pyamplicol/generation/structural_source_proof.py @@ -0,0 +1,649 @@ +# SPDX-License-Identifier: 0BSD +"""Generation-time structural source and physical-lane evidence. + +This module deliberately operates before ``artifact.json`` exists. Its output +is provenance rather than runtime state, so callers bind its declared payload +SHA directly when a later numerical witness needs the proof identity. +""" + +from __future__ import annotations + +import hashlib +import json +from collections.abc import Mapping, Sequence +from pathlib import Path +from typing import Any + +from pyamplicol.artifacts.security import sha256_file + +from .evaluator_container import PacbinReader + +SCHEMA = "pyamplicol-generation-structural-source-proof-v1" +ROLE = "structural-source-proof" +SEMANTIC_MAP_DOMAINS = { + "current_member_map": "current_member_map-v1", + "interaction_row_map": "interaction_row_map-v1", + "closure_map": "closure_map-v1", + "source_contract": "source_contract-v1", +} +# Compiler executables record provenance; they are not members of the artifact payload. +_DIAGNOSTIC_PATH_FIELDS = frozenset({"compiler_path"}) + + +def canonical_sha256(domain: str, value: object) -> str: + """Hash canonical JSON under a distinct semantic domain.""" + + encoded = json.dumps( + {"domain": domain, "value": value}, + ensure_ascii=True, + sort_keys=True, + separators=(",", ":"), + allow_nan=False, + ).encode("ascii") + return hashlib.sha256(encoded).hexdigest() + + +def semantic_map_witnesses(execution: Mapping[str, object]) -> dict[str, object]: + """Project one execution DTO into four non-interchangeable exact maps.""" + + predicates = { + "current_member_map": lambda key: ( + "current" in key + or key + in { + "source_count", + "source_fill", + "source_row_count", + "sources", + "value_storage", + } + ), + "interaction_row_map": lambda key: ( + "interaction" in key + or "contribution" in key + or key in {"stages", "schedule"} + ), + "closure_map": lambda key: ( + "closure" in key + or "amplitude" in key + or "destination" in key + or "reduction" in key + ), + "source_contract": lambda key: key + in { + "color_accuracy", + "external_particles", + "external_pdg_order", + "kind", + "model", + "normalization", + "parameter_layout", + "process", + "process_key", + "runtime_metadata", + "schema_version", + }, + } + + def project(predicate: Any) -> list[dict[str, object]]: + rows: list[dict[str, object]] = [] + + def visit(value: object, path: str) -> None: + if isinstance(value, Mapping): + for raw_key in sorted(value, key=str): + key = str(raw_key) + child = value[raw_key] + child_path = f"{path}.{key}" if path else key + if predicate(key): + rows.append({"path": child_path, "value": child}) + else: + visit(child, child_path) + elif isinstance(value, Sequence) and not isinstance(value, str | bytes): + for index, child in enumerate(value): + visit(child, f"{path}[{index}]") + + visit(execution, "") + if not rows: + raise ValueError("execution lacks a required exact semantic-map witness") + return rows + + result: dict[str, object] = {} + identities: set[str] = set() + for name, predicate in predicates.items(): + rows = project(predicate) + digest = canonical_sha256(SEMANTIC_MAP_DOMAINS[name], rows) + if digest in identities: + raise ValueError("semantic maps repeat one content identity") + identities.add(digest) + result[name] = {"sha256": digest, "rows": rows} + return result + + +def _payload_paths(value: object) -> tuple[str, ...]: + paths: set[str] = set() + + def visit(item: object, field: str | None = None) -> None: + if isinstance(item, Mapping): + for key, child in item.items(): + visit(child, str(key)) + elif isinstance(item, Sequence) and not isinstance(item, str | bytes): + for child in item: + visit(child, field) + elif ( + isinstance(item, str) + and field is not None + and field not in _DIAGNOSTIC_PATH_FIELDS + and (field == "path" or field.endswith("_path")) + and item + ): + candidate = Path(item) + if candidate.is_absolute() or ".." in candidate.parts: + raise ValueError(f"execution payload path is not confined: {item!r}") + paths.add(candidate.as_posix()) + + visit(value) + return tuple(sorted(paths)) + + +def _structural_metrics(value: object) -> list[dict[str, object]]: + """Retain every explicit structural counter without inferred aliases.""" + + rows: list[dict[str, object]] = [] + + def visit(item: object, path: str) -> None: + if isinstance(item, Mapping): + for raw_key in sorted(item, key=str): + key = str(raw_key) + child = item[raw_key] + child_path = f"{path}.{key}" if path else key + if ( + isinstance(child, int) + and not isinstance(child, bool) + and ( + key.endswith("_count") + or key.endswith("_size") + or key.endswith("_size_bytes") + ) + ): + if child < 0: + raise ValueError( + f"structural counter {child_path} must be non-negative" + ) + rows.append({"path": child_path, "value": child}) + else: + visit(child, child_path) + elif isinstance(item, Sequence) and not isinstance(item, str | bytes): + for index, child in enumerate(item): + visit(child, f"{path}[{index}]") + + visit(value, "") + if not rows: + raise ValueError("execution lane has no explicit structural counters") + return rows + + +def execution_lanes( + execution: Mapping[str, object], +) -> tuple[tuple[str, Mapping[str, object]], ...]: + """Enumerate each logical execution lane; preserve physical aliases.""" + + kind = execution.get("kind") + if kind != "pyamplicol-runtime-execution": + return (("primary", execution),) + + lanes: list[tuple[str, Mapping[str, object]]] = [] + + def add(record: object, path: str) -> None: + if not isinstance(record, Mapping): + raise ValueError(f"compiled execution lane {path} is malformed") + if not isinstance(record.get("dag_summary"), Mapping): + raise ValueError(f"compiled execution lane {path} lacks a DAG summary") + lanes.append((path, record)) + helicity_sum = record.get("helicity_sum_execution") + if helicity_sum is not None: + add(helicity_sum, f"{path}.helicity_sum_execution") + for key in ("color_selector_executions", "helicity_selector_executions"): + wrappers = record.get(key, ()) + if wrappers is None: + continue + if not isinstance(wrappers, Sequence) or isinstance(wrappers, str | bytes): + raise ValueError(f"compiled execution {path}.{key} is malformed") + for index, wrapper in enumerate(wrappers): + if not isinstance(wrapper, Mapping): + raise ValueError( + f"compiled execution {path}.{key}[{index}] is malformed" + ) + add(wrapper.get("execution"), f"{path}.{key}[{index}].execution") + + add(execution, "primary") + return tuple(lanes) + + +def _resolve_loose_payload( + root: Path, + process_id: str, + relative: str, +) -> str | None: + candidates = (relative, f"processes/{process_id}/{relative}") + for candidate in candidates: + path = root / candidate + if path.is_file() and not path.is_symlink(): + return candidate + return None + + +def build_generation_structural_proof( + *, + artifact_root: Path, + process_id: str, + source_revision: str, + native_build_inputs_sha256: str, + execution_path: str, + execution_sha256: str, + execution: Mapping[str, object], + evaluator_container_path: str | None, + evaluator_container_index_sha256: str | None, +) -> dict[str, object]: + """Build exact source/lane evidence from the private artifact staging tree.""" + + lanes = execution_lanes(execution) + member_records: dict[str, dict[str, object]] = {} + container_record: dict[str, object] | None = None + if evaluator_container_path is not None: + container = artifact_root / evaluator_container_path + if not container.is_file() or container.is_symlink(): + raise ValueError("evaluator container is absent during structural proof") + with PacbinReader.open(container, verify_payloads=True) as reader: + if ( + evaluator_container_index_sha256 is None + or reader.index.index_sha256 != evaluator_container_index_sha256 + ): + raise ValueError("evaluator container index identity changed") + member_records = { + member.logical_path: { + "logical_path": member.logical_path, + "kind": member.kind.name.lower(), + "length": member.length, + "sha256": member.sha256, + } + for member in reader.members + } + container_record = { + "path": evaluator_container_path, + "sha256": sha256_file(container), + "size_bytes": container.stat().st_size, + "index_sha256": reader.index.index_sha256, + "member_count": len(reader.members), + } + + loose_objects: dict[str, dict[str, object]] = { + execution_path: { + "path": execution_path, + "sha256": execution_sha256, + "size_bytes": (artifact_root / execution_path).stat().st_size, + } + } + used_members: set[str] = set() + lane_records: list[dict[str, object]] = [] + roles: list[dict[str, str]] = [] + for lane_id, lane in lanes: + referenced_loose: set[str] = {execution_path} + referenced_members: set[str] = set() + + def include( + relative: str, + lane_loose: set[str], + lane_members: set[str], + ) -> None: + if relative in member_records: + lane_members.add(relative) + used_members.add(relative) + return + loose = _resolve_loose_payload(artifact_root, process_id, relative) + if loose is None: + # Some manifest paths are logical roots interpreted by the + # runtime rather than physical files. Preserve them in the + # lane source map, but never claim them as persisted objects. + return + if loose in lane_loose: + return + lane_loose.add(loose) + path = artifact_root / loose + loose_objects.setdefault( + loose, + { + "path": loose, + "sha256": sha256_file(path), + "size_bytes": path.stat().st_size, + }, + ) + if path.suffix == ".json": + try: + nested = json.loads(path.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError) as error: + raise ValueError( + f"referenced lane manifest is not valid JSON: {loose}" + ) from error + for nested_relative in _payload_paths(nested): + include(nested_relative, lane_loose, lane_members) + + for relative in _payload_paths(lane): + include(relative, referenced_loose, referenced_members) + metrics = _structural_metrics(lane) + subtree_sha = canonical_sha256("execution-lane-subtree-v1", lane) + metric_sha = canonical_sha256("execution-lane-structural-metrics-v1", metrics) + lane_records.append( + { + "lane_id": lane_id, + "subtree_sha256": subtree_sha, + "structural_metrics_sha256": metric_sha, + "structural_metrics": metrics, + "loose_payload_paths": sorted(referenced_loose), + "pacbin_member_paths": sorted(referenced_members), + } + ) + roles.extend( + { + "lane_id": lane_id, + "object_kind": "loose-payload", + "object_path": path, + } + for path in sorted(referenced_loose) + ) + roles.extend( + { + "lane_id": lane_id, + "object_kind": "pacbin-member", + "object_path": path, + } + for path in sorted(referenced_members) + ) + + inventory: dict[str, object] = { + "status": "complete", + "loose_payloads": [loose_objects[path] for path in sorted(loose_objects)], + "pacbin_container": container_record, + "pacbin_members": [member_records[path] for path in sorted(used_members)], + "lanes": lane_records, + "roles": roles, + } + inventory["inventory_sha256"] = canonical_sha256( + "physical-lane-inventory-v1", + inventory, + ) + proof: dict[str, object] = { + "schema": SCHEMA, + "status": "complete", + "process_id": process_id, + "source_identity": { + "git_revision": source_revision, + "native_build_inputs_sha256": native_build_inputs_sha256, + }, + "execution": { + "path": execution_path, + "sha256": execution_sha256, + "kind": execution.get("kind"), + "color_accuracy": execution.get("color_accuracy"), + }, + "semantic_maps": semantic_map_witnesses(execution), + "physical_lane_inventory": inventory, + } + proof["proof_content_sha256"] = canonical_sha256( + "generation-structural-source-proof-v1", + proof, + ) + return proof + + +def validate_generation_structural_proof( + raw: Mapping[str, object], + *, + artifact_root: Path, + expected_process_id: str, + expected_source_revision: str, + expected_native_build_inputs_sha256: str | None = None, + expected_execution_path: str | None = None, + expected_execution_sha256: str | None = None, + execution: Mapping[str, object] | None = None, +) -> dict[str, object]: + """Recompute a generation proof and authenticate its persisted inventory.""" + + proof = dict(raw) + if proof.get("schema") != SCHEMA or proof.get("status") != "complete": + raise ValueError("generation structural proof schema/status is invalid") + if proof.get("process_id") != expected_process_id: + raise ValueError("generation structural proof process identity changed") + source_identity = proof.get("source_identity") + if not isinstance(source_identity, Mapping) or ( + source_identity.get("git_revision") != expected_source_revision + ): + raise ValueError("generation structural proof source revision changed") + native_inputs = source_identity.get("native_build_inputs_sha256") + if ( + not isinstance(native_inputs, str) + or len(native_inputs) != 64 + or ( + expected_native_build_inputs_sha256 is not None + and native_inputs != expected_native_build_inputs_sha256 + ) + ): + raise ValueError("generation structural proof native-input identity changed") + execution_identity = proof.get("execution") + if not isinstance(execution_identity, Mapping): + raise ValueError("generation structural proof execution identity is absent") + execution_path = execution_identity.get("path") + execution_sha = execution_identity.get("sha256") + if ( + not isinstance(execution_path, str) + or not isinstance(execution_sha, str) + or ( + expected_execution_path is not None + and execution_path != expected_execution_path + ) + or ( + expected_execution_sha256 is not None + and execution_sha != expected_execution_sha256 + ) + ): + raise ValueError("generation structural proof execution identity changed") + execution_file = artifact_root / execution_path + if ( + not execution_file.is_file() + or execution_file.is_symlink() + or sha256_file(execution_file) != execution_sha + ): + raise ValueError("generation structural proof execution payload changed") + if execution is None: + loaded = json.loads(execution_file.read_text(encoding="utf-8")) + if not isinstance(loaded, Mapping): + raise ValueError("generation structural execution is not an object") + execution = loaded + if execution_identity.get("kind") != execution.get("kind") or ( + execution_identity.get("color_accuracy") != execution.get("color_accuracy") + ): + raise ValueError("generation structural execution semantics changed") + + maps = proof.get("semantic_maps") + if not isinstance(maps, Mapping): + raise ValueError("generation structural semantic maps are absent") + expected_maps = semantic_map_witnesses(execution) + if dict(maps) != expected_maps: + raise ValueError("generation structural semantic maps do not recompute") + + inventory = proof.get("physical_lane_inventory") + if not isinstance(inventory, Mapping) or inventory.get("status") != "complete": + raise ValueError("generation structural physical inventory is incomplete") + inventory_value = dict(inventory) + declared_inventory_sha = inventory_value.pop("inventory_sha256", None) + if declared_inventory_sha != canonical_sha256( + "physical-lane-inventory-v1", + inventory_value, + ): + raise ValueError("generation structural inventory digest changed") + + loose = inventory.get("loose_payloads") + if not isinstance(loose, list) or not loose: + raise ValueError("generation structural loose-payload inventory is empty") + loose_paths: set[str] = set() + content_ids: set[str] = set() + for index, item in enumerate(loose): + if not isinstance(item, Mapping): + raise ValueError(f"generation structural loose payload {index} is invalid") + path = item.get("path") + digest = item.get("sha256") + size = item.get("size_bytes") + if ( + not isinstance(path, str) + or path in loose_paths + or not isinstance(digest, str) + or not isinstance(size, int) + or isinstance(size, bool) + or size < 0 + ): + raise ValueError(f"generation structural loose payload {index} is invalid") + loose_paths.add(path) + concrete = artifact_root / path + if ( + not concrete.is_file() + or concrete.is_symlink() + or concrete.stat().st_size != size + or sha256_file(concrete) != digest + ): + raise ValueError(f"generation structural loose payload {path} changed") + content_ids.add(digest) + + members = inventory.get("pacbin_members") + if not isinstance(members, list): + raise ValueError("generation structural pacbin member inventory is invalid") + container = inventory.get("pacbin_container") + actual_members: dict[str, dict[str, object]] = {} + if container is not None: + if not isinstance(container, Mapping): + raise ValueError("generation structural pacbin container is invalid") + path = container.get("path") + if not isinstance(path, str): + raise ValueError("generation structural pacbin container path is invalid") + concrete = artifact_root / path + if ( + not concrete.is_file() + or concrete.is_symlink() + or concrete.stat().st_size != container.get("size_bytes") + or sha256_file(concrete) != container.get("sha256") + ): + raise ValueError("generation structural pacbin container changed") + with PacbinReader.open(concrete, verify_payloads=True) as reader: + if reader.index.index_sha256 != container.get("index_sha256") or len( + reader.members + ) != container.get("member_count"): + raise ValueError("generation structural pacbin index changed") + actual_members = { + member.logical_path: { + "logical_path": member.logical_path, + "kind": member.kind.name.lower(), + "length": member.length, + "sha256": member.sha256, + } + for member in reader.members + } + declared_member_paths: set[str] = set() + for index, item in enumerate(members): + if not isinstance(item, Mapping): + raise ValueError(f"generation structural pacbin member {index} is invalid") + path = item.get("logical_path") + if not isinstance(path, str) or path in declared_member_paths: + raise ValueError(f"generation structural pacbin member {index} is invalid") + declared_member_paths.add(path) + if actual_members.get(path) != dict(item): + raise ValueError(f"generation structural pacbin member {path} changed") + digest = item.get("sha256") + if isinstance(digest, str): + content_ids.add(digest) + if container is None and members: + raise ValueError("generation structural pacbin members lack a container") + + lane_values = inventory.get("lanes") + roles = inventory.get("roles") + if not isinstance(lane_values, list) or not isinstance(roles, list): + raise ValueError("generation structural lane/role inventory is invalid") + expected_lanes = execution_lanes(execution) + if len(lane_values) != len(expected_lanes): + raise ValueError("generation structural lane count changed") + lane_ids: set[str] = set() + for raw_lane, (lane_id, lane) in zip( + lane_values, + expected_lanes, + strict=True, + ): + if not isinstance(raw_lane, Mapping) or raw_lane.get("lane_id") != lane_id: + raise ValueError("generation structural lane identity changed") + if lane_id in lane_ids: + raise ValueError("generation structural lane identity is duplicated") + lane_ids.add(lane_id) + metrics = _structural_metrics(lane) + if ( + raw_lane.get("subtree_sha256") + != canonical_sha256("execution-lane-subtree-v1", lane) + or raw_lane.get("structural_metrics") != metrics + or raw_lane.get("structural_metrics_sha256") + != canonical_sha256("execution-lane-structural-metrics-v1", metrics) + ): + raise ValueError("generation structural lane evidence changed") + loose_refs = raw_lane.get("loose_payload_paths") + member_refs = raw_lane.get("pacbin_member_paths") + if ( + not isinstance(loose_refs, list) + or not set(loose_refs) <= loose_paths + or not isinstance(member_refs, list) + or not set(member_refs) <= declared_member_paths + ): + raise ValueError("generation structural lane references are incomplete") + expected_roles = { + ( + str(lane["lane_id"]), + "loose-payload", + str(path), + ) + for lane in lane_values + if isinstance(lane, Mapping) + for path in lane.get("loose_payload_paths", ()) + } | { + ( + str(lane["lane_id"]), + "pacbin-member", + str(path), + ) + for lane in lane_values + if isinstance(lane, Mapping) + for path in lane.get("pacbin_member_paths", ()) + } + actual_roles = { + ( + str(role.get("lane_id")), + str(role.get("object_kind")), + str(role.get("object_path")), + ) + for role in roles + if isinstance(role, Mapping) + } + if actual_roles != expected_roles or len(actual_roles) != len(roles): + raise ValueError("generation structural role coverage is not exact") + + declared_proof_sha = proof.pop("proof_content_sha256", None) + if declared_proof_sha != canonical_sha256( + "generation-structural-source-proof-v1", + proof, + ): + raise ValueError("generation structural proof content digest changed") + return dict(raw) + + +__all__ = [ + "ROLE", + "SCHEMA", + "SEMANTIC_MAP_DOMAINS", + "build_generation_structural_proof", + "canonical_sha256", + "execution_lanes", + "semantic_map_witnesses", + "validate_generation_structural_proof", +] diff --git a/pyamplicol/src/pyamplicol/generation/umami_semantics.py b/pyamplicol/src/pyamplicol/generation/umami_semantics.py new file mode 100644 index 0000000000..ca2a6371fc --- /dev/null +++ b/pyamplicol/src/pyamplicol/generation/umami_semantics.py @@ -0,0 +1,461 @@ +# SPDX-License-Identifier: 0BSD +"""Conservative, model-independent equality keys for UMAMI representatives. + +These keys describe the executed recurrence circuit, not its particle names or +its values at sample points. Local callable expressions are interned after +binding their inputs; mutable parameter names remain free symbols. Different +arena layouts can miss an equality, which is preferable to guessing one. +This export-only analysis never modifies the numerical schedule. +""" + +from __future__ import annotations + +import hashlib +import json +import re +from collections.abc import Mapping, Sequence +from typing import TYPE_CHECKING + +from .._internal.physics.symbols import ModelSymbolRegistry +from ..api.errors import ArtifactError + +if TYPE_CHECKING: + from ..models.contracts import CompiledModelIR + from ..models.prepared import PreparedKernelPack, PreparedKernelRecord + from .recurrence_schedule_sharing import RecurrenceProcessRemap + +_TOKEN = re.compile(r"[A-Za-z_][A-Za-z_0-9]*(?:::[A-Za-z_][A-Za-z_0-9]*)*") +_NONE = (1 << 32) - 1 + + +def _key(value: object) -> str: + digest = hashlib.sha256() + for chunk in json.JSONEncoder( + sort_keys=True, separators=(",", ":"), allow_nan=False + ).iterencode(value): + digest.update(chunk.encode("utf-8")) + return digest.hexdigest() + + +class ParameterSemantics: + """Resolve definitions, never substitute the defaults of external inputs.""" + + def __init__( + self, + ir: CompiledModelIR | Mapping[str, object], + catalog_parameters: Sequence[object] = (), + ): + payload = ir if isinstance(ir, Mapping) else ir.to_dict() + self._parameters = {str(p["name"]): p for p in _objects(payload["parameters"])} + self._couplings = {str(c["name"]): c for c in _objects(payload["couplings"])} + parameter_names = { + p.template_id: p.name + for p in catalog_parameters + if hasattr(p, "template_id") + } + # Contact decomposition can introduce derived couplings not present in + # the original UFO IR. The catalog already certifies their exact + # expression and external dependencies; do not compare their defaults. + self._derived = { + p.name: ( + p.value_type, + p.exact_expression_digest, + tuple(parameter_names[i] for i in p.dependency_parameter_ids), + ) + for p in catalog_parameters + if getattr(p, "parameter_kind", None) == "derived" + } + registry = ModelSymbolRegistry(str(payload["name"])) + self._names = { + spelling: name + for name in (*self._parameters, *self._couplings, *self._derived) + for spelling in (name, registry.qualified_name(name)) + } + self._cache: dict[str, object] = {} + self._active: set[str] = set() + + def parameter(self, name: str) -> object: + if name in self._cache: + return self._cache[name] + if name in self._active: + raise ValueError("cyclic symbolic parameter definition") + self._active.add(name) + try: + parameter = self._parameters.get(name) + coupling = self._couplings.get(name) + if parameter is not None: + if parameter["nature"] == "external": + value: object = ("external", name, parameter["parameter_type"]) + elif parameter.get("expression") is not None: + value = self.expression( + str( + parameter.get("resolved_expression") + or parameter["expression"] + ) + ) + else: + value = ( + "constant", + parameter["parameter_type"], + parameter.get("value"), + ) + elif coupling is not None: + value = self.expression( + str(coupling.get("resolved_expression") or coupling["expression"]) + ) + elif name in self._derived: + value_type, expression_digest, dependencies = self._derived[name] + if not expression_digest: + raise ValueError("derived parameter has no exact expression") + value = ( + "derived-catalog-parameter", + value_type, + expression_digest, + tuple(self.parameter(dependency) for dependency in dependencies), + ) + else: + # Runtime-owned parameters retain their actual names. There + # is no model-specific interpretation or default-value match. + value = ("runtime-parameter", name) + self._cache[name] = value + return value + finally: + self._active.remove(name) + + def expression( + self, expression: str, bindings: Mapping[str, object] | None = None + ) -> object: + local = bindings or {} + # A prepared input can be a Symbolica function atom such as p(0), + # rather than an identifier. Match complete declared inputs before + # ordinary tokens, so their roles/components cannot be lost. + scanner = ( + re.compile( + r"(? str: + bindings: dict[str, object] = {} + for contract in kernel.input_contracts: + role = str(contract["role"]) + component = int(contract["component"]) + if role == "model-parameter": + name = parameter_names[int(contract["model_parameter_index"])] + value: object = (role, parameters.parameter(name), component) + elif role in {"coupling-real", "coupling-imag"}: + value = (role, coupling, component) + else: + value = (role, component) + bindings[str(contract["symbol"])] = value + return _key( + ( + kernel.contract_kind, + # The ordered expressions define the output coordinates. Display + # labels may contain auxiliary/contact IDs, which are not algebra. + len(kernel.output_layout), + parent_permutation, + [ + parameters.expression(expr, bindings) + for expr in kernel.exact_expressions + ], + ) + ) + + +def _exact_executor_couplings(pack, exact_ids: Mapping[int, int]) -> dict[int, object]: + """Bind exact rationals, without Decimal rounding at ambient precision.""" + catalog = pack.recurrence_template_catalog + records = { + record.template_id: record + for record in (*catalog.transitions, *catalog.closures) + } + kernels = {record.kernel_id: record for record in pack.kernels} + result = {} + for direct in pack.recurrence_direct_template_catalog.templates: + kernel_id = exact_ids.get(direct.direct_executor_id) + if kernel_id is None or not any( + contract["role"] in {"coupling-real", "coupling-imag"} + for contract in kernels[kernel_id].input_contracts + ): + continue + values = set() + for template_id in direct.semantic_template_ids: + value = records[template_id].binding_coupling + values.add( + ( + int(value.real_numerator), + int(value.real_denominator), + int(value.imag_numerator), + int(value.imag_denominator), + ) + ) + if len(values) != 1: + raise ValueError("executor has no uniform exact coupling") + result[direct.direct_executor_id] = next(iter(values)) + return result + + +def recurrence_structural_keys( + *, + exact_sections: Mapping[str, object], + runtime_metadata: Mapping[str, object], + kernel_pack: PreparedKernelPack, + ir: CompiledModelIR | Mapping[str, object], + physics: Mapping[str, object], + remap: RecurrenceProcessRemap, + color_contraction_payload: bytes | None, +) -> dict[str, str]: + """Prove equality only for fully comparable, unremapped exact circuits. + + The same ordered instruction graph and fully bound local operations imply the + same amplitudes for every momentum/parameter value. Colour contraction and + normalization are included, so equality is of the complete squared observable. + Unknown plans return no key; ordinary generation/evaluation stays available. + """ + if exact_sections.get("abi") != "pyamplicol-recurrence-exact-sections-v1": + return {} + if physics.get("color_accuracy") != "lc" and color_contraction_payload is None: + return {} + if ( + remap.source_slots != tuple(range(len(remap.source_slots))) + or any( + v != 1 for v in (*remap.source_momentum_signs, *remap.source_helicity_signs) + ) + or remap.state_template_changes + or remap.source_template_changes + or remap.direct_executor_changes + or remap.parameter_slot_changes + or any( + remap.source_state_indices[start:end] != tuple(range(end - start)) + for start, end in zip( + remap.source_state_offsets[:-1], + remap.source_state_offsets[1:], + strict=True, + ) + ) + or remap.public_flow_ids != tuple(range(len(remap.public_flow_ids))) + or remap.physical_sector_ids != tuple(range(len(remap.physical_sector_ids))) + ): + return {} + catalog = kernel_pack.recurrence_template_catalog + direct_catalog = kernel_pack.recurrence_direct_template_catalog + if catalog is None or direct_catalog is None: + return {} + # Reuse the exact executor's authoritative bindings, including intrinsics + # whose algebraic kernel is distinct from their native callable payload. + from ..runtime.recurrence_exact._plan import ( + _executor_exact_kernel_ids, + _executor_parent_permutations, + ) + + try: + exact_ids = _executor_exact_kernel_ids(kernel_pack) + couplings = _exact_executor_couplings(kernel_pack, exact_ids) + parent_orders = _executor_parent_permutations(kernel_pack) + parameter_names = { + p.prepared_parameter_id: p.name + for p in catalog.parameters + if p.prepared_parameter_id is not None + } + parameters = ParameterSemantics(ir, catalog.parameters) + kernels = {k.kernel_id: k for k in kernel_pack.kernels} + # The exported executor catalog can cover the entire prepared model; + # only referenced operations are part of this process's circuit. + used_executors = {int(r[3]) for r in _rows(exact_sections["row_groups"])} | { + int(r[10]) for r in _rows(exact_sections["source_dispatch_variants"]) + } + used_executors.discard(_NONE) + executor_keys: dict[int, str] = {} + for row in _rows(exact_sections["executors"]): + executor_id = int(row[0]) + if executor_id not in used_executors: + continue + kernel_id = exact_ids.get(executor_id) + expression = ( + _kernel_key( + kernels[kernel_id], + parameters, + parameter_names, + couplings.get(executor_id, ()), + parent_orders.get(executor_id, (0, 1)), + ) + if kernel_id is not None + else ("intrinsic", row[7]) + ) + if kernel_id is None: + # Source algorithms are specified by the source IR below; + # diagonal closure reduction uses the retained exact factors + # and closure rows. The identity finalizer is parameter-free. + from ..models.recurrence_direct_template import ( + RECURRENCE_DIRECT_IDENTITY_FINALIZER, + ) + + closure_reduction = row[1] == "closure" and re.fullmatch( + r"rusticol\.closure-reduce\.v1:[0-9a-f]{24}", str(row[7]) + ) + if row[7] is None or ( + row[1] != "source" + and not closure_reduction + and row[7] != RECURRENCE_DIRECT_IDENTITY_FINALIZER + ): + return {} + if row[1] == "source": + # The suffix identifies a source's full catalog record + # (including flavour labels); its executable algorithm is + # the prefix and its bound physics is the SourceIR below. + # Strip only this documented digest format, not arbitrary + # text from an unfamiliar source implementation. + source_algorithm = re.fullmatch( + r"(rusticol\.source-fill\.[a-z0-9_-]+\.v1):[0-9a-f]{24}", + str(row[7]), + ) + if source_algorithm is None: + return {} + expression = ("intrinsic", source_algorithm[1]) + executor_keys[executor_id] = _key((row[1:6], expression)) + source_keys: dict[int, str] = {} + for item in _objects(runtime_metadata["source_templates"]): + source_ir = _mapping(item["source_ir"]) + identity = _mapping(source_ir["identity"]) + mass_name = source_ir.get("mass_parameter") + source_keys[int(item["source_template_id"])] = _key( + ( + item["dimension"], + item["helicity"], + item["chirality"], + item["spin_state"], + source_ir["wavefunction_family"], + identity["orientation"], + item["crossing"], + source_ir["statistics"], + source_ir["component_dimension"], + source_ir["basis"], + None + if source_ir.get("width_parameter") is None + else parameters.parameter(str(source_ir["width_parameter"])), + None if mass_name is None else parameters.parameter(str(mass_name)), + ) + ) + program = { + k: v + for k, v in exact_sections.items() + if k + not in { + "process_id", + "semantic_digest", + "runtime_layout_digest", + "executors", + } + } + # Current IDs and state-template IDs are diagnostic; the actual arena + # ranges, stage clearing, bound source operations and kernels remain. + program["currents"] = [ + [None, r[1], None, *r[3:]] for r in _rows(program["currents"]) + ] + program["row_groups"] = [ + [*r[:3], None if r[3] == _NONE else executor_keys[int(r[3])], *r[4:]] + for r in _rows(program["row_groups"]) + ] + if program["strategy"] != "all-flow-union": + program["sources"] = [ + [*r[:3], source_keys[int(r[3])], *r[4:]] + for r in _rows(program["sources"]) + ] + program["source_dispatch_variants"] = [ + [ + *r[:6], + source_keys[int(r[6])], + None, + None, + r[9], + None if r[10] == _NONE else executor_keys[int(r[10])], + *r[11:], + ] + for r in _rows(program["source_dispatch_variants"]) + ] + # Keep all normalization dependencies, not just their default number. + normalization = runtime_metadata["normalization"] + external_layout = [ + (v["source_slot"], v["is_initial"]) + for v in _objects(runtime_metadata["external_legs"]) + ] + parameter_projection = [ + ( + v["runtime_slot"], + parameters.parameter(str(v["runtime_name"])), + v["component"], + ) + for v in _objects(runtime_metadata["parameter_projection"]) + ] + color_key = ( + None + if color_contraction_payload is None + else hashlib.sha256(color_contraction_payload).hexdigest() + ) + circuit = _key( + ( + program, + normalization, + external_layout, + parameter_projection, + physics["color_accuracy"], + color_key, + [h["values"] for h in _objects(physics["helicities"])], + ) + ) + return { + str(c["id"]): _key((circuit, c["index"], c.get("coefficient", 1.0))) + for c in _objects(physics["color_components"]) + } + except (ArtifactError, KeyError, TypeError, ValueError, IndexError): + # Unsupported/missing semantic data cannot authorize grouping. + return {} + + +def _mapping(value: object) -> Mapping[str, object]: + if not isinstance(value, Mapping): + raise TypeError("semantic record must be a mapping") + return value + + +def _sequence(value: object) -> Sequence[object]: + if isinstance(value, str | bytes) or not isinstance(value, Sequence): + raise TypeError("semantic records must be a sequence") + return value + + +def _rows(value: object) -> tuple[Sequence[object], ...]: + return tuple(_sequence(v) for v in _sequence(value)) + + +def _objects(value: object) -> tuple[Mapping[str, object], ...]: + return tuple(_mapping(v) for v in _sequence(value)) diff --git a/pyamplicol/src/pyamplicol/generation/validation.py b/pyamplicol/src/pyamplicol/generation/validation.py new file mode 100644 index 0000000000..f3dff78d21 --- /dev/null +++ b/pyamplicol/src/pyamplicol/generation/validation.py @@ -0,0 +1,226 @@ +# SPDX-License-Identifier: 0BSD +"""Deterministic validation-point records for generated process artifacts.""" + +from __future__ import annotations + +import hashlib +import math +from collections.abc import Mapping +from dataclasses import dataclass, replace +from typing import TYPE_CHECKING + +from ..models.base import Model +from .dag_types import GenericDAG +from .phase_space import massive_rambo_final_state + +if TYPE_CHECKING: + from .contracts import CanonicalProcessIR + +FourMomentum = tuple[float, float, float, float] + + +@dataclass(frozen=True, slots=True) +class ValidationPointRecord: + process_id: str + process: str + seed: int + particles: tuple[tuple[int, FourMomentum], ...] = () + error: str | None = None + + @property + def available(self) -> bool: + return self.error is None + + @property + def four_vectors(self) -> tuple[FourMomentum, ...]: + if not self.available: + return () + return tuple(momentum for _pdg, momentum in self.particles) + + def to_mapping(self) -> dict[str, object]: + points: list[object] = [] + if self.available: + points.append( + [ + { + "pdg": pdg, + "momentum": [_decimal_string(value) for value in momentum], + } + for pdg, momentum in self.particles + ] + ) + return { + "schema_version": 1, + "kind": "pyamplicol-rusticol-validation-momenta", + "process_id": self.process_id, + "process": self.process, + "seed": self.seed, + "available": self.available, + "error": self.error, + "points": points, + } + + +def build_validation_point( + dag: GenericDAG, + model: Model, + *, + process_id: str, + seed: int, +) -> ValidationPointRecord: + return build_process_validation_point( + dag.process, + model, + process_id=process_id, + seed=seed, + ) + + +def build_process_validation_point( + process: CanonicalProcessIR, + model: Model, + *, + process_id: str, + seed: int, +) -> ValidationPointRecord: + """Build validation momenta without requiring a materialized DAG.""" + + try: + particles = _build_particles(process, model, seed=seed) + except (ArithmeticError, KeyError, RuntimeError, ValueError) as exc: + return ValidationPointRecord( + process_id=process_id, + process=process.process, + seed=seed, + error=str(exc), + ) + return ValidationPointRecord( + process_id=process_id, + process=process.process, + seed=seed, + particles=particles, + ) + + +def rotate_validation_point( + point: ValidationPointRecord, + *, + rotation_seed: int, +) -> ValidationPointRecord: + """Apply a deterministic proper rotation to one physical event.""" + + if not point.available: + return point + digest = hashlib.sha256( + f"{rotation_seed}:proper-spatial-rotation-v1".encode("ascii") + ).digest() + denominator = float(1 << 64) + azimuth = 2.0 * math.pi * ( + (int.from_bytes(digest[:8], "big") + 0.5) / denominator + ) + cosine_polar = ( + 2.0 * ((int.from_bytes(digest[8:16], "big") + 0.5) / denominator) + - 1.0 + ) + sine_polar = math.sqrt(max(0.0, 1.0 - cosine_polar * cosine_polar)) + cos_azimuth = math.cos(azimuth) + sin_azimuth = math.sin(azimuth) + # Rz(azimuth) Ry(polar), a proper orthogonal map taking +z to the + # deterministic direction above. + rotation = ( + ( + cos_azimuth * cosine_polar, + -sin_azimuth, + cos_azimuth * sine_polar, + ), + ( + sin_azimuth * cosine_polar, + cos_azimuth, + sin_azimuth * sine_polar, + ), + (-sine_polar, 0.0, cosine_polar), + ) + + def rotated(momentum: FourMomentum) -> FourMomentum: + energy, x_component, y_component, z_component = momentum + spatial = (x_component, y_component, z_component) + return ( + energy, + *tuple( + sum(row[column] * spatial[column] for column in range(3)) + for row in rotation + ), + ) + + return replace( + point, + particles=tuple( + (pdg, rotated(momentum)) for pdg, momentum in point.particles + ), + ) + + +def validation_point_map( + records: tuple[ValidationPointRecord, ...], +) -> Mapping[str, Mapping[str, object]]: + return {record.process_id: record.to_mapping() for record in records} + + +def _build_particles( + process: CanonicalProcessIR, + model: Model, + *, + seed: int, +) -> tuple[tuple[int, FourMomentum], ...]: + initial_pdgs = tuple(int(pdg) for pdg in process.initial_pdgs) + final_pdgs = tuple(int(pdg) for pdg in process.final_pdgs) + if len(initial_pdgs) != 2: + raise ValueError("validation momenta require a two-particle initial state") + if not final_pdgs: + raise ValueError("validation momenta require at least one final-state particle") + final_masses = tuple(_mass(model, pdg) for pdg in final_pdgs) + threshold = sum(final_masses) + if len(final_pdgs) == 1: + if threshold <= 0.0: + raise ValueError( + "no finite center-of-mass point exists for one massless final state" + ) + sqrt_s = threshold + final_momenta: tuple[FourMomentum, ...] = ((float(sqrt_s), 0.0, 0.0, 0.0),) + else: + sqrt_s = max(1000.0, threshold + 100.0) + final_momenta = massive_rambo_final_state( + len(final_pdgs), + sqrt_s=sqrt_s, + masses=final_masses, + seed=seed, + ) + beam_energy = 0.5 * sqrt_s + particles = ( + (initial_pdgs[0], (beam_energy, 0.0, 0.0, beam_energy)), + (initial_pdgs[1], (beam_energy, 0.0, 0.0, -beam_energy)), + *tuple(zip(final_pdgs, final_momenta, strict=True)), + ) + if not all(math.isfinite(value) for _, momentum in particles for value in momentum): + raise ArithmeticError("validation momenta contain non-finite components") + return particles + + +def _mass(model: Model, pdg: int) -> float: + value = float(model.mass(pdg)) + if not math.isfinite(value) or value < 0.0: + raise ValueError(f"particle {pdg} has invalid mass {value!r}") + return value + + +def _decimal_string(value: float) -> str: + return format(float(value), ".17g") + + +__all__ = [ + "ValidationPointRecord", + "build_process_validation_point", + "build_validation_point", + "rotate_validation_point", + "validation_point_map", +] diff --git a/pyamplicol/src/pyamplicol/licensing.py b/pyamplicol/src/pyamplicol/licensing.py new file mode 100644 index 0000000000..f8bf9bda9f --- /dev/null +++ b/pyamplicol/src/pyamplicol/licensing.py @@ -0,0 +1,311 @@ +# SPDX-License-Identifier: 0BSD +"""Symbolica licensing and generation-resource policy.""" + +from __future__ import annotations + +import importlib +import os +import sys +import threading +from collections.abc import Callable, Sequence +from dataclasses import dataclass +from types import ModuleType +from typing import TextIO + +from pyamplicol.config import ( + Action, + ClampRequest, + ConfigResolution, + GenerationConfig, + RunConfig, + config_to_dict, + resolve_config, +) + +_SUGGESTION_LOCK = threading.Lock() +_SUGGESTION_EMITTED = False +_RESOURCE_PATHS = frozenset( + { + "generation.workers", + "evaluator.optimization.cores", + } +) + + +@dataclass(frozen=True, slots=True) +class SymbolicaLicenseState: + licensed: bool + restricted: bool + + +def _load_symbolica() -> ModuleType: + return importlib.import_module("symbolica") + + +def prepare_symbolica_environment(*, suppress_banner: bool) -> None: + """Set banner policy before Symbolica's first import.""" + + if suppress_banner: + os.environ["SYMBOLICA_HIDE_BANNER"] = "1" + + +def detect_symbolica_license( + *, + suggest: bool = True, + json_mode: bool = False, + stream: TextIO | None = None, + loader: Callable[[], ModuleType] = _load_symbolica, +) -> SymbolicaLicenseState: + """Query Symbolica's actual package-or-personal license state. + + This call must stay within pyamplicol: OEM licenses are package-scoped. + """ + + prepare_symbolica_environment(suppress_banner=not suggest or json_mode) + module = loader() + checker = getattr(module, "is_licensed", None) + if not callable(checker): + raise RuntimeError("installed Symbolica does not provide is_licensed()") + licensed = bool(checker()) + if not licensed and suggest and not json_mode: + _suggest_license(sys.stderr if stream is None else stream) + return SymbolicaLicenseState(licensed=licensed, restricted=not licensed) + + +def _suggest_license(stream: TextIO) -> None: + global _SUGGESTION_EMITTED + with _SUGGESTION_LOCK: + if _SUGGESTION_EMITTED: + return + stream.write( + "pyAmpliCol is using Symbolica restricted mode (one generation " + "worker and one Symbolica core). Request a free license with " + "'pyamplicol request-symbolica-trial-license' or " + "'pyamplicol request-symbolica-hobbyist-license'.\n" + ) + stream.flush() + _SUGGESTION_EMITTED = True + + +def reset_suggestion_state_for_tests() -> None: + global _SUGGESTION_EMITTED + with _SUGGESTION_LOCK: + _SUGGESTION_EMITTED = False + + +def _cpu_budget() -> int: + affinity = getattr(os, "sched_getaffinity", None) + if callable(affinity): + try: + return max(1, len(affinity(0))) + except OSError: + pass + return max(1, os.cpu_count() or 1) + + +def symbolica_resource_clamps( + config: RunConfig, + state: SymbolicaLicenseState, + *, + cpu_budget: int | None = None, + process_count: int | None = None, +) -> tuple[ClampRequest, ...]: + """Share one CPU budget across concrete-process workers and evaluator cores. + + Licensed partitioning is deferred when the concrete process count is not yet + known. Restricted mode does not need that count and is always clamped eagerly. + """ + + if cpu_budget is not None and ( + isinstance(cpu_budget, bool) + or not isinstance(cpu_budget, int) + or cpu_budget < 1 + ): + raise ValueError("cpu_budget must be a positive integer") + if process_count is not None and ( + isinstance(process_count, bool) + or not isinstance(process_count, int) + or process_count < 1 + ): + raise ValueError("process_count must be a positive integer") + requested_workers = config.generation.workers + requested_cores = config.evaluator.optimization.cores + clamps: list[ClampRequest] = [] + + if state.restricted: + if requested_workers != 1: + clamps.append( + ClampRequest( + "generation.workers", + 1, + "Symbolica restricted mode permits one generation instance", + ) + ) + if requested_cores != 1: + clamps.append( + ClampRequest( + "evaluator.optimization.cores", + 1, + "Symbolica restricted mode permits one Symbolica core", + ) + ) + return tuple(clamps) + + if process_count is None: + return () + + budget = _cpu_budget() if cpu_budget is None else cpu_budget + assert budget is not None + workers = ( + min(process_count, budget) + if requested_workers == "auto" + else min(requested_workers, process_count, budget) + ) + if requested_workers != workers: + clamps.append( + ClampRequest( + "generation.workers", + workers, + "shared affinity-aware CPU budget for concurrent process generation", + ) + ) + cores = ( + max(1, budget // workers) + if requested_cores == "auto" + else min(requested_cores, max(1, budget // workers)) + ) + if requested_cores != cores: + clamps.append( + ClampRequest( + "evaluator.optimization.cores", + cores, + "shared affinity-aware CPU budget for Symbolica evaluator work", + ) + ) + return tuple(clamps) + + +def resolve_symbolica_resource_config( + config: GenerationConfig | RunConfig | ConfigResolution | None, + state: SymbolicaLicenseState, + *, + cpu_budget: int | None = None, + process_count: int | None = None, +) -> ConfigResolution: + """Apply current resource policy without losing original provenance. + + Resource clamps are recalculated from the original requested values. This + removes provisional or stale process-count clamps while preserving unrelated + adjustments and their ordering. + """ + + if isinstance(config, ConfigResolution): + requested = config.requested + inherited = config.clamps + else: + requested = ( + RunConfig(action=Action.GENERATE) + if config is None + else RunConfig(action=Action.GENERATE, generation=config) + if isinstance(config, GenerationConfig) + else config + ) + inherited = () + + preserved = tuple( + ClampRequest(item.path, item.effective, item.reason) + for item in inherited + if item.path not in _RESOURCE_PATHS + ) + base = resolve_config(config_to_dict(requested), clamps=preserved) + policy = symbolica_resource_clamps( + base.effective, + state, + cpu_budget=cpu_budget, + process_count=process_count, + ) + replacements = {item.path: item for item in policy} + merged: list[ClampRequest] = [] + emitted: set[str] = set() + for inherited_clamp in inherited: + if inherited_clamp.path in _RESOURCE_PATHS: + replacement = replacements.get(inherited_clamp.path) + if replacement is not None and inherited_clamp.path not in emitted: + merged.append(replacement) + emitted.add(inherited_clamp.path) + continue + merged.append( + ClampRequest( + inherited_clamp.path, + inherited_clamp.effective, + inherited_clamp.reason, + ) + ) + for policy_clamp in policy: + if policy_clamp.path not in emitted: + merged.append(policy_clamp) + emitted.add(policy_clamp.path) + + return resolve_config(config_to_dict(requested), clamps=merged) + + +def request_trial_license( + name: str, + email: str, + organization: str, + *, + loader: Callable[[], ModuleType] = _load_symbolica, +) -> None: + _validate_identity((name, email, organization), requires_organization=True) + function = getattr(loader(), "request_trial_license", None) + if not callable(function): + raise RuntimeError( + "installed Symbolica does not provide request_trial_license()" + ) + function(name, email, organization) + + +def request_hobbyist_license( + name: str, + email: str, + *, + loader: Callable[[], ModuleType] = _load_symbolica, +) -> None: + _validate_identity((name, email), requires_organization=False) + function = getattr(loader(), "request_hobbyist_license", None) + if not callable(function): + raise RuntimeError( + "installed Symbolica does not provide request_hobbyist_license()" + ) + function(name, email) + + +def _validate_identity( + values: Sequence[str], + *, + requires_organization: bool, +) -> None: + expected = 3 if requires_organization else 2 + if len(values) != expected or any( + not isinstance(value, str) or not value.strip() for value in values + ): + fields = ( + "name, email, and organization" + if requires_organization + else "name and email" + ) + raise ValueError(f"Symbolica license requests require non-empty {fields}") + email = values[1] + if "@" not in email or email.startswith("@") or email.endswith("@"): + raise ValueError("Symbolica license requests require a valid email address") + + +__all__ = [ + "SymbolicaLicenseState", + "detect_symbolica_license", + "prepare_symbolica_environment", + "request_hobbyist_license", + "request_trial_license", + "resolve_symbolica_resource_config", + "symbolica_resource_clamps", +] diff --git a/pyamplicol/src/pyamplicol/models/__init__.py b/pyamplicol/src/pyamplicol/models/__init__.py new file mode 100644 index 0000000000..cdd2f889ec --- /dev/null +++ b/pyamplicol/src/pyamplicol/models/__init__.py @@ -0,0 +1,36 @@ +# SPDX-License-Identifier: 0BSD +from __future__ import annotations + +import importlib +from typing import Any + +_LAZY_EXPORTS = { + "BuiltinSMModel": (".builtin", "BuiltinSMModel"), + "CompiledUFOModel": (".external", "CompiledUFOModel"), + "ModelCompileOptions": (".loading", "ModelCompileOptions"), + "compile_builtin_model_ir": (".compiler", "compile_builtin_model_ir"), + "compile_model_source": (".loading", "compile_model_source"), + "compile_ufo_model_ir": (".compiler", "compile_ufo_model_ir"), + "load_compiled_model": (".loading", "load_compiled_model"), +} + + +def __getattr__(name: str) -> Any: + target = _LAZY_EXPORTS.get(name) + if target is None: + raise AttributeError(name) + module_name, attribute = target + value = getattr(importlib.import_module(module_name, __name__), attribute) + globals()[name] = value + return value + + +__all__ = [ + "BuiltinSMModel", + "CompiledUFOModel", + "ModelCompileOptions", + "compile_builtin_model_ir", + "compile_model_source", + "compile_ufo_model_ir", + "load_compiled_model", +] diff --git a/pyamplicol/src/pyamplicol/models/_physics_ir.py b/pyamplicol/src/pyamplicol/models/_physics_ir.py new file mode 100644 index 0000000000..4a888356ab --- /dev/null +++ b/pyamplicol/src/pyamplicol/models/_physics_ir.py @@ -0,0 +1,1108 @@ +# SPDX-License-Identifier: 0BSD +"""Canonical, model-owned physics contracts used by generation and runtimes.""" + +from __future__ import annotations + +import hashlib +import json +import math +from collections.abc import Mapping, Sequence +from dataclasses import dataclass +from numbers import Real +from typing import Literal, cast + +ParticleOrientation = Literal["particle", "antiparticle", "self-conjugate"] +ParticleStatistics = Literal["boson", "fermion", "ghost", "auxiliary"] +WavefunctionFamily = Literal[ + "scalar", + "fermion", + "vector", + "spin2", + "ghost", + "auxiliary", +] +MomentumTransform = Literal["identity", "negate-four-momentum"] +PropagatorKind = Literal[ + "identity", + "scalar", + "weyl-fermion", + "dirac-fermion", + "vector", + "spin2", + "custom", + "unsupported", +] +PropagatorGauge = Literal[ + "feynman", + "unitary", + "de-donder", + "fierz-pauli", + "model-supplied", +] +PropagatorMassClass = Literal["massless", "massive", "not-applicable"] +GoldstonePolicy = Literal[ + "not-applicable", + "absorbed", + "explicit", + "model-supplied", +] +TensorFlattening = Literal["row-major-last-axis-fastest"] +TENSOR_ORDERING_CONTRACT_VERSION = 1 + + +def _strict_ir_fields( + payload: Mapping[str, object], + *, + required: set[str], + context: str, +) -> Mapping[str, object]: + if not isinstance(payload, Mapping): + raise TypeError(f"{context} must be a mapping") + fields = set(payload) + missing = required - fields + if missing: + names = ", ".join(sorted(missing)) + raise ValueError(f"{context} is missing required fields: {names}") + unknown = fields - required + if unknown: + names = ", ".join(sorted(str(name) for name in unknown)) + raise ValueError(f"{context} has unknown fields: {names}") + return payload + + +def _strict_ir_sequence(value: object, context: str) -> tuple[object, ...]: + if isinstance(value, (str, bytes)) or not isinstance(value, Sequence): + raise TypeError(f"{context} must be an array") + return tuple(value) + + +@dataclass(frozen=True, slots=True) +class TensorAxisIR: + """One semantic tensor axis, independent of a source UFO index label.""" + + name: str + space: str + extent: int + + def __post_init__(self) -> None: + if not isinstance(self.name, str) or not self.name: + raise ValueError("tensor axis name must be a non-empty string") + if not isinstance(self.space, str) or not self.space: + raise ValueError("tensor axis space must be a non-empty string") + if isinstance(self.extent, bool) or not isinstance(self.extent, int): + raise TypeError("tensor axis extent must be an integer") + if self.extent <= 0: + raise ValueError("tensor axis extent must be positive") + + def to_json_dict(self) -> dict[str, object]: + return {"name": self.name, "space": self.space, "extent": self.extent} + + @classmethod + def from_json_dict(cls, payload: Mapping[str, object]) -> TensorAxisIR: + fields = _strict_ir_fields( + payload, + required={"name", "space", "extent"}, + context="tensor axis IR", + ) + name = fields["name"] + space = fields["space"] + extent = fields["extent"] + if not isinstance(name, str) or not isinstance(space, str): + raise TypeError("tensor axis name and space must be strings") + if isinstance(extent, bool) or not isinstance(extent, int): + raise TypeError("tensor axis extent must be an integer") + return cls(name=name, space=space, extent=extent) + + +@dataclass(frozen=True, slots=True) +class TensorOrderingIR: + """Content-addressed mapping from stored components to a canonical tensor.""" + + ordering_id: str + basis: str + axes: tuple[TensorAxisIR, ...] + flattening: TensorFlattening + component_basis: tuple[int, ...] + component_expansion: tuple[tuple[int, int] | None, ...] + + def __post_init__(self) -> None: + if not isinstance(self.basis, str) or not self.basis: + raise ValueError("tensor ordering basis must be a non-empty string") + if self.flattening != "row-major-last-axis-fastest": + raise ValueError(f"unsupported tensor flattening {self.flattening!r}") + if not all(isinstance(axis, TensorAxisIR) for axis in self.axes): + raise TypeError("tensor ordering axes must contain TensorAxisIR records") + axis_names = tuple(axis.name for axis in self.axes) + if len(set(axis_names)) != len(axis_names): + raise ValueError("tensor ordering axis names must be unique") + if not self.component_basis: + raise ValueError("tensor ordering component basis must not be empty") + if any( + isinstance(index, bool) or not isinstance(index, int) + for index in self.component_basis + ): + raise TypeError("tensor ordering component basis must contain integers") + canonical_size = self.canonical_size + if len(self.component_expansion) != canonical_size: + raise ValueError( + "tensor ordering expansion size does not match its canonical axes" + ) + if len(set(self.component_basis)) != len(self.component_basis): + raise ValueError("tensor ordering component basis must be unique") + if any(index < 0 or index >= canonical_size for index in self.component_basis): + raise ValueError("tensor ordering component basis index is out of range") + referenced_slots: set[int] = set() + for entry in self.component_expansion: + if entry is None: + continue + if ( + not isinstance(entry, tuple) + or len(entry) != 2 + or any( + isinstance(value, bool) or not isinstance(value, int) + for value in entry + ) + ): + raise TypeError( + "tensor ordering expansion entries must be (slot, sign) pairs" + ) + slot, sign = entry + if slot < 0 or slot >= self.stored_size: + raise ValueError("tensor ordering expansion refers to an absent slot") + if sign not in {-1, 1}: + raise ValueError("tensor ordering expansion sign must be +/-1") + referenced_slots.add(slot) + if referenced_slots != set(range(self.stored_size)): + raise ValueError( + "tensor ordering expansion does not cover every stored slot" + ) + for slot, canonical_component in enumerate(self.component_basis): + if self.component_expansion[canonical_component] != (slot, 1): + raise ValueError( + "tensor ordering component basis disagrees with its expansion" + ) + expected_id = self.content_id( + basis=self.basis, + axes=self.axes, + flattening=self.flattening, + component_basis=self.component_basis, + component_expansion=self.component_expansion, + ) + if self.ordering_id != expected_id: + raise ValueError( + "tensor ordering ID does not match its semantic component contract" + ) + + @property + def canonical_size(self) -> int: + return math.prod(axis.extent for axis in self.axes) if self.axes else 1 + + @property + def stored_size(self) -> int: + return len(self.component_basis) + + @classmethod + def identity( + cls, + *, + basis: str, + axes: Sequence[TensorAxisIR], + ) -> TensorOrderingIR: + axis_tuple = tuple(axes) + size = math.prod(axis.extent for axis in axis_tuple) if axis_tuple else 1 + component_basis = tuple(range(size)) + component_expansion = tuple((index, 1) for index in range(size)) + return cls.create( + basis=basis, + axes=axis_tuple, + component_basis=component_basis, + component_expansion=component_expansion, + ) + + @classmethod + def create( + cls, + *, + basis: str, + axes: Sequence[TensorAxisIR], + component_basis: Sequence[int], + component_expansion: Sequence[tuple[int, int] | None], + flattening: TensorFlattening = "row-major-last-axis-fastest", + ) -> TensorOrderingIR: + axis_tuple = tuple(axes) + basis_tuple = tuple(component_basis) + expansion_tuple = tuple(component_expansion) + return cls( + ordering_id=cls.content_id( + basis=basis, + axes=axis_tuple, + flattening=flattening, + component_basis=basis_tuple, + component_expansion=expansion_tuple, + ), + basis=basis, + axes=axis_tuple, + flattening=flattening, + component_basis=basis_tuple, + component_expansion=expansion_tuple, + ) + + @staticmethod + def content_id( + *, + basis: str, + axes: Sequence[TensorAxisIR], + flattening: TensorFlattening, + component_basis: Sequence[int], + component_expansion: Sequence[tuple[int, int] | None], + ) -> str: + payload = { + "basis": basis, + "axes": [axis.to_json_dict() for axis in axes], + "flattening": flattening, + "component_basis": list(component_basis), + "component_expansion": [ + None if entry is None else list(entry) for entry in component_expansion + ], + } + canonical = json.dumps(payload, sort_keys=True, separators=(",", ":")) + return "tensor-ordering-sha256:" + hashlib.sha256( + canonical.encode("utf-8") + ).hexdigest() + + def to_json_dict(self) -> dict[str, object]: + return { + "ordering_id": self.ordering_id, + "basis": self.basis, + "axes": [axis.to_json_dict() for axis in self.axes], + "flattening": self.flattening, + "component_basis": list(self.component_basis), + "component_expansion": [ + None if entry is None else list(entry) + for entry in self.component_expansion + ], + } + + @classmethod + def from_json_dict(cls, payload: Mapping[str, object]) -> TensorOrderingIR: + fields = _strict_ir_fields( + payload, + required={ + "ordering_id", + "basis", + "axes", + "flattening", + "component_basis", + "component_expansion", + }, + context="tensor ordering IR", + ) + ordering_id = fields["ordering_id"] + basis = fields["basis"] + flattening = fields["flattening"] + if not all( + isinstance(value, str) for value in (ordering_id, basis, flattening) + ): + raise TypeError("tensor ordering ID, basis, and flattening must be strings") + axes = tuple( + TensorAxisIR.from_json_dict(axis) + for axis in _strict_ir_sequence(fields["axes"], "tensor ordering axes") + if isinstance(axis, Mapping) + ) + axis_values = _strict_ir_sequence(fields["axes"], "tensor ordering axes") + if len(axes) != len(axis_values): + raise TypeError("tensor ordering axes must contain mappings") + component_basis_values = _strict_ir_sequence( + fields["component_basis"], "tensor ordering component basis" + ) + if any( + isinstance(value, bool) or not isinstance(value, int) + for value in component_basis_values + ): + raise TypeError("tensor ordering component basis must contain integers") + expansion_values = _strict_ir_sequence( + fields["component_expansion"], "tensor ordering component expansion" + ) + expansion: list[tuple[int, int] | None] = [] + for entry in expansion_values: + if entry is None: + expansion.append(None) + continue + pair = _strict_ir_sequence(entry, "tensor ordering expansion entry") + if len(pair) != 2 or any( + isinstance(value, bool) or not isinstance(value, int) for value in pair + ): + raise TypeError( + "tensor ordering expansion entry must be an integer pair" + ) + expansion.append((pair[0], pair[1])) + return cls( + ordering_id=ordering_id, + basis=basis, + axes=axes, + flattening=cast(TensorFlattening, flattening), + component_basis=cast(tuple[int, ...], component_basis_values), + component_expansion=tuple(expansion), + ) + + +@dataclass(frozen=True, slots=True) +class TensorIndexBindingIR: + """Provenance for one normalized tensor index used by a UFO vertex term.""" + + origin: str + space: str + source_component: int | None + source_leg: int | None + source_dummy: int | None + normalized_name: str + + def __post_init__(self) -> None: + for field_name in ("origin", "space", "normalized_name"): + value = getattr(self, field_name) + if not isinstance(value, str) or not value: + raise ValueError(f"tensor index binding {field_name} must not be empty") + if (self.source_leg is None) == (self.source_dummy is None): + raise ValueError( + "tensor index binding must identify exactly one source leg or dummy" + ) + for field_name in ("source_component", "source_leg", "source_dummy"): + value = getattr(self, field_name) + if value is not None and ( + isinstance(value, bool) or not isinstance(value, int) + ): + raise TypeError(f"tensor index binding {field_name} must be an integer") + if self.source_component is not None and self.source_component < 0: + raise ValueError("tensor index source component must be non-negative") + if self.source_leg is not None and self.source_leg <= 0: + raise ValueError("tensor index source leg must be positive") + if self.source_dummy == 0: + raise ValueError("tensor index source dummy must be nonzero") + + def to_json_dict(self) -> dict[str, object]: + return { + "origin": self.origin, + "space": self.space, + "source_component": self.source_component, + "source_leg": self.source_leg, + "source_dummy": self.source_dummy, + "normalized_name": self.normalized_name, + } + + @classmethod + def from_json_dict(cls, payload: Mapping[str, object]) -> TensorIndexBindingIR: + fields = _strict_ir_fields( + payload, + required={ + "origin", + "space", + "source_component", + "source_leg", + "source_dummy", + "normalized_name", + }, + context="tensor index binding IR", + ) + for field_name in ("origin", "space", "normalized_name"): + if not isinstance(fields[field_name], str): + raise TypeError(f"tensor index binding {field_name} must be a string") + for field_name in ("source_component", "source_leg", "source_dummy"): + value = fields[field_name] + if value is not None and ( + isinstance(value, bool) or not isinstance(value, int) + ): + raise TypeError(f"tensor index binding {field_name} must be an integer") + return cls( + origin=cast(str, fields["origin"]), + space=cast(str, fields["space"]), + source_component=cast(int | None, fields["source_component"]), + source_leg=cast(int | None, fields["source_leg"]), + source_dummy=cast(int | None, fields["source_dummy"]), + normalized_name=cast(str, fields["normalized_name"]), + ) + + +@dataclass(frozen=True, slots=True) +class CompiledCurrentOrderingRecord: + """Map one stored current state to the full ordering used by kernels.""" + + particle: str + chirality: int + ordering_id: str + kernel_ordering_id: str + input_embedding: tuple[int | None, ...] + result_projection: tuple[int, ...] + + def __post_init__(self) -> None: + for field_name in ("particle", "ordering_id", "kernel_ordering_id"): + value = getattr(self, field_name) + if not isinstance(value, str) or not value: + raise ValueError(f"current ordering {field_name} must not be empty") + if isinstance(self.chirality, bool) or not isinstance(self.chirality, int): + raise TypeError("current ordering chirality must be an integer") + if any( + value is not None + and (isinstance(value, bool) or not isinstance(value, int)) + for value in self.input_embedding + ): + raise TypeError("current input embedding must contain integers or null") + if any( + isinstance(value, bool) or not isinstance(value, int) + for value in self.result_projection + ): + raise TypeError("current result projection must contain integers") + + @property + def selector(self) -> tuple[str, int]: + return self.particle, self.chirality + + def to_json_dict(self) -> dict[str, object]: + return { + "particle": self.particle, + "chirality": self.chirality, + "ordering_id": self.ordering_id, + "kernel_ordering_id": self.kernel_ordering_id, + "input_embedding": list(self.input_embedding), + "result_projection": list(self.result_projection), + } + + @classmethod + def from_json_dict( + cls, payload: Mapping[str, object] + ) -> CompiledCurrentOrderingRecord: + fields = _strict_ir_fields( + payload, + required={ + "particle", + "chirality", + "ordering_id", + "kernel_ordering_id", + "input_embedding", + "result_projection", + }, + context="compiled current ordering", + ) + for field_name in ("particle", "ordering_id", "kernel_ordering_id"): + if not isinstance(fields[field_name], str): + raise TypeError(f"current ordering {field_name} must be a string") + chirality = fields["chirality"] + if isinstance(chirality, bool) or not isinstance(chirality, int): + raise TypeError("current ordering chirality must be an integer") + embedding = _strict_ir_sequence( + fields["input_embedding"], "current input embedding" + ) + projection = _strict_ir_sequence( + fields["result_projection"], "current result projection" + ) + return cls( + particle=cast(str, fields["particle"]), + chirality=chirality, + ordering_id=cast(str, fields["ordering_id"]), + kernel_ordering_id=cast(str, fields["kernel_ordering_id"]), + input_embedding=cast(tuple[int | None, ...], embedding), + result_projection=cast(tuple[int, ...], projection), + ) + + +def _integer(value: object, label: str) -> int: + if isinstance(value, bool) or not isinstance(value, int): + raise TypeError(f"{label} must be an integer") + return value + + +def _boolean(value: object, label: str) -> bool: + if not isinstance(value, bool): + raise TypeError(f"{label} must be a boolean") + return value + + +def _optional_string(value: object) -> str | None: + if value is None: + return None + if not isinstance(value, str): + raise TypeError("optional propagator metadata must be a string or null") + return value + + +def _particle_orientation(value: object) -> ParticleOrientation: + if value not in {"particle", "antiparticle", "self-conjugate"}: + raise ValueError(f"invalid particle orientation {value!r}") + return cast(ParticleOrientation, value) + + +def _propagator_kind(value: object) -> PropagatorKind: + if value not in { + "identity", + "scalar", + "weyl-fermion", + "dirac-fermion", + "vector", + "spin2", + "custom", + "unsupported", + }: + raise ValueError(f"invalid propagator kind {value!r}") + return cast(PropagatorKind, value) + + +def _optional_propagator_gauge(value: object) -> PropagatorGauge | None: + if value is None: + return None + if value not in { + "feynman", + "unitary", + "de-donder", + "fierz-pauli", + "model-supplied", + }: + raise ValueError(f"invalid propagator gauge {value!r}") + return cast(PropagatorGauge, value) + + +def _goldstone_policy(value: object) -> GoldstonePolicy: + if value not in { + "not-applicable", + "absorbed", + "explicit", + "model-supplied", + }: + raise ValueError(f"invalid propagator Goldstone policy {value!r}") + return cast(GoldstonePolicy, value) + + +def _propagator_mass_class(value: object) -> PropagatorMassClass: + if value not in {"massless", "massive", "not-applicable"}: + raise ValueError(f"invalid propagator mass class {value!r}") + return cast(PropagatorMassClass, value) + + +@dataclass(frozen=True, slots=True) +class ParticleIdentityIR: + """Identity of one oriented model state, independent of SM taxonomy.""" + + canonical_id: str + species_id: str + anti_canonical_id: str + display_name: str + anti_display_name: str + pdg_label: int + anti_pdg_label: int + orientation: ParticleOrientation + self_conjugate: bool + + def __post_init__(self) -> None: + for field_name in ( + "canonical_id", + "species_id", + "anti_canonical_id", + "display_name", + "anti_display_name", + ): + if not getattr(self, field_name): + raise ValueError(f"particle identity {field_name} must not be empty") + for field_name in ("pdg_label", "anti_pdg_label"): + value = getattr(self, field_name) + if isinstance(value, bool) or not isinstance(value, int): + raise TypeError(f"particle identity {field_name} must be an integer") + if self.orientation not in { + "particle", + "antiparticle", + "self-conjugate", + }: + raise ValueError(f"invalid particle orientation {self.orientation!r}") + if self.self_conjugate != (self.canonical_id == self.anti_canonical_id): + raise ValueError( + "particle self-conjugacy must agree with its canonical anti relation" + ) + if self.self_conjugate != (self.pdg_label == self.anti_pdg_label): + raise ValueError( + "particle self-conjugacy must agree with its PDG-label anti relation" + ) + if (self.orientation == "self-conjugate") != self.self_conjugate: + raise ValueError( + "self-conjugate source orientation must agree with particle identity" + ) + + def to_json_dict(self) -> dict[str, object]: + return { + "canonical_id": self.canonical_id, + "species_id": self.species_id, + "anti_canonical_id": self.anti_canonical_id, + "display_name": self.display_name, + "anti_display_name": self.anti_display_name, + "pdg_label": self.pdg_label, + "anti_pdg_label": self.anti_pdg_label, + "orientation": self.orientation, + "self_conjugate": self.self_conjugate, + } + + @classmethod + def from_json_dict(cls, payload: Mapping[str, object]) -> ParticleIdentityIR: + return cls( + canonical_id=str(payload["canonical_id"]), + species_id=str(payload["species_id"]), + anti_canonical_id=str(payload["anti_canonical_id"]), + display_name=str(payload["display_name"]), + anti_display_name=str(payload["anti_display_name"]), + pdg_label=_integer(payload["pdg_label"], "particle PDG label"), + anti_pdg_label=_integer( + payload["anti_pdg_label"], + "antiparticle PDG label", + ), + orientation=_particle_orientation(payload["orientation"]), + self_conjugate=_boolean( + payload["self_conjugate"], + "particle self-conjugacy", + ), + ) + + +@dataclass(frozen=True, slots=True) +class SourceStateIR: + helicity: int + chirality: int + spin_state: int | tuple[int, ...] + + def __post_init__(self) -> None: + for field_name in ("helicity", "chirality"): + value = getattr(self, field_name) + if isinstance(value, bool) or not isinstance(value, int): + raise TypeError(f"source state {field_name} must be an integer") + if isinstance(self.spin_state, bool) or not isinstance( + self.spin_state, (int, tuple) + ): + raise TypeError("source spin state must be an integer or integer tuple") + if isinstance(self.spin_state, tuple) and not all( + isinstance(value, int) and not isinstance(value, bool) + for value in self.spin_state + ): + raise TypeError("source spin-state tuples must contain only integers") + + def to_json_dict(self) -> dict[str, object]: + return { + "helicity": self.helicity, + "chirality": self.chirality, + "spin_state": ( + list(self.spin_state) + if isinstance(self.spin_state, tuple) + else self.spin_state + ), + } + + +@dataclass(frozen=True, slots=True) +class CrossingIR: + """Transform an outgoing source basis into an incoming physical leg.""" + + momentum_transform: MomentumTransform = "negate-four-momentum" + helicity_factor: int = 1 + chirality_factor: int = 1 + spin_state_factor: int = 1 + phase: tuple[float, float] = (1.0, 0.0) + + def __post_init__(self) -> None: + if self.momentum_transform not in {"identity", "negate-four-momentum"}: + raise ValueError( + f"unsupported crossing momentum transform {self.momentum_transform!r}" + ) + for field_name in ( + "helicity_factor", + "chirality_factor", + "spin_state_factor", + ): + value = getattr(self, field_name) + if isinstance(value, bool) or not isinstance(value, int): + raise TypeError(f"crossing {field_name} must be an integer") + if value not in {-1, 1}: + raise ValueError(f"crossing {field_name} must be -1 or 1") + if not isinstance(self.phase, (tuple, list)) or len(self.phase) != 2: + raise ValueError("crossing phase must be a finite complex pair") + if any( + isinstance(value, bool) or not isinstance(value, Real) + for value in self.phase + ): + raise TypeError("crossing phase components must be real numbers") + phase = (float(self.phase[0]), float(self.phase[1])) + if not all(math.isfinite(value) for value in phase): + raise ValueError("crossing phase must be a finite complex pair") + if phase == (0.0, 0.0): + raise ValueError("crossing phase must be nonzero") + object.__setattr__(self, "phase", phase) + + @classmethod + def identity(cls) -> CrossingIR: + return cls(momentum_transform="identity") + + def apply(self, state: SourceStateIR) -> SourceStateIR: + spin_state = state.spin_state + if self.spin_state_factor != 1: + if not isinstance(spin_state, int): + raise TypeError( + "crossing cannot multiply a structured source spin state" + ) + spin_state *= self.spin_state_factor + return SourceStateIR( + helicity=state.helicity * self.helicity_factor, + chirality=state.chirality * self.chirality_factor, + spin_state=spin_state, + ) + + def to_json_dict(self) -> dict[str, object]: + return { + "momentum_transform": self.momentum_transform, + "helicity_factor": self.helicity_factor, + "chirality_factor": self.chirality_factor, + "spin_state_factor": self.spin_state_factor, + "phase": list(self.phase), + } + + +@dataclass(frozen=True, slots=True) +class SourceIR: + identity: ParticleIdentityIR + statistics: ParticleStatistics + wavefunction_family: WavefunctionFamily + component_dimension: int + states: tuple[SourceStateIR, ...] + crossing: CrossingIR + basis: str + mass_parameter: str | None = None + width_parameter: str | None = None + + def __post_init__(self) -> None: + if self.statistics not in {"boson", "fermion", "ghost", "auxiliary"}: + raise ValueError(f"invalid source statistics {self.statistics!r}") + if self.wavefunction_family not in { + "scalar", + "fermion", + "vector", + "spin2", + "ghost", + "auxiliary", + }: + raise ValueError( + f"invalid source wavefunction family {self.wavefunction_family!r}" + ) + expected_statistics: ParticleStatistics + if self.wavefunction_family == "fermion": + expected_statistics = "fermion" + elif self.wavefunction_family == "ghost": + expected_statistics = "ghost" + elif self.wavefunction_family == "auxiliary": + expected_statistics = "auxiliary" + else: + expected_statistics = "boson" + if self.statistics != expected_statistics: + raise ValueError( + f"source wavefunction family {self.wavefunction_family!r} requires " + f"statistics {expected_statistics!r}, got {self.statistics!r}" + ) + if isinstance(self.component_dimension, bool) or not isinstance( + self.component_dimension, int + ): + raise TypeError("source component dimension must be an integer") + if self.component_dimension < 1: + raise ValueError("source component dimension must be positive") + if not self.states: + raise ValueError("source metadata must declare at least one spin state") + if not self.basis: + raise ValueError("source basis must not be empty") + + def to_json_dict(self) -> dict[str, object]: + return { + "identity": self.identity.to_json_dict(), + "statistics": self.statistics, + "wavefunction_family": self.wavefunction_family, + "component_dimension": self.component_dimension, + "states": [state.to_json_dict() for state in self.states], + "crossing": self.crossing.to_json_dict(), + "basis": self.basis, + "mass_parameter": self.mass_parameter, + "width_parameter": self.width_parameter, + } + + +@dataclass(frozen=True, slots=True) +class PropagatorIR: + identity: ParticleIdentityIR + chirality: int + kind: PropagatorKind + backend: str + basis: str + applies_propagator: bool + kernel: str + full_tensor_network_ready: bool + mass_class: PropagatorMassClass + gauge: PropagatorGauge | None = None + numerator: str | None = None + denominator: str | None = None + mass_parameter: str | None = None + width_parameter: str | None = None + custom_source: str | None = None + auxiliary_policy: str | None = None + goldstone_policy: GoldstonePolicy = "not-applicable" + description: str = "" + + def __post_init__(self) -> None: + if not self.backend or not self.basis or not self.kernel: + raise ValueError("propagator backend, basis, and kernel must not be empty") + if self.kind not in { + "identity", + "scalar", + "weyl-fermion", + "dirac-fermion", + "vector", + "spin2", + "custom", + "unsupported", + }: + raise ValueError(f"invalid propagator kind {self.kind!r}") + if self.gauge not in { + None, + "feynman", + "unitary", + "de-donder", + "fierz-pauli", + "model-supplied", + }: + raise ValueError(f"invalid propagator gauge {self.gauge!r}") + if self.goldstone_policy not in { + "not-applicable", + "absorbed", + "explicit", + "model-supplied", + }: + raise ValueError( + f"invalid propagator Goldstone policy {self.goldstone_policy!r}" + ) + if self.mass_class not in {"massless", "massive", "not-applicable"}: + raise ValueError(f"invalid propagator mass class {self.mass_class!r}") + if not self.applies_propagator and self.auxiliary_policy is None: + raise ValueError( + "a no-propagator current must declare its auxiliary policy" + ) + if not self.applies_propagator and self.kind != "identity": + raise ValueError("a no-propagator current must use the identity kind") + if self.applies_propagator and self.kind == "identity": + raise ValueError("an identity current cannot apply a propagator") + if (self.kind == "identity") != (self.mass_class == "not-applicable"): + raise ValueError( + "only identity currents may use a not-applicable mass class" + ) + if self.kind == "vector" and self.gauge is None: + raise ValueError("a vector propagator must declare its gauge") + if self.kind == "custom" and ( + self.gauge != "model-supplied" or not self.custom_source + ): + raise ValueError( + "a custom propagator must declare its model-supplied source" + ) + if self.goldstone_policy == "absorbed" and not ( + self.kind == "vector" and self.gauge == "unitary" + ): + raise ValueError( + "an absorbed Goldstone mode requires a unitary-gauge vector" + ) + + def to_json_dict(self) -> dict[str, object]: + return { + "identity": self.identity.to_json_dict(), + "particle_id": self.identity.pdg_label, + "chirality": self.chirality, + "kind": self.kind, + "backend": self.backend, + "basis": self.basis, + "applies_propagator": self.applies_propagator, + "kernel": self.kernel, + "full_tensor_network_ready": self.full_tensor_network_ready, + "mass_class": self.mass_class, + "gauge": self.gauge, + "numerator": self.numerator, + "denominator": self.denominator, + "mass_parameter": self.mass_parameter, + "width_parameter": self.width_parameter, + "custom_source": self.custom_source, + "auxiliary_policy": self.auxiliary_policy, + "goldstone_policy": self.goldstone_policy, + "description": self.description, + } + + @classmethod + def from_json_dict(cls, payload: Mapping[str, object]) -> PropagatorIR: + identity = payload.get("identity") + if not isinstance(identity, Mapping): + raise TypeError("propagator identity must be a mapping") + return cls( + identity=ParticleIdentityIR.from_json_dict(identity), + chirality=_integer(payload["chirality"], "propagator chirality"), + kind=_propagator_kind(payload["kind"]), + backend=str(payload["backend"]), + basis=str(payload["basis"]), + applies_propagator=_boolean( + payload["applies_propagator"], + "propagator application flag", + ), + kernel=str(payload["kernel"]), + full_tensor_network_ready=_boolean( + payload["full_tensor_network_ready"], + "propagator tensor-network readiness", + ), + mass_class=_propagator_mass_class(payload["mass_class"]), + gauge=_optional_propagator_gauge(payload.get("gauge")), + numerator=_optional_string(payload.get("numerator")), + denominator=_optional_string(payload.get("denominator")), + mass_parameter=_optional_string(payload.get("mass_parameter")), + width_parameter=_optional_string(payload.get("width_parameter")), + custom_source=_optional_string(payload.get("custom_source")), + auxiliary_policy=_optional_string(payload.get("auxiliary_policy")), + goldstone_policy=_goldstone_policy( + payload.get("goldstone_policy", "not-applicable") + ), + description=str(payload.get("description", "")), + ) + + +@dataclass(frozen=True, slots=True) +class ContractionIR: + name: str + left_basis: str + right_basis: str + coefficients: tuple[tuple[float, float], ...] + chirality_relation: Literal["any", "equal", "opposite"] = "any" + metric_signature: str | None = None + + def __post_init__(self) -> None: + for field_name in ("name", "left_basis", "right_basis"): + value = getattr(self, field_name) + if not isinstance(value, str): + raise TypeError(f"contraction {field_name} must be a string") + if not self.name or not self.left_basis or not self.right_basis: + raise ValueError("contraction name and bases must not be empty") + if isinstance(self.coefficients, (str, bytes)) or not isinstance( + self.coefficients, Sequence + ): + raise TypeError("contraction coefficients must be a sequence") + if not self.coefficients: + raise ValueError("contraction must declare component coefficients") + coefficients: list[tuple[float, float]] = [] + for index, value in enumerate(self.coefficients): + if isinstance(value, (str, bytes)) or not isinstance(value, Sequence): + raise TypeError( + f"contraction coefficient {index} must be a complex pair" + ) + if len(value) != 2: + raise ValueError( + "contraction coefficients must be finite complex pairs" + ) + if any( + isinstance(component, bool) or not isinstance(component, Real) + for component in value + ): + raise TypeError( + "contraction coefficient components must be real numbers" + ) + coefficient = (float(value[0]), float(value[1])) + if not all(math.isfinite(component) for component in coefficient): + raise ValueError( + "contraction coefficients must be finite complex pairs" + ) + coefficients.append(coefficient) + normalized_coefficients = tuple(coefficients) + if not any(value != (0.0, 0.0) for value in normalized_coefficients): + raise ValueError("contraction must have a nonzero component coefficient") + object.__setattr__(self, "coefficients", normalized_coefficients) + if not isinstance(self.chirality_relation, str): + raise TypeError("contraction chirality relation must be a string") + if self.chirality_relation not in {"any", "equal", "opposite"}: + raise ValueError( + f"invalid contraction chirality relation {self.chirality_relation!r}" + ) + if self.metric_signature is not None: + if not isinstance(self.metric_signature, str): + raise TypeError("contraction metric signature must be a string or null") + if not self.metric_signature: + raise ValueError("contraction metric signature must not be empty") + + def to_json_dict(self) -> dict[str, object]: + return { + "name": self.name, + "left_basis": self.left_basis, + "right_basis": self.right_basis, + "coefficients": [list(value) for value in self.coefficients], + "chirality_relation": self.chirality_relation, + "metric_signature": self.metric_signature, + } + + @classmethod + def from_json_dict(cls, payload: Mapping[str, object]) -> ContractionIR: + if not isinstance(payload, Mapping): + raise TypeError("contraction IR must be a mapping") + required_fields = { + "name", + "left_basis", + "right_basis", + "coefficients", + "chirality_relation", + "metric_signature", + } + fields = set(payload) + missing = required_fields - fields + if missing: + names = ", ".join(sorted(missing)) + raise ValueError(f"contraction IR is missing required fields: {names}") + unknown = fields - required_fields + if unknown: + names = ", ".join(sorted(str(name) for name in unknown)) + raise ValueError(f"contraction IR has unknown fields: {names}") + + coefficients = payload["coefficients"] + if isinstance(coefficients, (str, bytes)) or not isinstance( + coefficients, Sequence + ): + raise TypeError("contraction coefficients must be an array") + parsed_coefficients: list[tuple[float, float]] = [] + for index, value in enumerate(coefficients): + if isinstance(value, (str, bytes)) or not isinstance(value, Sequence): + raise TypeError( + f"contraction coefficient {index} must be a complex pair" + ) + if len(value) != 2: + raise ValueError( + f"contraction coefficient {index} must have two components" + ) + if any( + isinstance(component, bool) or not isinstance(component, Real) + for component in value + ): + raise TypeError( + "contraction coefficient components must be real numbers" + ) + parsed_coefficients.append((float(value[0]), float(value[1]))) + + name = payload["name"] + left_basis = payload["left_basis"] + right_basis = payload["right_basis"] + if not isinstance(name, str): + raise TypeError("contraction name must be a string") + if not isinstance(left_basis, str) or not isinstance(right_basis, str): + raise TypeError("contraction bases must be strings") + metric_signature = payload["metric_signature"] + if metric_signature is not None and not isinstance(metric_signature, str): + raise TypeError("contraction metric signature must be a string or null") + chirality_relation = payload["chirality_relation"] + if not isinstance(chirality_relation, str): + raise TypeError("contraction chirality relation must be a string") + if chirality_relation not in {"any", "equal", "opposite"}: + raise ValueError( + f"invalid contraction chirality relation {chirality_relation!r}" + ) + return cls( + name=name, + left_basis=left_basis, + right_basis=right_basis, + coefficients=tuple(parsed_coefficients), + chirality_relation=cast( + Literal["any", "equal", "opposite"], chirality_relation + ), + metric_signature=metric_signature, + ) diff --git a/pyamplicol/src/pyamplicol/models/adjoint_color_certificate.py b/pyamplicol/src/pyamplicol/models/adjoint_color_certificate.py new file mode 100644 index 0000000000..612368b5f5 --- /dev/null +++ b/pyamplicol/src/pyamplicol/models/adjoint_color_certificate.py @@ -0,0 +1,201 @@ +# SPDX-License-Identifier: 0BSD +"""Colour-only certificates for a connected DDM tree with singlet spectators. + +This is not a Yang--Mills helicity theorem. Arbitrary Lorentz interactions +are permitted when their *actual compiled colour tensors* are connected trees +of structure constants and adjoint identities. A colourless propagator +between two such trees would instead produce a colour forest. Until a +forest decomposition is selected, the proof therefore permits at most one +mixed coloured/singlet vertex in a complete tree amplitude. +""" + +from __future__ import annotations + +import ast +import re +from collections import Counter +from collections.abc import Mapping +from functools import lru_cache +from typing import TYPE_CHECKING + +from .tensors import normalize_color_expression + +if TYPE_CHECKING: + from ..processes.ir import CanonicalProcessIR + from .base import Model + from .contracts import CompiledModelIR + + +def adjoint_tree_color_basis_is_proven( + model: Model, + process: CanonicalProcessIR, + *, + max_coupling_orders: Mapping[str, int] | None = None, +) -> bool: + """Dispatch the model-owned connected-adjoint-tree certificate.""" + + return model.adjoint_tree_color_basis_is_proven( + process, max_coupling_orders=max_coupling_orders + ) + + +def compiled_adjoint_tree_color_basis_is_proven( + ir: CompiledModelIR, + process: CanonicalProcessIR, + *, + max_coupling_orders: Mapping[str, int] | None = None, +) -> bool: + """Conservatively prove connected f-tree colour for this process domain. + + Species closure deliberately overestimates reachable vertices; no colour + tensor can be discarded by a numerical probe or by a UFO/model name. + A common positive order with a one-insertion budget proves that two + mixed vertices cannot occur. For example HIG <= 1 certifies a single + effective Higgs insertion, while unrestricted Higgs exchange is rejected. + """ + + if ( + len(process.adjoint_labels) < 2 + or process.fundamental_labels + or process.antifundamental_labels + or len(process.adjoint_labels) + len(process.singlet_labels) + != len(process.legs) + ): + return False + particles = {particle.name: particle for particle in ir.particles} + by_pdg = {particle.pdg_code: particle for particle in ir.particles} + reachable: set[str] = set() + for leg in process.legs: + if leg.outgoing_pdg is None or leg.outgoing_pdg not in by_pdg: + return False + reachable.add(by_pdg[leg.outgoing_pdg].name) + limits = { + str(name).upper(): int(value) + for name, value in (max_coupling_orders or {}).items() + } + active = {} + changed = True + while changed: + changed = False + for term in ir.vertex_terms: + orders = {name.upper(): value for name, value in term.coupling_orders} + # Nonnegative coupling orders make per-vertex pruning sound. + if any(value < 0 for value in orders.values()): + return False + if any(orders.get(name, 0) > limit for name, limit in limits.items()): + continue + for output_index, output_name in enumerate(term.particles): + if not all( + name in reachable + for index, name in enumerate(term.particles) + if index != output_index + ): + continue + active[term.id] = term + result = particles[output_name].antiname + if result not in reachable: + reachable.add(result) + changed = True + mixed_orders = [] + found_coloured_tree = False + for term in active.values(): + representations = tuple(particles[name].color for name in term.particles) + coloured_count = sum(color != 1 for color in representations) + if not coloured_count: + continue + if any(color not in {1, 8} for color in representations): + return False + if not _is_connected_adjoint_tree_tensor( + term.color_source, term.color_expression, representations + ): + return False + found_coloured_tree = True + if coloured_count != len(representations): + mixed_orders.append( + {name.upper(): value for name, value in term.coupling_orders} + ) + if mixed_orders and not any( + 0 <= limit < 2 * min(orders.get(name, 0) for orders in mixed_orders) + for name, limit in limits.items() + ): + return False + return found_coloured_tree + + +@lru_cache(maxsize=512) +def _is_connected_adjoint_tree_tensor( + source: str, expression: str, representations: tuple[int, ...] +) -> bool: + """Prove each source monomial is a tree, then bind it to the compiled tensor.""" + + coloured_legs = { + index for index, color in enumerate(representations, start=1) if color != 1 + } + if len(coloured_legs) < 2 or any(color not in {1, 8} for color in representations): + return False + source_python = re.sub(r"UFO::(?:\{\}::)?", "", source) + try: + monomials = _tensor_monomials(ast.parse(source_python, mode="eval").body) + except (SyntaxError, TypeError, ValueError): + return False + if not monomials or not all( + _monomial_is_tree(factors, coloured_legs) for factors in monomials + ): + return False + from . import compiler_symbolica as _sym + + _sym._ensure_symbolica() + try: + normalized = normalize_color_expression(source, representations).expression + return (_sym.E(expression) - _sym.E(normalized)).expand() == _sym.E("0") + except (RuntimeError, TypeError, ValueError): + return False + + +def _tensor_monomials(node: ast.AST) -> list[tuple[tuple[int, ...], ...]]: + """Expand a small UFO colour polynomial, never evaluating Python code.""" + + if isinstance(node, ast.Constant) and isinstance(node.value, int | float | complex): + return [()] + if isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.UAdd | ast.USub): + return _tensor_monomials(node.operand) + if isinstance(node, ast.BinOp): + left = _tensor_monomials(node.left) + right = _tensor_monomials(node.right) + if isinstance(node.op, ast.Add | ast.Sub): + return left + right + if isinstance(node.op, ast.Mult): + return [(*a, *b) for a in left for b in right] + if isinstance(node.op, ast.Div) and right == [()]: + return left + if isinstance(node, ast.Call) and isinstance(node.func, ast.Name): + arity = {"f": 3, "Identity": 2}.get(node.func.id) + if arity is None or len(node.args) != arity or node.keywords: + raise ValueError("not a structure constant or adjoint identity") + labels = tuple(ast.literal_eval(argument) for argument in node.args) + if any(type(label) is not int or label == 0 for label in labels): + raise ValueError("colour tensor indices must be nonzero integers") + return [(labels,)] + raise ValueError("not a polynomial of structure constants and identities") + + +def _monomial_is_tree( + factors: tuple[tuple[int, ...], ...], coloured_legs: set[int] +) -> bool: + counts = Counter(label for factor in factors for label in factor) + if {label for label in counts if label > 0} != coloured_legs: + return False + if any(count != (1 if label > 0 else 2) for label, count in counts.items()): + return False + if sum(label < 0 for label in counts) != len(factors) - 1: + return False + reached = {0} + changed = True + while changed: + changed = False + indices = {label for index in reached for label in factors[index] if label < 0} + for index, factor in enumerate(factors): + if index not in reached and indices.intersection(factor): + reached.add(index) + changed = True + return len(reached) == len(factors) diff --git a/pyamplicol/src/pyamplicol/models/base.py b/pyamplicol/src/pyamplicol/models/base.py new file mode 100644 index 0000000000..9fdc32948b --- /dev/null +++ b/pyamplicol/src/pyamplicol/models/base.py @@ -0,0 +1,1586 @@ +# SPDX-License-Identifier: 0BSD +from __future__ import annotations + +import math +from collections.abc import Collection, Mapping, Sequence +from dataclasses import dataclass, field +from functools import cached_property +from typing import Any, Literal, cast + +from ._physics_ir import ( + ContractionIR, + CrossingIR, + GoldstonePolicy, + ParticleIdentityIR, + ParticleOrientation, + ParticleStatistics, + PropagatorGauge, + PropagatorIR, + PropagatorKind, + PropagatorMassClass, + SourceIR, + SourceStateIR, + WavefunctionFamily, +) +from .contact_decomposition import ( + CompiledContactOrbitCertificate, + CompiledContactOrbitStep, +) + + +@dataclass(frozen=True) +class Particle: + pdg: int + anti_pdg: int + spin: int + dimension: int + color_rep: int + mass: float = 0.0 + width: float = 0.0 + charge: float = 0.0 + weak_isospin: tuple[float, float] = (0.0, 0.0) + weak_hypercharge: tuple[float, float] = (0.0, 0.0) + + +def _runtime_particle_parameter_name( + declared: str | None, + *, + particle_pdg: int, + kind: str, + available_names: Collection[str], +) -> str | None: + """Resolve a model-owned name or its prepared-catalog fallback.""" + + if declared is not None: + return declared + fallback = f"particle.{particle_pdg}.{kind}" + return fallback if fallback in available_names else None + + +@dataclass(frozen=True) +class Vertex: + kind: int + particles: tuple[int, int, int] + coupling: tuple[float, float] = (1.0, 0.0) + + +CouplingOrders = tuple[tuple[str, int], ...] +QuantumNumberFlow = tuple[tuple[str, str], ...] +RecurrenceFlavourFlowOperation = Literal[ + "constant-result", + "append-left-result", + "append-right-result", + "concat-left-right-result", +] +RecurrenceQuantumNumberFlowOperation = Literal["particle-static-result"] +RecurrenceLCColorShapeKind = Literal[ + "singlet-forest", + "fundamental-open-string", + "antifundamental-open-string", + "adjoint-segment", +] +RecurrenceLCColorComponentKind = Literal["open-string", "adjoint-segment", "trace"] +RecurrenceLCColorComponentRole = Literal["active", "passive", "none"] +RecurrenceLCColorSourceSeedOperation = Literal["empty", "singleton"] +RecurrenceLCColorOperation = Literal[ + "concatenate-join", + "concatenate-keep", + "inherit-left", + "inherit-right", + "empty", + "close", +] + + +@dataclass(frozen=True) +class SourceSpinState: + helicity: int + chirality: int + spin_state: int | tuple[int, ...] + + +@dataclass(frozen=True) +class QuantumFlow: + chirality: int + spin_state: int | tuple[int, ...] + flavour_flow: tuple[int, ...] + quantum_number_flow: QuantumNumberFlow + coupling: tuple[float, float] + + +@dataclass(frozen=True) +class RecurrenceQuantumFlowContract: + """Declarative dynamic-flow contract consumed by recurrence generation.""" + + flavour_flow_operation: RecurrenceFlavourFlowOperation + quantum_number_flow_operation: RecurrenceQuantumNumberFlowOperation + admission_scope: Literal["static-input-contract"] = "static-input-contract" + + def __post_init__(self) -> None: + if self.flavour_flow_operation not in { + "constant-result", + "append-left-result", + "append-right-result", + "concat-left-right-result", + }: + raise ValueError( + "unsupported recurrence flavour-flow operation " + f"{self.flavour_flow_operation!r}" + ) + if self.quantum_number_flow_operation != "particle-static-result": + raise ValueError( + "unsupported recurrence quantum-number-flow operation " + f"{self.quantum_number_flow_operation!r}" + ) + if self.admission_scope != "static-input-contract": + raise ValueError( + f"unsupported recurrence admission scope {self.admission_scope!r}" + ) + + def to_json_dict(self) -> dict[str, str]: + return { + "admission_scope": self.admission_scope, + "flavour_flow_operation": self.flavour_flow_operation, + "quantum_number_flow_operation": self.quantum_number_flow_operation, + } + + +@dataclass(frozen=True) +class RecurrenceCurrentEmbeddingContract: + """Exact embedding of one source current into a full runtime current.""" + + input_embedding: tuple[int | None, ...] + source_dimension: int + full_dimension: int + source_basis: str + full_basis: str + proof_kind: str + provenance: tuple[tuple[str, str], ...] = () + + def __post_init__(self) -> None: + if self.source_dimension <= 0 or self.full_dimension <= 0: + raise ValueError("recurrence current dimensions must be positive") + if len(self.input_embedding) != self.full_dimension: + raise ValueError( + "recurrence current embedding length must equal the full dimension" + ) + populated = tuple( + int(component) + for component in self.input_embedding + if component is not None + ) + if tuple(sorted(populated)) != tuple(range(self.source_dimension)): + raise ValueError( + "recurrence current embedding must use every source component " + "exactly once" + ) + if not self.source_basis or not self.full_basis or not self.proof_kind: + raise ValueError( + "recurrence current embedding basis and proof labels must be nonempty" + ) + if self.provenance != tuple(sorted(set(self.provenance))): + raise ValueError( + "recurrence current embedding provenance must be sorted and unique" + ) + + @property + def source_projection(self) -> tuple[int, ...]: + """Return full-component indices ordered by source component.""" + + inverse = [-1] * self.source_dimension + for full_component, source_component in enumerate(self.input_embedding): + if source_component is not None: + inverse[int(source_component)] = full_component + return tuple(inverse) + + def to_json_dict(self) -> dict[str, object]: + return { + "full_basis": self.full_basis, + "full_dimension": self.full_dimension, + "input_embedding": list(self.input_embedding), + "proof_kind": self.proof_kind, + "provenance": [list(item) for item in self.provenance], + "source_basis": self.source_basis, + "source_dimension": self.source_dimension, + "source_projection": list(self.source_projection), + } + + +@dataclass(frozen=True) +class RecurrenceLCColorSourceSeedContract: + """Compiler-owned LC forest seed for one external source current.""" + + operation: RecurrenceLCColorSourceSeedOperation + output_shape_kind: RecurrenceLCColorShapeKind + component_kind: RecurrenceLCColorComponentKind | None + component_role: RecurrenceLCColorComponentRole + proof_digest: str | None = None + provenance: tuple[tuple[str, str], ...] = () + + def __post_init__(self) -> None: + if self.operation not in {"empty", "singleton"}: + raise ValueError( + f"unsupported recurrence LC source operation {self.operation!r}" + ) + if self.output_shape_kind not in { + "singlet-forest", + "fundamental-open-string", + "antifundamental-open-string", + "adjoint-segment", + }: + raise ValueError( + f"unsupported recurrence LC source shape {self.output_shape_kind!r}" + ) + if self.component_role not in {"active", "passive", "none"}: + raise ValueError( + f"unsupported recurrence LC source role {self.component_role!r}" + ) + if self.operation == "empty": + if self.component_kind is not None or self.component_role != "none": + raise ValueError( + "an empty recurrence LC source cannot declare a component" + ) + elif self.component_kind is None or self.component_role == "none": + raise ValueError( + "a singleton recurrence LC source requires component kind and role" + ) + expected = { + "singlet-forest": ("empty", None, "none"), + "fundamental-open-string": ("singleton", "open-string", "active"), + "antifundamental-open-string": ( + "singleton", + "open-string", + "active", + ), + "adjoint-segment": ("singleton", "adjoint-segment", "active"), + }[self.output_shape_kind] + if (self.operation, self.component_kind, self.component_role) != expected: + raise ValueError( + "recurrence LC source seed does not match its output shape" + ) + if self.proof_digest is not None and ( + len(self.proof_digest) != 64 + or any( + character not in "0123456789abcdef" + for character in self.proof_digest + ) + ): + raise ValueError( + "recurrence LC source proof digest must be lowercase SHA-256" + ) + if self.provenance != tuple(sorted(set(self.provenance))): + raise ValueError( + "recurrence LC source provenance must be sorted and unique" + ) + + +@dataclass(frozen=True) +class RecurrenceLCColorWitnessContract: + """One exact ordered-word term in a model-owned LC transition contract.""" + + input_permutation: tuple[int, int] + reverse_parent_mask: int + component_operation: RecurrenceLCColorOperation + result_component_kind: RecurrenceLCColorComponentKind | None + result_component_role: RecurrenceLCColorComponentRole + exact_factor: tuple[float, float] = (1.0, 0.0) + proof_digest: str | None = None + provenance: tuple[tuple[str, str], ...] = () + + def __post_init__(self) -> None: + if self.input_permutation not in {(0, 1), (1, 0)}: + raise ValueError( + "recurrence LC color input permutation must be (0, 1) or (1, 0)" + ) + if self.reverse_parent_mask not in range(4): + raise ValueError("recurrence LC color reversal mask must fit two inputs") + if self.component_operation not in { + "concatenate-join", + "concatenate-keep", + "inherit-left", + "inherit-right", + "empty", + "close", + }: + raise ValueError( + "unsupported recurrence LC color operation " + f"{self.component_operation!r}" + ) + if self.result_component_role not in {"active", "passive", "none"}: + raise ValueError( + f"unsupported recurrence LC result role {self.result_component_role!r}" + ) + if self.component_operation == "concatenate-join": + if ( + self.result_component_kind is None + or self.result_component_role == "none" + ): + raise ValueError( + "an LC join requires a result component kind and " + "active/passive role" + ) + elif self.component_operation == "close": + if self.result_component_role != "none": + raise ValueError( + "an LC closure cannot declare a recurrence result component" + ) + elif ( + self.result_component_kind is not None + or self.result_component_role + != ( + "active" + if self.component_operation in {"inherit-left", "inherit-right"} + else "none" + ) + ): + raise ValueError( + "LC non-join result roles must match their exact component operation" + ) + if self.exact_factor == (0.0, 0.0): + raise ValueError("recurrence LC color witness factor must be nonzero") + if self.proof_digest is not None and ( + len(self.proof_digest) != 64 + or any( + character not in "0123456789abcdef" for character in self.proof_digest + ) + ): + raise ValueError( + "recurrence LC color witness proof digest must be lowercase SHA-256" + ) + if self.provenance != tuple(sorted(set(self.provenance))): + raise ValueError( + "recurrence LC color witness provenance must be sorted and unique" + ) + + +@dataclass(frozen=True) +class RecurrenceLCColorTransitionContract: + """Closed, compiler-owned color-state contract for one local transition.""" + + rule_kind: str + witnesses: tuple[RecurrenceLCColorWitnessContract, ...] + + def __post_init__(self) -> None: + if not self.rule_kind: + raise ValueError("recurrence LC color rule kind must be nonempty") + if not self.witnesses: + raise ValueError("recurrence LC color transition requires exact witnesses") + + +@dataclass(frozen=True) +class VertexLoweringRule: + kind: int + backend: str + tensor_names: tuple[str, ...] = () + expression_head: str = "" + full_tensor_network_ready: bool = False + description: str = "" + kernel: str = "" + input_roles: tuple[str, str] = ("", "") + output_role: str = "" + coupling_mode: str = "none" + + def to_json_dict(self) -> dict[str, object]: + return { + "kind": self.kind, + "backend": self.backend, + "tensor_names": list(self.tensor_names), + "expression_head": self.expression_head, + "full_tensor_network_ready": self.full_tensor_network_ready, + "description": self.description, + "kernel": self.kernel, + "input_roles": list(self.input_roles), + "output_role": self.output_role, + "coupling_mode": self.coupling_mode, + } + + +@dataclass(frozen=True) +class VertexEvaluationEquivalence: + """Verified relation between oriented kernels used for evaluation reuse. + + ``factor`` states that the concrete oriented kernel equals ``factor`` times + its equivalence-class representative after applying ``input_order``. + ``input_exchange_factor``, when present, additionally proves that exchanging + the representative's two inputs multiplies it by that factor. Keeping this + model-owned lets compiled UFO models persist relations proven from their + actual lowered Symbolica expressions. + """ + + class_id: str + factor: tuple[float, float] = (1.0, 0.0) + input_order: tuple[int, int] = (0, 1) + input_exchange_factor: tuple[float, float] | None = None + verified: bool = True + + def __post_init__(self) -> None: + if not self.class_id: + raise ValueError("vertex evaluation equivalence class must be non-empty") + if self.input_order not in {(0, 1), (1, 0)}: + raise ValueError("vertex evaluation input order must be (0, 1) or (1, 0)") + if not all(math.isfinite(component) for component in self.factor): + raise ValueError("vertex evaluation equivalence factor must be finite") + if self.factor == (0.0, 0.0): + raise ValueError("vertex evaluation equivalence factor must be nonzero") + if self.input_exchange_factor is not None: + if not all( + math.isfinite(component) for component in self.input_exchange_factor + ): + raise ValueError( + "vertex evaluation input-exchange factor must be finite" + ) + if self.input_exchange_factor == (0.0, 0.0): + raise ValueError( + "vertex evaluation input-exchange factor must be nonzero" + ) + + def to_json_dict(self) -> dict[str, object]: + return { + "class_id": self.class_id, + "factor": list(self.factor), + "input_order": list(self.input_order), + "input_exchange_factor": ( + None + if self.input_exchange_factor is None + else list(self.input_exchange_factor) + ), + "verified": self.verified, + } + + +@dataclass(frozen=True) +class PropagatorLoweringRule: + particle_id: int + chirality: int + backend: str + full_tensor_network_ready: bool + applies_propagator: bool + kernel: str + kind: PropagatorKind + mass_class: PropagatorMassClass + gauge: PropagatorGauge | None = None + numerator: str | None = None + denominator: str | None = None + custom_source: str | None = None + auxiliary_policy: str | None = None + goldstone_policy: GoldstonePolicy = "not-applicable" + description: str = "" + + def to_json_dict(self) -> dict[str, object]: + return { + "particle_id": self.particle_id, + "chirality": self.chirality, + "backend": self.backend, + "full_tensor_network_ready": self.full_tensor_network_ready, + "applies_propagator": self.applies_propagator, + "kernel": self.kernel, + "kind": self.kind, + "mass_class": self.mass_class, + "gauge": self.gauge, + "numerator": self.numerator, + "denominator": self.denominator, + "custom_source": self.custom_source, + "auxiliary_policy": self.auxiliary_policy, + "goldstone_policy": self.goldstone_policy, + "description": self.description, + } + + +@dataclass +class Model: + name: str + compiled: Any | None = field(default=None, repr=False, compare=False) + particles: dict[int, Particle] = field(default_factory=dict) + vertices: tuple[Vertex, ...] = () + _source_ir_by_particle: dict[int, SourceIR] = field( + default_factory=dict, + init=False, + repr=False, + compare=False, + ) + _propagator_ir_by_state: dict[tuple[int, int], PropagatorIR] = field( + default_factory=dict, + init=False, + repr=False, + compare=False, + ) + _direct_contraction_ir_by_state: dict[ + tuple[int, int, int, int], ContractionIR | None + ] = field( + default_factory=dict, + init=False, + repr=False, + compare=False, + ) + _closure_contraction_ir_by_state: dict[tuple[int, int], ContractionIR | None] = ( + field( + default_factory=dict, + init=False, + repr=False, + compare=False, + ) + ) + + @cached_property + def _species_by_pdg(self) -> dict[int, Particle]: + species: dict[int, Particle] = {} + for particle in self.particles.values(): + species.setdefault(particle.pdg, particle) + species.setdefault(particle.anti_pdg, particle) + return species + + @cached_property + def _property_sign_by_pdg(self) -> dict[int, int]: + signs: dict[int, int] = {} + for particle in self.particles.values(): + signs.setdefault(particle.pdg, 1) + if particle.anti_pdg != particle.pdg: + signs.setdefault(particle.anti_pdg, -1) + else: + signs.setdefault(particle.anti_pdg, 1) + return signs + + @cached_property + def _vertices_by_input(self) -> dict[tuple[str, int, int], tuple[Vertex, ...]]: + return {} + + def particle(self, pdg: int) -> Particle: + species = self._species_by_pdg.get(pdg) + if species is None: + raise KeyError(f"particle not in model: {pdg}") + return species + + def anti_particle(self, pdg: int) -> int: + particle = self.particle(pdg) + return particle.anti_pdg if particle.pdg == pdg else particle.pdg + + def mass(self, pdg: int) -> float: + return self.particle(pdg).mass + + def width(self, pdg: int) -> float: + return self.particle(pdg).width + + def spin(self, pdg: int) -> int: + spin = self.particle(pdg).spin + if spin < 0: + raise ValueError(f"spin is ill-defined for particle {pdg}") + return spin + + def dimension(self, pdg: int) -> int: + return self.particle(pdg).dimension + + def charge(self, pdg: int) -> float: + return self._property_sign(pdg) * self.particle(pdg).charge + + def weak_isospin_l(self, pdg: int) -> float: + return self._property_sign(pdg) * self.particle(pdg).weak_isospin[0] + + def weak_isospin_r(self, pdg: int) -> float: + return self._property_sign(pdg) * self.particle(pdg).weak_isospin[1] + + def color_rep(self, pdg: int) -> int: + del pdg + raise NotImplementedError("model does not define colour representations") + + def color_dim(self, pdg: int) -> int: + return abs(self.color_rep(pdg)) + + def is_fermion(self, pdg: int) -> bool: + del pdg + raise NotImplementedError("model does not define a fermion role") + + def is_chiral_eligible(self, pdg: int) -> bool: + del pdg + raise NotImplementedError("model does not define chiral source eligibility") + + def is_fundamental_colored_fermion(self, pdg: int) -> bool: + del pdg + raise NotImplementedError( + "model does not define a fundamental colored-fermion role" + ) + + def is_massless_adjoint_vector(self, pdg: int) -> bool: + del pdg + raise NotImplementedError( + "model does not define a massless adjoint-vector role" + ) + + def is_singlet(self, pdg: int) -> bool: + return self.color_rep(pdg) == 1 + + def vertex_lowering_rule(self, kind: int) -> VertexLoweringRule: + raise NotImplementedError + + def vertex_evaluation_equivalence( + self, + kind: int, + ) -> VertexEvaluationEquivalence: + """Return a conservative, always-safe evaluation equivalence class. + + Models with compiled symbolic kernels may override this with broader + classes. A class unique to one model type and vertex kind still enables + exact fan-out reuse when several DAG attachments use the same kernel and + input currents. + """ + + model_type = f"{type(self).__module__}.{type(self).__qualname__}" + return VertexEvaluationEquivalence(class_id=f"{model_type}:{int(kind)}") + + def vertex_contact_orbit_contracts( + self, + kind: int, + ) -> tuple[ + tuple[CompiledContactOrbitCertificate, ...], + tuple[CompiledContactOrbitStep, ...], + ]: + """Return compiler-owned contact-path orbit proofs when they exist.""" + + del kind + return (), () + + def global_helicity_flip_equivalence_is_proven( + self, + vertices: Sequence[Vertex], + ) -> bool: + """Return whether the model proves parity pairing for these vertices. + + Coupling-order names and particle labels are not such a proof. External + models therefore remain fail-closed until their compiler records an + expression-level equivalence certificate. + """ + + del vertices + return False + + def pure_massless_adjoint_helicity_zero_rule_is_proven( + self, + process: Any, + vertices: Sequence[Vertex], + ) -> bool: + """Return whether all-equal and one-opposite helicities vanish. + + This tree-amplitude identity requires more than massless adjoint-vector + external states. Generic and external models therefore remain + fail-closed until their compiler can provide an expression-level proof. + """ + + del process, vertices + return False + + def adjoint_current_reflection_phase( + self, + vertex: Vertex, + ) -> tuple[float, float] | None: + """Return a proven two-input adjoint-current reflection phase. + + A non-null result certifies the local off-shell identity obtained by + exchanging the two inputs of ``vertex``. This is deliberately + separate from full-amplitude LC trace reflection: a pure gauge + subcurrent may be reusable inside a mixed process even when the full + process has no trace-reflection reduction. Generic models remain + fail-closed until their lowered component expressions prove the + identity. + """ + + del vertex + return None + + def lc_trace_reflection_equivalence_is_proven(self, process: Any) -> bool: + """Return whether one LC trace may represent its reversed ordering. + + Reflection folding is an amplitude-level identity, not a consequence of + adjoint colour alone. External models therefore remain fail-closed + unless their compiled model records a proof and overrides this hook. + """ + + del process + return False + + def shared_single_trace_color_basis_is_proven(self, process: Any) -> bool: + """Return whether NLC/full may use the shared single-trace recursion. + + Adjoint representation metadata alone does not prove that the model's + color tensors obey the Yang--Mills trace relations assumed by that + optimized recursion. Generic models therefore remain fail-closed. + """ + + del process + return False + + def adjoint_tree_color_basis_is_proven( + self, + process: Any, + *, + max_coupling_orders: Mapping[str, int] | None = None, + ) -> bool: + """Prove connected DDM colour, independently of helicity identities. + + Singlet exchange can produce a colour forest even when every local + adjoint tensor is a structure constant. Implementations must account + for the selected interactions and coupling-order limits. + """ + + del process, max_coupling_orders + return False + + def propagator_lowering_rule( + self, + particle_id: int, + chirality: int = 0, + ) -> PropagatorLoweringRule: + del particle_id, chirality + raise NotImplementedError("model does not define propagator lowering") + + def source_spin_states(self, particle_id: int) -> tuple[SourceSpinState, ...]: + if self.is_chiral_eligible(particle_id): + return ( + SourceSpinState(helicity=-1, chirality=-1, spin_state=-1), + SourceSpinState(helicity=1, chirality=1, spin_state=1), + ) + spin = self.spin(particle_id) + if spin == 1: + return (SourceSpinState(helicity=0, chirality=0, spin_state=0),) + if spin == 2: + return ( + SourceSpinState(helicity=-1, chirality=0, spin_state=-1), + SourceSpinState(helicity=1, chirality=0, spin_state=1), + ) + if spin == 3: + return ( + SourceSpinState(helicity=-1, chirality=0, spin_state=-1), + SourceSpinState(helicity=0, chirality=0, spin_state=0), + SourceSpinState(helicity=1, chirality=0, spin_state=1), + ) + return (SourceSpinState(helicity=0, chirality=0, spin_state=0),) + + def source_wavefunction_kind(self, particle_id: int) -> str: + dimension = self.current_dimension(particle_id) + if self.is_fermion(particle_id): + return "fermion" + if dimension == 1: + return "scalar" + if dimension in {2, 4}: + return "vector" + if dimension == 16: + return "spin2" + return "unknown" + + def source_orientation(self, particle_id: int) -> str: + """Return the source orientation from the model's anti-particle relation.""" + + particle = self.particle(particle_id) + antiparticle_id = self.anti_particle(particle_id) + if self.anti_particle(antiparticle_id) != particle_id: + raise ValueError( + f"particle/antiparticle relation is not involutive for source " + f"particle {particle_id}" + ) + if antiparticle_id == particle_id: + orientation = "self-conjugate" + elif particle_id == particle.pdg and antiparticle_id == particle.anti_pdg: + orientation = "particle" + elif particle_id == particle.anti_pdg and antiparticle_id == particle.pdg: + orientation = "antiparticle" + else: + raise ValueError( + f"source particle {particle_id} is inconsistent with its model-owned " + "particle/antiparticle relation" + ) + if orientation == "self-conjugate" and self.is_fermion(particle_id): + raise ValueError( + f"unsupported self-conjugate fermion source {particle_id}: " + "Majorana/FNV source wavefunctions are not implemented" + ) + return orientation + + def _particle_identity_ir(self, particle_id: int) -> ParticleIdentityIR: + """Return canonical oriented identity without assigning an SM role.""" + + particle_id = int(particle_id) + particle = self.particle(particle_id) + anti_particle_id = int(self.anti_particle(particle_id)) + self_conjugate = particle_id == anti_particle_id + canonical_id = f"model:{self.name}:state:{particle_id}" + anti_canonical_id = f"model:{self.name}:state:{anti_particle_id}" + return ParticleIdentityIR( + canonical_id=canonical_id, + species_id=f"model:{self.name}:species:{particle.pdg}", + anti_canonical_id=anti_canonical_id, + display_name=f"pdg_{particle_id}", + anti_display_name=f"pdg_{anti_particle_id}", + pdg_label=particle_id, + anti_pdg_label=anti_particle_id, + orientation=cast(ParticleOrientation, self.source_orientation(particle_id)), + self_conjugate=self_conjugate, + ) + + def _source_crossing_ir(self, particle_id: int) -> CrossingIR: + """Return the model-owned transform from outgoing to incoming source.""" + + particle_id = int(particle_id) + if self.is_chiral_eligible(particle_id): + return CrossingIR(chirality_factor=-1, spin_state_factor=-1) + if self.source_wavefunction_kind(particle_id) in {"vector", "spin2"}: + return CrossingIR(helicity_factor=-1, spin_state_factor=-1) + return CrossingIR() + + def _source_ir(self, particle_id: int) -> SourceIR: + """Return the canonical source contract for one oriented particle. + + A compiled model's structural metadata is immutable during generation. + Caching here ensures DAG construction, physics metadata, and execution + schemas all consume the same contract object. + """ + + particle_id = int(particle_id) + cache = self.__dict__.setdefault("_source_ir_by_particle", {}) + cached = cache.get(particle_id) + if cached is not None: + return cached + source_ir = self._build_source_ir(particle_id) + return cache.setdefault(particle_id, source_ir) + + def _build_source_ir(self, particle_id: int) -> SourceIR: + """Build an uncached source contract for an oriented particle.""" + + states = tuple( + SourceStateIR( + helicity=int(state.helicity), + chirality=int(state.chirality), + spin_state=state.spin_state, + ) + for state in self.source_spin_states(particle_id) + ) + dimensions = { + int(self.current_dimension(particle_id, state.chirality)) + for state in states + } + if len(dimensions) != 1: + raise ValueError( + f"source {particle_id} has state-dependent component dimensions: " + f"{sorted(dimensions)}" + ) + wavefunction_family = self.source_wavefunction_kind(particle_id) + if wavefunction_family not in { + "scalar", + "fermion", + "vector", + "spin2", + "ghost", + "auxiliary", + }: + raise ValueError( + f"source {particle_id} has unsupported wavefunction family " + f"{wavefunction_family!r}" + ) + auxiliary = self.auxiliary_kind(particle_id) + statistics: ParticleStatistics + if auxiliary is not None: + statistics = "auxiliary" + elif self.spin(particle_id) < 0: + statistics = "ghost" + elif self.is_fermion(particle_id): + statistics = "fermion" + else: + statistics = "boson" + return SourceIR( + identity=self._particle_identity_ir(particle_id), + statistics=statistics, + wavefunction_family=cast(WavefunctionFamily, wavefunction_family), + component_dimension=dimensions.pop(), + states=states, + crossing=self._source_crossing_ir(particle_id), + basis=self._current_basis(particle_id, states[0].chirality), + mass_parameter=self._runtime_parameter_name(particle_id, "mass"), + width_parameter=self._runtime_parameter_name(particle_id, "width"), + ) + + def _propagator_ir( + self, + particle_id: int, + chirality: int = 0, + ) -> PropagatorIR: + """Project the active propagator lowering into a typed model contract.""" + + particle_id = int(particle_id) + chirality = int(chirality) + key = (particle_id, chirality) + cache = self.__dict__.setdefault("_propagator_ir_by_state", {}) + cached = cache.get(key) + if cached is not None: + return cached + rule = self.propagator_lowering_rule(particle_id, chirality) + auxiliary_policy = rule.auxiliary_policy + if not rule.applies_propagator: + auxiliary_policy = ( + auxiliary_policy or self.auxiliary_kind(particle_id) or rule.kernel + ) + default_numerator, default_denominator = _propagator_formula_metadata( + rule.kind, rule.gauge + ) + result = PropagatorIR( + identity=self._particle_identity_ir(particle_id), + chirality=chirality, + kind=rule.kind, + backend=rule.backend, + basis=self._current_basis(particle_id, chirality), + applies_propagator=rule.applies_propagator, + kernel=rule.kernel, + full_tensor_network_ready=rule.full_tensor_network_ready, + mass_class=rule.mass_class, + gauge=rule.gauge, + numerator=rule.numerator or default_numerator, + denominator=rule.denominator or default_denominator, + mass_parameter=self._runtime_parameter_name(particle_id, "mass"), + width_parameter=self._runtime_parameter_name(particle_id, "width"), + custom_source=rule.custom_source, + auxiliary_policy=auxiliary_policy, + goldstone_policy=rule.goldstone_policy, + description=rule.description, + ) + return cache.setdefault(key, result) + + def _current_basis(self, particle_id: int, chirality: int = 0) -> str: + dimension = int(self.current_dimension(particle_id, chirality)) + auxiliary = self.auxiliary_kind(particle_id) + if auxiliary is not None: + return f"auxiliary:{auxiliary}" + if self.is_fermion(particle_id): + return "weyl-chiral" if dimension == 2 else "dirac" + family = self.source_wavefunction_kind(particle_id) + if family == "scalar" and dimension == 1: + return "scalar" + if family == "vector" and dimension == 4: + return "lorentz-vector" + if family == "spin2" and dimension == 16: + return "lorentz-rank-2" + if dimension == 6: + return "antisymmetric-lorentz-pair" + return f"components:{dimension}" + + def _runtime_parameter_name(self, particle_id: int, kind: str) -> str | None: + provider = getattr(self, f"runtime_{kind}_parameter_name", None) + if not callable(provider): + return None + name = provider(int(particle_id)) + return None if name is None else str(name) + + def runtime_normalization_payload(self, dag: Any) -> dict[str, object]: + """Return model-owned averaging, symmetry, color, and coupling factors.""" + + del dag + raise NotImplementedError("model does not define runtime normalization") + + def runtime_normalization_parameter_defaults(self) -> Mapping[str, float]: + """Return mutable runtime parameters used by normalization only.""" + + return {} + + def recurrence_quantum_flow_contract( + self, + vertex: Vertex, + left_particle_id: int, + right_particle_id: int, + ) -> RecurrenceQuantumFlowContract: + """Declare the finite dynamic-flow law used by recurrence generation.""" + + del vertex, left_particle_id, right_particle_id + raise NotImplementedError( + "model does not declare a recurrence quantum-flow contract" + ) + + def recurrence_current_embedding_contract( + self, + particle_id: int, + source_chirality: int, + ) -> RecurrenceCurrentEmbeddingContract: + """Declare how a source state embeds into a runtime full current.""" + + del particle_id, source_chirality + raise NotImplementedError( + "model does not declare recurrence current-embedding contracts" + ) + + def _standard_recurrence_current_embedding_contract( + self, + particle_id: int, + source_chirality: int, + ) -> RecurrenceCurrentEmbeddingContract: + """Return the standard Weyl-to-Dirac current embedding.""" + + particle = int(particle_id) + chirality = int(source_chirality) + if chirality not in {-1, 1} or not self.is_chiral_eligible(particle): + raise NotImplementedError( + "standard recurrence current embedding requires a massless " + "chiral fermion source" + ) + source_dimension = int(self.current_dimension(particle, chirality)) + full_dimension = int(self.current_dimension(particle, 0)) + source_basis = self._current_basis(particle, chirality) + full_basis = self._current_basis(particle, 0) + if ( + source_dimension != 2 + or full_dimension != 4 + or source_basis != "weyl-chiral" + or full_basis != "dirac" + ): + raise NotImplementedError( + "standard recurrence current embedding supports a two-component " + "Weyl source in a four-component Dirac current" + ) + embedding = ( + (0, 1, None, None) + if chirality == 1 + else (None, None, 0, 1) + ) + return RecurrenceCurrentEmbeddingContract( + input_embedding=embedding, + source_dimension=source_dimension, + full_dimension=full_dimension, + source_basis=source_basis, + full_basis=full_basis, + proof_kind="standard-weyl-dirac-current-ordering-v1", + provenance=( + ("chirality", f"{chirality:+d}"), + ("contract", "standard-weyl-dirac-current-ordering-v1"), + ( + "particle-identity", + self._particle_identity_ir(particle).canonical_id, + ), + ), + ) + + def _standard_recurrence_quantum_flow_contract( + self, + vertex: Vertex, + left_particle_id: int, + right_particle_id: int, + ) -> RecurrenceQuantumFlowContract: + """Return the exact contract implemented by ``combine_flavour_flow``.""" + + result_particle = int(vertex.particles[2]) + left_particle = int(left_particle_id) + right_particle = int(right_particle_id) + if self.is_fermion(result_particle): + if self.is_fermion(left_particle): + operation: RecurrenceFlavourFlowOperation = "append-left-result" + elif self.is_fermion(right_particle): + operation = "append-right-result" + else: + operation = "constant-result" + else: + operation = "constant-result" + return RecurrenceQuantumFlowContract( + flavour_flow_operation=operation, + quantum_number_flow_operation="particle-static-result", + ) + + def recurrence_lc_color_shape_contract( + self, + particle_id: int, + chirality: int = 0, + ) -> RecurrenceLCColorShapeKind: + """Declare the ordered partial-color shape of one current state.""" + + del particle_id, chirality + raise NotImplementedError( + "model does not declare recurrence LC color-shape contracts" + ) + + def recurrence_lc_source_color_contract( + self, + particle_id: int, + chirality: int = 0, + ) -> RecurrenceLCColorSourceSeedContract: + """Declare the exact LC forest seeded by one external source.""" + + del particle_id, chirality + raise NotImplementedError( + "model does not declare recurrence LC source-color contracts" + ) + + def _standard_recurrence_lc_source_color_contract( + self, + particle_id: int, + chirality: int = 0, + ) -> RecurrenceLCColorSourceSeedContract: + shape = self._standard_recurrence_lc_color_shape_contract( + particle_id, + chirality, + ) + representation = int(self.color_rep(int(particle_id))) + provenance = tuple( + sorted( + ( + ("contract", "standard-lc-source-seed-v1"), + ("color-representation", str(representation)), + ("shape", shape), + ) + ) + ) + if shape == "singlet-forest": + return RecurrenceLCColorSourceSeedContract( + "empty", + shape, + None, + "none", + provenance=provenance, + ) + component_kind: RecurrenceLCColorComponentKind = ( + "adjoint-segment" if shape == "adjoint-segment" else "open-string" + ) + return RecurrenceLCColorSourceSeedContract( + "singleton", + shape, + component_kind, + "active", + provenance=provenance, + ) + + def _standard_recurrence_lc_color_shape_contract( + self, + particle_id: int, + chirality: int = 0, + ) -> RecurrenceLCColorShapeKind: + del chirality + representation = int(self.color_rep(int(particle_id))) + if representation == 1: + return "singlet-forest" + if representation == 3: + return "fundamental-open-string" + if representation == -3: + return "antifundamental-open-string" + if representation == 8: + return "adjoint-segment" + raise NotImplementedError( + "standard recurrence LC color shapes support only representations " + f"1, 3, -3, and 8; got {representation}" + ) + + def recurrence_lc_color_transition_contract( + self, + vertex: Vertex, + *, + closure: bool, + ) -> RecurrenceLCColorTransitionContract: + """Declare exact ordered partial-color operations for one transition.""" + + del vertex, closure + raise NotImplementedError( + "model does not declare recurrence LC color-transition contracts" + ) + + def _standard_recurrence_lc_color_transition_contract( + self, + vertex: Vertex, + *, + closure: bool, + ) -> RecurrenceLCColorTransitionContract: + representations = tuple( + int(self.color_rep(particle)) for particle in vertex.particles + ) + rule_kind = str(self.vertex_color_structure(vertex)) + if rule_kind in {"model-defined", "generic-tensor"}: + absolute = tuple(abs(value) for value in representations) + if all(value == 1 for value in absolute): + rule_kind = "singlet" + else: + raise NotImplementedError( + "colored recurrence tensors require an exact model-owned " + "color projection; got an unresolved tensor for " + f"representations {representations!r}" + ) + if closure: + if any(abs(value) != 1 for value in representations): + raise NotImplementedError( + "colored recurrence closures require compiler-owned LC " + "closure terms" + ) + return RecurrenceLCColorTransitionContract( + rule_kind, + ( + RecurrenceLCColorWitnessContract( + input_permutation=(0, 1), + reverse_parent_mask=0, + component_operation="close", + result_component_kind=None, + result_component_role="none", + ), + ), + ) + if rule_kind != "singlet": + raise NotImplementedError( + "colored recurrence transitions require compiler-owned ordered-word " + f"terms; generic rule {rule_kind!r} is validation metadata only" + ) + if rule_kind == "singlet": + return RecurrenceLCColorTransitionContract( + rule_kind, + ( + RecurrenceLCColorWitnessContract( + input_permutation=(0, 1), + reverse_parent_mask=0, + component_operation="empty", + result_component_kind=None, + result_component_role="none", + ), + ), + ) + raise NotImplementedError(f"unsupported recurrence LC color rule {rule_kind!r}") + + def allowed_quantum_flows( + self, + vertex: Vertex, + left_index: Any, + right_index: Any, + ) -> tuple[QuantumFlow, ...]: + result_particle = vertex.particles[2] + chiralities = _model_vertex_result_chiralities( + self, + vertex, + left_index, + right_index, + ) + flows: list[QuantumFlow] = [] + for chirality in chiralities: + flows.append( + QuantumFlow( + chirality=chirality, + spin_state=self.result_spin_state(result_particle, chirality), + flavour_flow=self.combine_flavour_flow( + result_particle, + left_index, + right_index, + ), + quantum_number_flow=self.quantum_number_flow(result_particle), + coupling=vertex.coupling, + ) + ) + return tuple(flows) + + def combine_flavour_flow( + self, + result_particle: int, + left_index: Any, + right_index: Any, + ) -> tuple[int, ...]: + left_pdg = _index_particle_id(left_index) + right_pdg = _index_particle_id(right_index) + left_flow = _index_flavour_flow(left_index) + right_flow = _index_flavour_flow(right_index) + + if self.is_fermion(result_particle): + if self.is_fermion(left_pdg): + return _append_flavour_transition(left_flow, result_particle) + if self.is_fermion(right_pdg): + return _append_flavour_transition(right_flow, result_particle) + + return (result_particle,) + + def result_spin_state(self, particle_id: int, chirality: int) -> int: + if self.is_fermion(particle_id): + return chirality + return 0 + + def current_dimension(self, particle_id: int, chirality: int = 0) -> int: + if chirality != 0 and self.is_chiral_eligible(particle_id): + return 2 + try: + return self.dimension(particle_id) + except KeyError: + return 0 + + def direct_contraction_ir( + self, + left_particle_id: int, + right_particle_id: int, + left_chirality: int = 0, + right_chirality: int = 0, + ) -> ContractionIR | None: + """Return an explicitly installed contraction for two current states.""" + + key = ( + int(left_particle_id), + int(left_chirality), + int(right_particle_id), + int(right_chirality), + ) + return self._direct_contraction_ir_by_state.get(key) + + def closure_contraction_ir( + self, + particle_id: int, + chirality: int = 0, + ) -> ContractionIR | None: + """Return an explicitly installed one-current closure projection.""" + + key = (int(particle_id), int(chirality)) + return self._closure_contraction_ir_by_state.get(key) + + def direct_contraction_possible( + self, + left_particle_id: int, + right_particle_id: int, + ) -> bool: + """Over-approximate closure using only declared contraction records.""" + + left = int(left_particle_id) + right = int(right_particle_id) + return any( + value is not None and key[0] == left and key[2] == right + for key, value in self._direct_contraction_ir_by_state.items() + ) + + def quantum_number_flow(self, particle_id: int) -> QuantumNumberFlow: + del particle_id + raise NotImplementedError("model does not define exact quantum-number flow") + + def auxiliary_kind(self, particle_id: int) -> str | None: + del particle_id + return None + + def vertex_component_expression( + self, + kind: int, + left: Sequence[Any], + right: Sequence[Any], + *, + result_particle_id: int, + result_chirality: int, + left_chirality: int = 0, + right_chirality: int = 0, + coupling: tuple[Any, Any] = (1.0, 0.0), + left_momentum: Sequence[Any] | None = None, + right_momentum: Sequence[Any] | None = None, + ) -> tuple[Any, ...]: + raise NotImplementedError + + def propagator_component_expression( + self, + particle_id: int, + value: Sequence[Any], + momentum: Sequence[Any], + *, + chirality: int = 0, + propagator: PropagatorIR | None = None, + ) -> tuple[Any, ...]: + del propagator + raise NotImplementedError + + def iter_vertices(self, *, color_accuracy: str = "lc") -> tuple[Vertex, ...]: + if color_accuracy == "lc": + return tuple(self.vertices) + if color_accuracy in {"nlc", "full"}: + return tuple(self.vertices) + raise ValueError(f"unknown colour accuracy: {color_accuracy}") + + def vertices_for_inputs( + self, + left_pdg: int, + right_pdg: int, + *, + color_accuracy: str = "lc", + ) -> tuple[Vertex, ...]: + key = (color_accuracy, int(left_pdg), int(right_pdg)) + if key not in self._vertices_by_input: + self._vertices_by_input[key] = tuple( + vertex + for vertex in self.iter_vertices(color_accuracy=color_accuracy) + if vertex.particles[0] == left_pdg and vertex.particles[1] == right_pdg + ) + return self._vertices_by_input[key] + + def vertices_accepting( + self, + left_pdg: int, + right_pdg: int, + *, + color_accuracy: str = "lc", + ) -> tuple[Vertex, ...]: + """Return model vertices for a local current-current combination. + + This is the process-generic name used by the DAG compiler. It is a + thin alias around the model table lookup, but keeping the name at the + model boundary prevents production code from classifying whole process + families before asking which local interactions are allowed. + """ + + return self.vertices_for_inputs( + left_pdg, + right_pdg, + color_accuracy=color_accuracy, + ) + + def skip_duplicate_vertex_orientation(self, vertex: Vertex) -> bool: + """Return whether a mirrored table entry should be skipped by DAG sweeps. + + Generic DAG generation asks the model because duplicated orientations are + a model-table convention, not a process-family rule. + """ + + return False + + def vertex_closure_allowed(self, vertex: Vertex) -> bool: + """Return whether a scalar vertex result represents a vacuum closure. + + The legacy built-in model uses auxiliary scalar result particles for a + few contact interactions. Generic UFO particles are ordinary currents + unless their compiled lowering explicitly introduces such an auxiliary. + """ + + return True + + def vertex_coupling_orders(self, vertex: Vertex) -> CouplingOrders: + """Return model-generic coupling-order increments for one vertex. + + The keys intentionally mirror UFO-style coupling-order names. They are + used only as local model metadata, so DAG pruning can cap e.g. QCD or + QED order without recognizing whole process families. + """ + + del vertex + return () + + def coupling_order_hierarchies(self) -> dict[str, int]: + """Return UFO-style priorities used by minimal-order generation. + + A lower hierarchy value is preferred. Models without explicit order + metadata conservatively assign equal priority to every observed order. + """ + + return {} + + def vertex_color_weight( + self, + vertex: Vertex, + *, + color_accuracy: str, + ) -> tuple[float, float]: + """Return the model-owned coefficient for one projected color vertex.""" + + del vertex, color_accuracy + return (1.0, 0.0) + + def vertex_color_structure(self, vertex: Vertex) -> str: + """Return the model-level color tensor family for local projection. + + Built-in kernels already encode their projected color algebra. External + tensor models override this hook so the generic color engine can apply + flow-dependent identities without recognizing particles or processes. + """ + + del vertex + return "model-defined" + + def vertex_is_internal_contact_fragment(self, vertex: Vertex) -> bool: + del vertex + return False + + def combine_coupling_orders( + self, + left_index: Any, + right_index: Any, + vertex: Vertex, + ) -> CouplingOrders: + totals: dict[str, int] = {} + for orders in ( + _index_coupling_orders(left_index), + _index_coupling_orders(right_index), + self.vertex_coupling_orders(vertex), + ): + for name, value in orders: + totals[str(name).upper()] = totals.get(str(name).upper(), 0) + int( + value + ) + return tuple(sorted((name, value) for name, value in totals.items() if value)) + + def current_allowed(self, index: Any) -> bool: + """Return whether a generated current index is valid in this model.""" + + try: + particle_id = int(index.particle_id) + chirality = int(getattr(index, "chirality", 0)) + if chirality != 0 and self.is_chiral_eligible(particle_id): + return True + return self.dimension(particle_id) > 0 + except (KeyError, TypeError, ValueError): + return False + + def _species_particle(self, pdg: int) -> Particle | None: + return self._species_by_pdg.get(pdg) + + def _property_sign(self, pdg: int) -> int: + try: + return self._property_sign_by_pdg[pdg] + except KeyError as exc: + raise KeyError(f"particle not in model: {pdg}") from exc + + +def _propagator_formula_metadata( + kind: PropagatorKind, + gauge: PropagatorGauge | None, +) -> tuple[str | None, str | None]: + """Return descriptive formulas for the implemented propagator kernel.""" + + if kind == "identity": + return ("identity", "1") + if kind == "custom": + return (None, None) + if kind == "weyl-fermion": + return ("i*weyl_slash(momentum)", "momentum_squared") + if kind == "dirac-fermion": + return ( + "i*(dirac_slash(momentum)+oriented_mass)", + "momentum_squared-mass_squared+i*mass*width", + ) + if kind == "vector" and gauge == "feynman": + return ("-i*metric", "momentum_squared") + if kind == "vector" and gauge == "unitary": + return ( + "-i*(metric-momentum_outer/mass_squared)", + "momentum_squared-mass_squared+i*mass*width", + ) + if kind == "spin2": + return ( + "i*spin2_projector", + "momentum_squared-mass_squared+i*mass*width", + ) + if kind == "scalar": + return ("i", "momentum_squared-mass_squared+i*mass*width") + return (None, None) + + +from .expressions import ( # noqa: E402 + _append_flavour_transition, + _index_coupling_orders, + _index_flavour_flow, + _index_particle_id, + _model_vertex_result_chiralities, +) + +__all__ = [ + "ContractionIR", + "CouplingOrders", + "Model", + "Particle", + "PropagatorLoweringRule", + "QuantumFlow", + "QuantumNumberFlow", + "RecurrenceCurrentEmbeddingContract", + "RecurrenceFlavourFlowOperation", + "RecurrenceLCColorComponentKind", + "RecurrenceLCColorComponentRole", + "RecurrenceLCColorOperation", + "RecurrenceLCColorShapeKind", + "RecurrenceLCColorSourceSeedContract", + "RecurrenceLCColorSourceSeedOperation", + "RecurrenceLCColorTransitionContract", + "RecurrenceLCColorWitnessContract", + "RecurrenceQuantumFlowContract", + "RecurrenceQuantumNumberFlowOperation", + "SourceSpinState", + "Vertex", + "VertexEvaluationEquivalence", + "VertexLoweringRule", +] diff --git a/pyamplicol/src/pyamplicol/models/builtin/__init__.py b/pyamplicol/src/pyamplicol/models/builtin/__init__.py new file mode 100644 index 0000000000..a375f4e4c8 --- /dev/null +++ b/pyamplicol/src/pyamplicol/models/builtin/__init__.py @@ -0,0 +1,6 @@ +# SPDX-License-Identifier: 0BSD +from __future__ import annotations + +from .model import BuiltinSMModel + +__all__ = ["BuiltinSMModel"] diff --git a/pyamplicol/src/pyamplicol/models/builtin/adapters.py b/pyamplicol/src/pyamplicol/models/builtin/adapters.py new file mode 100644 index 0000000000..69dae50c38 --- /dev/null +++ b/pyamplicol/src/pyamplicol/models/builtin/adapters.py @@ -0,0 +1,185 @@ +# SPDX-License-Identifier: 0BSD +"""Adapters from the hand-written built-in SM to canonical model records.""" + +from __future__ import annotations + +import hashlib +from pathlib import Path + +from .model import BuiltinSMModel +from .process_catalog import PDGS + + +def build_model_payload() -> tuple[dict[str, object], dict[str, tuple[float, float]]]: + """Return the canonical compiler payload for the built-in SM.""" + + model = BuiltinSMModel() + particles = [] + parameters: dict[str, tuple[float, float]] = { + "alpha_s": (0.118, 0.0), + "alpha_ew": (float(model.alpha_ew), 0.0), + } + for _key, particle in sorted(model.particles.items()): + mass_name = ( + _parameter_name("mass", particle.pdg) + if float(particle.mass) != 0.0 + else "ZERO" + ) + width_name = ( + _parameter_name("width", particle.pdg) + if float(particle.width) != 0.0 + else "ZERO" + ) + if mass_name != "ZERO": + parameters[mass_name] = (float(particle.mass), 0.0) + if width_name != "ZERO": + parameters[width_name] = (float(particle.width), 0.0) + for pdg in dict.fromkeys((particle.pdg, particle.anti_pdg)): + is_antiparticle = pdg != particle.pdg + anti_pdg = particle.pdg if is_antiparticle else particle.anti_pdg + color = particle.color_rep + if is_antiparticle and abs(color) in {3, 6}: + color = -color + particles.append( + { + "pdg_code": pdg, + "name": _particle_name(pdg), + "antiname": _particle_name(anti_pdg), + "spin": _ufo_spin(pdg, particle.spin), + "color": color, + "mass": mass_name, + "width": width_name, + "texname": _particle_name(pdg), + "antitexname": _particle_name(anti_pdg), + "charge": -particle.charge if is_antiparticle else particle.charge, + "quantum_numbers": [ + list(item) for item in model.quantum_number_flow(pdg) + ], + "ghost_number": 0, + "lepton_number": 0, + "y_charge": 0, + "propagating": particle.spin >= 0, + "goldstoneboson": False, + "propagator": None, + "component_dimension": particle.dimension, + "auxiliary_kind": model.auxiliary_kind(pdg), + } + ) + return ( + { + "name": "built-in-sm", + "restriction": None, + "orders": [ + {"name": "QCD", "expansion_order": 99, "hierarchy": 1}, + {"name": "QED", "expansion_order": 99, "hierarchy": 2}, + ], + "parameters": [ + { + "name": name, + "nature": "external", + "parameter_type": "real", + "value": [value[0], value[1]], + "expression": None, + "lhablock": "PYAMPLICOL", + "lhacode": [index], + } + for index, (name, value) in enumerate( + sorted(parameters.items()), + start=1, + ) + ], + "particles": particles, + "propagators": [], + "lorentz_structures": [], + "couplings": [], + "vertex_rules": [ + { + "name": f"builtin_vertex_{index}", + "particles": [_particle_name(pdg) for pdg in vertex.particles], + "color_structures": ["1"], + "lorentz_structures": [f"builtin_kind_{vertex.kind}"], + "couplings": [[f"builtin_coupling_{index}"]], + "builtin_kind": vertex.kind, + "builtin_coupling": list(vertex.coupling), + } + for index, vertex in enumerate(model.vertices) + ], + "functions": [], + "form_factors": [], + "direct_contractions": [ + { + "left_particle": _particle_name(left_particle), + "left_chirality": left_chirality, + "right_particle": _particle_name(right_particle), + "right_chirality": right_chirality, + "contraction_ir": contraction_ir.to_json_dict(), + } + for ( + left_particle, + left_chirality, + right_particle, + right_chirality, + ), contraction_ir in sorted( + model._direct_contraction_ir_by_state.items() + ) + if contraction_ir is not None + ], + "closure_contractions": [ + { + "particle": _particle_name(particle), + "chirality": chirality, + "contraction_ir": contraction_ir.to_json_dict(), + } + for (particle, chirality), contraction_ir in sorted( + model._closure_contraction_ir_by_state.items() + ) + if contraction_ir is not None + ], + "builtin_model": True, + }, + parameters, + ) + + +def source_digest() -> str: + """Hash all implementation files behind the built-in model boundary.""" + + digest = hashlib.sha256() + root = Path(__file__).resolve().parent + for path in sorted(root.glob("*.py")): + digest.update(path.name.encode("utf-8") + b"\0") + digest.update(path.read_bytes()) + digest.update(b"\0") + return digest.hexdigest() + + +def _parameter_name(field: str, pdg: int) -> str: + pdg_token = f"m{abs(int(pdg))}" if int(pdg) < 0 else str(int(pdg)) + return f"{field}_{pdg_token}" + + +def _particle_name(pdg: int) -> str: + candidates = [name for name, value in PDGS.items() if int(value) == pdg] + if candidates: + return min(candidates, key=lambda name: (len(name), name)) + return f"pdg_{pdg}" + + +def _ufo_spin(pdg: int, internal_spin: int) -> int: + """Translate the legacy kernel spin tag to the UFO 2S+1 convention.""" + + if internal_spin < 0: + return -1 + absolute_pdg = abs(pdg) + if 1 <= absolute_pdg <= 6 or 11 <= absolute_pdg <= 16: + return 2 + if absolute_pdg in {21, 22, 23, 24}: + return 3 + if absolute_pdg == 25: + return 1 + raise ValueError( + f"cannot map built-in particle {pdg} with spin tag {internal_spin} to UFO spin" + ) + + +__all__ = ["build_model_payload", "source_digest"] diff --git a/pyamplicol/src/pyamplicol/models/builtin/compiler.py b/pyamplicol/src/pyamplicol/models/builtin/compiler.py new file mode 100644 index 0000000000..cdeab56efd --- /dev/null +++ b/pyamplicol/src/pyamplicol/models/builtin/compiler.py @@ -0,0 +1,77 @@ +# SPDX-License-Identifier: 0BSD +"""Canonical IR compilation for the hand-written built-in SM.""" + +from __future__ import annotations + +from collections.abc import Mapping + +from .. import compiler_symbolica as _sym +from ..compiler_gauge import compile_goldstone_partner_records +from ..compiler_records import ( + _mappings, + _order, + _parameter, + _particle_record, + _sequence, +) +from ..contracts import ( + CompiledClosureContractionRecord, + CompiledDirectContractionRecord, + CompiledModelIR, + CompiledVertexTerm, +) + + +def compile_model_ir(model: Mapping[str, object]) -> CompiledModelIR: + _sym._ensure_symbolica() + particles = tuple( + _particle_record(item) for item in _mappings(model.get("particles")) + ) + terms = tuple( + CompiledVertexTerm( + id=index, + vertex=str(vertex["name"]), + particles=tuple(str(value) for value in _sequence(vertex["particles"])), + color_index=0, + lorentz_index=0, + color_source="built-in", + color_expression="built-in", + lorentz_name=str(vertex["builtin_kind"]), + lorentz_source="built-in", + lorentz_expression="built-in", + coupling=f"builtin_coupling_{index}", + coupling_expression=str(vertex.get("builtin_coupling", [1.0, 0.0])), + coupling_orders=(), + backend="built-in", + ) + for index, vertex in enumerate(_mappings(model.get("vertex_rules"))) + ) + parameters = tuple( + _parameter(item) for item in _mappings(model.get("parameters")) + ) + return CompiledModelIR( + name=str(model.get("name", "built-in-sm")), + orders=tuple(_order(item) for item in _mappings(model.get("orders"))), + parameters=parameters, + particles=particles, + couplings=(), + propagators=(), + vertex_terms=terms, + oriented_kernels=(), + direct_contractions=tuple( + CompiledDirectContractionRecord.from_dict(item) + for item in _mappings(model.get("direct_contractions")) + ), + closure_contractions=tuple( + CompiledClosureContractionRecord.from_dict(item) + for item in _mappings(model.get("closure_contractions")) + ), + goldstone_partners=compile_goldstone_partner_records( + particles, + parameters, + (), + ), + ) + + +__all__ = ["compile_model_ir"] diff --git a/pyamplicol/src/pyamplicol/models/builtin/definitions.py b/pyamplicol/src/pyamplicol/models/builtin/definitions.py new file mode 100644 index 0000000000..5263b13a9c --- /dev/null +++ b/pyamplicol/src/pyamplicol/models/builtin/definitions.py @@ -0,0 +1,341 @@ +# SPDX-License-Identifier: 0BSD +from __future__ import annotations + +from ..base import ( + Particle, + Vertex, +) + + +class BuiltinSMDefinitionMixin: + def _build_particles(self) -> list[Particle]: + particles: list[Particle] = [] + for i in range(1, 6): + if i % 2 == 0: + particles.append( + Particle( + i, + -i, + 2, + 4, + 3, + charge=2.0 / 3.0, + weak_isospin=(0.5, 0.0), + weak_hypercharge=(1.0 / 3.0, 4.0 / 3.0), + ) + ) + else: + particles.append( + Particle( + i, + -i, + 2, + 4, + 3, + charge=-1.0 / 3.0, + weak_isospin=(-0.5, 0.0), + weak_hypercharge=(1.0 / 3.0, -2.0 / 3.0), + ) + ) + particles.extend( + [ + Particle( + 6, + -6, + 2, + 4, + 3, + mass=173.0, + width=1.491500, + charge=2.0 / 3.0, + weak_isospin=(0.5, 0.0), + weak_hypercharge=(1.0 / 3.0, 4.0 / 3.0), + ), + Particle(21, 21, 2, 4, 8), + Particle(99, 99, -1, 4, 1), + Particle(-21, -21, -1, 6, 8), + Particle(22, 22, 2, 4, 1), + Particle(23, 23, 3, 4, 1, mass=91.188, width=2.441404), + Particle(-23, -23, -1, 6, 1), + Particle( + 24, + -24, + 3, + 4, + 1, + mass=80.419002445756163, + width=2.0476, + charge=1.0, + weak_isospin=(1.0, 1.0), + ), + Particle( + 25, + 25, + 1, + 1, + 1, + mass=125.0, + width=0.0063823389999999999, + weak_isospin=(-0.5, -0.5), + weak_hypercharge=(1.0, 1.0), + ), + Particle(125, 125, -1, 1, 1), + Particle(126, 126, -1, 1, 1), + Particle(127, 127, -1, 1, 1), + Particle(26, -26, -1, 6, 1, charge=1.0, weak_isospin=(1.0, 1.0)), + ] + ) + for i in range(1, 4): + charged = 11 + (2 * i - 2) + neutrino = 12 + (2 * i - 2) + particles.append( + Particle( + charged, + -charged, + 2, + 4, + 1, + charge=-1.0, + weak_isospin=(-0.5, 0.0), + weak_hypercharge=(-1.0, -2.0), + ) + ) + particles.append( + Particle( + neutrino, + -neutrino, + 2, + 4, + 1, + weak_isospin=(0.5, 0.0), + weak_hypercharge=(-1.0, 0.0), + ) + ) + return particles + + def _build_vertices(self) -> list[Vertex]: + vertices: list[Vertex] = [ + Vertex(0, (21, 21, 21)), + Vertex(1, (21, 21, -21)), + Vertex(2, (-21, 21, 21)), + Vertex(3, (21, -21, 21)), + ] + self._extend_quark_gluon_vertices(vertices) + self._extend_electroweak_gauge_vertices(vertices) + self._extend_higgs_vertices(vertices) + self._extend_lepton_vertices(vertices) + return vertices + + def _extend_quark_gluon_vertices(self, vertices: list[Vertex]) -> None: + for i in range(1, 7): + vertices.extend( + [ + Vertex(4, (21, i, i)), + Vertex(5, (21, -i, -i)), + Vertex(6, (i, 21, i)), + Vertex(7, (-i, 21, -i)), + Vertex(9, (-i, i, 21)), + Vertex(8, (i, -i, 99), (1.0 / 3.0, 0.0)), + Vertex(4, (99, i, i)), + Vertex(5, (99, -i, -i)), + Vertex(6, (i, 99, i)), + Vertex(7, (-i, 99, -i)), + Vertex(10, (i, 22, i), self.photon_fermion_coupling(i)), + Vertex(11, (-i, 22, -i), self.photon_fermion_coupling(-i)), + Vertex(10, (i, 23, i), self.z_fermion_coupling(i)), + Vertex(11, (-i, 23, -i), self.z_fermion_coupling(-i)), + ] + ) + if i % 2 == 0: + vertices.append( + Vertex(10, (i, -24, i - 1), (self.charged_current_coupling(), 0.0)) + ) + vertices.append( + Vertex(11, (-i, 24, -i + 1), (self.charged_current_coupling(), 0.0)) + ) + else: + vertices.append( + Vertex(10, (i, 24, i + 1), (self.charged_current_coupling(), 0.0)) + ) + vertices.append( + Vertex( + 11, (-i, -24, -i - 1), (self.charged_current_coupling(), 0.0) + ) + ) + + def _extend_electroweak_gauge_vertices(self, vertices: list[Vertex]) -> None: + ngc = self.neutral_gauge_coupling() + wc = self.weak_coupling() + vertices.extend( + [ + Vertex(12, (24, -24, 23), (-ngc, 0.0)), + Vertex(12, (-24, 24, 23), (ngc, 0.0)), + Vertex(12, (24, -24, 22), (-self.charge(24), 0.0)), + Vertex(12, (-24, 24, 22), (self.charge(24), 0.0)), + Vertex(12, (24, 22, 24), (self.charge(24), 0.0)), + Vertex(12, (-24, 22, -24), (self.charge(-24), 0.0)), + Vertex(12, (22, 24, 24), (-self.charge(24), 0.0)), + Vertex(12, (22, -24, -24), (-self.charge(-24), 0.0)), + Vertex(12, (24, 23, 24), (ngc, 0.0)), + Vertex(12, (-24, 23, -24), (-ngc, 0.0)), + Vertex(12, (23, 24, 24), (-ngc, 0.0)), + Vertex(12, (23, -24, -24), (ngc, 0.0)), + Vertex(13, (24, -24, -23), (wc, 0.0)), + Vertex(13, (-24, 24, -23), (-wc, 0.0)), + Vertex(14, (-23, 24, 24), (wc, 0.0)), + Vertex(14, (-23, -24, -24), (-wc, 0.0)), + Vertex(15, (24, -23, 24), (-wc, 0.0)), + Vertex(15, (-24, -23, -24), (wc, 0.0)), + Vertex(13, (24, 22, 26), (self.charge(24), 0.0)), + Vertex(13, (-24, 22, -26), (-self.charge(-24), 0.0)), + Vertex(13, (22, 24, 26), (-self.charge(24), 0.0)), + Vertex(13, (22, -24, -26), (self.charge(-24), 0.0)), + Vertex(13, (24, 23, 26), (ngc, 0.0)), + Vertex(13, (-24, 23, -26), (ngc, 0.0)), + Vertex(13, (23, 24, 26), (-ngc, 0.0)), + Vertex(13, (23, -24, -26), (-ngc, 0.0)), + Vertex(14, (26, 22, 24), (self.charge(26), 0.0)), + Vertex(14, (-26, 22, -24), (-self.charge(-26), 0.0)), + Vertex(14, (26, -24, 22), (-self.charge(26), 0.0)), + Vertex(14, (-26, 24, 22), (self.charge(-26), 0.0)), + Vertex(14, (26, 23, 24), (ngc, 0.0)), + Vertex(14, (-26, 23, -24), (ngc, 0.0)), + Vertex(14, (26, -24, 23), (-ngc, 0.0)), + Vertex(14, (-26, 24, 23), (-ngc, 0.0)), + Vertex(15, (22, 26, 24), (-self.charge(26), 0.0)), + Vertex(15, (22, -26, -24), (self.charge(-26), 0.0)), + Vertex(15, (24, -26, 22), (self.charge(24), 0.0)), + Vertex(15, (-24, 26, 22), (-self.charge(-24), 0.0)), + Vertex(15, (23, 26, 24), (-ngc, 0.0)), + Vertex(15, (23, -26, -24), (-ngc, 0.0)), + Vertex(15, (24, -26, 23), (ngc, 0.0)), + Vertex(15, (-24, 26, 23), (ngc, 0.0)), + ] + ) + + def _extend_higgs_vertices(self, vertices: list[Vertex]) -> None: + wc = self.weak_coupling() + wc2 = wc**2 + cw2 = self.cos_weak**2 + for i in range(1, 7): + if self.mass(i) == 0.0: + continue + coupling = self.mass(i) * wc / (self.mass(24) * 2.0) + vertices.append(Vertex(16, (i, 25, i), (coupling, 0.0))) + vertices.append(Vertex(16, (-i, 25, -i), (coupling, 0.0))) + vertices.extend( + [ + Vertex(17, (24, -24, 25), (self.mass(24) * wc, 0.0)), + Vertex(17, (-24, 24, 25), (self.mass(24) * wc, 0.0)), + Vertex(18, (25, 24, 24), (self.mass(24) * wc, 0.0)), + Vertex(18, (25, -24, -24), (self.mass(24) * wc, 0.0)), + Vertex(19, (24, 25, 24), (self.mass(24) * wc, 0.0)), + Vertex(19, (-24, 25, -24), (self.mass(24) * wc, 0.0)), + Vertex( + 17, + (23, 23, 25), + (self.mass(23) * self.weak_coupling_over_cosine(), 0.0), + ), + Vertex( + 18, + (25, 23, 23), + (self.mass(23) * self.weak_coupling_over_cosine(), 0.0), + ), + Vertex( + 19, + (23, 25, 23), + (self.mass(23) * self.weak_coupling_over_cosine(), 0.0), + ), + Vertex( + 20, + (25, 25, 25), + ((-3.0 / 2.0) * wc * (self.mass(25) ** 2 / self.mass(24)), 0.0), + ), + Vertex(17, (24, -24, 127), (0.5 * wc2, 0.0)), + Vertex(17, (-24, 24, 127), (0.5 * wc2, 0.0)), + Vertex(17, (23, 23, 127), (0.5 * wc2 / cw2, 0.0)), + Vertex(20, (125, 25, 25), (1.0, 0.0)), + Vertex(20, (25, 125, 25), (1.0, 0.0)), + Vertex( + 20, + (25, 25, 125), + ((-3.0 / 4.0) * wc2 * self.mass(25) ** 2 / self.mass(24) ** 2, 0.0), + ), + Vertex(20, (127, 25, 25), (1.0, 0.0)), + Vertex(20, (25, 127, 25), (1.0, 0.0)), + Vertex(20, (25, 25, 126), (1.0, -10.0)), + Vertex(18, (126, 23, 23), (-0.5 * wc2 / cw2, 0.0)), + Vertex(19, (23, 126, 23), (-0.5 * wc2 / cw2, 0.0)), + Vertex(18, (126, 24, 24), (-0.5 * wc2, 0.0)), + Vertex(18, (126, -24, -24), (-0.5 * wc2, 0.0)), + Vertex(19, (24, 126, 24), (-0.5 * wc2, 0.0)), + Vertex(19, (-24, 126, -24), (-0.5 * wc2, 0.0)), + ] + ) + + def _extend_lepton_vertices(self, vertices: list[Vertex]) -> None: + for i in range(1, 4): + charged = 11 + (2 * i - 2) + neutrino = 12 + (2 * i - 2) + vertices.extend( + [ + Vertex( + 21, + (charged, -charged, 22), + self.photon_fermion_coupling(charged), + ), + Vertex( + 22, + (-charged, charged, 22), + self.photon_fermion_coupling(-charged), + ), + Vertex( + 21, (charged, -charged, 23), self.z_fermion_coupling(charged) + ), + Vertex( + 22, (-charged, charged, 23), self.z_fermion_coupling(-charged) + ), + Vertex( + 21, + (charged, -neutrino, -24), + (self.charged_current_coupling(), 0.0), + ), + Vertex( + 22, + (-charged, neutrino, 24), + (self.charged_current_coupling(), 0.0), + ), + Vertex( + 10, + (charged, 22, charged), + self.photon_fermion_coupling(charged), + ), + Vertex( + 11, + (-charged, 22, -charged), + self.photon_fermion_coupling(-charged), + ), + Vertex( + 23, + (22, charged, charged), + self.photon_fermion_coupling(charged), + ), + Vertex( + 24, + (22, -charged, -charged), + self.photon_fermion_coupling(-charged), + ), + Vertex( + 10, (charged, 23, charged), self.z_fermion_coupling(charged) + ), + Vertex( + 11, (-charged, 23, -charged), self.z_fermion_coupling(-charged) + ), + Vertex( + 23, (23, charged, charged), self.z_fermion_coupling(charged) + ), + Vertex( + 24, (23, -charged, -charged), self.z_fermion_coupling(-charged) + ), + ] + ) diff --git a/pyamplicol/src/pyamplicol/models/builtin/expressions.py b/pyamplicol/src/pyamplicol/models/builtin/expressions.py new file mode 100644 index 0000000000..9657eb70be --- /dev/null +++ b/pyamplicol/src/pyamplicol/models/builtin/expressions.py @@ -0,0 +1,503 @@ +# SPDX-License-Identifier: 0BSD +from __future__ import annotations + +import math +from functools import cache +from numbers import Number +from typing import Any + +from ..base import Model, Vertex +from ..expressions import ( + _expr_minkowski_dot, + _index_particle_id, +) + + +@cache +def _exact_imaginary_inverse_sqrt_two() -> Any: + from symbolica import E + + return E("1i/sqrt(2)") + + +def _imaginary_inverse_sqrt_two(*values: Any) -> Any: + """Keep symbolic vertices exact without changing direct numeric evaluation.""" + + if all(isinstance(value, Number) for value in values): + return 1j / math.sqrt(2.0) + return _exact_imaginary_inverse_sqrt_two() + + +def _flat_index(indices: tuple[int, ...], dims: tuple[int, ...]) -> int: + index = 0 + for value, dim in zip(indices, dims, strict=True): + index = index * dim + value + return index + + +def _index_chirality(index: Any) -> int: + return int(getattr(index, "chirality", 0)) + + +def _expr_vector_slash_terms( + vector: tuple[Any, ...], +) -> tuple[Any, Any, Any, Any]: + v0, v1, v2, v3 = vector + return v0 + v3, v0 - v3, v1 + 1j * v2, v1 - 1j * v2 + + +def _builtin_vertex_result_chiralities( + model: Model, + vertex: Vertex, + left_index: Any, + right_index: Any, +) -> tuple[int, ...]: + result_pdg = vertex.particles[2] + if model.is_chiral_eligible(result_pdg): + input_chirality = _model_fermion_input_chirality(model, left_index, right_index) + if input_chirality == 0: + return (0,) + if not _model_weyl_vertex_allowed(vertex, result_pdg, input_chirality): + return () + return (input_chirality,) + + if _model_is_fermion_pair_to_vector_vertex(vertex.kind): + left_pdg = _index_particle_id(left_index) + right_pdg = _index_particle_id(right_index) + left_chirality = ( + _index_chirality(left_index) if model.is_fermion(left_pdg) else 0 + ) + right_chirality = ( + _index_chirality(right_index) if model.is_fermion(right_pdg) else 0 + ) + if ( + left_chirality != 0 + and right_chirality != 0 + and left_chirality != -right_chirality + ): + return () + if not _model_fermion_pair_vector_coupling_allowed( + vertex, + left_chirality, + right_chirality, + ): + return () + return (0,) + + +def _model_fermion_input_chirality( + model: Model, + left_index: Any, + right_index: Any, +) -> int: + left_pdg = _index_particle_id(left_index) + right_pdg = _index_particle_id(right_index) + left_chirality = _index_chirality(left_index) + right_chirality = _index_chirality(right_index) + if model.is_fermion(left_pdg) and left_chirality != 0: + return left_chirality + if model.is_fermion(right_pdg) and right_chirality != 0: + return right_chirality + return 0 + + +def _model_weyl_vertex_allowed( + vertex: Vertex, + result_pdg: int, + chirality: int, +) -> bool: + if vertex.kind == 16: + return False + if vertex.kind in {10, 11, 23, 24}: + index = _model_fermion_coupling_index(result_pdg, chirality) + return vertex.coupling[index] != 0.0 + return True + + +def _model_fermion_coupling_index(pdg: int, chirality: int) -> int: + if pdg > 0: + return 0 if chirality == -1 else 1 + return 0 if chirality == 1 else 1 + + +def _model_is_fermion_pair_to_vector_vertex(kind: int) -> bool: + return kind in {8, 9, 21, 22} + + +def _model_fermion_pair_vector_coupling_allowed( + vertex: Vertex, + left_chirality: int, + right_chirality: int, +) -> bool: + if vertex.kind in {8, 9}: + return True + if left_chirality == 0 or right_chirality == 0: + return any(component != 0.0 for component in vertex.coupling) + if vertex.kind == 21: + index = 0 if left_chirality == -1 and right_chirality == 1 else 1 + return vertex.coupling[index] != 0.0 + if vertex.kind == 22: + index = 0 if left_chirality == 1 and right_chirality == -1 else 1 + return vertex.coupling[index] != 0.0 + return True + + +def _expr_three_vector_current( + left: tuple[Any, ...], + left_momentum: tuple[Any, ...], + right: tuple[Any, ...], + right_momentum: tuple[Any, ...], +) -> tuple[Any, ...]: + dot = _expr_minkowski_dot(left, right) + left_dot_right_momentum = _expr_minkowski_dot(left, right_momentum) + right_dot_left_momentum = _expr_minkowski_dot(right, left_momentum) + prefactor = _imaginary_inverse_sqrt_two( + *left, *left_momentum, *right, *right_momentum + ) + return tuple( + prefactor + * ( + dot * (left_momentum[index] - right_momentum[index]) + + 2.0 + * ( + left_dot_right_momentum * right[index] + - right_dot_left_momentum * left[index] + ) + ) + for index in range(4) + ) + + +def _expr_three_vector_current_coupled( + left: tuple[Any, ...], + left_momentum: tuple[Any, ...], + right: tuple[Any, ...], + right_momentum: tuple[Any, ...], + coupling: tuple[Any, Any], +) -> tuple[Any, ...]: + dot = _expr_minkowski_dot(left, right) + tmp2_momentum = tuple( + 2.0 * right_momentum[index] + left_momentum[index] for index in range(4) + ) + tmp3_momentum = tuple( + -2.0 * left_momentum[index] - right_momentum[index] for index in range(4) + ) + tmp2 = _expr_minkowski_dot(left, tmp2_momentum) + tmp3 = _expr_minkowski_dot(right, tmp3_momentum) + prefactor = ( + _imaginary_inverse_sqrt_two( + *left, *left_momentum, *right, *right_momentum, *coupling + ) + * coupling[0] + ) + return tuple( + prefactor + * ( + dot * (left_momentum[index] - right_momentum[index]) + + tmp2 * right[index] + + tmp3 * left[index] + ) + for index in range(4) + ) + + +def _expr_two_vector_to_tensor( + left: tuple[Any, ...], + right: tuple[Any, ...], +) -> tuple[Any, ...]: + return ( + left[0] * right[1] - left[1] * right[0], + left[0] * right[2] - left[2] * right[0], + left[0] * right[3] - left[3] * right[0], + left[1] * right[2] - left[2] * right[1], + left[1] * right[3] - left[3] * right[1], + left[2] * right[3] - left[3] * right[2], + ) + + +def _expr_tensor_vector_to_vector( + tensor: tuple[Any, ...], + vector: tuple[Any, ...], +) -> tuple[Any, ...]: + prefactor = 0.5j + return ( + (tensor[0] * vector[1] + tensor[1] * vector[2] + tensor[2] * vector[3]) + * prefactor, + (tensor[0] * vector[0] + tensor[3] * vector[2] + tensor[4] * vector[3]) + * prefactor, + (tensor[1] * vector[0] - tensor[3] * vector[1] + tensor[5] * vector[3]) + * prefactor, + (tensor[2] * vector[0] - tensor[4] * vector[1] - tensor[5] * vector[2]) + * prefactor, + ) + + +def _expr_vector_tensor_to_vector( + vector: tuple[Any, ...], + tensor: tuple[Any, ...], +) -> tuple[Any, ...]: + prefactor = 0.5j + return ( + (-vector[1] * tensor[0] - vector[2] * tensor[1] - vector[3] * tensor[2]) + * prefactor, + (-vector[0] * tensor[0] - vector[2] * tensor[3] - vector[3] * tensor[4]) + * prefactor, + (-vector[0] * tensor[1] + vector[1] * tensor[3] - vector[3] * tensor[5]) + * prefactor, + (-vector[0] * tensor[2] + vector[1] * tensor[4] + vector[2] * tensor[5]) + * prefactor, + ) + + +def _expr_fermion_vector_weyl( + fermion: tuple[Any, ...], + vector: tuple[Any, ...], + chirality: int, + *, + antifermion: bool, + coupling: tuple[Any, Any] | None, +) -> tuple[Any, ...]: + tmp1, tmp2, tmp3, tmp4 = _expr_vector_slash_terms(vector) + prefactor = _imaginary_inverse_sqrt_two(*fermion, *vector, *(coupling or ())) + f1, f2 = fermion + if antifermion: + if chirality == 1: + factor = prefactor if coupling is None else prefactor * coupling[0] + return ( + factor * (tmp1 * f1 + tmp4 * f2), + factor * (tmp2 * f2 + tmp3 * f1), + ) + if chirality == -1: + factor = prefactor if coupling is None else prefactor * coupling[1] + return ( + factor * (tmp2 * f1 - tmp4 * f2), + factor * (tmp1 * f2 - tmp3 * f1), + ) + else: + if chirality == 1: + factor = prefactor if coupling is None else prefactor * coupling[1] + return ( + factor * (tmp2 * f1 - tmp3 * f2), + factor * (tmp1 * f2 - tmp4 * f1), + ) + if chirality == -1: + factor = prefactor if coupling is None else prefactor * coupling[0] + return ( + factor * (tmp1 * f1 + tmp3 * f2), + factor * (tmp2 * f2 + tmp4 * f1), + ) + raise ValueError("fermion-vector Weyl kernel needs nonzero chirality") + + +def _embed_weyl_current_in_dirac( + current: tuple[Any, ...], + chirality: int, +) -> tuple[Any, ...]: + """Embed a massless chiral current in the built-in Dirac basis.""" + + if len(current) != 2: + raise ValueError("Weyl-to-Dirac embedding expects two components") + zero = current[0] * 0 + if chirality == -1: + return (*current, zero, zero) + if chirality == 1: + return (zero, zero, *current) + raise ValueError("Weyl-to-Dirac embedding needs nonzero chirality") + + +def _expr_fermion_vector_dirac( + fermion: tuple[Any, ...], + vector: tuple[Any, ...], + *, + antifermion: bool, + coupling: tuple[Any, Any] | None, +) -> tuple[Any, ...]: + if len(fermion) != 4 or len(vector) != 4: + raise ValueError("Dirac fermion-vector current expects dimensions 4 and 4") + tmp1, tmp2, tmp3, tmp4 = _expr_vector_slash_terms(vector) + prefactor = _imaginary_inverse_sqrt_two(*fermion, *vector, *(coupling or ())) + f1, f2, f3, f4 = fermion + if coupling is None: + left_coupling = 1.0 + right_coupling = 1.0 + else: + left_coupling, right_coupling = coupling + if antifermion: + upper = prefactor * right_coupling + lower = prefactor * left_coupling + return ( + upper * (tmp2 * f3 - tmp4 * f4), + upper * (tmp1 * f4 - tmp3 * f3), + lower * (tmp1 * f1 + tmp4 * f2), + lower * (tmp2 * f2 + tmp3 * f1), + ) + upper = prefactor * left_coupling + lower = prefactor * right_coupling + return ( + upper * (tmp1 * f3 + tmp3 * f4), + upper * (tmp2 * f4 + tmp4 * f3), + lower * (tmp2 * f1 - tmp3 * f2), + lower * (tmp1 * f2 - tmp4 * f1), + ) + + +def _expr_fermion_antifermion_to_vector_weyl( + *, + fermion: tuple[Any, ...], + antifermion: tuple[Any, ...], + coupling: tuple[Any, Any], + fermion_chirality: int, + antifermion_chirality: int, +) -> tuple[Any, ...]: + prefactor = _imaginary_inverse_sqrt_two(*fermion, *antifermion, *coupling) + left, right = coupling + f1, f2 = fermion + a1, a2 = antifermion + if fermion_chirality == -1 and antifermion_chirality == 1: + factor = prefactor * left + return ( + factor * (f1 * a1 + f2 * a2), + -factor * (f2 * a1 + f1 * a2), + 1j * factor * (-f2 * a1 + f1 * a2), + factor * (-f1 * a1 + f2 * a2), + ) + if fermion_chirality == 1 and antifermion_chirality == -1: + factor = prefactor * right + return ( + factor * (f1 * a1 + f2 * a2), + factor * (f1 * a2 + f2 * a1), + 1j * factor * (-f1 * a2 + f2 * a1), + factor * (f1 * a1 - f2 * a2), + ) + return (0j, 0j, 0j, 0j) + + +def _expr_fermion_antifermion_to_vector_dirac( + *, + fermion: tuple[Any, ...], + antifermion: tuple[Any, ...], + coupling: tuple[Any, Any], +) -> tuple[Any, ...]: + if len(fermion) != 4 or len(antifermion) != 4: + raise ValueError( + "Dirac fermion-antifermion vector current expects dimensions 4 and 4" + ) + prefactor = _imaginary_inverse_sqrt_two(*fermion, *antifermion, *coupling) + left_coupling, right_coupling = coupling + f1, f2, f3, f4 = fermion + a1, a2, a3, a4 = antifermion + left = ( + f3 * a1 + f4 * a2, + -(f4 * a1 + f3 * a2), + 1j * (-f4 * a1 + f3 * a2), + -f3 * a1 + f4 * a2, + ) + right = ( + f1 * a3 + f2 * a4, + f1 * a4 + f2 * a3, + 1j * (-f1 * a4 + f2 * a3), + f1 * a3 - f2 * a4, + ) + return tuple( + prefactor * (left_coupling * left[index] + right_coupling * right[index]) + for index in range(4) + ) + + +def _expr_fermion_scalar_to_fermion( + fermion: tuple[Any, ...], + scalar: tuple[Any, ...], + coupling: tuple[Any, Any], +) -> tuple[Any, ...]: + if len(fermion) != 4 or len(scalar) != 1: + raise ValueError("Dirac fermion-scalar current expects dimensions 4 and 1") + prefactor = -_imaginary_inverse_sqrt_two(*fermion, *scalar, *coupling) + return tuple( + prefactor * coupling[0] * scalar[0] * component for component in fermion + ) + + +def _two_gluon_to_tensor_data() -> list[complex]: + data = [0j] * (6 * 4 * 4) + metric = (1.0, -1.0, -1.0, -1.0) + for tensor_index, (i, j) in enumerate(_ANTISYM_PAIRS): + data[_flat_index((i, j, tensor_index), (4, 4, 6))] = ( + 1.0 / (metric[i] * metric[j]) + ) + 0j + data[_flat_index((j, i, tensor_index), (4, 4, 6))] = ( + -1.0 / (metric[j] * metric[i]) + ) + 0j + return data + + +def _tensor_gluon_to_gluon_data() -> list[complex]: + data = [0j] * (6 * 4 * 4) + prefactor = 0.5j + metric = (1.0, -1.0, -1.0, -1.0) + rows = { + 0: ((0, 1, 1), (1, 2, 1), (2, 3, 1)), + 1: ((0, 0, 1), (3, 2, 1), (4, 3, 1)), + 2: ((1, 0, 1), (3, 1, -1), (5, 3, 1)), + 3: ((2, 0, 1), (4, 1, -1), (5, 2, -1)), + } + for out, entries in rows.items(): + for tensor_index, gluon_index, sign in entries: + data[_flat_index((tensor_index, gluon_index, out), (6, 4, 4))] = ( + sign * prefactor / metric[gluon_index] + ) + return data + + +def _gluon_tensor_to_gluon_data() -> list[complex]: + data = [0j] * (6 * 4 * 4) + prefactor = 0.5j + metric = (1.0, -1.0, -1.0, -1.0) + rows = { + 0: ((1, 0, -1), (2, 1, -1), (3, 2, -1)), + 1: ((0, 0, -1), (2, 3, -1), (3, 4, -1)), + 2: ((0, 1, -1), (1, 3, 1), (3, 5, -1)), + 3: ((0, 2, -1), (1, 4, 1), (2, 5, 1)), + } + for out, entries in rows.items(): + for gluon_index, tensor_index, sign in entries: + data[_flat_index((gluon_index, tensor_index, out), (4, 6, 4))] = ( + sign * prefactor / metric[gluon_index] + ) + return data + + +def _quark_vector_weyl_data(*, chirality: int) -> list[Any]: + prefactor = _exact_imaginary_inverse_sqrt_two() + data: list[Any] = [prefactor * 0] * (2 * 4 * 2) + metric = (1.0, -1.0, -1.0, -1.0) + + def add(q_in: int, vector: int, q_out: int, coefficient: Any) -> None: + # Tensor.dense accepts logical interface order, not canonical storage order. + data[_flat_index((q_in, vector, q_out), (2, 4, 2))] = ( + coefficient / metric[vector] + ) + + if chirality == 1: + add(0, 0, 0, prefactor) + add(0, 3, 0, -prefactor) + add(1, 1, 0, -prefactor) + add(1, 2, 0, -1j * prefactor) + add(1, 0, 1, prefactor) + add(1, 3, 1, prefactor) + add(0, 1, 1, -prefactor) + add(0, 2, 1, 1j * prefactor) + return data + if chirality == -1: + add(0, 0, 0, prefactor) + add(0, 3, 0, prefactor) + add(1, 1, 0, prefactor) + add(1, 2, 0, 1j * prefactor) + add(1, 0, 1, prefactor) + add(1, 3, 1, -prefactor) + add(0, 1, 1, prefactor) + add(0, 2, 1, -1j * prefactor) + return data + raise ValueError(f"unsupported Weyl chirality: {chirality}") + + +_ANTISYM_PAIRS = ((0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)) diff --git a/pyamplicol/src/pyamplicol/models/builtin/lowering.py b/pyamplicol/src/pyamplicol/models/builtin/lowering.py new file mode 100644 index 0000000000..ec0e68ed83 --- /dev/null +++ b/pyamplicol/src/pyamplicol/models/builtin/lowering.py @@ -0,0 +1,876 @@ +# SPDX-License-Identifier: 0BSD +from __future__ import annotations + +from collections.abc import Sequence +from typing import Any + +from .._physics_ir import PropagatorIR +from ..base import ( + VertexEvaluationEquivalence, + VertexLoweringRule, +) +from ..expressions import ( + _as_expression, + _expr_antifermion_propagator_dirac, + _expr_antifermion_propagator_weyl, + _expr_fermion_propagator_dirac, + _expr_fermion_propagator_weyl, + _expr_minkowski_dot, + _minkowski_square_expression, + _number, +) +from .expressions import ( + _embed_weyl_current_in_dirac, + _exact_imaginary_inverse_sqrt_two, + _expr_fermion_antifermion_to_vector_dirac, + _expr_fermion_antifermion_to_vector_weyl, + _expr_fermion_scalar_to_fermion, + _expr_fermion_vector_dirac, + _expr_fermion_vector_weyl, + _expr_tensor_vector_to_vector, + _expr_three_vector_current, + _expr_three_vector_current_coupled, + _expr_two_vector_to_tensor, + _expr_vector_tensor_to_vector, + _flat_index, + _gluon_tensor_to_gluon_data, + _imaginary_inverse_sqrt_two, + _quark_vector_weyl_data, + _tensor_gluon_to_gluon_data, + _two_gluon_to_tensor_data, +) +from .symbols import symbols + + +def _as_dirac_current( + current: tuple[Any, ...], + chirality: int, +) -> tuple[Any, ...]: + if len(current) == 4: + return current + return _embed_weyl_current_in_dirac(current, chirality) + + +class BuiltinSMLoweringMixin: + def build_tensor_library(self) -> Any: + from symbolica.community.tensor import ( + Representation, + Tensor, + TensorLibrary, + TensorName, + ) + + library = TensorLibrary.hep_lib_atom() + mink = Representation.mink(4) + antisym = Representation(symbols.antisymmetric_lorentz_pair_name, 6) + weyl = Representation(symbols.weyl_spinor_name, 2) + two_gluon_to_tensor = TensorName(symbols.display_name("two_gluon_to_tensor")) + tensor_gluon_to_gluon = TensorName( + symbols.display_name("tensor_gluon_to_gluon") + ) + gluon_tensor_to_gluon = TensorName( + symbols.display_name("gluon_tensor_to_gluon") + ) + quark_vector_weyl_plus = TensorName( + symbols.display_name("quark_vector_weyl_plus") + ) + quark_vector_weyl_minus = TensorName( + symbols.display_name("quark_vector_weyl_minus") + ) + + library.register( + Tensor.dense( + two_gluon_to_tensor(mink, mink, antisym), + _two_gluon_to_tensor_data(), + ) + ) + library.register( + Tensor.dense( + tensor_gluon_to_gluon(antisym, mink, mink), + _tensor_gluon_to_gluon_data(), + ) + ) + library.register( + Tensor.dense( + gluon_tensor_to_gluon(mink, antisym, mink), + _gluon_tensor_to_gluon_data(), + ) + ) + library.register( + Tensor.dense( + quark_vector_weyl_plus(weyl, mink, weyl), + _quark_vector_weyl_data(chirality=1), + ) + ) + library.register( + Tensor.dense( + quark_vector_weyl_minus(weyl, mink, weyl), + _quark_vector_weyl_data(chirality=-1), + ) + ) + return library + + def vertex_lowering_rule(self, kind: int) -> VertexLoweringRule: + if kind == 0: + return VertexLoweringRule( + kind=kind, + backend="spenso", + tensor_names=("g", symbols.display_name("current_momentum")), + expression_head="three_gluon_current", + full_tensor_network_ready=True, + description="color-ordered three-gluon current", + kernel="three_vector_current", + input_roles=("vector", "vector"), + output_role="vector", + coupling_mode="fixed", + ) + if kind == 1: + return VertexLoweringRule( + kind=kind, + backend="spenso", + tensor_names=(symbols.display_name("two_gluon_to_tensor"),), + expression_head=symbols.display_name("two_gluon_to_tensor"), + full_tensor_network_ready=True, + description="two gluons to auxiliary antisymmetric tensor", + kernel="two_vector_to_tensor", + input_roles=("vector", "vector"), + output_role="antisymmetric_tensor", + coupling_mode="fixed", + ) + if kind == 2: + return VertexLoweringRule( + kind=kind, + backend="spenso", + tensor_names=(symbols.display_name("tensor_gluon_to_gluon"),), + expression_head=symbols.display_name("tensor_gluon_to_gluon"), + full_tensor_network_ready=True, + description="auxiliary tensor and gluon to gluon current", + kernel="tensor_vector_to_vector", + input_roles=("antisymmetric_tensor", "vector"), + output_role="vector", + coupling_mode="fixed", + ) + if kind == 3: + return VertexLoweringRule( + kind=kind, + backend="spenso", + tensor_names=(symbols.display_name("gluon_tensor_to_gluon"),), + expression_head=symbols.display_name("gluon_tensor_to_gluon"), + full_tensor_network_ready=True, + description="gluon and auxiliary tensor to gluon current", + kernel="vector_tensor_to_vector", + input_roles=("vector", "antisymmetric_tensor"), + output_role="vector", + coupling_mode="fixed", + ) + if kind == 6: + return VertexLoweringRule( + kind=kind, + backend="spenso", + tensor_names=( + symbols.display_name("quark_vector_weyl_minus"), + symbols.display_name("quark_vector_weyl_plus"), + ), + expression_head="quark_gluon_weyl_current", + full_tensor_network_ready=True, + description="Weyl quark-gluon current", + kernel="fermion_vector_to_fermion", + input_roles=("fermion", "vector"), + output_role="fermion", + coupling_mode="fixed", + ) + if kind in {4, 5, 7, 9}: + qcd_role_map: dict[int, tuple[tuple[str, str], str, str]] = { + 4: (("vector", "fermion"), "fermion", "vector-fermion current"), + 5: ( + ("vector", "antifermion"), + "antifermion", + "vector-antifermion current", + ), + 7: ( + ("antifermion", "vector"), + "antifermion", + "antifermion-vector current", + ), + 9: ( + ("antifermion", "fermion"), + "vector", + "antifermion-fermion vector current", + ), + } + input_roles, output_role, description = qcd_role_map[kind] + return VertexLoweringRule( + kind=kind, + backend="symbolica", + expression_head="quark_gluon_weyl_current", + full_tensor_network_ready=True, + description=f"Weyl QCD {description}", + kernel=( + "fermion_pair_to_vector" + if kind == 9 + else "fermion_vector_to_fermion" + ), + input_roles=input_roles, + output_role=output_role, + coupling_mode="fixed", + ) + if kind == 10: + return VertexLoweringRule( + kind=kind, + backend="spenso", + tensor_names=( + symbols.display_name("quark_vector_weyl_minus"), + symbols.display_name("quark_vector_weyl_plus"), + ), + expression_head="fermion_gauge_weyl_current", + full_tensor_network_ready=True, + description="Weyl fermion electroweak current with graph coupling", + kernel="fermion_vector_to_fermion", + input_roles=("fermion", "vector"), + output_role="fermion", + coupling_mode="vertex", + ) + if kind in {11, 21, 22, 23, 24}: + fermion_gauge_role_map: dict[ + int, + tuple[tuple[str, str], str, str, str], + ] = { + 11: ( + ("antifermion", "vector"), + "antifermion", + "antifermion electroweak current", + "fermion_vector_to_fermion", + ), + 21: ( + ("fermion", "antifermion"), + "vector", + "lepton-antilepton electroweak current", + "fermion_pair_to_vector", + ), + 22: ( + ("antifermion", "fermion"), + "vector", + "antilepton-lepton electroweak current", + "fermion_pair_to_vector", + ), + 23: ( + ("vector", "fermion"), + "fermion", + "vector-fermion electroweak current", + "fermion_vector_to_fermion", + ), + 24: ( + ("vector", "antifermion"), + "antifermion", + "vector-antifermion electroweak current", + "fermion_vector_to_fermion", + ), + } + input_roles, output_role, description, kernel = fermion_gauge_role_map[kind] + return VertexLoweringRule( + kind=kind, + backend="symbolica", + expression_head="fermion_gauge_weyl_current", + full_tensor_network_ready=True, + description=f"Weyl {description} with runtime coupling", + kernel=kernel, + input_roles=input_roles, + output_role=output_role, + coupling_mode="vertex", + ) + if kind == 8: + return VertexLoweringRule( + kind=kind, + backend="symbolica", + expression_head="qcd_u1_subtraction_current", + full_tensor_network_ready=True, + description="QCD U(1) subtraction current", + kernel="fermion_pair_to_vector", + input_roles=("fermion", "antifermion"), + output_role="vector", + coupling_mode="vertex", + ) + if kind in {12, 13, 14, 15}: + vector_role_map: dict[int, tuple[str, tuple[str, str], str, str]] = { + 12: ( + "three_vector_current", + ("vector", "vector"), + "vector", + "electroweak three-vector current", + ), + 13: ( + "two_vector_to_tensor", + ("vector", "vector"), + "antisymmetric_tensor", + "two electroweak vectors to auxiliary tensor", + ), + 14: ( + "tensor_vector_to_vector", + ("antisymmetric_tensor", "vector"), + "vector", + "auxiliary tensor and vector to electroweak vector", + ), + 15: ( + "vector_tensor_to_vector", + ("vector", "antisymmetric_tensor"), + "vector", + "electroweak vector and auxiliary tensor to vector", + ), + } + kernel, input_roles, output_role, description = vector_role_map[kind] + return VertexLoweringRule( + kind=kind, + backend="symbolica", + expression_head=kernel, + full_tensor_network_ready=True, + description=f"{description} with runtime coupling", + kernel=kernel, + input_roles=input_roles, + output_role=output_role, + coupling_mode="vertex", + ) + if kind == 16: + return VertexLoweringRule( + kind=kind, + backend="symbolica", + expression_head="fermion_scalar_to_fermion", + description=("massive Dirac fermion-scalar Yukawa current"), + full_tensor_network_ready=True, + kernel="fermion_scalar_to_fermion", + input_roles=("fermion", "scalar"), + output_role="fermion", + coupling_mode="vertex", + ) + if kind in {17, 18, 19, 20}: + scalar_role_map: dict[int, tuple[str, tuple[str, str], str, str]] = { + 17: ( + "two_vector_to_scalar", + ("vector", "vector"), + "scalar", + "two vectors to scalar current", + ), + 18: ( + "scalar_vector_to_vector", + ("scalar", "vector"), + "vector", + "scalar-vector to vector current", + ), + 19: ( + "vector_scalar_to_vector", + ("vector", "scalar"), + "vector", + "vector-scalar to vector current", + ), + 20: ( + "two_scalar_to_scalar", + ("scalar", "scalar"), + "scalar", + "scalar-scalar to scalar current", + ), + } + kernel, input_roles, output_role, description = scalar_role_map[kind] + return VertexLoweringRule( + kind=kind, + backend="symbolica", + expression_head=kernel, + full_tensor_network_ready=True, + description=f"{description} with runtime coupling", + kernel=kernel, + input_roles=input_roles, + output_role=output_role, + coupling_mode="vertex", + ) + return VertexLoweringRule( + kind=kind, + backend="unimplemented", + description="no native pyamplicol lowering rule is registered yet", + kernel="unknown", + ) + + def vertex_evaluation_equivalence( + self, + kind: int, + ) -> VertexEvaluationEquivalence: + """Expose exact input-orientation identities of built-in kernels.""" + + antisymmetric_same_inputs = { + 0: "three-vector-current", + 1: "two-vector-to-tensor", + } + if kind in antisymmetric_same_inputs: + return VertexEvaluationEquivalence( + class_id=f"builtin-sm:{antisymmetric_same_inputs[kind]}", + input_exchange_factor=(-1.0, 0.0), + ) + + pure_gauge_orientations = { + 2: ("tensor-vector-to-vector", (0, 1), (1.0, 0.0)), + 3: ("tensor-vector-to-vector", (1, 0), (-1.0, 0.0)), + } + relation = pure_gauge_orientations.get(kind) + if relation is not None: + class_name, input_order, factor = relation + return VertexEvaluationEquivalence( + class_id=f"builtin-sm:{class_name}", + factor=factor, + input_order=input_order, + ) + + quark_gluon_orientations = { + 4: ("quark-gluon-to-quark", (1, 0)), + 6: ("quark-gluon-to-quark", (0, 1)), + 5: ("antiquark-gluon-to-antiquark", (1, 0)), + 7: ("antiquark-gluon-to-antiquark", (0, 1)), + } + relation = quark_gluon_orientations.get(kind) + if relation is not None: + class_name, input_order = relation + return VertexEvaluationEquivalence( + class_id=f"builtin-sm:{class_name}", + input_order=input_order, + ) + + fermion_gauge_orientations = { + 10: ("fermion-vector-to-fermion", (0, 1)), + 23: ("fermion-vector-to-fermion", (1, 0)), + 11: ("antifermion-vector-to-antifermion", (0, 1)), + 24: ("antifermion-vector-to-antifermion", (1, 0)), + } + relation = fermion_gauge_orientations.get(kind) + if relation is not None: + class_name, input_order = relation + return VertexEvaluationEquivalence( + class_id=f"builtin-sm:{class_name}", + input_order=input_order, + ) + + fermion_pair_orientations = { + 21: (0, 1), + 22: (1, 0), + } + input_order = fermion_pair_orientations.get(kind) + if input_order is not None: + return VertexEvaluationEquivalence( + class_id="builtin-sm:fermion-pair-to-vector", + input_order=input_order, + ) + + scalar_vector_orientations = { + 18: (0, 1), + 19: (1, 0), + } + input_order = scalar_vector_orientations.get(kind) + if input_order is not None: + return VertexEvaluationEquivalence( + class_id="builtin-sm:scalar-vector-to-vector", + input_order=input_order, + ) + + return super().vertex_evaluation_equivalence(kind) + + def vertex_component_expression( + self, + kind: int, + left: Sequence[Any], + right: Sequence[Any], + *, + result_particle_id: int, + result_chirality: int, + left_chirality: int = 0, + right_chirality: int = 0, + coupling: tuple[Any, Any] = (1.0, 0.0), + left_momentum: Sequence[Any] | None = None, + right_momentum: Sequence[Any] | None = None, + ) -> tuple[Any, ...]: + """Lower a local model vertex into component expressions. + + The inputs are already component tuples for the two parent currents. + This method is intentionally process-blind: all decisions are local to + the vertex kind, chirality labels, particle id, coupling, and optional + current momenta. + """ + + if kind == 0: + if left_momentum is None or right_momentum is None: + raise ValueError("three-vector current requires parent momenta") + return _expr_three_vector_current( + tuple(left), + tuple(left_momentum), + tuple(right), + tuple(right_momentum), + ) + if kind == 1: + return _expr_two_vector_to_tensor(tuple(left), tuple(right)) + if kind == 2: + return _expr_tensor_vector_to_vector(tuple(left), tuple(right)) + if kind == 3: + return _expr_vector_tensor_to_vector(tuple(left), tuple(right)) + if kind == 12: + if left_momentum is None or right_momentum is None: + raise ValueError("three-vector current requires parent momenta") + return _expr_three_vector_current_coupled( + tuple(left), + tuple(left_momentum), + tuple(right), + tuple(right_momentum), + coupling, + ) + if kind == 13: + return tuple( + coupling[0] * component + for component in _expr_two_vector_to_tensor(tuple(left), tuple(right)) + ) + if kind == 14: + return tuple( + coupling[0] * component + for component in _expr_tensor_vector_to_vector( + tuple(left), + tuple(right), + ) + ) + if kind == 15: + return tuple( + coupling[0] * component + for component in _expr_vector_tensor_to_vector( + tuple(left), + tuple(right), + ) + ) + if kind == 16: + return _expr_fermion_scalar_to_fermion( + tuple(left), + tuple(right), + coupling, + ) + if kind == 17: + return ( + _imaginary_inverse_sqrt_two(*left, *right, *coupling) + * coupling[0] + * _expr_minkowski_dot(tuple(left), tuple(right)), + ) + if kind == 18: + prefactor = _imaginary_inverse_sqrt_two(*left, *right, *coupling) + return tuple( + prefactor * coupling[0] * left[0] * component + for component in tuple(right) + ) + if kind == 19: + prefactor = _imaginary_inverse_sqrt_two(*left, *right, *coupling) + return tuple( + prefactor * coupling[0] * right[0] * component + for component in tuple(left) + ) + if kind == 20: + phase = 1j if coupling[1] == -10.0 else 1.0 + prefactor = _imaginary_inverse_sqrt_two(*left, *right, *coupling) + return (prefactor * phase * coupling[0] * left[0] * right[0],) + if kind in {4, 6}: + fermion, vector = ( + (tuple(right), tuple(left)) + if kind == 4 + else (tuple(left), tuple(right)) + ) + if len(fermion) == 4: + return _expr_fermion_vector_dirac( + fermion, + vector, + antifermion=False, + coupling=None, + ) + return _expr_fermion_vector_weyl( + fermion, + vector, + result_chirality, + antifermion=False, + coupling=None, + ) + if kind in {5, 7}: + antifermion, vector = ( + (tuple(right), tuple(left)) + if kind == 5 + else (tuple(left), tuple(right)) + ) + if len(antifermion) == 4: + return _expr_fermion_vector_dirac( + antifermion, + vector, + antifermion=True, + coupling=None, + ) + return _expr_fermion_vector_weyl( + antifermion, + vector, + result_chirality, + antifermion=True, + coupling=None, + ) + if kind == 9: + fermion = tuple(right) + antifermion = tuple(left) + if len(fermion) == 4 or len(antifermion) == 4: + return _expr_fermion_antifermion_to_vector_dirac( + fermion=_as_dirac_current(fermion, right_chirality), + antifermion=_as_dirac_current(antifermion, left_chirality), + coupling=(1.0, 1.0), + ) + return _expr_fermion_antifermion_to_vector_weyl( + fermion=fermion, + antifermion=antifermion, + coupling=(1.0, 1.0), + fermion_chirality=right_chirality, + antifermion_chirality=left_chirality, + ) + if kind == 8: + qcd_coupling = (coupling[0], coupling[0]) + fermion = tuple(left) + antifermion = tuple(right) + if len(fermion) == 4 or len(antifermion) == 4: + return _expr_fermion_antifermion_to_vector_dirac( + fermion=_as_dirac_current(fermion, left_chirality), + antifermion=_as_dirac_current(antifermion, right_chirality), + coupling=qcd_coupling, + ) + return _expr_fermion_antifermion_to_vector_weyl( + fermion=fermion, + antifermion=antifermion, + coupling=qcd_coupling, + fermion_chirality=left_chirality, + antifermion_chirality=right_chirality, + ) + if kind in {10, 23}: + fermion, vector = ( + (tuple(left), tuple(right)) + if kind == 10 + else (tuple(right), tuple(left)) + ) + input_chirality = left_chirality if kind == 10 else right_chirality + if len(fermion) == 4: + return _expr_fermion_vector_dirac( + fermion, + vector, + antifermion=False, + coupling=coupling, + ) + current = _expr_fermion_vector_weyl( + fermion, + vector, + input_chirality, + antifermion=False, + coupling=coupling, + ) + if self.current_dimension(result_particle_id, result_chirality) == 4: + return _embed_weyl_current_in_dirac(current, input_chirality) + return current + if kind in {11, 24}: + antifermion, vector = ( + (tuple(left), tuple(right)) + if kind == 11 + else (tuple(right), tuple(left)) + ) + input_chirality = left_chirality if kind == 11 else right_chirality + if len(antifermion) == 4: + return _expr_fermion_vector_dirac( + antifermion, + vector, + antifermion=True, + coupling=coupling, + ) + current = _expr_fermion_vector_weyl( + antifermion, + vector, + input_chirality, + antifermion=True, + coupling=coupling, + ) + if self.current_dimension(result_particle_id, result_chirality) == 4: + return _embed_weyl_current_in_dirac(current, input_chirality) + return current + if kind == 21: + fermion = tuple(left) + antifermion = tuple(right) + if len(fermion) == 4 or len(antifermion) == 4: + return _expr_fermion_antifermion_to_vector_dirac( + fermion=_as_dirac_current(fermion, left_chirality), + antifermion=_as_dirac_current(antifermion, right_chirality), + coupling=coupling, + ) + return _expr_fermion_antifermion_to_vector_weyl( + fermion=fermion, + antifermion=antifermion, + coupling=coupling, + fermion_chirality=left_chirality, + antifermion_chirality=right_chirality, + ) + if kind == 22: + fermion = tuple(right) + antifermion = tuple(left) + if len(fermion) == 4 or len(antifermion) == 4: + return _expr_fermion_antifermion_to_vector_dirac( + fermion=_as_dirac_current(fermion, right_chirality), + antifermion=_as_dirac_current(antifermion, left_chirality), + coupling=coupling, + ) + return _expr_fermion_antifermion_to_vector_weyl( + fermion=fermion, + antifermion=antifermion, + coupling=coupling, + fermion_chirality=right_chirality, + antifermion_chirality=left_chirality, + ) + raise ValueError(f"vertex kind {kind} has no component expression lowering") + + def propagator_component_expression( + self, + particle_id: int, + value: Sequence[Any], + momentum: Sequence[Any], + *, + chirality: int = 0, + propagator: PropagatorIR | None = None, + ) -> tuple[Any, ...]: + metadata = propagator or self._propagator_ir(particle_id, chirality) + if metadata.identity.canonical_id != self._particle_identity_ir( + particle_id + ).canonical_id or metadata.chirality != int(chirality): + raise ValueError("propagator metadata does not match the current") + components = tuple(value) + current_momentum = tuple(momentum) + if not metadata.applies_propagator: + return components + if metadata.kind == "vector" and metadata.gauge == "feynman": + denominator = _minkowski_square_expression(current_momentum) + prefactor = -1j / denominator + return tuple(component * prefactor for component in components) + if metadata.kind == "vector" and metadata.gauge == "unitary": + mass = self.mass(particle_id) + width = self.width(particle_id) + denominator = ( + _minkowski_square_expression(current_momentum) + - mass * mass + + 1j * mass * width + ) + prefactor = -1j / denominator + longitudinal = _expr_minkowski_dot(components, current_momentum) / ( + mass * mass + ) + return tuple( + (components[index] - current_momentum[index] * longitudinal) * prefactor + for index in range(4) + ) + if metadata.kind == "weyl-fermion": + if metadata.identity.orientation == "antiparticle": + return _expr_antifermion_propagator_weyl( + components, + current_momentum, + chirality, + ) + return _expr_fermion_propagator_weyl( + components, + current_momentum, + chirality, + ) + if metadata.kind == "dirac-fermion": + if metadata.identity.orientation == "antiparticle": + return _expr_antifermion_propagator_dirac( + components, + current_momentum, + self.mass(particle_id), + self.width(particle_id), + ) + return _expr_fermion_propagator_dirac( + components, + current_momentum, + self.mass(particle_id), + self.width(particle_id), + ) + if metadata.kind == "scalar": + mass = self.mass(particle_id) + width = self.width(particle_id) + denominator = ( + _minkowski_square_expression(current_momentum) + - mass * mass + + 1j * mass * width + ) + prefactor = 1j / denominator + return tuple(component * prefactor for component in components) + raise ValueError( + f"propagator kind {metadata.kind!r} in gauge {metadata.gauge!r} " + f"is not lowered for particle " + f"{particle_id}" + ) + + def three_gluon_current_expression( + self, + *, + left_slot: Any, + right_slot: Any, + output_slot: Any, + left_momentum_tensor_name: str, + right_momentum_tensor_name: str, + dummy_prefix: str, + ) -> Any: + from symbolica import Expression + from symbolica.community.tensor import Representation, TensorName + + mink = Representation.mink(4) + metric = TensorName.g() + left_momentum = TensorName(left_momentum_tensor_name) + right_momentum = TensorName(right_momentum_tensor_name) + left_dot_slot = mink(f"{dummy_prefix}_left_dot") + right_dot_slot = mink(f"{dummy_prefix}_right_dot") + prefactor = _exact_imaginary_inverse_sqrt_two() + + return prefactor * ( + metric(left_slot, right_slot).to_expression() + * ( + left_momentum(output_slot).to_expression() + - right_momentum(output_slot).to_expression() + ) + + Expression.num(2.0) + * ( + metric(left_slot, left_dot_slot).to_expression() + * right_momentum(left_dot_slot).to_expression() + * metric(right_slot, output_slot).to_expression() + - metric(right_slot, right_dot_slot).to_expression() + * left_momentum(right_dot_slot).to_expression() + * metric(left_slot, output_slot).to_expression() + ) + ) + + def gluon_propagator_tensor_data( + self, + momentum: Sequence[Any], + ) -> list[Any]: + prefactor = _number(-1j) / _minkowski_square_expression(momentum) + metric = (1.0, -1.0, -1.0, -1.0) + data = [_number(0.0)] * (4 * 4) + for index, sign in enumerate(metric): + data[_flat_index((index, index), (4, 4))] = _number(sign) * prefactor + return data + + def quark_weyl_propagator_tensor_data( + self, + momentum: Sequence[Any], + *, + chirality: int, + ) -> list[Any]: + prefactor = _number(1j) / _minkowski_square_expression(momentum) + p0, p1, p2, p3 = (_as_expression(value) for value in momentum) + data = [_number(0.0)] * (2 * 2) + + def set_entry(q_in: int, q_out: int, value: Any) -> None: + data[_flat_index((q_in, q_out), (2, 2))] = prefactor * value + + if chirality == 1: + set_entry(0, 0, p0 + p3) + set_entry(1, 0, p1 + _number(1j) * p2) + set_entry(0, 1, p1 - _number(1j) * p2) + set_entry(1, 1, p0 - p3) + return data + if chirality == -1: + set_entry(0, 0, p0 - p3) + set_entry(1, 0, -(p1 + _number(1j) * p2)) + set_entry(0, 1, -(p1 - _number(1j) * p2)) + set_entry(1, 1, p0 + p3) + return data + raise ValueError(f"unsupported Weyl chirality: {chirality}") diff --git a/pyamplicol/src/pyamplicol/models/builtin/lowering_reports.py b/pyamplicol/src/pyamplicol/models/builtin/lowering_reports.py new file mode 100644 index 0000000000..4d22d03465 --- /dev/null +++ b/pyamplicol/src/pyamplicol/models/builtin/lowering_reports.py @@ -0,0 +1,386 @@ +# SPDX-License-Identifier: 0BSD +"""Built-in-model diagnostics for symbolic recursion lowering.""" + +from __future__ import annotations + +import time +from collections import Counter +from typing import Any + +from .lowering_tensor import ( + _build_auxiliary_tensor_probe, + _build_color_probe, + _clean_symbolica_string, + _current_dimension, + _current_momentum_currents, + _GraphTensorExpressionBuilder, + _preview_expression, + _propagator_lowering_ready, + _register_parametric_current_momenta, + _register_parametric_source_currents, + _source_current_count, + _source_currents, +) +from .lowering_types import ( + RecursionLoweringPlan, + SymbolicLoweringReport, + TensorNetworkBlueprint, + VertexLoweringReport, + VertexLoweringStep, +) +from .model import BuiltinSMModel +from .symbols import symbols + +_MAX_RECURSION_EXPRESSION_PREVIEW = 4096 + + +def build_symbolic_lowering_report( + model: BuiltinSMModel, + graph: Any | None = None, +) -> SymbolicLoweringReport: + """Exercise the real Symbolica tensor hooks used by ME lowering. + + This is intentionally a small, deterministic probe: it validates that the + model-owned auxiliary four-gluon tensors are registered in spenso and that + symbolic color simplification is available. It is not the full ME evaluator. + """ + + tensor_probe = _build_auxiliary_tensor_probe(model) + color_probe = _build_color_probe() + recursion_plan = None if graph is None else _build_recursion_plan(graph) + vertex_lowering = ( + None if graph is None else _build_vertex_lowering_report(model, graph) + ) + tensor_network_blueprint = ( + None if graph is None else _build_tensor_network_blueprint(model, graph) + ) + full_me_tensor_network_ready = ( + tensor_network_blueprint.full_me_tensor_network_ready + if tensor_network_blueprint is not None + else False + ) + return SymbolicLoweringReport( + tensor_library="TensorLibrary.hep_lib_atom", + tensor_network_probe=tensor_probe, + color_algebra_probe=color_probe, + recursion_plan=recursion_plan, + vertex_lowering=vertex_lowering, + tensor_network_blueprint=tensor_network_blueprint, + full_me_tensor_network_ready=full_me_tensor_network_ready, + ) + + +def _build_recursion_plan(graph: Any) -> RecursionLoweringPlan: + assignments = tuple( + _interaction_assignment(interaction) for interaction in graph.interactions + ) + amplitudes = tuple( + symbols.amplitude( + _current_atom(left), + _current_atom(right), + ) + for left, right in graph.amplitudes + ) + assignment_sum = _sum_expressions(assignments) + amplitude_sum = _sum_expressions(amplitudes) + plan_expression = symbols.matrix_element_plan(assignment_sum, amplitude_sum) + vertex_kind_counts = Counter( + int(interaction.vertex_kind) for interaction in graph.interactions + ) + expression = _clean_symbolica_string(str(plan_expression)) + tensor_route_vertex_kinds = tuple( + kind for kind in (1, 2, 3) if vertex_kind_counts.get(kind, 0) > 0 + ) + first_assignments = tuple( + _clean_symbolica_string(str(assignment)) for assignment in assignments[:5] + ) + return RecursionLoweringPlan( + engine="symbolica", + current_count=len(graph.currents), + interaction_count=len(graph.interactions), + amplitude_count=len(graph.amplitudes), + source_current_count=_source_current_count(graph), + vertex_kind_counts=tuple(sorted(vertex_kind_counts.items())), + tensor_route_vertex_kinds=tensor_route_vertex_kinds, + color_order=tuple(int(index) for index in graph.color_order), + expression=_preview_expression(expression), + expression_length=len(expression), + expression_truncated=len(expression) > _MAX_RECURSION_EXPRESSION_PREVIEW, + first_assignments=first_assignments, + ) + + +def _interaction_assignment(interaction: Any) -> Any: + return symbols.assignment( + _current_atom(interaction.result), + symbols.vertex( + int(interaction.vertex_kind), + _current_atom(interaction.left), + _current_atom(interaction.right), + _number(interaction.coupling[0]), + _number(interaction.coupling[1]), + ), + ) + + +def _current_atom(current: Any) -> Any: + return symbols.current( + int(current.pdg), + _label_atom(tuple(int(label) for label in current.external_labels)), + int(current.chirality), + ) + + +def _label_atom(labels: tuple[int, ...]) -> Any: + return symbols.label(labels) + + +def _number(value: float) -> Any: + from symbolica import Expression + + return Expression.num(value) + + +def _sum_expressions(expressions: tuple[Any, ...]) -> Any: + from symbolica import Expression + + total = Expression.num(0) + for expression in expressions: + total = total + expression + return total + + +def _current_key_tuple(current: Any) -> tuple[int, tuple[int, ...], int]: + return ( + int(current.pdg), + tuple(int(label) for label in current.external_labels), + int(current.chirality), + ) + + +def _build_vertex_lowering_report( + model: BuiltinSMModel, + graph: Any, +) -> VertexLoweringReport: + steps = tuple( + _vertex_lowering_step(model, index, interaction) + for index, interaction in enumerate(graph.interactions, start=1) + ) + backend_counts = Counter(step.backend for step in steps) + ready_kind_counts = Counter( + step.vertex_kind for step in steps if step.full_tensor_network_ready + ) + pending_kind_counts = Counter( + step.vertex_kind for step in steps if not step.full_tensor_network_ready + ) + tensor_names = tuple( + sorted({tensor_name for step in steps for tensor_name in step.tensor_names}) + ) + return VertexLoweringReport( + total_interactions=len(steps), + full_tensor_network_ready=bool(steps) + and all(step.full_tensor_network_ready for step in steps), + backend_counts=tuple(sorted(backend_counts.items())), + ready_vertex_kind_counts=tuple(sorted(ready_kind_counts.items())), + pending_vertex_kind_counts=tuple(sorted(pending_kind_counts.items())), + tensor_names=tensor_names, + first_steps=steps[:8], + ) + + +def _vertex_lowering_step( + model: BuiltinSMModel, + index: int, + interaction: Any, +) -> VertexLoweringStep: + rule = model.vertex_lowering_rule(int(interaction.vertex_kind)) + return VertexLoweringStep( + index=index, + vertex_kind=int(interaction.vertex_kind), + backend=rule.backend, + tensor_names=rule.tensor_names, + expression_head=rule.expression_head, + full_tensor_network_ready=rule.full_tensor_network_ready, + result_current=_clean_symbolica_string(str(_current_atom(interaction.result))), + left_current=_clean_symbolica_string(str(_current_atom(interaction.left))), + right_current=_clean_symbolica_string(str(_current_atom(interaction.right))), + ) + + +def _build_tensor_network_blueprint( + model: BuiltinSMModel, + graph: Any, +) -> TensorNetworkBlueprint: + from symbolica.community.tensor import TensorNetwork, as_tensor + + max_interactions_to_build = 96 + max_interactions_to_execute = 40 + vertex_lowering = _build_vertex_lowering_report(model, graph) + ready_interactions = sum( + count for _, count in vertex_lowering.ready_vertex_kind_counts + ) + pending_interactions = sum( + count for _, count in vertex_lowering.pending_vertex_kind_counts + ) + placeholder_vertex_kinds = tuple( + kind for kind, _ in vertex_lowering.pending_vertex_kind_counts + ) + registered_tensor_names = vertex_lowering.tensor_names + source_currents = _source_currents(graph) + momentum_currents = _current_momentum_currents(graph) + current_leaf_count = len(source_currents) + source_parameter_count = sum( + _current_dimension(current) for current in source_currents + ) + momentum_parameter_count = 4 * len(momentum_currents) + parametric_parameter_count = source_parameter_count + momentum_parameter_count + propagator_ready = _propagator_lowering_ready(graph) + full_ready = vertex_lowering.full_tensor_network_ready and propagator_ready + + if len(graph.interactions) > max_interactions_to_build: + return TensorNetworkBlueprint( + engine="spenso", + status="size-guarded", + current_count=len(graph.currents), + interaction_count=len(graph.interactions), + amplitude_count=len(graph.amplitudes), + expression_built=False, + expression_executed=False, + full_me_tensor_network_ready=False, + propagator_lowering_ready=propagator_ready, + ready_interactions=ready_interactions, + pending_interactions=pending_interactions, + placeholder_vertex_kinds=placeholder_vertex_kinds, + registered_tensor_names=registered_tensor_names, + current_leaf_count=current_leaf_count, + parametric_external_current_count=current_leaf_count, + parametric_source_current_parameter_count=source_parameter_count, + parametric_current_momentum_count=len(momentum_currents), + parametric_momentum_parameter_count=momentum_parameter_count, + parametric_parameter_count=parametric_parameter_count, + expression=None, + expression_length=None, + expression_truncated=None, + executed_expression=None, + executed_expression_length=None, + executed_expression_truncated=None, + execution_time_s=None, + ) + + try: + builder = _GraphTensorExpressionBuilder(model, graph) + raw_expression = builder.matrix_element_skeleton() + expression = ( + as_tensor(raw_expression) + .simplify_algebra( + gamma=False, + color=True, + contract="none", + color_substitute_cof_dimension_invariants=True, + ) + .to_expression() + ) + expression_text = _clean_symbolica_string(str(expression)) + if len(graph.interactions) > max_interactions_to_execute: + return TensorNetworkBlueprint( + engine="spenso", + status="execution-size-guarded", + current_count=len(graph.currents), + interaction_count=len(graph.interactions), + amplitude_count=len(graph.amplitudes), + expression_built=True, + expression_executed=False, + full_me_tensor_network_ready=False, + propagator_lowering_ready=propagator_ready, + ready_interactions=ready_interactions, + pending_interactions=pending_interactions, + placeholder_vertex_kinds=placeholder_vertex_kinds, + registered_tensor_names=registered_tensor_names, + current_leaf_count=current_leaf_count, + parametric_external_current_count=current_leaf_count, + parametric_source_current_parameter_count=source_parameter_count, + parametric_current_momentum_count=len(momentum_currents), + parametric_momentum_parameter_count=momentum_parameter_count, + parametric_parameter_count=parametric_parameter_count, + expression=_preview_expression(expression_text), + expression_length=len(expression_text), + expression_truncated=( + len(expression_text) > _MAX_RECURSION_EXPRESSION_PREVIEW + ), + executed_expression=None, + executed_expression_length=None, + executed_expression_truncated=None, + execution_time_s=None, + ) + + start = time.perf_counter() + library = model.build_tensor_library() + _register_parametric_source_currents(library, graph) + _register_parametric_current_momenta(model, library, graph) + network = TensorNetwork(expression, library) + network.execute(library=library) + scalar = network.result_scalar() + execution_time_s = time.perf_counter() - start + executed_text = _clean_symbolica_string(str(scalar)) + dangling = as_tensor(scalar).list_dangling() + status = "scalar-skeleton" if not dangling else "dangling-indices" + except (RuntimeError, TypeError, ValueError) as exc: + return TensorNetworkBlueprint( + engine="spenso", + status=f"failed: {exc}", + current_count=len(graph.currents), + interaction_count=len(graph.interactions), + amplitude_count=len(graph.amplitudes), + expression_built=False, + expression_executed=False, + full_me_tensor_network_ready=False, + propagator_lowering_ready=propagator_ready, + ready_interactions=ready_interactions, + pending_interactions=pending_interactions, + placeholder_vertex_kinds=placeholder_vertex_kinds, + registered_tensor_names=registered_tensor_names, + current_leaf_count=current_leaf_count, + parametric_external_current_count=current_leaf_count, + parametric_source_current_parameter_count=source_parameter_count, + parametric_current_momentum_count=len(momentum_currents), + parametric_momentum_parameter_count=momentum_parameter_count, + parametric_parameter_count=parametric_parameter_count, + expression=None, + expression_length=None, + expression_truncated=None, + executed_expression=None, + executed_expression_length=None, + executed_expression_truncated=None, + execution_time_s=None, + ) + + return TensorNetworkBlueprint( + engine="spenso", + status=status, + current_count=len(graph.currents), + interaction_count=len(graph.interactions), + amplitude_count=len(graph.amplitudes), + expression_built=True, + expression_executed=True, + full_me_tensor_network_ready=full_ready, + propagator_lowering_ready=propagator_ready, + ready_interactions=ready_interactions, + pending_interactions=pending_interactions, + placeholder_vertex_kinds=placeholder_vertex_kinds, + registered_tensor_names=registered_tensor_names, + current_leaf_count=current_leaf_count, + parametric_external_current_count=current_leaf_count, + parametric_source_current_parameter_count=source_parameter_count, + parametric_current_momentum_count=len(momentum_currents), + parametric_momentum_parameter_count=momentum_parameter_count, + parametric_parameter_count=parametric_parameter_count, + expression=_preview_expression(expression_text), + expression_length=len(expression_text), + expression_truncated=len(expression_text) > _MAX_RECURSION_EXPRESSION_PREVIEW, + executed_expression=_preview_expression(executed_text), + executed_expression_length=len(executed_text), + executed_expression_truncated=( + len(executed_text) > _MAX_RECURSION_EXPRESSION_PREVIEW + ), + execution_time_s=execution_time_s, + ) diff --git a/pyamplicol/src/pyamplicol/models/builtin/lowering_shared.py b/pyamplicol/src/pyamplicol/models/builtin/lowering_shared.py new file mode 100644 index 0000000000..9be5706a8a --- /dev/null +++ b/pyamplicol/src/pyamplicol/models/builtin/lowering_shared.py @@ -0,0 +1,20 @@ +# SPDX-License-Identifier: 0BSD +"""Small expression helpers shared by built-in-SM lowering phases.""" + +from __future__ import annotations + +from typing import Any + + +def _number(value: float) -> Any: + from symbolica import Expression + + return Expression.num(value) + + +def _current_key_tuple(current: Any) -> tuple[int, tuple[int, ...], int]: + return ( + int(current.pdg), + tuple(int(label) for label in current.external_labels), + int(current.chirality), + ) diff --git a/pyamplicol/src/pyamplicol/models/builtin/lowering_tensor.py b/pyamplicol/src/pyamplicol/models/builtin/lowering_tensor.py new file mode 100644 index 0000000000..7285b14e43 --- /dev/null +++ b/pyamplicol/src/pyamplicol/models/builtin/lowering_tensor.py @@ -0,0 +1,911 @@ +# SPDX-License-Identifier: 0BSD +"""Built-in-model tensor-network expression construction and Symbolica probes.""" + +from __future__ import annotations + +import re +import time +from typing import Any + +from ..._internal.physics.parameters import ParamBuilder, SymbolicaEvaluatorBundle +from .lowering_shared import _current_key_tuple, _number +from .lowering_types import ColorAlgebraProbe, TensorNetworkProbe +from .model import BuiltinSMModel +from .symbols import symbols + +_MAX_RECURSION_EXPRESSION_PREVIEW = 4096 + + +def build_tensor_network_scalar_bundle( + model: BuiltinSMModel, + graph: Any, + *, + name: str, + collect_expression_metadata: bool = False, +) -> SymbolicaEvaluatorBundle: + """Build a Symbolica evaluator bundle from the propagated tensor network.""" + + from symbolica.community.tensor import TensorNetwork, as_tensor + + total_start = time.perf_counter() + library = model.build_tensor_library() + param_builder = ParamBuilder() + _register_parametric_source_currents(library, graph, param_builder) + _register_parametric_current_momenta(model, library, graph, param_builder) + raw_expression = _GraphTensorExpressionBuilder( + model, graph + ).matrix_element_skeleton() + expression = as_tensor(raw_expression).simplify_algebra( + gamma=False, + contract="none", + color_substitute_cof_dimension_invariants=True, + ) + network = TensorNetwork(expression, library) + reduction_start = time.perf_counter() + network.execute(library=library) + scalar = network.result_scalar() + reduction_time_s = time.perf_counter() - reduction_start + evaluator_start = time.perf_counter() + evaluator = scalar.evaluator(param_builder.parameter_symbols()) + if param_builder.real_valued_inputs: + evaluator.set_real_params(param_builder.real_valued_inputs) + evaluator_build_time_s = time.perf_counter() - evaluator_start + total_build_time_s = time.perf_counter() - total_start + expression_length = None + executed_expression_length = None + if collect_expression_metadata: + expression_length = len(_clean_symbolica_string(str(expression))) + executed_expression_length = len(_clean_symbolica_string(str(scalar))) + return SymbolicaEvaluatorBundle( + name=name, + evaluator=evaluator, + param_builder=param_builder, + complex_inputs=True, + metadata={ + "engine": "symbolica", + "kernel": "tensor-network-scalar", + "current_count": len(graph.currents), + "interaction_count": len(graph.interactions), + "amplitude_count": len(graph.amplitudes), + "expression_length": expression_length, + "executed_expression_length": executed_expression_length, + "expression_metadata_collected": collect_expression_metadata, + "tensor_network_reduction_s": reduction_time_s, + "symbolica_evaluator_build_s": evaluator_build_time_s, + "total_build_s": total_build_time_s, + }, + ) + + +def build_interleaved_tensor_network_scalar_bundle( + model: BuiltinSMModel, + graph: Any, + *, + name: str, + collect_expression_metadata: bool = False, +) -> SymbolicaEvaluatorBundle: + """Build a Symbolica evaluator by executing one aggregate TensorNetwork in steps.""" + + total_start = time.perf_counter() + library = model.build_tensor_library() + param_builder = ParamBuilder() + _register_parametric_source_currents(library, graph, param_builder) + _register_parametric_current_momenta(model, library, graph, param_builder) + + build_start = time.perf_counter() + builder = _GraphTensorExpressionBuilder(model, graph) + network, interleaved_metadata = builder.matrix_element_interleaved_network( + library, + execute_between=True, + ) + network_build_s = time.perf_counter() - build_start + + final_start = time.perf_counter() + network.execute(library=library) + scalar = network.result_scalar() + final_reduction_s = time.perf_counter() - final_start + + evaluator_start = time.perf_counter() + evaluator = scalar.evaluator(param_builder.parameter_symbols()) + if param_builder.real_valued_inputs: + evaluator.set_real_params(param_builder.real_valued_inputs) + evaluator_build_time_s = time.perf_counter() - evaluator_start + total_build_time_s = time.perf_counter() - total_start + return SymbolicaEvaluatorBundle( + name=name, + evaluator=evaluator, + param_builder=param_builder, + complex_inputs=True, + metadata={ + "engine": "symbolica", + "kernel": "interleaved-tensor-network-scalar", + "strategy": "interleaved", + "current_count": len(graph.currents), + "interaction_count": len(graph.interactions), + "amplitude_count": len(graph.amplitudes), + "network_build_s": network_build_s, + "interleaved_execution_s": interleaved_metadata["execution_s"], + "interleaved_execution_count": interleaved_metadata["execution_count"], + "interleaved_multiplication_count": interleaved_metadata[ + "multiplication_count" + ], + "interleaved_addition_count": interleaved_metadata["addition_count"], + "tensor_network_reduction_s": final_reduction_s, + "symbolica_evaluator_build_s": evaluator_build_time_s, + "total_build_s": total_build_time_s, + "executed_expression_length": ( + len(_clean_symbolica_string(str(scalar))) + if collect_expression_metadata + else None + ), + "expression_metadata_collected": collect_expression_metadata, + }, + ) + + +class _GraphTensorExpressionBuilder: + def __init__(self, model: BuiltinSMModel, graph: Any) -> None: + from symbolica.community.tensor import Representation, TensorName + + self.model = model + self.graph = graph + self._mink = Representation.mink(4) + self._aux6 = Representation(symbols.antisymmetric_lorentz_pair_name, 6) + self._weyl = Representation(symbols.weyl_spinor_name, 2) + self._tensor_name = TensorName + self._interactions_by_result: dict[ + tuple[int, tuple[int, ...], int], list[Any] + ] = {} + for interaction in graph.interactions: + self._interactions_by_result.setdefault( + _current_key_tuple(interaction.result), + [], + ).append(interaction) + + def matrix_element_skeleton(self) -> Any: + from symbolica import Expression + + total = Expression.num(0) + for amplitude_index, (left, right) in enumerate(self.graph.amplitudes, start=1): + slots = self._slots_for_current(left, f"amp_{amplitude_index}") + total = total + ( + self._current_expression(left, slots) + * self._current_expression(right, slots) + ) + return total + + def matrix_element_interleaved_network( + self, + library: Any, + *, + execute_between: bool = True, + ) -> tuple[Any, dict[str, float | int]]: + from symbolica.community.tensor import TensorNetwork + + metadata: dict[str, float | int] = { + "execution_s": 0.0, + "execution_count": 0, + "multiplication_count": 0, + "addition_count": 0, + } + total = None + for amplitude_index, (left, right) in enumerate(self.graph.amplitudes, start=1): + slots = self._slots_for_current(left, f"amp_{amplitude_index}") + left_network = self._current_network( + left, + slots, + library, + execute_between=execute_between, + metadata=metadata, + ) + right_network = self._current_network( + right, + slots, + library, + execute_between=execute_between, + metadata=metadata, + ) + amplitude_network = self._multiply_networks( + left_network, + right_network, + library, + execute_between=execute_between, + metadata=metadata, + ) + total = ( + amplitude_network + if total is None + else self._add_networks( + total, + amplitude_network, + library, + execute_between=execute_between, + metadata=metadata, + ) + ) + if total is None: + total = TensorNetwork.zero() + return total, metadata + + def _current_network( + self, + current: Any, + output_slots: tuple[Any, ...], + library: Any, + *, + execute_between: bool, + metadata: dict[str, float | int], + ) -> Any: + from symbolica.community.tensor import TensorNetwork, as_tensor + + interactions = self._interactions_by_result.get(_current_key_tuple(current)) + if not interactions: + expression = self._current_leaf(current, output_slots) + return TensorNetwork( + as_tensor(expression) if output_slots else expression, library + ) + + needs_propagator = _current_needs_propagator(self.graph, current) + result_slots = ( + self._slots_for_current( + current, + self._propagator_dummy_prefix(current), + ) + if needs_propagator + else output_slots + ) + total = None + for interaction_index, interaction in enumerate(interactions, start=1): + left_slots = self._slots_for_current( + interaction.left, + self._dummy_prefix(interaction, interaction_index, "left"), + ) + right_slots = self._slots_for_current( + interaction.right, + self._dummy_prefix(interaction, interaction_index, "right"), + ) + expression = self._vertex_tensor( + interaction, left_slots, right_slots, result_slots + ) + term = TensorNetwork( + as_tensor(expression) + if left_slots or right_slots or result_slots + else expression, + library, + ) + term = self._multiply_networks( + term, + self._current_network( + interaction.left, + left_slots, + library, + execute_between=execute_between, + metadata=metadata, + ), + library, + execute_between=execute_between, + metadata=metadata, + ) + term = self._multiply_networks( + term, + self._current_network( + interaction.right, + right_slots, + library, + execute_between=execute_between, + metadata=metadata, + ), + library, + execute_between=execute_between, + metadata=metadata, + ) + if needs_propagator: + expression = self._propagator_tensor( + current, result_slots, output_slots + ) + term = self._multiply_networks( + TensorNetwork( + as_tensor(expression) + if result_slots or output_slots + else expression, + library, + ), + term, + library, + execute_between=execute_between, + metadata=metadata, + ) + total = ( + term + if total is None + else self._add_networks( + total, + term, + library, + execute_between=execute_between, + metadata=metadata, + ) + ) + if total is None: + return TensorNetwork.zero() + return total + + def _multiply_networks( + self, + left: Any, + right: Any, + library: Any, + *, + execute_between: bool, + metadata: dict[str, float | int], + ) -> Any: + result = left * right + metadata["multiplication_count"] = int(metadata["multiplication_count"]) + 1 + if execute_between: + result = self._execute_network(result, library, metadata) + return result + + def _add_networks( + self, + left: Any, + right: Any, + library: Any, + *, + execute_between: bool, + metadata: dict[str, float | int], + ) -> Any: + result = left + right + metadata["addition_count"] = int(metadata["addition_count"]) + 1 + if execute_between: + result = self._execute_network(result, library, metadata) + return result + + def _execute_network( + self, + network: Any, + library: Any, + metadata: dict[str, float | int], + ) -> Any: + from symbolica.community.tensor import TensorNetwork + + start = time.perf_counter() + network.execute(library=library) + metadata["execution_s"] = float(metadata["execution_s"]) + ( + time.perf_counter() - start + ) + metadata["execution_count"] = int(metadata["execution_count"]) + 1 + return TensorNetwork.one() * network.result_tensor(library) + + def _current_expression(self, current: Any, output_slots: tuple[Any, ...]) -> Any: + from symbolica import Expression + + key = _current_key_tuple(current) + interactions = self._interactions_by_result.get(key) + if not interactions: + return self._current_leaf(current, output_slots) + + needs_propagator = _current_needs_propagator(self.graph, current) + result_slots = ( + self._slots_for_current( + current, + self._propagator_dummy_prefix(current), + ) + if needs_propagator + else output_slots + ) + total = Expression.num(0) + for interaction_index, interaction in enumerate(interactions, start=1): + left_slots = self._slots_for_current( + interaction.left, + self._dummy_prefix(interaction, interaction_index, "left"), + ) + right_slots = self._slots_for_current( + interaction.right, + self._dummy_prefix(interaction, interaction_index, "right"), + ) + total = total + ( + self._vertex_tensor(interaction, left_slots, right_slots, result_slots) + * self._current_expression(interaction.left, left_slots) + * self._current_expression(interaction.right, right_slots) + ) + if needs_propagator: + total = self._propagator_tensor(current, result_slots, output_slots) * total + return total + + def _current_leaf(self, current: Any, output_slots: tuple[Any, ...]) -> Any: + return self._tensor_name(_current_tensor_name(current))( + *output_slots + ).to_expression() + + def _vertex_tensor( + self, + interaction: Any, + left_slots: tuple[Any, ...], + right_slots: tuple[Any, ...], + output_slots: tuple[Any, ...], + ) -> Any: + kind = int(interaction.vertex_kind) + if kind == 0: + return self.model.three_gluon_current_expression( + left_slot=left_slots[0], + right_slot=right_slots[0], + output_slot=output_slots[0], + left_momentum_tensor_name=_current_momentum_tensor_name( + interaction.left + ), + right_momentum_tensor_name=_current_momentum_tensor_name( + interaction.right + ), + dummy_prefix=self._dummy_prefix(interaction, 0, "three_gluon"), + ) + if kind == 1: + return self._tensor_name(symbols.two_gluon_to_tensor_name)( + left_slots[0], + right_slots[0], + output_slots[0], + ).to_expression() + if kind == 2: + return self._tensor_name(symbols.tensor_gluon_to_gluon_name)( + left_slots[0], + right_slots[0], + output_slots[0], + ).to_expression() + if kind == 3: + return self._tensor_name(symbols.gluon_tensor_to_gluon_name)( + left_slots[0], + right_slots[0], + output_slots[0], + ).to_expression() + if kind == 6: + return self._tensor_name( + _quark_vector_weyl_tensor_name(int(interaction.result.chirality)) + )( + left_slots[0], + right_slots[0], + output_slots[0], + ).to_expression() + if kind == 10: + return ( + _number( + _weyl_coupling_for_chirality( + int(interaction.result.chirality), + _coupling_pair(interaction.coupling), + ) + ) + * self._tensor_name( + _quark_vector_weyl_tensor_name(int(interaction.result.chirality)) + )( + left_slots[0], + right_slots[0], + output_slots[0], + ).to_expression() + ) + return self._tensor_name(symbols.vertex_kind_tensor_name(kind))( + *left_slots, + *right_slots, + *output_slots, + _number(float(interaction.coupling[0])), + _number(float(interaction.coupling[1])), + ).to_expression() + + def _propagator_tensor( + self, + current: Any, + input_slots: tuple[Any, ...], + output_slots: tuple[Any, ...], + ) -> Any: + return self._tensor_name(_propagator_tensor_name(current))( + input_slots[0], + output_slots[0], + ).to_expression() + + def _slots_for_current(self, current: Any, prefix: str) -> tuple[Any, ...]: + pdg = int(current.pdg) + if pdg == -21: + return (self._aux6(f"{prefix}_A"),) + if _is_lorentz_vector_current_pdg(pdg): + return (self._mink(f"{prefix}_mu"),) + if 1 <= abs(pdg) <= 6 or 11 <= abs(pdg) <= 16: + return (self._weyl(f"{prefix}_alpha"),) + return () + + def _dummy_prefix(self, interaction: Any, index: int, side: str) -> str: + labels = "_".join(str(label) for label in interaction.result.external_labels) + chirality = int(interaction.result.chirality) + return ( + f"v{int(interaction.vertex_kind)}_{labels}_" + f"{_signed_label(chirality)}_{index}_{side}" + ) + + def _propagator_dummy_prefix(self, current: Any) -> str: + labels = "_".join(str(label) for label in current.external_labels) + return ( + f"prop_{_signed_label(int(current.pdg))}_{labels}_" + f"{_signed_label(int(current.chirality))}" + ) + + def _current_output_prefix(self, current: Any) -> str: + labels = "_".join(str(label) for label in current.external_labels) + return ( + f"cur_{_signed_label(int(current.pdg))}_{labels}_" + f"{_signed_label(int(current.chirality))}" + ) + + +def _current_tensor_name(current: Any) -> str: + labels = "_".join(str(label) for label in current.external_labels) + return symbols.qualified_name( + f"current_{_signed_label(int(current.pdg))}_{labels}_" + f"{_signed_label(int(current.chirality))}" + ) + + +def _signed_label(value: int) -> str: + if value < 0: + return f"m{abs(value)}" + return f"p{value}" + + +def _quark_vector_weyl_tensor_name(chirality: int) -> str: + if chirality == 1: + return symbols.quark_vector_weyl_plus_name + if chirality == -1: + return symbols.quark_vector_weyl_minus_name + raise ValueError(f"Weyl vector tensor requires nonzero chirality, got {chirality}") + + +def _weyl_coupling_for_chirality( + chirality: int, + coupling: tuple[float, float], +) -> float: + if chirality == 1: + return coupling[1] + if chirality == -1: + return coupling[0] + raise ValueError(f"Weyl coupling requires nonzero chirality, got {chirality}") + + +def _coupling_pair(coupling: Any) -> tuple[float, float]: + if len(coupling) != 2: + raise ValueError(f"expected a two-component coupling, got {coupling}") + return float(coupling[0]), float(coupling[1]) + + +def _source_current_count(graph: Any) -> int: + return len(_source_currents(graph)) + + +def _source_currents(graph: Any) -> tuple[Any, ...]: + interactions_by_result: dict[tuple[int, tuple[int, ...], int], list[Any]] = {} + for interaction in graph.interactions: + interactions_by_result.setdefault( + _current_key_tuple(interaction.result), + [], + ).append(interaction) + source_keys: set[tuple[int, tuple[int, ...], int]] = set() + seen: set[tuple[int, tuple[int, ...], int]] = set() + + def visit(current: Any) -> None: + key = _current_key_tuple(current) + if key in seen: + return + seen.add(key) + interactions = interactions_by_result.get(key) + if not interactions: + source_keys.add(key) + return + for interaction in interactions: + visit(interaction.left) + visit(interaction.right) + + for left, right in graph.amplitudes: + visit(left) + visit(right) + return tuple( + current + for current in graph.currents + if _current_key_tuple(current) in source_keys + ) + + +def _register_parametric_source_currents( + library: Any, + graph: Any, + builder: ParamBuilder | None = None, +) -> ParamBuilder: + from symbolica.community.tensor import Representation + + if builder is None: + builder = ParamBuilder() + mink = Representation.mink(4) + aux6 = Representation(symbols.antisymmetric_lorentz_pair_name, 6) + weyl = Representation(symbols.weyl_spinor_name, 2) + for current in _source_currents(graph): + pdg = int(current.pdg) + if pdg == -21: + representation = aux6 + elif _is_lorentz_vector_current_pdg(pdg): + representation = mink + elif 1 <= abs(pdg) <= 6 or 11 <= abs(pdg) <= 16: + representation = weyl + else: + continue + builder.register_rank1_tensor( + library, + tensor_name=_current_tensor_name(current), + representation=representation, + head=_current_parameter_head(current), + length=_current_dimension(current), + role="source_current", + ) + return builder + + +def _register_parametric_current_momenta( + model: BuiltinSMModel, + library: Any, + graph: Any, + builder: ParamBuilder | None = None, +) -> ParamBuilder: + from symbolica.community.tensor import Representation, Tensor, TensorName + + if builder is None: + builder = ParamBuilder() + mink = Representation.mink(4) + weyl = Representation(symbols.weyl_spinor_name, 2) + momentum_symbols_by_current: dict[ + tuple[int, tuple[int, ...], int], tuple[Any, ...] + ] = {} + for current in _current_momentum_currents(graph): + momentum_symbols = builder.register_rank1_tensor( + library, + tensor_name=_current_momentum_tensor_name(current), + representation=mink, + head=_current_momentum_parameter_head(current), + length=4, + role="current_momentum", + real_valued=True, + ) + momentum_symbols_by_current[_current_key_tuple(current)] = momentum_symbols + + for current in _propagating_currents(graph): + momentum_symbols = momentum_symbols_by_current[_current_key_tuple(current)] + pdg = int(current.pdg) + if pdg == 21: + library.register( + Tensor.dense( + TensorName(_propagator_tensor_name(current))(mink, mink), + model.gluon_propagator_tensor_data(momentum_symbols), + ) + ) + elif _is_weyl_fermion_current(current): + library.register( + Tensor.dense( + TensorName(_propagator_tensor_name(current))(weyl, weyl), + model.quark_weyl_propagator_tensor_data( + momentum_symbols, + chirality=int(current.chirality), + ), + ) + ) + return builder + + +def _current_momentum_currents(graph: Any) -> tuple[Any, ...]: + currents: dict[tuple[int, tuple[int, ...], int], Any] = {} + for interaction in graph.interactions: + if int(interaction.vertex_kind) == 0: + currents.setdefault(_current_key_tuple(interaction.left), interaction.left) + currents.setdefault( + _current_key_tuple(interaction.right), interaction.right + ) + for current in _propagating_currents(graph): + currents.setdefault(_current_key_tuple(current), current) + return tuple(currents.values()) + + +def _propagating_currents(graph: Any) -> tuple[Any, ...]: + result_keys = { + _current_key_tuple(interaction.result) for interaction in graph.interactions + } + amplitude_keys = { + _current_key_tuple(current) + for amplitude in graph.amplitudes + for current in amplitude + } + return tuple( + current + for current in graph.currents + if _current_key_tuple(current) in result_keys + and _current_key_tuple(current) not in amplitude_keys + and _has_supported_propagator(current) + ) + + +def _current_needs_propagator(graph: Any, current: Any) -> bool: + propagating_keys = { + _current_key_tuple(propagating_current) + for propagating_current in _propagating_currents(graph) + } + return _current_key_tuple(current) in propagating_keys + + +def _has_supported_propagator(current: Any) -> bool: + pdg = int(current.pdg) + return pdg == 21 or _is_weyl_fermion_current(current) + + +def _is_weyl_fermion_current(current: Any) -> bool: + pdg = int(current.pdg) + return (1 <= abs(pdg) <= 6 or 11 <= abs(pdg) <= 16) and int(current.chirality) != 0 + + +def _is_lorentz_vector_current_pdg(pdg: int) -> bool: + return pdg in (21, 22, 23, 24, -24) + + +def _current_dimension(current: Any) -> int: + pdg = int(current.pdg) + if pdg == -21: + return 6 + if _is_lorentz_vector_current_pdg(pdg): + return 4 + if 1 <= abs(pdg) <= 6 or 11 <= abs(pdg) <= 16: + return 2 + return 0 + + +def _current_parameter_head(current: Any) -> tuple[str, ...]: + labels = tuple(str(label) for label in current.external_labels) + return ( + "current", + _signed_label(int(current.pdg)), + "_".join(labels) if labels else "empty", + _signed_label(int(current.chirality)), + ) + + +def _current_momentum_tensor_name(current: Any) -> str: + labels = "_".join(str(label) for label in current.external_labels) + return symbols.qualified_name( + f"current_momentum_{_signed_label(int(current.pdg))}_{labels}_" + f"{_signed_label(int(current.chirality))}" + ) + + +def _propagator_tensor_name(current: Any) -> str: + labels = "_".join(str(label) for label in current.external_labels) + if int(current.pdg) == 21: + head = "gluon_propagator" + elif _is_weyl_fermion_current(current): + head = "quark_weyl_propagator" + else: + head = "propagator" + return symbols.qualified_name( + f"{head}_{_signed_label(int(current.pdg))}_{labels}_" + f"{_signed_label(int(current.chirality))}" + ) + + +def _current_momentum_parameter_head(current: Any) -> tuple[str, ...]: + labels = tuple(str(label) for label in current.external_labels) + return ( + "current_momentum", + _signed_label(int(current.pdg)), + "_".join(labels) if labels else "empty", + _signed_label(int(current.chirality)), + ) + + +def _propagator_lowering_ready(graph: Any) -> bool: + result_currents = { + _current_key_tuple(interaction.result): interaction.result + for interaction in graph.interactions + } + amplitude_keys = { + _current_key_tuple(current) + for amplitude in graph.amplitudes + for current in amplitude + } + for key, current in result_currents.items(): + if key in amplitude_keys or int(current.pdg) == -21: + continue + if not _has_supported_propagator(current): + return False + return bool(graph.interactions) + + +def _build_auxiliary_tensor_probe( + model: BuiltinSMModel, +) -> TensorNetworkProbe: + from symbolica.community.tensor import ( + Representation, + TensorName, + TensorNetwork, + as_tensor, + ) + + library = model.build_tensor_library() + mink = Representation.mink(4) + antisym = Representation(symbols.antisymmetric_lorentz_pair_name, 6) + two_gluon_to_tensor = TensorName(symbols.two_gluon_to_tensor_name) + tensor_gluon_to_gluon = TensorName(symbols.tensor_gluon_to_gluon_name) + + expression = ( + two_gluon_to_tensor( + mink("mu"), + mink("nu"), + antisym("A"), + ).to_expression() + * tensor_gluon_to_gluon( + antisym("A"), + mink("nu"), + mink("rho"), + ).to_expression() + ) + network = TensorNetwork(as_tensor(expression), library) + network.execute(library=library) + result = network.result_tensor(library) + output_size = len(result) + structure = result.structure + entries = tuple(complex(result[i]) for i in range(output_size)) + nonzero = tuple( + (index, value) for index, value in enumerate(entries) if abs(value) > 1.0e-15 + ) + weighted_checksum = sum((index + 1) * value for index, value in enumerate(entries)) + return TensorNetworkProbe( + engine="spenso", + tensor_names=( + symbols.two_gluon_to_tensor_name, + symbols.tensor_gluon_to_gluon_name, + ), + expression=_clean_symbolica_string(str(expression)), + output_structure=_clean_symbolica_string(str(structure)), + output_rank=2, + output_size=output_size, + nonzero_entries=len(nonzero), + max_abs_entry=max((abs(value) for value in entries), default=0.0), + weighted_checksum=(weighted_checksum.real, weighted_checksum.imag), + first_nonzero_entries=tuple( + (index, _format_complex(value)) for index, value in nonzero[:4] + ), + ) + + +def _build_color_probe() -> ColorAlgebraProbe: + from symbolica.community.tensor import TensorExpression + + structure_constant = TensorExpression.color_f(8) + + def color_f(i: int, j: int, k: int) -> Any: + return structure_constant(i, j, k) + + expression = color_f(1, 2, 3) * color_f(3, 2, 1) + simplified = expression.simplify_algebra( + gamma=False, + contract="none", + color_substitute_cof_dimension_invariants=True, + ) + return ColorAlgebraProbe( + engine="tensor", + input_expression=_clean_symbolica_string(str(expression)), + simplified_expression=_clean_symbolica_string(str(simplified)), + ) + + +def _format_complex(value: complex) -> str: + real = 0.0 if abs(value.real) < 1.0e-15 else value.real + imag = 0.0 if abs(value.imag) < 1.0e-15 else value.imag + return f"{real:.16g}{imag:+.16g}j" + + +def _clean_symbolica_string(value: str) -> str: + return re.sub(r"\x1b\[[0-9;]*m", "", value) + + +def _preview_expression(value: str) -> str: + if len(value) <= _MAX_RECURSION_EXPRESSION_PREVIEW: + return value + suffix = "..." + preview_length = _MAX_RECURSION_EXPRESSION_PREVIEW - len(suffix) + return f"{value[:preview_length]}{suffix}" diff --git a/pyamplicol/src/pyamplicol/models/builtin/lowering_types.py b/pyamplicol/src/pyamplicol/models/builtin/lowering_types.py new file mode 100644 index 0000000000..ed56385356 --- /dev/null +++ b/pyamplicol/src/pyamplicol/models/builtin/lowering_types.py @@ -0,0 +1,108 @@ +# SPDX-License-Identifier: 0BSD +"""Built-in-SM diagnostics for symbolic lowering results.""" + +from __future__ import annotations + +from dataclasses import dataclass + + +@dataclass(frozen=True) +class TensorNetworkProbe: + engine: str + tensor_names: tuple[str, ...] + expression: str + output_structure: str + output_rank: int + output_size: int + nonzero_entries: int + max_abs_entry: float + weighted_checksum: tuple[float, float] + first_nonzero_entries: tuple[tuple[int, str], ...] + + +@dataclass(frozen=True) +class ColorAlgebraProbe: + engine: str + input_expression: str + simplified_expression: str + + +@dataclass(frozen=True) +class RecursionLoweringPlan: + engine: str + current_count: int + interaction_count: int + amplitude_count: int + source_current_count: int + vertex_kind_counts: tuple[tuple[int, int], ...] + tensor_route_vertex_kinds: tuple[int, ...] + color_order: tuple[int, ...] + expression: str + expression_length: int + expression_truncated: bool + first_assignments: tuple[str, ...] + + +@dataclass(frozen=True) +class VertexLoweringStep: + index: int + vertex_kind: int + backend: str + tensor_names: tuple[str, ...] + expression_head: str + full_tensor_network_ready: bool + result_current: str + left_current: str + right_current: str + + +@dataclass(frozen=True) +class VertexLoweringReport: + total_interactions: int + full_tensor_network_ready: bool + backend_counts: tuple[tuple[str, int], ...] + ready_vertex_kind_counts: tuple[tuple[int, int], ...] + pending_vertex_kind_counts: tuple[tuple[int, int], ...] + tensor_names: tuple[str, ...] + first_steps: tuple[VertexLoweringStep, ...] + + +@dataclass(frozen=True) +class TensorNetworkBlueprint: + engine: str + status: str + current_count: int + interaction_count: int + amplitude_count: int + expression_built: bool + expression_executed: bool + full_me_tensor_network_ready: bool + propagator_lowering_ready: bool + ready_interactions: int + pending_interactions: int + placeholder_vertex_kinds: tuple[int, ...] + registered_tensor_names: tuple[str, ...] + current_leaf_count: int + parametric_external_current_count: int + parametric_source_current_parameter_count: int + parametric_current_momentum_count: int + parametric_momentum_parameter_count: int + parametric_parameter_count: int + expression: str | None + expression_length: int | None + expression_truncated: bool | None + executed_expression: str | None + executed_expression_length: int | None + executed_expression_truncated: bool | None + execution_time_s: float | None + + +@dataclass(frozen=True) +class SymbolicLoweringReport: + tensor_library: str + tensor_network_probe: TensorNetworkProbe + color_algebra_probe: ColorAlgebraProbe + recursion_plan: RecursionLoweringPlan | None = None + vertex_lowering: VertexLoweringReport | None = None + tensor_network_blueprint: TensorNetworkBlueprint | None = None + full_me_tensor_network_ready: bool = False diff --git a/pyamplicol/src/pyamplicol/models/builtin/model.py b/pyamplicol/src/pyamplicol/models/builtin/model.py new file mode 100644 index 0000000000..5949f79bd6 --- /dev/null +++ b/pyamplicol/src/pyamplicol/models/builtin/model.py @@ -0,0 +1,811 @@ +# SPDX-License-Identifier: 0BSD +from __future__ import annotations + +import math +from collections import Counter +from collections.abc import Iterable, Mapping +from dataclasses import dataclass +from functools import cached_property +from typing import Any, Literal, cast + +from .._physics_ir import ContractionIR, ParticleIdentityIR, ParticleOrientation +from ..base import ( + CouplingOrders, + Model, + PropagatorLoweringRule, + QuantumNumberFlow, + RecurrenceCurrentEmbeddingContract, + RecurrenceLCColorComponentKind, + RecurrenceLCColorOperation, + RecurrenceLCColorShapeKind, + RecurrenceLCColorSourceSeedContract, + RecurrenceLCColorTransitionContract, + RecurrenceLCColorWitnessContract, + RecurrenceQuantumFlowContract, + Vertex, +) +from .definitions import BuiltinSMDefinitionMixin +from .expressions import _builtin_vertex_result_chiralities +from .lowering import BuiltinSMLoweringMixin +from .process_catalog import PDGS + +_ELECTRIC_CHARGE_BY_PARTICLE = { + 1: "-1/3", + 2: "2/3", + 3: "-1/3", + 4: "2/3", + 5: "-1/3", + 6: "2/3", + 11: "-1", + 13: "-1", + 15: "-1", + 24: "1", + 26: "1", +} + + +class BuiltinModel(Model): + """Legacy built-in-model conventions isolated from external UFO models.""" + + @cached_property + def _color_rep_by_pdg(self) -> dict[int, int]: + reps: dict[int, int] = {} + for pdg, particle in self._species_by_pdg.items(): + color = particle.color_rep + if self._property_sign_by_pdg[pdg] < 0 and abs(color) == 3: + reps[pdg] = -color + else: + reps[pdg] = color + return reps + + def color_rep(self, pdg: int) -> int: + try: + return self._color_rep_by_pdg[pdg] + except KeyError as exc: + raise KeyError(f"particle not in model: {pdg}") from exc + + def is_quark(self, pdg: int) -> bool: + return 1 <= pdg <= 6 + + def is_antiquark(self, pdg: int) -> bool: + return -6 <= pdg <= -1 + + def is_lepton(self, pdg: int) -> bool: + return 11 <= pdg <= 16 + + def is_antilepton(self, pdg: int) -> bool: + return -16 <= pdg <= -11 + + def is_fermion(self, pdg: int) -> bool: + return ( + self.is_quark(pdg) + or self.is_antiquark(pdg) + or self.is_lepton(pdg) + or self.is_antilepton(pdg) + ) + + def is_chiral_eligible(self, pdg: int) -> bool: + return self.is_fermion(pdg) and self.mass(pdg) == 0.0 + + def _vertex_result_chiralities( + self, + vertex: Vertex, + left_index: Any, + right_index: Any, + ) -> tuple[int, ...]: + return _builtin_vertex_result_chiralities( + self, + vertex, + left_index, + right_index, + ) + + def recurrence_quantum_flow_contract( + self, + vertex: Vertex, + left_particle_id: int, + right_particle_id: int, + ) -> RecurrenceQuantumFlowContract: + return self._standard_recurrence_quantum_flow_contract( + vertex, + left_particle_id, + right_particle_id, + ) + + def recurrence_current_embedding_contract( + self, + particle_id: int, + source_chirality: int, + ) -> RecurrenceCurrentEmbeddingContract: + return self._standard_recurrence_current_embedding_contract( + particle_id, + source_chirality, + ) + + def recurrence_lc_color_shape_contract( + self, + particle_id: int, + chirality: int = 0, + ) -> RecurrenceLCColorShapeKind: + return self._standard_recurrence_lc_color_shape_contract( + particle_id, + chirality, + ) + + def recurrence_lc_source_color_contract( + self, + particle_id: int, + chirality: int = 0, + ) -> RecurrenceLCColorSourceSeedContract: + return self._standard_recurrence_lc_source_color_contract( + particle_id, + chirality, + ) + + def recurrence_lc_color_transition_contract( + self, + vertex: Vertex, + *, + closure: bool, + ) -> RecurrenceLCColorTransitionContract: + rule_kind = self.vertex_color_structure(vertex) + representations = tuple( + self.color_rep(particle) for particle in vertex.particles + ) + if closure: + if all(abs(value) == 1 for value in representations[:2]): + closure_kind = None + elif {representations[0], representations[1]} == {3, -3}: + closure_kind = "open-string" + elif representations[0] == representations[1] == 8: + closure_kind = "trace" + else: + raise ValueError( + "built-in recurrence closure has unsupported color inputs " + f"{representations[:2]!r}" + ) + return RecurrenceLCColorTransitionContract( + rule_kind, + ( + RecurrenceLCColorWitnessContract( + input_permutation=(0, 1), + reverse_parent_mask=0, + component_operation="close", + result_component_kind=closure_kind, + result_component_role="none", + ), + ), + ) + + def witness( + permutation: tuple[int, int], + operation: RecurrenceLCColorOperation, + result_component: RecurrenceLCColorComponentKind | None, + factor: tuple[float, float] = (1.0, 0.0), + ) -> RecurrenceLCColorWitnessContract: + result_role = ( + "passive" + if operation == "concatenate-join" and abs(representations[2]) == 1 + else "active" + if operation + in {"concatenate-join", "inherit-left", "inherit-right"} + else "none" + ) + return RecurrenceLCColorWitnessContract( + input_permutation=permutation, + reverse_parent_mask=0, + component_operation=operation, + result_component_kind=result_component, + result_component_role=result_role, + exact_factor=factor, + ) + + if rule_kind == "adjoint-structure-constant": + return RecurrenceLCColorTransitionContract( + rule_kind, + ( + witness((0, 1), "concatenate-join", "adjoint-segment"), + witness( + (1, 0), + "concatenate-join", + "adjoint-segment", + (-1.0, 0.0), + ), + ), + ) + if rule_kind == "fundamental-generator": + orientation = { + # The open color word is always fundamental -> adjoints -> + # antifundamental, independently of the Lorentz input order. + 4: ((1, 0), "open-string"), + 5: ((0, 1), "open-string"), + 6: ((0, 1), "open-string"), + 7: ((1, 0), "open-string"), + 9: ((1, 0), "adjoint-segment"), + }.get(vertex.kind) + if orientation is None: + raise ValueError( + f"built-in fundamental-generator kernel {vertex.kind} has no " + "compiler-owned LC orientation" + ) + permutation, result_component = orientation + return RecurrenceLCColorTransitionContract( + rule_kind, + (witness(permutation, "concatenate-join", result_component),), + ) + if rule_kind == "color-identity": + colored_inputs = tuple( + index + for index, representation in enumerate(representations[:2]) + if abs(representation) != 1 + ) + if len(colored_inputs) == 0: + operation = "empty" + permutation = (0, 1) + result_component = None + elif len(colored_inputs) == 1: + operation = ( + "inherit-left" if colored_inputs[0] == 0 else "inherit-right" + ) + permutation = (0, 1) + result_component = None + elif len(colored_inputs) == 2 and abs(representations[2]) == 1: + operation = "concatenate-join" + permutation = ( + (0, 1) + if representations[0] == 3 or representations[1] == -3 + else (1, 0) + ) + result_component = "open-string" + else: + raise ValueError( + "built-in color-identity kernel has unsupported recurrence " + f"representations {representations!r}" + ) + return RecurrenceLCColorTransitionContract( + rule_kind, + (witness(permutation, operation, result_component),), + ) + if rule_kind == "singlet": + return RecurrenceLCColorTransitionContract( + rule_kind, + (witness((0, 1), "empty", None),), + ) + raise ValueError( + f"built-in kernel {vertex.kind} has unsupported recurrence color rule " + f"{rule_kind!r}" + ) + + def vertex_color_structure(self, vertex: Vertex) -> str: + """Return the exact built-in tensor family for one oriented vertex.""" + + representations = tuple( + self.color_rep(particle) for particle in vertex.particles + ) + absolute = tuple(abs(value) for value in representations) + if all(value == 1 for value in absolute): + return "singlet" + if all(value == 8 for value in representations): + if vertex.kind not in {0, 1, 2, 3}: + raise ValueError( + f"built-in vertex kind {vertex.kind} has an unknown adjoint tensor" + ) + return "adjoint-structure-constant" + if sorted(absolute) == [1, 3, 3]: + return "color-identity" + if sorted(absolute) == [3, 3, 8]: + if vertex.kind not in {4, 5, 6, 7, 9}: + raise ValueError( + f"built-in vertex kind {vertex.kind} has an unknown " + "quark-gluon tensor" + ) + return "fundamental-generator" + raise ValueError( + f"built-in vertex kind {vertex.kind} has unsupported color representations " + f"{representations!r}" + ) + + def is_fundamental_colored_fermion(self, pdg: int) -> bool: + return self.is_quark(pdg) or self.is_antiquark(pdg) + + def is_massless_adjoint_vector(self, pdg: int) -> bool: + return pdg == 21 + + def is_gluon(self, pdg: int) -> bool: + return pdg in (21, 99) + + def is_singlet(self, pdg: int) -> bool: + return not (abs(pdg) <= 6 or pdg == 21) + + def is_tensor(self, pdg: int) -> bool: + return pdg in (-21, -23, 26, -26) + + def is_massive_boson(self, pdg: int) -> bool: + return pdg == 23 or abs(pdg) == 24 + + def is_photon(self, pdg: int) -> bool: + return pdg == 22 + + def is_higgs(self, pdg: int) -> bool: + return pdg == 25 + + def quantum_number_flow(self, particle_id: int) -> QuantumNumberFlow: + particle = self.particle(particle_id) + expression = _ELECTRIC_CHARGE_BY_PARTICLE.get(particle.pdg, "0") + if expression != "0" and self._property_sign(particle_id) < 0: + expression = ( + expression.removeprefix("-") + if expression.startswith("-") + else f"-{expression}" + ) + return (("electric_charge", expression),) + + def _particle_identity_ir(self, particle_id: int) -> ParticleIdentityIR: + particle_id = int(particle_id) + anti_particle_id = int(self.anti_particle(particle_id)) + particle = self.particle(particle_id) + display_name = _display_name(particle_id) + anti_display_name = _display_name(anti_particle_id) + species_name = _display_name(particle.pdg) + return ParticleIdentityIR( + canonical_id=f"model:{self.name}:state:{display_name}", + species_id=f"model:{self.name}:species:{species_name}", + anti_canonical_id=f"model:{self.name}:state:{anti_display_name}", + display_name=display_name, + anti_display_name=anti_display_name, + pdg_label=particle_id, + anti_pdg_label=anti_particle_id, + orientation=cast(ParticleOrientation, self.source_orientation(particle_id)), + self_conjugate=particle_id == anti_particle_id, + ) + + def propagator_lowering_rule( + self, + particle_id: int, + chirality: int = 0, + ) -> PropagatorLoweringRule: + if self.is_tensor(particle_id): + return PropagatorLoweringRule( + particle_id=particle_id, + chirality=chirality, + backend="identity", + full_tensor_network_ready=True, + applies_propagator=False, + kernel="auxiliary_tensor_embedded_propagator", + kind="identity", + mass_class="not-applicable", + auxiliary_policy="antisymmetric-tensor", + description=( + "auxiliary-tensor propagator factors are embedded in the " + "adjacent built-in-SM vertex kernels" + ), + ) + if particle_id in (125, 126, 127): + return PropagatorLoweringRule( + particle_id=particle_id, + chirality=chirality, + backend="identity", + full_tensor_network_ready=True, + applies_propagator=False, + kernel="auxiliary_scalar_no_propagator", + kind="identity", + mass_class="not-applicable", + auxiliary_policy="builtin-higgsor-scalar", + description=( + "Higgsor auxiliary scalar currents are non-propagating in " + "the built-in-SM model" + ), + ) + if particle_id in (21, 22, 99): + return PropagatorLoweringRule( + particle_id=particle_id, + chirality=chirality, + backend="symbolica", + full_tensor_network_ready=True, + applies_propagator=True, + kernel="massless_vector_feynman_gauge", + kind="vector", + mass_class="massless", + gauge="feynman", + description="massless vector propagator in mostly-minus metric", + ) + if abs(particle_id) == 24 or particle_id == 23: + return PropagatorLoweringRule( + particle_id=particle_id, + chirality=chirality, + backend="symbolica", + full_tensor_network_ready=True, + applies_propagator=True, + kernel="massive_vector_unitary_gauge", + kind="vector", + mass_class="massive", + gauge="unitary", + goldstone_policy="absorbed", + description="massive vector propagator with width", + ) + if self.is_chiral_eligible(particle_id) and chirality != 0: + return PropagatorLoweringRule( + particle_id=particle_id, + chirality=chirality, + backend="spenso", + full_tensor_network_ready=True, + applies_propagator=True, + kernel="weyl_fermion", + kind="weyl-fermion", + mass_class="massless", + description="massless Weyl fermion propagator", + ) + if self.is_fermion(particle_id) and ( + self.mass(particle_id) != 0.0 or chirality == 0 + ): + return PropagatorLoweringRule( + particle_id=particle_id, + chirality=chirality, + backend="symbolica", + full_tensor_network_ready=True, + applies_propagator=True, + kernel=( + "massive_dirac_fermion" + if self.mass(particle_id) != 0.0 + else "massless_dirac_fermion" + ), + kind="dirac-fermion", + mass_class=("massive" if self.mass(particle_id) != 0.0 else "massless"), + description=( + "massive Dirac fermion propagator" + if self.mass(particle_id) != 0.0 + else "massless full-Dirac fermion propagator" + ), + ) + if self.is_higgs(particle_id): + return PropagatorLoweringRule( + particle_id=particle_id, + chirality=chirality, + backend="symbolica", + full_tensor_network_ready=True, + applies_propagator=True, + kernel="scalar_with_width", + kind="scalar", + mass_class="massive", + description="scalar propagator with optional width", + ) + return PropagatorLoweringRule( + particle_id=particle_id, + chirality=chirality, + backend="unimplemented", + full_tensor_network_ready=False, + applies_propagator=True, + kernel="unknown", + kind="unsupported", + mass_class=("massive" if self.mass(particle_id) != 0.0 else "massless"), + description="no built-in-model propagator lowering is registered", + ) + + def auxiliary_kind(self, particle_id: int) -> str | None: + if particle_id == 99: + return "u1-subtraction-color-flow-vector" + if self.is_tensor(particle_id): + return "antisymmetric-tensor" + return None + + def _install_builtin_contractions(self) -> None: + """Install the built-in model's explicit current bilinear forms.""" + + direct: dict[tuple[int, int, int, int], ContractionIR | None] = {} + closure: dict[tuple[int, int], ContractionIR | None] = {} + particle_ids = tuple(sorted(self._species_by_pdg)) + for left_particle_id in particle_ids: + right_particle_id = self.anti_particle(left_particle_id) + for left_chirality in self._builtin_contraction_chiralities( + left_particle_id + ): + for right_chirality in self._builtin_contraction_chiralities( + right_particle_id + ): + contraction_ir = self._builtin_direct_contraction_ir( + left_particle_id, + right_particle_id, + left_chirality=left_chirality, + right_chirality=right_chirality, + ) + if contraction_ir is not None: + direct[ + ( + left_particle_id, + left_chirality, + right_particle_id, + right_chirality, + ) + ] = contraction_ir + for chirality in self._builtin_contraction_chiralities(left_particle_id): + if self.current_dimension(left_particle_id, chirality) == 1: + closure[(left_particle_id, chirality)] = ContractionIR( + name="scalar", + left_basis="scalar", + right_basis="scalar", + coefficients=((1.0, 0.0),), + chirality_relation="any", + metric_signature=None, + ) + self._direct_contraction_ir_by_state = direct + self._closure_contraction_ir_by_state = closure + + def _builtin_contraction_chiralities(self, particle_id: int) -> tuple[int, ...]: + if self.is_chiral_eligible(particle_id): + return (-1, 0, 1) + return (0,) + + def _builtin_direct_contraction_ir( + self, + left_particle_id: int, + right_particle_id: int, + *, + left_chirality: int, + right_chirality: int, + ) -> ContractionIR | None: + left_dimension = self.current_dimension(left_particle_id, left_chirality) + right_dimension = self.current_dimension(right_particle_id, right_chirality) + if left_dimension != right_dimension: + return None + + chirality_relation: Literal["any", "opposite"] = "any" + metric_signature: str | None = None + if left_dimension == 1: + name = "scalar" + coefficients = ((1.0, 0.0),) + elif left_dimension == 2: + if left_chirality != -right_chirality: + return None + name = "weyl" + coefficients = ((1.0, 0.0), (1.0, 0.0)) + chirality_relation = "opposite" + elif left_dimension == 4: + if self.is_fermion(left_particle_id) != self.is_fermion(right_particle_id): + return None + if self.is_fermion(left_particle_id): + name = "dirac" + coefficients = ((1.0, 0.0),) * 4 + else: + name = "lorentz" + coefficients = ( + (1.0, 0.0), + (-1.0, 0.0), + (-1.0, 0.0), + (-1.0, 0.0), + ) + metric_signature = "mostly-minus" + elif left_dimension == 6: + name = "antisymmetric-tensor" + coefficients = ((1.0, 0.0),) * 6 + else: + return None + return ContractionIR( + name=name, + left_basis=self._current_basis(left_particle_id, left_chirality), + right_basis=self._current_basis(right_particle_id, right_chirality), + coefficients=coefficients, + chirality_relation=chirality_relation, + metric_signature=metric_signature, + ) + + +@dataclass +class BuiltinSMModel(BuiltinSMLoweringMixin, BuiltinSMDefinitionMixin, BuiltinModel): + """Built-in Standard Model production path with pinned reference conventions.""" + + name: str = "built-in-sm-leading-color" + alpha_s_mz: float = 0.119 + alpha_s_me_check: float = 0.118 + alpha_ew: float = 1.0 / 132.507 + sin_weak: float = 0.47143025548407230 + sqrt_s: float = 14000.0 + + def __post_init__(self) -> None: + self.particles = { + particle.pdg: particle for particle in self._build_particles() + } + self.vertices = tuple(self._build_vertices()) + self._install_builtin_contractions() + + @cached_property + def cos_weak(self) -> float: + return math.sqrt(1.0 - self.sin_weak**2) + + def weak_coupling(self) -> float: + return 1.0 / self.sin_weak + + def neutral_gauge_coupling(self) -> float: + return self.weak_coupling() * self.cos_weak + + def charged_current_coupling(self) -> float: + return self.weak_coupling() / math.sqrt(2.0) + + def weak_coupling_over_cosine(self) -> float: + return self.weak_coupling() / self.cos_weak + + def photon_fermion_coupling(self, pdg: int) -> tuple[float, float]: + particle = self.particle(pdg) + return particle.charge, particle.charge + + def z_fermion_coupling(self, pdg: int) -> tuple[float, float]: + particle = self.particle(pdg) + charge = particle.charge + left = particle.weak_isospin[0] + right = particle.weak_isospin[1] + prefactor = self.weak_coupling_over_cosine() + return ( + prefactor * (left - charge * self.sin_weak**2), + prefactor * (right - charge * self.sin_weak**2), + ) + + def leading_color_factor(self, process: Iterable[int]) -> int: + exponent_twice = 0 + for pdg in process: + if pdg == 21: + exponent_twice += 2 + elif 1 <= abs(pdg) <= 6: + exponent_twice += 1 + if exponent_twice % 2: + raise ValueError(f"non-integer leading-color exponent for {tuple(process)}") + return 3 ** (exponent_twice // 2) + + def runtime_normalization_payload(self, dag: Any) -> dict[str, object]: + """Preserve the established built-in-SM normalization convention.""" + + initial = tuple(int(pdg) for pdg in dag.process.initial_pdgs) + final = tuple(int(pdg) for pdg in dag.process.final_pdgs) + average_factor = 1 + for pdg in initial: + if pdg == 21: + average_factor *= 16 + elif 1 <= abs(pdg) <= 6: + average_factor *= 6 + else: + average_factor *= 2 + identical_factor = math.prod( + math.factorial(count) for count in Counter(final).values() + ) + electroweak_power = ( + max(1, len(dag.process.singlet_labels)) if dag.process.singlet_labels else 0 + ) + qcd_power = max(0, len(dag.process.legs) - 2 - electroweak_power) + return { + "color_accuracy": dag.process.color_accuracy, + "color_factor": int(self.leading_color_factor((*initial, *final))), + "average_factor": average_factor, + "identical_factor": identical_factor, + "global_coupling_factor": ( + (4.0 * math.pi * self.alpha_s_me_check) ** qcd_power + * (2.0 * 4.0 * math.pi * self.alpha_ew) ** electroweak_power + ), + "qcd_coupling_power": qcd_power, + "electroweak_coupling_power": electroweak_power, + "couplings_in_stage_evaluators": True, + "coupling_policy": "stage evaluators include local vertex couplings", + } + + def runtime_normalization_parameter_defaults(self) -> dict[str, float]: + """Expose the built-in SM coupling normalization as runtime inputs.""" + + return { + "normalization.alpha_s_me_check": float(self.alpha_s_me_check), + "normalization.alpha_ew": float(self.alpha_ew), + } + + def skip_duplicate_vertex_orientation(self, vertex: Vertex) -> bool: + """Skip mirrored model-table entries already covered by DAG sweeps.""" + + return False + + def vertex_coupling_orders(self, vertex: Vertex) -> CouplingOrders: + """Classify built-in SM vertices by UFO-style coupling order.""" + + if vertex.kind in {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}: + return (("QCD", 1),) + return (("QED", 1),) + + def global_helicity_flip_equivalence_is_proven( + self, + vertices: Iterable[Vertex], + ) -> bool: + """Use the pinned built-in-SM QCD kernel inventory as the proof source.""" + + for vertex in vertices: + orders = self.vertex_coupling_orders(vertex) + if not orders and self.vertex_is_internal_contact_fragment(vertex): + continue + if not orders or any(name != "QCD" for name, _value in orders): + return False + return True + + def pure_massless_adjoint_helicity_zero_rule_is_proven( + self, + process: Any, + vertices: Iterable[Vertex], + ) -> bool: + """Prove the tree-level Yang--Mills helicity-zero rule. + + The proof is deliberately tied to the built-in model's registered + massless-adjoint and auxiliary-tensor kernels. A QCD coupling-order + label alone would also admit quark vertices and is not sufficient. + """ + + legs = tuple(getattr(process, "legs", ())) + if not legs or any( + getattr(leg, "outgoing_pdg", None) is None + or not self.is_massless_adjoint_vector(int(leg.outgoing_pdg)) + or self.mass(int(leg.outgoing_pdg)) != 0.0 + for leg in legs + ): + return False + + pure_gauge_inventory = { + vertex + for vertex in self.vertices + if all( + self.is_massless_adjoint_vector(particle_id) + or self.auxiliary_kind(particle_id) == "antisymmetric-tensor" + for particle_id in vertex.particles + ) + } + used_vertices = tuple(vertices) + return bool(used_vertices) and all( + vertex in pure_gauge_inventory for vertex in used_vertices + ) + + def adjoint_current_reflection_phase( + self, + vertex: Vertex, + ) -> tuple[float, float] | None: + """Expose the pinned three-gauge-boson current antisymmetry.""" + + if vertex.kind != 0 or not all( + self.is_massless_adjoint_vector(particle_id) + for particle_id in vertex.particles + ): + return None + return (-1.0, 0.0) + + def lc_trace_reflection_equivalence_is_proven(self, process: Any) -> bool: + """Prove reflection folding for the pinned pure-gluon implementation.""" + + legs = tuple(getattr(process, "legs", ())) + return bool(legs) and all( + getattr(leg, "outgoing_pdg", None) is not None + and self.is_massless_adjoint_vector(int(leg.outgoing_pdg)) + for leg in legs + ) + + def shared_single_trace_color_basis_is_proven(self, process: Any) -> bool: + """Prove the shared NLC/full trace basis for built-in Yang--Mills.""" + + return self.lc_trace_reflection_equivalence_is_proven(process) + + def adjoint_tree_color_basis_is_proven( + self, + process: Any, + *, + max_coupling_orders: Mapping[str, int] | None = None, + ) -> bool: + """Certify only the pinned implementation's connected pure-gluon trees.""" + + del max_coupling_orders + # Subclasses may change the interaction inventory and must prove their + # own certificate rather than inherit the pinned SM's reflection hook. + return ( + type(self) is BuiltinSMModel + and self.lc_trace_reflection_equivalence_is_proven(process) + ) + + def coupling_order_hierarchies(self) -> dict[str, int]: + return {"QCD": 1, "QED": 2} + + +def _display_name(particle_id: int) -> str: + candidates = [name for name, pdg in PDGS.items() if int(pdg) == particle_id] + if not candidates: + return f"pdg_{particle_id}" + return min(candidates, key=lambda name: (len(name), name)) + + +__all__ = ["BuiltinModel", "BuiltinSMModel"] diff --git a/pyamplicol/src/pyamplicol/models/builtin/process_catalog.py b/pyamplicol/src/pyamplicol/models/builtin/process_catalog.py new file mode 100644 index 0000000000..2ddac7cfd1 --- /dev/null +++ b/pyamplicol/src/pyamplicol/models/builtin/process_catalog.py @@ -0,0 +1,335 @@ +# SPDX-License-Identifier: 0BSD +"""Legacy built-in Standard Model process catalog.""" + +from __future__ import annotations + +from dataclasses import dataclass + +from ...processes.core_syntax import ( + _anonymous_options, + _split_repeat_token, + _tokenize_side, +) +from .process_types import BuiltinParsedProcess as ParsedProcess +from .process_types import ( + BuiltinProcessEnumeration as ProcessEnumeration, +) +from .process_types import ( + BuiltinSubprocessRecord as SubprocessRecord, +) + +QUARKS = frozenset({"d", "u", "s", "c", "b", "t"}) +ANTIQUARKS = frozenset({f"{q}~" for q in QUARKS}) +SINGLETS = frozenset( + { + "a", + "z", + "w+", + "w-", + "e+", + "e-", + "mu+", + "mu-", + "ta+", + "ta-", + "ve", + "ve~", + "vm", + "vm~", + "vt", + "vt~", + "h", + } +) +GLUONS = frozenset({"g"}) +ALL_COLOURED = QUARKS | ANTIQUARKS | GLUONS +PDGS = { + "g": "21", + "d": "1", + "u": "2", + "s": "3", + "c": "4", + "b": "5", + "t": "6", + "d~": "-1", + "u~": "-2", + "s~": "-3", + "c~": "-4", + "b~": "-5", + "t~": "-6", + "a": "22", + "z": "23", + "w+": "24", + "w-": "-24", + "e+": "-11", + "e-": "11", + "mu+": "-13", + "mu-": "13", + "ta+": "-15", + "ta-": "15", + "ve": "12", + "ve~": "-12", + "vm": "14", + "vm~": "-14", + "vt": "16", + "vt~": "-16", + "h": "25", +} +ANTI_PARTICLE = { + "g": "g", + "d": "d~", + "u": "u~", + "s": "s~", + "c": "c~", + "b": "b~", + "t": "t~", + "d~": "d", + "u~": "u", + "s~": "s", + "c~": "c", + "b~": "b", + "t~": "t", + "a": "a", + "z": "z", + "w+": "w-", + "w-": "w+", + "e+": "e-", + "e-": "e+", + "mu+": "mu-", + "mu-": "mu+", + "ta+": "ta-", + "ta-": "ta+", + "ve": "ve~", + "ve~": "ve", + "vm": "vm~", + "vm~": "vm", + "vt": "vt~", + "vt~": "vt", + "h": "h", +} +SORT_PARTICLES = { + "g": 13, + "d": 1, + "u": 2, + "s": 3, + "c": 4, + "b": 5, + "t": 6, + "d~": 7, + "u~": 8, + "s~": 9, + "c~": 10, + "b~": 11, + "t~": 12, + "a": 80, + "z": 81, + "w+": 82, + "w-": 83, + "e+": 84, + "e-": 85, + "mu+": 86, + "mu-": 87, + "ta+": 88, + "ta-": 89, + "ve": 90, + "ve~": 91, + "vm": 92, + "vm~": 93, + "vt": 94, + "vt~": 95, + "h": 96, +} +CHARGES3 = { + "g": 0, + "d": -1, + "u": 2, + "s": -1, + "c": 2, + "b": -1, + "t": 2, + "d~": 1, + "u~": -2, + "s~": 1, + "c~": -2, + "b~": 1, + "t~": -2, + "a": 0, + "z": 0, + "w+": 3, + "w-": -3, + "e+": 3, + "e-": -3, + "mu+": 3, + "mu-": -3, + "ta+": 3, + "ta-": -3, + "ve": 0, + "ve~": 0, + "vm": 0, + "vm~": 0, + "vt": 0, + "vt~": 0, + "h": 0, +} +FAMILY = { + "g": 0, + "d": 1, + "u": 1, + "s": 11, + "c": 11, + "b": 21, + "t": 21, + "d~": -1, + "u~": -1, + "s~": -11, + "c~": -11, + "b~": -21, + "t~": -21, + "a": 0, + "z": 0, + "w+": 0, + "w-": 0, + "e+": -31, + "e-": 31, + "mu+": -41, + "mu-": 41, + "ta+": -51, + "ta-": 51, + "ve": 31, + "ve~": -31, + "vm": 41, + "vm~": -41, + "vt": 51, + "vt~": -51, + "h": 0, +} + + +@dataclass(frozen=True) +class ParticleSelectionMetadata: + name: str + pdg: int + anti_name: str + charge3: int + family: int + color_rep: int + is_fermion: bool + is_antiparticle: bool + + @property + def is_colored(self) -> bool: + return self.color_rep != 1 + + +def selection_metadata(name: str) -> ParticleSelectionMetadata: + return _PARTICLE_SELECTION_METADATA[name] + + +def _metadata_color_rep(name: str) -> int: + if name in QUARKS: + return 3 + if name in ANTIQUARKS: + return -3 + if name in GLUONS: + return 8 + return 1 + + +_PARTICLE_SELECTION_METADATA = { + name: ParticleSelectionMetadata( + name=name, + pdg=int(PDGS[name]), + anti_name=ANTI_PARTICLE[name], + charge3=CHARGES3[name], + family=FAMILY[name], + color_rep=_metadata_color_rep(name), + is_fermion=name in QUARKS + or name in ANTIQUARKS + or abs(int(PDGS[name])) in range(11, 17), + is_antiparticle=name in ANTIQUARKS or int(PDGS[name]) < 0, + ) + for name in PDGS +} + + +def process_uses_inclusive_labels(process: str) -> bool: + parts = process.lower().replace("bar", "~").split(">") + if len(parts) != 2: + raise ValueError("invalid collision format; expected 'initial > final'") + for token in (*_tokenize_side(parts[0].strip()), *_tokenize_side(parts[1].strip())): + _, item = _split_repeat_token(token) + if item in {"p", "j"}: + return True + if ( + item.startswith("[") + and item.endswith("]") + and any(option in {"p", "j"} for option in _anonymous_options(item)) + ): + return True + return False + + +def request_allows_charged_current(request: ParsedProcess) -> bool: + """Return whether built-in request quantum numbers imply charged-current flow.""" + + if "w+" in request.rest or "w-" in request.rest: + return True + non_qcd_rest = [ + particle + for particle in request.rest + if particle not in ALL_COLOURED and particle in CHARGES3 + ] + return abs(sum(CHARGES3[particle] for particle in non_qcd_rest)) == 3 + + +def concrete_processes_from_inclusive_enumeration( + enumeration: ProcessEnumeration, +) -> tuple[str, ...]: + processes: dict[str, None] = {} + for group in enumeration.groups: + for record in group.records: + processes.setdefault(_record_to_physical_process(record), None) + return tuple(sorted(processes, key=_concrete_process_sort_key)) + + +def _record_to_physical_process(record: SubprocessRecord) -> str: + initial = tuple(ANTI_PARTICLE[p] for p in record.process[:2]) + final = record.process[2:] + return f"{' '.join(initial)} > {' '.join(final)}" + + +def _concrete_process_sort_key(process: str) -> tuple[object, ...]: + initial, _, final = process.partition(">") + initial_tokens = tuple(_tokenize_side(initial.strip())) + final_tokens = tuple(_tokenize_side(final.strip())) + q_qbar_first = not ( + len(initial_tokens) == 2 + and not initial_tokens[0].endswith("~") + and initial_tokens[1].endswith("~") + ) + return ( + q_qbar_first, + tuple(SORT_PARTICLES.get(token, 999) for token in initial_tokens), + tuple(SORT_PARTICLES.get(token, 999) for token in final_tokens), + initial_tokens, + final_tokens, + ) + + +__all__ = [ + "ALL_COLOURED", + "ANTIQUARKS", + "ANTI_PARTICLE", + "CHARGES3", + "FAMILY", + "GLUONS", + "PDGS", + "QUARKS", + "SINGLETS", + "SORT_PARTICLES", + "ParticleSelectionMetadata", + "concrete_processes_from_inclusive_enumeration", + "process_uses_inclusive_labels", + "request_allows_charged_current", + "selection_metadata", +] diff --git a/pyamplicol/src/pyamplicol/models/builtin/process_enumeration.py b/pyamplicol/src/pyamplicol/models/builtin/process_enumeration.py new file mode 100644 index 0000000000..8a5a227a7c --- /dev/null +++ b/pyamplicol/src/pyamplicol/models/builtin/process_enumeration.py @@ -0,0 +1,646 @@ +# SPDX-License-Identifier: 0BSD +"""Legacy built-in-SM concrete subprocess enumeration.""" + +from __future__ import annotations + +import itertools +import math +import re +from collections import Counter +from collections.abc import Iterable, Sequence +from dataclasses import replace + +from ...processes.core_syntax import ( + OrderTuple, + ProcessTuple, + _tokenize_side, + expand_process_variants, +) +from .process_catalog import ( + ALL_COLOURED, + ANTI_PARTICLE, + ANTIQUARKS, + CHARGES3, + FAMILY, + GLUONS, + PDGS, + QUARKS, + SINGLETS, + SORT_PARTICLES, +) +from .process_catalog import ( + request_allows_charged_current as _request_allows_charged_current, +) +from .process_types import BuiltinParsedProcess as ParsedProcess +from .process_types import ( + BuiltinPhaseSpaceGroup as PhaseSpaceGroup, +) +from .process_types import ( + BuiltinProcessEnumeration as ProcessEnumeration, +) +from .process_types import ( + BuiltinProcessOptions as ProcessOptions, +) +from .process_types import ( + BuiltinSubprocessRecord as SubprocessRecord, +) + + +def _ordered_compositions(total: int, parts: int) -> tuple[tuple[int, ...], ...]: + if parts <= 0: + return ((),) if total == 0 else () + if parts == 1: + return ((total,),) + return tuple( + (first, *rest) + for first in range(total + 1) + for rest in _ordered_compositions(total - first, parts - 1) + ) + + +def _chunk_sequence( + values: Sequence[int], + chunk_lengths: Sequence[int], +) -> tuple[tuple[int, ...], ...]: + chunks: list[tuple[int, ...]] = [] + start = 0 + for length in chunk_lengths: + chunks.append(tuple(values[start : start + length])) + start += length + return tuple(chunks) + + +class ProcessEnumerator: + """Structured port of the legacy process_list.py enumeration logic.""" + + def __init__(self, options: ProcessOptions | None = None) -> None: + self.options = options or ProcessOptions() + self.flavour_scheme = self._flavour_scheme(self.options.flavour_scheme) + self.massless_qcd = ( + self.flavour_scheme + | frozenset(f"{q}~" for q in self.flavour_scheme) + | GLUONS + ) + self.proton = self.massless_qcd + self.jet = self.massless_qcd + self._phase_space_orders: dict[OrderTuple, list[SubprocessRecord]] = {} + self._all_keys_sorted: list[OrderTuple] = [] + self._process_order_to_index: dict[tuple[ProcessTuple, OrderTuple], int] = {} + + def parse(self, process_string: str) -> ParsedProcess: + variants = expand_process_variants(process_string) + if len(variants) != 1: + raise ValueError( + "ProcessEnumerator.parse expects one concrete process; " + "use enumerate_process_set for process sets or multiparticle expansion" + ) + input_string = variants[0].lower().replace("bar", "~") + parts = input_string.split(">") + if len(parts) != 2: + raise ValueError("invalid collision format; expected 'initial > final'") + + initial_state = _tokenize_side(parts[0].strip()) + if len(initial_state) != 2: + raise ValueError("exactly two incoming particles are required") + crossed_initial = tuple( + ANTI_PARTICLE[p] if p not in {"p", "j"} else p for p in initial_state + ) + + final_state = _tokenize_side(parts[1].strip()) + jet_count = 0 + rest: list[str] = [] + for token in final_state: + jet_match = re.fullmatch(r"(\d+)j", token) + if token == "j": + jet_count += 1 + elif jet_match: + jet_count += int(jet_match.group(1)) + else: + rest.append(token) + leptons: list[str] = [] + if self.options.include_resonance: + leptons = [p for p in rest if 11 <= abs(int(PDGS[p])) <= 16] + for lepton in leptons: + rest.remove(lepton) + charge = sum(CHARGES3[p] for p in leptons) + if charge == 0: + rest.append("z") + elif charge < 0: + rest.append("w-") + else: + rest.append("w+") + + self._validate_particles([*crossed_initial, *rest]) + return ParsedProcess(crossed_initial, jet_count, tuple(rest), tuple(leptons)) + + def enumerate(self, process_string: str) -> ProcessEnumeration: + request = self.parse(process_string) + unique_processes = self._generate_all_unique_processes(request) + subprocesses = self._generate_all_processes(unique_processes, request) + self._phase_space_orders = self._combine_results( + self._process_subprocess(proc) for proc in subprocesses + ) + self._all_keys_sorted = sorted(self._phase_space_orders) + self._determine_multichannel_partners_and_symmetry_factor() + self._check_consistency() + + groups = tuple( + PhaseSpaceGroup( + group_id=i + 1, + phase_space_order=key, + records=tuple( + sorted(self._phase_space_orders[key], key=self._sort_record) + ), + ) + for i, key in enumerate(self._all_keys_sorted) + ) + return ProcessEnumeration( + request=request, + options=self.options, + unique_processes=tuple( + tuple(proc) + for proc in sorted( + ( + tuple(sorted(p, key=lambda x: SORT_PARTICLES[x])) + for p in unique_processes + ), + key=self._sort_process, + ) + ), + groups=groups, + ) + + def enumerate_color_complete(self, process_string: str) -> ProcessEnumeration: + """Enumerate a reference-only colour-complete legacy process file. + + The production LC process list deliberately removes colour-order + representatives related by legacy symmetries. That is correct for + ordinary integration and LC library timing, but NLC/full-colour + generated-library validation needs raw amplitudes for every colour + basis row used by AmpliCol's colour matrix. This reference path keeps + all model-compatible candidate colour words in a single phase-space + group and avoids process-family assumptions. + """ + + request = self.parse(process_string) + unique_processes = self._generate_all_unique_processes(request) + subprocesses = self._generate_all_processes(unique_processes, request) + records: list[SubprocessRecord] = [] + seen: set[tuple[ProcessTuple, OrderTuple]] = set() + for proc in sorted(subprocesses, key=self._sort_process): + for perm in self._candidate_color_orders(proc): + order = tuple(perm) + key = (proc, order) + if key in seen: + continue + seen.add(key) + records.append( + SubprocessRecord( + process=proc, + color_order=order, + multichannel_partners=(0,), + identical_factor=self._identical_particle_symmetry_factor(proc), + ) + ) + if not records: + return ProcessEnumeration( + request=request, + options=self.options, + unique_processes=(), + groups=(), + ) + return ProcessEnumeration( + request=request, + options=self.options, + unique_processes=tuple( + tuple(proc) + for proc in sorted( + ( + tuple(sorted(p, key=lambda x: SORT_PARTICLES[x])) + for p in unique_processes + ), + key=self._sort_process, + ) + ), + groups=( + PhaseSpaceGroup( + group_id=1, + phase_space_order=tuple(range(len(records[0].process))), + records=tuple(records), + ), + ), + ) + + def _flavour_scheme(self, value: int) -> frozenset[str]: + flavours = ("d", "u", "s", "c", "b", "t") + if not 1 <= value <= 6: + raise ValueError(f"unknown flavour scheme: {value}") + return frozenset(flavours[:value]) + + def _validate_particles(self, particles: Iterable[str]) -> None: + unknown = sorted(set(particles).difference(PDGS).difference({"p", "j"})) + if unknown: + raise ValueError(f"unknown particle name(s): {unknown}") + + def _valid_color_order(self, proc: ProcessTuple, perm: OrderTuple) -> bool: + found_quark = found_antiquark = found_singlet = found_gluon = found_first = ( + False + ) + quark_idx = -1 + for idx in perm: + if idx == 0: + found_first = True + particle = proc[idx] + if particle in QUARKS: + if found_quark or found_gluon: + return False + found_quark = True + found_antiquark = found_singlet = found_gluon = False + quark_idx = idx + elif particle in ANTIQUARKS: + if found_antiquark or found_singlet or not found_quark: + return False + if not found_first: + return False + found_antiquark = True + found_quark = found_singlet = found_gluon = False + _ = quark_idx + elif particle in GLUONS: + if found_antiquark or found_singlet: + return False + found_gluon = True + else: + if found_quark or found_gluon or not found_antiquark: + return False + found_singlet = True + return not (found_gluon and perm[0] != 0) + + def _unique_color_order(self, proc: ProcessTuple, perm: OrderTuple) -> bool: + zero = perm.index(0) + perm_mapped = perm[zero:] + perm[:zero] + for particle in ALL_COLOURED: + particle_positions = [ + i + 2 for i, part in enumerate(proc[2:]) if part == particle + ] + previous_position = 0 + for position in particle_positions: + mapped_position = perm_mapped.index(position) + if mapped_position < previous_position: + return False + previous_position = mapped_position + return True + + def _order_proc_perm( + self, proc: ProcessTuple, perm: OrderTuple + ) -> tuple[ProcessTuple, OrderTuple]: + zero = perm.index(0) + perm_mapped = list(perm[zero:] + perm[:zero]) + elements_to_order = [ + i for i in perm_mapped if proc[i] in self.massless_qcd and i > 1 + ] + indices = [perm_mapped.index(x) for x in elements_to_order] + for idx, value in zip(indices, sorted(elements_to_order), strict=True): + perm_mapped[idx] = value + perm_ordered = tuple( + perm_mapped[len(perm) - zero :] + perm_mapped[: len(perm) - zero] + ) + proc_ordered: list[str | None] = [None] * len(proc) + for i, perm_index in enumerate(perm_ordered): + proc_ordered[perm_index] = proc[perm[i]] + return tuple(x for x in proc_ordered if x is not None), perm_ordered + + def _process_subprocess( + self, proc: ProcessTuple + ) -> dict[OrderTuple, list[SubprocessRecord]]: + local: dict[OrderTuple, list[SubprocessRecord]] = {} + for perm in self._candidate_color_orders(proc): + order = tuple(perm) + if not self._valid_color_order(proc, order): + continue + if not self._unique_color_order(proc, order): + continue + ordered_proc, ordered_perm = self._order_proc_perm(proc, order) + zero = ordered_perm.index(0) + perm_mapped = tuple(ordered_perm[zero:] + ordered_perm[:zero]) + local.setdefault(perm_mapped, []).append( + SubprocessRecord(ordered_proc, ordered_perm) + ) + return local + + def _candidate_color_orders(self, proc: ProcessTuple) -> Iterable[OrderTuple]: + quark_indices = tuple( + i for i, particle in enumerate(proc) if particle in QUARKS + ) + anti_indices = tuple( + i for i, particle in enumerate(proc) if particle in ANTIQUARKS + ) + gluon_indices = tuple( + i for i, particle in enumerate(proc) if particle in GLUONS + ) + singlet_indices = tuple( + i for i, particle in enumerate(proc) if particle in SINGLETS + ) + if not quark_indices: + for tail in itertools.permutations( + index for index in gluon_indices if index != 0 + ): + yield (0, *tail) + return + + if singlet_indices: + singlet_perms = tuple(itertools.permutations(singlet_indices)) + else: + singlet_perms = ((),) + gluon_compositions = _ordered_compositions( + len(gluon_indices), + len(quark_indices), + ) + singlet_compositions = _ordered_compositions( + len(singlet_indices), + len(quark_indices), + ) + for quark_order in itertools.permutations(quark_indices): + for anti_order in itertools.permutations(anti_indices): + for gluon_perm in itertools.permutations(gluon_indices): + for gluon_chunks in gluon_compositions: + gluon_by_line = _chunk_sequence(gluon_perm, gluon_chunks) + for singlet_perm in singlet_perms: + for singlet_chunks in singlet_compositions: + singlet_by_line = _chunk_sequence( + singlet_perm, + singlet_chunks, + ) + order: list[int] = [] + for line in range(len(quark_indices)): + order.append(quark_order[line]) + order.extend(gluon_by_line[line]) + order.append(anti_order[line]) + order.extend(singlet_by_line[line]) + yield tuple(order) + + def _valid_process( + self, + proc: Sequence[str], + *, + allow_charged_current: bool | None = None, + ) -> bool: + allow_cc = ( + self.options.include_cc + if allow_charged_current is None + else allow_charged_current + ) + nq = self._count_matching(proc, QUARKS) + naq = self._count_matching(proc, ANTIQUARKS) + if nq != naq: + return False + if sum(CHARGES3[x] for x in proc) != 0: + return False + if not self.options.include_cc and sum(FAMILY[x] for x in proc) != 0: + return False + if not allow_cc and "w+" not in proc and "w-" not in proc: + for q in QUARKS: + if self._count_matching(proc, [q]) != self._count_matching( + proc, [f"{q}~"] + ): + return False + return not (nq == 0 and self._count_matching(proc, SINGLETS) > 0) + + def _compatible_unique_process( + self, request: ParsedProcess, proc: Sequence[str] + ) -> bool: + mandatory = [p for p in request.initial_state if p not in {"p", "j"}] + mandatory.extend(request.rest) + proc_local = list(proc) + try: + for particle in mandatory: + proc_local.remove(particle) + except ValueError: + return False + return True + + def _compatible_process(self, request: ParsedProcess, proc: ProcessTuple) -> bool: + proc_local = list(proc[2:]) + for i in (0, 1): + if ( + request.initial_state[i] not in {"p", "j"} + and proc[i] != request.initial_state[i] + ): + return False + try: + for particle in request.rest: + proc_local.remove(particle) + except ValueError: + return False + return True + + def _generate_all_unique_processes( + self, request: ParsedProcess + ) -> set[ProcessTuple]: + processes: list[list[str]] = [[]] + for part in request.initial_state: + if part not in {"p", "j"} and part not in self.massless_qcd: + raise ValueError( + "initial state should be a proton or massless QCD parton" + ) + + if request.jet_count == 0 and all( + part not in {"p", "j"} for part in request.initial_state + ): + fixed_process = tuple( + sorted( + (*request.initial_state, *request.rest), + key=lambda item: SORT_PARTICLES[item], + ) + ) + if self._valid_process( + fixed_process, + allow_charged_current=_request_allows_charged_current(request), + ) and self._compatible_unique_process(request, fixed_process): + return {fixed_process} + return set() + + qcd_rest = sum(1 for part in request.rest if part in self.massless_qcd) + for _part_index in range(request.jet_count + 2 + qcd_rest): + new_processes: list[list[str]] = [] + for candidate in processes: + for particle in self.massless_qcd: + new_processes.append(sorted([*candidate, particle])) + processes = new_processes + + for part in request.rest: + if part in self.massless_qcd: + continue + for candidate in processes: + candidate.append(part) + + return { + tuple(proc) + for proc in processes + if self._valid_process( + proc, + allow_charged_current=_request_allows_charged_current(request), + ) + and self._compatible_unique_process(request, proc) + } + + def _generate_all_processes( + self, unique_processes: set[ProcessTuple], request: ParsedProcess + ) -> set[ProcessTuple]: + processes: set[ProcessTuple] = set() + for proc in unique_processes: + for i, j in itertools.combinations(range(len(proc)), 2): + if proc[i] in self.jet and proc[j] in self.jet: + remaining = [ + entry for k, entry in enumerate(proc) if k not in (i, j) + ] + proc1 = (proc[i], proc[j], *remaining) + proc2 = (proc[j], proc[i], *remaining) + if self._compatible_process(request, proc1): + processes.add(proc1) + if self._compatible_process(request, proc2): + processes.add(proc2) + return processes + + def _combine_results( + self, results: Iterable[dict[OrderTuple, list[SubprocessRecord]]] + ) -> dict[OrderTuple, list[SubprocessRecord]]: + combined: dict[OrderTuple, list[SubprocessRecord]] = {} + for result in results: + for key, value in result.items(): + combined.setdefault(key, []).extend(value) + return combined + + def _determine_multichannel_partners_and_symmetry_factor(self) -> None: + self._build_process_index() + for key in self._all_keys_sorted: + for index, record in enumerate(tuple(self._phase_space_orders[key])): + self._phase_space_orders[key][index] = self._record_with_partners( + record + ) + + for key in self._all_keys_sorted: + for index, record in enumerate(tuple(self._phase_space_orders[key])): + self._phase_space_orders[key][index] = replace( + record, + identical_factor=record.identical_factor + * self._identical_particle_symmetry_factor(record.process), + ) + + def _build_process_index(self) -> None: + self._process_order_to_index = {} + for i, key in enumerate(self._all_keys_sorted): + for record in self._phase_space_orders[key]: + self._process_order_to_index[(record.process, record.color_order)] = i + + def _record_with_partners(self, record: SubprocessRecord) -> SubprocessRecord: + proc = record.process + perm = record.color_order + singlet_indices = [perm.index(i) for i, p in enumerate(proc) if p in SINGLETS] + anti_quark_indices = tuple( + perm.index(i) for i, p in enumerate(proc) if p in ANTIQUARKS + ) + nsinglets = len(singlet_indices) + if len(singlet_indices) > 1: + singlet_perms = tuple(itertools.permutations(singlet_indices)) + elif len(singlet_indices) == 1: + singlet_perms = (tuple(singlet_indices),) + else: + singlet_perms = () + + iden = float(math.factorial(max(len(anti_quark_indices) - 1, 0))) + possible: list[tuple[OrderTuple, ProcessTuple]] = [] + if not singlet_perms: + possible.append((perm, proc)) + elif anti_quark_indices: + for singlets in singlet_perms: + for chunks in _ordered_compositions( + nsinglets, + len(anti_quark_indices), + ): + starts = [0] + for chunk in chunks[:-1]: + starts.append(starts[-1] + chunk) + singlet_chunks = tuple( + singlets[start : start + chunk] + for start, chunk in zip(starts, chunks, strict=True) + ) + order_list: list[int] = [] + for i in range(len(perm)): + if i in anti_quark_indices: + anti_index = anti_quark_indices.index(i) + order_list.extend( + perm[p] + for p in ( + (anti_quark_indices[anti_index],) + + singlet_chunks[anti_index] + ) + ) + elif i not in singlet_indices: + order_list.append(perm[i]) + possible.append((tuple(order_list), proc)) + else: + raise RuntimeError( + "colour-singlet partners require at least one quark line" + ) + + partners: list[int] = [] + for possible_order, process in possible: + partner = self._process_order_to_index.get((process, possible_order)) + if partner is None: + raise RuntimeError( + "expected multichannel partner not found for " + f"{process} {possible_order}" + ) + if partner in partners: + raise RuntimeError( + f"duplicate multichannel partner for {process} {possible_order}" + ) + partners.append(partner) + return replace( + record, + multichannel_partners=tuple(sorted(partners)), + identical_factor=1.0 / iden, + ) + + def _identical_particle_symmetry_factor(self, proc: ProcessTuple) -> float: + factor = 1.0 + for particle in ALL_COLOURED: + factor *= max(1, math.factorial(proc[2:].count(particle))) + return factor + + def _check_consistency(self) -> None: + all_processes: dict[ProcessTuple, float] = {} + for key in self._all_keys_sorted: + for record in self._phase_space_orders[key]: + proc = ( + record.process[0], + record.process[1], + *sorted(record.process[2:], key=lambda x: SORT_PARTICLES[x]), + ) + all_processes[proc] = all_processes.get(proc, 0.0) + ( + record.identical_factor / len(record.multichannel_partners) + ) + for proc, count in all_processes.items(): + expected = self.expected_number_of_dual_amplitudes(proc) + if abs(count - expected) > 1e-5: + raise RuntimeError( + f"inconsistent number of dual amplitudes for {proc}: " + f"{count}, expected {expected}" + ) + + def expected_number_of_dual_amplitudes(self, proc: Sequence[str]) -> float: + nq = self._count_matching(proc, QUARKS) + ng = self._count_matching(proc, GLUONS) + if nq == 0: + return math.factorial(ng - 1) + return math.factorial(ng) * math.comb(ng + nq - 1, nq - 1) * math.factorial(nq) + + def _count_matching(self, main: Sequence[str], check: Iterable[str]) -> int: + counts = Counter(main) + return sum(counts[item] for item in check if item in counts) + + +def enumerate_processes( + process_string: str, options: ProcessOptions | None = None +) -> ProcessEnumeration: + return ProcessEnumerator(options).enumerate(process_string) diff --git a/pyamplicol/src/pyamplicol/models/builtin/process_ir.py b/pyamplicol/src/pyamplicol/models/builtin/process_ir.py new file mode 100644 index 0000000000..374510bc1c --- /dev/null +++ b/pyamplicol/src/pyamplicol/models/builtin/process_ir.py @@ -0,0 +1,220 @@ +# SPDX-License-Identifier: 0BSD +"""Legacy built-in-SM process IR construction.""" + +from __future__ import annotations + +from collections import Counter + +from ...processes.core_syntax import ( + canonical_process_key, +) +from ...processes.ir import ( + CanonicalProcessIR, + ColorEndpointSummary, + ColorRole, + LegSide, + ParticleStatistics, + ProcessLegIR, + ProcessSetEntryIR, + ProcessSetIR, + SourceOrientation, + WavefunctionFamily, +) +from .process_catalog import ( + ANTI_PARTICLE, + ANTIQUARKS, + GLUONS, + PDGS, + QUARKS, + SINGLETS, +) +from .process_enumeration import ProcessEnumerator +from .process_selection import enumerate_generic_process_set, enumerate_process_set +from .process_types import ( + BuiltinProcessOptions as ProcessOptions, +) +from .process_types import ( + BuiltinProcessSetEntry as ProcessSetEntry, +) + +_VECTOR_NAMES = frozenset({"a", "z", "w+", "w-"}) + + +def build_process_ir( + process: str, + *, + color_accuracy: str = "lc", + options: ProcessOptions | None = None, +) -> CanonicalProcessIR: + parsed = ProcessEnumerator(options).parse(process) + physical_initial = tuple( + particle if particle in {"p", "j"} else ANTI_PARTICLE[particle] + for particle in parsed.initial_state + ) + canonical = f"{' '.join(physical_initial)} > {' '.join(parsed.rest)}" + legs = tuple( + [ + *( + _leg_ir( + label=index + 1, + side="initial", + particle=particle, + outgoing_particle=parsed.initial_state[index], + ) + for index, particle in enumerate(physical_initial) + ), + *( + _leg_ir( + label=index + 3, + side="final", + particle=particle, + outgoing_particle=particle, + ) + for index, particle in enumerate(parsed.rest) + ), + ] + ) + counts = Counter(leg.color_role for leg in legs) + return CanonicalProcessIR( + process=canonical, + key=canonical_process_key(canonical), + color_accuracy=color_accuracy, + legs=legs, + color_endpoints=ColorEndpointSummary( + fundamental_count=counts["fundamental"], + antifundamental_count=counts["antifundamental"], + pair_count=min( + counts["fundamental"], + counts["antifundamental"], + ), + ), + ) + + +def build_process_set_ir( + process_string: str, + *, + color_accuracy: str = "lc", + options: ProcessOptions | None = None, + generic: bool = False, + max_quark_pairs: int | None = None, +) -> ProcessSetIR: + enumeration = ( + enumerate_generic_process_set( + process_string, + options=options, + max_quark_pairs=max_quark_pairs, + ) + if generic + else enumerate_process_set(process_string, options=options) + ) + entries = tuple( + _process_set_entry_ir(entry, color_accuracy=color_accuracy) + for entry in enumeration.entries + ) + return ProcessSetIR( + request=enumeration.request, + color_accuracy=color_accuracy, + entries=entries, + ) + + +def _process_set_entry_ir( + entry: ProcessSetEntry, + *, + color_accuracy: str, +) -> ProcessSetEntryIR: + ir = build_process_ir( + entry.process, + color_accuracy=color_accuracy, + options=entry.enumeration.options, + ) + return ProcessSetEntryIR( + key=entry.key, + process=entry.process, + ir=ir, + n_groups=len(entry.enumeration.groups), + n_records=entry.enumeration.n_records, + n_unique_processes=len(entry.enumeration.unique_processes), + ) + + +def _leg_ir( + *, + label: int, + side: LegSide, + particle: str, + outgoing_particle: str, +) -> ProcessLegIR: + statistics, wavefunction_family, color_role, source_orientation = _structural_roles( + outgoing_particle + ) + return ProcessLegIR( + label=label, + side=side, + particle=particle, + outgoing_particle=outgoing_particle, + pdg=_pdg_or_none(particle), + outgoing_pdg=_pdg_or_none(outgoing_particle), + statistics=statistics, + wavefunction_family=wavefunction_family, + color_role=color_role, + source_orientation=source_orientation, + ) + + +def _pdg_or_none(particle: str) -> int | None: + if particle in {"p", "j"}: + return None + return int(PDGS[particle]) + + +def _structural_roles( + outgoing_particle: str, +) -> tuple[ + ParticleStatistics, + WavefunctionFamily, + ColorRole, + SourceOrientation, +]: + if outgoing_particle in {"p", "j"}: + return "inclusive", "inclusive", "inclusive", "inclusive" + + pdg = abs(int(PDGS[outgoing_particle])) + if ( + outgoing_particle in QUARKS + or outgoing_particle in ANTIQUARKS + or pdg in {11, 12, 13, 14, 15, 16} + ): + statistics: ParticleStatistics = "fermion" + wavefunction_family: WavefunctionFamily = "fermion" + elif outgoing_particle in GLUONS or outgoing_particle in _VECTOR_NAMES: + statistics = "boson" + wavefunction_family = "vector" + elif outgoing_particle == "h" or outgoing_particle in SINGLETS: + statistics = "boson" + wavefunction_family = "scalar" + else: + raise ValueError(f"unknown built-in particle role for {outgoing_particle!r}") + + color_role: ColorRole + if outgoing_particle in QUARKS: + color_role = "fundamental" + elif outgoing_particle in ANTIQUARKS: + color_role = "antifundamental" + elif outgoing_particle in GLUONS: + color_role = "adjoint" + else: + color_role = "singlet" + + if ANTI_PARTICLE[outgoing_particle] == outgoing_particle: + source_orientation: SourceOrientation = "self-conjugate" + elif int(PDGS[outgoing_particle]) > 0: + source_orientation = "particle" + else: + source_orientation = "antiparticle" + + return statistics, wavefunction_family, color_role, source_orientation + + +__all__ = ["build_process_ir", "build_process_set_ir"] diff --git a/pyamplicol/src/pyamplicol/models/builtin/process_selection.py b/pyamplicol/src/pyamplicol/models/builtin/process_selection.py new file mode 100644 index 0000000000..42e0d2b787 --- /dev/null +++ b/pyamplicol/src/pyamplicol/models/builtin/process_selection.py @@ -0,0 +1,595 @@ +# SPDX-License-Identifier: 0BSD +"""Legacy built-in-SM process-set selection and compatibility reporting.""" + +from __future__ import annotations + +import itertools +import time +from collections import Counter +from collections.abc import Sequence + +from ...processes.core_syntax import ( + ProcessTuple, + _tokenize_side, + canonical_process_key, + expand_process_variants, +) +from .process_catalog import ( + ANTI_PARTICLE, + ANTIQUARKS, + QUARKS, + SORT_PARTICLES, + concrete_processes_from_inclusive_enumeration, + process_uses_inclusive_labels, + request_allows_charged_current, + selection_metadata, +) +from .process_enumeration import ProcessEnumerator +from .process_types import ( + BuiltinPhaseSpaceGroup as PhaseSpaceGroup, +) +from .process_types import ( + BuiltinProcessEnumeration as ProcessEnumeration, +) +from .process_types import ( + BuiltinProcessOptions as ProcessOptions, +) +from .process_types import ( + BuiltinProcessSelectionRecord as ProcessSelectionRecord, +) +from .process_types import ( + BuiltinProcessSelectionReport as ProcessSelectionReport, +) +from .process_types import ( + BuiltinProcessSetEntry as ProcessSetEntry, +) +from .process_types import ( + BuiltinProcessSetEnumeration as ProcessSetEnumeration, +) +from .process_types import ( + BuiltinSubprocessRecord as SubprocessRecord, +) + + +def enumerate_process_set( + process_string: str, + options: ProcessOptions | None = None, +) -> ProcessSetEnumeration: + active_options = options or ProcessOptions() + entries: list[ProcessSetEntry] = [] + seen: set[str] = set() + for process in expand_process_variants(process_string): + inclusive_enumeration: ProcessEnumeration | None = None + if process_uses_inclusive_labels(process): + inclusive_enumeration = ProcessEnumerator(active_options).enumerate(process) + concrete_processes = concrete_processes_from_inclusive_enumeration( + inclusive_enumeration + ) + else: + concrete_processes = (process,) + for concrete_process in concrete_processes: + key = canonical_process_key(concrete_process) + if key in seen: + continue + seen.add(key) + enumeration = ProcessEnumerator(active_options).enumerate(concrete_process) + if enumeration.n_records == 0: + continue + entries.append( + ProcessSetEntry( + key=key, + process=concrete_process, + enumeration=enumeration, + ) + ) + if not entries: + raise ValueError(f"no valid processes found for {process_string!r}") + return ProcessSetEnumeration( + request=process_string, + options=active_options, + entries=tuple(entries), + ) + + +def enumerate_generic_process_set( + process_string: str, + options: ProcessOptions | None = None, + *, + max_quark_pairs: int | None = None, + use_prefilter: bool = True, +) -> ProcessSetEnumeration: + """Expand process-set syntax for generic DAG generation. + + The generic compiler only needs concrete subprocess keys and parser + metadata. It should not pay the legacy phase-space/color-order enumeration + cost for an already concrete partonic process; colour sectors and closures + are discovered later by the model-driven DAG. Inclusive ``p``/``j`` + requests still use the legacy enumerator once to obtain concrete children. + """ + + active_options = options or ProcessOptions() + report = build_generic_process_selection_report( + process_string, + active_options, + max_quark_pairs=max_quark_pairs, + use_prefilter=use_prefilter, + ) + if not report.entries: + raise ValueError(f"no valid processes found for {process_string!r}") + return ProcessSetEnumeration( + request=process_string, + options=active_options, + entries=report.entries, + selection_report=report, + ) + + +def build_generic_process_selection_report( + process_string: str, + options: ProcessOptions | None = None, + *, + max_quark_pairs: int | None = None, + use_prefilter: bool = True, +) -> ProcessSelectionReport: + """Return selected generic subprocesses plus enumeration diagnostics.""" + + started = time.perf_counter() + active_options = options or ProcessOptions() + entries: list[ProcessSetEntry] = [] + seen: set[str] = set() + records: list[ProcessSelectionRecord] = [] + rejection_counts: Counter[str] = Counter() + candidate_count = 0 + evaluated_count = 0 + stage_timings: list[tuple[str, float]] = [] + + stage_started = time.perf_counter() + variants = expand_process_variants(process_string) + stage_timings.append(("expand", time.perf_counter() - stage_started)) + + stage_started = time.perf_counter() + for process in variants: + if use_prefilter: + result = _generic_selection_records_from_request( + process, + active_options, + max_quark_pairs=max_quark_pairs, + seen=seen, + ) + else: + result = _legacy_selection_records_from_request( + process, + active_options, + max_quark_pairs=max_quark_pairs, + seen=seen, + ) + candidate_count += result[0] + evaluated_count += result[1] + rejection_counts.update(result[2]) + records.extend(result[3]) + stage_timings.append(("prefilter", time.perf_counter() - stage_started)) + + stage_started = time.perf_counter() + for record in records: + if record.status != "selected": + continue + enumeration = _lightweight_concrete_process_enumeration( + record.process, + active_options, + ) + if enumeration.n_records == 0: + rejection_counts["model-reachability"] += 1 + continue + entries.append( + ProcessSetEntry( + key=record.key, + process=record.process, + enumeration=enumeration, + ) + ) + stage_timings.append(("canonicalize", time.perf_counter() - stage_started)) + duplicate_count = sum(1 for record in records if record.status == "duplicate") + selected_count = len(entries) + rejected_count = candidate_count - selected_count - duplicate_count + return ProcessSelectionReport( + request=process_string, + options=active_options, + entries=tuple(entries), + records=tuple(records), + candidate_count=candidate_count, + evaluated_count=evaluated_count, + selected_count=selected_count, + duplicate_count=duplicate_count, + rejected_count=max(rejected_count, 0), + rejection_counts=tuple(sorted(rejection_counts.items())), + stage_timings=tuple(stage_timings), + elapsed_s=time.perf_counter() - started, + prefilter_enabled=use_prefilter, + ) + + +def _generic_selection_records_from_request( + process: str, + options: ProcessOptions, + *, + max_quark_pairs: int | None, + seen: set[str], +) -> tuple[int, int, Counter[str], list[ProcessSelectionRecord]]: + if process_uses_inclusive_labels(process): + return _prefilter_inclusive_request( + process, + options, + max_quark_pairs=max_quark_pairs, + seen=seen, + ) + return _classify_concrete_request( + process, + options, + max_quark_pairs=max_quark_pairs, + seen=seen, + source=process, + symmetric_initial=False, + ) + + +def _legacy_selection_records_from_request( + process: str, + options: ProcessOptions, + *, + max_quark_pairs: int | None, + seen: set[str], +) -> tuple[int, int, Counter[str], list[ProcessSelectionRecord]]: + if process_uses_inclusive_labels(process): + enumeration = ProcessEnumerator(options).enumerate(process) + concrete_processes = concrete_processes_from_inclusive_enumeration(enumeration) + else: + concrete_processes = (process,) + records: list[ProcessSelectionRecord] = [] + rejection_counts: Counter[str] = Counter() + candidate_count = 0 + evaluated_count = 0 + for concrete_process in concrete_processes: + result = _classify_concrete_request( + concrete_process, + options, + max_quark_pairs=max_quark_pairs, + seen=seen, + source=process, + symmetric_initial=False, + ) + candidate_count += result[0] + evaluated_count += result[1] + rejection_counts.update(result[2]) + records.extend(result[3]) + return candidate_count, evaluated_count, rejection_counts, records + + +def _classify_concrete_request( + process: str, + options: ProcessOptions, + *, + max_quark_pairs: int | None, + seen: set[str], + source: str, + symmetric_initial: bool, +) -> tuple[int, int, Counter[str], list[ProcessSelectionRecord]]: + enumerator = ProcessEnumerator(options) + request = enumerator.parse(process) + physical_initial = tuple(ANTI_PARTICLE[p] for p in request.initial_state) + final_state = tuple(request.rest) + return _classify_physical_candidate( + enumerator, + physical_initial, + final_state, + source=source, + allow_charged_current=request_allows_charged_current(request), + max_quark_pairs=max_quark_pairs, + seen=seen, + symmetric_initial=symmetric_initial, + ) + + +def _prefilter_inclusive_request( + process: str, + options: ProcessOptions, + *, + max_quark_pairs: int | None, + seen: set[str], +) -> tuple[int, int, Counter[str], list[ProcessSelectionRecord]]: + enumerator = ProcessEnumerator(options) + request = enumerator.parse(process) + initial_options: list[tuple[str, ...]] = [] + parton_options = tuple( + sorted(enumerator.massless_qcd, key=lambda p: SORT_PARTICLES[p]) + ) + for crossed_particle in request.initial_state: + if crossed_particle in {"p", "j"}: + initial_options.append(parton_options) + else: + initial_options.append((ANTI_PARTICLE[crossed_particle],)) + + symmetric_initial = ( + len(request.initial_state) == 2 + and request.initial_state[0] == request.initial_state[1] + and request.initial_state[0] in {"p", "j"} + ) + final_candidates = tuple( + _generic_final_candidate(request.rest, jets) + for jets in itertools.combinations_with_replacement( + parton_options, + request.jet_count, + ) + ) + final_by_charge: Counter[int] = Counter( + final_charge for _, final_charge, _, _, _ in final_candidates + ) + final_by_charge_family: Counter[tuple[int, int]] = Counter( + (final_charge, final_family) + for _, final_charge, final_family, _, _ in final_candidates + ) + bucketed_finals: dict[ + tuple[int, int | None], list[tuple[ProcessTuple, int, int, int, int]] + ] = {} + for candidate in final_candidates: + final_state, final_charge, final_family, _, _ = candidate + del final_state + bucketed_finals.setdefault((final_charge, None), []).append(candidate) + bucketed_finals.setdefault((final_charge, final_family), []).append(candidate) + + allow_charged_current = request_allows_charged_current(request) + records: list[ProcessSelectionRecord] = [] + rejection_counts: Counter[str] = Counter() + candidate_count = 0 + evaluated_count = 0 + for initial_state in itertools.product(*initial_options): + crossed_initial = tuple(ANTI_PARTICLE[p] for p in initial_state) + initial_charge = _charge3_sum(crossed_initial) + initial_family = _family_sum(crossed_initial) + candidate_count += len(final_candidates) + charge_key = -initial_charge + charge_matches = final_by_charge[charge_key] + rejection_counts["charge"] += len(final_candidates) - charge_matches + if options.include_cc: + candidate_bucket = bucketed_finals.get((charge_key, None), ()) + else: + family_key = -initial_family + family_matches = final_by_charge_family[(charge_key, family_key)] + rejection_counts["fermion-family"] += charge_matches - family_matches + candidate_bucket = bucketed_finals.get((charge_key, family_key), ()) + for final_state, _, _, _, _ in candidate_bucket: + result = _classify_physical_candidate( + enumerator, + initial_state, + final_state, + source=process, + allow_charged_current=allow_charged_current, + max_quark_pairs=max_quark_pairs, + seen=seen, + symmetric_initial=symmetric_initial, + prechecked_charge_family=True, + ) + candidate_count += result[0] - 1 + evaluated_count += result[1] + rejection_counts.update(result[2]) + records.extend(result[3]) + return candidate_count, evaluated_count, rejection_counts, records + + +def _classify_physical_candidate( + enumerator: ProcessEnumerator, + physical_initial: Sequence[str], + final_state: Sequence[str], + *, + source: str, + allow_charged_current: bool, + max_quark_pairs: int | None, + seen: set[str], + symmetric_initial: bool, + prechecked_charge_family: bool = False, +) -> tuple[int, int, Counter[str], list[ProcessSelectionRecord]]: + display_process = _canonical_physical_process( + physical_initial, + final_state, + symmetric_initial=symmetric_initial, + ) + canonical_initial, _, canonical_final = display_process.partition(">") + canonical_crossed_initial = tuple( + ANTI_PARTICLE[p] for p in _tokenize_side(canonical_initial.strip()) + ) + canonical_final_state = tuple(_tokenize_side(canonical_final.strip())) + all_outgoing = (*canonical_crossed_initial, *canonical_final_state) + charge3 = _charge3_sum(all_outgoing) + family = _family_sum(all_outgoing) + quark_lines = _quark_pair_count(all_outgoing) + records: list[ProcessSelectionRecord] = [] + rejection_counts: Counter[str] = Counter() + evaluated_count = 1 + reason = "" + if not prechecked_charge_family and charge3 != 0: + reason = "charge" + elif ( + not prechecked_charge_family + and not enumerator.options.include_cc + and family != 0 + ): + reason = "fermion-family" + elif max_quark_pairs is not None and quark_lines > int(max_quark_pairs): + reason = "max-quark-lines" + elif _single_impossible_colored_state(all_outgoing): + reason = "single-coloured-state" + elif not _valid_generic_process( + enumerator, + all_outgoing, + allow_charged_current=allow_charged_current, + ): + reason = "model-reachability" + + key = canonical_process_key(display_process) + dedupe_key = _process_dedupe_signature( + physical_initial, + final_state, + symmetric_initial=symmetric_initial, + ) + if reason: + rejection_counts[reason] += 1 + return 1, evaluated_count, rejection_counts, [] + if dedupe_key in seen: + records.append( + ProcessSelectionRecord( + source=source, + process=display_process, + key=key, + status="duplicate", + reason="canonical-duplicate", + quark_lines=quark_lines, + charge3=charge3, + family=family, + ) + ) + return 1, evaluated_count, rejection_counts, records + seen.add(dedupe_key) + records.append( + ProcessSelectionRecord( + source=source, + process=display_process, + key=key, + status="selected", + reason="selected", + quark_lines=quark_lines, + charge3=charge3, + family=family, + ) + ) + return 1, evaluated_count, rejection_counts, records + + +def _canonical_physical_process( + initial_state: Sequence[str], + final_state: Sequence[str], + *, + symmetric_initial: bool, +) -> str: + initial = tuple(initial_state) + if symmetric_initial: + initial = tuple(sorted(initial, key=lambda particle: SORT_PARTICLES[particle])) + final = tuple(final_state) + return f"{' '.join(initial)} > {' '.join(final)}" + + +def _process_dedupe_signature( + initial_state: Sequence[str], + final_state: Sequence[str], + *, + symmetric_initial: bool, +) -> str: + initial = tuple(initial_state) + if symmetric_initial: + initial = tuple(sorted(initial, key=lambda particle: SORT_PARTICLES[particle])) + final = tuple(sorted(final_state, key=lambda particle: SORT_PARTICLES[particle])) + return f"{' '.join(initial)} > {' '.join(final)}" + + +def _single_impossible_colored_state(process: Sequence[str]) -> bool: + return ( + sum(1 for particle in process if selection_metadata(particle).is_colored) == 1 + ) + + +def _valid_generic_process( + enumerator: ProcessEnumerator, + process: Sequence[str], + *, + allow_charged_current: bool, +) -> bool: + if enumerator._valid_process( + process, + allow_charged_current=allow_charged_current, + ): + return True + if any(selection_metadata(particle).is_colored for particle in process): + return False + if _charge3_sum(process) != 0: + return False + return enumerator.options.include_cc or _family_sum(process) == 0 + + +def _quark_pair_count(process: Sequence[str]) -> int: + quarks, antiquarks = _quark_counts(process) + return min(quarks, antiquarks) + + +def _charge3_sum(process: Sequence[str]) -> int: + return sum(selection_metadata(particle).charge3 for particle in process) + + +def _family_sum(process: Sequence[str]) -> int: + return sum(selection_metadata(particle).family for particle in process) + + +def _quark_counts(process: Sequence[str]) -> tuple[int, int]: + counts = Counter(process) + quarks = sum(counts[item] for item in QUARKS if item in counts) + antiquarks = sum(counts[item] for item in ANTIQUARKS if item in counts) + return quarks, antiquarks + + +def _generic_final_candidate( + rest: Sequence[str], + jets: Sequence[str], +) -> tuple[ProcessTuple, int, int, int, int]: + final_state = tuple( + sorted( + (*rest, *jets), + key=lambda particle: SORT_PARTICLES[particle], + ) + ) + quarks, antiquarks = _quark_counts(final_state) + return ( + final_state, + _charge3_sum(final_state), + _family_sum(final_state), + quarks, + antiquarks, + ) + + +def _lightweight_concrete_process_enumeration( + process: str, + options: ProcessOptions, +) -> ProcessEnumeration: + enumerator = ProcessEnumerator(options) + request = enumerator.parse(process) + all_outgoing = (*request.initial_state, *request.rest) + if not _valid_generic_process( + enumerator, + all_outgoing, + allow_charged_current=request_allows_charged_current(request), + ): + return ProcessEnumeration( + request=request, + options=options, + unique_processes=(), + groups=(), + ) + order = tuple(range(len(all_outgoing))) + return ProcessEnumeration( + request=request, + options=options, + unique_processes=( + tuple(sorted(all_outgoing, key=lambda item: SORT_PARTICLES[item])), + ), + groups=( + PhaseSpaceGroup( + group_id=1, + phase_space_order=order, + records=( + SubprocessRecord( + process=tuple(all_outgoing), + color_order=order, + multichannel_partners=(0,), + identical_factor=1.0, + ), + ), + ), + ), + ) diff --git a/pyamplicol/src/pyamplicol/models/builtin/process_types.py b/pyamplicol/src/pyamplicol/models/builtin/process_types.py new file mode 100644 index 0000000000..27ddcb4ba6 --- /dev/null +++ b/pyamplicol/src/pyamplicol/models/builtin/process_types.py @@ -0,0 +1,135 @@ +# SPDX-License-Identifier: 0BSD +"""Built-in-SM process enumeration and selection records. + +These records describe the historical built-in process catalogue. They are +not part of the model-neutral process syntax or external-model API. +""" + +from __future__ import annotations + +from dataclasses import dataclass + +from ...processes.core_syntax import OrderTuple, ProcessTuple + + +@dataclass(frozen=True) +class BuiltinParsedProcess: + initial_state: ProcessTuple + jet_count: int + rest: ProcessTuple + leptons: ProcessTuple = () + + +@dataclass(frozen=True) +class BuiltinProcessOptions: + flavour_scheme: int = 5 + include_3qqbar: bool = False + include_cc: bool = False + include_resonance: bool = False + serial: bool = True + + +@dataclass(frozen=True) +class BuiltinSubprocessRecord: + process: ProcessTuple + color_order: OrderTuple + multichannel_partners: tuple[int, ...] = () + identical_factor: float = 1.0 + + +@dataclass(frozen=True) +class BuiltinPhaseSpaceGroup: + group_id: int + phase_space_order: OrderTuple + records: tuple[BuiltinSubprocessRecord, ...] + + +@dataclass(frozen=True) +class BuiltinProcessEnumeration: + request: BuiltinParsedProcess + options: BuiltinProcessOptions + unique_processes: tuple[ProcessTuple, ...] + groups: tuple[BuiltinPhaseSpaceGroup, ...] + + @property + def n_external(self) -> int: + if self.unique_processes: + return len(self.unique_processes[0]) + if self.groups and self.groups[0].records: + return len(self.groups[0].records[0].process) + return 0 + + @property + def n_records(self) -> int: + return sum(len(group.records) for group in self.groups) + + +@dataclass(frozen=True) +class BuiltinProcessSetEntry: + key: str + process: str + enumeration: BuiltinProcessEnumeration + + +@dataclass(frozen=True) +class BuiltinProcessSelectionRecord: + source: str + process: str + key: str + status: str + reason: str + quark_lines: int + charge3: int + family: int + + +@dataclass(frozen=True) +class BuiltinProcessSelectionReport: + request: str + options: BuiltinProcessOptions + entries: tuple[BuiltinProcessSetEntry, ...] + records: tuple[BuiltinProcessSelectionRecord, ...] + candidate_count: int + evaluated_count: int + selected_count: int + duplicate_count: int + rejected_count: int + rejection_counts: tuple[tuple[str, int], ...] + stage_timings: tuple[tuple[str, float], ...] + elapsed_s: float + prefilter_enabled: bool + + @property + def selected_records(self) -> tuple[BuiltinProcessSelectionRecord, ...]: + return tuple(record for record in self.records if record.status == "selected") + + @property + def duplicate_records(self) -> tuple[BuiltinProcessSelectionRecord, ...]: + return tuple(record for record in self.records if record.status == "duplicate") + + +@dataclass(frozen=True) +class BuiltinProcessSetEnumeration: + request: str + options: BuiltinProcessOptions + entries: tuple[BuiltinProcessSetEntry, ...] + selection_report: BuiltinProcessSelectionReport | None = None + + @property + def default_key(self) -> str: + if not self.entries: + raise ValueError("process set is empty") + return self.entries[0].key + + +__all__ = [ + "BuiltinParsedProcess", + "BuiltinPhaseSpaceGroup", + "BuiltinProcessEnumeration", + "BuiltinProcessOptions", + "BuiltinProcessSelectionRecord", + "BuiltinProcessSelectionReport", + "BuiltinProcessSetEntry", + "BuiltinProcessSetEnumeration", + "BuiltinSubprocessRecord", +] diff --git a/pyamplicol/src/pyamplicol/models/builtin/symbols.py b/pyamplicol/src/pyamplicol/models/builtin/symbols.py new file mode 100644 index 0000000000..713273220b --- /dev/null +++ b/pyamplicol/src/pyamplicol/models/builtin/symbols.py @@ -0,0 +1,83 @@ +# SPDX-License-Identifier: 0BSD +from __future__ import annotations + +from functools import cached_property +from typing import Any + +from ..._internal.physics.symbols import SymbolRegistry + + +class BuiltinSymbolRegistry(SymbolRegistry): + """Symbolica heads owned exclusively by built-in-SM lowering.""" + + @cached_property + def antisymmetric_lorentz_pair_name(self) -> str: + return self.qualified_name("antisymmetric_lorentz_pair") + + @cached_property + def weyl_spinor_name(self) -> str: + return self.qualified_name("weyl_spinor") + + def vertex_kind_tensor_name(self, kind: int) -> str: + return self.qualified_name(f"vertex_kind_{int(kind)}") + + def label(self, labels: tuple[int, ...]) -> Any: + return self.symbol("label::" + "_".join(str(label) for label in labels)) + + @cached_property + def two_gluon_to_tensor_name(self) -> str: + return self.qualified_name("two_gluon_to_tensor") + + @cached_property + def tensor_gluon_to_gluon_name(self) -> str: + return self.qualified_name("tensor_gluon_to_gluon") + + @cached_property + def gluon_tensor_to_gluon_name(self) -> str: + return self.qualified_name("gluon_tensor_to_gluon") + + @cached_property + def quark_vector_weyl_plus_name(self) -> str: + return self.qualified_name("quark_vector_weyl_plus") + + @cached_property + def quark_vector_weyl_minus_name(self) -> str: + return self.qualified_name("quark_vector_weyl_minus") + + @cached_property + def current(self) -> Any: + return self.symbol("current") + + @cached_property + def vertex(self) -> Any: + return self.symbol("vertex") + + @cached_property + def assignment(self) -> Any: + return self.symbol("assign") + + @cached_property + def amplitude(self) -> Any: + return self.symbol("amplitude") + + @cached_property + def matrix_element_plan(self) -> Any: + return self.symbol("matrix_element_plan") + + @cached_property + def momentum(self) -> Any: + return self.symbol("momentum") + + @cached_property + def current_momentum(self) -> Any: + return self.symbol("current_momentum") + + @cached_property + def polarization(self) -> Any: + return self.symbol("polarization") + + +symbols = BuiltinSymbolRegistry() + + +__all__ = ["BuiltinSymbolRegistry", "symbols"] diff --git a/pyamplicol/src/pyamplicol/models/builtin/validation.py b/pyamplicol/src/pyamplicol/models/builtin/validation.py new file mode 100644 index 0000000000..6894bda6ed --- /dev/null +++ b/pyamplicol/src/pyamplicol/models/builtin/validation.py @@ -0,0 +1,111 @@ +# SPDX-License-Identifier: 0BSD +"""Legacy built-in-SM validation kinematics.""" + +from __future__ import annotations + +from ..._internal.physics.types import ExternalMomentum, NativeEvaluationError +from ...generation.phase_space import massive_rambo_final_state +from .model import BuiltinSMModel +from .process_ir import build_process_ir + + +def legacy_rambo_z_gluon_point( + process: str, + model: BuiltinSMModel, + *, + gluon_count: int, + sqrt_s: float, + seed: int, +) -> tuple[ExternalMomentum, ...]: + """Generate a deterministic RAMBO point for q q~ -> Z + n g.""" + + if gluon_count < 1: + raise NativeEvaluationError("RAMBO Z-gluon points need at least one gluon") + z_mass = model.mass(23) + if sqrt_s <= z_mass: + raise NativeEvaluationError("sqrt(s) must be above the Z mass") + pdgs = _physical_pdgs(process) + expected = gluon_count + 3 + if len(pdgs) != expected: + raise NativeEvaluationError( + f"expected {expected} external particles for q q~ -> Z + {gluon_count} g" + ) + final_pdgs = (*((21,) * gluon_count), 23) + final_masses = tuple(0.0 if pdg == 21 else model.mass(pdg) for pdg in final_pdgs) + if sum(final_masses) >= sqrt_s: + raise NativeEvaluationError("final-state masses exceed sqrt(s)") + final_momenta = massive_rambo_final_state( + len(final_masses), + sqrt_s=sqrt_s, + masses=final_masses, + seed=seed, + ) + beam_energy = 0.5 * sqrt_s + return ( + ExternalMomentum(pdgs[0], (beam_energy, 0.0, 0.0, beam_energy)), + ExternalMomentum(pdgs[1], (beam_energy, 0.0, 0.0, -beam_energy)), + *( + ExternalMomentum(pdg, momentum) + for pdg, momentum in zip(final_pdgs, final_momenta, strict=True) + ), + ) + + +def generic_validation_point( + process: str, + *, + model: BuiltinSMModel | None = None, + sqrt_s: float | None = None, + seed: int = 101, +) -> tuple[ExternalMomentum, ...]: + """Return deterministic two-beam kinematics for a built-in-SM process.""" + + model = model or BuiltinSMModel() + ir = build_process_ir(process) + initial_pdgs = tuple(int(pdg) for pdg in ir.initial_pdgs) + final_pdgs = tuple(int(pdg) for pdg in ir.final_pdgs) + if len(initial_pdgs) != 2: + raise NativeEvaluationError( + "generic validation momenta currently require a two-body initial state" + ) + if not final_pdgs: + raise NativeEvaluationError( + "generic validation momenta require at least one final-state particle" + ) + final_masses = tuple(float(model.mass(pdg)) for pdg in final_pdgs) + threshold = sum(final_masses) + if sqrt_s is None: + sqrt_s = threshold if len(final_pdgs) == 1 else max(1000.0, threshold + 100.0) + if sqrt_s < threshold: + raise NativeEvaluationError("sqrt(s) is below the final-state mass threshold") + if len(final_pdgs) == 1: + if threshold <= 0.0: + raise NativeEvaluationError( + "no finite centre-of-mass validation point exists for one " + "massless final state" + ) + final_momenta = ((float(sqrt_s), 0.0, 0.0, 0.0),) + else: + final_momenta = massive_rambo_final_state( + len(final_pdgs), + sqrt_s=float(sqrt_s), + masses=final_masses, + seed=seed, + ) + beam_energy = 0.5 * float(sqrt_s) + return ( + ExternalMomentum(initial_pdgs[0], (beam_energy, 0.0, 0.0, beam_energy)), + ExternalMomentum(initial_pdgs[1], (beam_energy, 0.0, 0.0, -beam_energy)), + *( + ExternalMomentum(pdg, momentum) + for pdg, momentum in zip(final_pdgs, final_momenta, strict=True) + ), + ) + + +def _physical_pdgs(process: str) -> tuple[int, ...]: + ir = build_process_ir(process) + return (*ir.initial_pdgs, *ir.final_pdgs) + + +__all__ = ["generic_validation_point", "legacy_rambo_z_gluon_point"] diff --git a/pyamplicol/src/pyamplicol/models/compiler.py b/pyamplicol/src/pyamplicol/models/compiler.py new file mode 100644 index 0000000000..5f1671025b --- /dev/null +++ b/pyamplicol/src/pyamplicol/models/compiler.py @@ -0,0 +1,53 @@ +# SPDX-License-Identifier: 0BSD +"""Compatibility facade for model IR compilation.""" + +from __future__ import annotations + +from collections.abc import Mapping + +from .compiler_contact_trees import eager_color_singlet_vertex_term_components +from .compiler_entry import compile_ufo_model_ir +from .compiler_kernels import ( + _as_expression as _as_expression, +) +from .compiler_kernels import ( + _spin_representations as _spin_representations, +) +from .compiler_kernels import ( + _spin_slots as _spin_slots, +) +from .compiler_records import ( + _replace_evaluator_constants as _replace_evaluator_constants, +) +from .contracts import ( + CompiledCouplingOrder, + CompiledCouplingRecord, + CompiledModelIR, + CompiledParameterRecord, + CompiledParticleRecord, + CompiledPropagatorRecord, + CompiledVertexTerm, +) +from .contracts import ( + CompiledOrientedKernel as CompiledOrientedKernel, +) + + +def compile_builtin_model_ir(model: Mapping[str, object]) -> CompiledModelIR: + """Compile the built-in model without loading it on external-model imports.""" + from .builtin.compiler import compile_model_ir + + return compile_model_ir(model) + +__all__ = [ + "CompiledCouplingOrder", + "CompiledCouplingRecord", + "CompiledModelIR", + "CompiledParameterRecord", + "CompiledParticleRecord", + "CompiledPropagatorRecord", + "CompiledVertexTerm", + "compile_builtin_model_ir", + "compile_ufo_model_ir", + "eager_color_singlet_vertex_term_components", +] diff --git a/pyamplicol/src/pyamplicol/models/compiler_auxiliary_components.py b/pyamplicol/src/pyamplicol/models/compiler_auxiliary_components.py new file mode 100644 index 0000000000..e2eb9a867c --- /dev/null +++ b/pyamplicol/src/pyamplicol/models/compiler_auxiliary_components.py @@ -0,0 +1,298 @@ +# SPDX-License-Identifier: 0BSD +"""Remove unobservable components from compiler-owned contact-tree currents.""" + +from __future__ import annotations + +from collections.abc import Mapping, Sequence +from dataclasses import replace + +from .._internal.physics.symbols import ModelSymbolRegistry +from . import compiler_symbolica as _sym +from .compiler_kernels import _canonicalize_oriented_kernel_component +from .contracts import CompiledOrientedKernel, CompiledParticleRecord + +# A reduced component is a signed sum of original producer components. Only +# identical/opposite consumer columns are combined; no numerical rank decision +# or parameter-dependent change of basis is involved. +_ComponentGroup = tuple[tuple[int, int], ...] +_ComponentBasis = tuple[_ComponentGroup, ...] + + +def reduce_contact_auxiliary_components( + particles: Sequence[CompiledParticleRecord], + kernels: Sequence[CompiledOrientedKernel], + *, + auxiliary_particles: Sequence[CompiledParticleRecord], + model_symbols: ModelSymbolRegistry, +) -> tuple[tuple[CompiledParticleRecord, ...], tuple[CompiledOrientedKernel, ...]]: + """Reduce opaque auxiliaries before ordering/kernel preparation. + + The caller supplies only its newly lowered contact-tree auxiliaries. Their + sole consumers are oriented kernels: unlike physical particles and other + auxiliary representations, they have no wavefunctions, propagators, direct + contractions, or externally prescribed tensor basis to preserve. + + For every input orientation, certify ``F(v) = sum_i C_i v_i`` exactly. A + zero column is unused; columns ``C_i = +/- C_j`` need only the corresponding + signed sum of producer components. All kernels and both input sides are + considered before changing any representation. Repeat after a reduction + so that discarded downstream components can expose upstream reductions. + """ + candidates = {particle.name for particle in auxiliary_particles} + particles, kernels = tuple(particles), tuple(kernels) + if not candidates: + return particles, kernels + _sym._ensure_symbolica() + while True: + bases: dict[str, _ComponentBasis] = {} + dimensions: dict[str, int] = {} + exact_kernels: dict[int, tuple[_sym.Expression, ...] | None] = {} + for particle in particles: + if ( + particle.name not in candidates + or particle.spin != -1 + or particle.propagating + or particle.propagator is not None + or particle.antiname != particle.name + or particle.component_dimension is None + or particle.component_dimension <= 1 + ): + continue + involved = tuple( + kernel for kernel in kernels if particle.name in kernel.particles + ) + producers = tuple( + kernel for kernel in involved if kernel.particles[2] == particle.name + ) + # Basis-sensitive metadata is derived later, not rewritten here. + if not producers or any( + kernel.input_ordering_ids + or kernel.output_ordering_id + or kernel.evaluation_equivalence_verified + or any( + name == particle.name and leg != -1 + for name, leg in zip( + kernel.particles, kernel.source_particle_legs, strict=True + ) + ) + for kernel in involved + ): + continue + dimension = particle.component_dimension + if any( + len(kernel.component_expressions) != dimension for kernel in producers + ): + continue + for kernel in involved: + if kernel.kind not in exact_kernels: + exact_kernels[kernel.kind] = _exact_kernel_components(kernel) + # Algebra on an unsupported floating coefficient could round away + # a small difference. Leave that whole current basis untouched. + if any(exact_kernels[kernel.kind] is None for kernel in involved): + continue + basis = _consumer_basis( + particle.name, + dimension, + involved, + exact_kernels=exact_kernels, + model_symbols=model_symbols, + ) + # Keep an entirely unobserved family unchanged: removing its last + # component would require eliminating the family, not a basis change. + if basis and len(basis) < dimension: + bases[particle.name] = basis + dimensions[particle.name] = dimension + if not bases: + return particles, kernels + particles = tuple( + replace(particle, component_dimension=len(bases[particle.name])) + if particle.name in bases + else particle + for particle in particles + ) + kernels = tuple( + _reduce_kernel( + kernel, + bases, + dimensions, + exact_kernels=exact_kernels, + model_symbols=model_symbols, + ) + for kernel in kernels + ) + + +def _exact_kernel_components( + kernel: CompiledOrientedKernel, +) -> tuple[_sym.Expression, ...] | None: + """Admit only numeric atoms on which the change of basis is exact.""" + from symbolica import AtomType + + expressions = tuple( + _sym._exact_binary64_coefficients(_sym.E(source)) + for source in kernel.component_expressions + ) + pending = list(expressions) + checked: list[_sym.Expression] = [] + exact_units = tuple(_sym.E(str(value)) for value in (0, 1, -1)) + unit_replacements: list[_sym.Replacement] = [] + imaginary_unit = _sym.E("1i") + while pending: + atom = pending.pop() + kind = atom.get_type() + if kind == AtomType.Var: + continue + if kind != AtomType.Num: + pending.extend(atom) + continue + if any(bool(atom.matches(previous)) for previous in checked): + continue + checked.append(atom) + # Numeric equality uses the exact stored rational value of Float atoms + # in Symbolica, including arbitrary precision. It therefore recognizes + # floating units without rounding a nearby coefficient onto that unit. + unit = next((value for value in exact_units if bool(atom == value)), None) + if unit is not None: + if not bool(atom.matches(unit)): + unit_replacements.append(_sym.Replacement(atom, unit)) + continue + # Symbolica's rational-polynomial conversion rejects floats, but also + # complex rational numbers. Split the latter and require a literal + # roundtrip, so this check never promotes a rounded Float to a rational. + try: + real = ((atom + atom.conj()) / 2).to_rational_polynomial().to_expression() + imag = ( + ((atom - atom.conj()) / (2 * imaginary_unit)) + .to_rational_polynomial() + .to_expression() + ) + except (TypeError, ValueError): + return None + if not bool(atom.matches(real + imaginary_unit * imag)): + return None + return tuple( + expression.replace_multiple(unit_replacements) + if unit_replacements + else expression + for expression in expressions + ) + + +def _consumer_basis( + particle: str, + dimension: int, + kernels: Sequence[CompiledOrientedKernel], + *, + exact_kernels: Mapping[int, tuple[_sym.Expression, ...] | None], + model_symbols: ModelSymbolRegistry, +) -> _ComponentBasis | None: + zero = _sym.E("0") + columns: list[list[_sym.Expression]] = [[] for _ in range(dimension)] + for kernel in kernels: + for side, name in zip(("left", "right"), kernel.particles[:2], strict=True): + if name != particle: + continue + currents = tuple( + model_symbols.kernel_component(kernel.kind, side, index) + for index in range(dimension) + ) + current_symbols = set(currents) + expressions = exact_kernels[kernel.kind] + assert expressions is not None + for expression in expressions: + expanded = expression.expand() + present = set(expanded.get_all_symbols(False)) + try: + coefficients = tuple( + expanded.coefficient(current).expand() + if current in present + else zero + for current in currents + ) + except (TypeError, ValueError): + return None + if any( + current_symbols.intersection(coefficient.get_all_symbols(False)) + for coefficient in coefficients + ): + return None + reconstructed = sum( + ( + coefficient * current + for coefficient, current in zip( + coefficients, currents, strict=True + ) + ), + zero, + ) + if (expanded - reconstructed).expand().to_canonical_string() != "0": + return None + for column, coefficient in zip(columns, coefficients, strict=True): + column.append(coefficient) + + groups: list[list[tuple[int, int]]] = [] + known: dict[tuple[_sym.Expression, ...], tuple[int, int]] = {} + for index, column in enumerate(columns): + # Compare expanded atoms themselves, independently of text formatting + # and the parentheses chosen for complex numeric coefficients. + key = tuple(column) + if not key or all(value == zero for value in key): + continue + existing = known.get(key) + if existing is None: + group = len(groups) + groups.append([(index, 1)]) + known[key] = (group, 1) + negative = tuple((-value).expand() for value in column) + known[negative] = (group, -1) + else: + group, sign = existing + groups[group].append((index, sign)) + return tuple(tuple(group) for group in groups) + + +def _reduce_kernel( + kernel: CompiledOrientedKernel, + bases: dict[str, _ComponentBasis], + dimensions: dict[str, int], + *, + exact_kernels: Mapping[int, tuple[_sym.Expression, ...] | None], + model_symbols: ModelSymbolRegistry, +) -> CompiledOrientedKernel: + if not any(name in bases for name in kernel.particles): + return kernel + zero = _sym.E("0") + expressions = exact_kernels[kernel.kind] + assert expressions is not None + output_basis = bases.get(kernel.particles[2]) + if output_basis is not None: + expressions = tuple( + sum((sign * expressions[index] for index, sign in group), zero) + for group in output_basis + ) + replacements: list[_sym.Replacement] = [] + for side, name in zip(("left", "right"), kernel.particles[:2], strict=True): + if name not in bases: + continue + representatives = { + group[0][0]: model_symbols.kernel_component(kernel.kind, side, index) + for index, group in enumerate(bases[name]) + } + replacements.extend( + _sym.Replacement( + model_symbols.kernel_component(kernel.kind, side, index), + representatives.get(index, zero), + ) + for index in range(dimensions[name]) + ) + return replace( + kernel, + component_expressions=tuple( + _canonicalize_oriented_kernel_component( + expression.replace_multiple(replacements) + if replacements + else expression + ).to_canonical_string() + for expression in expressions + ), + ) diff --git a/pyamplicol/src/pyamplicol/models/compiler_color_flow.py b/pyamplicol/src/pyamplicol/models/compiler_color_flow.py new file mode 100644 index 0000000000..024df21180 --- /dev/null +++ b/pyamplicol/src/pyamplicol/models/compiler_color_flow.py @@ -0,0 +1,527 @@ +# SPDX-License-Identifier: 0BSD +"""Compiler-owned color-flow auxiliaries derived from proven model tensors.""" + +from __future__ import annotations + +import hashlib +import json +from collections.abc import Sequence +from dataclasses import replace + +from .._internal.physics.symbols import ModelSymbolRegistry +from . import compiler_symbolica as _sym +from .compiler_kernels import ( + _canonicalize_oriented_kernel_component, + _remap_kernel_symbols, +) +from .contracts import ( + CompiledLCColorTransitionTerm, + CompiledOrientedKernel, + CompiledParticleRecord, + CompiledPropagatorRecord, + compiled_particle_component_dimension, +) +from .tensors import normalize_color_expression + +_U1_SUBTRACTION_AUXILIARY = "u1-subtraction-color-flow-vector" + + +class _CompiledLCColorTransitionCompilation(tuple): + """Transition tuple carrying compiler-owned closure companions. + + The oriented-kernel compiler call site predates recurrence closure + contracts and assigns only the transition tuple. ``CompiledOrientedKernel`` + consumes this private companion immediately and persists the two catalogs + as separate fields. + """ + + compiler_closure_terms: tuple[CompiledLCColorTransitionTerm, ...] + + def __new__( + cls, + transition_terms: tuple[CompiledLCColorTransitionTerm, ...], + closure_terms: tuple[CompiledLCColorTransitionTerm, ...], + ) -> _CompiledLCColorTransitionCompilation: + instance = super().__new__(cls, transition_terms) + instance.compiler_closure_terms = closure_terms + return instance + + +def compile_lc_color_transition_terms( + kernel: CompiledOrientedKernel, + oriented_representations: tuple[int, int, int], + *, + proof_source: str, + provenance: tuple[tuple[str, str], ...] = (), + tensor_role_representations: tuple[int, int, int] | None = None, +) -> tuple[CompiledLCColorTransitionTerm, ...]: + """Compile exact ordered-word operations from a certified local tensor. + + This function is part of model compilation. Runtime and recurrence-builder + code consume its closed records and never repeat tensor-family inference. + """ + + structure = kernel.color_projection_structure + if structure is None: + raise ValueError(f"kernel {kernel.kind} has no certified color projection") + tensor_representations = ( + oriented_representations + if tensor_role_representations is None + else tuple(int(value) for value in tensor_role_representations) + ) + if tuple(abs(value) for value in tensor_representations) != tuple( + abs(value) for value in oriented_representations + ): + raise ValueError( + f"kernel {kernel.kind} tensor-role representations " + f"{tensor_representations!r} do not match its oriented current shapes " + f"{oriented_representations!r}" + ) + shapes = tuple(_lc_color_shape(value) for value in oriented_representations) + result_shape = shapes[2] + + def term( + permutation: tuple[int, int], + operation: str, + result_component_kind: str | None, + exact_factor_expression: str = "1", + *, + closure: bool = False, + ) -> CompiledLCColorTransitionTerm: + result_component_role = ( + "passive" + if operation == "concatenate-join" and abs(output_representation) == 1 + else "active" + if operation in {"concatenate-join", "inherit-left", "inherit-right"} + else "none" + ) + payload = { + "color_expression": kernel.color_expression, + "color_source": kernel.color_source, + "exact_factor_expression": exact_factor_expression, + "input_permutation": list(permutation), + "input_shape_kinds": list(shapes[:2]), + "kernel_kind": kernel.kind, + "operation": operation, + "oriented_representations": list(oriented_representations), + "tensor_role_representations": list(tensor_representations), + "proof_source": proof_source, + "result_component_kind": result_component_kind, + "result_component_role": result_component_role, + "result_shape_kind": None if closure else result_shape, + "source_particle_legs": list(kernel.source_particle_legs), + "structure": structure, + } + digest = hashlib.sha256( + json.dumps(payload, sort_keys=True, separators=(",", ":")).encode("utf-8") + ).hexdigest() + term_provenance = tuple( + sorted( + ( + ("compiler-proof-source", proof_source), + ( + "contract-kind", + "closure" if closure else "transition", + ), + ("kernel-kind", str(kernel.kind)), + ( + "source-particle-legs", + ",".join(str(value) for value in kernel.source_particle_legs), + ), + *provenance, + ) + ) + ) + return CompiledLCColorTransitionTerm( + input_permutation=permutation, + reverse_parent_mask=0, + component_operation=operation, + result_component_kind=result_component_kind, + result_component_role=result_component_role, + input_shape_kinds=(shapes[0], shapes[1]), + result_shape_kind=None if closure else result_shape, + exact_factor_expression=exact_factor_expression, + proof_digest=digest, + provenance=term_provenance, + ) + + def compiled( + *transition_terms: CompiledLCColorTransitionTerm, + ) -> tuple[CompiledLCColorTransitionTerm, ...]: + left_representation, right_representation = oriented_representations[:2] + if abs(left_representation) == abs(right_representation) == 1: + closure_kind = None + elif {left_representation, right_representation} == {3, -3}: + closure_kind = "open-string" + elif left_representation == right_representation == 8: + closure_kind = "trace" + else: + closure_terms: tuple[CompiledLCColorTransitionTerm, ...] = () + return _CompiledLCColorTransitionCompilation( + tuple(transition_terms), + closure_terms, + ) + closure_terms = ( + term( + (0, 1), + "close", + closure_kind, + closure=True, + ), + ) + return _CompiledLCColorTransitionCompilation( + tuple(transition_terms), + closure_terms, + ) + + left_representation, right_representation, output_representation = ( + oriented_representations + ) + if structure == "singlet": + if oriented_representations != (1, 1, 1): + raise ValueError( + "a colored literal-singlet transition requires explicit contact " + f"provenance; kernel {kernel.kind} has {oriented_representations!r}" + ) + return compiled(term((0, 1), "empty", None)) + if structure == "color-identity": + colored_inputs = tuple( + index + for index, representation in enumerate(oriented_representations[:2]) + if abs(representation) != 1 + ) + if len(colored_inputs) == 0: + return compiled(term((0, 1), "empty", None)) + if len(colored_inputs) == 1: + operation = "inherit-left" if colored_inputs[0] == 0 else "inherit-right" + return compiled(term((0, 1), operation, None)) + if len(colored_inputs) == 2 and abs(output_representation) == 1: + if {left_representation, right_representation} == {3, -3}: + permutation = (0, 1) if left_representation == 3 else (1, 0) + result_component = "open-string" + elif left_representation == right_representation == 8: + permutation = (0, 1) + result_component = "trace" + else: + raise ValueError( + f"kernel {kernel.kind} has unsupported identity contraction " + f"{oriented_representations!r}" + ) + return compiled( + term( + permutation, + "concatenate-join", + result_component, + ) + ) + raise ValueError( + f"kernel {kernel.kind} has unsupported color-identity orientation " + f"{oriented_representations!r}" + ) + if structure == "fundamental-generator": + try: + fundamental = tensor_representations.index(3) + antifundamental = tensor_representations.index(-3) + adjoint = tensor_representations.index(8) + except ValueError as exc: + raise ValueError( + f"kernel {kernel.kind} fundamental generator has invalid oriented " + f"tensor roles {tensor_representations!r} for current shapes " + f"{oriented_representations!r}" + ) from exc + tensor_output_representation = tensor_representations[2] + if tensor_output_representation == -3: + permutation = (fundamental, adjoint) + result_component = "open-string" + elif tensor_output_representation == 3: + permutation = (adjoint, antifundamental) + result_component = "open-string" + elif tensor_output_representation == 8: + permutation = (fundamental, antifundamental) + result_component = "adjoint-segment" + else: + raise ValueError( + f"kernel {kernel.kind} fundamental generator has unsupported output " + f"tensor representation {tensor_output_representation}" + ) + if set(permutation) != {0, 1}: + raise ValueError( + f"kernel {kernel.kind} fundamental generator output is not slot 2" + ) + return compiled( + term( + permutation, + "concatenate-join", + result_component, + ) + ) + if structure == "adjoint-structure-constant": + if oriented_representations != (8, 8, 8): + raise ValueError( + f"kernel {kernel.kind} structure constant has representations " + f"{oriented_representations!r}" + ) + return compiled( + term((0, 1), "concatenate-join", "adjoint-segment"), + term( + (1, 0), + "concatenate-join", + "adjoint-segment", + "-1", + ), + ) + raise ValueError( + f"kernel {kernel.kind} has unsupported recurrence color projection " + f"{structure!r}" + ) + + +def _lc_color_shape(representation: int) -> str: + return { + 1: "singlet-forest", + 3: "fundamental-open-string", + -3: "antifundamental-open-string", + 8: "adjoint-segment", + }.get(representation) or _unsupported_lc_color_shape(representation) + + +def _unsupported_lc_color_shape(representation: int) -> str: + raise ValueError( + f"recurrence LC color witnesses do not support representation {representation}" + ) + + +def synthesize_fundamental_fierz_auxiliaries( + particles: Sequence[CompiledParticleRecord], + kernels: Sequence[CompiledOrientedKernel], + propagators: Sequence[CompiledPropagatorRecord], + *, + model_symbols: ModelSymbolRegistry, +) -> tuple[tuple[CompiledParticleRecord, ...], tuple[CompiledOrientedKernel, ...]]: + """Materialize the singlet term of a proven fundamental Fierz identity. + + A certified ``T^a_ij`` kernel implies the SU(3) color-flow decomposition + into an ordinary line connection and a ``1 / N_c`` singlet subtraction. + The synthetic current reuses the exact compiled Lorentz/coupling kernel; + no particle name or PDG label participates in the proof. + + This milestone deliberately targets the supported QCD-like contract: + massless self-conjugate adjoint vectors with a default propagator and + fundamental fermion lines. Models outside that contract keep the existing + exact sector-partition fallback instead of acquiring an unproven auxiliary. + """ + + particle_by_name = {particle.name: particle for particle in particles} + propagator_by_particle = { + propagator.particle: propagator for propagator in propagators + } + eligible_sources: set[str] = set() + for kernel in kernels: + source_name = _eligible_adjoint_source( + kernel, + particle_by_name, + propagator_by_particle, + ) + if source_name is not None: + eligible_sources.add(source_name) + if not eligible_sources: + return tuple(particles), tuple(kernels) + + used_pdgs = {abs(particle.pdg_code) for particle in particles} + next_pdg = max(9_100_000, max(used_pdgs, default=0) + 1) + + def allocate_pdg() -> int: + nonlocal next_pdg + while next_pdg in used_pdgs: + next_pdg += 1 + result = next_pdg + used_pdgs.add(result) + next_pdg += 1 + return result + + quantum_number_names = sorted( + { + name + for particle in particles + for name, _expression in particle.quantum_numbers + } + | {"electric_charge"} + ) + auxiliary_by_source: dict[str, CompiledParticleRecord] = {} + for source_name in sorted(eligible_sources): + source = particle_by_name[source_name] + auxiliary_name = f"__pyamplicol_u1_subtraction_{source_name}" + auxiliary_by_source[source_name] = CompiledParticleRecord( + name=auxiliary_name, + antiname=auxiliary_name, + pdg_code=allocate_pdg(), + spin=source.spin, + color=1, + mass=source.mass, + width=source.width, + charge=0.0, + quantum_numbers=tuple((name, "0") for name in quantum_number_names), + ghost_number=0, + propagating=False, + goldstoneboson=False, + propagator=None, + component_dimension=compiled_particle_component_dimension(source), + auxiliary_kind=_U1_SUBTRACTION_AUXILIARY, + ) + + synthetic: list[CompiledOrientedKernel] = [] + next_kind = max((kernel.kind for kernel in kernels), default=-1) + 1 + for kernel in kernels: + source_name = _kernel_adjoint_source( + kernel, + particle_by_name, + eligible_sources, + ) + if source_name is None: + continue + source_slot = kernel.particles.index(source_name) + representations = tuple( + particle_by_name[name].color for name in kernel.particles + ) + if source_slot == 2 and representations[:2] != (3, -3): + # Match the canonical oriented closure used by the LC recursion. + # The reverse input ordering is an equivalent physical kernel, but + # materializing both would create duplicate subtraction currents. + continue + + auxiliary = auxiliary_by_source[source_name] + synthetic_particles = list(kernel.particles) + synthetic_particles[source_slot] = auxiliary.name + synthetic_representations = tuple( + auxiliary.color if index == source_slot else representations[index] + for index in range(3) + ) + colored_legs = tuple( + index + 1 + for index, representation in enumerate(synthetic_representations) + if abs(representation) != 1 + ) + if len(colored_legs) != 2: + raise ValueError( + f"Fierz auxiliary for kernel {kernel.kind} did not leave two " + "fundamental color legs" + ) + color_source = f"UFO::Identity({colored_legs[0]},{colored_legs[1]})" + color_expression = normalize_color_expression( + color_source, + synthetic_representations, + ).expression + + kind = next_kind + len(synthetic) + coefficient = complex(*(kernel.color_projection_coefficient or (1.0, 0.0))) + if source_slot == 2: + fundamental_dimension = abs(representations[0]) + coefficient /= fundamental_dimension + source_legs = list(kernel.source_particle_legs) + source_legs[source_slot] = -1 + synthetic_kernel = CompiledOrientedKernel( + kind=kind, + term_id=kernel.term_id, + vertex=f"{kernel.vertex}::u1-subtraction", + particles=tuple(synthetic_particles), + source_particle_legs=tuple(source_legs), + component_expressions=tuple( + _canonicalize_oriented_kernel_component( + _remap_kernel_symbols( + _sym.E(component), + old_kind=kernel.kind, + new_kind=kind, + model_symbols=model_symbols, + ) + ).to_canonical_string() + for component in kernel.component_expressions + ), + coupling_expression=kernel.coupling_expression, + coupling_orders=kernel.coupling_orders, + runtime_parameters=kernel.runtime_parameters, + color_source=color_source, + color_expression=color_expression, + color_projection_structure="color-identity", + color_projection_coefficient=( + float(coefficient.real), + float(coefficient.imag), + ), + lc_color_normalization_power=kernel.lc_color_normalization_power, + term_ids=kernel.term_ids, + ) + synthetic.append( + replace( + synthetic_kernel, + lc_color_transition_terms=compile_lc_color_transition_terms( + synthetic_kernel, + synthetic_representations, # type: ignore[arg-type] + proof_source="fundamental-fierz-identity-v1", + provenance=(("source-kernel-kind", str(kernel.kind)),), + ), + ) + ) + + return ( + (*particles, *auxiliary_by_source.values()), + (*kernels, *synthetic), + ) + + +def _kernel_adjoint_source( + kernel: CompiledOrientedKernel, + particles: dict[str, CompiledParticleRecord], + eligible_sources: set[str], +) -> str | None: + if kernel.color_projection_structure != "fundamental-generator": + return None + matches = tuple(name for name in kernel.particles if name in eligible_sources) + if len(matches) != 1: + return None + source_name = matches[0] + representations = tuple(particles[name].color for name in kernel.particles) + if representations.count(8) != 1: + return None + if sorted(abs(value) for value in representations if value != 8) != [3, 3]: + return None + return source_name + + +def _eligible_adjoint_source( + kernel: CompiledOrientedKernel, + particles: dict[str, CompiledParticleRecord], + propagators: dict[str, CompiledPropagatorRecord], +) -> str | None: + if kernel.color_projection_structure != "fundamental-generator": + return None + representations = tuple(particles[name].color for name in kernel.particles) + adjoint_slots = tuple( + index + for index, representation in enumerate(representations) + if representation == 8 + ) + if len(adjoint_slots) != 1: + return None + if sorted(abs(value) for value in representations if value != 8) != [3, 3]: + return None + if not all( + particles[name].statistics == "fermion" + for index, name in enumerate(kernel.particles) + if index != adjoint_slots[0] + ): + return None + source = particles[kernel.particles[adjoint_slots[0]]] + propagator = propagators.get(source.name) + if ( + source.spin != 3 + or source.mass.upper() != "ZERO" + or source.width.upper() != "ZERO" + or not source.self_conjugate + or (propagator is not None and propagator.custom) + ): + return None + return source.name + + +__all__ = [ + "synthesize_fundamental_fierz_auxiliaries", +] diff --git a/pyamplicol/src/pyamplicol/models/compiler_contact_trees.py b/pyamplicol/src/pyamplicol/models/compiler_contact_trees.py new file mode 100644 index 0000000000..e444bf87b0 --- /dev/null +++ b/pyamplicol/src/pyamplicol/models/compiler_contact_trees.py @@ -0,0 +1,1765 @@ +# SPDX-License-Identifier: 0BSD +"""Color-singlet four-point contact-tree compilation.""" + +from __future__ import annotations + +import math +import re +import sys +from collections.abc import Mapping, Sequence +from dataclasses import replace +from itertools import product + +from .._internal.physics.symbols import ModelSymbolRegistry, symbols +from . import compiler_symbolica as _sym +from .compiler_contacts import ( + _execute_dense_tensor, + _normalized_structure_constant_factors, + _source_structure_constant_factor_orientations, + _source_structure_constant_product_coefficient, +) +from .compiler_kernels import ( + _as_expression, + _canonicalize_oriented_kernel_component, + _function_arguments, + _permutation_sign, + _remap_kernel_symbols, + _replace_expression_symbols, + _spin_axis_labels, + _spin_dimension, + _spin_representations, + _spin_slots, +) +from .compiler_records import _replace_evaluator_constants +from .contact_decomposition import ( + CONTACT_ORBIT_ALGORITHM, + CONTACT_ORBIT_ALGORITHM_VERSION, + HEFT_CONTACT_ORBIT_EVALUATOR_CLASS, + CompiledContactOrbitCertificate, + CompiledContactOrbitStep, + canonical_contact_orbit_steps, +) +from .contracts import ( + CompiledOrientedKernel, + CompiledParticleRecord, + CompiledVertexTerm, + _ContactTreeNode, + compiled_particle_component_dimension, +) +from .tensors import normalize_color_expression, project_trilinear_color_expression + +_DIRECT_DENSE_CONTACT_INPUT_WORK = 1_024 +_MAX_STAGED_CONTACT_INPUT_WORK = 256 + + +def _compile_color_singlet_contact_trees( + terms: Sequence[CompiledVertexTerm], + particles: Sequence[CompiledParticleRecord], + *, + start_kind: int, + model_symbols: ModelSymbolRegistry, +) -> tuple[tuple[CompiledParticleRecord, ...], tuple[CompiledOrientedKernel, ...]]: + """Lower arbitrary color-singlet contacts to balanced trivalent trees.""" + + particle_by_name = {particle.name: particle for particle in particles} + used_pdgs = {abs(particle.pdg_code) for particle in particles} + next_pdg = max(9_000_000, max(used_pdgs, default=0) + 1) + auxiliary_particles: list[CompiledParticleRecord] = [] + kernels: list[CompiledOrientedKernel] = [] + final_template_cache: dict[ + tuple[object, ...], + tuple[int, tuple[_sym.Expression, ...]], + ] = {} + + def allocate_pdg() -> int: + nonlocal next_pdg + while next_pdg in used_pdgs: + next_pdg += 1 + result = next_pdg + used_pdgs.add(result) + next_pdg += 1 + return result + + for term in terms: + if term.valence < 4 or not _contact_term_is_color_singlet(term): + continue + if term.valence == 4 and "ufo_momentum_" not in term.lorentz_expression: + continue + source_particles = tuple(particle_by_name[name] for name in term.particles) + scalar_product_tree = all(particle.spin == 1 for particle in source_particles) + oriented_result_particles: set[str] = set() + for result_leg in range(term.valence): + source_result = source_particles[result_leg] + if source_result.name in oriented_result_particles: + continue + oriented_result_particles.add(source_result.name) + input_legs = tuple(leg for leg in range(term.valence) if leg != result_leg) + + def build_node( + legs: tuple[int, ...], + *, + source_particles: tuple[CompiledParticleRecord, ...] = source_particles, + scalar_product_tree: bool = scalar_product_tree, + term: CompiledVertexTerm = term, + result_leg: int = result_leg, + ) -> _ContactTreeNode: + if len(legs) == 1: + leg = legs[0] + return _ContactTreeNode( + legs=legs, + particle=source_particles[leg], + physical_leg=leg, + ) + split = len(legs) // 2 + left_node = build_node(legs[:split]) + right_node = build_node(legs[split:]) + auxiliary_name = ( + f"__pyamplicol_contact_tree_{term.id}_r{result_leg}_" + + "_".join(str(leg) for leg in legs) + ) + auxiliary_dimension = ( + 1 + if scalar_product_tree + else sum( + _spin_dimension(source_particles[leg].spin) + 4 for leg in legs + ) + ) + auxiliary = CompiledParticleRecord( + name=auxiliary_name, + antiname=auxiliary_name, + pdg_code=allocate_pdg(), + spin=-1, + color=1, + mass="ZERO", + width="ZERO", + charge=0.0, + quantum_numbers=(("electric_charge", "0"),), + ghost_number=0, + propagating=False, + goldstoneboson=False, + propagator=None, + component_dimension=auxiliary_dimension, + auxiliary_kind=( + f"ufo-contact-tree:{term.id}:result-{result_leg}:" + + ",".join(str(leg) for leg in legs) + ), + ) + auxiliary_particles.append(auxiliary) + node = _ContactTreeNode( + legs=legs, + particle=auxiliary, + left=left_node, + right=right_node, + ) + _append_contact_tree_partial_kernels( + kernels, + term=term, + node=node, + source_particles=source_particles, + scalar_product_tree=scalar_product_tree, + start_kind=start_kind, + model_symbols=model_symbols, + ) + return node + + split = len(input_legs) // 2 + left_root = build_node(input_legs[:split]) + right_root = build_node(input_legs[split:]) + assignment_multiplicity = _contact_tree_assignment_multiplicity( + input_legs, + source_particles, + left_root, + right_root, + ) + _append_contact_tree_final_kernels( + kernels, + term=term, + source_particles=source_particles, + left_node=left_root, + right_node=right_root, + result_leg=result_leg, + result_name=particle_by_name[source_result.antiname].name, + scalar_product_tree=scalar_product_tree, + assignment_multiplicity=assignment_multiplicity, + start_kind=start_kind, + template_cache=final_template_cache, + model_symbols=model_symbols, + ) + return tuple(auxiliary_particles), tuple(kernels) + + +def _compile_heft_colored_contact_trees( + terms: Sequence[CompiledVertexTerm], + particles: Sequence[CompiledParticleRecord], + *, + start_kind: int, + model_symbols: ModelSymbolRegistry, +) -> tuple[tuple[CompiledParticleRecord, ...], tuple[CompiledOrientedKernel, ...]]: + """Lower only the scalar-HEFT one- and two-``f`` contact families.""" + + particle_by_name = {particle.name: particle for particle in particles} + used_pdgs = {abs(particle.pdg_code) for particle in particles} + next_pdg = max(9_050_000, max(used_pdgs, default=0) + 1) + auxiliary_particles: list[CompiledParticleRecord] = [] + kernels: list[CompiledOrientedKernel] = [] + + def allocate_pdg() -> int: + nonlocal next_pdg + while next_pdg in used_pdgs: + next_pdg += 1 + result = next_pdg + used_pdgs.add(result) + next_pdg += 1 + return result + + for term in terms: + source_particles = tuple(particle_by_name[name] for name in term.particles) + topology = _heft_contact_color_topology( + term, + source_particles, + model_symbols=model_symbols, + ) + if topology is None: + continue + factors, source_coefficient = topology + singlet_leg = next( + index + for index, particle in enumerate(source_particles) + if particle.color == 1 + ) + colored_legs = tuple( + index + for index, particle in enumerate(source_particles) + if particle.color == 8 + ) + + def leaf( + leg: int, + *, + source_particles: tuple[CompiledParticleRecord, ...] = source_particles, + ) -> _ContactTreeNode: + return _ContactTreeNode( + legs=(leg,), + particle=source_particles[leg], + physical_leg=leg, + ) + + def combine( + left: _ContactTreeNode, + right: _ContactTreeNode, + *, + result_leg: int, + label: str, + factor: tuple[int, int, int] | None, + left_token: int | None, + right_token: int | None, + output_token: int, + term: CompiledVertexTerm = term, + source_particles: tuple[CompiledParticleRecord, ...] = source_particles, + ) -> _ContactTreeNode: + legs = (*left.legs, *right.legs) + auxiliary_name = ( + f"__pyamplicol_heft_contact_{term.id}_r{result_leg}_{label}" + ) + auxiliary = CompiledParticleRecord( + name=auxiliary_name, + antiname=auxiliary_name, + pdg_code=allocate_pdg(), + spin=-1, + color=8, + mass="ZERO", + width="ZERO", + charge=0.0, + quantum_numbers=(("electric_charge", "0"),), + ghost_number=0, + propagating=False, + goldstoneboson=False, + propagator=None, + component_dimension=_heft_node_component_dimension( + legs, + source_particles, + include_momenta=term.valence == 4, + ), + auxiliary_kind=( + f"ufo-heft-contact-tree:{term.id}:result-{result_leg}:" + + ",".join(str(leg) for leg in legs) + ), + ) + auxiliary_particles.append(auxiliary) + node = _ContactTreeNode( + legs=legs, + particle=auxiliary, + left=left, + right=right, + ) + _append_heft_contact_partial_kernels( + kernels, + term=term, + node=node, + result_leg=result_leg, + source_particles=source_particles, + factor=factor, + left_token=left_token, + right_token=right_token, + output_token=output_token, + include_momenta=term.valence == 4, + start_kind=start_kind, + model_symbols=model_symbols, + ) + return node + + oriented_result_particles: set[str] = set() + for result_leg, source_result in enumerate(source_particles): + if source_result.name in oriented_result_particles: + continue + oriented_result_particles.add(source_result.name) + result_token = result_leg + 1 if source_result.color == 8 else None + input_legs = tuple(leg for leg in range(term.valence) if leg != result_leg) + + if term.valence == 4: + factor = factors[0] + if result_token is None: + pair_legs = tuple(sorted(colored_legs[:2])) + pair_tokens = (pair_legs[0] + 1, pair_legs[1] + 1) + output_token = next( + token for token in factor if token not in set(pair_tokens) + ) + left_root = combine( + leaf(pair_legs[0]), + leaf(pair_legs[1]), + result_leg=result_leg, + label="f", + factor=factor, + left_token=pair_tokens[0], + right_token=pair_tokens[1], + output_token=output_token, + ) + remaining_leg = next( + leg for leg in input_legs if leg not in set(pair_legs) + ) + right_root = leaf(remaining_leg) + left_token = output_token + right_token = remaining_leg + 1 + final_factor = None + else: + remaining_colored = tuple( + leg for leg in colored_legs if leg != result_leg + ) + if len(remaining_colored) != 2: + raise ValueError("scalar-HEFT Hggg tree has no gluon pair") + standalone_leg, identity_leg = remaining_colored + left_root = leaf(standalone_leg) + right_root = combine( + leaf(identity_leg), + leaf(singlet_leg), + result_leg=result_leg, + label="identity", + factor=None, + left_token=identity_leg + 1, + right_token=None, + output_token=identity_leg + 1, + ) + left_token = standalone_leg + 1 + right_token = identity_leg + 1 + final_factor = factor + else: + dummy = next(value for value in factors[0] if value < 0) + if result_token is None: + roots: list[_ContactTreeNode] = [] + for index, factor in enumerate(factors): + pair_legs = tuple( + sorted(value - 1 for value in factor if value > 0) + ) + roots.append( + combine( + leaf(pair_legs[0]), + leaf(pair_legs[1]), + result_leg=result_leg, + label=f"f{index}", + factor=factor, + left_token=pair_legs[0] + 1, + right_token=pair_legs[1] + 1, + output_token=dummy, + ) + ) + left_root, right_root = roots + left_token = right_token = dummy + final_factor = None + else: + result_factor = next( + factor for factor in factors if result_token in factor + ) + outer_factor = next( + factor for factor in factors if factor != result_factor + ) + outer_legs = tuple( + sorted(value - 1 for value in outer_factor if value > 0) + ) + left_root = combine( + leaf(outer_legs[0]), + leaf(outer_legs[1]), + result_leg=result_leg, + label="outer-f", + factor=outer_factor, + left_token=outer_legs[0] + 1, + right_token=outer_legs[1] + 1, + output_token=dummy, + ) + remaining_token = next( + value + for value in result_factor + if value > 0 and value != result_token + ) + remaining_leg = remaining_token - 1 + right_root = combine( + leaf(remaining_leg), + leaf(singlet_leg), + result_leg=result_leg, + label="identity", + factor=None, + left_token=remaining_token, + right_token=None, + output_token=remaining_token, + ) + left_token = dummy + right_token = remaining_token + final_factor = result_factor + + assignment_multiplicity = _contact_tree_assignment_multiplicity( + input_legs, + source_particles, + left_root, + right_root, + ) + _append_heft_contact_final_kernels( + kernels, + term=term, + source_particles=source_particles, + left_node=left_root, + right_node=right_root, + result_leg=result_leg, + result_name=particle_by_name[source_result.antiname].name, + factor=final_factor, + left_token=left_token, + right_token=right_token, + output_token=result_token, + source_coefficient=source_coefficient, + assignment_multiplicity=assignment_multiplicity, + start_kind=start_kind, + model_symbols=model_symbols, + ) + + return tuple(auxiliary_particles), tuple(kernels) + + +def _record_heft_contact_orbit_certificates( + terms: Sequence[CompiledVertexTerm], + particles: Sequence[CompiledParticleRecord], + *, + model_symbols: ModelSymbolRegistry, +) -> tuple[CompiledVertexTerm, ...]: + """Certify the exact physical-assignment orbit of supported HEFT trees.""" + + particle_by_name = {particle.name: particle for particle in particles} + certified: list[CompiledVertexTerm] = [] + for term in terms: + source_particles = tuple(particle_by_name[name] for name in term.particles) + if ( + _heft_contact_color_topology( + term, + source_particles, + model_symbols=model_symbols, + ) + is None + ): + certified.append(term) + continue + if term.contact_orbit_certificate is not None: + raise ValueError( + f"scalar-HEFT term {term.id} already has a contact-orbit certificate" + ) + equivalence_classes: dict[str, int] = {} + leg_classes = tuple( + equivalence_classes.setdefault(particle.name, len(equivalence_classes)) + for particle in source_particles + ) + certified.append( + replace( + term, + contact_orbit_certificate=CompiledContactOrbitCertificate( + algorithm=CONTACT_ORBIT_ALGORITHM, + algorithm_version=CONTACT_ORBIT_ALGORITHM_VERSION, + term_id=term.id, + vertex=term.vertex, + particles=term.particles, + color_expression=term.color_expression, + lorentz_expression=term.lorentz_expression, + coupling_expression=term.coupling_expression, + evaluator_class=HEFT_CONTACT_ORBIT_EVALUATOR_CLASS, + physical_leg_equivalence_classes=leg_classes, + reconstruction_factor="1", + ), + ) + ) + return tuple(certified) + + +def _heft_contact_orbit_step( + term: CompiledVertexTerm, + *, + stage: str, + result_leg: int, + left_covered_legs: tuple[int, ...], + right_covered_legs: tuple[int, ...], + source_particle_legs: tuple[int, int, int], +) -> CompiledContactOrbitStep: + return CompiledContactOrbitStep( + algorithm=CONTACT_ORBIT_ALGORITHM, + algorithm_version=CONTACT_ORBIT_ALGORITHM_VERSION, + term_id=term.id, + stage=stage, + result_leg=result_leg, + left_covered_legs=left_covered_legs, + right_covered_legs=right_covered_legs, + source_particle_legs=source_particle_legs, + reconstruction_factor="1", + ) + + +def _heft_contact_color_topology( + term: CompiledVertexTerm, + source_particles: Sequence[CompiledParticleRecord], + *, + model_symbols: ModelSymbolRegistry, +) -> tuple[tuple[tuple[int, int, int], ...], str] | None: + colors = tuple(particle.color for particle in source_particles) + if term.valence == 4 and colors.count(8) == 3 and colors.count(1) == 1: + expected_count = 1 + elif term.valence == 5 and colors.count(8) == 4 and colors.count(1) == 1: + expected_count = 2 + else: + return None + factors = _normalized_structure_constant_factors( + term.color_expression, + expected_count=expected_count, + ) + if len(factors) != expected_count: + return None + colored_tokens = { + index + 1 + for index, particle in enumerate(source_particles) + if particle.color == 8 + } + if expected_count == 1: + if set(factors[0]) != colored_tokens or any(value < 1 for value in factors[0]): + return None + else: + dummy_sets = tuple( + {value for value in factor if value < 0} for factor in factors + ) + if any(len(values) != 1 for values in dummy_sets): + return None + shared = dummy_sets[0] & dummy_sets[1] + positives = tuple(value for factor in factors for value in factor if value > 0) + if ( + len(shared) != 1 + or set(positives) != colored_tokens + or len(positives) != len(set(positives)) + or any(len(factor) != 3 for factor in factors) + ): + return None + source_factors = _function_arguments(term.color_source, "f") + if len(source_factors) != expected_count: + return None + compact_source = re.sub(r"\s+", "", term.color_source) + tensor = r"(?:UFO::(?:\{\}::)?)?f\(-?\d+,-?\d+,-?\d+\)" + if ( + re.fullmatch(rf"{tensor}(?:\*{tensor}){{{expected_count - 1}}}", compact_source) + is None + ): + return None + coefficient = _source_structure_constant_product_coefficient( + term.color_source, + model_symbols=model_symbols, + ) + if coefficient is None: + return None + oriented_factors = _source_structure_constant_factor_orientations( + source_factors, factors + ) + if oriented_factors is None: + return None + return oriented_factors, coefficient + + +def _heft_trilinear_color( + particles: tuple[ + CompiledParticleRecord, CompiledParticleRecord, CompiledParticleRecord + ], +) -> tuple[str, str, int]: + representations = tuple(particle.color for particle in particles) + colored_slots = tuple( + index + 1 + for index, representation in enumerate(representations) + if representation == 8 + ) + if len(colored_slots) == 3: + source = "UFO::f(1,2,3)" + expected = "adjoint-structure-constant" + normalization_power = 1 + elif len(colored_slots) == 2 and set(representations) <= {1, 8}: + source = f"UFO::Identity({colored_slots[0]},{colored_slots[1]})" + expected = "color-identity" + normalization_power = 0 + else: + raise ValueError( + f"scalar-HEFT contact node has unsupported colors {representations!r}" + ) + expression = normalize_color_expression(source, representations).expression + structure, coefficient = project_trilinear_color_expression( + expression, + representations, + ) + if structure != expected or coefficient != 1.0 + 0.0j: + raise ValueError( + "scalar-HEFT contact node failed exact trilinear color projection" + ) + return source, expression, normalization_power + + +def _heft_node_base_dimension( + legs: Sequence[int], + source_particles: Sequence[CompiledParticleRecord], +) -> int: + return math.prod(_spin_dimension(source_particles[leg].spin) for leg in legs) + + +def _heft_node_component_dimension( + legs: Sequence[int], + source_particles: Sequence[CompiledParticleRecord], + *, + include_momenta: bool, +) -> int: + base_dimension = _heft_node_base_dimension(legs, source_particles) + return base_dimension * (1 + (4 * len(legs) if include_momenta else 0)) + + +def _heft_node_base_index( + node: _ContactTreeNode, + source_particles: Sequence[CompiledParticleRecord], + component_by_leg: Mapping[int, int], +) -> int: + index = 0 + for leg in node.legs: + dimension = _spin_dimension(source_particles[leg].spin) + component = component_by_leg[leg] + if not 0 <= component < dimension: + raise ValueError("scalar-HEFT contact component is outside its basis") + index = index * dimension + component + return index + + +def _heft_node_base_expression( + kind: int, + side: str, + node: _ContactTreeNode, + source_particles: Sequence[CompiledParticleRecord], + component_by_leg: Mapping[int, int], + *, + model_symbols: ModelSymbolRegistry, +) -> _sym.Expression: + if node.is_leaf: + if node.physical_leg is None: + raise ValueError("scalar-HEFT contact leaf has no source leg") + index = component_by_leg[node.physical_leg] + else: + index = _heft_node_base_index(node, source_particles, component_by_leg) + return model_symbols.kernel_component(kind, side, index) + + +def _heft_node_momentum_weighted_expression( + kind: int, + side: str, + node: _ContactTreeNode, + source_particles: Sequence[CompiledParticleRecord], + component_by_leg: Mapping[int, int], + *, + leg: int, + momentum_component: int, + model_symbols: ModelSymbolRegistry, +) -> _sym.Expression: + base = _heft_node_base_expression( + kind, + side, + node, + source_particles, + component_by_leg, + model_symbols=model_symbols, + ) + if node.is_leaf: + if node.physical_leg != leg: + raise ValueError("scalar-HEFT leaf momentum belongs to another source leg") + return base * model_symbols.kernel_momentum( + kind, + side, + momentum_component, + ) + try: + leg_position = node.legs.index(leg) + except ValueError as error: + raise ValueError( + "scalar-HEFT auxiliary momentum belongs to another source node" + ) from error + base_dimension = _heft_node_base_dimension(node.legs, source_particles) + base_index = _heft_node_base_index(node, source_particles, component_by_leg) + jet_index = ( + base_dimension + + (leg_position * 4 + momentum_component) * base_dimension + + base_index + ) + if jet_index >= compiled_particle_component_dimension(node.particle): + raise ValueError("scalar-HEFT momentum jet is absent from an auxiliary") + return model_symbols.kernel_component(kind, side, jet_index) + + +def _heft_contact_final_component_expressions( + term: CompiledVertexTerm, + source_particles: Sequence[CompiledParticleRecord], + *, + left_node: _ContactTreeNode, + right_node: _ContactTreeNode, + result_leg: int, + kind: int, + canonical_left_side: str, + canonical_right_side: str, + model_symbols: ModelSymbolRegistry, +) -> tuple[_sym.Expression, ...]: + """Rebuild one HEFT Lorentz tensor linearly from authenticated node jets.""" + + input_legs = tuple(leg for leg in range(term.valence) if leg != result_leg) + if set((*left_node.legs, *right_node.legs)) != set(input_legs): + raise ValueError("scalar-HEFT final nodes do not cover every input leg") + component_placeholders = { + leg: tuple( + model_symbols.symbol( + f"compiler::heft_term_{term.id}_result_{result_leg}_" + f"leg_{leg}_component_{component}" + ) + for component in range(_spin_dimension(source_particles[leg].spin)) + ) + for leg in input_legs + } + momentum_placeholders = { + leg: tuple( + model_symbols.symbol( + f"compiler::heft_term_{term.id}_result_{result_leg}_" + f"leg_{leg}_momentum_{component}" + ) + for component in range(4) + ) + for leg in input_legs + } + oracle_term = replace(term, color_source="1", color_expression="1") + oracle_components = eager_color_singlet_vertex_term_components( + oracle_term, + source_particles, + result_leg=result_leg, + input_components=component_placeholders, + input_momenta=momentum_placeholders, + model_symbols=model_symbols, + ) + zero = _sym.E("0") + momentum_zeroes = { + momentum: zero + for values in momentum_placeholders.values() + for momentum in values + } + component_ranges = tuple( + range(_spin_dimension(source_particles[leg].spin)) for leg in input_legs + ) + results: list[_sym.Expression] = [] + for source_component in oracle_components: + expanded = source_component.expand() + translated = zero + recovered = zero + for indices in product(*component_ranges): + component_by_leg = dict(zip(input_legs, indices, strict=True)) + monomial = math.prod( + component_placeholders[leg][component_by_leg[leg]] for leg in input_legs + ) + coefficient = expanded.coefficient(monomial) + if coefficient == zero: + continue + constant = _replace_expression_symbols(coefficient, momentum_zeroes) + reconstructed_coefficient = constant + left_base = _heft_node_base_expression( + kind, + canonical_left_side, + left_node, + source_particles, + component_by_leg, + model_symbols=model_symbols, + ) + right_base = _heft_node_base_expression( + kind, + canonical_right_side, + right_node, + source_particles, + component_by_leg, + model_symbols=model_symbols, + ) + translated += constant * left_base * right_base + for leg in input_legs: + node, side, other_base = ( + (left_node, canonical_left_side, right_base) + if leg in left_node.legs + else (right_node, canonical_right_side, left_base) + ) + for momentum_component, momentum in enumerate( + momentum_placeholders[leg] + ): + momentum_coefficient = _replace_expression_symbols( + coefficient.coefficient(momentum), + momentum_zeroes, + ) + if momentum_coefficient == zero: + continue + reconstructed_coefficient += momentum_coefficient * momentum + translated += ( + momentum_coefficient + * _heft_node_momentum_weighted_expression( + kind, + side, + node, + source_particles, + component_by_leg, + leg=leg, + momentum_component=momentum_component, + model_symbols=model_symbols, + ) + * other_base + ) + if ( + coefficient - reconstructed_coefficient + ).expand().to_canonical_string() != "0": + raise ValueError( + "scalar-HEFT contact Lorentz momentum degree exceeds one" + ) + recovered += reconstructed_coefficient * monomial + if (expanded - recovered).expand().to_canonical_string() != "0": + raise ValueError( + "scalar-HEFT contact Lorentz tensor is not multilinear in its inputs" + ) + results.append(_replace_evaluator_constants(translated)) + return tuple(results) + + +def _append_heft_contact_partial_kernels( + kernels: list[CompiledOrientedKernel], + *, + term: CompiledVertexTerm, + node: _ContactTreeNode, + result_leg: int, + source_particles: Sequence[CompiledParticleRecord], + factor: tuple[int, int, int] | None, + left_token: int | None, + right_token: int | None, + output_token: int, + include_momenta: bool, + start_kind: int, + model_symbols: ModelSymbolRegistry, +) -> None: + if node.left is None or node.right is None: + raise ValueError("scalar-HEFT contact partial is missing a child") + if not node.left.is_leaf or not node.right.is_leaf: + raise ValueError("scalar-HEFT contact partial must join two physical legs") + orientations = ( + ((node.left, node.right),) + if node.left.particle.name == node.right.particle.name + else ((node.left, node.right), (node.right, node.left)) + ) + for actual_left, actual_right in orientations: + kind = start_kind + len(kernels) + swapped = actual_left is node.right + canonical_left_side = "right" if swapped else "left" + canonical_right_side = "left" if swapped else "right" + left_dimension = _spin_dimension(node.left.particle.spin) + right_dimension = _spin_dimension(node.right.particle.spin) + left_components = tuple( + model_symbols.kernel_component(kind, canonical_left_side, index) + for index in range(left_dimension) + ) + right_components = tuple( + model_symbols.kernel_component(kind, canonical_right_side, index) + for index in range(right_dimension) + ) + base_components = tuple( + left * right for left in left_components for right in right_components + ) + encoded_components = list(base_components) + if include_momenta: + for side in (canonical_left_side, canonical_right_side): + for momentum_component in range(4): + momentum = model_symbols.kernel_momentum( + kind, + side, + momentum_component, + ) + encoded_components.extend( + base * momentum for base in base_components + ) + actual_left_token, actual_right_token = ( + (right_token, left_token) if swapped else (left_token, right_token) + ) + sign = ( + 1 + if factor is None + else _permutation_sign( + factor, + (actual_left_token, actual_right_token, output_token), # type: ignore[arg-type] + ) + ) + particles = ( + actual_left.particle, + actual_right.particle, + node.particle, + ) + source_particle_legs = ( + _contact_tree_source_leg(actual_left), + _contact_tree_source_leg(actual_right), + -1, + ) + color_source, color_expression, color_power = _heft_trilinear_color(particles) + kernels.append( + CompiledOrientedKernel( + kind=kind, + term_id=term.id, + vertex=f"{term.vertex}::contact-heft-tree-partial", + particles=tuple(particle.name for particle in particles), + source_particle_legs=source_particle_legs, + component_expressions=tuple( + _canonicalize_oriented_kernel_component( + sign * component + ).to_canonical_string() + for component in encoded_components + ), + coupling_expression="1", + coupling_orders=(), + runtime_parameters=(), + color_source=color_source, + color_expression=color_expression, + lc_color_normalization_power=color_power, + term_ids=(), + contact_orbit_steps=( + _heft_contact_orbit_step( + term, + stage="partial", + result_leg=result_leg, + left_covered_legs=actual_left.legs, + right_covered_legs=actual_right.legs, + source_particle_legs=source_particle_legs, + ), + ), + ) + ) + + +def _append_heft_contact_final_kernels( + kernels: list[CompiledOrientedKernel], + *, + term: CompiledVertexTerm, + source_particles: Sequence[CompiledParticleRecord], + left_node: _ContactTreeNode, + right_node: _ContactTreeNode, + result_leg: int, + result_name: str, + factor: tuple[int, int, int] | None, + left_token: int, + right_token: int | None, + output_token: int | None, + source_coefficient: str, + assignment_multiplicity: int, + start_kind: int, + model_symbols: ModelSymbolRegistry, +) -> None: + orientations = ( + ((left_node, right_node),) + if left_node.particle.name == right_node.particle.name + else ((left_node, right_node), (right_node, left_node)) + ) + canonical_kind = start_kind + len(kernels) + components = _heft_contact_final_component_expressions( + term, + source_particles, + left_node=left_node, + right_node=right_node, + result_leg=result_leg, + kind=canonical_kind, + canonical_left_side="left", + canonical_right_side="right", + model_symbols=model_symbols, + ) + prefactor = ( + _sym.E(source_coefficient) + * symbols.derived_coupling(model_symbols.model_name, term.id) + / assignment_multiplicity + ) + weighted = tuple( + _canonicalize_oriented_kernel_component(component * prefactor) + for component in components + ) + result_particle = next( + particle for particle in source_particles if particle.name == result_name + ) + for actual_left, actual_right in orientations: + kind = start_kind + len(kernels) + swapped = actual_left is right_node + actual_left_token, actual_right_token = ( + (right_token, left_token) if swapped else (left_token, right_token) + ) + sign = ( + 1 + if factor is None + else _permutation_sign( + factor, + (actual_left_token, actual_right_token, output_token), # type: ignore[arg-type] + ) + ) + oriented_components = tuple( + _canonicalize_oriented_kernel_component( + sign + * _remap_kernel_symbols( + component, + old_kind=canonical_kind, + new_kind=kind, + model_symbols=model_symbols, + swap_sides=swapped, + ) + ) + for component in weighted + ) + particles = (actual_left.particle, actual_right.particle, result_particle) + source_particle_legs = ( + _contact_tree_source_leg(actual_left), + _contact_tree_source_leg(actual_right), + result_leg, + ) + color_source, color_expression, color_power = _heft_trilinear_color(particles) + kernels.append( + CompiledOrientedKernel( + kind=kind, + term_id=term.id, + vertex=f"{term.vertex}::contact-heft-tree-final", + particles=tuple(particle.name for particle in particles), + source_particle_legs=source_particle_legs, + component_expressions=tuple( + component.to_canonical_string() for component in oriented_components + ), + coupling_expression="1", + coupling_orders=term.coupling_orders, + runtime_parameters=(f"derived_coupling_{term.id}",), + color_source=color_source, + color_expression=color_expression, + lc_color_normalization_power=color_power, + term_ids=(term.id,), + contact_orbit_steps=( + _heft_contact_orbit_step( + term, + stage="final", + result_leg=result_leg, + left_covered_legs=actual_left.legs, + right_covered_legs=actual_right.legs, + source_particle_legs=source_particle_legs, + ), + ), + ) + ) + + +def _append_contact_tree_partial_kernels( + kernels: list[CompiledOrientedKernel], + *, + term: CompiledVertexTerm, + node: _ContactTreeNode, + source_particles: Sequence[CompiledParticleRecord], + scalar_product_tree: bool, + start_kind: int, + model_symbols: ModelSymbolRegistry, +) -> None: + if node.left is None or node.right is None: + raise ValueError("contact tree partial node is missing a child") + orientations = ( + ((node.left, node.right),) + if node.left.particle.name == node.right.particle.name + else ((node.left, node.right), (node.right, node.left)) + ) + for actual_left, actual_right in orientations: + kind = start_kind + len(kernels) + canonical_left_side = "left" if actual_left is node.left else "right" + canonical_right_side = "right" if actual_right is node.right else "left" + left_payload = _contact_tree_node_payload( + kind, + canonical_left_side, + node.left, + source_particles, + scalar_product_tree=scalar_product_tree, + model_symbols=model_symbols, + ) + right_payload = _contact_tree_node_payload( + kind, + canonical_right_side, + node.right, + source_particles, + scalar_product_tree=scalar_product_tree, + model_symbols=model_symbols, + ) + components = ( + (left_payload[0] * right_payload[0],) + if scalar_product_tree + else (*left_payload, *right_payload) + ) + kernels.append( + CompiledOrientedKernel( + kind=kind, + term_id=term.id, + vertex=f"{term.vertex}::contact-tree-partial", + particles=( + actual_left.particle.name, + actual_right.particle.name, + node.particle.name, + ), + source_particle_legs=( + _contact_tree_source_leg(actual_left), + _contact_tree_source_leg(actual_right), + -1, + ), + component_expressions=tuple( + component.to_canonical_string() for component in components + ), + coupling_expression="1", + coupling_orders=(), + runtime_parameters=(), + color_source="1", + color_expression="1", + lc_color_normalization_power=0, + term_ids=(), + ) + ) + + +def _append_contact_tree_final_kernels( + kernels: list[CompiledOrientedKernel], + *, + term: CompiledVertexTerm, + source_particles: Sequence[CompiledParticleRecord], + left_node: _ContactTreeNode, + right_node: _ContactTreeNode, + result_leg: int, + result_name: str, + scalar_product_tree: bool, + assignment_multiplicity: int, + start_kind: int, + template_cache: dict[ + tuple[object, ...], + tuple[int, tuple[_sym.Expression, ...]], + ], + model_symbols: ModelSymbolRegistry, +) -> None: + orientations = ( + ((left_node, right_node),) + if left_node.particle.name == right_node.particle.name + else ((left_node, right_node), (right_node, left_node)) + ) + canonical_kind = start_kind + len(kernels) + if scalar_product_tree: + left_payload = _contact_tree_node_payload( + canonical_kind, + "left", + left_node, + source_particles, + scalar_product_tree=True, + model_symbols=model_symbols, + ) + right_payload = _contact_tree_node_payload( + canonical_kind, + "right", + right_node, + source_particles, + scalar_product_tree=True, + model_symbols=model_symbols, + ) + canonical_components = ( + left_payload[0] * right_payload[0] * _sym.E(term.lorentz_expression), + ) + else: + template_key = ( + term.lorentz_expression, + tuple(particle.spin for particle in source_particles), + result_leg, + left_node.legs, + right_node.legs, + ) + cached = template_cache.get(template_key) + if cached is None: + canonical_components = _contact_tree_final_component_expressions( + term, + source_particles, + left_node=left_node, + right_node=right_node, + result_leg=result_leg, + kind=canonical_kind, + canonical_left_side="left", + canonical_right_side="right", + model_symbols=model_symbols, + ) + template_cache[template_key] = canonical_kind, canonical_components + else: + template_kind, template_components = cached + canonical_components = tuple( + _remap_kernel_symbols( + component, + old_kind=template_kind, + new_kind=canonical_kind, + model_symbols=model_symbols, + ) + for component in template_components + ) + prefactor = ( + symbols.derived_coupling(model_symbols.model_name, term.id) + / assignment_multiplicity + ) + canonical_weighted_components = tuple( + _canonicalize_oriented_kernel_component(component * prefactor) + for component in canonical_components + ) + for actual_left, actual_right in orientations: + kind = start_kind + len(kernels) + components = tuple( + _remap_kernel_symbols( + component, + old_kind=canonical_kind, + new_kind=kind, + model_symbols=model_symbols, + swap_sides=actual_left is right_node, + ) + for component in canonical_weighted_components + ) + kernels.append( + CompiledOrientedKernel( + kind=kind, + term_id=term.id, + vertex=f"{term.vertex}::contact-tree-final", + particles=( + actual_left.particle.name, + actual_right.particle.name, + result_name, + ), + source_particle_legs=( + _contact_tree_source_leg(actual_left), + _contact_tree_source_leg(actual_right), + result_leg, + ), + component_expressions=tuple( + component.to_canonical_string() for component in components + ), + coupling_expression="1", + coupling_orders=term.coupling_orders, + runtime_parameters=(f"derived_coupling_{term.id}",), + color_source=term.color_source, + color_expression=term.color_expression, + lc_color_normalization_power=term.lc_color_normalization_power, + term_ids=(term.id,), + ) + ) + + +def _contact_tree_node_payload( + kind: int, + side: str, + node: _ContactTreeNode, + source_particles: Sequence[CompiledParticleRecord], + *, + scalar_product_tree: bool, + model_symbols: ModelSymbolRegistry, +) -> tuple[_sym.Expression, ...]: + if node.is_leaf: + if node.physical_leg is None: + raise ValueError("contact tree leaf has no source leg") + dimension = _spin_dimension(source_particles[node.physical_leg].spin) + components = tuple( + model_symbols.kernel_component(kind, side, index) + for index in range(dimension) + ) + if scalar_product_tree: + return components + momenta = tuple( + model_symbols.kernel_momentum(kind, side, index) for index in range(4) + ) + return (*components, *momenta) + dimension = compiled_particle_component_dimension(node.particle) + return tuple( + model_symbols.kernel_component(kind, side, index) for index in range(dimension) + ) + + +def _contact_tree_final_component_expressions( + term: CompiledVertexTerm, + source_particles: Sequence[CompiledParticleRecord], + *, + left_node: _ContactTreeNode, + right_node: _ContactTreeNode, + result_leg: int, + kind: int, + canonical_left_side: str, + canonical_right_side: str, + model_symbols: ModelSymbolRegistry, +) -> tuple[_sym.Expression, ...]: + payload_by_leg = { + **_contact_tree_payload_by_leg( + kind, + canonical_left_side, + left_node, + source_particles, + model_symbols=model_symbols, + ), + **_contact_tree_payload_by_leg( + kind, + canonical_right_side, + right_node, + source_particles, + model_symbols=model_symbols, + ), + } + momentum_by_leg = { + leg: momentum for leg, (_components, momentum) in payload_by_leg.items() + } + momentum_by_leg[result_leg] = tuple( + -sum( + (momentum[component] for momentum in momentum_by_leg.values()), + _sym.E("0"), + ) + for component in range(4) + ) + library = _sym.TensorLibrary.hep_lib_atom() + expression = _sym.E(term.lorentz_expression) + minkowski = _sym.Representation.mink(4) + for leg, momentum in momentum_by_leg.items(): + library.register( + _sym.Tensor.dense( + _sym.TensorName(model_symbols.ufo_momentum_tensor_name(leg + 1))( + minkowski + ), + momentum, + ) + ) + input_work = math.prod( + _spin_dimension(source_particles[leg].spin) for leg in payload_by_leg + ) + if input_work > _DIRECT_DENSE_CONTACT_INPUT_WORK and sys.platform.startswith( + "linux" + ): + result = _execute_contact_tensor_staged( + expression, + library, + source_particles, + payload_by_leg=payload_by_leg, + result_leg=result_leg, + kind=kind, + model_symbols=model_symbols, + ) + else: + for leg, (components, _momentum) in sorted(payload_by_leg.items()): + expression *= _contact_tree_physical_tensor_expression( + library, + kind=kind, + leg=leg, + spin=source_particles[leg].spin, + components=components, + model_symbols=model_symbols, + ) + result = _execute_dense_tensor( + expression, + library, + axis_labels=_spin_axis_labels( + source_particles[result_leg].spin, + result_leg + 1, + ), + ) + expected = _spin_dimension(source_particles[result_leg].spin) + if len(result) != expected: + raise ValueError( + f"contact tree final {term.vertex}/{term.id} produced {len(result)} " + f"components, expected {expected}" + ) + return tuple( + _replace_evaluator_constants(_as_expression(result[index])) + for index in range(len(result)) + ) + + +def _execute_contact_tensor_staged( + expression: _sym.Expression, + library: _sym.TensorLibrary, + source_particles: Sequence[CompiledParticleRecord], + *, + payload_by_leg: Mapping[ + int, + tuple[tuple[_sym.Expression, ...], tuple[_sym.Expression, ...]], + ], + result_leg: int, + kind: int, + model_symbols: ModelSymbolRegistry, +) -> tuple[_sym.Expression, ...]: + """Contract high-rank inputs in bounded, model-independent stages.""" + + dimensions = tuple(_spin_dimension(particle.spin) for particle in source_particles) + input_legs = tuple(sorted(payload_by_leg, key=lambda leg: (-dimensions[leg], leg))) + if set(input_legs) != set(range(len(source_particles))) - {result_leg}: + raise ValueError("staged contact payload does not cover every input leg") + for leg in input_legs: + if len(payload_by_leg[leg][0]) != dimensions[leg]: + raise ValueError(f"staged contact input leg {leg} has wrong dimension") + remaining_legs = list(range(len(source_particles))) + pending_legs = list(input_legs) + step = 0 + while pending_legs: + stage_legs: list[int] = [] + stage_work = 1 + while pending_legs: + next_leg = pending_legs[0] + next_work = stage_work * dimensions[next_leg] + if stage_legs and next_work > _MAX_STAGED_CONTACT_INPUT_WORK: + break + stage_legs.append(pending_legs.pop(0)) + stage_work = next_work + for leg in stage_legs: + expression *= _contact_tree_physical_tensor_expression( + library, + kind=kind, + leg=leg, + spin=source_particles[leg].spin, + components=payload_by_leg[leg][0], + model_symbols=model_symbols, + ) + remaining_legs.remove(leg) + axis_labels = tuple( + label + for remaining_leg in remaining_legs + for label in _spin_axis_labels( + source_particles[remaining_leg].spin, + remaining_leg + 1, + ) + ) + dense = _execute_dense_tensor( + expression, + library, + axis_labels=axis_labels, + ) + expected = math.prod(dimensions[index] for index in remaining_legs) + if len(dense) != expected: + raise ValueError( + f"staged contact contraction {step} produced {len(dense)} " + f"components, expected {expected}" + ) + if not pending_legs: + return tuple(_as_expression(component) for component in dense) + representations = tuple( + representation + for remaining_leg in remaining_legs + for representation in _spin_representations( + source_particles[remaining_leg].spin + ) + ) + if not representations: + if len(dense) != 1: + raise ValueError("scalar staged contact must have one component") + expression = _as_expression(dense[0]) + library = _sym.TensorLibrary.hep_lib_atom() + continue + slots = tuple( + slot + for remaining_leg in remaining_legs + for slot in _spin_slots( + source_particles[remaining_leg].spin, + remaining_leg + 1, + ) + ) + library = _sym.TensorLibrary.hep_lib_atom() + name = _sym.TensorName( + model_symbols.kernel_tensor_name(kind, f"contact_stage_{step}") + ) + library.register( + _sym.Tensor.dense( + name(*representations), + tuple(_as_expression(component) for component in dense), + ) + ) + expression = name(*slots).to_expression() + step += 1 + raise ValueError("staged contact contraction has no input legs") + + +def eager_color_singlet_vertex_term_components( + term: CompiledVertexTerm, + particles: Sequence[CompiledParticleRecord], + *, + result_leg: int, + input_components: Mapping[int, Sequence[_sym.Expression]], + input_momenta: Mapping[int, Sequence[_sym.Expression]], + coupling: _sym.Expression | None = None, + model_symbols: ModelSymbolRegistry | None = None, +) -> tuple[_sym.Expression, ...]: + """Contract one original color-singlet n-ary term without its lowered tree.""" + + _sym._ensure_symbolica() + + if not _contact_term_is_color_singlet(term): + raise ValueError("the eager n-ary oracle currently requires a color singlet") + if not 0 <= result_leg < term.valence: + raise ValueError(f"result leg {result_leg} is outside valence {term.valence}") + particle_by_name = {particle.name: particle for particle in particles} + source_particles = tuple(particle_by_name[name] for name in term.particles) + tensor_symbols = model_symbols or symbols.model("eager-contact-oracle") + input_legs = set(range(term.valence)) - {result_leg} + if set(input_components) != input_legs or set(input_momenta) != input_legs: + raise ValueError( + "eager n-ary inputs must provide every non-result leg exactly once" + ) + + library = _sym.TensorLibrary.hep_lib_atom() + expression = _sym.E(term.lorentz_expression) + momentum_by_leg: dict[int, tuple[_sym.Expression, ...]] = {} + for leg in sorted(input_legs): + components = tuple(input_components[leg]) + expected_dimension = _spin_dimension(source_particles[leg].spin) + if len(components) != expected_dimension: + raise ValueError( + f"input leg {leg} has {len(components)} components, " + f"expected {expected_dimension}" + ) + momentum = tuple(input_momenta[leg]) + if len(momentum) != 4: + raise ValueError(f"input leg {leg} momentum must have four components") + momentum_by_leg[leg] = momentum + expression *= _contact_tree_physical_tensor_expression( + library, + kind=term.id, + leg=leg, + spin=source_particles[leg].spin, + components=components, + model_symbols=tensor_symbols, + ) + + momentum_by_leg[result_leg] = tuple( + -sum( + (momentum[component] for momentum in momentum_by_leg.values()), + _sym.E("0"), + ) + for component in range(4) + ) + minkowski = _sym.Representation.mink(4) + for leg, momentum in momentum_by_leg.items(): + library.register( + _sym.Tensor.dense( + _sym.TensorName(tensor_symbols.ufo_momentum_tensor_name(leg + 1))( + minkowski + ), + momentum, + ) + ) + if coupling is not None: + expression *= coupling + result = _execute_dense_tensor( + expression, + library, + axis_labels=_spin_axis_labels( + source_particles[result_leg].spin, + result_leg + 1, + ), + ) + expected_dimension = _spin_dimension(source_particles[result_leg].spin) + if len(result) != expected_dimension: + raise ValueError( + f"eager n-ary term {term.vertex}/{term.id} produced {len(result)} " + f"components, expected {expected_dimension}" + ) + return tuple( + _replace_evaluator_constants(_as_expression(result[index])) + for index in range(len(result)) + ) + + +def _contact_tree_payload_by_leg( + kind: int, + side: str, + node: _ContactTreeNode, + source_particles: Sequence[CompiledParticleRecord], + *, + model_symbols: ModelSymbolRegistry, +) -> dict[int, tuple[tuple[_sym.Expression, ...], tuple[_sym.Expression, ...]]]: + payload = _contact_tree_node_payload( + kind, + side, + node, + source_particles, + scalar_product_tree=False, + model_symbols=model_symbols, + ) + result: dict[ + int, tuple[tuple[_sym.Expression, ...], tuple[_sym.Expression, ...]] + ] = {} + cursor = 0 + for leg in node.legs: + dimension = _spin_dimension(source_particles[leg].spin) + components = tuple(payload[cursor : cursor + dimension]) + cursor += dimension + momentum = tuple(payload[cursor : cursor + 4]) + cursor += 4 + result[leg] = components, momentum + if cursor != len(payload): + raise ValueError("contact tree payload layout mismatch") + return result + + +def _contact_tree_physical_tensor_expression( + library: _sym.TensorLibrary, + *, + kind: int, + leg: int, + spin: int, + components: Sequence[_sym.Expression], + model_symbols: ModelSymbolRegistry, +) -> _sym.Expression: + representations = _spin_representations(spin) + if not representations: + if len(components) != 1: + raise ValueError("scalar contact input must have one component") + return components[0] + name = _sym.TensorName(model_symbols.contact_leg_tensor_name(kind, leg)) + library.register(_sym.Tensor.dense(name(*representations), components)) + return name(*_spin_slots(spin, leg + 1)).to_expression() + + +def _contact_tree_assignment_multiplicity( + input_legs: Sequence[int], + source_particles: Sequence[CompiledParticleRecord], + left_root: _ContactTreeNode, + right_root: _ContactTreeNode, +) -> int: + species_counts: dict[str, int] = {} + for leg in input_legs: + name = source_particles[leg].name + species_counts[name] = species_counts.get(name, 0) + 1 + permutations = math.prod(math.factorial(count) for count in species_counts.values()) + symmetry_nodes = ( + _contact_tree_same_input_node_count(left_root) + + _contact_tree_same_input_node_count(right_root) + + int(left_root.particle.name == right_root.particle.name) + ) + symmetry_divisor = 2**symmetry_nodes + if permutations % symmetry_divisor: + raise ValueError("contact tree assignment symmetry is not integral") + return permutations // symmetry_divisor + + +def _contact_tree_same_input_node_count(node: _ContactTreeNode) -> int: + if node.is_leaf: + return 0 + if node.left is None or node.right is None: + raise ValueError("contact tree internal node is missing a child") + return ( + int(node.left.particle.name == node.right.particle.name) + + _contact_tree_same_input_node_count(node.left) + + _contact_tree_same_input_node_count(node.right) + ) + + +def _contact_tree_source_leg(node: _ContactTreeNode) -> int: + return -1 if node.physical_leg is None else node.physical_leg + + +def _contact_term_is_color_singlet(term: CompiledVertexTerm) -> bool: + return term.color_source in {"1", "UFO::{}::1"} or term.color_expression == "1" + + +def _deduplicate_contact_partials( + auxiliary_particles: Sequence[CompiledParticleRecord], + kernels: Sequence[CompiledOrientedKernel], + terms: Sequence[CompiledVertexTerm], + *, + model_symbols: ModelSymbolRegistry, +) -> tuple[tuple[CompiledParticleRecord, ...], tuple[CompiledOrientedKernel, ...]]: + term_by_id = {term.id: term for term in terms} + auxiliary_by_name = {particle.name: particle for particle in auxiliary_particles} + representative_by_signature: dict[tuple[object, ...], tuple[str, int]] = {} + replacement: dict[str, str] = {} + retained: list[CompiledOrientedKernel] = [] + + for kernel in kernels: + if not kernel.vertex.endswith("::contact-partial"): + retained.append(kernel) + continue + auxiliary = auxiliary_by_name[kernel.particles[2]] + term = term_by_id[kernel.term_id] + substitutions = { + symbols.derived_coupling( + model_symbols.model_name, + int(name.rsplit("_", 1)[1]), + ): _sym.E(term.coupling_expression) + for name in kernel.runtime_parameters + if name.startswith("derived_coupling_") + } + normalized_components = tuple( + _replace_expression_symbols( + _remap_kernel_symbols( + _sym.E(component), + old_kind=kernel.kind, + new_kind=0, + model_symbols=model_symbols, + ), + substitutions, + ).to_canonical_string() + for component in kernel.component_expressions + ) + signature = ( + kernel.particles[:2], + compiled_particle_component_dimension(auxiliary), + auxiliary.color, + kernel.coupling_orders, + kernel.color_source, + kernel.lc_color_normalization_power, + normalized_components, + auxiliary.auxiliary_kind if kernel.contact_orbit_steps else None, + ) + representative = representative_by_signature.get(signature) + if representative is None: + representative_by_signature[signature] = ( + auxiliary.name, + len(retained), + ) + retained.append(kernel) + else: + representative_name, representative_index = representative + replacement[auxiliary.name] = representative_name + retained[representative_index] = replace( + retained[representative_index], + contact_orbit_steps=canonical_contact_orbit_steps( + retained[representative_index].contact_orbit_steps, + kernel.contact_orbit_steps, + ), + ) + + if not replacement: + return tuple(auxiliary_particles), tuple(kernels) + + rewritten = tuple( + replace( + kernel, + particles=tuple(replacement.get(name, name) for name in kernel.particles), + ) + for kernel in retained + ) + particles = tuple( + particle for particle in auxiliary_particles if particle.name not in replacement + ) + return particles, rewritten diff --git a/pyamplicol/src/pyamplicol/models/compiler_contacts.py b/pyamplicol/src/pyamplicol/models/compiler_contacts.py new file mode 100644 index 0000000000..f57063400f --- /dev/null +++ b/pyamplicol/src/pyamplicol/models/compiler_contacts.py @@ -0,0 +1,1009 @@ +# SPDX-License-Identifier: 0BSD +"""Four-point contact component compression and fusion.""" + +from __future__ import annotations + +import math +import re +from collections.abc import Mapping, Sequence +from dataclasses import replace + +from .._internal.physics.symbols import ModelSymbolRegistry, symbols +from . import compiler_symbolica as _sym +from .compiler_kernels import ( + _as_expression, + _canonicalize_oriented_kernel_component, + _component_symbols, + _function_arguments, + _input_tensor_expression, + _ordered_dense_tensor_components, + _permutation_sign, + _remap_kernel_symbols, + _replace_expression_symbols, + _spin_axis_labels, + _spin_dimension, + _spin_representations, + _spin_slots, +) +from .compiler_records import _replace_evaluator_constants +from .contact_decomposition import ( + CONTACT_DECOMPOSITION_ALGORITHM, + CONTACT_DECOMPOSITION_ALGORITHM_VERSION, + CONTACT_ORBIT_ALGORITHM, + CONTACT_ORBIT_ALGORITHM_VERSION, + CONTACT_ORBIT_EVALUATOR_CLASS, + CompiledContactDecompositionProof, + CompiledContactDecompositionSplit, + CompiledContactDummyIndexMapping, + CompiledContactOrbitCertificate, + CompiledContactOrientationProof, + CompiledContactUnsupportedReason, + canonical_contact_orbit_steps, + contact_orbit_evaluator_class_is_certifiable, +) +from .contracts import ( + CompiledOrientedKernel, + CompiledParticleRecord, + CompiledVertexTerm, + compiled_particle_component_dimension, +) + + +def _fuse_contact_finals( + kernels: Sequence[CompiledOrientedKernel], + terms: Sequence[CompiledVertexTerm], + *, + model_symbols: ModelSymbolRegistry, +) -> tuple[CompiledOrientedKernel, ...]: + coupling_by_term = {term.id: term.coupling_expression for term in terms} + groups: dict[tuple[object, ...], list[CompiledOrientedKernel]] = {} + passthrough: list[CompiledOrientedKernel] = [] + for kernel in kernels: + if not kernel.vertex.endswith("::contact-final"): + passthrough.append(kernel) + continue + key = ( + kernel.particles, + kernel.coupling_orders, + kernel.color_source, + kernel.lc_color_normalization_power, + ) + groups.setdefault(key, []).append(kernel) + + fused: list[CompiledOrientedKernel] = [] + for members in groups.values(): + first = members[0] + runtime_aliases: dict[_sym.Expression, _sym.Expression] = {} + canonical_runtime_name: str | None = None + if all( + coupling_by_term.get(member.term_id) == coupling_by_term.get(first.term_id) + for member in members + ): + canonical_runtime_name = f"derived_coupling_{first.term_id}" + canonical_runtime = symbols.derived_coupling( + model_symbols.model_name, + first.term_id, + ) + runtime_aliases = { + symbols.derived_coupling( + model_symbols.model_name, + member.term_id, + ): canonical_runtime + for member in members + } + components = tuple( + _canonicalize_oriented_kernel_component( + sum( + ( + _replace_expression_symbols( + _remap_kernel_symbols( + _sym.E(member.component_expressions[index]), + old_kind=member.kind, + new_kind=first.kind, + model_symbols=model_symbols, + ), + runtime_aliases, + ) + for member in members + ), + _sym.E("0"), + ) + ).to_canonical_string() + for index in range(len(first.component_expressions)) + ) + fused.append( + replace( + first, + vertex=first.vertex.replace( + "::contact-final", + "::contact-final-fused", + ), + component_expressions=components, + runtime_parameters=( + (canonical_runtime_name,) + if canonical_runtime_name is not None + else tuple( + sorted( + { + name + for member in members + for name in member.runtime_parameters + } + ) + ) + ), + term_ids=tuple( + term_id + for member in members + for term_id in (member.term_ids or (member.term_id,)) + ), + contact_orbit_steps=canonical_contact_orbit_steps( + *(member.contact_orbit_steps for member in members) + ), + ) + ) + return tuple(sorted((*passthrough, *fused), key=lambda kernel: kernel.kind)) + + +def _contact_partial_component_expressions( + term: CompiledVertexTerm, + particle_by_name: Mapping[str, CompiledParticleRecord], + *, + left_leg: int, + right_leg: int, + open_legs: tuple[int, ...], + kind: int, + model_symbols: ModelSymbolRegistry, +) -> tuple[str, ...]: + library = _sym.TensorLibrary.hep_lib_atom() + expression = _sym.E(term.lorentz_expression) + particles = tuple(particle_by_name[name] for name in term.particles) + expression *= _input_tensor_expression( + library, + kind=kind, + side="left", + spin=particles[left_leg].spin, + leg=left_leg + 1, + components=_component_symbols( + kind, + "left", + particles[left_leg].spin, + model_symbols=model_symbols, + ), + model_symbols=model_symbols, + ) + expression *= _input_tensor_expression( + library, + kind=kind, + side="right", + spin=particles[right_leg].spin, + leg=right_leg + 1, + components=_component_symbols( + kind, + "right", + particles[right_leg].spin, + model_symbols=model_symbols, + ), + model_symbols=model_symbols, + ) + result = _execute_dense_tensor( + expression, + library, + axis_labels=tuple( + label + for leg in open_legs + for label in _spin_axis_labels(particles[leg].spin, leg + 1) + ), + ) + expected = math.prod(_spin_dimension(particles[leg].spin) for leg in open_legs) + if len(result) != expected: + raise ValueError( + f"contact partial {term.vertex}/{term.id} produced {len(result)} " + f"components, expected {expected}" + ) + return tuple( + _replace_evaluator_constants( + _as_expression(result[index]) + ).to_canonical_string() + for index in range(len(result)) + ) + + +def _contact_final_component_expressions( + particles: Sequence[CompiledParticleRecord], + auxiliary: CompiledParticleRecord, + *, + open_legs: tuple[int, ...], + remaining_leg: int, + result_leg: int, + kind: int, + auxiliary_on_left: bool, + component_expansion: tuple[tuple[int, int] | None, ...], + model_symbols: ModelSymbolRegistry, +) -> tuple[str, ...]: + library = _sym.TensorLibrary.hep_lib_atom() + auxiliary_side = "left" if auxiliary_on_left else "right" + physical_side = "right" if auxiliary_on_left else "left" + auxiliary_symbols = tuple( + model_symbols.kernel_component(kind, auxiliary_side, component) + for component in range(compiled_particle_component_dimension(auxiliary)) + ) + expanded_auxiliary = tuple( + _sym.E("0") if entry is None else entry[1] * auxiliary_symbols[entry[0]] + for entry in component_expansion + ) + expression = _contact_auxiliary_tensor_expression( + library, + kind=kind, + side=auxiliary_side, + particles=particles, + open_legs=open_legs, + components=expanded_auxiliary, + model_symbols=model_symbols, + ) + expression *= _input_tensor_expression( + library, + kind=kind, + side=physical_side, + spin=particles[remaining_leg].spin, + leg=remaining_leg + 1, + components=_component_symbols( + kind, + physical_side, + particles[remaining_leg].spin, + model_symbols=model_symbols, + ), + model_symbols=model_symbols, + ) + result = _execute_dense_tensor( + expression, + library, + axis_labels=_spin_axis_labels(particles[result_leg].spin, result_leg + 1), + ) + expected = _spin_dimension(particles[result_leg].spin) + if len(result) != expected: + raise ValueError( + f"contact final for source leg {result_leg} produced {len(result)} " + f"components, expected {expected}" + ) + return tuple( + _replace_evaluator_constants( + _as_expression(result[index]) + ).to_canonical_string() + for index in range(len(result)) + ) + + +def _contact_auxiliary_tensor_expression( + library: _sym.TensorLibrary, + *, + kind: int, + side: str, + particles: Sequence[CompiledParticleRecord], + open_legs: tuple[int, ...], + components: Sequence[_sym.Expression], + model_symbols: ModelSymbolRegistry, +) -> _sym.Expression: + representations = tuple( + representation + for leg in open_legs + for representation in _spin_representations(particles[leg].spin) + ) + if not representations: + if len(components) != 1: + raise ValueError("scalar contact auxiliary must have one component") + return components[0] + slots = tuple( + slot for leg in open_legs for slot in _spin_slots(particles[leg].spin, leg + 1) + ) + name = _sym.TensorName(model_symbols.kernel_tensor_name(kind, side)) + library.register(_sym.Tensor.dense(name(*representations), components)) + return name(*slots).to_expression() + + +def _execute_dense_tensor( + expression: _sym.Expression, + library: _sym.TensorLibrary, + *, + axis_labels: Sequence[str], +) -> tuple[object, ...]: + if not axis_labels and set(expression.get_all_symbols()) == set( + expression.get_all_symbols(include_function_symbols=False) + ): + # A rank-zero expression with no function indeterminates contains no + # tensor heads to contract. Spenso need not construct a tensor network. + return (expression,) + network = _sym.TensorNetwork( + _sym.as_tensor(expression) if axis_labels else expression, library + ) + network.execute(library=library) + result = network.result_tensor(library) + return _ordered_dense_tensor_components(result, axis_labels).values + + +def _contact_auxiliary_color( + term: CompiledVertexTerm, + particles: Sequence[CompiledParticleRecord], + *, + remaining_leg: int, + result_leg: int, +) -> int: + colors = tuple(particle.color for particle in particles) + if all(color == 1 for color in colors): + return 1 + if "f(" in term.color_source or "::f(" in term.color_source: + return 8 + remaining = abs(colors[remaining_leg]) + result = abs(colors[result_leg]) + if remaining == 1: + return int(colors[result_leg]) + if result == 1: + return int(colors[remaining_leg]) + if remaining == result == 8: + return 1 + return 1 + + +def _compress_contact_components( + components: Sequence[str], +) -> tuple[tuple[int, ...], tuple[tuple[int, int] | None, ...]]: + representatives: list[_sym.Expression] = [] + representative_indices: list[int] = [] + expansion: list[tuple[int, int] | None] = [] + zero = _sym.E("0") + for index, source in enumerate(components): + expression = _sym.E(source) + if str(expression) == "0": + expansion.append(None) + continue + match: tuple[int, int] | None = None + for basis_index, representative in enumerate(representatives): + if str((expression - representative).expand()) == "0": + match = (basis_index, 1) + break + if str((expression + representative).expand()) == "0": + match = (basis_index, -1) + break + if match is None: + match = (len(representatives), 1) + representatives.append(expression) + representative_indices.append(index) + expansion.append(match) + if not representatives: + representatives.append(zero) + representative_indices.append(0) + expansion[0] = (0, 1) + return tuple(representative_indices), tuple(expansion) + + +def _record_contact_decomposition_proofs( + terms: Sequence[CompiledVertexTerm], + particles: Sequence[CompiledParticleRecord], + *, + model_symbols: ModelSymbolRegistry, +) -> tuple[CompiledVertexTerm, ...]: + """Attach explicit proof records before four-point contact lowering.""" + + particle_by_name = {particle.name: particle for particle in particles} + result: list[CompiledVertexTerm] = [] + for term in terms: + if term.valence != 4 or "ufo_momentum_" in term.lorentz_expression: + result.append(term) + continue + source_particles = tuple(particle_by_name[name] for name in term.particles) + proof = _build_contact_decomposition_proof( + term, + source_particles, + particle_by_name, + model_symbols=model_symbols, + ) + result.append( + replace( + term, + contact_decomposition_proof=proof, + contact_orbit_certificate=_build_contact_orbit_certificate( + term, + source_particles, + proof, + ), + ) + ) + return tuple(result) + + +def _build_contact_orbit_certificate( + term: CompiledVertexTerm, + source_particles: Sequence[CompiledParticleRecord], + proof: CompiledContactDecompositionProof, +) -> CompiledContactOrbitCertificate | None: + """Issue the deliberately narrow compiler-owned contact-orbit proof.""" + + if ( + proof.status != "proven" + or not proof.matches(term) + or tuple(particle.name for particle in source_particles) != term.particles + or not contact_orbit_evaluator_class_is_certifiable( + color_source=term.color_source, + color_expression=term.color_expression, + lorentz_expression=term.lorentz_expression, + decomposition_kinds=tuple( + split.decomposition_kind for split in proof.splits + ), + particle_contracts=tuple( + ( + particle.spin, + particle.color, + particle.statistics, + particle.wavefunction_family, + particle.self_conjugate, + ) + for particle in source_particles + ), + ) + ): + return None + equivalence_classes: dict[str, int] = {} + leg_classes = tuple( + equivalence_classes.setdefault(particle.name, len(equivalence_classes)) + for particle in source_particles + ) + if len(leg_classes) != 4: + raise ValueError("contact-orbit certificate requires exactly four particles") + return CompiledContactOrbitCertificate( + algorithm=CONTACT_ORBIT_ALGORITHM, + algorithm_version=CONTACT_ORBIT_ALGORITHM_VERSION, + term_id=term.id, + vertex=term.vertex, + particles=( + source_particles[0].name, + source_particles[1].name, + source_particles[2].name, + source_particles[3].name, + ), + color_expression=term.color_expression, + lorentz_expression=term.lorentz_expression, + coupling_expression=term.coupling_expression, + evaluator_class=CONTACT_ORBIT_EVALUATOR_CLASS, + physical_leg_equivalence_classes=( + leg_classes[0], + leg_classes[1], + leg_classes[2], + leg_classes[3], + ), + reconstruction_factor="1", + ) + + +def _build_contact_decomposition_proof( + term: CompiledVertexTerm, + source_particles: Sequence[CompiledParticleRecord], + particle_by_name: Mapping[str, CompiledParticleRecord], + *, + model_symbols: ModelSymbolRegistry, +) -> CompiledContactDecompositionProof: + result_legs: list[int] = [] + result_particle_names: set[str] = set() + for result_leg, source_result in enumerate(source_particles): + if source_result.name in result_particle_names: + continue + result_particle_names.add(source_result.name) + result_legs.append(result_leg) + + literal_singlet = _contact_term_has_literal_color_singlet(term) + if literal_singlet and any(particle.color != 1 for particle in source_particles): + return _contact_proof( + term, + status="unsupported", + unsupported_reasons=( + _contact_unsupported_reason( + "literal-singlet-with-colored-legs", + "a literal color singlet cannot contract non-singlet external legs", + color_representations=",".join( + str(particle.color) for particle in source_particles + ), + ), + ), + ) + + splits: list[CompiledContactDecompositionSplit] = [] + unsupported: list[CompiledContactUnsupportedReason] = [] + for result_leg in result_legs: + color_split = _four_point_contact_color_split( + term, + result_leg, + model_symbols=model_symbols, + ) + if color_split is None: + unsupported.append( + _unsupported_contact_color_split_reason( + term, + result_leg, + model_symbols=model_symbols, + ) + ) + continue + splits.append( + _contact_decomposition_split_proof( + term, + source_particles, + particle_by_name, + result_leg=result_leg, + color_split=color_split, + model_symbols=model_symbols, + ) + ) + if unsupported: + return _contact_proof( + term, + status="unsupported", + unsupported_reasons=tuple(unsupported), + ) + return _contact_proof(term, status="proven", splits=tuple(splits)) + + +def _contact_decomposition_split_proof( + term: CompiledVertexTerm, + source_particles: Sequence[CompiledParticleRecord], + particle_by_name: Mapping[str, CompiledParticleRecord], + *, + result_leg: int, + color_split: tuple[ + tuple[int, int], + int, + str, + str, + int, + int, + tuple[int, ...], + tuple[int, ...], + int, + str, + ], + model_symbols: ModelSymbolRegistry, +) -> CompiledContactDecompositionSplit: + ( + pair_legs, + remaining_leg, + outer_color_source, + final_color_source, + outer_color_power, + final_color_power, + outer_color_factor, + final_color_factor, + color_dummy, + color_coefficient, + ) = color_split + open_legs = ( + min(remaining_leg, result_leg), + max(remaining_leg, result_leg), + ) + canonical_components = _contact_partial_component_expressions( + term, + particle_by_name, + left_leg=pair_legs[0], + right_leg=pair_legs[1], + open_legs=open_legs, + kind=term.id, + model_symbols=model_symbols, + ) + representative_indices, component_expansion = _compress_contact_components( + canonical_components + ) + assignment_multiplicity = sum( + source_particles[leg].name == source_particles[remaining_leg].name + for leg in range(4) + if leg != result_leg + ) + pair_orders = ( + (pair_legs,) + if source_particles[pair_legs[0]].name == source_particles[pair_legs[1]].name + else (pair_legs, tuple(reversed(pair_legs))) + ) + canonical_outer_parity = ( + _permutation_sign( + outer_color_factor, + (color_dummy, pair_legs[0] + 1, pair_legs[1] + 1), + ) + if outer_color_factor + else 1 + ) + orientations: list[CompiledContactOrientationProof] = [] + for left_leg, right_leg in pair_orders: + outer_parity = ( + _permutation_sign( + outer_color_factor, + (color_dummy, left_leg + 1, right_leg + 1), + ) + if outer_color_factor + else 1 + ) + orientations.append( + CompiledContactOrientationProof( + stage="partial", + input_legs=(left_leg, right_leg), + permutation_parity=outer_parity, + scalar_prefactor=_canonical_scalar_prefactor( + _sym.E(str(outer_parity)) / canonical_outer_parity + ), + ) + ) + + for auxiliary_on_left in (True, False): + final_input_tokens = ( + (color_dummy, remaining_leg + 1) + if auxiliary_on_left + else (remaining_leg + 1, color_dummy) + ) + final_parity = ( + _permutation_sign( + final_color_factor, + (result_leg + 1, *final_input_tokens), + ) + if final_color_factor + else 1 + ) + orientations.append( + CompiledContactOrientationProof( + stage="final", + input_legs=( + (-1, remaining_leg) if auxiliary_on_left else (remaining_leg, -1) + ), + permutation_parity=final_parity, + scalar_prefactor=_canonical_scalar_prefactor( + _sym.E(color_coefficient) + * canonical_outer_parity + * final_parity + / assignment_multiplicity + ), + ) + ) + + dummy_mapping = ( + CompiledContactDummyIndexMapping( + source_index=color_dummy, + normalized_symbol=f"ufo_c_dummy_{abs(color_dummy)}_adjoint", + outer_slot=outer_color_factor.index(color_dummy), + final_slot=final_color_factor.index(color_dummy), + ) + if outer_color_factor + else None + ) + return CompiledContactDecompositionSplit( + decomposition_kind=( + "two-structure-constants" if outer_color_factor else "literal-color-singlet" + ), + result_leg=result_leg, + pair_legs=pair_legs, + remaining_leg=remaining_leg, + outer_color_source=outer_color_source, + final_color_source=final_color_source, + outer_color_factor=outer_color_factor, + final_color_factor=final_color_factor, + dummy_index_mapping=dummy_mapping, + outer_color_normalization_power=outer_color_power, + final_color_normalization_power=final_color_power, + color_coefficient=color_coefficient, + auxiliary_color=_contact_auxiliary_color( + term, + source_particles, + remaining_leg=remaining_leg, + result_leg=result_leg, + ), + open_legs=open_legs, + component_axis_order=tuple( + label + for leg in open_legs + for label in _spin_axis_labels(source_particles[leg].spin, leg + 1) + ), + component_basis_order=representative_indices, + component_expansion=component_expansion, + assignment_multiplicity=assignment_multiplicity, + canonical_outer_parity=canonical_outer_parity, + orientations=tuple(orientations), + ) + + +def _contact_proof( + term: CompiledVertexTerm, + *, + status: str, + splits: tuple[CompiledContactDecompositionSplit, ...] = (), + unsupported_reasons: tuple[CompiledContactUnsupportedReason, ...] = (), +) -> CompiledContactDecompositionProof: + return CompiledContactDecompositionProof( + status=status, + algorithm=CONTACT_DECOMPOSITION_ALGORITHM, + algorithm_version=CONTACT_DECOMPOSITION_ALGORITHM_VERSION, + term_id=term.id, + vertex=term.vertex, + particles=term.particles, + color_index=term.color_index, + lorentz_index=term.lorentz_index, + original_color_source=term.color_source, + normalized_color_expression=term.color_expression, + lorentz_name=term.lorentz_name, + original_lorentz_source=term.lorentz_source, + normalized_lorentz_expression=term.lorentz_expression, + splits=splits, + unsupported_reasons=unsupported_reasons, + ) + + +def _contact_unsupported_reason( + code: str, + message: str, + **context: str, +) -> CompiledContactUnsupportedReason: + return CompiledContactUnsupportedReason( + code=code, + message=message, + context=tuple(sorted(context.items())), + ) + + +def _unsupported_contact_color_split_reason( + term: CompiledVertexTerm, + result_leg: int, + *, + model_symbols: ModelSymbolRegistry, +) -> CompiledContactUnsupportedReason: + common = { + "normalized_color_expression": term.color_expression, + "result_leg": str(result_leg), + } + factor_arguments = _function_arguments(term.color_expression, "::f") + if len(factor_arguments) != 2: + return _contact_unsupported_reason( + "unsupported-color-factor-count", + "contact color expression is not exactly two structure constants", + factor_count=str(len(factor_arguments)), + **common, + ) + factors = _normalized_structure_constant_factors(term.color_expression) + if len(factors) != 2: + return _contact_unsupported_reason( + "malformed-structure-constant-indices", + "contact structure-constant indices are not normalized adjoint indices", + **common, + ) + if ( + _source_structure_constant_product_coefficient( + term.color_source, + model_symbols=model_symbols, + ) + is None + ): + return _contact_unsupported_reason( + "non-scalar-color-prefactor", + "contact structure constants retain a non-scalar color factor", + **common, + ) + shared_dummies = set(value for value in factors[0] if value < 0) & set( + value for value in factors[1] if value < 0 + ) + if len(shared_dummies) != 1: + return _contact_unsupported_reason( + "non-unique-shared-dummy", + "contact structure constants must share exactly one adjoint dummy", + shared_dummy_count=str(len(shared_dummies)), + **common, + ) + result_index = result_leg + 1 + if not any(result_index in factor for factor in factors): + return _contact_unsupported_reason( + "result-leg-absent-from-color-factors", + "contact result leg is absent from both structure constants", + **common, + ) + return _contact_unsupported_reason( + "unsupported-structure-constant-topology", + "contact structure constants do not form a trivalent auxiliary split", + **common, + ) + + +def _canonical_scalar_prefactor(expression: _sym.Expression) -> str: + return str(expression.to_canonical_string()) + + +def _contact_term_has_literal_color_singlet(term: CompiledVertexTerm) -> bool: + return term.color_source in {"1", "UFO::{}::1"} or term.color_expression == "1" + + +def _validated_contact_decomposition_proof( + term: CompiledVertexTerm, +) -> CompiledContactDecompositionProof | None: + proof = term.contact_decomposition_proof + if proof is None: + return None + if not proof.matches(term): + raise ValueError( + f"contact decomposition proof identity mismatch for term {term.id}" + ) + if ( + proof.algorithm != CONTACT_DECOMPOSITION_ALGORITHM + or proof.algorithm_version != CONTACT_DECOMPOSITION_ALGORITHM_VERSION + ): + raise ValueError( + f"unsupported contact decomposition proof algorithm for term {term.id}: " + f"{proof.algorithm}/v{proof.algorithm_version}" + ) + return proof if proof.status == "proven" else None + + +def _four_point_contact_color_split( + term: CompiledVertexTerm, + result_leg: int, + *, + model_symbols: ModelSymbolRegistry | None = None, +) -> ( + tuple[ + tuple[int, int], + int, + str, + str, + int, + int, + tuple[int, ...], + tuple[int, ...], + int, + str, + ] + | None +): + active_symbols = model_symbols or symbols.model("contact_color_parser") + factors = _normalized_structure_constant_factors(term.color_expression) + color_coefficient = _source_structure_constant_product_coefficient( + term.color_source, + model_symbols=active_symbols, + ) + if len(factors) == 2 and color_coefficient is not None: + factors = ( + _source_structure_constant_factor_orientations( + _function_arguments(term.color_source, "f"), factors + ) + or () + ) + if len(factors) == 2 and color_coefficient is not None: + shared_dummies = set(value for value in factors[0] if value < 0) & set( + value for value in factors[1] if value < 0 + ) + if len(shared_dummies) == 1: + dummy = next(iter(shared_dummies)) + result_index = result_leg + 1 + final_factor = next( + (factor for factor in factors if result_index in factor), + None, + ) + if final_factor is not None: + outer_factor = factors[1] if final_factor is factors[0] else factors[0] + pair = tuple(value - 1 for value in outer_factor if value > 0) + remaining = tuple( + value - 1 + for value in final_factor + if value > 0 and value != result_index + ) + if len(pair) == 2 and len(remaining) == 1: + canonical_f = "UFO::{}::f(1,2,3)" + return ( + (pair[0], pair[1]), + remaining[0], + canonical_f, + canonical_f, + 1, + 1, + outer_factor, + final_factor, + dummy, + color_coefficient, + ) + + if term.color_source in {"1", "UFO::{}::1"} or term.color_expression == "1": + input_legs = tuple(leg for leg in range(4) if leg != result_leg) + return ( + (input_legs[0], input_legs[1]), + input_legs[2], + term.color_source, + "1", + term.lc_color_normalization_power, + 0, + (), + (), + -1, + "1", + ) + return None + + +def _source_structure_constant_product_coefficient( + expression: str, + *, + model_symbols: ModelSymbolRegistry, +) -> str | None: + """Return the explicit scalar multiplying two source UFO f tensors. + + The normalized Spenso expression may acquire a sign while canonicalizing + antisymmetric structure-constant arguments. The decomposition records + those permutations separately, so using the normalized scalar here would + apply that sign twice. + """ + + _sym._ensure_symbolica() + parsed = model_symbols.expression(expression) + wildcards = tuple( + _sym.E(f"ufo_contact_color_argument_{index}_") for index in range(3) + ) + coefficient = parsed.replace( + model_symbols.symbol("f")(*wildcards), + _sym.E("1"), + bottom_up=True, + repeat=True, + ) + if coefficient.get_all_symbols(): + return None + return str(coefficient.to_canonical_string()) + + +def _source_structure_constant_factor_orientations( + source_factors: Sequence[Sequence[str]], + normalized_factors: Sequence[tuple[int, ...]], +) -> tuple[tuple[int, int, int], ...] | None: + """Recover source permutations after normalized tensor authentication. + + The source scalar coefficient must accompany source f-tensor orientations, + independently of antisymmetric permutations chosen by the CAS printer. + """ + + try: + source = tuple( + tuple(int(argument.strip()) for argument in factor) + for factor in source_factors + ) + except ValueError: + return None + if any(len(factor) != 3 for factor in source): + return None + source_dummies = {value for factor in source for value in factor if value < 0} + normalized_dummies = { + value for factor in normalized_factors for value in factor if value < 0 + } + if len(source_dummies) != len(normalized_dummies): + return None + dummy_map = dict( + zip(sorted(source_dummies), sorted(normalized_dummies), strict=True) + ) + oriented = tuple( + tuple(dummy_map.get(value, value) for value in factor) for factor in source + ) + if sorted(sorted(factor) for factor in oriented) != sorted( + sorted(factor) for factor in normalized_factors + ): + return None + return oriented # type: ignore[return-value] + + +def _normalized_structure_constant_factors( + expression: str, + *, + expected_count: int = 2, +) -> tuple[tuple[int, ...], ...]: + """Return typed f-tensor index words from a normalized color monomial.""" + + if expression.count("::f(") != expected_count: + return () + result: list[tuple[int, ...]] = [] + for arguments in _function_arguments(expression, "::f"): + indices: list[int] = [] + for argument in arguments: + dummy = re.search(r"ufo_c_dummy_([0-9]+)_adjoint", argument) + if dummy is not None: + indices.append(-int(dummy.group(1))) + continue + external = re.search(r"ufo_c_([0-9]+)", argument) + if external is None: + return () + indices.append(int(external.group(1))) + if len(indices) != 3: + return () + result.append(tuple(indices)) + return tuple(result) diff --git a/pyamplicol/src/pyamplicol/models/compiler_contractions.py b/pyamplicol/src/pyamplicol/models/compiler_contractions.py new file mode 100644 index 0000000000..d8c7581339 --- /dev/null +++ b/pyamplicol/src/pyamplicol/models/compiler_contractions.py @@ -0,0 +1,247 @@ +# SPDX-License-Identifier: 0BSD +"""Compile model-owned current-contraction records for external models.""" + +from __future__ import annotations + +from collections.abc import Sequence + +from ._physics_ir import ContractionIR +from .contracts import ( + CompiledClosureContractionRecord, + CompiledDirectContractionRecord, + CompiledParameterRecord, + CompiledParticleRecord, + CompiledPropagatorRecord, + compiled_current_dimension, + compiled_particle_component_dimension, + compiled_particle_is_chiral_eligible, +) + + +def compile_contraction_records( + particles: Sequence[CompiledParticleRecord], + parameters: Sequence[CompiledParameterRecord], + propagators: Sequence[CompiledPropagatorRecord], +) -> tuple[ + tuple[CompiledDirectContractionRecord, ...], + tuple[CompiledClosureContractionRecord, ...], +]: + """Derive concrete contraction states from explicit compiled metadata. + + This is deliberately a finite compiler step. Runtime generation consumes + the resulting records and does not infer a contraction from component + dimensions. + """ + + particles_by_name = {particle.name: particle for particle in particles} + parameters_by_name = {parameter.name: parameter for parameter in parameters} + propagators_by_name = {propagator.name: propagator for propagator in propagators} + direct: list[CompiledDirectContractionRecord] = [] + closure: list[CompiledClosureContractionRecord] = [] + + for left in particles: + try: + right = particles_by_name[left.antiname] + except KeyError as exc: + raise ValueError( + f"particle {left.name!r} refers to absent antiparticle " + f"{left.antiname!r}" + ) from exc + + left_is_weyl = compiled_particle_is_chiral_eligible( + left, + parameters=parameters_by_name, + propagators=propagators_by_name, + ) + right_is_weyl = compiled_particle_is_chiral_eligible( + right, + parameters=parameters_by_name, + propagators=propagators_by_name, + ) + if left_is_weyl != right_is_weyl: + states: tuple[tuple[int, int], ...] = () + elif left_is_weyl: + states = ((-1, 1), (0, 0), (1, -1)) + else: + states = ((0, 0),) + for left_chirality, right_chirality in states: + contraction = _compile_direct_contraction( + left, + right, + left_chirality=left_chirality, + right_chirality=right_chirality, + parameters=parameters_by_name, + propagators=propagators_by_name, + ) + if contraction is not None: + direct.append( + CompiledDirectContractionRecord( + left_particle=left.name, + left_chirality=left_chirality, + right_particle=right.name, + right_chirality=right_chirality, + contraction_ir=contraction, + ) + ) + + scalar_projection = _compile_scalar_closure(left) + if scalar_projection is not None: + closure.append( + CompiledClosureContractionRecord( + particle=left.name, + chirality=0, + contraction_ir=scalar_projection, + ) + ) + + return tuple(direct), tuple(closure) + + +def _compile_direct_contraction( + left: CompiledParticleRecord, + right: CompiledParticleRecord, + *, + left_chirality: int, + right_chirality: int, + parameters: dict[str, CompiledParameterRecord], + propagators: dict[str, CompiledPropagatorRecord], +) -> ContractionIR | None: + try: + left_dimension = compiled_current_dimension( + left, + left_chirality, + parameters=parameters, + propagators=propagators, + ) + right_dimension = compiled_current_dimension( + right, + right_chirality, + parameters=parameters, + propagators=propagators, + ) + except ValueError: + return None + if left_dimension != right_dimension: + return None + + if left_chirality != 0 or right_chirality != 0: + if ( + left.statistics != "fermion" + or right.statistics != "fermion" + or left.wavefunction_family != "fermion" + or right.wavefunction_family != "fermion" + or left_dimension != 2 + or left_chirality != -right_chirality + ): + return None + return ContractionIR( + name="weyl", + left_basis="weyl-chiral", + right_basis="weyl-chiral", + coefficients=((1.0, 0.0),) * 2, + chirality_relation="opposite", + metric_signature=None, + ) + + if ( + left.statistics == right.statistics == "boson" + and left.wavefunction_family == right.wavefunction_family == "scalar" + and left_dimension == 1 + ): + return ContractionIR( + name="scalar", + left_basis="scalar", + right_basis="scalar", + coefficients=((1.0, 0.0),), + ) + if ( + left.statistics == right.statistics == "fermion" + and left.wavefunction_family == right.wavefunction_family == "fermion" + and left_dimension == 4 + ): + return ContractionIR( + name="dirac", + left_basis="dirac", + right_basis="dirac", + coefficients=((1.0, 0.0),) * 4, + ) + if ( + left.statistics == right.statistics == "boson" + and left.wavefunction_family == right.wavefunction_family == "vector" + and left_dimension == 4 + ): + return ContractionIR( + name="lorentz", + left_basis="lorentz-vector", + right_basis="lorentz-vector", + coefficients=( + (1.0, 0.0), + (-1.0, 0.0), + (-1.0, 0.0), + (-1.0, 0.0), + ), + metric_signature="mostly-minus", + ) + if ( + left.auxiliary_kind + == right.auxiliary_kind + == "u1-subtraction-color-flow-vector" + and left_dimension == 4 + ): + basis = "auxiliary:u1-subtraction-color-flow-vector" + return ContractionIR( + name="lorentz", + left_basis=basis, + right_basis=basis, + coefficients=( + (1.0, 0.0), + (-1.0, 0.0), + (-1.0, 0.0), + (-1.0, 0.0), + ), + metric_signature="mostly-minus", + ) + if ( + left.statistics == right.statistics == "boson" + and left.wavefunction_family == right.wavefunction_family == "spin2" + and left_dimension == 16 + ): + metric = (1.0, -1.0, -1.0, -1.0) + return ContractionIR( + name="lorentz-rank-2", + left_basis="lorentz-rank-2", + right_basis="lorentz-rank-2", + coefficients=tuple( + (metric[mu] * metric[nu], 0.0) + for mu in range(4) + for nu in range(4) + ), + metric_signature="mostly-minus-tensor-product", + ) + return None + + +def _compile_scalar_closure( + particle: CompiledParticleRecord, +) -> ContractionIR | None: + try: + dimension = compiled_particle_component_dimension(particle) + except ValueError: + return None + if ( + particle.statistics != "boson" + or particle.wavefunction_family != "scalar" + or dimension != 1 + ): + return None + return ContractionIR( + name="scalar", + left_basis="scalar", + right_basis="scalar", + coefficients=((1.0, 0.0),), + chirality_relation="any", + metric_signature=None, + ) + + +__all__ = ["compile_contraction_records"] diff --git a/pyamplicol/src/pyamplicol/models/compiler_entry.py b/pyamplicol/src/pyamplicol/models/compiler_entry.py new file mode 100644 index 0000000000..ac936ffe71 --- /dev/null +++ b/pyamplicol/src/pyamplicol/models/compiler_entry.py @@ -0,0 +1,1025 @@ +# SPDX-License-Identifier: 0BSD +"""Entry point for compiling external UFO model IR.""" + +from __future__ import annotations + +import hashlib +import json +from collections.abc import Mapping, Sequence +from dataclasses import replace + +from .._internal.physics.symbols import ModelSymbolRegistry, symbols +from . import compiler_symbolica as _sym +from .compiler_auxiliary_components import reduce_contact_auxiliary_components +from .compiler_color_flow import ( + compile_lc_color_transition_terms, + synthesize_fundamental_fierz_auxiliaries, +) +from .compiler_contact_trees import ( + _compile_color_singlet_contact_trees, + _compile_heft_colored_contact_trees, + _deduplicate_contact_partials, + _record_heft_contact_orbit_certificates, +) +from .compiler_contacts import ( + _build_contact_decomposition_proof, + _contact_final_component_expressions, + _contact_partial_component_expressions, + _contact_term_has_literal_color_singlet, + _fuse_contact_finals, + _record_contact_decomposition_proofs, + _validated_contact_decomposition_proof, +) +from .compiler_contractions import compile_contraction_records +from .compiler_gauge import compile_goldstone_partner_records +from .compiler_kernels import ( + _canonicalize_oriented_kernel_component, + _fuse_oriented_kernels, + _is_compile_time_zero_parameter, + _is_single_structure_constant, + _lc_color_normalization_power, + _oriented_component_expressions, + _remap_kernel_symbols, + _replace_expression_symbols, + _spin_axis_labels, +) +from .compiler_records import ( + _coupling, + _mappings, + _order, + _parameter, + _particle, + _propagator, + _resolve_coupling_records, + _resolve_parameter_records, + _sequence, +) +from .compiler_tensor_ordering import compile_tensor_ordering_metadata +from .contact_decomposition import ( + CompiledContactDecompositionSplit, + CompiledContactOrbitStep, + CompiledContactOrientationProof, + compiled_contact_orbit_step, +) +from .contracts import ( + CompiledModelIR, + CompiledOrientedKernel, + CompiledParameterRecord, + CompiledParticleRecord, + CompiledPropagatorRecord, + CompiledVertexTerm, + compiled_particle_component_dimension, +) +from .tensors import ( + classify_trilinear_color_expression, + normalize_color_expression, + normalize_lorentz_expression, + project_trilinear_color_expression, +) + +_SUPPORTED_TRILINEAR_COLOR_PROJECTIONS = frozenset( + { + "singlet", + "color-identity", + "fundamental-generator", + "adjoint-structure-constant", + } +) + + +def compile_ufo_model_ir(model: Mapping[str, object]) -> CompiledModelIR: + _sym._ensure_symbolica() + model_name = str(model.get("name", "unnamed-model")) + model_symbols = symbols.model(model_name) + particles = tuple(_particle(item) for item in _mappings(model.get("particles"))) + particle_by_name = {particle.name: particle for particle in particles} + lorentz_by_name = { + str(item["name"]): item for item in _mappings(model.get("lorentz_structures")) + } + parameter_records = tuple( + _parameter(item, model_symbols=model_symbols) + for item in _mappings(model.get("parameters")) + ) + parameter_records = _resolve_parameter_records(parameter_records, model_symbols) + coupling_records = tuple( + _coupling(item, model_symbols=model_symbols) + for item in _mappings(model.get("couplings")) + ) + coupling_records = _resolve_coupling_records( + coupling_records, + parameter_records, + model_symbols, + ) + coupling_by_name = {coupling.name: coupling for coupling in coupling_records} + terms: list[CompiledVertexTerm] = [] + for vertex in _mappings(model.get("vertex_rules")): + particle_names = tuple(str(value) for value in _sequence(vertex["particles"])) + try: + vertex_particles = tuple(particle_by_name[name] for name in particle_names) + except KeyError as exc: + raise ValueError( + f"vertex {vertex.get('name')} refers to unknown particle {exc.args[0]}" + ) from exc + colors = tuple(particle.color for particle in vertex_particles) + color_sources = tuple( + str(value) for value in _sequence(vertex["color_structures"]) + ) + lorentz_names = tuple( + str(value) for value in _sequence(vertex["lorentz_structures"]) + ) + coupling_matrix = _sequence(vertex["couplings"]) + if len(coupling_matrix) != len(color_sources): + raise ValueError( + f"vertex {vertex.get('name')} coupling rows do not match " + "color structures" + ) + normalized_colors = tuple( + normalize_color_expression(source, colors) for source in color_sources + ) + normalized_lorentz = [] + for name in lorentz_names: + try: + lorentz = lorentz_by_name[name] + except KeyError as exc: + raise ValueError( + f"vertex {vertex.get('name')} refers to unknown Lorentz " + f"structure {name}" + ) from exc + normalized_lorentz.append( + normalize_lorentz_expression( + str(lorentz["structure"]), + tuple(int(value) for value in _sequence(lorentz["spins"])), + model_symbols=model_symbols, + ) + ) + for color_index, row_value in enumerate(coupling_matrix): + row = _sequence(row_value) + if len(row) != len(lorentz_names): + raise ValueError( + f"vertex {vertex.get('name')} coupling columns do not match " + "Lorentz structures" + ) + for lorentz_index, coupling_value in enumerate(row): + if coupling_value is None: + continue + coupling_name = str(coupling_value) + try: + coupling = coupling_by_name[coupling_name] + except KeyError as exc: + raise ValueError( + f"vertex {vertex.get('name')} refers to unknown coupling " + f"{coupling_name}" + ) from exc + source_lorentz = lorentz_by_name[lorentz_names[lorentz_index]] + terms.append( + CompiledVertexTerm( + id=len(terms), + vertex=str(vertex["name"]), + particles=particle_names, + color_index=color_index, + lorentz_index=lorentz_index, + color_source=color_sources[color_index], + color_expression=normalized_colors[color_index].expression, + lorentz_name=lorentz_names[lorentz_index], + lorentz_source=str(source_lorentz["structure"]), + lorentz_expression=normalized_lorentz[lorentz_index].expression, + coupling=coupling.name, + coupling_expression=coupling.resolved_expression, + coupling_orders=coupling.orders, + lc_color_normalization_power=( + _lc_color_normalization_power(color_sources[color_index]) + ), + ) + ) + propagators = tuple( + _propagator(item, particles) for item in _mappings(model.get("propagators")) + ) + terms = list( + _record_contact_decomposition_proofs( + terms, + particles, + model_symbols=model_symbols, + ) + ) + terms = list( + _record_heft_contact_orbit_certificates( + terms, + particles, + model_symbols=model_symbols, + ) + ) + oriented_kernels = _compile_oriented_kernels( + terms, + particles, + parameter_records, + propagators, + model_symbols, + ) + contact_particles, contact_kernels = _compile_four_point_contact_kernels( + terms, + particles, + start_kind=len(oriented_kernels), + model_symbols=model_symbols, + ) + contact_particles, contact_kernels = _deduplicate_contact_partials( + contact_particles, + contact_kernels, + terms, + model_symbols=model_symbols, + ) + contact_kernels = _fuse_contact_finals( + contact_kernels, + terms, + model_symbols=model_symbols, + ) + particles = (*particles, *contact_particles) + oriented_kernels = (*oriented_kernels, *contact_kernels) + tree_start_kind = max((kernel.kind for kernel in oriented_kernels), default=-1) + 1 + tree_particles, tree_kernels = _compile_color_singlet_contact_trees( + terms, + particles, + start_kind=tree_start_kind, + model_symbols=model_symbols, + ) + particles = (*particles, *tree_particles) + oriented_kernels = (*oriented_kernels, *tree_kernels) + heft_start_kind = max((kernel.kind for kernel in oriented_kernels), default=-1) + 1 + heft_particles, heft_kernels = _compile_heft_colored_contact_trees( + terms, + particles, + start_kind=heft_start_kind, + model_symbols=model_symbols, + ) + particles = (*particles, *heft_particles) + oriented_kernels = (*oriented_kernels, *heft_kernels) + particles, oriented_kernels = reduce_contact_auxiliary_components( + particles, + oriented_kernels, + auxiliary_particles=(*tree_particles, *heft_particles), + model_symbols=model_symbols, + ) + ( + annotated_terms, + oriented_kernels, + tensor_orderings, + current_orderings, + ) = compile_tensor_ordering_metadata( + terms, + particles, + oriented_kernels, + parameter_records, + propagators, + ) + terms = list(annotated_terms) + oriented_kernels = _annotate_oriented_kernel_color_projections( + oriented_kernels, + particles, + terms, + ) + particles, oriented_kernels = synthesize_fundamental_fierz_auxiliaries( + particles, + oriented_kernels, + propagators, + model_symbols=model_symbols, + ) + ( + annotated_terms, + oriented_kernels, + tensor_orderings, + current_orderings, + ) = compile_tensor_ordering_metadata( + terms, + particles, + oriented_kernels, + parameter_records, + propagators, + ) + terms = list(annotated_terms) + oriented_kernels = _annotate_oriented_kernel_evaluation_equivalence( + oriented_kernels, + particles, + terms, + model_symbols, + ) + direct_contractions, closure_contractions = compile_contraction_records( + particles, + parameter_records, + propagators, + ) + goldstone_partners = compile_goldstone_partner_records( + particles, + parameter_records, + propagators, + ) + return CompiledModelIR( + name=model_name, + orders=tuple(_order(item) for item in _mappings(model.get("orders"))), + parameters=parameter_records, + particles=particles, + couplings=coupling_records, + propagators=propagators, + vertex_terms=tuple(terms), + oriented_kernels=oriented_kernels, + direct_contractions=direct_contractions, + closure_contractions=closure_contractions, + tensor_orderings=tensor_orderings, + current_orderings=current_orderings, + goldstone_partners=goldstone_partners, + ) + + +def _annotate_oriented_kernel_color_projections( + kernels: Sequence[CompiledOrientedKernel], + particles: Sequence[CompiledParticleRecord], + terms: Sequence[CompiledVertexTerm], +) -> tuple[CompiledOrientedKernel, ...]: + particle_by_name = {particle.name: particle for particle in particles} + term_by_id = {term.id: term for term in terms} + annotated: list[CompiledOrientedKernel] = [] + for kernel in kernels: + term = term_by_id.get(kernel.term_id) + oriented_representations = tuple( + particle_by_name[name].color for name in kernel.particles + ) + if term is not None and term.valence == 3 and kernel.vertex == term.vertex: + representations = tuple( + particle_by_name[name].color for name in term.particles + ) + certified_structure, coefficient = project_trilinear_color_expression( + term.color_expression, + representations, + ) + if certified_structure not in _SUPPORTED_TRILINEAR_COLOR_PROJECTIONS: + raise ValueError( + f"vertex {term.vertex!r} has unsupported trilinear color " + f"tensor {term.color_source!r}; pyAmpliCol could not prove it " + "as a singlet, identity, fundamental generator, or adjoint " + "structure constant" + ) + structure, _ = classify_trilinear_color_expression( + kernel.color_expression, + kernel.color_source, + oriented_representations, + allow_source_fallback=True, + ) + if structure not in {certified_structure, "generic-tensor"}: + raise ValueError( + f"vertex {term.vertex!r} changed color-tensor family while " + "being oriented" + ) + if structure == "generic-tensor": + # Numeric prefactors and the dual representation carried by an + # oriented result state can hide the family from the cheap + # oriented recognizer. The original trilinear term and the + # compiler-owned source-leg permutation already certify it. + structure = certified_structure + recurrence_structure = certified_structure + recurrence_proof_source = "exact-trilinear-symbolica-projection-v1" + recurrence_provenance = (("vertex-term-id", str(term.id)),) + tensor_role_representations = tuple( + representations[source_leg] + for source_leg in kernel.source_particle_legs + ) + else: + # Contact fragments are emitted only after compiler_contacts has + # serialized a complete decomposition proof. Their compact source + # is compiler-owned, so textual recognition is safe at this point. + structure, coefficient = classify_trilinear_color_expression( + kernel.color_expression, + kernel.color_source, + oriented_representations, + allow_source_fallback=True, + ) + if structure == "generic-tensor": + raise ValueError( + f"compiler-generated kernel {kernel.vertex!r} has no proven " + "color-flow projection" + ) + contact_contract = _contact_recurrence_color_contract( + kernel, + term_by_id, + ) + if contact_contract is None: + recurrence_structure = structure + recurrence_proof_source = "compiler-generated-color-projection-v1" + recurrence_provenance = () + else: + ( + recurrence_structure, + recurrence_proof_source, + recurrence_provenance, + ) = contact_contract + tensor_role_representations = ( + oriented_representations[0], + oriented_representations[1], + _dual_color_representation(oriented_representations[2]), + ) + annotated_kernel = replace( + kernel, + color_projection_structure=structure, + color_projection_coefficient=( + float(coefficient.real), + float(coefficient.imag), + ), + lc_recurrence_color_rule_kind=recurrence_structure, + ) + recurrence_kernel = replace( + annotated_kernel, + color_projection_structure=recurrence_structure, + ) + annotated_kernel = replace( + annotated_kernel, + lc_color_transition_terms=compile_lc_color_transition_terms( + recurrence_kernel, + oriented_representations, + proof_source=recurrence_proof_source, + provenance=recurrence_provenance, + tensor_role_representations=tensor_role_representations, + ), + ) + annotated.append(annotated_kernel) + return tuple(annotated) + + +def _dual_color_representation(representation: int) -> int: + return -representation if abs(representation) == 3 else representation + + +def _contact_recurrence_color_contract( + kernel: CompiledOrientedKernel, + term_by_id: dict[int, CompiledVertexTerm], +) -> tuple[str, str, tuple[tuple[str, str], ...]] | None: + is_partial = kernel.vertex.endswith("::contact-partial") + is_final = kernel.vertex.endswith(("::contact-final", "::contact-final-fused")) + if not is_partial and not is_final: + # Balanced contact-tree lowering has its own compiler-owned singlet + # auxiliaries and is not described by the four-point split proof. + # Its trilinear color projection is authenticated by the generic + # compiler-generated contract below. + return None + + stage = "partial" if is_partial else "final" + owner_matches = [] + orientation_matches = [] + fused_term_ids = set(kernel.term_ids or (kernel.term_id,)) + # Contact-partial deduplication and final fusion may retain an auxiliary + # owned by a term that is no longer the fused kernel's primary term ID. + # Match the authenticated auxiliary name and source-leg orientation against + # the complete proof catalog instead of trusting that incidental owner. + for term in term_by_id.values(): + if term.contact_decomposition_proof is None: + continue + proof = term.contact_decomposition_proof + if proof.status != "proven": + continue + for split in proof.splits: + auxiliary_name = f"__pyamplicol_contact_{term.id}_r{split.result_leg}" + owns_auxiliary = ( + kernel.particles[2] == auxiliary_name + if stage == "partial" + else auxiliary_name in kernel.particles[:2] + ) + if owns_auxiliary: + owner_matches.append((term, proof, split)) + orientation_is_relevant = ( + owns_auxiliary if stage == "partial" else term.id in fused_term_ids + ) + if not orientation_is_relevant: + continue + for orientation in split.orientations: + if orientation.stage != stage: + continue + expected_source_legs = ( + orientation.input_legs[0], + orientation.input_legs[1], + -1 if stage == "partial" else split.result_leg, + ) + if kernel.source_particle_legs == expected_source_legs: + orientation_matches.append((term, proof, split, orientation)) + if len(owner_matches) != 1 or not orientation_matches: + raise ValueError( + f"compiler-generated contact kernel {kernel.vertex!r} has " + f"{len(owner_matches)} recurrence auxiliary owners and " + f"{len(orientation_matches)} exact stage orientations" + ) + term, proof, split = owner_matches[0] + if any( + candidate_split.decomposition_kind != split.decomposition_kind + for _term, _proof, candidate_split, _orientation in orientation_matches + ): + raise ValueError( + f"compiler-generated contact kernel {kernel.vertex!r} fuses " + "incompatible recurrence color decompositions" + ) + recurrence_structure = { + "literal-color-singlet": "singlet", + "two-structure-constants": "adjoint-structure-constant", + }[split.decomposition_kind] + return ( + recurrence_structure, + f"{proof.algorithm}-recurrence-{stage}-v{proof.algorithm_version}", + ( + ("contact-decomposition-kind", split.decomposition_kind), + ("contact-result-leg", str(split.result_leg)), + ("contact-stage", stage), + ("vertex-term-id", str(term.id)), + ), + ) + + +def _annotate_oriented_kernel_evaluation_equivalence( + kernels: Sequence[CompiledOrientedKernel], + particles: Sequence[CompiledParticleRecord], + terms: Sequence[CompiledVertexTerm], + model_symbols: ModelSymbolRegistry, +) -> tuple[CompiledOrientedKernel, ...]: + """Prove exact signed/permuted kernel relations from lowered expressions. + + The comparison uses the concrete component expressions after resolving + generated coupling aliases. It is therefore independent of how a UFO + author spelled or ordered the source Lorentz/color structures. Only exact + Symbolica canonical equality up to an overall sign and input exchange is + recorded; kernels that do not pass that test remain in distinct classes. + """ + + particle_by_name = {particle.name: particle for particle in particles} + term_by_id = {term.id: term for term in terms} + annotated: list[CompiledOrientedKernel] = [] + for kernel in kernels: + derived_couplings = { + symbols.derived_coupling(model_symbols.model_name, term_id): _sym.E( + term_by_id[term_id].coupling_expression + ) + for name in kernel.runtime_parameters + if name.startswith("derived_coupling_") + for term_id in (int(name.rsplit("_", 1)[1]),) + if term_id in term_by_id + } + coupling = _replace_expression_symbols( + _sym.E(kernel.coupling_expression), + derived_couplings, + ) + # The algebraic colour-flow normalization is evaluated by the kernel, + # so it must also participate in proofs of shared kernel evaluations. + if kernel.lc_color_normalization_power: + coupling *= _sym.E(f"2^(-{kernel.lc_color_normalization_power}/2)") + dimensions = tuple( + compiled_particle_component_dimension(particle) + for particle in ( + particle_by_name[kernel.particles[0]], + particle_by_name[kernel.particles[1]], + particle_by_name[kernel.particles[2]], + ) + ) + candidates: list[tuple[str, tuple[int, int], tuple[float, float], str]] = [] + components_by_input_order: dict[ + tuple[int, int], tuple[_sym.Expression, ...] + ] = {} + for input_order, swap_sides in (((0, 1), False), ((1, 0), True)): + oriented_components = tuple( + ( + _replace_expression_symbols( + _remap_kernel_symbols( + _sym.E(component), + old_kind=kernel.kind, + new_kind=0, + model_symbols=model_symbols, + swap_sides=swap_sides, + ), + derived_couplings, + ) + * coupling + ).expand() + for component in kernel.component_expressions + ) + components_by_input_order[input_order] = oriented_components + oriented_dimensions = ( + dimensions[input_order[0]], + dimensions[input_order[1]], + dimensions[2], + ) + for sign in (1, -1): + # Horner/factor collection optimizes evaluation, but is not a + # unique algebraic normal form: mirrored kernels can retain + # different factorizations of the same coupling polynomial. + # Proof identities must use expanded expressions, while the + # stored runtime components remain optimized and unchanged. + component_strings = tuple( + (sign * component).expand().to_canonical_string() + for component in oriented_components + ) + signature = json.dumps( + { + "input_dimensions": list(oriented_dimensions[:2]), + "output_dimension": oriented_dimensions[2], + "components": list(component_strings), + }, + sort_keys=True, + separators=(",", ":"), + ) + candidates.append( + ( + signature, + input_order, + (float(sign), 0.0), + hashlib.sha256(signature.encode("utf-8")).hexdigest(), + ) + ) + _signature, input_order, factor, class_digest = min( + candidates, + key=lambda candidate: candidate[0], + ) + input_exchange_factor: tuple[float, float] | None = None + if dimensions[0] == dimensions[1]: + direct_components = components_by_input_order[(0, 1)] + swapped_components = components_by_input_order[(1, 0)] + zero = _sym.E("0").to_canonical_string() + for exchange_sign in (1, -1): + if all( + (swapped - exchange_sign * direct).expand().to_canonical_string() + == zero + for direct, swapped in zip( + direct_components, + swapped_components, + strict=True, + ) + ): + input_exchange_factor = (float(exchange_sign), 0.0) + break + annotated.append( + replace( + kernel, + evaluation_class=f"symbolica-sha256:{class_digest}", + evaluation_factor=factor, + evaluation_input_order=input_order, + evaluation_input_exchange_factor=input_exchange_factor, + evaluation_equivalence_verified=True, + ) + ) + reference_factor_by_class: dict[str, complex] = {} + normalized: list[CompiledOrientedKernel] = [] + for kernel in annotated: + class_factor_complex = complex(*kernel.evaluation_factor) + reference_factor = reference_factor_by_class.setdefault( + kernel.evaluation_class, + class_factor_complex, + ) + relative_factor = class_factor_complex / reference_factor + normalized.append( + replace( + kernel, + evaluation_factor=( + float(relative_factor.real), + float(relative_factor.imag), + ), + ) + ) + return tuple(normalized) + + +def _compile_oriented_kernels( + terms: Sequence[CompiledVertexTerm], + particles: Sequence[CompiledParticleRecord], + parameters: Sequence[CompiledParameterRecord], + propagators: Sequence[CompiledPropagatorRecord], + model_symbols: ModelSymbolRegistry, +) -> tuple[CompiledOrientedKernel, ...]: + particle_by_name = {particle.name: particle for particle in particles} + parameter_by_name = {parameter.name: parameter for parameter in parameters} + propagator_by_name = {propagator.name: propagator for propagator in propagators} + external_parameters = { + parameter.name for parameter in parameters if parameter.nature == "external" + } + kernels: list[CompiledOrientedKernel] = [] + for term in terms: + if term.valence != 3: + continue + oriented_result_particles: set[str] = set() + for result_leg in range(3): + result_source_name = term.particles[result_leg] + if result_source_name in oriented_result_particles: + continue + oriented_result_particles.add(result_source_name) + input_legs = tuple(leg for leg in range(3) if leg != result_leg) + input_orders = ( + (input_legs,) + if term.particles[input_legs[0]] == term.particles[input_legs[1]] + else (input_legs, tuple(reversed(input_legs))) + ) + for left_leg, right_leg in input_orders: + result_source = particle_by_name[term.particles[result_leg]] + try: + result_name = particle_by_name[result_source.antiname].name + except KeyError as exc: + raise ValueError( + f"vertex {term.vertex} particle {result_source.name} refers to " + f"absent antiparticle {result_source.antiname}" + ) from exc + ordered_components = _oriented_component_expressions( + term, + particle_by_name, + left_leg=left_leg, + right_leg=right_leg, + result_leg=result_leg, + kind=len(kernels), + model_symbols=model_symbols, + use_transverse_massless_yang_mills=( + _term_supports_transverse_massless_yang_mills( + term, + particle_by_name, + parameter_by_name, + propagator_by_name, + ) + ), + ) + coupling_symbols = set( + _sym.E(term.coupling_expression).get_all_symbols(False) + ) + runtime_parameters = tuple( + sorted( + name + for name in external_parameters + if model_symbols.symbol(name) in coupling_symbols + ) + ) + kernels.append( + CompiledOrientedKernel( + kind=len(kernels), + term_id=term.id, + vertex=term.vertex, + particles=( + term.particles[left_leg], + term.particles[right_leg], + result_name, + ), + source_particle_legs=(left_leg, right_leg, result_leg), + component_expressions=tuple( + str(component) for component in ordered_components.values + ), + coupling_expression=term.coupling_expression, + coupling_orders=term.coupling_orders, + runtime_parameters=runtime_parameters, + color_source=term.color_source, + color_expression=term.color_expression, + lc_color_normalization_power=( + term.lc_color_normalization_power + ), + term_ids=(term.id,), + output_ordering_id=ordered_components.ordering_id, + ) + ) + return _fuse_oriented_kernels( + kernels, + model_name=model_symbols.model_name, + ) + + +def _term_supports_transverse_massless_yang_mills( + term: CompiledVertexTerm, + particles: Mapping[str, CompiledParticleRecord], + parameters: Mapping[str, CompiledParameterRecord], + propagators: Mapping[str, CompiledPropagatorRecord], +) -> bool: + """Prove the field/propagator contract needed by transverse YM lowering.""" + + if not _is_single_structure_constant(term.color_expression): + return False + for name in term.particles: + particle = particles[name] + propagator = ( + None + if particle.propagator is None + else propagators.get(particle.propagator) + ) + if not ( + particle.spin == 3 + and particle.color == 8 + and particle.self_conjugate is True + and _is_compile_time_zero_parameter(particle.mass, parameters) + and particle.propagating + and particle.ghost_number == 0 + and propagator is not None + and propagator.particle == particle.name + and not propagator.custom + ): + return False + return True + + +def _compile_four_point_contact_kernels( + terms: Sequence[CompiledVertexTerm], + particles: Sequence[CompiledParticleRecord], + *, + start_kind: int, + model_symbols: ModelSymbolRegistry, +) -> tuple[tuple[CompiledParticleRecord, ...], tuple[CompiledOrientedKernel, ...]]: + """Lower momentum-independent four-point tensors through dense auxiliaries.""" + + particle_by_name = {particle.name: particle for particle in particles} + used_pdgs = {abs(particle.pdg_code) for particle in particles} + next_pdg = max(9_000_000, max(used_pdgs, default=0) + 1) + auxiliary_particles: list[CompiledParticleRecord] = [] + kernels: list[CompiledOrientedKernel] = [] + + def allocate_pdg() -> int: + nonlocal next_pdg + while next_pdg in used_pdgs: + next_pdg += 1 + result = next_pdg + used_pdgs.add(result) + next_pdg += 1 + return result + + for term in terms: + if term.valence != 4: + continue + if "ufo_momentum_" in term.lorentz_expression: + continue + source_particles = tuple(particle_by_name[name] for name in term.particles) + proof = _validated_contact_decomposition_proof(term) + if ( + proof is None + and term.contact_decomposition_proof is None + and _contact_term_has_literal_color_singlet(term) + and all(particle.color == 1 for particle in source_particles) + ): + # Preserve direct construction of uncolored test/compiler terms. + # UFO compilation always serializes this trivial proof beforehand. + proof = _build_contact_decomposition_proof( + term, + source_particles, + particle_by_name, + model_symbols=model_symbols, + ) + if proof is None: + # Leave the complete term unlowered so model preflight reports a + # structured unsupported-contact-color-lowering error. Partial + # orientation output would incorrectly make the term appear valid. + continue + for split in proof.splits: + result_leg = split.result_leg + source_result = source_particles[result_leg] + remaining_leg = split.remaining_leg + open_legs = split.open_legs + component_axis_order = tuple( + label + for leg in open_legs + for label in _spin_axis_labels(source_particles[leg].spin, leg + 1) + ) + if component_axis_order != split.component_axis_order: + raise ValueError( + f"contact decomposition component order mismatch for term " + f"{term.id} result leg {result_leg}" + ) + auxiliary_name = f"__pyamplicol_contact_{term.id}_r{result_leg}" + representative_indices = split.component_basis_order + component_expansion = split.component_expansion + auxiliary_dimension = len(representative_indices) + auxiliary = CompiledParticleRecord( + name=auxiliary_name, + antiname=auxiliary_name, + pdg_code=allocate_pdg(), + spin=-1, + color=split.auxiliary_color, + mass="ZERO", + width="ZERO", + charge=0.0, + quantum_numbers=(("electric_charge", "0"),), + ghost_number=0, + propagating=False, + goldstoneboson=False, + propagator=None, + component_dimension=auxiliary_dimension, + auxiliary_kind=( + f"ufo-contact:{term.id}:result-{result_leg}:" + + ",".join(str(leg) for leg in open_legs) + ), + ) + auxiliary_particles.append(auxiliary) + + partial_orientations = tuple( + item for item in split.orientations if item.stage == "partial" + ) + for orientation in partial_orientations: + left_leg, right_leg = orientation.input_legs + kind = start_kind + len(kernels) + components = _contact_partial_component_expressions( + term, + particle_by_name, + left_leg=left_leg, + right_leg=right_leg, + open_legs=open_legs, + kind=kind, + model_symbols=model_symbols, + ) + kernels.append( + CompiledOrientedKernel( + kind=kind, + term_id=term.id, + vertex=f"{term.vertex}::contact-partial", + particles=( + source_particles[left_leg].name, + source_particles[right_leg].name, + auxiliary.name, + ), + source_particle_legs=(left_leg, right_leg, -1), + component_expressions=tuple( + _canonicalize_oriented_kernel_component( + _sym.E(components[index]) + * _sym.E(orientation.scalar_prefactor) + ).to_canonical_string() + for index in representative_indices + ), + coupling_expression="1", + coupling_orders=(), + runtime_parameters=(), + color_source="1", + color_expression="1", + lc_color_normalization_power=0, + term_ids=(), + contact_orbit_steps=_compiled_contact_orbit_steps( + term, + split, + orientation, + ), + ) + ) + + result_name = particle_by_name[source_result.antiname].name + final_orientations = tuple( + item for item in split.orientations if item.stage == "final" + ) + for orientation in final_orientations: + auxiliary_on_left = orientation.input_legs[0] == -1 + left_name, right_name = ( + (auxiliary.name, source_particles[remaining_leg].name) + if auxiliary_on_left + else (source_particles[remaining_leg].name, auxiliary.name) + ) + kind = start_kind + len(kernels) + derived_coupling = symbols.derived_coupling( + model_symbols.model_name, + term.id, + ) + final_prefactor = ( + _sym.E(orientation.scalar_prefactor) * derived_coupling + ) + combined_color_source = ( + f"{split.outer_color_source}*{split.final_color_source}" + if split.decomposition_kind == "two-structure-constants" + else term.color_source + ) + kernels.append( + CompiledOrientedKernel( + kind=kind, + term_id=term.id, + vertex=f"{term.vertex}::contact-final", + particles=(left_name, right_name, result_name), + source_particle_legs=( + -1 if auxiliary_on_left else remaining_leg, + remaining_leg if auxiliary_on_left else -1, + result_leg, + ), + component_expressions=tuple( + (_sym.E(component) * final_prefactor).to_canonical_string() + for component in _contact_final_component_expressions( + source_particles, + auxiliary, + open_legs=open_legs, + remaining_leg=remaining_leg, + result_leg=result_leg, + kind=kind, + auxiliary_on_left=auxiliary_on_left, + component_expansion=component_expansion, + model_symbols=model_symbols, + ) + ), + coupling_expression="1", + coupling_orders=term.coupling_orders, + runtime_parameters=(f"derived_coupling_{term.id}",), + color_source=combined_color_source, + color_expression=combined_color_source, + lc_color_normalization_power=( + split.outer_color_normalization_power + + split.final_color_normalization_power + ), + term_ids=(term.id,), + contact_orbit_steps=_compiled_contact_orbit_steps( + term, + split, + orientation, + ), + ) + ) + return tuple(auxiliary_particles), tuple(kernels) + + +def _compiled_contact_orbit_steps( + term: CompiledVertexTerm, + split: CompiledContactDecompositionSplit, + orientation: CompiledContactOrientationProof, +) -> tuple[CompiledContactOrbitStep, ...]: + certificate = term.contact_orbit_certificate + if certificate is None: + return () + return (compiled_contact_orbit_step(certificate, split, orientation),) diff --git a/pyamplicol/src/pyamplicol/models/compiler_gauge.py b/pyamplicol/src/pyamplicol/models/compiler_gauge.py new file mode 100644 index 0000000000..4d9fb911d1 --- /dev/null +++ b/pyamplicol/src/pyamplicol/models/compiler_gauge.py @@ -0,0 +1,90 @@ +# SPDX-License-Identifier: 0BSD +"""Compile explicit gauge/Goldstone partner contracts from normalized UFO IR.""" + +from __future__ import annotations + +from collections.abc import Mapping, Sequence + +from .contracts import ( + CompiledGoldstonePartnerRecord, + CompiledParameterRecord, + CompiledParticleRecord, + CompiledPropagatorRecord, + _goldstone_vector_quantum_contract_matches, + _resolved_mass_expression, +) + + +def compile_goldstone_partner_records( + particles: Sequence[CompiledParticleRecord], + parameters: Sequence[CompiledParameterRecord], + propagators: Sequence[CompiledPropagatorRecord], +) -> tuple[CompiledGoldstonePartnerRecord, ...]: + """Resolve every declared Goldstone once, before executable model loading.""" + + parameter_by_name: Mapping[str, CompiledParameterRecord] = { + parameter.name: parameter for parameter in parameters + } + propagator_by_particle = { + propagator.particle: propagator for propagator in propagators + } + vectors = tuple( + particle + for particle in particles + if particle.spin == 3 + and particle.wavefunction_family == "vector" + and particle.propagating + and not particle.goldstoneboson + ) + records: list[CompiledGoldstonePartnerRecord] = [] + for goldstone in particles: + if not goldstone.goldstoneboson: + continue + if goldstone.spin != 1 or not goldstone.propagating: + raise ValueError( + f"Goldstone {goldstone.name!r} must be a propagating scalar" + ) + candidates = tuple( + vector + for vector in vectors + if _goldstone_vector_quantum_contract_matches( + goldstone, + vector, + parameter_by_name, + ) + and _resolved_mass_expression(vector, parameter_by_name) != "0" + ) + if len(candidates) > 1: + raise ValueError( + f"Goldstone {goldstone.name!r} ambiguously matches vectors " + f"{[vector.name for vector in candidates]!r}" + ) + mass_expression = _resolved_mass_expression( + goldstone, + parameter_by_name, + ) + if not candidates: + records.append( + CompiledGoldstonePartnerRecord( + goldstone=goldstone.name, + vector=None, + policy="explicit", + mass_expression=mass_expression, + ) + ) + continue + vector = candidates[0] + propagator = propagator_by_particle.get(vector.name) + custom = propagator is not None and propagator.custom + records.append( + CompiledGoldstonePartnerRecord( + goldstone=goldstone.name, + vector=vector.name, + policy="model-supplied" if custom else "absorbed", + mass_expression=mass_expression, + ) + ) + return tuple(records) + + +__all__ = ["compile_goldstone_partner_records"] diff --git a/pyamplicol/src/pyamplicol/models/compiler_kernels.py b/pyamplicol/src/pyamplicol/models/compiler_kernels.py new file mode 100644 index 0000000000..70f7d1114e --- /dev/null +++ b/pyamplicol/src/pyamplicol/models/compiler_kernels.py @@ -0,0 +1,654 @@ +# SPDX-License-Identifier: 0BSD +"""Oriented-kernel fusion and component expression lowering.""" + +from __future__ import annotations + +import math +import re +from collections.abc import Mapping, Sequence +from itertools import product + +from .._internal.physics.symbols import ModelSymbolRegistry, symbols +from . import compiler_symbolica as _sym +from .compiler_records import _replace_evaluator_constants, _sequence +from .compiler_tensor_ordering import ( + OrderedComponents, + identity_ordering_for_materialized_axes, +) +from .contracts import ( + CompiledOrientedKernel, + CompiledParameterRecord, + CompiledParticleRecord, + CompiledVertexTerm, +) + + +def _function_arguments(source: str, head_suffix: str) -> tuple[tuple[str, ...], ...]: + """Extract balanced canonical arguments for one fully qualified function.""" + + marker = head_suffix + "(" + result: list[tuple[str, ...]] = [] + cursor = 0 + while True: + marker_start = source.find(marker, cursor) + if marker_start < 0: + break + open_index = marker_start + len(head_suffix) + depth = 0 + brace_depth = 0 + argument_start = open_index + 1 + arguments: list[str] = [] + index = open_index + while index < len(source): + character = source[index] + if character == "(": + depth += 1 + elif character == ")": + depth -= 1 + if depth == 0: + arguments.append(source[argument_start:index]) + result.append(tuple(arguments)) + cursor = index + 1 + break + elif character == "{": + brace_depth += 1 + elif character == "}": + brace_depth -= 1 + elif character == "," and depth == 1 and brace_depth == 0: + arguments.append(source[argument_start:index]) + argument_start = index + 1 + index += 1 + else: + raise ValueError(f"unbalanced canonical function {head_suffix}") + return tuple(result) + + +def _permutation_sign( + actual: tuple[int, ...], + canonical: tuple[int, ...], +) -> int: + if sorted(actual) != sorted(canonical): + raise ValueError( + f"color-factor indices {actual} do not match local orientation {canonical}" + ) + positions = {value: index for index, value in enumerate(canonical)} + permutation = tuple(positions[value] for value in actual) + inversions = sum( + permutation[left] > permutation[right] + for left in range(len(permutation)) + for right in range(left + 1, len(permutation)) + ) + return -1 if inversions % 2 else 1 + + +def _fuse_oriented_kernels( + kernels: Sequence[CompiledOrientedKernel], + *, + model_name: str, +) -> tuple[CompiledOrientedKernel, ...]: + model_symbols = symbols.model(model_name) + groups: dict[tuple[object, ...], list[CompiledOrientedKernel]] = {} + for kernel in kernels: + key = ( + kernel.vertex, + kernel.particles, + kernel.source_particle_legs, + kernel.coupling_orders, + kernel.color_source, + kernel.color_expression, + kernel.lc_color_normalization_power, + kernel.input_ordering_ids, + kernel.output_ordering_id, + ) + groups.setdefault(key, []).append(kernel) + + fused: list[CompiledOrientedKernel] = [] + for members in groups.values(): + kind = len(fused) + first = members[0] + remapped_components = [ + tuple( + _remap_kernel_symbols( + _sym.E(component), + old_kind=member.kind, + new_kind=kind, + model_symbols=model_symbols, + ) + for component in member.component_expressions + ) + for member in members + ] + components = tuple( + _canonicalize_oriented_kernel_component( + sum( + ( + remapped[index] + * symbols.derived_coupling(model_name, member.term_id) + for member, remapped in zip( + members, + remapped_components, + strict=True, + ) + ), + _sym.E("0"), + ) + ) + for index in range(len(remapped_components[0])) + ) + coupling_expression = "1" + fused.append( + CompiledOrientedKernel( + kind=kind, + term_id=first.term_id, + vertex=first.vertex, + particles=first.particles, + source_particle_legs=first.source_particle_legs, + component_expressions=tuple( + component.to_canonical_string() for component in components + ), + coupling_expression=coupling_expression, + coupling_orders=first.coupling_orders, + runtime_parameters=tuple( + sorted({f"derived_coupling_{member.term_id}" for member in members}) + ), + color_source=first.color_source, + color_expression=first.color_expression, + lc_color_normalization_power=first.lc_color_normalization_power, + term_ids=tuple( + term_id + for member in members + for term_id in (member.term_ids or (member.term_id,)) + ), + input_ordering_ids=first.input_ordering_ids, + output_ordering_id=first.output_ordering_id, + ) + ) + return tuple(fused) + + +def _canonicalize_oriented_kernel_component( + expression: _sym.Expression, +) -> _sym.Expression: + """Cancel numeric sums, factor couplings, and group primitive inputs.""" + + return expression.expand_num().collect_factors().collect_horner() + + +def _remap_kernel_symbols( + expression: _sym.Expression, + *, + old_kind: int, + new_kind: int, + model_symbols: ModelSymbolRegistry, + swap_sides: bool = False, +) -> _sym.Expression: + source = expression.to_canonical_string() + kernel_symbols = { + (side, momentum_marker == "momentum_", int(index)) + for side, momentum_marker, index in re.findall( + rf"kernel_{old_kind}_(left|right)_(momentum_)?([0-9]+)", + source, + ) + } + replacements: list[_sym.Replacement] = [] + for side, is_momentum, index in kernel_symbols: + target_side = ("right" if side == "left" else "left") if swap_sides else side + if is_momentum: + replacements.append( + _sym.Replacement( + model_symbols.kernel_momentum(old_kind, side, index), + model_symbols.kernel_momentum(new_kind, target_side, index), + ) + ) + else: + replacements.append( + _sym.Replacement( + model_symbols.kernel_component(old_kind, side, index), + model_symbols.kernel_component(new_kind, target_side, index), + ) + ) + return expression.replace_multiple(replacements) if replacements else expression + + +def _replace_expression_symbols( + expression: _sym.Expression, + substitutions: Mapping[_sym.Expression, _sym.Expression], +) -> _sym.Expression: + result = expression + for source, target in substitutions.items(): + result = result.replace(source, target) + return result + + +def _oriented_component_expressions( + term: CompiledVertexTerm, + particle_by_name: Mapping[str, CompiledParticleRecord], + *, + left_leg: int, + right_leg: int, + result_leg: int, + kind: int, + model_symbols: ModelSymbolRegistry, + use_transverse_massless_yang_mills: bool = False, +) -> OrderedComponents: + library = _sym.TensorLibrary.hep_lib_atom() + expression = _sym.E(term.lorentz_expression) + particles = tuple(particle_by_name[name] for name in term.particles) + left_symbols = _component_symbols( + kind, + "left", + particles[left_leg].spin, + model_symbols=model_symbols, + ) + right_symbols = _component_symbols( + kind, + "right", + particles[right_leg].spin, + model_symbols=model_symbols, + ) + expression *= _input_tensor_expression( + library, + kind=kind, + side="left", + spin=particles[left_leg].spin, + leg=left_leg + 1, + components=left_symbols, + model_symbols=model_symbols, + ) + expression *= _input_tensor_expression( + library, + kind=kind, + side="right", + spin=particles[right_leg].spin, + leg=right_leg + 1, + components=right_symbols, + model_symbols=model_symbols, + ) + left_momentum = tuple( + model_symbols.kernel_momentum(kind, "left", component) for component in range(4) + ) + right_momentum = tuple( + model_symbols.kernel_momentum(kind, "right", component) + for component in range(4) + ) + result_momentum = tuple( + -(left_momentum[component] + right_momentum[component]) + for component in range(4) + ) + momentum_by_leg = { + left_leg: left_momentum, + right_leg: right_momentum, + result_leg: result_momentum, + } + minkowski = _sym.Representation.mink(4) + for leg, momentum in momentum_by_leg.items(): + library.register( + _sym.Tensor.dense( + _sym.TensorName(model_symbols.ufo_momentum_tensor_name(leg + 1))( + minkowski + ), + momentum, + ) + ) + axis_labels = _spin_axis_labels(particles[result_leg].spin, result_leg + 1) + # Plain expressions already carry the correct empty scalar interface. + network = _sym.TensorNetwork( + _sym.as_tensor(expression) if axis_labels else expression, library + ) + network.execute(library=library) + result = network.result_tensor(library) + ordered_result = _ordered_dense_tensor_components( + result, + axis_labels, + ) + result_components = ordered_result.values + expected_dimension = _spin_dimension(particles[result_leg].spin) + if len(result_components) != expected_dimension: + raise ValueError( + f"oriented kernel {term.vertex}/{term.id} produced " + f"{len(result_components)} " + f"components for spin {particles[result_leg].spin}, expected " + f"{expected_dimension}" + ) + components = tuple( + _replace_evaluator_constants(_as_expression(component)) + for component in result_components + ) + if all(particle.spin == 3 for particle in particles): + compact = _compact_yang_mills_three_vector_components( + left_leg=left_leg, + right_leg=right_leg, + result_leg=result_leg, + left_components=left_symbols, + right_components=right_symbols, + momentum_by_leg=momentum_by_leg, + ) + scale = _equivalent_component_scale(components, compact) + if scale is not None: + if use_transverse_massless_yang_mills: + compact = _transverse_yang_mills_three_vector_components( + left_components=left_symbols, + right_components=right_symbols, + left_momentum=left_momentum, + right_momentum=right_momentum, + ) + components = tuple(scale * component for component in compact) + return OrderedComponents( + ordering=ordered_result.ordering, + values=tuple(component.to_canonical_string() for component in components), + ) + + +def _compact_yang_mills_three_vector_components( + *, + left_leg: int, + right_leg: int, + result_leg: int, + left_components: Sequence[_sym.Expression], + right_components: Sequence[_sym.Expression], + momentum_by_leg: Mapping[int, Sequence[_sym.Expression]], +) -> tuple[_sym.Expression, ...]: + """Return the compact canonical Yang-Mills three-vector contraction. + + The caller retains this form only after proving algebraic equivalence to the + fully materialized spenso tensor. This makes the optimization independent + of UFO Lorentz names and source-expression layout. + """ + + components_by_leg = { + left_leg: tuple(left_components), + right_leg: tuple(right_components), + } + terms = ( + ((0, 1), 0, 1, 2), + ((0, 2), 2, 0, 1), + ((1, 2), 1, 2, 0), + ) + outputs: list[_sym.Expression] = [] + for component in range(4): + output = _sym.E("0") + for ( + metric_legs, + positive_momentum_leg, + negative_momentum_leg, + vector_leg, + ) in terms: + momentum = tuple( + momentum_by_leg[positive_momentum_leg][index] + - momentum_by_leg[negative_momentum_leg][index] + for index in range(4) + ) + if result_leg == vector_leg: + output += momentum[component] * _minkowski_dot( + components_by_leg[metric_legs[0]], + components_by_leg[metric_legs[1]], + ) + continue + other_metric_leg = ( + metric_legs[1] if metric_legs[0] == result_leg else metric_legs[0] + ) + output += components_by_leg[other_metric_leg][component] * _minkowski_dot( + momentum, + components_by_leg[vector_leg], + ) + outputs.append(output) + return tuple(outputs) + + +def _transverse_yang_mills_three_vector_components( + *, + left_components: Sequence[_sym.Expression], + right_components: Sequence[_sym.Expression], + left_momentum: Sequence[_sym.Expression], + right_momentum: Sequence[_sym.Expression], +) -> tuple[_sym.Expression, ...]: + """Return the transverse Berends-Giele massless gauge current. + + The full Yang-Mills contraction differs only by terms proportional to each + parent current's self-momentum contraction. AmpliCol removes those terms + for massless adjoint gauge currents. Applying the same tensor-derived + reduction preserves its fixed-width recursion convention without relying + on model-specific particle or Lorentz names. + """ + + left = tuple(left_components) + right = tuple(right_components) + left_p = tuple(left_momentum) + right_p = tuple(right_momentum) + dot = _minkowski_dot(left, right) + left_dot_right_p = _minkowski_dot(left, right_p) + right_dot_left_p = _minkowski_dot(right, left_p) + return tuple( + dot * (left_p[index] - right_p[index]) + + 2 * (left_dot_right_p * right[index] - right_dot_left_p * left[index]) + for index in range(4) + ) + + +def _equivalent_component_scale( + materialized: Sequence[_sym.Expression], + compact: Sequence[_sym.Expression], +) -> _sym.Expression | None: + if len(materialized) != len(compact) or not materialized: + return None + if any( + frozenset( + symbol.to_canonical_string() for symbol in dense.get_all_symbols(False) + ) + != frozenset( + symbol.to_canonical_string() for symbol in candidate.get_all_symbols(False) + ) + for dense, candidate in zip(materialized, compact, strict=True) + ): + return None + for scale in (_sym.E("1"), _sym.E("-1")): + if all( + (dense - scale * candidate).expand().to_canonical_string() == "0" + for dense, candidate in zip(materialized, compact, strict=True) + ): + return scale + # The compact Yang-Mills convention is only certified for an exact sign. + # Retaining the fully materialized tensor is always correct for every other + # normalization. In particular, do not derive a general ratio here: + # Symbolica's polynomial cancellation aborts the process for float atoms. + return None + + +def _is_compile_time_zero_parameter( + name: str, + parameters: Mapping[str, CompiledParameterRecord], +) -> bool: + if name.upper() == "ZERO": + return True + parameter = parameters.get(name) + if parameter is None or parameter.nature != "internal": + return False + expression = _sym.E(parameter.resolved_expression) + if expression.get_all_symbols(False): + return False + try: + return complex(expression.evaluate({})) == 0.0 + except (TypeError, ValueError): + return False + + +def _is_single_structure_constant(expression: str) -> bool: + return ( + expression.count("::f(") == 1 + and len(_function_arguments(expression, "::f")) == 1 + and "::t(" not in expression.lower() + and "::d(" not in expression.lower() + ) + + +def _minkowski_dot( + left: Sequence[_sym.Expression], + right: Sequence[_sym.Expression], +) -> _sym.Expression: + if len(left) != 4 or len(right) != 4: + raise ValueError("Minkowski dot products require four components") + return left[0] * right[0] - sum( + (left[index] * right[index] for index in range(1, 4)), + _sym.E("0"), + ) + + +def _input_tensor_expression( + library: _sym.TensorLibrary, + *, + kind: int, + side: str, + spin: int, + leg: int, + components: Sequence[_sym.Expression], + model_symbols: ModelSymbolRegistry, +) -> _sym.Expression: + representations = _spin_representations(spin) + if not representations: + if len(components) != 1: + raise ValueError("scalar current must have exactly one component") + return components[0] + name = _sym.TensorName(model_symbols.kernel_tensor_name(kind, side)) + library.register(_sym.Tensor.dense(name(*representations), components)) + slots = _spin_slots(spin, leg) + return name(*slots).to_expression() + + +def _spin_representations(spin: int) -> tuple[_sym.Representation, ...]: + minkowski = _sym.Representation.mink(4) + if spin in {-1, 1}: + return () + if spin == 2: + return (_sym.Representation.bis(4),) + if spin == 3: + return (minkowski,) + if spin == 5: + return (minkowski, minkowski) + raise ValueError(f"unsupported UFO spin code {spin}") + + +def _spin_slots(spin: int, leg: int): + representations = _spin_representations(spin) + if spin == 2: + return (representations[0](f"ufo_s_1_{leg}"),) + if spin == 3: + return (representations[0](f"ufo_l_1_{leg}"),) + if spin == 5: + return ( + representations[0](f"ufo_l_1_{leg}"), + representations[1](f"ufo_l_2_{leg}"), + ) + return () + + +def _spin_axis_labels(spin: int, leg: int) -> tuple[str, ...]: + if spin == 2: + return (f"ufo_s_1_{leg}",) + if spin == 3: + return (f"ufo_l_1_{leg}",) + if spin == 5: + return (f"ufo_l_1_{leg}", f"ufo_l_2_{leg}") + return () + + +def _ordered_dense_tensor_components( + tensor: object, + expected_axis_labels: Sequence[str], +) -> OrderedComponents: + """Flatten a dense tensor in explicit physical-index order.""" + + tensor = tensor.to_dense() + expected = tuple(str(label) for label in expected_axis_labels) + if not expected: + if len(tensor) != 1: + raise ValueError("scalar tensor result unexpectedly has open indices") + return OrderedComponents( + ordering=identity_ordering_for_materialized_axes((), ()), + values=tuple(tensor[index] for index in range(len(tensor))), + ) + + # Component coordinates follow the tensor's logical axes, not its canonical + # structure signature, which can put the same axes in a different order. + arguments = tuple(slot.to_expression() for slot in tensor.axes) + actual: list[str] = [] + for argument in arguments: + source = argument.to_canonical_string() + labels = re.findall(r"ufo_[sl]_[0-9]+_[0-9]+", source) + if len(labels) != 1: + raise ValueError( + "tensor result has an unrecognized physical index: " + source + ) + actual.append(labels[0]) + if len(set(actual)) != len(actual) or set(actual) != set(expected): + raise ValueError( + "tensor result index labels do not match the requested physical order: " + f"actual={actual}, expected={list(expected)}" + ) + + positions = tuple(actual.index(label) for label in expected) + shape = tuple(int(dimension) for dimension in tensor.shape) + if len(shape) != len(actual) or math.prod(shape) != len(tensor): + raise ValueError( + "tensor result does not cover the complete Cartesian component grid" + ) + by_coordinates: dict[tuple[int, ...], object] = {} + for flat_index, coordinates in enumerate(product(*(range(size) for size in shape))): + key = tuple(int(coordinates[position]) for position in positions) + by_coordinates[key] = tensor[flat_index] + ordered_shape = tuple(shape[position] for position in positions) + canonical_coordinates = product(*(range(size) for size in ordered_shape)) + ordering = identity_ordering_for_materialized_axes( + expected, + ordered_shape, + ) + return OrderedComponents( + ordering=ordering, + values=tuple(by_coordinates[key] for key in canonical_coordinates), + ) + + +def _spin_dimension(spin: int) -> int: + return {-1: 1, 1: 1, 2: 4, 3: 4, 5: 16}[spin] + + +def _component_symbols( + kind: int, + side: str, + spin: int, + *, + model_symbols: ModelSymbolRegistry, +) -> tuple[_sym.Expression, ...]: + return tuple( + model_symbols.kernel_component(kind, side, component) + for component in range(_spin_dimension(spin)) + ) + + +def _lc_color_normalization_power(source: str) -> int: + """Count normalized non-Abelian tensors in one UFO color monomial.""" + + expression = _sym.E(source) + return sum( + len(list(expression.match(_sym.E(pattern)))) + for pattern in ( + "UFO::T(a_,b_,c_)", + "UFO::f(a_,b_,c_)", + "UFO::d(a_,b_,c_)", + ) + ) + + +def _as_expression(value: object) -> _sym.Expression: + _sym._ensure_symbolica() + if isinstance(value, _sym.Expression): + return value + raise TypeError( + f"spenso kernel component is not an Expression: {type(value).__name__}" + ) + + +def cast_tuple3(value: object) -> tuple[str, str, str]: + values = tuple(str(item) for item in _sequence(value)) + if len(values) != 3: + raise ValueError("oriented kernel particles must have length three") + return values[0], values[1], values[2] diff --git a/pyamplicol/src/pyamplicol/models/compiler_records.py b/pyamplicol/src/pyamplicol/models/compiler_records.py new file mode 100644 index 0000000000..44ae646400 --- /dev/null +++ b/pyamplicol/src/pyamplicol/models/compiler_records.py @@ -0,0 +1,294 @@ +# SPDX-License-Identifier: 0BSD +"""Canonical UFO record parsing and expression resolution.""" + +from __future__ import annotations + +import math +from collections.abc import Mapping, Sequence +from dataclasses import replace +from fractions import Fraction +from typing import cast + +from .._internal.physics.symbols import ModelSymbolRegistry +from . import compiler_symbolica as _sym +from .contracts import ( + DEFAULT_FEYNMAN_PROPAGATOR_SOURCE, + MODEL_SUPPLIED_PROPAGATOR_SOURCE, + PROPAGATOR_SOURCE_FIELD, + CompiledCouplingOrder, + CompiledCouplingRecord, + CompiledParameterRecord, + CompiledParticleRecord, + CompiledPropagatorRecord, + validate_quantum_number_flow, +) + +_COMPILER_OWNED_PARTICLE_FIELDS = frozenset({"auxiliary_kind", "component_dimension"}) + + +def cast_int_tuple3(value: object) -> tuple[int, int, int]: + values = tuple(int(item) for item in _sequence(value)) + if len(values) != 3: + raise ValueError("oriented kernel source legs must have length three") + return values[0], values[1], values[2] + + +def _order(item: Mapping[str, object]) -> CompiledCouplingOrder: + return CompiledCouplingOrder( + name=str(item["name"]), + expansion_order=int(item["expansion_order"]), + hierarchy=int(item["hierarchy"]), + ) + + +def _parameter( + item: Mapping[str, object], + *, + model_symbols: ModelSymbolRegistry | None = None, +) -> CompiledParameterRecord: + expression = _optional_string(item.get("expression")) + if expression is not None and model_symbols is not None: + expression = model_symbols.expression_string(expression) + value = _optional_pair(item.get("value")) + return CompiledParameterRecord( + name=str(item["name"]), + nature=str(item["nature"]), + parameter_type=str(item["parameter_type"]), + value=value, + expression=expression, + resolved_expression=( + expression + if expression is not None + else _numeric_expression(value or (0.0, 0.0)).to_canonical_string() + ), + lhablock=_optional_string(item.get("lhablock")), + lhacode=tuple(int(value) for value in _sequence(item.get("lhacode"))), + ) + + +def _particle( + item: Mapping[str, object], +) -> CompiledParticleRecord: + name = str(item["name"]) + supplied_metadata = sorted(_COMPILER_OWNED_PARTICLE_FIELDS.intersection(item)) + if supplied_metadata: + fields = ", ".join(supplied_metadata) + raise ValueError( + f"particle {name!r} supplies compiler-owned metadata in " + f"untrusted input: {fields}" + ) + return _particle_record(item) + + +def _particle_record(item: Mapping[str, object]) -> CompiledParticleRecord: + name = str(item["name"]) + raw_charge = item.get("charge", 0.0) + # Loader 1.0 exports exact rational strings; retain them in charge-flow + # proofs while keeping the public numerical charge a float. + exact_charge = Fraction(raw_charge) if isinstance(raw_charge, str) else None + charge = float(exact_charge if exact_charge is not None else raw_charge) + quantum_numbers = item.get("quantum_numbers") + if quantum_numbers is None: + charge_flow = ( + str(exact_charge) + if exact_charge is not None + else _exact_float_expression(charge) + ) + quantum_numbers = (("electric_charge", charge_flow),) + return CompiledParticleRecord( + name=name, + antiname=str(item["antiname"]), + pdg_code=int(item["pdg_code"]), + spin=int(item["spin"]), + color=int(item["color"]), + mass=str(item["mass"]), + width=str(item["width"]), + charge=charge, + quantum_numbers=validate_quantum_number_flow( + quantum_numbers, + context=f"particle {name!r}", + ), + ghost_number=int(item.get("ghost_number", 0)), + propagating=bool(item.get("propagating", True)), + goldstoneboson=bool(item.get("goldstoneboson", False)), + propagator=_optional_string(item.get("propagator")), + component_dimension=cast(int | None, item.get("component_dimension")), + auxiliary_kind=_optional_string(item.get("auxiliary_kind")), + ) + + +def _exact_float_expression(value: float) -> str: + if not math.isfinite(value): + raise ValueError("particle electric charge must be finite") + numerator, denominator = value.as_integer_ratio() + if denominator == 1: + return str(numerator) + return f"{numerator}/{denominator}" + + +def _coupling( + item: Mapping[str, object], + *, + model_symbols: ModelSymbolRegistry | None = None, +) -> CompiledCouplingRecord: + expression = str(item["expression"]) + if model_symbols is not None: + expression = model_symbols.expression_string(expression) + return CompiledCouplingRecord( + name=str(item["name"]), + expression=expression, + resolved_expression=expression, + value=_optional_pair(item.get("value")), + orders=_orders(item.get("orders")), + ) + + +def _propagator( + item: Mapping[str, object], + particles: Sequence[CompiledParticleRecord], +) -> CompiledPropagatorRecord: + particle_name = str(item["particle"]) + particle = next( + (candidate for candidate in particles if candidate.name == particle_name), + None, + ) + name = str(item["name"]) + if particle is None: + raise ValueError( + f"propagator {name!r} refers to unknown particle {particle_name!r}" + ) + if particle.propagator != name: + raise ValueError( + f"propagator {name!r} is not linked from particle {particle_name!r}" + ) + source = str(item.get(PROPAGATOR_SOURCE_FIELD, "")) + if source not in { + DEFAULT_FEYNMAN_PROPAGATOR_SOURCE, + MODEL_SUPPLIED_PROPAGATOR_SOURCE, + }: + raise ValueError( + f"propagator {name!r} has no valid generation-time source metadata" + ) + return CompiledPropagatorRecord( + name=name, + particle=particle_name, + numerator=str(item["numerator"]), + denominator=str(item["denominator"]), + custom=source == MODEL_SUPPLIED_PROPAGATOR_SOURCE, + ) + + +def _orders(value: object) -> tuple[tuple[str, int], ...]: + result = [] + for pair in _sequence(value): + values = _sequence(pair) + if len(values) != 2: + raise ValueError("coupling order must be [name, value]") + result.append((str(values[0]), int(values[1]))) + return tuple(result) + + +def _pair(value: object) -> tuple[float, float]: + pair = _sequence(value) + if len(pair) != 2: + raise ValueError("complex value must be [real, imaginary]") + return float(pair[0]), float(pair[1]) + + +def _optional_pair(value: object) -> tuple[float, float] | None: + return None if value is None else _pair(value) + + +def _optional_string(value: object) -> str | None: + return None if value is None else str(value) + + +def _resolve_parameter_records( + records: Sequence[CompiledParameterRecord], + model_symbols: ModelSymbolRegistry, +) -> tuple[CompiledParameterRecord, ...]: + by_name = {record.name: record for record in records} + parameter_symbols = {name: model_symbols.symbol(name) for name in by_name} + resolved: dict[str, _sym.Expression] = {} + active: list[str] = [] + + def resolve(name: str) -> _sym.Expression: + if name in resolved: + return resolved[name] + if name in active: + cycle = " -> ".join((*active, name)) + raise ValueError(f"cyclic UFO parameter definitions: {cycle}") + record = by_name[name] + if record.nature == "external": + resolved[name] = parameter_symbols[name] + return resolved[name] + active.append(name) + expression = ( + model_symbols.expression(record.expression) + if record.expression is not None + else _numeric_expression(record.value or (0.0, 0.0)) + ) + expression_symbols = set(expression.get_all_symbols(False)) + for dependency, symbol in parameter_symbols.items(): + if dependency == name or symbol not in expression_symbols: + continue + expression = expression.replace(symbol, resolve(dependency)) + active.pop() + resolved[name] = expression + return resolved[name] + + return tuple( + replace(record, resolved_expression=resolve(record.name).to_canonical_string()) + for record in records + ) + + +def _resolve_coupling_records( + records: Sequence[CompiledCouplingRecord], + parameters: Sequence[CompiledParameterRecord], + model_symbols: ModelSymbolRegistry, +) -> tuple[CompiledCouplingRecord, ...]: + replacements = { + model_symbols.symbol(parameter.name): _sym.E(parameter.resolved_expression) + for parameter in parameters + if parameter.nature != "external" + } + result = [] + for record in records: + expression = model_symbols.expression(record.expression) + expression_symbols = set(expression.get_all_symbols(False)) + for symbol, replacement_expression in replacements.items(): + if symbol in expression_symbols: + expression = expression.replace(symbol, replacement_expression) + result.append( + replace( + record, + resolved_expression=expression.to_canonical_string(), + ) + ) + return tuple(result) + + +def _replace_evaluator_constants(expression: _sym.Expression) -> _sym.Expression: + # Only call at the f64 evaluator boundary. The model's symbolic records + # must retain Pi: rounding it before resolving G and G**2 independently + # breaks gauge identities that higher-precision evaluation cannot recover. + return expression.replace(_sym.E("pi"), _sym.E(repr(math.pi))) + + +def _numeric_expression(value: tuple[float, float]) -> _sym.Expression: + real, imaginary = value + return _sym.E(repr(real)) + _sym.E(repr(imaginary)) * _sym.E("1𝑖") # noqa: RUF001 + + +def _sequence(value: object) -> list[object]: + return list(value) if isinstance(value, (list, tuple)) else [] + + +def _mappings(value: object) -> list[dict[str, object]]: + result = [] + for item in _sequence(value): + if not isinstance(item, dict): + raise ValueError("compiled model list entry must be an object") + result.append({str(key): element for key, element in item.items()}) + return result diff --git a/pyamplicol/src/pyamplicol/models/compiler_symbolica.py b/pyamplicol/src/pyamplicol/models/compiler_symbolica.py new file mode 100644 index 0000000000..ed07d479c4 --- /dev/null +++ b/pyamplicol/src/pyamplicol/models/compiler_symbolica.py @@ -0,0 +1,91 @@ +# SPDX-License-Identifier: 0BSD +"""Process-wide lazy Symbolica namespace for model compilation.""" + +from __future__ import annotations + +import math +from threading import RLock +from typing import Any + +E: Any = None +S: Any = None +Expression: Any = None +Replacement: Any = None +Tensor: Any = None +Representation: Any = None +TensorLibrary: Any = None +TensorName: Any = None +TensorNetwork: Any = None +as_tensor: Any = None +_SYMBOLICA_READY = False +_SYMBOLICA_LOCK = RLock() + + +def _ensure_symbolica() -> None: + global E, S, Expression, Replacement, Tensor, Representation + global TensorLibrary, TensorName, TensorNetwork, as_tensor, _SYMBOLICA_READY + + if _SYMBOLICA_READY: + return + with _SYMBOLICA_LOCK: + if _SYMBOLICA_READY: + return + from symbolica import E as expression_parser + from symbolica import Expression as expression_type + from symbolica import Replacement as replacement_type + from symbolica import S as symbol + from symbolica.community.tensor import Representation as representation_type + from symbolica.community.tensor import Tensor as tensor_type + from symbolica.community.tensor import TensorLibrary as tensor_library_type + from symbolica.community.tensor import TensorName as tensor_name_type + from symbolica.community.tensor import TensorNetwork as tensor_network_type + from symbolica.community.tensor import as_tensor as tensor_expression + + E = expression_parser + S = symbol + Expression = expression_type + Replacement = replacement_type + Tensor = tensor_type + Representation = representation_type + TensorLibrary = tensor_library_type + TensorName = tensor_name_type + TensorNetwork = tensor_network_type + as_tensor = tensor_expression + _SYMBOLICA_READY = True + + +def _exact_binary64_coefficients(expression: Any) -> Any: + """Replace binary64 atoms by rationals with exactly their stored value. + + Literal matching preserves the numeric kind and precision: rational and + higher-precision atoms must never be rounded through Python's ``complex``. + """ + from symbolica import AtomType + + _ensure_symbolica() + pending = [expression] + replacements: dict[Any, Any] = {} + while pending: + atom = pending.pop() + kind = atom.get_type() + if kind == AtomType.Var: + continue + if kind != AtomType.Num: + pending.extend(atom) + continue + try: + value = complex(atom) + except (TypeError, ValueError, OverflowError): + continue + if not math.isfinite(value.real) or not math.isfinite(value.imag): + continue + if not bool(atom.matches(Expression.num(value))): + continue + real_n, real_d = value.real.as_integer_ratio() + imag_n, imag_d = value.imag.as_integer_ratio() + replacements[atom] = E(f"({real_n}/{real_d})+1i*({imag_n}/{imag_d})") + if not replacements: + return expression + return expression.replace_multiple( + [Replacement(source, target) for source, target in replacements.items()] + ) diff --git a/pyamplicol/src/pyamplicol/models/compiler_tensor_ordering.py b/pyamplicol/src/pyamplicol/models/compiler_tensor_ordering.py new file mode 100644 index 0000000000..3986b6b06e --- /dev/null +++ b/pyamplicol/src/pyamplicol/models/compiler_tensor_ordering.py @@ -0,0 +1,507 @@ +# SPDX-License-Identifier: 0BSD +"""Compile explicit component-ordering contracts for external UFO models.""" + +from __future__ import annotations + +import re +from collections.abc import Mapping, Sequence +from dataclasses import dataclass, replace + +from ._physics_ir import ( + TENSOR_ORDERING_CONTRACT_VERSION, + CompiledCurrentOrderingRecord, + TensorAxisIR, + TensorIndexBindingIR, + TensorOrderingIR, +) +from .contracts import ( + CompiledOrientedKernel, + CompiledParameterRecord, + CompiledParticleRecord, + CompiledPropagatorRecord, + CompiledVertexTerm, + compiled_particle_component_dimension, + compiled_particle_is_chiral_eligible, +) + +_NORMALIZED_INDEX_PATTERN = re.compile( + r"\bufo_(?P[lsc])_(?:" + r"dummy_(?P[0-9]+)(?:_(?P[a-z]+))?" + r"|(?P[0-9]+)_(?P[0-9]+)" + r"|(?P[0-9]+))\b" +) +_NORMALIZED_INDEX_TOKEN_PATTERN = re.compile(r"\bufo_[lsc]_[A-Za-z0-9_]+\b") +_COLOR_DUMMY_SPACES = frozenset( + {"adjoint", "fundamental", "antifundamental"} +) + + +@dataclass(frozen=True, slots=True) +class OrderedComponents: + """Materialized values with their proven canonical storage contract.""" + + ordering: TensorOrderingIR + values: tuple[object, ...] + + def __post_init__(self) -> None: + if len(self.values) != self.ordering.stored_size: + raise ValueError( + "materialized component count does not match its tensor ordering" + ) + + @property + def ordering_id(self) -> str: + return self.ordering.ordering_id + + +def identity_ordering_for_materialized_axes( + expected_axis_labels: Sequence[str], + extents: Sequence[int], +) -> TensorOrderingIR: + labels = tuple(str(label) for label in expected_axis_labels) + dimensions = tuple(int(extent) for extent in extents) + if len(labels) != len(dimensions): + raise ValueError("tensor axis labels and extents must have equal length") + spaces = tuple(_space_from_axis_label(label) for label in labels) + return TensorOrderingIR.identity( + basis=_basis_for_spaces(spaces), + axes=tuple( + TensorAxisIR(name=f"axis-{index}", space=space, extent=extent) + for index, (space, extent) in enumerate( + zip(spaces, dimensions, strict=True) + ) + ), + ) + + +def compile_vertex_index_bindings( + term: CompiledVertexTerm, + particle_by_name: Mapping[str, CompiledParticleRecord], +) -> tuple[TensorIndexBindingIR, ...]: + """Record physical-leg and dummy provenance without making it identity.""" + + descriptors: list[tuple[str, str, int | None, int | None, int | None]] = [] + seen: set[tuple[str, str, int | None, int | None, int | None]] = set() + for origin, expression in ( + ("color", term.color_expression), + ("lorentz", term.lorentz_expression), + ): + matches = tuple(_NORMALIZED_INDEX_PATTERN.finditer(expression)) + matched_spans = {match.span() for match in matches} + for token in _NORMALIZED_INDEX_TOKEN_PATTERN.finditer(expression): + if token.span() not in matched_spans: + raise ValueError( + f"vertex term {term.id} contains malformed normalized tensor " + f"index {token.group(0)!r}" + ) + for match in matches: + family = match.group("family") + component_text = match.group("component") + leg_text = match.group("leg") or match.group("leg_only") + dummy_text = match.group("dummy") + source_component = ( + None if component_text is None else int(component_text) + ) + source_leg = None if leg_text is None else int(leg_text) + source_dummy = None if dummy_text is None else -int(dummy_text) + _validate_index_source( + term, + origin=origin, + family=family, + source_component=source_component, + source_leg=source_leg, + source_dummy=source_dummy, + dummy_space=match.group("dummy_space"), + particle_by_name=particle_by_name, + ) + space = _binding_space( + family, + source_leg=source_leg, + dummy_space=match.group("dummy_space"), + particles=term.particles, + particle_by_name=particle_by_name, + ) + descriptor = ( + origin, + space, + source_component, + source_leg, + source_dummy, + ) + if descriptor not in seen: + seen.add(descriptor) + descriptors.append(descriptor) + + dummy_ordinals: dict[tuple[str, str, int], int] = {} + next_dummy_by_space: dict[tuple[str, str], int] = {} + bindings: list[TensorIndexBindingIR] = [] + for origin, space, component, leg, dummy in descriptors: + if dummy is None: + normalized_name = f"{origin}:{space}:leg-{leg}" + if component is not None: + normalized_name += f":component-{component}" + else: + key = (origin, space, dummy) + if key not in dummy_ordinals: + space_key = (origin, space) + ordinal = next_dummy_by_space.get(space_key, 0) + next_dummy_by_space[space_key] = ordinal + 1 + dummy_ordinals[key] = ordinal + normalized_name = f"{origin}:{space}:dummy-{dummy_ordinals[key]}" + bindings.append( + TensorIndexBindingIR( + origin=origin, + space=space, + source_component=component, + source_leg=leg, + source_dummy=dummy, + normalized_name=normalized_name, + ) + ) + return tuple(bindings) + + +def compile_tensor_ordering_metadata( + terms: Sequence[CompiledVertexTerm], + particles: Sequence[CompiledParticleRecord], + kernels: Sequence[CompiledOrientedKernel], + parameters: Sequence[CompiledParameterRecord], + propagators: Sequence[CompiledPropagatorRecord], +) -> tuple[ + tuple[CompiledVertexTerm, ...], + tuple[CompiledOrientedKernel, ...], + tuple[TensorOrderingIR, ...], + tuple[CompiledCurrentOrderingRecord, ...], +]: + """Attach and cross-check every external model component-ordering reference.""" + + particle_by_name = {particle.name: particle for particle in particles} + parameters_by_name = {parameter.name: parameter for parameter in parameters} + propagators_by_name = {propagator.name: propagator for propagator in propagators} + contact_orderings = _contact_auxiliary_orderings(terms, particle_by_name) + ordering_registry: dict[str, TensorOrderingIR] = {} + + def register(ordering: TensorOrderingIR) -> TensorOrderingIR: + previous = ordering_registry.setdefault(ordering.ordering_id, ordering) + if previous != ordering: + raise ValueError( + f"tensor ordering digest collision for {ordering.ordering_id!r}" + ) + return previous + + full_ordering_by_particle: dict[str, TensorOrderingIR] = {} + current_orderings: list[CompiledCurrentOrderingRecord] = [] + for particle in particles: + full = register( + contact_orderings.get( + particle.name, + _full_particle_ordering(particle), + ) + ) + full_ordering_by_particle[particle.name] = full + identity = tuple(range(full.stored_size)) + current_orderings.append( + CompiledCurrentOrderingRecord( + particle=particle.name, + chirality=0, + ordering_id=full.ordering_id, + kernel_ordering_id=full.ordering_id, + input_embedding=identity, + result_projection=identity, + ) + ) + if compiled_particle_is_chiral_eligible( + particle, + parameters=parameters_by_name, + propagators=propagators_by_name, + ): + for chirality in (-1, 1): + projected = register( + TensorOrderingIR.identity( + basis=f"weyl-chirality:{chirality:+d}", + axes=( + TensorAxisIR( + name="axis-0", + space="weyl-spinor", + extent=2, + ), + ), + ) + ) + input_embedding = ( + (None, None, 0, 1) + if chirality == -1 + else (0, 1, None, None) + ) + result_projection = (0, 1) if chirality == -1 else (2, 3) + current_orderings.append( + CompiledCurrentOrderingRecord( + particle=particle.name, + chirality=chirality, + ordering_id=projected.ordering_id, + kernel_ordering_id=full.ordering_id, + input_embedding=input_embedding, + result_projection=result_projection, + ) + ) + + annotated_terms: list[CompiledVertexTerm] = [] + for term in terms: + source_ordering_ids = tuple( + full_ordering_by_particle[name].ordering_id for name in term.particles + ) + index_bindings = compile_vertex_index_bindings(term, particle_by_name) + if term.source_ordering_ids and term.source_ordering_ids != source_ordering_ids: + raise ValueError(f"vertex term {term.id} has stale source tensor orderings") + if term.index_bindings and term.index_bindings != index_bindings: + raise ValueError(f"vertex term {term.id} has stale tensor index bindings") + annotated_terms.append( + replace( + term, + source_ordering_ids=source_ordering_ids, + index_bindings=index_bindings, + ) + ) + + annotated_kernels: list[CompiledOrientedKernel] = [] + for kernel in kernels: + input_ordering_ids = ( + full_ordering_by_particle[kernel.particles[0]].ordering_id, + full_ordering_by_particle[kernel.particles[1]].ordering_id, + ) + output_ordering_id = full_ordering_by_particle[ + kernel.particles[2] + ].ordering_id + if ( + kernel.input_ordering_ids + and kernel.input_ordering_ids != input_ordering_ids + ): + raise ValueError(f"oriented kernel {kernel.kind} has stale input orderings") + if ( + kernel.output_ordering_id + and kernel.output_ordering_id != output_ordering_id + ): + raise ValueError(f"oriented kernel {kernel.kind} has stale output ordering") + annotated_kernels.append( + replace( + kernel, + input_ordering_ids=input_ordering_ids, + output_ordering_id=output_ordering_id, + term_ids=kernel.term_ids or (kernel.term_id,), + ) + ) + + return ( + tuple(annotated_terms), + tuple(annotated_kernels), + tuple(sorted(ordering_registry.values(), key=lambda item: item.ordering_id)), + tuple(current_orderings), + ) + + +def _contact_auxiliary_orderings( + terms: Sequence[CompiledVertexTerm], + particle_by_name: Mapping[str, CompiledParticleRecord], +) -> dict[str, TensorOrderingIR]: + result: dict[str, TensorOrderingIR] = {} + for term in terms: + proof = term.contact_decomposition_proof + if proof is None or proof.status != "proven": + continue + for split in proof.splits: + expected_axis_order = tuple( + label + for leg in split.open_legs + for label in _spin_axis_labels( + particle_by_name[term.particles[leg]].spin, + leg + 1, + ) + ) + if split.component_axis_order != expected_axis_order: + raise ValueError( + f"contact term {term.id} result leg {split.result_leg} has " + "non-canonical component axes" + ) + name = f"__pyamplicol_contact_{term.id}_r{split.result_leg}" + spaces = tuple( + _space_from_axis_label(label) for label in split.component_axis_order + ) + ordering = TensorOrderingIR.create( + basis=( + "contact-scalar" + if not spaces + else "contact:" + "x".join(spaces) + ), + axes=tuple( + TensorAxisIR(name=f"axis-{index}", space=space, extent=4) + for index, space in enumerate(spaces) + ), + component_basis=split.component_basis_order, + component_expansion=split.component_expansion, + ) + previous = result.setdefault(name, ordering) + if previous != ordering: + raise ValueError( + f"contact auxiliary {name!r} has conflicting tensor orderings" + ) + return result + + +def _full_particle_ordering(particle: CompiledParticleRecord) -> TensorOrderingIR: + expected_dimension = {-1: 1, 1: 1, 2: 4, 3: 4, 5: 16}.get(particle.spin) + component_dimension = compiled_particle_component_dimension(particle) + if expected_dimension is None or component_dimension != expected_dimension: + basis = "opaque-auxiliary" + if particle.auxiliary_kind: + basis += f":{particle.auxiliary_kind.split(':', 1)[0]}" + return TensorOrderingIR.identity( + basis=basis, + axes=( + TensorAxisIR( + name="axis-0", + space="opaque-component", + extent=component_dimension, + ), + ), + ) + spaces = { + -1: (), + 1: (), + 2: ("bispinor",), + 3: ("lorentz-vector",), + 5: ("lorentz-vector", "lorentz-vector"), + }.get(particle.spin) + if spaces is None: + raise ValueError( + f"particle {particle.name!r} has unsupported tensor spin {particle.spin}" + ) + return TensorOrderingIR.identity( + basis=_basis_for_spaces(spaces), + axes=tuple( + TensorAxisIR(name=f"axis-{index}", space=space, extent=4) + for index, space in enumerate(spaces) + ), + ) + + +def _basis_for_spaces(spaces: Sequence[str]) -> str: + values = tuple(spaces) + if not values: + return "scalar" + if values == ("bispinor",): + return "dirac" + if values == ("lorentz-vector",): + return "lorentz-vector" + if values == ("lorentz-vector", "lorentz-vector"): + return "lorentz-rank-2" + return "tensor-product:" + "x".join(values) + + +def _space_from_axis_label(label: str) -> str: + if re.fullmatch(r"ufo_s_[0-9]+_[0-9]+", label): + return "bispinor" + if re.fullmatch(r"ufo_l_[0-9]+_[0-9]+", label): + return "lorentz-vector" + raise ValueError(f"unsupported physical tensor axis label {label!r}") + + +def _spin_axis_labels(spin: int, leg: int) -> tuple[str, ...]: + if spin in {-1, 1}: + return () + if spin == 2: + return (f"ufo_s_1_{leg}",) + if spin == 3: + return (f"ufo_l_1_{leg}",) + if spin == 5: + return (f"ufo_l_1_{leg}", f"ufo_l_2_{leg}") + raise ValueError(f"unsupported tensor spin {spin} on source leg {leg}") + + +def _validate_index_source( + term: CompiledVertexTerm, + *, + origin: str, + family: str, + source_component: int | None, + source_leg: int | None, + source_dummy: int | None, + dummy_space: str | None, + particle_by_name: Mapping[str, CompiledParticleRecord], +) -> None: + if origin == "color" and family != "c": + raise ValueError( + f"vertex term {term.id} color expression contains a non-color index" + ) + if origin == "lorentz" and family == "c": + raise ValueError( + f"vertex term {term.id} Lorentz expression contains a color index" + ) + if source_dummy is not None: + if family == "c": + if dummy_space not in _COLOR_DUMMY_SPACES: + raise ValueError( + f"vertex term {term.id} has unknown color dummy space " + f"{dummy_space!r}" + ) + elif dummy_space is not None: + raise ValueError( + f"vertex term {term.id} has a typed non-color dummy index" + ) + return + if source_leg is None or source_leg > len(term.particles): + raise ValueError(f"tensor index refers to absent source leg {source_leg}") + particle = particle_by_name[term.particles[source_leg - 1]] + if family == "c": + if source_component is not None: + raise ValueError( + f"vertex term {term.id} color index has an invalid component ordinal" + ) + if particle.color not in {-3, 3, 8}: + raise ValueError( + f"vertex term {term.id} color index refers to singlet particle " + f"{particle.name!r}" + ) + return + expected_family = "s" if particle.spin == 2 else "l" + label = f"ufo_{family}_{source_component}_{source_leg}" + if family != expected_family or label not in _spin_axis_labels( + particle.spin, + source_leg, + ): + raise ValueError( + f"vertex term {term.id} tensor index {label!r} is incompatible with " + f"particle {particle.name!r} spin {particle.spin}" + ) + + +def _binding_space( + family: str, + *, + source_leg: int | None, + dummy_space: str | None, + particles: Sequence[str], + particle_by_name: Mapping[str, CompiledParticleRecord], +) -> str: + if family == "l": + return "lorentz-vector" + if family == "s": + return "bispinor" + if source_leg is None: + return "color-" + (dummy_space or "unknown") + if source_leg < 1 or source_leg > len(particles): + raise ValueError(f"tensor index refers to absent source leg {source_leg}") + particle = particle_by_name[particles[source_leg - 1]] + return { + -3: "color-antifundamental", + 3: "color-fundamental", + 8: "color-adjoint", + }[particle.color] + + +__all__ = [ + "TENSOR_ORDERING_CONTRACT_VERSION", + "OrderedComponents", + "compile_tensor_ordering_metadata", + "compile_vertex_index_bindings", + "identity_ordering_for_materialized_axes", +] diff --git a/pyamplicol/src/pyamplicol/models/contact_decomposition.py b/pyamplicol/src/pyamplicol/models/contact_decomposition.py new file mode 100644 index 0000000000..1a76e4243f --- /dev/null +++ b/pyamplicol/src/pyamplicol/models/contact_decomposition.py @@ -0,0 +1,1074 @@ +# SPDX-License-Identifier: 0BSD +"""Serialized proof records for explicit UFO contact decomposition.""" + +from __future__ import annotations + +from collections.abc import Mapping +from dataclasses import dataclass +from typing import Protocol + +CONTACT_DECOMPOSITION_ALGORITHM = "ufo-four-point-contact-decomposition" +CONTACT_DECOMPOSITION_ALGORITHM_VERSION = 2 +CONTACT_ORBIT_ALGORITHM = "compiler-certified-contact-orbit" +CONTACT_ORBIT_ALGORITHM_VERSION = 2 +CONTACT_ORBIT_EVALUATOR_CLASS = "constant-scalar-literal-singlet-self-conjugate-boson" +HEFT_CONTACT_ORBIT_EVALUATOR_CLASS = "scalar-heft-colored-contact-tree" +CONTACT_ORBIT_EVALUATOR_CLASSES = frozenset( + {CONTACT_ORBIT_EVALUATOR_CLASS, HEFT_CONTACT_ORBIT_EVALUATOR_CLASS} +) + + +def contact_orbit_evaluator_class_is_certifiable( + *, + color_source: str, + color_expression: str, + lorentz_expression: str, + decomposition_kinds: tuple[str, ...], + particle_contracts: tuple[tuple[int, int, str, str, bool | None], ...], +) -> bool: + """Recognize the one compiler-owned evaluator class admitted by the proof.""" + + return ( + len(particle_contracts) == 4 + and color_expression == "1" + and lorentz_expression == "1" + and (color_source in {"1", "UFO::{}::1"} or color_expression == "1") + and bool(decomposition_kinds) + and all(kind == "literal-color-singlet" for kind in decomposition_kinds) + and all( + spin == 1 + and color == 1 + and statistics == "boson" + and wavefunction_family == "scalar" + and self_conjugate is True + for spin, color, statistics, wavefunction_family, self_conjugate in ( + particle_contracts + ) + ) + ) + + +class _ContactTerm(Protocol): + @property + def id(self) -> int: ... + + @property + def vertex(self) -> str: ... + + @property + def particles(self) -> tuple[str, ...]: ... + + @property + def color_index(self) -> int: ... + + @property + def lorentz_index(self) -> int: ... + + @property + def color_source(self) -> str: ... + + @property + def color_expression(self) -> str: ... + + @property + def lorentz_name(self) -> str: ... + + @property + def lorentz_source(self) -> str: ... + + @property + def lorentz_expression(self) -> str: ... + + +@dataclass(frozen=True) +class CompiledContactUnsupportedReason: + code: str + message: str + context: tuple[tuple[str, str], ...] = () + + def __post_init__(self) -> None: + if not self.code or not self.message: + raise ValueError( + "contact decomposition reason must have a code and message" + ) + if self.context != tuple(sorted(self.context)): + raise ValueError("contact decomposition reason context must be sorted") + names = tuple(name for name, _value in self.context) + if any(not name for name in names) or len(names) != len(set(names)): + raise ValueError( + "contact decomposition reason context names must be non-empty and " + "unique" + ) + + def to_dict(self) -> dict[str, object]: + return { + "code": self.code, + "message": self.message, + "context": dict(self.context), + } + + @classmethod + def from_dict( + cls, + payload: Mapping[str, object], + ) -> CompiledContactUnsupportedReason: + fields = _strict_record_fields( + payload, + required={"code", "message", "context"}, + context="compiled contact unsupported reason", + ) + context = _strict_mapping(fields["context"], "contact reason context") + return cls( + code=_strict_string(fields["code"], "contact reason code"), + message=_strict_string(fields["message"], "contact reason message"), + context=tuple( + sorted( + ( + _strict_string(name, "contact reason context name"), + _strict_string(value, "contact reason context value"), + ) + for name, value in context.items() + ) + ), + ) + + +@dataclass(frozen=True) +class CompiledContactDummyIndexMapping: + source_index: int + normalized_symbol: str + outer_slot: int + final_slot: int + + def __post_init__(self) -> None: + if self.source_index >= 0: + raise ValueError("contact dummy source index must be negative") + if not self.normalized_symbol: + raise ValueError("contact dummy normalized symbol must not be empty") + if self.outer_slot not in range(3) or self.final_slot not in range(3): + raise ValueError("contact dummy slots must address a structure constant") + + def to_dict(self) -> dict[str, object]: + return { + "source_index": self.source_index, + "normalized_symbol": self.normalized_symbol, + "outer_slot": self.outer_slot, + "final_slot": self.final_slot, + } + + @classmethod + def from_dict( + cls, + payload: Mapping[str, object], + ) -> CompiledContactDummyIndexMapping: + fields = _strict_record_fields( + payload, + required={"source_index", "normalized_symbol", "outer_slot", "final_slot"}, + context="compiled contact dummy-index mapping", + ) + return cls( + source_index=_strict_integer( + fields["source_index"], "contact dummy source index" + ), + normalized_symbol=_strict_string( + fields["normalized_symbol"], "contact dummy normalized symbol" + ), + outer_slot=_strict_integer( + fields["outer_slot"], "contact dummy outer slot" + ), + final_slot=_strict_integer( + fields["final_slot"], "contact dummy final slot" + ), + ) + + +@dataclass(frozen=True) +class CompiledContactOrientationProof: + stage: str + input_legs: tuple[int, int] + permutation_parity: int + scalar_prefactor: str + + def __post_init__(self) -> None: + if self.stage not in {"partial", "final"}: + raise ValueError(f"unknown contact proof orientation stage {self.stage!r}") + if self.permutation_parity not in {-1, 1}: + raise ValueError("contact orientation parity must be -1 or 1") + if not self.scalar_prefactor: + raise ValueError("contact orientation scalar prefactor must not be empty") + auxiliary_count = self.input_legs.count(-1) + if self.stage == "partial" and ( + auxiliary_count or any(leg < 0 for leg in self.input_legs) + ): + raise ValueError("contact partial orientation requires two physical legs") + if self.stage == "final" and auxiliary_count != 1: + raise ValueError("contact final orientation requires one auxiliary leg") + + def to_dict(self) -> dict[str, object]: + return { + "stage": self.stage, + "input_legs": list(self.input_legs), + "permutation_parity": self.permutation_parity, + "scalar_prefactor": self.scalar_prefactor, + } + + @classmethod + def from_dict( + cls, + payload: Mapping[str, object], + ) -> CompiledContactOrientationProof: + fields = _strict_record_fields( + payload, + required={ + "stage", + "input_legs", + "permutation_parity", + "scalar_prefactor", + }, + context="compiled contact orientation proof", + ) + return cls( + stage=_strict_string(fields["stage"], "contact orientation stage"), + input_legs=_strict_int_pair( + fields["input_legs"], "contact orientation input legs" + ), + permutation_parity=_strict_integer( + fields["permutation_parity"], "contact orientation parity" + ), + scalar_prefactor=_strict_string( + fields["scalar_prefactor"], "contact orientation scalar prefactor" + ), + ) + + +@dataclass(frozen=True) +class CompiledContactDecompositionSplit: + decomposition_kind: str + result_leg: int + pair_legs: tuple[int, int] + remaining_leg: int + outer_color_source: str + final_color_source: str + outer_color_factor: tuple[int, ...] + final_color_factor: tuple[int, ...] + dummy_index_mapping: CompiledContactDummyIndexMapping | None + outer_color_normalization_power: int + final_color_normalization_power: int + color_coefficient: str + auxiliary_color: int + open_legs: tuple[int, int] + component_axis_order: tuple[str, ...] + component_basis_order: tuple[int, ...] + component_expansion: tuple[tuple[int, int] | None, ...] + assignment_multiplicity: int + canonical_outer_parity: int + orientations: tuple[CompiledContactOrientationProof, ...] + + def __post_init__(self) -> None: + if self.decomposition_kind not in { + "literal-color-singlet", + "two-structure-constants", + }: + raise ValueError( + f"unknown contact decomposition kind {self.decomposition_kind!r}" + ) + physical_legs = (*self.pair_legs, self.remaining_leg, self.result_leg) + if any(leg < 0 for leg in physical_legs) or len(set(physical_legs)) != 4: + raise ValueError("contact decomposition must partition four physical legs") + if self.open_legs != tuple(sorted((self.remaining_leg, self.result_leg))): + raise ValueError("contact decomposition open-leg order is not canonical") + if self.assignment_multiplicity <= 0: + raise ValueError("contact assignment multiplicity must be positive") + if ( + self.outer_color_normalization_power < 0 + or self.final_color_normalization_power < 0 + ): + raise ValueError("contact color normalization powers must be non-negative") + if self.canonical_outer_parity not in {-1, 1}: + raise ValueError("contact canonical outer parity must be -1 or 1") + if not self.color_coefficient: + raise ValueError("contact color coefficient must not be empty") + _validate_auxiliary_color(self.auxiliary_color) + if not self.component_basis_order or not self.component_expansion: + raise ValueError("contact component basis and expansion must not be empty") + if len(set(self.component_basis_order)) != len(self.component_basis_order): + raise ValueError( + "contact component basis order must not contain duplicates" + ) + if any( + source < 0 or source >= len(self.component_expansion) + for source in self.component_basis_order + ): + raise ValueError("contact component basis source is outside the expansion") + for entry in self.component_expansion: + if entry is None: + continue + basis_index, coefficient = entry + if basis_index not in range(len(self.component_basis_order)): + raise ValueError( + "contact component expansion has an absent basis index" + ) + if coefficient not in {-1, 1}: + raise ValueError("contact component expansion coefficient must be +/-1") + for basis_index, source_component in enumerate(self.component_basis_order): + if self.component_expansion[source_component] != (basis_index, 1): + raise ValueError( + "contact component basis order disagrees with its expansion" + ) + partials = tuple(item for item in self.orientations if item.stage == "partial") + finals = tuple(item for item in self.orientations if item.stage == "final") + if not partials or not finals: + raise ValueError( + "contact decomposition requires partial and final orientations" + ) + allowed_partial_orders = {self.pair_legs, tuple(reversed(self.pair_legs))} + if partials[0].input_legs != self.pair_legs or any( + item.input_legs not in allowed_partial_orders for item in partials + ): + raise ValueError( + "contact partial orientations disagree with the chosen pair" + ) + if any( + tuple(leg for leg in item.input_legs if leg >= 0) != (self.remaining_leg,) + for item in finals + ): + raise ValueError( + "contact final orientations disagree with the remaining leg" + ) + if self.decomposition_kind == "two-structure-constants": + if ( + len(self.outer_color_factor) != 3 + or len(self.final_color_factor) != 3 + or self.dummy_index_mapping is None + ): + raise ValueError( + "two-structure-constant contact proof requires two factors and a " + "dummy mapping" + ) + mapping = self.dummy_index_mapping + if ( + self.outer_color_factor[mapping.outer_slot] != mapping.source_index + or self.final_color_factor[mapping.final_slot] != mapping.source_index + ): + raise ValueError( + "contact dummy-index mapping disagrees with the chosen factors" + ) + elif ( + self.outer_color_factor + or self.final_color_factor + or self.dummy_index_mapping is not None + ): + raise ValueError( + "literal singlet contact proof must not contain color factors" + ) + + def to_dict(self) -> dict[str, object]: + return { + "decomposition_kind": self.decomposition_kind, + "result_leg": self.result_leg, + "pair_legs": list(self.pair_legs), + "remaining_leg": self.remaining_leg, + "outer_color_source": self.outer_color_source, + "final_color_source": self.final_color_source, + "outer_color_factor": list(self.outer_color_factor), + "final_color_factor": list(self.final_color_factor), + "dummy_index_mapping": ( + None + if self.dummy_index_mapping is None + else self.dummy_index_mapping.to_dict() + ), + "outer_color_normalization_power": self.outer_color_normalization_power, + "final_color_normalization_power": self.final_color_normalization_power, + "color_coefficient": self.color_coefficient, + "auxiliary_color": self.auxiliary_color, + "open_legs": list(self.open_legs), + "component_axis_order": list(self.component_axis_order), + "component_basis_order": list(self.component_basis_order), + "component_expansion": [ + None if entry is None else list(entry) + for entry in self.component_expansion + ], + "assignment_multiplicity": self.assignment_multiplicity, + "canonical_outer_parity": self.canonical_outer_parity, + "orientations": [item.to_dict() for item in self.orientations], + } + + @classmethod + def from_dict( + cls, + payload: Mapping[str, object], + ) -> CompiledContactDecompositionSplit: + fields = _strict_record_fields( + payload, + required={ + "decomposition_kind", + "result_leg", + "pair_legs", + "remaining_leg", + "outer_color_source", + "final_color_source", + "outer_color_factor", + "final_color_factor", + "dummy_index_mapping", + "outer_color_normalization_power", + "final_color_normalization_power", + "color_coefficient", + "auxiliary_color", + "open_legs", + "component_axis_order", + "component_basis_order", + "component_expansion", + "assignment_multiplicity", + "canonical_outer_parity", + "orientations", + }, + context="compiled contact decomposition split", + ) + dummy_payload = fields["dummy_index_mapping"] + return cls( + decomposition_kind=_strict_string( + fields["decomposition_kind"], "contact decomposition kind" + ), + result_leg=_strict_integer(fields["result_leg"], "contact result leg"), + pair_legs=_strict_int_pair(fields["pair_legs"], "contact pair legs"), + remaining_leg=_strict_integer( + fields["remaining_leg"], "contact remaining leg" + ), + outer_color_source=_strict_string( + fields["outer_color_source"], "contact outer color source" + ), + final_color_source=_strict_string( + fields["final_color_source"], "contact final color source" + ), + outer_color_factor=_strict_int_tuple( + fields["outer_color_factor"], "contact outer color factor" + ), + final_color_factor=_strict_int_tuple( + fields["final_color_factor"], "contact final color factor" + ), + dummy_index_mapping=( + None + if dummy_payload is None + else CompiledContactDummyIndexMapping.from_dict( + _strict_mapping(dummy_payload, "contact dummy-index mapping") + ) + ), + outer_color_normalization_power=_strict_integer( + fields["outer_color_normalization_power"], + "contact outer color normalization power", + ), + final_color_normalization_power=_strict_integer( + fields["final_color_normalization_power"], + "contact final color normalization power", + ), + color_coefficient=_strict_string( + fields["color_coefficient"], "contact color coefficient" + ), + auxiliary_color=_strict_integer( + fields["auxiliary_color"], "contact auxiliary color" + ), + open_legs=_strict_int_pair(fields["open_legs"], "contact open legs"), + component_axis_order=_strict_string_tuple( + fields["component_axis_order"], "contact component axis order" + ), + component_basis_order=_strict_int_tuple( + fields["component_basis_order"], "contact component basis order" + ), + component_expansion=_strict_optional_int_pairs( + fields["component_expansion"], "contact component expansion" + ), + assignment_multiplicity=_strict_integer( + fields["assignment_multiplicity"], "contact assignment multiplicity" + ), + canonical_outer_parity=_strict_integer( + fields["canonical_outer_parity"], "contact canonical outer parity" + ), + orientations=tuple( + CompiledContactOrientationProof.from_dict(item) + for item in _strict_mappings( + fields["orientations"], "contact decomposition orientations" + ) + ), + ) + + +@dataclass(frozen=True) +class CompiledContactDecompositionProof: + status: str + algorithm: str + algorithm_version: int + term_id: int + vertex: str + particles: tuple[str, ...] + color_index: int + lorentz_index: int + original_color_source: str + normalized_color_expression: str + lorentz_name: str + original_lorentz_source: str + normalized_lorentz_expression: str + splits: tuple[CompiledContactDecompositionSplit, ...] = () + unsupported_reasons: tuple[CompiledContactUnsupportedReason, ...] = () + + def __post_init__(self) -> None: + if self.status not in {"proven", "unsupported"}: + raise ValueError( + f"unknown contact decomposition proof status {self.status!r}" + ) + if ( + self.algorithm != CONTACT_DECOMPOSITION_ALGORITHM + or self.algorithm_version != CONTACT_DECOMPOSITION_ALGORITHM_VERSION + ): + raise ValueError( + "unsupported contact decomposition proof algorithm: " + f"{self.algorithm}/v{self.algorithm_version}" + ) + if not self.vertex or not self.particles: + raise ValueError("contact decomposition proof requires a term identity") + if len(self.particles) != 4: + raise ValueError("contact decomposition proof requires four particles") + result_legs = tuple(split.result_leg for split in self.splits) + if len(result_legs) != len(set(result_legs)): + raise ValueError("contact decomposition proof has duplicate result legs") + if self.status == "proven" and (not self.splits or self.unsupported_reasons): + raise ValueError("proven contact decomposition must contain only splits") + if self.status == "unsupported" and ( + self.splits or not self.unsupported_reasons + ): + raise ValueError( + "unsupported contact decomposition must contain only reasons" + ) + + def matches(self, term: _ContactTerm) -> bool: + return ( + self.term_id == term.id + and self.vertex == term.vertex + and self.particles == term.particles + and self.color_index == term.color_index + and self.lorentz_index == term.lorentz_index + and self.original_color_source == term.color_source + and self.normalized_color_expression == term.color_expression + and self.lorentz_name == term.lorentz_name + and self.original_lorentz_source == term.lorentz_source + and self.normalized_lorentz_expression == term.lorentz_expression + ) + + def to_dict(self) -> dict[str, object]: + return { + "status": self.status, + "algorithm": self.algorithm, + "algorithm_version": self.algorithm_version, + "term_identity": { + "term_id": self.term_id, + "vertex": self.vertex, + "particles": list(self.particles), + "color_index": self.color_index, + "lorentz_index": self.lorentz_index, + "original_color_source": self.original_color_source, + "normalized_color_expression": self.normalized_color_expression, + "lorentz_name": self.lorentz_name, + "original_lorentz_source": self.original_lorentz_source, + "normalized_lorentz_expression": self.normalized_lorentz_expression, + }, + "splits": [split.to_dict() for split in self.splits], + "unsupported_reasons": [ + reason.to_dict() for reason in self.unsupported_reasons + ], + } + + @classmethod + def from_dict( + cls, + payload: Mapping[str, object], + ) -> CompiledContactDecompositionProof: + fields = _strict_record_fields( + payload, + required={ + "status", + "algorithm", + "algorithm_version", + "term_identity", + "splits", + "unsupported_reasons", + }, + context="compiled contact decomposition proof", + ) + identity = _strict_record_fields( + _strict_mapping(fields["term_identity"], "contact term identity"), + required={ + "term_id", + "vertex", + "particles", + "color_index", + "lorentz_index", + "original_color_source", + "normalized_color_expression", + "lorentz_name", + "original_lorentz_source", + "normalized_lorentz_expression", + }, + context="compiled contact term identity", + ) + return cls( + status=_strict_string(fields["status"], "contact proof status"), + algorithm=_strict_string(fields["algorithm"], "contact proof algorithm"), + algorithm_version=_strict_integer( + fields["algorithm_version"], "contact proof algorithm version" + ), + term_id=_strict_integer(identity["term_id"], "contact proof term id"), + vertex=_strict_string(identity["vertex"], "contact proof vertex"), + particles=_strict_string_tuple( + identity["particles"], "contact proof particles" + ), + color_index=_strict_integer( + identity["color_index"], "contact proof color index" + ), + lorentz_index=_strict_integer( + identity["lorentz_index"], "contact proof Lorentz index" + ), + original_color_source=_strict_string( + identity["original_color_source"], "contact proof color source" + ), + normalized_color_expression=_strict_string( + identity["normalized_color_expression"], + "contact proof normalized color expression", + ), + lorentz_name=_strict_string( + identity["lorentz_name"], "contact proof Lorentz name" + ), + original_lorentz_source=_strict_string( + identity["original_lorentz_source"], "contact proof Lorentz source" + ), + normalized_lorentz_expression=_strict_string( + identity["normalized_lorentz_expression"], + "contact proof normalized Lorentz expression", + ), + splits=tuple( + CompiledContactDecompositionSplit.from_dict(item) + for item in _strict_mappings(fields["splits"], "contact proof splits") + ), + unsupported_reasons=tuple( + CompiledContactUnsupportedReason.from_dict(item) + for item in _strict_mappings( + fields["unsupported_reasons"], + "contact proof unsupported reasons", + ) + ), + ) + + +@dataclass(frozen=True, order=True, slots=True) +class CompiledContactOrbitCertificate: + """Compiler proof that contact paths describe one physical assignment orbit.""" + + algorithm: str + algorithm_version: int + term_id: int + vertex: str + particles: tuple[str, ...] + color_expression: str + lorentz_expression: str + coupling_expression: str + evaluator_class: str + physical_leg_equivalence_classes: tuple[int, ...] + reconstruction_factor: str + + def __post_init__(self) -> None: + if ( + self.algorithm != CONTACT_ORBIT_ALGORITHM + or self.algorithm_version != CONTACT_ORBIT_ALGORITHM_VERSION + ): + raise ValueError( + "unsupported contact-orbit certificate algorithm: " + f"{self.algorithm}/v{self.algorithm_version}" + ) + if self.term_id < 0 or not self.vertex: + raise ValueError("contact-orbit certificate requires a term identity") + if len(self.particles) not in {4, 5} or any( + not particle for particle in self.particles + ): + raise ValueError("contact-orbit certificate particles must be non-empty") + if self.evaluator_class not in CONTACT_ORBIT_EVALUATOR_CLASSES: + raise ValueError( + f"unsupported contact-orbit evaluator class {self.evaluator_class!r}" + ) + if self.evaluator_class == CONTACT_ORBIT_EVALUATOR_CLASS and ( + len(self.particles) != 4 + or self.color_expression != "1" + or self.lorentz_expression != "1" + ): + raise ValueError( + "contact-orbit certificate requires literal scalar-singlet tensors" + ) + if self.evaluator_class == HEFT_CONTACT_ORBIT_EVALUATOR_CLASS and ( + not self.color_expression or not self.lorentz_expression + ): + raise ValueError("HEFT contact-orbit tensors must be non-empty") + if not self.coupling_expression: + raise ValueError( + "contact-orbit certificate coupling expression must not be empty" + ) + classes = self.physical_leg_equivalence_classes + if len(classes) != len(self.particles) or any(value < 0 for value in classes): + raise ValueError("contact-orbit equivalence classes must be non-negative") + canonical: dict[str, int] = {} + expected = tuple( + canonical.setdefault(particle, len(canonical)) + for particle in self.particles + ) + if classes != expected: + raise ValueError( + "contact-orbit physical-leg equivalence classes are not canonical" + ) + if self.reconstruction_factor != "1": + raise ValueError("contact-orbit reconstruction factor must be one") + + def to_dict(self) -> dict[str, object]: + return { + "algorithm": self.algorithm, + "algorithm_version": self.algorithm_version, + "term_id": self.term_id, + "vertex": self.vertex, + "particles": list(self.particles), + "color_expression": self.color_expression, + "lorentz_expression": self.lorentz_expression, + "coupling_expression": self.coupling_expression, + "evaluator_class": self.evaluator_class, + "physical_leg_equivalence_classes": list( + self.physical_leg_equivalence_classes + ), + "reconstruction_factor": self.reconstruction_factor, + } + + @classmethod + def from_dict( + cls, + payload: Mapping[str, object], + ) -> CompiledContactOrbitCertificate: + fields = _strict_record_fields( + payload, + required={ + "algorithm", + "algorithm_version", + "term_id", + "vertex", + "particles", + "color_expression", + "lorentz_expression", + "coupling_expression", + "evaluator_class", + "physical_leg_equivalence_classes", + "reconstruction_factor", + }, + context="compiled contact-orbit certificate", + ) + particles = _strict_string_tuple(fields["particles"], "contact-orbit particles") + classes = _strict_int_tuple( + fields["physical_leg_equivalence_classes"], + "contact-orbit physical-leg equivalence classes", + ) + if len(particles) not in {4, 5} or len(classes) != len(particles): + raise ValueError( + "contact-orbit certificate must describe four or five legs" + ) + return cls( + algorithm=_strict_string(fields["algorithm"], "contact-orbit algorithm"), + algorithm_version=_strict_integer( + fields["algorithm_version"], "contact-orbit algorithm version" + ), + term_id=_strict_integer(fields["term_id"], "contact-orbit term ID"), + vertex=_strict_string(fields["vertex"], "contact-orbit vertex"), + particles=particles, + color_expression=_strict_string( + fields["color_expression"], "contact-orbit color expression" + ), + lorentz_expression=_strict_string( + fields["lorentz_expression"], "contact-orbit Lorentz expression" + ), + coupling_expression=_strict_string( + fields["coupling_expression"], "contact-orbit coupling expression" + ), + evaluator_class=_strict_string( + fields["evaluator_class"], "contact-orbit evaluator class" + ), + physical_leg_equivalence_classes=classes, + reconstruction_factor=_strict_string( + fields["reconstruction_factor"], + "contact-orbit reconstruction factor", + ), + ) + + +@dataclass(frozen=True, order=True, slots=True) +class CompiledContactOrbitStep: + """Exact physical-leg lineage for one compiler-lowered contact kernel.""" + + algorithm: str + algorithm_version: int + term_id: int + stage: str + result_leg: int + left_covered_legs: tuple[int, ...] + right_covered_legs: tuple[int, ...] + source_particle_legs: tuple[int, int, int] + reconstruction_factor: str + + def __post_init__(self) -> None: + if ( + self.algorithm != CONTACT_ORBIT_ALGORITHM + or self.algorithm_version != CONTACT_ORBIT_ALGORITHM_VERSION + ): + raise ValueError( + "unsupported contact-orbit step algorithm: " + f"{self.algorithm}/v{self.algorithm_version}" + ) + if self.term_id < 0 or self.stage not in {"partial", "final"}: + raise ValueError("contact-orbit step has an invalid term or stage") + if self.result_leg not in range(5): + raise ValueError("contact-orbit result leg must address supported input") + if self.reconstruction_factor != "1": + raise ValueError("contact-orbit step reconstruction factor must be one") + covered = (*self.left_covered_legs, *self.right_covered_legs) + if ( + not self.left_covered_legs + or not self.right_covered_legs + or any(leg not in range(5) for leg in covered) + or len(covered) != len(set(covered)) + ): + raise ValueError("contact-orbit step has invalid covered physical legs") + left_source, right_source, output_source = self.source_particle_legs + if self.stage == "partial": + if ( + len(self.left_covered_legs) != 1 + or len(self.right_covered_legs) != 1 + or (left_source, right_source) + != (self.left_covered_legs[0], self.right_covered_legs[0]) + or output_source != -1 + or self.result_leg in covered + ): + raise ValueError("contact-orbit partial step lineage is inconsistent") + else: + arity = len(covered) + 1 + if ( + arity not in {4, 5} + or set(covered) != (set(range(arity)) - {self.result_leg}) + or output_source != self.result_leg + or any( + len(side) not in {1, 2} + for side in (self.left_covered_legs, self.right_covered_legs) + ) + ): + raise ValueError("contact-orbit final step lineage is inconsistent") + expected_sources = tuple( + -1 if len(side) > 1 else side[0] + for side in (self.left_covered_legs, self.right_covered_legs) + ) + if (left_source, right_source) != expected_sources: + raise ValueError("contact-orbit final source lineage is inconsistent") + + def to_dict(self) -> dict[str, object]: + return { + "algorithm": self.algorithm, + "algorithm_version": self.algorithm_version, + "term_id": self.term_id, + "stage": self.stage, + "result_leg": self.result_leg, + "left_covered_legs": list(self.left_covered_legs), + "right_covered_legs": list(self.right_covered_legs), + "source_particle_legs": list(self.source_particle_legs), + "reconstruction_factor": self.reconstruction_factor, + } + + @classmethod + def from_dict(cls, payload: Mapping[str, object]) -> CompiledContactOrbitStep: + fields = _strict_record_fields( + payload, + required={ + "algorithm", + "algorithm_version", + "term_id", + "stage", + "result_leg", + "left_covered_legs", + "right_covered_legs", + "source_particle_legs", + "reconstruction_factor", + }, + context="compiled contact-orbit step", + ) + source_legs = _strict_int_tuple( + fields["source_particle_legs"], "contact-orbit step source legs" + ) + if len(source_legs) != 3: + raise ValueError("contact-orbit step must describe three kernel slots") + return cls( + algorithm=_strict_string(fields["algorithm"], "contact-orbit algorithm"), + algorithm_version=_strict_integer( + fields["algorithm_version"], "contact-orbit algorithm version" + ), + term_id=_strict_integer(fields["term_id"], "contact-orbit term ID"), + stage=_strict_string(fields["stage"], "contact-orbit stage"), + result_leg=_strict_integer( + fields["result_leg"], "contact-orbit result leg" + ), + left_covered_legs=_strict_int_tuple( + fields["left_covered_legs"], "contact-orbit left covered legs" + ), + right_covered_legs=_strict_int_tuple( + fields["right_covered_legs"], "contact-orbit right covered legs" + ), + source_particle_legs=(source_legs[0], source_legs[1], source_legs[2]), + reconstruction_factor=_strict_string( + fields["reconstruction_factor"], + "contact-orbit reconstruction factor", + ), + ) + + +def canonical_contact_orbit_steps( + *groups: tuple[CompiledContactOrbitStep, ...], +) -> tuple[CompiledContactOrbitStep, ...]: + """Return the deterministic union carried by one retained compiler kernel.""" + + return tuple(sorted({step for group in groups for step in group})) + + +def compiled_contact_orbit_step( + certificate: CompiledContactOrbitCertificate, + split: CompiledContactDecompositionSplit, + orientation: CompiledContactOrientationProof, +) -> CompiledContactOrbitStep: + """Derive the exact compiler lineage named by one proven orientation.""" + + if orientation.stage == "partial": + left_covered = (orientation.input_legs[0],) + right_covered = (orientation.input_legs[1],) + source_particle_legs = ( + orientation.input_legs[0], + orientation.input_legs[1], + -1, + ) + else: + auxiliary_on_left = orientation.input_legs[0] == -1 + pair_legs = tuple(sorted(split.pair_legs)) + remaining = (split.remaining_leg,) + left_covered, right_covered = ( + (pair_legs, remaining) if auxiliary_on_left else (remaining, pair_legs) + ) + source_particle_legs = ( + -1 if auxiliary_on_left else split.remaining_leg, + split.remaining_leg if auxiliary_on_left else -1, + split.result_leg, + ) + return CompiledContactOrbitStep( + algorithm=CONTACT_ORBIT_ALGORITHM, + algorithm_version=CONTACT_ORBIT_ALGORITHM_VERSION, + term_id=certificate.term_id, + stage=orientation.stage, + result_leg=split.result_leg, + left_covered_legs=left_covered, + right_covered_legs=right_covered, + source_particle_legs=source_particle_legs, + reconstruction_factor=certificate.reconstruction_factor, + ) + + +def _validate_auxiliary_color(value: int) -> None: + if value not in {-3, 1, 3, 8}: + raise ValueError( + f"contact auxiliary uses unsupported UFO color representation {value}" + ) + + +def _strict_record_fields( + payload: Mapping[str, object], + *, + required: set[str], + context: str, +) -> Mapping[str, object]: + fields = set(payload) + missing = required - fields + if missing: + names = ", ".join(sorted(missing)) + raise ValueError(f"{context} is missing required fields: {names}") + unknown = fields - required + if unknown: + names = ", ".join(sorted(str(name) for name in unknown)) + raise ValueError(f"{context} has unknown fields: {names}") + return payload + + +def _strict_mapping(value: object, context: str) -> Mapping[str, object]: + if not isinstance(value, Mapping): + raise TypeError(f"{context} must be a mapping") + return value + + +def _strict_string(value: object, context: str) -> str: + if not isinstance(value, str): + raise TypeError(f"{context} must be a string") + return value + + +def _strict_integer(value: object, context: str) -> int: + if isinstance(value, bool) or not isinstance(value, int): + raise TypeError(f"{context} must be an integer") + return value + + +def _strict_values(value: object, context: str) -> tuple[object, ...]: + if isinstance(value, (str, bytes)) or not isinstance(value, list | tuple): + raise TypeError(f"{context} must be an array") + return tuple(value) + + +def _strict_int_tuple(value: object, context: str) -> tuple[int, ...]: + return tuple( + _strict_integer(item, f"{context} entry") + for item in _strict_values(value, context) + ) + + +def _strict_int_pair(value: object, context: str) -> tuple[int, int]: + result = _strict_int_tuple(value, context) + if len(result) != 2: + raise ValueError(f"{context} must contain two entries") + return result[0], result[1] + + +def _strict_string_tuple(value: object, context: str) -> tuple[str, ...]: + return tuple( + _strict_string(item, f"{context} entry") + for item in _strict_values(value, context) + ) + + +def _strict_optional_int_pairs( + value: object, + context: str, +) -> tuple[tuple[int, int] | None, ...]: + return tuple( + None if item is None else _strict_int_pair(item, f"{context} entry") + for item in _strict_values(value, context) + ) + + +def _strict_mappings( + value: object, + context: str, +) -> tuple[Mapping[str, object], ...]: + return tuple( + _strict_mapping(item, f"{context} entry") + for item in _strict_values(value, context) + ) + + +__all__ = [ + "CONTACT_DECOMPOSITION_ALGORITHM", + "CONTACT_DECOMPOSITION_ALGORITHM_VERSION", + "CompiledContactDecompositionProof", + "CompiledContactDecompositionSplit", + "CompiledContactDummyIndexMapping", + "CompiledContactOrientationProof", + "CompiledContactUnsupportedReason", +] diff --git a/pyamplicol/src/pyamplicol/models/contracts.py b/pyamplicol/src/pyamplicol/models/contracts.py new file mode 100644 index 0000000000..208623f691 --- /dev/null +++ b/pyamplicol/src/pyamplicol/models/contracts.py @@ -0,0 +1,2355 @@ +# SPDX-License-Identifier: 0BSD +from __future__ import annotations + +import math +from collections.abc import Mapping +from dataclasses import dataclass +from typing import Any, cast + +from .._internal.physics.symbols import symbols +from ._physics_ir import ( + TENSOR_ORDERING_CONTRACT_VERSION, + CompiledCurrentOrderingRecord, + ContractionIR, + TensorAxisIR, + TensorIndexBindingIR, + TensorOrderingIR, +) +from .base import QuantumNumberFlow +from .contact_decomposition import ( + CONTACT_ORBIT_EVALUATOR_CLASS, + HEFT_CONTACT_ORBIT_EVALUATOR_CLASS, + CompiledContactDecompositionProof, + CompiledContactDecompositionSplit, + CompiledContactDummyIndexMapping, + CompiledContactOrbitCertificate, + CompiledContactOrbitStep, + CompiledContactOrientationProof, + CompiledContactUnsupportedReason, + compiled_contact_orbit_step, + contact_orbit_evaluator_class_is_certifiable, +) + +PROPAGATOR_SOURCE_FIELD = "pyamplicol_source" +DEFAULT_FEYNMAN_PROPAGATOR_SOURCE = "default-feynman" +MODEL_SUPPLIED_PROPAGATOR_SOURCE = "model-supplied" + +SUPPORTED_COLOR_REPRESENTATIONS = frozenset({-3, 1, 3, 8}) + +_CANONICAL_COMPONENT_DIMENSIONS = {-1: 1, 1: 1, 2: 4, 3: 4, 5: 16} + + +def validate_color_representation(value: int, *, context: str = "particle") -> int: + representation = int(value) + if representation not in SUPPORTED_COLOR_REPRESENTATIONS: + raise ValueError( + f"{context} uses unsupported UFO color representation {representation}" + ) + return representation + + +def validate_quantum_number_flow( + value: object, + *, + context: str = "particle", +) -> QuantumNumberFlow: + if not isinstance(value, list | tuple): + raise ValueError(f"{context} quantum numbers must be a sequence") + result: list[tuple[str, str]] = [] + for item in value: + if not isinstance(item, list | tuple) or len(item) != 2: + raise ValueError( + f"{context} quantum-number entries must be [name, expression] pairs" + ) + name, expression = item + if not isinstance(name, str) or not name: + raise ValueError( + f"{context} quantum-number names must be non-empty strings" + ) + if not isinstance(expression, str) or not expression: + raise ValueError( + f"{context} quantum-number expressions must be non-empty strings" + ) + _constant_quantum_number_expression( + expression, + context=f"{context} quantum number {name!r}", + ) + result.append((name, expression)) + + names = tuple(name for name, _expression in result) + if names != tuple(sorted(set(names))): + raise ValueError(f"{context} quantum-number names must be sorted and unique") + return tuple(result) + + +def _constant_quantum_number_expression( + expression: str, + *, + context: str, +) -> Any: + from . import compiler_symbolica as _sym + + _sym._ensure_symbolica() + try: + parsed = _sym.E(expression) + except Exception as exc: + raise ValueError(f"{context} is not a valid Symbolica expression") from exc + # Symbolica may expose built-in constants such as pi or the Euler constant + # through get_all_symbols(), so use its semantic constant predicate rather + # than treating every reported symbol as a free model variable. + if not parsed.is_constant(): + raise ValueError(f"{context} must be symbol-free") + if not parsed.is_real(): + raise ValueError(f"{context} must be real") + if not parsed.is_finite(): + raise ValueError(f"{context} must be a finite real constant") + return parsed + + +@dataclass(frozen=True) +class CompiledCouplingOrder: + name: str + expansion_order: int + hierarchy: int + + def to_dict(self) -> dict[str, object]: + return { + "name": self.name, + "expansion_order": self.expansion_order, + "hierarchy": self.hierarchy, + } + + +@dataclass(frozen=True) +class CompiledParameterRecord: + name: str + nature: str + parameter_type: str + value: tuple[float, float] | None + expression: str | None + resolved_expression: str + lhablock: str | None + lhacode: tuple[int, ...] + + def to_dict(self) -> dict[str, object]: + return { + "name": self.name, + "nature": self.nature, + "parameter_type": self.parameter_type, + "value": None if self.value is None else list(self.value), + "expression": self.expression, + "resolved_expression": self.resolved_expression, + "lhablock": self.lhablock, + "lhacode": list(self.lhacode), + } + + +@dataclass(frozen=True) +class CompiledParticleRecord: + name: str + antiname: str + pdg_code: int + spin: int + color: int + mass: str + width: str + charge: float + quantum_numbers: QuantumNumberFlow + ghost_number: int + propagating: bool + goldstoneboson: bool + propagator: str | None + component_dimension: int | None = None + auxiliary_kind: str | None = None + statistics: str = "" + wavefunction_family: str = "" + color_role: str = "" + self_conjugate: bool | None = None + source_orientation: str = "" + + def __post_init__(self) -> None: + if self.component_dimension is not None: + if isinstance(self.component_dimension, bool) or not isinstance( + self.component_dimension, int + ): + raise TypeError( + f"particle {self.name!r} component dimension must be an integer" + ) + if self.component_dimension <= 0: + raise ValueError( + f"particle {self.name!r} component dimension must be positive" + ) + if not math.isfinite(float(self.charge)): + raise ValueError(f"particle {self.name!r} charge must be finite") + quantum_numbers = validate_quantum_number_flow( + self.quantum_numbers, + context=f"particle {self.name!r}", + ) + if not any(name == "electric_charge" for name, _ in quantum_numbers): + raise ValueError( + f"particle {self.name!r} must declare exact electric_charge metadata" + ) + object.__setattr__( + self, + "quantum_numbers", + quantum_numbers, + ) + derived = _particle_role_metadata(self) + for field_name, expected in derived.items(): + supplied = getattr(self, field_name) + if supplied in {"", None}: + object.__setattr__(self, field_name, expected) + elif supplied != expected: + raise ValueError( + f"particle {self.name!r} has inconsistent {field_name}: " + f"{supplied!r}, expected {expected!r}" + ) + + def to_dict(self) -> dict[str, object]: + return { + "name": self.name, + "antiname": self.antiname, + "pdg_code": self.pdg_code, + "spin": self.spin, + "color": self.color, + "mass": self.mass, + "width": self.width, + "charge": self.charge, + "quantum_numbers": [list(item) for item in self.quantum_numbers], + "ghost_number": self.ghost_number, + "propagating": self.propagating, + "goldstoneboson": self.goldstoneboson, + "propagator": self.propagator, + "component_dimension": self.component_dimension, + "auxiliary_kind": self.auxiliary_kind, + "statistics": self.statistics, + "wavefunction_family": self.wavefunction_family, + "color_role": self.color_role, + "self_conjugate": self.self_conjugate, + "source_orientation": self.source_orientation, + } + + +def compiled_particle_component_dimension( + particle: CompiledParticleRecord, +) -> int: + if particle.component_dimension is not None: + return particle.component_dimension + try: + return _CANONICAL_COMPONENT_DIMENSIONS[particle.spin] + except KeyError as exc: + raise ValueError( + f"particle {particle.name!r} has unsupported UFO spin code {particle.spin}" + ) from exc + + +def _particle_role_metadata(particle: CompiledParticleRecord) -> dict[str, object]: + representation = validate_color_representation( + particle.color, + context=f"particle {particle.name!r}", + ) + if particle.ghost_number != 0: + statistics = "ghost" + elif particle.auxiliary_kind is not None or particle.spin < 0: + statistics = "auxiliary" + elif particle.spin % 2 == 0: + statistics = "fermion" + else: + statistics = "boson" + + if statistics == "fermion": + wavefunction_family = "fermion" + elif particle.spin == 1: + wavefunction_family = "scalar" + elif particle.spin == 3: + wavefunction_family = "vector" + elif particle.spin == 5: + wavefunction_family = "spin2" + elif statistics == "ghost": + wavefunction_family = "ghost" + else: + wavefunction_family = "auxiliary" + + color_role = { + -3: "antifundamental", + 1: "singlet", + 3: "fundamental", + 8: "adjoint", + }[representation] + self_conjugate = particle.name == particle.antiname + if self_conjugate: + source_orientation = "self-conjugate" + elif particle.pdg_code > 0: + source_orientation = "particle" + elif particle.pdg_code < 0: + source_orientation = "antiparticle" + else: + raise ValueError( + f"non-self-conjugate particle {particle.name!r} cannot use PDG code zero" + ) + return { + "statistics": statistics, + "wavefunction_family": wavefunction_family, + "color_role": color_role, + "self_conjugate": self_conjugate, + "source_orientation": source_orientation, + } + + +def _resolved_mass_expression( + particle: CompiledParticleRecord, + parameters: Mapping[str, CompiledParameterRecord], +) -> str: + if particle.mass.upper() == "ZERO": + return "0" + try: + return parameters[particle.mass].resolved_expression + except KeyError as exc: + raise ValueError( + f"particle {particle.name!r} refers to absent mass parameter " + f"{particle.mass!r}" + ) from exc + + +def _goldstone_vector_quantum_contract_matches( + goldstone: CompiledParticleRecord, + vector: CompiledParticleRecord, + parameters: Mapping[str, CompiledParameterRecord], +) -> bool: + return ( + vector.spin == 3 + and vector.wavefunction_family == "vector" + and vector.propagating + and not vector.goldstoneboson + and vector.color == goldstone.color + and vector.quantum_numbers == goldstone.quantum_numbers + and _resolved_mass_expression(vector, parameters) + == _resolved_mass_expression(goldstone, parameters) + ) + + +@dataclass(frozen=True) +class CompiledCouplingRecord: + name: str + expression: str + resolved_expression: str + value: tuple[float, float] | None + orders: tuple[tuple[str, int], ...] + + def to_dict(self) -> dict[str, object]: + return { + "name": self.name, + "expression": self.expression, + "resolved_expression": self.resolved_expression, + "value": None if self.value is None else list(self.value), + "orders": [[name, value] for name, value in self.orders], + } + + +@dataclass(frozen=True) +class CompiledPropagatorRecord: + name: str + particle: str + numerator: str + denominator: str + custom: bool + + def to_dict(self) -> dict[str, object]: + return { + "name": self.name, + "particle": self.particle, + "numerator": self.numerator, + "denominator": self.denominator, + "custom": self.custom, + } + + +@dataclass(frozen=True) +class CompiledGoldstonePartnerRecord: + """Compiler-owned disposition of one UFO-declared Goldstone mode.""" + + goldstone: str + vector: str | None + policy: str + mass_expression: str + association_basis: str = ( + "ufo-goldstone+spin+color+quantum-numbers+resolved-mass+propagator-v1" + ) + + def __post_init__(self) -> None: + if self.policy not in {"absorbed", "explicit", "model-supplied"}: + raise ValueError(f"invalid compiled Goldstone policy {self.policy!r}") + if not self.goldstone or not self.mass_expression or not self.association_basis: + raise ValueError("compiled Goldstone metadata must not be empty") + if self.policy == "explicit" and self.vector is not None: + raise ValueError("an explicit Goldstone mode cannot name a vector partner") + if self.policy != "explicit" and not self.vector: + raise ValueError( + f"a {self.policy} Goldstone mode must name its vector partner" + ) + + def to_dict(self) -> dict[str, object]: + return { + "goldstone": self.goldstone, + "vector": self.vector, + "policy": self.policy, + "mass_expression": self.mass_expression, + "association_basis": self.association_basis, + } + + @classmethod + def from_dict( + cls, + payload: Mapping[str, object], + ) -> CompiledGoldstonePartnerRecord: + fields = _strict_record_fields( + payload, + required={ + "goldstone", + "vector", + "policy", + "mass_expression", + "association_basis", + }, + context="compiled Goldstone partner", + ) + vector_value = fields["vector"] + if vector_value is not None and not isinstance(vector_value, str): + raise TypeError("compiled Goldstone vector must be a string or null") + return cls( + goldstone=_strict_string(fields["goldstone"], "goldstone"), + vector=vector_value, + policy=_strict_string(fields["policy"], "policy"), + mass_expression=_strict_string( + fields["mass_expression"], "mass expression" + ), + association_basis=_strict_string( + fields["association_basis"], "association basis" + ), + ) + + +@dataclass(frozen=True) +class CompiledVertexTerm: + id: int + vertex: str + particles: tuple[str, ...] + color_index: int + lorentz_index: int + color_source: str + color_expression: str + lorentz_name: str + lorentz_source: str + lorentz_expression: str + coupling: str + coupling_expression: str + coupling_orders: tuple[tuple[str, int], ...] + backend: str = "ufo" + lc_color_normalization_power: int = 0 + contact_decomposition_proof: CompiledContactDecompositionProof | None = None + contact_orbit_certificate: CompiledContactOrbitCertificate | None = None + source_ordering_ids: tuple[str, ...] = () + index_bindings: tuple[TensorIndexBindingIR, ...] = () + + @property + def valence(self) -> int: + return len(self.particles) + + def to_dict(self) -> dict[str, object]: + payload: dict[str, object] = { + "id": self.id, + "vertex": self.vertex, + "particles": list(self.particles), + "valence": self.valence, + "color_index": self.color_index, + "lorentz_index": self.lorentz_index, + "color_source": self.color_source, + "color_expression": self.color_expression, + "lorentz_name": self.lorentz_name, + "lorentz_source": self.lorentz_source, + "lorentz_expression": self.lorentz_expression, + "coupling": self.coupling, + "coupling_expression": self.coupling_expression, + "coupling_orders": [[name, value] for name, value in self.coupling_orders], + "backend": self.backend, + "lc_color_normalization_power": self.lc_color_normalization_power, + "source_ordering_ids": list(self.source_ordering_ids), + "index_bindings": [item.to_json_dict() for item in self.index_bindings], + } + if self.contact_decomposition_proof is not None: + payload["contact_decomposition_proof"] = ( + self.contact_decomposition_proof.to_dict() + ) + payload["contact_orbit_certificate"] = ( + None + if self.contact_orbit_certificate is None + else self.contact_orbit_certificate.to_dict() + ) + return payload + + +@dataclass(frozen=True) +class CompiledLCColorTransitionTerm: + """Compiler-certified executable term for an LC color-state transition.""" + + input_permutation: tuple[int, int] + reverse_parent_mask: int + component_operation: str + result_component_kind: str | None + result_component_role: str + input_shape_kinds: tuple[str, str] + result_shape_kind: str | None + exact_factor_expression: str + proof_digest: str + provenance: tuple[tuple[str, str], ...] = () + + def __post_init__(self) -> None: + if not isinstance(self.input_permutation, tuple) or any( + isinstance(value, bool) or not isinstance(value, int) + for value in self.input_permutation + ): + raise TypeError("compiled LC color input permutation must be integer tuple") + if self.input_permutation not in {(0, 1), (1, 0)}: + raise ValueError( + "compiled LC color input permutation must be (0, 1) or (1, 0)" + ) + if isinstance(self.reverse_parent_mask, bool) or not isinstance( + self.reverse_parent_mask, int + ): + raise TypeError("compiled LC color reverse-parent mask must be an integer") + if self.reverse_parent_mask not in range(4): + raise ValueError( + "compiled LC color reverse-parent mask must fit two inputs" + ) + + supported_operations = { + "concatenate-join", + "concatenate-keep", + "inherit-left", + "inherit-right", + "empty", + "close", + } + if not isinstance(self.component_operation, str): + raise TypeError("compiled LC color component operation must be a string") + if self.component_operation not in supported_operations: + raise ValueError( + "unsupported compiled LC color component operation " + f"{self.component_operation!r}" + ) + + supported_component_kinds = {"open-string", "adjoint-segment", "trace"} + if self.result_component_kind is not None and not isinstance( + self.result_component_kind, str + ): + raise TypeError( + "compiled LC color result component kind must be a string or null" + ) + if self.result_component_kind is not None and ( + self.result_component_kind not in supported_component_kinds + ): + raise ValueError("unsupported compiled LC color result component kind") + if self.result_component_role not in {"active", "passive", "none"}: + raise ValueError("unsupported compiled LC color result component role") + if self.component_operation == "concatenate-join": + if ( + self.result_component_kind is None + or self.result_component_role == "none" + ): + raise ValueError( + "compiled LC joins require a result kind and active/passive role" + ) + elif self.component_operation == "close": + if self.result_component_role != "none": + raise ValueError( + "compiled LC closures cannot declare a recurrence result component" + ) + elif self.result_component_kind is not None or self.result_component_role != ( + "active" + if self.component_operation in {"inherit-left", "inherit-right"} + else "none" + ): + raise ValueError( + "compiled LC non-join result role does not match its operation" + ) + + supported_shape_kinds = { + "singlet-forest", + "fundamental-open-string", + "antifundamental-open-string", + "adjoint-segment", + } + if ( + not isinstance(self.input_shape_kinds, tuple) + or len(self.input_shape_kinds) != 2 + ): + raise TypeError( + "compiled LC color input shapes must be a two-element tuple" + ) + if any(not isinstance(value, str) for value in self.input_shape_kinds): + raise TypeError("compiled LC color input shapes must be strings") + if any(value not in supported_shape_kinds for value in self.input_shape_kinds): + raise ValueError( + "compiled LC color term requires two supported input shape kinds" + ) + if self.result_shape_kind is not None and not isinstance( + self.result_shape_kind, str + ): + raise TypeError( + "compiled LC color result shape kind must be a string or null" + ) + if self.component_operation == "close": + if self.result_shape_kind is not None: + raise ValueError( + "compiled LC color closure term cannot declare a result shape" + ) + elif self.result_shape_kind not in supported_shape_kinds: + raise ValueError( + "non-closure compiled LC color term requires a supported result shape" + ) + + if not isinstance(self.exact_factor_expression, str): + raise TypeError("compiled LC color exact factor must be a string") + if ( + not self.exact_factor_expression + or self.exact_factor_expression != self.exact_factor_expression.strip() + ): + raise ValueError( + "compiled LC color exact factor must be a nonempty canonical string" + ) + if not isinstance(self.proof_digest, str): + raise TypeError("compiled LC color proof digest must be a string") + if len(self.proof_digest) != 64 or any( + character not in "0123456789abcdef" for character in self.proof_digest + ): + raise ValueError("compiled LC color proof digest must be lowercase SHA256") + + if not isinstance(self.provenance, tuple): + raise TypeError("compiled LC color provenance must be an immutable tuple") + for item in self.provenance: + if not isinstance(item, tuple) or len(item) != 2: + raise TypeError( + "compiled LC color provenance entries must be key/value tuples" + ) + key, value = item + if not isinstance(key, str) or not isinstance(value, str): + raise TypeError( + "compiled LC color provenance keys and values must be strings" + ) + if not key or not value or key != key.strip() or value != value.strip(): + raise ValueError( + "compiled LC color provenance keys and values must be nonempty " + "canonical strings" + ) + provenance_keys = tuple(key for key, _value in self.provenance) + if provenance_keys != tuple(sorted(set(provenance_keys))): + raise ValueError( + "compiled LC color provenance keys must be sorted and unique" + ) + + def to_dict(self) -> dict[str, object]: + return { + "component_operation": self.component_operation, + "exact_factor_expression": self.exact_factor_expression, + "input_permutation": list(self.input_permutation), + "input_shape_kinds": list(self.input_shape_kinds), + "proof_digest": self.proof_digest, + "provenance": [list(item) for item in self.provenance], + "result_component_kind": self.result_component_kind, + "result_component_role": self.result_component_role, + "result_shape_kind": self.result_shape_kind, + "reverse_parent_mask": self.reverse_parent_mask, + } + + @classmethod + def from_dict( + cls, + payload: Mapping[str, object], + ) -> CompiledLCColorTransitionTerm: + required = { + "component_operation", + "exact_factor_expression", + "input_permutation", + "input_shape_kinds", + "proof_digest", + "result_component_kind", + "result_shape_kind", + "reverse_parent_mask", + } + fields = set(payload) + missing = required - fields + if missing: + names = ", ".join(sorted(missing)) + raise ValueError( + f"compiled LC color transition term is missing required fields: {names}" + ) + unknown = fields - required - {"provenance", "result_component_role"} + if unknown: + names = ", ".join(sorted(str(name) for name in unknown)) + raise ValueError( + f"compiled LC color transition term has unknown fields: {names}" + ) + + input_permutation = _required_sequence_field( + payload, + "input_permutation", + context="compiled LC color transition term", + ) + input_shape_kinds = _required_sequence_field( + payload, + "input_shape_kinds", + context="compiled LC color transition term", + ) + if len(input_permutation) != 2: + raise ValueError( + "compiled LC color input permutation must contain two entries" + ) + if len(input_shape_kinds) != 2: + raise ValueError("compiled LC color input shapes must contain two entries") + + provenance_value = payload.get("provenance", ()) + if isinstance(provenance_value, (str, bytes)) or not isinstance( + provenance_value, list | tuple + ): + raise TypeError("compiled LC color provenance must be an array") + provenance: list[tuple[str, str]] = [] + for index, item in enumerate(provenance_value): + if isinstance(item, (str, bytes)) or not isinstance(item, list | tuple): + raise TypeError( + "compiled LC color provenance item " + f"{index} must be a key/value array" + ) + if len(item) != 2: + raise ValueError( + "compiled LC color provenance item " + f"{index} must contain two entries" + ) + provenance.append( + ( + _strict_string(item[0], "compiled LC color provenance key"), + _strict_string(item[1], "compiled LC color provenance value"), + ) + ) + + result_component_kind = payload["result_component_kind"] + if result_component_kind is not None and not isinstance( + result_component_kind, str + ): + raise TypeError( + "compiled LC color result component kind must be a string or null" + ) + result_shape_kind = payload["result_shape_kind"] + if result_shape_kind is not None and not isinstance(result_shape_kind, str): + raise TypeError( + "compiled LC color result shape kind must be a string or null" + ) + result_component_role = payload.get("result_component_role") + if result_component_role is None: + operation = payload["component_operation"] + result_component_role = ( + "passive" + if operation == "concatenate-join" + and result_shape_kind == "singlet-forest" + else "active" + if operation in {"concatenate-join", "inherit-left", "inherit-right"} + else "none" + ) + return cls( + input_permutation=( + _strict_integer( + input_permutation[0], "compiled LC color input permutation" + ), + _strict_integer( + input_permutation[1], "compiled LC color input permutation" + ), + ), + reverse_parent_mask=_strict_integer( + payload["reverse_parent_mask"], + "compiled LC color reverse-parent mask", + ), + component_operation=_strict_string( + payload["component_operation"], + "compiled LC color component operation", + ), + result_component_kind=result_component_kind, + result_component_role=_strict_string( + result_component_role, + "compiled LC color result component role", + ), + input_shape_kinds=( + _strict_string(input_shape_kinds[0], "compiled LC color input shape"), + _strict_string(input_shape_kinds[1], "compiled LC color input shape"), + ), + result_shape_kind=result_shape_kind, + exact_factor_expression=_strict_string( + payload["exact_factor_expression"], + "compiled LC color exact factor", + ), + proof_digest=_strict_string( + payload["proof_digest"], "compiled LC color proof digest" + ), + provenance=tuple(provenance), + ) + + +@dataclass(frozen=True) +class CompiledOrientedKernel: + kind: int + term_id: int + vertex: str + particles: tuple[str, str, str] + source_particle_legs: tuple[int, int, int] + component_expressions: tuple[str, ...] + coupling_expression: str + coupling_orders: tuple[tuple[str, int], ...] + runtime_parameters: tuple[str, ...] + color_source: str + color_expression: str + color_projection_structure: str | None = None + color_projection_coefficient: tuple[float, float] | None = None + lc_color_normalization_power: int = 0 + term_ids: tuple[int, ...] = () + evaluation_class: str = "" + evaluation_factor: tuple[float, float] = (1.0, 0.0) + evaluation_input_order: tuple[int, int] = (0, 1) + evaluation_input_exchange_factor: tuple[float, float] | None = None + evaluation_equivalence_verified: bool = False + input_ordering_ids: tuple[str, ...] = () + output_ordering_id: str = "" + lc_recurrence_color_rule_kind: str | None = None + lc_color_transition_terms: tuple[CompiledLCColorTransitionTerm, ...] = () + lc_color_closure_terms: tuple[CompiledLCColorTransitionTerm, ...] = () + contact_orbit_steps: tuple[CompiledContactOrbitStep, ...] = () + + def __post_init__(self) -> None: + if self.contact_orbit_steps != tuple(sorted(set(self.contact_orbit_steps))): + raise ValueError("compiled contact-orbit steps must be sorted and unique") + if self.lc_recurrence_color_rule_kind is not None and ( + not isinstance(self.lc_recurrence_color_rule_kind, str) + or not self.lc_recurrence_color_rule_kind + ): + raise ValueError( + "compiled oriented-kernel recurrence color rule must be a " + "nonempty string" + ) + compiler_closure_terms = getattr( + self.lc_color_transition_terms, + "compiler_closure_terms", + None, + ) + if compiler_closure_terms is not None: + compiler_closure_terms = tuple(compiler_closure_terms) + if self.lc_color_closure_terms and ( + self.lc_color_closure_terms != compiler_closure_terms + ): + raise ValueError( + "compiled oriented-kernel LC closure terms conflict with " + "compiler-owned closure companions" + ) + object.__setattr__( + self, + "lc_color_transition_terms", + tuple(self.lc_color_transition_terms), + ) + object.__setattr__( + self, + "lc_color_closure_terms", + compiler_closure_terms, + ) + if not isinstance(self.lc_color_transition_terms, tuple): + raise TypeError( + "compiled oriented-kernel LC color transition terms must be a tuple" + ) + if any( + not isinstance(item, CompiledLCColorTransitionTerm) + for item in self.lc_color_transition_terms + ): + raise TypeError( + "compiled oriented-kernel LC color transition terms must contain " + "typed records" + ) + if not isinstance(self.lc_color_closure_terms, tuple): + raise TypeError( + "compiled oriented-kernel LC color closure terms must be a tuple" + ) + if any( + not isinstance(item, CompiledLCColorTransitionTerm) + for item in self.lc_color_closure_terms + ): + raise TypeError( + "compiled oriented-kernel LC color closure terms must contain " + "typed records" + ) + if any( + item.component_operation == "close" + for item in self.lc_color_transition_terms + ): + raise ValueError( + "compiled oriented-kernel LC transition terms cannot contain " + "closure operations" + ) + if any( + item.component_operation != "close" for item in self.lc_color_closure_terms + ): + raise ValueError( + "compiled oriented-kernel LC closure terms must contain only " + "closure operations" + ) + + def to_dict(self) -> dict[str, object]: + return { + "kind": self.kind, + "term_id": self.term_id, + "vertex": self.vertex, + "particles": list(self.particles), + "source_particle_legs": list(self.source_particle_legs), + "component_expressions": list(self.component_expressions), + "coupling_expression": self.coupling_expression, + "coupling_orders": [[name, value] for name, value in self.coupling_orders], + "runtime_parameters": list(self.runtime_parameters), + "color_source": self.color_source, + "color_expression": self.color_expression, + "color_projection_structure": self.color_projection_structure, + "color_projection_coefficient": ( + None + if self.color_projection_coefficient is None + else list(self.color_projection_coefficient) + ), + "lc_color_normalization_power": self.lc_color_normalization_power, + "term_ids": list(self.term_ids or (self.term_id,)), + "evaluation_class": self.evaluation_class, + "evaluation_factor": list(self.evaluation_factor), + "evaluation_input_order": list(self.evaluation_input_order), + "evaluation_input_exchange_factor": ( + None + if self.evaluation_input_exchange_factor is None + else list(self.evaluation_input_exchange_factor) + ), + "evaluation_equivalence_verified": (self.evaluation_equivalence_verified), + "input_ordering_ids": list(self.input_ordering_ids), + "output_ordering_id": self.output_ordering_id, + "lc_recurrence_color_rule_kind": self.lc_recurrence_color_rule_kind, + "lc_color_transition_terms": [ + item.to_dict() for item in self.lc_color_transition_terms + ], + "lc_color_closure_terms": [ + item.to_dict() for item in self.lc_color_closure_terms + ], + "contact_orbit_steps": [ + step.to_dict() for step in self.contact_orbit_steps + ], + } + + +@dataclass(frozen=True) +class _ContactTreeNode: + legs: tuple[int, ...] + particle: CompiledParticleRecord + physical_leg: int | None = None + left: _ContactTreeNode | None = None + right: _ContactTreeNode | None = None + + @property + def is_leaf(self) -> bool: + return self.physical_leg is not None + + +@dataclass(frozen=True) +class CompiledDirectContractionRecord: + left_particle: str + left_chirality: int + right_particle: str + right_chirality: int + contraction_ir: ContractionIR + + def __post_init__(self) -> None: + _validate_particle_selector_name(self.left_particle, "left particle") + _validate_particle_selector_name(self.right_particle, "right particle") + _validate_chirality(self.left_chirality, "left chirality") + _validate_chirality(self.right_chirality, "right chirality") + if not isinstance(self.contraction_ir, ContractionIR): + raise TypeError("direct contraction must contain a ContractionIR") + + @property + def selector(self) -> tuple[str, int, str, int]: + return ( + self.left_particle, + self.left_chirality, + self.right_particle, + self.right_chirality, + ) + + def to_dict(self) -> dict[str, object]: + return { + "left_particle": self.left_particle, + "left_chirality": self.left_chirality, + "right_particle": self.right_particle, + "right_chirality": self.right_chirality, + "contraction_ir": self.contraction_ir.to_json_dict(), + } + + @classmethod + def from_dict( + cls, + payload: Mapping[str, object], + ) -> CompiledDirectContractionRecord: + fields = _strict_record_fields( + payload, + required={ + "left_particle", + "left_chirality", + "right_particle", + "right_chirality", + "contraction_ir", + }, + context="compiled direct contraction", + ) + return cls( + left_particle=_strict_string(fields["left_particle"], "left particle"), + left_chirality=_strict_integer(fields["left_chirality"], "left chirality"), + right_particle=_strict_string(fields["right_particle"], "right particle"), + right_chirality=_strict_integer( + fields["right_chirality"], "right chirality" + ), + contraction_ir=ContractionIR.from_json_dict( + _strict_mapping(fields["contraction_ir"], "contraction IR") + ), + ) + + +@dataclass(frozen=True) +class CompiledClosureContractionRecord: + particle: str + chirality: int + contraction_ir: ContractionIR + + def __post_init__(self) -> None: + _validate_particle_selector_name(self.particle, "particle") + _validate_chirality(self.chirality, "chirality") + if not isinstance(self.contraction_ir, ContractionIR): + raise TypeError("closure contraction must contain a ContractionIR") + + @property + def selector(self) -> tuple[str, int]: + return self.particle, self.chirality + + def to_dict(self) -> dict[str, object]: + return { + "particle": self.particle, + "chirality": self.chirality, + "contraction_ir": self.contraction_ir.to_json_dict(), + } + + @classmethod + def from_dict( + cls, + payload: Mapping[str, object], + ) -> CompiledClosureContractionRecord: + fields = _strict_record_fields( + payload, + required={"particle", "chirality", "contraction_ir"}, + context="compiled closure contraction", + ) + return cls( + particle=_strict_string(fields["particle"], "particle"), + chirality=_strict_integer(fields["chirality"], "chirality"), + contraction_ir=ContractionIR.from_json_dict( + _strict_mapping(fields["contraction_ir"], "contraction IR") + ), + ) + + +@dataclass(frozen=True) +class CompiledModelIR: + name: str + orders: tuple[CompiledCouplingOrder, ...] + parameters: tuple[CompiledParameterRecord, ...] + particles: tuple[CompiledParticleRecord, ...] + couplings: tuple[CompiledCouplingRecord, ...] + propagators: tuple[CompiledPropagatorRecord, ...] + vertex_terms: tuple[CompiledVertexTerm, ...] + oriented_kernels: tuple[CompiledOrientedKernel, ...] + direct_contractions: tuple[CompiledDirectContractionRecord, ...] + closure_contractions: tuple[CompiledClosureContractionRecord, ...] + tensor_orderings: tuple[TensorOrderingIR, ...] = () + current_orderings: tuple[CompiledCurrentOrderingRecord, ...] = () + goldstone_partners: tuple[CompiledGoldstonePartnerRecord, ...] = () + + def __post_init__(self) -> None: + self._validate_particle_identities() + self._validate_goldstone_partners() + self._validate_contractions() + self._validate_contact_decomposition_proofs() + self._validate_contact_orbit_contracts() + self._validate_tensor_orderings() + for context, expression in self._executable_expressions(): + if "UFO::" in expression: + raise ValueError( + f"{context} retains a process-global UFO symbol; " + "regenerate it through the model symbol registry" + ) + + def _validate_particle_identities(self) -> None: + by_name: dict[str, CompiledParticleRecord] = {} + by_pdg: dict[int, CompiledParticleRecord] = {} + for particle in self.particles: + if particle.name in by_name: + raise ValueError( + f"compiled model contains duplicate particle name {particle.name!r}" + ) + if particle.pdg_code in by_pdg: + raise ValueError( + f"compiled model contains duplicate PDG code {particle.pdg_code}" + ) + by_name[particle.name] = particle + by_pdg[particle.pdg_code] = particle + for particle in self.particles: + anti = by_name.get(particle.antiname) + if anti is None: + raise ValueError( + f"particle {particle.name!r} refers to absent antiparticle " + f"{particle.antiname!r}" + ) + if anti.antiname != particle.name: + raise ValueError( + f"particle/antiparticle relation is not involutive for " + f"{particle.name!r} and {anti.name!r}" + ) + if anti is not particle and anti.pdg_code != -particle.pdg_code: + raise ValueError( + f"non-self-conjugate pair {particle.name!r}/{anti.name!r} must " + "use opposite signed PDG codes" + ) + if anti is particle: + for name, expression in particle.quantum_numbers: + parsed = _constant_quantum_number_expression( + expression, + context=( + f"self-conjugate particle {particle.name!r} quantum " + f"number {name!r}" + ), + ) + if parsed.to_canonical_string() != "0": + raise ValueError( + f"self-conjugate particle {particle.name!r} must have " + f"zero quantum number {name!r}" + ) + continue + particle_names = tuple(name for name, _ in particle.quantum_numbers) + anti_names = tuple(name for name, _ in anti.quantum_numbers) + if particle_names != anti_names: + raise ValueError( + f"particle/antiparticle pair {particle.name!r}/{anti.name!r} " + "must declare the same quantum numbers" + ) + for (name, expression), (_anti_name, anti_expression) in zip( + particle.quantum_numbers, + anti.quantum_numbers, + strict=True, + ): + total = _constant_quantum_number_expression( + expression, + context=f"particle {particle.name!r} quantum number {name!r}", + ) + _constant_quantum_number_expression( + anti_expression, + context=f"particle {anti.name!r} quantum number {name!r}", + ) + if total.to_canonical_string() != "0": + raise ValueError( + f"particle/antiparticle pair {particle.name!r}/{anti.name!r} " + f"must have exactly negated quantum number {name!r}" + ) + + def _validate_goldstone_partners(self) -> None: + from .compiler_gauge import compile_goldstone_partner_records + + expected = compile_goldstone_partner_records( + self.particles, + self.parameters, + self.propagators, + ) + if self.goldstone_partners != expected: + raise ValueError( + "compiled Goldstone partner contracts are stale or non-canonical" + ) + + def _validate_contact_decomposition_proofs(self) -> None: + for term in self.vertex_terms: + proof = term.contact_decomposition_proof + requires_proof = ( + term.backend == "ufo" + and term.valence == 4 + and "ufo_momentum_" not in term.lorentz_expression + ) + if proof is None and requires_proof: + raise ValueError( + f"UFO four-point term {term.id} has no contact decomposition proof" + ) + if proof is None: + continue + if term.valence != 4: + raise ValueError( + f"contact decomposition proof on non-four-point term {term.id}" + ) + if not proof.matches(term): + raise ValueError( + f"contact decomposition proof identity mismatch for term {term.id}" + ) + + def _validate_contact_orbit_contracts(self) -> None: + particles = {particle.name: particle for particle in self.particles} + certificates: dict[int, CompiledContactOrbitCertificate] = {} + expected_steps: set[CompiledContactOrbitStep] = set() + heft_terms: set[int] = set() + for term in self.vertex_terms: + certificate = term.contact_orbit_certificate + proof = term.contact_decomposition_proof + source_particles = tuple(particles[name] for name in term.particles) + scalar_certifiable = ( + proof is not None + and proof.status == "proven" + and proof.matches(term) + and contact_orbit_evaluator_class_is_certifiable( + color_source=term.color_source, + color_expression=term.color_expression, + lorentz_expression=term.lorentz_expression, + decomposition_kinds=tuple( + split.decomposition_kind for split in proof.splits + ), + particle_contracts=tuple( + ( + particle.spin, + particle.color, + particle.statistics, + particle.wavefunction_family, + particle.self_conjugate, + ) + for particle in source_particles + ), + ) + ) + heft_certifiable = False + if certificate is not None and ( + certificate.evaluator_class == HEFT_CONTACT_ORBIT_EVALUATOR_CLASS + ): + from .compiler_contact_trees import _heft_contact_color_topology + + heft_certifiable = ( + _heft_contact_color_topology( + term, + source_particles, + model_symbols=symbols.model(self.name), + ) + is not None + ) + certifiable = scalar_certifiable or heft_certifiable + if certificate is None: + if scalar_certifiable: + raise ValueError( + f"certifiable contact term {term.id} has no orbit certificate" + ) + continue + if not certifiable: + raise ValueError( + f"contact-orbit certificate for term {term.id} names an " + "excluded evaluator class" + ) + if ( + certificate.term_id != term.id + or certificate.vertex != term.vertex + or certificate.particles != term.particles + or certificate.color_expression != term.color_expression + or certificate.lorentz_expression != term.lorentz_expression + or certificate.coupling_expression != term.coupling_expression + ): + raise ValueError( + f"contact-orbit certificate identity mismatch for term {term.id}" + ) + certificates[term.id] = certificate + if certificate.evaluator_class == CONTACT_ORBIT_EVALUATOR_CLASS: + assert proof is not None + expected_steps.update( + compiled_contact_orbit_step(certificate, split, orientation) + for split in proof.splits + for orientation in split.orientations + ) + else: + heft_terms.add(term.id) + + actual_steps: list[CompiledContactOrbitStep] = [] + seen_heft_terms: set[int] = set() + for kernel in self.oriented_kernels: + is_heft_kernel = "::contact-heft-tree-" in kernel.vertex + if is_heft_kernel: + if kernel.term_id not in heft_terms: + raise ValueError( + f"HEFT contact kernel {kernel.kind} has no authenticated term" + ) + if len(kernel.contact_orbit_steps) != 1: + raise ValueError( + f"HEFT contact kernel {kernel.kind} must carry one orbit step" + ) + seen_heft_terms.add(kernel.term_id) + for step in kernel.contact_orbit_steps: + if step.term_id not in certificates: + raise ValueError( + f"contact-orbit step on kernel {kernel.kind} has no certificate" + ) + expected_suffixes = ( + ("::contact-partial", "::contact-heft-tree-partial") + if step.stage == "partial" + else ( + "::contact-final", + "::contact-final-fused", + "::contact-heft-tree-final", + ) + ) + if not kernel.vertex.endswith(expected_suffixes): + raise ValueError( + f"contact-orbit step stage mismatch for kernel {kernel.kind}" + ) + certificate = certificates[step.term_id] + arity = len(certificate.particles) + referenced_legs = { + step.result_leg, + *step.left_covered_legs, + *step.right_covered_legs, + } + if any(leg not in range(arity) for leg in referenced_legs): + raise ValueError( + f"contact-orbit step on kernel {kernel.kind} exceeds term arity" + ) + if is_heft_kernel: + expected_source_legs = ( + -1 + if len(step.left_covered_legs) > 1 + else step.left_covered_legs[0], + -1 + if len(step.right_covered_legs) > 1 + else step.right_covered_legs[0], + -1 if step.stage == "partial" else step.result_leg, + ) + if kernel.source_particle_legs != expected_source_legs: + raise ValueError( + "HEFT contact-orbit lineage mismatch for kernel " + f"{kernel.kind}" + ) + expected_steps.add(step) + actual_steps.append(step) + if seen_heft_terms != heft_terms: + missing = sorted(heft_terms - seen_heft_terms) + unexpected = sorted(seen_heft_terms - heft_terms) + raise ValueError( + "HEFT contact-orbit certificates and lowered terms disagree: " + f"missing={missing}, unexpected={unexpected}" + ) + if len(actual_steps) != len(set(actual_steps)): + raise ValueError( + "compiled contact-orbit lineage contains duplicate retained steps" + ) + if set(actual_steps) != expected_steps: + raise ValueError( + "compiled contact-orbit lineage is incomplete after contact fusion" + ) + + def _validate_contractions(self) -> None: + particles = {particle.name: particle for particle in self.particles} + parameters = {parameter.name: parameter for parameter in self.parameters} + propagators = {propagator.name: propagator for propagator in self.propagators} + direct_selectors: set[tuple[str, int, str, int]] = set() + for record in self.direct_contractions: + if not isinstance(record, CompiledDirectContractionRecord): + raise TypeError( + "compiled model direct contractions must contain typed records" + ) + if record.selector in direct_selectors: + raise ValueError( + f"compiled model contains duplicate direct contraction selector " + f"{record.selector!r}" + ) + direct_selectors.add(record.selector) + try: + left = particles[record.left_particle] + right = particles[record.right_particle] + except KeyError as exc: + raise ValueError( + f"direct contraction refers to absent particle {exc.args[0]!r}" + ) from exc + if left.antiname != right.name or right.antiname != left.name: + raise ValueError( + f"direct contraction particles {left.name!r}/{right.name!r} " + "are not an antiparticle pair" + ) + left_dimension = compiled_current_dimension( + left, + record.left_chirality, + parameters=parameters, + propagators=propagators, + ) + right_dimension = compiled_current_dimension( + right, + record.right_chirality, + parameters=parameters, + propagators=propagators, + ) + coefficient_count = len(record.contraction_ir.coefficients) + if left_dimension != right_dimension or coefficient_count != left_dimension: + raise ValueError( + f"direct contraction selector {record.selector!r} has " + f"{coefficient_count} coefficients for current dimensions " + f"{left_dimension} and {right_dimension}" + ) + _validate_concrete_chirality_relation( + record.contraction_ir, + record.left_chirality, + record.right_chirality, + context=f"direct contraction selector {record.selector!r}", + ) + + closure_selectors: set[tuple[str, int]] = set() + for record in self.closure_contractions: + if not isinstance(record, CompiledClosureContractionRecord): + raise TypeError( + "compiled model closure contractions must contain typed records" + ) + if record.selector in closure_selectors: + raise ValueError( + f"compiled model contains duplicate closure contraction selector " + f"{record.selector!r}" + ) + closure_selectors.add(record.selector) + try: + particle = particles[record.particle] + except KeyError as exc: + raise ValueError( + f"closure contraction refers to absent particle {exc.args[0]!r}" + ) from exc + dimension = compiled_current_dimension( + particle, + record.chirality, + parameters=parameters, + propagators=propagators, + ) + contraction = record.contraction_ir + if ( + dimension != 1 + or len(contraction.coefficients) != 1 + or contraction.name != "scalar" + or contraction.left_basis != "scalar" + or contraction.right_basis != "scalar" + or contraction.metric_signature is not None + ): + raise ValueError( + f"closure contraction selector {record.selector!r} must be a " + "one-component scalar projection" + ) + _validate_concrete_chirality_relation( + contraction, + record.chirality, + record.chirality, + context=f"closure contraction selector {record.selector!r}", + ) + + def _validate_tensor_orderings(self) -> None: + orderings: dict[str, TensorOrderingIR] = {} + for ordering in self.tensor_orderings: + if not isinstance(ordering, TensorOrderingIR): + raise TypeError( + "compiled model tensor orderings must contain TensorOrderingIR" + ) + if ordering.ordering_id in orderings: + raise ValueError( + f"compiled model contains duplicate tensor ordering " + f"{ordering.ordering_id!r}" + ) + orderings[ordering.ordering_id] = ordering + + particles = {particle.name: particle for particle in self.particles} + parameters = {parameter.name: parameter for parameter in self.parameters} + propagators = {propagator.name: propagator for propagator in self.propagators} + current_orderings: dict[tuple[str, int], CompiledCurrentOrderingRecord] = {} + for record in self.current_orderings: + if not isinstance(record, CompiledCurrentOrderingRecord): + raise TypeError( + "compiled model current orderings must contain typed records" + ) + if record.selector in current_orderings: + raise ValueError( + f"compiled model contains duplicate current ordering selector " + f"{record.selector!r}" + ) + current_orderings[record.selector] = record + try: + particle = particles[record.particle] + except KeyError as exc: + raise ValueError( + f"current ordering refers to absent particle {record.particle!r}" + ) from exc + try: + ordering = orderings[record.ordering_id] + kernel_ordering = orderings[record.kernel_ordering_id] + except KeyError as exc: + raise ValueError( + f"current ordering {record.selector!r} refers to absent tensor " + f"ordering {exc.args[0]!r}" + ) from exc + expected_dimension = compiled_current_dimension( + particle, + record.chirality, + parameters=parameters, + propagators=propagators, + ) + if ordering.stored_size != expected_dimension: + raise ValueError( + f"current ordering {record.selector!r} stores " + f"{ordering.stored_size} components, expected {expected_dimension}" + ) + if len(record.input_embedding) != kernel_ordering.stored_size: + raise ValueError( + f"current ordering {record.selector!r} input embedding has " + "the wrong kernel size" + ) + embedded = tuple( + value for value in record.input_embedding if value is not None + ) + if sorted(embedded) != list(range(ordering.stored_size)): + raise ValueError( + f"current ordering {record.selector!r} input embedding does not " + "map every stored component exactly once" + ) + if ( + len(record.result_projection) != ordering.stored_size + or len(set(record.result_projection)) != ordering.stored_size + or any( + value < 0 or value >= kernel_ordering.stored_size + for value in record.result_projection + ) + ): + raise ValueError( + f"current ordering {record.selector!r} result projection is invalid" + ) + + has_external_contract = any(term.backend == "ufo" for term in self.vertex_terms) + if has_external_contract and (not orderings or not current_orderings): + raise ValueError( + "compiled UFO model is missing explicit tensor ordering metadata" + ) + if orderings or current_orderings: + for particle in self.particles: + if (particle.name, 0) not in current_orderings: + raise ValueError( + f"particle {particle.name!r} has no full current ordering" + ) + + term_by_id = {term.id: term for term in self.vertex_terms} + for term in self.vertex_terms: + if term.backend == "ufo" and len(term.source_ordering_ids) != term.valence: + raise ValueError( + f"UFO vertex term {term.id} does not declare one source ordering " + "per particle leg" + ) + if term.source_ordering_ids: + for leg, (particle_name, ordering_id) in enumerate( + zip(term.particles, term.source_ordering_ids, strict=True), + start=1, + ): + if ordering_id not in orderings: + raise ValueError( + f"vertex term {term.id} source leg {leg} refers to absent " + f"tensor ordering {ordering_id!r}" + ) + current = current_orderings.get((particle_name, 0)) + if current is None or current.kernel_ordering_id != ordering_id: + raise ValueError( + f"vertex term {term.id} source leg {leg} ordering does not " + f"match particle {particle_name!r}" + ) + normalized_names: set[str] = set() + for binding in term.index_bindings: + if not isinstance(binding, TensorIndexBindingIR): + raise TypeError( + f"vertex term {term.id} index bindings must be typed records" + ) + if binding.normalized_name in normalized_names: + raise ValueError( + f"vertex term {term.id} repeats normalized tensor index " + f"{binding.normalized_name!r}" + ) + normalized_names.add(binding.normalized_name) + if binding.source_leg is not None and binding.source_leg > term.valence: + raise ValueError( + f"vertex term {term.id} tensor index refers to absent source " + f"leg {binding.source_leg}" + ) + + for kernel in self.oriented_kernels: + if kernel.evaluation_input_order not in {(0, 1), (1, 0)}: + raise ValueError( + f"oriented kernel {kernel.kind} has invalid evaluation input order" + ) + exchange_factor = kernel.evaluation_input_exchange_factor + if exchange_factor is not None: + if not kernel.evaluation_equivalence_verified: + raise ValueError( + f"oriented kernel {kernel.kind} has an unverified input-" + "exchange relation" + ) + if exchange_factor not in {(1.0, 0.0), (-1.0, 0.0)}: + raise ValueError( + f"oriented kernel {kernel.kind} has invalid input-exchange " + "phase" + ) + if len(kernel.input_ordering_ids) != 2 or not kernel.output_ordering_id: + if has_external_contract: + raise ValueError( + f"oriented kernel {kernel.kind} is missing tensor ordering " + "references" + ) + continue + referenced_ids = (*kernel.input_ordering_ids, kernel.output_ordering_id) + try: + for ordering_id in kernel.input_ordering_ids: + orderings[ordering_id] + output_ordering = orderings[kernel.output_ordering_id] + except KeyError as exc: + raise ValueError( + f"oriented kernel {kernel.kind} refers to absent tensor ordering " + f"{exc.args[0]!r}" + ) from exc + for particle_name, ordering_id in zip( + kernel.particles, + referenced_ids, + strict=True, + ): + current = current_orderings.get((particle_name, 0)) + if current is None or current.kernel_ordering_id != ordering_id: + raise ValueError( + f"oriented kernel {kernel.kind} ordering does not match " + f"particle {particle_name!r}" + ) + if len(kernel.component_expressions) != output_ordering.stored_size: + raise ValueError( + f"oriented kernel {kernel.kind} has " + f"{len(kernel.component_expressions)} components for output " + f"ordering size {output_ordering.stored_size}" + ) + term = term_by_id.get(kernel.term_id) + if term is None: + raise ValueError( + f"oriented kernel {kernel.kind} refers to absent term " + f"{kernel.term_id}" + ) + for kernel_slot, source_leg in enumerate(kernel.source_particle_legs): + if source_leg < 0: + continue + if source_leg >= term.valence: + raise ValueError( + f"oriented kernel {kernel.kind} refers to absent source leg " + f"{source_leg}" + ) + if term.source_ordering_ids[source_leg] != referenced_ids[kernel_slot]: + raise ValueError( + f"oriented kernel {kernel.kind} source-leg ordering disagrees " + f"with vertex term {term.id}" + ) + for record in self.direct_contractions: + if current_orderings: + for particle, chirality in ( + (record.left_particle, record.left_chirality), + (record.right_particle, record.right_chirality), + ): + if (particle, chirality) not in current_orderings: + raise ValueError( + f"direct contraction selector {record.selector!r} has no " + f"current ordering metadata for {(particle, chirality)!r}" + ) + for record in self.closure_contractions: + if current_orderings and record.selector not in current_orderings: + raise ValueError( + f"closure contraction selector {record.selector!r} has no current " + "ordering metadata" + ) + + if has_external_contract: + # The content IDs make each ordering internally immutable, but they do + # not prove that it describes this model. Recompile the complete + # contract from the normalized model IR and require exact equality. + # This also validates source index bindings and the runtime's chiral + # embedding/projection conventions without maintaining a second set of + # validation rules. + from .compiler_tensor_ordering import compile_tensor_ordering_metadata + + ( + expected_terms, + expected_kernels, + expected_orderings, + expected_current_orderings, + ) = compile_tensor_ordering_metadata( + self.vertex_terms, + self.particles, + self.oriented_kernels, + self.parameters, + self.propagators, + ) + if expected_terms != self.vertex_terms: + raise ValueError( + "compiled UFO model tensor index bindings are not canonical" + ) + if expected_kernels != self.oriented_kernels: + raise ValueError( + "compiled UFO model kernel tensor orderings are not canonical" + ) + if expected_orderings != self.tensor_orderings: + raise ValueError( + "compiled UFO model tensor orderings do not match its particles " + "and contact proofs" + ) + if expected_current_orderings != self.current_orderings: + raise ValueError( + "compiled UFO model current tensor mappings are not canonical" + ) + + def _executable_expressions(self) -> tuple[tuple[str, str], ...]: + """Return scalar/evaluator expressions, excluding raw tensor source.""" + + result: list[tuple[str, str]] = [] + for parameter in self.parameters: + if parameter.expression is not None: + result.append( + (f"parameter {parameter.name} expression", parameter.expression) + ) + result.append( + ( + f"parameter {parameter.name} resolved expression", + parameter.resolved_expression, + ) + ) + for coupling in self.couplings: + result.extend( + ( + (f"coupling {coupling.name} expression", coupling.expression), + ( + f"coupling {coupling.name} resolved expression", + coupling.resolved_expression, + ), + ) + ) + for term in self.vertex_terms: + result.append((f"vertex term {term.id} coupling", term.coupling_expression)) + for kernel in self.oriented_kernels: + result.append( + (f"oriented kernel {kernel.kind} coupling", kernel.coupling_expression) + ) + result.extend( + (f"oriented kernel {kernel.kind} component {index}", expression) + for index, expression in enumerate(kernel.component_expressions) + ) + return tuple(result) + + @property + def max_vertex_valence(self) -> int: + return max((term.valence for term in self.vertex_terms), default=0) + + @property + def symbol_namespace(self) -> str: + return symbols.model(self.name).namespace + + def to_dict(self) -> dict[str, object]: + return { + "name": self.name, + "symbol_namespace": self.symbol_namespace, + "orders": [item.to_dict() for item in self.orders], + "parameters": [item.to_dict() for item in self.parameters], + "particles": [item.to_dict() for item in self.particles], + "couplings": [item.to_dict() for item in self.couplings], + "propagators": [item.to_dict() for item in self.propagators], + "vertex_terms": [item.to_dict() for item in self.vertex_terms], + "oriented_kernels": [item.to_dict() for item in self.oriented_kernels], + "direct_contractions": [ + item.to_dict() for item in self.direct_contractions + ], + "closure_contractions": [ + item.to_dict() for item in self.closure_contractions + ], + "tensor_orderings": [item.to_json_dict() for item in self.tensor_orderings], + "current_orderings": [ + item.to_json_dict() for item in self.current_orderings + ], + "goldstone_partners": [item.to_dict() for item in self.goldstone_partners], + "max_vertex_valence": self.max_vertex_valence, + } + + @staticmethod + def from_dict(payload: Mapping[str, object]) -> CompiledModelIR: + name = str(payload["name"]) + expected_namespace = symbols.model(name).namespace + if payload.get("symbol_namespace") != expected_namespace: + raise ValueError( + "compiled model symbol namespace mismatch; regenerate the model" + ) + return CompiledModelIR( + name=name, + orders=tuple( + CompiledCouplingOrder( + name=str(item["name"]), + expansion_order=_integer(item["expansion_order"]), + hierarchy=_integer(item["hierarchy"]), + ) + for item in _mappings(payload.get("orders")) + ), + parameters=tuple( + CompiledParameterRecord( + name=str(item["name"]), + nature=str(item["nature"]), + parameter_type=str(item["parameter_type"]), + value=_optional_pair(item.get("value")), + expression=_optional_string(item.get("expression")), + resolved_expression=str(item["resolved_expression"]), + lhablock=_optional_string(item.get("lhablock")), + lhacode=tuple( + _integer(value) for value in _sequence(item.get("lhacode")) + ), + ) + for item in _mappings(payload.get("parameters")) + ), + particles=tuple( + CompiledParticleRecord( + name=str(item["name"]), + antiname=str(item["antiname"]), + pdg_code=_integer(item["pdg_code"]), + spin=_integer(item["spin"]), + color=_integer(item["color"]), + mass=str(item["mass"]), + width=str(item["width"]), + charge=_floating(item["charge"]), + quantum_numbers=validate_quantum_number_flow( + item["quantum_numbers"], + context=f"particle {str(item['name'])!r}", + ), + ghost_number=_integer(item["ghost_number"]), + propagating=bool(item["propagating"]), + goldstoneboson=bool(item["goldstoneboson"]), + propagator=_optional_string(item.get("propagator")), + component_dimension=cast( + int | None, item.get("component_dimension") + ), + auxiliary_kind=_optional_string(item.get("auxiliary_kind")), + statistics=str(item.get("statistics", "")), + wavefunction_family=str(item.get("wavefunction_family", "")), + color_role=str(item.get("color_role", "")), + self_conjugate=( + None + if item.get("self_conjugate") is None + else bool(item["self_conjugate"]) + ), + source_orientation=str(item.get("source_orientation", "")), + ) + for item in _mappings(payload.get("particles")) + ), + couplings=tuple( + CompiledCouplingRecord( + name=str(item["name"]), + expression=str(item["expression"]), + resolved_expression=str(item["resolved_expression"]), + value=_optional_pair(item.get("value")), + orders=_orders(item.get("orders")), + ) + for item in _mappings(payload.get("couplings")) + ), + propagators=tuple( + CompiledPropagatorRecord( + name=str(item["name"]), + particle=str(item["particle"]), + numerator=str(item["numerator"]), + denominator=str(item["denominator"]), + custom=bool(item["custom"]), + ) + for item in _mappings(payload.get("propagators")) + ), + vertex_terms=tuple( + CompiledVertexTerm( + id=_integer(item["id"]), + vertex=str(item["vertex"]), + particles=tuple( + str(value) for value in _sequence(item["particles"]) + ), + color_index=_integer(item["color_index"]), + lorentz_index=_integer(item["lorentz_index"]), + color_source=str(item["color_source"]), + color_expression=str(item["color_expression"]), + lorentz_name=str(item["lorentz_name"]), + lorentz_source=str(item["lorentz_source"]), + lorentz_expression=str(item["lorentz_expression"]), + coupling=str(item["coupling"]), + coupling_expression=str(item["coupling_expression"]), + coupling_orders=_orders(item.get("coupling_orders")), + backend=str(item.get("backend", "ufo")), + lc_color_normalization_power=_integer( + item.get("lc_color_normalization_power", 0) + ), + contact_decomposition_proof=( + None + if item.get("contact_decomposition_proof") is None + else CompiledContactDecompositionProof.from_dict( + _strict_mapping( + item["contact_decomposition_proof"], + "contact decomposition proof", + ) + ) + ), + contact_orbit_certificate=( + None + if _required_field( + item, + "contact_orbit_certificate", + context="compiled vertex term", + ) + is None + else CompiledContactOrbitCertificate.from_dict( + _strict_mapping( + item["contact_orbit_certificate"], + "contact-orbit certificate", + ) + ) + ), + source_ordering_ids=tuple( + _strict_string(value, "vertex source ordering ID") + for value in _required_sequence_field( + item, + "source_ordering_ids", + context="compiled vertex term", + ) + ), + index_bindings=tuple( + TensorIndexBindingIR.from_json_dict( + _strict_mapping(value, "tensor index binding") + ) + for value in _required_sequence_field( + item, + "index_bindings", + context="compiled vertex term", + ) + ), + ) + for item in _mappings(payload.get("vertex_terms")) + ), + oriented_kernels=tuple( + CompiledOrientedKernel( + kind=_integer(item["kind"]), + term_id=_integer(item["term_id"]), + vertex=str(item["vertex"]), + particles=cast_tuple3(item["particles"]), + source_particle_legs=cast_int_tuple3(item["source_particle_legs"]), + component_expressions=tuple( + str(value) for value in _sequence(item["component_expressions"]) + ), + coupling_expression=str(item["coupling_expression"]), + coupling_orders=_orders(item.get("coupling_orders")), + runtime_parameters=tuple( + str(value) for value in _sequence(item["runtime_parameters"]) + ), + color_source=str( + item.get("color_source", item["color_expression"]) + ), + color_expression=str(item["color_expression"]), + color_projection_structure=_optional_string( + item.get("color_projection_structure") + ), + color_projection_coefficient=( + None + if item.get("color_projection_coefficient") is None + else _pair(item.get("color_projection_coefficient")) + ), + lc_color_normalization_power=_integer( + item.get("lc_color_normalization_power", 0) + ), + term_ids=tuple( + _integer(value) + for value in _sequence( + item.get("term_ids", [_integer(item["term_id"])]) + ) + ), + evaluation_class=str( + item.get( + "evaluation_class", + f"unverified-kernel-{_integer(item['kind'])}", + ) + ), + evaluation_factor=_pair(item.get("evaluation_factor", (1.0, 0.0))), + evaluation_input_order=cast_int_tuple2( + item.get("evaluation_input_order", (0, 1)) + ), + evaluation_input_exchange_factor=( + None + if item.get("evaluation_input_exchange_factor") is None + else _pair(item.get("evaluation_input_exchange_factor")) + ), + evaluation_equivalence_verified=bool( + item.get("evaluation_equivalence_verified", False) + ), + input_ordering_ids=tuple( + _strict_string(value, "kernel input ordering ID") + for value in _required_sequence_field( + item, + "input_ordering_ids", + context="compiled oriented kernel", + ) + ), + output_ordering_id=_strict_string( + _required_field( + item, + "output_ordering_id", + context="compiled oriented kernel", + ), + "kernel output ordering ID", + ), + lc_recurrence_color_rule_kind=_optional_string( + item.get("lc_recurrence_color_rule_kind") + ), + lc_color_transition_terms=( + _compiled_lc_color_transition_terms( + item.get("lc_color_transition_terms", ()) + ) + ), + lc_color_closure_terms=( + _compiled_lc_color_closure_terms( + item.get("lc_color_closure_terms", ()) + ) + ), + contact_orbit_steps=tuple( + CompiledContactOrbitStep.from_dict(step) + for step in _mappings( + _required_field( + item, + "contact_orbit_steps", + context="compiled oriented kernel", + ) + ) + ), + ) + for item in _mappings(payload.get("oriented_kernels")) + ), + direct_contractions=tuple( + CompiledDirectContractionRecord.from_dict(item) + for item in _required_mappings(payload, "direct_contractions") + ), + closure_contractions=tuple( + CompiledClosureContractionRecord.from_dict(item) + for item in _required_mappings(payload, "closure_contractions") + ), + tensor_orderings=tuple( + TensorOrderingIR.from_json_dict(item) + for item in _required_mappings(payload, "tensor_orderings") + ), + current_orderings=tuple( + CompiledCurrentOrderingRecord.from_json_dict(item) + for item in _required_mappings(payload, "current_orderings") + ), + goldstone_partners=tuple( + CompiledGoldstonePartnerRecord.from_dict(item) + for item in _required_mappings(payload, "goldstone_partners") + ), + ) + + +def compiled_particle_is_chiral_eligible( + particle: CompiledParticleRecord, + *, + parameters: Mapping[str, CompiledParameterRecord], + propagators: Mapping[str, CompiledPropagatorRecord], +) -> bool: + """Return whether compilation proved a two-component Weyl current valid.""" + + if ( + particle.statistics != "fermion" + or particle.wavefunction_family != "fermion" + or particle.self_conjugate + or not particle.propagating + ): + return False + try: + component_dimension = compiled_particle_component_dimension(particle) + except ValueError: + return False + if component_dimension != 4: + return False + if particle.propagator is not None: + propagator = propagators.get(particle.propagator) + if propagator is None or propagator.custom: + return False + if particle.mass.upper() == "ZERO": + return True + parameter = parameters.get(particle.mass) + if parameter is None or parameter.nature.lower() == "external": + return False + from . import compiler_symbolica as _sym + + _sym._ensure_symbolica() + return ( + _sym.E(parameter.resolved_expression).expand().to_canonical_string() + == _sym.E("0").to_canonical_string() + ) + + +def compiled_current_dimension( + particle: CompiledParticleRecord, + chirality: int, + *, + parameters: Mapping[str, CompiledParameterRecord], + propagators: Mapping[str, CompiledPropagatorRecord], +) -> int: + """Resolve one concrete compiled current state without model heuristics.""" + + value = _validate_chirality(chirality, "chirality") + if value != 0: + if value not in {-1, 1}: + raise ValueError(f"unsupported concrete chirality {value}") + if not compiled_particle_is_chiral_eligible( + particle, + parameters=parameters, + propagators=propagators, + ): + raise ValueError( + f"particle {particle.name!r} does not support projected Weyl currents" + ) + return 2 + return compiled_particle_component_dimension(particle) + + +def _validate_concrete_chirality_relation( + contraction: ContractionIR, + left_chirality: int, + right_chirality: int, + *, + context: str, +) -> None: + relation = contraction.chirality_relation + if relation == "equal" and left_chirality != right_chirality: + raise ValueError(f"{context} violates equal chirality relation") + if relation == "opposite" and ( + left_chirality == 0 + or right_chirality == 0 + or left_chirality != -right_chirality + ): + raise ValueError(f"{context} violates opposite chirality relation") + + +def _validate_particle_selector_name(value: object, context: str) -> str: + if not isinstance(value, str): + raise TypeError(f"compiled contraction {context} must be a string") + if not value: + raise ValueError(f"compiled contraction {context} must not be empty") + return value + + +def _validate_chirality(value: object, context: str) -> int: + if isinstance(value, bool) or not isinstance(value, int): + raise TypeError(f"compiled contraction {context} must be an integer") + return value + + +def _strict_record_fields( + payload: Mapping[str, object], + *, + required: set[str], + context: str, +) -> Mapping[str, object]: + if not isinstance(payload, Mapping): + raise TypeError(f"{context} must be a mapping") + fields = set(payload) + missing = required - fields + if missing: + names = ", ".join(sorted(missing)) + raise ValueError(f"{context} is missing required fields: {names}") + unknown = fields - required + if unknown: + names = ", ".join(sorted(str(name) for name in unknown)) + raise ValueError(f"{context} has unknown fields: {names}") + return payload + + +def _strict_mapping(value: object, context: str) -> Mapping[str, object]: + if not isinstance(value, Mapping): + raise TypeError(f"{context} must be a mapping") + return value + + +def _strict_string(value: object, context: str) -> str: + if not isinstance(value, str): + raise TypeError(f"{context} must be a string") + return value + + +def _strict_integer(value: object, context: str) -> int: + if isinstance(value, bool) or not isinstance(value, int): + raise TypeError(f"{context} must be an integer") + return value + + +def _required_mappings( + payload: Mapping[str, object], + field: str, +) -> tuple[Mapping[str, object], ...]: + if field not in payload: + raise ValueError(f"compiled model is missing required field {field!r}") + value = payload[field] + if isinstance(value, (str, bytes)) or not isinstance(value, list | tuple): + raise TypeError(f"compiled model field {field!r} must be an array") + result: list[Mapping[str, object]] = [] + for index, item in enumerate(value): + if not isinstance(item, Mapping): + raise TypeError( + f"compiled model field {field!r} item {index} must be a mapping" + ) + result.append(item) + return tuple(result) + + +def _required_field( + payload: Mapping[str, object], + field: str, + *, + context: str, +) -> object: + if field not in payload: + raise ValueError(f"{context} is missing required field {field!r}") + return payload[field] + + +def _required_sequence_field( + payload: Mapping[str, object], + field: str, + *, + context: str, +) -> tuple[object, ...]: + value = _required_field(payload, field, context=context) + if isinstance(value, (str, bytes)) or not isinstance(value, list | tuple): + raise TypeError(f"{context} field {field!r} must be an array") + return tuple(value) + + +def cast_tuple3(value: object) -> tuple[str, str, str]: + values = tuple(str(item) for item in _sequence(value)) + if len(values) != 3: + raise ValueError("oriented kernel particles must have length three") + return values[0], values[1], values[2] + + +def cast_int_tuple3(value: object) -> tuple[int, int, int]: + values = tuple(_integer(item) for item in _sequence(value)) + if len(values) != 3: + raise ValueError("oriented kernel source legs must have length three") + return values[0], values[1], values[2] + + +def cast_int_tuple2(value: object) -> tuple[int, int]: + values = tuple(_integer(item) for item in _sequence(value)) + if len(values) != 2: + raise ValueError("evaluation input order must have length two") + return values[0], values[1] + + +def _orders(value: object) -> tuple[tuple[str, int], ...]: + result: list[tuple[str, int]] = [] + for pair in _sequence(value): + values = _sequence(pair) + if len(values) != 2: + raise ValueError("coupling order must be [name, value]") + result.append((str(values[0]), _integer(values[1]))) + return tuple(result) + + +def _pair(value: object) -> tuple[float, float]: + pair = _sequence(value) + if len(pair) != 2: + raise ValueError("complex value must be [real, imaginary]") + return _floating(pair[0]), _floating(pair[1]) + + +def _optional_pair(value: object) -> tuple[float, float] | None: + return None if value is None else _pair(value) + + +def _optional_string(value: object) -> str | None: + return None if value is None else str(value) + + +def _integer(value: object) -> int: + if not isinstance(value, str | int | float): + raise ValueError(f"expected an integer-compatible value, got {value!r}") + return int(value) + + +def _floating(value: object) -> float: + if not isinstance(value, str | int | float): + raise ValueError(f"expected a numeric value, got {value!r}") + return float(value) + + +def _sequence(value: object) -> list[object]: + return list(value) if isinstance(value, list | tuple) else [] + + +def _mappings(value: object) -> list[dict[str, object]]: + return [dict(item) for item in _sequence(value) if isinstance(item, Mapping)] + + +def _compiled_lc_color_transition_terms( + value: object, +) -> tuple[CompiledLCColorTransitionTerm, ...]: + if isinstance(value, (str, bytes)) or not isinstance(value, list | tuple): + raise TypeError( + "compiled oriented-kernel LC color transition terms must be an array" + ) + return tuple( + CompiledLCColorTransitionTerm.from_dict( + _strict_mapping(item, "compiled LC color transition term") + ) + for item in value + ) + + +def _compiled_lc_color_closure_terms( + value: object, +) -> tuple[CompiledLCColorTransitionTerm, ...]: + if isinstance(value, (str, bytes)) or not isinstance(value, list | tuple): + raise TypeError( + "compiled oriented-kernel LC color closure terms must be an array" + ) + return tuple( + CompiledLCColorTransitionTerm.from_dict( + _strict_mapping(item, "compiled LC color closure term") + ) + for item in value + ) + + +__all__ = [ + "SUPPORTED_COLOR_REPRESENTATIONS", + "TENSOR_ORDERING_CONTRACT_VERSION", + "CompiledClosureContractionRecord", + "CompiledContactDecompositionProof", + "CompiledContactDecompositionSplit", + "CompiledContactDummyIndexMapping", + "CompiledContactOrientationProof", + "CompiledContactUnsupportedReason", + "CompiledCouplingOrder", + "CompiledCouplingRecord", + "CompiledCurrentOrderingRecord", + "CompiledDirectContractionRecord", + "CompiledLCColorTransitionTerm", + "CompiledModelIR", + "CompiledOrientedKernel", + "CompiledParameterRecord", + "CompiledParticleRecord", + "CompiledPropagatorRecord", + "CompiledVertexTerm", + "TensorAxisIR", + "TensorIndexBindingIR", + "TensorOrderingIR", + "compiled_current_dimension", + "compiled_particle_component_dimension", + "compiled_particle_is_chiral_eligible", + "validate_color_representation", + "validate_quantum_number_flow", +] diff --git a/pyamplicol/src/pyamplicol/models/expressions.py b/pyamplicol/src/pyamplicol/models/expressions.py new file mode 100644 index 0000000000..0439276cde --- /dev/null +++ b/pyamplicol/src/pyamplicol/models/expressions.py @@ -0,0 +1,225 @@ +# SPDX-License-Identifier: 0BSD +from __future__ import annotations + +from collections.abc import Sequence +from functools import lru_cache +from numbers import Number +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from .base import CouplingOrders, Model, Vertex + + +def _number(value: complex | float) -> Any: + from symbolica import Expression + + return Expression.num(value) + + +@lru_cache(maxsize=128) +def _exact_complex_expression(value: complex) -> Any: + """Represent a stored binary64 weight without rounding symbolic factors. + + In particular, multiplying a rational coefficient by a Python complex + unit must not turn that coefficient into a floating-point approximation. + """ + from symbolica import E + + real_numerator, real_denominator = value.real.as_integer_ratio() + imag_numerator, imag_denominator = value.imag.as_integer_ratio() + return E( + f"({real_numerator}/{real_denominator})" + f"+1i*({imag_numerator}/{imag_denominator})" + ) + + +def _imaginary_unit_for(values: Sequence[Any]) -> Any: + """Keep symbolic propagator phases exact without changing numeric inputs.""" + if all(isinstance(value, Number) for value in values): + return 1j + from symbolica import Expression + + if any(isinstance(value, Expression) for value in values): + return _exact_complex_expression(1j) + return 1j + + +def _as_expression(value: Any) -> Any: + if isinstance(value, int | float | complex): + return _number(value) + return value + + +def _minkowski_square_expression(momentum: Sequence[Any]) -> Any: + if len(momentum) != 4: + raise ValueError("Minkowski momentum needs four components") + if all(isinstance(value, int | float | complex) for value in momentum): + p0, p1, p2, p3 = momentum + return p0 * p0 - p1 * p1 - p2 * p2 - p3 * p3 + p0, p1, p2, p3 = (_as_expression(value) for value in momentum) + return p0 * p0 - p1 * p1 - p2 * p2 - p3 * p3 + + +def _model_vertex_result_chiralities( + model: Model, + vertex: Vertex, + left_index: Any, + right_index: Any, +) -> tuple[int, ...]: + resolver = getattr(model, "_vertex_result_chiralities", None) + if resolver is None: + return (0,) + return tuple(resolver(vertex, left_index, right_index)) + + +def _index_particle_id(index: Any) -> int: + if hasattr(index, "particle_id"): + return int(index.particle_id) + return int(index.pdg) + + +def _index_flavour_flow(index: Any) -> tuple[int, ...]: + flow = getattr(index, "flavour_flow", None) + if flow is None: + return (_index_particle_id(index),) + return tuple(int(value) for value in flow) + + +def _index_coupling_orders(index: Any) -> CouplingOrders: + orders = getattr(index, "coupling_orders", None) + if orders is None: + return () + return tuple( + sorted( + (str(name).upper(), int(value)) for name, value in orders if int(value) != 0 + ) + ) + + +def _append_flavour_transition( + flow: tuple[int, ...], + result_particle: int, +) -> tuple[int, ...]: + if flow and flow[-1] == result_particle: + return flow + return (*flow, result_particle) + + +def _expr_fermion_propagator_weyl( + fermion: tuple[Any, ...], + momentum: tuple[Any, ...], + chirality: int, +) -> tuple[Any, ...]: + energy, px, py, pz = momentum + denominator = _minkowski_square_expression(momentum) + imaginary = _imaginary_unit_for((*fermion, *momentum)) + prefactor = imaginary / denominator + tmp1 = energy + pz + tmp2 = energy - pz + tmp3 = px + imaginary * py + tmp4 = px - imaginary * py + f1, f2 = fermion + if chirality == 1: + return ( + (tmp1 * f1 + tmp3 * f2) * prefactor, + (tmp2 * f2 + tmp4 * f1) * prefactor, + ) + if chirality == -1: + return ( + (tmp2 * f1 - tmp3 * f2) * prefactor, + (tmp1 * f2 - tmp4 * f1) * prefactor, + ) + raise ValueError("Weyl fermion propagator expression needs nonzero chirality") + + +def _expr_antifermion_propagator_weyl( + antifermion: tuple[Any, ...], + momentum: tuple[Any, ...], + chirality: int, +) -> tuple[Any, ...]: + energy, px, py, pz = momentum + denominator = _minkowski_square_expression(momentum) + imaginary = _imaginary_unit_for((*antifermion, *momentum)) + prefactor = imaginary / denominator + tmp1 = -(energy + pz) + tmp2 = -(energy - pz) + tmp3 = -(px + imaginary * py) + tmp4 = -(px - imaginary * py) + a1, a2 = antifermion + if chirality == 1: + return ( + (tmp2 * a1 - tmp4 * a2) * prefactor, + (tmp1 * a2 - tmp3 * a1) * prefactor, + ) + if chirality == -1: + return ( + (tmp1 * a1 + tmp4 * a2) * prefactor, + (tmp2 * a2 + tmp3 * a1) * prefactor, + ) + raise ValueError("Weyl antifermion propagator expression needs nonzero chirality") + + +def _expr_fermion_propagator_dirac( + fermion: tuple[Any, ...], + momentum: tuple[Any, ...], + mass: float, + width: float, +) -> tuple[Any, ...]: + if len(fermion) != 4 or len(momentum) != 4: + raise ValueError("Dirac fermion propagator expects four components") + energy, px, py, pz = momentum + imaginary = _imaginary_unit_for((*fermion, *momentum, mass, width)) + denominator = ( + _minkowski_square_expression(momentum) - mass * mass + imaginary * mass * width + ) + prefactor = imaginary / denominator + tmp1 = energy + pz + tmp2 = energy - pz + tmp3 = px + imaginary * py + tmp4 = px - imaginary * py + f1, f2, f3, f4 = fermion + return ( + (tmp1 * f3 + tmp3 * f4 + mass * f1) * prefactor, + (tmp2 * f4 + tmp4 * f3 + mass * f2) * prefactor, + (tmp2 * f1 - tmp3 * f2 + mass * f3) * prefactor, + (tmp1 * f2 - tmp4 * f1 + mass * f4) * prefactor, + ) + + +def _expr_antifermion_propagator_dirac( + antifermion: tuple[Any, ...], + momentum: tuple[Any, ...], + mass: float, + width: float, +) -> tuple[Any, ...]: + if len(antifermion) != 4 or len(momentum) != 4: + raise ValueError("Dirac antifermion propagator expects four components") + energy, px, py, pz = momentum + imaginary = _imaginary_unit_for((*antifermion, *momentum, mass, width)) + denominator = ( + _minkowski_square_expression(momentum) - mass * mass + imaginary * mass * width + ) + prefactor = imaginary / denominator + tmp1 = -(energy + pz) + tmp2 = -(energy - pz) + tmp3 = -(px + imaginary * py) + tmp4 = -(px - imaginary * py) + a1, a2, a3, a4 = antifermion + return ( + (tmp2 * a3 - tmp4 * a4 + mass * a1) * prefactor, + (tmp1 * a4 - tmp3 * a3 + mass * a2) * prefactor, + (tmp1 * a1 + tmp4 * a2 + mass * a3) * prefactor, + (tmp2 * a2 + tmp3 * a1 + mass * a4) * prefactor, + ) + + +def _expr_minkowski_dot( + left: tuple[Any, ...], + right: tuple[Any, ...], +) -> Any: + return ( + left[0] * right[0] + - left[1] * right[1] + - left[2] * right[2] + - left[3] * right[3] + ) diff --git a/pyamplicol/src/pyamplicol/models/external.py b/pyamplicol/src/pyamplicol/models/external.py new file mode 100644 index 0000000000..10b9d5f360 --- /dev/null +++ b/pyamplicol/src/pyamplicol/models/external.py @@ -0,0 +1,199 @@ +# SPDX-License-Identifier: 0BSD +from __future__ import annotations + +from collections.abc import Mapping +from copy import copy +from typing import TYPE_CHECKING, Any + +from .._internal.physics.symbols import symbols +from .base import ( + Model, + Particle, + Vertex, +) +from .contracts import validate_color_representation + +if TYPE_CHECKING: + from .loading import CompiledModel + +from . import compiler_symbolica as _sym +from .external_catalog import ExternalModelCatalogMixin +from .external_evaluation import ExternalModelEvaluationMixin +from .external_helpers import _spin_dimension +from .external_kernels import ExternalModelKernelMixin +from .external_symmetries import derive_external_symmetry_certificates + + +class CompiledUFOModel( + ExternalModelCatalogMixin, + ExternalModelEvaluationMixin, + ExternalModelKernelMixin, + Model, +): + """Compiled-UFO model boundary consumed by generic DAG generation.""" + + def __init__( + self, + compiled: CompiledModel, + runtime_parameters: Mapping[str, Any] | None = None, + ) -> None: + _sym._ensure_symbolica() + self.compiled = compiled + self.name = compiled.name + self._model_symbols = symbols.model(self.name) + self._particle_records_by_name = { + particle.name: particle for particle in compiled.ir.particles + } + self._particle_records_by_pdg = { + particle.pdg_code: particle for particle in compiled.ir.particles + } + self._direct_contraction_ir_by_state = { + ( + self._particle_records_by_name[record.left_particle].pdg_code, + record.left_chirality, + self._particle_records_by_name[record.right_particle].pdg_code, + record.right_chirality, + ): record.contraction_ir + for record in compiled.ir.direct_contractions + } + self._closure_contraction_ir_by_state = { + ( + self._particle_records_by_name[record.particle].pdg_code, + record.chirality, + ): record.contraction_ir + for record in compiled.ir.closure_contractions + } + for particle in compiled.ir.particles: + validate_color_representation( + particle.color, + context=f"particle {particle.name!r}", + ) + self._parameter_records = { + parameter.name: parameter for parameter in compiled.ir.parameters + } + self._propagator_records_by_particle_name = { + propagator.particle: propagator for propagator in compiled.ir.propagators + } + self._vertex_terms = {term.id: term for term in compiled.ir.vertex_terms} + self._coupling_records_by_name = { + coupling.name: coupling for coupling in compiled.ir.couplings + } + defaults = { + name: complex(value[0], value[1]) + for name, value in compiled.parameter_defaults.items() + } + self._runtime_parameters = {**defaults, **dict(runtime_parameters or {})} + self.particles = { + record.pdg_code: Particle( + pdg=record.pdg_code, + anti_pdg=self._particle_records_by_name[record.antiname].pdg_code, + spin=record.spin, + dimension=( + record.component_dimension + if record.component_dimension is not None + else _spin_dimension(record.spin) + ), + color_rep=record.color, + mass=float(complex(self._parameter_default(record.mass)).real), + width=float(complex(self._parameter_default(record.width)).real), + charge=record.charge, + ) + for record in compiled.ir.particles + } + self._kernels = {kernel.kind: kernel for kernel in compiled.ir.oriented_kernels} + self._symmetry_certificates = derive_external_symmetry_certificates(compiled.ir) + self._kernel_component_expression_cache: dict[ + int, tuple[_sym.Expression, ...] + ] = {} + self._kernel_coupling_expression_cache: dict[int, _sym.Expression] = {} + self._kernel_function_specs: dict[ + tuple[int, int, int, int], + tuple[tuple[_sym.Expression, tuple[int, ...]], ...], + ] = {} + self._symbolica_kernel_functions: dict[ + tuple[_sym.Expression, tuple[_sym.Expression, ...]], _sym.Expression + ] = {} + self._runtime_derived_definitions = { + parameter.name: parameter.resolved_expression + for parameter in compiled.ir.parameters + if parameter.nature.lower() == "internal" + and parameter.expression is not None + } + self._runtime_derived_definitions.update( + { + name: self._vertex_terms[ + int(name.rsplit("_", 1)[1]) + ].coupling_expression + for kernel in self._kernels.values() + for name in kernel.runtime_parameters + if name.startswith("derived_coupling_") + } + ) + self._runtime_derived_expression_cache: dict[str, _sym.Expression] = {} + self._runtime_derived_default_cache: dict[str, complex] = {} + self._runtime_derived_domain_cache: dict[str, str] = {} + self._runtime_parameter_domain_cache: dict[str, str] = {} + self._weyl_projection_support: dict[tuple[int, int, int, int], bool] = {} + self._custom_propagator_expressions: dict[ + int, + tuple[_sym.Expression, tuple[str, ...]], + ] = {} + self._custom_propagator_templates: dict[int, tuple[_sym.Expression, ...]] = {} + self._color_projection_cache: dict[int, tuple[str, complex]] = {} + self._goldstone_partner_records = { + record.goldstone: record for record in compiled.ir.goldstone_partners + } + for record in compiled.ir.goldstone_partners: + if record.vector is None: + continue + vector = self._particle_records_by_name[record.vector] + propagator = self._propagator_ir(vector.pdg_code) + if propagator.goldstone_policy != record.policy: + raise ValueError( + f"compiled Goldstone {record.goldstone!r} policy does not " + f"match vector {record.vector!r} lowering" + ) + self.inactive_goldstone_names = frozenset( + record.goldstone + for record in compiled.ir.goldstone_partners + if record.policy == "absorbed" + ) + self.vertices = tuple( + Vertex( + kind=kernel.kind, + particles=( + self._particle_records_by_name[kernel.particles[0]].pdg_code, + self._particle_records_by_name[kernel.particles[1]].pdg_code, + self._particle_records_by_name[kernel.particles[2]].pdg_code, + ), + coupling=(1.0, 0.0), + ) + for kernel in compiled.ir.oriented_kernels + if not any( + name in self.inactive_goldstone_names for name in kernel.particles + ) + ) + vertices_by_input: dict[tuple[int, int], list[Vertex]] = {} + for vertex in self.vertices: + vertices_by_input.setdefault( + (vertex.particles[0], vertex.particles[1]), + [], + ).append(vertex) + self._compiled_vertices_by_input = { + key: tuple(vertices) for key, vertices in vertices_by_input.items() + } + + def with_runtime_parameters( + self, + parameters: Mapping[str, Any], + ) -> CompiledUFOModel: + model = copy(self) + model._runtime_parameters = { + **self._runtime_parameters, + **dict(parameters), + } + model._runtime_derived_default_cache = {} + return model + + +__all__ = ["CompiledUFOModel"] diff --git a/pyamplicol/src/pyamplicol/models/external_catalog.py b/pyamplicol/src/pyamplicol/models/external_catalog.py new file mode 100644 index 0000000000..9da0fecc38 --- /dev/null +++ b/pyamplicol/src/pyamplicol/models/external_catalog.py @@ -0,0 +1,599 @@ +# SPDX-License-Identifier: 0BSD +from __future__ import annotations + +from collections.abc import Mapping, Sequence +from typing import TYPE_CHECKING, Any, cast + +from ._physics_ir import ParticleIdentityIR, ParticleOrientation +from .base import ( + Particle, + QuantumFlow, + QuantumNumberFlow, + RecurrenceCurrentEmbeddingContract, + RecurrenceLCColorComponentKind, + RecurrenceLCColorOperation, + RecurrenceLCColorShapeKind, + RecurrenceLCColorSourceSeedContract, + RecurrenceLCColorTransitionContract, + RecurrenceLCColorWitnessContract, + RecurrenceQuantumFlowContract, + SourceSpinState, + Vertex, + VertexEvaluationEquivalence, + VertexLoweringRule, +) +from .contact_decomposition import ( + CompiledContactOrbitCertificate, + CompiledContactOrbitStep, +) + +if TYPE_CHECKING: + pass + +from .external_helpers import _spin_dimension + + +class ExternalModelCatalogMixin: + def particle(self, pdg: int) -> Particle: + try: + return self.particles[int(pdg)] + except KeyError as exc: + raise KeyError(f"particle not in model: {pdg}") from exc + + def vertices_for_inputs( + self, + left_pdg: int, + right_pdg: int, + *, + color_accuracy: str = "lc", + ) -> tuple[Vertex, ...]: + if color_accuracy not in {"lc", "nlc", "full"}: + raise ValueError(f"unknown colour accuracy: {color_accuracy}") + return self._compiled_vertices_by_input.get( + (int(left_pdg), int(right_pdg)), + (), + ) + + def anti_particle(self, pdg: int) -> int: + return self.particle(pdg).anti_pdg + + def mass(self, pdg: int) -> Any: + return self._real_parameter_value( + self._particle_records_by_pdg[int(pdg)].mass, + field="mass", + ) + + def width(self, pdg: int) -> Any: + return self._real_parameter_value( + self._particle_records_by_pdg[int(pdg)].width, + field="width", + ) + + def spin(self, pdg: int) -> int: + return self._particle_records_by_pdg[int(pdg)].spin + + def dimension(self, pdg: int) -> int: + record = self._particle_records_by_pdg[int(pdg)] + if record.component_dimension is not None: + return record.component_dimension + return _spin_dimension(record.spin) + + def current_dimension(self, particle_id: int, chirality: int = 0) -> int: + if chirality != 0 and self.is_chiral_eligible(particle_id): + return 2 + return self.dimension(particle_id) + + def color_rep(self, pdg: int) -> int: + return self._particle_records_by_pdg[int(pdg)].color + + def color_dim(self, pdg: int) -> int: + return abs(self.color_rep(pdg)) + + def charge(self, pdg: int) -> float: + return self._particle_records_by_pdg[int(pdg)].charge + + def quantum_number_flow(self, particle_id: int) -> QuantumNumberFlow: + return self._particle_records_by_pdg[int(particle_id)].quantum_numbers + + def is_fermion(self, pdg: int) -> bool: + return self._particle_records_by_pdg[int(pdg)].statistics == "fermion" + + def is_chiral_eligible(self, pdg: int) -> bool: + if not self.is_fermion(pdg): + return False + propagator = self._propagator_record(pdg) + if propagator is not None and propagator.custom: + return False + particle = self._particle_records_by_pdg[int(pdg)] + if particle.mass.upper() == "ZERO": + return True + record = self._parameter_records.get(particle.mass) + return ( + record is not None + and record.nature != "external" + and self._parameter_default(particle.mass) == 0.0 + ) + + def is_fundamental_colored_fermion(self, pdg: int) -> bool: + return self.is_fermion(pdg) and abs(self.color_rep(pdg)) == 3 + + def is_massless_adjoint_vector(self, pdg: int) -> bool: + return ( + self.spin(pdg) == 3 + and self.color_rep(pdg) == 8 + and complex( + self._parameter_default(self._particle_records_by_pdg[int(pdg)].mass) + ).real + == 0.0 + ) + + def is_singlet(self, pdg: int) -> bool: + return self.color_rep(pdg) == 1 + + def source_spin_states(self, particle_id: int) -> tuple[SourceSpinState, ...]: + if self.is_chiral_eligible(particle_id): + return super().source_spin_states(particle_id) + spin = self.spin(particle_id) + massive = ( + complex( + self._parameter_default( + self._particle_records_by_pdg[int(particle_id)].mass + ) + ).real + != 0.0 + ) + if spin == 1: + helicities = (0,) + elif spin == 2: + helicities = (-1, 1) + elif spin == 3: + helicities = (-1, 0, 1) if massive else (-1, 1) + elif spin == 5: + helicities = (-2, -1, 0, 1, 2) if massive else (-2, 2) + else: + raise ValueError( + f"unsupported source spin {spin} for particle {particle_id}" + ) + return tuple( + SourceSpinState(helicity=helicity, chirality=0, spin_state=helicity) + for helicity in helicities + ) + + def source_wavefunction_kind(self, particle_id: int) -> str: + return self._particle_records_by_pdg[int(particle_id)].wavefunction_family + + def source_orientation(self, particle_id: int) -> str: + record = self._particle_records_by_pdg[int(particle_id)] + if record.statistics == "fermion" and record.self_conjugate: + raise ValueError( + f"unsupported self-conjugate fermion source {particle_id}: " + "Majorana/FNV source wavefunctions are not implemented" + ) + return record.source_orientation + + def _particle_identity_ir(self, particle_id: int) -> ParticleIdentityIR: + record = self._particle_records_by_pdg[int(particle_id)] + anti_record = self._particle_records_by_name[record.antiname] + species_record = ( + record + if record.source_orientation in {"particle", "self-conjugate"} + else anti_record + ) + return ParticleIdentityIR( + canonical_id=f"model:{self.name}:state:{record.name}", + species_id=f"model:{self.name}:species:{species_record.name}", + anti_canonical_id=f"model:{self.name}:state:{anti_record.name}", + display_name=record.name, + anti_display_name=anti_record.name, + pdg_label=int(record.pdg_code), + anti_pdg_label=int(anti_record.pdg_code), + orientation=cast(ParticleOrientation, record.source_orientation), + self_conjugate=bool(record.self_conjugate), + ) + + def allowed_quantum_flows( + self, + vertex: Vertex, + left_index: Any, + right_index: Any, + ) -> tuple[QuantumFlow, ...]: + result_particle = vertex.particles[2] + left_chirality = int(getattr(left_index, "chirality", 0)) + right_chirality = int(getattr(right_index, "chirality", 0)) + result_chiralities = ( + (0,) + if ( + self.is_chiral_eligible(result_particle) + and left_chirality == 0 + and right_chirality == 0 + ) + else ( + (-1, 1) + if self.is_chiral_eligible(result_particle) + else (0,) + ) + ) + return tuple( + QuantumFlow( + chirality=result_chirality, + spin_state=( + result_chirality if self.is_chiral_eligible(result_particle) else 0 + ), + flavour_flow=self.combine_flavour_flow( + result_particle, + left_index, + right_index, + ), + quantum_number_flow=self.quantum_number_flow(result_particle), + coupling=(1.0, 0.0), + ) + for result_chirality in result_chiralities + if self._weyl_projection_is_nonzero( + vertex.kind, + left_chirality, + right_chirality, + result_chirality, + ) + ) + + def recurrence_quantum_flow_contract( + self, + vertex: Vertex, + left_particle_id: int, + right_particle_id: int, + ) -> RecurrenceQuantumFlowContract: + return self._standard_recurrence_quantum_flow_contract( + vertex, + left_particle_id, + right_particle_id, + ) + + def recurrence_current_embedding_contract( + self, + particle_id: int, + source_chirality: int, + ) -> RecurrenceCurrentEmbeddingContract: + particle = self._particle_records_by_pdg[int(particle_id)] + chirality = int(source_chirality) + ordering = next( + ( + record + for record in self.compiled.ir.current_orderings + if record.particle == particle.name + and int(record.chirality) == chirality + ), + None, + ) + if ordering is None: + raise NotImplementedError( + "compiled model has no current-ordering contract for " + f"particle={particle.name!r}, chirality={chirality:+d}; " + "re-run the original `pyamplicol model compile ...` command " + "with the current pyAmpliCol compiler" + ) + return RecurrenceCurrentEmbeddingContract( + input_embedding=tuple(ordering.input_embedding), + source_dimension=int(self.current_dimension(particle_id, chirality)), + full_dimension=int(self.current_dimension(particle_id, 0)), + source_basis=self._current_basis(particle_id, chirality), + full_basis=self._current_basis(particle_id, 0), + proof_kind="compiled-current-ordering-embedding-v1", + provenance=tuple( + sorted( + ( + ("chirality", f"{chirality:+d}"), + ("kernel-ordering-id", ordering.kernel_ordering_id), + ("ordering-id", ordering.ordering_id), + ("particle", particle.name), + ) + ) + ), + ) + + def recurrence_lc_color_shape_contract( + self, + particle_id: int, + chirality: int = 0, + ) -> RecurrenceLCColorShapeKind: + return self._standard_recurrence_lc_color_shape_contract( + particle_id, + chirality, + ) + + def recurrence_lc_source_color_contract( + self, + particle_id: int, + chirality: int = 0, + ) -> RecurrenceLCColorSourceSeedContract: + return self._standard_recurrence_lc_source_color_contract( + particle_id, + chirality, + ) + + def recurrence_lc_color_transition_contract( + self, + vertex: Vertex, + *, + closure: bool, + ) -> RecurrenceLCColorTransitionContract: + kernel = self._kernel(vertex.kind) + terms = ( + kernel.lc_color_closure_terms + if closure + else kernel.lc_color_transition_terms + ) + contract_kind = "closure" if closure else "transition" + if not terms: + raise NotImplementedError( + f"compiled kernel {kernel.kind} has no compiler-owned LC color " + f"{contract_kind} terms; re-run the original `pyamplicol model " + "compile ...` command with the current pyAmpliCol compiler" + ) + input_shapes = tuple( + self.recurrence_lc_color_shape_contract(particle) + for particle in vertex.particles[:2] + ) + result_shape = ( + None + if closure + else self.recurrence_lc_color_shape_contract(vertex.particles[2]) + ) + witnesses = [] + for term in terms: + if ( + term.input_shape_kinds != input_shapes + or term.result_shape_kind != result_shape + ): + raise ValueError( + f"compiled kernel {kernel.kind} LC color {contract_kind} term " + "does not match its live oriented particle-state shapes" + ) + if (term.component_operation == "close") != closure: + expectation = ( + "a closure operation" + if closure + else "a non-closure operation" + ) + raise ValueError( + f"compiled kernel {kernel.kind} LC color {contract_kind} term " + f"must contain {expectation}" + ) + factor = { + "1": (1.0, 0.0), + "-1": (-1.0, 0.0), + }.get(term.exact_factor_expression) + if factor is None: + raise NotImplementedError( + f"compiled kernel {kernel.kind} LC witness factor " + f"{term.exact_factor_expression!r} is not supported by " + "recurrence v1" + ) + witnesses.append( + RecurrenceLCColorWitnessContract( + input_permutation=term.input_permutation, + reverse_parent_mask=term.reverse_parent_mask, + component_operation=cast( + RecurrenceLCColorOperation, + term.component_operation, + ), + result_component_kind=cast( + RecurrenceLCColorComponentKind | None, + term.result_component_kind, + ), + result_component_role=term.result_component_role, + exact_factor=factor, + proof_digest=term.proof_digest, + provenance=term.provenance, + ) + ) + return RecurrenceLCColorTransitionContract( + str( + kernel.lc_recurrence_color_rule_kind + or kernel.color_projection_structure + ), + tuple(witnesses), + ) + + def vertex_lowering_rule(self, kind: int) -> VertexLoweringRule: + kernel = self._kernel(kind) + return VertexLoweringRule( + kind=kind, + backend="spenso-ufo", + tensor_names=(kernel.vertex,), + expression_head="compiled_ufo_kernel", + full_tensor_network_ready=True, + description="typed and oriented UFO tensor kernel", + kernel="compiled_ufo_kernel", + input_roles=(kernel.particles[0], kernel.particles[1]), + output_role=kernel.particles[2], + coupling_mode="external-model-parameters", + ) + + def vertex_evaluation_equivalence( + self, + kind: int, + ) -> VertexEvaluationEquivalence: + kernel = self._kernel(kind) + if not kernel.evaluation_equivalence_verified or not kernel.evaluation_class: + return super().vertex_evaluation_equivalence(kind) + input_order = tuple(int(value) for value in kernel.evaluation_input_order) + if input_order not in {(0, 1), (1, 0)}: + raise ValueError( + f"compiled UFO kernel {kind} has invalid evaluation input order " + f"{input_order}" + ) + return VertexEvaluationEquivalence( + class_id=kernel.evaluation_class, + factor=kernel.evaluation_factor, + input_order=input_order, + input_exchange_factor=(kernel.evaluation_input_exchange_factor), + verified=True, + ) + + def vertex_contact_orbit_contracts( + self, + kind: int, + ) -> tuple[ + tuple[CompiledContactOrbitCertificate, ...], + tuple[CompiledContactOrbitStep, ...], + ]: + kernel = self._kernel(kind) + steps = kernel.contact_orbit_steps + certificates = tuple( + self._vertex_terms[term_id].contact_orbit_certificate + for term_id in sorted({step.term_id for step in steps}) + ) + if any(certificate is None for certificate in certificates): + raise ValueError( + f"compiled kernel {kind} has an unpaired contact-orbit step" + ) + return ( + tuple( + certificate + for certificate in certificates + if certificate is not None + ), + steps, + ) + + def vertex_coupling_orders( + self, + vertex: Vertex, + ) -> tuple[tuple[str, int], ...]: + return self._kernel(vertex.kind).coupling_orders + + def global_helicity_flip_equivalence_is_proven( + self, + vertices: Sequence[Vertex], + ) -> bool: + """Accept parity reduction only for algebraically certified kernels.""" + + kinds = self._symmetry_certificates.parity_kernel_kinds + return bool(vertices) and all(vertex.kind in kinds for vertex in vertices) + + def pure_massless_adjoint_helicity_zero_rule_is_proven( + self, + process: Any, + vertices: Sequence[Vertex], + ) -> bool: + """Recognize the tree-level Yang--Mills helicity-zero theorem.""" + + names = self._symmetry_certificates.yang_mills_adjoint_names + kinds = self._symmetry_certificates.yang_mills_kernel_kinds + legs = tuple(getattr(process, "legs", ())) + return ( + bool(legs) + and bool(vertices) + and all(self._particle_name_for_leg(leg) in names for leg in legs) + and all(vertex.kind in kinds for vertex in vertices) + ) + + def adjoint_current_reflection_phase( + self, + vertex: Vertex, + ) -> tuple[float, float] | None: + """Return an exact lowered-kernel reflection certificate, if present.""" + + return dict(self._symmetry_certificates.adjoint_current_reflection_phases).get( + vertex.kind + ) + + def lc_trace_reflection_equivalence_is_proven(self, process: Any) -> bool: + """Recognize reflection folding for a certified Yang--Mills sector.""" + + names = self._symmetry_certificates.yang_mills_adjoint_names + legs = tuple(getattr(process, "legs", ())) + return bool(legs) and all( + self._particle_name_for_leg(leg) in names for leg in legs + ) + + def shared_single_trace_color_basis_is_proven(self, process: Any) -> bool: + """Accept shared traces only for the certified Yang--Mills sector.""" + + names = self._symmetry_certificates.yang_mills_adjoint_names + legs = tuple(getattr(process, "legs", ())) + return bool(legs) and all( + self._particle_name_for_leg(leg) in names for leg in legs + ) + + def adjoint_tree_color_basis_is_proven( + self, + process: Any, + *, + max_coupling_orders: Mapping[str, int] | None = None, + ) -> bool: + """Certify actual colour tensors without assuming Yang--Mills helicities.""" + + from .adjoint_color_certificate import ( + compiled_adjoint_tree_color_basis_is_proven, + ) + + return compiled_adjoint_tree_color_basis_is_proven( + self.compiled.ir, process, max_coupling_orders=max_coupling_orders + ) + + def _particle_name_for_leg(self, leg: Any) -> str | None: + outgoing_pdg = getattr(leg, "outgoing_pdg", None) + if outgoing_pdg is None: + return None + record = self._particle_records_by_pdg.get(int(outgoing_pdg)) + return None if record is None else record.name + + def coupling_order_hierarchies(self) -> dict[str, int]: + return { + str(order.name).upper(): max(1, int(order.hierarchy)) + for order in self.compiled.ir.orders + } + + def vertex_color_weight( + self, + vertex: Vertex, + *, + color_accuracy: str, + ) -> tuple[float, float]: + if color_accuracy not in {"lc", "nlc", "full"}: + raise ValueError(f"unknown colour accuracy: {color_accuracy}") + power = self._kernel(vertex.kind).lc_color_normalization_power + structure, coefficient = self._vertex_color_projection(vertex) + # The possibly irrational 2**(-power/2) belongs to the symbolic + # kernel. Recurrence colour weights are exact complex rationals. + if structure in { + "adjoint-structure-constant", + "adjoint-structure-constant-product", + }: + phase = (-1j) ** power + weight = coefficient * phase + return (weight.real, weight.imag) + return (coefficient.real, coefficient.imag) + + def vertex_color_structure(self, vertex: Vertex) -> str: + return self._vertex_color_projection(vertex)[0] + + def _vertex_color_projection(self, vertex: Vertex) -> tuple[str, complex]: + cached = self._color_projection_cache.get(vertex.kind) + if cached is not None: + return cached + kernel = self._kernel(vertex.kind) + if ( + kernel.color_projection_structure is not None + and kernel.color_projection_coefficient is not None + ): + projected = ( + kernel.color_projection_structure, + complex(*kernel.color_projection_coefficient), + ) + else: + raise ValueError( + f"compiled external-model kernel {kernel.kind} has no certified " + "color-flow projection; recompile the UFO/JSON model with the " + "current pyAmpliCol version" + ) + self._color_projection_cache[vertex.kind] = projected + return projected + + def vertex_is_internal_contact_fragment(self, vertex: Vertex) -> bool: + return "::contact-" in self._kernel(vertex.kind).vertex + + def vertex_closure_allowed(self, vertex: Vertex) -> bool: + del vertex + return False diff --git a/pyamplicol/src/pyamplicol/models/external_evaluation.py b/pyamplicol/src/pyamplicol/models/external_evaluation.py new file mode 100644 index 0000000000..92066eadc2 --- /dev/null +++ b/pyamplicol/src/pyamplicol/models/external_evaluation.py @@ -0,0 +1,673 @@ +# SPDX-License-Identifier: 0BSD +from __future__ import annotations + +from collections.abc import Mapping, Sequence +from typing import TYPE_CHECKING, Any + +from .._internal.physics.symbols import symbols +from ._physics_ir import PropagatorGauge, PropagatorIR, PropagatorKind +from .base import ( + PropagatorLoweringRule, +) +from .compiler_kernels import ( + _as_expression, + _ordered_dense_tensor_components, + _spin_axis_labels, + _spin_representations, + _spin_slots, +) +from .compiler_records import _replace_evaluator_constants +from .expressions import ( + _expr_antifermion_propagator_dirac, + _expr_antifermion_propagator_weyl, + _expr_fermion_propagator_dirac, + _expr_fermion_propagator_weyl, + _expr_minkowski_dot, + _imaginary_unit_for, + _minkowski_square_expression, +) +from .tensors import ( + normalize_lorentz_expression, +) + +if TYPE_CHECKING: + pass + +from . import compiler_symbolica as _sym +from .external_helpers import _expr_spin2_propagator, _is_numeric, _replace_symbols + + +class ExternalModelEvaluationMixin: + def vertex_component_expression( + self, + kind: int, + left: Sequence[Any], + right: Sequence[Any], + *, + result_particle_id: int, + result_chirality: int, + left_chirality: int = 0, + right_chirality: int = 0, + coupling: tuple[Any, Any] = (1.0, 0.0), + left_momentum: Sequence[Any] | None = None, + right_momentum: Sequence[Any] | None = None, + ) -> tuple[Any, ...]: + del coupling + kernel = self._kernel(kind) + expected_result = self._particle_records_by_name[kernel.particles[2]].pdg_code + if int(result_particle_id) != expected_result: + raise ValueError( + f"kernel {kind} returns particle {expected_result}, " + f"not {result_particle_id}" + ) + left_momentum = tuple(left_momentum or (0.0, 0.0, 0.0, 0.0)) + right_momentum = tuple(right_momentum or (0.0, 0.0, 0.0, 0.0)) + if len(left_momentum) != 4 or len(right_momentum) != 4: + raise ValueError("UFO kernels require four-component input momenta") + runtime_parameter_values = { + name: self._parameter_value(name) for name in kernel.runtime_parameters + } + if not all( + _is_numeric(value) + for value in ( + *left, + *right, + *left_momentum, + *right_momentum, + *runtime_parameter_values.values(), + ) + ): + return self._kernel_function_component_calls( + kernel, + left, + right, + left_chirality=left_chirality, + right_chirality=right_chirality, + result_chirality=result_chirality, + left_momentum=left_momentum, + right_momentum=right_momentum, + runtime_parameter_values=runtime_parameter_values, + ) + components = self._projected_kernel_components( + kernel, + left, + right, + left_chirality=left_chirality, + right_chirality=right_chirality, + result_chirality=result_chirality, + left_momentum=left_momentum, + right_momentum=right_momentum, + runtime_parameter_values=runtime_parameter_values, + ) + coupling_expression = self._resolved_kernel_coupling_expression( + kernel, + runtime_parameter_values, + numeric=True, + ) + return tuple(component * coupling_expression for component in components) + + def symbolica_function_definitions( + self, + ) -> Mapping[tuple[_sym.Expression, tuple[_sym.Expression, ...]], _sym.Expression]: + """Return lazily materialized kernel functions used by stage expressions.""" + + return self._symbolica_kernel_functions + + def propagator_lowering_rule( + self, + particle_id: int, + chirality: int = 0, + ) -> PropagatorLoweringRule: + auxiliary_kind = self.auxiliary_kind(particle_id) + if ( + auxiliary_kind is not None + and auxiliary_kind != "u1-subtraction-color-flow-vector" + ): + return PropagatorLoweringRule( + particle_id=particle_id, + chirality=0, + backend="identity", + full_tensor_network_ready=True, + applies_propagator=False, + kernel="ufo_contact_auxiliary_no_propagator", + kind="identity", + mass_class="not-applicable", + auxiliary_policy=auxiliary_kind, + description="synthetic UFO contact current with no propagator", + ) + if chirality != 0 and self.is_chiral_eligible(particle_id): + return PropagatorLoweringRule( + particle_id=particle_id, + chirality=chirality, + backend="spenso-ufo-weyl", + full_tensor_network_ready=True, + applies_propagator=True, + kernel="ufo_weyl_fermion", + kind="weyl-fermion", + mass_class="massless", + description="UFO massless fermion projected to a Weyl current", + ) + spin = self.spin(particle_id) + massive = ( + complex( + self._parameter_default( + self._particle_records_by_pdg[int(particle_id)].mass + ) + ).real + != 0.0 + ) + custom = self._propagator_record(particle_id) + if custom is not None and custom.custom: + return PropagatorLoweringRule( + particle_id=particle_id, + chirality=0, + backend="spenso-ufo-custom", + full_tensor_network_ready=True, + applies_propagator=True, + kernel="ufo_custom_propagator", + kind="custom", + mass_class="massive" if massive else "massless", + gauge="model-supplied", + numerator=custom.numerator, + denominator=custom.denominator, + custom_source=custom.name, + goldstone_policy="model-supplied", + description=( + "model-supplied UFO propagator lowered without gauge conversion" + ), + ) + standard_contracts: dict[ + int, + tuple[PropagatorKind, str, PropagatorGauge | None], + ] = { + 1: ("scalar", "ufo_scalar_propagator", None), + 2: ("dirac-fermion", "ufo_dirac_propagator", None), + 3: ( + "vector", + ( + "ufo_massive_vector_propagator" + if massive + else "ufo_massless_vector_propagator" + ), + "unitary" if massive else "feynman", + ), + 5: ( + "spin2", + ( + "ufo_massive_spin2_fierz_pauli" + if massive + else "ufo_massless_spin2_de_donder" + ), + "fierz-pauli" if massive else "de-donder", + ), + } + try: + kind, kernel, gauge = standard_contracts[spin] + except KeyError as exc: + raise ValueError( + f"UFO spin code {spin} has no supported propagator contract" + ) from exc + return PropagatorLoweringRule( + particle_id=particle_id, + chirality=0, + backend="symbolica-ufo", + full_tensor_network_ready=spin in {1, 2, 3, 5}, + applies_propagator=True, + kernel=kernel, + kind=kind, + mass_class="massive" if massive else "massless", + gauge=gauge, + goldstone_policy=( + "absorbed" if spin == 3 and massive else "not-applicable" + ), + description="UFO propagator with runtime mass and width", + ) + + def propagator_component_expression( + self, + particle_id: int, + value: Sequence[Any], + momentum: Sequence[Any], + *, + chirality: int = 0, + propagator: PropagatorIR | None = None, + ) -> tuple[Any, ...]: + metadata = propagator or self._propagator_ir(particle_id, chirality) + if metadata.identity.canonical_id != self._particle_identity_ir( + particle_id + ).canonical_id or metadata.chirality != int(chirality): + raise ValueError("propagator metadata does not match the current") + if not metadata.applies_propagator: + return tuple(value) + if metadata.kind == "weyl-fermion": + components = tuple(value) + current_momentum = tuple(momentum) + return ( + _expr_antifermion_propagator_weyl( + components, + current_momentum, + chirality, + ) + if metadata.identity.orientation == "antiparticle" + else _expr_fermion_propagator_weyl( + components, + current_momentum, + chirality, + ) + ) + if metadata.kind == "custom": + custom = self._propagator_record(particle_id) + if ( + custom is None + or not custom.custom + or custom.name != metadata.custom_source + ): + raise ValueError( + "custom propagator contract does not match its compiled source" + ) + return self._evaluate_custom_propagator( + particle_id, + value, + momentum, + ) + if len(momentum) != 4: + raise ValueError("UFO propagators require four-momentum components") + if metadata.mass_class == "massless": + mass = 0.0 + width = 0.0 + else: + mass = self.mass(particle_id) + width = self.width(particle_id) + imaginary = _imaginary_unit_for((*value, *momentum, mass, width)) + if metadata.kind == "scalar": + if len(value) != 1: + raise ValueError("scalar propagator expects one current component") + denominator = ( + _minkowski_square_expression(momentum) + - mass * mass + + imaginary * mass * width + ) + return (imaginary * value[0] / denominator,) + if metadata.kind == "dirac-fermion": + components = tuple(value) + current_momentum = tuple(momentum) + return ( + _expr_antifermion_propagator_dirac( + components, + current_momentum, + mass, + width, + ) + if metadata.identity.orientation == "antiparticle" + else _expr_fermion_propagator_dirac( + components, + current_momentum, + mass, + width, + ) + ) + if metadata.kind == "vector": + if len(value) != 4: + raise ValueError("vector propagator expects four current components") + current = tuple(value) + current_momentum = tuple(momentum) + if metadata.gauge == "feynman": + denominator = _minkowski_square_expression(current_momentum) + return tuple( + -imaginary * component / denominator for component in current + ) + if metadata.gauge != "unitary": + raise ValueError( + f"unsupported vector propagator gauge {metadata.gauge!r}" + ) + denominator = ( + _minkowski_square_expression(current_momentum) + - mass * mass + + imaginary * mass * width + ) + longitudinal = _expr_minkowski_dot(current, current_momentum) / ( + mass * mass + ) + return tuple( + -imaginary + * (current[index] - current_momentum[index] * longitudinal) + / denominator + for index in range(4) + ) + if metadata.kind == "spin2": + dimension = self._runtime_parameters.get("dim", 4.0) + return _expr_spin2_propagator( + tuple(value), + tuple(momentum), + mass, + width, + dimension=dimension, + massive=metadata.mass_class == "massive", + ) + raise NotImplementedError( + f"generic propagator kind {metadata.kind!r} is not implemented" + ) + + def runtime_parameter_names_for_vertex(self, kind: int) -> tuple[str, ...]: + return self._kernel(kind).runtime_parameters + + def runtime_derived_parameter_definitions(self) -> dict[str, str]: + return dict(self._runtime_derived_definitions) + + def runtime_derived_parameter_definitions_for( + self, + names: Sequence[str], + ) -> dict[str, str]: + return { + name: self._runtime_derived_definitions[name] + for name in names + if name in self._runtime_derived_definitions + } + + def runtime_derived_parameter_domains(self) -> dict[str, str]: + """Return phases that follow exactly from UFO parameter declarations.""" + + return self.runtime_derived_parameter_domains_for( + tuple(self._runtime_derived_definitions) + ) + + def runtime_derived_parameter_domains_for( + self, + names: Sequence[str], + ) -> dict[str, str]: + """Return provable phases for only the requested derived couplings.""" + + parameter_symbols = { + self._model_symbols.symbol(parameter.name): self._runtime_domain_symbol( + parameter.name, + self._runtime_parameter_domain(parameter.name), + ) + for parameter in self._parameter_records.values() + } + domains: dict[str, str] = {} + for name in names: + cached = self._runtime_derived_domain_cache.get(name) + if cached is not None: + domains[name] = cached + continue + if name not in self._runtime_derived_definitions: + continue + parameter = self._parameter_records.get(name) + if parameter is not None: + domain = self._runtime_parameter_domain(name) + self._runtime_derived_domain_cache[name] = domain + domains[name] = domain + continue + expression = self._model_symbols.expression( + self._runtime_derived_definitions[name] + ) + for source, replacement in parameter_symbols.items(): + expression = expression.replace(source, replacement) + if expression.is_real(): + domain = "real" + elif (-1j * expression).is_real(): + domain = "imaginary" + else: + domain = "complex" + self._runtime_derived_domain_cache[name] = domain + domains[name] = domain + return domains + + def _runtime_parameter_domain( + self, + name: str, + visiting: frozenset[str] = frozenset(), + ) -> str: + cached = self._runtime_parameter_domain_cache.get(name) + if cached is not None: + return cached + parameter = self._parameter_records[name] + if parameter.parameter_type.lower() == "real": + domain = "real" + elif parameter.nature.lower() != "internal" or name in visiting: + domain = "complex" + else: + expression = self._model_symbols.expression(parameter.resolved_expression) + symbols = set(expression.get_all_symbols(False)) + nested_visiting = visiting | {name} + for dependency in self._parameter_records.values(): + source = self._model_symbols.symbol(dependency.name) + if source not in symbols: + continue + replacement = self._runtime_domain_symbol( + dependency.name, + self._runtime_parameter_domain( + dependency.name, + nested_visiting, + ), + ) + expression = expression.replace(source, replacement) + if expression.is_real(): + domain = "real" + elif (-1j * expression).is_real(): + domain = "imaginary" + else: + domain = "complex" + self._runtime_parameter_domain_cache[name] = domain + return domain + + def _runtime_domain_symbol(self, name: str, domain: str): + domain_symbol = symbols.runtime_domain( + self.name, + name, + domain, + ) + return domain_symbol + + def auxiliary_kind(self, particle_id: int) -> str | None: + return self._particle_records_by_pdg[int(particle_id)].auxiliary_kind + + def runtime_derived_parameter_defaults(self) -> dict[str, complex]: + return self.runtime_derived_parameter_defaults_for( + tuple(self._runtime_derived_definitions) + ) + + def runtime_derived_parameter_defaults_for( + self, + names: Sequence[str], + ) -> dict[str, complex]: + substitutions: dict[_sym.Expression, Any] | None = None + defaults: dict[str, complex] = {} + for name in names: + cached = self._runtime_derived_default_cache.get(name) + if cached is not None: + defaults[name] = cached + continue + expression = self._runtime_derived_definitions.get(name) + if expression is None: + continue + template = self._runtime_derived_expression_cache.get(name) + if template is None: + template = self._model_symbols.expression(expression) + self._runtime_derived_expression_cache[name] = template + if substitutions is None: + substitutions = { + self._model_symbols.symbol(parameter_name): value + for parameter_name, value in self._runtime_parameters.items() + } + try: + value = complex(template.evaluate(substitutions)) + except Exception: + value = complex(_replace_symbols(template, substitutions)) + self._runtime_derived_default_cache[name] = value + defaults[name] = value + return defaults + + def runtime_parameter_names_for_particle(self, pdg: int) -> tuple[str, ...]: + particle = self._particle_records_by_pdg[int(pdg)] + custom = self._propagator_record(pdg) + if custom is not None and custom.custom: + _expression, names = self._custom_propagator_expression(int(pdg)) + return names + return tuple( + name + for name in (particle.mass, particle.width) + if name.upper() != "ZERO" + and ( + name in self.compiled.parameter_defaults + or name in self._runtime_derived_definitions + ) + ) + + def _custom_propagator_expression( + self, + particle_id: int, + ) -> tuple[_sym.Expression, tuple[str, ...]]: + cached = self._custom_propagator_expressions.get(int(particle_id)) + if cached is not None: + return cached + record = self._propagator_record(particle_id) + if record is None or not record.custom: + raise ValueError(f"particle {particle_id} has no custom UFO propagator") + spin = self.spin(particle_id) + normalized = normalize_lorentz_expression( + f"({record.numerator})/({record.denominator})", + (spin, spin), + model_symbols=self._model_symbols, + vertex=False, + ) + expression = self._model_symbols.expression(normalized.expression) + for parameter in self._parameter_records.values(): + if parameter.nature == "external": + continue + expression = expression.replace( + self._model_symbols.symbol(parameter.name), + self._model_symbols.expression(parameter.resolved_expression), + ) + expression = _replace_evaluator_constants(expression) + symbols = set(expression.get_all_symbols(False)) + names = tuple( + sorted( + parameter.name + for parameter in self._parameter_records.values() + if parameter.nature == "external" + and self._model_symbols.symbol(parameter.name) in symbols + ) + ) + result = expression, names + self._custom_propagator_expressions[int(particle_id)] = result + return result + + def _custom_propagator_template( + self, + particle_id: int, + ) -> tuple[_sym.Expression, ...]: + cached = self._custom_propagator_templates.get(int(particle_id)) + if cached is not None: + return cached + expression, _names = self._custom_propagator_expression(particle_id) + spin = self.spin(particle_id) + components = tuple( + symbols.custom_propagator_component(self.name, "input", index) + for index in range(self.dimension(particle_id)) + ) + momenta = tuple( + symbols.custom_propagator_component(self.name, "momentum", index) + for index in range(4) + ) + library = _sym.TensorLibrary.hep_lib_atom() + representations = _spin_representations(spin) + if representations: + name = _sym.TensorName(symbols.custom_propagator_tensor_name(self.name)) + library.register(_sym.Tensor.dense(name(*representations), components)) + expression *= name(*_spin_slots(spin, 1)).to_expression() + else: + expression *= components[0] + minkowski = _sym.Representation.mink(4) + for leg in (1, 2): + library.register( + _sym.Tensor.dense( + _sym.TensorName(self._model_symbols.ufo_momentum_tensor_name(leg))( + minkowski + ), + momenta, + ) + ) + axis_labels = _spin_axis_labels(spin, 2) + network = _sym.TensorNetwork( + _sym.as_tensor(expression) if axis_labels else expression, library + ) + network.execute(library=library) + tensor = network.result_tensor(library) + tensor_components = _ordered_dense_tensor_components( + tensor, + axis_labels, + ).values + expected = self.dimension(particle_id) + if len(tensor_components) != expected: + raise ValueError( + f"custom propagator {particle_id} produced {len(tensor_components)} " + f"components, expected {expected}" + ) + template = tuple( + _replace_evaluator_constants(_as_expression(component)) + for component in tensor_components + ) + self._custom_propagator_templates[int(particle_id)] = template + return template + + def _evaluate_custom_propagator( + self, + particle_id: int, + value: Sequence[Any], + momentum: Sequence[Any], + ) -> tuple[Any, ...]: + if len(value) != self.dimension(particle_id): + raise ValueError( + f"custom propagator for {particle_id} expects " + f"{self.dimension(particle_id)} current components" + ) + if len(momentum) != 4: + raise ValueError("UFO custom propagators require four-momentum components") + substitutions = { + **{ + symbols.custom_propagator_component( + self.name, + "input", + index, + ): component + for index, component in enumerate(value) + }, + **{ + symbols.custom_propagator_component( + self.name, + "momentum", + index, + ): component + for index, component in enumerate(momentum) + }, + **{ + self._model_symbols.symbol(name): self._parameter_value(name) + for name in self._custom_propagator_expression(particle_id)[1] + }, + } + return tuple( + _replace_symbols(component, substitutions) + for component in self._custom_propagator_template(particle_id) + ) + + def runtime_mass_parameter_name(self, pdg: int) -> str | None: + name = self._particle_records_by_pdg[int(pdg)].mass + return ( + name + if name in self.compiled.parameter_defaults + or name in self._runtime_derived_definitions + else None + ) + + def runtime_width_parameter_name(self, pdg: int) -> str | None: + name = self._particle_records_by_pdg[int(pdg)].width + return ( + name + if name in self.compiled.parameter_defaults + or name in self._runtime_derived_definitions + else None + ) + + def runtime_parameter_defaults(self) -> dict[str, tuple[float, float]]: + return dict(self.compiled.parameter_defaults) diff --git a/pyamplicol/src/pyamplicol/models/external_helpers.py b/pyamplicol/src/pyamplicol/models/external_helpers.py new file mode 100644 index 0000000000..7157fa71c5 --- /dev/null +++ b/pyamplicol/src/pyamplicol/models/external_helpers.py @@ -0,0 +1,151 @@ +# SPDX-License-Identifier: 0BSD +from __future__ import annotations + +from collections.abc import Mapping, Sequence +from numbers import Number +from typing import TYPE_CHECKING, Any + +from .contracts import ( + CompiledParameterRecord, +) +from .expressions import ( + _exact_complex_expression, + _imaginary_unit_for, + _minkowski_square_expression, +) + +if TYPE_CHECKING: + pass + +from . import compiler_symbolica as _sym + + +def _record_default(record: CompiledParameterRecord) -> complex: + if record.value is not None: + return complex(record.value[0], record.value[1]) + return complex(_sym.E(record.resolved_expression).evaluate({})) + + +def _spin_dimension(spin: int) -> int: + try: + return {-1: 1, 1: 1, 2: 4, 3: 4, 5: 16}[int(spin)] + except KeyError as exc: + raise ValueError(f"unsupported UFO spin code {spin}") from exc + + +def _chirality_tag(chirality: int) -> str: + value = int(chirality) + if value < 0: + return f"m{abs(value)}" + if value > 0: + return f"p{value}" + return "z" + + +def _replace_symbols( + expression: _sym.Expression, substitutions: Mapping[_sym.Expression, Any] +) -> Any: + symbols = set(expression.get_all_symbols(False)) + replacements = [ + _sym.Replacement(symbol, value) + for symbol, value in substitutions.items() + if symbol in symbols + ] + if not replacements: + return expression + return expression.replace_multiple(replacements) + + +def _is_numeric(value: Any) -> bool: + return isinstance(value, Number) + + +def _is_zero(value: Any) -> bool: + if isinstance(value, int | float | complex): + return value == 0 + return isinstance(value, _sym.Expression) and value.to_canonical_string() == "0" + + +def _expr_spin2_propagator( + value: Sequence[Any], + momentum: Sequence[Any], + mass: Any, + width: Any, + *, + dimension: Any, + massive: bool, +) -> tuple[Any, ...]: + if len(value) != 16: + raise ValueError("spin-2 propagator expects sixteen current components") + if len(momentum) != 4: + raise ValueError("spin-2 propagator expects four momentum components") + tensor = tuple(tuple(value[4 * mu + nu] for nu in range(4)) for mu in range(4)) + metric = (1, -1, -1, -1) + imaginary = _imaginary_unit_for((*value, *momentum, mass, width, dimension)) + one = 1 if isinstance(imaginary, complex) else _exact_complex_expression(1 + 0j) + if not isinstance(imaginary, complex) and isinstance(dimension, float | complex): + dimension = _exact_complex_expression(complex(dimension)) + half = one / 2 + denominator = ( + _minkowski_square_expression(momentum) - mass * mass + imaginary * mass * width + ) + if not massive: + trace = sum( + (metric[index] * tensor[index][index] for index in range(4)), + 0, + ) + trace_weight = one / (dimension - 2) + projected = tuple( + half * (tensor[mu][nu] + tensor[nu][mu]) + - (metric[mu] * trace * trace_weight if mu == nu else 0) + for mu in range(4) + for nu in range(4) + ) + return tuple(imaginary * component / denominator for component in projected) + + mass_squared = mass * mass + first_projected = tuple( + tuple( + tensor[mu][nu] + - momentum[mu] + * sum( + ( + metric[alpha] * momentum[alpha] * tensor[alpha][nu] + for alpha in range(4) + ), + 0, + ) + / mass_squared + for nu in range(4) + ) + for mu in range(4) + ) + transverse = tuple( + tuple( + first_projected[mu][nu] + - momentum[nu] + * sum( + ( + metric[beta] * momentum[beta] * first_projected[mu][beta] + for beta in range(4) + ), + 0, + ) + / mass_squared + for nu in range(4) + ) + for mu in range(4) + ) + transverse_trace = sum( + (metric[index] * transverse[index][index] for index in range(4)), + 0, + ) + projected = tuple( + half * (transverse[mu][nu] + transverse[nu][mu]) + - ((metric[mu] if mu == nu else 0) - momentum[mu] * momentum[nu] / mass_squared) + * transverse_trace + / 3 + for mu in range(4) + for nu in range(4) + ) + return tuple(imaginary * component / denominator for component in projected) diff --git a/pyamplicol/src/pyamplicol/models/external_kernels.py b/pyamplicol/src/pyamplicol/models/external_kernels.py new file mode 100644 index 0000000000..be93d6b58c --- /dev/null +++ b/pyamplicol/src/pyamplicol/models/external_kernels.py @@ -0,0 +1,444 @@ +# SPDX-License-Identifier: 0BSD +from __future__ import annotations + +import math +from collections import Counter +from collections.abc import Mapping, Sequence +from typing import TYPE_CHECKING, Any + +from .._internal.physics.symbols import symbols +from .contracts import ( + CompiledOrientedKernel, +) + +if TYPE_CHECKING: + pass + +from . import compiler_symbolica as _sym +from .external_helpers import ( + _chirality_tag, + _is_numeric, + _is_zero, + _record_default, + _replace_symbols, +) + + +class ExternalModelKernelMixin: + def runtime_parameter_type(self, name: str) -> str: + try: + return self._parameter_records[str(name)].parameter_type + except KeyError as exc: + raise KeyError(f"unknown runtime model parameter {name!r}") from exc + + def runtime_normalization_payload(self, dag: Any) -> dict[str, object]: + initial_pdgs = tuple(int(pdg) for pdg in dag.process.initial_pdgs) + final_pdgs = tuple(int(pdg) for pdg in dag.process.final_pdgs) + average_factor = 1 + for pdg in initial_pdgs: + average_factor *= len(self.source_spin_states(pdg)) + average_factor *= max(1, abs(self.color_rep(pdg))) + identical_factor = math.prod( + math.factorial(count) for count in Counter(final_pdgs).values() + ) + color_factor = self._external_color_normalization_factor( + (*initial_pdgs, *final_pdgs) + ) + return { + "color_accuracy": dag.process.color_accuracy, + "color_factor": float(color_factor), + "average_factor": float(average_factor), + "identical_factor": float(identical_factor), + "global_coupling_factor": 1.0, + "couplings_in_stage_evaluators": True, + "coupling_policy": ( + "UFO coupling expressions are fully included in generated stage " + "evaluators; no built-in-SM global coupling factor is applied" + ), + } + + def _external_color_normalization_factor(self, process: Sequence[int]) -> int: + exponent_twice = 0 + for pdg in process: + representation = abs(self.color_rep(int(pdg))) + if representation == 8: + exponent_twice += 2 + elif representation == 3: + exponent_twice += 1 + elif representation != 1: + raise ValueError( + f"unsupported leading-color representation {representation}" + ) + if exponent_twice % 2: + raise ValueError(f"non-integer leading-color exponent for {tuple(process)}") + return 3 ** (exponent_twice // 2) + + def _kernel(self, kind: int) -> CompiledOrientedKernel: + try: + return self._kernels[int(kind)] + except KeyError as exc: + raise KeyError(f"unknown compiled UFO kernel kind {kind}") from exc + + def _weyl_projection_is_nonzero( + self, + kind: int, + left_chirality: int, + right_chirality: int, + result_chirality: int, + ) -> bool: + key = (kind, left_chirality, right_chirality, result_chirality) + cached = self._weyl_projection_support.get(key) + if cached is not None: + return cached + kernel = self._kernel(kind) + left_pdg = self._particle_records_by_name[kernel.particles[0]].pdg_code + right_pdg = self._particle_records_by_name[kernel.particles[1]].pdg_code + left = tuple( + symbols.weyl_probe(self.name, kind, "left", "component", index) + for index in range(self.current_dimension(left_pdg, left_chirality)) + ) + right = tuple( + symbols.weyl_probe(self.name, kind, "right", "component", index) + for index in range(self.current_dimension(right_pdg, right_chirality)) + ) + components = self._projected_kernel_components( + kernel, + left, + right, + left_chirality=left_chirality, + right_chirality=right_chirality, + result_chirality=result_chirality, + left_momentum=tuple( + symbols.weyl_probe( + self.name, + kind, + "left", + "momentum", + index, + ) + for index in range(4) + ), + right_momentum=tuple( + symbols.weyl_probe( + self.name, + kind, + "right", + "momentum", + index, + ) + for index in range(4) + ), + ) + supported = any(not _is_zero(component) for component in components) + self._weyl_projection_support[key] = supported + return supported + + def _projected_kernel_components( + self, + kernel: CompiledOrientedKernel, + left: Sequence[Any], + right: Sequence[Any], + *, + left_chirality: int, + right_chirality: int, + result_chirality: int, + left_momentum: Sequence[Any], + right_momentum: Sequence[Any], + runtime_parameter_values: Mapping[str, Any] | None = None, + ) -> tuple[Any, ...]: + left_pdg = self._particle_records_by_name[kernel.particles[0]].pdg_code + right_pdg = self._particle_records_by_name[kernel.particles[1]].pdg_code + result_pdg = self._particle_records_by_name[kernel.particles[2]].pdg_code + full_left = self._embed_weyl_current(left_pdg, left_chirality, left) + full_right = self._embed_weyl_current(right_pdg, right_chirality, right) + parameter_values = ( + {name: self._parameter_value(name) for name in kernel.runtime_parameters} + if runtime_parameter_values is None + else dict(runtime_parameter_values) + ) + substitutions: dict[_sym.Expression, Any] = {} + for index, value in enumerate(full_left): + substitutions[ + self._model_symbols.kernel_component(kernel.kind, "left", index) + ] = value + for index, value in enumerate(full_right): + substitutions[ + self._model_symbols.kernel_component(kernel.kind, "right", index) + ] = value + for index, value in enumerate(left_momentum): + substitutions[ + self._model_symbols.kernel_momentum(kernel.kind, "left", index) + ] = value + for index, value in enumerate(right_momentum): + substitutions[ + self._model_symbols.kernel_momentum(kernel.kind, "right", index) + ] = value + for name, value in parameter_values.items(): + substitutions[symbols.runtime_model_parameter(self.name, name)] = value + templates = self._kernel_component_expressions(kernel) + if all(_is_numeric(value) for value in substitutions.values()): + try: + components = tuple( + complex(template.evaluate(substitutions)) for template in templates + ) + except Exception: + components = tuple( + _replace_symbols(template, substitutions) for template in templates + ) + else: + components = tuple( + _replace_symbols(template, substitutions) for template in templates + ) + if not self.is_chiral_eligible(result_pdg): + return components + if result_chirality == 0: + return components + if result_chirality == 1: + return components[2:4] + if result_chirality == -1: + return components[0:2] + raise ValueError("a projected Weyl result requires chirality -1, 0, or +1") + + def _kernel_function_component_calls( + self, + kernel: CompiledOrientedKernel, + left: Sequence[Any], + right: Sequence[Any], + *, + left_chirality: int, + right_chirality: int, + result_chirality: int, + left_momentum: Sequence[Any], + right_momentum: Sequence[Any], + runtime_parameter_values: Mapping[str, Any], + ) -> tuple[Any, ...]: + key = ( + int(kernel.kind), + int(left_chirality), + int(right_chirality), + int(result_chirality), + ) + function_specs = self._kernel_function_specs.get(key) + if function_specs is None: + function_specs = self._define_kernel_component_functions( + kernel, + left_dimension=len(left), + right_dimension=len(right), + left_chirality=left_chirality, + right_chirality=right_chirality, + result_chirality=result_chirality, + ) + self._kernel_function_specs[key] = function_specs + arguments = ( + *left, + *right, + *left_momentum, + *right_momentum, + *(runtime_parameter_values[name] for name in kernel.runtime_parameters), + ) + return tuple( + function(*(arguments[index] for index in argument_indices)) + for function, argument_indices in function_specs + ) + + def _define_kernel_component_functions( + self, + kernel: CompiledOrientedKernel, + *, + left_dimension: int, + right_dimension: int, + left_chirality: int, + right_chirality: int, + result_chirality: int, + ) -> tuple[tuple[_sym.Expression, tuple[int, ...]], ...]: + key_tag = "_".join( + ( + str(int(kernel.kind)), + _chirality_tag(left_chirality), + _chirality_tag(right_chirality), + _chirality_tag(result_chirality), + ) + ) + formal_left = tuple( + symbols.kernel_function_argument( + self.name, + key_tag, + "left", + index, + ) + for index in range(left_dimension) + ) + formal_right = tuple( + symbols.kernel_function_argument( + self.name, + key_tag, + "right", + index, + ) + for index in range(right_dimension) + ) + formal_left_momentum = tuple( + symbols.kernel_function_argument( + self.name, + key_tag, + "left_momentum", + index, + ) + for index in range(4) + ) + formal_right_momentum = tuple( + symbols.kernel_function_argument( + self.name, + key_tag, + "right_momentum", + index, + ) + for index in range(4) + ) + formal_parameters = { + name: symbols.kernel_function_argument( + self.name, + key_tag, + "parameter", + index, + ) + for index, name in enumerate(kernel.runtime_parameters) + } + formal_arguments = ( + *formal_left, + *formal_right, + *formal_left_momentum, + *formal_right_momentum, + *(formal_parameters[name] for name in kernel.runtime_parameters), + ) + components = self._projected_kernel_components( + kernel, + formal_left, + formal_right, + left_chirality=left_chirality, + right_chirality=right_chirality, + result_chirality=result_chirality, + left_momentum=formal_left_momentum, + right_momentum=formal_right_momentum, + runtime_parameter_values=formal_parameters, + ) + coupling_expression = self._resolved_kernel_coupling_expression( + kernel, + formal_parameters, + ) + bodies = tuple(component * coupling_expression for component in components) + functions = tuple( + symbols.kernel_function_component(self.name, key_tag, index) + for index in range(len(bodies)) + ) + specs: list[tuple[_sym.Expression, tuple[int, ...]]] = [] + for function, body in zip(functions, bodies, strict=True): + body_symbols = set(body.get_all_symbols(False)) + argument_indices = tuple( + index + for index, argument in enumerate(formal_arguments) + if argument in body_symbols + ) + used_arguments = tuple( + formal_arguments[index] for index in argument_indices + ) + self._symbolica_kernel_functions[(function, used_arguments)] = body + specs.append((function, argument_indices)) + return tuple(specs) + + def _kernel_component_expressions( + self, + kernel: CompiledOrientedKernel, + ) -> tuple[_sym.Expression, ...]: + cached = self._kernel_component_expression_cache.get(kernel.kind) + if cached is None: + cached = tuple( + _sym.E(component) for component in kernel.component_expressions + ) + self._kernel_component_expression_cache[kernel.kind] = cached + return cached + + def _resolved_kernel_coupling_expression( + self, + kernel: CompiledOrientedKernel, + runtime_parameter_values: Mapping[str, Any], + *, + numeric: bool = False, + ) -> Any: + template = self._kernel_coupling_expression_cache.get(kernel.kind) + if template is None: + template = _sym.E(kernel.coupling_expression) + # Keep the colour-flow normalization algebraic until evaluation. + # Putting sqrt(2) in a binary64 recurrence weight would freeze its + # rounding error even when evaluating the process at high precision. + power = kernel.lc_color_normalization_power + if power: + template *= _sym.E(f"2^(-{power}/2)") + self._kernel_coupling_expression_cache[kernel.kind] = template + substitutions = { + symbols.runtime_model_parameter(self.name, name): value + for name, value in runtime_parameter_values.items() + } + if numeric and all(_is_numeric(value) for value in substitutions.values()): + try: + return complex(template.evaluate(substitutions)) + except Exception: + pass + return _replace_symbols(template, substitutions) + + def _embed_weyl_current( + self, + particle_id: int, + chirality: int, + values: Sequence[Any], + ) -> tuple[Any, ...]: + components = tuple(values) + if not self.is_chiral_eligible(particle_id): + return components + if chirality == 0: + if len(components) != 4: + raise ValueError("a full Dirac input must have four components") + return components + if len(components) != 2: + raise ValueError("a projected Weyl input must have two components") + if chirality == 1: + return (components[0], components[1], 0.0, 0.0) + if chirality == -1: + return (0.0, 0.0, components[0], components[1]) + raise ValueError("a projected Weyl input requires chirality -1, 0, or +1") + + def _propagator_record(self, particle_id: int): + name = self._particle_records_by_pdg[int(particle_id)].name + return self._propagator_records_by_particle_name.get(name) + + def _parameter_default(self, name: str) -> complex: + if name.upper() == "ZERO": + return 0.0 + 0.0j + record = self._parameter_records.get(name) + if record is None: + raise ValueError(f"model parameter {name!r} is not defined") + return _record_default(record) + + def _parameter_value(self, name: str) -> Any: + if name in self._runtime_parameters: + return self._runtime_parameters[name] + definition = self._runtime_derived_definitions.get(name) + if definition is not None: + return _replace_symbols( + _sym.E(definition), + { + self._model_symbols.symbol(parameter_name): value + for parameter_name, value in self._runtime_parameters.items() + if not parameter_name.startswith("derived_coupling_") + }, + ) + return self._parameter_default(name) + + def _real_parameter_value(self, name: str, *, field: str) -> Any: + value = self._parameter_value(name) + if isinstance(value, int | float | complex): + numeric = complex(value) + if numeric.imag != 0.0: + raise ValueError(f"particle {field} parameter {name!r} is not real") + return numeric.real + return value diff --git a/pyamplicol/src/pyamplicol/models/external_symmetries.py b/pyamplicol/src/pyamplicol/models/external_symmetries.py new file mode 100644 index 0000000000..1eaf90181a --- /dev/null +++ b/pyamplicol/src/pyamplicol/models/external_symmetries.py @@ -0,0 +1,876 @@ +# SPDX-License-Identifier: 0BSD +"""Algebraic symmetry certificates for compiled external models. + +The certificates in this module are deliberately derived from normalized +Lorentz/color tensors and exact coupling relations. UFO particle names, +vertex names, PDG assignments, and coupling-order labels are not proof input. +""" + +from __future__ import annotations + +import hashlib +import json +import re +from collections import defaultdict +from dataclasses import dataclass + +from .._internal.physics.symbols import ModelSymbolRegistry, symbols +from . import compiler_symbolica as _sym +from .contracts import ( + CompiledCouplingRecord, + CompiledModelIR, + CompiledOrientedKernel, + CompiledParameterRecord, + CompiledParticleRecord, + CompiledPropagatorRecord, + CompiledVertexTerm, +) +from .tensors import ( + _constant_expression_ratio, + classify_trilinear_color_expression, + normalize_color_expression, + normalize_lorentz_expression, +) + +_YM_THREE_VECTOR = ( + "-UFO::Metric(UFO::idx(1,1),UFO::idx(1,2))" + "*UFO::P(UFO::idx(1,3),UFO::idx(1,2))" + "-UFO::Metric(UFO::idx(1,1),UFO::idx(1,3))" + "*UFO::P(UFO::idx(1,2),UFO::idx(1,1))" + "-UFO::Metric(UFO::idx(1,2),UFO::idx(1,3))" + "*UFO::P(UFO::idx(1,1),UFO::idx(1,3))" + "+UFO::Metric(UFO::idx(1,1),UFO::idx(1,2))" + "*UFO::P(UFO::idx(1,3),UFO::idx(1,1))" + "+UFO::Metric(UFO::idx(1,1),UFO::idx(1,3))" + "*UFO::P(UFO::idx(1,2),UFO::idx(1,3))" + "+UFO::Metric(UFO::idx(1,2),UFO::idx(1,3))" + "*UFO::P(UFO::idx(1,1),UFO::idx(1,2))" +) + +_YM_FOUR_VECTOR_BASIS = ( + ( + "UFO::f(-1,1,2)*UFO::f(3,4,-1)", + "-UFO::Metric(UFO::idx(1,1),UFO::idx(1,3))" + "*UFO::Metric(UFO::idx(1,2),UFO::idx(1,4))" + "+UFO::Metric(UFO::idx(1,1),UFO::idx(1,4))" + "*UFO::Metric(UFO::idx(1,2),UFO::idx(1,3))", + ), + ( + "UFO::f(-1,1,3)*UFO::f(2,4,-1)", + "-UFO::Metric(UFO::idx(1,1),UFO::idx(1,2))" + "*UFO::Metric(UFO::idx(1,3),UFO::idx(1,4))" + "+UFO::Metric(UFO::idx(1,1),UFO::idx(1,4))" + "*UFO::Metric(UFO::idx(1,2),UFO::idx(1,3))", + ), + ( + "UFO::f(-1,1,4)*UFO::f(2,3,-1)", + "-UFO::Metric(UFO::idx(1,1),UFO::idx(1,2))" + "*UFO::Metric(UFO::idx(1,3),UFO::idx(1,4))" + "+UFO::Metric(UFO::idx(1,1),UFO::idx(1,3))" + "*UFO::Metric(UFO::idx(1,2),UFO::idx(1,4))", + ), +) + +_CONTACT_ORIGIN = re.compile(r"^ufo-contact:(?P[0-9]+):") +_CONTRACTED_INDEX = re.compile( + r"^ufo_(?P[cls])_dummy_(?P